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 wget dentro de un único Pod puede agotar sus propios puertos efímeros antes de que decidas detenerlo —vas a ver mensajes como wget: can't connect to remote host: Cannot assign requested address en sus logs si lo dejas correr varios minutos—. Eso no es un problema del Service ni del Deployment: 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
  1. Confirmar la línea base: CPU baja, REPLICAS en el número heredado del Módulo 2 (o el que fuera al momento de crear el HPA).
  2. Aplicar load-generator.yaml, generando 30 bucles concurrentes de tráfico contra /health.
  3. Observar cpu subir muy por encima de 50%, y REPLICAS subir hasta maxReplicas (6) — confirmado con un evento SuccessfulRescale ... reason: cpu resource utilization (percentage of request) above target.
  4. Detener el generador de carga.
  5. Observar cpu bajar casi de inmediato, pero REPLICAS esperar el stabilizationWindowSeconds: 60 antes de reducirse.
  6. Confirmar REPLICAS bajando a minReplicas (2), con un segundo evento SuccessfulRescale ... 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

  1. Kubernetes — Horizontal Pod Autoscaling: Autoscaling on multiple metrics and custom metrics — referencia oficial sobre cómo un HPA puede escalar por más de una métrica, un caso que este proyecto no cubre pero que la documentación oficial extiende directamente.
  2. Kubernetes — HorizontalPodAutoscaler Walkthrough: Autoscaling Behavior — la sección oficial exacta sobre stabilizationWindowSeconds y las políticas de Scale Up/Scale Down que este proyecto observó en acción.
  3. kubernetes-and-eks-in-production-guide (NIEVA), Módulo 2, lección 8 — el proyecto anterior, con el mismo Deployment/Service, del que parte este módulo completo.