OpenTofu Integration

Updated

September 12, 2026

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 version

Output (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-approve

No 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=false

Case 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 tofu

A 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

  1. nix develop --command tofu version and confirm a store path (which tofu).
  2. tofu plan on random_pet without AWS.
  3. tofu fmt -check on a badly indented file, fix, re-run.
  4. Write down where state lives for the desk (bucket + key) in the repo README — not the credentials.
  5. nix develop --command tofu init -input=false with NIX_SSL_CERT_FILE set and network blocked (unshare -n in a lab) — it must still find providers.