DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Terraform Tutorial

Introduction to Terraform
Installing Terraform
The Terraform Core Workflow
Terraform Variables and Outputs
Terraform State and Backends
Terraform Modules
Terraform Data Sources
Terraform Count and for_each
Terraform Expressions and Functions
Terraform Dynamic Blocks
Terraform Workspaces
Terraform with AWS
Terraform Provisioners
Terraform CI/CD Pipeline
Terraform Security

Terraform Local Values

PreviousPrev
Next

Learn how Terraform local values work, when to use locals instead of variables or data sources, and how to simplify repetitive expressions in your infrastructure code.

What Are Local Values in Terraform?

Terraform local values, usually just called locals, are named expressions defined inside a locals block. They let you store a calculated value once and then reuse it throughout your configuration.

If input variables are the values that come into your Terraform code, local values are the values you compute and organize inside it.

A basic example looks like this:

variable "environment" {
  type = string
}

variable "project_name" {
  type = string
}

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
}

Here, local.name_prefix saves you from repeating the same string construction everywhere.

Locals do not accept input from the user. They are not part of the public interface of a module. They are strictly an internal convenience that improves readability and removes duplication.

Why Local Values Matter

Beginners sometimes skip locals and write everything inline. Terraform allows that, but the configuration becomes noisy very quickly.

For example:

resource "aws_cloudwatch_log_group" "app" {
  name = "/aws/${var.project_name}/${var.environment}/app"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${var.project_name}-${var.environment}-logs"
}

resource "aws_iam_role" "app" {
  name = "${var.project_name}-${var.environment}-app-role"
}

The repeated project_name + environment pattern appears everywhere. If your naming convention changes, you have to update many places.

With locals:

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_cloudwatch_log_group" "app" {
  name = "/aws/${local.name_prefix}/app"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
}

resource "aws_iam_role" "app" {
  name = "${local.name_prefix}-app-role"
}

This is easier to read and easier to change.

Defining Locals with the locals Block

A locals block can define one or many local values:

locals {
  environment_upper = upper(var.environment)
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
  enable_backups = var.environment == "prod"
}

You reference them with local.<name>:

tags = local.common_tags

Note the syntax carefully:

  • var.name for input variables
  • local.name for local values
  • module.name.output_name for module outputs

When to Use Locals vs Variables vs Data Sources

This is one of the most important conceptual distinctions in Terraform.

Use a variable when the value should come from outside

Variables are for values that should be supplied by:

  • the person running Terraform
  • a parent module
  • a .tfvars file
  • a CI/CD pipeline

Example:

variable "region" {
  type = string
}

The region is a decision the caller should make, so it belongs in a variable.

Use a local when the value is internal logic

Locals are for values derived from variables, resources, or expressions inside the module.

Example:

locals {
  bucket_name = "${var.project_name}-${var.environment}-logs"
}

This does not need to be exposed to the caller. It is implementation detail.

Use a data source when Terraform must query an external system

Data sources fetch existing information from a provider.

Example:

data "aws_availability_zones" "available" {}

This value comes from AWS, not from the user and not from pure local computation.

Quick decision guide

Ask this question:

  • Should a caller choose this value? Use a variable.
  • Can Terraform calculate this value from what it already knows? Use a local.
  • Must Terraform ask a provider for this value? Use a data source.

Understanding this boundary keeps your module interfaces clean.

Using Expressions in Locals

Local values can contain almost any Terraform expression. This makes them very powerful.

String expressions

locals {
  name_prefix = "${var.project_name}-${var.environment}"
  log_group   = "/aws/${var.project_name}/${var.environment}/app"
}

Arithmetic expressions

locals {
  desired_capacity = var.environment == "prod" ? 4 : 2
  total_disk_size  = var.instance_count * var.disk_size_gb
}

Map and list expressions

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  private_subnet_names = [
    for index, az in var.availability_zones :
    "${var.project_name}-${var.environment}-private-${index + 1}"
  ]
}

Referencing resources

Locals can use resource attributes too:

locals {
  alb_dns_name = aws_lb.app.dns_name
}

This is valid, but use it thoughtfully. If the value is only used once, creating a local may add unnecessary indirection.

Using Functions in Locals

Terraform includes many built-in functions, and locals are a great place to use them.

merge()

A classic pattern is combining default tags with user-supplied tags.

variable "tags" {
  type    = map(string)
  default = {}
}

locals {
  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    },
    var.tags
  )
}

Now every resource can reuse local.common_tags.

lower(), upper(), replace()

Useful for naming standards:

locals {
  normalized_project = replace(lower(var.project_name), "_", "-")
  name_prefix        = "${local.normalized_project}-${var.environment}"
}

concat() and distinct()

Useful for combining lists:

locals {
  all_cidrs = distinct(concat(var.office_cidrs, var.vpn_cidrs))
}

lookup()

A common way to choose environment-specific values:

locals {
  instance_size = lookup(
    {
      dev     = "t3.micro"
      staging = "t3.small"
      prod    = "t3.medium"
    },
    var.environment,
    "t3.micro"
  )
}

jsonencode()

Great for IAM policies or ECS task definitions:

locals {
  bucket_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject"]
        Resource = "${aws_s3_bucket.assets.arn}/*"
      }
    ]
  })
}

This keeps complex JSON generation readable.

Using Conditionals in Locals

Locals often centralize environment-specific decisions.

locals {
  instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
  backup_days   = var.environment == "prod" ? 30 : 7
  multi_az      = var.environment == "prod" ? true : false
}

Now you express the rule once and reuse it everywhere.

You can also make conditionals more expressive by grouping them in locals rather than scattering ternary operators across resource blocks.

Common Patterns for Local Values

1. Common tags

This is probably the most common real-world use of locals.

locals {
  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
      Owner       = "platform-team"
    },
    var.extra_tags
  )
}

Then:

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.instance_type
  tags          = local.common_tags
}

This keeps tags consistent across your infrastructure.

2. Name prefixes

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

Then:

resource "aws_security_group" "app" {
  name = "${local.name_prefix}-sg"
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "${local.name_prefix}-artifacts"
}

If the naming convention changes later, you update one local value instead of many resources.

3. Environment-specific values

locals {
  desired_count = {
    dev     = 1
    staging = 2
    prod    = 4
  }[var.environment]
}

This is cleaner than repeating the same environment branching logic in multiple places.

4. Standardized subnet names

locals {
  public_subnet_names = [
    for index, az in var.availability_zones :
    "${var.project_name}-${var.environment}-public-${index + 1}"
  ]
}

5. Feature flags derived from environment

locals {
  enable_waf        = var.environment == "prod"
  enable_monitoring = contains(["staging", "prod"], var.environment)
}

This keeps business rules in one place.

A Complete Example

variable "project_name" {
  type = string
}

variable "environment" {
  type = string
}

variable "availability_zones" {
  type = list(string)
}

variable "extra_tags" {
  type    = map(string)
  default = {}
}

locals {
  normalized_project = replace(lower(var.project_name), "_", "-")
  name_prefix        = "${local.normalized_project}-${var.environment}"

  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    },
    var.extra_tags
  )

  instance_type = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.medium"
  }[var.environment]

  backup_retention_days = var.environment == "prod" ? 30 : 7
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
  tags   = local.common_tags
}

resource "aws_db_instance" "app" {
  allocated_storage      = 20
  engine                 = "postgres"
  instance_class         = local.instance_type == "t3.medium" ? "db.t3.medium" : "db.t3.micro"
  backup_retention_period = local.backup_retention_days
}

This demonstrates the main purpose of locals: moving repeated logic and naming rules into one understandable section.

Avoiding Overuse of Locals

Locals are helpful, but too many locals can make Terraform harder to read.

Bad pattern: creating locals for trivial values

locals {
  bucket_acl = "private"
}

If a value is only used once and adds no clarity, keeping it inline may be simpler.

Bad pattern: turning every resource attribute into a local

locals {
  s3_bucket_id = aws_s3_bucket.logs.id
}

If you use that ID once, just reference aws_s3_bucket.logs.id directly.

Bad pattern: hiding module inputs behind unnecessary locals

locals {
  region = var.region
}

This adds one more name without adding meaning.

Good rule of thumb

Use locals when they do at least one of these things:

  • remove repetition
  • clarify intent
  • group related logic
  • centralize naming or tagging rules
  • simplify complex expressions

If a local does none of those things, it may not be worth creating.

Best Practices for Terraform Locals

  1. Use clear names. name_prefix, common_tags, and backup_retention_days are much better than x or config1.
  2. Group related locals together. Keep naming, tagging, and environment-specific settings organized.
  3. Prefer locals for internal logic. Do not expose implementation details as variables unless callers truly need to control them.
  4. Do not over-abstract. Terraform is configuration, not a general-purpose programming language.
  5. Keep module interfaces small. Use locals to compute internal values from a smaller set of inputs.

Final Thoughts

Local values are one of the easiest ways to make Terraform code more readable. They help you avoid copy-paste, keep naming conventions consistent, and express environment-specific logic once instead of in ten different places.

A useful mental model is:

  • Variables are inputs chosen by the caller
  • Locals are internal calculations chosen by the module author
  • Data sources are external facts retrieved from the provider

If you apply that rule consistently, your Terraform code will be easier to understand and easier to maintain as it grows.


Knowledge Check

Exercise

Question 1: Local Syntax

How do you reference a Terraform local value named name_prefix?

Exercise

Question 2: Variables vs Locals

Which value is usually the best candidate for a local instead of an input variable?

Exercise

Question 3: Overusing Locals

Which example best shows unnecessary overuse of Terraform locals?

PreviousPrev
Next

Continue Learning

terraform destroy

Learn terraform destroy, safely tearing down infrastructure, using -target for partial destroys, understanding the destroy plan, and when to use destroy vs decommission. Includes terraform fmt and validate.

12 min read·Easy

Terraform Variables and Outputs

Learn how Terraform input variables, local values, and output values work together to make infrastructure code reusable, readable, and easier to share across environments.

5 min read·Easy

Terraform Input Variables

Learn Terraform input variables in depth, including variable blocks, types, defaults, validation rules, tfvars files, CLI flags, environment variables, and sensitive values.

12 min read·Easy

Explore Related Topics

AW

AWS Tutorials

Provision AWS infrastructure with Terraform

CI

CI/CD Tutorials

Run terraform plan/apply in CI pipelines

Try the Tool

YAML Validator

Validate Terraform and Kubernetes YAML configs before applying.

On This Page

What Are Local Values in Terraform?Why Local Values MatterDefining Locals with the `locals` BlockWhen to Use Locals vs Variables vs Data SourcesUse a variable when the value should come from outsideUse a local when the value is internal logicUse a data source when Terraform must query an external systemQuick decision guideUsing Expressions in LocalsString expressionsArithmetic expressionsMap and list expressionsReferencing resourcesUsing Functions in Locals`merge()``lower()`, `upper()`, `replace()``concat()` and `distinct()``lookup()``jsonencode()`Using Conditionals in LocalsCommon Patterns for Local Values1. Common tags2. Name prefixes3. Environment-specific values4. Standardized subnet names5. Feature flags derived from environmentA Complete ExampleAvoiding Overuse of LocalsBad pattern: creating locals for trivial valuesBad pattern: turning every resource attribute into a localBad pattern: hiding module inputs behind unnecessary localsGood rule of thumbBest Practices for Terraform LocalsFinal ThoughtsKnowledge Check