skip to content

Why must a Helm chart be rendered before conftest can evaluate its Kubernetes manifests?

level: juniorimportance: must knowfreq 64%

answer

  1. what the engine can actually parse
  2. Go template text, not YAML
  3. fields appear only after values apply
  4. helm template, then pipe to conftest
  5. no match means no deny, exit 0

basics

~10 s

A chart's templates are Go template text, not valid YAML, and fields hidden behind conditionals only appear once values are applied. Render the chart first, then evaluate the manifests it produces.

solid answer

~50 s

conftest parses structured documents - YAML or JSON - and hands each one to a Rego rule as `input`. A chart under `templates/` is not that: it is Go template text with `{{ ... }}` actions, so the parser either errors or, worse, reads something that is not the manifest that will be applied. Whole fields are conditional: a `resources` block, a readiness probe or a PodDisruptionBudget may only be emitted when a values file enables them. So the correct shape is `helm template <release> <chart> -f <values> | conftest test -`, and for Kustomize `kubectl kustomize <overlay> | conftest test -`. This also matters because in Rego undefined is not false: if the rule body never matches a document with `kind: Deployment`, it produces no message at all and the run exits green. An unparsed or wrongly rendered input therefore reads as a pass, not as an error.

code

yaml · 12 lines
yaml
# templates/deployment.yaml (un-rendered)
spec:
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          {{- if .Values.resources }}
          resources:
{{ toYaml .Values.resources | indent 12 }}
          {{- end }}
          ...

go deeper

for a junior

Be ready to say plainly that a chart is a template, not a manifest, and that you run helm template first and evaluate its output. Knowing the pipe into conftest is enough at this level.

for a middle

Explain both failure modes: a parse error, and the quieter case where a template parses into a document the rules never match, so the run exits green. Name undefined-is-not-false as the reason.

for a senior

Demonstrate that you treat the render inputs as part of the control: which values file, which overlay, which subchart versions. Show that you keep the rendered output as an artifact so a blocked change can be diagnosed.

for a principal

Own the argument that a gate's guarantee is bounded by the artifact it reads. Be able to say what classes of rule this input can never support, rather than letting the check imply coverage it does not have.

## What the engine actually consumes conftest is a runner for Rego policies over **structured configuration documents**. It parses an input file (YAML, JSON and several other formats), binds the parsed document to the variable `input`, and evaluates rules in a package - conventionally `main` - collecting the messages produced by `deny`, `violation` and `warn` rules. Anything that is not parseable into a document is not something it can reason about. A Helm chart is not a document. The files under `templates/` are **Go text templates**: text with `{{ ... }}` actions that are executed against a values object to *produce* manifests. Until `helm template` (or an install/upgrade) runs, the manifest does not exist. ```yaml # templates/deployment.yaml - before rendering - name: {{ .Chart.Name }} {{- if .Values.resources }} resources: {{ toYaml .Values.resources | indent 12 }} {{- end }} ``` Two distinct things go wrong if you point a policy engine at that file. **1. It may not parse at all.** A line like `{{ toYaml .Values.resources | indent 12 }}` sitting where a mapping is expected is not valid YAML. Depending on the fragment you get a parse error - noisy, but at least honest. **2. It may parse into something that is not your manifest.** Many template files *are* accidentally valid YAML, because `{{ .Values.image.tag }}` inside quotes is just a string. Now the engine happily evaluates a document whose `kind` is a template expression, whose container image is the literal text `{{ .Values.image.repository }}`, and which is missing every field that lives behind an `{{- if }}`. This is the dangerous case, and it is dangerous because of how Rego handles absence. ## Undefined is not false A conftest rule is typically written as a rule that produces a message when something is wrong: ```rego package main deny contains msg if { input.kind == "Deployment" some c in input.spec.template.spec.containers not c.resources.limits.memory msg := sprintf("container %s has no memory limit", [c.name]) } ``` If `input.kind` is not `Deployment` - because the document is a half-parsed template, or because the field is a template expression - the body is **undefined**. Undefined is not false and it is not a failure: the rule simply yields no result, the deny set is empty, and conftest exits 0. A green gate over an un-rendered chart is not evidence that the chart is compliant; it is evidence that the rule never found anything to talk about. Junior candidates routinely read that green as a pass, and it is the single most common way an IaC gate becomes decorative. ## Rendering, and rendering *fairly* `helm template my-release ./chart -f values-prod.yaml | conftest test -` produces the multi-document manifest stream that would actually be applied, and that stream is a fair subject for rules about resource requests and limits, readiness probes, or the presence of a PodDisruptionBudget. Kustomize is the same story one layer over: `kubectl kustomize overlays/prod` applies the overlay's patches, and a base evaluated on its own can be missing - or still carrying - exactly the field the rule cares about. Which inputs you render with is then a policy decision in its own right. A chart rendered with the chart's default `values.yaml` and the same chart rendered with the production values file are two different documents, and only one of them resembles what ships. Charts with subchart dependencies add another variable: the rendered output depends on the dependency versions resolved into `charts/`. ## What rendering still does not give you The rendered stream is a set of documents this chart produces, and nothing else. It does not contain objects that already exist in the cluster, objects produced by a different chart, defaults the API server will apply on admission, or anything a controller will add afterwards. Rules that need those cannot be written here honestly, no matter how the input is rendered - that is a property of the artifact, not a gap in the engine. ## The practical shape Render once, in one place, with an explicit values file; feed the rendered output to the policy run; and keep the rendered manifest as a build artifact so a blocked developer can read the exact document the rule judged. That last part is what turns "the gate says no" into a fixable message.

  • Suppose the raw template file did parse cleanly as YAML - why is a rule about memory limits still unsafe over it?
    Because the `resources` block sits behind an `{{- if .Values.resources }}` guard, so it is absent from the raw file regardless of what any values file would produce. The rule finds nothing to complain about and passes, and it would also pass for a values file that legitimately omits limits. Absence in the template says nothing about absence in the rendered manifest.
  • conftest exits 0 on a chart directory. What is the first thing you check?
    Whether anything was actually evaluated. Confirm the input was the rendered stream rather than template files, and check that at least one rule matched a document - for example by adding a rule that denies when no document has a recognised `kind`. In Rego an unmatched body is undefined, which silently contributes nothing rather than failing.
  • Does the same reasoning apply to Kustomize overlays?
    Yes. Run `kubectl kustomize overlays/prod` (or `kustomize build`) and evaluate the output. A base evaluated alone is missing the overlay's patches, so a rule can pass on the base and fail on what actually deploys - or the reverse, if the patch is what removes the field.

Checking an un-rendered chart is like proofreading a mail-merge template: you can see the placeholders, but not the letter any recipient will actually receive.

saying these in an interview costs you the question

  • Assumes the policy engine understands Go template syntax
  • Reads an empty result as proof the manifests are compliant
  • Treats an undefined Rego rule body as false
  • Evaluates chart templates directly and calls that coverage
  • Forgets that a values file decides whether a field exists at all

context