Chapter 5: Iteration and Conditionals
Topic 30

for_each

IterationHCL

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.

for_each over a set of strings
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.

Removing one key leaves the rest untouched
web["api"]
Keyed on "api". Unrelated to "worker", so its address never shifts — no diff.
web["worker"]
The one key you removed. Terraform destroys exactly this instance and nothing else.
web["cron"]
Keyed on "cron". Its named slot is stable, so the plan is one clean destroy — not a cascade.

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.

for_each over a map of objects
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.

Common Mistakes
  • Deriving a for_each key from a resource attribute that is only known after apply, hitting the "keys must be known at plan time" error.
  • Feeding for_each a list instead of a set or map, getting a type error — convert with toset() 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_each was meant to prevent.
  • Using a set of objects rather than a map, then having no stable key to address an instance by.
Best Practices
  • Default to for_each for 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_each keys 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.key so the resource name in AWS matches its key in state.
Comparable tools CloudFormation no native per-key iteration over resources Pulumi host-language map iteration with explicit names Terraform for_each is what makes iteration production-safe

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_each recreates 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_each source; it must always be a map of objects

You pass a plain list(string) directly to for_each. What happens?

  • A type error — for_each needs a set or map, so you wrap it with toset()
  • It works fine, and each instance is keyed by its numeric position in the list
  • Terraform converts it to count automatically
  • It silently deduplicates and keeps the order

You got correct