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'
}
}
}
}π‘Need help with your Bicep/Terraform setup?
I can review your current infrastructure code or help you build production-ready modules.
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
Want to Go Deeper?
Azure Landing Zone Readiness Workshop
A hands-on 1-day workshop where we design your management group hierarchy, deploy policies with Bicep, configure hub-spoke networking, and build your implementation roadmap.
Tags
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?