We closed module 2 with an uncomfortable diagnosis: everything AlpinaShop has built is exposed. The VM alpinashop-web-1 has port 80 open to the internet and port 22 reachable from any address, the alpinashop-pedidos instance can be reached by public IP, the GKE Service published an IP with no HTTPS, and there is no boundary at all between what should be public and what should never be. This lesson builds that boundary.

The network is the layer everything else rests on. Before putting a load balancer, a CDN or a WAF in place, you have to decide where each machine lives, which address ranges it uses, who can talk to whom and how traffic reaches the internet. In Google Cloud that decision takes shape in a VPC (Virtual Private Cloud): a software-defined private network, isolated from other customers, inside which your instances, your clusters, your databases and your load balancers live together.

Google Cloud's VPC network has a property that surprises anyone coming from AWS or from a traditional data centre: it is global. A single network can span every region on the planet, and two machines in Belgium and in Tokyo inside the same VPC talk to each other over private IP with no tunnels and no gateways. Subnets, by contrast, are regional. Understanding that asymmetry is 80 % of understanding Google's network.

In this lesson you will design and implement the alpinashop-vpc network with its subnets, plan the addressing with no overlaps, write AlpinaShop's real set of firewall rules, give internet access to machines without a public IP using Cloud NAT, reach Google's APIs without going out to the internet using Private Google Access, and finally connect to alpinashop-pedidos over private IP.

Warning. This lesson has direct security implications. The design proposed here is reasonable for a small business like AlpinaShop, but any network architecture heading for production must be reviewed by a security professional, and if you handle personal data or payment details, by a compliance officer as well.

Contents

  1. What a VPC is and how it differs from a traditional network
  2. Default network, auto mode and custom mode
  3. Planning the addressing: CIDR with no overlaps
  4. Creating alpinashop-vpc and its subnets
  5. Secondary ranges for GKE
  6. Internal and external IP addresses, ephemeral and static
  7. VPC firewall rules: the complete model
  8. AlpinaShop's real rule set
  9. Routes and the default gateway
  10. Cloud NAT: internet access without a public IP
  11. Private Google Access: reaching the APIs without going out to the internet
  12. Private services access: Cloud SQL over private IP
  13. VPC Flow Logs: diagnosis and auditing
  14. What is out of scope for this lesson

  1. What a VPC is and how it differs from a traditional network

In a classic data centre the network is physical: switches, VLANs, cabling, ranges somebody wrote down in a spreadsheet eight years ago. In Google Cloud the network is a software abstraction built on top of Google's global network. The practical differences are these:

Concept Traditional network Google Cloud VPC
Scope One data centre, one site Global: every region at once
Segmentation VLANs + physical routers Regional subnets inside the same network
Connection between sites Dedicated links, VPN Automatic inside the VPC, over Google's internal network
Firewall An appliance at one point in the network Distributed: applied on every interface of every VM
Changing a range Maintenance window You add a new subnet; widening a range is one command
Cost of the internal network Hardware amortisation Traffic between zones and regions, metered per GB

Three consequences worth committing to memory from the start:

  • The network is a global resource of the project. It does not belong to a region. When you create alpinashop-vpc in alpinashop-prod, it exists everywhere.
  • Subnets are regional, not zonal. A subnet in europe-west1 covers europe-west1-b, -c and -d. A VM in any of those zones can take an IP from that subnet. This is what lets the regional MIG alpinashop-web-mig spread instances across three zones using a single subnet.
  • The firewall is not "somewhere". There is no perimeter firewall that traffic passes through: each rule is applied in the hypervisor, on each VM's own network interface. That is why traffic between two VMs in the same subnet is also subject to rules, and why you cannot "dodge" the firewall by putting the machines next to each other.

  1. Default network, auto mode and custom mode

When you created the alpinashop-prod project, Google automatically created a network called default. It is convenient for experimenting and a bad idea for production. Let us see why.

There are two network modes:

Auto mode (--subnet-mode=auto) Custom mode (--subnet-mode=custom)
Subnets One per region, created for you, with fixed ranges from 10.128.0.0/9 None: you create them, with whatever range you decide
New regions A subnet appears automatically Nothing changes
CIDR control None Total
Risk of overlap with the office or another VPC High Controlled
Initial firewall rules Permissive (includes SSH and RDP from 0.0.0.0/0) None, apart from the implied ones

The default network is auto mode and it also ships with preconfigured firewall rules that open port 22 (SSH) and port 3389 (RDP) to the whole internet. That is exactly the problem AlpinaShop has been dragging along since module 2.

Every serious organization uses custom mode, for three reasons:

  1. Control of the addressing. If tomorrow AlpinaShop connects its office over VPN (the subject of 07-03), the ranges must not clash. With auto mode, Google decides for you in every present and future region.
  2. Minimal attack surface. There are no subnets in regions you do not use and no permissive rules somebody forgot to review.
  3. Reproducibility. The network is described in code (Terraform, in 06-07) and recreated identically in alpinashop-dev.

The first real task, then, is to create a custom network and stop using default:

# Set the working project (named configuration from module 1)
gcloud config set project alpinashop-prod

# See what is there right now
gcloud compute networks list
gcloud compute networks subnets list --filter="network:default"

Later on, once everything has been migrated, the default network is deleted. Not on day one: there are live resources inside it. The correct order is create the new network → move workloads → delete the old one.

  1. Planning the addressing: CIDR with no overlaps

Before writing a single command, you plan on paper. A bad addressing plan is paid for over years, because a subnet's primary range can be widened, but not shrunk or moved, and because two networks with overlapping ranges can never be connected.

A minimal CIDR reminder:

Notation Mask Total addresses Usable addresses in GCP
/24 255.255.255.0 256 252
/22 255.255.252.0 1,024 1,020
/20 255.255.240.0 4,096 4,092
/16 255.255.0.0 65,536 65,532

Google reserves four addresses in every subnet: the network address, the gateway (always the second one, x.x.x.1), and two reserved for future use (the last two). That is why a /24 gives 252 usable IPs, not 254.

AlpinaShop's plan reserves the private block 10.10.0.0/16 for production and deliberately leaves space between subnets:

Use Range Region Comment
sn-web-euw1 (primary) 10.10.0.0/24 europe-west1 Catalogue MIG instances
sn-datos-euw1 (primary) 10.10.1.0/24 europe-west1 Internal backends and batch jobs
sn-web-euw1 secondary gke-pods 10.20.0.0/16 — Pods of alpinashop-cluster
sn-web-euw1 secondary gke-servicios 10.21.0.0/20 — Services of alpinashop-cluster
Private services access (Cloud SQL) 10.30.0.0/20 — Range handed to Google for managed services
Reserved for future subnets 10.10.2.0/24 … 10.10.255.0/24 — Do not allocate without updating this document
Reserved for alpinashop-dev 10.60.0.0/16 — Another VPC, another project
AlpinaShop office (future VPN) 192.168.10.0/24 — Never use inside the VPC

Golden rules when planning:

  • Never use 10.0.0.0/24 or 192.168.1.0/24. They are the ranges half the world uses, including your colleagues' home networks; the day there is a VPN, they will clash.
  • Leave gaps. It is easier to beg forgiveness than to reorganise addresses.
  • Document the plan and put it under version control. The addressing document lives in the repository, not in Marta's memory.
  • Kubernetes pod ranges are big. A /16 for pods is not excessive: GKE reserves a block per node.

  1. Creating alpinashop-vpc and its subnets

With the plan settled, the implementation is three commands:

# 1) The network, in custom mode and with global routing
gcloud compute networks create alpinashop-vpc \
  --project=alpinashop-prod \
  --subnet-mode=custom \
  --bgp-routing-mode=global \
  --description="AlpinaShop main production network"

Two parameters deserve an explanation:

  • --subnet-mode=custom: it creates no subnet automatically. That is what we want.
  • --bgp-routing-mode=global: when in future there is a Cloud Router (VPN or Interconnect, the subject of 07-03), the learned routes will be propagated to all regions, not just the router's own. It is the right value for a network that may grow; changing it later forces you to reconfigure the router.
# 2) Web tier subnet
gcloud compute networks subnets create sn-web-euw1 \
  --project=alpinashop-prod \
  --network=alpinashop-vpc \
  --region=europe-west1 \
  --range=10.10.0.0/24 \
  --enable-private-ip-google-access \
  --enable-flow-logs \
  --logging-aggregation-interval=interval-5-sec \
  --logging-flow-sampling=0.5 \
  --logging-metadata=include-all

# 3) Data and internal backends subnet
gcloud compute networks subnets create sn-datos-euw1 \
  --project=alpinashop-prod \
  --network=alpinashop-vpc \
  --region=europe-west1 \
  --range=10.10.1.0/24 \
  --enable-private-ip-google-access \
  --enable-flow-logs \
  --logging-flow-sampling=0.5

What each option does, in detail:

  • --range: the primary range. It is the one shared out among the VMs' interfaces.
  • --enable-private-ip-google-access: lets a VM without a public IP reach Google's APIs (Cloud Storage, BigQuery, Secret Manager…). We develop this in section 11. Always switch it on; it has no cost and no downside.
  • --enable-flow-logs and its parameters: enables flow logging. --logging-flow-sampling=0.5 samples half the flows, which halves the log cost while keeping diagnostic value. We look at it in section 13.

Check:

gcloud compute networks subnets describe sn-web-euw1 \
  --region=europe-west1 \
  --format="table(name, ipCidrRange, gatewayAddress, privateIpGoogleAccess)"

You will see that gatewayAddress is 10.10.0.1: the gateway is always the second address in the range, and it is not a machine you can touch but a distributed function of the network.

  1. Secondary ranges for GKE

The alpinashop-cluster cluster we created in 02-05 needs addresses for pods and for services, and those addresses do not come out of the subnet's primary range but out of secondary ranges (what Google calls alias IP ranges). This is GKE's VPC-native networking model, and it is the one always used in Autopilot.

gcloud compute networks subnets update sn-web-euw1 \
  --region=europe-west1 \
  --add-secondary-ranges=gke-pods=10.20.0.0/16,gke-servicios=10.21.0.0/20

Why such large ranges:

Element Range Approximate capacity
Nodes 10.10.0.0/24 (primary) 252 nodes
Pods 10.20.0.0/16 65,536 addresses; GKE assigns a /24 per node by default
Services (ClusterIP) 10.21.0.0/20 4,096 services

The far-reaching consequence: with VPC-native networking, a pod has a real VPC IP. There is no internal NAT between pods and VMs. That means a firewall rule can refer directly to the pod range, and that a VM in sn-datos-euw1 can receive traffic from a pod and see its genuine source IP. It is simpler to reason about and easier to audit.

When creating a new cluster the ranges are specified like this (a reference for when you recreate the cluster on the new network):

gcloud container clusters create-auto alpinashop-cluster \
  --region=europe-west1 \
  --network=alpinashop-vpc \
  --subnetwork=sn-web-euw1 \
  --cluster-secondary-range-name=gke-pods \
  --services-secondary-range-name=gke-servicios

  1. Internal and external IP addresses, ephemeral and static

Every VM always has an internal IP (from the subnet) and, optionally, an external IP. And each one can be ephemeral (assigned at boot and lost when the machine stops) or static (reserved, outliving the instance).

Type Scope Persistence Indicative cost Typical use in AlpinaShop
Internal ephemeral Subnet For as long as the VM lives Free MIG instances
Internal static Subnet Reserved Free Internal backends with a fixed name
External ephemeral Internet For as long as the VM lives Billed while in use Nothing in production
External static regional Internet Reserved Small cost; more expensive when unused Regional load balancer, Cloud NAT
External static global Internet (anycast) Reserved Small cost Global HTTPS load balancer IP (03-02)

Let us reserve now the global IP the next lesson's load balancer will use and the IPs Cloud NAT will need:

# Global anycast IP for the external HTTPS load balancer (we use it in 03-02 and 03-07)
gcloud compute addresses create alpinashop-lb-ip \
  --project=alpinashop-prod \
  --global \
  --ip-version=IPV4

gcloud compute addresses describe alpinashop-lb-ip --global --format="value(address)"

# Regional static IP for the Cloud NAT egress
gcloud compute addresses create alpinashop-nat-ip \
  --project=alpinashop-prod \
  --region=europe-west1

Reserving the NAT egress IP has a concrete advantage: AlpinaShop's payment gateway requires an allowlist of IP addresses. If the egress IP were ephemeral, it would change when the NAT was recreated and payments would stop working on a Tuesday afternoon for no apparent reason.

A billing warning: a reserved static external IP that is not attached to anything is billed all the same, and at a higher rate than if it were in use. It is one of the most common cost leaks. Review them periodically:

gcloud compute addresses list --filter="status=RESERVED" \
  --format="table(name, region, address, status)"

  1. VPC firewall rules: the complete model

The VPC firewall is stateful (if you allow the inbound connection, the reply goes out with no rule needed) and distributed (applied on every interface). A rule has these elements:

Element Values Notes
Direction INGRESS (inbound) or EGRESS (outbound) INGRESS by default
Action allow or deny There is no "log only"; that is what --enable-logging is for
Priority 0 – 65535, lower wins 1000 by default
Source (ingress) CIDR ranges, network tags, service accounts You pick one of the three types
Destination (egress) CIDR ranges
Targets The whole network, network tags, or service accounts The "who it applies to"
Protocols and ports tcp:80,443, udp:53, icmp, all

There are also four implied rules that you do not see in the list and cannot delete:

Implied rule Direction Action Priority
Allow all egress EGRESS allow 65535
Deny all ingress INGRESS deny 65535
Allow DHCP, DNS and metadata (169.254.169.254) EGRESS allow Always
Block outbound port 25 EGRESS deny Always (anti-spam)

The practical consequence is the one that defines the working discipline: by default nothing comes in and everything goes out. Everything you want to allow inbound has to be written down; so does everything you want to stop going out.

Network tags versus service accounts

This is the firewall's most important design decision. There are two ways of selecting which machines a rule applies to:

Criterion Network tags (--target-tags) Service accounts (--target-service-accounts)
Who can set them Anyone with compute.instances.setTags Only someone who can act as that service account
Change on the fly Yes, live Requires stopping the instance
Risk A developer can tag their VM as web and inherit its network permissions Controlled by IAM
Readability Very high High
Recommendation Acceptable for small environments Preferable in production

AlpinaShop uses a mixed and explicit model: tags for inbound rules from the internet (where readability matters and the machines belong to the MIG) and service accounts for data access. And it documents that decision, because mixing the two criteria without a criterion is a classic source of holes.

  1. AlpinaShop's real rule set

This is the complete set. Read it all before running it: each rule answers a concrete need.

# ---------------------------------------------------------------
# 1) HTTP/HTTPS traffic ONLY from Google's load balancer ranges.
#    130.211.0.0/22 and 35.191.0.0/16 are the ranges from which
#    Google Cloud originates load balancer requests and health
#    checks. They are NOT "the internet".
# ---------------------------------------------------------------
gcloud compute firewall-rules create fw-allow-lb-health-web \
  --network=alpinashop-vpc \
  --direction=INGRESS \
  --action=allow \
  --priority=1000 \
  --source-ranges=130.211.0.0/22,35.191.0.0/16 \
  --target-tags=web-catalogo \
  --rules=tcp:80,tcp:8080 \
  --description="HTTP and health checks from the global load balancer"

# ---------------------------------------------------------------
# 2) SSH ONLY from the Identity-Aware Proxy (IAP) range.
#    35.235.240.0/20 is the range from which IAP opens the TCP
#    tunnel. With this, NO public IP is needed on the VMs.
# ---------------------------------------------------------------
gcloud compute firewall-rules create fw-allow-ssh-iap \
  --network=alpinashop-vpc \
  --direction=INGRESS \
  --action=allow \
  --priority=1000 \
  --source-ranges=35.235.240.0/20 \
  --rules=tcp:22 \
  --description="SSH only through IAP (see 03-04)"

# ---------------------------------------------------------------
# 3) The web tier can talk to the data tier over PostgreSQL.
#    Selector by service account: only the machines running
#    as sa-catalogo-web can open port 5432.
# ---------------------------------------------------------------
gcloud compute firewall-rules create fw-allow-web-to-datos \
  --network=alpinashop-vpc \
  --direction=INGRESS \
  --action=allow \
  --priority=1000 \
  --source-service-accounts=sa-catalogo-web@alpinashop-prod.iam.gserviceaccount.com \
  --target-tags=capa-datos \
  --rules=tcp:5432 \
  --description="PostgreSQL access from the catalogue application"

# ---------------------------------------------------------------
# 4) Explicitly deny the rest of the ingress, with logging.
#    Priority 65000: below all the previous rules, above the
#    implied 65535 rule. It serves to SEE in the logs what is
#    being attempted and is not allowed.
# ---------------------------------------------------------------
gcloud compute firewall-rules create fw-deny-all-ingress \
  --network=alpinashop-vpc \
  --direction=INGRESS \
  --action=deny \
  --priority=65000 \
  --source-ranges=0.0.0.0/0 \
  --rules=all \
  --enable-logging \
  --description="Explicit logged denial of everything not allowed"

A diagram of the result:

flowchart TB
    Internet([Internet])
    LB[Global HTTPS load balancer<br/>130.211.0.0/22 · 35.191.0.0/16]
    IAP[Identity-Aware Proxy<br/>35.235.240.0/20]

    subgraph VPC["alpinashop-vpc · global"]
      subgraph SNW["sn-web-euw1 · 10.10.0.0/24 · europe-west1"]
        MIG["alpinashop-web-mig<br/>tag: web-catalogo<br/>SA: sa-catalogo-web"]
        GKE["alpinashop-cluster<br/>pods 10.20.0.0/16"]
      end
      subgraph SND["sn-datos-euw1 · 10.10.1.0/24"]
        BATCH["Batch jobs / internal backends<br/>tag: capa-datos"]
      end
      NAT[Cloud NAT]
    end

    PSA["Private services access<br/>10.30.0.0/20"]
    SQL[(alpinashop-pedidos<br/>private IP)]

    Internet -->|443| LB
    LB -->|80/8080 tag web-catalogo| MIG
    IAP -->|22| MIG
    MIG -->|5432 SA sa-catalogo-web| SQL
    MIG --> NAT --> Internet
    SQL --- PSA
    PSA --- VPC

Why 0.0.0.0/0 on port 22 is a mistake

It is the most widespread pattern and the most dangerous. Opening tcp:22 to 0.0.0.0/0 literally means that any machine on the planet can try to authenticate against your server. The consequences are not theoretical:

  • A server with port 22 open receives thousands of authentication attempts a day from the very first minute. Automated scanners sweep every cloud provider's ranges continuously.
  • One weak credential, one reused key or one flaw in sshd is enough to turn it into an intrusion.
  • The noise in the logs hides the real attacks.
  • It shuts the door on traceability: you do not know who got in, only which key was used.

The correct alternative is IAP TCP forwarding, rule number 2 in the block above. With it:

  • The VMs need no public IP.
  • Access goes through Google, which requires authentication with the corporate identity (Marta, Dani) and applies IAM: anyone without the roles/iap.tunnelResourceAccessor role does not even get as far as the SSH greeting.
  • Every session is logged with the real identity of the person.
# SSH connection with no public IP, through IAP
gcloud compute ssh alpinashop-web-1 \
  --zone=europe-west1-b \
  --tunnel-through-iap

The details of IAP and its permissions are developed in 03-04 (IAM); here it is enough to know that the port 22 firewall rule points at 35.235.240.0/20 and never at the internet.

Checking rules before they hurt

# See all the rules sorted by priority
gcloud compute firewall-rules list \
  --filter="network:alpinashop-vpc" \
  --sort-by=priority \
  --format="table(name, direction, priority, sourceRanges.list(), targetTags.list(), allowed[].map().firewall_rule().list())"

# Simulate: can this VM receive traffic on port 22 from the internet?
gcloud compute networks get-effective-firewalls alpinashop-web-1 \
  --zone=europe-west1-b 2>/dev/null || \
gcloud compute instances network-interfaces get-effective-firewalls alpinashop-web-1 \
  --zone=europe-west1-b

  1. Routes and the default gateway

Routes decide where a packet goes out. Every VPC is born with two kinds of automatic route:

Route Destination Next hop Can it be deleted
Subnet route The CIDR of each subnet The VPC itself No (created and deleted with the subnet)
Default route 0.0.0.0/0 Internet gateway Yes

The fact that the default route can be deleted is a very powerful security tool: if you remove the 0.0.0.0/0 route from a VPC, no machine in that network can reach the internet, not even with a public IP. It is the pattern used by isolated networks for handling sensitive data. AlpinaShop does not go that far in production, but it is useful for Lucía's analytics project if it ever handles personal data.

gcloud compute routes list --filter="network:alpinashop-vpc" \
  --format="table(name, destRange, nextHopGateway, priority)"

Custom routes (for example, sending certain traffic to a network appliance) and routes learned over BGP from a VPN belong to 07-03.

  1. Cloud NAT: internet access without a public IP

We have already said that VMs should not have a public IP. But they need to reach the internet to update system packages, install Python dependencies or call the payment gateway's API. The solution is Cloud NAT: a managed service, with no instances to maintain, that translates private addresses into one or more public egress IPs.

Key points:

  • Cloud NAT is outbound only. Nobody can initiate a connection from the internet towards a VM through it. That is exactly what we want.
  • It is not a machine: there is no bottleneck to size and no patches to apply.
  • It is configured per region and attached to a Cloud Router, which in this case does no BGP: it is just the service's anchor point.
# 1) Cloud Router (mandatory for NAT, even though it announces no routes here)
gcloud compute routers create alpinashop-router-euw1 \
  --network=alpinashop-vpc \
  --region=europe-west1

# 2) The NAT gateway, with the static IP reserved in section 6
gcloud compute routers nats create alpinashop-nat-euw1 \
  --router=alpinashop-router-euw1 \
  --region=europe-west1 \
  --nat-custom-subnet-ip-ranges=sn-web-euw1,sn-datos-euw1 \
  --nat-external-ip-pool=alpinashop-nat-ip \
  --enable-logging \
  --log-filter=ERRORS_ONLY \
  --min-ports-per-vm=128

The options in detail:

  • --nat-custom-subnet-ip-ranges: only these two subnets go out through NAT. The alternative (--nat-all-subnet-ip-ranges) is convenient but less explicit.
  • --nat-external-ip-pool=alpinashop-nat-ip: uses our static IP. That is the address AlpinaShop gives the payment gateway for its allowlist. The alternative --auto-allocate-nat-external-ips lets Google choose, and the IPs can change.
  • --min-ports-per-vm=128: each VM reserves 128 source ports. It is the parameter behind Cloud NAT's most typical failure: if a machine opens many simultaneous outbound connections (for example, a job downloading thousands of files) and runs out of ports, new connections fail with a confusing error. The symptom shows up in the metrics as nat_allocation_failed.
  • --log-filter=ERRORS_ONLY: logs failures only. ALL generates a considerable volume of logs and the associated cost.

Verification from a VM with no public IP:

gcloud compute ssh alpinashop-web-1 --zone=europe-west1-b --tunnel-through-iap

# Inside the VM: the egress IP must be that of alpinashop-nat-ip
curl -s https://ifconfig.me

  1. Private Google Access: reaching the APIs without going out to the internet

When a VM with no public IP calls storage.googleapis.com to read from the alpinashop-catalogo bucket, which way does that traffic go? There are two possible answers, and the good one is not the obvious one.

  • Without Private Google Access: the request goes out through Cloud NAT, reaches the internet, resolves a public Google IP and comes back. It works, but it leaves Google's network, consumes NAT capacity and is billed as egress traffic.
  • With Private Google Access enabled on the subnet: the request is routed internally towards the front ends of Google's APIs. It does not go through the internet, it does not consume NAT ports and it is not billed as internet egress.

We already enabled it when creating the subnets with --enable-private-ip-google-access. If you need to enable it afterwards:

gcloud compute networks subnets update sn-web-euw1 \
  --region=europe-west1 \
  --enable-private-ip-google-access

gcloud compute networks subnets describe sn-datos-euw1 \
  --region=europe-west1 \
  --format="value(privateIpGoogleAccess)"

It is an option that must be enabled always, on every subnet, in every environment. It has no cost, no downside, and it affects almost everything AlpinaShop will do: reading the bucket, writing to BigQuery (module 4), reading a secret from Secret Manager (03-06) or pulling an image from Artifact Registry.

There is a variant, Private Service Connect, that lets you reach Google APIs or third-party services over a private IP of your own, with DNS control. It is a building block of larger architectures and is studied in 07-03.

  1. Private services access: Cloud SQL over private IP

In 02-03 we created alpinashop-pedidos with a public IP and connected using the Auth Proxy. It worked, but the instance was reachable from the internet (albeit protected by authorised networks and credentials). Now we move it to a private IP, which is the correct configuration.

The mechanism is called private services access and it works like this: you hand a range of your VPC to Google, Google creates the managed resources there (the Cloud SQL instance) and establishes a peering between your network and theirs. From your VPC, the database appears with an ordinary IP from that range.

flowchart LR
    subgraph VPC["alpinashop-vpc"]
      VM["alpinashop-web-1<br/>10.10.0.5"]
      RES["Reserved range<br/>10.30.0.0/20"]
    end
    subgraph GOOG["Google managed services network"]
      SQL[(alpinashop-pedidos<br/>10.30.0.3)]
    end
    VM -->|5432 over private IP| RES
    RES -.network peering.-> GOOG
    GOOG --> SQL

Implementation, step by step:

# 1) Enable the service networking API
gcloud services enable servicenetworking.googleapis.com --project=alpinashop-prod

# 2) Reserve the range we hand to Google (the one in the plan: 10.30.0.0/20)
gcloud compute addresses create alpinashop-psa-range \
  --global \
  --purpose=VPC_PEERING \
  --addresses=10.30.0.0 \
  --prefix-length=20 \
  --network=alpinashop-vpc \
  --description="Range for managed services (Cloud SQL)"

# 3) Establish the private connection
gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges=alpinashop-psa-range \
  --network=alpinashop-vpc \
  --project=alpinashop-prod

# 4) Check
gcloud services vpc-peerings list \
  --network=alpinashop-vpc \
  --project=alpinashop-prod

With the connection established, the private IP is assigned to the instance and the public one is withdrawn:

# Assign a private IP inside alpinashop-vpc
gcloud sql instances patch alpinashop-pedidos \
  --project=alpinashop-prod \
  --network=projects/alpinashop-prod/global/networks/alpinashop-vpc \
  --no-assign-ip

# See the resulting private IP
gcloud sql instances describe alpinashop-pedidos \
  --format="value(ipAddresses[].ipAddress, ipAddresses[].type)"

Details that avoid nasty surprises:

  • --no-assign-ip withdraws the public IP. Do it only once you have verified that the private connection works from a VM; otherwise you are left with no access until you revert it.
  • The range you hand over cannot be shrunk afterwards without recreating the connection. A /20 leaves room for future instances (the alpinashop-pedidos-replica-informes replica also consumes addresses from it).
  • The Flask application's connection string changes from the public IP to the private one; the Auth Proxy is still valid and still the recommended option because it adds encryption and IAM authentication, but it is no longer the only way in.
  • Changing a Cloud SQL instance's network involves a restart. Schedule it outside business hours.

  1. VPC Flow Logs: diagnosis and auditing

Flow logs record a sample of the connections crossing the VMs' interfaces: source, destination, ports, bytes, packets, and whether they were allowed or denied. They are good for three things:

  1. Diagnosing why something does not connect (did the packet arrive? did a rule deny it?).
  2. Auditing: seeing which external destinations your infrastructure really talks to.
  3. Analysing the cost of traffic between zones and regions.

We already enabled them when creating the subnets. A typical query in Cloud Logging (the service is studied in depth in 06-06; here we use it as a tool):

# Denied connections towards the web tier in the last hour
gcloud logging read '
  resource.type="gce_subnetwork"
  AND log_id("compute.googleapis.com/firewall")
  AND jsonPayload.disposition="DENIED"
' --project=alpinashop-prod --limit=20 --freshness=1h \
  --format="table(
    timestamp,
    jsonPayload.connection.src_ip,
    jsonPayload.connection.dest_ip,
    jsonPayload.connection.dest_port,
    jsonPayload.rule_details.reference
  )"
# Flows with the most bytes sent: who is generating traffic?
gcloud logging read '
  resource.type="gce_subnetwork"
  AND log_id("compute.googleapis.com/vpc_flows")
' --project=alpinashop-prod --limit=10 --freshness=30m \
  --format="table(
    jsonPayload.connection.src_ip,
    jsonPayload.connection.dest_ip,
    jsonPayload.bytes_sent
  )"

Cost and privacy considerations:

  • Flow logs are billed by the volume of logs ingested. On a network with heavy traffic, 100 % sampling can be expensive. 0.5 is a reasonable starting point; 0.1 for very high traffic.
  • They contain client IP addresses, which in the European Union are personal data. Set a retention policy in line with the GDPR and document the processing. This is one of those decisions that must be validated by the compliance officer before production.

As well as flow logs, for one-off diagnosis there is Network Intelligence Center and its connectivity testing tool, which simulates a packet and tells you exactly which rule allowed or denied it, without generating real traffic:

gcloud network-management connectivity-tests create prueba-web-a-sql \
  --source-instance=projects/alpinashop-prod/zones/europe-west1-b/instances/alpinashop-web-1 \
  --destination-cloud-sql-instance=projects/alpinashop-prod/instances/alpinashop-pedidos \
  --destination-port=5432 \
  --protocol=TCP

gcloud network-management connectivity-tests describe prueba-web-a-sql \
  --format="value(reachabilityDetails.result)"

  1. What is out of scope for this lesson

So that you are not left with a sense of a gap, this is what exists and we have not covered here:

Topic Where it is covered
Shared VPC across projects 07-03
Peering between your own VPCs 07-03
Cloud VPN, Interconnect, hybrid connectivity 07-03
Private Service Connect in depth 07-03
Load balancing 03-02 (the next one)
Cloud Armor and DDoS protection 03-05
IAP and network permissions 03-04
Cloud Logging in depth 06-06
Network organization policies (e.g. forbidding public IPs) 07-07

Common Mistakes and Tips

  • Using the default network in production. It brings subnets in every region and rules that open SSH and RDP to the internet. Create a custom network from the start and delete default once there are no resources left inside it.
  • Opening tcp:22 to 0.0.0.0/0. The classic mistake. Use IAP with the rule from 35.235.240.0/20 and remove the public IPs from the VMs.
  • Forgetting the load balancer ranges. If you do not allow 130.211.0.0/22 and 35.191.0.0/16, the health checks fail, the backend is marked unhealthy and the load balancer returns 502 with no clear explanation. It is, by a distance, the number one failure in the next lesson.
  • Choosing "convenient" ranges like 10.0.0.0/16 or 192.168.1.0/24. They will clash on VPN day. Plan and document the addressing before creating anything.
  • Believing that subnets are zonal. They are regional. You do not need a subnet per zone; one regional subnet serves a regional MIG across three zones.
  • Confusing high priority with a high number. In the VPC firewall the lower number wins. An allow rule with priority 100 beats a deny with priority 1000.
  • Leaving reserved static external IPs unused. They are billed, and at a higher rate. Audit gcloud compute addresses list --filter="status=RESERVED" every month.
  • Not enabling Private Google Access. Without it, all traffic to Google's APIs goes out to the internet through NAT: more cost, more latency and NAT port exhaustion.
  • Withdrawing Cloud SQL's public IP before checking the private one. Verify private connectivity from a VM first and only then apply --no-assign-ip.
  • Sizing min-ports-per-vm badly in Cloud NAT. A batch job with many outbound connections exhausts the ports and causes intermittent failures that are hard to attribute. Watch the allocation failure metric.
  • Trusting the firewall alone. The VPC firewall controls the network level. Authentication, authorisation and input validation remain the application's responsibility.

Exercises

Exercise 1 — Addressing plan for alpinashop-dev

Marta needs to replicate the network in the alpinashop-dev project. The requirement is that the ranges must not overlap with production (10.10.0.0/16, secondaries 10.20.0.0/16 and 10.21.0.0/20, private services access 10.30.0.0/20) nor with the office (192.168.10.0/24), because both environments will be connected in future.

Design the plan (network, two subnets in europe-west1, GKE secondary ranges and a range for managed services) and write the gcloud commands that create it, with Private Google Access enabled.

Exercise 2 — Firewall rules for the internal reporting service

Lucía needs a reporting VM called alpinashop-informes-1 in sn-datos-euw1, with the tag informes, meeting these requirements:

  1. Only Lucía and Marta can get in over SSH, and only through IAP.
  2. The VM must be able to query the alpinashop-pedidos-replica-informes replica on port 5432.
  3. The VM must not receive traffic from the internet or from the web tier.
  4. The VM must be able to download packages from PyPI (internet egress) without having a public IP.

Write the necessary firewall rules and explain which component solves point 4.

Exercise 3 — Diagnosing a 502

After creating the load balancer (a preview of the next lesson), the catalogue returns 502 and in the console the MIG backends appear as UNHEALTHY. The application responds correctly if you run curl http://10.10.0.5:8080/salud from another VM in the same subnet.

List the three most likely causes in order of probability and the command you would use to confirm or rule out each one.


Solutions

Solution 1

Proposed plan (block 10.60.0.0/16 reserved in the addressing document for development):

Use Range
sn-web-euw1-dev (primary) 10.60.0.0/24
sn-datos-euw1-dev (primary) 10.60.1.0/24
Secondary gke-pods-dev 10.70.0.0/16
Secondary gke-servicios-dev 10.71.0.0/20
Private services access 10.80.0.0/20

None of them overlaps with 10.10/16, 10.20/16, 10.21/20, 10.30/20 or 192.168.10.0/24.

gcloud config set project alpinashop-dev

gcloud compute networks create alpinashop-vpc-dev \
  --subnet-mode=custom \
  --bgp-routing-mode=global

gcloud compute networks subnets create sn-web-euw1-dev \
  --network=alpinashop-vpc-dev \
  --region=europe-west1 \
  --range=10.60.0.0/24 \
  --secondary-range=gke-pods-dev=10.70.0.0/16,gke-servicios-dev=10.71.0.0/20 \
  --enable-private-ip-google-access

gcloud compute networks subnets create sn-datos-euw1-dev \
  --network=alpinashop-vpc-dev \
  --region=europe-west1 \
  --range=10.60.1.0/24 \
  --enable-private-ip-google-access

gcloud compute addresses create alpinashop-psa-range-dev \
  --global --purpose=VPC_PEERING \
  --addresses=10.80.0.0 --prefix-length=20 \
  --network=alpinashop-vpc-dev

Note: the secondary ranges can be given at creation time with --secondary-range or added later with --add-secondary-ranges.

Solution 2

# (1) SSH only through IAP, applied to the reporting VM
gcloud compute firewall-rules create fw-allow-ssh-iap-informes \
  --network=alpinashop-vpc \
  --direction=INGRESS --action=allow --priority=1000 \
  --source-ranges=35.235.240.0/20 \
  --target-tags=informes \
  --rules=tcp:22

# (2) The Cloud SQL replica lives in the private services access
#     range; outbound traffic is allowed by the implied EGRESS
#     rule, so no ingress rule is needed.
#     If you wanted to restrict egress explicitly:
gcloud compute firewall-rules create fw-egress-informes-sql \
  --network=alpinashop-vpc \
  --direction=EGRESS --action=allow --priority=900 \
  --destination-ranges=10.30.0.0/20 \
  --target-tags=informes \
  --rules=tcp:5432

# (3) No ingress rule is created from the internet or from the web
#     tier. The implied "deny all ingress" rule and the explicit
#     fw-deny-all-ingress (priority 65000) already cover it.
#     Check that no existing rule applies to the tag:
gcloud compute firewall-rules list \
  --filter="network:alpinashop-vpc AND targetTags:informes" \
  --format="table(name, direction, priority, sourceRanges.list())"

Point 4: it is solved by Cloud NAT. The alpinashop-nat-euw1 gateway already covers sn-datos-euw1, so the VM, with no public IP, reaches the internet to install from PyPI. On top of that, to download from the alpinashop-catalogo bucket or write to BigQuery it will not use NAT but Private Google Access, which is enabled on the subnet.

As for "only Lucía and Marta": the firewall does not distinguish between people. That part is solved with IAM, granting roles/iap.tunnelResourceAccessor and roles/compute.osLogin only to the corresponding groups; it is covered in 03-04.

Solution 3

In order of probability:

  1. The firewall rule for the health check ranges is missing. It is by far the most frequent cause. The check originates from 130.211.0.0/22 and 35.191.0.0/16, not from the internet or from the subnet.
gcloud compute firewall-rules list \
  --filter="network:alpinashop-vpc AND sourceRanges:(130.211.0.0/22 OR 35.191.0.0/16)" \
  --format="table(name, allowed[].map().firewall_rule().list(), targetTags.list())"
  1. The network tag does not match. The rule points at web-catalogo but the MIG's instance template does not apply that tag to new machines.
gcloud compute instances list --filter="name~alpinashop-web-mig" \
  --format="table(name, zone, tags.items.list(), status)"
  1. The check points at the wrong port or path. The application listens on 8080 and exposes /salud, but the check is configured on port 80 or on /.
gcloud compute health-checks describe hc-catalogo \
  --format="yaml(type, httpHealthCheck.port, httpHealthCheck.requestPath)"

A cross-cutting tool: the firewall logs of fw-deny-all-ingress will show the denied attempts from 35.191.0.0/16 if the first cause is the right one, and Network Intelligence Center's connectivity test confirms the exact rule that is blocking.

Conclusion

AlpinaShop now has a real network. You have understood the property that defines Google's network — the VPC is global, subnets are regional — and why that simplifies architectures which on other providers demand acrobatics. You have ruled out the default network with arguments and created alpinashop-vpc in custom mode, with global routing and a documented addressing plan that reserves 10.10.0.0/16 for production, leaves room to grow and does not clash with the office or with alpinashop-dev.

On that network you have created sn-web-euw1 (10.10.0.0/24) and sn-datos-euw1 (10.10.1.0/24), with secondary ranges gke-pods (10.20.0.0/16) and gke-servicios (10.21.0.0/20) so that alpinashop-cluster uses VPC-native networking and its pods have real addresses. You have reserved the global IP alpinashop-lb-ip for the load balancer we are about to build and the IP alpinashop-nat-ip so that the payment gateway can authorise a stable egress address.

You have learned the complete firewall model — stateful, distributed, priority where the lower number wins, four implied rules, selection by tag or by service account — and you have written AlpinaShop's real set: HTTP and health checks only from 130.211.0.0/22 and 35.191.0.0/16, SSH only from the IAP range 35.235.240.0/20, PostgreSQL only from the sa-catalogo-web service account, and an explicit logged denial that shows you what is knocking at the door. And you know why 0.0.0.0/0 on port 22 is not a convenience but a pending incident.

Finally, the machines no longer need a public IP: Cloud NAT gives them controlled egress with a known static IP, Private Google Access takes them to Google's APIs without setting foot on the internet, private services access has put alpinashop-pedidos on a private IP from the 10.30.0.0/20 range, and VPC Flow Logs turn network problems into log queries instead of guesswork.

The network is ready, but the catalogue is still served from a machine's IP. In the next lesson, 03-02, Cloud Load Balancing, we put a global external HTTP(S) load balancer in front: a single anycast address serving from the point of presence nearest each European customer, spreading traffic across the instances of the alpinashop-web-mig MIG, serving the images straight from the alpinashop-catalogo bucket through a URL map, and checking the backends' health over that /salud path we have already authorised in the firewall. Every network piece we have just put in place exists precisely so that load balancer works first time.

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