Migrate a Terraform Project to OpenTofu (Safely, with State and CI)
Quick answer
OpenTofu forked from Terraform 1.5.x and is state-compatible, so migrating is mostly a binary swap — but 'mostly' is where projects get burned. This tutorial walks the safe path: back up state, install tofu, gate on a no-change plan, fix provider registry and lock-file differences, and cut CI over deliberately.
- Step 1: Pin Your Versions and Back Up State
- Step 2: Install the OpenTofu CLI
- Step 3: Select a Non-Prod Workspace
- Step 4: Initialize with OpenTofu
- Step 5: The Safety Gate — Confirm a No-Op Plan
intermediate · 45 min
Before you begin
- An existing Terraform project (>= 1.5) with a remote backend
- terraform and git installed and working
- Ability to install the tofu CLI on your machine and CI runners
- A non-prod workspace or environment you can migrate and test first
OpenTofu forked from Terraform at version 1.5.x, back when Terraform's license changed from MPL to the BUSL. Because it's a fork of the same codebase, OpenTofu speaks the same HCL, reads the same state format, and understands the same backend configuration. That's what makes migrating "mostly a binary swap" — you point the tofu CLI at the project you already have, and it picks up your backend and state as-is. If you're still deciding whether the switch is worth it, read OpenTofu vs Terraform or our toolkit comparison first; this tutorial assumes you've decided to move.
The catch is that "mostly" hides the parts that actually bite: OpenTofu resolves providers from registry.opentofu.org instead of registry.terraform.io, so your .terraform.lock.hcl needs regenerating; any Terraform-1.6+ features you adopted after the fork may not exist yet; and HCP Terraform-only features (Sentinel, Stacks) simply don't run on OpenTofu. This is a controlled cutover, not a sed on your CI file.
The whole strategy hinges on one gate: after swapping binaries, tofu plan must report no changes. If it does, your state and config are compatible and you can proceed. If it doesn't, you stop and investigate before anything touches real infrastructure. We'll do the entire thing on a non-prod workspace first.
What You'll Build
- A verified backup of your remote state before any tool touches it
- OpenTofu installed alongside Terraform, both usable side by side
- A no-op
tofu planon a non-prod workspace — the safety gate that proves compatibility - A regenerated
.terraform.lock.hclwith providers sourced fromregistry.opentofu.org - A CI pipeline calling
tofuinstead ofterraform, cut over deliberately - A documented rollback path back to Terraform
Step 1: Pin Your Versions and Back Up State
Before anything, record exactly what you're running so you can reproduce or revert. State is the one thing you cannot regenerate — treat the backup as non-negotiable.
terraform version # note the exact version, e.g. 1.5.7 or 1.9.x
terraform providers # list provider sources and versions in use
# Pull a local copy of the remote state as a backup
terraform state pull > state-backup-$(date +%Y%m%d-%H%M%S).tfstateStore that backup somewhere outside the working directory (an S3 prefix, a secure bucket, wherever your team keeps break-glass artifacts). If your backend supports versioning — S3 versioning, Terraform Cloud state history — confirm it's on. That's your real safety net.
Also confirm the state isn't locked before you start; a stale lock will block both terraform and tofu. If you hit lock errors, see fixing the Terraform state lock error.
Step 2: Install the OpenTofu CLI
Install tofu without removing terraform — you want both available so you can compare plans and roll back instantly.
1# macOS (Homebrew)
2brew install opentofu
3
4# Linux (standalone installer script)
5curl -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
6chmod +x install-opentofu.sh
7./install-opentofu.sh --install-method standalone
8rm install-opentofu.sh
9
10# Verify
11tofu versiontofu version should print an OpenTofu version (1.6 or newer). Keep both binaries on your PATH; the commands are identical (tofu init, tofu plan, tofu apply), which is exactly why the swap is low-friction.
Step 3: Select a Non-Prod Workspace
Never migrate production first. Pick a dev or staging workspace and confirm you're on it before running anything.
terraform workspace list # see available workspaces
terraform workspace select dev # or use your non-prod backend/var setupIf you separate environments by directory or backend config rather than workspaces, cd into the non-prod one instead. The point is the same: prove the migration on something you can break without paging anyone.
Step 4: Initialize with OpenTofu
Run tofu init in the project. OpenTofu reads your existing backend configuration and connects to the same remote state Terraform was using — no state migration, no re-import.
tofu initUnder the hood it re-runs backend initialization and downloads providers. Because OpenTofu pulls from registry.opentofu.org, it fetches the OpenTofu-hosted mirror of your providers. For the mainstream providers (AWS, Google, Azure, Kubernetes, Helm) these mirror the same upstream binaries, so this normally just works. If a provider fails to resolve here, jump to Step 6 before going further.
Step 5: The Safety Gate — Confirm a No-Op Plan
This is the single most important step. Run a plan and demand that OpenTofu reports no changes.
tofu planYou want to see:
No changes. Your infrastructure matches the configuration.
That output means OpenTofu read your state, evaluated your config, and found the live infrastructure already matches — i.e. the migration introduced zero drift. Do not apply anything if the plan shows changes. A non-empty plan after only swapping binaries signals a real incompatibility (a provider version difference, a post-1.5 language feature, or a lock-file mismatch), and applying it could modify or destroy resources. Investigate first; the Common Issues section below covers the usual culprits.
If it's clean, you've proven compatibility on this workspace.
Step 6: Regenerate the Lock File for OpenTofu's Registry
Your .terraform.lock.hcl records provider checksums keyed to registry.terraform.io. After migrating you want it to reflect registry.opentofu.org sources and OpenTofu-verified checksums. Regenerate it:
tofu init -upgradeIf you build and apply across multiple OSes (local macOS, Linux CI), record hashes for every platform so CI doesn't fail on a missing checksum:
tofu providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64 \
-platform=darwin_amd64Commit the updated .terraform.lock.hcl. Then re-run tofu plan one more time to confirm the refreshed lock file still yields No changes. Only after this second clean plan should you consider the workspace migrated.
Step 7: Cut Over CI
With the CLI proven locally, update your pipeline to call tofu. The commands map one-to-one, so this is mostly a rename plus installing the binary on the runner.
1# Example GitHub Actions step
2# - name: Setup OpenTofu
3# uses: opentofu/setup-opentofu@v1
4# with:
5# tofu_version: "1.9.0"
6#
7# - run: tofu init -input=false
8# - run: tofu plan -input=false -out=tfplan
9# - run: tofu apply -input=false tfplanIf your CI wraps commands in a Makefile or shell script, change the binary name there and keep the flags identical:
# Before
terraform init && terraform plan -out=tfplan
# After
tofu init && tofu plan -out=tfplanRoll this out per environment in the same order you validated: dev first, watch a full plan/apply cycle succeed, then promote to staging and finally production. Keep the terraform binary installed on runners until every environment is confirmed — that's your instant rollback lever.
Step 8: Know What You're Leaving Behind
OpenTofu is a fork, not a mirror. Some Terraform features are HCP Terraform / Terraform Enterprise-only and do not run on OpenTofu:
- Sentinel policy-as-code — does not run on OpenTofu. Migrate policy enforcement to Open Policy Agent (OPA/Conftest) or OpenTofu-compatible tooling.
- Terraform Stacks and other HCP Terraform-exclusive workflow features.
- Any provider or module that resolves only from
registry.terraform.iowith no OpenTofu registry mirror — rare, but check niche/private providers.
Also flag any config that relies on Terraform features added after the 1.5 fork point. OpenTofu has since shipped its own features (some overlapping, some unique), so a given 1.6+ Terraform construct may be present, absent, or named differently. Grep your code for newer functions and blocks and confirm each against the OpenTofu docs before you rely on it.
Step 9 (Optional): Adopt OpenTofu-Only Features
Once you're fully on OpenTofu, you can use things Terraform doesn't offer — most notably client-side state encryption, which encrypts state and plan files at rest without depending on backend-level encryption:
1terraform {
2 encryption {
3 key_provider "pbkdf2" "mykey" {
4 passphrase = var.state_passphrase # supply via TF_VAR or a secrets manager
5 }
6 method "aes_gcm" "encrypt" {
7 keys = key_provider.pbkdf2.mykey
8 }
9 state {
10 method = method.aes_gcm.encrypt
11 }
12 plan {
13 method = method.aes_gcm.encrypt
14 }
15 }
16}Adopt these deliberately and last. The moment you write encrypted state or use an OpenTofu-only construct, you've crossed a one-way door: rolling back to Terraform is no longer a clean binary swap (see Tear Down).
Common Issues
- Provider fails to resolve on
tofu init— a provider in your config has no mirror onregistry.opentofu.org, or its source address is pinned toregistry.terraform.io. Set an explicitsourceinrequired_providersand re-runtofu init -upgrade. For private providers, point OpenTofu at your own registry/mirror. - Lock-file checksum mismatch in CI — your
.terraform.lock.hcllacks hashes for the CI runner's platform. Runtofu providers lockwith every-platformyou build on and commit the result. tofu planshows unexpected changes — usually a provider version difference between what Terraform resolved and what OpenTofu resolved. Compareterraform providersvstofu providersoutput and pin the version inrequired_providersso both tools agree.- Version-constraint or unknown-function errors — you're using a Terraform 1.6+ feature that doesn't exist (or differs) in your OpenTofu version. Check the OpenTofu docs for the equivalent, or upgrade OpenTofu to a version that supports it.
- Stale state lock blocks init/plan — a previous run left the lock held. Confirm no run is active, then release it (
force-unlockas a last resort). Details in the state lock fix guide.
Frequently Asked Questions
Do I need to migrate or convert my state file?
No. OpenTofu forked from Terraform 1.5.x and reads the same state format from the same backends, so tofu init connects to your existing remote state with no conversion. This is the core reason the migration is low-risk — your state is the compatibility anchor, which is exactly why you back it up but don't rewrite it.
Can I roll back to Terraform after switching to OpenTofu?
Yes, as long as you haven't adopted OpenTofu-only features. State written by OpenTofu is compatible back to the 1.5.x fork point, so reverting is another binary swap plus a terraform init. The exception: if you enabled state encryption or used constructs Terraform doesn't understand, Terraform can't read that state — so treat those features as a one-way door.
Why does tofu plan show changes right after migrating?
The most common cause is that OpenTofu resolved a different provider version than Terraform did, producing a diff in computed attributes. Compare provider versions between the two tools and pin them in required_providers. Other causes are a post-1.5 Terraform feature OpenTofu handles differently, or a lock file that still references registry.terraform.io. Never apply a surprise plan — investigate first.
Will Sentinel policies keep working on OpenTofu?
No. Sentinel is an HCP Terraform / Terraform Enterprise feature and does not run on OpenTofu. If you rely on Sentinel for policy-as-code, plan to move that enforcement to Open Policy Agent (OPA), Conftest, or another OpenTofu-compatible policy engine as part of the migration, not after.
Do I have to update every provider source address in my code?
Usually not. OpenTofu maps well-known provider sources to its own registry automatically, so mainstream providers resolve without edits. You only need explicit source addresses in required_providers for providers that don't have an OpenTofu mirror or that you host privately — and pinning source and version explicitly is good practice regardless.
Tear Down
Rolling back is the reverse binary swap, valid only if you avoided OpenTofu-only features:
1# Point Terraform back at the same project and state
2terraform init
3
4# Confirm a no-op plan under Terraform, exactly as you did for OpenTofu
5terraform plan # expect: No changes.
6
7# Restore CI to call terraform instead of tofuIf you did adopt OpenTofu-only features (state encryption, Stacks, unique functions), you cannot cleanly roll back — Terraform won't read that state. In that case, restore the pre-migration state backup from Step 1 into a Terraform-compatible backend and reconcile drift manually. This is why you keep the backup and delay OpenTofu-only features until the migration is proven and committed.
Official References
- OpenTofu — Migrate from Terraform — Official migration guide and supported version matrix
- OpenTofu — Installation — All install methods (Homebrew, standalone, package managers, CI actions)
- OpenTofu — State encryption — Client-side state and plan encryption configuration
- OpenTofu Registry — Provider and module registry that OpenTofu resolves against
- opentofu/setup-opentofu — Official GitHub Action for installing
tofuin CI
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.