Terraform Error Guide: '# forces replacement' — Stop Accidental Resource Destroys
Understand Terraform's '# forces replacement' plan diff: find the immutable attribute driving destroy-and-create, and use lifecycle, ignore_changes, moved blocks and -target to avoid data loss.
- #terraform
- #iac
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
”# forces replacement” is not strictly an error — it is a warning inside a terraform plan that can quietly destroy production data if you apply it without reading closely. Terraform prints it next to an attribute whose change cannot be done in place, so the resource must be destroyed and recreated. The plan header switches from ~ update to -/+ destroy and then create:
# aws_db_instance.main must be replaced
-/+ resource "aws_db_instance" "main" {
~ engine_version = "15.4" -> "16.1" # forces replacement
~ id = "db-ABC123" -> (known after apply)
# (18 unchanged attributes hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
The # forces replacement comment marks the exact attribute responsible. Some attributes are immutable in the underlying API — you cannot change a database’s engine on the fly, or a subnet’s CIDR — so Terraform’s only option is to replace the whole resource. Applying this on a stateful resource means the old instance (and its data) is deleted.
Symptoms
terraform planshows-/+or “must be replaced” and a non-zero “to destroy” count you did not expect.- One attribute line carries the
# forces replacementcomment. - A trivial-looking change (a tag scheme, a name, an AZ) triggers a full recreate.
- CI plans surface a destroy on a database, volume, or stateful resource during an otherwise routine change.
Common Root Causes
- An immutable attribute changed — engine version, availability zone, subnet, name, or another field the provider cannot update in place.
- A resource was renamed or moved in code, so Terraform sees the old address destroyed and a new one created rather than a rename.
- A computed or provider-defaulted value drifted, nudging an immutable attribute.
- A module refactor that changed a resource’s key inside a
for_each/count, changing its address. - An intentional but risky change (e.g. resizing to a type that requires recreation) applied without safeguards.
Diagnostic Workflow
Read the plan carefully and isolate exactly which attribute forces the replacement. Save the plan and grep for the marker so it cannot hide among unchanged attributes:
terraform plan -out=tfplan
terraform show tfplan | grep -B2 'forces replacement'
For a machine-readable view, render the plan as JSON and inspect the replace reasons — this tells you the resource address and the attributes triggering replacement without visual scanning:
terraform show -json tfplan | \
jq '.resource_changes[] | select(.change.actions == ["delete","create"])
| {address, replace_paths: .change.replace_paths}'
Confirm what is currently in state for that resource so you understand what would be destroyed:
terraform state show aws_db_instance.main
If the “replacement” is really just a rename or a moved resource, prefer a moved block or a state move over a destroy/create. A moved block records the rename in code so no resource is actually replaced:
moved {
from = aws_db_instance.database
to = aws_db_instance.main
}
The imperative equivalent, when you must fix state directly:
terraform state mv aws_db_instance.database aws_db_instance.main
Example Root Cause Analysis
A pull request that only meant to tweak tags produced Plan: 1 to add, 0 to change, 1 to destroy on a production RDS instance. The team nearly approved it before reading the diff.
terraform show tfplan | grep -B2 'forces replacement' revealed the culprit was not the tags at all — an unrelated edit had bumped engine_version from 15.4 to 16.1, and RDS treats a major-version change on that path as immutable in this configuration, forcing replacement. The replace_paths in the JSON plan confirmed engine_version was the sole trigger. Destroying the instance would have deleted the database.
Because the version bump was eventually wanted but not in this PR, the immediate fix was to revert engine_version in code so the plan showed only the intended tag update. For the later, deliberate upgrade, the team planned a safe path using create_before_destroy so a replacement could never leave a window with no database, and reviewed the RDS upgrade procedure (snapshot first) rather than relying on Terraform to recreate blindly:
resource "aws_db_instance" "main" {
# ...
lifecycle {
create_before_destroy = true
# ignore drift on an attribute the provider recomputes:
ignore_changes = [latest_restorable_time]
}
}
Prevention Best Practices
- Always read the “to destroy” count and every
# forces replacementline before approving a plan, especially in CI where it scrolls past easily. - Use
lifecycle { create_before_destroy = true }on resources where a brief absence is unacceptable, so the new resource exists before the old is removed. - Use
ignore_changesfor attributes that drift outside Terraform (autoscaling-set sizes, provider-recomputed fields) to avoid needless replacements. - Use
movedblocks (orterraform state mv) when refactoring addresses so a rename is never interpreted as destroy-and-create. - Scope risky applies with
-targetto review one resource at a time, and take a snapshot/backup before replacing any stateful resource. - Add a CI gate that fails the pipeline when a plan proposes to destroy a protected resource, or set
prevent_destroy = trueon critical ones.
Quick Command Reference
terraform plan -out=tfplan # capture the plan
terraform show tfplan | grep -B2 'forces replacement' # find the trigger
terraform show -json tfplan | jq '.resource_changes[]' # replace_paths per resource
terraform state show <address> # what would be destroyed
terraform state mv <old> <new> # fix a rename without recreate
# lifecycle { create_before_destroy = true; prevent_destroy = true }
Conclusion
”# forces replacement” is Terraform being honest that an immutable attribute changed and the resource must be recreated — harmless on a stateless resource, catastrophic on a database. Treat the “to destroy” count as a stop sign: grep the plan for the marker, confirm which attribute is responsible, and decide deliberately. Use moved blocks for renames, create_before_destroy and prevent_destroy for safety, and always snapshot stateful resources first. For more IaC fixes, see the Terraform guides.
Get 500 Battle-Tested DevOps AI Prompts — Free
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.