Banzai Cloud is now part of Cisco

Banzai Cloud Logo Close
Home Products Benefits Blog Company Contact

The content of this page hasn't been updated for years and might refer to discontinued products and projects.

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!" }}

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
  name: peeking-cardinal-test
{{ 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
  name: peeking-cardinal-test
{{ .Values.annotations | indent 4 }}

Note: Use indent or even better nindent to avoid manually spacing. nindent is almost identical to indent, but begins a new line. It may seem unnecessary, but look how much more readable it makes the file.

# nindent
  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.

{{ | default  (include "" .) }}

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.

A raw string constant. Where you can use special characters freely.
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.
{{"output" | printf "%q"}}
A parenthesized argument.
{{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
  - from: example
  - from: another-example

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 }}
  {{- range .Values.config.policyFile }}
  - {{ .from }}.{{ $rootDomain }}
  {{- end }}

Which renders to this:

# rendered

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
       labels: {{ include "" . }} {{ .Release.Name }}
       {{ if .Values.annotations }}
 {{ toYaml .Values.annotations | indent 8 }}
       {{ end }}
# rendered output
      labels: test mouthy-zebra


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
       labels: {{ include "" . }} {{ .Release.Name }}
       {{- if .Values.annotations }}
 {{ toYaml .Values.annotations | indent 8 }}
       {{- end }}
# rendered output
      labels: test flabby-badger

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" }}
    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.


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: "{{ }}"
name: "Tom"

# template
{{ tpl .Values.template . }}

# output

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
# values.yaml
clientId: AAAA

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 }}

apiVersion: v1
 kind: Secret
   name: mysecret
 type: Opaque
   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 the templates 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