Azure Landing Zone with Bicep

Deploy an enterprise Azure governance foundation using Bicep infrastructure as code.

Introduction

30 Second Summary

Every large organization needs guardrails to keep cloud resources organized, secure, and compliant. Without them, teams spin up resources in random regions, skip tagging, and create a sprawl that nobody can manage.

In this project, you will build the foundational governance layer of an Azure Landing Zone using Bicep infrastructure as code. You will deploy a management group hierarchy, enforce governance policies, and set up centralized monitoring.

What You'll Build

You will create a structured management group hierarchy with Azure Policy governance and a centralized Log Analytics workspace, all deployed through modular Bicep templates that span tenant, management group, and subscription scopes.

By the end of this project, you'll have:

  • A management group hierarchy with 5 groups following the Cloud Adoption Framework reference architecture.
  • Custom Azure Policy definitions and assignments that enforce governance rules across your management groups, all defined in Bicep.
  • A centralized Log Analytics workspace for monitoring, deployed via a subscription-scoped Bicep module from a management group deployment.
  • A policy initiative that bundles multiple governance policies together (Secret Mission).

Are there any prerequisites?

You need basic Bicep template experience (parameters, resources, outputs) and an Azure subscription where you have the Global Admin role. Global Admin access is required to create management groups and deploy resources at the tenant level.

Before We Start

Before We Start...

Set Up Your Tools and Elevate Azure Access

Before you can deploy any infrastructure to Azure, you need the right tools installed and the right level of access.

Landing zone deployments happen at the tenant level, which is the highest scope in Azure. That means your account needs elevated permissions that go beyond a normal subscription. In this step, you'll install your tools, elevate your access, and create the project folder structure.

In This Step, Get Ready To:
  • Install and verify Azure CLI and the Bicep extension.
  • Log in and elevate your Global Admin access to the tenant root scope.
  • Create a modular project folder structure for your Bicep templates.
Install and Verify Azure CLI and Bicep

The Azure CLI is your command-line interface for managing Azure resources. It bundles the Bicep CLI, so installing Azure CLI gives you both tools at once.

macOS

  • Open your terminal and run the following command to install Azure CLI via Homebrew:
brew update && brew install azure-cli

Don't have Homebrew?

Install it first by visiting brew.sh and following the instructions.

Windows

  • Open PowerShell as Administrator and run the following command:
winget install --id Microsoft.AzureCLI -e

After installation completes, close and reopen your terminal so the az command is available.

  • Verify that Azure CLI is installed correctly:
az --version

You should see the Azure CLI version number along with a list of installed extensions.

  • Verify that the Bicep CLI is available:
az bicep version

What does this command do?

This checks that the Bicep CLI is bundled with your Azure CLI installation. If Bicep is not installed, Azure CLI will automatically download it the first time you run this command.

I see a version number

You're all set. Both Azure CLI and Bicep are installed and working.

I see an error

If you see "command not found" or a similar error, try closing and reopening your terminal. If the issue persists, run az bicep install to manually install the Bicep CLI.

Still stuck? Help me troubleshoot my Azure CLI or Bicep installation.

  • Open VS Code.
  • Go to the Extensions view by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Search for Bicep and install the official extension published by Microsoft.

Pro tip: syntax highlighting

The Bicep extension gives you syntax highlighting, IntelliSense, and error checking for .bicep files. It makes catching typos and exploring resource properties much faster.

Log In and Elevate Global Admin Access

Deploying management groups requires permissions at the tenant root scope. Even with the Global Admin role in Microsoft Entra ID, you don't automatically have Azure resource management access at the root. You need to explicitly elevate your access first.

  • Log in to Azure from your terminal:
az login

What does this command do?

This opens a browser window where you authenticate with your Azure account. Once you sign in, the CLI stores your credentials so you can run deployment commands from the terminal.

A browser window opens for authentication. Sign in with the account that has the Global Admin role.

Now you need to elevate your access so you can deploy resources at the tenant root scope. This is done in the Azure portal.

  • Open the Azure portal in your browser.
  • Search for and open Microsoft Entra ID.
  • Select Properties in the left navigation menu.
  • Scroll down to Access management for Azure resources.
  • Toggle this setting to Yes.
  • Click Save.

Why do I need to elevate my access?

Enabling this setting grants your account the User Access Administrator role at the tenant root scope (/). Without this, tenant-level deployments like creating management groups will fail with authorization errors.

📣 Remember to disable this later

This elevated access is a powerful permission. The Before You Go section at the end of this project will remind you to toggle this setting back to No once you're done.

Create Your Project Folder Structure

A well-organized folder structure keeps your Bicep templates manageable as the project grows. You'll create a root directory with a main.bicep orchestrator file and a modules/ folder containing separate files for each deployment concern.

  • Run the following commands in your terminal to create the project structure:

macOS

mkdir -p alz-bicep/modules
touch alz-bicep/main.bicep
touch alz-bicep/modules/managementGroups.bicep
touch alz-bicep/modules/policies.bicep
touch alz-bicep/modules/monitoring.bicep

Windows

New-Item -ItemType Directory -Path alz-bicep\modules -Force
New-Item -ItemType File -Path alz-bicep\main.bicep -Force
New-Item -ItemType File -Path alz-bicep\modules\managementGroups.bicep -Force
New-Item -ItemType File -Path alz-bicep\modules\policies.bicep -Force
New-Item -ItemType File -Path alz-bicep\modules\monitoring.bicep -Force

What does this command do?

This creates an alz-bicep project directory with a modules/ subfolder. Each file maps to a specific deployment concern:

  • main.bicep is the orchestrator that calls all modules.
  • managementGroups.bicep defines the management group hierarchy.
  • policies.bicep contains policy definitions and assignments.
  • monitoring.bicep deploys the Log Analytics workspace.
  • Open the project folder in VS Code:
code alz-bicep

VS Code opens with your project structure visible in the Explorer panel. You should see main.bicep at the root and four .bicep files inside the modules/ folder.

Why a modular structure?

Each Bicep file targets a different deployment scope (tenant, management group, or subscription). Splitting them into modules lets the orchestrator call each one at the correct scope using Bicep's scope property. This is the standard pattern for landing zone deployments.

Your tools are installed, your access is elevated, and your project structure is ready. Next up, you'll write and deploy the management group hierarchy that forms the backbone of your landing zone.

Deploy the Management Group Hierarchy

In the previous step, you set up your tools, elevated your access, and created a modular project structure. Now it's time to build the backbone of your landing zone.

Management groups are the top-level organizational containers in Azure. They sit above subscriptions and let you apply governance (like policies and access controls) to multiple subscriptions at once. The Cloud Adoption Framework recommends a specific hierarchy pattern that separates platform concerns from workload concerns. In this step, you'll define that hierarchy in Bicep and deploy it to your Azure tenant.

In This Step, Get Ready To:
  • Write a tenant-scoped Bicep template that defines 5 management groups.
  • Deploy the management group hierarchy to your Azure tenant.
  • Verify the hierarchy in both the CLI and the Azure portal.
Write the Management Group Bicep Template

Management groups are tenant-level resources, which means the Bicep template needs to target the tenant scope. The CAF reference architecture uses a custom root group that sits under Azure's built-in Tenant Root Group. All other groups branch out from this custom root.

You'll create a simplified version of the CAF hierarchy with 5 groups: a custom root group (alz), and four child groups for Platform, Landing Zones, Decommissioned, and Sandbox.

  • Open the modules/managementGroups.bicep file in VS Code.
  • Add the following Bicep code to define the management group hierarchy.
targetScope = 'tenant'

// Custom root management group under the Tenant Root Group
resource rootMG 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: 'alz'
  properties: {
    displayName: 'Azure Landing Zones'
  }
}

// Platform group for shared infrastructure services
resource platformMG 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: 'alz-platform'
  properties: {
    displayName: 'Platform'
    details: {
      parent: {
        id: rootMG.id
      }
    }
  }
}

// Landing Zones group for workload subscriptions
resource landingZonesMG 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: 'alz-landingzones'
  properties: {
    displayName: 'Landing Zones'
    details: {
      parent: {
        id: rootMG.id
      }
    }
  }
}

// Decommissioned group for subscriptions being retired
resource decommissionedMG 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: 'alz-decommissioned'
  properties: {
    displayName: 'Decommissioned'
    details: {
      parent: {
        id: rootMG.id
      }
    }
  }
}

// Sandbox group for experimentation and dev/test
resource sandboxMG 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: 'alz-sandbox'
  properties: {
    displayName: 'Sandbox'
    details: {
      parent: {
        id: rootMG.id
      }
    }
  }
}

// Output the IDs for use by other modules
output rootMGId string = rootMG.id
output platformMGId string = platformMG.id
output landingZonesMGId string = landingZonesMG.id
output decommissionedMGId string = decommissionedMG.id
output sandboxMGId string = sandboxMG.id

What does this template do?

The targetScope = 'tenant' line tells Bicep this template deploys at the tenant level, which is required for management group resources.

Each management group uses the Microsoft.Management/managementGroups@2021-04-01 resource type. The name property is the unique ID, while displayName is the human-readable label. The details.parent.id property establishes the parent-child relationship. The root group has no parent specified, so it defaults to sitting under the Tenant Root Group.

💡 Why a Simplified Hierarchy?

The full CAF architecture includes additional groups like Identity, Management, and Connectivity under Platform, plus Corp and Online under Landing Zones. This project uses a simplified 5-group hierarchy to teach the core pattern without the complexity. Once you understand how parent-child relationships work, extending the hierarchy is straightforward.

Deploy the Management Group Hierarchy

Since management groups are tenant-level resources, you'll use the az deployment tenant create command. This command requires a --location parameter to store the deployment metadata (not the resources themselves).

  • Open your terminal and navigate to your project root directory.
  • Run the following command to deploy the management group hierarchy.
az deployment tenant create \
  --location [[REGION="your Azure region, e.g. eastus"]] \
  --template-file modules/managementGroups.bicep

What does this command do?

The az deployment tenant create command deploys a Bicep template at the tenant scope. The --location flag specifies where Azure stores the deployment metadata. It does not affect where the management groups are created, since management groups are global resources.

The deployment may take a minute or two to complete. Once it finishes, you'll see a JSON output with the deployment details and a provisioningState of Succeeded.

  • Verify the hierarchy was created by listing all management groups.
az account management-group list --output table

What should I see?

You should see a table listing all 5 management groups you just created: alz, alz-platform, alz-landingzones, alz-decommissioned, and alz-sandbox. You'll also see the default Tenant Root Group in the list.

🙋‍♀️ Getting an authorization error?

If you see an error about insufficient permissions, make sure you completed the Global Admin elevation step. Navigate to Microsoft Entra ID > Properties in the Azure portal and confirm that Access management for Azure resources is set to Yes. You may also need to log out and log back in with az login for the elevated permissions to take effect. Help me troubleshoot this error.

Verify the Hierarchy in the Azure Portal

The CLI output confirms the groups exist, but the Azure portal gives you a visual tree view that makes the parent-child relationships much clearer.

  • Open the Azure portal.
  • Search for Management groups in the top search bar and select it.
  • Expand the Tenant Root Group to see the Azure Landing Zones group nested underneath.
  • Expand Azure Landing Zones to confirm all four child groups appear: Platform, Landing Zones, Decommissioned, and Sandbox.

Pro tip: propagation delay

Management group operations can take a few minutes to propagate. If you don't see all groups immediately in the portal, wait a minute and refresh the page.

Your management group hierarchy is live. Next up, you'll define custom Azure policies in Bicep and assign them to enforce governance rules across your landing zones.

Define and Assign Governance Policies

Your management group hierarchy gives you a structure to organize resources. Now you need rules that enforce governance across that structure. Azure Policy lets you define rules as code and assign them to management groups, so every subscription underneath automatically inherits the governance controls. In this step, you'll write a custom policy definition and assign it to your Landing Zones management group.

In This Step, Get Ready To:
  • Write a custom Azure Policy definition that restricts resource deployment to allowed regions.
  • Assign the policy to the Landing Zones management group so all child subscriptions inherit it.
  • Deploy and verify the policy using the Azure CLI.
Write a Custom Policy Definition in Bicep

Azure has hundreds of built-in policies, but built-in policies are tenant-level resources that you cannot modify. Writing a custom policy teaches you the full policy lifecycle: define, assign, and evaluate. You'll create a policy that restricts which Azure regions resources can be deployed to.

Why Custom Policies Instead of Built-In?

Built-in policies are convenient, but they live at the tenant scope and can't be customized. Writing your own policy definitions gives you full control over the logic, parameters, and effect. Once you understand how custom policies work, you can adapt them to any governance requirement your organization needs.

Custom policy definitions cannot be deployed at the tenant scope. They must be deployed at the management group or subscription scope. Your Bicep file needs to reflect this.

  • Create a new file called modules/policies.bicep in your project directory.
  • Add the following Bicep code to define a custom allowed-regions policy.
targetScope = 'managementGroup'

// Parameter: list of Azure regions where resources are allowed
param allowedLocations array = [
  'eastus'
  'westus2'
  'centralus'
]

param landingZonesMgId string

// Custom policy definition: deny resources outside allowed regions
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2025-03-01' = {
  name: 'policy-allowed-regions'
  properties: {
    policyType: 'Custom'
    mode: 'All'
    displayName: 'Allowed Regions Only'
    description: 'Restrict resource deployment to a set of allowed Azure regions.'
    parameters: {
      allowedLocations: {
        type: 'Array'
        metadata: {
          displayName: 'Allowed locations'
          description: 'The list of locations that resources can be deployed to.'
        }
      }
    }
    policyRule: {
      if: {
        not: {
          field: 'location'
          in: '[parameters(\'allowedLocations\')]'
        }
      }
      then: {
        effect: 'deny'
      }
    }
  }
}

What Does This Template Do?

The targetScope = 'managementGroup' line tells Bicep this template deploys at the management group level. The policy definition uses a deny effect, which means any resource deployment to a region not in the allowedLocations list will be blocked. The policyType: 'Custom' and mode: 'All' settings ensure the policy evaluates all resource types.

Assign the Policy to the Landing Zones Management Group

A policy definition on its own does nothing. You need to assign it to a scope for it to take effect. By assigning the policy to the Landing Zones management group, every subscription under that group will inherit the restriction.

  • Add the following policy assignment resource to the bottom of modules/policies.bicep.
// Assign the allowed-regions policy to the Landing Zones management group
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2025-03-01' = {
  name: 'assign-allowed-regions'
  properties: {
    displayName: 'Enforce Allowed Regions on Landing Zones'
    description: 'Restricts resource deployment to approved Azure regions for all Landing Zone subscriptions.'
    policyDefinitionId: policyDefinition.id
    parameters: {
      allowedLocations: {
        value: allowedLocations
      }
    }
  }
}

What Does This Assignment Do?

The policyDefinitionId: policyDefinition.id line references the policy definition using its Bicep symbolic name. This is a key advantage of defining both resources in the same template. Bicep automatically resolves the full resource ID for you, so you don't need to construct it manually.

Your complete modules/policies.bicep file now contains both the policy definition and its assignment. Save the file before moving on.

Deploy and Verify the Policy

Since the policy template targets the management group scope, you'll use az deployment mg create to deploy it. You need to target the Landing Zones management group so the assignment takes effect there.

  • Run the following command to deploy the policy template.
az deployment mg create \
  --management-group-id alz-landing-zones \
  --location [[REGION="your Azure region, e.g. eastus"]] \
  --template-file modules/policies.bicep \
  --parameters landingZonesMgId='alz-landing-zones'

What Does This Command Do?

The --management-group-id flag specifies which management group to deploy to. The --location flag sets where Azure stores the deployment metadata (not where your resources go). The policy definition and assignment are created at the management group scope you specified.

The deployment may take a minute or two to complete. Once it finishes, verify that the policy assignment was created.

  • Run the following command to list policy assignments on the Landing Zones management group.
az policy assignment list \
  --scope /providers/Microsoft.Management/managementGroups/alz-landing-zones \
  --output table

What Should I See?

You should see a table with your assign-allowed-regions assignment listed. The display name should show Enforce Allowed Regions on Landing Zones. This confirms the policy is active and will deny resource deployments outside your allowed regions for any subscription under the Landing Zones management group.

I see my policy assignment

Your governance policy is live. Any subscription placed under the Landing Zones management group will now be restricted to deploying resources only in the regions you specified.

I see an error

Having Trouble with the Deployment?

  • If you see an authorization error, make sure you still have the "Access management for Azure resources" toggle enabled in Microsoft Entra ID Properties.
  • If the management group is not found, verify that the management group ID matches exactly what you used in Step 2. Management group operations can take a few minutes to propagate.
  • If you see a Bicep compilation error, check that your targetScope is set to 'managementGroup' and not 'tenant'. Custom policy definitions cannot be deployed at the tenant scope.

Still stuck? Help me debug my policy deployment.

  • Open the Azure portal and navigate to Policy.
  • Select Definitions in the left sidebar.
  • Search for Allowed Regions Only to confirm your custom policy definition appears.
  • Select Assignments in the left sidebar to verify the assignment is active on the Landing Zones management group.

Your governance layer is now in place. Any subscription under Landing Zones will be blocked from deploying resources outside your approved regions. Next up, you'll deploy a centralized monitoring workspace to complete the core infrastructure.

Deploy Centralized Monitoring

Your management groups are organized and your governance policies are enforcing rules across the hierarchy. The next piece of any enterprise landing zone is centralized monitoring. In this step, you'll deploy a Log Analytics workspace into a dedicated resource group, and you'll do it from a management group deployment using Bicep's cross-scope module pattern.

In This Step, Get Ready To:
  • Write a subscription-scoped Bicep module that creates a resource group and Log Analytics workspace.
  • Create an orchestrator template that deploys the monitoring module from the management group scope.
  • Verify the Log Analytics workspace is live and ready for centralized monitoring.
Write a Subscription-Scoped Monitoring Module

A Log Analytics workspace is a subscription-level resource, but your landing zone deployment lives at the management group scope. Bicep solves this mismatch with modules. You write the monitoring resources in a separate file scoped to subscription, then call that module from a higher-scoped template. This cross-scope deployment pattern is the key architectural technique in landing zone projects.

  • Create a new file called modules/monitoring.bicep.
  • Paste the following Bicep template into the file.
targetScope = 'subscription'

@description('The Azure region for monitoring resources.')
param location string

@description('The name of the resource group for monitoring.')
param resourceGroupName string = 'rg-alz-monitoring'

@description('The name of the Log Analytics workspace.')
param workspaceName string = 'law-alz-monitoring'

// Create a dedicated resource group for monitoring resources
resource monitoringRg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: resourceGroupName
  location: location
}

// Deploy the Log Analytics workspace into the monitoring resource group
module workspace 'workspace.bicep' = {
  name: 'deploy-log-analytics'
  scope: monitoringRg
  params: {
    location: location
    workspaceName: workspaceName
  }
}

@description('The resource ID of the Log Analytics workspace.')
output workspaceId string = workspace.outputs.workspaceId

This template is scoped to subscription, which means it can create resource groups. It creates a resource group called rg-alz-monitoring and then uses a nested module to deploy the workspace inside that resource group.

The nested module references a workspace.bicep file that handles the actual workspace resource. You need to create that file next.

  • Create a new file called modules/workspace.bicep.
  • Paste the following template into the file.
@description('The Azure region for the workspace.')
param location string

@description('The name of the Log Analytics workspace.')
param workspaceName string

// Deploy the Log Analytics workspace with a 30-day retention period
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: workspaceName
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}

@description('The resource ID of the Log Analytics workspace.')
output workspaceId string = logAnalytics.id

What does this template do?

This file deploys a single Log Analytics workspace using the Microsoft.OperationalInsights/workspaces@2023-09-01 resource type. The PerGB2018 SKU is the standard pricing tier, and the free tier allows up to 500 MB/day of data ingestion. The workspace outputs its resource ID so the parent module can reference it.

💡 Why Two Separate Files?

A subscription-scoped template can create resource groups, but it cannot directly create resources inside them in the same file. You need a resource-group-scoped module (no targetScope declaration, which defaults to resourceGroup) to deploy resources into a specific resource group. The scope: monitoringRg line in the parent module bridges the two scopes.

Orchestrate the Deployment From the Management Group Scope

Your monitoring module is subscription-scoped, but your landing zone deployment operates at the management group level. You need an orchestrator template that calls the monitoring module and targets a specific subscription. This is the cross-scope deployment pattern that makes Bicep so powerful for landing zone architectures.

  • Create a new file called modules/monitoringDeploy.bicep.
  • Paste the following orchestrator template into the file.
targetScope = 'managementGroup'

@description('The subscription ID where monitoring resources will be deployed.')
param subscriptionId string

@description('The Azure region for monitoring resources.')
param location string

// Deploy the monitoring module into the target subscription
module monitoring 'monitoring.bicep' = {
  name: 'deploy-monitoring'
  scope: subscription(subscriptionId)
  params: {
    location: location
  }
}

@description('The resource ID of the Log Analytics workspace.')
output workspaceId string = monitoring.outputs.workspaceId

What is cross-scope deployment?

The scope: subscription(subscriptionId) line is the key. It tells Bicep to deploy the monitoring module at the subscription scope, even though this orchestrator template runs at the management group scope. Bicep's scope functions (tenant(), managementGroup(), subscription()) let you reach across Azure's scope hierarchy from a single deployment.

Deploy and Verify the Monitoring Workspace

You need your Azure subscription ID to tell the orchestrator where to deploy the monitoring resources. Then you'll deploy the orchestrator at the management group scope.

  • Run the following command to get your subscription ID.
az account show --query id --output tsv
  • Copy the subscription ID from the output.
  • Deploy the orchestrator template at the management group scope using the following command.
az deployment mg create \
  --management-group-id alz-platform \
  --location [[LOCATION="your Azure region, e.g. eastus"]] \
  --template-file modules/monitoringDeploy.bicep \
  --parameters subscriptionId=[[SUBID="your subscription ID"]] location=[[LOCATION="your Azure region, e.g. eastus"]]

What does this command do?

The az deployment mg create command deploys a Bicep template at the management group scope. The --management-group-id flag specifies which management group to target. The template then uses the subscription() scope function to reach down into your subscription and create the resource group and workspace there.

The deployment may take a minute or two to complete.

I see a successful deployment

You should see JSON output with "provisioningState": "Succeeded" in the response. Your monitoring resources are deployed.

I see an error

Having trouble with the deployment?

Common issues include:

  • An incorrect subscription ID. Double-check the output of az account show --query id --output tsv.
  • A management group ID that doesn't match what you created in Step 2. Verify with az account management-group list --output table.
  • A file path issue. Make sure you're running the command from your project root directory.

Paste your error message here for help: help me debug this deployment error.

Now verify that the Log Analytics workspace was actually created in your subscription.

  • Run the following command to check the workspace.
az monitor log-analytics workspace show \
  --resource-group rg-alz-monitoring \
  --workspace-name law-alz-monitoring \
  --output table

What should I see?

You should see a table with your workspace name, resource group, location, and provisioning state showing Succeeded. This confirms the cross-scope deployment worked. A management group-scoped template successfully created resources inside your subscription.

  • Open the Azure portal and navigate to Resource groups.
  • Click on rg-alz-monitoring.
  • Confirm that the law-alz-monitoring Log Analytics workspace appears inside the resource group.

Your centralized monitoring is in place. You've now deployed resources across three different Azure scopes: tenant, management group, and subscription. Next up, you'll tie all of these modules together into a single orchestrator template that deploys the entire landing zone foundation in one command.

Tie It All Together with an Orchestrator Template

You have all three components deployed individually: management groups, policies, and monitoring. In a real enterprise environment, you would not deploy each piece separately every time. Instead, you need a single entry point that orchestrates the entire landing zone deployment across all three scopes.

In this step, you will build a main.bicep orchestrator that calls all your modules in sequence. This is the key architectural pattern in landing zone deployments: a single tenant-scoped template that cascades deployments down through management groups and into subscriptions.

In This Step, Get Ready To:
  • Create a main orchestrator template that calls all three modules across different scopes.
  • Deploy the complete landing zone foundation with a single command.
  • Validate the end-to-end deployment across all resource types.
Create the Main Orchestrator

The orchestrator is a tenant-scoped Bicep template that uses module blocks with scope functions to deploy each component at the correct level. Bicep provides three scope functions you will use here: tenant() for management groups, managementGroup() for policies, and subscription() for monitoring resources.

  • Open your project folder in VS Code.
  • Create a new file called main.bicep in the root of your project directory.
  • Add the following content to main.bicep:
targetScope = 'tenant'

// Parameters
param location string = 'eastus'
param subscriptionId string

// Module 1: Deploy management group hierarchy at tenant scope
module managementGroups 'modules/managementGroups.bicep' = {
  name: 'deploy-management-groups'
  scope: tenant()
}

// Module 2: Deploy policies at the Landing Zones management group scope
module policies 'modules/policies.bicep' = {
  name: 'deploy-policies'
  scope: managementGroup('alz-landingzones')
  dependsOn: [
    managementGroups
  ]
}

// Module 3: Deploy monitoring resources at subscription scope
module monitoring 'modules/monitoring.bicep' = {
  name: 'deploy-monitoring'
  scope: subscription(subscriptionId)
  params: {
    location: location
  }
  dependsOn: [
    managementGroups
  ]
}

What Does This Template Do?

This orchestrator template is set to targetScope = 'tenant', which means it deploys at the highest level in Azure. From there, each module block uses a different scope function to target the right level:

  • tenant() keeps the management groups module at the tenant level, where management group resources belong.
  • managementGroup('alz-landingzones') targets the policies module at the Landing Zones management group, since custom policy definitions cannot be deployed at tenant scope.
  • subscription(subscriptionId) drops the monitoring module into a specific subscription where resource groups and workspaces live.

The dependsOn blocks ensure the management groups exist before policies or monitoring resources try to deploy into them.

💡 Why a Single Orchestrator Instead of Separate Deployments?

You could deploy each module individually with separate CLI commands, which is what you did in earlier steps. An orchestrator template lets you deploy everything with one command, manage dependencies between components, and track the entire landing zone as a single deployment. This is the pattern used in production Cloud Adoption Framework deployments.

Deploy the Complete Landing Zone Foundation

Before deploying, you need your Azure subscription ID. The orchestrator needs this to know where to deploy the monitoring resources.

  • Run the following command to find your subscription ID:
az account show --query id --output tsv
  • Copy the subscription ID from the output.
  • Deploy the full orchestrator with the following command:
az deployment tenant create \
  --location eastus \
  --template-file main.bicep \
  --parameters subscriptionId=[[SUBID="your-subscription-id"]]

What Should I See?

The deployment cascades across all three scopes. You will see the CLI processing the tenant-level deployment first, then the management group and subscription modules. The entire deployment may take a few minutes to complete. When it finishes, you will see a JSON output with "provisioningState": "Succeeded".

🙋‍♀️ Seeing an Authorization Error?

If you see an error about insufficient permissions, make sure you still have Access management for Azure resources enabled in Microsoft Entra ID. This setting can sometimes revert. Also confirm you are logged in with the correct account by running az account show.

If you see errors about management groups already existing, that is expected. Bicep handles existing resources by updating them in place.

Still stuck? Help me troubleshoot this deployment error.

Validate the End-to-End Deployment

With the orchestrator deployment complete, you should verify that every component was created correctly across all three scopes.

  • Verify the management group hierarchy:
az account management-group list --output table
  • Confirm you see all five management groups: your custom root group, Platform, Landing Zones, Decommissioned, and Sandbox.
  • Verify the policy assignment on the Landing Zones management group:
az policy assignment list \
  --scope /providers/Microsoft.Management/managementGroups/alz-landingzones \
  --output table
  • Confirm your allowed-regions policy assignment appears in the output.
  • Verify the Log Analytics workspace:
az monitor log-analytics workspace show \
  --resource-group rg-alz-monitoring \
  --workspace-name [[LANAME="your-log-analytics-workspace-name"]] \
  --output table
  • Confirm the workspace exists and shows a Succeeded provisioning state.

All three commands returning successful results confirms your orchestrator deployed the complete landing zone foundation in a single pass.

  • Open the Azure portal and navigate to Management Groups.
  • Verify the full hierarchy is displayed with the correct parent-child relationships.
  • Navigate to Policy in the portal and select Assignments.
  • Confirm your allowed-regions policy assignment is listed under the Landing Zones management group scope.

Your entire Azure Landing Zone foundation is now deployed from a single Bicep orchestrator. Next up, you can take on the Secret Mission to extend your governance with a policy initiative.

Secret mission

Create a Policy Initiative

You've deployed individual policies, but enterprise environments bundle related policies into initiatives for easier management. Can you create a policy initiative that combines your allowed-regions policy with a new require-tags policy, and assign the whole bundle to your Landing Zones management group?

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Management groups, policy definitions, and policy assignments are all free in Azure. The Log Analytics workspace stays within the free tier as long as you remain under 500 MB/day ingestion, which is easy when no data is flowing in.

Resources you used:

  • 5 management groups (free).
  • Custom policy definition (free).
  • Policy assignment on Landing Zones management group (free).
  • Log Analytics workspace in rg-alz-monitoring resource group (free tier, 500 MB/day).
  • Resource group rg-alz-monitoring (free).
  • Elevated Global Admin access (Access management for Azure resources toggle in Microsoft Entra ID).
  • Local project files (Bicep templates and modules).

Keep everything running

No action needed. Choose this if you're still actively building on your landing zone foundation or plan to extend it soon.

  • Management groups and policies cost nothing, so there is no financial reason to remove them.
  • The Log Analytics workspace is free as long as daily ingestion stays under 500 MB. With no active workloads sending data, you will stay well within this limit.
  • Keep the elevated Global Admin access enabled only if you plan to make more tenant-level deployments soon. Otherwise, consider disabling it for security.

Pause - I'll come back to this later

Since all resources in this project are free, pausing is mostly about reducing your security surface area.

  • Disable the elevated access toggle. Navigate to Microsoft Entra ID > Properties and set Access management for Azure resources to No.
  • Leave management groups, policies, and the Log Analytics workspace in place. They cost nothing and will be ready when you return.
  • Keep your local Bicep project files so you can redeploy or extend them later.

Delete - I don't want to use this again

Remove all project resources and start fresh. Delete resources in reverse order to avoid dependency errors.

  • Remove the policy assignment first:
az policy assignment delete --name <assignment-name> --scope <landing-zones-mg-resource-id>
  • Remove the custom policy definition:
az policy definition delete --name <policy-definition-name> --management-group <mg-id>
  • Delete the resource group containing the Log Analytics workspace:
az group delete --name rg-alz-monitoring --yes
  • Delete the management groups in reverse order (children first, then parent). You must delete child groups before their parent:
az account management-group delete --name <sandbox-mg>
az account management-group delete --name <decommissioned-mg>
az account management-group delete --name <landing-zones-mg>
az account management-group delete --name <platform-mg>
az account management-group delete --name <root-mg>
  • Disable the elevated access toggle. Navigate to Microsoft Entra ID > Properties and set Access management for Azure resources to No.
  • Delete your local project folder if you no longer need the Bicep templates.

Mission accomplished

Nicely Done

You just built the governance foundation that every enterprise Azure environment needs. Your tenant now has a structured management group hierarchy, policy-based governance, and centralized monitoring, all deployed and managed through Bicep infrastructure as code.

Here's what you accomplished:

  • Built a management group hierarchy following the Cloud Adoption Framework reference architecture, with tenant-scoped Bicep deployments.
  • Wrote custom Azure Policy definitions and assignments as Bicep code to enforce governance rules across your landing zones.
  • Mastered cross-scope Bicep deployments by orchestrating resources across tenant, management group, and subscription scopes from a single modular project.
  • Created a policy initiative that bundles multiple governance policies together and assigned it to your Landing Zones management group.

Ready to quiz yourself?