Production Readiness: Non-Obvious Generation Gotchas
Deep-dive reference for k8s-yaml-generator. Load this when generating a
Deployment or Pod spec that will run outside a scratch/dev cluster — the
defaults Kubernetes silently applies are frequently not the defaults an
author expects, and getting them wrong only surfaces under load or during a
node drain, not at kubectl apply time.
imagePullPolicy defaults are tag-dependent, not a single default
Kubernetes does not apply one blanket default for imagePullPolicy. The
default is derived from the image tag:
image: myapp:latest(or no tag at all) → default isAlways. Every pod start re-pulls the image, even if the same tag was pulled seconds ago on the same node.image: myapp:v1.2.3(any non-latesttag) → default isIfNotPresent. A stale local image on a node is reused silently if the tag was already cached there, even if the registry’sv1.2.3has since been overwritten.
Both defaults are traps in different directions: latest + Always makes
rollouts non-deterministic and slow (registry round-trip on every pod start);
a pinned tag + IfNotPresent can serve a stale image forever if someone
force-pushes the same tag to the registry. Always set imagePullPolicy
explicitly rather than relying on either inferred default, and never reuse a
tag once it has been pushed.
Requests without limits change the pod’s QoS class
The combination of resources.requests and resources.limits determines
the pod’s Quality of Service class, which controls eviction order under node
memory pressure:
| requests | limits | QoS class | Eviction priority |
|---|---|---|---|
| unset | unset | BestEffort |
Evicted first |
| set, requests < limits | set | Burstable |
Evicted second |
| set | set, requests == limits | Guaranteed |
Evicted last |
A manifest with limits but no requests is silently treated as
requests == limits (Kubernetes back-fills requests from limits when
requests is omitted), so it is never actually BestEffort — but a manifest
with requests and no limits stays Burstable with an unbounded ceiling,
which defeats the purpose of setting requests in the first place. For a
production workload that must survive a memory-pressure eviction event
without being picked first, set both fields, with requests == limits, to
land in Guaranteed.
Probe threshold interaction can create restart storms
livenessProbe and readinessProbe are evaluated independently, but tuning
them identically is a common cause of cascading restarts under load: if
initialDelaySeconds and periodSeconds are equal for both probes and the
application is briefly slow to respond (a GC pause, a cold cache), the
liveness probe can fail at the same moment the readiness probe is already
pulling the pod from service — the kubelet then restarts a container that
was about to recover on its own, resetting warm caches and connections and
making the underlying slowness worse. Give the liveness probe a longer
failureThreshold × periodSeconds window than the readiness probe, so
readinessProbe can take the pod out of rotation without livenessProbe
racing to kill it.
terminationGracePeriodSeconds does not wait for child processes
The default 30-second grace period sends SIGTERM to PID 1 in the
container and waits; it does not propagate to child processes unless PID 1
forwards the signal itself. A shell-form CMD/ENTRYPOINT (CMD myapp --flag) runs the shell as PID 1 and the application as a child that never
receives SIGTERM directly — the shell exits, orphaning the child, which is
then hard-killed by SIGKILL at the end of the grace period with no chance
to flush in-flight work. Use exec-form entrypoints (["myapp", "--flag"]),
or a lightweight init (tini, dumb-init) as PID 1, so SIGTERM reaches
the actual process.