Skip to content

Runbook de Respuesta a Incidentes

El procedimiento único y autoritativo para gestionar incidentes en producción en tlsstress.art — desde el primer page hasta el postmortem. Acompaña al manual de operación de observabilidad (docs/observability/RUNBOOK.md) y a las reglas de alerta en observability/cloud/prometheus/alerts.yml, observability/cloud/alertmanager/alertmanager.yml, y k8s/74-token-ledger-prometheus-rules.yaml.

Alcance. Este runbook cubre la ruta de dinero / aprovisionamiento / datos: registro del cliente → pago Stripe → aprovisionamiento del tenant → ledger de la token-economy → dashboard del operador on-prem, además de la plataforma que los observa. No reemplaza los chaos drills por componente (chaos-*.md) ni el DR drill (dr-drill.md); te dice cómo clasificar, triar, mitigar, comunicar y aprender de un incidente en todos ellos.

Regla de oro. Mitiga primero el impacto al cliente, encuentra la causa raíz después. Un cliente que pagó sin tenant, un ledger corrupto o una base de datos que la app no alcanza son todos SEV-1 hasta que se demuestre lo contrario.


1. Clasificación de severidad

La severidad se fija por el impacto al cliente/financiero, no por qué componente se rompió. Fíjala en los primeros 5 minutos; puede subir o bajar conforme descubres más.

Severidad Definición Ejemplos ¿Page? Cadencia de comunicación
SEV-1 Dinero perdido/en riesgo, corrupción de datos, o un cliente pagado roto de forma material ProvisioningJobsStuck (pagado, sin tenant), TokenLedgerOverspentUtxo (corrupción de ledger), AppDBDown (DB inalcanzable), RDSStorageCritical, R2BucketPublic Sí (pager critical) Cada 30 min
SEV-2 Degradado pero sin dinero perdido aún; indicador anticipado de un SEV-1 RDSConnectionsHigh, RDSStorageLow, TokenLedgerStaleBillingWebhooks, StripeFailedPaymentsSpike, DashboardDBDown Sí si es de madrugada Cada 2 h
SEV-3 Localizado / cosmético / vantage única; sin impacto al cliente JourneyVantageChallenged, CloudflareThreatSpike (info), RUMPoorLCP, una vantage sintética inestable No En la resolución

La severidad también es una etiqueta en la alerta. severity="critical" en Prometheus enruta al receiver pager (re-page cada hora + Slack @channel); warning/info caen en el firehose #tlsstress-observability. Ver alertmanager.yml.


2. El flujo de guardia (triaje → mitigación → resolución → postmortem)

flowchart TD
    A([Alerta dispara / reporte recibido]) --> B{Reconocer<br/>en 5 min}
    B -->|pager critical| C[Abrir el incidente:<br/>fijar SEV, iniciar hilo]
    B -->|firehose Slack| C
    C --> D{Clasificar severidad<br/>por impacto dinero/cliente}
    D -->|SEV-1| E[Declarar incidente:<br/>IC + escriba + comunicación]
    D -->|SEV-2/3| F[Respondedor único]
    E --> G[TRIAJE: leer el runbook_url<br/>de la alerta + dashboards golden]
    F --> G
    G --> H{¿Causa raíz conocida<br/>o mitigación obvia?}
    H -->|No| I[Escalar al dueño del dominio<br/>ver §6 escalamiento]
    I --> G
    H -->|Sí| J[MITIGAR: detener el sangrado<br/>self-heal / rollback / escalar]
    J --> K{¿Impacto al cliente<br/>detenido?}
    K -->|No| L[Subir la severidad,<br/>ampliar el escalamiento]
    L --> J
    K -->|Sí| M[RESOLVER: verificar alerta limpia,<br/>datos reconciliados, sin backlog]
    M --> N{¿SEV-1 o SEV-2?}
    N -->|Sí| O[POSTMORTEM<br/>en 5 días hábiles]
    N -->|No| P([Cerrar incidente])
    O --> Q[Ítems de acción seguidos<br/>hasta su conclusión]
    Q --> P

2.1 Triaje (primeros 10 minutos)

  1. Reconoce el page para que el equipo sepa que está asumido (silencia el re-page del pager).
  2. Lee la alerta. Toda alerta crítica de dinero/DB lleva un runbook_url — síguelo. Las anotaciones summary y description están escritas para decirte qué se rompió y la primera acción.
  3. Abre los dashboards golden (…/grafana): cloud-overview, business-revenue, aws-infra, y el cockpit del operador para incidentes DUT-side. Confirma que la alerta es real (no una sola vantage inestable) y halla el radio de impacto.
  4. Busca una señal más fuerte. Un critical a menudo viene con warnings correlacionados (p. ej. RDSConnectionsHighAppDBDown). Las inhibit_rules del Alertmanager suprimen los warnings derivados, así que confía en el critical.

2.2 Mitigar

Detén el impacto al cliente con la palanca segura más rápida, aunque sea temporal: self-heal (dejar que obs-db-selfheal redespliegue), rollback del último deploy, escalar el recurso, o poner feature-flag en la ruta rota. Documenta cada acción en el hilo — el postmortem necesita la línea de tiempo.

2.3 Resolver

Un incidente solo está resuelto cuando todas estas condiciones se cumplen: - La alerta que disparó se limpió (y se mantiene limpia durante un repeat_interval). - El impacto al cliente desapareció (re-ejecuta el sintético / journey relevante). - Los datos están reconciliados — ningún job de aprovisionamiento atascado, ninguna UTXO over-spent, ningún backlog de webhook de billing sin procesar. - La mitigación es duradera, o hay un follow-up registrado para hacerla duradera.

2.4 Postmortem

Todo SEV-1 y SEV-2 recibe un postmortem sin culpables en 5 días hábiles (plantilla en §8). Los SEV-3 reciben una línea en el hilo del incidente.


3. Alertas clave de dinero / aprovisionamiento / datos

Son las alertas que, al dispararse, significan que un cliente está o puede estar pagando por nada — o que el ledger financiero está mal. Trátalas como SEV-1 a menos que el triaje pruebe lo contrario. Fuente de verdad de las alertas de ledger: k8s/74-token-ledger-prometheus-rules.yaml.

3.1 ProvisioningJobsStuck — cliente pagado, sin tenant (SEV-1)

  • Expr: tlsstress_provisioning_stuck > 0 por 30m · severity: critical.
  • Significa: uno o más jobs de aprovisionamiento están atascados (>15 min sin progreso, o ≥8 intentos). Un cliente que pagó no recibió su tenant. Es la peor clase de incidente: pérdida silenciosa de dinero con un cliente insatisfecho.
  • Primeras acciones:
  • Lee el runbook_url de la alerta (RUNBOOK #provisioning-stuck).
  • Revisa el orchestrator Temporal para el/los workflow(s) que fallaron y el cron reconcile-provisioning — ¿está corriendo y progresando?
  • Identifica al/los cliente(s) afectado(s) y el paso de la saga que falló. No re-drive a ciegas: un re-drive ingenuo puede acuñar un segundo deployment dpl_ o poner en cero una quota pagada. Re-drive solo el paso que falló.
  • Si no puedes aprovisionar dentro del SLA, comunícate con el cliente (§7) y prepara una ruta de reembolso/crédito con finanzas.

3.2 AppDBDown — la app no alcanza su base de datos (SEV-1)

  • Expr: probe_success{job="blackbox-ready"} == 0 por 2m · severity: critical.
  • Significa: la sonda profunda /api/ready (que ejecuta SELECT 1 en cada pool) está fallando → la app está arriba pero su DB es inalcanzable, o la app está caída. Es la alerta que la caída del admin del 2026-06-16 no tenía: la app servía HTTP 200 amable desde el /api/health superficial mientras su DB estaba inalcanzable, así que nada disparó por ~33 horas.
  • Primeras acciones (RCA completo en RUNBOOK #61-db-unreachable):
  • curl -s https://<app>.tlsstress.art/api/ready | jq — ve qué check de DB está ok:false.
  • El obs-db-selfheal ya redespliega el servicio App Runner en un 503 (re-obtiene el secret + reconstruye el pool). Observa el deploy; si recupera, terminaste.
  • ¿Persiste? Verifica que el RDS esté available (aws rds describe-db-instances) y que el DATABASE_URL de la app autentique como su rol no-maestro dedicado (octopus_admin_app / tlsstress_app) — nunca el maestro rotado tlsstress_admin. RDS rota el secret-maestro gestionado cada ~7 días; una app apuntada al maestro se rompe en cada rotación.

La lección del 2026-06-16, codificada. Las apps ahora conectan como roles no-maestros dedicados, el /api/ready es una sonda profunda, el AppDBDown dispara en ~2 min, y el obs-db-selfheal redespliega automáticamente. Si ves "DB indisponible" en una app pero el blackbox está verde, estás viendo un health check superficial — sondea /api/ready, no /api/health.

3.3 Integridad del ledger — las alertas de la token-economy (SEV mixta)

El ledger de la token-economy es la fuente de verdad financiera. Sus alertas viven en k8s/74-token-ledger-prometheus-rules.yaml y hacen scrape de app.tlsstress.art/api/metrics (bearer del AWS Secrets Manager tlsstress-phase0/metrics-token).

Alerta Expr Sev Significa + primera acción
TokenLedgerOverspentUtxo tlsstress_utxo_overspent > 0 (1m) critical Corrupción de ledger — una nota con spent_amount > amount. El CHECK de la base lo hace imposible, así que un valor no-cero es un bug profundo. Congela los rescates, investiga de inmediato, no dejes que se propague.
ProvisioningJobsStuck tlsstress_provisioning_stuck > 0 (30m) critical Ver §3.1 — cliente pagado sin tenant.
TokenLedgerScrapeDown absent(tlsstress_tsu_circulating) (10m) warning Sin métricas del ledger por 10m — METRICS_TOKEN rotado sin actualizar el secret montado, o /api/metrics caído. Quedas ciego a todas las alertas de ledger hasta arreglarlo.
TokenLedgerStaleBillingWebhooks tlsstress_webhooks_stale_billing > 0 (30m) warning Webhooks Stripe de billing atascados no-terminales >1h — un crédito puede no haberse aplicado (cliente pagó, saldo no recargado). Revisa el procesador de webhooks + el dashboard Stripe.
TokenLedgerStaleActiveTickets tlsstress_tickets_stale_active > 0 (30m) warning Tickets ACTIVE más allá de expiry+24h — el sweep tlsstress-expire-tickets-hourly está enfermo.
TokenLedgerOutboxBacklog tlsstress_outbox_pending > 100 (30m) warning Outbox de webhooks acumulándose (>100 no entregados) — revisa deliver-webhooks + salud de los receptores.
TokenLedgerUsageWithoutLiveness tlsstress_usage_without_liveness > 0 (1h) warning Una licencia reportó uso en 24h sin heartbeat — posible tamper de token / clock-skew / replay de token copiado. El cobro lo cubre L2; esta es una señal de integridad.

3.4 Saturación de RDS — page antes del 503 (SEV-2 → SEV-1)

El AppDBDown solo dispara cuando el DB ya está totalmente inalcanzable. El exporter aws-rds-metrics page en la saturación primero (umbrales ajustados para db.t4g.micro, ~112 conexiones máx, 1 GB RAM — re-ajusta al redimensionar):

  • RDSConnectionsHigh (> 90, warning) — agotamiento de pool acercándose; nuevas conexiones rechazan antes de que /api/ready cambie. Revisa PgBouncer / fuga de conexión.
  • RDSStorageLow (< 2 GB, warning) / RDSStorageCritical (< 1 GB, critical) — RDS detiene las escrituras cuando el storage se agota. Aumenta el storage asignado ahora: aws rds modify-db-instance --allocated-storage.
  • RDSCPUHigh (> 85%, warning) / RDSMemoryLow (< 100 MB, warning) — presión de carga / riesgo de OOM.
  • RDSMetricsBlind (rds_metrics_scrape_ok == 0, warning) — el exporter no puede leer CloudWatch (key RO expirada / IAM / throttling). Todas las alertas RDS de arriba quedan silenciosas hasta que esto recupere.

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

Estos disparan en el Prometheus on-prem, no en la caja cloud (observability/prometheus/alerts/web-agent-alerts.yml):

  • DashboardDown (up{job="dashboard"} == 0, critical) — el proceso/scrape del cockpit del operador es inalcanzable. Según CLAUDE.md, el dashboard es la única interfaz del operador, así que esto ciega al operador.
  • DashboardDBDown (dashboard_db_up == 0, critical) — el cockpit está arriba pero su Postgres es inalcanzable (la clase de fallo de DB silencioso del 2026-06-16, reflejada DUT-side). El cockpit muestra datos congelados y un badge rojo en vivo "sem dados (DB)". Revisa el PgBouncer / Postgres on-prem; el /api/ready del dashboard es la sonda profunda.

3.6 Señales de negocio / pago (SEV-2/3)

Del grupo business de la cloud (poller RO de Stripe): - StripeFailedPaymentsSpike (> 5 hoy, warning) — indicador anticipado de un problema de tarjeta/procesador o fraude. - StripeOpenDisputes (> 0, warning) — los chargebacks necesitan respuesta humana dentro de la ventana de Stripe.


4. El dead-man's-switch (quién vigila al vigía)

Dos meta-alertas mantienen honesta a la plataforma:

  • Watchdog (vector(1), siempre disparando) enruta al receiver deadman → un heartbeat externo en healthchecks.io. Cuando el heartbeat deja de llegar, el servicio externo te page — es decir, cuando todo el stack de obs está caído y ya no puede alertar. Es la única alerta que quieres mantener disparando.
  • ObsComponentDown (up{job=~"grafana|loki|tempo|vector|prometheus|…"} == 0, critical) — un componente de obs cayó; la plataforma puede estar parcialmente ciega.

Si dejas de recibir cualquier alerta y la caja se ve bien, sospecha de la ruta de alerta (webhook Slack rotado, Alertmanager atascado) antes de asumir que todo está bien.


5. Chuleta de primera respuesta

# Caja de obs cloud (Hetzner)
ssh -i ~/.ssh/tlsstress_f2_hetzner root@89.167.3.1
cd /opt/obs && docker compose ps                 # ¿el stack está sano?
docker compose logs -f alertmanager              # ¿la alerta fluye?

# ¿El DB de una app es realmente alcanzable? (sonda profunda — lección del 2026-06-16)
curl -s https://app.tlsstress.art/api/ready   | jq
curl -s https://admin.tlsstress.art/api/ready | jq

# ¿Métricas del ledger vivas? (bearer en AWS SM tlsstress-phase0/metrics-token)
#   tlsstress_tsu_circulating ausente → TokenLedgerScrapeDown
#   tlsstress_provisioning_stuck > 0  → ProvisioningJobsStuck
#   tlsstress_utxo_overspent     > 0  → TokenLedgerOverspentUtxo (corrupción)

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

6. Escalamiento

Escala cuando: no puedas mitigar en los primeros 30 minutos de un SEV-1, el radio de impacto esté creciendo, el fix necesite una acción privilegiada que no posees, o la integridad de dinero/datos esté en duda.

Dominio Señal Dueño / ruta
Aprovisionamiento / saga ProvisioningJobsStuck Dueño del orchestrator Temporal; cron reconcile-provisioning; no re-drive a ciegas
Base de datos / RDS AppDBDown, RDS*, DashboardDBDown Guardia de DB; DDL in-VPC vía instancia NAT (SSM) — abre el ingress temp de SG hacia sg-02bf33572b96f2855 + lectura temp de secret, quita ambos después
Dinero / ledger TokenLedger*, Stripe* Finanzas + dueño del ledger; congela rescates en caso de corrupción
Edge / Cloudflare EdgeVantageDown, CloudflareThreatSpike Dueño del edge; WAF / DNS
Backups / DR R2Backup*, R2BucketPublic, R2BucketLockDisabled Dueño de DR; R2BucketPublic es un SEV-1 de seguridad — cierra el acceso público ahora

El pager. Las alertas critical re-pagean cada hora y hacen @channel en Slack. Para paging garantizado de teléfono de madrugada, cablea el slot pre-construido en alertmanager.yml (ntfy.sh $0, PagerDuty free tier, o SMTP) — ver RUNBOOK §6.3. Hasta entonces, un critical a las 3 a.m. depende de que alguien esté mirando Slack.


7. Comunicación

Audiencia Cuándo Canal Contenido
Interno (hilo del incidente) En la declaración, luego en cada cadencia (§1) #tlsstress-observability SEV, impacto, hipótesis actual, hora del próximo update
Cliente(s) afectado(s) SEV-1 con impacto al cliente (p. ej. aprovisionamiento fallido), en cuanto esté acotado E-mail de @tlsstress.art Qué está afectado, que estás en ello, ETA o próximo update — sin jerga interna, sin culpa
Status page Outage visible al cliente https://status.tlsstress.art Estado de componente en lenguaje simple; actualiza al mitigar + resolver

Reglas prácticas: un canal de incidente (sin hilos paralelos); el Incident Commander es dueño de la cadencia de comunicación; nunca prometas una causa raíz antes de tenerla; subestima los ETAs. Para casos de aprovisionamiento/reembolso, involucra a finanzas temprano.


8. Plantilla de postmortem

Copia esto en docs/postmortems/YYYY-MM-DD-<slug>.md. Sin culpables — foco en sistemas y señales, nunca en individuos.

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

- **Severidad:** SEV-_  · **Duración:** <detección→resolución>  · **Impacto al cliente:** <quién/qué/$>
- **Detectado por:** <nombre de la alerta | reporte del cliente | manual>  · **Retraso de detección:** <inicio-del-impacto → primera alerta>
- **Incident Commander:** <nombre>  · **Escriba:** <nombre>

## Resumen
<2–3 frases: qué se rompió, a quién afectó, cómo se resolvió.>

## Línea de tiempo (UTC)
| Hora | Evento |
|---|---|
| 03:28 | <p. ej. secret-maestro del RDS rotado> |
| ...   | <primera alerta / page> |
| ...   | <mitigación aplicada> |
| ...   | <resuelto> |

## Causa raíz
<La cadena real de causalidad. Usa "5 porqués". Distingue el disparador de la
condición subyacente que lo hizo posible.>

## Detección
<¿Disparó la alerta correcta? ¿Qué tan rápido? Si la detección se retrasó (el caso
~33h-ciego del 2026-06-16: /api/health superficial, 200 amable, sin alerta de DB),
ese gap es en sí mismo un ítem de acción.>

## Resolución & recuperación
<Qué detuvo el sangrado. ¿Se reconciliaron los datos (jobs atascados limpiados,
ledger consistente, backlog de webhook drenado)?>

## Qué salió bien / qué salió mal
- Salió bien: <p. ej. self-heal redesplegó automáticamente>
- Salió mal: <p. ej. sin pager de teléfono → 4h para reconocer>

## Ítems de acción (dueño · fecha · seguimiento)
| Acción | Dueño | Fecha | Link |
|---|---|---|---|
| <p. ej. agregar alerta de /api/ready profundo> | | | |

## Lecciones / prevenciones
<Qué cambio sistémico previene esta *clase* de incidente — no solo esta instancia.>

9. Runbooks & referencias relacionados