InfrastructureSenior

Kubernetes

Kubernetes is a control loop with an API in front of it. You do not tell it to start a container; you record a desired state, and controllers keep comparing that record to what is actually running and act on the difference — forever, not once. Every object it offers and every surprising behaviour it has comes from that. This page covers the loop, the handful of objects that matter, why resource limits and health probes cause most production incidents, and when you genuinely need any of it.

You declare
A state, not a command
Controllers
Reconcile continuously
Most incidents
Limits and probes

A control loop, not a command runner

The mental model that makes everything else fall into place is a thermostat. You do not tell a thermostat to turn on the heating; you tell it the temperature you want, and it keeps measuring and acting until the room matches — and then keeps measuring. Kubernetes works the same way. You submit an object describing what should be true, the API server stores it, and a controller loops forever: read the desired state, read the actual state, act on the difference.

Kubernetes is a control loop. You submit an object describing what should be true, the API server stores it, and a controller loops forever: read the desired state, read the actual state, act on the difference — then read again. This is why deleting a Pod by hand accomplishes nothing lasting, since the Deployment still says three replicas and a controller makes a third; why applying the same manifest twice is harmless, since you restated a fact rather than issued a second instruction; and why the useful question during an incident is what the cluster believes should be true and what is stopping it, which the object's status and events answer. It is also the argument for keeping manifests in a repository: a manual change is an override some controller will undo.

This is why deleting a Pod by hand accomplishes nothing lasting: the Deployment still says three replicas, so a controller notices two and makes a third. It is why applying the same manifest twice is harmless — you restated a fact rather than issued a second instruction. And it is why the interesting question during an incident is never "did the command run" but "what does the cluster currently believe should be true, and what is stopping it". The answer is in the object's status and its events, which is the first place to look and the last place people look.

The objects you actually need

The API surface is enormous and the working set is small. Six objects cover most of what a service needs, and each exists because a controller reconciles it against something.

  • Pod

    One or more containers that share a network address and can share a volume. The smallest thing scheduled — and never created directly.

  • Deployment

    Says how many identical Pods should exist and from which image. Handles rolling updates and rollbacks. This is what you write.

  • Service

    A stable name and address in front of a changing set of Pods. Pods come and go with new addresses; the Service does not.

  • Ingress

    Routes outside HTTP traffic to Services by host and path, and terminates TLS. One entry point instead of one per service.

  • ConfigMap & Secret

    Configuration and credentials supplied to Pods as env vars or files. A Secret is base64-encoded, not encrypted — enable encryption at rest.

  • StatefulSet

    For Pods that need stable identity and their own persistent storage. Necessary for databases, and a reason to ask whether you want one here at all.

A Service is worth one extra sentence because it is where the naming confusion lives. Inside the cluster, a Service gives you a DNS name that resolves to a stable virtual address, and traffic to it is spread across whichever Pods currently match its selector. That indirection is the whole reason a Pod can be replaced at any moment without anything else being reconfigured — and it is also why "the Pod is running but nothing can reach it" is almost always a selector that does not match the Pod's labels.

Where the incidents come from

Two settings cause more production trouble than everything else combined, and both look like boilerplate. The first is resources. A request is what the scheduler reserves when deciding which node a Pod fits on; a limit is the ceiling enforced while it runs. Set requests too low and the node is oversubscribed, so Pods land where there is no room to grow. Set them too high and you pay for capacity nobody uses. Omit them entirely and the scheduler is guessing, which is how one service ends up starving its neighbours on the same node.

The two limits behave differently when reached, and confusing them wastes hours. Exceeding the memory limit gets the process killed by the kernel: the Pod shows OOMKilled, restarts, and there is no stack trace because no exception was ever raised. Exceeding the CPU limit kills nothing — the process is throttled, so the symptom is latency that grows under load with no error anywhere, which people spend a day blaming on the database. Memory kills, CPU slows: that sentence saves the day it takes to learn it.

ProbeWhat it answersWhat happens when it fails
ReadinessCan this Pod take traffic right now?It is removed from the Service and left running. The safe one, and the one to reach for first.
LivenessIs this process wedged and beyond recovery?The container is killed and restarted. Powerful, and the usual cause of a self-inflicted outage.
StartupHas a slow-booting application finished starting?The other probes are held off until it passes, which is how a slow start stops being a restart loop.

How this shows up in real delivery

The honest question is whether you need it at all, and the answer is less often yes than the industry conversation suggests. Kubernetes buys you scheduling across many machines, self-healing, rolling updates and one declarative interface for teams that each run several services. It charges for that in a permanent operational surface: upgrades, networking, storage classes, access control, and a class of failure whose cause is the platform rather than your code. Three services on two machines get most of the benefit from a container runtime and a load balancer, at a fraction of the cost — and a managed platform that hides the control plane is the middle option worth considering before either extreme.

If you do run it, the practice that pays for itself fastest is keeping manifests in a repository and having something apply them, rather than people running commands against the cluster. The cluster then has a reviewable history, a rollback is a revert, and the difference between what should be running and what is running becomes visible instead of anecdotal. Templating — Helm, Kustomize or either of their descendants — earns its place the moment the same manifests exist per environment, and stops earning it the moment nobody can tell what the templates produce.

On stateful workloads: the platform can run a database, and the question is whether it should. A StatefulSet plus persistent volumes gives you identity and storage that survive rescheduling, and it also gives you responsibility for backups, failover, version upgrades and the storage layer's own failure modes — on top of a scheduler that is designed to move things. Teams that run their own databases this way generally have someone whose job that is. Teams that do not are usually better served by a managed database and a cluster that holds only stateless work.

Where it degrades

  • Adopting it for three services on two machines, and paying the operational cost for none of the benefit.
  • A liveness probe with a dependency in its path, which turns a slow database into a cluster-wide restart storm.
  • No resource requests, so the scheduler guesses and one service starves its neighbours.
  • Chasing an OOMKilled restart for a stack trace that was never produced, because the kernel did the killing.
  • Changes made with kubectl edit on a live cluster, which the next apply silently discards.
  • Secrets committed as manifests, base64-encoded and mistaken for encrypted.
  • A Service whose selector does not match the Pod labels, so a healthy Pod receives nothing.
  • Running your own database in-cluster with nobody owning backups, failover or storage.
  • Templating layered on templating, until no one can say what is actually applied.

When to use it

Use it when

  • Many services across many machines, where scheduling and self-healing are work you would otherwise do by hand.
  • Several teams needing one declarative interface and one set of guarantees to deploy against.
  • Workloads whose demand varies enough that scaling replicas up and down is a routine event.
  • Organisations already committed to it, where a managed control plane removes most of the operational cost.

Avoid it when

  • A small number of services on a small number of machines, where a runtime and a load balancer suffice.
  • Teams with no one to own the platform, where the cluster becomes the least understood thing in production.
  • Stateful databases, unless somebody genuinely owns backups, failover and the storage layer.
  • As a way to get portability between clouds, which the managed add-ons around the cluster quietly remove.

Found this useful?

Share it with someone who is working on the same problem.