Operación
Guía de despliegue
Cómo se instala, configura y pone en producción cada componente. Incluye el aprovisionamiento desde cero en una cuenta nueva de Google Cloud y Cloudflare, el flujo de despliegue del día a día, el entorno local y el procedimiento de reversión.
Resumen en una línea: ambos componentes despliegan solos al hacer merge a
main. El trabajo manual es únicamente el aprovisionamiento inicial y la carga de
variables de entorno.
0 · Requisitos previos
| Herramienta | Para qué | Verificación |
|---|---|---|
| Docker + Compose v2 | Construir la imagen y correr el entorno local | docker compose version |
| Google Cloud SDK | Aprovisionar y operar GCP | gcloud version |
| Node.js 24 + pnpm 10.18 | Construir el frontend | node -v · pnpm -v |
| Wrangler CLI | Operar Cloudflare Workers | wrangler whoami |
| Cliente PostgreSQL 17 | Backup y restore de la base | pg_dump --version |
| Python 3.11/3.12 | Correr manage.py fuera de Docker | python --version |
El cliente de PostgreSQL debe ser 17. pg_dump se niega a volcar un
servidor más nuevo que él, y Cloud SQL corre 17.9. El postgresql-client de Debian
bookworm es 15 y no sirve; hay que instalar el paquete 17 desde el repositorio PGDG. El
repositorio incluye un Dockerfile.local que ya lo trae.
Parte A · Backend en Google Cloud
El backend es un contenedor Docker que corre en Cloud Run, con la base en Cloud SQL y los archivos en Cloud Storage. Cloud Build es quien construye y publica.
A1 · Crear el proyecto y habilitar APIs
# Proyecto (el actual se llama "hermetica"; usar el id que corresponda)
gcloud projects create PROJECT_ID --name="Hermetica"
gcloud config set project PROJECT_ID
gcloud billing projects link PROJECT_ID --billing-account=BILLING_ID
# APIs necesarias
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
sqladmin.googleapis.com \
storage.googleapis.com \
cloudscheduler.googleapis.com \
secretmanager.googleapis.com
A2 · Base de datos (Cloud SQL)
gcloud sql instances create hermetica-comercial \
--database-version=POSTGRES_17 \
--region=us-central1 \
--tier=db-custom-2-7680 \
--storage-auto-increase \
--backup-start-time=07:00 \
--retained-backups-count=14 \
--require-ssl
gcloud sql databases create hermetica-comercial-db --instance=hermetica-comercial
gcloud sql users create hermetica-sql --instance=hermetica-comercial --password='…'
# Allowlist de IPs que pueden conectarse (Cloud Run sale por IPs dinámicas:
# usar conector de VPC / Cloud SQL Auth Proxy, o autorizar las IPs de trabajo)
gcloud sql instances patch hermetica-comercial --authorized-networks=IP_1,IP_2
Hoy la instancia usa IP pública con allowlist y sslmode=require. Si se
reconstruye desde cero, la opción recomendada es conectar Cloud Run por Cloud SQL Auth
Proxy / conector serverless y dejar la instancia sin IP pública. El código no cambia: solo
cambian DB_HOST y DB_PORT.
A3 · Almacenamiento de archivos (Cloud Storage)
El propio cloudbuild.yaml crea el bucket si no existe, pero conviene crearlo a
mano para controlar la política de acceso:
gsutil mb -l us-central1 gs://hermetica_bucket
gsutil iam ch allUsers:objectViewer gs://hermetica_bucket # lectura pública (config. actual)
gsutil cors set cors.json gs://hermetica_bucket
La cuenta de servicio de Cloud Run necesita roles/storage.objectAdmin sobre el
bucket para poder escribir adjuntos y fotos.
A4 · Registro de imágenes (Artifact Registry)
gcloud artifacts repositories create lu-hermetica-repo \
--repository-format=docker --location=us-central1 \
--description="Imágenes del backend comercial"
A5 · Permisos de Cloud Build
La cuenta de servicio de Cloud Build necesita poder desplegar en Cloud Run y usar la cuenta de servicio del servicio. Sin estos dos roles, la build falla en el último paso:
PROJECT_NUMBER=$(gcloud projects describe PROJECT_ID --format='value(projectNumber)')
CB_SA="${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:${CB_SA}" --role="roles/run.admin"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:${CB_SA}" --role="roles/iam.serviceAccountUser"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:${CB_SA}" --role="roles/artifactregistry.writer"
A6 · Disparador de Cloud Build
Conectar el repositorio de GitHub y crear el disparador sobre main:
gcloud builds triggers create github \
--name=hermetica-backend \
--repo-owner=HERMETICA-SAC \
--repo-name=lu-hermetica-comercial-backend \
--branch-pattern="^main$" \
--build-config=cloudbuild.yaml \
--region=us-central1
El disparador publica un check-run en GitHub llamado hermetica-backend
en cada commit de main. Eso permite seguir el estado del despliegue desde el propio
pull request, sin abrir la consola de GCP.
A7 · Variables de entorno del servicio
cloudbuild.yaml despliega con --update-env-vars (no
--set-env-vars) precisamente para no borrar en cada deploy las variables que
se administran fuera del repositorio. Estas se cargan una sola vez sobre el servicio de Cloud Run
y sobreviven a los despliegues.
| Variable | Obligatoria | Para qué |
|---|---|---|
DB_PASSWORD | Sí | Contraseña de Cloud SQL. Además es el interruptor que hace a settings.py usar PostgreSQL en vez de SQLite y activar el almacenamiento en GCS. |
DB_NAME · DB_USER · DB_HOST · DB_PORT · DB_SSLMODE | No | Tienen valores por defecto apuntando a la instancia actual. Definirlas explícitamente al migrar de instancia. |
GCS_PROJECT_ID | Sí | Proyecto del bucket de archivos. |
SECRET_KEY | Sí | Clave de firma de Django (sesiones, tokens). Rotarla invalida las sesiones abiertas. |
DEBUG · ALLOWED_HOSTS | Sí | DEBUG=False en producción; hosts permitidos separados por coma. |
FRONTEND_URL | Sí | URL pública del frontend; se usa para construir los enlaces de los correos. |
CRON_SECRET | Sí | Secreto que valida el disparo del cron de tareas vencidas (cabecera X-Cron-Secret). |
ANTHROPIC_API_KEY · ANTHROPIC_MODEL | Solo Chat Nisira | Motor del chat analítico sobre el datawarehouse. |
OPENAI_API_KEY · WHISPER_MODEL | Solo voz | Transcripción de audio en el chat. |
ELEVENLABS_API_KEY · ELEVENLABS_VOICE_ID · ELEVENLABS_MODEL | Solo voz | Síntesis de voz de las respuestas. |
ANALYTICS_CHAT_ENABLED · ANALYTICS_CHAT_MAX_TOOL_ITERATIONS | No | Interruptor y tope de iteraciones del Chat Nisira. |
DISPATCH_SOURCE_BASE_URL · DISPATCH_SOURCE_USERNAME · DISPATCH_SOURCE_PASSWORD | Sí | Conexión al datawarehouse Nisira que alimenta la importación. |
APPROVAL_NOTIFY_EMAILS · OVERDUE_NOTIFY_EMAILS · DELETE_NOTIFY_EMAILS | No | Destinatarios de las notificaciones por correo (CSV). Tienen valores por defecto en el código que conviene reemplazar en el traspaso. |
MEDIA_URL_OVERRIDE · USE_GCS | No | Ayudas para desarrollo: ver imágenes de producción desde un entorno local. |
# Cargar/actualizar variables sin tocar las demás
gcloud run services update lu-hermetica-comercial-backend \
--region=us-central1 \
--update-env-vars SECRET_KEY=…,ANTHROPIC_API_KEY=…,CRON_SECRET=…
# Ver las que están cargadas hoy
gcloud run services describe lu-hermetica-comercial-backend --region=us-central1 \
--format='value(spec.template.spec.containers[0].env)'
Deuda técnica a corregir en el traspaso. Hoy hay credenciales escritas directamente
en el código: la contraseña de la base en cloudbuild.yaml, la del correo en
settings.py y la de Nisira en
dispatch/management/commands/import_dispatch.py. Lo correcto es moverlas a
Secret Manager y referenciarlas con --set-secrets en el deploy. Mientras
eso no se haga, cualquiera con acceso de lectura al repositorio tiene acceso a producción.
A8 · Primer despliegue y despliegues posteriores
Con el disparador creado, el despliegue es simplemente:
- Abrir un pull request contra
main. - Hacer merge. Cloud Build arranca solo.
- Esperar el check
hermetica-backenden verde (3–6 minutos típicos).
Para lanzar una build a mano, sin pasar por GitHub:
gcloud builds submit --config=cloudbuild.yaml --project=PROJECT_ID .
# Seguimiento
gcloud builds list --project=PROJECT_ID --limit=5
gcloud builds log BUILD_ID --project=PROJECT_ID --stream
Un check en verde implica que las migraciones se aplicaron. El paso
manage.py migrate --noinput corre antes del gcloud run deploy y Cloud
Build aborta la build si devuelve un código distinto de cero. Si la build terminó bien, el
esquema de la base ya está al día.
A9 · Verificación, logs y reversión
# ¿Responde el servicio?
curl -s https://SERVICIO.run.app/health/
# → {"status":"healthy","service":"lu-hermetica-comercial-backend"}
# Logs en vivo
gcloud run services logs tail lu-hermetica-comercial-backend --region=us-central1
# Errores de la última hora
gcloud logging read \
'resource.type="cloud_run_revision" AND severity>=ERROR' \
--limit=50 --freshness=1h --project=PROJECT_ID
# Revertir a la revisión anterior (inmediato, sin rebuild)
gcloud run revisions list --service=lu-hermetica-comercial-backend --region=us-central1
gcloud run services update-traffic lu-hermetica-comercial-backend \
--region=us-central1 --to-revisions=REVISION_ANTERIOR=100
La reversión del código no revierte la base. Si el despliegue incluía una migración destructiva, volver a la revisión anterior deja el código viejo contra un esquema nuevo. Ante una migración de riesgo, tomar un backup antes (sección D) y planificar la migración inversa.
Parte B · Frontend en Cloudflare
El frontend es un sitio estático (el dist/ que produce Vite) servido por un
Cloudflare Worker con static assets. No hay servidor Node en ejecución.
B1 · Configuración del Worker
Toda la configuración vive en wrangler.json, en la raíz del repositorio:
{
"name": "lu-hermetica-comercial-webapp",
"compatibility_date": "2025-12-18",
"assets": { "directory": "./dist" }
}
B2 · Conectar el repositorio (Workers Builds)
- En el panel de Cloudflare: Workers & Pages → Create → Connect to Git y elegir el repositorio del frontend.
- Configurar la build:
- Comando de build:
pnpm install --frozen-lockfile && pnpm build - Directorio de salida:
dist - Rama de producción:
main
- Comando de build:
- Definir la variable de build
VITE_API_BASE_URLcon la URL pública de la API (por ejemplohttps://…run.app/api). Es la única variable que necesita el frontend. - Guardar. A partir de ahí, cada push a una rama genera un preview y cada
merge a
maindespliega a producción (~4 minutos).
VITE_API_BASE_URL se congela en el bundle. Vite sustituye las variables
en tiempo de compilación, no de ejecución. Cambiar la URL de la API exige reconstruir y
volver a desplegar el frontend, no solo editar una variable.
B3 · Dominio propio
Hoy el sitio responde en hermetica.lumini.dev, un subdominio del proveedor. Para
apuntarlo a un dominio de Hermética:
- Agregar la zona (por ejemplo
hermetica.pe) a la cuenta de Cloudflare, o delegar solo el subdominio. - En el Worker: Settings → Domains & Routes → Add custom domain e indicar
app.hermetica.pe(o el que corresponda). Cloudflare emite el certificado TLS automáticamente. - Agregar ese origen a
CORS_ALLOWED_ORIGINSenhermetica_dir/settings.pydel backend y desplegar. Si se omite este paso, el sitio carga pero todas las llamadas a la API fallan por CORS. - Actualizar
FRONTEND_URLen Cloud Run para que los enlaces de los correos apunten al dominio nuevo.
B4 · Despliegue manual y reversión
# Build y publicación manual (requiere wrangler autenticado)
pnpm install --frozen-lockfile
pnpm build
wrangler deploy
# Reversión: desde el panel, Deployments → versión anterior → Rollback
wrangler deployments list
wrangler rollback VERSION_ID
Después de desplegar, el usuario puede seguir viendo la versión anterior. La app es
una PWA con precache: el service worker nuevo queda en espera hasta que se cierran todas las
pestañas del dominio, y un refresco forzado no lo desbloquea. Para verificar un despliegue,
comparar el hash del bundle (curl -s https://DOMINIO/ | grep -o 'index-[A-Za-z0-9_-]*\.js')
en vez de fiarse de lo que se ve en pantalla.
Parte C · Entorno local
C1 · Backend
# 1. PostgreSQL local en Docker
docker compose -f docker-compose.local.yml --env-file .env.local up -d
# El contenedor hermetica_local_postgres debe quedar "healthy"
# 2. Variables y migraciones
set -o allexport && source .env.local && set +o allexport
python manage.py migrate
python manage.py create_super_admin
# 3. Servidor
python manage.py runserver 0.0.0.0:8001
Con DB_PASSWORD vacío, settings.py cae a SQLite
(db.sqlite3) y a almacenamiento en disco. Es el modo más rápido para levantar el
proyecto por primera vez; para reproducir el comportamiento de producción hay que usar
PostgreSQL.
C2 · Frontend
pnpm install
echo 'VITE_API_BASE_URL=http://127.0.0.1:8001/api' > .env
pnpm dev # http://localhost:5173
El backend ya permite CORS desde localhost:5173 a localhost:5177, así
que no hace falta tocar nada del lado del servidor.
Parte D · Base de datos
El repositorio incluye comandos de gestión para respaldar y restaurar, con guardas para evitar
accidentes (se niegan a volcar localhost, o a restaurar sobre un host remoto).
D1 · Respaldo de producción
set -o allexport && source .env.prod && set +o allexport
# Verificar a qué base apunta antes de nada
python manage.py shell -c "from django.conf import settings; d=settings.DATABASES['default']; print(d['HOST'], d['NAME'])"
python manage.py dump_production_db
# → backups/prod_AAAAMMDD_HHMMSS.dump (formato pg_dump -Fc, comprimido)
Al terminar imprime un recuento fila por fila de cada tabla del esquema public.
Ese reporte es lo que se compara contra el del restore para comprobar que no se perdió nada.
Opciones: --format plain para SQL en texto, --output para otra ruta,
--skip-counts para omitir el recuento.
D2 · Restauración en local
set -o allexport && source .env.local && set +o allexport
python manage.py restore_backup_to_local backups/prod_AAAAMMDD_HHMMSS.dump
# DROP SCHEMA + pg_restore. Solo acepta hosts locales.
D3 · Respaldos administrados
Cloud SQL tiene sus propios backups automáticos y recuperación a un punto en el tiempo. Es la red de seguridad real; los volcados manuales son para copias de trabajo.
gcloud sql backups list --instance=hermetica-comercial
gcloud sql backups create --instance=hermetica-comercial # backup a demanda
gcloud sql export sql hermetica-comercial gs://BUCKET/dump.sql.gz \
--database=hermetica-comercial-db # exportar a GCS
Checklist de puesta en producción desde cero
- Proyecto de GCP creado, facturación vinculada y APIs habilitadas. A1
- Instancia de Cloud SQL, base y usuario creados; backups automáticos activos. A2
- Bucket de Cloud Storage con la política de acceso decidida. A3
- Repositorio de Artifact Registry creado. A4
- Roles de IAM otorgados a la cuenta de servicio de Cloud Build. A5
- Disparador de Cloud Build apuntando a
main. A6 - Variables de entorno cargadas en el servicio de Cloud Run. A7
- Primera build en verde y
/health/respondiendo. A8 - Datos restaurados en la base nueva si es una migración. D
- Worker de Cloudflare conectado al repositorio, con
VITE_API_BASE_URL. B2 - Dominio propio apuntado y agregado a
CORS_ALLOWED_ORIGINS. B3 - Cron de importación configurado y verificado. Cron
- Cloud Scheduler del Portal Gerencial recreado con su
CRON_SECRET. Cron