kubectl explain kubernetes --recursive

The Kubernetes handbook

Start at zero and end able to debug a real cluster. Eighteen objects, each with a diagram, a working manifest, what happens when you apply it, and the mistake everyone makes first — plus a kubectl reference and a troubleshooting table organised by the symptom you are staring at.

No prior Kubernetes knowledge assumed — but you should be comfortable with containers. Manifests come from the example-voting-app — vote, result, worker, redis, db — the app is just familiar scaffolding; the point is always the Kubernetes object. Never used a cluster? kind create cluster or minikube start gives you one in two minutes, and everything here works on it.
client Service vote:8080 Node Pod vote Pod vote Pod redis Pod worker queues vote
Part 1 · The idea

Start here

Three short sections. If you only read one thing on this page, read the first one — everything else in Kubernetes is a variation on it.

New to this?Read Part 1 and 2 in order. Skip Part 3 until something in Part 2 stops surprising you.
Been here before?Use the filter box above, or jump to the kubectl and troubleshooting tables in Part 4.
Stuck right now?Go straight to Troubleshooting — it's organised by the symptom you're staring at.

NAMEDESIREDACTUALACTION

01 What Kubernetes actually does

One idea, repeated everywhere: you describe what you want, and a loop keeps reality matching it.

You never tell Kubernetes to do things. You tell it what should be true — "there should be three copies of this container running" — and write that down in a YAML file. Kubernetes stores that as the desired state. Separately, controllers watch the actual state of the cluster and, whenever the two differ, take a small step to close the gap. Forever.

In plain English

It's a thermostat. You don't turn the heating on and off — you set 21°C and walk away. The thermostat reads the room, compares it to your number, and acts. If someone opens a window, it reacts on its own, because it never stopped checking.

"Kubernetes self-heals" is just this: a controller noticed a difference and fixed it. There's no intelligence in it, and that's the good news — it's predictable.

1 · You declare kubectl apply -f 2 · API server writes it to etcd 3 · Controller desired vs actual? 4 · kubelet acts starts/stops containers reality is reported back — the loop never stops desired state actual state

Every object in this handbook plugs into step 3. A Deployment controller, a ReplicaSet controller, a Job controller — same loop, different definition of "correct".

Why this matters on day one

Two practical consequences that trip up nearly everyone:

Because it's a loop…What you'll notice
changes aren't instantkubectl apply returns immediately, but the cluster may take seconds or minutes to converge. "It didn't work" often means "it hasn't finished yet". Watch with -w.
manual changes get undoneDelete a Pod that a Deployment owns and it comes straight back. The loop doesn't care that you meant it. To actually remove it, change the desired state.
errors are events, not exceptionsNothing throws. A Pod that can't be scheduled just sits there, and the reason is in kubectl describe under Events. That's where you look, every time.
the same file is safe to re-applyApplying an unchanged manifest is a no-op. This is why Kubernetes config lives in git and gets applied on every deploy.
Gotcha

Desired state is stored in etcd and is the single source of truth for the cluster — but it is not the source of truth for you. If you fix something with kubectl edit and don't update the YAML in your repo, the next deploy silently reverts it. Treat the cluster as a cache of your git repo.

apiVersionkindmetadataspec

02 Anatomy of a manifest

Every YAML file you'll ever write has the same four keys. Learn them once.

Kubernetes objects look wildly different until you notice they're all the same envelope with different contents. Whether it's a Pod, a Service, or something a vendor invented last week, the top level is identical.

the shape of every manifestannotated
apiVersion: apps/v1        # which API group + version owns this kind
kind: Deployment          # what type of object this is
metadata:                 # who it is: name, namespace, labels, annotations
  name: vote
  labels:
    app: vote
spec:                     # WHAT YOU WANT — the desired state. You write this.
  replicas: 3
  ...
# status:                # WHAT IS. The cluster writes this. Never put it in your file.
KeyWhat it meansHow to find the right value
apiVersionWhich slice of the API this object belongs to. v1 is the original core group (Pod, Service, ConfigMap, Secret, PVC). Anything else is grouped: apps/v1, batch/v1, networking.k8s.io/v1.kubectl explain deployment prints it. Copying an old blog post is the #1 source of "no matches for kind".
kindThe object type, capitalised exactly: Deployment, not deployment.kubectl api-resources lists every kind your cluster knows, with its apiVersion and short name.
metadataIdentity. name is required and unique per namespace per kind. labels are how other objects find this one. annotations are free-form notes for humans and tools.Name must be lowercase, alphanumeric and dashes — no underscores, no capitals.
specYour desired state. Everything specific to the kind lives here.kubectl explain deployment.spec --recursive is the offline documentation, and it always matches your cluster's version.
statusRead-only, filled in by the cluster. Shows what's actually true.Never write it. Read it with kubectl get x -o yaml.
The one command worth memorising now

kubectl explain is a full API reference built into the tool you already have. It works offline, and it describes your cluster's version rather than whatever version the internet was on.

kubectl explain pod.spec.containers.resources
kubectl explain ingress.spec.rules --recursive

Three YAML rules that cause most beginner errors

RuleWhy it bites
indent with spacesA tab anywhere is a hard parse error. Most editors insert tabs by default in unknown file types.
indentation is meaningports under the wrong level attaches to the wrong object and is silently ignored, rather than failing.
quote ambiguous scalarsYAML reads yes, no, on, off as booleans and 8080 as a number. If a field wants a string, quote it: "8080".
Check it before you apply it
$ kubectl apply -f vote.yaml --dry-run=server    ← validates against the real API, changes nothing
deployment.apps/vote configured (server dry run)

$ kubectl diff -f vote.yaml                     ← shows exactly what would change
OBJECTOWNSFINDS BYPART

03 The object map

How the pieces in Part 2 fit together, before you meet them one at a time.

There are only two kinds of relationship in this picture, and telling them apart makes the rest of Kubernetes readable. Solid lines are ownership: this object created that one and will delete it. Dashed lines are selection: this object found that one by matching labels, and neither owns the other.

Ingress HTTP routing Service stable address sends /path to Deployment what you write ReplicaSet keeps count = 2 creates Pod app=vote Pod app=vote creates selects app=vote ConfigMap Secret PersistentVolumeClaim mounted into by name solid = owns and will delete · dashed = found by label or name, no ownership

Delete the Deployment and the ReplicaSet and Pods go with it. Delete the Service and the Pods carry on, unreachable — nothing owned them.

Why this shape

Each layer does one job. Pod = run containers. ReplicaSet = keep N of them alive. Deployment = swap one ReplicaSet for another safely, which is what a release is. Stacking them is why you get rolling updates and instant rollback without writing any of that logic yourself.

You will write Deployments and Services almost every day, ReplicaSets essentially never, and bare Pods only to debug.

Part 2 · Core objects

The eight you'll actually use

In dependency order — each one builds on the last. Every section has a diagram, a real manifest, what happens when you apply it, and the mistake people make first.


NAMEKINDROLENO.

04 Nodes

The machines your Pods actually run on.

A Node is a worker machine — VM or bare metal — registered with the cluster's control plane. Every Node runs three things you rely on constantly: the kubelet (an agent that talks to the API server and starts/stops containers), kube-proxy (programs the networking rules that make Services work), and a container runtime (containerd, CRI-O). The control plane itself — API server, scheduler, controller-manager, etcd — usually isn't a "Node" you deploy workloads onto; it's the brain deciding where your Pods go.

Cluster Control plane API server scheduler controller-mgr etcd Node A kubelet · kube-proxy vote worker redis Node B kubelet · kube-proxy result db

The control plane decides placement; Nodes are just where Pods physically live.

What you'd actually run

$ kubectl get nodes -o wideterminal
NAME       STATUS   ROLES           AGE   VERSION   INTERNAL-IP
node-a     Ready    <none>          14d   v1.30.2   10.128.0.4
node-b     Ready    <none>          14d   v1.30.2   10.128.0.5
Behavior

If node-b's kubelet stops sending heartbeats (crash, network partition), the control plane marks it NotReady. After a grace period (5 minutes by default), Pods on it are evicted and rescheduled onto node-a — assuming node-a has capacity. This is the actual mechanism behind "Kubernetes self-heals," and it's a controller reacting to state, not magic.

Gotcha

Pod scheduling is bound by a Node's allocatable capacity, not its raw capacity (some CPU/memory is reserved for the OS and kubelet itself). A Pod requesting more than any single Node can allocate stays Pending forever with an "Insufficient cpu" event — it will never be split across Nodes.

NAMEREADYSTATUSRESTARTS

05 Pods

The smallest thing you can deploy — and the thing you almost never create directly.

A Pod wraps one or more containers that share a network namespace (same IP, containers reach each other over localhost) and can share storage volumes. Pods are disposable: when one dies, Kubernetes doesn't repair it — it throws it away and, if something is managing it, creates a brand-new Pod with a new IP. Nothing about a Pod's identity is meant to be stable.

Pod · 10.244.1.7 worker optional sidecar (e.g. log shipper) shared network ns optional shared volume → redis, db

Containers inside one Pod talk over localhost; the Pod as a whole talks out to other Pods via Services.

A bare Pod spec (don't do this in practice — see below)

worker-pod.yamlmanifest
apiVersion: v1
kind: Pod
metadata:
  name: worker
  labels:
    app: worker
spec:
  containers:
    - name: worker
      image: dockersamples/examplevotingapp_worker
      env:
        - name: REDIS_HOST
          value: "redis"
        - name: DB_HOST
          value: "db"
Behavior — bare Pod vs. managed Pod
$ kubectl delete pod worker           (created directly, no controller)
pod "worker" deleted
$ kubectl get pods
No resources found                    ← gone. nothing brings it back.

$ kubectl delete pod vote-7d9f8b6c4-x2z1p   (owned by a ReplicaSet)
pod "vote-7d9f8b6c4-x2z1p" deleted
$ kubectl get pods
vote-7d9f8b6c4-m4q7k   1/1   Running   0   4s   ← replaced automatically
Gotcha

CrashLoopBackOff means the container keeps starting and exiting; Kubernetes backs off exponentially between restarts (10s, 20s, 40s…), so a broken app doesn't hammer the node. Separately: a failing liveness probe kills and restarts the container; a failing readiness probe just pulls the Pod out of Service traffic without touching it — mixing these up is one of the most common early debugging traps.

NAMELABELSSELECTORMATCHES

06 Labels & selectors

The glue. Almost nothing in Kubernetes connects to anything by name — it connects by label.

A label is a key/value pair you stick on an object. A selector is a query over labels. That's the entire mechanism, and it's how a Service finds its Pods, how a ReplicaSet knows which Pods are "its", and how you filter anything on the command line. Once this clicks, the next four sections stop being mysterious.

In plain English

Labels are sticky notes. A Service doesn't hold a list of Pod names — Pods come and go too fast for that to work. It says "send traffic to anything wearing an app=vote sticker", and the moment a new Pod appears wearing one, it starts getting traffic.

Service: vote selector: app=vote Pod vote-a app=vote, tier=frontend match Pod vote-b app=vote, tier=frontend match Pod result-a app=result ignored Pod vote-c app=Vote ← capital V missed

Matching is exact and case-sensitive. There is no warning when a selector matches nothing — you just get a Service that silently routes to no one.

labels in a manifestmanifest
metadata:
  labels:                 # what this object IS
    app: vote
    tier: frontend
    env: prod
  annotations:            # notes for humans and tools — never used for selection
    owner: "team-payments"
    kubernetes.io/change-cause: "bump to v2"
---
# A selector is the query side. Two forms:
selector:
  matchLabels:            # simple equality — 95% of what you write
    app: vote
  matchExpressions:       # set-based, for the other 5%
    - {key: env, operator: In, values: [prod, staging]}
    - {key: tier, operator: Exists}
Behavior — selectors on the command line
$ kubectl get pods -l app=vote                 ← everything wearing the sticker
$ kubectl get pods -l app=vote,tier=frontend   ← comma means AND, never OR
$ kubectl get pods -l 'env in (prod,staging)'  ← set-based
$ kubectl get pods -l '!canary'                ← label absent
$ kubectl get pods --show-labels               ← what is actually on them

$ kubectl delete pods -l app=vote              ← same query, destructive. check with get first.
Gotcha

A selector that matches nothing is not an error. A Service with a typo'd selector is created happily, reports a healthy-looking ClusterIP, and drops every request. The single fastest check when "the Service isn't working" is kubectl get endpoints <name> — if that's empty, your labels don't match, and no amount of networking debugging will help.

Convention worth adopting early

Kubernetes suggests a standard set so that tooling can understand your objects: app.kubernetes.io/name, /instance, /version, /component, /part-of, /managed-by. You don't need all of them on day one — but pick a scheme before you have fifty services, because relabelling a live ReplicaSet's selector is not allowed.

NAMEDESIREDCURRENTREADY

07 ReplicaSets

The controller that keeps "N copies of this Pod" true, always.

A ReplicaSet's job is narrow: watch for Pods matching its label selector, and reconcile reality toward a desired count. Too few → create more. Too many (someone manually made an extra) → delete one. It runs this reconcile loop continuously, not just once. You'll rarely author a ReplicaSet by hand — Deployments create and own them — but understanding this layer is what makes Deployment rollouts make sense.

ReplicaSet desired: 3 Pod vote-a Pod vote-b Pod vote-c reconcile loop
redis-rs.yamlmanifest
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: redis
spec:
  replicas: 2
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:alpine
Behavior
$ kubectl delete pod redis-8f6d9c7-k2n1x
pod "redis-8f6d9c7-k2n1x" deleted
$ kubectl get pods -l app=redis --watch
redis-8f6d9c7-k2n1x   1/1   Terminating
redis-8f6d9c7-p9v3q   0/1   ContainerCreating   ← RS noticed within its next tick
redis-8f6d9c7-p9v3q   1/1   Running
Gotcha

selector.matchLabels is effectively immutable once Pods are running under it. If you hand-edit a Pod's labels so it no longer matches, the ReplicaSet doesn't "notice a lost Pod and fix it" — it just sees it's short one Pod and creates a new one. Your relabeled Pod keeps running, orphaned, invisible to kubectl get pods -l app=redis.

NAMEREADYUP-TO-DATEAVAILABLE

08 Deployments

What you actually write. Manages ReplicaSets so you get rollouts and rollbacks for free.

A Deployment is a ReplicaSet manager. Change the Pod template — a new image tag, a new env var — and the Deployment doesn't edit Pods in place; it creates a new ReplicaSet with the new template and shifts replica count from the old ReplicaSet to the new one, gradually, per its strategy. This is literally the mechanism behind a "rolling update." The old ReplicaSet isn't deleted — it's scaled to zero and kept around, which is what makes instant rollback possible.

Deployment RS · v1 (old) 3 → 0 replicas RS · v2 (new) 0 → 3 replicas Service (unchanged)

The Service's selector doesn't change — it just starts finding new-version Pods as the old ReplicaSet scales down.

vote-deployment.yamlmanifest
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vote
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: vote
  template:
    metadata:
      labels:
        app: vote
    spec:
      containers:
        - name: vote
          image: dockersamples/examplevotingapp_vote:v1
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet: { path: /, port: 80 }
            initialDelaySeconds: 3
Behavior
$ kubectl set image deployment/vote vote=dockersamples/examplevotingapp_vote:v2
$ kubectl rollout status deployment/vote
Waiting for deployment "vote" rollout to finish: 1 old replicas pending termination...
Waiting for deployment "vote" rollout to finish: 2 of 3 updated replicas available...
deployment "vote" successfully rolled out

$ kubectl rollout undo deployment/vote     ← instant, because old RS never went away
Gotcha

Default maxUnavailable: 25% rounds oddly at low replica counts — with replicas: 1, a rolling update can still take your only Pod down unless you explicitly set maxSurge to bring up the new one first. And without a readinessProbe, a Pod is considered ready the instant its container starts — the Service can send it real traffic before your app has finished booting.

NAMETYPECLUSTER-IPPORT(S)

09 Services

A stable address in front of Pods that keep coming and going.

Pods are disposable and their IPs change constantly; a Service gives you one thing that doesn't change — a virtual IP and a DNS name — and load-balances traffic across whichever Pods currently match its label selector and are passing their readiness probe. Under the hood, an Endpoints object tracks that live Pod list, and kube-proxy programs the actual routing rules on every Node.

ClusterIP internal only used by: redis, db NodePort every Node, one fixed port used by: vote :31000 result :31001 LoadBalancer cloud LB, public IP used by: vote (in prod)

Same selector-based routing underneath all three — they differ only in where the entry point is exposed.

vote-service.yamlmanifest
apiVersion: v1
kind: Service
metadata:
  name: vote
spec:
  type: NodePort
  selector:
    app: vote
  ports:
    - port: 80
      targetPort: 80
      nodePort: 31000
---
# redis needs no type — ClusterIP is the default,
# and it's all a same-cluster worker needs to reach it
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  selector:
    app: redis
  ports:
    - port: 6379
Behavior
$ kubectl get endpoints vote
NAME   ENDPOINTS
vote   10.244.1.7:80,10.244.2.9:80,10.244.2.11:80

$ kubectl delete pod vote-7d9f8b6c4-x2z1p
$ kubectl get endpoints vote
vote   10.244.2.9:80,10.244.2.11:80   ← 3rd IP dropped instantly, requests never hit it
Gotcha

A Service only routes to Pods that are Ready. A Pod stuck failing its readiness check doesn't error loudly — it just quietly disappears from Endpoints and stops getting traffic, which looks like "the app is fine but nobody's hitting it." Also: within the cluster, Pods can reach a Service by name alone (redis) inside the same namespace, or the full redis.default.svc.cluster.local from anywhere.

NAMEKINDDATAAGE

10 ConfigMaps & Secrets

Getting config and credentials out of your image.

Same API shape, different intent: a ConfigMap holds non-sensitive key/value config (which two options the vote app shows), a Secret holds sensitive values (the Postgres password). Both can be injected into a Pod two ways — as environment variables, fixed at container start, or mounted as files, which can update live. That distinction matters more than it sounds like.

Secret · db-creds ConfigMap · vote-opts Pod env: DB_PASSWORD volume: /config/options
db-secret.yaml + db-deployment.yaml (excerpt)manifest
apiVersion: v1
kind: Secret
metadata:
  name: db-creds
stringData:                    # kubectl encodes this to base64 for you
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: postgres
---
# inside the db container spec:
env:
  - name: POSTGRES_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-creds
        key: POSTGRES_PASSWORD
Behavior

Update a ConfigMap that's mounted as a volume and the file inside the Pod updates within about a minute — no restart. Update one that's injected as an env var and nothing happens until the Pod is recreated, because env vars are copied in once at container start and never re-read.

Gotcha

Secret values are base64-encoded, not encrypted. Anyone who can kubectl get secret db-creds -o yaml in that namespace can read the password in one command. Real secret hygiene needs etcd encryption at rest plus RBAC on who can read Secrets — or an external system like Vault or Sealed Secrets.

NAMESTATUSCAPACITYACCESS MODE

11 Storage — PersistentVolume & PersistentVolumeClaim

Making sure votes survive a Pod restart.

A container's own filesystem dies with the container. For db, that would mean every vote vanishes on a routine restart. A PersistentVolumeClaim (PVC) is a request — "5Gi, mountable by one Node at a time" — and a PersistentVolume (PV) is the actual disk that satisfies it, usually provisioned automatically by a StorageClass rather than hand-written. The Pod just mounts the claim; it doesn't care what's backing it.

PVC · 5Gi PV (bound) cloud disk binds backed by mounted by db Pod
db-pvc.yaml + volume mount (excerpt)manifest
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 5Gi
---
# inside the db Pod spec:
volumes:
  - name: pgdata
    persistentVolumeClaim:
      claimName: db-data
containers:
  - name: db
    volumeMounts:
      - mountPath: /var/lib/postgresql/data
        name: pgdata
Behavior

Delete the db Pod. A new one is scheduled, reattaches the same PVC, and every prior vote is still there. Without the PVC, the new Pod would start with a completely empty Postgres data directory — this one line of config is the entire difference between "durable" and "resets on restart."

Gotcha

ReadWriteOnce means one Node at a time, not one Pod — but in practice it means if the scheduler tries to place the rescheduled Pod on a different Node than where the cloud disk is currently attached, it can hang in ContainerCreating waiting for detach/reattach. It also means you can't just bump replicas on a Deployment to scale this database — multiple Pods can't share one RWO volume as independent writers.

NAMESTATUSAGESCOPE

12 Namespaces

Folders for your cluster — and the reason your kubectl get pods looks empty.

A namespace is a naming scope. Two teams can each have a Service called api as long as they're in different namespaces. It's also the boundary that RBAC permissions and resource quotas attach to, which is what makes it more than cosmetic organisation.

In plain English

It's a folder. Same filename can exist in two folders. kubectl only looks in one folder at a time — the default one unless told otherwise — which is why your Pods "disappear" the moment you deploy them somewhere else.

working with namespacesterminal
$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   14d       # where you land if you never say otherwise
kube-system       Active   14d       # the cluster's own components. don't deploy here.
kube-public       Active   14d
kube-node-lease   Active   14d       # node heartbeats live here

$ kubectl get pods -n voting         # one namespace
$ kubectl get pods -A                # all of them — the "where did it go" command

$ kubectl config set-context --current --namespace=voting
                                     # stop typing -n on every command

What is and isn't namespaced

NamespacedCluster-wide
Pod, Deployment, ReplicaSet, Service, Ingress, ConfigMap, Secret, PVC, Job, CronJob, ServiceAccount, Role, RoleBindingNode, PersistentVolume, StorageClass, Namespace itself, ClusterRole, ClusterRoleBinding, CustomResourceDefinition

Unsure? kubectl api-resources --namespaced=false lists the cluster-wide ones on your cluster.

Talking across namespaces

DNS is how you cross the boundary. Inside one namespace, the short name works. From another, use the longer form:

service DNS resolutionreference
redis                              # same namespace — what your app config usually holds
redis.voting                       # from another namespace
redis.voting.svc                   # fully explicit-ish
redis.voting.svc.cluster.local     # the real, complete name
Behavior

Deleting a namespace deletes everything inside it — every Deployment, Service, Secret and PVC — in one command, with no extra confirmation. It's the fastest way to clean up an experiment, and the fastest way to ruin a Tuesday. There is no undo.

Gotcha

Namespaces are not a security boundary on their own. By default, a Pod in namespace a can open a network connection to a Pod in namespace b — the flat Pod network doesn't care about namespaces. Isolation needs a NetworkPolicy (and a CNI plugin that enforces one). Namespaces limit names, permissions and quota, not traffic.

Gotcha

A namespace stuck in Terminating forever is almost always a finalizer on some object inside it waiting for a controller that no longer exists. kubectl get namespace x -o yaml and look at spec.finalizers — that's the thread to pull, not a bug in Kubernetes.

Part 3 · Beyond the basics

The next six you'll meet

You can ship a real application with Part 2 alone. These are what you reach for when the answer to "how do I do X" stops being a Deployment.


NAMECLASSHOSTSPORTS

13 Ingress

One entry point, many Services — routed by hostname and URL path.

A LoadBalancer Service gives you a public IP per Service, which gets expensive and unmanageable fast. An Ingress is a single front door that reads the HTTP request and decides which Service should handle it. It works at layer 7, so it understands hostnames, paths, and TLS in a way a Service never can.

The part everyone misses first

An Ingress object is just a rule sheet. On its own it does nothing at all. You also need an Ingress Controller — ingress-nginx, Traefik, HAProxy, or your cloud's — actually running in the cluster to read those rules and route traffic. Applying an Ingress to a cluster with no controller produces a happy-looking object with no ADDRESS and no effect whatsoever.

internet Ingress Controller one LoadBalancer IP terminates TLS Service: vote ClusterIP Service: result ClusterIP Service: api ClusterIP vote.example.com result.example.com /api/*

Three public hostnames, one cloud load balancer, three ordinary ClusterIP Services behind it.

voting-ingress.yamlmanifest
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: voting
spec:
  ingressClassName: nginx      # WHICH controller handles this. omit it and nothing may.
  tls:
    - hosts: [vote.example.com]
      secretName: vote-tls      # a Secret of type kubernetes.io/tls
  rules:
    - host: vote.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: vote      # a normal ClusterIP Service
                port: { number: 80 }
    - host: result.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: result
                port: { number: 80 }
Gotcha

An Ingress can only route to Services in its own namespace. And ADDRESS staying empty in kubectl get ingress for more than a minute means no controller claimed it — usually a missing or misspelled ingressClassName. Check with kubectl get ingressclass.

NAMEREADYAGEIDENTITY

14 StatefulSets

For Pods that need a name and a disk that follow them around.

A Deployment treats its Pods as interchangeable — random name suffixes, any order, all sharing whatever volume you gave them. That's exactly wrong for databases, queues and anything that clusters. A StatefulSet gives each Pod a stable ordinal identity (db-0, db-1), its own PersistentVolumeClaim created from a template, and ordered startup, update and shutdown.

DeploymentStatefulSet
Pod namesvote-7d9f8b-x2z1p (random)db-0, db-1 (stable, predictable)
Storageall replicas share whatever PVC you nameone PVC per Pod, created automatically
Start orderall at once0, then 1, then 2 — each waits for Ready
Rolling updateany orderhighest ordinal first, downward
DNSService name onlydb-0.db.default.svc.cluster.local per Pod
Use forstateless web apps, workers, APIsPostgres, Kafka, Redis clusters, anything with peers
db-statefulset.yaml (excerpt)manifest
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db            # must be a HEADLESS service (clusterIP: None)
  replicas: 3
  selector:
    matchLabels: { app: db }
  template:
    metadata:
      labels: { app: db }
    spec:
      containers:
        - name: postgres
          image: postgres:16
          volumeMounts:
            - { name: data, mountPath: /var/lib/postgresql/data }
  volumeClaimTemplates:      # each Pod gets its OWN 5Gi disk from this
    - metadata: { name: data }
      spec:
        accessModes: ["ReadWriteOnce"]
        resources: { requests: { storage: 5Gi } }
Gotcha

Deleting a StatefulSet does not delete its PVCs — on purpose, so you don't lose a database by typing one command. Scaling down doesn't either. Those disks keep costing money until you delete them by hand: kubectl delete pvc -l app=db. Also, a StatefulSet gives you stable identity, not replication — Postgres still has to be configured to replicate. Kubernetes runs the Pods; it does not understand your database.

NAMEDESIREDREADYNODE SELECTOR

15 DaemonSets

Exactly one copy on every Node — including Nodes that don't exist yet.

Some things are per-machine rather than per-application: shipping logs, exporting metrics, running a CNI agent. A DaemonSet has no replicas field because the count isn't yours to choose — it's however many Nodes there are. Add a Node to the cluster and a Pod appears on it automatically; drain the Node and it goes away.

log-agent-daemonset.yaml (excerpt)manifest
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
spec:
  selector:
    matchLabels: { app: log-agent }
  template:
    metadata:
      labels: { app: log-agent }
    spec:
      tolerations:                    # run on control-plane nodes too
        - { key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule }
      containers:
        - name: agent
          image: fluent/fluent-bit:3.0
          volumeMounts:
            - { name: varlog, mountPath: /var/log, readOnly: true }
      volumes:
        - name: varlog
          hostPath: { path: /var/log }   # reads the Node's own filesystem
Gotcha

DaemonSet Pods skip most scheduling rules but not taints. Control-plane Nodes are tainted by default, so a monitoring agent silently misses them unless you add the toleration above — and that's exactly the Node you most want metrics from.

NAMECOMPLETIONSDURATIONAGE

16 Jobs & CronJobs

For work that finishes — migrations, batch imports, nightly reports.

Every controller so far assumes the Pod should run forever, and restarts it when it exits. A Job inverts that: it runs a Pod until it exits successfully, then stops. A CronJob creates Jobs on a schedule. Use these for anything with an end, and never try to model a migration as a Deployment — it'll loop forever in CrashLoopBackOff for succeeding.

migrate-job.yaml + nightly-cronjob.yamlmanifest
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  backoffLimit: 4              # retries before the Job is marked Failed
  ttlSecondsAfterFinished: 3600 # auto-delete an hour after finishing
  template:
    spec:
      restartPolicy: OnFailure   # Never or OnFailure — "Always" is rejected here
      containers:
        - name: migrate
          image: myapp/migrate:v2
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"        # standard cron, in the CLUSTER's timezone (UTC by default)
  concurrencyPolicy: Forbid    # skip a run if the previous one is still going
  successfulJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - { name: report, image: myapp/report:v2 }
Behavior
$ kubectl get jobs
NAME         COMPLETIONS   DURATION   AGE
db-migrate   1/1           12s        3m      ← done. Pod stays around so you can read logs.

$ kubectl logs job/db-migrate                 ← works even after it finished
Gotcha

Without ttlSecondsAfterFinished, completed Jobs and their Pods pile up forever — a CronJob running every five minutes will bury your kubectl get pods within a day. And a CronJob guarantees at least once, not exactly once: a missed schedule can fire late, and a Job can create a replacement Pod. Your batch job must be safe to run twice.

REQUESTSLIMITSPROBESHPA

17 Resources, probes & autoscaling

The three settings that decide whether your app survives a busy Tuesday.

Requests vs limits

A request is what the scheduler reserves for you — it's how a Node is chosen, and it's guaranteed. A limit is the ceiling the runtime enforces. They do completely different jobs and are constantly confused.

Over the limit: CPUOver the limit: memory
what happensthrottled — the app just gets slowerOOMKilled — the container is killed instantly and restarted
you'll seelatency spikes, no events, nothing in logsReason: OOMKilled in kubectl describe pod, restart count climbing
adviceset requests honestly; be generous or omit CPU limitsalways set a memory limit, and make it real
a production-shaped container specmanifest
containers:
  - name: vote
    image: dockersamples/examplevotingapp_vote:v2
    resources:
      requests: { cpu: "100m", memory: "128Mi" }   # 100m = 0.1 of a core
      limits:   { memory: "256Mi" }

    startupProbe:        # "has it booted yet?" — the other two wait for this
      httpGet: { path: /healthz, port: 80 }
      failureThreshold: 30
      periodSeconds: 2    # allows up to 60s to start, without a slow liveness probe

    readinessProbe:      # "should it get traffic?" — failing removes it from the Service
      httpGet: { path: /ready, port: 80 }
      periodSeconds: 5

    livenessProbe:       # "is it wedged?" — failing RESTARTS the container
      httpGet: { path: /healthz, port: 80 }
      periodSeconds: 10
      failureThreshold: 3
Which probe is which

startup — "still booting, don't judge me yet." readiness — "don't send me traffic right now", non-destructive, reversible. liveness — "kill me, I'm stuck", destructive.

The classic self-inflicted outage: pointing livenessProbe at an endpoint that touches the database. The database gets slow, every Pod fails liveness, every Pod restarts at once, and you turn a slow dependency into a full outage. Liveness should test the process, readiness can test dependencies.

Horizontal Pod Autoscaler

vote-hpa.yamlmanifest
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vote
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: vote }
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }
Gotcha

HPA computes utilisation as a percentage of the CPU request. No request means no percentage, so the HPA reports <unknown> and never scales. It also needs metrics-server installed — it isn't in a stock cluster. And if you set replicas in a Deployment that an HPA also manages, the two fight: leave replicas out of the manifest once an HPA owns it.

NAMEKINDVERBSSCOPE

18 RBAC & ServiceAccounts

Who is allowed to do what — to the cluster, and from inside it.

Two questions, four objects. Who is a User (you, from your kubeconfig) or a ServiceAccount (a Pod). What is a Role — a list of verbs on resources, scoped to one namespace — or a ClusterRole, the same thing cluster-wide. A RoleBinding connects the two. That's the whole model.

read-only access for a Podmanifest
apiVersion: v1
kind: ServiceAccount
metadata: { name: vote-reader }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: pod-reader }
rules:
  - apiGroups: [""]              # "" is the core group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: vote-reader-can-read-pods }
subjects:
  - { kind: ServiceAccount, name: vote-reader }
roleRef:
  { kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }
---
# then, in the Pod spec:
spec:
  serviceAccountName: vote-reader
  automountServiceAccountToken: true
The command that answers every RBAC question
$ kubectl auth can-i delete pods
no

$ kubectl auth can-i get pods --as=system:serviceaccount:default:vote-reader
yes                                ← test someone else's permissions without being them

$ kubectl auth can-i --list         ← everything you're allowed to do here
Gotcha

RBAC is additive only — there is no "deny" rule. Permissions are the union of every binding that applies to you, so you cannot subtract one by adding another; you have to remove the binding that granted it. Also, every Pod gets the namespace's default ServiceAccount unless told otherwise, and its token is mounted into the container at /var/run/secrets/kubernetes.io/serviceaccount. Set automountServiceAccountToken: false on Pods that never call the API — which is most of them.

Part 4 · Reference

Look things up

The part you come back to. Commands, symptoms, and vocabulary.


COMMANDWHAT IT DOESWHEN·

kubectl, the useful half

There are hundreds of flags. These are the ones you'll use every day.

Looking around

CommandWhat it gives you
kubectl get podsThe list. Add -o wide for Node and Pod IP, -w to watch it change live.
kubectl get allDeployments, ReplicaSets, Pods and Services in one view. Lies slightly — it misses ConfigMaps, Secrets, PVCs and Ingresses.
kubectl get pods -AEvery namespace. The "where did my thing go" command.
kubectl describe pod XThe single most useful command in Kubernetes. Config, state, and the Events list at the bottom — which is where the actual reason for almost every failure is written.
kubectl get events --sort-by=.lastTimestampCluster-wide recent history. Add -w during a deploy to watch decisions happen.
kubectl get X -o yamlThe full object including the status the cluster wrote. How you find out what really got applied.
kubectl explain X.specOffline field documentation matching your cluster version. Add --recursive for the whole tree.
kubectl api-resourcesEvery kind this cluster knows, with apiVersion, short name, and whether it's namespaced.

Debugging a running thing

CommandWhat it gives you
kubectl logs Xstdout of the container. -f to follow, --tail=50 to trim.
kubectl logs X --previousLogs of the crashed container, not the running one. Essential for CrashLoopBackOff — the current container usually has nothing in it yet.
kubectl logs -l app=vote --all-containersLogs from every Pod matching a label at once.
kubectl exec -it X -- shA shell inside the container. -- separates kubectl's flags from the command.
kubectl debug -it X --image=busybox --target=appAttaches a debug container to a running Pod. Works when the image has no shell.
kubectl port-forward svc/vote 8080:80Reach a ClusterIP Service from your laptop. Fastest way to test without exposing anything.
kubectl run tmp --rm -it --image=busybox -- shA throwaway Pod inside the cluster network — for testing DNS and reachability from the inside.
kubectl top podLive CPU and memory per Pod. Needs metrics-server.

Changing things

CommandWhat it gives you
kubectl apply -f dir/Apply every manifest in a directory. Idempotent — safe to re-run.
kubectl diff -f x.yamlWhat would change. Run this before every apply you're unsure about.
kubectl apply -f x.yaml --dry-run=serverFull server-side validation, zero changes.
kubectl scale deploy/vote --replicas=5Quick scale. Remember it's not in your git repo — the next apply reverts it.
kubectl set image deploy/vote vote=img:v2Trigger a rolling update without editing YAML.
kubectl rollout status deploy/voteBlocks until the rollout finishes or fails. Use it in CI.
kubectl rollout undo deploy/voteBack to the previous ReplicaSet. Near-instant.
kubectl rollout restart deploy/voteRecreate every Pod with the same spec — how you pick up a changed ConfigMap or Secret.
kubectl delete -f x.yamlDelete exactly what that file created. Safer than deleting by name.
Two aliases worth setting today
alias k=kubectl
source <(kubectl completion bash)   # or zsh — tab-completes Pod names, saves hours
complete -o default -F __start_kubectl k
SYMPTOMMEANINGNEXT COMMAND·

Troubleshooting by symptom

Find the status you're staring at. Nearly all of these end at the same place: the Events section of kubectl describe pod.

The loop, every time

1. kubectl get pods — what's the status?  2. kubectl describe pod X — read Events at the bottom, they're in time order.  3. kubectl logs X --previous — if the container started at all.

Events answer "why won't it start". Logs answer "why did it die". Reaching for logs on a Pod that never started is the most common wasted five minutes.

You seeIt meansDo this
PendingNo Node will take it: not enough CPU/memory, a taint you don't tolerate, or an unbound PVC.describe pod → Events says "Insufficient cpu" or "had taint". Then kubectl top nodes.
ImagePullBackOff
ErrImagePull
The image name is wrong, the tag doesn't exist, or the registry needs credentials.describe pod → Events shows the exact registry error. Check the tag really exists; add an imagePullSecret if private.
CrashLoopBackOffThe container starts, exits, and Kubernetes keeps restarting it with growing delays. This is an app problem, not a cluster one.kubectl logs X --previous. Usually a missing env var, an unreachable dependency, or a bad command.
CreateContainerConfigErrorA referenced ConfigMap or Secret — or a specific key inside one — doesn't exist.describe pod names the missing object. Check spelling and namespace.
Running, but 0/1 READYThe container is up but the readiness probe is failing, so the Service won't send it traffic.describe pod → "Readiness probe failed". Check the path, port, and whether the app needs longer to boot.
OOMKilledThe container exceeded its memory limit and was killed instantly.describe pod → Last State. Raise limits.memory or fix the leak. Restart count climbing is the tell.
Init:0/1An init container hasn't finished. Everything else waits.kubectl logs X -c <init-container-name>.
Terminating, foreverA finalizer is waiting on a controller, or the app is ignoring SIGTERM until the grace period ends.get pod X -o yaml → check metadata.finalizers. Force only as a last resort: --grace-period=0 --force.
EvictedThe Node ran out of memory or disk and the kubelet started killing Pods to survive.describe node → Conditions. Set resource requests so the scheduler stops overcommitting.
Service reachable but nothing respondsThe selector matches no Ready Pods.kubectl get endpoints <svc>. Empty = label mismatch or all Pods unready. Fix that, not the network.
Ingress ADDRESS is emptyNo Ingress Controller claimed it.kubectl get ingressclass, then check spec.ingressClassName matches one.
Node NotReadyThe kubelet stopped reporting: crash, disk pressure, or network partition.describe node → Conditions. After ~5 minutes its Pods get rescheduled elsewhere.
ConfigMap changed, app didn'tEnv vars are copied once at container start and never re-read.kubectl rollout restart deploy/X. Mount as a volume instead if you need live updates.
"no matches for kind"Wrong apiVersion for your cluster, or a CRD that isn't installed.kubectl api-resources | grep -i <kind>.

Recap: which objects a real app needs

Not a walkthrough — just where each piece above lands, service by service.

ServiceObjectsWhy
redisDeployment + ClusterIP ServiceInternal-only queue, no persistence needed
dbDeployment + ClusterIP Service + Secret + PVCNeeds durable storage and credentials out of the image
workerDeployment onlyNothing calls it — no Service needed
voteDeployment + NodePort/LoadBalancer ServiceNeeds to be reachable from outside the cluster
resultDeployment + NodePort/LoadBalancer ServiceSame, on a different exposed port
TERMPLAIN ENGLISH··

Glossary

Words this page and every Kubernetes doc assume you already know.

Cluster
The whole thing: a control plane plus the Nodes it manages.
Control plane
The brain — API server, scheduler, controller-manager, etcd. Decides what should happen; doesn't run your app.
Node
A machine that runs Pods. Runs a kubelet and a container runtime.
kubelet
The agent on each Node. Takes instructions from the API server and makes containers exist.
etcd
The database holding all cluster state. If you lose etcd with no backup, you lose the cluster's memory.
Controller
A loop that watches one kind of object and nudges reality toward its spec. Kubernetes is a pile of these.
Reconciliation
The comparing-and-correcting that controllers do. The core idea on this page.
Declarative
You describe the end state, not the steps. apply is declarative; create is not.
Manifest
A YAML file describing one or more objects.
Object / resource
Anything with an apiVersion and kind that the API server can store.
Label
A key/value tag used for selection. How Services find Pods.
Annotation
A key/value note for humans and tools. Never used for selection.
Selector
A query over labels.
Endpoints / EndpointSlice
The live list of Pod IPs currently behind a Service. Your first stop when a Service seems dead.
Ephemeral
Thrown away rather than repaired. Pods are ephemeral; that's a design choice, not a limitation.
Taint / toleration
A Node repels Pods (taint) unless a Pod says it can cope (toleration). How control-plane Nodes stay free of your app.
Affinity
Rules pulling Pods toward or away from Nodes or each other.
Drain / cordon
Cordon stops new Pods landing on a Node; drain also evicts what's there. What you run before maintenance.
QoS class
Guaranteed, Burstable or BestEffort, derived from your requests and limits. Decides who gets killed first under pressure.
Finalizer
A "wait, clean up first" marker that blocks deletion until a controller clears it. The usual cause of stuck Terminating.
CRD
CustomResourceDefinition — a new kind added by an add-on, treated exactly like a built-in one.
Operator
A custom controller plus a CRD, encoding how to run a specific piece of software.
Headless Service
A Service with clusterIP: None. No load balancing — DNS returns the Pod IPs directly. Required by StatefulSets.
CNI
The plugin giving every Pod an IP and making Pod-to-Pod networking work. Calico, Cilium, Flannel.
Helm
A package manager for Kubernetes — templated manifests bundled as a "chart".

Questions & answers

The things people ask next, roughly interview depth. Answers included — try to answer each one yourself before opening it.

Nodes & scheduling ↳ #nodes▶
  • How does the scheduler pick a Node?Two phases. Filtering throws out Nodes that can't work: not enough allocatable CPU/memory for the Pod's requests, taints without a matching toleration, unsatisfiable affinity, no access to the required volume. Scoring then ranks the survivors — spreading across Nodes and zones, preferring Nodes that already have the image cached. Highest score wins. Note it uses requests, never limits, and never current actual usage.
  • What happens between a Node going NotReady and its Pods running elsewhere?The kubelet stops renewing its Lease. After node-monitor-grace-period (~40s) the node controller marks the Node NotReady. That adds a node.kubernetes.io/unreachable taint, and Pods carry a default toleration for it with tolerationSeconds: 300 — so after ~5 more minutes they're evicted and their controllers create replacements. Bare Pods with no controller are simply gone.
  • How do you stop two replicas sharing a Node?podAntiAffinity with topologyKey: kubernetes.io/hostname. Use requiredDuringScheduling for a hard rule — replicas will stay Pending rather than share — or preferred for best-effort. topologySpreadConstraints is the newer, gentler way to say "spread evenly across zones".
Pods ↳ #pods▶
  • How do you force startup order between containers in one Pod?Regular containers all start together — order isn't yours to choose. Use an initContainer, which runs to completion before any app container starts, and chain several if you need steps. For a sidecar that must be up first and stay up, Kubernetes 1.29+ supports native sidecars: an init container with restartPolicy: Always.
  • Why can a Pod never move between Nodes?A Pod's identity is bound to its Node at scheduling time — spec.nodeName is effectively immutable, and its network namespace, IP and any local state live on that machine. "Moving" a Pod means deleting it and creating a different one with a new name and IP. Accepting that is what makes Services and controllers necessary, and it's why nothing should depend on a Pod IP.
  • What is terminationGracePeriodSeconds protecting?In-flight requests. On delete, the Pod is removed from Service endpoints and the container gets SIGTERM; the app should stop accepting new work and finish what it has. After the grace period (30s by default) it gets SIGKILL. An app that ignores SIGTERM drops live connections on every deploy — a very common source of "random 502s during rollout".
ReplicaSets & Deployments ↳ #replicasets▶
  • What stops two controllers racing each other?Optimistic concurrency. Every object carries a resourceVersion; a write includes the version you read, and the API server rejects it with a conflict if it changed underneath you. Controllers then re-read and retry. Ownership also helps: metadata.ownerReferences tells a ReplicaSet which Pods are its business, so two ReplicaSets don't fight over the same Pod.
  • Why is selector.matchLabels immutable?Because the selector defines which Pods the controller owns. Changing it would instantly orphan every existing Pod — the controller would see zero matches, create a full new set, and leave the old ones running and unmanaged. Rather than allow that, the API rejects the change; you create a new object instead.
  • Why does RollingUpdate break a single-writer database?It briefly runs old and new Pods at once. Two Postgres processes mounting the same RWO volume is corruption, and the ReadWriteOnce access mode will usually just block the second Pod — so the rollout hangs instead. Use strategy: Recreate for a single writer, or a StatefulSet with per-Pod volumes and ordered updates.
  • How would you do canary or blue-green with only Deployments and Services?Canary: two Deployments sharing a label the Service selects (app=vote) but differing in version. Traffic splits in proportion to replica counts — 9 stable + 1 canary is roughly 10%. Crude, but it needs nothing extra. Blue-green: two full Deployments with distinct version labels; flip by editing the Service selector, which switches all traffic atomically and back just as fast.
Services & networking ↳ #services▶
  • How does kube-proxy actually implement a ClusterIP?A ClusterIP is a virtual IP that no interface owns — it only exists as routing rules. In iptables mode kube-proxy writes DNAT rules rewriting the destination to a random backing Pod IP; rule evaluation is a linear chain, so thousands of Services get slow to update and traverse. In IPVS mode it uses the kernel's load balancer with hash-based lookup, which scales far better and offers real algorithms (round-robin, least-conn).
  • A client holds a long-lived TCP connection and you scale the Deployment. What happens?Nothing — and that's the problem. Load balancing happens at connection time, so an established connection stays pinned to its original Pod. New replicas get no traffic from existing clients, and connections to a removed Pod are cut. This is why gRPC and HTTP/2 clients load-balance badly through a plain Service: one connection, one backend. The fixes are client-side load balancing with a headless Service, or a proxy that balances per request.
  • Where's the line between a Service and an Ingress?A Service is layer 4 — IPs and ports, no idea what HTTP is, one Service per backend. An Ingress is layer 7: it reads hostnames, paths, headers and TLS, and routes across many Services from one entry point. Ingress doesn't replace Services; it sits in front of them and still forwards to a ClusterIP.
Config, Secrets & storage ↳ #config / #storage▶
  • Secrets are just base64 — what does a real pipeline look like?Three layers. At rest: enable etcd encryption (or a KMS provider) so a disk image isn't a credential dump. Access: RBAC so only the ServiceAccounts that need a Secret can read it — and remember get secret is a full read. Source of truth: keep plaintext out of git — Sealed Secrets (encrypted values, safe to commit), External Secrets Operator syncing from Vault or a cloud secret manager, or CSI-mounted secrets that never become Kubernetes objects at all. Then rotate on a schedule, because none of it helps if the value never changes.
  • Why is ReadWriteMany rare on cloud block storage?Because a block device is a raw disk, and filesystems like ext4 assume one machine owns them. Two Nodes writing the same block device corrupts it — the cloud simply won't attach one disk to two VMs read-write. RWX requires a network file system with a locking protocol: NFS, EFS, Azure Files, CephFS. Different technology, different performance, usually more money.
  • If the database needed more than one writer, what changes?You'd move from a Deployment to a StatefulSet — stable identities (db-0, db-1), one PVC per Pod via volumeClaimTemplates, ordered startup, and per-Pod DNS so peers can find each other. But that only provides the scaffolding: Kubernetes still has no idea what replication means. The actual leader election and data replication is the database's job, which is why people reach for an Operator that encodes it.
Where to go next▶
  • Get a cluster on your laptopkind create cluster or minikube start. Both are free, both run in Docker, and everything on this page works on them except LoadBalancer Services and real cloud disks.
  • Stop writing YAML by handHelm for packaging and templating, Kustomize (built into kubectl via -k) for per-environment overlays without templates. Learn Kustomize first — it's simpler and already installed.
  • Stop applying by handGitOps: Argo CD or Flux watch a git repo and reconcile the cluster toward it — the same loop from section 01, applied to your repo. It also removes the "someone kubectl edited prod" class of problem entirely.
  • The three things this page skippedNetworkPolicy (Pod-to-Pod firewall rules — the actual isolation namespaces don't give you), PodDisruptionBudget (stops a Node drain taking down all your replicas at once), and resource quotas / LimitRanges (per-namespace ceilings). All three matter the moment more than one team shares a cluster.