DEV Community

Cover image for Hands-On Kubernetes Autoscaling: Building a Local HPA Lab with KinD and Python
Alan Varghese
Alan Varghese

Posted on

Hands-On Kubernetes Autoscaling: Building a Local HPA Lab with KinD and Python

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:

  1. A Python HTTP app runs in a Kubernetes deployment with defined CPU requests (100m) and limits (500m).
  2. An HPA controller targets 50% average CPU utilization across pods.
  3. Metrics Server collects pod resource consumption from the Kubelet.
  4. 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 kind or via mise)
  • kubectl (brew install kubectl or 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"
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Build the image locally:

docker build -t kubernetes-autoscaling:1.0 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create the cluster:

kind create cluster --name autoscaling-demo --config kind-config.yaml
Enter fullscreen mode Exit fullscreen mode

Now load the locally built Docker image into the KinD node:

kind load docker-image kubernetes-autoscaling:1.0 --name autoscaling-demo
Enter fullscreen mode Exit fullscreen mode

💡 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 set imagePullPolicy: IfNotPresent in 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.

  1. Deploy the official Metrics Server:
   kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Enter fullscreen mode Exit fullscreen mode
  1. 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"}
   ]'
Enter fullscreen mode Exit fullscreen mode
  1. Verify that metrics are flowing:
   kubectl rollout status -n kube-system deployment metrics-server
   kubectl top nodes
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Deploy everything to your cluster:

kubectl apply -f k8s/
Enter fullscreen mode Exit fullscreen mode

Check the initial state:

kubectl get hpa autoscaling-app
Enter fullscreen mode Exit fullscreen mode

You should see:

NAME              REFERENCE                    TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
autoscaling-app   Deployment/autoscaling-app   0%/50%    1         5         1          1m
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Terminal 2 (Watch Pods):

kubectl get pods -l app=autoscaling-app -w
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Host Networking vs Container Contexts: When running inside Docker containers or DevContainers, 127.0.0.1 refers to the container itself. Use host.docker.internal along with certSANs in your control plane config to enable seamless cross-environment communication.
  2. 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.
  3. Kubelet TLS in Local Clusters: KinD nodes rely on self-signed certs. Metrics Server requires --kubelet-insecure-tls in local testbeds to successfully pull node metrics.
  4. Readiness Probes Prevent Traffic Blackholes: During sudden scale-up, newly provisioned pods must pass their /health readiness check before receiving ingress traffic from the Service.
  5. 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)

🏁 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)