Tools Used in Kubernetes · kubectl

kubectl

kubectl is the command-line client that talks to kube-apiserver on your behalf — every other page in this course has already shown you dozens of its commands in passing, because there's no way to touch a real cluster without it. This page is the tool guide those pages point back to: the three genuinely different ways kubectl lets you manage an object (and why mixing them causes real incidents), the --dry-run=client -o yaml trick that turns any one-liner into a starting manifest, how a kubeconfig's contexts and namespaces decide which cluster your next command actually hits, kubectl explain as a live, self-updating reference for any resource including your own CRDs, and how to extend the CLI itself with krew plugins. It closes with the gotchas that show up once kubectl stops being a toy on a laptop and starts being the thing three different people and a CI pipeline all point at the same production cluster with.

☺ Explain it like I'm 10

kubectl is a universal remote control, and the cluster is the TV. You press a button — "channel up," "volume 5," "turn on subtitles" — and the remote translates that into an infrared signal the TV already knows how to read. The remote doesn't have a screen of its own and doesn't store anything; it's just very good at turning "what you want" into the exact signal the TV expects. The tricky part is that a universal remote can be paired with more than one TV, and if you're not looking at which one it's currently paired with, pressing "power off" hits whichever set is currently selected — which is exactly why this page spends real time on contexts, not just buttons.

🐰Your host for this topic: Remy the Rabbit — all reflex, a command out before you've finished asking. Nobody on the Squad drills raw kubectl speed the way Remy does, which makes the CLI itself Remy's department.

What kubectl actually is

☺ Like you're 10: It's not magic and it's not special — it reads a config file to find your cluster, turns what you typed into a web request, and prints back whatever the server says.

The Kubernetes API & the controller pattern already covers this in depth, so only the short version belongs here: kubectl reads your kubeconfig (usually ~/.kube/config) to find a cluster's address and your credentials, turns the verb and resource you typed into an HTTPS request against kube-apiserver, and pretty-prints whatever JSON comes back. It carries no special privilege of its own — a curl request with the same bearer token gets treated identically by the same RBAC and admission checks. That means everything below is really about one thing: how precisely you can control which request kubectl sends, and to which cluster.

Three ways to manage an object — and why mixing them bites

☺ Like you're 10: You can tell kubectl exactly what to do right now, hand it a whole file to swap in, or hand it a file and let it figure out only what changed — and those three are not interchangeable habits.

Kubernetes documentation draws this distinction formally, and it's worth taking seriously rather than treating all three as "ways to run kubectl":

apply's trick is a three-way merge. It doesn't just diff your file against the live object — it also reads a third source: the kubectl.kubernetes.io/last-applied-configuration annotation, a JSON snapshot of the file you applied last time, which apply silently writes onto the object as it goes. Comparing all three lets it answer a much more precise question than replace ever can: "did this specific field actually change in what I'm managing, or did something else change it out from under me?"

# apply preserves fields it didn't tell it to change
$ kubectl apply -f deployment.yaml                    # replicas: 3, last-applied now says 3
$ kubectl scale deployment/web --replicas=7            # something else (you, an HPA) bumps live to 7
$ kubectl apply -f deployment.yaml                     # file still says 3, unchanged since last-applied
# → apply sees file == last-applied for replicas, so it leaves the LIVE value (7) alone

# kubectl diff shows you the same three-way comparison apply would act on, before it acts
$ kubectl diff -f deployment.yaml
⚠ Watch out

That preservation only works because the file's replicas field didn't change relative to last-applied — it is not a promise that apply will always leave a field alone. If a Deployment is under an HPA, the standard practice is to omit spec.replicas from the manifest entirely, so there's nothing for a future edit to that field in the file to ever fight the autoscaler over. And kubectl replace -f gives you none of this nuance at all — it's a full PUT, so if someone scaled the Deployment live and you then replace it with a file that still says replicas: 3, the live count silently drops back to 3, no merge, no mercy.

The other trap is switching styles mid-object. An object created with kubectl create or kubectl run has no last-applied-configuration annotation, because nothing declarative ever touched it. The first kubectl apply -f against that object prints a warning and falls back to treating the live object as if last-applied were empty — safe in the common case, but the honest fix is to pick one management style per object and stay in it, which is exactly why this course's own manifests are all written and re-applied with apply, never mixed with imperative edits in between.

Imperative commands kubectl scale / expose / set image your typed command no file, anywhere Live object one field patched directly Imperative config kubectl create/replace -f file.yaml file.yaml whole-object overwrite Live object fully replaced, no merge Declarative config kubectl apply -f file.yaml file.yaml last-applied annot. live object three-way merge Live object only changed fields patched

--dry-run=client -o yaml: your manifest generator

☺ Like you're 10: Type the quick imperative command you already know, add two flags, and kubectl hands you back a real starter YAML file instead of actually doing anything.

Every imperative kubectl create and kubectl expose subcommand builds a full object in memory before it ever sends it anywhere — --dry-run=client stops it right there and -o yaml prints the object instead of submitting it. The result is a syntactically correct, sensibly defaulted manifest for exactly the object you described, in seconds, which beats hand-typing Deployment boilerplate from memory or a half-remembered example every time.

# scaffold a Deployment, edit the result, then apply it for real
$ kubectl create deployment web --image=nginx:1.27 --replicas=3 --dry-run=client -o yaml > deployment.yaml

# same trick for a Service, a ConfigMap, and a Job
$ kubectl create service clusterip web --tcp=80:8080 --dry-run=client -o yaml > service.yaml
$ kubectl create configmap web-config --from-literal=LOG_LEVEL=info --dry-run=client -o yaml > configmap.yaml
$ kubectl expose deployment web --port=80 --target-port=8080 --dry-run=client -o yaml

$ kubectl apply -f deployment.yaml -f service.yaml -f configmap.yaml

--dry-run=client never leaves your machine — it doesn't even need a reachable cluster. --dry-run=server is the stricter sibling: kubectl submits the request to the real API server, which runs it through defaulting, validation, and every admission webhook that would normally fire, and then reports what would happen — without persisting it. That catches things client-side dry-run structurally can't, like a validating webhook rejecting the object or a mutating webhook injecting a sidecar, which is exactly why kubectl apply -f manifest.yaml --dry-run=server earns a spot as a pre-flight check in CI, ahead of the real apply.

◆ Key idea

Client-side dry-run answers "is this syntactically a valid object." Server-side dry-run answers "would the cluster, with its actual webhooks and policies, actually accept this." They are not the same question, and a manifest that passes the first can still fail the second.

Contexts & namespaces: which cluster, which corner of it

☺ Like you're 10: Your kubeconfig can remember several TVs at once — a context is which TV, which remote control code, and which room in that TV you're currently pointed at.

A kubeconfig file is three lists and one pointer: clusters (a name, a server URL, a CA certificate), users (a name, and credentials — a client cert, a token, or an exec plugin that fetches one on the fly), and contexts (a name that ties one cluster to one user, plus an optional default namespace). current-context is the pointer saying which of those tuples every bare kubectl command uses right now.

# ~/.kube/config — abbreviated
apiVersion: v1
kind: Config
current-context: staging
clusters:
- name: staging-cluster
  cluster: { server: https://staging.k8s.internal:6443, certificate-authority: ca.crt }
- name: prod-cluster
  cluster: { server: https://prod.k8s.internal:6443, certificate-authority: ca.crt }
users:
- name: prashant
  user: { exec: { command: aws, args: ["eks", "get-token", "--cluster-name", "prod"] } }
contexts:
- name: staging
  context: { cluster: staging-cluster, user: prashant, namespace: checkout }
- name: prod
  context: { cluster: prod-cluster, user: prashant, namespace: checkout }
$ kubectl config get-contexts                                # list every context, mark the current one
$ kubectl config current-context                              # just the pointer
$ kubectl config use-context prod                              # flip the pointer
$ kubectl config set-context --current --namespace=checkout    # change the current context's default namespace
$ kubectl config view --minify                                 # only the currently active cluster/user/context

KUBECONFIG can list several files separated by : (or ; on Windows), and kubectl merges them into one logical config — the common pattern is one file per cluster, checked out from wherever that cluster's operator hands them out, merged automatically rather than hand-edited into a single giant file.

kubeconfig context: dev context: staging ← current context: prod kubectl config use-context flips which row is current dev apiserver staging apiserver prod apiserver

A shared cluster credential means current-context is shared, mutable, hidden state — the exact opposite of a safe thing to rely on right before typing kubectl delete namespace checkout. Two habits fix that. First, plugins: kubectl krew install ctx ns gives you kubectl ctx and kubectl ns — fast, fuzzy-searchable switchers modeled on Ahmet Alp Balkan's original kubectx/kubens — and most shell prompt integrations (a Powerlevel10k segment, an oh-my-zsh plugin) print the current context and namespace on every line, so you see it before you type, not after you've already run the command. Second, scripts and CI should never depend on ambient current-context at all — pass --context and --namespace explicitly on every invocation, so a pipeline behaves identically no matter what some human last left the shared runner's kubeconfig pointed at.

kubectl explain: reading the API's own schema

☺ Like you're 10: Instead of Googling "what fields does a Deployment have," you just ask the cluster itself — it always has the current, correct answer.

kubectl explain reads the OpenAPI schema kube-apiserver publishes about itself (the same schema the API & controller pattern page covers as the discovery mechanism behind server-side validation), field by field, with the description text the API's own maintainers wrote. Because it queries the live server rather than a bundled, potentially-stale copy, it's automatically correct for whatever Kubernetes version the cluster is actually running — and it works identically on a CRD your platform team shipped yesterday, as long as that CRD's openAPIV3Schema is filled in.

$ kubectl explain deployment                                       # top-level fields, one paragraph each
$ kubectl explain deployment.spec.strategy                          # drill into one nested field
$ kubectl explain deployment.spec.template.spec.containers.resources --recursive   # the whole subtree at once
$ kubectl explain --api-version=batch/v1 cronjob.spec.schedule      # pin the version if more than one is served
$ kubectl explain database.spec                                     # works on your own CRD identically

Reach for it before reaching for a search engine any time a manifest field's exact meaning, type, or valid values are in doubt — it's faster, it's always in sync with the cluster in front of you, and unlike most blog posts it can't be answering for a Kubernetes version two years out of date.

Extending the CLI: plugins via krew

☺ Like you're 10: kubectl can learn new tricks — anyone can write one and hand it to you, and krew is just the app store that finds and installs them.

A kubectl plugin is nothing more exotic than an executable named kubectl-something sitting somewhere on your PATH — kubectl scans for that naming convention on startup, and kubectl something runs it, passing along any extra arguments. There's no registration step and no compiled-in list; the convention alone is the whole mechanism. krew is a plugin manager built on top of that convention: it maintains a searchable index of community plugins, handles the download and install for your OS/architecture, and — being itself installed as a kubectl plugin — even upgrades itself the same way.

# one-time krew install: see krew.sigs.k8s.io for the current official snippet per OS
$ export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"

$ kubectl krew search                          # browse the whole index
$ kubectl krew install ctx ns neat tree who-can # install several plugins at once
$ kubectl krew list                             # what's installed
$ kubectl krew upgrade                          # upgrade everything, including krew itself

A handful earn a permanent spot on most clusters people actually operate: ctx/ns for fast context and namespace switching (covered above); neat, which strips managedFields, resourceVersion, and other server-generated noise out of a kubectl get -o yaml so the output is something you'd actually want to read or re-apply; tree, which walks the ownerReferences chain from the object model page to print an object and everything it owns as one indented tree — a Deployment, its ReplicaSets, and their Pods, in one glance; and who-can, which answers "which subjects can do this verb on this resource" directly from live RBAC bindings, instead of you tracing RoleBindings by hand.

Day-to-day commands

☺ Like you're 10: A short list covers most of what you'll type in any given hour — look, describe, read logs, get inside, and roll back if it goes wrong.

# the read loop: what exists, why, and what it's saying
$ kubectl get pods -n checkout -o wide
$ kubectl describe pod checkout-7d9f4c-x8k2q -n checkout
$ kubectl logs -f checkout-7d9f4c-x8k2q -n checkout --previous       # --previous: the crashed container's last log

# getting inside a running (or not-quite-running) container
$ kubectl exec -it checkout-7d9f4c-x8k2q -n checkout -- sh
$ kubectl port-forward svc/checkout 8080:80 -n checkout
$ kubectl cp checkout-7d9f4c-x8k2q:/var/log/app.log ./app.log -n checkout

# surgical edits without a full apply
$ kubectl patch deployment checkout -n checkout -p '{"spec":{"replicas":5}}'
$ kubectl label pod checkout-7d9f4c-x8k2q tier=backend -n checkout
$ kubectl annotate deployment checkout kubernetes.io/change-cause="bump to 1.5.0" -n checkout

# rollouts, and backing out of one
$ kubectl rollout status deployment/checkout -n checkout
$ kubectl rollout undo deployment/checkout -n checkout --to-revision=3

# resource pressure and blocking waits, useful in scripts
$ kubectl top pods -n checkout --sort-by=memory
$ kubectl wait --for=condition=Available deployment/checkout -n checkout --timeout=120s
🐰 Remy's drill · 15 min

On any cluster you can reach: generate a Deployment with --dry-run=client -o yaml, apply it, then run kubectl explain deployment.spec.strategy.rollingUpdate --recursive and set maxSurge/maxUnavailable explicitly based on what it tells you. Scale it imperatively with kubectl scale, then re-apply the original file unchanged and confirm the replica count you scaled to survives the apply — that's the three-way merge from earlier, proven rather than taken on faith. Finish by switching namespace with kubectl config set-context --current --namespace= and re-running one command with no -n flag to see it land somewhere else entirely.

Gotchas and failure modes

☺ Like you're 10: Most kubectl mistakes aren't typos — they're the CLI doing exactly what you told it, in a cluster or namespace you didn't mean.

The unnamespaced default is default, silently. Any command without -n and without a context-level default namespace set lands in the default namespace — not "wherever you were last," not an error. A script that assumes it inherited a namespace from a previous cd-style command is assuming something kubectl never promised.

--field-selector is not a general filter. Unlike -l/label selectors, which match any label you've set, --field-selector only works against a small, resource-specific allow-list (status.phase, metadata.name, spec.nodeName, and a handful more) — trying it against an arbitrary field returns a "field label not supported" error, not the filtered list you expected.

kubectl exec needs a shell that exists and a container that's actually running. Against a CrashLoopBackOff Pod, or a distroless image with no shell at all, exec simply fails. kubectl debug solves both: kubectl debug -it checkout-7d9f4c-x8k2q --image=busybox --target=checkout -n checkout attaches an ephemeral container that shares the target container's process namespace, giving you a shell and a set of tools next to a container that never had either.

kubectl get all doesn't get all. It's a fixed, hardcoded list of common resource types — it silently excludes ConfigMaps, Secrets, ResourceQuotas, and any CRD-backed resource, regardless of what actually exists in the namespace. Treat it as a quick glance, never as a complete inventory.

A long-running -w/--watch can die quietly. The underlying watch connection can be closed by the API server (an idle timeout, an apiserver restart during a rolling upgrade) without kubectl automatically reconnecting in older versions — a terminal that looks like it's still live-tailing may in fact have gone stale minutes ago. For anything you actually need to trust, prefer a tool built to reconnect — k9s's live views handle this for you — over a bare -w left running unattended.

⚠ Watch out — --force is a delete, not a gentler apply

kubectl replace --force and kubectl apply --force don't retry more politely — they delete the object and recreate it from scratch when a normal update is rejected (typically because you're trying to change an immutable field). That means a real, if usually brief, gap where the object doesn't exist: for a Pod, a restart; for anything a Service was routing to, a window of failed requests; for anything with ownerReferences pointing at it, a moment where those references are briefly orphaned. Reach for --force only once you've confirmed the field really is immutable and you've accepted the gap — not as a reflex fix for a rejected apply.

kubectl vs. the alternatives

☺ Like you're 10: Other tools give you a nicer window onto the exact same cluster — none of them replace kubectl, they mostly sit on top of it.

OptionModelBest whenCosts you
kubectlScriptable CLI, one HTTPS request per invocationAutomation, CI, precise one-off commands, anything that needs to be reproducible in a pipelineNo persistent view — every question is a fresh command; nothing live-updates on its own
k9sTerminal UI wrapping kubectl-equivalent API calls, live-refreshingFast interactive triage across many resources — logs, exec, and delete without retyping selectors each timeNot scriptable; still a human driving it, one cluster/context at a time
Lens / OpenLensDesktop GUI, graphical resource browser and metricsNewcomers, or anyone who wants graphs and a resource tree without memorizing flagsA heavier client to install and keep patched; less naturally scriptable than a CLI
Kubernetes DashboardWeb UI, deployed as a workload inside the cluster itselfA shared, browser-based view for people who won't install a CLI at allOne more thing running in-cluster with its own RBAC surface to secure carefully
kubectl + krew pluginsThe same CLI, extended for one specific job (RBAC introspection, ownership trees, output cleanup)You already live in kubectl and want one sharp tool added, not a whole new interfaceOne more thing to keep updated per machine; not every plugin is actively maintained

Note what's deliberately absent from that table: Helm and Kustomize aren't kubectl alternatives at all — they're templating and packaging layers that ultimately still hand kubectl (or the same API kubectl talks to) a fully-rendered manifest. Asking "kubectl or Helm" is the same category error as asking "Ansible or Terraform" on the DevOps course's Ansible page — different jobs, routinely used together, not competitors for the same slot.

🎬 At the Pod Squad
🐰

Remy the Rabbit: Deployment's scaled, Service is exposed, done — three commands, twenty seconds.

🦊

Foxy: Wait, done where? Which cluster did that just hit?

🐰

Remy the Rabbit: ...current-context. Whatever that was set to.

🦉

Professor Owl: Which is exactly the habit worth breaking. Speed is fine. Speed with an invisible target isn't.

👺

Gizmo the Gremlin: Or just always run with --force when apply complains — never fails twice. 🤑

🐢

Timmy the Turtle: --force deletes the object and recreates it, Gizmo. That "never fails twice" is a restart you didn't ask for, every single time.

🐰

Remy the Rabbit: Fine, fine — kubectl config get-contexts first, from now on, before anything that scales or deletes. Still faster than you'd think.

🦫

Benny the Beaver: And when you're ready to write it down properly instead of typing it from memory again — --dry-run=client -o yaml. I keep every manifest I've ever kept that way.

🐢 Timmy's checkpoint

1. Name the three ways to manage a Kubernetes object with kubectl, and which one alone reads a three-way merge before deciding what to change. 2. What three things does kubectl apply compare against each other, and what does it use to remember what you applied last time? 3. What's the practical difference between --dry-run=client and --dry-run=server, and which one can catch a rejecting admission webhook? 4. Name the four parts of a kubeconfig entry (three lists and one pointer), and what current-context actually points at. 5. Why does kubectl explain stay correct even against a Kubernetes version released last week, or a CRD nobody on the platform team documented? 6. What convention turns any executable on your PATH into a kubectl plugin, with no registration step? 7. Why does kubectl replace --force risk a brief outage in a way a normal kubectl apply doesn't?

Check your answers
  1. Imperative commands (kubectl scale, kubectl expose, etc. — no file, acts directly on the live object), imperative object configuration (kubectl create -f/kubectl replace -f — a whole-file overwrite), and declarative object configuration (kubectl apply -f). Only apply reads a three-way merge before deciding what to change — the other two simply act on whatever they were told, without checking what else may have changed the live object.
  2. apply compares your file, the live object, and the kubectl.kubernetes.io/last-applied-configuration annotation — a JSON snapshot of the file from the previous apply, which apply writes onto the object itself as it goes, giving it a memory of what it last intentionally set.
  3. --dry-run=client renders the object locally and never contacts the API server at all — it can't validate anything server-side. --dry-run=server submits the request for real defaulting, validation, and admission processing without persisting the result, so it's the one that can catch a validating webhook rejecting the object or a mutating webhook changing it.
  4. clusters (server URL + CA), users (credentials), contexts (a name tying one cluster to one user, plus an optional default namespace), and the current-context pointer, which says which context tuple every bare kubectl command uses right now.
  5. Because it reads the OpenAPI schema live from the server currently in front of it rather than a bundled or cached copy — the schema is generated by that exact cluster's exact API version, and works identically for a CRD as long as that CRD's openAPIV3Schema is filled in, with no separate documentation to keep in sync.
  6. Any executable named kubectl-<name> found on PATH is automatically runnable as kubectl <name> — kubectl just scans for that naming convention at startup. krew is a package manager built on top of that convention, not a separate plugin mechanism of its own.
  7. Because --force means delete-then-recreate rather than a true in-place update — it's used specifically when a normal update is rejected for trying to change an immutable field, and the object genuinely doesn't exist for a brief window in between, unlike a normal apply's in-place patch.