7 Helm Best Practices with Examples
As we develop our charts, we’ll notice we have multiple solutions to most problems. When we can solve something in, say, eight different ways, as beginners, it may be hard to know which path would be best.
So let’s go through a list of general guidelines, requirements, and recommendations, just to get a sense of some optimal ways to deal with Helm charts.
#1. Chart Names and Versions
Chart Names
Chart names should only contain lowercase letters (and, if necessary, numbers). Also, if your name has to have more than one word, separate these words with hyphens '-', for example, my-nginx-app.
Valid names:
nginx
wordpress
wordpress-on-nginx
Invalid names:
Nginx
Wordpress
wordpressOnNginx
wordpress_on_nginx
Version Numbers
Helm recommends what is called SemVer2 notation (Semantic Versioning 2.0.0). The short explanation is this: version numbers are in the form of 1.2.3 (MAJOR.MINOR.PATCH). The first number represents the major version number, the second the minor, and the third is the patch number.
Let’s say you just created a new application last night. This could be version 0.1.0. You find a security bug, and you patch it. This would be version 0.1.1. Later, you find another bug, and you patch that too. You arrive at version 0.1.2. You make some very small changes to your app - like maybe you change a function so that it executes 2% faster - this could be version 0.2.0.
Notice how when you update the minor version number, the patch number gets reset to 0. And finally, you make a huge update to your app, adding a lot of new features and changing some functionalities. This way, you arrive at version 1.0.0.
#2. values.yaml file
Variable Names
All variable names in the values.yaml file should begin with lowercase letters.
Valid name:
enabled: false
Invalid name:
Enabled: false
Often, your variable will need to contain multiple words to describe it better. Use what is called camelcase
: the first word starts with a lowercase letter, but the next ones all start with a capital letter.
Examples of valid names:
replicaCount: 3
wordpressUsername: user
wordpressSkipInstall: false
Invalid names:
ReplicaCount: 3
wordpress-username: user
wordpress_skip_install: false
Flat vs. Nested Values
We saw in our exercises that we can have nested values like this:
image:
repository:
pullPolicy: IfNotPresent
tag: "1.16.0"
This provides some logical grouping. In the snippet above image
is the parent variable with three children values. But this can be rewritten to a flat format, like this:
imageRepository:
imagePullPolicy: IfNotPresent
imageTag: "1.16.0"
When to use nested format:
- If you have many related variables and at least one of them is always used.
When to use flat format:
- If you have very few related variables
- If all your related variables are optional
Quote Strings
Always wrap your string values between quote signs. For instance:
enabled: "false"
The configuration above assigns a string value 'false' to the enabled
variable. Consider the value assignment below:
enabled: false
This will assign a boolean type to the enabled
variable.
Large Integers
Assume you have this:
number: 1234567
When parsing your template files, this value might get converted to scientific notation and end up as 1.234567e+06
. If you run into this issue (which may happen especially with large numbers), you can define a number as a string.
number: "1234567"
Then, when you need to use this variable in your template files, prefix it with the type conversion function int (or int64 for very large numbers).
number: {{ int .Values.number }}
This way, the string “1234567” will get converted into the number 1234567 and won’t end up as 1.234567e+06
.
You can see other type conversion functions here.
Documentation for the values.yaml File
If when a user opens up a values.yaml file all they get is a long list of variables, it can be very hard for them to figure out what each of them is used for. That’s why you should always document each by adding a comment above it. The comment starts with a #. Example:
# replicaCount sets the number of replicas to use if autoscaling is enabled
replicaCount: 3
Notice how we start the comment with the actual variable name. It’s recommended you do it the same way to make searching easier. It also helps automatic documentation tools correlate these notes with the parameters that they describe.
To learn more about adding comments to your YAML file, check out our blog post: How to Add YAML Comments with Examples.
#3. Template Files
Names
Template file names should all be lowercase letters. If they contain multiple words, these should be separated with dashes, '-'.
Examples of correct file names:
deployment.yaml
tls-secrets.yaml
Incorrect file names:
Deployment.yaml
tls_secrets.yaml
These file names should indicate the kind of Kubernetes resource they define so that users can get an idea of what's in the file from a glance. For long resource names, like a Persistent Volume Claim, you might use their acronym (PVC in this case) and end up with pvc.yaml.
YAML Comments vs. Template Comments
A template comment follows this syntax:
{{/*
Comment goes here
*/}}
You should document anything that is not immediately obvious. Often, your .tpl files (like _helpers.tpl) will need to be commented on thoroughly, as it’s not easy to figure out what goes on there. Take a look at this:
{{- define "wordpress.memcached.fullname" -}}
{{- printf "%s-memcached" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "wordpress.image" -}}
{{- include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) -}}
{{- end -}}
It's pretty hard to figure out what is going on in the file above, and even if we understand what happens, we might wonder, “Why use trunc 63
to truncate to 63 characters?” We can add some comments to clarify it:
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "wordpress.memcached.fullname" -}}
{{- printf "%s-memcached" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Return the proper WordPress image name
*/}}
{{- define "wordpress.image" -}}
{{- include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) -}}
{{- end -}}
The file above becomes much easier to understand.
This type of comment (template comment) won’t be displayed if the user enters a command like helm install –debug
to see what manifests this chart would generate. If you need these comments to be output in such commands, you can use YAML comments instead. These look like this:
# This comment would be seen in a helm install --debug command.
spec:
type: ClusterIP
ports:
- name: metrics
port: {{ .Values.metrics.service.port }}
protocol: TCP
targetPort: metrics
#4. Dependencies
Versions
When your chart depends on other charts, you must declare in Chart.yaml the versions of these dependency charts. Instead of using an exact version like:
version: 1.8.2
You should declare an approximate version by prefixing it with the tilde ~
sign.
version: ~1.8.2
This is equivalent to saying, “The version number starts with 1.8, and it can be anything higher than or equal to 1.8.2 but strictly lower than 1.9.0 (it can be 1.8.9 or 1.8.10, but not 1.9.0)”.
URLs
When available, use the more secure https://
for repository URLs instead of http://
. HTTP’s encryption and certificate authentication provide extra security for your requests. For instance, they make it harder for someone to perform a man-in-the-middle attack, which can be used to inject malicious code into our dependency charts.
#5. Recommended Labels
Kubernetes operators find objects by looking at the metadata - more specifically, the labels in the metadata section. There are many objects in the Kubernetes cluster, but only part of them are installed by and managed by Helm. Adding a label such as helm.sh/chart: nginx-0.1.0
makes it easy for operators to find all objects installed by the nginx chart, version 0.1.0.
Your templates should, preferably, generate templates with labels similar to this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-nginx
labels:
helm.sh/chart: nginx-0.1.0
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
spec:
serviceAccountName: RELEASE-NAME-nginx
securityContext:
{}
containers:
- name: nginx
securityContext:
{}
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{}
Check out this page to better understand how to use labels. To see an example of how labels are used in templates, use this command:
helm create sample
Then explore the sample/templates/ directory and sample/templates/_helpers.tpl file to see how these are defined in a clean, rather simple chart.
#6. Pods and PodTemplates
Whenever we declare Kubernetes objects such as Deployments, ReplicationControllers, ReplicaSets, DaemonSets, or StatefulSets, we include definitions of PodTemplates.
We tell Kubernetes the kind of Pods that it will need to launch, hence a template for these Pods. That is because something like a deployment, although just one object, might launch tens of Pods. Here’s a list of best practices when we work with such sections.
Images Versions/Tags
Do not use an image tag such as latest
or any other similar tag that doesn’t point to a specific, fixed image. The latest
tag is what we’d call a “floating” tag, meaning the image version it points to is constantly changing; today, it might point to 1.2.3. Tomorrow, when a new version is launched, it might point to 1.2.4 or, even worse, to 2.0.1 (a major update with significant changes).
The components installed by the chart usually need to fit perfectly together, and the latest
tag takes out the perfect fit guarantee. If there is a major update to one component, it might not work with the other pieces due to incompatible changes in the latest version.
The recommended practice is to point to a specific image version and use values.yaml as the place where it can be modified. See the sample values.yaml below:
image:
repository:
pullPolicy: IfNotPresent
tag: "1.16.0"
Here is the deployment.yaml that pulls the image name and version (tag) from the values.yaml file:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
If the user did not define a repository name in his values.yaml file, we use a default name for the chart, nginx. We didn’t do this in our example, but we should define a default version in a real production-ready chart. The line:
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag }}"
Should be:
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag | default "1.16.0" }}"
This way, we achieve the best of both worlds:
- The chart will produce predictable results; the same images will be installed in all default installs (when the user doesn’t bother to choose his settings).
- The user still has the power to choose other versions for his images if he has some specific needs. For example, if his desired features only exist in a newer software version).
So we give our users predictable results and the freedom to choose if they so desire.
Include Selectors in Your PodTemplates
Consider the manifest below that would get generated by the default nginx/templates/deployment.yaml file created by the helm create nginx
command:
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-nginx
labels:
helm.sh/chart: nginx-0.1.0
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
spec:
serviceAccountName: RELEASE-NAME-nginx
securityContext:
{}
containers:
- name: nginx
securityContext:
{}
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{}
As we mentioned earlier, objects like Kubernetes deployments can create multiple pods. There will also be Pods launched by other objects, resulting in many Pods that are mixed together. There needs to be a way to know which Pods belong to a group and which belong to another group.
This is exactly what selector labels help in doing. Your selectors should be the label, or labels (can be one or more) that you know will never change for the entire duration that the object is running. Let us see this in the example below:
labels:
helm.sh/chart: nginx-0.1.0
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
We chose these two as our selectors:
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: RELEASE-NAME
We can't choose app.kubernetes.io/version: "1.16.0"
as a selector since the version label will definitely change in the future.
#7. Custom Resource Definitions
Custom Resource Definitions (CRDs) are basically a way to add extra features to Kubernetes. These require some special attention on our part when using them with Helm. To understand why to think about this:
Kubernetes is built to ingest definitions of many objects, even all at once. Say we send it some Deployment manifests for some MySQL pods, PersistentVolume manifests, PersistentVolumeClaims, and so on. We, as the users, don’t care in which order we send these to Kubernetes: But MySQL would not be able to work if it does not have a persistent volume where it can store its database.
Keep in mind that we do not specify anywhere, “Hey, first launch these persistent volumes, and only after that launch the MySQL pods.” But our cluster will know what to do. When we want to install CRDs with Helm, though, the story is different.
Let’s say we define a cool new object in our CRD. Now, we can use this new type of object in Kubernetes by starting our manifests with something like:
apiVersion: "stable.example.com/v1"
kind: CoolObject
But Helm needs to send both things to our cluster: the CRD and the manifest of this object. Now, the order matters, or more specifically, the timing. A few seconds might pass until this new CRD is registered. If Kubernetes receives this object before the CRD is registered, it will not know what this is and how to create it since it does not know the CRD. It simply doesn’t know what this CoolObject
is.
To tackle this, Helm gives you a special location for CRDs. You need to add a crds directory to the chart we have created. This directory should exist in the same place where we have the templates and charts directory.
nginx
├── charts
├── Chart.yaml
├── crds
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── NOTES.txt
│ ├── service.yaml
│ └── tests
│ └── test-nginx.yaml
└── values.yaml
Now Helm will install your CRDs first, then your other objects.
You can’t have a line like this in your CRD.
name: {{ .Chart.Name }}
It should be the exact content you want to pass along:
name: someName
Disadvantages of Installing CRDs with Helm
You cannot upgrade or delete CRDs with Helm.
You also cannot perform a –dry-run install to test if your chart works with Kubernetes without actually installing anything. The dry run would require the CRD to actually get installed so that the other objects in your chart get recognized on a dry run. But that defeats the purpose of not installing anything. Without the CRD, any objects in your chart that want to use this Custom Resource will fail on a dry run test.
ENROLL in our Helm For Beginner's Course to learn other concepts, such as creating Helm Charts, adding dependencies in them, and distributing them.
Conclusion
Helm charts provide a standardized way of packaging and deploying applications, making it easier for teams to collaborate and share their work. Follow the best practices listed in this article to create reliable Charts. Also, ensure that you perform tests on your charts before deploying in a production environment.
If you want a quick intro to Helm charts - how to create and install them, check out this video:
More on Helm: