Chapter 5: Iteration and Conditionals
Topic 34

Splat Expressions

HCL

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.

collect every instance's private IP
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.

for_each is a map — use values() or a for
# 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.

Common Mistakes
  • Using [*] on a for_each resource — a map, not a list — and getting nothing useful; you need values(...) or a for.
  • Expecting splat to filter or transform beyond projecting one attribute, where a for expression 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.
Best Practices
  • Use [*].attr for the simple "this field from all of them" case over a count resource.
  • Reach for a for expression when you need filtering, transformation, or to handle a for_each map.
  • Convert for_each results with values(...) before splatting if you really want a list.
  • Keep splat to single-attribute projection; anything richer belongs in a for expression.
  • Prefer the modern [*] form over the legacy .*, whose trailing index applies to the whole result rather than per element.
Comparable tools Pulumi host-language list comprehensions CloudFormation no equivalent Terraform splat is a convenience over its own for expression

Knowledge Check

What does aws_instance.web[*].id produce?

  • A list of the id of every instance, in instance order
  • A map keyed by each instance's index, with the id as the value
  • The id of only the first instance
  • A single concatenated string of all the IDs

Why doesn't [*] work directly on a for_each resource?

  • for_each produces a map, not a list, so you use values(...) or a for instead
  • Splat is feature-flagged off by Terraform whenever for_each is set anywhere on the resource block
  • for_each attributes 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 for expression with an if clause — splat can't filter, only project
  • Splat with a trailing condition appended: aws_instance.web[*].private_ip if ...
  • aws_instance.web[*].tags.role[*].private_ip
  • filter(aws_instance.web[*], "api")

You got correct