6 exam tips covering domain areas, tricky scenarios, and commands to memorize.
Certified Kubernetes Application Developer — designing and building apps
Explanation
The CKAD exam allows access to the official Kubernetes documentation, but navigating web pages takes time. A much faster alternative is kubectl explain, which gives you the field structure and descriptions for any Kubernetes resource directly in the terminal. You can drill down into nested fields by chaining them with dots — for example, kubectl explain pod.spec.containers.livenessProbe shows you exactly which fields are available for configuring liveness probes. This is invaluable when you cannot remember the exact field name or whether a property is a string, integer, or nested object. You can also use the --recursive flag to print the entire resource tree at a glance, which helps you understand the overall structure quickly. Make kubectl explain your first instinct whenever you are unsure about a field path. It is faster than searching docs, less error-prone than guessing, and it works offline. During practice, use it regularly so that navigating resource hierarchies becomes second nature during the actual exam.
Command / YAML
# Explain the top-level fields of a Pod resource kubectl explain pod # Drill into the spec of a pod kubectl explain pod.spec # Look up container-level fields like image and ports kubectl explain pod.spec.containers # Find the exact fields for a liveness probe kubectl explain pod.spec.containers.livenessProbe # Check the httpGet probe configuration fields kubectl explain pod.spec.containers.livenessProbe.httpGet # Show the full recursive tree for a Deployment kubectl explain deployment --recursive # Look up volume mount fields kubectl explain pod.spec.containers.volumeMounts # Check the fields for a PersistentVolumeClaim spec kubectl explain pvc.spec
Common Mistake
Candidates spend valuable minutes searching through the Kubernetes documentation website when kubectl explain would give them the exact field path and type in seconds. They also forget the --recursive flag, which prints the entire resource schema tree for quick scanning.
💬 Comments
Explanation
The CKAD exam frequently tests your ability to work with multi-container pods, including sidecar containers, init containers, and ambassador patterns. An init container runs to completion before the main application container starts and is commonly used for setup tasks like waiting for a database to be ready, populating a shared volume, or downloading configuration files. A sidecar container runs alongside the main container for the entire pod lifecycle and typically handles logging, monitoring, or proxying. The ambassador pattern uses a sidecar container that proxies network connections from the main container to external services. When designing multi-container pods, understand that all containers in a pod share the same network namespace (localhost) and can share volumes. Init containers are defined under spec.initContainers while regular sidecar containers go under spec.containers. Practice creating pods with multiple containers and shared volumes, as the YAML can get long and errors in indentation or volume mount paths are common under exam pressure.
Command / YAML
# Multi-container pod with init container and sidecar
apiVersion: v1
kind: Pod
metadata:
name: multi-container-app # Pod name
labels:
app: webapp # Label for identification
spec:
initContainers: # Init containers run before main containers
- name: init-config # Name of the init container
image: busybox:1.36 # Lightweight image for setup tasks
command: ['sh', '-c'] # Run a shell command
args: # Arguments for the command
- 'echo "Initializing..." && wget -O /work-dir/config.json http://config-svc:8080/config'
volumeMounts: # Mount shared volume to exchange data
- name: shared-data # Reference the shared volume by name
mountPath: /work-dir # Mount path inside the init container
containers: # Main application containers
- name: app # Primary application container
image: nginx:1.25 # Application image
ports:
- containerPort: 80 # Port the app listens on
volumeMounts: # Mount the same shared volume
- name: shared-data # Same volume name as init container
mountPath: /usr/share/nginx/html # Where app reads the data
- name: log-sidecar # Sidecar container for log forwarding
image: busybox:1.36 # Lightweight sidecar image
command: ['sh', '-c'] # Run a tail command to stream logs
args:
- 'tail -f /var/log/nginx/access.log' # Stream access logs
volumeMounts: # Mount the log directory
- name: log-volume # Reference the log volume
mountPath: /var/log/nginx # Path to nginx logs
volumes: # Define shared volumes
- name: shared-data # Volume shared between init and app containers
emptyDir: {} # Ephemeral volume that exists for pod lifetime
- name: log-volume # Volume shared between app and sidecar
emptyDir: {} # Another ephemeral volume for logsCommon Mistake
Candidates put init containers under spec.containers instead of spec.initContainers, which causes them to run as regular containers rather than initialization containers. They also forget that init containers run sequentially and must exit successfully before the main containers start.
💬 Comments
Explanation
CronJobs and Jobs are regularly tested on the CKAD exam, and you need to be comfortable both creating them and troubleshooting failures. A CronJob creates Job objects on a recurring schedule using standard cron syntax. Key fields to memorize include spec.schedule for the cron expression, spec.jobTemplate for the Job template, spec.successfulJobsHistoryLimit and spec.failedJobsHistoryLimit for controlling how many completed and failed Jobs are retained, and spec.concurrencyPolicy for controlling whether concurrent Jobs are allowed. When debugging a failed Job, start by checking the Job status with kubectl get jobs, then examine the pod logs with kubectl logs, and use kubectl describe to look for events like image pull failures or resource limits. The restartPolicy for Jobs must be set to Never or OnFailure — setting it to Always will cause a validation error. Also know that spec.backoffLimit controls how many times a failed Job retries before being considered permanently failed. Practice creating CronJobs imperatively with kubectl create cronjob and then modifying the generated YAML for more complex configurations.
Command / YAML
# Create a CronJob imperatively that runs every 5 minutes kubectl create cronjob log-cleanup --image=busybox:1.36 --schedule="*/5 * * * *" -- /bin/sh -c "echo Cleaning logs at $(date)" # Generate CronJob YAML for editing without creating it kubectl create cronjob db-backup --image=postgres:16 --schedule="0 2 * * *" --dry-run=client -o yaml > cronjob.yaml # List all jobs created by a cronjob kubectl get jobs --selector=job-name -n default # Check the logs of the most recent job pod kubectl logs job/log-cleanup-28100000 # Describe a job to see failure events kubectl describe job log-cleanup-28100000 # Debug a failed pod from a job kubectl get pods --selector=job-name=log-cleanup-28100000 --show-labels # Manually trigger a job from a cronjob for testing kubectl create job test-backup --from=cronjob/db-backup
Common Mistake
Candidates set restartPolicy to Always in a Job or CronJob pod template, which causes a validation error. Jobs require restartPolicy to be either Never or OnFailure. They also forget to check pod logs and jump straight to kubectl describe, missing the actual application error message.
💬 Comments
Explanation
CKAD tasks often involve adding a sidecar container for logging, proxying, or data sharing. Remember these key principles: all containers in a Pod share the same network namespace (so they can communicate via `localhost`) and can share storage volumes. A long-running sidecar (like a log forwarder) belongs in the `spec.containers` array alongside the main application container. They start in parallel. To share files, define a volume (e.g., `emptyDir`) in `spec.volumes` and mount it into both containers using `volumeMounts`. **Exam Trap**: Using a different `name` for the `volumeMounts` in each container, which prevents them from sharing the same volume. Another trap is putting a long-running process in an `initContainer`, which will block the main container from ever starting.
Command / YAML
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
# Both containers are defined in the 'containers' array
containers:
- name: main-app
image: nginx:1.25
volumeMounts:
# Mount the shared volume
- name: shared-logs
mountPath: /var/log/nginx
- name: log-collector-sidecar
image: busybox:1.36
# This sidecar runs continuously
command: ["sh", "-c", "tail -f /var/log/nginx/access.log"]
volumeMounts:
# Mount the same shared volume by name
- name: shared-logs
mountPath: /var/log/nginx
# The shared volume is defined once for the Pod
volumes:
- name: shared-logs
emptyDir: {}Common Mistake
Putting a long-running sidecar in initContainers, or using different volume names/mount paths so containers cannot see the same files.
💬 Comments
Explanation
Init containers are for setup tasks that must complete *before* the main application container starts. They are defined in `spec.initContainers` and run sequentially in the order they are listed. Common use cases include: waiting for a database to be available, downloading configuration files, or running database migrations. If any init container fails, the kubelet restarts it according to the pod's `restartPolicy`. The main containers will not start until all init containers have succeeded. **Exam Trap**: Forgetting that `command` and `args` in YAML must be specified as a list of strings. A common task is to wait for a service, which requires a shell command like `['sh', '-c', 'until nslookup db-service; do sleep 1; done']`. Another trap is misconfiguring the shared volume, so the main container cannot access the files prepared by the init container.
Command / YAML
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
# This container runs first
- name: wait-for-db
image: busybox:1.36
# Use 'sh -c' for shell commands
command: ['sh', '-c', 'until nslookup mysql-svc; do echo waiting for db; sleep 2; done;']
# This container runs second, after the first one succeeds
- name: setup-files
image: busybox:1.36
command: ['sh', '-c', 'echo "Initialized" > /work-dir/status.txt']
volumeMounts:
- name: workdir
mountPath: /work-dir
containers:
- name: main-app
image: my-app:1.0
volumeMounts:
- name: workdir
mountPath: /data
volumes:
- name: workdir
emptyDir: {}Common Mistake
Placing setup containers in spec.containers instead of spec.initContainers, or omitting shared volumeMounts so the app never sees files the init step wrote.
💬 Comments
Explanation
Understanding the three probe types is essential for CKAD. - **`readinessProbe`**: Should this pod receive traffic? If it fails, the pod is removed from the Service's endpoints. Use this to check for dependency health or cache warming. - **`livenessProbe`**: Is this container still alive? If it fails, the container is restarted. Use this only for detecting deadlocks, not for temporary issues. - **`startupProbe`**: For slow-starting applications (like Java apps), this probe runs first and disables the liveness probe until it succeeds. This prevents the liveness probe from killing the container before it has a chance to start. **Exam Trap**: Using a liveness probe on a slow-starting app without a startup probe, which leads to a `CrashLoopBackOff` state as the container is killed repeatedly. Another common mistake is using the same deep health check for both liveness and readiness, causing transient dependency issues to trigger unnecessary container restarts.
Command / YAML
apiVersion: v1
kind: Pod
metadata:
name: probed-app
spec:
containers:
- name: app
image: my-java-app:1.0
ports:
- containerPort: 8080
name: http
# For slow-starting apps, use a startupProbe to delay the liveness probe
startupProbe:
httpGet:
path: /healthz
port: http
failureThreshold: 30 # Allows up to 150s (30*5) for startup
periodSeconds: 5
# Readiness: Is the app ready for traffic? (checks dependencies)
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 5
# Liveness: Is the app process deadlocked? (lightweight check)
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15 # Starts after startupProbe succeeds
periodSeconds: 10Common Mistake
Using a liveness probe on an endpoint that is slow to become ready, causing restart loops instead of adding a startupProbe or readinessProbe.
💬 Comments