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

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

No artigo anterior foram apresentados os primeiros movimentos no terminal. Ag...

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa
Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa O Que o Ter...

GitLab Self-Hosted: Soberania Total sobre Código e Pipelines
GitLab Self-Hosted: Soberania Total sobre Código e Pipelines

Há contextos em que usar um serviço SaaS para hospedar código e pipelines não...