Fix Terraform "Invalid for_each Argument" Error

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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.

NOTE
Examples use the built-in 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.

text
Configuration defines keys  →  Plan builds instance map  →  Apply creates each instance

Pass 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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/list-type
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/list-type

Write a list-typed variable straight into for_each:

bash
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
}
EOF

Initialize the directory, then validate:

bash
terraform init -input=false
output
Terraform has been successfully initialized!

Run validate to surface the type error:

bash
terraform validate -no-color
output
Error: 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():

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset

Write the corrected configuration with toset():

bash
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
}
EOF

Run init and validate:

bash
terraform init -input=false && terraform validate -no-color
output
Success! The configuration is valid.

Plan shows keyed instances instead of numeric indexes:

bash
terraform plan -no-color
output
# 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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/unknown-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/unknown-keys

Point for_each at a set built from random_id.seed.hex:

bash
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
}
EOF

Initialize and plan:

bash
terraform init -input=false

Plan fails because the set member stays unknown until random_id is created:

bash
terraform plan -no-color
output
Plan: 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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/static-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/static-keys

Keep for_each keys in a static map and reference random_id only in input:

bash
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}"
}
EOF

Validate and apply:

bash
terraform init -input=false && terraform validate -no-color
output
Success! The configuration is valid.

Apply so Terraform can resolve the random suffix inside each input:

bash
terraform apply -auto-approve -input=false
output
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

List state addresses — keys are configuration-defined, values absorbed the random suffix:

bash
terraform state list
output
random_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.

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/map-objects
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/map-objects

Define a map of objects and read each.value.port inside the resource:

bash
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}"
}
EOF

After init, validate:

bash
terraform init -input=false && terraform validate -no-color
output
Success! The configuration is valid.

Plan shows keyed instances with computed inputs:

bash
terraform plan -no-color
output
# 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:

bash
terraform apply -auto-approve -input=false
output
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

List state to confirm keyed addresses:

bash
terraform state list
output
terraform_data.svc["api"]
terraform_data.svc["web"]

Inside the resource block:

text
each.key        → "web" or "api"
each.value      → object with port
each.value.port → 80 or 8080

Fix 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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/fixes/nested-map
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/nested-map

Build local.by_name from the list before passing it to for_each:

bash
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}"
}
EOF

Apply and list instances:

bash
terraform init -input=false && terraform apply -auto-approve -input=false && terraform state list
output
Apply 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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/duplicate-keys
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/duplicate-keys

Two list items share the same name, which collides when the for expression builds map keys:

bash
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
}
EOF

terraform validate may pass because the HCL is syntactically valid. Plan surfaces the collision:

bash
terraform init -input=false && terraform plan -no-color
output
Error: 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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-invalid-for-each-argument/errors/each-context
cd ~/terraform-labs/terraform-invalid-for-each-argument/errors/each-context

Reference each.value without declaring for_each:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "svc" {
  input = each.value
}
EOF

Validate to see that each is unavailable outside for_each:

bash
terraform init -input=false && terraform validate -no-color
output
Error: 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:

bash
cd ~/terraform-labs/terraform-invalid-for-each-argument/fixes/toset
terraform validate -no-color
output
Success! The configuration is valid.

Plan next and confirm keyed instance addresses appear:

bash
terraform plan -no-color

The plan should list keyed addresses such as terraform_data.svc["web"], not numeric indexes.

Apply so Terraform records the instances in state:

bash
terraform apply -auto-approve -input=false
output
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

List state to confirm the instance addresses were recorded:

bash
terraform state list
output
terraform_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:

bash
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 || true

References


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.


Frequently Asked Questions

1. Why does Terraform say Invalid for_each argument?

Terraform requires for_each to receive a map or a set of strings whose instance keys are known during planning. Passing a list or tuple, using values that stay unknown until apply as keys, or building a map with duplicate keys all trigger Invalid for_each argument or a related planning error.

2. Can for_each use a list in Terraform?

Not directly. Wrap a list of unique strings with toset() or convert the list to a map with stable keys using a for expression. If you need per-item attributes, prefer a map keyed by a configuration-defined identifier rather than a bare list.

3. How do I fix for_each values known only after apply?

Define map keys statically in configuration and put apply-time results in the values only. Terraform must know resource instance addresses during plan, so it cannot use resource attributes that remain unknown until apply as for_each keys or set members.

4. What is the difference between toset and a map for for_each?

toset converts a list of unique strings into a set where each string is both the key and the value. A map gives you separate keys and richer values, which is safer when list order changes or when each instance needs more than one attribute.

5. Does terraform validate catch every for_each mistake?

validate catches wrong types and invalid each references, but duplicate keys in a for expression and unknown for_each values often appear only during terraform plan. Run both commands when debugging for_each failures.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)