How to Fix Terraform 'prevent_destroy' Blocking Resource Destruction
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: LOW (intentional safeguard, but blocks legitimate ops) | Time to Fix: 5 mins
TL;DR
- What broke: Terraform hit a
prevent_destroy = truelifecycle guard on a resource and hard-stopped the plan/apply, refusing to delete or replace it. - How to fix it: Remove or conditionally gate the
prevent_destroyflag in the resource'slifecycleblock, then re-runterraform applyorterraform destroy. - Use our Client-Side Sandbox above to paste your failing
.tfconfig and auto-refactor the lifecycle block without leaking your state or resource IDs.
The Incident (What Does the Error Mean?)
Raw error output from terraform plan or terraform destroy:
Error: Instance cannot be destroyed
on main.tf line 12, in resource "aws_db_instance" "primary":
12: resource "aws_db_instance" "primary" {
Resource aws_db_instance.primary has lifecycle.prevent_destroy set to true.
To allow this resource to be destroyed, remove the prevent_destroy lifecycle
configuration, or use the -target flag to target a different resource.
Immediate consequence: The entire terraform apply or terraform destroy run is aborted. If this resource is being replaced due to a forced replacement (e.g., changing an immutable argument like engine_version on RDS), your whole pipeline is dead until the flag is removed. This is a full stop — Terraform will not proceed with any downstream resources in the graph either.
The Attack Vector / Blast Radius
prevent_destroy is a deliberate safeguard, not a bug — but it becomes an operational hazard in three scenarios:
Forced replacement of immutable resources. Changing an argument that requires destroy-and-recreate (e.g.,
allocated_storagereduction on RDS,availability_zoneon an EC2 instance) will trigger this error even onterraform apply. Your deployment pipeline halts.Environment teardown in CI/CD. Ephemeral environments (PR previews, staging) that provisioned a protected resource can never be cleanly destroyed by automation. The pipeline fails, the environment leaks, and cloud costs accumulate.
State drift and import conflicts. If you import an existing resource into state and the config has
prevent_destroy = true, any future reconciliation that requires replacement is permanently blocked until a human intervenes.
Blast radius: At minimum, a blocked pipeline. At worst, leaked infrastructure running indefinitely in non-prod accounts because no automated teardown can complete.
How to Fix It (The Solution)
Basic Fix — Remove the Flag
Locate the lifecycle block on the offending resource and remove prevent_destroy or set it to false.
resource "aws_db_instance" "primary" {
identifier = "prod-postgres"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
lifecycle {
- prevent_destroy = true
+ prevent_destroy = false
}
}
After the change, run:
terraform apply # if replacing
# or
terraform destroy -target=aws_db_instance.primary
⚠️ For stateful resources (RDS, S3, EBS): Take a manual snapshot or enable final snapshot before proceeding. This flag existed for a reason.
Enterprise Best Practice — Environment-Scoped Guard via Variable
Hardcoding prevent_destroy = true in shared modules is the root cause of most pipeline failures. Gate it on a variable so production is protected and non-prod can be torn down freely.
variable "is_production" {
type = bool
default = false
}
resource "aws_db_instance" "primary" {
identifier = "prod-postgres"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
lifecycle {
- prevent_destroy = true
+ prevent_destroy = var.is_production
}
}
In your terraform.tfvars per workspace:
# production.tfvars
+is_production = true
# staging.tfvars
+is_production = false
This pattern lets CI/CD destroy staging freely while production retains the guard. Use Terraform workspaces or Terragrunt inputs to inject the correct value per environment.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov policy to flag hardcoded prevent_destroy = true in non-prod modules:
Checkov doesn't natively warn on prevent_destroy, but you can write a custom check or use OPA:
# opa/prevent_destroy_check.rego
package terraform.lifecycle
deny[msg] {
resource := input.resource_changes[_]
resource.change.before.lifecycle.prevent_destroy == true
not input.variables.is_production.value == true
msg := sprintf("Resource %v has prevent_destroy=true in a non-production context.", [resource.address])
}
2. Pre-plan hook in Atlantis / Terraform Cloud:
Add a pre_plan script that greps for hardcoded prevent_destroy = true in module sources and fails the run with a descriptive message if found outside of designated production module paths.
#!/bin/bash
# pre-plan-check.sh
if grep -r 'prevent_destroy\s*=\s*true' ./modules/shared/; then
echo "ERROR: Hardcoded prevent_destroy=true found in shared module. Use var.is_production instead."
exit 1
fi
3. Terraform Sentinel (HCP Terraform/Enterprise):
Use a Sentinel policy to enforce that prevent_destroy is always driven by a workspace variable, never a literal true.
4. Module contract documentation: Any module that manages stateful resources (RDS, ElastiCache, S3) must document its prevent_destroy behavior in its README.md and expose it as an input variable. Treat hardcoded prevent_destroy = true in a shared module as a bug, not a feature.