Sitemap

Kubernetes, Beyond the YAML: kubectl from First Principles

10 min readOct 6, 2026

--

Previously: in Part 2 we learned that YAML is only the order form, and the object inside the cluster is the real thing. Every one of those objects reached the cluster through one tool.

Press enter or click to view image in full size

The one tool everyone touches

Most people who use Kubernetes never log in to a node, open etcd or read the scheduler’s code. Almost all of them use kubectl.

kubectl is the official command-line tool for Kubernetes. You use it to create objects, read their status, stream logs, open a shell in a container, and change which cluster you are working with. It ships with every Kubernetes release and works the same way against a laptop cluster, AKS, GKE or EKS.

This part explains kubectl from first principles: what it actually is, how it knows which cluster to talk to, how its commands are built, and the features most people never discover. Examples use orders-api, the app we carry through the series.

What kubectl is, and isn’t

kubectl feels like it controls the cluster. It doesn’t. kubectl is an HTTP client. It turns what you type into HTTPS requests against the Kubernetes API, then formats the response for your screen.

On the component map from Part 1, kubectl sits outside the cluster. It never touches nodes, containers or etcd directly. It talks to exactly one thing: the API server’s address. Anything kubectl can do, a script calling the same API with curl could do too.

You can watch the requests go out. Add -v=6 to any command to print the HTTP calls kubectl makes:

$ kubectl get pods -n orders -v=6
GET https://orders-staging.example.com:443/api/v1/namespaces/orders/pods?limit=500 200 OK in 38 milliseconds

(Output trimmed; the real log lines also carry timestamps and source file names.)

Every command maps to an HTTP method on a URL:

Press enter or click to view image in full size

Three things follow from this. kubectl needs to know where the API server is, who you are, and which version of the API it’s speaking. The first two come from a file called kubeconfig; the third is why keeping kubectl close to your cluster’s version matters.

kubeconfig: where and who

kubeconfig is a YAML file, by default at ~/.kube/config, that answers two questions for kubectl: which API server to call, and which identity to present. It holds three lists and one pointer (Kubernetes docs):

  • clusters: where to connect, meaning the API server’s URL and the certificate authority used to trust it.
  • users: how to authenticate, with a client certificate, a token, or a command that fetches credentials. AKS and GKE use this last option through credential plugins that sign you in with Microsoft Entra ID or Google.
  • contexts: named pairings of one cluster, one user and, optionally, a default namespace.
  • current-context: the context kubectl uses whenever a command doesn’t name one.

A trimmed kubeconfig for orders-api, with a staging and a production cluster:

apiVersion: v1
kind: Config
clusters:
- name: staging-gke
cluster:
server: https://orders-staging.example.com
- name: prod-aks
cluster:
server: https://orders-prod.example.com
users:
- name: dev-user
user:
exec: { ... } # credential plugin
contexts:
- name: staging
context: { cluster: staging-gke, user: dev-user, namespace: orders }
- name: prod-aks
context: { cluster: prod-aks, user: dev-user, namespace: orders }
current-context: staging

The same file, drawn out. Any command that doesn’t name a context goes to the one current-context points at.

Press enter or click to view image in full size

The commands you’ll use to move around:

kubectl config get-contexts          # list contexts; * marks the current one
kubectl config current-context # which one am I using?
kubectl config use-context prod-aks # switch (rewrites current-context in the file)
kubectl --context staging get pods # use a context for one command only

One detail surprises people: use-context doesn't change a setting in your shell. It edits the file on disk, so every terminal reading that file switches too. The KUBECONFIG environment variable lets each terminal point at its own file, and --context targets a cluster for a single command without touching the file at all.

Anatomy of a command

Almost every kubectl command follows one shape:

kubectl <verb> <resource> [name] [flags]
kubectl get    pods       orders-api-2xkqp  -n orders -o yaml
  • Verb: what to do, such as get, describe, apply, delete, logs or exec.
  • Resource: the kind of object, such as pods, deployments or services. Singular, plural and short names all work, so pod, pods and po are the same. kubectl api-resources lists every kind with its short name.
  • Name: optional. Leave it out to act on every object of that kind, or narrow with a label selector.
  • Flags: modify the request or the output.

The flags you’ll use most:

Press enter or click to view image in full size

Two habits save a lot of typing. kubectl explain reads field documentation straight from the cluster (kubectl explain deployment.spec.strategy), and shell completion (kubectl completion bash or zsh) completes resource names as you type.

The command families

kubectl has dozens of commands, but they fall into four families. Learn the family and you can guess the command.

Press enter or click to view image in full size

A typical session with orders-api uses all four:

# inspect
kubectl get deploy,pods -n orders -l app=orders-api
kubectl describe pod orders-api-2xkqp -n orders # Events at the bottom
# change
kubectl apply -f orders-api.yaml
kubectl rollout status deploy/orders-api -n orders
# debug
kubectl logs deploy/orders-api -n orders -f
kubectl port-forward svc/orders-api 8080:80 -n orders
# check
kubectl auth can-i update deployments -n orders

Note rollout. To restart an app, kubectl rollout restart deploy/orders-api replaces pods gradually through their owner, the Deployment. Deleting pods by hand works, but removes them all at once. Change the owner, never the thing it owns.

Output formats

The same object can be shown many ways. kubectl asks the API server for either a summary table or the full object, then formats it for you, and -o chooses how.

Press enter or click to view image in full size

A few that earn their place in your notes:

# which node is each orders-api pod on?
kubectl get pods -n orders -l app=orders-api -o wide
# the image every orders-api pod is running
kubectl get pods -n orders -l app=orders-api \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
# a custom table of name and restart count
kubectl get pods -n orders -o custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount

-o yaml is also the fastest way to see Part 2's lesson in action: you'll find far more fields than your manifest ever set, because the API server filled in defaults and the controllers wrote status.

Imperative vs declarative

There are two ways to tell Kubernetes what you want, and kubectl supports both.

Imperative commands describe an action: “create this,” “scale that to five.”

kubectl create deployment orders-api --image=orders-api:1.4 -n orders
kubectl scale deployment orders-api --replicas=5 -n orders

They’re fast for experiments, but leave no record of the intended state. The next person can’t tell why there are five replicas.

Get kashmira lodha’s stories in your inbox

Join Medium for free to get updates from this writer.

Declarative management describes the result, in a file, and lets Kubernetes work out the steps:

kubectl apply -f orders-api.yaml

apply compares the file with what's live and sends only the difference. The file lives in Git, so the desired state is reviewed, versioned and reproducible. This is how production clusters should be run, and it is the foundation of GitOps tools such as Argo CD and Flux.

Client-side vs server-side apply

Classic kubectl apply works out the difference on your machine and stores a copy of your last file in an annotation on the object. Server-side apply (kubectl apply --server-side), GA since Kubernetes 1.22, moves that work into the API server. The server records which tool or person owns each field, in metadata.managedFields, and reports a conflict if two owners try to set the same field (Kubernetes docs). That makes it the safer choice when several tools, such as a CI pipeline and an autoscaler, touch the same object.

Preview before you apply

kubectl diff -f orders-api.yaml                      # what would change
kubectl apply -f orders-api.yaml --dry-run=server # would the API server accept it

diff shows the exact changes against the live cluster. --dry-run=server sends the request through every check the API server performs, including permissions and admission policies, and stops before saving. Part 4 opens up what those checks are.

Extending kubectl

Plugins

Any executable on your PATH named kubectl-<something> becomes a kubectl command. A script called kubectl-orders runs when you type kubectl orders. kubectl plugin list shows what's installed.

Krew is the community plugin manager, with a catalogue of hundreds of plugins. A few popular ones:

kubectl krew install ctx ns tree
kubectl ctx # switch context interactively
kubectl ns orders # switch default namespace
kubectl tree deploy orders-api -n orders # show Deployment → ReplicaSet → Pods

kubectl tree is a good companion to Part 2: it draws the ownership chain you learned about, straight from the live cluster.

kuberc: your own defaults and aliases

kubeconfig describes clusters and credentials. kuberc is a separate file, ~/.kube/kuberc, for how you like kubectl to behave. It never contains cluster details or credentials (Kubernetes docs). It reached beta in Kubernetes 1.34 (Kubernetes blog) and has been on by default since 1.36, which also added a kubectl kuberc command for viewing and editing it (Cloudsmith).

It has two main sections: defaults, which set a flag for a command unless you override it, and aliases, which create shortcuts. The kubectl maintainers recommend two defaults: server-side apply and interactive delete.

apiVersion: kubectl.config.k8s.io/v1beta1
kind: Preference
defaults:
- command: apply
options:
- name: server-side
default: "true"
- command: delete
options:
- name: interactive
default: "true"
aliases:
- name: getn
command: get
options:
- name: output
default: yaml

With this file, kubectl delete lists what it's about to remove and waits for a yes, and kubectl getn pods prints YAML. Passing a flag explicitly, such as --interactive=false, always wins over the default. Setting KUBERC=off disables the file entirely, which helps when troubleshooting.

Safe habits

kubectl does exactly what it’s told, against whichever cluster kubeconfig points at. Most kubectl mistakes come from a few predictable surprises, and each one has a habit that prevents it.

Know which cluster and namespace you’re in

The context carries a namespace. Leave out -n and kubectl uses the context's default namespace. An apply meant for orders can land in default, and a later get looks empty.

Multiple kubeconfig files merge. KUBECONFIG can list several files separated by colons. kubectl merges them, and the first file to define a value wins, so two files with the same context name can surprise you.

The habits that prevent both:

  • Know where you are. Show the current context and namespace in your shell prompt (kube-ps1 is a popular option). Check kubectl config current-context before anything destructive.
  • Name the cluster for risky commands. kubectl --context staging delete ... affects only that command and never edits your kubeconfig.
  • Keep one kubeconfig per terminal when you work across clusters: export KUBECONFIG=~/.kube/staging.yaml.

Look before you change or delete

Deleting a namespace deletes everything in it. That includes Secrets and PersistentVolumeClaims. With a Delete reclaim policy, it also deletes the disks behind them.

  • Preview changes with kubectl diff and --dry-run=server before apply.
  • Make deletes ask first with kubectl delete -i, or set it as a kuberc default.

Keep Git as the source of truth

apply overwrites edit. A change made with kubectl edit is undone the next time someone applies a file that sets the same field.

-o yaml is not a manifest. The live object includes defaults, status and managed fields. Copying it back into Git as your source file brings all of that with it.

  • Change the file, then apply it. Edits made only in the live cluster don’t survive the next apply.
  • Change owners, not what they own. Scale and restart Deployments, not individual pods or ReplicaSets.

Debugging map

Press enter or click to view image in full size

Cheat sheet

  • kubectl is an HTTP client. Every command is a request to the API server. -v=6 shows it.
  • Shape: kubectl <verb> <resource> [name] [flags]
  • kubeconfig = clusters + users + contexts + current-context. use-context edits the file.
  • Four families: inspect (get, describe), change (apply, rollout), debug (logs, exec, debug), check (config, auth can-i).
  • Output: -o wide, -o yaml, -o jsonpath, -o custom-columns.
  • Declarative beats imperative: keep manifests in Git, diff first, apply second.
  • Extend it: plugins via krew; defaults and aliases in ~/.kube/kuberc.
  • Change the owner: kubectl rollout restart deploy/orders-api.

A short history

kubectl has grown from a thin wrapper around the API into a toolkit with its own configuration, plugins and debugging features. The milestones worth knowing:

Press enter or click to view image in full size

The direction is consistent: more logic moves to the API server (server-side apply, server-side dry runs), and kubectl becomes a safer, more configurable front end.

Next up

Every command in this part became an HTTP request to one address. So who is listening there, and what does it check before it says yes?

--

--