count Index Shift: Resources Destroyed and Recreated
count addresses instances by position. Three servers become
aws_instance.web[0], [1] and [2], and the state binds each real object to
one of those addresses.
Remove an element from the middle of the list and every later element shifts
down one index. The state still holds the same three objects at the same three
addresses, but each address now describes a different list element. Terraform
compares the object it has at [1] against what used to be element 2. Where the
shifted attributes force replacement, the plan is not one destroy:
# aws_instance.web[0] must be replaced
# aws_instance.web[1] must be replaced
# aws_instance.web[2] will be destroyed
Plan: 2 to add, 0 to change, 3 to destroy.
Only one instance was meant to leave. Three are going.
The remediation that makes it worse
Two responses cause the outage rather than avoid it.
Applying the shifted plan is the first. The destroys are real, and so is the loss of anything that lived on those instances.
Switching straight to for_each and applying is the second. That conversion
changes every address from an index to a key, so Terraform sees the whole set as
gone and a new set as needed. On its own it destroys and recreates every
instance in the collection, which is the same incident by a different route.
The safe remediation
Move the state to match the intended addresses first, then plan.
# READ-ONLY: record what the state currently holds
terraform state list | grep aws_instance.web
terraform state show 'aws_instance.web[0]'
Write down which real object should end up at which new address. In the example
above the first element left the list, so the object at [1] belongs at [0]
and the object at [2] belongs at [1]. Every destination is still occupied,
so park the departing object at an address the configuration does not declare:
terraform state mv 'aws_instance.web[0]' 'aws_instance.web_pending_removal'
Now each destination frees up in turn, lowest first:
terraform state mv 'aws_instance.web[1]' 'aws_instance.web[0]'
terraform state mv 'aws_instance.web[2]' 'aws_instance.web[1]'
Brackets are meaningful in most shells, so quote every address.
Re-plan and read the summary line before going any further:
terraform plan
The only action left should be the destroy of
aws_instance.web_pending_removal, which has no block in the configuration and
is the instance that was genuinely removed. If the plan still proposes a replace
on a surviving instance, one of the moves is wrong; correct the state rather
than approving the plan.
The durable fix
count gives identity by position, so any change to the middle of the list
repeats this incident. for_each with a stable key gives identity by key, and a
key does not move when a sibling is removed.
Pair the conversion with one moved block per instance in the same commit, so
Terraform treats each change as a rename rather than a replacement:
moved {
from = aws_instance.web[0]
to = aws_instance.web["api"]
}
moved {
from = aws_instance.web[1]
to = aws_instance.web["worker"]
}
moved blocks are not a substitute for the re-index above. Terraform reads a
sequence of moved blocks as one object’s move history, so a cascade of
index-to-index moves lands every object at the end of the chain. Use
terraform state mv to re-index within count, and moved blocks for the
one-to-one index-to-key mapping of a for_each conversion.
Plan again and confirm the summary reads no changes before the apply.