Migrating to OpenTofu 1.x while keeping most of your Terraform code intact — from swapping the binary to validating IaC quality with HCL-native tests
In August 2023, HashiCorp switched Terraform's license from MPL v2.0 to BSL (Business Source License), sending a small shockwave through the IaC ecosystem. The full weight of the change only became clear after reading the BSL's competition clause carefully — and almost immediately after, Gruntwork, Spacelift, Harness, and other major toolchain vendors joined forces to create OpenTofu. The momentum accelerated further when IBM's acquisition of HashiCorp completed in February 2025.
This article covers three core topics: how compatible OpenTofu is with Terraform, how an actual migration unfolds, and how systematically you can validate IaC code with tofu test.
One thing to address upfront, honestly. The reason the title says "most code as-is" is intentional. Code that specifies provider sources with full paths (registry.terraform.io/hashicorp/...) requires modification. Codebases that already use the shorthand form (hashicorp/aws) — which is most of them — carry over without changes. Knowing this distinction going in makes the actual difficulty of migration much clearer.
The bottom line: migration is less about technical rewriting and more about binary and pipeline replacement plus organizational change management. Fidelity Investments put it well after migrating over 50,000 state files and 4 million+ resources: "Organizational change management was a greater challenge than the technical barriers."
Core Concepts
Where OpenTofu Comes From
OpenTofu is a fully open-source fork that branches from the last MPL codebase of Terraform 1.5.x. It retains the MPL v2.0 license and operates under multi-organization governance within the Linux Foundation.
Adoption figures reveal the trend. According to Scalr's 2026 survey, approximately 12% of IaC practitioners use OpenTofu, and 27% are considering adopting it (source). Annual download growth of 300%, approximately 9.8 million cumulative downloads (encore.dev, 2026). The numbers make it clear this has diverged beyond a simple fork into an independent product.
Version Roadmap — Unique Features Accumulate from 1.7 Onward
Early in the fork's life, the common question was "what's actually different from Terraform?" — but starting with 1.7, unique features have piled up and the two are now clearly diverging.
| Version | Released | Key Features |
|---|---|---|
| 1.7 | 2024 | State file AES-GCM encryption, import block for_each, dynamic provider-defined functions |
| 1.8 | 2024 | Early variable evaluation including backend configuration, test resource overrides |
| 1.9 | 2025 | Provider iteration with for_each, -exclude flag |
| 1.10 | 2025 | OCI registry support, OpenTelemetry tracing, native S3 locking |
| 1.11 | 2025 | Ephemeral resources and values, write-only attributes, enabled meta-argument |
| 1.12 | 2026 | Dynamic prevent_destroy, destroy = false lifecycle, concurrent provider installation speed improvements |
Native S3 locking (locking without DynamoDB) was introduced first in OpenTofu 1.10, and Terraform added the same capability in its 1.10 with the use_lockfile option. Both tools are converging on the direction of eliminating the burden of managing a DynamoDB table in AWS environments.
Decision Flow for Evaluating Whether to Switch
"So should I switch now or not?" The answer varies by situation, so the flowchart below can serve as a starting point.
Terraform Stacks is an HCP Terraform-exclusive feature not available in OpenTofu. If it's central to your core workflow, it makes sense to first evaluate alternative combinations like Atlantis or Spacelift before making the switch.
Practical Migration
Migration proceeds in five steps. Step 2 (fixing source paths) only applies to codebases that specify full provider paths.
Step 1. Install OpenTofu
On macOS, it's a single Homebrew command.
brew install opentofuOn Linux, use the official install script.
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | shVerify the version after installation:
tofu version
# OpenTofu v1.12.x
# on linux_amd64Step 2. Check Provider Source Paths (Only for Affected Codebases)
If your code explicitly specifies providers with full paths, tofu init will error. For large monorepos, pull a list first.
grep -r "registry.terraform.io" .If any matches are found, do a bulk replace to shorthand form.
# Before — full path specified
terraform {
required_providers {
aws = {
source = "registry.terraform.io/hashicorp/aws"
version = "~> 5.0"
}
}
}
# After — shorthand form
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}Skip this step if your codebase doesn't use full paths.
Step 3. tofu init — The Critical First Step of Migration
Run it directly in your existing Terraform working directory.
tofu init
# To update the existing lock file
tofu init -upgradeIf .terraform.lock.hcl exists when you run tofu init, it will be regenerated based on registry.opentofu.org. Deleting the lock file and regenerating is an option when first migrating, but deleting it unpins your provider versions, which means init may pull the latest versions. To avoid inadvertently switching to unintended provider versions, use the -upgrade flag instead, or if you do delete the lock file, make sure the team reviews the updated versions before committing.
Step 4. Confirm No Drift with tofu plan
Because the state file format is fully shared with Terraform, resources created by prior terraform apply runs are recognized by OpenTofu as-is.
tofu plan
# Plan: 0 to add, 0 to change, 0 to destroy.
# This message means migration is ready to proceedStep 5. Replace the CI/CD Pipeline
For GitHub Actions, replace setup-terraform with opentofu/setup-opentofu.
# .github/workflows/tofu.yml
jobs:
tofu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup OpenTofu
uses: opentofu/setup-opentofu@v1
with:
tofu_version: "1.12.x"
- name: Tofu Init
run: tofu init
- name: Tofu Plan
run: tofu plan -out=tfplan
- name: Tofu Apply
if: github.ref == 'refs/heads/main'
run: tofu apply tfplanState Encryption — A Differentiator Over Open-Source Terraform
This feature does not exist in open-source Terraform (Terraform Enterprise/HCP Terraform offer separate encryption capabilities). You can configure AES-GCM encryption of state files stored in S3 directly within HCL.
# main.tf
terraform {
encryption {
key_provider "pbkdf2" "my_passphrase" {
passphrase = var.state_passphrase
}
method "aes_gcm" "my_method" {
keys = key_provider.pbkdf2.my_passphrase
}
state {
method = method.aes_gcm.my_method
}
}
backend "s3" {
bucket = "my-tofu-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
}
}For production environments, using AWS KMS as the key provider instead of a passphrase is more natural.
key_provider "aws_kms" "my_kms" {
kms_key_id = "arn:aws:kms:ap-northeast-2:123456789012:key/your-key-id"
region = "ap-northeast-2"
}tofu test — HCL-Native Testing Framework
HCL-native testing (terraform test / tofu test) is a concept first introduced in Terraform 1.6 and is not exclusive to OpenTofu. However, in the context of already switching to OpenTofu, tofu test is a practical entry point for immediately introducing an IaC testing culture using only HCL, without a Go environment. Terratest requires knowing Go and setting up a local Go environment; tofu test has none of that friction and works right in the same directory.
Understanding the test types first makes writing them much easier.
Place .tftest.hcl files in a tests/ directory and run tofu test.
Offline Unit Tests with Mock Providers
Runs in CI without AWS credentials. The example below assumes: aws_vpc.main is a single VPC resource, and aws_subnet.private is a subnet resource declared with count = 3.
# tests/vpc.tftest.hcl
mock_provider "aws" {
mock_resource "aws_vpc" {
defaults = {
id = "vpc-offline-test-01"
cidr_block = "10.0.0.0/16"
}
}
mock_resource "aws_subnet" {
defaults = {
id = "subnet-offline-test-01"
availability_zone = "ap-northeast-2a"
}
}
}
run "vpc_cidr_is_correct" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR must be 10.0.0.0/16"
}
}
# Works when aws_subnet.private is declared with count = 3
run "subnet_count_is_three" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "There must be 3 private subnets"
}
}Integration Tests That Apply to Real Cloud and Validate
Creates resources for real, validates state with asserts, and automatically cleans up when the test ends.
# tests/s3_integration.tftest.hcl
# Assumes a module with aws_s3_bucket.main and aws_s3_bucket_versioning.main defined
variables {
bucket_name = "my-test-bucket-tofu-ci"
environment = "test"
}
run "bucket_exists_and_versioned" {
command = apply
assert {
condition = aws_s3_bucket.main.bucket == var.bucket_name
error_message = "Bucket name does not match the variable"
}
assert {
condition = aws_s3_bucket_versioning.main.versioning_configuration[0].status == "Enabled"
error_message = "Bucket versioning must be enabled"
}
}override_resource in 1.8+ — Swap Only Specific Resources with Mocks
Useful when you want to mock only a specific resource while keeping everything else real, rather than mocking the entire provider. aws_db_instance.main must be defined in the module.
# tests/mixed.tftest.hcl
run "use_real_vpc_mock_rds" {
command = plan
override_resource {
target = aws_db_instance.main
values = {
endpoint = "mock-rds.cluster.local"
port = 5432
}
}
assert {
condition = aws_db_instance.main.port == 5432
error_message = "RDS port must be 5432"
}
}Execution is straightforward.
# Run all tests
tofu test
# Run a specific file only
tofu test -filter=tests/vpc.tftest.hcl
# Verbose output
tofu test -verbosePros and Cons Analysis
Objective Comparison Table
| Item | OpenTofu | Terraform OSS |
|---|---|---|
| License | MPL v2.0 (OSI-approved) | BSL 1.1 (includes competition clause) |
| Governance | Linux Foundation | IBM/HashiCorp |
| Cost | Completely free | CLI free, HCP Terraform paid |
| State file encryption | Native support (1.7+) | Not supported |
Provider for_each |
Supported (1.9+) | Not supported |
| OCI registry | Supported (1.10+) | Not supported |
| S3 locking without DynamoDB | Supported (1.10+) | Supported (1.10+, use_lockfile) |
| HCL-native testing | tofu test |
terraform test (1.6+, similar) |
| Terraform Stacks | Not supported | HCP Terraform exclusive |
| Remote Run / Sentinel | Not supported (separate tools needed) | HCP Terraform exclusive |
| Market share | ~12% (Scalr, 2026) | 33%+ (varies by survey) |
| Community resources | Growing | Substantially larger |
Common Mistakes in Practice
Not fixing provider source paths. If your code explicitly uses the registry.terraform.io/hashicorp/... form, tofu init will error. For large monorepos, run grep -r "registry.terraform.io" ahead of time to get the list.
Skipping version review after deleting the lock file. Deleting .terraform.lock.hcl and regenerating with tofu init unpins versions, which may pull the latest provider versions. Use the -upgrade flag instead, or if you do delete and regenerate, make sure the team reviews and commits the updated lock file versions.
Not cleaning up resources after a test apply. tofu test auto-cleans up after apply tests, but resources can linger if the run aborts due to a failure in CI. Adding a tofu destroy to a cleanup hook in your CI pipeline is the safe move.
Missing Terraform Cloud state backend migration. If you're using HCP Terraform's remote backend, it's more natural to first migrate to a self-managed backend (S3, GCS, Azure Blob) before switching to OpenTofu.
Closing Thoughts
To summarize: switching to OpenTofu demands more energy on organizational change management than on the technical migration itself, and the technical work is mostly binary replacement and pipeline updates. As with Fidelity's case, building a success story with a high-usage team first and using that result as an organizational signal is an approach that genuinely works.
Unique features like state encryption, provider for_each, OCI registry, and ephemeral resources are creating a growing feature gap with open-source Terraform. It's already difficult to view this as just a simple fork.
Here are three steps you can start with today.
Step 1 — Run tofu init on one side project. Keeping your existing code as-is and just swapping the binary to see if plan produces identical output is enough to gauge the actual difficulty of migration.
Step 2 — Write a .tftest.hcl for your most frequently changed module. Writing one plan-level unit test with mock_provider — no credentials needed — immediately demonstrates the value of the HCL-native test framework.
Step 3 — Replace one CI pipeline with opentofu/setup-opentofu. Starting with just changing one action name from setup-terraform lets you see exactly what the impact looks like at the actual pipeline level.
References
- OpenTofu Official Migration Documentation
- OpenTofu Migration Guide (Official)
- OpenTofu 1.7.0 Release Announcement — State Encryption
- What's new in OpenTofu 1.9?
- What's new in OpenTofu 1.12?
- tofu test Command Official Documentation
- State and Plan Encryption — OpenTofu Official Documentation
- Fidelity Investments Migration Case Study (OpenTofu Official Blog)
- Fidelity Migration: A DevOps Success Story (Harness)
- Spacelift — How to Migrate From Terraform to OpenTofu
- Spacelift — OpenTofu vs Terraform Comparison
- env0 — Terratest vs OpenTofu Test In-Depth Comparison
- env0 — OpenTofu v1.7 State Encryption Deep Dive
- Scalr — OpenTofu vs Terraform (2026)
- Scalr — The Complete Guide to the OpenTofu Native Test Framework
- ControlMonkey — Terraform to OpenTofu Migration Guide 2025
- encore.dev — OpenTofu vs Terraform in 2026