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 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.).
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"]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.
Mapping between map keys and resource addresses
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"].idA 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.
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"].idcount 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.
| Item | count | for_each (map) | for_each (set) |
|---|---|---|---|
| Instance address | Numeric index such as resource.name[0] | resource.name["key"] | resource.name["value"] |
| Diff stability | Brittle under reordering (churn is common) | Stable as long as keys do not change | Stable as long as set membership does not change |
| Expressiveness of values | Assigning per-instance attributes is somewhat verbose | value can hold an object | Cannot hold a value (key == value string) |
| Typical use case | N identical copies | Multiple named instances with individual settings | Existence-flag-style presence management |
| Behavior on key change | N/A (index-based) | Destroy old key + create new key | Remove 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.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.
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 }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.
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.appAssociate
問題 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?
正解: 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.
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.
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...