Introduction

For a typical cloud-native application with a 3-tier architecture, the diagram below illustrates how it might be described in terms of Kubernetes objects. In this example, each tier consists of a Deployment and Service object, and may additionally define ConfigMap or Secret objects. Each of these objects are typically defined in separate YAML files, and are fed into the kubectl command line tool. 3-tier application architecture on Kubernetes A Helm chart encapsulates each of these YAML definitions, provides a mechanism for configuration at deploy-time and allows you to define metadata and documentation that might be useful when sharing the package. Helm can be useful in different scenarios:

  • Find and use popular software packaged as Kubernetes charts
  • Share your own applications as Kubernetes charts
  • Create reproducible builds of your Kubernetes applications
  • Intelligently manage your Kubernetes object definitions
  • Manage releases of Helm packages

Let’s explore the second and third scenarios by creating our first chart.

Step 1: Generate your first chart

The best way to get started with a new chart is to use the helm create command to scaffold out an example we can build on. Use this command to create a new chart named mychart in a new directory:

1helm create mychart

Helm will create a new directory in your project called mychart with the structure shown below. Let’s navigate our new chart (pun intended) to find out how it works.

 1mychart
 2|-- Chart.yaml
 3|-- charts
 4|-- templates
 5|   |-- NOTES.txt
 6|   |-- _helpers.tpl
 7|   |-- deployment.yaml
 8|   |-- ingress.yaml
 9|   `-- service.yaml
10`-- values.yaml

Templates

The most important piece of the puzzle is the templates/ directory. This is where Helm finds the YAML definitions for your Services, Deployments and other Kubernetes objects. If you already have definitions for your application, all you need to do is replace the generated YAML files for your own. What you end up with is a working chart that can be deployed using the helm install command. It’s worth noting however, that the directory is named templates, and Helm runs each file in this directory through a Go template rendering engine. Helm extends the template language, adding a number of utility functions for writing charts. Open the service.yaml file to see what this looks like:

 1apiVersion: v1
 2kind: Service
 3metadata:
 4name: {{ template "fullname" . }}
 5labels:
 6    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
 7spec:
 8type: {{ .Values.service.type }}
 9ports:
10- port: {{ .Values.service.externalPort }}
11    targetPort: {{ .Values.service.internalPort }}
12    protocol: TCP
13    name: {{ .Values.service.name }}
14selector:
15    app: {{ template "fullname" . }}

This is a basic Service definition using templating. When deploying the chart, Helm will generate a definition that will look a lot more like a valid Service. We can do a dry-run of a helm install and enable debug to inspect the generated definitions:

 1helm install --dry-run --debug ./mychart
 2...
 3# Source: mychart/templates/service.yaml
 4apiVersion: v1
 5kind: Service
 6metadata:
 7name: pouring-puma-mychart
 8labels:
 9    chart: "mychart-0.1.0"
10spec:
11type: ClusterIP
12ports:
13- port: 80
14    targetPort: 80
15    protocol: TCP
16    name: nginx
17selector:
18    app: pouring-puma-mychart
19...

Values

The template in service.yaml makes use of the Helm-specific objects .Chart and .Values.. The former provides metadata about the chart to your definitions such as the name, or version. The latter .Values object is a key element of Helm charts, used to expose configuration that can be set at the time of deployment. The defaults for this object are defined in the values.yaml file. Try changing the default value for service.internalPort and execute another dry-run, you should find that the targetPort in the Service and the containerPort in the Deployment changes. The service.internalPort value is used here to ensure that the Service and Deployment objects work together correctly. The use of templating can greatly reduce boilerplate and simplify your definitions. If a user of your chart wanted to change the default configuration, they could provide overrides directly on the command-line:

1helm install --dry-run --debug ./mychart --set service.internalPort=8080

For more advanced configuration, a user can specify a YAML file containing overrides with the –values option.

Helpers and other functions

The service.yaml template also makes use of partials defined in __helpers.tpl_, as well as functions like replace. The Helm documentation has a deeper walkthrough of the templating language, explaining how functions, partials and flow control can be used when developing your chart.

Documentation

Another useful file in the templates/ directory is the NOTES.txt file. This is a templated, plaintext file that gets printed out after the chart is successfully deployed. As we’ll see when we deploy our first chart, this is a useful place to briefly describe the next steps for using a chart. Since NOTES.txt is run through the template engine, you can use templating to print out working commands for obtaining an IP address, or getting a password from a Secret object. As mentioned earlier, a Helm chart consists of metadata that is used to help describe what the application is, define constraints on the minimum required Kubernetes and/or Helm version and manage the version of your chart. All of this metadata lives in the Chart.yaml file. The Helm documentation describes the different fields for this file.

Step 2: Deploy your first chart

The chart you generated in the previous step is set up to run an NGINX server exposed via a Kubernetes Service. By default, the chart will create a ClusterIP type Service, so NGINX will only be exposed internally in the cluster. To access it externally, we’ll use the NodePort type instead. We can also set the name of the Helm release so we can easily refer back to it. Let’s go ahead and deploy our NGINX chart using the helm install command:

1helm install example ./mychart --set service.type=NodePort
2NAME:   example
3……
4
5NOTES:
61. Get the application URL by running these commands:
7export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services example-mychart)
8export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
9echo http://$NODE_IP:$NODE_PORT/

The output of helm install displays a handy summary of the state of the release, what objects were created, and the rendered NOTES.txt file to explain what to do next. Run the commands in the output to get a URL to access the NGINX service and pull it up in your browser. nginx server default page If all went well, you should see the NGINX welcome page as shown above. Congratulations! You’ve just deployed your very first service packaged as a Helm chart!

Step 3: Modify chart to deploy a custom service

The generated chart creates a Deployment object designed to run an image provided by the default values. This means all we need to do to run a different service is to change the referenced image in values.yaml. We are going to update the chart to run a todo list application available on Docker Hub. In values.yaml, update the image keys to reference the todo list image:

1image:
2  repository: prydonius/todo
3  pullPolicy: IfNotPresent
4  # Overrides the image tag whose default is the chart appVersion.
5  tag: "1.0.0"

As you develop your chart, it’s a good idea to run it through the linter to ensure you’re following best practices and that your templates are well-formed. Run the helm lint command to see the linter in action:

 1helm lint ./mychart
 2==> Linting ./mychart
 3[INFO] Chart.yaml: icon is recommended
 4
 51 chart(s) linted, no failures
 6
 7# modify and test again
 8echo "malformed" > mychart/values.yaml
 9helm lint ./mychart
10==> Linting mychart
11[INFO] Chart.yaml: icon is recommended
12[ERROR] values.yaml: unable to parse YAML
13    error converting YAML to JSON: yaml: line 34: could not find expected ':'
14
15Error: 1 chart(s) linted, 1 chart(s) failed

This time, the linter tells us that it was unable to parse my values.yaml file correctly. With the line number hint, we can easily find the fix the bug we introduced. Now that the chart is once again valid, run helm install again to deploy the todo list application:

1helm install example2 ./mychart --set service.type=NodePort
2NAME:   example2
3……
4
5NOTES:
61. Get the application URL by running these commands:
7export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services example2-mychart)
8export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
9echo http://$NODE_IP:$NODE_PORT/

Once again, we can run the commands in the NOTES to get a URL to access our application.

Todo List Application If you have already built containers for your applications, you can run them with your chart by updating the default values or the Deployment template. Check out the Bitnami Docs for an containerizing your applications. So far in this tutorial, we’ve been using the helm install command to install a local, unpacked chart. However, if you are looking to share your charts with your team or the community, your consumers will typically install the charts from a tar package. We can use helm package to create the tar package:

1helm package ./mychart

Helm will create a mychart-0.1.0.tgz package in our working directory, using the name and version from the metadata defined in the Chart.yaml file. A user can install from this package instead of a local directory by passing the package as the parameter to helm install.

1helm install example3 mychart-0.1.0.tgz --set service.type=NodePort

Repositories

In order to make it much easier to share packages, Helm has built-in support for installing packages from an HTTP server. Helm reads a repository index hosted on the server which describes what chart packages are available and where they are located. We can use the helm serve command to run a local repository to serve our chart.

1helm serve
2Regenerating index. This may take a moment.
3Now serving you on 127.0.0.1:8879

Now, in a separate terminal window, you should be able to see your chart in the local repository and install it from there:

1helm search local
2NAME            VERSION DESCRIPTION
3local/mychart   0.1.0   A Helm chart for Kubernetes
4
5helm install example4 local/mychart --set service.type=NodePort

To set up a remote repository you can follow the guide in the Helm documentation.

Dependencies

As the applications your packaging as charts increase in complexity, you might find you need to pull in a dependency such as a database. Helm allows you to specify sub-charts that will be created as part of the same release. To define a dependency, create a requirements.yaml file in the chart root directory:

1cat > ./mychart/requirements.yaml <<EOF
2dependencies:
3- name: mariadb
4version: 0.6.0
5repository: https://charts.helm.sh/stable
6EOF

Much like a runtime language dependency file (such as Python’s requirements.txt), the requirements.yaml file allows you to manage your chart’s dependencies and their versions. When updating dependencies, a lockfile is generated so that subsequent fetching of dependencies use a known, working version. Run the following command to pull in the MariaDB dependency we defined:

 1helm dep update ./mychart
 2Hang tight while we grab the latest from your chart repositories...
 3...Unable to get an update from the "local" chart repository (http://127.0.0.1:8879/charts):
 4    Get http://127.0.0.1:8879/charts/index.yaml: dial tcp 127.0.0.1:8879: getsockopt: connection refused
 5...Successfully got an update from the "bitnami" chart repository
 6...Successfully got an update from the "incubator" chart repository
 7Update Complete. *Happy Helming!*
 8Saving 1 charts
 9Downloading mariadb from repo
10$ ls ./mychart/charts
11mariadb-0.6.0.tgz

Helm has found a matching version in the bitnami repository and has fetched it into my chart’s sub-chart directory. Now when we go and install the chart, we’ll see that MariaDB’s objects are created too:

1helm install example5 ./mychart --set service.type=NodePort
2NAME:   example5
3……
4
5NOTES:
61. Get the application URL by running these commands:
7export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services example5-mychart)
8export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
9echo http://$NODE_IP:$NODE_PORT/

Contribute to the Bitnami repository!

As a chart author, you can help to build out Bitnami’s chart repository by improving existing charts or submitting new ones. Checkout https://kubeapps.dev to see what’s currently available and head to https://github.com/bitnami/charts to get involved.

We’ve walked through some of the ways Helm supercharges the delivery of applications on Kubernetes. From an empty directory, you were able to get a working Helm chart out of a single command, deploy it to your cluster and access an NGINX server. Then, by simply changing a few lines and re-deploying, you had a much more useful todo list application running on your cluster! Beyond templating, linting, sharing and managing dependencies, here are some other useful tools available to chart authors: