How to Fix Terraform 'Known After Apply' Error in for_each Map Keys
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 15–30 mins
TL;DR
- What broke: Terraform's
for_eachrequires all map keys to be fully resolved at plan time. You used a computed attribute (e.g., a resource ID, ARN, or dynamic name) as a map key — Terraform can't build its dependency graph and hard-stops. - How to fix it: Re-key your map using a value known before apply — a static string, an input variable, or a
localvalue derived from non-computed fields. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your failing
.tfblock and get corrected HCL without sending your config to any external server.
The Incident (What Does the Error Mean?)
You ran terraform apply (or terraform plan) and hit this:
Error: Invalid for_each argument
on main.tf line 14, in resource "aws_iam_role_policy_attachment" "this":
14: for_each = { for r in aws_iam_role.roles : r.id => r }
The "for_each" map includes keys derived from resource attributes that
cannot be determined until apply, so Terraform cannot determine the full
set of keys that will identify the instances of this resource.
To work around this, use the -target argument to first apply only the
resources that the for_each value depends on, and then apply normally.
Immediate consequence: The entire terraform apply is blocked. No resources in this module are created or updated. If this is a pipeline, the deploy is dead. The -target workaround Terraform suggests is a trap — it introduces state drift and is explicitly discouraged for production use.
Terraform's execution model requires a complete, static dependency graph at plan time. for_each map keys are used to name state addresses (e.g., aws_iam_role_policy_attachment.this["arn:aws:iam::123456789:role/my-role"]). If that key is unknown until apply, Terraform cannot build the graph node — full stop.
The Blast Radius
This isn't just an inconvenience. In a CI/CD pipeline:
- Every downstream resource depending on the
for_eachresource is also blocked. - Teams using
-targetas a workaround silently corrupt state — resources get created outside the full graph, leading to phantom drift that surfaces weeks later as mysterious diffs. - In multi-environment setups (dev/staging/prod using the same module), the bug is often masked in dev (where resources pre-exist in state) and explodes in prod on first apply against a fresh account.
- If the computed key is an AWS ARN or GCP resource ID, the map key changes every time the upstream resource is recreated — causing cascading replacement of all
for_eachinstances.
How to Fix It
Root Cause Pattern
The canonical mistake: using resource.attribute (computed post-apply) as the key of a for_each map.
# BAD: r.id is unknown at plan time — it's assigned by AWS after creation
- resource "aws_iam_role_policy_attachment" "this" {
- for_each = { for r in aws_iam_role.roles : r.id => r }
- role = each.value.name
- policy_arn = var.policy_arn
- }
# GOOD: r.name is defined in your Terraform config — known at plan time
+ resource "aws_iam_role_policy_attachment" "this" {
+ for_each = { for r in aws_iam_role.roles : r.name => r }
+ role = each.value.name
+ policy_arn = var.policy_arn
+ }
Rule: The key in key => value must resolve from your .tf source or var.* — never from a provider-assigned attribute like .id, .arn, .unique_id.
Basic Fix — Re-key on a Static Attribute
If you're iterating over resources you define, always key on the field you wrote, not the field AWS/GCP assigned.
# Pattern: iterating over a map variable to create resources and attach policies
variable "roles" {
type = map(object({ policy_arn = string }))
}
resource "aws_iam_role" "this" {
for_each = var.roles
name = each.key
}
resource "aws_iam_role_policy_attachment" "this" {
- for_each = { for k, v in aws_iam_role.this : v.id => v }
+ for_each = aws_iam_role.this # keys are already the static map keys from var.roles
role = each.value.name
policy_arn = var.roles[each.key].policy_arn
}
When you do for_each = aws_iam_role.this directly (a resource map), Terraform uses the same keys as the upstream for_each — which are your static var.roles keys. Problem solved.
Enterprise Best Practice — Locals as the Single Source of Truth
For complex pipelines, define a local that normalizes all keys upfront. Nothing downstream ever touches a computed attribute as a key.
- # Anti-pattern: building maps inline inside resource blocks using computed fields
- resource "aws_route53_record" "this" {
- for_each = { for ep in aws_lb.app.subnet_mapping : ep.allocation_id => ep }
- ...
- }
+ # Best practice: local defines the authoritative key set from known values only
+ locals {
+ subnet_records = {
+ for idx, subnet_id in var.subnet_ids :
+ "record-${idx}" => {
+ subnet_id = subnet_id
+ lb_dns = aws_lb.app.dns_name # value (not key) can be computed
+ }
+ }
+ }
+
+ resource "aws_route53_record" "this" {
+ for_each = local.subnet_records # keys are "record-0", "record-1" — static
+ name = each.value.lb_dns
+ ...
+ }
Key discipline: In local maps used for for_each, the left side of => must never reference a resource.* computed field. The right side (value) can reference anything.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. terraform validate is not enough. It won't catch this class of error — you need a plan-time check.
2. Enforce plan-before-apply in your pipeline:
# In GitHub Actions / GitLab CI — always run plan and capture exit code
terraform plan -detailed-exitcode -out=tfplan
# Exit code 2 = changes pending (not an error). Exit code 1 = hard error including this one.
3. Checkov rule: Checkov doesn't natively catch this, but tflint with the terraform ruleset does:
tflint --enable-rule=terraform_required_providers
# More importantly, use the tflint-ruleset-aws plugin which flags dynamic key patterns
tflint --plugin-dir ~/.tflint.d/plugins
4. OPA/Conftest policy to ban computed keys in for_each:
# policy/no_computed_foreach_keys.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
# Flag any for_each whose key_type is marked unknown_only
resource.change.before == null
resource.change.after_unknown.id == true
msg := sprintf("Resource '%s' uses a computed key in for_each. Re-key on a static attribute.", [resource.address])
}
5. Module contract rule (enforce in code review): Any module that exposes a for_each-able output must export a stable string key (name, slug, index) alongside computed attributes. Document this in your module's outputs.tf with explicit comments marking which outputs are safe as for_each keys.