Skip to main content
RunBook Academy

← All break/fix scenarios in Terraform

advancedterraform-plan~25 min

count Index Shift: Resources Destroyed and Recreated

Reported symptoms

  • ●The plan destroys and recreates many resources
  • ●The count.index shifted
  • ●The resource identity changed

Evidence

  • · The plan shows the change
  • · The resource addresses changed
  • · The count changed
Diagnosis and resolutionclick to reveal

Root cause

count addresses instances by position. Remove an element from the middle of the list and every later element shifts down one index, so the object Terraform already tracks at aws_instance.web[1] is compared against what used to be element 2. Where the shifted attributes force replacement, each shifted instance is destroyed and recreated, and the highest index is destroyed outright. This is the count antipattern.

Remediation

1. Do not apply the shifted plan; the destroy and recreate it proposes is the incident, not the fix. 2. Record what the state holds with terraform state list and terraform state show, and decide which real object belongs at each new address. 3. Park the object that is genuinely leaving at an address the configuration does not declare, so its slot is free. 4. Re-index the surviving objects onto their new addresses with terraform state mv, taking the lowest free destination first. 5. Re-plan and confirm the only remaining action is the destroy of the parked object. 6. Apply. The durable fix is for_each with a stable key, and that conversion needs one moved block per instance in the same commit, or it destroys and recreates the estate by itself.

Verification

Every surviving object keeps the ID it had before the change, the only destroy in the plan is the instance that was genuinely removed, and the incident is documented.

Prevention

1. Key collections with for_each and a stable key rather than count, so identity comes from the key and not from the position. 2. Pair any count-to-for_each conversion with moved blocks in the same commit. 3. Read the add, change and destroy counts in the plan summary before approving. 4. Rehearse list changes in a development workspace first.

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.