debugging_helm

8 March 2022

Helm is the package manager for Kubernetes. It has been written in go templates and helps you write Kubernetes templates and package it as a Chart with all of its dependencies. Writing these templates in order to render Kubernetes manifests can still be a painful experience, even more when you are trying to debug and understand errors. This article won't go over the latest Helm release (3), what is it and what are its fundamental commands.

 

There is no good debugger support available today, so you can spend a lot of time searching for similar issues on the web, and in a lot of cases don't even find valuable answers. In this article, I will show you how you can test and validate your Helm Charts.

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.
  • 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 that Kubernetes will deploy:

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 objects. 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!