A small Kubernetes demo on Go: Envoy Gateway accepts traffic, gateway calls worker, worker does CPU-bound work, and Prometheus with Grafana show what's happening under load.
No databases, queues, or extra glue code. The point of the project is different: spin up a cluster quickly, fire off requests, watch the metrics, and see how HPA starts adjusting the replica count.
apps/gateway- HTTP facade that acceptsGET /api/workand proxies the request to worker.apps/worker- service with a heavyGET /work, deliberately designed to load the CPU.scripts/loadtest- a simple load generator for the demo stand.pkg- shared code for configuration, health checks, and Prometheus metrics.charts- Helm chart to deploy the entire stand to Kubernetes.
Interactive version:
presets/architecture.html- open locally in a browser.
Request flow:
- an external request arrives at
Envoy Gateway; HTTPRouteforwards it togateway-service;gatewaycallsworker-service;workerdoes the work and returns a response with the pod name;Prometheusscrapes metrics,Grafanadraws charts,HPAwatches CPU and scales the worker.
gateway is a plain HTTP server on :8080. It exposes:
/healthz/readyz/metrics/api/work
The idea is simple: gateway doesn't do heavy work itself - it just forwards the request and appends its own pod name to the response.
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, cfg.WorkerURL+"/work", nil)
if err != nil {
http.Error(w, "Failed to build worker request", http.StatusInternalServerError)
return
}worker also starts an HTTP server and exposes:
/healthz/readyz/metrics/work
The most important endpoint is /work. It intentionally loads the CPU, then waits briefly and returns the pod name:
for i := 0; i < 5_000_000; i++ {
if i%100_000 == 0 {
select {
case <-ctx.Done():
http.Error(w, "request canceled", http.StatusRequestTimeout)
return
default:
}
}
_ = i * i
}
timer := time.NewTimer(20 * time.Millisecond)
defer timer.Stop()This is exactly what's needed for the demo:
- as concurrency increases, CPU starts rising;
- latency climbs as well;
HPAsees the load and adds replicas;- responses show which pod actually handled the request.
loadtest is a small separate container that hammers the target URL and logs progress.
It's useful when you need to check:
- how
workerbehaves under parallel requests; - how metrics react in
Prometheus; - how quickly
HPAkicks in.
pkg contains a shared metrics layer. It registers:
http_requests_totalhttp_request_duration_seconds- standard process/go collectors
This makes it convenient to use in Grafana and alerts without extra code in each service.
Useful PromQL queries to explore in Grafana:
rate(process_cpu_seconds_total{namespace="default"}[5m])
sum(rate(http_requests_total{namespace="default", app="worker"}[5m])) by (status)
histogram_quantile(
0.95,
sum(rate(http_request_duration_seconds_bucket{namespace="default", app="worker"}[5m])) by (le)
)
The chart already includes basic alerts:
- high CPU;
- high memory;
- high error rate;
- high latency;
- pod not ready.
flowchart LR
Load[Load test] --> W[Worker]
W -->|http_requests_total| Prom[Prometheus]
W -->|http_request_duration_seconds| Prom
Prom --> Graf[Grafana]
Prom --> HPA[HPA]
HPA --> W
In this project the load is not decorative:
workerruns millions of operations in a loop;loadtestfires parallel requests;HPAscalesworkerby CPU with a50%target, from2to8replicas;- Grafana shows rising
CPU,RPS, andp95 latency.
If you watch it as a live demo, the picture looks like this:
- Start
loadtest. - CPU on
workerrises almost immediately. - Latency starts growing along with the request queue.
HPAadds new replicas.curlresponses show different pod names.
The easiest way to bring everything up is via Taskfile:
task upWhat task up does:
- builds
gatewayandworkerimages; - loads them into
k3d; - deploys
Envoy Gateway; - installs
kube-prometheus-stack; - deploys the chart;
- shows pod and service status.
If you prefer to do it manually, the order is:
task cluster
task init:envoy
task init:monitoring
task build
task load
task deploytask status
task grafana
task ui
task downtask status- shows pods and services;task grafana- opens a port-forward to Grafana;task ui- opensk9sin thedefaultnamespace;task down- removes the Helm release and the k3d cluster.
After deployment the cluster listens on localhost:8080, so you can hit the gateway without any extra port-forwarding:
curl http://localhost:8080/api/work
curl http://localhost:8080/metrics | headA typical response looks like:
Gateway [Pod: gateway-7c4d8f5b9c-xyz12] got work: worker-6d7f9c8b4f-abcd3
This is handy when you want to see at a glance that requests are already being spread across different pods.
The repository includes a ready-made task load-test, but it's more of a short smoke test. If you need visible CPU growth and autoscaling, let the test run for minutes, not seconds.
Direct run against worker:
kubectl run go-worker-loadtest --rm -i --restart=Never \
--image loadtest:v1 \
--env TARGET_URL=http://worker-service.default.svc.cluster.local:8080/work \
--env DURATION=3m \
--env CONCURRENCY=100 \
--env REQUEST_TIMEOUT=30s \
--env REPORT_EVERY=10sRun through gateway:
kubectl run go-gateway-loadtest --rm -i --restart=Never \
--image loadtest:v1 \
--env TARGET_URL=http://gateway-service.default.svc.cluster.local:8080/api/work \
--env DURATION=3m \
--env CONCURRENCY=100 \
--env REQUEST_TIMEOUT=30s \
--env REPORT_EVERY=10sFor a clean HPA test, hit worker directly. To verify the full request path, run through gateway.
Things worth watching during the run:
kubectl get hpa;- Grafana application dashboard;
- Grafana cluster dashboard;
Prometheustargets and alerts.
PORTWORKER_URLREQUEST_TIMEOUTREAD_TIMEOUTWRITE_TIMEOUTIDLE_TIMEOUTSHUTDOWN_TIMEOUT
PORTREAD_TIMEOUTWRITE_TIMEOUTIDLE_TIMEOUTSHUTDOWN_TIMEOUT
TARGET_URLDURATIONCONCURRENCYREQUEST_TIMEOUTREPORT_EVERY
Images are built separately because gateway and worker use the shared pkg as an additional build context.
docker build -t gateway:v1 --build-context pkg=pkg ./apps/gateway
docker build -t worker:v1 --build-context pkg=pkg ./apps/worker
docker build -t loadtest:v1 ./scripts/loadtestcharts/values.yaml defaults to:
gateway:v1worker:v1loadtest:v1
imagePullPolicy: Never is set, so images must be loaded into the cluster before deploying to k3d.
apps/
gateway/
worker/
pkg/
scripts/
loadtest/
charts/
Taskfile.yaml
readyzis currently a stub and always returns200 OK; in a real project you would typically add a dependency check here.HPAwatches CPU, not RPS, so CPU load is what matters for the autoscaling demo.- The chart uses
Envoy Gateway, not a regular Ingress.
