Have you ever wanted to experiment with Kubernetes Horizontal Pod Autoscaler (HPA) without spinning up costly cloud clusters on AWS, GCP, or Azure?
Testing autoscaling locally in your homelab or dev environment is one of the most effective ways to master Kubernetes workload mechanics. But when you try setting up HPA locally using KinD (Kubernetes in Docker), you quickly discover that it is not just about applying an HPA YAML file. You have to wire up metrics collection, handle container-to-host networking, configure CPU requests accurately, and navigate self-signed certificates.
In this walkthrough, I will take you through building a complete, local autoscaling lab from scratch—from writing a lightweight Python web service to triggering real-time pod scaling under simulated traffic surges.
🏛️ Architecture at a Glance
Here is how the components interact inside our local KinD cluster:
flowchart TD
subgraph Traffic["Workload Ingress"]
Generator["Load Generator Pod (BusyBox)"]
end
subgraph Cluster["KinD Cluster (Docker)"]
SVC["Service: autoscaling-service (ClusterIP: 80 -> 8000)"]
subgraph Workload["Deployment: autoscaling-app"]
P1["Pod 1 (Base)"]
P2["Pod 2 (Scaled)"]
PN["Pod ... (Up to 5)"]
end
subgraph MetricsControl["Metrics & Autoscaling Pipeline"]
Kubelet["Kubelet Summary API"]
MS["Metrics Server\n(--kubelet-insecure-tls)"]
HPA["HPA Controller\n(Target: 50% CPU)"]
end
end
Generator -->|HTTP Flood| SVC
SVC --> P1
SVC -.-> P2
SVC -.-> PN
Kubelet -->|Scrapes CPU/Mem| MS
MS -->|Custom Metrics API| HPA
HPA -->|Adjust Replicas: 1 ➔ 5| Workload
The Workflow:
- A Python HTTP app runs in a Kubernetes deployment with defined CPU requests (
100m) and limits (500m). - An HPA controller targets
50%average CPU utilization across pods. - Metrics Server collects pod resource consumption from the Kubelet.
- When we simulate traffic using a load generator pod, the CPU threshold is breached, and Kubernetes automatically provisions up to 5 replicas. Once load drops, the cluster safely scales back down to 1 replica.
🛠️ Prerequisites & Local Tooling
To follow along, make sure you have:
- Docker Desktop (or Docker Engine)
-
KinD (
brew install kindor via mise) -
kubectl (
brew install kubectlor via mise)
(Optional) If you use mise or VS Code Dev Containers, you can manage these CLI tools declaratively with a mise.toml configuration:
[tools]
docker-cli = "latest"
kind = "latest"
kubectl = "latest"
python = "latest"
Step 1: Writing the Python Microservice
We keep the application clean and lightweight using Python's built-in http.server. It exposes a root route for service requests and a /health endpoint for Kubernetes health probes.
app.py:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
return
self.send_response(200)
self.end_headers()
self.wfile.write(b"Kubernetes Autoscaling App")
def log_message(self, format, *args):
return
server = HTTPServer(("0.0.0.0", 8000), Handler)
server.serve_forever()
Next, wrap it in a slim Docker image using python:3.12-slim:
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
Build the image locally:
docker build -t kubernetes-autoscaling:1.0 .
Step 2: Creating the KinD Cluster (with DevContainer / Host Networking Support)
If you work inside Dev Containers or DevPod, running kubectl from inside a container to reach a KinD cluster running on the host requires special attention: 127.0.0.1 inside a container refers to the container itself, not the host machine!
To fix this, we configure certSANs in kind-config.yaml to include host.docker.internal:
kind-config.yaml:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
certSANs:
- host.docker.internal
nodes:
- role: control-plane
Create the cluster:
kind create cluster --name autoscaling-demo --config kind-config.yaml
Now load the locally built Docker image into the KinD node:
kind load docker-image kubernetes-autoscaling:1.0 --name autoscaling-demo
💡 Tip: In local KinD setups, Kubernetes cannot pull images from your host's local Docker daemon unless you explicitly load them using
kind load docker-image. Also setimagePullPolicy: IfNotPresentin your deployment manifest.
Step 3: Deploying and Patching Metrics Server
By default, Kubernetes HPA cannot autoscale without Metrics Server.
In production, Kubelets have valid certificates. In KinD, Kubelet serving certificates are self-signed, causing Metrics Server to throw certificate verification errors.
- Deploy the official Metrics Server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
- Patch Metrics Server with
--kubelet-insecure-tls:
kubectl patch -n kube-system deployment metrics-server --type='json' -p='[
{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}
]'
- Verify that metrics are flowing:
kubectl rollout status -n kube-system deployment metrics-server
kubectl top nodes
Once kubectl top nodes outputs CPU and memory metrics without errors, your metrics pipeline is operational.
Step 4: Crafting the Kubernetes Manifests
1. The Deployment (k8s/deployment.yaml)
Notice the resources.requests.cpu: "100m" specification. This is critical. HPA calculates CPU utilization percentage as:
$$\text{Utilization} = \frac{\text{Current CPU Usage}}{\text{Requested CPU}} \times 100$$
Without a defined CPU request, HPA cannot compute percentage-based targets and will report <unknown>/50%.
apiVersion: apps/v1
kind: Deployment
metadata:
name: autoscaling-app
spec:
replicas: 1
selector:
matchLabels:
app: autoscaling-app
template:
metadata:
labels:
app: autoscaling-app
spec:
containers:
- name: autoscaling-app
image: kubernetes-autoscaling:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
resources:
requests:
cpu: "100m"
limits:
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
2. The Service (k8s/service.yaml)
A ClusterIP service provides a steady, stable DNS entry point (autoscaling-service) regardless of how many pods scale up or down:
apiVersion: v1
kind: Service
metadata:
name: autoscaling-service
spec:
selector:
app: autoscaling-app
ports:
- port: 80
targetPort: 8000
type: ClusterIP
3. The HorizontalPodAutoscaler (k8s/hpa.yaml)
We configure scaling boundaries between 1 and 5 replicas, targeting 50% average CPU utilization:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: autoscaling-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: autoscaling-app
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
Deploy everything to your cluster:
kubectl apply -f k8s/
Check the initial state:
kubectl get hpa autoscaling-app
You should see:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
autoscaling-app Deployment/autoscaling-app 0%/50% 1 5 1 1m
Step 5: Putting the Autoscaler to the Test
Now comes the fun part: simulating high traffic load and watching Kubernetes respond in real time.
1. Watch HPA and Pods
Open two separate terminal windows or split panes:
Terminal 1 (Watch HPA):
kubectl get hpa autoscaling-app -w
Terminal 2 (Watch Pods):
kubectl get pods -l app=autoscaling-app -w
2. Trigger the Load Generator
In a third terminal, run an ephemeral BusyBox pod that hammers the service with continuous HTTP requests:
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while true; do wget -q -O- http://autoscaling-service; done"
3. The Scale-Up Reaction
Within 1–2 minutes, watch the CPU utilization spike well above the 50% target:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
autoscaling-app Deployment/autoscaling-app 0%/50% 1 5 1 2m
autoscaling-app Deployment/autoscaling-app 78%/50% 1 5 2 3m
autoscaling-app Deployment/autoscaling-app 134%/50% 1 5 4 4m
autoscaling-app Deployment/autoscaling-app 55%/50% 1 5 5 5m
Kubernetes automatically scales the deployment to maximum capacity (5 pods) to absorb the incoming flood.
4. The Scale-Down Cooldown
Terminate the load generator by pressing Ctrl + C.
Kubernetes uses a default stabilization window (typically 5 minutes) before scaling down to prevent pod "flapping" (rapid oscillation between scale-up and scale-down). Once the cooldown elapses and CPU utilization drops to 0%, HPA smoothly brings replica count back to 1.
🧠 Key Takeaways & Troubleshooting Wisdom
Setting up this lab highlights several foundational concepts every Kubernetes practitioner should master:
-
Host Networking vs Container Contexts: When running inside Docker containers or DevContainers,
127.0.0.1refers to the container itself. Usehost.docker.internalalong withcertSANsin your control plane config to enable seamless cross-environment communication. -
CPU Requests are Not Optional for HPA: If you don't define
resources.requests.cpu, HPA cannot calculate percentage-based utilization and will stall in an<unknown>status. -
Kubelet TLS in Local Clusters: KinD nodes rely on self-signed certs. Metrics Server requires
--kubelet-insecure-tlsin local testbeds to successfully pull node metrics. -
Readiness Probes Prevent Traffic Blackholes: During sudden scale-up, newly provisioned pods must pass their
/healthreadiness check before receiving ingress traffic from the Service. -
The 7-Layer Kubernetes Debugging Workflow: Whenever autoscaling behaves unexpectedly, check in order:
- Pod Status (
kubectl get pods) - Deployment Status (
kubectl get deployment) - Service & Endpoints (
kubectl get endpoints) - Metrics Server Health (
kubectl top pods, nodes) - HPA Status & Events (
kubectl describe hpa) - Cluster Events (
kubectl get events --sort-by='.metadata.creationTimestamp') - Pod Logs (
kubectl logs)
- Pod Status (
🏁 Conclusion
Building an autoscaling environment on your local machine gives you deep visibility into how Kubernetes orchestrates scaling events, monitors workloads, and manages service discovery—all without cloud provider costs or latency.
Try tweaking the HPA algorithm with custom metrics, memory thresholds, or scaling behaviors next!
Have questions or run into issues setting up your own KinD HPA lab? Drop a comment below and let's discuss!
Top comments (0)