Writing Helm Charts

Saurabh Sharma

Helm is a dedicated package manager for Kubernetes.

Although the Kubernetes objects are deployed in YAML’s there was a need to package and club them together and customise the deployment (not the K8S deployment).

Helm uses Go templates.

Please refer to official go documentation on template package for details. You can also look at examples in my repo here.

Actions from the official documentation

{{/* a comment */}}
{{- /* a comment with white space trimmed from preceding and following text */ -}}
	A comment; discarded. May contain newlines.
	Comments do not nest and must start and end at the
	delimiters, as shown here.

{{pipeline}}
	The default textual representation (the same as would be
	printed by fmt.Print) of the value of the pipeline is copied
	to the output.

{{if pipeline}} T1 {{end}}
	If the value of the pipeline is empty, no output is generated;
	otherwise, T1 is executed. The empty values are false, 0, any
	nil pointer or interface value, and any array, slice, map, or
	string of length zero.
	Dot is unaffected.

{{if pipeline}} T1 {{else}} T0 {{end}}
	If the value of the pipeline is empty, T0 is executed;
	otherwise, T1 is executed. Dot is unaffected.

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
	To simplify the appearance of if-else chains, the else action
	of an if may include another if directly; the effect is exactly
	the same as writing
		{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

{{range pipeline}} T1 {{end}}
	The value of the pipeline must be an array, slice, map, or channel.
	If the value of the pipeline has length zero, nothing is output;
	otherwise, dot is set to the successive elements of the array,
	slice, or map and T1 is executed. If the value is a map and the
	keys are of basic type with a defined order, the elements will be
	visited in sorted key order.

{{range pipeline}} T1 {{else}} T0 {{end}}
	The value of the pipeline must be an array, slice, map, or channel.
	If the value of the pipeline has length zero, dot is unaffected and
	T0 is executed; otherwise, dot is set to the successive elements
	of the array, slice, or map and T1 is executed.

{{template "name"}}
	The template with the specified name is executed with nil data.

{{template "name" pipeline}}
	The template with the specified name is executed with dot set
	to the value of the pipeline.

{{block "name" pipeline}} T1 {{end}}
	A block is shorthand for defining a template
		{{define "name"}} T1 {{end}}
	and then executing it in place
		{{template "name" pipeline}}
	The typical use is to define a set of root templates that are
	then customized by redefining the block templates within.

{{with pipeline}} T1 {{end}}
	If the value of the pipeline is empty, no output is generated;
	otherwise, dot is set to the value of the pipeline and T1 is
	executed.

{{with pipeline}} T1 {{else}} T0 {{end}}
	If the value of the pipeline is empty, dot is unaffected and T0
	is executed; otherwise, dot is set to the value of the pipeline
	and T1 is executed.

Example

Let’s show a go template example before we move to the Helm

// Letter a template for the automatic reply.
const Letter = `
Dear {{.Name}},
{{/* Check if the person identified by .Name attended the event. */}}
{{if .Attended}}
It was a pleasure to meet you at the wedding.
{{- else}}
{{/* Person identified by .Name did not attended the event. */}}
It is a shame you couldn't make it to the wedding.
{{- end}}
{{with .Gift -}}
Thank you for the lovely {{.}}.
{{end}}
Best wishes,
Josie
`

// Recipient Identifies the recipient
type Recipient struct {
	Name, Gift string
	Attended   bool
}

If I need to evaluate this template which is nothing just a letter template that works on some values provided.

Will use sample values for the struct Recipient as under

var recipients = []two.Recipient{
		{"Aunt Mildred", "bone china tea set", true},
		{"Uncle John", "moleskin pants", false},
		{"Cousin Rodney", "", false},
	}
// exampleTwo - Uses the actions and pipelines
func exampleTwo() {
	t := template.Must(template.New("letter").Parse(two.Letter))

	// Execute the template for each recipient.
	for _, r := range recipients {
		err := t.Execute(os.Stdout, r)
		if err != nil {
			log.Println("executing template:", err)
		}
	}
}

Once invoked it will print on the console.

Dear Aunt Mildred,

It was a pleasure to meet you at the wedding.
Thank you for the lovely bone china tea set.

Best wishes,
Josie
Dear Uncle John,

It is a shame you couldn't make it to the wedding.
Thank you for the lovely moleskin pants.

Best wishes,
Josie
Dear Cousin Rodney,

It is a shame you couldn't make it to the wedding.

Best wishes,
Josie

Local – helm version

The local helm version I am using on my box – helm version

version.BuildInfo{Version:"v3.3.1", GitCommit:"249e5215cde0c3fa72e27eb7a30e8d55c9696144", GitTreeState:"dirty", GoVersion:"go1.15"}
helm version --short
v3.3.1+g249e521

Creating my chart

helm create epserver

It creates some files as mentioned below

.
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

I will give a brief about most of the file I edit, but you read more anytime from the official Helm documentation

Chart.yaml

A YAML file containing information about the chart

templates/ [DIRECTORY]

A directory of templates that, when combined with values, will generate valid Kubernetes manifest files.

values.yaml

The default configuration values for this chart

charts/ [DIRECTORY]

A directory containing any charts upon which this chart depends.

Added some details in the Chart.yaml, which are self explanatory

# Minimum Version 3 required so v2
apiVersion: v2
name: epserver
description: A sample go server that exposes few endpoints.
type: application
version: 2.0.0
kubeVersion: ~1.18.x
appVersion: 3.0.0

serviceaccount.yaml

{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "epserver.serviceAccountName" . }}
  labels:
    {{- include "epserver.labels" . | nindent 4 }}
  {{- with .Values.serviceAccount.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
{{- end }}

I will use my application epserver and create a chart for it just to check if simple modifications work.

All, code is available in my github repo.

Values.yaml

# Replica count
replicaCount: 1
# Only 1 replica required

# Labels for container
appname: epserver

# define the environment variable.
env:
  defined: true
  vars:
    - name: SERVERPORT
      value: "9090"

image:
  repository: samarthya/epserver
  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
  tag: "2.0"

imagePullSecrets: []
nameOverride: "samarthya"
fullnameOverride: "eps"

# Samarthya: Added v1
environment: "test"

serviceAccount:
  # Specifies whether a service account should be created
  create: true
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: sa-epserver

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  type: ClusterIP
  port: 9090

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources:
# define limits
  limits:
    memory: "250Mi"
    cpu: "2000m"

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

Can you spot the changes done?

  • I have added resources, limits, memory
  • Have added service port
  • Added Image
image:
  repository: samarthya/epserver
  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
  tag: "2.0"
  • Added a network policy for allow all traffic to the pods.
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
spec:
  podSelector:
    matchLabels:
      app: {{ .Values.appname }}
  ingress:
  - {}
  policyTypes:
  - Ingress

Publishing Chart

Step 1

helm package .

This should generate the chart packaging like

epserver-2.0.0.tgz

You can update the local repo or publish to your own helm-git-repo.

Command that come in handy

helm repo search samarthya

Where samarthya is my local repo

helm repo list
NAME     	URL                                                           
bitnami  	https://charts.bitnami.com/bitnami                            
samarthya	https://raw.githubusercontent.com/samarthya/hlmepserver/master
helm repo index .

This you can run in the local directory where the tgz was generated.

helm install eps samarthya/epserver
NAME: eps
LAST DEPLOYED: Mon Sep 14 17:48:05 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=samarthya,app.kubernetes.io/instance=eps" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80

You can check all the objects created

k get all
NAME                       READY   STATUS              RESTARTS   AGE
pod/eps-58fbc9cfb4-2jbdz   0/1     ContainerCreating   0          3s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/eps          ClusterIP   10.111.254.111   <none>        9090/TCP   3s
service/kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP    28d

NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/eps   0/1     1            0           3s

NAME                             DESIRED   CURRENT   READY   AGE
replicaset.apps/eps-58fbc9cfb4   1         1         0       3s
curl -m 2 http://10.111.254.111:9090/hello?msg=My_message_my_rule
{"message":"My_message_my_rule","timestamp":"2020-09-14T18:00:27.929269256Z"}

You can see the output will reflect the message is working.

k logs service/eps
2020/09/14 17:48:12  DBG: Accepting traffic for /hello
2020/09/14 17:48:12  DBG: Accepting traffic for /health
 -------- Welcome to my server ------- 
2020/09/14 17:48:12  DBG: Accepting for /any.html
2020/09/14 17:48:12  DBG: Port  :9090
2020/09/14 17:48:12 My Simple Http Server
2020/09/14 17:48:14  DBG: Method -  GET
2020/09/14 17:48:14  DBG: Body -  {}
2020/09/14 17:48:14  DBG: URL -  /health
2020/09/14 17:48:14     { 
2020/09/14 17:48:14      DBG: Server is Healthy!
2020/09/14 17:48:14      DBG: 2020-09-14 17:48:14.594181453 +0000 UTC
2020/09/14 17:48:14     } 

References

  • https://masterminds.github.io/sprig/
  • https://helm.sh/docs/chart_template_guide/builtin_objects/
  • ServiceAccountName
  • https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#serviceaccount-v1-core