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 emobservability/cloud/prometheus/alerts.yml,observability/cloud/alertmanager/alertmanager.yml, ek8s/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)¶
- Reconheça o page para o time saber que está assumido (silencia o re-page do pager).
- Leia o alerta. Todo alerta crítico de dinheiro/DB carrega um
runbook_url— siga-o. As anotaçõessummaryedescriptionforam escritas para dizer o que quebrou e a primeira ação. - 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. - Procure um sinal mais alto. Um
criticalmuitas vezes vem comwarnings correlacionados (ex.:RDSConnectionsHigh→AppDBDown). Asinhibit_rulesdo 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 > 0por 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_urldo 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"} == 0por 2m · severity: critical. - Significa: a sonda profunda
/api/ready(que rodaSELECT 1em 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 serviaHTTP 200gracioso do/api/healthraso 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-selfhealjá 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 oDATABASE_URLdo app autentica como sua role não-mestre dedicada (octopus_admin_app/tlsstress_app) — nunca o mestre rotacionadotlsstress_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, oAppDBDowndispara em ~2 min, e oobs-db-selfhealredeploya 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/readyvirar. 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/readydo 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 receiverdeadman→ 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
criticalre-pageiam de hora em hora e fazem@channelno Slack. Para paging garantido de telefone de madrugada, fie o slot pré-construído noalertmanager.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¶
- Operação de observabilidade:
docs/observability/RUNBOOK.md(§6 incidentes, §6.1 DB-inalcançável, §6.2 RDS, §6.3 pager) - Alertas de ledger:
k8s/74-token-ledger-prometheus-rules.yaml - Regras de alerta cloud + roteamento:
observability/cloud/prometheus/alerts.yml,observability/cloud/alertmanager/alertmanager.yml - Alertas do dashboard DUT-side:
observability/prometheus/alerts/web-agent-alerts.yml - DR drill:
docs/runbooks/dr-drill.md· Restauração:docs/runbooks/restore-from-backup.md - Arquitetura:
docs/ARCHITECTURE.md