Canary deploy com GitHub Actions e Docker: métricas, rollout e rollback automático
Por John Pires · 08 de março de 2026

Canary deploy com GitHub Actions e Docker: métricas, rollout e rollback automático
Contexto e limitações da abordagem convencional
Por que deploys tradicionais falham
Deploys tradicionais do tipo "deploy tudo de uma vez" expõem todo o tráfego a mudanças sem testá-las em produção. O resultado são picos de erro observáveis apenas após o deploy, maior tempo de recuperação e impacto direto nos usuários. Para times com 1–3 anos de experiência, isso costuma traduzir-se em deploys agendados por medo, rollback manuais e longos ciclos de feedback.
Limitações de ferramentas "out-of-the-box"
Plataformas como Kubernetes e Docker resolvem empacotamento e orquestração, mas não entregam um pipeline completo de canary com métricas e rollback automático. Ingress controllers (Nginx, Traefik) suportam divisão de tráfego básica; service mesh (Istio) dá mais controle, mas exige complexidade operacional. GitHub Actions pode orquestrar tudo, mas o workflow precisa ser projetado para controlar pesos, consultar métricas e executar rollback sem intervenção humana.
Consequências práticas
Sem automatizar métricas e rollback, você terá: deploys manuais, incidentes mais longos, alto custo de interrupção e menos confiança para tentar mudanças frequentes. A estratégia canary reduz isso, mas só se o rollout for controlado por métricas relevantes e rollback automático.
Deep dive na implementação interna
Arquitetura de componentes
Arquitetura prática proposta: GitHub Actions para CI/CD; Docker para build/push; Kubernetes para execução; Nginx Ingress Controller para split de tráfego por peso; Prometheus para métricas (erros e latência). Um script de rollout (bash/posix) faz a orquestração: atualiza imagem do canary, ajusta weight na ingress, consulta Prometheus e decide avançar ou rollback.
Fluxo de dados e métricas
Fluxo padrão:
- Commit -> build Docker -> push registry
- GitHub Action atualiza deployment canary image
- Script aplica peso inicial (ex: 10%) no ingress canary
- Loop: após cada step, consulta Prometheus para erroRate e p95Latency
- Se métricas dentro do SLA, aumenta peso; se ultrapassa threshold, rollback automático
Erros comuns e armadilhas
Erros práticos observados em implementação:
- Consultar métricas com queries erradas (por ex. não filtrar por job/environment).
- Atualizar peso no ingress enquanto o controller reverte a alteração por configuração centralizada.
- Esquecer de capturar a imagem anterior do stable antes de promover o canary — impede rollback limpo.
- Timeouts curtos na espera entre passos; algumas métricas demoram a convergir em produção.
Dica: sempre capture o estado atual (imagem, peso do ingress) antes de qualquer mudança. Scripts idempotentes reduzem surpresas.
Solução otimizada com código
Mostrarei código funcional e específico para um pipeline: Dockerfile para aplicação, manifests Kubernetes com Ingress Nginx canary, workflow GitHub Actions que invoca um script de rollout, e o script de rollout que implementa a lógica de métricas e rollback.
1) Dockerfile: imagem de produção
Contexto: Dockerfile para uma API Python (uvicorn/fastapi) com foco em imagem leve e variável de runtime. Esse exemplo é real e suficiente para o pipeline demonstrado.
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
COPY pyproject.toml poetry.lock ./
RUN apt-get update && apt-get install -y gcc libpq-dev --no-install-recommends \
&& pip install --upgrade pip poetry \
&& poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi \
&& apt-get remove -y gcc libpq-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
O que faz: monta imagem de produção com dependências via poetry. O resultado: imagem pronta para ser publicada no registry e usada em deployments Kubernetes.
2) Manifests Kubernetes (stable + canary + ingress)
Contexto: dois Deployments (stable e canary) e duas Ingress resources: a principal e a canary com anotações do Nginx Ingress Controller. Esse padrão permite controlar peso via anotações.
# k8s/myapp-stable.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
labels:
app: myapp
track: stable
spec:
replicas: 3
selector:
matchLabels:
app: myapp
track: stable
template:
metadata:
labels:
app: myapp
track: stable
spec:
containers:
- name: myapp
image: registry.example.com/myapp:stable-0001
ports:
- containerPort: 8080
---
# k8s/myapp-canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
labels:
app: myapp
track: canary
spec:
replicas: 1
selector:
matchLabels:
app: myapp
track: canary
template:
metadata:
labels:
app: myapp
track: canary
spec:
containers:
- name: myapp
image: registry.example.com/myapp:canary-temp
ports:
- containerPort: 8080
---
# k8s/ingress.yaml (nginx)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80
---
# k8s/ingress-canary.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress-canary
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "0"
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service-canary
port:
number: 80
O que faz: cria as duas rotas. A ingress-canary começa com weight 0; o script de rollout atualiza essa anotação para controlar tráfego. Serviços myapp-service e myapp-service-canary apontam para os respectivos deployments.
3) Workflow GitHub Actions: build, push e acionamento do rollout
Contexto: workflow que constrói a imagem, envia para registro e executa o script de rollout no runner que possui acesso kubectl (kubeconfig via secret). O workflow passa variáveis para o script: image tag, prometheus endpoint, thresholds.
# .github/workflows/canary-deploy.yml
name: Canary Deploy
on:
push:
branches: [ main ]
jobs:
build-and-canary:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push image
uses: docker/build-push-action@v4
with:
push: true
tags: registry.example.com/myapp:${{ github.sha }}
file: ./Dockerfile
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: '1.27.0'
- name: Configure kubeconfig
run: |
echo "${{ secrets.KUBECONFIG }}" > $HOME/.kube/config
chmod 600 $HOME/.kube/config
- name: Run canary rollout
env:
IMAGE: registry.example.com/myapp:${{ github.sha }}
PROM_URL: ${{ secrets.PROM_URL }}
PROM_QUERY: 'sum(rate(http_requests_total{job="myapp",code!~"2.."}[1m])) / sum(rate(http_requests_total{job="myapp"}[1m]))'
ERROR_THRESHOLD: '0.03' # 3% error rate
run: |
chmod +x ./deploy/canary-rollout.sh
./deploy/canary-rollout.sh
O que faz: constrói e publica imagem. Em seguida, executa o script de rollout localmente no runner (que carrega kubeconfig via secret). O runner precisa ter permissão para alterar ingress e deployments.
4) Script de rollout e rollback (bash)
Contexto: script que aplica imagem ao canary, itera pesos e consulta Prometheus. Em caso de violação do threshold, faz rollback: retorna peso a 0 e restaura imagem estável. Ele é idempotente e registra ações.
# deploy/canary-rollout.sh
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE=default
STABLE_DEPLOY=myapp-stable
CANARY_DEPLOY=myapp-canary
INGRESS=myapp-ingress-canary
IMAGE=${IMAGE:?}
PROM_URL=${PROM_URL:?}
PROM_QUERY=${PROM_QUERY:?}
ERROR_THRESHOLD=${ERROR_THRESHOLD:-0.03}
STEP_WEIGHTS=(10 25 50 75 100)
SLEEP_SECS=30
log(){ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
# Capture current stable image to allow rollback
prev_stable_image=$(kubectl -n $NAMESPACE get deploy $STABLE_DEPLOY -o jsonpath='{.spec.template.spec.containers[0].image}')
log "Stable image: $prev_stable_image"
# Update canary image
kubectl -n $NAMESPACE set image deployment/$CANARY_DEPLOY ${CANARY_DEPLOY}= $IMAGE || kubectl -n $NAMESPACE set image deployment/$CANARY_DEPLOY myapp=$IMAGE
log "Set canary image to $IMAGE"
# Ensure ingress weight starts at 0
kubectl -n $NAMESPACE annotate ingress $INGRESS nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
for w in "${STEP_WEIGHTS[@]}"; do
log "Setting canary weight to $w%"
kubectl -n $NAMESPACE annotate ingress $INGRESS nginx.ingress.kubernetes.io/canary-weight="$w" --overwrite
log "Sleeping $SLEEP_SECS seconds to let metrics settle"
sleep $SLEEP_SECS
# Query Prometheus for error rate
prom_result=$(curl -sG --data-urlencode "query=${PROM_QUERY}" "${PROM_URL}/api/v1/query")
# Extract value from JSON safely (requires jq)
if command -v jq >/dev/null 2>&1; then
value=$(echo "$prom_result" | jq -r '.data.result[0].value[1] // "0"')
else
# crude fallback
value=$(echo "$prom_result" | sed -n 's/.*\["[0-9]*",\"\([0-9.eE+-]*\)\"\].*/\1/p' || echo 0)
fi
value=${value:-0}
log "Observed error rate: $value (threshold $ERROR_THRESHOLD)"
cmp=$(awk -v a="$value" -v b="$ERROR_THRESHOLD" 'BEGIN{print (a > b)}')
if [ "$cmp" -eq 1 ]; then
log "Error threshold breached. Rolling back."
# rollback: reset weight and restore stable image
kubectl -n $NAMESPACE annotate ingress $INGRESS nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
kubectl -n $NAMESPACE set image deployment/$STABLE_DEPLOY myapp=$prev_stable_image --record || true
log "Rollback executed: restored stable image $prev_stable_image"
exit 1
fi
log "Step OK. Continuing to next weight."
done
log "Canary promoted: copying image from canary to stable"
kubectl -n $NAMESPACE set image deployment/$STABLE_DEPLOY myapp=$IMAGE --record
kubectl -n $NAMESPACE annotate ingress $INGRESS nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
log "Promotion complete"
O que faz: o script é o coração da solução. Ele aplica imagem no canary, manipula o peso do ingress, consulta Prometheus e decide promover ou rollback. Resultado: deploy seguro e automático.
Análise de complexidade e trade-offs
Complexidade operacional
Introduzir canary com métricas adiciona componentes: Prometheus, permissões kubectl no runner e scripts bem testados. A manutenção inclui: validar queries Prometheus, monitorar falsos positivos/negativos e garantir que o ingress controller respeite anotações. Esse custo paga-se pela redução de risco em produção e menor MTTR.
Custo e latência de deploy
Trade-offs:
- Latência do deploy aumenta: rollouts por peso precisam de tempo para métricas estabilizarem.
- Custo de infraestrutura: replica sets extras e armazenamento para métricas.
- Benefício: blast radius reduzido e confiança para liberar mudanças com maior frequência.
Observabilidade e SLOs
Para decisões automáticas, defina SLOs claros (ex: erro < 3% e p95 < 200ms). As queries Prometheus precisam refletir esses SLOs e ser testadas offline antes do rollout. Erros na query levam a rollbacks indevidos ou promoções ruins.
Quando NAO usar essa abordagem
Aplicações stateful ou com migrações de schema
Quando alterações involvem mudanças de schema, stateful sets ou contratos de banco de dados que não suportam versões simultâneas, canary sem estratégia de compatibilidade pode quebrar dados. Em migrações com incompatibilidade, prefira deploys controlados com feature flags e migração out-of-band.
Ambientes sem suporte a split de tráfego
Se o ambiente não permitir dividir tráfego por peso (por exemplo, infra legada sem ingress/controller que suporte canary), a abordagem falha. Usar blue/green sem split ou feature flags pode ser alternativa mais prática.
Equipes sem cultura de observabilidade
Se o time não tem métricas confiáveis ou falta disciplina em criar dashboards e alertas, automatizar rollbacks pode causar mais dano (rollbacks injustificados) do que benefício. Primeiro, invista em métricas de qualidade e testes de carga que simulem canaries.
Checklist de implantação e erros comuns
Checklist prático para rodar em produção
Antes de ativar o pipeline canary, validar:
- Prometheus coleta métricas corretas e a query devolve valor esperado;
- Kubeconfig no runner tem RBAC granular (apenas namespaces/recursos necessários);
- Ingress controller respeita anotações e atualiza weights rapidamente;
- Scripts testados em staging com imagens artificiais e flapping controlado;
- Alerting configurado para rollback humano quando necessário (Backup plan).
Armadilhas operacionais
Principais armadilhas e como evitá-las:
- Sem coleta de logs correlacionados: inclua tracing básico para identificar diferenças entre stable e canary;
- Usar thresholds rígidos demais: prefira thresholds com buffers (ex: 3% +- 1%) e escalonamento de ações;
- Confundir métricas rollout-driven com ruído: agregue por 1-5 minutos antes de decidir.
Dica: inclua um modo dry-run para o script de rollout que apenas emite comandos kubectl sem executá-los. Teste isso em staging antes de ativar no main.
Conclusão prática
Resumo executivo
Canary com GitHub Actions + Docker + Kubernetes + Prometheus é uma solução pragmática quando você precisa reduzir risco em produção. A combinação de build/push no Actions, manipulação de ingress-weight via kubectl e decisão baseada em métricas cria um pipeline que promove confiança e reduz MTTR.
Próximos passos recomendados
Implante em staging, monte um conjunto de testes de carga que simule 10%-100% tráfego e ajuste thresholds. Automatize observability: dashboards para erro_rate, latência p95 e saturação de recursos. Evolua para service mesh se precisar de routing mais sofisticado (header-based splits, retries).
Implementar canary com rollback automático exige disciplina: teste queries Prometheus, capture estado antes de mudanças e mantenha scripts idempotentes. Com esses cuidados, o processo reduz risco e acelera entregas sem sacrificar estabilidade.
O repositório de exemplos citado neste artigo contém os manifests e scripts prontos para copiar e adaptar ao seu cluster. Ajuste nomes de serviço, queries e thresholds antes de rodar em produção.
Boa prática final: automatize também o observability checklist (prometheus uptime, alertmanager config) para impedir execuções sem coleta mínima de métricas.
Assuntos: Docker, github-actions, Kubernetes, prometheus, canary-deploy, ci-cd
Leia também
Canary deploy com GitHub Actions em Kubernetes usando Helm e Flagger
Guia prático para implementar canary deploy com GitHub Actions, Helm e Flagger: padrões de pipeline, exemplos de Canary CRD e AnalysisTemplate, integrações…
Docker Buildx + GitHub Actions: publicar imagens multi-arch (ARM/x86) corretamente
Guia prático para publicar imagens Docker multi-arch (arm64/x86) usando Docker Buildx e GitHub Actions. Mostro o jeito errado, o certo, workflows, debug e…
Escanear imagens Docker no CI com Trivy e GitHub Actions — falhe por CVSS
Aprenda a escanear imagens Docker no CI com Trivy e GitHub Actions e a fazer o pipeline falhar quando uma vulnerabilidade atingir um limiar CVSS. Passo a…
GitHub Actions: build, testar em container e publicar imagem no GHCR
Pipeline prática: build de imagem Docker, executar testes no container carregado e publicar no GHCR usando GitHub Actions. Código e workflow para iniciar e…
Validando assinaturas de imagens Docker no Kubernetes com cosign e OPA Gatekeeper
Guia prático para validar assinaturas de imagens Docker no Kubernetes usando cosign e OPA Gatekeeper: padrões, exemplos com CI (GitHub Actions)…
Testcontainers e GitHub Actions: testar imagens Docker na CI corretamente
Guia prático de como rodar testes de integração com imagens Docker usando Testcontainers no GitHub Actions: padrões certos e anti-patterns, workflows…