Terraform for expressions are a way to transform collection types (list/map/set/tuple/object) into a different shape. They are evaluated as HCL expressions and reshape input or intermediate data rather than increasing the number of resources.
On the Associate exam, common topics include the syntax difference between building lists and maps, filtering with if, working with indexes, behavior on duplicate keys, and how a for expression differs in role from for_each. This article organizes the official, stable behavior in a form you can apply directly at work.
A for expression walks a collection and produces a new collection. Use square brackets to build a list and curly braces to build a map. Append an if clause to filter elements. Evaluation happens at the expression level and does not affect the number of resources created.
The canonical forms are: build a list with [for v in xs : EXPR if COND], build a map with { for k, v in m : NEW_K => NEW_V if COND }. You can also include the index when iterating a list, as in [for i, v in xs : ...]. Output order is stable for lists; map key order is unspecified, but pulling values via values() returns a list with a defined order.
| Feature | Primary use | Output / effect | Filterable? |
|---|---|---|---|
| for expression | Reshape input data (build/transform list or map) | Returns a new collection (resource count does not change) | Yes (via if) |
| Splat operator (x.*.attr) | Concisely extract the same attribute from each element | Returns a list | No (handle conditions separately) |
| for_each (meta-argument) | Create multiple resources or modules | Materializes multiple resources | Indirectly (pre-shape the map/list) |
Flow of a for expression
Basic examples (list transform, map build, filter)
variable "names" { type = list(string) }
locals {
upper_list = [for n in var.names : upper(n)]
name_len = { for n in var.names : n => length(n) }
only_long = [for n in var.names : n if length(n) > 3]
}
output "upper_list" { value = local.upper_list }
output "name_len" { value = local.name_len }
output "only_long" { value = local.only_long }To produce a list from another list, start with [for v in list : EXPR] and reach for [for i, v in list : ...] when you need the index. Combine with if to drop entries such as empty strings.
To flatten nested lists, expand the inner level inside a for expression and then apply flatten() at the end. Preserving order while transforming is one of the strengths of for expressions.
Shaping lists (index access, flattening)
variable "roles" { type = list(string) default = ["web", "db", "", "cache"] }
locals {
upper_roles = [for i, r in var.roles : "${i}-${upper(r)}" if length(r) > 0]
ports_nested = [[80, 443], [8080], []]
ports_flat = flatten([for ps in local.ports_nested : [for p in ps : p]])
}
output "upper_roles" { value = local.upper_roles }
output "ports_flat" { value = local.ports_flat }Build maps with { for k, v in m : NEW_K => NEW_V }. The usual playbook is to normalize keys and values (trimspace, lower/upper, replace) and drop pairs that fail the condition. Because key collisions raise a plan-time error, pre-deduplicate with distinct() or rework your key design.
To turn a list into a map, pick a field as the key, as in { for o in list : o.name => o.id }. Remember that values(map) returns just the values as a list, which you can feed into further list transforms.
Normalizing maps and converting list to map
variable "tags" { type = map(string) }
variable "servers" {
type = list(object({ name = string, id = string }))
}
locals {
normalized_tags = {
for k, v in var.tags : lower(trimspace(k)) => trimspace(v)
if v != null && trimspace(v) != ""
}
server_map = { for s in var.servers : s.name => s.id }
server_ids_doubled = [for id in values(local.server_map) : "srv-${id}"]
}
output "normalized_tags" { value = local.normalized_tags }
output "server_map" { value = local.server_map }
output "server_ids_doubled" { value = local.server_ids_doubled }The if clause in a for expression decides whether an element is included in the output. Elements that evaluate to false are simply not produced, so the list closes up and the map drops the key entirely. nulls are not removed automatically, so include them in the condition explicitly when you need to.
When unknown values are present at plan time, the result of the if can also be unknown. In that case, the final filtering is resolved at apply time and the plan shows "(known after apply)". At the Associate level, understanding this evaluation timing concept is enough.
Filters that account for null and unknown
variable "users" {
type = list(object({ name = string, active = bool }))
}
locals {
active_names = [for u in var.users : u.name if try(u.active, false)]
non_empty = [for s in ["a", "", "b", null] : s if s != null && s != ""]
kept_nulls = [for s in ["a", "", "b", null] : s] # Without a condition, null is kept
}
output "active_names" { value = local.active_names }
output "non_empty" { value = local.non_empty }
output "kept_nulls" { value = local.kept_nulls }Keep for expressions focused on shaping input data, and leave creating or destroying resources to the for_each on resource/module. For dynamic blocks, build the list/map you pass to for_each ahead of time with a for expression — it is the safer pattern.
In real work, normalize structured data (security group rules, tags, labels, IAM policy statements) with a for expression and then expand it via dynamic / for_each. This pattern is highly repeatable.
Shape with for, expand with dynamic (example: SG rules)
variable "ingress_rules" {
type = list(object({ port = number, cidr = string }))
}
locals {
ingress_norm = [for r in var.ingress_rules : {
port = r.port
cidr = trimspace(r.cidr)
} if r.cidr != null && trimspace(r.cidr) != ""]
}
resource "aws_security_group" "example" {
name = "example"
description = "example"
vpc_id = "vpc-xxxxxxxx"
dynamic "ingress" {
for_each = local.ingress_norm
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [ingress.value.cidr]
}
}
}
Common exam topics: the [ ... ] vs { ... } syntax difference, the position of if, retrieving the index, Duplicate key, using values() / keys() together, and combining flatten with a double for. Key collisions when building a map are a classic trick question.
The splat operator is concise but cannot do conditional shaping or build keys. Remember the rule of thumb: reach for a for expression whenever you need filtering or non-trivial reshaping.
Safe patterns (avoiding duplicates, extracting keys/values)
locals {
# Avoid key collisions (deduplicate first, then build the map)
uniq = distinct(["a", "b", "a"]) # => ["a", "b"]
m = { for v in local.uniq : v => upper(v) } # Keys are unique
# Transform the value list of a map
doubled = [for v in values({ a = 1, b = 2 }) : v * 2] # => [2, 4]
# Build a map from two lists with zipmap
keys_ = ["env", "app"]
vals_ = ["prod", "api"]
labels = zipmap(keys_, vals_) # => { env = "prod", app = "api" }
}
output "m" { value = local.m }
output "doubled" { value = local.doubled }
output "labels" { value = local.labels }Associate
問題 1
Which is the correct evaluation result of the following for expression? variable "roles" { type = list(string) default = ["web", "db", "", "cache"] } locals { upper_roles = [for i, r in var.roles : "${i}-${upper(r)}" if length(r) > 0] } output "upper_roles" { value = local.upper_roles }
正解: B
Because the list-building for expression is [for i, r in ... : ... if length(r) > 0], empty strings are filtered out. The output is a list whose elements take the form "index-UPPERCASED", giving ["0-WEB", "1-DB", "3-CACHE"]. The result is not a map, so D is incorrect.
What is the difference between a for expression and resource for_each?
A for expression is an expression that produces or transforms a value (list/map, etc.) and does not increase the number of resources. for_each is a meta-argument that replicates a resource (or module) based on the map/list you pass in. The for expression shapes data, while for_each expands resources.
How do I use the index when iterating over a list?
For a list, add a second identifier as in [for i, v in var.list : EXPR] and i will hold the 0-based index. For a map, [for k, v in var.map : ...] gives you the key in k.
What happens if keys collide when building a map?
Duplicate keys raise a Duplicate key error during planning. Avoid it by pre-deduplicating with distinct(), reworking your key design, or appending an index or namespace to make keys unique.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...