Azure Landing Zone Governance with Bicep
Deploy a governed Azure Landing Zone hierarchy with policy enforcement using Bicep.
Introduction
30 Second Summary
Every growing company hits a point where its cloud resources start sprawling. Without guardrails, teams create resources in the wrong region and compliance issues pile up.
In this project, you will use Bicep to build the governance foundation of an Azure Landing Zone. You will deploy a management group hierarchy with policies that control where resources can be created, then test it by watching Azure block a deployment to a forbidden region.
What You'll Build
You will run a deployment command from VS Code and see your entire governance hierarchy appear in the Azure Portal, ready to block any resource that lands in an unapproved region.
By the end of this project, you'll have:
- A management group hierarchy matching the Azure Landing Zone reference architecture that you can see and navigate in the Azure Portal.
- A custom Azure Policy enforced on your subscription, with a live test where Azure blocks a resource deployed to a non-allowed region.
- A modified hierarchy through code: adding a new Sandbox management group by editing your Bicep template and re-deploying.
- Secret Mission: A tagging enforcement policy and a Decommissioned management group to round out enterprise-grade governance.
Are there any prerequisites?
This project requires an Azure tenant with Global Administrator access and one Azure subscription. You should also have VS Code installed and be familiar with basic Bicep syntax.
Before We Start
Before we dive in, take a moment to lock in what you are about to build and why it matters.
You are going to build the governance foundation of an Azure Landing Zone, entirely through Bicep code. Think about why managing this through code is more powerful than clicking through the Azure Portal.
Set Up Your Bicep Development Environment
Before you can deploy management groups and policies to your Azure tenant, you need the right tools in place.
In this step, you will install the Azure CLI, set up the Bicep extension in VS Code, and elevate your permissions so you can deploy resources at the tenant scope.
In this step, get ready to:
- Install or verify Azure CLI and the Bicep extension for VS Code.
- Log in and elevate access for tenant-scoped deployments.
- Create the project folder and Bicep files.
Install or verify Azure CLI and the Bicep extension
Azure CLI is the command-line tool you will use to deploy Bicep templates to Azure. It needs to be version 2.20.0 or later for Bicep support.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Check whether Azure CLI is installed by running this command:
az version
✔️ I see version 2.20.0 or higher
You are all set. The output includes an azure-cli version number at 2.20.0 or above. Move on to the next instruction.
ⓧ I see an older version
You have Azure CLI installed, but it is too old to support Bicep. Upgrade it by running:
az upgrade
After the upgrade completes, run az version again to confirm the version is 2.20.0 or higher.
ⓧ Command not found
Azure CLI is not installed yet. Follow the instructions for your operating system.
macOS
- Install Azure CLI with Homebrew by running:
brew update && brew install azure-cli
Windows
- Install Azure CLI with WinGet by running this in PowerShell:
winget install --exact --id Microsoft.AzureCLI
Reopen your terminal
After installation completes, close and reopen your terminal window before running any az commands. Azure CLI will not be available until you do.
After installing, run az version to confirm the version is 2.20.0 or higher.
- In VS Code, open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type Bicep in the search bar.
- Find the extension called Bicep by Microsoft and click Install.
What does the Bicep extension do?
The Bicep extension gives you syntax highlighting, auto-completion, and error checking for .bicep files. It makes writing infrastructure-as-code much faster because you get real-time feedback on your templates.
- Confirm the Bicep CLI is available by running:
az bicep version
You should see a Bicep CLI version number printed in your terminal.
Not seeing a Bicep version?
Make sure your Azure CLI is version 2.20.0 or later by running az version. Older versions do not include automatic Bicep support.
If the issue persists, help me troubleshoot my az bicep version command.
Log in and elevate access for tenant-scoped deployments
Deploying management groups requires permissions at the Azure tenant root scope. Being a Global Administrator in Microsoft Entra ID does not automatically grant these permissions. You need to elevate your access first.
- Log in to your Azure tenant by running:
az login
What happens when you run az login?
Azure CLI opens your default browser and asks you to sign in with your Microsoft account. After you authenticate, the CLI stores your credentials so you can run commands against your tenant.
- Confirm you are connected to the correct tenant by running:
az account show
You should see your tenant name and subscription ID in the output. Confirm this is the tenant where you want to deploy your landing zone hierarchy.
- Open the Azure Portal at portal.azure.com.
- In the search bar at the top, type Microsoft Entra ID and select it from the results.
- Under Manage in the left menu, click Properties.
- Scroll down to Access management for Azure resources and set the toggle to Yes.
- Click Save.
Why do I need to toggle this setting?
Being a Global Administrator in Entra ID does not automatically give you Azure RBAC permissions at the tenant root scope. Enabling this toggle grants you the User Access Administrator role at root scope, which lets you assign yourself the Owner role needed for tenant-level Bicep deployments.
Now you need to find your user ID and assign yourself the Owner role at the tenant root scope.
- Get your user object ID by running:
az ad signed-in-user show --query id --output tsv
Copy the ID that is printed. You will paste it into the next command.
- Assign yourself the Owner role at the root scope by running:
az role assignment create --assignee "[[YOUR_USER_ID="your user object ID from the previous command"]]" --scope "/" --role "Owner"
What does this command do?
- --assignee is your user object ID from the previous command.
- --scope "/" targets the tenant root, the highest level in the Azure hierarchy.
- --role "Owner" gives you full control to create and manage management groups at the tenant level.
- Log out and back in to refresh your credentials by running:
az logout
- Log back in by running:
az login
Your refreshed session now has the Owner role at the root scope. You are ready to deploy tenant-level resources.
Getting a permissions error on the role assignment?
Make sure you toggled Access management for Azure resources to Yes and clicked Save in the Azure Portal. Without this toggle, you do not have permission to assign roles at the root scope.
If the issue persists, help me troubleshoot my role assignment at the root scope.
Create the project folder and files
Your Bicep templates need a home. You will create a project folder and two files that will hold all your infrastructure code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir alz-governance
cd alz-governance
- Create the two Bicep files you will use throughout this project by running:
macOS
touch managementGroups.bicep policy.bicep
Windows
New-Item managementGroups.bicep, policy.bicep
- Confirm the files exist by running:
ls
You should see managementGroups.bicep and policy.bicep in the output.
What are these files for?
managementGroups.bicep will define the management group hierarchy for your Azure Landing Zone. policy.bicep will define the Azure Policy that restricts where resources can be deployed.
- Open the alz-governance folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Select the alz-governance folder on your Desktop, then click Open.
You should see both managementGroups.bicep and policy.bicep listed in the VS Code Explorer sidebar.
Don't see the files in VS Code?
Make sure you opened the alz-governance folder itself, not the Desktop. The files should appear at the root of the Explorer panel.
If you still do not see them, help me find my project files in VS Code.
Your development environment is ready. Next up, you will write the Bicep template that creates the entire management group hierarchy in one deployment.
Deploy the Management Group Hierarchy
Your development environment is wired up and ready to go. Now it is time to build the actual skeleton of your Azure Landing Zone.
The management group hierarchy is the foundation that everything else hangs on. Without it, there is nowhere to assign policies, place subscriptions, or enforce governance. In this step, you will write a Bicep template that defines the full hierarchy and deploy it to your Azure tenant.
In this step, get ready to:
- Write the management groups Bicep template defining the full hierarchy.
- Deploy the hierarchy to your Azure tenant.
- Verify the hierarchy in the Azure Portal.
Write the management groups Bicep template
The Azure Landing Zone reference architecture from Microsoft's Cloud Adoption Framework defines a specific management group tree. You will build this tree in Bicep, starting from the top and working down through the Platform and Landing Zones branches.
- Open managementGroups.bicep in VS Code and paste the following code:
targetScope = 'tenant'
@description('Prefix for the management group hierarchy.')
param parTopLevelManagementGroupPrefix string = 'alz'
@description('Display name for the top-level management group.')
param parTopLevelManagementGroupDisplayName string = 'Azure Landing Zones'
// Intermediate Root Management Group
resource mgIntermediateRoot 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: parTopLevelManagementGroupPrefix
properties: {
displayName: parTopLevelManagementGroupDisplayName
}
}
// Platform Management Group
resource mgPlatform 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-platform'
properties: {
displayName: 'Platform'
details: {
parent: {
id: mgIntermediateRoot.id
}
}
}
}
What does this code do?
- targetScope = 'tenant' tells Bicep this template deploys at the tenant level, the highest scope in Azure. Management groups live above subscriptions, so you need tenant scope to create them.
- The two param declarations set a naming prefix (alz) and display name. Every management group name in the hierarchy will use this prefix, keeping names consistent.
- mgIntermediateRoot is the top of your custom hierarchy. It sits below Azure's built-in Tenant Root Group.
- mgPlatform is the first child. The properties.details.parent.id field links it to the intermediate root, creating the parent-child relationship.
- Save managementGroups.bicep.
- Check the editor for red squiggly underlines. The Bicep extension validates your syntax in real time. You should see no errors.
Seeing red underlines?
- Make sure every opening brace has a matching closing brace. Bicep resources use nested braces for properties, details, and parent.
- Confirm the API version string is exactly Microsoft.Management/managementGroups@2024-02-01-preview. A typo in the version causes an unresolvable type error.
- Check that your Bicep extension is installed and active. Open the Problems tab at the bottom of VS Code to see specific error messages. Help me fix a Bicep syntax error in my management group template.
The Platform branch needs three children: Management, Connectivity, and Identity. These map to the core platform services in the Cloud Adoption Framework.
- Add the Management and Connectivity management groups below the code you just wrote in managementGroups.bicep:
// Platform - Management
resource mgManagement 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-management'
properties: {
displayName: 'Management'
details: {
parent: {
id: mgPlatform.id
}
}
}
}
// Platform - Connectivity
resource mgConnectivity 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-connectivity'
properties: {
displayName: 'Connectivity'
details: {
parent: {
id: mgPlatform.id
}
}
}
}
What's happening here?
Both Management and Connectivity set mgPlatform.id as their parent. Bicep resolves these references at deploy time, so the parent-child relationships are created automatically.
- Save managementGroups.bicep and confirm no red underlines appear.
Seeing an error on mgPlatform.id?
Make sure the mgPlatform resource from the first chunk is still in your file above these two resources. Bicep needs the parent resource to be declared before it can reference its id. Help me fix a reference error in my Bicep management group template.
- Add the Identity management group and the Landing Zones branch below the Connectivity resource in managementGroups.bicep:
// Platform - Identity
resource mgIdentity 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-identity'
properties: {
displayName: 'Identity'
details: {
parent: {
id: mgPlatform.id
}
}
}
}
// Landing Zones Management Group
resource mgLandingZones 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-landingzones'
properties: {
displayName: 'Landing Zones'
details: {
parent: {
id: mgIntermediateRoot.id
}
}
}
}
What does this code do?
Identity completes the Platform branch. Landing Zones is a new top-level branch under the intermediate root, sitting alongside Platform. This is where governed workload subscriptions will live.
Notice that mgLandingZones references mgIntermediateRoot.id as its parent, not mgPlatform.id. Platform and Landing Zones are siblings, not nested.
- Save managementGroups.bicep and confirm no red underlines appear.
Red underlines on mgIntermediateRoot.id?
Scroll up and make sure the mgIntermediateRoot resource from your first chunk is still intact. If you accidentally deleted or modified it, re-paste the first code block from above. Help me fix my Bicep file so mgIntermediateRoot is properly referenced.
- Add the Corp and Online children of Landing Zones, plus the output declarations, at the bottom of managementGroups.bicep:
// Landing Zones - Corp
resource mgCorp 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-corp'
properties: {
displayName: 'Corp'
details: {
parent: {
id: mgLandingZones.id
}
}
}
}
// Landing Zones - Online
resource mgOnline 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-online'
properties: {
displayName: 'Online'
details: {
parent: {
id: mgLandingZones.id
}
}
}
}
output outIntermediateRootMgId string = mgIntermediateRoot.id
output outLandingZonesMgId string = mgLandingZones.id
What does this code do?
- Corp and Online are the two Landing Zones children. Corp is for internal corporate workloads. Online is for internet-facing workloads.
- The two output declarations export the intermediate root and Landing Zones IDs. Later templates can reference these outputs to target the right scope.
- Save managementGroups.bicep and confirm no red underlines appear across the entire file.
Seeing errors on the output lines?
Each output references a resource by its symbolic name (mgIntermediateRoot, mgLandingZones). Check that these names match the resource declarations above exactly. Bicep symbolic names are case-sensitive. Help me fix my Bicep output references.
✔️ Awesome, I've got everything!
Great. Make sure you have saved managementGroups.bicep before moving on to the deployment.
ⓧ I'd like to double check the full code
Scroll through your managementGroups.bicep file and confirm these sections appear in order:
- targetScope = 'tenant' at the very top.
- Two param declarations: parTopLevelManagementGroupPrefix and parTopLevelManagementGroupDisplayName.
- Intermediate root: mgIntermediateRoot.
- Platform branch: mgPlatform, mgManagement, mgConnectivity, mgIdentity.
- Landing Zones branch: mgLandingZones, mgCorp, mgOnline.
- Two output lines: outIntermediateRootMgId and outLandingZonesMgId.
Deploy the hierarchy to your Azure tenant
Your Bicep template is ready. Time to send it to Azure and watch the hierarchy come to life.
- Deploy the management group hierarchy by running this command in your VS Code terminal:
az deployment tenant create --name alz-mg-deployment --location eastus --template-file managementGroups.bicep
What does this command do?
- az deployment tenant create tells Azure CLI to deploy at the tenant scope, matching the targetScope = 'tenant' in your Bicep file.
- --name gives the deployment a name so you can track it later.
- --location eastus stores the deployment metadata in the East US region. This does not affect where management groups are created. Management groups are global resources.
The deployment takes about 1-2 minutes as Azure creates each management group in the hierarchy.
- Wait for the command to complete. You should see JSON output that includes "provisioningState": "Succeeded".
Seeing an authorization error?
- Confirm you completed the access elevation in Step 1. The Owner role at the root scope (/) is required for tenant-scoped deployments.
- If you elevated access but still see an error, try logging out and back in to refresh your credentials:
az logout
az login
Then re-run the deployment command. Help me fix an authorization error when deploying management groups at tenant scope.
Verify the hierarchy in the Azure Portal
The deployment succeeded in the terminal. Before you move on, take a moment to predict what the hierarchy looks like.
Based on the Bicep code you wrote, how many top-level branches should you see under the intermediate root? What children should each branch have?
- Open the Azure Portal at portal.azure.com.
- Search for Management groups in the top search bar and select it.
- Click Start using management groups if this is your first time on this page.
You should see your alz intermediate root listed under the Tenant Root Group. Expand it to reveal two branches: Platform and Landing Zones.
- Expand Platform. You should see Management, Connectivity, and Identity as children.
- Expand Landing Zones. You should see Corp and Online as children.
What am I looking at?
This is the exact management group structure recommended by Microsoft's Cloud Adoption Framework for Azure Landing Zones. Platform holds shared infrastructure services. Landing Zones hold workload subscriptions that inherit governance policies.
Everything you see was created from your single Bicep file. If you ever need to recreate this hierarchy in a different tenant, you can run the same template.
Don't see the management groups?
- Refresh the Management groups page in the Azure Portal. Newly created management groups can take a moment to appear.
- If you only see the Tenant Root Group with no children, go back to your terminal and check whether the deployment command printed "provisioningState": "Succeeded". If it shows a different status, re-run the deployment command.
- Help me find my deployed management groups in the Azure Portal.
Your management group hierarchy is live. Next up, you will add governance rules by deploying an Azure Policy that restricts where resources can be created.
Add Policy Governance to the Landing Zone
Your management group hierarchy is deployed and visible in the Azure Portal. That structure gives you a place to organize subscriptions, but on its own it does not enforce any rules.
Management groups without policies are just empty folders. In this step, you will write and deploy a custom Azure Policy that restricts which Azure regions resources can be deployed to. You will assign it to the Landing Zones management group so every subscription placed under it inherits the rule automatically.
In this step, get ready to:
- Write a policy definition and assignment in policy.bicep.
- Deploy the policy to the alz-landingzones management group.
- Check the compliance dashboard and discover why it shows no results yet.
Write the scope and allowed-locations parameter
The policy template targets the management group scope, not the tenant scope you used for managementGroups.bicep. This is because policies are scoped to the management group they govern.
- Switch to the empty policy.bicep file in VS Code and add the target scope and allowed-locations parameter by pasting this code:
// Deploy at the management group level
targetScope = 'managementGroup'
// Define which Azure regions are allowed for resource deployment
@description('An array of allowed Azure regions for resource deployment.')
param parAllowedLocations array = [
'eastus'
'eastus2'
'westus2'
]
What does this code do?
The targetScope = 'managementGroup' line tells Bicep this template will be deployed at a management group, not a subscription or tenant.
The parAllowedLocations parameter is an array of region names. Any resource deployed to a region not in this list will be denied by the policy.
- Save policy.bicep.
- Check the Problems panel at the bottom of VS Code. You should see zero errors or warnings.
Seeing a red underline on targetScope?
Make sure the Bicep extension is installed and active. If you see a squiggly underline, check that the value is exactly 'managementGroup' with single quotes and no extra spaces.
Help me fix this targetScope error
Define the policy rule
A policy definition describes the rule Azure evaluates against every resource deployment. This one checks whether a resource's location is in the allowed list, and denies it if not.
- Below the parameter block in policy.bicep, add the policy definition resource by pasting this code:
// Custom policy that denies resources outside the allowed regions
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2025-03-01' = {
name: 'allowed-locations-policy'
properties: {
policyType: 'Custom'
mode: 'All'
displayName: 'Allowed Locations'
description: 'Restricts resource deployment to the specified Azure regions.'
parameters: {}
policyRule: {
if: {
not: {
field: 'location'
in: parAllowedLocations
}
}
then: {
effect: 'deny'
}
}
}
}
What does this policy rule do?
- policyType: 'Custom' marks this as a policy you wrote, not a built-in Azure policy.
- mode: 'All' means the policy evaluates every resource type, not just those with tags or locations.
- The policyRule block is the logic: if a resource's location is NOT in the parAllowedLocations array, deny the deployment.
- Save policy.bicep.
- Check the Problems panel again. You should still see zero errors.
Seeing errors about unrecognized properties?
Make sure you pasted the code below the closing ] of the parAllowedLocations parameter, not inside it. Each resource block must be at the top level of the file.
Help me debug my policy.bicep errors
Add the policy assignment
A policy definition on its own is just a blueprint. An assignment activates it against a specific scope so Azure starts evaluating resources.
- Below the policyDefinition resource in policy.bicep, add the policy assignment resource by pasting this code:
// Assign the policy so it actively evaluates resources
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2025-03-01' = {
name: 'enforce-allowed-locations'
properties: {
policyDefinitionId: policyDefinition.id
displayName: 'Enforce Allowed Locations'
description: 'Denies resource creation outside of the allowed Azure regions.'
}
}
What does this code do?
The policyAssignment resource links the policy definition to the management group where you deploy this template. The key line is policyDefinitionId: policyDefinition.id, which tells Azure which policy to enforce.
Because this template uses targetScope = 'managementGroup', the assignment automatically applies to whichever management group you specify in the deployment command.
- Save policy.bicep.
- Check the Problems panel one more time. You should see zero errors.
✔️ Awesome, I've got everything!
Great. Make sure you have saved policy.bicep before moving on to the deployment.
ⓧ I'd like to double check the full code
Your policy.bicep should contain three blocks in this order. Scroll up and confirm each one matches the code you pasted:
- The targetScope declaration and parAllowedLocations parameter (the first chunk).
- The policyDefinition resource with the deny rule (the second chunk).
- The policyAssignment resource referencing policyDefinition.id (the third chunk).
Make sure there are no duplicate lines or missing closing braces, then save the file.
Deploy the policy to the Landing Zones management group
- Deploy both the policy definition and its assignment to the alz-landingzones management group by running this command:
az deployment mg create --name alz-policy-deployment --location eastus --management-group-id alz-landingzones --template-file policy.bicep
What does this command do?
- az deployment mg create deploys a Bicep template at the management group scope.
- --management-group-id alz-landingzones targets the Landing Zones management group. This is where both the policy definition and assignment are created.
- --location eastus specifies where Azure stores the deployment metadata. It does not affect the policy's scope.
The deployment takes about 30 seconds. You should see a JSON output with "provisioningState": "Succeeded" in the response.
Seeing an authorization error?
Make sure you are still logged in with elevated access. Run az account show to confirm you are connected to the correct tenant. If your session expired, run az login again.
Help me fix this authorization error
Check the compliance dashboard
The policy is deployed. Before you check the compliance dashboard, take a moment to predict: what do you think the compliance status will show?
- In the Azure Portal, search for Policy in the top search bar and select it.
- Select Assignments in the left sidebar.
- Click the Scope filter at the top of the page.
- Select the alz-landingzones management group, then click Select.
- Confirm the Enforce Allowed Locations assignment appears in the list.
Now check the compliance status.
- Select Compliance in the left sidebar of the Policy page.
- Make sure the scope is still set to alz-landingzones.
The compliance state shows Not started with 0 resources evaluated. The policy is assigned, but it has nothing to evaluate yet.
Why does compliance show Not started?
Azure Policy only evaluates resources inside subscriptions that are placed under the management group. Right now, there are no subscriptions under alz-landingzones or any of its children (Corp, Online).
The policy is ready and waiting. It just needs a subscription to govern. You will close this gap in the next step.
Your governance rule is in place, but it has no subscriptions to enforce. Next up, you will place your subscription under the Corp management group and watch the policy deny a resource deployment in real time.
Place Your Subscription and See the Policy in Action
You deployed an allowed-locations policy to the Landing Zones management group, but the compliance dashboard showed "Not started" with zero resources evaluated.
That gap exists because policies assigned to a management group only evaluate resources in subscriptions placed under it. Right now, no subscriptions live under Landing Zones. In this step, you will move your subscription into the hierarchy and prove the policy works by watching Azure deny a resource in a forbidden region.
In this step, get ready to:
- Place your subscription under the Corp management group so it inherits the allowed-locations policy.
- Test the policy by creating resource groups in both allowed and denied regions.
Place your subscription under the Corp management group
Your subscription currently sits under the Tenant Root Group, outside the management group hierarchy you built. Moving it under alz-corp places it inside Landing Zones, where your allowed-locations policy is assigned.
- First, grab your subscription ID by running this command in the VS Code terminal:
az account show --query id --output tsv
The terminal prints a string of letters and numbers. This is your subscription ID. Copy it.
- Move your subscription under the Corp management group by running this command:
az account management-group subscription add --name alz-corp --subscription [[SUB_ID="your subscription ID"]]
What does this command do?
This command moves your subscription under the alz-corp management group. Corp is a child of Landing Zones, so your subscription now inherits the allowed-locations policy you deployed in the previous step.
Moving a subscription only changes which Azure Policy assignments it inherits. It does not affect billing, resource access, or existing resources. You can move it back at any time.
- Verify the placement by opening the Azure Portal.
- Search for Management groups in the top search bar and select it.
- Expand alz, then Landing Zones, then Corp. You should see your subscription listed under Corp.
Don't see your subscription under Corp?
- Double-check that the subscription ID you pasted matches the one from az account show --query id --output tsv.
- Try refreshing the Management groups page in the portal. The move can take a moment to appear.
- Make sure you have Owner permissions at the root scope. If the command returned a permission error, revisit the role assignment from Step 1.
Still stuck? Help me troubleshoot why my subscription isn't appearing under the Corp management group.
Test the policy enforcement
Your subscription now sits inside the Landing Zones hierarchy, which means the allowed-locations policy is actively evaluating every resource you create. Time to prove it works.
Before you run the next command, think about this: your policy allows eastus, eastus2, and westus2. What do you think will happen if you try to create a resource group in northeurope?
- Attempt to create a resource group in a non-allowed region by running this command:
az group create --name test-policy-rg --location northeurope
Azure denies the request with a policy violation error. The northeurope region is not in your allowed list, so the deny effect blocks the deployment before it reaches the resource provider. Your governance is working.
Why was the request denied?
The deny effect in Azure Policy is evaluated at request time. When you ask Azure to create a resource, Policy checks the request against all applicable assignments before forwarding it to the resource provider.
Your policy checks whether the location field is in the allowed list. Since northeurope is not in ['eastus', 'eastus2', 'westus2'], Azure returns a 403 Forbidden and the resource group is never created.
- Now confirm that an allowed region works by running this command:
az group create --name test-policy-rg --location eastus
This time the command succeeds. Azure creates the resource group because eastus is in your allowed locations list.
- Clean up the test resource group by running this command:
az group delete --name test-policy-rg --yes --no-wait
What do these flags do?
- --yes skips the confirmation prompt so you do not have to type y.
- --no-wait starts the deletion in the background so your terminal is immediately available for the next command.
Not seeing a policy denial for northeurope?
- Policy evaluation can take a few minutes to propagate after moving your subscription. Wait 2-3 minutes, then retry the az group create command.
- Confirm your subscription is under alz-corp in the Azure Portal. If the subscription never moved, the policy does not apply.
- Verify the policy assignment exists by navigating to Policy > Assignments in the Azure Portal, then setting the scope to the alz-landingzones management group. You should see the Enforce Allowed Locations assignment listed.
Still stuck? Help me figure out why my allowed-locations policy is not denying resources in northeurope.
Your Landing Zone is alive. The management group hierarchy now has a real subscription under it, and the policy is actively blocking resources in unapproved regions. Next up, you will modify the hierarchy by adding a Sandbox management group through code, proving the full IaC lifecycle.
Modify the Hierarchy Through IaC
Your subscription is under Corp, and the allowed-locations policy is actively blocking resources in non-allowed regions. Your Azure Landing Zone governance is working.
The real value of managing your Landing Zone as code is that every change is tracked, reviewable, and repeatable. In this step, you will add a Sandbox management group by editing your Bicep template and re-deploying. This is the same IaC lifecycle that platform teams use daily.
In this step, get ready to:
- Add a Sandbox management group to your managementGroups.bicep template.
- Preview the infrastructure change with what-if before deploying.
- Deploy the updated hierarchy and verify it in the Azure Portal.
Add a Sandbox management group to the Bicep template
The Azure Landing Zone reference architecture includes a Sandbox management group for experimentation. Teams place subscriptions there for testing, isolated from the governed Landing Zones.
- Switch back to managementGroups.bicep in VS Code.
- Find the mgOnline resource block. It is the last management group resource before the two output lines at the bottom of the file.
- Add the following Sandbox management group resource after the closing } of mgOnline and before the output lines:
// Sandbox Management Group (added in Step 5)
resource mgSandbox 'Microsoft.Management/managementGroups@2024-02-01-preview' = {
name: '${parTopLevelManagementGroupPrefix}-sandbox'
properties: {
displayName: 'Sandbox'
details: {
parent: {
id: mgIntermediateRoot.id
}
}
}
}
What does this code do?
- mgSandbox creates a new management group named alz-sandbox with the display name Sandbox.
- parent: { id: mgIntermediateRoot.id } places it directly under the intermediate root, at the same level as Platform and Landing Zones.
- Sandbox management groups are where teams place subscriptions used for experimentation, kept separate from governed production workloads.
- Save managementGroups.bicep.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Your managementGroups.bicep file should now contain these sections in order:
- targetScope = 'tenant' at the top.
- Two parameters: parTopLevelManagementGroupPrefix and parTopLevelManagementGroupDisplayName.
- Eight original management group resources: mgIntermediateRoot, mgPlatform, mgManagement, mgConnectivity, mgIdentity, mgLandingZones, mgCorp, mgOnline.
- The new mgSandbox resource you just added, with its parent set to mgIntermediateRoot.id.
- Two outputs: outIntermediateRootMgId and outLandingZonesMgId.
Preview the change with what-if
Before deploying, you can preview exactly what Azure will create, modify, or delete using the --what-if flag. This is the IaC equivalent of a code review for infrastructure.
Before you run this, what do you think the output will show? Will Azure modify any of your existing management groups, or just add the new one?
- Preview the change by running this command:
az deployment tenant create --what-if --location eastus --template-file managementGroups.bicep
The output shows exactly one new resource to be created (the Sandbox management group) and no changes to existing resources. Every existing management group shows as NoChange.
Why use what-if?
Bicep deployments are declarative. You describe the desired end state, and Azure figures out what to create, update, or leave alone. The --what-if flag shows you that plan before anything changes.
In production, platform teams run what-if in CI/CD pipelines so reviewers can approve infrastructure changes before they go live. It catches accidental deletions, scope changes, and drift.
Seeing an error instead of the what-if output?
- Make sure you saved managementGroups.bicep before running the command. Unsaved changes are not picked up by the Bicep CLI.
- Check that your terminal is in the alz-governance folder. Run ls and confirm you see managementGroups.bicep listed.
- If you see a syntax error, scroll up to the mgSandbox resource and confirm every brace is matched and the parent.id references mgIntermediateRoot.id.
Still stuck? Ask for help.
Deploy and verify the updated hierarchy
- Deploy the updated template by running this command:
az deployment tenant create --name alz-mg-update --location eastus --template-file managementGroups.bicep
Why does this command work without deleting anything?
Bicep deployments are idempotent. Azure compares the template to the current state and only creates what is missing. Your eight existing management groups are left untouched because they already match the template.
The deployment takes about a minute. Once it finishes, it is time to verify.
Before you check the Azure Portal, where in the hierarchy do you expect the Sandbox management group to appear?
- In the Azure Portal, search for and open Management groups.
- Expand the Azure Landing Zones intermediate root.
You should see three children under the intermediate root: Platform, Landing Zones, and the new Sandbox. All existing management groups and their children are unchanged.
Don't see the Sandbox management group?
- Refresh the Management groups page in the Azure Portal. It can take a moment for new groups to appear.
- Check the deployment output in your terminal. If it shows Succeeded, the management group was created. Try refreshing the Portal again.
- If the deployment failed, scroll through the terminal output for an error message. A common cause is a typo in mgIntermediateRoot.id for the parent reference.
Still not seeing it? Ask for help.
Secret mission
Add a Tagging Enforcement Policy
Enterprise teams enforce tagging standards alongside location restrictions to track costs and ownership. Can you write a second Azure Policy that denies any resource group missing a CostCenter tag, deploy it to your Landing Zones, and prove it works? Then round out the management group hierarchy with a Decommissioned group.
Clean Up Your Resources
Clean Up Your Resources
Management groups, policy definitions, policy assignments, and role assignments are all free Azure resources with no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Management group hierarchy (alz, alz-platform, alz-management, alz-connectivity, alz-identity, alz-landingzones, alz-corp, alz-online, alz-sandbox, alz-decommissioned).
- Azure Policy definitions and assignments on the alz-landingzones management group.
- Owner role assignment at the tenant root scope (/).
- Elevated access setting in Microsoft Entra ID.
- Subscription placed under alz-corp management group.
- Local alz-governance project folder (managementGroups.bicep, policy.bicep, taggingPolicy.bicep).
Keep everything running
No action needed. Management groups and policies cost nothing, so you can leave them in place as the foundation for future Azure Landing Zone projects.
- Your subscription stays under alz-corp with governance policies actively enforcing compliance.
- The management group hierarchy, policy definitions, and policy assignments remain in place and ready to build on.
Pause - I'll come back to this later
Move your subscription back to the Tenant Root Group to stop active policy enforcement. The management groups and policies stay in place for when you return.
- Remove your subscription from the alz-corp management group by running:
az account management-group subscription remove --name alz-corp --subscription [[SUB_ID="your subscription ID"]]
- Your subscription returns to the Tenant Root Group. No policies from the Landing Zone hierarchy apply to it anymore.
- When you are ready to resume, move the subscription back with az account management-group subscription add.
Delete - I don't want to use this again
Remove all project resources and revert your tenant to its original state.
Move your subscription back
- Remove your subscription from alz-corp by running:
az account management-group subscription remove --name alz-corp --subscription [[SUB_ID="your subscription ID"]]
Delete policy assignments and definitions
Remove the policy assignments first, then the policy definitions. Assignments must be deleted before their parent definitions.
- Delete the allowed-locations policy assignment by running:
az policy assignment delete --name enforce-allowed-locations --scope "/providers/Microsoft.Management/managementGroups/alz-landingzones"
- If you completed the Secret Mission, delete the tagging policy assignment by running:
az policy assignment delete --name enforce-costcenter-tag --scope "/providers/Microsoft.Management/managementGroups/alz-landingzones"
- Delete the allowed-locations policy definition by running:
az policy definition delete --name allowed-locations-policy --management-group alz-landingzones
- If you completed the Secret Mission, delete the tagging policy definition by running:
az policy definition delete --name require-costcenter-tag --management-group alz-landingzones
Delete the management group hierarchy
Management groups must be empty before they can be deleted. Delete children first, then their parents.
- Delete the Platform branch by running:
az account management-group delete --name alz-management
az account management-group delete --name alz-connectivity
az account management-group delete --name alz-identity
az account management-group delete --name alz-platform
- Delete the Landing Zones branch by running:
az account management-group delete --name alz-corp
az account management-group delete --name alz-online
az account management-group delete --name alz-landingzones
- Delete the remaining top-level groups and the intermediate root by running:
az account management-group delete --name alz-sandbox
# Skip the next line if you did not complete the Secret Mission
az account management-group delete --name alz-decommissioned
az account management-group delete --name alz
Remove elevated access
- Remove the Owner role assignment at the tenant root scope by running:
az role assignment delete --assignee [[USER_ID="your user principal name or object ID"]] --scope "/" --role "Owner"
- In the Azure Portal, open Microsoft Entra ID.
- Select Properties.
- Toggle Access management for Azure resources back to No.
- Click Save.
Delete the local project folder
- Delete the alz-governance folder. On macOS or Linux, run:
rm -rf ~/Desktop/alz-governance
- On Windows, run this in Command Prompt instead:
rmdir /s /q %USERPROFILE%\Desktop\alz-governance
Nice Work!
Nice Work!
Nice work! You just built and deployed the governance foundation of an Azure Landing Zone, entirely from Bicep code in VS Code. Your tenant now has a production-aligned management group hierarchy with live policy enforcement that denies non-compliant resources in real time.
You've learned how to:
- Deploy a management group hierarchy matching the Cloud Adoption Framework reference architecture to your Azure tenant using tenant-scoped Bicep deployments.
- Write and deploy a custom Azure Policy that restricts resource locations, place your subscription under the governed hierarchy, and prove enforcement by watching Azure deny a resource in a non-allowed region.
- Modify live infrastructure through the IaC lifecycle: editing your Bicep template to add a Sandbox management group, previewing the change with what-if, and re-deploying with zero impact on existing resources.
- Secret Mission: Add a tagging enforcement policy that denies resource groups missing a CostCenter tag, and add a Decommissioned management group to round out enterprise-grade governance.
Ready to quiz yourself?