Um sistema de software não termina quando o último deploy é feito. Ele começa. O trabalho de construção — arquitetura, código, pipelines, infraestrutura — foi a preparação para o verdadeiro desafio: operar o sistema ao longo do tempo, com usuários reais, tráfego real, falhas reais e a pressão constante de entregar novas funcionalidades sem degradar o que já existe.
Este artigo final do capstone implementa as operações em produção da plataforma de e-commerce: dashboards de observabilidade calibrados para o negócio, alertas com runbooks que guiam o engenheiro de plantão, um experimento de Chaos Engineering que valida a resiliência do sistema e os processos que sustentam a melhoria contínua. Ao final, uma retrospectiva sobre a jornada dos doze meses desta série.
Dashboards de Observabilidade do Negócio
Dashboards técnicos — CPU, memória, latência — são necessários mas insuficientes. Em um sistema de e-commerce, as métricas que importam para o negócio são pedidos criados por minuto, taxa de conversão do checkout, valor médio do pedido e receita por hora. Quando essas métricas caem, algo está errado — mesmo que as métricas de infraestrutura estejam normais.
// services/order-service/src/metrics/negocio.metrics.js
const { Counter, Histogram, Gauge } = require('prom-client');
// Métricas de negócio expostas pelo order-service
const pedidosCriados = new Counter({
name: 'loja_pedidos_criados_total',
help: 'Total de pedidos criados',
labelNames: ['canal', 'metodo_pagamento'],
});
const valorPedido = new Histogram({
name: 'loja_pedido_valor_reais',
help: 'Valor dos pedidos em reais',
labelNames: ['metodo_pagamento'],
buckets: [10, 25, 50, 100, 200, 500, 1000, 2000, 5000],
});
const checkoutsIniciados = new Counter({
name: 'loja_checkouts_iniciados_total',
help: 'Total de checkouts iniciados',
});
const checkoutsConcluidos = new Counter({
name: 'loja_checkouts_concluidos_total',
help: 'Total de checkouts concluídos com pagamento aprovado',
labelNames: ['metodo_pagamento'],
});
const estoqueBaixo = new Gauge({
name: 'loja_produtos_estoque_baixo',
help: 'Número de produtos com estoque abaixo do mínimo',
labelNames: ['categoria'],
});
// Registra as métricas nos pontos certos do código
function registrarCheckoutIniciado() {
checkoutsIniciados.inc();
}
function registrarPedidoCriado(pedido) {
pedidosCriados.inc({
canal: pedido.canal || 'web',
metodo_pagamento: pedido.metodo_pagamento,
});
valorPedido.observe(
{ metodo_pagamento: pedido.metodo_pagamento },
pedido.total
);
checkoutsConcluidos.inc({
metodo_pagamento: pedido.metodo_pagamento,
});
}
module.exports = {
registrarCheckoutIniciado,
registrarPedidoCriado,
estoqueBaixo,
};
Dashboard Principal no Grafana
{
"dashboard": {
"title": "Loja — Visão Operacional",
"tags": ["loja", "negocio", "producao"],
"refresh": "30s",
"panels": [
{
"title": "Pedidos por Minuto",
"type": "timeseries",
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
"targets": [{
"expr": "sum(rate(loja_pedidos_criados_total[5m])) * 60",
"legendFormat": "Pedidos/min"
}],
"thresholds": {
"steps": [
{ "color": "red", "value": 0 },
{ "color": "yellow", "value": 5 },
{ "color": "green", "value": 10 }
]
}
},
{
"title": "Taxa de Conversão do Checkout",
"type": "stat",
"description": "Percentual de checkouts iniciados que resultam em pedido pago",
"gridPos": { "x": 12, "y": 0, "w": 6, "h": 8 },
"targets": [{
"expr": "sum(rate(loja_checkouts_concluidos_total[30m])) / sum(rate(loja_checkouts_iniciados_total[30m])) * 100",
"legendFormat": "Conversão %"
}],
"fieldConfig": {
"defaults": {
"unit": "percent",
"thresholds": {
"steps": [
{ "color": "red", "value": 0 },
{ "color": "yellow", "value": 60 },
{ "color": "green", "value": 75 }
]
}
}
}
},
{
"title": "Receita por Hora (Projeção)",
"type": "stat",
"gridPos": { "x": 18, "y": 0, "w": 6, "h": 8 },
"targets": [{
"expr": "sum(rate(loja_pedido_valor_reais_sum[1h])) * 3600",
"legendFormat": "R$/hora"
}],
"fieldConfig": {
"defaults": { "unit": "currencyBRL" }
}
},
{
"title": "Valor Médio do Pedido (últimas 24h)",
"type": "stat",
"gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 },
"targets": [{
"expr": "sum(rate(loja_pedido_valor_reais_sum[24h])) / sum(rate(loja_pedido_valor_reais_count[24h]))",
"legendFormat": "Ticket Médio"
}],
"fieldConfig": {
"defaults": { "unit": "currencyBRL" }
}
},
{
"title": "Taxa de Erro HTTP por Serviço",
"type": "timeseries",
"gridPos": { "x": 0, "y": 12, "w": 24, "h": 8 },
"targets": [{
"expr": "sum by (service) (rate(http_requests_total{status=~'5..',namespace='producao'}[5m])) / sum by (service) (rate(http_requests_total{namespace='producao'}[5m])) * 100",
"legendFormat": "{{service}}"
}],
"fieldConfig": {
"defaults": {
"unit": "percent",
"custom": {
"lineWidth": 2
}
}
}
},
{
"title": "Latência p99 por Serviço",
"type": "timeseries",
"gridPos": { "x": 0, "y": 20, "w": 24, "h": 8 },
"targets": [{
"expr": "histogram_quantile(0.99, sum by (service, le) (rate(http_request_duration_seconds_bucket{namespace='producao'}[5m])))",
"legendFormat": "p99 {{service}}"
}],
"fieldConfig": {
"defaults": { "unit": "s" }
}
},
{
"title": "Mensagens na DLQ",
"description": "Qualquer valor > 0 requer investigação imediata",
"type": "stat",
"gridPos": { "x": 0, "y": 28, "w": 12, "h": 4 },
"targets": [{
"expr": "sum(aws_sqs_approximate_number_of_messages_visible{queue_name=~'.*-dlq'})",
"legendFormat": "Mensagens na DLQ"
}],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "color": "green", "value": 0 },
{ "color": "red", "value": 1 }
]
}
}
}
}
]
}
}
Alertas Calibrados com Runbooks
# infrastructure/kubernetes/platform/prometheus/rules/loja-alertas.yaml
groups:
- name: loja.negocio
rules:
# Queda brusca de pedidos — pode indicar falha no checkout
- alert: LojaPedidosCaindoBruscamente
expr: |
(
sum(rate(loja_pedidos_criados_total[5m])) * 60
)
<
(
sum(rate(loja_pedidos_criados_total[1h] offset 5m)) * 60 * 0.5
)
for: 5m
labels:
severity: critical
team: checkout
annotations:
summary: "Queda de >50% nos pedidos nos últimos 5 minutos"
description: |
Volume atual: {{ $value | printf "%.1f" }} pedidos/min
Comparando com média da última hora.
Possíveis causas: falha no checkout, gateway de pagamento,
serviço de catálogo ou problema de infraestrutura.
runbook_url: "https://wiki.empresa.com/runbooks/loja/queda-pedidos"
dashboard_url: "https://grafana.empresa.com/d/loja-overview"
# Taxa de conversão muito baixa
- alert: LojaTaxaConversaoBaixa
expr: |
sum(rate(loja_checkouts_concluidos_total[30m]))
/
sum(rate(loja_checkouts_iniciados_total[30m]))
* 100 < 50
for: 15m
labels:
severity: warning
team: checkout
annotations:
summary: "Taxa de conversão abaixo de 50% nos últimos 30 minutos"
description: |
Taxa atual: {{ $value | printf "%.1f" }}%
A taxa esperada é >70%. Investigar erros no order-service
e no gateway de pagamento.
runbook_url: "https://wiki.empresa.com/runbooks/loja/taxa-conversao"
- name: loja.infraestrutura
rules:
# SLO de disponibilidade — burn rate elevado
- alert: LojaSLODisponibilidadeBurnRateAlto
expr: |
(
sum(rate(http_requests_total{status=~"5..",namespace="producao"}[1h]))
/
sum(rate(http_requests_total{namespace="producao"}[1h]))
) > (1 - 0.995) * 14.4
for: 2m
labels:
severity: critical
team: plataforma
annotations:
summary: "Burn rate do SLO de disponibilidade 14.4x acima do normal"
description: |
Taxa de erro atual: {{ $value | printf "%.3f" }}
Ao ritmo atual, o orçamento de erros mensal será
consumido em menos de 2 dias.
runbook_url: "https://wiki.empresa.com/runbooks/slo/burn-rate-critico"
# Latência do order-service no checkout
- alert: LojaCheckoutLatenciaElevada
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket{
service="order-service",
route="/checkout"
}[5m])) by (le)
) > 2
for: 5m
labels:
severity: warning
team: checkout
annotations:
summary: "Latência p99 do checkout acima de 2 segundos"
description: |
p99 atual: {{ $value | printf "%.2f" }}s
Investigar: banco de dados, gateway de pagamento,
serviço de catálogo (verificação de estoque).
runbook_url: "https://wiki.empresa.com/runbooks/loja/latencia-checkout"
# Mensagens acumulando na DLQ
- alert: LojaMensagensDLQ
expr: |
sum(aws_sqs_approximate_number_of_messages_visible{
queue_name=~"loja-producao-.*-dlq"
}) by (queue_name) > 0
for: 1m
labels:
severity: warning
team: plataforma
annotations:
summary: "Mensagens na DLQ: {{ $labels.queue_name }}"
description: |
{{ $value }} mensagem(ns) na DLQ {{ $labels.queue_name }}.
Mensagens na DLQ indicam falhas repetidas de processamento.
Investigar logs do serviço consumidor.
runbook_url: "https://wiki.empresa.com/runbooks/sqs/mensagens-dlq"
Runbook: Queda de Pedidos
# Runbook: Queda Brusca de Pedidos
**Alerta:** LojaPedidosCaindoBruscamente
**Severidade:** Critical — acionar plantão imediatamente
**Equipe:** Checkout
---
## Diagnóstico Rápido (primeiros 5 minutos)
### 1. Verificar o dashboard principal
Acesse: https://grafana.empresa.com/d/loja-overview
Observe nos últimos 15 minutos:
- [ ] Taxa de pedidos (deve estar baixa se o alerta está ativo)
- [ ] Taxa de erro HTTP por serviço
- [ ] Latência p99 por serviço
### 2. Verificar o API Gateway
```bash
# Status dos pods do API Gateway
kubectl get pods -n producao -l app=api-gateway
# Logs dos últimos 5 minutos
kubectl logs -n producao -l app=api-gateway \
--since=5m --tail=100 | grep -E '"level":"error"'
# Taxa de erros no API Gateway
kubectl exec -n producao -it deploy/api-gateway -- \
curl -s localhost:3000/metrics | grep http_requests_total
3. Verificar o Order Service
# Status do order-service
kubectl get pods -n producao -l app=order-service
# Logs de erro
kubectl logs -n producao -l app=order-service \
--since=5m | grep -E '"level":"error"|"level":"fatal"'
# Verificar se o banco de dados está acessível
kubectl exec -n producao -it deploy/order-service -- \
node -e "require('./src/db/conexoes').testarConexao()"
4. Verificar o Gateway de Pagamento
# Circuit breaker do payment-gateway aberto?
kubectl logs -n producao -l app=order-service \
--since=10m | grep "payment-gateway" | grep -E "aberto|ABERTO"
# Teste manual do endpoint de pagamento
curl -s -o /dev/null -w "%{http_code}" \
https://api.gateway-pagamento.com/health
Ações de Mitigação
Se o banco de dados estiver inacessível:
# Verificar status do RDS
aws rds describe-db-instances \
--db-instance-identifier loja-producao-order \
--query 'DBInstances[0].{Status:DBInstanceStatus,AZ:AvailabilityZone}'
# Forçar failover se a instância primária estiver degradada
aws rds reboot-db-instance \
--db-instance-identifier loja-producao-order \
--force-failover
Se o gateway de pagamento estiver fora:
O circuit breaker deve abrir automaticamente e retornar HTTP 503 para os clientes ao invés de timeout.
Verificar: o circuit breaker do payment-gateway está aberto?
kubectl logs -n producao -l app=order-service --since=5m \
| grep "circuit breaker payment-gateway"
Se não abriu: reiniciar os pods do order-service para resetar o estado.
Se os pods estiverem em CrashLoopBackOff:
# Verifica o motivo do crash
kubectl describe pod -n producao \
$(kubectl get pods -n producao -l app=order-service \
-o jsonpath='{.items[0].metadata.name}')
# Rollback para a versão anterior via ArgoCD
argocd app rollback loja-producao-order --hard-refresh
Escalonamento
| Tempo sem resolução | Escalonar para |
|---|---|
| 10 minutos | Tech Lead de Checkout |
| 20 minutos | Gerente de Engenharia |
| 30 minutos | CTO + Comunicação Crise |
Pós-Resolução
- [ ] Registrar o incidente no sistema de tickets
- [ ] Comunicar usuários afetados se > 5 minutos de impacto
- [ ] Agendar postmortem se SEV-1 ou duração > 15 minutos
---
## Experimento de Chaos Engineering
Com o sistema em produção e os mecanismos de observabilidade funcionando, o primeiro experimento de Chaos Engineering valida a resiliência real:
```bash
#!/bin/bash
# scripts/chaos/experimento-checkout-resiliente.sh
# Valida que o checkout continua funcionando quando o catalog-service falha
set -euo pipefail
log() { echo "[$(date -u +%H:%M:%S)] $*"; }
NAMESPACE="producao"
log "=== Experimento: Resiliência do Checkout com Falha do Catalog Service ==="
log ""
log "Hipótese: O checkout continua funcionando para itens já em cache"
log "quando o catalog-service fica indisponível."
log ""
# ── Fase 1: Baseline ──────────────────────────────────────────────
log "FASE 1: Medindo baseline pré-experimento..."
BASELINE_TAXA=$(curl -s "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=
sum(rate(loja_pedidos_criados_total[5m])) * 60
' | jq '.data.result[0].value[1] | tonumber')
log "Taxa de pedidos baseline: ${BASELINE_TAXA} pedidos/min"
BASELINE_PODS=$(kubectl get pods -n $NAMESPACE \
-l app=catalog-service \
--field-selector=status.phase=Running \
-o name | wc -l)
log "Pods do catalog-service: ${BASELINE_PODS}"
# Verifica que o sistema está saudável antes de começar
if (( $(echo "$BASELINE_TAXA < 1" | bc -l) )); then
log "ERRO: Taxa de pedidos muito baixa para o experimento (${BASELINE_TAXA})"
log "O sistema precisa ter tráfego real para validar a hipótese."
exit 1
fi
# ── Fase 2: Injeção de Falha ──────────────────────────────────────
log ""
log "FASE 2: Injetando falha — reduzindo catalog-service para 0 réplicas..."
# Salva o número de réplicas atual para restaurar depois
REPLICAS_ORIGINAIS=$(kubectl get deployment catalog-service \
-n $NAMESPACE -o jsonpath='{.spec.replicas}')
# Escala o catalog-service para 0 — simula falha completa do serviço
kubectl scale deployment catalog-service \
-n $NAMESPACE --replicas=0
log "catalog-service escalado para 0 réplicas"
log "Aguardando pods terminarem..."
kubectl wait --for=delete pod \
-l app=catalog-service \
-n $NAMESPACE \
--timeout=60s 2>/dev/null || true
log "Todos os pods do catalog-service foram terminados"
log "Monitorando o sistema por 3 minutos..."
# ── Fase 3: Monitoramento ─────────────────────────────────────────
log ""
log "FASE 3: Monitoramento durante a falha..."
ERROS_DETECTADOS=0
for i in $(seq 1 12); do
sleep 15
# Taxa de pedidos durante a falha
TAXA_ATUAL=$(curl -s "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=
sum(rate(loja_pedidos_criados_total[1m])) * 60
' | jq '.data.result[0].value[1] | tonumber // 0')
# Taxa de erros no checkout
TAXA_ERRO=$(curl -s "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=
sum(rate(http_requests_total{
service="order-service",
route="/checkout",
status=~"5.."
}[1m]))
/
sum(rate(http_requests_total{
service="order-service",
route="/checkout"
}[1m])) * 100
' | jq '.data.result[0].value[1] | tonumber // 0')
# Estado do circuit breaker
CB_ESTADO=$(kubectl logs \
-n $NAMESPACE \
-l app=order-service \
--since=30s \
2>/dev/null | grep "catalog-service" | \
grep -oE '"estado":"[A-Z_]+"' | tail -1 || echo '"estado":"DESCONHECIDO"')
log " T+${i}m | Pedidos/min: ${TAXA_ATUAL} | Erros: ${TAXA_ERRO}% | CB: ${CB_ESTADO}"
# Condição de parada — sistema completamente degradado
if (( $(echo "$TAXA_ERRO > 20" | bc -l) )); then
log " ⚠️ Taxa de erros acima de 20% — condição de parada ativada"
ERROS_DETECTADOS=1
break
fi
done
# ── Fase 4: Restauração ───────────────────────────────────────────
log ""
log "FASE 4: Restaurando catalog-service..."
kubectl scale deployment catalog-service \
-n $NAMESPACE --replicas=$REPLICAS_ORIGINAIS
kubectl rollout status deployment/catalog-service \
-n $NAMESPACE --timeout=120s
log "catalog-service restaurado com ${REPLICAS_ORIGINAIS} réplicas"
# ── Fase 5: Verificação de Recuperação ───────────────────────────
log ""
log "FASE 5: Verificando recuperação completa..."
sleep 60
TAXA_RECUPERADA=$(curl -s "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=
sum(rate(loja_pedidos_criados_total[2m])) * 60
' | jq '.data.result[0].value[1] | tonumber // 0')
log "Taxa após recuperação: ${TAXA_RECUPERADA} pedidos/min"
# ── Resultado ────────────────────────────────────────────────────
log ""
log "=== RESULTADO DO EXPERIMENTO ==="
log ""
HIPOTESE_VALIDADA=true
if [ $ERROS_DETECTADOS -eq 1 ]; then
log "❌ HIPÓTESE REFUTADA: Sistema degradou além do aceitável"
log " Taxa de erros ultrapassou 20% durante a falha"
log ""
log " Ações corretivas sugeridas:"
log " 1. Revisar o fallback do circuit breaker do catalog-service"
log " 2. Aumentar o TTL do cache de produtos"
log " 3. Implementar resposta degradada (produtos sem verificação de estoque)"
HIPOTESE_VALIDADA=false
else
log "✅ HIPÓTESE VALIDADA: Sistema manteve operação durante falha do catalog-service"
log ""
log " Baseline: ${BASELINE_TAXA} pedidos/min"
log " Recuperado: ${TAXA_RECUPERADA} pedidos/min"
fi
log ""
log "Documentar resultado em: docs/chaos/$(date +%Y-%m-%d)-checkout-resiliente.md"
exit $([ "$HIPOTESE_VALIDADA" = "true" ] && echo 0 || echo 1)
Checklist Operacional de Produção
# Checklist: Sistema em Produção
## Diário (automatizado — verificar às 9h)
- [ ] Taxa de erros < 0.5% nas últimas 24h
- [ ] Nenhuma mensagem nas DLQs
- [ ] Backup do RDS completado com sucesso
- [ ] Certificados SSL com mais de 30 dias de validade
- [ ] Nenhum pod em CrashLoopBackOff
- [ ] Uso de storage abaixo de 70%
## Semanal (segunda-feira)
- [ ] Revisar recomendações do AWS Compute Optimizer
- [ ] Verificar CVEs novas nas imagens de container (Trivy)
- [ ] Revisar findings do AWS Security Hub
- [ ] Checar mensagens na DLQ da semana — investigar padrões
- [ ] Revisar métricas DORA da semana
- [ ] Relatório de custos — verificar anomalias
## Mensal (primeiro dia útil)
- [ ] Executar experimento de Chaos Engineering planejado
- [ ] Testar restauração de backup do banco de dados
- [ ] Revisar e atualizar runbooks com base nos incidentes do mês
- [ ] Coletar evidências SOC2 automáticas
- [ ] Revisar acessos IAM — remover acessos desnecessários
- [ ] Atualizar versão do Kubernetes (se nova versão disponível)
- [ ] Game Day trimestral (a cada 3 meses)
## A Cada Deploy em Produção
- [ ] Verificar métricas 5 minutos após o deploy
- [ ] Confirmar que o ArgoCD reporta aplicação Healthy
- [ ] Validar que os healthchecks estão passando
- [ ] Checar logs por erros não esperados
Retrospectiva: Doze Meses de Jornada
Este é o último artigo de uma série que começou com o cursor piscando em um terminal vazio e chegou a um sistema de e-commerce completo rodando em Kubernetes com observabilidade full-stack, segurança em camadas, pipeline de CI/CD automatizado e práticas de Chaos Engineering.
O Que Foi Construído
Ao longo de cinquenta e dois artigos organizados em dez módulos, foram cobertos:
O Módulo 1 estabeleceu as fundações — o terminal Linux, a árvore de diretórios, permissões, processos, shell scripting e SSH. São habilidades que parecem básicas mas que determinam a eficácia de tudo que vem depois. Um engenheiro que conhece profundamente o sistema operacional trabalha de maneira qualitativamente diferente de quem o evita.
O Módulo 2 cobriu o controle de versão com Git e GitHub — não apenas os comandos, mas os fluxos de trabalho que times de alta performance usam: Git Flow, proteção de branches, revisão de código estruturada, versionamento semântico e automação com GitHub Actions.
O Módulo 3 mergulhou em containers com Docker — desde os namespaces e cgroups que tornam os containers possíveis até as práticas de segurança e otimização de imagens que os tornam seguros e eficientes em produção.
O Módulo 4 construiu pipelines de CI/CD completos — pirâmide de testes, gestão de secrets, estratégias de deploy e sistemas de notificação que dão ao time visibilidade de cada mudança.
O Módulo 5 cobriu Infraestrutura como Código com Terraform e Ansible — da VPC completa na AWS até a gestão de estado remoto, módulos reutilizáveis e a integração entre provisionamento de infraestrutura e configuração de servidores.
O Módulo 6 implementou observabilidade full-stack — os três pilares (métricas, logs e traces), Prometheus e Grafana, OpenTelemetry para tracing distribuído e a cultura de alertas eficazes com runbooks.
O Módulo 7 aprofundou os serviços AWS essenciais — EC2, ECS, Lambda, RDS, ElastiCache, Route53, CloudFront e ACM — com decisões de arquitetura e Terraform funcional para cada um.
O Módulo 8 entrou no Kubernetes — dos fundamentos da arquitetura do cluster até EKS gerenciado, IRSA, Karpenter, GitOps com ArgoCD e estratégias avançadas de deploy com Argo Rollouts.
O Módulo 9 cobriu as dimensões mais maduras da engenharia de software: DevSecOps com segurança em todo o pipeline, compliance com LGPD e SOC2, FinOps, resiliência com Chaos Engineering, Platform Engineering com Backstage e a cultura de postmortems e melhoria contínua.
O Módulo 10 integrou tudo no projeto capstone — arquitetura de microsserviços, infraestrutura completa provisionada com Terraform, cinco serviços implementados com práticas de produção, pipeline completo de CI/CD e operações em produção.
O Que Não Foi Dito
Nenhuma série de cinquenta e dois artigos pode ser completa. Há territórios importantes que foram tocados superficialmente ou deixados de fora:
Service Mesh com Istio ou Linkerd — para organizações com muitos microsserviços, um service mesh oferece observabilidade de tráfego inter-serviço, mutual TLS automático e traffic management que os serviços não precisam implementar. É uma camada de infraestrutura de comunicação que complementa tudo que foi visto.
Banco de dados em Kubernetes com Operators — o capstone usou RDS gerenciado pela AWS, que é a escolha certa para a maioria dos casos. Mas operadores como o CloudNativePG permitem rodar PostgreSQL de alta disponibilidade dentro do cluster, com failover automático e backup nativo do Kubernetes.
Multi-cloud e Disaster Recovery entre regiões — o curso focou na AWS, mas os princípios de Terraform, Kubernetes e observabilidade se aplicam ao Azure e GCP. Arquiteturas ativas-ativas entre regiões ou entre clouds são o próximo nível de resiliência.
Machine Learning Operations (MLOps) — a integração de modelos de ML em pipelines de software é uma disciplina emergente com suas próprias ferramentas (MLflow, Kubeflow, SageMaker Pipelines) e desafios únicos de versionamento, reprodutibilidade e monitoramento de deriva.
O Princípio que Permanece
As ferramentas mudam. O Kubernetes que existe hoje é diferente do que existia há cinco anos, e será diferente daqui a cinco anos. Novos serviços AWS aparecem regularmente. Frameworks de observabilidade evoluem. Práticas de segurança se adaptam a novas ameaças.
O que não muda é o princípio fundamental que atravessa todos os cinquenta e dois artigos: sistemas de software são organismos sociotécnicos. Eles são construídos e operados por pessoas que trabalham juntas, usando ferramentas que incorporam decisões de design, em organizações com culturas que determinam o que é possível.
A parte técnica — os comandos, o código, a configuração — é necessária mas não suficiente. O que separa organizações que entregam software de alto impacto de forma sustentável das que lutam cronicamente com instabilidade e lentidão é a combinação de excelência técnica com práticas culturais que promovem aprendizado, colaboração e melhoria contínua.
Postmortems sem culpa. Métricas DORA rastreadas e discutidas. Chaos Engineering praticado regularmente. Platform Engineering que empodera times. Segurança como responsabilidade compartilhada. Custo como métrica de engenharia.
Essas práticas não são adições opcionais ao trabalho técnico — são o que transforma o trabalho técnico em valor sustentável.
O Próximo Passo É Seu
Uma série de artigos pode transferir conhecimento — conceitos, padrões, código. O que ela não pode transferir é a experiência de operar sistemas reais sob pressão real, de tomar decisões de arquitetura com informações incompletas, de conduzir um postmortem às 3 da manhã após um incidente de quatro horas.
Essa experiência só vem da prática. O capstone deste curso é um ponto de partida — um sistema completo que pode ser implantado, modificado, quebrado intencionalmente e recuperado. Cada experimento de Chaos Engineering, cada incidente real, cada decisão de arquitetura que se prova errada seis meses depois é aprendizado que nenhum artigo pode substituir.
A jornada de doze meses desta série foi um mapa. O território é o sistema em produção, com usuários reais, falhas inesperadas e o trabalho contínuo de fazer software melhor.
Referências Finais para Aprofundamento
Para continuar aprendendo - The Phoenix Project — IT Revolution — itrevolution.com — Romance técnico que narra a transformação DevOps de uma empresa fictícia. Captura os desafios organizacionais e humanos que os artigos técnicos não conseguem transmitir. Leitura obrigatória para qualquer profissional de DevOps. - Site Reliability Engineering — Google — sre.google — O livro que definiu o papel de SRE, disponível gratuitamente online. Capítulos sobre eliminação de toil, gerenciamento de mudanças e postmortems são referências definitivas. - CNCF Landscape — landscape.cncf.io — Mapa atualizado de todo o ecossistema Cloud Native. Útil para identificar ferramentas em cada categoria e entender onde as tecnologias desta série se encaixam no panorama maior.
Comunidades - CNCF Slack — cloud-native.slack.com — Comunidade oficial da CNCF com canais dedicados a Kubernetes, ArgoCD, Prometheus, OpenTelemetry e dezenas de outras ferramentas. - HashiCorp Discuss — discuss.hashicorp.com — Fórum oficial da HashiCorp para Terraform, Vault e outras ferramentas, com respostas da equipe de engenharia e da comunidade.
Certificações para consolidar o aprendizado - AWS Certified DevOps Engineer Professional — Certificação avançada que valida as práticas de CI/CD, monitoramento, segurança e alta disponibilidade na AWS abordadas nesta série. - Certified Kubernetes Administrator — CKA — cncf.io — Certificação prática que valida o conhecimento operacional de Kubernetes através de tarefas reais em um cluster ao vivo. - HashiCorp Certified Terraform Associate — developer.hashicorp.com — Certificação que valida o domínio do Terraform para provisionamento de infraestrutura como código.
Módulo 10 — Projeto Final
Encerramento da Série
Dominando DevOps & Cloud em 1 Ano foi concluída com sucesso.
Cinquenta e dois artigos. Dez módulos. Doze meses de conteúdo progressivo que vai do terminal Linux ao Kubernetes em produção, passando por containers, CI/CD, infraestrutura como código, observabilidade, AWS, segurança, compliance, FinOps, resiliência e cultura organizacional.
Fica o agradecimento a cada leitor que acompanhou esta jornada — do ls -la ao kubectl rollout status, do primeiro git commit ao ArgoCD sincronizando em produção.
O código está escrito. A infraestrutura está provisionada. O pipeline está rodando.
Agora é hora de operar.
Exercícios
Exercício 1
O alerta LojaPedidosCaindoBruscamente compara a taxa dos últimos 5 minutos com metade da média da última hora. Um erro de configuração derruba todos os pods do order-service — nenhum pedido é criado e nada mais é exportado ao Prometheus. O alerta dispara?
Ver resposta
✓ Resposta: Não. E o motivo é o que torna esse alerta perigoso: ele foi projetado para detectar queda parcial e falha em silêncio exatamente na falha total.
Quando os pods morrem, o Prometheus deixa de conseguir fazer scrape e a série loja_pedidos_criados_total fica stale. Passados cerca de 5 minutos, ela some do índice de séries ativas. A partir daí, rate(loja_pedidos_criados_total[5m]) não devolve zero — devolve vazio. E em PromQL, uma comparação cujo lado esquerdo é um vetor vazio produz um resultado vazio: não há nada para satisfazer a condição, o alerta permanece inactive, e ninguém é acordado.
É a distinção que separa "o valor é zero" de "não há valor", e ela vale para qualquer alerta escrito sobre uma métrica que o próprio serviço exporta: a métrica desaparece junto com o serviço. Alertas construídos assim funcionam bem em degradação parcial e são inúteis na indisponibilidade completa — que é justamente o cenário mais grave.
A correção é acrescentar uma regra que detecte a ausência, com absent():
- alert: LojaSemMetricasDePedidos
expr: absent(rate(loja_pedidos_criados_total[5m]))
for: 3m
labels:
severity: critical
annotations:
summary: "Métrica de pedidos ausente — order-service pode estar fora do ar"
Complementarmente, vale alertar sobre o próprio alvo de coleta, que não depende da aplicação exportar nada: up{job="order-service"} == 0. Essa métrica é gerada pelo Prometheus, e não pelo serviço, então continua existindo quando o serviço morre.
Um segundo problema mora no mesmo alerta, e aparece de madrugada: a comparação é relativa. Se o tráfego noturno normalmente cai a menos da metade da média da hora anterior, o alerta dispara todo dia às 2h sem que nada esteja errado — e a equipe aprende a ignorá-lo, que é o mecanismo de alert fatigue. Uma condição de piso absoluto (and sum(rate(…)) < N) ou a comparação com o mesmo horário da semana anterior (offset 7d) resolve isso.
Exercício 2
O painel "Taxa de Conversão do Checkout" divide loja_checkouts_concluidos_total por loja_checkouts_iniciados_total, e o help da primeira métrica diz "checkouts concluídos com pagamento aprovado". Confronte isso com onde ela é incrementada, no negocio.metrics.js, e com o fluxo de checkout implementado no artigo dos microsserviços. A métrica mede o que promete?
Ver resposta
✓ Resposta: Não. Ela conta pedidos criados, não pagamentos aprovados — e por isso a taxa de conversão exibida é sistematicamente maior que a real.
O incremento acontece dentro de registrarPedidoCriado(pedido), junto com pedidosCriados e valorPedido. No fluxo do CheckoutService, o pedido é gravado com status 'aguardando_pagamento' ao final do iniciarCheckout — antes de qualquer cobrança. O pagamento só é processado depois, em processarPagamento, que pode falhar por erro do gateway ou ser recusado pelo emissor, levando o pedido a 'cancelado'.
Ou seja: todo cartão recusado é contabilizado como conversão. Em e-commerce brasileiro, a recusa de cartão é frequente o bastante para deslocar a métrica em vários pontos percentuais — e o desvio não é constante, porque cresce justamente quando há problema no gateway.
O efeito é pior do que um número impreciso. O alerta LojaTaxaConversaoBaixa observa essa mesma razão com limite de 50%: se o gateway de pagamento passar a recusar tudo, os pedidos continuam sendo criados, a métrica continua subindo, a taxa permanece alta e o alerta não dispara. O indicador criado para detectar falha no funil de compra é cego para a falha mais cara dele — e o mesmo vale para loja_pedido_valor_reais, que soma receita nunca recebida no painel de "Receita por Hora".
A correção é registrar o evento no ponto em que ele de fato ocorre:
// em processarPagamento, após a aprovação confirmada
await PedidoRepository.atualizarStatus(pedidoId, 'pago', { … });
checkoutsConcluidos.inc({ metodo_pagamento: pedido.metodo_pagamento });
valorPedido.observe({ metodo_pagamento: pedido.metodo_pagamento }, pedido.total);
Vale manter as duas contagens separadas — pedidos criados e pagamentos aprovados —, porque a razão entre elas é a taxa de aprovação de pagamento, um indicador próprio e valioso. O erro não foi medir demais, foi dar a uma métrica o nome de outra: um painel que afirma medir conversão e mede intenção de compra leva o negócio a decisões erradas com confiança total.
Exercício 3
O experimento de chaos injeta a falha com kubectl scale deployment catalog-service --replicas=0. Considerando o HorizontalPodAutoscaler do catalog-service (minReplicas: 3) e a Application do ArgoCD com selfHeal: true e ignoreDifferences em /spec/replicas, o experimento consegue manter o serviço fora do ar pelos 3 minutos previstos?
Ver resposta
✓ Resposta: Não. O HPA restaura as réplicas em cerca de 15 a 30 segundos, e o experimento mede um sistema que nunca chegou a falhar de verdade.
Vale seguir cada controlador para ver por que só um deles age. O ArgoCD não interfere: a Application declara ignoreDifferences para /spec/replicas, justamente para não brigar com o autoscaler, então o selfHeal ignora o campo que o experimento alterou. O HPA, ao contrário, tem minReplicas: 3 como piso absoluto: no seu ciclo de reconciliação seguinte ele observa 0 réplicas, considera isso abaixo do mínimo e escala de volta. Os pods sobem, passam pelo readiness e voltam a atender.
O resultado é um experimento que reporta sucesso sem ter testado nada. A taxa de erros não passa de 20% porque a indisponibilidade durou segundos; o script conclui "✅ HIPÓTESE VALIDADA" e o time registra em docs/chaos/ que o checkout sobrevive à queda do catálogo. A confiança adquirida é falsa — e essa é a pior saída possível para um experimento de resiliência, pior do que não tê-lo executado.
Para injetar a falha de fato, é preciso agir onde nenhum controlador reconcilia. Três caminhos, em ordem de fidelidade:
- Cortar a rede, não a computação: uma
NetworkPolicytemporária que bloqueie o tráfego do order-service para o catalog-service. Os pods continuam de pé — HPA e ArgoCD nada percebem —, e o efeito observado é o mesmo de uma indisponibilidade, com a vantagem de ser exatamente o tipo de falha que o circuit breaker deveria tratar. - Suspender o HPA antes do experimento e restaurá-lo depois, assumindo o risco de esquecer o passo de restauração.
- AWS FIS com stop conditions, que traz a parada de emergência automática que este script não tem.
Repare que o script já cuida bem de duas coisas: verifica que há tráfego suficiente antes de começar (um experimento sem carga não prova nada) e define uma condição de parada em 20% de erro. O que falta é a etapa de validar que a falha foi realmente injetada — checar, logo após a injeção, que o catalog-service está mesmo inacessível. Sem essa verificação, todo o resto do experimento mede o sistema em operação normal.
Exercício 4
O runbook orienta, quando o gateway de pagamento está fora e o circuit breaker aparentemente não abriu: "Se não abriu: reiniciar os pods do order-service para resetar o estado." Avalie essa instrução.
Ver resposta
✓ Resposta: Ela é contraproducente — piora exatamente o problema que pretende resolver.
O circuit breaker é um objeto em memória, criado no carregamento do módulo. Reiniciar o pod o recria no estado FECHADO, com contagemFalhas = 0. Se o gateway de pagamento continua fora — e ele continua, porque o problema não é do order-service —, os pods recém-subidos voltam a tentar todas as chamadas, esperando o timeoutRequisicao de 10 segundos configurado para o pagamento, até acumularem 3 falhas e abrirem de novo. O "reset" desfaz a proteção e devolve latência máxima aos usuários durante o reaquecimento.
Há um segundo custo, imediato: reiniciar pods em meio a um incidente derruba as requisições em andamento — inclusive pagamentos já enviados ao gateway cujo resultado o serviço deixará de processar, produzindo cobranças sem pedido correspondente. Em um serviço que movimenta dinheiro, é o pior momento possível para reciclar processos.
O diagnóstico correto começa por questionar a premissa. Se o circuit breaker "não abriu", há explicações mais prováveis do que estado corrompido:
- Ele abriu, mas em outro pod. O estado é por processo — cada uma das réplicas mantém sua própria contagem, e o
kubectl logs -l app=order-servicemistura tudo. Um pod que recebeu poucas requisições ainda pode estar fechado. - As falhas não são consecutivas. Como o contador zera a cada sucesso, um gateway que responde intermitentemente mantém o circuito fechado indefinidamente — a limitação estrutural do contador consecutivo.
- O gateway está respondendo, mas recusando. Uma resposta HTTP 200 com
aprovado: falsenão é falha do ponto de vista do cliente HTTP: o circuito não deve abrir, e a queda de conversão tem outra origem.
A instrução útil no runbook é observar, não reiniciar — e o sistema já expõe o dado necessário. A métrica circuit_breaker_state traz o estado por instância, muito mais confiável que raspar logs com grep:
curl -s "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=circuit_breaker_state{nome="payment-gateway"}'
# 0 = fechado · 1 = half-open · 2 = aberto
Runbooks são escritos para serem seguidos por alguém sob pressão, às 3 da manhã, sem tempo para avaliar se o passo faz sentido. Uma ação destrutiva sugerida em um runbook será executada — e por isso cada passo precisa ser justificado no próprio documento.
Exercício 5
O alerta de SLO usa o limiar (1 - 0.995) * 14.4. De onde vem o número 14.4, e o que ele significa em termos de orçamento de erros? Que fragilidade essa regra de janela única apresenta?
Ver resposta
✓ Resposta: 14.4 é o burn rate — quantas vezes mais rápido que o normal o orçamento de erros está sendo consumido.
A aritmética é direta. Com SLO de 99,5%, o orçamento é de 0,5% de requisições com erro ao longo do período — 30 dias. Gastar esse orçamento a uma taxa 1× significa chegar ao fim do mês com ele exatamente esgotado. A 14,4×, o mês inteiro é consumido em 30 / 14.4 ≈ 2,08 dias — daí a descrição do alerta falar em "menos de 2 dias". O limiar em taxa de erro absoluta é 0,005 × 14,4 = 0,072, ou 7,2% das requisições falhando.
O valor não é arbitrário: 14.4 é o múltiplo canônico do SRE Workbook do Google para a janela de 1 hora, escolhido de modo que o alerta dispare quando 2% do orçamento mensal tiver sido consumido em uma única hora. Alertar por burn rate, e não por limiar fixo de erro, é o que mantém a proporcionalidade: 7,2% de erro por uma hora merece plantão; a mesma taxa por dez segundos, não.
A fragilidade é o uso de janela única. Com apenas [1h] e for: 2m, a regra herda os dois defeitos que a abordagem multi-janela existe para corrigir:
- Reação lenta a uma queda severa. Se 100% das requisições começarem a falhar agora, a média de 1 hora leva vários minutos para ultrapassar 7,2% — o alerta demora a disparar em plena indisponibilidade total.
- Alerta que não se apaga. Depois que o incidente é resolvido, a janela de 1 hora continua carregando os erros passados, e o alerta permanece ativo por bastante tempo com o sistema já saudável.
A forma recomendada combina uma janela longa (que dá significância estatística) com uma curta (que confirma que o problema é atual), exigindo as duas simultaneamente:
expr: |
(
sum(rate(http_requests_total{status=~"5..",namespace="producao"}[1h]))
/ sum(rate(http_requests_total{namespace="producao"}[1h])) > 0.072
)
and
(
sum(rate(http_requests_total{status=~"5..",namespace="producao"}[5m]))
/ sum(rate(http_requests_total{namespace="producao"}[5m])) > 0.072
)
Na prática se usam dois ou três pares desses, com burn rates decrescentes: 14,4× em 1h para acionar plantão, 6× em 6h e 1× em 3 dias para abrir um ticket. A severidade do alerta passa a acompanhar a velocidade do dano — que é a ideia central de operar por orçamento de erros em vez de por limiar fixo.