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?
- Cambiar el mensaje de error en un endpoint de texto
- Actualizar el prompt de análisis de sentimiento con una nueva instrucción importante
- Cambiar el modelo de gpt-4o-mini a gpt-4o en producción
Ver solución
- Cambiar mensaje de error: Rolling update — bajo riesgo, los mensajes de error no afectan la lógica
- Cambio importante de prompt: Blue-Green — el prompt nuevo puede dar outputs muy diferentes; quieres poder hacer rollback instantáneo si la calidad baja
- 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
- Martin Fowler — Blue-Green Deployment — El artículo canónico
- Martin Fowler — Canary Release — El patrón explicado
- Kubernetes Rolling Updates — Documentación
- GitHub Actions — CI/CD para automatizar el checklist pre-deploy