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/googleprovider 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 instance — rc-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.
| Axis | Cloud SQL | AlloyDB | Cloud Spanner |
|---|---|---|---|
| Scale ceiling | One instance's compute/storage; read replicas extend reads only | Higher, via disaggregated storage and read pools | Near-linear horizontal scale by adding nodes/processing units |
| Consistency model | Standard relational (single primary) | Standard relational (single primary) | External consistency, TrueTime-backed |
| Write availability | Zonal, or regional with HA standby (§12.8) | Zonal, or regional with HA | Regional or true multi-region, quorum-committed |
| Regional write latency | Lowest (single zone/region) | Lowest (single zone/region) | Higher in multi-region configs (§12.25) |
| Operational burden | Lowest — closest to a conventional managed database | Low — same model, some tuning for read pools | Higher — schema must account for key distribution (§12.24) |
| IAM / audit granularity | Instance-level roles; pgAudit/audit-plugin logging (§12.2–§12.3) | Same as Cloud SQL for PostgreSQL | Instance and database-level roles; no per-engine audit plugin needed |
| Cost shape | Pay for provisioned machine + storage | Pay for provisioned machine + storage, generally higher floor | Pay 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 isPROJECT_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), andcontained 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.cryptoKeyEncrypterDecrypteron 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 theirmanagedvariants (§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 §
| Control | Why it matters | How to verify |
|---|---|---|
| No Cloud SQL instance has a public IP without a documented exception | Public IP is the largest single exposure a managed database can have | gcloud sql instances list --format="table(name,settings.ipConfiguration.ipv4Enabled)" |
constraints/sql.restrictPublicIp (or the managed variant) enforced at project or org | Backstops the private-networking default against a one-off create | gcloud org-policies describe constraints/sql.restrictPublicIp --project=PROJECT_ID --effective (§2.30) |
| IAM database authentication enabled for every PostgreSQL/MySQL instance | Removes 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 Manager | The bootstrap credential is the one every operator knows exists | gcloud 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 instance | Closes unencrypted direct-protocol connections that bypass the proxy | gcloud sql instances describe INSTANCE --format="value(settings.settingsVersion,ipConfiguration.sslMode)" |
| CMEK configured at creation for instances holding regulated data | Default encryption is adequate but not independently auditable or revocable | gcloud sql instances describe INSTANCE --format="value(diskEncryptionConfiguration)" (§12.14, §14.8) |
cloudsql.enable_pgaudit on for PostgreSQL instances holding sensitive data | Built-in logging flags are coarser and can log password-bearing statements | gcloud sql instances describe INSTANCE --format="value(settings.databaseFlags)" |
local_infile off on MySQL instances unless explicitly required | Prevents a common data-exfiltration and file-read vector | Same flag inspection as above |
--deletion-protection set on every production instance | Prevents an accidental or malicious single-command deletion | gcloud sql instances describe INSTANCE --format="value(settings.deletionProtectionEnabled)" |
| Automated backups and point-in-time recovery enabled with adequate retention | Bounds data-loss exposure from both operator error and attack | gcloud sql instances describe INSTANCE --format="value(settings.backupConfiguration)" |
| Database Migration Service connection profiles removed after cutover | Abandoned profiles leave a second, unmonitored copy of production data | gcloud database-migration connection-profiles list --region=REGION |
| Cloud SQL Auth Proxy or a Language Connector used for every out-of-VPC connection | Removes reliance on network allowlists and long-lived passwords (§12.17) | Review application configuration and --connector-enforcement on the instance |
Sources §
- Cloud SQL overview — https://cloud.google.com/sql (last validated 2026-09-03)
- Cloud SQL for PostgreSQL database flags — https://cloud.google.com/sql/docs/postgres/flags (last validated 2026-09-03)
- Cloud SQL for MySQL database flags — https://cloud.google.com/sql/docs/mysql/flags (last validated 2026-09-03)
- Cloud SQL for SQL Server database flags — https://cloud.google.com/sql/docs/sqlserver/flags (last validated 2026-09-03)
- Cloud SQL for PostgreSQL high availability — https://cloud.google.com/sql/docs/postgres/high-availability (last validated 2026-09-03)
- Cloud SQL for PostgreSQL CMEK configuration — https://cloud.google.com/sql/docs/postgres/configure-cmek (last validated 2026-09-03)
- Cloud SQL editions overview — https://cloud.google.com/sql/docs/mysql/editions-intro (last validated 2026-09-03)
- Cloud SQL Auth Proxy overview — https://cloud.google.com/sql/docs/postgres/sql-proxy (last validated 2026-09-03)
- Cloud SQL for MySQL IAM database authentication — https://cloud.google.com/sql/docs/mysql/iam-logins (last validated 2026-09-03)
- AlloyDB for PostgreSQL overview — https://cloud.google.com/alloydb (last validated 2026-09-03)
- Cloud Spanner overview — https://cloud.google.com/spanner (last validated 2026-09-03)
- Cloud SQL Python Connector — https://github.com/GoogleCloudPlatform/cloud-sql-python-connector (last validated 2026-09-03)
- Google Cloud SDK 583.0.0 command surface resolved offline (
gcloud sql,gcloud alloydb,gcloud spanner,gcloud database-migration--help), 2026-09-03 hashicorp/googleprovider resource schemas forgoogle_sql_database_instance,google_alloydb_cluster,google_spanner_instance,google_secret_manager_secret— https://registry.terraform.io/providers/hashicorp/google/latest/docs (provider 8.x) (last validated 2026-09-03)