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
- Qué deja de hacer Marta: base de datos gestionada frente a autogestionada
- Motores, ediciones y dimensionado
- Crear la instancia
alpinashop-pedidos - Bases de datos, usuarios y contraseñas
- Alta disponibilidad regional y failover
- Réplicas de lectura para los informes de Lucía
- Copias de seguridad, retención y recuperación a un punto en el tiempo
- Conectividad: IP pública, IP privada y el Auth Proxy
- Conectar la aplicación Flask
- Migrar los datos actuales
- Mantenimiento programado y ventanas
- Escalado vertical y límites
- Cuándo Cloud SQL se queda corto: AlloyDB y Spanner
- 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.
- 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.
- Crear la instancia
alpinashop-pedidos
alpinashop-pedidosgcloud 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=catalogoEste 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=REGIONALactiva 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=1000registra 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)"
- 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-passwordUsa 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.
- 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-prodsí; paraalpinashop-dev, no.
Se puede probar de verdad, y hay que probarlo:
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.
- 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=catalogoCaracterí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.
- 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=tiendaLas 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.
- 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 faltaCloud 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-pedidosEse identificador proyecto:region:instancia es el nombre de conexión de la instancia, y lo necesitarás constantemente:
Con el proxy en marcha, cualquier cliente PostgreSQL apunta a localhost como si la base estuviera en tu máquina:
Y si estás en Cloud Shell, no hace falta ni descargarlo:
- 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.
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=Truees 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=1800evita conexiones caducadas por los tiempos de inactividad de los intermediarios de red.pool_sizedebe multiplicarse por el número de procesos. Si gunicorn arranca 4 workers conpool_size=5, son 20 conexiones por instancia. Con 10 instancias del MIG en un pico de otoño, 200 conexiones: exactamente elmax_connectionsque 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.
- 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.
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=postgresPaso 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.
- 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:00Los 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.
- 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=100GBLí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_connectionsdepende de la memoria de la instancia; cada conexión de PostgreSQL es un proceso con su memoria. - No hay superusuario. El usuario
postgresde Cloud SQL tiene privilegios amplios pero no esSUPERUSER. 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.
- 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 delGRANTquedan 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
ANALYZEtras 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_statementdesde 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
- Crea
alpinashop-pedidos-devcon PostgreSQL 16,db-g1-smallodb-custom-1-3840, eneurope-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. - Crea la base de datos
tienday los usuariosapp_catalogoeinformes_lectura. - Conéctate con
gcloud sql connecty crea el esquematiendacon una tablaproductos(sku, nombre, precio, stock, activo). - Concede a
app_catalogopermisos de lectura/escritura y ainformes_lecturasolo lectura, incluyendo los privilegios por defecto. - Comprueba desde
informes_lecturaque unINSERTfalla y unSELECTfunciona.
Ejercicio 2: copias, PITR y recuperación
- Inserta tres productos y anota la hora exacta.
- Simula un accidente:
DELETE FROM tienda.productos;sinWHERE. - Clona la instancia a un instante anterior al borrado.
- Verifica en el clon que los datos están y explica cómo devolverías solo esa tabla a la instancia original.
- Borra el clon y calcula qué te habría costado tenerlo un mes encendido.
Ejercicio 3: conexión desde Flask con tolerancia a fallos
- Escribe una aplicación Flask mínima que se conecte con el conector de Python y exponga
/productos. - Configura el pool con
pool_pre_ping,pool_recycley unpool_sizejustificado para 4 workers de gunicorn y hasta 6 instancias. - Calcula el número total de conexiones en el peor caso y compáralo con
max_connections. - Explica qué ocurriría durante un failover con y sin
pool_pre_ping. - 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-passwordEn 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=tiendaSELECT count(*) FROM tienda.productos; -- funciona
INSERT INTO tienda.productos(sku, nombre, precio)
VALUES ('X-1', 'prueba', 1.00); -- ERROR: permission deniedSolució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# 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=tiendaPara 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=postgresEsta 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.
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])- El cálculo en el peor caso:
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.
-
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. Conpool_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. -
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
- ¿Qué es Google Cloud Platform?
- Configuración de tu cuenta de GCP
- Descripción general de la consola de GCP
- Proyectos, jerarquía de recursos y facturación
- Regiones, zonas y modelo de responsabilidad compartida
- Cloud Shell y la CLI de gcloud
Módulo 2: Servicios principales de GCP
- Compute Engine: máquinas virtuales en Google Cloud
- Cloud Storage: almacenamiento de objetos
- Cloud SQL: bases de datos relacionales gestionadas
- App Engine: plataforma como servicio
- Google Kubernetes Engine (GKE)
- Bases de datos NoSQL: Firestore, Bigtable y Spanner
- Cómo elegir el servicio de cómputo adecuado
Módulo 3: Redes y seguridad
- Redes VPC
- Balanceo de carga en la nube
- Cloud CDN
- Gestión de identidad y acceso (IAM)
- Cloud Armor
- Secretos y cifrado: Secret Manager y Cloud KMS
- Cloud DNS, certificados TLS y publicación segura de servicios
Módulo 4: Datos y análisis
- BigQuery: el almacén de datos analítico
- Cloud Dataflow: procesamiento de datos por lotes y en streaming
- Cloud Dataproc: Spark y Hadoop gestionados
- Cloud Pub/Sub: mensajería asíncrona
- Cloud Data Fusion: integración de datos sin código
- Orquestación de pipelines con Cloud Composer y Workflows
- Gobierno del dato y cuadros de mando con Dataplex y Looker Studio
Módulo 5: Aprendizaje automático e IA
- Vertex AI: la plataforma de machine learning de GCP
- AutoML: modelos a medida sin escribir código
- TensorFlow en GCP: entrenamiento y servicio de modelos
- API de lenguaje natural
- API de visión
- IA generativa en Vertex AI: modelos Gemini y embeddings
- MLOps: del modelo al producto con Vertex AI Pipelines
Módulo 6: DevOps y monitoreo
- Cloud Build: integración continua en GCP
- Cloud Source Repositories y gestión del código fuente
- Cloud Functions: funciones sin servidor
- Cloud Monitoring (antes Stackdriver): métricas, paneles y alertas
- Cloud Deployment Manager e infraestructura como código nativa
- Cloud Logging y Cloud Trace: logs, trazas y diagnóstico
- Terraform en GCP: infraestructura como código en la práctica
Módulo 7: Temas avanzados de GCP
- Híbrido y multinube con Anthos
- Computación sin servidor con Cloud Run
- Redes avanzadas: VPC compartida, peering y conectividad híbrida
- Mejores prácticas de seguridad
- Gestión y optimización de costos
- Fiabilidad: SLO, alta disponibilidad y recuperación ante desastres
- Gobierno a escala: organización, políticas y auditoría
