Helm, Kustomize and GitOps with Argo CD · Helm Charts That Don't Surprise You

Broken YAML from Template Indentation

toYaml without the right indentation renders manifests that are invalid or, worse, valid but wrong; render and validate in CI.

Create a free account to save this page, take notes and track your progress. Sign in

Scenario

Example company: someone adds custom pod annotations and environment variables to a chart template. The chart installs fine in development, where those values are empty.

What you see in production

The production install fails with error converting YAML to JSON: yaml: line 32: did not find expected key, or, in another chart, succeeds while the environment variables silently land in the wrong place in the manifest.

The naive code

      containers:
        - name: app
          env:
{{ toYaml .Values.env }}            # not indented: items land at column 0

Reproduce it

Render a template with the indentation bug and with nindent, and validate both with kubeconform:

toYaml without and with nindent
mkdir -p chart/templates
printf 'apiVersion: v2\nname: app\nversion: 0.1.0\n' > chart/Chart.yaml
printf 'env:\n  - name: LOG_LEVEL\n    value: info\n  - name: REGION\n    value: eu-west-1\n' > chart/values.yaml
write() {
cat > chart/templates/deploy.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata: {name: app}
spec:
  selector: {matchLabels: {app: app}}
  template:
    metadata: {labels: {app: app}}
    spec:
      containers:
        - name: app
          image: example/app:1.0.0
          env:
$1
EOF
}
write '{{ toYaml .Values.env }}'
echo "--- without indentation:"
helm template app ./chart 2>&1 | head -1 | cut -c1-120
write '          {{- toYaml .Values.env | nindent 12 }}'
echo "--- with nindent 12:"
helm template app ./chart | sed -n '/env:/,$p'
helm template app ./chart | kubeconform -strict -summary - | sed 's/Summary: //'

Output

--- without indentation:
Error: YAML parse error on app/templates/deploy.yaml: error converting YAML to JSON: yaml: line 12: did not find expecte
--- with nindent 12:
          env:
            - name: LOG_LEVEL
              value: info
            - name: REGION
              value: eu-west-1
1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0

Run this on your computer — see “Get Started” for setup.

Root cause

Go templates produce text, and YAML structure depends on indentation. toYaml outputs a block starting at column 0; inserted under a nested key it either breaks parsing or attaches the content to a different parent. Bugs hide when the value is empty in the environment where the chart was tested.

The fix

  • Use {{- toYaml .Values.x | nindent N }} (the - trims the preceding newline, nindent adds a newline and indents).
  • Wrap optional blocks in {{- with .Values.x }}...{{- end }} so empty values render nothing.
  • In CI, render with representative values for every environment (including non-empty optional blocks) and validate with kubeconform -strict.
  • Add helm unittest tests for templates with logic.

Trade-offs

  • Templating YAML is inherently fragile; keep templates simple and push complexity into values structure.
  • Strict validation needs schemas for CRDs (kubeconform supports CRD schema catalogs).

Detect it in production

  • CI renders that fail parsing or schema validation.
  • Manifests in the cluster missing fields set in values (configuration drift checks).

Key points

  • Templates output text; YAML depends on indentation.
  • Use toYaml | nindent and with blocks for optional values.
  • Render every environment's values in CI and validate strictly.

Further reading: Helm template function list: nindent · Chart best practices: templates

Practice questions

What does {{- toYaml .Values.env | nindent 12 }} do?

Answer

Trims whitespace before the action, converts the value to YAML, then starts it on a new line indented by 12 spaces so it nests correctly under the parent key.

Why do template indentation bugs often reach production?

Answer

They only show when the optional value is non-empty; if development and CI use empty values, the broken branch is never rendered.

How do you catch rendering bugs in CI?

Answer

Render the chart with each environment's values (helm template) and validate the output strictly (kubeconform), plus helm unittest for template logic.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?