The past two months I have been actively migrating all my services from dedicated servers to Kubernetes cluster. I have done the containerisation of the existing applications and have written the Kubernetes manifests. However, I was missing a way to configure, release, version, rollback and inspect the deployments. This is when I have discovered Helm.

Lets keep things simple.

Key definitions:

Helm is a chart manager.

is a chart manager. Charts are packages of pre-configured Kubernetes resources.

are packages of pre-configured Kubernetes resources. Release is a collection of Kubernetes resources deployed to the cluster using Helm.

Helm is used to:

Make configurable releases

Upgrade, delete, inspect releases made using Helm

Helm is made of two components:

helm client. Used to create, fetch, search and validate charts and to instruct tiller.

client. Used to create, fetch, search and validate charts and to instruct tiller. tiller server. Runs inside the Kubernetes cluster and manages the releases.

Install Helm

macOS users can install Helm client using Homebrew.

brew install kubernetes-helm

Use helm client to deploy tiller to the Kubernetes cluster:

helm init uses the ~/.kube/config configuration to connect to the Kubernetes cluster. Ensure that your configuration is referencing a cluster that is safe to make test deployments.

helm init

Instructions for other platforms, refer to https://github.com/kubernetes/helm/blob/master/docs/quickstart.md.

Define a Chart

Helm documentation includes a succinct guide to defining Helm charts. I recommend reading the guide in full. However, I will guide you through a “Hello, World!” example.

A chart is organized as a collection of files inside of a directory. The directory name is the name of the chart.

$ mkdir ./hello-world

$ cd ./hello-world

A chart must include a chart definition file, Chart.yaml . Chart definition file must define two properties: name and version (Semantic Versioning 2).

$ cat <<'EOF' > ./Chart.yaml

name: hello-world

version: 1.0.0

EOF

A chart must define templates used to generate Kubernetes manifests, e.g.

$ mkdir ./templates

$ cat <<'EOF' > ./templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: hello-world

spec:

replicas: 1

template:

metadata:

labels:

app: hello-world

spec:

containers:

- name: hello-world

image: gcr.io/google-samples/node-hello:1.0

ports:

- containerPort: 8080

protocol: TCP

EOF $ cat <<'EOF' > ./templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: hello-world

spec:

type: NodePort

ports:

- port: 8080

targetPort: 8080

protocol: TCP

selector:

app: hello-world

EOF

That is all thats required to make a release.

Release, list, inspect, delete, rollback, purge

Use helm install RELATIVE_PATH_TO_CHART to make a release.

$ helm install .

NAME: cautious-shrimp

LAST DEPLOYED: Thu Jan 5 11:32:04 2017

NAMESPACE: default

STATUS: DEPLOYED RESOURCES:

==> v1/Service

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

hello-world 10.0.0.175 <nodes> 8080:31419/TCP 0s ==> extensions/Deployment

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

hello-world 1 1 1 0 0s

helm install . used Kubernetes manifests in ./templates directory to create a deployment and a service:

$ kubectl get po,svc

NAME READY STATUS RESTARTS AGE

po/hello-world-52480365-gntxz 1/1 Running 0 1m

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

svc/hello-world 10.0.0.175 <nodes> 8080/TCP 1m

svc/kubernetes 10.0.0.1 <none> 443/TCP 6d

Use helm ls to list deployed releases:

$ helm ls

NAME REVISION UPDATED STATUS CHART

cautious-shrimp 1 Thu Jan 5 11:32:04 2017 DEPLOYED hello-world-1.0.0

Use helm status RELEASE_NAME to inspect a particular release:

$ helm status cautious-shrimp

LAST DEPLOYED: Thu Jan 5 11:32:04 2017

NAMESPACE: default

STATUS: DEPLOYED RESOURCES:

==> v1/Service

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

hello-world 10.0.0.175 <nodes> 8080:31419/TCP 6m ==> extensions/Deployment

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

hello-world 1 1 1 1 6m

Use helm delete RELEASE_NAME to remove all Kubernetes resources associated with the release:

$ helm delete cautious-shrimp

Use helm ls --deleted to list deleted releases:

helm ls --deleted

NAME REVISION UPDATED STATUS CHART

cautious-shrimp 1 Thu Jan 5 11:32:04 2017 DELETED hello-world-1.0.0

Use helm rollback RELEASE_NAME REVISION_NUMBER to restore a deleted release:

$ helm rollback cautious-shrimp 1

Rollback was a success! Happy Helming!

$ helm ls

NAME REVISION UPDATED STATUS CHART

cautious-shrimp 2 Thu Jan 5 11:47:57 2017 DEPLOYED hello-world-1.0.0

$ helm status cautious-shrimp

LAST DEPLOYED: Thu Jan 5 11:47:57 2017

NAMESPACE: default

STATUS: DEPLOYED RESOURCES:

==> v1/Service

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE

hello-world 10.0.0.42 <nodes> 8080:32367/TCP 2m ==> extensions/Deployment

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

hello-world 1 1 1 1 2m

Use helm delete --purge RELEASE_NAME to remove all Kubernetes resources associated with with the release and all records about the release from the store.

$ helm delete --purge cautious-shrimp

$ helm ls --deleted

That was truly satisfying!

A clean release, inspection, removal, rollback and purge.

Configuring releases

I have shown you how to manage a life cycle of a Helm release. That was cool. However, that is not even the reason why I started looking into Helm: I needed a tool to configure releases.

Helm Chart templates are written in the Go template language, with the addition of 50 or so add-on template functions from the Sprig library and a few other specialized functions.

Values for the templates are supplied using values.yaml file, e.g.

$ cat <<'EOF' > ./values.yaml

image:

repository: gcr.io/google-samples/node-hello

tag: '1.0'

EOF

A generic warning about YAML configuration. Be careful to quote numeric values, e.g. if in the above example I have written tag: 1.0 (without quotes) it would have been interpreted as a number, i.e. 1 .

Values that are supplied via a values.yaml file are accessible from the .Values object in a template.

$ cat <<'EOF' > ./templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: hello-world

spec:

replicas: 1

template:

metadata:

labels:

app: hello-world

spec:

containers:

- name: hello-world

image: {{ .Values.image.repository }}:{{ .Values.image.tag }}

ports:

- containerPort: 8080

protocol: TCP

EOF

Values in the values.yaml can be overwritten at the time of making the release using --values YAML_FILE_PATH or --set key1=value1,key2=value2 parameters, e.g.

$ helm install --set image.tag='latest' .

Values provided using --values parameter are merged with values defined in values.yaml file and values provided using --set parameter are merged with the resulting values, i.e. --set overwrites provided values of --value , --value overwrites provided values of values.yaml .

Debugging

Using templates to generate the manifests require to be able to preview the result. Use --dry-run --debug options to print the values and the resulting manifests without deploying the release:

$ helm install . --dry-run --debug --set image.tag=latest

Created tunnel using local port: '49636'

SERVER: "localhost:49636"

CHART PATH: /Users/gajus/Documents/dev/gajus/hello-world

NAME: eyewitness-turkey

REVISION: 1

RELEASED: Thu Jan 5 13:02:49 2017

CHART: hello-world-1.0.0

USER-SUPPLIED VALUES:

image:

tag: latest COMPUTED VALUES:

image:

repository: gcr.io/google-samples/node-hello

tag: latest HOOKS:

MANIFEST: ---

# Source: hello-world/templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: hello-world

spec:

type: NodePort

ports:

- port: 8080

targetPort: 8080

protocol: TCP

selector:

app: hello-world ---

# Source: hello-world/templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: hello-world

spec:

replicas: 1

template:

metadata:

labels:

app: hello-world

spec:

containers:

- name: hello-world

image: gcr.io/google-samples/node-hello:latest

ports:

- containerPort: 8080

protocol: TCP

Using predefined values

In addition to user supplied values, there is a number of predefined values:

.Release is used to refer to the resulting release values, e.g. .Release.Name , .Release.Time .

is used to refer to the resulting release values, e.g. , . .Chart is used to refer to the Chart.yaml configuration.

is used to refer to the configuration. .Files is used to refer to the files in the chart directory.

I am using the predefined values to create labels that enable inspection of the Kubernetes resources:

$ cat <<'EOF' > ./templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

labels:

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

version: {{ .Chart.Version }}

release: {{ .Release.Name }}

spec:

replicas: 1

template:

metadata:

labels:

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

version: {{ .Chart.Version }}

release: {{ .Release.Name }}

spec:

containers:

- name: hello-world

image: {{ .Values.image.repository }}:{{ .Values.image.tag }}

ports:

- containerPort: 8080

protocol: TCP

EOF

$ cat <<'EOF' > ./templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

labels:

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

version: {{ .Chart.Version }}

release: {{ .Release.Name }}

spec:

type: NodePort

ports:

- port: 8080

targetPort: 8080

protocol: TCP

selector:

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

EOF

Note, that it is your responsibility to ensure that all resources have a unique name and labels. In the above example, I am using .Release.Name and .Chart.Name to create a resource name.

{{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

Note: Kubernetes resource labels and names are restricted to 63 characters. http://kubernetes.io/docs/user-guide/labels/#syntax-and-character-set

Partials

You have probably noticed a repeating pattern in the templates:

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

version: {{ .Chart.Version }}

release: {{ .Release.Name }}

We are using the same labels for all resources.

Furthermore, our resource name is a lengthy expression:

{{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

This repetition becomes even more apparent in large applications, with dozens of different resources. Lets see what we can do about it.

Lets create a _helpers.tpl file and use it to declare partials that we can later import into the templates:

Files in ./templates directory that start with _ are not considered Kubernetes manifests. The rendered version of these files are not sent to Kubernetes.

$ cat <<'EOF' > ./templates/_helpers.tpl

{{- define "hello-world.release_labels" }}

app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }}

version: {{ .Chart.Version }}

release: {{ .Release.Name }}

{{- end }} {{- define " hello-world. full_name" -}}

{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 -}}

{{- end -}}

EOF

Now we have two partials: hello-world.release_labels and hello-world.full_name that we can use in the templates:

Template names are global. Because templates in subcharts are compiled together with top-level templates, you should be careful to name your templates with chart-specific names. This is the reason I am using Chart name to prefix template names.

$ cat <<'EOF' > ./templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: {{ template "hello-world.full_name" . }}

labels:

{{- include "hello-world.release_labels" . | indent 4 }}

spec:

replicas: 1

template:

metadata:

labels:

{{- include "hello-world.release_labels" . | indent 8 }}

spec:

containers:

- name: hello-world

image: {{ .Values.image.repository }}:{{ .Values.image.tag }}

ports:

- containerPort: 8080

protocol: TCP

EOF

$ cat <<'EOF' > ./templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: {{ template "hello-world.full_name" . }}

labels:

{{- include "hello-world.release_labels" . | indent 4 }}

spec:

type: NodePort

ports:

- port: 8080

targetPort: 8080

protocol: TCP

selector:

app: {{ template "hello-world.full_name" . }}

EOF

Notice that I sometimes use template and sometimes include to include a partial. Read the named templates guide to learn the difference.

One last time, lets print the resulting manifest:

$ helm install . --dry-run --debug

Created tunnel using local port: '50655'

SERVER: "localhost:50655"

CHART PATH: /Users/gajus/Documents/dev/gajus/hello-world

NAME: kindred-deer

REVISION: 1

RELEASED: Thu Jan 5 14:46:41 2017

CHART: hello-world-1.0.0

USER-SUPPLIED VALUES:

{} COMPUTED VALUES:

image:

repository: gcr.io/google-samples/node-hello

tag: "1.0" HOOKS:

MANIFEST: ---

# Source: hello-world/templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: kindred-deer-hello-world

labels:

app: kindred-deer-hello-world

version: 1.0.0

release: kindred-deer

spec:

type: NodePort

ports:

- port: 8080

targetPort: 8080

protocol: TCP

selector:

app: kindred-deer-hello-world ---

# Source: hello-world/templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: kindred-deer-hello-world

labels:

app: kindred-deer-hello-world

version: 1.0.0

release: kindred-deer

spec:

replicas: 1

template:

metadata:

labels:

app: kindred-deer-hello-world

version: 1.0.0

release: kindred-deer

spec:

containers:

- name: hello-world

image: gcr.io/google-samples/node-hello:1.0

ports:

- containerPort: 8080

protocol: TCP

Isn’t this awesome?