Install
# Debian / Ubuntu via the official apt repo
curl -fsSL https://get.opentofu.org/opentofu.gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/opentofu.gpg
echo "deb [signed-by=/etc/apt/keyrings/opentofu.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main" \
| sudo tee /etc/apt/sources.list.d/opentofu.list
sudo apt update
sudo apt install tofu
# macOS
brew install opentofu
# Or single binary via the install script (works everywhere)
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh \
| sh -s -- --install-method standalone
tofu version
The binary is named tofu; everywhere you used to type terraform, type tofu instead.
Migrating from Terraform
For Terraform 1.5.x or earlier, the state file is fully compatible — just run tofu init in the existing project and continue. For Terraform 1.6+, there are minor state-format differences; tofu init --migrate-state handles the conversion.
Two things to update:
- The
required_versionblock. Terraform's version is checked againstterraform_version; OpenTofu reads it differently:terraform { required_version = ">= 1.6.0" # works for both # Or version-specific: # required_version = ">= 1.8.0" } - Provider declarations — mostly unchanged. Providers come from the same registries; OpenTofu also supports its own registry and the OCI-registry option:
terraform { required_providers { aws = { source = "hashicorp/aws" # registry.opentofu.org by default version = "~> 5.0" } } }
The OpenTofu-only features worth knowing
State encryption
State files contain raw resource attributes — passwords, private keys, IDs. Terraform stores them unencrypted; OpenTofu can encrypt them at rest using a configurable key provider:
terraform {
encryption {
key_provider "pbkdf2" "passphrase" {
passphrase = "<long-secret>"
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.passphrase
}
state {
method = method.aes_gcm.default
}
plan {
method = method.aes_gcm.default
}
}
}
Or use AWS KMS / GCP KMS / Azure Key Vault / HashiCorp Vault (see that tutorial) for the key. State and plan files are now ciphertext; lose the key, you lose the state, but a compromise of the backend storage no longer reveals secrets.
Dynamic provider iteration
In Terraform, for_each and count work for resources but not for provider blocks. OpenTofu lifts that restriction (since 1.7):
variable "regions" {
type = list(string)
default = ["us-east-1", "us-west-2", "eu-central-1"]
}
provider "aws" {
for_each = toset(var.regions)
alias = each.key
region = each.key
}
resource "aws_s3_bucket" "logs" {
for_each = toset(var.regions)
provider = aws[each.key]
bucket = "logs-${each.key}"
}
This makes truly-multi-region deployments declarative, where in Terraform you'd resort to module duplication.
The removed block
Removing a resource from config used to mean deleting the resource from the cloud (after a plan/apply). The removed block declares "remove from state, don't destroy the actual resource":
removed {
from = aws_instance.old_server
lifecycle {
destroy = false
}
}
The instance stays running; Terraform/OpenTofu stops managing it. Useful when transferring ownership to another module / state file, or before importing into a separate workspace.
OCI-registry providers and modules
Push providers and modules to any OCI registry (Docker Hub, GitHub Container Registry, GitLab, ECR, a self-hosted registry):
terraform {
required_providers {
custom = {
source = "oci.example.com/team/myprovider"
version = "1.2.3"
}
}
}
The registry is the same kind of OCI registry containers use; provider distribution becomes "docker push."
A working module structure
infra/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
└── modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
# versions.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
null = { source = "hashicorp/null", version = "~> 3.2" }
}
backend "s3" {
bucket = "tf-state-example"
key = "infra.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # native S3 locking, no DynamoDB needed
}
}
# main.tf
provider "aws" {
region = var.region
}
module "vpc" {
source = "./modules/vpc"
cidr = "10.0.0.0/16"
tags = local.tags
}
use_lockfile = true is the OpenTofu (and Terraform 1.10+) native S3 state-locking using S3 conditional writes — no DynamoDB table needed.
The standard workflow
tofu init # download providers, set up the backend
tofu fmt -recursive # canonical formatting
tofu validate # syntax + reference check
tofu plan -out=tfplan # generate a binary plan file
tofu apply tfplan # apply exactly what the plan promised
tofu state list # see what's in state
tofu state show aws_instance.web
tofu destroy # tear it all down
The plan -out + apply <file> pattern is the right discipline: review the plan, share it, then apply that exact plan. apply without a plan file re-plans and could pick up drift in between.
CI integration
# GitHub Actions
- uses: opentofu/setup-opentofu@v1
with: { tofu_version: 1.10.0 }
- run: tofu init
- run: tofu fmt -recursive -check
- run: tofu validate
- run: tofu plan -out=tfplan
- run: tofu apply -auto-approve tfplan
For Argo CD / GitOps shape, pair OpenTofu with Atlantis or Spacelift / env0 (the latter two are SaaS but free tiers exist).
Worth knowing
- The provider registry at
registry.opentofu.orgmirrors the HashiCorp registry for community providers but ships under MPL-2.0. The HashiCorp registry is also reachable. - Provider releases sometimes diverge: the HashiCorp registry may have features that haven't been published to OpenTofu's registry yet, and vice versa. For most providers (AWS, GCP, Azure, Kubernetes), they're synchronized.
- Existing Terraform modules from the Terraform Registry work unchanged in OpenTofu.
- For teams committed to Terraform Cloud, OpenTofu doesn't drop in directly; pair with self-hosted backends + Atlantis / Spacelift / env0 for the same shape of workflow.