Em 2012, o Amazon Prime Video sofreu um incidente durante um deploy que derrubou o serviço por 49 minutos. A causa era um simples bug introduzido na versão nova que não havia sido detectado nos testes — e o deploy havia substituído todas as instâncias de uma vez. O rollback demorou porque o processo de deploy era lento. Se o deploy tivesse sido incremental — enviando tráfego progressivamente para a nova versão enquanto monitorava erros — o impacto teria sido limitado a uma fração dos usuários por poucos minutos antes da detecção.
A estratégia de deploy determina o perfil de risco de uma entrega: quanta superfície de usuários é exposta ao novo código e por quanto tempo antes que problemas possam ser detectados e revertidos. Não existe estratégia universalmente correta — cada uma tem trade-offs que dependem do contexto: tipo de mudança, volume de tráfego, tolerância a risco, e capacidade de rollback.
Rolling deploy — o padrão default do Kubernetes
Rolling deploy é o comportamento padrão de um Kubernetes Deployment: substituir gradualmente pods da versão antiga por pods da nova versão, mantendo um número mínimo de pods disponíveis durante o processo.
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # criar até 2 pods além do desejado durante o update
maxUnavailable: 0 # nunca remover um pod antes de ter um novo pronto
Com maxUnavailable: 0 e maxSurge: 2, o processo é: criar 2 novos pods (versão nova) → esperar ficarem prontos → remover 2 pods antigos → repetir até substituir todos. Em nenhum momento o sistema fica abaixo de 10 pods disponíveis.
O problema do rolling deploy: durante a transição, ambas as versões estão em produção simultaneamente. Se a nova versão introduz uma mudança incompatível no formato de resposta de uma API, usuários terão respostas inconsistentes dependendo de qual pod serviu a request. Mudanças backward-incompatíveis exigem uma das outras estratégias.
Blue-green — switch instantâneo
Blue-green mantém dois ambientes idênticos: blue (produção atual) e green (nova versão). O deploy é atomicamente feito mudando o switch de tráfego de blue para green:
┌──────────────┐
┌────│ Load Balancer │────┐
│ └──────────────┘ │
100% →─┤ ├─→ 0%
│ │
┌────▼────┐ ┌───────▼─────┐
│ Blue │ │ Green │
│(v1.0) │ │ (v1.1) │
│ 10 pods │ │ 10 pods │
└─────────┘ └─────────────┘
(switch → green)
┌──────────┐ ┌─────────────┐
│ Blue │ │ Green │
│(v1.0) │ ←────│ (v1.1) │
│ mantido │ 0% traffic 100% traffic
└──────────┘ └─────────────┘
O rollback em blue-green é instantâneo: basta apontar o load balancer de volta para blue. Isso é único — outras estratégias não oferecem rollback instantâneo com versão anterior completamente preservada.
Em Kubernetes, blue-green pode ser implementado com dois Deployments e alterando o selector do Service:
# Deployment blue (produção atual)
apiVersion: apps/v1
kind: Deployment
metadata:
name: meu-app-blue
spec:
template:
metadata:
labels:
app: meu-app
version: blue
---
# Deployment green (nova versão)
apiVersion: apps/v1
kind: Deployment
metadata:
name: meu-app-green
spec:
template:
metadata:
labels:
app: meu-app
version: green
---
# Service — aponta para blue ou green
apiVersion: v1
kind: Service
metadata:
name: meu-app
spec:
selector:
app: meu-app
version: blue # ← mudar para "green" para o switch
O custo de blue-green é o dobro da infraestrutura durante o período de transição. Para sistemas grandes, isso pode ser proibitivo. Em cloud com auto-scaling, o custo é temporário — mas para databases ou sistemas stateful com custo fixo alto, blue-green pode não ser viável.
Canary deploy — exposição progressiva com dados
Canary deploy expõe a nova versão para uma fração do tráfego, monitorando métricas antes de expandir. O nome vem dos canários usados em minas de carvão — se o canário morre, há problema.
┌──────────────┐
│ Load Balancer│
└──────┬───────┘
│
90% ─────────┼────────── 10%
↓ │ ↓
┌─────────┐ │ ┌──────────┐
│Stable │ │ │ Canary │
│(v1.0) │ │ │ (v1.1) │
│ 9 pods │ │ │ 1 pod │
└─────────┘ │ └──────────┘
│
Monitorar:
- Error rate
- Latência p99
- Métricas de negócio
Em Kubernetes com Argo Rollouts (ou NGINX/Istio para controle de tráfego), o processo é automatizado com progressive delivery:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: meu-app
spec:
strategy:
canary:
steps:
- setWeight: 10 # 10% do tráfego para a nova versão
- pause: {duration: 10m} # esperar 10 minutos
- analysis: # verificar métricas automaticamente
templates:
- templateName: success-rate
- setWeight: 50 # 50% se análise passou
- pause: {duration: 20m}
- analysis:
templates:
- templateName: success-rate
- setWeight: 100 # 100% — promoção completa
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 5m
successCondition: result[0] >= 0.99 # 99% de sucesso
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{status!~"5..",app="meu-app",version="canary"}[5m]))
/
sum(rate(http_requests_total{app="meu-app",version="canary"}[5m]))
O Argo Rollouts reverte automaticamente se a análise falhar — sem intervenção humana. O canário detectou o problema antes de atingir 100% dos usuários.
Feature flags — deploy desacoplado de release
Feature flags (ver Conceito 04) são a estratégia ortogonal a todas as outras: você pode fazer um rolling deploy de código novo, mas a feature nova está desabilitada — o "deploy" e o "release" são eventos separados.
Esse desacoplamento resolve um problema fundamental: qualquer uma das estratégias acima muda a versão do código para todos os usuários ao mesmo tempo (ou progressivamente). Feature flags permitem rollout por segmento:
- Habilitar para 1% dos usuários por 24h → sem problemas → 10% → 100%
- Habilitar apenas para beta testers ou usuários premium
- Habilitar apenas em uma região geográfica
- Habilitar instantaneamente ou reverter sem nenhum deploy
A combinação de canary deploy + feature flags é o estado da arte: o canary deploy garante que o código novo não tem regressões técnicas (erros, latência), e o feature flag controla a exposição de negócio da nova funcionalidade.
Database migrations sem downtime — expand-contract
O maior desafio de deploy sem downtime é banco de dados: migrations que renomeiam colunas, mudam tipos, ou removem campos são backward-incompatíveis com a versão anterior da aplicação.
O padrão expand-contract (também chamado de parallel change) resolve isso em três fases:
Fase 1 — Expand (deploy 1): adicionar a nova coluna, manter a antiga. A nova versão escreve em ambas, lê da antiga.
-- Migration: adicionar coluna nova, manter antiga
ALTER TABLE users ADD COLUMN full_name VARCHAR(500);
-- Application code v2 (durante Expand)
-- Escreve em ambas:
UPDATE users SET name = $1, full_name = $2 WHERE id = $3
-- Lê da antiga (compatível com v1 ainda em pods durante rolling deploy):
SELECT name FROM users WHERE id = $1
Fase 2 — Migrate (script ou deploy 2): preencher a nova coluna com dados da antiga para todos os registros existentes. A nova versão agora lê da nova coluna.
-- Backfill em batches para não causar lock
DO $$
DECLARE
batch_size INT := 1000;
offset_val INT := 0;
BEGIN
LOOP
UPDATE users
SET full_name = name
WHERE id IN (
SELECT id FROM users
WHERE full_name IS NULL
LIMIT batch_size
OFFSET offset_val
);
EXIT WHEN NOT FOUND;
offset_val := offset_val + batch_size;
PERFORM pg_sleep(0.1); -- não exaurir I/O
END LOOP;
END $$;
Fase 3 — Contract (deploy 3): remover a coluna antiga após confirmar que nenhum pod da versão anterior está rodando.
-- Só fazer isso após confirmar que todos os pods são v3
ALTER TABLE users DROP COLUMN name;
O expand-contract transforma uma migration backward-incompatível em três mudanças backward-compatíveis. O custo é o tempo (3 deploys em vez de 1) e o espaço temporário de manter duas colunas. Para tabelas muito grandes, a fase de backfill pode levar horas — nesse caso, ferramentas como pg_repack ou Percona Online Schema Change ajudam a fazer a mudança sem locks prolongados.
Comparação de estratégias
| Estratégia | Velocidade de rollback | Custo de infra | Complexidade | Versões simultâneas |
|---|---|---|---|---|
| Rolling | Lento (novo deploy) | Baixo (+maxSurge temporário) | Baixa | Sim (durante transição) |
| Blue-green | Instantâneo (switch) | Alto (2x infra) | Média | Não (switch atômico) |
| Canary | Rápido (reduzir tráfego) | Baixo (poucos pods canário) | Alta | Sim (controlado) |
| Feature flags | Instantâneo (toggle) | Nenhum | Média (dívida de código) | Não (código único, comportamento variável) |
Para serviços de alta criticidade com SLA rigoroso: canary deploy automático com Argo Rollouts + feature flags para controle de release. Para serviços com banco de dados e mudanças de schema: expand-contract obrigatório, independente da estratégia de deploy do serviço. Para startups em estágio inicial: rolling deploy padrão do Kubernetes com testes sólidos é suficiente — canary e blue-green adicionam complexidade operacional que não compensa até ter volume de tráfego para detectar problemas estatisticamente. Para mudanças backward-incompatíveis de API: blue-green com versionamento de API é a única estratégia que evita versões simultâneas servindo respostas diferentes.
Decisões de engenharia
Perguntas de entrevista
Por que rolling deploy pode ser problemático para mudanças backward-incompatíveis, e como mitigar?
Durante um rolling deploy, há um período de transição em que ambas as versões do serviço estão rodando simultaneamente. Se um cliente faz duas requisições em sequência e cada uma vai para um pod diferente (uma v1 e uma v2), ele pode receber respostas inconsistentes — especialmente se a v2 mudou o formato do JSON, renomeou campos, ou alterou o contrato da API.
Exemplos de mudanças que quebram durante rolling: renomear um campo de resposta (user_id → userId), mudar o tipo de um campo (string → número), remover um campo que o cliente usa, ou mudar a semântica de um código de status HTTP. Clientes que fazem parse de respostas podem falhar dependendo de qual versão serviu.
As mitigações são: (1) backward compatibility por design — nunca remover ou renomear campos em uma única versão; introduza o novo campo, deprecie o antigo, remova apenas depois que todos os consumers migraram; (2) versionamento de API — /v1/users e /v2/users coexistem; migre consumers gradualmente; (3) blue-green deploy para mudanças incompatíveis — o switch atômico garante que todos os pods são da mesma versão ao mesmo tempo; (4) feature flags para desacoplar o deploy da mudança de comportamento.
Como o Argo Rollouts implementa canary deploy com análise automática, e o que acontece quando a análise falha?
O Argo Rollouts substitui o objeto Deployment do Kubernetes por um objeto Rollout, que adiciona estratégias de deploy avançadas. Para canary, você define uma sequência de passos: aumentar o peso do tráfego para o canary, pausar por um intervalo, executar uma análise automática, e repetir até 100%.
A análise é feita via AnalysisTemplate — um objeto que define métricas a verificar (Prometheus, Datadog, CloudWatch), intervalos de verificação, condições de sucesso, e limite de falhas. Por exemplo: a taxa de sucesso do canary deve ser >= 99% medida a cada 5 minutos, e pode falhar até 3 vezes antes de abortar. O Argo Rollouts consulta o Prometheus diretamente com a query configurada.
Quando a análise falha (taxa de sucesso abaixo do threshold, ou número de erros excede o limite), o Argo Rollouts automaticamente: interrompe o avanço do canary, reverte o peso do tráfego para 0% no canary, e marca o Rollout como degraded. Os pods da versão nova ficam pausados — o rollback para 100% na versão estável é instantâneo (o peso já voltou para 0%). Sem intervenção humana necessária para o rollback.
Explique o padrão expand-contract passo a passo para renomear uma coluna em uma tabela com 10M de registros sem downtime.
O problema: renomear users.name para users.full_name em uma tabela com 10M de registros. Um ALTER TABLE RENAME COLUMN direto causa lock na tabela durante a execução e é incompatível com a versão v1 da aplicação que ainda usa name.
Fase 1 — Expand (PR 1): Migration: ALTER TABLE users ADD COLUMN full_name VARCHAR(500) (sem NOT NULL, sem DEFAULT — adicionar coluna vazia é fast). Application v2: escreve em ambas as colunas (name e full_name) em toda operação de INSERT/UPDATE; continua lendo de name. Essa versão é compatível com v1 (que só conhece name) durante o rolling deploy. Deploy e aguardar que todos os pods sejam v2.
Fase 2 — Migrate: Script de backfill em batches de 1.000 registros: UPDATE users SET full_name = name WHERE full_name IS NULL LIMIT 1000, repetindo com sleep entre cada batch para não saturar I/O. Com 10M de registros e batches de 1.000 com 100ms de sleep, leva ~17 minutos. Application v3: lê de full_name, ainda escreve em ambas (compatível com v2 durante rolling).
Fase 3 — Contract (PR 3): Após confirmar que todos os pods são v3: migration ALTER TABLE users DROP COLUMN name. Application v4: remove referências à coluna antiga, escreve apenas em full_name. O lock do DROP COLUMN é muito mais rápido que o de RENAME porque não há transformação de dados.
Em que cenários blue-green é claramente superior ao canary, e quando o custo não se justifica?
Blue-green é claramente superior ao canary em três cenários: (1) mudanças backward-incompatíveis de API onde coexistência de versões durante canary causaria erros de consistência — blue-green troca de forma atômica; (2) requisito de rollback instantâneo contratual — "se algo der errado, voltar em 30 segundos" é realista com blue-green, não com canary que exige reduzir peso gradualmente; (3) volume de tráfego baixo onde 10% do tráfego para canary não é estatisticamente significativo — um serviço com 50 req/hora tem 5 req/hora no canary, insuficiente para detectar bugs de baixa taxa.
O custo de blue-green não se justifica quando: o serviço tem custo fixo alto (banco de dados gerenciado com preço de instância — duplicar não é opção), a infra é muito grande (2x de 1.000 pods por horas é custoso), ou quando a mudança é pequena e pode ser protegida por feature flags + testes sólidos sem custo de infra adicional. Para um deploy de bugfix de 3 linhas que passou em 100% do pipeline de testes, blue-green é overhead sem benefício proporcional.
Como a combinação de canary deploy + feature flags representa o estado da arte em deploy seguro, e como cada um resolve diferentes dimensões de risco?
Canary deploy e feature flags são ortogonais — cada um resolve uma dimensão diferente de risco, e a combinação cobre ambas.
Canary deploy controla a dimensão técnica: verifica que a nova versão do código não introduz regressões de performance (latência p99 aumentou?), confiabilidade (error rate aumentou?), ou recursos (memory leak?). O canary expõe 10% do tráfego real à nova versão e verifica métricas objetivas automaticamente — se algo quebrar tecnicamente, o rollback é automático antes de 100% dos usuários serem afetados. Canary valida que "o código funciona corretamente".
Feature flags controlam a dimensão de produto: verifica que a nova funcionalidade tem o comportamento de negócio esperado (usuários estão convertendo mais? tempo na tela aumentou? suporte recebeu mais tickets?). Feature flags permitem rollout por segmento (1% → 5% → 20% → 100%), targeting por perfil de usuário (beta testers primeiro), e rollback de negócio sem deploy de código. Feature flags validam que "a funcionalidade gera o valor esperado".
A combinação: o canary deploy garante que o código novo chega à produção sem regressões técnicas; o feature flag garante que a nova funcionalidade é exposta gradualmente e pode ser desabilitada instantaneamente se o comportamento de negócio for inesperado — sem precisar de um rollback de código (que leva minutos) ou de um novo canary deploy.
Exercícios práticos
Configure um Deployment com 5 réplicas usando maxUnavailable: 0 e maxSurge: 1. Implemente uma readiness probe que simula um tempo de warmup de 10 segundos (o endpoint /ready retorna 503 por 10 segundos após o start, depois retorna 200). Faça um rolling deploy para uma nova versão da imagem e verifique usando kubectl get pods -w que novos pods só recebem tráfego após passar na readiness probe, e que o número de pods disponíveis nunca cai abaixo de 5. Meça a latência das requisições durante o deploy para confirmar zero downtime.
Instale o Argo Rollouts em um cluster local. Converta um Deployment existente para um Rollout com estratégia canary: 10% → pause 5min → análise → 50% → pause 10min → análise → 100%. Crie um AnalysisTemplate que consulta o Prometheus (instale kube-prometheus-stack) verificando que a error rate do canary é menor que 1%. Simule uma falha: faça uma versão nova que retorna 50% de errors 500, e verifique que o Argo Rollouts reverte automaticamente para a versão estável sem intervenção manual. Em seguida, faça uma versão boa e verifique a promoção automática.
Critério: Uma versão com alta error rate é revertida automaticamente pelo Argo Rollouts antes de chegar a 50% do tráfego; uma versão saudável é promovida para 100% automaticamente após as análises passarem; o dashboard do Argo Rollouts mostra o histórico de análises com os valores de métrica coletados.Em um banco PostgreSQL com uma tabela users com coluna email (varchar), implemente a migration expand-contract completa para adicionar a coluna email_normalized (email em lowercase) sem downtime. Fase 1 (Expand): adicionar coluna email_normalized, atualizar a aplicação para escrever nas duas colunas, ler da antiga. Fase 2 (Migrate): script de backfill em batches de 500 registros com 50ms de sleep entre batches, depois atualizar aplicação para ler da nova. Fase 3 (Contract): remover coluna antiga. Meça o tempo de cada fase e confirme que a tabela nunca ficou locked por mais de 1 segundo.
email_normalized contém todos os emails em lowercase; nenhuma query na aplicação falhou durante o processo de migração; o backfill processou registros em batches sem causar lock da tabela por mais de 1 segundo.
Implemente blue-green deploy manual em Kubernetes: crie dois Deployments (app-blue com versão v1 e app-green com versão v2) com labels version: blue e version: green respectivamente. Configure um Service que seleciona apenas version: blue. Gere carga contínua no serviço (usando hey ou k6) enquanto executa o switch do selector do Service de blue para green. Meça a latência durante o switch e verifique que o número de erros 5xx é zero. Execute o rollback (switch de volta para blue) e confirme que é instantâneo.
Construa um pipeline completo de deploy seguro combinando GitHub Actions + Argo Rollouts + feature flags. O pipeline deve: (1) fazer deploy da nova imagem como canary com 10% do tráfego via Argo Rollouts; (2) aguardar a análise automática (AnalysisTemplate com Prometheus); (3) se a análise passar, promover para 100%; (4) se falhar, reverter automaticamente e criar um GitHub Issue com os valores de métrica que falharam. A feature nova no código deve ser controlada por um feature flag (Unleash ou equivalente) que começa desabilitado — habilitar o flag é um passo separado após o canary ser promovido.
Critério: O pipeline completo (push → canary → análise → promoção) roda de forma automatizada sem intervenção humana para o caso happy path; uma versão com erro é revertida e um GitHub Issue é criado com os valores das métricas que causaram a falha; a feature nova só fica visível para usuários após o flag ser habilitado explicitamente.Referências
- docs Argo Rollouts — Progressive Delivery
- article Martin Fowler — BlueGreenDeployment
- article Martin Fowler — Expand Contract
- article Google — Patterns for Scalable and Resilient Apps: Deployments
- book Jez Humble & David Farley — Continuous Delivery
- docs Flagger — Progressive Delivery for Kubernetes
- article Netflix TechBlog — Automated Canary Analysis at Netflix
- article GitHub Engineering — How we deploy safely at GitHub
- article Atlassian — Blue-green deployments vs canary releases
- docs Kubernetes — Deployment Strategies
- article Stripe — Migrating a database with zero downtime
- article PlanetScale — Online Schema Changes