Application Design and Build
Last verified against its sources on 23 September 2026
Application Design and Build covers the first 20% of the CKAD exam: turning source code into a container image, then picking the Kubernetes workload resource, container topology and storage that actually fits what the application needs to do.
None of these choices are exam trivia — the grader runs your manifest and checks the resulting state, so picking a Deployment where the task needs a DaemonSet, or a plain container where the task needs a native sidecar, produces a wrong answer even if every field inside it is spelled correctly.
Container Images and Workload Resources
- Build a container image with a multi-stage Dockerfile so the final image ships only what the running application needs.
- Choose between a Deployment, DaemonSet, Job or CronJob for a given application need.
A container image is a read-only, layered filesystem plus
metadata — the entry point, environment variables, exposed
ports — packaged to the OCI (Open Container Initiative) image
format that Docker, containerd and Kubernetes all understand.
You build one from a Dockerfile: each instruction (FROM,
RUN, COPY) adds a new, cached layer, and the builder only
redoes the layers from the line you changed onward, which is
why ordering matters — put the slow-changing dependency install
before the fast-changing source copy.
A multi-stage build uses more than one FROM in the same
Dockerfile so the final image ships only what the running
application needs, not the compiler, package manager cache, or
test framework that built it. The build stage installs
everything required to compile the code; the final stage starts
fresh from a minimal base image and copies across only the
compiled binary or the interpreted source plus its runtime
dependencies. A smaller image pulls faster, scans faster for
vulnerabilities, and gives an attacker fewer tools to work with
if the container is ever compromised.
dockerfile
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app .
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
ENTRYPOINT ["/app"]Kubernetes rarely runs a Pod on its own — instead you describe the Pod you want inside a workload resource, and a controller keeps that many Pods running. The CKAD exam expects you to pick the right one for a scenario rather than default to a Deployment out of habit.
A Deployment manages a set of interchangeable, stateless replicas and is the right default for anything that just needs "N copies running, roll updates in and out safely." A DaemonSet instead runs exactly one Pod on every node (or every node matching a selector) and adds or removes that Pod automatically as nodes join or leave — the fit for a log collector or a node-level metrics agent, where the requirement is "one per node," not "N copies." A Job runs a Pod to completion once — the Pod exits successfully and the Job is done, which suits a database migration or a one-off data import. A CronJob wraps a Job in a schedule, using standard cron syntax — the fit whenever the requirement is "run this on a timer," such as a nightly backup.
The question to ask is not "which resource have I used before" but "what does this Pod's lifecycle actually need": does it run forever and get replaced, does it need exactly one copy per node, does it run once to completion, or does it run on a schedule? Answering that correctly is worth more marks than memorising YAML syntax, because the exam grades the running result, not the command you typed to get there.
You need a log-shipping agent that runs on every node in the cluster, including new ones added later. Deployment or DaemonSet?Answer it yourself first, then open this.
DaemonSet — it places exactly one Pod per matching node and follows the node set automatically, which a Deployment's fixed replica count does not do.
Jobs and CronJobs
- Run a Pod to completion with a Job, controlling parallelism and retries.
- Schedule a recurring Job with a CronJob and control how overlapping runs are handled.
A Job, in the batch/v1 API group, runs one or more Pods
until a specified number of them complete successfully, then
stops. Its Pod template must set restartPolicy to OnFailure
or Never — Always, the default for an ordinary Pod, is
rejected, because a Job's Pod is meant to finish, not be kept
alive forever. Two fields shape how the work is spread out:
.spec.completions sets how many successful Pod completions the
Job needs before it is done, and .spec.parallelism caps how
many of those Pods run at once. .spec.backoffLimit sets how
many times a failing Pod is retried — with a fresh Pod created
for each attempt, not the same Pod restarted in place — before
the whole Job is marked Failed.
A CronJob wraps a Job template in a schedule, written in
standard cron syntax, and creates a new Job each time that
schedule fires. Because a CronJob's job is only to create Jobs
on time, everything you already know about a Job's
completions, parallelism and backoffLimit still applies to
the Job it creates.
bash
apiVersion: batch/v1
kind: Job
metadata:
name: report-export
spec:
completions: 3
parallelism: 1
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: export
image: reports:1.4The .spec.concurrencyPolicy field on a CronJob decides what
happens if a scheduled run's Job is still going when the next
scheduled time arrives. Allow, the default, lets both run at
once. Forbid skips the new run entirely if the previous one
is still active. Replace cancels the still-running Job and
starts the new one in its place.
.spec.startingDeadlineSeconds bounds how late a missed run can
still be started; if that many seconds pass after the scheduled
time with no Job created, the run is skipped rather than started
late. The CronJob controller itself only checks for due
schedules roughly every ten seconds, so setting the deadline
below that makes it practical for a run to be judged
already-missed before the controller gets a chance to start it.
A CronJob runs every 10 minutes with concurrencyPolicy Replace, and a run is still going when the next scheduled time arrives. What happens?Answer it yourself first, then open this.
The still-running Job is cancelled, and a new Job starts in its place for the new scheduled time.
Multi-Container Pod Design Patterns
- Use a regular init container to prepare a Pod before its main container starts.
- Use a native sidecar container to run a helper process alongside the main container for the Pod's whole lifetime.
All containers in one Pod share the same network namespace and
can share volumes, which is what makes multi-container Pods
useful for tightly coupled helper processes. Kubernetes
recognizes two built-in patterns for containers listed under
initContainers. A regular init container runs to
completion, in the order listed, before the next init container
or any main container starts — the fit for one-time setup such
as cloning a config repo into a shared volume. A native
sidecar container is also listed under initContainers, but
sets restartPolicy: Always; the kubelet starts it, waits for
it to report started (immediately, or once its startupProbe
succeeds), then moves on to the next init container or the main
containers — while the sidecar itself keeps running for the
rest of the Pod's life, independently restartable if it
crashes.
This sidecar mechanism has been stable and enabled by default
since Kubernetes v1.33, reached after an alpha release in
v1.28 and a beta period through v1.29–1.32. Before that
history, "sidecar" was purely a naming convention — a second
entry under the ordinary containers list, with no special
startup ordering or shutdown ordering relative to the main
container at all.
bash
apiVersion: v1
kind: Pod
metadata:
name: web-with-logs
spec:
initContainers:
- name: migrate-db
image: migrator:2.1
- name: log-shipper
image: log-shipper:1.0
restartPolicy: Always
containers:
- name: web
image: web-app:3.0Use a regular init container for anything that must happen exactly once before the app is reachable and then get out of the way — a database migration, a wait-for-dependency check, a config fetch. Use a native sidecar for anything that must keep running alongside the app for as long as it's up — a log shipper, a metrics exporter, a local proxy.
One nuance is worth knowing cold for the exam: a Job whose
Pod includes a long-running native sidecar is not blocked from
completing by that sidecar. Once every ordinary container in
the Pod has exited, Kubernetes sends the sidecar a termination
signal on its own, specifically so a Job with a sidecar can
still be marked Complete rather than hang forever waiting for
a helper process that was never meant to exit on its own.
A Job's Pod has a main container plus a native sidecar that runs forever. Does the sidecar block the Job from completing?Answer it yourself first, then open this.
No — once the main container finishes, Kubernetes sends the sidecar a termination signal automatically so the Job can still be marked Complete.
Persistent and Ephemeral Volumes
- Mount an ephemeral volume such as emptyDir to share data between containers in a Pod or hold scratch space.
- Claim persistent storage for a Pod with a PersistentVolumeClaim and choose an access mode that matches the workload.
Containers in a Pod have separate filesystems by default; a
volume is a directory made available to some or all of a
Pod's containers, decoupling storage from any single
container's lifetime. Volumes split into two lifetimes.
Ephemeral volumes, such as emptyDir, are created when the
Pod is assigned to a node and exist only as long as that Pod
does. Persistent volumes exist independently of any one
Pod: a PersistentVolume (PV) represents an actual piece of
storage, and a PersistentVolumeClaim (PVC) is a namespaced
request for storage that a Pod mounts by name, without needing
to know how or where that storage is actually provisioned.
For any volume type, data survives a container crash and restart within the same Pod — what changes its fate is whether the Pod itself goes away.
bash
apiVersion: v1
kind: Pod
metadata:
name: fetch-and-serve
spec:
containers:
- name: fetcher
image: fetcher:1.0
volumeMounts:
- name: shared
mountPath: /data
- name: server
image: nginx:1.27
volumeMounts:
- name: shared
mountPath: /usr/share/nginx/html
readOnly: true
volumes:
- name: shared
emptyDir: {}A PVC's accessModes field states how many nodes, and in what
mode, the underlying storage can be mounted at once:
ReadWriteOnce (RWO) allows read-write mounting by a single
node; ReadOnlyMany (ROX) allows many nodes to mount the same
volume read-only at the same time; ReadWriteMany (RWX) allows
many nodes to mount it read-write concurrently; and
ReadWriteOncePod (RWOP) is the strictest of the four,
restricting the volume to exactly one Pod cluster-wide rather
than one node. Not every storage backend supports every mode —
a cloud block-storage volume, for instance, typically only
supports RWO.
When a PVC names no storageClassName and the cluster has a
StorageClass marked default, Kubernetes provisions a new
PersistentVolume dynamically to satisfy the claim using that
default class, rather than requiring an administrator to have
pre-created one. Because a PersistentVolume is a cluster-scoped
object but a PersistentVolumeClaim is namespaced, a Pod can only
mount a PVC that lives in its own namespace.
Five read-only replicas need to mount the same volume at once, and none of them write to it. Which access mode fits?Answer it yourself first, then open this.
ReadOnlyMany (ROX) — it allows many nodes, and the Pods scheduled to them, to mount the same volume read-only concurrently.
Sources
- Kubernetes Documentation — Images
- Kubernetes Documentation — Workload Resources
- CNCF — Certified Kubernetes Application Developer (CKAD)
- Kubernetes Documentation — Job
- Kubernetes Documentation — CronJob
- Kubernetes Documentation — Sidecar Containers
- Kubernetes Documentation — Volumes
- Kubernetes Documentation — Persistent Volumes