Módulo 3: Configuration Secrets Health And Autoscaling
7. `HorizontalPodAutoscaler`: escalar por métricas, no por corazonada
Descripción
Cada vez que esta guía cambió el número de réplicas de andes-cargo-status-api —de 2 a 3, en el proyecto del Módulo 2—, lo hiciste tú, editando un número a mano en deployment.yaml. Eso funciona en un laboratorio, pero es la tercera pregunta sin responder de la lección 1: ¿qué pasa si el tráfico real cambia más rápido de lo que cualquier persona puede reaccionar? Esta lección instala la pieza que responde eso —el HorizontalPodAutoscaler— y, antes de llegar ahí, resuelve un requisito real que casi nadie anticipa la primera vez que lo necesita: Kubernetes no sabe cuánta CPU usa un Pod a menos que algo se lo diga.
Conexión con el módulo
Esta es la tercera y última pieza de las tres que la lección 1 prometió. La lección 8 —el proyecto de este módulo— genera carga real contra andes-cargo-status-api y observa el HorizontalPodAutoscaler de esta lección reaccionar de verdad, subiendo y bajando réplicas sin que nadie edite ningún YAML durante el proceso.
El problema que resuelve un HorizontalPodAutoscaler
Un HorizontalPodAutoscaler (HPA) es un objeto de Kubernetes que ajusta, de forma continua y automática, el campo replicas de un Deployment —el mismo campo que editaste a mano en el Módulo 2—, según una métrica real que tú declaras. En vez de "siempre 3 réplicas, decidido una vez", el HPA implementa "tantas réplicas como hagan falta para que el promedio de uso de CPU se mantenga cerca de un objetivo, ni una más ni una menos, ajustado cada pocos segundos".
QUÉ HACE UN HorizontalPodAutoscaler
┌─────────────────┐ lee métricas cada ┌──────────────────┐
│ metrics-server │ ◀────pocos segundos───│ HorizontalPod- │
│ (CPU real de │ │ Autoscaler │
│ cada Pod) │ └─────────┬──────────┘
└─────────────────┘ │
│ ajusta replicas
▼
┌──────────────────┐
│ Deployment │
│ andes-cargo- │
│ status-api │
└──────────────────┘
Analogía: el aire acondicionado, no el reloj
Un edificio podría tener una regla fija: "el aire acondicionado sube al máximo de 9 a 5, y se apaga el resto del tiempo" — una decisión tomada una vez, sin relación con la temperatura real de cada momento. Un termostato de verdad hace algo distinto: mide la temperatura real, constantemente, y ajusta el enfriamiento según lo que encuentra —más fuerte en un día de calor extremo aunque sea festivo, apenas encendido en un día templado aunque sea horario pico—. Un Deployment con replicas: 3 fijo es el primer edificio: una decisión tomada una vez, sin relación con la carga real. Un HorizontalPodAutoscaler es el termostato: mide una métrica real (CPU, en esta lección) y ajusta las réplicas en consecuencia, sin que nadie tenga que estar mirando un reloj ni adivinando cuándo va a subir el tráfico.
El requisito que casi nadie anticipa: metrics-server
Un HPA necesita leer el uso real de CPU de cada Pod — y ese dato no existe en un clúster de Kubernetes recién instalado. kind, como la mayoría de las distribuciones de Kubernetes, no incluye metrics-server por defecto. Confírmalo tú mismo antes de instalar nada:
kubectl top nodes
Qué esperar (literal, en un clúster sin metrics-server):
error: Metrics API not available
kubectl top —el comando que resume CPU/memoria de nodos o Pods— depende de la misma API que un HPA usa por debajo (metrics.k8s.io). Sin metrics-server corriendo, ni tú ni el HPA tienen ningún número real que consultar.
Instala metrics-server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Qué esperar (literal, ejecutado — la lista completa de objetos que el manifiesto oficial crea):
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
Confirma la versión instalada:
kubectl get deployment metrics-server -n kube-system -o jsonpath='{.spec.template.spec.containers[0].image}'
Qué esperar (la versión estable más reciente al momento de escribir esta guía — verifica la tuya, puede haber avanzado):
registry.k8s.io/metrics-server/metrics-server:v0.9.0
El gotcha: certificados autofirmados de kind
Espera unos segundos y prueba de nuevo:
kubectl top nodes
Qué esperar (todavía falla — esto es el gotcha, no un error tuyo):
error: Metrics API not available
Revisa los logs del propio metrics-server para entender por qué:
kubectl logs -n kube-system -l k8s-app=metrics-server --tail=20
Qué esperar (literal, ejecutado):
E0814 20:07:05.393308 1 scraper.go:149] "Failed to scrape node" err="Get \"https://172.19.0.4:10250/metrics/resource\": tls: failed to verify certificate: x509: cannot validate certificate for 172.19.0.4 because it doesn't contain any IP SANs" node="andes-cargo-cluster-worker"
E0814 20:07:05.398048 1 scraper.go:149] "Failed to scrape node" err="Get \"https://172.19.0.2:10250/metrics/resource\": tls: failed to verify certificate: x509: cannot validate certificate for 172.19.0.2 because it doesn't contain any IP SANs" node="andes-cargo-cluster-control-plane"
E0814 20:07:05.398262 1 scraper.go:149] "Failed to scrape node" err="Get \"https://172.19.0.3:10250/metrics/resource\": tls: failed to verify certificate: x509: cannot validate certificate for 172.19.0.3 because it doesn't contain any IP SANs" node="andes-cargo-cluster-worker2"
I0814 20:07:25.113290 1 server.go:192] "Failed probe" probe="metric-storage-ready" err="no metrics to serve"
La causa exacta, sin adivinar: metrics-server necesita conectarse al kubelet de cada nodo por HTTPS (puerto 10250) para leer sus métricas — y verificar ese certificado TLS contra una autoridad certificadora, como hace por defecto contra un clúster de nube real. El problema es que kind genera certificados de kubelet autofirmados, sin ninguna entrada de IP SAN (Subject Alternative Name) que declare explícitamente las IPs internas del clúster — un detalle normal de un clúster de laboratorio, que metrics-server interpreta correctamente como "no puedo confiar en este certificado". Este no es un error de tu instalación: es una fricción documentada, conocida, entre metrics-server y cualquier clúster con certificados de kubelet no verificables de esta forma —incluido kind, minikube, y varias otras distribuciones locales.
La solución oficial es la bandera --kubelet-insecure-tls, que le dice a metrics-server: "conéctate de todas formas, sin verificar el certificado del kubelet". Es aceptable en un laboratorio local como este, donde ya confías en la red completa del clúster; en un EKS real, el plano de control gestionado por AWS resuelve este problema de raíz con certificados verificables por defecto, y esta bandera no hace falta.
kubectl patch deployment metrics-server -n kube-system --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
Qué esperar:
deployment.apps/metrics-server patched
Confirma que la bandera quedó agregada al final de la lista de argumentos existentes:
kubectl get deployment metrics-server -n kube-system -o jsonpath='{.spec.template.spec.containers[0].args}'
Qué esperar (literal):
["--cert-dir=/tmp","--secure-port=10250","--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname","--kubelet-use-node-status-port","--metric-resolution=15s","--kubelet-insecure-tls"]
Espera a que el nuevo Pod de metrics-server arranque y confirma:
kubectl top nodes
Qué esperar (literal, ejecutado — los números de CPU/memoria van a variar según la carga real de tu máquina en el momento exacto en que corras el comando):
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
andes-cargo-cluster-control-plane 131m 1% 974Mi 12%
andes-cargo-cluster-worker 62m 0% 403Mi 5%
andes-cargo-cluster-worker2 28m 0% 345Mi 4%
kubectl top pods -n andes-cargo
Qué esperar (literal — tus tres Pods, con CPU real, en reposo):
NAME CPU(cores) MEMORY(bytes)
andes-cargo-status-api-65fcd6f6c8-45hq5 2m 40Mi
andes-cargo-status-api-65fcd6f6c8-9hbfr 1m 40Mi
andes-cargo-status-api-65fcd6f6c8-kr7hv 1m 40Mi
metrics-server está sano, y por primera vez en esta guía, hay un número real de uso de CPU por Pod al que un HPA puede referirse.
Un requisito adicional: resources.requests
Un HPA que escala por porcentaje de utilización de CPU (averageUtilization) necesita un punto de referencia contra el cual calcular ese porcentaje — y ese punto de referencia es resources.requests.cpu, el mismo campo que declara cuánta CPU "reserva" cada Pod al programarse en un nodo. Sin ese campo, 50% de utilización no tiene ningún denominador contra el cual calcularse. Agrégalo al Deployment:
# deployment.yaml (fragmento nuevo dentro de containers[0])
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
100m significa cien milicores, o el 10% de un núcleo de CPU — el valor que el HPA va a usar como el "100%" de referencia para este Pod específico. Aplica el Deployment completo con este bloque agregado:
kubectl apply -f deployment.yaml
kubectl rollout status deployment/andes-cargo-status-api -n andes-cargo --timeout=60s
Qué esperar (mismo patrón de RollingUpdate ya conocido de las lecciones 4 y 6):
deployment.apps/andes-cargo-status-api configured
Waiting for deployment "andes-cargo-status-api" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "andes-cargo-status-api" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "andes-cargo-status-api" rollout to finish: 1 old replicas are pending termination...
deployment "andes-cargo-status-api" successfully rolled out
Crea el HorizontalPodAutoscaler
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: andes-cargo-status-api-hpa
namespace: andes-cargo
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: andes-cargo-status-api
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
behavior:
scaleDown:
stabilizationWindowSeconds: 60
Cada campo, explicado:
scaleTargetRef— a quéDeploymentcontrola esteHPA. El mismo mecanismo de referencia por nombre que ya conoces deselector/matchLabels, pero apuntando a un objeto entero, no a un conjunto de Pods.minReplicas/maxReplicas— el rango dentro del cual elHPApuede moverse. Nunca va a bajar de 2 (así elServicenunca se queda con una sola réplica), ni va a subir de 6 (un techo de seguridad, para que un pico de tráfico no consuma toda la capacidad del clúster de laboratorio sin límite).metrics[0]— la métrica objetivo: utilización de CPU, con un objetivo de50%del valor declarado enresources.requests.cpu. Si el promedio real sube por encima de50%, elHPAagrega réplicas; si baja, las quita —siempre dentro del rangominReplicas/maxReplicas.behavior.scaleDown.stabilizationWindowSeconds: 60— una salvaguarda deliberada: cuando la CPU baja, elHPAespera 60 segundos de métricas consistentemente bajas antes de reducir réplicas, para no reaccionar a una caída momentánea y volver a escalar hacia arriba segundos después (un patrón conocido como flapping). No hay unstabilizationWindowSecondsequivalente para escalar hacia arriba en esta configuración —por diseño: reaccionar rápido a un pico de tráfico real es más importante que evitar una réplica de más por unos segundos.
kubectl apply -f hpa.yaml
Qué esperar:
horizontalpodautoscaler.autoscaling/andes-cargo-status-api-hpa created
Espera unos segundos —el HPA necesita al menos un ciclo de su período de sincronización (15 segundos, por defecto) para calcular la métrica actual— y confirma:
kubectl get hpa -n andes-cargo
Qué esperar (literal, ejecutado — 1% es tu CPU en reposo, el valor real va a variar):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 3 33s
cpu: 1%/50% — muy por debajo del objetivo, así que el HPA no tiene ningún motivo para escalar todavía. REPLICAS: 3 porque el Deployment ya tenía 3 réplicas cuando el HPA empezó a controlarlo, un número dentro del rango 2-6 que el HPA no tiene motivo para tocar en reposo.
Confirma la política de comportamiento completa:
kubectl describe hpa andes-cargo-status-api-hpa -n andes-cargo
Qué esperar (fragmento relevante, literal):
Behavior:
Scale Up:
Stabilization Window: 0 seconds
Select Policy: Max
Policies:
- Type: Pods Value: 4 Period: 15 seconds
- Type: Percent Value: 100 Period: 15 seconds
Scale Down:
Stabilization Window: 60 seconds
Select Policy: Max
Policies:
- Type: Percent Value: 100 Period: 15 seconds
Fíjate en Scale Up: aunque hpa.yaml solo declaró explícitamente behavior.scaleDown, Kubernetes rellenó automáticamente una política de Scale Up por defecto —agresiva a propósito, para reaccionar rápido a un pico real—, mientras que Scale Down usa exactamente los 60 segundos que sí declaraste. Esta asimetría —subir rápido, bajar con cautela— es la misma filosofía que ya viste en las probes de la lección 5: la consecuencia "barata" (agregar capacidad) se dispara con más soltura que la consecuencia "cara de deshacer mal" (quitar capacidad de golpe).
Errores comunes
Crear el HPA antes de que el Deployment termine su RollingUpdate con resources.requests incluido, y ver un error transitorio de métricas. Qué pasa: si el HPA se crea mientras todavía existen Pods de la plantilla vieja (sin resources.requests), el HPA puede reportar brevemente un error como failed to get cpu utilization: missing request for cpu in container ... of Pod .... Por qué pasa: un HPA que calcula porcentaje de utilización necesita que todos los Pods que está promediando tengan resources.requests.cpu declarado — un solo Pod sin ese campo invalida el cálculo completo. Cómo detectarlo: kubectl describe hpa muestra un evento Warning FailedGetResourceMetric mencionando un Pod específico. Cómo corregirlo: espera a que kubectl rollout status confirme que el RollingUpdate terminó por completo antes de crear el HPA — el error se resuelve solo en cuanto todos los Pods activos comparten la misma plantilla con resources.requests.
Olvidar --kubelet-insecure-tls y asumir que metrics-server está roto (de expectativa, el gotcha central de esta lección). Qué pasa: alguien instala metrics-server con el manifiesto oficial, ve error: Metrics API not available persistiendo después de esperar, y concluye que el proyecto tiene un bug. Cómo detectarlo: kubectl logs -n kube-system -l k8s-app=metrics-server muestra específicamente x509: cannot validate certificate ... because it doesn't contain any IP SANs. Cómo corregirlo: esta lección documentó la causa exacta y la solución —--kubelet-insecure-tls— porque es una fricción conocida y esperada en kind (y en la mayoría de clústeres locales), no un defecto del proyecto metrics-server ni de tu instalación.
Confundir resources.requests con resources.limits al pensar en el objetivo del HPA (conceptual). Qué pasa: alguien asume que averageUtilization: 50 se calcula contra resources.limits.cpu (250m en esta lección), no contra resources.requests.cpu (100m), y se sorprende de que el HPA escale "antes de lo esperado". Cómo detectarlo: si tus cálculos manuales de "a qué porcentaje real de CPU debería escalar" no coinciden con lo que observas. Cómo corregirlo: un HPA de tipo Utilization siempre calcula el porcentaje contra requests, nunca contra limits — con requests.cpu: 100m y objetivo 50%, el HPA reacciona cuando el uso real cruza 50m por Pod, sin importar qué tan lejos esté eso de limits.cpu: 250m.
Ejercicios
Ejercicio 1 — Reconstruye la secuencia completa de instalación. Sin volver a la lección, enumera los pasos, en orden, desde confirmar que metrics-server no existe hasta ver el primer kubectl get hpa con un número real.
Ver solución
- Confirmar con
kubectl top nodesque no haymetrics-server(error: Metrics API not available). - Instalar el manifiesto oficial con
kubectl apply -f .../components.yaml. - Confirmar que
kubectl top nodessigue fallando, y diagnosticar la causa conkubectl logs(certificados autofirmados sin IP SANs). - Parchear el
Deploymentdemetrics-serverpara agregar--kubelet-insecure-tls. - Confirmar
kubectl top nodes/kubectl top podsfuncionando. - Agregar
resources.requests/resources.limitsalDeploymentdeandes-cargo-status-api, aplicar, y esperar elRollingUpdate. - Crear
hpa.yaml, aplicarlo, y confirmarkubectl get hpamostrando un porcentaje real de CPU.
Ejercicio 2 — Explica el gotcha de TLS a un colega sin usar la palabra "certificado". En dos o tres frases, sin usar la palabra "certificado" ni "TLS", explica por qué metrics-server falla en kind sin --kubelet-insecure-tls.
Ver solución
Una respuesta razonable: "metrics-server necesita confiar en la identidad de cada nodo antes de leer sus métricas, y en un clúster de laboratorio como kind, esa identidad no viene firmada de una forma que metrics-server pueda verificar automáticamente por defecto. La bandera le dice explícitamente 'confía de todas formas' — algo razonable en un laboratorio local, pero que un clúster de nube real como EKS no necesita, porque ahí la identidad de cada nodo sí es verificable de forma estándar."
Ejercicio 3 — Calcula cuándo escalaría este HPA. Con resources.requests.cpu: 100m y averageUtilization: 50, ¿a partir de cuántos milicores de uso promedio por Pod el HPA de esta lección empezaría a agregar réplicas?
Ver solución
A partir de 50m de uso promedio por Pod (el 50% de los 100m declarados en requests.cpu). Si el promedio real de los Pods actuales supera ese umbral de forma sostenida, el HPA calcula cuántas réplicas adicionales harían falta para volver a bajar el promedio cerca del objetivo, y ajusta replicas en consecuencia — el mecanismo exacto que la lección 8 va a disparar con carga real.
Resumen y siguiente paso
Esta lección instaló la tercera y última pieza de este módulo: metrics-server, con el gotcha real de certificados autofirmados de kind documentado y resuelto (--kubelet-insecure-tls); resources.requests/resources.limits en el Deployment, el punto de referencia que cualquier HPA basado en utilización necesita; y el HorizontalPodAutoscaler andes-cargo-status-api-hpa en sí, con un rango de 2 a 6 réplicas y un objetivo de 50% de CPU. En reposo, el HPA no tiene motivo para actuar —lo confirmaste con cpu: 1%/50%—, pero el mecanismo completo ya está en su lugar, esperando una condición real.
Antes de avanzar deberías poder: explicar por qué metrics-server no viene incluido por defecto en kind, y diagnosticar el error de certificados sin adivinar; explicar por qué resources.requests (no limits) es el denominador de un HPA de tipo Utilization; y leer un kubectl describe hpa completo, incluidas sus políticas de Scale Up/Scale Down.
Siguiente lección: proyecto de este módulo, andes-cargo-status-api bajo carga. Ahí generas tráfico real, ves el HPA reaccionar de verdad —réplicas subiendo cuando la CPU sube, bajando cuando la carga cesa— y cierras el módulo completo con el sistema entero funcionando junto.
Recursos
- Kubernetes — Horizontal Pod Autoscaling — la guía oficial completa del mecanismo de esta lección.
- Kubernetes — HorizontalPodAutoscaler Walkthrough — tutorial oficial paso a paso, con el mismo patrón de
resources.requests+HPAde esta lección. kubernetes-sigs/metrics-server— repositorio oficial del proyecto, incluida la documentación de--kubelet-insecure-tlsy por qué hace falta en clústeres locales.kind— Known Issues — documentación oficial dekindsobre fricciones conocidas de un clúster local, el mismo tipo de gotcha (certificados autofirmados dekubelet) que esta lección documentó paso a paso.