Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Azure Landing Zone with Terraform
Azure Landing Zone with Terraform
PROJECTAdvanced

Azure Landing Zone with Terraform

Deploy enterprise-ready Azure environment with hub-spoke network, Azure Firewall, Log Analytics, Defender for Cloud following Microsoft CAF best practices.

Dylan H.

Cloud Architecture

February 3, 2026
11 min read
8-12 hours

Tools & Technologies

TerraformAzure CLIVS CodeGit

Azure Landing Zone with Terraform

Deploy a production-ready Azure Landing Zone following Microsoft's Cloud Adoption Framework (CAF). This project implements hub-spoke networking, centralized security with Azure Firewall, comprehensive monitoring, and security controls.

Project Overview

What We're Building

┌─────────────────────────────────────────────────────────────────────┐
│                   Azure Landing Zone Architecture                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Management Group Hierarchy                                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Root Management Group                     │   │
│  │  ├── Platform                                                │   │
│  │  │   ├── Identity (Azure AD, Domain Controllers)            │   │
│  │  │   ├── Management (Monitoring, Automation)                │   │
│  │  │   └── Connectivity (Hub, Firewall, DNS)                  │   │
│  │  ├── Landing Zones                                          │   │
│  │  │   ├── Corp (Internal workloads)                          │   │
│  │  │   └── Online (Internet-facing)                           │   │
│  │  └── Sandbox (Development/Testing)                          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  Hub-Spoke Network Topology                                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                         ┌─────────┐                          │   │
│  │                         │   Hub   │                          │   │
│  │                         │  VNet   │                          │   │
│  │                         │10.0.0.0 │                          │   │
│  │                         └────┬────┘                          │   │
│  │                              │                               │   │
│  │              ┌───────────────┼───────────────┐               │   │
│  │              │               │               │               │   │
│  │         ┌────▼────┐    ┌─────▼─────┐   ┌─────▼─────┐        │   │
│  │         │  Spoke  │    │   Spoke   │   │   Spoke   │        │   │
│  │         │  Prod   │    │    Dev    │   │    DMZ    │        │   │
│  │         │10.1.0.0 │    │ 10.2.0.0  │   │ 10.3.0.0  │        │   │
│  │         └─────────┘    └───────────┘   └───────────┘        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Prerequisites

  • Azure subscription with Owner access
  • Terraform 1.5+ installed
  • Azure CLI installed and authenticated
  • Git for version control
  • Understanding of Azure networking concepts

Part 1: Foundation Setup

Step 1: Project Structure

azure-landing-zone/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── modules/
│   ├── hub-network/
│   ├── spoke-network/
│   ├── firewall/
│   ├── bastion/
│   ├── log-analytics/
│   └── policy/
├── policies/
│   ├── tagging/
│   ├── network/
│   └── security/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
└── README.md

Step 2: Configure Providers

# providers.tf
terraform {
  required_version = ">= 1.5.0"
 
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.80.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.45.0"
    }
  }
 
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstate"
    container_name       = "landing-zone"
    key                  = "terraform.tfstate"
  }
}
 
provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = false
    }
    key_vault {
      purge_soft_delete_on_destroy    = false
      recover_soft_deleted_key_vaults = true
    }
  }
}
 
provider "azuread" {}

Step 3: Define Variables

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "prod"
}
 
variable "location" {
  description = "Primary Azure region"
  type        = string
  default     = "eastus"
}
 
variable "location_secondary" {
  description = "Secondary Azure region for DR"
  type        = string
  default     = "westus2"
}
 
variable "hub_vnet_address_space" {
  description = "Address space for hub VNet"
  type        = list(string)
  default     = ["10.0.0.0/16"]
}
 
variable "spoke_vnets" {
  description = "Configuration for spoke VNets"
  type = map(object({
    address_space  = list(string)
    dns_servers    = list(string)
    subnets        = map(object({
      address_prefix = string
      nsg_rules      = list(any)
    }))
  }))
  default = {}
}
 
variable "tags" {
  description = "Default tags for all resources"
  type        = map(string)
  default = {
    Environment = "Production"
    ManagedBy   = "Terraform"
    Project     = "Landing-Zone"
  }
}

Part 2: Hub Network Module

Step 4: Hub Network Module

# modules/hub-network/main.tf
resource "azurerm_resource_group" "hub" {
  name     = "rg-hub-${var.environment}-${var.location}"
  location = var.location
  tags     = var.tags
}
 
resource "azurerm_virtual_network" "hub" {
  name                = "vnet-hub-${var.environment}-${var.location}"
  location            = azurerm_resource_group.hub.location
  resource_group_name = azurerm_resource_group.hub.name
  address_space       = var.address_space
  dns_servers         = var.dns_servers
 
  tags = var.tags
}
 
# Gateway Subnet (for VPN/ExpressRoute)
resource "azurerm_subnet" "gateway" {
  name                 = "GatewaySubnet"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [cidrsubnet(var.address_space[0], 8, 0)] # /24
}
 
# Azure Firewall Subnet
resource "azurerm_subnet" "firewall" {
  name                 = "AzureFirewallSubnet"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [cidrsubnet(var.address_space[0], 8, 1)] # /24
}
 
# Azure Bastion Subnet
resource "azurerm_subnet" "bastion" {
  name                 = "AzureBastionSubnet"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [cidrsubnet(var.address_space[0], 8, 2)] # /24
}
 
# Management Subnet
resource "azurerm_subnet" "management" {
  name                 = "snet-management"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [cidrsubnet(var.address_space[0], 8, 3)] # /24
}
 
# DNS Subnet
resource "azurerm_subnet" "dns" {
  name                 = "snet-dns"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [cidrsubnet(var.address_space[0], 8, 4)] # /24
 
  delegation {
    name = "dns-resolver"
    service_delegation {
      name = "Microsoft.Network/dnsResolvers"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action"
      ]
    }
  }
}

Step 5: Azure Firewall Module

# modules/firewall/main.tf
resource "azurerm_public_ip" "firewall" {
  name                = "pip-fw-${var.environment}-${var.location}"
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = "Static"
  sku                 = "Standard"
  zones               = ["1", "2", "3"]
 
  tags = var.tags
}
 
resource "azurerm_firewall_policy" "main" {
  name                = "fwpol-${var.environment}-${var.location}"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku                 = "Premium"
 
  dns {
    proxy_enabled = true
    servers       = var.dns_servers
  }
 
  threat_intelligence_mode = "Deny"
 
  intrusion_detection {
    mode = "Deny"
  }
 
  tags = var.tags
}
 
resource "azurerm_firewall" "main" {
  name                = "fw-${var.environment}-${var.location}"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku_name            = "AZFW_VNet"
  sku_tier            = "Premium"
  firewall_policy_id  = azurerm_firewall_policy.main.id
  zones               = ["1", "2", "3"]
 
  ip_configuration {
    name                 = "configuration"
    subnet_id            = var.firewall_subnet_id
    public_ip_address_id = azurerm_public_ip.firewall.id
  }
 
  tags = var.tags
}
 
# Network Rule Collection
resource "azurerm_firewall_policy_rule_collection_group" "network" {
  name               = "rcg-network-rules"
  firewall_policy_id = azurerm_firewall_policy.main.id
  priority           = 200
 
  network_rule_collection {
    name     = "AllowAzureServices"
    priority = 210
    action   = "Allow"
 
    rule {
      name                  = "AllowAzureCloud"
      protocols             = ["TCP", "UDP"]
      source_addresses      = ["10.0.0.0/8"]
      destination_addresses = ["AzureCloud"]
      destination_ports     = ["443", "445"]
    }
 
    rule {
      name                  = "AllowDNS"
      protocols             = ["TCP", "UDP"]
      source_addresses      = ["10.0.0.0/8"]
      destination_addresses = ["168.63.129.16"]
      destination_ports     = ["53"]
    }
  }
 
  network_rule_collection {
    name     = "AllowInterVnet"
    priority = 220
    action   = "Allow"
 
    rule {
      name                  = "SpokeToSpoke"
      protocols             = ["Any"]
      source_addresses      = ["10.0.0.0/8"]
      destination_addresses = ["10.0.0.0/8"]
      destination_ports     = ["*"]
    }
  }
}
 
# Application Rule Collection
resource "azurerm_firewall_policy_rule_collection_group" "application" {
  name               = "rcg-application-rules"
  firewall_policy_id = azurerm_firewall_policy.main.id
  priority           = 300
 
  application_rule_collection {
    name     = "AllowMicrosoft"
    priority = 310
    action   = "Allow"
 
    rule {
      name = "WindowsUpdate"
      protocols {
        type = "Https"
        port = 443
      }
      source_addresses  = ["10.0.0.0/8"]
      destination_fqdns = ["*.microsoft.com", "*.windowsupdate.com", "*.windows.com"]
    }
 
    rule {
      name = "AzureManagement"
      protocols {
        type = "Https"
        port = 443
      }
      source_addresses  = ["10.0.0.0/8"]
      destination_fqdns = ["*.azure.com", "*.azure-automation.net", "*.azure-api.net"]
    }
  }
}

Part 3: Spoke Networks and Peering

Step 6: Spoke Network Module

# modules/spoke-network/main.tf
resource "azurerm_resource_group" "spoke" {
  name     = "rg-spoke-${var.spoke_name}-${var.environment}"
  location = var.location
  tags     = var.tags
}
 
resource "azurerm_virtual_network" "spoke" {
  name                = "vnet-spoke-${var.spoke_name}-${var.environment}"
  location            = azurerm_resource_group.spoke.location
  resource_group_name = azurerm_resource_group.spoke.name
  address_space       = var.address_space
  dns_servers         = var.dns_servers
 
  tags = var.tags
}
 
# Dynamic subnet creation
resource "azurerm_subnet" "subnets" {
  for_each = var.subnets
 
  name                 = each.key
  resource_group_name  = azurerm_resource_group.spoke.name
  virtual_network_name = azurerm_virtual_network.spoke.name
  address_prefixes     = [each.value.address_prefix]
 
  dynamic "delegation" {
    for_each = each.value.delegation != null ? [each.value.delegation] : []
    content {
      name = delegation.value.name
      service_delegation {
        name    = delegation.value.service
        actions = delegation.value.actions
      }
    }
  }
}
 
# NSG for each subnet
resource "azurerm_network_security_group" "subnets" {
  for_each = var.subnets
 
  name                = "nsg-${each.key}-${var.spoke_name}"
  location            = azurerm_resource_group.spoke.location
  resource_group_name = azurerm_resource_group.spoke.name
 
  dynamic "security_rule" {
    for_each = each.value.nsg_rules
    content {
      name                       = security_rule.value.name
      priority                   = security_rule.value.priority
      direction                  = security_rule.value.direction
      access                     = security_rule.value.access
      protocol                   = security_rule.value.protocol
      source_port_range          = security_rule.value.source_port_range
      destination_port_range     = security_rule.value.destination_port_range
      source_address_prefix      = security_rule.value.source_address_prefix
      destination_address_prefix = security_rule.value.destination_address_prefix
    }
  }
 
  tags = var.tags
}
 
# Associate NSGs with subnets
resource "azurerm_subnet_network_security_group_association" "subnets" {
  for_each = var.subnets
 
  subnet_id                 = azurerm_subnet.subnets[each.key].id
  network_security_group_id = azurerm_network_security_group.subnets[each.key].id
}
 
# Peering to Hub
resource "azurerm_virtual_network_peering" "spoke_to_hub" {
  name                         = "peer-to-hub"
  resource_group_name          = azurerm_resource_group.spoke.name
  virtual_network_name         = azurerm_virtual_network.spoke.name
  remote_virtual_network_id    = var.hub_vnet_id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  use_remote_gateways          = var.use_remote_gateways
}
 
# Hub to Spoke peering
resource "azurerm_virtual_network_peering" "hub_to_spoke" {
  name                         = "peer-to-${var.spoke_name}"
  resource_group_name          = var.hub_resource_group_name
  virtual_network_name         = var.hub_vnet_name
  remote_virtual_network_id    = azurerm_virtual_network.spoke.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = true
  use_remote_gateways          = false
}
 
# Route table with firewall as next hop
resource "azurerm_route_table" "spoke" {
  name                = "rt-spoke-${var.spoke_name}"
  location            = azurerm_resource_group.spoke.location
  resource_group_name = azurerm_resource_group.spoke.name
 
  route {
    name                   = "to-firewall"
    address_prefix         = "0.0.0.0/0"
    next_hop_type          = "VirtualAppliance"
    next_hop_in_ip_address = var.firewall_private_ip
  }
 
  tags = var.tags
}
 
resource "azurerm_subnet_route_table_association" "spoke" {
  for_each = {
    for k, v in var.subnets : k => v
    if !contains(["GatewaySubnet", "AzureFirewallSubnet", "AzureBastionSubnet"], k)
  }
 
  subnet_id      = azurerm_subnet.subnets[each.key].id
  route_table_id = azurerm_route_table.spoke.id
}

Part 4: Monitoring and Security

Step 7: Log Analytics and Monitoring

# modules/log-analytics/main.tf
resource "azurerm_resource_group" "monitoring" {
  name     = "rg-monitoring-${var.environment}"
  location = var.location
  tags     = var.tags
}
 
resource "azurerm_log_analytics_workspace" "main" {
  name                = "log-${var.environment}-${var.location}"
  location            = azurerm_resource_group.monitoring.location
  resource_group_name = azurerm_resource_group.monitoring.name
  sku                 = "PerGB2018"
  retention_in_days   = 90
 
  tags = var.tags
}
 
# Enable solutions
resource "azurerm_log_analytics_solution" "security" {
  solution_name         = "Security"
  location              = azurerm_resource_group.monitoring.location
  resource_group_name   = azurerm_resource_group.monitoring.name
  workspace_resource_id = azurerm_log_analytics_workspace.main.id
  workspace_name        = azurerm_log_analytics_workspace.main.name
 
  plan {
    publisher = "Microsoft"
    product   = "OMSGallery/Security"
  }
}
 
resource "azurerm_log_analytics_solution" "network" {
  solution_name         = "NetworkMonitoring"
  location              = azurerm_resource_group.monitoring.location
  resource_group_name   = azurerm_resource_group.monitoring.name
  workspace_resource_id = azurerm_log_analytics_workspace.main.id
  workspace_name        = azurerm_log_analytics_workspace.main.name
 
  plan {
    publisher = "Microsoft"
    product   = "OMSGallery/NetworkMonitoring"
  }
}
 
# Diagnostic settings for firewall
resource "azurerm_monitor_diagnostic_setting" "firewall" {
  name                       = "diag-firewall"
  target_resource_id         = var.firewall_id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
 
  enabled_log {
    category = "AzureFirewallApplicationRule"
  }
 
  enabled_log {
    category = "AzureFirewallNetworkRule"
  }
 
  enabled_log {
    category = "AzureFirewallDnsProxy"
  }
 
  metric {
    category = "AllMetrics"
    enabled  = true
  }
}

Step 8: Azure Policy Module

# modules/policy/main.tf
 
# Require tags on resource groups
resource "azurerm_policy_definition" "require_tags" {
  name         = "require-tags-on-rg"
  policy_type  = "Custom"
  mode         = "All"
  display_name = "Require specified tags on resource groups"
 
  policy_rule = jsonencode({
    if = {
      allOf = [
        {
          field  = "type"
          equals = "Microsoft.Resources/subscriptions/resourceGroups"
        },
        {
          field  = "[concat('tags[', parameters('tagName'), ']')]"
          exists = "false"
        }
      ]
    }
    then = {
      effect = "deny"
    }
  })
 
  parameters = jsonencode({
    tagName = {
      type = "String"
      metadata = {
        displayName = "Tag Name"
        description = "Name of the tag to require"
      }
    }
  })
}
 
# Allowed locations
resource "azurerm_policy_assignment" "allowed_locations" {
  name                 = "allowed-locations"
  scope                = var.scope
  policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c"
  display_name         = "Allowed locations"
 
  parameters = jsonencode({
    listOfAllowedLocations = {
      value = var.allowed_locations
    }
  })
}
 
# Enable Defender for Cloud
resource "azurerm_security_center_subscription_pricing" "defender" {
  for_each = toset([
    "VirtualMachines",
    "SqlServers",
    "AppServices",
    "StorageAccounts",
    "KeyVaults",
    "Dns",
    "Arm"
  ])
 
  tier          = "Standard"
  resource_type = each.value
}

Part 5: Main Deployment

Step 9: Main Configuration

# main.tf
locals {
  spoke_configs = {
    prod = {
      address_space = ["10.1.0.0/16"]
      subnets = {
        "snet-web" = {
          address_prefix = "10.1.1.0/24"
          delegation     = null
          nsg_rules = [
            {
              name                       = "AllowHTTPS"
              priority                   = 100
              direction                  = "Inbound"
              access                     = "Allow"
              protocol                   = "Tcp"
              source_port_range          = "*"
              destination_port_range     = "443"
              source_address_prefix      = "*"
              destination_address_prefix = "*"
            }
          ]
        }
        "snet-app" = {
          address_prefix = "10.1.2.0/24"
          delegation     = null
          nsg_rules      = []
        }
        "snet-data" = {
          address_prefix = "10.1.3.0/24"
          delegation     = null
          nsg_rules      = []
        }
      }
    }
    dev = {
      address_space = ["10.2.0.0/16"]
      subnets = {
        "snet-dev" = {
          address_prefix = "10.2.1.0/24"
          delegation     = null
          nsg_rules      = []
        }
      }
    }
  }
}
 
# Hub Network
module "hub_network" {
  source = "./modules/hub-network"
 
  environment   = var.environment
  location      = var.location
  address_space = var.hub_vnet_address_space
  dns_servers   = ["168.63.129.16"]
  tags          = var.tags
}
 
# Azure Firewall
module "firewall" {
  source = "./modules/firewall"
 
  environment         = var.environment
  location            = var.location
  resource_group_name = module.hub_network.resource_group_name
  firewall_subnet_id  = module.hub_network.firewall_subnet_id
  dns_servers         = ["168.63.129.16"]
  tags                = var.tags
}
 
# Spoke Networks
module "spoke_networks" {
  source   = "./modules/spoke-network"
  for_each = local.spoke_configs
 
  spoke_name              = each.key
  environment             = var.environment
  location                = var.location
  address_space           = each.value.address_space
  subnets                 = each.value.subnets
  dns_servers             = [module.firewall.private_ip_address]
  hub_vnet_id             = module.hub_network.vnet_id
  hub_vnet_name           = module.hub_network.vnet_name
  hub_resource_group_name = module.hub_network.resource_group_name
  firewall_private_ip     = module.firewall.private_ip_address
  use_remote_gateways     = false
  tags                    = var.tags
}
 
# Monitoring
module "monitoring" {
  source = "./modules/log-analytics"
 
  environment = var.environment
  location    = var.location
  firewall_id = module.firewall.id
  tags        = var.tags
}
 
# Bastion
module "bastion" {
  source = "./modules/bastion"
 
  environment         = var.environment
  location            = var.location
  resource_group_name = module.hub_network.resource_group_name
  subnet_id           = module.hub_network.bastion_subnet_id
  tags                = var.tags
}

Verification Checklist

Infrastructure:

  • Hub VNet deployed with all subnets
  • Azure Firewall operational
  • Spoke VNets created and peered
  • Route tables configured
  • NSGs applied

Security:

  • Firewall rules configured
  • Defender for Cloud enabled
  • Azure Policy assigned
  • Diagnostic settings configured

Networking:

  • DNS resolution working
  • Spoke-to-spoke via firewall
  • Internet egress via firewall
  • Bastion connectivity working

Resources

  • Azure Landing Zone Accelerator
  • Cloud Adoption Framework
  • Hub-Spoke Reference Architecture

Questions? Reach out in our community Discord!

#Azure#Terraform#IaC#Cloud Architecture#Security#Landing Zone

Related Articles

CI/CD Pipeline with GitHub Actions and Azure

Build a secure CI/CD pipeline with GitHub Actions deploying to Azure. Covers build, test, security scanning (SAST/DAST), and deployment with OIDC...

11 min read

Keycloak SSO: Self-Hosted Identity Provider for Your Homelab

Deploy Keycloak with Docker Compose and PostgreSQL to build a centralised single sign-on platform for your homelab services, with OIDC integration for...

11 min read

Build a Centralized Log Management System with Loki and

Deploy a scalable log management solution using Grafana Loki. Learn to aggregate, search, and alert on logs from your entire infrastructure.

6 min read
Back to all Projects