Skip to content
Infrastructure as Code iac resources 4 min read

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.

Aspectcountfor_each
AcceptsA whole numberA map or a set of strings
Instance keyInteger index ([0], [1])Stable string key (["assets"])
Removing a middle elementRecreates everything after itDestroys only that element
Current elementcount.indexeach.key, each.value
Referenceaws_x.y[0], aws_x.y[*]aws_x.y["key"], values(aws_x.y)

Removing "logs" from the map above plans 1 to destroy—just aws_s3_bucket.this["logs"]. The "assets" instance is never touched. The equivalent count-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_each must 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_each over count whenever instances have a meaningful, stable identity so additions and removals never churn the rest of the fleet.
  • Drive for_each from a map(object(...)) when each instance needs several attributes—reach them through each.value.attribute.
  • Convert lists with toset() and use static, human-readable keys; never key off values that change between runs.
  • Keep for_each keys 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 with for expressions or values() for clean, keyed outputs.
  • Remember count and for_each are mutually exclusive on a single resource block—pick one per resource.
Last updated June 14, 2026
Was this helpful?