Why Kubernetes Is Worth Learning
Kubernetes (K8s) is the operating system of the cloud. It's complicated, and there's no pretending otherwise. But if you're running containerized applications at any scale — even a few microservices — it solves real problems that are painful to solve manually.
The problems K8s solves: How do you restart containers that crash? How do you roll out updates without downtime? How do you scale up when traffic spikes? How do you route traffic to the right containers? K8s has opinions on all of these, backed by years of Google's production experience.
Core Architecture
Kubernetes Cluster
├── Control Plane (formerly "master")
│ ├── API Server — All communication goes through here
│ ├── etcd — Cluster state storage (distributed key-value)
│ ├── Scheduler — Decides which node runs which Pod
│ └── Controller Manager — Ensures desired state matches actual state
│
└── Worker Nodes (1 to thousands)
├── kubelet — Agent that runs on each node, talks to API server
├── kube-proxy — Network proxy, manages Service routing
└── Container Runtime — containerd/CRI-O (runs your containers)
The fundamental loop: you tell the API server your desired state ("I want 3 replicas of this container"). Controllers compare desired vs actual state and make changes to reach desired state.
Pods: The Smallest Unit
A Pod is one or more containers that share network and storage:
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: my-app
environment: production
spec:
containers:
- name: app
image: my-app:1.0.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi" # Minimum guaranteed
cpu: "100m" # 100 millicores = 0.1 CPU
limits:
memory: "256Mi" # Maximum allowed
cpu: "500m"
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
# Apply and inspect
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod my-app
kubectl logs my-app
kubectl exec -it my-app -- /bin/sh
Don't run Pods directly in production — they don't restart automatically. Use Deployments.
Deployments: Managed Pod Lifecycle
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # Run 3 Pods
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Create 1 extra pod during update
maxUnavailable: 0 # Never go below 3 running pods
template:
metadata:
labels:
app: my-app # Must match selector
spec:
containers:
- name: app
image: my-app:1.0.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
# Common Deployment operations
kubectl apply -f deployment.yaml
# Scale manually
kubectl scale deployment my-app --replicas=5
# Rolling update
kubectl set image deployment/my-app app=my-app:2.0.0
# Watch the rollout
kubectl rollout status deployment/my-app
# Rollback if something goes wrong
kubectl rollout undo deployment/my-app
# See rollout history
kubectl rollout history deployment/my-app
kubectl rollout undo deployment/my-app --to-revision=2
Services: Stable Networking
Pods are ephemeral — they die and get replaced with new IPs. Services provide a stable endpoint:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
selector:
app: my-app # Routes to Pods with this label
ports:
- port: 80 # Service port
targetPort: 3000 # Container port
type: ClusterIP # Internal only (default)
---
# For external access in cloud:
apiVersion: v1
kind: Service
metadata:
name: my-app-loadbalancer
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
type: LoadBalancer # Cloud provider provisions a load balancer
Service types:
ClusterIP: Internal to cluster (default)NodePort: Exposes on a port on each node (30000-32767)LoadBalancer: Creates a cloud load balancer (AWS ELB, GCP LB, etc.)ExternalName: DNS alias for an external service
Ingress: HTTP Routing
For routing HTTP traffic to multiple services:
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-prod # Auto TLS
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls-cert
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- host: admin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 80
ConfigMaps and Secrets
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
# Key-value pairs
LOG_LEVEL: "info"
API_BASE_URL: "https://api.example.com"
# Multi-line values (config files)
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://localhost:3000;
}
}
---
# secret.yaml (base64 encoded values)
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
url: cG9zdGdyZXNxbDovL3VzZXI6cGFzc0Bob3N0L2Ri # base64 encoded
password: c3VwZXJzZWNyZXQ=
# Create secrets from command line (avoids base64 encoding manually)
kubectl create secret generic db-credentials --from-literal=url="postgresql://user:pass@host/db" --from-literal=password="supersecret"
# Or from files
kubectl create secret generic tls-cert --from-file=tls.crt=./cert.pem --from-file=tls.key=./key.pem
# Using ConfigMap and Secret in a Deployment
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config # All keys become env vars
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
volumeMounts:
- name: config-volume
mountPath: /etc/nginx
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: nginx.conf
path: nginx.conf
Namespaces: Environment Separation
# Create namespaces for environment isolation
kubectl create namespace staging
kubectl create namespace production
# Deploy to specific namespace
kubectl apply -f deployment.yaml -n production
# View resources in a namespace
kubectl get all -n production
# Set default namespace for kubectl
kubectl config set-context --current --namespace=production
# Resource quotas per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
services: "20"
Health Checks
spec:
containers:
- name: app
# Liveness: Is the container alive? Restart if fails.
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30 # Wait 30s before first check
periodSeconds: 10 # Check every 10 seconds
timeoutSeconds: 5 # Fail if no response in 5s
failureThreshold: 3 # Restart after 3 consecutive failures
# Readiness: Is the container ready to receive traffic?
# Remove from Service endpoints if fails (no restart)
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1 # Mark ready after 1 success
# Startup: Slow-starting containers — overrides liveness during startup
startupProbe:
httpGet:
path: /health/live
port: 3000
failureThreshold: 30 # Allow up to 5 minutes to start (30 * 10s)
periodSeconds: 10
HorizontalPodAutoscaler
# autoscaler.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale when CPU > 70%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Essential kubectl Commands
# Cluster info
kubectl cluster-info
kubectl get nodes
kubectl top nodes # CPU/memory usage
# Resources
kubectl get pods -A # All namespaces
kubectl get pods -o wide # Show node assignment
kubectl describe pod <name> # Detailed info + events
kubectl logs <pod> -f # Follow logs
kubectl logs <pod> --previous # Logs from crashed container
# Debugging
kubectl exec -it <pod> -- bash # Shell into container
kubectl port-forward <pod> 8080:3000 # Local port forwarding
kubectl cp <pod>:/path/to/file ./local-file # Copy files
# Apply / Delete
kubectl apply -f . # Apply all YAML in directory
kubectl delete -f deployment.yaml
kubectl delete pod <name> --force # Force delete stuck pod
# Context switching
kubectl config get-contexts
kubectl config use-context production-cluster
# Dry run (test without applying)
kubectl apply -f deployment.yaml --dry-run=client
Kubernetes has a steep learning curve, but the abstractions are well-designed once you understand the mental model. The control loop (desired state → actual state) underlies everything, and once you internalize it, the behavior of each resource type becomes predictable.
→ Convert Docker run commands to Docker Compose format with the Docker Run Converter.