Módulo 5: Security Scanning y Rollback Inteligente
Diagnóstico Inteligente Post-Rollback
Diagnóstico Inteligente Post-Rollback
Descripción
Esta es la cápsula que cierra el ciclo de resiliencia: cuando el rollback automático se ejecuta (cápsula 04), Claude Code genera un diagnóstico detallado de lo que pasó. Sin diagnóstico, el rollback solo evita el daño inmediato — pero no genera aprendizaje. Con diagnóstico, cada incident se convierte en información estructurada que el equipo puede usar para evitar la próxima.
Aprendes a generar diagnósticos automáticos que: identifican el commit/PR que introdujo el problema, correlacionan métricas con cambios de código, sugieren fix probable, producen un postmortem draft, y notifican al equipo con contexto rico — no solo "rollback executed".
Al terminar, vas a tener un sistema donde un incident en producción se convierte automáticamente en un postmortem draft listo para revisión, con root cause analysis, timeline reconstruido, y plan de acción.
El Diagnóstico que Importa
SIN DIAGNÓSTICO (rollback básico):
🚨 Rollback ejecutado
- Métrica: error_rate
- Valor: 5.2%
- Threshold: 2%
→ El equipo sabe QUE algo se rompió
→ Empieza a investigar desde cero
→ Pierde 30-60 minutos identificando root cause
CON DIAGNÓSTICO (este módulo):
🚨 Rollback ejecutado + Análisis
ROOT CAUSE PROBABLE:
El commit a3b5c8d (PR #847 "Update payment validation")
modificó la función validate_amount() en payment_service.py:42.
El cambio invierte la condición original — antes rechazaba
montos negativos, ahora los permite.
EVIDENCIA:
- Error rate subió 2 minutos después del deploy
- Errors concentrados en el endpoint /api/payments
- Stack traces apuntan a payment_service.validate_amount()
- Diff del commit muestra: `if amount < 0` cambió a `if amount > 0`
FIX SUGERIDO:
Revertir el cambio de la línea 42 a la condición original
`if amount < 0`. Re-aplicar lógica deseada después del deploy
con tests que cubran el caso de monto negativo.
POSTMORTEM DRAFT:
[link al draft generado]
→ El equipo tiene el contexto completo en 5 minutos
→ Decisión informada sobre qué hacer next
→ Postmortem draft listo para review
La diferencia es ~45 minutos de tiempo del equipo por incident. Multiplicalo por 5-10 incidents/año = mucho tiempo recuperado.
Las 5 Piezas del Diagnóstico
1. CAUSE IDENTIFICATION
¿Qué commit/PR introdujo el problema?
- Time correlation: deploy time vs error onset
- Diff analysis: qué cambió en ese PR
- Author + reviewer
2. EVIDENCE TRAIL
¿Qué señales muestran el problema?
- Métricas que cruzaron threshold (con valores)
- Endpoints/paths afectados
- Stack traces relevantes
- Logs de error patterns
3. ROOT CAUSE ANALYSIS
¿Por qué falla el código?
- Lógica errónea en el cambio
- Side effect no anticipado
- Falta de validación
- Race condition introducida
4. FIX SUGGESTION
¿Cómo arreglarlo?
- Revertir el cambio (corto plazo, ya hecho)
- Re-aplicar con corrección (mediano plazo)
- Mejora estructural para prevenir clase de bug (largo plazo)
5. POSTMORTEM DRAFT
Documento listo para review humano
- Timeline reconstruido
- Impact assessment
- Action items propuestos
Cada pieza es valiosa por separado. Juntas convierten "rollback" en "incident learning".
El Workflow Extendido
# Continuación del workflow de la cápsula 04
# Adicional al rollback, agrega diagnóstico
diagnose-and-document:
needs: monitor-and-rollback
if: needs.monitor-and-rollback.outputs.rollback_needed == '1'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install anthropic requests
- name: Generate diagnosis
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
METRICS_API_URL: ${{ secrets.METRICS_API_URL }}
METRICS_API_TOKEN: ${{ secrets.METRICS_API_TOKEN }}
DEPLOY_SHA: ${{ needs.deploy.outputs.new_release }}
PREVIOUS_SHA: ${{ needs.deploy.outputs.previous_release }}
run: python scripts/diagnose_incident.py
- name: Create postmortem issue
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: python scripts/create_postmortem_issue.py
- name: Upload diagnosis
uses: actions/upload-artifact@v4
with:
name: incident-diagnosis
path: |
diagnosis.json
postmortem_draft.md
El Script de Diagnóstico
"""scripts/diagnose_incident.py
Genera diagnóstico completo del incident con context de:
- Commits del deploy
- Métricas durante la degradación
- Logs de error (si accesibles)
"""
import json
import os
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
from anthropic import Anthropic
import requests
def get_deploy_commits(deploy_sha: str, previous_sha: str) -> list[dict]:
"""Obtener commits incluidos en el deploy."""
result = subprocess.run(
["git", "log", f"{previous_sha}..{deploy_sha}",
"--format=%H||%an||%s", "--no-merges"],
capture_output=True, text=True, check=True,
)
commits = []
for line in result.stdout.strip().split("\n"):
if not line:
continue
parts = line.split("||")
if len(parts) >= 3:
commits.append({
"sha": parts[0],
"author": parts[1],
"message": parts[2],
})
return commits
def get_commit_diff(sha: str) -> str:
"""Obtener diff completo de un commit (truncado)."""
result = subprocess.run(
["git", "show", "--stat", "--format=", sha],
capture_output=True, text=True, check=True,
)
stat = result.stdout
# Diff completo (truncado)
diff_result = subprocess.run(
["git", "show", "--format=", sha],
capture_output=True, text=True, check=True,
)
diff = diff_result.stdout[:5000] # truncar para no saturar context
return f"STATS:\n{stat}\n\nDIFF (truncated):\n{diff}"
def get_pr_for_commit(sha: str, repo: str, token: str) -> dict | None:
"""Encontrar PR asociado al commit."""
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
url = f"https://api.github.com/repos/{repo}/commits/{sha}/pulls"
r = requests.get(url, headers=headers)
if r.ok and r.json():
pr = r.json()[0]
return {
"number": pr["number"],
"title": pr["title"],
"url": pr["html_url"],
"body": pr.get("body", "")[:1000],
}
return None
def get_metrics_during_incident(
deploy_time: str,
rollback_time: str,
api_url: str | None,
api_token: str | None,
) -> dict:
"""Obtener métricas durante el periodo del incident."""
if not api_url:
# Mock para demo
return {
"error_rate_max": 5.2,
"error_rate_baseline": 0.5,
"latency_p95_max_ms": 2500,
"latency_p95_baseline_ms": 800,
"affected_endpoints": ["/api/payments", "/api/orders"],
"error_samples": [
"ValueError: Invalid amount",
"ValidationError: amount must be positive",
],
}
# En producción: query a Prometheus/Datadog
# ...
return {}
def generate_diagnosis_with_claude(
commits: list[dict],
pr_info: dict | None,
metrics: dict,
rollback_reason: dict,
) -> dict:
"""Llamar a Claude para análisis estructurado."""
# Construir contexto rico
commits_summary = "\n".join(
f"- {c['sha'][:8]} ({c['author']}): {c['message']}"
for c in commits
)
prompt = f"""Analiza este incident de producción y genera un diagnóstico estructurado.
CONTEXTO DEL INCIDENT:
- Rollback ejecutado por: {rollback_reason.get('description', 'unknown')}
- Métrica disparadora: {rollback_reason.get('metric')}
- Valor: {rollback_reason.get('current_value')}
- Threshold: {rollback_reason.get('threshold')}
COMMITS DEPLOYADOS:
{commits_summary}
{f'PR ASOCIADO: #{pr_info["number"]} — {pr_info["title"]}' if pr_info else 'No hay PR asociado claro'}
{f'Description: {pr_info["body"][:500]}' if pr_info else ''}
DIFF DEL COMMIT MÁS RELEVANTE:
{get_commit_diff(commits[0]['sha']) if commits else 'No commits'}
MÉTRICAS DURANTE INCIDENT:
{json.dumps(metrics, indent=2)}
Genera un diagnóstico JSON con esta estructura:
{{
"root_cause": {{
"likely_commit": "sha del commit responsable",
"explanation": "explicación clara de por qué este cambio causó el problema",
"confidence": "high|medium|low"
}},
"evidence": [
"evidencia 1 (correlación temporal, stack trace, etc.)",
"evidencia 2",
"..."
],
"fix_suggestion": {{
"short_term": "qué hacer ahora (típicamente: revisar el rollback)",
"medium_term": "cómo re-aplicar el cambio sin el bug",
"long_term": "mejora estructural para prevenir esta clase de bug"
}},
"lessons_learned": [
"lección 1 (qué falló en el proceso)",
"lección 2",
"..."
],
"action_items": [
{{"item": "...", "priority": "high|medium|low", "owner": "team|individual"}}
]
}}
Sé específico — usa referencias a archivos, líneas, valores reales.
Devuelve SOLO el JSON.
"""
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-5", # Sonnet para razonamiento profundo
max_tokens=4000,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = "\n".join(text.split("\n")[1:-1])
return json.loads(text)
def generate_postmortem_draft(diagnosis: dict, commits: list, pr_info: dict | None,
rollback_reason: dict, metrics: dict) -> str:
"""Generar postmortem draft en markdown."""
incident_id = datetime.utcnow().strftime("%Y-%m-%d-%H%M")
rc = diagnosis["root_cause"]
fix = diagnosis["fix_suggestion"]
md = f"""# Incident Postmortem: {incident_id}
**Status:** Draft (auto-generated)
**Severity:** Auto-rolled back
## Summary
Auto-rollback ejecutado en producción debido a degradación de **{rollback_reason.get('metric')}** ({rollback_reason.get('current_value')} vs threshold {rollback_reason.get('threshold')}).
Likely root cause: commit `{rc['likely_commit'][:8]}`{f' (PR #{pr_info["number"]})' if pr_info else ''}
## Timeline
- **T+0**: Deploy a producción ({commits[0]['sha'][:8] if commits else 'unknown'})
- **T+~2min**: Métricas empiezan a degradar
- **T+~5min**: Threshold cruzado, rollback automático ejecutado
- **T+~7min**: Servicio estabilizado en versión anterior
## Impact
- Affected endpoints: {', '.join(metrics.get('affected_endpoints', []))}
- Error rate peak: {metrics.get('error_rate_max', 'unknown')}% (baseline: {metrics.get('error_rate_baseline', 'unknown')}%)
- Latency p95 peak: {metrics.get('latency_p95_max_ms', 'unknown')}ms (baseline: {metrics.get('latency_p95_baseline_ms', 'unknown')}ms)
- Estimated user impact: [TBD by on-call]
## Root Cause
**{rc['explanation']}**
Confidence: {rc['confidence']}
### Evidence
"""
for ev in diagnosis["evidence"]:
md += f"- {ev}\n"
md += f"""
## Resolution
### Short-term (already done)
{fix['short_term']}
### Medium-term
{fix['medium_term']}
### Long-term
{fix['long_term']}
## Lessons Learned
"""
for lesson in diagnosis["lessons_learned"]:
md += f"- {lesson}\n"
md += "\n## Action Items\n\n| Priority | Action | Owner |\n|----------|--------|-------|\n"
for ai in diagnosis["action_items"]:
md += f"| {ai['priority']} | {ai['item']} | {ai['owner']} |\n"
md += f"""
## Commits Deployed
"""
for c in commits:
md += f"- `{c['sha'][:8]}` ({c['author']}): {c['message']}\n"
md += f"""
---
*Auto-generated postmortem. Review and update with human insights before publishing.*
*Generated by Claude Code at {datetime.utcnow().isoformat()}*
"""
return md
def main() -> int:
deploy_sha = os.environ["DEPLOY_SHA"]
previous_sha = os.environ["PREVIOUS_SHA"]
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITHUB_TOKEN"]
# 1. Cargar rollback reason (del script de monitoring)
rollback_reason = json.loads(Path("rollback_reason.json").read_text())
# 2. Obtener commits del deploy
commits = get_deploy_commits(deploy_sha, previous_sha)
print(f"Commits in deploy: {len(commits)}")
# 3. PR info del commit más reciente
pr_info = get_pr_for_commit(commits[0]["sha"], repo, token) if commits else None
# 4. Métricas durante incident
metrics = get_metrics_during_incident(
deploy_time=rollback_reason.get("timestamp"),
rollback_time=datetime.utcnow().isoformat(),
api_url=os.environ.get("METRICS_API_URL"),
api_token=os.environ.get("METRICS_API_TOKEN"),
)
# 5. Diagnóstico con Claude
print("Generando diagnóstico con Claude...")
diagnosis = generate_diagnosis_with_claude(commits, pr_info, metrics, rollback_reason)
# 6. Postmortem draft
postmortem = generate_postmortem_draft(diagnosis, commits, pr_info, rollback_reason, metrics)
# Save
Path("diagnosis.json").write_text(json.dumps(diagnosis, indent=2))
Path("postmortem_draft.md").write_text(postmortem)
print(f"\nRoot cause: {diagnosis['root_cause']['explanation']}")
print(f"Confidence: {diagnosis['root_cause']['confidence']}")
print(f"Fix sugerido: {diagnosis['fix_suggestion']['medium_term']}")
return 0
if __name__ == "__main__":
sys.exit(main())
Crear Issue de Postmortem
"""scripts/create_postmortem_issue.py
Crea un GitHub Issue con el postmortem draft.
"""
import json
import os
import sys
from pathlib import Path
import requests
def main() -> int:
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITHUB_TOKEN"]
postmortem_file = Path("postmortem_draft.md")
diagnosis_file = Path("diagnosis.json")
if not postmortem_file.exists():
print("ERROR: postmortem_draft.md no encontrado", file=sys.stderr)
return 1
body = postmortem_file.read_text()
# Diagnóstico para construir título
diagnosis = json.loads(diagnosis_file.read_text()) if diagnosis_file.exists() else {}
rc = diagnosis.get("root_cause", {})
likely_commit = rc.get("likely_commit", "unknown")[:8]
title = f"Incident postmortem: rollback automático de {likely_commit}"
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
}
payload = {
"title": title,
"body": body,
"labels": ["incident", "postmortem", "auto-generated"],
}
url = f"https://api.github.com/repos/{repo}/issues"
r = requests.post(url, headers=headers, json=payload)
if r.status_code == 201:
issue = r.json()
print(f"Issue creado: {issue['html_url']}")
return 0
print(f"ERROR creando issue: {r.status_code} {r.text}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Notificación con Diagnóstico
Agregas al script de notificación de la cápsula 04 el contexto del diagnóstico:
# Update a scripts/notify_rollback.py
def main() -> int:
reason = json.loads(Path("rollback_reason.json").read_text())
# Cargar diagnóstico si está disponible
diagnosis = None
if Path("diagnosis.json").exists():
diagnosis = json.loads(Path("diagnosis.json").read_text())
# Generar mensaje con/sin diagnosis
if diagnosis:
rc = diagnosis["root_cause"]
message = f"""🚨 **Auto-rollback executed** (with diagnosis)
**Trigger:** {reason['description']}
**Root cause (likely):** {rc['explanation'][:200]}...
**Confidence:** {rc['confidence']}
**Fix sugerido (medium-term):** {diagnosis['fix_suggestion']['medium_term']}
📋 Postmortem draft creado como GitHub Issue.
On-call: revisar incidente y validar diagnóstico.
"""
else:
message = generate_basic_notification(reason)
# ... resto del send a Slack
Calibrar la Confianza del Diagnóstico
Claude no siempre acierta el root cause. La estructura del diagnóstico debe incluir nivel de confianza y el equipo debe tratar el output como draft, no verdad absoluta.
HIGH CONFIDENCE (~70% de los casos):
- Un solo commit en el deploy
- PR description menciona el área que falló
- Diff del commit muestra cambio claramente relacionado
MEDIUM CONFIDENCE (~25% de los casos):
- Múltiples commits, varios podrían ser causa
- Métricas correlacionadas pero no concluyentes
- Cambio podría ser causa secundaria
LOW CONFIDENCE (~5% de los casos):
- Cambios indirectos (ej. update de dependency)
- Bug que estaba latente y se activa por load
- Race condition o timing-specific bug
Importante: el equipo nunca debe confiar 100% en el diagnóstico automático. Es un starting point, no un veredicto. La validación humana es parte del proceso de postmortem.
Trampas Comunes
Error 1: Tratar el diagnóstico como verdad final
Síntoma: Equipo aplica el "fix sugerido" sin validar y rompe algo más.
Por qué pasa: Confianza ciega en el output del modelo.
Cómo corregir: Siempre flagear diagnosis como "draft" o "automated suggestion". Validación humana antes de actuar.
Error 2: Diagnóstico sin contexto suficiente
Síntoma: El diagnóstico es genérico ("verificar el último commit") porque el script no pasó info útil al modelo.
Por qué pasa: Pasar solo "rollback executed" sin diff, métricas, PR info.
Cómo corregir: Pasar contexto rico: commits, diffs, métricas, PR descriptions, stack traces si accesibles.
Error 3: Postmortem sin action items concretos
Síntoma: El postmortem dice "review process" — no es accionable.
Por qué pasa: Prompt no enfatiza specificity en action items.
Cómo corregir: Pedir explícitamente: "action items concretos con owner y prioridad".
Error 4: Crear issue automático sin review
Síntoma: Issues con info incorrecta llenan el repo, equipo deja de leerlos.
Por qué pasa: Trust ciego en el auto-generation.
Cómo corregir: Issues marcados claramente como "auto-generated, requires review". Asignar a alguien específico para validar antes de circular.
Error 5: No incluir info de previous incidents similares
Síntoma: Cada incident se trata como nuevo, no aprovechamos patrones de incidents previos.
Por qué pasa: El sistema no busca incidents similares en el historial.
Cómo corregir: Antes del análisis, query GitHub Issues con label incident para encontrar similares. Pasarlos como contexto al modelo.
Diagnóstico
Pregunta 1: ¿Tu sistema genera diagnóstico automático del rollback o solo notifica?
Solo notificación = equipo invierte 30+ min identificando root cause. Diagnóstico = equipo arranca con contexto.
Pregunta 2: ¿El diagnóstico incluye nivel de confianza?
Sin confidence, el equipo puede aplicar fixes incorrectos confiando ciegamente. Con confidence, sabe cuándo validar más.
Pregunta 3: ¿Generas un postmortem draft o solo un mensaje a Slack?
Slack se pierde. Issue de GitHub = persistente, accionable, trackeable.
Pregunta 4: ¿Tu diagnóstico tiene action items concretos?
Action items vagos = no se hacen. Concretos con owner = se ejecutan.
Pregunta 5: ¿El equipo entiende que el diagnóstico es draft, no verdad final?
Si confían 100%, los falsos positivos del modelo causan más daño. Treat as starting point.
Ejercicios
Ejercicio 1: Generación básica de diagnóstico (Medio)
Implementa diagnose_incident.py que:
- Carga rollback_reason.json
- Lista commits del deploy
- Llama a Claude con contexto básico
- Genera diagnosis.json estructurado
Verifica con un rollback simulado.
Ejercicio 2: Postmortem draft (Medio)
Agrega generación de postmortem markdown desde el diagnosis JSON. El postmortem debe tener:
- Summary
- Timeline
- Root cause + evidence
- Resolution (short/medium/long term)
- Action items con priorities
Ejercicio 3: Issue automático con review workflow (Difícil)
Implementa:
- Creación automática del Issue con postmortem draft
- Asignación a un on-call según schedule (cron o config)
- Label
requires-human-review - Auto-comment después de 24h si nadie validó: "Este issue necesita review"
Resumen
- Diagnóstico transforma rollback en aprendizaje — el equipo arranca con contexto, no desde cero
- 5 piezas del diagnóstico: cause, evidence, root cause analysis, fix, postmortem
- Confidence levels comunican incertidumbre del modelo al equipo
- Postmortem draft es starting point, no veredicto — siempre review humano
- Action items concretos con owner = se ejecutan; vagos = se ignoran
- Issue persistente > mensaje Slack efímero
- Patrones de incidents previos mejoran el diagnóstico futuro
Próxima cápsula: Módulo 6 — Proyecto Integrador: Pipeline CI/CD Completo. Combinas todo lo aprendido en módulos 1-5 en un pipeline end-to-end production-ready. Es el cierre de la guía y el portfolio piece más grande.
Recursos Adicionales
- Site Reliability Engineering: Postmortem Culture — Capítulo del libro de Google sobre cultura de postmortems
- Etsy: Code as Craft - Blameless PostMortems — Filosofía de postmortems sin blame
- Atlassian: Incident Management — Framework de incident management
- PagerDuty: Postmortem Template — Template estándar de postmortem
- Honeycomb: Observability for Incidents — Cómo observability acelera diagnóstico
- Claude Code root cause analysis use cases — Patterns aplicables