ADR-0007: Objetivo de despliegue
- Estado: Accepted
- Fecha: 2026-05-12
- Responsables de la decisión: Waldemar Szemat
Contexto y planteamiento del problema
Section titled “Contexto y planteamiento del problema”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?
Impulsores de la decisión
Section titled “Impulsores de la decisión”- Misma imagen en todas partes: el despliegue debe construir desde el mismo
Dockerfileque 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.
Opciones consideradas
Section titled “Opciones consideradas”- Google Cloud Run, contenedor de una sola instancia (elegida): mismo
Dockerfile mediante un build desde fuente,
--max-instances 1, regiónus-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.
Resultado de la decisión
Section titled “Resultado de la decisión”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.
Confirmación
Section titled “Confirmación”- Una matriz de CI de
tag-guarden verde en una etiquetavX.Y.Z, seguida de una corrida en verde del flujo de trabajodeployque se autenticó sin llaves vía WIF. - Una URL pública alcanzable en
https://agent.szemat.pro. - El servicio responde
GET /healthcon200 OK. - El servicio responde
POST /chaten 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).
Consecuencias
Section titled “Consecuencias”Positivas
Section titled “Positivas”- 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.
Negativas
Section titled “Negativas”- 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 1limita 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).
Neutrales
Section titled “Neutrales”- Una puerta
tag-guardy un flujo de trabajodeployse 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.
Pros y contras de las opciones
Section titled “Pros y contras de las opciones”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
Dockerfiledel 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 1hace 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.coseñ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.
Render Web Service, nivel gratuito
Section titled “Render Web Service, nivel gratuito”- Buena, porque el mismo
Dockerfilese 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.
Niveles gratuitos de Fly.io / Railway
Section titled “Niveles gratuitos de Fly.io / Railway”- 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.
Vercel Hobby
Section titled “Vercel Hobby”- 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.
Cloudflare Workers AI
Section titled “Cloudflare Workers AI”- Mala, porque el stack de Python + Chroma embebido tendría que ser reescrito contra el runtime de Workers.
Streamlit Community Cloud
Section titled “Streamlit Community Cloud”- 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.
Más información
Section titled “Más información”- Runbook del operador: referencia de despliegue
- Documentación de Google Cloud Run: https://cloud.google.com/run/docs
- Workload Identity Federation para GitHub Actions: https://cloud.google.com/iam/docs/workload-identity-federation
- Documentación del Docker SDK de Hugging Face Spaces: https://huggingface.co/docs/hub/spaces-sdks-docker
- Documentación del nivel gratuito de Render: https://render.com/docs/free
- Hilo de post-mortem del nivel gratuito de Fly.io: https://community.fly.io/t/free-tier-is-dead/20651
- Límites de runtime de funciones de Vercel: https://vercel.com/docs/functions/runtimes#max-duration
- MADR 4.0.0: https://adr.github.io/madr/
Capa de resiliencia de despliegue
Section titled “Capa de resiliencia de despliegue”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-Afteren 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.