VPC Peering: AWS vs GCP vs Azure Networking
Master cloud networking with VPC peering, transit gateways, and hybrid connectivity. Compare AWS VPC, GCP VPC, and Azure VNet architectures with practical examples.
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
| Concept | AWS | GCP | Azure |
|---|---|---|---|
| Virtual Network | VPC | VPC | VNet |
| Subnet Scope | AZ-specific | Regional | Regional |
| NAT Gateway | Per-AZ | Regional | Per-subnet |
| Peering | Non-transitive | Non-transitive | Non-transitive |
| Transit Hub | Transit Gateway | Network Connectivity Center | Virtual WAN |
| Private DNS | Route 53 Resolver | Cloud DNS | Private 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
- GCP subnets are regional — simpler than AWS AZ-specific subnets
- VPC peering is non-transitive everywhere — use transit hubs for complex topologies
- Plan CIDR ranges carefully — overlapping ranges break peering
- NAT gateways are expensive — consider alternatives for dev environments
- 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.”