
Connect Cloud Run & GKE to Cloud SQL: Auth Connector, IAM DB Auth & Connection Pooling | 2026
Connect Cloud Run or GKE workloads to Cloud SQL without passwords or public IPs: use the Cloud SQL Auth Proxy v2 as a Unix socket or TCP sidecar, enable IAM database authentication so the workload's service account is the database user, and front the proxy with PgBouncer connection pooling to cap server connections under high concurrency. This guide covers all three layers end-to-end in Terraform.
By Mateusz Chmielewski · Aug 22, 2026 · 14 min read
What Is the Cloud SQL Auth Proxy?
The Cloud SQL Auth Proxy is a binary (or container) that sits between your application and Cloud SQL. It intercepts PostgreSQL/MySQL connections on a local Unix socket or TCP port, wraps them in mutual TLS (mTLS) authenticated with your workload's IAM service account, and forwards them to the Cloud SQL instance over the Cloud SQL Admin API. No inbound firewall holes, no static passwords, and no certificate management in application code — the proxy handles all of it automatically, rotating mTLS certificates every hour.
Think of the Auth Proxy like a hotel concierge who checks your room key card (IAM service account) at the front desk and escorts you to the right floor (Cloud SQL instance), so you never need to carry a physical door key (database password) or know the building's internal layout (network routing). The concierge also rotates the access codes every hour without telling you — it just works.
| Concept | Explanation | When to use |
|---|---|---|
| Cloud SQL Auth Proxy v2 | A sidecar binary that authenticates to Cloud SQL via IAM, establishes mTLS, and exposes a local socket or port to the application. | GKE pods, GCE VMs, on-premises, and any compute that is not Cloud Run (which has a built-in socket factory) |
| Cloud Run Built-in Connector | Cloud Run has native Cloud SQL socket support — set CLOUD_SQL_INSTANCE_CONNECTION_NAME env var; the platform mounts a Unix socket at /cloudsql/<INSTANCE>. | All Cloud Run services — no sidecar container required |
| IAM Database Authentication (auto-IAM) | A Cloud SQL feature that maps a Google service account email to a PostgreSQL role, granting database login via an IAM-issued short-lived token rather than a password. | Any workload that can be assigned a service account — eliminates Secrets Manager password rotation |
| Workload Identity (GKE) | GKE feature that binds a Kubernetes ServiceAccount to a Google IAM ServiceAccount, so pods can authenticate to Google APIs (including Cloud SQL) without a mounted JSON key. | Every GKE workload — replace all key-file auth patterns |
| PgBouncer Connection Pooling | A lightweight PostgreSQL connection pooler that multiplexes many application connections onto a smaller set of real server connections, preventing Cloud SQL's max_connections limit from being breached. | High-concurrency Cloud Run services (many instances × many goroutines) or GKE deployments with more than ~100 concurrent queries |
| Instance Connection Name | The unique identifier for a Cloud SQL instance: <project-id>:<region>:<instance-name>, used by the Auth Proxy and Cloud Run connector. | Required in all connection configurations — retrieve with gcloud sql instances describe |
Why Use Auth Proxy + IAM DB Auth Instead of Direct Connection?
The naive approach — opening a Cloud SQL public IP, adding `authorized_networks = 0.0.0.0/0`, and hardcoding a password in an environment variable — creates three simultaneous production risks: the database is reachable from the internet, credentials leak via config dumps or logs, and password rotation requires a deployment. Cloud Run and GKE services that connect to Cloud SQL with direct public IPs plus static passwords have been the most common cause of database credential exposure in GCP environments.
The Auth Proxy + IAM DB Auth pattern eliminates all three risks in one move. The Cloud SQL instance keeps `ipv4_enabled = false` (private IP only, as covered in [Cloud SQL PostgreSQL in Production](/tutorial/cloud-sql-postgres-production-private-ip-ha-backups-terraform)). The Auth Proxy authenticates with the workload's service account, rotating its own mTLS certificate every hour. IAM database authentication means the database user is just a role — no password to rotate, no secret to leak. Pair this with PgBouncer for connection pooling to handle Cloud Run's stateless scaling, which is described in the [Cloud Run Production Checklist](/tutorial/cloud-run-production-checklist-min-instances-concurrency-cold-starts-cost). For the full compute tier setup, see [Production Cloud Run Service with Terraform](/tutorial/production-cloud-run-terraform-custom-domain-iam-vpc-connector-cicd) and [GKE Autopilot Production Checklist](/tutorial/gke-autopilot-production-checklist-pdb-hpa-vpa-spot-cost).
| Feature | thisService | altA | altB |
|---|---|---|---|
| Database password required | No — IAM DB Auth uses service account token | Direct public IP + static password: Yes, stored in env var | Private IP direct: Yes, still needs a password |
| Network exposure | No public IP — private or proxy socket only | Public IP with authorized_networks | Private IP (requires VPC connector for Cloud Run) |
| Certificate management | Automated by Auth Proxy (rotates hourly) | Manual CA cert management | Manual CA cert management |
| Connection pooling | PgBouncer in transaction mode (N app → M server) | None — each Cloud Run instance opens own connection | None — application-side pooling only |
| Terraform-friendly | Yes — google_sql_user type=CLOUD_IAM_SERVICE_ACCOUNT | Yes but leaks password into state | Yes — private IP only |
Prerequisites
- A running Cloud SQL for PostgreSQL 16 instance with private IP and deletion_protection = true (see the sibling guide Cloud SQL PostgreSQL in Production)
- Terraform v1.6+ with google provider ~> 5.x and a configured backend
- gcloud CLI v480.0+ authenticated (gcloud auth application-default login)
- GKE Autopilot or Standard cluster with Workload Identity enabled (for the GKE path)
- A Cloud Run service or GKE Deployment to attach to Cloud SQL
- IAM roles: Cloud SQL Admin, IAM Admin, Service Account Admin (or roles/owner in a sandbox)
Step-by-Step Guide
Step 1: Grant the Workload Service Account the Cloud SQL Client Role
Create or reuse a dedicated Google service account for the application and grant it roles/cloudsql.client — the minimum IAM role the Auth Proxy and Cloud Run socket factory need. roles/cloudsql.client grants the right to establish connections via the Cloud SQL Admin API. Without it, the Auth Proxy and Cloud Run connector both fail at startup with PERMISSION_DENIED.
resource "google_service_account" "app" {
account_id = "cloud-sql-app"
display_name = "App SA — Cloud SQL connector"
}
# Minimum permission for Auth Proxy and Cloud Run connector
resource "google_project_iam_member" "sql_client" {
project = var.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.app.email}"
}
Step 2: Create an IAM Database User (Passwordless)
Create a Cloud SQL user of type CLOUD_IAM_SERVICE_ACCOUNT. Cloud SQL maps the service account email to a PostgreSQL role — no password is ever set or stored. IAM database authentication replaces the static password with a short-lived OAuth 2 token that the Auth Proxy fetches automatically. The token rotates every hour; no Secrets Manager rotation job required.
resource "google_sql_user" "app_iam" {
name = google_service_account.app.email
instance = google_sql_database_instance.postgres.name
type = "CLOUD_IAM_SERVICE_ACCOUNT"
# no password field — authentication is via IAM token
}
# Grant the role table privileges after creation (run once via psql):
# GRANT ALL ON SCHEMA public TO "cloud-sql-app@<project>.iam";
# GRANT ALL ON ALL TABLES IN SCHEMA public TO "cloud-sql-app@<project>.iam";
Step 3: Connect Cloud Run to Cloud SQL (Built-in Socket — No Sidecar)
Configure the Cloud Run service with CLOUD_SQL_INSTANCE_CONNECTION_NAME and the Unix socket path. Cloud Run's platform mounts the socket automatically — no Auth Proxy container needed. Cloud Run's built-in Cloud SQL socket factory handles auth, certificate rotation, and reconnection transparently. Adding an explicit Auth Proxy sidecar wastes CPU/memory and complicates health checks.
resource "google_cloud_run_v2_service" "app" {
name = "my-app"
location = "europe-west1"
template {
service_account = google_service_account.app.email
containers {
image = "europe-west1-docker.pkg.dev/${var.project_id}/my-app/api:latest"
env {
name = "DB_SOCKET"
value = "/cloudsql/${google_sql_database_instance.postgres.connection_name}"
}
env {
name = "DB_USER"
# IAM DB Auth: use service account email without .gserviceaccount.com domain
value = trimsuffix(google_service_account.app.email, ".gserviceaccount.com")
}
env {
name = "DB_NAME"
value = "appdb"
}
}
annotations = {
"run.googleapis.com/cloudsql-instances" = google_sql_database_instance.postgres.connection_name
}
}
}
Step 4: Deploy Auth Proxy as a GKE Sidecar with Workload Identity
For GKE workloads, add the cloud-sql-proxy container as a sidecar. Configure Workload Identity to bind the Kubernetes ServiceAccount to the Google IAM ServiceAccount so no JSON key is needed. GKE does not have the built-in Cloud Run socket factory; the Auth Proxy sidecar provides an equivalent Unix socket or TCP port inside the Pod. Workload Identity ensures the proxy authenticates with the pod's IAM identity, not a mounted key file.
# Terraform: Workload Identity binding
resource "google_service_account_iam_member" "workload_identity" {
service_account_id = google_service_account.app.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[default/my-app]"
}
Step 5: GKE Pod Spec — Auth Proxy Sidecar Container
Add the cloud-sql-proxy sidecar to the Pod spec, sharing an emptyDir volume for the Unix socket. The application container connects to the socket path instead of a TCP port. Unix socket communication is faster than loopback TCP (no network stack overhead) and is the recommended pattern for containerized workloads — it avoids port conflicts and simplifies health checks.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
metadata:
annotations:
# Workload Identity: bind KSA to Google SA
iam.gke.io/gcp-service-account: cloud-sql-app@PROJECT_ID.iam.gserviceaccount.com
spec:
serviceAccountName: my-app # annotated KSA
volumes:
- name: cloudsql-socket
emptyDir: {}
containers:
- name: my-app
image: europe-west1-docker.pkg.dev/PROJECT_ID/my-app/api:latest
env:
- name: DB_SOCKET
value: /cloudsql/PROJECT_ID:europe-west1:prod-postgres-16/.s.PGSQL.5432
- name: DB_USER
value: cloud-sql-app@PROJECT_ID.iam
- name: DB_NAME
value: appdb
volumeMounts:
- name: cloudsql-socket
mountPath: /cloudsql
- name: cloud-sql-proxy
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.15
args:
- "--unix-socket=/cloudsql"
- "--auto-iam-authn" # enable IAM DB Auth token exchange
- "PROJECT_ID:europe-west1:prod-postgres-16"
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
volumeMounts:
- name: cloudsql-socket
mountPath: /cloudsql
Step 6: Add PgBouncer Connection Pooling for High-Concurrency Workloads
Deploy PgBouncer as a second sidecar (GKE) or as a separate Cloud Run service (Cloud Run) in transaction pooling mode to multiplex many application connections onto a small pool of real server connections. Cloud SQL for PostgreSQL has a hard max_connections limit (roughly 25 × vCPUs by default). Cloud Run scales horizontally with many instances, each potentially opening multiple connections — without pooling, connection exhaustion causes FATAL: sorry, too many clients already.
# pgbouncer.ini mounted from a ConfigMap
[databases]
appdb = host=/cloudsql/PROJECT_ID:europe-west1:prod-postgres-16 dbname=appdb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 5432
auth_type = cert # trust the Auth Proxy's mTLS socket
pool_mode = transaction # best for short-lived queries; use session for SET/TEMP
max_client_conn = 1000 # app-facing connections
default_pool_size = 20 # real server connections per database/user pair
server_idle_timeout = 60
log_connections = 1
log_disconnections = 1
Step 7: Verify End-to-End Connection via Auth Proxy
Run a diagnostic sequence to confirm IAM authentication, proxy socket, and database connectivity from both Cloud Run and GKE. A silent misconfiguration (wrong connection name, missing IAM binding) surfaces only on the first query in production — catch it in staging with a forced connect-and-query test.
# --- Cloud Run: check logs for successful connection ---
gcloud run services describe my-app \
--region=europe-west1 \
--format='value(status.conditions)'
gcloud logging read \
'resource.type=cloud_run_revision AND textPayload:"connected to"' \
--limit=5 --project=PROJECT_ID
# --- GKE: exec into the app container and test psql ---
kubectl exec -it deploy/my-app -c my-app -- \
psql "host=/cloudsql/PROJECT_ID:europe-west1:prod-postgres-16 dbname=appdb user=cloud-sql-app@PROJECT_ID.iam" \
-c 'SELECT current_user, now();'
# --- Check proxy metrics (if --health-check enabled) ---
kubectl exec -it deploy/my-app -c cloud-sql-proxy -- \
wget -qO- http://localhost:9090/metrics | grep cloudsql_proxy_connections
Verification & Health Check
Best Practices
- Never Use a Static Password with IAM DB Auth Available
- Use Unix Sockets, Not TCP Loopback, for Auth Proxy
- Always Enable PgBouncer for Cloud Run Horizontal Scale
Common Mistakes
- {"errorCode":"ERROR failed to connect to instance; PERMISSION_DENIED","symptoms":"Auth Proxy logs show 'PERMISSION_DENIED' on startup. Cloud Run service fails with connection timeout. GKE proxy container in CrashLoopBackOff.","rootCause":"The workload service account is missing roles/cloudsql.client. For GKE, Workload Identity may not be configured — the proxy is running as the node's default SA, which typically lacks Cloud SQL access.","fixCommand":"gcloud projects add-iam-policy-binding PROJECT_ID --member='serviceAccount:cloud-sql-app@PROJECT_ID.iam.gserviceaccount.com' --role='roles/cloudsql.client'","code":"resource \"google_project_iam_member\" \"sql_client\" {\n project = var.project_id\n role = \"roles/cloudsql.client\"\n member = \"serviceAccount:${google_service_account.app.email}\"\n}\n","language":"hcl","filename":"iam.tf","prevention":"Gate Terraform apply with a policy check that verifies roles/cloudsql.client is assigned before the service or deployment resource is created."}
- {"errorCode":"FATAL password authentication failed for user","symptoms":"psql or application throws 'FATAL: password authentication failed for user \"[email protected]\"' even though the IAM user exists.","rootCause":"The Auth Proxy was started without --auto-iam-authn, so it does not exchange the IAM token for a database auth token. The instance may also be missing the cloudsql.iam_authentication database flag.","fixCommand":"gcloud sql instances patch prod-postgres-16 --database-flags=cloudsql.iam_authentication=on","code":"# In deployment.yaml, Auth Proxy args must include:\nargs:\n - \"--auto-iam-authn\"\n - \"--unix-socket=/cloudsql\"\n - \"PROJECT_ID:europe-west1:prod-postgres-16\"\n","language":"yaml","filename":"deployment.yaml","prevention":"Add --auto-iam-authn to the Auth Proxy args in all environments from day one. Verify the database flag with: gcloud sql instances describe <instance> --format='value(settings.databaseFlags)'."}
- {"errorCode":"ERROR prepared statement already exists / invalid transaction","symptoms":"Application using ORM (Django, SQLAlchemy, Hibernate) throws prepared statement errors or rollback failures when using PgBouncer in transaction mode.","rootCause":"PgBouncer transaction pooling releases the server connection after each transaction, so named prepared statements from a previous connection are not available.","fixCommand":"Switch PgBouncer to session mode, or disable server-side prepared statements in the ORM/driver.","code":"# Option A: session mode (one server connection per client session — less pooling benefit)\npool_mode = session\n\n# Option B: disable prepared statements in the driver (psycopg2 example):\n# conn = psycopg2.connect(..., options=\"-c plan_cache_mode=force_generic_plan\")\n# Or for SQLAlchemy: create_engine(..., execution_options={\"prepared_statement_cache_size\": 0})\n","language":"ini","filename":"pgbouncer.ini","prevention":"Test PgBouncer transaction mode in staging with the same ORM and driver configuration used in production before enabling it for live traffic."}
Cost Analysis
| Feature | metric | cost1k | cost10k | cost100k | cost1m |
|---|---|---|---|---|---|
| Auth Proxy compute overhead per Cloud Run instance (50m CPU / 64Mi RAM) | $0.00 | $0.00 | ~$2/mo | ~$8/mo | |
| PgBouncer Cloud Run service (always-on min-instances=1, 256Mi) | $4.00 | $4.00 | $4.00 | $4.00 | |
| Secrets Manager (static password alternative) — per secret version access | $0.00 | $0.03 | $0.30 | $3.00 | |
| IAM DB Auth token exchange — no per-request charge | $0.00 | $0.00 | $0.00 | $0.00 |