for_each
for_each creates one resource instance per element of a map or set, addressed by key — aws_instance.web["api"], ["worker"] — instead of by numeric index. The key is the identity, and that single design choice is why for_each is the right iteration tool for almost everything: adding or removing an element only affects that element's instance, and never disturbs the others.
Where count hands each instance a position that shifts when the list changes, for_each binds each instance to a stable name. Remove "worker" from a three-key map and Terraform destroys exactly aws_instance.web["worker"] — ["api"] and ["cron"] are untouched. For any collection with distinct identities or one that changes over time, this is the correct tool.
Basic Usage
Assign a set or a map to for_each. Inside the block, each.key is the element's key and each.value is its value. With a set built by toset([...]), key and value are the same string; with a map, the key is the map key and the value is whatever you stored under it.
resource "aws_iam_user" "team" { for_each = toset(["alice", "bob", "carol"]) name = each.value # each.key == each.value for a set } # Addressed as aws_iam_user.team["alice"], ["bob"], ["carol"]
Key-Based Stable Identity
The address aws_instance.web["api"] is keyed on the string "api", recorded that way in state. Removing an unrelated key changes nothing about the "api" instance — its key did not move, so Terraform sees no diff for it. This is the renumbering trap solved: there are no positions to shift, only named slots that come and go independently.
Maps vs Sets as Source
Use a set of strings when the values are themselves the natural identities and need no extra configuration — a list of usernames, a set of bucket names. Use a map when each instance carries several configured values keyed by a stable name. A map is also the only way to give for_each meaningful per-instance data beyond the key itself.
Iterating Complex Data
The strongest pattern is for_each over a map(object({...})): one map entry per resource, each holding a small object of settings. The key names the instance and stays stable; each.value.cidr and each.value.az pull the per-instance fields. This is how you describe a fleet of distinctly-configured subnets or instances in one readable block.
variable "subnets" { type = map(object({ cidr = string, az = string })) default = { public = { cidr = "10.0.1.0/24", az = "us-east-1a" } private = { cidr = "10.0.2.0/24", az = "us-east-1b" } } } resource "aws_subnet" "this" { for_each = var.subnets vpc_id = aws_vpc.main.id cidr_block = each.value.cidr availability_zone = each.value.az tags = { Name = each.key } # public, private }
The Plan-Time Constraint
for_each keys must be known at plan time. Terraform builds the instance addresses before it talks to any API, so a key derived from an attribute that is only known after apply — a generated ID, an ARN of a not-yet-created resource — fails at plan time, because Terraform cannot predict how many instances to create. Map keys, and every member of a set, must be known at plan time; only a map's values may be unknown. Keep keys to static, deterministic strings you control in the configuration.
- Deriving a
for_eachkey from a resource attribute that is only known after apply, hitting the "keys must be known at plan time" error. - Feeding
for_eacha list instead of a set or map, getting a type error — convert withtoset()first. - Building keys from values that can collide, so two elements produce the same key and one is silently dropped.
- Using non-deterministic or order-dependent keys (an index, a timestamp), reintroducing exactly the churn
for_eachwas meant to prevent. - Using a set of objects rather than a map, then having no stable key to address an instance by.
- Default to
for_eachfor any collection of resources with distinct identities. - Use a
map(object)when each instance needs several configured values, keyed by a stable name. - Keep
for_eachkeys static, unique, and known at plan time — never derived from a post-apply attribute. - Convert a list to a set with
toset()only when the values are themselves the natural keys. - Tag each instance with
each.keyso the resource name in AWS matches its key in state.
Knowledge Check
Why does removing one key from a for_each map not disturb the other instances?
- Each instance is bound to a stable key, not a position, so the others' addresses never shift
for_eachrecreates every instance on each apply, so order is irrelevant- Terraform sorts the map keys alphabetically before each apply, so an instance's position never matters
- Maps are immutable, so removing a key has no effect on state
What is the constraint on for_each keys?
- They must be known at plan time — they can't depend on a not-yet-created resource's attribute
- They must be sequential integers so that Terraform can order the resulting instances deterministically
- They can be anything, including values known only after apply
- They must be globally unique across the entire configuration
When should the source be a map(object) rather than a set of strings?
- When each instance needs several configured values keyed by a stable name
- Whenever there are more than three instances
- Only when the keys are numeric
- A set is never a valid
for_eachsource; it must always be a map of objects
You pass a plain list(string) directly to for_each. What happens?
- A type error —
for_eachneeds a set or map, so you wrap it withtoset() - It works fine, and each instance is keyed by its numeric position in the list
- Terraform converts it to
countautomatically - It silently deduplicates and keeps the order
You got correct