OpenTofu
Open-source infrastructure as code tool and community-driven fork of Terraform.
Installation
Section titled “Installation”Platform Installation
Section titled “Platform Installation”| Command | Description |
|---|---|
brew install opentofu | Install on macOS with Homebrew |
snap install --classic opentofu | Install on Linux with Snap |
choco install opentofu | Install on Windows with Chocolatey |
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh | Install via shell script |
docker run -it ghcr.io/opentofu/opentofu | Run via Docker |
tofu --version | Show installed version |
tofu -help | Show available commands |
Version Manager Installation
Section titled “Version Manager Installation”# Using tofuenv (like tfenv for OpenTofu)
git clone https://github.com/tofuutils/tofuenv.git ~/.tofuenv
echo 'export PATH="$HOME/.tofuenv/bin:$PATH"' >> ~/.bashrc
# List available versions
tofuenv list-remote
# Install specific version
tofuenv install 1.8.0
tofuenv use 1.8.0
# Pin version in project
echo "1.8.0" > .opentofu-version
Core Commands
Section titled “Core Commands”Workflow Commands
Section titled “Workflow Commands”| Command | Description |
|---|---|
tofu init | Initialize working directory and download providers |
tofu init -upgrade | Re-initialize and upgrade provider/module versions |
tofu init -backend-config=prod.hcl | Initialize with backend config file |
tofu plan | Preview infrastructure changes |
tofu plan -out=plan.tfplan | Save plan to file for later apply |
tofu plan -target=aws_instance.web | Plan changes for specific resource only |
tofu plan -var-file=prod.tfvars | Plan with specific variable file |
tofu apply | Apply infrastructure changes (prompts for confirmation) |
tofu apply -auto-approve | Apply changes without confirmation |
tofu apply plan.tfplan | Apply a saved plan file |
tofu destroy | Destroy all managed infrastructure |
tofu destroy -auto-approve | Destroy without confirmation |
tofu destroy -target=aws_instance.web | Destroy specific resource only |
Inspection Commands
Section titled “Inspection Commands”| Command | Description |
|---|---|
tofu validate | Validate configuration syntax |
tofu fmt | Format configuration files |
tofu fmt -check | Check if files are formatted (CI-friendly) |
tofu fmt -recursive | Format all files in subdirectories |
tofu output | Display all output values |
tofu output -json | Display outputs in JSON format |
tofu output db_password | Display specific output value |
tofu show | Show current state in human-readable form |
tofu show -json | Show state in JSON format |
tofu show plan.tfplan | Show saved plan details |
tofu graph | Generate resource dependency graph (DOT format) |
tofu graph | dot -Tpng > graph.png | Render dependency graph as PNG |
tofu refresh | Reconcile state with real infrastructure |
tofu providers | List providers used in configuration |
Configuration (HCL)
Section titled “Configuration (HCL)”Provider Configuration
Section titled “Provider Configuration”# Required providers block
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
# State encryption (OpenTofu exclusive feature)
encryption {
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.mykey
}
state {
method = method.aes_gcm.default
enforced = true
}
plan {
method = method.aes_gcm.default
enforced = true
}
key_provider "pbkdf2" "mykey" {
passphrase = var.state_passphrase
}
}
}
# Provider with default configuration
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "opentofu"
Environment = var.environment
Project = var.project_name
}
}
}
# Provider alias for multi-region
provider "aws" {
alias = "west"
region = "us-west-2"
}
# Use aliased provider on a resource
resource "aws_instance" "west_server" {
provider = aws.west
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
Resource Definitions
Section titled “Resource Definitions”# Basic resource
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = "${var.project_name}-web"
}
}
# Data source (read existing resources)
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# Local values
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "opentofu"
}
az_count = length(data.aws_availability_zones.available.names)
}
# Count and for_each
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = merge(local.common_tags, {
Name = "${var.project_name}-public-${count.index}"
Type = "public"
})
}
resource "aws_security_group_rule" "ingress" {
for_each = {
http = { port = 80, cidrs = ["0.0.0.0/0"] }
https = { port = 443, cidrs = ["0.0.0.0/0"] }
ssh = { port = 22, cidrs = [var.admin_cidr] }
}
type = "ingress"
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = each.value.cidrs
security_group_id = aws_security_group.main.id
}
Variables & Outputs
Section titled “Variables & Outputs”Variable Definitions
Section titled “Variable Definitions”| Command | Description |
|---|---|
variable "name" { type = string } | Define a string variable |
variable "port" { type = number default = 8080 } | Variable with default value |
variable "tags" { type = map(string) } | Map variable |
variable "cidrs" { type = list(string) } | List variable |
variable "config" { type = object({ ... }) } | Object variable with structure |
variable "db_pass" { sensitive = true } | Mark variable as sensitive (hidden in output) |
variable "env" { validation { ... } } | Variable with custom validation |
Passing Variables
Section titled “Passing Variables”| Command | Description |
|---|---|
tofu plan -var="name=value" | Pass variable via CLI |
tofu plan -var-file="prod.tfvars" | Pass variables from file |
TF_VAR_name=value tofu plan | Pass via environment variable |
Create terraform.tfvars file | Auto-loaded variable values |
Create *.auto.tfvars file | Auto-loaded variable files |
Variable & Output Examples
Section titled “Variable & Output Examples”# variables.tf
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_config" {
type = object({
instance_type = string
volume_size = number
encrypted = bool
})
default = {
instance_type = "t3.micro"
volume_size = 20
encrypted = true
}
}
variable "database_password" {
type = string
sensitive = true
}
# outputs.tf
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "web_url" {
description = "URL of the web server"
value = "https://${aws_lb.web.dns_name}"
}
output "db_connection_string" {
description = "Database connection string"
value = "postgresql://${var.db_username}:${var.database_password}@${aws_db_instance.main.endpoint}/${var.db_name}"
sensitive = true
}
Variable Files
Section titled “Variable Files”# prod.tfvars
environment = "prod"
instance_config = {
instance_type = "t3.large"
volume_size = 100
encrypted = true
}
# dev.tfvars
environment = "dev"
instance_config = {
instance_type = "t3.micro"
volume_size = 20
encrypted = false
}
State Management
Section titled “State Management”State Commands
Section titled “State Commands”| Command | Description |
|---|---|
tofu state list | List all resources in state |
tofu state show aws_instance.web | Show details of specific resource |
tofu state mv aws_instance.old aws_instance.new | Rename resource in state |
tofu state rm aws_instance.web | Remove resource from state (keeps real infra) |
tofu state pull | Pull remote state to stdout |
tofu state push | Push local state to remote backend |
tofu import aws_instance.web i-1234567 | Import existing resource into state |
tofu state replace-provider hashicorp/aws registry.opentofu.org/hashicorp/aws | Replace provider in state |
tofu force-unlock LOCK_ID | Release a stuck state lock |
Backend Configuration
Section titled “Backend Configuration”# S3 backend (most common)
terraform {
backend "s3" {
bucket = "my-tofu-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tofu-locks"
encrypt = true
}
}
# PostgreSQL backend
terraform {
backend "pg" {
conn_str = "postgres://user:pass@db.example.com/tofu_state?sslmode=require"
}
}
# HTTP backend (works with GitLab, Terraform Cloud alternatives)
terraform {
backend "http" {
address = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod"
lock_address = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod/lock"
unlock_address = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod/lock"
username = "gitlab-ci-token"
password = var.ci_token
}
}
# Local backend (default, for development)
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
State Encryption (OpenTofu Exclusive)
Section titled “State Encryption (OpenTofu Exclusive)”# State encryption with AWS KMS
terraform {
encryption {
key_provider "aws_kms" "main" {
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abcd-1234"
region = "us-east-1"
key_spec = "AES_256"
}
method "aes_gcm" "default" {
keys = key_provider.aws_kms.main
}
state {
method = method.aes_gcm.default
enforced = true
}
}
}
Modules
Section titled “Modules”Module Usage
Section titled “Module Usage”| Command | Description |
|---|---|
module "vpc" { source = "./modules/vpc" } | Use a local module |
module "vpc" { source = "github.com/org/repo//modules/vpc" } | Use a GitHub module |
module "vpc" { source = "registry.opentofu.org/..." } | Use OpenTofu registry module |
tofu get | Download and update modules |
tofu get -update | Force update all modules |
Module Structure
Section titled “Module Structure”modules/
└── vpc/
├── main.tf # Resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider requirements
└── README.md # Usage documentation
Module Example
Section titled “Module Example”# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(var.tags, { Name = var.name })
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${count.index}"
})
}
# modules/vpc/variables.tf
variable "name" { type = string }
variable "cidr" { type = string }
variable "azs" { type = list(string) }
variable "public_subnets" { type = list(string) }
variable "tags" { type = map(string) default = {} }
# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "public_subnet_ids" { value = aws_subnet.public[*].id }
# Using the module
module "vpc" {
source = "./modules/vpc"
name = "production"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
tags = local.common_tags
}
Migration from Terraform
Section titled “Migration from Terraform”Drop-In Compatibility
Section titled “Drop-In Compatibility”| Command | Description |
|---|---|
Replace terraform with tofu in commands | CLI commands are drop-in compatible |
tofu init -upgrade | Re-initialize with OpenTofu providers |
Keep existing .tf files unchanged | HCL syntax is fully compatible |
Keep existing .tfstate files | State format is compatible |
Keep existing .terraform.lock.hcl | Lock file is compatible |
Update CI/CD to use opentofu/setup-opentofu action | Switch GitHub Actions |
Migration Checklist
Section titled “Migration Checklist”Migration from Terraform to OpenTofu:
1. Install OpenTofu alongside Terraform
2. In your project directory, run: tofu init
3. Run: tofu plan
- Compare output with: terraform plan
- Should be identical (no changes)
4. Replace 'terraform' with 'tofu' in CI/CD pipelines
5. Update GitHub Actions:
- Old: hashicorp/setup-terraform
- New: opentofu/setup-opentofu
6. Update lock file provider sources (if needed):
- tofu providers lock
7. Enable state encryption (OpenTofu exclusive):
- Add encryption block to terraform {} block
8. Switch module registry references:
- registry.terraform.io → registry.opentofu.org
- (Both still work, but OpenTofu registry is preferred)
GitHub Actions CI/CD
Section titled “GitHub Actions CI/CD”name: OpenTofu Plan & Apply
on:
push:
branches: [main]
pull_request:
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.8.0
- name: Init
run: tofu init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Plan
run: tofu plan -out=plan.tfplan -no-color
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Apply (main only)
if: github.ref == 'refs/heads/main'
run: tofu apply -auto-approve plan.tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
OpenTofu Exclusive Features
Section titled “OpenTofu Exclusive Features”State Encryption
Section titled “State Encryption”Encrypt state files at rest — unavailable in Terraform. Supports PBKDF2, AWS KMS, GCP KMS, and Azure Key Vault key providers.
Client-Side State Encryption
Section titled “Client-Side State Encryption”Unlike Terraform Cloud’s server-side encryption, OpenTofu encrypts state before it leaves your machine, ensuring no third party can read your infrastructure state.
Registry Independence
Section titled “Registry Independence”OpenTofu maintains its own provider and module registry at registry.opentofu.org, ensuring long-term availability independent of HashiCorp’s licensing decisions.
Best Practices
Section titled “Best Practices”-
Use remote state with locking — configure an S3 + DynamoDB backend (or equivalent) to prevent concurrent modifications and enable team collaboration.
-
Enable state encryption — use OpenTofu’s exclusive encryption feature with KMS or PBKDF2 to protect sensitive data in state files.
-
Pin provider and module versions — use
~>constraints (e.g.,~> 5.0) to allow patch updates while preventing breaking changes. -
Use workspaces or directory separation for environments — keep dev, staging, and production state separate to prevent accidental cross-environment changes.
-
Write modules for reusable patterns — extract common patterns (VPCs, databases, Kubernetes clusters) into modules with clear inputs and outputs.
-
Validate variables with constraints — add
validationblocks to variables to catch invalid values before they reach the cloud provider. -
Save plans before applying — use
tofu plan -out=plan.tfplanthentofu apply plan.tfplanto ensure you apply exactly what you reviewed. -
Use
default_tagson providers — set organization-wide tags (ManagedBy, Environment, Project) at the provider level to ensure consistent tagging. -
Import existing resources — use
tofu importto bring unmanaged infrastructure under OpenTofu control rather than recreating it. -
Run
tofu fmt -checkin CI — enforce consistent formatting across all team members by failing CI builds on unformatted files. -
Use data sources for external references — reference existing infrastructure (AMIs, VPCs, DNS zones) with
datablocks instead of hardcoding IDs. -
Review plans carefully — always read the plan output before applying. Pay attention to
destroyandreplaceactions that could cause downtime.