How to Reduce Helm Chart Boilerplate with Named Templates

Photo by ready made from Pexels | Modified from original

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:

  1. The name of the template
  2. 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/.

Austin Dewey

Austin Dewey is a DevOps engineer focused on delivering a streamlined developer experience on cloud and container technologies. Austin started his career with Red Hat’s consulting organization, where he helped drive success at many different Fortune 500 companies by automating deployments on Red Hat’s Kubernetes-based PaaS, OpenShift Container Platform. Currently, Austin works at fintech startup Prime Trust, building automation to scale financial infrastructure and support developers on Kubernetes and AWS. Austin is the author of "Learn Helm", a book focused on packaging and delivering applications to Kubernetes, and he enjoys writing about open source technologies at his blog in his free time, austindewey.com.

2 Comments

  1. 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.

    1. 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.

Leave a Reply