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:

  1. The required_version block. Terraform's version is checked against terraform_version; OpenTofu reads it differently:
    terraform {
      required_version = ">= 1.6.0"   # works for both
    
      # Or version-specific:
      # required_version = ">= 1.8.0"
    }
  2. 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.org mirrors 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.