A 20-minute hands-on walkthrough. You'll deploy a dummy app, bind it to a LeanNodes flow, scale it to zero, hit its URL from your browser, and watch it cold-start live in the UI.
Prerequisites: a working LeanNodes install (UI reachable, orchestrator
/readyz = 200, Istio meshConfig has the leannodes-authz provider).
If you're not there yet, follow getting-started.md
first.
One flow with one managed Deployment. The simplest possible shape:
user → browser → GET http://demo.your-domain.com/hello
│
▼
Istio ingress calls LeanNodes ext_authz
│
▼
LeanNodes matches host+path to the flow
│
▼
scales `echo` from 0 → 1, waits for Ready
│
▼
returns OK; Envoy routes to the echo pod
│
▼
"hello from leannodes echo"
After 10 minutes of inactivity (shortened to 15s in local dev), the idle reconciler scales echo back to 0. The next request does the whole cycle again.
kubectl apply -f deploy/examples/sample-app.yamlConfirms:
kubectl -n leannodes-demo get deploy echo svc echo
# NAME READY UP-TO-DATE AVAILABLE AGE
# echo 1/1 1 1 5sThis is just a plain hashicorp/http-echo Pod. Nothing LeanNodes-specific.
Pick a hostname that resolves to your ingress. Replace
demo.REPLACE_ME.com throughout this tutorial with your real choice.
# Real cluster: point DNS at your ingress LB first
# (ExternalDNS automates this if you use the annotation in
# deploy/examples/gateway-virtualservice.yaml)
# Local dev: add an /etc/hosts entry
echo "127.0.0.1 demo.REPLACE_ME.com" | sudo tee -a /etc/hosts
# Apply the Gateway + VirtualService
sed 's/demo.REPLACE_ME.com/demo.your-domain.com/g' \
deploy/examples/gateway-virtualservice.yaml | kubectl apply -f -Verify the route exists before moving on:
# Cloud install — hit the ingress directly
curl -v http://demo.your-domain.com/hello
# Expected while LeanNodes has NO flow yet: 200 OK + "hello from leannodes echo"
# (Envoy routes directly to echo; no ext_authz in play because there's
# no AuthorizationPolicy scoped to this host yet.)
# Local dev — port-forward first
kubectl --context kind-leannodes-dev -n istio-system port-forward \
svc/istio-ingressgateway 18443:80 &
curl -v http://demo.REPLACE_ME.com:18443/helloIf this first curl returns 200 OK, the Istio routing is correct and
we're ready to create the flow. If it returns no healthy upstream
or no route matched, fix the Gateway/VirtualService before
continuing — see troubleshooting.md.
Open the LeanNodes UI:
- Cloud:
https://leannodes.your-domain.com - Local dev:
http://localhost:8080
Login. You land on the Dashboard.
Click Flows in the left sidebar → New flow (top-right).
You land in the editor. On the left is the workload sidebar with every managed workload in the cluster, grouped by namespace. On the right is the flow settings panel.
- Name:
demo-hello - Owner namespace:
leannodes-demo - Binding 1 host:
demo.REPLACE_ME.com(your real host) - Binding 1 path:
/*(prefix match for all paths) - Leave the rest at defaults (
windowSeconds: 600,fastHoldSec: 30, etc.)
In the left sidebar, expand the leannodes-demo namespace. Find
echo and drag it onto the canvas in the middle. A single node card
appears labelled echo.
The node properties panel opens on the right — close it (keep
the defaults: targetReplicas: 1, readiness k8s + minStable: 1).
Click Create flow at the top-right.
On success the UI navigates to the flow detail page. The state pill
next to the flow name shows Idle. The "Nodes" table shows echo
at 1/1 ready (because we haven't scaled it down yet). The DAG
visualisation shows one standalone node.
Behind the scenes: the orchestrator wrote a per-flow
AuthorizationPolicy to istio-system, auto-granted you
read+update+delete on this flow (F9), and emitted a flow.created
audit event (bottom of the page).
Inspect what it wrote:
kubectl -n istio-system get authorizationpolicy \
-l leannodes.io/flow-id=<your-flow-id> \
-o yaml | grep -A 10 "rules:"You should see hosts: [demo.REPLACE_ME.com, demo.REPLACE_ME.com:*]
and paths: [/*] with action: CUSTOM + provider leannodes-authz.
kubectl -n leannodes-demo scale deployment echo --replicas=0Back in the UI, wait ~5s then refresh the flow detail page. The
Nodes table shows echo 0/0. The no live pods pill appears
next to the state pill ("state says READY but zero replicas — flow
was scaled down externally"). This is the "cold" state — the flow
definition is still in place, the underlying pod is just off.
After ~15s the idle reconciler notices and transitions the flow
state to IDLE. On a real cluster this takes windowSeconds=600s
(10 min) from the last matching traffic hit; local dev shortens to
15s via the LEANNODES_IDLE_RECONCILE_SEC env var.
Open a second browser tab (keep the flow detail page open in tab 1 so you can watch the state flip).
Go to: http://demo.REPLACE_ME.com/hello
(local dev: http://demo.REPLACE_ME.com:18443/hello)
What happens:
| Time | UI tab | Browser tab |
|---|---|---|
| 0.0s | state pill: Idle | "Connecting…" |
| ~0.1s | state pill flips: Idle → Starting (amber pulsing) | still waiting |
| ~0.2s | "Live cold-start" card appears; echo node status: scaling |
still waiting |
| ~1-3s | Nodes table: echo 0 → 1 desired, then 1/1 ready |
still waiting |
| ~3-5s | +1.5s xDS settle delay (orchestrator waiting for istiod push) | still waiting |
| ~3-5s | state pill: Starting → Ready; "Last cold-start" card populates | renders "hello from leannodes echo" |
First request = HTTP 200. No retry needed, no 503. The whole point of the design.
If you see a 503 no healthy upstream instead, you've hit one of the
bugs we fixed in v1.0.0. Either your image is older than v1.0.0, or
there's a config mismatch — see troubleshooting.md.
Refresh the browser tab. Response is near-instant (<100ms) because LeanNodes's in-memory flow cache hits before any DDB lookup, and the pod is already 1/1 ready.
Same with curl, if you prefer the terminal view:
time curl -s http://demo.REPLACE_ME.com/hello
# hello from leannodes echo
# real 0m0.04sThe difference between 3-5s (cold) and 40ms (warm) is the whole product. Every first-hit pays the cold-start tax; every subsequent hit within the idle window is free.
Stop making requests and wait for windowSeconds to elapse — 10 min
on a real install, 15s on local dev.
In the UI, you'll see:
- State pill: Ready → Draining (brief, ~1s)
- Nodes table:
echo 1 → 0 desired - State pill: Draining → Idle
- Audit log gets a new
flow.drainedevent
Confirm kubectl-side:
kubectl -n leannodes-demo get deploy echo
# echo 0/0 0 0 (back to cold)You can skip the wait by clicking Drain on the flow detail page. Same result, happens in ~2s.
Hit the URL again → the full cold-start loop repeats.
The flow detail page tells the whole story of each cold-start cycle:
- Header state pill — where the flow is right now (Idle / Starting / Ready / Draining / Failed)
- Nodes table — live replica counts + health per managed workload
- Last cold-start card — per-node elapsed times + attempt counts for the most recent DAG run. Greyed out + marked "(history)" when the flow is idle so you don't mistake stale history for current state.
- Recent cold-starts table — last 20 DAG runs (7-day retention), with expandable per-level, per-node detail
- Audit log — who did what: create, update, warm (manual cold-start trigger), drain (manual or idle-driven), pin/unpin, ACL grants (F9)
- Access panel (admins only) — per-flow grants for user/group/role subjects
Nothing else to do — this is LeanNodes in steady state.
# Remove the flow (cascades the AuthorizationPolicy + ACL rows)
curl -X DELETE http://leannodes.your-domain.com/api/v1/flows/<flow-id>
# Remove the Istio Gateway + VirtualService + sample app
kubectl delete -f deploy/examples/gateway-virtualservice.yaml
kubectl delete -f deploy/examples/sample-app.yaml
# Remove /etc/hosts entry (local dev)
sudo sed -i.bak '/demo.REPLACE_ME.com/d' /etc/hosts- How a flow maps traffic (host+path) to a DAG of workloads
- What each of the 5 states means + when transitions happen
- Where to look in the UI for live vs historical state
- How to confirm the plumbing worked (AuthorizationPolicy written,
flow.createdin audit log, xDS-settled Ready)
- Try the diamond DAG:
deploy/examples/flow-diamond-dag.jsonshows parallel fan-out + fan-in across 4 nodes. - Try the External probe:
deploy/examples/flow-with-external.jsonshows gating a managed DAG on a probe-only external dependency. - Read the concepts doc for the full mental model: cold-start lifecycle internals, DDB state layout, RBAC model.
- Instrument alerts: the shipped PrometheusRule covers
LeanNodesOrchestratorDown,ColdStartErrorSpike,LeaderElectionFlapping, etc. Wire them to your Alertmanager via the chart'smonitoring.alerts.runbookBasevalue pointing atdocs/runbook.md.