All Tools View Categories Blog About Contact Privacy

Kubernetes Orchestration Explained: How It Works and Why You Need It

Kubernetes Orchestration Explained: How It Works and Why You Need It

Kubernetes is the warehouse robot for your containers — you declare "I want 3 copies of my app, healthy, at version 1.2.3," and Kubernetes continuously bins, places, heals, scales, and rolls them out with zero downtime. This guide explains how orchestration works and why you need it, from control plane vs workers to Deployment YAML, probes, and production best practices — no prior K8s needed.

TL;DR — Kubernetes Orchestration:
  • What it is: declarative container orchestrator — you write desired state (YAML: replicas:3, image:myapp:1.2.3), controllers continuously reconcile observed → desired.
  • Why: without it you docker run manually, ssh to restart crashed nodes at 3am, and juggle scp deploys with downtime. With K8s: self-healing, bin-packing, rolling updates (maxUnavailable:25%), autoscaling (HPA on 60% CPU), and declarative GitOps.
  • Architecture: Control plane (API Server → etcd → Scheduler → Controller Manager → CCM) manages state; workers (kubelet + kube-proxy + containerd + CNI/CSI) run Pods. kubectl apply -f deployment.yaml → API validates → etcd → scheduler → kubelet.
  • Core objects: Pod (1+ containers) → Deployment (replicas + selector + template) → ReplicaSet → Service (stable ClusterIP) → Ingress (L7) → ConfigMap/Secret + Namespace.
  • Prod checklist: liveness/readiness/startupProbe + requests/limits + RollingUpdate + PodDisruptionBudget + runAsNonRoot/readOnlyRootFS. Generate correct Deployment without syntax errors via our Kubernetes deployment generator and audit it with our Kubernetes best practices checker; tail structured JSON logs with our Kubernetes pod log formatter instead of raw kubectl logs blobs.

Why Orchestration? — The Pain Kubernetes Solves

Docker alone is excellent for one host: docker run -d -p 3000:3000 myapp:1.2 and Compose for app + db + cache on that host. It breaks when you need high availability, scale, and zero-downtime deploys across machines.

Without orchestration: scheduling is manual — you pick vm1 vs vm2 by gut; healing is you at 3am sshing to docker restart after a crash; scaling is buying a VM, copying env files, joining a manual load balancer; deploy is scp + docker pull + docker stop/start with a visible gap; discovery is hard-coded IPs that change. None is declarative, versioned, or auditable.

Why Kubernetes orchestration vs manual Docker no self-heal scale deploy pain

With Kubernetes you declare replicas:3, image:myapp:1.2.3, and strategy: RollingUpdate. Controllers keep 3 healthy copies spread across nodes, reschedule when a node dies, autoscale via HorizontalPodAutoscaler (60% CPU → 3→10), and roll out 1.2.3 → 1.2.4 with maxUnavailable:25% — no manual ssh, no downtime. Analogy: Docker is a shipping container, Compose is stacking containers on one truck, Kubernetes is a port with cranes (scheduler), yard planners (controllers), and robots (kubelet) that continuously place, monitor, and replace containers. Docs: Kubernetes overview and What is Kubernetes.

When Do You Actually Need Kubernetes?

Rule of thumb: 1 service on 1–2 VMs → Docker/Compose is simpler and cheaper. 3+ services needing HA, scale-to-zero/peak, zero-downtime, blue-green/canary, or GitOps audit → Kubernetes pays. If you run a single API with 100 requests/day on one VM, K8s control plane overhead (3 etcd masters, networking) is overkill. If you run web + api + worker + db + redis needing rolling deploys without dropping requests, Kubernetes' declarative model and bin-packing (fitting Pods tightly by requests) save more than its complexity costs.

Architecture — Control Plane vs Workers (Who Does What)

Kubernetes has two halves; only the API server talks to etcd:

  1. Control plane (brain — runs on 1 or 3 masters, no user Pods ideally):
    • kube-apiserver — REST gateway, validates YAML, the only writer to etcd; all other components watch via API. Exposed via kubectl.
    • etcd — consistent key-value store — the source of truth for desired (Deployments) and observed (Node heartbeats). Lose etcd, lose cluster state — back it up.
    • kube-scheduler — assigns unassigned Pods to Nodes by predicates (fits CPU/mem requests? affinity? taints?) and priorities (spread).
    • kube-controller-manager — loops like Deployment → ReplicaSet → Pods, Job → Pods, Node lifecycle; continuously compares observed vs desired.
    • cloud-controller-manager (CCM) — cloud-specific: creates cloud LB for Service LoadBalancer, mounts volumes.
  2. Worker nodes (muscle — where your Pods run):
    • kubelet — agent that ensures Pod spec from API is running: pulls image, starts containers via runtime, runs probes, reports status.
    • kube-proxy — programs iptables/IPVS so Service ClusterIP load-balances to Pod endpoints.
    • Container runtime — containerd or CRI-O (Docker shim removed in 1.24).
    • CNI — Calico/Cilium for Pod network; CSI for storage drivers.
Kubernetes architecture control plane API etcd scheduler controller worker kubelet proxy

Data flow: kubectl apply -f deployment.yaml → API validates + writes to etcd → watchers fire → controller sees Deployment with replicas:3 but 0 Pods → creates 3 Pods → scheduler binds each Pod to a Node → kubelet on that Node sees assignment → pulls image + runs containers + reports Ready → endpoints controller adds Pods to Service endpoints → kube-proxy programs rules. Workers never call scheduler; scheduler never calls kubelet — everything via API+etcd. Docs: Cluster Architecture and Components.

Managed Control Plane — You Rarely Run Masters Yourself Today

In prod, most teams use managed control planes (EKS, GKE, AKS) — the cloud provider runs etcd+API highly available, you pay for workers. Self-hosting with kubeadm is educational and valid, but etcd quorum (3 masters) and upgrades are operational burden. See kubeadm bootstrap for self-managed.

Core Objects — Pod, Deployment, ReplicaSet, Service, Ingress, ConfigMap/Secret, Namespace

Kubernetes objects are just YAML with apiVersion, kind, metadata, spec. Seven shape everything:

  • Pod — smallest deployable: 1+ containers that share IP, localhost, and volumes, scheduled together, ephemeral (IP dies with Pod). You rarely create Pods directly; Deployments do. Spec: Pods.
  • ReplicaSet — ensures N Pod copies match a selector; usually owned by a Deployment.
  • Deployment — declarative replicas + template + selector + strategy (RollingUpdate, Recreate); manages ReplicaSets for rolling updates and rollbacks. Docs: Deployments.
  • Service — stable virtual IP/DNS (myapp.default.svc.cluster.local) that load-balances to Pods matching selector: app=myapp, even as Pod IPs churn. Types: ClusterIP (internal), NodePort, LoadBalancer (cloud LB), ExternalName. See Service.
  • Ingress — L7 routing: host: api.example.com, path /v1/ → Service myapp:80, TLS termination; needs controller (nginx, ALB). See Ingress.
  • ConfigMap/Secret — config: envFrom or volume mounts; Secret is base64-encoded (not encrypted by default — enable encryption at rest or external KMS).
  • Namespace — virtual cluster partition (default, kube-system, prod) for isolation + quotas; most objects are namespaced.
Kubernetes core objects Pod Deployment ReplicaSet Service Ingress ConfigMap Secret Namespace
Deployment myapp (replicas:3, selector app=myapp)
 └─ ReplicaSet myapp-6f9d8- 
     ├─ Pod myapp-6f9d8-abc (node1, 10.244.1.5)
     ├─ Pod myapp-6f9d8-def (node2, 10.244.2.7)
     └─ Pod myapp-6f9d8-ghi (node3, 10.244.3.2)
Service myapp (ClusterIP 10.96.0.1) selector app=myapp → endpoints = 3 Pod IPs (IPVS)
Ingress api.example.com /api/* → Service myapp:80

You read this as: Deployment declares template, Service selects by labels, Ingress routes by host/path. Labels are the glue — mismatch selector vs template.labels and Pods are orphaned (Deployment shows 0 available).

How It Works — Declarative Reconciliation in One Loop

Everything is a loop: compare observed (etcd) to desired (spec), create/update/delete to close the gap, repeat every seconds. Concrete replicas:3 flow:

  1. kubectl apply -f deployment.yaml → API validates schema, writes Deployment to etcd (versioned).
  2. Deployment controller watches → sees desired 3 vs observed 0 ReplicaSets → creates ReplicaSet myapp-6f9d8 with 3.
  3. ReplicaSet controller → creates 3 Pod objects (Pending, unassigned).
  4. Scheduler watches unassigned Pods → scores nodes (fits requests? zone? affinity? taint?) → binds myapp-abc → node1 etc. via API → etcd.
  5. Kubelet on node1 watches assigned Pods → containerd pulls ghcr.io/org/app:1.2.3 → starts containers → runs readinessProbe → writes Pod status Ready → etcd.
  6. Endpoints controller → sees 3 Ready Pods matching Service → updates Endpoints → kube-proxy programs IPVS.
  7. Loop continues: node dies → Node controller marks NotReady after 40s → Deployment controller sees 2 vs 3 → creates replacement → scheduler → kubelet.
Kubernetes reconciliation loop kubectl apply API etcd controller scheduler kubelet

Rolling update is the same loop with two ReplicaSets: maxUnavailable:25% (of 4 =1) + maxSurge:25% (1) → new RS scales to 1, old RS scales down to 2, readiness ensures new Pod is Ready before old terminates → zero downtime without you scripting. Rollback is kubectl rollout undo deployment/myapp → controller scales old RS up. Docs: Updating a Deployment.

Events — The First Debug Surface

Controllers emit Events (different from logs): kubectl get events --sort-by=.lastTimestamp shows FailedScheduling, Unhealthy, BackOff, Killing with reason+message — the fastest clue before reading pod logs. Events expire after 1h, so watch them during deploys.

Deployment YAML — 9 Fields That Decide Production vs Demo

Valid YAML applies, but prod needs 9 fields beyond replicas:

apiVersion: apps/v1
kind: Deployment
metadata: { name: myapp, labels: { app: myapp, version: v1.2.3 } }
spec:
  replicas: 3
  selector: { matchLabels: { app: myapp } }          # immutable — must match template labels
  strategy: { type: RollingUpdate, rollingUpdate: { maxUnavailable: 25%, maxSurge: 25% } }
  template:
    metadata: { labels: { app: myapp, version: v1.2.3 } }
    spec:
      containers:
      - name: app
        image: ghcr.io/org/app:1.2.3               # pin tag, not :latest — rollback needs immutable
        ports: [{ containerPort: 3000, name: http }]
        envFrom: [{ configMapRef: { name: myapp-config } }]
        livenessProbe:  { httpGet: { path: /health, port: http }, initialDelaySeconds: 15, periodSeconds: 10, failureThreshold: 3 }
        readinessProbe: { httpGet: { path: /ready,  port: http }, periodSeconds: 5 }
        startupProbe:   { httpGet: { path: /health, port: http }, failureThreshold: 30, periodSeconds: 5 } # for slow boot
        resources: { requests: { cpu: "100m", memory: "128Mi" }, limits: { cpu: "500m", memory: "256Mi" } }
        securityContext: { runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } }
      terminationGracePeriodSeconds: 30
Kubernetes Deployment YAML replicas selector probes resources securityContext strategy
  • selector vs template.labels — selector is immutable; change labels without changing selector → orphaned Pods (Deployment shows 0 available) — recreate needed.
  • Probesliveness restarts crashed container (inside Pod), readiness removes Pod from Service endpoints until 200, startupProbe disables liveness until boot completes (otherwise boot liveness kills slow start). Missing probes → K8s can't distinguish starting vs hung → bad rollout.
  • resources.request/limit — request is scheduler bin-packing input; limit is cgroup kill threshold. No requests → scheduler over-packs node → OOMKill under load. QOS: Guaranteed requests==limits, Burstable requests<limits, BestEffort none — avoid BestEffort in prod. Docs: Managing Resources.
  • RollingUpdatemaxUnavailable + maxSurge control downtime vs burst capacity; 25%/25% is balanced for 4+ replicas.

Don't hand-type this from memory — generate validated YAML with our Kubernetes deployment generator (pick replicas, image, ports, probes, resources, securityContext) and it emits correct apiVersion/kind/selector without indentation errors or missing matchLabels. Generate via our Kubernetes deployment generator.

ConfigMap, Secret, Namespace — Config Without Rebuild

apiVersion: v1
kind: ConfigMap
metadata: { name: myapp-config }
data: { LOG_LEVEL: "info", FEATURE_X: "true" }
---
apiVersion: v1
kind: Secret
metadata: { name: myapp-secret }
type: Opaque
data: { DATABASE_URL: "cG9zdGdyZXM6Ly8..." }  # base64, not encrypted by default
---
# Pod envFrom:
envFrom: [{ configMapRef: { name: myapp-config } }, { secretRef: { name: myapp-secret } }]
# or volume mount for certs

Change ConfigMap → Pods need restart to see new values (kubectl rollout restart deployment/myapp or reloader). Secrets in etcd are base64, not encrypted unless you enable encryption at rest or use external KMS/Vault — default Secrets in etcd are as sensitive as the etcd backup.

Services, Ingress, and DNS — Stable Names for Ephemeral Pods

apiVersion: v1
kind: Service
metadata: { name: myapp }
spec:
  selector: { app: myapp }
  ports: [{ port: 80, targetPort: http, protocol: TCP }]
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: myapp, annotations: { nginx.ingress.kubernetes.io/rewrite-target: / } }
spec:
  rules:
  - host: api.example.com
    http: { paths: [{ path: /api, pathType: Prefix, backend: { service: { name: myapp, port: { number: 80 } } } }] }
  tls: [{ hosts: [api.example.com], secretName: myapp-tls }]

Service DNS is myapp.default.svc.cluster.local — Pods call http://myapp in same namespace, CoreDNS resolves to ClusterIP → kube-proxy IPVS → Pod endpoints (even as Pod IPs churn). Ingress needs a controller (ingress-nginx, AWS ALB) actually implementing the rules — Ingress without controller is just YAML. Needs ingressClassName in recent K8s. Docs: Service, Ingress.

Network Policies — Default Allow Is Unsafe

By default, every Pod can talk to every Pod. Add NetworkPolicy to deny-by-default then allow: apiVersion: networking.k8s.io/v1, kind: NetworkPolicy, spec: podSelector: { matchLabels: { app: myapp } } policyTypes: [Ingress] ingress: [from: [podSelector:{app:frontend}] ports:[{port:http}]]. Without this, one compromised Pod scans the whole cluster. See Network Policies.

Autoscaling — HPA, VPA, and Cluster Autoscaler (Three Layers)

Autoscaling is three knobs at different layers:

  1. HorizontalPodAutoscaler (HPA) — replicas vs metric: apiVersion: autoscaling/v2, spec: scaleTargetRef: { kind: Deployment, name: myapp }, minReplicas: 3, maxReplicas: 20, metrics: [{ type: Resource, resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } } }] → when avg CPU >60%, controller bumps replicas. Needs metrics-server + requests.cpu set — without requests, utilization is undefined. Docs: HPA.
  2. VerticalPodAutoscaler (VPA) — adjusts requests/limits per Pod based on history; conflicts with HPA on same metric — use VPA for rightsizing in staging, HPA for scale in prod.
  3. Cluster Autoscaler (CA) — nodes vs pending Pods: when Pods are Pending due to insufficient CPU/mem, CA asks cloud to add nodes; scales in via eviction. Managed on EKS/GKE via node groups. See Cluster Autoscaler (GitHub).

Set all three? Start with HPA (workload) + CA (capacity). HPA reacts in ~60s (metric window), CA in minutes (node boot). Without HPA, Pods saturate before new nodes help.

Updates — RollingUpdate, Recreate, Blue-Green, Canary

  • RollingUpdate (default): incremental new ReplicaSet scale-up + old scale-down with readiness gating. Safe for stateless: kubectl set image deployment/myapp app=ghcr.io/org/app:1.2.4 → watch kubectl rollout status deployment/myapp. Rollback: kubectl rollout undo deployment/myapp. Docs: Rolling Update.
  • Recreate: old ReplicaSet to 0 then new up — downtime, use for RWO volumes that can't mount twice.
  • Blue-Green / Canary: not native to Deployment — use two Deployments (myapp-v1, myapp-v2) + Service selector flip or progressive controllers (Argo Rollouts, Flagger) that weight traffic 5%→50%→100% and abort on metric.

Add PodDisruptionBudget (PDB) for voluntary disruptions: apiVersion: policy/v1, kind: PodDisruptionBudget, spec: { minAvailable: 2, selector: { matchLabels: { app: myapp } } } ensures eviction (CA scale-in, node drain) never drops below 2 available. Without PDB, a drain can take all 3 replicas offline at once. See PDB.

Best Practices — 6 Checks Before Prod (Audit with Checker)

CheckProd ValueFix
Probesliveness restart + readiness gatelivenessProbe httpGet /health, readinessProbe /ready, startupProbe for slow
Resourcesscheduling + QOS + HPArequests: cpu 100m mem 128Mi, limits: 500m 256Mi
Securityleast privilegerunAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, drop: [ALL]
Imageimmutable + pullTag 1.2.3 not :latest, imagePullPolicy: IfNotPresent, private → imagePullSecrets
Deploymentzero-downtimeRollingUpdate 25%/25%, PDB minAvailable:2, labels consistent
Observabilitydebug + scaleJSON logs level/msg, HPA cpu 60%, labels app/version
Kubernetes best practices probes resources security deployment observability

Manual review misses cross-object drift (selector vs labels, limit vs request). Paste your Deployment/Service YAML into our Kubernetes best practices checker — it flags missing probes, unset limits, privileged escalation, :latest, and readiness gaps in one pass. Fix the red items before kubectl apply.

Security Deep Dive — Beyond the Table

Two defaults bite: No NetworkPolicy = allow all — add deny-by-default then allow frontend → api → db explicitly; Secrets in etcd not encrypted by default — enable encryption at rest or mount via external Secrets Store. Also set seccompProfile: { type: RuntimeDefault } and non-root. Audit: Pod Security Standards (restricted).

Logs and Troubleshooting — From CrashLoopBackOff to Ready

When a Pod isn't Ready, read status top-down:

  1. kubectl get pods -l app=myappRunning 2/3 → ready vs total; Pending, CrashLoopBackOff, ImagePullBackOff are distinct.
  2. kubectl describe pod myapp-abc-123 — Events: FailedScheduling (no node fits), Unhealthy (probe), BackOff (crash), FailedMount (PVC).
  3. kubectl logs deploy/myapp -c app --tail=100 -f + kubectl logs myapp-abc-123 --previous — previous crash's stdout before restart (critical for boot failures where current logs are empty). Add kubectl logs --timestamps --since=10m for correlation.
  4. kubectl get events --sort-by=.lastTimestamp — cluster-wide, includes scheduler and controller reasons.
  5. kubectl exec -it myapp-abc-123 -- sh — inside container to test DNS (getent hosts myapp), files, config.
ImagePullBackOff → image: typo or private without imagePullSecrets
kubectl describe pod  # Events: Failed to pull image "ghcr.io/org/app:wrong"
kubectl get secret regcred && kubectl get serviceaccount default -o yaml # check imagePullSecrets

CrashLoopBackOff → app exits fast (restart backoff 10s → 20s → ...)
kubectl logs myapp-abc-123 --previous # last crash stack trace
kubectl describe pod  # Liveness probe failed? Check path/port

Pending → scheduler found no node
kubectl describe pod  # 0/3 nodes available: insufficient cpu
kubectl top nodes  # needs metrics-server
kubectl get pvc  # Bound? Pending PVC blocks Pod

NotReady → readinessProbe failing → pod removed from Service endpoints
kubectl get endpoints myapp  # empty? → readiness issue
curl from debug pod: kubectl run curl --image=curlimages/curl -i --rm -- curl http://myapp:80/ready
Kubernetes logs kubectl logs describe events exec troubleshooting CrashLoopBackOff

Raw kubectl logs interleaves JSON with plain text and no level color — tailing 500 lines blurs info vs error. Pipe through our Kubernetes pod log formatter to filter by level, highlight JSON fields (level/msg/rt/status), and group by Pod — especially for kubectl logs deployment/myapp --previous bursts where the crashed container's error line is hidden among 200 info lines. For pipeline, ensure app logs JSON ({"level":"error","msg":"db timeout","rt":"2.1s"}) not free-form text.

Metrics — CPU/Memory Without Them You're Blind

Install metrics-server (kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml) → kubectl top pod --all-namespaces and HPA metrics appear. Without it, HPA shows /60%. See metrics-server and HPA metrics.

Should You Choose Kubernetes — Decision Tree and Alternatives

Decision:
  • 1 app, 1 VM, no scale: Docker + systemd + maybe Caddy. Add Docker Compose for 3 local services. Overhead: minimal.
  • 3–10 services, need HA/zero-downtime/rollbacks: Kubernetes (managed EKS/GKE/AKS). Overhead: control plane managed, but manifests + CI/CD + observability.
  • Batch/worker heavy: still K8s with Job/CronJob + queue semantics.
  • Only need container deploy without K8s API: ECS, Cloud Run, Nomad — simpler than K8s if you don't need its ecosystem.

If you go K8s, start with kind (Kubernetes IN Docker) locally: kind create cluster && kubectl apply -f deployment.yaml && kubectl get pods — full cluster on laptop without cloud bill. See kind. For GitOps, add Argo CD or Flux to sync manifests from Git — then kubectl apply becomes Git push.

Explore templates and validators beyond this guide via our Kubernetes tools collection — deployment generator, best practices checker, log formatter, plus upcoming HPA and NetworkPolicy helpers — so you generate valid YAML instead of debugging indentation at 2am.

Practice Lab — 5 Minutes from Kind to Rolling Update

kind create cluster
kubectl create namespace demo
kubectl create deployment myapp --image=nginx:alpine --replicas=3 --port=80 -n demo
kubectl expose deployment myapp --type=ClusterIP --port=80 -n demo
kubectl get pods,svc -n demo -o wide
kubectl set image deployment/myapp nginx=nginx:alpine --record -n demo  # no change, demo
kubectl rollout status deployment/myapp -n demo
kubectl scale deployment/myapp --replicas=5 -n demo && kubectl get pods -w -n demo  # watch reconcile
kubectl delete namespace demo && kind delete cluster

What you just practiced: declarative create deployment (generates Deployment spec), expose via Service (selector auto), scale (observed 3 → desired 5 → new Pods), and cleanup via Namespace delete (namespaces garbage-collect all child objects). Next: paste the generated Deployment YAML into the best practices checker, fix reds, then re-apply.

StatefulSets, Jobs, and CronJobs — Not Everything Is a Deployment

Deployments are for stateless replicas that are interchangeable. Three other controllers cover different lifecycles:

  • StatefulSet — identity + stable storage. Use for databases (Postgres, Redis, Kafka) where each replica needs a stable name (db-0, db-1) and stable PVC (data-db-0 survives Pod reschedule). Unlike Deployment pods (myapp-abc-xyz random hash), StatefulSet pods are ordinal, created in order, and each keeps its volume. Headless Service (clusterIP: None) gives DNS db-0.db.default.svc.cluster.local for peer discovery. Docs: StatefulSets.
  • Job — run once to completion. Use for migrations, batch processing: apiVersion: batch/v1, kind: Job, spec: { completions: 1, template: { spec: { containers: [{ name: migrate, image: myapp:1.2.3, command: ["npm","run","migrate"] }], restartPolicy: Never } } }. Job tracks success; restartPolicy is Never/OnFailure (not Always like Deployment). See Jobs.
  • CronJob — scheduled Jobs. apiVersion: batch/v1, kind: CronJob, spec: { schedule: "0 2 * * *", jobTemplate: { spec: { template: { spec: { containers: [{ name: backup, image: pg:16, command: ["pg_dump"] }] } } } } } — cluster cron. concurrencyPolicy: Forbid/Replace controls overlap. Check startingDeadlineSeconds and successfulJobsHistoryLimit to avoid flooding history. Docs: CronJobs.

Storage — From EmptyDir to PVC and StorageClass

Pods are ephemeral; volumes persist beyond them:

  • emptyDir — scratch tied to Pod lifetime (share between sidecars, die with Pod).
  • ConfigMap/Secret volumes — config as files.
  • PersistentVolumeClaim (PVC) — request storage: apiVersion: v1, kind: PersistentVolumeClaim, metadata: { name: data }, spec: { accessModes: [ReadWriteOnce], resources: { requests: { storage: 10Gi } }, storageClassName: gp3 } → Dynamic provisioning via StorageClass (AWS gp3, GKE pd-standard) creates PV and binds. AccessModes: ReadWriteOnce (one node), ReadWriteMany (needs EFS/NFS), ReadOnlyMany. See Persistent Volumes and Storage Classes.

Beginner pitfall: Deployments + PVC ReadWriteOnce with 3 replicas all mount the same PVC → only one Pod schedules where volume is attached, 2 stay Pending with Multi-Attach error. Use StatefulSet with volumeClaimTemplates so each replica gets its own PVC.

Packaging — Helm and Kustomize (Don't Copy-Paste YAML)

Copy-pasting deployment.yaml per env (dev/staging/prod) drifts. Two package managers solve it:

  • Kustomize (built into kubectl): base Deployment + overlays per env via kustomization.yaml patches (change replicas: 3 → 10 for prod without forking files). Run kubectl apply -k overlays/prod. Docs: Kustomize.
  • Helm: charts with templates + values.yaml: helm install myapp ./chart --set replicaCount=3. Templating is powerful but adds Go template complexity. For small teams, start Kustomize; grow to Helm when charts are shared. Chart hub: Artifact Hub.

Either pairs with GitOps (Argo CD/Flux syncs the rendered manifests from Git to cluster, shows drift, and allows safe sync + prune). Without GitOps, kubectl apply history is local and unauditable.

Cost and Ops Reality — Requests, Limits, and FinOps

Most beginners over-provision by 3×. Two steps cut bill 30–60%:

  1. Set requests honestly by observing: kubectl top pod --all-namespaces over a week (p50/p95) → set requests near p50 (scheduler guarantee) and limits near p95 ×1.2 (burst). Too low request → Pending; too high → node under-utilized.
  2. Right-size HPA target: 60% CPU is balanced; 40% wastes, 80% risques throttling. Add VPA in recommendation mode (updateMode: Off) to suggest without auto-resizing while you validate HPA.

Use kube-resource-report or managed FinOps (Kubecost) to see namespace waste. Without observability, autoscaling saves nothing because you scale blind.

Frequently Asked Questions

What is Kubernetes orchestration in simple terms?

You declare desired state (replicas:3, image:1.2.3) in YAML; Kubernetes continuously reconciles reality to match — scheduling Pods to Nodes, restarting failed containers, load-balancing via Services, and rolling out updates with readiness gating. No ssh, no manual placement.

What's the difference between Docker and Kubernetes?

Docker packages and runs one container on one host. Kubernetes orchestrates many containers across many hosts — scheduling, healing, scaling, rolling updates, service discovery, and secrets/config. Use Docker/Compose for local/single-host; K8s for distributed, HA, and declarative GitOps.

What is a Pod vs Deployment vs Service?

Pod = 1+ containers that share IP and lifecycle, ephemeral. Deployment = declarative controller that owns ReplicaSet to keep N Pods at a version via rolling updates. Service = stable ClusterIP/DNS (kube-proxy IPVS) that load-balances to Pods matching its selector as Pod IPs churn.

Do I need Kubernetes for a small project?

Likely not. One API on one VM is simpler with Docker/Compose or Cloud Run/ECS. Choose K8s when you have 3+ services needing HA, autoscaling, zero-downtime deploys, or GitOps audit — otherwise its control plane overhead isn't justified.

How do rolling updates work without downtime?

Deployment creates a new ReplicaSet for the new image and scales it up in increments (maxSurge), while scaling the old down in increments (maxUnavailable), only removing an old Pod after a new Pod passes readinessProbe and joins Service endpoints. PDB minAvailable:2 prevents voluntary drains from taking you below 2.

Why is my Pod CrashLoopBackOff or ImagePullBackOff?

CrashLoopBackOff → app exits; check kubectl logs --previous and describe for probe failure or missing env. ImagePullBackOff → wrong image:tag or private registry without imagePullSecrets on the ServiceAccount — describe shows Failed to pull image.

How do I see logs effectively?

kubectl logs deploy/myapp -c app --tail=100 -f --timestamps and --previous for last crash. For structured apps, pipe JSON logs through a formatter to filter by level:error and highlight fields — raw blobs hide errors among info lines.