Why does a Helm chart that generates a password with randAlphaNum break on upgrade?
answer
- Nothing is carried over between renders
- The function does what its name says
- Render twice and compare
- A checksum annotation makes it loud
- Read the old value before generating
basics
~20 sBecause every helm upgrade re-renders the template from scratch and randAlphaNum returns a new string each time. The Secret is overwritten with a fresh password while the application, or whatever trusts the old one, keeps using it.
solid answer
~50 sHelm re-renders every template on each `helm upgrade`; nothing carries over from the previous revision, so `randAlphaNum` produces a different value each run and the Secret is overwritten with a password nothing else knows. If the workload reads the Secret only at startup, the break appears later on the next restart; if the chart carries a checksum annotation over its config, every upgrade rolls the pods because the annotation changed. Confirm it by rendering twice and diffing — identical values must produce identical output. The fixes are to stop generating (require the value, or take the name of a Secret something else owns), or to `lookup` the existing Secret and reuse its value, falling back to generation only when absent. `helm.sh/resource-policy: keep` does **not** help: it governs deletion, not update, so the upgrade still overwrites.
code
yaml · 15 lines{{- $name := printf "%s-signing" (include "pdfsign.fullname" .) -}}
{{- $prev := lookup "v1" "Secret" .Release.Namespace $name -}}
{{- $pass := "" -}}
{{- if $prev -}}
{{- $pass = index $prev.data "passphrase" | b64dec -}}
{{- else -}}
{{- $pass = randAlphaNum 24 -}}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $name }}
type: Opaque
data:
passphrase: {{ $pass | b64enc | quote }}go deeper
Remember that a chart is re-rendered in full on every upgrade and that a random function returns something new each time. Do not assume a value generated at install is remembered by Helm.
Explain the mechanism precisely: no state carries between renders, so the Secret is overwritten. Be able to demonstrate it by rendering twice and diffing, and to describe the lookup-and-reuse template shape.
Diagnose it from the symptom — an unrelated bump followed by authentication failures, or pods rolling on every upgrade because a config checksum changed. Weigh reuse-by-lookup against removing generation entirely, and say why the annotation everyone reaches for does not help.
Take a position on whether charts in your estate may generate credentials at all, given that lookup trades determinism for convenience and that disaster recovery into a fresh namespace silently mints new material. Say who owns rotation once you decide.
### Templates have no memory A Helm chart is a pure function of its inputs. Every `helm upgrade` re-renders all of `templates/` from scratch against the merged values, and nothing in that render is carried over from the previous revision. `randAlphaNum 24` is sprig's random-string generator: it produces a fresh 24-character string every single time it is evaluated. So a template like ```yaml data: passphrase: {{ randAlphaNum 24 | b64enc | quote }} ``` produces one passphrase at install and a *different* passphrase at every subsequent upgrade, including upgrades that change nothing else. Helm then applies that Secret, and the credential the cluster holds silently stops matching the credential anything else knows. ### What the failure looks like in practice For a PDF-signing service whose passphrase unlocks a key stored elsewhere, the symptom is that signing starts failing shortly after an unrelated chart bump — a resource-limit tweak, say — with an authentication or decryption error and no deployment that looks relevant. Two details make it confusing: * If the workload only reads the Secret at process start, the running pods keep working on the old value until something restarts them, so the failure can appear hours later on the next node drain, and correlating it with the upgrade is hard. * If the chart carries a checksum annotation over the rendered config — the common `checksum/config` pattern that rolls pods when their configuration changes — the annotation changes on *every* upgrade, because the random value changed. Now every upgrade is a full rollout even when nothing was edited, and the breakage is immediate rather than delayed. Chart churn like that is often the first clue that something in the render is non-deterministic. Either way, the diagnosis is the same: render the chart twice and diff it. `helm template` run back to back against identical values should be byte-identical; if the Secret differs between two runs, the chart is generating the value, not carrying it. ### The three real fixes **1. Make it an input.** The simplest and most defensible option: the chart does not generate the credential at all. It takes it from values (or, better, takes the *name* of a Secret something else created) and refuses to install without it, using `required` to fail the render with a readable message rather than shipping an empty string. Determinism is restored because there is nothing random left. **2. Look up the existing value and reuse it.** Helm's `lookup` function reads a live object from the cluster at render time, so the template can keep whatever it generated on the first install: ```yaml {{- $name := printf "%s-signing" (include "pdfsign.fullname" .) -}} {{- $prev := lookup "v1" "Secret" .Release.Namespace $name -}} {{- $pass := "" -}} {{- if $prev -}} {{- $pass = index $prev.data "passphrase" | b64dec -}} {{- else -}} {{- $pass = randAlphaNum 24 -}} {{- end }} ``` This works, and it is what mature charts do — but be honest about its caveats, because an interviewer will push on them. `lookup` needs an actual cluster connection and returns an empty result during a client-side render, so `helm template` and a client-side dry run take the *generate* branch and produce a different answer every time; any pipeline that diffs rendered output has to account for that. It also means the chart's output now depends on cluster state, so the same values no longer determine the result. And on a restore into a fresh namespace the lookup finds nothing and a brand-new credential is generated, which is a silent data-loss scenario if something outside the cluster trusts the old one. **3. Move ownership out of the chart.** Let something outside the release create and rotate the Secret — a bootstrap step, a platform process, or an external secret operator — and have the chart consume it by name. Helm then never renders the material, never stores it in the release record, and cannot clobber it. ### The fix that does not work The frequent wrong answer is `helm.sh/resource-policy: keep` on the generated Secret. That annotation controls **deletion**: it keeps the object when the release is uninstalled or when the resource is dropped from the chart. It does not exempt anything from being updated. An upgrade still writes the newly generated value straight over the existing one, so the annotation buys nothing here — and it also leaves an orphaned Secret behind after uninstall, so a later reinstall inherits an object Helm no longer thinks it owns. The second wrong answer is "pin it with a pre-install hook so it only runs once". A `pre-install` hook genuinely does not run on upgrade, so a hook-annotated Secret is created once — but hook resources are outside the release's normal lifecycle and are commonly deleted by a `hook-delete-policy`, so this trades a regeneration bug for an ownership one. Use `lookup`, or stop generating.
- What does the lookup-and-reuse pattern cost you?Determinism and portability. `lookup` needs a live cluster connection and returns nothing during a client-side render, so `helm template` and a client-side dry run take the generate branch and produce different output on every run — which breaks any pipeline that diffs rendered manifests. The chart's output now depends on cluster state rather than values alone, and restoring into a fresh namespace silently mints a new credential because the lookup finds nothing.
- Would helm.sh/resource-policy: keep on the generated Secret prevent this?No. That annotation is about deletion — it keeps the object when the release is uninstalled or when the resource is removed from the chart. Updates are unaffected, so the upgrade still writes the newly generated password over the old one. It also leaves an orphan behind after uninstall, which a later reinstall then collides with.
- How would you notice this class of bug before it reaches production?Render the chart twice with identical values in CI and fail the build if the output differs. That single check catches every non-deterministic template — random generators, timestamps, anything reaching outside the inputs — and it is cheap because it needs no cluster. It is also the check that tells you a chart cannot be reviewed by diffing rendered output.
saying these in an interview costs you the question
- Believing Helm remembers previously generated random values
- Suggesting resource-policy keep to stop the value changing
- Blaming the workload rather than the non-deterministic render
- Not knowing lookup returns nothing in a client-side render
- Assuming an unchanged values file means an unchanged render