Cloud networking is the foundation everything else builds on. Get it wrong, and you’ll spend months refactoring. This guide covers VPC architecture, peering, transit connectivity, and hybrid cloud patterns across all three major providers.

Networking Concepts Compared

ConceptAWSGCPAzure
Virtual NetworkVPCVPCVNet
Subnet ScopeAZ-specificRegionalRegional
NAT GatewayPer-AZRegionalPer-subnet
PeeringNon-transitiveNon-transitiveNon-transitive
Transit HubTransit GatewayNetwork Connectivity CenterVirtual WAN
Private DNSRoute 53 ResolverCloud DNSPrivate DNS Zones

AWS VPC Architecture

Complete VPC with Terraform

# VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

# Public Subnets
resource "aws_subnet" "public" {
  count = 3

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "public-${data.aws_availability_zones.available.names[count.index]}"
    Tier = "public"
  }
}

# Private Subnets
resource "aws_subnet" "private" {
  count = 3

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-${data.aws_availability_zones.available.names[count.index]}"
    Tier = "private"
  }
}

# NAT Gateways (one per AZ for HA)
resource "aws_eip" "nat" {
  count  = 3
  domain = "vpc"

  tags = {
    Name = "nat-eip-${count.index}"
  }
}

resource "aws_nat_gateway" "main" {
  count = 3

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "nat-${data.aws_availability_zones.available.names[count.index]}"
  }
}

# Route Tables
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table" "private" {
  count  = 3
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "private-rt-${count.index}"
  }
}

# Route Table Associations
resource "aws_route_table_association" "public" {
  count          = 3
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count          = 3
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

VPC Peering

# Peering between two VPCs
resource "aws_vpc_peering_connection" "peer" {
  vpc_id        = aws_vpc.main.id
  peer_vpc_id   = aws_vpc.other.id
  peer_owner_id = var.peer_account_id  # For cross-account
  peer_region   = "us-west-2"          # For cross-region

  auto_accept = false  # Must be accepted manually for cross-account

  tags = {
    Name = "main-to-other"
  }
}

# Accepter side (in peer account/region)
resource "aws_vpc_peering_connection_accepter" "peer" {
  provider                  = aws.peer
  vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
  auto_accept               = true

  tags = {
    Name = "other-from-main"
  }
}

# Route to peer VPC
resource "aws_route" "to_peer" {
  count                     = 3
  route_table_id            = aws_route_table.private[count.index].id
  destination_cidr_block    = "10.1.0.0/16"
  vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
}

Transit Gateway for Hub-and-Spoke

resource "aws_ec2_transit_gateway" "main" {
  description                     = "Main transit gateway"
  default_route_table_association = "disable"
  default_route_table_propagation = "disable"
  dns_support                     = "enable"
  vpn_ecmp_support                = "enable"

  tags = {
    Name = "main-tgw"
  }
}

resource "aws_ec2_transit_gateway_vpc_attachment" "main" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.main.id
  subnet_ids         = aws_subnet.private[*].id

  transit_gateway_default_route_table_association = false
  transit_gateway_default_route_table_propagation = false

  tags = {
    Name = "main-vpc-attachment"
  }
}

resource "aws_ec2_transit_gateway_route_table" "main" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id

  tags = {
    Name = "main-tgw-rt"
  }
}

resource "aws_ec2_transit_gateway_route" "default" {
  destination_cidr_block         = "0.0.0.0/0"
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.egress.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.main.id
}

GCP VPC Architecture

VPC with Subnets

resource "google_compute_network" "main" {
  name                    = "main-vpc"
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
}

resource "google_compute_subnetwork" "private" {
  count = 3

  name          = "private-${var.regions[count.index]}"
  ip_cidr_range = cidrsubnet("10.0.0.0/16", 8, count.index)
  region        = var.regions[count.index]
  network       = google_compute_network.main.id

  private_ip_google_access = true

  log_config {
    aggregation_interval = "INTERVAL_5_SEC"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }

  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = cidrsubnet("10.100.0.0/16", 8, count.index)
  }

  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = cidrsubnet("10.200.0.0/16", 8, count.index)
  }
}

# Cloud NAT (regional, one per region)
resource "google_compute_router" "main" {
  count   = 3
  name    = "router-${var.regions[count.index]}"
  region  = var.regions[count.index]
  network = google_compute_network.main.id
}

resource "google_compute_router_nat" "main" {
  count  = 3
  name   = "nat-${var.regions[count.index]}"
  router = google_compute_router.main[count.index].name
  region = var.regions[count.index]

  nat_ip_allocate_option             = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"

  log_config {
    enable = true
    filter = "ERRORS_ONLY"
  }
}

# Firewall rules
resource "google_compute_firewall" "allow_internal" {
  name    = "allow-internal"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
  }

  allow {
    protocol = "udp"
  }

  allow {
    protocol = "icmp"
  }

  source_ranges = ["10.0.0.0/8"]
}

resource "google_compute_firewall" "allow_ssh_iap" {
  name    = "allow-ssh-iap"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  # IAP IP range
  source_ranges = ["35.235.240.0/20"]
}

VPC Peering

resource "google_compute_network_peering" "peer_main" {
  name                 = "main-to-other"
  network              = google_compute_network.main.id
  peer_network         = google_compute_network.other.id
  export_custom_routes = true
  import_custom_routes = true
}

resource "google_compute_network_peering" "peer_other" {
  name                 = "other-to-main"
  network              = google_compute_network.other.id
  peer_network         = google_compute_network.main.id
  export_custom_routes = true
  import_custom_routes = true
}

Shared VPC

# In host project
resource "google_compute_shared_vpc_host_project" "host" {
  project = var.host_project_id
}

# In service project
resource "google_compute_shared_vpc_service_project" "service" {
  host_project    = var.host_project_id
  service_project = var.service_project_id
}

# Grant access to specific subnets
resource "google_compute_subnetwork_iam_member" "service_account" {
  project    = var.host_project_id
  region     = "us-central1"
  subnetwork = "private-us-central1"
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:${var.service_account_email}"
}

Azure VNet Architecture

VNet with Subnets

resource "azurerm_virtual_network" "main" {
  name                = "main-vnet"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  address_space       = ["10.0.0.0/16"]

  tags = var.tags
}

resource "azurerm_subnet" "public" {
  count = 3

  name                 = "public-${count.index}"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [cidrsubnet("10.0.0.0/16", 8, count.index)]
}

resource "azurerm_subnet" "private" {
  count = 3

  name                 = "private-${count.index}"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [cidrsubnet("10.0.0.0/16", 8, count.index + 10)]

  service_endpoints = ["Microsoft.Storage", "Microsoft.Sql"]
}

# NAT Gateway
resource "azurerm_public_ip" "nat" {
  name                = "nat-pip"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  allocation_method   = "Static"
  sku                 = "Standard"
  zones               = ["1", "2", "3"]
}

resource "azurerm_nat_gateway" "main" {
  name                    = "main-nat"
  location                = azurerm_resource_group.main.location
  resource_group_name     = azurerm_resource_group.main.name
  sku_name                = "Standard"
  idle_timeout_in_minutes = 10
  zones                   = ["1"]
}

resource "azurerm_nat_gateway_public_ip_association" "main" {
  nat_gateway_id       = azurerm_nat_gateway.main.id
  public_ip_address_id = azurerm_public_ip.nat.id
}

resource "azurerm_subnet_nat_gateway_association" "private" {
  count          = 3
  subnet_id      = azurerm_subnet.private[count.index].id
  nat_gateway_id = azurerm_nat_gateway.main.id
}

# Network Security Group
resource "azurerm_network_security_group" "private" {
  name                = "private-nsg"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  security_rule {
    name                       = "AllowVNetInbound"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "VirtualNetwork"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyInternetInbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }
}

VNet Peering

resource "azurerm_virtual_network_peering" "main_to_other" {
  name                         = "main-to-other"
  resource_group_name          = azurerm_resource_group.main.name
  virtual_network_name         = azurerm_virtual_network.main.name
  remote_virtual_network_id    = azurerm_virtual_network.other.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  use_remote_gateways          = false
}

resource "azurerm_virtual_network_peering" "other_to_main" {
  name                         = "other-to-main"
  resource_group_name          = azurerm_resource_group.other.name
  virtual_network_name         = azurerm_virtual_network.other.name
  remote_virtual_network_id    = azurerm_virtual_network.main.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  use_remote_gateways          = false
}

Hybrid Connectivity

AWS Site-to-Site VPN

resource "aws_vpn_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-vpn-gateway"
  }
}

resource "aws_customer_gateway" "onprem" {
  bgp_asn    = 65000
  ip_address = var.onprem_gateway_ip
  type       = "ipsec.1"

  tags = {
    Name = "onprem-gateway"
  }
}

resource "aws_vpn_connection" "main" {
  vpn_gateway_id      = aws_vpn_gateway.main.id
  customer_gateway_id = aws_customer_gateway.onprem.id
  type                = "ipsec.1"
  static_routes_only  = false

  tags = {
    Name = "main-vpn"
  }
}

GCP Cloud VPN

resource "google_compute_ha_vpn_gateway" "main" {
  name    = "main-vpn-gateway"
  network = google_compute_network.main.id
  region  = "us-central1"
}

resource "google_compute_external_vpn_gateway" "onprem" {
  name            = "onprem-gateway"
  redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT"

  interface {
    id         = 0
    ip_address = var.onprem_gateway_ip
  }
}

resource "google_compute_vpn_tunnel" "main" {
  count = 2

  name                  = "vpn-tunnel-${count.index}"
  region                = "us-central1"
  vpn_gateway           = google_compute_ha_vpn_gateway.main.id
  peer_external_gateway = google_compute_external_vpn_gateway.onprem.id
  shared_secret         = var.vpn_shared_secret
  router                = google_compute_router.vpn.id

  vpn_gateway_interface           = count.index
  peer_external_gateway_interface = 0
}

Key Takeaways

  1. GCP subnets are regional — simpler than AWS AZ-specific subnets
  2. VPC peering is non-transitive everywhere — use transit hubs for complex topologies
  3. Plan CIDR ranges carefully — overlapping ranges break peering
  4. NAT gateways are expensive — consider alternatives for dev environments
  5. Use private endpoints — avoid sending traffic over the internet

“The network is the foundation. Spend time getting it right upfront — refactoring VPC architecture after applications are deployed is painful and risky.”