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