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
countonly for a simple on/off toggle:count = var.enabled ? 1 : 0. - Use
for_eachfor any collection of distinct, named things.
Example
# 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.
# 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 destroyedfor_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.
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 untouchedMigrate 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.
# 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