Kubernetes and KEDA
Phase 3 adds a Kubernetes mode alongside Docker Compose. The same binaries and images run in both environments; only orchestration and the autoscaler actuator change.
How KEDA fits in test-scale
Section titled “How KEDA fits in test-scale”test-scale splits autoscaling into two layers:
| Layer | Responsibility | Docker Compose | Kubernetes |
|---|---|---|---|
| Brain | Decide how many workers are needed (ceil(target_rps / per_worker_rps)) |
Shared | Shared |
| Scaler | Apply that count to the runtime | DockerScaler → docker compose scale |
KedaScaler → Prometheus gauge → KEDA → Deployment |
KEDA does not decide capacity. The control plane Brain computes the number (e.g. 2000 RPS ÷ 500 RPS/worker = 4 workers). KEDA is the Kubernetes actuator that turns that decision into running pods.
Why not CPU-based HPA?
Section titled “Why not CPU-based HPA?”Load-test workers often sit idle, then ramp hard. CPU-based Horizontal Pod Autoscaler reacts to current load, which is too slow for pre-warming workers before a run starts. test-scale pre-scales before dispatch so workers are ready when the run begins.
The KEDA loop (step by step)
Section titled “The KEDA loop (step by step)”sequenceDiagram
participant User
participant CP as Control Plane
participant Brain
participant Gauge as testscale_desired_workers
participant Prom as Prometheus
participant KEDA
participant K8s as Worker Deployment
participant Worker
User->>CP: POST /api/v1/runs
CP->>Brain: PreScale(target_rps=2000)
Brain->>Brain: desired = ceil(2000/500) = 4
Brain->>Gauge: SetDesiredWorkers(4)
Prom->>Gauge: scrape /metrics every 5s
KEDA->>Prom: query testscale_desired_workers
Prom-->>KEDA: value = 4
KEDA->>K8s: scale worker Deployment → 4 replicas
K8s-->>Worker: start pods
Brain->>Prom: poll kube_deployment_status_replicas_available
Brain-->>CP: 4 workers ready
CP->>Worker: NATS start signal
Note over CP,Worker: run executes…
CP->>Brain: Drain()
Brain->>Gauge: SetDesiredWorkers(0)
KEDA->>K8s: scale to minReplicaCount (0)
- Submit run — orchestrator calls
Brain.PreScale(). - Brain computes — e.g. 4 workers for 2000 RPS.
- KedaScaler writes — sets
testscale_desired_workersgauge on the control plane/metricsendpoint. - Prometheus scrapes — ctrl-plane and aggregator targets (Helm configures this).
- KEDA polls Prometheus — ScaledObject runs
query: testscale_desired_workerswiththreshold: "1". - KEDA scales Deployment — with threshold
1, a metric value of4means KEDA targets 4 replicas (ceil(metric / threshold)). - Brain waits — polls
kube_deployment_status_replicas_availableuntil ready count ≥ desired (requires kube-state-metrics in the cluster). - Run dispatches — workers register over NATS and load starts.
- Drain — on completion, gauge →
0; KEDA scales workers to zero aftercooldownPeriod(30s in the Helm chart).
Key objects
Section titled “Key objects”| Object | Role |
|---|---|
ScaledObject |
Links worker Deployment to Prometheus trigger |
testscale_desired_workers |
Gauge the control plane sets; KEDA reads |
kube_deployment_status_replicas_available |
Brain reads to confirm pods are ready |
Prerequisites
Section titled “Prerequisites”- Kubernetes cluster (1.25+)
- KEDA installed (
helm install keda kedacore/keda --namespace keda --create-namespace) - kube-state-metrics (for Brain capacity polling)
- Container images built and pushed (or use
imagePullPolicy: Neverwith local images)
Quick start (kind — local validation)
Section titled “Quick start (kind — local validation)”Your current kubectl context may point at a remote cluster (e.g. AKS). Phase 3
local validation uses kind so you do not need cloud permissions.
# Install kind (macOS)brew install kind
# One-command bring-up + smoke testmake k8s-verify
# Or step by stepmake k8s-up # kind cluster + KEDA + kube-state-metrics + helm installmake k8s-smoke # submit ci-smoke.yaml and wait for completionmake k8s-down # uninstall (DELETE_CLUSTER=1 make k8s-down removes kind cluster)Scripts live in deploy/kubernetes/scripts/ (up.sh, smoke.sh, down.sh).
Install (manual)
Section titled “Install (manual)”# Build images (from repo root)docker build -f deploy/docker/Dockerfile.ctrl-plane -t test-scale:local .docker build -f deploy/docker/Dockerfile.worker -t test-scale:local .docker build -f deploy/docker/Dockerfile.aggregator -t test-scale:local .
# Install the platformhelm install test-scale deploy/kubernetes/helm/test-scale \ --set image.repository=test-scale \ --set image.tag=local \ --set image.pullPolicy=NeverConfigure the control plane
Section titled “Configure the control plane”# test-scale.yaml (or env vars)mode: kubernetes
autoscaler: backend: keda # or k8s — same code path per_worker_rps: 500 prometheus_url: http://test-scale-prometheus:9090 k8s_namespace: default k8s_deployment: test-scale-workerEnvironment equivalents:
AUTOSCALER_BACKEND=kedaPROMETHEUS_URL=http://test-scale-prometheus:9090K8S_DEPLOYMENT=test-scale-workerRun a test
Section titled “Run a test”kubectl port-forward svc/test-scale-ctrl-plane 8080:8080
./test-scale submit -f test-plan.yaml./test-scale status --run-id <slug> --watchVerify KEDA is scaling
Section titled “Verify KEDA is scaling”# Desired count set by control planecurl -s http://localhost:8080/metrics | grep testscale_desired_workers
# KEDA ScaledObject statuskubectl get scaledobject test-scale-worker -o yaml
# Worker podskubectl get pods -l app.kubernetes.io/component=worker -wWhat Phase 3 includes vs. Phase 4
Section titled “What Phase 3 includes vs. Phase 4”| Phase 3 (this) | Phase 4 (later) |
|---|---|
| Helm chart (ctrl-plane, worker, aggregator, NATS, Postgres, Prometheus) | Ingress + cert-manager — see 07-k8s-ingress-tls.md |
| KEDA ScaledObject | Multi-namespace isolation |
| KedaScaler backend | Resource quotas |
/metrics on control plane |
Geo-distributed workers |
| Managed Postgres connection string support | Cluster-level observability |
External access (Ingress + TLS)
Section titled “External access (Ingress + TLS)”For production or kind development without port-forward, see 07-k8s-ingress-tls.md.
Quick start:
make k8s-ingress-upecho '127.0.0.1 test-scale.local' | sudo tee -a /etc/hostsopen https://test-scale.local:30443Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
| Workers stay at 0 | KEDA not installed, or Prometheus not scraping /metrics |
ScaledObject READY=False |
KEDA runs in the keda namespace — use the full Prometheus DNS (test-scale-prometheus.test-scale.svc.cluster.local), not a short name |
insufficient worker capacity |
kube-state-metrics missing; Brain cannot read ready replicas |
| Workers never drain | Drain() not called — check run completion / orchestrator logs |
KEDA shows False activation |
testscale_desired_workers is 0 or not in Prometheus |
See also 04-deployment-models.md for the full Compose vs Kubernetes comparison.