Why does a Helm chart fail to render with `function "env" not defined` when sprig documents an env function?
answer
- The function map is built, then two are dropped
- Names resolve before anything renders
- Same chart plus same values, same manifest
- A public chart could read your shell
- The supported channel leaves a record
basics
~20 sHelm registers the sprig function set but deliberately deletes env and expandenv, and Go templates reject unknown function names while parsing. A chart therefore cannot read the environment of whoever runs helm; caller-side data must arrive as values.
solid answer
~50 sHelm builds its template function map from sprig v3.3.0 and then removes `env` and `expandenv` from it. Go's template engine resolves function names at parse time, so a chart calling one of them fails immediately with `function "env" not defined` — in `helm template` exactly as in `helm install`, and before any object is applied. The removal is deliberate: a render should be a pure function of the chart plus its values, so the same chart and the same values produce the same manifest on a laptop, on a CI runner and in a controller. Reading the caller's environment would break that, and would let a chart pull a token out of a runner's environment into a rendered object. The supported route is to pass the value in explicitly — `--set`, `--set-string` or a values file — which also records it in the release's stored values.
code
bash · 6 lineshelm upgrade --install fanout ./fanout-batch \
--set migration.image="$MIGRATION_IMAGE" \
--set-string build.revision="$CI_COMMIT_SHA"
# the render used exactly these, and the release records them
helm get values fanoutgo deeper
Recall that Helm's function set is sprig minus env and expandenv, and that the failure is an undefined-function error rather than an empty string. Know that pipeline data is passed in with --set or a values file.
Explain why the name fails at parse time rather than render time, and give the reason for the removal: the render must be a pure function of chart plus values, and a chart must not be able to read the installer's environment.
Connect it to operations: values passed explicitly are recorded with the release and can be recovered later, while an environment read leaves no trace. Be ready to name the determinism holes that remain, such as the random helpers.
Frame it as a supply-chain and reproducibility boundary you are defending: a chart is untrusted code that renders on your operators' machines. Decide what the pipeline injects, what the values contract exposes, and how a render stays reviewable.
### What Helm actually registers Helm renders charts with Go's `text/template`, and it hands that engine a function map built from the sprig library — sprig v3.3.0 in current Helm. That is where the vast majority of the functions a chart calls come from: `default`, `quote`, `trunc`, `upper`, `b64enc`, `coalesce`, the dictionary and list helpers, the date helpers. Helm then adds a small set of its own on top, and — the part that surprises people — *deletes* two of sprig's: `env` and `expandenv`. Because Go's template engine resolves function names when it parses the template, not when it executes it, calling a name that is not in the map is not a runtime nil or an empty string. It is a parse failure, reported as `function "env" not defined`. There is no way to guard it with an `if`: the template does not compile at all. The same error appears from `helm template`, from `helm lint`, and from `helm install`, which at least means the failure is cheap to discover. ### Why they were taken out The argument is that a chart's render must be a pure function of two inputs: the chart, and the values it was given. Hold both constant and the manifest is identical no matter who ran the command. That property is what makes several everyday things work: - `helm template` in CI produces the same YAML that `helm install` will apply, so reviewing the rendered output means something. - Two engineers debugging the same release can reproduce each other's render. - A diff between two renders is signal, not noise about whose shell had which variable exported. An `env` function destroys that. The same chart and the same values would render differently on a developer laptop, on a CI runner and on a build agent, and the difference would be invisible in the chart, in the values files and in the release's stored values. It is also a straightforward exfiltration path: a chart pulled from a public repository could read a registry credential or a cloud token out of the environment of whoever installed it and place it into a rendered object — an annotation, a ConfigMap, anything the chart author chose. ### What to do instead Pass the value in. A pipeline that knows the image digest, the environment name or the config revision sets it explicitly: ```bash helm upgrade --install fanout ./chart \ --set migration.image="$MIGRATION_IMAGE" \ --set-string build.revision="$CI_COMMIT_SHA" ``` This is more than a workaround. It makes the input part of the release: the values Helm stores with the revision record it, and `helm get values` will show what the render actually used. An `env` lookup would leave no trace at all — the release would be unexplainable six months later. Where the data is genuinely a file rather than a scalar, `--set-file` reads a file's contents into a value, which covers the common case of injecting a certificate or a rendered config produced earlier in the pipeline. ### The determinism hole that remains Removing `env` closes one hole; it does not make every chart's render deterministic. The sprig random helpers — `randAlphaNum`, `uuidv4` and their relatives — are present and are re-evaluated on every render. A template that generates a password with `randAlphaNum 24` produces a *different* password on every upgrade, which is why charts that do this pair it with a lookup of the existing object or with the resource-keep annotation. When someone asks why `env` is missing, the follow-up worth volunteering is that determinism is a property the chart author still has to maintain by hand. ### Version note The removal of `env` and `expandenv` is long-standing and identical in Helm 3 and Helm 4 — there is no flag that restores them. Helm 4 did extend the function set in one visible way: it added a duration family (`durationSeconds` through `durationWeeks`, plus `durationRoundTo` and `durationTruncateTo`) that Helm 3.21 does not have. That matters for charts you publish rather than only install yourself: a template using those functions parses under Helm 4 and fails with the same class of `function ... not defined` error on a Helm 3 client, which is a compatibility decision to make consciously rather than discover in someone else's cluster.
- Could you not just wrap the `env` call in an `if` so it only runs where it is available?No. Go's template engine resolves function names at parse time, so a template naming a function that is not in the map fails before any branch is evaluated. An `if` guards execution, not compilation. The same is true for any function you invent: the error is `function "..." not defined`, and it appears identically from `helm template`, `helm lint` and `helm install`.
- What did Helm 4 add to the function set that a Helm 3 client will reject?A duration family: `durationSeconds`, `durationMinutes`, `durationHours`, `durationDays`, `durationWeeks`, plus `durationRoundTo` and `durationTruncateTo`. They are absent from Helm 3.21, so a chart that uses them parses under Helm 4 and fails to render on a Helm 3 client with an undefined-function error. If the chart is published for others to install, that is a compatibility decision, not a free upgrade.
- Does removing `env` make a chart's render deterministic?It removes one source of non-determinism, not all of them. The sprig random helpers such as `randAlphaNum` and `uuidv4` are still present and re-evaluate on every render, so a generated password changes on each upgrade. Rendering against the live cluster or generating values from the current time has the same effect. Determinism stays the chart author's responsibility.
saying these in an interview costs you the question
- Claiming env works once the variable is exported
- Thinking an if block can guard an undefined function
- Saying a flag or env var restores env in Helm
- Assuming Helm ships plain upstream sprig unchanged
- Believing removing env made every render deterministic
- Reading pipeline data from the environment instead of passing values