Declaração do Problema
Precisa de fazer deploy de novas versões da sua aplicação sem interromper o serviço aos utilizadores, garantindo migrações de base de dados em segurança e a capacidade de fazer rollback rapidamente caso surjam problemas.
Comparação de Estratégias de Deployment
| Estratégia | Tempo de inatividade | Velocidade de rollback | Custo de recursos | Complexidade |
|---|
| Rolling Update | Zero | Rápido | Baixo | Baixa |
| Blue-Green | Zero | Instantâneo | 2x | Média |
| Canary | Zero | Rápido | Baixo-Médio | Elevada |
| Testes A/B | Zero | Rápido | Baixo-Médio | Elevada |
Estratégia 1: Rolling Update (Predefinição do Kubernetes)
Como Funciona
O Kubernetes substitui gradualmente os pods antigos por novos, garantindo que a disponibilidade mínima é mantida ao longo de todo o processo.
Time 0: [v1] [v1] [v1] [v1] ← All running v1
Time 1: [v1] [v1] [v1] [v2] ← One v2 starting
Time 2: [v1] [v1] [v2] [v2] ← Two v2 ready
Time 3: [v1] [v2] [v2] [v2] ← Three v2 ready
Time 4: [v2] [v2] [v2] [v2] ← All running v2
Configuração
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # No máximo, 1 pod pode ficar indisponível
maxSurge: 1 # No máximo, 1 pod extra durante o rollout
template:
spec:
containers:
- name: app
image: myapp:v2
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
Encerramento Elegante
Garanta que a sua aplicação trata SIGTERM corretamente:
// Exemplo em Node.js
const server = app.listen(8080);
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
// Fechar ligações à base de dados
db.close();
process.exit(0);
});
// Forçar fecho após 30 segundos
setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 30000);
});
# Configuração do Kubernetes
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
# Dar tempo ao load balancer para remover o pod da rotação
command: ["/bin/sh", "-c", "sleep 10"]
Estratégia 2: Deployment Blue-Green
Como Funciona
Execute dois ambientes idênticos. Encaminhe todo o tráfego para um (blue), faça deploy no outro (green) e, depois, faça a comutação.
┌──────────────────┐
│ Load Balancer │
└────────┬─────────┘
│
┌───────────┼───────────┐
│ │ │
┌──────▼──────┐ │ ┌──────▼──────┐
│ Blue │ │ │ Green │
│ (v1.0) │◀───┘ │ (v1.1) │
│ ACTIVE │ │ STANDBY │
└─────────────┘ └─────────────┘
Implementação no Kubernetes
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
labels:
app: myapp
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: app
image: myapp:v1.0
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
labels:
app: myapp
version: green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: app
image: myapp:v1.1
---
# service.yaml - comutar alterando o selector
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # Alterar para 'green' para comutar
ports:
- port: 80
targetPort: 8080
Script de Comutação
#!/bin/bash
CURRENT=$(kubectl get service myapp -o jsonpath='{.spec.selector.version}')
if [ "$CURRENT" = "blue" ]; then
NEW="green"
else
NEW="blue"
fi
echo "Switching from $CURRENT to $NEW"
kubectl patch service myapp -p "{\"spec\":{\"selector\":{\"version\":\"$NEW\"}}}"
echo "Traffic now routing to $NEW"
Estratégia 3: Deployment Canary
Como Funciona
Faça deploy da nova versão para um pequeno subconjunto de utilizadores primeiro e, depois, aumente gradualmente.
Phase 1: [v1][v1][v1][v1][v1][v1][v1][v1][v1][v2] ← 10% canary
Phase 2: [v1][v1][v1][v1][v1][v2][v2][v2][v2][v2] ← 50% canary
Phase 3: [v2][v2][v2][v2][v2][v2][v2][v2][v2][v2] ← 100% promoted
Usando Nginx Ingress
# Deployment principal (estável)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
spec:
replicas: 9
template:
spec:
containers:
- name: app
image: myapp:v1.0
---
# Deployment canary
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
spec:
replicas: 1
template:
spec:
containers:
- name: app
image: myapp:v1.1
---
# Ingress estável
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-stable
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-stable
port:
number: 80
---
# Ingress canary com peso
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 80
Canary com Istio
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: myapp
subset: canary
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp
spec:
host: myapp
subsets:
- name: stable
labels:
version: stable
- name: canary
labels:
version: canary
Migrações de Base de Dados Seguras
Princípios
- Compatível com versões anteriores: O código antigo tem de funcionar com o novo esquema
- Compatível com versões futuras: O código novo tem de funcionar com o esquema antigo durante o rollout
- Alterações pequenas e incrementais: Nunca migrações «big-bang»
Padrão: Migração Expand-Contract
Fase 1: Expand (Adicionar nova coluna)
-- Migração 1: Adicionar nova coluna, manter a antiga
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
// Código v2: Escrever em ambas, ler da antiga
const user = await db.query('SELECT first_name, last_name, full_name FROM users');
user.displayName = user.full_name || `${user.first_name} ${user.last_name}`;
// Ao guardar, escrever em ambas
await db.query(
'UPDATE users SET first_name = $1, last_name = $2, full_name = $3',
[firstName, lastName, fullName]
);
Fase 2: Migrar Dados
-- Migração 2: Backfill de dados
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name)
WHERE full_name IS NULL;
Fase 3: Contract (Remover colunas antigas)
// Código v3: Ler/escrever apenas na nova coluna
const user = await db.query('SELECT full_name FROM users');
-- Migração 3: Remover colunas antigas (depois de todos os pods estarem em v3)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;
Job de Migração no Kubernetes
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-v1-2
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 1; done']
containers:
- name: migrate
image: myapp:v1.2
command: ["npm", "run", "db:migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
Procedimentos de Rollback
Rollback no Kubernetes
# Ver histórico do deployment
kubectl rollout history deployment/myapp
# Rollback para a versão anterior
kubectl rollout undo deployment/myapp
# Rollback para uma revisão específica
kubectl rollout undo deployment/myapp --to-revision=2
# Verificar o estado do rollback
kubectl rollout status deployment/myapp
Rollback Instantâneo Blue-Green
# Basta voltar a comutar o selector do service
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'
Rollback da Base de Dados
-- Ter sempre uma migração de rollback pronta
-- down-migration.sql
ALTER TABLE users ADD COLUMN first_name VARCHAR(255);
ALTER TABLE users ADD COLUMN last_name VARCHAR(255);
UPDATE users SET
first_name = SPLIT_PART(full_name, ' ', 1),
last_name = SPLIT_PART(full_name, ' ', 2);
Boas Práticas de Health Checks
Implementar Três Endpoints
// /health - Liveness: O processo está vivo?
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// /ready - Readiness: O serviço consegue tratar pedidos?
app.get('/ready', async (req, res) => {
try {
await db.query('SELECT 1');
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
// /startup - Startup: O serviço terminou a inicialização?
let isStarted = false;
app.get('/startup', (req, res) => {
if (isStarted) {
res.status(200).json({ status: 'started' });
} else {
res.status(503).json({ status: 'starting' });
}
});
Configuração de Probes no Kubernetes
spec:
containers:
- name: app
startupProbe:
httpGet:
path: /startup
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3
A Mentalidade de Senior: É Sobre Estado, Não Código
«Maintenance Mode» não é aceitável em sistemas de nível senior. O verdadeiro desafio não é trocar código — é gerir estado.
Porque as Migrações de Base de Dados São a Parte Difícil
Regra: Alterações na base de dados têm de ser compatíveis com versões anteriores com o código antigo e o novo a correrem em simultâneo.
Cenário: Renomear uma Coluna
- Ingénuo:
ALTER TABLE users RENAME COLUMN name TO full_name; - Resultado: O código antigo (ainda em execução durante o deploy) consulta
name. A BD tem full_name. Falha.
Padrão Senior: Expand e Contract 1. Deploy 1 (Expand): Adicionar a coluna fullname. O código escreve em ambas name e fullname. Lê a partir de name. 2. Backfill: Executar um script para copiar name para fullname nas linhas antigas. 3. Deploy 2 (Switch): O código lê a partir de fullname. Escreve em ambas. 4. Deploy 3 (Contract): O código escreve apenas em full_name. Remover a coluna name.
Este padrão requer 3 deployments em vez de 1, mas garante zero pedidos perdidos.
Escolher a Estratégia Certa
| Cenário | Estratégia Recomendada |
|---|
| Atualização standard da app, com foco em custos | Rolling Update |
| Caminho crítico, necessidade de rollback instantâneo | Blue-Green |
| Alteração de alto risco, necessidade de rollout gradual | Canary |
| Experiência A/B com segmentação de utilizadores | Canary com encaminhamento baseado em header |
Dicas Pro:
- Rolling Update: TEM de ter uma
readinessProbe. Caso contrário, o K8s irá enviar tráfego para o novo pod antes de a app estar carregada, causando 502. - Blue-Green: Duplica o custo (precisa de 2x recursos), mas com comutação instantânea e rollback instantâneo.
- Canary: A opção mais segura — fazer deploy para 5% dos utilizadores, monitorizar métricas e, depois, aumentar para 20%, 50%, 100%.
Istio VirtualService para Canary
Ao usar Istio, pode fazer encaminhamento ponderado:
# Istio VirtualService
route:
- destination:
host: my-service
subset: v1
weight: 90
- destination:
host: my-service
subset: v2
weight: 10
Checklist de Zero-Downtime
- [ ] A aplicação trata SIGTERM de forma elegante
- [ ] O hook PreStop dá tempo para a atualização do load balancer
- [ ] A readiness probe verifica todas as dependências
- [ ] A estratégia de rolling update está configurada com valores apropriados
- [ ] As migrações de base de dados são compatíveis com versões anteriores
- [ ] O padrão Expand-Contract é usado para alterações de esquema
- [ ] Feature flags para nova funcionalidade
- [ ] Alertas de monitorização para a saúde do deployment
- [ ] Runbook para procedimentos de rollback
- [ ] Testes de carga realizados antes de releases principais
- [ ] Deployment durante janelas de baixo tráfego (se aplicável)
Em Resumo
Zero-downtime é, na sua maioria, sobre compatibilidade da base de dados. Se as alterações ao seu esquema quebrarem a versão anterior do código, nenhuma magia do Kubernetes o irá salvar. Expanda sempre primeiro e, depois, contraia.
Artigos Relacionados na Wiki