This is the second part of a very popular post, Helm from basics to advanced. In the previous post (we highly suggest you read it, if you haven’t done so already) we covered Helm’s basics, and finished with an examination of design principles. In this post, we’d like to continue our discussion of Helm by exploring best practices and taking a look at some common mistakes.
Before we start, if you really want to get deep down into the nitty gritty of Helm, I suggest you to read the official guide to creating templates. Now, let’s proceed, and run through some relatively simple but lesser known features.
Removing a default key π︎
There are many cases in which default values just donβt fit a deployment. To eliminate these unwanted values, simply set them to null.
helm install stable/nginx-controller --set ingress.hosts=null
Required parameters π︎
If there is no optimal
default value, but such a value is necessary for your deployment, you can use the required
function.
{{ required "A valid foo is required!" .Values.foo }}
Using pipelines π︎
When using yaml, you have to be extra cautious with new lines and indentation. To help with that, use functions like indent
.
# indent
metadata:
name: peeking-cardinal-test
annotations:
{{ indent 4 .Values.annotations }}
In a Helm template — and, in general, in Golang templates — we can use different filters to construct a pipeline. Pipelines are basically a chain of function calls, called commands, where the input (the final argument) of each filter comes from the output of the previous one. These pipelines can make your templates simpler and more readable; see below for some examples of how to use them.
# indent
metadata:
name: peeking-cardinal-test
annotations:
{{ .Values.annotations | indent 4 }}
Note: Use
indent
or even betternindent
to avoid manually spacing.nindent
is almost identical toindent
, but begins a new line. It may seem unnecessary, but look how much more readable it makes the file.
# nindent
metadata:
name: peeking-cardinal-test
annotations: {{ .Values.annotations | nindent 4 }}
Another good example using a pipeline is the default
function. If your default values do not originate from values.yaml
, you can use it to provide computed values as defaults.
{{ .Values.name | default (include "chart.name" .) }}
Notice how the brackets around the include call ensure the order of the execution
A bit more about Go templates π︎
To better understand function calls and pipelines, here is a full example from the Go documentation. The following code will guide you, step-by-step, to a better understanding of how functions work within the Go template. All of the following examples produce the quoted word "output"
:
A string constant.
{{"\"output\""}}
{{`"output"`}}
printf
parameters are identical to fmt.Printf
from Go.
{{printf "%q" "output"}}
{{"output" | printf "%q"}}
{{printf "%q" (print "out" "put")}}
{{"put" | printf "%s%s" "out" | printf "%q"}}
{{"output" | printf "%s" | printf "%q"}}
Scope of the arguments π︎
When you are writing complex structures like iterations
you should be aware of the arguments scope
. Up to this point we only used arguments and function in the main
scope represented with .
(dot). So when you write .Values.config
it means you are using the main
scope and Helm puts all your parameters from values.yaml
under the Values
argument. Function calls like range
or with
narrows this scope to a local context. The following example illustrates how scopes work.
# values.yaml
config:
rootDomain: rootdomain.com
policyFile:
- from: example
to: httpbin.org
- from: another-example
to: httpbin.org
This (above) values.yaml contains a config.policyFile
which is a list
of objects. These objects have two attributes namely: from
and to
. There is another string variable config.rootDomain
. We want to iterate through this list and suffix the from
parameters with rootDomain
. Lets check the following snippet:
# ingress.yaml
...
{{- $rootDomain := .Values.config.rootDomain }}
hosts:
{{- range .Values.config.policyFile }}
- {{ .from }}.{{ $rootDomain }}
{{- end }}
...
Which renders to this:
# rendered
hosts:
- example.rootdomain.com
- another-example.rootdomain.com
You may notice that we declared a variable $rootDomain
before the range
invocation. The reason behind this is that we can’t reach outer scope within the range
execution. However you can use these variables similar to any argument. This means that you can get child parameters like in the following example:
# variable example
{{ $config := .Values.config }}
...
{{ $config.rootDomain }}
The with
statement works similarly. Take a look at the following examples:
{{with "output"}}{{printf "%q" .}}{{end}}
A with action using dot.
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
A with action that creates and uses a variable.
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
A with action that uses a variable in another action.
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
The same, but pipelined.
Controlling whitespace π︎
In an application deployment there are lots of control statements like if
and range
. Again, because we’re rendering yaml files, it’s very important that indentations and newlines be put in exactly the right places. Some of you may know where I’m going with all this; let’s take a look.
# deployment.yaml
...
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "test.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
{{ if .Values.annotations }}
{{ toYaml .Values.annotations | indent 8 }}
{{ end }}
spec:
# rendered output
template:
metadata:
labels:
app.kubernetes.io/name: test
app.kubernetes.io/instance: mouthy-zebra
annotations:
spec:
Note how there is a superfluous empty line after the annotations
key. Wondering why? Because the rendering engine removes the contents inside brackets — {{ }}
— but the preceding whitespace and the subsequent newline remain. In many cases it doesn’t make a difference in the meaning — yaml allows redundant and repeated white spaces at many places, but it is a common practice in the Helm community to generate a terse markup to make the output easier to read and to avoid cases where the difference causes invalid markup, or a valid one with a non-trivially different meaning.
Fortunately, Helm helps remedy this with a special whitespace manipulator, the -
or hyphen. Using it is simple. If you want to chomp the preceding whitespace you simply write {{-
, and if you want to chomp the ensuing whitespace use -}}
.
# deployment.yaml
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "test.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
{{- if .Values.annotations }}
{{ toYaml .Values.annotations | indent 8 }}
{{- end }}
spec:
# rendered output
template:
metadata:
labels:
app.kubernetes.io/name: test
app.kubernetes.io/instance: flabby-badger
annotations:
spec:
Make sure there is a space between the
-
and the rest of your directive.{{- 3 }}
translates to βremove a whitespace to the left and print ‘3’β, while{{-3}}
means βprint ‘-3’β. (from Helm documentation)
Define templates π︎
Don’t forget that you can define your own templates like the ones generated in _helpers.tpl
.
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
When do I use include
and when do I use template
π︎
There are several charts in which both include
and template
appear. They do the same job, which is to render a predefined template. However, the preferred way of using include
is described in the Helm documentation, thusly:
“Because template is an action, and not a function, there is no way to pass the output of a template call to other functions; the data is simply inserted inline.”
Templates in templates π︎
You may think we’ve been through the tough stuff, but, fortunately (or unfortunately), that’s not the case; let’s take a look at templates embedded in templates.
{{ tpl TEMPLATE_STRING VALUES }}
The tpl
function evaluates its first argument as a template in the context of its second argument, and returns the rendered result. Simple as that. Now let’s look at an example from the Helm documentation.
# values
template: "{{ .Values.name }}"
name: "Tom"
# template
{{ tpl .Values.template . }}
# output
Tom
Exercise π︎
To summarise let’s perform a small exercise:
Here, we have an application with a toml configuration. We want to generate this file based on the values in values.yaml. However, there are some credentials in this file, so we want to store it as a Kubernetes secret.
# Snippet from the app.conf
[github]
clientId=XXXXX
clientSecret=XXXXXXXXXXXXXXXXXXX
# values.yaml
clientId: AAAA
clientSecret: BBBBBBBBBBBBBBB
Hint: you can read the contents of a file in the Chart with the
Files.Get
function.
Here’s a possible solution.
#chart/app.conf
[github]
clientId={{ .Values.clientId }}
clientSecret={{ .Values.clientSecret }}
#chart/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
config.cfg: {{ tpl (.Files.Get "app.conf") . | b64enc }}
As you see, the trick is to get the TEMPLATE_STRING
from a file with .Files.Get
and pass all values with the .
argument. After that we just need to encode its output with base64
as Kubernetes specification requires.
Note: File paths are relative to the chart
root
directory and you can NOT import files from thetemplates
directory.
Thank you for reading the second part of our Helm blog. Stay tuned, because the next article of the series will cover Helm 3 and some of the exciting changes it will bring.
Learn more about Helm:
About Banzai Cloud π︎
Banzai Cloud is changing how private clouds are built, simplifying the development, deployment, and scaling of complex applications, and bringing the full power of Kubernetes and Cloud Native technologies to developers and enterprises everywhere.
#multicloud #hybridcloud #BanzaiCloud