Eran las 3:15 AM de un martes cuando PagerDuty comenzó a sonar incesantemente. Nuestro cluster de producción, que maneja alrededor de 15,000 TPS durante picos de tráfico nocturno, estaba arrojando una cascada de errores 502 Bad Gateway y 504 Gateway Timeout. Al revisar los logs de AWS EKS, el escenario era desolador: múltiples nodos habían desaparecido simultáneamente. No fue un fallo de hardware, ni un bug en el código; fue la implacable naturaleza de las Instancias Spot reclamando su capacidad. A pesar de que la arquitectura prometía un ahorro del 70%, la inestabilidad estaba costando más en reputación que lo que ahorrábamos en infraestructura. Este artículo detalla cómo pasamos de interrupciones caóticas a un manejo elegante de fallos, alineándonos con los principios de FinOps sin sacrificar la calidad del servicio.
El Problema: La Brecha de los 2 Minutos
En nuestro entorno ejecutábamos Kubernetes versión 1.28 sobre nodos t3.large y m5.large. La promesa de las Instancias Spot es simple: AWS te ofrece capacidad de cómputo sobrante con un descuento masivo, pero con una condición crítica: pueden reclamar la instancia con solo 2 minutos de aviso.
El problema no es el reclamo en sí, sino cómo Kubernetes reacciona (o falla en reaccionar) ante él. Por defecto, el plano de control de EKS no es consciente de que AWS ha enviado una señal de terminación al sistema operativo del nodo. El resultado es que el Scheduler sigue enviando tráfico y programando Pods en un nodo que está a segundos de morir.
upstream connect error or disconnect/reset before headers. Esto ocurría porque las conexiones TCP establecidas se cortaban abruptamente cuando el kernel del nodo mataba los procesos Docker/Containerd sin previo aviso de drenaje.
Intentamos mitigar esto inicialmente sobredimensionando el cluster (Overprovisioning), pensando que tener más nodos absorbería el golpe. Fue un error. Si bien la capacidad de cómputo total era suficiente, el tiempo que tardaba el Load Balancer en marcar los targets como "unhealthy" era mayor que el tiempo que tardaba el nodo en apagarse. Necesitábamos una estrategia de Alta Disponibilidad proactiva, no reactiva.
Por qué PodDisruptionBudget (PDB) no fue suficiente
Nuestra primera "solución" fue configurar agresivos PodDisruptionBudgets y aumentar las livenessProbes. Asumimos que si garantizábamos que al menos el 80% de los pods estuvieran disponibles, el cluster sobreviviría. Sin embargo, los PDBs están diseñados para interrupciones voluntarias (como un kubectl drain manual o una actualización de versión). Cuando AWS reclama una instancia Spot, es un evento involuntario desde la perspectiva de K8s si no se conecta el evento de infraestructura con la API de Kubernetes. El nodo simplemente desaparecía, ignorando por completo nuestras reglas de presupuesto de interrupción.
La Solución: AWS Node Termination Handler + Graceful Shutdown
La clave para resolver esto es interceptar la notificación de metadatos de instancia (IMDS) que AWS envía y traducirla inmediatamente en comandos de Kubernetes Draining. Para esto, implementamos el AWS Node Termination Handler (NTH) en modo DaemonSet (para mayor simplicidad y menor latencia en clusters medianos).
A continuación, muestro la configuración crítica que desplegamos usando Helm. No basta con instalarlo; hay que configurarlo para que acordonar el nodo (cordon) sea inmediato.
# values.yaml para aws-node-termination-handler
# Recomendación: Usar modo IMDS para clusters < 100 nodos
enableSpotInterruptionDraining: true
enableRebalanceMonitoring: true
enableScheduledEventDraining: true
# Configuración crítica para evitar "Falsos Positivos" en logs
# y asegurar que el drenaje respete los tiempos de la aplicación
deleteLocalDataOnDrain: true
ignoreDaemonSets: true
# Webhook para notificar a Slack (Opcional pero recomendado para observabilidad)
webhookURL: "https://hooks.slack.com/services/..."
# Ajuste de recursos para asegurar que el handler nunca sea evictado
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
Al aplicar esta configuración, el flujo cambia drásticamente. Cuando AWS emite la señal de "Spot Interruption Warning":
- El Pod del NTH detecta el evento vía metadatos (
169.254.169.254). - Inmediatamente envía una orden de
cordonal nodo afectado (nadie nuevo entra). - Inicia el proceso de
drain, desalojando los pods existentes respetando susterminationGracePeriodSeconds.
Pero el trabajo no termina en la infraestructura. La aplicación también debe saber cómo morir dignamente. Si tu aplicación Java o Node.js recibe un SIGTERM y se apaga instantáneamente, seguirás perdiendo peticiones en vuelo (in-flight requests). Tuvimos que añadir un preStop hook en nuestros Deployments.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
template:
spec:
containers:
- name: app
image: my-app:v2
lifecycle:
preStop:
exec:
# Esperar 15s antes de matar el proceso permite
# que el Load Balancer deje de enviar tráfico y se completen transacciones.
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 45
El comando sleep 15 es contraintuitivo pero vital. Cuando Kubernetes pone un Pod en estado Terminating, elimina su IP de los Endpoints del Servicio. Sin embargo, esta propagación no es atómica ni instantánea en todo el cluster (kube-proxy, ingress controller, ALB). El sleep mantiene el contenedor vivo lo suficiente para procesar las peticiones residuales mientras la red converge.
| Métrica | Sin NTH (Legacy) | Con NTH + Draining |
|---|---|---|
| Tasa de Error (5xx) | 2.4% durante reclamos | 0.001% (casi nulo) |
| Tiempo de Recuperación | 5-10 minutos (Manual) | Automático (Segundos) |
| Ahorro Mensual (FinOps) | $0 (Usábamos On-Demand por miedo) | ~$4,200 (70% ahorro) |
Como se observa en la tabla, la mejora no fue solo técnica, sino financiera. La confianza en el sistema nos permitió migrar cargas de trabajo críticas (como el procesador de pagos) a Spot Instances, logrando el objetivo de FinOps que la dirección exigía.
Casos Borde y Advertencias
Aunque esta solución es robusta, existen escenarios donde el NTH en modo DaemonSet no es suficiente. Si gestionas clusters masivos (más de 100 nodos) o utilizas múltiples grupos de autoescalado, la interrogación constante a los metadatos de cada nodo puede generar saturación o rate-limits en la API de EC2.
Para esos casos, se recomienda implementar el NTH en Queue Processor Mode. Esta arquitectura escucha una cola SQS alimentada por eventos de CloudWatch EventBridge, centralizando la gestión de interrupciones en lugar de distribuir la detección en cada nodo. Además, ten cuidado con los StatefulSets; el drenaje automático puede corromper datos si la replicación de la base de datos no es lo suficientemente rápida para sincronizar antes de los 2 minutos.
Conclusión
Operar con Instancias Spot en un entorno productivo de Kubernetes es un juego de alto riesgo si no se tienen las protecciones adecuadas. La implementación del Node Termination Handler, combin
Post a Comment