Terraform For Expressions

A baker takes a tray of plain dough, shapes each piece, and places the results on a new tray. A for expression performs the same job on data. It loops over a list or a map, transforms every item, and produces a new list or map. For expressions reshape data without extra resources, which keeps configurations short and readable.

The Basic Idea

Diagram: Input Tray to Output Tray

  Input list                 Rule                 Output list
 +----------+       +-----------------+       +-----------+
 | "web"    |       |                 |       | "WEB"     |
 | "api"    | ----> |  upper(item)    | ----> | "API"     |
 | "db"     |       |                 |       | "DB"      |
 +----------+       +-----------------+       +-----------+

Building a List

Square brackets produce a list. The pattern reads: for each item in the source, produce this value.

variable "names" {
  default = ["web", "api", "db"]
}

locals {
  upper_names = [for n in var.names : upper(n)]
}

The result is ["WEB", "API", "DB"].

Using the Index

Add a second name before the item to receive its position.

locals {
  numbered = [for i, n in var.names : "${i}-${n}"]
}

The result is ["0-web", "1-api", "2-db"].

Building a Map

Curly braces produce a map. The arrow => separates each key from its value.

locals {
  name_lengths = { for n in var.names : n => length(n) }
}

The result is { web = 3, api = 3, db = 2 }.

Looping Over an Existing Map

Receive the key and value together.

variable "servers" {
  default = {
    web = "t3.micro"
    api = "t3.small"
  }
}

locals {
  labels = { for name, size in var.servers : name => "${name}-${size}" }
}

The result is { web = "web-t3.micro", api = "api-t3.small" }.

Filtering with if

Add an if clause at the end to keep only the items that pass a test.

variable "users" {
  default = [
    { name = "alice", active = true },
    { name = "bob",   active = false },
    { name = "carol", active = true }
  ]
}

locals {
  active_users = [for u in var.users : u.name if u.active]
}

The result is ["alice", "carol"].

Diagram: Filter in Action

  alice (active)    ---- passes ---->  alice
  bob   (inactive)  ---- blocked  --X
  carol (active)    ---- passes ---->  carol

Splat Expressions: A Shortcut

When you only need one attribute from every item, the splat symbol [*] saves typing.

aws_instance.web[*].id

# Same result with a for expression
[for inst in aws_instance.web : inst.id]

Splat handles simple cases. A for expression handles filtering and renaming.

Grouping Results

Add three dots after a map value to group items that share a key.

locals {
  fruits = [
    { name = "apple",  color = "red" },
    { name = "cherry", color = "red" },
    { name = "banana", color = "yellow" }
  ]

  by_color = { for f in local.fruits : f.color => f.name... }
}

The result is { red = ["apple", "cherry"], yellow = ["banana"] }.

A Practical Example: Tags for Many Buckets

variable "buckets" {
  default = ["logs", "images", "backups"]
}

resource "aws_s3_bucket" "this" {
  for_each = toset(var.buckets)
  bucket   = "shop-${each.key}-2026"
}

output "bucket_arns" {
  value = { for name, b in aws_s3_bucket.this : name => b.arn }
}

The output shows each bucket name beside its ARN. The for expression builds the map from the created resources.

Square Brackets Versus Curly Braces

WrapperOutputNeeds Arrow
[ for ... ]ListNo
{ for ... }MapYes, key => value

Key Points

  • A for expression transforms one collection into a new list or map.
  • Square brackets build lists, and curly braces build maps with =>.
  • Add an if clause at the end to filter items.
  • Use two loop variables to read an index or a key and value together.
  • The splat symbol [*] collects one attribute from every item.
  • Three dots after a map value group items under a shared key.