May 14, 2026·5 min read·engineeringterraformlifecyclemodules

OpenTofu 1.12 makes destroy protection a module input

OpenTofu 1.12 lets prevent_destroy depend on values in its module. The release also distinguishes blocking destruction from forgetting an object while leaving it running.

OpenTofu 1.12.0, released on May 14, lets prevent_destroy reference values available elsewhere in the same module. The release announcement demonstrates a boolean variable controlling database protection. This is useful for reusable modules whose callers need different destruction policies. The protection can now be an explicit input to the module instead of requiring the author to hard-code the same choice for every caller.

Terraform's reusable language over infrastructure APIs is one of the reasons I reach for it by default. I am also particularly interested in OpenTofu's permissive licensing. This release addresses a practical module design concern within that model. I want configuration to express a caller's intended protection policy without requiring a second copy of a module solely to change a lifecycle setting.

The new flexibility does not expand what prevent_destroy protects. Its behavior still depends on the resource declaration and the plan. A module input makes that policy configurable, which also makes the input part of the module's interface. I would review its default and its documentation with the same care as other inputs that can affect existing infrastructure.

A boolean controls the existing protection

The release's example declares a boolean variable with a default of true and references it from the resource's lifecycle block. The central declaration is ordinary input configuration:

variable "prevent_destroy_database" {
  type    = bool
  default = true
}

Within the database resource, the lifecycle fragment then becomes:

lifecycle {
  prevent_destroy = var.prevent_destroy_database
}

This second block belongs inside a managed resource declaration. It is not a complete database configuration, and it does not depend on a particular provider's database arguments. The relevant new behavior is the reference to a value in the same module. A caller can supply the variable while the resource remains defined inside the module.

The resource behavior documentation at the release tag says that a true value causes OpenTofu to reject a plan that would destroy the associated object while the argument remains in configuration. That includes accidental replacement, which can require destroying an existing object. Protection therefore has consequences for ordinary changes as well as a deliberate destroy operation.

I would document why a module defaults to protection and what a caller should inspect before disabling it. The input's name should make the destructive consequence understandable during configuration review. A boolean that is easy to change still represents a decision about whether OpenTofu may destroy an object managed at that address.

Removing configuration removes this protection

The documentation states a limitation that matters especially for modules. If the resource block disappears entirely, its prevent_destroy setting disappears with it. That setting cannot prevent destruction in that case. Making the value dynamic does not turn it into a persistent independent rule associated with the remote object.

This limits the claims I would make about a protected module. A true input can reject a destructive plan while the resource and its lifecycle argument remain present. It does not establish that removing the module call will preserve the remote object. A module consumer needs to understand both cases before relying on the input as a retention policy.

I would include plan-based checks for the protection's intended use. One caller configuration should leave protection enabled and attempt a change that requires destruction. Another should demonstrate the deliberate opt-out. Reviewing those outcomes gives the module author evidence about the input's behavior, without implying that a passing test can cover every remote service's retention guarantees.

The choice also affects changes that genuinely require replacement. A rejection is the expected outcome when protection is active. The operator then needs to decide whether to change the proposed operation or disable protection deliberately. Treating the rejection as an inconvenience to suppress would defeat the reason for giving the module this interface.

Forgetting an object has different semantics

OpenTofu 1.12 also introduces the resource lifecycle setting destroy = false. The same release-tag documentation explains that this causes OpenTofu to forget the resource instance instead of destroying the remote object. Forgetting removes the instance from state. The object can remain in the remote system, but OpenTofu no longer tracks it through that state entry.

Unlike dynamic prevent_destroy, lifecycle.destroy accepts only constant boolean values in this version. The documentation also says the destroy choice persists in state after application. These differences matter when designing a module interface. The two settings cannot be treated as interchangeable ways to express the same policy.

The documentation warns that prevent_destroy has no effect on an action changed to forgetting by destroy = false. It also explains that adding configuration back after forgetting can cause OpenTofu to attempt creation again. Reconnecting management to an existing object may require import. I would choose forgetting only when the intended outcome includes relinquishing that state association.

For callers who want an accidental destructive change to fail, prevent_destroy expresses that intent. For callers who need an object retained while its state association is removed, forgetting expresses another operation. A module should describe which result it supports and make the resulting ownership clear to whoever will operate the retained object.

The release also adds -json-into=FILENAME, which writes machine-readable output to a file while preserving normal human-readable output. Its announcement explains that tools can consume the JSON as a supplement without implementing the entire human interface. For lifecycle changes, I would use that capability to retain structured plan information alongside the output an operator reviews, then check that both describe the intended destruction or forgetting behavior.

Provider installation also changes in this release. The announcement says OpenTofu Registry now supplies the official checksums needed by the other installation methods. After initialization, the lock file can support a shared plugin cache and matching packages from a local mirror without a separate providers lock operation. I would keep that installation concern separate from lifecycle policy when checking an upgrade. A module can initialize successfully and still propose an unwanted lifecycle action. Conversely, a rejected destructive plan can be the intended result even when provider installation completed without any errors. Each check needs an expected outcome of its own.