La Hipótesis de Estado Estable

Todo experimento de chaos comienza con una hipótesis. Una acción desestructurada como terminar un pod al azar sin expectativas medibles no es un experimento. Una hipótesis correctamente formulada tiene esta forma:

"Cuando terminemos el 30% de los pods de API en el namespace de checkout, esperamos: la tasa de éxito de requests se mantiene arriba del 99.5%, la latencia p99 se queda debajo de 500ms, y el HPA de Kubernetes escala pods de reemplazo dentro de 60 segundos."

Esta es tu hipótesis de estado estable. Define:

  1. La perturbación, qué se está rompiendo
  2. Las métricas de estado estable, cómo se ve lo "normal"
  3. La tolerancia, cuánta desviación es aceptable

Antes de correr nada, se necesita un baseline. Capturá al menos 30 minutos de métricas de estado estable bajo carga representativa:

# Capturar métricas baseline con Prometheus
cat <<'PROMQL'
# Tasa de éxito (debería ser >99.9% en baseline)
sum(rate(http_requests_total{status=~"2.."}[5m]))
/
sum(rate(http_requests_total[5m]))

# Latencia p99 (capturar valor baseline)
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

# Cantidad de pods (baseline para verificación de scaling)
count(kube_pod_status_phase{namespace="checkout", phase="Running"})
PROMQL

Registrá estos valores. Son tu snapshot del "antes". Si tus métricas de estado estable ya son inestables antes del experimento, arreglá eso primero. No es posible detectar degradación inducida por chaos si el baseline es ruidoso.

LitmusChaos: chaos nativo de Kubernetes

LitmusChaos es el proyecto de chaos engineering del CNCF construido para Kubernetes. Usa CRDs para definir experimentos, lo que significa que tus experimentos de chaos son YAML versionado y revisable, igual que todo lo demás en tu cluster.

Instalalo:

helm repo add litmuschaos https://litmuschaos.github.io/litmus-helm/
helm install litmus litmuschaos/litmus \
 --namespace litmus --create-namespace \
 --set portal.server.serviceType=ClusterIP

Este es un experimento representativo, eliminación de pods en el namespace de checkout con controles específicos de blast radius:

apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
 name: checkout-pod-kill
 namespace: checkout
spec:
 engineState: active
 appinfo:
 appns: checkout
 applabel: app=checkout-api
 appkind: deployment
 chaosServiceAccount: litmus-admin
 experiments:
 - name: pod-delete
 spec:
 components:
 env:
 - name: TOTAL_CHAOS_DURATION
 value: "120"
 - name: CHAOS_INTERVAL
 value: "30"
 - name: PODS_AFFECTED_PERC
 value: "30"
 - name: FORCE
 value: "false"
 - name: SEQUENCE
 value: "parallel"
 probe:
 - name: success-rate-probe
 type: promProbe
 mode: Continuous
 runProperties:
 probeTimeout: 5s
 interval: 10s
 retry: 3
 promProbe/inputs:
 endpoint: http://prometheus:9090
 query: >
 sum(rate(http_requests_total{namespace="checkout",status=~"2.."}[1m]))
 /
 sum(rate(http_requests_total{namespace="checkout"}[1m]))
 comparator:
 type: float
 criteria: ">="
 value: "0.995"
 - name: latency-probe
 type: promProbe
 mode: Continuous
 runProperties:
 probeTimeout: 5s
 interval: 10s
 retry: 3
 promProbe/inputs:
 endpoint: http://prometheus:9090
 query: >
 histogram_quantile(0.99,
 sum(rate(http_request_duration_seconds_bucket{namespace="checkout"}[1m]))
 by (le))
 comparator:
 type: float
 criteria: "<="
 value: "0.5"

Los probes son la red de seguridad. Si la tasa de éxito baja del 99.5% o el p99 supera 500ms, LitmusChaos aborta el experimento y el chaos engine transiciona a estado stopped. Esto es innegociable, nunca corras un experimento sin condiciones de abort.

Chaos de red: el verdadero asesino

Matar pods es el "hello world" del chaos engineering. Los outages reales usualmente involucran particiones de red, fallas de DNS e inyección de latencia. LitmusChaos maneja estos también:

apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
 name: checkout-network-latency
 namespace: checkout
spec:
 engineState: active
 appinfo:
 appns: checkout
 applabel: app=checkout-api
 appkind: deployment
 experiments:
 - name: pod-network-latency
 spec:
 components:
 env:
 - name: NETWORK_INTERFACE
 value: "eth0"
 - name: TARGET_CONTAINER
 value: "checkout-api"
 - name: NETWORK_LATENCY
 value: "200"
 - name: JITTER
 value: "50"
 - name: TOTAL_CHAOS_DURATION
 value: "180"
 - name: DESTINATION_IPS
 value: "10.0.15.0/24"

Este tipo de experimento comúnmente detecta bugs en la lógica de reintentos. Considerar backoff exponencial con un máximo de 3 reintentos, pero el timeout por intento era de 5 segundos. Agregale 200ms de latencia de red a cada llamada a la DB, y de repente cada reintento tarda 5.2 segundos. Tres reintentos = 15.6 segundos en total. El gateway HTTP tenía un timeout de 10 segundos. Cada intento de reintento después del segundo era trabajo tirado, el cliente ya había recibido un 504.

La solución fue simple: timeout por intento de 2 segundos, timeout total de 8 segundos, máximo 4 reintentos. Pero este tipo de problema rara vez se detecta sin inyectar latencia específicamente en el path de la base de datos.

Chaos Monkey y el linaje de Netflix

Chaos Monkey, la herramienta original de chaos de Netflix, opera diferente a LitmusChaos. Termina instancias al azar en tu entorno de AWS. La filosofía es chaos continuo, corre constantemente, matando cosas al azar, y tus sistemas lo manejan o no.

Para setups nativos de AWS, el Simian Army (la evolución de Chaos Monkey) se integra con Auto Scaling Groups:

import boto3
import random
import logging

logger = logging.getLogger('chaos-monkey')

def chaos_monkey_run(asg_name: str, kill_probability: float = 0.1):
 """Mata una instancia random en el ASG con la probabilidad dada"""
 ec2 = boto3.client('ec2')
 asg = boto3.client('autoscaling')

 response = asg.describe_auto_scaling_groups(
 AutoScalingGroupNames=[asg_name]
 )
 instances = response['AutoScalingGroups'][0]['Instances']
 healthy = [i for i in instances if i['HealthStatus'] == 'Healthy']

 if len(healthy) <= 1:
 logger.warning(f"Solo {len(healthy)} instancia(s) healthy en {asg_name}, salteando")
 return

 if random.random() > kill_probability:
 logger.info(f"Los dados dicen que hoy no hay chaos para {asg_name}")
 return

 victim = random.choice(healthy)
 instance_id = victim['InstanceId']

 logger.info(f"Terminando {instance_id} en {asg_name}")
 ec2.terminate_instances(InstanceIds=[instance_id])

 return instance_id

La salvaguarda clave: nunca matés si solo queda una instancia healthy. Y observar el parámetro de probabilidad, en producción, correr al 10% de probabilidad por ciclo es lo común. La mayoría de los ciclos no hacen nada. Pero en una semana, cada servicio recibe un golpe.

Metodología GameDay

Un GameDay es una sesión estructurada de chaos donde todo el equipo participa. No es solo sobre el experimento técnico, es sobre observar cómo los humanos responden a las fallas.

Este es un template probado:

## Plan de GameDay: Failover de Base de Datos

**Fecha**: 2026-01-09 14:00 UTC
**Duración**: 90 minutos (60 min experimento + 30 min retro)
**Facilitador**: Lucio
**Participantes**: Equipo backend, SRE, ingeniero de guardia

### Pre-condiciones
- [ ] Todos los participantes informados del alcance del experimento
- [ ] Dashboards abiertos: Grafana main, PagerDuty, Slack #incidents
- [ ] Procedimiento de abort documentado y entendido
- [ ] Soporte al cliente notificado (no detalles, solo "estamos corriendo tests de resiliencia")

### Hipótesis de estado estable
Cuando la base de datos primaria (RDS Aurora) haga failover a la read replica:
1. Las operaciones de escritura pausan por <10 segundos
2. Las operaciones de lectura continúan sin interrupción
3. La aplicación reconecta automáticamente dentro de 30 segundos
4. No hay pérdida de datos
5. La alerta de PagerDuty se dispara dentro de 2 minutos

### Condiciones de abort
- Error rate supera 20% por más de 60 segundos
- Outage completo del servicio (0 requests exitosos)
- Corrupción de datos detectada
- Cualquier participante llama abort

La retro es donde vive el valor real. Un GameDay típico puede revelar:

  1. El connection pool reconecta bien, pero mantiene conexiones stale por 45 segundos antes de health-checkearlas
  2. La alerta de PagerDuty no se dispara porque el intervalo de chequeo es de 5 minutos, ajustarlo a 30 segundos resuelve esto
  3. El ingeniero de guardia no sabe dónde está el runbook de failover de RDS, agregarlo al wiki de respuesta a incidentes cierra la brecha

Nada de esto habría salido a la superficie leyendo diagramas de arquitectura.

Control de blast radius: lo innegociable

Todo experimento necesita límites de blast radius. Estos deben aplicarse en múltiples capas:

Nivel de infraestructura: Etiquetá nodos y namespaces que son elegibles para chaos. Todo lo demás está fuera de límites.

# Solo estos nodos pueden ser target
kubectl label nodes worker-{1..3} chaos-eligible=true

# Opt-in a nivel de namespace
kubectl annotate namespace checkout litmuschaos.io/chaos=true

Nivel de experimento: Tope en el porcentaje de recurse es afectados. Nunca pases del 50% para un primer experimento en cualquier target.

Nivel temporal: Configurar duración máxima. Si el experimento excede su ventana, se auto-revierte.

Nivel de métricas: Probes que auto-abortan cuando los budgets de SLO están amenazados.

Nivel organizacional: Aprobación de change management para experimentos en producción. Un proceso liviano de RFC funciona bien, un thread en Slack en #chaos-proposals con el YAML del experimento adjunto y dos aprobaciones de SRE requeridas.

Construyendo el hábito

Chaos engineering no es una herramienta, es una práctica. La herramienta es la parte fácil. Lo difícil es hacerlo un hábito. Esto es lo que funciona:

  1. Slot semanal de chaos: Todos los jueves a las 14:00 UTC, corre un experimento. Está en el calendario del equipo. Innegociable.
  2. Scoreboard de chaos: Trackeá experimentos corridos, issues encontrados, mejoras en MTTR. Hacelo visible.
  3. Pipeline de incidente a experimento: Después de cada incidente, preguntá "¿qué experimento de chaos habría catcheado esto?" Agregalo al backlog.
  4. Complejidad graduada: Iniciar con pod kills. Graduá a chaos de red. Después fallas multi-AZ. Después failovers regionales. Cada nivel requiere éxito demostrado en el nivel anterior.

Un experimento de failover de base de datos como el descrito arriba puede llevar a un fix del connection pool que prevenga múltiples outages potenciales durante el año siguiente. Cada uno sería un incidente p1 afectando a miles de usuarios. Cuarenta y siete segundos de disrupción durante un experimento controlado es una lección extraordinariamente barata comparada con la alternativa.