← blog · August 14, 2026

Terraform or OpenTofu: the license question and the real technical differences

What the 2023 license change actually broke, why OpenTofu was born under the Linux Foundation, and where the two CLIs genuinely diverge today: state encryption, provider for_each, early variable evaluation and the exclude flag, plus the migration steps and what breaks along the way.

If you write Terraform, the world has been split in two since 2023. Your HCL files stay almost identical either way, but the binary that runs them now differs in license, in the registry it pulls providers from, and in the language features it supports. For most teams the decision does not come from "which one is technically better" but from "what will legal say when they read the BUSL text" and "which side ships the three features we are waiting on". Here is what actually separates them.

What the license change really did

On 10 August 2023 HashiCorp moved its product portfolio, Terraform included, from the Mozilla Public License 2.0 to the Business Source License 1.1. Terraform 1.5.x was the last MPL release; 1.6.0 was the first under BUSL.

BUSL is not an open source license. It is a source-available commercial license with two moving parts. The Change Date converts each released version to MPL 2.0 four years after publication. The Additional Use Grant allows production use, but forbids offering the software to third parties on a hosted or embedded basis in a way that competes with the licensor's commercial products.

In practice this did not restrict an ordinary company managing its own cloud footprint with Terraform. It bit somewhere else: companies selling IaC platforms, managed service providers, consultancies running Terraform inside customer accounts. It also created an uncertainty cost that is hard to measure. "Competing offering" is contract language, not a technical definition, and compliance teams tend to eliminate the risk rather than sign under a phrase like that. Work internal platform teams had been doing for years, wrapping Terraform in a self-service interface, suddenly became something that needed a legal review.

The second hit came from the registry side. The Terraform Registry terms of service were updated to say that providers and modules downloaded from it may only be used with, or in support of, Terraform. That forced the split into distribution, not just into code.

How the fork became an institution

The reaction gathered first as a manifesto, then became code under the name OpenTF, and in September 2023 landed under the Linux Foundation as OpenTofu. The codebase forked from Terraform 1.5.x and stayed under MPL 2.0. The first stable release, 1.6, shipped on 10 January 2024.

Sitting under a foundation has no direct technical meaning, but it outweighs technical line items in procurement: the possibility of a single company relicensing the project on a commercial whim is structurally closed. Ownership on the other side changed too; IBM closed its acquisition of HashiCorp on 27 February 2025.

Where the two tools stand now

Terraform 1.15 shipped on 29 April 2026, OpenTofu 1.12 on 14 May 2026, with OpenTofu 1.13 in beta. The version numbers look close, but the release calendars are independent and the feature lists have been diverging.

The CLI differences that matter

State encryption

The Terraform CLI writes state as plaintext JSON. Encryption at the backend layer, S3 server-side encryption for example, protects your bucket, not the file. The output of terraform state pull, a plan file that lands in a CI log, the local working directory on a developer laptop: all still plaintext. For any resource that generates a database password or a private key, that is a real exposure surface.

OpenTofu 1.7 (April 2024) pulled encryption into the language itself:

terraform {
  encryption {
    key_provider "pbkdf2" "passphrase" {
      passphrase = var.state_passphrase
    }

    method "aes_gcm" "default" {
      keys = key_provider.pbkdf2.passphrase
    }

    state {
      method   = method.aes_gcm.default
      enforced = true
    }

    plan {
      method   = method.aes_gcm.default
      enforced = true
    }
  }
}

The passphrase provider is fine for local development; in production you use the AWS KMS, GCP KMS or OpenBao key providers. enforced = true stops an accidental plaintext write, and the fallback block lets you decrypt with the old key while writing with the new one during rotation.

Early variable evaluation

Not being able to use variables in backend configuration, module sources and encryption blocks is exactly where wrapper scripts are born: template engines that generate a backend.tf per environment, CI steps that rewrite files with sed. OpenTofu 1.8 made those blocks readable from variables and locals, as long as they do not depend on resources, data sources or module outputs. Terraform 1.15 narrowed the gap by allowing variables in module source and version; backend and encryption configuration still differ.

Provider for_each

Hand-writing a provider block per region has been an old annoyance in multi-region and multi-account setups. OpenTofu 1.9 fixed it:

variable "regions" {
  type = map(object({
    cidr = string
  }))
}

provider "aws" {
  alias    = "by_region"
  for_each = var.regions
  region   = each.key
}

resource "aws_vpc" "main" {
  for_each   = var.regions
  provider   = aws.by_region[each.key]
  cidr_block = each.value.cidr
}

The constraints are deliberately tight: the provider must carry an alias, the for_each expression must resolve statically (no resource or data source output), and the resource's for_each must not be the same expression as the provider's. Terraform has no equivalent.

exclude

-target has existed for years; its inverse did not. OpenTofu 1.9 added -exclude, and 1.10 added -target-file and -exclude-file. On a stack with hundreds of resources, leaving one broken resource out and applying the rest is a completely different job from hand-writing a -target list. On the Terraform side this is still an open request.

The rest

OpenTofu 1.10 brought provider mirrors in OCI registries (valuable for air-gapped environments), native S3 state locking that needs no DynamoDB table, and OpenTelemetry tracing. 1.11 added the enabled meta-argument, a readable form of the count = var.enabled ? 1 : 0 idiom. 1.12 allowed prevent_destroy to read from a variable, so the same module can carry different protection in production and in test.

Traffic runs the other way too. Ephemeral resources and write-only arguments landed in Terraform first and reached OpenTofu in 1.11 (December 2025). List resources (.tfquery.hcl files and terraform query) and action blocks, both introduced in Terraform 1.14, have no OpenTofu counterpart today. Stacks, Sentinel policies and everything on the HCP side were never part of the fork in the first place.

How split is the ecosystem, really

Far less than expected. Providers and SDKs stayed under MPL 2.0; the relicensing covered the products themselves only. OpenTofu runs its own registry but feeds from the same upstream repositories, and short addresses like hashicorp/aws resolve directly. Module coupling was always weaker anyway, since module sources can be git, https or object storage.

The risk sits in a narrow spot: niche vendor providers published to one registry only. Searching every address in your terraform providers output against the other registry is a five-minute job before you migrate, and it removes the surprise entirely.

Migration, and what breaks

On a typical stack the move takes an afternoon:

terraform state pull > state-backup.json
cp .terraform.lock.hcl lock-backup.hcl

tofu init -upgrade
tofu providers lock -platform=linux_amd64 -platform=darwin_arm64
tofu plan -detailed-exitcode

The result you want is zero changes in the plan. If a diff appears, a provider version drifted; the problem is in your version constraints, not in the migration.

Concrete things that will bite:

  1. The lock file. .terraform.lock.hcl records provider addresses including the registry hostname, and checksums for the same provider version can differ between the two registries. Without -upgrade, the first init fails on a checksum mismatch. If CI runs on more than one architecture, pass every platform you use to providers lock, or the failure will find you in the first pipeline run.
  2. State metadata is a one-way door. The first apply writes its own version information into state; after that, going back to the old tool produces a "state snapshot was created by a newer version" error. Your rollback plan has to be restoring the backup file, not running the old binary.
  3. Do not turn encryption on day one. The other tool cannot read encrypted state, so closing that door before you have validated the migration also closes your rollback. Migrate first, encrypt after a few successful applies.
  4. Stacks coupled through terraform_remote_state. In a staged migration, a stack you have not moved keeps reading the state of one you have; the moment encryption is enabled, that read breaks. Map the dependency direction and move the leaves first.
  5. The tool chain. The binary name changes, so wrapper scripts, pre-commit hooks, orchestration tools and CI templates all need a pass. .tf keeps working; .tofu is optional and invisible to the other tool, which is occasionally exactly what you want.
  6. Version constraints. A required_version expression inside a module is evaluated against the version number of whichever tool you are running. A community module demanding Terraform 1.14 is rejected under OpenTofu 1.12, and it is worth checking whether that constraint was ever real.

Which one, and when

If you run the CLI yourself, OpenTofu should be the default. Three reasons: license uncertainty disappears completely, feature velocity is at least equal and clearly ahead in places like state encryption, and the migration costs an afternoon. The alternative, staying on the official one "just in case", buys no measurable technical benefit.

The cases for staying on Terraform are equally clear. If you are tied to HCP Terraform or Terraform Enterprise, to Sentinel policies or to Stacks, the fork does not solve your problem, it only swaps the CLI layer. If a single-vendor enterprise support contract is a procurement requirement, the OpenTofu answer comes from commercial platform vendors rather than from the project itself. And if you need something like list resources or action blocks today, staying beats waiting.

The one thing I would not do is try to support both. A wrapper that "calls whichever one is installed" produces a permanent conflict in the lock file and in state metadata; teams trying to keep both tools' checksums in the same repository usually work this out in week two.

Thanks to the four-year Change Date, every Terraform release published under BUSL eventually converts to MPL 2.0. But when that date arrives you will be holding four-year-old code, which is not an answer to the question you are asking today.