Helper Files and Named Templates

Helm is a widely used package manager for Kubernetes. It simplifies the definition, installation, and upgrading of complex Kubernetes applications. Two of its key features that contribute to its efficiency are named templates and helper files.

In this blog, we will see what are Helper Files and Named Templates in Helm.

Labeling in Helm

Helm offers a flexible and convenient means of labeling Kubernetes objects during deployment. Labels are key-value pairs that can be attached to any Kubernetes object, allowing for organization, grouping, and selection of objects based on their shared attributes.

Let’s say we want more complex labeling than what we currently have in the file below:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-release-nginx
  labels:    
    app.kubernetes.io/name: nginx
    app.kubernetes.io/instance: my-release

We would need to repeatedly write these labels in multiple spots. For example, in our case, in the deployment.yaml template, we have three places where these labels would appear.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: RELEASE-NAME-nginx
  labels:    
    app.kubernetes.io/name: nginx
    app.kubernetes.io/instance: RELEASE-NAME
spec:
  selector:
    matchLabels:      
      app.kubernetes.io/name: nginx
      app.kubernetes.io/instance: RELEASE-NAME
  template:
    metadata:
      labels:        
        app.kubernetes.io/name: nginx
        app.kubernetes.io/instance: RELEASE-NAME
    spec:
      containers:
        - name: nginx
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

Furthermore, we’ll need to use these same labels in some other Kubernetes objects, too. In our exercise, we’ll also create a service.yaml template so that we get a Kubernetes service for our Nginx app to expose our website to the Internet. Now imagine that instead of these two lines, we have even more that we repeatedly need to rewrite, one by one, in templatized form, in multiple .yaml files. It would be pretty tedious, not to mention error-prone. In our case, we would need to repeat these lines in every spot, they’re required:

app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}

Now, this is easy to remember. But for more complex stuff, we might forget that we actually used .Release.Name for our instance label. As we create more and more .yaml template files, in the fifth file we create, say, 5 days later, we might mistakenly use .Chart.Name for our instance label because we simply forgot what we did last time. But there’s a better way to do this.

Helper Files / Named Templates

Helm considers files in the templates/ directory templates that should be rendered into a Kubernetes manifest file. There are some exceptions; for example, the NOTES.txt file, which we’ll explore in a later blog, and files that have names that start with an underscore are treated differently.

These files are used to store what Helm documentation calls partials and helpers. But that sounds a bit cryptic. What we store here is, in a way, similar to functions in programming. We can sort of create a library, define the functions we need here once, and then use them in every template file that requires them without rewriting the same functions all over again.

What are Helper Files?

Helper files are files that contain reusable code and are especially useful for defining complex Kubernetes objects that are used repeatedly in a chart. By defining these objects in a helper file, developers can avoid code repetition and create templates that are more concise, readable, and easier to maintain.

What are Named Templates?

There are some exceptions; for example, the NOTES.txt file, which we’ll explore in a later blog, and files that have names that start with an underscore are treated differently.

Helm for Beginners Course

Enroll in our Helm for Beginners Course, which covers all of Helm fundamentals. It includes video lectures, interactive exercises, and hands-on labs to help you internalize concepts and commands.

Enroll now!

We mentioned how we have those labels that keep repeating themselves in various spots. So let’s define those two lines in such a helper file. We’ll name this file _helpers.tpl, as this is common practice.

{{/*
Here, we generate selector labels. It's highly recommended that you include comments here, so that other people know what this section does, but it's not mandatory.
*/}}
{{- define "nginx.labels" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

We basically defined a sort of function, and we decided to call it nginx.labels. Of course, we say function here just to give programmers an idea that these are similar to functions in regular coding practice. But these are technically called named templates in Helm’s world.

We could have named our template any way we wanted, but this convention of prefixing the name with the current chart’s name makes it easy to avoid some errors. For example, say we simply named it labels. We might have some complex charts that depend on subcharts. If those sub-charts also have a helper file that contains a label named template, it could override what we defined here, generating entirely different kinds of content, creating confusion for us, and leading to long debugging sessions to trace what went wrong.

If you stick to the name_of_chart.name_of_template format for all your named templates, you should easily avoid such issues, as each named template in every chart and sub-chart should now have a unique name.

So now that we have defined this repetitive stuff, we need to use it in three different spots in our deployment.yaml template, let’s see how we actually do that (equivalent to calling a function in the main program).

Under the two labels: sections and the matchLabels: section, we delete everything and replace it with the named template defined in our helper file. The final result will look like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-nginx
  labels:
    {{- include "nginx.labels" . | indent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "nginx.labels" . | indent 6 }}
  template:
    metadata:
      labels:
        {{- include "nginx.labels" . | indent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

So, with a simple include keyword we can just call on our named template, defined in our _helpers.tpl file, to fill in the stuff we need to add here, namely our two templatized labels. Pretty easy. But it’s worth noting the additional stuff we did. We used the - sign to remove the unnecessary empty extra lines that these include lines would add to the generated manifest.

Also, we used the indent function to insert the necessary number of spaces in front of each of our labels.

But most importantly, we see a dot . after include and the name of the template to include. The dot is a way to pass the so-called top-level scope to this named template. Remember, our named template looks like this

{{- define "nginx.labels" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

.Chart.Name and {{ .Release.Name }} need to fetch the chart name and release the name from somewhere. If you’re a developer, you probably already understand the need for scopes here and what actually happens. If you’re not, you can think of it this way.

By using . to pass the top-level scope here, when Helm enters the named template and begins to process that, when it finally needs to fetch the .Chart.Name it knows it should pull this from the name of the current chart it’s in (the main chart, the top level chart) because it has been instructed to look in the current top-level scope. Without a scope, it would not know from what context or what environment to extract those values from.

For example, imagine a named template is called from a dependency (child chart of the main chart). Does this named template pull the chart name of the parent chart? Or does it pull the chart name of the child chart? Without adding scope, it does not know where to get this data.

Now, let’s explore the manifest that our newly rewritten chart generates.

user@debian:~$ helm template ./nginx
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: RELEASE-NAME-nginx
  labels:    
    app.kubernetes.io/name: nginx
    app.kubernetes.io/instance: RELEASE-NAME
spec:
  selector:
    matchLabels:      
      app.kubernetes.io/name: nginx
      app.kubernetes.io/instance: RELEASE-NAME
  template:
    metadata:
      labels:        
        app.kubernetes.io/name: nginx
        app.kubernetes.io/instance: RELEASE-NAME
    spec:
      containers:
        - name: nginx
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

That's pretty cool; for 6 lines that get generated, we only had to write 3 lines in our deployment.yaml. More importantly, we can now use this named template in any file we need without having to remember the specifics of how we actually defined/templatized these labels.

Now, as mentioned earlier, let’s see how we can use the same named template from our helper file in our other template files.

To complete our small Nginx Helm chart, we also need a service object so that we can make the Nginx pods accessible from any web browser. In the case of deployment, we’d need a semi-complex structure here, with a load balancer in front of our service, but we’ll keep it simple and just make something that works well enough for our testing purposes.

So, we’ll create another template file, named service.yaml, that will generate a manifest for a service object.

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-nginx
  labels:
    {{- include "nginx.labels" . | indent 4 }}
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "nginx.labels" . | indent 4 }}

We can see how, once again, we made use of our named template in two spots here. So we see yet another example of why _helper files and named templates are useful.

Conclusion

It might not be immediately obvious what you want to include in your helper files, but you can start by thinking, "What do I/will I use over and over and over again in most of my template files?" Another way to figure out what is useful to have in these helper files is by downloading charts made by professionals and seeing what they included there.

SUBSCRIBE to gain access to this comprehensive course and 80+ additional courses on Linux, DevOps, AWS, Azure, GCP, Kubernetes, Docker, Ansible, Terraform, Python, and more. Join us on this transformative educational adventure and unlock the power of the cloud.


More on Helm: