Every helm upgrade of a chart rolls all its Pods although the image and values are unchanged - how do you find the cause?
answer
- Only one block of the spec matters
- Compare two stored revisions, not the cluster
- Two emitted labels move on every bump
- Hashing a whole file hashes its labels too
- Render twice locally and compare
basics
~20 sDiff the Pod template between the stored revisions, not the whole manifest. The usual culprits are version-bearing labels such as helm.sh/chart on the Pod template, a checksum annotation hashing a whole rendered file that includes those labels, or a non-deterministic function in the render.
solid answer
~50 sA workload rolls when its Pod template changes, so the question is which field moved. Pull the stored manifests for two revisions with `helm get manifest <release> --revision N` and diff only the `spec.template` block. Three causes account for almost all of it. First, the chart's full label set is applied to the Pod template, and `helm.sh/chart` embeds the chart version while `app.kubernetes.io/version` embeds `appVersion` - so any version bump changes the template. Second, a `checksum/config` annotation that hashes an entire rendered file picks up that file's own version-bearing labels, so the digest moves even when no config value changed. Third, a non-deterministic render - a timestamp, a random string, a generated certificate - produces a fresh value every time. Then decide: narrow the checksum to the values subtree, drop version labels from the template while keeping them on the object, or accept the rolls.
code
bash · 8 lineshelm get manifest scoring-prod --revision 41 > /tmp/r41.yaml
helm get manifest scoring-prod --revision 42 > /tmp/r42.yaml
diff /tmp/r41.yaml /tmp/r42.yaml
# cheapest test of all: is the render itself deterministic?
helm template scoring-prod ./scoring-chart > /tmp/a.yaml
helm template scoring-prod ./scoring-chart > /tmp/b.yaml
diff /tmp/a.yaml /tmp/b.yamlgo deeper
Take away the core fact: a workload restarts because its Pod template changed, and metadata counts as a change. Learn to look at the template block first rather than guessing at the image.
Be ready to name the fields that move on a version-only bump and to explain why hashing an entire rendered file couples the digest to the chart version.
Show method and judgement: isolate the changed field across revisions, test determinism locally, then argue which metadata is worth a restart for this particular workload and what you lose by removing it.
Set the estate-wide default for what belongs on a Pod template versus an object, weigh restart cost against version observability, and make sure your release cadence and your chart conventions are not fighting each other.
## The scenario A fraud-scoring endpoint runs at 6 replicas from a chart that also carries a pre-upgrade migration hook and a 240-line values file. The team ships small chart edits constantly - roughly eleven version bumps a week, mostly `2.14.3` to `2.14.4` style patch bumps for template tidying. Every one of those upgrades restarts all 6 scoring Pods, each of which loads a model on start-up. Nobody changed the image, and nobody changed a value. ## Step 1: prove it is the Pod template Do not diff the whole manifest - it always differs, because the version labels are on every object. Compare only what the controller actually watches: ```bash helm get manifest scoring-prod --revision 41 > /tmp/r41.yaml helm get manifest scoring-prod --revision 42 > /tmp/r42.yaml diff /tmp/r41.yaml /tmp/r42.yaml ``` and read only the hunks inside a `spec.template` block. Everything else is noise for this question. To predict the *next* upgrade before running it, render the pending change with `helm upgrade --dry-run=server` and diff that against the current stored manifest. Do not be misled by the migration hook: hook Pods are created fresh for each upgrade by design, so a new Job Pod every time is expected and is not the workload restarting. ## Step 2: the three usual causes **Version-bearing labels on the Pod template.** This is the most common by a wide margin. The chart's full label set - the one that correctly stays out of the immutable selector - is applied to `spec.template.metadata.labels`, and it contains `helm.sh/chart: scoring-2.14.4` and `app.kubernetes.io/version`. Bump either version and the Pod template is different, so the workload rolls. Nothing is broken; the chart is doing what it was written to do. The question is whether you meant it. **A checksum annotation with too broad an input.** A chart that writes `checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}` hashes the *entire rendered file*, and that file's own `metadata.labels` carry the same version-bearing labels. So the digest changes on a version-only bump even though not one config key moved. Narrowing the input to the values subtree that actually feeds the config - `{{ toYaml .Values.scoring.config | sha256sum }}` - removes the coupling and rolls Pods exactly when configuration changes. **A non-deterministic render.** Anything that returns a new value on each render and reaches the Pod template, directly or through something you hash: a timestamp, a random string, a freshly generated certificate or password. The tell is that two consecutive `helm template` runs with identical inputs already differ - which is the cheapest test in the whole investigation, because it needs no cluster at all. Render twice, diff, and if the local renders disagree the cause is in the chart, not the upgrade. ## Step 3: decide, do not just fix Once you know which field moves, this becomes a judgement call about what a restart is worth. - **Keep the version labels on the Pod template** and accept the rolls. You get per-Pod version attribution: a query over running Pods answers "what is deployed" without consulting release history. For a workload where a restart is cheap this is the right default. - **Emit the version labels on the object's metadata only**, and keep the Pod template's labels down to the stable ones. Rollouts then track image and config changes rather than chart edits. The cost is that Pods no longer carry the app version, so dashboards and log queries that group by it lose their key - check what consumes that label before you remove it. - **Narrow the checksum input.** This one is close to free and worth doing regardless. - **Reduce the churn at its source** if the eleven bumps a week are mostly cosmetic - but that is a release-hygiene answer, and the chart should still be correct under a high bump rate. For a model-loading scoring endpoint I would narrow the checksum, strip the chart and version labels from the Pod template while keeping them on the object, and note the decision in the chart's documentation so the next author does not "restore" them. ## What not to do Do not reach for a replacement flag - the resource is applying fine, and replacing it makes the disruption total instead of rolling. Do not add a timestamp annotation to make rollouts "predictable": that guarantees a restart on every upgrade, which is the problem, not the fix. And do not conclude the platform is restarting Pods on its own; the Pod template changed, and the diff will show you exactly where.
- How do you tell a genuinely non-deterministic template apart from a version-driven change without touching the cluster?Render the same chart twice locally with identical inputs and diff the output. A version-driven change is stable across those two renders and only differs between chart versions; a timestamp, random string or generated certificate differs between two renders seconds apart. It is the fastest discriminator available and it costs nothing, so run it before you start reading revision history.
- The team asks you to fix this by pinning a fixed annotation value. What do you say?A hard-coded value stops all rollouts, including the ones you need when configuration genuinely changes - you have swapped over-restarting for silently running stale config. The fix is to make the Pod template reflect exactly what should cause a restart: hash the configuration that matters, and keep metadata that only describes the chart on the object rather than on the template.
- What do you lose by removing app.kubernetes.io/version from the Pod template?Per-Pod version attribution. Anything that groups running Pods by application version - dashboards, log queries, ad-hoc checks during an incident - loses its key, and answering "which version is actually running" then means consulting the release rather than the Pods. Check what consumes the label first; if something does, keep it and accept the rollouts, or expose the version some other way.
saying these in an interview costs you the question
- Blames the platform for restarting Pods randomly
- Assumes an unchanged image means an unchanged Pod template
- Diffs whole manifests instead of the Pod template
- Adds a timestamp annotation to make rollouts predictable
- Reaches for a replace flag to stop the churn
- Confuses per-upgrade hook Pods with the workload restarting