Writing a Helm Chart: A Comprehensive Guide for Beginners

Deploying applications in Kubernetes can be complex due to the many moving parts involved. Unlike traditional monolithic applications, Kubernetes applications are often composed of many microservices that need to be deployed and managed independently. This can make it challenging to keep track of all the resources required to run an application, such as deployments, services, pods, and config maps. That's where a Helm Chart comes in!

In this blog, you'll learn what a Helm chart is and how to write it.

What is a Helm chart?

A Helm chart is a package manager for Kubernetes that helps define, install, and upgrade complex Kubernetes applications. It contains all the Kubernetes resources required to run an application, such as deployments, services, and other artifacts, and can be easily versioned, shared, and reused. By using a Helm chart, you can simplify the deployment process and make it more repeatable and scalable.

Learn more about Kubernetes components: Kubernetes Architecture Explained: Overview for DevOps Enthusiasts

Helm charts are incredibly versatile; they can automate almost any Kubernetes package installation. In a way, they’re similar to installation wizards we use on the Windows operating system.

Besides extracting the application’s files and directories, an installation wizard can also add relevant shortcuts to the desktop, configure the application to launch when Windows starts up, install additional libraries that are needed, and so on. The same is true for Helm charts. Although charts are technically not programs, they can act like programs. Besides installing various Kubernetes objects to get our app up and running, they can do some extra stuff, too.

But it can be overwhelming to think about a million things a helm chart can do. So let’s start with something simple and learn the important bits and pieces, one by one.

Want a quick guide to Helm concepts and features? Check out this video.

Writing Our First Helm Chart

In this section, we'll cover building a Helm Chart step-by-step.

Prerequisites

You'll need access to a running Kubernetes cluster to follow along with the examples in this post. If you don’t have access to one, you can use a tool such as minikube to set up a Kubernetes cluster. Also, you’ll need to have kubectl and Helm installed on your local machine.  

The commands in this blog are executed on KodeKloud's Helm Playground. With this playground, you won't need to go through the hassle of installing any additional software— everything you need is already set up and ready to use.

Using Helm’s Built-In Help

First, we need to know what we can do with the Helm command. We'll use the built-in help command to see this.

Run the command below to see Helm's subcommand:

helm --help

To create a Helm chart for installing a basic Nginx website in your Kubernetes cluster, follow these step-by-step instructions:

Step 1: Creating a Directory

Open your terminal and navigate to the directory where you want to create your chart.

Step 2: Creating a Chart

Use the following command to create a new chart with the name "nginx":

helm create nginx

This will create a basic chart directory structure for you to work with.

Step 3: Adding Information to the Chart

Open the Chart.yaml file in the nginx chart directory using a text editor of your choice. This file contains important information about your chart, including its name, version, and description. You can modify the properties in this file as needed.

We can see everything nicely described in the comments (lines preceded with the # sign).

Let’s assume the following scenario. We built this for our company, and we want the other employees who use it to have some basic info about the chart. We’ll modify the description and add an email at the end so people can contact us if they have any questions.

The final result should look like this:

apiVersion: v2
name: nginx
description: Basic Nginx website for our company

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

maintainers:
- email: [email protected]
  name: John Smith

Now save the file. Note that you should respect the same formatting rules you would follow when editing a regular YAML file for Kubernetes (e.g., under “maintainers,” we align everything with double spaces; one space after “-” and two spaces before “name” so that they’re both aligned the same way, signaling they’re children of the “maintainers” group).

Now, let’s switch to the templates directory, where most of the magic happens.

Step 4: Customizing the Chart

Normally, a deployment could look something like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

But we don’t want to use static values. We want our chart to dynamically generate things such as the deployment name, which may be based on the release name a user chose when he installed the chart. We also want to set the number of replicas based on values the users might set in their values.yaml file, and so on. In a nutshell, we need to write the chart in such a way that it adapts to each user’s needs.

Imagine that in our chart, we use this name for the deployment, highlighted below:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment

Now, imagine we install a release based on this chart. All goes well. The release is installed, and a deployment named nginx-deployment makes its way into our Kubernetes cluster. But if we try to install a second release based on this chart, it will fail. Why? Because Helm would try to create a second deployment with the same name: nginx-deployment. This conflicts with the older one, which uses that name.

So, how do we solve this? We templatize the name of the deployment. We use Helm’s templating language to basically tell it Hey, Helm, the name of this deployment should be based on how the user chose to name his release. Take a look at this line:

name: {{ .Release.Name }}-nginx

This way, if we create a release called release1 and another called release2, the first one will name the deployment release1-nginx while the second will call it release2-nginx. And we now have guaranteed unique names.

Generally speaking, we should follow similar techniques for all Kubernetes objects that must have unique values/names. Helm does not allow us to install two releases with the same name, so by basing the names of our Kubernetes objects on the release name, we can be sure that our charts can never create two objects with the same name. Furthermore, when we later explore our objects, seeing something called release5-pvc quickly helps us identify to which release this object belongs.

So, for now, add these contents to the deployment.yaml file and save it.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-nginx
  labels:
    app: nginx
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

The second interesting thing we see here is {{ .Values.replicaCount }}. The opening and closing curly brackets {{ }} enclose a template directive, and the syntax is specific to the Go templating language.

Instead of using a static replica number here, we pull in the number of replicas we want from our values.yaml file, from the field titled replicaCount. Next, we pull the name for our container directly from the chart’s name, with {{ .Chart.Name }}. So, we can not only pull values from the values.yaml file, but also from other parts of the chart’s structure.

We can see that some stuff starts with capital letters while others start with lowercase letters. There is a convention that Helm’s built-in values start with capital letters. That’s why we have .Chart.Name not .Chart.name. The chart’s name is built-in/static and has not been chosen by the user.

The replicaCount in .Values.replicaCount is something that a user chooses based on their values.yaml file, so that’s why it starts with a lowercase letter.

We can use multiple template directives in a single line, as we see next. In the line, image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" we extract the name of our image from nested user-defined values in our values.yaml file.

To get an idea of what values get fetched here, here’s an example of the relevant parts from the .yaml file.

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

With that line in our deployment.yaml, and the values defined in values.yaml, Helm will pass this through its templating engine and finally generate a line like image: "nginx:1.16.0" in the final manifest that will be sent out to Kubernetes when installing this chart.

Let’s create the values.yaml file and see all of this in action. First, let’s go back up one directory to put the file in its proper location.

We’ll remove the sample values file that was generated by the helm create command.

And let’s create our own values.yaml file. Copy and paste the following content and save the file:

replicaCount: 1

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

Want to learn more about Deployments: Kubernetes Deployment: Strategies, Explanation, and Examples

Step 5: Testing Our Chart

Let’s back up one directory to pass our chart to the next Helm command correctly. So, how do we make sure that our chart is actually doing what it’s supposed to do? There are several ways we can check.

First, we want to ensure that the templating stuff we added in deployment.yaml is actually generating what we’d expect in the final (parsed) manifest that it will send to Kubernetes. We can verify what would be generated with this command:

helm template ./nginx

This output shows us what each template .yaml file would generate, and everything looks correct; the values we defined in values.yaml were picked up and used by our deployment.yaml template file.

---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: RELEASE-NAME-nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

To verify that the chart is correctly defined, we can also use this command:

helm lint ./nginx

The exact problem files are indicated, making debugging much easier. In this case, no problems are found, as those are marked with ERROR rather than INFO. Adding an icon to our chart is just a recommendation to make it more easily identifiable with that graphical representation.

==> Linting ./nginx
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Before actually installing this, we can do what is called a dry run. This pretends to install the package to the cluster, and it can catch things that Kubernetes would complain about in a real install.

helm install --dry-run my-nginx-release ./nginx

For example, if the objects are not well defined, Kubernetes might throw errors like these:

user@debian:~$ helm install --dry-run my-nginx-release ./nginx

Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: ValidationError(Deployment.spec.template): unknown field "containers" in io.k8s.api.core.v1.PodTemplateSpec

But in our case, everything is OK, so we see this output, once again showing us the manifests (definitions of our object/s) that would get deployed into Kubernetes.

NAME: my-nginx-release
LAST DEPLOYED: Sat Jun  5 01:37:58 2021
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx-release-nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

Now, we can install the chart.

helm install my-nginx-release ./nginx

Let’s see if any pods were launched.

kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-777d596565-p9scx   1/1     Running   0          28s

It’s pretty exciting to see our first chart launching our first pod in a real Kubernetes cluster.

Enroll in our Helm for the Absolute Beginners course to learn more Helm concepts.

Helm for Beginners | KodeKloud
Learn and get certified with simple and easy hands-on labs

Conclusion

Helm simplifies deploying, managing, and upgrading applications on a Kubernetes cluster. While working with charts, use the values.yaml file to store configuration values for your chart. This makes it easier to customize your chart for different environments. Remember to also test your charts thoroughly to ensure they work as expected on different Kubernetes versions and configurations.


More on Helm: