Canary release com Gateway API: o Flagger desacopla o rollout do mesh que você escolheu
Canary release automatizado sempre teve o mesmo problema de fundo: a lógica de “desloca 10% do tráfego, observa métricas, avança ou volta atrás” fica amarrada à ferramenta que faz o roteamento. Com Istio puro, isso é um VirtualService com pesos manuais. Com Linkerd, era TrafficSplit — hoje descontinuado. Trocar de mesh ou de ingress controller significa reescrever a automação do zero.
O Flagger — subprojeto do Flux, hoje Graduated no CNCF — resolve isso faz tempo com adapters por provider (Istio, Linkerd, Contour, NGINX, cada um com sua CRD de roteamento). O que mudou é que a Gateway API virou provider de primeira classe: gatewayapi:v1. Na prática, isso quer dizer uma coisa concreta: você descreve a intenção do canary numa única CRD do Flagger, e ele gera o HTTPRoute sozinho — pros detalhes de implementação, não importa se por baixo tem Istio, Cilium, Envoy Gateway ou outro GatewayClass compatível.
Este é um guia de padrão de referência, baseado no tutorial oficial do Flagger. Adapte os nomes de métrica e os valores de threshold à sua realidade antes de levar pra produção.
Pré-requisito: Gateway API instalado, Flagger apontando pra ele
Você precisa dos CRDs da Gateway API (HTTPRoute e companhia) e de uma implementação que os satisfaça — qualquer GatewayClass válido serve, desde Istio até Cilium (se você já configurou o Cilium 1.20 com Gateway API, como vimos aqui, esse mesmo Gateway serve de base).
kubectl apply --server-side -k "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.4.0"
O Flagger entra configurado pra esse provider:
helm upgrade -i flagger flagger/flagger \
--namespace flagger-system \
--set meshProvider=gatewayapi:v1 \
--set metricsServer=http://prometheus.istio-system:9090
E você precisa de um Gateway já criado, com um listener liberando as rotas do seu domínio — o Flagger não cria o Gateway, só o HTTPRoute que se anexa a ele.
O recurso Canary: a única coisa que você escreve
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: podinfo
progressDeadlineSeconds: 60
service:
port: 9898
targetPort: 9898
hosts:
- www.example.com
gatewayRefs:
- name: gateway
namespace: istio-ingress
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: error-rate
templateRef:
name: error-rate
namespace: flagger-system
thresholdRange:
max: 1
interval: 1m
- name: latency
templateRef:
name: latency
namespace: flagger-system
thresholdRange:
max: 0.5
interval: 30s
Isso é tudo que você precisa declarar. Quando o Flagger detecta uma mudança real no targetRef (imagem, env, ConfigMap ou Secret montado), ele cria, sozinho, um segundo deployment (podinfo-primary), os Services de canary e primary, e o HTTPRoute que distribui tráfego entre os dois. Você nunca escreve esse HTTPRoute — se você criar um manualmente apontando pro mesmo host, ele entra em conflito com o que o Flagger gerencia.
O rollout avança em stepWeight (aqui, 10%) a cada interval (1 minuto), até maxWeight (50%), checando as métricas a cada avanço. Se o error-rate passar de 1% ou a latência p99 passar de 500ms, o Flagger não promove — ele registra uma falha. Depois de threshold falhas consecutivas (5, no exemplo), o peso volta pra zero, o canary escala pra zero réplicas e o rollout é marcado como Failed. Sem intervenção manual, sem alerta que ninguém viu a tempo.
As métricas acima (error-rate, latency) não existem por padrão — são MetricTemplates que você declara com PromQL, apontando pro Prometheus da sua malha. O exemplo oficial usa queries de istio_requests_total e istio_request_duration_milliseconds_bucket — se seu provider não é Istio, a query muda, mas o resto da mecânica (threshold, rollback) é igual.
O que isso NÃO cobre de fábrica
Três capacidades aparecem como extensão do padrão básico de peso — e cada uma tem uma dependência que vale checar antes de assumir que “funciona”:
- A/B testing por header ou cookie (em vez de peso fixo, trocar
stepWeight/maxWeightpormatchcomheaderseiterations) — funciona com qualquerHTTPRoute, porque matching por header é parte do core da Gateway API. - Session affinity (
analysis.sessionAffinity, pra garantir que o mesmo usuário continue na mesma versão durante o teste) — exige que a implementação suporte o filtroResponseHeaderModifier. Nem todo GatewayClass suporta. - Traffic mirroring (
analysis.mirror: true, copia a requisição real pro canary sem expor a resposta dele ao usuário) — exige suporte ao filtroRequestMirror.
Esses filtros fazem parte da Gateway API como especificação, mas cada implementação decide o que implementa e em qual nível de conformidade (core, extended, implementation-specific). Antes de montar a automação em torno de session affinity ou mirroring, confere a tabela de conformidade do seu provider especificamente — “suporta Gateway API” não é binário.
Quando vale escolher esse caminho
Se você já está comprometido com um único mesh e não pretende trocar, o adapter nativo dele (VirtualService do Istio, por exemplo) te dá acesso a recursos específicos daquele mesh que a Gateway API ainda não padronizou. O ganho de usar gatewayapi:v1 como provider do Flagger aparece em dois cenários concretos: você opera múltiplos clusters com implementações de Gateway diferentes e quer uma única definição de canary que funcione em todos; ou você está migrando de mesh e não quer reescrever a automação de progressive delivery no meio do caminho.
Se seu fluxo de deploy já passa pelo Argo CD — como vimos no post sobre ApplicationSet multi-cluster — o Canary do Flagger entra como mais um manifesto do Git, sincronizado normalmente. Progressive delivery e GitOps não competem: o Argo CD aplica o Canary, o Flagger toma as decisões de tráfego e rollback em cima dele.