Un monitor de Datadog se dispara a las 2:14 AM. El CPU de RDS está al 95%. El engineer de on-call abre la laptop, revisa CloudWatch, mira los logs de pods de Kubernetes, busca en Slack la última vez que pasó esto, recuerda vagamente un post-mortem en algún lugar de Confluence, no lo encuentra, arranca de cero. Cuarenta y cinco minutos después tiene una causa raíz.
El agente que construimos tarda 8 segundos.
No porque lea métricas más rápido — cualquier dashboard puede leer métricas. Sino porque simultáneamente conoce la arquitectura de la empresa, recupera el runbook exacto para ese cluster RDS, trae el post-mortem de la última vez que pasó esto, revisa la config de Terraform para entender por qué max_connections podría estar mal configurado, lee el historial reciente de rollouts de Kubernetes, y cruza la latencia de edge de Cloudflare. No solo ve el incendio. Entiende el edificio.
Esa es la diferencia entre una IA que lee tu infraestructura y una que la conoce. La tecnología que hace esto posible es Retrieval-Augmented Generation — y no es una mejora menor. Es todo el juego.
Con Qué Trabajábamos
El cliente corre una plataforma de gaming multijugador: cientos de miles de jugadores concurrentes durante eventos pico, requisitos de latencia sub-100ms en todo el stack, y el modelo de amenaza único que viene con el gaming competitivo — los ataques DDoS son una característica del paisaje, no una excepción. Su infraestructura:
- Kubernetes (EKS) para todos los workloads de aplicación
- Datadog para APM, logs, métricas, monitors y synthetics
- AWS para RDS (PostgreSQL), nodos EC2, load balancers ELB, alarmas de CloudWatch
- Cloudflare para WAF, mitigación de DDoS, caché CDN y routing de edge
- Un wiki de Confluence con años de docs de arquitectura, runbooks, playbooks de on-call y post-mortems
- Un backend en Go y frontend en React viviendo en Git
- Terraform gestionando toda la infraestructura
La situación de on-call antes de que arrancáramos:
| Métrica | Línea de Base |
|---|---|
| Tiempo medio de resolución (MTTR) | 45 minutos |
| Incidentes que requirieron escalación | ~68% |
| Incidentes diagnosticados correctamente en los primeros 10 min | ~22% |
| Pages de on-call por semana | ~31 |
| Horas de engineer/semana en respuesta a incidentes | ~18 horas |
El equipo no era lento. La respuesta a incidentes es genuinamente difícil cuando el contexto está disperso en una docena de sistemas. Nos propusimos centralizar ese contexto — y después hacerlo consultable en tiempo real.
La Arquitectura: Cinco Integraciones, Un Cerebro RAG
El agente tiene dos capas: recolección de datos en vivo e indexación de conocimiento recuperado. Los datos en vivo le dicen qué está pasando ahora mismo. RAG le dice qué significa.
Capa 1 — Integraciones en Vivo (Solo Lectura)
La integración con Datadog usa la API v2 con un par de API/App key con scope acotado. El agente puede consultar cualquier métrica, leer cualquier log, inspeccionar cualquier trace de APM, y revisar el estado de monitors y las tasas de burn de SLOs en tiempo real.
def query_datadog_metrics(metric: str, query: str, from_time: int, to_time: int) -> dict:
"""Query Datadog metrics API for a time series."""
url = "https://api.datadoghq.com/api/v1/query"
params = {
"from": from_time,
"to": to_time,
"query": query,
}
headers = {
"DD-API-KEY": DD_API_KEY,
"DD-APPLICATION-KEY": DD_APP_KEY,
}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
# Ejemplo: CPU de RDS en los últimos 30 minutos
rds_cpu = query_datadog_metrics(
metric="aws.rds.cpuutilization",
query="avg:aws.rds.cpuutilization{dbinstanceidentifier:prod-postgres-primary}",
from_time=int(time.time()) - 1800,
to_time=int(time.time()),
)
La integración con Kubernetes usa el cliente oficial de Python con un ClusterRole de solo lectura — get/list/watch sobre pods, eventos, deployments, HPAs, nodos y replicasets. Sin verbos de escritura en ningún lado.
La integración con AWS usa boto3 con un IAM role de solo lectura: cloudwatch:GetMetricData, rds:Describe*, ec2:Describe*, elasticloadbalancing:Describe*. El agente puede revisar el replication lag de RDS, conteos de conexiones, alarmas de CloudWatch y salud de ELB en una sola llamada.
La integración con Cloudflare usa la Analytics API con un token de solo lectura scoped a Zone Analytics, Firewall Analytics y Cache Analytics. El agente revisa conteos de eventos WAF, bot scores, estado de mitigación DDoS, cache hit ratios y latencia de edge por región.
Capa 2 — La Base de Conocimiento RAG (La Protagonista)
Acá es donde el agente pasa de "dashboard útil" a "el engineer que lleva cinco años acá y recuerda todo".
Indexamos cuatro corpus en Pinecone usando el modelo text-embedding-3-large de OpenAI (3.072 dimensiones, consistentemente mejor recall que ada-002 en contenido técnico):
-
Wiki de Confluence — cada doc de arquitectura, runbook, playbook de on-call, línea de tiempo de incidentes y registro de decisiones. Chunkeado a 512 tokens con overlap de 64 tokens. Metadatos: título de página, última modificación, path de sección, autor.
-
Código fuente — el backend en Go y el frontend en React, chunkeados por función y módulo. Los metadatos incluyen path del archivo, nombre de función, paquete y contexto circundante. El agente puede buscar la implementación exacta de un endpoint de API, encontrar todos los lugares donde se consulta una tabla específica de la base de datos, o recuperar patrones de manejo de errores para un servicio específico.
-
Terraform/IaC — cada definición de recurso, módulo y archivo de variables. Cuando el agente ve una alarma de RDS, puede recuperar el recurso de Terraform que define esa instancia: la clase de instancia, los valores del parameter group de
max_connections, la retención de backups, la configuración multi-AZ, y qué security groups permiten el acceso. Esto cierra la brecha entre "la alarma está disparada" y "la configuración que la está causando". -
RCAs e incidentes pasados — cada reporte de incidente que el equipo escribió. Embebido con el título del incidente, servicios afectados, resumen de causa raíz y pasos de resolución. El agente tiene la biblioteca completa de patrones de fallas históricas.
La función de recuperación corre en el momento de la consulta con el contexto completo del incidente como query:
def retrieve_relevant_context(
query: str,
namespaces: list[str],
top_k: int = 8,
) -> list[dict]:
"""
Retrieve relevant chunks from Pinecone across multiple namespaces.
namespaces: ["runbooks", "source-code", "terraform", "incidents"]
"""
query_embedding = openai_client.embeddings.create(
input=query,
model="text-embedding-3-large",
).data[0].embedding
results = []
for namespace in namespaces:
response = pinecone_index.query(
vector=query_embedding,
top_k=top_k,
namespace=namespace,
include_metadata=True,
)
for match in response.matches:
results.append({
"namespace": namespace,
"score": match.score,
"text": match.metadata["text"],
"source": match.metadata.get("source", "unknown"),
"title": match.metadata.get("title", ""),
})
# Ordenar por score de relevancia en todos los namespaces
results.sort(key=lambda x: x["score"], reverse=True)
return results[:top_k * 2] # Devolver los mejores matches entre todos los namespaces
Fase 1 — Recolección de Contexto en Paralelo
Cuando se dispara un webhook de Datadog, el agente recolecta datos en vivo de las cuatro integraciones de forma simultánea. La infraestructura de gaming se mueve rápido — un DDoS puede pasar de cero a tráfico total en menos de 30 segundos — por lo que la recolección de datos secuencial no es una opción.
import asyncio
from dataclasses import dataclass
@dataclass
class IncidentContext:
alert: dict
datadog: dict
kubernetes: dict
aws: dict
cloudflare: dict
rag_chunks: list[dict]
async def collect_incident_context(alert: dict) -> IncidentContext:
"""Collect all live context in parallel, then query RAG."""
# Etapa 1: todas las integraciones en vivo se disparan simultáneamente
dd_task = asyncio.create_task(collect_datadog_context(alert))
k8s_task = asyncio.create_task(collect_kubernetes_context(alert))
aws_task = asyncio.create_task(collect_aws_context(alert))
cf_task = asyncio.create_task(collect_cloudflare_context(alert))
dd_ctx, k8s_ctx, aws_ctx, cf_ctx = await asyncio.gather(
dd_task, k8s_task, aws_task, cf_task,
return_exceptions=True, # Una fuente que falla no bloquea las demás
)
# Etapa 2: construir una query rica del contexto en vivo para la recuperación RAG
rag_query = build_rag_query(alert, dd_ctx, k8s_ctx, aws_ctx, cf_ctx)
rag_chunks = await asyncio.get_event_loop().run_in_executor(
None,
retrieve_relevant_context,
rag_query,
["runbooks", "source-code", "terraform", "incidents"],
)
return IncidentContext(
alert=alert,
datadog=dd_ctx,
kubernetes=k8s_ctx,
aws=aws_ctx,
cloudflare=cf_ctx,
rag_chunks=rag_chunks,
)
Tiempo total de recolección: 4–7 segundos para datos en vivo, más ~1,2 segundos para el embedding RAG y la recuperación. El engineer obtiene un panorama completo antes de desbloquear su laptop.
Fase 2 — Correlación y Análisis con Claude
El contexto ensamblado va a Claude Sonnet para análisis profundo. Usamos Sonnet (no Haiku) acá porque la correlación multi-fuente — cinco streams de datos en vivo más ocho o más chunks RAG — requiere el tipo de razonamiento estructurado donde el tiempo adicional de inferencia (6–9 segundos) vale la pena. Para eventos simples de un solo monitor, primero lo mandamos a Haiku y solo escalamos si la confianza es MEDIA o inferior.
def analyze_incident(context: IncidentContext) -> dict:
"""Run full incident analysis with Claude Sonnet."""
rag_context_block = format_rag_chunks(context.rag_chunks)
live_context_block = format_live_context(context)
system_prompt = """You are an SRE analyst with deep expertise in this specific gaming platform.
You have access to real-time metrics, logs, Kubernetes state, AWS infrastructure, and
Cloudflare edge data. You also have retrieved relevant runbooks, past incident reports,
Terraform configurations, and source code from the team's knowledge base.
Your job: given all this context, identify the most likely root cause, assess confidence,
and suggest the safest corrective action.
Rules:
- Correlate timing precisely: if a K8s rollout happened 90 seconds before the alert, flag it
- Cross-reference RAG findings explicitly: "per the RDS runbook retrieved (score 0.91)..."
- Distinguish DDoS from legitimate traffic spikes using Cloudflare WAF data and event calendar
- Rate confidence HIGH / MEDIUM / LOW with explicit reasoning
- Always suggest rollback before config changes; config changes before restarts
- If a past incident matches this pattern, reference it by incident ID"""
response = anthropic_client.messages.create(
model="claude-sonnet-4-5",
max_tokens=3000,
system=system_prompt,
messages=[{
"role": "user",
"content": f"LIVE CONTEXT:\n{live_context_block}\n\nRAG RETRIEVED:\n{rag_context_block}",
}],
)
return parse_analysis(response.content[0].text)
Fase 3 — Brief de Incidente en Slack
El agente postea un brief estructurado en el canal de incidentes del equipo. El formato evolucionó durante varios meses de iteración — esto es lo que el equipo realmente usa para tomar decisiones:
🔴 INCIDENTE — prod-postgres-primary: CPU 95% (umbral: 80%)
Disparado: 02:14 UTC | Duración: 6 min
📊 ESTADO EN VIVO
• RDS CPU: 95% | Conexiones: 487/500 | Replication lag: 2,1s
• K8s: deployment game-api hizo rollout a v3.41.0 @ 02:11 UTC (2 min antes del pico)
• Cloudflare: WAF limpio, sin señal DDoS | Cache hit: 71% (normal)
• ELB: saludable, sin pico de 5xx en edge
🔍 CAUSA RAÍZ — confianza ALTA (0,87)
Código nuevo en game-api v3.41.0 introdujo una query de leaderboard que realiza
un full table scan en `player_scores` (recuperado de código fuente:
leaderboard_service.go:247, commit a3f91b2). Bajo carga concurrente durante
el inicio del torneo diario de las 02:00 UTC, esta query satura las conexiones RDS.
📚 MATCH EN BASE DE CONOCIMIENTO
• Runbook: "RDS Connection Exhaustion — prod-postgres-primary" (score 0,93)
→ Recomienda agregar índice en (game_id, score DESC) + cap de connection pool en 400
• Incidente pasado: INC-2024-047 (score 0,89) — patrón idéntico, misma ventana de torneo
→ Resolución: rollback del deploy + índice agregado. CPU RDS se normalizó en 4 min.
• Config Terraform: rds_prod.tf línea 34 — max_connections via parameter group: 500
→ El runbook recomienda 400 máx para dejar margen a conexiones de admin
💡 ACCIONES RECOMENDADAS
1. ROLLBACK de game-api a v3.40.9 (inmediato, menor riesgo)
→ kubectl rollout undo deployment/game-api -n production
2. Después de que el rollback se estabilice, agregar índice de DB (de resolución INC-2024-047):
→ CREATE INDEX CONCURRENTLY idx_player_scores_game_score
ON player_scores (game_id, score DESC);
3. Capear connection pool en config de game-api: DB_POOL_MAX=80 (según runbook)
📋 DESGLOSE DE CONFIANZA
• Correlación de deploy: v3.41.0 desplegado 2 min antes del pico ✓
• Match en código fuente: full table scan en nueva query de leaderboard ✓
• Match histórico: INC-2024-047 patrón idéntico ✓
• Cloudflare limpio: descarta DDoS ✓
El engineer lee esto, verifica que el timestamp del rollout coincida, y ejecuta el comando kubectl. Sin buscar. Sin revisar Confluence a las 2 AM.
Lo Que el Agente Encontró Que los Humanos Consistentemente Pasaban por Alto
Durante los primeros cuatro meses en producción surgieron tres categorías de hallazgos.
Patrones de degradación lenta. Un memory leak en un microservicio Go estaba creciendo un 0,3% por hora — visible únicamente durante sesiones de gaming de 8 horas e invisible en cualquier dashboard sin la ventana de tiempo correcta. El agente correlacionó métricas de memoria de contenedores de Datadog con el historial de reinicios de pods de Kubernetes a lo largo de tres semanas de datos y surfaceó la tendencia antes del próximo torneo importante. El equipo había estado atribuyendo los reinicios a "OOMKilled intermittentemente" sin conectarlos con la duración de las sesiones.
Configuration drift entre sistemas. Una réplica de lectura de RDS estaba corriendo 45 segundos detrás del primario durante horas pico. El agente recuperó la config de Terraform vía RAG (rds_replica.tf, max_connections = 100 — el default) y la comparó con la recomendación del runbook (400 para esta clase de instancia). También encontró el doc de mejores prácticas de AWS RDS en el wiki que indicaba que un max_connections bajo causa acumulación de cola bajo replicación con muchas escrituras. Los humanos del equipo nunca habían conectado esos tres documentos en el mismo modelo mental.
Falsos positivos silenciosos del WAF. Una regla del WAF de Cloudflare agregada después de un evento DDoS estaba bloqueando upgrades de conexión WebSocket legítimas desde regiones específicas — los jugadores de esas áreas reportaban desconexiones aleatorias, que soporte tagueaba como "problemas del lado del cliente". El agente correlacionó los logs de eventos de bloqueo del WAF (Cloudflare API) con tickets de queja de jugadores embebidos en el wiki de soporte (RAG), los hizo coincidir por región y timestamp, y elevó el hallazgo con un ID de regla WAF específico y el patrón de excepción recomendado. Esto había estado invisible por seis semanas.
Los Números Finales
Después de cuatro meses en producción:
| Métrica | Antes | Después | Cambio |
|---|---|---|---|
| Tiempo medio de resolución (MTTR) | 45 min | 8 min | -82% |
| Incidentes autodiagnosticados (confianza ALTA) | — | 71% | — |
| Tasa de falsos positivos | — | 6% | — |
| Escalaciones de on-call que requirieron despertar a alguien | ~68% | ~27% | -60% |
| Horas de engineer/semana en respuesta a incidentes | ~18 hs | ~6 hs | -67% |
| Incidentes resueltos antes de que el engineer actúe | 0% | 43% | — |
El 43% de "resueltos antes de que el engineer actúe" cubre incidentes L1 donde el diagnóstico del agente fue de confianza ALTA, la acción era un rollback o un cambio de config conocido como seguro, y el gate de auto-remediation (validación de segunda instancia de Claude + allowlist de acciones) lo aprobó automáticamente. Sin despertar a nadie.
Qué Haríamos Diferente
Indexar el schema de métricas primero, no último. Pasamos dos semanas después del deploy inicial construyendo mappings entre nombres de métricas de Datadog y su significado de negocio — qué métrica aws.rds.* corresponde a qué cluster RDS específico, cuál es el rango normal para el p99 de cada servicio. Embeber este schema en el índice RAG como documento estructurado (en lugar de intentar inferirlo en el momento de la consulta) habría mejorado la precisión desde el día uno. Hacé esto antes de indexar cualquier otra cosa.
Chunkear Terraform por recurso, no por archivo. Inicialmente chunkeamos los archivos de Terraform por conteo de tokens, lo que a veces dividía un bloque de recurso entre dos chunks, perdiendo la conexión entre los atributos de un recurso. Cambiar a chunking consciente de recursos — cada bloque resource {} como unidad atómica, siempre junto — mejoró notablemente la capacidad del agente para entender la configuración de infraestructura. El límite correcto del chunk es semántico, no posicional.
Construir el pipeline de actualización RAG antes del agente, no después. Lanzamos el agente hablando con un índice estático, y después nos apuramos a construir las integraciones de webhooks de CI/CD para mantenerlo actualizado. Una base de conocimiento desactualizada es peor que ninguna — el agente recupera runbooks obsoletos con confianza. El pipeline de re-indexación debería ser lo primero que construyas, no lo último.
Arrancar con RAG solo de incidentes antes de agregar código fuente. El corpus de post-mortems y runbooks entregó mejoras de precisión inmediatas y medibles. El corpus de código fuente agregó valor significativo pero también introdujo ruido de recuperación — las firmas de funciones Go a veces hacían match por sintaxis en lugar de relevancia semántica. Tendríamos que haber ajustado la calidad de recuperación en el corpus más simple primero, y recién después agregar el más ruidoso.
¿Estás construyendo un agente SRE para un entorno multi-stack complejo? Las integraciones son la parte fácil — hacer bien la base de conocimiento RAG es donde la mayoría de los equipos subestima la inversión necesaria. Hablemos de la arquitectura antes de que empieces a indexar.