TT Lab
Get started
Learn Learning paths Courses

Authoring and Shipping Helm Charts

Injecting values Safely With Template Functions

Continue in TT Lab

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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].

Notes

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.