New field note:Rebuilding TopQbank: why we chose greenfield TypeScript over modernizingRead it →
← All insightsArchitecture

An Azure landing zone that costs almost nothing until you need it

How we built a hub-and-spoke Azure landing zone for IDS.Consulting in Terraform, with cost switches that kept over $1,000 a month switched off during the build.

CloudGuard Insights · · 7 min read

IDS.Consulting runs a Windows web application with a SQL back end for its customers. It had grown up in a hand-built Azure estate: virtual machines with public IPs, settings changed in the portal, and no clear line between production and everything else.

We were asked to build a proper landing zone beside it, ready to receive the application, without disrupting what was already running.

The shape of it

We built a classic hub-and-spoke design, entirely in Terraform:

  • Management groups and Azure Policy set guardrails across every subscription, with central Log Analytics for logs.
  • A connectivity hub in its own subscription holds Azure Firewall, Azure Bastion and the private DNS zones.
  • Production and non-production spokes each get their own subscription, virtual network, Key Vault, Defender plans and a SQL server with an elastic pool.
  • An edge layer runs Front Door Premium with one WAF policy per environment: Detection mode in non-production, Prevention mode in production.
  • An apps layer puts the Windows IIS servers in the spokes with no public IP at all. Traffic arrives only through Front Door, over a Private Link Service in front of an internal load balancer.

Each layer has its own Terraform state, so a change to a spoke can never touch the hub.

For readers who think in AWS

If you know AWS better than Azure, the mapping is close. Subscriptions are accounts, management groups are organizational units, Azure Policy plays the role of service control policies, and the hub is a shared networking account with Transit Gateway and a central firewall. Front Door is CloudFront with a WAF built in, Bastion is Session Manager, and a managed identity is an instance profile.

The problem with building a landing zone

While a landing zone is being built, nobody uses it. But the expensive parts start billing the moment they exist. Azure Firewall, Bastion and two SQL elastic pools would have cost well over $1,000 a month before a single customer request arrived.

The usual fix is to turn things off in the portal and remember to turn them back on. That breaks the one rule that makes Infrastructure as Code worth having: the next terraform apply quietly undoes whatever someone changed by hand.

Cost switches, in code

Instead, every expensive component has a switch in the stack’s variables:

Switch Saves while off Turn it on when
firewall_enabled about $300 a month Before the first virtual machine
bastion_enabled about $140 a month Only while someone needs to sign in; off again after
sql_enabled (per spoke) about $370 to $550 a month each Non-production: when migration testing starts. Production: just before the migration
auto_shutdown about half the VM cost Always on in non-production, never in production

Because the switches live in code and in pull requests, everyone can see what is on, and an apply never surprises anyone.

Making it impossible to forget

A switch you have to remember to flip is just another way to forget. So the apps layer refuses to plan while the firewall or SQL is off for its environment, with a message that names the switch to flip and the stacks to re-apply.

That matters because the dependencies are real. Without the firewall, the spoke has no route to the internet, so the virtual machines would lose Windows Update, backups and their monitoring agents. The guard turns a silent production problem into a clear error at plan time.

Deploying without keys

The pipeline runs in GitHub Actions and signs in to Azure with OIDC federation: no client secrets are stored anywhere. There are two identities, one that can only plan and one that can apply, and every pull request shows its plan in the job summary. Formatting, validation, tflint and Checkov run on every change.

Merging a pull request applies nothing. A person runs the apply workflow by hand, and it applies the layers in order: management, connectivity, edge, then each environment’s spoke and apps.

What carries over to your platform

  • Separate state per layer, so the blast radius of a mistake stays small.
  • No public IPs on application servers; put the edge in front and use private connectivity behind it.
  • Put cost decisions in code, next to the resources they control.
  • Turn forgotten dependencies into plan-time errors.
  • Federate CI with your cloud instead of storing secrets.

If you have a hand-built estate and want a clean landing zone beside it, this pattern lets you build calmly and pay only for what you’re actually using.

Working on something similar? Let's talk.Book a free consultation