🗂️ Namespaces & RBAC INTERMEDIATE CKAD
A fresh cluster has one flat space where everything lives together. In production, teams, environments, and security boundaries demand isolation. Namespaces and RBAC are how you build that.
A Namespace is a virtual partition inside a single Kubernetes cluster. Resources inside one namespace are isolated from resources in another — you can have a Service named web in namespace: dev and a completely separate Service also named web in namespace: production, with no conflicts.
Namespaces are how teams share a cluster without stepping on each other. Common patterns:
- By environment:
dev,staging,production— same cluster, isolated environments - By team:
team-payments,team-auth,team-notifications - By application:
app-frontend,app-backend,monitoring
K8s ships with four built-in namespaces: default (where resources go if you don't specify), kube-system (K8s own components), kube-public (publicly readable resources), and kube-node-lease (node heartbeat objects).
# Create namespaces kubectl create namespace dev kubectl create namespace production # Or declaratively: # apiVersion: v1 # kind: Namespace # metadata: # name: dev # Deploy into a specific namespace with -n flag kubectl apply -f deployment.yaml -n production # List pods in a specific namespace kubectl get pods -n production # List pods across ALL namespaces kubectl get pods --all-namespaces kubectl get pods -A # shorthand # Set a default namespace for your session (so you stop typing -n every time) kubectl config set-context --current --namespace=production # Check which namespace is currently active kubectl config view --minify | grep namespace # Cross-namespace DNS: reach a Service in another namespace # Format: <service-name>.<namespace>.svc.cluster.local # Example: curl http://api-service.production.svc.cluster.local
By default, anyone with cluster access can do anything. RBAC lets you define precisely who (users, service accounts) can do what (verbs: get, list, create, delete) on which resources (pods, deployments, secrets) in which namespaces.
RBAC has four core objects:
- Role — A set of permissions scoped to a single namespace. "In namespace
dev, allow listing Pods." - ClusterRole — Same as Role, but applies cluster-wide across all namespaces. Used for cluster-level resources (Nodes, PersistentVolumes) that have no namespace.
- RoleBinding — Grants a Role to a user or ServiceAccount within a namespace.
- ClusterRoleBinding — Grants a ClusterRole to a user or ServiceAccount cluster-wide.
# A Role that allows READ-ONLY access to Pods and logs in the "dev" namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: pod-reader namespace: dev rules: - apiGroups: [""] # "" = core API group (pods, services, configmaps) resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] --- # Bind the Role to a ServiceAccount named "ci-runner" in the same namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-pods-binding namespace: dev subjects: - kind: ServiceAccount name: ci-runner namespace: dev roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io
Every Pod that needs to call the Kubernetes API (e.g., a CI tool, a custom controller, a monitoring agent) needs a ServiceAccount. A ServiceAccount is K8s's identity system for processes — the equivalent of a user account for a Pod.
Every namespace has a default ServiceAccount, but best practice is to create dedicated ServiceAccounts with the minimum permissions needed for each workload. The ServiceAccount's token is automatically mounted into the Pod at /var/run/secrets/kubernetes.io/serviceaccount/token.
# Check what the current user is allowed to do kubectl auth can-i create pods kubectl auth can-i delete deployments -n production # Check what a specific ServiceAccount can do (impersonate it) kubectl auth can-i list pods \ --as=system:serviceaccount:dev:ci-runner # List all Role and ClusterRole bindings kubectl get rolebindings -A kubectl get clusterrolebindings # Create a ServiceAccount kubectl create serviceaccount ci-runner -n dev # Quick RBAC: give a ServiceAccount edit access to a namespace kubectl create rolebinding ci-editor \ --clusterrole=edit \ --serviceaccount=dev:ci-runner \ -n dev # Built-in ClusterRoles you can reuse: # view — read-only access to most resources # edit — read-write, no RBAC or secrets changes # admin — full namespace control # cluster-admin — superuser, full cluster control
The exam often gives you a scenario: "Create a ServiceAccount named deployer in namespace apps. Give it permission to create and delete Deployments." You'll need to create the ServiceAccount, write a Role with the right verbs and resources, and bind them with a RoleBinding — all from memory in the terminal. Practice until this is automatic.
kubectl auth can-i delete pods --as=system:serviceaccount:dev:ci-runner do?📈 Horizontal Pod Autoscaler INTERMEDIATE CKAD
Manually running kubectl scale is not a production strategy. The Horizontal Pod Autoscaler watches real metrics and scales your Deployment automatically — up when load increases, down when it drops.
The Horizontal Pod Autoscaler (HPA) is a control loop that runs every 15 seconds. It queries the Metrics Server for current resource utilization, compares it against your target, and adjusts the replica count on the target Deployment (or StatefulSet) accordingly.
The formula K8s uses to calculate the desired replica count:
# desiredReplicas = ceil( currentReplicas × (currentMetricValue / desiredMetricValue) ) # Example: target 50% CPU, currently 3 replicas running at 80% average CPU # desiredReplicas = ceil( 3 × (80 / 50) ) = ceil(4.8) = 5 replicas # Another example: 5 replicas running at 20% CPU, target 50% # desiredReplicas = ceil( 5 × (20 / 50) ) = ceil(2.0) = 2 replicas → scale DOWN
The HPA respects the minReplicas and maxReplicas bounds you set — it will never scale below the minimum or above the maximum.
The HPA depends on the Metrics Server being installed in your cluster. It collects CPU and memory data from the kubelet on each node. On minikube:
minikube addons enable metrics-server. On cloud clusters (EKS, GKE, AKS) it's usually pre-installed. Without it, kubectl top pods won't work and HPAs will show <unknown> for current metrics.apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web-deployment # The Deployment to autoscale minReplicas: 2 # Never go below 2 (for availability) maxReplicas: 20 # Never exceed 20 (cost ceiling) metrics: # Scale when average CPU across all pods exceeds 60% - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 # Also scale when average memory exceeds 512Mi per pod - type: Resource resource: name: memory target: type: AverageValue averageValue: 512Mi behavior: scaleDown: stabilizationWindowSeconds: 300 # Wait 5min before scaling down (prevents flapping) policies: - type: Pods value: 2 # Remove at most 2 pods per minute periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 # Scale up immediately — don't wait policies: - type: Percent value: 100 # Can double replica count per 15s periodSeconds: 15
# Create an HPA imperatively (quick way — good for the CKAD exam) kubectl autoscale deployment web-deployment \ --min=2 --max=20 --cpu-percent=60 # Watch the HPA in real time (see current vs desired replicas) kubectl get hpa -w # Output columns: # NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS # web-hpa Deployment/web 45%/60% 2 20 3 # See resource usage per pod (requires Metrics Server) kubectl top pods -n production kubectl top nodes # Generate load to test autoscaling (run in a separate terminal) kubectl run load-generator \ --image=busybox --rm -it --restart=Never -- \ /bin/sh -c "while true; do wget -q -O- http://web-service; done" # Watch pods scale up in another terminal kubectl get pods -w
resources.requests.cpu set, the HPA has nothing to calculate a percentage against and will show <unknown>. Always set resource requests on any Deployment you intend to autoscale.
⚙️ Jobs & CronJobs INTERMEDIATE CKAD
Deployments keep Pods running forever. But some workloads need to run once and finish — database migrations, report generation, data exports. That's what Jobs are for. CronJobs schedule them on a timer.
A Job creates one or more Pods and ensures they complete successfully. Unlike a Deployment (which restarts Pods if they exit), a Job considers a Pod "done" when its container exits with code 0. If it fails (non-zero exit), the Job retries it up to backoffLimit times.
Jobs are perfect for:
- Database schema migrations before a new app version rolls out
- One-off data processing or ETL tasks
- Sending a batch of emails or notifications
- Generating a report and uploading it to cloud storage
- Seeding a database with initial data
apiVersion: batch/v1 kind: Job metadata: name: db-migration-v2 spec: completions: 1 # How many pods must succeed (default 1) parallelism: 1 # How many pods to run simultaneously backoffLimit: 4 # Retry failed pods up to 4 times before Job fails activeDeadlineSeconds: 300 # Kill the Job if it hasn't finished in 5 minutes ttlSecondsAfterFinished: 3600 # Auto-delete the Job 1 hour after it completes template: spec: restartPolicy: OnFailure # REQUIRED for Jobs — Never or OnFailure only containers: - name: migrate image: myapp:v2.0 command: ["python", "manage.py", "migrate"] env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-credentials key: url
A CronJob creates Jobs on a schedule defined by a cron expression — the same syntax used by Unix cron. The CronJob controller creates a new Job object at each scheduled time, which in turn creates the Pod.
Cron expression format: minute hour day-of-month month day-of-week
0 2 * * *— Every day at 2:00 AM*/15 * * * *— Every 15 minutes0 9 * * 1-5— Weekdays at 9:00 AM0 0 1 * *— First day of every month at midnight
apiVersion: batch/v1 kind: CronJob metadata: name: nightly-report spec: schedule: "0 2 * * *" # Run every day at 2:00 AM UTC timeZone: "America/Chicago" # K8s 1.27+ supports timezone spec concurrencyPolicy: Forbid # Don't start a new job if the previous is still running successfulJobsHistoryLimit: 3 # Keep last 3 successful Job objects failedJobsHistoryLimit: 1 # Keep last 1 failed Job for debugging startingDeadlineSeconds: 120 # Skip if can't start within 2min of scheduled time jobTemplate: spec: backoffLimit: 2 template: spec: restartPolicy: OnFailure containers: - name: report-generator image: myapp:v2.0 command: ["python", "generate_report.py", "--upload-s3"]
# Apply and watch a Job run to completion kubectl apply -f job.yaml kubectl get jobs -w # See which pods the Job created kubectl get pods --selector=job-name=db-migration-v2 # Read the logs from the completed job pod kubectl logs job/db-migration-v2 # Manually trigger a CronJob immediately (useful for testing) kubectl create job manual-run --from=cronjob/nightly-report # List CronJobs and their last schedule time kubectl get cronjobs # Suspend a CronJob (stops future runs without deleting it) kubectl patch cronjob nightly-report -p '{"spec":{"suspend":true}}'
Jobs and CronJobs appear on the exam regularly. Know the three concurrencyPolicy values cold: Allow (default — concurrent runs allowed), Forbid (skip new run if previous still running), and Replace (kill the running job and start a new one). Also remember that restartPolicy: Always is not valid for Jobs — only OnFailure or Never.
backoffLimit: 3. What happens?🔥 Network Policies INTERMEDIATE CKAD
By default, every Pod in a Kubernetes cluster can talk to every other Pod — no firewall, no restrictions. In production, that's a security disaster. Network Policies are K8s's firewall rules.
Without any Network Policies, a compromised Pod can reach your database, your internal APIs, your secret stores — anything in the cluster. Most K8s security incidents involve lateral movement: attacker compromises one Pod, then pivots to others freely. Network Policies stop this cold.
A NetworkPolicy selects a group of Pods (via label selectors) and defines rules for their ingress (incoming) and egress (outgoing) traffic. Once a Pod is selected by at least one NetworkPolicy, all traffic that isn't explicitly allowed is denied.
Important: Network Policies are enforced by the CNI plugin (Container Network Interface) — the networking layer of your cluster. Not all CNI plugins support Network Policies. Calico, Cilium, and Weave do. The default kubenet on many managed clusters does not — check your cluster's CNI before relying on them.
# POLICY 1: Default-deny all ingress in the production namespace # Apply this FIRST, then selectively open traffic below apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress namespace: production spec: podSelector: {} # Empty selector = select ALL pods in this namespace policyTypes: - Ingress # No ingress rules defined = deny all ingress --- # POLICY 2: Only allow the web Pods to reach the database Pods apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-web-to-db namespace: production spec: podSelector: matchLabels: app: database # This policy applies to database pods policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: web # Only Pods labeled app=web can connect ports: - protocol: TCP port: 5432 # Only on the PostgreSQL port --- # POLICY 3: Allow ingress from a specific namespace (e.g., monitoring) apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-monitoring namespace: production spec: podSelector: {} # All pods in production policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring # From the monitoring namespace
kubectl run test --image=busybox --rm -it -n production -- wget -qO- --timeout=3 http://database-service:5432. If it times out, the policy is working. Also useful: the Cilium Network Policy Editor at networkpolicy.io — a visual tool for building and understanding policies.
podSelector: {} and policyTypes: [Ingress] but no ingress rules. What is the effect on all Pods in that namespace?🐘 StatefulSets — Ordered, Stable, Persistent INTERMEDIATE CKAD
Deployments treat all their Pods as identical and interchangeable. StatefulSets give each Pod a permanent identity — a stable name, a stable network hostname, and its own dedicated persistent storage that follows it across rescheduling.
When you run PostgreSQL in a Deployment and the Pod restarts, a few things go wrong:
- The Pod gets a random new name (
postgres-7d8f9b) — breaking any peer discovery in a cluster - The Pod gets a new IP — breaking connection strings that somehow relied on it
- If you scaled to 3 replicas, all three would try to write to the same storage, corrupting the database
StatefulSets solve these problems by guaranteeing three things that Deployments don't:
- Stable, predictable Pod names:
postgres-0,postgres-1,postgres-2— always, across restarts - Stable network identity: Each Pod gets its own DNS entry:
postgres-0.postgres-service.namespace.svc.cluster.local - Dedicated per-Pod PVCs: Each Pod gets its own PersistentVolumeClaim that travels with it.
postgres-0always getsdata-postgres-0, not a shared volume.
# A Headless Service is required — it provides the stable DNS entries # ClusterIP: None means no load balancing — each pod gets its own DNS record apiVersion: v1 kind: Service metadata: name: postgres spec: clusterIP: None # Headless! No load balancing. selector: app: postgres ports: - port: 5432 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres spec: serviceName: "postgres" # Must match the Headless Service name above replicas: 3 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:16-alpine ports: - containerPort: 5432 env: - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-secret key: password volumeMounts: - name: data mountPath: /var/lib/postgresql/data # volumeClaimTemplates creates a dedicated PVC per Pod automatically! volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: standard resources: requests: storage: 10Gi # Result: creates data-postgres-0, data-postgres-1, data-postgres-2
⚖️ Resource Quotas & LimitRanges INTERMEDIATE CKAD
Resource limits on individual Pods prevent one container from consuming too much. Quotas and LimitRanges enforce those rules at the namespace level — preventing entire teams from consuming more than their fair share of the cluster.
A ResourceQuota sets hard limits on the total amount of resources that can be consumed by all objects in a namespace. If a new Pod would push the namespace over its quota, the API server rejects it.
Quotas can cap: CPU requests and limits, memory requests and limits, number of Pods, number of Services, number of PVCs, total storage requested, and more.
apiVersion: v1 kind: ResourceQuota metadata: name: team-payments-quota namespace: team-payments spec: hard: # Compute resources (requests = guaranteed, limits = maximum) requests.cpu: "4" # Total CPU requests across all pods: 4 cores requests.memory: 8Gi # Total memory requests: 8 GiB limits.cpu: "8" # Total CPU limits: 8 cores limits.memory: 16Gi # Object count limits pods: "20" # Max 20 pods in this namespace services: "10" persistentvolumeclaims: "5" # Storage limits requests.storage: 50Gi # Total storage across all PVCs
# See current quota usage vs limits for a namespace kubectl describe resourcequota -n team-payments # Output: # Name: team-payments-quota # Namespace: team-payments # Resource Used Hard # -------- ---- ---- # limits.cpu 3 8 # limits.memory 6Gi 16Gi # pods 7 20 # requests.cpu 1500m 4 # requests.memory 3Gi 8Gi # requests.storage 20Gi 50Gi
A LimitRange operates at the individual resource level within a namespace. It defines:
- Default requests and limits — applied automatically to containers that don't specify them. This prevents the
<unknown>HPA problem from the Autoscaling lesson. - Minimum and maximum bounds — a container can't request less than the min or more than the max. Prevents both resource starvation and runaway resource claims.
- Max limit-to-request ratio — prevents Pods from requesting 100m CPU but claiming a 64-core limit.
apiVersion: v1 kind: LimitRange metadata: name: default-limits namespace: team-payments spec: limits: - type: Container default: # Applied if container has no limits set cpu: 500m memory: 256Mi defaultRequest: # Applied if container has no requests set cpu: 100m memory: 128Mi min: # Container must request at least this much cpu: 50m memory: 64Mi max: # Container cannot claim more than this cpu: "2" memory: 2Gi maxLimitRequestRatio: cpu: 4 # Limit can be at most 4× the request - type: PersistentVolumeClaim min: storage: 1Gi max: storage: 20Gi # No single PVC can claim more than 20Gi
pods: "5" and currently has 5 running Pods. A developer applies a new Deployment with 2 replicas. What happens?🪆 Init & Sidecar Containers INTERMEDIATE CKAD
Multi-container Pods unlock powerful patterns. Init containers run setup tasks before your app starts. Sidecar containers extend it without modifying the app image itself.
Init containers run to completion before any of the main containers in a Pod start. They run in order — init-1 must finish before init-2 starts, init-2 before the main container starts. If an init container fails, the whole Pod restarts.
This ordering guarantee solves a class of race conditions that are extremely hard to handle otherwise:
- Database migrations — wait for the DB to be ready, run migrations, then start the app
- Dependency checking — wait for another service to be available before starting
- Configuration injection — fetch a TLS cert or secret from Vault, write it to a shared volume, then the main container reads it
- File permission setup — chown a mounted volume directory before the app (running as non-root) tries to write to it
apiVersion: v1 kind: Pod metadata: name: app-with-init spec: initContainers: # Init 1: Wait until PostgreSQL is ready to accept connections - name: wait-for-db image: busybox command: ['sh', '-c', 'until nc -z postgres-service 5432; do echo waiting for db; sleep 2; done'] # Init 2: Run database migrations (only after DB is ready) - name: run-migrations image: myapp:v2.0 command: ["python", "manage.py", "migrate"] env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secret key: url containers: # Main container only starts after BOTH init containers succeed - name: web image: myapp:v2.0 ports: - containerPort: 8080
A sidecar is a regular container that runs alongside the main container in the same Pod — sharing its network and storage. Sidecars add capabilities to the main app without the app needing to know they exist.
The three classic multi-container patterns:
- Sidecar — Enhances or extends the main container. Example: a log shipper that reads logs from a shared volume and forwards them to a central aggregator. The app just writes logs to disk; the sidecar handles shipping.
- Ambassador — Proxies network traffic for the main container. Example: a local proxy that handles service discovery, retries, and circuit breaking. The app talks to
localhost:8080; the ambassador handles routing to the real service. - Adapter — Standardizes the main container's output. Example: a container that converts proprietary metrics format into Prometheus format. The app exposes metrics its own way; the adapter translates them.
apiVersion: v1 kind: Pod metadata: name: app-with-sidecar spec: volumes: # A shared in-memory volume both containers can read/write - name: shared-logs emptyDir: {} containers: # Main container: writes application logs to the shared volume - name: web-app image: myapp:v2.0 volumeMounts: - name: shared-logs mountPath: /var/log/app # Sidecar: reads from the shared volume and ships to Loki # The main app has NO IDEA this is happening - name: log-shipper image: grafana/promtail:latest args: - -config.file=/etc/promtail/config.yaml volumeMounts: - name: shared-logs mountPath: /var/log/app readOnly: true resources: requests: cpu: 50m memory: 32Mi limits: cpu: 100m memory: 64Mi
Init containers appear frequently on the exam. Common tasks: "Add an init container that waits for a service to be available" or "Add an init container that copies a file to a shared emptyDir volume before the main app starts." Know that init containers have their own image, command, and resources, and that the main containers don't start until all inits succeed. Also know the difference between emptyDir (ephemeral shared volume) and a PVC.
⛵ Helm — The Kubernetes Package Manager INTERMEDIATE
Deploying a real application means managing 10-20 YAML files — Deployments, Services, ConfigMaps, Secrets, Ingress, RBAC, HPAs. Helm packages all of them into a single, versioned, configurable unit called a chart.
Helm is to Kubernetes what apt is to Ubuntu or npm is to Node.js — a package manager. It introduces three concepts:
- Chart — A package of pre-configured K8s resource templates. Like a Dockerfile for your entire application stack. Charts can depend on other charts.
- Release — A specific instance of a chart deployed into a cluster. You can install the same chart multiple times with different names and configs — e.g.,
helm install prod myapp/myappandhelm install staging myapp/myapp --set replicas=1. - Repository — A collection of charts hosted somewhere. Artifact Hub (artifacthub.io) is the public registry with thousands of charts for open-source tools.
# Add a chart repository helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update # Search for charts in a repo helm search repo bitnami/postgresql # See all configurable values for a chart helm show values bitnami/postgresql # Install a chart as a release named "my-db" into the "production" namespace helm install my-db bitnami/postgresql \ -n production \ --set auth.postgresPassword=mysecret \ --set primary.persistence.size=20Gi # Install using a values file (better than --set for many values) helm install my-db bitnami/postgresql -f values-prod.yaml # List all deployed Helm releases helm list -A # Upgrade a release (like applying new config or a new chart version) helm upgrade my-db bitnami/postgresql -f values-prod.yaml # Roll back a release to the previous revision helm rollback my-db 1 # See revision history for a release helm history my-db # Uninstall a release (removes all K8s objects it created) helm uninstall my-db -n production # Preview what a chart will generate without installing (dry run) helm install my-db bitnami/postgresql --dry-run --debug
Charts have a specific directory structure. Helm generates this scaffold for you with helm create:
# values.yaml — the defaults, overridable at install time replicaCount: 3 image: repository: myapp tag: "v1.2.0" service: port: 80 resources: limits: cpu: 500m memory: 256Mi autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 --- # templates/deployment.yaml — uses Go template syntax to inject values apiVersion: apps/v1 kind: Deployment spec: replicas: {{ .Values.replicaCount }} template: spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" resources: {{- toYaml .Values.resources | nindent 12 }} # Override at install: helm install prod . --set image.tag=v2.0.0 --set replicaCount=5
helm install monitoring prometheus-community/kube-prometheus-stack. Cert-manager: helm install cert-manager jetstack/cert-manager. Nginx Ingress: helm install ingress-nginx ingress-nginx/ingress-nginx. What used to take hours of YAML wrangling is now one command with sane defaults and full configurability.
helm install v2 myapp/myapp and it broke in production. How do you instantly get back to the previous working version?🏆 CKAD Exam Prep — Pass on the First Try
The CKAD is a 2-hour, hands-on, terminal-only exam. No multiple choice. You get a live cluster and 15-20 real tasks to complete. Speed and muscle memory are the difference between passing and failing.
| Domain | Weight | Key Topics |
|---|---|---|
| Application Design & Build | 20% | Multi-container pods, init containers, Jobs, CronJobs, volumes |
| Application Deployment | 20% | Deployments, rolling updates, Helm, blue-green strategies |
| Application Observability & Maintenance | 15% | Probes, logging, debugging, API deprecations |
| Application Environment, Config & Security | 25% | ConfigMaps, Secrets, SecurityContext, ServiceAccounts, RBAC, resource limits |
| Services & Networking | 20% | Services, Ingress, Network Policies |
Set these up the moment the exam starts. They save hundreds of keystrokes over 2 hours:
# The single most important alias — type k instead of kubectl everywhere alias k=kubectl # Enable kubectl autocomplete (tab-complete resource names and types) source <(kubectl completion bash) complete -F __start_kubectl k # Set your default namespace to whatever the question specifies k config set-context --current --namespace=target-namespace # Dry-run shortcut — generate YAML without creating anything export DO="--dry-run=client -o yaml" # Usage: k run mypod --image=nginx $DO > pod.yaml # Usage: k create deploy web --image=nginx $DO > deploy.yaml # Set your editor to vim (faster than nano for YAML editing) export KUBE_EDITOR="vim"
The fastest CKAD approach: use imperative commands to generate a YAML scaffold, redirect it to a file, edit only what you need to change, then apply. Never write YAML from scratch.
# Generate Pod YAML k run web --image=nginx:alpine --port=80 --labels=app=web \ --dry-run=client -o yaml > pod.yaml # Generate Deployment YAML k create deploy web --image=nginx:alpine --replicas=3 \ --dry-run=client -o yaml > deploy.yaml # Generate Service YAML (expose a deployment) k expose deploy web --port=80 --target-port=80 --type=ClusterIP \ --dry-run=client -o yaml > svc.yaml # Generate ConfigMap k create configmap app-config --from-literal=LOG_LEVEL=info \ --dry-run=client -o yaml > cm.yaml # Generate Secret k create secret generic db-secret --from-literal=password=s3cr3t \ --dry-run=client -o yaml > secret.yaml # Generate ServiceAccount k create serviceaccount deployer --dry-run=client -o yaml > sa.yaml # Generate Role + RoleBinding (can't dry-run bind, but can generate each) k create role pod-reader \ --verb=get,list,watch --resource=pods \ --dry-run=client -o yaml > role.yaml k create rolebinding read-pods \ --role=pod-reader --serviceaccount=default:deployer \ --dry-run=client -o yaml > rb.yaml # Generate CronJob k create cronjob cleanup \ --image=busybox --schedule="0 2 * * *" \ -- /bin/sh -c "echo cleaning up" \ --dry-run=client -o yaml > cj.yaml # Look up ANY field you forget — this is like man pages for K8s k explain pod.spec.initContainers k explain deployment.spec.strategy.rollingUpdate k explain networkpolicy.spec.ingress
A significant portion of the CKAD is debugging existing broken configurations. Work through this checklist systematically:
# Step 1: Get the big picture k get all -n target-namespace # Step 2: What's the Pod status? (CrashLoopBackOff? Pending? OOMKilled?) k get pods -n target-namespace # Step 3: Describe the failing Pod — look at Events at the bottom! # "Back-off restarting failed container" → check logs # "Insufficient cpu/memory" → quota or node capacity issue # "Failed to pull image" → wrong image name or tag # "Readiness probe failed" → app not ready on that port/path k describe pod failing-pod-name # Step 4: Check the logs k logs failing-pod-name k logs failing-pod-name --previous # logs from the crashed previous container # Step 5: Get a shell in a running (or debug) pod k exec -it pod-name -- sh # Step 6: Run a temporary debug pod k run debug --image=busybox --rm -it --restart=Never -- sh # Step 7: Test connectivity from inside the cluster k run curl-test --image=curlimages/curl --rm -it --restart=Never -- \ curl -s http://web-service.default.svc.cluster.local # Step 8: Edit the broken resource in place k edit pod failing-pod-name k edit deployment web
Time yourself — you should finish in under 25 minutes
- Create namespace
ckad-practice. Set it as your default context. - Create a ServiceAccount named
app-runner. Create a Role that allowsget,list, andwatchonpodsandconfigmaps. Bind it with a RoleBinding. - Create a ConfigMap named
app-configwithLOG_LEVEL=debugandMAX_WORKERS=4. - Create a Secret named
app-secretwithAPI_KEY=abc123. - Create a Deployment named
webwith 3 replicas usingnginx:alpine. Inject the ConfigMap as env vars and the Secret as an env var. Add liveness and readiness probes on port 80, path/. Set resource requests and limits. Use theapp-runnerServiceAccount. - Create a ClusterIP Service exposing the Deployment on port 80.
- Create an HPA targeting 70% CPU, min 2, max 8 replicas.
- Create a LimitRange setting default CPU request to 100m and default memory limit to 256Mi.
- Create a NetworkPolicy that only allows ingress to the
webpods from pods labeledrole=frontend. - Create a CronJob that runs
echo "hello"every 5 minutes, withconcurrencyPolicy: Forbid. - Verify everything:
kubectl get all,cm,secret,hpa,netpol,cronjob,limitrange,sa,role,rolebinding -n ckad-practice
Must-do preparation:
- Complete all scenarios on killer.sh (included free with your CKAD registration — 2 attempts). It's harder than the real exam. If you can pass killer.sh, you'll pass the CKAD.
- Practice on KodeKloud's CKAD course — the labs give you a real cluster in the browser. Hands-on, not videos.
- Know your vim basics:
i(insert),Esc,:wq(save),:q!(quit no save),dd(delete line),yy/p(copy/paste),:%s/old/new/g(find/replace). You will be in vim on the exam. - During the exam: read each question twice, note the namespace, note the context switch command at the top of each question and run it before doing anything else.
echo "ready" before the main nginx container starts. You want to generate the base YAML quickly. What command do you run first?Pending state. What is the most likely cause and where do you look first?