Destino Dev

Blog

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
Esse fluxo minimiza blast radius e automatiza decisões.

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.
Corrigir esses pontos garante rollouts mais previsíveis.

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.
Escolha dos thresholds e tempos de espera impacta diretamente o balanço entre segurança e velocidade.

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:

  1. Prometheus coleta métricas corretas e a query devolve valor esperado;
  2. Kubeconfig no runner tem RBAC granular (apenas namespaces/recursos necessários);
  3. Ingress controller respeita anotações e atualiza weights rapidamente;
  4. Scripts testados em staging com imagens artificiais e flapping controlado;
  5. 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