Kubernetes
Seven Kubernetes defaults I change on day one
I've inherited a fair number of clusters now — some built by teams who knew exactly what they were doing, some assembled from Stack Overflow answers under deadline. The striking thing is how consistent the gaps are. It's more or less the same list every time, and none of it is advanced.
So this is my day-one pass. Not a hardening guide, just the things that have caused me a page at an unpleasant hour.
1. Resource requests on everything
A pod with no requests is invisible to the scheduler. It gets placed as though it
needs nothing, which means the scheduler will happily stack a node until it falls over. Then
the kubelet starts evicting under memory pressure, and pods with no requests are first in the
queue — including, quite often, the one you needed to diagnose the problem.
Set requests from observed usage, not from a guess. For memory I set limits equal
to requests — memory is incompressible, so a pod that exceeds its limit is OOM
killed and I'd rather that be predictable. For CPU I set a request and usually leave the limit
off, because CPU throttling under CFS quota causes latency weirdness that's genuinely hard to
attribute. That's a mildly contrarian position and I'm comfortable with it.
Then add a LimitRange per namespace so a workload deployed without requests gets
sensible ones by default instead of none.
2. Separate readiness and liveness — and use a startup probe
The two probes answer different questions. Readiness: should traffic come to me? Liveness:
should I be restarted? Pointing both at the same /health endpoint that also checks
the database is how you turn a slow database into a cluster-wide restart storm — every pod
fails liveness simultaneously, every pod restarts, none of them fix the database.
Rules I stick to: readiness may check dependencies. Liveness must only check that this process
is responsive. And if the app takes more than a few seconds to start, that's a startup probe's
job, not a large initialDelaySeconds on liveness.
The initialDelaySeconds approach forces a bad trade: long enough for a slow boot
means a genuinely hung pod also takes that long to be noticed. A startup probe lets you be
patient at boot and strict afterwards.
3. A PodDisruptionBudget for anything that matters
This is the one that catches people during a node upgrade. Without a PDB, a drain will evict every replica of your service at once — the eviction API has no reason not to. You find out during maintenance, which is exactly when you'd assumed you were being careful.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
minAvailable: 1 # or "50%"
selector:
matchLabels:
app: api
One caveat: a PDB on a single-replica deployment blocks the drain entirely rather than protecting anything. If it's important enough to need a PDB, it's important enough to run more than one replica.
4. Topology spread, not just replica count
Three replicas is not high availability if all three are on the same node. I've seen that exact configuration described as HA in a design document.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
I default to ScheduleAnyway rather than DoNotSchedule. Perfect spread
is a preference; refusing to schedule during a zone incident is a self-inflicted outage.
5. Default-deny NetworkPolicy
Out of the box, every pod can reach every other pod. That means a compromised frontend can talk directly to your database, and nothing in the cluster will comment on it.
Apply a default-deny ingress policy per namespace, then allow what's needed. It's tedious for about a day and then it's just how the namespace works. Do remember to allow DNS to kube-system — everyone forgets, everything breaks in a confusing way, and then everyone remembers.
6. Pin image tags, drop unnecessary privileges
:latest means your pods are not reproducible. Two replicas of the same Deployment
can be running different code, which produces bug reports that are impossible to act on. Pin
to a tag, or better a digest, and let your deployment tooling update it.
While you're in the pod spec: runAsNonRoot, drop capabilities, read-only root
filesystem where the app tolerates it. Most images run as root because nobody said otherwise,
not because they need to.
7. Make the Helm chart say what it means
Less a Kubernetes default than a habit. The charts I inherit usually have every value
plumbed through to every field, so a reviewer can't tell what actually differs between staging
and production without diffing two values.yaml files line by line.
I'd rather the chart encode opinions — sensible probes, requests, a PDB, spread constraints, all present by default — and expose a small surface for the things that genuinely vary: image tag, replica count, resource sizing, environment-specific config. Fewer knobs means a chart that's hard to deploy badly, and the defaults carry your standards into every new service without anyone having to remember them.
Nothing on this list is clever, and that's why it's worth writing down. Almost every Kubernetes incident I've been involved in traced back to one of these rather than to anything subtle about the control plane. The exotic failure modes make better conference talks; the boring ones are what actually wake you up.