Karpenter + Spot Instances + Scale-to-Zero: Cómo Bajamos los Costos de EKS un 70%

Hexagonal Kubernetes nodes consolidating in orbital formation with gold lightning bolts and navy void background, representing Spot instance efficiency gains

Nuestro cliente, una fintech Serie B con más de 200 microservicios en EKS, estaba quemando $47.000/mes solo en cómputo de Kubernetes. Las palabras exactas de su CTO: "Estamos gastando más en infraestructura que en salarios de ingeniería. Algo está muy mal."

No estaba equivocado. Después de dos semanas de auditoría, encontramos los sospechosos de siempre: pods sobreaprovisionados, clusters de dev/staging siempre encendidos, cero Spot instances, y Cluster Autoscaler peleando contra node groups que no matcheaban los patrones de carga.

Acá está exactamente lo que hicimos para bajar esos $47K a $14K — una reducción del 70.2% — sin un solo incidente en producción.

La Auditoría: A Dónde Iba la Plata

Primero corrimos un análisis completo de utilización de recursos. Los números eran brutales:

Métrica Valor
Utilización promedio de CPU 12%
Utilización promedio de memoria 23%
Nodos corriendo 24/7 38
Pods sin HPA 87%
Uso de Spot instances 0%
Uptime de clusters dev/staging 24/7

En otras palabras: estaban pagando por 8 veces más cómputo del que necesitaban, y corriendo entornos de desarrollo a toda hora para un equipo que trabaja de 9 a 18.

Fase 1: Karpenter Reemplaza Cluster Autoscaler

Cluster Autoscaler funciona, pero es lento y rígido. Necesita node groups predefinidos, no puede mezclar tipos de instancia de forma eficiente, y tarda 3-5 minutos en hacer scale-up. Karpenter es lo opuesto: mira los pods pendientes, encuentra el tipo de instancia más barato que los contiene, y lo aprovisiona en menos de 60 segundos.

Nuestra configuración de NodePool para Karpenter:

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: node.kubernetes.io/instance-type
          operator: In
          values:
            - m5.large
            - m5.xlarge
            - m5a.large
            - m5a.xlarge
            - m6i.large
            - m6i.xlarge
            - c5.large
            - c5.xlarge
            - c5a.large
            - r5.large
            - r5a.large
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["us-east-1a", "us-east-1b", "us-east-1c"]
      nodeClassRef:
        name: default
  limits:
    cpu: "200"
    memory: 400Gi
  disruption:
    consolidationPolicy: WhenUnderutilized
    consolidateAfter: 30s

Decisiones clave:

  • Selección amplia de tipos de instancia. Más tipos de instancia = mayor disponibilidad de Spot y precios más bajos. Listamos 11 familias en vez de encerrarnos en una sola.
  • Multi-AZ. La capacidad de Spot varía por AZ. Distribuirnos en 3 zonas significa que casi nunca nos interrumpen.
  • Consolidación agresiva. consolidateAfter: 30s hace que Karpenter repaquete los pods en menos nodos en cuanto baja la utilización. Se acabaron los nodos a medio llenar.

Resultados: Karpenter vs Cluster Autoscaler

Métrica Antes (CA) Después (Karpenter)
Tiempo de scale-up 3-5 min 30-60 seg
Cantidad de nodos (promedio) 38 14
Diversidad de tipos de instancia 2 tipos 11 tipos
Eficiencia de bin packing ~30% ~78%

Fase 2: Spot Instances para el 80% de las Cargas

La realidad sobre Spot: la mayoría de los equipos le tienen miedo porque creen que las cargas van a morir de forma aleatoria. En la práctica, con la configuración correcta, las tasas de interrupción de Spot son menores al 5% en pools de instancias diversificados.

Clasificamos cada carga en tres tiers:

Tier 1: Spot-Ready (80% de los pods)

Servicios stateless, workers en background, batch jobs, cualquier cosa con graceful shutdown.

# Agregado a cada deployment elegible para Spot
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 120
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api-gateway

Tier 2: On-Demand con Fallback a Spot (15%)

Servicios stateful, proxies de bases de datos, servicios con conexiones long-running.

Tier 3: On-Demand Only (5%)

Brokers de Kafka, primaries de Redis, cualquier cosa donde una interrupción de nodo provoque pérdida de datos.

Usamos los node labels karpenter.sh/capacity-type de Karpenter y reglas de pod affinity para rutear las cargas a los nodos correctos:

# En el spec del deployment para cargas Tier 1
affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 90
        preference:
          matchExpressions:
            - key: karpenter.sh/capacity-type
              operator: In
              values: ["spot"]

El preferredDuringScheduling significa: intentá Spot primero, pero hacé fallback a On-Demand si no hay capacidad Spot disponible. Ningún pod se queda sin schedulear.

Ahorro con Spot

Descuento promedio de Spot sobre nuestra mezcla de instancias: 67% respecto a On-Demand. Como el 80% de las cargas corrían en Spot, el descuento combinado fue de aproximadamente 54%.

Fase 3: Scale-to-Zero para Dev y Staging

Esta fue la ganancia más fácil con el mayor impacto. Los clusters de dev y staging corrían 24/7 para un equipo que trabaja de 9 a 18, de lunes a viernes. Eso es 72% de cómputo desperdiciado.

Implementamos KEDA (Kubernetes Event-Driven Autoscaling) con scaling basado en cron:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: api-gateway-dev
  namespace: development
spec:
  scaleTargetRef:
    name: api-gateway
  minReplicaCount: 0
  maxReplicaCount: 2
  triggers:
    - type: cron
      metadata:
        timezone: America/Argentina/Buenos_Aires
        start: "0 9 * * 1-5"   # Scale up lun-vie 9 AM
        end: "0 19 * * 1-5"    # Scale down lun-vie 7 PM
        desiredReplicas: "2"

Para staging, agregamos un trigger HTTP para que suba on-demand cuando alguien golpea el endpoint:

triggers:
  - type: cron
    metadata:
      timezone: America/Argentina/Buenos_Aires
      start: "0 9 * * 1-5"
      end: "0 19 * * 1-5"
      desiredReplicas: "2"
  - type: prometheus
    metadata:
      serverAddress: http://prometheus:9090
      metricName: http_requests_total
      query: sum(rate(http_requests_total{namespace="staging"}[5m]))
      threshold: "1"

Cuando todos los pods de dev/staging escalan a cero, la consolidación de Karpenter se activa y elimina los nodos por completo. Cero pods = cero nodos = cero costo fuera del horario laboral.

Impacto del Scale-to-Zero

Entorno Antes (24/7) Después (Programado) Ahorro
Development $8.200/mes $2.300/mes 72%
Staging $6.100/mes $1.800/mes 70%

Fase 4: Right-Sizing de los Pods de Producción

La pieza final: la mayoría de los pods pedían 2-4 veces más recursos de los que usaban. Deployamos Vertical Pod Autoscaler en modo recomendación durante 2 semanas, y después aplicamos las sugerencias:

# Antes
resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "1000m"
    memory: "1Gi"

# Después (basado en recomendaciones de VPA)
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "300m"
    memory: "384Mi"

Esto solo redujo la cantidad de nodos necesarios un 40%, porque Karpenter aprovisiona instancias más chicas (y más baratas) cuando los pods piden menos recursos.

Los Números Finales

Categoría Antes Después Ahorro
Cómputo producción $32.700 $10.200 69%
Development $8.200 $2.300 72%
Staging $6.100 $1.800 70%
Total $47.000 $14.300 70.2%

Ahorro mensual: $32.700. Ahorro anual: $392.400.

La implementación tardó 3 semanas. ROI: aproximadamente 6 horas.

Qué Haríamos Diferente

Empezar con el right-sizing antes que Spot. Hicimos Karpenter primero, pero si hubiéramos hecho el right-sizing de los pods antes, Karpenter habría sido aún más eficiente desde el día uno.

Usar Savings Plans para el baseline de On-Demand. El 20% de las cargas que tienen que correr en On-Demand deberían estar cubiertas por Compute Savings Plans de 1 año para un descuento adicional del 30%. Lo estamos implementando ahora.

Configurar alertas de costo antes. Construimos el dashboard de FinOps después de la optimización. Tendríamos que haberlo hecho primero para que el equipo pudiera ver el impacto en tiempo real.


¿Estás gastando demasiado en Kubernetes? Hicimos esta optimización para 8 equipos. Pedí una evaluación gratuita de infraestructura — te decimos exactamente dónde está el desperdicio.

Preguntas frecuentes

¿Spot es realmente seguro para cargas productivas?

Sí, con la configuración correcta. Las tasas de interrupción de Spot son menores al 5% cuando diversificás entre 10+ tipos de instancia y 3 AZs. La clave está en la clasificación de cargas: los servicios stateless con graceful shutdown (APIs web, workers, batch jobs) van a Spot. Los servicios stateful (brokers de Kafka, primaries de Redis, bases de datos) se quedan en On-Demand. Con terminationGracePeriodSeconds: 120 y los pod disruption budgets correctos, los usuarios nunca se enteran de las interrupciones.

¿Cómo manejás las interrupciones de Spot sin tener downtime?

Cuatro defensas: (1) diversificá los tipos de instancia para que AWS siempre tenga capacidad Spot disponible, (2) distribuí los pods en 3 AZs con topology constraints, (3) configurá terminationGracePeriodSeconds: 120 para que los pods drenen limpio en el aviso de 2 minutos, (4) usá preferredDuringSchedulingIgnoredDuringExecution para que las cargas hagan fallback a On-Demand si no hay Spot disponible, en vez de quedarse en Pending.

Cluster Autoscaler vs Karpenter — ¿cómo se decide?

Si estás en EKS y querés optimizar costos, migrá a Karpenter. Cluster Autoscaler necesita node groups predefinidos, no puede mezclar tipos de instancia de forma eficiente, y tarda 3-5 minutos en hacer scale-up. Karpenter mira los pods pendientes, encuentra la instancia más barata que los contiene, y la aprovisiona en menos de 60 segundos. La eficiencia de bin packing sola justifica 30-50% menos nodos. En GKE, quedate con su autoscaler; en AKS el provider de Karpenter todavía está madurando, así que hay que evaluar caso a caso.

¿Cuándo conviene quedarse en On-Demand en vez de Spot?

Tres categorías: (1) servicios stateful donde perder un nodo significa perder datos (brokers de Kafka, primaries de Redis, masters de Elasticsearch, etcd), (2) conexiones long-running que no pueden terminarse limpiamente (servidores WebSocket a escala, APIs de long-polling), (3) cargas con SLAs estrictos donde incluso una ventana de drain de 2 minutos es demasiado riesgosa (trading en tiempo real, procesamiento crítico de pagos). Todo lo demás puede correr en Spot sin problemas.

¿Cómo escalás dev/staging a cero sin romperle la experiencia al equipo de desarrollo?

Los triggers cron de KEDA escalan las cargas hacia arriba a las 9 AM de lunes a viernes en la zona horaria local, y hacia abajo a las 7 PM. Para staging específicamente, sumamos un trigger HTTP para que el cluster también suba on-demand cuando alguien golpea un endpoint. Los desarrolladores no lo notan — tienen la misma experiencia durante el horario laboral y la infraestructura desaparece a la noche. Agregamos una notificación en Slack para que los equipos sepan cuándo su entorno está dormido y cómo despertarlo manualmente.

¿Se puede usar este approach en GKE o AKS, o es solo para EKS?

El approach general funciona en todos lados, pero las herramientas difieren. En GKE usá Cluster Autoscaler con el profile optimize-utilization + Preemptible VMs como equivalente de Spot. En AKS usá Cluster Autoscaler con Spot node pools. KEDA scale-to-zero funciona igual en los tres. VPA también. El proyecto Karpenter-para-otras-clouds está en curso pero todavía no está production-ready (a principios de 2026).