The Terraform / OpenTofu Fork: Where Things Stand in 2026
The HashiCorp BSL license change in August 2023 triggered a fork that has now matured into a genuine alternative. OpenTofu 1.8, backed by the Linux Foundation and many enterprise adopters, has diverged meaningfully from Terraform with features that aren't coming back upstream. Understanding which tool to use, how to migrate between them, and what's new in each is now essential knowledge for infrastructure teams.
This guide covers the significant new features in both Terraform 1.9 and OpenTofu 1.8, and provides a tested migration path if you're considering switching from one to the other.
What's New in Terraform 1.9
1. Input Variable Validation with References
Previously, variable validation conditions could only reference the variable itself (var). Terraform 1.9 allows validation conditions to reference other variables and local values:
# Before 1.9: Could only self-reference
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
# After 1.9: Can reference other variables
variable "instance_type" {
type = string
validation {
# Reference another variable in validation!
condition = !(var.environment == "prod" && startswith(var.instance_type, "t2."))
error_message = "Production environments cannot use t2 instance types."
}
}
variable "min_capacity" {
type = number
validation {
condition = var.min_capacity <= var.max_capacity # Cross-variable reference
error_message = "min_capacity must not exceed max_capacity."
}
}
variable "max_capacity" {
type = number
}
2. Improved Moved Blocks for Refactoring
# Scenario: You're moving resources from a flat structure to modules
# Without moved blocks: terraform plan would destroy and recreate
# OLD state path
# aws_instance.web_server
# NEW state path (after moving to module)
# module.web.aws_instance.server
# moved.tf — tells Terraform to update state path, not recreate resource
moved {
from = aws_instance.web_server
to = module.web.aws_instance.server
}
# For moving resources within a for_each (new in 1.9)
moved {
from = aws_security_group.app["web"]
to = module.app_sg["web"].aws_security_group.main
}
# Verify moved blocks work before applying
terraform plan # Should show 0 to add, 0 to destroy, N moved
# After apply, moved blocks can be left in place (Terraform ignores completed moves)
# Or removed after all team members have applied
3. Deprecation: -refresh=false and Remote State in provider blocks
# DEPRECATED in 1.9: Using credentials in provider block directly
provider "aws" {
region = "us-east-1"
access_key = "AKIA..." # DEPRECATED — use environment variables or OIDC
secret_key = "..." # DEPRECATED
}
# CORRECT: Use environment variables or OIDC
provider "aws" {
region = var.aws_region
# Credentials from: AWS_ACCESS_KEY_ID env var, IAM role, or OIDC
}
What's New in OpenTofu 1.8
1. Provider-Defined Functions — The Big Feature
OpenTofu 1.8 introduces provider-defined functions — providers can now export custom functions usable in your HCL. This is a major feature not present in Terraform. AWS, Azure, and GCP providers are already adding functions:
# OpenTofu 1.8 only — provider-defined functions
# The AWS provider exposes ARN parsing functions
output "account_id_from_arn" {
value = provider::aws::arn_parse("arn:aws:iam::123456789:role/MyRole").account_id
}
output "region_from_arn" {
value = provider::aws::arn_parse(aws_lambda_function.main.arn).region
}
# Custom provider function example (in a custom provider)
# functions.go
func ARNParseFunction() function.Function {
return function.New(&function.Definition{
Summary: "Parse an AWS ARN into its components",
Parameters: []function.Parameter{
{Name: "arn", Type: cty.String},
},
Return: function.PlainReturn{Type: cty.Object(map[string]cty.Type{
"partition": cty.String,
"service": cty.String,
"region": cty.String,
"account_id": cty.String,
"resource": cty.String,
})},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// ... implementation
},
})
}
2. Loopable Import Blocks
# OpenTofu 1.8: Import multiple resources with for_each
# Scenario: Import 50 existing S3 buckets into state
locals {
existing_buckets = {
logs = "my-company-logs"
assets = "my-company-assets"
backups = "my-company-backups"
# ... 47 more
}
}
# Single import block with for_each — imports all 50 buckets
import {
for_each = local.existing_buckets
id = each.value
to = aws_s3_bucket.existing[each.key]
}
resource "aws_s3_bucket" "existing" {
for_each = local.existing_buckets
bucket = each.value
}
# Terraform 1.9 still requires one import block per resource:
import { id = "my-company-logs"; to = aws_s3_bucket.logs }
import { id = "my-company-assets"; to = aws_s3_bucket.assets }
# ... 48 more
Migrating from Terraform to OpenTofu
# Step 1: Install OpenTofu alongside Terraform (don't remove Terraform yet) # macOS brew install opentofu # Linux curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | sh # Step 2: Verify OpenTofu can read your existing state cd your-terraform-project tofu init # Uses same .terraform.lock.hcl format # Step 3: Validate all resources parse correctly tofu validate # Step 4: Dry-run plan — should match terraform plan output tofu plan -out=tofu.plan # Step 5: If plans match, switch CI/CD to use tofu instead of terraform # Replace in your pipeline: # terraform init → tofu init # terraform plan → tofu plan # terraform apply → tofu apply # Step 6: State is fully compatible — no migration needed! # OpenTofu reads and writes Terraform state format # Step 7: Update .terraform.lock.hcl (provider hashes differ slightly) tofu providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64
State Management Best Practices (Both Tools)
# terraform/opentofu backend with state locking (AWS S3 + DynamoDB)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/main.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/..."
dynamodb_table = "terraform-state-lock"
# Workspace-aware key (for multi-environment)
# key = "env:/${terraform.workspace}/main.tfstate"
}
required_version = ">= 1.9.0" # or ">= 1.8.0" for OpenTofu
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
# Create the DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Testing Your Terraform/OpenTofu Code
# Terraform/OpenTofu built-in test framework (1.6+)
# tests/s3_bucket.tftest.hcl
variables {
bucket_name = "test-bucket-12345"
environment = "test"
}
run "creates_bucket_with_correct_tags" {
command = plan # or apply (creates real resources — use with care)
assert {
condition = aws_s3_bucket.main.bucket == var.bucket_name
error_message = "S3 bucket name does not match variable"
}
assert {
condition = aws_s3_bucket.main.tags["Environment"] == var.environment
error_message = "Environment tag not set correctly"
}
}
run "bucket_versioning_enabled" {
assert {
condition = aws_s3_bucket_versioning.main.versioning_configuration[0].status == "Enabled"
error_message = "Versioning must be enabled"
}
}
# Run tests
tofu test # Runs all .tftest.hcl files
tofu test -filter=tests/s3_bucket.tftest.hcl # Run specific test file
Which Should You Choose in 2026?
Both tools work for most use cases. The decision factors:
- Choose Terraform 1.9+ if: You use HCP Terraform (Terraform Cloud), your organization has existing Terraform Enterprise contracts, or you rely on providers that haven't tested OpenTofu compatibility
- Choose OpenTofu 1.8+ if: You want provider-defined functions, loopable imports, you're building open-source tooling, or your organization has concerns about BSL licensing for internal use or redistribution
- State is compatible: You can switch back and forth without migration — both tools read the same state format
Marcus Rodriguez
Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.