Every major cloud hands you a private virtual network, an address range, subnets and a packet filter. GCP hands you those too, and then makes one structural choice that nobody else makes: the network object itself is global. A VPC in GCP has no region field. It is a project-level resource that exists everywhere at once, and regions enter the model only when you carve a subnet out of it. That single decision propagates into routing, into how firewall rules are addressed, into what a shared network can span, and into a set of failure modes with no analogue on the other platforms. This article is about the differences. The vendor-neutral model that sits underneath - CIDR planning arithmetic, subnets as failure domains, route tables, stateful versus stateless filtering, split-horizon DNS, flow logs, MTU, tunnels versus circuits - is developed in Cloud networking architecture, and everything below assumes it.

A global network with regional subnets

A VPC is a thin container. It carries a name, a subnet mode, a BGP routing mode, an MTU and a collection of subnets, routes and firewall rules. It does not carry a location. The subnet is the only object in the model that is bound to a region, and it is bound to exactly one.

The practical consequence is the thing worth internalising. Put a VM in a subnet in us-east4 and another in a subnet in asia-southeast1, in the same VPC, and they reach each other on RFC1918 addresses immediately. There is no peering to establish, no transit object to provision, no gateway to attach and no route you wrote. The subnet route for every subnet in the network is present in every region's forwarding state from the moment the subnet exists. Traffic rides Google's own backbone rather than leaving and re-entering the network at each end.

What you pay for that is scope. Because the VPC is one administrative object, a firewall rule is network-wide and a route with a wide destination applies estate-wide. The containment that a regional-network model gives you accidentally - a mistake in one region simply cannot reach another because the networks are separate objects - has to be created deliberately here, either by running separate VPCs or by narrowing rules with the selectors described below.

One clarification, because the word "global" invites it: the control plane and the address space are global, not the failure domain. Subnets remain regional and instances remain zonal. A regional outage does not remove the VPC, but it removes everything living in that region's subnets, and a global VPC does nothing to change that.

GCP VPC structureVPC (global)spans regionsSubnets (regional)one per regionFirewall rules (global)apply cluster-wideGlobal scope simplifies multi-region; firewall rules cluster-wide by tags/labels
Global VPC layout.
Advertisement

Auto mode, custom mode, and the range you inherit

Every new project gets a network called default in auto mode. Auto mode creates one subnet in every region automatically, drawn from a predetermined slice of 10.128.0.0/9, and it keeps creating them: when Google launches a new region, your network grows a subnet there with no change ticket, no review and no entry in your address plan.

Two things follow. First, you did not choose those ranges, which means they were chosen without reference to your corporate address space or to any network you might later want to connect to - and peering refuses overlapping ranges outright, so an inherited collision is discovered at the moment it is most expensive to fix. Second, an address footprint that changes on Google's release schedule is not something you can hand to a network team as a stable document.

Custom mode is the opposite: the network starts empty and every subnet is an explicit act with an explicit range in an explicit region. Regions you do not use consume nothing. Conversion runs one way only - auto to custom is supported, custom to auto is not - so the cheap move is to create custom-mode networks from the start and suppress the default network at the organisation level with the compute.skipDefaultNetworkCreation constraint, rather than converting hundreds of projects later.

Subnets grow in place

A subnet's primary IPv4 range can be expanded by shortening its prefix while the subnet is carrying live traffic. A /24 becomes a /20 with one API call. No subnet is recreated, no instance is stopped, no address is renumbered and no route is rewritten by hand. The constraint is that the new range must be a strict superset of the old one and must not collide with any other subnet in the same VPC or with any range reachable through a peering. Shrinking is not offered, so expansion is a ratchet.

This genuinely softens the day-one-forever framing that address planning carries elsewhere. What it does not soften is the layout. Expansion needs room immediately above the existing block, so if the adjacent space was handed to the next subnet in the sequence there is nothing to grow into, and the fact that the operation is easy is irrelevant. The planning discipline on GCP is therefore about reserving gaps rather than about guessing final sizes: allocate a /16 per region on paper, hand out a /22 from the bottom of it, and let each subnet expand into the deliberately empty space above it.

Secondary ranges are separate CIDRs attached to the same subnet object. They can be added after creation and expanded the same way, which matters for the alias-IP case discussed later.

One filtering layer, not two

GCP has exactly one packet filter for VPC traffic: VPC firewall rules, enforced at each instance's virtual NIC by the hypervisor host, stateful in both directions. There is no stateless, subnet-attached, numbered ACL layer. The two-layer debugging problem - a stateful layer and a stateless one that must independently permit the return packet, producing a connection that half-opens and then hangs - is set out at length in the vendor-neutral article, and on GCP it simply does not arise. There is no object on which you can forget the ephemeral reply port.

You give something up along with it. The stateless subnet layer was also the coarse control: one object that constrained everything in a subnet no matter what an instance owner did to their own rules. GCP replaces that with hierarchical firewall policies, which sit above the project rather than below the instance - a better place for a guardrail, but a different one, and if you migrate a design that relied on subnet ACLs for blast-radius control you have to rebuild that control at the organisation level rather than expecting to find it in the network.

A rule is a direction (ingress or egress), an action (allow or deny), a priority from 0 to 65535 defaulting to 1000, a set of protocols and ports, a source or destination, and a target selector. Every VPC also carries two implied rules at priority 65535 that are not objects and cannot be deleted: allow all egress and deny all ingress. A fresh network therefore lets everything out and nothing in, and the egress half is the one people forget when they write a data-exfiltration threat model.

Precedence is lowest number wins. When two rules of equal priority disagree, deny wins. That is a genuinely useful property rather than trivia: you can delegate rule authorship at priority 1000 and keep a deny at 1000 that the delegates cannot out-vote without explicitly lowering their number, which is a visible, reviewable act.

One vocabulary warning. "Ingress" and "egress" here mean packet direction at a NIC. They are not the ingress and egress rules of a VPC Service Controls perimeter, which govern API calls crossing a service boundary and are evaluated by Google's API front ends, not by the network. The two mechanisms share nothing but the words.

Selectors: network tags and service accounts instead of CIDRs

Here is the part that reads strangest coming from another cloud. A firewall rule's target need not be an address at all. A rule can apply to every instance carrying a given network tag, or to every instance running as a given service account. On the ingress side, the source can be expressed the same way: source tags or source service accounts, both restricted to instances in the same network.

The value is the usual one - a rule reading "the API tier may reach the database tier" stays correct across autoscaling, instance replacement and redeploys without anyone editing it, and there is no address list to drift out of date. But the service-account form adds something a group reference does not have: it is IAM-gated. Attaching a service account to an instance requires iam.serviceAccounts.actAs on that service account, which is a permission somebody has to grant deliberately. There is no equivalent gate on tags. Anyone holding compute.instances.setTags, which arrives bundled in ordinary instance-administration roles, can add db-client to a VM they control and inherit everything that tag was ever permitted to reach. A service-account selector states an authorization fact; a tag selector states a convention.

Tags and service accounts cannot be mixed within a role in the same rule. A rule targets by tag or by service account, never both, so moving an estate from one to the other is a rule-by-rule rewrite rather than a gradual widening - budget for it rather than discovering it mid-migration.

The quiet failure is worth stating on its own. Tags are free-form strings registered nowhere. Write a rule whose target tag is web-serve when the instances carry web-server and the API accepts it without complaint. The rule is created, it is syntactically valid, it appears in the console exactly as intended, and it matches zero instances for the rest of its life. Nothing errors and nothing warns. The symptom is a connection refused with no rule apparently blocking it, and rule ordering cannot save you because the rule that should have matched was never in the evaluated set. Audit by asking which instances a rule actually applies to, never by reading what the rule says.

Advertisement

Hierarchical firewall policies at the org and folder level

A hierarchical firewall policy attaches to the organisation or to a folder rather than to a VPC. It therefore covers every network in every project underneath it, including projects created next year and networks whose owners never see the policy and cannot edit it.

Evaluation runs top down: the organisation policy first, then folder policies from the outermost folder inward, then the network's own VPC firewall rules. A policy rule takes one of three actions - allow, deny, or goto_next. The third is the delegation verb. It means "this level has considered this traffic and is deliberately deferring the decision downward", which is how you carve an exception without the lower level being able to re-open the thing you actually care about.

The pattern that earns its keep is a small set of non-negotiables at the top: deny SSH and RDP from 0.0.0.0/0 across the entire organisation, deny the handful of protocols that have no legitimate use in your estate, and goto_next for internal ranges so project teams retain full control over everything that is genuinely their business. A project owner cannot override the denies, because the policy is not in their project. This is the coarse control the missing ACL layer used to provide, relocated to a level where it is an organisational statement rather than a subnet artefact.

Policies can be scoped by target service account and by network, and one policy object can be associated with several folders, so a guardrail set is a single reviewable artefact rather than a copy per project that drifts.

What this looks like in gcloud

# Custom mode: nothing exists until you say so. Global BGP routing so that
# one hybrid landing zone serves every region in the network.
gcloud compute networks create prod-net \
    --subnet-mode=custom \
    --bgp-routing-mode=global

# Primary range plus two named secondary ranges for a VPC-native cluster.
gcloud compute networks subnets create prod-use4 \
    --network=prod-net --region=us-east4 \
    --range=10.20.0.0/22 \
    --secondary-range=pods=10.60.0.0/14,services=10.64.0.0/20 \
    --enable-private-ip-google-access \
    --enable-flow-logs

# Grow the primary range later, in place, with the VMs still running.
# 10.20.0.0/22 -> 10.20.0.0/20. Superset only; there is no shrink.
gcloud compute networks subnets expand-ip-range prod-use4 \
    --region=us-east4 --prefix-length=20

# Identity as the selector rather than an address list.
gcloud compute firewall-rules create allow-api-to-db \
    --network=prod-net --direction=INGRESS --action=allow \
    --priority=1000 --rules=tcp:5432 \
    --source-service-accounts=api@prod-apps.iam.gserviceaccount.com \
    --target-service-accounts=db@prod-apps.iam.gserviceaccount.com

# Catch-all deny at 1100, NOT at 1000. At equal priority a deny beats an
# allow, so a 1000 deny here would also kill the rule above.
gcloud compute firewall-rules create deny-db-everything-else \
    --network=prod-net --direction=INGRESS --action=deny \
    --priority=1100 --rules=tcp:5432 --source-ranges=0.0.0.0/0 \
    --target-service-accounts=db@prod-apps.iam.gserviceaccount.com

# Guardrails one level up, where no project owner can edit them.
gcloud compute firewall-policies create \
    --organization=ORG_ID --short-name=org-guardrails

gcloud compute firewall-policies rules create 100 \
    --firewall-policy=org-guardrails --organization=ORG_ID \
    --direction=INGRESS --action=deny \
    --src-ip-ranges=0.0.0.0/0 --layer4-configs=tcp:22,tcp:3389

# Internal traffic is the projects' business: defer, do not decide.
gcloud compute firewall-policies rules create 200 \
    --firewall-policy=org-guardrails --organization=ORG_ID \
    --direction=INGRESS --action=goto_next \
    --src-ip-ranges=10.0.0.0/8 --layer4-configs=all

gcloud compute firewall-policies associations create \
    --firewall-policy=org-guardrails --folder=FOLDER_ID

Note what is absent: there is no route-table object to create and no association between a route table and a subnet. The network has one route collection, and selectivity comes from tags on the routes themselves.

Routes are first-class objects with priority and tags

Creating a subnet creates a system-generated subnet route that cannot be deleted while the subnet exists and outranks everything else for its own range. Alongside it, a new network gets a default route for 0.0.0.0/0 pointing at the default internet gateway, at priority 1000. That one you can delete, and deleting it is the standard move for forcing all egress through an inspection appliance or for making a network genuinely private.

Custom static routes take a destination range, a priority, and a next hop: an instance, a specific NIC address on an instance, an internal passthrough load balancer, a VPN tunnel, or the default internet gateway. Selection is longest-prefix-match first, then priority, with equal-cost paths sharing traffic.

The distinctive feature is that a static route can carry instance tags, in which case it applies only to instances wearing them. That is per-instance routing inside a single network. The same VPC can send tagged workloads' internet-bound traffic to a firewall appliance while everything else takes the default route out, with no separate subnet, no separate route table and no relocating of workloads to make the topology match the policy. It is the same selector idea as the firewall layer, applied to forwarding.

The failure mode here is specific and common: a next-hop-instance route outlives its instance. Delete the VM and the route remains, silently blackholing everything that matched it. Leave the VM running but unhealthy and the route remains, blackholing just as thoroughly, because a static route has no health signal. This is why serious appliance designs point the route at an internal passthrough load balancer rather than at a VM - the load balancer health-checks its backends and the route's next hop stays valid while the fleet underneath changes.

Cloud Router and the dynamic routing mode that catches people

Cloud Router is a regional resource that speaks BGP on the VPC's behalf over a VPN tunnel or a VLAN attachment. It does not forward packets; it learns prefixes from the peer, installs them as dynamic routes in the VPC, and advertises your ranges outward.

The setting that decides your hybrid architecture is a property of the network, not of the router: --bgp-routing-mode, which is regional or global.

In regional mode - the default - a Cloud Router advertises only the subnets in its own region, and the prefixes it learns are installed for that region only. Your europe-west1 subnets are not advertised by a router in us-east4 and cannot use what it learned. Hybrid connectivity becomes a per-region build: a router, an attachment and a peering relationship for every region you operate in.

In global mode, every Cloud Router in the network advertises every subnet in the network, and learned prefixes are usable network-wide. One hybrid landing zone serves the whole estate, on-premises sees a single coherent view of your address space, and workloads move between regions without a routing change on anyone's part. This mode is only coherent because the VPC is global in the first place - it is the setting that converts the global-network property from a curiosity into an architecture.

Flipping the mode later is not a local change. It alters what your routers advertise to a peer you do not administer, which makes it a change on somebody else's network with somebody else's change process. Decide at network creation. The BGP mechanics themselves - custom advertised ranges, MED-based failover, graceful restart - are a topic of their own and are not what the VPC model turns on.

Shared VPC: host projects, service projects, and the IAM seam

Shared VPC designates one project as the host. The host owns the network, the subnets, the routes, the firewall rules and usually the hybrid connectivity. Other projects attach to it as service projects, and an instance created in a service project can put its NIC directly in a subnet that belongs to the host. The network resource lives in one project while the workload is billed and administered in another, which is the entire point: network administration is centralised without centralising compute ownership, quota or spend.

The IAM boundary is where the confusion lives, and it has three distinct pieces.

roles/compute.xpnAdmin is granted at the organisation or folder level and decides who may enable host projects and attach service projects to them. It is not a project role, so the person a developer naturally asks - their project owner - cannot grant it and frequently does not know who can.

roles/compute.networkUser decides who may use a subnet. Granted on one subnet, it scopes a service project team to exactly that subnet. Granted on the host project as a whole, it grants every subnet in it, which is almost always broader than anyone intended and is the default shortcut taken under time pressure.

And by construction, nobody in a service project can create firewall rules or routes. They can only ask. That is the real organisational trade Shared VPC makes, and whether it succeeds depends far more on whether the network team can answer requests within a sprint than on anything technical.

The failure that reliably eats a day is that granting networkUser to the humans is not sufficient. Google's own service agents act on your behalf and need it too - the service project's Compute Engine service agent and its Google APIs service agent must hold networkUser on the subnet before a managed instance group, a GKE cluster or an internal load balancer can be created there. The symptom is a create call failing with a permission error that names a principal nobody on the team recognises, referring to a resource in a project the developer cannot open. See GCP IAM for how those principals and the hierarchy behind them work.

One structural limit shapes large estates: a service project attaches to exactly one host project. Organisations that outgrow a single shared network reach for peering or for a connectivity hub, which is the next section.

Peering versus Network Connectivity Center

VPC Network Peering joins two networks' data planes directly. Both sides must create their half; the peering activates only when both exist, and either side can delete its half unilaterally, at which point traffic stops with no negotiation. Two details decide whether it does what you expect.

Subnet routes are always exchanged. Every subnet in each network becomes reachable from the other automatically, and there is no filter. Custom routes are not. Your static routes and the dynamic routes your Cloud Router learned cross only if one side exports them and the other imports them - two independent flags that must both be set, and the usual cause of "the peering is up but on-prem is unreachable through it".

Address ranges cannot overlap. The peering is refused at creation, and the constraint persists afterwards: you cannot later create a subnet, or expand one, into a range a peer already occupies. A two-year-old peering can therefore block an expansion today and the fix requires another team's cooperation.

Peering is non-transitive. A-to-B and B-to-C never yields A-to-C, and no route will appear to suggest otherwise. The mesh arithmetic that follows from that is worked through in the peering article; the GCP-specific addition is that every pair is two objects to create and two sets of export and import flags to keep consistent, so the operational cost grows faster than the diagram suggests.

Network Connectivity Center is the hub answer. A hub with VPC spokes gives transitive connectivity between spokes without a mesh, and hybrid spokes - VLAN attachments, VPN tunnels, router appliances - let on-premises reach every spoke through one landing point. The tradeoff is the one every hub makes: transitivity becomes the default, so isolation is now something you assert deliberately rather than something you inherit from the topology.

A workable rule: peering when you have a small, deliberate number of long-lived relationships and the non-transitivity is the isolation you want; a hub once the count of relationships starts growing faster than the count of networks.

Private Google Access and Private Service Connect are not the same mechanism

Both let an instance with no external address reach a service without touching the public internet, which is why they get conflated. They work completely differently and are not substitutes for each other.

Private Google Access

PGA is a boolean on a subnet. Switch it on and instances in that subnet holding only internal addresses may send traffic to Google API endpoints; the packets follow the default internet gateway route but never leave Google's network, and the source remains the instance's internal address. It allocates nothing - PGA is a routing and identity arrangement, not plumbing, and it consumes none of your address space.

Two dependencies catch people. It needs a route to the default internet gateway to still exist, so the perfectly reasonable act of deleting that route to force egress through an appliance also turns off Private Google Access. And it usually needs DNS work: point *.googleapis.com at private.googleapis.com (199.36.153.8/30), or, inside a service perimeter, at restricted.googleapis.com (199.36.153.4/30), which resolves only the services a perimeter can protect and deliberately refuses the rest.

Private Service Connect

PSC is plumbing. You create an endpoint - a forwarding rule holding an internal address drawn from a subnet you own - which targets either a bundle of Google APIs or a service published by a producer: another team's VPC, a SaaS vendor, a managed database. Clients connect to your address.

That changes what is possible. Because the address belongs to your network, it is reachable across peering and across hybrid links, where PGA's Google-owned ranges require explicit routing on the far side. Because it is a forwarding rule, it is subject to your firewall rules and appears in your address plan like anything else. And because the producer side sees only a NAT'd connection and never learns your address space, PSC works between two parties who cannot agree on non-overlapping CIDRs - precisely the case peering cannot handle at all.

The one-line version: PGA to reach Google's own APIs from a private subnet; PSC when you need a consumer-owned address, when the consumer sits behind a peering or on-premises, or when the producer is not Google.

Alias IP ranges: the VPC side of pod addressing

A subnet can carry named secondary ranges alongside its primary one. An instance's NIC can then be assigned an alias range drawn from one of them, and addresses inside that range are routed to that instance by the fabric directly.

The architectural point is that these are real VPC addresses, not an overlay. No route object is created per instance, so alias networking consumes no route quota and needs no route-programming convergence when an instance appears or disappears. Firewall rules match alias addresses exactly as they match primary ones, so a pod is a first-class thing your rules can talk about. Anti-spoofing still applies - an instance may only source traffic from addresses actually assigned to it, so an alias cannot be forged by a compromised workload.

GKE is the consumer that matters. A VPC-native cluster places pods in one secondary range and services in another, which is why pods are reachable from on-premises over interconnect and why load balancers can target pods directly. The cluster-side sizing arithmetic - maximum pods per node, the slab each node claims, how large the ranges must therefore be - is developed in the GKE article and is not repeated here. From the VPC's side the facts that matter are three: the secondary range is a property of the subnet, it must not overlap anything in the network or in any peer, and the cluster binds to it at creation time.

Secondary ranges can be added and expanded like primary ones. What cannot be changed after the fact is what a running cluster was told to use, which sets up the next section.

Failure modes you only get on this model

The tag that matches nothing. Described above, and repeated here because the symptom is what makes it dangerous. No error, no warning, no diff - just a rule that is correct in every visible respect and applies to zero instances. Detect it by listing what a rule actually targets, and prevent it by generating tags from the same source of truth that generates instances.

The tag on the wrong instance. The other half of the same coin. Tags are additive and free, so an instance built from a template that happens to carry bastion-access silently inherits everything that tag was ever permitted to reach. The blast radius is not a subnet - it is every rule anywhere in the network that names that tag, and because the VPC is global, "anywhere" spans every region you operate in.

Secondary range exhaustion during a scale-up. The pod range was sized against the node count at design time and the autoscaler's ceiling has since been raised past it. New nodes come up and cannot be allocated a pod slab; the cluster stops scaling while CPU, memory and every node-level signal look healthy. Alarm on free space in the secondary ranges, not only the primary.

Shared VPC permission confusion. The two-project split means a failing operation names a principal and a resource in projects the person debugging cannot see. Before concluding the network is broken, verify that both the human and the relevant service agents hold networkUser on the specific subnet - not on some other subnet, and not merely on the project.

Regional dynamic routing on a global network. The VPC spans regions, so people assume the learned on-premises routes do too. In regional mode they do not. The symptom is a workload that functions in the region holding the interconnect and times out identically everywhere else, which reads like a regional outage rather than a routing-scope decision made years earlier by someone who has left.

Deleting the default route without accounting for Google APIs. The route is removed to force traffic through inspection. Nothing breaks immediately, because established connections and cached DNS keep working. Then every workload that talks to Cloud Storage, Logging or Secret Manager fails at roughly the same time, and the change that caused it is hours or days back in the log.

The peer you cannot expand into. Subnet expansion is validated against every peered network's ranges. A /22 you always intended to grow into a /20 is blocked by something a partner team created in a network you peered to two years ago, and the remedy is a conversation rather than a command.

GCP's VPC is not another cloud's with different nouns. The network is global and only the subnet is regional, so cross-region private traffic needs no connection object and no route you wrote - and the price is that a firewall rule or a wide route has estate-wide reach by default. There is one stateful filtering layer rather than two, and it selects on network tags and service accounts instead of addresses, which makes rules survive scaling but fail silently on a typo. Prefer service accounts to tags, because attaching a service account is IAM-gated and applying a tag is not. Put the non-negotiable denies in a hierarchical policy at the organisation or folder, where no project owner can edit them. Choose global BGP routing at network creation if hybrid connectivity will ever matter, because changing it later is a change on someone else's network. And keep Private Google Access and Private Service Connect straight: one is a routing arrangement that costs no addresses, the other is an endpoint in your own subnet that works in exactly the places peering cannot.