Writing a Helm Chart

Writing a Helm Chart

In this blog, we can see how to write a Helm Chart.

Helm charts are incredibly versatile and they can automate almost any kind of Kubernetes package installation we can think of. In a way, they’re similar to installation wizards we use on the Windows operating system. An installation wizard, besides extracting the application’s files and directories, 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 just 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.

Writing Our First Helm Chart

Helm has a built-in command to create a basic chart that we can start to work with. This will create the basic directory structure for a chart, along with the most important files we need.

Let’s see what we need to edit to make this chart install a basic Nginx website in our Kubernetes cluster.

First, we’ll enter the directory of our newly generated chart.

As discussed in the previous lessons, Chart.yaml is like an About page for the chart, where its core properties are defined: its name, descriptions about what it does, emails of the maintainers, dependencies, and so on. Let’s open this up using any editor.

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

Let’s assume the following scenario. We build this for our company and we want the other employees that 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.

We’ll delete all the content here and start from scratch as it’s easier to understand if we build everything piece by piece, exploring various concepts, such as functions, pipelines, flow controls, hooks, and so on, as we go along.

Making Our Chart Fetch Values from the values.yaml File

Let’s build a simple Kubernetes deployment that will run our Nginx pod(s).

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, 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.

Some Resources should Have Unique Names

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, 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 too.

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

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

save the file.

The second interesting thing we see here is {{ .Values.replicaCount }}. The opening and closing curly brackets {{ }} enclose what is called 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 see 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 other stuff with lowercase letters. There is a convention that Helm’s built-in values start with capital letters. That’s why we have .Chart.Name and not .Chart.name. The chart’s name is built-in/static, not chosen by the user. But the replicaCount in .Values.replicaCount is something that a user chose in 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 values.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 file. Copy and paste the following content and save the file:

replicaCount: 1

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

Making Sure Our Chart Is Working as Intended

Let’s go back up one directory so we can pass our chart correctly to the next Helm command.

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 of all, we want to make sure 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 just 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:

[email protected]:~$ 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.

You can read more in the helm documentation.: https://helm.sh/docs/chart_template_guide/

Checkout the Helm for the Absolute Beginners course here

Checkout the Complete Kubernetes learning path here