Writing Your First Helm Chart

October 3, 2017   

I recently found myself writing instructions on how to deploy an application to several Kubernetes platform and ended up writing a different Kubernetes manifests for each platform. 95% of the content was the same with just a few different directives based on how the particular platform handles ingress, or if we needed a Registry secret or a TLS certificate.

Kubernetes manifests are very declarative and don’t offer any way to put conditionals or variables that could be set in them. This is both a good and a bad thing. Enter Helm a Package Manager for Kubernetes. Helm allows you to package up your Kubernetes application as a package that can be deployed easily to Kubernetes, One of its features (and the one that interested me) the ability to template out your Kubernetes manifests.

If you already have a Kubernetes manifest its very easy to turn it into a Helm Chart that you can then iterate over and improve as you need to add more flexibility to it. In fact your first iteration of a Helm chart can be as simple as moving your manifests into a new directory and adding a few lines to a Chart.yaml file.

Prerequisites

You’ll need the following installed to follow along with this tutorial:

Prepare Environment

Bring up a test Kubernetes environment using Minikube:

$ minikube start
Starting local Kubernetes v1.7.5 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.

Wait a minute or so and install Helm’s tiller service to Kubernetes:

$ helm init
$HELM_HOME has been configured at /home/pczarkowski/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!

Create a path to work in:

$ mkdir -p ~/development/my-first-helm-chart
$ cd ~/development/my-first-helm-chart

If it fails out you may need to wait a few more minutes for minikube to become accessible.

Create Example Kubernetes Manifest.

Writing a Helm Chart is easier when you’re starting with an existing set of Kubernetes manifests. One of the easiest ways to get a basic working manifest is to ask Kubernetes to run something and then fetch the manifest.

$ mkdir manifests
$ kubectl run example --image=nginx:1.13.5-alpine \
    -o yaml > manifests/deployment.yaml
$ kubectl expose deployment example --port=80 --type=NodePort \
    -o yaml > manifests/service.yaml
$ minikube service example --url       
http://192.168.99.100:30254

All going well you should be able to hit the provided URL and get the “Welcome to nginx!” page. You’ll see you now have two Kubernetes manifests saved. We can use these to bootstrap our helm charts:

$ tree manifests
manifests
├── deployment.yaml
└── service.yaml
0 directories, 2 files

Before we move on we should clean up our environment. We can use the newly created manifests to help:

$ kubectl delete -f manifests
deployment "example" deleted
service "example" deleted

Create and Deploy a Basic Helm Chart

Helm has some tooling to create the scaffolding needed to start developing a new Helm Chart. We’ll create it with a placeholder name of helm:

$ helm create helm
Creating helm
tree helm
helm
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── ingress.yaml
│   ├── NOTES.txt
│   └── service.yaml
└── values.yaml
2 directories, 7 files

Helm will have created a number of files and directories.

  • Chart.yaml - the metadata for your Helm Chart.
  • values.yaml - values that can be used as variables in your templates.
  • templates/*.yaml - Example Kubernetes manifests.
  • _helpers.tpl - helper functions that can be used inside the templates.
  • templates/NOTES.txt - templated notes that are displayed on Chart install.

Edit Chart.yaml so that it looks like this:

apiVersion: v1
description: My First Helm Chart - NGINX Example
name: my-first-helm-chart
version: 0.1.0

Copy our example Kubernetes manifests over the provided templates and remove the currently unused ingress.yaml and NOTES.txt.

$ cp manifests/* helm/templates/
$ rm helm/templates/ingress.yaml
$ rm helm/templates/NOTES.txt

Next we should be able to install our helm chart which will deploy our application to Kubernetes:

$ helm install -n my-first-helm-chart helm
NAME:   my-first-helm-chart
LAST DEPLOYED: Tue Oct  3 10:20:57 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME     CLUSTER-IP  EXTERNAL-IP  PORT(S)       AGE
example  10.0.0.210  <nodes>      80:30254/TCP  0s

==> v1beta1/Deployment
NAME     DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
example  1        1        1           0          0s

Like before we can use minikube to get the URL:

$ minikube service example --url    
http://192.168.99.100:30254

Again accessing that URL via your we browser should get you the default NGINX welcome page.

Congratulations! You’ve just created and deployed your first Helm chart. However we’re not quite done yet. use Helm to delete your deployment and then lets move on to customizing the Helm Chart with variables and values:

$ helm del --purge my-first-helm-chart
release "my-first-helm-chart" deleted

Add variables to your Helm Chart

Check out helm/values.yaml and you’ll see there’s a lot of variables already defined by the example that helm provided when you created the helm chart. You’ll notice that it is has values for nginx in there. This is because Helm also uses nginx as their example. We can re-use some of the values provided but we should clean it up a bit.

Edit helm/values.yaml to look like this:

replicaCount: 1
image:
  repository: nginx
  tag: 1.13.5-alpine
  pullPolicy: IfNotPresent
  pullSecret:
service:
  type: NodePort

We can access any of these values in our templates using the golang templating engine. For example accessing replicaCount would be written as {{ .Values.replicaCount }}. Helm also provides information about the Chart and Release which we’ll also utilize.

Update your helm/templates/deployment.yaml to utilize our values:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  creationTimestamp: 2017-10-03T15:03:17Z
  generation: 1
  labels:
    run: "{{ .Release.Name }}"
    chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
    release: "{{ .Release.Name }}"
    heritage: "{{ .Release.Service }}"     
  name: "{{ .Release.Name }}"
  namespace: default
  resourceVersion: "3030"
  selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/example
  uid: fd03ac95-a84b-11e7-a417-0800277e13b3
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      run: "{{ .Release.Name }}"
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: "{{ .Release.Name }}"
    spec:
      {{- if .Values.image.pullSecret }}    
            imagePullSecrets:
              - name: "{{ .Values.image.pullSecret }}"
      {{- end }}          
      containers:
      - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        name: example
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status: {}

Note the use of the if statement around image.pullSecret being set. This sort of conditional becomes very important when making your Helm Chart portable across different Kubernetes platforms.

Next edit your helm/templates/service.yaml to look like:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: 2017-10-03T15:03:30Z
  labels:
    run: "{{ .Release.Name }}"
    chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
    release: "{{ .Release.Name }}"
    heritage: "{{ .Release.Service }}"  
  name: "{{ .Release.Name }}"
  namespace: default
  resourceVersion: "3066"
  selfLink: /api/v1/namespaces/default/services/example
  uid: 044d2b7e-a84c-11e7-a417-0800277e13b3
spec:
  clusterIP:
  externalTrafficPolicy: Cluster
  ports:
  - nodePort: 30254
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    run: "{{ .Release.Name }}"
  sessionAffinity: None
  type: "{{ .Values.service.type }}"
status:
  loadBalancer: {}

Once your files are written out you should be able to install the Helm Chart:

$ helm install -n second helm
NAME:   second
LAST DEPLOYED: Tue Oct  3 10:59:41 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME    CLUSTER-IP  EXTERNAL-IP  PORT(S)       AGE
second  10.0.0.160  <nodes>      80:30254/TCP  1s

==> v1beta1/Deployment
NAME    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
second  1        1        1           0          1s

Next use minikube to get the URL of the service, but since we templated the service name to match the release you’ll want to use this new name:

$ minikube service second --url
http://192.168.99.100:30254

Now lets try something fun. Change the image we’re using by upgrading the helm release and overriding some values on the command line:

$ helm upgrade --set image.repository=httpd --set image.tag=2.2.34-alpine second helm
Release "second" has been upgraded. Happy Helming!
LAST DEPLOYED: Tue Oct  3 11:09:30 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME    CLUSTER-IP  EXTERNAL-IP  PORT(S)       AGE
second  10.0.0.160  <nodes>      80:30254/TCP  9m

==> v1beta1/Deployment
NAME    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
second  1        1        1           0          9m

Then go to our minikube provided URL and you’ll see a different message It works!.

Clean up

use minikube delete to clean up your environment:

minikube delete
Deleting local Kubernetes cluster...
Machine deleted.

Conclusion

Helm is a very powerful way to package up your Kubernetes manifests to make them extensible and portable. While it is quite complicated its fairly easy to get started with it and if you’re like me you’ll find yourself replacing the Kubernetes manifests in your code repos with Helm Charts.

There’s a lot more you can do with Helm, we’ve just scratched the surface. Enjoy using and learning more about them!