Skip to main content
RunBook Academy

TerraformXIII · Variables, Outputs, and LocalsProduction Terraform

Locals: Internal Variables

Intermediate⏱ ~10 minbash

What you'll learn

  • Use `locals` to compute values that the operator does not set
  • Choose between a variable, a local, and an output for any given value
  • Reduce duplication of complex expressions across multiple resources
  • Avoid over-abstraction: locals that add indirection without clarity
  • Recognise the scope and reference rules of locals within a module

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

A locals block is where Terraform stores values that are derived from other values. The configuration produces them, not the operator. They are not exposed to the operator. They are not in .tfvars. They are not in the environment. They are the configuration’s own scratchpad, and the discipline is to use them when the value is genuinely computed — not when it is convenient.

A real production incident: the team wrapped every value in a locals block “for testability.” A 200-line module became 800 lines; the resources at the bottom referenced local.x_y_z for every attribute. A junior engineer tried to find where instance_type was set and gave up. The refactor that extracted the locals back into the resources took a week. The lesson: locals are a tool, not a style.

What locals do

locals are for derived values. They reduce duplication of complex expressions and document values that have business meaning but do not come from the operator.

# CONFIGURATION

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Owner       = var.team
    CostCentre  = var.cost_centre
    ChangeRef   = var.change_request_id
  }

  name_prefix = "${var.environment}-${var.region}"

  app_subnets = {
    for idx, cidr in var.app_subnet_cidrs :
    "app-${idx}" => {
      cidr_block        = cidr
      availability_zone = var.availability_zones[idx]
    }
  }
}

resource "aws_instance" "app" {
  count = length(var.app_subnet_cidrs)

  ami           = data.aws_ami.app.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.app[count.index].id

  tags = merge(
    local.common_tags,
    {
      Name = "${local.name_prefix}-app-${count.index}"
    },
  )
}

resource "aws_subnet" "app" {
  for_each = local.app_subnets

  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone
  vpc_id            = aws_vpc.main.id

  tags = merge(
    local.common_tags,
    { Name = "${each.key}" },
  )
}

The common_tags local removes the temptation to inline five lines of tags into every resource. The name_prefix local captures the environment-region prefix that would otherwise be repeated in every Name tag. The app_subnets local transforms a list of CIDRs into a map suitable for for_each.

When to use locals

Use a local when:

  • The same expression appears in three or more resources.
  • A value is derived from multiple variables (composite tags, name prefixes, naming conventions).
  • A for_each map needs to be built from a list of inputs.
  • A complex conditional expression would obscure a resource argument.
  • The derived value has business meaning that deserves a name (production_environment_name, backup_window_utc).

Do not use a local when:

  • The value comes from the operator — use a variable.
  • The value is consumed by another stack — use an output.
  • The expression is used once and is already simple.
  • You are building an abstraction layer over a single value.
  • The local is referenced exactly once (inline it).

Variables vs locals vs outputs

SourceSet by operatorComputed by configConsumed by other stacksScope
Variableyesdefaults may be expressionspassed through module inputsmodule / root
Localnoyesno — internal onlymodule / root
Outputnoyesyes — remote_state and modulesmodule / root
Operator provides     Configuration computes     Configuration exposes
     |                         |                          |
     v                         v                          v
  variable               local                      output
     |                         |                          |
     +-----------> resource argument <-------------------+

The decision flow:

Does the operator set the value?
  yes -> variable
  no  -> Does the value cross to another stack?
            yes -> output
            no  -> Is the value derived from other values?
                       yes -> local
                       no  -> inline literal

WhyThisMatters

WhyThisMatters The locals block is the configuration’s only place to express business logic. The team that inlines every expression loses the opportunity to document. The team that locals-everything loses the opportunity to read. The middle is the discipline.

Reference scope

Within a module, locals can reference:

  • Other locals (declared in the same block or earlier blocks)
  • Variables in the same module
  • Built-in functions and literals

Locals cannot reference:

  • Resources (not yet known at the locals evaluation stage)
  • Data sources (resolved later)
  • Outputs from other modules
# CONFIGURATION

locals {
  # OK: reference to another local
  full_name = "${local.name_prefix}-app"

  # OK: reference to a variable
  region_tags = {
    Region = var.region
  }

  # ERROR: reference to a resource
  # vpc_arn = aws_vpc.main.arn

  # ERROR: reference to a data source
  # account_id = data.aws_caller_identity.current.account_id
}

Locals are evaluated after variables and before resources. The order within a single locals block is irrelevant for evaluation; Terraform sorts references topologically.

Failure modes

  1. Local references a variable that does not exist. The plan fails with Reference to undeclared input variable. The error is clear; the fix is to declare the variable.

  2. Local used in for_each produces duplicates. The map keys are not unique; for_each errors with Duplicate object key. The fix is to deduplicate or rename keys.

  3. Local shadows a variable name. A local named region shadows var.region within its scope. The plan succeeds but the operator reads the wrong value. The fix is to rename the local (resolved_region).

  4. Local computed from a circular reference. Local A references local B which references local A. The plan fails with a cycle error.

  5. Local used to wrap every single-use value. The reader cannot follow the indirection. Refactor: inline single-use locals.

  6. Local used as a configuration-management layer. A local reads var.environment and produces a for_each map of forty keys. The local has become a hidden DSL. Refactor: lift the logic to an explicit input or a data source.

Production guidance

  • One locals.tf file per module, grouped by purpose (tags, naming, subnet maps, IAM policy documents).
  • Comment every local whose computation is not obvious in five seconds. The comment is the contract.
  • Do not reference resources or data sources in locals — they are evaluated too early.
  • Do not mark locals sensitive — the value flows into the state file regardless of the flag. Use a data source for sensitive derived values.
  • Audit locals quarterly: single-use locals should be inlined.

What comes next

The next lesson is Expressions for Production Configurations — the complexity cap for variable defaults, the reference scope that defaults can use, and when to lift a complex expression into a local or a data source.

Verification

  1. Can you set a local’s value with terraform apply -var=local.x=...? Why or why not?
  2. A local is referenced in exactly one resource. Should it be inlined?
  3. What is the difference between a local and an output?
  4. A local references data.aws_caller_identity.current.account_id. What happens at plan time, and why?

Knowledge check · 7 questions

  1. Q1. Who provides the value for a `locals` block entry?

  2. Q2. Locals can be overridden by `-var` CLI flags at apply time.

  3. Q3. Which of the following is the right use of a `locals` block?

  4. Q4. A local references a variable that does not exist. What happens at plan time?

  5. Q5. Which of the following are valid uses of a `locals` block? (Select all that apply.)

  6. Q6. A local is used to derive a database password from another variable. What is the production issue?

  7. Q7. A module has 5 locals and 30 resources. Each local is referenced exactly once. What is the right action?

Passing score: 75%. Answers are checked in this browser.