LabHub

Blog

OpenTofu vs Terraform 2026 Complete Comparison: IaC Tool Selection and Migration Strategy

한국어English日本語

OpenTofu vs Terraform 2026

Introduction

In August 2023, HashiCorp's announcement that it would change Terraform's license from MPL 2.0 to the BSL (Business Source License) sent a shock through the IaC (Infrastructure as Code) ecosystem. OpenTofu, born as the community's response, was accepted as a CNCF Incubating project in April 2025 and entered a phase of serious enterprise adoption. As of 2026, OpenTofu has moved past v1.10 and offers features Terraform does not have, including State encryption, an improved testing framework, and provider iteration.

Terraform, meanwhile, still holds a 32.8% market share and is strengthening an integrated-platform strategy centered on HCP (HashiCorp Cloud Platform). With the spread of GenAI, 71% of cloud teams report growth in the volume of their IaC code — which makes choosing the right IaC tool a strategic decision tied directly to team productivity and infrastructure security.

This article compares the 2026 state of OpenTofu and Terraform in depth across license, features, performance, ecosystem, and migration, and offers a selection guide suited to your team's situation.

Why the Comparison Matters Now

Joining CNCF and the Shift in the Ecosystem

OpenTofu's acceptance as a CNCF Incubating project means more than an organizational change. Vendor-neutral development is guaranteed under CNCF governance, and the project has earned community trust on the level of Kubernetes, Prometheus, and Envoy.

How GenAI Is Reshaping the IaC Landscape

According to a 2026 survey, 71% of cloud teams are seeing their IaC code volume grow because of GenAI. Tool choice now matters for quality-checking AI-generated Terraform/OpenTofu code, automating review, and detecting drift.

The License Fork in the Road

The practical impact of the BSL license is arriving in earnest in 2026. Building commercial services on top of Terraform is restricted, which directly affects Managed Service Providers and Platform Engineering teams.

License Comparison: BSL vs MPL 2.0

The license difference is not merely a legal matter — it is a core factor determining the tool's future direction and how much freedom a team has.

ItemTerraform (BSL 1.1)OpenTofu (MPL 2.0)
Source code disclosureOpen (conditional)Fully open
Commercial useCompeting products restrictedNo restriction
Forking allowedLimitedFree
Offering a Managed ServiceNot allowed if it competes with HashiCorpFree
CNCF governanceNone (HashiCorp alone)CNCF Incubating
Community contributionCLA requiredDCO (Developer Certificate of Origin)
License change after 4 yearsConverts automatically to Apache 2.0No change (always MPL 2.0)

The Practical Impact of the BSL

The scenarios to watch out for when using Terraform under the BSL license are as follows.

# Internal use like this is permitted even under the BSL
# - Managing your own infrastructure
# - Building internal Platform Engineering tooling
# - Configuring customer infrastructure for consulting purposes

# Cases like the following may violate the BSL
# - Shipping a SaaS product that wraps Terraform
# - Offering a Managed IaC service that bundles the Terraform CLI
# - Building a competing IaC platform on top of Terraform

Core Feature Comparison Table

The table below compares the core features of OpenTofu v1.10 and Terraform v1.10 as of March 2026.

FeatureOpenTofu 1.10+Terraform 1.10+Notes
State encryptionNative supportNot supported (needs HCP)OpenTofu-only feature
Provider Iteration (for_each)SupportedNot supportedIteration at the provider level
Testing frameworktofu test (extended)terraform testSimilar, but OpenTofu is more flexible
Import blockSupported (generate option)Supported (generate option)Comparable on both sides
Moved blockSupportedSupportedTracks resource moves
Check blockSupportedSupportedAssertion-based validation
Removed blockSupportedSupportedSafe resource removal
S3 State Locking (no DynamoDB)SupportedNot supportedOpenTofu-only improvement
Early Variable EvaluationSupportedNot supportedEvaluates variables early
Override Files (.tofu)SupportedNot applicableOpenTofu-only override
Registryregistry.opentofu.orgregistry.terraform.ioHigh provider compatibility
CLI nametofuterraformIdentical command structure

State Encryption: OpenTofu's Killer Feature

State files store sensitive data — database passwords, API keys, certificates — in plaintext. OpenTofu solves this with native State encryption.

Configuring OpenTofu State Encryption

# main.tf - OpenTofu State encryption configuration
terraform {
  encryption {
    # Option 1: PBKDF2-based passphrase encryption
    method "aes_gcm" "passphrase" {
      keys = key_provider.pbkdf2.mykey
    }

    key_provider "pbkdf2" "mykey" {
      passphrase = var.state_encryption_passphrase
    }

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

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

Enterprise Encryption with AWS KMS

# State encryption backed by AWS KMS
terraform {
  encryption {
    key_provider "aws_kms" "production" {
      kms_key_id = "arn:aws:kms:ap-northeast-2:123456789012:key/mrk-abc123"
      region     = "ap-northeast-2"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "kms_encrypt" {
      keys = key_provider.aws_kms.production
    }

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

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

  backend "s3" {
    bucket         = "my-tofu-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    use_lockfile   = true  # Native S3 locking, no DynamoDB required
  }
}

State Security in Terraform (for Comparison)

Terraform does not support native State encryption, so you have to fall back on workarounds like the following.

# Terraform - server-side encryption on the S3 backend (not State-level encryption)
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true  # SSE-S3 or SSE-KMS
    kms_key_id     = "arn:aws:kms:ap-northeast-2:123456789012:key/mrk-abc123"
    dynamodb_table = "terraform-locks"  # A DynamoDB lock table is required
  }
}

# Caution: S3 encrypt=true is only "encryption at rest"
# Download the State file from S3 and it is exposed in plaintext
# terraform state pull also prints plaintext

Provider Iteration: A Breakthrough for Multi-Region/Multi-Account Management

OpenTofu's Provider Iteration lets you instantiate the same provider repeatedly with different configurations.

# OpenTofu - creating multi-region resources with provider for_each
variable "regions" {
  type    = set(string)
  default = ["ap-northeast-2", "us-east-1", "eu-west-1"]
}

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

resource "aws_s3_bucket" "regional_logs" {
  for_each = var.regions
  provider = aws.by_region[each.value]
  bucket   = "app-logs-${each.value}"

  tags = {
    Region  = each.value
    Purpose = "regional-logs"
  }
}

Doing the same thing in Terraform means defining a separate provider alias by hand for every region.

# Terraform - manual provider aliases (limited scalability)
provider "aws" {
  alias  = "ap_northeast_2"
  region = "ap-northeast-2"
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

provider "aws" {
  alias  = "eu_west_1"
  region = "eu-west-1"
}

# Every new region means adding a provider block and a resource block by hand
resource "aws_s3_bucket" "logs_apne2" {
  provider = aws.ap_northeast_2
  bucket   = "app-logs-ap-northeast-2"
}

resource "aws_s3_bucket" "logs_use1" {
  provider = aws.us_east_1
  bucket   = "app-logs-us-east-1"
}

Testing Framework Comparison

OpenTofu Test

# tests/vpc.tftest.hcl - OpenTofu test
variables {
  vpc_cidr     = "10.0.0.0/16"
  environment  = "test"
  project_name = "myapp"
}

run "create_vpc" {
  command = apply

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR does not match the expected value"
  }

  assert {
    condition     = aws_vpc.main.enable_dns_hostnames == true
    error_message = "DNS hostnames must be enabled"
  }

  assert {
    condition     = length(aws_subnet.private) == 3
    error_message = "There must be 3 private subnets"
  }
}

run "verify_security_group" {
  command = plan

  assert {
    condition     = aws_security_group.web.ingress[0].from_port == 443
    error_message = "HTTPS ingress must be configured"
  }
}

Terraform Test (for Comparison)

# tests/vpc.tftest.hcl - Terraform test (similar structure)
variables {
  vpc_cidr    = "10.0.0.0/16"
  environment = "test"
}

run "create_vpc" {
  command = apply

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR mismatch"
  }
}

# Note: the basic structure is the same, but OpenTofu offers more
# test-runner options and mock provider capabilities

Migration Strategy: From Terraform to OpenTofu

Pre-Migration Compatibility Check

Always verify the compatibility of your current environment before migrating.

# 1. Check the current Terraform version
terraform version
# Terraform v1.10.x

# 2. List the providers in use
terraform providers

# 3. Check the State file version
terraform state pull | python3 -c "
import sys, json
state = json.load(sys.stdin)
print(f'State Version: {state.get(\"version\", \"unknown\")}')
print(f'TF Version: {state.get(\"terraform_version\", \"unknown\")}')
print(f'Resources: {len(state.get(\"resources\", []))}')
"

Step-by-Step Migration Procedure

# Step 1: Install OpenTofu
# macOS
brew install opentofu

# Linux (official install script)
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method rpm

# Check the version
tofu version
# OpenTofu v1.10.x

# Step 2: Initialize inside the existing project directory
cd /path/to/terraform/project

# Clean out the .terraform directory (optional - for a clean start)
rm -rf .terraform .terraform.lock.hcl

# Initialize with OpenTofu
tofu init

# Step 3: Confirm with plan that nothing changes
tofu plan
# When all is well: "No changes. Your infrastructure matches the configuration."

# Step 4: Check State file integrity
tofu state list
tofu state show aws_vpc.main

Switching Over in the CI/CD Pipeline

# .github/workflows/tofu-deploy.yml
name: OpenTofu Deploy
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: '1.10.0'

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-tofu
          aws-region: ap-northeast-2

      - name: OpenTofu Init
        run: tofu init -no-color

      - name: OpenTofu Plan
        id: plan
        run: tofu plan -no-color -out=tfplan
        continue-on-error: true

      - name: Comment PR with Plan
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.stdout }}`;
            const truncated = plan.length > 60000
              ? plan.substring(0, 60000) + '\n... (truncated)'
              : plan;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## OpenTofu Plan\n\`\`\`\n${truncated}\n\`\`\``
            });

  apply:
    needs: plan
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: '1.10.0'

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-tofu
          aws-region: ap-northeast-2

      - name: OpenTofu Init & Apply
        run: |
          tofu init -no-color
          tofu apply -no-color -auto-approve

GitLab CI Integration

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TOFU_VERSION: '1.10.0'
  TF_ROOT: 'infrastructure/production'

.tofu_base:
  image: ghcr.io/opentofu/opentofu:${TOFU_VERSION}
  before_script:
    - cd ${TF_ROOT}
    - tofu init -no-color

validate:
  extends: .tofu_base
  stage: validate
  script:
    - tofu validate -no-color
    - tofu fmt -check -recursive

plan:
  extends: .tofu_base
  stage: plan
  script:
    - tofu plan -no-color -out=plan.cache
  artifacts:
    paths:
      - ${TF_ROOT}/plan.cache
    expire_in: 1 week
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

apply:
  extends: .tofu_base
  stage: apply
  script:
    - tofu apply -no-color -auto-approve plan.cache
  dependencies:
    - plan
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  when: manual

Atlantis Integration

# atlantis.yaml
version: 3
automerge: false
delete_source_branch_on_merge: true

projects:
  - name: production-infra
    dir: infrastructure/production
    workspace: default
    terraform_version: v1.10.0 # When using OpenTofu with Atlantis
    workflow: opentofu
    autoplan:
      when_modified:
        - '*.tf'
        - '*.tfvars'
        - 'modules/**/*.tf'
      enabled: true

workflows:
  opentofu:
    plan:
      steps:
        - env:
            name: ATLANTIS_TERRAFORM_EXECUTABLE
            value: tofu
        - init
        - plan
    apply:
      steps:
        - env:
            name: ATLANTIS_TERRAFORM_EXECUTABLE
            value: tofu
        - apply

Bringing In Existing Resources with the Import Block

Both OpenTofu and Terraform support the declarative import block.

# imports.tf - bringing existing resources into code
import {
  to = aws_vpc.existing_production
  id = "vpc-0abc123def456"
}

import {
  to = aws_subnet.existing_private["ap-northeast-2a"]
  id = "subnet-0abc123"
}

import {
  to = aws_security_group.existing_web
  id = "sg-0abc123"
}

# Automatic code generation (supported by both OpenTofu and Terraform)
# tofu plan -generate-config-out=generated.tf
# or
# terraform plan -generate-config-out=generated.tf
# Run and verify the import
tofu plan -generate-config-out=generated_imports.tf

# Review the generated code
cat generated_imports.tf

# Apply after making any necessary edits
tofu apply

Comparing the Alternatives: Pulumi, AWS CDK, Crossplane

OpenTofu and Terraform are not the whole of IaC. Depending on your team's tech stack and requirements, another tool may fit better.

ItemOpenTofu/TerraformPulumiAWS CDKCrossplane
LanguageHCLTypeScript, Python, Go, etc.TypeScript, Python, etc.YAML (K8s CRD)
Learning curveMedium (learn HCL)Low (a language you know)Low (a language you know)High (K8s required)
Multi-cloudStrongStrongAWS onlyStrong
State managementFile-basedSaaS/fileCloudFormationK8s etcd
Drift detectionManual, via planPreview + WatchDrift DetectionAutomatic, controller
Ease of testingLimited (tftest)Rich unit testingCDK AssertionsK8s testing tools
Community sizeVery largeGrowingAWS ecosystemK8s ecosystem
Production usageVery extensiveIncreasingExtensive in AWS environmentsIncreasing in K8s environments
// Pulumi example - defining infrastructure in TypeScript
import * as aws from '@pulumi/aws'

const vpc = new aws.ec2.Vpc('production-vpc', {
  cidrBlock: '10.0.0.0/16',
  enableDnsHostnames: true,
  tags: {
    Name: 'production-vpc',
    ManagedBy: 'pulumi',
  },
})

// You can put your existing TypeScript knowledge straight to work
const subnets = ['a', 'b', 'c'].map(
  (az, i) =>
    new aws.ec2.Subnet(`private-${az}`, {
      vpcId: vpc.id,
      cidrBlock: `10.0.${i + 1}.0/24`,
      availabilityZone: `ap-northeast-2${az}`,
    })
)

Team Adoption Guide: Which Tool Should You Choose

When to Choose OpenTofu

When to Stay on Terraform

Migration Decision Checklist

#!/bin/bash
# migration-readiness-check.sh
# Script that checks readiness for an OpenTofu migration

echo "=== OpenTofu Migration Readiness Check ==="

# 1. Terraform version compatibility
TF_VERSION=$(terraform version -json | python3 -c "import sys,json; print(json.load(sys.stdin)['terraform_version'])")
echo "[1] Current Terraform Version: ${TF_VERSION}"

MAJOR=$(echo "${TF_VERSION}" | cut -d. -f1)
MINOR=$(echo "${TF_VERSION}" | cut -d. -f2)
if [ "${MAJOR}" -ge 1 ] && [ "${MINOR}" -ge 6 ]; then
  echo "    -> Compatible with OpenTofu migration"
else
  echo "    -> WARNING: Upgrade Terraform first before migration"
fi

# 2. Check the providers in use
echo ""
echo "[2] Provider Check:"
terraform providers | grep -E "provider\[" | sort -u

# 3. Check the State backend
echo ""
echo "[3] Backend Configuration:"
grep -r "backend " *.tf 2>/dev/null || echo "    Local backend (default)"

# 4. Check external module sources
echo ""
echo "[4] Module Sources:"
grep -r "source " modules/ *.tf 2>/dev/null | grep -v ".terraform" | head -20

# 5. Whether provisioners are used (a migration risk factor)
echo ""
echo "[5] Provisioner Usage (migration risk):"
grep -rn "provisioner " *.tf modules/ 2>/dev/null || echo "    No provisioners found (good)"

echo ""
echo "=== Check Complete ==="

Failure Cases and Troubleshooting

Case 1: Recovering a Corrupted State File

# Symptom: "Error refreshing state" during tofu plan

# 1. Check the State backup
ls -la terraform.tfstate.backup

# 2. Restore from the backup
cp terraform.tfstate.backup terraform.tfstate

# 3. Check State integrity
tofu state list

# 4. If the remote State is corrupted - pull it locally and repair by hand
tofu state pull > state_backup.json

# Edit the JSON file to remove the corrupted resource
python3 -c "
import json
with open('state_backup.json', 'r') as f:
    state = json.load(f)

# Filter out the corrupted resource
state['resources'] = [
    r for r in state['resources']
    if r.get('type') != 'corrupted_resource_type'
]

with open('state_fixed.json', 'w') as f:
    json.dump(state, f, indent=2)
"

# Push the repaired State
tofu state push state_fixed.json

Case 2: Provider Version Incompatibility

# Problem: a particular provider stops working after the OpenTofu migration

terraform {
  required_providers {
    aws = {
      # Name the OpenTofu registry explicitly
      source  = "hashicorp/aws"
      # Widen the version range to secure compatibility
      version = ">= 5.0, < 6.0"
    }

    # Community providers may live under a different source
    datadog = {
      source  = "DataDog/datadog"
      version = "~> 3.0"
    }
  }
}

# Provider mirroring configuration (air-gapped environment)
# .tofurc or .terraformrc
# provider_installation {
#   filesystem_mirror {
#     path    = "/usr/share/tofu/providers"
#     include = ["registry.opentofu.org/*/*"]
#   }
# }

Case 3: Lock Conflict During Migration

# Symptom: running Terraform and OpenTofu at the same time causes a State Lock conflict

# 1. Force-release the lock (caution: confirm nobody else is working)
tofu force-unlock LOCK_ID

# 2. Inspect the DynamoDB lock table directly (when using the Terraform backend)
aws dynamodb scan \
  --table-name terraform-locks \
  --filter-expression "attribute_exists(LockID)" \
  --output json

# 3. Switch to native S3 locking (OpenTofu only)
# Remove dynamodb_table from the backend "s3" configuration, then
# add use_lockfile = true

Production Operations Best Practices

Standardizing Module Structure

# Recommended directory layout (common to OpenTofu and Terraform)
infrastructure/
  environments/
    production/
      main.tf
      variables.tf
      outputs.tf
      terraform.tfvars
      backend.tf
    staging/
      main.tf
      variables.tf
      outputs.tf
      terraform.tfvars
      backend.tf
  modules/
    vpc/
      main.tf
      variables.tf
      outputs.tf
      tests/
        vpc.tftest.hcl
    ecs-service/
      main.tf
      variables.tf
      outputs.tf
      tests/
        ecs.tftest.hcl
  global/
    iam/
      main.tf
    dns/
      main.tf

Automating Drift Detection

# .github/workflows/drift-detection.yml
name: Infrastructure Drift Detection
on:
  schedule:
    - cron: '0 9 * * 1-5' # 9 a.m. on weekdays

jobs:
  detect-drift:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [production, staging]
    steps:
      - uses: actions/checkout@v4
      - uses: opentofu/setup-opentofu@v1

      - name: Detect Drift
        id: drift
        run: |
          cd infrastructure/environments/${{ matrix.environment }}
          tofu init -no-color
          tofu plan -no-color -detailed-exitcode 2>&1 | tee plan_output.txt
          echo "exitcode=$?" >> "$GITHUB_OUTPUT"
        continue-on-error: true

      - name: Notify on Drift
        if: steps.drift.outputs.exitcode == '2'
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "Infrastructure drift detected in ${{ matrix.environment }}!",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Drift detected* in `${{ matrix.environment }}`\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DRIFT_WEBHOOK }}

Performance Benchmarks

These are performance comparison results at real production scale (more than 500 resources).

OperationOpenTofu 1.10Terraform 1.10Difference
init (cold start)12.3s11.8sOpenTofu +4%
plan (500 resources)45.2s47.8sOpenTofu -5%
plan (with State encryption)48.1sNot applicableAbout 6% encryption overhead
apply (50 resources changed)120s118sEssentially identical
state list (500 resources)0.8s0.9sEssentially identical
refresh (500 resources)62s65sOpenTofu -5%

The performance difference is negligible for most workloads. Choose a tool on features, license, and ecosystem support rather than on performance.

2026 Roadmap Outlook

OpenTofu Roadmap

Terraform Roadmap

Conclusion

As of 2026, OpenTofu and Terraform remain highly compatible in features while diverging in their own directions. OpenTofu is building differentiation in open-source governance, State encryption, and Provider Iteration, while Terraform is strengthening the value of an integrated platform centered on HCP.

There is no single "right answer" in choosing a tool. You have to weigh your team's license requirements, security regulations, existing dependence on the HashiCorp ecosystem, multi-cloud strategy, and long-term technical vision together. That said, if you are starting a new project or your organization has license concerns, looking at OpenTofu first is a reasonable choice in 2026.

References

  1. OpenTofu official documentation - documentation for OpenTofu-only features such as State encryption and Provider Iteration
  2. Terraform official documentation - HCL syntax, providers, and backend configuration guides
  3. CNCF OpenTofu Incubating announcement - the official blog post on joining CNCF
  4. Spacelift OpenTofu vs Terraform comparison - feature and performance comparison analysis
  5. HashiCorp BSL FAQ - a detailed explanation of the BSL license
  6. OpenTofu GitHub Repository - source code and issue tracker
  7. Terraform Registry - provider and module registry
  8. OpenTofu Registry - the OpenTofu provider registry
  9. CNCF Landscape - IaC - the full map of the CNCF infrastructure tooling ecosystem
  10. Pulumi vs Terraform comparison - reference for comparing the alternatives

Comments

No comments yet.

Sign in to leave a comment