Problem
The client operated a payments platform across two Azure subscriptions that had grown organically over four years. Networking, identity, and policy were configured by hand through the portal, and no two environments matched. Every new environment required a six-week request cycle, and their auditors had flagged the absence of demonstrable, repeatable controls.
Three specific constraints shaped the engagement:
- Regulatory obligations required evidence that controls were enforced, not merely documented.
- A second UK region was needed for resilience without doubling the operational burden.
- The in-house team of five engineers had to be able to own the result without external support.
Solution
We rebuilt the estate as a subscription-vending landing zone, expressed entirely in Terraform and governed by Azure Policy.
- Management group hierarchy modelled around regulatory scope, so controls are inherited rather than reapplied per subscription.
- Subscription vending through a Terraform module and a pull-request template — a new environment is a merged PR, not a ticket.
- Policy as code with deny and deployIfNotExists assignments covering encryption, private endpoints, diagnostic settings, and permitted regions.
- Identity consolidated onto Entra ID workload identity federation, removing every long-lived service principal secret from the pipelines.
Migration ran environment by environment, lowest to highest, so the team gained confidence with the new model before production was touched.
Architecture
| Layer | Component | Purpose |
|---|---|---|
| Governance | Management groups + Azure Policy | Inherited, enforced controls with audit evidence |
| Provisioning | Terraform + subscription vending module | Repeatable environment creation |
| Network | Hub-and-spoke, dual region, Azure Firewall | Segmented egress with regional failover |
| Identity | Entra ID workload identity federation | Secretless CI/CD authentication |
| Delivery | GitHub Actions with OIDC | Plan on PR, apply on merge |
| Observability | Azure Monitor + centralised Log Analytics | Single query surface across regions |
Each spoke is deployed from the same module with a region parameter, so the secondary region is a configuration value rather than a parallel codebase:
module "spoke" {
for_each = toset(["uksouth", "ukwest"])
source = "./modules/spoke"
region = each.value
hub_vnet_id = module.hub[each.value].vnet_id
policy_set_id = azurerm_policy_set_definition.regulated.id
log_analytics = module.observability.workspace_id
}
Outcomes
- Environment provisioning reduced from six weeks to under one day, fully self-service.
- 100% of controls now enforced by policy with automated evidence export for auditors.
- Zero long-lived cloud credentials remaining in CI/CD.
- Second region brought online in three weeks, with failover validated by game day exercises.
- The client's own engineers made the final two production changes unaided, which was the agreed definition of a successful handover.