Loading learning content...
While Kubernetes offers dozens of resource types, three form the foundation of nearly every workload: Pods, Deployments, and Services. Understanding these resources deeply—their structure, lifecycle, and interactions—is essential for anyone working with Kubernetes.
These aren't just API objects; they represent fundamental abstractions that shape how you think about containerized applications. A Pod groups containers that share a lifecycle. A Deployment manages Pods declaratively with rolling updates and rollbacks. A Service provides stable networking for ephemeral Pods.
By the end of this page, you will understand the design philosophy behind Pods, Deployments, and Services. You'll know when to use each, how they interact, and the subtle behaviors that catch newcomers off guard. This knowledge forms the foundation for every Kubernetes deployment you'll ever build.
A Pod is the smallest deployable unit in Kubernetes—you don't deploy containers directly; you deploy Pods that contain containers. This may seem like unnecessary indirection, but it's a deliberate design choice with profound implications.
Why Pods, Not Just Containers?
The Pod abstraction enables:
Shared Networking: All containers in a Pod share the same network namespace. They communicate via localhost and share a single IP address.
Shared Storage: Pods can mount shared volumes accessible by all containers.
Co-scheduling: Containers in a Pod are always scheduled to the same node, guaranteeing low-latency communication.
Lifecycle Coupling: Containers in a Pod start together, and the Pod is only considered healthy when all containers are healthy.
Pod Structure:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
apiVersion: v1kind: Podmetadata: name: web-app namespace: production labels: app: web tier: frontend annotations: prometheus.io/scrape: "true"spec: # Init containers run before app containers, in order initContainers: - name: init-db image: busybox:1.36 command: ['sh', '-c', 'until nc -z db-service 5432; do sleep 1; done'] # Main application containers containers: - name: web-server image: nginx:1.25 ports: - containerPort: 80 protocol: TCP # Resource management resources: requests: cpu: "100m" # 0.1 CPU cores guaranteed memory: "128Mi" # 128 MiB guaranteed limits: cpu: "500m" # Max 0.5 CPU cores memory: "512Mi" # Max 512 MiB (OOMKilled if exceeded) # Health checks livenessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 15 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 80 periodSeconds: 5 # Volume mounts volumeMounts: - name: config mountPath: /etc/nginx/conf.d - name: cache mountPath: /var/cache/nginx # Sidecar container (common pattern) - name: log-shipper image: fluent/fluent-bit:2.2 volumeMounts: - name: logs mountPath: /var/log/nginx # Volumes shared across containers volumes: - name: config configMap: name: nginx-config - name: cache emptyDir: {} - name: logs emptyDir: {} # Node selection nodeSelector: disk: ssd # Restart policy restartPolicy: Always # Always, OnFailure, or Never # Service account for RBAC serviceAccountName: web-app-saPod Lifecycle Phases:
| Phase | Description |
|---|---|
| Pending | Pod accepted but not yet scheduled or images not pulled |
| Running | At least one container running or starting/restarting |
| Succeeded | All containers terminated successfully (exit code 0) |
| Failed | At least one container terminated with failure |
| Unknown | Pod state cannot be determined (node communication failure) |
Container States within a Pod:
Within a running Pod, each container has its own state:
Pods are designed to be disposable. They can be terminated at any time—by node failure, preemption, resource pressure, or during updates. Never rely on Pod persistence. Use StatefulSets for stable identity and persistent storage; use Deployments for stateless workloads with replicas.
While single-container Pods are most common, multi-container Pods enable powerful architectural patterns. Understanding these patterns helps you design better containerized applications.
The Sidecar Pattern:
A sidecar container extends the primary container's functionality without modifying it:
The Ambassador Pattern:
An ambassador container proxies network traffic from the main container to external services:
[Main App] → localhost:5432 → [Ambassador] → remote-db.example.com:5432
Benefits:
The Adapter Pattern:
An adapter container transforms the output of the main container to a standardized format:
Use multi-container Pods when containers have a tight coupling—they must be co-located, share resources, and have the same lifecycle. If containers can run independently, use separate Pods and communicate via Services. Over-packing Pods defeats Kubernetes' ability to scale and place containers optimally.
Health probes are critical for production reliability. They tell Kubernetes whether your containers are functioning correctly, enabling automatic recovery and traffic management.
Three Types of Probes:
| Probe Type | Purpose | Failure Consequence |
|---|---|---|
| Liveness | Is the container alive? | Container restarted |
| Readiness | Is the container ready to receive traffic? | Removed from Service endpoints |
| Startup | Has the container started successfully? | Liveness/readiness disabled until success |
Probe Mechanisms:
Probe Configuration:
123456789101112131415161718192021222324252627282930313233
containers: - name: app image: myapp:v1 # Startup probe: gives slow-starting containers time startupProbe: httpGet: path: /healthz port: 8080 failureThreshold: 30 # Retry up to 30 times periodSeconds: 10 # Every 10 seconds (5 min total) # Liveness probe: restart if container is broken livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 0 # Start immediately (after startup) periodSeconds: 10 # Check every 10 seconds timeoutSeconds: 1 # Timeout per check failureThreshold: 3 # 3 failures = restart successThreshold: 1 # 1 success = healthy # Readiness probe: control traffic routing readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 0 periodSeconds: 5 # Check more frequently timeoutSeconds: 1 failureThreshold: 3 # 3 failures = remove from service successThreshold: 1Best Practices for Probe Design:
Liveness probes should be simple: Check if the process is alive, not if dependencies are healthy. Otherwise, one failed database causes all containers to restart in a loop.
Readiness probes can be more comprehensive: Check database connections, cache warmth, etc. Failing readiness removes traffic, a safer response than restart.
Use startup probes for slow applications: Java apps with long startup times, apps that need to load large datasets—use startup probes to avoid premature liveness failures.
Set appropriate timeouts and thresholds: Too aggressive = false positives; too lenient = slow detection. Start conservative and tune based on observed behavior.
Avoid expensive checks: Probes run frequently; a probe that queries a database or does heavy computation adds load and latency.
A liveness probe that checks database connectivity can cause cascading failures: database has a brief hiccup → all pods restart → thundering herd on database recovery. Liveness should check local process health only. Use readiness to handle dependency failures gracefully.
You rarely create Pods directly in production. Instead, you use higher-level controllers. ReplicaSet is the controller that ensures a specified number of Pod replicas are running at all times.
ReplicaSet Components:
How ReplicaSets Work:
ReplicaSet Controller Reconciliation Loop: ┌────────────────────────────────────────────────────────────────┐│ 1. Watch for ReplicaSet changes and Pod changes │└────────────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────┐│ 2. List Pods matching the ReplicaSet's selector ││ (using labels like app=web, version=v1) │└────────────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────┐│ 3. Count matching Pods (current) vs desired replicas │└────────────────────────────────────────────────────────────────┘ │ ┌──────────────────┼──────────────────┐ ▼ ▼ ▼ current < desired current = desired current > desired │ │ │ ▼ ▼ ▼ Create new Pods Do nothing Delete excess Pods │ │ └──────────────────┬──────────────────┘ ▼┌────────────────────────────────────────────────────────────────┐│ 4. Update ReplicaSet status (replicas, readyReplicas, etc.) │└────────────────────────────────────────────────────────────────┘ │ ▼ Loop continues...ReplicaSet YAML:
12345678910111213141516171819202122232425
apiVersion: apps/v1kind: ReplicaSetmetadata: name: web-frontend labels: app: web tier: frontendspec: replicas: 3 selector: matchLabels: app: web tier: frontend template: # This is the Pod template metadata: labels: app: web # Must match selector tier: frontend # Must match selector spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80While you can create ReplicaSets directly, in practice you should always use Deployments. Deployments create and manage ReplicaSets for you, adding crucial features like rolling updates and rollbacks. The only time you might reference ReplicaSets is when debugging or understanding internal mechanics.
A Deployment provides declarative updates for Pods and ReplicaSets. It's the resource you'll use most often for stateless applications. When you update a Deployment, it orchestrates the rollout of changes safely and automatically.
What Deployments Provide:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
apiVersion: apps/v1kind: Deploymentmetadata: name: web-app namespace: production labels: app: web annotations: kubernetes.io/change-cause: "Update to v2 with new feature"spec: replicas: 5 # How many old ReplicaSets to keep for rollback revisionHistoryLimit: 10 # Selector must match template labels selector: matchLabels: app: web # Update strategy configuration strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 # At most 1 Pod unavailable during update maxSurge: 2 # At most 2 extra Pods during update # Pod template template: metadata: labels: app: web version: v2 spec: containers: - name: web image: myapp:v2 ports: - containerPort: 8080 resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "512Mi" readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10Update Strategies:
1. RollingUpdate (Default)
Gradually replaces Pods. Controlled by:
maxUnavailable: How many Pods can be unavailable (absolute number or percentage)maxSurge: How many extra Pods can be created (absolute number or percentage)Example: 5 replicas, maxUnavailable=1, maxSurge=2
2. Recreate
Terminate all existing Pods before creating new ones:
With 10 replicas, maxUnavailable=25%, maxSurge=25%: Kubernetes keeps at least 8 Pods running and creates up to 13 total. This allows replacing 2-3 Pods per wave, balancing speed and safety.
Understanding how Deployments manage updates through ReplicaSets is crucial for debugging and controlling rollouts.
How Rolling Updates Work:
Rolling Update: Image change from v1 to v2 Initial State: web-replica-set-abc (5 pods, v1) ┌──────────────────────────────────────────────────────────────────┐│ Step 1: Deployment updated (image: v1 → v2) │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Step 2: New ReplicaSet created (web-replica-set-xyz) ││ Old RS: 5 pods (v1), New RS: 0 pods (v2) │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Step 3: Scale up new RS (maxSurge) ││ Old RS: 5 pods, New RS: 2 pods (creating) ││ Total: 7 pods (within maxSurge limit) │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Step 4: New pods become Ready ││ Old RS: 5 pods, New RS: 2 pods (ready) │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Step 5: Scale down old RS (maxUnavailable) ││ Old RS: 3 pods, New RS: 2 pods ││ Total: 5 pods (within maxUnavailable) │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Steps 6-N: Repeat scale up new, scale down old ││ ... │└──────────────────────────────────────────────────────────────────┘ │ ▼┌──────────────────────────────────────────────────────────────────┐│ Final: Old RS scaled to 0, New RS has 5 ready pods ││ Old RS kept for rollback (revisionHistoryLimit) │└──────────────────────────────────────────────────────────────────┘Rollback Operations:
# View rollout history
kubectl rollout history deployment/web-app
# Rollback to previous revision
kubectl rollout undo deployment/web-app
# Rollback to specific revision
kubectl rollout undo deployment/web-app --to-revision=3
# Check rollout status
kubectl rollout status deployment/web-app
# Pause a rollout (for canary-like testing)
kubectl rollout pause deployment/web-app
# Resume a paused rollout
kubectl rollout resume deployment/web-app
What Triggers a Rollout?
Only changes to the Pod template (.spec.template) trigger a new rollout:
Changes to .spec.replicas do NOT trigger a rollout—they're handled by scaling the existing ReplicaSet.
Rollback works by keeping old ReplicaSets (scaled to 0 pods). The revisionHistoryLimit (default 10) controls how many old ReplicaSets to retain. Setting it too low limits rollback options; setting it too high wastes etcd storage.
Pods are ephemeral—they come and go, each with a different IP address. A Service provides a stable network identity for a set of Pods, enabling reliable communication in a dynamic environment.
The Problem Services Solve:
How Services Work:
A Service selects Pods using label selectors and provides:
service-name.namespace.svc.cluster.local)123456789101112131415161718192021222324252627
apiVersion: v1kind: Servicemetadata: name: web-service namespace: productionspec: # ClusterIP is the default type type: ClusterIP # Selector finds Pods to include selector: app: web tier: frontend # Port mapping ports: - name: http protocol: TCP port: 80 # Service port (clients connect here) targetPort: 8080 # Pod port (traffic forwarded here) - name: https protocol: TCP port: 443 targetPort: 8443 # Session affinity (optional) sessionAffinity: None # or ClientIP for sticky sessionsService Types:
| Type | Accessibility | Use Case |
|---|---|---|
| ClusterIP | Cluster-internal only | Internal microservice communication |
| NodePort | Cluster + external via node IP:port | Development, on-prem without LB |
| LoadBalancer | External via cloud load balancer | Production external access |
| ExternalName | DNS alias to external service | Accessing external services with Kubernetes DNS |
| Headless (ClusterIP: None) | Direct Pod IPs via DNS | StatefulSets, client-side load balancing |
Within the cluster, connect to services via DNS: http://web-service.production.svc.cluster.local:80 or just http://web-service from the same namespace. Kubernetes DNS resolves to the Service's ClusterIP, which load balances across healthy Pods.
Services don't magically know where Pods are. Endpoints and EndpointSlices are the objects that track which Pod IPs belong to a Service.
Endpoints (Legacy):
For each Service, Kubernetes creates an Endpoints object with the same name. This object lists all Pod IPs matching the Service's selector.
apiVersion: v1
kind: Endpoints
metadata:
name: web-service # Same name as Service
subsets:
- addresses:
- ip: 10.244.1.5
nodeName: worker-1
- ip: 10.244.2.8
nodeName: worker-2
ports:
- port: 8080
protocol: TCP
EndpointSlices (Modern):
EndpointSlices improve scalability by splitting endpoints across multiple objects. This is important for Services with thousands of endpoints.
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: web-service-abc12
labels:
kubernetes.io/service-name: web-service
endpoints:
- addresses:
- 10.244.1.5
conditions:
ready: true
serving: true
nodeName: worker-1
- addresses:
- 10.244.2.8
conditions:
ready: true
nodeName: worker-2
ports:
- port: 8080
protocol: TCP
Why EndpointSlices Matter:
Scalability: Large Endpoints objects (thousands of IPs) create huge API payloads. EndpointSlices limit each object to ~100 endpoints.
Efficient Updates: Updating one Pod only requires modifying one slice, not the entire endpoints list.
Topology Awareness: EndpointSlices include zone information for topology-aware routing.
Endpoint States:
| Condition | Meaning |
|---|---|
| ready | Pod is passing readiness probe |
| serving | Pod can serve traffic (even if terminating) |
| terminating | Pod is shutting down |
If a Service isn't routing traffic correctly, check EndpointSlices: kubectl get endpointslices -l kubernetes.io/service-name=web-service. If empty, your selector doesn't match any ready Pods. Verify labels and readiness probes.
Kubernetes provides multiple ways for applications to discover and connect to Services.
1. DNS-Based Discovery (Recommended)
Kubernetes runs a DNS server (CoreDNS) that creates DNS records for Services:
| DNS Name | Resolves To |
|---|---|
web-service | ClusterIP (same namespace) |
web-service.production | ClusterIP (cross-namespace) |
web-service.production.svc.cluster.local | ClusterIP (FQDN) |
_http._tcp.web-service.production.svc.cluster.local | SRV record with port |
For Headless Services (ClusterIP: None):
DNS Query Examples: # Normal Service (ClusterIP)$ nslookup web-service.production.svc.cluster.localName: web-service.production.svc.cluster.localAddress: 10.96.45.123 (ClusterIP) # Headless Service (ClusterIP: None)$ nslookup db-service.production.svc.cluster.localName: db-service.production.svc.cluster.localAddress: 10.244.1.5 (Pod IP)Address: 10.244.2.8 (Pod IP)Address: 10.244.3.2 (Pod IP) # StatefulSet Pods via Headless Service$ nslookup db-0.db-service.production.svc.cluster.localAddress: 10.244.1.5 (Specific Pod)2. Environment Variable Discovery (Legacy)
For each active Service, Kubernetes injects environment variables into Pods:
# For a service named 'web-service'
WEB_SERVICE_SERVICE_HOST=10.96.45.123
WEB_SERVICE_SERVICE_PORT=80
WEB_SERVICE_PORT=tcp://10.96.45.123:80
Limitations:
Best Practice: Always use DNS for service discovery. Environment variables are legacy and problematic at scale.
Some applications aggressively cache DNS responses. When Pods change, stale DNS can route to dead endpoints. Configure your application to respect DNS TTLs or use shorter TTLs. Java applications are notorious for infinite DNS caching—set networkaddress.cache.ttl appropriately.
Let's see how Pods, Deployments, and Services work together in a realistic scenario.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
# 1. ConfigMap for configurationapiVersion: v1kind: ConfigMapmetadata: name: web-configdata: API_URL: "http://api-service:8080" LOG_LEVEL: "info"---# 2. Deployment for the web tierapiVersion: apps/v1kind: Deploymentmetadata: name: web-frontendspec: replicas: 3 selector: matchLabels: app: web tier: frontend strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1 template: metadata: labels: app: web tier: frontend spec: containers: - name: web image: frontend:v1.2.3 ports: - containerPort: 3000 envFrom: - configMapRef: name: web-config resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "256Mi" readinessProbe: httpGet: path: /health port: 3000 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 3000 periodSeconds: 15---# 3. Service for internal cluster accessapiVersion: v1kind: Servicemetadata: name: web-servicespec: type: ClusterIP selector: app: web tier: frontend ports: - port: 80 targetPort: 3000---# 4. Service for external access (LoadBalancer)apiVersion: v1kind: Servicemetadata: name: web-external annotations: # Cloud-specific annotations service.beta.kubernetes.io/aws-load-balancer-type: "nlb"spec: type: LoadBalancer selector: app: web tier: frontend ports: - port: 443 targetPort: 3000What Happens When You Apply This:
ConfigMap created → Available for Pod environment injection
Deployment created → Deployment controller creates ReplicaSet
ReplicaSet created → ReplicaSet controller creates 3 Pods
Pods scheduled → Scheduler assigns each Pod to a node
Pods start → kubelet pulls images, starts containers
Probes pass → Pods become Ready
Endpoints populated → Endpoint controller adds Pod IPs to Service endpoints
Services route traffic → kube-proxy configures iptables/IPVS rules
LoadBalancer provisioned → Cloud controller creates external load balancer
Traffic flows → Internet → LB → Node → Pod
We've covered the three fundamental Kubernetes resources in depth. Let's consolidate the key takeaways:
What's Next:
Now that you understand the core resources, we'll explore the Kubernetes control plane and worker nodes in greater depth—examining how they work together to maintain cluster state and execute your workloads.
You now have a comprehensive understanding of Pods, Deployments, and Services—the building blocks of every Kubernetes application. These concepts will inform every deployment you design. Next, we'll examine the control plane and node architecture in detail.