You add one server to the middle of a list, run terraform plan, and Terraform announces it will destroy and recreate three resources you never touched. Nothing about them changed. This is the count trap, and it has caused more 2 a.m. incidents than almost any other Terraform footgun I know.
Why a list index is a landmine
When you build resources with count, Terraform addresses them by position: aws_instance.node[0], [1], [2]. That index is the resource’s identity in state. Remove or insert an element anywhere but the end and every index after it shifts — so [1] now points at what used to be [2]. Terraform doesn’t see “the list got shorter.” It sees “the thing at index 1 is a different thing now,” and the only way it knows to reconcile that is destroy-and-recreate.
# The trap: identity is the position in the list
variable "nodes" {
default = ["web-a", "web-b", "web-c"]
}
resource "aws_instance" "node" {
count = length(var.nodes)
tags = { Name = var.nodes[count.index] }
}
# Remove "web-a" and web-b and web-c each shift down one index —
# Terraform destroys and recreates BOTH to "fix" the mismatch.
The fix: address resources by a stable key, not a position
for_each keys each resource by a string you control instead of an ordinal Terraform controls. Delete one entry and the others keep their identity, because their identity was never their position.
# The fix: identity is a stable key
variable "nodes" {
default = ["web-a", "web-b", "web-c"]
}
resource "aws_instance" "node" {
for_each = toset(var.nodes)
tags = { Name = each.key }
}
# The address is now aws_instance.node["web-b"].
# Remove "web-a" and ONLY "web-a" is destroyed. The rest don't move.
If you’re already on count
Migrating isn’t rewrite-and-pray. Move the state entries to their new keyed addresses so Terraform keeps the existing resources instead of replacing them:
terraform state mv 'aws_instance.node[1]' 'aws_instance.node["web-b"]'
terraform state mv 'aws_instance.node[2]' 'aws_instance.node["web-c"]'
# Then swap count for for_each in the config and plan — it should show no changes.
Or, on Terraform 1.1+, declare the intent as code with a moved block and let the plan do the reshuffle for you. Either way, confirm the plan shows zero replacements before you apply. That plan output is the entire safety mechanism — read it.
The lesson
Use count only for genuinely anonymous, interchangeable replicas you’ll only ever scale from the end — three identical workers, nothing addressed individually. The moment a resource has a name, an identity, or a lifecycle of its own, key it with for_each. In infrastructure as code, how you address a thing is part of what the thing is. Get the identity wrong and Terraform will faithfully destroy production to make the map match the territory.
More field notes and the full examples are on GitHub: github.com/waghmaredb/vexpose-labs. Hit a stranger Terraform footgun than this one? Trade war stories on LinkedIn or X.
Leave a Reply