This is Part 1 of Reducing Helm Chart Boilerplate with Named Templates and Library Charts.
Part 2 is called How to Reduce Helm Chart Boilerplate with Library Charts.
One of the key benefits of Helm is that it helps reduce the amount of configuration a user needs to provide to deploy applications to Kubernetes. However, as Helm chart developers, there are times when you will also want to focus on reducing the amount of configuration required to create your chart templates. Consider a Helm chart template used to create a deployment, which might look like this:
apiVersion: apps/v1 kind: Deployment metadata: labels: app: {{ .Release.Name }} name: {{ .Release.Name }} spec: replicas: {{ .Values.replicas }} selector: matchLabels: app: {{ .Release.Name }} strategy: type: {{ .Values.strategy }} template: metadata: labels: app: {{ .Release.Name }} spec: containers: - image: {{ .Values.imageName }}:{{ .Values.imageTag }} name: {{ .Release.Name }}
If this is the only template in your Helm chart, then something like this is great! However, what happens if you need to create two or more deployments in your Helm chart? Will you copy-paste the same YAML into different files, making slight modifications where necessary? What happens if you need to make the same change across all of your deployments? Similarly, how do you handle resources that contain the same labels? Do you copy-paste those across each chart template and make changes to all templates when you need to add, remove, or modify a label?
In this article, I’ll talk about how you can reduce boilerplate in your Helm charts by using named templates. This article will serve as Part 1 of a two-part series called Reducing Helm Chart Boilerplate with Named Templates and Library Charts, where I’ll cover the following topics that will help you develop more easily-maintainable Helm charts:
-
Named Templates (this post)
-
Library Charts
Let’s get started by diving into named templates.
Understanding Named Templates
You can think of named templates as functions that you can use throughout your Helm chart. While “normal” chart templates generate Kubernetes resources, named templates provide reusable syntax or logic throughout your Helm chart. Below is an example named template that produces a standard set of labels:
{{- define "test-chart.labels" -}} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}
The define action marks the beginning of a new named template. In this example, the template is called “test-chart.labels”. The convention I like to use when naming a template is $CHART_NAME.$THING_THE_TEMPLATE_CREATES, so in this case, the Helm chart that this template belongs to is called “test-chart”, and the thing that this template creates is “labels”.
Named templates belong under the templates/ folder with the rest of your Kubernetes templates. However, they easily stand out from regular templates because files that define named templates are prefixed with an underscore (_) and contain the “.tpl” extension. The filename that’s most commonly used is _helpers.tpl, though you can use any other name and create multiple tpl files. A tpl file can contain one or more named templates.
Let’s take a look at how you can apply named templates throughout your Helm chart.
Applying Named Templates
Named templates are applied by using the include function. As parameters, the include function takes the following two arguments:
- The name of the template
- The object scope
The include function will then process the named template in the location where it is applied.
Here’s an example of applying the test-chart.labels template in a deployment (only the relevant YAML is shown):
apiVersion: apps/v1 kind: Deployment metadata: labels: {{- include "test-chart.labels" . | indent 4 }} name: {{ .Release.Name }} spec: ...
You’ll commonly see include used in a pipeline to format the output. In this case, the output is piped to indent to indent the labels 4 spaces. The result would look like this:
→ helm template my-test . --- # Source: test-chart/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/version: "1.16.0" app.kubernetes.io/managed-by: Helm app.kubernetes.io/instance: my-test name: my-test spec: ...
You can use this named template to add the same labels to each resource your Helm chart manages by adding the “include” invocation to your other templates. If you need to modify your labels, you can do this in a single location (the named template definition) rather than having to make the change in every resource.
Named templates can also call other named templates, so something like this is also possible, where the “helm.sh/chart” label is the result of the “test-chart.chartname” template.
{{- define "test-chart.labels" -}} helm.sh/chart: {{ include "test-chart.chartname" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}
With an understanding of how named templates can be applied, let’s explore some additional use cases.
Named Templates Use Cases
We explored how you can use named templates to create a standard set of labels. There are, however, many more possibilities that you can achieve with named templates. Here’s just a short list of common boilerplate items that you can encapsulate:
-
Container spec
-
Healthchecks
-
Volumes/volumeMounts
-
Selector labels
-
Environment variables
-
Ports/targetPorts
-
Script boilerplate in ConfigMaps
Whenever you have commonalities across two or more templates, you can write a named template to factor out that boilerplate and simplify chart maintenance. Here’s an example of a deployment that uses multiple named templates:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "test-chart.fullname" . }}-frontend labels: {{- include "test-chart.labels" . | nindent 4 }} spec: replicas: {{ .Values.frontendReplicas }} selector: matchLabels: app.kubernetes.io/name: {{ include "test-chart.name" . }}-frontend template: metadata: labels: app.kubernetes.io/name: {{ include "test-chart.name" . }}-frontend spec: {{- include "test-chart.serviceAccountName" . | nindent 6 }} containers: - name: {{ .Chart.Name }} image: {{ .Values.frontend.imageName }}:{{ .Values.frontend.imageTag }} {{- include "test-chart.healthchecks" . | nindent 10 }} {{- include "test-chart.resources" . | nindent 10 }} {{- include "test-chart.nodeselector" . | nindent 6 }}
In this example, the named templates replace the boilerplate between this frontend deployment and other deployments the chart might contain. Config that is specific to this frontend deployment is written as normal. Named templates can’t remove all of the YAML involved, but they can make it easier to maintain configurations that you would like to be shared across two or more resources, and they provide a common place to edit those configs.
Thanks for Reading!
I hope you enjoyed Part 1 of Reducing Helm Chart Boilerplate with Named Templates and Library Charts. In this post, we looked at how you can use named templates to encapsulate boilerplate across Helm chart resources. In Part 2, we’ll expand on the idea of named templates by looking at library charts and using them to capture boilerplate across two or more Helm charts.
Further Information
Check out the Helm documentation for more information on named templates at https://helm.sh/docs/chart_template_guide/named_templates/.
Hi Austin,
thanks for this article. This indeed reduced my boilerplate a lot.
I only had one minor issue.
I couldn’t use `{{- define “test-chart.labels” -}}`. This resulted in the error “mapping values are not allowed in this context” with `helm template` command in version 3.3.1. Just removing the last “-” and changing the line to `{{- define “test-chart.labels” }}` fixed the problem.
Just in case some else hits the same problem.
Glad you found the article helpful! I did a quick test using the
test-chart.labels
template with Helm 3.3.1 and was able to render, though we might have been testing against a different manifest.For folks who run into whitespace errors, removing the “-” often helps.
helm template --debug
is also a helpful troubleshooting tool since it shows you the rendered manifest, even in an error state.