| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/random 3.9.0 (unknown-key demo) |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Troubleshooting Terraform Invalid for_each argument — list and tuple type errors, toset and map conversion, keys known only after apply, map of objects with each.key and each.value, nested collections, duplicate keys, each used outside for_each, and verification with validate, plan, and state list. Does not cover count vs for_each choice, dynamic blocks, or cloud-specific resources. |
| Related guides | Terraform count vs for_each Terraform data types Terraform expressions Terraform resource dependencies Terraform Associate certification course |
Invalid for_each argument means Terraform rejected the collection you passed to for_each before it could build instance addresses. The error text usually tells you which case you hit — wrong type, unknown keys, or a related for expression problem — and the fix depends on that shape, not on the cloud provider.
This guide reproduces three distinct Invalid for_each argument failures on Ubuntu, then walks through the corrected patterns. For when to choose count versus for_each in the first place, see Terraform count vs for_each.
Each scenario uses its own directory under ~/terraform-labs/terraform-invalid-for-each-argument/ so state from one exercise does not affect the next.
terraform_data resource and, for the unknown-key demo, hashicorp/random. Run terraform init in each new directory before terraform validate or terraform plan.
Why Terraform returns Invalid for_each argument
for_each creates one instance per map entry or set member. Terraform records each instance in state at an address such as terraform_data.svc["web"], so it must know the full set of keys during planning.
Configuration defines keys → Plan builds instance map → Apply creates each instancePass a list, use apply-time values as keys, or build a map with duplicate keys and planning stops with Invalid for_each argument or a closely related error.
Reproduce the most common type mistake in an isolated directory:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/list-type
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/list-typeWrite a list-typed variable straight into for_each:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "names" {
type = list(string)
default = ["web", "api"]
}
resource "terraform_data" "svc" {
for_each = var.names
input = each.value
}
EOFInitialize the directory, then validate:
terraform init -input=falseTerraform has been successfully initialized!Run validate to surface the type error:
terraform validate -no-colorError: Invalid for_each argument
on main.tf line 9, in resource "terraform_data" "svc":
9: for_each = var.names
├────────────────
│ var.names is a list of string
The given "for_each" argument value is unsuitable: the "for_each" argument
must be a map, or set of strings, and you have provided a value of type list
of string.Read the type line first (list of string, tuple, map, or set). The second paragraph in unknown-key errors explains whether keys or values are the problem.
Fix a list or tuple used with for_each
for_each accepts a map or a set of strings — not a list or tuple. Lists are ordered; for_each keys must be an unordered collection with stable string identities.
Convert unique strings with toset()
When each list element is already the identity you want, wrap the list with toset():
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/tosetWrite the corrected configuration with toset():
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "names" {
type = list(string)
default = ["web", "api"]
}
resource "terraform_data" "svc" {
for_each = toset(var.names)
input = each.value
}
EOFRun init and validate:
terraform init -input=false && terraform validate -no-colorSuccess! The configuration is valid.Plan shows keyed instances instead of numeric indexes:
terraform plan -no-color# terraform_data.svc["api"] will be created
+ resource "terraform_data" "svc" {
+ input = "api"
}
# terraform_data.svc["web"] will be created
+ resource "terraform_data" "svc" {
+ input = "web"
}
Plan: 2 to add, 0 to change, 0 to destroy.toset() deduplicates strings. If ["web", "web"] collapses to one member, you get one instance — not an error. When duplicates must remain distinct, use a map with unique keys instead.
Prefer a map when values carry structure
When each item is an object or needs attributes beyond the key name, convert the list to a map in a locals block:
locals {
by_name = { for inst in var.instances : inst.name => inst }
}
resource "terraform_data" "svc" {
for_each = local.by_name
input = "${each.value.name}:${each.value.tier}"
}The for expression is covered in Terraform expressions. Terraform data types explains when a list, set, map, or object fits your input.
Fix for_each values known only after apply
The second common Invalid for_each argument appears when keys or set members come from resource attributes that stay unknown until apply.
Reproduce it with random_id:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/unknown-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/unknown-keysPoint for_each at a set built from random_id.seed.hex:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
resource "random_id" "seed" {
byte_length = 4
}
locals {
dynamic_names = [random_id.seed.hex]
}
resource "terraform_data" "svc" {
for_each = toset(local.dynamic_names)
input = each.value
}
EOFInitialize and plan:
terraform init -input=falsePlan fails because the set member stays unknown until random_id is created:
terraform plan -no-colorPlan: 1 to add, 0 to change, 0 to destroy.
Error: Invalid for_each argument
on main.tf line 17, in resource "terraform_data" "svc":
17: for_each = toset(local.dynamic_names)
├────────────────
│ local.dynamic_names is tuple with 1 element
The "for_each" set includes values derived from resource attributes that
cannot be determined until apply, and so Terraform cannot determine the full
set of keys that will identify the instances of this resource.
When working with unknown values in for_each, it's better to use a map value
where the keys are defined statically in your configuration and where only
the values contain apply-time results.Terraform is not saying random_id is wrong. It is saying instance addresses such as terraform_data.svc["2f4a8b1c"] cannot exist in the plan until random_id.seed.hex is known, and for_each keys must be known earlier. The same constraint applies to count.
Correct pattern — static keys, apply-time values
Define keys in configuration. Put computed results in argument values, not in the for_each collection:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/static-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/static-keysKeep for_each keys in a static map and reference random_id only in input:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
variable "services" {
type = map(string)
default = {
web = "placeholder"
api = "placeholder"
}
}
resource "random_id" "seed" {
byte_length = 4
}
resource "terraform_data" "svc" {
for_each = var.services
input = "${each.key}-${random_id.seed.hex}"
}
EOFValidate and apply:
terraform init -input=false && terraform validate -no-colorSuccess! The configuration is valid.Apply so Terraform can resolve the random suffix inside each input:
terraform apply -auto-approve -input=falseApply complete! Resources: 3 added, 0 changed, 0 destroyed.List state addresses — keys are configuration-defined, values absorbed the random suffix:
terraform state listrandom_id.seed
terraform_data.svc["api"]
terraform_data.svc["web"]For dependency ordering when one resource must exist before another reads its attributes, see Terraform resource dependencies.
Use a map of objects with for_each
Maps of objects are the usual shape when each instance needs multiple attributes. Keys become each.key; the object fields live under each.value.
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/map-objects
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/map-objectsDefine a map of objects and read each.value.port inside the resource:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "apps" {
type = map(object({
port = number
}))
default = {
web = { port = 80 }
api = { port = 8080 }
}
}
resource "terraform_data" "svc" {
for_each = var.apps
input = "${each.key}:${each.value.port}"
}
EOFAfter init, validate:
terraform init -input=false && terraform validate -no-colorSuccess! The configuration is valid.Plan shows keyed instances with computed inputs:
terraform plan -no-color# terraform_data.svc["api"] will be created
+ resource "terraform_data" "svc" {
+ input = "api:8080"
}
# terraform_data.svc["web"] will be created
+ resource "terraform_data" "svc" {
+ input = "web:80"
}
Plan: 2 to add, 0 to change, 0 to destroy.Apply to create the instances:
terraform apply -auto-approve -input=falseApply complete! Resources: 2 added, 0 changed, 0 destroyed.List state to confirm keyed addresses:
terraform state listterraform_data.svc["api"]
terraform_data.svc["web"]Inside the resource block:
each.key → "web" or "api"
each.value → object with port
each.value.port → 80 or 8080Fix nested collections before for_each
Nested structures must be transformed into a suitable for_each value — typically a flat map with stable keys, or a set of strings when each string itself is the instance identity. A for expression is enough for one level; deeper nesting may need flatten() — see Terraform expressions for flatten and collection transforms.
Convert a list of objects keyed by name:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/nested-map
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/nested-mapBuild local.by_name from the list before passing it to for_each:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "instances" {
type = list(object({
name = string
tier = string
}))
default = [
{ name = "web-1", tier = "frontend" },
{ name = "api-1", tier = "backend" },
]
}
locals {
by_name = { for inst in var.instances : inst.name => inst }
}
resource "terraform_data" "svc" {
for_each = local.by_name
input = "${each.value.name}:${each.value.tier}"
}
EOFApply and list instances:
terraform init -input=false && terraform apply -auto-approve -input=false && terraform state listApply complete! Resources: 2 added, 0 changed, 0 destroyed.
terraform_data.svc["api-1"]
terraform_data.svc["web-1"]If two list elements share the same name, the for expression fails during plan with a duplicate key error rather than silently overwriting.
Common for_each errors
These messages often appear alongside or instead of Invalid for_each argument. Each has a separate reproduction directory under the lab root.
Duplicate map keys in a for expression
Create a directory for the duplicate-key failure:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/duplicate-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/duplicate-keysTwo list items share the same name, which collides when the for expression builds map keys:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "instances" {
default = [
{ name = "web", tier = "a" },
{ name = "web", tier = "b" },
]
}
locals {
by_name = { for inst in var.instances : inst.name => inst }
}
resource "terraform_data" "svc" {
for_each = local.by_name
input = each.value.tier
}
EOFterraform validate may pass because the HCL is syntactically valid. Plan surfaces the collision:
terraform init -input=false && terraform plan -no-colorError: Duplicate object key
on main.tf line 9, in locals:
9: by_name = { for inst in var.instances : inst.name => inst }
├────────────────
│ inst.name is "web"
Two different items produced the key "web" in this 'for' expression. If
duplicates are expected, use the ellipsis (...) after the value expression to
enable grouping by key.Give each instance a unique key, or use the ellipsis grouping form when duplicates are intentional.
Sensitive values cannot be used for for_each
Terraform exposes for_each keys in resource addresses, so a sensitive collection cannot directly identify instances:
variable "services" {
type = set(string)
sensitive = true
default = ["web", "api"]
}
resource "terraform_data" "svc" {
for_each = var.services
input = each.value
}Terraform rejects the collection because the values would become visible in addresses such as terraform_data.svc["web"]. Keep sensitive data in each.value or another resource argument while deriving the instance keys from non-sensitive configuration values.
each used outside a for_each block
each.key and each.value exist only inside blocks that declare for_each:
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/each-context
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/each-contextReference each.value without declaring for_each:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "svc" {
input = each.value
}
EOFValidate to see that each is unavailable outside for_each:
terraform init -input=false && terraform validate -no-colorError: each.value cannot be used in this context
on main.tf line 3, in resource "terraform_data" "svc":
3: input = each.value
A reference to "each.value" has been used in a context in which it is
unavailable, such as when the configuration no longer contains the value in
its "for_each" expression. Remove this reference to each.value in your
configuration to work around this error.Add for_each, or replace each.value with a variable or local reference.
Quick reference
| Symptom | Likely cause | Fix |
|---|---|---|
must be a map, or set of strings + list or tuple |
List passed directly to for_each |
toset(var.names) or build a map with a for expression |
values derived from resource attributes / known only after apply |
Keys or set members depend on apply-time attributes | Static map keys; put computed values in resource arguments |
Sensitive collection rejected for for_each |
Instance keys are exposed in addresses and UI output | Derive keys from non-sensitive configuration; keep sensitive data in each.value or other arguments |
Key depends on uuid(), timestamp(), or another deferred impure function |
Terraform cannot determine stable keys during graph construction | Use configuration-defined keys; keep generated values in resource arguments |
Duplicate object key in for expression |
Two items share the same map key | Unique key field, or ellipsis grouping |
each.value cannot be used |
each referenced without for_each |
Add for_each or remove each |
Fewer instances than list items after toset() |
Duplicate strings collapsed by toset |
Map with distinct keys per item |
Working examples and verification
After any fix, confirm in four steps:
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset
terraform validate -no-colorSuccess! The configuration is valid.Plan next and confirm keyed instance addresses appear:
terraform plan -no-colorThe plan should list keyed addresses such as terraform_data.svc["web"], not numeric indexes.
Apply so Terraform records the instances in state:
terraform apply -auto-approve -input=falseApply complete! Resources: 2 added, 0 changed, 0 destroyed.List state to confirm the instance addresses were recorded:
terraform state listterraform_data.svc["api"]
terraform_data.svc["web"]validate checks configuration validity, plan proves Terraform can construct stable keyed instances, and after apply, state list confirms those instance addresses were actually recorded. Compare addresses before and after your fix. A list passed to for_each never reaches apply; after toset(), state shows string keys. After the static-key pattern, keys stay web and api even when input contains a random suffix from apply.
Destroy lab resources when you finish:
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/static-keys && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/map-objects && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/nested-map && terraform destroy -auto-approve -input=false 2>/dev/null || trueReferences
- for_each meta-argument — HashiCorp Developer
- Type constraints and conversion — HashiCorp Developer
- Functions: toset — HashiCorp Developer
Summary
Invalid for_each argument almost always means the collection you passed is the wrong type, its keys are not knowable during plan, or a for expression built a map with duplicate keys. Lists and tuples fail validation until you convert them with toset() or a keyed map; apply-time resource attributes cannot become for_each keys because Terraform must assign instance addresses before apply runs.
The fixes in this guide follow the same pattern: define stable string keys in configuration, keep computed results in argument values, and verify with terraform validate, terraform plan, and terraform state list so addresses such as terraform_data.svc["web"] match what you expect. When duplicates collapse after toset(), switch to an explicit map with unique keys per item.
For choosing between count and for_each, continue with Terraform count vs for_each. For collection types and structural constraints, see Terraform data types.

