Use Case — Create the US prod VPC in GCP Oregon (us-west1)
| Subsidiary | US (10.0.0.0/12) |
| Provider / region | GCP · us-west1 (Oregon) |
| Environment | prod |
| VPC | Global, custom-mode (us-gcp-prod-vpc) |
| Region block | 10.4.0.0/16 (us-west1) |
| Subnets | 4 regional subnets — one per tier, each spanning all 3 zones |
1. How GCP differs from AWS
The same four-tier design maps onto GCP very differently, because of two platform facts:
- A VPC is global. One VPC spans every region; you do not create a VPC per region. Regional subnets are added to the single global VPC.
- A subnet is regional, not zonal. A regional subnet automatically covers every zone in the region (
us-west1-a,-b,-c). You distribute workloads across zones using regional managed instance groups or GKE node pools — all drawing from the same subnet.
The practical result: the design that needed 12 subnets in AWS (4 tiers × 3 AZs) needs only 4 subnets in GCP (4 tiers × 1 regional subnet). The zone spread is automatic and free.
| Aspect | AWS (us-west-2) | GCP (us-west1) |
|---|---|---|
| VPC scope | Regional | Global |
| Subnet scope | One per AZ | One per region (spans all zones) |
| Subnets for this design | 12 | 4 |
| Internet ingress/egress | Internet Gateway + NAT gateway per AZ | Default internet route + Cloud NAT (regional) + external IPs |
| Pod/container IPs | Subnet IPs (or 100.64/10 for EKS) |
Alias secondary ranges on the subnet |
| L3/L4 filtering | Security groups + NACLs | VPC firewall rules (by network tag / service account) |
| Reserved IPs per subnet | 5 | 4 |
2. Where this fits in the plan
The addressing inherits top-down exactly as in the reference architecture; only the per-VPC /20 tier collapses, because one global VPC serves the region rather than a discrete regional VPC block:
US subsidiary 10.0.0.0/12
GCP 10.4.0.0/14
us-west1 10.4.0.0/16
prod env 10.4.0.0/18
tiers 4 × /22 regional subnets <-- this use case
3. Subnet plan
Four regional subnets, one per tier, keeping the /22-per-tier scheme. GKE pod and service ranges are allocated as secondary alias ranges from the enterprise CGNAT block (100.64.0.0/10), per the reference architecture, so they never consume routable RFC 1918 space.
| Tier | Subnet name | Primary CIDR | Secondary ranges (GKE) | Egress |
|---|---|---|---|---|
| Public | us-gcp-usw1-prod-public |
10.4.0.0/22 |
— | external IP / external LB |
| App | us-gcp-usw1-prod-app |
10.4.4.0/22 |
pods 100.64.0.0/16, services 100.65.0.0/20 |
Cloud NAT |
| Data | us-gcp-usw1-prod-data |
10.4.8.0/22 |
— | none (isolated; PGA only) |
| Mgmt | us-gcp-usw1-prod-mgmt |
10.4.12.0/22 |
— | Cloud NAT |
Global VPC · us-gcp-prod-vpc
│
└─ us-west1 (Oregon) — every subnet below spans zones a · b · c
├─ public 10.4.0.0/22
├─ app 10.4.4.0/22 (pods 100.64.0.0/16 · services 100.65.0.0/20)
├─ data 10.4.8.0/22
└─ mgmt 10.4.12.0/22
The spare /22 space in the region (10.4.16.0/20 onward) holds future tiers or a second VPC environment; the next GCP region (e.g. us-east4) takes 10.5.0.0/16.
4. Routing and access design
GCP has no per-subnet route table and no internet-gateway resource. The default route to the internet exists at the VPC level, and what actually controls reachability is whether instances have external IPs, whether Cloud NAT is attached, and the firewall rules.
- Public — instances reach the internet directly via external IPs or sit behind an external load balancer. No Cloud NAT needed.
- App and Mgmt — private (no external IPs). Outbound internet goes through Cloud NAT, attached to a regional Cloud Router. App NAT covers the pod alias range too, so GKE pods can pull images.
- Data — excluded from Cloud NAT, so it has no usable internet egress despite the implied default route. Private Google Access is enabled so it can still reach Google APIs (e.g. Cloud Storage) privately. Add an egress-deny firewall rule if you want hard isolation.
- Firewall — default-deny ingress. Representative rules: allow intra-enterprise traffic from
10.0.0.0/8, allow IAP (35.235.240.0/20) to the mgmt tag for SSH/RDP, and allow GCP load-balancer health-check ranges to the public/app tiers.
5. Terraform implementation
###############################################################################
# US office — GCP Oregon (us-west1) — prod
#
# Position in the enterprise IPAM hierarchy:
# US subsidiary 10.0.0.0/12
# GCP 10.0.0.0/14 -> 10.4.0.0/14
# us-west1 10.4.0.0/16
# prod env 10.4.0.0/18
# tiers 4 x /22 regional subnets <-- this file
#
# A GCP VPC is GLOBAL and subnets are REGIONAL (they span all zones in the
# region automatically), so this is 4 subnets, not 12.
###############################################################################
terraform {
required_version = ">= 1.5"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
variable "project_id" {
type = string
description = "GCP project ID for the US prod environment"
}
provider "google" {
project = var.project_id
region = local.region
}
###############################################################################
# Locals — the addressing plan
###############################################################################
locals {
region = "us-west1" # Oregon
labels = {
subsidiary = "us"
provider = "gcp"
region = "us-west1"
environment = "prod"
managed_by = "terraform"
}
# Four regional subnets, one per tier.
# pga = enable Private Google Access (private tiers reach Google APIs)
# nat = route outbound internet through Cloud NAT
# nat_all_ranges = NAT the secondary (pod) ranges too, not just the primary
# secondary = GKE alias ranges, carved from the enterprise 100.64.0.0/10 block
subnets = {
public = {
cidr = "10.4.0.0/22"
pga = false
nat = false
nat_all_ranges = false
secondary = []
}
app = {
cidr = "10.4.4.0/22"
pga = true
nat = true
nat_all_ranges = true
secondary = [
{ name = "us-gcp-usw1-prod-app-pods", cidr = "100.64.0.0/16" },
{ name = "us-gcp-usw1-prod-app-svcs", cidr = "100.65.0.0/20" },
]
}
data = {
cidr = "10.4.8.0/22"
pga = true
nat = false
nat_all_ranges = false
secondary = []
}
mgmt = {
cidr = "10.4.12.0/22"
pga = true
nat = true
nat_all_ranges = false
secondary = []
}
}
}
###############################################################################
# Global VPC (custom subnet mode)
###############################################################################
resource "google_compute_network" "vpc" {
name = "us-gcp-prod-vpc"
auto_create_subnetworks = false
routing_mode = "GLOBAL" # dynamic routes propagate across regions
description = "US subsidiary, prod environment, global VPC"
}
###############################################################################
# Regional subnets (one per tier)
###############################################################################
resource "google_compute_subnetwork" "this" {
for_each = local.subnets
name = "us-gcp-usw1-prod-${each.key}"
ip_cidr_range = each.value.cidr
region = local.region
network = google_compute_network.vpc.id
private_ip_google_access = each.value.pga
dynamic "secondary_ip_range" {
for_each = each.value.secondary
content {
range_name = secondary_ip_range.value.name
ip_cidr_range = secondary_ip_range.value.cidr
}
}
}
###############################################################################
# Cloud Router + Cloud NAT — outbound for the private tiers (app, mgmt)
###############################################################################
resource "google_compute_router" "router" {
name = "us-gcp-usw1-prod-router"
region = local.region
network = google_compute_network.vpc.id
}
resource "google_compute_router_nat" "nat" {
name = "us-gcp-usw1-prod-nat"
router = google_compute_router.router.name
region = local.region
nat_ip_allocate_option = "AUTO_ONLY"
source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"
dynamic "subnetwork" {
for_each = { for k, v in local.subnets : k => v if v.nat }
content {
name = google_compute_subnetwork.this[subnetwork.key].id
source_ip_ranges_to_nat = (
subnetwork.value.nat_all_ranges
? ["ALL_IP_RANGES"] # include GKE pod/service ranges
: ["PRIMARY_IP_RANGE"]
)
}
}
}
###############################################################################
# Firewall rules (default-deny ingress is implicit)
###############################################################################
resource "google_compute_firewall" "allow_internal" {
name = "us-gcp-usw1-prod-allow-internal"
network = google_compute_network.vpc.id
direction = "INGRESS"
priority = 1000
# Allow traffic from anywhere in the enterprise supernet.
source_ranges = ["10.0.0.0/8"]
allow { protocol = "tcp" }
allow { protocol = "udp" }
allow { protocol = "icmp" }
}
resource "google_compute_firewall" "allow_iap" {
name = "us-gcp-usw1-prod-allow-iap-ssh-rdp"
network = google_compute_network.vpc.id
direction = "INGRESS"
priority = 1000
# Identity-Aware Proxy source range — admin access without public IPs.
source_ranges = ["35.235.240.0/20"]
target_tags = ["mgmt"]
allow {
protocol = "tcp"
ports = ["22", "3389"]
}
}
resource "google_compute_firewall" "allow_health_checks" {
name = "us-gcp-usw1-prod-allow-health-checks"
network = google_compute_network.vpc.id
direction = "INGRESS"
priority = 1000
# GCP load-balancer / health-check source ranges.
source_ranges = ["130.211.0.0/22", "35.191.0.0/16"]
target_tags = ["public", "app"]
allow { protocol = "tcp" }
}
###############################################################################
# Outputs
###############################################################################
output "network_id" {
value = google_compute_network.vpc.id
}
output "subnet_self_links" {
description = "Map of tier => subnet self link"
value = { for k, s in google_compute_subnetwork.this : k => s.self_link }
}
output "gke_secondary_ranges" {
description = "Named alias ranges for GKE on the app subnet"
value = {
pods = "us-gcp-usw1-prod-app-pods"
services = "us-gcp-usw1-prod-app-svcs"
}
}
6. Notes
- One subnet, all zones. Because subnets are regional, high availability across
us-west1-a/b/ccomes from how you place workloads (regional MIGs, GKE regional clusters), not from the subnet layout. - GKE alias ranges. The
podsandservicessecondary ranges are referenced by name when you create the GKE cluster (ip_allocation_policy). Sizing the pod range generously up front avoids a painful migration later —/16supports large clusters. - IPAM-driven allocation. As with AWS, the CIDRs are explicit here for clarity. In production, allocate them from your IPAM source of truth (NetBox or GCP-side tooling) so the global plan stays authoritative.
- Hard data isolation. The data tier has no Cloud NAT, but the VPC's implied default internet route still exists. For strict isolation add a low-priority egress-deny firewall rule targeting the data tag, leaving Private Google Access as the only outbound path.
- Next layers. Hierarchical firewall policies (org/folder level), the GKE cluster itself, and internal load balancers are the natural follow-ons. The same module, re-pointed at
10.5.0.0/16, builds a second GCP region under the same global VPC.