Terraform

Terraform for_each Meta-Argument: Map/Set-Based Resource Generation

2026-04-19
NicheeLab Editorial Team

Terraform's for_each is a meta-argument that stably generates multiple instances from a collection (mainly a map or set). On the exam, the differences from count, the meaning of each key, and the stability of instance addresses are common topics.

This article covers when to use map vs set generation in practice, common pitfalls, how to apply for_each to modules, and what to focus on for the exam. We stick to stable concepts grounded in the official documentation and avoid version-specific behavior.

for_each Basics and Exam Perspective

for_each accepts a map or a set (typically a set of strings) and produces one resource (or module) instance per element. Each instance is addressed by its key, which makes it resilient to reordering and prevents unintended diffs.

Inside the resource or module block, you can reference each.key and each.value. For maps, key is the map key and value is the corresponding value (which can also be an object). For sets, key and value are the same string. At the Associate level, expect questions on the differences vs count (addressing, stability), how to choose between map and set, and type conversions (toset, tomap, keys, etc.).

  • for_each accepts a map (recommended) or a set (of strings)
  • Each instance is addressed by a key, like resource.type.name["key"]
  • You can use each.key / each.value (including inside module call blocks)
  • Sets are unordered, but because key == value (a string), addresses tend to stay stable
  • count is index-based and brittle under reordering; if instances have names, for_each is the better choice

Minimal example: generating null_resource from a map

variable "services" {
  type = map(object({
    size = number
  }))
  default = {
    web = { size = 2 }
    api = { size = 1 }
  }
}

resource "null_resource" "svc" {
  for_each = var.services

  triggers = {
    name = each.key
    size = tostring(each.value.size)
  }
}

# Example addresses:
# null_resource.svc["web"], null_resource.svc["api"]

for_each with Maps: Stable Generation of Named Instances

Because maps carry keys, they are ideal for stably creating named instances. Putting an object in the value lets you pack per-instance settings into it. Additions and deletions are detected per key, so addresses do not wobble.

In practice, the standard pattern is to feed for_each a map(object(...)) keyed by an ID or logical name. A key change is treated as destroying the old key and creating a new one, so it is important to design keys that you will not casually change, to avoid unintended rollouts.

  • Keys are the single most important factor for determining instance addresses
  • Values can be objects; reference per-instance attributes via each.value
  • A key change is not a rename; it is treated as a destroy plus a new create

Mapping between map keys and resource addresses

var.instances (map)web = { cpu=2, mem=4 } / api = { cpu=1, mem=2 }null_resource.node["web"]null_resource.node["api"]Expanded by for_each

Concrete example using map(object)

variable "instances" {
  type = map(object({
    cpu = number
    mem = number
    tags = map(string)
  }))
}

resource "null_resource" "node" {
  for_each = var.instances

  triggers = {
    name = each.key
    cpu  = tostring(each.value.cpu)
    mem  = tostring(each.value.mem)
  }
}

# Reference example: null_resource.node["web"].id

for_each with Sets: Existence-Flag-Style Generation

A set is an unordered collection of strings. It works well when all you need to express is whether a given name exists. Because each instance's address is based on its element string, addresses stay stable as long as the set membership does not change. Duplicates are removed automatically.

Sets do not carry value information, so if you need to attach attributes, use a map instead, or resolve the extra data through another data source. Note that converting a list with toset loses the original ordering.

  • each.key and each.value are equal; both are the element string
  • Duplicates are absorbed; ordering is undefined
  • If you need detailed attributes, switch to map(object)

Example with a set of strings

locals {
  names = toset(["web", "api", "web"])  # duplicate "web" is automatically removed
}

resource "null_resource" "role" {
  for_each = local.names

  triggers = {
    name = each.key  # == each.value
  }
}

# Reference example: null_resource.role["api"].id

count vs for_each: Comparison and Selection Criteria

count is simple and powerful, but because it depends on indexes, reordering or inserting/deleting items in the middle easily causes instance churn. If you can manage by name, for_each gives you stability.

On the Associate exam, common questions ask which one to use and why diffs are more stable with for_each.

  • Use for_each when you can name instances, and count when you simply need more of them
  • Migrating from existing count requires careful key design and use of moved blocks or state mv
  • Attributes that a set cannot carry should be expressed with map(object)
Itemcountfor_each (map)for_each (set)
Instance addressNumeric index such as resource.name[0]resource.name["key"]resource.name["value"]
Diff stabilityBrittle under reordering (churn is common)Stable as long as keys do not changeStable as long as set membership does not change
Expressiveness of valuesAssigning per-instance attributes is somewhat verbosevalue can hold an objectCannot hold a value (key == value string)
Typical use caseN identical copiesMultiple named instances with individual settingsExistence-flag-style presence management
Behavior on key changeN/A (index-based)Destroy old key + create new keyRemove old element + add new element

Safe migration from count to for_each (example)

# Old: created with count (brittle under reordering)
# resource "null_resource" "srv" {
#   count = length(var.names)
#   triggers = { name = var.names[count.index] }
# }

# New: convert to a map and use for_each (stable)
locals {
  names_map = { for n in var.names : n => { name = n } }
}
resource "null_resource" "srv" {
  for_each = local.names_map
  triggers = { name = each.key }
}

# If the mapping between existing resources and keys changes,
# use terraform state mv or a moved block to make the mapping
# explicit and migrate without downtime.

for_each on Modules and Design Patterns

You can also apply for_each to a module call. Inside the calling module block, reference each.key / each.value and pass them as inputs to the submodule. Inside the submodule itself, each is not available, so receive values via regular variables.

Managing a fleet of apps with a map(object) — using a logical name as the key and a configuration object as the value — is the most stable pattern in practice. Outputs are referenced as module.block["key"].output.

  • Use each on the caller side; inside the submodule, reference variables instead
  • It is important to fix keys to logical names and design them to stay invariant
  • Reference outputs as module.x["key"].out

for_each on a module call

variable "apps" {
  type = map(object({
    image = string
    replicas = number
  }))
}

module "app" {
  source  = "./modules/app"
  for_each = var.apps

  name     = each.key
  image    = each.value.image
  replicas = each.value.replicas
}

# Reference example: module.app["frontend"].endpoint

# modules/app/variables.tf
# variable "name" { type = string }
# variable "image" { type = string }
# variable "replicas" { type = number }

Common Pitfalls and Debugging Essentials

A key change triggers resource replacement. Pick keys that are immutable identifiers (logical names or IDs), and keep mutable information like display names inside the value object. Converting a list with toset loses ordering, so do not mix in logic that assumes a specific order.

When refactors require mapping existing addresses to new keys, migrate manually with terraform state mv or place a moved block in the configuration to prevent unplanned destruction. Use terraform plan to inspect diffs and terraform state list to check addresses.

  • Keep keys immutable; push mutable information into each.value
  • Sets dedupe and are unordered; do not assume order-dependent processing
  • During refactors, migrate safely using state mv or moved blocks
  • Apply filters at the comprehension stage when building the map (using an if clause)

Filtering and key-design example

locals {
  # Limit to apps on a stable version, keyed by logical name
  filtered = {
    for name, cfg in var.apps :
    name => cfg
    if startswith(cfg.version, "stable-")
  }
}

module "app" {
  source   = "./modules/app"
  for_each = local.filtered
  name     = each.key
  version  = each.value.version
}

# Debugging:
# terraform plan
# terraform state list | grep module.app

Check Your Understanding

Associate

問題 1

You want to stably manage multiple security rules (a collection of objects with id, from_port, and to_port). Even if the rule ordering changes in the future, you want to avoid unnecessary destroy/recreate operations. Which approach is most appropriate?

  1. Build a map(object(...)) keyed by the rule id and use for_each to generate instances
  2. Use count = length(var.rules) and reference each rule via count.index
  3. Convert rules to set(object(...)) and pass it to for_each
  4. Add depends_on to prevent ordering from affecting the result

正解: A

By passing a map(object) to for_each and keying it by id, each instance's address is pinned to its key, which prevents churn when the order changes. count is index-based and brittle under reordering. set(object) is not supported because the key cannot be uniquely determined. depends_on declares dependency order and does not solve address stability.

Frequently Asked Questions

Can I use count and for_each on the same resource or module at the same time?

No. You can only specify one of them. Use for_each when the instances have names or per-instance attributes, and use count when you simply need N identical copies.

What happens to ordering when you pass a set (converted with toset) to for_each?

Sets are unordered. The for_each key is the element string itself (== the value), so as long as the set members do not change, the addresses stay stable, but there is no concept of reordering. Duplicates are removed automatically.

When I use for_each on a module call, can I reference each.key / each.value inside the submodule?

No. each is only visible inside the calling module block. Inside the submodule, receive values via normal variables and, if needed, pass the contents of each.value from the caller as arguments.

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.