DevOps

Terraform 1.9 & OpenTofu 1.8: State Migration, Moved Blocks & Breaking Changes Guide

Terraform 1.9 introduces new variable validation features, improved moved blocks, and important deprecations. OpenTofu 1.8 adds provider-defined functions and loopable import blocks. We cover both with a complete migration guide.

M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

February 10, 2026
24 मिनट पढ़ने का समय

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
M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

अपने बुनियादी ढांचे को बदलने के लिए तैयार हैं?

आइए चर्चा करें कि हम आपकी कैसे मदद कर सकते हैं।