
Author: Steven Crickman
Edited by: Vahe Aslanyan
For nine years, Terraform was open source in the way most engineers mean the term: from its first release in 2014 through mid-2023, every line shipped under the Mozilla Public License 2.0 (MPL 2.0), an OSI-approved license with no restrictions on commercial use, redistribution, or competing products. In August 2023, HashiCorp changed course, relicensing all new Terraform releases under the Business Source License 1.1 (BUSL, sometimes written BSL). <cite index="9-1">This license permitted the community to fork the codebase, modify it, contribute to it, and deploy it on their own servers, without placing new restrictions on typical end users — but it did strictly impact distributors selling managed Terraform products, who now needed a commercial license from HashiCorp.</cite>
The community response was immediate. <cite index="6-1">Within weeks, major cloud providers, infrastructure vendors, and individual contributors formed the OpenTofu initiative</cite>, forking the last MPL-licensed release (Terraform 1.5.x) and placing the result under Linux Foundation governance as a permanently MPL 2.0-licensed project.
In December 2024, IBM completed its acquisition of HashiCorp for $6.4 billion, adding another layer of consideration for organizations already uneasy about the license change: Terraform's roadmap, pricing, and long-term stewardship are now a function of IBM's broader cloud portfolio strategy, not an independent company's. <cite index="1-1">2025–2026 saw the two projects diverge in meaningful technical ways — OpenTofu shipped native state encryption and provider-defined functions faster than Terraform, while Terraform focused on AI-assisted features and deeper HCP Terraform integration.</cite> As of early-to-mid 2026, <cite index="1-1">OpenTofu has roughly 12% adoption among IaC practitioners, with over a quarter of teams planning to evaluate or expand its use, while Terraform still commands the largest overall market share.</cite> This is no longer a hypothetical decision — it is one that many platform teams are actively working through.
This handbook is a practical guide for engineering teams evaluating or executing a migration off Terraform's BUSL license onto OpenTofu: what the license actually restricts, what's technically different between the two tools today, and how to execute a safe, low-drama migration of real infrastructure.
Under the MPL 2.0, Terraform's source code could be used, modified, and redistributed — including as the core of a competing commercial product — with the only real obligation being that modifications to MPL-licensed files themselves stay under MPL. <cite index="7-1">MPL 2.0 is what most engineers mean when they say "open source": use it for anything, sell products built on it, modify it as long as modifications to MPL files go back under MPL. There is no clause that asks what business you're in.</cite>
BUSL 1.1 is structured differently. <cite index="7-1">The license question only surfaces when somebody asks who else is allowed to use the software commercially, and specifically, who is allowed to compete with the licensor.</cite> Under Terraform's BUSL terms:
It's worth being precise here, because a lot of anxiety about the BUSL change is disproportionate to its actual legal effect for most teams:
Even for organizations in the "clearly fine" bucket, the BUSL change introduced a category of cost that's easy to underrate: governance friction. <cite index="6-1">Organizations with an open-source program office or strict vendor-neutrality requirements often can't treat this as pure legal paperwork — the license change becomes a real governance risk that has to be evaluated, documented, and periodically re-reviewed</cite>, even if the conclusion each time is "we're fine." That recurring review cost, multiplied across every internal tool and audit cycle that touches "what license is our IaC tooling under," is a real and ongoing tax that a permanently MPL-licensed tool doesn't impose.
<cite index="7-1">The OSI does not approve BUSL 1.1 as an open-source license</cite>, which matters concretely for organizations with procurement policies that require OSI-approved licenses for foundational tooling, or that report open-source usage as part of compliance, security, or ESG processes.
<cite index="3-1">OpenTofu is a community-driven, MPL-licensed fork of Terraform created in 2023 after HashiCorp relicensed Terraform under the Business Source License.</cite> <cite index="3-1">A coalition of vendors and community members forked Terraform 1.5, the last MPL version, with endorsing companies at fork time including Spacelift, env0, Gruntwork, Harness, Scalr, and Cloud Posse, among others.</cite> The project was placed under the Linux Foundation rather than any single company, specifically to prevent the exact scenario that motivated the fork in the first place: a single vendor unilaterally changing the license on code the ecosystem depends on.
<cite index="6-1">OpenTofu is governed by the Linux Foundation with a Technical Steering Committee drawn from multiple organizations, maintaining command-line compatibility with Terraform while operating under MPL 2.0.</cite> This is the same governance pattern used by Kubernetes, and it matters structurally: <cite index="2-1">it's a foundation-backed, vendor-neutral control plane with a defined contribution model and long-term stability commitment</cite>, rather than a single company's roadmap. Decisions about core features go through a public RFC process rather than being decided unilaterally, and — critically for the trust question that motivated the fork — <cite index="6-1">no single vendor can unilaterally change the license again.</cite>
A natural question for legal teams evaluating OpenTofu: is there any residual licensing risk inherited from forking a HashiCorp-originated codebase? <cite index="7-1">OpenTofu was forked from a pre-BUSL, MPL-licensed release, so the BUSL does not apply to OpenTofu's codebase, and the Linux Foundation and OpenTofu's legal counsel reviewed the licensing position before the fork was published.</cite> There was friction along the way: <cite index="9-1">in 2024, HashiCorp sent OpenTofu's maintainers a cease-and-desist letter alleging that OpenTofu had incorporated code from Terraform releases published after the license change</cite>, a dispute that was resolved without altering OpenTofu's licensing position. Organizations with particularly strict provenance requirements may still want their own counsel to review the history, but the public record on this is well documented and has been independently examined by multiple parties with an interest in getting it right.
For the overwhelming majority of day-to-day usage, OpenTofu and Terraform are the same tool. They share:
required_providers block resolves against by default, which can be pinned explicitly either way</cite>.init, plan, apply, destroy, workspaces, modules, remote backends.A configuration file that works in Terraform will, in the large majority of cases, work unmodified in OpenTofu:
# main.tf — valid in both Terraform and OpenTofu without modification
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Environment = "production"
}
}
Running this through either binary produces the same plan:
$ terraform init && terraform plan
# ...identical resource graph...
$ tofu init && tofu plan
# ...identical resource graph...
Since the fork, OpenTofu has shipped several features that do not exist in Terraform's open-source CLI, largely because they were community priorities that HashiCorp had deprioritized or reserved for HCP Terraform:
Native state encryption (OpenTofu 1.7+). OpenTofu can encrypt state at rest without a third-party backend or wrapper:
# encryption.tf — OpenTofu only
terraform {
encryption {
key_provider "pbkdf2" "mykey" {
passphrase = var.state_encryption_passphrase
}
method "aes_gcm" "mymethod" {
keys = key_provider.pbkdf2.mykey
}
state {
method = method.aes_gcm.mymethod
}
plan {
method = method.aes_gcm.mymethod
}
}
}
<cite index="6-1">This matters because state files contain sensitive data — API keys, database passwords, infrastructure topology — and Terraform requires external solutions such as encrypted S3 buckets, Terraform Cloud, or third-party state backends to achieve the same protection.</cite>
Provider-defined functions and for_each on provider configurations (OpenTofu 1.9+). <cite index="7-1">OpenTofu has shipped provider for_each (v1.9) and the -exclude flag (v1.9), none of which exist in Terraform's open-source CLI.</cite>
# provider for_each — OpenTofu only, useful for multi-region/multi-account setups
provider "aws" {
for_each = var.regions
alias = "by_region"
region = each.value
}
resource "aws_s3_bucket" "regional" {
for_each = var.regions
provider = aws.by_region[each.key]
bucket = "my-app-${each.key}"
}
Early variable evaluation, including in backend configuration (OpenTofu 1.8+). This removes a long-standing Terraform limitation where backend configuration blocks couldn't reference variables:
# backend.tf — OpenTofu only; variables usable directly in backend config
variable "environment" {
type = string
}
terraform {
backend "s3" {
bucket = "my-tfstate-${var.environment}"
key = "infra/terraform.tfstate"
region = "us-east-1"
}
}
Targeted exclusion (-exclude flag, v1.9), the inverse of -target:
# Apply everything except the specified resource
tofu apply -exclude=aws_instance.legacy_bastion
Ephemeral resources and write-only attributes (OpenTofu 1.11, late 2025/2026). <cite index="7-1">OpenTofu 1.11 adds ephemerality, ephemeral resources, write-only attributes, and the enabled meta-argument, continuing to evolve independently from Terraform's roadmap.</cite> Ephemeral resources let you reference values (e.g., a short-lived credential) that are used during a plan/apply but never persisted to state:
# Ephemeral resource — value is used but never written to state
ephemeral "random_password" "db_admin" {
length = 20
}
resource "aws_db_instance" "main" {
# ...
password = ephemeral.random_password.db_admin.result
}
The enabled meta-argument offers a cleaner alternative to the common count = var.enabled ? 1 : 0 pattern:
# enabled meta-argument — OpenTofu only, replaces the count-as-boolean idiom
resource "aws_instance" "optional" {
enabled = var.create_instance
ami = data.aws_ami.this.id
instance_type = "t3.micro"
}
It would be dishonest to present this as a one-sided comparison. Terraform retains real advantages, concentrated mostly in the surrounding platform rather than the CLI itself:
<cite index="8-1">Major cloud providers, Kubernetes operators, and TACOS platforms (Spacelift, Scalr, env0, Atlantis) all support OpenTofu</cite>, and this is a meaningfully different picture than it was in 2023: <cite index="8-1">the ecosystem gap argument from the fork's early days has largely closed.</cite> <cite index="4-1">OpenTofu's own registry hosts 3,900+ providers and 23,600+ modules</cite>, covering the practical needs of the large majority of infrastructure teams.
This is usually the first and biggest practical concern, and the news is good. <cite index="7-1">State produced by Terraform 1.5.x generally works in OpenTofu without conversion, and many later Terraform states also work as long as the configuration doesn't depend on Terraform-only features.</cite> <cite index="5-1">OpenTofu is confirmed to work with existing state files created with Terraform versions up through the 1.5.x line</cite>, and in practice compatibility extends further for most common configurations.
The safe verification procedure:
# 1. Back up existing state before touching anything
terraform state pull > terraform-state-backup-$(date +%Y%m%d).json
# 2. Install OpenTofu alongside Terraform (don't remove Terraform yet)
brew install opentofu # macOS, or use your platform's package manager
# 3. Run a plan with OpenTofu against the existing state — do NOT apply yet
tofu init
tofu plan
# 4. Compare the plan output carefully — it should show no changes
# (an empty diff means OpenTofu read the state and config identically to Terraform)
<cite index="7-1">On the first tofu apply, the terraform_version marker in state is rewritten to OpenTofu's version, but the resource entries themselves are not modified</cite> — meaning the migration is non-destructive to the actual resource data in state, and reversible if you decide to roll back before running an apply that changes infrastructure.
A structured, low-risk sequence for migrating a single workspace or root module:
# Search for HCP/Terraform Cloud-specific configuration that needs special handling
grep -r "cloud {" --include="*.tf" .
grep -r "backend \"remote\"" --include="*.tf" .
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.42.0" # exact pin, not a range, during migration
}
}
required_version = "~> 1.6"
}
.terraform provider cache in most cases.tofu init and tofu plan against a non-production workspace first. Never validate a migration path for the first time against production state.tofu workspace select staging
tofu init -upgrade=false
tofu plan -out=migration-check.tfplan
terraform plan -out=terraform-check.tfplan
tofu plan -out=opentofu-check.tfplan
# Convert both to JSON and diff programmatically for a large number of resources
terraform show -json terraform-check.tfplan > tf.json
tofu show -json opentofu-check.tfplan > tofu.json
diff <(jq -S . tf.json) <(jq -S . tofu.json)
tofu instead of terraform. Most CI systems just need the binary swapped and any explicit terraform CLI calls renamed:# GitHub Actions — before
- name: Terraform Plan
run: terraform plan -out=tfplan
# GitHub Actions — after
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: "1.9.0"
- name: OpenTofu Plan
run: tofu plan -out=tfplan
cloud block is HCP-specific and has no OpenTofu equivalent — an S3, GCS, or Azure Blob backend works identically under either tool:# Portable backend configuration, works identically under Terraform and OpenTofu
terraform {
backend "s3" {
bucket = "company-tfstate"
key = "infra/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
By default, required_providers blocks without an explicit source resolve against each tool's own default registry. To make configuration fully portable between both tools during a transition period, pin the source explicitly:
terraform {
required_providers {
aws = {
source = "registry.opentofu.org/hashicorp/aws" # explicit OpenTofu registry
version = "5.42.0"
}
}
}
<cite index="5-1">OpenTofu will not have its own providers; Terraform providers have not altered their licenses, and OpenTofu works with the current Terraform providers, but uses a separate registry that mirrors it.</cite> In practice this means provider behavior is identical regardless of which registry resolved it — the distinction matters for registry availability and update timing, not functionality.
Public and private Terraform modules generally work unmodified in OpenTofu, since modules are just HCL and inherit whatever provider/version constraints their calling root module specifies. The main thing to verify is that any module doesn't hard-code a required_version constraint that excludes OpenTofu version strings:
# A module's version constraint written defensively for both tools
terraform {
required_version = ">= 1.5.0" # avoid tool-specific version assumptions
}
Rather than a universal recommendation, the right choice depends on a small number of concrete factors specific to your organization:
Stay on Terraform if:
Migrate to OpenTofu if:
for_each, early variable evaluation, ephemeral resources) more than you value staying on HashiCorp's specific roadmap.This is not an irreversible decision. <cite index="7-1">Same binaries, same release pipelines — the only difference is which registry your required_providers resolves against by default, and you can pin to either explicitly if it matters.</cite> Organizations can and do run both tools during a transition period, and the migration mechanics in Section 5 are largely reversible if a rollback becomes necessary within the state-compatibility window.
terraform apply and tofu apply against the same state file interchangeably without a clear cutover point. This works technically in the near term but makes it unclear which tool is authoritative, complicating troubleshooting if plan output ever diverges.cloud block, Sentinel policy sets) migrates automatically. It doesn't — these require deliberate re-platforming, not a find-and-replace.terraform while local plans use tofu produces confusing, hard-to-reproduce discrepancies.A migration that "looks fine" in a quick manual check can still hide subtle drift, especially across large state files with hundreds of resources. A more rigorous validation approach is worth the extra hour it takes:
Rather than eyeballing two plan outputs, script the comparison so it can run in CI on every migration-related change:
#!/usr/bin/env bash
# validate-migration.sh — compares terraform and tofu plan output for parity
set -euo pipefail
WORKSPACE="${1:-staging}"
echo "Running Terraform plan..."
terraform workspace select "$WORKSPACE"
terraform plan -out=tf.plan -input=false
terraform show -json tf.plan | jq -S '.resource_changes' > tf-changes.json
echo "Running OpenTofu plan..."
tofu workspace select "$WORKSPACE"
tofu plan -out=tofu.plan -input=false
tofu show -json tofu.plan | jq -S '.resource_changes' > tofu-changes.json
if diff -q tf-changes.json tofu-changes.json > /dev/null; then
echo "PASS: plans are identical for workspace $WORKSPACE"
exit 0
else
echo "FAIL: plan divergence detected for workspace $WORKSPACE"
diff tf-changes.json tofu-changes.json || true
exit 1
fi
Running this against every workspace before cutover turns "we think it's fine" into a verifiable, repeatable check, and gives you a concrete artifact to point to if anyone later asks how the migration was validated.
For teams with a mature testing culture, both tools support native testing frameworks that can be run under either binary to confirm behavioral parity:
# tests/instance.tftest.hcl — works under both `terraform test` and `tofu test`
run "creates_instance_with_correct_tags" {
command = plan
assert {
condition = aws_instance.web.tags["Environment"] == "production"
error_message = "Environment tag was not set correctly"
}
}
run "instance_type_matches_variable" {
command = plan
variables {
instance_size = "t3.small"
}
assert {
condition = aws_instance.web.instance_type == "t3.small"
error_message = "Instance type did not match input variable"
}
}
terraform test # run once under Terraform
tofu test # run again under OpenTofu — results should match
A test suite that passes identically under both binaries is strong evidence that a module's behavior — not just its plan output for one specific state — is portable, which matters more than a single plan diff if the module gets reused across many environments over time.
During a phased migration, track a small set of concrete signals per workspace rather than relying on anecdotal "it seems fine" reports:
tofu plan runs showing unexpected changes relative to the last known-good Terraform plan for the same commit.tofu apply against the historical baseline failure rate for terraform apply on the same infrastructure.The official setup-opentofu action mirrors the ergonomics of hashicorp/setup-terraform, making pipeline migration mostly mechanical:
name: Infrastructure CI
on:
pull_request:
paths: ["infra/**"]
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: "1.9.0"
- name: Init
run: tofu init -input=false
- name: Validate
run: tofu validate
- name: Plan
run: tofu plan -out=tfplan -input=false
- name: Post plan summary as PR comment
uses: actions/github-script@v7
with:
script: |
// plan output already captured in previous step's logs
console.log("Plan complete — see job logs for details");
stages:
- validate
- plan
- apply
variables:
TOFU_VERSION: "1.9.0"
.tofu_setup: &tofu_setup
before_script:
- curl -Lo /usr/local/bin/tofu <https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_amd64>
- chmod +x /usr/local/bin/tofu
- tofu init -input=false
validate:
stage: validate
<<: *tofu_setup
script:
- tofu validate
plan:
stage: plan
<<: *tofu_setup
script:
- tofu plan -out=tfplan
artifacts:
paths: [tfplan]
apply:
stage: apply
<<: *tofu_setup
script:
- tofu apply -input=false tfplan
when: manual
only:
- main
Atlantis, a popular self-hosted PR-automation tool for Terraform/OpenTofu workflows, supports OpenTofu natively via its executable configuration:
# atlantis.yaml
version: 3
projects:
- name: production-infra
dir: infra/prod
workflow: opentofu
workflows:
opentofu:
plan:
steps:
- run: tofu init -input=false
- run: tofu plan -out=$PLANFILE
apply:
steps:
- run: tofu apply -input=false $PLANFILE
Keeping formatting and validation consistent across a team migrating gradually is easier with shared pre-commit hooks that work against either binary:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: tofu-fmt
name: OpenTofu format check
entry: tofu fmt -check -recursive
language: system
files: \.tf$
- id: tofu-validate
name: OpenTofu validate
entry: bash -c 'tofu init -backend=false && tofu validate'
language: system
files: \.tf$
pass_filenames: false
For a mid-sized platform team (a handful of platform engineers, on the order of 50-150 Terraform-managed workspaces), a realistic migration breakdown looks like this:
For an organization without HCP Terraform or Sentinel dependencies, a full migration is often achievable within 4-6 weeks end to end. For one with both deeply embedded, 2-4 months is a more realistic planning assumption once the Sentinel re-platforming work is included.
The BUSL license change did not, for most organizations, create an urgent legal emergency — <cite index="10-1">most internal teams who use Terraform for infrastructure automation will not notice substantial changes in day-to-day usage.</cite> But it did create a durable governance question that doesn't resolve itself: whether your organization's foundational infrastructure tooling should depend on a single vendor's licensing discretion, now filtered further through IBM's ownership. OpenTofu answers that question structurally, through Linux Foundation governance and a permanent MPL 2.0 license, and has used the years since the fork to ship real technical improvements — state encryption, provider for_each, ephemeral resources — rather than simply tracking Terraform feature-for-feature.
The migration itself, for most teams, is mechanically straightforward: state files are compatible, HCL syntax is unchanged, and the provider ecosystem is shared. The genuine complexity concentrates in the parts of the stack that were never just about the CLI to begin with — HCP Terraform's managed execution environment and Sentinel's policy-as-code investment — and those pieces deserve their own deliberate migration plan rather than being treated as an afterthought to a simple binary swap. Approached that way, moving off BUSL is a safe, well-trodden path rather than a risky leap.
required_providers block resolves plugin binaries; Terraform and OpenTofu maintain separate but functionally equivalent registries.cloud blocks, Terraform Cloud remote backends)
