Authoring and Shipping Helm Charts
Injecting values Safely With Template Functions
Goal
You learn by hand how to safely plug values into a manifest with template functions, and which tool to use when a value is missing, when it is required, and when it is a block.
Why it matters
A Helm template is not a YAML editor but a string generator. Only after the final result is completed as a string does the YAML parser read it. So when the indentation is off by two spaces, instead of an error the block vanishes wholesale or goes under the wrong parent. This is why you must attach nindent to a block spread out with toYaml — indent does not create the leading newline, so the first line sticks to the preceding key. Value design is the same. default is for "values that may be absent," and required is for "values that must not be deployed without." If you swap these two, you either deploy silently with a wrong default, or end up with a chart that nobody can use. Finally, be sure to remember that the precedence of values, from lowest, is the chart's values.yaml, the files given with -f (from left to right), and then --set, and that maps are merged deeply but lists are replaced as a whole.
Steps
- Create a chart in
/root/helm/tpl/labhub-api(you may start withhelm create labhub-api). Setimage.repositoryinvalues.yamltonginxandimage.tagto"1.27", and setappVersioninChart.yamlto"1.26". Then save the rendering withhelm template labhub-api /root/helm/tpl/labhub-api > /root/helm/tpl/out/base.yaml. The result must have a Deployment, the container image must be in the form저장소:태그(repository:tag), and no braces or<no value>may remain. - Write the container image tag as
{{ .Values.image.tag | default .Chart.AppVersion }}, and savehelm template labhub-api /root/helm/tpl/labhub-api --set image.tag="" > /root/helm/tpl/out/default.yaml. Even though you emptied the tag, the image must still carry a tag likenginx:1.26, and you must not uselatestas the default. - Put
ingress.enabled: trueandingress.host: api.labhub.localinvalues.yaml, and wrap the host inrequiredin/root/helm/tpl/labhub-api/templates/ingress.yaml(for example{{ required "ingress.host 를 반드시 지정하세요" .Values.ingress.host }}, where the message says you must specify ingress.host). Then make it fail on purpose withhelm template labhub-api /root/helm/tpl/labhub-api --set ingress.host="" > /root/helm/tpl/out/required-error.txt 2>&1and save that output. The file must show the rendering failure message along with which key is missing. - Fill
resourcesinvalues.yamlwithlimits.cpu: 500m,limits.memory: 512Mi,requests.cpu: 100m, andrequests.memory: 128Mi, and pass it wholesale in the Deployment container in the form{{- toYaml .Values.resources | nindent 12 }}. When you render/root/helm/tpl/out/base.yamlagain, the container'sresources.limits.cpuandresources.requests.memorymust be visible. - Put
envinvalues.yamlas a map and define three entries:APP_MODE: server,LOG_LEVEL: info, andTZ: Asia/Seoul. In the Deployment, build the containerenvlist withrange $k, $v := .Values.env. The containerenvin/root/helm/tpl/out/base.yamlmust have exactly 3 entries, and one of them must be namedLOG_LEVEL. - In
/root/helm/tpl/labhub-api/templates/_helpers.tpl, create a chart identification string (name-version) withdefine, and attachlabhub.io/chart: {{ include "labhub-api.chart" . }}to the Deployment'smetadata.annotations. Then write the difference betweentemplateandincludein one line in/root/helm/tpl/out/include-note.txt— it must say that include returns its result as a string, so you can chain post-processing (indentation) with a pipe. - Put
replicaCount: 2,env.LOG_LEVEL: info, andimage.tag: basein/root/helm/tpl/values-base.yaml, andreplicaCount: 4,env.LOG_LEVEL: debug, andimage.tag: stagein/root/helm/tpl/values-stage.yaml. Then runhelm template labhub-api /root/helm/tpl/labhub-api -f /root/helm/tpl/values-base.yaml -f /root/helm/tpl/values-stage.yaml --set image.tag=cli > /root/helm/tpl/out/merged.yaml. In the result,replicasmust be 4,LOG_LEVELmust bedebug, and the image tag must becli. Finally, write the precedence from lowest one per line in/root/helm/tpl/out/precedence.txt: the chart'svalues.yaml,-f values-base.yaml,-f values-stage.yaml,--set. - Add a ConfigMap template that exports the
configmap ofvalues.yamlasdata, and attachchecksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}to themetadata.annotationsof the Pod template. The Pod-levelspec.template.spec.securityContext.runAsNonRootmust betrue. Save the completed rendering as/root/helm/tpl/out/final.yaml(3 or more objects, 3 or more container env entries, no<no value>). Also savehelm lint /root/helm/tpl/labhub-api > /root/helm/tpl/out/lint.txt, with no[ERROR].
Notes
- Lab Pods start fresh for every lab, so charts made in other labs do not remain. The chart for this lab is also built from scratch under
/root/helm/tpl— this is where the fact that a chart is a reproducible package shows. - With
helm template ... -s templates/deployment.yamlyou can view just one file, and adding--debugshows even the intermediate result of a failed rendering. - After editing the templates in steps 4, 5, and 6, be sure to render
/root/helm/tpl/out/base.yamlagain and overwrite it. The grading of steps 1, 4, 5, and 6 all looks at this single file. - Common mistake 1: using
indent, so the first line of the block sticks to the preceding key. If you need a newline in front, it isnindent. - Common mistake 2: expecting lists to be merged too when you give two value files. Maps are merged deeply, but lists are replaced as a whole. This is why
envwas designed as a map and not a list in this lab.
Render by referencing values
Create a chart in /root/helm/tpl/labhub-api (you may start with helm create labhub-api). Set image.repository in values.yaml to nginx and image.tag to "1.27", and set appVersion in Chart.yaml to "1.26". Then save the rendering with helm template labhub-api /root/helm/tpl/labhub-api > /root/helm/tpl/out/base.yaml. The result must have a Deployment, the container image must be in the form 저장소:태그 (repository:tag), and no braces or <no value> may remain.
The image is put together by taking the repository and the tag from values separately. If braces or '' remain in the rendering result, it means the key you referenced is not in values.
Fill an empty value with default
Write the container image tag as {{ .Values.image.tag | default .Chart.AppVersion }}, and save helm template labhub-api /root/helm/tpl/labhub-api --set image.tag="" > /root/helm/tpl/out/default.yaml. Even though you emptied the tag, the image must still carry a tag like nginx:1.26, and you must not use latest as the default.
Even when you render with the tag emptied, the image must still carry a tag. If you take the value to use instead from Chart.yaml, the chart and the app version naturally match. latest is not the answer.
Enforce a required value with required
Put ingress.enabled: true and ingress.host: api.labhub.local in values.yaml, and wrap the host in required in /root/helm/tpl/labhub-api/templates/ingress.yaml (for example {{ required "ingress.host 를 반드시 지정하세요" .Values.ingress.host }}, where the message says you must specify ingress.host). Then make it fail on purpose with helm template labhub-api /root/helm/tpl/labhub-api --set ingress.host="" > /root/helm/tpl/out/required-error.txt 2>&1 and save that output. The file must show the rendering failure message along with which key is missing.
If a required value is empty, the rendering itself must fail. Write in the error message which key is missing. The failure output goes to standard error, so you must capture it too when you save.
Pass a block with toYaml and nindent
Fill resources in values.yaml with limits.cpu: 500m, limits.memory: 512Mi, requests.cpu: 100m, and requests.memory: 128Mi, and pass it wholesale in the Deployment container in the form {{- toYaml .Values.resources | nindent 12 }}. When you render /root/helm/tpl/out/base.yaml again, the container's resources.limits.cpu and resources.requests.memory must be visible.
A block you pass wholesale, like resource limits, is not written line by line. Whether a newline is needed in front or not is the difference between the two indentation functions.
Expand environment variables with range
Put env in values.yaml as a map and define three entries: APP_MODE: server, LOG_LEVEL: info, and TZ: Asia/Seoul. In the Deployment, build the container env list with range $k, $v := .Values.env. The container env in /root/helm/tpl/out/base.yaml must have exactly 3 entries, and one of them must be named LOG_LEVEL.
You iterate over the map in values, receiving it as two variables, key and value. It is safer to wrap values in quotes. Exactly three must come out.
Define a named template and include it
In /root/helm/tpl/labhub-api/templates/_helpers.tpl, create a chart identification string (name-version) with define, and attach labhub.io/chart: {{ include "labhub-api.chart" . }} to the Deployment's metadata.annotations. Then write the difference between template and include in one line in /root/helm/tpl/out/include-note.txt — it must say that include returns its result as a string, so you can chain post-processing (indentation) with a pipe.
Slot the piece made with define into the annotation spot. Leave a note on which of the two calling styles can be chained with a pipe, and why.
Check the value precedence
Put replicaCount: 2, env.LOG_LEVEL: info, and image.tag: base in /root/helm/tpl/values-base.yaml, and replicaCount: 4, env.LOG_LEVEL: debug, and image.tag: stage in /root/helm/tpl/values-stage.yaml. Then run helm template labhub-api /root/helm/tpl/labhub-api -f /root/helm/tpl/values-base.yaml -f /root/helm/tpl/values-stage.yaml --set image.tag=cli > /root/helm/tpl/out/merged.yaml. In the result, replicas must be 4, LOG_LEVEL must be debug, and the image tag must be cli. Finally, write the precedence from lowest one per line in /root/helm/tpl/out/precedence.txt: the chart's values.yaml, -f values-base.yaml, -f values-stage.yaml, --set.
Try applying two value files and a command-line option at once. For value files, the order in which you give them matters. Look at the result and write the order from lowest.
Add a configuration hash and a security context
Add a ConfigMap template that exports the config map of values.yaml as data, and attach checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} to the metadata.annotations of the Pod template. The Pod-level spec.template.spec.securityContext.runAsNonRoot must be true. Save the completed rendering as /root/helm/tpl/out/final.yaml (3 or more objects, 3 or more container env entries, no <no value>). Also save helm lint /root/helm/tpl/labhub-api > /root/helm/tpl/out/lint.txt, with no [ERROR].
There is a convention that prevents the problem of Pods not changing even when the ConfigMap content changes. Put the hash of the rendered configuration file in the Pod annotations. Fill in the Pod-level security settings as well.