DevOps

Resiliência e Chaos Engineering Já leu

18 min de leitura

Resiliência e Chaos Engineering
Todo engenheiro que opera sistemas em produção por tempo suficiente aprende uma lição inevitável: sistemas complexos falham. Não é uma questão de se, mas de quando e como. A questão relevante não é construir sistemas que

Todo engenheiro que opera sistemas em produção por tempo suficiente aprende uma lição inevitável: sistemas complexos falham. Não é uma questão de se, mas de quando e como. A questão relevante não é construir sistemas que nunca falhem — isso é impossível em qualquer escala significativa — mas construir sistemas que falhem de maneira previsível, controlada e recuperável.

A diferença entre um sistema resiliente e um frágil não está na ausência de falhas — está em como o sistema se comporta quando as falhas ocorrem. Um sistema resiliente degrada graciosamente: quando um componente falha, os demais continuam funcionando com capacidade reduzida, sem colapso em cascata. Um sistema frágil, ao contrário, amplifica falhas locais em interrupções sistêmicas.

O Chaos Engineering é a disciplina de experimentar intencionalmente falhas em sistemas de produção para descobrir fraquezas antes que elas causem incidentes reais. A premissa é contraintuitiva mas poderosa: se o sistema vai falhar de qualquer forma, é melhor que a primeira falha aconteça em condições controladas, com o time de plantão disponível, do que às 3 da manhã durante um pico de tráfego.


Os Padrões de Resiliência Fundamentais

Antes de experimentar caos, é necessário implementar os padrões que tornam um sistema capaz de tolerar falhas. Esses padrões são os blocos de construção de qualquer arquitetura resiliente.

Circuit Breaker

O Circuit Breaker protege um serviço de continuar tentando chamar um serviço dependente que está falhando. Funciona como um disjuntor elétrico: quando o número de falhas ultrapassa um limiar, o circuito "abre" e todas as chamadas subsequentes são rejeitadas imediatamente sem tentar o serviço downstream. Após um período de espera, o circuito entra em estado "half-open" e permite uma chamada de teste — se bem-sucedida, o circuito fecha; se não, continua aberto.

// src/resilience/circuit-breaker.js
class CircuitBreaker {
  constructor(options = {}) {
    this.nome = options.nome || 'circuit-breaker';
    this.limiarFalhas = options.limiarFalhas || 5;
    this.timeoutAbertura = options.timeoutAbertura || 60000;   // 60s
    this.timeoutRequisicao = options.timeoutRequisicao || 5000; // 5s

    // Estados: FECHADO (normal) | ABERTO (rejeitando) | HALF_OPEN (testando)
    this.estado = 'FECHADO';
    this.contagemFalhas = 0;
    this.ultimaFalha = null;
    this.contagemSuccessos = 0;

    // Métricas para o Prometheus
    this._inicializarMetricas();
  }

  _inicializarMetricas() {
    const { Counter, Gauge } = require('prom-client');

    this.metricaEstado = new Gauge({
      name: `circuit_breaker_state`,
      help: 'Estado do circuit breaker (0=fechado, 1=half-open, 2=aberto)',
      labelNames: ['nome'],
    });

    this.metricaChamadas = new Counter({
      name: `circuit_breaker_calls_total`,
      help: 'Total de chamadas pelo circuit breaker',
      labelNames: ['nome', 'resultado'],
    });
  }

  async executar(funcao, fallback = null) {
    // Circuito aberto — rejeita imediatamente
    if (this.estado === 'ABERTO') {
      const agora = Date.now();
      if (agora - this.ultimaFalha < this.timeoutAbertura) {
        this.metricaChamadas.inc({ nome: this.nome, resultado: 'rejeitado' });

        if (fallback) return fallback();
        throw new Error(
          `Circuit breaker ${this.nome} aberto — aguardando recuperação`
        );
      }
      // Timeout expirou — tenta half-open
      this._transicionarPara('HALF_OPEN');
    }

    try {
      // Executa com timeout
      const resultado = await Promise.race([
        funcao(),
        new Promise((_, reject) =>
          setTimeout(
            () => reject(new Error('Timeout da requisição')),
            this.timeoutRequisicao
          )
        ),
      ]);

      this._registrarSuccesso();
      this.metricaChamadas.inc({ nome: this.nome, resultado: 'sucesso' });
      return resultado;

    } catch (erro) {
      this._registrarFalha(erro);
      this.metricaChamadas.inc({ nome: this.nome, resultado: 'falha' });

      if (fallback) return fallback();
      throw erro;
    }
  }

  _registrarSuccesso() {
    if (this.estado === 'HALF_OPEN') {
      this.contagemSuccessos++;
      // Requer 2 sucessos consecutivos para fechar
      if (this.contagemSuccessos >= 2) {
        this._transicionarPara('FECHADO');
      }
    }
    this.contagemFalhas = 0;
  }

  _registrarFalha(erro) {
    this.ultimaFalha = Date.now();
    this.contagemFalhas++;
    this.contagemSuccessos = 0;

    console.warn(JSON.stringify({
      level: 'warn',
      msg: `Circuit breaker ${this.nome}: falha registrada`,
      contagemFalhas: this.contagemFalhas,
      limiar: this.limiarFalhas,
      erro: erro.message,
    }));

    if (
      this.estado !== 'ABERTO' &&
      this.contagemFalhas >= this.limiarFalhas
    ) {
      this._transicionarPara('ABERTO');
    }
  }

  _transicionarPara(novoEstado) {
    const estadoAnterior = this.estado;
    this.estado = novoEstado;

    const estadoNumerico = { FECHADO: 0, HALF_OPEN: 1, ABERTO: 2 };
    this.metricaEstado.set(
      { nome: this.nome },
      estadoNumerico[novoEstado]
    );

    if (novoEstado === 'FECHADO') {
      this.contagemFalhas = 0;
      this.contagemSuccessos = 0;
    }

    console.info(JSON.stringify({
      level: 'info',
      msg: `Circuit breaker ${this.nome}: transição de estado`,
      de: estadoAnterior,
      para: novoEstado,
    }));
  }

  obterEstado() {
    return {
      nome: this.nome,
      estado: this.estado,
      contagemFalhas: this.contagemFalhas,
      ultimaFalha: this.ultimaFalha
        ? new Date(this.ultimaFalha).toISOString()
        : null,
    };
  }
}

module.exports = { CircuitBreaker };

Retry com Backoff Exponencial e Jitter

Tentar novamente uma operação que falhou é razoável — mas tentar novamente imediatamente, com todos os clientes ao mesmo tempo, pode transformar uma falha temporária em uma cascata de sobrecarga. O backoff exponencial aumenta o intervalo entre tentativas exponencialmente. O jitter adiciona aleatoriedade para evitar que múltiplos clientes tentem ao mesmo tempo (problema do "thundering herd"):

// src/resilience/retry.js

async function comRetry(funcao, opcoes = {}) {
  const {
    tentativasMaximas = 3,
    delayBase = 1000,        // 1s
    delayMaximo = 30000,     // 30s
    multiplicador = 2,
    jitter = true,
    retryableErros = null,   // null = tenta qualquer erro
    onRetry = null,          // callback antes de cada retry
  } = opcoes;

  let tentativa = 0;

  while (true) {
    try {
      return await funcao();
    } catch (erro) {
      tentativa++;

      // Verifica se o erro é recuperável
      if (retryableErros && !retryableErros.some(e => erro instanceof e)) {
        throw erro;
      }

      // Verifica se deve tentar status HTTP específicos
      if (erro.status && ![429, 500, 502, 503, 504].includes(erro.status)) {
        throw erro;
      }

      if (tentativa >= tentativasMaximas) {
        console.error(JSON.stringify({
          level: 'error',
          msg: 'Todas as tentativas esgotadas',
          tentativas: tentativa,
          erro: erro.message,
        }));
        throw erro;
      }

      // Calcula delay com backoff exponencial
      const delayExponencial = Math.min(
        delayBase * Math.pow(multiplicador, tentativa - 1),
        delayMaximo
      );

      // Adiciona jitter para evitar thundering herd
      // Estratégia "full jitter": delay aleatório entre 0 e o delay calculado
      const delay = jitter
        ? Math.random() * delayExponencial
        : delayExponencial;

      console.warn(JSON.stringify({
        level: 'warn',
        msg: 'Tentativa falhou, tentando novamente',
        tentativa,
        tentativasMaximas,
        proximaTentativaMs: Math.round(delay),
        erro: erro.message,
      }));

      if (onRetry) {
        await onRetry({ tentativa, erro, delay });
      }

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Uso combinado com circuit breaker
class ClienteHTTPResilient {
  constructor(baseUrl, opcoes = {}) {
    this.baseUrl = baseUrl;
    this.circuitBreaker = new CircuitBreaker({
      nome: opcoes.nome || new URL(baseUrl).hostname,
      limiarFalhas: opcoes.limiarFalhas || 5,
      timeoutAbertura: opcoes.timeoutAbertura || 30000,
    });
  }

  async get(caminho, opcoes = {}) {
    return this.circuitBreaker.executar(
      () => comRetry(
        () => fetch(`${this.baseUrl}${caminho}`, {
          method: 'GET',
          signal: AbortSignal.timeout(5000),
          ...opcoes,
        }).then(async res => {
          if (!res.ok) {
            const erro = new Error(`HTTP ${res.status}`);
            erro.status = res.status;
            throw erro;
          }
          return res.json();
        }),
        { tentativasMaximas: 3, delayBase: 500 }
      ),
      opcoes.fallback
    );
  }
}

module.exports = { comRetry, ClienteHTTPResilient };

Bulkhead: Isolamento de Recursos

O padrão Bulkhead — referência às divisórias estanques de um navio — isola recursos entre diferentes partes do sistema para que a sobrecarga em uma parte não afete as demais. Na prática, significa ter pools de threads, conexões ou workers separados por funcionalidade:

// src/resilience/bulkhead.js
class Bulkhead {
  constructor(opcoes = {}) {
    this.nome = opcoes.nome;
    this.concorrenciaMaxima = opcoes.concorrenciaMaxima || 10;
    this.tamanhoFila = opcoes.tamanhoFila || 20;
    this.timeoutFila = opcoes.timeoutFila || 5000;

    this.executando = 0;
    this.fila = [];

    const { Gauge, Counter } = require('prom-client');

    this.metricaExecutando = new Gauge({
      name: 'bulkhead_executing',
      help: 'Chamadas em execução no bulkhead',
      labelNames: ['nome'],
    });

    this.metricaRejeitadas = new Counter({
      name: 'bulkhead_rejected_total',
      help: 'Chamadas rejeitadas pelo bulkhead',
      labelNames: ['nome'],
    });
  }

  async executar(funcao) {
    // Verifica se pode executar imediatamente
    if (this.executando < this.concorrenciaMaxima) {
      return this._executarAgora(funcao);
    }

    // Verifica se a fila tem espaço
    if (this.fila.length >= this.tamanhoFila) {
      this.metricaRejeitadas.inc({ nome: this.nome });
      throw new Error(
        `Bulkhead ${this.nome} saturado: fila cheia (${this.tamanhoFila})`
      );
    }

    // Adiciona à fila e aguarda
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        const index = this.fila.findIndex(item => item.resolve === resolve);
        if (index !== -1) this.fila.splice(index, 1);
        reject(new Error(`Timeout de fila no bulkhead ${this.nome}`));
      }, this.timeoutFila);

      this.fila.push({
        funcao,
        resolve,
        reject,
        timeout,
      });
    });
  }

  async _executarAgora(funcao) {
    this.executando++;
    this.metricaExecutando.set({ nome: this.nome }, this.executando);

    try {
      return await funcao();
    } finally {
      this.executando--;
      this.metricaExecutando.set({ nome: this.nome }, this.executando);
      this._processarFila();
    }
  }

  _processarFila() {
    if (this.fila.length === 0) return;
    if (this.executando >= this.concorrenciaMaxima) return;

    const proximo = this.fila.shift();
    clearTimeout(proximo.timeout);

    this._executarAgora(proximo.funcao)
      .then(proximo.resolve)
      .catch(proximo.reject);
  }
}

module.exports = { Bulkhead };

AWS Fault Injection Simulator

O AWS FIS — Fault Injection Simulator — é o serviço gerenciado da AWS para Chaos Engineering. Permite injetar falhas reais em recursos AWS — terminar instâncias EC2, introduzir latência em chamadas de rede, falhar chamadas de API do SSM, degradar bancos de dados RDS — de forma controlada e com mecanismos de parada de emergência.

Experimento: Terminação de Instâncias EC2

# fis.tf

# IAM Role para o FIS executar ações nos recursos
resource "aws_iam_role" "fis" {
  name = "${var.project_name}-${var.environment}-fis-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "fis.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "fis" {
  role = aws_iam_role.fis.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:TerminateInstances",
          "ec2:StopInstances",
          "ec2:DescribeInstances",
          "ecs:StopTask",
          "ecs:DescribeTasks",
          "rds:RebootDBInstance",
          "rds:FailoverDBCluster",
          "elasticache:RebootCacheCluster",
          "ssm:SendCommand",
          "ssm:GetCommandInvocation",
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:ResourceTag/Environment" = var.environment
            "aws:ResourceTag/ChaosElegible" = "true"
          }
        }
      },
      {
        Effect   = "Allow"
        Action   = ["cloudwatch:DescribeAlarms"]
        Resource = "*"
      }
    ]
  })
}

# Experimento 1: Termina 33% das instâncias do Auto Scaling Group
resource "aws_fis_experiment_template" "terminar_instancias" {
  description = "Termina 1/3 das instâncias EC2 para testar recuperação do ASG"
  role_arn    = aws_iam_role.fis.arn

  # Stop condition — aborta o experimento se o alarme disparar
  stop_condition {
    source = "aws:cloudwatch:alarm"
    value  = aws_cloudwatch_alarm.disponibilidade_critica.arn
  }

  # Alvo: instâncias EC2 com tag Environment e ChaosElegible
  target {
    name           = "instancias-app"
    resource_type  = "aws:ec2:instance"
    selection_mode = "PERCENT(33)"  # Termina 33%

    resource_tag {
      key   = "Environment"
      value = var.environment
    }

    resource_tag {
      key   = "ChaosElegible"
      value = "true"
    }
  }

  action {
    name      = "terminar-instancias"
    action_id = "aws:ec2:terminate-instances"

    target {
      key   = "Instances"
      value = "instancias-app"
    }
  }

  tags = local.tags_comuns
}

# Experimento 2: Introduz latência de rede via SSM
resource "aws_fis_experiment_template" "latencia_rede" {
  description = "Introduz 200ms de latência nas chamadas de rede para testar timeouts"
  role_arn    = aws_iam_role.fis.arn

  stop_condition {
    source = "aws:cloudwatch:alarm"
    value  = aws_cloudwatch_alarm.latencia_critica.arn
  }

  target {
    name           = "instancias-app"
    resource_type  = "aws:ec2:instance"
    selection_mode = "PERCENT(50)"

    resource_tag {
      key   = "ChaosElegible"
      value = "true"
    }
  }

  action {
    name      = "injetar-latencia"
    action_id = "aws:ssm:send-command"

    parameter {
      key   = "documentArn"
      value = "arn:aws:ssm:us-east-1::document/AWSFIS-Run-Network-Latency"
    }

    parameter {
      key   = "documentParameters"
      value = jsonencode({
        Interface       = "eth0"
        DelayMilliseconds = "200"
        JitterMilliseconds = "50"
        DurationSeconds = "120"
        InstallDependencies = "True"
      })
    }

    parameter {
      key   = "duration"
      value = "PT3M"  # Duração máxima do experimento: 3 minutos
    }

    target {
      key   = "Instances"
      value = "instancias-app"
    }
  }

  tags = local.tags_comuns
}

# Experimento 3: Falha no banco de dados RDS Multi-AZ
resource "aws_fis_experiment_template" "failover_rds" {
  description = "Força failover do RDS Multi-AZ para testar tempo de recuperação"
  role_arn    = aws_iam_role.fis.arn

  stop_condition {
    source = "none"
  }

  target {
    name          = "banco-dados"
    resource_type = "aws:rds:db"
    selection_mode = "ALL"

    resource_tag {
      key   = "Environment"
      value = var.environment
    }

    resource_tag {
      key   = "ChaosElegible"
      value = "true"
    }
  }

  action {
    name      = "failover-rds"
    action_id = "aws:rds:reboot-db-instances"

    parameter {
      key   = "forceFailover"
      value = "true"
    }

    target {
      key   = "DBInstances"
      value = "banco-dados"
    }
  }

  tags = local.tags_comuns
}

# Alarme de parada de emergência
resource "aws_cloudwatch_alarm" "disponibilidade_critica" {
  alarm_name          = "${var.project_name}-${var.environment}-disponibilidade-critica"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "HealthyHostCount"
  namespace           = "AWS/ApplicationELB"
  period              = 60
  statistic           = "Average"
  threshold           = 1

  dimensions = {
    LoadBalancer = aws_lb.aplicacao.arn_suffix
    TargetGroup  = aws_lb_target_group.aplicacao.arn_suffix
  }

  alarm_description = "Para o experimento FIS se não houver hosts saudáveis"
  tags              = local.tags_comuns
}

Executando Experimentos com a CLI

#!/bin/bash
# scripts/executar-experimento-chaos.sh
# Executa um experimento de Chaos Engineering com observação em tempo real

set -euo pipefail

TEMPLATE_ID="${1:?Uso: $0 <experiment-template-id>}"
REGIAO="us-east-1"

log() { echo "[$(date -u +%H:%M:%S)] $*"; }
erro() { echo "[$(date -u +%H:%M:%S)] ERRO: $*" >&2; exit 1; }

log "=== Iniciando Experimento de Chaos Engineering ==="
log "Template: $TEMPLATE_ID"

# Verifica que os sistemas estão saudáveis antes de começar
log "Verificando saúde dos sistemas antes do experimento..."
HOSTS_SAUDAVEIS=$(aws elbv2 describe-target-health \
  --target-group-arn "$TARGET_GROUP_ARN" \
  --query 'TargetHealthDescriptions[?TargetHealth.State==`healthy`] | length(@)' \
  --output text)

if [ "$HOSTS_SAUDAVEIS" -lt 2 ]; then
  erro "Sistema não está saudável o suficiente para chaos (${HOSTS_SAUDAVEIS} hosts). Abortando."
fi

log "Sistema saudável: $HOSTS_SAUDAVEIS hosts antes do experimento"

# Registra o início para correlacionar com métricas
INICIO_EXPERIMENTO=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Inicia o experimento
log "Iniciando experimento..."
EXPERIMENTO_ID=$(aws fis start-experiment \
  --experiment-template-id "$TEMPLATE_ID" \
  --region "$REGIAO" \
  --query 'experiment.id' \
  --output text)

log "Experimento iniciado: $EXPERIMENTO_ID"

# Monitora o experimento
log "Monitorando estado do experimento..."
while true; do
  STATUS=$(aws fis get-experiment \
    --id "$EXPERIMENTO_ID" \
    --region "$REGIAO" \
    --query 'experiment.state.status' \
    --output text)

  case "$STATUS" in
    "running")
      HOSTS_AGORA=$(aws elbv2 describe-target-health \
        --target-group-arn "$TARGET_GROUP_ARN" \
        --query 'TargetHealthDescriptions[?TargetHealth.State==`healthy`] | length(@)' \
        --output text 2>/dev/null || echo "N/A")
      log "Status: $STATUS | Hosts saudáveis: $HOSTS_AGORA"
      sleep 15
      ;;
    "completed")
      log "Experimento concluído com sucesso!"
      break
      ;;
    "stopped")
      log "AVISO: Experimento parado (stop condition ativada)"
      break
      ;;
    "failed")
      erro "Experimento falhou. Verificar logs do FIS."
      ;;
    *)
      log "Status inesperado: $STATUS"
      sleep 5
      ;;
  esac
done

# Aguarda recuperação e verifica estado final
log "Aguardando recuperação completa do sistema..."
sleep 60

HOSTS_FINAL=$(aws elbv2 describe-target-health \
  --target-group-arn "$TARGET_GROUP_ARN" \
  --query 'TargetHealthDescriptions[?TargetHealth.State==`healthy`] | length(@)' \
  --output text)

log "Estado final: $HOSTS_FINAL hosts saudáveis"
log "Início: $INICIO_EXPERIMENTO"
log "Fim: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

if [ "$HOSTS_FINAL" -ge "$HOSTS_SAUDAVEIS" ]; then
  log "✅ Sistema se recuperou completamente"
else
  log "⚠️  Sistema recuperou parcialmente: ${HOSTS_FINAL}/${HOSTS_SAUDAVEIS} hosts"
fi

Game Days: Praticando a Falha

O Game Day é a prática de simular falhas e incidentes em ambiente controlado com o time completo presente — uma forma de treino operacional que combina o Chaos Engineering técnico com o treinamento de resposta a incidentes.

Um Game Day típico segue a estrutura:

Pré-Game Day — definição do cenário, hipóteses a testar, métricas de sucesso e plano de rollback. O time documenta o comportamento esperado do sistema.

Execução — um facilitador conduz os experimentos enquanto o time observa métricas, toma decisões e responde como faria em um incidente real.

Revisão — análise do que aconteceu versus o que foi esperado, identificação de fraquezas e definição de ações corretivas.

## Game Day — Plano de Execução

**Data:** 2025-03-15
**Duração:** 4 horas (09h–13h)
**Facilitador:** Ana Costa
**Participantes:** Time de Plataforma + On-call

### Cenário 1: Falha de uma AZ inteira (09h00–10h00)
**Hipótese:** O sistema continua funcionando com latência < 500ms
se a AZ us-east-1b ficar indisponível.

**Ação:** Adicionar regra de NACL bloqueando todo o tráfego para a
subnet privada em us-east-1b.

**Métricas observadas:**
- Taxa de erro HTTP 5xx (alvo: < 0.1%)
- Latência p99 (alvo: < 500ms)
- Tempo de detecção do health check
- Tempo até rebalanceamento do ASG

**Rollback:** Remover a regra de NACL

---

### Cenário 2: Esgotamento do pool de conexões RDS (10h15–11h15)
**Hipótese:** A aplicação degrada graciosamente e retorna HTTP 503
ao invés de travar quando o pool de conexões é esgotado.

**Ação:** Injetar carga de 10x via k6 apontando para endpoint
que abre transações longas no banco.

**Métricas observadas:**
- Comportamento do circuit breaker
- Resposta HTTP da aplicação (503 vs travamento)
- DatabaseConnections no CloudWatch RDS
- Tempo de recuperação após a carga diminuir

---

### Cenário 3: Rollback de deploy com bug crítico (11h30–12h30)
**Hipótese:** O time consegue detectar e reverter um deploy
problemático em menos de 5 minutos.

**Ação:** Deploy de versão com bug que aumenta latência p99 em 10x.
Time precisa detectar, decidir reverter e executar.

**Métricas observadas:**
- MTTD (tempo até detecção via alerta)
- MTTR (tempo até rollback completo)
- Eficácia dos runbooks

Healthchecks e Graceful Shutdown

Resiliência não é apenas sobre sobreviver a falhas externas — é também sobre falhar graciosamente quando o próprio processo precisa ser encerrado:

// src/server.js — servidor com healthchecks e graceful shutdown completos

const express = require('express');
const app = express();

// Estado de prontidão do servidor
const estado = {
  pronto: false,        // readiness: aceita tráfego?
  vivo: true,           // liveness: processo está OK?
  encerrandoGrace: false,
};

// Endpoint de liveness — monitora se o processo está vivo
// Falha apenas em casos extremos: deadlock, memória corrompida
app.get('/health/live', (req, res) => {
  if (!estado.vivo) {
    return res.status(503).json({ status: 'unhealthy', motivo: 'processo-degradado' });
  }
  res.json({ status: 'healthy', uptime: process.uptime() });
});

// Endpoint de readiness — monitora se está pronto para receber tráfego
// Falha durante inicialização, encerramento ou quando dependências estão indisponíveis
app.get('/health/ready', async (req, res) => {
  if (estado.encerrandoGrace) {
    return res.status(503).json({
      status: 'not-ready',
      motivo: 'encerrando',
    });
  }

  if (!estado.pronto) {
    return res.status(503).json({
      status: 'not-ready',
      motivo: 'inicializando',
    });
  }

  // Verifica dependências críticas
  try {
    await Promise.all([
      verificarBancoDados(),
      verificarRedis(),
    ]);
    res.json({ status: 'ready' });
  } catch (erro) {
    res.status(503).json({
      status: 'not-ready',
      motivo: 'dependencia-indisponivel',
      detalhe: erro.message,
    });
  }
});

// Graceful shutdown — garante que requisições em andamento completam
function configurarGracefulShutdown(servidor) {
  const TIMEOUT_GRACE = 30000; // 30 segundos

  async function encerrar(sinal) {
    console.info(JSON.stringify({
      level: 'info',
      msg: `Recebido sinal ${sinal} — iniciando encerramento gracioso`,
    }));

    // 1. Marca como não-pronto — load balancer para de enviar tráfego
    estado.encerrandoGrace = true;

    // 2. Aguarda o load balancer detectar o unhealthy (geralmente 10-30s)
    await new Promise(resolve => setTimeout(resolve, 10000));

    // 3. Para de aceitar novas conexões
    servidor.close(async () => {
      console.info(JSON.stringify({
        level: 'info',
        msg: 'Servidor HTTP fechado — encerrando conexões com banco',
      }));

      // 4. Fecha conexões com banco e cache graciosamente
      try {
        await pool.end();
        await redisClient.quit();
        console.info(JSON.stringify({
          level: 'info',
          msg: 'Encerramento gracioso concluído',
        }));
        process.exit(0);
      } catch (erro) {
        console.error(JSON.stringify({
          level: 'error',
          msg: 'Erro no encerramento gracioso',
          erro: erro.message,
        }));
        process.exit(1);
      }
    });

    // Timeout forçado — se demorar mais que TIMEOUT_GRACE, força encerramento
    setTimeout(() => {
      console.error(JSON.stringify({
        level: 'error',
        msg: 'Timeout de encerramento gracioso — forçando saída',
      }));
      process.exit(1);
    }, TIMEOUT_GRACE);
  }

  process.on('SIGTERM', () => encerrar('SIGTERM'));
  process.on('SIGINT', () => encerrar('SIGINT'));
}

// Inicialização com verificação de dependências
async function inicializar() {
  console.info('Inicializando servidor...');

  // Aguarda dependências ficarem disponíveis
  await aguardarBancoDados();
  await aguardarRedis();

  // Executa migrations pendentes
  await executarMigrations();

  // Marca como pronto para receber tráfego
  estado.pronto = true;
  console.info('Servidor pronto para receber tráfego');
}

const servidor = app.listen(process.env.PORT || 3000, async () => {
  await inicializar();
});

configurarGracefulShutdown(servidor);

O Que Vem a Seguir

O próximo artigo aborda Platform Engineering — a disciplina emergente que consolida as práticas de DevOps em plataformas internas que permitem que times de desenvolvimento sejam autônomos sem precisar ser especialistas em infraestrutura. Serão cobertos o conceito de Internal Developer Platform, o Backstage da Spotify como portal de desenvolvedores e os princípios de "paved roads" que equilibram autonomia e padronização.


Referências para Aprofundamento

Chaos Engineering - Principles of Chaos Engineering — principlesofchaos.org — Manifesto original dos princípios de Chaos Engineering, definindo o método científico aplicado a sistemas distribuídos. - AWS Fault Injection Simulator — docs.aws.amazon.com — Documentação completa do AWS FIS com guia de criação de experimentos, ações disponíveis e integração com CloudWatch. - Chaos Engineering — O'Reilly — oreilly.com — Livro referência sobre Chaos Engineering por engenheiros da Netflix, cobrindo desde os fundamentos até implementações avançadas.

Resiliência - Release It! — Pragmatic Bookshelf — pragprog.com — Livro fundamental sobre padrões de resiliência em sistemas de produção, incluindo Circuit Breaker, Bulkhead, Timeout e Back Pressure. - AWS Resilience Hub — docs.aws.amazon.com — Documentação do AWS Resilience Hub, serviço que avalia automaticamente a resiliência de aplicações AWS contra RTO e RPO definidos.

Comentários

Mais em DevOps

Azure DevOps: Pipelines e Repositórios
Azure DevOps: Pipelines e Repositórios

O GitHub Actions foi a plataforma de CI/CD do currículo principal desta série...

ECS e Lambda: Containers e Serverless na AWS
ECS e Lambda: Containers e Serverless na AWS

Gerenciar servidores EC2 diretamente — aplicar patches, monitorar uso de disc...

Semantic Versioning e Tags de Release
Semantic Versioning e Tags de Release

Imagine receber uma mensagem de um colega dizendo: "o bug est&aacute; na vers...