Kubernetes · lesson 2 of 10
Intermediate
6Advanced
1Also part of Kubernetes
5- + Deploying FastAPI with Docker and Kubernetes: Images, Signals, and Probes from FastAPI
- + Deployment Strategies Compared: Rolling, Blue-Green, and Canary from DevOps
- + GitOps in Practice: Git as the Source of Truth from DevOps
- + Kubernetes Pod Security Admission: Enforcing the Restricted Profile from Cloud Security
- + CIS Kubernetes Benchmark Checks with kube-bench in kind from Cloud Security
Pods and Workloads
Understand pods, containers, labels, selectors, and how Kubernetes schedules and manages workload lifecycles.
By SkillByExample · Updated
Pod Basics
A Pod groups one or more containers together so they can run as a single unit on one node. Every container in a Pod shares the same network namespace (IP + ports) and can share storage volumes.
┌─────────────────────────────────────┐
│ Pod (shared IP: 10.244.0.5) │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ app:nginx │ │ sidecar:log │ │
│ │ port 80 │ │ (read logs) │ │
│ └─────────────┘ └──────────────┘ │
│ │
│ shared volume: /var/log/nginx │
└─────────────────────────────────────┘
Your First Pod Manifest
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-nginx
labels:
app: my-nginx
env: dev
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
# Apply the pod manifest
kubectl apply -f pod.yaml
# Check it started
kubectl get pods
# Detailed view — events, container status, node assignment
kubectl describe pod my-nginx
# Stream logs
kubectl logs -f my-nginx
# Open an interactive shell
kubectl exec -it my-nginx -- bash
# Delete the pod
kubectl delete pod my-nginx
# or
kubectl delete -f pod.yaml
Multi-Container Pod (Sidecar Pattern)
# pod-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
volumes:
- name: shared-logs
emptyDir: {}
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
- name: log-shipper
image: busybox:1.36
command: ["sh", "-c", "tail -f /logs/access.log"]
volumeMounts:
- name: shared-logs
mountPath: /logs
Both containers communicate over localhost since they share the same network namespace.
Labels and Selectors
Labels are the primary way Kubernetes objects find and relate to each other.
# Applying labels to a pod
metadata:
name: api-server
labels:
app: api # what app
version: v2 # which version
env: production # which environment
tier: backend # which tier
# List pods with their labels
kubectl get pods --show-labels
# Filter pods by label
kubectl get pods -l app=api
kubectl get pods -l env=production,tier=backend
# Add a label to a running pod (imperative)
kubectl label pod my-nginx region=us-east
# Remove a label
kubectl label pod my-nginx region-
# Select pods NOT matching a label
kubectl get pods -l 'env!=dev'
Pod Lifecycle
Pending → Running → Succeeded / Failed
↕
CrashLoopBackOff (if container keeps failing)
# Watch pod status transitions in real time
kubectl get pods --watch
# Check why a pod is in CrashLoopBackOff
kubectl describe pod <name> # look at Events section
kubectl logs <name> --previous # logs from previous (crashed) run
Init Containers
Init containers run to completion before main containers start — useful for waiting on databases or seeding config.
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- "until nc -z postgres-service 5432; do echo waiting; sleep 2; done"
containers:
- name: app
image: my-api:1.0
ports:
- containerPort: 8080
Resource Requests and Limits
resources:
requests:
cpu: "250m" # 0.25 cores guaranteed
memory: "128Mi" # 128 MB guaranteed
limits:
cpu: "500m" # max 0.5 cores
memory: "256Mi" # max 256 MB (OOMKilled if exceeded)
# Check resource usage for pods
kubectl top pods
# Check resource usage for nodes
kubectl top nodes
Health Probes
spec:
containers:
- name: api
image: my-api:1.0
livenessProbe: # restart if this fails
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe: # only send traffic when this passes
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
| Probe | Failure action |
|---|---|
livenessProbe | Restart the container |
readinessProbe | Remove from Service endpoints (no traffic) |
startupProbe | Block liveness/readiness until startup completes |
Learning Outcomes
You should be able to:
- Write and apply a Pod manifest with
kubectl apply - Explain the sidecar pattern and when to use it
- Use labels and selectors to organize and filter resources
- Configure resource requests/limits and health probes
- Reason about why Services route to the right Pods
Practice
- Apply
pod.yamlandpod-sidecar.yaml, addregion=us-easttomy-nginx, then select by that label. Before you run it, predict which Pods-l 'env!=dev'returns.
Solution
kubectl apply -f pod.yaml
kubectl apply -f pod-sidecar.yaml
kubectl wait --for=condition=Ready pod/my-nginx pod/app-with-sidecar --timeout=180s
kubectl label pod my-nginx region=us-east
kubectl get pods -l region=us-east --show-labels
kubectl get pods -l 'env!=dev'
$ kubectl apply -f pod.yaml
pod/my-nginx created
$ kubectl apply -f pod-sidecar.yaml
pod/app-with-sidecar created
$ kubectl wait --for=condition=Ready pod/my-nginx pod/app-with-sidecar --timeout=180s
pod/my-nginx condition met
pod/app-with-sidecar condition met
$ kubectl label pod my-nginx region=us-east
pod/my-nginx labeled
$ kubectl get pods -l region=us-east --show-labels
NAME READY STATUS RESTARTS AGE LABELS
my-nginx 1/1 Running 0 8s app=my-nginx,env=dev,region=us-east
$ kubectl get pods -l 'env!=dev'
NAME READY STATUS RESTARTS AGE
app-with-sidecar 2/2 Running 0 9s
app-with-sidecar has no env label at all, and != also matches Pods that lack the key. If
you mean “has env, but not dev”, write -l 'env,env!=dev'.
- Prove the two containers in
app-with-sidecarshare a network namespace: from thelog-shippercontainer, fetch nginx’s page overlocalhost. Then check which container’s logs record the request.
Solution
kubectl exec app-with-sidecar -c log-shipper -- wget -qO- http://localhost | grep '<title>'
kubectl logs app-with-sidecar -c log-shipper
kubectl logs app-with-sidecar -c app --tail=1
$ kubectl exec app-with-sidecar -c log-shipper -- wget -qO- http://localhost | grep '<title>'
<title>Welcome to nginx!</title>
$ kubectl logs app-with-sidecar -c log-shipper
127.0.0.1 - - [11/Oct/2026:05:05:24 +0000] "GET / HTTP/1.1" 200 615 "-" "Wget" "-"
$ kubectl logs app-with-sidecar -c app --tail=1
/docker-entrypoint.sh: Configuration complete; ready for start up
busybox has no web server, yet localhost:80 answered, because both containers share one IP.
The request appears only in the sidecar’s logs. Mounting the volume over /var/log/nginx hides
the image’s link from access.log to stdout, so nginx writes a real file and tail -f reads it.
- Write a Pod called
memory-hogthat runstail /dev/zeroinbusybox:1.36with a64Mimemory limit. What status does it reach, and where do you find the reason?
Solution
# pod-oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: memory-hog
spec:
containers:
- name: hog
image: busybox:1.36
command: ["sh", "-c", "tail /dev/zero"] # buffers forever, never finds a newline
resources:
requests:
memory: "32Mi"
limits:
memory: "64Mi"
$ kubectl apply -f pod-oom.yaml
pod/memory-hog created
$ kubectl get pod memory-hog
NAME READY STATUS RESTARTS AGE
memory-hog 0/1 OOMKilled 3 (28s ago) 45s
$ kubectl describe pod memory-hog | grep -A 5 'Last State'
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Sun, 11 Oct 2026 10:36:08 +0530
Finished: Sun, 11 Oct 2026 10:36:08 +0530
Ready: False
tail keeps buffering zeros while it looks for a final line, so it hits 64Mi within a second
and the kernel kills it. Exit code 137 is 128 + 9 (SIGKILL). The kubelet keeps restarting it,
waiting longer before each attempt, which is why RESTARTS sits at 3 while ”28s ago” keeps growing. That time counts from the
previous kill, and the next restart waits 40s.
- Give a plain
nginx:1.25Pod alivenessProbeon/healthzport 80, every 5 seconds. nginx has no such page. What happens over the next minute?
Solution
# pod-liveness.yaml
apiVersion: v1
kind: Pod
metadata:
name: bad-liveness
spec:
containers:
- name: nginx
image: nginx:1.25
livenessProbe:
httpGet:
path: /healthz # nginx has no such page: 404
port: 80
initialDelaySeconds: 5
periodSeconds: 5
$ kubectl apply -f pod-liveness.yaml
pod/bad-liveness created
$ kubectl get pod bad-liveness
NAME READY STATUS RESTARTS AGE
bad-liveness 1/1 Running 2 (5s ago) 46s
$ kubectl describe pod bad-liveness | grep -E 'Liveness|Unhealthy|Killing'
Liveness: http-get http://:80/healthz delay=5s timeout=1s period=5s #success=1 #failure=3
Warning Unhealthy 6s (x6 over 36s) kubelet spec.containers{nginx}: Liveness probe failed: HTTP probe failed with statuscode: 404
Normal Killing 6s (x2 over 26s) kubelet spec.containers{nginx}: Container nginx failed liveness probe, will be restarted
The Pod says Running, but nginx is restarted about every 20 seconds (x2 over 26s, about 20
seconds apart): the 5-second initialDelaySeconds applies again after each restart, then three
failed probes (#failure=3) 5 seconds apart. Any status outside 200-399 counts as a failure. A wrong
liveness path turns a healthy app into a restart loop, which is why describe is the first
place to look.
Clean up with kubectl delete pod my-nginx app-with-sidecar memory-hog bad-liveness.