Skip to content

OpenTofu

Open-source infrastructure as code tool and community-driven fork of Terraform.

CommandDescription
brew install opentofuInstall on macOS with Homebrew
snap install --classic opentofuInstall on Linux with Snap
choco install opentofuInstall on Windows with Chocolatey
curl -fsSL https://get.opentofu.org/install-opentofu.sh | shInstall via shell script
docker run -it ghcr.io/opentofu/opentofuRun via Docker
tofu --versionShow installed version
tofu -helpShow available commands
# 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
CommandDescription
tofu initInitialize working directory and download providers
tofu init -upgradeRe-initialize and upgrade provider/module versions
tofu init -backend-config=prod.hclInitialize with backend config file
tofu planPreview infrastructure changes
tofu plan -out=plan.tfplanSave plan to file for later apply
tofu plan -target=aws_instance.webPlan changes for specific resource only
tofu plan -var-file=prod.tfvarsPlan with specific variable file
tofu applyApply infrastructure changes (prompts for confirmation)
tofu apply -auto-approveApply changes without confirmation
tofu apply plan.tfplanApply a saved plan file
tofu destroyDestroy all managed infrastructure
tofu destroy -auto-approveDestroy without confirmation
tofu destroy -target=aws_instance.webDestroy specific resource only
CommandDescription
tofu validateValidate configuration syntax
tofu fmtFormat configuration files
tofu fmt -checkCheck if files are formatted (CI-friendly)
tofu fmt -recursiveFormat all files in subdirectories
tofu outputDisplay all output values
tofu output -jsonDisplay outputs in JSON format
tofu output db_passwordDisplay specific output value
tofu showShow current state in human-readable form
tofu show -jsonShow state in JSON format
tofu show plan.tfplanShow saved plan details
tofu graphGenerate resource dependency graph (DOT format)
tofu graph | dot -Tpng > graph.pngRender dependency graph as PNG
tofu refreshReconcile state with real infrastructure
tofu providersList providers used in 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"
}
# 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
}
CommandDescription
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
CommandDescription
tofu plan -var="name=value"Pass variable via CLI
tofu plan -var-file="prod.tfvars"Pass variables from file
TF_VAR_name=value tofu planPass via environment variable
Create terraform.tfvars fileAuto-loaded variable values
Create *.auto.tfvars fileAuto-loaded variable files
# 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
}
# 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
}
CommandDescription
tofu state listList all resources in state
tofu state show aws_instance.webShow details of specific resource
tofu state mv aws_instance.old aws_instance.newRename resource in state
tofu state rm aws_instance.webRemove resource from state (keeps real infra)
tofu state pullPull remote state to stdout
tofu state pushPush local state to remote backend
tofu import aws_instance.web i-1234567Import existing resource into state
tofu state replace-provider hashicorp/aws registry.opentofu.org/hashicorp/awsReplace provider in state
tofu force-unlock LOCK_IDRelease a stuck state lock
# 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 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
    }
  }
}
CommandDescription
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 getDownload and update modules
tofu get -updateForce update all modules
modules/
└── vpc/
    ├── main.tf          # Resources
    ├── variables.tf     # Input variables
    ├── outputs.tf       # Output values
    ├── versions.tf      # Provider requirements
    └── README.md        # Usage documentation
# 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
}
CommandDescription
Replace terraform with tofu in commandsCLI commands are drop-in compatible
tofu init -upgradeRe-initialize with OpenTofu providers
Keep existing .tf files unchangedHCL syntax is fully compatible
Keep existing .tfstate filesState format is compatible
Keep existing .terraform.lock.hclLock file is compatible
Update CI/CD to use opentofu/setup-opentofu actionSwitch GitHub Actions
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)
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 }}

Encrypt state files at rest — unavailable in Terraform. Supports PBKDF2, AWS KMS, GCP KMS, and Azure Key Vault key providers.

Unlike Terraform Cloud’s server-side encryption, OpenTofu encrypts state before it leaves your machine, ensuring no third party can read your infrastructure state.

OpenTofu maintains its own provider and module registry at registry.opentofu.org, ensuring long-term availability independent of HashiCorp’s licensing decisions.

  1. Use remote state with locking — configure an S3 + DynamoDB backend (or equivalent) to prevent concurrent modifications and enable team collaboration.

  2. Enable state encryption — use OpenTofu’s exclusive encryption feature with KMS or PBKDF2 to protect sensitive data in state files.

  3. Pin provider and module versions — use ~> constraints (e.g., ~> 5.0) to allow patch updates while preventing breaking changes.

  4. Use workspaces or directory separation for environments — keep dev, staging, and production state separate to prevent accidental cross-environment changes.

  5. Write modules for reusable patterns — extract common patterns (VPCs, databases, Kubernetes clusters) into modules with clear inputs and outputs.

  6. Validate variables with constraints — add validation blocks to variables to catch invalid values before they reach the cloud provider.

  7. Save plans before applying — use tofu plan -out=plan.tfplan then tofu apply plan.tfplan to ensure you apply exactly what you reviewed.

  8. Use default_tags on providers — set organization-wide tags (ManagedBy, Environment, Project) at the provider level to ensure consistent tagging.

  9. Import existing resources — use tofu import to bring unmanaged infrastructure under OpenTofu control rather than recreating it.

  10. Run tofu fmt -check in CI — enforce consistent formatting across all team members by failing CI builds on unformatted files.

  11. Use data sources for external references — reference existing infrastructure (AMIs, VPCs, DNS zones) with data blocks instead of hardcoding IDs.

  12. Review plans carefully — always read the plan output before applying. Pay attention to destroy and replace actions that could cause downtime.