# Karpenter vs Cluster Autoscaler en EKS: depende de dónde partes > Dos modelos de provisioning, no dos velocidades. Qué cambia de verdad entre Karpenter y Cluster Autoscaler en EKS con pools spot, qué revisar antes de elegir y por qué acabamos eligiendo Karpenter. - Autor: José Manuel García Locubiche - Publicado: 2026-09-29 - Actualizado: 2026-09-30 - Original: https://jose-m.net/2026/blog/tech/karpenter-vs-cluster-autoscaler-en-eks/ ---
## Una pregunta con respuesta de moda "¿Karpenter o Cluster Autoscaler?" es de esas preguntas que parecen resueltas antes de hacerlas. Karpenter tiene fama de ser más moderno, más rápido y más barato en spot, así que la tentación es instalarlo y pasar a otra cosa. Cuando me tocó evaluarlo en clústeres EKS reales, con años de historia y varios pools autogestionados, acabamos eligiendo Karpenter. Pero no por esas razones, y no antes de descartarlo una primera vez. El escenario era bastante típico: - Varios clústeres EKS 1.35, de QA y de producción. - Nodos en **self-managed node groups**: un Auto Scaling group (ASG) por pool, definidos en Terraform con launch templates y user data propio (`nodeadm`, módulos IPVS y ajustes de sistema). - Varios pools **100 % spot**, con mixed instances policy y estrategia `lowest-price`. - **Ningún autoscaler**: los ASG escalaban por sus propias políticas, no en función de los pods pendientes. - IRSA ya configurado. ## El problema de partida En realidad había dos problemas. El primero era de capacidad: sin autoscaler, un pod en `Pending` no hacía crecer nada. Los ASG subían y bajaban por sus políticas, que no saben qué pods esperan sitio. El segundo era el síntoma que lo había puesto todo sobre la mesa: inestabilidad en los nodos spot. La pregunta era qué autoscaler instalar. Lo que sigue son las diferencias que de verdad importan entre los dos, lo que apareció al tirar del hilo y lo que decidimos. Con un aviso por delante: lo más grave que encontramos no tenía nada que ver con el autoscaler. ## Dos modelos, no dos velocidades La diferencia de fondo entre los dos no es de rendimiento, es de modelo. **[Cluster Autoscaler](https://docs.aws.amazon.com/eks/latest/best-practices/cas.html)** no crea instancias. Observa los pods en `Pending`, simula si cabrían en un nodo nuevo de alguno de los grupos y, si es así, sube el `desired` del ASG correspondiente. Para bajar, busca nodos infrautilizados cuyos pods quepan en otro sitio, los drena y reduce el ASG. Quien lanza las instancias sigue siendo el ASG, o el managed node group. Cada grupo es, para Cluster Autoscaler, una plantilla de nodo: sabe qué obtendrá si añade uno. **[Karpenter](https://karpenter.sh/docs/)** no necesita ASG para los nodos que lanza. Lee los pods pendientes, calcula qué instancias les encajan y se las pide directamente a EC2 Fleet (`CreateFleet`), que elige entre las candidatas. Lo que antes era un ASG con su launch template pasa a ser un [`NodePool`](https://karpenter.sh/docs/concepts/nodepools/) (qué puede lanzar: tipos, spot u on-demand, zonas, labels, taints, límites y cuándo puede retirar nodos) y un [`EC2NodeClass`](https://karpenter.sh/docs/concepts/nodeclasses/) (cómo lanzarlo: AMI, subnets, security groups, rol, user data, configuración del kubelet). Cada nodo lanzado queda representado por un `NodeClaim`. De ahí sale una consecuencia que conviene tener clara desde el principio: **Karpenter no escala tus ASG existentes**. Adoptarlo significa sustituir el modelo de pools para la capacidad que le entregues, no añadir una pieza encima. Eso no quiere decir que ASG y Karpenter sean excluyentes. Lo normal es que convivan: un node group pequeño, fijo, donde corre el propio controlador de Karpenter (que [no debe correr en nodos que él gestiona](https://docs.aws.amazon.com/eks/latest/best-practices/karpenter.html)), y NodePools para el resto. Lo que no conviene es que dos autoscalers compitan por los mismos pods pendientes: la [guía oficial de migración](https://karpenter.sh/docs/getting-started/migrating-from-cas/) escala Cluster Autoscaler a cero antes de ir vaciando los node groups. ## Las diferencias que importan | | Cluster Autoscaler | Karpenter | |---|---|---| | Unidad de provisioning | El node group: sube o baja el `desired` de su ASG | La instancia: un `NodeClaim` por nodo | | Relación con ASG | Los necesita, self-managed o managed node groups | No los usa para sus nodos; convive con ASG y managed node groups en el mismo clúster | | Selección de instancias | La hace el ASG, dentro de la lista del grupo y con su allocation strategy | Karpenter filtra por los requisitos del pod y del NodePool; EC2 Fleet elige entre las candidatas | | Mixed instances | Simula con el primer tipo de la lista: el grupo debería tener una sola forma de CPU, memoria y GPU | Evalúa cada tipo con sus propios recursos; un NodePool puede mezclar formas | | Zona de disponibilidad | En un ASG multi-AZ no elige la zona; para cargas atadas a una zona, un grupo por AZ | Elige la zona al lanzar, según los requisitos del pod | | Estrategia spot | La del ASG (en managed node groups, `price-capacity-optimized` y no se cambia) | `price-capacity-optimized` | | Interrupciones spot | No las gestiona: hace falta NTH o managed node groups | Integradas, si configuras la cola SQS y las reglas de EventBridge; no actúa sobre rebalance recommendations | | Scale-down | Retira nodos por debajo del 50 % de requests (por defecto) si sus pods caben en otro sitio | Consolidación: borra nodos o los sustituye por otros más baratos, según la política del NodePool | | Predictibilidad | Alta: cada grupo tiene una lista corta de tipos | La que le des al NodePool: cuanto más lo acotas, más predecible y menos margen de ahorro | | Piezas que operar | Un Deployment y su IAM; con spot autogestionado, además NTH (SQS, EventBridge, lifecycle hooks) | Controlador fuera de sus propios nodos, CRDs, IAM de controlador y de nodo, SQS y EventBridge | | Versiones | Misma versión menor que el clúster | [Matriz de compatibilidad](https://karpenter.sh/docs/upgrading/compatibility/): Kubernetes 1.35 pide Karpenter 1.9 o posterior | | Dónde viven los pools | ASG y launch templates, o managed node groups: recursos de AWS en Terraform | NodePool y EC2NodeClass: objetos de Kubernetes | | Con managed node groups | Su complemento natural: EKS ya etiqueta los ASG para el autodiscovery | No los escala; conviven como capacidad fija | Casi todas las filas de Karpenter dependen de configuración. Esa es, precisamente, la diferencia. ### Los tipos de instancia de un grupo deberían tener la misma forma Es la limitación de Cluster Autoscaler que más sorprende. Para decidir si un scale-up sirve de algo, construye un nodo modelo con el [**primer tipo de instancia**](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md) de la mixed instances policy. Nadie le impide mezclar tipos con distinta memoria, pero la documentación es clara: deberían tener la misma CPU y la misma memoria, porque todos sus cálculos parten de ahí. Si los mezclas, las decisiones dejan de ser fiables. Un caso real: un pool spot con más de una docena de tipos `xlarge`, todos con 4 vCPU, pero mezclando `m*.xlarge` (16 GiB), `r*.xlarge` (32 GiB) e `i3.xlarge` (30,5 GiB). Si el primero de la lista es un `r4.xlarge`, Cluster Autoscaler escalará para un pod que pide 20 GiB, llegará un `m5.xlarge` de 16 GiB y el pod seguirá en `Pending`. Poner primero el tipo más pequeño evita ese caso, pero a cambio el grupo nunca escalará para un pod que solo cabe en los grandes. La solución limpia es partir el pool por forma: un grupo `m` y otro `r`. Y partirlo sin perder diversidad, porque en spot cada tipo de instancia en cada zona es un pool de capacidad distinto: `m5`, `m5a`, `m6i`, `m6a`, `m7i`... todos `xlarge` con 4 vCPU y 16 GiB, en el mismo grupo. La propia [guía de managed node groups](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) lo recomienda así cuando se usa Cluster Autoscaler. Karpenter no tiene este problema: evalúa cada tipo con su CPU y su memoria. ### Velocidad y coste: qué es arquitectura y qué depende de ti "Karpenter bien configurado suele ser más barato y más rápido" es una frase que se repite mucho. Tiene una parte de arquitectura y otra que depende del clúster. Lo que es arquitectura: - **Menos intermediarios al escalar.** Cluster Autoscaler cambia el `desired` y espera a que el ASG lance. Karpenter pide la instancia directamente. El arranque del nodo (AMI, user data, unirse al clúster, bajar imágenes) es el mismo en los dos, y suele ser lo que más tarda. - **Elige la instancia para los pods que esperan**, no para una plantilla fija. Con Cluster Autoscaler, el tamaño del nodo lo decidiste al diseñar el grupo. - **Puede sustituir nodos**, no solo retirarlos. Cluster Autoscaler, al bajar, solo quita nodos del tipo que ya hay; no cambia tres nodos medio vacíos por uno más pequeño. Lo que depende de ti: - **La amplitud del NodePool.** Si lo acotas a los mismos cuatro tipos que tenías en el ASG, la ventaja de coste se reduce mucho. - **La política de consolidación.** En la versión 1.14 hay tres: `WhenEmpty`, `Balanced` y `WhenEmptyOrUnderutilized`. Cuanto más agresiva, más ahorro y más pods reiniciados. - **La consolidación spot a spot** (sustituir un nodo spot por otro spot más barato) sigue detrás del feature gate `SpotToSpotConsolidation`, [desactivado por defecto](https://karpenter.sh/docs/reference/settings/). - **Las requests.** Karpenter y el scheduler solo ven requests. Si están mal, el nodo que elige también. AWS recomienda además [requests iguales a limits](https://docs.aws.amazon.com/eks/latest/best-practices/karpenter.html) para todo lo que no sea CPU cuando hay consolidación. - **Lo que bloquea la disrupción.** PDBs demasiado estrictos o la anotación `karpenter.sh/do-not-disrupt` impiden consolidar ese nodo. En resumen: Karpenter tiene más margen para ser más barato y más rápido. Que lo sea en tu clúster depende de cuánto de ese margen le dejes usar, y de cuánta rotación de pods estés dispuesto a aceptar a cambio. ### El coste de operarlo La comparación de piezas suele hacerse mal. "Cluster Autoscaler es un Deployment" solo es cierto si los nodos spot los drena otro. Con self-managed node groups, ese otro es NTH en modo cola, y NTH también necesita SQS, reglas de EventBridge y lifecycle hooks en los ASG. Karpenter añade encima: - Un controlador que tiene que correr en capacidad que él no gestiona: un node group o Fargate. - Sus CRDs, que se actualizan con el controlador. - Dos identidades IAM: la del controlador, con una política extensa, y la de los nodos. - La cola de interrupciones y sus reglas de EventBridge. Son opcionales, pero [sin ellas no hay gestión de interrupciones spot](https://karpenter.sh/docs/reference/settings/). - Su propio ciclo de versiones, que hay que alinear con el de EKS. Cluster Autoscaler tampoco es gratis en esto: se [despliega con la misma versión menor que el clúster](https://docs.aws.amazon.com/eks/latest/best-practices/cas.html), así que cada upgrade de EKS lo arrastra. La diferencia real no está en el número de piezas. Está en dónde vive la definición de la capacidad: en recursos de AWS gestionados con Terraform, o en objetos de Kubernetes que interpreta un controlador. Si el equipo ya gestiona todo en Terraform y valora que cada pool sea predecible, ese cambio pesa. ## Lo que encontramos al tirar del hilo Evaluando todo esto aparecieron problemas que no dependían de qué autoscaler eligiéramos. Y alguno era bastante más serio que la elección en sí. ### Las interrupciones spot no las gestionaba nadie Ni Cluster Autoscaler ni un ASG autogestionado drenan un nodo cuando AWS avisa de que va a reclamar la instancia spot. Sin nada más, no hay drenaje: dos minutos después del aviso la instancia desaparece con sus pods dentro. Hay tres formas de cubrirlo: - **[AWS Node Termination Handler (NTH)](https://github.com/aws/aws-node-termination-handler)** en modo cola: SQS, EventBridge y lifecycle hooks en los ASG. Es la opción para self-managed node groups, y la única de sus dos modos que atiende los lifecycle hooks del ASG. - **[Managed node groups](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html)**: EKS activa Capacity Rebalancing en los grupos spot. Cuando llega una rebalance recommendation, lanza un nodo de reemplazo y, cuando está `Ready`, acordona y drena el viejo. Si el aviso de dos minutos llega antes, empieza a drenar igualmente, *best effort*. NTH [no hace falta](https://github.com/aws/aws-node-termination-handler). - **[Karpenter](https://karpenter.sh/docs/concepts/disruption/#interruption)**, con su cola de interrupciones configurada: al recibir el aviso spot, marca con taint y drena el nodo mientras lanza el reemplazo en paralelo. También atiende eventos de mantenimiento programado y cambios de estado de la instancia. Las rebalance recommendations, no: para eso la documentación propone NTH al lado, aunque la guía de buenas prácticas de AWS desaconseja mezclar los dos. Ninguna de las tres garantiza los dos minutos completos. La propia documentación de managed node groups avisa de que, con muchas reclamaciones simultáneas o nodos muy cargados, algunos pods pueden terminar sin recibir la señal. Recomienda grace periods de 30 segundos o menos en cargas spot. Esto era, probablemente, la mayor parte de la "inestabilidad spot" del punto de partida. Y se arregla sin cambiar de autoscaler. ### La estrategia de asignación spot `lowest-price` no elige un único pool. Reparte entre los N pools más baratos de cada zona (el parámetro `SpotInstancePools`), y si uno se queda sin capacidad pasa al siguiente más barato. Pero solo mira el precio, no la capacidad disponible, y AWS [ya no la recomienda](https://docs.aws.amazon.com/autoscaling/ec2/userguide/allocation-strategies.html) precisamente por eso: es la estrategia con más riesgo de interrupción. `price-capacity-optimized` elige primero los pools con menos probabilidad de interrupción y, entre ellos, los más baratos. Es la que usa Karpenter para spot y la que usan los managed node groups creados en clústeres 1.28 o posteriores (en estos no se puede cambiar; los grupos más antiguos siguen con `capacity-optimized`). Si hoy usas `lowest-price`, este cambio por sí solo ya debería reducir interrupciones. Lo mismo vale para `spot_max_price`: AWS [desaconseja fijar un precio máximo](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotMarketOptions.html) porque las instancias se interrumpen más a menudo. Sin él, el tope es el precio on-demand. ### PodDisruptionBudgets Cualquier autoscaler drena nodos al bajar. Sin PDBs, un scale-down puede dejar un servicio sin réplicas. Conviene revisar que existan y que no bloqueen: ```bash # ALLOWED DISRUPTIONS = 0 blocks drains (and the autoscaler scale-down) kubectl get pdb -A # Single-replica workloads: a PDB cannot protect them kubectl get deploy,sts -A -o json \ | jq -r '.items[] | select(.spec.replicas == 1) | "\(.metadata.namespace)/\(.metadata.name)"' ``` Tres detalles que cambian cómo se leen: - **Un PDB solo protege de disrupciones voluntarias.** Kubernetes cuenta la desaparición de una VM como [disrupción involuntaria](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/). Si hay drenaje previo (NTH, managed node groups, Karpenter), el drenaje respeta el PDB, pero la instancia desaparece a los dos minutos igual. Contra una interrupción spot, lo que protege es tener réplicas repartidas en nodos y zonas distintas. - **No todos los caminos respetan el PDB.** En managed node groups, si un nodo sale por `AZRebalance` o porque alguien baja el `desired`, [EKS intenta desalojar durante 15 minutos](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) y después termina el nodo igualmente. - **Cluster Autoscaler es conservador con `kube-system`.** Con la configuración por defecto, [no retira un nodo que tenga pods de `kube-system` sin PDB](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md). Un CoreDNS sin PDB no solo es frágil: también fija los nodos donde corre. ### Todo spot, incluido lo que debería gestionar el spot Si todos tus nodos son spot, el autoscaler, NTH y CoreDNS también lo son. Una ola de interrupciones se lleva justo lo que debería gestionarla. Un ASG pequeño on-demand, con dos o tres instancias en zonas distintas, es un coste fijo pequeño que evita ese escenario. La guía de managed node groups recomienda lo mismo para las herramientas de gestión del clúster, y con Karpenter es casi obligatorio: su controlador no puede depender de los nodos que lanza. ### CoreDNS: el hallazgo que no buscábamos Revisando PDBs, vimos que CoreDNS no tenía ninguno. Y al mirar dónde corría: ```bash kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide # Managed node groups set eks.amazonaws.com/capacityType; self-managed ones need a label of their own kubectl get nodes -L topology.kubernetes.io/zone,eks.amazonaws.com/capacityType ``` Las dos réplicas estaban en **la misma zona y en nodos spot**. Bastaba con que AWS reclamara esos dos nodos, o con un problema en esa AZ, para dejar el clúster entero sin DNS. Y las dos reclamaciones no son sucesos independientes: en spot, cada tipo de instancia en cada zona es un pool de capacidad, así que dos nodos del mismo pool pueden caer a la vez. El Deployment era el que EKS instaló como componente autogestionado al crear el clúster, hace años: [dos réplicas](https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html), anti-affinity solo por nodo y ningún reparto por zona. Ese Deployment no se actualiza solo. El addon gestionado de EKS, en cambio, trae un PDB con `maxUnavailable: 1` desde `v1.9.3-eksbuild.6` y un `topologySpreadConstraints` por zona desde `v1.9.3-eksbuild.11` y `v1.10.1-eksbuild.7`. Un clúster antiguo puede no tener nada de eso sin que nadie lo haya tocado. La solución fue migrarlo a **addon gestionado de EKS** ([la guía oficial de instalación](https://docs.aws.amazon.com/eks/latest/userguide/coredns-add-on-create.html)), con tres réplicas, PDB y reparto por zona. Los dos últimos coinciden con los valores por defecto del addon actual, pero declararlos deja la intención escrita y a salvo de un cambio de defaults: ```bash aws eks create-addon --cluster-name --addon-name coredns \ --addon-version --resolve-conflicts OVERWRITE \ --configuration-values '{ "replicaCount": 3, "podDisruptionBudget": {"enabled": true, "maxUnavailable": 1}, "topologySpreadConstraints": [{ "maxSkew": 1, "topologyKey": "topology.kubernetes.io/zone", "whenUnsatisfiable": "ScheduleAnyway", "labelSelector": {"matchLabels": {"k8s-app": "kube-dns"}} }] }' ``` `ScheduleAnyway` es una preferencia, no una garantía: si una zona no tiene nodos con hueco, el scheduler pondrá dos réplicas juntas antes que dejar una en `Pending`. `DoNotSchedule` garantiza el reparto a cambio de exactamente eso, réplicas en `Pending` si falta capacidad en una zona. Con pools spot, `ScheduleAnyway` cambia réplicas en `Pending` por un reparto imperfecto. Si lo eliges, vigila el reparto. Antes de usar `OVERWRITE`, haz backup y comprueba que el Corefile y el Deployment no tengan cambios propios (stub domains, `rewrite`, tolerations…), porque se sobrescriben. Si los hay, pásalos en `configuration-values`: el addon [gestiona el ConfigMap entero](https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html) y el Corefile se conserva entre actualizaciones solo si va en la clave `corefile`. Verifica también que la ClusterIP del Service `kube-dns` no cambia: los nodos la tienen fijada como `clusterDNS` en su configuración del kubelet. Después, impórtalo en Terraform con `aws_eks_addon` y un bloque `import`. El mismo razonamiento vale para `kube-proxy` y `vpc-cni`, con un matiz importante: si usas IPVS, pásalo en `configuration-values` al adoptar kube-proxy, o volverá a modo iptables. Y un aviso aparte: el modo IPVS de kube-proxy está [deprecado desde Kubernetes 1.35](https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/5495-deprecate-ipvs-mode-in-kube-proxy), con `nftables` (GA desde 1.33) como sucesor. No hay prisa, porque el calendario de retirada es largo, pero conviene contarlo antes de reescribir el user data de los nodos para cualquier modelo nuevo. ## Qué era del autoscaler y qué no Con todo encima de la mesa, el reparto quedaba así: - **Del autoscaler:** que la capacidad no siguiera a los pods pendientes, y que los pools mezclaran formas de instancia (esto último solo importa con Cluster Autoscaler). - **De spot, con cualquier autoscaler:** que nadie drenara los nodos al recibir el aviso, y la estrategia `lowest-price`. - **De resiliencia, sin relación con el autoscaler:** workloads sin PDB, todo el control del clúster sobre spot y CoreDNS con sus dos réplicas en una sola zona. La inestabilidad spot que había abierto la conversación caía casi entera en el segundo grupo. El riesgo más grave, en el tercero. ## Las opciones que había sobre la mesa **Cluster Autoscaler sobre los self-managed node groups.** Reordenar los ASG por forma, añadir NTH y dejar que Cluster Autoscaler gobierne el `desired`. Es la que menos cambia hoy. **Managed node groups con Cluster Autoscaler.** Era el destino que teníamos planificado a medio plazo. Resuelve el drenaje spot de forma nativa y mantiene el modelo de ASG. Pero sigue exigiendo grupos de una sola forma, y los arreglos urgentes (interrupciones, PDBs, CoreDNS) no dependían de él. **Karpenter, con un node group fijo para lo crítico.** Sustituir los ASG spot por NodePools y dejar un managed node group pequeño y on-demand para el controlador y los componentes del clúster. Es la opción que más flexibilidad da y la que más cambia: pools, bootstrap y la forma de gestionar la capacidad. **EKS Auto Mode.** Conviene tenerlo en el mapa aunque aquí no encajaba. No es "Karpenter gestionado por AWS" sin más: es un modo de clúster en el que AWS opera [el autoscaling, basado en Karpenter y fuera del clúster](https://docs.aws.amazon.com/eks/latest/best-practices/automode.html), y además la red de pods, kube-proxy, el DNS del clúster, el driver EBS y la integración con balanceadores. Los nodos son instancias gestionadas por AWS con AMIs derivadas de Bottlerocket, [sin SSH ni SSM](https://docs.aws.amazon.com/eks/latest/userguide/automode.html), sin AMIs propias y con una vida máxima de 21 días. Se paga un [cargo por instancia](https://aws.amazon.com/eks/pricing/) sobre el precio de EC2, también en spot. Con un bootstrap propio basado en `nodeadm` y módulos del kernel, no había nada que llevar allí sin rehacerlo. ## Lo que decidimos La primera conclusión fue la contraria: Cluster Autoscaler con NTH sobre los ASG actuales y, más adelante, managed node groups. Pesaban las piezas nuevas que trae Karpenter, reescribir cada pool como `NodePool` + `EC2NodeClass` y perder los tipos de instancia acotados por pool en Terraform. Al bajar ese plan a tareas concretas, el balance cambió. Elegimos Karpenter por cuatro motivos: 1. **Los pools había que rehacerlos de todas formas.** El plan con Cluster Autoscaler pasaba por partir cada ASG por forma, cambiar la allocation strategy, montar NTH y, después, migrarlo todo otra vez a managed node groups. Eran dos migraciones de la capacidad spot. Con Karpenter es una. 2. **La limitación de forma desaparece.** El pool que mezclaba `m`, `r` e `i3` no hay que partirlo: puede ser un único NodePool, y Karpenter evalúa cada tipo con sus recursos reales. La diversificación spot que perdíamos al partir por forma, la conservamos. 3. **La pieza de interrupciones deja de ser transitoria.** Con Cluster Autoscaler, NTH era un puente hasta llegar a managed node groups. Con Karpenter, la cola SQS y las reglas de EventBridge son las mismas que habría necesitado NTH, pero forman parte del diseño final. Con un límite claro: Karpenter no actúa sobre las rebalance recommendations. 4. **El bootstrap se puede llevar, y había que revisarlo igualmente.** El user data de `nodeadm` pasa al `EC2NodeClass`, porque Karpenter [mezcla tu `NodeConfig` con el suyo](https://karpenter.sh/docs/concepts/nodeclasses/). Sus valores tienen prioridad y no hay launch templates propios, así que hay que validarlo entero. Pero con IPVS deprecado en 1.35, ese bootstrap iba a pasar por revisión con cualquier modelo. Lo que aceptamos a cambio: - **Un controlador más,** con sus CRDs, su IAM y su matriz de compatibilidad, que hay que actualizar al ritmo de EKS. - **Los pools dejan de ser recursos de AWS en Terraform** y pasan a ser objetos de Kubernetes versionados en Git. - **Menos predictibilidad por defecto.** La recuperamos acotando cada NodePool a familias y generaciones conocidas, en vez de dejarle todo el catálogo de EC2. La intención de ir a managed node groups no desaparece; cambia de papel. Pasa a ser la capacidad fija y on-demand del clúster, no el modelo de escalado. El plan quedó así: 1. PodDisruptionBudgets en los workloads críticos. 2. Un managed node group de sistema, on-demand y repartido en tres zonas, para el controlador de Karpenter y los componentes del clúster. 3. Mientras dura la migración, `price-capacity-optimized` en los ASG spot actuales: es un cambio pequeño que reduce interrupciones desde el primer día. 4. Karpenter en una versión compatible con 1.35, con la cola de interrupciones configurada desde el principio y un `EC2NodeClass` con el `NodeConfig` de `nodeadm`. Es el momento de decidir si el bootstrap sigue con IPVS o pasa a `nftables`. 5. Un NodePool por cada pool actual, acotado a familias conocidas, con límites y una política de consolidación conservadora al principio, que se abre cuando haya datos de rotación. 6. Migrar pool a pool: el NodePool empieza a recibir pods y el ASG equivalente baja hasta cero, como describe la [guía oficial de migración](https://karpenter.sh/docs/getting-started/migrating-from-cas/). ### Lo que queda: el reparto por zonas Ningún autoscaler corrige el reparto de pods que ya están corriendo. Cuando mueven pods, es para vaciar un nodo, no para equilibrar zonas. Y el scheduler solo tiene en cuenta el reparto al crear un pod: la documentación de Kubernetes lo dice sin rodeos, [no hay garantía de que las restricciones se sigan cumpliendo](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) cuando se eliminan pods. Tras una disrupción, los pods se concentran en las zonas que quedaron en pie y ahí se quedan, aunque la capacidad vuelva. El equipo de contenedores de AWS lo cuenta muy bien en [Fix pod distribution drift in Amazon EKS with the Kubernetes descheduler](https://aws.amazon.com/blogs/containers/fix-pod-distribution-drift-in-amazon-eks-with-the-kubernetes-descheduler/): en su prueba, una flota equilibrada de mil pods pasó más de 16 horas concentrada en dos zonas después de recuperar toda la capacidad. El **[descheduler](https://github.com/kubernetes-sigs/descheduler)** de Kubernetes, con `RemovePodsViolatingTopologySpreadConstraint`, corrige esa deriva. Con una trampa: por defecto solo actúa sobre restricciones `DoNotSchedule`. El reparto de CoreDNS, y el de la mayoría de workloads, es `ScheduleAnyway`, así que hay que incluirlo explícitamente con `constraints: [DoNotSchedule, ScheduleAnyway]`, como hace el artículo de AWS. Conviene añadirlo después, con el autoscalado ya estable, los PDBs en su sitio (el descheduler los respeta) y una medición previa del desequilibrio. Cada desalojo es un reinicio, así que el propio artículo recomienda empezar por un solo namespace y mantener `topologyBalanceNodeFit` activado, que es su valor por defecto, para no desalojar pods que no tienen un sitio mejor al que ir. ## Cómo decidir según tu punto de partida **Tienes ASG autogestionados y no quieres rediseñar los pools.** Cluster Autoscaler + NTH. Separa los grupos por forma de instancia sin perder diversidad, añade las tags de autodiscovery y deja que Cluster Autoscaler sea el dueño del `desired`. **Vas a migrar a managed node groups, o ya estás en ellos.** Cluster Autoscaler es su complemento natural: EKS etiqueta los ASG para el autodiscovery y drena los nodos spot por ti. Karpenter puede convivir con ellos, pero no los escala. Si lo instalas, los managed node groups se quedan como capacidad fija (el pool de sistema) y los NodePools cubren el resto. Es un modelo distinto, no un complemento. **Muchos pools spot, cargas muy variables y el foco en el coste.** Karpenter, más un node group de sistema pequeño y on-demand. Aquí la diversificación amplia y la consolidación sí tienen margen para compensar la complejidad, siempre que las requests sean fiables y el equipo acepte más rotación de pods. **Pocos tipos de instancia, capacidad bastante estable y un equipo pequeño.** Cluster Autoscaler. La ventaja de Karpenter se diluye y la simplicidad gana. **No quieres operar ningún autoscaler y puedes vivir con nodos estándar.** EKS Auto Mode: AWS opera el autoscaling (basado en Karpenter) y buena parte de los componentes del plano de datos, a cambio de un cargo por instancia y de renunciar a AMIs y bootstrap propios. Y en cualquiera de los cinco casos, antes de tocar el autoscaler: - ¿Quién drena los nodos spot cuando llega el aviso? - ¿Qué allocation strategy usan tus pools? - ¿Qué workloads críticos no tienen PDB, o tienen uno que bloquea todo? - ¿Qué pasa si una ola de interrupciones se lleva el autoscaler, el DNS y lo que drena los nodos? - ¿Dónde corre CoreDNS, y sobrevive a perder una zona? ### Si eliges Karpenter, como nosotros: tres apuntes prácticos - **No necesitas un release de Helm.** Puedes renderizar el chart con `helm template`, aplicar los CRDs desde el tag de la versión y versionar el YAML en Git. La [guía de migración](https://karpenter.sh/docs/getting-started/migrating-from-cas/) oficial lo hace así. - **Tampoco necesitas CloudFormation.** El rol de nodo, la cola SQS, las reglas de EventBridge y la política del controlador se pueden crear con la AWS CLI o con Terraform. La política del controlador es larga: la guía de migración la crea como una única inline policy, y la [plantilla de CloudFormation](https://karpenter.sh/docs/reference/cloudformation/) la reparte en seis managed policies. - **Configura la cola de interrupciones desde el primer día.** Sin `interruptionQueue`, Karpenter no drena los nodos spot antes de que AWS los reclame, y vuelves al problema del principio. ## Conclusión Karpenter es más flexible: elige la instancia para los pods que esperan, no depende de ASG para sus nodos y puede sustituir nodos por otros más baratos. Cluster Autoscaler es más predecible: escala plantillas que tú definiste, en recursos de AWS que ya sabes gestionar. Ninguna de las dos cosas es gratis. La flexibilidad se paga con un controlador más, otra forma de definir la capacidad y más rotación. La predictibilidad, con grupos de una sola forma y menos margen de ahorro. Nosotros elegimos Karpenter, pero no porque sea más rápido o más barato en abstracto. Lo elegimos porque los pools spot había que rediseñarlos de todas formas, y puestos a rediseñar, preferimos un modelo que no nos obliga a partir los grupos por forma ni a migrar dos veces. Si hubiéramos podido conservar los ASG tal como estaban, Cluster Autoscaler con NTH, PDBs y una estrategia spot sensata habría resuelto el problema con menos cambios. Ser la tecnología más flexible no convierte la migración en la decisión correcta. Lo que la justifica es el punto de partida. En cualquier caso, antes de cambiar de autoscaler conviene mirar lo básico: dónde corren tus componentes críticos, si tienen PDB y si sobreviven a perder una zona. Son riesgos que no aparecen en ningún dashboard hasta el día en que se materializan, y ese día [el tiempo hasta que alguien se entera](/2026/blog/tech/el-tiempo-entre-que-algo-falla-y-que-alguien-se-entera/) ya corre en tu contra. En nuestro caso, lo más grave no estaba en el autoscaler, sino en dos réplicas de CoreDNS durmiendo en la misma AZ.