Create an initial ALZ config
Clone the repo locally and add an initial Azure Landing Zone config based on the examples in the Terraform Registry.
Table of Contents
Overview
On this page you will:
-
Locally clone the repo generated by the Azure Landing Zone accelerator
-
Add the alz provider
Use the alz Terraform provider to pull the library definitions from the ALZ Library repo. You will learn plenty more about the alz provider and libraries throughout these labs, but for now we’ll just pull the most recent release of default definitions.
-
Update the variables.tf and add a main.tf to your Terraform config
Add a config based on the examples provided for the Azure Verified Modules in the Terraform registry.
-
Locally test your config will pass the CI tests
The CI workflow will test that your config meets minimum standards, and will pass the format, will initialise the providers, and passes the validation. You will also test it will create a terraform plan.
Override the backend with a terraform_override.tf file and test locally.
⚠️ You must not attempt to run terraform apply locally at his point. You will use the CI/CD workflow in the next page instead.
Clone the repo
-
Variables
github_repo="alz-mgmt" github_org="richeney-org"⚠️ Set to the correct values for your repo and org names.
-
Clone the repo
You may wish to switch to your standard directory for your git repos, if you have one.
git clone https://github.com/${github_org}/${github_repo} -
Change directory
cd $github_repo -
Open Visual Studio Code
code .
What is the ALZ Library repo?
We will be looking at libraries in greater detail in another series, but for the moment here is a brief introduction. You can read the documentation for the Azure Landing Zone libraries at https://azure.github.io/Azure-Landing-Zones-Library.
Libraries contain definitions for core compliancy guardrails. They define archetypes, collections of Azure Policy policies, initiatives, and assignments, plus custom role definitions. The archetypes. The architecture definitions then define the set of management groups, including their name, cosmetic display names, and the array of archetypes assigned to them.
The Azure Landing Zones Terraform provider can pull library definitions from different sources. These are then referenced by the ALZ Terraform module which specifies the architecture_name as well as any additional modifications, default values, etc. The two are closely related and these labs will help you to understand both.
The provider can pull from more than one source, and those sources can have dependencies on other libraries, which provides great scope for extensibility and customisation. We will explore this from a Microsoft partner perspective, extending the Azure Landing Zone and Sovereign Landing Zone baselines with reusable partner libraries, including country and industry packs for the sovereignty context. In addition, we will explore using archetype overrides to allow individual customers to customise their deployments.
The most common library source is the ALZ Library repo which is actively maintained by the Microsoft Customer Architecture and Engineering team (CAE). The repo contains library definitions for Azure Landing Zones, Sovereign Landing Zones, and Azure Monitoring Baseline Alerts. All are semantically versioned and you can view the releases.
Azure Landing Zone
Provider
Add the alz provider and specify the most recent Azure Landing Zone library release.
-
Check the releases page for the most recent platform/alz release.
At the time of writing this is 2025.09.3.
-
Add to the terraform.tf
provider "alz" { library_overwrite_enabled = true library_references = [ { path = "platform/alz" ref = "2025.09.3" } ] }The alz provider will pull in that version of the library definition from
https://github.com/Azure/Azure-Landing-Zones-Library/tree/{tag}/{path}where{tag}is{path}/{ref}.For example, https://github.com/Azure/Azure-Landing-Zones-Library/tree/platform/alz/2025.09.3/platform/alz for the core Azure Landing Zones library.
Variables
-
Add this code block to the variables.tf
variable "location" { type = string default = "uksouth" description = "Location for the resources" } variable "email_security_contact" { type = string default = "" description = "Email address for security alerts" }
Main
We will cover how the AVM modules work with the library as we progress through the labs. For the moment we will just get an example up and running,
-
Create a main.tf
data "azapi_client_config" "current" {} locals { management_resource_group_name = "rg-management-${var.location}" management_resource_group_id = "/subscriptions/${var.subscription_ids["management"]}/resourcegroups/${local.management_resource_group_name}" automation_account_name = "aa-management-${var.location}" log_analytics_workspace_name = "law-management-${var.location}" ama_user_assigned_managed_identity_name = "uami-management-ama-${var.location}" dcr_change_tracking_name = "dcr-change-tracking" dcr_defender_sql_name = "dcr-defender-sql" dcr_vm_insights_name = "dcr-vm-insights" ama_user_assigned_managed_identity_id = "${local.management_resource_group_id}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${local.ama_user_assigned_managed_identity_name}" ama_change_tracking_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_change_tracking_name}" ama_mdfc_sql_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_defender_sql_name}" ama_vm_insights_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_vm_insights_name}" log_analytics_workspace_id = "${local.management_resource_group_id}/providers/Microsoft.OperationalInsights/workspaces/${local.log_analytics_workspace_name}" } module "management_resources" { # <https://registry.terraform.io/modules/Azure/avm-ptn-alz-management/azurerm/latest> source = "Azure/avm-ptn-alz-management/azurerm" version = "0.9.0" location = var.location resource_group_name = local.management_resource_group_name automation_account_name = local.automation_account_name log_analytics_workspace_name = local.log_analytics_workspace_name data_collection_rules = { "change_tracking" = { "name" = local.dcr_change_tracking_name } "defender_sql" = { "name" = local.dcr_defender_sql_name } "vm_insights" = { "name" = local.dcr_vm_insights_name } } user_assigned_managed_identities = { ama = { name = local.ama_user_assigned_managed_identity_name } } } module "management_groups" { # <https://registry.terraform.io/modules/Azure/avm-ptn-alz/azurerm/latest> source = "Azure/avm-ptn-alz/azurerm" version = "0.14.0" architecture_name = "alz" location = var.location parent_resource_id = data.azapi_client_config.current.tenant_id # Tenant root group # retries = local.default_retries # timeouts = local.default_timeouts dependencies = { policy_assignments = [ module.management_resources.data_collection_rule_ids, module.management_resources.resource_id, module.management_resources.user_assigned_identity_ids, ] } policy_assignments_to_modify = { "alz" = { "policy_assignments" = { "Deploy-MDFC-Config-H224" = { "parameters" = { "ascExportResourceGroupLocation" = jsonencode({ value = var.location }) "ascExportResourceGroupName" = jsonencode({ value = "rg-asc-export-${var.location}" }) "emailSecurityContact" = jsonencode({ value = var.email_security_contact }) "enableAscForAppServices" = jsonencode({ value = "Disabled" }) "enableAscForArm" = jsonencode({ value = "Disabled" }) "enableAscForContainers" = jsonencode({ value = "Disabled" }) "enableAscForCosmosDbs" = jsonencode({ value = "Disabled" }) "enableAscForCspm" = jsonencode({ value = "Disabled" }) "enableAscForKeyVault" = jsonencode({ value = "Disabled" }) "enableAscForOssDb" = jsonencode({ value = "Disabled" }) "enableAscForServers" = jsonencode({ value = "Disabled" }) "enableAscForServersVulnerabilityAssessments" = jsonencode({ value = "Disabled" }) "enableAscForSql" = jsonencode({ value = "Disabled" }) "enableAscForSqlOnVm" = jsonencode({ value = "Disabled" }) "enableAscForStorage" = jsonencode({ value = "Disabled" }) } } } } "connectivity" = { "policy_assignments" = { "Enable-DDoS-VNET" = { "enforcement_mode" = "DoNotEnforce" } } } "corp" = { "policy_assignments" = { "Deploy-Private-DNS-Zones" = { "enforcement_mode" = "DoNotEnforce" } } } "landingzones" = { "policy_assignments" = { "Enable-DDoS-VNET" = { "enforcement_mode" = "DoNotEnforce" } } } } policy_default_values = { "ama_user_assigned_managed_identity_name" = jsonencode({ value = local.ama_user_assigned_managed_identity_name }) "ama_user_assigned_managed_identity_id" = jsonencode({ value = local.ama_user_assigned_managed_identity_id }) "ama_change_tracking_data_collection_rule_id" = jsonencode({ value = local.ama_change_tracking_data_collection_rule_id }) "ama_mdfc_sql_data_collection_rule_id" = jsonencode({ value = local.ama_mdfc_sql_data_collection_rule_id }) "ama_vm_insights_data_collection_rule_id" = jsonencode({ value = local.ama_vm_insights_data_collection_rule_id }) "log_analytics_workspace_id" = jsonencode({ value = local.log_analytics_workspace_id }) } subscription_placement = { "connectivity" = { "management_group_name" = "connectivity" "subscription_id" = var.subscription_ids["connectivity"] } "identity" = { "management_group_name" = "identity" "subscription_id" = var.subscription_ids["identity"] } "management" = { "management_group_name" = "management" "subscription_id" = var.subscription_ids["management"] } # "security" = { # "management_group_name" = "security" # "subscription_id" = var.subscription_ids["security"] # } } }⚠️ Note the subscription placement object. Comment (CTR+K,CTRL+C) and uncomment (CTRL+K,CTRL+U) as needed depending on how many subscriptions you are specifying.
Sovereign Landing Zone
Provider
Add the alz provider and specify the most recent Sovereign Landing Zone library release.
-
Check the releases page for the most recent platform/slz release.
At the time of writing this is 2025.10.1.
-
Add to the terraform.tf
provider "alz" { library_overwrite_enabled = true library_references = [ { path = "platform/slz" ref = "2025.10.1" } ] }The alz provider will pull in that version of the library definition from
https://github.com/Azure/Azure-Landing-Zones-Library/tree/{tag}/{path}where{tag}is{path}/{ref}.For example, https://github.com/Azure/Azure-Landing-Zones-Library/tree/platform/slz/2025.10.1/platform/slz for the Sovereign Landing Zone library.
Variables
-
Add this code block to the variables.tf
variable "location" { type = string default = "uksouth" description = "Location for the resources" } variable "email_security_contact" { type = string default = "" description = "Email address for security alerts" }
Main
The Sovereign Landing Zone example differs from the Azure Landing Zone example in only a few ways, all related to the management_groups module:
- the architecture_name is now set to
slzrather thanalz - the policy_assignments_to_modify object also refers to the
slzarchitecture name - the policy_default_values now includes
"allowed_locations" = jsonencode({ value = [${var.location}]})as this array of region shortcodes is expected by the additional archetypes
We won’t cover those changes in any real detail on this page. For the moment let’s get it up and running.
There is a later deep dive on libraries and how the AVM modules work with those library definitions and we’ll circle back to these changes at that poit.
-
Create a main.tf
data "azapi_client_config" "current" {} locals { management_resource_group_name = "rg-management-${var.location}" management_resource_group_id = "/subscriptions/${var.subscription_ids["management"]}/resourcegroups/${local.management_resource_group_name}" automation_account_name = "aa-management-${var.location}" log_analytics_workspace_name = "law-management-${var.location}" ama_user_assigned_managed_identity_name = "uami-management-ama-${var.location}" dcr_change_tracking_name = "dcr-change-tracking" dcr_defender_sql_name = "dcr-defender-sql" dcr_vm_insights_name = "dcr-vm-insights" ama_user_assigned_managed_identity_id = "${local.management_resource_group_id}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${local.ama_user_assigned_managed_identity_name}" ama_change_tracking_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_change_tracking_name}" ama_mdfc_sql_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_defender_sql_name}" ama_vm_insights_data_collection_rule_id = "${local.management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/${local.dcr_vm_insights_name}" log_analytics_workspace_id = "${local.management_resource_group_id}/providers/Microsoft.OperationalInsights/workspaces/${local.log_analytics_workspace_name}" } module "management_resources" { # <https://registry.terraform.io/modules/Azure/avm-ptn-alz-management/azurerm/latest> source = "Azure/avm-ptn-alz-management/azurerm" version = "0.9.0" location = var.location resource_group_name = local.management_resource_group_name automation_account_name = local.automation_account_name log_analytics_workspace_name = local.log_analytics_workspace_name data_collection_rules = { "change_tracking" = { "name" = local.dcr_change_tracking_name } "defender_sql" = { "name" = local.dcr_defender_sql_name } "vm_insights" = { "name" = local.dcr_vm_insights_name } } user_assigned_managed_identities = { ama = { name = local.ama_user_assigned_managed_identity_name } } } module "management_groups" { # <https://registry.terraform.io/modules/Azure/avm-ptn-alz/azurerm/latest> source = "Azure/avm-ptn-alz/azurerm" version = "0.14.0" architecture_name = "slz" location = var.location parent_resource_id = data.azapi_client_config.current.tenant_id # Tenant root group # retries = local.default_retries # timeouts = local.default_timeouts dependencies = { policy_assignments = [ module.management_resources.data_collection_rule_ids, module.management_resources.resource_id, module.management_resources.user_assigned_identity_ids, ] } policy_assignments_to_modify = { "slz" = { "policy_assignments" = { "Deploy-MDFC-Config-H224" = { "parameters" = { "ascExportResourceGroupLocation" = jsonencode({ value = var.location }) "ascExportResourceGroupName" = jsonencode({ value = "rg-asc-export-${var.location}" }) "emailSecurityContact" = jsonencode({ value = var.email_security_contact }) "enableAscForAppServices" = jsonencode({ value = "Disabled" }) "enableAscForArm" = jsonencode({ value = "Disabled" }) "enableAscForContainers" = jsonencode({ value = "Disabled" }) "enableAscForCosmosDbs" = jsonencode({ value = "Disabled" }) "enableAscForCspm" = jsonencode({ value = "Disabled" }) "enableAscForKeyVault" = jsonencode({ value = "Disabled" }) "enableAscForOssDb" = jsonencode({ value = "Disabled" }) "enableAscForServers" = jsonencode({ value = "Disabled" }) "enableAscForServersVulnerabilityAssessments" = jsonencode({ value = "Disabled" }) "enableAscForSql" = jsonencode({ value = "Disabled" }) "enableAscForSqlOnVm" = jsonencode({ value = "Disabled" }) "enableAscForStorage" = jsonencode({ value = "Disabled" }) } } } } "connectivity" = { "policy_assignments" = { "Enable-DDoS-VNET" = { "enforcement_mode" = "DoNotEnforce" } } } "corp" = { "policy_assignments" = { "Deploy-Private-DNS-Zones" = { "enforcement_mode" = "DoNotEnforce" } } } "landingzones" = { "policy_assignments" = { "Enable-DDoS-VNET" = { "enforcement_mode" = "DoNotEnforce" } } } } policy_default_values = { "allowed_locations" = jsonencode({ value = [var.location] }) "ama_user_assigned_managed_identity_name" = jsonencode({ value = local.ama_user_assigned_managed_identity_name }) "ama_user_assigned_managed_identity_id" = jsonencode({ value = local.ama_user_assigned_managed_identity_id }) "ama_change_tracking_data_collection_rule_id" = jsonencode({ value = local.ama_change_tracking_data_collection_rule_id }) "ama_mdfc_sql_data_collection_rule_id" = jsonencode({ value = local.ama_mdfc_sql_data_collection_rule_id }) "ama_vm_insights_data_collection_rule_id" = jsonencode({ value = local.ama_vm_insights_data_collection_rule_id }) "log_analytics_workspace_id" = jsonencode({ value = local.log_analytics_workspace_id }) } subscription_placement = { "connectivity" = { "management_group_name" = "connectivity" "subscription_id" = var.subscription_ids["connectivity"] } "identity" = { "management_group_name" = "identity" "subscription_id" = var.subscription_ids["identity"] } "management" = { "management_group_name" = "management" "subscription_id" = var.subscription_ids["management"] } # "security" = { # "management_group_name" = "security" # "subscription_id" = var.subscription_ids["security"] # } } }⚠️ Note the subscription placement object. Comment (CTR+K,CTRL+C) and uncomment (CTRL+K,CTRL+U) as needed depending on how many subscriptions you are specifying
Test Locally
We’ll now run a few tests locally before we commit the changes and create a pull request. We’ll override the backend so that we can run terraform commands at the CLI. , and then run a few commands to ensure we’ll pass the continuous integration (CI) workflow’s inbuilt checks.
Override the backend
The terraform block in the terraform.tf file includes an empty backend.
backend "azurerm" {}
The workflow’s will use the BACKEND_AZURE_* environment variables within the terraform init steps so that the managed identities write the remote state file to the correct storage account and container.
Before we progress we want to make sure that your config will pass the CI checks, so we will locally override the backend so that you can run the tests at the CLI.
-
Create a terraform_override.tf
terraform { backend "local" { path = "terraform.tfstate" } }ℹ️ The file should be greyed out in Visual Studio Code’s explorer pane as this filename is included in the .gitignore. It will not be included in any git commits.
-
Set the ARM_SUBSCRIPTION_ID env var
The config is also expecting a subscription ID for the main azurerm provider block. Export the environment variable, setting it to the management subscription GUID.
export ARM_SUBSCRIPTION_ID="{managementSubscriptionId}"
Check your config
-
Check the format
The CI checks will ensure that your committed code aligns with gofmt standards.
terraform fmtThis command will automatically correct issues where possible, listing the files it has modified. If there is no command output then all of the files are already compliant.
-
Initialise
terraform initConfirm that terraform has been successfully initialized and has downloaded the providers as expected.
-
Validate
Confirm that the config is syntactically sound.
terraform validateCorrect any errors until terraform validate succeeds.
Success! The configuration is valid.
Plan (optional)
If your ID has access to view the resources in both Azure and GitHub then you can test to see if the plan will succeed.
If your ID does not have that access the the plan should correctly fail. If so then skip this step and move onto the next page. When you commit your changes and create your pull request then the terraform plan will be tested in the CI workflow.
Note that running terraform plan locally just checks the code and is not using the remote state in the storage account as we overrode the backend. This may be more noticeable when adding in additional changes in later labs, but the displayed planned changes may be disregarded as it is really just a deeper validation at this stage.
The terraform plan outputs in the CI/CD pipelines will provide the accurate and definitive diff and should always be properly reviewed.
terraform apply! You will do this via the CI/CD pipelines on the next page.
-
Plan
terraform planThe output will show that terraform plans to create over 600 resources.
Next
If the tests all look good then your config should pass the CI tests. You’re done on this page.
On the next page we will run through the standard branch based workflow to commit your changes into a new branch and submit a pull request to trigger the CI/CD pipelines.
References
- https://aka.ms/alz
- https://aka.ms/alz/accelerator/docs
- https://github.com/Azure/alz-terraform-accelerator