Chapter 9: Building Real GCP Infrastructure
Topic 57

Shared VPC

NetworkingIAM

Shared VPC lets one host project own the network — subnets, firewall rules, routes — while many service projects attach their VMs, GKE nodes, and Cloud Run connectors to that network without owning it. For Hatch this means hatch-net-host holds the global VPC and its us-central1 subnets, while hatch-app-prod and hatch-app-staging run workloads on those subnets.

This is the central-network-team pattern: one team controls the IP space and the firewalls, and app teams consume what they are granted. It is a day-one architectural decision, not something you retrofit. Moving a live VPC into a host project after the fact is disruptive, so the host-and-service split is a choice you make before the first workload lands.

Host vs Service Projects

A google_compute_shared_vpc_host_project resource marks hatch-net-host as the host. A google_compute_shared_vpc_service_project attaches each service project to it. After both are in place, a VM in hatch-app-prod can put its network interface directly on a subnet that lives in the host project, even though the subnet is not in its own project.

The split is clean: the host project holds every network object, and the service projects hold only the workloads. The Hatch app's compute, its Cloud Run services, and its GKE nodes all live in the service projects, but every byte of their traffic rides subnets that hatch-net-host owns and governs.

Designating the host and attaching a service project
resource "google_compute_shared_vpc_host_project" "host" {
  project = "hatch-net-host"
}

resource "google_compute_shared_vpc_service_project" "prod" {
  host_project    = google_compute_shared_vpc_host_project.host.project
  service_project = "hatch-app-prod"
}

Why Centralize the Network

When one team owns CIDR allocation, firewall policy, and on-prem connectivity, app teams cannot accidentally create overlapping ranges or punch open a firewall. The network stops being a free-for-all and becomes a governed resource with a single owner. That is the entire reason Shared VPC exists: to separate "who runs the app" from "who controls the network."

For an org the size of Hatch, this is the difference between a coherent address plan and a sprawl of one-off VPCs that nobody can peer later. The platform team plans the IP space once, in hatch-net-host, and every service project draws from it under explicit grants instead of inventing its own networking.

Subnet-Level IAM

You do not hand a service project the whole VPC. You grant roles/compute.networkUser on specific subnets to the service project's identity. Grant the us-central1 app subnet to hatch-app-prod and the staging subnet to hatch-app-staging, and each project can use exactly the subnet it is meant to and nothing else.

The granularity is the point. Grant networkUser at the host-project level instead of per subnet and every service project can use every subnet — which defeats the whole reason you centralized in the first place. Scope each grant to the one subnet the project needs.

Granting networkUser on a single subnet
resource "google_compute_subnetwork_iam_member" "prod_app" {
  project    = "hatch-net-host"
  region     = "us-central1"
  subnetwork = "app-uc1"          # this subnet only, not the whole VPC
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:hatch-app-run@hatch-app-prod.iam.gserviceaccount.com"
}

The Identities That Need Access

It is not only your own service accounts that need compute.networkUser on the subnet. The Google-managed service agents — the GKE service agent and the Cloud Services agent — also need it, so that managed products can wire a NIC into the shared subnet on your behalf. Forget this grant and a GKE cluster or a Cloud Run connector in the service project fails to provision with an opaque permissions error that points nowhere useful.

The pattern to remember: whenever a service project runs GKE or Cloud Run against a shared subnet, grant networkUser both to the workload's own service account and to the relevant Google service agent. The managed provisioning only succeeds when both identities can reach the subnet.

Project Hierarchy Fit

The ownership model lines up with the org chart when the host project sits in a platform/ folder under central control and the service projects live in apps/. The team that owns platform/ owns the network; the teams that own apps/ own their workloads. Network ownership and organizational ownership match instead of one giant project trying to do everything.

This alignment is what makes Shared VPC sustainable as the org grows. New app projects land in apps/ and attach to the existing host network under the platform team's grants, rather than each spinning up an isolated VPC that has to be peered together later. The hierarchy does the governing.

Shared VPC ownership
Host project
hatch-net-host owns the VPC, subnets, routes, and firewall rules. Lives in platform/.
Service projects
hatch-app-prod and hatch-app-staging run workloads on host subnets. Live in apps/.
Subnet-level IAM
roles/compute.networkUser granted per subnet to each service project's identity and service agents.
Standalone VPC vs Shared VPC vs Peering

Standalone VPC — owned and consumed by one project. Fine for a single app where one team runs both the network and the workload.

Shared VPC — one host project owns the network and many service projects consume its subnets with subnet-level IAM. Choose it when a central team must govern IP space and firewalls across many projects, as Hatch does.

VPC Peering — connects two separately-owned VPCs as equals with non-transitive routes. Choose it to join networks across orgs or teams that each keep ownership — not to share one network internally.

Common Mistakes
  • Granting roles/compute.networkUser at the host-project level instead of on the specific subnet — every service project can then use every subnet, which defeats the whole point of subnet-level control.
  • Forgetting to grant the GKE or Cloud Services service agent networkUser on the subnet, so a GKE cluster or Cloud Run connector in the service project fails to provision with an opaque permissions error.
  • Putting the network in an app's own project and later trying to retrofit Shared VPC — moving a live VPC into a host project is disruptive, so the host/service split is a day-one decision.
  • Confusing Shared VPC with peering and reaching for peering to let two Hatch projects share one subnet — peering joins two networks, it does not let a second project consume the first's subnets.
  • Letting app teams create firewall rules in their service projects — firewalls live in the host project, so a rule written in hatch-app-prod has no effect on the shared network.
Best Practices
  • Make hatch-net-host the host project from day one and keep all subnets, routes, and firewall rules there under the platform team.
  • Grant roles/compute.networkUser per subnet to each service project's identity, scoping hatch-app-prod and hatch-app-staging to only the subnets they need.
  • Grant the required Google service agents networkUser on the subnet whenever a service project runs GKE or Cloud Run, so managed provisioning succeeds.
  • Align the host project to the platform/ folder and service projects to apps/, so network ownership matches the org hierarchy.
  • Decide the host-and-service split before the first workload lands, since moving a live VPC into a host project later is disruptive.
Comparable tools AWS RAM-shared subnets / Transit Gateway — rough analog, very different ownership semantics Config Connector ComputeSharedVPCHostProject / ComputeSharedVPCServiceProject gcloud compute shared-vpc — the imperative form

Knowledge Check

Why is roles/compute.networkUser granted on specific subnets rather than at the host-project level?

  • A project-level grant lets every service project use every subnet, defeating the point of centralized control
  • Subnet-level grants are the only kind GCP IAM supports for networking
  • Project-level grants cost more in IAM API calls
  • Subnets cannot be referenced by a project-level binding at all, so a per-subnet grant is the only form GCP IAM will accept

A GKE cluster in hatch-app-prod fails to provision with a permissions error on the shared subnet. What is the likely cause?

  • The Google service agent (GKE or Cloud Services) was not granted networkUser on the subnet
  • The host project was never marked as a Shared VPC host
  • The subnet is regional rather than zonal
  • GKE cannot run in a service project under Shared VPC and must be placed directly in the host project instead

When should you reach for VPC peering instead of Shared VPC?

  • To connect two separately-owned VPCs as equals, where each side keeps ownership of its own network
  • To let a second Hatch service project consume the host project's existing subnet without re-creating its own network
  • Whenever a service project needs a subnet in a different region
  • Any time you want one team to govern IP space across many projects

Where must Hatch's firewall rules be defined under Shared VPC?

  • In the host project hatch-net-host, because a rule in a service project has no effect on the shared network
  • In each service project, so every app team owns and edits its own firewalls against the shared network independently
  • In whichever project the VM happens to run in
  • Firewall rules are not supported on a Shared VPC network

You got correct