Helm Charts — Kubernetes
There are different ways of running production services at a high scale. One popular solution for running containers in production is Kubernetes. But interacting with Kubernetes directly comes with some caveats. Kubernetes is a very helpful tool for cloud-native developers. But it doesn’t cover all the bases on its own — there are some things that Kubernetes cannot solve or that are outside its scope.
What is Helm?
Helm is a Kubernetes package and operations manager. The name “kubernetes” is derived from the Greek word for “pilot” or “helmsman”, making Helm its steering wheel. Using a packaging manager, Charts, Helm allows us to package Kubernetes releases into a convenient zip (.tgz) file. A Helm chart can contain any number of Kubernetes objects, all of which are deployed as part of the chart. A Helm chart will usually contain at least a Deployment and a Service, but it can also contain an Ingress, Persistent Volume Claims, or any other Kubernetes object.
Helm charts are used to deploy an application, or one component of a larger application.
Helm helps you manage Kubernetes applications — Helm Charts help you define, install, and upgrade even the most complex Kubernetes application.
Charts are easy to create, version, share, and publish — so start using Helm and stop the copy-and-paste.
Why Do We Need Helm?
Kubernetes objects are challenging to manage. With helpful tools, the Kubernetes learning curve becomes smooth and manageable. Helm automates maintenance of YAML manifests for Kubernetes objects by packaging information into charts and advertises them to a Kubernetes cluster.
Helm keeps track of the versioned history of every chart installation and change. Rolling back to a previous version or upgrading to a newer version is completed with comprehensible commands.
The Purpose of Helm
Helm is a tool for managing Kubernetes packages called charts. Helm can do the following:
- Create new charts from scratch
- Package charts into chart archive (tgz) files
- Interact with chart repositories where charts are stored
- Install and uninstall charts into an existing Kubernetes cluster
- Manage the release cycle of charts that have been installed with Helm
For Helm, there are three important concepts:
- The chart is a bundle of information necessary to create an instance of a Kubernetes application.
- The config contains configuration information that can be merged into a packaged chart to create a releasable object.
- A release is a running instance of a chart, combined with a specific config.
Components
Helm is an executable which is implemented into two distinct parts:
The Helm Client is a command-line client for end users. The client is responsible for the following:
- Local chart development
- Managing repositories
- Managing releases
- Interfacing with the Helm library
- Sending charts to be installed
- Requesting upgrading or uninstalling of existing releases
The Helm Library provides the logic for executing all Helm operations. It interfaces with the Kubernetes API server and provides the following capability:
- Combining a chart and configuration to build a release
- Installing charts into Kubernetes, and providing the subsequent release object
- Upgrading and uninstalling charts by interacting with Kubernetes
The standalone Helm library encapsulates the Helm logic so that it can be leveraged by different clients.
Implementation
The Helm client and library is written in the Go programming language.
The library uses the Kubernetes client library to communicate with Kubernetes. Currently, that library uses REST+JSON. It stores information in Secrets located inside of Kubernetes. It does not need its own database.
Configuration files are, when possible, written in YAML.
How Does Helm Work?
Helm and Kubernetes work like a client/server application. The Helm client pushes resources to the Kubernetes cluster. The server-side depends on the version: Helm 2 uses Tiller while Helm 3 got rid of Tiller and entirely relies on the Kubernetes API.
Charts
Helm uses a packaging format called charts. A chart is a collection of files that describe a related set of Kubernetes resources. A single chart might be used to deploy something simple, like a memcached pod, or something complex, like a full web app stack with HTTP servers, databases, caches, and so on.
Charts are created as files laid out in a particular directory tree. They can be packaged into versioned archives to be deployed.
If you want to download and look at the files for a published chart, without installing it, you can do so with helm pull chartrepo/chartname
.
How to Create a Helm Chart
It’s pretty easy to create a chart in Helm. First, you need to have Helm installed. Then, just type in helm create <chart name>
and it will create a directory filled with files and other directories. Those files are required for Helm to create a chart.
Let’s take a closer look at what this file tree looks like and what the files are within it:
- chart.yaml: This is where you’ll put the information related to your chart. That includes the chart version, name, and description so you can find it if you publish it on an open repository. Also in this file you’ll be able to set external dependencies using the
dependencies
key. - values.yaml: Like we saw before, this is the file that contains defaults for variables.
- templates (dir): This is the place where you’ll put all your manifest files. Everything in here will be passed on and created in Kubernetes.
- charts: If your chart depends on another chart you own, or if you don’t want to rely on Helm’s default library (the default registry where Helm pull charts from), you can bring this same structure inside this directory. Chart dependencies are installed from the bottom to the top, which means if chart A depends on chart B, and B depends on C, the installation order will be C ->B ->A.
There are other fields, but these are the most common, and they’re the required ones.
A quick note: When installing Helm, make sure you’re installing version 3. Version 2 still works, but it needs a server-side component called Tiller, which ties your helm installation to a single cluster. Helm 3 removed this need with the addition of several CRDs, but it’s not supported in all Kubernetes versions.
How to Deploy a Simple Helm Application
Let’s get our hands dirty and make sure Helm is ready to use.
First, we need to be connected to a Kubernetes cluster. In this example, I will concentrate on a Kubernetes cluster that comes with your Docker setup. So if you use some other Kubernetes cluster, configurations and outputs might differ.
$ kubectl config use-context docker-desktop Switched to context "docker-desktop".$ kubectl get node
NAME STATUS ROLES AGE VERSION
docker-desktop Ready master 20d v1.19.3
Let’s deploy an Apache webserver using Helm. As a first step, we need to tell Helm what location to search by adding a Helm repository:
$ helm repo add bitnami https://charts.bitnami.com/bitnami
Let’s install the actual container:
$ helm install my-apache bitnami/apache --version 8.0.2
After a few minutes your deployment is ready. We can check the state of the containers using kubectl:
$ kubectl get pods NAME READY STATUS RESTARTS AGE
my-apahe-apache-589b8df6bd-q6m2n 1/1 Running 0 2m27s
Now, open http://localhost to see the default Apache exposed website locally. Also, Helm can show us information about current deployments:
$ helm list NAME REVISION STATUS CHART VERSION
my-apache 1 deployed apache-8.0.2 2.4.46
How to Upgrade a Helm Application
We can upgrade our deployed application to a new version like this:
$ helm upgrade my-apache bitnami/apache --version 8.0.3$ helm listNAME REVISION STATUS CHART VERSION
my-apache 2 deployed apache-8.0.3 2.4.46
The column Revision indicates that this is the 2nd version we’ve deployed.
How to Rollback a Helm Application
So let’s try to rollback to the first deployed version:
$ helm rollback my-apache 1 Rollback was a success! Happy Helming!$ helm list
NAME REVISION STATUS CHART VERSION
my-apache 3 deployed apache-8.0.2 2.4.46
This is a very powerful feature that allows you to roll back changes in production quickly.
I mentioned that Helm stores deployment information in secrets — here they are:
$ kubectl get secretNAME TYPE DATA AGE
default-token-nc4hn kubernetes.io/sat 3 20d
sh.helm.release.v1.my-apache.v1 helm.sh/release.v1 1 1m
sh.helm.release.v1.my-apache.v2 helm.sh/release.v1 1 1m
sh.helm.release.v1.my-apache.v3 helm.sh/release.v1 1 1m
How to Remove a Deployed Helm Application
Let’s clean up our Kubernetes by removing the my-apache release:
$ helm delete my-apache
release "my-apache" uninstalled
Helm gives you a very convenient way of managing a set of applications that enables you to deploy, upgrade, rollback and delete.
Now, we are ready to use more advanced Helm features that will boost your productivity!
How to Access Production-Ready Helm Charts
You can search public hubs for Charts that enable you to quickly deploy your desired application with a customizable configuration.
A Helm Chart doesn’t just contain a static set of definitions. Helm comes with capabilities to hook into any lifecycle state of a Kubernetes deployment. This means during the installation or upgrade of an application, various actions can be executed like creating a database update before updating the actual database.
This powerful definition of Helm Charts lets you share and improve an executable description of a deployment setup that spans from initial installation and version upgrades to rollback capabilities.
Helm might be heavy for a simple container like a single node web server, but it’s very useful for more complex applications. For example it works great for a distributed system like Kafka or Cassandra that usually runs on multiple distributed nodes on different datacenters.
We’ve already leveraged Helm to deploy a single Apache container. Now, we will deploy a production-ready WordPress application that contains:
- Containers that serve WordPress,
- Instances of MariaDB for persistence and
- Prometheus sidecar containers for each WordPress container to expose health metrics.
Before we deploy, it’s recommended to increase your Docker limits to at least 4GB of memory.
Setting everything up sounds like a job that would take weeks. To make it resilient and scale, probably a job that would take months. In these areas, Helm Charts can really shine. Due to the growing community, there might already be a Helm Chart that we can use.
How to Deploy WordPress and MariaDB
There are different public hubs for Helm Charts. One of them is artifacthub.io. We can search for “WordPress” and find an interesting WordPress Chart.
On the right side, there is an install button. If you click it, you get clear instructions about what to do:
$ helm repo add bitnami https://charts.bitnami.com/bitnami$ helm install my-wordpress bitnami/wordpress --version 10.1.4
You will also see some instructions that tell you how to access the admin interface and the admin password after installation.
Here is how you can get and decode the password for the admin user on Mac OS:
$ echo Username: user
$ echo Password: $(kubectl get secret --namespace default my-wordpress-3 -o jsonpath="{.data.wordpress-password}" | base64 --decode)Username: user
Password: sZCa14VNXe
On windows, you can get the password for the user user in the powershell:
$pw=kubectl get secret --namespace default my-wordpress -o jsonpath="{.data.wordpress-password}"
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($pw))
Our local development will be available at: http://localhost.
Our admin interface will be available at: https://localhost/admin.
So we have everything to run it locally. But in production, we want to scale some parts of it to serve more and more visitors. We can scale the number of WordPress services. We also want to expose some health metrics like the usage of our CPU and memory.
We can download the example configuration for production from the maintainer of the WordPress Chart. The most important changes are:
### Start 3 WordPress instances that will all receive
### requests from our visitors. A load-balancer will distribute calls
### to all containers evenly.
replicaCount: 3### start a sidecar container that will expose metrics for your wordpress container
metrics:
enabled: true
image:
registry: docker.io
repository: bitnami/apache-exporter
tag: 0.8.0-debian-10-r243
Let’s stop the default application:
$ helm delete my-wordpress
release "my-wordpress" uninstalled
How to Start a Multi-instance WordPress and MariaDB Deployment
Deploy a new release using the production values:
$ helm install my-wordpress-prod bitnami/wordpress --version 10.1.4 -f values-production.yaml
This time, we have more containers running:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-wordpress-prod-5c9776c976-4bs6f 2/2 Running 0 103s
my-wordpress-prod-5c9776c976-9ssmr 2/2 Running 0 103s
my-wordpress-prod-5c9776c976-sfq84 2/2 Running 0 103s
my-wordpress-prod-mariadb-0 1/1 Running 0 103s
We see 4 lines: 1 line for MariaDB, and 3 lines for our actual WordPress pods.
A pod in Kubernetes is a group of containers. Each group contains 2 containers, one for WordPress and one for an exporter for Prometheus that exposes valuable metrics in a special format.
As in the default setup, we can open localhost and play with our WordPress application.
How to Access Exposed Health Metrics
We can check the exposed health metrics by proxying to one of the running pods:
kubectl port-forward my-wordpress-prod-5c9776c976-sfq84 9117:9117
Make sure to replace the pod-id with your own pod ID when you execute the port-forward command.
Now, we are connected to port 9117 of the WordPress Prometheus exporter and map the port to our local port 9117. Open http://localhost:9117 to check the output.
If you are not used to the Prometheus format, it might be a little bit confusing in the beginning. But it’s actually pretty easy to read. Each line without ‘#’ contains a metric key and a value behind it:
apache_cpuload 1.2766
process_resident_memory_bytes 1.6441344e+07
If you are not used to such metrics, don’t worry — you will get used to them quickly. You can Google each of the keys and find out what it means. After some time, you will identify what metrics are the most valuable for you and how they behave as soon as your containers receive more and more production traffic.
Let’s tidy up our setup by:
$ helm delete my-wordpress-prod release "my-wordpress-prod" uninstalled
We touched on a lot of deployment areas and features. We deployed multiple WordPress instances and scaled it up to more containers for production. You could even go one step further and activate auto-scaling. Check out the documentation of the Helm Chart and play around with it!
MariaDB Helm Chart
The persistence of the helm Chart for WordPress depends on MariaDB. It builds on another Helm Chart for MariaDB that you can configure and scale to your needs by, for example, starting multiple replicas.
The possibilities that you have when running containers in production using Kubernetes are enormous. The definition of the WordPress Chart is publicly available.
How to Create a Template for Custom Applications
Helm adds a lot more flexibility to your Kubernetes deployment files. Kubernetes deployment files are static by their nature. This means, adjustments like
- desired container count,
- environment variables or
- CPU and memory limit
are not adjustable by using plain Kubernetes deployment files. Either you solve this by duplicating configuration files or you put placeholders in your Kubernetes deployment files that are replaced at deploy-time.
Both of these solutions require some additional work and will not scale well if you deploy a lot of applications with different variations.
But for sure, there is a smarter solution that is based on Helm that contains a lot of handy features from the Helm community. Let’s create a custom Chart for a blogging engine, this time for a NodeJS based blog called ghost blog.
How to Start a Ghost Blog Using Docker
A simple instance can be started using pure Docker:
docker run --rm -p 2368:2368 --name my-ghost ghost
Our blog is available at: http://localhost:2368.
Let’s stop the instance to be able to launch another one using Kubernetes:
$ docker rm -f my-ghost my-ghost
Now, we want to deploy the ghost blog with 2 instances in our Kubernetes cluster. Let’s set up a plain deployment first:
# file 'application/deployment.yaml'apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost-app
spec:
selector:
matchLabels:
app: ghost-app
replicas: 2
template:
metadata:
labels:
app: ghost-app
spec:
containers:
- name: ghost-app
image: ghost ports:
- containerPort: 2368
and put a load balancer before it to be able to access our container and to distribute the traffic to both containers:
# file 'application/service.yaml'apiVersion: v1
kind: Service
metadata:
name: my-service-for-ghost-app
spec:
type: LoadBalancer
selector:
app: ghost-app
ports:
- protocol: TCP
port: 80
targetPort: 2368
We can now deploy both resources using kubectl:
$ kubectl apply -f ./appplication/deployment.yaml -f ./appplication/service.yaml deployment.apps/ghost-app created
service/my-service-for-ghost-app created
The ghost application is now available via http://localhost. Let’s again stop the application:
$ kubectl delete -f ./appplication/deployment.yaml -f ./appplication/service.yaml deployment.apps/ghost-app delete
service/my-service-for-ghost-app delete
So far so good, it works with plain Kubernetes. But what if we need different settings for different environments?
Imagine that we want to deploy it to multiple data centers in different stages (non-prod, prod). You will end up duplicating your Kubernetes files over and over again. It will be hell for maintenance. Instead of scripting a lot, we can leverage Helm.
Let’s create a new Helm Chart from scratch:
$ helm create my-ghost-app Creating my-ghost-app
Helm created a bunch of files for you that are usually important for a production-ready service in Kubernetes. To concentrate on the most important parts, we can remove a lot of the created files. Let’s go through the only required files for this example.
We need a project file that is called Chart.yaml:
# Chart.yamlapiVersion: v2
name: my-ghost-app
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: 1.16.0
The deployment template file:
# templates/deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: ghost-app
spec:
selector:
matchLabels:
app: ghost-app
replicas: {{ .Values.replicaCount }}
template:
metadata:
labels:
app: ghost-app
spec:
containers:
- name: ghost-app
image: ghost
ports:
- containerPort: 2368
env:
- name: url
{{- if .Values.prodUrlSchema }}
value: http://{{ .Values.baseUrl }}
{{- else }}
value: http://{{ .Values.datacenter }}.non-prod.{{ .Values.baseUrl }}
{{- end }}
It looks very similar to our plain Kubernetes file. Here, you can see different placeholders for the replica count, and an if-else condition for the environment variable called url. In the following files, we will see all the values defined.
The service template file:
# templates/service.yamlapiVersion: v1
kind: Service
metadata:
name: my-service-for-my-webapp
spec:
type: LoadBalancer
selector:
app: ghost-app
ports:
- protocol: TCP
port: 80
targetPort: 2368
Our Service configuration is completely static.
The values for the templates are the last missing parts of our Helm Chart. Most importantly, there is a default values file required called values.yaml:
# values.yamlreplicaCount: 1
prodUrlSchema: false
datacenter: us-east
baseUrl: myapp.org
A Helm Chart should be able to run just by using the default values. Before you proceed, make sure that you have deleted:
- my-ghost-app/templates/tests/test-connection.yaml
- my-ghost-app/templates/serviceaccount.yaml
- my-ghost-app/templates/ingress.yaml
- my-ghost-app/templates/hpa.yaml
- my-ghost-app/templates/NOTES.txt.
We can get the final output that would be sent to Kubernetes by executing a “dry-run”:
$ helm template --debug my-ghost-appinstall.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /helm/my-ghost-app---
# Source: my-ghost-app/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-service-for-my-webapp
spec:
type: LoadBalancer
selector:
app: my-example-app
ports:
- protocol: TCP
port: 80
targetPort: 2368
---
# Source: my-ghost-app/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost-app
spec:
selector:
matchLabels:
app: ghost-app
replicas: 1
template:
metadata:
labels:
app: ghost-app
spec:
containers:
- name: ghost-app
image: ghost
ports:
- containerPort: 2368
env:
- name: url
value: us-east.non-prod.myapp.org
Helm inserted all the values and also set url to us-east.non-prod.myapp.org
because in the values.yaml
, prodUrlSchema
is set to false and the datacenter is set to us-east.
To get some more flexibility, we can define some override value files. Let’s define one for each datacenter:
# values.us-east.yaml
datacenter: us-east# values.us-west.yaml
datacenter: us-west
and one for each stage:
# values.nonprod.yaml
replicaCount: 1
prodUrlSchema: false# values.prod.yaml
replicaCount: 3
prodUrlSchema: true
We can now use Helm to combine them as we want and check the result again:
$ helm template --debug my-ghost-app -f my-ghost-app/values.nonprod.yaml -f my-ghost-app/values.us-east.yaml install.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /helm/my-ghost-app---
# Source: my-ghost-app/templates/service.yaml
# templates/service.yamlapiVersion: v1
kind: Service
metadata:
name: my-service-for-my-webapp
spec:
type: LoadBalancer
selector:
app: my-example-app
ports:
- protocol: TCP
port: 80
targetPort: 2368
---
# Source: my-ghost-app/templates/deployment.yaml
# templates/deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: ghost-app
spec:
selector:
matchLabels:
app: ghost-app
replicas: 1
template:
metadata:
labels:
app: ghost-app
spec:
containers:
- name: ghost-app
image: ghost
ports:
- containerPort: 2368
env:
- name: url
value: http://us-east.non-prod.myapp.org
And for sure, we can do a final deployment:
$ helm install -f my-ghost-app/values.prod.yaml my-ghost-prod ./my-ghost-app/NAME: my-ghost-prod
LAST DEPLOYED: Mon Dec 21 00:09:17 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
As before, our ghost blog is available via http://localhost.
We can delete this deployment and deploy the application with us-east and non prod settings like this:
$ helm delete my-ghost-prod
release "my-ghost-prod" uninstalled$ helm install -f my-ghost-app/values.nonprod.yaml -f my-ghost-app/values.us-east.yaml my-ghost-nonprod ./my-ghost-app
We finally clean up our Kubernetes deployment via Helm:
$ helm delete my-ghost-nonprod
So we can combine multiple override value files as we want. We can automate deployments in a flexible way that we need for many use-cases of deployment pipelines.
Especially for companies, this means defining Chart Skeletons once to ensure the required criteria are fulfilled. Later, you can copy them and adjust them to the needs of your application.
How to Host a Helm Chart
Ok, you created your chart, now what? Do we have to download the entire repository to install those charts? No! Helm has a public library for the most used charts, which kind of works like Docker Hub.
You can also create your own chart repository and host it online. Helm drinks from the same fountain as HomeBrew, or Linux. You can tap these repositories to download charts contained in them.
Since a chart repository is basically an index.yaml
file served from a static web server, you can pretty much create a chart repository out of anywhere.
This means you don’t need to have your repository cloned forever, and your charts can be private as well. You only need to create a chart repository.
You can even use hosted services like Azure CR to do the job, or you can have a full solution called Chart Museum, which allows you to store your charts and provides you with a neat UI.