Manifests and kubectl apply
YAML as the interface: apiVersion, kind, metadata and spec, the difference between apply and create, and why declaring the end state beats issuing commands.
YAML as the interface: apiVersion, kind, metadata and spec, the difference between apply and create, and why declaring the end state beats issuing commands.
You have created a pod two ways: with kubectl run, which reads like
a Docker command, and with kubectl apply -f pod.yaml, which reads
like a file. Both worked. Only one of them is how Kubernetes is
actually used, and the difference is worth a lesson because it
changes what your cluster is: a set of things somebody did, or a
description you keep.
By the end of this lesson you will know what apply really does, why
it behaves differently from create, how to read any manifest you are
handed, and how to keep a directory of them without it becoming a
mess.
Imperative means telling the cluster to perform an action:
kubectl run web --image=nginx --port=80
kubectl scale deployment api --replicas=5
kubectl set image deployment/api api=myapp:1.4.3
kubectl delete pod webDeclarative means writing down what should exist and handing that over:
kubectl apply -f deployment.yamlExcellent for exploring.
Quick, no file, and you can tab-complete your way to an answer.
Nothing records what you did. Later, nobody can say why the cluster looks the way it does, whether the change was intentional, or what it looked like before.
The answer lives in a file.
A file can be reviewed in a pull request, diffed against last month, reverted, copied to a second environment, and applied by a pipeline instead of a person.
The cluster stops being the source of truth about itself and becomes a projection of your repository.
Every object — pod, Deployment, Service, Ingress, all of them — has the same four top-level fields. Once you can see them, an unfamiliar manifest stops being intimidating.
apiVersion names the API group and version. Core objects — Pod,
Service, ConfigMap, Secret, Namespace — are plain v1. Workloads are
apps/v1. Ingress is networking.k8s.io/v1. Getting it wrong
produces no matches for kind, and the fix is to look it up:
kind is the type, capitalised exactly as shown.
metadata is identity: a name unique within its namespace and
kind, an optional namespace, and labels. Labels are not
decoration — they are the mechanism by which Deployments find their
pods and Services find their endpoints.
spec is the desired state, and it is where every type differs.
There is no need to memorise it:
kubectl explain reads the schema out of your actual cluster, so it
is always the right version and never out of date.
kubectl apply means: make this object match this file, whether or
not it already exists.
Three runs, three different words, one command. Being safe to re-run is what makes it usable from a pipeline: applying the same manifests on every deploy is a no-op when nothing changed.
The interesting part is how it merges, because apply records which
fields you manage.
Reverted to the default.
apply knows it owned that field last time. Deleting it from
your file is a deliberate statement, so it acts on it.
Left completely alone.
Defaults the cluster filled in, and values another controller manages — an autoscaler's replica count, for instance.
Your file is not a complete description of the object, and
apply does not pretend it is.
Compare with create, which is not idempotent:
Bad — the change exists only in the cluster, and the next apply silently undoes it:
Good — the change is in the file that the cluster is derived from:
An imperative scale during an incident is a legitimate emergency
action. The cost is that the file still says 3, so the next routine
apply — quite possibly a pipeline, hours later, applying an
unrelated change — quietly takes you back to 3 under load. Whatever
you do imperatively in a hurry, put it in the file afterwards.
Two flags answer "what would this do", and both are worth using habitually against a cluster you care about.
That is the actual difference between the cluster and your file. It catches the two mistakes that matter: applying a file you had forgotten you edited, and applying to the wrong cluster — where the diff will be enormous and obviously wrong.
--dry-run=server sends the object for full validation — admission
controllers, defaulting, everything — and then discards it. It
catches invalid manifests without changing anything.
A real application is several objects. Two ways to organise them, and both are fine.
Several documents in one file, separated by ---:
A directory of files, applied together:
Order mostly does not matter, which surprises people. Apply a Deployment that references a ConfigMap which does not exist yet and the pods wait, then start when it appears — because a controller is reconciling continuously rather than executing a script. The exceptions are objects that must exist to be referenced at all, like a Namespace or a CustomResourceDefinition.
Delete by file, which removes exactly what the file describes:
Or by label, which is why labelling everything consistently pays off:
Give every object in an application the same app: label and the
whole set becomes addressable as a group — for inspecting, for
deleting, and for the Services and selectors coming up.
You can now write, inspect and apply any object, which means the rest of this course is about which objects to write. The next one is the Deployment — the answer to the pod's fatal flaw, and the object you will write most often.
Before that, try the generator trick on something you have already
built: kubectl create deployment with --dry-run=client -o yaml,
and read what it produces. Recognising the four fields in generated
output is the fastest way to become comfortable with manifests you
did not write.
--- live +++ merged
spec:
- replicas: 3
+ replicas: 5
template:
spec:
containers:
- - image: myapp:1.4.2
+ - image: myapp:1.4.3k8s/
├── configmap.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yamlapiVersion: apps/v1 # which API this object speaks
kind: Deployment # what type it is
metadata: # who it is
name: api
namespace: default
labels:
app: api
spec: # what you want to be true
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp:1.4.2kubectl api-resources | grep -i ingress
kubectl explain ingress # shows the right apiVersionkubectl explain deployment.spec
kubectl explain deployment.spec.template.spec.containers
kubectl explain pod.spec.containers.resources --recursivekubectl apply -f deployment.yaml # created
kubectl apply -f deployment.yaml # unchanged
# edit the file, change replicas to 5
kubectl apply -f deployment.yaml # configuredkubectl create -f deployment.yaml # created
kubectl create -f deployment.yaml # Error: already existskubectl scale deployment api --replicas=10# edit deployment.yaml: replicas: 10
kubectl apply -f deployment.yamlkubectl diff -f deployment.yamlkubectl apply -f deployment.yaml --dry-run=serverapiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp:1.4.2kubectl apply -f k8s/ # every file in the directory
kubectl apply -f k8s/ --recursive # and subdirectories
kubectl apply -f https://example.com/manifest.yamlkubectl delete -f k8s/kubectl get all -l app=api
kubectl delete all -l app=api# The declarative loop
kubectl apply -f deployment.yaml # create or update, re-runnable
kubectl apply -f k8s/ # a whole directory
kubectl apply -f k8s/ --recursive # and its subdirectories
kubectl delete -f k8s/ # remove what the files describe
# Before you apply, against anything that matters
kubectl config current-context # the right cluster?
kubectl diff -f deployment.yaml # what would change
kubectl apply -f x.yaml --dry-run=server # would it validate
# Generating a manifest instead of writing one
kubectl create deployment api --image=myapp:1.4.2 \
--dry-run=client -o yaml > deployment.yaml
kubectl run web --image=nginx --dry-run=client -o yaml
kubectl get deployment api -o yaml # the live object, in full
# Finding out what a field is called
kubectl api-resources # kinds, short names, apiVersions
kubectl explain deployment.spec
kubectl explain pod.spec.containers --recursive
# The four fields in every manifest
# apiVersion v1 for core; apps/v1 for workloads
# kind Pod, Deployment, Service, ...
# metadata name, namespace, labels
# spec the desired state
# (status) written by the cluster; never by you
# Imperative commands: fine to explore, then put it in the file
kubectl scale deployment api --replicas=5
kubectl set image deployment/api api=myapp:1.4.3
kubectl edit deployment api # changes the cluster only
# Working with a group of objects
kubectl get all -l app=api
kubectl delete all -l app=api # not ConfigMaps/Secrets/PVCs