Chapter 12

Relational Databases

Scope. This chapter covers Google Cloud's managed relational databases — Cloud SQL, AlloyDB, and Cloud Spanner — from the security engineer's position: network exposure, authentication, encryption, backup, and the operational controls that decide blast radius when a database is compromised or misconfigured. It does not teach SQL, schema design, or query tuning. Cloud KMS mechanics are Chapter 14, Secret Manager is Chapter 13, and Sensitive Data Protection is Chapter 15; this chapter cites each rather than repeating it. Prerequisites. Chapter 5 (§5.4 subnets, §5.18 Private Service Access), Chapter 3 (§3.9 IAM roles, §3.4 groups), §2.30§2.31 organization policy. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0, hashicorp/google provider 8.x; see sources at end.

A managed relational database removes patching, replication plumbing, and failover orchestration from your team's workload, but it does not remove the decisions that determine whether a breach of the application tier becomes a breach of the data tier. Every Cloud SQL instance ships with a public IP option, a legacy password-based user model, and a default encryption posture that is adequate but not the strongest available — three defaults a SecDevOps engineer has to actively override, not merely accept.

This chapter treats network isolation, authentication, and encryption as the three axes that matter most. Private IP and Private Service Access remove the database from the public internet entirely; IAM database authentication removes the shared password from the picture for the engines that support it; the Cloud SQL Auth Proxy and its newer connector library give an encrypted, IAM-authorized path even where private networking alone is not enough. Backups, point-in-time recovery, and high availability protect against data loss and availability failure — a different threat model from an attacker with network access, but one every production database needs regardless.

The chapter also covers where Cloud SQL stops being the right tool. AlloyDB adds PostgreSQL-compatible performance for demanding transactional and analytical workloads without changing the security model; Cloud Spanner trades regional simplicity for externally-consistent, horizontally scalable, multi-region SQL. §12.26 closes the chapter with the decision framework: which engine, and why, on the axes that actually matter to an operator who has to run it.

12.1 Cloud SQL §

Cloud SQL is Google Cloud's managed relational database service for PostgreSQL, MySQL, and SQL Server. Google handles the underlying VM, storage, OS patching, and database engine binary; you retain control of the schema, the users, the network exposure, and the configuration flags that govern behavior. It is a single-region service at the instance level — a primary lives in one zone or, with high availability, is backed by a standby in a second zone of the same region (§12.8). It is not multi-region; for that, see AlloyDB (§12.20) or Cloud Spanner (§12.23).

Cloud SQL instances come in two edition tiers set with --edition: Enterprise and Enterprise Plus. Enterprise Plus adds a local-SSD-backed data cache that materially improves read performance for read-heavy workloads, and near-zero-downtime maintenance — Google's own comparison states Enterprise Plus delivers sub-second downtime for maintenance and planned operations, against roughly sixty seconds of downtime on Enterprise. For a production instance where a maintenance window (§12.12) is a real availability risk, Enterprise Plus is the correct default, not an upsell to defer.

The instance name is a project-scoped resource name, not a DNS name: this book's convention is rc-saas-<env>-sql-<nn>, for example rc-saas-prod-sql-01 in rc-saas-prod-data-01. The instance's fully qualified connection identity — used by the Auth Proxy and IAM database authentication — is PROJECT_ID:REGION:INSTANCE, so rc-saas-prod-data-01:us-central1:rc-saas-prod-sql-01 (§12.17).

Security posture. A newly created instance can be given a public IP with no authorized networks configured, which leaves it reachable only by IP allowlist rules you add — but the temptation to add a broad allowlist is the single most common Cloud SQL misconfiguration. The least-privilege default for this book is --no-assign-ip with Private Service Access (§12.6), IAM database authentication where the engine supports it (§12.7), and the Auth Proxy or a language connector for anything that must reach the instance from outside the VPC (§12.17). Organization policy enforces this at the project or organization level (§2.30) with constraints/sql.restrictPublicIp and constraints/sql.restrictAuthorizedNetworks — apply the managed variants, constraints/sql.managed.restrictPublicIp and constraints/sql.managed.restrictAuthorizedNetworks, on estates that have adopted Google's current constraint generation.

Console. Console → SQL → Create instance, choosing the engine, edition, region, and machine configuration. Network settings (Private IP, Public IP, authorized networks) and data protection settings (automated backups, point-in-time recovery, deletion protection) are both on the creation page and worth setting there rather than patching afterward.

CLI.

gcloud sql instances create rc-saas-prod-sql-01 \
  --project=rc-saas-prod-data-01 \
  --database-version=POSTGRES_17 \
  --edition=ENTERPRISE_PLUS \
  --tier=db-perf-optimized-N-4 \
  --region=us-central1 \
  --availability-type=REGIONAL \
  --no-assign-ip \
  --network=projects/rc-saas-shared-net-01/global/networks/vpc-prod \
  --enable-google-private-path \
  --deletion-protection \
  --require-ssl

API and IAM. The resource is sqladmin.googleapis.com projects.instances. Day-to-day operators hold roles/cloudsql.client (connect only) or roles/cloudsql.instanceUser (IAM database authentication login, §12.7); platform teams hold roles/cloudsql.admin. None of these should be layered on top of a project-wide Editor grant — grant them directly on the project or, once resource-level IAM for individual instances is available in your estate, on the instance.

Pitfall. --tier and --edition interact: some machine configurations and features (the data cache, near-zero-downtime maintenance) are Enterprise Plus–only, and downgrading an existing Enterprise Plus instance to Enterprise is a real migration, not a flag flip. Decide the edition at design time.

12.2 PostgreSQL §

Cloud SQL for PostgreSQL supports a defined set of major versions at any time — the gcloud sql instances create --help enumeration confirms POSTGRES_9_6 through POSTGRES_18 as valid --database-version values in the currently installed SDK. Confirm the exact set available to your project at creation time; older major versions age out of support and a create call will reject a retired one outright.

Two settings matter most for a security engineer. cloudsql.enable_pgaudit, a Cloud SQL–specific boolean database flag, turns on pgAudit's detailed, session- and object-level audit logging — the only path to production-grade audit trails on PostgreSQL, since the built-in logging flags are coarser. log_connections and log_disconnections record session lifecycle events with no restart required; log_statement (none, ddl, mod, all) records statement text, but Google's own flag documentation warns that a non-none setting can log statements containing passwords, and recommends pgAudit instead for anything beyond DDL-only logging.

IAM database authentication (§12.7) is supported here, and it is the posture this book prefers: enable it at creation or with gcloud sql instances patch --database-flags=cloudsql.iam_authentication=on, then map IAM principals to database users instead of distributing passwords at all.

gcloud sql instances create rc-saas-prod-sql-02 \
  --project=rc-saas-prod-data-01 \
  --database-version=POSTGRES_17 \
  --region=us-central1 \
  --no-assign-ip \
  --network=projects/rc-saas-shared-net-01/global/networks/vpc-prod \
  --database-flags=cloudsql.enable_pgaudit=on,log_connections=on,\
log_disconnections=on,cloudsql.iam_authentication=on

Pitfall. log_statement=all is the flag teams reach for when an auditor asks for query logging, and it writes statement text — including statements that embed a password in a CREATE ROLE or ALTER USER — into Cloud Logging, where a much wider audience can read it than could read the database. Use pgAudit for audit requirements and keep log_statement at none or ddl.

12.3 MySQL §

Cloud SQL for MySQL's supported major versions, per the installed SDK's enumeration, run MYSQL_5_6 through MYSQL_9_7 as valid --database-version values — but MYSQL_5_6 and MYSQL_5_7 are legacy and should not appear in a new deployment; default to the current MYSQL_8_0 or MYSQL_8_4 line unless an application dependency forces otherwise.

The flag that matters most for security is local_infile, which controls whether LOAD DATA LOCAL INFILE can read arbitrary files from the client's filesystem into a table — a path that has been used for both data exfiltration and, on vulnerable clients, arbitrary file read. Leave it off unless a specific, reviewed workload needs it. Cloud SQL for MySQL does not run the open-source MySQL Enterprise Audit plugin; its own audit capability is the cloudsql_mysql_audit_* flag family (write period, max query length, data-masking commands and regex), which writes structured audit events to Cloud Logging — turn it on for any instance holding regulated data rather than relying on general_log, which is unstructured and expensive at volume.

IAM database authentication (§12.7) is supported for MySQL, using the same cloudsql.iam_authentication flag as PostgreSQL.

Pitfall. MySQL truncates long usernames, and a service account email is long. The database username derived from an IAM principal is therefore not always the full address, which breaks grants written against the address you expected. Confirm the actual derived username on the instance before writing GRANT statements against it.

12.4 SQL Server §

Cloud SQL for SQL Server ships in Express, Web, Standard, and Enterprise editions across the 2017, 2019, 2022, and 2025 major versions, set through --database-version (for example SQLSERVER_2022_STANDARD). The edition is a licensing decision as much as a technical one: Express caps CPU, memory, and database size and is not appropriate for production; Enterprise is required for the highest resource ceilings and carries the corresponding license cost baked into the tier price.

The security-relevant flag unique to this engine is contained database authentication, a boolean server configuration option that lets a database authenticate users using credentials stored in the database itself rather than in server-level logins — useful for portability but expanding the database's own attack surface, since a contained database can create its own logins without server-admin involvement. Review its use case by case rather than enabling it estate-wide. The remote access flag governs remote procedure calls and should stay off unless a specific replication or linked-server scenario requires it.

IAM database authentication is not supported for SQL Server — Cloud SQL's IAM login mechanism (§12.7) covers PostgreSQL and MySQL only. SQL Server authentication therefore depends entirely on SQL Server logins, making credential rotation through Secret Manager (§12.15) and network isolation (§12.6) proportionally more important for this engine.

12.5 Database Instances §

An instance is the unit of compute, storage, networking, and lifecycle for Cloud SQL: one engine, one major version, one region, one machine configuration, holding any number of databases and users. Instances are not free-standing — they belong to a project, inherit that project's organization policy, and are billed continuously whether or not a connection is active.

Machine configuration is set through --tier (a predefined shape) or the --cpu/--memory pair for a custom shape, and storage through --storage-size with --storage-type of SSD (recommended) or the legacy HDD. Storage grows automatically by default (--storage-auto-increase) and never shrinks — plan the initial size deliberately rather than over-provisioning as a habit, since downsizing means a migration to a new instance.

gcloud sql instances describe INSTANCE returns the full resource, including IP addresses, current database flags, backup configuration, and the disk encryption key if CMEK is configured (§12.14) — it is the fastest way to audit an existing instance's posture without opening the console. gcloud sql instances patch changes most settings in place; a few, including --database-version for a downgrade and the disk encryption key, cannot be changed after creation and require a new instance plus migration (§12.19).

Pitfall. gcloud sql instances patch applies some changes immediately and defers others to the next maintenance window (§12.12); a flag change that requires a restart is not instantaneous even when the command returns success. Use --database-flags review in a maintenance window you control for production instances.

12.6 Private IP §

Private IP gives a Cloud SQL instance an address inside your VPC's allocated range instead of a public one, reached over the peering that Private Service Access establishes (§5.18 owns the PSA mechanism: the allocated range, the peering connection, and which managed services use it). This book's fixed PSA range is 10.160.0.0/24, peered into vpc-prod, alongside the 10.128.0.0/20 application subnet.

Configure it with --no-assign-ip and --network pointing at the Shared VPC. For an instance whose clients are entirely inside the VPC — the common case — this closes the public attack surface completely: there is no IP for an internet scanner to find. --enable-google-private-path additionally lets Google-managed services in other projects (for example a Cloud Run service using Private Service Connect, or a Dataflow job) reach the instance over Google's internal network without a VPC peering of their own.

The anti-pattern, worth naming because it still appears in older tutorials and architecture diagrams: a public IP restricted to a list of --authorized-networks. It works, but every authorized CIDR is a standing exception that must be reviewed indefinitely, the list drifts as office and NAT egress IPs change, and it puts the instance's listener directly on the internet regardless of who is allowed to reach it. Prefer Private IP; where a migration path genuinely needs a temporary public listener, scope --authorized-networks to the narrowest possible range and remove it once the private path is live.

Private Service Connect for Cloud SQL is GA and offers an alternative to PSA peering: the consumer creates a PSC endpoint with its own address in its own subnet, and the connection never establishes a VPC peering at all — useful where PSA's non-transitive peering (§5.18) or its shared, unpeerable address space is a constraint, such as connecting from a separate organization or a network that already peers elsewhere.

gcloud sql instances patch rc-saas-prod-sql-01 \
  --project=rc-saas-prod-data-01 \
  --network=projects/rc-saas-shared-net-01/global/networks/vpc-prod \
  --no-assign-ip \
  --enable-google-private-path

12.7 IAM Database Authentication §

IAM database authentication lets a PostgreSQL or MySQL user log in with a short-lived Google-issued token in place of a database password, so the credential a compromised application leaks is neither long-lived nor reusable outside its narrow scope. It is supported for PostgreSQL and MySQL; SQL Server has no equivalent mechanism (§12.4).

Enable it on the instance with the cloudsql.iam_authentication database flag, then create a database user of --type=cloud_iam_service_account or --type=cloud_iam_user mapped to the IAM principal's email (a service account's email for workload access, following this book's sa-app-<env> naming). The principal must also hold roles/cloudsql.instanceUser and roles/cloudsql.client to authenticate and open the connection. Grant it to sa-app-prod, the application's attached service account, rather than to a human principal for anything running in production.

gcloud sql users create sa-app-prod@rc-saas-prod-data-01.iam \
  --project=rc-saas-prod-data-01 \
  --instance=rc-saas-prod-sql-01 \
  --type=cloud_iam_service_account

Connect through the Auth Proxy or a language connector with --auto-iam-authn (§12.17); the proxy exchanges the caller's IAM credential for a short-lived database login on each connection, so there is no password to distribute, rotate, or leak. This is the preferred authentication posture for this book wherever the engine supports it — the database-specific password position for the engines and cases where it does not is covered in §12.15.

Pitfall. IAM database authentication governs authentication, not authorization — the mapped database user still needs the SQL-level GRANT privileges you would assign any other user. An IAM login with default privileges and an over-permissioned public schema is still an over-permissioned account.

12.8 High Availability §

Cloud SQL high availability keeps a synchronously replicated standby instance in a second zone of the same region, promoted automatically if the primary fails. Set --availability-type=REGIONAL at creation, or patch an existing zonal instance to it later. Google's own documentation describes writes as replicated to disk in both zones before a transaction is reported committed, so a successful commit already has zone-redundant durability, and states that during a failover you can expect the instance to be unavailable for about a minute, with the caveat that the exact duration varies by environment — treat that as a qualitative expectation of roughly a minute of unavailability rather than a number to design a stricter SLA around. No formal RTO is published. The high-availability page states only "When a failover occurs, you can expect the instance to be unavailable for about sixty seconds," immediately followed by "This duration might differ based on your Cloud SQL environment." The SLA covers the HA configuration, not a failover duration, so an RTO tighter than a minute is yours to prove by testing (§E.9), not Google's to guarantee.

A continuous heartbeat check drives failover, and Google Cloud states plainly that failover does not proceed if both the primary and standby are unresponsive at once — HA protects against a single-zone failure, not a regional one. For that, see read replicas across regions (§12.9) or a fundamentally multi-region engine (§12.23, §12.25). The application reconnects using the same connection string or private IP after failover; no client-side reconfiguration is required, though connection pools and long-lived sessions should be tuned to retry rather than fail hard on the interruption.

Security posture. HA is a resilience control, not an access control — the standby inherits every network and IAM setting of the primary, so misconfigured public access or authorized networks apply to both. Manual failover for testing is gcloud sql instances failover INSTANCE; run it in a non-production instance first to confirm application retry behavior before relying on it in an incident.

12.9 Read Replicas §

A read replica is an independent, queryable copy of a Cloud SQL instance kept current through the engine's native replication stream, used to offload read traffic and, when placed in a second region, to provide a warm copy for regional failure. Unlike the HA standby (§12.8), a replica is a distinct billed instance you connect to directly and can promote to a standalone primary.

Create one with gcloud sql instances create REPLICA_NAME --master-instance-name=PRIMARY. Cross-region replicas inherit the primary's network configuration model but need their own Private Service Access peering or Private IP allocation in the replica's region — a common oversight is provisioning the replica with a public IP because the private range was never extended to the second region.

Security posture. A replica is a full copy of the data with its own IAM surface; grant roles/cloudsql.client and, for IAM database authentication, roles/cloudsql.instanceUser on the replica explicitly rather than assuming a grant on the primary covers it. Promotion (gcloud sql instances promote-replica) is irreversible and should require the same change approval as any other production database topology change.

12.10 Backups §

Automated backups run on a daily window you set with --backup-start-time, retained by count with --retained-backups-count and, for transaction logs supporting point-in-time recovery, by day count with --retained-transaction-log-days. On-demand backups (gcloud sql backups create --instance=INSTANCE) supplement the automated schedule before a risky change such as a major version upgrade or a bulk data migration.

Security posture. Backups are full copies of production data and inherit the source instance's sensitivity, but not automatically its access controls — a backup export to Cloud Storage (§12.19, Chapter 11) lands in a bucket with its own IAM, and that bucket must be reviewed independently. Backups encrypted with CMEK (§12.14) remain encrypted with the same key; if the key is destroyed, the backup is unrecoverable along with the live instance — coordinate backup retention with the key's protection posture (§14.8).

gcloud sql instances patch rc-saas-prod-sql-01 \
  --project=rc-saas-prod-data-01 \
  --backup-start-time=09:00 \
  --enable-point-in-time-recovery \
  --retained-backups-count=30 \
  --retained-transaction-log-days=7

Pitfall. --retained-backups-count and --retained-transaction-log-days are independent settings — a high backup count with a short transaction-log retention still limits how far back point-in-time recovery (§12.11) can reach.

12.11 Point-in-Time Recovery §

Point-in-time recovery restores an instance to any moment covered by retained transaction logs, rather than only to the moment of a daily backup — the difference between losing at most a backup interval of data and losing at most a few seconds of it. It requires --enable-point-in-time-recovery and depends on --retained-transaction-log-days (§12.10) for how far back a recovery point can reach.

Recovery is performed as a clone: gcloud sql instances clone SOURCE DEST --point-in-time =TIMESTAMP --restore-database-name=DATABASE for PostgreSQL and SQL Server, or --bin-log-file-name/--bin-log-position for MySQL's binary-log-based recovery. The clone is a new, independent instance — recovery never overwrites the source, which means an incident response recovering from ransomware or accidental deletion can validate the recovered clone before cutting the application over to it.

Security posture. The recovered clone inherits none of the source's network or IAM configuration by default in the sense that it is a brand-new instance resource — apply Private IP, IAM grants, and organization policy to it exactly as you would a new production instance before it takes traffic; do not assume "cloned from a hardened instance" means "hardened."

12.12 Maintenance Windows §

Cloud SQL applies operating system and database engine patches during a maintenance window you choose with --maintenance-window-day and --maintenance-window-hour, and a --maintenance-release-channel of production (default, stable) or preview (earlier access to upcoming changes). Preview is explicitly a pre-production channel — do not run it against a production instance; use it only on a non-critical instance to see upcoming changes early.

On Enterprise Plus (§12.1), Google states maintenance operations complete with sub-second downtime, against roughly sixty seconds on Enterprise — a material difference for any instance with a tight availability target, and a reason to weight the edition decision around maintenance impact rather than data cache alone. A maintenance window still matters on Enterprise Plus for change control and predictability even though the outage itself is far shorter.

gcloud sql instances patch rc-saas-prod-sql-01 \
  --project=rc-saas-prod-data-01 \
  --maintenance-window-day=SUN \
  --maintenance-window-hour=6 \
  --maintenance-release-channel=production

12.13 Database Flags §

Database flags configure engine-level server parameters through --database-flags, a comma-separated name=value list, with boolean flags taking on/off and a value-less flag such as skip_grant_tables written with a trailing =. They are the mechanism behind every engine-specific control this chapter names — cloudsql.enable_pgaudit and log_statement for PostgreSQL (§12.2), local_infile for MySQL (§12.3), contained database authentication for SQL Server (§12.4) — set once here and referenced from each engine section rather than repeated.

Not every flag applies without a restart; check gcloud sql flags describe FLAG --database-version=VERSION for the specific engine and version before assuming a change is live immediately. Google's own guidance warns that some flag settings can affect instance availability or stability and can remove the instance from the Cloud SQL SLA — review any flag outside the documented, supported set with that in mind, and never set an availability-affecting flag on a production instance without testing it on a clone (§12.11) first.

gcloud sql instances patch rc-saas-prod-sql-02 \
  --project=rc-saas-prod-data-01 \
  --database-flags="cloudsql.enable_pgaudit=on,log_connections=on,\
log_disconnections=on,log_statement=ddl"

Pitfall. Database flags set outside Terraform drift silently: the next terraform apply that touches settings.database_flags replaces the whole list, dropping any flag added by hand. Manage flags exclusively in one place, and treat a console flag change on a production instance as an incident to reconcile, not a fix.

12.14 Encryption §

Every Cloud SQL instance is encrypted at rest by default with Google-managed keys — no configuration required. Customer-managed encryption keys (CMEK) replace the default key with one you control in Cloud KMS, giving you the ability to audit every use of the key and to revoke it independently of the database. Chapter 14 owns Cloud KMS itself — key rings, rotation, and protection levels; this section covers only the Cloud SQL wiring.

Set --disk-encryption-key (with --disk-encryption-key-keyring, --disk-encryption-key-location, and --disk-encryption-key-project as needed for a fully qualified key) at instance creation only — Google's documentation states plainly that customer-managed encryption keys cannot be enabled on an existing instance, so this is a day-one decision, not a later hardening pass. The key ring's location must match the instance's region; a multi-region or global key ring is rejected at creation.

Before creating the instance, grant the Cloud SQL service agent — service-PROJECT_NUMBER@gcp-sa-cloud-sql.iam.gserviceaccount.com — the roles/cloudkms.cryptoKeyEncrypterDecrypter role on the key, or instance creation fails. See §14.8 for how the key ring and key are provisioned and how rotation and destruction are governed.

gcloud sql instances create rc-saas-prod-sql-03 \
  --project=rc-saas-prod-data-01 \
  --database-version=POSTGRES_17 \
  --region=us-central1 \
  --no-assign-ip \
  --network=projects/rc-saas-shared-net-01/global/networks/vpc-prod \
  --disk-encryption-key=projects/rc-saas-shared-sec-01/locations/us-central1/\
keyRings/kr-us-central1-sql/cryptoKeys/k-sql-prod

Pitfall. CMEK covers the instance's storage, and it does not extend to a backup exported to Cloud Storage or a copy loaded into another system. An export lands in a bucket with its own encryption configuration (§11.19), so a CMEK'd database routinely feeds an export path that is not — and crypto-shredding the database key (§14.17) then leaves the export fully readable.

12.15 Database Secrets §

Cloud SQL's built-in database users authenticate with passwords, and the first such credential — the default administrative user created alongside the instance — is a bootstrap credential: something to rotate immediately and store in Secret Manager rather than leave at its initial value or hand out over chat. Chapter 13 owns Secret Manager's resource model, versions, and rotation mechanics (§13.7§13.10); this section states only the database-specific position.

The preferred posture is to avoid this class of credential almost entirely by using IAM database authentication (§12.7) wherever the engine supports it — PostgreSQL and MySQL — so there is no long-lived database password to store, rotate, or leak in the first place. Where a password-based user is unavoidable (SQL Server always, or a legacy application that cannot integrate the Auth Proxy or a connector), generate it with random_password, store it as a Secret Manager version, and grant the application's service account roles/secretmanager.secretAccessor scoped to that one secret.

A password must never appear in Terraform state in plain text — see §13.13 for eliminating secrets from state entirely. Note that this is a stronger requirement than "no literal secret in the configuration": random_password.result and a conventional secret_data or password argument are all persisted to state, so a configuration that generates a password and never types it still leaks it. Both resources below therefore use write-only arguments, which are never written to state and require Terraform 1.11 or later:

ephemeral "random_password" "sql_admin" {
  length  = 32
  special = true
}

resource "google_secret_manager_secret" "sql_admin" {
  project   = "rc-saas-shared-sec-01"
  secret_id = "rc-saas-prod-sql-01-admin"
  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "sql_admin" {
  secret                 = google_secret_manager_secret.sql_admin.id
  secret_data_wo         = ephemeral.random_password.sql_admin.result
  secret_data_wo_version = 1
}

resource "google_sql_user" "admin" {
  project             = "rc-saas-prod-data-01"
  instance            = "rc-saas-prod-sql-01"
  name                = "sqladmin"
  password_wo         = ephemeral.random_password.sql_admin.result
  password_wo_version = 1
}

Bumping secret_data_wo_version and password_wo_version together is what triggers a rotation; because neither value is in state, Terraform cannot diff them and uses the version counter instead. The better answer for an application principal is to skip the password entirely and use IAM database authentication (§12.7).

12.16 Connection Pooling §

A relational database has a hard ceiling on concurrent connections, and a modern application's per-request connection pattern exhausts it far faster than the SQL workload itself would justify — the classic failure mode is an autoscaling application tier that scales connections rather than throughput. AlloyDB (§12.20) ships built-in server-side connection pooling configurable at instance creation (--enable-connection-pooling, --connection-pooling-max-pool-size, --connection-pooling-pool-mode, and related flags); Cloud SQL does not, and needs pooling handled either in the application's own driver-level pool or through an external pooler.

Security posture. A pooler that multiplexes many application-level sessions onto fewer database connections tends to authenticate once and multiplex behind that identity — verify that your pooling layer does not silently downgrade per-request IAM database authentication (§12.7) to a single shared credential for the pool. Configure application-side pool sizing conservatively (a small per-instance maximum, and --max-connections reviewed at the database level) rather than relying on the database to reject excess connections gracefully under load.

12.17 Cloud SQL Auth Proxy §

The Cloud SQL Auth Proxy is the connectivity mechanism this book uses whenever a client needs to reach Cloud SQL from outside the instance's own VPC, or wants IAM-governed access without managing TLS certificates or an authorized-networks allowlist by hand. It runs as a local process — alongside the application on a VM, or as a sidecar container in a Pod or Cloud Run service — and presents a local database endpoint that the application connects to using the ordinary database protocol. Behind that local endpoint, the proxy opens a secure tunnel to a companion process at the instance, encrypting the connection with TLS 1.3 and authenticating it with the caller's IAM credential rather than a network-level allowlist or a client certificate you provision by hand. Google's documentation describes this as removing the need for authorized networks or manual SSL configuration entirely — the proxy is authorization, not merely encryption.

Any principal that connects through the proxy needs roles/cloudsql.client at minimum, and roles/cloudsql.instanceUser in addition if it uses IAM database authentication (§12.7) through the proxy's --auto-iam-authn flag. The proxy is addressed by the instance's connection name, PROJECT_ID:REGION:INSTANCE — for this book's naming, rc-saas-prod-data-01:us-central1:rc-saas-prod-sql-01. Note that cloud-sql-proxy is a separate downloadable binary, not a gcloud subcommand:

INSTANCE_CONNECTION_NAME="rc-saas-prod-data-01:us-central1:rc-saas-prod-sql-01"
cloud-sql-proxy "${INSTANCE_CONNECTION_NAME}" \
  --auto-iam-authn \
  --private-ip \
  --port=5432

--private-ip tells the proxy to reach the instance over its private address rather than a public one, so the proxy itself must run somewhere with network access to the VPC — typically the same VPC or one connected to it — even though the client-to-proxy hop can be local. --auto-iam-authn layers IAM database authentication onto the tunnel so no database password is ever configured on the client side.

Running the proxy as a sidecar — one proxy container per Pod, alongside the application container, listening on localhost — is the pattern this book recommends for GKE and Cloud Run: it keeps the IAM identity scoped to the workload's own attached service account rather than a shared proxy deployment, and it fails closed with the Pod rather than becoming a separate availability dependency. Running it alongside the application as a co-located process on a VM is the equivalent pattern for Compute Engine. Chapters 9 and 10 cite this section for the GKE volume/sidecar surface (§9.19) and the Cloud Run integration surface (§10.23) respectively rather than re-describing the proxy.

The newer alternative is the Cloud SQL Language Connectors — libraries such as the Cloud SQL Python Connector that embed the same TLS 1.3 encryption and IAM authorization directly in the application process, with no separate proxy binary or sidecar to deploy. Google describes the Python connector as removing the need to distribute SSL certificates or manage firewalls and source or destination IP addresses at all, while supporting the same automatic IAM database authentication as the proxy. Prefer a language connector for a new application in a supported language; keep the standalone proxy binary for applications, languages, or CLI tooling the connector library does not cover.

Pitfall. --connector-enforcement takes NOT_REQUIRED or REQUIRED, and the API reference is specific about what REQUIRED covers: "Require all connections to use Cloud SQL connectors, including the Cloud SQL Auth Proxy and Cloud SQL Java, Python, and Go connectors." The side effect is the part that surprises people — "Note: This disables all existing authorized networks" — so enforcing it removes the authorized-network path in one step rather than narrowing it. New instances default to NOT_REQUIRED, and patching an instance without the field leaves it unchanged.

12.18 Secure Application Connectivity §

Putting §12.6, §12.7, and §12.17 together: the secure default connection path for an application reaching Cloud SQL is a Private IP instance with no public IP, a service account identity (sa-app-prod) holding roles/cloudsql.client and, where the engine supports it, roles/cloudsql.instanceUser, connecting through a Cloud SQL Auth Proxy sidecar or a language connector using --auto-iam-authn. No password, no authorized-networks list, and no inbound public listener exist anywhere on that path.

--ssl-mode=ENCRYPTED_ONLY (or TRUSTED_CLIENT_CERTIFICATE_REQUIRED for mutual TLS) on the instance closes the one remaining gap: a client that bypasses the proxy entirely and connects with a raw, unencrypted database protocol connection over the private network. Set it regardless of whether every client is expected to use the proxy, since it costs nothing and removes a class of misconfiguration.

gcloud sql instances patch rc-saas-prod-sql-01 \
  --project=rc-saas-prod-data-01 \
  --ssl-mode=ENCRYPTED_ONLY

Pitfall. A connection pooler or ORM configured with a plain hostname and password, bypassing the proxy "temporarily" during a migration, is the most common way this posture quietly regresses. Audit --connector-enforcement and instance logs for direct-protocol connections rather than assuming the proxy is universally in use.

12.19 Database Migration Service §

Database Migration Service moves data into Cloud SQL or AlloyDB from a source database — on-premises, another cloud, or a self-managed Compute Engine instance — using a connection profile that describes the source and, for continuous replication, keeps the destination in sync until cutover. gcloud database-migration connection-profiles create has engine-scoped subcommands (cloudsql, alloydb) matching the migration destination.

Security posture. A connection profile stores the source database's connection details, including credentials, as a Database Migration Service resource — treat it with the same sensitivity as the source credential itself, and grant roles/datamigration.admin only to the operators actually running the cutover, not broadly to the data platform team. Where the source is reachable only over a private network, the migration job needs network access equivalent to the source's own security posture; do not open a public path to the source solely to simplify the migration.

Pitfall. A continuous replication job that is abandoned mid-migration (source and destination both live, application still pointed at the source) leaves a second full copy of production data with its own, easily forgotten access surface. Tear down the connection profile and job once cutover is confirmed.

12.20 AlloyDB §

AlloyDB is Google Cloud's PostgreSQL-compatible database service built for the workloads Cloud SQL's shared compute-and-storage architecture struggles with: it separates compute and storage so each scales independently, and adds a columnar cache that lets a single instance serve demanding analytical queries alongside its transactional load. It speaks the PostgreSQL wire protocol and extension surface, so existing PostgreSQL clients, drivers, and tooling work unchanged.

An AlloyDB deployment is a cluster — this book's naming rc-saas-<env>-alloy-<nn> — holding a primary instance and, optionally, read pool instances for horizontally scaled read traffic. Security posture mirrors Cloud SQL for PostgreSQL: private-only networking through the same Private Service Access peering (§12.6, §5.18), IAM database authentication, and CMEK wiring through --kms-key at cluster creation, citing §14.8 for the key itself. §12.21 and §12.22 cover the architecture and workload fit in more depth.

12.21 AlloyDB Architecture §

AlloyDB's disaggregated architecture separates the database engine (compute) from the log and data storage layer, which is distributed across multiple zones independently of the compute layer. A zonal failure that would take down a conventional single-node database's storage does not take down AlloyDB's storage layer, and adding read capacity means adding a read pool instance against the same shared storage rather than provisioning and resynchronizing a full replica.

The columnar engine (Google's marketing name for the in-memory column-oriented acceleration layer) transparently accelerates analytical queries against the same row-oriented tables transactional workloads use, without a separate ETL pipeline into a warehouse. Connection pooling is built into the instance rather than bolted on (§12.16), with --enable-connection-pooling and a family of --connection-pooling-* flags controlling pool mode, size, and idle timeouts.

Security posture. The architecture does not change the IAM or network model from Cloud SQL — the same roles/alloydb.client (connect) and roles/alloydb.databaseUser (IAM database authentication) apply, and private networking (§12.6) is the only supported connectivity path; AlloyDB has no public IP option at all, which removes an entire category of misconfiguration Cloud SQL still permits.

12.22 AlloyDB for PostgreSQL Workloads §

Choose AlloyDB over Cloud SQL for PostgreSQL when a workload genuinely needs one of its differentiators: read-heavy transactional traffic that benefits from read pool instances sharing storage with the primary, mixed transactional-and-analytical access on the same tables without a separate warehouse, or the built-in connection pooling removing an external pooler from the architecture. A straightforward transactional workload with modest read scaling needs does not need AlloyDB — Cloud SQL for PostgreSQL is simpler to operate and has no cluster/instance topology to reason about.

Migration from Cloud SQL for PostgreSQL to AlloyDB is a logical data migration (§12.19), not an in-place upgrade — AlloyDB is wire-compatible but is a distinct managed service with its own resource model.

12.23 Cloud Spanner §

Cloud Spanner is Google Cloud's horizontally scalable, globally consistent relational database: a single logical database can span regions, scale compute and storage independently and near-linearly by adding nodes or processing units, and still offer SQL, schemas, and ACID transactions rather than the eventual-consistency trade-offs typical of a horizontally scaled NoSQL store. An instancerc-saas-<env>-spanner-<nn> in this book's naming — is a compute and replication allocation; databases live inside it and can use either Spanner's native SQL dialect or, with --database-dialect=POSTGRESQL, a PostgreSQL-compatible dialect for easier migration of existing tooling.

Capacity and IAM. Spanner instances are provisioned in edition (STANDARD, ENTERPRISE, ENTERPRISE_PLUS) and sized in processing units or nodes (1,000 processing units per node) rather than a CPU/memory pair — capacity is purchased as a throughput and storage allocation shared across every database in the instance. IAM is enforced at the instance and database level: roles/spanner.databaseUser for application access, roles/spanner.databaseReader for read-only tooling, and roles/spanner.admin reserved for platform operators provisioning instances and databases.

12.24 Distributed SQL §

What "distributed SQL" buys, concretely, is external consistency: Spanner guarantees that if transaction A commits before transaction B starts, any observer sees A's effects before B's, everywhere in the system, not just within one replica. This is built on TrueTime, Google's globally synchronized clock infrastructure providing microsecond-level bounded time uncertainty across data centers, which lets Spanner order transactions correctly without the coordination overhead a conventional distributed database would need to achieve the same guarantee.

That guarantee is not free, and the price is paid in schema design. Spanner splits a table into ranges by primary key and distributes them across nodes, so a schema with a monotonically increasing primary key (an auto-incrementing integer, a timestamp) concentrates all writes onto whichever range currently holds the highest key — a hotspot that caps write throughput regardless of how much compute you add. The standard mitigation is a well-distributed key, such as a UUID or a hashed prefix ahead of a sequential component, chosen at schema design time; retrofitting key distribution onto a live, hotspotted table is a significant migration. Interleaved tables and secondary indexes carry their own distribution considerations and deserve the same care.

Pitfall. Hotspotting does not appear in testing. A load test with synthetic UUID keys distributes perfectly; the production workload keyed on created_at or an auto-increment column serializes onto one range. Test with production-shaped keys, or the first evidence will be a write-throughput ceiling that adding nodes does not move.

12.25 Multi-Region Databases §

A Spanner instance's configuration determines its replica topology: a regional configuration keeps all replicas in one region for the lowest write latency, and a multi-region configuration spreads replicas — including a majority quorum of read-write replicas — across multiple regions for regional-failure tolerance and lower read latency close to distant users. Instance configuration is set once at creation with --config and is not changed in place; moving between configurations means creating a new instance and migrating data.

The consequence of external consistency (§12.24) in a multi-region configuration is write latency: every write requires a commit across a quorum of regionally distributed replicas, so a multi-region instance's write latency is bounded by the round-trip time across its regions, not by any single region's local disk speed. Choose a multi-region configuration only when the read-latency or availability benefit outweighs that fixed write-latency floor; a workload with a single write-heavy region and geographically distributed readers is often better served by a regional configuration plus read-heavy client routing than by paying the multi-region write cost on every transaction.

Pitfall. Instance configuration is fixed at creation, so "start regional and go multi-region later" is a data migration to a new instance, not a setting change. Decide the topology against the availability requirement up front (§12.26).

12.26 Selecting a Relational Database §

The decision among Cloud SQL, AlloyDB, and Spanner is rarely about SQL feature support — all three speak PostgreSQL-compatible SQL to a first approximation — and almost always about scale ceiling, consistency model, and how much operational and IAM granularity the workload actually needs.

AxisCloud SQLAlloyDBCloud Spanner
Scale ceilingOne instance's compute/storage; read replicas extend reads onlyHigher, via disaggregated storage and read poolsNear-linear horizontal scale by adding nodes/processing units
Consistency modelStandard relational (single primary)Standard relational (single primary)External consistency, TrueTime-backed
Write availabilityZonal, or regional with HA standby (§12.8)Zonal, or regional with HARegional or true multi-region, quorum-committed
Regional write latencyLowest (single zone/region)Lowest (single zone/region)Higher in multi-region configs (§12.25)
Operational burdenLowest — closest to a conventional managed databaseLow — same model, some tuning for read poolsHigher — schema must account for key distribution (§12.24)
IAM / audit granularityInstance-level roles; pgAudit/audit-plugin logging (§12.2§12.3)Same as Cloud SQL for PostgreSQLInstance and database-level roles; no per-engine audit plugin needed
Cost shapePay for provisioned machine + storagePay for provisioned machine + storage, generally higher floorPay for processing units/nodes; scales with allocated capacity, not just data size

Default to Cloud SQL for a conventional application database with a single-region write pattern and a team that wants the least operational surface — it is the right choice for the large majority of transactional workloads in this book's reference estates. Move to AlloyDB when the same workload profile needs materially better read scaling, mixed transactional/analytical access on the same data, or you would otherwise be operating an external connection pooler and an ETL pipeline into a warehouse alongside Cloud SQL — AlloyDB folds both into the database itself. Move to Spanner only when the workload genuinely needs horizontal write scale beyond what a single primary can sustain, or an externally-consistent multi-region write path is a hard requirement — and accept in exchange a schema design discipline around key distribution (§12.24) and, in multi-region configurations, a write-latency floor no amount of compute removes (§12.25). Choosing Spanner for its scale story on a workload that never approaches a single instance's ceiling adds schema constraints and cost with no offsetting benefit.

Chapter Summary §

  • Cloud SQL is single-region per instance; use Enterprise Plus for the data cache and near-zero-downtime maintenance where availability during maintenance windows matters.
  • Instance names follow rc-saas-<env>-sql-<nn>; the connection name is PROJECT_ID:REGION:INSTANCE.
  • Private IP with Private Service Access (§5.18, §12.6) is the default; public IP with authorized networks is a standing exception list and an anti-pattern outside migration.
  • IAM database authentication (§12.7) removes the password from the picture for PostgreSQL and MySQL; SQL Server has no equivalent and depends on rotated, Secret Manager–stored passwords (§12.15, citing §13.13 for keeping them out of Terraform state).
  • cloudsql.enable_pgaudit (PostgreSQL), local_infile (MySQL, leave off), and contained database authentication (SQL Server, review case by case) are the engine-specific flags that matter most for a security engineer.
  • HA (§12.8) protects against a single-zone failure with roughly a minute of unavailability on failover; it is not a substitute for cross-region read replicas or a multi-region engine.
  • CMEK on Cloud SQL (§12.14) must be set at instance creation — it cannot be added later — and requires the Cloud SQL service agent to hold roles/cloudkms.cryptoKeyEncrypterDecrypter on the key; Chapter 14 owns key management itself.
  • §12.17 is this book's single reference for the Cloud SQL Auth Proxy: TLS 1.3, IAM authorization, sidecar or co-located deployment, and the newer Language Connectors as an in-process alternative. Chapters 9 and 10 cite it rather than re-describing it.
  • Organization policy constraints — always written with the constraints/ prefix — enforce the private-networking default: constraints/sql.restrictPublicIp, constraints/sql.restrictAuthorizedNetworks, and their managed variants (§2.30).
  • AlloyDB is wire-compatible PostgreSQL with disaggregated storage, built-in connection pooling, and no public IP option at all.
  • Cloud Spanner trades single-primary simplicity for TrueTime-backed external consistency and near-linear horizontal scale; schema design must avoid hotspotting a monotonic key.
  • Multi-region Spanner configurations pay a write-latency floor bounded by cross-region round-trip time in exchange for regional write availability and closer reads.
  • §12.26's decision framework: default to Cloud SQL, move to AlloyDB for read/analytical scale without an external pooler, move to Spanner only for genuine horizontal write scale or a multi-region external-consistency requirement.

Security Checklist §

ControlWhy it mattersHow to verify
No Cloud SQL instance has a public IP without a documented exceptionPublic IP is the largest single exposure a managed database can havegcloud sql instances list --format="table(name,settings.ipConfiguration.ipv4Enabled)"
constraints/sql.restrictPublicIp (or the managed variant) enforced at project or orgBackstops the private-networking default against a one-off creategcloud org-policies describe constraints/sql.restrictPublicIp --project=PROJECT_ID --effective (§2.30)
IAM database authentication enabled for every PostgreSQL/MySQL instanceRemoves the shared password from the connection path (§12.7)gcloud sql instances describe INSTANCE --format="value(settings.databaseFlags)" for cloudsql.iam_authentication=on
Default administrative password rotated and stored in Secret ManagerThe bootstrap credential is the one every operator knows existsgcloud secrets versions list SECRET_ID shows recent rotation; confirm no password literal in Terraform state (§13.13)
--ssl-mode=ENCRYPTED_ONLY or stricter on every instanceCloses unencrypted direct-protocol connections that bypass the proxygcloud sql instances describe INSTANCE --format="value(settings.settingsVersion,ipConfiguration.sslMode)"
CMEK configured at creation for instances holding regulated dataDefault encryption is adequate but not independently auditable or revocablegcloud sql instances describe INSTANCE --format="value(diskEncryptionConfiguration)" (§12.14, §14.8)
cloudsql.enable_pgaudit on for PostgreSQL instances holding sensitive dataBuilt-in logging flags are coarser and can log password-bearing statementsgcloud sql instances describe INSTANCE --format="value(settings.databaseFlags)"
local_infile off on MySQL instances unless explicitly requiredPrevents a common data-exfiltration and file-read vectorSame flag inspection as above
--deletion-protection set on every production instancePrevents an accidental or malicious single-command deletiongcloud sql instances describe INSTANCE --format="value(settings.deletionProtectionEnabled)"
Automated backups and point-in-time recovery enabled with adequate retentionBounds data-loss exposure from both operator error and attackgcloud sql instances describe INSTANCE --format="value(settings.backupConfiguration)"
Database Migration Service connection profiles removed after cutoverAbandoned profiles leave a second, unmonitored copy of production datagcloud database-migration connection-profiles list --region=REGION
Cloud SQL Auth Proxy or a Language Connector used for every out-of-VPC connectionRemoves reliance on network allowlists and long-lived passwords (§12.17)Review application configuration and --connector-enforcement on the instance

Sources §