Anatomy of a Kubernetes Object
YAML Structure
Every Kubernetes object follows the same four-section structure. Understanding this structure means you can read and write any Kubernetes manifest:
# Every Kubernetes object has these four top-level fields:
apiVersion: apps/v1 # API group + version
kind: Deployment # Object type
metadata: # Identity and organisation
name: payment-service # Unique within namespace
namespace: production # Logical grouping (default: "default")
labels: # Key-value pairs for selection/organisation
app: payment
tier: backend
version: v2.1
annotations: # Non-identifying metadata
deployment.kubernetes.io/revision: "3"
description: "Handles payment processing"
spec: # DESIRED STATE — what you want
replicas: 3
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
spec:
containers:
- name: payment
image: payment:v2.1
# status: # ACTUAL STATE — set by Kubernetes (read-only)
# availableReplicas: 3
# readyReplicas: 3
# conditions:
# - type: Available
# status: "True"
apiVersion + kind tell Kubernetes what type of object this is. metadata identifies it. spec declares what you want (desired state). status reports what actually exists (set by controllers, never by you).
Labels & Selectors
Labels are the glue that connects Kubernetes objects. A Service finds its pods through labels. A Deployment manages pods through labels. Without labels, objects would be isolated and unmanageable.
# Labels: key-value pairs attached to objects
metadata:
labels:
app: payment # Application name
tier: backend # Architecture tier
environment: prod # Deployment environment
version: v2.1 # Current version
team: platform # Owning team
# Selectors: how objects find each other
# Equality-based:
selector:
matchLabels:
app: payment
tier: backend
# Set-based (more powerful):
selector:
matchExpressions:
- key: environment
operator: In
values: ["prod", "staging"]
- key: tier
operator: NotIn
values: ["frontend"]
- key: version
operator: Exists
# Label operations with kubectl:
# Add labels to existing objects:
kubectl label pods payment-pod-abc12 version=v2.2 --overwrite
# Select pods by label:
kubectl get pods -l app=payment # Exact match
kubectl get pods -l 'tier in (backend, middleware)' # Set-based
kubectl get pods -l app=payment,tier=backend # Multiple conditions
# Show labels as columns:
kubectl get pods --show-labels
kubectl get pods -L app,tier,version # Show specific labels as columns
# Remove a label:
kubectl label pods payment-pod-abc12 version-
Annotations
Annotations store non-identifying metadata — information that tools, libraries, or humans need but that Kubernetes doesn't use for selection:
# Annotations: arbitrary metadata (not used for selection)
metadata:
annotations:
# Deployment history
kubernetes.io/change-cause: "Updated image to v2.1 for security patch"
# Ingress configuration
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Monitoring
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
# Team/ownership
owner: "platform-team@company.com"
oncall: "platform-pager"
# Build info
git-commit: "a1b2c3d4e5f6"
build-date: "2026-05-14T10:30:00Z"
Pods
A Pod is the smallest deployable unit in Kubernetes — one or more containers that share networking (same IP) and storage (shared volumes). You rarely create pods directly; controllers manage them for you.
Pod Lifecycle
stateDiagram-v2
[*] --> Pending: Pod created
Pending --> Running: Containers started
Running --> Succeeded: All containers exit 0
Running --> Failed: Container exits non-zero
Running --> Unknown: Node communication lost
Pending --> Failed: Image pull fails
Failed --> [*]
Succeeded --> [*]
# A complete Pod specification:
apiVersion: v1
kind: Pod
metadata:
name: web-app
labels:
app: web
spec:
# Restart policy: Always (default), OnFailure, Never
restartPolicy: Always
# Resource requests and limits
containers:
- name: web
image: nginx:1.25-alpine
ports:
- containerPort: 80
name: http
resources:
requests: # Minimum guaranteed resources (scheduling)
memory: "128Mi"
cpu: "100m" # 100 millicores = 0.1 CPU core
limits: # Maximum allowed (enforcement)
memory: "256Mi"
cpu: "500m"
# Environment variables
env:
- name: APP_ENV
value: "production"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database.host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
# Health probes
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 10
# Volume mounts
volumeMounts:
- name: app-data
mountPath: /data
- name: config-volume
mountPath: /etc/config
readOnly: true
# Volumes
volumes:
- name: app-data
emptyDir: {}
- name: config-volume
configMap:
name: app-config
Multi-Container Patterns
Pods can contain multiple containers that share the same network namespace (localhost communication) and storage volumes. Three common patterns:
| Pattern | Purpose | Example |
|---|---|---|
| Sidecar | Extend/enhance main container | Log shipper, Istio proxy, monitoring agent |
| Ambassador | Proxy external connections | Database connection pooler, API gateway |
| Adapter | Transform output for consumption | Log format converter, metrics exporter |
# Sidecar pattern: main app + log collector
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
containers:
# Main application container
- name: app
image: my-app:v1.0
ports:
- containerPort: 8080
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
# Sidecar: ships logs to central logging
- name: log-shipper
image: fluentd:v1.16
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
readOnly: true
- name: fluentd-config
mountPath: /fluentd/etc
volumes:
- name: shared-logs
emptyDir: {}
- name: fluentd-config
configMap:
name: fluentd-config
Init Containers
Init containers run sequentially before the main containers start. They're used for setup tasks: waiting for dependencies, populating shared volumes, or running database migrations.
# Init containers: run before main containers
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
# Wait for database to be ready
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres-svc 5432; do echo waiting for db; sleep 2; done']
# Run database migrations
- name: run-migrations
image: my-app:v1.0
command: ['./migrate', '--direction=up']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
containers:
- name: app
image: my-app:v1.0
ports:
- containerPort: 8080
Workload Controllers
ReplicaSets
A ReplicaSet ensures a specified number of pod replicas are running at all times. If pods die, it creates new ones. If too many exist, it removes extras. You rarely create ReplicaSets directly — Deployments manage them for you.
# ReplicaSet reconciliation in action:
kubectl get rs
# NAME DESIRED CURRENT READY AGE
# payment-6f7g8h9j 3 3 3 2d
# Kill a pod — ReplicaSet immediately creates a replacement:
kubectl delete pod payment-6f7g8h9j-abc12
kubectl get pods -w
# payment-6f7g8h9j-abc12 1/1 Terminating 0 2d
# payment-6f7g8h9j-xyz99 0/1 Pending 0 0s
# payment-6f7g8h9j-xyz99 0/1 ContainerCreating 0 1s
# payment-6f7g8h9j-xyz99 1/1 Running 0 3s
Deployments
Deployments are the primary workload controller for stateless applications. They manage ReplicaSets and provide declarative updates with rollout strategies:
# Deployment with rolling update strategy
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
labels:
app: payment
spec:
replicas: 5
# Rolling update: replace pods gradually
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # Allow 2 extra pods during update
maxUnavailable: 1 # At most 1 pod unavailable during update
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
version: v2.1
spec:
containers:
- name: payment
image: payment:v2.1
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
# Deployment operations:
# Update image (triggers rolling update):
kubectl set image deployment/payment-service payment=payment:v2.2
# Watch rollout progress:
kubectl rollout status deployment/payment-service
# Waiting for deployment "payment-service" rollout to finish:
# 3 out of 5 new replicas have been updated...
# 4 out of 5 new replicas have been updated...
# deployment "payment-service" successfully rolled out
# View rollout history:
kubectl rollout history deployment/payment-service
# REVISION CHANGE-CAUSE
# 1 Initial deployment
# 2 kubectl set image deployment/payment-service payment=payment:v2.1
# 3 kubectl set image deployment/payment-service payment=payment:v2.2
# Rollback to previous version:
kubectl rollout undo deployment/payment-service
# Rollback to specific revision:
kubectl rollout undo deployment/payment-service --to-revision=1
# Pause/resume rollout (for canary-style testing):
kubectl rollout pause deployment/payment-service
# ... test the partial rollout ...
kubectl rollout resume deployment/payment-service
StatefulSets
StatefulSets are for applications that need stable identity and persistent storage — databases, message queues, and distributed systems that require ordered startup and unique network names.
# StatefulSet for a PostgreSQL replica set
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless # Required: headless Service for DNS
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
# Each pod gets its own PVC (persistent storage)
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
# Result: pods get stable names and storage:
# postgres-0 → PVC data-postgres-0 (50Gi)
# postgres-1 → PVC data-postgres-1 (50Gi)
# postgres-2 → PVC data-postgres-2 (50Gi)
# DNS: postgres-0.postgres-headless.default.svc.cluster.local
| Property | Deployment | StatefulSet |
|---|---|---|
| Pod names | Random suffix (pod-7f8g9h-abc12) | Ordered (postgres-0, postgres-1, postgres-2) |
| Storage | Shared or ephemeral | Per-pod persistent volumes |
| Startup order | All pods start simultaneously | Sequential (0 before 1 before 2) |
| Network identity | Via Service (random routing) | Stable DNS per pod |
| Use case | Stateless web apps, APIs | Databases, Kafka, Elasticsearch |
DaemonSets
A DaemonSet ensures one pod runs on every node (or a subset of nodes). Used for node-level agents like log collectors, monitoring, and network plugins:
# DaemonSet: run a monitoring agent on every node
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
tolerations:
# Run on ALL nodes including masters
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
ports:
- containerPort: 9100
hostPort: 9100
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
Jobs & CronJobs
# Job: run a task to completion
apiVersion: batch/v1
kind: Job
metadata:
name: database-backup
spec:
backoffLimit: 3 # Retry 3 times on failure
activeDeadlineSeconds: 300 # Timeout after 5 minutes
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "postgres-0.postgres-headless", "-U", "admin", "-d", "appdb"]
---
# CronJob: scheduled recurring task
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *" # Every day at 2 AM
concurrencyPolicy: Forbid # Don't start new if previous still running
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "postgres-0.postgres-headless", "-U", "admin", "-d", "appdb"]
Configuration Objects
ConfigMaps
ConfigMaps separate configuration from container images, allowing the same image to run in different environments with different settings:
# Create ConfigMap from literal values:
kubectl create configmap app-config \
--from-literal=database.host=postgres-svc \
--from-literal=database.port=5432 \
--from-literal=log.level=info
# Create ConfigMap from a file:
kubectl create configmap nginx-config --from-file=nginx.conf
# View ConfigMap:
kubectl get configmap app-config -o yaml
# ConfigMap definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
database.host: "postgres-svc"
database.port: "5432"
log.level: "info"
app.properties: |
server.port=8080
spring.datasource.url=jdbc:postgresql://postgres-svc:5432/appdb
spring.redis.host=redis-svc
---
# Using ConfigMap in a Pod:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: my-app:v1.0
# As environment variables:
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database.host
# As volume mount (file):
volumeMounts:
- name: config
mountPath: /etc/app/config
readOnly: true
volumes:
- name: config
configMap:
name: app-config
Secrets
Secrets store sensitive data (passwords, tokens, certificates). They're similar to ConfigMaps but with additional protections: base64-encoded at rest, can be encrypted in etcd, and access can be restricted via RBAC.
# Create Secret:
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cur3P@ssw0rd!'
# Create TLS Secret:
kubectl create secret tls app-tls \
--cert=tls.crt \
--key=tls.key
# View Secret (values are base64-encoded):
kubectl get secret db-credentials -o yaml
# data:
# username: YWRtaW4= (base64 of "admin")
# password: UzNjdXIzUEBzc3cwcmQh
# Decode a secret value:
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
Services & Discovery
Service Types
A Service provides a stable network endpoint for a set of pods. Pods come and go, but the Service IP and DNS name remain constant:
# ClusterIP Service (default): internal-only access
apiVersion: v1
kind: Service
metadata:
name: payment-svc
spec:
type: ClusterIP
selector:
app: payment # Routes to all pods with label app=payment
ports:
- port: 80 # Service port (what clients connect to)
targetPort: 8080 # Pod port (where containers listen)
protocol: TCP
---
# NodePort Service: accessible from outside on every node's IP
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # External port (30000-32767)
---
# LoadBalancer Service: provisions cloud load balancer
apiVersion: v1
kind: Service
metadata:
name: web-public
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 443
targetPort: 8080
flowchart TD
subgraph ClusterIP
C1[Internal DNS] --> CS[Service 10.96.x.x]
CS --> P1[Pod 10.244.1.5]
CS --> P2[Pod 10.244.2.8]
end
subgraph NodePort
EXT1[External:30080] --> N1[Node1:30080]
EXT1 --> N2[Node2:30080]
N1 --> NS[Service]
N2 --> NS
NS --> P3[Pod]
NS --> P4[Pod]
end
subgraph LoadBalancer
LB[Cloud LB
Public IP] --> N3[Node1]
LB --> N4[Node2]
N3 --> LS[Service]
N4 --> LS
LS --> P5[Pod]
LS --> P6[Pod]
end
DNS Resolution
Kubernetes automatically creates DNS records for Services. Pods can reach any Service by name without knowing IP addresses:
# DNS naming convention:
# <service-name>.<namespace>.svc.cluster.local
# From a pod in the "default" namespace:
curl http://payment-svc # Same namespace (short name)
curl http://payment-svc.default # Explicit namespace
curl http://payment-svc.default.svc.cluster.local # Fully qualified
# From a pod in a DIFFERENT namespace:
curl http://payment-svc.production # Cross-namespace
curl http://payment-svc.production.svc.cluster.local
# Headless Service (no ClusterIP, returns pod IPs directly):
# Useful for StatefulSets where clients need individual pod addresses
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
spec:
clusterIP: None # Makes it headless
selector:
app: postgres
ports:
- port: 5432
# DNS returns individual pod IPs:
# nslookup postgres-headless → 10.244.1.5, 10.244.2.8, 10.244.3.12
# nslookup postgres-0.postgres-headless → 10.244.1.5 (specific pod)
Exercises
maxSurge: 1 and maxUnavailable: 0. Watch the rollout with kubectl rollout status. Then roll back. Explain what happens to the ReplicaSets during each operation.
kubectl exec.
Conclusion
The Kubernetes object model gives you a universal vocabulary for declaring infrastructure. Key takeaways:
- Everything is an API object with apiVersion, kind, metadata, spec, and status
- Labels are the connective tissue — Services find pods, controllers manage pods, all through label selectors
- Controllers maintain desired state — Deployments, ReplicaSets, StatefulSets, DaemonSets, Jobs
- Configuration is separated from images — ConfigMaps for settings, Secrets for sensitive data
- Services provide stable endpoints — DNS-based discovery regardless of pod IP changes
In Part 8, we'll dive deep into Kubernetes networking — the flat networking model, CNI plugins, pod-to-pod communication, and how network policies control traffic flow between services.