TT Lab
Get started
Learn Learning paths Courses

Authoring and Shipping Helm Charts

The Dot Moves and Lines Get Glued — Two Roots of Template Failures

Continue in TT Lab

Summary in one line

Most template accidents come not from syntax but from two things — with and range changing what the dot points to, and whitespace trimming wiping out newlines.

Why this is a problem

Errors from Helm templates usually look like this.

Error: YAML parse error on frontier/templates/portal.yaml:
error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context

This message is the YAML parser speaking. It failed while trying to read the result as YAML after the template was fully rendered, so the line number in the message is a line number of the rendered result, not of the template file. That is why looking at line 5 of the template reveals nothing wrong. The culprit is the two characters -}} at the end of a tag three lines above, and those characters appear nowhere in the error.

There is only one way out of this. Look at the broken result directly. helm template --debug prints even a result that does not parse as YAML, as it is. When you look at the output, you can see at a glance that the annotation block is stuck on the same line after annotations:, and that the following labels: is also stuck to the end of the previous line. Guessing at the cause turns into observing.

The dot changes from block to block

with and range are not convenience syntax but blocks that swap the context.

{{- with .Values.app }}
data:
  team: {{ .team }}
  release: {{ .Release.Name }}
{{- end }}

Inside the block, . is no longer the root but .Values.app. So .team is found and .Release is not. Helm says this.

nil pointer evaluating interface {}.Name

This message does not point to the cause either. It means .Release is nil, but to a person .Release is always there, so they do not suspect it. The solution is the variable $, which points to the root. $ is bound to the context at the moment the template started and does not change inside any block. You can write {{ $.Release.Name }}.

range is the same. If you take variables as in range $i, $e := .Values.envs, you can safely use the index and the element, and you fetch root values with $.Values.... If you use only . without taking variables and then need a root value again inside, that is where you get stuck.

indent and nindent, and whitespace markers

{{- removes the whitespace and newlines before the tag, and -}} removes the whitespace and newlines after the tag. Most accidents happen on the trailing side. That is because the next line gets stuck whole onto the previous line.

The difference between indent and nindent is on the same axis.

Function What it does Where to use it
indent 4 Puts four spaces in front of each line In a place where the line has already been broken
nindent 4 Puts a newline first and then four spaces When plugging a block right under a key

The place where you plug a map under annotations: is almost always nindent. If you use indent here, the first line of the block sticks on the same line as annotations: and produces something like annotations: owner: sre, and the YAML parser rejects this as "a mapping value is not allowed here."

If you drop the quotes, the type of the value changes

The third trap blows up after the rendering has succeeded.

data:
  tag: {{ .Values.release.tag }}       # values 에는 "1.10"
  enabled: {{ .Values.release.enabled }}  # values 에는 "no"

The render result is tag: 1.10 and enabled: no. Since the quotes are gone, these values are no longer strings. A parser using YAML 1.1 rules reads no as false, and 1.10, read as a number, loses its trailing 0 and becomes 1.1. The data of a ConfigMap accepts only strings, so the API server rejects it.

cannot unmarshal bool into Go struct field ConfigMap.data of type string

Values that people treat as strings, such as image tags, versions, phone numbers, and country codes, should by default go through quote in the template. Conversely, if you attach quote where a number is required (replicas, port), the same kind of rejection comes from there.

What it looks like in the field

These three are usually caught at different points in the deployment pipeline. Scope and whitespace accidents are caught immediately at render time, but quote accidents survive until the cluster looks at them. So if you run only helm template and move on saying "it renders," it blows up at the latest point. That is why many teams put a single line of helm template | kubectl apply --dry-run=server into CI — a real API server checks the schema and types.

You also use devices that block values themselves before deployment. required stops the render when a value is missing, and fail stops it when a value makes no sense. Both have messages written by people, so if you write "what to fix and how" in them, whoever reads it during an outage can act right away. And helm lint --strict turns things the default lint only reports as warnings (such as an uppercase letter in an object name) into failures. Once you see the same chart end with 0 under helm lint and with 1 under --strict, it becomes clear which one to put in CI.

What you will do in the next lab

You cause on purpose the error of not finding .Release inside with and fix it with $. You take variables in range and use the index and a root value together. You deliberately get the whitespace trimming wrong to break the rendering, then read the broken result with --debug and fix it with nindent. You confirm that a value without quotes is rejected by the API server, and finally use required, fail, and helm lint --strict to keep the same accidents from reaching deployment.