Home / Networking

Route Cloud Run & GKE Egress Through a Static IP with Terraform | Serverless VPC Access + NAT

Route Cloud Run & GKE Egress Through a Static IP with Terraform | Serverless VPC Access + NAT

Cloud Run and GKE use ephemeral IPs by default, which breaks third-party IP allow-lists and audit requirements. By attaching a Serverless VPC Access connector, routing traffic through a VPC, and using Cloud NAT with reserved static IPs, you get predictable egress addresses with fully Terraform-managed infrastructure.

By Mateusz Chmielewski · Jul 27, 2026 · 17 min read

What Is Static Egress for Serverless Workloads?

Static egress means outbound traffic from Cloud Run, Cloud Functions, or GKE leaves Google Cloud through one or more predictable public IP addresses. This is achieved by routing serverless traffic into a VPC via Serverless VPC Access, then through Cloud NAT with reserved external IPs.

Think of ephemeral egress as sending mail from a random post office box every day. Static egress is like having a fixed business mailing address that recipients can recognize and trust.

ConceptExplanationWhen to use
Serverless VPC Access ConnectorA managed connector that bridges serverless services to your VPC network.When Cloud Run, Cloud Functions, or App Engine need to reach private RFC1918 resources or route through Cloud NAT.
Cloud NATA managed NAT gateway that lets private or serverless workloads reach the internet using external IPs.When you need outbound internet access with static or predictable source IPs.
Reserved Static IPA persistent regional external IP address that you own until released.When third parties allow-list your outbound IP addresses or for audit and compliance.
Connector SubnetA dedicated /28 subnet used exclusively by the Serverless VPC Access connector.Always create a dedicated /28; do not share it with VMs or GKE nodes.
Egress SettingCloud Run setting that controls whether only private-ranges or all traffic flows through the connector.Use PRIVATE_RANGES_ONLY to avoid NAT costs for public traffic; use ALL_TRAFFIC when all egress must use the static IP.
Cloud RouterA control-plane component that pairs with Cloud NAT to manage NAT routing.Required for Cloud NAT; create one per region where you need static egress.

Why Use Static Egress for Cloud Run and GKE?

By default, Cloud Run containers and GKE nodes use ephemeral public IPs or Google-managed NAT for outbound traffic. The source IP changes over time, breaking SaaS allow-lists, firewall rules, compliance audits, and API rate-limiting policies.

Serverless VPC Access plus Cloud NAT with reserved static IPs gives serverless workloads a fixed, controllable egress identity without managing proxy VMs. Combine with [GKE Spot VMs](/tutorial/spot-vms-preemptible-gke-cost-optimization) for cost optimization, [VPC Service Controls](/tutorial/terraform-vpc-service-controls-custom-module-guide) for zero-trust perimeters, and [Shared VPC](/tutorial/shared-vpc-gcp-terraform-host-service-projects-iam) for enterprise networking.

FeaturethisServicealtAaltB
Source IP PredictabilityYes (reserved IPs)No (ephemeral NAT)Yes (proxy VMs)
Operational OverheadLow (managed)NoneHigh (patch, scale, monitor VMs)
Cloud Run SupportNative via VPC connectorN/AComplex sidecar/proxy setup
GKE SupportNative via Cloud NATN/APossible but fragile
CostMedium (NAT + connector + IPs)LowHigh (VM compute)

Prerequisites

  • GCP project with billing enabled
  • Terraform CLI v1.5.0+ and a GCS remote backend configured
  • IAM roles: `roles/compute.networkAdmin`, `roles/compute.securityAdmin`, and `roles/editor` or custom equivalents
  • APIs enabled: Compute Engine, Serverless VPC Access, Cloud Run, Kubernetes Engine
  • An existing VPC or permission to create one

Step-by-Step Guide

Step 1: Configure Terraform Backend and Providers

Setting up remote state, required providers, and project/region defaults. Serverless networking resources span multiple APIs. A stable backend and pinned provider versions prevent accidental drift and team conflicts.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }

  backend "gcs" {
    bucket = "my-org-tfstate"
    prefix = "serverless-egress/prod"
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

Step 2: Define Variables with Validation

Declaring typed variables for region, network, subnet ranges, connector sizing, and NAT IP count. Validation blocks catch misconfiguration early, such as invalid CIDR sizes or unsupported connector machine types.

variable "project_id" {
  description = "GCP project ID where resources are created"
  type        = string
}

variable "region" {
  description = "Primary region for the connector, router, and NAT"
  type        = string
  default     = "europe-west1"
}

variable "network_name" {
  description = "Name of the VPC network to use or create"
  type        = string
  default     = "serverless-network"
}

variable "connector_subnet_cidr" {
  description = "Dedicated /28 CIDR for the Serverless VPC Access connector"
  type        = string
  default     = "10.0.0.0/28"

  validation {
    condition     = can(regex("^\\d+\\.\\d+\\.\\d+\\.\\d+/28

chmielewski.dev | GCP Engineering & Architecture Blueprints

Insights, guides, and architectural blueprints for creators shipping on Google Cloud Platform.

quot;, var.connector_subnet_cidr)) error_message = "Connector subnet must be a /28 CIDR block." } } variable "nat_ip_count" { description = "Number of static IPs to reserve for Cloud NAT" type = number default = 2 validation { condition = var.nat_ip_count >= 1 && var.nat_ip_count <= 32 error_message = "nat_ip_count must be between 1 and 32." } } variable "connector_machine_type" { description = "Machine type for the Serverless VPC Access connector" type = string default = "e2-micro" validation { condition = contains(["f1-micro", "e2-micro", "e2-standard-4"], var.connector_machine_type) error_message = "Unsupported connector machine type." } } variable "connector_min_instances" { type = number default = 2 } variable "connector_max_instances" { type = number default = 10 } variable "create_gke_cluster" { description = "Whether to create a private GKE cluster example" type = bool default = false } variable "gke_node_cidr" { description = "CIDR for GKE node subnet" type = string default = "10.0.16.0/24" } variable "gke_pods_cidr" { description = "Secondary CIDR for GKE pods" type = string default = "10.4.0.0/14" } variable "gke_services_cidr" { description = "Secondary CIDR for GKE services" type = string default = "10.0.32.0/20" }

Step 3: Create the VPC and Connector Subnet

Building a custom-mode VPC and a dedicated /28 subnet for the Serverless VPC Access connector. The connector subnet must be exclusive to the connector. Sharing it with other resources causes IP exhaustion and routing issues.

resource "google_compute_network" "vpc" {
  name                    = var.network_name
  auto_create_subnetworks = false
  routing_mode            = "REGIONAL"
  project                 = var.project_id
}

resource "google_compute_subnetwork" "connector" {
  name          = "${var.region}-connector-subnet"
  network       = google_compute_network.vpc.id
  region        = var.region
  ip_cidr_range = var.connector_subnet_cidr
  project       = var.project_id

  private_ip_google_access = true

  log_config {
    aggregation_interval = "INTERVAL_5_SEC"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }

  labels = {
    managed_by = "terraform"
    purpose    = "serverless-vpc-access"
  }
}

resource "google_compute_subnetwork" "gke_nodes" {
  count = var.create_gke_cluster ? 1 : 0

  name          = "${var.region}-gke-nodes"
  network       = google_compute_network.vpc.id
  region        = var.region
  ip_cidr_range = var.gke_node_cidr
  project       = var.project_id

  private_ip_google_access = true

  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = var.gke_pods_cidr
  }

  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = var.gke_services_cidr
  }
}

Step 4: Reserve Static IPs for Cloud NAT

Creating regional external IP addresses that Cloud NAT will use as egress sources. Without reserved IPs, Cloud NAT uses auto-allocated ephemeral addresses that change over time, defeating the purpose of allow-listing.

resource "google_compute_address" "nat" {
  count = var.nat_ip_count

  name         = "${var.region}-nat-ip-${count.index}"
  region       = var.region
  project      = var.project_id
  address_type = "EXTERNAL"
  network_tier = "PREMIUM"

  labels = {
    managed_by = "terraform"
    purpose    = "cloud-nat"
  }
}

output "nat_ip_addresses" {
  description = "Reserved static IPs used by Cloud NAT"
  value       = google_compute_address.nat[*].address
}

Step 5: Deploy Cloud Router and Cloud NAT

Creating a Cloud Router and Cloud NAT that uses the reserved static IPs for outbound traffic. Cloud Router is the control plane for Cloud NAT. Manual IP assignment ensures predictable egress addresses.

resource "google_compute_router" "router" {
  name    = "${var.region}-router"
  region  = var.region
  network = google_compute_network.vpc.id
  project = var.project_id
}

resource "google_compute_router_nat" "nat" {
  name                               = "${var.region}-nat"
  router                             = google_compute_router.router.name
  region                             = var.region
  project                            = var.project_id
  nat_ip_allocate_option             = "MANUAL_ONLY"
  nat_ips                            = google_compute_address.nat[*].self_link
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"

  min_ports_per_vm = 64
  max_ports_per_vm = 65536

  log_config {
    enable = true
    filter = "ERRORS_ONLY"
  }
}

Step 6: Create the Serverless VPC Access Connector

Deploying the managed connector that links Cloud Run to the VPC. Cloud Run cannot directly attach to a subnet; it requires a Serverless VPC Access connector to route traffic into the VPC.

resource "google_vpc_access_connector" "connector" {
  name          = "${var.region}-connector"
  region        = var.region
  project       = var.project_id
  network       = google_compute_network.vpc.id
  ip_cidr_range = var.connector_subnet_cidr

  machine_type  = var.connector_machine_type
  min_instances = var.connector_min_instances
  max_instances = var.connector_max_instances

  subnet {
    name       = google_compute_subnetwork.connector.name
    project_id = var.project_id
  }

  depends_on = [google_compute_router_nat.nat]
}

Step 7: Deploy Cloud Run Service with VPC Connector

Creating a Cloud Run service that routes egress through the VPC connector and Cloud NAT. This is the final wiring step that makes Cloud Run traffic originate from the reserved static IPs.

resource "google_service_account" "cloud_run" {
  account_id   = "cloud-run-static-egress"
  display_name = "Cloud Run static egress service account"
  project      = var.project_id
}

resource "google_project_iam_member" "vpc_access_user" {
  project = var.project_id
  role    = "roles/vpcaccess.user"
  member  = "serviceAccount:${google_service_account.cloud_run.email}"
}

resource "google_cloud_run_v2_service" "api" {
  name     = "static-egress-api"
  location = var.region
  project  = var.project_id

  template {
    containers {
      image = "gcr.io/cloudrun/hello:latest"

      env {
        name  = "EGRESS_MODE"
        value = "static-ip"
      }
    }

    vpc_access {
      connector = google_vpc_access_connector.connector.id
      egress    = "ALL_TRAFFIC"
    }

    service_account = google_service_account.cloud_run.email
  }

  depends_on = [google_project_iam_member.vpc_access_user]
}

Step 8: Deploy a Private GKE Cluster Using the Same NAT

Creating a VPC-native private GKE cluster whose nodes have no external IPs and route egress through Cloud NAT. Private GKE clusters improve security by removing public IPs from nodes while still allowing outbound internet access through Cloud NAT.

resource "google_container_cluster" "primary" {
  count = var.create_gke_cluster ? 1 : 0

  name     = "private-static-egress"
  location = var.region
  project  = var.project_id

  network    = google_compute_network.vpc.id
  subnetwork = google_compute_subnetwork.gke_nodes[0].id

  private_cluster_config {
    enable_private_nodes    = true
    enable_private_endpoint = false
    master_ipv4_cidr_block  = "172.16.0.0/28"
  }

  ip_allocation_policy {
    cluster_secondary_range_name  = "pods"
    services_secondary_range_name = "services"
  }

  remove_default_node_pool = true
  initial_node_count       = 1
}

resource "google_container_node_pool" "primary_nodes" {
  count = var.create_gke_cluster ? 1 : 0

  name       = "primary-pool"
  location   = var.region
  cluster    = google_container_cluster.primary[0].id
  project    = var.project_id
  node_count = 2

  node_config {
    machine_type = "e2-medium"
    oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"]

    metadata = {
      disable-legacy-endpoints = "true"
    }

    labels = {
      managed_by = "terraform"
    }
  }
}

Verification

Best Practices

  • Reserve Static IPs Explicitly
  • Use a Dedicated /28 Connector Subnet
  • Choose the Right Egress Setting
  • Size Connectors for Throughput
  • Use Regional Cloud NAT per Region
  • Export Static IPs as Outputs
  • Monitor NAT Port Exhaustion

Common Mistakes

  • {"errorCode":"CONNECTOR_SUBNET_INVALID","symptoms":"VPC connector creation fails with invalid subnet or CIDR error.","rootCause":"The connector subnet is not /28, overlaps with another range, or is not in the same region.","fixCommand":"Verify with `gcloud compute networks subnets list --project=PROJECT_ID`.","prevention":"Always allocate a clean /28 per region and validate it with a Terraform regex."}
  • {"errorCode":"NAT_IP_NOT_USED","symptoms":"Outbound traffic still uses an ephemeral IP, not the reserved static IP.","rootCause":"Cloud NAT is configured with AUTO_ONLY, or the workload subnet is not included in NAT source ranges.","fixCommand":"Set `nat_ip_allocate_option = \"MANUAL_ONLY\"` and `source_subnetwork_ip_ranges_to_nat = \"ALL_SUBNETWORKS_ALL_IP_RANGES\"`.","prevention":"Use manual IP allocation and verify Cloud Run egress setting is ALL_TRAFFIC when public traffic must use the static IP."}
  • {"errorCode":"ASYMMETRIC_ROUTING","symptoms":"Packets drop or connections hang when returning from an external destination.","rootCause":"Traffic leaves through Cloud NAT but returns through a different path, such as a VPN or load balancer backend.","fixCommand":"Review VPC routes and ensure return traffic reaches the same Cloud NAT gateway.","prevention":"Use Cloud NAT for all outbound internet traffic from a subnet; avoid mixing NAT and direct external IPs on the same source range."}
  • {"errorCode":"CONNECTOR_SCALING_LIMIT","symptoms":"Cloud Run requests timeout or fail under load; connector CPU is high.","rootCause":"Connector `max_instances` is too low for the connection or throughput demand.","fixCommand":"Increase `max_instances` or upgrade `machine_type` to `e2-standard-4`.","prevention":"Load-test expected peak traffic and set max_instances at least 50% above observed peak."}
  • {"errorCode":"GKE_IMAGE_PULL_FAILED","symptoms":"Private GKE nodes cannot start pods because images cannot be pulled.","rootCause":"Nodes have no external IP and Private Google Access is not enabled on the node subnet.","fixCommand":"Enable `private_ip_google_access = true` on the GKE node subnet.","prevention":"Enable Private Google Access and ensure Artifact Registry is reachable through restricted.googleapis.com or Private Service Connect."}

Cost Analysis

Featuremetriccost1kcost10kcost100kcost1m
Cloud NAT Gateway$32.40$32.40$32.40$32.40
Static IP Addresses$7.20$7.20$7.20$7.20
Serverless VPC Access Connector~$15.00~$30.00~$75.00~$150.00
NAT Data Processing~$22.50~$45.00~$112.50~$225.00

References

Browse all tutorials