OpenTofu Integration
OpenTofu Integration
Terraform’s license change is why OpenTofu exists. The boring default for a new desk is: pkgs.opentofu with withPlugins on nixpkgs 26.05, .tf files, same backend discipline.
Mental model
OpenTofu is a Terraform-compatible CLI. Providers from nixpkgs attach the same way: withPlugins builds a wrapper whose plugin directory is in /nix/store. tofu init does not need the network for those providers. State still lives where you put it (S3, local) — Nix pins the binary, not the AWS account.
nix develop → tofu on PATH
plugins = /nix/store/…-opentofu-plugins
tofu init → offline providers
tofu plan → state from the backend
Commands: tofu init|plan|apply|fmt. Do not install both Terraform and OpenTofu on PATH unwrapped. State files from Terraform 1.x often import; test in a clone of state first.
Worked examples
Case 1: devShell with pinned plugins
Save as flake.nix:
# flake.nix
{
description = "Desk OpenTofu";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
tofu = pkgs.opentofu.withPlugins (p: [
p.aws
p.random
p.local
]);
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ tofu ];
};
};
}nix develop --command tofu versionOutput (shape):
OpenTofu v1.x.x
on linux_amd64
The path is under /nix/store. which tofu must not be ~/.local/bin/tofu.
withPlugins is the provider binary. You still commit .terraform.lock.hcl (hashes of those plugins). tofu init -input=false in this shell should not hit the registry; if it does, PATH is not the wrapper. required_providers in .tf documents versions; the store pin is what actually runs. Do not also set TF_PLUGIN_CACHE_DIR to a home directory that then races the wrapper.
Case 2: Same main.tf as Terraform
Save as main.tf:
# main.tf
resource "random_pet" "desk" {
length = 2
}
output "name" {
value = random_pet.desk.id
}
nix develop --command tofu init
nix develop --command tofu plan
nix develop --command tofu apply -auto-approveNo AWS required. Local state appears as terraform.tfstate — add *.tfstate* to .gitignore.
Case 3: Backend still not in Nix
Save as backend.tf:
# backend.tf
terraform {
backend "s3" {
bucket = "desk-tf-state"
key = "desk/opentofu.tfstate"
region = "eu-central-1"
}
}
Credentials for the backend come from the environment or sops (AWS_PROFILE, AWS_ACCESS_KEY_ID), not from a string in the flake. Nix does not substitute your account id.
nix develop --command tofu init -input=falseCase 4: CI splits plan and apply
nix develop --command tofu fmt -check
nix develop --command tofu init -input=false -backend=false # fmt/validate, no state
nix develop --command tofu init -input=false # plan needs the backend
nix develop --command tofu validate
nix develop --command tofu plan -input=false -out=plan.bin-backend=false is the PR job that must not need AWS credentials. Apply is a separate, protected job on main (environment protection, two reviewers) with the backend. Commit plan.bin nowhere. Artifact the plan between jobs if you must, with a short TTL. TF_IN_AUTOMATION=1 turns prompts into errors — set it in CI.
Case 5: Do not mix CLIs
# in a desk-only shellHook, if humans still type terraform:
# alias terraform=tofu
# better: teach tofuA shell that has both unwrapped is how state gets written by the wrong binary. tofu state pull after a surprise terraform apply is a long afternoon.
nix develop --command bash -c 'which tofu; command -v terraform || echo no-terraform'which tofu is /nix/store/…. terraform should be missing, or a wrapper that is tofu. Two real binaries is the incident.
State migration: copy the backend key (desk/opentofu.tfstate.migratetest), tofu init, tofu plan against the copy, then cut. Not Friday in production.
The trap
The trap is migrating state on Friday in production without tofu plan against a copied state. The other trap is terraform init / tofu init downloading providers from the registry after you already pinned them in withPlugins — that means PATH is not the wrapper (which tofu is ~/.local or a distro package).
A third: both pkgs.terraform and pkgs.opentofu on the same PATH. A fourth: committing plan.bin or *.tfstate.
The boring rule
- New work: OpenTofu from 26.05.
withPlugins. Commit.terraform.lock.hcl. - One CLI on PATH. Store path, not
~/.local. No Terraform next to it. - State credentials are secrets (sops / env), not Nix strings.
- PR:
fmt+init -backend=false+validate. Apply on main with protection. - Test state migration on a copy of the backend key.
Try this
nix develop --command tofu versionand confirm a store path (which tofu).tofu planonrandom_petwithout AWS.tofu fmt -checkon a badly indented file, fix, re-run.- Write down where state lives for the desk (bucket + key) in the repo README — not the credentials.
nix develop --command tofu init -input=falsewithNIX_SSL_CERT_FILEset and network blocked (unshare -nin a lab) — it must still find providers.