Módulo 5: Security Scanning y Rollback Inteligente

Rollback Automático con Triggers

Rollback Automático con Triggers

Descripción

Esta cápsula cubre la última línea de defensa del pipeline: rollback automático cuando algo sale mal en producción. Aunque tengas code review, security scanning, readiness validation y approval humano, siempre va a haber un deploy que falla en producción. La diferencia entre un equipo que sobrevive a esos incidents y uno que sufre semanas de fallout es cuánto tarda en revertir.

Aprendes a definir qué métricas monitorear post-deploy, configurar triggers automáticos de rollback, ejecutar el revert sin intervención humana cuando las señales son claras, y mantener el balance correcto entre "rollback agresivo" (false positives = downtime innecesario) y "rollback conservador" (slow response = más usuarios afectados).

Al terminar, vas a tener un sistema donde un deploy fallido se revierte en menos de 5 minutos automáticamente — y donde el rollback explica qué pasó (lo que cubre la cápsula 05).


Por Qué el Rollback Manual Falla

ROLLBACK MANUAL EN INCIDENT REAL:

T+0:    Deploy a producción
T+2min: Métricas empiezan a degradar
T+5min: Alert llega al on-call (si está configurada)
T+8min: On-call abre el laptop
T+12min: Identifica que es post-deploy
T+15min: Decide hacer rollback
T+18min: Encuentra el comando exacto
T+22min: Ejecuta revert
T+25min: Verifica que la métrica volvió

TIEMPO TOTAL: ~25 minutos de degradación
USUARIOS AFECTADOS: variable, potencialmente miles

Versus rollback automático:

ROLLBACK AUTOMÁTICO:

T+0:    Deploy a producción
T+2min: Métricas degradan (error rate >5%)
T+3min: Trigger detecta el threshold
T+3min: Rollback automático ejecutándose
T+5min: Métricas estabilizadas
T+5min: On-call notificado del rollback completado

TIEMPO TOTAL: ~5 minutos de degradación
USUARIOS AFECTADOS: orden de magnitud menor

5 min vs 25 min es la diferencia entre un blip casi invisible y un incident que tu equipo va a recordar.


Las Métricas que Disparan Rollback

TIER 1 — Síntomas claros (rollback automático apropiado):

  ✅ Error rate sube significativamente
     → Threshold típico: >2% sobre baseline
     → Tiempo de observación: 2-5 minutos sostenidos
  
  ✅ Latency p95 sube significativamente
     → Threshold típico: 2-3x el baseline
     → Tiempo de observación: 3-5 minutos sostenidos
  
  ✅ Health check falla
     → Threshold: 3+ health checks consecutivos fallidos
     → Tiempo de observación: inmediato

TIER 2 — Síntomas que requieren juicio (alert para humano):

  ⚠️  Throughput baja
     → Puede ser real (bug) o esperado (cambio de comportamiento)
     → Mejor: alert al on-call, no rollback automático
  
  ⚠️  Memory/CPU sube
     → Puede ser memory leak o cambio en workload
     → Mejor: alert al on-call

TIER 3 — Síntomas indirectos (no disparan rollback):

  ❌ Métricas de negocio (signups, conversiones)
     → Demasiado ruido del comportamiento del usuario
  
  ❌ Errors de un tercero (Stripe, AWS)
     → No es nuestro deploy

La regla: rollback automático solo en Tier 1. Otras métricas → alert humano que decide.


Setup del Monitoring

Antes de configurar rollback, necesitas monitoring con métricas accesibles vía API. Stack típico:

APPLICATION METRICS:
  → Prometheus + Grafana (open source, self-hosted)
  → Datadog (paid, full-featured)
  → New Relic (paid, focus en APM)
  → CloudWatch (AWS native)

LOGS / ERRORS:
  → Sentry (errors)
  → Loki / Splunk / Elasticsearch (logs)

INFRASTRUCTURE:
  → Mismas herramientas, métricas de cluster

Cualquiera funciona. Lo importante es tener API accesible desde el workflow que diga: "en los últimos N minutos, ¿el error rate fue >X%?"


El Workflow con Rollback

# .github/workflows/deploy-with-rollback.yml
name: Deploy with Auto-Rollback

on:
  push:
    branches: [main]

permissions:
  contents: read
  deployments: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    outputs:
      previous_release: ${{ steps.deploy.outputs.previous_release }}
      new_release: ${{ steps.deploy.outputs.new_release }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture previous release
        id: previous
        run: |
          # Tu mecanismo: git tag, kubectl, etc.
          PREV=$(./scripts/get_current_release.sh production)
          echo "previous=$PREV" >> $GITHUB_OUTPUT
      
      - name: Deploy
        id: deploy
        run: |
          NEW=$(./scripts/deploy.sh production)
          echo "previous_release=${{ steps.previous.outputs.previous }}" >> $GITHUB_OUTPUT
          echo "new_release=$NEW" >> $GITHUB_OUTPUT
      
      - name: Initial smoke tests
        run: ./scripts/smoke_tests.sh https://app.example.com

  monitor-and-rollback:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      
      - run: pip install requests anthropic
      
      - name: Monitor metrics for 10 minutes
        id: monitor
        env:
          METRICS_API_URL: ${{ secrets.METRICS_API_URL }}
          METRICS_API_TOKEN: ${{ secrets.METRICS_API_TOKEN }}
          DEPLOY_TIME: ${{ needs.deploy.outputs.new_release }}
        run: |
          python scripts/monitor_post_deploy.py
          echo "rollback_needed=$?" >> $GITHUB_OUTPUT
      
      - name: Execute rollback if needed
        if: steps.monitor.outputs.rollback_needed == '1'
        env:
          PREVIOUS_RELEASE: ${{ needs.deploy.outputs.previous_release }}
        run: |
          echo "🚨 Rolling back to $PREVIOUS_RELEASE"
          ./scripts/rollback.sh production "$PREVIOUS_RELEASE"
      
      - name: Notify team of rollback
        if: steps.monitor.outputs.rollback_needed == '1'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: python scripts/notify_rollback.py
      
      - name: Notify success
        if: steps.monitor.outputs.rollback_needed == '0'
        run: echo "✅ Deploy estable, no rollback necesario"

El Script de Monitoring

"""scripts/monitor_post_deploy.py

Monitorea métricas post-deploy. Retorna exit 1 si necesita rollback.
"""
import json
import os
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
import requests


@dataclass
class MetricThreshold:
    name: str
    threshold: float
    description: str
    sustained_minutes: int = 3  # cuánto tiempo debe estar sobre el threshold


THRESHOLDS = [
    MetricThreshold(
        name="error_rate",
        threshold=0.02,  # 2%
        description="Error rate > 2% sostenido por 3 min",
        sustained_minutes=3,
    ),
    MetricThreshold(
        name="latency_p95",
        threshold=2.0,  # 2x baseline
        description="Latency p95 > 2x baseline sostenido por 5 min",
        sustained_minutes=5,
    ),
    MetricThreshold(
        name="health_check_failures",
        threshold=3,
        description="3+ health checks consecutivos fallidos",
        sustained_minutes=1,
    ),
]


def fetch_metric(name: str, window_minutes: int = 1) -> float | None:
    """Obtener métrica del sistema de monitoring."""
    api_url = os.environ.get("METRICS_API_URL")
    api_token = os.environ.get("METRICS_API_TOKEN")
    
    if not api_url:
        # Mock para testing
        # En producción real, query a Prometheus/Datadog/etc.
        return mock_metric(name)
    
    headers = {"Authorization": f"Bearer {api_token}"}
    params = {
        "metric": name,
        "window": f"{window_minutes}m",
    }
    
    try:
        r = requests.get(f"{api_url}/query", headers=headers, params=params, timeout=10)
        if r.ok:
            data = r.json()
            return data.get("value")
    except Exception as e:
        print(f"WARNING: failed to fetch {name}: {e}", file=sys.stderr)
    
    return None


def mock_metric(name: str) -> float:
    """Mock para demo — en producción reemplazar con API real."""
    # Simula valores normales por defecto
    return {
        "error_rate": 0.005,
        "latency_p95": 1.0,
        "health_check_failures": 0,
    }.get(name, 0.0)


def check_threshold(threshold: MetricThreshold) -> tuple[bool, float]:
    """Verificar si la métrica está sostenidamente sobre el threshold.
    
    Retorna (is_over_threshold, current_value).
    """
    samples = []
    samples_needed = threshold.sustained_minutes
    
    for i in range(samples_needed):
        value = fetch_metric(threshold.name, window_minutes=1)
        if value is not None:
            samples.append(value)
        else:
            samples.append(0)  # asumir normal si no hay data
        
        if i < samples_needed - 1:
            time.sleep(60)  # esperar 1 min entre samples
    
    # Todos los samples sobre el threshold
    all_over = all(s > threshold.threshold for s in samples)
    avg_value = sum(samples) / len(samples) if samples else 0
    
    return all_over, avg_value


def main() -> int:
    """Monitorear por 10 minutos y decidir si necesita rollback."""
    print(f"=== Post-deploy monitoring ({datetime.utcnow().isoformat()}) ===\n")
    
    # Esperar 1 min después del deploy para que las métricas se estabilicen
    print("Waiting 60s for metrics to stabilize...")
    time.sleep(60)
    
    # Check cada threshold
    for threshold in THRESHOLDS:
        print(f"Monitoring {threshold.name}...")
        is_over, value = check_threshold(threshold)
        
        if is_over:
            print(f"  🚨 ALERT: {threshold.description}")
            print(f"  Current value: {value}")
            print(f"  Action: ROLLBACK")
            
            # Salvar info del rollback para los siguientes steps
            with open("rollback_reason.json", "w") as f:
                json.dump({
                    "metric": threshold.name,
                    "threshold": threshold.threshold,
                    "current_value": value,
                    "description": threshold.description,
                    "timestamp": datetime.utcnow().isoformat(),
                }, f, indent=2)
            
            return 1  # exit 1 → rollback needed
        
        print(f"  ✅ OK: {threshold.name} = {value}")
    
    print("\n✅ Monitoring complete. Deploy stable.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

El Script de Rollback

#!/bin/bash
# scripts/rollback.sh — ejecutar rollback al release anterior
set -euo pipefail

ENV="${1:?'Usage: rollback.sh <env> <previous_release>'}"
PREVIOUS_RELEASE="${2:?'Usage: rollback.sh <env> <previous_release>'}"

echo "[rollback] Rolling back $ENV to $PREVIOUS_RELEASE..."

# Ajusta según tu plataforma:

# Kubernetes
# kubectl rollout undo deployment/app -n $ENV --to-revision=$PREVIOUS_RELEASE

# Heroku
# heroku releases:rollback v$PREVIOUS_RELEASE --app $ENV

# AWS ECS
# aws ecs update-service --cluster $ENV --service app \
#   --task-definition app:$PREVIOUS_RELEASE

# Vercel / Netlify
# vercel rollback $PREVIOUS_RELEASE

# Custom deploy script
./scripts/deploy.sh "$ENV" --release "$PREVIOUS_RELEASE"

echo "[rollback] Rollback to $PREVIOUS_RELEASE complete"

# Verificar smoke tests
./scripts/smoke_tests.sh https://app.example.com

echo "[rollback] Verification complete"

Notificación Post-Rollback

"""scripts/notify_rollback.py

Notificar al equipo del rollback automático con contexto.
"""
import json
import os
import sys
from pathlib import Path
import requests
from anthropic import Anthropic


def generate_notification(reason: dict) -> str:
    """Generar mensaje de notificación con contexto."""
    client = Anthropic()
    
    prompt = f"""El sistema acaba de hacer rollback automático en producción.

CONTEXTO:
{json.dumps(reason, indent=2)}

Genera un mensaje BREVE para Slack que:
1. Empiece con "🚨 Auto-rollback executed"
2. Explique qué métrica disparó el rollback
3. Diga qué hace el on-call ahora (verificar estabilización, investigar root cause)
4. Sea profesional y actionable, máximo 100 palabras

Output: solo el mensaje, sin texto adicional.
"""
    
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=300,
        messages=[{"role": "user", "content": prompt}],
    )
    
    return response.content[0].text.strip()


def send_slack(message: str, webhook: str):
    """Enviar a Slack."""
    payload = {"text": message, "username": "Auto-Rollback Bot", "icon_emoji": ":rotating_light:"}
    r = requests.post(webhook, json=payload, timeout=10)
    if not r.ok:
        print(f"WARNING: Slack failed: {r.status_code}", file=sys.stderr)


def main() -> int:
    reason_file = Path("rollback_reason.json")
    if not reason_file.exists():
        print("ERROR: rollback_reason.json no encontrado", file=sys.stderr)
        return 1
    
    reason = json.loads(reason_file.read_text())
    message = generate_notification(reason)
    
    print("--- Notification ---")
    print(message)
    print("---")
    
    webhook = os.environ.get("SLACK_WEBHOOK")
    if webhook:
        send_slack(message, webhook)
        print("Notificación enviada a Slack")
    
    # Adicional: PagerDuty para incidents urgentes
    pd_token = os.environ.get("PAGERDUTY_TOKEN")
    if pd_token:
        # Crear incident en PagerDuty
        # ...
        pass
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

Calibrar los Thresholds

Los valores de threshold son específicos de tu sistema. Calíbralos basándote en:

DATOS A RECOPILAR:
1. Baseline normal: ¿cuál es el error rate típico? Latency p95?
2. Variabilidad histórica: ¿cuánto fluctúa naturalmente?
3. Falsos positivos previos: ¿hubo alerts que no eran reales?

REGLA GENERAL:
- Threshold = baseline_max + 2-3x desviación estándar
- Sustained time: lo suficiente para descartar spikes (2-5 min típico)
- Cuando dudes, threshold más permisivo (rollback only si claro)

Ejemplo de calibración

Si tu error rate normal es 0.5% con desviación estándar de 0.2%:

  • Baseline + 3σ = 0.5% + 0.6% = ~1.1%
  • Threshold conservador: 2% (claramente sobre baseline)
  • Sustained: 3 min (descarta spikes momentáneos)

Trampas Comunes

Error 1: Rollback agresivo (false positives)

Síntoma: Rollbacks frecuentes en deploys que no tenían bug real. El equipo pierde confianza.

Por qué pasa: Thresholds demasiado bajos o tiempo de observación demasiado corto.

Cómo corregir: Aumentar thresholds y/o sustained time. Mejor ser conservador con rollback que con alert humano.

Error 2: Rollback conservador (false negatives)

Síntoma: Bug llega a producción, métricas degradan, pero el rollback no dispara porque "no llegó al threshold".

Por qué pasa: Threshold demasiado alto o ventana muy larga.

Cómo corregir: Calibrar con datos históricos. Revisar incidents pasados — ¿qué thresholds los hubieran detectado?

Error 3: No capturar el release anterior antes del deploy

Síntoma: Necesitas revertir pero no sabes a qué versión.

Por qué pasa: El script de deploy no guarda el release anterior antes de aplicar el nuevo.

Cómo corregir: Step explícito que captura el current release antes del deploy y lo expone como output del job.

Error 4: Rollback sin verificar que estabilizó

Síntoma: Rollback ejecutado, pero las métricas siguen mal porque el rollback también falló.

Por qué pasa: El script de rollback ejecuta sin verificar smoke tests post-rollback.

Cómo corregir: Smoke tests después del rollback. Si el rollback en sí falla, escalar a humano (PagerDuty).

Error 5: Thresholds estáticos sin actualizar

Síntoma: Thresholds configurados hace 1 año cuando el sistema era distinto. Ya no son apropiados.

Por qué pasa: Falta de revisión periódica.

Cómo corregir: Review trimestral de thresholds. Comparar con incidents recientes — ¿hubieran disparado correctamente?


Diagnóstico

Pregunta 1: ¿Tu pipeline tiene rollback automático configurado?

Sin rollback automático, el primer incident en producción tarda 25+ minutos. Con rollback, ~5 minutos.

Pregunta 2: ¿Qué métricas disparan tu rollback?

Tier 1: error rate, latency p95, health checks. Si solo monitoreas "is the app up?", te faltan signals importantes.

Pregunta 3: ¿Cuánto tarda tu rollback desde detection hasta restored service?

Target: <5 minutos. Si es más, optimizar el script de rollback (cache de imágenes, etc.).

Pregunta 4: ¿Capturas el "previous release" antes del deploy?

Si no, no puedes revertir programáticamente. Step de captura es esencial.

Pregunta 5: ¿Los thresholds están calibrados a tu sistema o son genéricos?

Genéricos = false positives o false negatives. Calibrados a baseline real = preciso.


Ejercicios

Ejercicio 1: Capture previous release (Fácil)

Implementa un script get_current_release.sh que retorna el release actualmente deployado en producción. Adáptalo a tu plataforma (Kubernetes, Heroku, etc.).

Ejercicio 2: Monitoring básico con thresholds (Medio)

Implementa monitor_post_deploy.py con:

  1. Mock de métricas (función mock_metric)
  2. Thresholds para error_rate, latency, health checks
  3. Sustained time check (samples cada minuto)
  4. Exit code 1 si necesita rollback

Probarlo simulando métricas que excedan el threshold.

Ejercicio 3: Pipeline completo con rollback (Difícil)

Combina:

  1. Deploy
  2. Capture previous release
  3. Monitor post-deploy (10 min)
  4. Rollback automático si necesita
  5. Notificación con contexto

Probarlo en un ambiente de staging — forzar un "fake fail" para verificar el rollback.


Resumen

  • Rollback manual = 25 min, rollback automático = 5 min — orden de magnitud diferencia
  • Tier 1 metrics disparan rollback: error rate, latency p95, health checks
  • Tier 2 metrics alertan a humano: throughput, memory, CPU
  • Sustained time evita false positives de spikes momentáneos
  • Capture previous release ANTES del deploy — necesario para revertir programáticamente
  • Verificar smoke tests post-rollback — el rollback en sí puede fallar
  • Calibrar thresholds a tu sistema, no usar valores genéricos

Próxima cápsula: 05 — Diagnóstico inteligente post-rollback. El rollback funciona, pero no enseña. La cápsula final del módulo: cuando el rollback se ejecuta, Claude Code analiza qué pasó, identifica root cause, y sugiere fix. El rollback se vuelve aprendizaje sistematizado.


Recursos Adicionales

  1. Site Reliability Engineering: Effective Troubleshooting — Capítulo del libro de Google
  2. Argo Rollouts — Rollback automático en Kubernetes
  3. Spinnaker Canary Analysis — Para canary + rollback combinado
  4. Datadog Synthetics — Monitoring continuo de endpoints
  5. Prometheus Alertmanager — Alerting open source
  6. Honeycomb Observability — Observability moderna para sistemas distribuidos