Sign in
← Kubernetes: basics
6 min · easy · Lesson 6 of 11

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

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

--dry-run=client kubectl assembles the object on your machine admission is not checked printed to the terminal nothing reached the cluster --dry-run=server kubectl assembles the object API server schema, admission, quotas object in the response not written to storage
Neither option creates anything, but their checks differ.

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

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-basics/explain-dry-run