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: 30shace 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.