Prefer for_each Over count

count keys resources by position, so deleting the middle one reshuffles everything. for_each keys by a stable name.

This is the refactor that has saved me from the most 2am destroys. count creates resources indexed by number: [0], [1], [2]. Remove the item in the middle of the list and every resource after it shifts down an index — Terraform reads that as "destroy and recreate several things."

Why for_each wins

for_each keys each instance by a stable string, like aws_iam_user.team["alice"]. Remove bob and only bob is destroyed; alice and carol never move.

Rule of thumb

  • Use count only for a simple on/off toggle: count = var.enabled ? 1 : 0.
  • Use for_each for any collection of distinct, named things.

Example

Example · hcl
# Fragile: removing "bob" shifts every later index
variable "users" {
  default = ["alice", "bob", "carol"]
}
resource "aws_iam_user" "count_based" {
  count = length(var.users)
  name  = var.users[count.index]  # aws_iam_user.count_based[1]
}

# Robust: each user keyed by a stable name
resource "aws_iam_user" "each_based" {
  for_each = toset(var.users)
  name     = each.value          # aws_iam_user.each_based["bob"]
}

When to use it

  • A team switches from count to for_each on IAM users so removing a user from the middle of the list does not trigger recreation of all subsequent users.
  • An engineer uses for_each with a map of subnet configurations keyed by AZ name so adding a new AZ creates only that new subnet without touching existing ones.
  • A platform team migrates ten count-based resources to for_each to fix a recurring CI problem where index reshuffling caused unexpected destroys on unrelated resources.

More examples

count index reshuffling problem

Demonstrates why count with a list is fragile: removing an element reshuffles all higher indexes, causing unnecessary destroys.

Example · bash
# count with a list: ["alice", "bob", "carol"]
# aws_iam_user.user[0] = alice
# aws_iam_user.user[1] = bob
# aws_iam_user.user[2] = carol

# Remove "bob" -> list becomes ["alice", "carol"]
# aws_iam_user.user[1] now = carol
# Terraform DESTROYS bob AND carol, recreates carol at index 1

# With for_each keyed by name: only bob is destroyed

for_each with stable keys

Uses for_each with a set so each user is keyed by name; removing one user destroys only that user without affecting others.

Example · hcl
variable "iam_users" {
  type    = set(string)
  default = ["alice", "bob", "carol"]
}

resource "aws_iam_user" "team" {
  for_each = var.iam_users
  name     = each.key
}

# Remove "bob" from the set:
# Only aws_iam_user.team["bob"] is destroyed
# alice and carol are untouched

Migrate count to for_each safely

Shows the terraform state mv commands needed to migrate three count-indexed subnet resources to for_each-keyed resources without recreation.

Example · bash
# Before: resource "aws_subnet" "public" { count = 3 }
# After:  resource "aws_subnet" "public" { for_each = var.subnets_map }

# Move each indexed resource to a named key
terraform state mv 'aws_subnet.public[0]' 'aws_subnet.public["us-east-1a"]'
terraform state mv 'aws_subnet.public[1]' 'aws_subnet.public["us-east-1b"]'
terraform state mv 'aws_subnet.public[2]' 'aws_subnet.public["us-east-1c"]'

# Plan should now show 0 changes
terraform plan

Discussion

  • Be the first to comment on this lesson.