Helm

Running instance of a chart with a specific config is called a release.

Helm tracks an installed chart in the Kubernetes cluster using releases. This allows us to install a single chart multiple times with different releases in a cluster. Releases are stored as Secrets by default in the namespace of the release directly.

We can share charts as archives through repositories. It is basically a location where packages charts can be stored and shared. There is a distributed community chart repository by the name Artifact Hub where we can collaborate. We can also create our own private chart repositories. We can add any number of chart repositories to work with.

Example

helm create hello-world

The resulting structure will be

hello-world /ram
  Chart.yaml. <= This is the main file that contains the description of our chart
  values.yaml <= Default values for our chart 
  templates / <= Where Kubernetes resources are defined as templates
  charts /    <= Optional directory that may contain sub-charts
  .helmignore <= Define patterns to ignore when packaging

Template language

🔗 Official reference

How to run below experiment: in generated chart by command helm create remove all files from templates folder and add only below content to file templates/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"

{{ .Release.Name }} The values that are passed into a template can be thought of as namespaced objects, where a dot (.) separates each namespaced element. The leading dot before Release indicates that we start with the top-most namespace for this scope. So we could read .Release.Name as "start at the top namespace, find the Release object, then look inside of it for an object called Name".

Chart local render

helm install --debug --dry-run goodly-guppy-fake-name ./YOUR_CHART_FOLDER

If rendering fails try to use following command

helm install --dry-run --disable-openapi-validation moldy-jaguar ./mychart

There are a few commands that can help you debug.

  • helm lint is your go-to tool for verifying that your chart follows best practices

  • helm template --debug will test rendering chart templates locally.

  • helm install --dry-run --debug will also render your chart locally without installing it, but will also check if conflicting resources are already running on the cluster. Setting --dry-run=server will additionally execute any lookup in your chart towards the server.

  • helm get manifest: This is a good way to see what templates are installed on the server.

Built-in objects

🔗More details. This can be references as e.g. {{ .Release.Name }}

  • Release

  • Values. Empty by default, comes from values.yaml file and from user-supplied files

  • Chart: The contents of the Chart.yaml file

  • Subcharts

  • Files

  • Capabilities: This provides information about what capabilities the Kubernetes cluster supports.

  • Template: Contains information about the current template that is being executed

The built-in values always begin with a capital letter.

Values files

This object provides access to values passed into the chart. Its contents come from multiple sources (in order of precedence)

  • The values.yaml file in the chart

  • If this is a subchart, the values.yaml file of a parent chart

  • A values file is passed into helm install or helm upgrade with the -f flag (helm install -f myvals.yaml ./mychart)

  • Individual parameters are passed with --set (such as helm install --set foo=bar ./mychart)

Assuming values.yaml has following content

favoriteDrink: coffee
favorite:
  food: pizza

We can use it inside of the template

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}
  food: {{ .Values.favorite.food }}

If you need to delete a key from the default values, you may override the value of the key to be null

Template Functions and Pipelines

Template functions follow the syntax functionName arg1 arg2...

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ quote .Values.favorite.drink }}

Helm has over 60 available functions. Some of them are defined by the Go template language itself. Most of the others are part of the Sprig template library.

Pipelines

Like in Unix

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | repeat 5 | quote }}
  food: {{ .Values.favorite.food | upper | quote }}

Results in

  drink: "coffeecoffeecoffeecoffeecoffee"
  food: "PIZZA"

There is a useful command default

drink: {{ .Values.favorite.drink | default "tea" | quote }}

the default command is perfect for computed values, which cannot be declared inside values.yaml. For example:

drink: {{ .Values.favorite.drink | default (printf "%s-tea" (include "fullname" .)) }}

Flow Control

Helm's template language provides the following control structures:

  • if/else for creating conditional blocks

  • with to specify a scope

  • range, which provides a "for each"-style loop

In addition to these, it provides a few actions for declaring and using named template segments:

  • define declares a new named template inside of your template

  • template imports a named template

  • block declares a special kind of fillable template area

if else

{{ if PIPELINE }}
  # Do something
{{ else if OTHER PIPELINE }}
  # Do something else
{{ else }}
  # Default case
{{ end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  {{ if eq .Values.favorite.drink "coffee" }}mug: "true"{{ end }}

Controlling Whitespace

When the template engine runs, it removes the contents inside of {{ and }}, but it leaves the remaining whitespace exactly as is.

The curly brace syntax of template declarations can be modified with special characters to tell the template engine to chomp whitespace. {{- (with the dash and space added) indicates that whitespace should be chomped left, while -}} means whitespace to the right should be consumed. Be careful! Newlines are whitespace!

Lets substitute an * for each whitespace that will be deleted following this rule. An * at the end of the line indicates a newline character that would be removed

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  food: {{ .Values.favorite.food | upper | quote }}*
**{{- if eq .Values.favorite.drink "coffee" }}
  mug: "true"*
**{{- end }}

The result will be

apiVersion: v1
kind: ConfigMap
metadata:
  name: clunky-cat-configmap
data:
  food: "PIZZA"
  mug: "true"

Sometimes it's easier to tell the template system how to indent for you instead of trying to master the spacing of template directives. For that reason, you may sometimes find it useful to use the indent function ({{ indent 2 "mug:true" }}).

with

{{ with PIPELINE }}
  # restricted scope
{{ end }}

Scopes can be changed. with can allow you to set the current scope (.) to a particular object. For example, we've been working with .Values.favorite. Let's rewrite our ConfigMap to alter the . scope to point to .Values.favorite:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }} <= set scope to .Values.favorite
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  release: {{ .Release.Name }} <= this will fail as scope is unreachable
  release: {{ $.Release.Name }} <= this will work as $ is mapped to the root scope
  {{- end }}

The block after with only executes if the value of PIPELINE is not empty.

range

Assuming values.yaml

pizzaToppings:
  - mushrooms
  - cheese
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  toppings: |-
    {{- range .Values.pizzaToppings }}
    - {{ . | title | quote }}
    {{- end }}    

range changes the scope, thats why we access the value by . in - {{ . | title | quote }} Note. title (title case function)

The result will be

apiVersion: v1
kind: ConfigMap
metadata:
  name: edgy-dragonfly-configmap
data:
  toppings: |-
    - "Mushrooms"
    - "Cheese"

toppings: |- line is declaring a multi-line string. So our list of toppings is actually not a YAML list. It's a big string. Why would we do this? Because the data in ConfigMaps data is composed of key/value pairs, where both the key and the value are simple strings

Variables

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- $relname := .Release.Name -}} <= variable is assigned
  {{- with .Values.favorite }}
  food: {{ .food | upper | quote }}
  release: {{ $relname }} 
  {{- end }}

Variable values does not respect the present scope, so we can use .Release.Name inside of scope .Values.favorite

Variables are particularly useful in range loops. They can be used on list-like objects to capture both the index and the value:

  toppings: |-
    {{- range $index, $topping := .Values.pizzaToppings }}
      {{ $index }}: {{ $topping }}
    {{- end }}    

Note that range comes first, then the variables, then the assignment operator, then the list. This will assign the integer index (starting from zero) to $index and the value to $topping. Running it will produce:

  toppings: |-
      0: mushrooms
      1: cheese
      2: peppers
      3: onions     

For data structures that have both a key and a value, we can use range to get both. For example, we can loop through .Values.favorite like this:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

Now on the first iteration, $key will be drink and $val will be coffee, and on the second, $key will be food and $val will be pizza. Running the above will generate this:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: eager-rabbit-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

There is one variable that will always point to the root context: - $ -. This can be very useful when you are looping in a range and you need to know the chart's release name.

An example illustrating this:

{{- range .Values.tlsSecrets }}
---
apiVersion: v1
kind: Secret
metadata:
  name: {{ .name }}
  labels:
    # Many helm templates would use `.` below, but that will not work,
    # however `$` will work here
    app.kubernetes.io/name: {{ template "fullname" $ }}
    # I cannot reference .Chart.Name, but I can do $.Chart.Name
    helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}"
    app.kubernetes.io/instance: "{{ $.Release.Name }}"
    # Value from appVersion in Chart.yaml
    app.kubernetes.io/version: "{{ $.Chart.AppVersion }}"
    app.kubernetes.io/managed-by: "{{ $.Release.Service }}"
type: kubernetes.io/tls
data:
  tls.crt: {{ .certificate }}
  tls.key: {{ .key }}
{{- end }}

Named templates

Files whose name begins with an underscore (_) are assumed to not have a k8smanifest inside. These files are not rendered to Kubernetes object definitions, but are available everywhere within other chart templates for use.

Define and template

The define action allows us to create a named template inside of a template file. Its syntax goes like this:

{{- define "MY.NAME" }}
  # body of template here
{{- end }}

For example, we can define a template to encapsulate a Kubernetes block of labels:

{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}

Now we can embed this template inside of our existing ConfigMap, and then include it with the template action:

{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" }}

Conventionally, Helm charts put these templates inside of a partials file, usually _helpers.tpl. Let's move this function there:

{{/* Generate basic labels */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
    chart: {{ .Chart.Name }}
{{- end }}

By convention, define functions should have a simple documentation block ({{/* ... */}}) describing what they do.

Scopes in template

When a named template (created with define) is rendered, it will receive the scope passed in by the template call. In our example, we included the template like this:

{{- template "mychart.labels" }}

No scope was passed in, so within the template we cannot access anything in .. This is easy enough to fix, though. We simply pass a scope to the template:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" . }}

include function

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.

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
{{ include "mychart.app" . | indent 4 }} <= include can be used with pipelines
data:
{{ include "mychart.app" . | indent 2 }}

Last updated

Was this helpful?