Module 3: Configuration Secrets Health And Autoscaling
8. Project: `andes-cargo-status-api` under load
Description
This project closes Module 3 with the test all the previous lessons prepared for: you're going to generate real traffic against andes-cargo-status-api, watch lesson 7's HorizontalPodAutoscaler really react — replicas rising while load rises — and then, after stopping the load, watch those same replicas drop on their own, with no one editing deployment.yaml even once. Everything that follows ran against andes-cargo-cluster, with the exact replica count at each point being your own variable value — the mechanism, not the number, is what this project demonstrates.
Connection to the module
Each lesson in this module built one piece: ConfigMap/Secret (lessons 2-4), probes (lessons 5-6), HorizontalPodAutoscaler/metrics-server (lesson 7). This project is the first time you see the third piece working under a real condition — not at rest, like lesson 7 ended — closing the complete arc lesson 1 opened: from "3 replicas running" to a system with externalized configuration, health verification, and capacity that adjusts itself.
Step 1 — Confirm the baseline before generating load
kubectl get hpa -n andes-cargo
What to expect (your resting CPU percentage may vary slightly, but should be well below 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
What to expect (real CPU, very low, at rest):
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
This is your "before" snapshot — keep it in mind to compare against what follows.
Step 2 — The load generator
A single sequential HTTP client doesn't generate enough work to move the CPU needle for a service as lightweight as /health — you need real concurrency. This load generator launches 30 parallel loops inside a single busybox Pod, each one requesting /health nonstop:
# 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
Notice the Service's DNS name (status-api-service.andes-cargo.svc.cluster.local) — the same <service>.<namespace>.svc.cluster.local pattern you already know from Module 2, and in the same andes-cargo namespace as the service itself, so traffic stays entirely inside the cluster.
kubectl apply -f load-generator.yaml
What to expect:
pod/load-generator created
Step 3 — Watch the HPA react: replicas going up
kubectl top pods -n andes-cargo -l app=andes-cargo-status-api
kubectl get hpa -n andes-cargo
What to expect (~10 seconds after starting the load — literal, executed; your CPU numbers and the exact moment it crosses the threshold are going to vary):
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% — well above the target. REPLICAS: 6 — the HPA already scaled to the maximum maxReplicas allows, and you can already count six real Pods with CPU well above their 100m request each. No one edited deployment.yaml: the HPA calculated, on its own, how many replicas were needed to try to bring the average down toward the target, and adjusted replicas directly on the Deployment.
Confirm the exact event that recorded that decision:
kubectl describe hpa andes-cargo-status-api-hpa -n andes-cargo
What to expect (Events fragment, literal — Age is your variable value):
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 — the HPA documents, in readable text, exactly why it made the decision it made. No ambiguity about the cause.
Step 4 — Stop the load
kubectl delete pod load-generator -n andes-cargo --wait=false
What to expect:
pod "load-generator" deleted from andes-cargo namespace
Honest note: a load generator as aggressive as 30 concurrent
wgetloops inside a single Pod can exhaust its own ephemeral ports before you decide to stop it — you're going to see messages likewget: can't connect to remote host: Cannot assign requested addressin its logs if you leave it running for several minutes. That's not a problem with theServiceor theDeployment: it's the generator itself running out of local network resources to open more connections. For this project's purpose — crossing the threshold once and watching the mechanism react — there's no need to sustain the load for more than a minute or two.
Step 5 — Watch the HPA react: replicas going down
CPU drops almost immediately once the load generator stops sending traffic — but the HPA, by design (stabilizationWindowSeconds: 60 in hpa.yaml, lesson 7), waits before reducing replicas, so it doesn't react to a momentary dip:
kubectl get hpa -n andes-cargo
What to expect (right after stopping the load — CPU already dropped, but REPLICAS hasn't yet, because the stabilization window is still running):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 6 2m47s
Wait about a minute (the full stabilizationWindowSeconds: 60 window) and repeat:
kubectl get hpa -n andes-cargo
What to expect (literal, executed — REPLICAS dropped, straight to minReplicas, with no intermediate steps, because lesson 7's Scale Down policy allows reducing up to 100% in a single 15-second cycle once the stabilization window is satisfied):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: 1%/50% 2 6 2 4m47s
Confirm the second event, the exact counterpart of the first:
kubectl describe hpa andes-cargo-status-api-hpa -n andes-cargo
What to expect (Events fragment, 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
Both events, one under the other: New size: 6 when CPU rose, New size: 2 when it dropped and stayed down long enough. That's the complete cycle — up and down, both automatic, both documented by the HPA itself with its exact reason — with you never touching deployment.yaml at any point in this project.
THIS PROJECT'S COMPLETE CYCLE (numbers — yours are going to vary)
at rest high load load stopped settled
REPLICAS: 3 ──▶ REPLICAS: 6 ────────▶ REPLICAS: 6 (waiting) ──▶ REPLICAS: 2
cpu: 1%/50% cpu: 95%/50% cpu: 1%/50% cpu: 1%/50%
"above target" "below target"
(60s stabilization
window before dropping)
Step 6 — The final checklist: this module's complete system
kubectl get all -n andes-cargo
kubectl get configmap,secret -n andes-cargo
kubectl get hpa -n andes-cargo
What to expect (Pod names and the final REPLICAS are your variable value, depending on when you run this relative to the HPA's cycle; object names — Deployment, Service, ConfigMap, Secret, HorizontalPodAutoscaler — are literal):
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
Six object types, one single story: the Deployment Module 2 declared, now with ConfigMap/Secret mounted (lessons 2-4), probes continuously verifying its health (lessons 5-6), and a HorizontalPodAutoscaler deciding, on its own, how many replicas are needed at each moment (lessons 7-8) — the same status-api-service Service as always, with no change, serving a stable front regardless of how many replicas are behind it at any exact moment.
Common mistakes
Expecting an exact replica count at the end of the experiment, instead of the mechanism (expectation, the most important one to prevent in this project). What happens: someone compares their own REPLICAS: 2 (or whatever number) against someone else's, and worries something's wrong if it doesn't match exactly. Why it happens: the rest of this guide has a lot of perfectly literal output, and it's easy to generalize that expectation to this project too. How to spot it: if you compare your final replica count against a specific "expected" value. How to fix it: this guide's honesty rule (named since the design stage) explicitly marks a HorizontalPodAutoscaler event's final replica count as variable — it depends on the real load generated, your machine's speed, and the exact moment you ran each command. What's literal, and what this project demonstrates, is the mechanism: it rises when CPU crosses the threshold, drops when it stays below, both with a reason documented in Events.
Interpreting wget's exhausted-port messages as a Service or Deployment failure (discipline). What happens: someone leaves load-generator running for several minutes, sees error messages in its logs, and starts reviewing service.yaml or deployment.yaml looking for a problem. How to spot it: the exact message (Cannot assign requested address) is a local network error from the generator Pod itself, not an HTTP response from andes-cargo-status-api. How to fix it: as Step 4 warned, a synthetic load generator with too much concurrency can exhaust its own ephemeral ports before the real target (the Service) has any problem — the HPA already served its purpose once you crossed the threshold once, no need to sustain the load indefinitely.
Creating lesson 7's HPA more than once, without noticing (workflow). What happens: someone runs kubectl apply -f hpa.yaml again in this project, expecting it to "restart" the HPA, and instead Kubernetes simply confirms the object already exists with no changes (unchanged, not created). How to spot it: if your kubectl apply -f hpa.yaml's result says configured or unchanged instead of created. How to fix it: there's no need to recreate the HPA for this project — the same object from lesson 7 is still running, continuously evaluating the metric; you only need to generate real load to see it react, not redeclare it.
Exercises
Exercise 1 — Reconstruct the whole cycle from memory. Without going back to the project, draw or describe, in your own words, this project's complete sequence: from the baseline to the final state, including both SuccessfulRescale events.
See solution
- Confirm the baseline: low CPU,
REPLICASat the number inherited from Module 2 (or whatever it was when the HPA was created). - Apply
load-generator.yaml, generating 30 concurrent traffic loops against/health. - Watch
cpurise well above50%, andREPLICASrise up tomaxReplicas(6) — confirmed with aSuccessfulRescale ... reason: cpu resource utilization (percentage of request) above targetevent. - Stop the load generator.
- Watch
cpudrop almost immediately, butREPLICASwait for thestabilizationWindowSeconds: 60before reducing. - Confirm
REPLICASdropping tominReplicas(2), with a secondSuccessfulRescale ... reason: All metrics below targetevent.
Exercise 2 — Explain why REPLICAS dropped straight to 2, with no intermediate steps. With lesson 7's Scale Down policy in mind (Percent: 100, Period: 15 seconds), explain why this project's HPA went from 6 to 2 replicas in a single cycle, instead of dropping gradually.
See solution
The Scale Down policy allows reducing up to 100% of current replicas in a 15-second period — meaning there's no limit forcing a gradual reduction, beyond the 60-second Stabilization Window that must be satisfied before reducing at all. Once that window was satisfied, and with metrics already well below the target (cpu: 1%/50%), the HPA calculated that the correct replica count for that CPU level was the allowed minimum (minReplicas: 2), and applied that change all at once, with no artificial intermediate steps.
Exercise 3 — Design an experiment with a lower maxReplicas. If hpa.yaml had maxReplicas: 4 instead of 6, and you generated this project's same load (which in this lesson's actual run pushed utilization to 95%-183%), what replica count would you expect to see at the peak, and why?
See solution
You'd expect to see REPLICAS: 4 at the peak — maxReplicas is a hard ceiling the HPA never crosses, no matter how far above target the real metric is. Even if the HPA's math suggested more than 4 replicas were needed to bring utilization down to target, the HPA would stop at the declared maximum, and CPU utilization would stay above the 50% target until the real load dropped on its own — the reason maxReplicas should be chosen with the cluster's real available capacity in mind, not just as an arbitrary number.
Summary and next step
This project closed Module 3 with the complete system working together: you generated real load with a 30-concurrent-loop load-generator, watched the HPA really scale — from 3 to 6 replicas, with the SuccessfulRescale ... above target event as evidence — stopped the load, and watched that same HPA reduce replicas back to the minimum — SuccessfulRescale ... All metrics below target, after respecting its 60-second stabilization window. The final checklist confirmed this module's three pieces working on the same andes-cargo-status-api Deployment and status-api-service Service from Module 2: ConfigMap/Secret mounted, health probes active, and a HorizontalPodAutoscaler adjusting capacity on its own.
Before moving on you should be able to: explain why an autoscaling event's final replica count is variable, while the mechanism is literal; read an HPA's Events to confirm the exact reason for each scaling decision; and reconstruct, from memory, this project's complete up-and-down cycle.
Next module: networking, Ingress, and NetworkPolicy. Module 4 takes exactly this same system — configured, with health verified, scaling on its own — and exposes it to the cluster's external traffic with Ingress (replacing the temporary kubectl port-forward you've used since Module 2), and protects it with NetworkPolicy, closing the textual delegation cloud-security-and-guardrails-guide made to this guide.
Resources
- Kubernetes — Horizontal Pod Autoscaling: Autoscaling on multiple metrics and custom metrics — official reference on how an HPA can scale on more than one metric, a case this project doesn't cover but the official documentation extends directly.
- Kubernetes — HorizontalPodAutoscaler Walkthrough: Autoscaling Behavior — the exact official section on
stabilizationWindowSecondsand theScale Up/Scale Downpolicies this project watched in action. kubernetes-and-eks-in-production-guide(NIEVA), Module 2, lesson 8 — the previous project, with the sameDeployment/Service, this whole module builds on.