HerméticaDocumentación técnica

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

HerramientaPara quéVerificación
Docker + Compose v2Construir la imagen y correr el entorno localdocker compose version
Google Cloud SDKAprovisionar y operar GCPgcloud version
Node.js 24 + pnpm 10.18Construir el frontendnode -v · pnpm -v
Wrangler CLIOperar Cloudflare Workerswrangler whoami
Cliente PostgreSQL 17Backup y restore de la basepg_dump --version
Python 3.11/3.12Correr manage.py fuera de Dockerpython --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.

VariableObligatoriaPara qué
DB_PASSWORDContraseñ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_SSLMODENoTienen valores por defecto apuntando a la instancia actual. Definirlas explícitamente al migrar de instancia.
GCS_PROJECT_IDProyecto del bucket de archivos.
SECRET_KEYClave de firma de Django (sesiones, tokens). Rotarla invalida las sesiones abiertas.
DEBUG · ALLOWED_HOSTSDEBUG=False en producción; hosts permitidos separados por coma.
FRONTEND_URLURL pública del frontend; se usa para construir los enlaces de los correos.
CRON_SECRETSecreto que valida el disparo del cron de tareas vencidas (cabecera X-Cron-Secret).
ANTHROPIC_API_KEY · ANTHROPIC_MODELSolo Chat NisiraMotor del chat analítico sobre el datawarehouse.
OPENAI_API_KEY · WHISPER_MODELSolo vozTranscripción de audio en el chat.
ELEVENLABS_API_KEY · ELEVENLABS_VOICE_ID · ELEVENLABS_MODELSolo vozSíntesis de voz de las respuestas.
ANALYTICS_CHAT_ENABLED · ANALYTICS_CHAT_MAX_TOOL_ITERATIONSNoInterruptor y tope de iteraciones del Chat Nisira.
DISPATCH_SOURCE_BASE_URL · DISPATCH_SOURCE_USERNAME · DISPATCH_SOURCE_PASSWORDConexión al datawarehouse Nisira que alimenta la importación.
APPROVAL_NOTIFY_EMAILS · OVERDUE_NOTIFY_EMAILS · DELETE_NOTIFY_EMAILSNoDestinatarios de las notificaciones por correo (CSV). Tienen valores por defecto en el código que conviene reemplazar en el traspaso.
MEDIA_URL_OVERRIDE · USE_GCSNoAyudas 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:

  1. Abrir un pull request contra main.
  2. Hacer merge. Cloud Build arranca solo.
  3. Esperar el check hermetica-backend en 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)

  1. En el panel de Cloudflare: Workers & Pages → Create → Connect to Git y elegir el repositorio del frontend.
  2. Configurar la build:
    • Comando de build: pnpm install --frozen-lockfile && pnpm build
    • Directorio de salida: dist
    • Rama de producción: main
  3. Definir la variable de build VITE_API_BASE_URL con la URL pública de la API (por ejemplo https://…run.app/api). Es la única variable que necesita el frontend.
  4. Guardar. A partir de ahí, cada push a una rama genera un preview y cada merge a main despliega 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:

  1. Agregar la zona (por ejemplo hermetica.pe) a la cuenta de Cloudflare, o delegar solo el subdominio.
  2. 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.
  3. Agregar ese origen a CORS_ALLOWED_ORIGINS en hermetica_dir/settings.py del backend y desplegar. Si se omite este paso, el sitio carga pero todas las llamadas a la API fallan por CORS.
  4. Actualizar FRONTEND_URL en 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

  1. Proyecto de GCP creado, facturación vinculada y APIs habilitadas. A1
  2. Instancia de Cloud SQL, base y usuario creados; backups automáticos activos. A2
  3. Bucket de Cloud Storage con la política de acceso decidida. A3
  4. Repositorio de Artifact Registry creado. A4
  5. Roles de IAM otorgados a la cuenta de servicio de Cloud Build. A5
  6. Disparador de Cloud Build apuntando a main. A6
  7. Variables de entorno cargadas en el servicio de Cloud Run. A7
  8. Primera build en verde y /health/ respondiendo. A8
  9. Datos restaurados en la base nueva si es una migración. D
  10. Worker de Cloudflare conectado al repositorio, con VITE_API_BASE_URL. B2
  11. Dominio propio apuntado y agregado a CORS_ALLOWED_ORIGINS. B3
  12. Cron de importación configurado y verificado. Cron
  13. Cloud Scheduler del Portal Gerencial recreado con su CRON_SECRET. Cron