OpenTofu
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
OpenTofu is an open-source infrastructure as code tool that began in 2023 as a fork of the last
open-source Terraform code. It reads the same configuration language, drives the same providers through the same plugin
protocol, keeps its state in the same kind of file and offers the same workflow of init, plan and apply through a
binary called tofu. Since the fork the two projects have each added features the other does not have, so the question
"can I just swap one for the other" has an answer that depends on the version you run and on what your configuration
uses.
This page is written against Terraform 1.17 (1.17.0 was released on 2026-10-21) and OpenTofu 1.13 (1.13.0 was
published on 2026-09-30 and 1.13.2 on 2026-10-21). It covers how the fork came about and who governs it, what
compatibility does and does not promise, how the version numbers relate, how to install tofu, how to migrate from
Terraform and back, which features exist only in OpenTofu or only in Terraform, and how to choose. Most of the other pages
of this section already contain a "Terraform and OpenTofu differences" table for their own topic; this page is the place
that ties them together. It builds on Introduction and Infrastructure as Code, which
explains the licence history in outline, and on Getting Started, which installs
Terraform and the version managers.
The examples use the Bookshelf scenario of the section (catalog-api, reviews-api and web services backed by
PostgreSQL and Redis, a bucket for cover images, a network, DNS, secrets and IAM). Every value that would be a credential,
an account identifier, a key or a state is a placeholder. No tofu or terraform binary was available while this page
was written, so commands are shown without output unless the output is quoted from an official page, and anything shown is
abbreviated and illustrative.
History and governance
Terraform was developed in the open by HashiCorp under the Mozilla Public License 2.0 (MPL-2.0). On 2023-08-10 HashiCorp announced that future releases of its products would move to the Business Source License 1.1 (BSL, also written BUSL). The OpenTofu manifesto, the document that started the fork, describes the change as "a poison pill for Terraform" and argues that the licence and its additional use grant are vague enough that companies, vendors and developers cannot be sure their use will not be judged competitive with HashiCorp’s products. It asked HashiCorp to return Terraform to a fully open-source licence such as MPL-2.0 or Apache 2.0, and says that when HashiCorp had not responded by 2023-08-25 the signatories created a fork. The manifesto lists five principles for that fork: truly open source under a well-known licence that will not suddenly change, community-driven, impartial, layered and modular, and backwards-compatible.
Source: The OpenTF Manifesto
| Date | Event |
|---|---|
Until 2023-08 |
Terraform is developed by HashiCorp under MPL-2.0. The last release of the 1.5 series, 1.5.7, was published on 2023-09-07 and remains MPL-2.0. |
2023-08-10 |
HashiCorp moves its products to the Business Source License 1.1. |
2023-08-16 |
The |
2023-08-25 |
The OpenTofu announcement post describes the fork, the Linux Foundation as its home and the goal of eventually joining the Cloud Native Computing Foundation. |
2023-10-04 |
Terraform 1.6.0, the first BSL-licensed minor release, is published. |
2024-01-10 |
OpenTofu 1.6.0, the first stable release, is published as a drop-in replacement for Terraform 1.5.x, with a new registry and the native test feature. |
2025-02-27 |
IBM completes its acquisition of HashiCorp. |
2025-04-23 |
OpenTofu is accepted into the Cloud Native Computing Foundation at the Sandbox maturity level. |
2026-09-30 |
OpenTofu 1.13.0 is published. |
2026-10-21 |
Terraform 1.17.0 and OpenTofu 1.13.2 are published. |
Source: OpenTofu announces fork of Terraform, OpenTofu 1.6.0 release, OpenTofu at the CNCF, Terraform releases and IBM completes acquisition of HashiCorp
The same history as a timeline:
The name changed along the way: the first announcement spoke of "OpenTF", and the project settled on "OpenTofu". The OpenTofu announcement post, which this page cites for the fork, uses only the final name, so the rename itself is documented by press coverage rather than by the project’s own pages. The date on which the Linux Foundation accepted the project is likewise reported by secondary sources as 2023-09-20; the project’s own pages say only that the team "completed all documents required" to join and that the project is today listed as a series of LF Projects, LLC.
Licence and governance
The OpenTofu repository is licensed under MPL-2.0, as the GitHub API reports for opentofu/opentofu. Terraform’s own
licence is the BSL 1.1, which converts each release to MPL-2.0 on its change date, four years after that version is
published (see the introduction page for what the BSL does and does not restrict). In practice the licence matters to three
groups: vendors that build competing hosted services, organisations whose policy requires an OSI-approved licence, and
anyone who wants a vendor-neutral governance model. For a team that runs the CLI in its own pipelines, the day-to-day effect
of the BSL is nil, and the choice is about features and ecosystem rather than law.
The OpenTofu FAQ describes how the project is run: a core team led by a technical lead chooses the most important features and bug fixes, a steering committee decides on big changes, and major changes to OpenTofu core go through a public RFC process guided by community feedback. The project sits under the Linux Foundation, and was accepted by the CNCF as a Sandbox project on 2025-04-23. The FAQ names organisations that back the project, among them Gruntwork, Spacelift, Harness, env0 and Scalr; those are vendors of platforms that run Terraform-compatible workflows, which is one reason the fork has tooling support beyond the CLI.
Each minor series has a bounded security-support window. The changelog of the 1.13 series says it is supported until 2027-08-01, and the unreleased 1.14 series on the main branch says until 2028-02-01. Terraform’s support policy is a HashiCorp matter and is not described on this page.
What compatibility means
OpenTofu’s stated aim is to "maintain compatibility with Terraform configurations", and the FAQ says it aims for feature parity in future releases while leaving the decision of which features to add to the community. That is a goal, not a guarantee, and it applies in five different places that are worth separating.
| Layer | What carries over and what does not |
|---|---|
Configuration language |
|
Providers |
OpenTofu does not publish its own providers. It uses the same provider binaries and the same plugin protocol, which is why the AWS, Azure, Google Cloud, Docker and Kubernetes providers of the other pages work with both tools. The difference is where they are downloaded from (next section). |
State |
The FAQ states that OpenTofu works with state files created by Terraform up to 1.5.x, and the migration guides extend that
to later Terraform versions with a few changes (see the migration section). After |
CLI |
The commands are the same, with the name |
Registry and ecosystem |
OpenTofu has its own registry for providers and modules. Tools around the CLI (TFLint, Checkov, Trivy, terraform-docs,
Terragrunt, Atlantis and others) generally support both, and the platforms backing the fork support |
Two consequences follow. First, compatibility is asymmetric over time: a configuration written for Terraform 1.5 runs on OpenTofu unchanged, while a configuration written with the newest OpenTofu features does not run on Terraform. Second, because each project has its own roadmap, the set of constructs that work on both tools is a moving intersection. The version table below lists, for each feature, when each tool first had it, which is the data needed to compute that intersection for your own versions.
The registry and the dependency lock file
Terraform downloads providers and modules from registry.terraform.io. OpenTofu’s default is registry.opentofu.org: a
provider source address without a hostname, such as hashicorp/aws, means registry.opentofu.org/hashicorp/aws, and if the
source argument is omitted entirely OpenTofu implies registry.opentofu.org/hashicorp/<LOCAL NAME>. The OpenTofu
documentation recommends explicit source addresses for every provider. Anyone can also host a registry that implements the
provider registry protocol under another hostname. The public OpenTofu registry is searchable at search.opentofu.org, which
the FAQ describes as separate from the Terraform registry.
terraform {
required_version = ">= 1.8"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.0"
}
}
}
The same block resolves to registry.terraform.io/kreuzwerker/docker under Terraform and to
registry.opentofu.org/kreuzwerker/docker under OpenTofu. The dependency lock file .terraform.lock.hcl records the provider
address, the selected version and the checksums, so the entries made by the two tools name different hostnames. The practical
effects are these:
-
A repository that is built by both tools in CI should either keep one lock file per tool or give the source address a full hostname, so that the two tools do not overwrite each other’s entries. The CI page of this section shows a matrix build.
-
When a lock file made by Terraform references
registry.terraform.io,tofu initsince OpenTofu 1.10 can pick the equivalent provider onregistry.opentofu.org, which removes most of the friction of the first migration. -
OpenTofu 1.12 records checksums for all supported platforms in the lock file at
tofu init, and the registry supplies a fuller set of checksums, so the lock file is complete afterinitandtofu providers lockis rarely needed. The 1.12 release post makes the same point. -
A provider or module that exists on one registry and not on the other produces "Failed to query available provider packages" or "Module not found". The migration guides give those two messages as the symptom of a package missing from the OpenTofu registry, and recommend rolling back and reporting the missing item.
The full treatment of provider requirements, the lock file and mirrors is on Providers.
Version mapping
The version numbers are independent. OpenTofu 1.6 was derived from Terraform 1.5.x code, and since then each project has numbered its own releases. "OpenTofu 1.13" is not "Terraform 1.13", and a Terraform feature released in 1.14 is not necessarily in a particular OpenTofu release.
| Minor | Terraform (first release of the series) | OpenTofu (first release of the series) |
|---|---|---|
1.5 |
1.5.7 is the last of the series, published 2023-09-07 (MPL-2.0) |
none; the fork starts from this code |
1.6 |
1.6.0 on 2023-10-04 |
1.6.0 on 2024-01-10 |
1.7 |
1.7.0 on 2024-01-17 |
1.7.0 on 2024-04-30 |
1.8 |
1.8.0 on 2024-04-10 |
1.8.0 on 2024-07-29 |
1.9 |
1.9.0 on 2024-06-26 |
1.9.0 on 2025-01-09 |
1.10 |
1.10.0 on 2024-11-27 |
1.10.0 on 2025-06-24 |
1.11 |
1.11.0 on 2025-02-27 |
1.11.0 on 2025-12-09 |
1.12 |
1.12.0 on 2025-05-14 |
1.12.0 on 2026-05-14 |
1.13 |
1.13.0 on 2025-08-20 |
1.13.0 on 2026-09-30 |
1.14 |
1.14.0 on 2025-11-19 |
not released; unreleased on the main branch |
1.15 |
1.15.0 on 2026-04-29 |
not released |
1.16 |
1.16.0 on 2026-08-26 |
not released |
1.17 |
1.17.0 on 2026-10-21 |
not released |
The table shows two things. OpenTofu’s minor releases are less frequent than Terraform’s since 1.9, which is why a Terraform feature typically reaches OpenTofu a release or more later, if it arrives at all. And the numbers drift apart, so any statement of the form "OpenTofu N equals Terraform N" is wrong after 1.6. What matters is the feature, so the next table lists the features that both tools have, with the version in which each first appeared.
| Feature | Terraform | OpenTofu |
|---|---|---|
|
1.7 |
1.7 |
|
1.7 |
1.7 |
Provider-defined functions |
1.8 |
1.7 |
|
1.8 |
1.7 |
|
1.7 |
1.8 |
Variables and locals in module |
1.15 |
1.8 |
|
1.8 |
1.10 |
Built-in |
1.8 |
1.10 |
|
1.10 |
1.10 |
S3 backend locking without DynamoDB ( |
1.10 (generally available in 1.11) |
1.10 |
Ephemeral variables, outputs and resources |
1.10 |
1.11 |
Write-only attributes |
1.11 |
1.11 |
|
1.15 |
1.10 |
|
1.15 |
1.13 |
Windows ARM64 builds |
1.15 |
1.13 |
|
1.16 |
1.12 |
Source: Terraform v1.7 CHANGELOG, Terraform v1.8 CHANGELOG, Terraform v1.10 CHANGELOG, Terraform v1.11 CHANGELOG, Terraform v1.15 CHANGELOG, Terraform v1.16 CHANGELOG, OpenTofu v1.7 CHANGELOG, OpenTofu v1.8 CHANGELOG, OpenTofu v1.10 CHANGELOG, OpenTofu v1.11 CHANGELOG, OpenTofu v1.12 CHANGELOG and OpenTofu v1.13 CHANGELOG
Reading the table: Terraform’s built-in terraform provider functions, for example, arrived in Terraform 1.8 and in OpenTofu
1.10, so a configuration that calls provider::terraform::encode_tfvars works on Terraform 1.8 and later and on OpenTofu 1.10
and later, but not on OpenTofu 1.8 or 1.9. Not every row is a clean "same feature": the OpenTofu page on
ephemerality says the feature is "inspired by and aims for compatibility" with Terraform 1.12 and that OpenTofu may accept
breaking changes later if an incompatibility turns up. Treat a row as "the same name and intent", and test the
combination you rely on.
OpenTofu keeps a short list of release lines supported at once: the 1.13 and 1.12 documentation sets are the current ones on
opentofu.org, and the patch cadence is visible on the releases page (1.12.9 and 1.13.2 were both published on 2026-10-21).
Patch releases carry security fixes; 1.13.2, for example, lists a fix for excessive CPU use when tofu init connects to an
attacker-controlled HTTP/2 server.
Source: OpenTofu Docs — Ephemerality and OpenTofu v1.13 CHANGELOG
Installing tofu
The OpenTofu installation page lists these routes: Homebrew on macOS and Linux, distribution packages (Debian and Ubuntu
.deb, RHEL and derivatives .rpm, Fedora, Alpine .apk, an Ubuntu Snap and FreeBSD ports), Windows packages, the OCI
container image on the GitHub Container Registry, and a standalone download that needs no installation. Choose by the same
criteria as for Terraform: a package manager when it exists for your platform, because it updates cleanly, and a version
manager when different projects need different versions.
| Route | Command or note |
|---|---|
Homebrew |
|
Debian or Ubuntu |
The installer script with |
Windows |
|
Standalone |
The installer script with |
Container image |
|
Version manager |
tenv, which also manages Terraform, Terragrunt, Terramate and Atmos. |
Source: OpenTofu Docs — Installing OpenTofu, Homebrew, Debian and Ubuntu and Windows
The installer script
The standalone installer is one script that serves several install methods. The documentation shows the following flow for Linux, macOS and BSD. It downloads the script, makes it executable, asks you to read it before running it, and removes it afterwards.
# Download the installer script
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
# Make it executable, then read it before you run it
chmod +x install-opentofu.sh
# Run the installer (use --install-method deb on Debian and Ubuntu)
./install-opentofu.sh --install-method standalone
# Remove the installer
rm -f install-opentofu.sh
tofu -version
The standalone method verifies what it downloads. It needs either cosign or GnuPG on the machine, or the option
--skip-verify (-skipVerify in PowerShell) to skip the check; in a pipeline, install cosign rather than skipping. The
release also publishes a SHA256SUMS file, a signature and a certificate, and the page documents verifying the checksum
file with cosign verify-blob against the release workflow identity, as well as comparing the hash of the zip with
sha256sum, shasum -a 256 or Get-FileHash. The page itself carries a warning that its sample scripts for macOS and
Windows have small mistakes, so read them before you copy them.
The container image
Since OpenTofu 1.10 the official images are no longer supported for direct use, and they are not a base image for your own
images either: the 1.9 notes already announced this and 1.10 states it. The minimal tags (minimal, 1-minimal, 1.9-minimal
and an exact version such as 1.9.1-minimal) hold only the tofu binary at /usr/local/bin/tofu, and the supported use is a
multi-stage build that copies it into an image you control.
FROM ghcr.io/opentofu/opentofu:minimal AS tofu
FROM alpine:3.20
RUN apk add --no-cache git curl
COPY --from=tofu /usr/local/bin/tofu /usr/local/bin/tofu
WORKDIR /workspace
docker build -t bookshelf-tofu .
docker run --rm bookshelf-tofu tofu --version
The Dockerfile follows the documented pattern with the documentation’s example base image; pin the tag and the base image by
digest in a real pipeline. The alternative described on the same page is to run the standalone installer in your image with
--install-method standalone --install-path /usr/local/bin, which needs bash, curl and gpg in the image.
tenv
tenv is a version manager written in Go and described as the successor of tofuenv and
tfenv. It manages OpenTofu (tofu), Terraform (terraform and tf), Terragrunt, Terramate and Atmos, installs a proxy
binary for each tool, and resolves the version from the environment, from a version file or from a default. That makes it
the usual answer to "which binary does this repository use": the proxy chooses.
brew install tenv
# List stable releases, install one, and make it the default
tenv tofu list-remote -s
tenv tofu install 1.13.2
tenv tofu use 1.13.2
# Write a version file in the current directory
tenv tofu use -w 1.13.2
Source: tenv on GitHub
The version file for OpenTofu is .opentofu-version; Terraform uses .terraform-version (and .tfswitchrc), and tenv also
reads .tool-versions and Terragrunt’s constraints. Setting TENV_AUTO_INSTALL to true makes tenv install a missing version
on demand, and the tofu and tf commands accept -i for the same purpose. The agnostic tf proxy chooses between
OpenTofu and Terraform from the version files alone, so a repository that contains .opentofu-version runs tofu and one
that contains .terraform-version runs Terraform. The tenv page does not mention a .tofu-version file. In GitHub Actions
the action opentofu/setup-opentofu (current major v2) installs tofu, reads .opentofu-version through its
tofu_version_file input and also accepts an asdf-format .tool-versions file.
Source: tenv on GitHub and opentofu/setup-opentofu on GitHub
Migrating from Terraform to OpenTofu
The OpenTofu migration overview describes a process that it calls safe and reversible: back up state and code, install
OpenTofu, initialise and verify, and test with a small change. The detail is in the migration guide, and there are two
variants worth knowing: version-specific guides for older Terraform releases, and a separate guide for systems made of
several root modules linked by terraform_remote_state.
The procedure
The current generic guide gives these steps, which this page restates with the Bookshelf commands:
-
Back up state and configuration. For local state copy
terraform.tfstate; for a remote backend follow the backend’s own procedure (the guide mentions S3 versioning and snapshotting the state bucket) and test a restore at least once. Commit the configuration, ideally on a migration branch. -
Install OpenTofu and check
tofu --version. -
Run
tofu init. Providers and modules are downloaded from the OpenTofu registry and the backend is initialised. -
Run
tofu plan. The expected result is "No changes" or the same plan Terraform would produce. If the plan shows something unexpected, do not apply: investigate, and consider rolling back. -
Run
tofu apply. The guide notes that this step updates the state file format if needed, even when there are no infrastructure changes. -
Make a small, non-critical change (the guide suggests adding a tag), plan and apply it.
# 1. Back up (local state shown; for a remote backend use its own procedure)
cp terraform.tfstate terraform.tfstate.pre-tofu
# or, for any backend, take a copy through the CLI
terraform state pull > state-before-tofu.json
# 2. Confirm Terraform is clean first
terraform plan -detailed-exitcode
# 3. Switch the binary
tofu --version
tofu init
tofu plan
tofu apply
# 4. A first small change, then review and apply it
tofu plan -out=tfplan
tofu apply tfplan
Source: OpenTofu Docs — Migration guide
The state pull backup and the -detailed-exitcode check are additions of this page; the guide asks for a backup and a
clean plan but does not prescribe the commands. Do not run tofu apply while Terraform changes are still pending: the
version-specific guides say migrating with pending changes is possible but not recommended, and ask for terraform apply to be
repeated until terraform plan reports no changes.
The version-specific guides
For Terraform 1.6 to 1.9 the OpenTofu documentation has one page per Terraform minor version. The pages in the 1.9 documentation set exist for Terraform 1.9, 1.8, 1.7, 1.6 and "1.5 or lower". The current documentation set has only the generic guide above, and I could not find version-specific guides for Terraform 1.10 or later. Each page says the same three things: start from the last patch of that Terraform minor (1.5.7, 1.6.6, 1.7.5, 1.9.8), migrate to a specific OpenTofu release first (1.6.2 for Terraform 1.5 and 1.6, 1.7.1, 1.8.2, 1.9.0), and then upgrade to the latest OpenTofu through the normal upgrade guide.
The incompatibilities these pages list are a snapshot of what differed at that moment, and several have since been closed:
| Difference listed by the guide | Guide | Status in later OpenTofu |
|---|---|---|
S3 backend: remove |
Terraform 1.6, 1.7, 1.8, 1.9 guides |
OpenTofu 1.10 documents |
|
Terraform 1.8, 1.9 guides |
Added to the built-in |
|
Terraform 1.7, 1.8, 1.9 guides |
OpenTofu 1.10 lets |
|
Terraform 1.7, 1.8, 1.9 guides |
OpenTofu 1.9 allows both blocks inside a |
Variable |
Terraform 1.9 guide |
OpenTofu 1.9 allows references to variables, data sources and similar in validation. |
Source: OpenTofu Docs — Migrating from Terraform 1.9, Migrating from Terraform 1.8, Migrating from Terraform 1.7, OpenTofu v1.9 CHANGELOG and OpenTofu v1.10 CHANGELOG
The lesson is to use the guides as a checklist of categories rather than a current list: the backends, the removed block,
the built-in terraform provider functions, the test mocks and variable validation are where behaviour has differed, and
tofu plan after tofu init is the real test. The upgrade guide for OpenTofu itself adds that new minor series are
"typically straightforward" to adopt, asks for a tested recovery plan, a clean tofu plan before the switch and a state backup
(tofu state pull for remote state), and describes the rollback.
Interdependent configurations
If several root modules read each other’s outputs through terraform_remote_state, the order of migration matters. The guide
states the rule as "Migrate dependent configurations before their dependencies." The reasoning is that OpenTofu can read
Terraform 1.x state, but Terraform may not reliably read state written by OpenTofu, especially when OpenTofu-specific features
are in use. A consumer that has been migrated can still read a producer’s Terraform state; a Terraform consumer reading a
producer that OpenTofu already wrote may fail. So the order is bottom-up in the dependency graph:
-
Map every use of
terraform_remote_stateand draw which configuration reads which state. -
Identify the leaves, the configurations that nothing else reads from.
-
Back up all state files, not only the ones about to change, and record the Terraform and provider versions. The guide also suggests first bringing all configurations to the same Terraform version.
-
Migrate a leaf, plan and apply it, and confirm that each configuration reading it still plans cleanly.
-
Repeat up the tree and migrate root configurations last, avoiding OpenTofu-specific features until everything has moved.
For Bookshelf the shared network and the secrets configuration are read by the application configuration, so the order is application first, then the network and secrets root modules. Track which configurations run on which tool while the migration is in progress; the CI page shows a matrix that runs both.
What to check in a Bookshelf repository
Go through this list before the first tofu plan:
-
Terraform version. Which Terraform release last wrote the state? Use the matching row in the version table to find constructs that OpenTofu did not yet have at that release.
-
Constructs that OpenTofu lacks.
actionblocks andaction_trigger,listblocks and.tfquery.hclfiles, Stacks files, theterraform_datastoreblock, variables and locals in provider requirements,-minimal-refreshandterraform query. See the Terraform-only section below. -
Backend. An
s3,gcs,azurermorpgbackend carries over. Acloudblock targets HCP Terraform; OpenTofu documents acloudblock for services that implement the cloud backend protocol, and notes that such services may implement only a subset of the features. Confirm what your platform supports before moving a workspace. -
Providers and modules. Run
tofu initin a scratch copy and look for "Failed to query available provider packages" and "Module not found". -
CI. Replace
hashicorp/setup-terraformwithopentofu/setup-opentofu, replace the command names, and keep a separate.terraform.lock.hclstrategy. -
Policy and scanning. Sentinel and Terraform Policy are HCP Terraform features. Conftest, Checkov, Trivy and TFLint work on both tools (see Policy as Code).
-
Provisioners.
winrmconnections were deprecated in OpenTofu 1.12 and removed in 1.13: the 1.13 series reports an error forconnection { type = "winrm" }, and OpenSSH for Windows is the recommended replacement.
Migrating back
The generic guide’s rollback is short. Stop using OpenTofu, restore state from the backups if any state change was made, run
terraform init and terraform plan, and confirm that the plan shows no unexpected changes. That works cleanly only while
nothing OpenTofu-specific has entered the configuration or the state. After a successful tofu apply the state has been
written by OpenTofu, so a state backup taken before the migration is the safe point, and every change since then is lost
unless it is re-applied.
The things that block a return to Terraform are the ones on the other side of the version table:
-
Encrypted state. If OpenTofu encrypts state or plans, Terraform cannot read them. Decrypt first using the documented rollback: keep the original method, add an
unencryptedmethod, move the original intofallback, makeunencryptedthe primary method, setenforced = false, apply, and only then remove thestateblock. See the encryption section below. -
OpenTofu-only syntax. The
encryptionblock, providerfor_each,enabled, backend variables,const, thelanguageblock andoci://module sources must be removed or replaced. Moving such code into.tofufiles (below) makes this a matter of deleting files, because Terraform does not load.tofufiles. -
Lock file. Terraform will record
registry.terraform.ioentries; review the diff. -
Version gaps. Ephemeral values exist in Terraform from 1.10 and in OpenTofu from 1.11, so code written for OpenTofu 1.11 that uses them needs Terraform 1.10 or later to go back, and
enabledhas no Terraform equivalent at all.
The OpenTofu pages do not describe a supported way to go back after state has been rewritten beyond the backup-and-restore procedure, so treat the state backup as the only guaranteed path.
Writing configuration for both tools
A repository that must work with both tools has three techniques available.
.tofu files
OpenTofu code lives in files ending in .tf or .tofu; the JSON variants end in .tf.json or .tofu.json. When a .tf and
a .tofu file with the same base name exist in the same directory, OpenTofu loads only the .tofu file and ignores the
.tf one: with foo.tf and foo.tofu, foo.tofu wins. The same rule applies to override files
(override.tofu and names ending in _override.tofu) and to test files, where .tofutest.hcl and .tofutest.json are read
by tofu test and take precedence over .tftest.hcl and .tftest.json of the same base name. The documentation’s use case
is exactly this: a module author ships a .tofu file that OpenTofu prefers while the .tf file with the same base name
remains for Terraform. Terraform does not load files with the .tofu extension, which is why a file with an OpenTofu-only
construct and no .tf twin is simply invisible to Terraform. The .tofu mechanism was added in OpenTofu 1.8.
# versions.tf -- read by Terraform; OpenTofu ignores it because versions.tofu exists
terraform {
required_version = ">= 1.8"
}
# versions.tofu -- read by OpenTofu only
language {
compatible_with {
opentofu = ">= 1.12"
}
}
Source: OpenTofu Docs — Files and directory structure, OpenTofu Docs — Override files and OpenTofu Docs — Settings
The split into versions.tf and versions.tofu is the one the OpenTofu settings page recommends: required_version is kept
for Terraform compatibility, and older OpenTofu releases treated it as an OpenTofu constraint, so a versions.tofu file holds
either an OpenTofu required_version (when older OpenTofu releases must be supported) or a language block, and the
versions.tf file with Terraform’s constraint is ignored by OpenTofu when versions.tofu is present. The language block is
accepted only in OpenTofu 1.12 and later and is not recognised by any Terraform version. Its compatible_with block has an
opentofu argument, and other tools may define their own arguments. It may also carry edition, whose only valid keyword
today is tofu2024 and has no effect, and experiments, a literal list of keywords for experimental features, which a module
should omit unless it uses one.
Keep to the intersection
The other technique is not to use the extensions at all and write only constructs that appear in both columns of the version
table for the versions you support. A short list of what that rules out in practice: backend or encryption blocks that use
variables, provider for_each, enabled, -exclude, oci:// sources, and, in the other direction, action and list blocks, the
terraform_data store block and variables in provider requirements. Many teams find that the discipline is cheap
because the extensions are used at the edge (the backend, the encryption) rather than in modules.
Run both in CI
The third technique tests the configuration with both tools, so that a use of an extension is caught the day it is written. The CI page of this section explains the pipeline in depth; the shape is a matrix over the tool.
name: validate
on: pull_request
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
tool: [terraform, tofu]
steps:
- uses: actions/checkout@v7
- uses: hashicorp/setup-terraform@v4
if: matrix.tool == 'terraform'
- uses: opentofu/setup-opentofu@v2
if: matrix.tool == 'tofu'
- run: ${{ matrix.tool }} fmt -check -recursive
- run: ${{ matrix.tool }} init -backend=false
- run: ${{ matrix.tool }} validate
Source: opentofu/setup-opentofu and hashicorp/setup-terraform
The action majors are the ones current when this page was written: actions/checkout v7, hashicorp/setup-terraform v4 and
opentofu/setup-opentofu v2. Both setup actions install a wrapper script around the binary by default; each has a setting to
turn the wrapper off if you depend on exact exit codes or output. See
CI/CD and Automation for the full workflow with saved plans and OIDC.
OpenTofu-only features
The features in this section are documented by OpenTofu and are not in the Terraform 1.17 reference pages that were checked while writing. Each one is introduced with the OpenTofu version that added it, taken from that version’s changelog. A configuration that uses any of them no longer runs on Terraform.
| Feature | Since | In one line |
|---|---|---|
State and plan encryption |
1.7 |
Client-side encryption of state and plan files with key providers and methods. |
Early evaluation of variables and locals |
1.8 |
Variables and locals in |
|
1.8 |
OpenTofu-specific files that take precedence over |
Provider |
1.9 |
One aliased provider block yields several provider instances. |
|
1.9 |
Plan and apply everything except an address and what depends on it. |
OCI registries |
1.10 |
|
|
1.11 |
Zero-or-one instance without |
Dynamic |
1.12 |
Lifecycle arguments that accept module symbols or forget instead of destroy. |
|
1.12 |
Variables usable in module sources and backends, and OpenTofu-specific version constraints. |
State and plan encryption
OpenTofu 1.7 introduced optional client-side encryption of state and plan files, for local storage and for every backend. The data is encrypted before it leaves the machine, so the backend only ever stores ciphertext. The state and backend pages of this section show the mechanism in outline (see State and Backends and Remote State); this section is the complete reference.
The encryption block lives inside terraform and has these parts:
| Part | Role |
|---|---|
|
Produces the key material. Types: |
|
Does the encryption, taking its key from a provider through |
|
Select the method for each kind of data. Each may have |
|
A |
A first configuration
The documentation’s passphrase example uses a variable with a default for brevity. The Bookshelf version has no default: the
passphrase arrives through TF_VAR_passphrase, in line with the secrets guidance of this section.
terraform {
required_version = ">= 1.8"
encryption {
key_provider "pbkdf2" "bookshelf" {
passphrase = var.passphrase
}
method "aes_gcm" "bookshelf" {
keys = key_provider.pbkdf2.bookshelf
}
state {
method = method.aes_gcm.bookshelf
enforced = true
}
plan {
method = method.aes_gcm.bookshelf
enforced = true
}
}
}
variable "passphrase" {
type = string
sensitive = true
}
# The passphrase must be at least 16 characters. Read it from a secret manager, never from the repository.
export TF_VAR_passphrase="$(read-from-your-secret-manager bookshelf-state-passphrase)"
tofu init
tofu apply
The command read-from-your-secret-manager is a placeholder for whatever your organisation uses. The block is adapted from
the documented example; the documentation says to change the passphrase to at least 16 characters and to run tofu apply
after the setup. Config values used for encryption can be variables and locals, but not state data or provider-defined
functions, because they must be resolved during tofu init.
The key providers differ in what they need, as the documentation lists them:
| Key provider | Arguments and notes |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
All providers accept encrypted_metadata_alias, which fixes the name stored in the encrypted file’s metadata so that the
provider can be renamed later. The aes_gcm method needs a key of 16, 24 or 32 bytes. The documentation warns that short or
static keys cause "key saturation", and recommends either PBKDF2 with a long passphrase or a KMS with automatic rotation.
The external method takes encrypt_command and decrypt_command, and unencrypted takes no arguments and exists only to
migrate to or from encryption.
A KMS-backed configuration
In production a KMS key is usually preferable to a passphrase because the key rotates and access is audited. The block below uses placeholders for the key identifier and region, and one key per state, as the documentation recommends so that each state is isolated and separately governed.
terraform {
encryption {
key_provider "aws_kms" "catalog" {
kms_key_id = "alias/bookshelf-catalog-state"
key_spec = "AES_256"
region = "eu-west-1"
}
method "aes_gcm" "catalog" {
keys = key_provider.aws_kms.catalog
}
state {
method = method.aes_gcm.catalog
}
}
}
Supplying the configuration with TF_ENCRYPTION
The environment variable TF_ENCRYPTION supplies the contents of the encryption block without the terraform { encryption { } }
wrapper. It is parsed as HCL or JSON and is merged with the code, and where both define the same item the environment
overrides the code. That makes it the way to keep key settings out of the repository or to switch them per environment. The
documentation recommends enforced = true whenever configuration comes partly from the environment, so that a missing
variable cannot silently produce an unencrypted write.
export TF_ENCRYPTION=$(cat <<EOF
key_provider "pbkdf2" "bookshelf" {
passphrase = "${BOOKSHELF_STATE_PASSPHRASE}"
}
method "aes_gcm" "bookshelf" {
keys = key_provider.pbkdf2.bookshelf
}
state {
method = method.aes_gcm.bookshelf
enforced = true
}
EOF
)
tofu plan
The shell expands ${BOOKSHELF_STATE_PASSPHRASE} before OpenTofu sees the text, so the variable must be set from a secret store
in the pipeline and must not be printed. The structure of the heredoc follows the documented one.
Enabling encryption on an existing project
OpenTofu refuses to read plain-text state once encryption is configured, as a guard against tampered files. To encrypt a
project that already has state, add the unencrypted method as a temporary fallback.
terraform {
encryption {
method "unencrypted" "migrate" {}
key_provider "pbkdf2" "bookshelf" {
passphrase = var.passphrase
}
method "aes_gcm" "bookshelf" {
keys = key_provider.pbkdf2.bookshelf
}
state {
method = method.aes_gcm.bookshelf
fallback {
method = method.unencrypted.migrate
}
}
}
}
Run tofu apply, which rewrites the state encrypted; then remove the fallback block and optionally set enforced = true.
Repeat for plan { } if plans are stored. Rolling back is the mirror image: keep the original method, add the unencrypted
method, move the original method into fallback, make unencrypted the primary method, set enforced = false, apply, and
remove the state block afterwards. The original method must not be removed or changed until the migration has finished.
The same fallback mechanism rotates a passphrase, switches KMS system or renames a provider: OpenTofu tries the new method
first, falls back to the old one on read, and always writes with the new one. The Azure provider is the exception, since
switching between symmetric and asymmetric keys, or changing the symmetric key size, must change the existing key provider in
place.
The cautions are worth restating. Once enabled, OpenTofu cannot read the state without the key, so back up the key and an
unencrypted copy of the state first and test the restore. Do not rename key providers or methods afterwards unless
encrypted_metadata_alias is set. If two projects exchange state through terraform_remote_state and use different keys,
give them the same encrypted_metadata_alias where key names conflict, and bind a method to the data source in
remote_state_data_sources. Encryption protects data at rest; it does not protect against data loss, replay of an older
state or plan, or the person running tofu. A deprecated provider or method remains supported for one more minor version and
prints a warning on plan and apply.
Early evaluation: variables and locals in static positions
Terraform cannot know a state backend before it has initialised, so its backend block accepts only literal values. OpenTofu
1.8 added early evaluation: the backend block, the source and version of module blocks and the encryption block may
use input variables and locals, provided they resolve during tofu init, before state is available. References to state
data and provider-defined functions are not allowed. Terraform later added the module source and version half of this in
1.15 (see the version table), but still not the backend.
terraform {
required_version = ">= 1.8"
backend "s3" {
bucket = local.state_bucket
key = "bookshelf/catalog/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
}
}
variable "environment" {
type = string
}
locals {
state_bucket = "bookshelf-${var.environment}-tfstate"
}
TF_VAR_environment=dev tofu init
The documentation’s own example is a single local used for region; the bucket name above is a Bookshelf adaptation of the
same rule. The practical points: the value must be known at init time (a variable that is not supplied is prompted for or
fails), the values are written to .terraform/ just like -backend-config values, and the documentation advises against
carrying secrets this way and recommends environment variables for credentials. -backend-config files and .tfbackend files
remain supported, and a repository that must also run under Terraform keeps using them.
Variables can also drive module sources. The const argument of a variable states explicitly that its value may be used as a
constant, that is, without access to state, in module sources and backends. const = true allows it, const = false forbids
it even if the value would qualify, and when const is left out OpenTofu inspects the configuration to decide. The argument
arrived in OpenTofu 1.12.
variable "network_module_version" {
type = string
const = true
}
module "network" {
source = "example-org/network/docker"
version = var.network_module_version
}
The module address above is a placeholder. The pattern with a local also works for a Git source such as
${local.modules_repo}/storage${local.modules_version}, which is the documentation’s own example. Mind the limit stated
there: the source and version fields cannot reference state data or provider-defined functions. The modules page of this
section covers sources in general (see Modules).
Provider for_each
OpenTofu 1.9 added for_each to aliased provider blocks, so one block produces an instance per element of a map, object or
set of strings. The instance is selected with a bracketed key, aws.by_region[each.key]. This is useful for Bookshelf’s
covers bucket when covers must be replicated to several regions, since it removes one hand-written alias per region.
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
variable "replica_regions" {
type = map(object({
bucket_suffix = string
}))
}
provider "aws" {
alias = "by_region"
for_each = var.replica_regions
region = each.key
}
resource "aws_s3_bucket" "covers_replica" {
for_each = var.replica_regions
provider = aws.by_region[each.key]
bucket = "bookshelf-covers-${each.value.bucket_suffix}"
}
The documented rules: the for_each argument can be used only together with alias; the collection is a map, an object or a
set of strings, and the keys become the instance keys; the provider reference (aws.by_region) must stay static and only the
bracketed key is dynamic; provider arguments must be known before apply, so variables are safe and resource attributes are
not; and a child module can receive a provider instance through providers while declaring the aliases it expects with
configuration_aliases. Removing an element is the usual trap: a provider instance has to remain in the configuration until
the resources that use it are destroyed, so removal takes extra rounds (see
Providers for the workaround).
Excluding addresses from a plan
-exclude=ADDRESS is the complement of -target. It focuses planning on resource instances that do not match the address and
that do not depend on it. The two options are mutually exclusive, and the documentation says to use -exclude only in
exceptional circumstances, as it does for -target. Since OpenTofu 1.10 the errors about unknown count and for_each
values recommend -exclude over -target, and 1.10 added the file forms -exclude-file=FILENAME and
-target-file=FILENAME.
# Plan everything except the DNS module and whatever depends on it
tofu plan -exclude=module.dns
# Same, with the addresses taken from a file
tofu plan -exclude-file=excluded-addresses.txt
Source: OpenTofu Docs — tofu plan
The Terraform 1.17 plan reference does not list -exclude, -concise, -json-into or -show-sensitive. OpenTofu has the
first and several more: -concise (1.10) suppresses progress messages, -json-into=FILE (1.12) writes the machine-readable
output to a file while the human-readable output stays on the terminal, -show-sensitive (1.9) stops redacting sensitive
values, -consolidate-warnings=false turns off warning consolidation, and -deprecation takes module:all (the default),
module:local or module:none to control deprecation warnings.
OCI registries
OpenTofu 1.10 added support for registries that implement the OCI Distribution specification (version 1.1.0) as a source of
module packages and as a provider mirror. The term here is the Open Container Initiative, the same registries that hold
container images; it is unrelated to Terraform’s oci state backend for Oracle Cloud Infrastructure, which has the same
three letters.
A module source uses the oci:// prefix, the registry domain and a repository. The default tag is latest, and the tag or
digest query argument selects another one, only one of the two at a time.
module "network" {
source = "oci://registry.example.com/bookshelf/modules/network?tag=v1.0.0"
}
Source: OpenTofu Docs — Module sources
OpenTofu looks for credentials in the places where the Docker CLI, Podman, Buildah and ORAS store them, so an existing
docker login is usually enough. As a provider mirror an OCI registry is a secondary installation source only: it cannot yet
be the primary source for a provider. The mirror is configured in the CLI configuration file, with a repository template and
include patterns that select which providers come from it.
provider_installation {
oci_mirror {
repository_template = "registry.example.com/bookshelf-providers/${namespace}/${type}"
include = ["registry.opentofu.org/*/*"]
}
}
The template must interpolate every part of the source address that include does not fix exactly (hostname, namespace
and type are available). Pushing a mirror has strict requirements: tags are version numbers with an underscore in place of
the plus sign in build metadata, the index manifest has the artifactType application/vnd.opentofu.provider, each platform
manifest has application/vnd.opentofu.provider-target, and each image manifest has one archive/zip layer with the provider
package. The documentation recommends ORAS 1.3.0 or later and pushing to a local directory first. In 1.13 the command
tofu providers lock gained the -oci-mirror option, and OCI installs track separate transient credentials per repository.
The enabled meta-argument
OpenTofu 1.11 added enabled, written inside a lifecycle block, as an alternative to count = var.flag ? 1 : 0 for a
resource or module that exists zero or one times. Unlike count, a single instance keeps its plain address, so there is no
[0] to write, and migrating from count needs no moved block: OpenTofu moves the state from the indexed instance to the
plain one.
terraform {
required_version = ">= 1.11"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.0"
}
}
}
variable "enable_cache" {
type = bool
default = true
}
resource "docker_image" "redis" {
name = "redis:7-alpine"
}
resource "docker_container" "cache" {
name = "bookshelf-cache"
image = docker_image.redis.image_id
lifecycle {
enabled = var.enable_cache
}
}
output "cache_name" {
value = try(docker_container.cache.name, "not-created")
}
When enabled is false the resource evaluates to null, and referencing it directly is an error at plan or apply, so access
it with try() or a null check, as the output does. The rules: a resource or module cannot combine enabled with count
or for_each; the value must be a known, non-null, non-sensitive, non-ephemeral boolean; and it works on module blocks as
well. The lifecycle page of this section compares it with count (see
Lifecycle, Validation and Checks).
Dynamic prevent_destroy and destroy = false
Terraform processes lifecycle arguments before it evaluates arbitrary expressions, and its reference says that only literal
values can be used in the block. OpenTofu 1.12 allows prevent_destroy to refer to other symbols of the same module, such as
input variables, so a shared module can protect a production database by default while a development caller turns the
protection off. OpenTofu 1.12 also added the destroy lifecycle argument: destroy = false makes OpenTofu forget the
instance, removing it from state without destroying the real object. It accepts only constant booleans and is persisted in
state.
variable "protect_database" {
type = bool
default = true
}
resource "docker_volume" "postgres_data" {
name = "bookshelf-postgres-data"
lifecycle {
prevent_destroy = var.protect_database
}
}
removed {
from = docker_container.legacy_web
lifecycle {
destroy = false
}
}
The removed block is the documented example; its from address cannot include an instance key. Terraform 1.16 also lists
destroy = false in its changelog, although the Terraform 1.17 resource reference page checked for this page does not document
it. A tofu destroy that forgets resources exits with a non-zero status unless -suppress-forget-errors is given (1.12).
More from OpenTofu 1.9 to 1.13
These are the other additions that matter in practice, grouped by release. Each is taken from the changelog of that version.
-
1.9. Variable validation can reference other variables, data sources and similar; provider-defined functions work in validation;
-consolidate-warningsand-consolidate-errors;tofu consoletakes multi-line expressions;tofu testerrors on invalid override and mock fields. -
1.10. Deprecated input variables and outputs warn callers;
elementaccepts negative indices;&&and||short-circuit;removedblocks acceptlifecycleandprovisionerconfiguration;movedblocks move remote objects between resource types;tofu showgains-stateand-plan=PLANFILE; thepgbackend stores several states withtable_nameandindex_nameand uses finer locks, and a v1.10pgbackend must not share a database with older versions; the global provider cache is safe for concurrent use; thehttpbackend supportstofu force-unlock. -
1.11. Ephemeral values, resources and write-only attributes;
enabled; non-ephemeral variables can be supplied at apply time if the values match the plan; thes3backend can tag state and lock objects; line-by-line diffs of multiline strings in lists;mock_provideracceptsfor_eachin tests;issensitivereturns unknown for unknown input. -
1.12.
constvariables; thelanguageblock; dynamicprevent_destroy;destroy = false;importblocks acceptidentityinstead ofid; deprecation warnings for deprecated resource attributes and blocks, controlled by-deprecation=;replace_triggered_byalso triggers when the referenced resource is replaced; concurrent provider downloads;-json-into;localbackend writes pretty-printed state;azurermsupports Azure DevOps workload identity federation. -
1.13. The
convertfunction and theassume...family give OpenTofu type hints and assertions about unknown values, so module authors can reduce unknowns during planning, and the author must ensure the hints hold at apply; Windows on ARM64 is officially supported; saved plan files include provider schemas, sotofu showoften does not launch providers;tofu testsupports instances, including wildcards, in resource overrides;plan -outworks withlifecycle { destroy = false }replacements; two experiments, symbol libraries (reusable functions and types imported like modules) and linting with-lint, are early and their syntax may change.
Terraform-only features
The reverse list is the set of Terraform features that the OpenTofu documentation does not have. The statement "not in OpenTofu" always means "not found in the OpenTofu documentation or command list at the time of writing", and OpenTofu may add any of them later; the project’s issue tracker is the place to check a specific one.
| Terraform feature | Since | OpenTofu |
|---|---|---|
Stacks ( |
1.13 (CLI) |
The Stacks documentation describes an HCP Terraform feature. OpenTofu has no |
HCP Terraform integration (the |
long-standing |
OpenTofu documents a |
|
1.14 |
Not in the OpenTofu documentation. The tracking issue |
|
1.14 |
Not present; the OpenTofu command list has no |
Terraform Policy and the |
1.17 (stated as generally available in the changelog) |
Not present; it is an HCP Terraform and Terraform feature. The Terraform Policy documentation still carried a beta label when checked (see Policy as Code). |
|
1.16 |
Not found in the OpenTofu documentation. |
Variables and locals in provider requirements |
1.17 |
Not documented by OpenTofu. |
|
1.17 |
Not documented by OpenTofu. |
|
1.16 |
Not checked individually; verify against |
Source: Terraform v1.17 CHANGELOG, Terraform v1.16 CHANGELOG, Terraform v1.14 CHANGELOG, Terraform v1.13 CHANGELOG, OpenTofu Docs — CLI commands, OpenTofu Docs — Cloud backend, Terraform Docs — terraform plan and OpenTofu issue 3309 — Support action blocks
Two categories of "parity" deserve a separate mention. Ephemeral values and write-only attributes exist in both tools, but at
different versions (Terraform 1.10 and 1.11, OpenTofu 1.11), which is a version question rather than a feature gap. The test
framework exists in both too, with different file extensions allowed (.tofutest.hcl in OpenTofu), and OpenTofu’s
mock_provider accepts a source argument pointing to a file or directory of provider overrides in the unreleased 1.14.
Choosing between them
There is no universally correct answer, and the useful part of the decision is knowing which facts decide it.
Choose Terraform when you use HCP Terraform or Terraform Enterprise with its integrated policy, no-code and Stacks features, when your platform vendor supports Terraform and not OpenTofu, when you want the actions, query and Terraform Policy features described above, or when your organisation has a commercial relationship with IBM and HashiCorp and the BSL poses no difficulty. The BSL restricts offering Terraform in competition with IBM’s paid products; it does not restrict running it for your own infrastructure.
Choose OpenTofu when your organisation requires an OSI-approved licence or a vendor-neutral governance model, when you build
a product or a platform that runs infrastructure code on behalf of others, when you want state encryption without extra
tooling, when the early-evaluation, provider for_each, -exclude and enabled features remove real workarounds from your
code, or when you want OCI as a module and provider distribution channel.
Support both when you maintain modules that other teams use with either tool. Keep to the intersection of the version
table, put OpenTofu-only constructs in .tofu files, and run the CI matrix.
| Question | Terraform | OpenTofu |
|---|---|---|
Licence |
BSL 1.1 (each release converts to MPL-2.0 after four years) |
MPL-2.0 |
Governance |
IBM (HashiCorp) |
Linux Foundation project, CNCF Sandbox since 2025-04-23; steering committee and public RFCs |
Newest release in this section |
1.17.0 (2026-10-21) |
1.13.2 (2026-10-21) |
Registry |
|
|
Encrypting state and plans |
Rely on the storage service |
Built in ( |
Variables in backend configuration |
No |
Yes, when resolvable at |
Multi-region providers |
Static aliases |
Static aliases and provider |
Conditional single resource |
|
|
Actions, query, Terraform Policy, Stacks |
Yes |
No |
HCP Terraform features |
Yes |
A |
Whatever you choose, make the choice visible: put the tool and its version in a version file (.opentofu-version or
.terraform-version) that tenv or the CI action reads, pin the minor version, and write the decision down. A team that
silently has some engineers on terraform and some on tofu against the same state will sooner or later write a state
the other tool cannot read.
Pitfalls
-
Running both tools against the same state. Once OpenTofu has applied, the state is OpenTofu’s. Terraform may not read it reliably, and cannot read it at all once it is encrypted. Choose one tool per state, and migrate bottom-up when states reference each other.
-
Treating the version numbers as equal. Terraform 1.14 is not OpenTofu 1.14. Use the version table, and test the exact pair you depend on.
-
Enabling encryption without a key backup. Lose the key and the state is gone. Back up the key and an unencrypted state, and test the restore before enabling
enforced. -
Renaming a key provider or method. The ciphertext carries the names. Use
fallbackorencrypted_metadata_alias. -
Forgetting that early evaluation needs a value at init time.
tofu initfails or prompts for a variable used in the backend that is not set. Pass it with-var, a var file orTF_VAR_. -
Using a variable for a secret in the backend block. The value is written to
.terraform/and, per the documentation, to plan files; use environment variables for credentials. -
Removing an element from a provider
for_eachand from its resources in one change. The provider instance has to stay until its resources are gone. -
Sharing one lock file between the two tools. The hostnames differ, so the entries conflict; use full hostnames or one lock file per tool.
-
Pending changes at migration time. Apply or discard them under Terraform first.
-
A Docker base image from
ghcr.io/opentofu/opentofu. Unsupported since 1.10; copy the binary in a multi-stage build. -
Assuming a feature is unavailable because a book says so. The books are older than the features; check the changelog.
What changed recently
-
OpenTofu 1.13.0 (2026-09-30): WinRM provisioner connections removed (deprecated in 1.12);
convertand theassume...functions; Windows ARM64 as an official platform;providers lock -oci-mirror; new arguments for state encryption key providers (encryption_context,additional_authenticated_data,associated_data);base64gzipoutput is no longer byte-for-byte identical, so resources using it may show updates; macOS 13 or later is required; the last series with official 32-bit builds; experiments for symbol libraries and linting. -
OpenTofu 1.14 (unreleased on the main branch): drops 32-bit builds, ignores
provider_metablocks, lets module outputs declare a type, adds asourceargument tomock_providerand reads*.tfrcfiles from system directories. -
OpenTofu 1.12.0 (2026-05-14): dynamic
prevent_destroy,destroy = false,constvariables, thelanguageblock,identityinimportblocks,-json-into, concurrent provider installation and complete lock-file checksums. -
OpenTofu 1.11.0 (2025-12-09): ephemeral values, resources and write-only attributes; the
enabledmeta-argument;azure_vaultkey provider. -
OpenTofu 1.10.0 (2025-06-24): OCI module sources and provider mirror; S3 locking without DynamoDB; deprecated variables and outputs; external key providers.
-
Terraform 1.17.0 (2026-10-21): variables and locals in provider requirements,
-minimal-refresh, and Terraform Policy announced as generally available in the changelog. -
Terraform 1.16.0 (2026-08-26):
terraform_datastore,importblocks inside modules,destroy = false,graph -format=mermaid, actionon_failuremodes. -
Terraform 1.15.0 (2026-04-29): variables and locals in module
sourceandversion,deprecatedon variables and outputs, theconvertfunction. -
Terraform 1.14.0 (2025-11-19):
actionblocks,listblocks andterraform query. -
Governance: OpenTofu was accepted into the CNCF at Sandbox level on 2025-04-23.
Where the books and older articles are outdated or wrong
-
"OpenTofu is just Terraform 1.5 with a different name." It was, in January 2024. Since then it has its own features (encryption, early evaluation, provider
for_each,enabled, OCI) and has implemented several Terraform features (ephemeral values, write-only attributes, deprecation of variables) on its own schedule. -
"State files stay compatible forever." The FAQ promises compatibility with Terraform state up to 1.5.x. Later versions work with documented changes, and OpenTofu-encrypted state is unreadable by Terraform.
-
"Package managers stop at Terraform 1.5.7." Older sources say this after the licence change; the HashiCorp Homebrew tap and apt and yum repositories ship current Terraform.
-
"OpenTofu has no encryption or testing." It had encryption from 1.7 and a native test framework from 1.6.
-
"Use the
ghcr.io/opentofu/opentofuimage directly." Not supported since 1.10. -
Terraform Cloud. The product has been HCP Terraform since 2024-04-22.
-
Terraform CDK. Do not recommend it; it was archived on 2025-12-10 (see the introduction page).