Inside a Terraform resource that sets for_each = toset(var.names) over a list of strings, what do each.key and each.value hold, and how does that change when for_each is given a map instead?
answer
- a set has no separate value
- key names the instance
- value carries the payload
- list of objects needs projecting
- the key is the address
basics
~20 sWith a set of strings, each.key and each.value are both the element itself. With a map, each.key is the map key and each.value is the corresponding value, which can be an object carrying per-instance settings.
solid answer
~50 s`for_each` accepts a map or a set of strings, and the `each` object reflects which one you gave it. For a set, there is no separate value, so `each.key` and `each.value` are the same string — the element — and the instance address is that string. For a map, `each.key` is the key and `each.value` is whatever the key maps to, commonly an object, so you can write `each.value.instance_type` while the address stays `["api"]`. That is the reason the map form dominates in real code: a set only lets instances differ by their name, whereas a map gives each instance a payload. When the source data is a list of objects, project it into a map with a `for` expression keyed by a stable field — `for_each = { for u in var.users : u.name => u }` — which both satisfies the type requirement and picks the instance key deliberately.
code
hcl · 13 linesvariable "users" {
type = list(object({ name = string, role = string }))
default = [
{ name = "ana", role = "admin" },
{ name = "ben", role = "dev" },
]
}
resource "aws_iam_user" "team" {
for_each = { for u in var.users : u.name => u }
name = each.key
tags = { role = each.value.role }
}go deeper
Remember that each.key and each.value are the same string for a set, while a map gives you a distinct key and value. Know that a plain list cannot be passed to for_each.
Explain why the map form dominates real configurations — the value carries per-instance settings — and show projecting a list of objects into a map with a for expression keyed by a stable field.
Treat the key as an identity decision: it is the state address, renaming it rebuilds the object, and the choice of key determines how safely the collection can be edited later.
Standardise how collections enter modules across the estate — the key's source, whether it comes from a variable or decoded data, and how a key rename is reviewed — so that instance identity is a convention rather than each author's preference.
## The each object When a resource block uses `for_each`, Terraform evaluates the block once per element and exposes an object named `each` with exactly two attributes: - `each.key` — the unique identifier for this instance, always a string, and the value that appears in the resource address. - `each.value` — the data associated with that key. What those hold depends on the collection type you supplied. ## Set of strings: key and value coincide ```hcl resource "aws_iam_user" "team" { for_each = toset(["ana", "ben"]) name = each.value # "ana" / "ben" } ``` A set has elements but no key/value pairing, so Terraform uses the element as both. `each.key == each.value` here, and instances live at `aws_iam_user.team["ana"]` and `["ben"]`. Using `each.value` reads more naturally for a plain name, but either works. Note that `toset` is doing real work: it converts a list to a set, which **removes duplicates** and drops the ordering. Removing ordering is exactly the point — it is why a set is acceptable as an instance source and a list is not. Duplicate removal is a quieter consequence: two identical names in the input silently become one instance rather than an error. ## Map: key and value are different things ```hcl variable "users" { type = map(object({ role = string })) default = { ana = { role = "admin" } ben = { role = "dev" } } } resource "aws_iam_user" "team" { for_each = var.users name = each.key # "ana" tags = { role = each.value.role } # "admin" } ``` Now the key names the instance and the value carries its configuration. This is the form most production code uses, because instances usually differ by more than their name: a size here, a CIDR there, a flag on one of them. Adding a field to the object type adds a per-instance knob without touching the addressing at all. ## Building the map from a list of objects Source data frequently arrives as a list of objects — from a variable, from a YAML file decoded with `yamldecode`, from a module's output. A list cannot be given to `for_each`, so project it: ```hcl resource "aws_iam_user" "team" { for_each = { for u in var.users : u.name => u } name = each.key tags = { role = each.value.role } } ``` The `for` expression with a `=>` produces a map. Two things deserve care: 1. **Choose the key deliberately.** It becomes the permanent instance address, so pick a field that identifies the thing and will not be rewritten casually. Keying by an ordinal position recreates the index fragility you switched to `for_each` to escape. 2. **Keys must be unique.** Duplicate keys in a `for` expression are an error unless you group them, so a duplicate name in the input surfaces at plan rather than silently collapsing — a small advantage over `toset` on a list. ## Renaming a key is a replacement Because the key is the address, changing it is not an edit — it is a removal at the old address plus a creation at the new one. Fixing a typo in a key destroys and recreates that instance. Treat keys as identifiers with the same permanence you would give a primary key, and use a `moved` block if a rename is genuinely necessary and the object must survive. ## Quick reference | for_each value | each.key | each.value | address | |---|---|---|---| | `toset(["ana"])` | `"ana"` | `"ana"` | `...["ana"]` | | `{ ana = {...} }` | `"ana"` | the object | `...["ana"]` | Start with a set when the only difference between instances is a name, and move to a map the moment one instance needs to differ — the change is additive and the addresses stay put.
- Why does converting a list with toset() sometimes change how many instances you get?Because a set has no duplicates. Two identical strings in the source list collapse into one element, so you get one instance rather than two, with no error. Building a map with a `for` expression is stricter — duplicate keys fail the plan instead of quietly merging.
- What happens when you rename a key in the map passed to for_each?The old key's instance is destroyed and a new one is created under the new key, because the key is the instance address rather than an attribute. If the underlying object must survive the rename, declare a `moved` block from the old address to the new one so Terraform re-addresses state instead of rebuilding.
saying these in an interview costs you the question
- Says each.key is unavailable when for_each takes a set
- Expects each.value to hold an object when given a set
- Thinks renaming a map key edits the instance in place
- Passes a list directly and expects Terraform to convert it
- Assumes toset preserves duplicates and ordering