Use Case — Create the US prod VPC in AWS Oregon (us-west-2)

Subsidiary US (10.0.0.0/12)
Provider / region AWS · us-west-2 (Oregon)
Environment prod
VPC CIDR 10.1.0.0/20 (4,096 addresses)
Subnets 12 — 4 tiers × 3 availability zones

1. Where this VPC sits in the plan

The VPC inherits its range top-down from the enterprise IPAM hierarchy:

US subsidiary      10.0.0.0/12
  AWS              10.0.0.0/14
    us-west-2      10.1.0.0/16
      prod env     10.1.0.0/18
        VPC        10.1.0.0/20   <-- this use case

A /20 divides into exactly four /22 blocks — one per tier. Each /22 holds the three availability-zone /24 subnets plus one spare /24 for growth, so every tier is also a clean summary route.


2. Subnet plan

Twelve subnets, grouped by tier. The leftover /24 in each /22 (.3, .7, .11, .15) is reserved headroom.

Tier (/22 summary) Subnet AZ CIDR Default route
Public — 10.1.0.0/22 pub1 us-west-2a 10.1.0.0/24 Internet Gateway
pub2 us-west-2b 10.1.1.0/24 Internet Gateway
pub3 us-west-2c 10.1.2.0/24 Internet Gateway
App — 10.1.4.0/22 app1 us-west-2a 10.1.4.0/24 NAT (per-AZ)
app2 us-west-2b 10.1.5.0/24 NAT (per-AZ)
app3 us-west-2c 10.1.6.0/24 NAT (per-AZ)
Data — 10.1.8.0/22 data1 us-west-2a 10.1.8.0/24 none (isolated)
data2 us-west-2b 10.1.9.0/24 none (isolated)
data3 us-west-2c 10.1.10.0/24 none (isolated)
Mgmt — 10.1.12.0/22 mgmt1 us-west-2a 10.1.12.0/24 NAT (per-AZ)
mgmt2 us-west-2b 10.1.13.0/24 NAT (per-AZ)
mgmt3 us-west-2c 10.1.14.0/24 NAT (per-AZ)

Layout across the three zones:

VPC · us-west-2 (Oregon) · 10.1.0.0/20
│
├─ us-west-2a              ├─ us-west-2b              ├─ us-west-2c
│   pub1   10.1.0.0/24     │   pub2   10.1.1.0/24     │   pub3   10.1.2.0/24
│   app1   10.1.4.0/24     │   app2   10.1.5.0/24     │   app3   10.1.6.0/24
│   data1  10.1.8.0/24     │   data2  10.1.9.0/24     │   data3  10.1.10.0/24
│   mgmt1  10.1.12.0/24    │   mgmt2  10.1.13.0/24    │   mgmt3  10.1.14.0/24
Usable hosts: AWS reserves 5 addresses per subnet, so each /24 yields 251 assignable addresses.

3. Routing design

The route tables enforce the security posture of each tier:

  • Public — egress and ingress through an Internet Gateway. These subnets host the NAT gateways and any internet-facing load balancers.
  • App and Mgmt — private, outbound-only through a per-AZ NAT gateway. One NAT per AZ means a single AZ failure does not sever connectivity for the others.
  • Data — no default route at all. Databases are reachable only from within the VPC (and via peering/transit you explicitly add).

4. Terraform implementation

###############################################################################
# US office — AWS Oregon (us-west-2) — prod VPC
#
# Position in the enterprise IPAM hierarchy:
#   US subsidiary      10.0.0.0/12
#     AWS              10.0.0.0/14
#       us-west-2      10.1.0.0/16
#         prod env     10.1.0.0/18
#           VPC        10.1.0.0/20   <-- this file
#
# 12 subnets = 4 tiers x 3 AZs. Each tier is a /22 summary holding three /24s
# (one per AZ) plus a spare /24 for growth.
###############################################################################

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = local.region
}

###############################################################################
# Locals — the single place that defines the addressing plan
###############################################################################

locals {
  region   = "us-west-2"
  vpc_cidr = "10.1.0.0/20"

  # Common tags applied to every resource.
  tags = {
    subsidiary  = "us"
    provider    = "aws"
    region      = "us-west-2"
    environment = "prod"
    managed_by  = "terraform"
    ipam_plan   = "us-aws-usw2-prod"
  }

  # The three availability zones this VPC spans.
  azs = ["us-west-2a", "us-west-2b", "us-west-2c"]

  # All 12 subnets, keyed by name. CIDRs come straight from the plan above.
  #   tier       = public | app | data | mgmt
  #   public     -> route to Internet Gateway
  #   app, mgmt  -> route to per-AZ NAT gateway (outbound only)
  #   data       -> no default route (VPC-internal only)
  subnets = {
    pub1  = { cidr = "10.1.0.0/24",  az = "us-west-2a", tier = "public" }
    pub2  = { cidr = "10.1.1.0/24",  az = "us-west-2b", tier = "public" }
    pub3  = { cidr = "10.1.2.0/24",  az = "us-west-2c", tier = "public" }

    app1  = { cidr = "10.1.4.0/24",  az = "us-west-2a", tier = "app" }
    app2  = { cidr = "10.1.5.0/24",  az = "us-west-2b", tier = "app" }
    app3  = { cidr = "10.1.6.0/24",  az = "us-west-2c", tier = "app" }

    data1 = { cidr = "10.1.8.0/24",  az = "us-west-2a", tier = "data" }
    data2 = { cidr = "10.1.9.0/24",  az = "us-west-2b", tier = "data" }
    data3 = { cidr = "10.1.10.0/24", az = "us-west-2c", tier = "data" }

    mgmt1 = { cidr = "10.1.12.0/24", az = "us-west-2a", tier = "mgmt" }
    mgmt2 = { cidr = "10.1.13.0/24", az = "us-west-2b", tier = "mgmt" }
    mgmt3 = { cidr = "10.1.14.0/24", az = "us-west-2c", tier = "mgmt" }
  }

  # Convenience groupings.
  public_subnets  = { for k, v in local.subnets : k => v if v.tier == "public" }
  private_subnets = { for k, v in local.subnets : k => v if contains(["app", "mgmt"], v.tier) }
  data_subnets    = { for k, v in local.subnets : k => v if v.tier == "data" }
}

###############################################################################
# VPC
###############################################################################

resource "aws_vpc" "main" {
  cidr_block           = local.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(local.tags, { Name = "us-aws-usw2-prod-vpc" })
}

# To pull this CIDR from AWS IPAM instead of hardcoding it, drop cidr_block and use:
#   ipv4_ipam_pool_id   = aws_vpc_ipam_pool.us_aws_usw2.id
#   ipv4_netmask_length = 20

###############################################################################
# Subnets (all 12)
###############################################################################

resource "aws_subnet" "this" {
  for_each = local.subnets

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  # Only public subnets auto-assign public IPs.
  map_public_ip_on_launch = each.value.tier == "public"

  tags = merge(local.tags, {
    Name = "us-aws-usw2-prod-${each.key}"
    tier = each.value.tier
  })
}

###############################################################################
# Internet Gateway + public routing
###############################################################################

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.tags, { Name = "us-aws-usw2-prod-igw" })
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.tags, { Name = "us-aws-usw2-prod-rt-public" })
}

resource "aws_route" "public_default" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.main.id
}

resource "aws_route_table_association" "public" {
  for_each       = local.public_subnets
  subnet_id      = aws_subnet.this[each.key].id
  route_table_id = aws_route_table.public.id
}

###############################################################################
# NAT gateways — one per AZ, each living in that AZ's public subnet
###############################################################################

# Map AZ -> the public subnet key in that AZ (e.g. "us-west-2a" => "pub1").
locals {
  public_subnet_by_az = { for k, v in local.public_subnets : v.az => k }
}

resource "aws_eip" "nat" {
  for_each = toset(local.azs)
  domain   = "vpc"
  tags     = merge(local.tags, { Name = "us-aws-usw2-prod-eip-nat-${each.key}" })
}

resource "aws_nat_gateway" "this" {
  for_each      = toset(local.azs)
  allocation_id = aws_eip.nat[each.key].id
  subnet_id     = aws_subnet.this[local.public_subnet_by_az[each.key]].id

  tags       = merge(local.tags, { Name = "us-aws-usw2-prod-nat-${each.key}" })
  depends_on = [aws_internet_gateway.main]
}

###############################################################################
# Private routing (app + mgmt) — per-AZ route table to that AZ's NAT
###############################################################################

resource "aws_route_table" "private" {
  for_each = toset(local.azs)
  vpc_id   = aws_vpc.main.id
  tags     = merge(local.tags, { Name = "us-aws-usw2-prod-rt-private-${each.key}" })
}

resource "aws_route" "private_default" {
  for_each               = toset(local.azs)
  route_table_id         = aws_route_table.private[each.key].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.this[each.key].id
}

resource "aws_route_table_association" "private" {
  for_each       = local.private_subnets
  subnet_id      = aws_subnet.this[each.key].id
  route_table_id = aws_route_table.private[each.value.az].id
}

###############################################################################
# Data routing — isolated, no default route (VPC-internal only)
###############################################################################

resource "aws_route_table" "data" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.tags, { Name = "us-aws-usw2-prod-rt-data" })
}

resource "aws_route_table_association" "data" {
  for_each       = local.data_subnets
  subnet_id      = aws_subnet.this[each.key].id
  route_table_id = aws_route_table.data.id
}

###############################################################################
# Outputs
###############################################################################

output "vpc_id" {
  value = aws_vpc.main.id
}

output "subnet_ids" {
  description = "Map of subnet name => subnet id"
  value       = { for k, s in aws_subnet.this : k => s.id }
}

output "subnet_ids_by_tier" {
  description = "Subnet ids grouped by tier"
  value = {
    public = [for k, v in local.public_subnets : aws_subnet.this[k].id]
    app    = [for k, v in local.subnets : aws_subnet.this[k].id if v.tier == "app"]
    data   = [for k, v in local.data_subnets : aws_subnet.this[k].id]
    mgmt   = [for k, v in local.subnets : aws_subnet.this[k].id if v.tier == "mgmt"]
  }
}

5. Notes

  • IPAM-driven allocation. The CIDRs are hardcoded here so the plan is easy to verify. In production, switch the VPC to draw its /20 from the AWS IPAM pool (see the commented block on aws_vpc.main) so allocation is enforced rather than copied by hand.
  • NAT cost. Three NAT gateways carry an hourly charge plus per-GB data processing. For non-prod environments, collapse to a single shared NAT to save cost.
  • Growth headroom. The spare /24 in each tier (10.1.3.0/24, 10.1.7.0/24, 10.1.11.0/24, 10.1.15.0/24) absorbs a fourth AZ or tier expansion without disturbing the layout.
  • Next layers. Security groups and NACLs per tier (e.g. data accepts 5432/3306 only from the app tier) are the natural follow-on. The same module, re-pointed at a different /14, stamps out the Japan and UK regional VPCs.

Read more

SOP — Create and Manage Microsoft Azure for Enterprise

Version 1.0 Date 2026-06-22 Domain mptwork.com Model Identity rooted in the existing Microsoft Entra tenant; hierarchy of Management Groups → Subscriptions → Resource Groups → Resources Consoles Azure portal portal.azure.com · Entra admin center entra.microsoft.com Companion SOPs M365/Entra — sop-subscribe-manage-m365-mptwork.md, sop-entra-id-authentication-sso-mptwork.md · Network/IPAM — enterprise-ipam-reference-architecture.md · AWS

By admin

SOP — Create and Manage a Google Cloud (GCP) Account

Version 1.0 Date 2026-06-22 Domain mptwork.com Model GCP Organization rooted in Cloud Identity / Google Workspace for mptwork.com; hierarchy of Folders → Projects; workforce access via Google Groups, optionally federated to Microsoft Entra Companion SOPs Google Workspace — sop-subscribe-manage-google-workspace-mptwork.md · Entra auth & SSO — sop-entra-id-authentication-sso-mptwork.md · Network/IPAM — enterprise-ipam-reference-architecture.md

By admin