Skip to content
CKAD: Certified Kubernetes Application Developer

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"]
The build stage compiles the binary; the final stage ships only that binary.

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.4
A Job template's Pod spec must not use the default restartPolicy.

The .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.0
A regular init container, a native sidecar, then the main container.

Use 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.

Startup order for a Pod with one regular init container and one native sidecar.
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: {}
Two containers sharing scratch space through an emptyDir volume.

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