GCP Networking: Shared VPC vs VPC Peering for Multi-Project Organizations

GCP Network Topology Decisions That Stick

Google Cloud's networking forces an early architectural decision that's hard to change later: how do you connect projects? GCP gives two primary options -- Shared VPC and VPC Peering. Pick wrong, and you'll spend months migrating workloads. I've built both in production environments, and the right answer depends on how your organization operates more than how your technology works.

Shared VPC: Centralized Control

Shared VPC designates one host project that owns the network. Service projects attach to it and use subnets from the host's VPC. Your network team controls IP allocation, firewall rules, and routing from a single location. Application teams deploy into subnets they don't manage.

# Enable Shared VPC on the host project
gcloud compute shared-vpc enable host-project-id

# Attach a service project
gcloud compute shared-vpc associated-projects add service-project-id \
    --host-project=host-project-id

# Grant subnet-level access to a specific service account
gcloud projects add-iam-policy-binding host-project-id \
    --member="serviceAccount:service-12345@compute-system.iam.gserviceaccount.com" \
    --role="roles/compute.networkUser" \
    --condition='expression=resource.name.endsWith("platform-prod-us-central1"),title=subnet-restriction'

The condition on the IAM binding is important. Without it, the service account can deploy into any subnet in the Shared VPC. With it, you restrict each team to their designated subnets. This prevents the "one team accidentally deploys into another team's subnet" incident that I've seen happen twice.

Subnet Design for Shared VPC

IP planning matters significantly more in Shared VPC because all projects share the same address space. We allocate subnets by environment and team, with generous sizing because expanding a subnet requires recreating it:

resource "google_compute_subnetwork" "team_platform_prod" {
  name          = "platform-prod-us-central1"
  ip_cidr_range = "10.10.0.0/20"
  region        = "us-central1"
  network       = google_compute_network.shared_vpc.id
  project       = var.host_project_id

  secondary_ip_range {
    range_name    = "gke-pods"
    ip_cidr_range = "10.100.0.0/16"
  }
  secondary_ip_range {
    range_name    = "gke-services"
    ip_cidr_range = "10.200.0.0/20"
  }

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

Secondary IP ranges for GKE are the most commonly undersized allocation. Each GKE node consumes a /24 from the pod range by default (110 pods per node). A 50-node cluster needs at least a /18 for pods. Undersize this range, and nodes fail to schedule because there aren't enough pod IPs -- an error message that doesn't obviously point to the IP range exhaustion.

Firewall Management at Scale

Centralized firewall rules are the greatest strength and greatest headache of Shared VPC. Consistent enforcement -- no team opens port 22 to 0.0.0.0/0. But also a bottleneck -- every firewall change request goes through the network team's queue.

We mitigate the bottleneck with hierarchical firewall policies. Organization-level policies enforce non-negotiable rules (deny SSH from internet, deny all egress to known-bad IPs). Project-level policies let teams manage their own application-specific rules within guardrails.

resource "google_compute_organization_security_policy" "baseline" {
  display_name = "org-baseline-policy"
  type         = "FIREWALL"
}

resource "google_compute_organization_security_policy_rule" "deny_ssh_internet" {
  policy_id = google_compute_organization_security_policy.baseline.id
  action    = "deny"
  direction = "INGRESS"
  priority  = 1000

  match {
    config {
      src_ip_ranges = ["0.0.0.0/0"]
      layer4_config {
        ip_protocol = "tcp"
        ports       = ["22"]
      }
    }
  }
}

resource "google_compute_organization_security_policy_rule" "allow_iap_ssh" {
  policy_id = google_compute_organization_security_policy.baseline.id
  action    = "allow"
  direction = "INGRESS"
  priority  = 900

  match {
    config {
      src_ip_ranges = ["35.235.240.0/20"]
      layer4_config {
        ip_protocol = "tcp"
        ports       = ["22"]
      }
    }
  }
}

The IAP (Identity-Aware Proxy) rule allows SSH only through Google's IAP tunnel, which requires authentication. Direct SSH from the internet is blocked at the organization level where individual projects can't override it.

VPC Peering: Distributed Autonomy

VPC Peering connects separate VPCs so resources communicate using internal IPs. Each project owns its VPC, manages its own firewall rules, and allocates its own IP ranges independently.

# Create peering from project A to project B
gcloud compute networks peerings create peer-a-to-b \
    --network=vpc-project-a \
    --peer-project=project-b \
    --peer-network=vpc-project-b \
    --export-custom-routes \
    --import-custom-routes

# Both sides must create peering
gcloud compute networks peerings create peer-b-to-a \
    --project=project-b \
    --network=vpc-project-b \
    --peer-project=project-a \
    --peer-network=vpc-project-a \
    --export-custom-routes \
    --import-custom-routes

This gives teams full control over their networking. They create subnets, modify firewall rules, and configure Cloud NAT without filing tickets. For small teams or organizations where each team operates independently, this autonomy is valuable. The tradeoff: no centralized network policy enforcement, and coordination between teams requires communication rather than configuration.

The Peering Limit Problem

GCP limits VPC peering to 25 connections per VPC (recently raised from the original 15, but still a hard ceiling). In organizations with 30+ projects needing connectivity, you hit this limit. Worse, peering is non-transitive -- if project A peers with a hub VPC and project B peers with the same hub, A and B cannot communicate through the hub.

Workarounds exist but add complexity: Cloud VPN tunnels between the hub and spoke VPCs (transitive routing works through VPN), or Network Connectivity Center which provides a managed hub. Both cost more than Shared VPC and add latency. If you're building workarounds for peering limitations, that's a sign you should have chosen Shared VPC.

Private Service Access

Both models need Private Service Access for managed services like Cloud SQL, Memorystore, and Filestore. The configuration differs:

# Shared VPC: configure in the host project
gcloud compute addresses create google-managed-services \
    --project=host-project-id \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=20 \
    --network=shared-vpc

gcloud services vpc-peerings connect \
    --project=host-project-id \
    --service=servicenetworking.googleapis.com \
    --ranges=google-managed-services \
    --network=shared-vpc

With Shared VPC, you configure this once in the host project and all service projects benefit. With VPC Peering, each project configures its own private service access, duplicating the IP range allocation and the peering connection.

Decision Framework

Choose Shared VPC when you have a dedicated network team or platform team, need centralized IP management and policy enforcement, run GKE clusters with services communicating across team boundaries, or have more than 15 projects needing network connectivity.

Choose VPC Peering when teams need full networking autonomy and can manage their own security, you have fewer than 15 projects, workloads are self-contained without cross-project dependencies, or you're an early-stage company where centralized network management adds overhead without proportional benefit.

Migration Reality

Moving from VPC Peering to Shared VPC -- the more common migration direction -- takes months for a production environment. You can't convert a standalone VPC into a Shared VPC service project without recreating resources. The migration path: create the Shared VPC host, define subnets matching your needs, deploy new workloads in service projects, migrate existing workloads one service at a time, then decommission old VPCs and peering connections.

We migrated a 22-project organization in four months. The single biggest time sink was IP address conflicts between the old peered VPCs and the new Shared VPC subnets. Plan your IP allocation scheme carefully before starting, and document every existing IP range in use across all projects. The five hours spent on IP audit saves five weeks of debugging routing conflicts.