Kubernetes simplifica la orquestación de contenedores, pero también puede convertirse en un sumidero de costos si no se gestiona con intención. Estudios de la industria estiman que entre el 30% y el 50% del gasto en cloud de organizaciones que usan Kubernetes se desperdicia en recursos sobreaprovisionados, nodos ociosos y configuraciones por defecto que nadie reviso. La buena noticia es que existen estrategias concretas y herramientas especializadas para recuperar ese gasto sin sacrificar rendimiento ni disponibilidad. Esta guía cubre desde el right-sizing básico hasta el uso de Karpenter y herramientas de visibilidad de costos.

Right-sizing: el primer paso

El right-sizing consiste en ajustar los recursos asignados a cada workload para que coincidan con lo que realmente consume. Es la optimización con mayor impacto inmediato porque no requiere cambios arquitecturales.

El problema del sobreaprovisionamiento

Cuando un equipo despliega una aplicación sin datos de consumo real, tiende a pedir mas recursos de los necesarios “por si acaso”. Un pod que pide 1 CPU y 2Gi de memoria pero usa 200m y 400Mi esta ocupando espacio que otros pods podrian usar, y el cluster necesita mas nodos para acomodar esa capacidad fantasma.

Como diagnosticar

Compara requests vs uso real con métricas de Prometheus:

# CPU: ratio de uso real vs request
sum(rate(container_cpu_usage_seconds_total{namespace="production"}[5m])) by (pod)
/
sum(kube_pod_container_resource_requests{resource="cpu",namespace="production"}) by (pod)

# Memoria: ratio de uso real vs request
sum(container_memory_working_set_bytes{namespace="production"}) by (pod)
/
sum(kube_pod_container_resource_requests{resource="memory",namespace="production"}) by (pod)

Si el ratio esta consistentemente por debajo de 0.5, los requests estan sobredimensionados.

Resource requests y limits: configuración correcta

Estrategia recomendada

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-catalogo
spec:
  template:
    spec:
      containers:
      - name: api
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi

Las reglas prácticas para definir requests y limits:

  • Requests: Basalos en el percentil 95 de consumo real observado durante al menos una semana. Esto es lo que el scheduler usa para colocar pods en nodos.
  • Limits de CPU: Algunos equipos prefieren no poner limit de CPU para evitar throttling. Si los pones, que sean 2-3x el request.
  • Limits de memoria: Siempre pon limit de memoria. Un pod sin memory limit puede causar OOMKill en otros pods del nodo.

Vertical Pod Autoscaler para recomendaciones

VPA en modo recomendación analiza el consumo histórico y sugiere valores:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-catalogo-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-catalogo
  updatePolicy:
    updateMode: "Off"  # Solo recomienda, no modifica
  resourcePolicy:
    containerPolicies:
    - containerName: api
      minAllowed:
        cpu: 50m
        memory: 64Mi
      maxAllowed:
        cpu: 2
        memory: 2Gi

Revisa las recomendaciones con kubectl describe vpa api-catalogo-vpa y ajusta manualmente.

Spot instances y nodos preemptibles

Las instancias spot (AWS), preemptible (GCP) o spot VMs (Azure) cuestan entre un 60% y un 90% menos que las instancias on-demand. Son ideales para workloads que toleran interrupciones.

Workloads candidatos para spot

  • Jobs de CI/CD y pipelines de datos.
  • Batch processing y ETL.
  • Workloads stateless con multiples replicas (el HPA compensa la perdida de un nodo).
  • Ambientes de desarrollo y staging.

Workloads que NO deben correr en spot

  • Bases de datos stateful con una sola replica.
  • Componentes críticos sin redundancia.
  • Workloads con tiempos de arranque muy largos.

Configuración en EKS con node groups mixtos

managedNodeGroups:
  - name: on-demand-base
    instanceType: m6i.xlarge
    minSize: 3
    maxSize: 3
    capacityType: ON_DEMAND
    labels:
      capacity-type: on-demand
  - name: spot-workers
    instanceTypes: [m6i.xlarge, m5.xlarge, m5a.xlarge, m6a.xlarge]
    minSize: 0
    maxSize: 20
    capacityType: SPOT
    labels:
      capacity-type: spot
    taints:
      - key: spot
        value: "true"
        effect: PreferNoSchedule

Usar multiples tipos de instancia en el grupo spot aumenta la disponibilidad al diversificar los pools de capacidad.

Cluster Autoscaler vs Karpenter

Cluster Autoscaler

Es la solución tradicional. Observa pods pendientes y escala node groups predefinidos:

  • Funciona con node groups estaticos (debes predefinir tipos de instancia).
  • Escalado mas lento (minutos).
  • Soporta multiples cloud providers.

Karpenter

Es la alternativa moderna, creada por AWS pero con soporte creciente para otros providers:

  • No requiere node groups predefinidos. Karpenter elige el tipo de instancia optimo para los pods pendientes.
  • Escalado mas rápido (segundos).
  • Consolida nodos subutilizados automáticamente.
  • Permite definir restricciones flexibles:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64"]
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: karpenter.k8s.aws/instance-category
        operator: In
        values: ["m", "c", "r"]
      - key: karpenter.k8s.aws/instance-generation
        operator: Gt
        values: ["4"]
  limits:
    cpu: 100
    memory: 200Gi
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 720h

La política consolidationPolicy: WhenUnderutilized es clave: Karpenter automáticamente reemplaza nodos subutilizados por instancias mas pequeñas, optimizando costos de forma continua.

Detección de recursos ociosos

Namespaces de desarrollo olvidados

Es comun que ambientes de desarrollo o staging queden corriendo indefinidamente. Implementa políticas de ciclo de vida:

# Encontrar namespaces sin actividad reciente
kubectl get namespaces -o json | jq -r '.items[] |
  select(.metadata.labels["environment"]=="dev") |
  .metadata.name' | while read ns; do
    LAST_POD=$(kubectl get pods -n $ns --sort-by=.metadata.creationTimestamp \
      -o jsonpath='{.items[-1].metadata.creationTimestamp}' 2>/dev/null)
    echo "Namespace: $ns - Ultimo pod: $LAST_POD"
done

PVCs sin usar

Los volumenes persistentes que quedaron huerfanos despues de borrar deployments siguen generando costos de almacenamiento:

# Listar PVCs no montados por ningun pod
kubectl get pvc --all-namespaces -o json | jq -r '
  .items[] | select(.status.phase=="Bound") |
  "\(.metadata.namespace)/\(.metadata.name)"' | while read pvc; do
    NS=$(echo $pvc | cut -d/ -f1)
    NAME=$(echo $pvc | cut -d/ -f2)
    PODS=$(kubectl get pods -n $NS -o json | jq -r \
      ".items[].spec.volumes[]?.persistentVolumeClaim.claimName" | grep -c "$NAME")
    if [ "$PODS" -eq 0 ]; then
      echo "PVC sin usar: $pvc"
    fi
done

Namespace quotas y governance

Las quotas previenen que un equipo consuma todos los recursos del cluster:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-backend-quota
  namespace: backend
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    persistentvolumeclaims: "10"
    pods: "50"

Complementa las quotas con LimitRanges para establecer defaults y maximos por pod, evitando que un solo deployment acapare la cuota completa del namespace.

Herramientas de visibilidad de costos

Kubecost

Kubecost asigna costos reales a namespaces, deployments, labels y equipos. Funcionalidades principales:

  • Dashboard con desglose de costos por namespace, label o equipo.
  • Alertas cuando el gasto supera umbrales definidos.
  • Recomendaciones de right-sizing basadas en consumo real.
  • Savings insights que identifican recursos ociosos y sobreaprovisionados.

OpenCost

Es la alternativa open source, donada a la CNCF por Kubecost. Proporciona las funcionalidades core de asignación de costos sin las features enterprise. Ideal para equipos que quieren visibilidad básica sin costo de licencia.

Métricas clave a monitorear

  • Cost per namespace/team: Permite chargeback o showback interno.
  • CPU/Memory efficiency: Ratio de uso real vs allocated. Objetivo: mantenerlo por encima de 0.6.
  • Idle cost: Costo de recursos reservados pero no utilizados.
  • Cluster efficiency: Porcentaje de la capacidad total que esta siendo usada efectivamente.

Checklist de optimización de costos

  1. Configurar resource requests y limits en todos los deployments.
  2. Desplegar VPA en modo recomendación y revisar semanalmente.
  3. Implementar HPA para workloads con tráfico variable.
  4. Usar spot instances para workloads tolerantes a interrupciones.
  5. Evaluar Karpenter para reemplazar Cluster Autoscaler.
  6. Establecer ResourceQuotas por namespace.
  7. Auditar PVCs huerfanos y namespaces inactivos mensualmente.
  8. Desplegar Kubecost u OpenCost para visibilidad continua.
  9. Configurar alertas de gasto por equipo/namespace.
  10. Revisar recomendaciones de right-sizing cada sprint.

Conclusion

La optimización de costos en Kubernetes no es un proyecto puntual sino una disciplina continua. Requiere visibilidad (saber cuanto se gasta y donde), governance (quotas, policies, ownership) y herramientas que automaticen las decisiones de escalado y right-sizing. La combinación de configuraciones correctas de recursos, uso estrategico de spot instances, autoescalado inteligente con Karpenter y herramientas de visibilidad como Kubecost puede reducir el gasto en cloud entre un 30% y un 60% sin impactar la experiencia del usuario.

Recursos adicionales