← All case studies

From Portal-Clicked to OpenTofu: A Regulated Fintech's Azure Estate as Code

CTO at Assetera · 2026 - Present

Replacing a hand-built Azure estate with OpenTofu modules, four environments, keyless deploys, and image promotion from testnet to production. Built so that DORA's questions about ICT risk, change management, and exit plans have answers in the repository.

Azure
OpenTofu
DORA
Infrastructure as Code
DevOps

Problem

Assetera's Azure estate had grown through the portal. Resources were spread across several subscriptions, with billing that ran through different channels. Some of it served the live product, some of it served projects that had ended long ago, and most of it ran around the clock with almost no traffic. There was no description of the estate that could rebuild it.

That is a cost problem, but for a firm under DORA it is first a risk problem. You must be able to show how your ICT systems are built, who can change them, how a change is reviewed, how you recover, and how you would leave a provider. None of that is possible when the source of truth is the portal.

My background was AWS and CloudFormation. Azure was new to me, so the estate also had to be one I could reason about from the code alone.

Constraints

The live service had to keep running while the new estate was built next to it.

No long-lived secrets in CI. The rule was keyless from the start.

Small team, so few moving parts. Every module had to be understandable by one person reading it.

Portable by design. DORA asks for exit strategies. OpenTofu instead of Bicep, plain containers instead of a proprietary runtime model, and no feature that only exists in one provider without a reason recorded in an ADR.

Architecture

OpenTofu, one repository. About 14 modules: Container Apps environment, Front Door with WAF, Key Vault, Service Bus, Postgres, Redis, container registry, DNS, network, identity, observability, platform apps, and the GitHub organisation and repositories themselves. Four live stacks: nonprod, testnet, prod, and a shared platform stack.

Keyless everywhere. GitHub Actions authenticate to Azure with OIDC. Applications reach databases, queues, and vaults with managed identities and Entra authentication. Secrets that must exist, such as third-party API keys, live in one Key Vault per environment, and a preflight check fails the plan if a referenced secret is missing.

Build once, promote by digest. Every merge to main builds one amd64 image, tagged with the git commit, and deploys it to the test environment. Production receives the same image, imported by digest through a separate, governed workflow. Rollback means promoting the previous commit's image again, and that procedure is written down.

One way in. Public traffic enters through Front Door with a WAF policy. Admin surfaces have their own IP allowlists. Each Container App also restricts its own ingress to Front Door, so the direct app URLs do not answer.

GitHub as code. Repository settings, branch protection, environments, and code owners are part of the same OpenTofu repository, so access to production is reviewed like any other change.

Observability. Log Analytics and Grafana with Entra login, and structured logs from every service.

My role

I designed the module structure and the environment topology and recorded both as ADRs. I wrote most of the modules and all of the live stacks, applied every change myself during the build-up, and mapped the legacy estate resource by resource so that each shutdown was a deliberate, reversible step.

Outcome

Tradeoffs

Local apply instead of apply from CI. Plans run in CI, but applies happen from a reviewed plan on an operator's machine. That keeps the most privileged credentials out of the pipeline. It costs some convenience and depends on discipline, which the review process enforces.

Platform defaults are not safe defaults. Container Apps adds no health probes by default, and an edge WAF is not a seal on its own. Each of those lessons became a module default, so the next service gets it right without anyone remembering.

Shared capacity per environment. One Postgres server and one Redis per environment keep cost close to traffic. The modules allow a dedicated instance for any service that needs it later.