We’re getting closer to the end of this series of Kubernetes-101 articles! In the summary of this article I will list the topics that are left to discuss before I close this series.
In this article we will take a look at Helm: a package manager for Kubernetes applications.
The TL;DR1 version of Helm is this: we construct a Helm chart and parametrize it using values that we specify in a values.yaml file, together they create the Kubernetes manifests that we apply in our Kubernetes cluster. This is illustrated in the following figure.
Helm Background#
This article will be hands-on. We will create a basic application consisting of a Service and a Deployment with three Pods. I will keep it this simple to focus on the basics of Helm, not the complications of an advanced application.
Helm is a package manager for applications running on Kubernetes. We can think of Helm as a way of converting our Kubernetes manifests into dynamic templates. When I say dynamic I mean that it is parametrized, but also that we can control things like what components to include using boolean flags, we can use if-else statements to change the application based on parameter values, and we can use loops to create more or less of certain resource types depending on values of other parameters.
Helm is ubiquitous in the world of Kubernetes, so it is a good idea to be familiar with the basics. It is also featured in the Certified Kubernetes Application Developer (CKAD) certification.
If you find a tool or application you would like to use, it is often the case that a Helm chart for it already exists, either from an official source or created by someone like you or me! Imagine that you would like to deploy a Postgres database to your Kubernetes cluster. You might search the web for Postgres Helm and you would likely end up at bitnami.com/stack/postgresql/helm which is a ready-to-use Helm chart for Postgres that bitnami has made available.
Apart from using pre-made Helm charts you can also create your own Helm charts for your own applications, and that is what we will do in the rest of this article.
How to Install Helm?#
I am using a mac, so I use Homebrew to install applications. Instructions for how to install it on a different operating system can be found in the official documentation for Helm. I open a terminal and run brew install helm
:
$ brew install helm
(output truncated)
==> Summary
🍺 /opt/homebrew/Cellar/helm/3.10.3: 64 files, 48.4MB
I can now verify that Helm is installed:
$ helm version --short
v3.10.3+g835b733
Construct Kubernetes Manifests#
To start off we will construct regular Kubernetes manifests for our application. This is usually where you start if you want to create a new Helm chart from the beginning. I will just show you the finished Kubernetes manifests, and afterwards I will discuss what they contain.
I put the Deployment manifest in deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.22.1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
I put the Service manifest in service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
We are familiar with these manifests from earlier articles (see Deployments and Services). The Deployment creates three replicas of our Nginx web server and the Service exposes the container port 80 as a NodePort
type of Service. We see a few things that are repeated a few times in the manifests. First of all the label app: nginx
appears four times, and the port number 80 also appears four times. Four times might not sound like much, but they are all opportunities for mistakes if and when we need to update the values. These are the kinds of things that Helm will help us with.
How to Structure a Helm Chart?#
An application package in Helm is called a Helm chart. When creating a Helm chart there is a file structure that you should follow:
$ tree .
.
├── Chart.yaml
├── charts
├── templates
│ ├── template1.yaml
│ └── template2.yaml
└── values.yaml
2 directories, 2 files
Chart.yaml
is the main file describing the chartcharts
is a directory that could contain other Helm charts called subcharts (I will not use it in this example)templates
is a directory that contains our actual templates that will turn into ready-to-use Kubernetes manifests when we run Helmvalues.yaml
contains values for parameters that will be inserted into the templates
How to Build a Helm Chart?#
To start building our Helm chart we can run helm create
:
$ helm create nginx-chart
Creating nginx-chart
This gives us a started chart with the following content:
$ cd nginx-chart
$ tree .
.
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
When you are new to Helm it is a good idea to look through all the generated files. They are filled with comments that explain what is going on. I want to create an even simpler example than what was generated for me, so I remove a few files until my directory contains the following:
$ tree .
.
├── Chart.yaml
├── templates
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml
1 directory, 5 files
This looks similar to what I described before, except for the file _helpers.tpl
. We’ll see what this file is soon.
Chart.yaml#
Let us start in Chart.yaml
, I edit it so it looks like this:
apiVersion: v2
name: nginx-chart
description: A Helm chart for a simple Nginx application
type: application
version: 1.0.0
appVersion: "1.22.1"
Let us go through the different properties in this file:
apiVersion
is the Helm chart API version, similar to the API version for a Kubernetes resource, it defines what properties we can specify in this filename
is the name of the Helm chart (no surprises there)description
is also self-explanatory, use it to provide a description of what the chart containstype
could be eitherapplication
orlibrary
application
is a collection of templates that define an application, just what we want to do in this examplelibrary
is a collection of utilities or functions that can be used in other Helm charts, but it does not contain any templates
version
specifies the version of the Helm chart itself, if you are developing your own Helm chart you should follow semantic versioningappVersion
is usually used for the version of the main application container that is used, in this example it will be the version of the Nginx containers
values.yaml#
Next we look at values.yaml
. This file is a collection of properties that we can use in our templates, the format of this file is up to us to decide, as long as it is valid YAML. I edit the sample file so that it looks like this:
nameOverride: ""
image:
repository: nginx
pullPolicy: IfNotPresent
tag: ""
service:
type: NodePort
port: 80
deployment:
replicaCount: 3
livenessProbe: true
I have a single root property nameOverride
which is blank. Then there are three blocks of properties.
image
contains properties for the container image.repository
is the repository where the image will be fetched from, I only specifynginx
as the value so the image will be fetched from the default location of Docker Hub.pullPolicy
specifies the policy for when the Docker image should be pulled, in this case I say that it should be pulled if it does not exist on the host machine.tag
specifies the image tag, in this case I leave it empty.
service
specifies properties related to the Service resource.deployment
specifies properties related to the Deployment resource.
_helpers.tpl#
_helpers.tpl
is, as the name suggests, a helper file. It contains a few reusable snippets that I can refer to from my templates. These snippets are copied from the starter-sample that Helm provided for me, because I saw no reason not to use them! I will go through the snippets one by one.
Helm uses a template language where we bake template directives into our YAML files. A template directive is enclosed in {{
and }}
blocks.
The first snippet is this:
{{- define "nginx-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
Let me go through this snippet line-by-line:
- The first line marks the start of this snippet and it defines the name of it. The name determines how I can refer to it in my templates. To use this snippet I will add a template directive like the following in a template:
{{ include "nginx-chart.name" . }}
. - The second line is the actual content of this snippet. What we see here is called a pipeline, there are three separate statements in a row, separated by the pipe symbol
|
.default .Chart.Name .Values.nameOverride
uses thedefault
function that takes two arguments, the first argument is a default value and the second is an override value. If the override values is set to an actual value then it will be used, if it is empty or undefined the default.Chart.Name
will be used. In this case the default value.Chart.Name
comes from thename
property of the Chart itself, defined inChart.yaml
. The override value.Values.nameOverride
comes from the propertynameOverride
invalues.yaml
.trunc 63
truncates the name to 63 characters, this is due to a limit for the name of Kubernetes resources.trimSuffix "-"
removes a trailing-
character from a string, if it exists
- The third line marks the end of the snippet.
So to put into words what this snippet does: it takes the name of the chart, possibly using an override value that we define, shortens the name to 63 characters and removes a trailing dash character.
The second snippet concerns the selector labels used to identify Pods:
{{- define "nginx-chart.selectorLabels" -}}
app: nginx
{{- end }}
This snippet is named nginx-chart.selectorLabels
and consists simply of the YAML app: nginx
.
The final snippet is this:
{{- define "nginx-chart.labels" -}}
{{ include "nginx-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end }}
Let me go through this snippet in more detail:
{{- define "nginx-chart.labels" -}}
marks the start of the snippet namednginx-chart.labels
{{ include "nginx-chart.selectorLabels" . }}
includes the content of the snippet namednginx-chart.selectorLabels
that I defined earlier, i.e. it will include theapp: nginx
label.{{- if .Chart.AppVersion }}
starts an if-statement, and it is true if the.Chart.AppVersion
property inChart.yaml
is defined.- If the if-statement is true then an additional label
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
is added to the list of labels. The value of the label is{{ .Chart.AppVersion | quote }}
which is another pipeline of directives, it basically takes the value.Chart.AppVersion
and applies quotes around it. - The last two lines look the same:
{{- end }}
. The first one marks the end of the if-statement, and the second one marks the end of the whole snippet.
service.yaml#
Now we come to the first template file service.yaml
. I take my previous service.yaml
manifest and add some template directives to it. When I am done editing I have the following:
apiVersion: v1
kind: Service
metadata:
name: { { include "nginx-chart.name" . } }
labels: { { - include "nginx-chart.labels" . | nindent 4 } }
spec:
type: { { .Values.service.type } }
selector: { { - include "nginx-chart.selectorLabels" . | nindent 4 } }
ports:
- protocol: TCP
port: { { .Values.service.port } }
targetPort: { { .Values.service.port } }
This manifest is relatively easy to understand even if this is the first time you use Helm, but let me go through the important pieces below:
- On row 4 I include the
nginx-chart.name
snippet from_helpers.tpl
. - On row 6 I include the labels from the
nginx-chart.labels
snippet, this is followed by thenindent
function. This function indents the content 4 spaces in this case (because the argument to the function was a 4). This is important because YAML expects the indentation to be accurate, otherwise there will be an error. - On row 8 I use a value from
values.yaml
to specify the type of the Service. - On row 10 I include the selector labels from the
nginx-char.selectorLabels
snippet followed by thenindent
function. - Finally on row 13 and 14 I use the value for the port number from
values.yaml
deployment.yaml#
Like I did for the Service in service.yaml
I can do for the Deployment in deployment.yaml
. I edit the file to use values from values.yaml
as well as helper snippets from _helpers.tpl
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "nginx-chart.name" . }}
labels:
{{- include "nginx-chart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.deployment.replicaCount }}
selector:
matchLabels:
{{- include "nginx-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "nginx-chart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
{{- if .Values.deployment.livenessProbe }}
livenessProbe:
httpGet:
path: /
port: {{ .Values.service.port }}
{{- end }}
Most parts are similar to what I did in service.yaml
, but we see a few new things:
- For the container image I use string interpolation and construct the value of the image by combining the repository
{{ .Values.image.repository }}
with the image tag. The tag value is{{ .Values.image.tag | default .Chart.AppVersion }}
which means that if.Values.image.tag
is specified I will use it, otherwise.Chart.AppVersion
will be used as the default. - I have surrounded the
livenessProbe
for my container with an if-statement. If.Values.deployment.livenessProbe
is set to true (invalues.yaml
) then the livenessProbe will be included, otherwise it will not.
How to Deploy Our Helm Chart?#
Our Helm chart is complete and we are ready to install it into our cluster. I am using a local Minikube cluster, but the procedure to install the Helm chart is the same no matter what cluster you are working with. So far in this series of articles we have been using kubectl apply
to deploy our manifests to our clusters, but this will not work for Helm charts since kubectl
does not natively understand what a Helm chart is. Instead we will use the helm
command line tool. This is why we installed it after all!
To install a Helm chart we can run helm install
, but we can also run helm upgrade
and add the --install
flag. The difference is that helm upgrade
is used to upgrade an existing Helm chart to a new version, while helm install
is used to install a new Helm chart. However, if I add the --install
flag to helm upgrade
it will install the Helm chart if it does not exist to begin with. This allows me to use a single command for the installation and the following upgrades.
So if I am located in the directory of my Helm chart I can run helm upgrade --install
to install my Helm chart:
$ helm upgrade --install nginx .
Release "nginx" does not exist. Installing it now.
NAME: nginx
LAST DEPLOYED: Tue Jan 10 17:27:43 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
With the command I provided a name (nginx
) and a path to the chart (the current directory .
). The output indicates that this chart was not installed before, thus it is installed and it states that the installed version has REVISION: 1
.
I can check if my Pods are running with kubectl get pods
:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-chart-7474569c7-l4klb 1/1 Running 0 2m
nginx-chart-7474569c7-mqgmv 1/1 Running 0 2m
nginx-chart-7474569c7-z7qfs 1/1 Running 0 2m
It seems like it worked just fine! I can also make sure my Service exists with kubectl get services
:
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-chart NodePort 10.108.66.55 <none> 80:31463/TCP 2m
How to Upgrade Our Existing Helm Chart?#
To apply an update to an existing Helm chart we first make a small edit. In Chart.yaml
I change the value of appVersion
from 1.22.1
to 1.23.1
. After editing my Chart.yaml
looks like this:
apiVersion: v2
name: nginx-chart
description: A Helm chart for a simple Nginx application
type: application
version: 1.0.0
appVersion: "1.23.1"
Now I can use the same command to update my chart like I did to install it:
$ helm upgrade --install nginx .
Release "nginx" has been upgraded. Happy Helming!
NAME: nginx
LAST DEPLOYED: Tue Jan 10 17:33:14 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
We see that we now have REVISION: 2
.
How to Uninstall Our Helm Chart?#
To remove an existing Helm chart we can run helm uninstall
:
$ helm uninstall nginx
release "nginx" uninstalled
How to Use a Public Helm Chart?#
We have gone through how to create and install our own Helm charts. What about publicly available Helm charts? I mentioned that there are official, and not as official, Helm charts available on the web. Concretely I showed you that there was a Helm chart for Postgres available from bitnami. I will continue with that example to demonstrate how to use a publicly available Helm chart.
The first step is to add the bitnami repository using the helm
CLI:
$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
A Helm repository is a collection of Helm charts gathered in a repository, similar to a Docker registry. There are many Helm repositories available on the web, and you can create your own private repositories or repositories for your organization. Once we have added a repository we can install Helm charts from this repository. For this we again use the helm
CLI:
$ helm upgrade --install postgres-release bitnami/postgresql
Release "postgres-release" does not exist. Installing it now.
NAME: postgres-release
LAST DEPLOYED: Tue Jan 10 16:17:28 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: postgresql
CHART VERSION: 12.1.8
APP VERSION: 15.1.0
(output truncated)
I give my release the name postgres-release
, and I say that the source Helm chart is coming from bitnami/postgres
.
If I want to customize the chart I can copy the values.yaml
file from this Helm chart and make any edits I need to it and then provide it in the helm upgrade
command like so:
$ helm upgrade --install postgres-release bitnami/postgresql --values ./local/path/values.yaml
Release "postgres-release" does not exist. Installing it now.
(output truncated)
Summary#
We have seen a basic example of how to take a few regular Kubernetes manifests and turn them into a Helm chart that packages our application in a dynamic format. I just showed the minimum to get started with Helm, if you are interested in learning more I recommend checking out the official documentation.
Apart from creating our own Helm chart we also saw how to use a publicly available Helm chart.
Helm is a ubiquitous tool in the Kubernetes world. Learning it is definitely not a waste of time if you intend to use Kubernetes in your career.
Here I will outline what is left in this Kubernetes-101 series of articles:
- Ingresses
- ServiceAccounts
- NetworkPolicies
- SecurityContexts
- Wrap-up or 10,000-foot view to finish off!
In the next article I will discuss what the Ingress resource is.
Too long; didn’t read ↩︎