Initializing Enclave...

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 = true lifecycle 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_destroy flag in the resource's lifecycle block, then re-run terraform apply or terraform destroy.
  • Use our Client-Side Sandbox above to paste your failing .tf config 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:

  1. Forced replacement of immutable resources. Changing an argument that requires destroy-and-recreate (e.g., allocated_storage reduction on RDS, availability_zone on an EC2 instance) will trigger this error even on terraform apply. Your deployment pipeline halts.

  2. 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.

  3. 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.

Related Diagnostics

"Part of the Safety Utility Matrix."

View all 1 Safety Tools →