Kubernetes Fundamentals
Pods, Deployments, Services — the mental model for the system that runs most modern infrastructure.
What Kubernetes Actually Does
Kubernetes (K8s) is a container orchestrator. You describe what you WANT running. Kubernetes figures out HOW to make it so. When something goes wrong, it tries to fix it.
You say: "Run 3 copies of this image on this network with these resources, restart them if they crash, and reschedule them if a node dies."
Kubernetes does:
• Picks worker nodes that have capacity → schedules containers
• Restarts crashed containers
• Replaces failed nodes by moving containers elsewhere
• Routes traffic to healthy instances
• Rolls out new versions without downtime
• Scales up/down based on load
The mental shift from Docker: Docker runs containers on ONE machine. Kubernetes runs containers across MANY machines as if they were one giant computer.
Why this matters in 2026: K8s is the de facto standard for serious container deployments. Every cloud provides managed K8s (EKS, GKE, AKS). If you're going to work with containers in production, you'll work with Kubernetes.
Caveat: K8s is complex. For small projects, simpler alternatives (Cloud Run, ECS, Fly.io, Railway) are often better. Use K8s when you genuinely need its power.
The Cluster Mental Model
A Kubernetes cluster has two types of machines:
┌─────────────────────────────────────────────────────────┐
│ CONTROL PLANE │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ API │ │ Scheduler│ │Controller│ │ etcd │ │
│ │ server │ │ │ │ Manager │ │ (state) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ instructs workers
▼
┌──────────────┬──────────────┬──────────────┐
│ Worker 1 │ Worker 2 │ Worker 3 │
│ ┌────────┐ │ ┌────────┐ │ ┌────────┐ │
│ │kubelet │ │ │kubelet │ │ │kubelet │ │
│ └────────┘ │ └────────┘ │ └────────┘ │
│ ┌────────┐ │ ┌────────┐ │ ┌────────┐ │
│ │ Pod │ │ │ Pod │ │ │ Pod │ │
│ │ Pod │ │ │ Pod │ │ │ Pod │ │
│ └────────┘ │ └────────┘ │ └────────┘ │
└──────────────┴──────────────┴──────────────┘
Control plane — the brain. Receives your wishes, decides what to do, monitors state.
• API server — entry point. You talk to it via kubectl or YAML.
• Scheduler — picks which worker should run each new pod.
• Controller manager — reconciles desired state vs actual state.
• etcd — distributed key-value store holding the entire cluster's state.
Workers (nodes) — where containers actually run.
• kubelet — agent that talks to the API server, runs containers, reports status.
• kube-proxy — handles networking on the node.
• Container runtime — containerd, CRI-O, etc.
When you use managed K8s (EKS, GKE), the cloud provider runs the control plane. You only manage workers (or even those are managed in serverless modes like GKE Autopilot, EKS Fargate).
The Core Objects
Kubernetes is a system for managing objects. You describe objects in YAML and submit them. The cluster makes reality match.
The essential objects:
Pod — the smallest deployable unit. Usually one container, sometimes a tightly-coupled few. Pods are ephemeral; they get IPs but they come and go.
Deployment — manages a SET of identical Pods. Says "I want N copies of this Pod, replace any that die, rollout new versions safely." This is what you'll create most often.
Service — a stable network endpoint for a set of Pods. Pods come and go; the Service IP and DNS name stay the same. Other services connect to the Service, not to individual Pods.
Ingress — routes external HTTP(S) traffic to Services. Like an L7 load balancer config.
ConfigMap — non-secret config (env vars, files mounted into Pods).
Secret — sensitive config. Same shape as ConfigMap, but base64-encoded and treated specially.
PersistentVolume / PersistentVolumeClaim — storage that survives Pod restarts. Used for databases, file storage.
Namespace — logical grouping. production, staging, team-a are typical namespaces. Resources in different namespaces don't see each other unless explicitly configured to.
Your First Deployment
A complete app — a Deployment, a Service, and an Ingress:
# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: ghcr.io/me/myapp:abc1234
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests:
cpu: 100m # 0.1 CPU
memory: 128Mi
limits:
cpu: 500m # 0.5 CPU
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
periodSeconds: 5
---
# service.yml
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp # routes to Pods with this label
ports:
- port: 80
targetPort: 3000
type: ClusterIP # internal-only
---
# ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts: [api.example.com]
secretName: myapp-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
Apply:
kubectl apply -f deployment.yml -f service.yml -f ingress.yml
kubectl get pods # see your pods
kubectl get deployment myapp # see deployment status
kubectl logs deploy/myapp # tail logs from any pod in the deployment
kubectl describe pod <name> # debug a specific pod
This is a complete production-ready setup. Three replicas of your app, behind a stable internal service, exposed via HTTPS. Restart on crash, replace on node failure, rolling deploys when you change the image.
Probes — Health Checks That Matter
Two health checks every Pod should have:
Liveness probe — "is this process alive?" If it fails repeatedly, K8s restarts the container.
Readiness probe — "is this process ready to receive traffic?" If it fails, K8s removes the Pod from Service rotation but DOES NOT restart it.
Why both? Imagine your app is healthy but temporarily can't reach the database. You want to stop receiving traffic (readiness fail) but don't want to restart (liveness pass). Eventually the database comes back, readiness recovers, traffic flows.
Common probe types:
# HTTP — most common
livenessProbe:
httpGet:
path: /health
port: 3000
# Command — for non-HTTP services
livenessProbe:
exec:
command: ["/bin/check.sh"]
# TCP — just check the port is open
livenessProbe:
tcpSocket:
port: 6379
Tuning:
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 30 # wait this long after start before first check
periodSeconds: 10 # check every 10s
timeoutSeconds: 3 # fail if no response in 3s
failureThreshold: 3 # restart after 3 consecutive failures
A common bug: liveness too aggressive. The app is slow to start, K8s decides it's dead, restarts it. Goto 10. Use initialDelaySeconds (or better, startupProbe for slow-starting apps).
What endpoints to use:
• /health — basic process alive (just return 200)
• /ready — fully initialized AND can reach dependencies (DB, cache)
Different endpoints for different purposes.
Resource Requests & Limits
Every Pod should specify resources:
resources:
requests: # what the Pod needs
cpu: 100m # 100 millicores = 0.1 CPU
memory: 128Mi
limits: # the cap
cpu: 500m
memory: 512Mi
Requests — used by the scheduler to pick a node with capacity. K8s GUARANTEES at least this much.
Limits — the maximum. CPU above limit gets throttled. MEMORY above limit gets the container killed (OOM).
Why both matter:
• No requests → the scheduler can't place your Pod intelligently → cluster gets imbalanced
• No limits → one buggy Pod can starve every other Pod on the node
• Mismatched (request 100m, limit 1000m) → "burstable" — fine but unpredictable
QoS classes (assigned automatically):
• Guaranteed — requests = limits (best, won't be evicted under pressure)
• Burstable — requests < limits (medium)
• BestEffort — no requests/limits (worst, evicted first under pressure)
For production workloads, use Guaranteed or Burstable. BestEffort is fine for batch jobs that can be killed and retried.
Right-sizing — start conservative, watch metrics:
• kubectl top pod shows actual usage
• Tools like Goldilocks, VPA recommendations help
• Aim for: limit ≈ peak observed × 1.2-1.5
ConfigMaps & Secrets
Don't bake config into images. Inject it.
ConfigMap for non-secret data:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
log-level: info
feature-flags.json: |
{
"newCheckout": true,
"betaSearch": false
}
Use it in a Pod:
spec:
containers:
- name: app
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log-level
volumeMounts:
- name: config
mountPath: /etc/app
volumes:
- name: config
configMap:
name: app-config
Secrets — the same shape but for sensitive data:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
database-url: postgres://...
api-key: sk_live_...
Note: Secrets are base64-encoded, not encrypted. They're only "secret" in the sense that:
• They're stored separately
• RBAC can restrict who reads them
• Some tooling treats them with extra caution
For real secrets, use:
• External Secrets Operator — sync from Vault/AWS Secrets Manager into K8s Secrets
• Sealed Secrets — encrypted at rest in Git
• SOPS — encrypted YAML files
• Kubernetes API server with KMS encryption at rest
Never commit Secret YAMLs to Git unencrypted.
kubectl — Your Daily Driver
Commands you'll use constantly:
# Context (which cluster you're talking to)
kubectl config current-context
kubectl config use-context my-cluster
# Namespaces (default to namespace if not set)
kubectl config set-context --current --namespace=production
# List things
kubectl get pods # in current namespace
kubectl get pods -n kube-system # in another namespace
kubectl get pods -A # all namespaces
kubectl get pods -o wide # more columns
kubectl get pods --watch # live updates
# Inspect
kubectl describe pod <name> # detailed status, events
kubectl logs <pod> # logs from one pod
kubectl logs deploy/myapp # logs from any pod in the deployment
kubectl logs <pod> -f # follow
kubectl logs <pod> --previous # logs from last terminated container
# Exec / port-forward
kubectl exec -it <pod> -- bash
kubectl port-forward svc/myapp 8080:80 # myapp:80 → localhost:8080
# Apply / delete
kubectl apply -f deployment.yml
kubectl delete -f deployment.yml
kubectl delete pod <name> # force restart (replaced by deployment)
# Scale
kubectl scale deployment myapp --replicas=5
kubectl rollout restart deployment myapp
kubectl rollout status deployment myapp
kubectl rollout undo deployment myapp
# Edit live (careful!)
kubectl edit deployment myapp
Aliases worth setting up:
alias k=kubectl
alias kgp='kubectl get pods'
alias kgd='kubectl get deploy'
alias kdp='kubectl describe pod'
alias kl='kubectl logs'
Tools that make K8s nicer:
• k9s — terminal UI for K8s. Once you try it, you won't go back to raw kubectl for browsing.
• stern — multi-pod log tailing
• kubectx / kubens — fast context/namespace switching
⁂ Back to all modules