CoursesKubernetes administrationkubectl & the object model

kubectl & the object model

The client, the REST API, and imperative vs declarative.

Beginner10 min · lesson 2 of 65
In plain terms
kubectl is the remote control for your cluster. You press a button (a command) and the cluster carries it out — you never walk over and flip switches on the machines by hand.

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.

web-deploy.yaml
apiVersion: apps/v1 # the type, plus its API group
kind: Deployment
metadata:
name: web
namespace: prod
labels:
app: web
spec: # desired state, you write this
replicas: 3
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: nginx:1.27
status: # observed state, the system writes this
readyReplicas: 3
terminal
kubectl apply -f web-deploy.yaml
kubectl get deploy web -n prod
output
deployment.apps/web created
NAME READY UP-TO-DATE AVAILABLE AGE
web 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.

terminal
kubectl explain deployment.spec.replicas
output
GROUP: apps
KIND: Deployment
VERSION: v1
FIELD: replicas <integer>
DESCRIPTION:
Number of desired pods. This is a pointer to distinguish between explicit
zero 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.

terminal
kubectl get pods -n prod -v=6
output
I0716 09:14:02.331847 18422 loader.go:395] Config loaded from file: /home/you/.kube/config
I0716 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 milliseconds
NAME READY STATUS RESTARTS AGE
web-7d9f5c8b6d-4xk2t 1/1 Running 0 58s
web-7d9f5c8b6d-9p2mn 1/1 Running 0 58s
web-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.

terminal
kubectl create deployment web --image=nginx:1.27 --replicas=3 \
--dry-run=client -o yaml
output
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: web
spec:
containers:
- image: nginx:1.27
name: nginx
resources: {}
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.

terminal
kubectl diff -f web-deploy.yaml
output
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: web
namespace: prod
spec:
- replicas: 5
+ replicas: 3
(exit code 1: the live object differs from the file)
Which way should you drive the cluster?
You need to change the cluster
which path fits the change?
scratch, debugging, exam clock
Imperative
kubectl run, create, scale: instant, nothing saved to git
you want a manifest without typing it
Generate, then keep
create ... --dry-run=client -o yaml > app.yaml, then apply it
anything real: prod, reviewed, repeatable
Declarative
kubectl apply -f from git; the cluster reconciles to the file
Rule of thumb: if you'd be sad to lose it, it belongs in a file under apply, not in a one-off command.
apply can only prune what it has tracked
Client-side apply works out what to change by diffing your file against a hidden annotation, kubectl.kubernetes.io/last-applied-configuration, that only gets written when you use apply. Create the object with kubectl create, or hand-edit it with kubectl edit, and that annotation ends up missing or stale. So later, when you drop a field from your YAML and re-apply, the field stays put on the live object, because apply has no record that you ever set it and assumes something else owns it. Two ways out: manage the object with apply from its very first creation, or switch to kubectl apply --server-side, where the API server tracks ownership field by field and prunes what you remove.

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.

terminal
$ 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.

Quick check
01A Deployment is managed by 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?
Incorrect — apply reconciles the live object toward the file, so it does not preserve an out-of-band change you never wrote back.
Correct — the file is the desired state, so the hand-scaled 8 is overwritten and the drift vanishes with no warning. That is exactly why imperative edits and declarative management don't mix.
Incorrect — apply merges rather than erroring on a changed object; only a stale resourceVersion would conflict, and CI is not sending one.
Incorrect — apply takes effect immediately; no restart or rollout trigger is involved for a field like replicas.
02Running any kubectl command with -v=6 prints the raw HTTP call it makes. Under the hood, what is kubectl edit actually doing?
Incorrect — kubectl is only a client; it never touches etcd, and only the API server reads or writes the store.
Incorrect — the lesson maps edit to a read-then-patch pair, not a single replacing PUT.
Incorrect — edit does not recreate the object; that would change its identity and drop its history.
Correct — the lesson states edit is really a GET followed by a PATCH, the same verbs get and apply lean on.
03A Deployment was first created with 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?
Correct — without that annotation apply has no record you ever set the field, so it leaves it; server-side apply tracks ownership field by field and prunes what you drop.
Incorrect — apply can prune fields it tracks; the problem here is specifically the missing annotation because the object was created with create first.
Incorrect — dropping an optional block does not make the manifest invalid, and no rejection occurred.
Incorrect — the live object reflects a real merge decision, not a stale cache, and restarting nothing changes the ownership tracking.

Related