We moved a Terraform 1.16.6 configuration to OpenTofu 1.13.1, then moved it back, and the resources never noticed. The plan on each side of the move came back with no changes. A Terraform to OpenTofu migration does not change a resource by itself, and any change would have shown in those plans. What it touches is the lock file, every provider address in your state, the version pin you thought was guarding the pipeline, and some of the configuration written for Terraform 1.14 and later.
CloudSpinx writes and hands over infrastructure as code for engineering teams in Kenya and across East Africa, OpenTofu unless a client's tooling needs Terraform, so we ran the move ourselves before telling anyone to make it. That work sits in our DevOps and infrastructure-as-code practice. Four binaries, each checked against its vendor's SHA256SUMS: Terraform 1.16.6, OpenTofu 1.13.1, and OpenTofu 1.12.0 and 1.11.14 to pin down when one behavior changed. The providers were random, null and local, so nothing left the working directory and no remote backend was involved.
Is the Terraform license a reason to move?
For most teams, no. Terraform has shipped under the Business Source License since 1.6.0, and its license file now names IBM as the licensor. The grant lets you "make production use of the Licensed Work", unless that use includes "offering the Licensed Work to third parties on a hosted or embedded basis" to compete with IBM's paid versions. Running your own estate is not that. Selling a platform that runs Terraform for other companies, overlapping what IBM sells, is.
Each version also converts to MPL 2.0 four years after it is published, so Terraform 1.6.0, released on 4 October 2023, becomes available under MPL 2.0 in October 2027.
The honest reasons to move are features and governance. OpenTofu is MPL 2.0 under the Linux Foundation, and it has state encryption, for_each on provider blocks and an -exclude flag for plans. We tried all three on Terraform 1.16.6 and it refused each one. It is a fair question to put to any DevOps consultant you are vetting: which of the two do they write, and what did their last move break?
What the move does to the lock file and the state
The first tofu init in a directory Terraform has been using reinstalls the same provider versions and rewrites the lock file on the way (output trimmed):
$ tofu init
...
Warning: Dependency lock file entries automatically updated
OpenTofu automatically rewrote some entries in your dependency lock file:
- registry.terraform.io/hashicorp/local => registry.opentofu.org/hashicorp/local
- registry.terraform.io/hashicorp/null => registry.opentofu.org/hashicorp/null
- registry.terraform.io/hashicorp/random => registry.opentofu.org/hashicorp/random
The version selections were preserved, but the hashes were not because the
OpenTofu project's provider releases are not byte-for-byte identical.
tofu plan found no changes. The first tofu apply, with nothing to apply, still rewrote the state: terraform_version went from 1.16.6 to 1.13.1, and every resource's provider moved from registry.terraform.io to registry.opentofu.org.
None of that is a trap on its own. Going back worked. terraform init fetched the providers from both registries so it could read that state, which left six entries in the lock file, terraform plan found no changes, and the next terraform apply put every address back. An edit after that planned an ordinary replacement, so Terraform owned the resources again.
The trap is the pipeline. A CI job that runs init -lockfile=readonly, which is the right setting for CI, fails on its first OpenTofu run with Error: Provider dependency changes detected and exit code 1, while the lock file in the repository stays exactly as Terraform wrote it. Commit the rewritten lock file in the same pull request that swaps the binary. If your pipeline pins the Terraform version in a setup step, that line changes in the same pull request too.
Why OpenTofu ignores required_version in a .tf file
Since 1.12.0, OpenTofu takes required_version only from a file ending in .tofu or .tofu.json. In a .tf file it is accepted and ignored. Our 1.13.1 binary planned without complaint against required_version = ">= 2.0", and again against "< 1.10". The parser's own comment says the argument is "ignored unless it appears in a file named with a ".tofu" or similar suffix that indicates it's intended for OpenTofu rather than its predecessor".
That is a sensible rule, because the pin in a Terraform module describes Terraform. It also means a guard you relied on is gone, and nothing tells you. OpenTofu 1.11.14 does the opposite, reading Terraform's required_version = ">= 1.15" as its own and refusing to run:
Error: Unsupported OpenTofu Core version
The fix the OpenTofu settings page recommends is two files, and it planned cleanly on all four binaries we tried:
# versions.tf
terraform {
required_version = ">= 1.15"
}
# versions.tofu: when both exist, OpenTofu skips versions.tf
terraform {
required_version = ">= 1.11"
}
OpenTofu 1.12 also added a language block for the same job. Keep it out of .tf files, because Terraform 1.16.6 stops at Unsupported block type.
- obeys this pin
- no pin in force
- refuses to run
versions.tf, OpenTofu 1.12 and later obeys no pin at all and 1.11 obeys Terraform's. A versions.tofu beside it gives each binary one constraint of its own.Terraform 1.16.6 ignored every .tofu file we gave it, so adding one changes nothing for a team still on Terraform. That is what makes it safe to do before the move rather than during it.
Terraform 1.14 and later blocks OpenTofu will not parse
The fork has moved in both directions, and a module that has kept up with Terraform's releases will not all come across. We wrote one small configuration per feature and ran init and validate on Terraform 1.16.6 and OpenTofu 1.13.1. Terraform accepted every row.
| Written for Terraform | Added in | OpenTofu 1.13.1 |
|---|---|---|
action block with an action_trigger | 1.14 | Unsupported block type |
type on an output block | 1.15 | Unsupported argument |
import block inside a child module | 1.16 | Import blocks are only allowed in the root module |
store block in terraform_data | 1.16 | Unsupported block type |
ephemeral resources | 1.10 | accepted |
lifecycle { destroy = false } | 1.16 | accepted |
deprecated on a variable | 1.15 | accepted |
Module sources taken from a variable split the other way. Terraform 1.16.6 insisted on const = true (Only literal values and const variables can be evaluated during init), and OpenTofu took the variable with or without it.
So before anyone swaps a binary, run tofu init and tofu validate in every root module on a branch. It costs nothing, and it turns each refused row into a file and a line number. If your modules use actions, imports inside modules or store blocks, the move includes rewriting those modules, and that rewrite is the expensive part of leaving Terraform. Send the repository layout and your Terraform version through the scoping form if you would rather we ran that sweep and told you what it found.

Encrypted state is the one-way door
Encryption is the best reason to move, and the only change to state we found that Terraform cannot follow you back through. Terraform writes resource attributes to state as plain text: the 24-character random_password we generated sat in terraform.tfstate, readable by anyone holding the file, marked sensitive only in the metadata beside it. OpenTofu encrypts the state body, leaving only the lineage, the serial, a format tag and the key metadata readable. With a key provider, an aes_gcm method and an unencrypted fallback for the existing plain state, all in a .tofu file, which Terraform ignores (its validate still passed), one tofu apply rewrote the state as encrypted_data plus key metadata. Leave out the fallback and tofu init stops at encountered unencrypted payload without unencrypted method configured.
Terraform could not read the encrypted state at all, with or without -lock=false:
Error: Unsupported state file format
The state file does not have a "version" attribute, which is required to
identify the format version.
OpenTofu with the encryption block removed refused as well: This state file is encrypted and can not be read without an encryption configuration.
The way back exists only while you hold the key. Swap the two methods, plain text as the primary and the old method as the fallback, run tofu apply once, and the state is ordinary JSON again. Terraform planned no changes after that, which is the rollback OpenTofu's encryption documentation describes. Lose the key and there is no way back, for either tool.
Move first and encrypt a few weeks later. Until you encrypt, Terraform's own init, one apply and a second init put the lock file and the state back the way Terraform wrote them, so you learn whether OpenTofu suits your team while the door is still open. When you do encrypt, use a cloud KMS key provider rather than a passphrase in a file, keep a backup of the key away from the state bucket, and write the decrypt step into the runbook before anyone needs it.
- crosses, comes back
- crosses, stops working
- refused
Both binaries wrote plain state as format version 4. The encrypted file has no version attribute at all, which is the exact field Terraform's error names.
Terraform vs OpenTofu: who should stay
Stay on Terraform if your runs live in HCP Terraform. Its workspace setting chooses "the Terraform version to use for all operations in the workspace" and the settings page names no other engine, so on our reading the runs have to leave the platform before the binary matters, and that is a project of its own. We did not test whether OpenTofu can keep its state there in local execution mode. Stay too if your modules lean on actions, imports inside modules or store blocks and you sell nothing built on Terraform. The license already permits what you do, and a rewrite buys you nothing you asked for.
Move if you want the tool to encrypt state, or if you sell a hosted service that runs Terraform for other companies, the case the license's competitive-offering clause is about. It is also the default we build on when the choice is ours: one Google Cloud estate we rebuilt as code is thirty-three OpenTofu modules across eight projects.
Most teams sit between the two. Shared modules, CI on GitHub Actions or GitLab, state in a bucket, and none of the refused rows above. Add the versions.tofu files now, since Terraform does not read them. Then move one root module at a time with its lock file in the same commit, and leave encryption for last. We did not test remote backends here, so read every plan before you trust it.
Send us the Terraform version, where your state lives, how many root modules you run and the CI file that runs plan, through the DevOps scoping form or on WhatsApp at +254 713 403 044. We reply with what in this post applies to your code and one figure for the move, or a short note saying stay where you are, free and with nothing to sign. If you go ahead, we handle the move with your team, one root module at a time, and leave the decrypt runbook in your repository.