Postgres on Google Cloud / Lesson 8 of 8

Private IP and the VPC

Taking away the public IP is one line of configuration and one genuinely hard problem. The hard problem is you.

≈ 20 minutes · networking · read before you plan the switch

Everything so far has assumed the instance has a public IP address. That is not as alarming as it sounds — lesson 2 covered why: the allowlist is empty, there is no password, and access is granted to an identity. The public endpoint is already identity-gated.

But once your workloads also run inside Google Cloud, the public IP has no remaining job. Removing it deletes the last internet-facing surface and the cross-cloud network hop at the same time. This lesson is what that costs.

The short version

Private IP is one line on the instance and one line in your application. The work is not there. It is in the network you have to build first, and in the developer access you have to replace before you switch it off — because the moment the public IP goes, every laptop in the team loses its connection.

What a VPC is, in one paragraph

A VPC network is your own private network inside Google Cloud. It spans every region, and you carve subnets out of it, one per region, each with a range of internal IP addresses. A VM you create gets an address from a subnet. Two VMs on the same VPC can talk over internal addresses without touching the internet.

Your managed PostgreSQL instance is not a VM you own. It runs in a network Google operates. So "put the database on my VPC" cannot mean what it sounds like — and the mechanism that bridges the gap is the part worth understanding.

Private services access, and the range you cannot resize

The classic mechanism is private services access, usually written PSA. You reserve a block of addresses in your VPC, hand it to Google, and Google peers its network to yours and puts your instance's private address inside that block.

your VPC Google's service producer network ──────── ───────────────────────────────── subnet 10.0.0.0/20 └─ your VMs ┌──────────────────────┐ │ your instance │ allocated range ◄──VPC peering──►│ private IP from │ 10.8.0.0/16 │ the allocated range │ (reserved, empty, └──────────────────────┘ yours but unused)
# 1. reserve the range — nothing uses it, it just gets set aside
gcloud compute addresses create google-managed-services \
  --global --purpose=VPC_PEERING --prefix-length=16 \
  --network=my-vpc

# 2. peer your VPC to Google's service producer network
gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges=google-managed-services --network=my-vpc
DecisionWhat the docs say
How big? The minimum size is a single /24 block, but the recommended size is a /16 block. Take the /16. It costs nothing.
Which addresses? Select an allocated range that is completely separate from current and future subnet ranges — including anything reachable over peering. Future is the word that matters.
Can I change it later? You can't modify an allocated range. You delete it and create a larger contiguous one, or bolt a second allocation onto the connection.
Choose the range while nothing is using it

An allocated range is immutable, and it has to avoid address space the organisation has not decided to use yet. Getting it wrong is cheap today and expensive after go-live. Build the network before you need it — a reserved range and an idle peering are both free, and doing it later converts a one-line change into a network change against a live production database.

Private Service Connect, the newer option

PSA peers two whole networks together, which means one VPC per instance and a peering relationship to manage. Private Service Connect (PSC) instead publishes the instance behind a service attachment, and each consumer creates an endpoint — an internal address in their own subnet — pointing at it.

Private services accessPrivate Service Connect
Mechanism VPC peering to Google's network A service attachment plus per-consumer endpoints
Address space A dedicated allocated range you cannot resize Addresses from your ordinary subnets
Reach One VPC network Google's docs: connect from multiple Virtual Private Cloud (VPC) networks that belong to different groups, teams, projects, or organizations
Choose it when One VPC, one team, and it is already built Several projects or organisations need the database, or you would rather not manage a peering

For a single team with one VPC, PSA is fine and is what most existing setups use. If you are choosing today and more than one project will connect, look at PSC first.

The switch itself

Three changes, and the first two are trivial:

# 1. the instance — this RESTARTS it
gcloud sql instances patch INSTANCE_NAME \
  --network=projects/PROJECT_ID/global/networks/my-vpc \
  --no-assign-ip
# 2. the application — one argument to the connector
from google.cloud.sql.connector import Connector, IPTypes

connector = Connector(ip_type=IPTypes.PRIVATE)
# getconn() and the engine are otherwise exactly as in lesson 5

Everything from lesson 4 survives this unchanged. The three layers are unaffected: IAM still decides who may connect, the IAM database user still has to exist, and the PostgreSQL grants still decide what you may do. Private IP is a network change, not an access-control change — and confusing the two is how people conclude they no longer need the grants.

Enabling private IP restarts the instance

From the docs: configuring an existing Cloud SQL instance to use private IP causes the instance to restart, resulting in downtime. Schedule it. And do the network build in a separate, earlier change — PSA peering takes several minutes to establish and you do not want that on the clock.

The third change is the hard one

You have just removed the only route your team had. The proxy on a laptop reaches the instance over its public IP. There is no public IP now. Every developer, every GUI client, and every ad-hoc psql is locked out — and so is anything that restores a dump or runs a manual migration from someone's machine.

Solve this before you flip the switch, not after

This is the single most-missed item in a private-IP migration. It is a real piece of work — not a footnote — and discovering it on the day means either a rollback or a team that cannot reach its database.

The replacement: a bastion reached over IAP

The standard answer is a small VM inside the VPC, with no external IP at all, reached through IAP TCP forwarding. Google describes it as letting you establish an encrypted tunnel over which you can forward SSH, RDP, and other traffic to VM instances — which means the bastion needs no public address either.

laptop Google your VPC ────── ────── ──────── cloud-sql-proxy :5432 │ └── IAP tunnel ──────► identity check ─────────► bastion VM (encrypted) roles/iap. (no external IP) tunnelResourceAccessor │ │ private IP ▼ your instance
# open the tunnel — leave it running
gcloud compute start-iap-tunnel BASTION_VM 5432 \
  --local-host-port=localhost:5432 --zone=ZONE

# the firewall rule IAP needs; the range is fixed and documented
gcloud compute firewall-rules create allow-iap-ingress \
  --network=my-vpc --direction=INGRESS --action=allow \
  --rules=tcp:22,tcp:5432 --source-ranges=35.235.240.0/20

35.235.240.0/20 is not arbitrary — the documentation states it contains all IP addresses that IAP uses for TCP forwarding. And note what this is not: it is not an allowlist of your team's home addresses. Access is still granted by identity, through roles/iap.tunnelResourceAccessor. The model from lesson 2 holds all the way down.

The alternatives

OptionTrade-off
Bastion VM plus IAP Most control, and one more VM to patch and pay for. The usual choice.
Cloud Workstations A managed development environment already inside the VPC. Nothing to maintain; a per-seat cost, and people have to work in it.
Run the proxy in the cluster Fine for applications, useless for someone who needs a GUI client on a laptop.
Keep the public IP Entirely defensible. The endpoint is identity-gated with an empty allowlist, which is why lesson 2 works. You are choosing to keep a small attack surface in exchange for not building any of this.

The order that works

Check yourself

You run --no-assign-ip on Friday. What breaks that nobody mentioned in the plan?

The proxy on a laptop reached the instance over the public IP. There is not one now. The three access layers are untouched — this is purely a network change — but the network it removed was the one your team used. Build the bastion and IAP tunnel first.

Why choose the allocated range before anything needs it?

You can't modify an allocated range. Changing it means deleting and recreating, against a live database if you left it late. A /16 is recommended but a /24 is permitted, and the reservation is free.

After the switch, which of the three access layers changed?

None. Identity still decides who connects, the database user still has to exist, and the grants still decide what you may do. Private IP changes the route, not the permissions — which is also why it is not a substitute for any of them.

From memory: the two lines that perform the switch, and the one thing you must build before running either.

--no-assign-ip on the instance, and ip_type=IPTypes.PRIVATE in the connector. Before either: the replacement developer access — a no-external-IP bastion reached over an IAP tunnel — tested while the public IP still works.