De tot el que AlpinaShop té al seu servidor físic, la base de dades tienda és el més valuós i el més fràgil. Conté les comandes, els clients, l'estoc i els preus. Corre sobre un únic PostgreSQL que la Marta va instal·lar fa anys, sense rèplica, sense failover, amb un pg_dump nocturn que ningú no ha provat mai de restaurar i amb l'actualització de versió major pendent des de fa dos cicles perquè "no és bon moment". Si aquell disc falla un dissabte d'octubre, AlpinaShop no ven.

Cloud SQL és el servei de bases de dades relacionals gestionades de Google Cloud: PostgreSQL, MySQL i SQL Server amb còpies automàtiques, alta disponibilitat regional, rèpliques de lectura, pedaços gestionats i recuperació a un punt en el temps. En aquesta lliçó migrarem la base de dades tienda a la instància alpinashop-pedidos, connectarem l'aplicació Flask de manera segura i deixarem a la Lucía una rèplica on llançar els seus informes sense castigar la botiga.

Contingut

  1. Què deixa de fer la Marta: base de dades gestionada davant d'autogestionada
  2. Motors, edicions i dimensionament
  3. Crear la instància alpinashop-pedidos
  4. Bases de dades, usuaris i contrasenyes
  5. Alta disponibilitat regional i failover
  6. Rèpliques de lectura per als informes de la Lucía
  7. Còpies de seguretat, retenció i recuperació a un punt en el temps
  8. Connectivitat: IP pública, IP privada i l'Auth Proxy
  9. Connectar l'aplicació Flask
  10. Migrar les dades actuals
  11. Manteniment programat i finestres
  12. Escalat vertical i límits
  13. Quan Cloud SQL es queda curt: AlloyDB i Spanner

  1. Què deixa de fer la Marta: base de dades gestionada davant d'autogestionada

La manera més honesta d'explicar el valor de Cloud SQL és enumerar tasques i veure qui les fa.

Tasca PostgreSQL de la Marta avui Cloud SQL
Instal·lar i configurar el motor La Marta, a mà Google, en minuts
Aplicar pedaços de seguretat del motor La Marta, quan troba un forat Google, a la finestra de manteniment
Apedaçar el sistema operatiu La Marta Google (no hi ha SO accessible)
Còpies de seguretat Script pg_dump nocturn Automàtiques, incrementals, gestionades
Verificar que les còpies serveixen Ningú Restauració provada amb una comanda
Recuperar a un instant concret Impossible PITR amb WAL, al segon
Failover si cau el servidor Manual, hores Automàtic, de l'ordre d'un minut
Rèpliques de lectura Configuració manual complexa Una comanda
Xifratge en repòs i en trànsit Manual Per defecte
Mètriques i alertes El que la Marta hagi muntat Cloud Monitoring integrat
Escalar CPU o memòria Comprar maquinari Canviar el tipus i reiniciar
Actualitzar de versió major Projecte de setmanes Operació assistida
Ajust fi del postgresql.conf Control total Només flags permesos
Extensions i superuser Control total Llista d'extensions suportades, sense superusuari real

Les dues últimes files són el preu a pagar: perds accés de superusuari i control total del motor. A Cloud SQL no hi ha SSH a la màquina, no pots instal·lar qualsevol extensió ni tocar tots els paràmetres. Per a la immensa majoria d'aplicacions —inclosa AlpinaShop— és un intercanvi excel·lent. Si el teu producte depèn d'una extensió exòtica o d'un pg_hba.conf recargolat, hauràs de comprovar la compatibilitat abans o quedar-te a Compute Engine.

Hi ha també un canvi de cost que convé dir sense adorns: una VM amb PostgreSQL instal·lat és més barata que una instància Cloud SQL equivalent. El que compres amb aquesta diferència és el temps de la Marta i l'eliminació d'un risc que avui no està cobert. Si poses preu a una tarda d'indisponibilitat en plena campanya, el compte surt sol.

  1. Motors, edicions i dimensionament

Motors disponibles:

Motor Versions habituals Notes
PostgreSQL 13 a 17 El més complet a Cloud SQL; extensions populars suportades (pg_stat_statements, postgis, pgvector)
MySQL 8.0, 8.4 Molt utilitzat; rèpliques i grups de lectura madurs
SQL Server 2019, 2022 (Express a Enterprise) Llicència inclosa al preu per hora; el més car

AlpinaShop ja utilitza PostgreSQL, així que l'elecció és evident: PostgreSQL 16, mateixa família que la seva instal·lació actual, sense canvis a l'aplicació.

Edicions. Cloud SQL s'ofereix en dues edicions que convé distingir:

Enterprise Enterprise Plus
Rendiment Estàndard Més gran (memòria cau de dades, màquines més potents)
Failover De l'ordre d'1 minut Molt inferior (segons)
Manteniment Amb reinici breu Gairebé sense interrupció
Retenció de còpies Fins a 365 dies Més gran, amb més granularitat
Cost Menor Notablement més gran

Per a l'arrencada d'AlpinaShop, Enterprise és suficient. Si en el futur la botiga no tolera ni un minut de tall, Enterprise Plus és la via d'escapament sense canviar de servei.

Dimensionament. Es tria un tipus de màquina igual que a Compute Engine. Criteris pràctics per a una base de dades:

  • La memòria és el primer. L'objectiu és que el conjunt de dades "calent" (índexs i taules consultades amb freqüència) càpiga a la RAM. Una base de dades que fa lectures de disc constantment va lenta per molta CPU que tingui.
  • El disc determina les IOPS. Com vam veure a 02-01, en discos persistents el rendiment creix amb la mida. Un disc de 10 GB té molt poques IOPS encara que les teves dades ocupin 8 GB.
  • Activa el creixement automàtic d'emmagatzematge. Que la base de dades s'aturi per disc ple és un incident evitable.
  • Comença modest i escala. Canviar el tipus de màquina és una operació de minuts amb reinici.

La base de dades tienda d'AlpinaShop ocupa uns 12 GB. Triem db-custom-2-7680 (2 vCPU, 7,5 GB de RAM) amb 50 GB de disc SSD i creixement automàtic: espai de sobres perquè el disc no limiti les IOPS i RAM suficient per cachejar el conjunt actiu.

  1. Crear la instància alpinashop-pedidos

gcloud services enable sqladmin.googleapis.com

gcloud sql instances create alpinashop-pedidos \
  --project=alpinashop-prod \
  --database-version=POSTGRES_16 \
  --edition=enterprise \
  --tier=db-custom-2-7680 \
  --region=europe-west1 \
  --storage-type=SSD \
  --storage-size=50GB \
  --storage-auto-increase \
  --availability-type=REGIONAL \
  --backup-start-time=03:00 \
  --retained-backups-count=14 \
  --enable-point-in-time-recovery \
  --retained-transaction-log-days=7 \
  --maintenance-window-day=SUN \
  --maintenance-window-hour=4 \
  --maintenance-release-channel=production \
  --database-flags=max_connections=200,log_min_duration_statement=1000 \
  --labels=entorno=prod,equipo=plataforma,centro-coste=tienda,aplicacion=catalogo

Aquesta comanda concentra gairebé totes les decisions de la lliçó, així que la desglossem:

  • --region=europe-west1 (no zona): Cloud SQL és un servei regional. La instància principal viu en una zona, però l'elecció s'expressa a escala de regió.
  • --availability-type=REGIONAL activa l'alta disponibilitat: una instància en espera en una altra zona amb replicació síncrona. És el flag que converteix un punt únic de fallada en una arquitectura tolerant.
  • --storage-auto-increase: el disc creix sol quan s'omple. Mai no decreix, així que continua vigilant-ne el creixement.
  • --backup-start-time=03:00 (hora UTC): còpia diària en horari de baix trànsit.
  • --enable-point-in-time-recovery + --retained-transaction-log-days=7: desa els WAL per poder restaurar a qualsevol instant dels últims 7 dies.
  • --maintenance-window-*: Google aplicarà les actualitzacions els diumenges a les 4:00 UTC, no un dimarts a les 11:00.
  • --maintenance-release-channel=production: rep les versions ja madures, no les més recents.
  • --database-flags: els paràmetres del motor s'ajusten aquí. log_min_duration_statement=1000 registra tota consulta que trigui més d'un segon, que és la millor eina de diagnòstic de rendiment que existeix i costa zero configurar-la.

La creació triga uns minuts. En acabar:

gcloud sql instances describe alpinashop-pedidos \
  --format="table(name, state, databaseVersion, settings.tier, settings.availabilityType, ipAddresses[].ipAddress)"

  1. Bases de dades, usuaris i contrasenyes

Una instància és el servidor; a dins hi viuen les bases de dades i els usuaris.

# Crear la base de dades 'tienda'
gcloud sql databases create tienda \
  --instance=alpinashop-pedidos \
  --charset=UTF8 \
  --collation=es_ES.UTF8

# Fixar la contrasenya de l'usuari administratiu 'postgres'
gcloud sql users set-password postgres \
  --instance=alpinashop-pedidos \
  --prompt-for-password

# Usuari de l'aplicacio, amb permisos limitats
gcloud sql users create app_catalogo \
  --instance=alpinashop-pedidos \
  --prompt-for-password

# Usuari de nomes lectura per als informes de la Lucia
gcloud sql users create informes_lectura \
  --instance=alpinashop-pedidos \
  --prompt-for-password

Utilitza sempre --prompt-for-password: escriure la contrasenya a la línia de comandes la deixa a l'historial de bash i als registres d'auditoria.

Crear l'usuari no li dóna permisos dins de la base: això es fa amb SQL. Connecta't (apartat 8) i executa:

-- Esquema propi de l'aplicacio, en lloc de fer servir 'public'
CREATE SCHEMA IF NOT EXISTS tienda AUTHORIZATION app_catalogo;

-- L'aplicacio: lectura i escriptura de dades, res de DDL destructiu
GRANT USAGE ON SCHEMA tienda TO app_catalogo;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tienda TO app_catalogo;
ALTER DEFAULT PRIVILEGES IN SCHEMA tienda
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_catalogo;

-- La Lucia: nomes lectura, i nomes sobre aquest esquema
GRANT USAGE ON SCHEMA tienda TO informes_lectura;
GRANT SELECT ON ALL TABLES IN SCHEMA tienda TO informes_lectura;
ALTER DEFAULT PRIVILEGES IN SCHEMA tienda
  GRANT SELECT ON TABLES TO informes_lectura;

-- Impedir que qualsevol creï objectes a l'esquema public
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

La clàusula ALTER DEFAULT PRIVILEGES és la que sol faltar: sense ella, els permisos s'apliquen només a les taules que existien en aquell moment, i qualsevol taula creada després queda inaccessible per a l'aplicació. És una font clàssica d'errors després d'un desplegament.

Cloud SQL també admet autenticació IAM: en lloc de contrasenyes, els usuaris i els comptes de servei s'autentiquen amb la seva identitat de Google Cloud i un token de vida curta. És l'opció recomanada perquè elimina les contrasenyes del sistema. Ho deixem apuntat aquí i es desenvolupa a 03-04.

  1. Alta disponibilitat regional i failover

Amb --availability-type=REGIONAL, Cloud SQL manté una instància en espera en una altra zona d'europe-west1, amb replicació síncrona del disc: cada escriptura es confirma a totes dues zones abans de donar el commit per bo.

graph LR
    APP[App Flask<br/>europe-west1] -->|escriptures i lectures| P[Principal<br/>europe-west1-b]
    P -.->|replicacio sincrona| S[En espera<br/>europe-west1-c]
    P -->|replicacio asincrona| R[Replica de lectura<br/>europe-west1-d]
    R -->|nomes lectura| L[Informes de la Lucia]
    S -.->|failover automatic<br/>mateixa IP i nom| P

Què significa a la pràctica:

  • La instància en espera no serveix trànsit. No és una rèplica de lectura: existeix només per prendre el relleu. Pagues per ella i no la utilitzes, i aquesta és precisament l'assegurança que compres.
  • El failover és automàtic i conserva l'adreça de connexió. L'aplicació no canvia la seva configuració; només veu un tall de connexions d'aproximadament un minut (menys a Enterprise Plus).
  • L'aplicació ha de saber reconnectar. Aquest és el punt que s'oblida: si el teu pool de connexions no reintenta, el failover es converteix en un error visible per al client. Configura reintents al pool (ho veurem a l'apartat 9).
  • Duplica aproximadament el cost de la instància. És la decisió econòmica més clara d'aquesta lliçó: per a alpinashop-prod sí; per a alpinashop-dev, no.

Es pot provar de debò, i cal provar-ho:

gcloud sql instances failover alpinashop-pedidos

Executa aquesta comanda en un entorn de proves mentre l'aplicació està funcionant i observa quant triga a recuperar-se. Un pla d'alta disponibilitat que mai no s'ha exercitat és una hipòtesi, no un pla.

  1. Rèpliques de lectura per als informes de la Lucía

La Lucía llança consultes d'agregació sobre les comandes: vendes per categoria, evolució mensual, productes sense rotació. Executades contra la base principal, competeixen per CPU i memòria amb la botiga, i un informe pesat un dissabte de campanya pot alentir el checkout.

La solució immediata és una rèplica de lectura: una còpia asíncrona que accepta consultes SELECT.

gcloud sql instances create alpinashop-pedidos-replica-informes \
  --master-instance-name=alpinashop-pedidos \
  --region=europe-west1 \
  --tier=db-custom-2-7680 \
  --labels=entorno=prod,equipo=datos,centro-coste=analitica,aplicacion=catalogo

Característiques que cal tenir clares:

  • La replicació és asíncrona: la rèplica va lleugerament endarrerida (normalment mil·lisegons o pocs segons). Per a informes és irrellevant; per llegir una comanda just després de crear-la, no serveix.
  • És de només lectura. Qualsevol escriptura falla.
  • Pot tenir un tipus de màquina diferent del principal: si els informes de la Lucía necessiten més memòria, els la pots donar sense tocar producció.
  • Pot estar en una altra regió, cosa que serveix a més com a pla de recuperació davant de desastre geogràfic.
  • Es pot promoure a instància independent amb gcloud sql instances promote-replica. És una operació irreversible que trenca la replicació, útil en una recuperació real o per crear un entorn de proves amb dades actuals.

Vigila el retard de replicació:

gcloud sql instances describe alpinashop-pedidos-replica-informes \
  --format="value(replicaConfiguration)"

A Cloud Monitoring, la mètrica rellevant és database/replication/replica_lag (06-04). Una alerta quan superi uns minuts evita informes que semblen correctes però estan desactualitzats.

On és el límit. Una rèplica de lectura resol el problema d'avui, però les consultes analítiques sobre una base transaccional sempre seran ineficients: PostgreSQL desa per files, i un informe que agrega una columna sobre milions de comandes llegeix files senceres. La solució definitiva per a la Lucía és exportar les dades a BigQuery, amb emmagatzematge columnar i motor pensat per agregar. Això és el mòdul 4. De moment, la rèplica és la resposta correcta i barata.

  1. Còpies de seguretat, retenció i recuperació a un punt en el temps

Ja vam configurar còpies diàries amb 14 dies de retenció i PITR de 7 dies en crear la instància. Vegem què implica cada mecanisme.

Còpies automàtiques. Diàries, incrementals, emmagatzemades fora de la instància i sense cost de rendiment apreciable. No són pg_dump: són còpies de l'emmagatzematge.

# Llistar copies disponibles
gcloud sql backups list --instance=alpinashop-pedidos

# Copia manual abans d'una operacio arriscada
gcloud sql backups create --instance=alpinashop-pedidos \
  --description="Abans de migrar l'esquema de comandes v3"

Aquesta última comanda és un hàbit que val el seu pes en or: abans de qualsevol migració d'esquema, una còpia manual amb descripció explícita.

Restaurar. Hi ha dos escenaris molt diferents:

# 1. Restaurar una copia SOBRE la instancia original (sobreescriu: destructiu)
gcloud sql backups restore <BACKUP_ID> \
  --restore-instance=alpinashop-pedidos

# 2. Restaurar a una instancia NOVA (el recomanable en un incident)
gcloud sql instances clone alpinashop-pedidos alpinashop-pedidos-restaurada \
  --point-in-time="2026-08-05T09:15:00Z"

Gairebé sempre vols el segon. Restaurar sobre la instància original destrueix l'estat actual, incloses les dades posteriors a l'incident, que potser vols conservar. Clonar a una instància nova et deixa comparar, extreure només el necessari i decidir amb calma.

Recuperació a un punt en el temps (PITR). És la diferència entre "recupero la còpia d'anit" i "recupero l'estat exacte de les 09:14:59, un segon abans que el script esborrés la taula de preus". Funciona combinant l'última còpia amb els registres de transaccions (WAL). Requereix tenir PITR habilitat abans de l'incident; activar-lo després no permet viatjar al passat.

Un exemple realista d'ús: el Dani executa a les 09:15 un UPDATE sense WHERE sobre productos. A les 09:20 es detecta. La seqüència correcta és clonar a alpinashop-pedidos-restaurada amb --point-in-time a les 09:14:59, verificar-hi que els preus són correctes, exportar únicament la taula afectada i reimportar-la a producció. La botiga no s'atura i no es perd cap venda d'aquells cinc minuts.

Exportacions lògiques. A més de les còpies gestionades, convé exportar periòdicament a Cloud Storage en format SQL: serveix per migrar, per endur-se les dades fora de Google Cloud i com a còpia independent del servei.

gcloud sql export sql alpinashop-pedidos \
  gs://alpinashop-backups/tienda/tienda-$(date +%Y%m%d).sql.gz \
  --database=tienda

Les còpies gestionades van lligades a la instància: si algú esborra la instància, s'esborren amb ella (excepte còpies finals). Una exportació en un bucket amb versionatge i retenció és una capa de protecció diferent. Aquesta és la raó, per cert, per la qual vam dedicar la lliçó anterior a Cloud Storage abans que aquesta.

  1. Connectivitat: IP pública, IP privada i l'Auth Proxy

Connectar-se a Cloud SQL és on més gent s'encalla, així que anem per parts.

Mètode Com funciona Seguretat Quan utilitzar-lo
IP pública + xarxes autoritzades La instància té IP pública; només s'accepten connexions de les IP que autoritzis Mitjana: depèn d'una llista d'IP, que en oficines amb IP dinàmica és un problema Accés puntual d'administració
IP privada (VPC) La instància rep una IP dins de la teva xarxa privada; no és abastable des d'internet Alta Producció, quan el client és a la mateixa VPC
Cloud SQL Auth Proxy Un procés local obre un túnel xifrat i autenticat amb IAM cap a la instància Molt alta: sense exposar IP, amb identitat i xifratge gestionats La recomanació general, especialment en desenvolupament
Connectors de llenguatge Biblioteca que integra l'Auth Proxy dins de l'aplicació Molt alta Producció amb Python, Java, Go o Node

Xarxes autoritzades (IP pública). Només per a accés administratiu, i mai 0.0.0.0/0:

gcloud sql instances patch alpinashop-pedidos \
  --authorized-networks="88.20.13.45/32" \
  --no-assign-ip   # elimina la IP publica quan ja no calgui

Cloud SQL Auth Proxy. És la peça clau. Un binari que s'executa al costat de la teva aplicació, escolta a localhost i reenvia les connexions a Cloud SQL per un túnel TLS, autenticant-se amb les teves credencials de Google Cloud. Avantatges: la instància no necessita IP pública, no cal gestionar certificats ni llistes d'IP, i l'accés es controla amb el rol IAM roles/cloudsql.client.

# Descarregar el proxy (versio 2)
curl -o cloud-sql-proxy \
  https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.11.0/cloud-sql-proxy.linux.amd64
chmod +x cloud-sql-proxy

# Arrencar-lo: escolta a localhost:5432
./cloud-sql-proxy --port 5432 \
  alpinashop-prod:europe-west1:alpinashop-pedidos

Aquest identificador projecte:regio:instancia és el nom de connexió de la instància, i el necessitaràs constantment:

gcloud sql instances describe alpinashop-pedidos \
  --format="value(connectionName)"

Amb el proxy en marxa, qualsevol client PostgreSQL apunta a localhost com si la base fos a la teva màquina:

psql "host=127.0.0.1 port=5432 user=app_catalogo dbname=tienda"

I si ets a Cloud Shell, no cal ni descarregar-lo:

gcloud sql connect alpinashop-pedidos --user=postgres --database=tienda

  1. Connectar l'aplicació Flask

En producció no es llança un procés proxy a part: s'utilitza el connector de Python, que fa el mateix dins del procés de l'aplicació.

pip install "cloud-sql-python-connector[pg8000]" sqlalchemy flask
import os
import sqlalchemy
from flask import Flask, jsonify
from google.cloud.sql.connector import Connector, IPTypes

app = Flask(__name__)

NOM_CONNEXIO = os.environ["INSTANCIA_SQL"]   # alpinashop-prod:europe-west1:alpinashop-pedidos
USUARI = os.environ["DB_USER"]               # app_catalogo
PASSWORD = os.environ["DB_PASS"]             # injectada des de Secret Manager (03-06)
BASE_DADES = os.environ.get("DB_NAME", "tienda")

connector = Connector()


def _crear_connexio():
    """Obre una connexio nova a traves del connector de Cloud SQL."""
    return connector.connect(
        NOM_CONNEXIO,
        "pg8000",
        user=USUARI,
        password=PASSWORD,
        db=BASE_DADES,
        ip_type=IPTypes.PUBLIC,   # IPTypes.PRIVATE si la instancia nomes te IP privada
    )


# El pool de connexions es crea UNA vegada, en arrencar l'aplicacio.
engine = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=_crear_connexio,
    pool_size=5,           # connexions permanents per proces
    max_overflow=2,        # connexions extra en pics
    pool_timeout=30,       # segons esperant una connexio lliure
    pool_recycle=1800,     # recicla connexions cada 30 min
    pool_pre_ping=True,    # comprova la connexio abans d'utilitzar-la
)


@app.route("/productos")
def llistar_productes():
    consulta = sqlalchemy.text(
        """
        SELECT sku, nombre, precio, stock
        FROM tienda.productos
        WHERE activo = true
        ORDER BY nombre
        LIMIT 50
        """
    )
    with engine.connect() as conn:
        files = conn.execute(consulta).mappings().all()
    return jsonify([dict(f) for f in files])


@app.route("/producto/<sku>")
def detall_producte(sku):
    consulta = sqlalchemy.text(
        "SELECT sku, nombre, precio, stock FROM tienda.productos WHERE sku = :sku"
    )
    with engine.connect() as conn:
        fila = conn.execute(consulta, {"sku": sku}).mappings().first()
    if fila is None:
        return {"error": "no trobat"}, 404
    return dict(fila)

Aspectes del codi que cal entendre bé:

  • pool_pre_ping=True és el flag que fa sobreviure al failover. Abans de lliurar una connexió del pool, comprova que continua viva; si el failover la va tancar, la descarta i n'obre una altra. Sense això, després d'un failover l'aplicació retorna errors fins que es reinicia.
  • pool_recycle=1800 evita connexions caducades pels temps d'inactivitat dels intermediaris de xarxa.
  • pool_size s'ha de multiplicar pel nombre de processos. Si gunicorn arrenca 4 workers amb pool_size=5, són 20 connexions per instància. Amb 10 instàncies del MIG en un pic de tardor, 200 connexions: exactament el max_connections que vam configurar. Aquest càlcul és la causa número u de caigudes per esgotament de connexions, i cal fer-lo abans, no després.
  • Consultes parametritzades (:sku). No concatenis mai cadenes en SQL: és la porta de la injecció SQL.
  • La contrasenya ve d'una variable d'entorn, i en producció aquesta variable s'omple des de Secret Manager (03-06), mai d'un fitxer al repositori.

Si el nombre de connexions es converteix en un problema, el patró habitual és posar PgBouncer al davant, o utilitzar Cloud SQL Enterprise Plus amb el seu gestor de connexions integrat.

  1. Migrar les dades actuals

Ha arribat el moment de moure la base tienda real. Per a 12 GB, el camí més simple i controlat és pg_dump més importació des de Cloud Storage.

Pas 1: bolcat al servidor d'origen.

# Format SQL pla, sense propietaris ni privilegis (els usuaris son uns altres al desti)
pg_dump \
  --host=localhost \
  --username=postgres \
  --dbname=tienda \
  --no-owner \
  --no-acl \
  --format=plain \
  --file=/tmp/tienda.sql

gzip /tmp/tienda.sql

--no-owner i --no-acl eviten que el bolcat intenti assignar propietaris i permisos a usuaris que no existeixen a Cloud SQL. És la causa més freqüent d'errors durant una importació.

Pas 2: pujar el bolcat al bucket.

gcloud storage cp /tmp/tienda.sql.gz gs://alpinashop-backups/migracion/

Pas 3: autoritzar Cloud SQL a llegir el bucket. La instància té el seu propi compte de servei, i necessita permís explícit:

SA=$(gcloud sql instances describe alpinashop-pedidos \
  --format="value(serviceAccountEmailAddress)")

gcloud storage buckets add-iam-policy-binding gs://alpinashop-backups \
  --member="serviceAccount:$SA" \
  --role="roles/storage.objectViewer"

Aquest pas s'oblida sempre i produeix un error de permisos poc descriptiu. Recorda-ho: la instància de Cloud SQL és una identitat més, i sense permís no llegeix el teu bucket.

Pas 4: importar.

gcloud sql import sql alpinashop-pedidos \
  gs://alpinashop-backups/migracion/tienda.sql.gz \
  --database=tienda \
  --user=postgres

Pas 5: verificar. No donis mai per bona una migració sense contrastar:

-- Nombre de files per taula
SELECT relname AS tabla, n_live_tup AS filas_aprox
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;

-- Comprovacions de negoci: totals que han de coincidir amb l'origen
SELECT count(*) AS pedidos, sum(total) AS importe_total FROM tienda.pedidos;
SELECT count(*) AS productos FROM tienda.productos;

-- Actualitzar estadistiques del planificador despres d'una carrega massiva
ANALYZE;

Aquest ANALYZE final importa: després d'una importació massiva, les estadístiques del planificador estan buides i les consultes poden anar absurdament lentes fins que es recalculen.

El problema del tall. Aquest procediment implica aturar les escriptures mentre es bolca i s'importa: per a 12 GB, potser 30-40 minuts de botiga en mode lectura. És assumible si es fa de matinada.

Quan no és assumible, l'eina és Database Migration Service (DMS): crea una rèplica contínua des del teu PostgreSQL d'origen cap a Cloud SQL, manté la sincronització mentre la botiga continua funcionant i permet un tall final de segons. Requereix que l'origen tingui replicació lògica habilitada i connectivitat amb Google Cloud. Per a una migració de desenes o centenars de GB amb exigència de disponibilitat, és l'opció correcta; per als 12 GB d'AlpinaShop en una matinada de diumenge, pg_dump és més simple i suficient.

  1. Manteniment programat i finestres

Google actualitza el motor i la infraestructura periòdicament. Aquestes actualitzacions impliquen un reinici breu, i vols decidir quan passa.

gcloud sql instances patch alpinashop-pedidos \
  --maintenance-window-day=SUN \
  --maintenance-window-hour=4 \
  --maintenance-release-channel=production \
  --deny-maintenance-period-start-date=2026-10-01 \
  --deny-maintenance-period-end-date=2026-11-15 \
  --deny-maintenance-period-time=00:00:00

Els tres últims flags són especialment valuosos per a AlpinaShop: defineixen un període de denegació de manteniment que bloqueja les actualitzacions durant la campanya de tardor. Google les posposarà fins després del 15 de novembre.

Canal Què rep Recomanació
preview Versions noves abans Només entorns de prova
production Versions ja estabilitzades Producció

Bona pràctica: mantén alpinashop-dev a preview amb la finestra un dia abans que producció. Així els canvis arriben primer a desenvolupament i tens marge de reacció.

  1. Escalat vertical i límits

Cloud SQL escala verticalment per a escriptures: no hi ha manera de repartir les escriptures entre diverses instàncies.

# Canviar el tipus de maquina (implica reinici, uns minuts de tall)
gcloud sql instances patch alpinashop-pedidos --tier=db-custom-4-15360

# Ampliar el disc (en calent, sense tall; mai no es pot reduir)
gcloud sql instances patch alpinashop-pedidos --storage-size=100GB

Límits i consideracions que cal conèixer:

  • El disc creix però no es redueix. Si actives el creixement automàtic i una càrrega anòmala infla el disc a 2 TB, continuaràs pagant 2 TB. L'única sortida és exportar i recrear.
  • Les connexions són un recurs escàs. max_connections depèn de la memòria de la instància; cada connexió de PostgreSQL és un procés amb la seva memòria.
  • No hi ha superusuari. L'usuari postgres de Cloud SQL té privilegis amplis però no és SUPERUSER. Algunes extensions i operacions no estan disponibles.
  • Extensions limitades a la llista suportada. Comprova la teva abans de migrar.
  • Escalar té sostre. El tipus de màquina més gran disponible marca el límit físic de la teva base de dades. Si t'hi acostes, és senyal que necessites una altra arquitectura.

  1. Quan Cloud SQL es queda curt: AlloyDB i Spanner

Tres senyals que Cloud SQL se t'ha quedat petit: les escriptures saturen la instància més gran disponible; necessites escriptura activa en diverses regions simultàniament; o les consultes analítiques sobre la mateixa base de dades són inevitables i molt pesades.

Servei Què és Quan justifica el canvi
Cloud SQL PostgreSQL/MySQL/SQL Server gestionat, una instància principal El cas general. Fins a uns pocs TB i una regió
AlloyDB Compatible amb PostgreSQL, rearquitecturat per Google: emmagatzematge distribuït, molt més rendiment transaccional i un motor columnar integrat per a analítica Necessites molt més rendiment sense sortir de l'ecosistema PostgreSQL, o barrejar càrrega transaccional i analítica
Cloud Spanner Base de dades relacional distribuïda globalment, amb transaccions fortament consistents i escalat horitzontal d'escriptures Escala global, escriptures en diverses regions, disponibilitat extrema. Cost base molt superior

Per a AlpinaShop, Cloud SQL cobreix amb escreix l'horitzó previsible: una botiga espanyola amb pics estacionals és molt lluny d'aquests límits. La ruta de creixement natural seria, primer, descarregar l'analítica a BigQuery (mòdul 4); després, si el volum transaccional ho exigís, AlloyDB, que permet migrar amb canvis mínims per la seva compatibilitat amb PostgreSQL. Spanner s'estudia amb detall a la lliçó 02-06, dins del panorama de bases de dades no relacionals i distribuïdes.

Errors habituals i consells

  • Crear la instància sense alta disponibilitat "i ja l'activarem". Canviar a REGIONAL després és possible, però implica reinici i se sol posposar indefinidament.
  • Confiar en còpies que mai no s'han restaurat. Programa una restauració de prova trimestral a una instància clonada.
  • Activar PITR després de l'incident. No serveix: cal tenir-lo abans.
  • Restaurar sobre la instància original en un incident. Clona a una instància nova i decideix amb calma.
  • Oblidar ALTER DEFAULT PRIVILEGES. Les taules creades després del GRANT queden inaccessibles.
  • No donar permís de lectura del bucket al compte de servei de la instància abans d'importar.
  • Importar sense --no-owner --no-acl. Falla per usuaris inexistents al destí.
  • No fer ANALYZE després de la importació. Consultes absurdament lentes per estadístiques buides.
  • Dimensionar el pool sense multiplicar per processos i instàncies. Esgotament de connexions en el pitjor moment possible.
  • Ometre pool_pre_ping. L'aplicació no sobreviu a un failover.
  • Exposar la IP pública amb xarxes autoritzades àmplies. Utilitza l'Auth Proxy o el connector.
  • Consell: activa log_min_duration_statement des del primer dia. És diagnòstic gratuït.
  • Consell: fes una còpia manual amb descripció abans de cada migració d'esquema.
  • Consell: defineix un període de denegació de manteniment que cobreixi la campanya de tardor.
  • Consell: exporta periòdicament a un bucket amb versionatge, com a còpia independent del cicle de vida de la instància.

Exercicis

Exercici 1: crear i assegurar la instància de desenvolupament

  1. Crea alpinashop-pedidos-dev amb PostgreSQL 16, db-g1-small o db-custom-1-3840, a europe-west1, amb disponibilitat zonal (no regional) i còpies diàries amb 7 dies de retenció. Justifica per què en desenvolupament no posem alta disponibilitat.
  2. Crea la base de dades tienda i els usuaris app_catalogo i informes_lectura.
  3. Connecta't amb gcloud sql connect i crea l'esquema tienda amb una taula productos (sku, nombre, precio, stock, activo).
  4. Concedeix a app_catalogo permisos de lectura/escriptura i a informes_lectura només lectura, incloent-hi els privilegis per defecte.
  5. Comprova des d'informes_lectura que un INSERT falla i un SELECT funciona.

Exercici 2: còpies, PITR i recuperació

  1. Insereix tres productes i anota l'hora exacta.
  2. Simula un accident: DELETE FROM tienda.productos; sense WHERE.
  3. Clona la instància a un instant anterior a l'esborrat.
  4. Verifica al clon que les dades hi són i explica com retornaries només aquella taula a la instància original.
  5. Esborra el clon i calcula què t'hauria costat tenir-lo un mes encès.

Exercici 3: connexió des de Flask amb tolerància a fallades

  1. Escriu una aplicació Flask mínima que es connecti amb el connector de Python i exposi /productos.
  2. Configura el pool amb pool_pre_ping, pool_recycle i un pool_size justificat per a 4 workers de gunicorn i fins a 6 instàncies.
  3. Calcula el nombre total de connexions en el pitjor cas i compara'l amb max_connections.
  4. Explica què passaria durant un failover amb i sense pool_pre_ping.
  5. Indica d'on hauria de venir la contrasenya en producció i per què no d'una variable al codi.

Solucions

Solució 1

gcloud sql instances create alpinashop-pedidos-dev \
  --database-version=POSTGRES_16 \
  --tier=db-custom-1-3840 \
  --region=europe-west1 \
  --availability-type=ZONAL \
  --storage-type=SSD --storage-size=20GB --storage-auto-increase \
  --backup-start-time=02:00 --retained-backups-count=7 \
  --labels=entorno=dev,equipo=plataforma,centro-coste=tienda,aplicacion=catalogo

gcloud sql databases create tienda --instance=alpinashop-pedidos-dev
gcloud sql users create app_catalogo --instance=alpinashop-pedidos-dev --prompt-for-password
gcloud sql users create informes_lectura --instance=alpinashop-pedidos-dev --prompt-for-password

En desenvolupament no posem alta disponibilitat perquè duplica el cost per protegir-nos d'un risc que en desenvolupament no té conseqüències: si la instància cau unes hores, ningú no perd una venda. L'alta disponibilitat es paga on hi ha ingressos en joc. És la mateixa lògica de repartiment de recursos que vam aplicar amb els labels de centro-coste a 01-04.

-- 3 i 4
CREATE SCHEMA IF NOT EXISTS tienda;

CREATE TABLE tienda.productos (
    sku     TEXT PRIMARY KEY,
    nombre  TEXT NOT NULL,
    precio  NUMERIC(10,2) NOT NULL CHECK (precio >= 0),
    stock   INTEGER NOT NULL DEFAULT 0,
    activo  BOOLEAN NOT NULL DEFAULT true
);

GRANT USAGE ON SCHEMA tienda TO app_catalogo, informes_lectura;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tienda TO app_catalogo;
GRANT SELECT ON ALL TABLES IN SCHEMA tienda TO informes_lectura;

ALTER DEFAULT PRIVILEGES IN SCHEMA tienda
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_catalogo;
ALTER DEFAULT PRIVILEGES IN SCHEMA tienda
  GRANT SELECT ON TABLES TO informes_lectura;
# 5. Comprovacio amb l'usuari de nomes lectura
gcloud sql connect alpinashop-pedidos-dev --user=informes_lectura --database=tienda
SELECT count(*) FROM tienda.productos;            -- funciona
INSERT INTO tienda.productos(sku, nombre, precio)
VALUES ('X-1', 'prova', 1.00);                     -- ERROR: permission denied

Solució 2

-- 1
INSERT INTO tienda.productos (sku, nombre, precio, stock) VALUES
  ('MOC-40',  'Motxilla Trekking 40L',      89.90, 25),
  ('BOT-GTX', 'Botes Gore-Tex Alpina',     149.00, 12),
  ('TDA-2P',  'Tenda 2 places Ultralight', 219.50,  8);

SELECT now();   -- anotar aquesta marca temporal
-- 2. L'accident
DELETE FROM tienda.productos;
# 3. Clonar a un instant anterior (requereix PITR habilitat a la instancia)
gcloud sql instances clone alpinashop-pedidos-dev alpinashop-pedidos-rescate \
  --point-in-time="2026-08-05T09:14:59Z"

# 4. Verificar al clon
gcloud sql connect alpinashop-pedidos-rescate --user=postgres --database=tienda

Per retornar només aquella taula a la instància original, s'exporta del clon i s'importa a producció, sense tocar la resta:

gcloud sql export sql alpinashop-pedidos-rescate \
  gs://alpinashop-backups/rescate/productos.sql.gz \
  --database=tienda --table=tienda.productos

gcloud sql import sql alpinashop-pedidos-dev \
  gs://alpinashop-backups/rescate/productos.sql.gz \
  --database=tienda --user=postgres

Aquest és l'avantatge de clonar en lloc de restaurar a sobre: es recupera exactament la taula danyada i es conserven totes les dades correctes escrites després de l'incident.

# 5. Neteja
gcloud sql instances delete alpinashop-pedidos-rescate --quiet

Un clon és una instància completa i es factura com a tal des del moment en què existeix: una db-custom-1-3840 amb 20 GB d'SSD volta les desenes de dòlars al mes. Deixar clons de rescat oblidats encesos és una despesa silenciosa molt comuna; esborra'ls tan bon punt acabi la recuperació.

Solució 3

import os
import sqlalchemy
from flask import Flask, jsonify
from google.cloud.sql.connector import Connector

app = Flask(__name__)
connector = Connector()


def _connectar():
    return connector.connect(
        os.environ["INSTANCIA_SQL"],
        "pg8000",
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASS"],
        db="tienda",
    )


engine = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=_connectar,
    pool_size=5,
    max_overflow=2,
    pool_recycle=1800,
    pool_pre_ping=True,
)


@app.route("/productos")
def productes():
    with engine.connect() as conn:
        files = conn.execute(
            sqlalchemy.text("SELECT sku, nombre, precio FROM tienda.productos")
        ).mappings().all()
    return jsonify([dict(f) for f in files])
  1. El càlcul en el pitjor cas:
(pool_size + max_overflow) x workers x instancies
(5 + 2) x 4 x 6 = 168 connexions

Amb max_connections=200 hi ha marge, però és estret: queden 32 connexions per a les tasques de manteniment, els informes de la Lucía i les sessions administratives. Si el MIG pogués arribar a 10 instàncies, serien 280 connexions i la base de dades les rebutjaria. Les opcions són baixar pool_size a 3, augmentar la memòria de la instància per elevar max_connections, o introduir PgBouncer.

  1. Durant un failover, les connexions obertes del pool queden trencades. Sense pool_pre_ping, SQLAlchemy lliura aquestes connexions mortes i cada petició falla amb un error de connexió fins que el pool es recicla o l'aplicació es reinicia: minuts d'errors 500 visibles per al client. Amb pool_pre_ping, cada connexió es verifica abans d'utilitzar-se; les trencades es descarten i se n'obren de noves de manera transparent, i l'usuari només percep una latència una mica més gran durant uns segons.

  2. En producció la contrasenya ha de venir de Secret Manager (lliçó 03-06), injectada com a variable d'entorn o llegida a l'arrencada amb el compte de servei de l'aplicació. Mai al codi ni al repositori perquè quedaria a l'historial de Git per sempre, seria visible per a qualsevol amb accés al repositori i no podria rotar-se sense tornar a desplegar. L'opció òptima és directament l'autenticació IAM de Cloud SQL, que elimina la contrasenya.

Conclusió

La base de dades tienda ha deixat de ser el punt feble d'AlpinaShop. Has vist en una taula concreta quines tasques deixa de fer la Marta —pedaços, còpies, failover, rèpliques, xifratge— i què es perd a canvi: el superusuari i el control total del motor, un intercanvi que per a aquesta aplicació és clarament favorable. Has triat PostgreSQL 16, edició Enterprise i un dimensionament raonat en què la memòria i la mida del disc importen més que la CPU. Has creat alpinashop-pedidos a europe-west1 amb una sola comanda que concentra les decisions importants: alta disponibilitat regional, còpies diàries amb 14 dies de retenció, PITR de 7 dies, finestra de manteniment el diumenge de matinada i registre de consultes lentes des del primer minut.

Has creat la base tienda amb usuaris diferenciats i permisos mínims, sense oblidar ALTER DEFAULT PRIVILEGES. Has entès que la instància en espera de l'alta disponibilitat no serveix trànsit i que el seu valor és en el failover automàtic que conserva l'adreça de connexió, i que aquest failover només és transparent si l'aplicació reconnecta —d'aquí pool_pre_ping. Has creat una rèplica de lectura perquè els informes de la Lucía no competeixin amb la botiga, sabent que és una solució correcta però temporal, perquè l'analítica de debò viu a BigQuery. Has configurat còpies, has après a clonar a un punt en el temps en lloc de restaurar destructivament, i has vist per què una exportació a un bucket amb versionatge és una capa de protecció diferent de les còpies gestionades. Has comparat IP pública, IP privada, Auth Proxy i connector de Python, i has connectat el catàleg Flask amb un pool ben dimensionat i consultes parametritzades. I has fet la migració real amb pg_dump, importació des del bucket, verificació i ANALYZE, amb Database Migration Service anotat per quan el tall no sigui acceptable.

AlpinaShop ja té el seu còmput en màquines virtuals, les seves imatges en un bucket i les seves dades en una base gestionada. Però la Marta continua mantenint sistemes operatius, plantilles d'instància i startup scripts. A 02-04, App Engine, veurem què passa quan s'elimina també aquesta capa: què és un PaaS, en què es diferencien l'entorn estàndard i el flexible, com es descriu una aplicació sencera en un app.yaml de vint línies, com es despleguen versions i es divideix el trànsit per fer un canary, com es configura l'escalat —inclòs l'escalat a zero— i com la mateixa aplicació Flask accedeix a Cloud SQL i a Cloud Storage sense que ningú administri ni un sol servidor.

Curs de Google Cloud Platform (GCP)

Mòdul 1: Introducció a Google Cloud Platform

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats