Skip to content

ADR-0007: Objetivo de despliegue

  • Estado: Accepted
  • Fecha: 2026-05-12
  • Responsables de la decisión: Waldemar Szemat

Esta es una implementación de referencia pública. Una URL de demo en vivo es en sí misma una señal determinante: un lector está a dos desplazamientos de hacer clic en un enlace que abre un agente conversacional multi-turno real en un navegador. Esa URL debe ser alcanzable sin un muro de tarjeta de crédito, debe correr la misma imagen que el Dockerfile del proyecto construye localmente, y debe desplegarse de una manera que nunca publique un commit sin validar. Cualquier divergencia entre la imagen de desarrollo y la imagen desplegada socava la historia del arnés de evaluación; el punto central es que lo que corre en CI es lo que corre en producción.

El agente es FastAPI / Uvicorn de un solo proceso, con un almacén Chroma embebido y un embedder de fallback integrado, despachando llamadas de LLM a un proveedor externo (OpenAI por defecto; ver ADR-0002). Sin GPU, sin pesos de modelo que alojar, sin disco persistente más allá de la pequeña KB sintética. Sus primitivas de resiliencia en proceso (limitador de tasa, caché de respuestas, checkpointer de HITL pausado) no se comparten entre workers, así que el despliegue debe poder correr una sola instancia de forma determinista.

¿Cómo distribuimos una URL de demo pública y siempre alcanzable de este agente a costo casi nulo bajo carga de demostración, desde el mismo Dockerfile que el proyecto distribuye, desplegada del lado del servidor y condicionada a una matriz de CI en verde para que una build en rojo nunca alcance producción, y revertida en una sola operación cuando algo se rompe?

  • Misma imagen en todas partes: el despliegue debe construir desde el mismo Dockerfile que el proyecto distribuye. Sin divergencia de un Dockerfile exclusivo de producción entre la laptop de un contribuidor, CI y producción.
  • Despliegue condicionado, del lado del servidor: producción debe publicar solo desde una etiqueta de versión cuya matriz de CI completa quedó en verde, decidido por la plataforma, no por un operador corriendo un comando a mano.
  • Credenciales sin llaves: sin llave JSON de cuenta de servicio de larga vida almacenada como secreto de CI; el despliegue se autentica con un token federado de corta vida.
  • Redespliegues que preservan secretos: las credenciales de proveedores e integraciones viven en un almacén de secretos administrado y sobreviven a cada redespliegue sin volver a ingresarse.
  • Costo casi nulo a escala de demo: la plataforma de demostración es de duración indefinida; un servicio de escala a cero y una sola instancia mantiene el gasto real dentro de la franquicia siempre gratuita del host a tráfico de demostración.
  • Determinismo de una sola instancia: el limitador de tasa, la caché y el checkpointer de HITL en proceso requieren que una sola instancia sirva todo el tráfico; el host debe hacer del techo de una sola instancia un ajuste de primera clase.
  • Rollback en una sola operación: un mal despliegue se revierte desplazando el tráfico de vuelta a la revisión anterior sana, sin reconstrucción.
  • Dominio personalizado: la demo en vivo es alcanzable en un dominio propio del proyecto, no solo en un hostname generado por la plataforma.
  • Google Cloud Run, contenedor de una sola instancia (elegida): mismo Dockerfile mediante un build desde fuente, --max-instances 1, región us-central1, despliegue sin llaves con Workload Identity Federation condicionado a una matriz de CI en verde de la etiqueta, rollback basado en revisiones, mapeo de dominio personalizado, secretos administrados, escala a cero.
  • Hugging Face Spaces, Docker SDK, CPU Basic gratis: la misma imagen en un host genuinamente de costo cero y siempre activo; conservado como el destino de despliegue secundario documentado. Su borde no ofrece el pipeline condicionado y sin llaves, el control de dominio personalizado ni el almacén de secretos administrado que la postura de producción quiere, y su ruta de despliegue es un espejo con force-push en lugar de un build condicionado.
  • Hugging Face Spaces, Gradio / Streamlit SDK: mismo host, pero el SDK construye la interfaz; la superficie FastAPI que el proyecto distribuye no coincidiría con la imagen desplegada.
  • Render Web Service, nivel gratuito: mismo objetivo de Dockerfile mediante $PORT; duerme tras 15 minutos de inactividad con un arranque en frío de 30-60 s; sin pipeline de despliegue condicionado y sin llaves de fábrica.
  • Fly.io, nivel gratuito: el nivel gratuito original terminó en octubre de 2024, reemplazado por una postura de crédito de prueba de $5 / mes.
  • Railway, nivel gratuito: el plan gratuito se retiró en agosto de 2023 a favor de créditos de prueba de $5 / mes; no es de costo casi nulo.
  • Vercel, plan Hobby: el timeout de función serverless de 10 s en Hobby mata cualquier stream de agente multi-turno una vez que el viaje de ida y vuelta del LLM excede el límite.
  • Cloudflare Workers AI: una plataforma de enrutamiento de modelos más que un host Docker genérico; el stack de Python + Chroma embebido necesitaría reescribirse contra el runtime de Workers.
  • Streamlit Community Cloud: gratis, pero ata la demo a una interfaz de Streamlit; la superficie FastAPI se vuelve inaccesible.
  • Modal: créditos de prueba, luego pago por segundo; el estado estable no es de costo casi nulo.

Opción elegida: Google Cloud Run, contenedor de una sola instancia, con Hugging Face Spaces, Docker SDK documentado como el destino secundario del operador en la referencia de despliegue y alcanzable desde la misma imagen con un cambio de puerto ($PORT).

El servicio ai-agent-eval-harness corre en el proyecto ai-agent-eval-harness, región us-central1, limitado a --max-instances 1, y es alcanzable en el dominio personalizado agent.szemat.pro. Cloud Build construye la imagen desde el Dockerfile del repositorio mediante un despliegue desde fuente, de modo que la misma imagen corre en tres lugares: la laptop de un contribuidor, el arnés de evaluación y la demo pública. Una imagen, un modelo mental, un conjunto de comportamientos bajo prueba.

El despliegue es del lado del servidor, sin llaves y condicionado. Una etiqueta de versión que coincide con vX.Y.Z dispara el flujo de trabajo tag-guard, que vuelve a ejecutar la matriz de CI completa contra el commit etiquetado. Solo cuando esa corrida concluye success se dispara el flujo de trabajo deploy (vía workflow_run), se autentica en Google Cloud con Workload Identity Federation (un token OIDC de corta vida, sin llave JSON almacenada) y corre un despliegue desde fuente de Cloud Run que preserva los secretos. Una matriz de CI en rojo significa que el job de despliegue se omite y nada se publica. La reconstrucción desde fuente preserva las variables de entorno y los vínculos con Secret Manager existentes del servicio; una revisión fallida mantiene el tráfico en la anterior sana.

El techo de una sola instancia es determinante: el limitador de tasa, la caché de respuestas y el checkpointer de HITL en proceso no se comparten entre instancias, así que el servicio corre exactamente una instancia por diseño. La escala a cero de Cloud Run mantiene el gasto real dentro de la franquicia siempre gratuita del host a tráfico de demostración; el contenedor no aloja ningún LLM local, así que solo maneja la recuperación de RAG, la orquestación y el HTTP, despachando los completados a un proveedor externo.

  • Una matriz de CI de tag-guard en verde en una etiqueta vX.Y.Z, seguida de una corrida en verde del flujo de trabajo deploy que se autenticó sin llaves vía WIF.
  • Una URL pública alcanzable en https://agent.szemat.pro.
  • El servicio responde GET /health con 200 OK.
  • El servicio responde POST /chat en modo demo contra un cliente stub fuera de línea (sin claves del lado del llamador requeridas para la demo pública).
  • La referencia de despliegue documenta el bootstrap del operador (pool / proveedor de WIF + cuenta de servicio de despliegue de mínimo privilegio, vínculos con Secret Manager, primer despliegue).
  • Despliegues condicionados, del lado del servidor: solo una etiqueta de versión publicada-en-verde puede alcanzar producción; un operador no puede publicar a mano un commit sin validar.
  • Credenciales sin llaves: no existe ni se almacena ninguna llave de cuenta de servicio de larga vida; el despliegue usa un token federado de corta vida.
  • Mismo Dockerfile en todas partes: desarrollo, CI, producción.
  • Redespliegues que preservan secretos: los vínculos con secretos administrados sobreviven a cada despliegue desde fuente.
  • Rollback en una sola operación: desplazar el tráfico de vuelta a la revisión anterior sana; sin reconstrucción.
  • Dominio personalizado: la demo en vivo se sirve en agent.szemat.pro.
  • Sin alojamiento de pesos de modelo: evita LFS y cualquier nivel de almacenamiento en el host; mantiene la imagen pequeña.
  • Arranque en frío con escala a cero: la primera solicitud tras una ventana de inactividad paga un arranque en frío del contenedor (importación de dependencias, ingesta inicial de la KB, carga del embedder).
  • Techo de una sola instancia: --max-instances 1 limita la concurrencia horizontal; escalar hacia afuera requeriría mover las primitivas en proceso a un almacén compartido (Redis, Postgres), fuera de alcance para la demo.
  • Cableado de nube único: el despliegue sin llaves necesita un pool / proveedor de WIF y una cuenta de servicio de despliegue de mínimo privilegio configurados una vez antes de que el pipeline pueda autenticarse.
  • Solo CPU: el contenedor no puede alojar un LLM local; la demo depende de un proveedor externo para los completados. Por diseño (ver ADR-0002).
  • Una puerta tag-guard y un flujo de trabajo deploy se vuelven parte del diseño de CI del repositorio.
  • Las variables de repositorio (WIF_PROVIDER, WIF_SERVICE_ACCOUNT) se vuelven requeridas para el despliegue en vivo; los contribuidores sin derechos de despliegue trabajan localmente sin ellas.
  • La misma imagen se mantiene desplegable a Hugging Face Spaces como destino secundario, así que la referencia de despliegue documenta dos hosts.

Google Cloud Run, contenedor de una sola instancia

Section titled “Google Cloud Run, contenedor de una sola instancia”
  • Buena, porque el despliegue está condicionado a una matriz de CI en verde y corre del lado del servidor, sin llaves vía WIF.
  • Buena, porque un build desde fuente corre el Dockerfile del proyecto tal cual y preserva los secretos administrados a través de los redespliegues.
  • Buena, porque el rollback es un desplazamiento de tráfico en una sola operación a la revisión anterior.
  • Buena, porque --max-instances 1 hace de la postura requerida de una sola instancia un ajuste de primera clase, y la escala a cero mantiene el costo de la demo cerca de cero.
  • Buena, porque el servicio se mapea a un dominio personalizado propio del proyecto.
  • Mala, porque la escala a cero implica un arranque en frío en la primera solicitud tras la inactividad.
  • Mala, porque el escalado horizontal hacia afuera necesita un almacén compartido para las primitivas en proceso.

Hugging Face Spaces, Docker SDK, CPU Basic gratis

Section titled “Hugging Face Spaces, Docker SDK, CPU Basic gratis”
  • Buena, porque la misma imagen corre sin cambios a costo genuinamente cero, siempre activa, lo que la vuelve un destino de demo secundario ideal.
  • Buena, porque el dominio huggingface.co señala “implementación de referencia de IA” de un vistazo.
  • Mala, porque su borde no ofrece pipeline de despliegue condicionado y sin llaves, ni almacén de secretos administrado, ni control de dominio personalizado; la ruta de despliegue es un espejo con force-push en lugar de un build condicionado.
  • Mala, porque el front-matter YAML de la tarjeta del Space requiere un intercambio exclusivo de despliegue del README raíz.

Hugging Face Spaces, Gradio / Streamlit SDK

Section titled “Hugging Face Spaces, Gradio / Streamlit SDK”
  • Buena, porque los SDKs distribuyen una interfaz alojada y opinada.
  • Mala, porque la superficie de la demo diverge de la superficie FastAPI que el proyecto distribuye; lo desplegado deja de ser lo mismo que lo construido localmente.
  • Buena, porque el mismo Dockerfile se despliega con un cambio de una variable de entorno ($PORT).
  • Mala, porque el sueño por inactividad de 15 minutos es más agresivo que la escala a cero en un servicio ya caliente, y no hay pipeline de despliegue condicionado y sin llaves de fábrica.
  • Mala, porque los niveles gratuitos históricos se retiraron (Fly.io octubre de 2024, Railway agosto de 2023) a favor de créditos de prueba; el estado estable no es de costo casi nulo.
  • Mala, porque el timeout de función de 10 segundos mata cualquier stream de agente multi-turno una vez que el viaje de ida y vuelta del LLM lo excede.
  • Mala, porque el stack de Python + Chroma embebido tendría que ser reescrito contra el runtime de Workers.
  • Mala, porque la interfaz ata la demo a Streamlit; la superficie FastAPI se vuelve inaccesible.
  • Mala, porque los créditos de prueba expiran; el estado estable cuesta dinero.

Tres primitivas por proceso se distribuyen para hacer que la demo se degrade con elegancia bajo carga en lugar de exponer errores upstream en bruto. Las tres se controlan por banderas de habilitación, de modo que están desactivadas por defecto para pruebas deterministas y activadas para el servicio en vivo:

  • Limitador de tasa por sesión. Un limitador de ventana deslizante por clave (por IP de cliente). Un llamador que excede el límite recibe un HTTP 429 con una cabecera Retry-After en lugar de un 502 en bruto. Ajustable mediante ajustes de máximo de solicitudes y segundos de ventana. Una dependencia deliberadamente no se añadió; el limitador en proceso mantiene la postura libre de framework.
  • Cadena de fallback de proveedor. Un envoltorio alrededor del Protocol LLMClient: una falla transitoria de OpenAI (HTTP 429, 5xx, o una falla de transporte pelada) cae en cascada a Anthropic antes de que cualquier error alcance el frontend. Un 4xx que no es 429 es un error genuino del cliente y se re-lanza sin cambios. Consistente con la abstracción de proveedor de ADR-0002 - el fallback es un envoltorio a nivel de Protocol, no un cambio a nivel de nodo.
  • Caché de respuestas de TTL corto. Una caché en proceso acotada y de TTL corto, indexada por la tupla normalizada (entrada, locale, modelo), de modo que los clics de “cargar ejemplo” de la SPA se sirven sin golpear al proveedor.

Consecuencia de una sola instancia. Las tres primitivas son por proceso, al igual que el checkpointer HITL en memoria (ver ADR-0001). El servicio, por lo tanto, corre una sola instancia por diseño (--max-instances 1); una segunda instancia no compartiría el limitador, la caché ni el estado del hilo en pausa. Un despliegue multi-instancia necesitaría un almacén compartido (Redis, Postgres), que está fuera de alcance para la demo. Esto está documentado en la referencia de despliegue y en el comentario del Dockerfile.

Embedder integrado. El embedder de fallback local integrado es BAAI/bge-small-en-v1.5, compatible con CPU en el servicio de una sola instancia; el embedder primario es Voyage voyage-3.5 cuando hay una clave de Voyage configurada. Ver ADR-0004 para la decisión del embedder; la postura de despliegue aquí no se ve afectada por la elección del modelo.