for Expressions
A for expression takes one collection and produces another, applying a transformation to every element along the way. It is HCL’s equivalent of map/filter/comprehension from general-purpose languages, and it is the tool you reach for whenever the shape of your data does not quite match what a resource argument or a for_each meta-argument expects. Unlike the for_each and count meta-arguments, a for expression does not create resources — it builds a value. Everything here behaves identically in Terraform 1.5+ and OpenTofu, since both share the HCL2 grammar.
Building a list
Wrapping a for expression in square brackets produces a tuple (a list). You name a temporary variable for each element, then write the expression after the colon that computes the output element.
variable "names" {
type = list(string)
default = ["web", "api", "worker"]
}
locals {
upper_names = [for name in var.names : upper(name)]
prefixed = [for name in var.names : "svc-${name}"]
}
output "prefixed" {
value = local.prefixed
}
Output:
Changes to Outputs:
+ prefixed = [
+ "svc-web",
+ "svc-api",
+ "svc-worker",
]
When iterating a list, the single loop variable is the element value. When iterating a map or object, you may declare two variables — the key and the value.
variable "ports" {
type = map(number)
default = {
http = 80
https = 443
}
}
locals {
port_labels = [for name, port in var.ports : "${name}:${port}"]
}
This yields ["http:80", "https:443"]. The result of a list for expression is always a tuple, even when the source is a map.
Building a map
Wrapping a for expression in curly braces produces an object (a map). You must supply two result expressions separated by the => arrow: the key on the left and the value on the right.
variable "users" {
type = list(object({
name = string
role = string
}))
default = [
{ name = "alice", role = "admin" },
{ name = "bob", role = "viewer" },
]
}
locals {
roles_by_user = { for u in var.users : u.name => u.role }
}
output "roles_by_user" {
value = local.roles_by_user
}
Output:
Changes to Outputs:
+ roles_by_user = {
+ alice = "admin"
+ bob = "viewer"
}
Map keys must be unique. If two iterations produce the same key Terraform raises an error, unless you group the values — append ... after the value expression to collect colliding entries into a list.
locals {
users_by_role = { for u in var.users : u.role => u.name... }
}
This produces { admin = ["alice"], viewer = ["bob"] }, grouping every user under their role.
Filtering with if
Append an if clause to keep only the elements for which a condition is true. The condition is evaluated against the current loop variables, so filtering composes naturally with transformation.
variable "instances" {
type = list(object({
name = string
enabled = bool
}))
default = [
{ name = "blue", enabled = true },
{ name = "green", enabled = false },
{ name = "red", enabled = true },
]
}
locals {
active = [for i in var.instances : i.name if i.enabled]
}
The result is ["blue", "red"]. Combine if with a map projection to drop disabled records entirely while reshaping what remains.
Feeding for_each and count
The most common production use of a for expression is preparing data for for_each. Because for_each requires a map (or a set of strings) with stable, known keys, you typically convert a list of objects into a keyed map first.
variable "buckets" {
type = list(object({
name = string
versioning = bool
}))
default = [
{ name = "logs-prod", versioning = true },
{ name = "assets", versioning = false },
]
}
resource "aws_s3_bucket" "this" {
for_each = { for b in var.buckets : b.name => b }
bucket = each.value.name
}
resource "aws_s3_bucket_versioning" "this" {
for_each = { for b in var.buckets : b.name => b if b.versioning }
bucket = aws_s3_bucket.this[each.key].id
versioning_configuration {
status = "Enabled"
}
}
Output:
# aws_s3_bucket.this["assets"] will be created
# aws_s3_bucket.this["logs-prod"] will be created
# aws_s3_bucket_versioning.this["logs-prod"] will be created
Plan: 3 to add, 0 to change, 0 to destroy.
Note how the if b.versioning filter means only logs-prod gets a versioning resource, while both buckets are created.
Warning: Use a stable, meaningful attribute as the
for_eachkey, notcount.indexor list position. If you key by index and later remove an element, every subsequent resource shifts and Terraform plans a destroy/recreate cascade.
Ordering and the result type
A list for expression over a map iterates keys in lexical order, giving deterministic output. A list for produces a tuple; a map for produces an object. If a resource argument requires a set, wrap the result with the toset function.
| Form | Result type | Use when |
|---|---|---|
[for x in c : expr] | tuple/list | Transforming or filtering into a list |
[for k, v in c : expr] | tuple/list | Projecting map entries into a list |
{for x in c : k => v} | object/map | Keying records for for_each |
{for x in c : k => v...} | object/map | Grouping colliding keys into lists |
Best Practices
- Convert lists of objects into maps with a
forexpression before passing them tofor_each, keying on a stable, unique attribute. - Apply
iffilters inside theforexpression rather than building the full collection and pruning it afterward. - Keep complex
forexpressions inlocalsso the transformation is named, reviewable, and reused. - Use the grouping form (
...) instead of manual merge logic when several elements legitimately share a key. - Wrap results with
tosetwhen an argument expects a set, and remember a mapforalready deduplicates by key. - Avoid keying
for_eachmaps by list index — use real identifiers so removing one element does not reshuffle the rest.