for_each
The for_each meta-argument creates one resource instance for every element of a map or a set of strings. Unlike count, which keys instances by integer position, for_each keys them by a stable identifier you control. That single difference is what makes for_each the right tool whenever each instance has a meaningful identity—adding or removing one element never disturbs the others. for_each behaves identically in Terraform 1.5+ and OpenTofu.
Iterating over a map
When for_each receives a map, Terraform produces one instance per entry. Inside the block you reference the current element through two symbols: each.key is the map key, and each.value is the value associated with that key.
resource "aws_iam_user" "team" {
for_each = {
alice = "developer"
bob = "developer"
carol = "admin"
}
name = each.key
tags = {
Role = each.value
}
}
This declares three IAM users, each keyed by its username. A more realistic shape uses a map of objects so each instance can carry several attributes:
variable "buckets" {
type = map(object({
versioning = bool
acl = string
}))
default = {
"logs" = { versioning = false, acl = "private" }
"assets" = { versioning = true, acl = "public-read" }
}
}
resource "aws_s3_bucket" "this" {
for_each = var.buckets
bucket = "devcraftly-${each.key}"
}
resource "aws_s3_bucket_versioning" "this" {
for_each = var.buckets
bucket = aws_s3_bucket.this[each.key].id
versioning_configuration {
status = each.value.versioning ? "Enabled" : "Suspended"
}
}
Output:
Terraform will perform the following actions:
# aws_s3_bucket.this["assets"] will be created
# aws_s3_bucket.this["logs"] will be created
# aws_s3_bucket_versioning.this["assets"] will be created
# aws_s3_bucket_versioning.this["logs"] will be created
Plan: 4 to add, 0 to change, 0 to destroy.
Notice the addresses are keyed by string—aws_s3_bucket.this["assets"]—not by integer.
The keyed address: type.name[“key”]
A for_each resource is addressed as a map of instances. You access a single instance with a string subscript and the whole collection with values() or by iterating in an expression.
# One instance by key
output "assets_bucket_arn" {
value = aws_s3_bucket.this["assets"].arn
}
# All ARNs as a list
output "all_bucket_arns" {
value = [for b in aws_s3_bucket.this : b.arn]
}
# All instances as a map keyed by name
output "buckets_by_name" {
value = { for k, b in aws_s3_bucket.this : k => b.id }
}
Because the key is part of the address, references stay readable and intentional—aws_s3_bucket.this["logs"] says exactly which object you mean.
Why for_each beats count for stable identity
With count, instances live in an ordered list addressed by position. Remove the first element of a three-item list and every later element shifts down one index, so Terraform plans to destroy and recreate instances that did not actually change. for_each avoids this entirely: each instance is bound to its key, so removing one key only destroys that one instance and leaves the rest untouched.
| Aspect | count | for_each |
|---|---|---|
| Accepts | A whole number | A map or a set of strings |
| Instance key | Integer index ([0], [1]) | Stable string key (["assets"]) |
| Removing a middle element | Recreates everything after it | Destroys only that element |
| Current element | count.index | each.key, each.value |
| Reference | aws_x.y[0], aws_x.y[*] | aws_x.y["key"], values(aws_x.y) |
Removing
"logs"from the map above plans1 to destroy—justaws_s3_bucket.this["logs"]. The"assets"instance is never touched. The equivalentcount-based change would churn unrelated instances.
Converting lists to sets
for_each accepts a map or a set(string), but not a list—list ordering would reintroduce the positional fragility for_each exists to avoid. When you start from a list, convert it with toset(). With a set, each.key and each.value are equal (the set member itself).
variable "zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "private" {
for_each = toset(var.zones)
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = cidrsubnet("10.0.0.0/16", 8, index(var.zones, each.key))
tags = {
Name = "private-${each.key}"
}
}
Output:
# aws_subnet.private["us-east-1a"] will be created
# aws_subnet.private["us-east-1b"] will be created
# aws_subnet.private["us-east-1c"] will be created
Plan: 3 to add, 0 to change, 0 to destroy.
The set members feeding
for_eachmust be known at plan time. If they derive from another resource’s not-yet-created attributes (for example a generated ID), apply that resource first or restructure the keys around static values.
Best Practices
- Prefer
for_eachovercountwhenever instances have a meaningful, stable identity so additions and removals never churn the rest of the fleet. - Drive
for_eachfrom amap(object(...))when each instance needs several attributes—reach them througheach.value.attribute. - Convert lists with
toset()and use static, human-readable keys; never key off values that change between runs. - Keep
for_eachkeys known at plan time; if a key would come from a computed attribute, split the apply or rekey on stable input. - Reference instances by their explicit key (
resource.name["key"]) and expose collections withforexpressions orvalues()for clean, keyed outputs. - Remember
countandfor_eachare mutually exclusive on a single resource block—pick one per resource.