OpenTofu Migration Guide: Moving 200+ Terraform Modules Without Downtime
HashiCorp's BSL licensing change forced a reckoning across the infrastructure-as-code community. OpenTofu — the Linux Foundation-backed fork — emerged as the open-source alternative. But how do you actually migrate a production Terraform estate without breaking everything?
One-field diagnostic start
Send one work email. Yash replies with the matching service path, first evidence step, and owner handoff for this issue.
One owner, one affected system, and the next buyer or recovery deadline mapped.
# OpenTofu Migration Guide: Moving 200+ Terraform Modules Without Downtime
HashiCorp's BSL licensing change forced a reckoning across the infrastructure-as-code community. OpenTofu — the Linux Foundation-backed fork — emerged as the open-source alternative. But how do you actually migrate a production Terraform estate without breaking everything?
We've done this migration for three clients now, totaling 200+ modules across multiple AWS accounts. Here's the complete playbook.
Why Migrate Now?
The technical argument is simple: OpenTofu maintains full compatibility with Terraform's HCL syntax and state format. The strategic argument is stronger: your infrastructure tooling shouldn't be subject to unilateral licensing changes by a single vendor.
OpenTofu 1.8+ introduced client-side state encryption — a feature Terraform doesn't offer. The fork is diverging in useful ways.
Pre-Migration Audit
Step 1: Provider Inventory
Not all providers require changes. Run this audit:
# List all providers in use
grep -r "required_providers" --include="*.tf" -l | \
xargs grep "source" | sort -uMost providers (AWS, GCP, Azure, Cloudflare) work identically. Watch for:
hashicorp/consul and hashicorp/vault — these still work but check version compatibilityStep 2: Binary Reference Scan
# Find every reference to 'terraform' binary
grep -rn "terraform " scripts/ .github/ .gitlab-ci.yml Makefile \
--include="*.sh" --include="*.yml" --include="*.yaml" --include="Makefile"We typically find 30-50 references across CI/CD configs, wrapper scripts, Makefiles, and documentation.
Step 3: State File Compatibility Test
OpenTofu reads Terraform state files natively. Verify:
tofu init
tofu state list # Should match terraform state list exactly
tofu state show aws_instance.web # Spot-check a few resourcesMigration Execution
Phase 1: Install OpenTofu
# Linux (snap)
snap install --classic opentofu
# macOS (brew)
brew install opentofu
# Docker
docker pull ghcr.io/opentofu/opentofu:latestPhase 2: CI/CD Pipeline Updates
GitHub Actions:
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: "1.8.0"
- run: tofu init
- run: tofu plan -out=plan.tfplanGitLab CI:
image: ghcr.io/opentofu/opentofu:1.8
script:
- tofu init -backend-config="..."
- tofu planAtlantis: Update repos.yaml to use tofu binary. Atlantis supports OpenTofu natively since v0.28.
Phase 3: Lock File Migration
# Remove old lock file
rm .terraform.lock.hcl
# Regenerate with OpenTofu
tofu init -upgradeThe new lock file (.terraform.lock.hcl — same name) will reference the same provider versions.
Phase 4: Staged Rollout
1. Week 1: Migrate non-critical modules (dev/staging) 2. Week 2: Run tofu plan against production — compare output with last terraform plan 3. Week 3: Switch production pipelines 4. Week 4: Remove Terraform binary, update documentation
Post-Migration: Leveraging OpenTofu-Only Features
Client-Side State Encryption
terraform {
encryption {
method "aes_gcm" "default" {
keys = key_provider.aws_kms.default
}
state {
method = method.aes_gcm.default
}
}
}This encrypts state at rest — before it reaches your backend. Terraform doesn't offer this.
Common Pitfalls
1. Wrapper scripts with version checks — Scripts that run terraform version and parse output will break. Grep for version-parsing logic in your automation. 2. Terragrunt compatibility — Terragrunt supports OpenTofu since v0.54. If you're on an older version, update Terragrunt first, then migrate the binary. 3. IDE plugins — VS Code's Terraform extension works with OpenTofu out of the box. JetBrains users need the separate OpenTofu plugin from the marketplace. 4. Documentation drift — Search-replace "terraform" with "tofu" in all runbooks, READMEs, and onboarding docs. This is easy to forget and causes confusion for new team members. 5. Terraform Cloud/Enterprise dependencies — If you're using Terraform Cloud for remote state or runs, you'll need an alternative. OpenTofu works with any S3/GCS/Azure Blob backend, plus Spacelift and env0 support OpenTofu natively.
Testing Strategy
Don't just run tofu plan and call it done. Build a proper validation matrix:
#!/bin/bash
# migration-test.sh — run against each module
set -euo pipefail
MODULE_DIR=$1
cd "$MODULE_DIR"
# 1. Init
tofu init -upgrade 2>&1 | tee /tmp/tofu-init.log
# 2. Validate
tofu validate
# 3. Plan (compare with last terraform plan)
tofu plan -out=tofu.plan -no-color > /tmp/tofu-plan.txt
# 4. State operations
tofu state list > /tmp/tofu-state.txt
# 5. Compare resource counts
TF_COUNT=$(wc -l < /tmp/tf-state.txt 2>/dev/null || echo 0)
TOFU_COUNT=$(wc -l < /tmp/tofu-state.txt)
if [ "$TF_COUNT" != "$TOFU_COUNT" ]; then
echo "WARNING: Resource count mismatch! TF=$TF_COUNT TOFU=$TOFU_COUNT"
exit 1
fi
echo "Module $MODULE_DIR: PASS"Run this against every module in staging before touching production. We scripted this and ran it across all 200+ modules in under an hour.
Results
For our largest migration (200+ modules, 3 AWS accounts):
The migration is far less scary than it looks. The state compatibility is rock-solid. The only real work is updating your CI/CD pipelines and wrapper scripts — the infrastructure itself doesn't change at all.
Should You Wait?
No. Here's why:
The best time to migrate was when BSL was announced. The second best time is this weekend.
---
Need help migrating your Terraform estate to OpenTofu? We've done this at scale. Book a free consultationBook a free consultationhttps://techsaas.cloud/contact or explore our DevOps servicesDevOps serviceshttps://techsaas.cloud/services.
Need the next owner and evidence step mapped?
Send the current system and deadline. Yash replies with the service path, first proof artifact, and handoff owner.