|
| 1 | +# NemoClaw Kubernetes deployment — CPU (HPA, optional) |
| 2 | + |
| 3 | +**Optional pre-GPU chart** — use only if you do not have cluster GPUs yet and want to test agents/HPA against **NVIDIA Inference Hub** (Nemotron Ultra). For the real deployment, use the **[GPU chart](README-gpu.md)**. |
| 4 | + |
| 5 | +Helm chart and scripts for **CPU agent pods** with **HPA**. Each pod uses **1 CPU** and proxies remote inference (no `nvidia.com/gpu`). |
| 6 | + |
| 7 | +**Index:** [deploy/README.md](README.md) · **Models (CPU vs GPU):** [helm/README.md](helm/README.md) |
| 8 | + |
| 9 | +This document is the **CPU deployment guide** — install, operate, and load-test the optional CPU chart. GPU deployment: [README-gpu.md](README-gpu.md). |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Install (CPU only) |
| 14 | + |
| 15 | +You do **not** install the chart, then HPA, then load test as three separate steps. One script installs **metrics-server + Helm chart + CPU HPA** together. |
| 16 | + |
| 17 | +```bash |
| 18 | +cd ~/NemoClaw/deploy/helm/nemoclaw-cpu |
| 19 | +source ~/.nemoclaw/secrets.env # NVIDIA_INFERENCE_HUB_API_KEY=sk-... |
| 20 | + |
| 21 | +./scripts/install-hpa.sh |
| 22 | +``` |
| 23 | + |
| 24 | +Confirm idle state (no workload required): |
| 25 | + |
| 26 | +```bash |
| 27 | +kubectl get hpa -n nemoclaw |
| 28 | +kubectl get pods -n nemoclaw |
| 29 | +``` |
| 30 | + |
| 31 | +Expect **1 Running pod** and HPA **REPLICAS 1** (CPU well below the ~65% target). **Autoscaling only happens under load** — see [Idle vs load test](#idle-vs-load-test). |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## Port-forward |
| 36 | + |
| 37 | +CPU agent listens on **8080** (Service, pod, and local port-forward): |
| 38 | + |
| 39 | +```bash |
| 40 | +kubectl port-forward -n nemoclaw svc/nemoclaw-nemoclaw-cpu-agent 8080:8080 |
| 41 | +``` |
| 42 | + |
| 43 | +**Verify** Inference Hub (needed for chat, not for rollout): |
| 44 | + |
| 45 | +```bash |
| 46 | +curl -s http://127.0.0.1:8080/healthz # always ok if pod is up |
| 47 | +curl -s http://127.0.0.1:8080/readyz # 200 = Hub key OK; 503 = fix secrets |
| 48 | +``` |
| 49 | + |
| 50 | +Override host port: `LOCAL_PORT=8080 ./scripts/install-hpa.sh` (default is 8080). |
| 51 | + |
| 52 | +Optional demo of scale up/down: |
| 53 | + |
| 54 | +```bash |
| 55 | +./scripts/hpa-load-test.sh |
| 56 | +./scripts/hpa-reset.sh # return to idle baseline after the test |
| 57 | +``` |
| 58 | + |
| 59 | + |
| 60 | +<img width="1832" height="1302" alt="Screenshot 2026-05-28 at 12 49 11 PM" src="https://github.com/user-attachments/assets/a91dd398-059a-4e3f-afec-07b2b66b12b0" /> |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +<img width="1926" height="550" alt="Screenshot 2026-05-28 at 12 42 34 PM" src="https://github.com/user-attachments/assets/25b876d5-19a8-4758-8880-5702b38d5f51" /> |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | +If install keeps failing with **rollout failed** or Deployment **`0 up-to-date`**: |
| 71 | + |
| 72 | +```bash |
| 73 | +./scripts/cluster-recover.sh |
| 74 | +``` |
| 75 | + |
| 76 | +Chart details: [helm/nemoclaw-cpu/README.md](helm/nemoclaw-cpu/README.md) · [CPU vs GPU comparison](helm/README.md) |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## Scripts |
| 81 | + |
| 82 | +All scripts live under `deploy/helm/nemoclaw-cpu/scripts/`. Run from the chart directory after sourcing `~/.nemoclaw/secrets.env`. |
| 83 | + |
| 84 | +| Script | What it does | When to use | |
| 85 | +|--------|----------------|-------------| |
| 86 | +| **`install-hpa.sh`** | Enables **metrics-server**; `helm upgrade --install` with **CPU HPA** (`values-step2-hpa.yaml`, min **1** / max **7**); readiness on **`/healthz`** so rollout succeeds without Hub traffic. | **First install** or healthy cluster refresh. | |
| 87 | +| **`hpa-reset.sh`** | Deletes load-test Jobs; force-deletes **pods** and stale ReplicaSets; **keeps HPA + Deployment** by default; `helm upgrade` to idle baseline. | After load test, stuck load-test pods, return to idle. | |
| 88 | +| **`cluster-recover.sh`** | Removes stray resources, **`helm uninstall`**, wipes namespace leftovers, **restarts MicroK8s**, runs **`install-hpa.sh`**. | Repeated **rollout failed**, Deployment **`0 updated \| 0 total`**, ghost HPA. | |
| 89 | +| **`hpa-load-test.sh`** | Switches to **saturate** values (400m CPU/pod); runs in-cluster load Job (~12 min); watches scale up then down. | Prove HPA only — not required for setup. | |
| 90 | +| **`install-performance-hpa.sh`** | Optional Prometheus + adapter + inflight-metric HPA. Heavy; may timeout on small VMs. | Advanced only. | |
| 91 | +| **`hpa-common.sh`** | Shared helpers (not run directly). | — | |
| 92 | + |
| 93 | +### Workflow |
| 94 | + |
| 95 | +```text |
| 96 | +First time: install-hpa.sh |
| 97 | +After load test / tidy: hpa-reset.sh |
| 98 | +Rollout keeps failing: cluster-recover.sh |
| 99 | +Prove autoscaling: hpa-load-test.sh → watch → hpa-reset.sh |
| 100 | +``` |
| 101 | + |
| 102 | +Do **not** run `hpa-reset.sh && install-hpa.sh` unless you used `SKIP_HELM=1` on reset. Reset already runs `helm upgrade`. |
| 103 | + |
| 104 | +### Environment variables |
| 105 | + |
| 106 | +| Variable | Scripts | Effect | |
| 107 | +|----------|---------|--------| |
| 108 | +| `DELETE_DEPLOYMENT=1` | `hpa-reset.sh` | Delete Deployment before reinstall (stuck rollout) | |
| 109 | +| `DELETE_HPA=1` | `hpa-reset.sh` | Delete HPA; two-phase reinstall (`desiredReplicas=0`) | |
| 110 | +| `SKIP_HELM=1` | `hpa-reset.sh` | kubectl cleanup only; then run `install-hpa.sh` | |
| 111 | +| `RUN_LOAD_TEST=1` | `hpa-reset.sh` | Run `hpa-load-test.sh` after reset | |
| 112 | +| `RESTART_MICROK8S=0` | `cluster-recover.sh` | Skip MicroK8s restart (cleanup only) | |
| 113 | +| `RUN_INSTALL=0` | `cluster-recover.sh` | Cleanup without reinstall | |
| 114 | +| `MIN_REPLICAS` / `MAX_REPLICAS` | install, reset | Override HPA bounds (default 1 / 7) | |
| 115 | +| `ROLLOUT_TIMEOUT` | install, reset | Seconds to wait for rollout (default 300) | |
| 116 | +| `LOCAL_PORT` | install | Host port for port-forward hints (default **8080**) | |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Architecture |
| 121 | + |
| 122 | +```text |
| 123 | +┌─────────────────────────────────────────────────────────────────────────┐ |
| 124 | +│ Your VM (Brev) │ |
| 125 | +│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ |
| 126 | +│ │ NemoClaw (fork) │ │ MicroK8s — namespace: nemoclaw │ │ |
| 127 | +│ │ Telegram / OpenShell │ │ agent pods: 1 CPU each (idle) │ │ |
| 128 | +│ │ (not in this chart) │ │ HPA min 1 … max 7 (CPU metrics) │ │ |
| 129 | +│ └──────────┬───────────┘ └──────────────┬───────────────────┘ │ |
| 130 | +│ │ same API key │ metrics-server │ |
| 131 | +└─────────────┼───────────────────────────────────┼────────────────────────┘ |
| 132 | + │ HTTPS │ |
| 133 | + ▼ ▼ |
| 134 | + ┌────────────────────────────────────────────────┐ |
| 135 | + │ NVIDIA Inference Hub (Nemotron Ultra) │ |
| 136 | + └────────────────────────────────────────────────┘ |
| 137 | +``` |
| 138 | + |
| 139 | +HPA scales on **CPU of agent pods** in `nemoclaw`, not on VM Telegram/OpenShell traffic. |
| 140 | + |
| 141 | +--- |
| 142 | + |
| 143 | +## Load balancer |
| 144 | + |
| 145 | +This chart uses a **ClusterIP** Service (`nemoclaw-nemoclaw-cpu-agent`) — **no AWS ALB/NLB** and no Ingress by default. |
| 146 | + |
| 147 | +| Resource | Type | Role | |
| 148 | +|----------|------|------| |
| 149 | +| Agent Service | **ClusterIP** | In-cluster VIP; kube-proxy balances **new connections** across Ready CPU agent pods | |
| 150 | +| `kubectl port-forward` | Local tunnel | Debug on **8080**; not production routing | |
| 151 | +| Load-test Job | In-cluster client | Hits Service DNS → all replicas as HPA scales | |
| 152 | + |
| 153 | +HPA changes replica count; the Service picks up new pods automatically. That is **not** the same as an external load balancer or GPU-aware routing. |
| 154 | + |
| 155 | +**Next step (not implemented):** add **NGINX Ingress** (or AWS Load Balancer Controller on EKS) for external clients and smarter HTTP routing (least-connections, timeouts, rate limits). See [README-gpu.md — Load balancer](README-gpu.md#load-balancer) for the same pattern on GPU (including example **`g6e.12xlarge`** / 4× L40S reference node). |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Prerequisites |
| 160 | + |
| 161 | +| Item | Notes | |
| 162 | +|------|--------| |
| 163 | +| MicroK8s or K8s 1.25+ | `microk8s status --wait-ready` | |
| 164 | +| `helm` 3, `kubectl` | | |
| 165 | +| Inference Hub key | `~/.nemoclaw/secrets.env` | |
| 166 | + |
| 167 | +```bash |
| 168 | +# ~/.nemoclaw/secrets.env |
| 169 | +export NVIDIA_INFERENCE_HUB_API_KEY='sk-...' |
| 170 | +``` |
| 171 | + |
| 172 | +`install-hpa.sh` enables metrics-server on MicroK8s when present. Confirm: |
| 173 | + |
| 174 | +```bash |
| 175 | +kubectl get apiservice v1beta1.metrics.k8s.io |
| 176 | +# AVAILABLE should be True |
| 177 | +``` |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Readiness and Inference Hub |
| 182 | + |
| 183 | +| Probe | Path | Used for | |
| 184 | +|-------|------|----------| |
| 185 | +| Liveness | `/healthz` | Pod restart if process dead | |
| 186 | +| Readiness (default) | `/healthz` | Rollout success (`probes.readinessChecksInferenceHub: false`) | |
| 187 | +| Manual check | `/readyz` | Inference Hub reachable (chat / load test) | |
| 188 | + |
| 189 | +Install succeeds when the agent process is up. **`/readyz` can still return 503** if the API key is wrong — fix secrets and `helm upgrade`, or use: |
| 190 | + |
| 191 | +```bash |
| 192 | +helm upgrade nemoclaw . -n nemoclaw -f values-step2-hpa.yaml \ |
| 193 | + --reuse-values --set inference.apiKey="${NVIDIA_INFERENCE_HUB_API_KEY}" |
| 194 | +``` |
| 195 | + |
| 196 | +To require Hub for readiness (stricter, install may fail without valid key): |
| 197 | + |
| 198 | +```bash |
| 199 | +helm upgrade nemoclaw . -n nemoclaw --reuse-values \ |
| 200 | + --set probes.readinessChecksInferenceHub=true |
| 201 | +``` |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Values overlays |
| 206 | + |
| 207 | +| File | Used by | Purpose | |
| 208 | +|------|---------|---------| |
| 209 | +| `values-step2-hpa.yaml` | `install-hpa.sh`, `hpa-reset.sh` | Idle baseline: 1 CPU/pod, CPU % HPA, `/healthz` readiness | |
| 210 | +| `values-step2-hpa-saturate.yaml` | `hpa-load-test.sh` | 400m CPU request/pod so up to 7 replicas fit on 8 vCPU | |
| 211 | +| `values-step2-hpa-performance.yaml` | `install-performance-hpa.sh` | Scale on `nemoclaw_http_inflight_requests` (Prometheus) | |
| 212 | + |
| 213 | +--- |
| 214 | + |
| 215 | +## Idle vs load test |
| 216 | + |
| 217 | +### Idle (no traffic) |
| 218 | + |
| 219 | +| Resource | Expected | |
| 220 | +|----------|----------| |
| 221 | +| Pods | **1** agent, `Running`, `READY 1/1` | |
| 222 | +| HPA | `MINPODS 1`, `MAXPODS 7`, **`REPLICAS 1`** | |
| 223 | +| TARGETS | `cpu: <low>%/65%` (may show `<unknown>` briefly after install) | |
| 224 | + |
| 225 | +**No scale-up or scale-down** without workload — HPA stays at min replicas. |
| 226 | + |
| 227 | +### Under load (`hpa-load-test.sh`) |
| 228 | + |
| 229 | +Replicas rise toward **7** while CPU is high; after the Job ends, count drifts back to **1** over ~2–8 minutes (scale-down stabilization ~120s). |
| 230 | + |
| 231 | +Watch (use separate commands — some kubectl versions reject `hpa,pods` together): |
| 232 | + |
| 233 | +```bash |
| 234 | +kubectl get hpa -n nemoclaw -w |
| 235 | +kubectl get pods -n nemoclaw -w |
| 236 | +kubectl top pods -n nemoclaw |
| 237 | +less /tmp/nemoclaw-hpa-watch.log |
| 238 | +``` |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Troubleshooting |
| 243 | + |
| 244 | +| Symptom | What to try | |
| 245 | +|---------|-------------| |
| 246 | +| `install-hpa.sh` → **rollout failed** | `./scripts/cluster-recover.sh` | |
| 247 | +| Deployment **`0 up-to-date`**, no ReplicaSet | `./scripts/cluster-recover.sh` (restarts MicroK8s) | |
| 248 | +| HPA **`REPLICAS 0`** / `desiredReplicas=0` | `DELETE_HPA=1 ./scripts/hpa-reset.sh` — never `kubectl scale … --replicas=0` | |
| 249 | +| `/readyz` **503** after install | Fix `NVIDIA_INFERENCE_HUB_API_KEY`; pod can still be Running | |
| 250 | +| Port-forward fails | Check nothing else is bound to local port **8080** | |
| 251 | +| Load-test pods stuck `Terminating` | `./scripts/hpa-reset.sh` | |
| 252 | +| HPA slow to scale down | Normal after load stops | |
| 253 | +| Prometheus install timeout | Use CPU path only (`install-hpa.sh`) | |
| 254 | + |
| 255 | +--- |
| 256 | + |
| 257 | +## Optional: Prometheus performance HPA |
| 258 | + |
| 259 | +Scales on `nemoclaw_http_inflight_requests` instead of CPU. Requires kube-prometheus-stack + prometheus-adapter; **heavy** on small VMs. |
| 260 | + |
| 261 | +```bash |
| 262 | +PROM_HELM_TIMEOUT=35m ./scripts/install-performance-hpa.sh |
| 263 | +``` |
| 264 | + |
| 265 | +See [helm/nemoclaw-cpu/observability.md](helm/nemoclaw-cpu/observability.md). |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## Directory layout |
| 270 | + |
| 271 | +```text |
| 272 | +deploy/ |
| 273 | +├── README-cpu.md ← this file |
| 274 | +├── README-gpu.md ← GPU deployment |
| 275 | +└── helm/ |
| 276 | + ├── README.md ← CPU vs GPU comparison (models, endpoints) |
| 277 | + ├── nemoclaw-cpu/ |
| 278 | + │ ├── README.md |
| 279 | + │ ├── values.yaml |
| 280 | + │ ├── values-step2-hpa.yaml |
| 281 | + │ ├── values-step2-hpa-saturate.yaml |
| 282 | + │ ├── values-step2-hpa-performance.yaml |
| 283 | + │ ├── scripts/ |
| 284 | + │ │ ├── install-hpa.sh ← start here |
| 285 | + │ │ ├── cluster-recover.sh ← rollout / controller stuck |
| 286 | + │ │ ├── hpa-reset.sh |
| 287 | + │ │ ├── hpa-load-test.sh |
| 288 | + │ │ ├── install-performance-hpa.sh |
| 289 | + │ │ └── hpa-common.sh |
| 290 | + │ └── files/ |
| 291 | + └── nemoclaw-gpu/ |
| 292 | +``` |
| 293 | + |
| 294 | +Cursor skill: `.cursor/skills/nemoclaw-k8s-hpa/` |
| 295 | + |
| 296 | +--- |
| 297 | + |
| 298 | +## Uninstall |
| 299 | + |
| 300 | +```bash |
| 301 | +helm uninstall nemoclaw -n nemoclaw |
| 302 | +helm uninstall prometheus-adapter kube-prometheus -n monitoring 2>/dev/null || true |
| 303 | +kubectl delete namespace nemoclaw monitoring --ignore-not-found |
| 304 | +``` |
| 305 | + |
| 306 | +--- |
| 307 | + |
| 308 | +## Further reading |
| 309 | + |
| 310 | +- [README-gpu.md](README-gpu.md) — GPU + Ollama deployment |
| 311 | +- [helm/nemoclaw-cpu/README.md](helm/nemoclaw-cpu/README.md) — chart values, manual helm, load-test tuning |
| 312 | +- [NVIDIA Inference Hub](https://inference-api.nvidia.com) |
| 313 | +- [Kubernetes HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/) |
0 commit comments