Generating YAML with --dry-run=client
Nobody passes CKAD by typing `apiVersion: apps/v1` from memory sixteen times. Every object on the exam has an imperative generator that will print a correct skeleton, and the skill being tested is editing that skeleton, not recalling indentation. This is the generator for each kind you will be asked for, including the two that need the object to exist first.
Working at Exam Speed Guide 3 of 44 Beginner
- Kubernetes1.36.4
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 15 min
- Reviewed23 August 2026
Written against the versions above. `--dry-run=client` renders locally and never contacts the API server for validation, which is why it is instant and why it will happily generate a manifest that the server would reject. `--dry-run=server` validates properly and is worth using when a manifest is refused and you cannot see why.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA4001 | 192.168.0.191 | Ubuntu 26.04 LTS | Single Node (control plane, untainted) | 2 Core | 4 GB | 50 GB |
Before you start
- kubectl. Most of this guide never reaches the API server at all.
- The session creates a namespace
ckad-genand one Deployment, so that the two generators which need an existing object have one to work from.
-
Pod and Deployment, the two you will type most
kubectl rungenerates a Pod;kubectl create deploymentgenerates a Deployment. Both with--dry-run=client -o yaml:apiVersion: v1 kind: Pod metadata: name: web spec: containers: - image: nginx:alpine name: webThat is the shape you edit. Add a volume, add an env var, add a probe - but never type the four lines above.
The habit worth building now: always redirect to a file.
kubectl run web --image=nginx:alpine --dry-run=client -o yaml > pod.yaml, edit,kubectl apply -f pod.yaml. Editing a file is recoverable; getting half-way through a heredoc is not.bash Example session kubectl create namespace ckad-gennamespace/ckad-gen createdkubectl -n ckad-gen run web --image=nginx:alpine --dry-run=client -o yamlapiVersion: v1kind: Podmetadata: labels: run: web name: web namespace: ckad-genspec: containers: - image: nginx:alpine name: web resources: {} dnsPolicy: ClusterFirst restartPolicy: Alwaysstatus: {}kubectl -n ckad-gen create deployment api --image=nginx:alpine --replicas=3 --dry-run=client -o yaml | head -20apiVersion: apps/v1kind: Deploymentmetadata: labels: app: api name: api namespace: ckad-genspec: replicas: 3 selector: matchLabels: app: api strategy: {} template: metadata: labels: app: api spec: containers: - image: nginx:alpineExpected resultTwo valid manifests, printed instantly.
Success conditionYou have the two skeletons the majority of tasks start from.
-
Job, CronJob, ConfigMap, Secret
These four are where hand-typing costs the most, because each has a nesting level people get wrong under pressure - a Job's
spec.template.spec, a CronJob'sspec.jobTemplate.spec.template.spec.Note how a command is passed: everything after
--becomes the container'sargs. And note the Secret generator does the base64 for you:data: token: czNjcjN0Four generators, four correct manifests, no nesting recalled from memory. The CronJob one in particular is worth practising until it is reflex -
spec.jobTemplate.spec.template.spec.containersis four levels deep and it is where hand-written CronJobs fail.bash Example session kubectl -n ckad-gen create job pi --image=perl:5.34 --dry-run=client -o yaml -- perl -Mbignum=bpi -wle 'print bpi(200)'apiVersion: batch/v1kind: Jobmetadata: name: pi namespace: ckad-genspec: template: metadata: {} spec: containers: - command: - perl - -Mbignum=bpi - -wle - print bpi(200) image: perl:5.34 name: pi resources: {} restartPolicy: Neverstatus: {}kubectl -n ckad-gen create cronjob tick --image=busybox:1.36 --schedule='*/1 * * * *' --dry-run=client -o yaml -- /bin/sh -c dateapiVersion: batch/v1kind: CronJobmetadata: name: tick namespace: ckad-genspec: jobTemplate: metadata: name: tick spec: template: metadata: {} spec: containers: - command: - /bin/sh - -c - date image: busybox:1.36 name: tick resources: {} restartPolicy: OnFailure schedule: '*/1 * * * *'status: {}kubectl -n ckad-gen create configmap app --from-literal=MODE=prod --from-literal=TIMEOUT=30 --dry-run=client -o yamlapiVersion: v1data: MODE: prod TIMEOUT: "30"kind: ConfigMapmetadata: name: app namespace: ckad-genkubectl -n ckad-gen create secret generic api-key --from-literal=token=s3cr3t --dry-run=client -o yamlapiVersion: v1data: token: czNjcjN0kind: Secretmetadata: name: api-key namespace: ckad-genExpected resultFour manifests, with the Secret already base64-encoded.
Success conditionYou can produce the four most awkward kinds without recalling their nesting.
-
The two that need the object to exist first
kubectl exposeandkubectl create ingressboth reference something. Create the Deployment first, then generate against it:spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: apiThe selector was filled in for you from the Deployment's labels, which is the single most common thing to get wrong when writing a Service by hand - see the networking track for what a mismatched selector looks like.
The Ingress generator takes a compact rule string:
--rule='shop.example.com/*=api:80'and expands it into the nested
rules[].http.paths[].backend.servicestructure, withpathType: Prefixfilled in. That structure is genuinely hard to type correctly and you should never try.bash Example session kubectl -n ckad-gen create deployment api --image=nginx:alpine --replicas=3 --dry-run=client -o yaml | head -20apiVersion: apps/v1kind: Deploymentmetadata: labels: app: api name: api namespace: ckad-genspec: replicas: 3 selector: matchLabels: app: api strategy: {} template: metadata: labels: app: api spec: containers: - image: nginx:alpinekubectl -n ckad-gen expose deployment api --port=80 --target-port=80 --dry-run=client -o yamlapiVersion: v1kind: Servicemetadata: labels: app: api name: api namespace: ckad-genspec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: apistatus: loadBalancer: {}kubectl -n ckad-gen create ingress site --rule='shop.example.com/*=api:80' --dry-run=client -o yamlapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: site namespace: ckad-genspec: rules: - host: shop.example.com http: paths: - backend: service: name: api port: number: 80 path: / pathType: Prefixstatus: loadBalancer: {}Expected resultA Service with the selector filled in, and a fully expanded Ingress.
Success conditionYou never hand-write a Service selector or an Ingress path again.
-
What this is actually worth
kubectl ... --dry-run=client -o yaml > 0.02s user 0.01s system 35% cpu 0.088 totalEighty-eight milliseconds for fifteen lines of correct YAML. Typing those fifteen lines takes a competent person somewhere around a minute, and roughly one time in four they will produce something that does not parse.
Strip the boilerplate the generator adds -
creationTimestamp,status, emptyresources- and eleven lines remain. Those eleven lines are what you would have typed anyway.Across sixteen tasks, this is the difference between finishing and not. It is the highest-value habit on this exam and it takes an afternoon to build.
bash Example session time kubectl -n ckad-gen run t1 --image=nginx:alpine --dry-run=client -o yaml > /tmp/t1.yamlkubectl -n ckad-gen run t1 --image=nginx:alpine --dry-run=client -o yaml > 0.02s user 0.01s system 35% cpu 0.088 totalwc -l /tmp/t1.yaml15 /tmp/t1.yamlkubectl -n ckad-gen run t2 --image=nginx:alpine --dry-run=client -o yaml | grep -vE 'creationTimestamp|status|resources|dnsPolicy|restartPolicy|terminationGracePeriod|schedulerName|securityContext|serviceAccount' | wc -l11kubectl delete namespace ckad-gen --wait=falsenamespace "ckad-gen" deletedExpected resultSub-100ms generation, 15 lines down to 11 of substance.
Success conditionYou have a measured reason to stop typing YAML.
Troubleshooting
--dry-run=clientproduced a manifest the server rejects.Why: Client dry-run does no server-side validation at all.
Fix:Re-run with
--dry-run=server, which validates against the real API and admission chain.There is no generator for the object you need.
Why: Only a subset of kinds have imperative generators.
Fix:Generate the nearest kind that does and edit it, or use guide 13 to build the fields you need.
kubectl exposesays it cannot find the resource.Why:
exposereads an existing object to build the selector.Fix:Create the Deployment first, or write the Service by hand with a selector that matches the Pod labels exactly.