Anyone who has run Infrastructure as Code (IaC) in production for a while knows it: the hard part isn't creating a resource — it's protecting that resource differently per environment, detaching it safely, importing existing ones accurately, and not wrestling with lock files in CI.
OpenTofu 1.12.0 (released May 14, 2026) takes direct aim at exactly these operational pains. Forked from Terraform after HashiCorp's 2023 BSL license change, OpenTofu added OCI registry support and native S3 locking in 1.10, ephemeral values and the enabled meta-argument in 1.11, and has now diverged into a mature IaC engine on its own track rather than "a fork chasing Terraform." This article breaks down five of 1.12's key features from an operations and architecture angle, then lays out how ManoIT rolled them into our internal multi-cloud IaC.
1. Why OpenTofu 1.12 Now — From Fork to Its Own Track
Some context first. When IBM acquired HashiCorp in December 2024, enterprise uncertainty around the BSL license grew, accelerating OpenTofu evaluation. As of April 2026, roughly 12% of IaC practitioners have adopted OpenTofu, with another 27% planning to evaluate or expand it, while organizations like Boeing, Capital One, and AMD run it in production. Many teams run both — Terraform for legacy, OpenTofu for greenfield.
The important shift is that the two tools are no longer two versions of the same product. OpenTofu has diverged toward native state encryption, provider-defined functions, and a faster release cadence; Terraform has gone toward AI-assisted features and deeper HCP integration. 1.12 sits in the middle of that divergence as a signal of operational maturity: "lifecycle control made dynamic, imports made accurate, lock files made automatic."
| Version | Released | Theme | Headline |
|---|---|---|---|
| 1.10.0 | 2025-06 | Deployment / security base | OCI registry, native S3 lock, external key providers (state encryption) |
| 1.11 | H2 2025 | Expressiveness | Ephemeral values, enabled meta-argument, stronger moved/removed |
| 1.12.0 | 2026-05-14 | Operational maturity | Dynamic prevent_destroy, destroy=false, identity import, checksum automation, -json-into |
2. Dynamic prevent_destroy — Per-Environment Delete Protection via Variables
prevent_destroy is a lifecycle argument that tells OpenTofu to error out if a plan would destroy a given object. It's commonly used for objects whose deletion would cause a major outage, or whose recreation requires manual work outside OpenTofu (like restoring a backup). The problem: until now this value was a static decision hard-coded into configuration. If you share a module across prod and dev — wanting the prod database extremely hard to delete but the dev one easy to replace — a static value left you stuck.
1.12.0 lets prevent_destroy be defined dynamically in terms of other values within the same module (such as input variables). It's the first lifecycle argument to be made dynamic, with more planned (umbrella issue #1329).
variable "prevent_destroy_database" {
type = bool
default = true # Protected by default. Turn off via the dev module block.
}
resource "example_database" "example" {
# ...
lifecycle {
# 1.12: can reference variables -> control delete protection per environment from one module
prevent_destroy = var.prevent_destroy_database
}
}
Ops tip: keep the shared module default at
true(protected) and passfalseexplicitly only from dev/staging callers. "Safe by default, exceptions explicit" is the pattern that prevents deletion accidents with the least code.
3. destroy = false — Remove From State Without Destroying the Remote Object
Another new lifecycle meta-argument, destroy = false, lets you remove a managed resource from state without first destroying the remote object. Previously, "I want to take this out of OpenTofu management but keep the actual infrastructure alive" had to be worked around with removed blocks or state rm. Expressed as a lifecycle argument, the intent now stays in code.
resource "aws_s3_bucket" "legacy_logs" {
bucket = "manoit-legacy-logs"
lifecycle {
# Exclude from destroy plans -> bucket is preserved even when dropped from state
destroy = false
}
}
⚠️ Warning: an object dropped from state via
destroy = falseis no longer tracked by OpenTofu. If other code creates a new resource with the same name, you may hit an "already exists" conflict — sort out your import/naming policy right after detaching.
4. Resource Identity Import — From Guessing IDs to Schema-Based
In OpenTofu, importing existing infrastructure has long meant "getting the resource's id string exactly right." But id formats vary wildly by resource type, and resources using composite keys (multiple combined attributes) are awkward to express in a single id. 1.12.0 introduces import by resource identity, pointing at the remote object via the attributes defined by the resource type's identity schema, instead of a single id string.
For example, hashicorp/aws's aws_ssm_maintenance_window_target has an identity schema requiring both id and window_id. You can now specify these via the import block's identity argument.
import {
to = aws_ssm_maintenance_window_target.example
# 1.12: point precisely via identity schema attributes instead of guessing id
identity = {
window_id = "mw-0123456789abcdef0"
id = "12345678-90ab-cdef-1234-567890abcdef"
}
}
For bulk imports, combine this with the import block's
for_each(loopable imports, introduced in 1.10). The identity-schema + for_each combo turns "deterministically importing hundreds of existing resources" into a single block.
5. Provider Checksum & Install Improvements — The End of tofu providers lock
This is the change CI/CD operators will welcome most. Previously, teams using a global plugin cache or local mirror found the dependency lock file missing checksums after tofu init, forcing a separate tofu providers lock run. The lock file only had zh: (zip hashes), while the h1: hashes needed for cache/mirror verification were only computed locally.
In 1.12.0, the OpenTofu Registry officially provides the full set of checksum formats needed by all install methods. So a single tofu init fills the lock file with both h1: and zh: hashes, letting you verify a global cache or local mirror immediately. tofu providers lock now remains only for its original purpose: populating origin-registry checksums on systems reconfigured to use an alternate install source.
# After upgrading, the first init auto-adds h1: hashes to the lock file
tofu init
# No longer needed just because of cache/mirror (as long as you use the default registry)
# tofu providers lock -platform=linux_amd64 -platform=darwin_arm64
# Confirm both zh:/h1: hash types landed in the lock file
grep -E '"(zh|h1):' .terraform.lock.hcl | head
On top of this, concurrent provider installation was added. When many providers are needed, install requests are parallelized to cut tofu init time. The effect is most noticeable on monolithic root modules with 10+ providers.
| Situation | Up to 1.11 | 1.12.0 |
|---|---|---|
| Global cache / mirror verification | run providers lock manually after init
|
h1 & zh auto-filled in one init
|
| Installing many providers | sequential requests | concurrent (parallel) -> faster init |
| Lock file hashes | mostly zh:, h1: computed locally |
full formats prepopulated at install time |
6. Simultaneous Output (-json-into) and Observable IaC
Many OpenTofu commands support both human-oriented UI output and machine-readable JSON, but until now you could only get one or the other. For tools building alternative UIs, this meant "you must reimplement the entire UI from JSON alone before it's usable." 1.12.0's -json-into=FILENAME option sends the same machine-readable output as -json to a separate file, while the standard output keeps showing the normal human-facing UI.
# Human UI in the terminal, machine JSON to a file, simultaneously
tofu apply -json-into=apply-events.json
# To consume streaming events in real time, use a named pipe / special device
mkfifo /tmp/tofu-events
tofu apply -json-into=/tmp/tofu-events &
# Read the pipe from another process to update a web/terminal UI instantly
Stream the JSON into an IPC object like a named pipe or /dev/fd/N, and an external tool can responsively display progress concurrently with OpenTofu's execution. Combined with the local-only OpenTelemetry tracing introduced in 1.10, this opens the path to treating "IaC execution as an observable pipeline."
7. Deprecations — WinRM Provisioners and 32-bit
1.12 is an operations-hardening release with few breaking changes, but you must be aware of two deprecation notices.
| Item | 1.12 status | Action |
|---|---|---|
| WinRM provisioner connections | warning (deprecated), still functional | slated for removal in v1.13 -> migrate to OpenSSH for Windows |
32-bit CPU (386, arm) official builds |
no change in 1.12 (notice only) | warnings from v1.13 -> builds dropped later, move to 64-bit (amd64/arm64) |
⚠️ Warning: if you have provisioners using
connection { type = "winrm" }, "later" won't cut it. It's fully removed in v1.13, so use this upgrade as the trigger to plan an OpenSSH-for-Windows migration. 32-bit environments likewise need a 64-bit move reviewed within the next year.
8. Cumulative Changes: 1.10 -> 1.12
For teams jumping from 1.9 or below, here are the cumulative highlights.
| Area | Introduced | Core |
|---|---|---|
| State encryption (external key providers) | 1.10 | AWS/GCP KMS, OpenBao, PBKDF2 key provider chaining |
| Native S3 state locking | 1.10 | S3 backend locking without DynamoDB |
| OCI registry distribution | 1.10 | distribute providers/modules to air-gapped environments |
Ephemeral values / enabled meta-argument |
1.11 | in-memory-only data, conditional enable beyond count/for_each |
| Dynamic prevent_destroy / destroy=false | 1.12 | per-environment delete protection, state-only removal |
| Identity import / checksum automation | 1.12 | schema-based import, full hashes in one init |
9. ManoIT Internal Adoption Checklist
| # | Task | Owner | Done when |
|---|---|---|---|
| 1 | Bump staging root module to 1.12.0, review lock-file diff after first init
|
Platform | h1 & zh hashes auto-added confirmed |
| 2 | Parameterize prevent_destroy in shared DB/storage modules (default true) |
Module owners | prod=protected, dev=off controlled by caller |
| 3 | Apply destroy = false to legacy resources to keep but unmanage |
Domain owners | 0 destroys in plan, remote object preserved |
| 4 | Re-organize composite-key resources (SSM targets, etc.) via identity import | Domain owners | deterministic import via import block + for_each |
| 5 | Remove the manual tofu providers lock step from CI |
DevOps | init alone verifies cache/mirror |
| 6 | Stream apply events to the internal dashboard via -json-into
|
Observability | live execution progress displayed |
| 7 | Inventory WinRM provisioners -> OpenSSH migration roadmap | Infra | 0 winrm uses before v1.13 |
10. Conclusion — "The Next IaC Challenge Isn't Creation, It's Lifecycle"
If I had to sum up OpenTofu 1.12.0 in one line: "creating resources is a solved problem; what remains is the lifecycle operations of protecting them differently per environment, detaching them safely, importing them accurately, and not fighting lock files." Dynamic prevent_destroy lets you control delete protection per environment from a single module. destroy = false keeps the intent of "unmanage but preserve" in code. Identity import ends the era of guessing IDs. Checksum automation strips the manual tofu providers lock out of CI. And -json-into elevates IaC execution into an observable pipeline.
Three closing recommendations. (1) Review the lock-file diff on the first init after upgrading — a bulk addition of h1 hashes is normal, and committing it makes cache/mirror friction disappear. (2) Parameterize prevent_destroy in shared modules — the change that cuts deletion accidents most for the least code. (3) If you use WinRM provisioners, inventory them now — the v1.13 removal is not "optional" but a "scheduled deadline." The shortest one-liner: "this sprint, bump staging to 1.12, turn the shared DB module's prevent_destroy into a variable, and run plans on both prod and dev once."
Originally published at ManoIT Tech Blog.
Top comments (0)