DEV Community

secopslog for SecOpsLog

Posted on • Originally published at secopslog.com

Migrating from Terraform to OpenTofu, safely

Move an existing project, safely.

Migrating from Terraform to OpenTofu is less an engine transplant than a badge swap. OpenTofu began as a line-for-line fork of Terraform 1.5, so your .tf files and your state mean exactly the same thing to tofu as they did to terraform; your .terraform.lock.hcl is understood in the identical format, the one file tofu init will regenerate as it re-sources provider hashes from OpenTofu's registry. The migration is mostly deleting one binary from your habits and pointing your muscle memory at another. The care goes not into rewriting code but into proving that nothing changed: the acceptance test for a good migration is that the very first tofu plan against your existing state comes back empty. Everything below is about earning that empty plan and having a way back if you don't get it.

Back up state before you touch anything

Even a drop-in replacement deserves a rollback plan, because state is the only irreplaceable artifact in the project. Your .tf files live in git and can be re-checked-out at will, but the state file is the map from that code to the real resources it owns; lose it or corrupt it and you own orphaned infrastructure with no way to plan against it. So copy the state and the dependency lock file before you run a single tofu command. If you're on a remote backend, pull a local snapshot with the terraform binary while it's still installed, and note the exact Terraform version that last wrote the state, because that version dictates whether OpenTofu can read it cleanly.

# Local backend: copy the state and the provider lock
cp terraform.tfstate terraform.tfstate.backup.pre-tofu
cp .terraform.lock.hcl .terraform.lock.hcl.backup

# Remote backend: snapshot state to a local file first
terraform state pull > pre-tofu.tfstate

# Record which Terraform version wrote this state (matters below)
terraform version
Enter fullscreen mode Exit fullscreen mode

Swap terraform for tofu and confirm an empty plan

With the tofu CLI installed, the operative commands are the same three you already run. tofu init re-reads your backend and re-resolves providers (now from the OpenTofu registry) and rewrites .terraform.lock.hcl with OpenTofu-sourced hashes for the same provider versions. Then tofu plan is your verdict. A migration that worked prints No changes. Your infrastructure matches the configuration. If tofu instead wants to add, change, or destroy anything, stop and read the diff: a non-empty plan almost never means your infrastructure drifted overnight. It means a provider version or source address resolved differently, so fix the resolution rather than applying the plan.

tofu init            # re-resolves providers, rewrites the lock with OpenTofu hashes
tofu plan            # the acceptance test -- expect an empty diff

# Expected tail of a clean migration:
# No changes. Your infrastructure matches the configuration.

# The regenerated .terraform.lock.hcl is a repo file, not state --
# commit it to version control so CI resolves identical hashes:
git add .terraform.lock.hcl

# A clean, empty-plan cutover needs no apply. Your next real
# infrastructure change is the first 'tofu apply' you run.
Enter fullscreen mode Exit fullscreen mode

What changes — and what reassuringly doesn't

Very little of your configuration is actually Terraform-specific, and the one construct people fear they must rip out survives the fork. OpenTofu kept the terraform {} settings block name, so you rename nothing, and it still honours TF_VAR_, TF_WORKSPACE, TF_CLI_ARGS, and every other TF_-prefixed environment variable, so none of your CI variables change (OpenTofu also adds TOFU_-prefixed equivalents that win when both are set). Even the cloud {} block lives on: OpenTofu retains it, and its cloud backend documents connecting to HCP Terraform (formerly Terraform Cloud) or Terraform Enterprise, so you are not forced to delete it just to run tofu. What genuinely does not cross the fork are HCP Terraform's proprietary features — Sentinel policy sets, run tasks, drift detection, no-code modules — which are HashiCorp-only and have no OpenTofu equivalent. So the real decision is platform, not syntax: keep the cloud block if you're staying on HCP or Terraform Enterprise, or, if you're leaving that platform entirely, swap it for a standard backend such as S3 or GCS. Only the CLI you invoke is guaranteed to change.

# The cloud block is NOT Terraform-only -- OpenTofu keeps it, and its
# cloud backend can target HCP Terraform or Terraform Enterprise:
terraform {
  cloud {
    organization = "acme"
    workspaces { name = "prod" }
  }
}

# Only if you are LEAVING that platform entirely do you swap it for a
# standard backend:
terraform {
  backend "s3" {
    bucket = "acme-tofu-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Match your OpenTofu version to the Terraform you're leaving: State is broadly forward-compatible: OpenTofu 1.6 forked from Terraform 1.5/1.6, and later OpenTofu releases deliberately track Terraform's state format, so tofu reads state written by Terraform 1.5 through 1.9 without a format wall. Two real hazards remain. First, the version marker: state records the version that last wrote it, and a tool can refuse state stamped by a version newer than itself with a 'state snapshot was created by … which is newer than current' error — so if a teammate already ran terraform apply with a version ahead of the OpenTofu you install, install an OpenTofu release at least as new before you migrate. Second, config features: a project using a Terraform-only feature newer than your OpenTofu (for example a 1.8+ provider-defined function) can fail tofu init or tofu plan even when the state itself reads fine — again cured by installing a recent-enough OpenTofu and migrating from a version you control rather than one someone has already leapfrogged. Never hand-edit the version or terraform_version fields to force a load; you'll risk corrupting resource decoding and losing the state you were trying to save. Finally, treat the cutover as one-way: add a guard in CI so nobody keeps running terraform apply against tofu-managed state and quietly stamps the marker back the other way.


This lesson is part of the free *OpenTofu** course at SecOpsLog — hands-on, command-first DevSecOps tutorials. Read the original with the full interactive version →*

Top comments (0)