Pod Anatomy #
In the Kubernetes ecosystem, we often hear that application containers never run directly on a Worker Node. Instead, containers are always wrapped inside the smallest abstraction object called a Pod. As the smallest schedulable unit, a Pod acts as a logical host providing a shared compute environment for one or more application containers.
For developer and DevOps teams, writing a Pod spec isn’t just about copying a YAML template from the official documentation. Behind the manifest structure lie dozens of configuration fields with deep implications for security, performance, fault tolerance, and cluster cost efficiency. Understanding Pod anatomy thoroughly helps us design stable, secure, and manageable production-ready application manifests.
Pod Manifest Structure in Detail #
Every time we create a Pod manifest in YAML or JSON format, the document consists of four main field blocks. The API Server processes this structure strictly to validate whether the object meets the Kubernetes configuration contract.
Here’s the main Pod manifest skeleton with an explanation of each role:
apiVersion: v1 # Main API version for core Kubernetes objects
kind: Pod # The resource type we want to create
metadata: # Identity metadata (name, namespace, labels, annotations)
name: payment-api-prod # Unique Pod name within the same namespace
namespace: finance # Logical namespace where the Pod is placed
spec: # Technical spec for containers, volumes, and runtime policies
containers:
- name: web-app
image: nginx:1.25
Besides the three input fields above, there’s one important field block we never write manually in the manifest document: status.
- Managed by the Control Plane: The status block is filled asynchronously by Kubernetes (through the Kubelet and Controllers) to report the Pod’s current real condition (e.g. the allocated Pod IP, container health status, and its lifecycle phase).
- Read-Only: We only read this status block via
kubectl get pod -o yamlfor observation, debugging, and system monitoring.
The Identity Layer: Metadata, Labels, and Annotations #
The metadata block is the Pod’s identity card. This layer is used by all internal cluster components to organize networking, security permissions, and integrations with external systems.
Labels #
Labels are simple key-value pairs attached to the Pod object. They serve as search criteria (Selectors) for grouping Pods.
- Service Selector:
Serviceobjects use label selectors to determine which Pods are allowed to receive network traffic. - Deployment & ReplicaSet: Use label selectors to track the number of actively running Pod replicas.
- Horizontal Pod Autoscaler (HPA): Targets Pods based on label matching.
We’re advised to standardize labels in production, for example:
metadata:
labels:
app.kubernetes.io/name: payment-gateway # Main application name
app.kubernetes.io/part-of: finance-sys # Part of a larger system
app.kubernetes.io/version: v1.4.2 # Specific release version
environment: production # Deployment environment
team: backend-engineers # Owning team
Annotations #
Unlike Labels, which are used for search and selection, Annotations are used to store long, non-identity data. Annotations are typically consumed by external libraries, custom controllers, or monitoring tools.
Example annotation usage in production:
metadata:
annotations:
prometheus.io/scrape: "true" # Tells Prometheus to scrape metrics
prometheus.io/port: "9090" # Application metrics port
vault.hashicorp.com/agent-inject: "true" # Triggers Vault sidecar injection
Container Spec and Image Lifecycle #
The core of a Pod declaration lives in the spec.containers array. This section defines the application containers that will run together inside one Pod.
spec:
containers:
- name: api-server
image: my-registry.io/finance/api:v1.4.2
imagePullPolicy: IfNotPresent
ports:
- name: http-port
containerPort: 8080
protocol: TCP
command: ["/app/bin/server"]
args: ["--port=8080", "--verbose=true"]
Container Field Explanations: #
ports: Defines the container ports. It’s highly recommended to give thenameproperty (e.g.http-port) so Service objects can point to that port name instead of a static port number. This makes it easy to change the application port later without editing the Service manifest.command&args: Used to override the default Dockerfile configuration. Thecommandproperty overrides theENTRYPOINTinstruction in the Docker image, whileargsoverrides theCMDinstruction.imagePullPolicy: Determines the container image download policy from the registry to the Worker Node:Always: The Kubelet always contacts the image registry to pull the latest image every time the Pod runs, even if an image with that tag already exists on the node’s local disk. This is the default policy when using the:latestimage tag.IfNotPresent: The Kubelet only pulls the image from the registry if it’s not already available on the node’s local disk. This policy is very efficient for minimizing container cold-start time in production.Never: The Kubelet never tries to contact the registry. It assumes the image was installed manually on the host node.
The Resource Contract: Requests vs Limits #
Kubernetes requires us to declare container compute needs through the resources block. This configuration acts as a working contract between our application and the cluster Scheduler.
resources:
requests:
cpu: "250m" # 250 millicores (equivalent to 0.25 vCPU)
memory: "256Mi" # 256 Mebibytes (MiB)
limits:
cpu: "1000m" # 1000 millicores (equivalent to 1 vCPU)
memory: "512Mi" # 512 Mebibytes (MiB)
CPU (Compressible Resource) #
CPU is managed using the CFS (Completely Fair Scheduler) scheduler in the Linux kernel. CPU is compressible (can be pressed/slowed down):
- Requests: The CPU request value is used by the Scheduler to find a node with remaining capacity. This value is guaranteed to always be available to the container.
- Limits: The maximum CPU consumption limit. If a container tries to consume CPU beyond the limit, Kubernetes won’t kill the container. The Linux kernel only performs throttling (limiting the container’s CPU cycle rate), which makes application performance drop drastically.
Memory (Incompressible Resource) #
Memory is managed rigidly. Memory is incompressible (can’t be pressed):
- Requests: Helps the Scheduler place the Pod on a node with sufficient remaining physical memory.
- Limits: A hard physical memory limit. If a container consumes memory beyond the limit, the Linux kernel triggers the Out Of Memory (OOM) Killer mechanism. The container is instantly force-killed with the
OOMKilledstatus (exit code 137).
Here’s a comparison of OS behavior for CPU and Memory when limits are exceeded:
| Behavior Dimension | CPU Limit Exceeded | Memory Limit Exceeded |
|---|---|---|
| System Action | Throttling (app slows down) | OOMKilled (container dies suddenly) |
| Resilience | Container stays alive | Container is auto-restarted by the Kubelet |
| User Impact | Request latency increases | Service experiences momentary failure (downtime) |
Environment Variables and the Downward API #
Sometimes application containers need dynamic configuration that varies across deployment environments (Development, Staging, Production). Kubernetes provides three mechanisms for injecting environment variables:
env:
# 1. Direct Static Values
- name: APP_MODE
value: "production"
# 2. Reference from ConfigMap / Secret
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: payment-secrets
key: db-password
# 3. Downward API (Fetching Internal Pod Information)
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
The Downward API #
The Downward API lets application containers know their own metadata without querying the Kubernetes API Server directly using a token. This improves cluster security because we don’t need to open RBAC permissions for applications just to know the Pod name or host IP address.
Common metadata fields accessed through the Downward API include:
metadata.name: The current Pod name.metadata.namespace: The namespace where the Pod runs.status.podIP: The Pod’s internal IP address.spec.nodeName: The name of the Worker Node where the Pod is scheduled.
Volume Abstraction and Volume Mounts #
By default, the container filesystem is ephemeral. If a container crashes and gets restarted by the Kubelet, all new files written inside the container are lost without a trace. To persist data, we must use the volumes abstraction.
spec:
volumes:
- name: config-directory
configMap:
name: app-config-files
- name: cache-scratch
emptyDir: {}
- name: database-storage
persistentVolumeClaim:
claimName: db-pvc
containers:
- name: main-app
image: my-app:v1
volumeMounts:
- name: config-directory
mountPath: /app/config
readOnly: true
- name: cache-scratch
mountPath: /tmp/cache
Main Volume Characteristics: #
emptyDir: A temporary empty volume created inside the host node’s storage directory. This volume only lasts as long as the Pod lives. If a container inside the Pod dies and is restarted, the data in emptyDir stays safe. However, if the Pod is deleted from the node (evicted or scaled down), emptyDir and all its contents are destroyed forever.configMap/secret: Mounts ConfigMap or Secret contents as plain text files inside the container directory. This feature automatically updates the files in the container when we update the ConfigMap object in the API Server (the sync process takes about 60 seconds).persistentVolumeClaim(PVC): Connects the Pod to an external persistent storage volume (like AWS EBS, Google Persistent Disk, or NFS) whose lifecycle is separate from the Pod’s lifecycle.
Health Checking: Liveness, Readiness, and Startup Probes #
Kubernetes monitors our application health through the Kubelet agent using three probe mechanisms (health checks):
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz/live
port: 8080
failureThreshold: 30
periodSeconds: 10
flowchart TD
PodStart["Pod Runs (Container Booting)"] --> StartupRun{"1. Startup Probe\nHas the app started?"}
StartupRun -- "Failed (Limit Exceeded)" --> StartupKill["Kubelet restarts the Container"]
StartupRun -- "Success" --> LiveReadyActive["Turn off the Startup Probe\nActivate Liveness & Readiness Probes"]
LiveReadyActive --> LivenessCheck{"2. Liveness Probe\nIs the app hung/deadlocked?"}
LiveReadyActive --> ReadinessCheck{"3. Readiness Probe\nIs the app ready for traffic?"}
LivenessCheck -- "Failed" --> LiveKill["Kubelet restarts the Container"]
LivenessCheck -- "Success" --> LiveOK["Let It Keep Running"]
ReadinessCheck -- "Failed" --> ReadRemove["Remove the Pod IP from the Service Endpoint\n(Traffic stopped)"]
ReadinessCheck -- "Success" --> ReadAdd["Add the Pod IP to the Service Endpoint\n(Traffic allowed in)"]
StartupKill --> PodStart
LiveKill --> PodStartThe Three Probe Types in Detail: #
- Liveness Probe: Determines whether our application container is still alive or suffering a systemic failure (deadlock). If the liveness probe returns an HTTP error code (like
500) several times in a row perfailureThreshold, the Kubelet kills and restarts the container. - Readiness Probe: Determines whether the container is ready to accept incoming user traffic. If the readiness probe fails, Kubernetes doesn’t kill the container. It only removes the Pod’s IP address from the Service endpoint list. Users aren’t routed to a not-yet-ready Pod (for example while the application is still loading caches into memory).
- Startup Probe: Used specifically to protect applications that take a very long time to boot (e.g. an old enterprise Java app needing 5 minutes to start). While the startup probe is still running and hasn’t succeeded, the liveness and readiness probes are disabled. This prevents the Kubelet from prematurely killing the container by mistaking an early boot process for a hang.
Graceful Termination and Restart Policy #
Kubernetes is designed to be dynamic. Pods can be moved, deleted, or replaced at any time. To avoid transactions being cut off mid-way (corrupted transactions), Kubernetes applies a graceful termination cycle:
Pod Deletion Flow:
1. Delete Command Received (kubectl delete or autoscale scale-down)
│
▼
2. Pod status changes to "Terminating"
│
▼
3. Two Flows Execute in Parallel:
├── A. The Pod IP is removed from all Service Endpoints (New traffic rejected)
└── B. The Kubelet sends the SIGTERM signal to the main process (PID 1) inside the container
│
▼
4. The Application Performs Cleanup (Graceful Shutdown):
├── Finish in-flight HTTP requests
└── Close the database connection pool in an orderly fashion
│
▼
5. Wait until the "terminationGracePeriodSeconds" duration expires (default: 30 seconds)
│
▼
6. If the process hasn't died, the Kubelet sends the SIGKILL signal (Force Kill)
If our application needs a long cleanup time (e.g. 45 seconds to flush an in-memory queue), we must raise the tolerance value in the Pod manifest:
spec:
terminationGracePeriodSeconds: 60 # Gives the application 60 seconds before force-kill
Restart Policy #
Determined by the spec.restartPolicy property to control the Kubelet’s action when a container dies:
Always(Default): Always restart the container regardless of exit code. Perfect for web/API applications that must stay up.OnFailure: Only restart the container if it exits with an error code (exit code not equal to 0). Perfect for batch processing Jobs.Never: Never restart the container. Right for one-shot diagnostic tasks.
Interpreting Pod Status and Conditions #
Understanding Pod status helps us troubleshoot operational problems in production:
# Quickly check the Pod phase
kubectl get pod payment-api-prod
# View the detailed event history
kubectl describe pod payment-api-prod
Pod Phases: #
Pending: The Pod manifest has been accepted by the API Server, but the Scheduler hasn’t found a matching node yet, or the node is still downloading the container image from the registry.Running: The Pod has been bound to a Worker Node, all containers have been created, and at least one container is running or in the startup/restart process.Succeeded: All containers in the Pod have completed successfully (exit code 0) and won’t be restarted (e.g. a finished Job).Failed: All containers in the Pod have stopped, and at least one container stopped with an error code (non-zero exit code).Unknown: The API Server has lost communication with the Kubelet on the Worker Node hosting the Pod (usually due to physical server network issues).
Anti-Patterns in Pod Configuration #
Here are fatal mistakes developers often make when writing Pod manifests in production:
Anti-Pattern 1: Ignoring Requests/Limits (BestEffort QoS) #
Deploying Pods without resource bounds for the sake of YAML convenience.
ANTI-PATTERN: Not Writing the resources Block in the Container Spec
// WHAT WE DO:
- Deploy database and API microservice Pods without declaring CPU & RAM requests/limits specs.
// THE CONSEQUENCES IN PRODUCTION:
- The Pod is categorized as "BestEffort" QoS class (lowest priority).
- If the Worker Node overloads (runs out of physical RAM), the Kubelet immediately picks BestEffort Pods
as the first targets for force-kill (eviction) to save the host node system's health.
- Our application keeps dying suddenly at random without any internal error logs from the app itself.
✓ THE RIGHT SOLUTION:
- Always declare realistic CPU and RAM requests blocks to get the "Burstable" or "Guaranteed" QoS class.
- This guides the Scheduler to find nodes that can physically accommodate the application load.
Anti-Pattern 2: Using the :latest Image Tag in Production #
Relying on the default tag without an explicit version number.
ANTI-PATTERN: Writing image: my-app:latest in the Pod Spec
// WHAT WE DO:
- Write the :latest tag for the container image in the production cluster's Deployment manifest.
- Push code updates to the registry with the :latest tag, then expect Kubernetes to update automatically.
// THE CONSEQUENCES IN PRODUCTION:
- No Changes: Because Kubernetes works declaratively, if the image name and tag don't change
(stays :latest), the Deployment Controller assumes no configuration change. Our application won't be updated.
- If a node crashes and restarts the Pod, it silently pulls the latest image.
This creates a chaotic situation where some Pods run old code and others run new code.
✓ THE RIGHT SOLUTION:
- Always use explicit, unique version tags (e.g. a Git commit SHA or semantic versions like `:v1.4.2`).
- This guarantees code consistency across all Pod replicas in our production cluster.
Summary #
- The Smallest Scheduling Unit — A Pod is a logical host abstraction housing one or more application containers so they can run together in the cluster.
- The Mandatory Compute Contract — Always declare CPU/Memory requests and limits on every production container to guarantee resource allocation priority and a safe QoS class.
- Throttling vs OOMKilled — Exceeding CPU limits triggers throttling (slow app), while exceeding Memory limits triggers OOMKilled (instant container death).
- Two-Way Label Relationships — Use Labels for Service/HPA targeting groups, and Annotations for storing supporting metadata for external tools.
- Application Health Checks — Apply Liveness Probes to detect deadlocks, Readiness Probes to manage network traffic routing, and Startup Probes to protect slow boot processes.
- Orderly Shutdown — Make sure the application listens for the
SIGTERMsignal for graceful shutdown, and tuneterminationGracePeriodSecondsto match resource cleanup needs.- Use Explicit Tags — Avoid
:latestimage tags to guarantee version accuracy and smooth declarative rolling update cycles in Kubernetes.
← Previous: Etcd & Cluster Consistency Next: Single & Multi Container →