What does the splat expression aws_instance.web[*].id evaluate to in Terraform, and when do you have to use a for expression instead?
answer
- sugar over one specific comprehension
- projects a single attribute
- non-list gets wrapped, null becomes empty
- no filter, no keys, no transform
- keyed collections need values() first
basics
~20 sIt evaluates to a list of the id attribute of every element in aws_instance.web, in order. A splat only traverses attributes of a list-like value, so once you need filtering, a computed key, or output shaped as a map, you must write a for expression instead.
solid answer
~50 s`[*]` is the splat operator: it takes a list, set or tuple and projects one attribute across every element, so `aws_instance.web[*].id` is the shorthand for `[for i in aws_instance.web : i.id]`. It is limited on purpose — it can only walk attributes and indexes, so there is no filtering, no transformation, and no way to produce a map. That is when you switch to a `for` expression. One quirk worth knowing: applied to a value that is *not* a list, set or tuple, `[*]` wraps it in a single-element list, and `null` becomes an empty list — which is why you see it used to safely read an attribute of an optional single nested block. And when a resource is addressed by key rather than by number, the address is a map, so the splat does not apply; you go through `values(...)` or write the `for` expression directly.
code
hcl · 14 lines# splat: one attribute, list result, order preserved
output "instance_ids" {
value = aws_instance.web[*].id
}
# the exact equivalent, written out
output "instance_ids_long" {
value = [for i in aws_instance.web : i.id]
}
# splat cannot do this: keyed output
output "id_by_name" {
value = { for name, i in aws_instance.by_name : name => i.id }
}go deeper
Know that [*] pulls one attribute out of every element and returns a list, and be able to say it is shorthand for a for expression over the same collection.
Explain the limits precisely: attribute traversal only, no filtering or key production, plus the wrapping rule where a non-list becomes a one-element list and null becomes an empty list. Know the legacy .*. form exists.
Show that you reason about the shape of module outputs: prefer a keyed map over a positional list where consumers need to identify a specific instance, and never build order-sensitive values on a splat whose upstream ordering came from a map or set.
Own the output contract across the estate. Positional lists in module outputs quietly couple every consumer to ordering; deciding that outputs are keyed by a stable identifier is a repo-wide convention worth enforcing, not a per-module style preference.
## What the operator does A splat expression projects an attribute across a collection. Written in full: ```hcl aws_instance.web[*].id # is shorthand for [for i in aws_instance.web : i.id] ``` The result is a tuple in the same order as the source, so `output "ids" { value = aws_instance.web[*].id }` gives you a clean list of ids without a comprehension. You can chain further traversal — `aws_instance.web[*].root_block_device[0].volume_id` — and you can index the result like any other list. ## The two splat forms There are historically two: - **Full splat**, `[*]`, introduced with the HCL2 language. It works on lists, sets and tuples, supports further attribute access and indexing, and applies the wrapping rule below. - **Attribute-only splat**, `.*.`, the legacy form (`aws_instance.web.*.id`). It still parses and covers the simple case, but it does not support the same chained traversal. New code should use `[*]`. ## The wrapping rule This is the part that surprises people. If `[*]` is applied to a value that is not a list, set or tuple: - a non-null value is wrapped in a **single-element list**; - `null` becomes an **empty list**. That behaviour exists for optional single nested blocks. A block that may or may not be present reads as a value or as null, and `something[*].attribute` turns both cases into a list you can safely iterate or splat further, rather than blowing up with "Attempt to get attribute from null value". It is a useful idiom, but it also means a splat never protects you from a typo: applying `[*]` to something that was never a collection quietly produces a one-element list instead of an error. ## Where splats stop and for expressions start The splat has no filter clause, no result expression and no key production. The moment you need any of: - **filtering** — only the instances in one subnet; - **transformation** — upper-casing, string building, arithmetic; - **a map output** — id keyed by name; - **combining two attributes** into an object, you are writing a `for` expression. In practice a splat is the right choice for exactly one job — "give me this one attribute from all of them" — and a for expression for everything else. Reaching for a splat and then post-processing the result with three more function calls is usually a sign the for expression was the simpler code all along. ## When the collection is keyed rather than numbered A resource whose instances are addressed by *key* rather than by number is a map, and the splat operator does not apply to maps. That case is where the wrapping rule bites hardest, because there is no error to warn you — you either get a rejection or a nonsensical single-element result depending on the exact expression. Two clean options: ```hcl values(aws_instance.web)[*].id # convert to a list first [for k, i in aws_instance.web : i.id] # or just write the comprehension { for k, i in aws_instance.web : k => i.id } # keyed output, often what you wanted ``` The third form is frequently the better output shape anyway: consumers of the module can look an id up by name instead of relying on positional order. ## Ordering guarantees A splat over a list-like value preserves the source order. If that source is derived from a map or a set somewhere upstream, the order is lexical rather than insertion-based. Never build something order-sensitive — a comma-joined string used as an identifier, an index into another list — on top of a splat whose source ordering you have not reasoned about. If the order is meaningful, make it explicit with `sort()` on a stable field, or produce a map so no order is implied at all. ## Interview framing The honest one-line summary is: a splat is syntactic sugar over one specific for expression. Knowing that lets you answer both directions of the question — how to expand a splat into a comprehension, and when the sugar has run out.
- Why does applying [*] to a single object return a one-element list rather than an error?It is deliberate ergonomics for optional single nested blocks. Such a block reads as either a value or null, and `block[*].attr` normalises both into a list — one element or zero — so downstream code can iterate uniformly instead of guarding against null. The cost is that a genuine mistake, splatting something that was never a collection, is silently tolerated.
- You need instance ids keyed by instance name for a module output. Splat or for expression?For expression, in the object form: `{ for k, i in aws_instance.web : k => i.id }`. A splat can only produce a positional list, and a positional list is a fragile contract for consumers — a keyed map lets them look up exactly the one they want and does not shift meaning when the underlying collection changes.
- What is the difference between aws_instance.web[*].id and aws_instance.web.*.id?The first is the modern full splat; the second is the legacy attribute-only form. Both cover the simple projection, but the full splat also supports chained attribute access and indexing after the projection, and it applies the single-value wrapping rule. New code should use `[*]`.
saying these in an interview costs you the question
- Thinks a splat can filter with a condition
- Expects a splat over a map to give a list of values
- Assumes [*] on a non-collection is a syntax error
- Treats splat output order as insertion order
- Chains three functions onto a splat instead of writing the for expression