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.
Centralised networking looks simple until Private Link appears
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.



