A live 3D visualization of a Kubernetes cluster, built as an object to look at rather than a tool to use. Point it at any cluster you can read.
The control plane is an ivory octahedron, the worker nodes are hex prisms on a slowly turning ring, and every pod is a small coloured cube on a shell around its node. It carries the visual language of the background animation on luisg.me: warm charcoal, ivory wireframes, one accent red, taxonomy hues, ambient dust.
Every clip below is a real recording of the scene, produced by the same code that runs against the cluster. See regenerating the clips.
Four questions, four channels, and none of them fight: hue is identity, tumble is CPU, size is memory, and distance is pressure.
![]() |
A node's prism breathes at a rate set by its share of allocatable CPU, from about 0.3Hz idle to 1.2Hz saturated. A busy node is visibly working before you click anything. |
![]() |
The prism is a vessel: an inner hex reaches for the rim as the node's memory fills. Linear, because node memory already arrives as a share of allocatable. |
![]() |
The octahedron spins up with CPU and swells its inner solid with memory, and it greys, warms and grows a containment ring on exactly the conditions a worker does. It is a node, not a decoration. |
An HA control plane is drawn in full: each member gets its own octahedron and its own pod shell, held in a tight constellation at the centre so three machines still read as one control plane.
Every Kubernetes lifecycle event has its own animation. There is no shared generic flash.
On desktop a floating panel sits over the left of the scene: node and pod counts with aggregate CPU and memory, every namespace with its colour and pod count, and a short list of what just happened.
The rail is desktop only. On a phone it would cover the thing it describes.
npm install
npm run dev # chaos mode: a synthetic cluster that misbehaves on purposeThen open http://localhost:5173.
Chaos mode is the development environment. A crashloop animation cannot be
iterated on against a cluster that crashloops once a day, so --fake runs a
scripted cluster that produces every event kind on a loop. It speaks the exact
same protocol as the real thing.
To watch a real cluster instead, using whatever context your kubeconfig currently points at:
npm run build
node server/dist/main.jsMouse and trackpad
- Drag to orbit; a quick flick throws it and it coasts to a stop.
- Scroll to zoom. A trackpad pinch (ctrl+wheel) gets a finer step.
- Middle-drag, right-drag or shift-drag to pan.
RorHomerecentres and re-frames.
Touch
- One finger orbits.
- Two fingers pinch to zoom and drag to pan, at the same time.
- Tap a node or pod to select it; a pinch never counts as a tap.
Either way, clicking a node or a pod flies the camera to it and opens its panel, and Escape or a tap on empty space pulls back out. Every automatic motion stops the instant you touch the camera and eases back a couple of seconds after you let go.
The camera frames itself to the viewport, so the whole cluster fits on a phone in portrait as well as on a wide monitor, and re-frames when the device rotates. On narrow screens the info panel becomes a bottom sheet rather than a floating card.
shared/ the wire protocol and the colour palette, imported by both sides
server/ watches the Kubernetes API, derives semantic events, serves the app
web/ three.js scene, animations, camera, the rail and the info panel
deploy/ namespace, read-only RBAC, deployment, service, ingress
The browser never parses Kubernetes. The server translates raw API churn into a
small closed vocabulary of semantic events (pod.oomkilled, node.recovered,
and so on) and the scene maps each one to an animation. Adding an animation is
a new module under web/src/scene/anim/ plus one line in an index.
Every GIF above is produced from the running scene, not drawn by hand.
web/src/showcase.ts holds a registry of clips, each of which builds a
synthetic cluster, freezes everything that is not the subject, and plays one
rule or one animation on a fixed timestep.
npm run dev # the showcase is served at /showcase.html
node scripts/capture.mjs # every clip, or pass ids to capture a fewThe capture script drives a headless browser over CDP, steps each clip one
deterministic frame at a time, screenshots it and hands the frames to ffmpeg.
It needs ffmpeg and a Chromium-family browser; set AURORA_BROWSER to point
at one.
You can also just watch a clip in a normal browser:
http://localhost:5173/showcase.html?clip=pod-oomkilled.
kubectl apply -k deploy/That gives you a namespace, a read-only ClusterRole, a single-replica Deployment, a Service and an Ingress. Two things to change first:
deploy/ingress.yaml— the hostname (aurora.local) and theingressClassName(traefik), to match your cluster.deploy/deployment.yaml— the image, if you would rather build your own than use the published one. The GitHub Actions workflow publishes multi-arch (amd64 and arm64, so it runs on a Raspberry Pi) toghcr.io/<your-org>/auroraon push tomain.
If you run GitOps, point your Kustomization at deploy/ rather than applying
by hand.
Aurora is read-only. Its ClusterRole grants get, list and watch on
nodes, pods, events and kubelet leases, plus reads on metrics.k8s.io. There
are no write verbs and there never will be.
It needs metrics-server for the CPU and memory readings. Without it the scene still runs; nodes and pods simply sit at their idle size and rate.
npm testThe event derivation reducer is where bugs would actually live, so that is where the tests are, along with the pure visual mappings, the namespace palette and the focus set. The scene itself is verified by watching chaos mode.
Apache 2.0. Aurora is read-only by design: it watches a cluster and never changes it.























