Módulo 8: Proyecto Integrador — Deployed AI System

5. Runbook Operativo — Qué Hacer Cuando Algo Falla

Descripción

En esta cápsula vas a crear un runbook operativo: un documento con procedimientos paso a paso para responder a incidencias comunes en producción. El servicio cae, el deploy falla, los costes se disparan, la latencia se degrada — para cada escenario, tendrás un procedimiento claro con diagnóstico, resolución, y prevención. Un sistema sin runbook es un sistema que solo tú puedes operar — y eso no escala.

Contexto: Hasta ahora desplegaste y validaste. Pero el deployment no termina cuando la app está online — termina cuando está documentada y operable. El runbook es el artefacto que convierte tu sistema de "funciona mientras estés tú" a "cualquier ingeniero puede operarlo." Es la diferencia entre un prototipo y un sistema de producción.


Por Qué Necesitas un Runbook

Sin runbook vs con runbook

SIN RUNBOOK — Servicio caído a las 3am:
1. Recibes alerta (si configuraste alertas)
2. ¿Qué hago? ¿Dónde veo los logs?
3. Abro la laptop, trato de recordar los comandos
4. Googleo "cómo ver logs en Railway"
5. 20 min después: encuentro el error
6. 30 min después: aplico un fix
7. 45 min total de downtime
8. Si estás de vacaciones: nadie sabe qué hacer

CON RUNBOOK — Servicio caído a las 3am:
1. Recibes alerta
2. Abres el runbook → Sección: "Servicio no responde"
3. Paso 1: Verificar status → railway status
4. Paso 2: Ver logs → railway logs --lines 100
5. Paso 3: Si OOM → restart con más memoria
6. Paso 4: Si API key → verificar secrets
7. 10 min total de downtime
8. Cualquier persona del equipo puede seguir los pasos

La estructura de un runbook

docs/runbook.md
├── Metadata (autores, última actualización, servicios cubiertos)
├── Acceso y herramientas (URLs, credenciales, dashboards)
├── Procedimientos por escenario:
│   ├── Servicio no responde
│   ├── Deploy falla
│   ├── Latencia degradada
│   ├── Costes inesperados
│   ├── API key expirada/inválida
│   └── Rollback de emergencia
├── Escalación (a quién contactar si no puedes resolver)
└── Post-mortem template

Template de Runbook Completo

Metadata y acceso

# Runbook Operativo — [Nombre del Sistema AI]

**Última actualización:** [Fecha]
**Autor:** [Tu nombre]
**Versión:** 1.0

## Información del Sistema

| Item | Valor |
|------|-------|
| URL producción | https://tu-app.railway.app |
| Health check | https://tu-app.railway.app/health |
| Readiness | https://tu-app.railway.app/health/ready |
| Dashboard plataforma | https://railway.app/project/xxx |
| GitHub repo | https://github.com/tu-user/tu-repo |
| CI/CD pipeline | https://github.com/tu-user/tu-repo/actions |
| Monitoring | https://uptimerobot.com/dashboard |

## Credenciales y Acceso

| Servicio | Cómo acceder |
|----------|-------------|
| Railway dashboard | Login con GitHub en railway.app |
| GitHub Actions | Repo → Actions tab |
| OpenAI dashboard | platform.openai.com (cuenta del equipo) |
| UptimeRobot | uptimerobot.com (email del equipo) |

⚠️ Las API keys y secrets están en la plataforma de deployment.
NUNCA se almacenan en el repo ni en este documento.

Procedimiento 1: Servicio No Responde

Síntomas

  • UptimeRobot reporta downtime
  • curl https://tu-app.railway.app/health retorna error o timeout
  • Usuarios reportan que la app no funciona

Diagnóstico

# Paso 1: Confirmar que el servicio está caído
curl -v https://tu-app.railway.app/health
# Si timeout → el container no está corriendo
# Si 502/503 → la plataforma está arriba pero la app no

# Paso 2: Verificar el estado en la plataforma
# Railway
railway status
railway logs --lines 50

# Render
# Dashboard → tu-servicio → Events → ver último deploy/crash

# Fly.io
flyctl status
flyctl logs --lines 50

# Paso 3: Buscar el error en logs
# Errores comunes:
# - "OOM killed" → se quedó sin memoria
# - "ModuleNotFoundError" → dependencia faltante
# - "OPENAI_API_KEY" → variable no configurada
# - "Address already in use" → puerto en conflicto

Resolución

# Si OOM (Out of Memory):
# Opción A: Restart del servicio
railway up --detach  # Railway
flyctl restart       # Fly.io

# Opción B: Incrementar memoria (si el plan lo permite)
# Railway: Settings → Resources → aumentar memoria
# Fly.io: flyctl scale memory 512  # MB

# Si dependencia faltante:
# Verificar que requirements.txt está actualizado
pip freeze > requirements.txt
git add requirements.txt && git commit -m "fix: update dependencies" && git push

# Si variable de entorno faltante:
# Railway
railway variables set OPENAI_API_KEY=sk-...

# Fly.io
flyctl secrets set OPENAI_API_KEY=sk-...

# Si el container no inicia:
# Verificar logs detallados
railway logs --lines 200 | head -50  # Primeras líneas = startup errors

Prevención

  • Configurar alertas de memoria en la plataforma
  • Health checks cada 5 minutos con UptimeRobot
  • Smoke tests post-deploy que verifican inferencia

Procedimiento 2: Deploy Falla

Síntomas

  • GitHub Actions pipeline muestra ❌ en el job de deploy
  • La plataforma muestra "Deploy failed" en el dashboard
  • El servicio sigue corriendo con la versión anterior

Diagnóstico

# Paso 1: Ver qué job falló en GitHub Actions
# GitHub → Actions → último workflow run → ver logs del job que falló

# Paso 2: Si falló en "test"
# Los tests no pasan. El deploy no se ejecutó (correcto).
# Ver el output de pytest para identificar el test que falla.

# Paso 3: Si falló en "build"
# El Docker image no se construye.
# Errores comunes:
# - pip install falla → dependencia con versión incompatible
# - COPY falla → archivo referenciado no existe
# - Dockerfile syntax error

# Paso 4: Si falló en "deploy"
# El trigger de deploy no funcionó.
# - Token/secret expirado → regenerar en la plataforma
# - Deploy hook URL cambió → actualizar en GitHub Secrets
# - La plataforma tiene un incident → verificar status page

# Paso 5: Si falló en "validate"
# El deploy se hizo pero el sistema no funciona.
# ESTO ES UN PROBLEMA — necesitas decidir si hacer rollback.

Resolución

# Si el test falla:
# 1. Revisar el error en el log de GitHub Actions
# 2. Reproducir localmente: pytest tests/ -v
# 3. Fixear el test o el código
# 4. Push del fix → el pipeline re-ejecuta

# Si el build falla:
# 1. Reproducir localmente: docker build -t test .
# 2. Fixear el Dockerfile o requirements.txt
# 3. Push del fix

# Si el deploy trigger falla:
# 1. Verificar que el secret/token está configurado
# 2. Verificar que no expiró
# Railway: railway login → railway whoami
# Fly.io: flyctl auth token
# 3. Regenerar si expiró y actualizar en GitHub Secrets

# Si la validación falla (deploy se hizo pero no funciona):
# ROLLBACK INMEDIATO:
# Opción A: Revert del commit
git revert HEAD
git push

# Opción B: Deploy manual de versión anterior
# Railway
railway rollback

# Opción C: GitHub Actions → Rollback workflow → Run

Prevención

  • Ejecutar pytest y docker build localmente antes de push
  • Renovar tokens de plataforma antes de que expiren
  • Tener workflow de rollback listo para emergencias

Procedimiento 3: Latencia Degradada

Síntomas

  • Smoke tests reportan latencia > target (ej: > 5s)
  • Usuarios reportan que la app "va lenta"
  • Métricas de latencia muestran incremento progresivo

Diagnóstico

# Paso 1: Medir latencia actual
time curl -s -X POST https://tu-app.railway.app/api/inference \
    -H "Content-Type: application/json" \
    -d '{"prompt": "Say hello"}'

# Paso 2: Identificar la causa
# A. ¿Es la plataforma?
time curl -s https://tu-app.railway.app/health
# Si /health tarda > 500ms → problema de plataforma o red

# B. ¿Es la API de OpenAI?
time curl -s https://api.openai.com/v1/models \
    -H "Authorization: Bearer $OPENAI_API_KEY"
# Si tarda > 2s → OpenAI está lento, no es tu culpa

# C. ¿Es tu código?
# Verificar si hay memory pressure
# Railway: dashboard → Metrics → Memory usage
# Si está cerca del límite → la app está swapping

Resolución

# Si es cold start (free tier):
# Las plataformas con free tier duermen la app después de inactividad.
# Primer request después de dormir = 5-30s de cold start.
# Solución: Upgrade a plan pago, o aceptar el cold start.

# Si es memory pressure:
# Opción A: Optimizar uso de memoria
# - Reducir batch sizes
# - Usar streaming en vez de cargar toda la respuesta en RAM
# - Limitar tamaño de context window

# Opción B: Escalar verticalmente
# Railway: Settings → Resources → más RAM
# Fly.io: flyctl scale memory 1024

# Si es la API de OpenAI:
# No hay mucho que hacer. Opciones:
# - Implementar caching (Redis) para responses frecuentes
# - Reducir tokens (prompts más cortos)
# - Cambiar a modelo más rápido (gpt-4o-mini vs gpt-4o)

# Si es un bug en tu código:
# Profiling básico:
import time

@app.post("/api/inference")
async def inference(request: InferenceRequest):
    t0 = time.time()
    # ... preprocessing
    t1 = time.time()
    # ... LLM call
    t2 = time.time()
    # ... postprocessing
    t3 = time.time()

    timings = {
        "preprocessing_ms": (t1-t0)*1000,
        "llm_call_ms": (t2-t1)*1000,
        "postprocessing_ms": (t3-t2)*1000,
        "total_ms": (t3-t0)*1000,
    }
    # Log timings para identificar el bottleneck

Prevención

  • Establecer performance baselines (cápsula 07)
  • Monitorear latencia con smoke tests periódicos
  • Implementar caching para requests repetitivos

Procedimiento 4: Costes Inesperados

Síntomas

  • Factura de la plataforma más alta de lo esperado
  • Factura de OpenAI más alta de lo esperado
  • Alertas de spending (si configuradas)

Diagnóstico

# Paso 1: Identificar qué servicio genera el coste
# Plataforma: dashboard → Billing → Usage breakdown
# OpenAI: platform.openai.com → Usage → ver por día

# Paso 2: Analizar el patrón
# ¿El tráfico incrementó legítimamente?
# → Más usuarios = más costes (esperado)

# ¿Hay tráfico inesperado?
# → Posible bot, scraping, o DDoS

# ¿Un prompt genera muchos tokens?
# → Un bug que envía contexts muy largos

# Paso 3: Verificar tráfico en logs
# Railway
railway logs --lines 500 | grep "POST /api/inference" | wc -l
# ¿Cuántos requests en las últimas horas?

Resolución

# Si es tráfico excesivo → implementar rate limiting
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/inference")
@limiter.limit("10/minute")  # 10 requests por minuto por IP
async def inference(request: Request, body: InferenceRequest):
    ...

# Si es un prompt bug → verificar token count
from tiktoken import encoding_for_model

enc = encoding_for_model("gpt-4o-mini")

@app.post("/api/inference")
async def inference(body: InferenceRequest):
    tokens = len(enc.encode(body.prompt))
    if tokens > 2000:
        raise HTTPException(400, "Prompt too long")
    ...
# Si necesitas parar el servicio de emergencia
# Railway
railway down

# Fly.io
flyctl scale count 0

# Render: Dashboard → Settings → Suspend service

Prevención

  • Configurar spending alerts en OpenAI (Settings → Limits)
  • Implementar rate limiting desde el día 1
  • Establecer cost baseline y revisar semanalmente

Procedimiento 5: Rollback de Emergencia

Cuándo activar

ACTIVAR ROLLBACK SI:
├── Health check falla post-deploy y no se recupera en 5 min
├── Error rate > 50% en producción
├── Los smoke tests de inferencia fallan consistentemente
└── El sistema produce respuestas incorrectas o peligrosas

NO ACTIVAR ROLLBACK SI:
├── Solo un endpoint minor falla, el resto funciona
├── La latencia subió pero el servicio responde
├── Es un issue cosmético (formato de respuesta diferente)
└── El error estaba antes del deploy (no es regresión)

Procedimiento paso a paso

# === PASO 1: CONFIRMAR QUE NECESITAS ROLLBACK ===
# Verificar health
curl -s https://tu-app.railway.app/health
# Verificar inferencia
curl -s -X POST https://tu-app.railway.app/api/inference \
    -H "Content-Type: application/json" \
    -d '{"prompt": "Test"}'

# === PASO 2: IDENTIFICAR ÚLTIMA VERSIÓN BUENA ===
git log --oneline -10
# Identifica el commit antes del deploy que rompió

# === PASO 3: EJECUTAR ROLLBACK ===

# Opción A: GitHub Actions (si tienes workflow de rollback)
# GitHub → Actions → Rollback → Run workflow
# Input: SHA del commit bueno

# Opción B: Manual desde la plataforma
# Railway
railway rollback

# Fly.io
flyctl releases
# Identifica la release buena
flyctl deploy --image registry.fly.io/tu-app:release-N

# Render
# Dashboard → Manual Deploy → selecciona commit anterior

# Opción C: Git revert
git revert HEAD  # Revierte el último commit
git push main    # Triggerea nuevo deploy

# === PASO 4: VERIFICAR ROLLBACK ===
sleep 60
curl -s https://tu-app.railway.app/health
python scripts/smoke_test.py https://tu-app.railway.app

# === PASO 5: COMUNICAR ===
# Notifica al equipo:
# "Rollback ejecutado a [commit]. Causa: [descripción].
#  Servicio restaurado a las [hora]. Investigando root cause."

# === PASO 6: POST-MORTEM ===
# Crear issue en GitHub con template de post-mortem (ver abajo)

Template de Post-Mortem

# Post-Mortem: [Título del incidente]

**Fecha:** [Fecha del incidente]
**Duración:** [Tiempo de downtime]
**Severidad:** [Alta/Media/Baja]
**Autor:** [Quién escribe el post-mortem]

## Timeline

| Hora | Evento |
|------|--------|
| HH:MM | Deploy ejecutado (commit abc123) |
| HH:MM | Alerta de UptimeRobot: servicio caído |
| HH:MM | Ingeniero investiga, identifica causa |
| HH:MM | Rollback ejecutado |
| HH:MM | Servicio restaurado, smoke tests pasan |

## Causa Raíz

[Descripción técnica de qué causó el incidente]

## Impacto

- Usuarios afectados: [número o descripción]
- Duración del downtime: [minutos]
- Requests fallidos: [estimación]

## Qué Salió Bien

- [El monitoreo detectó el problema en X minutos]
- [El runbook tenía el procedimiento correcto]
- [El rollback funcionó]

## Qué Salió Mal

- [No se detectó en los tests]
- [El deploy se hizo sin smoke tests]
- [No teníamos runbook para este escenario]

## Action Items

- [ ] [Acción 1 — quién — fecha límite]
- [ ] [Acción 2 — quién — fecha límite]
- [ ] [Acción 3 — quién — fecha límite]

Troubleshooting

Problema 1: "No tengo acceso a la plataforma cuando hay un incidente"

Causa: Solo una persona tiene las credenciales.

Solución: Documentar en el runbook cómo acceder. Usar cuentas de equipo donde sea posible. En Railway/Render/Fly.io, invitar a colaboradores al proyecto. Nunca depender de una sola persona.

Problema 2: "El runbook tiene procedimientos pero no sé cuál aplicar"

Causa: Los síntomas se solapan entre procedimientos.

Solución: Agregar un diagrama de decisión al inicio del runbook:

¿El servicio responde a /health?
├── NO → Procedimiento 1: Servicio no responde
└── SÍ → ¿La inferencia funciona?
    ├── NO → Procedimiento 2 ó 5 (verificar si fue post-deploy)
    └── SÍ → ¿La latencia es aceptable?
        ├── NO → Procedimiento 3: Latencia degradada
        └── SÍ → ¿Los costes son normales?
            ├── NO → Procedimiento 4: Costes inesperados
            └── SÍ → El sistema está saludable ✅

Problema 3: "Después del rollback, necesito deployar el fix pero no sé si es seguro"

Causa: Miedo a que el fix también rompa algo.

Solución:

# 1. Verifica el fix localmente COMPLETAMENTE
docker build -t test . && docker run -d --name test -p 8000:8000 \
    -e OPENAI_API_KEY=$OPENAI_API_KEY test
python scripts/smoke_test.py http://localhost:8000
docker stop test && docker rm test

# 2. Si tienes staging, despliega ahí primero
# 3. Si no tienes staging, merge el fix con confidence
# 4. Monitora el deploy con más atención de lo normal

Ejercicios Prácticos

Ejercicio 1: Crea tu runbook base

Crea el archivo docs/runbook.md con la metadata, información del sistema, y al menos 3 procedimientos.

Ver solución
# Runbook Operativo — Mi AI System

**Última actualización:** 2026-03-08
**Autor:** [Tu nombre]
**Versión:** 1.0

## Información del Sistema

| Item | Valor |
|------|-------|
| URL producción | https://mi-app.railway.app |
| Health check | https://mi-app.railway.app/health |
| Dashboard | https://railway.app/project/xxx |
| Repo | https://github.com/user/repo |
| CI/CD | https://github.com/user/repo/actions |
| Monitoring | https://uptimerobot.com |

## Procedimientos

### 1. Servicio no responde
[Copiar procedimiento 1 adaptado a tu plataforma]

### 2. Deploy falla
[Copiar procedimiento 2 adaptado]

### 3. Rollback de emergencia
[Copiar procedimiento 5 adaptado]

El runbook mínimo viable tiene: metadata + accesos + 3 procedimientos. Crece con el tiempo.

Ejercicio 2: Simula una incidencia

Rompe algo intencionalmente (cambia una env var a un valor inválido), detecta el problema con tu health check, y sigue tu runbook para resolverlo.

Ver solución
# 1. Romper: Cambiar la API key a un valor inválido
# Railway: railway variables set OPENAI_API_KEY=invalid-key

# 2. Esperar a que el servicio se actualice (~30s)
sleep 30

# 3. Detectar: Health check
curl -s https://mi-app.railway.app/health/ready
# Debería mostrar openai: "error"

# 4. Diagnosticar: Seguir Procedimiento 1 del runbook
# "Verificar logs" → railway logs --lines 50
# Debería mostrar error de autenticación de OpenAI

# 5. Resolver: Restaurar la API key
# railway variables set OPENAI_API_KEY=sk-la-key-correcta

# 6. Verificar: Smoke tests
sleep 30
python scripts/smoke_test.py https://mi-app.railway.app
# Debería pasar todos los tests

Este ejercicio valida que tu flujo de detección → diagnóstico → resolución funciona.

Ejercicio 3: Crea el diagrama de decisión

Dibuja el diagrama de decisión que conecta síntomas con procedimientos (como el mostrado en troubleshooting).

Ver solución
## Diagrama de Decisión de Incidencias

¿El servicio responde a /health?
│
├── NO → ¿Hubo un deploy reciente (<30 min)?
│   ├── SÍ → Rollback inmediato (Proc. 5)
│   └── NO → Verificar plataforma (Proc. 1)
│
└── SÍ → ¿/health/ready muestra "ready"?
    │
    ├── NO → ¿Qué dependencia falla?
    │   ├── OpenAI → Verificar API key (Proc. 1 - secrets)
    │   └── Otra → Verificar servicio específico
    │
    └── SÍ → ¿La inferencia responde correctamente?
        │
        ├── NO → ¿Fue post-deploy?
        │   ├── SÍ → Rollback (Proc. 5)
        │   └── NO → Debug código (Proc. 3)
        │
        └── SÍ → ¿Latencia aceptable?
            ├── NO → Proc. 3: Latencia
            └── SÍ → Sistema saludable ✅

Agrega este diagrama al inicio de tu runbook para que sea lo primero que veas durante un incidente.

Ejercicio 4: Escribe un post-mortem de práctica

Usando el template de post-mortem, documenta la incidencia simulada del Ejercicio 2.

Ver solución
# Post-Mortem: API Key Inválida en Producción

**Fecha:** 2026-03-08
**Duración:** ~5 minutos
**Severidad:** Alta (inferencia no funcionaba)
**Autor:** [Tu nombre]

## Timeline

| Hora | Evento |
|------|--------|
| 14:00 | API key cambiada a valor inválido (simulado) |
| 14:01 | Health/ready check muestra OpenAI como "error" |
| 14:02 | Smoke test de inferencia falla |
| 14:03 | Causa identificada: API key inválida en logs |
| 14:04 | API key restaurada |
| 14:05 | Smoke tests pasan, servicio restaurado |

## Causa Raíz
La API key de OpenAI fue cambiada a un valor inválido.

## Impacto
- Inferencia no disponible por ~5 minutos
- Health check básico seguía respondiendo 200 (misleading)

## Action Items
- [ ] Mejorar health check para fallar si OpenAI no conecta
- [ ] Agregar alerta específica para errores de autenticación
- [ ] Documentar procedimiento de rotación de API keys

Resumen

  • Un runbook convierte tu sistema de "solo tú puedes operarlo" a "cualquiera puede operarlo"
  • Incluye: metadata del sistema, accesos, y procedimientos paso a paso para incidencias comunes
  • Los 5 procedimientos esenciales: servicio caído, deploy fallido, latencia degradada, costes inesperados, rollback
  • Cada procedimiento tiene: síntomas, diagnóstico, resolución, y prevención
  • El diagrama de decisión conecta síntomas con procedimientos — es lo primero que consultas durante un incidente
  • Los post-mortems documentan incidencias para aprender y prevenir recurrencias
  • El runbook es un documento vivo — actualízalo después de cada incidencia real

Recursos Adicionales

  1. PagerDuty Incident Response Guide — Guía completa de respuesta a incidentes
  2. Google SRE Book — Postmortem Culture — La biblia de post-mortems
  3. Atlassian Incident Management — Handbook de incident management
  4. Render Incident Response — Troubleshooting en Render
  5. Railway Docs — Observability — Logs y observabilidad en Railway
  6. Fly.io — Monitoring — Métricas en Fly.io