Skip to content

luisg0nc/aurora

Repository files navigation

Aurora

CI Licence

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.

A cluster running: the control plane at the centre, five worker nodes on the ring, pods orbiting each of them

Every clip below is a real recording of the scene, produced by the same code that runs against the cluster. See regenerating the clips.


The visual language

Four questions, four channels, and none of them fight: hue is identity, tumble is CPU, size is memory, and distance is pressure.

Colour is the namespace

Pods in several colours orbiting one node A pod's colour comes from a stable hash of its namespace, so media is always the same hue. The palette holds sixteen swatches, because eight collides once a cluster has a dozen namespaces, and a legend showing two namespaces with the same dot is worse than no legend.

Size is memory

One pod growing as its memory climbs while its neighbour holds still Cube size is the pod's working set, on a log scale. Real pod memory spans three orders of magnitude, so a linear ramp would render everything except the outliers identical. Here one pod climbs from 10Mi to 900Mi while its neighbour holds still.

Tumble is CPU

One pod spinning up as its CPU climbs Tumble rate is CPU, and only CPU. Pods used to carry a random rate multiplier for character, which meant an idle pod could visibly out-spin a busy one. Only the starting angle is random now, so the swarm still looks unsynchronised but two pods spinning alike genuinely use alike.

Distance is pressure

A tightly limited pod drifting outward as it approaches its ceiling How close a pod is to its own limit, max(mem/memLimit, cpu/cpuLimit). Hot pods drift outward, so a straining node reads as a silhouette. This deliberately disagrees with size: a 232Mi pod with a 4Gi ceiling sits calm and close in, while a 103Mi pod limited to 200Mi is the hottest thing on its node. Pods with no limit fall back to absolute size.

Nodes breathe with CPU

One node breathing faster as its CPU climbs 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.

Nodes fill up with memory

Two nodes filling with an inner hex as memory climbs 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 control plane plays the same game

The core octahedron spinning up and swelling as load climbs 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.


Pod events

Every Kubernetes lifecycle event has its own animation. There is no shared generic flash.

A mote leaving the core and blooming into a cube Scheduled. A mote detaches from the control plane, arcs down the link to the target node, and blooms into a cube that spirals into the orbit shell.
A small pod breathing while it pulls its image Pulling. The cube sits small and breathing while the image comes down.
A pod snapping solid and throwing one clean ring Ready. It snaps solid, throws one clean pulse ring, and settles with an elastic overshoot.
A pod collapsing to a point and re-inflating white-hot Restart. Collapses to a point and re-inflates white-hot, its orbit phase jittered.
A pod strobing red arrhythmically and wobbling off its shell Crashloop. An arrhythmic red strobe on a decaying cycle, wobbling off its orbit plane. It persists until the condition clears and overrides everything else: imminent death outranks resource pressure.
A pod imploding and shoving its neighbours outward OOM killed. Implodes to a dense point, then a shockwave ring that physically shoves its neighbours before they ease back.
A pod dissolving into a spray of drifting motes Terminating. Dissolves into a spray of motes drifting outward and fading.
A wireframe ghost arcing between two nodes Migrated. A wireframe ghost arcs across the ring from the old node to the new, and the cube reforms where it lands. The server correlates the two pods by controller, so this is one event rather than an unrelated death and birth.

Node events

A node unfolding as it joins Joined. The prism unfolds as it locks into its slot, and the core pulses.
A node stuttering, greying and sagging, then relighting NotReady, then recovered. It stutters, its link frays and goes dark, it desaturates and sags out of the ring plane, and its pods dim and slow. Recovery sends motes back out along the link and everything relights.
A node warming to amber under memory pressure Pressure. Edges warm to amber and the node breathes deeper, for as long as the pressure holds.
A slow dashed ring rotating around a cordoned node Cordoned. A slow dashed containment ring rotates around the prism.
Motes travelling from nodes inward to the core Heartbeat. Each mote is a kubelet renewing its lease, roughly every ten seconds. Nothing else rides the links at rest, which is the point: a dead kubelet stops renewing immediately, so its link falls silent seconds before the node greys out. The silence is the first warning.

The rail

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.

One namespace staying lit while the rest recede Namespace focus. Hovering a namespace previews it, clicking pins it, and several can be pinned at once. Focus ghosts rather than hides: the other pods shrink and grey but stay where they are, so you keep the cluster's full shape while your namespace lights up inside it. Seeing one namespace spread evenly across every node while another sits entirely on one is the insight, and hiding the rest would throw it away.
The cluster receding into the dark after the server goes away Losing the server. The scene keeps running on its last known state and recedes into the dark. A dropped connection reads as the cluster going quiet, not as an error toast. It reconnects with backoff and animates whatever changed while it was blind.

The rail is desktop only. On a phone it would cover the thing it describes.


Running it

npm install
npm run dev          # chaos mode: a synthetic cluster that misbehaves on purpose

Then 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.js

Interacting

Mouse 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.
  • R or Home recentres 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 ring turning slowly while pods orbit their nodes

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.

How it fits together

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.

Regenerating the clips

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 few

The 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.

Deploying

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 the ingressClassName (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) to ghcr.io/<your-org>/aurora on push to main.

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.

Tests

npm test

The 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.

Licence

Apache 2.0. Aurora is read-only by design: it watches a cluster and never changes it.

About

A live 3D visualization of a Kubernetes cluster, built as an object to look at rather than a tool to use.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages