Skip to main content

Deployment Options

Compare Portal, Bicep, Terraform, and Pulumi approaches for landing zone deployment.

Deployment Approaches Overview

Comparison Matrix

FeaturePortalALZ BicepCAF TerraformPulumi
Learning CurveLowMediumMediumMedium-High
CustomizationLimitedHighVery HighHighest
State ManagementN/AARMTerraform StatePulumi State
Multi-cloudNoNoYesYes
GitOps ReadyNoYesYesYes
Team PreferenceOperatorsAzure-nativePlatform teamsDevelopers
Microsoft SupportYesCommunityCommunityCommunity
Best ForPOC, LearningAzure-only shopsMulti-cloud/TF teamsDev-centric orgs

Option 1: Azure Portal

Deploy to Azure Button

The quickest way to deploy a reference landing zone:

[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](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

ProsCons
✅ 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

ApproachBest ForAvoid If
PortalPOC, LearningProduction
ALZ BicepAzure-only, Ops teamsMulti-cloud needs
CAF TerraformMulti-cloud, TF teamsNo TF experience
PulumiDeveloper-centricOps-heavy teams
CustomUnique requirementsTime constraints

Next Steps

Continue to Bicep Accelerator for a deep dive into ALZ Bicep implementation.