Helm Template Functions and Pipelines

In this blog, we will look at what is Template Functions and Pipelines in Helm.
In the chart we’ve built so far, we’ve assigned proper values to every customizable setting we listed in values.yaml and referenced in deployment.yaml. So for a line like image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
Helm can just fetch the values we supplied in our values.yaml file and generate the final result which might look something like image: "nginx:1.16.0"
. But it was easy in our case. We have a small values.yaml to work with. We just had to customize four settings. But in a complex chart, there might be hundreds of customizable settings. Most users won’t supply values for all of them. This leads to a situation where something like the image name might end up empty. Let’s actually see this in practice.
Let’s open the values.yaml file and we’ll delete the "nginx"
value we assigned to the repository name. The final result will look like this:
replicaCount: 1
image:
repository:
pullPolicy: IfNotPresent
tag: "1.16.0"
If we now look at the manifest that this chart would generate. we’ll see that the repository name is indeed missing.
[email protected]:~$ helm template ./nginx
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: ":1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
If we’d try to install this chart, it would seem like nothing went wrong at first glance, but when we’d take a look at the pods, we’d see this:
[email protected]:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-6c76ffbdd7-z4qgf 0/1 InvalidImageName 0 3s
No pods get launched, with an error status showing InvalidImageName. That’s because our chart, without a repository name provided in the values.yaml file, generated the incomplete image name image: ":1.16.0"
instead of what it was before, image: "nginx:1.16.0"
. So what we need here is a simple way for our chart to have some default values it can fall back on, in case the users don’t provide anything in their values.yaml file. The logic would be If the user provided something in his values.yaml file, then use that. If he didn’t, then use this default value: ‘nginx’. We can do such a thing by using functions.
Functions
Using functions in a chart is straightforward. Let’s open up our deployment.yaml template file and see one function in action.
In our template, we generate the container name by fetching the chart’s name. So basically, the container will be named exactly like the chart is named. But what if this name has to respect some kind of convention? For example, maybe we need the container name to be in all capital letters. We can use the upper function to take a text value and transform it to all capital letters, so nginx or Nginx would become NGINX when passed through this function.
Scroll down to the containers section in this file and add the function upper before .Chart.Name
. The final content should look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-nginx
labels:
app: nginx
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: {{ upper .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
If we now check the manifest that our chart would generate after saving the file:
helm template ./nginx
we see this:
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: NGINX
image: ":1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
The container name is in all capital letters now.
Generally speaking, you should be thinking about functions whenever you need to transform the data that you are fetching to your chart. However, it’s not limited to this scenario as there are many kinds of functions and some can even generate data from nothing. For example, if you want to add a timestamp to an object you generate in Kubernetes, you can use the now
function to add the current datetime to any location in the manifest.
For example, the template file could have a line like:
timeLaunched: {{ now }}
And the manifest generated by this chart would look like this:
timeLaunched: 2021-06-08 08:38:50.701447903 -0400 EDT m=+0.049411442
There’s a pretty big list of useful functions we have available and this page is a good starting point to get familiar with the ones you might need: https://helm.sh/docs/chart_template_guide/function_list/.
Pipelines
But let’s get back to what we actually needed here: using default values if the user does not provide any in his values.yaml file.
In the process, we’ll also learn that we can use functions in a different way, through pipelines.
Pipelines in Helm charts are pretty similar to pipelines you might have seen or used in a Linux environment’s command line.
For example, to print abcd on the screen we would use a simple command such as
echo "abcd"
But if we want to take the output of this command and then process it in some way, in another command, we can use a pipeline. In this case, we take the normal output of the echo command, which would be abcd, pass it to the tr
command which processes it, transforms all letters to uppercase and outputs the final result
[email protected]:~$ echo "abcd" | tr a-z A-Z
ABCD
Syntax and logic for how we use pipelines in Helm charts are very similar.
Let’s open our deployment.yaml and see.
First of all, let’s edit the line name: {{ upper .Chart.Name }}
. In this form, we are basically saying, use this function, upper, on this fetched value, .Chart.Name
. But with pipelines, we’re going to say “Ok, we extracted a piece of text here, from .Chart.Name
. Now take this piece of text and send it to this function here, upper.
Our name: {{ upper .Chart.Name }}
line becomes
name: {{ .Chart.Name | upper }}
(this is actually an invalid name for a container, as it isn’t allowed to name it with all uppercase letters, but we’re just testing things out and we’ll revert this change after we see its effect)
Now let’s continue and also solve our earlier problem. We saw that without an image repository value defined in the values.yaml file, our chart fails at install.
Let’s make it use a default repository name, in case the user does not provide any value in the values.yaml file.
We’ll modify the line
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
and turn it to
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag }}"
It’s worth noting that it’s important to wrap nginx in “” quote marks. We can’t write default nginx
, we need to write default "nginx"
otherwise Helm’s interpreter would think nginx is the name of some function and throw an error like Error: parse error at (nginx/templates/deployment.yaml:19): function “nginx” not defined.
Now our final deployment.yaml file should look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-nginx
labels:
app: nginx
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: {{ .Chart.Name | upper }}
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
And now if we take a look at the manifest this chart would generate with helm template ./nginx
we see this output:
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: NGINX
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
So we see the line image: "nginx:1.16.0"
where the nginx value has magically reappeared, even though our current values.yaml file does not currently provide any repository name.
replicaCount: 1
image:
repository:
pullPolicy: IfNotPresent
tag: "1.16.0"
Now let’s open up the deployments template file and remove the upper function pipeline so that our chart generates a valid container name.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-nginx
labels:
app: nginx
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
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
Checkout the Helm for the Absolute Beginners course here
Checkout the Complete Kubernetes learning path here