The previous lesson ended with a very specific gap: a ReplicaSet keeps N copies of a template alive, but it does not know how to change version. You changed the template's image and the existing pods carried on quite happily with the old one. The Deployment is the piece that closes that gap and, by a wide margin, the object you will write most in your professional life with Kubernetes. It governs ReplicaSets and, through them, pods: it gives you replicas, self-healing, scaling, controlled updates, revision history and a way back. In this lesson you will understand the Deployment → ReplicaSet → Pod chain and exactly what each link contributes, you will finally turn the bare web-store pod into a 3-replica Deployment, you will create the bookings-api one, you will learn to read the columns of kubectl get deploy and the status conditions precisely, you will scale in two different ways and you will observe what happens in the cluster when an image changes. You will also finish knowing when a Deployment is not the right workload.

Contents

  1. The Deployment → ReplicaSet → Pod chain
  2. From bare pod to Deployment: web-store
  3. The complete manifest, annotated
  4. The bookings-api Deployment
  5. Reading kubectl get deploy: READY, UP-TO-DATE, AVAILABLE
  6. status, conditions and kubectl rollout status
  7. Scaling: kubectl scale and the manifest
  8. Observation: what happens when the image changes
  9. When a Deployment is not the right workload

  1. The Deployment → ReplicaSet → Pod chain

Kubernetes could have crammed all the functionality into a single object. It did not, and the reason is a good one: each level has one responsibility and only one.

flowchart TD
    D["<b>Deployment</b> web-store<br/>Manages VERSIONS<br/>history, update, rollback"]
    RS1["<b>ReplicaSet</b> web-store-7d9f8c6b4<br/>version 1.27.0 · replicas: 3<br/>Manages QUANTITY"]
    RS2["<b>ReplicaSet</b> web-store-5c8b7a2d1<br/>version 1.26.0 · replicas: 0<br/>previous revision, retained"]
    P1["Pod web-store-7d9f8c6b4-4kx7d"]
    P2["Pod web-store-7d9f8c6b4-9wq2m"]
    P3["Pod web-store-7d9f8c6b4-pv6cl"]
    D --> RS1
    D --> RS2
    RS1 --> P1
    RS1 --> P2
    RS1 --> P3

The division of labour, in a table:

Level Its only question What it can do What it cannot do
Pod Are my containers alive? Restart crashed containers (via the kubelet and restartPolicy) Recreate itself if it disappears
ReplicaSet Are there N pods with these labels? Create and delete pods until the number adds up Change version
Deployment Which version should be serving, and how do I get there? Create ReplicaSets, shift replicas between them, keep history, undo Nothing else: it delegates everything else

The key to understanding Deployments is this sentence: a Deployment does not manage pods, it manages ReplicaSets. Every time you change something in the template, the Deployment creates a new ReplicaSet and moves replicas from the old one to the new one. One ReplicaSet per revision, and each one knows how to maintain its own number of pods. The Deployment orchestrates the transfer.

That hexadecimal suffix in the names (web-store-7d9f8c6b4) is not random: it is the pod-template-hash, a hash computed over the contents of the template. The Deployment adds it as a label to every pod and to every ReplicaSet's selector, and that is how it ensures two ReplicaSets of the same Deployment never fight over the same pods. It is exactly the kind of set-based selector that, as we saw, the old ReplicationController could not express.

  1. From bare pod to Deployment: web-store

At the end of module 1 we left web-store as a bare pod, and we saw that deleting it did not bring it back. The time has come to settle that debt.

We start from an empty namespace, just as it was left at the end of the previous lesson:

kubectl config set-context --current --namespace=rutas-norte-dev
kubectl get all
No resources found in rutas-norte-dev namespace.

Converting a pod into a Deployment always follows the same mechanical pattern:

Bare pod Deployment
apiVersion: v1 apiVersion: apps/v1
kind: Pod kind: Deployment
metadata metadata (of the Deployment)
— spec.replicas
— spec.selector
spec: (the containers) spec.template.spec
metadata.labels spec.template.metadata.labels

In other words: the whole pod sinks one level and becomes the template, and replicas and selector appear above it.

  1. The complete manifest, annotated

# k8s/base/web-store-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-store
  namespace: rutas-norte-dev
  labels:
    app: web-store
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
  annotations:
    kubernetes.io/change-cause: "Initial rollout of web-store 1.27.0"
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: web-store
      environment: dev
  template:
    metadata:
      labels:
        app: web-store
        app.kubernetes.io/part-of: rutas-norte
        environment: dev
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - name: http
              containerPort: 80
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"

What is new compared with the previous lesson's ReplicaSet, field by field:

  • kind: Deployment, same apps/v1 group.
  • metadata.annotations with kubernetes.io/change-cause: free text that will be recorded in the revision history as the "reason" for this rollout. We will make full use of it in the next lesson.
  • spec.replicas: 3: three replicas of web-store. The Deployment does not create them itself; it delegates that to its ReplicaSet.
  • spec.revisionHistoryLimit: 5: how many old ReplicaSets (with 0 replicas) are kept so you can undo. The default is 10.
  • spec.selector: mandatory and immutable, just as in the ReplicaSet. And with the same warning: make it precise, app + environment.
  • spec.template: identical to the ReplicaSet's. Here lies the conceptual key: any change under template triggers a new revision; changes outside it (such as replicas) do not.

spec.strategy does not appear because the default value, RollingUpdate, is the one we want. That field and all its parameters are the entire content of the next lesson, Updates, Rollbacks and Deployment Strategies.

Apply it:

kubectl apply -f k8s/base/web-store-deployment.yaml
kubectl get deploy,rs,pods
deployment.apps/web-store created

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web-store   3/3     3            3           9s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/web-store-7d9f8c6b4    3         3         3       9s

NAME                              READY   STATUS    RESTARTS   AGE
pod/web-store-7d9f8c6b4-4kx7d     1/1     Running   0          9s
pod/web-store-7d9f8c6b4-9wq2m     1/1     Running   0          9s
pod/web-store-7d9f8c6b4-pv6cl     1/1     Running   0          9s

There is the complete chain, made real in three blocks of output. You created one object and three levels appeared. Check the genealogy:

kubectl get rs web-store-7d9f8c6b4 -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get pod web-store-7d9f8c6b4-4kx7d -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Deployment/web-store
ReplicaSet/web-store-7d9f8c6b4

And look at the label the Deployment has added to the pods on its own:

kubectl get pods -l app=web-store --show-labels
NAME                         READY   STATUS    RESTARTS   AGE   LABELS
web-store-7d9f8c6b4-4kx7d    1/1     Running   0          2m    app=web-store,app.kubernetes.io/part-of=rutas-norte,environment=dev,pod-template-hash=7d9f8c6b4

You did not write pod-template-hash. The Deployment added it, and it is what will let it tell one revision's pods from another's.

Verify that it does now heal itself:

kubectl delete pod web-store-7d9f8c6b4-4kx7d
kubectl get pods -l app=web-store
pod "web-store-7d9f8c6b4-4kx7d" deleted

NAME                         READY   STATUS    RESTARTS   AGE
web-store-7d9f8c6b4-9wq2m    1/1     Running   0          3m
web-store-7d9f8c6b4-hs4bd    1/1     Running   0          2s
web-store-7d9f8c6b4-pv6cl    1/1     Running   0          3m

The bare pod in module 1 never came back. This one comes back in two seconds, and what brings it back is not the Deployment but its ReplicaSet, exactly as you learned in the previous lesson.

  1. The bookings-api Deployment

Second component. Same scheme, with the API's particularities.

# k8s/base/bookings-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bookings-api
  namespace: rutas-norte-dev
  labels:
    app: bookings-api
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
  annotations:
    kubernetes.io/change-cause: "Initial rollout of bookings-api 2.4.0"
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: bookings-api
      environment: dev
  template:
    metadata:
      labels:
        app: bookings-api
        app.kubernetes.io/part-of: rutas-norte
        environment: dev
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: node:20-alpine
          command: ["node", "-e"]
          args:
            - |
              const http = require('http');
              const pod = process.env.HOSTNAME;
              http.createServer((req, res) => {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify({service: 'bookings-api', version: '2.4.0', pod}));
              }).listen(3000, () => console.log(`bookings-api 2.4.0 ready on ${pod}`));
          ports:
            - name: http
              containerPort: 3000
          env:
            - name: ENVIRONMENT
              value: "dev"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

Deliberate details in this manifest:

  • replicas: 2: two are enough in development. In production the number will be decided by the autoscaling of module 9.
  • terminationGracePeriodSeconds: 30 inside template.spec: remember from the Pods lesson that this is what stops a booking in flight from being cut off during a rollout.
  • The response includes the pod name (process.env.HOSTNAME, which inside a pod is its name). This will help us in the Services lesson to demonstrate load balancing: each request will answer with a different pod.
kubectl apply -f k8s/base/bookings-api-deployment.yaml
kubectl get deploy
deployment.apps/bookings-api created

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   2/2     2            2           14s
web-store      3/3     3            3           9m

Check that the API responds, using an ephemeral pod like the ones from the Pods lesson:

kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s http://$(kubectl get pod -l app=bookings-api -o jsonpath='{.items[0].status.podIP}'):3000/availability
{"service":"bookings-api","version":"2.4.0","pod":"bookings-api-6b4c9d7f5-x2jkp"}
pod "test" deleted

It works, but note the awkwardness: we had to find out the IP of a specific pod in order to talk to it. That IP will change on the next rollout. The problem is teed up for the Services lesson.

  1. Reading kubectl get deploy: READY, UP-TO-DATE, AVAILABLE

These three columns carry the densest information in all of Kubernetes, and confusing them leads to wrong diagnoses. All three count pods, but they count different things.

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   2/2     2            2           14s
Column What it counts exactly status field
READY ready pods / desired pods. "Ready" = all its containers pass their checks readyReplicas / replicas
UP-TO-DATE Pods created with the current revision of the template updatedReplicas
AVAILABLE Pods that have been ready for at least minReadySeconds in a row availableReplicas

The useful way to read them is to ask what each discrepancy means:

Situation Interpretation
READY 3/3, UP-TO-DATE 3, AVAILABLE 3 All correct and stable
READY 2/3 One pod is not ready: starting up, or failing
UP-TO-DATE 1 out of 3 Update in progress: only 1 pod has the new version
READY 3/3 but AVAILABLE 2 One pod is ready but has not yet met minReadySeconds; it counts as "young"
READY 0/3 for minutes Broken rollout: look at the pods with describe

An example of a half-finished update, which you will see a lot:

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   4/4     2            3           12m

That reads as follows: there are 4 ready pods and the desired number is 4, but only 2 have the new version, and only 3 have been up long enough to count as available. We are in the middle of a rolling update, and pods from two revisions still coexist.

To see more, -o wide adds images and selector:

kubectl get deploy -o wide
NAME           READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES            SELECTOR
bookings-api   2/2     2            2           14m   api          node:20-alpine    app=bookings-api,environment=dev
web-store      3/3     3            3           23m   nginx        nginx:1.27-alpine app=web-store,environment=dev

  1. status, conditions and kubectl rollout status

Like every Kubernetes object, a Deployment has a spec (what you ask for) and a status (what there is). The full status:

kubectl get deploy web-store -o jsonpath='{.status}' | python3 -m json.tool
{
    "availableReplicas": 3,
    "conditions": [
        {
            "lastTransitionTime": "2026-08-05T12:03:11Z",
            "lastUpdateTime": "2026-08-05T12:03:11Z",
            "message": "Deployment has minimum availability.",
            "reason": "MinimumReplicasAvailable",
            "status": "True",
            "type": "Available"
        },
        {
            "lastTransitionTime": "2026-08-05T12:03:05Z",
            "lastUpdateTime": "2026-08-05T12:03:11Z",
            "message": "ReplicaSet \"web-store-7d9f8c6b4\" has successfully progressed.",
            "reason": "NewReplicaSetAvailable",
            "status": "True",
            "type": "Progressing"
        }
    ],
    "observedGeneration": 1,
    "readyReplicas": 3,
    "replicas": 3,
    "updatedReplicas": 3
}

Conditions are Kubernetes' standard mechanism for expressing partial states, and a Deployment has three types:

Condition status: True means status: False means
Available There are enough available replicas to serve The Deployment is not serving reliably
Progressing The rollout is advancing (or finished well) It has stalled: it exceeded progressDeadlineSeconds
ReplicaFailure (only appears when there is a problem) Pods cannot be created: quota, permissions, limits —

The combination Available: True + Progressing: True with reason NewReplicaSetAvailable is the signature of a healthy, finished Deployment.

Another field worth attention is observedGeneration. Every change to the spec increments metadata.generation; the controller copies that number into status.observedGeneration once it has processed the change. If you see generation: 5 and observedGeneration: 4, the controller has not yet reacted to your latest change.

Day to day you do not read the JSON: you use the command that interprets it for you.

kubectl rollout status deployment/web-store
deployment "web-store" successfully rolled out

kubectl rollout status blocks the terminal until the rollout finishes and returns exit code 0 if it goes well, non-zero if it fails. That is why it is the instruction placed in any CI/CD pipeline right after kubectl apply: it turns "I have sent the change" into "the change is working". It accepts --timeout:

kubectl apply -f k8s/base/web-store-deployment.yaml
kubectl rollout status deployment/web-store --timeout=120s

  1. Scaling: kubectl scale and the manifest

Rutas Norte has a bank-holiday weekend coming up and bookings-api replicas need raising. Two routes, the same as with the ReplicaSet.

Imperative, to react right now:

kubectl scale deployment bookings-api --replicas=4
kubectl get deploy bookings-api
deployment.apps/bookings-api scaled

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   4/4     4            4           18m

Note a hugely important detail:

kubectl get rs -l app=bookings-api
NAME                     DESIRED   CURRENT   READY   AGE
bookings-api-6b4c9d7f5   4         4         4       18m

No new ReplicaSet has appeared. Scaling does not change the template, so it is not a new revision: the same ReplicaSet goes from 2 to 4. This distinction —changes that create a revision versus changes that do not— is the backbone of the next lesson.

Declarative, the correct one according to the project conventions: edit replicas: 4 in k8s/base/bookings-api-deployment.yaml, commit and apply.

kubectl apply -f k8s/base/bookings-api-deployment.yaml

And a very common practical warning: if you scale with kubectl scale and do not carry the change into the file, the next kubectl apply will take the replicas back to the manifest's value. You will lose the extra capacity at the worst possible moment. That is the reason for the project rule: the cluster reflects Git, not the other way round.

A third route, useful for automating without editing files by hand:

kubectl patch deployment bookings-api --type=merge -p '{"spec":{"replicas":4}}'

Let's go back to 2 before moving on:

kubectl scale deployment bookings-api --replicas=2

  1. Observation: what happens when the image changes

This is the observation that motivates the whole next lesson. Change the nginx version of web-store and look at the structure that appears, without worrying about the mechanism yet.

kubectl set image deployment/web-store nginx=nginx:1.27.1-alpine
kubectl get rs -l app=web-store
deployment.apps/web-store image updated

NAME                   DESIRED   CURRENT   READY   AGE
web-store-5f6d8b9c7    3         3         3       25s
web-store-7d9f8c6b4    0         0         0       31m

There are two ReplicaSets. The new one (5f6d8b9c7) has the 3 replicas; the old one (7d9f8c6b4) has dropped to 0 but has not been deleted. And there lies the key to the history: that empty ReplicaSet keeps the complete definition of the previous version, so going back is simply a matter of giving it its replicas again.

Compare that with what happened in the previous lesson when running kubectl set image against a ReplicaSet: nothing, the pods kept the old image. Here, by contrast, the pods are new and have the new image:

kubectl get pods -l app=web-store -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
NAME                         IMAGE
web-store-5f6d8b9c7-2xk4m    nginx:1.27.1-alpine
web-store-5f6d8b9c7-7q9wd    nginx:1.27.1-alpine
web-store-5f6d8b9c7-nb3rt    nginx:1.27.1-alpine

And the history already has two entries:

kubectl rollout history deployment/web-store
deployment.apps/web-store
REVISION  CHANGE-CAUSE
1         Initial rollout of web-store 1.27.0
2         <none>

For now, take away these four observations:

  1. Changing the template creates a new ReplicaSet, with a different pod-template-hash.
  2. The Deployment shifts replicas from the old one to the new one gradually, not all at once.
  3. Old ReplicaSets are kept at 0 replicas as history (up to revisionHistoryLimit).
  4. During the transfer pods from two versions coexist, and that is what makes deploying without downtime possible.

How that transfer is controlled step by step —maxSurge, maxUnavailable, Recreate, pause, undo— is exactly the content of Updates, Rollbacks and Deployment Strategies. And the advanced variants, blue-green and canary, are waiting in 11-04.

Take web-store back to the original image so you start the next lesson from a known point:

kubectl apply -f k8s/base/web-store-deployment.yaml
kubectl rollout status deployment/web-store
deployment.apps/web-store configured
deployment "web-store" successfully rolled out

  1. When a Deployment is not the right workload

The Deployment is the default option, but not the only one. It is designed for stateless, interchangeable, long-running workloads: its pods are cattle, not pets. They are named with random suffixes, they are created and destroyed in any order and none of them has an identity of its own.

When that does not fit, there is another object:

Need Correct object Why the Deployment fails Lesson
Stable identity and storage per replica (databases, queues, quorum-based clusters) StatefulSet Random names, no guaranteed startup order, all replicas would share the same volume 06-01
Exactly one pod on every node (log, metrics or network agents) DaemonSet A Deployment spreads N replicas wherever they fit, with no guarantee of node coverage 06-02
A task that runs, finishes and that's it Job A Deployment would restart the pod forever once it finished (restartPolicy: Always is mandatory) 06-03
A task scheduled by time of day CronJob A Deployment has no notion of a calendar 06-03

Applied to the six components of Rutas Norte:

Component Workload Reason
web-store Deployment Stateless, identical and interchangeable replicas
bookings-api Deployment Stateless; all persistence lives in PostgreSQL and Redis
bookings-postgres StatefulSet It needs its own disk, a stable network name and ordered startup
redis-cache Deployment (one replica) or StatefulSet With a single instance and expendable state, a Deployment is enough
notifications-worker Deployment Long-running stateless process; it receives no traffic, so it will carry no Service
occupancy-reports CronJob It runs at 02:30, finishes and does not come back until the next day

A warning that prevents a classic mistake: redis-cache as a Deployment is only acceptable with one replica. If you scaled it to 3, you would have three independent caches behind the same name, and each bookings-api request would go to a different cache with different contents. The result would be erratic cache hits that are impossible to diagnose. Scaling is not free: it only works if the component is genuinely stateless.

Common Mistakes and Tips

  • Forgetting spec.selector. It is mandatory in apps/v1. Without it, the API rejects the manifest.
  • Trying to change the selector of an existing Deployment. It is immutable. If you need to, you have to delete the Deployment (with --cascade=orphan if you do not want to interrupt the service) and create a new one.
  • Putting the labels only in the top-level metadata. The ones that govern the pods are in spec.template.metadata.labels. It is the most repeated beginner's mistake.
  • Scaling with kubectl scale and not touching the file. The next apply will revert the change, probably at a bad moment.
  • Confusing READY with AVAILABLE. READY means "it can serve"; AVAILABLE means "it can serve and has been stable for the minimum required time".
  • Believing that kubectl apply waits for the rollout to finish. It does not wait: it only sends the object. To wait for real, chain kubectl rollout status.
  • Using a Deployment for PostgreSQL. With more than one replica, several instances would try to mount the same volume and write at once. Corruption guaranteed.
  • Scaling redis-cache to several replicas with a Deployment. You will get N incoherent caches, not one replicated cache.
  • Tip: generate the skeleton with kubectl create deployment bookings-api --image=node:20-alpine --replicas=2 --dry-run=client -o yaml > deploy.yaml and add labels, resources and annotations afterwards.
  • Tip: to see a component's complete chain at a glance, kubectl get deploy,rs,pods -l app=web-store. Three levels in a single command.

Exercises

Exercise 1: A notifications-worker Deployment

notifications-worker is a background process that consumes a queue and sends emails. It receives no inbound traffic, so it carries no ports and no Service.

Write k8s/base/notifications-worker-deployment.yaml with 2 replicas in rutas-norte-dev, image busybox:1.36, a command that writes sending batch of confirmations from <pod name> every 10 seconds, the project's three labels, terminationGracePeriodSeconds: 45 and requests of 50m/64Mi. Then:

  1. Apply it and wait using the command that blocks until the rollout finishes.
  2. Show the Deployment → ReplicaSet → Pod chain with a single command.
  3. Check in the logs that both pods are working.
  4. Explain why this component has a longer grace period than web-store.

Exercise 2: Reading the state of a rollout

A colleague sends you this capture from a preproduction cluster and asks for a diagnosis:

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   3/5     2            3           7m
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    False   ProgressDeadlineExceeded

Answer, justifying each answer:

  1. How many pods should exist and how many are ready?
  2. How many have the new version?
  3. Is the service down?
  4. What has happened to the rollout?
  5. Which two commands would you run, in that order, to find the root cause?

Exercise 3: From bare pod to Deployment, and choosing the right workload

  1. Create a bare redis-cache pod by hand in rutas-norte-dev with redis:7.2-alpine and the project's three labels.
  2. Convert it into a 1-replica Deployment called redis-cache, writing the manifest and deleting the bare pod. Verify that the cache responds to redis-cli ping.
  3. Scale the Deployment to 3 replicas and explain why this is a design mistake for Rutas Norte, even though it technically works. Go back to 1.
  4. For each of the six Rutas Norte components, state the correct object and one sentence of justification.

Solutions

Solution 1

# k8s/base/notifications-worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notifications-worker
  namespace: rutas-norte-dev
  labels:
    app: notifications-worker
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
  annotations:
    kubernetes.io/change-cause: "Initial rollout of notifications-worker 1.2.0"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: notifications-worker
      environment: dev
  template:
    metadata:
      labels:
        app: notifications-worker
        app.kubernetes.io/part-of: rutas-norte
        environment: dev
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: worker
          image: busybox:1.36
          command: ["sh", "-c"]
          args:
            - |
              trap 'echo "closing: finishing pending sends"; sleep 5; exit 0' TERM
              while true; do
                echo "sending batch of confirmations from $HOSTNAME"
                sleep 10
              done
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"
kubectl apply -f k8s/base/notifications-worker-deployment.yaml
kubectl rollout status deployment/notifications-worker
kubectl get deploy,rs,pods -l app=notifications-worker
kubectl logs -l app=notifications-worker --tail=2 --prefix
deployment.apps/notifications-worker created
deployment "notifications-worker" successfully rolled out

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/notifications-worker   2/2     2            2           12s

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/notifications-worker-8c7b5d94f    2         2         2       12s

NAME                                         READY   STATUS    RESTARTS   AGE
pod/notifications-worker-8c7b5d94f-k2xrt     1/1     Running   0          12s
pod/notifications-worker-8c7b5d94f-p9mzq     1/1     Running   0          12s

[pod/notifications-worker-8c7b5d94f-k2xrt/worker] sending batch of confirmations from notifications-worker-8c7b5d94f-k2xrt
[pod/notifications-worker-8c7b5d94f-p9mzq/worker] sending batch of confirmations from notifications-worker-8c7b5d94f-p9mzq
  1. The grace period is longer because notifications-worker may be halfway through sending an email when it receives the SIGTERM. If the kubelet kills it before it finishes, a customer who has paid for their ticket is left with no receipt and the message can end up in an ambiguous state (consumed from the queue but not sent). web-store only serves static files: its requests last milliseconds and 30 seconds is more than enough.

Solution 2

  1. There should be 5 pods (spec.replicas = 5, the denominator of READY) and 3 are ready. Two are missing.
  2. Only 2 have the new version (UP-TO-DATE). The other ready pods belong to the previous revision.
  3. It is not down. Available: True with reason MinimumReplicasAvailable indicates there are enough available replicas to serve traffic. It is degraded, though: it is serving with 3 pods instead of 5.
  4. The rollout has stalled: Progressing: False with reason ProgressDeadlineExceeded means it exceeded progressDeadlineSeconds (600 s by default) without advancing. The 2 pods of the new version never become ready, so the Deployment cannot carry on retiring the old ones. Typical cause: a non-existent image, CrashLoopBackOff from configuration or a badly tuned probe.
  5. The two commands, in this order:
# 1. See which pods are failing and for what reason
kubectl get pods -l app=bookings-api -n rutas-norte-pre

# 2. The root cause of the problematic pod
kubectl describe pod <pod-that-wont-start> -n rutas-norte-pre
# and, if the container restarts:
kubectl logs <pod-that-wont-start> -n rutas-norte-pre --previous

Solution 3

# 1. Bare pod
kubectl run redis-cache --image=redis:7.2-alpine \
  --labels="app=redis-cache,app.kubernetes.io/part-of=rutas-norte,environment=dev"
# 2. k8s/base/redis-cache-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
  annotations:
    kubernetes.io/change-cause: "Initial rollout of redis-cache 7.2"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis-cache
      environment: dev
  template:
    metadata:
      labels:
        app: redis-cache
        app.kubernetes.io/part-of: rutas-norte
        environment: dev
    spec:
      containers:
        - name: redis
          image: redis:7.2-alpine
          ports:
            - name: redis
              containerPort: 6379
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "256Mi"
kubectl delete pod redis-cache
kubectl apply -f k8s/base/redis-cache-deployment.yaml
kubectl rollout status deployment/redis-cache
kubectl exec deploy/redis-cache -- redis-cli ping
pod "redis-cache" deleted
deployment.apps/redis-cache created
deployment "redis-cache" successfully rolled out
PONG

Practical note: kubectl exec deploy/redis-cache automatically picks one of the Deployment's pods, so there is no need to copy the name with its random suffix.

# 3. Scale to 3 and back
kubectl scale deployment redis-cache --replicas=3
kubectl get pods -l app=redis-cache
kubectl scale deployment redis-cache --replicas=1

It is a design mistake because the three replicas do not form a replicated cache: they are three independent, empty caches, each with its own memory. When in the next lesson we put a Service in front, each bookings-api query will land on a different replica, so the seat availability written to replica A will not be found when reading from B. The result would be a cache hit rate of around 33 %, extra queries to bookings-postgres and, worst of all, inconsistent responses depending on which replica the request lands on. Replicating Redis properly requires a StatefulSet with replica or cluster configuration, the subject of module 6.

  1. The correct object per component:
Component Object Justification
web-store Deployment Stateless; any replica serves any request
bookings-api Deployment Stateless; it persists everything in PostgreSQL and Redis
bookings-postgres StatefulSet It needs its own volume, a stable network identity and ordered startup
redis-cache 1-replica Deployment Expendable, rebuildable state; more replicas would require a StatefulSet
notifications-worker Deployment Long-running, stateless process with no inbound traffic
occupancy-reports CronJob Scheduled, finite execution every night at 02:30

Conclusion

You now have a command of the object that holds up most of any Kubernetes platform. You have seen the Deployment → ReplicaSet → Pod chain with a clear-cut responsibility at every link: the pod keeps its containers alive, the ReplicaSet keeps the quantity, and the Deployment manages the versions. You know that a Deployment commands ReplicaSets rather than pods, and that the pod-template-hash is the trick that lets two revisions coexist without fighting over the same pods.

You have settled the debt we had been carrying since module 1: web-store is no longer a fragile pod but a 3-replica Deployment that recovers on its own in two seconds, and bookings-api has its own with 2 replicas. You can read the three columns of kubectl get deploy without confusing them —READY is what is serving, UP-TO-DATE what has the current version, AVAILABLE what has also been stable for the minimum time— and interpret the Available and Progressing conditions, the ones that will tell you in production whether a rollout is going well, is degraded or has stalled. You scale with kubectl scale to put out fires and with the manifest so the change lasts, and you know the cluster reflects Git and not the other way round. On top of that, you have the judgement not to force the tool: PostgreSQL calls for a StatefulSet, an agent per node calls for a DaemonSet, the nightly report calls for a CronJob, and scaling redis-cache to three replicas would break the cache's coherence.

The most interesting thing you have seen is still unexplained: on changing the image a second ReplicaSet appeared, the old one dropped to 0 replicas without being deleted and for a few seconds pods from two versions coexisted. That is not a side effect, it is the mechanism with which Rutas Norte will finally achieve its goal of zero downtime on deployments, against the one or two minutes of outage in the small hours of Tuesday. In the next lesson, Updates, Rollbacks and Deployment Strategies, we will control that transfer pod by pod with maxSurge and maxUnavailable, we will see why bookings-postgres would not tolerate a rolling update, and we will learn to diagnose and undo a broken rollout with kubectl rollout undo.

Kubernetes Course

Module 1: Introduction to Kubernetes

Module 2: Core Kubernetes Components

Module 3: Configuration and Secret Management

Module 4: Networking in Kubernetes

Module 5: Storage in Kubernetes

Module 6: Advanced Kubernetes Concepts

Module 7: Monitoring and Logging

Module 8: Kubernetes Security

Module 9: Scaling and Performance

Module 10: Kubernetes Ecosystem and Tooling

Module 11: Case Studies and Real-World Applications

Module 12: Preparing for Kubernetes Certification

© Copyright 2026. All rights reserved