
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.
Helm tries to solve some of the challenges with useful features that increase productivity and reduce maintenance efforts of complex deployments.
In this post you will learn:
- What Helm is
- The most common use-cases of Helm
- How to configure and deploy a publicly available Helm package
- How to deploy a custom application using Helm
Every code example in this post requires a Kubernetes cluster. The easiest way to get a cluster to play with is to install Docker and activate its Kubernetes cluster feature. Also, you need to install kubectl and Helm to interact with your cluster.
Please note: When you try the examples, be patient. If you are too fast then the containers are not ready. It might take a few minutes until the containers can receive requests.
What is Helm?
Helm calls itself ”The Kubernetes package manager”. It is a command-line tool that enables you to create and use so-called Helm Charts.
A Helm Chart is a collection of templates and settings that describe a set of Kubernetes resources. Its power spans from managing a single node definition to a highly scalable multi-node cluster.
The architecture of Helm has changed over the last years. The current version of Helm communicates directly to your Kubernetes cluster via Rest. If you read something about Tiller in the context of Helm, then you’re reading an old article. Tiller was removed in Helm 3.
Helm itself is stateful. When a Helm Chart gets installed, the defined resources are getting deployed and meta-information is stored in Kubernetes secrets.
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 list NAME 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 secret NAME 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.
In the next section, we will create our own Helm Chart with a basic application to understand the fundamentals of creating a Helm Chart and to make a static container deployment more dynamic.
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.yaml apiVersion: 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.yaml apiVersion: 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.yaml apiVersion: 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.yaml replicaCount: 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-app install.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.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
# 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: 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.
Conclusion
The power of a great templating engine and the possibility of executing releases, upgrades, and rollbacks makes Helm great. On top of that comes the publicly available Helm Chart Hub that contains thousands of production-ready templates. This makes Helm a must-have tool in your toolbox if you work with Kubernetes on a bigger scale!
I hope you enjoyed this hands-on tutorial. Motivate yourself to Google around, check out other examples, deploy containers, connect them, and use them.
You will learn many cool features in the future that enable you to ship your application to production in an effortless, reusable and scalable way.
As always, I appreciate any feedback and comments. Say thank you by following me on Twitter and by sharing this post with others. I write blog posts to share my knowledge in a very “hands-on” way to foster motivation, eagerness and excitement.
References:
