Skip to main content

Command Palette

Search for a command to run...

Landing Zones as Code: Modern Azure Foundations with GitOps

Published
15 min read
Landing Zones as Code: Modern Azure Foundations with GitOps
D

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!

💡
This rule is even more important if you aren't deploying ALZ in your own organization, but you are doing it as an MSP for your customers. The report should be added to project documentation, so any significant change in the setup can be handled as a change request with an adequate compensation for the 'rework'.

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 ALZ PowerShell 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.

I will be focusing on my experience with Bicep - GitHub combination, so the behavior will be different, if you use Terraform or ADO!

Both assets have a clear separation between Bootstrap and Deploy phases:

  • You mark your Bootstrap choices in the Accelerator - Bootstrap sheet and the inputs.yaml file

  • You specify your Deploy choices in the Accelerator - Bicep sheet and the platform-landing-zone.yaml.

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.

I tried the bootstrap with only two subscriptions - management and connectivity - but I got an error about missing config keys under the 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.

💡
I was using an organization that is part of GitHub Enterprise Cloud account, but for testing you could create a free organization. You won't be able to configure all security and governance features though.

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-accelerator repository 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

💡
TIP: Once you have that module installed, you can easily validate if your machine has all the required tools by running the 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.yaml file located in the config folder 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_name is used to build up the default resource names. Only lowercase letters and numbers are allowed. Example: rg-<service_name>-mgmt-uksouth-001

    • environment_name is 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-001

    • postfix_number is used to build up the default resource names. Only numbers are allowed. Default is 1. Example: rg-alz-mgmt-uksouth-<postfix_number>

    • apply_approvers is 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.yaml file 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:

You might be wondering if you need to handle Terraform state file or commit the HCL config files to a version control. The short answer is "No". This is a one-off operation and the Accelerator doesn't currently support managing the bootstrap configuration in VCS.

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 main branch requiring pull requests to be reviewed and approved before merging

    • Require 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 Plan and Apply

  • a GitHub team - alz-mgmt-approvers that is authorized to approve deployments to production environment in the Continuous Deployment workflow.

  • a collection of repository and environment variables:

💡
The Accelerator doesn't create secrets but prefers variables for storing key values for OIDC authentication. This is because secrets are encrypted and not visible in the GitHub Actions logs, while variables can be used directly in the workflows without additional steps. This makes the troubleshooting and debugging of workflows easier.

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:

ALZ Accelerator contains a large collection of policies that get assigned, some audit resource configuration, some prevent insecure resources, others enable services - like Microsoft Defender for Cloud - that will have an impact on your Azure bill. You should spend time reviewing those policies, their effect, and impact on your cloud foundation and its cost, before you trigger the deployment!

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:

https://www.youtube.com/@MicrosoftCAE