All modules in this track (20)
In this module, you'll learn to:
- Read any manifest using the four fields every object has
- Write valid YAML, and connect objects with labels and selectors
- Use namespaces, and work declaratively with
kubectl apply
Before you start: have the kind cluster from Your First Cluster running. Every example here works on it.
Every object has the same shape
Every Kubernetes object, whether it runs your app, routes traffic, or grants a permission, is described with the same four top-level fields. Once you can read one, you can read them all.
apiVersion: apps/v1 # 1. which API group and version defines this kind
kind: Deployment # 2. what type of object it is
metadata: # 3. identity: name, namespace, labels
name: hello
namespace: default
labels:
app: hello
spec: # 4. the desired state: what you want to exist
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80| Field | What it means | You'll see values like |
|---|---|---|
apiVersion | Which version of the API this object follows | v1, apps/v1, networking.k8s.io/v1 |
kind | The type of object | Pod, Deployment, Service, ConfigMap |
metadata | Its name, namespace, labels, and annotations | name: hello |
spec | What you want. You write this | replicas, image, ports |
status | What is. The cluster writes this, never you | ready replicas, pod IP, conditions |
That last row is the reconciliation loop again: you write spec, controllers work until status matches it. Run kubectl get deployment hello -o yaml and you'll see both.
Just enough to read and write manifests
YAML is a way of writing structured data using indentation instead of brackets. Five rules cover nearly every manifest:
- Indentation is structure. Use spaces, never tabs. Two spaces per level is the convention. A line indented under another belongs to it.
key: valuemakes a map (a set of named fields). Note the space after the colon.- A dash
-starts a list item.containers:is a list, so each container begins with- name:. ---separates documents, so one file can hold a Deployment and its Service.#starts a comment. Quote values that look like numbers or booleans but must be strings, such as"true"or"8080"in an annotation.
# A map with a nested map and a list
metadata:
name: hello # key: value
labels: # a nested map
app: hello
tier: web
spec:
containers: # a list...
- name: web # ...whose first item is a map
image: nginx:1.27
- name: sidecar # second item
image: busybox:1.36The most common beginner error is indentation that's one space off, which either fails with a parse error or, worse, silently attaches a field to the wrong parent. kubectl apply --dry-run=server -f file.yaml checks a file against the cluster without changing anything.
How objects find each other
Kubernetes objects don't reference each other by ID. They use labels (key/value tags in metadata.labels) and selectors (queries that match labels). In the Deployment above, selector.matchLabels: app: hello means "the pods I own are the ones labelled app: hello", and the pod template gives every new pod that label.
A Service finds its pods the same way. That's the glue between almost every object in this track:
# See labels on pods
kubectl get pods --show-labels
# Select pods by label
kubectl get pods -l app=hello
kubectl get pods -l 'tier in (web, api)'
# Add a label to a running pod
kubectl label pod <pod-name> owner=me
If a selector and a label don't match, nothing errors. The Service just has no pods behind it, which is the first thing Services & Cluster Networking teaches you to check.
Sections of a cluster
A namespace groups related objects. Names only have to be unique inside a namespace, so two teams can both have a Deployment called api. Every command runs against one namespace, default unless you say otherwise.
# List namespaces; kube-system holds Kubernetes' own components
kubectl get namespaces
# Create one and work in it
kubectl create namespace shop
kubectl get pods -n shop
# Every namespace at once
kubectl get pods -A
# Make "shop" the default for your current context
kubectl config set-context --current --namespace=shopNamespaces are also where permissions and quotas attach, covered in Namespaces, RBAC & Service Accounts.
Imperative commands and declarative files
In the last module you used imperative commands: kubectl create deployment, kubectl scale. They're quick for experiments, but nothing records what you did, so nobody can review it or recreate it.
Real work uses the declarative style: write the desired state in a file, keep the file in Git, and run kubectl apply. Apply is safe to repeat. It creates what's missing, changes what's different, and leaves the rest alone.
# Let kubectl write the YAML for you, as a starting point
kubectl create deployment hello --image=nginx:1.27 \
--dry-run=client -o yaml > hello.yaml
# Create or update everything in the file
kubectl apply -f hello.yaml
# Edit the file (say, replicas: 3), then preview the change before applying it
kubectl diff -f hello.yaml
kubectl apply -f hello.yaml
# Delete everything the file describes
kubectl delete -f hello.yamlLater in the track, Helm & Kustomize handle the same files across several environments, and GitOps with Argo CD runs apply for you whenever Git changes.
The kubectl commands you'll use every day
| Command | What it does |
|---|---|
kubectl get <kind> | List objects. Add -o wide for more columns, -o yaml for everything, -w to watch |
kubectl describe <kind> <name> | Human-readable detail, including recent events. The first place to look when something's wrong |
kubectl logs <pod> | A container's output. -f follows it, --previous shows the last crashed run |
kubectl exec -it <pod> -- sh | A shell inside a running container |
kubectl apply -f <file> | Create or update from a file |
kubectl delete -f <file> | Delete what a file describes |
kubectl get events --sort-by=.lastTimestamp | What the cluster has been doing recently, newest at the bottom |
kubectl explain <kind>.<field> | Built-in documentation for any field, such as kubectl explain deployment.spec.replicas |
kubectl port-forward <svc-or-pod> 8080:80 | Reach something in the cluster from your laptop |
kubectl config get-contexts | Which clusters you can talk to, and which one is current |
Deploy an app and its Service from one file
Save this as hello.yaml, a Deployment and a Service in one file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels:
app: hello
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello
labels:
app: hello
spec:
selector:
app: hello
ports:
- port: 80
targetPort: 80
kubectl apply -f hello.yaml
kubectl get deployment,service,pods -l app=hello
# The pod IPs behind the Service
kubectl get endpointslices -l kubernetes.io/service-name=hello
# Break the link on purpose: in hello.yaml, change the Service's selector
# to app: hellooo, apply again, and re-run the endpointslices command.
# The Service now has no pods behind it. Change it back and apply again.
# Clean up
kubectl delete -f hello.yamlImplementation notes
- Start from generated YAML.
--dry-run=client -o yamland the examples in the Kubernetes docs are better starting points than writing manifests from memory. - Don't copy
statusor generated fields back into your files.kubectl get -o yamloutput includesstatus,uid,resourceVersion, and timestamps. Strip them before saving a manifest. - Use the same label keys everywhere. The common convention is
app.kubernetes.io/nameandapp.kubernetes.io/instance; short labels likeappwork fine as long as everyone agrees on them. kubectl editchanges the live object only. It's handy in a lab, but the change isn't in any file, so the nextapplyfrom Git quietly undoes it.
Key terms
- Manifest: a YAML file describing one or more objects you want to exist.
- spec and status: what you want (you write it) and what is (the cluster writes it).
- Label: a key/value tag on an object, such as
app: hello. - Selector: a query that matches labels; how Deployments and Services find their pods.
- Namespace: a named section of a cluster; names only need to be unique inside one.
- Declarative: describing the desired state in files and applying them, rather than issuing one-off commands.
Pop quiz
Five questions: three on the ideas in this module, and two scenarios where you apply them. The order changes every time you take it, and 4 out of 5 passes.
5 questions. Decide on your answer to each, then check it.
-
Concept
Which field of an object is written by the cluster, never by you?
Show answer
statusYou write
spec, the desired state. Controllers writestatus, the actual state, and keep working until the two match. -
Scenario
Your Service's selector says
app: web, but your Deployment's pods are labelledapp: hello. You apply both. What happens?Show answer
Both are created, but the Service has no pods behind it, so requests fail
Objects find each other through labels and selectors. A mismatch isn't an error; the Service simply matches nothing, which is why "no endpoints" is the first thing to check.
-
Concept
In a manifest, what does a line starting with
-mean?Show answer
An item in a list
A dash starts a list item, such as each container under
containers:. Comments start with#, and---separates documents in one file. -
Concept
Why prefer
kubectl apply -fwith files in Git over commands likekubectl createandkubectl scale?Show answer
The files are a reviewable, repeatable record of what should be running
Imperative commands leave no record. Files in Git can be reviewed, diffed, and re-applied safely, which is the foundation for everything from Helm to GitOps.
-
Scenario
You run
kubectl get podsand see nothing, but you know your app is running. What's the most likely reason?Show answer
The pods are in a different namespace from your current one
Commands run against one namespace,
defaultunless you say otherwise. Trykubectl get pods -A, or add-n <namespace>.