Nimbus is a small multi-tenant Kubernetes control plane written in Go.
It does four main things:
- Creates one Kubernetes namespace per tenant and applies resource quotas.
- Watches a
ComputeRequestCRD and creates workloads for it. - Schedules those workloads with a custom bin-packing scheduler.
- Tracks tenant CPU and memory usage for autoscaling and billing.
The repo also includes a React dashboard, Helm chart, Docker setup, and basic Prometheus/Grafana manifests.
- Go
- Kubernetes
- PostgreSQL
- React
- Docker
- Helm
- Prometheus and Grafana
cmd/api/main.go API entrypoint
internal/tenant/ tenant storage and namespace provisioning
internal/controller/ ComputeRequest controller
internal/scheduler/ custom scheduler
internal/autoscaler/ autoscaling loop
internal/billing/ usage collection and invoicing
internal/middleware/ JWT auth and CORS
internal/kubeutil/ shared Kubernetes and metrics helpers
frontend/ React dashboard
k8s/crds/ ComputeRequest CRD
k8s/helm/nimbus/ Helm chart
k8s/monitoring/ Prometheus and Grafana manifests
Tenants are stored in PostgreSQL.
When a tenant is created through the API, Nimbus:
- writes the tenant record to Postgres
- creates a Kubernetes namespace
- applies a
ResourceQuotafor CPU, memory, and pod count
Workloads are defined through a ComputeRequest custom resource.
The controller checks for new requests every 10 seconds. If the matching Deployment does not exist yet, it creates one in the tenant namespace.
Workloads created by the controller use nimbus-scheduler.
The scheduler checks for pending pods every 5 seconds. It scores nodes by live CPU utilization from metrics.k8s.io and picks the busiest schedulable node first. That gives the scheduler a bin-packing bias instead of spreading pods evenly.
The autoscaler checks Nimbus-managed Deployments every 30 seconds.
It compares live CPU and memory usage against the resources requested by the running pods:
- scale up at 70% or higher
- scale down at 30% or lower
- keep replicas between 1 and 5
The billing engine collects live pod CPU and memory usage every 30 seconds.
It stores accumulated tenant usage in memory and also persists usage totals to PostgreSQL. Invoices are generated through the API from the accumulated usage records.
The API uses JWT tokens signed with HS256.
/auth/tokenissues a token- most API routes require
Authorization: Bearer <token> - admin tokens can view all tenants
- non-admin tokens are scoped to their own tenant
- Go 1.21+
- Node.js 18+
- Docker
- kubectl
- kind or another Kubernetes cluster
- PostgreSQL
- metrics-server installed in the cluster
kind create cluster --name nimbus
kubectl apply -f k8s/crds/computerequest.yaml
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlThe metrics server is required for:
- scheduler node scoring
- autoscaling
- billing
If PostgreSQL is already installed locally:
psql postgresql://localhost:5432/postgres -c "CREATE USER nimbus WITH PASSWORD 'nimbus123';"
psql postgresql://localhost:5432/postgres -c "CREATE DATABASE nimbus OWNER nimbus;"Or run it in Docker:
docker run -d \
--name nimbus-postgres \
-e POSTGRES_USER=nimbus \
-e POSTGRES_PASSWORD=nimbus123 \
-e POSTGRES_DB=nimbus \
-p 5432:5432 \
postgres:15go mod tidy
go run cmd/api/main.goThe API reads these environment variables if set:
DB_HOSTDB_PORTDB_NAMEDB_USERDB_PASSWORDDB_SSLMODEJWT_SECRETKUBECONFIG
cd frontend
npm install
npm startFrontend URL:
http://localhost:3000
This is the quickest way to make the dashboard show real activity.
TOKEN=$(curl -s -X POST http://localhost:8080/auth/token \
-H 'Content-Type: application/json' \
-d '{"tenant_id":"admin","tenant_name":"admin"}' | jq -r .token)curl -s -X POST http://localhost:8080/tenants \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"tenant-a","cpu_quota":"500m","memory_quota":"512Mi"}'Verify it:
kubectl get ns nimbus-tenant-a
kubectl get resourcequota -n nimbus-tenant-a
curl -s http://localhost:8080/tenants -H "Authorization: Bearer $TOKEN"kubectl apply -f - <<'EOF'
apiVersion: nimbus.io/v1
kind: ComputeRequest
metadata:
name: web-server
namespace: nimbus-tenant-a
spec:
tenantId: tenant-a
image: nginx
cpu: "200m"
memory: "256Mi"
replicas: 1
EOFVerify it:
kubectl get computerequest -n nimbus-tenant-a
kubectl get deployment -n nimbus-tenant-a
kubectl get pods -n nimbus-tenant-a -o wideExpected result:
- the controller creates
deployment/nimbus-web-server - the pod starts in
nimbus-tenant-a - the pod uses
nimbus-scheduler - the scheduler binds it to a node
You can check the scheduler assignment directly:
kubectl get pod -n nimbus-tenant-a -o jsonpath='{range .items[*]}{.metadata.name}{" scheduler="}{.spec.schedulerName}{" node="}{.spec.nodeName}{"\n"}{end}'Open http://localhost:3000 and click Refresh.
What should update:
Tenants:tenant-aappearsWorkloads: tenant is no longer idleOverview: pod count is above 0Scheduler: scheduled pod, node score, and recent binding appearBilling: usage starts showing after the next billing loop
The billing engine samples usage every 30 seconds.
After about 30 to 60 seconds:
curl -s http://localhost:8080/billing/usage \
-H "Authorization: Bearer $TOKEN"
curl -s http://localhost:8080/billing/invoice/tenant-a \
-H "Authorization: Bearer $TOKEN"Expected result:
CPUMilliSecondsandMemoryMBSecondsstart increasing- invoice totals become non-zero
kubectl get pods -n nimbus-tenant-a -wIf everything is working, the dashboard should show:
Overview:Tenants = 1,Active Tenants = 1,Pods = 1Workloads:tenant-awith1 podScheduler: one scheduled pod and one recent bindingBilling: one usage record and a non-zero invoice after the billing loop runs
If the tenant exists but the pages still show zeros, the usual reason is:
- you created a tenant, but not a workload
A tenant only creates:
- a Postgres record
- a Kubernetes namespace
- a
ResourceQuota
It does not create a pod by itself.
To make Overview, Workloads, Scheduler, and Billing show activity, you need a ComputeRequest.
docker compose up --buildThis starts:
- PostgreSQL
- the Go API
- the React frontend
The compose setup mounts your local kubeconfig into the API container so it can still talk to the cluster.
POST /auth/token
Content-Type: application/jsonExample body:
{
"tenant_id": "admin",
"tenant_name": "admin"
}GET /tenants
POST /tenants
GET /tenants/:id
DELETE /tenants/:id
GET /billing/usage
GET /billing/invoice/:tenant
GET /metrics
apiVersion: nimbus.io/v1
kind: ComputeRequest
metadata:
name: my-service
namespace: nimbus-acme
spec:
tenantId: acme
image: nginx
cpu: "200m"
memory: "256Mi"
replicas: 1Apply it with:
kubectl apply -f my-request.yamlIf you want Prometheus and Grafana locally:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.adminPassword=nimbus123
kubectl apply -f k8s/monitoring/servicemonitor.yaml
kubectl apply -f k8s/monitoring/grafana-dashboard.yamlhelm install nimbus k8s/helm/nimbus
helm upgrade nimbus k8s/helm/nimbus --set image.tag=v2
helm uninstall nimbus