CloudRunbook | Practical Cloud Engineering
Azure Landing Zones: the practical guide (secure-by-default, engineer-friendly)
A hands-on walkthrough of Azure Landing Zones (ALZ): what they are, why they matter, and a runbook-style path to deploying a secure platform foundation.
- Design the management group tree, policy packs, and logging story once, then force every subscription through it.
- Use Terraform or Bicep for everything, including policy initiatives and diagnostic settings, so drift is reviewable.
- Day 0: enforce the minimum viable baseline; Day 30: tighten the deny set; quarterly: prove it still works.
- Do not enable every Defender/Policy toggle on day one. Pick the controls you can respond to.
- Treat the platform like a product: backlog, releases, telemetry, and rollback.
- Access: Owner on the tenant root management group to create the hierarchy, plus Resource Policy Contributor at the intermediate root for policy work. Elevating a Global Administrator to User Access Administrator at tenant root is a deliberate, temporary step — turn it off afterwards.
- Tooling: Azure CLI 2.60+, Terraform 1.7+ with
azurerm3.100+, or Bicep CLI 0.28+. - Blast radius: management group moves and policy assignments apply tenant-wide. Pilot on a sandbox subscription first.
Why landing zones still matter
If you inherited an Azure estate where every subscription looks different, RBAC is welded to people, and nobody can prove logs land anywhere useful, you have already met the debt a landing zone avoids. A landing zone is the operating model plus the technical scaffolding that makes every new workload trustworthy by default. If vending a subscription takes a service request and a best-effort checklist, you do not have one yet.
What good looks like
- One management group hierarchy with clear owners: tenant root → an intermediate root you own →
PlatformandLandingZonesas siblings, with workload scopes under Landing Zones. Platform must not be a parent of Landing Zones, or every platform policy inherits straight into workload subscriptions. - Subscription vending pipeline that assigns RBAC, policy, and diagnostics automatically based on metadata.
- Guardrails split into deny (foot-guns), deployIfNotExists (baseline config), and audit (future enforcement).
- Shared networking/DNS patterns owned by platform, not retrofitted per workload.
- Logging + security operations with agreed routings for alerts, playbooks, and break-glass accounts.
- Runbooks for change: Day 0 baseline, Day 30 hardening, quarterly review with measured drift.
Baseline decision points
- Management group layout – Keep it shallow, and put your own intermediate root directly under
Tenant Rootso you never assign policy at tenant root itself. Under it:Platformfor shared services,LandingZonesfor workloads, plusSandboxandDecommissioned. These are siblings, not a chain. - Subscription lifecycle – Who can request one, what metadata is mandatory, how long until retirement, and how vending is automated (GitHub Actions / Azure DevOps).
- Identity boundaries – Which groups map to which RBAC roles, how PIM and break-glass are handled, and where admin workstations sit.
- Policy scope – Decide which guardrails land at the
LandingZonesmanagement group and which are delegated to workload owners. Avoid “deny everything” until telemetry proves it is safe. - Networking + DNS – Hub and spoke vs vWAN, who owns private DNS zones, how Private Endpoint approvals work, and how shared firewalls are funded.
- Operations + alerts – Where Defender, Sentinel, and platform alerts go, who triages, and how exemptions expire.
Signal vs noise
- Enable now: cost guardrails, public network denies, default diagnostic settings, Defender for Cloud plans you have owners for (Servers, SQL, Storage).
- Delay until Day 30: advanced Defender plans (Containers, APIs) if the SOC is not ready; aggressive TLS mandates if legacy workloads still exist; network micro-segmentation before hub routing is proven.
- Probably never: auto-enabling extensions nobody operates (legacy Log Analytics agents), enforcing niche policies because “the template had them,” or enabling Defender plans without budget. Every alert without an owner is noise.
Phased rollout
- Day 0 (Baseline) – Deploy management groups, core policies (deny obvious foot-guns, deploy diagnostics), subscription vending automation, and central logging.
- Day 30 (Hardening) – Review alert volume, promote high-confidence audits to deny, enable the next set of Defender plans, and extend diagnostics to tier-two services.
- Quarterly (Operate) – Measure drift (policy compliance, RBAC, diagnostics), prune unused subscriptions, update the initiative versions, and review exemptions.
Runbook: build an Azure landing zone you can scale
- Map the governance skeleton
Document the management group hierarchy, owner per scope, and where policy/role assignments will live. Keep the diagram in version control so reviews have a source of truth.
Use Terraform to pin the structure. Replace the IDs below with your tenant IDs and display names.
- Choose IaC rails (Terraform and Bicep)
Standardise on Terraform for hierarchy / vending and Bicep for platform-native teams who prefer Azure deployments. Everything goes through pull requests. No portal drift.
- Put privileged access on rails
Separate the platform admin identity from workload owners, enforce PIM, and document break-glass usage. Do not issue Owner at tenant root.
- Set guardrails (deny, deploy, audit)
Build initiatives for each landing zone persona (platform, mission-critical, sandbox). Start with denies for public IP foot-guns, deployIfNotExists to push diagnostic settings, and audits for upcoming change.
- Standardise networking and DNS
Pick hub-and-spoke or vWAN, decide who approves Private Endpoints, and publish the DNS ownership model. Without it, Private Link will be a 02:00 call-out.
- Make logging non-negotiable
Push subscription activity and resource diagnostics to a central workspace, enforce RBAC for the operations team, and write down who owns KQL.
- Automate subscription vending
The pipeline must: create the subscription, move it under the right management group, assign RBAC via groups, attach policy initiative, and configure diagnostics. Fail the build if any step is skipped.
- Treat the landing zone as a product
Maintain a backlog, change log, and release notes. Pilots first, then progressive rollout via the vending pipeline. Track exemptions with expiry dates.
Before running the subscription vending pipeline, apply this Terraform snippet to create management groups and a dedicated logging workspace scope. Update the parent_management_group_id and role assignment object IDs to match your tenant.
variable "platform_team_object_id" {
type = string
description = "Object ID of the Entra group that operates the platform."
}
# Your own intermediate root. Everything else hangs off this, so tenant root
# stays free of assignments and the whole hierarchy stays movable.
resource "azurerm_management_group" "root" {
display_name = "CloudRunbook"
name = "mg-crb"
}
# Platform and LandingZones are SIBLINGS. If Landing Zones sits under Platform,
# every platform policy inherits into workload subscriptions.
resource "azurerm_management_group" "platform" {
display_name = "Platform"
name = "mg-platform"
parent_management_group_id = azurerm_management_group.root.id
}
resource "azurerm_management_group" "landingzones" {
display_name = "LandingZones"
name = "mg-landingzones"
parent_management_group_id = azurerm_management_group.root.id
}
resource "azurerm_management_group" "sandbox" {
display_name = "Sandbox"
name = "mg-sandbox"
parent_management_group_id = azurerm_management_group.root.id
}
resource "azurerm_resource_group" "operations" {
name = "rg-operations-core"
location = "uksouth"
}
resource "azurerm_log_analytics_workspace" "landing" {
name = "lz-law-plat"
location = azurerm_resource_group.operations.location
resource_group_name = azurerm_resource_group.operations.name
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_role_assignment" "log_contributor" {
scope = azurerm_log_analytics_workspace.landing.id
role_definition_name = "Log Analytics Contributor"
principal_id = var.platform_team_object_id
}When creating initiatives, keep parameters lightweight so they can be reused. This Bicep creates an initiative plus an assignment at the LandingZones management group.
Two things people get wrong here. scope is not a property of a policy assignment — the assignment lands wherever the deployment targets, which is what targetScope sets. And any initiative containing a deployIfNotExists or modify effect needs a managed identity plus a role assignment, or remediation silently does nothing.
targetScope = 'managementGroup'
@description('Resource ID of the central Log Analytics workspace.')
param workspaceId string
@description('Region for the assignment identity. Not where policy applies.')
param location string = 'uksouth'
// Resolve real definition IDs first, e.g.:
// az policy definition list --query "[?contains(displayName,'diagnostic')].{name:displayName,id:id}" -o table
resource initiative 'Microsoft.Authorization/policySetDefinitions@2023-04-01' = {
name: 'alz-lz-guardrails'
properties: {
displayName: 'Landing Zone Guardrails'
description: 'Baseline deny + deploy rules for workload subscriptions.'
policyType: 'Custom'
parameters: {
logAnalyticsWorkspaceId: {
type: 'String'
metadata: { displayName: 'Central Log Analytics workspace' }
}
}
policyDefinitions: [
{
policyDefinitionReferenceId: 'deny-public-ip'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/<deny-public-ip-id>'
}
{
policyDefinitionReferenceId: 'deploy-diagnostics'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/<deploy-diagnostics-id>'
parameters: {
logAnalytics: {
value: '[parameters(\'logAnalyticsWorkspaceId\')]'
}
}
}
]
}
}
resource assignment 'Microsoft.Authorization/policyAssignments@2023-04-01' = {
name: 'alz-lz-guardrails'
// Required because the initiative contains a deployIfNotExists effect.
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
displayName: 'Apply baseline guardrails to LandingZones'
policyDefinitionId: initiative.id
parameters: {
logAnalyticsWorkspaceId: {
value: workspaceId
}
}
}
}
// The assignment identity needs rights to remediate. Contributor is the blunt
// option; scope it to the least-privileged role your definitions actually need.
resource remediationRights 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(managementGroup().id, assignment.id, 'Contributor')
properties: {
// Contributor
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'b24988ac-6180-42a0-ab88-20f7382dd24c'
)
principalId: assignment.identity.principalId
principalType: 'ServicePrincipal'
}
}Deploy it at the management group, not a subscription:
az deployment mg create \
--management-group-id mg-landingzones \
--location uksouth \
--template-file guardrails.bicep \
--parameters workspaceId="/subscriptions/<sub-id>/resourceGroups/rg-operations-core/providers/Microsoft.OperationalInsights/workspaces/lz-law-plat"Use the Azure CLI to verify a subscription emits activity logs to your workspace. Subscription-level diagnostic settings have their own sub-command — az monitor diagnostic-settings list works on resources, not subscriptions, and will fail here.
az account set --subscription "<workload-subscription-id>"
az monitor diagnostic-settings subscription list \
--query "value[].{name:name, workspace:workspaceId}" -o tableValidation checks
Use these KQL queries and governance checks to prove the landing zone is behaving.
Run this against your central Log Analytics workspace. It lists subscriptions that used to send activity logs and have since gone quiet.
Note the shape: you can’t find silence by filtering log rows, because a subscription that stopped sending has no rows to match. Take the last timestamp per subscription first, then test it.
AzureActivity
| summarize lastSeen = max(TimeGenerated) by SubscriptionId
| where lastSeen < ago(1d)
| order by lastSeen ascThat still only catches subscriptions that reported at some point. A subscription that never onboarded produces nothing at all, so compare the result against your expected inventory — Azure Resource Graph is the quickest source:
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions'
| project subscriptionId, name, state = tostring(properties.state)
| where state =~ 'Enabled'- ✓
A new subscription can be vended by pipeline and is compliant on day 0 (RBAC, policy, logging).
- ✓
All subscriptions sit under the intended management group with no manual assignments.
- ✓
Platform subscriptions carry tighter controls than workload subscriptions.
- ✓
Policy initiative compliance is ≥95%, assignments are versioned in Git, and exemptions have an expiry date.
- ✓
Log Analytics workspace receives Activity, Resource, and Defender signals within 5 minutes of change.
- ✓
Defender plans enabled match the documented matrix (Servers, SQL, Storage at Day 0; Containers from Day 30).
- ✓
Networking has a defined standard and DNS has a named owner.
- ✓
Subscription vending pipeline output includes RBAC, policy assignment, and diagnostics confirmations.
Common pitfalls
Landing zones are an operating model. If you do not plan for updates and drift control, your guardrails will decay.
Private connectivity is powerful, but it requires a clear DNS pattern and ownership model.
Over-modelling the org in management groups and policies makes governance brittle. Start simple, evolve intentionally.
Start with denies for clear foot-guns. Audit the rest, then tighten based on evidence.
Engineers tweaking RBAC/policy manually will undo your work. Block portal edits unless through break-glass.
Enabling every plan without owners creates noise and cost. Start with the ones you can respond to.
If teams cannot see what the platform changed last week, they bypass it. Publish release notes.
Rollback / back-out plan
- Policy releases – disable or reassign the offending policy assignment; leave the definition in place to avoid referencing issues. Remediate the resources separately.
- Vending pipeline failures – subscriptions created during failed runs should be moved to a quarantine management group, stripped of RBAC, then reprocessed once the pipeline is fixed.
- Networking/DNS rollbacks – revert to the previous hub configuration through IaC; publish the change window and flush Private DNS zones after validation.
- Logging changes – if diagnostic settings cause duplicate ingestion cost, roll back the deployIfNotExists policy assignment and reapply once the policy definition is corrected.
Document every rollback in the platform change log and include the date you expect to re-attempt the change. Landing zones are only “done” when you can repeat them safely.
A landing zone is how you make “secure by default” real. The fastest teams are not the ones with no controls. They are the ones with standard controls that do not require constant negotiation.