All posts
July 4, 20263 min read

Why we run OpenTofu (and what its lock file taught our CI)

OpenTofuArchitectureTerraform

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 rm un-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 plan only. apply stays 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.

Building something like this?

We help teams take AI from idea to production. Tell us what you're working on.

Get in touch