Deploy an Azure Landing Zone with Terraform

Deploy an enterprise Azure Landing Zone management group hierarchy using Terraform.

Introduction

30 Second Summary

Every large organization running on Azure needs a way to keep hundreds of teams, subscriptions, and workloads organized and secure without manually policing each one.

In this project, you will use Terraform to deploy the foundational governance skeleton of an Azure Landing Zone, including a management group hierarchy and policy guardrails that enforce rules across your entire cloud environment.

What You'll Build

You will build the same management group hierarchy and Azure Policy structure that enterprise organizations use to govern their Azure environments at scale, defined entirely as Infrastructure as Code.

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

  • A Terraform project with provider configuration, variables, and resource definitions.
  • The complete ALZ management group hierarchy with 10+ management groups following Microsoft's reference architecture.
  • Azure Policy assignments enforcing governance guardrails for allowed regions and required tags.
  • A verified deployment visible in the Azure Portal, plus a secret mission to add centralized monitoring with a Log Analytics workspace.

Are there any prerequisites?

You need an active Azure account with a subscription and sufficient permissions to create management groups (by default, any Azure user can do this). You should also have basic familiarity with the command line and have VS Code and Azure CLI already installed.

Install Terraform and Set Up Your Project

Before you can define your Azure Landing Zone as code, you need the tool that turns code into cloud infrastructure.

Terraform is an Infrastructure as Code tool that lets you describe cloud resources in configuration files, then creates those resources for you.

In this step, you'll install Terraform, connect it to your Azure account, and create the project folder structure that will hold your landing zone configuration.

In This Step, Get Ready To:
  • Install the Terraform CLI on your machine.
  • Authenticate with Azure and confirm your subscription context.
  • Create the project directory and Terraform file structure.
Install Terraform

Terraform is a command-line tool. You install it once on your machine, and then use it from your terminal to manage cloud resources.

macOS

On macOS, you'll use Homebrew to install Terraform from HashiCorp's official tap.

  • Open your terminal and run the following commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Do Not Use the Default Homebrew Formula

You might see guides online that say to run brew install terraform. That formula from the default Homebrew tap is stuck at version 1.5.7 due to licensing changes. The HashiCorp tap (hashicorp/tap) provides the latest version.

  • Verify the installation:
terraform -version

I see a version number

You should see output like Terraform v1.14.x. The exact patch version may differ, but as long as you see v1.14 or higher, you're good to go.

Command not found

If you see command not found: terraform, try the following:

  • Make sure Homebrew itself is installed by running brew --version.
  • Re-run brew tap hashicorp/tap and then brew install hashicorp/tap/terraform.
  • Open a new terminal window and try terraform -version again.

Still stuck? Help me troubleshoot my Terraform installation on macOS.

Windows

On Windows, the easiest way to install Terraform is using Chocolatey or by downloading the binary directly.

  • If you have Chocolatey installed, open PowerShell as Administrator and run:
choco install terraform

If you don't have Chocolatey, download the Terraform binary from the official Terraform install page and add it to your system PATH.

  • Verify the installation:
terraform -version

I see a version number

You should see output like Terraform v1.14.x. The exact patch version may differ, but as long as you see v1.14 or higher, you're good to go.

I see an error

If PowerShell doesn't recognize the terraform command, Terraform may not be on your system PATH.

  • Close and reopen PowerShell, then try again.
  • If you installed manually, make sure the folder containing terraform.exe is added to your system PATH environment variable.

Still stuck? Help me troubleshoot my Terraform installation on Windows.

What Is Terraform?

Terraform reads configuration files written in HCL (HashiCorp Configuration Language) and translates them into API calls to cloud providers like Azure, AWS, or GCP.

Instead of clicking through the Azure Portal to create resources, you describe what you want in a file and Terraform builds it for you. This means your infrastructure is version-controlled, repeatable, and reviewable, just like application code.

Log In to Azure CLI

Terraform doesn't have its own login system for Azure. Instead, it piggybacks on your existing Azure CLI session. When you run az login, Terraform can use that authenticated session to create resources on your behalf.

  • Run the following command to log in to Azure:
az login

A browser window opens for you to sign in with your Azure credentials. After signing in, your terminal displays a list of your subscriptions.

  • Run the following command to confirm which subscription and tenant you're connected to:
az account show

What Should I See?

The output shows your subscription name, subscription ID, and tenant ID. Make a note of the id field (your subscription ID). You'll need it in the next step when configuring the Terraform provider.

You can also grab just the subscription ID on its own with this command:

az account show --query id -o tsv

What Does This Command Do?

The --query id flag filters the output to only the id field. The -o tsv flag outputs the value as plain text without quotes, making it easy to copy.

Create the Project Directory and File Structure

Terraform projects are organized into .tf files inside a single directory. When you run Terraform commands, it reads every .tf file in the current directory and combines them into one configuration. Splitting your code across multiple files keeps things organized as the project grows.

macOS

  • Run the following commands in your terminal to create the project folder and files:
mkdir azure-landing-zone
cd azure-landing-zone
touch main.tf variables.tf outputs.tf

Windows

  • Run the following commands in PowerShell to create the project folder and files:
mkdir azure-landing-zone
cd azure-landing-zone
New-Item main.tf, variables.tf, outputs.tf

What Are These Files For?

  • main.tf holds the provider configuration and resource definitions. This is where you'll describe the Azure resources you want to create.
  • variables.tf holds input variables that make your configuration reusable. Instead of hardcoding values, you define them as variables.
  • outputs.tf holds output values that Terraform displays after a deployment. These are useful for grabbing resource IDs or URLs.
  • Open the project folder in VS Code:
code .

VS Code opens with your three empty .tf files in the sidebar. You'll start filling these in during the next step.

Pro tip: install the Terraform extension

Search for "HashiCorp Terraform" in the VS Code Extensions panel. This extension gives you syntax highlighting, auto-completion, and formatting for .tf files.

Your tools are installed and your project structure is ready. Next up, you'll write your first Terraform code to configure the Azure provider and define the variables for your landing zone.

Configure the Terraform Azure Provider

Your project directory is ready with three empty .tf files. Before you can create any Azure resources, Terraform needs to know which cloud platform you are targeting and how to authenticate.

In this step, you will configure the Azure provider, define reusable input variables, and initialize your project so Terraform can communicate with Azure.

In This Step, Get Ready To:
  • Write the provider configuration that connects Terraform to your Azure subscription.
  • Define input variables to make your landing zone configuration reusable.
  • Initialize the project and verify that Terraform can reach Azure.
Write the Provider Configuration in main.tf

A Terraform provider is a plugin that lets Terraform interact with a specific cloud platform or service. For Azure, the provider is called azurerm. You need two blocks to set it up: a terraform block that pins the provider version, and a provider block that configures authentication.

  • Open main.tf in VS Code.
  • Add the following configuration:
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  subscription_id = "[[SUBSCRIPTION_ID=\"your Azure subscription ID\"]]"
}

What Does This Configuration Do?

  • The terraform block tells Terraform to download the azurerm provider from HashiCorp's registry. The ~> 4.0 constraint allows any 4.x version but prevents jumping to a future major version that could have breaking changes.
  • The provider block configures how Terraform authenticates to Azure. The features {} block is required by the azurerm provider (even when empty).
  • Terraform uses your existing Azure CLI session from az login to authenticate. The subscription_id tells Terraform which subscription to target.

Pro tip: find your subscription ID

If you did not note your subscription ID from Step 1, run this command to retrieve it:

az account show --query id -o tsv
Define Input Variables in variables.tf

Hard-coding values like region names and ID prefixes directly into resource definitions makes your configuration rigid. Terraform variables let you define values in one place and reference them throughout your project. If you ever need to change the deployment region or naming convention, you update a single variable instead of hunting through every file.

  • Open variables.tf and add the following variable definitions:
variable "location" {
  description = "The Azure region for resource deployments"
  type        = string
  default     = "eastus"
}

variable "root_name" {
  description = "Display name prefix for management groups"
  type        = string
  default     = "ALZ"
}

variable "root_id" {
  description = "Short ID prefix for management group IDs"
  type        = string
  default     = "alz"
}

How Do Terraform Variables Work?

Each variable block declares an input that your configuration can reference using var.variable_name syntax. The default value is used automatically unless you override it at runtime.

For example, later you will reference var.root_id to build management group names like alz-platform and alz-landingzones. Changing the root_id default would rename all of them at once.

Initialize the Project and Verify the Configuration

With the provider and variables defined, you can now initialize your Terraform project. The terraform init command downloads the provider plugin you specified and prepares your working directory.

  • Open your terminal in the azure-landing-zone directory.
  • Run the initialization command:
terraform init

What Should I See?

You should see output that says "Terraform has been successfully initialized!" along with messages about downloading the hashicorp/azurerm provider.

Two new items appear in your project directory after initialization.

  • The .terraform directory stores the downloaded provider plugin binaries. You do not edit anything in this folder.
  • The .terraform.lock.hcl file records the exact provider version and checksums that were installed. This ensures everyone on a team uses the same provider version.

Now verify that Terraform can connect to Azure and that your configuration is valid.

  • Run the plan command:
terraform plan

What Does Terraform Plan Do?

The terraform plan command compares your configuration files against the current state of your infrastructure and shows you exactly what changes Terraform would make. It is a dry run that does not modify anything.

Since you have not defined any resources yet, the output should say "No changes. Your infrastructure matches the configuration." This confirms that your provider is configured correctly and Terraform can reach Azure.

I see "No changes"

Your provider configuration is working. Terraform successfully authenticated to Azure using your CLI session and confirmed there is nothing to create yet.

I see an error

Having Trouble?

Here are the most common issues:

  • "Error: No valid credential sources found" means your Azure CLI session has expired. Run az login again to re-authenticate.
  • "Error: subscription_id is required" means you did not replace the placeholder in your main.tf provider block. Run az account show --query id -o tsv to get your subscription ID and paste it into the subscription_id field.
  • Syntax errors in HCL usually point to a missing quote, brace, or bracket. Double-check that your main.tf matches the code block above exactly.

If you are still stuck, help me debug this Terraform init or plan error.

Your Terraform project is now wired up to Azure and ready to create real infrastructure. Next up, you will define the full Azure Landing Zone management group hierarchy and deploy it with a single command.

Build the Management Group Hierarchy

Your Terraform project is initialized and connected to Azure. Now it is time to define the actual infrastructure. In this step, you will create the complete Azure Landing Zone management group hierarchy, which is the organizational backbone that enterprises use to govern workloads at scale.

Management groups sit above subscriptions in Azure's resource hierarchy. They let you apply governance policies to many subscriptions at once instead of configuring each one individually. The ALZ reference architecture defines a specific tree of management groups, each with a clear purpose.

In This Step, Get Ready To:
  • Define the root, Platform, and Landing Zones management groups as Terraform resources.
  • Add all remaining management groups to complete the ALZ reference architecture hierarchy.
  • Deploy the full hierarchy to Azure using terraform apply and verify the results.
Create the Top-Level Management Groups

Every Azure tenant has a built-in Tenant Root Group at the very top. The ALZ architecture does not assign policies directly to this root. Instead, it creates an intermediate root management group one level below it. This intermediate root is where organization-wide policies live. Below that, two major branches form the tree: Platform (for shared infrastructure services) and Landing Zones (for workload subscriptions).

  • Open main.tf in VS Code.
  • Add the following azurerm_management_group resources below your existing provider configuration.
# Intermediate root management group - sits below the Tenant Root Group
resource "azurerm_management_group" "root" {
  name         = "${var.root_id}-root"
  display_name = "${var.root_name}"
}

# Platform branch - for shared infrastructure services
resource "azurerm_management_group" "platform" {
  name                       = "${var.root_id}-platform"
  display_name               = "${var.root_name} - Platform"
  parent_management_group_id = azurerm_management_group.root.id
}

# Landing Zones branch - for workload subscriptions
resource "azurerm_management_group" "landing_zones" {
  name                       = "${var.root_id}-landing-zones"
  display_name               = "${var.root_name} - Landing Zones"
  parent_management_group_id = azurerm_management_group.root.id
}

How Does the Parent-Child Relationship Work?

The parent_management_group_id attribute is what builds the tree structure. When you set it to azurerm_management_group.root.id, Terraform creates a reference between the child and parent groups. The intermediate root group has no parent_management_group_id set, so Azure automatically places it under the Tenant Root Group.

Terraform also uses these references to figure out the correct creation order. It knows it must create the root group before the Platform or Landing Zones groups.

What is a Management Group?

A management group is an Azure container that sits above subscriptions. It lets you organize subscriptions into a hierarchy and apply governance controls (like policies and access permissions) to many subscriptions at once. Any policy assigned to a management group is automatically inherited by all its child groups and subscriptions.

Complete the Hierarchy With the Remaining Groups

The ALZ reference architecture has several more management groups that organize workloads by purpose. Under Platform, there are three specialized groups. Under Landing Zones, there are two workload archetypes. Two more groups sit directly under the intermediate root for special purposes.

  • Add the following resources to main.tf, below the three management groups you already created.
# --- Platform children ---

# Management - for Log Analytics, Automation, monitoring tools
resource "azurerm_management_group" "management" {
  name                       = "${var.root_id}-management"
  display_name               = "${var.root_name} - Management"
  parent_management_group_id = azurerm_management_group.platform.id
}

# Connectivity - for hub networking, DNS, ExpressRoute
resource "azurerm_management_group" "connectivity" {
  name                       = "${var.root_id}-connectivity"
  display_name               = "${var.root_name} - Connectivity"
  parent_management_group_id = azurerm_management_group.platform.id
}

# Identity - for Active Directory, Entra Domain Services
resource "azurerm_management_group" "identity" {
  name                       = "${var.root_id}-identity"
  display_name               = "${var.root_name} - Identity"
  parent_management_group_id = azurerm_management_group.platform.id
}

# --- Landing Zones children ---

# Corp - for workloads requiring corporate network connectivity
resource "azurerm_management_group" "corp" {
  name                       = "${var.root_id}-corp"
  display_name               = "${var.root_name} - Corp"
  parent_management_group_id = azurerm_management_group.landing_zones.id
}

# Online - for internet-facing workloads
resource "azurerm_management_group" "online" {
  name                       = "${var.root_id}-online"
  display_name               = "${var.root_name} - Online"
  parent_management_group_id = azurerm_management_group.landing_zones.id
}

# --- Root-level children ---

# Sandbox - for testing and exploration, isolated from production
resource "azurerm_management_group" "sandbox" {
  name                       = "${var.root_id}-sandbox"
  display_name               = "${var.root_name} - Sandbox"
  parent_management_group_id = azurerm_management_group.root.id
}

# Decommissioned - for retired subscriptions pending deletion
resource "azurerm_management_group" "decommissioned" {
  name                       = "${var.root_id}-decommissioned"
  display_name               = "${var.root_name} - Decommissioned"
  parent_management_group_id = azurerm_management_group.root.id
}

What is Each Management Group For?

Each group in the ALZ hierarchy has a specific purpose:

  • Management holds monitoring and automation resources like Log Analytics.
  • Connectivity holds hub networking, DNS, and ExpressRoute configurations.
  • Identity holds Active Directory and domain services.
  • Corp is for workloads that need corporate network connectivity (e.g., internal line-of-business apps).
  • Online is for internet-facing workloads (e.g., public websites, APIs).
  • Sandbox is an isolated space for testing and exploration, kept separate from production.
  • Decommissioned holds retired subscriptions that are pending deletion.
Deploy the Hierarchy to Azure

You now have 10 management group resources defined in your Terraform configuration. Time to preview what Terraform will create and then deploy it.

  • Run terraform plan to preview the changes.
terraform plan

What Should I See?

The plan output shows every resource Terraform will create. You should see Plan: 10 to add, 0 to change, 0 to destroy at the bottom. Each management group appears with a + symbol indicating it will be created. Review the names and display names to confirm they match what you expect.

  • Run terraform apply to deploy the management groups.
terraform apply
  • Type yes when prompted to confirm the deployment.

Terraform creates each management group in the correct order, starting with the intermediate root and working down the tree. This takes about 30-60 seconds.

Getting a Permissions Error?

By default, any user in an Azure tenant can create management groups. However, if your tenant admin has restricted this setting, you will need Owner or Contributor permissions at the tenant root group level. You could also use the Management Group Contributor role. Check with your admin or help me troubleshoot my management group permissions.

Now add output values so you can easily reference the management group IDs later.

  • Open outputs.tf and add the following output definitions.
output "root_management_group_id" {
  description = "The ID of the intermediate root management group"
  value       = azurerm_management_group.root.id
}

output "platform_management_group_id" {
  description = "The ID of the Platform management group"
  value       = azurerm_management_group.platform.id
}

output "landing_zones_management_group_id" {
  description = "The ID of the Landing Zones management group"
  value       = azurerm_management_group.landing_zones.id
}

What Are Terraform Outputs?

Outputs are values that Terraform displays after a successful apply. They are useful for surfacing important information like resource IDs that you might need in other configurations or scripts. You can also view them anytime by running terraform output.

  • Run terraform apply again to register the outputs.
terraform apply
  • Type yes when prompted.

This time the plan shows no new resources to create (since the management groups already exist), but it registers the new outputs. You should see the three management group IDs printed at the end.

Your entire ALZ management group hierarchy is now live in Azure. Next up, you will assign Azure Policy guardrails to enforce governance rules across this hierarchy.

Assign Azure Policy Governance Guardrails

Your management group hierarchy gives you the organizational structure. Now you need the rules that actually enforce governance across it. Azure Policy lets you define guardrails that automatically apply to every subscription and resource within a management group scope. In this step, you will assign two built-in policies: one to restrict which Azure regions resources can be deployed to, and another to require tags on resource groups.

In This Step, Get Ready To:
  • Assign an "Allowed locations" policy to restrict resource deployment regions across the entire landing zone.
  • Assign a "Require a tag on resource groups" policy to enforce tagging standards on workload subscriptions.
  • Deploy and verify both policy assignments using the Terraform plan/apply workflow.
Assign the Allowed Locations Policy

One of the most common governance controls in enterprise Azure environments is restricting which regions resources can be deployed to. This prevents teams from accidentally spinning up resources in regions that violate data residency requirements or are outside your organization's approved footprint.

Azure has a built-in policy definition called "Allowed locations" that does exactly this. Instead of writing custom policy JSON, you can reference this built-in definition by its ID and configure the list of approved regions as a parameter.

  • Open main.tf and add the following policy assignment resource after your management group resources.
resource "azurerm_management_group_policy_assignment" "allowed_locations" {
  name                 = "Allowed-Locations"
  management_group_id  = azurerm_management_group.root.id
  policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c"
  enforce              = true

  parameters = jsonencode({
    listOfAllowedLocations = {
      value = ["eastus", "westus2"]
    }
  })
}

What does this resource do?

This creates an azurerm_management_group_policy_assignment resource that attaches the built-in "Allowed locations" policy to your intermediate root management group. Every subscription and resource underneath this management group will inherit this restriction.

  • management_group_id scopes the policy to your intermediate root, so it applies to the entire landing zone hierarchy.
  • policy_definition_id references the built-in Azure Policy by its unique ID. You do not need to write any policy JSON yourself.
  • parameters passes the list of approved regions as a JSON-encoded value. You can adjust this list to match your organization's requirements.

What is Azure Policy?

Azure Policy is Azure's built-in governance engine. It evaluates resources against rules you define and can deny non-compliant deployments, audit existing resources, or automatically remediate drift. Policy definitions and assignments are free Azure resources.

Assign the Require Tag Policy to Landing Zones

Tagging is how enterprise teams track cost ownership, environment type, and compliance scope across hundreds of resource groups. Without enforced tagging, teams forget to tag resources and cost attribution becomes impossible.

This second policy assignment targets the Landing Zones management group specifically, not the root. This demonstrates how policy inheritance works. Policies assigned at a parent management group are inherited by all its children. The Landing Zones group contains Corp and Online, so both workload archetypes will require the tag.

  • Add the following resource to main.tf below the allowed locations assignment.
resource "azurerm_management_group_policy_assignment" "require_tag_rg" {
  name                 = "Require-Env-Tag-RG"
  management_group_id  = azurerm_management_group.landing_zones.id
  policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/96670d01-0a4d-4649-9c89-2d3abc0a5025"
  enforce              = true

  parameters = jsonencode({
    tagName = {
      value = "Environment"
    }
  })
}

What does this resource do?

This assigns the built-in "Require a tag on resource groups" policy to the Landing Zones management group. Any resource group created under a subscription in the Landing Zones scope (Corp or Online) must include an Environment tag. If someone tries to create a resource group without it, Azure will deny the deployment.

Pro tip: scoping policies strategically

Notice that the region restriction is assigned at the root (applies everywhere), but the tagging requirement is assigned only to Landing Zones. In a real enterprise, you would keep broad security policies at the root and apply workload-specific policies at lower levels. The Sandbox management group intentionally has fewer restrictions so teams can experiment freely.

Deploy and Verify the Policy Assignments
  • Run terraform plan to preview the two new policy assignment resources.
terraform plan

The plan output should show 2 resources to add. You should see both azurerm_management_group_policy_assignment.allowed_locations and azurerm_management_group_policy_assignment.require_tag_rg listed with a + symbol indicating they will be created.

  • Run terraform apply and type yes when prompted to deploy the policy assignments.
terraform apply

What should I see?

Terraform should output Apply complete! Resources: 2 added, 0 changed, 0 destroyed. confirming both policy assignments were created successfully.

Now verify the assignments in the Azure Portal.

  • Open the Azure Portal and search for Policy in the top search bar.
  • Click Assignments in the left sidebar.
  • Change the scope to your intermediate root management group to see the Allowed-Locations assignment.
  • Change the scope to the Landing Zones management group to see the Require-Env-Tag-RG assignment.

Why is the compliance state showing as "Not started"?

Policy compliance evaluation can take up to 30 minutes after a new assignment is created. The compliance state will initially show as "Not started" and will update once Azure's policy engine completes its first evaluation cycle. This is normal behavior.

Your landing zone now has real governance teeth. Any resource deployed under the intermediate root must be in an approved region, and any resource group created in a Landing Zones subscription must carry an Environment tag. Next up, you will explore the full deployment in the Azure Portal and learn how Terraform tracks all of these resources in its state file.

Verify and Explore Your Landing Zone in the Azure Portal

Your Terraform code has deployed the full landing zone foundation, but seeing it live in the Azure Portal is where it all clicks. In this step, you will walk through the portal to verify every management group and policy assignment exists exactly as your code defined it. You will also explore Terraform state to understand how Terraform tracks the connection between your code and the real Azure resources.

In This Step, Get Ready To:
  • Verify the full ALZ management group hierarchy in the Azure Portal.
  • Confirm both Azure Policy assignments are active and scoped correctly.
  • Explore Terraform state to see how your code maps to real Azure resources.
Verify the Management Group Hierarchy

The Azure Portal gives you a visual tree view of your management groups. This is the quickest way to confirm that Terraform created the correct parent-child relationships across all 10+ groups.

  • Open the Azure Portal in your browser.
  • Search for Management groups in the top search bar and select it.
  • Click on your intermediate root management group (the one with your root_id prefix, e.g. alz) to expand the tree.
  • Click through each branch to verify the full hierarchy.

You should see the complete ALZ reference architecture tree. Under your intermediate root, Platform contains Connectivity, Identity, and Management. Landing Zones contains Corp and Online. Sandbox and Decommissioned sit directly under the intermediate root.

What should I see?

Your management group tree should look like this:

  • Tenant Root Group

If any groups are missing, check your main.tf for typos in the parent_management_group_id attributes and re-run terraform apply.

Verify Your Azure Policy Assignments

Your Terraform code assigned two Azure Policy governance guardrails. Now you need to confirm they are active and scoped to the correct management groups.

  • Search for Policy in the Azure Portal search bar and select Azure Policy.
  • Click Assignments in the left sidebar.
  • Click the Scope filter at the top and select your intermediate root management group.
  • Confirm you see the Allowed locations policy assignment listed.
  • Change the Scope filter to the Landing Zones management group.
  • Confirm you see the Require a tag on resource groups policy assignment listed.

Why is the compliance state showing as "Not started"?

Azure Policy compliance evaluation can take up to 30 minutes after a new assignment is created. The policies are active and will enforce rules on new resource deployments immediately. The compliance dashboard just needs time to scan existing resources.

How does policy inheritance work?

The Allowed locations policy is assigned at the intermediate root, which means it applies to every child management group beneath it. Platform, Landing Zones, Sandbox, Decommissioned, and all their children inherit this restriction. The tag requirement policy is assigned only to Landing Zones, so it applies to Corp and Online but not to Platform or Sandbox.

Explore Terraform State

Terraform state is the mapping between your .tf configuration files and the real Azure resources that exist in your tenant. Every time you run terraform apply, Terraform updates this state file so it knows what to create, update, or destroy on the next run.

  • Run the following command in your terminal to list every resource Terraform is tracking.
terraform state list

You should see a list of all your azurerm_management_group and azurerm_management_group_policy_assignment resources. Each line corresponds to a real Azure resource.

  • Run the following command to see the full details of every tracked resource.
terraform show

What does this command do?

terraform state list shows a compact list of resource addresses (like azurerm_management_group.platform). terraform show outputs the full attribute details for every resource, including IDs, names, and parent relationships. This is the complete picture of what Terraform knows about your infrastructure.

Do not delete the terraform.tfstate file

The terraform.tfstate file in your project directory is how Terraform tracks your deployed resources. If you delete it, Terraform loses its connection to the real Azure resources and cannot manage or destroy them. In production environments, teams store state remotely in Azure Blob Storage to prevent accidental loss.

You now have a fully verified Azure Landing Zone foundation. Every management group, every policy assignment, and every Terraform state entry confirms that your infrastructure-as-code deployment is working exactly as designed.

Secret mission

Add Centralized Monitoring with a Log Analytics Workspace

The management group hierarchy and policy guardrails are only the first layer of a real Azure Landing Zone. Enterprise teams also need centralized monitoring, where diagnostic logs from every Azure resource flow into a single place. In this secret mission, you will deploy a Log Analytics workspace to set up that monitoring foundation.

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 and Azure Policy assignments are completely free, so there are no ongoing costs from the core project. If you completed the secret mission, the Log Analytics workspace has a free tier of 5 GB/day ingestion and will not cost anything unless you start sending large volumes of data.

Resources you used:

  • Azure management groups (10+ groups in the ALZ hierarchy).
  • Azure Policy assignments (Allowed locations, Require a tag on resource groups).
  • Local Terraform state file (terraform.tfstate) and provider plugins (.terraform directory).
  • Secret mission only: Azure resource group (rg-alz-management) and Log Analytics workspace.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the landing zone with additional components.

  • Management groups and Azure Policy assignments are free Azure resources with no ongoing cost.
  • The Log Analytics workspace (secret mission) is free under 5 GB/day ingestion with 31-day retention.
  • Keep your terraform.tfstate file safe. This is how Terraform tracks what it created in Azure.

Pause - I'll come back to this later

Since all resources in this project are free, there is nothing to pause. Your management groups, policies, and Log Analytics workspace will sit idle at no cost.

  • Keep your azure-landing-zone project folder intact so you can pick up where you left off.
  • Do not delete the terraform.tfstate file. Without it, Terraform loses track of the resources it created and cannot manage or destroy them.
  • When you return, run az login to re-authenticate, then terraform plan to check the current state of your resources.

Delete - I don't want to use this again

Remove all project resources from Azure and clean up your local files.

  • Open your terminal and navigate to the azure-landing-zone project directory.
  • Run terraform destroy and type yes when prompted. This deletes all management groups, policy assignments, and any secret mission resources (resource group and Log Analytics workspace). Terraform handles the deletion order automatically.
  • After the destroy completes, you can delete the entire azure-landing-zone folder from your computer.

Mission accomplished

That's a Wrap

Nice work! You just deployed an enterprise-grade Azure Landing Zone foundation entirely through Terraform code.

Your Azure tenant now has the same governance skeleton that enterprise organizations use to manage security, compliance, and workload isolation at scale.

Here's what you learned:

  • Built a complete Terraform project from scratch with provider configuration, variables, outputs, and the full init, plan, apply, and destroy workflow.
  • Deployed the full ALZ management group hierarchy matching Microsoft's reference architecture, with 10+ management groups organized across Platform, Landing Zones, Sandbox, and Decommissioned scopes.
  • Assigned Azure Policy guardrails for region restriction and tag enforcement, implementing policy-driven governance that automatically applies to all child management groups and subscriptions.
  • Added centralized monitoring with a Log Analytics workspace, setting up the foundation for enterprise-grade observability across your landing zone.

Ready to quiz yourself?