Deployment Options
Compare Portal, Bicep, Terraform, and Pulumi approaches for landing zone deployment.
Deployment Approaches Overview
Comparison Matrix
| Feature | Portal | ALZ Bicep | CAF Terraform | Pulumi |
|---|---|---|---|---|
| Learning Curve | Low | Medium | Medium | Medium-High |
| Customization | Limited | High | Very High | Highest |
| State Management | N/A | ARM | Terraform State | Pulumi State |
| Multi-cloud | No | No | Yes | Yes |
| GitOps Ready | No | Yes | Yes | Yes |
| Team Preference | Operators | Azure-native | Platform teams | Developers |
| Microsoft Support | Yes | Community | Community | Community |
| Best For | POC, Learning | Azure-only shops | Multi-cloud/TF teams | Dev-centric orgs |
Option 1: Azure Portal
Deploy to Azure Button
The quickest way to deploy a reference landing zone:
[](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FEnterprise-Scale%2Fmain%2FeslzArm%2FeslzArm.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FEnterprise-Scale%2Fmain%2FeslzArm%2Feslz-portal.json)
Portal Deployment Steps
Pros & Cons
| Pros | Cons |
|---|---|
| ✅ No IaC knowledge required | ❌ No version control |
| ✅ Guided experience | ❌ Hard to reproduce |
| ✅ Quick for POC | ❌ Limited customization |
| ✅ Good for learning | ❌ No CI/CD integration |
Option 2: ALZ Bicep
Overview
ALZ Bicep is the Azure-native IaC approach using modular Bicep files.
Repository: Azure/ALZ-Bicep
Module Structure
ALZ-Bicep/
├── infra-as-code/
│ └── bicep/
│ ├── modules/
│ │ ├── managementGroups/
│ │ ├── policy/
│ │ ├── roleAssignments/
│ │ ├── subscriptionPlacement/
│ │ ├── logging/
│ │ ├── hubNetworking/
│ │ └── spokeNetworking/
│ └── orchestration/
│ ├── mgDiagSettingsAll/
│ ├── subPlacementAll/
│ └── policyAssignmentAll/
Deployment Flow
Sample: Management Groups Module
// modules/managementGroups/managementGroups.bicep
targetScope = 'tenant'
@description('Prefix for management group names')
param parTopLevelManagementGroupPrefix string = 'alz'
@description('Display name prefix for management groups')
param parTopLevelManagementGroupDisplayName string = 'Azure Landing Zones'
// Intermediate Root Management Group
resource resIntermediateRootMG 'Microsoft.Management/managementGroups@2021-04-01' = {
name: parTopLevelManagementGroupPrefix
properties: {
displayName: parTopLevelManagementGroupDisplayName
}
}
// Platform Management Group
resource resPlatformMG 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${parTopLevelManagementGroupPrefix}-platform'
properties: {
displayName: 'Platform'
details: {
parent: {
id: resIntermediateRootMG.id
}
}
}
}
// Identity Management Group
resource resIdentityMG 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${parTopLevelManagementGroupPrefix}-platform-identity'
properties: {
displayName: 'Identity'
details: {
parent: {
id: resPlatformMG.id
}
}
}
}
// Landing Zones Management Group
resource resLandingZonesMG 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${parTopLevelManagementGroupPrefix}-landingzones'
properties: {
displayName: 'Landing Zones'
details: {
parent: {
id: resIntermediateRootMG.id
}
}
}
}
// Corp Management Group
resource resCorpMG 'Microsoft.Management/managementGroups@2021-04-01' = {
name: '${parTopLevelManagementGroupPrefix}-landingzones-corp'
properties: {
displayName: 'Corp'
details: {
parent: {
id: resLandingZonesMG.id
}
}
}
}
// Outputs
output outIntermediateRootMGId string = resIntermediateRootMG.id
output outPlatformMGId string = resPlatformMG.id
output outLandingZonesMGId string = resLandingZonesMG.id
ALZ Bicep Deployment
# Step 1: Management Groups
az deployment tenant create \
--name "alz-mg-deploy" \
--location eastus \
--template-file infra-as-code/bicep/modules/managementGroups/managementGroups.bicep \
--parameters @config/managementGroups.parameters.json
# Step 2: Custom Policy Definitions
az deployment mg create \
--name "alz-policy-deploy" \
--location eastus \
--management-group-id "alz" \
--template-file infra-as-code/bicep/modules/policy/definitions/customPolicyDefinitions.bicep
# Step 3: Logging
az deployment mg create \
--name "alz-logging-deploy" \
--location eastus \
--management-group-id "alz-platform-management" \
--template-file infra-as-code/bicep/modules/logging/logging.bicep \
--parameters @config/logging.parameters.json
# Step 4: Hub Networking
az deployment mg create \
--name "alz-hub-deploy" \
--location eastus \
--management-group-id "alz-platform-connectivity" \
--template-file infra-as-code/bicep/modules/hubNetworking/hubNetworking.bicep \
--parameters @config/hubNetworking.parameters.json
Option 3: CAF Terraform
Overview
CAF (Cloud Adoption Framework) Terraform module is the multi-cloud friendly approach.
Repository: Azure/terraform-azurerm-caf-enterprise-scale
Module Usage
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate001"
container_name = "tfstate"
key = "alz.tfstate"
}
}
provider "azurerm" {
features {}
}
provider "azurerm" {
alias = "connectivity"
subscription_id = var.connectivity_subscription_id
features {}
}
provider "azurerm" {
alias = "management"
subscription_id = var.management_subscription_id
features {}
}
module "enterprise_scale" {
source = "Azure/caf-enterprise-scale/azurerm"
version = "~> 5.0"
providers = {
azurerm = azurerm
azurerm.connectivity = azurerm.connectivity
azurerm.management = azurerm.management
}
# Base configuration
root_parent_id = data.azurerm_client_config.current.tenant_id
root_id = "alz"
root_name = "Azure Landing Zones"
# Default location
default_location = "eastus"
# Subscription IDs
subscription_id_connectivity = var.connectivity_subscription_id
subscription_id_identity = var.identity_subscription_id
subscription_id_management = var.management_subscription_id
# Enable features
deploy_core_landing_zones = true
deploy_corp_landing_zones = true
deploy_online_landing_zones = true
deploy_sap_landing_zones = false
deploy_connectivity_resources = true
deploy_identity_resources = true
deploy_management_resources = true
# Configure connectivity
configure_connectivity_resources = local.configure_connectivity_resources
configure_management_resources = local.configure_management_resources
}
Connectivity Configuration
# locals.tf
locals {
configure_connectivity_resources = {
settings = {
hub_networks = [
{
enabled = true
config = {
address_space = ["10.0.0.0/16"]
location = "eastus"
link_to_ddos_protection_plan = true
dns_servers = []
bgp_community = ""
subnets = []
virtual_network_gateway = {
enabled = true
config = {
address_prefix = "10.0.1.0/24"
gateway_sku_expressroute = "ErGw2AZ"
gateway_sku_vpn = "VpnGw2AZ"
advanced_vpn_settings = {
enable_bgp = true
active_active = true
private_ip_address_allocation = "Dynamic"
default_local_network_gateway_id = ""
vpn_client_configuration = []
bgp_settings = []
custom_route = []
}
}
}
azure_firewall = {
enabled = true
config = {
address_prefix = "10.0.2.0/24"
enable_dns_proxy = true
dns_servers = []
sku_tier = "Premium"
base_policy_id = ""
private_ip_ranges = []
threat_intelligence_mode = "Deny"
threat_intelligence_allowlist = []
availability_zones = {
zone_1 = true
zone_2 = true
zone_3 = true
}
}
}
spoke_virtual_network_resource_ids = []
enable_outbound_virtual_network_peering = true
enable_hub_network_mesh_peering = false
}
}
]
vwan_hub_networks = []
ddos_protection_plan = {
enabled = true
config = {
location = "eastus"
}
}
dns = {
enabled = true
config = {
location = "eastus"
enable_private_link_by_service = {
azure_blob_storage = true
azure_sql_database = true
azure_key_vault = true
azure_container_registry = true
}
private_link_locations = ["eastus"]
}
}
}
}
}
Terraform State Management
# backend.tf - Bootstrap state storage
resource "azurerm_resource_group" "state" {
name = "rg-terraform-state"
location = "eastus"
}
resource "azurerm_storage_account" "state" {
name = "stterraformstate001"
resource_group_name = azurerm_resource_group.state.name
location = azurerm_resource_group.state.location
account_tier = "Standard"
account_replication_type = "GRS"
min_tls_version = "TLS1_2"
blob_properties {
versioning_enabled = true
}
}
resource "azurerm_storage_container" "state" {
name = "tfstate"
storage_account_name = azurerm_storage_account.state.name
container_access_type = "private"
}
Option 4: Custom Implementation
When to Go Custom
- Unique organizational requirements
- Specific compliance needs
- Integration with existing tooling
- Full control over implementation
Custom Structure Example
landing-zone/
├── .github/
│ └── workflows/
│ ├── deploy-platform.yml
│ ├── deploy-landing-zone.yml
│ └── validate-pr.yml
├── modules/
│ ├── management-groups/
│ ├── policy-definitions/
│ ├── policy-assignments/
│ ├── hub-network/
│ ├── spoke-network/
│ └── logging/
├── environments/
│ ├── dev/
│ ├── test/
│ └── prod/
├── config/
│ ├── management-groups.json
│ ├── policies.json
│ └── network.json
└── scripts/
├── deploy.ps1
└── validate.ps1
CI/CD Pipeline Examples
GitHub Actions (Bicep)
# .github/workflows/deploy-alz.yml
name: Deploy Azure Landing Zone
on:
push:
branches: [main]
paths: ['infra/**']
pull_request:
branches: [main]
paths: ['infra/**']
permissions:
id-token: write
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Validate Bicep
run: |
az bicep build --file infra/main.bicep
- name: What-If
run: |
az deployment tenant what-if \
--location eastus \
--template-file infra/main.bicep \
--parameters @infra/parameters/prod.json
deploy:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy
run: |
az deployment tenant create \
--location eastus \
--template-file infra/main.bicep \
--parameters @infra/parameters/prod.json
Azure DevOps (Terraform)
# azure-pipelines.yml
trigger:
branches:
include:
- main
paths:
include:
- terraform/**
pool:
vmImage: 'ubuntu-latest'
variables:
- group: alz-secrets
stages:
- stage: Validate
jobs:
- job: Validate
steps:
- task: TerraformInstaller@1
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
backendServiceArm: 'azure-service-connection'
backendAzureRmResourceGroupName: 'rg-terraform-state'
backendAzureRmStorageAccountName: 'stterraformstate001'
backendAzureRmContainerName: 'tfstate'
backendAzureRmKey: 'alz.tfstate'
- task: TerraformTaskV4@4
displayName: 'Terraform Plan'
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
environmentServiceNameAzureRM: 'azure-service-connection'
- stage: Deploy
dependsOn: Validate
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Deploy
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: TerraformTaskV4@4
displayName: 'Terraform Apply'
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
environmentServiceNameAzureRM: 'azure-service-connection'
Decision Guide
Quick Reference Card
| Approach | Best For | Avoid If |
|---|---|---|
| Portal | POC, Learning | Production |
| ALZ Bicep | Azure-only, Ops teams | Multi-cloud needs |
| CAF Terraform | Multi-cloud, TF teams | No TF experience |
| Pulumi | Developer-centric | Ops-heavy teams |
| Custom | Unique requirements | Time constraints |
Next Steps
Continue to Bicep Accelerator for a deep dive into ALZ Bicep implementation.