Terraform Error Guide: 'value depends on resource attributes that cannot be determined until apply' (apply-time unknowns)
Fix Terraform apply-time unknown values that break for_each keys, dynamic blocks, and conditionals using known keys and staged -target applies.
- #terraform
- #troubleshooting
- #errors
- #planning
Exact Error Message
When Terraform builds a plan it walks a dependency graph and tries to resolve every expression to a concrete value. If the shape of your configuration (how many instances of a resource, or which keys exist) depends on an attribute that only exists after a resource is created, the plan stops cold:
Error: Invalid for_each argument
on main.tf line 18, in resource "aws_route53_record" "rec":
18: for_each = { for s in aws_subnet.app : s.id => s.cidr_block }
The "for_each" map cannot be determined until apply, so Terraform cannot
predict how many instances will be created. To work around this, use the
-target argument to first apply only the resources that the for_each value
depends on.
You will see close variants of this for count, dynamic blocks, and module meta-arguments. The common thread is always the same phrase: a value depends on resource attributes that cannot be determined until apply.
What the Error Means
Terraform runs in two phases. During plan, it computes the desired state without touching any real infrastructure. During apply, it actually creates and modifies resources, and only then does the cloud provider hand back generated attributes like IDs, ARNs, IP addresses, and CIDR ranges.
Some expressions can tolerate an unknown value: an attribute marked (known after apply) in the plan output is fine if it is just a string assigned to another resource’s argument. But meta-arguments are different. count, for_each, and the keys of a for_each map control how many resource instances exist and what their addresses are. Terraform must know those before it can build the graph, diff state, and show you an accurate plan.
If the value behind for_each or count is derived from aws_subnet.app[*].id and those subnets do not exist yet, Terraform genuinely cannot know how many records to plan. So it refuses, rather than guess. This is the “apply-time unknown” problem: a structural decision is gated on data that is not yet knowable at plan time.
Common Causes
- for_each keys derived from computed IDs. Using
s.id(a known-after-apply attribute) as a map key is the classic trigger. Values can be unknown; keys cannot. - count tied to a resource attribute.
count = length(aws_instance.web)when the instances are created in the same apply. - Dynamic blocks fed by computed lists. A
dynamic "ingress"block iterating overaws_security_group.other.idreferences that are not yet provisioned. - Module
count/for_eachon an output of another module. Module B’s instance count depends on Module A’s not-yet-created resources. - Conditionals over unknown booleans.
for_each = var.enabled ? toset(aws_x.y[*].arn) : []where the ARN set is unknown. - depends_on chains that defer creation. Adding
depends_onto a data source or resource can push the dependency’s resolution to apply time.
How to Reproduce the Error
Save this as main.tf. It tries to create a Route 53 record per subnet, keyed by the subnet ID that AWS only assigns at creation:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "app" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
}
resource "aws_route53_record" "rec" {
# subnet IDs are unknown until apply -> invalid for_each
for_each = { for s in aws_subnet.app : s.id => s.cidr_block }
zone_id = var.zone_id
name = "node-${each.key}.internal"
type = "A"
ttl = 300
records = [cidrhost(each.value, 4)]
}
Run a plan and Terraform rejects it because the map keys (s.id) are all (known after apply).
Diagnostic Commands
Use only read-only commands to confirm the cause. None of these change infrastructure.
# Validate syntax and basic semantics
terraform validate
# Produce a plan and read where the error points
terraform plan
# Inspect the dependency graph to see what feeds for_each
terraform graph | grep -A2 "aws_route53_record"
# Evaluate the problematic expression interactively
echo 'aws_subnet.app[*].id' | terraform console
# See what already exists in state (apply-time vs not-yet-created)
terraform state list | grep aws_subnet
In terraform console, an unknown value prints as a tuple of (known after apply) placeholders. That is the signal: if the keys are unknown, the for_each is invalid. You can also scan the plan output:
terraform plan -no-color | grep "known after apply"
If the attribute you are keying on shows up in that list, you have found the apply-time unknown.
Step-by-Step Resolution
There are two durable fixes and one tactical workaround.
1. Key by static, plan-time-known values (preferred). Restructure so the for_each keys come from input variables or locals that Terraform knows before apply. You can still reference computed attributes inside the values, just never in the keys.
variable "subnets" {
type = map(object({
cidr = string
}))
default = {
app-a = { cidr = "10.0.0.0/24" }
app-b = { cidr = "10.0.1.0/24" }
app-c = { cidr = "10.0.2.0/24" }
}
}
resource "aws_subnet" "app" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
}
resource "aws_route53_record" "rec" {
# keys come from var.subnets, which is known at plan time
for_each = var.subnets
zone_id = var.zone_id
name = "node-${each.key}.internal"
type = "A"
ttl = 300
# value can reference the computed attribute safely
records = [aws_subnet.app[each.key].id == "" ? "" : cidrhost(each.value.cidr, 4)]
}
Now both resources share the same known key set (app-a, app-b, app-c), so Terraform can plan the records even though the subnet IDs are still unknown.
2. Two-stage -target apply (tactical workaround). When you cannot refactor immediately, create the dependency first, then let its attributes become known:
# Stage 1: create only the resources for_each depends on
terraform apply -target=aws_subnet.app
# Stage 2: full apply now that subnet IDs are known
terraform apply
After stage 1, the subnet IDs are real values in state, so the for_each map can be computed. Treat this as a stopgap; Terraform itself prints a warning that -target is meant for exceptional recovery, not routine workflow.
3. Apply the same principle to dynamic blocks, modules, and conditionals. For a dynamic block, build the iterator from a variable or local list, not a computed list. For module meta-arguments, pass a known map into the module rather than wiring it to another module’s resources. For conditionals, gate on a plain boolean variable (var.enabled) instead of the truthiness of an unknown set.
If you are tracking these failures across a fleet of pipelines, our incident-response dashboard can help triage recurring plan-time breakages.
Prevention and Best Practices
- Derive structure from inputs. Map keys, counts, and iterator sets should trace back to variables and locals, never to
(known after apply)attributes. - Separate “what exists” from “what it becomes.” Decide instance identity from configuration; let computed attributes flow only into argument values.
- Run
terraform planin CI on every PR. Catch apply-time-unknown errors before merge, not during a production apply. - Split layered infrastructure into stacks. Put foundational resources (VPCs, subnets) in one root module/state and dependent resources in another, so the foundation’s outputs are already known when the second stack plans.
- Avoid unnecessary
depends_on. Implicit dependencies through references are usually enough; explicitdepends_oncan force values to be unknown at plan time. - Prefer
for_eachovercountfor sets of named things, so adding or removing one item does not reshuffle every other instance address.
Browse more guides in the Terraform category.
Related Errors
- Terraform Error: Invalid for_each argument — the focused
for_eachguide, including type and null-key issues. - Terraform Error: Dependency cycle — when resources reference each other in a loop and the graph cannot be ordered at all.
Frequently Asked Questions
Why can a value be (known after apply) in one place but break for_each?
Because argument values are allowed to be unknown during plan, but meta-arguments are not. for_each, count, and map keys decide the shape of the plan, so Terraform must resolve them before it can diff state. An unknown shape is unrecoverable at plan time, while an unknown string argument simply gets filled in during apply.
Is -target a safe permanent solution?
No. It is a recovery tool for breaking a circular bootstrap, and Terraform warns you when you use it. Routine reliance on -target produces partial applies and drift. Use it once to unblock, then refactor so keys come from known inputs.
Can I just wrap the value in try() or coalesce() to silence it?
Not for the key. Those functions still produce an unknown value when their input is unknown, so the for_each remains undeterminable. They help only with optional argument values, not with structural meta-arguments.
Why does this happen on the first apply but not on the second?
After the first successful apply, the dependency’s attributes exist in state and are known. On a fresh terraform apply with empty state, both resources are created in the same run, so the dependency’s IDs are still unknown when the dependent resource is planned.
Does splitting into separate modules fix it? It helps, but only if the modules are applied in separate stages with separate state. If both modules apply in one run, the downstream module still sees unknown values. The real fix is keying on inputs that are known before any apply.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.