Skip to main content
Terraform Plan Wants to Recreate Your Production Database? Read This First

Terraform Plan Wants to Recreate Your Production Database? Read This First

KU
• 6 min read•2 views

The plan says -/+ next to your RDS instance and one attribute ends in "forces replacement". Why it happens, how to undo it without data loss, and the CI gate that stops it reaching apply.

Key takeaways

  • When terraform plan shows -/+ on a database, Terraform intends to destroy it and create a new empty one.
  • Find the attribute marked forces replacement.
  • Usually it's a changed immutable setting, a renamed resource address, or a provider upgrade.
  • Revert the attribute or add a moved block, never apply, then add prevent_destroy and a CI check that fails on deletes.

Don't apply. A -/+ next to a database in terraform plan means "destroy and then create replacement": Terraform will delete your database and create a new, empty one with the same configuration. Somewhere in that resource's diff, one attribute is annotated # forces replacement, and that attribute is the whole story.

The good news is that this is almost always fixable without touching the database. Everything below applies to Terraform 1.x and OpenTofu, with AWS provider examples current as of October 2026. Here's how to find the cause and make the plan boring again.

Find the attribute that forces replacement

In a long plan the marker is easy to miss. Save the plan and list every resource Terraform intends to delete:

bash.bash
$ terraform plan -out=tfplan
$ terraform show -json tfplan \
    | jq -r '.resource_changes[]
             | select(.change.actions | index("delete"))
             | "\(.address)  \(.change.actions | join(","))"'
aws_db_instance.orders  delete,create

Then look at that resource in the human-readable plan and search for forces replacement. The attribute it sits on tells you which of the causes below you're dealing with.

The four usual causes

CauseWhat the plan showsFix
Changed an immutable attribute# forces replacement on e.g. identifier, storage_encrypted, kms_key_id, availability_zoneRevert, or plan a real migration
Renamed or moved the resourceOne address destroyed, another createdmoved block
Attribute drifted outside TerraformReplacement on a field someone changed in the consoleRevert in console, or ignore_changes
Provider upgrade changed behaviorReplacement after only a version bumpRead the changelog, pin, adjust config

Immutable attributes. Some settings can't be changed on a live RDS instance, so the provider replaces the instance instead. Encrypting an unencrypted database is the classic case: setting storage_encrypted = true on an existing instance forces replacement. The real path for that change is snapshot, copy the snapshot with encryption, restore, and cut over, planned as a migration, not an apply.

Renames and refactors. Moving a resource into a module or renaming it changes its address. Terraform sees the old address as removed and the new one as added. A moved block tells Terraform it's the same object:

hcl.hcl
# main.tf
moved {
  from = aws_db_instance.orders
  to   = module.database.aws_db_instance.this
}

Run the plan again; the delete should become a no-op or an in-place update. For larger reshuffles, our guide to Terraform state migration covers state mv and cross-state moves.

Drift. If someone changed the database in the console, Terraform plans to put it back, and for some attributes "back" means replace. Decide which side is right. If the console change should stay, update the code to match. If a field is managed elsewhere on purpose, lifecycle { ignore_changes = [...] } tells Terraform to stop proposing updates to it. Our Terraform drift guide walks through deciding which side wins.

If someone already applied it

Then the old instance is being deleted, and what you can recover depends on two settings in your configuration. With skip_final_snapshot = false and a final_snapshot_identifier, RDS takes a final snapshot before deletion, and you restore from it. If delete_automated_backups was set to false, the automated backups are retained and you can restore to a point in time within the retention period. Restore to a new instance, verify the data, then point the application at it.

Check both settings today, before you need them. A skip_final_snapshot = true added to make a test environment easier to tear down has a way of being copied into production modules. AWS also documents that support may be able to recover an unintentionally deleted instance for up to six days, so open a case immediately.

Make it impossible to apply by accident

Once the plan is clean, add guardrails so the next one can't slip through:

hcl.hcl
resource "aws_db_instance" "orders" {
  # ...
  deletion_protection = true

  lifecycle {
    prevent_destroy = true
  }
}

prevent_destroy makes Terraform reject any plan that would destroy the resource. Know its limit: Terraform's documentation says it doesn't stop destruction if the resource's configuration is removed entirely. deletion_protection covers that gap at the AWS level, because the API refuses to delete a protected instance.

Then gate CI. Fail the pipeline when a plan deletes anything stateful, and require an explicit, reviewed override:

bash.bash
# ci/check-plan.sh: fail on any planned delete of a database or bucket
$ terraform show -json tfplan | jq -e '
    [.resource_changes[]
     | select(.change.actions | index("delete"))
     | select(.type | test("aws_db_instance|aws_rds_cluster|aws_s3_bucket"))]
    | length == 0' > /dev/null || { echo "Plan deletes stateful resources"; exit 1; }

Questions people ask

What does "forces replacement" mean in a Terraform plan?

It means the attribute changed can't be updated on the existing object, so Terraform will destroy the resource and create a new one, shown as -/+. For a database, that means a new, empty instance unless it's restored from a snapshot. Find the attribute marked # forces replacement and revert it or plan a real migration.

How do I rename a Terraform resource without destroying it?

Add a moved block with the old and new addresses, then run terraform plan. Terraform updates the state to the new address instead of destroying and recreating the object. For older Terraform versions, terraform state mv does the same in state directly.

Does prevent_destroy stop all deletions?

No. prevent_destroy = true makes Terraform reject plans that destroy the resource while the setting is in your configuration. If someone deletes the resource block itself, the protection goes with it. Use the provider's deletion protection, such as deletion_protection on RDS, as a second layer.

The decision, concretely

  • Plan shows -/+ on a database right now? Don't apply. Find forces replacement and revert that change.
  • You renamed or moved the resource into a module? Add a moved block.
  • The change is intentional, like enabling encryption? Plan a snapshot-restore migration with a cutover window.
  • No guardrails today? Add prevent_destroy, deletion_protection and the CI gate before the next refactor.

The call we'd make

Treat any -/+ on a stateful resource as a stop-the-line event in review. Nearly every case we've seen came from a refactor or a version bump, not an intentional change, and a moved block or a revert fixed it in minutes. The guardrails cost ten lines of code and turn a potential data-loss incident into a failed CI job.

Sources

React
Share this post
KU

Kiril Urbonas

AI Engineer

570 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.