Skip to content

CloudRunbook | Practical Cloud Engineering

Azure Landing Zones: the practical guide (secure-by-default, engineer-friendly)

7 min read

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.

Applies to
Management GroupsAzure PolicyRBACSubscription Vending
Effort
1–2 weeks
Risk
High
Rollback
Plan required
Last reviewed
26 Jul 2026
TL;DR
  • 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.
Before you start
  • 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 azurerm 3.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 → Platform and LandingZones as 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

  1. Management group layout – Keep it shallow, and put your own intermediate root directly under Tenant Root so you never assign policy at tenant root itself. Under it: Platform for shared services, LandingZones for workloads, plus Sandbox and Decommissioned. These are siblings, not a chain.
  2. Subscription lifecycle – Who can request one, what metadata is mandatory, how long until retirement, and how vending is automated (GitHub Actions / Azure DevOps).
  3. Identity boundaries – Which groups map to which RBAC roles, how PIM and break-glass are handled, and where admin workstations sit.
  4. Policy scope – Decide which guardrails land at the LandingZones management group and which are delegated to workload owners. Avoid “deny everything” until telemetry proves it is safe.
  5. Networking + DNS – Hub and spoke vs vWAN, who owns private DNS zones, how Private Endpoint approvals work, and how shared firewalls are funded.
  6. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

HCL
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.

BICEP
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:

BASH
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.

BASH
az account set --subscription "<workload-subscription-id>"
az monitor diagnostic-settings subscription list \
  --query "value[].{name:name, workspace:workspaceId}" -o table

Validation 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.

KQL
AzureActivity
| summarize lastSeen = max(TimeGenerated) by SubscriptionId
| where lastSeen < ago(1d)
| order by lastSeen asc

That 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:

KQL
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

Pitfall: treating landing zones as a one-time project

Landing zones are an operating model. If you do not plan for updates and drift control, your guardrails will decay.

Pitfall: private endpoints without DNS standardisation

Private connectivity is powerful, but it requires a clear DNS pattern and ownership model.

Pitfall: too much complexity too early

Over-modelling the org in management groups and policies makes governance brittle. Start simple, evolve intentionally.

Pitfall: deny-first everywhere

Start with denies for clear foot-guns. Audit the rest, then tighten based on evidence.

Pitfall: portal overrides

Engineers tweaking RBAC/policy manually will undo your work. Block portal edits unless through break-glass.

Pitfall: unlimited Defender toggles

Enabling every plan without owners creates noise and cost. Start with the ones you can respond to.

Pitfall: missing change log

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.

Final thought

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.