Authoring and Shipping Helm Charts
The Render Output Was Not YAML — Scope and Whitespace
Goal
You deliberately trigger each of these: with and range changing the dot, whitespace trimming sticking lines together, and a value without quotes becoming a number or a boolean — and go on to how to block them with --debug, required, fail, and helm lint --strict.
Why it matters
Most accidents in templates come not from not knowing functions but from the result no longer being YAML. That is why the error message does not point to the cause. mapping values are not allowed in this context is the YAML parser speaking, not the template, so no matter how long you stare at that line, you cannot see that a single -}} is the culprit. For the same reason, the message that .Release is nil inside with cannot be read until you know the fact that "the dot changed." Add the quote problem on top, and the render goes through fine and the cluster rejects it — the kind that blows up at the latest point in the deployment pipeline. If you create each of these four yourself once, from then on you can decide where to look from just two lines of the message.
Steps
- Create a
/root/hc-scope/frontierchart (namefrontier, version0.1.0) and put invalues.yaml:app(nameportal, teamcore, port8080),notes(ownersre, runbookwiki/portal),envs(STAGE,PROD), andrelease(tag"1.10", enabled"no").templates/base.yamlis a ConfigMap whose name is.Values.app.nameand whosedata.portis the port wrapped in quotes. Save the result ofhelm template web /root/hc-scope/frontierto/root/hc-scope/out/base.yaml. - Create
/root/hc-scope/frontier/templates/scoped.yaml. Its name is<app.name>-scoped, wrap what comes under it in a{{- with .Values.app }}block, and inside it writedata.releaseas{{ .Release.Name }}anddata.teamas{{ .team }}. Rendering it fails — save that output to/root/hc-scope/out/with-error.txt. - In
/root/hc-scope/frontier/templates/scoped.yaml, leave thewithblock as it is and fix onlydata.releaseso that the release name renders. Save the render result to/root/hc-scope/out/scoped.yaml(release nameweb). The data must show two lines:release: webandteam: core. - Create
/root/hc-scope/frontier/templates/envs.yaml. Its name is<app.name>-envs, and looping over.Values.envswhile receiving it as two variables, index and value, it emits one line intodatafor each as<소문자 환경이름>: "<인덱스>-<app.port>"(lowercase environment name, index, and port). The render result must showstage: "0-8080"andprod: "1-8080". Save the result to/root/hc-scope/out/envs.yaml. - Create a separate
/root/hc-scope/brokenchart (namebroken) and put invalues.yaml:app(nameportal, teamcore) andnotes(ownersre, runbookwiki/portal).templates/portal.yamlhas the shape where, on the line underannotations:, annotations are plugged in with{{- toYaml .Values.notes | indent 4 -}}andlabels:comes after it. Rendering it fails — save the normal output to/root/hc-scope/out/ws-error.txtand the output with--debugattached to/root/hc-scope/out/ws-debug.txt. Do not fix this chart; leave it as it is. - Copy
/root/hc-scope/brokento/root/hc-scope/fixed(rename the chart tofixed) and fix onlytemplates/portal.yamlso that the render succeeds. The annotation block must come out as two lines indented by four spaces underannotations:, andlabels.teammust come out ascore. Save the result to/root/hc-scope/out/fixed.yaml. - Create
/root/hc-scope/frontier/templates/release.yaml. Its name is<릴리스이름>-release(release name), and you plug indata.tagas.Values.release.taganddata.enabledas.Values.release.enabledwithout quotes. Render only this template and save the result passed throughyq -o=jsonto/root/hc-scope/out/quote-yaml12.json, and save the output of feeding that render tokubectl apply --dry-run=serverto/root/hc-scope/out/quote-error.txt. Then fix it by applyingquoteto both values, and save the output that passes server validation again to/root/hc-scope/out/quote-ok.txt. - Create
/root/hc-scope/frontier/templates/guard.yaml. If the port is below 1024, stop withfail, and stopdata.teamwithrequiredwhen it has no value. With normal values, a ConfigMap named<app.name>-guardmust come out. Save the output rendered with--set app.port=80to/root/hc-scope/out/guard-fail.txt, the output rendered with--set app.team=nullto/root/hc-scope/out/guard-required.txt, and the whole result rendered with no options to/root/hc-scope/out/guard-ok.yaml. - Create a
/root/hc-scope/lintbadchart (namelintbad) and set the ConfigMap name intemplates/cm.yamltoBad_Name. Save the output and exit code ofhelm lintto/root/hc-scope/out/lint-plain.txt, and the output and exit code ofhelm lint --strictto/root/hc-scope/out/lint-strict.txt. Appendexit=<종료 코드>(the exit code goes in the placeholder) as the last line of both files. The answer to this step is that the same chart gets a different pass or fail.
Notes
helm template --debugshows even a result that does not parse as YAML, as it is- Render only one template with
helm template <릴리스> <차트> -s templates/<파일>(release, chart, and file in the placeholders) - When plugging a block right under a key, use
nindent, notindent - Common mistake: using
.Releaseor.Chartas they are inside awithblock — the root is$ - Common mistake: the
-}}at the end of a tag sticks the next line onto the previous line - Official documentation: https://helm.sh/docs/chart_template_guide/control_structures/ · https://helm.sh/docs/chart_template_guide/variables/ · https://helm.sh/docs/chart_template_guide/yaml_techniques/
Lay out the values structure first and render once
Create a /root/hc-scope/frontier chart (name frontier, version 0.1.0) and put in values.yaml: app (name portal, team core, port 8080), notes (owner sre, runbook wiki/portal), envs (STAGE, PROD), and release (tag "1.10", enabled "no"). templates/base.yaml is a ConfigMap whose name is .Values.app.name and whose data.port is the port wrapped in quotes. Save the result of helm template web /root/hc-scope/frontier to /root/hc-scope/out/base.yaml.
If you create it with helm create, skeleton templates come along with it, so in this lab it is cleaner to create the directories and files yourself. All you need are Chart.yaml, values.yaml, and templates/. Use web as the release name throughout this lab.
Cannot find .Release inside with
Create /root/hc-scope/frontier/templates/scoped.yaml. Its name is <app.name>-scoped, wrap what comes under it in a {{- with .Values.app }} block, and inside it write data.release as {{ .Release.Name }} and data.team as {{ .team }}. Rendering it fails — save that output to /root/hc-scope/out/with-error.txt.
When the condition is true, with changes what the dot (.) points to. Inside the block, the dot is no longer the root but .Values.app. So .team is found and .Release is not. The error goes to standard error, so capture it too with 2>&1. Read carefully "what is nil" in the message.
Get the root back with the dollar sign
In /root/hc-scope/frontier/templates/scoped.yaml, leave the with block as it is and fix only data.release so that the release name renders. Save the render result to /root/hc-scope/out/scoped.yaml (release name web). The data must show two lines: release: web and team: core.
The root context at the moment the template started is bound to $, and it does not change even inside with or range. It is the handle you use when you want to keep the convenience of having narrowed things down to .Values.app while still fetching things from the root.
Use root values together inside range too
Create /root/hc-scope/frontier/templates/envs.yaml. Its name is <app.name>-envs, and looping over .Values.envs while receiving it as two variables, index and value, it emits one line into data for each as <소문자 환경이름>: "<인덱스>-<app.port>" (lowercase environment name, index, and port). The render result must show stage: "0-8080" and prod: "1-8080". Save the result to /root/hc-scope/out/envs.yaml.
If you take two variables as in range $i, $e := .Values.envs, you can use the index and the element together, and $i and $e stay alive even when the dot changes. You fetch the root's port with $.Values.app.port. The function that lowercases is lower.
One whitespace trim collapses the YAML
Create a separate /root/hc-scope/broken chart (name broken) and put in values.yaml: app (name portal, team core) and notes (owner sre, runbook wiki/portal). templates/portal.yaml has the shape where, on the line under annotations:, annotations are plugged in with {{- toYaml .Values.notes | indent 4 -}} and labels: comes after it. Rendering it fails — save the normal output to /root/hc-scope/out/ws-error.txt and the output with --debug attached to /root/hc-scope/out/ws-debug.txt. Do not fix this chart; leave it as it is.
indent 4 puts in only four spaces without a newline, and the trailing -}} removes the newline and whitespace that follow. So the annotation block sticks on the same line as annotations:, and the next line's labels: also sticks onto the end of the previous line. The error message is the YAML parser speaking, so it does not point to the cause — if you add --debug, Helm shows the broken result as it is. Check there with your own eyes which lines got stuck together.
Pass the newline along too with nindent
Copy /root/hc-scope/broken to /root/hc-scope/fixed (rename the chart to fixed) and fix only templates/portal.yaml so that the render succeeds. The annotation block must come out as two lines indented by four spaces under annotations:, and labels.team must come out as core. Save the result to /root/hc-scope/out/fixed.yaml.
nindent 4 puts in a newline first and then indents by four spaces. The leading {{- only has to remove the whitespace before the template tag, and you do not use -}} at the end. It is easy if you memorize it as a rule — when plugging a block into the line right under a key, it is always nindent.
The cluster rejected it because the quotes were forgotten
Create /root/hc-scope/frontier/templates/release.yaml. Its name is <릴리스이름>-release (release name), and you plug in data.tag as .Values.release.tag and data.enabled as .Values.release.enabled without quotes. Render only this template and save the result passed through yq -o=json to /root/hc-scope/out/quote-yaml12.json, and save the output of feeding that render to kubectl apply --dry-run=server to /root/hc-scope/out/quote-error.txt. Then fix it by applying quote to both values, and save the output that passes server validation again to /root/hc-scope/out/quote-ok.txt.
You can render only one template with helm template <릴리스> <차트> -s templates/release.yaml (release and chart in the placeholders). Without quotes, 1.10 becomes a number and loses its trailing 0, and no becomes false under YAML 1.1 rules. The data of a ConfigMap accepts only strings, so the API server rejects it with a type conversion error. Think of it as a default that if a value is a string people read, you attach quote.
If the value is wrong, stop at the render stage
Create /root/hc-scope/frontier/templates/guard.yaml. If the port is below 1024, stop with fail, and stop data.team with required when it has no value. With normal values, a ConfigMap named <app.name>-guard must come out. Save the output rendered with --set app.port=80 to /root/hc-scope/out/guard-fail.txt, the output rendered with --set app.team=null to /root/hc-scope/out/guard-required.txt, and the whole result rendered with no options to /root/hc-scope/out/guard-ok.yaml.
fail lets you write the condition yourself, so it is used to block "a value that exists but makes no sense," and required blocks "when the value itself is missing." Write both messages so that a person can read them and fix things right away. --set app.team=null deletes that key — it is different from an empty string. For a numeric comparison, pass through int once, like lt (int .Values.app.port) 1024.
Make warnings be treated as errors
Create a /root/hc-scope/lintbad chart (name lintbad) and set the ConfigMap name in templates/cm.yaml to Bad_Name. Save the output and exit code of helm lint to /root/hc-scope/out/lint-plain.txt, and the output and exit code of helm lint --strict to /root/hc-scope/out/lint-strict.txt. Append exit=<종료 코드> (the exit code goes in the placeholder) as the last line of both files. The answer to this step is that the same chart gets a different pass or fail.
Kubernetes object names must follow the lowercase RFC 1123 rule, so uppercase letters and underscores become warnings. The default lint reports warnings and still ends with 0, but strict mode counts warnings as failures. If you use strict mode in CI, such names do not get as far as deployment. Read the exit code with $? right after the command.