Kubernetes-101: Helm
Tags: kubernetes
Published: 2023-06-09 10:00:00

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;DR[^tldr] 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivm4yek5icj2padg1c5o.png)

## 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](https://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](https://helm.sh/docs/intro/install/). I open a terminal and run `brew install helm`:

```console
$ 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:

```console
$ 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`:

```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`:

```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](./../deployments-1/) and [Services](./../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:

```console
$ tree .

.
├── Chart.yaml
├── charts
├── templates
│   ├── template1.yaml
│   └── template2.yaml
└── values.yaml

2 directories, 2 files
```

- `Chart.yaml` is the main file describing the chart
- `charts` 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 Helm
- `values.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`:

```console
$ helm create nginx-chart

Creating nginx-chart
```

This gives us a started chart with the following content:

```console
$ 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:

```console
$ 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:

```yaml
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 file
- `name` 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 contains
- `type` could be either `application` or `library`
  - `application` is a collection of templates that define an application, just what we want to do in this example
  - `library` 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 versioning
- `appVersion` 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:

```yaml
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 specify `nginx` 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:

```yaml
{{- 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 the `default` 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 the `name` property of the Chart itself, defined in `Chart.yaml`. The override value `.Values.nameOverride` comes from the property `nameOverride` in `values.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:

```yaml
{{- 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:

```yaml
{{- 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 named `nginx-chart.labels`
- `{{ include "nginx-chart.selectorLabels" . }}` includes the content of the snippet named `nginx-chart.selectorLabels` that I defined earlier, i.e. it will include the `app: nginx` label.
- `{{- if .Chart.AppVersion }}` starts an if-statement, and it is true if the `.Chart.AppVersion` property in `Chart.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:

```yaml
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 the `nindent` 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 the `nindent` 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`:

```yaml
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 (in `values.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:

```console
$ 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`:

```console
$ 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`:

```console
$ 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:

```yaml
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:

```console
$ 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`:

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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.

[^tldr]: Too long; didn't read