What Terraform lifecycle prevent_destroy actually locks down

Terraform State Management Best Practices Infrastructure as Code

What is Terraform lifecycle prevent_destroy?

prevent_destroy is a boolean argument inside the lifecycle meta-argument, the lifecycle block any resource can carry, and it stops Terraform from destroying critical resources by refusing any plan that would destroy them, including a plain terraform destroy. Set it to true and Terraform errors out instead, which makes it the simplest guard against accidental deletion and the data loss that follows from accidentally destroying a database. It's a Terraform-side safety net, not a change to the resource itself, and it only works when the resource block and its lifecycle rule are in your configuration.

Basic Terraform lifecycle prevent_destroy syntax

The argument lives inside a lifecycle block, nested inside the resource it protects:

resource "aws_instance" "checkout_api" {
ami = "ami-0123456789abcdef0" # placeholder, replace with a current AMI ID for your region
instance_type = "m6i.large"
lifecycle {
prevent_destroy = true
}
}

There's no separate configuration file and no provider-specific setup, just a lifecycle block added to whichever resource block needs it.

In Terraform, the value has to be a literal true or false, never an expression, a variable, or a conditional. The restriction comes from when Terraform evaluates lifecycle settings, which is during dependency graph construction, before it has resolved any input variables or local values.

ignore_changes is limited for the same reason. By the time a variable’s value is known, the graph has already been built.

What Terraform lifecycle prevent_destroy actually does

Terraform applies the check once it knows what a planned operation will do to a resource. If the plan calls for destroying a resource that carries the guard, whether that's a Terraform destroy or a configuration change that forces a replacement because an attribute changed in a way the provider can't update in place, Terraform stops and raises an error instead of proceeding.

Nothing in the real infrastructure moves. Any resource deletion Terraform attempts against a protected resource fails at the plan, and whoever ran it has to decide whether to try again.

prevent_destroy has no opinion on in-place updates. A change that Terraform can apply without the destruction and recreation of the resource goes through exactly as it would otherwise.

It's also enforced only at plan time: it governs what Terraform itself is willing to propose, not what AWS, GCP, or Azure will accept through their own APIs. Someone with console access can still delete the underlying database or bucket directly, and prevent_destroy will never know. Also, the rule itself isn't durable.

Terraform doesn't record most lifecycle settings to state (create_before_destroy is the one exception), so the protection exists as long as the resource block defining it is still present in your configuration. Delete the block, and there's no rule left for Terraform to evaluate.

The literal-value restriction traces back to when Terraform evaluates lifecycle settings, during dependency graph construction, before it resolves input variables or local values.

prevent_destroy has to be a fixed true or false for the same reason ignore_changes can't take a conditional expression. By the time Terraform would have evaluated a variable's value, graph construction has already happened.

prevent_destroy is best used for critical infrastructure (anything expensive, slow, or impossible to reproduce if it’s destroyed by accident), for example:

  • Production databases
  • Encryption keys
  • DNS zones
  • Core networking components

It's less a formal rule and more because recreating a misconfigured web server costs minutes, while recreating a production database costs its data.

A Terraform lifecycle prevent_destroy example in action

Say a team runs its primary application database through Terraform:

resource "aws_db_instance" "orders" {
identifier = "orders-production"
engine = "postgres"
engine_version = "16"
instance_class = "db.r6g.large"
allocated_storage = 200
username = "app_user"
manage_master_user_password = true
lifecycle {
prevent_destroy = true
}
}

If anyone runs terraform destroy against this state, whether in full or with -target=aws_db_instance.orders, Terraform refuses before touching anything:

╷
│ Error: Instance cannot be destroyed
│
│ on main.tf line 1:
│ 1: resource "aws_db_instance" "orders" {
│
│ Resource aws_db_instance.orders has lifecycle.prevent_destroy set, but
│ the plan calls for this resource to be destroyed. To avoid this error
│ and continue with the plan, either disable lifecycle.prevent_destroy
│ or reduce the scope of the plan using the -target flag.
╵

The same guard trips for a change not intended to be destructive. Someone edits engine from postgres to a different value to test a migration path locally, then accidentally commits it.

Terraform detects that the engine change can't be applied in place (the provider has to destroy and recreate the instance to change it), so the plan now calls for destroying aws_db_instance.orders on the way to replacing it, and the same error fires.

Setting create_before_destroy doesn’t change that. It only alters the order in which Terraform replaces a resource, so Terraform creates the new resource first and destroys the old resource afterwards, which helps with avoiding downtime. Destroying the old object is still part of the plan, though, so prevent_destroy blocks the replacement either way.

Nobody has to remember to protect important resources against that specific mistake separately. The resource is protected against every path that ends in destruction, intentional or not, for as long as the lifecycle rule remains in the configuration.

Common mistakes users make with Terraform lifecycle prevent_destroy

Don't assume the protection survives the entire deletion of the resource block

It doesn't.

If someone removes the whole aws_db_instance.orders block (not just the lifecycle block inside it) from the configuration, Terraform sees a resource that no longer exists in code and plans to destroy the real infrastructure on the next apply, prevent_destroy or not, because there's no rule left anywhere for it to check.

If the goal is to walk away from Terraform management without touching the resource, that's a job for a removed block (or the older terraform state rm command), not deleting the resource and hoping the guard holds.

Don't try to make the setting conditional

Something like prevent_destroy = var.environment == "production" looks reasonable and fails during validation, since the value has to be known before Terraform resolves variables.

The workaround is to define two versions of the resource, one with the lifecycle rule and one without, selecting between them with count.

Don't treat prevent_destroy as if it blocks every kind of change

You'll be surprised when an in-place update (a storage increase or an instance class bump) sails through without a prompt.

It was never meant to gate updates, only destruction.

Don't forget related resources

If you protect a database instance but leave its subnet group, parameter group, or attached security group unprotected, you could be allowing real damage. The destruction of the subnet group out from under a protected database can break it just as thoroughly as the destruction of the database directly. Prevent_destroy on one resource says nothing about its neighbors.

Don’t expect it to cover anything outside a Terraform plan

The check only runs while Terraform is building a plan, so anything that never produces one is invisible to it. A resource deleted through the console, the cloud CLI, or a direct API call is gone before Terraform hears about it, and a hand-edited state file can drop a resource from Terraform’s view without any check firing at all.

Alternatives to Terraform lifecycle prevent_destroy

Provider-native deletion protection covers the gap prevent_destroy can’t close on its own, and it works alongside the Terraform setting rather than competing with it. RDS instances and clusters expose their own deletion_protection argument, and most major clouds offer an equivalent on their stateful resource types.

The two sound alike, but prevent_destroy only governs what Terraform will propose, while deletion_protection governs what the AWS API will accept from any tool, including a click in the console.

Setting both means Terraform refuses to plan the destruction and AWS refuses to carry one out, which is a materially stronger guarantee than either control alone.

A different need often gets mistaken for this one. Taking a resource out of Terraform’s management without destroying the underlying infrastructure is a job for a removed block with a nested lifecycle block setting destroy = false.

The older terraform state rm command still works, but it gives no preview of what it’s about to do.

Neither replaces prevent_destroy. Stopping management of a resource and guaranteeing it is never destroyed are different goals, even though teams often reach for prevent_destroy to cover both.

Stategraph and Terraform lifecycle prevent_destroy

prevent_destroy only ever evaluates the plan running inside the one state that defines the resource. That's correct, deliberate scoping, not an oversight.

Friction shows up once a resource important enough to carry the guard, a shared production database or a core VPC, is also consumed elsewhere through terraform_remote_state, by teams with no visibility into whether the owning state's lifecycle rule is even still in place.

Those consuming states have no equivalent check of their own; prevent_destroy protects the resource from its own state's plans, with no comment on what depends on it from outside.

Blast radius analysis that spans state boundaries closes that gap. Pointed at a protected resource, it surfaces every dependent in every state Stategraph manages, not just the ones visible from inside the state that owns it, before anyone decides to lift the guard.

With Stategraph, lifting prevent_destroy for a reviewed, deliberate replacement stops being a leap of faith. Cross-state plans put the replacement and every downstream change into one plan and commit them as a single transaction, and resource-level locking holds only the resources involved, so every other team keeps working while the replacement goes through.

Try Stategraph free to see what a graph that doesn’t stop at a single state’s edge adds when prevent_destroy is only part of the picture.

Terraform lifecycle prevent_destroy FAQs

When should I enable prevent_destroy on a resource?

Use it when accidental destruction would be expensive, slow, or impossible to reverse: production databases, managed encryption keys, DNS zones, and shared networking components are the usual candidates.

It's less useful on anything cheap or fast to recreate, as the extra friction of lifting the guard for legitimate changes starts to outweigh the protection it buys.

Does OpenTofu support prevent_destroy the same way?

Mostly. OpenTofu forked Terraform’s codebase, so the syntax and the scope limited to resource blocks are the same. The exception is the literal-value restriction, which OpenTofu lifted in v1.12.0.

In OpenTofu, prevent_destroy can now reference other values in the same module, such as an input variable, so a single module can protect a database in production and leave it unprotected in development.

In Terraform, something like prevent_destroy = var.environment == "production" looks reasonable and fails during validation, as the value has to be known before Terraform resolves variables. OpenTofu 1.12.0 and later accept references to input variables here.

Related Terraform terms

  • Terraform lifecycle ignore_changes: The sibling lifecycle argument for handing attributes to an external process, rather than blocking destruction outright.
  • Terraform state rm: The older way to drop a resource from Terraform's management without destroying it, now largely superseded by the removed block.
  • Terraform moved block refactoring: Relevant when a prevent_destroy guard unexpectedly blocks a state migration Terraform represents internally as a destroy-and-recreate.
  • What is OpenTofu: Useful context if you're checking whether lifecycle behavior differs between Terraform and its open-source fork.