DEV Community

Cover image for Kubernetes-101: Helm
Mattias Fjellström
Mattias Fjellström

Posted on • Originally published at mattias.engineer

Kubernetes-101: Helm

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

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
Enter fullscreen mode Exit fullscreen mode

I can now verify that Helm is installed:

$ helm version --short

v3.10.3+g835b733
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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:

$ helm create nginx-chart

Creating nginx-chart
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

nameOverride: ""

image:
  repository: nginx
  pullPolicy: IfNotPresent
  tag: ""

service:
  type: NodePort
  port: 80

deployment:
  replicaCount: 3
  livenessProbe: true
Enter fullscreen mode Exit fullscreen mode

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:

{{- define "nginx-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
Enter fullscreen mode Exit fullscreen mode

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:

{{- define "nginx-chart.selectorLabels" -}}
app: nginx
{{- end }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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:

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 } }
Enter fullscreen mode Exit fullscreen mode

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:

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 }}
Enter fullscreen mode Exit fullscreen mode

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:

$ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.


  1. Too long; didn't read 

Top comments (0)