Helm from basics to advanced — part II

Wednesday, June 12th, 2019
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.
If you are looking for a place to securely store your Helm charts, remember that Banzai Cloud runs a free Helm Chart repository as a service. We store, scan and serve your Helm charts in a secure way; the free tier includes GitHub, GitLab and Bitbucket authentication and has a generous fair use policy.
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"' } }
A raw string constant. Where you can use special characters freely.
{{`"output"`}}
A function call. The printf
parameters are identical to
fmt.Printf
from Go.
{ { printf "%q" "output" } }
A function call whose final argument comes from the previous command. ```yaml {{"output" | printf "%q"}}
A parenthesized argument.
```yaml
{{printf "%q" (print "out" "put")}}
A more elaborate call.
{{"put" | printf "%s%s" "out" | printf "%q"}}
A longer chain.
{{"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.
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