skip to content

In Terraform, when are locals evaluated, and what shows up in the plan when a local's expression reads an attribute of a resource that does not exist yet?

level: middleimportance: should knowfreq 44%

answer

  1. graph nodes, not sequential statements
  2. file order does not matter
  3. unknown is contagious
  4. for_each keys must be known at plan
  5. no each.key inside a locals block

basics

~20 s

Terraform evaluates locals as nodes in the dependency graph, not top to bottom, so declaration order does not matter. A local that reads an attribute of an uncreated resource is unknown during plan, and every value derived from it renders as (known after apply).

solid answer

~50 s

Locals are not assignments executed in file order — each one becomes a node in the dependency graph, evaluated once its inputs are resolvable, which is why you can define `local.a` after the `local.b` it uses and why two locals referencing each other produce a dependency-cycle error rather than a stale value. When a local reads something Terraform cannot know until apply — an `id`, an `arn`, a generated password — the local itself is unknown at plan time, and anything using it renders as `(known after apply)`. That propagates: a `for_each` or a `count` whose expression depends on an unknown local cannot be planned at all, and Terraform errors telling you the value must be known. The fix is to key repetition off configuration you control rather than off resource attributes. Locals are also evaluated once per module, outside any resource's repetition scope, so `each.key` and `count.index` are not available inside a `locals` block.

code

hcl · 19 lines
hcl
variable "services" {
  type    = set(string)
  default = ["api", "worker"]
}

locals {
  # Keys come from declared input, so they are known at plan time.
  buckets = { for s in var.services : s => "acme-${s}-artifacts" }
}

resource "aws_s3_bucket" "artifacts" {
  for_each = local.buckets
  bucket   = each.value
}

locals {
  # Unknown until apply; fine as an argument, fatal as a for_each key.
  first_bucket_arn = aws_s3_bucket.artifacts["api"].arn
}

go deeper

for a junior

Recognise (known after apply) in plan output as Terraform saying the value is not knowable yet, not as a failure, and know that locals may be written in any order within the file.

for a middle

Explain the dependency graph: locals are nodes, order is irrelevant, cycles error out, and unknown values propagate through every expression that consumes them.

for a senior

Diagnose the failure mode in a real repository — a for_each keyed off a created resource's attribute — and restructure the configuration so repetition keys come from declared inputs instead.

for a principal

Set the convention that resource identity derives from declared configuration, never from runtime attributes, so plans stay reviewable and refactors do not silently re-address half the estate.

## Locals are graph nodes, not statements The mental model people bring from scripting languages — "this line runs, then the next" — is wrong here and produces two predictable surprises. Terraform builds a dependency graph over everything in the configuration: resources, data sources, variables, outputs and locals alike. Each locals entry becomes a node whose edges are the things its expression references. Terraform then evaluates in dependency order. The first consequence is that **declaration order is irrelevant**. This is valid: ```hcl locals { full_name = "${local.name_prefix}-db" name_prefix = "${var.project}-${var.environment}" } ``` Terraform resolves `name_prefix` first because `full_name` depends on it, regardless of the order in the file or across several `locals` blocks in different files. The second consequence is that **a cycle is an error, not a race**. If `local.a` references `local.b` and `local.b` references `local.a`, the graph has no valid order and Terraform refuses to proceed with a cycle error naming the participants. The same applies to a local that references itself. There is no "last definition wins". ## Unknown values During plan, Terraform knows the configuration and the prior state, and it refreshes what already exists — but attributes of resources that do not exist yet, or attributes the provider only fills in at create time, are *unknown*. An unknown value is a first-class concept in Terraform's evaluation, and it is contagious: any expression consuming an unknown produces an unknown. So a local like this: ```hcl locals { bucket_arn = aws_s3_bucket.artifacts.arn } ``` is unknown on the first plan, and everywhere `local.bucket_arn` is used the plan renders `(known after apply)`. That is normal and healthy — it is Terraform being honest that it cannot show you the final value. It becomes a problem only in the places where Terraform *requires* a known value. ## Where unknowns become a hard failure The important case is repetition. `for_each` needs its **keys** known at plan time, because the keys become resource addresses in state, and Terraform cannot plan changes to addresses it cannot name. If a local computes a map whose keys come from a resource attribute — say, keying off `aws_instance.web[*].id` — the plan fails with an error saying the `for_each` value depends on resource attributes that cannot be determined until apply. `count` behaves the same way for its numeric value. Note the asymmetry: for `for_each` the *values* in the map may be unknown; only the keys must be known. The fix is architectural, not syntactic: key repetition off something you control — an input variable, a static list, a map defined in a local from literals — and let the resource attributes flow into the *arguments* of those instances rather than into their identities. ## Locals are module-scoped, not instance-scoped A locals block is evaluated once per module instance. It is not re-evaluated per resource instance, which is why `each.key`, `each.value` and `count.index` are not in scope inside a `locals` block — those symbols only exist inside the resource, data or module block that declares the repetition. Attempting to use them yields an error about a reference to an unavailable symbol. The standard workaround is to build the *structure* in locals and iterate over it in the resource. A nested `for` expression that produces a map of composite keys, consumed by a single `for_each`, is the idiomatic way to express "one thing per subnet per environment" without needing per-instance evaluation inside the local itself. ## Refresh, sensitivity and inspection Because locals sit in the graph, one that depends on a data source is re-evaluated whenever that data source is read during the run — its value is not frozen from a previous apply. Locals are not stored in state at all; there is no recorded prior value to compare, only the expression and its inputs. For debugging, `terraform console` evaluates `local.<name>` interactively against the current state, which is the fastest way to check what a gnarly expression really produces before you wire it into fifty resources. Values that are unknown in the current context simply come back as unknown there too, which is itself informative.

  • Why must a for_each key be known at plan time when the map's values need not be?
    Because the keys become part of the resource address recorded in state — `aws_instance.web["api"]` — and Terraform must be able to name every instance it plans to create, update or destroy. Values are just arguments, so they may resolve during apply. That is why keying repetition off a resource attribute fails while passing that attribute as an argument is fine.
  • A plan is almost entirely (known after apply). Is that a bug?
    Usually not on a first apply into an empty environment, where nothing exists yet so almost every attribute is genuinely unknown. It is worth investigating when it appears on an incremental change to a stable estate: a common cause is a data source whose arguments depend on something being created in the same run, which makes its whole result unknown and cascades through every local derived from it.
  • How would you restructure a local whose map keys come from created instance IDs?
    Invert the direction. Key the map on something declared — a name, a role, an input list entry — and carry the instance ID as the map's value or look it up inside the consuming resource's arguments. Terraform can then plan stable addresses, and the unknown ID resolves during apply without ever appearing in a resource address.

saying these in an interview costs you the question

  • Thinks locals are evaluated top to bottom in file order
  • Says mutually referencing locals resolve in declaration order
  • Treats (known after apply) as an error rather than an unknown
  • Uses each.key inside a locals block
  • Keys for_each off an attribute of a resource being created

context