Skip to main content
Google Cloud · lesson 5 of 10
Google Cloud intermediate Lesson 5 of 10

Also part of: Networking

GCP VPC Networking Basics

Build a custom VPC with a subnet and firewall rules, then debug the classic case of a VM with a public IP that still can't reach the internet.

By SkillByExample · Published

gcp vpc networking firewall

The default VPC hides most of the decisions this lesson is about — it ships with a subnet in every region and rules that already allow SSH, RDP, and internal traffic. Building a custom VPC from nothing forces every one of those decisions into the open, including the one that surprises the most people: a VM can have a public IP and still not reach the internet.

Creating a custom VPC and subnet

gcloud compute networks create lessons-vpc --subnet-mode=custom

gcloud compute networks subnets create lessons-subnet \
    --network=lessons-vpc \
    --region=us-central1 \
    --range=10.10.0.0/24
Created [https://www.googleapis.com/compute/v1/projects/skillbyexample-437219/global/networks/lessons-vpc].
NAME          MODE    IPV4_RANGE  GATEWAY_IPV4
lessons-vpc   CUSTOM

Created [https://www.googleapis.com/compute/v1/projects/skillbyexample-437219/regions/us-central1/subnetworks/lessons-subnet].
NAME             REGION       NETWORK      RANGE
lessons-subnet   us-central1  lessons-vpc  10.10.0.0/24

--subnet-mode=custom is the important flag — without it, GCP auto-creates one subnet per region immediately, which defeats the point of seeing each piece added deliberately.

A VM on the new network, with no rules yet

gcloud compute instances create app-1 \
    --zone=us-central1-a \
    --machine-type=e2-micro \
    --network=lessons-vpc \
    --subnet=lessons-subnet \
    --image-family=debian-12 \
    --image-project=debian-cloud
Created [https://www.googleapis.com/compute/v1/projects/skillbyexample-437219/zones/us-central1-a/instances/app-1].
NAME   ZONE           MACHINE_TYPE  INTERNAL_IP  EXTERNAL_IP     STATUS
app-1  us-central1-a  e2-micro      10.10.0.2    35.192.44.118   RUNNING

It has an external IP — 35.192.44.118 — already. That alone is not enough.

The debugging sequence: “why can’t my instance reach the internet”

gcloud compute ssh app-1 --zone=us-central1-a --command="curl -s -m 5 https://www.google.com -o /dev/null -w '%{http_code}\n'"
curl: (28) Connection timed out after 5001 milliseconds

Step one: check the firewall rules on this network.

gcloud compute firewall-rules list --filter="network:lessons-vpc"
NAME  NETWORK  DIRECTION  PRIORITY  ALLOW  DENY  DISABLED

Empty — same as a fresh custom VPC always starts. Unlike the default network, a custom-mode VPC has no implied egress-allow rule, so even outbound HTTPS is blocked until a rule explicitly permits it.

gcloud compute firewall-rules create lessons-allow-egress \
    --network=lessons-vpc \
    --direction=EGRESS \
    --action=ALLOW \
    --rules=tcp,udp \
    --destination-ranges=0.0.0.0/0
Creating firewall...done.
NAME                   NETWORK      DIRECTION  PRIORITY  ALLOW      DENY  DISABLED
lessons-allow-egress   lessons-vpc  EGRESS     1000      tcp,udp          False
gcloud compute ssh app-1 --zone=us-central1-a --command="curl -s -m 5 https://www.google.com -o /dev/null -w '%{http_code}\n'"
200

The external IP was never the blocker — the missing egress rule was. This is the single most common “internet doesn’t work” ticket on a freshly built custom VPC.

Scoping ingress with a network tag instead of opening everything

gcloud compute instances add-tags app-1 --zone=us-central1-a --tags=web

gcloud compute firewall-rules create lessons-allow-web \
    --network=lessons-vpc \
    --direction=INGRESS \
    --action=ALLOW \
    --rules=tcp:80,tcp:443 \
    --source-ranges=0.0.0.0/0 \
    --target-tags=web
Updated [app-1].
Creating firewall...done.
NAME                 NETWORK      DIRECTION  PRIORITY  ALLOW           DENY  DISABLED
lessons-allow-web   lessons-vpc  INGRESS    1000      tcp:80,tcp:443        False

This rule only affects instances tagged web on this VPC — other instances on the same network, untagged, remain unreachable on 80/443 even though the rule exists.

Practice

1. Confirm the target-tags scoping by creating a second, untagged VM and testing port 80 against it.
gcloud compute instances create app-2 \
    --zone=us-central1-a --machine-type=e2-micro \
    --network=lessons-vpc --subnet=lessons-subnet \
    --image-family=debian-12 --image-project=debian-cloud

curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://<app-2-external-ip>/
curl: (28) Connection timed out after 5001 milliseconds

Same VPC, same firewall rule set, but app-2 was never tagged web, so lessons-allow-web never matches it — proof the rule is scoped by tag, not by network membership alone.

2. Add a DENY rule at a lower priority number than the ALLOW rule and see it win.
gcloud compute firewall-rules create lessons-deny-web \
    --network=lessons-vpc \
    --direction=INGRESS \
    --action=DENY \
    --rules=tcp:80 \
    --priority=900 \
    --target-tags=web
Creating firewall...done.
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://35.192.44.118/
curl: (28) Connection timed out after 5001 milliseconds

Priority 900 beats priority 1000 (lower number = evaluated first, and it’s a DENY), so this rule now overrides lessons-allow-web for the same traffic even though both target web.

3. Remove the external IP from app-1 and set up Cloud NAT so it can still reach the internet.
gcloud compute instances delete-access-config app-1 --zone=us-central1-a --access-config-name="External NAT"

gcloud compute routers create lessons-router --network=lessons-vpc --region=us-central1
gcloud compute routers nats create lessons-nat \
    --router=lessons-router --region=us-central1 \
    --nat-all-subnet-ip-ranges --auto-allocate-nat-external-ips
Updating instance...done.
Created [https://www.googleapis.com/compute/v1/projects/skillbyexample-437219/regions/us-central1/routers/lessons-router].
Creating NAT...done.

app-1 now has no public IP at all — nothing can initiate a connection to it — but outbound calls still succeed through the NAT gateway’s shared external IP.

4. List all routes on the VPC to see the default internet route Cloud NAT relies on.
gcloud compute routes list --filter="network:lessons-vpc"
NAME                              NETWORK      DEST_RANGE   NEXT_HOP           PRIORITY
default-route-a1b2c3d4e5f6g7h8i9   lessons-vpc  0.0.0.0/0    default-internet-gateway  1000

This system-generated route is what makes 0.0.0.0/0 traffic reach the internet gateway at all — firewall rules decide what’s allowed, but a route is what makes a destination reachable in the first place.

5. Clean up every resource created in this lesson.
gcloud compute instances delete app-1 app-2 --zone=us-central1-a --quiet
gcloud compute routers nats delete lessons-nat --router=lessons-router --region=us-central1 --quiet
gcloud compute routers delete lessons-router --region=us-central1 --quiet
gcloud compute firewall-rules delete lessons-allow-egress lessons-allow-web lessons-deny-web --quiet
gcloud compute networks subnets delete lessons-subnet --region=us-central1 --quiet
gcloud compute networks delete lessons-vpc --quiet
Deleted [app-1].
Deleted [app-2].
Deleted [lessons-nat].
Deleted [lessons-router].
Deleted [lessons-allow-egress].
Deleted [lessons-allow-web].
Deleted [lessons-deny-web].
Deleted [lessons-subnet].
Deleted [lessons-vpc].

A network can’t be deleted while any subnet, router, or instance still references it — this order (instances and routers first, then rules, then subnets, then the network) avoids the “resource in use” errors from doing it out of sequence.

Next: Cloud SQL — a managed Postgres instance, connecting to it, and a backup/restore cycle.

Frequently Asked Questions

What's the difference between the default VPC and a custom-mode VPC?
A new project's default VPC comes with one auto-created subnet per region and permissive firewall rules already in place. A custom-mode VPC starts with no subnets and no rules at all — every subnet and every allow rule has to be created explicitly, which is why real infrastructure almost always uses custom mode.
Why does a VM with an external IP still fail to reach the internet?
Two independent things gate outbound traffic: the firewall must have an egress-allow rule for the destination (GCP's implied allow-egress-to-anywhere rule only exists on the default network, not custom ones), and the subnet or route configuration must actually direct 0.0.0.0/0 traffic somewhere. A VM can have a public IP and still be blocked by either.
What is a Cloud NAT and when do I need one?
Cloud NAT lets instances with no external IP at all reach the internet for outbound requests (installing packages, calling an external API) without giving them a public IP to receive inbound traffic on. It's the standard way to let private VMs update or call out without exposing them directly.
Do firewall rules apply per-VM or per-network?
Per-network, filtered by network tags, service accounts, or IP ranges you specify in the rule — not per-VM. A rule with no target restriction applies to every instance on that VPC, which is why network tags exist: to scope a rule to only the instances that should be affected.
What's the difference between a firewall rule's priority and its action?
Priority (0-65535, lower wins) decides which rule is evaluated first when two rules could both match the same traffic; action (ALLOW or DENY) decides what that winning rule does. A DENY rule at priority 900 overrides an ALLOW rule at priority 1000 for the same traffic, even though both exist.