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/healthretorna 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
pytestydocker buildlocalmente 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
- PagerDuty Incident Response Guide — Guía completa de respuesta a incidentes
- Google SRE Book — Postmortem Culture — La biblia de post-mortems
- Atlassian Incident Management — Handbook de incident management
- Render Incident Response — Troubleshooting en Render
- Railway Docs — Observability — Logs y observabilidad en Railway
- Fly.io — Monitoring — Métricas en Fly.io