Azure Landing Zones: Technical Deep Dive for Architects
Bicep code, policy definitions, and implementation patterns
Iulian Mihai
Principal Cloud Architect & AI Innovation Leader

This is the implementation companion to my Principal Architect's Guide to Azure Landing Zones. If you're a fellow architect looking for concrete Bicep code, policy definitions, and management group structures — this is for you.
⚠️ Prerequisites
- • Azure subscription with Owner or User Access Administrator role
- • Azure CLI or PowerShell with Az module installed
- • Bicep CLI (comes with Azure CLI 2.20.0+)
- • Understanding of Azure Resource Manager and RBAC
Management Group Hierarchy: My Recommended Structure
The foundation of any Landing Zone is the management group hierarchy. Here's the structure I implement for most enterprise clients:
Tenant Root Group
└── Organization Root (e.g., "contoso")
├── Platform
│ ├── Management
│ │ └── [Management Subscription]
│ ├── Connectivity
│ │ └── [Connectivity Subscription]
│ └── Identity
│ └── [Identity Subscription]
├── Landing Zones
│ ├── Corp
│ │ ├── [Production Subscriptions]
│ │ └── [Non-Production Subscriptions]
│ └── Online
│ ├── [Public-facing workloads]
│ └── [Internet-exposed services]
├── Sandbox
│ └── [Developer sandboxes]
└── Decommissioned
└── [Subscriptions pending deletion]Why This Structure?
- Platform separation — Core infrastructure (identity, networking, monitoring) is isolated from workloads
- Corp vs Online — Different security postures for internal vs internet-facing workloads
- Sandbox isolation — Developers can experiment without affecting production policies
- Clear decommissioning path — No orphaned subscriptions floating around
Bicep: Creating the Management Group Hierarchy
Here's the Bicep code to deploy this hierarchy:
// management-groups.bicep
targetScope = 'tenant'
@description('The organization prefix (e.g., contoso)')
param orgPrefix string
@description('The location for deployment metadata')
param location string = 'westeurope'
// Root Management Group
resource rootMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: orgPrefix
properties: {
displayName: '${orgPrefix} Organization'
}
}
// Platform Management Groups
resource platformMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-platform'
properties: {
displayName: 'Platform'
details: {
parent: {
id: rootMg.id
}
}
}
}
resource managementMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-management'
properties: {
displayName: 'Management'
details: {
parent: {
id: platformMg.id
}
}
}
}
resource connectivityMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-connectivity'
properties: {
displayName: 'Connectivity'
details: {
parent: {
id: platformMg.id
}
}
}
}
resource identityMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-identity'
properties: {
displayName: 'Identity'
details: {
parent: {
id: platformMg.id
}
}
}
}
// Landing Zone Management Groups
resource landingZonesMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-landingzones'
properties: {
displayName: 'Landing Zones'
details: {
parent: {
id: rootMg.id
}
}
}
}
resource corpMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-corp'
properties: {
displayName: 'Corp'
details: {
parent: {
id: landingZonesMg.id
}
}
}
}
resource onlineMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-online'
properties: {
displayName: 'Online'
details: {
parent: {
id: landingZonesMg.id
}
}
}
}
// Sandbox
resource sandboxMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-sandbox'
properties: {
displayName: 'Sandbox'
details: {
parent: {
id: rootMg.id
}
}
}
}
// Decommissioned
resource decommissionedMg 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${orgPrefix}-decommissioned'
properties: {
displayName: 'Decommissioned'
details: {
parent: {
id: rootMg.id
}
}
}
}
output rootMgId string = rootMg.id
output platformMgId string = platformMg.id
output landingZonesMgId string = landingZonesMg.idDeploy with:
az deployment tenant create \
--location westeurope \
--template-file management-groups.bicep \
--parameters orgPrefix='contoso'Azure Policies: The Essential Set
These are the policies I deploy on every Landing Zone engagement. They're battle-tested across UN agencies, financial services, and Fortune 500 enterprises.
1. Deny Public IP on NICs
Prevents accidental exposure of VMs to the internet:
// policy-deny-public-ip.bicep
targetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'deny-public-ip-on-nic'
properties: {
displayName: 'Deny Public IP addresses on Network Interfaces'
description: 'Prevents creation of NICs with public IP addresses attached'
policyType: 'Custom'
mode: 'All'
metadata: {
version: '1.0.0'
category: 'Network'
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Network/networkInterfaces'
}
{
count: {
field: 'Microsoft.Network/networkInterfaces/ipConfigurations[*]'
where: {
field: 'Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIPAddress.id'
notEquals: ''
}
}
greaterOrEquals: 1
}
]
}
then: {
effect: 'deny'
}
}
}
}2. Enforce Resource Tagging
Critical for cost management and compliance. This policy requires specific tags on all resource groups:
// policy-require-tags.bicep
targetScope = 'managementGroup'
@description('Required tag names')
param requiredTags array = [
'CostCenter'
'Owner'
'Environment'
'Application'
]
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'require-rg-tags'
properties: {
displayName: 'Require mandatory tags on Resource Groups'
description: 'Enforces required tags on all resource groups for governance'
policyType: 'Custom'
mode: 'All'
metadata: {
version: '1.0.0'
category: 'Tags'
}
parameters: {
tagName: {
type: 'String'
metadata: {
displayName: 'Tag Name'
description: 'Name of the required tag'
}
}
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Resources/subscriptions/resourceGroups'
}
{
field: '[concat(\'tags[\', parameters(\'tagName\'), \']\')]'
exists: false
}
]
}
then: {
effect: 'deny'
}
}
}
}
// Create a policy initiative (policy set) for all required tags
resource policySetDefinition 'Microsoft.Authorization/policySetDefinitions@2021-06-01' = {
name: 'require-mandatory-tags'
properties: {
displayName: 'Require Mandatory Tags Initiative'
description: 'Enforces all mandatory tags on resource groups'
policyType: 'Custom'
metadata: {
version: '1.0.0'
category: 'Tags'
}
policyDefinitions: [for tag in requiredTags: {
policyDefinitionId: policyDefinition.id
parameters: {
tagName: {
value: tag
}
}
}]
}
}3. Allowed Locations (EU Data Sovereignty)
For EU-based organizations, this ensures data never leaves approved regions:
// policy-allowed-locations.bicep
targetScope = 'managementGroup'
@description('Allowed Azure regions for EU data sovereignty')
param allowedLocations array = [
'westeurope'
'northeurope'
'germanywestcentral'
'francecentral'
'swedencentral'
]
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'allowed-locations-eu'
properties: {
displayName: 'Allowed Locations - EU Only'
description: 'Restricts resource deployment to EU regions only'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
version: '1.0.0'
category: 'General'
}
parameters: {
listOfAllowedLocations: {
type: 'Array'
metadata: {
displayName: 'Allowed locations'
description: 'The list of allowed locations for resources'
strongType: 'location'
}
defaultValue: allowedLocations
}
}
policyRule: {
if: {
allOf: [
{
field: 'location'
notIn: '[parameters(\'listOfAllowedLocations\')]'
}
{
field: 'location'
notEquals: 'global'
}
{
field: 'type'
notEquals: 'Microsoft.AzureActiveDirectory/b2cDirectories'
}
]
}
then: {
effect: 'deny'
}
}
}
}4. Deny Storage Without HTTPS
// policy-storage-https.bicep
targetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'storage-require-https'
properties: {
displayName: 'Storage accounts should require HTTPS'
description: 'Ensures all storage accounts require secure transfer (HTTPS)'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
version: '1.0.0'
category: 'Storage'
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Storage/storageAccounts'
}
{
field: 'Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly'
notEquals: true
}
]
}
then: {
effect: 'deny'
}
}
}
}CAF Modules vs Custom: My Recommendation
The Azure Cloud Adoption Framework (CAF) provides Enterprise-Scale landing zone modules. Here's my honest assessment:
✅ Use CAF Modules When:
- • Greenfield deployment with no existing infrastructure
- • Standard enterprise requirements (no unusual compliance)
- • Limited internal Bicep/Terraform expertise
- • You want Microsoft-supported patterns
- • Faster time-to-production is priority
⚠️ Go Custom When:
- • Brownfield with existing management groups/policies
- • Highly specific compliance requirements (GDPR, PCI-DSS)
- • Multi-cloud strategy requiring consistency
- • Strong internal platform engineering team
- • Need fine-grained control over every resource
My Hybrid Approach
In practice, I often use a hybrid:
- Start with CAF structure — Use the management group hierarchy pattern
- Customize policies — CAF policies as baseline, then add custom ones
- Own your networking — Hub-spoke is universal, but implementation varies
- Abstract the modules — Create internal modules that wrap CAF or custom code
Hub-Spoke Network Topology
Here's a production-ready hub-spoke network setup:
// hub-network.bicep
targetScope = 'subscription'
@description('Azure region for deployment')
param location string = 'westeurope'
@description('Hub VNet address space')
param hubAddressPrefix string = '10.0.0.0/16'
@description('Gateway subnet prefix')
param gatewaySubnetPrefix string = '10.0.0.0/24'
@description('Azure Firewall subnet prefix')
param firewallSubnetPrefix string = '10.0.1.0/24'
@description('Bastion subnet prefix')
param bastionSubnetPrefix string = '10.0.2.0/24'
resource hubRg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'rg-connectivity-hub'
location: location
tags: {
CostCenter: 'Platform'
Owner: 'Platform Team'
Environment: 'Production'
Application: 'Network Hub'
}
}
module hubVnet 'modules/vnet.bicep' = {
scope: hubRg
name: 'hub-vnet'
params: {
name: 'vnet-hub-${location}'
location: location
addressPrefixes: [hubAddressPrefix]
subnets: [
{
name: 'GatewaySubnet'
addressPrefix: gatewaySubnetPrefix
}
{
name: 'AzureFirewallSubnet'
addressPrefix: firewallSubnetPrefix
}
{
name: 'AzureBastionSubnet'
addressPrefix: bastionSubnetPrefix
}
]
}
}
// Azure Firewall for centralized egress
module firewall 'modules/firewall.bicep' = {
scope: hubRg
name: 'hub-firewall'
params: {
name: 'afw-hub-${location}'
location: location
subnetId: hubVnet.outputs.subnets[1].id
sku: 'Premium' // Use Premium for IDPS and TLS inspection
}
}
// VPN Gateway for hybrid connectivity
module vpnGateway 'modules/vpn-gateway.bicep' = {
scope: hubRg
name: 'hub-vpn-gateway'
params: {
name: 'vpng-hub-${location}'
location: location
subnetId: hubVnet.outputs.subnets[0].id
sku: 'VpnGw2AZ' // Zone-redundant
}
}
output hubVnetId string = hubVnet.outputs.vnetId
output firewallPrivateIp string = firewall.outputs.privateIpDiagnostic Settings: Centralized Logging
Every enterprise landing zone needs centralized logging. Here's how to enforce it with policy:
// policy-diagnostic-settings.bicep
targetScope = 'managementGroup'
@description('Log Analytics Workspace ID for centralized logging')
param logAnalyticsWorkspaceId string
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'deploy-diagnostic-settings'
properties: {
displayName: 'Deploy Diagnostic Settings to Log Analytics'
description: 'Automatically configures diagnostic settings for supported resources'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
version: '1.0.0'
category: 'Monitoring'
}
parameters: {
logAnalytics: {
type: 'String'
metadata: {
displayName: 'Log Analytics Workspace'
description: 'The Log Analytics Workspace ID to send diagnostics to'
strongType: 'omsWorkspace'
}
defaultValue: logAnalyticsWorkspaceId
}
}
policyRule: {
if: {
field: 'type'
equals: 'Microsoft.KeyVault/vaults'
}
then: {
effect: 'deployIfNotExists'
details: {
type: 'Microsoft.Insights/diagnosticSettings'
existenceCondition: {
allOf: [
{
field: 'Microsoft.Insights/diagnosticSettings/workspaceId'
equals: '[parameters(\'logAnalytics\')]'
}
]
}
roleDefinitionIds: [
'/providers/Microsoft.Authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa'
'/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293'
]
deployment: {
properties: {
mode: 'incremental'
template: {
'$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
contentVersion: '1.0.0.0'
parameters: {
resourceName: { type: 'string' }
logAnalytics: { type: 'string' }
}
resources: [
{
type: 'Microsoft.KeyVault/vaults/providers/diagnosticSettings'
apiVersion: '2021-05-01-preview'
name: '[concat(parameters(\'resourceName\'), \'/Microsoft.Insights/setByPolicy\')]'
properties: {
workspaceId: '[parameters(\'logAnalytics\')]'
logs: [
{ category: 'AuditEvent', enabled: true }
{ category: 'AzurePolicyEvaluationDetails', enabled: true }
]
metrics: [
{ category: 'AllMetrics', enabled: true }
]
}
}
]
}
parameters: {
resourceName: { value: '[field(\'name\')]' }
logAnalytics: { value: '[parameters(\'logAnalytics\')]' }
}
}
}
}
}
}
}
}Implementation Checklist
🚀 Landing Zone Deployment Order
- 1Management Groups — Create hierarchy at tenant level
- 2Policy Definitions — Deploy custom policies to root MG
- 3Management Subscription — Log Analytics, Automation, Defender
- 4Connectivity Subscription — Hub VNet, Firewall, VPN/ExpressRoute
- 5Identity Subscription — Domain Controllers, AD Connect (if hybrid)
- 6Policy Assignments — Assign policies to appropriate MGs
- 7Landing Zone Subscriptions — Vend new subscriptions with guardrails
Common Pitfalls (And How I Avoid Them)
❌ Pitfall: Policy in Audit Mode Forever
Many teams deploy policies in "Audit" mode and never switch to "Deny". Set a deadline: audit for 2 weeks, then enforce.
❌ Pitfall: No Exemption Process
Policies without exemption paths create shadow IT. Build a governance process for time-limited exemptions with automatic expiry.
❌ Pitfall: Flat Subscription Model
Putting everything in one subscription makes cost allocation and RBAC impossible. Use subscriptions as isolation boundaries, not just billing containers.
❌ Pitfall: Hub Network Without Firewall
A hub without Azure Firewall or NVA is just a transit network. You lose all visibility and control over east-west and north-south traffic.
Next Steps
This post covers the structural foundation. In future deep dives, I'll cover:
- Subscription Vending Machine — Automated subscription provisioning
- Private DNS at Scale — Centralized DNS for Private Endpoints
- Cost Management Integration — FinOps from day one
- Defender for Cloud Setup — Security posture management
Need Help Implementing This?
I help enterprises design and deploy production-ready Azure Landing Zones. From greenfield setups to brownfield migrations — let's build it right.
Book a Landing Zone WorkshopTags
Need Help with Your Multi-Cloud Strategy?
I've helped Fortune 500 companies design and implement multi-cloud architectures that deliver real business value. Let's discuss how I can help your organization.
Book a ConsultationNot sure where to start?