Outshift Logo


9 min read

Blog thumbnail
Published on 06/12/2019
Last updated on 02/05/2024

Helm from basics to advanced — part II


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. chart-repo-service 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.
# nindent

metadata: name: peeking-cardinal-test annotations:
{{ .Values.annotations | nindent 4 }}
Note: Useindentor even betternindentto avoid manually spacing.nindentis 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.
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.
{{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

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

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



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 theFiles.Getfunction.
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 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
Subscribe card background
Subscribe to
the Shift!

Get emerging insights on emerging technology straight to your inbox.

Unlocking Multi-Cloud Security: Panoptica's Graph-Based Approach

Discover why security teams rely on Panoptica's graph-based technology to navigate and prioritize risks across multi-cloud landscapes, enhancing accuracy and resilience in safeguarding diverse ecosystems.

the Shift
emerging insights
on emerging technology straight to your inbox.

The Shift keeps you at the forefront of cloud native modern applications, application security, generative AI, quantum computing, and other groundbreaking innovations that are shaping the future of technology.

Outshift Background