Skip to content

필사 모드: OpenTofu 1.12's Dynamic prevent_destroy — Where the Lifecycle Block's Static Constraint Loosens, and What It Costs

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction — "Variables may not be used here"

Anyone who's used Terraform for a while has hit this error. Say you want to protect a database instance from being destroyed by mistake, so you write this,

variable "allow_destroy_database" {
  type    = bool
  default = false
}

resource "aws_db_instance" "example" {
  # ...
  lifecycle {
    prevent_destroy = !var.allow_destroy_database
  }
}

Terraform refuses. You can't use variables inside a lifecycle block. Even if you want to protect production while letting yourself tear down dev freely, there was no way to do it short of duplicating the same module or resorting to a count trick.

This isn't a constraint born of laziness. HashiCorp's lifecycle documentation states the reason plainly — lifecycle settings affect how Terraform builds and traverses the dependency graph, which is why "only literal values can be used." In other words, the point at which this is processed is too early to evaluate arbitrary expressions. Building the graph requires knowing the lifecycle values, but evaluating an expression requires the graph. Chicken and egg.

The request itself is old. hashicorp/terraform#10730 ("Unable to use var for ignore_changes or prevent_destroy") was opened on 2016-12-14 and closed on 2019-08-27, and the still-open formal request #25534 ("Allow variables in lifecycle block") was opened on 2020-07-09. As of this writing, #25534 has 66 👍 reactions. Reaction counts are only a weak signal of demand, not a precise measure — and keep in mind GitHub reaction counts change over time.

What OpenTofu 1.12 Did — Moving the Point of Evaluation

OpenTofu v1.12.0 was released on 2026-05-14 (per the GitHub release metadata) and loosened this constraint. In the official docs' words, the prevent_destroy argument of the lifecycle block can now reference other symbols within the same module — for instance, one of the module's input variables.

What's interesting is how they loosened it. PR #3474's description is clear. Previously, the config loader evaluated the prevent_destroy expression immediately, which forced it to be a constant expression producing a boolean. Now the loader stores the expression as-is and leaves evaluation to the language runtime. That makes it possible to reference dynamically determined values within the same module.

In other words, the premise that "you have to know it before building the graph" wasn't touched at all — evaluation was deferred as far as it could be deferred. tofu validate still performs the same boolean type check the old loader used to do, and any expression that can't be evaluated with a nil evaluation context is pushed forward to the plan stage to be checked there. The PR's argument is that at plan time there's more information available, so error messages get more precise too.

The example at the top — prevent_destroy = !var.allow_destroy_database — now works as-is in OpenTofu. The original request, opentofu#2522, was opened on 2025-02-14 and closed by this PR.

What Can Be Referenced, and What Can't

This is where it's worth being honest. "Dynamic" doesn't mean "anything goes."

The scope is limited to within the same module. And PR #3474 states it deliberately made the most conservative choice for the tricky cases — unknown values, ephemeral values, sensitive values, and references to local symbols like count.index. The reasoning is sound: loosening things further in a later version is always possible, but once something's been allowed and turns out to have been a mistake, you can't take it back. Loosening a rule is backward compatible; tightening one isn't.

The practical takeaway here is this — you can't decide prevent_destroy by referencing a resource attribute. Resource attributes can be unknown until apply, and unknown values are conservatively blocked. This feature is for "deciding the protection level per environment via a variable," not for "making the call by inspecting runtime state."

This isn't the first time this pattern has shown up. OpenTofu already introduced the enabled meta-argument to the lifecycle block in v1.11.0 (2025-12-09), and the constraints the official docs state for enabled are almost the same list — no unknown values, sensitive values, null values, ephemeral values, or non-boolean values, and it can't be used together with count or for_each. There's a consistent line in how the lifecycle block gets to be dynamic: the value can be dynamic, but it has to be knowable at plan time.

destroy = false — This Is a Different Animal

The same 1.12 release added a new lifecycle meta-argument called destroy. The name makes it sound like it belongs in the same bucket as prevent_destroy, but its character is quite different — and in fact it affects state management far more directly.

By default, OpenTofu destroys the actual infrastructure object when a resource disappears from the configuration, needs to be replaced, or is explicitly deleted with tofu destroy. Set destroy = false, and instead of destroying, OpenTofu plans to forget it — leaving the real object alone and removing it only from state.

resource "aws_db_instance" "example" {
  # ...
  lifecycle {
    destroy = false
  }
}

The use cases the official docs give make sense: tearing down the rest of an environment while keeping a regulated resource behind, keeping the old instance around during a replacement to make rollback easier, or dealing with an object that can't be destroyed in the first place because of an external dependency. The point is that it turns what used to be a manual tofu state rm into a reviewable configuration change.

But the strings attached aren't trivial. Let's go through them one at a time.

First, it only takes a constant. The docs nail this down — lifecycle.destroy accepts only a constant boolean value (true or false). We just said prevent_destroy became dynamic; destroy, introduced in the same release, is not dynamic. That means the lifecycle block didn't become dynamic as a whole — it varies argument by argument, and this asymmetry is confusing if you don't know about it going in.

Second, it sticks in state. This argument is stored in state. Once you apply destroy = false, OpenTofu won't plan to destroy that resource until you explicitly flip it back to true or remove the option from the configuration. It's deliberately conservative, judging based on the last-applied configuration. For a single-instance resource (one without count or for_each), you can override this stored value by writing destroy = true in a removed block, but resources using count or for_each don't have that escape hatch.

Third, it neutralizes prevent_destroy. This is the important one. The docs give an explicit warning: setting destroy = false turns the destroy action into a variant of the forget action, and prevent_destroy has no effect whatsoever. It's tempting to file both arguments under the same "safety switch" drawer and assume turning both on makes things safer — but in practice the latter switches off the former.

Fourth, undoing a forgotten resource is a hassle. Exactly as the docs warn — once a resource has been forgotten (removed from state), OpenTofu no longer tracks it. If you later add it back to the configuration under the same address, OpenTofu tries to create a new resource, and if the real object is still alive, that attempt can fail. To bring it back you have to import it, or use a different resource address.

Fifth, the exit code changes. Run tofu destroy while resources with destroy = false exist, and those get forgotten instead of destroyed; the command then exits with a non-zero code to signal that some resources weren't fully removed. CI reads that as a failure. 1.12 also added tofu destroy -suppress-forget-errors to handle exactly this case.

And one more thing — this isn't an OpenTofu-only invention. Terraform has its own removed block, and giving it destroy = false also removes a resource from state without destroying the real thing. The difference is where it applies. The removed block is a refactoring tool used while taking a resource out of the configuration; OpenTofu's lifecycle argument changes destroy behavior while the resource stays in the configuration. OpenTofu's own docs draw this line themselves — use the removed block for refactoring, and use lifecycle's destroy to control the destroy behavior of a resource that remains in the configuration.

What Dynamic prevent_destroy Still Doesn't Fix

As welcome as dynamic prevent_destroy is, it's worth being clear about the hole it does not close.

prevent_destroy protection is only in effect while that argument stays in the configuration. As the docs themselves state, deleting the entire resource block from the configuration also deletes the prevent_destroy setting along with it, and OpenTofu then allows the destroy. In other words, this safeguard blocks "a plan that deletes this resource" but not "a commit that deletes this resource's definition." Given that the latter is the most common path to accidentally destroying a resource, that's a fairly significant limitation.

Making it dynamic doesn't improve this. If anything, it can arguably make the surface area worse — now that the value of prevent_destroy hangs on a variable, a single line in a tfvars file or a single CI environment variable becomes the switch that turns protection off. Whether that switch sits somewhere that goes through code review, or somewhere anyone can override with a pipeline variable, is now your problem. What the feature bought you is flexibility, and flexibility demands discipline.

PR #3474 also left one open question. How do you test the effect of dynamic prevent_destroy with tofu test? The PR's author didn't answer this at all in the PR itself, noting it could be addressed separately later if there's interest. That means there's still no standard way to branch protection logic on a variable and then test that branch.

Terraform Went a Different Way

Around the same time, Terraform has been solving the same static-evaluation problem in a completely different spot.

Terraform v1.15.0 shipped on 2026-04-29, and its list of new features includes this — variables and locals can now be used in a module's source and version attributes (#38217). This, too, loosens the same family of constraint: "a value that has to be known at config-loading time." Importing a module requires knowing source, and if that value is a variable, the variable has to be evaluated first. Same direction. Just a different spot.

The same release also shipped the ability to attach a deprecated attribute to variable and output blocks (#38001), a convert function for precise inline type conversion (#38160), a terraform validate that now inspects the backend block too (#38021), and Windows ARM64 builds. And the lifecycle block — the doc sentence quoted earlier still holds — still accepts only literals.

What's interesting is that convergence is happening too. Both projects added support for aws login credentials in the s3 backend in this same cycle (OpenTofu #3767, Terraform #37976). Forks don't only diverge — sometimes the ecosystem pushes the same demand up from underneath and both sides end up building the same thing side by side.

As of this writing, 2026-07-16, the latest stable releases are OpenTofu v1.12.4 (2026-07-13) and Terraform v1.15.8 (2026-07-08), with Terraform currently running a v1.16.0 alpha.

So, Should You Use It?

Where it earns its keep

  • You need one module to cover dev, staging, and prod while varying only the protection level by environment. This is exactly what dynamic prevent_destroy is for, and it's clearly cleaner than duplicating modules or resorting to count tricks.
  • You have a procedure for tearing down an environment while keeping specific resources behind, and you're currently running that by hand through a tofu state rm runbook. destroy = false turns that manual step into a configuration change that shows up in the plan.

Where you should hold off or avoid it

  • Your configuration needs to run on both Terraform and OpenTofu. The moment you use this syntax, that configuration no longer works on Terraform. This is a one-way ticket if you want to keep the option of moving between the forks — more so if you're publishing a module publicly, since you don't get to decide which fork any given user runs.
  • If what you're actually after is preventing the accident of destroying a resource by mistake, prevent_destroy still can't close that hole in the first place — the path of deleting the block from the configuration. Process-level controls — plan review, banning -target, CI gates, state backups — remain the real defense; this is a supplement.
  • If you're approaching destroy = false with the mindset of "just turn it on to be safe." It sticks in state, it disables prevent_destroy, and undoing it requires an import. It's a switch that's easy to flip on and hard to flip off.

Closing

To sum up: Terraform's lifecycle block accepts only literals because it's processed at the point the dependency graph is being built — a structural constraint that's been pointed out since 2016. OpenTofu 1.12 loosened that constraint by moving prevent_destroy's evaluation from the config loader to the language runtime — while scoping it to within the same module, and deliberately keeping unknown, sensitive, and ephemeral values, along with count.index, conservatively blocked. It started narrow to avoid irreversible mistakes, and I think that call is the right one.

The destroy = false argument in the same release is a heavier thing than its name suggests. It only takes a constant, it's stored in state, it neutralizes prevent_destroy, undoing a forgotten resource requires an import, and it changes the exit code of tofu destroy. Useful, but you need to know all five of these before you flip it on.

And zoomed out, this isn't a story about which fork is "better." Both are pushing against the same wall of static evaluation — they're just pushing at different spots: OpenTofu at lifecycle, Terraform at module source and version. What that actually leaves practitioners with isn't a feature list, but a portability decision. The moment you use this syntax, your configuration belongs to one side only. Whether that's fine is the question to answer before whether the feature is neat.

References

현재 단락 (1/67)

Anyone who's used Terraform for a while has hit this error. Say you want to protect a database insta...

작성 글자: 0원문 글자: 14,305작성 단락: 0/67