Skip to content
Codeloom
Kubernetes

Kubernetes Pods and Services Deep Dive

A deep dive into Kubernetes Pods and Services: pod lifecycle, multi-container patterns, init containers, probes, ClusterIP, NodePort, LoadBalancer, and service discovery.

·9 min read · By Codeloom
Intermediate 15 min read

What you'll learn

  • Pod lifecycle phases and what each means
  • Multi-container pod patterns: sidecar, ambassador, adapter
  • Init containers for setup and dependency checks
  • Health probes: liveness, readiness, startup
  • Service types: ClusterIP, NodePort, LoadBalancer, and when to use each

Prerequisites

  • Basic Kubernetes concepts (clusters, nodes)
  • Familiarity with YAML and kubectl
  • Understanding of containers and Docker basics

Pods and Services are the two most fundamental resources in Kubernetes. A Pod runs your containers. A Service makes them accessible. Everything else in Kubernetes — Deployments, StatefulSets, Ingress — is built on top of these two primitives. Understanding them deeply is the foundation for working effectively with Kubernetes.

What a Pod actually is

A Pod is the smallest deployable unit in Kubernetes. It is not a container — it is a group of one or more containers that share the same network namespace, storage volumes, and lifecycle. Containers in a Pod share localhost and can communicate via IPC.

# pod.yaml — the simplest possible pod
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      ports:
        - containerPort: 80
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod nginx-pod
kubectl logs nginx-pod
kubectl delete pod nginx-pod

In practice, you almost never create Pods directly. You use a Deployment or StatefulSet, which creates Pods for you and handles replication, rolling updates, and self-healing. But understanding Pods is essential because every higher-level resource ultimately manages Pods.

Pod lifecycle

A Pod moves through several phases during its lifetime:

Pending → Running → Succeeded/Failed
                  ↘ Unknown (node lost)

Pending — The Pod has been accepted by the cluster but one or more containers are not yet running. This includes time spent waiting for scheduling, pulling images, or starting init containers.

Running — At least one container is running. The Pod has been bound to a node.

Succeeded — All containers in the Pod have terminated successfully (exit code 0) and will not be restarted. Typical for batch jobs.

Failed — At least one container terminated with a non-zero exit code.

Unknown — The Pod’s state cannot be determined, usually because the node is unreachable.

# Watch pod phase transitions
kubectl get pods -w

# See detailed status including container states
kubectl describe pod my-pod

Container restart policies

The restartPolicy determines what happens when a container exits.

apiVersion: v1
kind: Pod
metadata:
  name: restart-demo
spec:
  restartPolicy: Always  # Always | OnFailure | Never
  containers:
    - name: app
      image: my-app:latest
  • Always (default) — Restart the container regardless of exit code. Used by long-running services.
  • OnFailure — Restart only if the container exits with a non-zero code. Used by batch jobs.
  • Never — Never restart. Used for one-shot tasks.

Kubernetes uses exponential backoff for restarts: 10s, 20s, 40s, up to 5 minutes. A container that keeps crashing enters CrashLoopBackOff state.

Multi-container Pods

Multiple containers in a Pod share the same network and storage. The three main multi-container patterns are:

Sidecar pattern

A helper container that extends the main container’s functionality without modifying it.

apiVersion: v1
kind: Pod
metadata:
  name: web-with-logging
spec:
  volumes:
    - name: shared-logs
      emptyDir: {}
  containers:
    - name: web-server
      image: nginx:1.27
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/nginx
    - name: log-shipper
      image: fluentd:latest
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/nginx
          readOnly: true
      env:
        - name: FLUENTD_CONF
          value: "fluent.conf"

The web server writes logs to a shared volume. The log-shipper sidecar reads those logs and forwards them to a centralized logging system. Neither container needs to know about the other’s implementation.

Ambassador pattern

A proxy container that handles external communication on behalf of the main container.

apiVersion: v1
kind: Pod
metadata:
  name: app-with-proxy
spec:
  containers:
    - name: app
      image: my-app:latest
      env:
        - name: DB_HOST
          value: "localhost"  # Connects to ambassador on localhost
        - name: DB_PORT
          value: "5432"
    - name: cloud-sql-proxy
      image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest
      args:
        - "--port=5432"
        - "my-project:us-central1:my-db"

The app connects to localhost:5432. The Cloud SQL Proxy ambassador handles authentication, encryption, and connection pooling to the actual database.

Adapter pattern

A container that transforms the main container’s output into a standardized format.

apiVersion: v1
kind: Pod
metadata:
  name: app-with-metrics
spec:
  containers:
    - name: app
      image: legacy-app:latest
      ports:
        - containerPort: 8080
    - name: prometheus-adapter
      image: prom-adapter:latest
      ports:
        - containerPort: 9090
      args:
        - "--source=http://localhost:8080/status"
        - "--format=prometheus"

Init containers

Init containers run to completion before the main containers start. They are used for setup tasks, dependency checks, and initialization.

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 for database..."
            sleep 2
          done
          echo "Database is ready!"
    - name: run-migrations
      image: my-app:latest
      command: ["python", "manage.py", "migrate"]
      env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url
  containers:
    - name: app
      image: my-app:latest
      ports:
        - containerPort: 8000

Init containers run in order, one at a time. If any init container fails, the Pod restarts (subject to restartPolicy). The main containers do not start until all init containers succeed.

Health probes

Kubernetes uses probes to determine if a container is healthy, ready to serve traffic, or still starting up.

Liveness probe

Determines if the container is running. If the liveness probe fails, the container is killed and restarted.

containers:
  - name: app
    image: my-app:latest
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10
      failureThreshold: 3
      timeoutSeconds: 5

Readiness probe

Determines if the container is ready to accept traffic. If the readiness probe fails, the Pod’s IP is removed from Service endpoints. The container is not killed.

containers:
  - name: app
    image: my-app:latest
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      failureThreshold: 3

Startup probe

Used for slow-starting containers. Until the startup probe succeeds, liveness and readiness probes are disabled.

containers:
  - name: legacy-app
    image: legacy-app:latest
    startupProbe:
      httpGet:
        path: /healthz
        port: 8080
      failureThreshold: 30
      periodSeconds: 10
      # Allows up to 5 minutes for startup (30 * 10s)
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      periodSeconds: 10

Probe types include httpGet, exec (run a command), and tcpSocket (check if port is open).

# Exec probe example
livenessProbe:
  exec:
    command:
      - cat
      - /tmp/healthy
  periodSeconds: 5

# TCP socket probe example
readinessProbe:
  tcpSocket:
    port: 3306
  periodSeconds: 10

Services

A Service provides a stable network endpoint for a set of Pods. Pods are ephemeral — they get new IPs when they restart. Services provide a consistent DNS name and IP that routes to the current set of matching Pods.

ClusterIP (default)

Accessible only within the cluster. This is the default and most common type.

apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  type: ClusterIP  # Default, can be omitted
  selector:
    app: backend
  ports:
    - protocol: TCP
      port: 80        # Service port
      targetPort: 8080 # Container port
# Other pods can access this service at:
# backend-service (same namespace)
# backend-service.default.svc.cluster.local (fully qualified)

NodePort

Exposes the service on a static port on every node in the cluster. Accessible from outside the cluster via <NodeIP>:<NodePort>.

apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  type: NodePort
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
      nodePort: 30080  # Optional: auto-assigned from 30000-32767
# Accessible at any node's IP on port 30080
curl http://<node-ip>:30080

LoadBalancer

Provisions an external load balancer (on cloud providers). Builds on NodePort.

apiVersion: v1
kind: Service
metadata:
  name: public-api
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - protocol: TCP
      port: 443
      targetPort: 8443

On AWS, this creates an NLB or ALB. On GCP, a Google Cloud Load Balancer. On bare metal, you need a solution like MetalLB.

Headless Service

A Service with clusterIP: None. No load balancing or proxying. DNS returns the individual Pod IPs. Used for StatefulSets where clients need to connect to specific Pods.

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
# DNS returns individual Pod IPs
nslookup postgres-headless
# postgres-0.postgres-headless.default.svc.cluster.local → 10.244.1.5
# postgres-1.postgres-headless.default.svc.cluster.local → 10.244.2.8

Service discovery

Kubernetes provides two mechanisms for service discovery:

Every Service gets a DNS entry automatically: <service-name>.<namespace>.svc.cluster.local.

# In application code, just use the service name
import requests

# Same namespace
response = requests.get("http://backend-service/api/data")

# Different namespace
response = requests.get("http://backend-service.production.svc.cluster.local/api/data")

Environment variables

Kubernetes injects environment variables for each Service into every Pod.

# Automatically available in any pod (same namespace)
echo $BACKEND_SERVICE_SERVICE_HOST  # 10.96.0.15
echo $BACKEND_SERVICE_SERVICE_PORT  # 80

DNS is preferred because environment variables are only set at Pod creation time and do not update when Services change.

Resource requests and limits

Always set resource requests and limits on your containers. Without them, a single Pod can starve others of CPU and memory.

containers:
  - name: app
    image: my-app:latest
    resources:
      requests:
        memory: "128Mi"
        cpu: "250m"     # 0.25 CPU cores
      limits:
        memory: "256Mi"
        cpu: "500m"

Requests are what the scheduler uses to decide where to place the Pod. A node must have enough unrequested resources to accept the Pod.

Limits are the maximum the container can use. If a container exceeds its memory limit, it is killed (OOMKilled). If it exceeds CPU, it is throttled.

Set requests based on typical usage and limits based on peak usage. Avoid setting limits too low (causes throttling and OOMKills) or too high (wastes cluster resources).

Pods and Services are where Kubernetes starts making sense. Everything else — Deployments, HPA, Ingress — is built on these two concepts. Master the Pod lifecycle, understand how Services route traffic, and the rest of Kubernetes becomes much more approachable.