Use Case — US prod VNet in Azure
| Subsidiary | US (10.0.0.0/12) |
| Provider / region | Azure · West US 2 (westus2) |
| Environment | prod |
| VNet | Regional (vnet-us-prod-westus2), address space 10.8.0.0/19 |
| Subnets | 4 regional tier subnets (pub, app, data, mgmt) + platform subnets |
Each subnet is regional and automatically spans West US 2 zones 1, 2, and 3 — zone redundancy is achieved by deploying resources across zones (zonal VMs, zone-spread VMSS/AKS, zone-redundant services). "West US" (California) has no Availability Zones; West US 2 / West US 3 do.
1. Where this fits in the plan
US subsidiary 10.0.0.0/12
Azure 10.8.0.0/14
West US 2 10.8.0.0/16
prod env 10.8.0.0/18
VNet 10.8.0.0/19 <-- this use case
Each tier keeps the /22 block used across the AWS and GCP designs; the VNet is /19 to leave room for Azure's dedicated platform subnets.
2. Subnet plan
| Tier | Name | CIDR | Egress |
|---|---|---|---|
| Public | snet-us-prod-westus2-pub |
10.8.0.0/22 |
external LB / public IPs |
| App | snet-us-prod-westus2-app |
10.8.4.0/22 |
NAT Gateway |
| Data | snet-us-prod-westus2-data |
10.8.8.0/22 |
none (isolated) |
| Mgmt | snet-us-prod-westus2-mgmt |
10.8.12.0/22 |
NAT Gateway; Bastion access |
| Gateway | GatewaySubnet |
10.8.16.0/27 |
VPN / ExpressRoute gateway |
| Bastion | AzureBastionSubnet |
10.8.16.64/26 |
Azure Bastion |
| Firewall | AzureFirewallSubnet |
10.8.16.128/26 |
Azure Firewall |
AKS on app uses CNI Overlay: pods draw from a cluster-level overlay CIDR from the enterprise 100.64.0.0/10 block — pods 100.66.0.0/16, services 100.67.0.0/20 — set at cluster creation, not on the subnet.
VNet · vnet-us-prod-westus2 · 10.8.0.0/19 (West US 2)
│ regional — every subnet below spans zones 1 · 2 · 3
│
├─ pub 10.8.0.0/22
├─ app 10.8.4.0/22 (AKS overlay: pods 100.66.0.0/16 · svcs 100.67.0.0/20)
├─ data 10.8.8.0/22
├─ mgmt 10.8.12.0/22
│
└─ platform 10.8.16.0/24
├─ GatewaySubnet 10.8.16.0/27
├─ AzureBastionSubnet 10.8.16.64/26
└─ AzureFirewallSubnet 10.8.16.128/26
Usable hosts: Azure reserves 5 addresses per subnet, so a /22 yields 1,019 assignable addresses.3. Routing and access design
- pub — internet-facing via an external Load Balancer / public IPs. NSG allows 80/443 inbound and the Azure load-balancer probe.
- app and mgmt — private; outbound through a shared NAT Gateway. Mgmt reachable only via Azure Bastion.
- data — no NAT Gateway and an explicit outbound-internet deny in its NSG; reachable on DB ports only from
app. - NSGs are per tier and reference the
/22tier summaries.
4. Terraform implementation
###############################################################################
# US office — Azure West US 2 — prod VNet (pub / app / data / mgmt)
#
# US subsidiary 10.0.0.0/12
# Azure 10.8.0.0/14
# West US 2 10.8.0.0/16
# prod env 10.8.0.0/18
# VNet 10.8.0.0/19 <-- this file
#
# Azure subnets are REGIONAL and span all zones; one subnet per tier suffices.
# Zone spread is a resource property (zonal VM / zone-spread VMSS, AKS).
###############################################################################
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
variable "subscription_id" {
type = string
description = "Azure subscription ID for the US prod environment"
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
###############################################################################
# Locals — the addressing plan
###############################################################################
locals {
location = "westus2"
tags = {
subsidiary = "us"
provider = "azure"
region = "westus2"
environment = "prod"
managed_by = "terraform"
}
# tier key => CIDR (keys become the subnet name suffixes)
subnets = {
pub = "10.8.0.0/22"
app = "10.8.4.0/22"
data = "10.8.8.0/22"
mgmt = "10.8.12.0/22"
}
}
###############################################################################
# Resource group + VNet
###############################################################################
resource "azurerm_resource_group" "net" {
name = "rg-us-prod-westus2-network"
location = local.location
tags = local.tags
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-us-prod-westus2"
location = local.location
resource_group_name = azurerm_resource_group.net.name
address_space = ["10.8.0.0/19"]
tags = local.tags
}
###############################################################################
# Tier subnets (one per tier)
###############################################################################
resource "azurerm_subnet" "tier" {
for_each = local.subnets
name = "snet-us-prod-westus2-${each.key}"
resource_group_name = azurerm_resource_group.net.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [each.value]
}
###############################################################################
# Platform subnets (names are fixed/required by Azure)
###############################################################################
resource "azurerm_subnet" "gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.net.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.8.16.0/27"]
}
resource "azurerm_subnet" "bastion" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.net.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.8.16.64/26"]
}
resource "azurerm_subnet" "firewall" {
name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.net.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.8.16.128/26"]
}
###############################################################################
# NAT Gateway — outbound for the private tiers (app, mgmt)
###############################################################################
resource "azurerm_public_ip" "nat" {
name = "pip-natgw-us-prod-westus2"
location = local.location
resource_group_name = azurerm_resource_group.net.name
allocation_method = "Static"
sku = "Standard"
zones = ["1", "2", "3"] # zone-redundant
tags = local.tags
}
resource "azurerm_nat_gateway" "natgw" {
name = "natgw-us-prod-westus2"
location = local.location
resource_group_name = azurerm_resource_group.net.name
sku_name = "Standard"
tags = local.tags
}
resource "azurerm_nat_gateway_public_ip_association" "natgw" {
nat_gateway_id = azurerm_nat_gateway.natgw.id
public_ip_address_id = azurerm_public_ip.nat.id
}
resource "azurerm_subnet_nat_gateway_association" "app" {
subnet_id = azurerm_subnet.tier["app"].id
nat_gateway_id = azurerm_nat_gateway.natgw.id
}
resource "azurerm_subnet_nat_gateway_association" "mgmt" {
subnet_id = azurerm_subnet.tier["mgmt"].id
nat_gateway_id = azurerm_nat_gateway.natgw.id
}
###############################################################################
# Network Security Groups — one per tier
###############################################################################
resource "azurerm_network_security_group" "pub" {
name = "nsg-us-prod-westus2-pub"
location = local.location
resource_group_name = azurerm_resource_group.net.name
tags = local.tags
security_rule {
name = "allow-https-inbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["80", "443"]
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
security_rule {
name = "allow-lb-health-probe"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
}
}
resource "azurerm_network_security_group" "app" {
name = "nsg-us-prod-westus2-app"
location = local.location
resource_group_name = azurerm_resource_group.net.name
tags = local.tags
security_rule {
name = "allow-from-public-tier"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["443", "8080"]
source_address_prefix = "10.8.0.0/22" # pub tier
destination_address_prefix = "*"
}
security_rule {
name = "deny-internet-inbound"
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
}
resource "azurerm_network_security_group" "data" {
name = "nsg-us-prod-westus2-data"
location = local.location
resource_group_name = azurerm_resource_group.net.name
tags = local.tags
security_rule {
name = "allow-db-from-app"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["1433", "3306", "5432"]
source_address_prefix = "10.8.4.0/22" # app tier
destination_address_prefix = "*"
}
security_rule {
name = "deny-internet-outbound"
priority = 4000
direction = "Outbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "Internet"
}
}
resource "azurerm_network_security_group" "mgmt" {
name = "nsg-us-prod-westus2-mgmt"
location = local.location
resource_group_name = azurerm_resource_group.net.name
tags = local.tags
security_rule {
name = "allow-bastion-ssh-rdp"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["22", "3389"]
source_address_prefix = "10.8.16.64/26" # AzureBastionSubnet
destination_address_prefix = "*"
}
}
###############################################################################
# NSG -> subnet associations
###############################################################################
resource "azurerm_subnet_network_security_group_association" "pub" {
subnet_id = azurerm_subnet.tier["pub"].id
network_security_group_id = azurerm_network_security_group.pub.id
}
resource "azurerm_subnet_network_security_group_association" "app" {
subnet_id = azurerm_subnet.tier["app"].id
network_security_group_id = azurerm_network_security_group.app.id
}
resource "azurerm_subnet_network_security_group_association" "data" {
subnet_id = azurerm_subnet.tier["data"].id
network_security_group_id = azurerm_network_security_group.data.id
}
resource "azurerm_subnet_network_security_group_association" "mgmt" {
subnet_id = azurerm_subnet.tier["mgmt"].id
network_security_group_id = azurerm_network_security_group.mgmt.id
}
###############################################################################
# Outputs
###############################################################################
output "vnet_id" {
value = azurerm_virtual_network.vnet.id
}
output "tier_subnet_ids" {
description = "Map of tier => subnet id"
value = { for k, s in azurerm_subnet.tier : k => s.id }
}
output "platform_subnet_ids" {
value = {
gateway = azurerm_subnet.gateway.id
bastion = azurerm_subnet.bastion.id
firewall = azurerm_subnet.firewall.id
}
}
5. Notes
- Zone redundancy comes from deploying across zones 1/2/3 (zonal VMs, zone-spread VM Scale Sets, AKS node pools with
zones = [1,2,3], zone-redundant services); the zone-redundant NAT public IP keeps egress available during a zonal outage. - Splitting the app tier later. If you ever need more than one app subnet, subdivide the
10.8.4.0/22app block into/24s (e.g.app-a10.8.4.0/24,app-b10.8.5.0/24) without disturbing the other tiers. - Hard data isolation is enforced by no NAT Gateway plus the outbound-internet deny rule; for inspected egress instead, route
0.0.0.0/0throughAzureFirewallSubnetvia a UDR. - IPAM-driven allocation. CIDRs are explicit here for clarity; in production allocate them from your IPAM source of truth.