DRY Helm Charts for Micro-Services

TL;DR: A pattern for defining Helm charts for micro-services based apps, allowing both installation of single services and the whole app, while reusing most of the yaml resources.

Background

So, you have just discovered Helm and you’re trying to figure out how does it integrate with your SOA, or have been working with it for a while but you’re still not sure you’re doing things right. It has templates, but you still have to copy-paste your yaml resources (violates DRY principle). You’re not sure whether to create a chart for each micro-service or a chart for the whole app.
The first option gives you elasticity to install each one of your services independently, and the last option allows you to package your app and install it in a single line, but couples the installation of your services together, which feels like coupling the services a bit. So which one is better?
What if we could enjoy the best of both worlds?

The solution

We’ll create a chart for each service, an Umbrella chart, and a Common chart.
The punch here is the Common chart. This chart will contain templates for all the common resources of our services, viz. Deployment, Service, Ingress, ConfigMap and more.
Each service chart will only include the common templates required for the service and supply service-specific values.
Finally, the Umbrella chart will (surprisingly) just unify the services charts.

Drill down

We’re going to leverage Helm’s templating engine to define common resource yamls to our charts. As I previously mentioned, the common templates will lay in the Common chart. Let’s take a Deployment resource as an example:

# _deployment.yaml
{{- define "common.deployment" -}}
{{- $common := dict "Values" .Values.common -}}
{{- $noCommon := omit .Values "common" -}}
{{- $overrides := dict "Values" $noCommon -}}
{{- $noValues := omit . "Values" -}}
{{- with merge $noValues $overrides $common -}}
apiVersion: apps/v1beta2
kind: Deployment
{{ template "common.metadata" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
app.kubernetes.io/name: {{ include "common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}/{{ template "common.name" . }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
{{- end -}}
{{- end -}}

This template looks quite similar to the default Deployment template created by helm create command. Let’s review the nuances:

  1. The filename starts with _ , which signals Helm templating engine that this file isn’t a resource template but contains helper functions and definitions. Combining this with {{- define "common.deployment" -}} allows for later inclusion of this template in other charts.
  2. The following block does some magic as well.
{{- $common := dict "Values" .Values.common -}} 
{{- $noCommon := omit .Values "common" -}}
{{- $overrides := dict "Values" $noCommon -}}
{{- $noValues := omit . "Values" -}}
{{- with merge $noValues $overrides $common -}}

I won’t dive into the templating syntax, but in a sentence, it allows each chart depending on the Common chart to override the common’s values.yaml values with it’s own values defined in it’s values.yaml file.

Other resources are defined in a similar manner.

Now, re-using the resource in some service chart is quite simple:

  1. Add the Common chart as a dependency of the service chart.
  2. Create the following file:
# deployment.yaml
{{- template "common.deployment" . -}}

That’s it. Next time the service chart will be installed, it will take all it’s resources from the Common chart, while private values will override common values of the Common chart.

Finally, create an Umbrella chart for your app which requires all the service charts. No templates are needed, just requirements.yaml and Chart.yaml files.
Note that you can also override specific sub chart values using --set <subchart_name>.image=v2 , or in the Umbrella chart values.yaml file:

subchart_name:
image:
tag: v2

This is useful to inject dynamic values (like image.tag) during CI/CD process.

Seeing is believing

To complete the post, I published a full-fledged example to a Git repo containing two services (some Node.js server and a React frontend), a Common chart and an Umbrella chart. You can further inspect it to get a grasp of how everything should play together.

⎈Happy Helming!⎈

References

  1. Jesse Eichar’s Mimacom Blog
  2. The Chart Template Developer’s Guide
  3. Chart Development Tips and Tricks