Skip to content

OpenTofu Reference: Migration, Providers, Resources, Backends, Workspaces & Testing

OpenTofu is a drop-in replacement for Terraform (BSL-licensed since Aug 2023), maintained by the Linux Foundation. Almost everything in your existing .tf files works without changes. New features added since the fork: native end-to-end state encryption, tofu test built-in, .tofu file extension support, and provider-defined functions. Migration is usually sed 's/terraform {/terraform {/' — the hard part is evaluating remote state backends and module registry compatibility.

1. Migration from Terraform & Installation

Drop-in replacement steps, state migration, and tfenv / tofuenv
# Install (macOS):
brew install opentofu

# Or via tofuenv (manages multiple versions like tfenv):
git clone https://github.com/tofuutils/tofuenv.git ~/.tofuenv
export PATH="$HOME/.tofuenv/bin:$PATH"
tofuenv install 1.9.0
tofuenv use 1.9.0

# Migrating an existing Terraform project:
# 1. Replace terraform binary with tofu (most CI systems just need PATH change)
# 2. .tf files are identical — no syntax changes needed
# 3. If using Terraform Cloud remote backend → switch to local or OpenTofu-compatible backend
# 4. State is compatible: tofu init reads existing terraform.tfstate

# The lock file (.terraform.lock.hcl) is compatible — reuse as-is:
tofu init   # downloads providers, creates .terraform/

# Key commands (identical to terraform):
tofu init       # initialize, download providers/modules
tofu validate   # syntax + schema check
tofu plan       # show changes (add -out=tfplan to save)
tofu apply      # apply changes (tofu apply tfplan uses saved plan)
tofu destroy    # destroy all resources
tofu state list # list state
tofu output     # show output values
tofu fmt        # format .tf files

# State migration from Terraform Cloud to S3:
terraform state pull > terraform.tfstate   # export current state
# Configure S3 backend in providers.tf, then:
tofu init -migrate-state

2. Provider Configuration & Variables

Provider blocks, variable types, locals, sensitive values, and validation
# providers.tf — pin provider versions:
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.25"
    }
  }
  # OpenTofu native state encryption (new feature not in Terraform):
  encryption {
    key_provider "pbkdf2" "mykey" {
      passphrase = var.state_encryption_passphrase
    }
    method "aes_gcm" "default" {
      keys = key_provider.pbkdf2.mykey
    }
    state { method = method.aes_gcm.default }
  }
}

provider "aws" {
  region  = var.aws_region
  profile = var.aws_profile   # or use env: AWS_PROFILE, AWS_ACCESS_KEY_ID
}

# variables.tf:
variable "environment" {
  type        = string
  description = "Deployment environment"
  default     = "staging"
  validation {
    condition     = contains(["staging", "production"], var.environment)
    error_message = "Must be staging or production."
  }
}

variable "db_password" {
  type      = string
  sensitive = true   # value redacted from plan output + state display
}

variable "vpc_config" {
  type = object({
    cidr_block         = string
    availability_zones = list(string)
    private_subnets    = list(string)
  })
}

# locals.tf — computed values:
locals {
  name_prefix = "${var.environment}-${var.project}"
  common_tags = {
    Environment = var.environment
    ManagedBy   = "opentofu"
    Project     = var.project
  }
}

3. Resources, Data Sources & Modules

Resource lifecycle, depends_on, data sources, for_each, and module calls
# main.tf:
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_config.cidr_block
  enable_dns_hostnames = true
  tags                 = local.common_tags
}

# Data source — read existing resources without managing them:
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]   # Canonical
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
  }
}

# for_each — create multiple resources from a map:
variable "subnets" {
  type = map(object({ cidr = string, az = string }))
  default = {
    "web"  = { cidr = "10.0.1.0/24", az = "us-east-1a" }
    "app"  = { cidr = "10.0.2.0/24", az = "us-east-1b" }
    "data" = { cidr = "10.0.3.0/24", az = "us-east-1c" }
  }
}
resource "aws_subnet" "subnets" {
  for_each          = var.subnets
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  tags              = merge(local.common_tags, { Name = "${local.name_prefix}-${each.key}" })
}
# Reference: aws_subnet.subnets["web"].id

# Module call:
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${local.name_prefix}-eks"
  cluster_version = "1.31"
  vpc_id          = aws_vpc.main.id
  subnet_ids      = [for s in aws_subnet.subnets : s.id]

  eks_managed_node_groups = {
    default = { instance_types = ["m6i.large"], min_size = 2, max_size = 10 }
  }
}

# lifecycle — control resource replacement:
resource "aws_instance" "web" {
  lifecycle {
    create_before_destroy = true    # create new before destroying old
    prevent_destroy       = true    # block tofu destroy (use for databases)
    ignore_changes        = [tags]  # ignore tag drift
  }
}

4. Backends, Workspaces & Remote State

S3 backend with DynamoDB locking, workspace per-environment, and remote state data source
# backend.tf — S3 backend with state locking:
terraform {
  backend "s3" {
    bucket         = "my-tofu-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "tofu-state-lock"   # for state locking
    # kms_key_id = "arn:aws:kms:..."     # optional KMS encryption
  }
}

# DynamoDB lock table (create once):
resource "aws_dynamodb_table" "state_lock" {
  name         = "tofu-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  attribute { name = "LockID", type = "S" }
}

# Workspaces — separate state per environment (simpler than separate backends):
tofu workspace list
tofu workspace new staging
tofu workspace select production
tofu workspace show        # current workspace name

# Reference in code:
resource "aws_s3_bucket" "data" {
  bucket = "${terraform.workspace}-my-data-bucket"
}

# Remote state — read outputs from another stack:
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-tofu-state"
    key    = "network/terraform.tfstate"
    region = "us-east-1"
  }
}
# Use: data.terraform_remote_state.network.outputs.vpc_id

5. tofu test (Built-in Testing) & CI/CD

Write .tftest.hcl files, mock providers, and integrate with GitHub Actions
# tests/vpc.tftest.hcl — built-in testing (OpenTofu + Terraform 1.6+):
variables {
  environment = "test"
  project     = "myapp"
  aws_region  = "us-east-1"
}

# Mock provider (no real AWS calls needed):
mock_provider "aws" {
  mock_resource "aws_vpc" {
    defaults = { id = "vpc-mock123", arn = "arn:aws:ec2:us-east-1:123:vpc/vpc-mock123" }
  }
}

run "creates_vpc_with_correct_cidr" {
  command = plan

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR block is wrong: ${aws_vpc.main.cidr_block}"
  }
  assert {
    condition     = aws_vpc.main.enable_dns_hostnames == true
    error_message = "DNS hostnames should be enabled"
  }
}

# Run tests:
# tofu test                    # runs all *.tftest.hcl files
# tofu test -filter=tests/vpc.tftest.hcl

# GitHub Actions CI:
# - uses: opentofu/setup-opentofu@v1
#   with:
#     tofu_version: 1.9.0
# - run: tofu init
# - run: tofu validate
# - run: tofu plan -out=tfplan
# - run: tofu apply -auto-approve tfplan   # on merge to main only

# .gitignore:
# .terraform/
# *.tfstate
# *.tfstate.backup
# .terraform.lock.hcl  # commit this! it pins provider versions
# tfplan               # don't commit saved plans (contain secrets)

Track OpenTofu and IaC tooling releases at ReleaseRun. Related: Terraform Reference | Kubernetes Reference | GitHub Actions Reference

🔍 Free tool: Terraform Security Scanner — OpenTofu is HCL-compatible — paste your .tf config and scan for the same 9 security checks as Terraform.

Founded

2023 in London, UK

Contact

hello@releaserun.com