Saltar al contenido
cloud-webv0.1.0
Documentación

Bases de datos (Postgres)

cloud-web puede correr una base PostgreSQL gestionada para tus apps: la crea, la mantiene, le hace backups y la conecta a tu app por la red interna del clúster, sin exponerla a internet.

Qué obtenés

  • PostgreSQL 14, 15, 16 o 17 (16 por defecto), con un volumen persistente: los datos sobreviven a reinicios del contenedor, del agente y del nodo.
  • Una base y un usuario para tu app, con una contraseña generada. El usuario es dueño de su base y no es superusuario: no puede crear otros roles ni bases, ni ejecutar comandos del servidor.
  • Red interna: la base solo es alcanzable desde las apps que vinculás a ella, por el nombre <nombre>.db.internal. El puerto nunca se publica en el nodo y la base no tiene salida a internet.
  • Backups diarios automáticos, a pedido, restauración y descarga.
  • Carga de un dump (por ejemplo el de Cloud SQL), de un archivo o de una URL.

Límites que conviene conocer

  • Una base vive en un solo nodo y no se mueve: su volumen está en el disco de ese nodo. Tus apps vinculadas se despliegan en ese mismo nodo. Si el nodo se cae, la base queda no disponible hasta que vuelva (no se recrea vacía en otro lado).
  • No hay alta disponibilidad ni réplicas. Es una sola instancia.
  • Los backups se guardan en el mismo nodo que la base. No sobreviven a la pérdida de ese nodo: el dashboard lo avisa siempre. Para tener una copia fuera del nodo, descargá los backups (ver más abajo). Un destino externo automático (S3) está planificado.
  • No hay acceso desde fuera del clúster (ni túnel ni puerto público): para mirar los datos usá un backup, o una app tuya que se conecte por la red interna.
  • El tamaño que elegís es un límite blando: al llenarlo, la base pasa a solo lectura (en lugar de llenar el disco del nodo) y vuelve a escribir al bajar del 95 %.
  • Cambiar de versión mayor (por ejemplo de 15 a 16) no se hace en el lugar: creá una base nueva de la versión que quieras, cargá en ella un backup o un dump y cambiá la vinculación.
  • Cuotas por cuenta (las ajusta el operador): 2 bases, 20 GiB de almacenamiento en total, 5 GiB por dump subido y 3 backups manuales a la vez.

Crear una base

En el dashboard: Bases de datos → Nueva base de datos.

Campo Qué es
Nombre Letras minúsculas, números y guiones (hasta 40). Es el nombre interno: bo → bo.db.internal
Versión 14, 15, 16 o 17. Para cargar un dump, igual o mayor que la del origen
Almacenamiento 1, 5, 10, 25 o 50 GiB
Memoria 512 MiB, 1, 2 o 4 GiB. Ajusta el servidor (shared_buffers ≈ 25 %)
Backup diario Hora (UTC) y cuántos conservar (7 por defecto, hasta 30)

La base pasa por Creando y queda En marcha en un par de minutos (la primera vez de cada versión se baja la imagen de Postgres). Si falla, queda Fallida con el motivo y se puede eliminar.

Conectar tu app

  1. Abrí la base → Apps vinculadas → Vincular y elegí la app. Tiene que ser de la misma cuenta que la base: una base solo se vincula a apps de su dueño (un administrador ve las apps de todas las cuentas, pero el dashboard solo ofrece las de la cuenta dueña de la base; si la app es de otra cuenta, creá la base con esa cuenta).
    • Es imprescindible vincular: la app solo alcanza a la base (<nombre>.db.internal) si está vinculada, porque el vínculo conecta la app a la red de la base. Definir DATABASE_URL a mano no alcanza: la app falla con UnknownHostException o could not translate host name.
  2. Redesplegá la app. En cada despliegue se inyectan, como variables de solo lectura:
Variable Valor
DATABASE_URL postgres://usuario:contraseña@<nombre>.db.internal:5432/base?sslmode=disable
PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD Lo mismo, separado (lo que leen psql y la mayoría de las librerías)

Si tu app define una variable con el mismo nombre, gana la de la base y el dashboard la muestra como "provista por la base". La contraseña nunca se muestra: llega a la app y se enmascara en los logs.

  • La app se despliega en el nodo de su base. Una app puede usar varias bases mientras estén en el mismo nodo; la primera que vinculás provee DATABASE_URL y PG*, y cada una suma <NOMBRE>_DATABASE_URL.
  • sslmode=disable es deliberado: la red es interna, privada de tu cuenta y sin salida.
  • No se puede eliminar una base con apps vinculadas: desvinculalas primero.

Backups y restauración

  • Cada día, a la hora que elegiste, se toma un backup (pg_dump en formato custom). Se conservan los últimos N; siempre se conserva el último correcto. Podés pedir uno manual cuando quieras.
  • Cada backup muestra fecha, tamaño, suma SHA-256, duración y resultado. Si falla, o pasan más de 36 horas sin uno correcto, la base muestra una alerta.
  • Descargar un backup: el plano de control lo pide al nodo y lo conserva unas horas para que lo bajes. Es la forma de tener una copia fuera del nodo.
  • Restaurar (dos formas):
    • En una base nueva (lo recomendado): la original no se toca. La base nueva queda en el mismo nodo (los backups no salen de él).
    • Sobre la misma base: reemplaza sus datos. Pide confirmar el nombre, toma antes un backup de seguridad (pre-restore) y es todo o nada: carga el backup en una base temporal y, solo si salió bien, la intercambia (corta las conexiones; tu app reconecta sola). Si falla, los datos de antes quedan como estaban.
  • Durante una restauración o una carga no se hacen backups ni se despliegan las apps vinculadas.

Monitoreo y logs

  • Monitoreo (menú Monitoreo, sección Tus bases de datos): por cada base, su estado, el disco usado frente al elegido (con aviso al 80 %, al 95 % y al llenarse), las conexiones abiertas y el resultado del último backup. Las alertas de backup (el último falló, o hace más de 36 horas que no hay uno correcto) figuran ahí y en la propia base. Un administrador ve las alertas de todas las bases en la lista de Bases de datos.
  • Logs de Postgres (en la base): lo que escribe el servidor (arranques, errores y las consultas que tardan más de 1 segundo), con filtro por texto, seguimiento en vivo y descarga. Se conservan las últimas 5.000 líneas y 14 días, como los logs de las apps. Mirá acá primero si una carga o una consulta falla.

Migrar desde Cloud SQL (paso a paso)

Esta es la receta para pasar una base de Cloud SQL (por ejemplo noti42-postgres-db) a cloud-web.

0. Antes de empezar

  • Anotá la versión de Postgres de la instancia de Cloud SQL (consola → Overview).
  • Anotá las extensiones que usa la base: SELECT extname FROM pg_extension;. Se pueden crear las "de confianza" de Postgres (pgcrypto, uuid-ossp, pg_trgm, citext, hstore, unaccent, btree_gin, btree_gist…). Las que exigen superusuario o no vienen en la imagen (por ejemplo postgis) no están disponibles: la carga las rechaza y te dice cuál es, antes de tocar nada.
  • Verificá que tenés cuota: el tamaño del dump (descomprimido, más lo que crezca) debe caber en el almacenamiento que elijas.

1. Crear la base en cloud-web con una versión igual o mayor que la de Cloud SQL (un dump de una versión mayor que la base se rechaza). Elegí almacenamiento de sobra.

2. Exportar desde Cloud SQL a un bucket, en SQL plano y comprimido:

gcloud sql export sql noti42-postgres-db gs://MI-BUCKET/noti42.sql.gz --database=NOMBRE_DE_LA_BASE

Con la extensión .gz el archivo sale comprimido. El export de Cloud SQL trae dueños y permisos de roles que no existen en cloud-web (postgres, cloudsqlsuperuser…): la carga los ignora y deja todo a nombre del usuario de tu base.

3. Llevar el dump a cloud-web. Elegí una:

  • Subir el archivo: bajalo (gsutil cp gs://MI-BUCKET/noti42.sql.gz .) y en la base → Cargar un dump → Subir archivo. La subida va por partes y se puede retomar si se corta.
  • Desde una URL: generá una URL firmada de corta vida (gsutil signurl -d 1h clave.json gs://MI-BUCKET/noti42.sql.gz) y pegala en Cargar un dump → Desde una URL. Tiene que ser https y no llevar credenciales dentro. El nodo la baja con un contenedor aislado (no alcanza la red interna ni la LAN) y con tope de tiempo y de tamaño.

4. Cargar. Modo base vacía (lo normal en una base nueva). Se detecta el formato por el contenido (SQL plano, gzip, custom o tar). Antes de cargar se comprueba la versión de origen y las extensiones; la carga corre en una sola transacción y se detiene en el primer error. Al terminar ves el informe: estado, duración, bytes leídos, versión de origen y, si falló, el primer error con su número de línea del archivo original. Si falla, la base queda vacía y podés reintentar.

5. Verificar. Compará con Cloud SQL, por ejemplo, el conteo de filas de las tablas principales:

SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 20;

(para una comparación exacta, SELECT count(*) FROM tabla en ambos lados). Probá la app contra la base nueva en un despliegue de prueba antes del corte.

6. Vincular y redesplegar el back-office. Vinculá la app a la base y redesplegala: ya recibe DATABASE_URL. Si tu app lee otra variable (por ejemplo DB_HOST), usá PGHOST o configurala para leer DATABASE_URL.

7. El corte (con una ventana corta, sin replicación):

  1. Detené las escrituras en el origen: apagá el BO (o ponelo en mantenimiento) y, si hay otros escritores, ponelos en solo lectura.
  2. Exportá de nuevo desde Cloud SQL (paso 2) para tener el estado final.
  3. Cargá ese dump en la base de cloud-web en modo Reemplazar: pide confirmar el nombre, toma un backup de seguridad primero y solo cambia los datos si la carga completa salió bien.
  4. Reabrí el BO ya vinculado a la base nueva.
  5. Vuelta atrás: dejá Cloud SQL intacto (en solo lectura) unos días. Si algo sale mal, apuntá el BO de nuevo a Cloud SQL.

Con un dump de decenas de megabytes, la ventana son minutos.

Si la carga falla

Mensaje Qué hacer
the dump comes from PostgreSQL X, newer than this database Creá la base con una versión igual o mayor que X
the dump needs the extension X, which is not available… Esa extensión no se puede crear aquí: quitala del dump o de la base de origen
…only a superuser can create… Igual: el usuario de tu base no es superusuario
the database already has data Usá el modo Reemplazar (toma un backup primero) o una base vacía
the compressed dump is damaged El archivo .gz está cortado o corrupto: volvé a exportarlo o subirlo
permission denied… / must be owner… Una sentencia del dump exige privilegios que el usuario de la base no tiene. El mensaje incluye la línea
download failed / larger than the limit La URL venció o no es pública / el archivo supera el tope

Rotar credenciales, detener y eliminar

  • Rotar credenciales: genera una contraseña nueva, la aplica y redespliega las apps vinculadas que están en marcha (con la misma imagen, sin recompilar). La contraseña anterior deja de valer al aplicar la nueva: las conexiones ya abiertas siguen, las nuevas con la contraseña vieja fallan hasta que termina el redespliegue.
  • Detener / Iniciar: detiene el servidor sin tocar los datos. Si no logra iniciar, vuelve a Detenida con el motivo.
  • Eliminar: pide escribir el nombre. Toma un backup final, y recién entonces borra el contenedor, la red y el volumen. Si el backup final falla, no se borra nada. La base queda 7 días como eliminada: en ese plazo podés restaurar su backup final en una base nueva (Bases eliminadas). Después se purga.
  • Una cuenta suspendida deja sus bases detenidas (no se borran); al reactivarla vuelven las que detuvo la suspensión.

Referencia de la API

La API usa la sesión del dashboard (cookie cw_session y la cabecera X-Requested-With: cloudweb en las peticiones que modifican).

Método y ruta Qué hace
GET /api/databases · GET /api/databases?deleted=1 Tus bases · las eliminadas hace menos de 7 días
POST /api/databases Crear: {"name","version","storage_gb","memory_mb","backup_hour","backup_keep"}
GET /api/databases/{id} Estado, datos de conexión interna (sin contraseña), apps vinculadas, alertas
DELETE /api/databases/{id}?confirm=<nombre> Eliminar con backup final
POST /api/databases/{id}/stop · /start · /rotate Detener, iniciar, rotar credenciales
PUT / DELETE /api/databases/{id}/links/{app} Vincular / desvincular una app
GET / POST /api/databases/{id}/backups Listar / pedir un backup manual
POST /api/databases/{id}/backups/{backup}/export Pedir la descarga de un backup; luego GET /api/databases/{id}/files/{file} y …/download
POST /api/databases/{id}/restore {"backup_id","target":"same"|"new","name","confirm"}
POST /api/databases/{id}/uploads Reservar una subida: {"name","size"}
PUT /api/databases/{id}/uploads/{file}?offset=N Subir una parte (hasta 16 MiB); si el offset no es el esperado responde 409 con received
POST /api/databases/{id}/imports {"source":"upload"|"url","upload_id"|"url","mode":"empty"|"replace","confirm"}
GET /api/databases/{id}/imports Informe de las cargas