Banzai Cloud Logo Close
Home Products Benefits Blog Company Contact
Sign in
Author Sandor Guba

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 Charts repository as a service: charts.banzaicloud.io. 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 repository token creation

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

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

If you are interested in our technology and open source projects, follow us on GitHub, LinkedIn or Twitter:


Comments

comments powered by Disqus