De todo lo que AlpinaShop tiene en su servidor físico, la base de datos tienda es lo más valioso y lo más frágil. Contiene los pedidos, los clientes, el stock y los precios. Corre sobre un único PostgreSQL que Marta instaló hace años, sin réplica, sin failover, con un pg_dump nocturno que nadie ha probado nunca a restaurar y con la actualización de versión mayor pendiente desde hace dos ciclos porque "no es buen momento". Si ese disco falla un sábado de octubre, AlpinaShop no vende.

Cloud SQL es el servicio de bases de datos relacionales gestionadas de Google Cloud: PostgreSQL, MySQL y SQL Server con copias automáticas, alta disponibilidad regional, réplicas de lectura, parches gestionados y recuperación a un punto en el tiempo. En esta lección migraremos la base de datos tienda a la instancia alpinashop-pedidos, conectaremos la aplicación Flask de forma segura y dejaremos a Lucía una réplica donde lanzar sus informes sin castigar a la tienda.

Contenido

  1. Qué deja de hacer Marta: base de datos gestionada frente a autogestionada
  2. Motores, ediciones y dimensionado
  3. Crear la instancia alpinashop-pedidos
  4. Bases de datos, usuarios y contraseñas
  5. Alta disponibilidad regional y failover
  6. Réplicas de lectura para los informes de Lucía
  7. Copias de seguridad, retención y recuperación a un punto en el tiempo
  8. Conectividad: IP pública, IP privada y el Auth Proxy
  9. Conectar la aplicación Flask
  10. Migrar los datos actuales
  11. Mantenimiento programado y ventanas
  12. Escalado vertical y límites
  13. Cuándo Cloud SQL se queda corto: AlloyDB y Spanner

  1. Qué deja de hacer Marta: base de datos gestionada frente a autogestionada

La forma más honesta de explicar el valor de Cloud SQL es enumerar tareas y ver quién las hace.

Tarea PostgreSQL de Marta hoy Cloud SQL
Instalar y configurar el motor Marta, a mano Google, en minutos
Aplicar parches de seguridad del motor Marta, cuando encuentra hueco Google, en la ventana de mantenimiento
Parchear el sistema operativo Marta Google (no hay SO accesible)
Copias de seguridad Script pg_dump nocturno Automáticas, incrementales, gestionadas
Verificar que las copias sirven Nadie Restauración probada con un comando
Recuperar a un instante concreto Imposible PITR con WAL, al segundo
Failover si cae el servidor Manual, horas Automático, del orden de un minuto
Réplicas de lectura Configuración manual compleja Un comando
Cifrado en reposo y en tránsito Manual Por defecto
Métricas y alertas Lo que Marta haya montado Cloud Monitoring integrado
Escalar CPU o memoria Comprar hardware Cambiar el tipo y reiniciar
Actualizar de versión mayor Proyecto de semanas Operación asistida
Ajuste fino del postgresql.conf Control total Solo flags permitidos
Extensiones y superuser Control total Lista de extensiones soportadas, sin superusuario real

Las dos últimas filas son el precio a pagar: pierdes acceso de superusuario y control total del motor. En Cloud SQL no hay SSH a la máquina, no puedes instalar cualquier extensión ni tocar todos los parámetros. Para la inmensa mayoría de aplicaciones —incluida AlpinaShop— es un intercambio excelente. Si tu producto depende de una extensión exótica o de un pg_hba.conf retorcido, tendrás que comprobar la compatibilidad antes o quedarte en Compute Engine.

Hay también un cambio de coste que conviene decir sin adornos: una VM con PostgreSQL instalado es más barata que una instancia Cloud SQL equivalente. Lo que compras con esa diferencia es el tiempo de Marta y la eliminación de un riesgo que hoy no está cubierto. Si le pones precio a una tarde de indisponibilidad en plena campaña, la cuenta sale sola.

  1. Motores, ediciones y dimensionado

Motores disponibles:

Motor Versiones habituales Notas
PostgreSQL 13 a 17 El más completo en Cloud SQL; extensiones populares soportadas (pg_stat_statements, postgis, pgvector)
MySQL 8.0, 8.4 Muy usado; réplicas y grupos de lectura maduros
SQL Server 2019, 2022 (Express a Enterprise) Licencia incluida en el precio por hora; el más caro

AlpinaShop ya usa PostgreSQL, así que la elección es evidente: PostgreSQL 16, misma familia que su instalación actual, sin cambios en la aplicación.

Ediciones. Cloud SQL se ofrece en dos ediciones que conviene distinguir:

Enterprise Enterprise Plus
Rendimiento Estándar Mayor (caché de datos, máquinas más potentes)
Failover Del orden de 1 minuto Muy inferior (segundos)
Mantenimiento Con reinicio breve Casi sin interrupción
Retención de copias Hasta 365 días Mayor, con más granularidad
Coste Menor Notablemente mayor

Para el arranque de AlpinaShop, Enterprise es suficiente. Si en el futuro la tienda no tolera ni un minuto de corte, Enterprise Plus es la vía de escape sin cambiar de servicio.

Dimensionado. Se elige un tipo de máquina igual que en Compute Engine. Criterios prácticos para una base de datos:

  • La memoria es lo primero. El objetivo es que el conjunto de datos "caliente" (índices y tablas consultadas con frecuencia) quepa en RAM. Una base de datos que hace lecturas de disco constantemente va lenta por mucha CPU que tenga.
  • El disco determina las IOPS. Como vimos en 02-01, en discos persistentes el rendimiento crece con el tamaño. Un disco de 10 GB tiene muy pocas IOPS aunque tus datos ocupen 8 GB.
  • Activa el crecimiento automático de almacenamiento. Que la base de datos se detenga por disco lleno es un incidente evitable.
  • Empieza modesto y escala. Cambiar el tipo de máquina es una operación de minutos con reinicio.

La base de datos tienda de AlpinaShop ocupa unos 12 GB. Elegimos db-custom-2-7680 (2 vCPU, 7,5 GB de RAM) con 50 GB de disco SSD y crecimiento automático: espacio de sobra para que el disco no limite las IOPS y RAM suficiente para cachear el conjunto activo.

  1. Crear la instancia 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

Este comando concentra casi todas las decisiones de la lección, así que lo desglosamos:

  • --region=europe-west1 (no zona): Cloud SQL es un servicio regional. La instancia principal vive en una zona, pero la elección se expresa a nivel de región.
  • --availability-type=REGIONAL activa la alta disponibilidad: una instancia en espera en otra zona con replicación síncrona. Es el flag que convierte un punto único de fallo en una arquitectura tolerante.
  • --storage-auto-increase: el disco crece solo cuando se llena. Nunca decrece, así que sigue vigilando su crecimiento.
  • --backup-start-time=03:00 (hora UTC): copia diaria en horario de bajo tráfico.
  • --enable-point-in-time-recovery + --retained-transaction-log-days=7: guarda los WAL para poder restaurar a cualquier instante de los últimos 7 días.
  • --maintenance-window-*: Google aplicará las actualizaciones los domingos a las 4:00 UTC, no un martes a las 11:00.
  • --maintenance-release-channel=production: recibe las versiones ya maduras, no las más recientes.
  • --database-flags: los parámetros del motor se ajustan aquí. log_min_duration_statement=1000 registra toda consulta que tarde más de un segundo, que es la mejor herramienta de diagnóstico de rendimiento que existe y cuesta cero configurarla.

La creación tarda unos minutos. Al terminar:

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

  1. Bases de datos, usuarios y contraseñas

Una instancia es el servidor; dentro viven las bases de datos y los usuarios.

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

# Fijar la contraseña del usuario administrativo 'postgres'
gcloud sql users set-password postgres \
  --instance=alpinashop-pedidos \
  --prompt-for-password

# Usuario de la aplicacion, con permisos limitados
gcloud sql users create app_catalogo \
  --instance=alpinashop-pedidos \
  --prompt-for-password

# Usuario de solo lectura para los informes de Lucia
gcloud sql users create informes_lectura \
  --instance=alpinashop-pedidos \
  --prompt-for-password

Usa siempre --prompt-for-password: escribir la contraseña en la línea de comandos la deja en el historial de bash y en los logs de auditoría.

Crear el usuario no le da permisos dentro de la base: eso se hace con SQL. Conéctate (apartado 8) y ejecuta:

-- Esquema propio de la aplicacion, en lugar de usar 'public'
CREATE SCHEMA IF NOT EXISTS tienda AUTHORIZATION app_catalogo;

-- La aplicacion: lectura y escritura de datos, nada de DDL destructivo
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;

-- Lucia: solo lectura, y solo sobre este 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 cualquiera cree objetos en el esquema public
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

La cláusula ALTER DEFAULT PRIVILEGES es la que suele faltar: sin ella, los permisos se aplican solo a las tablas que existían en ese momento, y cualquier tabla creada después queda inaccesible para la aplicación. Es una fuente clásica de errores tras un despliegue.

Cloud SQL también admite autenticación IAM: en lugar de contraseñas, los usuarios y las cuentas de servicio se autentican con su identidad de Google Cloud y un token de vida corta. Es la opción recomendada porque elimina las contraseñas del sistema. Lo dejamos apuntado aquí y se desarrolla en 03-04.

  1. Alta disponibilidad regional y failover

Con --availability-type=REGIONAL, Cloud SQL mantiene una instancia en espera en otra zona de europe-west1, con replicación síncrona del disco: cada escritura se confirma en ambas zonas antes de dar el commit por bueno.

graph LR
    APP[App Flask<br/>europe-west1] -->|escrituras y lecturas| P[Principal<br/>europe-west1-b]
    P -.->|replicacion sincrona| S[En espera<br/>europe-west1-c]
    P -->|replicacion asincrona| R[Replica de lectura<br/>europe-west1-d]
    R -->|solo lectura| L[Informes de Lucia]
    S -.->|failover automatico<br/>misma IP y nombre| P

Qué significa en la práctica:

  • La instancia en espera no sirve tráfico. No es una réplica de lectura: existe solo para tomar el relevo. Pagas por ella y no la usas, y ese es precisamente el seguro que compras.
  • El failover es automático y conserva la dirección de conexión. La aplicación no cambia su configuración; solo ve un corte de conexiones de aproximadamente un minuto (menos en Enterprise Plus).
  • La aplicación debe saber reconectar. Este es el punto que se olvida: si tu pool de conexiones no reintenta, el failover se convierte en un error visible para el cliente. Configura reintentos en el pool (lo veremos en el apartado 9).
  • Duplica aproximadamente el coste de la instancia. Es la decisión económica más clara de esta lección: para alpinashop-prod sí; para alpinashop-dev, no.

Se puede probar de verdad, y hay que probarlo:

gcloud sql instances failover alpinashop-pedidos

Ejecuta este comando en un entorno de pruebas mientras la aplicación está funcionando y observa cuánto tarda en recuperarse. Un plan de alta disponibilidad que nunca se ha ejercitado es una hipótesis, no un plan.

  1. Réplicas de lectura para los informes de Lucía

Lucía lanza consultas de agregación sobre los pedidos: ventas por categoría, evolución mensual, productos sin rotación. Ejecutadas contra la base principal, compiten por CPU y memoria con la tienda, y un informe pesado un sábado de campaña puede ralentizar el checkout.

La solución inmediata es una réplica de lectura: una copia asíncrona que acepta consultas 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ísticas que hay que tener claras:

  • La replicación es asíncrona: la réplica va ligeramente por detrás (normalmente milisegundos o pocos segundos). Para informes es irrelevante; para leer un pedido justo después de crearlo, no sirve.
  • Es de solo lectura. Cualquier escritura falla.
  • Puede tener un tipo de máquina distinto al principal: si los informes de Lucía necesitan más memoria, puedes dársela sin tocar producción.
  • Puede estar en otra región, lo que sirve además como plan de recuperación ante desastre geográfico.
  • Se puede promover a instancia independiente con gcloud sql instances promote-replica. Es una operación irreversible que rompe la replicación, útil en una recuperación real o para crear un entorno de pruebas con datos actuales.

Vigila el retardo de replicación:

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

En Cloud Monitoring, la métrica relevante es database/replication/replica_lag (06-04). Una alerta cuando supere unos minutos evita informes que parecen correctos pero están desactualizados.

Dónde está el límite. Una réplica de lectura resuelve el problema de hoy, pero las consultas analíticas sobre una base transaccional siempre serán ineficientes: PostgreSQL guarda por filas, y un informe que agrega una columna sobre millones de pedidos lee filas enteras. La solución definitiva para Lucía es exportar los datos a BigQuery, con almacenamiento columnar y motor pensado para agregar. Eso es el módulo 4. Por ahora, la réplica es la respuesta correcta y barata.

  1. Copias de seguridad, retención y recuperación a un punto en el tiempo

Ya configuramos copias diarias con 14 días de retención y PITR de 7 días al crear la instancia. Veamos qué implica cada mecanismo.

Copias automáticas. Diarias, incrementales, almacenadas fuera de la instancia y sin coste de rendimiento apreciable. No son pg_dump: son copias del almacenamiento.

# Listar copias disponibles
gcloud sql backups list --instance=alpinashop-pedidos

# Copia manual antes de una operacion arriesgada
gcloud sql backups create --instance=alpinashop-pedidos \
  --description="Antes de migrar el esquema de pedidos v3"

Ese último comando es un hábito que vale su peso en oro: antes de cualquier migración de esquema, una copia manual con descripción explícita.

Restaurar. Hay dos escenarios muy distintos:

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

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

Casi siempre quieres el segundo. Restaurar sobre la instancia original destruye el estado actual, incluidos los datos posteriores al incidente, que quizá quieras conservar. Clonar a una instancia nueva te deja comparar, extraer solo lo necesario y decidir con calma.

Recuperación a un punto en el tiempo (PITR). Es la diferencia entre "recupero la copia de anoche" y "recupero el estado exacto de las 09:14:59, un segundo antes de que el script borrara la tabla de precios". Funciona combinando la última copia con los registros de transacciones (WAL). Requiere tener PITR habilitado antes del incidente; activarlo después no permite viajar al pasado.

Un ejemplo realista de uso: Dani ejecuta a las 09:15 un UPDATE sin WHERE sobre productos. A las 09:20 se detecta. La secuencia correcta es clonar a alpinashop-pedidos-restaurada con --point-in-time a las 09:14:59, verificar allí que los precios son correctos, exportar únicamente la tabla afectada y reimportarla en producción. La tienda no se detiene y no se pierde ninguna venta de esos cinco minutos.

Exportaciones lógicas. Además de las copias gestionadas, conviene exportar periódicamente a Cloud Storage en formato SQL: sirve para migrar, para llevarse los datos fuera de Google Cloud y como copia independiente del servicio.

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

Las copias gestionadas van ligadas a la instancia: si alguien borra la instancia, se borran con ella (salvo copias finales). Una exportación en un bucket con versionado y retención es una capa de protección distinta. Esta es la razón, por cierto, por la que dedicamos la lección anterior a Cloud Storage antes que esta.

  1. Conectividad: IP pública, IP privada y el Auth Proxy

Conectarse a Cloud SQL es donde más gente se atasca, así que vamos por partes.

Método Cómo funciona Seguridad Cuándo usarlo
IP pública + redes autorizadas La instancia tiene IP pública; solo se aceptan conexiones de las IP que autorices Media: depende de una lista de IP, que en oficinas con IP dinámica es un problema Acceso puntual de administración
IP privada (VPC) La instancia recibe una IP dentro de tu red privada; no es alcanzable desde internet Alta Producción, cuando el cliente está en la misma VPC
Cloud SQL Auth Proxy Un proceso local abre un túnel cifrado y autenticado con IAM hacia la instancia Muy alta: sin exponer IP, con identidad y cifrado gestionados La recomendación general, especialmente en desarrollo
Conectores de lenguaje Biblioteca que integra el Auth Proxy dentro de la aplicación Muy alta Producción con Python, Java, Go o Node

Redes autorizadas (IP pública). Solo para acceso administrativo, y nunca 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 cuando ya no haga falta

Cloud SQL Auth Proxy. Es la pieza clave. Un binario que se ejecuta junto a tu aplicación, escucha en localhost y reenvía las conexiones a Cloud SQL por un túnel TLS, autenticándose con tus credenciales de Google Cloud. Ventajas: la instancia no necesita IP pública, no hay que gestionar certificados ni listas de IP, y el acceso se controla con el rol IAM roles/cloudsql.client.

# Descargar el proxy (version 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

# Arrancarlo: escucha en localhost:5432
./cloud-sql-proxy --port 5432 \
  alpinashop-prod:europe-west1:alpinashop-pedidos

Ese identificador proyecto:region:instancia es el nombre de conexión de la instancia, y lo necesitarás constantemente:

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

Con el proxy en marcha, cualquier cliente PostgreSQL apunta a localhost como si la base estuviera en tu máquina:

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

Y si estás en Cloud Shell, no hace falta ni descargarlo:

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

  1. Conectar la aplicación Flask

En producción no se lanza un proceso proxy aparte: se usa el conector de Python, que hace lo mismo dentro del proceso de la aplicación.

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__)

NOMBRE_CONEXION = os.environ["INSTANCIA_SQL"]   # alpinashop-prod:europe-west1:alpinashop-pedidos
USUARIO = os.environ["DB_USER"]                 # app_catalogo
PASSWORD = os.environ["DB_PASS"]                # inyectada desde Secret Manager (03-06)
BASE_DATOS = os.environ.get("DB_NAME", "tienda")

connector = Connector()


def _crear_conexion():
    """Abre una conexion nueva a traves del conector de Cloud SQL."""
    return connector.connect(
        NOMBRE_CONEXION,
        "pg8000",
        user=USUARIO,
        password=PASSWORD,
        db=BASE_DATOS,
        ip_type=IPTypes.PUBLIC,   # IPTypes.PRIVATE si la instancia solo tiene IP privada
    )


# El pool de conexiones se crea UNA vez, al arrancar la aplicacion.
engine = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=_crear_conexion,
    pool_size=5,           # conexiones permanentes por proceso
    max_overflow=2,        # conexiones extra en picos
    pool_timeout=30,       # segundos esperando una conexion libre
    pool_recycle=1800,     # recicla conexiones cada 30 min
    pool_pre_ping=True,    # comprueba la conexion antes de usarla
)


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


@app.route("/producto/<sku>")
def detalle_producto(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 encontrado"}, 404
    return dict(fila)

Aspectos del código que hay que entender bien:

  • pool_pre_ping=True es el flag que hace sobrevivir al failover. Antes de entregar una conexión del pool, comprueba que sigue viva; si el failover la cerró, la descarta y abre otra. Sin esto, tras un failover la aplicación devuelve errores hasta que se reinicia.
  • pool_recycle=1800 evita conexiones caducadas por los tiempos de inactividad de los intermediarios de red.
  • pool_size debe multiplicarse por el número de procesos. Si gunicorn arranca 4 workers con pool_size=5, son 20 conexiones por instancia. Con 10 instancias del MIG en un pico de otoño, 200 conexiones: exactamente el max_connections que configuramos. Este cálculo es la causa número uno de caídas por agotamiento de conexiones, y hay que hacerlo antes, no después.
  • Consultas parametrizadas (:sku). Nunca concatenes cadenas en SQL: es la puerta de la inyección SQL.
  • La contraseña viene de una variable de entorno, y en producción esa variable se rellena desde Secret Manager (03-06), nunca de un fichero en el repositorio.

Si el número de conexiones se convierte en un problema, el patrón habitual es poner PgBouncer delante, o usar Cloud SQL Enterprise Plus con su gestor de conexiones integrado.

  1. Migrar los datos actuales

Ha llegado el momento de mover la base tienda real. Para 12 GB, el camino más simple y controlado es pg_dump más importación desde Cloud Storage.

Paso 1: volcado en el servidor de origen.

# Formato SQL plano, sin propietarios ni privilegios (los usuarios son otros en destino)
pg_dump \
  --host=localhost \
  --username=postgres \
  --dbname=tienda \
  --no-owner \
  --no-acl \
  --format=plain \
  --file=/tmp/tienda.sql

gzip /tmp/tienda.sql

--no-owner y --no-acl evitan que el volcado intente asignar propietarios y permisos a usuarios que no existen en Cloud SQL. Es la causa más frecuente de errores durante una importación.

Paso 2: subir el volcado al bucket.

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

Paso 3: autorizar a Cloud SQL a leer el bucket. La instancia tiene su propia cuenta de servicio, y necesita permiso explícito:

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"

Este paso se olvida siempre y produce un error de permisos poco descriptivo. Recuérdalo: la instancia de Cloud SQL es una identidad más, y sin permiso no lee tu bucket.

Paso 4: importar.

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

Paso 5: verificar. Nunca des por buena una migración sin contrastar:

-- Numero de filas por tabla
SELECT relname AS tabla, n_live_tup AS filas_aprox
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;

-- Comprobaciones de negocio: totales que deben coincidir con el origen
SELECT count(*) AS pedidos, sum(total) AS importe_total FROM tienda.pedidos;
SELECT count(*) AS productos FROM tienda.productos;

-- Actualizar estadisticas del planificador tras una carga masiva
ANALYZE;

Ese ANALYZE final importa: tras una importación masiva, las estadísticas del planificador están vacías y las consultas pueden ir absurdamente lentas hasta que se recalculan.

El problema del corte. Este procedimiento implica parar las escrituras mientras se vuelca e importa: para 12 GB, quizá 30-40 minutos de tienda en modo lectura. Es asumible si se hace de madrugada.

Cuando no es asumible, la herramienta es Database Migration Service (DMS): crea una réplica continua desde tu PostgreSQL de origen hacia Cloud SQL, mantiene la sincronización mientras la tienda sigue funcionando y permite un corte final de segundos. Requiere que el origen tenga replicación lógica habilitada y conectividad con Google Cloud. Para una migración de decenas o cientos de GB con exigencia de disponibilidad, es la opción correcta; para los 12 GB de AlpinaShop en una madrugada de domingo, pg_dump es más simple y suficiente.

  1. Mantenimiento programado y ventanas

Google actualiza el motor y la infraestructura periódicamente. Esas actualizaciones implican un reinicio breve, y quieres decidir cuándo ocurre.

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

Los tres últimos flags son especialmente valiosos para AlpinaShop: definen un periodo de denegación de mantenimiento que bloquea las actualizaciones durante la campaña de otoño. Google las pospondrá hasta después del 15 de noviembre.

Canal Qué recibe Recomendación
preview Versiones nuevas antes Solo entornos de prueba
production Versiones ya estabilizadas Producción

Buena práctica: mantén alpinashop-dev en preview con la ventana un día antes que producción. Así los cambios llegan primero a desarrollo y tienes margen de reacción.

  1. Escalado vertical y límites

Cloud SQL escala verticalmente para escrituras: no hay forma de repartir las escrituras entre varias instancias.

# Cambiar el tipo de maquina (implica reinicio, unos minutos de corte)
gcloud sql instances patch alpinashop-pedidos --tier=db-custom-4-15360

# Ampliar el disco (en caliente, sin corte; nunca se puede reducir)
gcloud sql instances patch alpinashop-pedidos --storage-size=100GB

Límites y consideraciones que hay que conocer:

  • El disco crece pero no se reduce. Si activas el crecimiento automático y una carga anómala infla el disco a 2 TB, seguirás pagando 2 TB. La única salida es exportar y recrear.
  • Las conexiones son un recurso escaso. max_connections depende de la memoria de la instancia; cada conexión de PostgreSQL es un proceso con su memoria.
  • No hay superusuario. El usuario postgres de Cloud SQL tiene privilegios amplios pero no es SUPERUSER. Algunas extensiones y operaciones no están disponibles.
  • Extensiones limitadas a la lista soportada. Comprueba la tuya antes de migrar.
  • Escalar tiene techo. El mayor tipo de máquina disponible marca el límite físico de tu base de datos. Si te acercas, es señal de que necesitas otra arquitectura.

  1. Cuándo Cloud SQL se queda corto: AlloyDB y Spanner

Tres señales de que Cloud SQL se te ha quedado pequeño: las escrituras saturan la instancia más grande disponible; necesitas escritura activa en varias regiones simultáneamente; o las consultas analíticas sobre la misma base de datos son inevitables y muy pesadas.

Servicio Qué es Cuándo justifica el cambio
Cloud SQL PostgreSQL/MySQL/SQL Server gestionado, una instancia principal El caso general. Hasta unos pocos TB y una región
AlloyDB PostgreSQL compatible, rearquitecturado por Google: almacenamiento distribuido, mucho más rendimiento transaccional y un motor columnar integrado para analítica Necesitas mucho más rendimiento sin salir del ecosistema PostgreSQL, o mezclar carga transaccional y analítica
Cloud Spanner Base de datos relacional distribuida globalmente, con transacciones fuertemente consistentes y escalado horizontal de escrituras Escala global, escrituras en varias regiones, disponibilidad extrema. Coste base muy superior

Para AlpinaShop, Cloud SQL cubre con holgura el horizonte previsible: una tienda española con picos estacionales está muy lejos de esos límites. La ruta de crecimiento natural sería, primero, descargar la analítica a BigQuery (módulo 4); después, si el volumen transaccional lo exigiera, AlloyDB, que permite migrar con cambios mínimos por su compatibilidad con PostgreSQL. Spanner se estudia con detalle en la lección 02-06, dentro del panorama de bases de datos no relacionales y distribuidas.

Errores Comunes y Consejos

  • Crear la instancia sin alta disponibilidad "y ya la activaremos". Cambiar a REGIONAL después es posible, pero implica reinicio y suele posponerse indefinidamente.
  • Confiar en copias que nunca se han restaurado. Programa una restauración de prueba trimestral a una instancia clonada.
  • Activar PITR después del incidente. No sirve: hay que tenerlo antes.
  • Restaurar sobre la instancia original en un incidente. Clona a una instancia nueva y decide con calma.
  • Olvidar ALTER DEFAULT PRIVILEGES. Las tablas creadas después del GRANT quedan inaccesibles.
  • No dar permiso de lectura del bucket a la cuenta de servicio de la instancia antes de importar.
  • Importar sin --no-owner --no-acl. Falla por usuarios inexistentes en destino.
  • No hacer ANALYZE tras la importación. Consultas absurdamente lentas por estadísticas vacías.
  • Dimensionar el pool sin multiplicar por procesos e instancias. Agotamiento de conexiones en el peor momento posible.
  • Omitir pool_pre_ping. La aplicación no sobrevive a un failover.
  • Exponer la IP pública con redes autorizadas amplias. Usa el Auth Proxy o el conector.
  • Consejo: activa log_min_duration_statement desde el primer día. Es diagnóstico gratuito.
  • Consejo: haz una copia manual con descripción antes de cada migración de esquema.
  • Consejo: define un periodo de denegación de mantenimiento que cubra la campaña de otoño.
  • Consejo: exporta periódicamente a un bucket con versionado, como copia independiente del ciclo de vida de la instancia.

Ejercicios

Ejercicio 1: crear y asegurar la instancia de desarrollo

  1. Crea alpinashop-pedidos-dev con PostgreSQL 16, db-g1-small o db-custom-1-3840, en europe-west1, con disponibilidad zonal (no regional) y copias diarias con 7 días de retención. Justifica por qué en desarrollo no ponemos alta disponibilidad.
  2. Crea la base de datos tienda y los usuarios app_catalogo e informes_lectura.
  3. Conéctate con gcloud sql connect y crea el esquema tienda con una tabla productos (sku, nombre, precio, stock, activo).
  4. Concede a app_catalogo permisos de lectura/escritura y a informes_lectura solo lectura, incluyendo los privilegios por defecto.
  5. Comprueba desde informes_lectura que un INSERT falla y un SELECT funciona.

Ejercicio 2: copias, PITR y recuperación

  1. Inserta tres productos y anota la hora exacta.
  2. Simula un accidente: DELETE FROM tienda.productos; sin WHERE.
  3. Clona la instancia a un instante anterior al borrado.
  4. Verifica en el clon que los datos están y explica cómo devolverías solo esa tabla a la instancia original.
  5. Borra el clon y calcula qué te habría costado tenerlo un mes encendido.

Ejercicio 3: conexión desde Flask con tolerancia a fallos

  1. Escribe una aplicación Flask mínima que se conecte con el conector de Python y exponga /productos.
  2. Configura el pool con pool_pre_ping, pool_recycle y un pool_size justificado para 4 workers de gunicorn y hasta 6 instancias.
  3. Calcula el número total de conexiones en el peor caso y compáralo con max_connections.
  4. Explica qué ocurriría durante un failover con y sin pool_pre_ping.
  5. Indica de dónde debería venir la contraseña en producción y por qué no de una variable en el código.

Soluciones

Solución 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 desarrollo no ponemos alta disponibilidad porque duplica el coste para protegernos de un riesgo que en desarrollo no tiene consecuencias: si la instancia cae unas horas, nadie pierde una venta. La alta disponibilidad se paga donde hay ingresos en juego. Es la misma lógica de reparto de recursos que aplicamos con los labels de centro-coste en 01-04.

-- 3 y 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. Comprobacion con el usuario de solo 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', 'prueba', 1.00);                    -- ERROR: permission denied

Solución 2

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

SELECT now();   -- anotar esta marca temporal
-- 2. El accidente
DELETE FROM tienda.productos;
# 3. Clonar a un instante anterior (requiere PITR habilitado en la instancia)
gcloud sql instances clone alpinashop-pedidos-dev alpinashop-pedidos-rescate \
  --point-in-time="2026-08-05T09:14:59Z"

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

Para devolver solo esa tabla a la instancia original, se exporta del clon y se importa en producción, sin tocar el resto:

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

Esta es la ventaja de clonar en lugar de restaurar encima: se recupera exactamente la tabla dañada y se conservan todos los datos correctos escritos después del incidente.

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

Un clon es una instancia completa y se factura como tal desde el momento en que existe: una db-custom-1-3840 con 20 GB de SSD ronda las decenas de dólares al mes. Dejar clones de rescate olvidados encendidos es un gasto silencioso muy común; bórralos en cuanto termine la recuperación.

Solución 3

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

app = Flask(__name__)
connector = Connector()


def _conectar():
    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=_conectar,
    pool_size=5,
    max_overflow=2,
    pool_recycle=1800,
    pool_pre_ping=True,
)


@app.route("/productos")
def productos():
    with engine.connect() as conn:
        filas = conn.execute(
            sqlalchemy.text("SELECT sku, nombre, precio FROM tienda.productos")
        ).mappings().all()
    return jsonify([dict(f) for f in filas])
  1. El cálculo en el peor caso:
(pool_size + max_overflow) x workers x instancias
(5 + 2) x 4 x 6 = 168 conexiones

Con max_connections=200 hay margen, pero es estrecho: quedan 32 conexiones para las tareas de mantenimiento, los informes de Lucía y las sesiones administrativas. Si el MIG pudiera llegar a 10 instancias, serían 280 conexiones y la base de datos las rechazaría. Las opciones son bajar pool_size a 3, aumentar la memoria de la instancia para elevar max_connections, o introducir PgBouncer.

  1. Durante un failover, las conexiones abiertas del pool quedan rotas. Sin pool_pre_ping, SQLAlchemy entrega esas conexiones muertas y cada petición falla con un error de conexión hasta que el pool se recicla o la aplicación se reinicia: minutos de errores 500 visibles para el cliente. Con pool_pre_ping, cada conexión se verifica antes de usarse; las rotas se descartan y se abren nuevas de forma transparente, y el usuario solo percibe una latencia algo mayor durante unos segundos.

  2. En producción la contraseña debe venir de Secret Manager (lección 03-06), inyectada como variable de entorno o leída en el arranque con la cuenta de servicio de la aplicación. Nunca en el código ni en el repositorio porque quedaría en el historial de Git para siempre, sería visible para cualquiera con acceso al repositorio y no podría rotarse sin volver a desplegar. La opción óptima es directamente la autenticación IAM de Cloud SQL, que elimina la contraseña.

Conclusión

La base de datos tienda ha dejado de ser el punto débil de AlpinaShop. Has visto en una tabla concreta qué tareas deja de hacer Marta —parches, copias, failover, réplicas, cifrado— y qué se pierde a cambio: el superusuario y el control total del motor, un intercambio que para esta aplicación es claramente favorable. Has elegido PostgreSQL 16, edición Enterprise y un dimensionado razonado en el que la memoria y el tamaño del disco importan más que la CPU. Has creado alpinashop-pedidos en europe-west1 con un solo comando que concentra las decisiones importantes: alta disponibilidad regional, copias diarias con 14 días de retención, PITR de 7 días, ventana de mantenimiento en domingo de madrugada y registro de consultas lentas desde el primer minuto.

Has creado la base tienda con usuarios diferenciados y permisos mínimos, sin olvidar ALTER DEFAULT PRIVILEGES. Has entendido que la instancia en espera de la alta disponibilidad no sirve tráfico y que su valor está en el failover automático que conserva la dirección de conexión, y que ese failover solo es transparente si la aplicación reconecta —de ahí pool_pre_ping. Has creado una réplica de lectura para que los informes de Lucía no compitan con la tienda, sabiendo que es una solución correcta pero temporal, porque la analítica de verdad vive en BigQuery. Has configurado copias, has aprendido a clonar a un punto en el tiempo en lugar de restaurar destructivamente, y has visto por qué una exportación a un bucket con versionado es una capa de protección distinta de las copias gestionadas. Has comparado IP pública, IP privada, Auth Proxy y conector de Python, y has conectado el catálogo Flask con un pool bien dimensionado y consultas parametrizadas. Y has hecho la migración real con pg_dump, importación desde el bucket, verificación y ANALYZE, con Database Migration Service anotado para cuando el corte no sea aceptable.

AlpinaShop ya tiene su cómputo en máquinas virtuales, sus imágenes en un bucket y sus datos en una base gestionada. Pero Marta sigue manteniendo sistemas operativos, plantillas de instancia y startup scripts. En 02-04, App Engine, veremos qué ocurre cuando se elimina también esa capa: qué es un PaaS, en qué se diferencian el entorno estándar y el flexible, cómo se describe una aplicación entera en un app.yaml de veinte líneas, cómo se despliegan versiones y se divide el tráfico para hacer un canary, cómo se configura el escalado —incluido el escalado a cero— y cómo la misma aplicación Flask accede a Cloud SQL y a Cloud Storage sin que nadie administre un solo servidor.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados