Introduction
You can now deploy, expose, and scale an application. The next operational question is: how do you replace the application version without taking the whole service offline?
A Kubernetes Deployment answers this with a rolling update. When its Pod template changes, the Deployment creates a new ReplicaSet, gradually starts new Pods, and removes old Pods only as the replacement becomes available. The Service keeps one stable address throughout this process.
In this lab, you will move an NGINX application from one pinned image to another, connect Deployment revisions to ReplicaSets and Pods, deliberately attempt a broken release, and roll back to the last healthy revision. You will also keep the saved manifest consistent with the recovered live state—an important habit that prevents a later kubectl apply from reintroducing the failure.
Read the Starting Environment
In this step, you will confirm the cluster connection and prepare an in-cluster HTTP client.
Environment startup: This lab starts a complete Kubernetes cluster for you. Configuring its control plane, node, and networking components usually takes 2–3 minutes. Please wait patiently for the environment to finish loading before running the commands below.
This course environment already contains a running Kubernetes cluster, so you do not need to create one. First, verify where kubectl will send commands. config current-context names the active connection, get node reads node health, and version reports both the local client and reachable API server versions:
kubectl config current-context
kubectl get node
kubectl version
The context and node are named labex-v135, the node is Ready, and the server reports Kubernetes v1.35.x. A context is the kubeconfig selection that connects kubectl to a particular cluster, user, and default namespace.
Create a small client Pod now. You will use it later to reach the application through its Service. kubectl run creates a Pod; --image chooses BusyBox, --restart=Never keeps it as a standalone Pod, and the -- separator introduces its long-running sleep 3600 command. kubectl wait then waits at most 30 seconds for its Ready condition:
kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
--restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s
This separates the caller from the web Pods, which is closer to how one workload calls another inside a cluster.
Deploy the Stable Baseline
In this step, you will establish a known-good application revision before making any changes.
Before practising an update, establish a known-good revision. Create one manifest containing a three-replica Deployment and a stable ClusterIP Service.
cd enters the prepared workspace. The here-document syntax cat <<'EOF' > release-web.yaml writes everything through the closing EOF into the file. The --- line separates two Kubernetes objects in one YAML file:
cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-web
annotations:
kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
replicas: 3
strategy:
type: RollingUpdate
selector:
matchLabels:
app: release-web
template:
metadata:
labels:
app: release-web
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: release-web
spec:
selector:
app: release-web
ports:
- name: http
port: 80
targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web
The Deployment owns the Pods, while the Service independently selects them by label. Updating the Deployment will not change the Service address.
Confirm the baseline from the client. In kubectl exec POD -- COMMAND, -- separates kubectl arguments from the command inside the container. wget -qO- fetches quietly to standard output, and the pipe passes the response to head so only its beginning is shown:
kubectl exec release-client -- wget -qO- http://release-web | head
Release a New Image Declaratively
In this step, you will change the saved Pod template and let the Deployment perform a rolling update.
A Deployment starts a rollout when its Pod template changes. The image is inside that template, so changing it creates a new revision and a new ReplicaSet.
Update both the image and the human-readable change cause in the saved manifest. Each sed -i 's/old/new/' file substitution edits the file in place. grep -nE then shows line numbers for either extended-regex term (change-cause or image:), letting you inspect both edits before applying:
cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl apply changes the desired state. The Deployment controller then works asynchronously until all three updated replicas are available. For a focused Pod table, -l selects the application label and -o custom-columns maps headings to object field paths:
kubectl get deployment release-web
kubectl get pods -l app=release-web \
-o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'
All three current replicas use the new image. You may also briefly see an old-image Pod in Terminating state after rollout success. That Pod is no longer a desired replica; Kubernetes is finishing its graceful shutdown in the background.
Connect Revisions, ReplicaSets, and Pods
In this step, you will inspect the objects behind the successful rollout and confirm that the Service stayed available.
The rollout is complete, but understanding what changed is more useful than seeing only “success.” kubectl rollout history reads stored Deployment revisions. The following get replicasets command uses -l to keep related objects and custom-columns to compare their replica counts and images:
kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
-o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'
You should see two ReplicaSets. The new one owns three Pods; the old one remains at zero replicas so its Pod template is available for rollback. A ReplicaSet name includes a hash derived from the Pod template, which is why the Pod names changed when the image changed.
Check the live image and Service backend count. Both JSONPath expressions use range to repeat an output template over a list. Literal spaces and {"\n"} make one readable line per Pod or endpoint:
kubectl get pods -l app=release-web \
-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head
The revision changed, but the Service still has three ready backends and the same stable name.
Diagnose a Broken Release
In this step, you will introduce a controlled image error and use Pod events to identify why the rollout cannot complete.
Now simulate a common release mistake: a container image tag that does not exist. Keep this failure on the same Deployment so the rollout mechanism can demonstrate an important safety property. The two sed -i commands edit annotation and image text in place. The expected rollout timeout would normally return a failure status; || true deliberately lets the lesson continue:
cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web
The rollout does not finish because a new Pod cannot pull its image. The || true lets the lesson continue after the expected timeout.
Find the failing Pod and inspect its events. $(...) saves command output in BAD_POD. The first pipe feeds Kubernetes JSON to jq; select(...) keeps the Pod with the broken image and -r returns its name as plain text. A second pipe to head -n1 keeps one name. Finally, sed -n '/Events:/,$p' prints the describe output from Events: through the end:
BAD_POD=$(kubectl get pods -l app=release-web \
-o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web
Look for ErrImagePull or ImagePullBackOff. Notice that the previous healthy ReplicaSet still has available Pods, so the Service can continue responding:
kubectl exec release-client -- wget -qO- http://release-web | head
This is availability during a failed rollout—not proof that the new release works.
Roll Back and Reconcile the Manifest
In this step, you will restore the previous healthy revision and then repair the saved manifest.
The live Deployment has revision history, so Kubernetes can restore the previous Pod template. rollout undo asks the Deployment controller to reuse the previous revision; rollout status waits for that recovery, and custom-columns prints the recovered image and ready count:
kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
-o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'
The image is healthy again and three replicas are ready. rollout undo creates a new revision from an older Pod template; it does not rewind the revision counter.
There is one more job. The YAML file still contains the broken image, so a future kubectl apply would break the Deployment again. Reconcile the saved desired state with the recovered live state. The substitutions repair the file, apply synchronizes it, and the final history command confirms the resulting revision trail:
cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web
Now both the cluster and the file describe the same healthy release.
Choose a Safer Update Budget
In this step, you will make the Deployment's rollout capacity and availability limits explicit.
Deployment strategy settings control the temporary capacity allowed during future updates:
maxUnavailableis how many desired replicas may be unavailable during a rollout.maxSurgeis how many extra Pods may temporarily exist above the desired replica count.
For this small three-replica application, require all three desired replicas to remain available and allow one extra Pod.
The sed command uses an address, /type: RollingUpdate/, to find the strategy line. Its a\ action appends the following newline-separated YAML with the required indentation. After applying, JSONPath extracts both strategy values so you can verify the edit without scanning the complete object:
cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\ rollingUpdate:\n maxUnavailable: 0\n maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
-o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web
Changing only strategy fields does not replace Pods because the Pod template did not change. On a future image update, Kubernetes may create one extra Pod and should not intentionally reduce available replicas below three.
This setting favors availability but needs spare cluster capacity. There is no universal best value: larger applications often use percentages, and real choices depend on capacity, startup time, and acceptable disruption.
Summary
You followed an application release through its full operational lifecycle:
- established a healthy Deployment and stable Service baseline;
- changed a pinned image declaratively and waited for the rollout;
- connected Deployment revisions to old and new ReplicaSets;
- diagnosed an image-pull failure while the previous release continued serving;
- rolled back to the last healthy Pod template;
- reconciled the manifest so a later apply cannot restore the bad image; and
- configured an explicit availability and surge budget for future updates.
The central idea is that a Deployment manages desired state over time. A rollout is not merely an image edit: it is a controlled transition between ReplicaSets that you should observe, verify, and be prepared to reverse.


