Skip to content

Kubernetes Deployments, Rollouts, and Application Configuration

Step 9 of 24

Kubernetes Deployments, Rollouts, ConfigMaps, and Secrets for CKA

Introduction

Kubernetes Deployments, ConfigMaps and Secrets are central to the Workloads & Scheduling domain (15% of the CKA). This lesson teaches rolling updates, rollback, application configuration and self-healing workloads. Practise creating a three-replica Deployment, replacing its image, recovering a bad release and configuring health probes. These are original practice scenarios aligned with the published competencies; the exam uses hands-on tasks rather than this course's multiple-choice quiz.

The official exam page currently lists Kubernetes v1.37. Confirm the version for your sitting and use documentation matching your lab. Examples below illustrate controller behaviour; private application images and files must be supplied by the exercise. The nginx tags are fixed lab examples, not production version recommendations.

After this lesson you will be able to create and update Deployments, control rollout strategy, undo a bad release, inject configuration with ConfigMaps and Secrets through environment variables and mounted files, add probes and restart policies, and choose between Deployments, DaemonSets, StatefulSets and Jobs.

Learning objectives

  • Describe how a Deployment, its ReplicaSets and Pods relate, and read rollout status and history.
  • Tune a rolling update with maxSurge and maxUnavailable, pause, resume and roll back.
  • Create ConfigMaps and Secrets from literals, files and directories, and consume them as environment variables, envFrom, volume mounts and subPath files.
  • Configure liveness, readiness and startup probes and understand restartPolicy.
  • Pick the right controller: Deployment, StatefulSet, DaemonSet, Job or CronJob.

Kubernetes Deployments and ReplicaSets

A Deployment declares the desired state of a stateless application. It manages ReplicaSets, and each ReplicaSet manages identical Pods. When you change the Pod template (image, env, resources, and so on), the Deployment creates a new ReplicaSet and gradually scales it up while scaling the old one down. Changing only replicas does not create a new ReplicaSet.

Bash
kubectl create deployment web --image=nginx:1.27 --replicas=3 -n demo
kubectl get deploy,rs,pods -n demo -l app=web
kubectl scale deployment web --replicas=5 -n demo
kubectl set image deployment/web nginx=nginx:1.27.2 -n demo   # container name is nginx
kubectl expose deployment web --port=80 --target-port=80 -n demo

Generate a manifest to edit rather than writing one from scratch:

Bash
kubectl create deployment web -n demo --image=nginx:1.27 --replicas=3 --dry-run=client -o yaml > web.yaml

The skeleton you should be able to write from memory:

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web              # must match the template labels; immutable after creation
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80

Facts that are tested:

  • spec.selector must match spec.template.metadata.labels, and it cannot be changed after creation.
  • The Deployment's Pods are named <deployment>-<replicaset-hash>-<random>.
  • Deleting a Pod owned by a ReplicaSet makes the ReplicaSet create another; deleting the Deployment deletes its ReplicaSets and Pods (use --cascade=orphan to keep them).
  • kubectl scale on a Deployment is declarative state in the cluster; if you manage the YAML in Git, update the file as well.

Rolling updates and rollbacks

The default strategy is RollingUpdate. Two knobs control the pace, either as absolute numbers or percentages:

Field Meaning Default
maxSurge Extra non-terminating replicas allowed during the update 25% (rounded up)
maxUnavailable How many Pods below replicas may be unavailable 25% (rounded down)

With 4 replicas and defaults, the controller allows one surge replica and budgets for at least 3 available replicas. Pods still terminating can temporarily push the total above 5. Setting maxUnavailable: 0 and maxSurge: 1 avoids intentionally reducing available replicas during the rollout, provided replacement Pods can start. Useful readiness checks, spare capacity and graceful connection draining are also needed for uninterrupted service; this strategy does not protect against independent failures. Recreate terminates old replicas before creating replacements during an upgrade. It introduces downtime and should not be treated as a general concurrency or data-locking guarantee.

A Pod counts as available only after it has been Ready for minReadySeconds. This is why readiness probes matter: without a meaningful readiness check, a running but broken application may count as ready and allow the rollout to continue.

Managing a rollout

Bash
kubectl rollout status deployment/web -n demo          # blocks until finished or failed
kubectl rollout history deployment/web -n demo
kubectl rollout history deployment/web --revision=2 -n demo   # see the template of a revision
kubectl rollout undo deployment/web -n demo                  # back to the previous revision
kubectl rollout undo deployment/web --to-revision=1 -n demo
kubectl rollout pause deployment/web -n demo           # batch several changes
kubectl set resources deployment/web -c nginx --limits=cpu=500m,memory=256Mi -n demo
kubectl rollout resume deployment/web -n demo
kubectl rollout restart deployment/web -n demo         # restart annotation changes the Pod template

Notes:

  • The history is the set of old ReplicaSets, limited by revisionHistoryLimit (default 10). Setting it to 0 removes the ability to roll back.
  • The CHANGE-CAUSE column is populated from the annotation kubernetes.io/change-cause. Use an explicit annotation rather than the obsolete --record flag: kubectl annotate deployment/web -n demo kubernetes.io/change-cause="upgrade to 1.27.2" --overwrite.
  • A rollout that cannot progress (for instance ImagePullBackOff) reports ProgressDeadlineExceeded after progressDeadlineSeconds (default 600). This status neither pauses the controller nor automatically rolls back: reconciliation continues, and fixing the cause can allow progress. Inspect the remaining healthy replicas and decide whether to fix or undo the release.
  • rollout undo restores a retained Pod template; it does not restore a replica count, changed ConfigMap/Secret contents, a database schema or application data. Use versioned configuration when a release must be reproducible.

Worked example: the update that stalls

Scenario. Fabrikam's shop Deployment has 4 replicas (maxSurge 25%, maxUnavailable 25%). You run kubectl set image deployment/shop app=fabrikam/shop:v2 and the status never completes. A snapshot of kubectl get pods shows 3 old Pods Running and new Pods in ImagePullBackOff (the tag has a typo, v2 should be 2.0).

Reasoning.

  1. kubectl rollout status deployment/shop reports that updated replicas are still unavailable.
  2. kubectl describe pod <new> shows Failed to pull image ... not found.
  3. Up to one old replica can be removed under the unavailable budget; the remaining three healthy replicas may still serve traffic. Capacity is reduced, so check latency and errors instead of assuming customers are unaffected. The number of failed new Pods can change as the controller reconciles.
  4. Fix choices: kubectl set image deployment/shop app=fabrikam/shop:2.0 to move forward, or kubectl rollout undo deployment/shop to return to the previous template. Both start a new rollout.
  5. Verify with kubectl rollout status and kubectl get rs (the broken ReplicaSet scales to 0).

Why not the alternatives? Deleting the failing Pod only makes the ReplicaSet recreate it with the same bad image. Scaling to 0 and back causes an outage. Editing the ReplicaSet directly is overwritten by the Deployment controller.

ConfigMaps

A ConfigMap stores non-confidential configuration as key-value pairs (up to 1 MiB). Values may be short strings or whole files.

Bash
kubectl create configmap app-config -n demo \
  --from-literal=LOG_LEVEL=info --from-literal=FEATURE_X=true
kubectl create configmap nginx-conf -n demo --from-file=nginx.conf        # key = file name
kubectl create configmap settings -n demo --from-file=app.properties=./dev.properties  # custom key
kubectl create configmap envfile -n demo --from-env-file=.env

There are four ways to consume it:

YAML
spec:
  containers:
  - name: app
    image: demo/app:1.0
    env:
    - name: LOG_LEVEL                 # 1. one key as one variable
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: LOG_LEVEL
    envFrom:                          # 2. all keys as variables
    - configMapRef:
        name: app-config
    volumeMounts:
    - name: conf                      # 3. keys as files in a directory
      mountPath: /etc/app
    - name: nginx-single              # 4. one file placed without hiding the directory
      mountPath: /etc/nginx/nginx.conf
      subPath: nginx.conf
  volumes:
  - name: conf
    configMap:
      name: app-config
  - name: nginx-single
    configMap:
      name: nginx-conf

Behaviour you must remember:

Consumption Updates after the ConfigMap changes?
Environment variables (env, envFrom) No; Pods must be restarted (for example kubectl rollout restart)
Volume mount Eventually: kubelet sync plus cache/watch propagation delay; the app must re-read the files
Volume mount with subPath No
Immutable ConfigMap (immutable: true) Never; you create a new one and change the reference

A missing ConfigMap referenced by env or a volume leaves the Pod in CreateContainerConfigError (or ContainerCreating for volumes) unless the reference is marked optional: true. Mounting a volume at a directory hides whatever the image had there, which is why subPath is used for a single file.

Secrets

A Secret has the same shape as a ConfigMap but is intended for sensitive data: passwords, tokens, TLS keys. Values are base64-encoded in the API, which is encoding, not encryption. Real protection comes from Kubernetes RBAC and Secret permissions, configured encryption at rest for etcd, and avoiding exposure in logs and environment dumps. A principal allowed to create workloads can often mount namespace Secrets even without direct Secret read permission.

Bash
kubectl create secret generic db-creds -n demo \
  --from-literal=username=app --from-literal=password='S3cr3t!'
kubectl create secret tls web-tls --cert=tls.crt --key=tls.key -n demo
kubectl create secret docker-registry regcred --docker-server=registry.example.com \
  --docker-username=bot --docker-password=pw -n demo
kubectl get secret db-creds -n demo -o jsonpath='{.data.password}' | base64 -d

The literal passwords above and below are dummy lab values. Real values on command lines can leak through shell history or process inspection; use protected files or an approved secret-management workflow.

Secret types you should recognise: Opaque (generic), kubernetes.io/tls, kubernetes.io/dockerconfigjson (image pull), kubernetes.io/basic-auth, kubernetes.io/ssh-auth, kubernetes.io/service-account-token.

Consumption mirrors ConfigMaps (secretKeyRef, secretRef in envFrom, secret: volumes). Use defaultMode to set file permissions, and imagePullSecrets for private registries:

YAML
spec:
  imagePullSecrets:
  - name: regcred
  containers:
  - name: app
    image: registry.example.com/app:1.0
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-creds
          key: password
    volumeMounts:
    - name: creds
      mountPath: /etc/creds
      readOnly: true
  volumes:
  - name: creds
    secret:
      secretName: db-creds
      defaultMode: 0400

Choose the consumption method your application supports. Files permit eventual updates when mounted without subPath, but the application must reload them. Environment values can appear in process dumps or debugging output and do not refresh in running containers. kubectl describe pod normally displays a Secret reference rather than decoding its value; it is incorrect to assume that it directly prints Secret-backed passwords. In YAML, data expects base64 values while stringData accepts plain text and is converted on write.

Worked example: refresh a Secret-backed environment variable

Scenario. Northwind's orders Deployment reads DB_PASSWORD from Secret db-creds via an environment variable. The DBA changes the password to N3wPass. You update the Secret, but the application keeps failing to connect.

Reasoning.

  1. Replace the Secret value: kubectl create secret generic db-creds -n demo --from-literal=password=N3wPass --dry-run=client -o yaml | kubectl apply -f -.
  2. Environment variables are fixed at container start, so running Pods still hold the old password.
  3. Trigger new Pods with kubectl rollout restart deployment/orders -n demo; the rolling strategy replaces them gradually.
  4. Verify completion with kubectl rollout status deployment/orders -n demo, then inspect kubectl logs deploy/orders -n demo without logging credentials and check the application's database health.

Why not the alternatives? Deleting all Pods simultaneously risks an outage. Waiting for propagation does not refresh environment variables; mounted-file updates also require application reload support. The Pod environment cannot be edited in place. This scenario starts after the old database password has been invalidated, so availability may already be lost. For planned rotation, coordinate overlapping valid credentials, rollout and old-credential revocation with the database team.

Self-healing: probes and restart policy

Kubernetes restarts failed containers and replaces failed Pods, but it needs you to say what "healthy" means.

Probe Question On failure
Liveness Is the process alive? kubelet terminates it after the failure threshold; restart follows restart policy
Readiness Can it accept traffic now? Endpoint becomes unready; normal Service routing avoids it (no restart)
Startup Has the slow application finished starting? Holds off liveness/readiness; repeated failure terminates it, subject to restart policy

Each probe is one of httpGet, tcpSocket, exec or grpc, with tuning fields initialDelaySeconds, periodSeconds, timeoutSeconds, successThreshold and failureThreshold.

YAML
containers:
- name: app
  image: demo/app:1.0
  readinessProbe:
    httpGet: { path: /ready, port: 8080 }
    periodSeconds: 5
    failureThreshold: 3
  livenessProbe:
    httpGet: { path: /healthz, port: 8080 }
    initialDelaySeconds: 10
    periodSeconds: 10
  startupProbe:
    httpGet: { path: /healthz, port: 8080 }
    failureThreshold: 30      # up to 30 x periodSeconds to start
    periodSeconds: 5

Guidelines: do not make liveness depend on downstream services (a database outage would restart every Pod in a loop), and use readiness for dependency checks.

restartPolicy is set at Pod level: Always (default; required for Deployments), OnFailure and Never (used by Jobs). After repeated crashes, the kubelet applies exponential back-off (10 s, 20 s, 40 s ... capped at 5 minutes), which you see as CrashLoopBackOff. These are default timings, not an invariant: feature gates and kubelet configuration can change restart delays. Container-level restart rules can override the Pod policy when supported; the examples here use ordinary application containers without overrides.

Comparing the workload controllers

Controller Use for Identity and storage Scheduling
Deployment Stateless apps Interchangeable Pods Any suitable node
StatefulSet Databases, clustered apps Stable names (db-0, db-1), stable DNS through a headless Service, one PVC per Pod via volumeClaimTemplates, ordered start and stop Any suitable node
DaemonSet One Pod per node (log agents, CNI, node exporters) Per node Every matching node, including new ones; tolerations needed for tainted nodes
Job Run-to-completion work (completions, parallelism, backoffLimit) Pods end when done Any
CronJob Scheduled Jobs (schedule, concurrencyPolicy) Creates Jobs Any

Useful commands:

Bash
kubectl create job pi --image=perl:5.40 -- perl -Mbignum=bpi -wle 'print bpi(2000)'
kubectl create cronjob nightly --image=busybox --schedule="0 2 * * *" -- /bin/sh -c "date"
kubectl get statefulset,ds -A
kubectl rollout status statefulset/db

DaemonSets have no kubectl create daemonset; generate a Deployment manifest with --dry-run=client -o yaml, change kind to DaemonSet, and delete the replicas and strategy fields. StatefulSets update Pods in reverse ordinal order by default and honour partition for canary updates.

Worked example: a node agent that must reach every node

Scenario. Contoso needs a log collector on every node, including control plane nodes that carry the taint node-role.kubernetes.io/control-plane:NoSchedule.

Reasoning.

  1. A DaemonSet places one Pod per node automatically and covers new nodes as they join.
  2. Control plane nodes are tainted, so the Pod template needs a toleration; without it those nodes are skipped.
  3. The agent only needs read access to host logs, so mount /var/log read-only through a hostPath volume.
YAML
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
  namespace: kube-system
spec:
  selector:
    matchLabels: { app: log-agent }
  template:
    metadata:
      labels: { app: log-agent }
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule
      containers:
      - name: agent
        image: registry.example.com/log-agent:1.0
        volumeMounts:
        - { name: varlog, mountPath: /var/log, readOnly: true }
      volumes:
      - name: varlog
        hostPath: { path: /var/log }

This manifest demonstrates placement and read-only mounting; supply a compatible collector image and its tailing/output configuration before expecting logs to be collected. Admission policy must permit the hostPath volume.

Verify with kubectl get ds log-agent -n kube-system (DESIRED matches eligible nodes, and READY confirms successful starts) and kubectl get pods -n kube-system -o wide -l app=log-agent.

Why not the alternatives? A Deployment with replicas equal to the node count does not guarantee one per node. A Deployment with pod anti-affinity is fragile and does not follow new nodes. A StatefulSet adds identity guarantees that are not needed.

Common pitfalls and exam tips

  • Wrong container name in set image. The syntax is deployment/<name> <container-name>=<image>; check with kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[*].name}'.
  • Labels and selectors. A selector that does not match the template labels is rejected; changing a selector on an existing Deployment is not allowed.
  • Env vars do not auto-refresh. After changing a ConfigMap or Secret used via env, run kubectl rollout restart. Plan capacity and Pod scheduling and HPA together so new replicas can start.
  • subPath mounts never refresh and mounting over a directory hides its content.
  • Base64 is not security. Use base64 -d to decode, and never paste decoded secrets into logs or answers.
  • Liveness probes pointing at dependencies cause cascading restarts; use readiness.
  • A missing readiness probe may let a running but broken application count as ready. Services and EndpointSlice readiness explain the effect on traffic.
  • Namespace and context. Always kubectl config use-context as instructed and add -n.
  • Exam tip: speed matters. Use kubectl create ... --dry-run=client -o yaml, kubectl explain deployment.spec.strategy --recursive, and kubectl rollout status to avoid guessing. Set alias k=kubectl and export do="--dry-run=client -o yaml" only if you are already comfortable with them; the environment allows them but the time to type is the point.

Mini case study: the faulty release at Adventure Works

Adventure Works runs the catalog Deployment (4 replicas, image catalog:1.8, namespace store). Configuration comes from ConfigMap catalog-config (env) and Secret catalog-db. Tasks:

  1. Plan an update to catalog:1.9 that retains four available replicas during normal rollout. Assume sufficient capacity and no independent failures.
  2. After the release, Pods crash-loop because CATALOG_CACHE_SIZE is missing in the ConfigMap. Restore service quickly, then fix properly.
  3. Add a readiness probe on /ready port 8080 so a bad build cannot take over silently.

Solution.

  1. Before releasing 1.9, install the readiness probe shown below and verify the current version's rollout. Set the update strategy with kubectl patch deployment catalog -n store -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}', then run kubectl set image deployment/catalog -n store catalog=catalog:1.9 and watch kubectl rollout status deployment/catalog -n store. With maxUnavailable: 0, the controller waits for a replacement to become available before intentionally removing an old replica. This is a rollout budget, not a guarantee against unrelated crashes or application errors.
  2. Fast recovery: kubectl rollout undo deployment/catalog -n store. Proper fix: kubectl patch configmap catalog-config -n store -p '{"data":{"CATALOG_CACHE_SIZE":"512"}}', then roll out 1.9 again. Because the values are consumed as environment variables, a restart is required for the Pods to see the new key. Check Pod status with kubectl get pods -n store; a missing key referenced through configMapKeyRef shows CreateContainerConfigError, while the crash loop here comes from the application failing at start.
  3. The readiness probe to configure before the release is below. Add it with kubectl edit deployment catalog -n store under containers[0]:
YAML
readinessProbe:
  httpGet: { path: /ready, port: 8080 }
  periodSeconds: 5

The edit changes the Pod template and starts another rollout; the new Pods only receive traffic once they report ready.

Commentary. A rollback can restore the previous Pod template, but recovery time depends on image pulls, startup and shared configuration. If the previous release also needs the missing key, restore that configuration too. Readiness and maxUnavailable: 0 reduce rollout risk; verify application health and traffic before declaring recovery.

Key terms

  • Deployment / ReplicaSet: controller for stateless apps / controller keeping a set of identical Pods.
  • Rolling update: gradual replacement of old Pods with new ones.
  • maxSurge / maxUnavailable: extra and missing Pods allowed during an update.
  • Rollout / revision: a version change and its numbered history entry.
  • ConfigMap / Secret: configuration data / sensitive data objects.
  • Probe: periodic health check (liveness, readiness, startup).
  • DaemonSet / StatefulSet: one Pod per node / Pods with stable identity and storage.
  • CrashLoopBackOff: container repeatedly failing, restarts delayed with back-off.

Summary checklist

  • kubectl create deployment, scale, set image, rollout status|history|undo|restart.
  • Selector matches template labels and is immutable; template changes create a new ReplicaSet.
  • maxSurge and maxUnavailable control the pace; readiness probes make rollouts safe.
  • ConfigMaps and Secrets: create from literal, file and env-file; consume via env, envFrom, volumes, subPath.
  • Env vars need a restart; volume mounts update (not subPath); base64 is not encryption.
  • Liveness restarts, readiness gates traffic, startup protects slow starts.
  • DaemonSet for per-node Pods (tolerations for taints), StatefulSet for stable identity and storage, Job/CronJob for batch.

Official sources