Why Terraform Stops Your Plan
Terraform operates on a strict contract: it must predict the exact address of every resource before it touches your infrastructure. If a resource address is aws_instance.web["web-01"], the key "web-01" must be known during the plan phase. Terraform cannot wait until the apply phase to decide what a resource will be named.
This error typically triggers when you use a dynamic value—like a generated AWS Instance ID or a sensitive password—as a key in your for_each loop. Because these values don't exist until the provider actually creates the resource, Terraform hits a logical dead end. It stops the process to prevent your state file from becoming inconsistent.
The Scenario: Dynamic DNS Records
Suppose you are deploying a cluster of three EC2 instances. You want to automatically create a Route53 DNS record for each one using the instance ID as part of the domain name. You might try this code:
resource "aws_instance" "server" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
resource "aws_route53_record" "dns" {
# This will FAIL because IDs are generated during apply
for_each = { for s in aws_instance.server : s.id => s.private_ip }
zone_id = "Z0123456789ABCDEF"
name = "${each.key}.example.com"
type = "A"
ttl = 300
records = [each.value]
}
AWS only assigns an ID (like i-04f123456789ab) after the instance is running. Since Terraform cannot know these IDs during the terraform plan step, it cannot determine the keys for your DNS records. This results in the Invalid for_each argument error.
Tactical Fix: The Target Workaround
Need a fix right now? You can force Terraform to create the dependencies first using the -target flag:
terraform apply -target=aws_instance.server
This command creates the instances and stores their IDs in your state file. A subsequent terraform apply will then work because the IDs are now "known" values. Use this sparingly. It is a manual band-aid that often breaks automated CI/CD pipelines.
Permanent Fix 1: Use Static Keys
The most reliable solution is to decouple your resource keys from dynamic cloud attributes. Instead of using a generated ID as a key, use a unique string that you define in your configuration.
By using a map or a list of strings, the keys are available immediately during the plan phase:
variable "node_names" {
type = list(string)
default = ["api-prod-01", "api-prod-02", "api-prod-03"]
}
resource "aws_instance" "server" {
for_each = toset(var.node_names)
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = { Name = each.key }
}
resource "aws_route53_record" "dns" {
# FIX: each.key is now a static string from var.node_names
for_each = aws_instance.server
zone_id = "Z0123456789ABCDEF"
name = "${each.key}.example.com"
type = "A"
ttl = 300
records = [each.value.private_ip]
}
Terraform now knows it needs to create aws_route53_record.dns["api-prod-01"] before it even talks to AWS. The private_ip value can remain "known after apply" because only the keys must be static.
Permanent Fix 2: Handling Sensitive Values
Sometimes the keys are technically known, but Terraform hides them because they are marked as sensitive. Terraform refuses to use sensitive values as resource keys because keys appear in plain text within your terminal and state files.
If you are certain the value is safe to expose in a resource address, wrap it in the nonsensitive() function:
resource "vault_generic_secret" "example" {
# Use nonsensitive to allow these keys to be used in resource addresses
for_each = { for k, v in var.secret_map : nonsensitive(k) => v }
path = "secret/${each.key}"
data_json = jsonencode(each.value)
}
Permanent Fix 3: Leveraging Data Sources
Data sources can often bridge the gap if you need to reference existing infrastructure. Unlike managed resources, data sources can frequently resolve during the "refresh" phase of a plan if their arguments are static.
data "aws_subnets" "public" {
filter {
name = "tag:Network"
values = ["public"]
}
}
resource "aws_instance" "nodes" {
# This works because the subnet IDs are fetched during the plan refresh
for_each = toset(data.aws_subnets.public.ids)
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
Verifying the Results
Run terraform plan to check your work. A successful fix will display a clear list of resource actions. For example, you should see specific addresses like aws_route53_record.dns["api-prod-01"] instead of an error message. If the error persists, double-check that you aren't accidentally using id, arn, or primary_access_key as a map key.

