Posted on 8 March 2022, updated on 15 March 2023.
Helm helps you write K8s templates and package them as a Chart. But it can be a painful experience when you are trying to debug/understand errors. I'll show you how you can test and validate your Helm Charts. This article won't go over the latest Helm release (3).
Helm Chart presentation
Helm packages are called Charts, and they consist of mainly YAML configuration files. Here’s the basic directory structure of a Chart based on the bests practices:
directory/
Chart.yaml # A YAML file containing information about the chart
values.yaml # The default configuration values for this chart
charts/ # A directory containing any charts upon which this chart depends.
templates/ # A directory of templates that, when combined with values,
# will generate valid Kubernetes manifest files.
And each part has specific roles :
- The Chart.yaml → It contains the description of your main Chart. Other Charts can be specified in the Charts/ directory as subcharts and retrieved from a chart repository.
- The values.yaml → This file contains the default values of the Chart and can be overridden by users during helm install or helm upgrade
- The templates/ → Contains the template files. When Helm evaluates a Chart, it will send all the files in the template rendering engine. Then it collects the result and sends it to Kubernetes.
This specific Template Rendering Engine can be quite difficult to work, since Helm errors are really painful to read and understand. We’ll try in this article to create a Chart and learn some tricks about context and range in Helm through templates.
But that also means you can use several go functions to template properly your Chart. Helm contains the Sprig Functions, except for env
and expandenv
for security reasons. It also has two special template functions: include
and ;required
. The include
function allows you to bring in another template, and then pass the results to other template functions.
Helm Chart case
Prerequisites: Helm 3 installed
Let’s start from zero and create a Chart named services:
helm create services
We won’t use subcharts in this example, so you can remove it:
rm rf services/charts
Then you should have the following Helm Chart:
services/
.helmignore
Chart.yaml
values.yaml
templates/
_helpers.tpl
deployment.yaml
hpa.yaml
ingress.yaml
NOTES.txt
service.yaml
serviceaccount.yaml
tests/
test-connection.yaml
We can see there are a lot of files in our templates/directory. How can we know what will be deployed when we’ll install our Chart?
helm template command can help us here. It will render our templates files based on our values.yaml to print the list of resources to deploy in the Kubernetes cluster:
helm template services/
---
# Source: services/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: RELEASE-NAME-services
labels:
helm.sh/chart: services-0.1.0
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
---
# Source: services/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: RELEASE-NAME-services
labels:
helm.sh/chart: services-0.1.0
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
---
# Source: services/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: RELEASE-NAME-services
labels:
helm.sh/chart: services-0.1.0
app.kubernetes.io/name: services
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: services
app.kubernetes.io/instance: RELEASE-NAME
template:
metadata:
labels:
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
spec:
serviceAccountName: RELEASE-NAME-services
securityContext:
{}
containers:
- name: services
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:
{}
---
# Source: services/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "RELEASE-NAME-services-test-connection"
labels:
helm.sh/chart: services-0.1.0
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['RELEASE-NAME-services:80']
restartPolicy: Never
→ This is the result you should have, and it tells you that your Chart is correct.
Let’s do some modifications on this Chart to see how to debug and understand a bit more Helm template rendering engine:
-
First, let’s say we don’t want any autoscaling in place, then delete templates/hpa.yaml and remove the following lines from your values.yaml:
replicaCount: 1 autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80
→ Then let’s try again:
helm template services/
Error: template: services/templates/deployment.yaml:8:20: executing "services/templates/deployment.yaml" at < .Values.autoscaling.enabled> : nil pointer evaluating interface {}.enabled
Use --debug flag to render out invalid YAML
→ We can see there is an error linked to the deployment.yaml:8:20. Let’s see what is there:
spec:
{- if not .Values.autoscaling.enabled }
replicas: { .Values.replicaCount }
{- end }
So here when we rendered this file, Helm had a nil pointer evaluating interface since we’ve removed the autoscaling part from our values.yaml. Let’s remove these lines since we want to remove the autoscaling (lines 8→10 included). After we try again with helm template, we can see we don’t have errors.
Helm Context
Let’s try to modify the context in our templates this time. We assume we want to generate more than one service account alongside our Chart.
Let’s analyze our ServiceAccount deployed. With our helm template, we’ve seen that we got the following ServiceAccount:
---
# Source: services/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: RELEASE-NAME-services
labels:
helm.sh/chart: services-0.1.0
app.kubernetes.io/name: services
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
---
It’s been generated with services/templates/serviceaccount.yaml:
{- if .Values.serviceAccount.create -}
apiVersion: v1
kind: ServiceAccount
metadata:
name: { include "services.serviceAccountName" . }
labels:
{- include "services.labels" . | nindent 4 }
{- with .Values.serviceAccount.annotations }
annotations:
{- toYaml . | nindent 4 }
{- end }
{- end }
On the services/values.yaml, it’s using the following values:
serviceAccount:
create: true
annotations: {}
name: ""
In this example, we would like to generate 3 ServiceAccounts: admin, data, and dev.
sa_list:
- name: "sa_admin"
create: true
annotations: {}
labels:
account: admin
- name: "sa_data"
create: true
annotations: {}
labels:
account: data
- name: "sa_dev"
create: true
annotations: {}
labels:
account: dev
But with this new structure, our services/templates/serviceaccount.yaml need to be changed since it won't work with helm template. We’ll use the range function this time to generate multiple ServiceAccount based on a single template file serviceaccount.yaml. Let’s analyze our current configuration:
{- if .Values.serviceAccount.create -}
apiVersion: v1
kind: ServiceAccount
metadata:
name: { include "services.serviceAccountName" . }
labels:
{- include "services.labels" . | nindent 4 }
{- with .Values.serviceAccount.annotations }
annotations:
{- toYaml . | nindent 4 }
{- end }
{- end }
Let’s analyze our template structure:
- [...] 1636362090000 →
- Here, if value = true, then [...] will be rendered
- 1636362090000
- Include let us include the value inside our template file when rendering.
- → Format your content to YAML
Let’s now modify our file with a range function:
{- range $sa_list := .Values.sa_list }
---
{- if .Values.serviceAccount.create -}
apiVersion: v1
kind: ServiceAccount
metadata:
name: { include "services.serviceAccountName" . }
labels:
{- include "services.labels" . | nindent 4 }
{- with .Values.serviceAccount.annotations }
annotations:
{- toYaml . | nindent 4 }
{- end }
{- end }
{- end }
→ Now, the template should generate ServiceAccount based on the list sa_list
To go further
We’ve seen a lot of useful tips about Helm, but there actually exist another tool you can use : Kustomize. Kustomize is a tool that uses layers and patches instead of templates to customize Kubernetes resources. It introduces the kustomization.yaml manifest file, in which users store deployment-specific configurations.
Test and Deploy. This kind comes tricky after all the validation mess we’ve seen above. There exists actually tools that might help you validate modifications on your main branch, like ct. As for example, you can test and release helm Chart with github-actions.
Conclusion
I hope all these tips will help you improve the quality of your Helm Charts, and help your team work better together! Most of these tips I found useful in my own experience, but since I am not all-knowing, there are probably different ways to improve your Chart as well 😉. Do not hesitate to share them with us on Twitter or LinkedIn!