kubectl explain and dry-run
How to get a ready manifest skeleton with one command and find the field you need without internet access.
You've already written a Pod manifest by hand. That's rare in practice: kubectl itself prints a manifest skeleton, and its built-in documentation tells you the name of the field you need. This lesson covers both commands and how a check on the client differs from a check on the server.
Concepts
- Manifest - a YAML file describing a Kubernetes object.
- Dry-run - a mode where kubectl or the API server assembles and checks the object, but doesn't create it.
- Admission - the checks the API server runs before writing an object: security policies, quotas, webhooks.
- ResourceQuota - a namespace object that limits how much of a resource (Pods, CPU, memory) can be used in that namespace.
Why you need dry-run
A manifest is a YAML file that describes a Kubernetes object. Writing it from scratch by hand is slow, and indentation is easy to get wrong.
The usual order is different. You generate a skeleton first, open it in
an editor, add the fields you need, then apply it. The skeleton comes from
kubectl, the command-line tool for managing the cluster.
--dry-run is a flag that tells kubectl to assemble the object, but not
create it.
kubectl run skeleton --image=nginx:1.27.3-alpine --dry-run=client -o yaml
nginx:1.27.3-alpine is here for a short, simple example. In production
you'll often see nginxinc/nginx-unprivileged:1.27.3-alpine: it runs
without root, which the restricted profile from the Pod Security
Standards requires.
--dry-run=client means: kubectl assembles the object on your machine
and prints it to the terminal. No request goes to the cluster, no Pod
appears.
-o yaml sets the output format to YAML. Without this flag kubectl
prints a short line instead of a manifest:
pod/skeleton created (dry run)
You can redirect the output straight to a file:
kubectl run skeleton --image=nginx:1.27.3-alpine --dry-run=client -o yaml > /tmp/skeleton.yaml
The file comes out valid, you can apply it with kubectl apply -f. People
usually add by hand what kubectl run can't generate: probes, resources,
volumes, command.
Subcommands of kubectl create give skeletons for other objects: kubectl create deployment, kubectl create configmap, kubectl create service clusterip, and more. kubectl create --help prints the full list.
client and server dry-run
The flag has two values.
--dry-run=client runs on your machine. kubectl assembles the object
itself and prints it, nothing goes to the server. It's fast.
--dry-run=server sends the object to the API server. The server runs
the full check: schema, admission, quotas, security policies. But the
object isn't saved to the cluster's storage. The response is the object
as it would look after being saved:
kubectl run skeleton --image=nginx:1.27.3-alpine --dry-run=server -o yaml
This output already has everything the server would add: namespace,
uid, creationTimestamp, imagePullPolicy, schedulerName, and other
default values. Without -o yaml the response is short:
pod/skeleton created (server dry run)
The server variant is stricter. It needs cluster access and permission to
create the object. For generating skeletons, client is enough, and before
apply it's worth checking the finished file with server:
kubectl apply -f /tmp/skeleton.yaml --dry-run=server
--dry-run=client doesn't check admission: security policies, quotas,
webhooks. It doesn't even catch a typo in a field name. A manifest that
passes the client check can still fail on the server.
For example, say the manifest has a typo: imagee instead of image.
Client dry-run answers pod/typo created (dry run), while server dry-run
rejects the file:
Error from server (BadRequest): error when creating "typo.yaml": Pod in version "v1" cannot be handled as a Pod: strict decoding error: unknown field "spec.containers[0].imagee"
kubectl explain
kubectl explain is the built-in documentation for object fields. It
doesn't need the internet, but it does need cluster access: kubectl gets
the field schema from the API server. That's why explain shows the fields
of the exact Kubernetes version you're connected to.
kubectl explain pod
The command shows which fields the Pod object has. You can go deeper
with a dot:
kubectl explain pod.spec
kubectl explain pod.spec.containers
kubectl explain pod.spec.containers.resources
Each field in the output has a type listed: <string>, <integer>,
<Object>, <[]Container> (a list), and so on. The -required- mark
means the object won't be accepted without that field.
The --recursive flag prints every nested field at once, with no
descriptions:
kubectl explain pod.spec.containers --recursive | head -30
This is handy when you've forgotten the exact field name but remember roughly where it lives.
What kubectl run writes into the skeleton
kubectl run skeleton --image=nginx:1.27.3-alpine --dry-run=client -o yaml
prints this (kubectl v1.36):
apiVersion: v1
kind: Pod
metadata:
labels:
run: skeleton
name: skeleton
spec:
containers:
- image: nginx:1.27.3-alpine
name: skeleton
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
This output has extra fields. status: {} and the empty resources: {}
are noise. The API server ignores them, so you don't have to delete them.
But a clean file is nicer in git, so people usually clean them up by
hand. Older kubectl versions also added creationTimestamp: null, which
gets removed the same way.
The run: skeleton label is added automatically. A label is a key-value
pair objects use to find each other.
restartPolicy: Always is the default value for a Pod, kubectl just
wrote it out explicitly.
kubectl writes the items of the containers list with no extra indent
(- image: sits directly under containers:). For YAML this is the same
as the variant with a two-space indent.
Common mistakes
Forgetting -o yaml. Instead of a manifest, the file gets one line:
pod/skeleton created (dry run).
Treating client dry-run as a full check. It knows nothing about quotas,
namespace policies, or even typos in field names. Run --dry-run=server
before apply into a shared cluster.
Looking for kubectl create pod. There's no such subcommand, kubectl run is what generates a Pod. There's no generator for StatefulSet or
DaemonSet either: take the Deployment skeleton and change kind and the
extra fields.
Writing --dry-run with no value. kubectl now reads it as client and
prints a warning, --dry-run is deprecated and can be replaced with --dry-run=client. Write the value explicitly.
Running the generator with no cluster configured. kubectl create deployment ... --dry-run=client still works, but kubectl run ... --dry-run=client and kubectl explain without API server access fail
with The connection to the server ... was refused.
Commands to practice
# Pod skeleton into a file
kubectl run skeleton --image=nginx:1.27.3-alpine --dry-run=client -o yaml > /tmp/skeleton.yaml
cat /tmp/skeleton.yaml
# Check the file on the server without creating the object
kubectl apply -f /tmp/skeleton.yaml --dry-run=server
# What's in spec.containers
kubectl explain pod.spec.containers | head -30
# Same tree, but flat
kubectl explain pod.spec.containers --recursive | head -30
# Deployment skeleton
kubectl create deployment web --image=nginx:1.27.3-alpine --replicas=3 --dry-run=client -o yaml
Homework
Practice the theory on your own cluster: 3 steps with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.
./run.sh start k8s-kubectl-explain-dryrun # set up the starting state
./run.sh verify k8s-kubectl-explain-dryrun # check the current step
In short
--dry-run=client -o yamlprints a ready-made manifest without creating anything in the cluster.clientassembles the object locally,serverruns it through all of the API server's checks (schema, admission, quotas), but doesn't save it.- The output gets written to a file, then gets what the generator can't produce: probes, resources, volumes, command.
- The skeleton keeps noise:
status: {}, emptyresources: {}, andstrategy: {}. kubectl explain pod.spec.containersshows the object's fields with no internet, but with cluster access. The--recursiveflag expands the whole tree.
What you'll need for the tasks
- Docker, kind v0.31.0 or newer, kubectl v1.35 or v1.36. Each lab's
setup/up.shbrings up its own cluster. kubectl create deployment --help: what flags the generator has.--dry-run=client -o yamland redirecting output to a file with> file.yaml.kubectl apply -f file.yaml --dry-run=serverbefore the realapply.kubectl explainwith a dot path, for exampledeployment.spec.- Any text editor for YAML (
nano, VS Code).
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-basics/explain-dry-run
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress