OpenTofu is a drop-in replacement for Terraform with an open-source MPL 2.0 license. Migration is straightforward for most projects — the CLI, HCL syntax, and state format are compatible. This guide covers the complete migration process including CI/CD updates and potential gotchas.
Prerequisites
- Existing Terraform project (any version 1.x)
- Access to your state backend (S3, GCS, Azure, etc.)
- Ability to update CI/CD pipelines
Step 1: Install OpenTofu
macOS
brew install opentofu
tofu version
Linux (Ubuntu/Debian)
curl -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method deb
tofu version
Linux (RHEL/Fedora)
curl -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method rpm
tofu version
Docker
docker pull ghcr.io/opentofu/opentofu:1.9
docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/opentofu/opentofu:1.9 init
Step 2: Test in Your Project
cd your-terraform-project/
# Remove Terraform's cached providers (OpenTofu will re-download)
rm -rf .terraform
rm .terraform.lock.hcl
# Initialize with OpenTofu
tofu init
# Plan — should show NO changes
tofu plan
If tofu plan shows no changes, your migration is 90% done. The state file, HCL syntax, and providers all work.
Step 3: Verify State Compatibility
# Check state is readable
tofu state list
# Verify a specific resource
tofu state show aws_instance.web
# Compare with what Terraform showed
# (If you still have terraform installed)
terraform state list # Should match exactly
Step 4: Update CI/CD
GitLab CI
# Before
stages:
- plan
- apply
plan:
image: hashicorp/terraform:1.10
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths: [tfplan]
apply:
image: hashicorp/terraform:1.10
script:
- terraform init
- terraform apply -auto-approve tfplan
# After
plan:
image: ghcr.io/opentofu/opentofu:1.9
script:
- tofu init
- tofu plan -out=tfplan
artifacts:
paths: [tfplan]
apply:
image: ghcr.io/opentofu/opentofu:1.9
script:
- tofu init
- tofu apply -auto-approve tfplan
GitHub Actions
# Before
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.10.0"
# After
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: "1.9.0"
- run: |
tofu init
tofu plan
tofu apply -auto-approve
Atlantis
# repos.yaml
repos:
- id: "/.*/",
workflow: opentofu
workflows:
opentofu:
plan:
steps:
- env:
name: ATLANTIS_TERRAFORM_VERSION
value: "1.9.0"
- init
- plan
Step 5: Update Wrapper Scripts
If you have shell scripts or Makefiles:
# Before
TF = terraform
# After — support both with a fallback
TF = $(shell command -v tofu 2>/dev/null || echo terraform)
plan:
$(TF) init
$(TF) plan
apply:
$(TF) init
$(TF) apply -auto-approve
Or use an alias for gradual migration:
# ~/.bashrc
alias terraform='tofu'
Step 6: Update required_version (Optional)
terraform {
# Before — Terraform only
required_version = ">= 1.7.0"
# After — allow both Terraform and OpenTofu
required_version = ">= 1.7.0"
# OpenTofu respects the same constraint
}
Step 7: Enable State Encryption (Optional)
One advantage of OpenTofu — client-side state encryption:
terraform {
encryption {
key_provider "aws_kms" "state_key" {
kms_key_id = "alias/terraform-state"
region = "us-east-1"
}
method "aes_gcm" "default" {
keys = key_provider.aws_kms.state_key
}
state {
method = method.aes_gcm.default
enforced = true
}
plan {
method = method.aes_gcm.default
enforced = true
}
}
}
After enabling encryption, the state file is encrypted before it reaches your backend. Even if someone accesses your S3 bucket, they can’t read the state without the KMS key.
Common Migration Issues
Provider Registry
OpenTofu uses registry.opentofu.org by default but falls back to registry.terraform.io. Most providers work without changes:
# Works in both — source is the same
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Lock File Differences
OpenTofu generates .terraform.lock.hcl in the same format. However, the hashes may differ between Terraform and OpenTofu for the same provider version. Re-run:
tofu providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64
HCP Terraform (Terraform Cloud) Backend
This is the main migration blocker. If you use HCP Terraform as your backend:
# This does NOT work with OpenTofu
terraform {
cloud {
organization = "my-org"
workspaces { name = "production" }
}
}
You need to migrate to a different backend first:
# Switch to S3 backend
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
# Migrate state from HCP to S3
terraform init -migrate-state
# Then switch to OpenTofu
tofu init
Sentinel → OPA
If you use Sentinel policies (HCP Terraform only), you’ll need to rewrite them as OPA/Rego policies:
# policy/no-public-s3.rego
package terraform.analysis
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls != true
msg := sprintf("S3 bucket %s must block public ACLs", [resource.address])
}
Migration Checklist
- Install OpenTofu on local machines
- Run
tofu init+tofu plan— verify no changes - Update CI/CD pipelines (image + command)
- Update wrapper scripts/Makefiles
- Regenerate
.terraform.lock.hcl - Migrate off HCP Terraform backend (if applicable)
- Rewrite Sentinel → OPA policies (if applicable)
- Enable state encryption (optional)
- Test apply in non-production first
- Update team documentation
Hands-On Courses
- Terraform for Beginners on CopyPasteLearn
- Terraform By Example — practical code examples
Conclusion
Migrating from Terraform to OpenTofu is a 30-minute task for most projects: install OpenTofu, run tofu init, verify tofu plan shows no changes, and update your CI/CD pipeline. The main blocker is HCP Terraform — if you use it as a backend, you’ll need to migrate state to S3/GCS first. For self-hosted backends, the migration is seamless.