Write Helm chart from scratch

arman-shafiei

Arman Shafiei

Posted on April 1, 2024

Write Helm chart from scratch

These days Kubernetes has become so popular and of course more complex. Deploying and managing it has become harder and requires lots of time and effort. One of the most useful tools that can help us to make it easy for us is Helm.

Helm is known as a package manager for Kubernetes. You can use it to deploy a large Kubernetes application with just one install command. It makes our deployments easier and more beneficial than just using simple the kubectl command to deploy everything by ourselves.

We usually use charts that are already available in Helm provided by other developers, but sometimes you need to write your own charts, That’s what this article is about. We’re going to write our chart from scratch and deploy it on our Kubernetes Cluster.

Prerequisites

For this article, we will need:

  • Redhat-based or Debian-based OS

  • K8s cluster version 1.24

  • Helm version 3.9

  • Nginx Ingress version 4.2

  • Access to the internet

NOTE: The requirements mentioned above aren’t mandatory, however, this article has been tested and done by those tools.

Verify requirements

The first thing we’re going to do is to check the version of the tools.

Run the commands and check the output.

# cat /etc/*-release
CentOS Linux release 7.9.2009 (Core)
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
.
.
.
CentOS Linux release 7.9.2009 (Core)
CentOS Linux release 7.9.2009 (Core)

kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.3", GitCommit:"aef86a93758dc3cb2c658dd9657ab4ad4afc21cb", GitTreeState:"clean", BuildDate:"2022-07-13T14:29:09Z", GoVersion:"go1.18.3", Compiler:"gc", Platform:"linux/amd64"}

#############################################################

helm version
version.BuildInfo{Version:"v3.9.2", GitCommit:"1addefbfe665c350f4daf868a9adc5600cc064fd", GitTreeState:"clean", GoVersion:"go1.17.12"}
Enter fullscreen mode Exit fullscreen mode

Also, we have two nodes in our cluster.

#------------#----------#-----------------#
|    Host    |   Role   |   IP Address    |
#------------#----------#-----------------#
|   node-1   |  master  |  192.168.24.66  |
|   node-2   |  worker  |  192.168.24.118 |
#------------#----------#-----------------#
Enter fullscreen mode Exit fullscreen mode

Create Helm files

First, create a directory and name it test. This is the name of our Helm project.

cd ~
mkdir test
Enter fullscreen mode Exit fullscreen mode

Second, create two YAML files named Chart.yaml and values.yaml.

touch values.yaml Chart.yaml
Enter fullscreen mode Exit fullscreen mode

Third, Create “.helmignore” file to ignore files we don’t need in our app.

touch .helmignore
Enter fullscreen mode Exit fullscreen mode

Fourth, go to this test directory and create two other directories: charts and templates.

mkdir charts templates
Enter fullscreen mode Exit fullscreen mode

Finally, create YAML files for our app inside the templates directory.

cd templates
touch deployment.yaml service.yaml ingress.yaml\
      configmap.yaml secret.yaml
Enter fullscreen mode Exit fullscreen mode

Write files

Write deployment.yaml

Open deployment.yaml file inside the templates directory and add these lines to it.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
  labels:
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/version: {{ .Chart.AppVersion }}
    app.kubernetes.io/managed-by: helm
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
    spec:
      containers:
        - name: python
          image: docker.io/armanshafiei/python-command-executor:latest
          ports:
           - containerPort: {{ .Values.config.port }}
             name: http
          livenessProbe:
            httpGet:
              path: "/"
              port: {{ .Values.config.port }}
            initialDelaySeconds: 5
            periodSeconds: 5
          {{- include "vol_conf_mount" . | nindent 10}}
      {{- include "vol_conf_define" . | nindent 6 }}
Enter fullscreen mode Exit fullscreen mode

Assuming You know how to write Kubernetes manifests, let’s explain some parts:

  • Line 4: “.Chart.Name” adds the value of the variable name we’re going to define in Chart.yaml file. It is the name of our Chart or App.

  • Line 7: “.Chart.AppVersion” adds the value of variable appVersion from Chart.yaml. It is the version of our App.

  • Line 8: Defines that this deployment is managed and created by Helm.

  • Line 10: “.Values.replicas” Adds the value of the replicas variable that we’ll define in values.yaml.

  • Line 32: This imports the function vol_conf_mount from _helpers.tpl and adds 10 spaces as indention. The reason is even though we’ve added indentions before include, it doesn’t have any effect. This function enables or disables the volumeMounts option for deployment.

  • Line 33: This imports the function vol_conf_define from _helpers.tpl and adds 6 spaces as indention. This function enables or disables the volumes definition for deployment.

Write service.yaml

Open service.yaml inside the templates directory and these lines to it.

apiVersion: v1
kind: Service
metadata:
  name: {{ .Chart.Name }}
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: {{ .Chart.Name }}
  ports:
    - name: data
      protocol: TCP
      port: {{ .Values.config.port }}
      targetPort: {{ .Values.config.port }}
Enter fullscreen mode Exit fullscreen mode

Let’s explain some lines:

  • Line 6: This service is of type LoadBalancer So that it will be assigned an IP address, Either via a cloud provider or your external load balancer(e.g. Metallb).

  • Line 8: Use the label of deployment to connect the service to it.

Write ingress.yaml

Edit the ingress.yaml file inside the templates directory and add these lines to it.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Chart.Name }}
  labels:
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/version: {{ .Chart.AppVersion }}
    app.kubernetes.io/managed-by: helm
spec:
  ingressClassName: nginx
  rules:
    - host: "my.example.com"
      http:
        paths:
          - path: "/"
            pathType: Prefix
            backend:
              service:
                name: {{ .Chart.Name }}
                port:
                  number: {{ .Values.config.port }}
Enter fullscreen mode Exit fullscreen mode

In Line 10, we have defined the ingress class. It is necessary as we are using Nginx Ingress otherwise it won’t work as expected.

Write configmap.yaml

Edit configmap.yaml inside the templates directory and add these lines.

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Chart.Name }}
data:
  bind: {{ quote .Values.config.bind }}
  port: {{ quote .Values.config.port }
Enter fullscreen mode Exit fullscreen mode

The configmap.yaml is not necessary and doesn’t have any effect on our app because our Python app is not using any environment variables. The only reason I added it is to demonstrate the usability of config maps in Helm.

Write _helpers.tpl

This file is where we define Helm functions that our charts can use. As you saw above, we used two functions in our deployment.yaml.

Edit _helpers.tpl inside the templates directory and add these lines.

{{/*
Put all functions here please!
*/}}
{{- define "vol_conf_mount" }}
{{- if eq .Values.custom_config "true" -}}
volumeMounts:
            - name: configs
              mountPath: "/usr/app/envs"
              readOnly: true
{{- end }}
{{- end }}
{{- define "vol_conf_define" }}
{{- if eq .Values.custom_config "true" -}}
volumes:
          - name: configs
            configMap:
              name: {{ .Chart.Name }}
{{- end }}
{{- end }}
Enter fullscreen mode Exit fullscreen mode

As you see, there are two functions in our file: vol_conf_mount and vol_conf_define.

The vol_conf_mount function starting from lines 5 to 12, contains the volumeMounts block. If the value of custom_config in values.yaml equals true then this function returns volumeMounts block. So we can use it to import the volumeMounts to our manifest.

The vol_conf_define function starting from lines 14 to 21, contains the volumes block. If the value of custom_config in values.yaml equals true, then this function returns the volumes block. So we can use it to import the volumes to our manifest.

NOTE: The first block starting from lines 1 to 3, is used for comments and doesn’t have any effect on our application performance. You can write anything inside it depending on your charts or functions.

Write Chart.yaml

This file contains information about your Helm chart itself like the version, name, apiVersion, type, etc. Each of the variables can be referenced in the manifests by using “.Chart.*” like “.Chart.Name”.

Open Chart.yaml file and add these lines.

apiVersion: v2
name: "python"
description: This helm chart is created from scratch
type: application
version: 1.0.0
appVersion: "1.0.0"
Enter fullscreen mode Exit fullscreen mode

Let’s see what the values above are:

  • Line 1: Specifies which API version Helm will use. For Helm 3 it must be v2.

  • Line 2: The name of our Helm chart. It can be anything related to our app functionality.

  • Line 3: A description of our chart.

  • Line 4: Defines the type of chart. It can be either application or library. The library type can not be deployed and is just a dependency that can be injected into other charts while the application type can be deployed.

  • Line 5: Specifies the version of the chart. Each time we change the app version or change the templates we should increase this value.

  • Line 6: Specifies the version of the app. Each time we change the version of the app or change its code, we should increase this value. It’s recommended to use double quotes for this value.

Write values.yaml

If we need to use a variable inside our templates it shall be defined here. We define the variables and assign them a value here, so this way we don’t need to change our templates each time we need to change our ports or environment variables.

Open the file values.yaml and add these lines.

replicas: 2
config:
  bind: 0.0.0.0
  port: 5000
custom_config: "true"
Enter fullscreen mode Exit fullscreen mode

As you saw earlier in the templates, we used some variables which are defined here.

Let's say you want to access the bind variable in line 3 inside our manifest. To do so, we go like this:

{{ .values.config.bind }}
Enter fullscreen mode Exit fullscreen mode

Write .helmignore

Let’s say you are using an editor like Vscode, of course, you don’t need files created by your editor. To ignore those files you need to create ".helmignore" and add the files you want to ignore in your Helm. For example:

.git/
.gitignore
*.swp
*.bak
*.tmp
.idea/
.vscode/
Enter fullscreen mode Exit fullscreen mode

We added files related to git, Vscode, and temporary files as you see above. This way we make sure our unnecessary files won’t be added to our Helm deployment.

Run the App

Now everything is ready, we can deploy our Helm chart. Deploy the chart with the following command

helm install --create-namespace -n python-app python-app\
     python-app/
Enter fullscreen mode Exit fullscreen mode

Access the App

We configured our Ingress object with the hostname "my.example.com", therefore we need to access it via this domain. To do that we add this line to our /etc/hosts file:

192.168.20.2 my.example.com
Enter fullscreen mode Exit fullscreen mode

Now if we curl the domain we should get the result:

curl "http://my.example.com/execute?command=touch&argument=/file1"
The command executed is: touch /file1
The exit status code is: 0
Machine-id: python-5f949b9dc8-9wvhh
Enter fullscreen mode Exit fullscreen mode

If we do it again we see the machine-id value is changed.

curl "http://my.example.com/execute?command=touch&argument=/file1"
The command executed is: touch /file1
The exit status code is: 0
Machine-id: python-5f949b9dc8-ljpqk
Enter fullscreen mode Exit fullscreen mode

And if we exec into our containers and check the hostnames we get the following:

Container 1:
Container 1

Container 2:
Container 2

The reason is we have two replicas of our app in a pod. So each time gets routed to a different container. That’s it. Our Helm chart is deployed and our application is running as expected.

Conclusion

So far we created a chart, deployed it on a server, and got a result. What we did for this Helm chart was naive and the simplest. There are lots more we could add to our chart like adding dependencies to inject other charts to our chart, notes to add guidelines about the application, and other Kubernetes objects, and etc. As we will learn more, we write perfect, complex charts and enhance them in the future. So keep learning

I hope you find this article useful and please leave a comment or contact me via my page.

For more information about Helm please visit: https://helm.sh/docs/

💖 💪 🙅 🚩
arman-shafiei
Arman Shafiei

Posted on April 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related