Chapter 9: Building Real GCP Infrastructure
Topic 59

Private Connectivity

NetworkingSecurity

A hardened GCP org keeps its VMs and managed services off the public internet entirely, and three mechanisms make that work: Private Service Access for reaching Google-managed services like Cloud SQL over a private IP, Private Service Connect for private endpoints to services and APIs, and Cloud NAT with a Cloud Router for outbound internet from VMs that have no external IP.

Hatch's app VMs and its Cloud SQL instance live entirely on internal addresses, which is why "no external IP" is an org-policy constraint the design has to satisfy from the start rather than bolt on later. The three mechanisms are not interchangeable — each solves a different leg of the private-posture problem, and mixing them up is the usual cause of "the VM can't reach Cloud SQL but can reach the internet."

Why "No External IP" Is a Policy

An external IP is an attack surface and a data-exfiltration path. Most security-conscious orgs set an org policy — constraints/compute.vmExternalIpAccess — that denies external IPs outright, so no engineer can attach one even by accident. Your network then has to give VMs both egress and service access without one, which is exactly what the rest of this topic builds.

For Hatch this is a hard constraint, not a preference. Every VM in hatch-app-prod starts with no public face, and the design has to make a no-external-IP VM fully functional — able to patch itself, reach Cloud SQL, and call Google APIs — using only the private mechanisms below.

Private Service Access for Cloud SQL

Google-managed services like Cloud SQL run in Google's own VPC, not yours. Private Service Access bridges the two: you reserve a range with google_compute_global_address and create a google_service_networking_connection that peers your VPC to the service producer's network. Cloud SQL then gets a private IP drawn from your reserved range and never needs a public endpoint.

The ordering matters and bites people. You must reserve the global address range before the service networking connection, or Cloud SQL's private IP allocation fails with a peering error. And size the reserved range generously — a tight /24 runs out of private IPs as you add managed instances, and the range is hard to grow after services attach.

Reserving a range and peering for private services
resource "google_compute_global_address" "psa" {
  project       = "hatch-net-host"
  name          = "psa-range"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16                # generous — hard to grow after services attach
  network       = google_compute_network.hatch.id
}

resource "google_service_networking_connection" "psa" {
  network                 = google_compute_network.hatch.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.psa.name]
}

Private Service Connect

Private Service Connect creates a private endpoint IP inside your own subnet that fronts a Google API or a published service. A google_compute_forwarding_rule-based PSC endpoint means traffic to those services stays on internal addresses, instead of leaving your VPC to reach *.googleapis.com over the public internet.

Where Private Service Access is the right tool for managed services that publish over service networking, PSC is the tool for API access and finer-grained private endpoints. The two coexist in a hardened design: PSA gives Cloud SQL its private IP, PSC gives the app a private path to Google APIs.

Cloud NAT and Cloud Router

A google_compute_router paired with a google_compute_router_nat gives internal-only VMs outbound internet — package mirrors, external APIs — through a managed NAT, with no per-VM external IP and no inbound exposure. This is egress without a public face: the VM can run apt and call an external API, but nothing on the internet can initiate a connection to it.

The boundary to keep straight is that Cloud NAT is outbound only. It solves egress and does nothing for ingress — no service reaches in to your VMs through it. And it is for the open internet, not for reaching Google services; use Private Service Access or PSC for those. Conflating the two is the classic "the VM has internet but can't reach Cloud SQL" symptom.

Cloud Router and Cloud NAT for egress without an external IP
resource "google_compute_router" "nat" {
  project = "hatch-net-host"
  name    = "nat-router-uc1"
  region  = "us-central1"
  network = google_compute_network.hatch.id
}

resource "google_compute_router_nat" "nat" {
  project                             = "hatch-net-host"
  name                                = "nat-uc1"
  router                              = google_compute_router.nat.name
  region                              = "us-central1"
  nat_ip_allocate_option              = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat  = "ALL_SUBNETWORKS_ALL_IP_RANGES"  # outbound only
}

Putting It Together for Hatch

The full picture: the app VMs in hatch-app-prod have no external IP. They reach Cloud SQL over Private Service Access, reach Google APIs over PSC or Private Google Access, and reach the wider internet only outbound through Cloud NAT. Every leg of connectivity is satisfied without a single public endpoint.

Put the Cloud Router and Cloud NAT in hatch-net-host alongside the subnets, so egress is managed centrally for every service project rather than reinvented per app. This is the fully private posture the org policy demands — egress, service access, and API access all on internal addresses, with the public internet reachable outbound only and never inbound.

Three legs of a private posture
Private Service Access
Peers your VPC to a Google service producer — Cloud SQL gets a private IP in a reserved range.
Private Service Connect
A private endpoint IP in your subnet for a Google API or published service.
Cloud NAT
Outbound internet for external-IP-less VMs through a managed NAT. Egress only, no ingress.
Private Service Access vs Private Service Connect vs Cloud NAT

Private Service Access — peers your VPC to a Google service producer so resources like Cloud SQL get a private IP in a reserved range. Choose it for managed services that publish over service networking.

Private Service Connect — creates a private endpoint IP in your subnet for a Google API or published service. Choose it for API access and finer-grained private endpoints.

Cloud NAT — gives external-IP-less VMs outbound internet. Choose it for egress, not for reaching Google services — mixing these up is the usual cause of "the VM can't reach Cloud SQL but can reach the internet."

Common Mistakes
  • Giving Cloud SQL a public IP "to get it working" under an org policy that bans external IPs, then discovering the policy blocks it anyway — set up Private Service Access and a private IP from the start.
  • Forgetting to reserve the google_compute_global_address range before the service_networking_connection, so Cloud SQL's private IP allocation fails with a peering error.
  • Expecting Cloud NAT to let services reach in to your VMs — NAT is outbound only, so it solves egress and does nothing for ingress.
  • Assuming an external IP is required for a VM to run apt or call an external API — Cloud NAT provides exactly that egress with no external IP and no inbound surface.
  • Sizing the Private Service Access reserved range too small — a tight /24 — and running out of private IPs as you add managed instances, because the range is hard to grow after services attach.
Best Practices
  • Enforce the no-external-IP org policy and design every VM to use Cloud NAT for egress, so a public IP is never the default.
  • Reserve a dedicated, generously-sized google_compute_global_address range for Private Service Access before connecting Cloud SQL or other managed services.
  • Use Private Service Connect or Private Google Access so calls to Google APIs stay on internal addresses instead of egressing to the public internet.
  • Put the Cloud Router and Cloud NAT in the host project alongside the subnets so egress is centrally managed for every service project.
  • Match the mechanism to the need — PSA for managed services, PSC for APIs, NAT for open-internet egress — instead of reaching for one tool for all three.
Comparable tools AWS PrivateLink (PSC analog), NAT Gateway (Cloud NAT analog), RDS private subnets Config Connector ComputeRouterNAT / ServiceNetworkingConnection gcloud compute routers nats — the imperative form

Knowledge Check

A Hatch VM with no external IP can run apt update but cannot reach Cloud SQL. What is missing?

  • Private Service Access — Cloud NAT covers internet egress but does not provide a private path to Cloud SQL
  • An external IP on the VM, which Cloud SQL requires
  • A second Cloud NAT gateway dedicated to database traffic, sharing the same Cloud Router but with its own reserved egress IP
  • A firewall rule allowing inbound traffic from Cloud SQL

Why must the google_compute_global_address range be reserved before the service networking connection?

  • The connection peers against that reserved range; without it, Cloud SQL's private IP allocation fails with a peering error
  • Terraform applies resources alphabetically by type and name, and the global address sorts ahead of the service networking connection
  • The reserved range is what blocks all external IPs on the VPC
  • It is only a style preference; the order does not affect the result

What does Cloud NAT explicitly not do?

  • Allow inbound connections to reach your VMs — it is outbound only
  • Let an external-IP-less VM reach the public mirrors it needs to run package updates
  • Provide egress to the public internet
  • Work without per-VM external IPs

Why do hardened orgs set constraints/compute.vmExternalIpAccess to deny external IPs?

  • An external IP is an attack surface and exfiltration path, and the private mechanisms cover egress and service access without one
  • External IPs are billed at a higher rate than Cloud NAT
  • VMs cannot reach Cloud SQL over Private Service Access while they still carry an external IP on their primary network interface card
  • The policy is required before a VPC can be made global

You got correct