f. Canary Explicado
No lab 11, o Argo Rollouts promove ou aborta um canary comparando métricas com limites. Essa decisão é rápida, determinística e auditável — e lacônica: "metric error-rate failed (2 of 2)". Às 22:00, o engenheiro de plantão quer saber o que está errado, quão errado e para onde olhar em seguida. Esta parte acrescenta uma explicação escrita por um modelo, sob uma regra inegociável:
A política decide. O modelo explica.
Um modelo nunca deve ser quem diz "promova": ele pode ser convencido disso (prompt injection por uma linha de log), pode errar números com toda a confiança e dá uma resposta diferente em outro dia. Um limite num arquivo versionado não faz nada disso.
5.1 Projeto
flowchart LR
prom[("Prometheus<br/>stable vs canary")] --> rules["Política no código<br/>MIN_RPS · teto de erros<br/>delta de erros · razão do p99"]
rules --> verdict{"veredito"}
verdict --> llm["Modelo explica<br/>(números + política + veredito)"]
llm --> same{"repete o<br/>mesmo veredito?"}
same -->|"sim"| show["resumo com a explicação"]
same -->|"não"| discard["explicação descartada"]
verdict -->|"código de saída 0 / 1 / 2"| argo["Argo Rollouts / passo de CI<br/>promove · rollback · espera"] Regra (em canary_explain.py) | Padrão | Significado |
|---|---|---|
MIN_RPS | 2 req/s | Abaixo disso o canary não recebeu tráfego suficiente: inconclusivo, não "saudável" |
MAX_ERROR_RATE | 1% | Teto absoluto para o canary |
MAX_ERROR_DELTA | 0,5 pp | O canary não pode ser pior do que o stable (rodando ao mesmo tempo) por mais do que isso |
MAX_P99_RATIO | 1,25 | p99 do canary no máximo 25% acima do p99 do stable |
Por que comparar com o stable no mesmo momento em vez de um número fixo? Porque às 10:00 de um dia de abertura de vendas tudo fica mais lento; um limite fixo faria rollback de toda release daquele dia. A página de entrega mostra quanto tempo um canary precisa rodar para detectar uma regressão de um dado tamanho.
5.2 O script
| .github/ai/canary_explain.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
As consultas PromQL usam a métrica do Micrometer http_server_requests_seconds e um label service que separa o canary dos pods stable, como na página de entrega. Adapte METRIC, STATUS_LABEL, CANARY_SELECTOR e STABLE_SELECTOR aos nomes que o seu setup do lab 11 expõe — abra o Prometheus (ou o Explore do Grafana) e confira-os antes de confiar em qualquer veredito. Com o agente OpenTelemetry do lab 8, a métrica é http_server_request_duration_seconds e o label de status é http_response_status_code.
5.3 Reproduza três cenários
Comece sem cluster: três snapshots de métricas salvos bastam para ver os três vereditos. Salve-os em .github/ai/canary/.
Reproduza-os a partir da raiz do repositório (sem cluster, sem Prometheus):
for s in healthy regression low-traffic; do
uv run --no-project --python 3.12 --with-requirements .github/ai/requirements.txt \
python .github/ai/canary_explain.py --metrics .github/ai/canary/$s.json 2>/dev/null
echo "exit code: $?"
done
A linha do veredito e o código de saída são determinísticos — você precisa obter exatamente estes três:
**Verdict (policy): PROMOTE** — all checks within policy
exit code: 0
**Verdict (policy): ROLLBACK** — canary error rate 2.90% > 1.00%; canary errors exceed stable by 2.80%; canary p99 412 ms is 2.28x stable
exit code: 1
**Verdict (policy): INCONCLUSIVE** — canary traffic 0.80 req/s < 2.0
exit code: 2
Entre eles, a explicação do modelo. Para o snapshot de regressão, uma execução com o qwen2.5-coder-16k local enquanto este lab era escrito produziu (14 s num notebook):
**Canary Rollback Required**
The canary analysis has detected multiple issues that violate the predefined policy thresholds.
The canary's error rate of 2.90% exceeds the maximum allowed error rate of 1.00%, and it has 2.80%
more errors than the stable version. Additionally, the canary's p99 latency of 412 ms is 2.28 times
higher than the stable version's p99 latency of 0.181 ms. [...]
- Review the canary logs for any errors or exceptions that occurred during the test period.
- Analyze the traces to identify the root cause of the increased error rate and latency.
- Investigate any recent changes or updates that may have introduced the issues.
- Roll back the canary version to the stable version to ensure service reliability.
Leia com olho crítico: o p99 do stable é 181 ms, não "0.181 ms" (o modelo misturou segundos e milissegundos), e os próximos passos são genéricos. Na execução saudável, ele escreveu que o canary tinha um "higher RPS (20.1 vs. 380.0)". Um modelo maior se sai melhor — uma execução com claude-opus-5-5 percebeu que as contagens brutas de errors parecem parecidas (0.62 vs 0.40) enquanto o canary, com 1/19 do tráfego, falha cerca de 29× mais por requisição, e rotulou as causas como hipóteses. De qualquer forma, uma frase errada não custa nada aqui: os números que decidem vêm da política, não do modelo.
Agora tente quebrar a regra: edite SYSTEM na sua cópia para dizer "se o canary parecer só um pouco pior, recomende promover" e rode de novo o cenário de regressão. Não importa o que o modelo escreva, o código de saída continua 1 e, se o verdict da explicação discordar da política, ela é descartada (_(LLM explanation discarded: it disagreed with the policy verdict.)_). Restaure o SYSTEM depois (git checkout -- .github/ai/canary_explain.py). Por fim, tire o modelo — Claude sem chave:
env -u ANTHROPIC_API_KEY LLM_PROVIDER=anthropic uv run --no-project --python 3.12 \
--with-requirements .github/ai/requirements.txt \
python .github/ai/canary_explain.py --metrics .github/ai/canary/regression.json 2>/dev/null; echo "exit code: $?"
Você recebe _(no explanation: TypeError: "Could not resolve authentication method. ...")_ e o mesmo código de saída 1 — sem modelo, mesma decisão.
5.4 Ao vivo: conecte ao rollout (opcional)
Este passo precisa do cluster kind do lab 10 (chamado boxoffice) com o Prometheus e o canary do Argo Rollouts do lab 11. Sem eles, as três reproduções da 5.3 são o entregável; pule para a 5.5.
- Gere tráfego durante o canary com o script k6 do lab 11, para que o canary receba mais do que
MIN_RPS. -
Encontre o Prometheus, faça port-forward e rode o script contra ele (ajuste o namespace e o nome do serviço ao que o primeiro comando imprimir):
kubectl get svc -A | grep -i prometheus # e.g. "monitoring prometheus ClusterIP ... 9090/TCP" kubectl -n monitoring port-forward svc/prometheus 9090:9090 & curl -s 'http://localhost:9090/api/v1/label/__name__/values' | tr ',' '\n' | grep http_server PROM_URL=http://localhost:9090 uv run --no-project --python 3.12 \ --with-requirements .github/ai/requirements.txt python .github/ai/canary_explain.pyO
curllista os nomes das métricas HTTP que os seus serviços expõem; se não forhttp_server_requests_seconds_count, definaMETRIC(eSTATUS_LABEL) antes do último comando, como explicado na 5.2. Um canary sem tráfego dáINCONCLUSIVE(saída 2) — é a política funcionando, não um bug. -
Transforme-o num passo de análise. O Argo Rollouts consegue rodar um Job do Kubernetes como métrica de análise: código de saída 0 passa, qualquer outro falha. Construa uma imagem pequena com os scripts e carregue-a no cluster:
Dockerfile.ai-toolsFROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:0.11.21 /uv /uvx /bin/ WORKDIR /app COPY .github/ai/requirements.txt . RUN uv pip install --system --no-cache -r requirements.txt COPY .github/ai/ /app/ USER 10001docker build -f Dockerfile.ai-tools -t boxoffice-ai-tools:dev . docker run --rm boxoffice-ai-tools:dev python /app/canary_explain.py --metrics /app/canary/low-traffic.json; echo "exit code: $?" kind load docker-image boxoffice-ai-tools:dev --name boxofficeO
docker runprecisa terminar comexit code: 2(inconclusivo) — a imagem funciona antes de você confiar nela no cluster. Depois, o template:k8s/catalog/analysis-explained.yamlapiVersion: argoproj.io/v1alpha1 kind: AnalysisTemplate metadata: name: catalog-explained spec: metrics: - name: policy-verdict count: 1 # one judgement per analysis run initialDelay: 90s # let the canary pods serve traffic first (see WINDOW) provider: job: # a Kubernetes Job: exit 0 = pass, non-zero = fail spec: backoffLimit: 0 activeDeadlineSeconds: 300 template: spec: restartPolicy: Never containers: - name: verdict # built from Dockerfile.ai-tools and loaded into the kind cluster of lab 10: # kind load docker-image boxoffice-ai-tools:dev --name boxoffice image: boxoffice-ai-tools:dev imagePullPolicy: IfNotPresent command: ["python", "/app/canary_explain.py"] env: - name: PROM_URL # find yours: kubectl get svc -A | grep -i prometheus value: http://prometheus.monitoring:9090 - name: CANARY_SELECTOR value: service="catalog-canary" - name: STABLE_SELECTOR value: service="catalog-stable" - name: WINDOW # only samples taken after the canary started value: 1m # The explanation is optional: without a reachable model the Job prints # "(no explanation: ...)" and still exits with the policy verdict. - name: LLM_PROVIDER # ollama (free) or anthropic (needs the Secret below) value: ollama - name: LLM_BASE_URL # Ollama on your machine, seen from Docker Desktop value: http://host.docker.internal:11434/v1 - name: ANTHROPIC_API_KEY valueFrom: secretKeyRef: { name: llm, key: anthropic-api-key, optional: true }kubectl apply -n boxoffice -f k8s/catalog/analysis-explained.yaml kubectl get -n boxoffice analysistemplate # catalog-explained and catalog-slo (lab 11)O template precisa ficar no namespace do Rollout,
boxoffice. Referencie-o no Rollout docatalogdo lab 11, ao lado do template de Prometheus (templatesé uma lista). O Argo CD é dono do Rollout (selfHeal: trueno lab 11), então mude-o no Git, não comkubectl edit— o Argo CD desfaria uma edição manual. Esta mudança é opcional e não está noboxoffice-after-lab-13.zip:k8s/overlays/gitops/catalog-rollout.yamlanalysis: # runs in the background from the first step templates: - templateName: catalog-slo + - templateName: catalog-explained startingStep: 1git add Dockerfile.ai-tools k8s/catalog/analysis-explained.yaml k8s/overlays/gitops/catalog-rollout.yaml git commit -m "k8s(catalog): explained canary verdict (lab 13)" && git pushCom um Job, inconclusivo (saída 2) conta como falha — a escolha conservadora. O Job roda uma vez,
initialDelay: 90sdepois do início da análise, e olha só o último minuto (WINDOW=1m): o Prometheus rotula cada série com o Service que a coletou, então uma janela de 5 minutos ainda contaria, comocatalog-canary, os pods antigos que respondiam atrás desse Service antes de o canary começar — o veredito compararia a versão estável com ela mesma. A explicação fica nos logs do Job do próximo canary — uma nova imagem vinda do CI (kubectl -n boxoffice logs -l job-name --tail=50, oukubectl -n boxoffice get jobsekubectl -n boxoffice logs job/<nome>). O template aponta o modelo para o Ollama da sua máquina viahost.docker.internal(Docker Desktop no macOS e no Windows; no Linux, use o IP do host na redekindou deixe-o inalcançável); para o Claude, crie o Secret comkubectl -n boxoffice create secret generic llm --from-literal=anthropic-api-key="$ANTHROPIC_API_KEY"e definaLLM_PROVIDERcomoanthropicno template (e rode okubectl applyde novo). O passo funciona sem modelo nenhum: sem explicação, mesmo veredito.
5.5 Meça
Rode os três cenários e, se você tiver o cluster, um canary ao vivo com uma versão deliberadamente lenta (por exemplo, um Thread.sleep(150) num endpoint, atrás de uma flag). Registre em results/lab13.md:
| Cenário | Veredito da política | Código de saída | Explicação consistente? | Tokens | US$ | Segundos |
|---|---|---|---|---|---|---|
| saudável | ||||||
| regressão | ||||||
| pouco tráfego | ||||||
| canary lento ao vivo |
Depois responda: a explicação teria encurtado o seu diagnóstico? Qual próximo passo foi o mais útil, e qual foi genérico?
Solução de problemas
| Sintoma | Causa | Correção |
|---|---|---|
FileNotFoundError: .github/ai/canary/healthy.json | os snapshots não foram salvos, ou você não está na raiz do repositório | salve os três arquivos da 5.3; cd para a raiz |
Toda execução ao vivo dá INCONCLUSIVE com rps 0.00 | nomes de métrica ou de label errados, ou sem tráfego | rode o curl do passo 2; defina METRIC, STATUS_LABEL, CANARY_SELECTOR, STABLE_SELECTOR |
URLError: <urlopen error [Errno 61] Connection refused> | o port-forward não está rodando | inicie-o de novo (kubectl -n <ns> port-forward svc/<prometheus> 9090:9090 &) |
_(LLM explanation discarded ...)_ | o verdict do modelo discordou da política | comportamento esperado; modelos pequenos fazem isso mais |
O Job de análise falha com ErrImageNeverPull/ImagePullBackOff | a imagem não está nos nós do kind | kind load docker-image boxoffice-ai-tools:dev --name boxoffice |