skip to content

A Terraform plan fails with "Invalid for_each argument: the for_each value depends on resource attributes that cannot be determined until apply". Why does Terraform refuse, and how do you restructure the configuration instead of reaching for -target?

level: middleimportance: should knowfreq 48%

answer

  1. expansion happens before the diff
  2. keys now, values later
  3. the id does not exist yet
  4. iterate the map you already keyed
  5. -target is a bootstrap, not a design

basics

~20 s

Terraform must expand a resource into addressed instances before it can render a diff, so for_each keys have to be known during plan. Fix it by keying on static input — variables, locals, or another resource's own keys — and letting the unknown values sit in each.value.

solid answer

~50 s

Expansion happens before diffing: Terraform has to know which instance addresses exist to compare them against state, so the *keys* of a `for_each` must be computable at plan time. The error appears when you derive keys from an attribute that only the provider can supply after creation — typically `for_each = toset([for v in aws_vpc.main : v.id])`. The values are allowed to be unknown; only the keys are not. The fix is almost always to key on something you already wrote down: iterate the map that another `for_each` resource exposes (`for_each = aws_vpc.main`, keys intact, `each.value.id` unknown until apply and that is fine), or key by a name from a variable and reference the id inside the block. Terraform's own hint suggests `-target` to apply in two stages; that is an escape hatch for a one-off, not a design, because the pipeline then needs a partial apply every time the config is built from scratch.

code

hcl · 11 lines
hcl
resource "aws_vpc" "main" {
  for_each   = toset(["blue", "green"])
  cidr_block = "10.0.0.0/16"
}

# Invalid for_each argument: keys are ids assigned during apply
resource "aws_subnet" "bad" {
  for_each   = toset([for v in aws_vpc.main : v.id])
  vpc_id     = each.value
  cidr_block = "10.0.1.0/24"
}

go deeper

for a junior

Recognise the message as a plan-time-unknown problem rather than a syntax mistake, and know that for_each keys must be values Terraform can compute before it applies anything.

for a middle

Explain the expand-then-diff ordering, state precisely that keys must be known while values may not, and show the rewrite that keys off a variable or another for_each resource's map.

for a senior

Notice the trap that the configuration plans cleanly on an already-applied workspace and only fails on a fresh build, and insist the fix land in the code rather than as a -target step in a disaster-recovery runbook.

for a principal

Set the expectation that every root module can be built from empty in one apply, and treat any required two-phase bootstrap as a design defect to be paid down or explicitly documented as an accepted risk.

## What the error is actually saying Terraform's run is ordered: it builds the resource graph, **expands** each resource into instances, then diffs each instance address against state to produce the plan. Expansion has to happen first, because "how many instances and what are they called" determines what the plan is even comparing. That ordering is the entire reason for the constraint: > for `count`, the number must be known at plan time; for `for_each`, the **keys** must be known at plan time. What is *not* required is that the values be known. A map whose keys you wrote down but whose values are attributes computed during apply is perfectly legal — the diff can show `(known after apply)` for those arguments because the instance addresses are already settled. ## The shape that triggers it The classic failure is building a collection out of identifiers that do not exist yet: ```hcl resource "aws_vpc" "main" { for_each = toset(["blue", "green"]) cidr_block = "10.0.0.0/16" } # Invalid for_each argument: the keys are VPC ids, # which the provider only assigns during apply. resource "aws_subnet" "bad" { for_each = toset([for v in aws_vpc.main : v.id]) vpc_id = each.value cidr_block = "10.0.1.0/24" } ``` On a first run the VPC ids are unknown, so Terraform cannot say whether there will be one instance, two, or two with different names, and it refuses rather than guessing. Confusingly, the same configuration *plans fine* once the VPCs exist and their ids are in state — which is how this reaches CI: it works on the developer's already-applied workspace and fails the moment someone builds a fresh environment. ## The fix: key on what you wrote, not on what the cloud returns A resource that itself uses `for_each` is exposed as a map keyed by the same keys. Iterating *that* keeps the keys static while letting the values be unknown: ```hcl resource "aws_subnet" "good" { for_each = aws_vpc.main # keys "blue" / "green" — known now vpc_id = each.value.id # unknown until apply — allowed cidr_block = "10.0.1.0/24" } ``` The general rule generalises well: **the key should come from your inputs, the unknown should live in the body.** If the source data is a list of objects, project it with a `for` expression keyed by a name you control: ```hcl for_each = { for s in var.subnets : s.name => s } ``` Other variants of the same move: - Iterate a variable or local (`var.environments`, `local.services`) and look the id up inside the block. - Where a value must come from elsewhere, read it with a data source that resolves at plan time rather than from a resource created in the same run. - Avoid computing keys with functions over unknown data — `toset`, `keys`, `merge` and friends all propagate unknownness; wrapping an unknown in `toset()` does not make it known. ## Why -target is not the answer The error text suggests applying the dependency first with `-target`, then running a normal apply. It genuinely works, and for a one-off bootstrap it is acceptable. As a design it is not, because: - every fresh environment now needs a documented two-phase apply, which someone will forget; - CI has to encode a partial apply, and a partial apply is exactly the thing pipelines are supposed to avoid; - the failure is silent on already-applied workspaces, so the two-phase requirement is invisible until disaster recovery. If you find yourself writing the `-target` step into a runbook, treat that as a signal to restructure the keys instead. ## count has the same constraint `count = length(something_unknown)` fails for identical reasons, with a matching "Invalid count argument" message. The workaround people reach for there — `length(var.list)` instead of `length(resource_attribute_list)` — is the same principle: derive multiplicity from configuration, never from provider output. ## What to say in an interview Name the ordering (expand, then diff), state the precise rule (keys known at plan, values may be unknown), show the fix of iterating the keyed map or a variable, and finish by explaining why the suggested `-target` is a bootstrap escape hatch rather than an architecture.

  • If only the keys must be known, why does wrapping the unknown collection in toset() not help?
    Because unknownness propagates through functions. `toset()` applied to a list whose elements are unknown produces a set whose membership is itself unknown, so Terraform still cannot decide how many instances exist or what to call them. Only changing where the keys come from fixes it.
  • Does count suffer from the same restriction?
    Yes — the count expression must be known at plan time too, and Terraform reports "Invalid count argument" with the same reasoning. The remedy is identical: derive the number from variables or locals rather than from an attribute the provider assigns during apply.

saying these in an interview costs you the question

  • Claims -target is the proper long-term fix
  • Thinks both keys and values must be known at plan time
  • Adds depends_on expecting it to resolve the unknown
  • Says running apply twice is the intended workflow
  • Believes toset() makes an unknown collection known

context