Módulo 3: Configuration Secrets Health And Autoscaling
8. Proyecto: `andes-cargo-status-api` bajo carga
Descripción
Este proyecto cierra el Módulo 3 con la prueba que todas las lecciones anteriores prepararon: vas a generar tráfico real contra andes-cargo-status-api, ver el HorizontalPodAutoscaler de la lección 7 reaccionar de verdad —réplicas subiendo mientras la carga sube—, y después, al detener la carga, ver esas mismas réplicas bajar solas, sin que nadie edite deployment.yaml ni una vez. Todo lo que sigue corrió contra andes-cargo-cluster, con el número exacto de réplicas en cada punto siendo tu propio valor variable —el mecanismo, no el número, es lo que este proyecto demuestra.
Conexión con el módulo
Cada lección de este módulo construyó una pieza: ConfigMap/Secret (lecciones 2-4), probes (lecciones 5-6), HorizontalPodAutoscaler/metrics-server (lección 7). Este proyecto es la primera vez que ves la tercera pieza funcionando bajo condición real —no en reposo, como terminó la lección 7—, cerrando el arco completo que la lección 1 abrió: de "3 réplicas corriendo" a un sistema con configuración externalizada, verificación de salud, y capacidad que se ajusta sola.
Paso 1 — Confirma la línea base antes de generar carga
kubectl get hpa -n andes-cargo
Qué esperar (tu porcentaje de CPU en reposo puede variar levemente, pero debería estar muy por debajo de 50%):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 3 33s
kubectl top pods -n andes-cargo -l app=andes-cargo-status-api
Qué esperar (CPU real, muy baja, en reposo):
NAME CPU(cores) MEMORY(bytes)
andes-cargo-status-api-548966dd97-cwtlv 2m 40Mi
andes-cargo-status-api-548966dd97-ksl7m 2m 40Mi
andes-cargo-status-api-548966dd97-qbl4h 1m 40Mi
Esta es tu foto de "antes" — guárdala mentalmente para comparar contra lo que sigue.
Paso 2 — El generador de carga
Un solo cliente HTTP secuencial no genera suficiente trabajo para mover la aguja de CPU de un servicio tan liviano como /health — necesitas concurrencia real. Este generador de carga lanza 30 bucles paralelos dentro de un único Pod busybox, cada uno pidiendo /health sin parar:
# load-generator.yaml
apiVersion: v1
kind: Pod
metadata:
name: load-generator
namespace: andes-cargo
labels:
app: load-generator
spec:
restartPolicy: Never
containers:
- name: load-generator
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- |
for i in $(seq 1 30); do
(while true; do wget -q -O- http://status-api-service.andes-cargo.svc.cluster.local/health >/dev/null; done) &
done
wait
Fíjate en el nombre DNS del Service (status-api-service.andes-cargo.svc.cluster.local) — el mismo patrón <service>.<namespace>.svc.cluster.local que ya conoces desde el Módulo 2, y en el mismo namespace andes-cargo que el propio servicio, para que el tráfico se quede completamente dentro del clúster.
kubectl apply -f load-generator.yaml
Qué esperar:
pod/load-generator created
Paso 3 — Observa el HPA reaccionar: réplicas subiendo
kubectl top pods -n andes-cargo -l app=andes-cargo-status-api
kubectl get hpa -n andes-cargo
Qué esperar (~10 segundos después de iniciar la carga — literal, ejecutado; tus números de CPU y el momento exacto en que cruza el umbral van a variar):
NAME CPU(cores) MEMORY(bytes)
andes-cargo-status-api-548966dd97-cjf9d 130m 41Mi
andes-cargo-status-api-548966dd97-cwtlv 102m 41Mi
andes-cargo-status-api-548966dd97-cxf5t 142m 41Mi
andes-cargo-status-api-548966dd97-ksl7m 76m 40Mi
andes-cargo-status-api-548966dd97-qbl4h 97m 41Mi
andes-cargo-status-api-548966dd97-tdqqs 136m 41Mi
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 95%/50% 2 6 6 117s
cpu: 95%/50% — muy por encima del objetivo. REPLICAS: 6 — el HPA ya escaló al máximo que permite maxReplicas, y ya puedes contar seis Pods reales con CPU muy por encima de su request de 100m cada uno. Nadie editó deployment.yaml: el HPA calculó, solo, cuántas réplicas hacían falta para intentar bajar el promedio hacia el objetivo, y ajustó replicas en el Deployment directamente.
Confirma el evento exacto que registró esa decisión:
kubectl describe hpa andes-cargo-status-api-hpa -n andes-cargo
Qué esperar (fragmento de Events, literal — el Age es tu valor variable):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 3m52s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage of request) above target
New size: 6; reason: cpu resource utilization (percentage of request) above target — el HPA documenta, en texto legible, exactamente por qué tomó la decisión que tomó. No hay ambigüedad sobre la causa.
Paso 4 — Detén la carga
kubectl delete pod load-generator -n andes-cargo --wait=false
Qué esperar:
pod "load-generator" deleted from andes-cargo namespace
Nota honesta: un generador de carga tan agresivo como 30 bucles concurrentes de
wgetdentro de un único Pod puede agotar sus propios puertos efímeros antes de que decidas detenerlo —vas a ver mensajes comowget: can't connect to remote host: Cannot assign requested addressen sus logs si lo dejas correr varios minutos—. Eso no es un problema delServiceni delDeployment: es el propio generador quedándose sin recursos de red locales para abrir más conexiones. Para el propósito de este proyecto —cruzar el umbral una vez y ver el mecanismo reaccionar— no hace falta sostener la carga más de uno o dos minutos.
Paso 5 — Observa el HPA reaccionar: réplicas bajando
La CPU cae casi de inmediato en cuanto el generador de carga deja de enviar tráfico —pero el HPA, por diseño (stabilizationWindowSeconds: 60 en hpa.yaml, lección 7), espera antes de reducir réplicas, para no reaccionar a una caída momentánea:
kubectl get hpa -n andes-cargo
Qué esperar (justo después de detener la carga — la CPU ya bajó, pero REPLICAS todavía no, porque la ventana de estabilización sigue corriendo):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 6 2m47s
Espera alrededor de un minuto (la ventana completa de stabilizationWindowSeconds: 60) y repite:
kubectl get hpa -n andes-cargo
Qué esperar (literal, ejecutado — REPLICAS bajó, directo a minReplicas, sin pasos intermedios, porque la política Scale Down de la lección 7 permite reducir hasta el 100% en un solo ciclo de 15 segundos una vez que se cumple la ventana de estabilización):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 2 4m47s
Confirma el segundo evento, el contrapunto exacto del primero:
kubectl describe hpa andes-cargo-status-api-hpa -n andes-cargo
Qué esperar (fragmento de Events, literal):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 3m52s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 52s horizontal-pod-autoscaler New size: 2; reason: All metrics below target
Los dos eventos, uno debajo del otro: New size: 6 cuando la CPU subió, New size: 2 cuando bajó y se sostuvo baja el tiempo suficiente. Ese es el ciclo completo —subida y bajada, ambas automáticas, ambas documentadas por el propio HPA con su razón exacta— sin que tú hayas tocado deployment.yaml en ningún momento de este proyecto.
EL CICLO COMPLETO DE ESTE PROYECTO (números — los tuyos van a variar)
reposo carga alta carga detenida estabilizado
REPLICAS: 3 ──▶ REPLICAS: 6 ────────▶ REPLICAS: 6 (espera) ──▶ REPLICAS: 2
cpu: 1%/50% cpu: 95%/50% cpu: 1%/50% cpu: 1%/50%
"above target" "below target"
(60s de stabilization
window antes de bajar)
Paso 6 — El checklist final: el sistema completo de este módulo
kubectl get all -n andes-cargo
kubectl get configmap,secret -n andes-cargo
kubectl get hpa -n andes-cargo
Qué esperar (los nombres de Pod y REPLICAS finales son tu valor variable, según cuándo corras esto respecto al ciclo del HPA; los nombres de objeto —Deployment, Service, ConfigMap, Secret, HorizontalPodAutoscaler— son literales):
NAME READY STATUS RESTARTS AGE
pod/andes-cargo-status-api-548966dd97-cwtlv 1/1 Running 0 6m17s
pod/andes-cargo-status-api-548966dd97-ksl7m 1/1 Running 0 6m5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/status-api-service ClusterIP 10.96.78.1 <none> 80/TCP 40m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/andes-cargo-status-api 2/2 2 2 41m
NAME DATA AGE
configmap/andes-cargo-status-api-config 3 19m
NAME TYPE DATA AGE
secret/andes-cargo-status-api-secrets Opaque 2 19m
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 2%/50% 2 6 2 5m50s
Seis tipos de objeto, una sola historia: el Deployment que el Módulo 2 declaró, ahora con ConfigMap/Secret montados (lecciones 2-4), probes verificando su salud continuamente (lecciones 5-6), y un HorizontalPodAutoscaler decidiendo, solo, cuántas réplicas hacen falta en cada momento (lecciones 7-8) — el mismo Service status-api-service de siempre, sin ningún cambio, sirviendo de frente estable sin importar cuántas réplicas haya detrás en cada momento exacto.
Errores comunes
Esperar un número exacto de réplicas al final del experimento, en vez del mecanismo (de expectativa, el más importante de prevenir en este proyecto). Qué pasa: alguien compara su propio REPLICAS: 2 (o el número que sea) contra el de otra persona, y se preocupa de que algo esté mal si no coincide exactamente. Por qué pasa: el resto de esta guía tiene mucha salida perfectamente literal, y es fácil generalizar esa expectativa a este proyecto también. Cómo detectarlo: si comparas tu número final de réplicas contra un valor "esperado" específico. Cómo corregirlo: la tabla de honestidad de esta guía (nombrada desde el diseño) marca explícitamente el número final de réplicas de un evento de HorizontalPodAutoscaler como variable — depende de la carga real generada, la velocidad de tu máquina, y el momento exacto en que corriste cada comando. Lo literal, y lo que este proyecto demuestra, es el mecanismo: sube cuando la CPU cruza el umbral, baja cuando se sostiene debajo, ambos con una razón documentada en los Events.
Interpretar los mensajes de wget sobre puertos agotados como un fallo del Service o del Deployment (de disciplina). Qué pasa: alguien deja el load-generator corriendo varios minutos, ve mensajes de error en sus logs, y empieza a revisar service.yaml o deployment.yaml buscando un problema. Cómo detectarlo: el mensaje exacto (Cannot assign requested address) es un error de red local del propio Pod generador, no una respuesta HTTP de andes-cargo-status-api. Cómo corregirlo: como advirtió el Paso 4, un generador de carga sintético con demasiada concurrencia puede agotar sus propios puertos efímeros antes de que el objetivo real (el Service) tenga ningún problema — el HPA ya cumplió su función en cuanto cruzaste el umbral una vez, no hace falta sostener la carga indefinidamente.
Crear el HPA de la lección 7 más de una vez, sin darse cuenta (de flujo). Qué pasa: alguien corre kubectl apply -f hpa.yaml de nuevo en este proyecto, esperando que "reinicie" el HPA, y en vez de eso Kubernetes simplemente confirma que el objeto ya existe sin cambios (unchanged, no created). Cómo detectarlo: si el resultado de tu kubectl apply -f hpa.yaml dice configured o unchanged en vez de created. Cómo corregirlo: no hace falta recrear el HPA para este proyecto — el mismo objeto de la lección 7 sigue corriendo, evaluando la métrica de forma continua; solo necesitas generar carga real para verlo reaccionar, no volver a declararlo.
Ejercicios
Ejercicio 1 — Reconstruye el ciclo completo de memoria. Sin volver al proyecto, dibuja o describe, en tus propias palabras, la secuencia completa de este proyecto: desde la línea base hasta el estado final, incluidos los dos eventos de SuccessfulRescale.
Ver solución
- Confirmar la línea base: CPU baja,
REPLICASen el número heredado del Módulo 2 (o el que fuera al momento de crear elHPA). - Aplicar
load-generator.yaml, generando 30 bucles concurrentes de tráfico contra/health. - Observar
cpusubir muy por encima de50%, yREPLICASsubir hastamaxReplicas(6) — confirmado con un eventoSuccessfulRescale ... reason: cpu resource utilization (percentage of request) above target. - Detener el generador de carga.
- Observar
cpubajar casi de inmediato, peroREPLICASesperar elstabilizationWindowSeconds: 60antes de reducirse. - Confirmar
REPLICASbajando aminReplicas(2), con un segundo eventoSuccessfulRescale ... reason: All metrics below target.
Ejercicio 2 — Explica por qué REPLICAS bajó directo a 2, sin pasos intermedios. Con la política Scale Down de la lección 7 delante (Percent: 100, Period: 15 seconds), explica por qué el HPA de este proyecto pasó de 6 a 2 réplicas en un solo ciclo, en vez de bajar gradualmente.
Ver solución
La política Scale Down permite reducir hasta el 100% de las réplicas actuales en un período de 15 segundos — es decir, no hay ningún límite que fuerce una reducción gradual, más allá de la Stabilization Window de 60 segundos que debe cumplirse antes de reducir en absoluto. Una vez que esa ventana se cumplió, y con las métricas ya muy por debajo del objetivo (cpu: 1%/50%), el HPA calculó que el número correcto de réplicas para ese nivel de CPU era el mínimo permitido (minReplicas: 2), y aplicó ese cambio de una sola vez, sin pasos intermedios artificiales.
Ejercicio 3 — Diseña un experimento con un maxReplicas más bajo. Si hpa.yaml tuviera maxReplicas: 4 en vez de 6, y generaras la misma carga de este proyecto (que en la ejecución real de esta lección llevó la utilización a 95%-183%), ¿qué número de réplicas esperarías ver en el pico, y por qué?
Ver solución
Esperarías ver REPLICAS: 4 en el pico —el maxReplicas es un techo duro que el HPA nunca cruza, sin importar qué tan por encima del objetivo esté la métrica real—. Incluso si el cálculo matemático del HPA sugiriera que hacen falta más de 4 réplicas para bajar la utilización al objetivo, el HPA se detendría en el máximo declarado, y la utilización de CPU seguiría por encima del 50% objetivo hasta que la carga real bajara por su cuenta — la razón por la que maxReplicas debe elegirse pensando en la capacidad real disponible del clúster, no solo como un número arbitrario.
Resumen y siguiente paso
Este proyecto cerró el Módulo 3 con el sistema completo funcionando junto: generaste carga real con un load-generator de 30 bucles concurrentes, viste el HPA escalar de verdad —de 3 a 6 réplicas, con el evento SuccessfulRescale ... above target como evidencia—, detuviste la carga, y viste el mismo HPA reducir las réplicas de vuelta al mínimo —SuccessfulRescale ... All metrics below target, después de respetar su ventana de estabilización de 60 segundos. El checklist final confirmó las tres piezas de este módulo funcionando sobre el mismo Deployment andes-cargo-status-api y el mismo Service status-api-service desde el Módulo 2: ConfigMap/Secret montados, probes de salud activas, y un HorizontalPodAutoscaler ajustando capacidad solo.
Antes de avanzar deberías poder: explicar por qué el número final de réplicas de un evento de autoscaling es variable, mientras que el mecanismo es literal; leer los Events de un HPA para confirmar la razón exacta de cada decisión de escalado; y reconstruir, de memoria, el ciclo completo de subida y bajada de este proyecto.
Siguiente módulo: redes, Ingress y NetworkPolicy. El Módulo 4 toma exactamente este mismo sistema —configurado, con salud verificada, escalando solo— y lo expone al tráfico externo del clúster con Ingress (reemplazando el kubectl port-forward temporal que usaste desde el Módulo 2), y lo protege con NetworkPolicy, cerrando la delegación textual que cloud-security-and-guardrails-guide le hizo a esta guía.
Recursos
- Kubernetes — Horizontal Pod Autoscaling: Autoscaling on multiple metrics and custom metrics — referencia oficial sobre cómo un
HPApuede escalar por más de una métrica, un caso que este proyecto no cubre pero que la documentación oficial extiende directamente. - Kubernetes — HorizontalPodAutoscaler Walkthrough: Autoscaling Behavior — la sección oficial exacta sobre
stabilizationWindowSecondsy las políticas deScale Up/Scale Downque este proyecto observó en acción. kubernetes-and-eks-in-production-guide(NIEVA), Módulo 2, lección 8 — el proyecto anterior, con el mismoDeployment/Service, del que parte este módulo completo.