Skip to content

Runbook de Resposta a Incidentes

O procedimento único e autoritativo para tratar incidentes em produção no tlsstress.art — do primeiro page ao postmortem. Complementa o manual de operação da observabilidade (docs/observability/RUNBOOK.md) e as regras de alerta em observability/cloud/prometheus/alerts.yml, observability/cloud/alertmanager/alertmanager.yml, e k8s/74-token-ledger-prometheus-rules.yaml.

Escopo. Este runbook cobre o caminho de dinheiro / provisionamento / dados: cadastro do cliente → pagamento Stripe → provisionamento do tenant → ledger da token-economy → dashboard do operador on-prem, além da plataforma que os observa. Ele não substitui os chaos drills por componente (chaos-*.md) nem o DR drill (dr-drill.md); ele diz como classificar, triar, mitigar, comunicar e aprender com um incidente em todos eles.

Regra de ouro. Mitigue primeiro o impacto ao cliente, ache a causa-raiz depois. Um cliente pago sem tenant, um ledger corrompido ou um banco que o app não alcança são todos SEV-1 até prova em contrário.


1. Classificação de severidade

A severidade é definida pelo impacto ao cliente/financeiro, não por qual componente quebrou. Defina-a nos primeiros 5 minutos; pode subir ou descer conforme você descobre mais.

Severidade Definição Exemplos Page? Cadência de comunicação
SEV-1 Dinheiro perdido/em risco, corrupção de dados, ou um cliente pago quebrado de forma material ProvisioningJobsStuck (pago, sem tenant), TokenLedgerOverspentUtxo (corrupção de ledger), AppDBDown (DB inalcançável), RDSStorageCritical, R2BucketPublic Sim (pager critical) A cada 30 min
SEV-2 Degradado mas sem dinheiro perdido ainda; indicador antecedente de um SEV-1 RDSConnectionsHigh, RDSStorageLow, TokenLedgerStaleBillingWebhooks, StripeFailedPaymentsSpike, DashboardDBDown Sim se for de madrugada A cada 2 h
SEV-3 Localizado / cosmético / vantagem única; sem impacto ao cliente JourneyVantageChallenged, CloudflareThreatSpike (info), RUMPoorLCP, uma vantagem sintética instável Não Na resolução

A severidade também é um rótulo no alerta. severity="critical" no Prometheus roteia para o receiver pager (re-page de hora em hora + Slack @channel); warning/info caem no firehose #tlsstress-observability. Ver alertmanager.yml.


2. O fluxo de plantão (triagem → mitigação → resolução → postmortem)

flowchart TD
    A([Alerta dispara / report recebido]) --> B{Reconhecer<br/>em até 5 min}
    B -->|pager critical| C[Abrir o incidente:<br/>definir SEV, iniciar thread]
    B -->|firehose Slack| C
    C --> D{Classificar severidade<br/>por impacto dinheiro/cliente}
    D -->|SEV-1| E[Declarar incidente:<br/>IC + escriba + comunicação]
    D -->|SEV-2/3| F[Respondedor único]
    E --> G[TRIAGEM: ler o runbook_url<br/>do alerta + dashboards golden]
    F --> G
    G --> H{Causa-raiz conhecida<br/>ou mitigação óbvia?}
    H -->|Não| I[Escalar ao dono do domínio<br/>ver §6 escalonamento]
    I --> G
    H -->|Sim| J[MITIGAR: estancar o sangramento<br/>self-heal / rollback / escala]
    J --> K{Impacto ao cliente<br/>cessado?}
    K -->|Não| L[Subir a severidade,<br/>ampliar o escalonamento]
    L --> J
    K -->|Sim| M[RESOLVER: verificar alerta limpo,<br/>dados reconciliados, sem backlog]
    M --> N{SEV-1 ou SEV-2?}
    N -->|Sim| O[POSTMORTEM<br/>em até 5 dias úteis]
    N -->|Não| P([Fechar incidente])
    O --> Q[Itens de ação acompanhados<br/>até a conclusão]
    Q --> P

2.1 Triagem (primeiros 10 minutos)

  1. Reconheça o page para o time saber que está assumido (silencia o re-page do pager).
  2. Leia o alerta. Todo alerta crítico de dinheiro/DB carrega um runbook_url — siga-o. As anotações summary e description foram escritas para dizer o que quebrou e a primeira ação.
  3. Abra os dashboards golden (…/grafana): cloud-overview, business-revenue, aws-infra, e o cockpit do operador para incidentes DUT-side. Confirme que o alerta é real (não uma única vantagem instável) e ache o raio de impacto.
  4. Procure um sinal mais alto. Um critical muitas vezes vem com warnings correlacionados (ex.: RDSConnectionsHighAppDBDown). As inhibit_rules do Alertmanager suprimem os warnings derivados, então confie no critical.

2.2 Mitigar

Estanque o impacto ao cliente com a alavanca segura mais rápida, mesmo que temporária: self-heal (deixar o obs-db-selfheal redeployar), rollback do último deploy, escalar o recurso, ou colocar feature-flag no caminho quebrado. Documente cada ação na thread — o postmortem precisa da linha do tempo.

2.3 Resolver

Um incidente só é resolvido quando todas estas condições valem: - O alerta que disparou limpou (e fica limpo por um repeat_interval). - O impacto ao cliente sumiu (re-rode o sintético / journey relevante). - Os dados estão reconciliados — nenhum job de provisionamento preso, nenhuma UTXO over-spent, nenhum backlog de webhook de billing não processado. - A mitigação é durável, ou há um follow-up registrado para torná-la durável.

2.4 Postmortem

Todo SEV-1 e SEV-2 ganha um postmortem sem culpados em até 5 dias úteis (template no §8). SEV-3 ganha uma linha na thread do incidente.


3. Alertas-chave de dinheiro / provisionamento / dados

São os alertas que, ao disparar, significam que um cliente está ou pode estar pagando por nada — ou que o ledger financeiro está errado. Trate-os como SEV-1 a menos que a triagem prove o contrário. Fonte de verdade dos alertas de ledger: k8s/74-token-ledger-prometheus-rules.yaml.

3.1 ProvisioningJobsStuck — cliente pago, sem tenant (SEV-1)

  • Expr: tlsstress_provisioning_stuck > 0 por 30m · severity: critical.
  • Significa: um ou mais jobs de provisionamento estão presos (>15 min sem progresso, ou ≥8 tentativas). Um cliente que pagou não recebeu o tenant. É a pior classe de incidente: perda silenciosa de dinheiro com um cliente insatisfeito.
  • Primeiras ações:
  • Leia o runbook_url do alerta (RUNBOOK #provisioning-stuck).
  • Verifique o orchestrator Temporal para o(s) workflow(s) que falharam e o cron reconcile-provisioning — ele está rodando e progredindo?
  • Identifique o(s) cliente(s) afetado(s) e o passo da saga que falhou. Não re-drive às cegas: um re-drive ingênuo pode cunhar um segundo deployment dpl_ ou zerar uma quota paga. Re-drive apenas o passo que falhou.
  • Se não conseguir provisionar dentro do SLA, comunique-se com o cliente (§7) e prepare um caminho de reembolso/crédito com o financeiro.

3.2 AppDBDown — o app não alcança o banco (SEV-1)

  • Expr: probe_success{job="blackbox-ready"} == 0 por 2m · severity: critical.
  • Significa: a sonda profunda /api/ready (que roda SELECT 1 em cada pool) está falhando → o app está no ar mas o DB está inalcançável, ou o app está fora. É o alerta que a queda do admin em 2026-06-16 não tinha: o app servia HTTP 200 gracioso do /api/health raso enquanto o DB estava inalcançável, então nada disparou por ~33 horas.
  • Primeiras ações (RCA completo no RUNBOOK #61-db-unreachable):
  • curl -s https://<app>.tlsstress.art/api/ready | jq — veja qual check de DB está ok:false.
  • O obs-db-selfheal já redeploya o serviço App Runner num 503 (re-busca o secret + reconstrói o pool). Acompanhe o deploy; se recuperar, está feito.
  • Persiste? Verifique se o RDS está available (aws rds describe-db-instances) e se o DATABASE_URL do app autentica como sua role não-mestre dedicada (octopus_admin_app / tlsstress_app) — nunca o mestre rotacionado tlsstress_admin. O RDS rotaciona o secret-mestre gerenciado a cada ~7 dias; um app apontado ao mestre quebra a cada rotação.

A lição de 2026-06-16, codificada. Os apps agora conectam como roles não-mestre dedicadas, o /api/ready é uma sonda profunda, o AppDBDown dispara em ~2 min, e o obs-db-selfheal redeploya automaticamente. Se você ver "DB indisponível" num app mas o blackbox estiver verde, você está olhando para um health check raso — sonde /api/ready, não /api/health.

3.3 Integridade do ledger — os alertas da token-economy (SEV mista)

O ledger da token-economy é a fonte de verdade financeira. Seus alertas vivem em k8s/74-token-ledger-prometheus-rules.yaml e fazem scrape de app.tlsstress.art/api/metrics (bearer do AWS Secrets Manager tlsstress-phase0/metrics-token).

Alerta Expr Sev Significa + primeira ação
TokenLedgerOverspentUtxo tlsstress_utxo_overspent > 0 (1m) critical Corrupção de ledger — uma nota com spent_amount > amount. O CHECK do banco torna isso impossível, então um valor não-zero é um bug profundo. Congele resgates, investigue imediatamente, não deixe propagar.
ProvisioningJobsStuck tlsstress_provisioning_stuck > 0 (30m) critical Ver §3.1 — cliente pago sem tenant.
TokenLedgerScrapeDown absent(tlsstress_tsu_circulating) (10m) warning Sem métricas do ledger por 10m — METRICS_TOKEN rotacionado sem atualizar o secret montado, ou /api/metrics fora. Você fica cego a todos os alertas de ledger até consertar.
TokenLedgerStaleBillingWebhooks tlsstress_webhooks_stale_billing > 0 (30m) warning Webhooks Stripe de billing presos não-terminais >1h — um crédito pode não ter aplicado (cliente pagou, saldo não recarregou). Verifique o processador de webhooks + o dashboard Stripe.
TokenLedgerStaleActiveTickets tlsstress_tickets_stale_active > 0 (30m) warning Tickets ACTIVE além de expiry+24h — o sweep tlsstress-expire-tickets-hourly está doente.
TokenLedgerOutboxBacklog tlsstress_outbox_pending > 100 (30m) warning Outbox de webhooks acumulando (>100 não entregues) — verifique deliver-webhooks + saúde dos receptores.
TokenLedgerUsageWithoutLiveness tlsstress_usage_without_liveness > 0 (1h) warning Uma licença reportou uso em 24h sem heartbeat — possível tamper de token / clock-skew / replay de token copiado. A cobrança é coberta por L2; este é um sinal de integridade.

3.4 Saturação do RDS — page antes do 503 (SEV-2 → SEV-1)

O AppDBDown só dispara quando o DB já está totalmente inalcançável. O exporter aws-rds-metrics page na saturação primeiro (limiares ajustados para db.t4g.micro, ~112 conexões máx, 1 GB RAM — re-ajuste no resize):

  • RDSConnectionsHigh (> 90, warning) — exaustão de pool se aproximando; novas conexões recusam antes do /api/ready virar. Verifique PgBouncer / vazamento de conexão.
  • RDSStorageLow (< 2 GB, warning) / RDSStorageCritical (< 1 GB, critical) — o RDS para escritas quando o storage acaba. Aumente o storage alocado agora: aws rds modify-db-instance --allocated-storage.
  • RDSCPUHigh (> 85%, warning) / RDSMemoryLow (< 100 MB, warning) — pressão de carga / risco de OOM.
  • RDSMetricsBlind (rds_metrics_scrape_ok == 0, warning) — o exporter não consegue ler o CloudWatch (key RO expirada / IAM / throttling). Todos os alertas RDS acima ficam silenciosos até isto recuperar.

3.5 Cockpit do operador DUT-side — DashboardDown / DashboardDBDown (SEV-2)

Estes disparam no Prometheus on-prem, não na caixa cloud (observability/prometheus/alerts/web-agent-alerts.yml):

  • DashboardDown (up{job="dashboard"} == 0, critical) — o processo/scrape do cockpit do operador está inalcançável. Pelo CLAUDE.md, o dashboard é a única interface do operador, então isto cega o operador.
  • DashboardDBDown (dashboard_db_up == 0, critical) — o cockpit está no ar mas seu Postgres está inalcançável (a classe de falha de DB silenciosa de 2026-06-16, espelhada DUT-side). O cockpit mostra dados congelados e um badge vermelho ao vivo "sem dados (DB)". Verifique o PgBouncer / Postgres on-prem; o /api/ready do dashboard é a sonda profunda.

3.6 Sinais de negócio / pagamento (SEV-2/3)

Do grupo business da cloud (poller RO da Stripe): - StripeFailedPaymentsSpike (> 5 hoje, warning) — indicador antecedente de problema de cartão/processador ou fraude. - StripeOpenDisputes (> 0, warning) — chargebacks precisam de resposta humana dentro da janela da Stripe.


4. O dead-man's-switch (quem vigia o vigia)

Dois meta-alertas mantêm a plataforma honesta:

  • Watchdog (vector(1), sempre disparando) roteia para o receiver deadman → um heartbeat externo no healthchecks.io. Quando o heartbeat para de chegar, o serviço externo te page — ou seja, quando toda a stack de obs está fora e não consegue mais alertar. É o único alerta que você quer manter disparando.
  • ObsComponentDown (up{job=~"grafana|loki|tempo|vector|prometheus|…"} == 0, critical) — um componente de obs caiu; a plataforma pode estar parcialmente cega.

Se você parar de receber qualquer alerta e a caixa parecer bem, suspeite do caminho de alerta (webhook Slack rotacionado, Alertmanager travado) antes de assumir que está tudo certo.


5. Cola de primeira resposta

# Caixa de obs cloud (Hetzner)
ssh -i ~/.ssh/tlsstress_f2_hetzner root@89.167.3.1
cd /opt/obs && docker compose ps                 # a stack está saudável?
docker compose logs -f alertmanager              # o alerta está fluindo?

# O DB de um app está realmente alcançável? (sonda profunda — lição de 2026-06-16)
curl -s https://app.tlsstress.art/api/ready   | jq
curl -s https://admin.tlsstress.art/api/ready | jq

# Métricas do ledger vivas? (bearer no AWS SM tlsstress-phase0/metrics-token)
#   tlsstress_tsu_circulating ausente → TokenLedgerScrapeDown
#   tlsstress_provisioning_stuck > 0  → ProvisioningJobsStuck
#   tlsstress_utxo_overspent     > 0  → TokenLedgerOverspentUtxo (corrupção)

# Estado do RDS quando AppDBDown / RDS* dispara
aws rds describe-db-instances --query 'DBInstances[].DBInstanceStatus'

6. Escalonamento

Escale quando: você não conseguir mitigar nos primeiros 30 minutos de um SEV-1, o raio de impacto estiver crescendo, o fix precisar de uma ação privilegiada que você não possui, ou a integridade de dinheiro/dados estiver em dúvida.

Domínio Sinal Dono / caminho
Provisionamento / saga ProvisioningJobsStuck Dono do orchestrator Temporal; cron reconcile-provisioning; não re-drive às cegas
Banco de dados / RDS AppDBDown, RDS*, DashboardDBDown Plantão de DB; DDL in-VPC via instância NAT (SSM) — abra o ingress temp de SG para sg-02bf33572b96f2855 + leitura temp de secret, remova ambos depois
Dinheiro / ledger TokenLedger*, Stripe* Financeiro + dono do ledger; congele resgates em caso de corrupção
Edge / Cloudflare EdgeVantageDown, CloudflareThreatSpike Dono do edge; WAF / DNS
Backups / DR R2Backup*, R2BucketPublic, R2BucketLockDisabled Dono de DR; R2BucketPublic é um SEV-1 de segurança — feche o acesso público agora

O pager. Alertas critical re-pageiam de hora em hora e fazem @channel no Slack. Para paging garantido de telefone de madrugada, fie o slot pré-construído no alertmanager.yml (ntfy.sh $0, PagerDuty free tier, ou SMTP) — ver RUNBOOK §6.3. Até lá, um critical às 3h depende de alguém olhando o Slack.


7. Comunicação

Audiência Quando Canal Conteúdo
Interno (thread do incidente) Na declaração, depois a cada cadência (§1) #tlsstress-observability SEV, impacto, hipótese atual, hora do próximo update
Cliente(s) afetado(s) SEV-1 com impacto ao cliente (ex.: provisionamento falho), assim que escopado E-mail de @tlsstress.art O que está afetado, que você está nisso, ETA ou próximo update — sem jargão interno, sem culpa
Status page Outage visível ao cliente https://status.tlsstress.art Status de componente em linguagem simples; atualize ao mitigar + resolver

Regras práticas: um canal de incidente (sem threads paralelas); o Incident Commander é dono da cadência de comunicação; nunca prometa uma causa-raiz antes de tê-la; subestime os ETAs. Para casos de provisionamento/reembolso, traga o financeiro cedo.


8. Template de postmortem

Copie isto para docs/postmortems/YYYY-MM-DD-<slug>.md. Sem culpados — foco em sistemas e sinais, nunca em indivíduos.

# Postmortem — <título curto> (<YYYY-MM-DD>)

- **Severidade:** SEV-_  · **Duração:** <detecção→resolução>  · **Impacto ao cliente:** <quem/o quê/$>
- **Detectado por:** <nome do alerta | report do cliente | manual>  · **Atraso de detecção:** <início-do-impacto → primeiro alerta>
- **Incident Commander:** <nome>  · **Escriba:** <nome>

## Resumo
<2–3 frases: o que quebrou, quem afetou, como foi resolvido.>

## Linha do tempo (UTC)
| Hora | Evento |
|---|---|
| 03:28 | <ex.: secret-mestre do RDS rotacionado> |
| ...   | <primeiro alerta / page> |
| ...   | <mitigação aplicada> |
| ...   | <resolvido> |

## Causa-raiz
<A cadeia real de causalidade. Use "5 porquês". Distinga o gatilho da condição
subjacente que o tornou possível.>

## Detecção
<O alerta certo disparou? Quão rápido? Se a detecção atrasou (o caso ~33h-cego de
2026-06-16: /api/health raso, 200 gracioso, sem alerta de DB), esse gap é em si um
item de ação.>

## Resolução & recuperação
<O que estancou o sangramento. Os dados foram reconciliados (jobs presos limpos,
ledger consistente, backlog de webhook drenado)?>

## O que foi bem / o que foi mal
- Foi bem: <ex.: self-heal redeployou automaticamente>
- Foi mal: <ex.: sem pager de telefone → 4h para reconhecer>

## Itens de ação (dono · prazo · acompanhamento)
| Ação | Dono | Prazo | Link |
|---|---|---|---|
| <ex.: adicionar alerta de /api/ready profundo> | | | |

## Lições / prevenções
<Que mudança sistêmica previne esta *classe* de incidente — não só esta instância.>

9. Runbooks & referências relacionados