{"id":165828,"date":"2026-04-12T20:36:17","date_gmt":"2026-04-12T17:36:17","guid":{"rendered":"https:\/\/computingforgeeks.com\/gcp-cloud-sql-postgresql-terraform\/"},"modified":"2026-04-12T20:36:17","modified_gmt":"2026-04-12T17:36:17","slug":"gcp-cloud-sql-postgresql-terraform","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/gcp-cloud-sql-postgresql-terraform\/","title":{"rendered":"Deploy Cloud SQL PostgreSQL with Terraform (2026 Guide)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Managing PostgreSQL on a VM gives you full control, but it also gives you full responsibility: patching, backups, failover, connection pooling, and the 3 AM pager when replication breaks. Cloud SQL takes those off your plate. You get a managed PostgreSQL instance with automated backups, point-in-time recovery, and optional high availability, all provisioned through Terraform so the configuration lives in version control where it belongs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers creating a Cloud SQL PostgreSQL 17 instance using both <code>gcloud<\/code> and Terraform, configuring private IP networking via VPC peering, enabling IAM database authentication, setting up read replicas, and connecting from GKE using the Cloud SQL Auth Proxy. For securing database credentials in GCP, see the <a href=\"https:\/\/computingforgeeks.com\/google-cloud-secret-manager-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">Secret Manager tutorial<\/a>. If your workloads run on GKE, the <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Workload Identity guide<\/a> explains how the Auth Proxy authenticates without exporting service account keys.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Verified working: <strong>April 2026<\/strong>. Cloud SQL PostgreSQL 17, Enterprise edition, Terraform google provider 6.x<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cloud SQL vs Self-Managed PostgreSQL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off is cost versus operational burden. Cloud SQL costs more per vCPU-hour than a Compute Engine VM running PostgreSQL, but you do not spend engineering time on patching, backup validation, or failover testing.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Feature<\/th><th>Cloud SQL (Enterprise)<\/th><th>Self-Managed on GCE<\/th><\/tr><\/thead><tbody><tr><td>Patching<\/td><td>Automated (maintenance window)<\/td><td>Manual, your responsibility<\/td><\/tr><tr><td>Backups<\/td><td>Automated daily + PITR, 7-day default retention<\/td><td>pg_dump \/ pgBackRest, self-managed<\/td><\/tr><tr><td>High Availability<\/td><td>One checkbox (regional HA with automatic failover)<\/td><td>Patroni\/repmgr + load balancer, significant setup<\/td><\/tr><tr><td>Read Replicas<\/td><td>API call or Terraform resource<\/td><td>Manual streaming replication config<\/td><\/tr><tr><td>Connection Pooling<\/td><td>Built-in (pgBouncer via AlloyDB Omni, or Auth Proxy)<\/td><td>PgBouncer\/Pgpool, self-managed<\/td><\/tr><tr><td>IAM Auth<\/td><td>Native (no passwords in connection strings)<\/td><td>Not applicable<\/td><\/tr><tr><td>Scale to Zero<\/td><td>Not supported (minimum 1 vCPU always running)<\/td><td>Not applicable<\/td><\/tr><tr><td>Max Storage<\/td><td>64 TB<\/td><td>Limited by disk size<\/td><\/tr><tr><td>PostgreSQL Versions<\/td><td>14, 15, 16, 17<\/td><td>Any version you compile<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Pricing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cloud SQL Enterprise edition charges per vCPU-hour and per GiB-hour of memory. There is no free tier for production instances (the free trial gives $300 in credits). Here are the real numbers for <code>us-central1<\/code> as of April 2026:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Resource<\/th><th>Rate<\/th><th>Minimal Instance (2 vCPU, 8 GiB)<\/th><\/tr><\/thead><tbody><tr><td>vCPU<\/td><td>$0.0413\/hr<\/td><td>$60.30\/month<\/td><\/tr><tr><td>Memory<\/td><td>$0.007\/GiB-hr<\/td><td>$40.88\/month<\/td><\/tr><tr><td>SSD Storage<\/td><td>$0.170\/GiB-month<\/td><td>$1.70\/month (10 GiB)<\/td><\/tr><tr><td><strong>Total (single zone)<\/strong><\/td><td><\/td><td><strong>~$103\/month<\/strong><\/td><\/tr><tr><td><strong>Total (HA, 2 zones)<\/strong><\/td><td><\/td><td><strong>~$206\/month<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">HA doubles the compute cost because GCP runs a standby instance in another zone. Storage is shared, so it does not double. For a full breakdown of how GCP services accumulate cost, the <a href=\"https:\/\/computingforgeeks.com\/gcp-costs-explained\/\" target=\"_blank\" rel=\"noreferrer noopener\">GCP costs guide<\/a> covers all the common gotchas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>GCP project with billing enabled<\/li>\n\n\n\n<li>APIs enabled: <code>sqladmin.googleapis.com<\/code>, <code>compute.googleapis.com<\/code>, <code>servicenetworking.googleapis.com<\/code><\/li>\n\n\n\n<li><code>gcloud<\/code> CLI authenticated (<code>gcloud auth application-default login<\/code>)<\/li>\n\n\n\n<li>Terraform 1.5+ with <code>google<\/code> provider 6.x<\/li>\n\n\n\n<li>A VPC network (default or custom)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Enable the required APIs:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud services enable sqladmin.googleapis.com \\\n  compute.googleapis.com \\\n  servicenetworking.googleapis.com<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Create an Instance with gcloud<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For a quick test or one-off instance, <code>gcloud<\/code> is the fastest path.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql instances create pg-demo \\\n  --database-version=POSTGRES_17 \\\n  --tier=db-custom-2-8192 \\\n  --region=us-central1 \\\n  --storage-size=10GB \\\n  --storage-type=SSD \\\n  --storage-auto-increase \\\n  --backup-start-time=03:00 \\\n  --enable-point-in-time-recovery \\\n  --maintenance-window-day=SUN \\\n  --maintenance-window-hour=4 \\\n  --deletion-protection<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Instance creation takes 3-5 minutes. Once ready, set the postgres user password:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql users set-password postgres \\\n  --instance=pg-demo \\\n  --password='YourSecurePassword2026!'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a database:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql databases create appdb --instance=pg-demo<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Create with Terraform<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform gives you reproducible, version-controlled infrastructure. The configuration below creates the instance, database, and user.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_sql_database_instance\" \"postgres\" {\n  name             = \"pg-demo\"\n  database_version = \"POSTGRES_17\"\n  region           = \"us-central1\"\n  project          = PROJECT_ID\n\n  deletion_protection = false  # Set true in production\n\n  settings {\n    tier              = \"db-custom-2-8192\"\n    disk_size         = 10\n    disk_type         = \"PD_SSD\"\n    disk_autoresize   = true\n    availability_type = \"ZONAL\"  # \"REGIONAL\" for HA\n\n    backup_configuration {\n      enabled                        = true\n      start_time                     = \"03:00\"\n      point_in_time_recovery_enabled = true\n      transaction_log_retention_days = 7\n\n      backup_retention_settings {\n        retained_backups = 7\n      }\n    }\n\n    maintenance_window {\n      day          = 7  # Sunday\n      hour         = 4\n      update_track = \"stable\"\n    }\n\n    ip_configuration {\n      ipv4_enabled    = false\n      private_network = google_compute_network.vpc.id\n    }\n\n    database_flags {\n      name  = \"cloudsql.iam_authentication\"\n      value = \"on\"\n    }\n  }\n\n  depends_on = [google_service_networking_connection.private_vpc_connection]\n}\n\nresource \"google_sql_database\" \"appdb\" {\n  name     = \"appdb\"\n  instance = google_sql_database_instance.postgres.name\n}\n\nresource \"google_sql_user\" \"app_user\" {\n  name     = \"appuser\"\n  instance = google_sql_database_instance.postgres.name\n  password = var.db_password\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Store the <code>db_password<\/code> variable in <a href=\"https:\/\/computingforgeeks.com\/google-cloud-secret-manager-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">Secret Manager<\/a> or a <code>terraform.tfvars<\/code> file excluded from version control. Never hardcode passwords in Terraform configs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Private IP Networking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By default, Cloud SQL gets a public IP. For production, disable the public IP and use private IP via VPC peering. This keeps database traffic off the internet entirely.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The private IP setup requires three resources: a reserved IP range, a VPC peering connection to Google&#8217;s service networking, and the Cloud SQL instance configured with <code>ipv4_enabled = false<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_compute_global_address\" \"private_ip_range\" {\n  name          = \"cloudsql-private-ip\"\n  purpose       = \"VPC_PEERING\"\n  address_type  = \"INTERNAL\"\n  prefix_length = 16\n  network       = google_compute_network.vpc.id\n}\n\nresource \"google_service_networking_connection\" \"private_vpc_connection\" {\n  network                 = google_compute_network.vpc.id\n  service                 = \"servicenetworking.googleapis.com\"\n  reserved_peering_ranges = [google_compute_global_address.private_ip_range.name]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With this in place, the Cloud SQL instance gets an internal IP from your VPC range. GKE pods and Compute Engine VMs in the same VPC can reach it directly. No public endpoint, no Cloud SQL Auth Proxy needed for basic connectivity (though the proxy adds connection pooling and IAM auth, which are still valuable).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">IAM Database Authentication<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">IAM auth eliminates passwords for database connections. Instead, the connecting service account gets a short-lived OAuth2 token that Cloud SQL validates. This is the recommended approach for GKE workloads using <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Workload Identity<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Enable IAM auth on the instance (we already set the database flag in Terraform). Create an IAM database user:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_sql_user\" \"iam_user\" {\n  name     = \"app-sa@PROJECT_ID.iam.gserviceaccount.com\"\n  instance = google_sql_database_instance.postgres.name\n  type     = \"CLOUD_IAM_SERVICE_ACCOUNT\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Grant the service account the <code>roles\/cloudsql.instanceUser<\/code> role and <code>roles\/cloudsql.client<\/code> role. The <code>instanceUser<\/code> role allows login, while <code>client<\/code> allows connecting via the Auth Proxy.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud projects add-iam-policy-binding PROJECT_ID \\\n  --member=\"serviceAccount:app-sa@PROJECT_ID.iam.gserviceaccount.com\" \\\n  --role=\"roles\/cloudsql.instanceUser\"\n\ngcloud projects add-iam-policy-binding PROJECT_ID \\\n  --member=\"serviceAccount:app-sa@PROJECT_ID.iam.gserviceaccount.com\" \\\n  --role=\"roles\/cloudsql.client\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then grant database-level permissions inside PostgreSQL:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>GRANT ALL PRIVILEGES ON DATABASE appdb TO \"app-sa@PROJECT_ID.iam\";<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Backups and Point-in-Time Recovery<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cloud SQL automated backups are enabled by default with a 7-day retention window. The backup runs daily at the time you specify (03:00 in our config). Point-in-time recovery uses the WAL (write-ahead log) to restore to any second within the retention window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Restore to a specific timestamp:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql instances clone pg-demo pg-demo-restored \\\n  --point-in-time=\"2026-04-10T14:30:00.000Z\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This creates a new instance from the backup. Cloud SQL does not support in-place PITR because that would require downtime on the running instance. The clone approach lets you validate the restore before switching traffic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Read Replicas<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Read replicas use PostgreSQL streaming replication under the hood. They are eventually consistent (replication lag is typically under 1 second) and can be in the same region or a different one for disaster recovery.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_sql_database_instance\" \"read_replica\" {\n  name                 = \"pg-demo-replica\"\n  master_instance_name = google_sql_database_instance.postgres.name\n  region               = \"us-central1\"\n  database_version     = \"POSTGRES_17\"\n\n  replica_configuration {\n    failover_target = false\n  }\n\n  settings {\n    tier            = \"db-custom-2-8192\"\n    disk_autoresize = true\n    disk_type       = \"PD_SSD\"\n\n    ip_configuration {\n      ipv4_enabled    = false\n      private_network = google_compute_network.vpc.id\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Point read-heavy application queries at the replica&#8217;s IP to offload the primary. Connection strings in your application should distinguish between write (primary) and read (replica) endpoints.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Connect from GKE with the Auth Proxy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Cloud SQL Auth Proxy handles encryption, IAM auth, and connection management. On GKE, run it as a sidecar container in the same pod as your application. This pattern means your app connects to <code>localhost:5432<\/code> and the proxy handles everything else.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>containers:\n  - name: app\n    image: your-app:latest\n    env:\n      - name: DB_HOST\n        value: \"127.0.0.1\"\n      - name: DB_PORT\n        value: \"5432\"\n      - name: DB_NAME\n        value: \"appdb\"\n  - name: cloud-sql-proxy\n    image: gcr.io\/cloud-sql-connectors\/cloud-sql-proxy:2.14.3\n    args:\n      - \"--structured-logs\"\n      - \"--auto-iam-authn\"\n      - \"PROJECT_ID:us-central1:pg-demo\"\n    securityContext:\n      runAsNonRoot: true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>--auto-iam-authn<\/code> flag enables automatic IAM authentication. Combined with <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">GKE Workload Identity<\/a>, no service account key file is needed. The Kubernetes service account maps to a GCP service account that has <code>roles\/cloudsql.client<\/code>, and the proxy uses it transparently.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Connect from Compute Engine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your application runs on a Compute Engine VM in the same VPC, you can connect directly to the private IP without the Auth Proxy. Install the PostgreSQL client:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y postgresql-client<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Connect using the private IP (find it in the Cloud SQL instance details or Terraform output):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>psql -h 10.0.1.50 -U appuser -d appdb<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For production, still use the Auth Proxy even on Compute Engine. It adds connection pooling and handles SSL certificate rotation automatically.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Monitoring<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cloud SQL exposes metrics natively in Cloud Monitoring (no agent installation needed). The most important metrics to watch:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>database\/cpu\/utilization<\/strong>: sustained usage above 80% means it is time to scale up vCPUs<\/li>\n\n\n\n<li><strong>database\/memory\/utilization<\/strong>: PostgreSQL uses shared_buffers aggressively, so 70-80% usage is normal<\/li>\n\n\n\n<li><strong>database\/disk\/utilization<\/strong>: enable auto-resize and alert at 85%<\/li>\n\n\n\n<li><strong>database\/postgresql\/num_backends<\/strong>: connection count approaching <code>max_connections<\/code> means you need connection pooling<\/li>\n\n\n\n<li><strong>database\/replication\/replica_byte_lag<\/strong>: for read replicas, sustained lag above 1 MB indicates the replica cannot keep up<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Create an alert policy for high CPU:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud alpha monitoring policies create \\\n  --notification-channels=CHANNEL_ID \\\n  --display-name=\"Cloud SQL CPU > 80%\" \\\n  --condition-display-name=\"High CPU\" \\\n  --condition-filter='resource.type=\"cloudsql_database\" AND metric.type=\"cloudsql.googleapis.com\/database\/cpu\/utilization\"' \\\n  --condition-threshold-value=0.8 \\\n  --condition-threshold-comparison=COMPARISON_GT \\\n  --condition-threshold-duration=300s<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Production Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before going live, verify these settings:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>High Availability<\/strong>: set <code>availability_type = \"REGIONAL\"<\/code> in Terraform. This creates a standby in another zone with automatic failover (doubles compute cost)<\/li>\n\n\n\n<li><strong>Maintenance window<\/strong>: schedule during lowest-traffic hours. Maintenance can cause a brief restart<\/li>\n\n\n\n<li><strong>Storage auto-resize<\/strong>: enabled by default, but set a storage auto-resize limit to prevent runaway growth from a bug flooding the database<\/li>\n\n\n\n<li><strong>Deletion protection<\/strong>: set <code>deletion_protection = true<\/code> in Terraform. Without it, a <code>terraform destroy<\/code> deletes the database with no confirmation<\/li>\n\n\n\n<li><strong>Private IP only<\/strong>: disable the public IP (<code>ipv4_enabled = false<\/code>). If you need occasional public access for debugging, use the Auth Proxy from your local machine instead<\/li>\n\n\n\n<li><strong>Database flags<\/strong>: tune <code>shared_buffers<\/code> (25% of RAM), <code>work_mem<\/code>, and <code>max_connections<\/code> based on workload. Cloud SQL exposes these as database flags<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Cloud SQL vs AWS RDS PostgreSQL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you are evaluating both clouds, this comparison covers the differences that actually matter in practice.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Feature<\/th><th>GCP Cloud SQL<\/th><th>AWS RDS PostgreSQL<\/th><\/tr><\/thead><tbody><tr><td>Pricing model<\/td><td>Per vCPU-hour + per GiB-hour<\/td><td>Per instance-hour (fixed tiers)<\/td><\/tr><tr><td>Minimal instance<\/td><td>~$103\/month (2 vCPU, 8 GiB)<\/td><td>~$49\/month (db.t4g.medium, 2 vCPU, 4 GiB)<\/td><\/tr><tr><td>HA architecture<\/td><td>Regional (standby in another zone)<\/td><td>Multi-AZ (synchronous standby)<\/td><\/tr><tr><td>Read replicas<\/td><td>Same or cross-region<\/td><td>Same or cross-region, up to 15<\/td><\/tr><tr><td>Serverless scale-to-zero<\/td><td>Not supported<\/td><td>Aurora Serverless v2 (scales to 0.5 ACU)<\/td><\/tr><tr><td>Private networking<\/td><td>VPC peering (google_service_networking)<\/td><td>Subnet placement (no peering needed)<\/td><\/tr><tr><td>IAM auth<\/td><td>Native (IAM database users)<\/td><td>Supported (RDS IAM auth tokens)<\/td><\/tr><tr><td>Connection proxy<\/td><td>Cloud SQL Auth Proxy (sidecar)<\/td><td>RDS Proxy (managed, $$$)<\/td><\/tr><tr><td>Backup retention<\/td><td>1-365 days<\/td><td>0-35 days (automated), manual snapshots unlimited<\/td><\/tr><tr><td>Max storage<\/td><td>64 TB<\/td><td>64 TB (128 TB with io2)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">RDS wins on entry-level pricing because of smaller instance types (t4g.micro starts at ~$12\/month). Cloud SQL wins on IAM integration depth and the Auth Proxy&#8217;s zero-config connection handling. Both are solid choices; pick the one that fits your existing cloud footprint.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Failed to create subnetwork. Couldn&#8217;t find free blocks in allocated IP ranges&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The reserved IP range for VPC peering is exhausted. Either the <code>prefix_length<\/code> is too small or another Cloud SQL instance already consumed the range. Increase the prefix length (e.g., from \/24 to \/16) or create an additional reserved range.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Connection timed out&#8221; from GKE pods<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most common cause is a missing VPC peering route. Verify the peering connection is active:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud compute networks peerings list --network=NETWORK_NAME<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the peering shows <code>ACTIVE<\/code> but connections still time out, check that the GKE cluster&#8217;s node network can route to the Cloud SQL private IP range. On Shared VPC setups, the host project must have the peering, not the service project.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;FATAL: Cloud SQL IAM user authentication failed&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The IAM database user does not match the connecting service account. The username must be the full email without the <code>.gserviceaccount.com<\/code> domain suffix for PostgreSQL IAM users, or with it for Cloud IAM service account types. Double-check the <code>google_sql_user<\/code> resource type: it should be <code>CLOUD_IAM_SERVICE_ACCOUNT<\/code> for service accounts and <code>CLOUD_IAM_USER<\/code> for human users.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Terraform destroy fails with &#8220;deletion_protection is enabled&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Set <code>deletion_protection = false<\/code> in the Terraform config, run <code>terraform apply<\/code> to update the instance, then run <code>terraform destroy<\/code>. This two-step process is intentional: it prevents accidental destruction of production databases.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cleanup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Remove all resources. If using Terraform:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>terraform destroy<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the instance has <code>deletion_protection<\/code> enabled, disable it first:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql instances patch pg-demo --no-deletion-protection\ngcloud sql instances delete pg-demo<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Delete the read replica separately (replicas must be deleted before the primary if using <code>gcloud<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud sql instances delete pg-demo-replica<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Can Cloud SQL PostgreSQL scale to zero?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Cloud SQL always runs at least one instance with the configured vCPU and memory. There is no serverless mode that scales to zero. If you need scale-to-zero for development databases, consider AlloyDB Omni (self-hosted) or Aurora Serverless v2 on AWS.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What PostgreSQL versions does Cloud SQL support?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">As of April 2026, Cloud SQL supports PostgreSQL 14, 15, 16, and 17. Version 17 is the latest available. Major version upgrades are supported in-place, but test the upgrade on a clone first because some extensions may need recompilation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How does high availability work?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Regional HA creates a standby instance in a different zone within the same region. Replication is synchronous: every write is confirmed on both the primary and standby before being acknowledged to the client. Failover is automatic and typically completes in under 60 seconds. The IP address stays the same after failover.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is the Cloud SQL Auth Proxy required?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not required, but strongly recommended. Without the proxy, you connect directly to the private IP using a password. The proxy adds automatic SSL\/TLS encryption, IAM-based authentication (no passwords), and connection health checks. On GKE, run it as a sidecar. On Compute Engine, run it as a systemd service.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I migrate from self-managed PostgreSQL to Cloud SQL?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use the Database Migration Service (DMS). Create a migration job with the source as your self-managed instance and the destination as a new Cloud SQL instance. DMS handles the initial full dump and then continuous replication via logical decoding until you are ready to cut over. Test the migration with a dry run first.<\/p>\n\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can Cloud SQL PostgreSQL scale to zero?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No. Cloud SQL always runs at least one instance with the configured vCPU and memory. There is no serverless mode. Consider AlloyDB Omni or Aurora Serverless v2 if you need scale-to-zero.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What PostgreSQL versions does Cloud SQL support?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"As of April 2026, Cloud SQL supports PostgreSQL 14, 15, 16, and 17. Major version upgrades are supported in-place.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How does Cloud SQL high availability work?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Regional HA creates a synchronous standby in a different zone. Failover is automatic and completes in under 60 seconds. The IP address stays the same after failover.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is the Cloud SQL Auth Proxy required?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Not required but strongly recommended. It adds automatic SSL\/TLS, IAM authentication without passwords, and connection health checks. Run it as a sidecar on GKE or a systemd service on Compute Engine.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How do I migrate from self-managed PostgreSQL to Cloud SQL?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Use the Database Migration Service (DMS). It handles the initial full dump and continuous replication via logical decoding until you cut over.\"\n      }\n    }\n  ]\n}\n<\/script>","protected":false},"excerpt":{"rendered":"<p>Production Cloud SQL PostgreSQL 17 setup with Terraform. Private IP, IAM auth, backups, read replicas, Auth Proxy for GKE, and real cost breakdown.<\/p>\n","protected":false},"author":3,"featured_media":165831,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2680,36939],"tags":[212,324,36175,688,559],"cfg_series":[39811],"class_list":["post-165828","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cloud","category-gcp","tag-automation","tag-databases","tag-gcp","tag-postgresql","tag-terraform","cfg_series-gcp-platform"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165828","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=165828"}],"version-history":[{"count":0,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165828\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/165831"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=165828"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=165828"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=165828"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=165828"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}