Use locals for cleaner expressions

Learn use locals for cleaner expressions in this Terraform foundations tutorial — hands-on steps, troubleshooting, and what to study next.

Focus: use locals for cleaner expressions

Sponsored

Your Terraform configuration reads like a cryptic math problem — var.environment == "production" ? var.instance_type : "t2.micro" repeated across every resource block. Every time you tweak a condition, you hunt through three files to find all the places it hides. This is the pain of inline expressions: repetition, drift, and brain-drain. Today, you'll learn how locals turn that mess into clean, named values that make your infrastructure not just functional, but readable.

The problem this lesson solves

Imagine you manage a web app in AWS. Your configuration has a module.webserver that needs the right instance type, a security group that needs the right CIDR block, and an autoscaling policy that needs the right threshold. Without locals, each of these might have its own inline expression:

resource "aws_instance" "app" {
  instance_type = var.environment == "production" ? "m5.large" : "t3.micro"
}

resource "aws_autoscaling_policy" "scale_up" {
  scaling_adjustment = var.environment == "production" ? 3 : 1
}

resource "aws_security_group" "app" {
  ingress {
    cidr_blocks = [var.environment == "production" ? "10.0.0.0/16" : "0.0.0.0/0"]
  }
}

What's wrong? Repetition — the same condition appears three times. If you change "production" to "prod", the logic diverges and you've created a bug. Poor readability — a reviewer can't instantly see the intent behind "m5.large". Maintenance cost — every new resource needs a copy-paste of the same fragile expression.

This lesson solves that by introducing locals — named values that let you define a value once and reuse it everywhere. By the end, you'll be able to refactor your code from cryptic conditionals to clean, self-documenting expressions.

Core concept / mental model

Think of locals as constants or variables within a single Terraform module. Unlike input variables (var.*) which are set from outside (via CLI or .tfvars), locals are computed inside the module, based on other values. They're like the intermediate results in a recipe: you don't ask the chef for "a bowl of brown sugar mixed with 2 tsp of cinnamon" every time — you prep the "brown sugar mix" once and use it.

Key definitions: - Local value — a named expression that Terraform evaluates once during planning, then reuses across the module. - locals block — a configuration block in a .tf file where you define one or more local values using name = expression. - Reference — you use local.<name> (note the singular local, not locals) to access the value anywhere in the module.

Here's the mental diagram:

Inputs (var.*)  →  Locals (local.*)  →  Resources & Modules
                      ↑
                (computed once)

Locals can depend on variables, other locals, resource attributes, and even function calls. They're resolved in order, so referencing another local works fine as long as there's no cycle (like two locals referencing each other).

Pro tip: Locals are evaluated during planning, not at runtime. This means they can be used in resource arguments, but not in dynamic block labels (which must be strings known at plan time).

How it works step by step

Let's refactor the earlier example to use locals. Follow these steps:

1. Identify repeated expressions. Look for the same condition or value across multiple resources. In our example, that's var.environment == "production" ? ... used three times with different results.

2. Define a locals block. At the top of your configuration (or in a separate locals.tf), create the block with names for each computed value.

locals {
  environment  = var.environment == "production" ? "production" : "staging"
  instance_type = local.environment == "production" ? "m5.large" : "t3.micro"
  scale_adjustment = local.environment == "production" ? 3 : 1
  allowed_cidrs = local.environment == "production" ? ["10.0.0.0/16"] : ["0.0.0.0/0"]
}

3. Replace inline expressions with local.<name>.

resource "aws_instance" "app" {
  instance_type = local.instance_type
}

resource "aws_autoscaling_policy" "scale_up" {
  scaling_adjustment = local.scale_adjustment
}

resource "aws_security_group" "app" {
  ingress {
    cidr_blocks = local.allowed_cidrs
  }
}

4. Reference locals in any order — Terraform handles dependencies. You can even use format(), join() or other functions inside locals to build complex values.

5. Keep locals minimal — If a value is used only once, it's often clearer to keep it inline. Locals shine for repeated or highly computed expressions.

Hands-on walkthrough

Let's build a complete example that uses locals to clean up a multi-resource setup. We'll create a simple web server with a security group and an autoscaling policy, all driven by a single local.is_production flag.

Step 1: Create main.tf with input variables and a locals block.

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "staging"
  validation {
    condition     = can(regex("^(production|staging)$", var.environment))
    error_message = "Environment must be 'production' or 'staging'."
  }
}

locals {
  is_production   = var.environment == "production"
  instance_type   = local.is_production ? "m5.large" : "t3.micro"
  scale_adjustment = local.is_production ? 3 : 1
  # Derived from instance type to show computed locals
  dedicated_host  = local.is_production ? true : false
}

# Simulated outputs to see the values without real AWS calls
output "instance_type" {
  value = local.instance_type
}

output "scale_adjustment" {
  value = local.scale_adjustment
}

output "dedicated_host" {
  value = local.dedicated_host
}

Step 2: Run terraform init and terraform plan (no providers needed here — just local evaluation).

$ terraform init
$ terraform plan

Changes to Outputs:
  + instance_type    = "t3.micro"
  + scale_adjustment = 1
  + dedicated_host   = false

You can apply this plan to save these new output values to the Terraform state.

Step 3: Switch the environment to production and see the locals change.

$ terraform plan -var "environment=production"

Changes to Outputs:
  + instance_type    = "m5.large"
  + scale_adjustment = 3
  + dedicated_host   = true

Notice how we changed one variable, and all locals updated coherently. That's the power of a single source of truth.

Step 4: Use locals in a real resource block. Here's a more realistic example with an AWS instance and a security group (requires an AWS provider, but demonstrates the pattern):

provider "aws" {
  region = "us-east-1"
}

locals {
  project       = "webapp"
  environment   = var.environment
  instance_type = local.environment == "production" ? "m5.large" : "t3.micro"
  tags = {
    Name        = "${local.project}-${local.environment}"
    Environment = local.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0" # Amazon Linux 2
  instance_type = local.instance_type

  tags = local.tags
}

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

Expected plan output (abbreviated):

# aws_instance.app will be created
+ resource "aws_instance" "app" {
    + ami               = "ami-0c55b159cbfafe1f0"
    + instance_type     = "t3.micro"
    + tags              = {
        + "Environment" = "staging"
        + "ManagedBy"   = "terraform"
        + "Name"        = "webapp-staging"
      }
  }

# aws_security_group.app will be created
+ resource "aws_security_group" "app" {
    + name_prefix = "webapp-sg"
    + tags        = {
        + "Environment" = "staging"
        + "ManagedBy"   = "terraform"
        + "Name"        = "webapp-staging"
      }
  }

You've just used locals to eliminate redundancy and keep the configuration DRY (Don't Repeat Yourself).

Compare options / when to choose what

Terraform offers several mechanisms for holding values. Here's how they compare:

Mechanism When to use Example Best for
variable Values that change per environment or user input var.region External inputs, from CLI/.tfvars
locals Derived values based on other values, used multiple times local.instance_type Repeated expressions, computed logic
resource attribute Outputs of created resources aws_instance.app.id Referencing real infrastructure attributes
data source Query external data at plan time data.aws_ami.ubuntu.id Fetching existing infrastructure info

Rule of thumb: If a value is computed from other values and used more than once, make it a local. If it's an input that users can change, use a variable. If it's a runtime attribute of a resource, reference it directly.

When you might not want locals: - Single-use expressions can stay inline for simplicity. - Values that depend on resource attributes that don't exist at plan time (e.g., an instance's public IP) — you should reference the resource attribute directly, not try to store it in a local. - Complex logic that requires loops or conditionals — locals are limited to expressions, not arbitrary code; consider for expressions or modules instead.

Troubleshooting & edge cases

Error: Cycle in local value — This happens when two locals reference each other (e.g., a = local.b and b = local.a). Fix by breaking the cycle: compute both from a common variable, or split into simpler values.

Error: Reference to undeclared variable — You used a var that doesn't exist, or you referenced locals instead of local. Remember: the block is locals, but references are local.<name> (singular).

Wrong output: Locals evaluated with wrong value — Locals are evaluated at plan time and then stored in the state. If you change a variable, you must run terraform plan again to see the new values. Don't expect them to change at apply time.

Edge case: Using locals in dynamic block labels — Labels must be literal strings or values known at plan time, so you cannot use a local that depends on a resource attribute. Example:

resource "aws_iam_policy" "example" {
  dynamic "statement" {
    for_each = local.statements
    content {
      # 'statement' here is fine
    }
  }
}

This works because the label statement is a literal. But if you tried to set the label from a local, Terraform would fail.

Multiple strings vs lists — Locals can be any type, but make sure you use the correct syntax for lists and maps. A common mistake is mixing [] with {} — a list of strings uses ["...", "..."], a map uses { key = "value" }.

What you learned & what's next

You've mastered use locals for cleaner expressions — the essential Terraform practice that transforms repetitive, hard-to-read configurations into clean, maintainable code. You learned: - The pain of inline expressions: repetition and drift. - The mental model of locals as named, computed constants. - The step-by-step process of identifying, defining, and referencing locals. - Hands-on examples with output and real AWS resources. - When to choose locals over variables or resource attributes. - Troubleshooting cycles, typos, and plan-time behavior.

Now that your expressions are clean, you're ready to take the next step in the Terraform foundations path: data sources. Data sources let you fetch existing infrastructure information (like AMIs or VPC IDs) at plan time, and you'll often combine them with locals to make your configurations even more dynamic and self-aware.

Keep your locals at the top of your file, name them clearly, and let them do the heavy lifting. Your future self — and your team — will thank you.

Practice recap

Create a small locals.tf file with a local.is_dev flag, and use it to set three different values (instance type, tags, and CIDR range) across at least three simulated resources. Test with both terraform plan -var 'environment=dev' and -var 'environment=prod', and confirm the outputs change coherently. Then try introducing a deliberate cycle to see the error, and fix it.

Common mistakes

  • Confusing locals block with local.<name> reference — always use the singular local to access the value.
  • Creating dependency cycles between locals (A references B, B references A) — break the cycle by computing both from a common variable.
  • Using locals for single-use expressions, which adds indirection without benefit — keep inline for simplicity.
  • Expecting locals to change at apply time based on resource attributes — they are evaluated at plan time and stored in state.

Variations

  1. Using locals with for_each to generate dynamic blocks or repeatable configurations.
  2. Separating locals into a dedicated locals.tf file for organization in larger modules.
  3. Combining locals with terraform console to test and debug complex expressions interactively.

Real-world use cases

  • Define a single tags local map and use it across multiple AWS resources for consistent tagging.
  • Compute environment-specific instance types and autoscaling thresholds in a shared module used by multiple services.
  • Derive network CIDR blocks and subnet names from a vpc_cidr local to avoid hardcoding in security groups and route tables.

Key takeaways

  • Locals store named expressions computed once, reducing repetition and improving readability.
  • Use local.<name> to reference values, not locals.
  • Locals are plan-time evaluated; they can depend on variables and other locals but not on resource attributes that appear at apply.
  • Refactor repeated inline conditions into locals to ensure consistency across resources.
  • Reserve locals for values used more than once or derived with complex logic.
  • Troubleshoot cycles and typos by checking reference syntax and dependency order.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.