for Expressions
A for expression transforms one collection into another inside an expression — distinct from for_each, which stamps out resources. You write [for s in var.sources : upper(s)] to reshape a list, or {for k, v in var.map : k => v.region} to reshape a map, and an optional if clause filters as it goes. This is how you turn a raw input variable into exactly the map a for_each wants.
The pairing matters. for_each wants a clean keyed map, but the input you actually receive from a variable is often a list of objects, or a map that needs filtering, or a nested structure that has to be flattened. The for expression is the reshaping tool that sits between the messy input and the tidy collection the resource consumes. Keep the two straight: for builds values, for_each builds resources.
List and Map Comprehensions
The bracket style decides the output type, not the input. [for x in coll : expr] with square brackets produces a list; {for k, v in coll : k => expr} with braces produces a map. You can iterate a map and emit a list, or iterate a list and emit a map — the output collection is whatever the bracket says, and the => in the brace form is what names each key.
# a list of source objects from a variable... variable "sources" { type = list(object({ name = string, location = string })) } # ...projected into the map for_each consumes locals { source_map = { for s in var.sources : s.name => s } }
Iterating Maps and Lists
Over a list you get each element in turn; over a map you get k, v pairs. When position matters on a list, the form for i, x in list gives you the index alongside the element. The iteration variables are yours to name — s, k, v, i are conventions, not keywords — and the expression after the colon can reference any of them plus anything else in scope.
Filtering with if
A trailing if clause drops elements as the expression runs. [for s in var.sources : s.name if s.enabled] keeps only the enabled sources, computing the filter inline instead of in a separate local. The filter runs before the key/value projection, so an element that fails the if never reaches the output at all — it is not emitted with a null, it is simply absent.
locals { enabled_topics = { for s in var.sources : s.name => s if s.enabled } }
Building a Map for for_each
This is the workhorse pattern of the whole chapter: take a list of source objects from a variable and project it into {for s in var.sources : s.name => s}, producing the keyed map that for_each consumes. List in, keyed map out. The resource block then sets for_each = local.source_map and reads each.value for each source's settings. The variable stays a friendly list for whoever fills it in, and the for expression adapts it to the shape Terraform's iteration needs.
Build that map in a named local, not inline in the for_each argument. A multi-step transform wedged into the resource block is unreadable, and the named local gives the plan and the diff a stable, legible reference. The key you project on must be unique — which is exactly the property that makes it a good for_each key — so choose a field that cannot collide across entries.
Grouping and Nesting
Two advanced forms cover the harder reshapes. The ... grouping form collects values under shared keys — group event types by tenant, for instance, so each tenant key maps to a list of its event types rather than overwriting on collision. And for expressions nest: an outer for over tenants and an inner for over each tenant's events flattens or pivots a nested input into the flat map a resource needs. These appear the moment your input variable is more nested than the resource's flat collection.
- Using
[...]brackets when you meant a map and getting a list back, or the reverse — the bracket type, not the input, determines the output collection. - Projecting a list to a map keyed on a non-unique field, so two source rows collide on the same key and one is silently dropped.
- Writing a heavy multi-step transform inline in a
for_eachargument where it is unreadable — build the map in a namedlocalso the plan and the diff are legible. - Forgetting the
ifclause filters before the key/value projection, then being surprised that a filtered-out element never reaches the map at all. - Reaching for a
forexpression to create resources — it only transforms values; resource fan-out isfor_eachorcount, and confusing the two is a beginner trap. - Building a deeply nested transform in one expression where a grouping form or a chain of named locals would read clearly — cleverness here costs the next reader an hour.
- Build the
for_eachmap in a namedlocalwith aforexpression, keyed on a stable unique field, rather than inlining the transform. - Use the
ifclause to filter collections in one expression instead of pre-filtering in extra locals. - Pick
[...]versus{...}deliberately based on whether the consumer wants a list or a keyed map. - Keep each
forexpression to one clear transformation; chain through named locals when the reshape has multiple steps. - Reach for the grouping
...form when keys repeat, so collisions become lists instead of silently dropping entries.
Knowledge Check
What determines whether a for expression returns a list or a map?
- The bracket style —
[...]yields a list,{...}with=>yields a map — regardless of the input type - The input type — iterating a list always yields a list, while iterating a map always yields a map back out
- Whether a trailing
iffilter clause is present in the expression - The number of iteration variables you declare before the colon
When does the if clause run relative to the key/value projection?
- Before — a filtered-out element is never emitted, so it never reaches the output collection
- After — the element is projected into the output first, then nulled out afterward by the filter clause
- In parallel — projection and filtering are independent passes
- Only on map-producing expressions, never on list-producing ones
Why must the key you project on when building a map be unique?
- Two entries sharing a key collide, and one is silently dropped from the resulting map
- Non-unique keys quietly cause the expression to fall back to returning a list instead of a map
- Terraform sorts the map by key and duplicates break the sort
- Maps require integer keys, which are always unique
How does a for expression differ from for_each?
- A
forexpression transforms values into a new collection;for_eachstamps out resource instances - They are fully interchangeable syntaxes for the exact same underlying iteration and expansion operation
- A
forexpression stamps out resource instances, whilefor_eachonly reshapes data forworks on maps,for_eachworks only on lists
You got correct