# GCP Cloud Engineer Foundations — Part 6: Storage & Managed Databases

> **Series:** GCP Cloud Engineer Foundations (6 of 8) — aligned to the Associate Cloud Engineer (ACE) exam
> **Part 1:** `01-fundamentals-and-resource-hierarchy.md` — Fundamentals & Resource Hierarchy
> **Part 2:** `02-billing-and-gcloud-tooling.md` — Billing, gcloud CLI & Infrastructure Tooling
> **Part 3:** `03-iam-and-identity.md` — IAM & Identity
> **Part 4:** `04-compute-engine-and-autoscaling.md` — Compute Engine & Autoscaling
> **Part 5:** `05-gke-and-serverless.md` — GKE & Serverless Compute
> **Part 6:** This file — Storage & Managed Databases
> **Part 7:** `07-networking-fundamentals.md` — Networking Fundamentals
> **Part 8:** `08-monitoring-logging-and-operations.md` — Monitoring, Logging & Operations
> **Questions:** `questions.md`

> Assumes you're comfortable with Part 3's IAM model and Part 5's compute platforms — this chapter is where the compute layer's data actually lives.

## Table of Contents

1. [What This Chapter Covers](#what-this-chapter-covers)
2. [Cloud Storage Fundamentals: Buckets, Objects, and Storage Classes](#cloud-storage-fundamentals-buckets-objects-and-storage-classes)
3. [Object Lifecycle Management and Autoclass](#object-lifecycle-management-and-autoclass)
4. [Choosing a Managed Database: The Decision Framework](#choosing-a-managed-database-the-decision-framework)
5. [Cloud SQL — The General-Purpose Workhorse](#cloud-sql--the-general-purpose-workhorse)
6. [AlloyDB — When Cloud SQL Isn't Fast Enough](#alloydb--when-cloud-sql-isnt-fast-enough)
7. [Cloud Spanner — Global Scale and Strong Consistency](#cloud-spanner--global-scale-and-strong-consistency)
8. [Firestore — Document Data for Semi-Structured Workloads](#firestore--document-data-for-semi-structured-workloads)
9. [Bigtable — Wide-Column at Massive Scale](#bigtable--wide-column-at-massive-scale)
10. [BigQuery — The Analytics Warehouse](#bigquery--the-analytics-warehouse)
11. [Connection Pooling and Database Connection Limits](#connection-pooling-and-why-a-database-can-run-out-of-connections-before-it-runs-out-of-capacity)
12. [Read Replicas — Scaling Reads Without Scaling Writes](#read-replicas--scaling-reads-without-scaling-writes)
13. [Pub/Sub's Role in the Data Layer](#pubsubs-role-in-the-data-layer)
14. [Database Migration Service — Moving Data Onto GCP](#database-migration-service--moving-data-onto-gcp)
15. [Backup and Disaster Recovery for Managed Databases](#backup-and-disaster-recovery-for-managed-databases)
16. [Customer-Managed Encryption Keys (CMEK)](#customer-managed-encryption-keys-cmek)
17. [A Full Worked Example: Meridian's Data Layer End to End](#a-full-worked-example-meridians-data-layer-end-to-end)
18. [Real-World Scenario: The Migration From Cloud SQL to AlloyDB](#real-world-scenario-the-migration-from-cloud-sql-to-alloydb)
19. [Second Real-World Scenario: The Connection Storm During a Flash Sale](#second-real-world-scenario-the-connection-storm-during-a-flash-sale)
20. [Database and Storage Terminology Map](#database-and-storage-terminology-map)
21. [How Cloud SQL Regional Failover Actually Works](#how-cloud-sql-regional-failover-actually-works)
22. [Pre-Flight Checklist: Is This Data Layer Production-Ready?](#pre-flight-checklist-is-this-data-layer-production-ready)
23. [Common Mistakes and Interview Traps](#common-mistakes-and-interview-traps)
24. [Chapter Recap: How the Pieces Connect](#chapter-recap-how-the-pieces-connect)
25. [Worked Cost and Complexity Comparison](#worked-cost-and-complexity-comparison)
26. [Third Real-World Scenario: The BigQuery Bill That Tripled Overnight](#third-real-world-scenario-the-bigquery-bill-that-tripled-overnight)
27. [Worked Practice Problems](#worked-practice-problems)
28. [Summary and What's Next](#summary-and-whats-next)

## What This Chapter Covers

**GCP offers more distinct managed database products than any other major cloud, and the ACE exam — along with real architecture work — tests your ability to pick correctly among them, not just operate whichever one you already know.** This chapter covers Cloud Storage (object storage) and the full managed database lineup: Cloud SQL, AlloyDB, Cloud Spanner, Firestore, Bigtable, and BigQuery, plus how Pub/Sub (introduced across earlier chapters as Meridian's messaging backbone) fits into the same data-layer picture.

🎯 By the end of this chapter, you'll be able to match a workload's actual data shape and access pattern to the correct GCP storage or database product, configure backups and encryption correctly, and explain precisely why a "the newest, most powerful option" instinct is usually the wrong way to choose among them.

Meridian's data layer, built up incrementally across this course's earlier chapters, becomes this chapter's throughline: `meridian-shipment-assets` (Cloud Storage, for shipment photos and documents), a Cloud SQL Postgres instance (the orders/shipments relational data `shipment-api` reads and writes), and a BigQuery warehouse (route-efficiency analytics `route-optimizer` and Ana's team query). This chapter also introduces where AlloyDB, Spanner, Firestore, and Bigtable would fit if Meridian's needs grew into their specific strengths — real decision points, not products the company happens to use today.

## Cloud Storage Fundamentals: Buckets, Objects, and Storage Classes

**A Cloud Storage bucket holds objects (files, blobs of any type), and every object lives in exactly one of five storage classes, each trading access latency and retrieval cost against storage price.**

| Storage class | Minimum storage duration | Retrieval cost | Fits |
|---|---|---|---|
| **Standard** | None | None | Frequently accessed data — active application assets, hot data |
| **Nearline** | 30 days | Low | Accessed roughly once a month or less — backups, infrequent reports |
| **Coldline** | 90 days | Moderate | Accessed a few times a year — disaster-recovery data, older archives |
| **Archive** | 365 days | Highest | Accessed rarely, if ever — long-term compliance retention |
| **Autoclass** | N/A — not a class itself | Varies | A bucket-level *feature* that automatically migrates objects between the four classes above based on real access patterns |

```bash
# Create a bucket for Meridian's shipment document uploads —
# Standard class, since these are actively accessed by the API
# for weeks after a shipment ships
gcloud storage buckets create gs://meridian-shipment-assets \
  --location=us-central1 \
  --default-storage-class=STANDARD \
  --uniform-bucket-level-access

# A separate bucket for compliance archival — old shipment
# records Meridian must retain but almost never reads
gcloud storage buckets create gs://meridian-compliance-archive \
  --location=us-central1 \
  --default-storage-class=ARCHIVE \
  --uniform-bucket-level-access
```

**`--uniform-bucket-level-access`** disables the older, more granular per-object ACL system in favor of IAM-only access control — the Cloud Storage equivalent of Part 3's whole IAM philosophy, and the current recommended default for every new bucket rather than an optional hardening step.

> [!TIP]
> **Best practice: enable Autoclass on any bucket whose access pattern isn't already well understood, rather than guessing a fixed storage class and manually building lifecycle rules around it.** Autoclass automatically moves objects between classes within about 24 hours of detecting an access-pattern change and charges no early-deletion fee for its own automatic transitions — genuinely lower operational burden than hand-tuned lifecycle rules for a bucket whose access pattern isn't already precisely known in advance.

## Object Lifecycle Management and Autoclass

**A lifecycle rule automatically transitions or deletes objects based on age or other conditions — the mechanism that would otherwise require someone manually auditing bucket contents on a recurring basis.**

```json
{
  "rule": [
    {
      "action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
      "condition": {"age": 30}
    },
    {
      "action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
      "condition": {"age": 90}
    },
    {
      "action": {"type": "Delete"},
      "condition": {"age": 2555}
    }
  ]
}
```

```bash
gcloud storage buckets update gs://meridian-shipment-assets \
  --lifecycle-file=lifecycle-policy.json
```

> [!IMPORTANT]
> **Manual lifecycle rules and Autoclass are mutually exclusive on the same bucket** — you cannot set a `SetStorageClass` lifecycle action on a bucket with Autoclass enabled. Choose manual rules specifically when a compliance requirement dictates exact minimum-duration timing (a regulation requiring records stay in a specific retention tier for a precise period) that Autoclass's automatic, access-pattern-driven behavior doesn't guarantee — Meridian's compliance archive uses manual rules for exactly this reason, while `meridian-shipment-assets`'s more organically-varying access pattern uses Autoclass.

## Choosing a Managed Database: The Decision Framework

**GCP's managed database lineup covers genuinely different data shapes and scale profiles, and defaulting to whichever one sounds most impressive is the single most common mistake this chapter exists to prevent.**

```mermaid
flowchart TD
    Start(["What does the workload need?"]) --> Q1{"Relational data,<br/>fits in one region?"}
    Q1 -->|Yes, standard needs| SQL["Cloud SQL"]
    Q1 -->|Yes, but needs more<br/>raw performance| AlloyDB["AlloyDB"]
    Q1 -->|No — needs global<br/>distribution + strong consistency| Spanner["Cloud Spanner"]
    Start --> Q2{"Semi-structured/document<br/>data, app-facing?"}
    Q2 -->|Yes| Firestore["Firestore"]
    Start --> Q3{"Massive-scale wide-column,<br/>low-latency lookups or<br/>time-series analytics?"}
    Q3 -->|Yes| Bigtable["Bigtable"]
    Start --> Q4{"Large-scale analytical<br/>queries over historical data?"}
    Q4 -->|Yes| BigQuery["BigQuery"]

    classDef default_choice fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    classDef specialized fill:#f0e9fb,stroke:#6d43c0,color:#10161c
    class SQL default_choice
    class AlloyDB,Spanner,Firestore,Bigtable,BigQuery specialized
```

*Cloud SQL is the safe, well-lit default path — every other branch requires a specific, named reason Cloud SQL doesn't satisfy, not general enthusiasm for a more advanced-sounding product.*

> [!WARNING]
> **Do not choose Cloud Spanner or AlloyDB because they sound more impressive or more "cloud-native" than Cloud SQL.** Spanner's global distribution and horizontal scale, and AlloyDB's performance advantage over standard PostgreSQL, both come with real added cost and complexity — Spanner in particular uses a non-standard SQL dialect by default (though it does offer a PostgreSQL-interface option) and a genuinely different consistency model than most engineers have direct experience with. Reach for either only once a specific, measured requirement Cloud SQL can't meet is identified.

## Cloud SQL — The General-Purpose Workhorse

**Cloud SQL is GCP's managed relational database for MySQL, PostgreSQL, or SQL Server, and the correct default choice for the large majority of relational workloads** — supporting up to 128 vCPUs and 864 GB RAM per instance, comfortably enough for most applications short of genuinely extreme scale.

```bash
# Create a Cloud SQL Postgres instance — Meridian's actual
# orders/shipments database
gcloud sql instances create meridian-orders-db \
  --database-version=POSTGRES_16 \
  --tier=db-custom-4-16384 \
  --region=us-central1 \
  --availability-type=REGIONAL \
  --storage-auto-increase \
  --backup-start-time=03:00

# Connect via Cloud SQL Auth Proxy — the recommended connection
# path, using IAM to authorize the connection itself rather than
# exposing the instance's IP directly
cloud-sql-proxy meridian-shipment-prod:us-central1:meridian-orders-db
```

**`--availability-type=REGIONAL`** provisions a synchronously-replicated standby in a second zone within the region, with automatic failover — the Cloud SQL equivalent of Part 4's regional-MIG blast-radius reasoning, applied to a stateful database where "just recreate it" isn't an option the way it is for a stateless VM.

| Cloud SQL Enterprise Plus feature | Why it matters |
|---|---|
| **Near-zero-downtime maintenance** | Planned maintenance windows no longer require the multi-second failover a Standard-edition instance experiences |
| **Data cache** | An additional in-memory caching layer reducing read latency for hot data, without application-level caching code |
| **Disaster recovery across regions** | A cross-region replica specifically for regional-outage recovery, distinct from the same-region HA standby above |

> [!TIP]
> **Best practice: use the Cloud SQL Auth Proxy or a private IP connection over a VPC connector (Part 5's `shipment-api` example), never a public IP with an allowlisted address range.** The Auth Proxy layers Part 3's IAM authorization onto the database connection itself, so revoking a service account's `roles/cloudsql.client` role immediately cuts off its database access — a cleaner, more consistent control point than managing network-level IP allowlists separately from IAM.

## AlloyDB — When Cloud SQL Isn't Fast Enough

**AlloyDB is a PostgreSQL-compatible database built for materially higher performance than standard Cloud SQL Postgres — roughly 4x faster for typical transactional workloads and up to 100x faster for analytical queries against the same data**, at a real cost premium (roughly 1.5-2x Cloud SQL per compute unit) that has to be justified by an actual measured bottleneck, not assumed proactively.

```bash
gcloud alloydb clusters create meridian-orders-alloydb \
  --region=us-central1 \
  --password=REDACTED

gcloud alloydb instances create meridian-orders-alloydb-primary \
  --cluster=meridian-orders-alloydb \
  --region=us-central1 \
  --instance-type=PRIMARY \
  --cpu-count=8
```

AlloyDB's PostgreSQL compatibility means the migration path from Cloud SQL Postgres is comparatively low-friction — the same SQL dialect, the same client libraries, the same application code — which is exactly why "migrate once Cloud SQL genuinely becomes the bottleneck" is a realistic, low-risk plan rather than a one-way door decided far in advance. Meridian's orders database runs on Cloud SQL today; AlloyDB is the concrete next step if the analytics side of the business ever needs live, low-latency analytical queries against the same transactional data Cloud SQL currently serves adequately.

## Cloud Spanner — Global Scale and Strong Consistency

**Spanner is GCP's globally-distributed, horizontally-scalable relational database, offering strong consistency across regions — a genuinely rare combination, since most distributed databases trade consistency for that scale.**

```bash
gcloud spanner instances create meridian-global-ledger \
  --config=nam-eur-asia1 \
  --description="Global ledger" \
  --nodes=3
```

Spanner earns its complexity specifically for workloads that need **both** global distribution **and** strong consistency simultaneously — a financial ledger that must never show inconsistent balances across regions, or a global inventory system where two regions must never both believe they hold the last unit of the same physical item. Meridian's current single-region operation has no such requirement; Spanner would be a solution in search of a problem the company doesn't have yet, and adopting it now would repeat exactly the "impressive-sounding, not actually needed" mistake this chapter's decision framework warns against.

## Firestore — Document Data for Semi-Structured Workloads

**Firestore stores semi-structured, document-shaped data with native support for transactions and real-time client synchronization — a strong fit for application data that doesn't map cleanly onto rigid relational tables, or for a mobile/web client that needs live updates without polling.**

```bash
gcloud firestore databases create --location=nam5 --type=firestore-native
```

```python
# A representative document write — semi-structured, nested data
# that would require multiple relational tables to represent cleanly
doc_ref = firestore_client.collection("driver_profiles").document("driver-482")
doc_ref.set({
    "name": "Jordan Reyes",
    "vehicle": {"type": "van", "plate": "MRD-4471"},
    "certifications": ["hazmat", "refrigerated"],
    "last_active": firestore.SERVER_TIMESTAMP,
})
```

A realistic Meridian fit that doesn't exist yet: a future driver mobile-app profile store, where each driver's data (vehicle details, certifications, preferences) is naturally document-shaped and would benefit from Firestore's real-time sync pushing profile updates to the driver's app instantly, rather than the app needing to poll a relational API on a schedule.

## Bigtable — Wide-Column at Massive Scale

**Bigtable is a wide-column NoSQL database built for very high-throughput, low-latency workloads at massive scale — both real-time point lookups and large-scale time-series analytics**, the database underlying some of Google's own largest internal systems.

```bash
gcloud bigtable instances create gps-timeseries \
  --cluster=gps-timeseries-cluster \
  --cluster-zone=us-central1-a \
  --cluster-num-nodes=3
```

This is the product Meridian's own GPS-ingestion pipeline would graduate to if raw ping volume ever outgrew what Pub/Sub-plus-a-relational-sink can comfortably handle — Bigtable's row-key design (typically `<vehicle-id>#<reverse-timestamp>`) is purpose-built for exactly the "many devices, continuous time-series writes, need fast recent-data lookups" shape the GPS pipeline has, at a scale (millions of pings per minute across a much larger fleet than Meridian's current few thousand vehicles) that would justify the added operational complexity of designing row keys and column families correctly.

> [!NOTE]
> Bigtable has no SQL interface and no joins — data modeling happens entirely through row-key design and column-family structure, decided upfront and genuinely difficult to change later without a data migration. This is a real, deliberate trade-off for the throughput and latency it delivers, not an oversight — plan the schema carefully before committing real data to it.

## BigQuery — The Analytics Warehouse

**BigQuery is GCP's serverless data warehouse — no infrastructure to provision, petabyte-scale SQL queries, and the product that already anchors Meridian's route-efficiency analytics**, introduced across earlier chapters as the destination for billing export (Part 2) and Cloud Asset Inventory exports (Part 1).

```sql
-- A representative analytics query — average delivery time by
-- route, over the last quarter, exactly the kind of question
-- Ana's team asks of the warehouse regularly
SELECT
  route_id,
  AVG(TIMESTAMP_DIFF(delivered_at, dispatched_at, MINUTE)) AS avg_delivery_minutes
FROM `meridian-shipment-prod.analytics.deliveries`
WHERE dispatched_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY route_id
ORDER BY avg_delivery_minutes DESC
```

```bash
# BigQuery's cost model bills primarily by data SCANNED, not by
# infrastructure running idle — partitioning and clustering a table
# directly reduces cost by letting a query skip irrelevant data
bq query --use_legacy_sql=false \
  'CREATE TABLE analytics.deliveries_partitioned
   PARTITION BY DATE(dispatched_at)
   CLUSTER BY route_id
   AS SELECT * FROM analytics.deliveries'
```

> [!TIP]
> **Best practice: partition large, frequently-queried tables by date and cluster by the column most commonly filtered on.** An unpartitioned table forces every query to scan the entire table's history even when only asking about the last week — a real, direct cost impact given BigQuery's scan-based pricing, and the single most impactful BigQuery cost optimization available before considering anything more exotic.

## Connection Pooling and Why a Database Can Run Out of Connections Before It Runs Out of Capacity

**A managed database has a hard limit on simultaneous connections, independent of its CPU or memory capacity — a limit that a scaled-out compute fleet can exhaust long before the database itself is under real load.** This is a genuinely common surprise for engineers used to thinking about database capacity purely in terms of CPU/memory sizing.

```mermaid
flowchart TD
    Fleet["GKE / Cloud Run fleet<br/>scaling to 50 instances"] --> Direct{"Each instance opens<br/>its own DB connections"}
    Direct -->|No pooling| Exhausted["Hundreds of connections —<br/>hits Cloud SQL's connection limit"]
    Direct -->|With PgBouncer/Cloud SQL<br/>connection pooling| Pooled["A small, bounded pool of<br/>real connections shared across instances"]

    classDef bad fill:#fbe8e6,stroke:#b3261e,color:#10161c
    classDef good fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    class Exhausted bad
    class Pooled good
```

*Autoscaling a compute fleet without a connection-pooling strategy in front of the database directly multiplies the connection-exhaustion risk — the exact interaction between Part 4/5's autoscaling and this chapter's database sizing that's easy to miss until it happens.*

```bash
# Cloud SQL's own built-in connection pooling — reduces the need for
# a separately-deployed pooler like PgBouncer for many workloads
gcloud sql instances patch meridian-orders-db \
  --database-flags=cloudsql.enable_pooled_pgbouncer=on
```

> [!WARNING]
> A managed instance group or GKE Deployment autoscaling to handle a traffic spike (Part 4 and Part 5's own autoscaling policies) can, without connection pooling in front of the database, trigger a *database-side* outage precisely because the compute layer scaled successfully — every new instance opening its own direct connection pool multiplies total connections far faster than the database's own utilization actually grows. Connection pooling is the piece that makes Part 4 and Part 5's autoscaling policies safe to combine with a stateful database backend at all.

## Read Replicas — Scaling Reads Without Scaling Writes

**A read replica is an asynchronously (or, for regional HA, synchronously) replicated copy of a database that serves read-only queries, offloading read traffic from the primary instance without adding write capacity.**

```bash
# Create a read replica for Ana's analytics queries — offloading
# read-heavy analytical traffic from the primary that shipment-api
# depends on for real-time writes
gcloud sql instances create meridian-orders-db-replica \
  --master-instance-name=meridian-orders-db \
  --tier=db-custom-4-16384 \
  --region=us-central1
```

Read replicas are the mechanism behind this chapter's own earlier AlloyDB migration scenario — Ana's team was querying a Cloud SQL read replica specifically to keep analytical load away from the primary instance `shipment-api` depends on for real-time order writes, and the replica's own replication lag becoming visibly worse under heavy analytical query load was the concrete signal that triggered the AlloyDB evaluation. **A read replica solves read-scaling, not analytical-query performance** — a distinction worth holding onto, since the two problems look similar (both involve read traffic) but have different correct fixes: more/bigger replicas for pure read-scaling, a different database product entirely (AlloyDB) for genuinely faster analytical query execution against the same data shape.

## Pub/Sub's Role in the Data Layer

Pub/Sub, Meridian's messaging backbone since Part 1's architecture diagram, deliberately sits **between** compute and storage rather than being a storage product itself — it decouples the GPS-ingestion workers (Part 4) from whatever eventually persists that data, meaning the storage destination (today, a Cloud SQL sink; potentially Bigtable at greater scale, per the earlier section) can change without touching the ingestion side's own code or infrastructure. This is the concrete architectural payoff of Part 1's original decision to introduce Pub/Sub as a decoupling layer rather than having ingestion workers write directly to a database — the storage decision documented throughout this chapter can evolve independently of the ingestion pipeline that feeds it.

## Database Migration Service — Moving Data Onto GCP

**Database Migration Service (DMS) handles the mechanics of moving an existing database — on-premises, on another cloud, or between GCP database products — into a managed GCP target with minimal downtime**, using continuous replication rather than a single offline dump-and-restore cutover.

```bash
# A representative migration job: an on-premises MySQL database
# moving to Cloud SQL, with continuous replication until cutover
gcloud database-migration connection-profiles create mysql \
  meridian-legacy-onprem \
  --region=us-central1 \
  --host=203.0.113.10 --port=3306 \
  --username=migration_user

gcloud database-migration migration-jobs create meridian-legacy-migration \
  --region=us-central1 \
  --type=CONTINUOUS \
  --source=meridian-legacy-onprem \
  --destination=meridian-orders-db \
  --mysql-source-config
```

The **`CONTINUOUS`** migration type keeps the source and destination in sync via ongoing replication, so the actual cutover — pointing the application at the new destination — can happen at a chosen moment with a genuinely small window of write-downtime, rather than requiring the source database to go fully offline for the entire data-copy duration. Meridian used exactly this pattern for its original data-center-to-GCP migration described across this course's early chapters: the legacy on-premises MySQL database replicated continuously into a new Cloud SQL instance for several days while the team validated data consistency, with the actual production cutover happening in a single planned maintenance window measured in minutes rather than the hours a full offline migration would have required.

| Migration approach | Downtime | Complexity | Reach for it when... |
|---|---|---|---|
| **Offline dump/restore** | Full duration of the copy (can be hours for a large database) | Low | A small database, or a maintenance window generous enough to absorb the full copy time |
| **DMS continuous replication** | Minutes, at a chosen cutover moment | Moderate — requires configuring and monitoring ongoing replication | A production database where extended downtime isn't acceptable |
| **Application-level dual-write** | Near zero, but with real data-consistency risk during the dual-write period | High — requires application code changes | An extremely downtime-sensitive migration where even a DMS cutover window is unacceptable |

> [!NOTE]
> DMS supports migrating not just from external sources into GCP, but also between GCP database products directly (Cloud SQL to AlloyDB, for instance) — the same continuous-replication mechanism that made Meridian's original on-premises migration low-risk is available for the AlloyDB migration scenario described earlier in this chapter, rather than requiring a hand-rolled replication setup for an intra-GCP move.

## Backup and Disaster Recovery for Managed Databases

| Product | Native backup mechanism |
|---|---|
| **Cloud SQL** | Automated daily backups plus point-in-time recovery (via transaction log retention) |
| **AlloyDB** | Continuous backup with point-in-time recovery, similar mechanism to Cloud SQL |
| **Spanner** | Managed backups plus point-in-time recovery within a configurable retention window |
| **Firestore** | Scheduled exports to Cloud Storage, plus point-in-time recovery for Firestore Native mode |
| **Bigtable** | Backups per table, restorable to a new or existing table |
| **BigQuery** | Time travel (query historical table state within a retention window) plus table snapshots |

```bash
# Cloud SQL point-in-time recovery — restore to a specific moment,
# not just the nearest daily backup
gcloud sql instances clone meridian-orders-db meridian-orders-db-restored \
  --point-in-time="2026-09-10T14:30:00Z"
```

> [!WARNING]
> A database's automated backup existing is not the same guarantee as "this database is recoverable" — Meridian's platform team runs a quarterly restore drill (the same drill discipline Part 3 applied to IAM access revocation) specifically because a backup that has never actually been restored is an unverified assumption, not a tested capability.

## Customer-Managed Encryption Keys (CMEK)

**Every GCP storage and database product encrypts data at rest by default using Google-managed keys — CMEK replaces that default with keys you create and control in Cloud KMS, adding the ability to revoke access to the underlying data by revoking the key itself.**

```bash
# Create a KMS key ring and key
gcloud kms keyrings create meridian-data-keys --location=us-central1
gcloud kms keys create orders-db-key \
  --keyring=meridian-data-keys --location=us-central1 \
  --purpose=encryption

# Create a Cloud SQL instance using that CMEK key instead of the
# Google-managed default
gcloud sql instances create meridian-orders-db-cmek \
  --database-version=POSTGRES_16 \
  --tier=db-custom-4-16384 \
  --region=us-central1 \
  --disk-encryption-key=projects/meridian-shipment-prod/locations/us-central1/keyRings/meridian-data-keys/cryptoKeys/orders-db-key
```

> [!IMPORTANT]
> **Revoking a CMEK key's IAM access (or destroying the key entirely) makes the encrypted data permanently unreadable, including by Google.** This is the actual point of CMEK — real, revocable control — but it also means a CMEK key deleted by mistake, with no equivalent safeguard, causes genuine, irreversible data loss. Treat CMEK key management with at least the same rigor as the database it protects, including its own backup/recovery plan for the keys themselves.

## A Full Worked Example: Meridian's Data Layer End to End

```bash
# 1. Cloud Storage for shipment documents, Autoclass enabled
gcloud storage buckets create gs://meridian-shipment-assets \
  --location=us-central1 --default-storage-class=STANDARD \
  --uniform-bucket-level-access
gcloud storage buckets update gs://meridian-shipment-assets --autoclass

# 2. Cloud SQL for the orders/shipments relational data, regional HA
gcloud sql instances create meridian-orders-db \
  --database-version=POSTGRES_16 --tier=db-custom-4-16384 \
  --region=us-central1 --availability-type=REGIONAL \
  --backup-start-time=03:00

# 3. Scoped IAM for shipment-api's service account (Part 3)
gcloud projects add-iam-policy-binding meridian-shipment-prod \
  --member="serviceAccount:shipment-api@meridian-shipment-prod.iam.gserviceaccount.com" \
  --role="roles/cloudsql.client"

# 4. BigQuery dataset for analytics, tables partitioned and clustered
bq mk --dataset meridian-shipment-prod:analytics

# 5. CMEK on the orders database, given its regulated shipment/PII content
# (already shown above)

# 6. A quarterly restore drill scheduled on the team calendar,
# same discipline as Part 3's IAM revocation drill
```

## Real-World Scenario: The Migration From Cloud SQL to AlloyDB

Eighteen months into operation, Ana's analytics team began running increasingly complex ad-hoc queries directly against a Cloud SQL read replica of the orders database — queries that started taking tens of seconds and, during peak load, noticeably slowed down the replica's replication lag, indirectly affecting how current the read replica's data was for other consumers. This was the specific, measured signal this chapter's decision framework calls for before reaching past Cloud SQL: not a vague sense that "we might need more performance eventually," but an actual bottleneck with a real, observable symptom.

The migration to AlloyDB was low-friction specifically because of PostgreSQL compatibility: the same schema, the same application queries, the same client library — only the connection string and instance management changed. Ana's team validated the migration by running the exact slow analytical queries against a parallel AlloyDB instance loaded with a production data snapshot, confirming the claimed performance improvement held for Meridian's actual query patterns (not just the generic benchmark numbers vendors publish) before cutting over the read-replica traffic.

> [!TIP]
> **Best practice: validate a database migration's performance claim against your own actual queries and data shape, not a vendor's generic benchmark.** A 100x improvement claim for analytical queries is a real, documented AlloyDB characteristic — but confirming it holds for *this specific workload's* query patterns before committing to a migration is what turns a marketing number into an actual basis for a production decision.

## Second Real-World Scenario: The Connection Storm During a Flash Sale

A retail partner integration briefly drove Meridian's shipment-tracking traffic to roughly 15x normal volume during a promotional event neither team had fully coordinated on in advance. `shipment-api`'s Cloud Run service, correctly configured with generous `--max-instances`, scaled out rapidly to absorb the request volume — and within minutes, Cloud SQL began rejecting new connections, well before its own CPU or memory utilization looked concerning on any dashboard.

The root cause was exactly the connection-exhaustion mechanism described earlier in this chapter: each new Cloud Run instance opened its own direct connection pool to Cloud SQL, and the instance count scaling to absorb traffic multiplied total database connections past Cloud SQL's configured limit long before the database's actual query load became the bottleneck. The immediate fix during the incident was capping `--max-instances` temporarily to stay under the connection ceiling — a blunt, traffic-limiting workaround, not a real solution. The durable fix, applied afterward, was enabling Cloud SQL's built-in pooled PgBouncer support shown earlier in this chapter, which let the same instance count scale without each one consuming a full direct connection slot.

> [!TIP]
> **Best practice: load-test the full stack — compute autoscaling AND the database connection ceiling together — before assuming a compute-layer autoscaling policy alone guarantees a workload can handle a traffic spike.** Part 4 and Part 5 both validated autoscaling in isolation; this incident is the concrete argument for testing the *combination* of compute scale-out and database connection limits specifically, since the compute layer succeeding at its own job (scaling out) was exactly what caused the database-layer failure.

## Database and Storage Terminology Map

| Concept | GCP | AWS | Azure |
|---|---|---|---|
| Object storage | Cloud Storage | S3 | Blob Storage |
| Managed general-purpose relational DB | Cloud SQL | RDS | Azure SQL Database |
| High-performance Postgres-compatible | AlloyDB | Aurora (PostgreSQL-compatible) | — (closest: Azure Database for PostgreSQL Hyperscale) |
| Globally distributed, strongly consistent | Cloud Spanner | — (closest: DynamoDB global tables, eventually consistent) | Cosmos DB (configurable consistency) |
| Document/NoSQL for app data | Firestore | DynamoDB | Cosmos DB |
| Wide-column at massive scale | Bigtable | DynamoDB / Keyspaces (Cassandra-compatible) | Cosmos DB (Cassandra API) |
| Serverless analytics warehouse | BigQuery | Redshift (not serverless by default) / Athena | Synapse Analytics |

**Where this mapping is weakest**: Spanner's combination of global distribution *and* strong consistency has no truly direct equivalent on AWS (DynamoDB global tables trade away strong cross-region consistency for availability) — this is a genuine GCP differentiator, not just a naming difference, worth remembering specifically because it's one of the few places the "just find the equivalent service" cross-cloud habit this course has built breaks down completely rather than just imperfectly.

## How Cloud SQL Regional Failover Actually Works

The `--availability-type=REGIONAL` flag from earlier in this chapter does real, specific work worth seeing as a sequence rather than taking on faith:

```mermaid
sequenceDiagram
    participant App as "shipment-api"
    participant Primary as "Primary zone instance"
    participant Standby as "Standby zone instance"
    participant DNS as "Cloud SQL connection name"

    App->>DNS: Connect via instance connection name
    DNS->>Primary: Route to current primary
    Note over Primary,Standby: Synchronous replication —<br/>every write confirmed on both before ack
    Primary--xPrimary: Zone failure
    DNS->>DNS: Detect primary unreachable
    DNS->>Standby: Promote standby to primary
    App->>DNS: Reconnect (brief interruption)
    DNS->>Standby: Route to new primary
```

*The application never targets a specific zone directly — it connects via the instance's stable connection name, which is what makes the failover transparent to `shipment-api`'s own code, at the cost of a brief connection interruption during the actual promotion.*

The "brief interruption" in that diagram is a real, measurable window — typically tens of seconds for a Standard-edition Cloud SQL instance, meaningfully shorter with Enterprise Plus's near-zero-downtime maintenance feature mentioned earlier in this chapter. `shipment-api`'s own database client library retries transient connection failures automatically, which is what turns that window into a brief latency blip for in-flight requests rather than a hard error surfaced to a customer tracking a shipment at exactly the wrong moment.

### From the Trenches: The Firestore Query That Needed a Composite Index

An engineer prototyping the future driver-profile Firestore use case wrote a query filtering on both `vehicle.type` and one of the `certifications` array values simultaneously — and it failed immediately with an error requiring a composite index, not a runtime performance warning. Firestore, unlike a relational database, requires indexes to be explicitly declared for any query combining multiple filter conditions beyond its small set of automatically-indexed single-field patterns; there's no equivalent of a relational database silently doing a slower full scan when an index is missing. The immediate fix was accepting the console's auto-generated index-creation link; the deeper lesson is that **Firestore's indexing model surfaces missing-index problems at query-write time, as a hard error, rather than as a later performance investigation** — a genuinely different failure mode from the relational world, worth knowing before it happens for the first time during what feels like routine prototyping.

## Pre-Flight Checklist: Is This Data Layer Production-Ready?

- [ ] Every managed database chosen against this chapter's decision framework, with a specific reason for anything beyond Cloud SQL
- [ ] Regional (multi-zone) availability configured for any production relational database
- [ ] Backups verified via an actual restore drill, not just confirmed to exist
- [ ] CMEK applied to any database holding regulated or sensitive data, with its own key-recovery plan
- [ ] BigQuery tables partitioned and clustered before they grow large enough for unpartitioned scans to become expensive
- [ ] Cloud Storage buckets use uniform bucket-level access and either Autoclass or a deliberately-designed manual lifecycle policy
- [ ] Database connections go through the Auth Proxy or a private VPC path, never a publicly-exposed IP with an IP allowlist as the only protection

## Common Mistakes and Interview Traps

| Mistake | Why it happens | The fix |
|---|---|---|
| Choosing Spanner or AlloyDB by default for a new project | They sound more advanced/impressive | Cloud SQL is the correct default — reach for either only against a specific, measured requirement |
| Running BigQuery queries against unpartitioned, unclustered large tables | Partitioning feels like an optimization to add "later" | Partition and cluster from the start for any table expected to grow large — retrofitting later still costs a full table rewrite |
| Assuming automated backups mean a database is recoverable | Backups existing feels like sufficient protection | Only a real, periodic restore drill confirms recoverability — an untested backup is an unverified assumption |
| Exposing a Cloud SQL instance via public IP with an allowlist | Feels simpler to set up than the Auth Proxy or a VPC connector | Use IAM-authorized connections (Auth Proxy) or private networking — consistent with every other access-control decision this course has made |
| Deleting a CMEK key without understanding the consequence | Key management feels like routine cleanup | A destroyed CMEK key makes its encrypted data permanently unreadable, including by Google — treat key deletion as a genuinely irreversible, high-stakes action |
| Migrating a workload to Bigtable before confirming the scale actually justifies its lack of SQL/joins | The word "massive scale" sounds appealing | Bigtable's real cost is schema design rigidity — confirm the throughput/latency need is real before accepting that trade-off |

## Chapter Recap: How the Pieces Connect

```mermaid
mindmap
  root((Storage & Databases))
    Object Storage
      Storage classes
      Lifecycle rules vs Autoclass
      Uniform bucket-level access
    Relational
      Cloud SQL default
      AlloyDB for performance
      Spanner for global scale
    Non-Relational
      Firestore for app data
      Bigtable for massive scale
    Analytics
      BigQuery
      Partitioning and clustering
    Operations
      Backup and restore drills
      CMEK key management
      Regional failover
```

## Worked Cost and Complexity Comparison

A single table pulling together the cost/complexity trade-off implicit throughout this chapter's decision framework, since the ACE exam and real design reviews both ultimately reduce to this trade-off:

| Product | Relative cost vs. Cloud SQL | Operational complexity vs. Cloud SQL | Justify by |
|---|---|---|---|
| Cloud SQL | Baseline | Baseline | Default — no special justification needed |
| AlloyDB | ~1.5-2x per compute unit | Slightly higher (managed, but a newer product with a smaller operational knowledge base on most teams) | A measured analytical or transactional performance bottleneck |
| Cloud Spanner | Materially higher at small scale | Higher — new consistency model, often a new SQL dialect | Genuine global distribution + strong consistency need |
| Bigtable | Node-based, can be cost-efficient at true scale | Higher — schema design (row keys, column families) up front, no SQL | Massive-scale throughput/latency need standard databases can't meet |
| BigQuery | Scan-based, cheap for infrequent large queries, expensive if queried carelessly | Low to operate, moderate to optimize well | Analytical queries over large historical datasets |

## Third Real-World Scenario: The BigQuery Bill That Tripled Overnight

A well-intentioned analyst built a dashboard against Meridian's unpartitioned `deliveries` table (before the partitioning fix described earlier in this chapter existed) that re-ran a full-table aggregation query every time anyone opened the dashboard, rather than caching results. Once a few team members started checking it regularly throughout the day, the query volume against a large, ungrowing-but-already-substantial table caused a visible spike in Meridian's BigQuery spend, discovered through Part 2's own scheduled billing-export cost query rather than a BigQuery-specific alert.

The fix combined two changes: partitioning and clustering the underlying table (this chapter's own recommendation, applied retroactively rather than from the start) and adding a materialized view refreshed on a schedule rather than recomputing the full aggregation on every dashboard load. **This is a direct instance of Part 2's own cost-visibility discipline catching a real problem** — the billing export query designed for general cost attribution was what actually surfaced the spike, not a purpose-built BigQuery monitoring tool, reinforcing that chapter's argument for making cost queries a scheduled habit rather than a reactive one-off.

> [!TIP]
> **Best practice: treat "does this query run on every page load" as a real design question for any dashboard built on top of BigQuery, the same way "does this query run on every request" matters for an application database.** BigQuery's serverless, no-infrastructure-to-manage nature makes it easy to forget that inefficient query patterns still carry a real, direct cost — there's no over-provisioned idle server to blame instead.

## Worked Practice Problems

**1. Meridian's analytics team wants live, sub-second dashboards querying the same transactional orders data `shipment-api` writes to continuously. Cloud SQL read replicas currently show noticeable replication lag under this query load. What's the correct next step per this chapter's decision framework, and why not jump straight to Spanner?**

The correct next step is evaluating AlloyDB, not Spanner — the actual problem (analytical query performance against transactional data, within a single region) is precisely what AlloyDB is built for, including its documented advantage for analytical queries specifically. Spanner solves a different problem (global distribution with strong cross-region consistency) that Meridian doesn't have — reaching for it here would add real complexity and cost for a global-scale capability the actual requirement never called for, exactly the "impressive-sounding but unnecessary" mistake this chapter warns against.

**2. A BigQuery table storing three years of delivery records has grown to the point where routine analytics queries are taking noticeably longer and costing more than they used to. What's the most likely first fix, and why does this differ from a Cloud SQL performance problem?**

Partitioning the table by date (and clustering by whatever column queries most commonly filter on, such as `route_id`) is the most likely first fix — BigQuery bills primarily by data scanned, so an unpartitioned table forces every query to scan the full three years even when only the last month is relevant. This differs from a typical Cloud SQL performance problem (usually addressed by indexing, query optimization, or vertical scaling) because BigQuery's cost and performance model is fundamentally scan-based rather than index-based — the fix that matters most is reducing how much data a query has to touch, not speeding up how a single row is looked up.

**3. Meridian is considering CMEK for the orders database given its shipment and customer PII content. What operational responsibility does adopting CMEK add that doesn't exist with Google-managed encryption, and why does this deserve the same rigor as the database itself?**

Adopting CMEK adds the responsibility of managing the KMS key's own lifecycle and access — because destroying or revoking access to a CMEK key makes its encrypted data permanently unreadable, including by Google, the key itself becomes as critical a piece of infrastructure as the database it protects. This deserves the same backup/recovery rigor as the database because a database with perfect backups but an accidentally-destroyed encryption key is just as unrecoverable as a database with no backups at all — the encryption key is now part of the actual recovery chain, not a separate, lower-stakes concern.

**4. Meridian's compliance team requires that certain shipment records remain in a specific storage tier for a legally-mandated minimum retention period, regardless of how frequently they happen to be accessed during that time. Why is Autoclass the wrong choice for this specific bucket, even though it's the generally recommended default?**

Autoclass makes storage-class transitions based on observed access patterns, not on a guaranteed minimum duration in a specific tier — a compliance requirement demanding records stay in a particular tier for an exact legally-mandated period needs the precise, predictable timing only a manual lifecycle rule provides. Autoclass and manual `SetStorageClass` lifecycle rules are mutually exclusive on the same bucket specifically because they represent two different guarantees: Autoclass optimizes for cost against real access behavior, while manual rules guarantee exact timing regardless of access behavior — the compliance use case needs the second guarantee, not the first.

**5. `route-optimizer`'s ML training pipeline needs to read a large historical dataset from BigQuery repeatedly during model training, and the team is debating whether to query BigQuery directly from each training run or export the data to Cloud Storage first. What consideration from this chapter should drive that decision?**

BigQuery's scan-based billing model means repeatedly querying the same large dataset directly — especially many times during iterative model training — could accumulate meaningful scan costs each time, the same "does this query run on every page load" consideration flagged in this chapter's BigQuery cost scenario, applied to a training loop instead of a dashboard. Exporting the dataset to Cloud Storage once and reading from there for repeated training runs avoids re-scanning the same BigQuery data on every iteration, trading a one-time export cost for eliminating repeated query costs — the right call specifically because the same data is read many times without changing between reads.

## Summary and What's Next

This chapter covered GCP's storage and database lineup end to end: Cloud Storage classes and lifecycle management, a concrete decision framework for choosing among Cloud SQL, AlloyDB, Spanner, Firestore, Bigtable, and BigQuery, backup/DR practices verified by real drills rather than assumed, and CMEK's real power and real risk. Meridian's data layer — Cloud Storage for assets, Cloud SQL for transactional data, BigQuery for analytics, with AlloyDB and Bigtable identified as the concrete next steps if specific measured needs materialize — demonstrates the decision framework in practice rather than abstractly.

The specific techniques worth carrying forward: default to the general-purpose, well-understood product (Cloud SQL, Standard storage class) and only reach further once a specific, measured requirement justifies it; verify backups via real restores, not just their existence; and treat encryption-key management as part of the database's own operational responsibility, not a separate checkbox.

**Part 7** moves to networking — VPC design, firewall rules, load balancing, and DNS — the layer connecting every compute and data resource this course has built so far, and the one Part 1 already flagged as GCP's most structurally different area from AWS and Azure.
