Cloud architecture13 min read

Azure Landing Zones in production: how to design them without turning cloud into bureaucracy

A mature Landing Zone does not control every deployment. It establishes clear guardrails and lets teams work autonomously within them.

David González
David GonzálezCEO · Cloud and DevOps
Detail of server infrastructure with blue indicators as a metaphor for a governed Azure Landing Zone

An Azure Landing Zone should not be a collection of policies, a Management Group hierarchy and a Hub-Spoke network drawn in Visio.

It should answer a much harder question:

how do we allow teams to deploy infrastructure autonomously without losing control of security, networking, identity, costs and compliance?

That is the real problem.

Because two extremes are fairly common.

In one, almost everything is allowed.

Each team creates resources, VNets, Key Vaults, Public IPs, Service Principals, Private Endpoints and Storage Accounts however it sees fit.

It works for a while.

Until someone needs to understand who has access to what, why seven different networks exist, where the logs end up, which resources are exposed to the Internet or who owns a subscription that has been generating costs for six months.

The other extreme does not work much better.

The platform team tries to prevent this by placing controls on absolutely everything.

Creating a subscription requires approval.

Creating a VNet requires approval.

Opening a route requires approval.

Granting access requires approval.

Creating a Private Endpoint requires approval.

Azure ends up becoming a kind of traditional data centre with an API in front of it.

That is not a well-designed Landing Zone either.

It is automated bureaucracy.

The difficulty lies in building something between those two extremes.

A platform that establishes clear, strong boundaries wherever genuine risk exists, while allowing freedom within those boundaries.

The Landing Zone does not start with the network

One of the most common mistakes when designing Azure is to start by drawing VNets.

Hub.

Spokes.

Firewalls.

Peerings.

Subnets.

Private Endpoints.

But before deciding how traffic will flow, there is a more important decision:

what will be our actual unit of isolation and governance?

In Azure, that unit should often be the subscription.

Not simply the Resource Group.

A subscription provides a much more useful administrative boundary for:

  • policy assignment;
  • budgets;
  • RBAC;
  • quotas;
  • workload separation;
  • lifecycle;
  • ownership;
  • operational isolation.

Microsoft uses precisely this principle in Azure Landing Zones: subscriptions act as units of management and scale.

That significantly changes how a tenant should be organised.

Ending up with something like this would not make much sense:

Subscription: Production

├── rg-app-a
├── rg-app-b
├── rg-data
├── rg-network
├── rg-monitoring
├── rg-security
├── rg-dev
├── rg-test
└── rg-random-things

That model looks simple at first.

But problems begin to emerge over time.

Policies affect workloads that should not share them.

Permissions become too broad.

Costs become harder to attribute.

Service limits begin to be shared.

And moving specific pieces later becomes considerably more difficult.

For a platform expected to grow, it is generally healthier to think in terms similar to these:

Tenant

├── Platform
│   ├── Identity
│   ├── Connectivity
│   └── Management

├── Landing Zones
│   ├── Corp
│   │   ├── workload-a-prod
│   │   └── workload-b-prod
│   │
│   └── Online
│       ├── ecommerce-prod
│       └── api-prod

└── Sandbox

Not because this hierarchy must be copied literally.

But because it separates two important concepts:

the platform that enables cloud use and the workloads that consume that platform.

Management Groups: do not mirror the organisation chart

Another common temptation is to replicate the company structure inside Management Groups.

Spain
├── Finance
├── Marketing
├── Engineering
└── Operations

It may seem logical.

But organisation charts change.

Architectures should change far less often.

A Management Group is particularly useful when it represents shared technical or governance requirements.

For example:

Landing Zones
├── Online
├── Corp
├── Regulated
└── Sandbox

The right question is not:

which department uses this subscription?

It is:

which set of controls should this subscription inherit?

Two applications owned by entirely different departments may require exactly the same policies.

And two applications owned by the same department may require radically different connectivity models.

The hierarchy should represent those differences.

Azure Policy should prevent dangerous states, not control every decision

This is where one of the most delicate parts begins.

Azure Policy is extremely powerful.

That is precisely why it is easy to misuse.

We can create policies to:

  • prevent Public IPs;
  • require specific SKUs;
  • restrict regions;
  • require tags;
  • deploy diagnostics;
  • disable public access;
  • enforce TLS;
  • restrict resource types;
  • check configuration;
  • automatically deploy certain components.

But being technically able to do something does not mean we should do it.

A policy should exist because there is a risk worth controlling.

Not because “we can standardise it”.

A useful classification is to separate policies into four groups.

1. Security guardrails

States that we simply do not want to allow.

For example:

Storage Account with public access
Key Vault accessible from the Internet
SQL Server with public network access enabled
Legacy TLS
Unauthorised Public IP

In these cases, Deny can make a great deal of sense.

2. Governance guardrails

Allowed regions.

Forbidden SKUs.

Restricted resource types.

Mandatory ownership.

These controls depend much more heavily on context.

3. Automatic configuration

For example:

DeployIfNotExists:
    Diagnostic Settings
        -> Log Analytics

This can reduce manual work considerably.

But it requires care.

A policy that automatically modifies resources effectively becomes another deployment system.

That has implications for debugging, permissions and lifecycle.

4. Audit

There are situations where it makes sense to observe before blocking.

Audit
AuditIfNotExists

During a migration, this can be much more intelligent than immediately deploying hundreds of Deny policies.

Designing governance for a new environment is one thing.

Trying to introduce it on top of five years of existing resources is another.

The mistake of starting everything with Deny

Suppose we decide:

No Storage Account may have public access.

Perfect.

We create:

effect = Deny

A few hours later, a deployment fails.

The application team does not know why.

Terraform returns an error.

The pipeline turns red.

Someone opens a ticket for the platform team.

The platform team discovers that a policy blocked the resource.

An exception is created.

A month later, nobody remembers why it exists.

A mature Landing Zone also needs an exception strategy.

Having policies is not enough.

We need to define:

Policy

Violation

Can workload remediate?

    ├── Yes → Fix workload

    └── No

     Exception

     Owner
     Reason
     Expiration
     Compensating control

A permanent exception with no owner or expiry date simply becomes a policy that does not apply.

RBAC: Owner should not be the universal solution

Another recurring problem appears with permissions.

When something does not work:

Assign Owner

Problem solved.

We have also just removed a significant part of the security model.

In Azure, it is important to distinguish clearly between:

Control plane
Data plane

Being able to configure a Storage Account is not the same as being able to read its blobs.

Administering a Key Vault is not the same as being able to retrieve its secrets.

And deploying a database is not the same as being able to access the data it contains.

A healthy model tries to avoid broad, persistent permissions.

For example:

Platform Team
    |
    +-- Network Contributor      -> Connectivity
    +-- Policy Contributor       -> Governance
    +-- Security Reader          -> Tenant
    +-- Reader                   -> Workloads

Application Team
    |
    +-- Contributor              -> Own subscription
    +-- no RBAC delegation
    +-- no Policy changes
    +-- no shared network changes

And for privileged operations:

Microsoft Entra PIM

Eligible Role

Activation

MFA / justification

Time limited privilege

The fundamental idea is to reduce the number of permanent privileges.

Especially:

Owner
User Access Administrator
Global Administrator

They should not become everyday permissions.

The traditional pattern is familiar:

                 Internet
                    |
               Azure Firewall
                    |
                  Hub
                 /   \
                /     \
            Spoke A   Spoke B

The hub usually concentrates:

  • Firewall;
  • VPN/ExpressRoute;
  • DNS;
  • routing;
  • shared services.

While the spokes contain workloads.

Microsoft continues to document Hub-Spoke as a fundamental topology for Landing Zones, normally keeping central connectivity in a dedicated subscription.

The problem begins when almost every PaaS service starts using Private Endpoints.

SQL.

Storage.

Key Vault.

Container Registry.

Cosmos DB.

App Configuration.

Service Bus.

That is when a component many architectures underestimate appears:

DNS.

A Private Endpoint without DNS design is an incident waiting to happen

Suppose we have:

mystorage.blob.core.windows.net

It normally resolves to a public IP address.

We add a Private Endpoint.

Now we want it to resolve internally to:

10.20.3.17

That means managing:

privatelink.blob.core.windows.net

and its Private DNS Zone.

Creating the zone is not the problem.

The problem is determining:

who should resolve it?

We have several options.

We can link Private DNS Zones directly to each spoke.

Or centralise resolution.

For example:

On-prem
   |
Corporate DNS
   |
Azure DNS Private Resolver
   |
Private DNS Zones
   |
Private Endpoints

Azure DNS Private Resolver can resolve private namespaces between Azure and external networks without requiring dedicated VMs as DNS forwarders. Microsoft documents both centralised and distributed architectures for this scenario.

This is where a problem we have seen far too many times appears:

Network connectivity: OK
NSG: OK
Route: OK
Private Endpoint: Approved
Application: Cannot connect

The diagnosis eventually comes down to:

nslookup database.database.windows.net

The application is resolving the wrong IP address.

It was not the firewall.

It was not the route.

It was not Kubernetes.

It was DNS.

That is why DNS design should be part of the Landing Zone from the beginning.

It should not be added when Private Endpoints begin to appear.

Private Endpoints in the hub or in each spoke?

There is no universal answer here.

Microsoft supports both models depending on the scenario. A Private Endpoint consumed by a single application can reasonably live in its spoke, while services shared by multiple workloads may justify centralisation.

Distributed model

Spoke App A
    └── Private Endpoint SQL A

Spoke App B
    └── Private Endpoint SQL B

Advantages:

  • clear ownership;
  • lifecycle aligned with the workload;
  • less dependence on the central team.

Disadvantages:

  • proliferation;
  • greater DNS complexity;
  • more network objects.

Centralised model

Hub
├── PE SQL
├── PE Storage
└── PE Key Vault

Advantages:

  • central control;
  • inspection;
  • more uniform topology.

Disadvantages:

  • strong dependence on the platform team;
  • larger blast radius;
  • lifecycle decoupled from the workload;
  • a potential organisational bottleneck.

These are precisely the kinds of decisions a Landing Zone should document.

Not merely deploy.

The real objective should be self-service

A mature cloud platform should not require someone to create a subscription manually every time a project appears.

There should be a repeatable process.

Something like:

name: payments-production

environment: production

landing_zone: online

region:
  primary: westeurope

network:
  enabled: true
  address_space: 10.40.0.0/16

budget:
  monthly: 10000

owners:
  - payments-platform-team

features:
  private_dns: true
  central_logging: true
  defender: true

That request should trigger automation.

Request

Validation

Subscription creation

Management Group assignment

Policy inheritance

RBAC

Budget

Network

Logging

Handover

Microsoft calls this pattern subscription vending and presents it as a way to standardise how teams request and receive governed subscriptions.

The central team’s objective should not be to create resources.

It should be to build the platform that allows other teams to create them correctly.

There is an enormous difference.

Should Terraform manage absolutely everything?

A Landing Zone normally needs Infrastructure as Code.

Terraform and OpenTofu are obvious candidates.

But there is another important decision:

where does platform code end and workload code begin?

I would avoid a giant repository like:

azure-everything/

with the entire tenant managed by a single state.

The blast radius is too large.

Separating states and responsibilities is more reasonable.

For example:

platform/
├── management-groups
├── policy
├── identity
├── connectivity
└── management

workloads/
├── payments
├── analytics
└── ecommerce

And within the platform:

state-management-groups
state-policy
state-connectivity-eu
state-connectivity-us

The important question is:

what would have to stop if this state became corrupted or the pipeline failed?

If the answer is:

our entire Azure tenant,

the boundary is probably too large.

Bootstrap: the problem that is often hidden

All this creates a paradox.

Terraform needs permission to create the platform.

But the platform is precisely what should control Terraform’s permissions.

Someone initially has to create:

State Storage Account
Managed Identity / App Registration
Federated credentials
RBAC
Key Vault

This initial process should be clearly defined.

Above all, it should be reproducible.

It should not depend on:

David created it once in the portal and nobody knows exactly how.

Ideally:

bootstrap/

platform identity

remote state

platform deployment

Once bootstrap is complete, human access should be reduced.

Pipelines should authenticate using federated identities or other mechanisms without persistent secrets wherever possible.

Centralised logging does not mean sending absolutely everything

Another frequent mistake:

We are going to send every log to Log Analytics.

Diagnostic Settings are enabled for everything.

A month later, a considerable bill appears.

Governance does not mean indiscriminately storing every event Azure generates.

We need to distinguish between:

Security telemetry
Operational telemetry
Audit telemetry
Debug telemetry

And ask:

  • who consumes this log?
  • how long do we need to retain it?
  • which problem does it allow us to investigate?
  • what volume does it produce?
  • is it duplicated?
  • should it really end up in Log Analytics?

A Landing Zone should enforce the existence of observability.

It does not necessarily have to enforce that absolutely everything is stored forever.

The Landing Zone also needs FinOps from day one

A new subscription should be born with:

Owner
Cost center
Environment
Budget
Alert thresholds
Mandatory tags

Not six months later.

Subscription vending makes it possible to integrate budgets and other cost controls during provisioning.

Trying to attribute spending afterwards often leaves us with resources like:

rg-prod-final
rg-prod-new
rg-test2
rg-shared
rg-temp

and nobody knows who owns them.

Tagging helps.

But it should not be used to repair a poorly designed ownership model.

A platform team should not become the department of “no”

This is probably the most important part of the entire design.

A Landing Zone can be technically impeccable and organisationally terrible.

If deploying something means opening:

NETWORK-381
SEC-224
CLOUD-991
IAM-182

waiting through four meetings and obtaining three approvals, we have not created a platform.

We have created a ticket queue.

A good model should look much more like this:

             PLATFORM TEAM
                  |
       defines safe boundaries
                  |
       +----------+----------+
       |                     |
    Policies              Modules
       |                     |
       +----------+----------+
                  |
             paved road
                  |
         APPLICATION TEAM
                  |
             self-service

The central team maintains the boundaries.

Product teams operate within them.

Guardrails versus gates

This distinction summarises the problem quite well.

A gate says:

you have to ask me for permission.

A guardrail says:

you can do it yourself as long as you remain within these conditions.

Cloud works much better when we use guardrails.

For example:

Gate

Open a ticket to request a Storage Account.

Guardrail

The developer can deploy the Storage Account directly, but:

Public Access     = Disabled
TLS               >= required version
Diagnostic logs   = Enabled
Region            = Approved
Encryption        = Required

They do not need to ask for permission.

They simply cannot create a configuration that violates the established boundaries.

That is far more scalable.

What a Landing Zone should actually deliver

If we had to assess an Azure Landing Zone tomorrow, we would not simply check whether Management Groups exist.

We would look for something much broader.

Organisation

Management Group hierarchy
Subscription boundaries
Workload ownership
Platform subscriptions
Sandbox strategy

Identity

Least privilege
PIM
Break-glass accounts
Workload identities
Federated CI/CD authentication

Governance

Policy initiatives
Exceptions
Deny strategy
Allowed regions
Resource restrictions

Network

Hub-Spoke / vWAN decision
Egress strategy
Firewall
Private Link
DNS architecture
Hybrid connectivity
IP addressing

Security

Defender
Key management
Secrets
Public exposure
Security telemetry

Operations

Central monitoring
Alert ownership
Logs
Runbooks
Incident access

FinOps

Budgets
Ownership
Tagging
Cost attribution
Resource lifecycle

Automation

Terraform/OpenTofu
State boundaries
CI/CD
Subscription vending
Reusable workload modules

And probably the most important question:

can a team create a new workload without the platform team performing twenty manual operations?

If the answer is no, there is still work to do.

The best Landing Zone is the one that stops drawing attention to itself

A good Landing Zone should not constantly remind us that it exists.

A developer should be able to deploy.

A security team should have visibility.

Finance should be able to attribute costs.

The platform team should be able to maintain standards.

And none of them should unnecessarily block the others.

That requires far more engineering than simply deploying a reference blueprint.

Because the real problem has never been creating a Management Group structure.

The problem is deciding:

what we centralise, what we delegate and where we place the boundaries.

Azure gives us all the pieces.

Management Groups.

Policies.

RBAC.

PIM.

Subscriptions.

Private Link.

DNS Private Resolver.

Firewall.

Terraform.

Budgets.

Log Analytics.

But a list of services does not constitute an architecture.

Architecture appears when we decide how they should relate to one another.

And especially when we understand the trade-offs behind those decisions.

A mature Azure Landing Zone does not attempt to control every deployment.

It builds an environment where most correct deployments can happen without human intervention.

And it automatically blocks the few states the organisation is simply unwilling to accept.

That is the difficult balance.

Governance without friction. Autonomy without chaos.