scaledjob-operator is a Kubernetes operator written in Go that watches a custom resource called ScaledJob and automatically creates Kubernetes Jobs in response to Redis queue depth.
When a queue fills up, the operator creates more Jobs to drain it. When the queue empties, it stops creating Jobs and lets the running ones finish. The operator is the only thing that touches the Jobs, you never scale manually.
The goal is to demonstrate a complete, production-shaped operator: a well-defined CRD, a correct reconcile loop, external state reading, and observable status.
This project was set up using Kubebuilder.
In simple terms: you create a ScaledJob, point it at a Redis list, and provide a Kubernetes Job template. The controller reads the queue depth, calculates how many Jobs should be active, creates owned Jobs, and updates status, metrics, and Kubernetes Events.
threshold: 10
minReplicas: 0
maxReplicas: 5With this config:
0 messages -> 0 Jobs
1-10 messages -> 1 Job
11-20 messages -> 2 Jobs
21-30 messages -> 3 Jobs
41+ messages -> 5 Jobs max
Completed and failed Jobs do not count as active. The controller does not delete running Jobs when the queue shrinks; they finish normally.
Prerequisites:
- Docker or another container runtime
- Kind
- kubectl
- redis-cli
Create a cluster and install the CRD:
kind create cluster --name scaledjob-operator-dev
kubectl config use-context kind-scaledjob-operator-dev
make installRun Redis in the cluster:
kubectl create deployment redis --image=redis:7
kubectl expose deployment redis --port=6379
kubectl rollout status deployment/redisForward Redis to your machine. Keep this running:
kubectl port-forward svc/redis 6380:6379Run the operator locally in another terminal:
make runApply the sample:
kubectl apply -k config/samples/Push 30 messages. With threshold: 10 and maxReplicas: 5, the operator targets ceil(30/10) = 3 Jobs:
redis-cli -p 6380 LPUSH demo-queue \
m1 m2 m3 m4 m5 m6 m7 m8 m9 m10 \
m11 m12 m13 m14 m15 m16 m17 m18 m19 m20 \
m21 m22 m23 m24 m25 m26 m27 m28 m29 m30Watch the operator respond (within ~5 seconds):
kubectl get scaledjobskubectl get jobskubectl describe scaledjob scaledjob-sampleNote: each worker pops one message then sleeps ten seconds before completing. The queue does not drain automatically — after all three Jobs finish, the controller reconciles again and creates three more until the queue is empty. To watch the full drain, leave the operator running and monitor with:
watch -n5 'redis-cli -p 6380 LLEN demo-queue && kubectl get scaledjobs && kubectl get jobs'Build and push the controller image:
export IMG=<registry>/scaledjob-operator:<tag>
make docker-build docker-push IMG=$IMGDeploy it:
make deploy IMG=$IMGFor an in-cluster Redis service, use a cluster DNS address in your ScaledJob:
redisAddress: redis.default.svc.cluster.local:6379Do not use localhost in production unless Redis is running in the same container network as the controller. For normal Kubernetes deployments, localhost means the controller pod itself.
Status fields:
status.queueDepthstatus.activeJobsstatus.desiredJobsstatus.lastScaleTimestatus.conditions
Events:
RedisUnreachableQueueConnectedCreatedJobsJobCreateFailed
Metrics:
scaledjob_queue_depthscaledjob_active_jobsscaledjob_desired_jobsscaledjob_reconcile_totalscaledjob_reconcile_errors_total
Run tests:
make testRegenerate manifests after RBAC or API marker changes:
make manifests generateClean up local demo resources:
kubectl delete -k config/samples/
kubectl delete deployment redis
kubectl delete service redis
kind delete cluster --name scaledjob-operator-dev