kubectl & the object model
The client, the REST API, and imperative vs declarative.
kubectl isn't the cluster. It's a client, the same way your mail app isn't the mail server. Type a command and kubectl turns it into an HTTP request (the same kind of request your browser makes to load a page) and sends it to one process, the API server. Then it prints whatever comes back. That's the whole job. The API server is the only piece that actually reads and writes the cluster's real state. kubectl just knows how to ask it nicely.
Think of the API server as the front desk of a big building, and kubectl as the phone on your desk. API is short for application programming interface, which is a fancy name for a well-defined way for one program to ask another program to do something. You never march into the server room and rewire things by hand. You call the front desk, say what you want, and the staff back there do the work and confirm it. Every kubectl get, apply, or delete is one of those phone calls. Once that clicks, kubectl stops being a pile of commands to memorize. It turns into something you can reason about, because what you're really reasoning about is the API underneath it.
The shape of every object
Everything you manage in Kubernetes is stored as an object. A Pod (the smallest unit, one or more containers that run together as a group), a Deployment, a Service, a Secret. Different jobs, same skeleton: every object has the same four sections. A shipping manifest is a fair picture of it. There's a header saying what kind of shipment this is. A label with the name and destination. An order describing what should be inside the box. And a tracking section the carrier keeps updating as the box moves across the country.
Here's how those four map onto the file. apiVersion and kind say what type of thing this is and which API group it belongs to. metadata holds the name, the namespace (think of it as a folder that groups and scopes objects), and any labels. spec is the part you write by hand: the desired state, what you want to exist. status is the part the system fills in: the observed state, what actually exists right now. You declare the spec. You read the status. You never hand-write the status yourself.
apiVersion: apps/v1 # the type, plus its API groupkind: Deploymentmetadata:name: webnamespace: prodlabels:app: webspec: # desired state, you write thisreplicas: 3selector:matchLabels: { app: web }template:metadata:labels: { app: web }spec:containers:- name: webimage: nginx:1.27status: # observed state, the system writes thisreadyReplicas: 3
kubectl apply -f web-deploy.yamlkubectl get deploy web -n prod
deployment.apps/web createdNAME READY UP-TO-DATE AVAILABLE AGEweb 3/3 3 3 15s
That gap between spec and status is the whole idea behind Kubernetes. A controller is a background loop running in the control plane, and it behaves like the thermostat on your wall. A thermostat reads the temperature you asked for, checks the actual temperature, and keeps kicking the heat on and off until the two match. A controller does the same with your objects. Your spec said replicas: 3. The controller saw a brand-new Deployment with zero Pods running, created three, and wrote status back as the 3/3 you just watched print. Then it keeps watching. Change the spec later and it quietly gets back to work.
Reading and exploring objects
Two commands make any unfamiliar object approachable. kubectl explain pulls the field-by-field documentation straight from the API server, so you're not guessing what goes where or memorizing a schema. kubectl get <object> -o yaml prints the live object exactly as it's stored, spec and status side by side, which is the fastest way to see what the cluster actually believes about something right now.
kubectl explain deployment.spec.replicas
GROUP: appsKIND: DeploymentVERSION: v1FIELD: replicas <integer>DESCRIPTION:Number of desired pods. This is a pointer to distinguish between explicitzero and not specified. Defaults to 1.
Want proof kubectl is only a client? Add -v=6 to any command and it prints the raw HTTP call it makes on the way out, before it prints the result.
kubectl get pods -n prod -v=6
I0716 09:14:02.331847 18422 loader.go:395] Config loaded from file: /home/you/.kube/configI0716 09:14:02.352119 18422 round_trippers.go:553] GET https://10.0.0.1:6443/api/v1/namespaces/prod/pods?limit=500 200 OK in 19 millisecondsNAME READY STATUS RESTARTS AGEweb-7d9f5c8b6d-4xk2t 1/1 Running 0 58sweb-7d9f5c8b6d-9p2mn 1/1 Running 0 58sweb-7d9f5c8b6d-qv7rl 1/1 Running 0 58s
Read that middle line closely. kubectl get became a plain HTTP GET against a URL. apply becomes a PATCH, delete becomes a DELETE, and edit is really a GET followed by a PATCH. When the request lands, the API server runs it through three gates, in order. First, authentication: who are you? Second, authorization: are you allowed to do this? That second gate is usually handled by RBAC, short for Role-Based Access Control, the rules that decide which users can touch which objects. Third, admission: does cluster policy accept this exact change? Only after all three does the server read or write the object in etcd, the cluster's key-value database where the real state actually lives on disk. kubectl did none of that work. It made one call and printed the reply.
Imperative vs declarative
There are two ways to tell the cluster what you want, and the gap between them matters more than it first looks. Imperative means giving an instruction for right now: create three replicas, scale to five, delete that Pod. It's like telling a cook to make you a sandwich. Clear and immediate, and finished the second it's done. Declarative is the opposite. You hand over a recipe card and say the kitchen should always match this. You describe the end state you want, and the system keeps working toward it on its own, forever.
The imperative verbs are kubectl create, run, scale, and expose. They're fast, which makes them great for scratch work, live debugging, and the ticking clock of a CKA (Certified Kubernetes Administrator) exam. Declarative management is kubectl apply -f pointed at a YAML file (plain-text config that's easy to read, diff, and review) that lives in git, the version-control system where teams store and review changes. In production, apply usually runs from CI, the continuous-integration pipeline that picks up merged changes and applies them for you. The point is that the repository, not somebody's shell history, is what defines the cluster.
The two aren't rivals, and the habit worth building uses both. Generate the object imperatively with --dry-run=client -o yaml, which builds a starting manifest without touching the cluster at all. Then save that file, commit it, and manage it with apply from there on out. You skip typing YAML from memory, and you still end up with a reviewed file as the record of what's supposed to exist.
kubectl create deployment web --image=nginx:1.27 --replicas=3 \--dry-run=client -o yaml
apiVersion: apps/v1kind: Deploymentmetadata:creationTimestamp: nulllabels:app: webname: webspec:replicas: 3selector:matchLabels:app: webstrategy: {}template:metadata:creationTimestamp: nulllabels:app: webspec:containers:- image: nginx:1.27name: nginxresources: {}status: {}
With the file in git, kubectl apply pushes it, and kubectl diff shows you exactly what would change before you commit to it. Think of diff as the declarative admin's safety net. It fetches the live object, merges your file on top, and prints only the delta, so you never apply blind. In the run below, someone had scaled the Deployment to 5 by hand while the file still said 3.
kubectl diff -f web-deploy.yaml
diff -u -N /tmp/LIVE-2841/apps.v1.Deployment.prod.web /tmp/MERGED-9013/apps.v1.Deployment.prod.web--- /tmp/LIVE-2841/apps.v1.Deployment.prod.web+++ /tmp/MERGED-9013/apps.v1.Deployment.prod.web@@ -6,7 +6,7 @@name: webnamespace: prodspec:- replicas: 5+ replicas: 3(exit code 1: the live object differs from the file)
Every resource has apiVersion, kind, metadata, and spec. Learn to read those four blocks and you can decode unfamiliar CRDs without a tutorial. Status is what the controllers wrote back — treat it as evidence, not as something you edit by hand.
kubectl explain and api-resources are the map when docs feel vague. Prefer them over guessing field names in a production change window.
Contexts in your kubeconfig pick cluster, user, and namespace together. A wrong context is the fastest way to apply the right YAML to the wrong place.
Try this
Create a tiny Deployment, then describe it and dump the raw object. Watch how imperative create and declarative apply both end as the same kind of API object.
$ kubectl apply -f web-deploy.yaml$ kubectl get deploy web -n prod$ kubectl explain deployment.spec.replicas$ kubectl get pods -n prod -v=6$ kubectl create deployment web --image=nginx:1.27 --replicas=3 \--dry-run=client -o yaml$ kubectl diff -f web-deploy.yaml
Takeaway
kubectl is only an HTTP client. The API server is the cluster. Imperative is for labs and emergencies; declarative apply is how you ship without losing fields you forgot to type.
kubectl apply -f web.yaml out of git, where the file says replicas: 3. During an incident someone runs kubectl scale deploy web --replicas=8 by hand, and nobody updates the file. What happens on the next apply from CI?-v=6 prints the raw HTTP call it makes. Under the hood, what is kubectl edit actually doing?kubectl create. Later you delete the resources block from its YAML and run kubectl apply -f. The live object still carries the old resource limits. Why, and what fixes it?create first.