Splat Expressions
A splat expression pulls one attribute out of a list of objects: aws_instance.web[*].id collects the id of every instance into a list. It is concise sugar over a for expression for the single most common case — "give me this field from all of them" — and it shows up constantly when you wire a fleet of resources into an output, a security-group rule, or a load-balancer target set.
Splat does exactly one thing — project an attribute across a list — and knowing its boundaries keeps it from surprising you. It works naturally over count, stumbles over for_each, and has a legacy .* form that handles a trailing index differently from the modern [*].
The Splat Operator
[*] projects an attribute across every element of a list. aws_instance.web[*].private_ip is read as "for each instance, give me its private_ip," and the result is a list of IPs in instance order. It is the daily tool for collecting IDs, ARNs, and IPs off a count-based resource.
resource "aws_instance" "web" { count = 3 ami = var.ami_id instance_type = "t3.micro" } output "private_ips" { value = aws_instance.web[*].private_ip # list of 3 IPs }
Equivalence to for
aws_instance.web[*].id desugars to [for i in aws_instance.web : i.id] — splat is the short form of that exact for expression. The moment you need anything more than projecting one attribute — a filter, a transformation, a computed value — the sugar runs out and you write the full for. Splat is the convenience; for is the general tool underneath it.
Splat with count vs for_each
Splat works on a count resource because that is a list. A for_each resource is a map, and [*] on a map does not give you what you want. Convert the map to a list of its values with values(...) first, or skip splat entirely and write a for over the map. This is the most common splat surprise: it silently misbehaves on the very resources you were told to prefer.
# aws_instance.web here uses for_each, so it is a map: # Wrong: [*] does not project across a map as you'd expect # aws_instance.web[*].id # Right: take the map's values first, then splat values(aws_instance.web)[*].id # Or write the for over key/value pairs explicitly [for k, v in aws_instance.web : v.id]
Null and Single-Value Behavior
Two splat forms exist. The modern full-splat [*] has a deliberate convenience: applied to a value that is not a list — a single object, or null — it wraps a non-null value in a one-element list and turns null into an empty list, which is what makes it safe over optional or zero-count resources. The legacy attribute-splat .* differs in where a trailing index or attribute applies — to the whole result rather than to each element — so prefer [*] in new code.
Common Uses
Splat earns its keep collecting a single field off a count fleet to feed something downstream: the IDs of subnets into a load-balancer's subnets, the private IPs of instances into a security-group rule, the ARNs of buckets into an IAM policy, the instance IDs into a target group. Anything richer than that one-field projection — filtering which instances, reshaping into a map — belongs in a for expression instead.
- Using
[*]on afor_eachresource — a map, not a list — and getting nothing useful; you needvalues(...)or afor. - Expecting splat to filter or transform beyond projecting one attribute, where a
forexpression is required. - Assuming the legacy
.*and modern[*]behave identically — they differ in where a trailing index applies: to the whole result for.*, per element for[*]. - Splatting a nested attribute path that does not uniformly exist across every element, producing nulls or an error.
- Forgetting that the splat result is a list, then trying to index it by a key as if it were a map.
- Use
[*].attrfor the simple "this field from all of them" case over acountresource. - Reach for a
forexpression when you need filtering, transformation, or to handle afor_eachmap. - Convert
for_eachresults withvalues(...)before splatting if you really want a list. - Keep splat to single-attribute projection; anything richer belongs in a
forexpression. - Prefer the modern
[*]form over the legacy.*, whose trailing index applies to the whole result rather than per element.
for expression
Knowledge Check
What does aws_instance.web[*].id produce?
- A list of the
idof every instance, in instance order - A map keyed by each instance's index, with the
idas the value - The
idof only the first instance - A single concatenated string of all the IDs
Why doesn't [*] work directly on a for_each resource?
for_eachproduces a map, not a list, so you usevalues(...)or aforinstead- Splat is feature-flagged off by Terraform whenever
for_eachis set anywhere on the resource block for_eachattributes are always known only after apply- Maps cannot expose computed attributes like
id
What is aws_instance.web[*].id shorthand for?
[for i in aws_instance.web : i.id]{ for i in aws_instance.web : i => i.id }lookup(aws_instance.web, "id")flatten(aws_instance.web.id)
You need only the instances whose tags.role is "api", projected to their IPs. What do you use?
- A
forexpression with anifclause — splat can't filter, only project - Splat with a trailing condition appended:
aws_instance.web[*].private_ip if ... aws_instance.web[*].tags.role[*].private_ipfilter(aws_instance.web[*], "api")
You got correct