A Kubernetes manifest looks like plain configuration — a YAML file with some familiar looking fields — but it works on a different model than most config files you've written before. Once that model clicks, the rest of the syntax is just vocabulary.
YAML describes desired state, not steps to run
Running kubectl apply -f deployment.yaml doesn't execute the file the way a script runs. It submits the YAML to the Kubernetes API server as a description of what you want to exist — "there should be 3 replicas of this container running." A controller then continuously compares that desired state against what's actually running and takes action to close the gap, on an ongoing basis, not just once at apply time. If a pod crashes five minutes later, nothing you ran caused the replacement — the controller noticed actual state had drifted from desired state and corrected it on its own.
Every manifest has four things
Regardless of what kind of object you're describing, every Kubernetes manifest needs the same four top-level fields:
apiVersion: v1 # which version of the API this object belongs to
kind: Pod # what kind of object this is
metadata: # identifying info — name is required
name: my-app
spec: # the actual configuration — shape depends on "kind"
containers:
- name: app
image: nginx:1.25apiVersion and kind together tell the API server which schema to validate spec against — a Pod's spec looks nothing like a Service's. metadata.name has to be unique within its kind and namespace, and follows stricter rules than you might expect: lowercase only, and mostly alphanumeric plus -/. — no uppercase, no underscores.
You'll rarely write a bare Pod
A Pod is the smallest thing Kubernetes runs, but a bare Pod isn't self-healing — if it dies, nothing restarts it. In practice you almost always wrap a pod definition in a Deployment, which adds replica count and handles restarting and rolling updates:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: nginx:1.25Notice the pod definition didn't change shape — it just moved under spec.template, with replicas added alongside it. StatefulSet, DaemonSet, and Job all follow the same pattern: their own spec fields, wrapped around the same pod template shape.
Labels select, annotations describe
Both live under metadata and both are key-value maps, which makes them easy to mix up. Labels are for identification — a Service finds the pods it should route to by matching spec.selector against pod labels, and nothing works if they don't match. Annotations are for everything else: arbitrary metadata tools attach that isn't meant to be selected on. kubectl apply itself uses one — the kubectl.kubernetes.io/last-applied-configuration annotation, a full copy of the last manifest you applied, used internally for three-way merges. It's a good concrete example of "metadata a tool needs, not something you'd ever select on."
One file, multiple resources
A single YAML file can describe several objects, separated by a line containing just ---:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: nginx:1.25
---
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app
ports:
- port: 80kubectl apply -f on a multi-document file applies every document in it — a common way to keep a Deployment and the Service that fronts it in one file, since they're almost always deployed and reasoned about together.
apiVersion isn't just a version number
Kubernetes actually removes old API versions on a published schedule — this isn't like a library where an old import path keeps working forever. extensions/v1beta1 stopped working in Kubernetes 1.16. A manifest written against a removed apiVersion looks completely normal — same fields, same indentation — right up until kubectl apply rejects it outright on a modern cluster.
Requests and limits decide who gets evicted first
A container can declare how much CPU/memory it expects to need (requests) and the most it's allowed to use (limits). Whether every container in a pod sets both, matching, determines the pod's QoS class — Guaranteed, Burstable, or BestEffort — which is exactly the order Kubernetes evicts pods in when a node runs low on resources. BestEffort (nothing set) goes first.
Mistakes worth avoiding early
- YAML indentation. Same as any YAML file — two spaces off and a field silently belongs to the wrong parent, with no compiler to catch it ahead of time.
- A Service with no matching selector. If
spec.selectordoesn't match any pod's labels, the Service exists but routes to nothing — no error, just silence. - Reapplying a live export verbatim.
kubectl get pod x -o yamlincludes fields the cluster generated —status,resourceVersion,uid— that aren't meant to be hand-edited and reapplied as-is.
Try it yourself
Kubernetes YAML Validator catches the structural mistakes above — including exactly which apiVersions have been removed and when — Kubernetes Manifest Formatter cleans up indentation and field order, Kubernetes Resource Calculator computes a manifest's actual QoS class instead of you working it out by hand, and K8s Diff compares two manifests while automatically ignoring the server-generated fields mentioned above. All four run entirely in your browser.