Terraform Module
Deep dive into CAF Terraform for deploying Azure Landing Zones.
CAF Terraform Overview
The Cloud Adoption Framework (CAF) Terraform module provides enterprise-scale landing zone deployment.
Repository: Azure/terraform-azurerm-caf-enterprise-scale
Prerequisites
Terraform Setup
# Install Terraform
brew install terraform
# Or download from HashiCorp
# https://www.terraform.io/downloads
# Verify installation
terraform version
Azure Authentication
# Service Principal (recommended for CI/CD)
export ARM_CLIENT_ID="<client-id>"
export ARM_CLIENT_SECRET="<client-secret>"
export ARM_SUBSCRIPTION_ID="<subscription-id>"
export ARM_TENANT_ID="<tenant-id>"
# Or use Azure CLI
az login
Required Permissions
| Scope | Role | Purpose |
|---|---|---|
| Tenant | Owner | Management groups |
| Root MG | Policy Contributor | Policy definitions |
| Subscriptions | Owner | Deploy resources |
Basic Implementation
Project Structure
terraform-alz/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── backend.tf
├── locals.tf
├── lib/
│ ├── archetype_definitions/
│ └── policy_assignments/
└── environments/
├── dev.tfvars
└── prod.tfvars
providers.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.80"
}
azapi = {
source = "Azure/azapi"
version = "~> 1.10"
}
}
}
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 {}
}
provider "azurerm" {
alias = "identity"
subscription_id = var.identity_subscription_id
features {}
}
backend.tf
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate001"
container_name = "tfstate"
key = "alz.tfstate"
}
}
variables.tf
variable "root_id" {
type = string
description = "Root ID for the management group hierarchy"
default = "alz"
}
variable "root_name" {
type = string
description = "Root name for the management group hierarchy"
default = "Azure Landing Zones"
}
variable "default_location" {
type = string
description = "Default location for resources"
default = "eastus"
}
variable "connectivity_subscription_id" {
type = string
description = "Subscription ID for connectivity resources"
}
variable "management_subscription_id" {
type = string
description = "Subscription ID for management resources"
}
variable "identity_subscription_id" {
type = string
description = "Subscription ID for identity resources"
}
variable "deploy_connectivity_resources" {
type = bool
description = "Deploy connectivity resources (hub network, firewall, etc.)"
default = true
}
variable "deploy_management_resources" {
type = bool
description = "Deploy management resources (Log Analytics, Automation, etc.)"
default = true
}
variable "hub_address_space" {
type = list(string)
description = "Address space for hub network"
default = ["10.0.0.0/16"]
}
main.tf
data "azurerm_client_config" "current" {}
module "enterprise_scale" {
source = "Azure/caf-enterprise-scale/azurerm"
version = "~> 5.2"
providers = {
azurerm = azurerm
azurerm.connectivity = azurerm.connectivity
azurerm.management = azurerm.management
}
# Root configuration
root_parent_id = data.azurerm_client_config.current.tenant_id
root_id = var.root_id
root_name = var.root_name
default_location = var.default_location
# Subscription placement
subscription_id_connectivity = var.connectivity_subscription_id
subscription_id_identity = var.identity_subscription_id
subscription_id_management = var.management_subscription_id
# Feature flags
deploy_core_landing_zones = true
deploy_corp_landing_zones = true
deploy_online_landing_zones = true
deploy_sap_landing_zones = false
deploy_demo_landing_zones = false
deploy_connectivity_resources = var.deploy_connectivity_resources
deploy_identity_resources = false
deploy_management_resources = var.deploy_management_resources
# Configuration
configure_connectivity_resources = local.configure_connectivity_resources
configure_management_resources = local.configure_management_resources
# Custom archetypes
library_path = "${path.root}/lib"
}
locals.tf
locals {
# Connectivity configuration
configure_connectivity_resources = {
settings = {
hub_networks = [
{
enabled = true
config = {
address_space = var.hub_address_space
location = var.default_location
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 = var.default_location
}
}
dns = {
enabled = true
config = {
location = var.default_location
enable_private_link_by_service = {
azure_api_management = true
azure_app_configuration_stores = true
azure_arc = true
azure_automation_dscandhybridworker = true
azure_automation_webhook = true
azure_backup = true
azure_batch_account = true
azure_bot_service_bot = true
azure_bot_service_token = true
azure_cache_for_redis = true
azure_cache_for_redis_enterprise = true
azure_container_registry = true
azure_cosmos_db_cassandra = true
azure_cosmos_db_gremlin = true
azure_cosmos_db_mongodb = true
azure_cosmos_db_sql = true
azure_cosmos_db_table = true
azure_data_explorer = true
azure_data_factory = true
azure_data_factory_portal = true
azure_data_health_data_services = true
azure_data_lake_file_system_gen2 = true
azure_database_for_mariadb_server = true
azure_database_for_mysql_server = true
azure_database_for_postgresql_server = true
azure_digital_twins = true
azure_event_grid_domain = true
azure_event_grid_topic = true
azure_event_hubs_namespace = true
azure_file_sync = true
azure_hdinsights = true
azure_iot_dps = true
azure_iot_hub = true
azure_key_vault = true
azure_key_vault_managed_hsm = true
azure_kubernetes_service_management = true
azure_machine_learning_workspace = true
azure_managed_disks = true
azure_media_services = true
azure_migrate = true
azure_monitor = true
azure_purview_account = true
azure_purview_studio = true
azure_relay_namespace = true
azure_search_service = true
azure_service_bus_namespace = true
azure_site_recovery = true
azure_sql_database_sqlserver = true
azure_synapse_analytics_dev = true
azure_synapse_analytics_sql = true
azure_synapse_studio = true
azure_web_apps_sites = true
azure_web_apps_static_sites = true
cognitive_services_account = true
microsoft_power_bi = true
signalr = true
signalr_webpubsub = true
storage_account_blob = true
storage_account_file = true
storage_account_queue = true
storage_account_table = true
storage_account_web = true
}
private_link_locations = [var.default_location]
public_dns_zones = []
private_dns_zones = []
}
}
}
location = var.default_location
tags = local.tags
advanced = null
}
# Management configuration
configure_management_resources = {
settings = {
log_analytics = {
enabled = true
config = {
retention_in_days = 365
enable_monitoring_for_vm = true
enable_monitoring_for_vmss = true
enable_solution_for_agent_health_assessment = true
enable_solution_for_anti_malware = true
enable_solution_for_change_tracking = true
enable_solution_for_service_map = true
enable_solution_for_sql_assessment = true
enable_solution_for_sql_vulnerability_assessment = true
enable_solution_for_sql_advanced_threat_detection = true
enable_solution_for_updates = true
enable_solution_for_vm_insights = true
enable_sentinel = true
}
}
security_center = {
enabled = true
config = {
email_security_contact = "security@contoso.com"
enable_defender_for_apis = true
enable_defender_for_app_services = true
enable_defender_for_arm = true
enable_defender_for_containers = true
enable_defender_for_cosmosdbs = true
enable_defender_for_cspm = true
enable_defender_for_dns = true
enable_defender_for_key_vault = true
enable_defender_for_oss_databases = true
enable_defender_for_servers = true
enable_defender_for_servers_vulnerability_assessments = true
enable_defender_for_sql_servers = true
enable_defender_for_sql_server_vms = true
enable_defender_for_storage = true
}
}
}
location = var.default_location
tags = local.tags
advanced = null
}
tags = {
Environment = "Production"
ManagedBy = "Terraform"
Repository = "terraform-alz"
}
}
Custom Archetypes
Directory Structure
lib/
├── archetype_definitions/
│ ├── archetype_definition_custom_corp.json
│ └── archetype_definition_custom_online.json
├── policy_assignments/
│ └── policy_assignment_require_tags.json
└── policy_definitions/
└── policy_definition_require_tags.json
Custom Archetype Definition
// lib/archetype_definitions/archetype_definition_custom_corp.json
{
"custom_corp": {
"policy_assignments": [
"Deploy-Private-Endpoints",
"Require-Tags"
],
"policy_definitions": [],
"policy_set_definitions": [],
"role_definitions": [],
"archetype_config": {
"parameters": {},
"access_control": {}
}
}
}
Custom Policy Assignment
// lib/policy_assignments/policy_assignment_require_tags.json
{
"name": "Require-Tags",
"type": "Microsoft.Authorization/policyAssignments",
"apiVersion": "2022-06-01",
"properties": {
"description": "Require mandatory tags on all resources",
"displayName": "Require Mandatory Tags",
"policyDefinitionId": "${root_scope_resource_id}/providers/Microsoft.Authorization/policyDefinitions/Require-Tags",
"enforcementMode": "Default",
"parameters": {
"tagName": {
"value": "CostCenter"
}
}
}
}
State Management
Remote State Configuration
# 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
delete_retention_policy {
days = 90
}
container_delete_retention_policy {
days = 90
}
}
network_rules {
default_action = "Deny"
ip_rules = ["<your-ip>"]
}
}
resource "azurerm_storage_container" "state" {
name = "tfstate"
storage_account_name = azurerm_storage_account.state.name
container_access_type = "private"
}
State Locking
Azure Storage handles state locking automatically via blob leases.
CI/CD Pipeline
GitHub Actions
name: Terraform ALZ
on:
push:
branches: [main]
paths: ['terraform/**']
pull_request:
branches: [main]
paths: ['terraform/**']
env:
TF_VERSION: '1.6.0'
WORKING_DIR: './terraform'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
terraform-plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- 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: Terraform Init
working-directory: ${{ env.WORKING_DIR }}
run: terraform init
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_USE_OIDC: true
- name: Terraform Plan
working-directory: ${{ env.WORKING_DIR }}
run: terraform plan -out=tfplan -no-color
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_USE_OIDC: true
- name: Upload Plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: ${{ env.WORKING_DIR }}/tfplan
terraform-apply:
needs: terraform-plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Download Plan
uses: actions/download-artifact@v4
with:
name: tfplan
path: ${{ env.WORKING_DIR }}
- 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: Terraform Init
working-directory: ${{ env.WORKING_DIR }}
run: terraform init
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_USE_OIDC: true
- name: Terraform Apply
working-directory: ${{ env.WORKING_DIR }}
run: terraform apply -auto-approve tfplan
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_USE_OIDC: true
Quick Reference Card
| Component | Configuration |
|---|---|
| Module Source | Azure/caf-enterprise-scale/azurerm |
| State Backend | azurerm (Azure Storage) |
| Auth | OIDC (recommended) or Service Principal |
| Providers | azurerm (3 aliases: main, connectivity, management) |
Next Steps
Continue to Subscription Vending to learn about automated subscription provisioning.