Pular para conteúdo

0026 — Control-plane de deploy no central-backend

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-30

Contexto

O build off-host (0022) dispara o deploy por uma ponte SSH na VM de produção: o build-deploy.yml faz ssh …:65024 "deploy <uuid>" e a ponte — uma chave forced-command com allow-list de uuids — chama a API local do Coolify. A ponte existe porque a API do Coolify só aceita o IP da casa, e o runner do GitHub tem IP arbitrário.

O allow-list fica órfão em todo cutover blue-green: migrar um recurso cria um recurso Coolify novo (uuid novo) e deleta o velho → o uuid autorizado some e o novo nunca entra. Verificado em 2026-08-30: a ponte recusa os 19 recursos pull da frota inteira (refused: uuid nao autorizado), Java e Flutter. Nunca quebrou só porque todo deploy até hoje foi via API direta (do IP da casa), nunca pela ponte — que só seria exercida numa release-por-tag autônoma. A causa-raiz é dupla: o allow-list guarda uuid (o que muda no cutover) como chave, e é mantido à mão num script fora de qualquer catálogo. E o central-backend — que já é o catálogo de apps e já fala com a API do Coolify (CoolifyClient) — não participa do deploy.

Decisão

O deploy da casa é orquestrado pelo central-backend (control-plane). O CI dispara por API M2M; a ponte SSH, o forced-command e o allow-list são removidos.

  • Contrato. O build-deploy.yml faz POST /api/ci/deploy no central com {app_id, target, image?}, autenticado por token dedicado CENTRAL_DEPLOY_TOKEN (validado por um filtro próprio em /api/ci/**, no molde do SetupAuthFilter — não o AdminAuthFilter, que é tudo-ou-nada e super-privilegiaria o token). O central resolve (app_id, target[, instance]) → recurso(s) Coolify e dispara o deploy de cada um pela API do Coolify. Resposta assíncrona (queued + deployment_uuid); o Coolify faz o rolling.

Correção 2026-09-12: quem chama o /api/ci/deploy é o job deploy do pipeline.yml (0027); o build-deploy.yml saiu do kit. - Mapa auto-curável. Uma tabela app_deploy_targets (app_id, target, instance, registry_slug, coolify_uuid, fqdn) guarda o vínculo; um reconcile casa os recursos do Coolify pelo slug de imagem (registry_slug == docker_registry_image_name do recurso == slug do app, 0024; detalhe na deploy.md), chave estável no cutover, nunca por uuid — nem pelo nome do recurso (o nome canônico <slug>-[<instance>-]<target>-pull é derivado/legível, não a chave; verificado empiricamente na Fase 2, 2026-09-03). Blue-green futuro cria uuid novo → o reconcile re-casa pelo slug de imagem. Fim da edição à mão.

Correção 2026-09-05: a âncora do reconcile é o slug do artefato que o recurso consome — a imagem, ou o slug do repo git quando o Coolify builda do git (stack compose) —, e o target inclui compose. Contrato e reconcile em deploy. - Central se deploya pelo próprio /api/ci/deploy — auto-referência É o design. O job deploy do pipeline.yml do central-backend é byte-idêntico ao de qualquer app: bate no endpoint público do próprio central (Bearer CENTRAL_DEPLOY_TOKEN), que relaya à API do Coolify localmente (o central roda dentro da casa → alcança o IP restrito). Seguro: o central atual está no ar no disparo, e a chamada ao Coolify enfileira e retorna antes do swap (rolling health-gated — se a nova falha, a anterior segue no ar). Break-glass (a única exceção): central fora do ar não se autodeploya → deploy manual pela UI/API do Coolify (de dentro da casa), no runbook (infraestrutura — deploy).

Correção 2026-08-31: o central se deploya pelo próprio /api/ci/deploy. O desenho original ("direto na API do Coolify, sem auto-referência") é inexequível do runner GitHub, cujo IP a API do Coolify não aceita (0027). - Na 0022: a topologia "dispara o deploy via ponte SSH" é substituída por este webhook no central.

O detalhe operável — corpo do endpoint, esquema da tabela, os 3 formatos de recurso, a rotina de reconcile, a migração — vive na página infraestrutura — deploy; a norma durável, na §9 da constituição (§1.9, um dono por fato).

Alternativas descartadas

  • Manter a ponte + auto-whitelist no cutover. Faria cada blue-green adicionar o uuid novo ao allow-list e remover o velho. Resolve o sintoma, mas mantém dois segredos (chave SSH na VM + allow-list), um script fora de catálogo, e não dá visibilidade do que está deployado. Preterido — trata o sintoma, não a raiz.
  • Resolução por domínio pura, sem tabela. O central já resolve uuid por fqdn == production_url (CoolifyClient.resolveAppUuid). Mas isso quebra no canary (2 recursos no mesmo domínio) e no integrador (6 recursos, N domínios), não modela target, e a UI (Fase 3) teria que bater no Coolify a cada render, sem estado próprio. Preterido — a dimensão de deploy precisa ser persistida.
  • Coolify auto-deploy no push da imagem. O Coolify redeployaria sozinho ao detectar imagem nova no registry. Menos controle do momento e sem orquestração/auditoria centralizada. Preterido — o central se deploya pelo próprio /api/ci/deploy (ver Decisão); a única exceção é o break-glass (central fora do ar → deploy manual pela UI/API do Coolify).

Consequências

  • app_deploy_targets (migration V10 no central-backend). A tabela apps (11 linhas, identidade) ganha uma dimensão de deploy (19 linhas — os apps expandidos pelos recursos: 1:1, canary 1:2, integrador 1:6). Semeada com os uuids atuais + mantida pelo reconcile.
  • Migração big-bang. Os ~18 build-deploy.yml trocam o step ssh … por curl POST ao central e o secret DEPLOY_SSH_KEY por CENTRAL_DEPLOY_TOKEN de uma vez; ao fim, a ponte + forced-command + allow-list são removidos. Sem regressão: a ponte já está morta (allow-list órfão) e tudo deploya via API.
  • Escopo faseado. Esta ADR é a Fase 1 (documentação). A Fase 2 (central-backend: V10, reconcile, CoolifyClient.deploy(), o endpoint /api/ci/deploy + filtro, a migração da frota) e a Fase 3 (central-ui: dashboard apps × recursos × estado × tipo × uuid, reusando /api/admin/monitor) têm decisão/spec próprias nos seus repos.
  • Encaixe constitucional. M2M <DESTINO>_API_TOKEN (seguranca); contrato para caller não-Java = OpenAPI publicado (0020); o control-plane vive no central (§7); a norma durável na §9.
  • Auditoria. Todo deploy disparado deixa rastro (quem/qual token, app, target, uuids, resultado) — ação privilegiada exige rastro (seguranca).