Terraform

Terraform for Expressions: Build Lists & Maps for Real Work and the Associate Exam

2026-04-19
NicheeLab Editorial Team

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.

Basic Syntax and Evaluation Model

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.

  • Lists use [ ... ] and maps use { ... } — do not mix up the brackets on the exam
  • Place if at the end (to the right of the colon). Elements that fail the condition are omitted from the output
  • Index when iterating a list: for i, v in var.list
  • Building a map fails with a Duplicate key error if keys collide
  • for expressions only reshape values. Use resource-level for_each to create multiple resources
FeaturePrimary useOutput / effectFilterable?
for expressionReshape 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 elementReturns a listNo (handle conditions separately)
for_each (meta-argument)Create multiple resources or modulesMaterializes multiple resourcesIndirectly (pre-shape the map/list)

Flow of a for expression

Input collectionlist / map / setfor element[, index]Transform expression (expr)(optional) if conditionOutputlist or map

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 }

Building and Transforming Lists in Practice

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.

  • Use for i, v in var.list when you need the index
  • Drop empty strings with if length(v) > 0, drop nulls with if v != null
  • Tame nesting with a double for plus flatten()

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 }

Building Maps and Reshaping Keys/Values

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.

  • Key collisions fail with Duplicate key. Always design keys to be unique
  • When filtering, put if at the end (do not embed it inside the expression)
  • When building a map from a list, decide up front which field acts as the primary key

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 }

Filter Conditions and Handling null / unknown

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.

  • if decides whether an element is kept. Lists close up automatically (they never become sparse)
  • Add if v != null when you want to drop nulls
  • When unknown is involved, the full output is not finalized at plan time

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 }

Using for Expressions in dynamic Blocks and Module Inputs

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.

  • Keep the roles separate: for expressions reshape, for_each expands
  • Pass a list or map to dynamic for_each. Map keys can be used as address specifiers
  • Finish filtering with if before handing the data to dynamic — it makes the logic clearer

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]
    }
  }
}

Associate Exam Pitfalls and Checkpoints

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.

  • [ ... ] is a list, { ... } is a map — do not confuse them
  • Get a list index via for i, v in var.list
  • When building a map, confirm keys are unique. Pre-process with distinct() if needed
  • Doubly-nested lists → [for x in xs : [for y in x : ...]] + flatten()
  • values(map) gives the list of values, keys(map) gives the list of keys

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 }

Check Your Understanding

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 }

  1. A. ["WEB", "DB", "CACHE"]
  2. B. ["0-WEB", "1-DB", "3-CACHE"]
  3. C. ["0-WEB", "1-DB", "2-", "3-CACHE"]
  4. D. { 0 = "WEB", 1 = "DB", 3 = "CACHE" }

正解: 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.

Frequently Asked Questions

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.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

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.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.