Nothing matches that. Try a shorter word.
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.
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.
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.
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 instant | kubectl 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 undone | Delete 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 exceptions | Nothing 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-apply | Applying an unchanged manifest is a no-op. This is why Kubernetes config lives in git and gets applied on every deploy. |
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.
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.
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.
| Key | What it means | How to find the right value |
|---|---|---|
| apiVersion | Which 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". |
| kind | The object type, capitalised exactly: Deployment, not deployment. | kubectl api-resources lists every kind your cluster knows, with its apiVersion and short name. |
| metadata | Identity. 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. |
| spec | Your 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. |
| status | Read-only, filled in by the cluster. Shows what's actually true. | Never write it. Read it with kubectl get x -o yaml. |
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
| Rule | Why it bites |
|---|---|
| indent with spaces | A tab anywhere is a hard parse error. Most editors insert tabs by default in unknown file types. |
| indentation is meaning | ports under the wrong level attaches to the wrong object and is silently ignored, rather than failing. |
| quote ambiguous scalars | YAML reads yes, no, on, off as booleans and 8080 as a number. If a field wants a string, quote it: "8080". |
$ 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
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.
Delete the Deployment and the ReplicaSet and Pods go with it. Delete the Service and the Pods carry on, unreachable — nothing owned them.
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.
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.
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.
The control plane decides placement; Nodes are just where Pods physically live.
What you'd actually run
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
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.
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.
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.
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)
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"
$ 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
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.
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.
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.
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.
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}
$ 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.
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.
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.
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.
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
$ 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
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.
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.
The Service's selector doesn't change — it just starts finding new-version Pods as the old ReplicaSet scales down.
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
$ 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
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.
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.
Same selector-based routing underneath all three — they differ only in where the entry point is exposed.
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
$ 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
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.
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.
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
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.
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.
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.
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
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."
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.
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.
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.
$ 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
| Namespaced | Cluster-wide |
|---|---|
| Pod, Deployment, ReplicaSet, Service, Ingress, ConfigMap, Secret, PVC, Job, CronJob, ServiceAccount, Role, RoleBinding | Node, 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:
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
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.
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.
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.
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.
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.
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.
Three public hostnames, one cloud load balancer, three ordinary ClusterIP Services behind it.
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 }
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.
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.
| Deployment | StatefulSet | |
|---|---|---|
| Pod names | vote-7d9f8b-x2z1p (random) | db-0, db-1 (stable, predictable) |
| Storage | all replicas share whatever PVC you name | one PVC per Pod, created automatically |
| Start order | all at once | 0, then 1, then 2 — each waits for Ready |
| Rolling update | any order | highest ordinal first, downward |
| DNS | Service name only | db-0.db.default.svc.cluster.local per Pod |
| Use for | stateless web apps, workers, APIs | Postgres, Kafka, Redis clusters, anything with peers |
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 } }
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.
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.
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
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.
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.
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 }
$ 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
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.
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: CPU | Over the limit: memory | |
|---|---|---|
| what happens | throttled — the app just gets slower | OOMKilled — the container is killed instantly and restarted |
| you'll see | latency spikes, no events, nothing in logs | Reason: OOMKilled in kubectl describe pod, restart count climbing |
| advice | set requests honestly; be generous or omit CPU limits | always set a memory limit, and make it real |
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
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
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 }
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.
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.
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
$ 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
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.
Look things up
The part you come back to. Commands, symptoms, and vocabulary.
kubectl, the useful half
There are hundreds of flags. These are the ones you'll use every day.
Looking around
| Command | What it gives you |
|---|---|
| kubectl get pods | The list. Add -o wide for Node and Pod IP, -w to watch it change live. |
| kubectl get all | Deployments, ReplicaSets, Pods and Services in one view. Lies slightly — it misses ConfigMaps, Secrets, PVCs and Ingresses. |
| kubectl get pods -A | Every namespace. The "where did my thing go" command. |
| kubectl describe pod X | The 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=.lastTimestamp | Cluster-wide recent history. Add -w during a deploy to watch decisions happen. |
| kubectl get X -o yaml | The full object including the status the cluster wrote. How you find out what really got applied. |
| kubectl explain X.spec | Offline field documentation matching your cluster version. Add --recursive for the whole tree. |
| kubectl api-resources | Every kind this cluster knows, with apiVersion, short name, and whether it's namespaced. |
Debugging a running thing
| Command | What it gives you |
|---|---|
| kubectl logs X | stdout of the container. -f to follow, --tail=50 to trim. |
| kubectl logs X --previous | Logs 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-containers | Logs from every Pod matching a label at once. |
| kubectl exec -it X -- sh | A shell inside the container. -- separates kubectl's flags from the command. |
| kubectl debug -it X --image=busybox --target=app | Attaches a debug container to a running Pod. Works when the image has no shell. |
| kubectl port-forward svc/vote 8080:80 | Reach a ClusterIP Service from your laptop. Fastest way to test without exposing anything. |
| kubectl run tmp --rm -it --image=busybox -- sh | A throwaway Pod inside the cluster network — for testing DNS and reachability from the inside. |
| kubectl top pod | Live CPU and memory per Pod. Needs metrics-server. |
Changing things
| Command | What it gives you |
|---|---|
| kubectl apply -f dir/ | Apply every manifest in a directory. Idempotent — safe to re-run. |
| kubectl diff -f x.yaml | What would change. Run this before every apply you're unsure about. |
| kubectl apply -f x.yaml --dry-run=server | Full server-side validation, zero changes. |
| kubectl scale deploy/vote --replicas=5 | Quick scale. Remember it's not in your git repo — the next apply reverts it. |
| kubectl set image deploy/vote vote=img:v2 | Trigger a rolling update without editing YAML. |
| kubectl rollout status deploy/vote | Blocks until the rollout finishes or fails. Use it in CI. |
| kubectl rollout undo deploy/vote | Back to the previous ReplicaSet. Near-instant. |
| kubectl rollout restart deploy/vote | Recreate every Pod with the same spec — how you pick up a changed ConfigMap or Secret. |
| kubectl delete -f x.yaml | Delete exactly what that file created. Safer than deleting by name. |
alias k=kubectl source <(kubectl completion bash) # or zsh — tab-completes Pod names, saves hours complete -o default -F __start_kubectl k
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.
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 see | It means | Do this |
|---|---|---|
| Pending | No 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. |
| CrashLoopBackOff | The 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. |
| CreateContainerConfigError | A 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 READY | The 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. |
| OOMKilled | The 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/1 | An init container hasn't finished. Everything else waits. | kubectl logs X -c <init-container-name>. |
| Terminating, forever | A 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. |
| Evicted | The 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 responds | The selector matches no Ready Pods. | kubectl get endpoints <svc>. Empty = label mismatch or all Pods unready. Fix that, not the network. |
| Ingress ADDRESS is empty | No Ingress Controller claimed it. | kubectl get ingressclass, then check spec.ingressClassName matches one. |
| Node NotReady | The kubelet stopped reporting: crash, disk pressure, or network partition. | describe node → Conditions. After ~5 minutes its Pods get rescheduled elsewhere. |
| ConfigMap changed, app didn't | Env 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.
| Service | Objects | Why |
|---|---|---|
| redis | Deployment + ClusterIP Service | Internal-only queue, no persistence needed |
| db | Deployment + ClusterIP Service + Secret + PVC | Needs durable storage and credentials out of the image |
| worker | Deployment only | Nothing calls it — no Service needed |
| vote | Deployment + NodePort/LoadBalancer Service | Needs to be reachable from outside the cluster |
| result | Deployment + NodePort/LoadBalancer Service | Same, on a different exposed port |
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.