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
Stuck on this Terraform error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
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.
Fixed it? Get 500 Terraform & 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.
Stuck on this? Start guided troubleshooting
Open an interactive diagnostic session with this error already loaded. Work a step-by-step plan, record what each check returns, land on a root cause, and export a clean incident summary — no account needed to start.
Did this fix your issue?
Solved it a different way?
Share the fix that worked for you — reviewed, then published to help the next engineer.
That looks like it may contain a secret (key, token, password, or connection string). Please remove it — a note with a detected secret can’t be published.
Thanks — that helps. Published notes appear after a quick review.
Trending errors this week
The error guides other engineers are actually reading right now.
- 1mount: wrong fs type, bad option, bad superblock
- 2Docker 'failed to set up container networking': Fix the Bridge and IP Pool
- 3Docker 'failed to create shim task': How to Fix the containerd Runtime Error
- 4Transport endpoint is not connected
- 5modprobe: FATAL: Module not found
- 6mount: wrong fs type, bad option, bad superblock
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.