Pular para o conteúdo

ADR-0007: Alvo de implantação

  • Status: Accepted
  • Data: 2026-05-12
  • Responsáveis pela decisão: Waldemar Szemat

Esta é uma implementação de referência pública. Uma URL de demo ao vivo é em si um sinal determinante: um leitor está a dois scrolls de clicar em um link que abre um agente conversacional multi-turno real em um navegador. Essa URL deve ser alcançável sem um muro de cartão de crédito, deve rodar a mesma imagem que o Dockerfile do projeto constrói localmente, e deve ser implantada de uma forma que nunca publique um commit não validado. Qualquer divergência entre a imagem de desenvolvimento e a imagem implantada compromete a história do harness de avaliação; o ponto inteiro é que o que roda em CI é o que roda em produção.

O agente é FastAPI / Uvicorn de processo único, com um armazenamento Chroma embarcado e um embedder de fallback embutido, despachando chamadas de LLM para um provedor externo (OpenAI por padrão; ver ADR-0002). Sem GPU, sem peso de modelo para hospedar, sem disco persistente além da pequena KB sintética. Suas primitivas de resiliência em processo (limitador de taxa, cache de respostas, checkpointer de HITL pausado) não são compartilhadas entre workers, então a implantação deve conseguir rodar uma única instância de forma determinística.

Como distribuímos uma URL de demo pública e sempre alcançável deste agente a custo quase nulo sob carga de demonstração, a partir do mesmo Dockerfile que o projeto distribui, implantada do lado do servidor e condicionada a uma matriz de CI verde para que uma build vermelha nunca alcance produção, e revertida em uma única operação quando algo quebra?

  • Mesma imagem em todo lugar: a implantação deve construir a partir do mesmo Dockerfile que o projeto distribui. Sem divergência de Dockerfile exclusivo de produção entre o laptop de um contribuidor, a CI e a produção.
  • Implantação condicionada, do lado do servidor: a produção deve publicar apenas a partir de uma tag de release cuja matriz de CI completa ficou verde, decidido pela plataforma, não por um operador rodando um comando à mão.
  • Credenciais sem chaves: sem chave JSON de conta de serviço de longa duração armazenada como segredo de CI; a implantação se autentica com um token federado de curta duração.
  • Redeploys que preservam segredos: as credenciais de provedores e integrações vivem em um armazenamento de segredos gerenciado e sobrevivem a cada redeploy sem serem reinseridas.
  • Custo quase nulo em escala de demo: a plataforma de demonstração é aberta; um serviço de escala a zero e instância única mantém o gasto real dentro da franquia sempre gratuita do host em tráfego de demonstração.
  • Determinismo de instância única: o limitador de taxa, o cache e o checkpointer de HITL em processo exigem que uma única instância atenda todo o tráfego; o host deve tornar o teto de instância única uma configuração de primeira classe.
  • Rollback em uma única operação: uma implantação ruim é revertida migrando o tráfego de volta para a revisão anterior saudável, sem reconstrução.
  • Domínio personalizado: a demo ao vivo é alcançável em um domínio próprio do projeto, não apenas em um hostname gerado pela plataforma.
  • Google Cloud Run, contêiner de instância única (escolhida): mesmo Dockerfile via um build a partir do fonte, --max-instances 1, região us-central1, implantação sem chaves com Workload Identity Federation condicionada a uma matriz de CI verde da tag, rollback baseado em revisões, mapeamento de domínio personalizado, segredos gerenciados, escala a zero.
  • Hugging Face Spaces, Docker SDK, CPU Basic gratuito: a mesma imagem em um host genuinamente de custo zero e sempre ativo; mantido como o alvo de implantação secundário documentado. Sua borda não oferece o pipeline condicionado e sem chaves, o controle de domínio personalizado nem o armazenamento de segredos gerenciado que a postura de produção quer, e seu caminho de implantação é um espelho com force-push em vez de um build condicionado.
  • Hugging Face Spaces, SDK Gradio / Streamlit: mesmo host, mas o SDK constrói a UI; a superfície FastAPI que o projeto distribui não corresponderia à imagem implantada.
  • Render Web Service, nível gratuito: mesmo alvo de Dockerfile via $PORT; dorme após 15 minutos de ociosidade com um cold start de 30-60 s; sem pipeline de implantação condicionado e sem chaves de fábrica.
  • Fly.io, nível gratuito: o nível gratuito original terminou em outubro de 2024, substituído por uma postura de crédito de teste de $5 / mês.
  • Railway, nível gratuito: plano gratuito retirado em agosto de 2023 em favor de créditos de teste de $5 / mês; não é de custo quase nulo.
  • Vercel, plano Hobby: timeout de função serverless de 10 s no Hobby mata qualquer stream de agente multi-turno assim que a ida e volta do LLM excede o limite.
  • Cloudflare Workers AI: uma plataforma de roteamento de modelos em vez de um host Docker genérico; o stack Python + Chroma embarcado precisaria ser reescrito contra o runtime do Workers.
  • Streamlit Community Cloud: gratuito, mas atrela a demo a uma UI Streamlit; a superfície FastAPI fica inacessível.
  • Modal: créditos de teste, depois pague-por-segundo; o regime permanente não é de custo quase nulo.

Opção escolhida: Google Cloud Run, contêiner de instância única, com Hugging Face Spaces, Docker SDK documentado como o alvo secundário do operador na referência de implantação e alcançável a partir da mesma imagem com uma mudança de porta ($PORT).

O serviço ai-agent-eval-harness roda no projeto ai-agent-eval-harness, região us-central1, limitado a --max-instances 1, e é alcançável no domínio personalizado agent.szemat.pro. O Cloud Build constrói a imagem a partir do Dockerfile do repositório via uma implantação a partir do fonte, de modo que a mesma imagem roda em três lugares: o laptop de um contribuidor, o harness de avaliação e a demo pública. Uma imagem, um modelo mental, um conjunto de comportamentos sob teste.

A implantação é do lado do servidor, sem chaves e condicionada. Uma tag de release que corresponde a vX.Y.Z dispara o workflow tag-guard, que roda novamente a matriz de CI completa contra o commit marcado com a tag. Somente quando essa execução conclui success é que o workflow deploy dispara (via workflow_run), autentica-se no Google Cloud com Workload Identity Federation (um token OIDC de curta duração, sem chave JSON armazenada) e roda um deploy a partir do fonte do Cloud Run que preserva os segredos. Uma matriz de CI vermelha significa que o job de implantação é ignorado e nada é publicado. A reconstrução a partir do fonte preserva as variáveis de ambiente e os vínculos com o Secret Manager existentes no serviço; uma revisão com falha mantém o tráfego na anterior saudável.

O teto de instância única é determinante: o limitador de taxa, o cache de respostas e o checkpointer de HITL em processo não são compartilhados entre instâncias, então o serviço roda exatamente uma instância por design. A escala a zero do Cloud Run mantém o gasto real dentro da franquia sempre gratuita do host em tráfego de demonstração; o contêiner não hospeda nenhum LLM local, então só lida com recuperação de RAG, orquestração e HTTP, despachando as completudes para um provedor externo.

  • Uma matriz de CI do tag-guard verde em uma tag vX.Y.Z, seguida de uma execução verde do workflow deploy que se autenticou sem chaves via WIF.
  • Uma URL pública alcançável em https://agent.szemat.pro.
  • O serviço responde a GET /health com 200 OK.
  • O serviço responde a POST /chat em modo demo contra um cliente stub offline (sem chaves do lado do chamador necessárias para a demo pública).
  • A referência de implantação documenta o bootstrap do operador (pool / provider de WIF + conta de serviço de implantação de privilégio mínimo, vínculos com o Secret Manager, primeira implantação).
  • Implantações condicionadas, do lado do servidor: apenas uma tag de release publicada-em-verde pode alcançar produção; um operador não pode publicar à mão um commit não validado.
  • Credenciais sem chaves: nenhuma chave de conta de serviço de longa duração existe ou é armazenada; a implantação usa um token federado de curta duração.
  • Mesmo Dockerfile em todo lugar: desenvolvimento, CI, produção.
  • Redeploys que preservam segredos: os vínculos com segredos gerenciados sobrevivem a cada implantação a partir do fonte.
  • Rollback em uma única operação: migre o tráfego de volta para a revisão anterior saudável; sem reconstrução.
  • Domínio personalizado: a demo ao vivo é servida em agent.szemat.pro.
  • Sem hospedagem de pesos de modelo: evita LFS e qualquer nível de armazenamento no host; mantém a imagem pequena.
  • Cold start com escala a zero: a primeira requisição após uma janela de ociosidade paga um cold start de contêiner (importação de dependências, ingestão inicial da KB, carregamento do embedder).
  • Teto de instância única: --max-instances 1 limita a concorrência horizontal; escalar para fora exigiria mover as primitivas em processo para um armazenamento compartilhado (Redis, Postgres), fora de escopo para a demo.
  • Cabeamento de nuvem único: a implantação sem chaves precisa de um pool / provider de WIF e uma conta de serviço de implantação de privilégio mínimo configurados uma vez antes de o pipeline poder se autenticar.
  • Somente CPU: o contêiner não consegue hospedar um LLM local; a demo depende de um provedor externo para completudes. Por design (ver ADR-0002).
  • Um gate tag-guard e um workflow deploy se tornam parte do layout de CI do repositório.
  • As variáveis de repositório (WIF_PROVIDER, WIF_SERVICE_ACCOUNT) se tornam necessárias para a implantação ao vivo; contribuidores sem direitos de implantação trabalham localmente sem elas.
  • A mesma imagem é mantida implantável no Hugging Face Spaces como alvo secundário, então a referência de implantação documenta dois hosts.
  • Boa, porque a implantação é condicionada a uma matriz de CI verde e roda do lado do servidor, sem chaves via WIF.
  • Boa, porque um build a partir do fonte roda o Dockerfile do projeto como está e preserva os segredos gerenciados através dos redeploys.
  • Boa, porque o rollback é uma migração de tráfego em uma única operação para a revisão anterior.
  • Boa, porque --max-instances 1 torna a postura exigida de instância única uma configuração de primeira classe, e a escala a zero mantém o custo da demo perto de zero.
  • Boa, porque o serviço mapeia para um domínio personalizado próprio do projeto.
  • Ruim, porque a escala a zero significa um cold start na primeira requisição após a ociosidade.
  • Ruim, porque o escalonamento horizontal para fora precisa de um armazenamento compartilhado para as primitivas em processo.

Hugging Face Spaces, Docker SDK, CPU Basic gratuito

Seção intitulada “Hugging Face Spaces, Docker SDK, CPU Basic gratuito”
  • Boa, porque a mesma imagem roda sem alterações a custo genuinamente zero, sempre ativa, o que a torna um alvo de demo secundário ideal.
  • Boa, porque o domínio huggingface.co sinaliza “implementação de referência de IA” em uma olhada.
  • Ruim, porque sua borda não oferece pipeline de implantação condicionado e sem chaves, nem armazenamento de segredos gerenciado, nem controle de domínio personalizado; o caminho de implantação é um espelho com force-push em vez de um build condicionado.
  • Ruim, porque o front-matter YAML do card do Space exige uma troca do README raiz exclusiva de implantação.
  • Boa, porque os SDKs distribuem uma UI opinativa e hospedada.
  • Ruim, porque a superfície da demo diverge da superfície FastAPI que o projeto distribui; a coisa implantada deixa de ser a mesma coisa que a coisa construída localmente.
  • Boa, porque o mesmo Dockerfile implanta com uma mudança de variável de ambiente ($PORT).
  • Ruim, porque o sono por ociosidade de 15 minutos é mais agressivo que a escala a zero em um serviço já aquecido, e não há pipeline de implantação condicionado e sem chaves de fábrica.
  • Ruim, porque os níveis gratuitos históricos foram retirados (Fly.io outubro de 2024, Railway agosto de 2023) em favor de créditos de teste; o regime permanente não é de custo quase nulo.
  • Ruim, porque o timeout de função de 10 segundos mata qualquer stream de agente multi-turno assim que a ida e volta do LLM o excede.
  • Ruim, porque o stack Python + Chroma embarcado teria que ser reescrito contra o runtime do Workers.
  • Ruim, porque a UI atrela a demo ao Streamlit; a superfície FastAPI fica inacessível.
  • Ruim, porque os créditos de teste expiram; o regime permanente custa dinheiro.

Três primitivas por processo são distribuídas para fazer a demo degradar graciosamente sob carga em vez de expor erros upstream crus. Todas as três são acionadas por flags de habilitação para que fiquem desligadas por padrão para testes determinísticos e ligadas para o serviço ao vivo:

  • Rate limiter por sessão. Um limiter de janela deslizante por chave (por IP de cliente). Um chamador acima do limite recebe um HTTP 429 com um cabeçalho Retry-After em vez de um 502 cru. Ajustável via configurações de máximo de requisições e segundos da janela. Uma dependência foi deliberadamente não adicionada; o limiter em processo mantém a postura sem framework.
  • Cadeia de fallback de provedor. Um wrapper em torno do Protocol LLMClient: uma falha transitória da OpenAI (HTTP 429, 5xx ou uma falha de transporte pura) cascateia para a Anthropic antes que qualquer erro alcance o frontend. Um 4xx que não seja 429 é um erro genuíno de cliente e é relançado inalterado. Consistente com a abstração de fornecedor do ADR-0002 - o fallback é um wrapper em nível de Protocol, não uma mudança em nível de nó.
  • Cache de resposta com TTL curto. Um cache em processo limitado e de TTL curto, chaveado na tupla normalizada (entrada, localidade, modelo), de modo que os cliques de “carregar exemplo” da SPA sejam atendidos sem atingir o provedor.

Consequência de instância única. Todas as três primitivas são por processo, assim como o checkpointer HITL em memória (ver ADR-0001). O serviço, portanto, roda uma única instância por design (--max-instances 1); uma segunda instância não compartilharia o limiter, o cache ou o estado de thread pausada. Uma implantação multi-instância precisaria de um armazenamento compartilhado (Redis, Postgres), o que está fora de escopo para a demo. Isso é documentado na referência de implantação e no comentário do Dockerfile.

Embedder embutido. O embedder de fallback local embutido é BAAI/bge-small-en-v1.5, compatível com CPU no serviço de instância única; o embedder primário é Voyage voyage-3.5 quando uma chave da Voyage está configurada. Ver ADR-0004 para a decisão do embedder; a postura de implantação aqui não é afetada pela escolha do modelo.