Módulo 8: Proyecto Integrador — Production-Ready AI System

2. Deployment Strategies

Descripción

El momento del deploy es cuando las cosas pueden salirte mal de formas inesperadas: configuración diferente en producción, secrets mal configurados, comportamiento distinto con tráfico real. Las estrategias de deployment no son ceremonias de DevOps — son herramientas que tú eliges para minimizar el riesgo de cada cambio. Para tus apps AI, hay consideraciones adicionales: los prompts son parte del código, cambiar de modelo es un cambio de comportamiento, y un deployment fallido puede ser invisible si tu sistema falla silenciosamente en vez de lanzar excepciones claras.


Las 3 estrategias principales

Rolling Update

Antes del deploy:
  Instancia 1: v1 (recibiendo tráfico)
  Instancia 2: v1 (recibiendo tráfico)
  Instancia 3: v1 (recibiendo tráfico)

Durante el deploy (gradual):
  Instancia 1: v2 (recibiendo tráfico — nueva versión)
  Instancia 2: v1 (recibiendo tráfico — versión anterior)
  Instancia 3: v1 (recibiendo tráfico — versión anterior)

Después del deploy:
  Instancia 1: v2 (recibiendo tráfico)
  Instancia 2: v2 (recibiendo tráfico)
  Instancia 3: v2 (recibiendo tráfico)

Características:
  - Downtime mínimo (siempre hay instancias sirviendo)
  - Rollback: revertir imagen y volver a hacer rolling
  - Riesgo: durante el deploy, hay instancias en v1 y v2 simultáneamente
  - Implicación AI: si cambias el prompt, habrá requests procesados con v1 y v2 del prompt
    → Los logs mostrarán respuestas inconsistentes durante la transición

Blue-Green

Estado normal (blue activo):
  BLUE (v1): recibiendo tráfico real
  GREEN: inactivo (última versión que funcionó)

Cuando hay nuevo deploy:
  1. Deployar v2 a GREEN
  2. Ejecutar smoke tests en GREEN
  3. Switch de tráfico: BLUE → GREEN
  4. GREEN (v2): recibiendo tráfico real
  5. BLUE (v1): en standby (por si hay rollback)

Rollback instantáneo:
  Si GREEN da problemas → switch de tráfico de vuelta a BLUE
  Tiempo de rollback: segundos

Características:
  - Rollback más rápido que rolling
  - Costo: dos entornos completos
  - Sin período de versiones mixtas
  - Ideal para cambios de prompts importantes

Canary

Estado inicial:
  MAIN: 100% del tráfico (v1)
  CANARY: 0% del tráfico

Deploy canary:
  MAIN: 90% del tráfico (v1)
  CANARY: 10% del tráfico (v2)

Si las métricas son buenas (error rate, latencia, costo):
  MAIN: 50% (v1)
  CANARY: 50% (v2)

Si sigue bien:
  MAIN: 0% (v1 — se depreca)
  CANARY: 100% (v2 — ahora es el main)

Si las métricas empeoran en cualquier punto:
  Rollback: MAIN vuelve a 100%, CANARY a 0%

Características:
  - El control más fino sobre el riesgo
  - Permite comparar v1 vs v2 en producción real (A/B testing de prompts)
  - Más complejo de configurar
  - Ideal cuando cambias modelos o prompts significativos

Consideraciones específicas para apps AI

Los prompts son parte del código

# ❌ Anti-pattern: prompt hardcoded en el código
async def analyze_sentiment(text: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "Analyze the sentiment..."},  # ← hardcoded
            {"role": "user", "content": text}
        ]
    )

# Problema: cambiar el prompt requiere cambiar el código y hacer un deploy completo
# Problema: no hay historial de cambios de prompts
# Problema: no puedes revertir el prompt sin revertir el código

# ✅ Pattern correcto: prompts en archivos versionados
# prompts/sentiment/v1.yaml:
#   version: "v1"
#   system: "You are a sentiment analysis expert..."

# En el código, cargar el prompt:
from src.prompts.loader import load_prompt
template = load_prompt("sentiment")  # Carga v1.yaml (o el que sea default)

# Al hacer un cambio de prompt:
# 1. Crear prompts/sentiment/v2.yaml con el nuevo prompt
# 2. Actualizar settings: prompt_version: "v2"
# 3. Commit y deploy
# 4. Si hay problema, revertir settings a "v1" (o revertir el commit)

Cambiar de modelo es un cambio de comportamiento

# Cambiar de gpt-4o a gpt-4o-mini NO es solo una change de config
# El modelo puede producir outputs de calidad diferente
# Esto es un cambio de comportamiento que necesita testing

# Antes de cambiar el modelo en producción:
# 1. Ejecutar la test suite con el nuevo modelo
# 2. Comparar outputs en staging (¿los scores de sentimiento son similares?)
# 3. Revisar los cost implications
# 4. Considerar canary: 10% del tráfico al nuevo modelo, comparar métricas

# En Settings:
class Settings(BaseSettings):
    openai_model: str = Field(
        default="gpt-4o",
        description="Cambiar este valor es un cambio de comportamiento — requiere testing"
    )
    
    # Para canary de modelo, podrías tener:
    canary_model: Optional[str] = Field(
        default=None,
        description="Si está seteado, X% del tráfico va a este modelo"
    )
    canary_traffic_percent: int = Field(default=10)

Pre-deploy checklist específico para AI

# scripts/pre_deploy.sh

#!/bin/bash
set -e  # Salir si cualquier comando falla

echo "=== Pre-Deploy Checklist ==="

echo ""
echo "1. Unit tests..."
python -m pytest tests/unit/ -q
echo "   ✅ Unit tests passed"

echo ""
echo "2. Guardrails tests..."
python -m pytest tests/ -k "guardrail" -q
echo "   ✅ Guardrails tests passed"

echo ""
echo "3. No secrets in code..."
if grep -r "sk-" src/ 2>/dev/null; then
    echo "   ❌ FOUND POTENTIAL API KEYS IN CODE"
    exit 1
fi
echo "   ✅ No secrets found"

echo ""
echo "4. .env not in git..."
if git ls-files .env 2>/dev/null | grep -q ".env"; then
    echo "   ❌ .env IS TRACKED IN GIT — SECURITY RISK"
    exit 1
fi
echo "   ✅ .env not tracked"

echo ""
echo "5. Production config validation..."
ENVIRONMENT=production python -c "
from src.config import get_settings
try:
    s = get_settings()
    print(f'   Config valid: model={s.openai_model}, env={s.environment}')
except Exception as e:
    print(f'   ERROR: {e}')
    exit(1)
"
echo "   ✅ Production config valid"

echo ""
echo "=== All pre-deploy checks passed ==="

Post-deploy validation

# scripts/post_deploy_check.py
"""
Verificaciones a ejecutar inmediatamente después del deploy.
"""
import time
import urllib.request
import json
import sys

BASE_URL = os.environ.get("APP_URL", "http://localhost:8000")

def smoke_test_health():
    """El servidor responde a health checks."""
    url = f"{BASE_URL}/health/live"
    req = urllib.request.urlopen(url, timeout=10)
    assert req.status == 200
    print("  ✅ /health/live: 200 OK")

def smoke_test_ready():
    """El servidor está listo para tráfico."""
    url = f"{BASE_URL}/health/ready"
    req = urllib.request.urlopen(url, timeout=10)
    assert req.status == 200
    print("  ✅ /health/ready: 200 OK")

def smoke_test_analyze():
    """El endpoint principal responde correctamente."""
    data = json.dumps({"text": "This product is amazing!"}).encode()
    req = urllib.request.Request(
        f"{BASE_URL}/api/v1/analyze",
        data=data,
        headers={"Content-Type": "application/json"}
    )
    response = urllib.request.urlopen(req, timeout=30)
    body = json.loads(response.read())
    
    assert response.status == 200
    assert "sentiment" in body
    assert "score" in body
    print(f"  ✅ /api/v1/analyze: {body['sentiment']} (score: {body['score']})")

def monitor_for_errors(duration_seconds: int = 60):
    """Monitorea logs por errores durante N segundos después del deploy."""
    print(f"  Monitoring for errors for {duration_seconds}s...")
    # En un sistema real, consultarías tu logging aggregator (Datadog, CloudWatch, etc.)
    # Aquí simplificado:
    time.sleep(min(duration_seconds, 15))  # En CI, 15s es suficiente
    print("  ✅ No critical errors detected in monitoring window")

def run_post_deploy(monitor: bool = True):
    print("=== Post-Deploy Validation ===\n")
    
    checks = [
        ("Health liveness", smoke_test_health),
        ("Health readiness", smoke_test_ready),
        ("Analyze endpoint", smoke_test_analyze),
    ]
    
    if monitor:
        checks.append(("Error monitoring", lambda: monitor_for_errors(60)))
    
    for name, check in checks:
        try:
            check()
        except Exception as e:
            print(f"  ❌ {name} FAILED: {e}")
            print(f"\n⚠️  POST-DEPLOY FAILURE — consider rollback")
            sys.exit(1)
    
    print("\n=== All post-deploy checks passed ===")
    print("✅ Deploy successful")

if __name__ == "__main__":
    run_post_deploy(monitor="--no-monitor" not in sys.argv)

Configuración por entorno

# La regla: nunca usar "if env == 'production'" en el código de negocio
# Toda la variación por entorno va en Settings (config)

# .env.development
ENVIRONMENT=development
LOG_LEVEL=DEBUG
USE_MOCK_PROVIDER=true
OPENAI_MODEL=gpt-4o-mini      # Más barato en dev
MAX_REQUESTS_PER_MINUTE=20    # Más bajo en dev

# .env.staging
ENVIRONMENT=staging
LOG_LEVEL=INFO
USE_MOCK_PROVIDER=false
OPENAI_MODEL=gpt-4o           # Igual que prod para testing real
MAX_REQUESTS_PER_MINUTE=60

# .env.production
ENVIRONMENT=production
LOG_LEVEL=INFO
USE_MOCK_PROVIDER=false
OPENAI_MODEL=gpt-4o
MAX_REQUESTS_PER_MINUTE=400   # 80% del límite del API
CIRCUIT_BREAKER_THRESHOLD=5
MAX_RETRY_ATTEMPTS=4
DAILY_BUDGET_LIMIT_USD=50.0

# La validación en Settings (del M6) asegura que en production:
# - USE_MOCK_PROVIDER=false obligatorio
# - LOG_LEVEL != DEBUG obligatorio
# - OPENAI_API_KEY presente obligatorio

Rollback procedure

# Procedure de rollback para cada estrategia

# Rolling (con Docker/docker-compose):
docker-compose up -d --scale app=3  # Volver a imagen anterior
# O si usas tags:
docker pull myapp:v1.2.3
docker-compose up -d

# Blue-Green (con nginx):
# En nginx.conf, cambiar upstream de green a blue:
# upstream app { server blue:8000; }  # Revertir a blue
nginx -s reload

# Kubernetes rolling:
kubectl rollout undo deployment/sentiment-app
kubectl rollout status deployment/sentiment-app  # Verificar que completó

# En todos los casos, ejecutar post-deploy después del rollback:
python scripts/post_deploy_check.py --no-monitor

Ejercicios

Ejercicio 1: Elegir la estrategia correcta

Para cada escenario, ¿qué estrategia de deployment usarías?

  1. Cambiar el mensaje de error en un endpoint de texto
  2. Actualizar el prompt de análisis de sentimiento con una nueva instrucción importante
  3. Cambiar el modelo de gpt-4o-mini a gpt-4o en producción
Ver solución
  1. Cambiar mensaje de error: Rolling update — bajo riesgo, los mensajes de error no afectan la lógica
  2. Cambio importante de prompt: Blue-Green — el prompt nuevo puede dar outputs muy diferentes; quieres poder hacer rollback instantáneo si la calidad baja
  3. Cambiar modelo: Canary — cambiar de modelo es el mayor cambio de comportamiento posible; comenzar con 5-10% del tráfico para validar que la calidad y el costo son aceptables antes de hacer el switch completo

Ejercicio 2: Script de canary con validación de métricas

Escribe un script en Python que simule un canary deployment: comienza con 10% del tráfico, verifica métricas (error rate < 5%, latencia p50 < 3s), y si pasa, incrementa a 50% y luego a 100%. Si las métricas fallan en cualquier etapa, hace rollback.

Ver solución
import time
import random
from dataclasses import dataclass

@dataclass
class CanaryMetrics:
    error_rate: float
    p50_latency_ms: float
    total_requests: int

def get_canary_metrics(canary_percent: int) -> CanaryMetrics:
    """Simula métricas del canary (en prod real: Datadog/Prometheus)."""
    return CanaryMetrics(
        error_rate=random.uniform(0.01, 0.04),
        p50_latency_ms=random.uniform(800, 2500),
        total_requests=canary_percent * 10,
    )

def canary_deploy(stages: list[int] = [10, 50, 100]):
    max_error_rate = 0.05
    max_p50_ms = 3000
    observation_seconds = 5

    for stage_percent in stages:
        print(f"\n🔄 Canary: {stage_percent}% del tráfico")
        print(f"   Observando por {observation_seconds}s...")
        time.sleep(observation_seconds)

        metrics = get_canary_metrics(stage_percent)
        print(f"   Error rate: {metrics.error_rate:.2%}")
        print(f"   p50 latency: {metrics.p50_latency_ms:.0f}ms")

        if metrics.error_rate > max_error_rate:
            print(f"   ❌ Error rate {metrics.error_rate:.2%} > {max_error_rate:.2%}")
            print(f"   🔙 ROLLBACK: volviendo a 0% canary")
            return False

        if metrics.p50_latency_ms > max_p50_ms:
            print(f"   ❌ p50 {metrics.p50_latency_ms:.0f}ms > {max_p50_ms}ms")
            print(f"   🔙 ROLLBACK: volviendo a 0% canary")
            return False

        print(f"   ✅ Métricas OK — avanzando")

    print("\n✅ Canary completado — 100% en nueva versión")
    return True

if __name__ == "__main__":
    canary_deploy()

Ejercicio 3: Procedure de rollback para cambio de prompt

Tu equipo desplegó un nuevo prompt v3 para análisis de sentimiento. Después de 15 minutos, notan que la precisión bajó de 92% a 78%. Escribe paso a paso el procedure de rollback incluyendo los comandos exactos para revertir el prompt, verificar la reversión, y comunicar al equipo.

Ver solución
# 1. IDENTIFICAR: confirmar que el problema es el prompt
jq 'select(.prompt_version == "v3") | {accuracy: .confidence, timestamp}' \
  logs/app.json | tail -5

# 2. REVERTIR: cambiar la versión del prompt
#    Opción A: revertir el commit
git log --oneline -5
git revert <commit-hash> --no-edit
git push origin main

#    Opción B: cambiar la config directamente (más rápido)
#    En .env.production: PROMPT_VERSION=v2

# 3. RE-DESPLEGAR con el prompt anterior
#    Blue-green: switch de tráfico instantáneo al environment anterior
#    Rolling:
docker-compose pull && docker-compose up -d

# 4. VERIFICAR que v2 está activo
curl -s http://localhost:8000/api/v1/analyze \
  -H "Content-Type: application/json" \
  -d '{"text": "Great product!"}' | jq '.prompt_version'
# → Debe retornar "v2"

# 5. MONITOREAR que la precisión volvió a niveles normales
jq 'select(.prompt_version == "v2") | .confidence' logs/app.json | \
  awk '{s+=$1; n++} END {print "Avg confidence:", s/n}'

# 6. COMUNICAR al equipo:
# "🔙 Rollback completado: prompt v3 → v2
#  Motivo: precisión cayó de 92% a 78% con v3
#  Estado actual: v2 activo, métricas normalizándose
#  Próximos pasos: investigar qué cambió en v3 antes de re-intentar"

Ejercicio 4: Configurar validación post-deploy con alertas

Escribe una función post_deploy_alert que ejecute los smoke tests, y si alguno falla, envíe una alerta (simulada) con los detalles del fallo y un link al runbook correspondiente.

Ver solución
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import json
import urllib.request

@dataclass
class DeployAlert:
    severity: str
    title: str
    detail: str
    runbook_url: str
    timestamp: str = ""

    def __post_init__(self):
        self.timestamp = datetime.utcnow().isoformat()

RUNBOOK_URLS = {
    "health_check": "https://docs.internal/runbook#health-check-failure",
    "smoke_test": "https://docs.internal/runbook#smoke-test-failure",
    "high_error_rate": "https://docs.internal/runbook#high-error-rate",
    "high_latency": "https://docs.internal/runbook#high-latency",
}

def post_deploy_alert(base_url: str = "http://localhost:8000") -> list[DeployAlert]:
    alerts = []

    # Check 1: Health
    try:
        req = urllib.request.urlopen(f"{base_url}/health/ready", timeout=10)
        if req.status != 200:
            alerts.append(DeployAlert(
                severity="critical",
                title="Health check failed post-deploy",
                detail=f"/health/ready returned HTTP {req.status}",
                runbook_url=RUNBOOK_URLS["health_check"],
            ))
    except Exception as e:
        alerts.append(DeployAlert(
            severity="critical",
            title="Server unreachable post-deploy",
            detail=str(e),
            runbook_url=RUNBOOK_URLS["health_check"],
        ))

    # Check 2: Smoke test del endpoint principal
    try:
        data = json.dumps({"text": "This is a great product"}).encode()
        req = urllib.request.Request(
            f"{base_url}/api/v1/analyze",
            data=data,
            headers={"Content-Type": "application/json"},
        )
        response = urllib.request.urlopen(req, timeout=30)
        body = json.loads(response.read())

        if "sentiment" not in body:
            alerts.append(DeployAlert(
                severity="critical",
                title="Analyze endpoint returns malformed response",
                detail=f"Missing 'sentiment' in response: {list(body.keys())}",
                runbook_url=RUNBOOK_URLS["smoke_test"],
            ))
    except Exception as e:
        alerts.append(DeployAlert(
            severity="critical",
            title="Analyze endpoint failed post-deploy",
            detail=str(e),
            runbook_url=RUNBOOK_URLS["smoke_test"],
        ))

    for alert in alerts:
        print(f"🚨 [{alert.severity.upper()}] {alert.title}")
        print(f"   Detail: {alert.detail}")
        print(f"   Runbook: {alert.runbook_url}")
        print(f"   Time: {alert.timestamp}")

    if not alerts:
        print("✅ Post-deploy: all checks passed, no alerts")

    return alerts

Troubleshooting

Problema: Rolling update causa respuestas inconsistentes durante el deploy

Síntoma: Durante un rolling update, algunos requests retornan resultados con el prompt v1 y otros con el prompt v2, confundiendo a los usuarios.

Causa: En un rolling update, instancias con v1 y v2 coexisten temporalmente. Si el prompt cambió significativamente, los outputs serán diferentes.

Solución:

# Opción 1: Usar blue-green en vez de rolling para cambios de prompt
# → Elimina el período de versiones mixtas

# Opción 2: Si debes usar rolling, hacer el deploy en horario de bajo tráfico
# → Minimiza el número de usuarios afectados

# Opción 3: Incluir la versión del prompt en la respuesta
# → El cliente puede detectar y manejar la inconsistencia
# En AnalyzeResponse: prompt_version: str = Field(...)

Problema: Blue-green duplica costos de infraestructura

Síntoma: Tienes dos entornos completos corriendo, duplicando costos de servidores.

Causa: Blue-green requiere dos entornos completos para funcionar.

Solución:

# Para AI apps, el costo principal no es infraestructura sino llamadas al LLM
# El entorno inactivo no hace llamadas al LLM → costo adicional mínimo

# Si el costo de infraestructura sí importa:
# 1. Entorno inactivo con réplicas mínimas (1 instancia)
# 2. Auto-escalar el activo según demanda
# 3. Apagar el inactivo después de confirmar estabilidad (24-48h)

# En Docker Compose:
# blue (activo): replicas: 3
# green (standby): replicas: 1  ← listo para escalar si hay rollback

Problema: El canary no detecta degradación de calidad del modelo

Síntoma: Las métricas de error rate y latencia del canary están bien, pero la calidad de las respuestas del nuevo modelo es peor.

Causa: Las métricas estándar (error rate, latencia) no capturan la calidad semántica de las respuestas del LLM.

Solución:

def check_canary_quality(canary_logs: list[dict]) -> bool:
    """Métricas de calidad específicas para AI en tu canary check."""
    avg_confidence = sum(l["confidence"] for l in canary_logs) / len(canary_logs)

    if avg_confidence < 0.75:
        print(f"⚠️ Confidence promedio: {avg_confidence:.2f} (umbral: 0.75)")
        return False

    degraded_pct = sum(1 for l in canary_logs if l.get("degraded")) / len(canary_logs)
    if degraded_pct > 0.1:
        print(f"⚠️ {degraded_pct:.0%} de requests usaron fallback")
        return False

    return True

Resumen

  • Rolling: el default — bajo riesgo, sin downtime, rollback gradual
  • Blue-Green: cuando necesitas rollback instantáneo — ideal para cambios de prompts
  • Canary: cuando cambias algo de alto impacto (modelo, prompt crítico) — valida en producción real con poco tráfico
  • Prompts son código: se versionan, se testean, se deployan, se revierten como cualquier otro código
  • Cambiar modelo = cambiar comportamiento: siempre requiere testing antes del switch completo
  • Pre-deploy script: ejecutable antes de cada deploy para verificar los checklist items automatizables
  • Post-deploy monitoring: los primeros 30-60 minutos después del deploy son críticos

Recursos adicionales

  1. Martin Fowler — Blue-Green Deployment — El artículo canónico
  2. Martin Fowler — Canary Release — El patrón explicado
  3. Kubernetes Rolling Updates — Documentación
  4. GitHub Actions — CI/CD para automatizar el checklist pre-deploy