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
Helm makes use of the Go template language and extends that to something called Helm template language. During the evaluation, every file inside the template directory is submitted to the template rendering engine. This is where the template directive injects actual values into the templates.
Template language
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 practiceshelm 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 anylookup
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 filesChart: The contents of the
Chart.yaml
fileSubcharts
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 chartIf this is a subchart, the
values.yaml
file of a parent chartA values file is passed into
helm install
orhelm upgrade
with the-f
flag (helm install -f myvals.yaml ./mychart
)Individual parameters are passed with
--set
(such ashelm 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 blockswith
to specify a scoperange
, 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 templatetemplate
imports a named templateblock
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
template names are global. If you declare two templates with the same name, whichever one is loaded last will be the one used. Because templates in subcharts are compiled together with top-level templates, you should be careful to name your templates with chart-specific names.
One popular naming convention is to prefix each defined template with the name of the chart: {{ define "mychart.labels" }}
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?