DEV Community

Cover image for Migrating from Terraform to OpenTofu: A Low-Risk Playbook
James Joyner
James Joyner

Posted on

Migrating from Terraform to OpenTofu: A Low-Risk Playbook

I've migrated a few real environments from Terraform to OpenTofu now, and the good news is that a careful migration is almost boring. The state format is compatible, the CLI is a near drop-in, and the whole thing can be done with a rollback path at every step. The bad news is that "almost boring" still has a couple of sharp edges, and the teams that get hurt are the ones who skip the parity check and go straight to apply. Here's the calm, low-risk playbook I actually follow.

Step 0: Know what "low-risk" means here

The core insight that makes this safe: OpenTofu reads the same HCL and the same state file that Terraform does. A migration is not a rewrite — it's swapping which binary talks to your existing state. That means at almost every step, your rollback is just "keep using the terraform binary." As long as you don't trigger a one-way-door feature (more on those later), you can walk back.

So the whole strategy is: prove parity before you change anything real, change one thing at a time, and keep the old binary installed until you're confident.

Step 1: Check and pin your Terraform version first

Before you touch OpenTofu, get your current setup deterministic. OpenTofu forked from the last MPL-licensed Terraform, so very old or very new Terraform configs can have edges. Find out exactly what you're running:

terraform version
Enter fullscreen mode Exit fullscreen mode

Pin it. If you're not already using a version manager or a pinned CI image, do that now — you want a fixed, known-good Terraform baseline to compare against and to fall back to. Also pin your provider versions in a lockfile:

terraform providers lock
Enter fullscreen mode Exit fullscreen mode

A migration where both the tool version and the provider versions are floating is a migration where you can't tell what caused a diff. Lock everything down first.

Step 2: Install tofu alongside, not instead

Install OpenTofu without removing Terraform. On a workstation or a scratch CI runner:

# Verify the binary is there and note the version
tofu version
Enter fullscreen mode Exit fullscreen mode

Keep both terraform and tofu on PATH during the migration. You'll be running them back to back to compare, and having both is what makes rollback trivial. As of 2026 the install methods and current versions are in the OpenTofu docs — check them rather than trusting a version number from a blog post (including this one).

Step 3: The parity check — init and plan, no apply

This is the heart of the whole exercise. Work on a copy or a non-production workspace first. Point OpenTofu at your existing configuration and existing state, initialize, and produce a plan — but do not apply.

# Fresh working dir state, same backend/state as before
tofu init

# The critical test: does OpenTofu see zero changes?
tofu plan -out tofu.plan
Enter fullscreen mode Exit fullscreen mode

What you want to see is a clean, no-changes plan. If OpenTofu reads your Terraform-written state and reports that nothing needs to change, you have parity. That's the green light.

If the plan shows drift, stop and read it carefully before doing anything. Common causes I've hit:

  • Provider version differences. OpenTofu resolved a slightly different provider than your pinned Terraform lockfile. Reconcile the versions.
  • Registry source differences. OpenTofu uses its own registry; a provider or module might resolve from a different source. Verify the provider actually publishes where OpenTofu looks.
  • A genuinely different interpretation of some config. Rare, but read the diff — do not apply your way past it.

Do this parity check per module/workspace, not once globally. State lives per-workspace, and a clean plan in one doesn't guarantee a clean plan in another.

Step 4: Apply once, deliberately, in a safe place

Once you've got a clean plan in a non-prod workspace, run the apply there so OpenTofu writes state at least once:

tofu apply tofu.plan
Enter fullscreen mode Exit fullscreen mode

Even a no-op apply may rewrite state metadata. That's fine and expected — but it's the moment worth noting, because after OpenTofu writes state, that workspace's state has been touched by tofu. Terraform can generally still read it, but this is the point where you start treating that workspace as "OpenTofu-managed." Do it somewhere you can afford to be wrong before you do it in production.

Step 5: Swap CI, one pipeline at a time

Now change the automation. In your CI config, this is usually as small as swapping the binary and the command name:

# Before
terraform init && terraform plan -out plan.tfout

# After
tofu init && tofu plan -out plan.tfout
Enter fullscreen mode Exit fullscreen mode

Roll it out per-pipeline, lowest-stakes environment first. Keep the plan-review gate in your pipeline — a human or a required approval looking at the plan output — for the first few runs on each environment. The whole point of a slow rollout is that if OpenTofu ever produces a plan you didn't expect, you catch it at plan time, not after apply.

I also recommend keeping a terraform-based fallback job available (even if disabled) during the transition, so reverting CI is a one-line change rather than an archaeology project.

Step 6: Watch for the one-way doors

Everything above is reversible as long as your config stays compatible with both tools. The way you lose your rollback is by adopting an OpenTofu-only feature. The big ones to be aware of:

  • Native state/plan encryption. Once OpenTofu encrypts your state, stock Terraform can't read it. This is a feature you may want — but adopt it as a deliberate, post-migration decision, not mid-migration.
  • Early variable evaluation in backend blocks or module sources. Configs that rely on it won't parse under Terraform.
  • .tofu / .tofu.json override files and provider-defined functions via the provider:: namespace. Both are OpenTofu-specific surface area.

My rule during the migration window: change the tool, not the config. Keep your HCL dual-compatible until every environment is on OpenTofu and stable. Only then start adopting the divergent features — and when you do, understand you're closing the door behind you. If you want more detail on the specific compatibility gotchas and error messages these features throw, I keep a running set of OpenTofu troubleshooting notes from real migrations.

Step 7: How to actually roll back

If something goes wrong before you've crossed a one-way door, rollback is genuinely simple:

  1. Switch the binary back. In CI and locally, tofu becomes terraform again.
  2. Re-init with Terraform so its lockfile and provider selections are in place: terraform init.
  3. Run a plan and confirm a clean, no-change result: terraform plan.
  4. Restore state from backup only if you actually corrupted or encrypted it. This is why you keep versioned state — an S3 bucket with versioning, or whatever your backend offers, so you can retrieve the pre-migration state object.

Back up your state before you start. A cheap tofu state pull > backup.tfstate (or the Terraform equivalent) before the first apply gives you a plain escape hatch. I've never had to use it on a careful migration, but the whole reason the migration feels calm is that the backup exists.

Takeaway

Migrating from Terraform to OpenTofu is mostly a swap, not a rewrite, and the parity check is what makes it safe: prove OpenTofu reads your existing state with a clean plan before you change anything real. Pin your versions, keep both binaries installed, roll CI out one environment at a time, and don't adopt one-way-door features until you're fully migrated and stable. Do it in that order and the scariest part of the whole thing will be how uneventful it is.

Top comments (0)