El Horizontal Pod Autoscaler (HPA) ajusta automáticamente el número de réplicas de un Deployment o StatefulSet según el uso de recursos de sus pods: añade réplicas cuando la carga sube y las retira cuando baja. En este tutorial instalarás metrics-server, desplegarás una aplicación de ejemplo, crearás un HPA con la API autoscaling/v2 que escala por CPU y memoria, ajustarás su comportamiento para evitar oscilaciones y lo verás actuar con una prueba de carga.

Requisitos previos

Para seguir esta guía necesitas:

  • Un clúster de Kubernetes 1.29 o superior con al menos dos nodos worker, por ejemplo desplegado con kubeadm sobre VPS de CubePath.
  • kubectl configurado contra el clúster con permisos de administrador.
  • Espacio libre en el clúster para unas 10 réplicas de un pod que pide 200m de CPU (unos 2 vCPU en total).

Cómo decide el HPA el número de réplicas

El controlador del HPA consulta las métricas cada 15 segundos y calcula las réplicas deseadas con esta fórmula:

réplicas deseadas = ceil(réplicas actuales × valor actual / valor objetivo)

Por ejemplo, con 2 réplicas al 100 % de CPU y un objetivo del 50 %, el HPA pide ceil(2 × 100 / 50) = 4 réplicas. El porcentaje se calcula sobre las requests del contenedor, no sobre sus limits, así que un pod sin requests de CPU no se puede escalar por CPU. Si la diferencia con el objetivo es menor del 10 %, el HPA no hace nada, para evitar cambios constantes.

Paso 1: Instalar metrics-server

El HPA obtiene el uso de CPU y memoria de la API metrics.k8s.io, que publica metrics-server a partir de los datos del kubelet de cada nodo. Comprueba si ya lo tienes (algunas distribuciones como k3s lo incluyen):

kubectl get deployment metrics-server -n kube-system

Si no existe, instala la última versión publicada por el proyecto:

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

Espera un minuto y comprueba que devuelve datos:

kubectl top nodes
NAME       CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
master-1   182m         9%       1320Mi          35%
worker-1   95m          4%       980Mi           26%
worker-2   88m          4%       1012Mi          27%

Si en su lugar ves error: Metrics API not available y el pod de metrics-server no llega a Ready, revisa sus logs:

kubectl logs -n kube-system deploy/metrics-server

En clústeres kubeadm es habitual el error x509: cannot validate certificate, porque los kubelets usan certificados autofirmados. La solución correcta es que los kubelets pidan certificados firmados por la CA del clúster (serverTLSBootstrap: true en la configuración del kubelet y aprobar los CSR). En un clúster de pruebas puedes desactivar la verificación añadiendo el argumento --kubelet-insecure-tls:

kubectl patch deployment metrics-server -n kube-system --type=json \
  -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

Paso 2: Desplegar una aplicación de ejemplo

Usarás la imagen hpa-example del proyecto Kubernetes, un servidor PHP que realiza un cálculo costoso en cada petición y así consume CPU con facilidad. Crea el manifiesto:

nano php-apache.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: php-apache
  template:
    metadata:
      labels:
        app: php-apache
    spec:
      containers:
        - name: php-apache
          image: registry.k8s.io/hpa-example
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 200m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
spec:
  selector:
    app: php-apache
  ports:
    - port: 80

Las requests son imprescindibles: el objetivo del 50 % de CPU que usarás después significa 100m por pod. Aplica el manifiesto:

kubectl apply -f php-apache.yaml
kubectl rollout status deployment/php-apache
deployment "php-apache" successfully rolled out

Paso 3: Crear el HPA

Define el HPA con la API autoscaling/v2, que permite combinar varias métricas y configurar el comportamiento del escalado:

nano php-apache-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Con varias métricas, el HPA calcula las réplicas necesarias para cada una y aplica la mayor. Aplícalo y consulta su estado:

kubectl apply -f php-apache-hpa.yaml
kubectl get hpa php-apache
NAME         REFERENCE               TARGETS                       MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache   cpu: 1%/50%, memory: 14%/80%   1         10        1          30s

Si en TARGETS aparece <unknown>, espera un minuto: el HPA necesita un par de ciclos para leer las primeras métricas.

Paso 4: Generar carga y ver el escalado

Abre una segunda terminal y lanza un pod que hace peticiones continuas al servicio:

kubectl run load-generator --rm -it --image=busybox:1.36 --restart=Never -- \
  /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

En la primera terminal, observa el HPA:

kubectl get hpa php-apache --watch

En uno o dos minutos la CPU supera el objetivo y las réplicas aumentan:

NAME         REFERENCE               TARGETS                        MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache   cpu: 1%/50%, memory: 14%/80%    1         10        1          3m
php-apache   Deployment/php-apache   cpu: 248%/50%, memory: 15%/80%  1         10        1          4m
php-apache   Deployment/php-apache   cpu: 248%/50%, memory: 15%/80%  1         10        4          4m
php-apache   Deployment/php-apache   cpu: 71%/50%, memory: 16%/80%   1         10        6          5m

Detén el generador de carga con Ctrl+C en la segunda terminal. El HPA no reduce réplicas de inmediato: por defecto espera una ventana de estabilización de 5 minutos para no deshacer el escalado ante una bajada puntual. Pasado ese tiempo volverá a 1 réplica.

Para ver las decisiones que ha tomado el HPA, revisa sus eventos:

kubectl describe hpa php-apache
Events:
  Type    Reason             Age   From                       Message
  ----    ------             ----  ----                       -------
  Normal  SuccessfulRescale  4m    horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  3m    horizontal-pod-autoscaler  New size: 6; reason: cpu resource utilization (percentage of request) above target

Paso 5: Ajustar el comportamiento del escalado

El campo behavior controla la velocidad a la que el HPA añade y retira réplicas. Una configuración habitual para servicios web es escalar hacia arriba rápido y hacia abajo despacio. Edita el HPA:

nano php-apache-hpa.yaml

Añade el bloque behavior dentro de spec, al mismo nivel que metrics:

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
        - type: Pods
          value: 4
          periodSeconds: 15
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
      selectPolicy: Min

Qué significa cada parte:

CampoEfecto
scaleUp.policiesCada 15 segundos puede duplicar las réplicas o añadir 4, lo que sea mayor (selectPolicy: Max).
scaleUp.stabilizationWindowSeconds: 0Escala hacia arriba en cuanto la métrica lo pide.
scaleDown.stabilizationWindowSeconds: 300Usa la recomendación más alta de los últimos 5 minutos antes de reducir.
scaleDown.policiesRetira como máximo el 50 % de las réplicas por minuto.

Aplica el cambio y comprueba que el HPA lo ha aceptado:

kubectl apply -f php-apache-hpa.yaml
kubectl get hpa php-apache -o jsonpath='{.spec.behavior.scaleDown}{"\n"}'
{"policies":[{"periodSeconds":60,"type":"Percent","value":50}],"selectPolicy":"Min","stabilizationWindowSeconds":300}

Repite la prueba de carga del paso 4 para ver cómo ahora las réplicas suben más deprisa y bajan de forma escalonada.

HPA y Vertical Pod Autoscaler

El Vertical Pod Autoscaler (VPA) no añade réplicas: cambia las requests de CPU y memoria de cada pod según su consumo real. No se instala con Kubernetes; es un proyecto aparte del repositorio kubernetes/autoscaler. No combines HPA y VPA sobre la misma métrica (por ejemplo, ambos sobre CPU), porque el VPA modifica las requests que el HPA usa como referencia y los dos se contradicen. Un uso seguro del VPA es el modo Off, que solo recomienda valores de requests para que los ajustes tú.

Solución de problemas

  • TARGETS muestra <unknown> de forma permanente. Ejecuta kubectl describe hpa php-apache. El evento FailedGetResourceMetric indica que metrics-server no funciona (vuelve al paso 1) o que algún contenedor del pod no declara requests para esa métrica, incluidos los sidecars.
  • Las réplicas no pasan de maxReplicas. Es el límite configurado. Si los pods nuevos se quedan en Pending, el clúster no tiene capacidad: revisa kubectl describe pod y añade nodos.
  • Las réplicas no bajan. Recuerda la ventana de estabilización de 5 minutos. Si escalas por memoria, comprueba con kubectl top pods si la aplicación libera memoria realmente.

Conclusión

Has instalado metrics-server, creado un HPA que escala por CPU y memoria y ajustado su comportamiento para reaccionar rápido ante picos sin oscilar al bajar. La clave está en definir requests realistas, porque todo el cálculo se basa en ellas. Como siguientes pasos, puedes escalar por métricas de la aplicación (peticiones por segundo, longitud de colas) con Prometheus y prometheus-adapter, probar KEDA para escalar según eventos externos, o añadir Cluster Autoscaler para que el clúster cree nodos cuando los pods nuevos no caben.