k9s
Every question kubectl answers, it answers once — you type a command, the server replies, the terminal goes still, and if you want to know whether anything changed you type the same command again. k9s exists to remove that gap. It's a terminal UI that opens a live, continuously-refreshing table onto whatever resource you're looking at, lets you jump between resource types by typing a couple of characters instead of a full command line, and turns describe/logs/exec/edit/delete — the five things you do to a Pod fifty times a day — into single keystrokes fired at whatever row your cursor is sitting on. It is not a separate control plane, a caching proxy, or a privilege of its own: underneath, it opens the identical watch connections and issues the identical authenticated requests kubectl would, against the same kube-apiserver, subject to the same RBAC. This page covers how it's built, the screen you're looking at, the config files you'll actually hand-edit, the keybindings worth committing to muscle memory, where it bites, and how it stacks up against Lens and the Kubernetes Dashboard.
kubectl is like calling a friend on the phone every time you want to know what's in the fridge — you ask, they check, they tell you, then the call ends and you know nothing new until you call again. k9s is like standing in front of the fridge with the door open: you can see everything at once, it updates itself the instant something changes, and you can just reach in and grab something instead of describing what you want over the phone. But it's still the same fridge, with the same lock on it — if you're not allowed to touch the top shelf on the phone, standing in front of the open door doesn't change that. Being able to see something and being allowed to touch it are still two different rules, no matter how you're looking at the fridge.
kubectl speed everywhere else in this course, and k9s is exactly that reflex given a live screen to run on; Foxy brings the other half, because a faster look around a cluster is worth the most in the first sixty seconds after something already looks wrong.What k9s actually is: a watch, not a shortcut around anything
☺ Like you're 10: It's not a secret back door into the cluster — it's the same front door kubectl uses, just left open on your screen instead of closed after every question.
The Kubernetes API & the controller pattern already established that every built-in controller works the same way: open a long-lived GET ?watch=true connection against the objects it cares about, keep a local cache in sync as events stream in, and act on the diff. k9s runs the identical pattern, just in service of a human instead of a reconcile loop — it opens watches (via client-go, the same Go library controllers are built on) against whatever resource list is on screen, and every row updates the instant the API server emits an event for it, with no polling loop and no re-typing kubectl get. That's the entire trick behind "live" — it isn't refreshing faster, it's being told the moment something changes, the same signal a controller reconciles on.
Two consequences follow directly from that, and both matter more than they sound like they should. First, k9s needs nothing installed in the cluster — no Deployment, no Service, no extra attack surface running alongside your workloads, unlike the Kubernetes Dashboard covered later on this page. It reads your kubeconfig exactly like kubectl does and talks straight to kube-apiserver from your own machine. Second, every action you trigger from a row — e to edit, Ctrl-D to delete, s to shell in — becomes a perfectly normal, individually authenticated API request, evaluated against RBAC and admission control exactly like a kubectl invocation would be. k9s has no elevated identity of its own to fall back on; if your RoleBinding doesn't grant delete on Pods, pressing Ctrl-D in k9s fails the exact same way kubectl delete pod would, just inside a UI instead of a shell prompt.
The screen, oriented
☺ Like you're 10: Four zones, always in the same place — where you are, what's in front of you, what you typed, and what your fingers can do about it right now.
Launch k9s with no arguments and it opens on the Pod list for your kubeconfig's current context and namespace — the same defaults a bare kubectl get pods would use. From there, every view on screen is built from the same four regions:
- The header (top) — current context, cluster, user, and a small CPU/memory gauge for the cluster as a whole, so you know which cluster you're looking at before you do anything else to it.
- The crumb trail (just under the header) — a breadcrumb of how you got to the current view (
Pods(checkout), then drill into one and it becomesPods(checkout)›containers), so backing out withEscretraces exactly where you came from. - The resource table (the bulk of the screen) — every row live, every column sortable, filterable with
/the moment you start typing. - The command line and hotkey footer (bottom) —
:opens the command line for typing a resource name to jump to; the footer beneath it lists exactly the keys that do something for the resource type currently on screen — a Pod's footer offersllogs andsshell, a Node's footer doesn't, because neither action means anything for a Node.
That last point is worth sitting with: k9s's keybindings are context-sensitive by design, not one universal cheat sheet. Press ? from any view and it prints the exact, current keybinding list for that resource type — the single most reliable reference, because it can't drift out of date the way a memorized list can once you customize hotkeys.yaml (below) or upgrade versions.
Command mode: how you actually move around
☺ Like you're 10: Instead of clicking through menus, you type a short word and hit Enter — like a search bar that teleports you.
Press :, type a resource name or its short alias, hit Enter, and you're looking at that resource type across the current namespace — no re-running a whole kubectl get line, no re-typing a namespace flag you already set. k9s discovers these names from the same API server discovery endpoint kubectl's own short names come from, so most of what you already type after kubectl get works unmodified after :.
:po jump to Pods :svc jump to Services :deploy jump to Deployments :ns jump to Namespaces (pick one to switch into) :cm jump to ConfigMaps :ctx jump to Contexts (pick one to switch into) :sa jump to ServiceAccounts :events cluster events, newest first :role Roles in this namespace :clusterrole cluster-scoped Roles :pulse cluster health dashboard: resource counts, error rates, sparklines :xray deploy ownership tree for every Deployment — Deployment → ReplicaSet → Pods, live :alias list every resource alias k9s currently knows, including any you added
:xray is worth calling out on its own: it draws the same ownerReferences tree that kubectl's tree krew plugin prints as static text — except live, expandable, and colored by health, so a ReplicaSet stuck at zero-of-three ready Pods is visually obvious rather than something you have to notice in a column of numbers. Once you're inside any list, / opens an inline filter (substring or regex) that narrows the visible rows without leaving the view, Esc clears the filter or steps back one level of the crumb trail, and Enter drills into whatever's selected — Enter on a Deployment shows its Pods, Enter on a Pod shows its containers, one level at a time, exactly mirroring the ownership hierarchy the object model page describes.
Config you actually write
☺ Like you're 10: k9s reads its own small stack of YAML files the way the cluster reads yours — a handful of settings files that shape how the tool behaves, sitting in a folder on your own machine.
k9s keeps its configuration under $XDG_CONFIG_HOME/k9s/ (typically ~/.config/k9s/ on macOS and Linux; older versions used ~/.k9s/ directly). Five files there do almost all of the customizing you'll ever want:
# ~/.config/k9s/config.yaml — top-level behavior
k9s:
refreshRate: 2 # seconds between UI refresh ticks (not the watch itself — that's already live)
readOnly: false # true disables edit/delete/scale/kill actions cluster-wide, client-side
noExitOnCtrlC: true # require :quit or q, so a stray Ctrl-C doesn't drop you out mid-session
ui:
enableMouse: false
skin: dracula # references a file under skins/dracula.yaml
logger:
tail: 200
sinceSeconds: 300
fullScreenLogs: false# ~/.config/k9s/plugins.yaml — shell out to any command, bound to a key, scoped to a resource type
plugins:
all-logs:
shortCut: Shift-L
description: "Logs, all containers, follow"
scopes: ["pods"]
command: kubectl
background: false
args: ["logs", "-f", "$NAME", "-n", "$NAMESPACE", "--context", "$CONTEXT", "--all-containers"]
# ~/.config/k9s/hotkeys.yaml — jump straight to a saved view with one keystroke
hotKeys:
shift-0:
shortCut: Shift-0
description: "Pods in kube-system"
command: pods
namespace: kube-system
# ~/.config/k9s/aliases.yaml — short names of your own, layered on top of the API server's own
alias:
cko: apps/v1/deploymentsPlugins are the one most worth adopting deliberately: a plugin's args gets $NAME, $NAMESPACE, and $CONTEXT substituted from whatever row is selected before the command runs, so a single keystroke can fire off a real, correctly-scoped kubectl invocation (or any other CLI — stern, a curl against a debug sidecar, your own script) without you ever typing the resource name and namespace by hand. It's the same idea as kubectl's own krew plugins, one layer up: instead of extending the CLI itself, you're binding the CLI's own commands to keys inside the UI that already knows which row you're pointing at.
Core keybindings for day-to-day work
☺ Like you're 10: A handful of single letters cover almost everything you'll do to a resource, all day, every day.
d describe the selected resource y view its YAML (read-only) e edit it in $EDITOR, same as `kubectl edit` l tail logs (Pods/containers) s shell in (`exec -it`, Pods/containers) Ctrl-D delete, with a confirm prompt Shift-F port-forward from the selected Pod Enter drill into what's selected / filter the current view Esc clear filter, or back one level : open the command line ? context-sensitive help for this view 0-9 jump to a favorite/recent namespace 0 (on the namespace picker) all namespaces
# a realistic five minutes: chase down one Deployment's Pods, tail logs, shell in, back out $ k9s -n checkout --context prod # launch straight into the namespace and context you need :deploy⏎ # jump to Deployments /checkout-web⏎ # filter to the one that matters ⏎ # drill in — now looking at that Deployment's Pods l # tail logs on the selected Pod Esc s # back out of logs, shell into the same Pod Esc Esc # back out to the Pod list, then the Deployment list :xray deploy⏎ # confirm the ReplicaSet and Pods actually match desired state
Nothing above is a k9s-only capability — every one of those keys fires a request kubectl already knows how to make. What k9s buys you is eliminating the retyping: the resource name, the namespace, and the context are already sitting in the row your cursor is on, so the keystroke is the whole cost. That's the entire value proposition in one sentence, and it's also exactly why k9s can never do anything RBAC wouldn't already let a well-aimed kubectl command do.
Gotchas and failure modes
☺ Like you're 10: Most k9s mistakes look like the tool being smart, when really it's just doing exactly what you told it, fast, in a namespace or cluster you didn't mean to touch.
Ctrl-D is one keystroke away from every row, all the time. The confirm prompt is real, but it's still one accidental keypress plus a reflexive "y" away from a delete — the same speed that makes k9s valuable for reads makes a mistaken write faster too. If you regularly work in k9s against a bastion box other people also use, set readOnly: true in config.yaml or launch with k9s --readonly; it's a client-side guard against your own fat-fingering, not a security boundary — the RBAC gate from earlier on this page is still the only thing that actually stops an unauthorized action, and someone with an unmodified config still deletes exactly what RBAC lets them delete.
Namespace scope hides as much as it shows. k9s opens on one namespace at a time by default, same as kubectl without -A. A cross-namespace problem — a NetworkPolicy blocking traffic between two namespaces, a ResourceQuota fight — can look like nothing's wrong from inside just one of them. Press 0 on the namespace picker (or launch with -A) before concluding a namespace is clean.
A live watch can still go stale. k9s reconnects its watches automatically far more often than a bare kubectl get -w left running would, but a real network partition or an API server restart mid-rolling-upgrade can still leave a view showing a connection-lost banner rather than silently going quiet — kubectl's own page covers the identical underlying watch-reconnection gap; the fix is the same instinct either way: trust a banner or an explicit reconnect over an assumption that "it would say something if it were stale."
Custom hotkeys and plugins can drift from what you remember. Once hotkeys.yaml or plugins.yaml has been edited — by you, six months ago, or by whoever set up the machine you're borrowing — the keybindings on screen are no longer the defaults documented anywhere online. ? from the current view is the one source of truth that can't be stale, because it's printing exactly what's configured on this machine right now.
k9s is a practice-speed and production-operations tool, not an exam-day one. CKA, CKAD, and CKS are performance-based exams delivered in a locked-down, browser-based remote terminal, and the officially provided environment ships a shell with kubectl and a text editor (vim/nano) — candidates cannot install or bring their own additional tooling into that terminal. Build your muscle memory in k9s during study if it helps you move faster, but drill the equivalent raw kubectl commands too, since those are what the proctored terminal actually gives you. Exact permitted tooling, documentation access, and proctoring rules change and are enforced strictly — this course is an independent, unofficial study resource, not affiliated with the CNCF or Linux Foundation, so confirm current exam-environment rules in your Linux Foundation candidate handbook before you sit, not from a study guide.
k9s vs. the alternatives
☺ Like you're 10: Every other option here is a different window onto the same cluster — none of them replace kubectl, and none of them get you past a permission kubectl wouldn't already grant you.
| Option | Model | Best when | Costs you |
|---|---|---|---|
| kubectl | Scriptable CLI, one HTTPS request per invocation | Automation, CI, precise one-off commands, anything that has to be reproducible in a pipeline — and the only option actually present in a proctored exam terminal | No persistent view; every question is a fresh command, retyped namespace and all |
| k9s | Terminal UI, live watches, same client-go/API path as kubectl | Fast interactive triage across many resources — filtering, drilling, logs and exec without retyping selectors each time | Not scriptable; still one human driving it, one cluster/context at a time; nothing installed remotely to help teammates |
| Lens / OpenLens | Desktop GUI, graphical resource browser, built-in metrics and terminal panes | Newcomers, or anyone who wants graphs and a resource tree without memorizing a single flag or key | A heavier client to install and keep patched per machine; less naturally scriptable or shareable over SSH than a terminal tool |
| Kubernetes Dashboard | Web UI, deployed as a workload running inside the cluster itself | A shared, browser-based view for people who won't install a CLI or a desktop app at all | One more thing running in-cluster with its own RBAC surface and ingress exposure to secure carefully — the one option on this table that isn't "nothing extra to run" |
The honest framing is the same one kubectl's own page lands on for Helm and Kustomize: this isn't "kubectl or k9s." k9s has no independent execution path — it is kubectl's own reach, wearing a faster interface, watching the same objects the observability page already told you to instrument, through the same RBAC gate the security page already told you not to trust a UI to bypass.
Foxy: Something in checkout is flapping — I can smell it before the dashboard even loads.
Remy the Rabbit: On it — k9s -n checkout, filter for the Deployment, drill in... three Pods, two Ready, one restarting every ninety seconds.
Foxy: Don't exec in yet. Pull d for describe first — I want the events, not a guess.
Remy the Rabbit: OOMKilled. Memory limit's set way under what this build actually needs.
Gizmo the Gremlin: Easy — just hit Ctrl-D on the limit and remove it entirely. No limit, no more OOMKills, ever. 🤑
Sol the Sloth: That's not fixing it, that's hiding it from the scheduler and letting it eat the node instead. Give me the actual usage numbers and I'll set a real limit.
Timmy the Turtle: And whatever you set — you'll only be able to set it because your RoleBinding already allows patching this Deployment. k9s didn't grant you anything new to get here.
Remy the Rabbit: Fair. e to edit, bump the limit to what Sol says, watch :xray deploy until the ReplicaSet settles. Still faster than four separate kubectl lines.
1. What underlying mechanism does k9s use to keep its resource tables "live," and which existing controllers in this course already use that same mechanism? 2. Why can k9s run with zero components installed inside the cluster, unlike the Kubernetes Dashboard? 3. If your RoleBinding doesn't grant delete on Pods, what happens when you press Ctrl-D on a Pod row in k9s? 4. Name the four regions of the k9s screen and what each one shows. 5. What does :xray deploy draw, and which existing kubectl krew plugin draws the conceptually identical thing as static text? 6. Name three of the five config files under ~/.config/k9s/ and what each one is for. 7. Why can't exam-day muscle memory rely on k9s specifically?
Check your answers
- Kubernetes watches (a long-lived
GET ?watch=true), viaclient-go— the identical mechanism every built-in controller (ReplicaSet's, Deployment's, and any operator's) uses to react to changes without polling, as covered in the API & controller pattern page. - Because k9s talks to
kube-apiserverdirectly from your own machine using your kubeconfig, the same way kubectl does — it needs no Deployment, Service, or in-cluster component of its own, unlike the Dashboard, which runs as a workload inside the cluster and so carries its own RBAC surface and exposure to secure. - The delete request fails the same way
kubectl delete podwould — k9s has no privilege beyond what your identity's RBAC bindings already grant, so a UI keystroke doesn't bypass authorization any more than a CLI command does. - The header (context/cluster/user and cluster-wide resource gauges), the crumb trail (how you navigated to the current view), the resource table (the live, filterable, sortable rows), and the command line/hotkey footer (
:to jump resources, plus the keys that apply to whatever resource type is currently on screen). - The ownership tree built from
ownerReferences— Deployment → ReplicaSet → Pods — drawn live and expandable; kubectl'streekrew plugin (covered on the kubectl page) draws the conceptually identical tree, just as static text from a single command. config.yaml(general behavior — refresh rate, read-only mode, skin),plugins.yaml(bind a key to any shell command, scoped to a resource type, with$NAME/$NAMESPACE/$CONTEXTsubstituted in),hotkeys.yaml(jump straight to a saved view with one keystroke),aliases.yaml(short names of your own layered on the API server's own), andviews.yaml(per-resource saved sort column/order).- Because the officially provided CKA/CKAD/CKS remote exam terminal ships only kubectl and a text editor — candidates cannot install additional tooling into it, so any speed built purely on k9s-specific keystrokes doesn't transfer; the underlying
kubectlcommands it's shelling out to are what the exam terminal actually gives you, and that's exactly what still needs to be fast on its own.