Skip to main content
RunBook Academy

TerraformV · The Terraform WorkflowProduction Terraform

terraform destroy: A Production-Dangerous Operation

Intermediate⏱ ~12 minbash

What you'll learn

  • Treat terraform destroy as the production-dangerous operation it is, with explicit pre-conditions
  • Use lifecycle.prevent_destroy as a per-resource guard against accidental destruction
  • Use terraform plan -destroy and -target to perform surgical destruction safely
  • Configure a CI policy that prevents destroy in production without an out-of-band approval
  • Plan the recovery path for the four most common destroy incidents, including data loss

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

terraform destroy is terraform apply with the inverse plan. It deletes every resource Terraform manages in the working directory, in reverse-dependency order, in a single operation. It is the only command in the workflow whose cost is measured in data loss, not in minutes. The lesson is not how to use it. The lesson is how to keep it from being the wrong tool for the moment.

What destroy does

terraform destroy is exactly equivalent to:

terraform plan -destroy
terraform apply

Internally, the CLI computes a plan where every resource is marked for destruction. The plan walks the dependency graph in reverse: resources with no dependents first, then their dependencies, then their dependencies, until the root of the graph is gone.

Phase 1: Refresh state from real world
Phase 2: Build dependency graph
Phase 3: Compute the destroy plan (every resource -> -)
Phase 4: Print the plan to stdout
Phase 5: [Prompt for confirmation unless -auto-approve]
Phase 6: Walk graph in reverse-dependency order, calling provider APIs
Phase 7: Empty the state file (all resources marked as destroyed)
Phase 8: Release state lock

For a 100-resource configuration, destroy takes the same 3-10 minutes that a full apply takes. The provider sees the same number of delete calls. The state file is left in a state where every resource is marked as destroyed but the state structure remains, ready for a fresh apply if the resources are recreated.

The argument forms

# Default: prompt for confirmation, then destroy everything.
terraform destroy

# Destroy a saved destroy-plan file. Skip re-plan and prompt.
terraform destroy tfplan.destroy

# Skip confirmation. CI-only and dangerous.
terraform destroy -auto-approve

# Destroy only one resource and its dependencies.
terraform destroy -target=aws_instance.legacy

# Skip interactive variable prompts.
terraform destroy -input=false

The same -out pattern applies: terraform plan -destroy -out=tfplan.destroy saves a binary destroy plan. terraform destroy tfplan.destroy consumes it without re-planning. This is the production pattern: review a destroy plan in a pull request, save it, and apply it later with explicit approval.

The per-resource guard: lifecycle.prevent_destroy

The lifecycle block has a meta-argument that stops a resource from being destroyed:

resource "aws_s3_bucket" "prod_logs" {
  bucket = "acme-prod-logs-2026"
  # ... configuration ...

  lifecycle {
    prevent_destroy = true
  }
}

With prevent_destroy = true, any plan that would destroy the resource — including a terraform destroy of the entire configuration, an apply that removes the block, or a -target=aws_s3_bucket.prod_logs destroy — fails with:

Error: Instance cannot be destroyed

  on main.tf line 12:
  12: resource "aws_s3_bucket" "prod_logs" {

Resource aws_s3_bucket.prod_logs has lifecycle.prevent_destroy set,
so the resource cannot be destroyed. Run "terraform destroy" without
the target argument to first remove the lifecycle block, then
re-run destroy. Alternatively, edit the state to remove the resource.

The flag exists for one reason: to force a deliberate, out-of-band decision. Removing the block from the configuration is the only way to destroy the resource through the normal workflow.

The right replacement for prevent_destroy

prevent_destroy has limits. It blocks destroy of one specific resource. For broader protection, layer three additional controls:

  1. Versioning + MFA delete on S3 buckets. The bucket itself can be deleted, but the objects survive and can be restored. This is the cloud-provider safety net for S3.

  2. Final snapshot on RDS. Set final_snapshot_identifier and skip_final_snapshot = false on aws_db_instance. Destroy creates a snapshot before deleting the instance. The snapshot lives until you explicitly remove it.

  3. Backup policies in the cloud account. AWS Backup, Azure Backup, GCP snapshot schedules. These are account-level controls, not Terraform-level, and they protect against destruction by any means — Terraform, console, malicious actor.

The hierarchy: prevent_destroy makes destruction reviewable; the cloud-provider safety nets make destruction reversible; the account-level backups make destruction cheap.

Surgical destroy with -target

For destroying a single resource without touching the rest of the configuration, -target is the right tool:

# Destroy one specific resource and its dependencies.
terraform destroy -target=aws_instance.legacy

# Save the destroy plan first, then apply it.
terraform plan -destroy -target=aws_instance.legacy -out=instance.tfplan.destroy
terraform apply instance.tfplan.destroy

The same caveat as -target for plan and apply applies here: -target truncates the dependency graph. Resources that the targeted resource depends on are also destroyed. If aws_instance.legacy has a security group and an IAM role attached, those are destroyed too if nothing else uses them.

The rule: surgical destroy is for resources whose dependency closure is small and well-known. For anything with cross-cutting dependencies, prefer removing the resource block from the configuration and running a normal plan.

The CI policy for destroy

terraform destroy in CI is a code smell. The right policy:

Production CI: never run terraform destroy in the apply job.
                Destruction in production happens by:
                1. Removing the resource block from configuration
                2. Opening a PR with the removal
                3. Running terraform plan in the PR (shows the destroy)
                4. Requiring two human approvals
                5. Merging and applying through the normal pipeline

Dev/staging CI: destroy may run automatically, but only against
                ephemeral environments that are recreated on every deploy.
                Example: a PR preview environment is destroyed when the
                PR is closed.

Sandbox:       destroy may run freely. Backups are not required.
                Use this for learning and for testing new providers.

The production rule is the inverse of the apply rule. Apply runs through a platform-level approval gate. Destroy runs through a code-review approval gate: the resource block has been deleted from the file, the PR shows the destroy plan, and the team has reviewed and approved. There is no command-line terraform destroy in production CI.

The cost of a runaway destroy

A runaway destroy is when terraform destroy runs against an environment it was not intended for. Three common causes:

  1. Wrong working directory. Operator runs destroy in prod/ when they meant dev/. Both directories share the same backend bucket but different key prefixes. The wrong state file is loaded.

  2. Wrong backend. Operator’s backend.hcl points at the production bucket. Destroy in dev wipes production.

  3. No -target, wrong intent. Operator intended to destroy one resource. The flag was forgotten. The full configuration is destroyed.

The blast radius of each:

  • Wrong working directory: recoverable. State can be inspected; the cloud may have lost all managed resources.
  • Wrong backend: recoverable if backups exist. State can be restored from the right backend, then the lost resources recreated.
  • No -target: recoverable if backups exist and the team has time. Otherwise: total data loss for resources without snapshots.

The lesson: always pass -target and a specific resource address. If you cannot name the resource you intend to destroy, do not destroy.

Production failure modes

1. terraform destroy runs in CI for prod without -target. Symptom: the entire production environment is destroyed. Cause: a CI script was copied from dev without changing the working directory. Recovery: this is the worst case. Restore state from backup; restore resources from the last known-good state. Communicate to stakeholders; do not attempt re-apply until the state is verified.

2. prevent_destroy = true blocks an unrelated apply. Symptom: an apply that should update a security group fails with “Instance cannot be destroyed” pointing at an S3 bucket the operator does not intend to touch. Cause: changing the bucket’s argument set in a way that forces replacement. Recovery: investigate the planned change. If replacement is genuinely required, remove prevent_destroy in a separate, explicit PR and re-apply.

3. IAM credentials allow destroy too broadly. Symptom: a compromised CI token can call terraform destroy against any environment. Cause: the apply IAM role includes s3:DeleteBucket, rds:DeleteDBInstance, and dynamodb:DeleteTable for resources it does not legitimately need to destroy. Recovery: narrow the role. Use separate roles for apply and for the rare destroy operations; grant destroy permission only when explicitly invoked.

4. Destroy of a stateful resource without backup. Symptom: RDS database is destroyed; the data is gone; no snapshot was taken. Cause: skip_final_snapshot = true was set in the configuration, or the destroy ran before the backup policy completed. Recovery: this is data loss. Restore from the most recent automated backup if one exists. If not, the lesson is to never let skip_final_snapshot reach production for stateful resources.

5. -target destroy of a resource with implicit dependencies. Symptom: destroying one EC2 instance also destroys the IAM role and security group that other instances depend on. Cause: -target includes the dependency closure; if the dependent resources have no other users, they are destroyed too. Recovery: read the plan carefully. Use -target only when the dependency closure is small and well-understood. Otherwise, remove the resource block and apply.

6. Destroy plan file handed off to wrong environment. Symptom: a destroy plan saved in dev is applied in production. Cause: the artefact bucket is shared across environments and the wrong file was downloaded. Recovery: this is a configuration-management failure as much as a Terraform failure. Use environment-specific artefact buckets; name files with the environment prefix; require human approval for production destroy.

Recovery and rollback

Recovery from destroy depends on what was destroyed and whether backups exist. The decision tree:

Was the destroyed resource backed up?
├── Yes (RDS snapshot, EBS snapshot, S3 versioning): restore from backup
│   └── Recreate the resource via terraform apply; restore data from the backup
└── No: data is gone
    └── Communicate; investigate why the backup policy failed; add the policy

State recovery is independent of resource recovery. The state file can be restored from the state backend’s version history (S3 has object versioning; GCS has object versioning; Terraform Cloud has state versions). The state is restored, then terraform plan shows the resources that need to be recreated, then terraform apply recreates them.

If the destroy also deleted the state file from the backend: this is rare with managed backends. The state file lives in the bucket; destroying the resource does not delete the state. Verify with terraform state list — even an empty state file is a state file.

Security implications

Destroy is the highest-privilege operation in the workflow. The IAM role that can destroy production can delete every resource in the production account. Mitigations:

  • A dedicated destroy role, separated from the apply role. The apply role cannot destroy; the destroy role can. Both are narrowly scoped.
  • Approval required to assume the destroy role. The role lives behind break-glass procedures or scheduled windows.
  • Cloud-provider-level protections: S3 MFA delete, RDS deletion protection (deletion_protection = true), DynamoDB point-in-time recovery.
  • Audit logging on the state backend and on the cloud-provider audit trail (CloudTrail, Cloud Audit Logs).

The destruction of production data is not a Terraform problem. It is an access-control problem that Terraform executes efficiently. Solve it at the access-control layer.

Performance implications

Destroy is bounded by provider delete API latency. For a 100-resource configuration, destroy takes 3-10 minutes. The state lock is held the entire time. Long destroys in a CI environment can hold the state lock past the job timeout, leaving the lock dangling.

Mitigation: explicit destroy plans for large estates, applied outside peak hours. Do not couple destroy to a CI job with a 30-minute timeout; use a runbook that runs the destroy manually with a longer timeout and human supervision.

Verification

# Always preview the destroy first.
terraform plan -destroy

# Save the destroy plan, review it, then apply it.
terraform plan -destroy -out=tfplan.destroy
terraform show tfplan.destroy | head -50

# For surgical destroy, verify the dependency closure is what you expect.
terraform plan -destroy -target=aws_instance.legacy

# Confirm state is empty after destroy (for full destroy).
terraform state list
# Should print nothing.

A healthy destroy run starts with a plan, ends with an empty state list, and has produced backups of every stateful resource before the API call. Anything else is operator error waiting to become an incident.

What comes next

The next lesson covers terraform output and terraform show — how to expose state to operators, how to hand values between pipeline stages, and how to keep sensitive values out of logs.

Knowledge check · 7 questions

  1. Q1. What does lifecycle.prevent_destroy = true do?

  2. Q2. A production destroy belongs in a pull request that removes the resource block, so a reviewer reads the resulting plan rather than approving a CI job that runs terraform destroy -auto-approve.

  3. Q3. You need to destroy a single EC2 instance without touching the rest of the configuration. Which command is correct?

  4. Q4. Which cloud-provider-level controls are the right complement to lifecycle.prevent_destroy? (Select all that apply.)

  5. Q5. An apply is failing with 'Instance cannot be destroyed' pointing at an S3 bucket the operator is not trying to touch. What is happening?

  6. Q6. What is the correct recovery path after a runaway destroy against production?

  7. Q7. An operator runs terraform destroy against a staging environment. The staging RDS instance is configured with skip_final_snapshot = true. The destroy succeeds. Two days later, the team needs a copy of a table that was in that database. What is the situation?

Passing score: 75%. Answers are checked in this browser.