Why we run OpenTofu (and what its lock file taught our CI)
When we rebuilt rawrtech.ai on Azure App Service with Cloudflare DNS, we
wrote about eating our own cooking: the whole stack lives in a versioned
infrastructure-as-code repo, the same discipline we sell to clients. One
ingredient we glossed over at the time: the binary that applies all of it is
tofu, not terraform. Here's why — and the one place where "drop-in
compatible" turned out to have a sharp edge.
The boring case for OpenTofu
This isn't an ideological choice. When HashiCorp moved Terraform to the
Business Source License, OpenTofu forked from the last MPL release and moved
under Linux Foundation stewardship. Same HCL, same providers, same
plan-and-apply workflow; for day-to-day work the only difference is typing
tofu. For a consultancy that ships infrastructure code to clients, the
open license removes a conversation nobody wants to have — no lawyer needs
to interpret "competitive offering" before a client can adopt our config.
The tool is the same shovel. We just picked the handle with fewer strings attached.
The lock file has a passport
Here's the sharp edge. When tofu init resolves providers, it writes
.terraform.lock.hcl, pinning versions and checksums — from
registry.opentofu.org. You commit that file, as you should.
Now put hashicorp/setup-terraform in your GitHub Actions workflow instead
of opentofu/setup-opentofu. The HashiCorp binary reads that lock file,
fails provider verification, and tells you — nothing useful. No "wrong
binary" hint. Just a lock file that won't verify, at the bottom of a red CI
log, for a config that works fine locally.
The rule we took away: CI must run the exact binary you run locally. "Compatible" describes the language, not the supply chain.
State lives where the config can't reach it
Our remote state is an Azure Storage account in its own dedicated resource group, so no workload teardown can ever touch it. Blob versioning and soft delete give us state history for free. Authentication is Entra ID only — there are zero storage account keys anywhere: not in CI, not in a password manager, nowhere.
And it was bootstrapped once, by hand, with the az CLI — deliberately
not managed by the configuration it serves. A config that can destroy its
own state file is a circular firing squad.
House rules for state
- Timestamped backup before any state operation. Every time. It costs ten seconds and has no failure mode.
tofu state rmun-manages a resource without touching the cloud — the safe exit when IaC should forget something it never should have owned.- After any change, the proof of stability is a clean "No changes" plan. Not a green pipeline — the plan.
- CI runs
planonly.applystays a human decision, made on purpose, by someone who read the plan.
What we deliberately keep out
Our MX and SPF records for Microsoft 365 are not in the config. That's not an oversight. Mail delivery should never depend on an apply going well, so email DNS lives where no apply can touch it. IaC scope is a design decision, not a default — the blast radius of a bad apply is exactly as large as you choose to make it.
The boring verdict
OpenTofu has been a non-event in the best sense: same shovel, same workflow, one less license clause. The lock file lesson bought us a rule we now apply everywhere — the tools in CI must be the tools on your laptop, byte for byte.
The lock file remembers where you've been.