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.

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 opentofu/opentofu repository is created on GitHub (the creation timestamp reported by the GitHub API).

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.

The same history as a timeline:

timeline title Terraform and the OpenTofu fork 2023-08 : HashiCorp moves to the BSL (08-10) : Manifesto published : OpenTofu repository created (08-16) : Fork announced (08-25) 2023-10 : Terraform 1.6.0 is the first BSL release 2024-01 : OpenTofu 1.6.0 first stable release 2024-04 : OpenTofu 1.7 adds state encryption 2025-02 : IBM completes the HashiCorp acquisition 2025-04 : OpenTofu accepted into the CNCF Sandbox 2026-09 : OpenTofu 1.13.0 2026-10 : Terraform 1.17.0

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

.tf and .tf.json files, blocks, expressions, functions, modules and meta-arguments are the same language. OpenTofu additionally reads .tofu and .tofu.json files and has extensions (early evaluation, provider for_each, the enabled meta-argument, the encryption block). A configuration that uses an extension no longer runs on Terraform.

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 tofu apply the state is written by OpenTofu. A state that OpenTofu has encrypted cannot be read by Terraform at all.

CLI

The commands are the same, with the name tofu. The environment variables keep their TF_ prefix (TF_VAR_, TF_LOG, TF_CLI_ARGS, TF_PLUGIN_CACHE_DIR), and TF_ENCRYPTION is OpenTofu’s own. OpenTofu has no query or stacks command. The .terraform working directory and the terraform.tfstate file name are unchanged.

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 tofu directly.

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 init since OpenTofu 1.10 can pick the equivalent provider on registry.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 after init and tofu providers lock is 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

removed block

1.7

1.7

for_each in import blocks

1.7

1.7

Provider-defined functions

1.8

1.7

issensitive function

1.8

1.7

mock_provider and override_* blocks in tests

1.7

1.8

Variables and locals in module source and version

1.15

1.8

moved blocks between resource types

1.8

1.10

Built-in terraform provider functions (encode_tfvars, decode_tfvars, encode_expr)

1.8

1.10

element accepts negative indices

1.10

1.10

S3 backend locking without DynamoDB (use_lockfile)

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

deprecated on variables and outputs

1.15

1.10

convert function

1.15

1.13

Windows ARM64 builds

1.15

1.13

lifecycle { destroy = false }

1.16

1.12

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.

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

brew update then brew install opentofu. Check with tofu -version.

Debian or Ubuntu

The installer script with --install-method deb, or the manual steps: install apt-transport-https, ca-certificates, curl and gnupg, store the OpenTofu signing keys in /etc/apt/keyrings/, add /etc/apt/sources.list.d/opentofu.list, then sudo apt-get update and sudo apt-get install -y tofu.

Windows

winget install --exact --id=OpenTofu.Tofu, or Scoop: scoop bucket add main then scoop install main/opentofu. If WinGet cannot find the command, %LOCALAPPDATA%\Microsoft\WinGet\Links must be on the PATH.

Standalone

The installer script with --install-method standalone (POSIX) or -installMethod standalone (PowerShell), or the release zip from GitHub.

Container image

ghcr.io/opentofu/opentofu, used only as a build stage (see below).

Version manager

tenv, which also manages Terraform, Terragrunt, Terramate and Atmos.

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.

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:

  1. 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.

  2. Install OpenTofu and check tofu --version.

  3. Run tofu init. Providers and modules are downloaded from the OpenTofu registry and the backend is initialised.

  4. 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.

  5. Run tofu apply. The guide notes that this step updates the state file format if needed, even when there are no infrastructure changes.

  6. 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

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 skip_s3_checksum, and the endpoints sso option or AWS_ENDPOINT_URL

Terraform 1.6, 1.7, 1.8, 1.9 guides

OpenTofu 1.10 documents skip_s3_checksum (it now also disables the AWS SDK default integrity checks); re-check the S3 backend page for the endpoint options before relying on either.

encode_tfvars, decode_tfvars, encode_expr not supported

Terraform 1.8, 1.9 guides

Added to the built-in terraform provider in OpenTofu 1.10.

removed block behaves differently: remove the lifecycle block, and the whole block if it set destroy = true

Terraform 1.7, 1.8, 1.9 guides

OpenTofu 1.10 lets removed blocks carry lifecycle and provisioner configuration; 1.12 adds destroy = false as a lifecycle argument of resources.

override_resource or override_data nested in mock_provider must be restructured

Terraform 1.7, 1.8, 1.9 guides

OpenTofu 1.9 allows both blocks inside a mock_provider.

Variable validation that references other variables or objects must be restructured

Terraform 1.9 guide

OpenTofu 1.9 allows references to variables, data sources and similar in validation.

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:

  1. Map every use of terraform_remote_state and draw which configuration reads which state.

  2. Identify the leaves, the configurations that nothing else reads from.

  3. 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.

  4. Migrate a leaf, plan and apply it, and confirm that each configuration reading it still plans cleanly.

  5. 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. action blocks and action_trigger, list blocks and .tfquery.hcl files, Stacks files, the terraform_data store block, variables and locals in provider requirements, -minimal-refresh and terraform query. See the Terraform-only section below.

  • Backend. An s3, gcs, azurerm or pg backend carries over. A cloud block targets HCP Terraform; OpenTofu documents a cloud block 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 init in a scratch copy and look for "Failed to query available provider packages" and "Module not found".

  • CI. Replace hashicorp/setup-terraform with opentofu/setup-opentofu, replace the command names, and keep a separate .terraform.lock.hcl strategy.

  • 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. winrm connections were deprecated in OpenTofu 1.12 and removed in 1.13: the 1.13 series reports an error for connection { 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 unencrypted method, move the original into fallback, make unencrypted the primary method, set enforced = false, apply, and only then remove the state block. See the encryption section below.

  • OpenTofu-only syntax. The encryption block, provider for_each, enabled, backend variables, const, the language block and oci:// module sources must be removed or replaced. Moving such code into .tofu files (below) makes this a matter of deleting files, because Terraform does not load .tofu files.

  • Lock file. Terraform will record registry.terraform.io entries; 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 enabled has 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"
  }
}

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

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 backend, module source and version, and the encryption block.

.tofu files

1.8

OpenTofu-specific files that take precedence over .tf files of the same name.

Provider for_each

1.9

One aliased provider block yields several provider instances.

-exclude

1.9

Plan and apply everything except an address and what depends on it.

OCI registries

1.10

oci:// module sources and an OCI provider mirror.

lifecycle { enabled }

1.11

Zero-or-one instance without count.

Dynamic prevent_destroy, destroy = false

1.12

Lifecycle arguments that accept module symbols or forget instead of destroy.

const variables and the language block

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

key_provider block (type and name labels)

Produces the key material. Types: pbkdf2, aws_kms, gcp_kms, azure_vault, openbao and external. OVHcloud also maintains a KMS provider built on external in a separate repository.

method block (type and name labels)

Does the encryption, taking its key from a provider through keys. Types: aes_gcm, external and unencrypted.

state { } and plan { }

Select the method for each kind of data. Each may have enforced = true (refuse to read or write unencrypted data) and a fallback { } block naming an older method to try when reading fails; writes always use the primary method.

remote_state_data_sources { }

A default method and named remote_state_data_source entries for terraform_remote_state data sources, addressed as myname, mymodule.myname or mymodule.myname[0].

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

pbkdf2

passphrase (minimum 16 characters) or chain (take the passphrase from another provider, for example an external one). key_length default 32, iterations minimum 200000 and default 600000, salt_length default 32 and hash_function sha256 or sha512 (default sha512; sha1 is not supported).

aws_kms

kms_key_id, key_spec (for example AES_256) and a region; authentication options match the S3 backend. Optional encryption_context, added in 1.13.

gcp_kms

kms_encryption_key and key_length (1 to 1024); authentication matches the GCS backend. Optional additional_authenticated_data (base64), added in 1.13.

azure_vault

vault_uri, vault_key_name and key_length. An asymmetric RSA key uses RSA-OAEP-256; an HSM symmetric key needs symmetric = true and symmetric_key_size. Always authenticates with Entra ID. Added in 1.11.

openbao

key_name, with token (or BAO_TOKEN), address (or BAO_ADDR), transit_engine_path, key_length (16, 32 or 64) and associated_data (added in 1.13). Compatible with Vault 1.14, the last MPL release, but not with later BSL versions.

external

command, an array that runs a program speaking a documented JSON protocol over stdin and stdout. Added in 1.10.

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

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"
}

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-warnings and -consolidate-errors; tofu console takes multi-line expressions; tofu test errors on invalid override and mock fields.

  • 1.10. Deprecated input variables and outputs warn callers; element accepts negative indices; && and || short-circuit; removed blocks accept lifecycle and provisioner configuration; moved blocks move remote objects between resource types; tofu show gains -state and -plan=PLANFILE; the pg backend stores several states with table_name and index_name and uses finer locks, and a v1.10 pg backend must not share a database with older versions; the global provider cache is safe for concurrent use; the http backend supports tofu 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; the s3 backend can tag state and lock objects; line-by-line diffs of multiline strings in lists; mock_provider accepts for_each in tests; issensitive returns unknown for unknown input.

  • 1.12. const variables; the language block; dynamic prevent_destroy; destroy = false; import blocks accept identity instead of id; deprecation warnings for deprecated resource attributes and blocks, controlled by -deprecation=; replace_triggered_by also triggers when the referenced resource is replaced; concurrent provider downloads; -json-into; local backend writes pretty-printed state; azurerm supports Azure DevOps workload identity federation.

  • 1.13. The convert function and the assume... 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, so tofu show often does not launch providers; tofu test supports instances, including wildcards, in resource overrides; plan -out works with lifecycle { 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 (terraform stacks and the Stacks configuration files)

1.13 (CLI)

The Stacks documentation describes an HCP Terraform feature. OpenTofu has no stacks command. Terragrunt, Terramate or separate root modules fill the same role (see Terraform Stacks).

HCP Terraform integration (the cloud block against HCP, Sentinel and OPA enforcement, no-code, run tasks, dynamic provider credentials)

long-standing

OpenTofu documents a cloud block for platforms that implement the cloud backend protocol, and states that such services may implement only a subset of features. HCP Terraform’s own features are HashiCorp’s.

action blocks and action_trigger in lifecycle; terraform plan -invoke

1.14

Not in the OpenTofu documentation. The tracking issue opentofu/opentofu#3309 is open, labelled needs-community-input and pending-decision; a contributor has pushed a fork branch, and nothing is merged.

list blocks, .tfquery.hcl files and terraform query

1.14

Not present; the OpenTofu command list has no query (see Import and Query).

Terraform Policy and the -policies flag

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).

terraform_data store block for ephemeral and sensitive values

1.16

Not found in the OpenTofu documentation.

Variables and locals in provider requirements

1.17

Not documented by OpenTofu.

-minimal-refresh on plan

1.17

Not documented by OpenTofu.

graph -format=mermaid, state show -json, workspace list -json

1.16

Not checked individually; verify against tofu graph, tofu state show and tofu workspace list before relying on them.

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

registry.terraform.io

registry.opentofu.org, searchable at search.opentofu.org

Encrypting state and plans

Rely on the storage service

Built in (encryption block, TF_ENCRYPTION)

Variables in backend configuration

No

Yes, when resolvable at init

Multi-region providers

Static aliases

Static aliases and provider for_each

Conditional single resource

count

count or lifecycle { enabled }

Actions, query, Terraform Policy, Stacks

Yes

No

HCP Terraform features

Yes

A cloud block for compatible services only

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 fallback or encrypted_metadata_alias.

  • Forgetting that early evaluation needs a value at init time. tofu init fails or prompts for a variable used in the backend that is not set. Pass it with -var, a var file or TF_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_each and 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); convert and the assume... 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); base64gzip output 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_meta blocks, lets module outputs declare a type, adds a source argument to mock_provider and reads *.tfrc files from system directories.

  • OpenTofu 1.12.0 (2026-05-14): dynamic prevent_destroy, destroy = false, const variables, the language block, identity in import blocks, -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 enabled meta-argument; azure_vault key 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_data store, import blocks inside modules, destroy = false, graph -format=mermaid, action on_failure modes.

  • Terraform 1.15.0 (2026-04-29): variables and locals in module source and version, deprecated on variables and outputs, the convert function.

  • Terraform 1.14.0 (2025-11-19): action blocks, list blocks and terraform 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/opentofu image 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).

References