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
- The Deployment → ReplicaSet → Pod chain
- From bare pod to Deployment:
web-store - The complete manifest, annotated
- The
bookings-apiDeployment - Reading
kubectl get deploy:READY,UP-TO-DATE,AVAILABLE status,conditionsandkubectl rollout status- Scaling:
kubectl scaleand the manifest - Observation: what happens when the image changes
- When a Deployment is not the right workload
- 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.
- From bare pod to Deployment:
web-store
web-storeAt 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:
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.
- 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, sameapps/v1group.metadata.annotationswithkubernetes.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 ofweb-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 undertemplatetriggers a new revision; changes outside it (such asreplicas) 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:
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 9sThere 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"}'And look at the label the Deployment has added to the pods on its own:
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=7d9f8c6b4You 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:
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 3mThe 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.
- The
bookings-api Deployment
bookings-api DeploymentSecond 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: 30insidetemplate.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.
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 9mCheck 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" deletedIt 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.
- Reading
kubectl get deploy: READY, UP-TO-DATE, AVAILABLE
kubectl get deploy: READY, UP-TO-DATE, AVAILABLEThese 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.
| 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:
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:
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
status, conditions and kubectl rollout status
status, conditions and kubectl rollout statusLike every Kubernetes object, a Deployment has a spec (what you ask for) and a status (what there is). The full status:
{
"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 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
- Scaling:
kubectl scale and the manifest
kubectl scale and the manifestRutas 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:
Note a hugely important detail:
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.
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:
Let's go back to 2 before moving on:
- 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.
deployment.apps/web-store image updated
NAME DESIRED CURRENT READY AGE
web-store-5f6d8b9c7 3 3 3 25s
web-store-7d9f8c6b4 0 0 0 31mThere 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].imageNAME 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-alpineAnd the history already has two entries:
For now, take away these four observations:
- Changing the
templatecreates a new ReplicaSet, with a differentpod-template-hash. - The Deployment shifts replicas from the old one to the new one gradually, not all at once.
- Old ReplicaSets are kept at 0 replicas as history (up to
revisionHistoryLimit). - 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:
- 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 inapps/v1. Without it, the API rejects the manifest. - Trying to change the
selectorof an existing Deployment. It is immutable. If you need to, you have to delete the Deployment (with--cascade=orphanif 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 inspec.template.metadata.labels. It is the most repeated beginner's mistake. - Scaling with
kubectl scaleand not touching the file. The nextapplywill revert the change, probably at a bad moment. - Confusing
READYwithAVAILABLE.READYmeans "it can serve";AVAILABLEmeans "it can serve and has been stable for the minimum required time". - Believing that
kubectl applywaits for the rollout to finish. It does not wait: it only sends the object. To wait for real, chainkubectl 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-cacheto 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.yamland 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:
- Apply it and wait using the command that blocks until the rollout finishes.
- Show the Deployment → ReplicaSet → Pod chain with a single command.
- Check in the logs that both pods are working.
- 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:
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceededAnswer, justifying each answer:
- How many pods should exist and how many are ready?
- How many have the new version?
- Is the service down?
- What has happened to the rollout?
- 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
- Create a bare
redis-cachepod by hand inrutas-norte-devwithredis:7.2-alpineand the project's three labels. - Convert it into a 1-replica Deployment called
redis-cache, writing the manifest and deleting the bare pod. Verify that the cache responds toredis-cli ping. - 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.
- 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 --prefixdeployment.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- The grace period is longer because
notifications-workermay be halfway through sending an email when it receives theSIGTERM. 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-storeonly serves static files: its requests last milliseconds and 30 seconds is more than enough.
Solution 2
- There should be 5 pods (
spec.replicas = 5, the denominator ofREADY) and 3 are ready. Two are missing. - Only 2 have the new version (
UP-TO-DATE). The other ready pods belong to the previous revision. - It is not down.
Available: Truewith reasonMinimumReplicasAvailableindicates there are enough available replicas to serve traffic. It is degraded, though: it is serving with 3 pods instead of 5. - The rollout has stalled:
Progressing: Falsewith reasonProgressDeadlineExceededmeans it exceededprogressDeadlineSeconds(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,CrashLoopBackOfffrom configuration or a badly tuned probe. - 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 --previousSolution 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 pingpod "redis-cache" deleted
deployment.apps/redis-cache created
deployment "redis-cache" successfully rolled out
PONGPractical 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=1It 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.
- 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
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
