Chapter 59: Kubernetes Pods in Depth: Architecture, YAML Anatomy and Pod Types
59.8 Pod lifecycle: phases, restartPolicy, termination and graceful shutdown
Why: the problem
Suppose we release a new version of the Flipkart checkout service during a sale. Kubernetes replaces the old pods. For a few seconds, some customers see 502 errors and some payments are cut in the middle. Nothing was wrong with the new code. The old pods were killed too fast: they got SIGTERM, did not handle it, and 30 seconds later got SIGKILL while requests were still running. Also, the Service kept sending traffic to pods that were already shutting down. To fix this we must understand the pod lifecycle: the phases Pending, Running, Succeeded, Failed and Unknown; container states; restartPolicy; and exactly what happens on delete: SIGTERM, preStop hook, the grace period, and SIGKILL. In this lesson we watch each phase on a kind cluster, compare the three restart policies, and build a pod that shuts down gracefully. Then we break it with an app that ignores SIGTERM and a preStop hook longer than the grace period.
What: every term in plain English
- Pod phase — A one-word summary: Pending, Running, Succeeded, Failed or Unknown.
- Pending — Accepted by the API, but not all containers are running yet: waiting for scheduling, image pull or init containers.
- Running — Bound to a node and at least one container is running or restarting.
- Succeeded / Failed — All containers have stopped: all with exit 0, or at least one with a non-zero code.
- Unknown — The node stopped reporting, often a network or node failure.
- Container state — Waiting, Running or Terminated, each with a reason like ContainerCreating, CrashLoopBackOff or Completed.
- restartPolicy — Always (default), OnFailure or Never. It applies to all containers in the pod.
- CrashLoopBackOff — The container keeps failing; the kubelet waits 10s, 20s, 40s and so on up to 5 minutes between restarts.
- SIGTERM — The polite stop signal sent first. The app should finish work and exit.
- terminationGracePeriodSeconds — Time between SIGTERM and SIGKILL. Default is 30.
- preStop hook — A command or HTTP call that runs before SIGTERM is sent.
- Conditions — PodScheduled, Initialized, ContainersReady and Ready, each True or False.
How: step by step
- Watch phases. kubectl get pod -w and kubectl describe pod.
- Pick restartPolicy. Always for servers, OnFailure for batch Jobs, Never for one-shot debug pods.
- Handle SIGTERM in the app. Stop accepting new requests, finish current ones, then exit.
- Add a preStop sleep. sleep 5 lets the Service and load balancer remove the pod first.
- Set the grace period. terminationGracePeriodSeconds longer than preStop plus the longest request.
- Add readiness. Ready=False removes the pod from Service endpoints.
- Test with delete. kubectl delete pod and read the app logs and events.
Example 1: Watch every phase and condition
Goal. Watch every phase and condition.
phases.yaml
apiVersion: v1
kind: Pod
metadata:
name: short-task
namespace: shop
spec:
restartPolicy: Never
containers:
- name: task
image: busybox:1.36
command: ["sh", "-c", "echo start; sleep 15; echo done"]
---
apiVersion: v1
kind: Pod
metadata:
name: bad-task
namespace: shop
spec:
restartPolicy: Never
containers:
- name: task
image: busybox:1.36
command: ["sh", "-c", "echo oops; exit 3"]
---
apiVersion: v1
kind: Pod
metadata:
name: no-image
namespace: shop
spec:
containers:
- name: task
image: busybox:9.99-does-not-exist
What each line does
short-task— Goes Pending, then Running for 15 seconds, then Succeeded.restartPolicy: Never— No restart, so the final phase is visible.exit 3— Non-zero exit, so bad-task ends Failed.busybox:9.99-does-not-exist— Stays Pending with ErrImagePull, then ImagePullBackOff.
Green output
$ kubectl apply -f phases.yaml
$ kubectl get pods -n shop -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,REASON:.status.containerStatuses[0].state.*.reason
NAME PHASE REASON
bad-task Failed Error
no-image Pending ImagePullBackOff
short-task Running none
$ kubectl get pod short-task -n shop -o jsonpath='{range .status.conditions[*]}{.type}={.status} {end}{"\n"}'
PodReadyToStartContainers=True Initialized=True Ready=True ContainersReady=True PodScheduled=True
$ sleep 15; kubectl get pod short-task -n shop
NAME READY STATUS RESTARTS AGE
short-task 0/1 Completed 0 20s
Expected result. STATUS in kubectl get is a friendly mix of phase and container reason. The real phase is in .status.phase.
Example 2: Compare restartPolicy Always, OnFailure and Never
Goal. Compare restartPolicy Always, OnFailure and Never.
restart.yaml
apiVersion: v1
kind: Pod
metadata:
name: rp-always
namespace: shop
spec:
restartPolicy: Always
containers:
- name: c
image: busybox:1.36
command: ["sh", "-c", "sleep 3; exit 0"]
---
apiVersion: v1
kind: Pod
metadata:
name: rp-onfailure
namespace: shop
spec:
restartPolicy: OnFailure
containers:
- name: c
image: busybox:1.36
command: ["sh", "-c", "sleep 3; exit 1"]
---
apiVersion: v1
kind: Pod
metadata:
name: rp-never
namespace: shop
spec:
restartPolicy: Never
containers:
- name: c
image: busybox:1.36
command: ["sh", "-c", "sleep 3; exit 1"]
What each line does
Always + exit 0— Restarts even after success. That is why servers use it, and why a finishing script loops in CrashLoopBackOff.OnFailure + exit 1— Restarts only on failure. Jobs use this.Never + exit 1— Stays Failed with the logs kept for reading.
Green output
$ kubectl apply -f restart.yaml
$ sleep 60; kubectl get pods -n shop -l '!app' | grep rp-
rp-always 0/1 CrashLoopBackOff 4 (20s ago) 60s
rp-onfailure 0/1 CrashLoopBackOff 4 (18s ago) 60s
rp-never 0/1 Error 0 60s
$ kubectl get pod rp-always -n shop -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
0
Expected result. rp-always exits 0 but still shows CrashLoopBackOff. A server pod must never exit. Use a Job for work that finishes.
Example 3: Graceful shutdown with preStop and a grace period
Goal. Graceful shutdown with preStop and a grace period.
graceful.yaml
apiVersion: v1
kind: Pod
metadata:
name: checkout
namespace: shop
labels:
app: checkout
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"]
args:
- trap 'echo "SIGTERM: finishing 3 open orders"; sleep 10; echo "clean exit"; exit 0' TERM;
echo "checkout ready";
while true; do sleep 1; done
lifecycle:
preStop:
exec:
command: ["sh", "-c", "echo preStop: waiting for LB > /proc/1/fd/1; sleep 5"]
readinessProbe:
exec:
command: ["true"]
periodSeconds: 2
What each line does
terminationGracePeriodSeconds: 45— Total time allowed: preStop 5s + app 10s fits easily.trap ... TERM— The shell catches SIGTERM, like a real app's shutdown handler.sleep 10 in trap— Stands in for finishing open requests.preStop sleep 5— Runs first. While it runs, the pod is already removed from Service endpoints./proc/1/fd/1— Writes to the main process stdout, so the preStop line shows in kubectl logs.
Green output
$ kubectl apply -f graceful.yaml
$ kubectl logs -n shop checkout -f &
checkout ready
$ time kubectl delete pod checkout -n shop
preStop: waiting for LB
SIGTERM: finishing 3 open orders
clean exit
pod "checkout" deleted
real 0m16.4s
Expected result. Order on delete: endpoints removed and preStop runs, then SIGTERM, then the app exits cleanly. SIGKILL never came.
Every command, step by step (kind lab with kubectl)
-
Watch a pod's life
kubectl apply -f phases.yaml kubectl get pod short-task -n shop -wWhat it does: Pending, ContainerCreating, Running, Completed.
short-task 0/1 Pending 0 0s short-task 0/1 ContainerCreating 0 0s short-task 1/1 Running 0 2s short-task 0/1 Completed 0 17s -
Read the phase field
kubectl get pod short-task -n shop -o jsonpath='{.status.phase}{"\n"}'What it does: The real phase, not the friendly STATUS.
Succeeded -
Read the last exit
kubectl get pod bad-task -n shop -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'What it does: 3, as the script returned.
3 -
Logs of a crashed run
kubectl logs -n shop rp-onfailure --previousWhat it does: --previous shows the run before the last restart.
(output of the last failed run) -
See back-off in events
kubectl get events -n shop --field-selector involvedObject.name=rp-always | tail -2What it does: Back-off restarting failed container.
Warning BackOff pod/rp-always Back-off restarting failed container c -
Graceful delete
kubectl apply -f graceful.yaml kubectl delete pod checkout -n shopWhat it does: Takes about 15 seconds, not instantly.
pod "checkout" deleted -
Ungraceful delete
kubectl apply -f graceful.yaml kubectl delete pod checkout -n shop --grace-period=0 --forceWhat it does: Skips everything. Only for stuck pods.
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. pod "checkout" force deleted -
Watch endpoints during shutdown
printf '%s\n' \ 'apiVersion: v1' \ 'kind: Service' \ 'metadata:' \ ' name: checkout' \ ' namespace: shop' \ 'spec:' \ ' selector:' \ ' app: checkout' \ ' ports:' \ ' - port: 80' \ ' targetPort: 80' \ > checkout-svc.yaml kubectl apply -f checkout-svc.yaml -f graceful.yaml # in a second terminal: kubectl delete pod checkout -n shop kubectl get endpointslices -n shop -l kubernetes.io/service-name=checkout -wWhat it does: The full Service file is written by the first command; its selector app: checkout matches the pod in graceful.yaml. With a Service in front, the IP disappears the moment delete starts.
checkout-abc12 IPv4 80 10.244.2.9 1m checkout-abc12 IPv4 80 none 1m -
Clean up
kubectl delete pod -n shop short-task bad-task no-image rp-always rp-onfailure rp-never kubectl delete svc checkout -n shopWhat it does: Ready for chapter 60.
pod "short-task" deleted
When it is red 1
Last State: Terminated
Reason: Error
Exit Code: 137
Normal Killing Stopping container app (grace period 30s exceeded)
Cause. The app ignored SIGTERM, so it was killed after the grace period.
Fix, step by step
- Handle SIGTERM in code (Node: process.on('SIGTERM'), Spring: server.shutdown=graceful).
- Run the app as PID 1 directly, or use tini, so the signal reaches it.
- Use exec form in the Dockerfile CMD, not shell form.
When it is red 2
Exit Code: 137 after exactly 30s, preStop logs show sleep 40
Cause. preStop is longer than terminationGracePeriodSeconds. The grace timer includes preStop.
Fix, step by step
- Make the grace period bigger than preStop plus shutdown time.
- For example preStop 5s, shutdown 20s, grace 40s.
When it is red 3
pod stuck in Terminating for 20 minutes
Cause. Usually a finalizer, or the node is unreachable.
Fix, step by step
- kubectl get pod -o jsonpath='{.metadata.finalizers}'.
- If the node is gone, kubectl delete pod --grace-period=0 --force.
- Check the node with kubectl get nodes.
When it is red 4
STATUS: Unknown node ch59-worker2 NotReady
Cause. The kubelet on that node stopped reporting.
Fix, step by step
- kubectl describe node ch59-worker2.
- After about 5 minutes pods are evicted and controllers make new ones (lesson 63.4).
- For kind: docker start ch59-worker2.
Practice tasks
Easy Task 1: Phases
Why: Read status.
What to do: Apply phases.yaml and print each pod's .status.phase.
- custom-columns.
Solution
kubectl get pods -n shop -o custom-columns=N:.metadata.name,P:.status.phaseEasy Task 2: Conditions
Why: Read conditions.
What to do: Print the conditions of short-task.
- jsonpath range.
Solution
kubectl get pod short-task -n shop -o jsonpath='{.status.conditions}'Easy Task 3: Previous
Why: Read crash logs.
What to do: Read the previous logs of rp-onfailure.
- --previous.
Solution
kubectl logs -n shop rp-onfailure --previousMedium Task 4: Policies
Why: Compare.
What to do: Run restart.yaml and explain each STATUS after one minute.
- Wait 60s.
Solution
kubectl get pods -n shop | grep rp-Medium Task 5: Back-off
Why: Timing.
What to do: Note the time between restarts of rp-always.
- get events.
Solution
kubectl get events -n shop --field-selector involvedObject.name=rp-alwaysMedium Task 6: Graceful
Why: Clean exit.
What to do: Delete checkout and see the three log lines.
- logs -f in another terminal.
Solution
kubectl delete pod checkout -n shopMedium Task 7: Ignore TERM
Why: Feel the pain.
What to do: Remove the trap and see delete take 45 seconds.
- Edit args.
Solution
time kubectl delete pod checkout -n shopMedium Task 8: Too long preStop
Why: Grace math.
What to do: Set preStop sleep 60 with grace 45 and read the exit code.
- Exit code 137.
Solution
preStop sleep 60Hard Task 9: Real app
Why: Spring Boot or Node.
What to do: Add a SIGTERM handler to a small Node server and test it in a pod.
- process.on('SIGTERM', ...).
Solution
process.on('SIGTERM', () => server.close(() => process.exit(0)))Hard Task 10: Zero 502
Why: Rollout safety.
What to do: Put checkout in a Deployment with 3 replicas, run a curl loop and roll out a change with no errors.
- preStop + readiness.
- kubectl rollout restart.
Solution
while true; do curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080; doneHomework: try at home
For one app you run, write down how long its longest request takes, then set preStop, terminationGracePeriodSeconds and a SIGTERM handler, and test with kubectl delete.
Recap
- Phases: Pending, Running, Succeeded, Failed, Unknown; containers have their own states.
- restartPolicy Always for servers, OnFailure for Jobs, Never for one-shot pods.
- CrashLoopBackOff means repeated failure with growing waits.
- On delete: endpoints removed and preStop, then SIGTERM, then SIGKILL after the grace period.
- Handle SIGTERM, add a short preStop sleep, and size the grace period.
- Chapter 60 starts the controllers that create and replace pods for you.
FAQ
Q. Why does my Completed pod show CrashLoopBackOff?
restartPolicy Always restarts even after exit 0. Use a Job for finishing work.
Q. Does preStop run on a crash?
No. It runs only when Kubernetes stops the container, such as delete, rollout or eviction.
Q. Is force delete safe?
Only when the node is gone. Otherwise two copies may run for a while.
Q. How long should preStop sleep be?
Usually 5 to 15 seconds, enough for load balancers to update.
Q. Why does my app not get SIGTERM?
A shell wrapper is PID 1 and does not forward signals. Use exec form CMD or tini.
Q. Can I change restartPolicy per container?
Only native sidecars may set Always; otherwise it is pod-wide.
Interview questions
-
Explain the pod lifecycle.
Model answer: Pending while scheduling, pulling and init; Running once bound with a running container; then Succeeded or Failed when all containers stop; Unknown if the node stops reporting.
-
What happens when a pod is deleted?
Model answer: It is marked Terminating, removed from endpoints, preStop runs, SIGTERM is sent, and after terminationGracePeriodSeconds remaining processes get SIGKILL.
-
How do you get zero-downtime shutdown?
Model answer: Handle SIGTERM, use readiness probes, add a preStop sleep, and set a long enough grace period.
-
What is CrashLoopBackOff and how do you debug it?
Model answer: Repeated container failure with exponential back-off; use describe, logs --previous, exit codes and events.
-
Difference between restart policies?
Model answer: Always restarts every exit, OnFailure only non-zero exits, Never never restarts.
Quick quiz
Q1. Name the five pod phases.
Pending, Running, Succeeded, Failed, Unknown.
Q2. What is the default restartPolicy?
Always.
Q3. What is sent first on delete, SIGTERM or SIGKILL?
SIGTERM, after preStop.
Q4. Default grace period?
30 seconds.
Q5. Which flag shows logs of the previous crash?
--previous.
Samajla ka?
Lab
- Create one kind cluster with one control plane and two workers.
- Keep every YAML file in a folder named k8s-ch59.
- Run kubectl get events after every lab.
- Break one thing on purpose in each lesson and fix it.
- Delete the cluster at the end with kind delete cluster.
Practice task
Build one pod with an init container, a sidecar log shipper and the main app, then explain each container to a friend.
Ravindra Bagale's Tip
Pod samajla tar Kubernetes samajla. Interview madhe pod lifecycle aani init vs sidecar nehmi vichartat.
In short
- A pod is the smallest unit: one or more containers sharing network and volumes.
- Every YAML has apiVersion, kind, metadata and spec.
- Init containers run first, one by one; sidecars run beside the app.
- Static pods are run by the kubelet from files, not by the API server.