Terraform for_each
What is Terraform for_each?
for_each is the Terraform meta-argument that turns a single resource, data, ephemeral, or module block into several resource instances, one per element of a map or set. Each instance gets its own identity, keyed by a map key or set member rather than a numeric index.
It sits alongside count, depends_on, lifecycle, and provider as one of Terraform's meta-arguments, and it's useful whenever an instance needs a stable, meaningful key instead of a position in a list.
It often gets confused with a dynamic block, which repeats a nested block inside a single resource rather than creating separate resource instances.
Basic for_each syntax
A for_each block needs one thing beyond the resource's normal arguments: a map variable or a set of strings to iterate over. Terraform doesn't have a set literal, so turning a plain list of values into a set has to go through toset() first, a conversion that drops duplicates and discards ordering.
Here is an example iterating over a set of team names:
For a set, each.key and each.value are identical, so use each.key. A map is more useful once each instance needs more than one distinct value:
Here each.key supplies the meaningful key ("primary", "failover") and each.value is the whole object, so for_each turns each key-value pair in the map into its own instance, and each instance can carry a completely different configuration, different instance types included, without a separate block for each one.
What for_each does
Once a block declares for_each, Terraform creates one resource instance per map key or set member and tracks each one separately in state.
In terraform plan output and everywhere else in the UI, instances show up addressed as <TYPE>.<NAME>["<KEY>"], string-keyed and quoted, in contrast to the numeric [0], [1] addressing that count produces. That addressing scheme is why for_each is needed: the key is the instance's identity, not its position.
Rename a key in the map you're iterating over, even without touching any other argument, and Terraform doesn't see a rename. It sees the old key's instance disappearing and an unrelated new instance appearing under the new key – nothing in the state model connects the two. Recognizing that they're related is left entirely to a human reading the diff.
A one-character typo fix to a map key produces the same destroy-and-recreate plan as swapping the resource entirely.
The keys and values Terraform iterates over also have to be known before any remote operation runs, which rules out a few categories of input outright.
You can't key off values Terraform only learns during apply, and sensitive values are rejected outright – both of which are covered under common mistakes below.
The same mechanics apply when for_each drives a data source instead of a resource: each instance still needs a unique key, and each can pull different values back from the underlying provider.
When two resources have a genuine one-to-one relationship, you can chain for_each between them by pointing the second block's for_each at the first resource itself (Terraform treats a for_each-driven resource as a map of objects everywhere else in the configuration).
The textbook case is a VPC and its internet gateway, provisioned together per network. Chaining for_each between them keeps the two sets of instance keys locked together without you needing to maintain a second, parallel map by hand.
An example of Terraform for_each in action
Here is another example: rolling out the same Terraform module across several environments, each with its own network range and instance sizing.
One module block, driven by a single map, produces module.network_baseline["dev"], module.network_baseline["staging"], and module.network_baseline["prod"] as three fully independent instances of the same module, each wired to its own CIDR block and instance type.
terraform plan reports all three by name; add a fourth entry to var.environments and it adds exactly one new instance instead of touching the other three. Meaningful keys, then, have a clear benefit over numeric ones: nothing about the existing instances shifts just because a new one showed up.
The same shape extends past environments. Multiple resources spun up per customer tenant, per Kubernetes namespace, per cloud region, or per compliance boundary all follow this pattern: a single resource or module block driven by one map of different configurations, with instance addressing that stays legible no matter how many entries the map holds.
Common mistakes users make with for_each
Most for_each errors trace back to a situation where Terraform needs to know the full set of keys before it touches any infrastructure.
Reaching for two collections in one for_each
A resource needs one instance per combination of two independent lists, say one DNS zone linked to every virtual network in a set. The instinct is to look for a way to nest one for_each loop inside another.
However, Terraform only accepts one for_each per block, so nested loops aren't a fix. You need to combine the collections into a single map or set first. setproduct builds every combination of elements from two or more sets, while flatten collapses nested structures into something for_each can iterate over directly:
Keying off a computed value
An access key or a generated ID (anything that only exists after apply runs) can't feed a for_each on the same plan. Terraform needs every key up front, so stick to values it already knows before apply, like a variable or a local, rather than one that depends on a resource being created first.
Impure functions like uuid() and timestamp() fail for the same reason – Terraform defers evaluating them past the point where for_each needs a value.
Mixing count and for_each on one block
Terraform rejects this outright rather than picking a winner. Every block gets exactly one or the other.
Passing sensitive values into for_each
Terraform errors immediately if you try this, and if you derive a for_each input from sensitive data through a function instead (say, extracting keys from a map with sensitive values), most functions propagate that sensitivity into the result rather than stripping it – so the derived value fails with the same error.
If the keys genuinely aren't secret, nonsensitive() strips the marking, which an explicit decision as Terraform prints every for_each key in plan output.
When count fits better than for_each
Regardless of resource type, count and for_each solve the same category of problem (avoid writing a separate block per resource instance), but they disagree on what an instance is.
countinstances are numbered:aws_instance.worker[0],[1],[2]. That numeric index shifts for everything below an insertion or removal point in the list.for_eachinstances are keyed. A key doesn't move just because an unrelated entry was added or removed elsewhere in the map.
Use count when instances really are interchangeable and only the quantity matters, a fixed pool of identical workers where losing worker [1] and getting a replacement back is a non-event.
Use for_each when an instance needs a value the others don't (a distinct region, say, or a distinct pricing tier), or the moment reordering the underlying collection shouldn't touch unrelated instances.
Migrating an existing count-based resource over to for_each is itself a destructive move by default: Terraform sees aws_instance.worker[0] disappearing and aws_instance.worker["blue"] appearing, with no idea they're the same infrastructure object – unless you tell it otherwise with a moved block.
Stategraph's perspective on for_each
As covered above, a key rename and a deliberate destroy-and-recreate look identical to Terraform.
One solution is to use a hand-written moved block per relocation, which works for three or four keys but turns into another thing to maintain when it's across dozens of per-tenant or per-region instances, where sorting out which keys actually moved versus which were added or removed on purpose is easy to get wrong.
We built stategraph refactor to solve this problem: it tracks HCL changes across a session as you edit and rename, then emits the moved blocks for you when you're done, instead of asking you to reconstruct the mapping by hand from a diff.
Use it once a for_each block is genuinely large enough that manual mapping is error-prone, not for a three-key map where you can just read the diff yourself.
Scale can also be a problem when you're applying changes across a large for_each fleet. A traditional state-level lock serializes every apply against that state, so touching three keys out of two hundred still queues behind whatever else is running.
Stategraph Velocity's resource-level locking checks for actual overlap between transactions rather than locking the whole state, so a change to a handful of for_each keys doesn't wait behind unrelated work that touches different instances in the same block.
Neither of these counts for much at small scale, but both can be decisive when a for_each map stops being something you can visualize.
If that is where your for_each blocks are already, try Stategraph free and bring an existing state along to see it against your own graph.
Related Terraform terms
- How Terraform's DAG really works covers how
countandfor_eachinstances become separate nodes in Terraform's dependency graph, which is the mechanism behind the addressing discussed above. - Terraform state locking, explained is about the state-locking behavior a large
for_eachfleet runs into once multiple people are applying against it concurrently. - Terraform blast radius reveals what's actually at stake when a
for_eachkey change ripples across every instance it touches. - Why multi-state transactions replace run-all goes deeper on
movedblock mechanics referenced in the alternatives and Stategraph sections above.