Inicio Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas

Optimización de costes en AKS: maxPods, el cuello de botella invisible

Optimización de costes en AKS: maxPods, el cuello de botella invisible

Tabla de contenidos

El problema: 3 nodos sin carga que no desescalan

Recibes un ticket de optimización de costes. El escenario es clásico:

  • Un AKS con 3 nodos en el node pool (configurado como 1-3 con autoscaler)
  • Solo 2 pods de aplicación real (app-water y app-corporate)
  • 23 pods de operaciones del propio clúster
  • CPU al 8%, memoria al 12%
  • Nadie entiende por qué hay 3 nodos activos si no hay carga

La petición es clara: "Necesitamos ajustar las máquinas a la realidad. Queremos 1 nodo. Si hay limitaciones por número de pods, revisemos qué se puede eliminar."

La DEVuición dice: "Si apenas hay carga, ¿por qué Kubernetes mantiene nodos extra?" La respuesta no está en la CPU ni en la RAM. Está en un parámetro que casi nadie revisa: maxPods.


Anatomía de un nodo AKS: los pods invisibles

Antes de diagnosticar, necesitas entender que en AKS cada nodo viene "precargado" con componentes del sistema que consumen slots de pods. Un nodo típico con Azure CNI, monitorización y políticas de seguridad tiene esta distribución:

Pods DEVocables (core de Kubernetes y Azure)

ComponenteFunciónEliminable
corednsResolución DNS DEVernaNO
kube-proxyEnrutamiento de red del clústerNO
azure-cnsAzure Container Networking (gestión de IPs)NO
azure-ip-masq-agentNAT para tráfico salienteNO
azure-npmNetwork Policies de AzureNO
retina-agentObservabilidad de redNO
cloud-node-managerSincronización nodo-AzureNO
csi-azuredisk-nodeDriver de discos AzureNO
csi-azurefile-nodeDriver de Azure FilesNO
csi-blob-nodeDriver de Blob StorageNO
metrics-serverMétricas para HPANO (necesario para autoscaling)

Eso ya son 11 pods que no puedes tocar. Y cada uno consume un slot del límite de maxPods.

Pods de gobernanza y seguridad (revisables)

ComponenteFunciónEliminable en DEV/DES
external-secretsSincroniza secretos desde Key VaultDepende de la app
azure-wi-webhookWorkload Identity (auth sin passwords)NO si las apps lo usan
kyvernoMotor de políticas de seguridadNegociable en entorno previo
dataprotection-*Backup nativo de Azure para AKSNO (protege ante errores)

Pods de observabilidad (los sospechosos)

ComponenteFunciónEliminable en DEV/DES
ama-logsAzure Monitor - logsSÍ (pierdes logs en Log Analytics)
ama-metrics-*Azure Monitor - métricas PrometheusSÍ (pierdes gráficas en portal)
prometheus-node-exporterExportador de métricas del nodo
clippy (ingress)Health check / monitoring del ingressReducible a 1 réplica
ingress-nginx-controllerBalanceador de tráficoReducible a 1 réplica

Diagnóstico paso a paso

Paso 1: Contar pods por nodo

BASH
kubectl get pods --all-namespaces -o wide | awk '{prDEV $8}' | sort | uniq -c

Resultado:

CODE
     26 aks-nodepool-xxxxx-vmss000004
     28 aks-nodepool-xxxxx-vmss00001c

Entre los dos nodos suman 54 pods. Si el límite por nodo es 30, matemáticamente es imposible consolidar en uno solo.

Paso 2: Confirmar la configuración del node pool

BASH
az aks nodepool show \
  --resource-group DEV-RG-AKS-01 \
  --cluster-name DEV-AKS-01 \
  --name defaultv2 \
  --query "{minCount: minCount, maxCount: maxCount, count: count, enableAutoScaling: enableAutoScaling, maxPods: maxPods}"

Resultado:

JSON
{
  "count": 2,
  "enableAutoScaling": true,
  "maxCount": 2,
  "maxPods": 30,
  "minCount": 1
}

Ahí está el problema: maxPods: 30. El Cluster Autoscaler calcula: "Si elimino el nodo 2, necesito reubicar 28 pods en el nodo 1. Pero el nodo 1 solo tiene 4 slots libres (30 - 26). Imposible. Me quedo con 2 nodos."

Paso 3: Verificar recursos (CPU/Memoria)

BASH
kubectl top nodes
CODE
NAME                                CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
aks-nodepool-xxxxx-vmss000004       346m         8%     4053Mi          12%
aks-nodepool-xxxxx-vmss00001c       257m         6%     3701Mi          11%

Combinados: 14% CPU, 23% Memoria. Un solo nodo tiene capacidad de sobra para albergar toda la carga. El cuello de botella no es hardware, es el límite de IPs/pods.

Paso 4: Verificar IPs disponibles en la subnet

En Azure CNI, cada pod reserva una IP privada de la subnet. Si subes maxPods a 60, el nodo necesitará 60 IPs.

Verifica el espacio disponible en el portal de Azure: Virtual Network → Subnet → Available IPs.

En nuestro caso, la subnet /23 tiene 512 IPs totales (~400 disponibles). Espacio más que suficiente.


¿Por qué maxPods es 30 por defecto?

Azure establece 30 como valor por defecto en Azure CNI por conservadurismo:

  • Cada pod consume una IP real de la subnet
  • En subnets pequeñas (/26, /27), un maxPods alto agotaría las IPs
  • Microsoft prefiere que el clúster "funcione" con la config más restrictiva

El problema es que este default se arrastra desde la creación del clúster y nadie lo revisa. En una subnet /23 con 512 IPs, mantener maxPods en 30 es como comprar un parking de 500 plazas y poner un cartel de "máximo 30 coches".


La solución: limpieza + recreación del node pool

¿Se puede solo limpiar pods para bajar a 30?

Hagamos las cuentas aplicando la limpieza máxima:

AcciónPods eliminadosTotal restante
Estado inicial54
Eliminar Prometheus + Azure Monitor (ama-*)-549
Reducir clippy a 1 réplica-247
Reducir ingress-nginx a 1 réplica-146

Resultado: 46 pods mínimos. Siguen sin caber en un nodo con maxPods=30.

Los componentes base de Azure CNI, drivers CSI y core de Kubernetes son inmutables. No puedes bajar de ~42 pods en un nodo AKS con la stack corporativa estándar.

La solución real: recrear el node pool con maxPods=60

El parámetro maxPods está blindado por Azure — no se puede modificar en caliente en un node pool existente. La única opción es una migración Blue/Green:

Fase 1: Limpieza previa

BASH
# Reducir réplicas de ingress a 1 (INT/DES no requiere HA)
kubectl scale deployment clippy --replicas=1 -n ingress-basic
kubectl scale deployment ingress-nginx-controller --replicas=1 -n ingress-basic

# Desactivar Azure Monitor (elimina pods ama-*)
az aks disable-addons --addons monitoring \
  --name INT-AKS-01 \
  --resource-group INT-RG-AKS-01

# Eliminar Prometheus (si se desplegó via Helm)
helm uninstall prometheus-stack -n prometheus

Fase 2: Crear el nuevo node pool

BASH
# Crear pool nuevo con maxPods=60 y 1 nodo fijo
az aks nodepool add \
  --resource-group DEV-RG-AKS-01 \
  --cluster-name DEV-AKS-01 \
  --name newpool \
  --node-count 1 \
  --max-pods 60 \
  --node-vm-size Standard_D4s_v5 \
  --mode System \
  --no-wait

Nota: Usa --mode System para que pueda alojar pods críticos del sistema. Verifica el tamaño de VM con az aks nodepool show sobre el pool actual.

Fase 3: Migrar pods al nuevo nodo

BASH
# Bloquear scheduling en el pool viejo
kubectl cordon -l agentpool=defaultv2

# Desalojar pods (se reubicarán en el nuevo nodo)
kubectl drain -l agentpool=defaultv2 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --grace-period=60

Fase 4: Verificar y eliminar el pool viejo

BASH
# Verificar que todos los pods están en el nuevo nodo
kubectl get pods --all-namespaces -o wide | awk '{prDEV $8}' | sort | uniq -c

# Si todo OK, eliminar el pool viejo
az aks nodepool delete \
  --resource-group DEV-RG-AKS-01 \
  --cluster-name DEV-AKS-01 \
  --name defaultv2 \
  --no-wait

Fase 5: Renombrar (opcional)

Si necesitas mantener la nomenclatura corporativa:

BASH
# Crear pool definitivo con nombre correcto
az aks nodepool add \
  --resource-group DEV-RG-AKS-01 \
  --cluster-name DEV-AKS-01 \
  --name defaultv2 \
  --node-count 1 \
  --max-pods 60 \
  --node-vm-size Standard_D4s_v5 \
  --mode System

# Migrar desde newpool
kubectl cordon -l agentpool=newpool
kubectl drain -l agentpool=newpool --ignore-daemonsets --delete-emptydir-data

# Eliminar pool temporal
az aks nodepool delete \
  --resource-group DEV-RG-AKS-01 \
  --cluster-name DEV-AKS-01 \
  --name newpool

Resultado final

Después de la migración:

BASH
$ kubectl get nodes
NAME                              STATUS   ROLES    AGE   VERSION
aks-defaultv2-xxxxx-vmss000000    Ready    <none>   2h    v1.35.4

$ kubectl get pods --all-namespaces | wc -l
47

$ kubectl top nodes
NAME                              CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
aks-defaultv2-xxxxx-vmss000000    603m         15%    7754Mi          24%

Un solo nodo con 46 pods de los 60 permitidos, CPU al 15% y memoria al 24%. El segundo y tercer nodo ya no existen y dejan de facturar.


Matriz de decisión: qué eliminar en DEV

Para entornos no productivos, esta es la guía de lo que se puede recortar:

ComponenteImpacto si se eliminaRecomendación DEV/DES
Prometheus / node-exporterSin métricas custom en GrafanaEliminar
Azure Monitor (ama-*)Sin logs ni métricas en portal AzureEliminar o mantener ama-logs solo
clippy (réplicas extra)Sin HA en health checksReducir a 1
ingress-nginx (réplicas extra)Sin HA en balanceoReducir a 1
dataprotection-*Sin backups del clústerMantener (protección ante errores)
kyvernoSin enforcement de políticasMantener (previene drift)
external-secretsSin sync de secretosMantener (necesario para apps)

Lecciones aprendidas

  1. maxPods es el verdadero limitante, no la CPU ni la RAM. Revísalo siempre que crees un clúster nuevo.
  1. El default de 30 es insuficiente para cualquier entorno con monitorización corporativa. Recomendación: configurar siempre 60-110 desde el día 0.
  1. Azure CNI consume IPs por adelantado — verifica el espacio de tu subnet antes de subir maxPods. Fórmula: nodos × maxPods ≤ IPs disponibles en subnet.
  1. El Cluster Autoscaler no puede violar maxPods — aunque tengas minCount=1, si los pods no caben en un solo nodo, el autoscaler mantendrá nodos extra.
  1. En DEV/DES no necesitas HA — 3 réplicas de ingress, 2 de nginx controller y backup corporativo en un entorno de pruebas es dinero tirado.
  1. Recrear el node pool es la única opción para cambiar maxPods. Planifica una ventana de mantenimiento y usa la estrategia Blue/Green.

Checklist de viabilidad antes de ejecutar

Antes de proponer esta solución a tu equipo, verifica estos 3 puntos:

  • [ ] IPs disponibles: subnet_size - 5 (reservadas Azure) - (nodos_actuales × maxPods_actual) > nuevo_maxPods
  • [ ] CPU/Memoria: Suma de kubectl top nodes < 70% en un solo nodo del tamaño elegido
  • [ ] Aplicaciones stateful: Si hay PersistentVolumes zonales, el nuevo nodo debe estar en la misma zona de disponibilidad

Si los tres son afirmativos, la migración es segura y sin pérdida de servicio.


Referencias

Comentarios