Capstone: Operações em Produção e Retrospectiva da Jornada

[320] Capstone: Operações em Produção e Retrospectiva da Jornada

O sistema não termina no último deploy — começa: dashboards com as métricas que importam para o negócio, alertas calibrados por burn rate de SLO com runbooks de plantão, um experimento de chaos que valida a resiliência do checkout, o checklist operacional e a retrospectiva dos doze meses da série.
DevOps

32 min de leitura

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 iniciarCheckoutantes 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 NetworkPolicy temporá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-service mistura 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: false nã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.

Comentários

Mais em DevOps

Docker Volumes e Redes: Persistência e Comunicação
Docker Volumes e Redes: Persistência e Comunicação

Volumes e redes no Docker: por que o sistema de arquivos do container é…

Pipeline com GitHub Actions: Build, Test e Deploy Automático
Pipeline com GitHub Actions: Build, Test e Deploy Automático

Um pipeline completo com GitHub Actions: jobs de lint, testes unitários e de…

Navegando pelo Sistema de Arquivos com Confiança
Navegando pelo Sistema de Arquivos com Confiança

A árvore de diretórios do Linux e o que cada ramo guarda: /etc, /var, /home…