Landing Zones as Code: Modern Azure Foundations with GitOps

Principal Solution Architect, working daily with Azure, primarily focusing on automation and "everything as code".
This post was written for the Azure Spring Clean 2026 event. Visit the official website to see the schedule and links to great content.
A year ago, I wrote an article for this community event titled New ALZ Accelerator: Lessons learned, where I shared my experience with pros and cons of using that accelerator. My feedback was overall positive but I wanted to have more granular control over the configuration of platform LZ, the level of abstraction was too much for me.
A lot has changed since that time, most notably the fact that the starter modules (for both Bicep and Terraform) are now using Azure Verified Modules, my second most favorite project.
Azure Landing Zones are the foundation for secure, scalable, and well-governed Azure environments—but getting from theory to a production-ready setup can still feel heavy.
In this article, we’ll take a look at the new Azure Landing Zone IaC Accelerator, how this tool simplifies the deployment and operation of Azure Landing Zones using Infrastructure as Code and GitOps.
You’ll see how this approach enables repeatability, consistency, and safer changes across environments—while aligning platform engineering practices with real-world operations.
Plan first, bootstrap later
The entire Accelerator experience is organized into four distinct phases, each having specific inputs and outputs, that are then used in the subsequent phase.
The first one, called Planning, allows you to capture essential inputs and design decisions for your Azure Landing Zone. It is typically executed as a workshop, where you, as the facilitator, invite key stakeholders - from business, to engineering, operations, and security - and provide them with a set of design questions with available options.
These could range from complex topics like network topology to simpler ones, such as What IaC tool do we want to use? or What SDLC platform should we choose? GitHub, Azure DevOps, or something else?
“If you fail to plan, you are planning to fail!” (Benjamin Franklin)
This might sound like an unnecessary overhead, but I would strongly encourage you to have a report or a set of Architecture Decision Records (ADRs) from this workshop, that your stakeholders formally sign-off. It will save you from disputes about what was agreed and approved. You don't want to rebuild the entire setup a few weeks or months after the initial deployment!
Luckily, the ALZ team provides you with two assets that can help you with collecting the inputs:
configuration files - two yaml files that are used by the
ALZPowerShell module as the orchestrator in the Bootstrap phase.checklist - an Excel spreadsheet that helps you gather the required information. It is a bit more presentable for non-technical stakeholders.
Both assets have a clear separation between Bootstrap and Deploy phases:
You mark your Bootstrap choices in the
Accelerator - Bootstrapsheet and theinputs.yamlfileYou specify your Deploy choices in the
Accelerator - Bicepsheet and theplatform-landing-zone.yaml.
Bootstrap-related decisions
There is a number of bootstrap decisions you need to make, from the IaC tool you prefer, your SDLC tool of choice, support for private agents/runners and private networking.
Here is how my inputs.yaml file look like:
iac_type: "bicep"
bootstrap_module_name: "alz_github"
starter_module_name: "platform_landing_zone"
bootstrap_location: "norwayeast"
root_parent_management_group_id: "" # Leave empty to use Tenant Root Group
use_self_hosted_runners: false
use_private_networking: false
github_personal_access_token: "github_pat_xxxxxx" # Can also be supplied via environment variable TF_VAR_github_personal_access_token
github_organization_name: "my-enterprise-org"
apply_approvers: ["david.pazdera@cegal.com"]
subscription_ids:
management: ""
connectivity: ""
identity: ""
security: ""
bootstrap_subscription_id: ""
You will need at least four (or eventually five) empty subscriptions that will represent different parts of your platform.
subscription_ids, as you can see in the screenshot below.
Please note, that you need a GitHub organization account, not just your private account. When creating PAT token(s), make sure you are selecting correct account when granting permissions, it needs to match with github_organization_name.
Starter module decisions
I have also noticed that the checklist spreadsheet got simplified (comparing to the previous versions), especially that Accelerator - Bicep starter module sheets, with decisions like "Deploy Bastion Host: Yes/No" (like you can see below):
The problem is, that these decisions don't have any direct mapping to platform-landing-zone.yaml config file, which is confusing to say the least.
I was curious if those two yaml files have some sort of 'schema', so I can better understand what behavior I can influence by using specific configuration keys and values:
I found one for the bootstrap phase here and a quite well documented example for Bicep - GitHub combo. If you are familiar with Terraform, you can also inspect this variables.tf file.
For the Bicep starter module, there is ALZ-Powershell.config.json file in the
Azure/alz-bicep-acceleratorrepository that could be treated as a schema file, or at least it shows the keys and default values. For most common input parameters, you can check the example here.
These yaml files, however, give you only limited control about what gets provisioned to your Platform landing zone. Don't worry, you will get much more granular control after you run the next phase.
Regardless what method for collecting those inputs you select, you will need to provide them as those .yaml files, when you run the ALZ Accelerator... or you can decide to use an interactive mode, where the Accelerator asks you about those points, but only for the bootstrap part. You need to update your platform landing zone configuration file with the required values!
Prerequisites - tools and permissions
I bet you are eager to get practical, but there is one step between planning and bootstrapping: we need to ensure our machine has necessary tools and we have correct accounts and permissions to continue without errors.
Machine
I was running the deployment on a MacBook and all the tools that were needed, I installed using Homebrew, but I bet you could do the same thing with WinGet or a similar package manager on Linux. All you need is:
- PowerShell 7.4+
- Azure CLI 2.55.0+
- Git
- Coding editor (not required but recommended)
With these tools in place, you need to start a PowerShell session, where you first install the ALZ module from the PowerShell Gallery: Install-PSResource -Name ALZ
Test-AcceleratorRequirement cmdlet.
Accounts and permissions
Apart from having those four empty Azure subscriptions (located under to 'Root Tenant' in the Management Group hierarchy), you need quite high permissions in Azure and Microsoft Entra.
Owner role on the chosen "top level" management group (in my case it was the 'Tenant Root Group')
User Access Administrator role on the
/root tenant level (required for AVM Bicep only)Owner role for the chosen subscriptions
Regardless if you have those permissions assigned statically or you use PIM, you will need to sign-in to your terminal session to the "target tenant":
az login --tenant "3877c9d0-cd6e-40ea-a2df-3da9634d92e7" --use-device-code
After signing using the device code and confirming the MFA challenge (or ideally using a passkey to authenticate to Entra ID), you should get a list of available subscriptions. Select the one you dedicated to Bootstrap resources. In my case, I used the same subscription as for the Management LZ.
You should double-check you have the right permissions, either by using the Portal, or Azure CLI, for example:
USER_OBJECT_ID=$(az ad signed-in-user show --query id -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)
MG_NAME="$TENANT_ID"
MG_SCOPE="/providers/Microsoft.Management/managementGroups/$MG_NAME"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
SUB_SCOPE="/subscriptions/$SUBSCRIPTION_ID"
# Check User Access Administrator at the root level
echo "Checking UAA role at the root level for $USER_OBJECT_ID"
az role assignment list --assignee "$USER_OBJECT_ID" --role "User Access Administrator" --scope "/" -o table
# Check Tenant Root Group Ownership
echo "Checking Owner at Tenant Root Group (\(MG_SCOPE) for \)USER_OBJECT_ID"
az role assignment list --assignee "\(USER_OBJECT_ID" --role "Owner" --scope "\)MG_SCOPE" --include-inherited true -o table
# Check subscription ownership
echo "Checking Owner on subscription \(SUBSCRIPTION_ID for \)USER_OBJECT_ID"
az role assignment list --assignee "\(USER_OBJECT_ID" --role "Owner" --scope "\)SUB_SCOPE" --include-inherited true -o table
Check this article for more details and explanations, how these permissions are being used.
Bootstrap your GitOps setup
Depending on your chosen IaC language, VCS, and self-hosted runners/agents choices, the Deploy-Accelerator command will create the following resources (or a subset of those):
Gain full control over the process
Since I prefer having a maximum control over the process, instead of using the interactive mode, I went for the advanced bootstrap option with my own twist:
- Initiate a folder structure to hold the configuration files for the bootstrap deployment:
New-AcceleratorFolderStructure -iacType bicep -versionControl github -targetFolderPath "~/repos/alz-acc-test-deployment"
Open the newly created configuration folder in VS Code:
code "~/repos/alz-acc-test-deployment/config"Update the
inputs.yamlfile located in theconfigfolder with the agreed-upon values from the planning phase. Ensure all required fields are filled out correctly. Pay attention to these parameters that shape the naming convention of your bootstrap resources:service_nameis used to build up the default resource names. Only lowercase letters and numbers are allowed. Example: rg-<service_name>-mgmt-uksouth-001environment_nameis used to differentiate between multiple environments (e.g., dev, test, prod). Used to build up the default resource names.Only lowercase letters and numbers are allowed. Example: rg-alz-<environment_name>-uksouth-001postfix_numberis used to build up the default resource names. Only numbers are allowed. Default is1. Example: rg-alz-mgmt-uksouth-<postfix_number>apply_approversis a list of email addresses of approvers for the apply stage in GitHub Actions. Must be a list of GitHub usernames or team slugs. Example: ["user1", "user2", "org/team-name"]
Update the
platform_landing_zone/inputs.yamlfile located in the same directory:At least one parameter require an update:
starter_locations: ["norwayeast"]You can use the prefix and postfix variables to add a consistent naming convention across all management groups. For custom management group structures, you can modify the Bicep code directly after running the bootstrap and prior to deploying the Platform Landing Zone.
You can influence the naming convention of Resource Groups by modifying
resource_group_{type}_prefix:keys, e.g.,resource_group_logging_name_prefix:
Now you are finally ready to run the bootstrap by using this command:
$targetFolderPath = "~/repos/alz-acc-test-deployment/"
Deploy-Accelerator -inputs "\(targetFolderPath/config/inputs.yaml", "\)targetFolderPath/config/platform-landing-zone.yaml" -output "$targetFolderPath/output"
This cmdlet has many parameters that can be used to customize the deployment process - for example: -bootstrap_module_version, -starter_module_version, -auto_approve, -skip_requirements_check but I haven't used them.
Behind the scenes, it leverages Terraform to provision all required resources using respective providers:
Validate the result
After the bootstrap deployment is complete, the following subdirectories and files are in the bootstrap directory:
As you can see, the tool pulled the 'latest' version of bootstrap and starter packages from their respective sources locally and used them in that Terraform flow.
More interesting is the view on the GitHub side. Since I decided to set the use_separate_repository_for_templates key to 'true', I got two repositories: one containing reusable (called) workflows:
... and another one storing the actual platform LZ configuration:
This repository was configured with:
a branch policy for the
mainbranch requiring pull requests to be reviewed and approved before mergingRequire a PR before merging, Org and repository admins can dismiss PR reviews
Require conversation resolution before merging
Require linear history
Do not allow bypassing the above settings
two environments named
PlanandApplya GitHub team -
alz-mgmt-approversthat is authorized to approve deployments to production environment in the Continuous Deployment workflow.a collection of repository and environment variables:
On the Azure side:
two managed identities were created in the bootstrap subscription
these UAMIs were assigned the following custom RBAC roles:
It is important to note, that at this stage, there are no changes made to the Management Groups, policies, and platform resources, except the creation of the bootstrap resources in the bootstrap subscription (in our case the same as the management LZ subscription).
Customize your Platform LZ configuration
We still have the option to shape and fine-tune the way our platform landing zone will look like, thanks to more modular approach and the fact this new accelerator uses Azure Verified Modules.
You can start by cloning your newly created GitHub repository to your dev machine:
gh repo clone my-org/alz-mgmt
code alz-mgmt
For example: You want to modify the Management Group hierarchy or choose, what policies are assigned or not assigned at what scope. You would follow those guides and modify respective .bicepparam files (you shouldn't have a need to modify any of the main.bicep files).
Here I am removing the 'Identity' Management Group from the hierarchy:
Once you are happy with your changes, simply create a new branch, stage and commit the changed files, and push them back to GitHub:
git checkout -b config-update
git add .
git commit -m "Modified Bicep code to remove unused subscriptions"
git push -u origin config-update
Final phase: Run
Review your configuration changes first
If you made any changes to the configuration, you need to create a Pull Request to merge those changes to the main branch. This will start the Continuous Integration (CI) workflow, which will validate the changes:
Bring popcorn and watch the deployment
Once the CI workflow completes successfully, you can merge the pull request into the main branch. This will trigger the Continuous Delivery (CD) workflow, which will finally deploy the Platform Landing Zone to Azure. 😎
Once the CD workflow reaches the "Deploy" stage, it will require approval from one of the designated approvers. You can approve the deployment by navigating to the "Actions" tab of the GitHub repository and selecting the "Deploy" job.
The workflow will continue and deploy the Platform Landing Zone to Azure. From this list of tasks you can see how is the deployment sequenced:
Final validation
If you don't get any errors in the workflow run, you should verify the result in the Azure Portal and see if:
- You have the full Management Group hierarchy with platform subscriptions in correct MGs:
- Custom roles available in the environment:
- ALZ Policies assigned at the right scopes:
- Your Platform subscriptions contain resources such as Log Analytics workspace, Data Collection Rules, Action Groups, Alert Rules, Hub VNet with Azure Firewall, etc.
Conclusion
My overall experience has been very positive:
the documentation has improved a lot,
the modularity of the new Accelerator is a great improvement (ALZ ❤️ AVM),
you are in better control and you can customize your setup way more than with the previous version.
There are still a few things on my wish list I would like to see in future releases like tag-driven enablement of Microsoft Defender for specific resources. My customers are ok to pay for extra protection of their production resources, but why should they enable those expensive features on dev and test environments?
Join the ALZ community
I would encourage you to join ALZ External Community calls by signing-up here: aka.ms/alz/communitycall
If you can't join them directly, you could always watch the recordings on YouTube:



