Terraform for_each vs count: Choosing the Right One
Pick the wrong iteration construct and a single list change destroys and recreates half your resources. Here's when to use for_each, when count is fine, and why.
- #terraform
- #for_each
- #count
- #iteration
- #hcl
- #devops
Few Terraform decisions cause more accidental destruction than choosing count when you should have used for_each. I’ve watched a team remove one item from the middle of a list and trigger Terraform to destroy and recreate every resource after it. In 25 years, this is one of the most common self-inflicted outages I see. Understanding the difference isn’t academic — it’s how you avoid wrecking production with a one-line edit.
Here’s the rule, and the reasoning behind it.
How each one tracks state
The core difference is how Terraform addresses the instances it creates:
countindexes by position:aws_instance.web[0],[1],[2].for_eachkeys by a stable identifier:aws_instance.web["api"],["worker"].
That addressing is what gets stored in state, and it’s everything.
Why count is a foot-gun for sets of things
Consider three buckets created with count over a list:
resource "aws_s3_bucket" "data" {
count = length(var.bucket_names)
bucket = var.bucket_names[count.index]
}
If var.bucket_names is ["logs", "events", "archive"], you get indices 0, 1, 2. Now remove "events" from the middle. The list becomes ["logs", "archive"]. Index 1 was events, now it’s archive. Index 2 disappears. Terraform’s plan:
- destroy aws_s3_bucket.data[1] # was events
~ update aws_s3_bucket.data[1] # now archive
- destroy aws_s3_bucket.data[2]
It destroys and recreates the wrong things because positions shifted. For named, independent resources, count over a list is a trap.
for_each keys by identity
The same thing with for_each over a set keeps each resource pinned to a stable key:
resource "aws_s3_bucket" "data" {
for_each = toset(var.bucket_names)
bucket = each.value
}
Now the addresses are data["logs"], data["events"], data["archive"]. Remove "events" and Terraform destroys exactly one resource — data["events"] — and leaves the others completely untouched. The key is identity, not position, so the plan does precisely what you meant.
The rule I follow
It’s simple:
- Use
for_eachfor a set or map of distinct, named things: buckets, IAM users, DNS records, subnets per AZ. Anything where each item has its own identity and removing one shouldn’t disturb the rest. - Use
countfor true on/off toggles or N identical, interchangeable copies where position genuinely doesn’t matter.
# Good use of count: conditional resource
resource "aws_cloudwatch_alarm" "prod_only" {
count = var.environment == "prod" ? 1 : 0
}
That conditional-create pattern is the one place count shines. For collections, reach for for_each by default.
for_each over a map for richer config
When each instance needs different settings, for_each over a map is the clean pattern:
resource "aws_instance" "node" {
for_each = var.nodes # { api = { type = "t3.large" }, ... }
instance_type = each.value.type
tags = { Name = each.key }
}
Each node is keyed by name and carries its own config. Add or remove a key and only that instance changes.
Migrating count to for_each safely
If you already have count-based resources and want to switch, don’t just edit and apply — that re-indexes everything. Use moved blocks to remap each instance from its index to its new key:
moved {
from = aws_s3_bucket.data[0]
to = aws_s3_bucket.data["logs"]
}
This tells Terraform the resource didn’t change, only its address did. No destroy, no recreate.
Where AI helps
AI is genuinely useful here for two things. First, generating the moved blocks for a count-to-for_each migration — paste the ordered list and the target keys, and it maps every index. Second, reviewing a plan: “Does this count-based resource risk re-indexing if the list changes?” It catches the foot-gun before you ship it. I keep these in my Terraform prompts and run migration PRs through our Code Review tool.
The takeaway
count indexes by position, for_each keys by identity — and that difference decides whether editing a list quietly recreates half your infrastructure. Use for_each for collections of named things, reserve count for conditional creates and truly interchangeable copies, and use moved blocks when you migrate. Get this one right and your plans always do exactly what you intended.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.