Mastering Helm Charts. Library Edition

Working on maintaining billions of workloads in thousands of Kubernetes clusters has taught me to avoid repetitive routines, simplify service onboarding, and keep all manifests consistent. Usually, most applications have the same or very similar requirements from a rendered Kubernetes manifest perspective (like labels requirements, selectors, affinity rules, and so on). A Library Chart can be a solution that saves you hundreds of hours of writing Helm charts and keeps you away from repeating the same code again and again.

Introduction

Helm is a powerful tool for managing Kubernetes applications. It simplifies the deployment process by using charts, which are packages of pre-configured Kubernetes resources. However, managing multiple charts can become repetitive and error-prone. This is where Library Charts come in handy.

Library Charts on Helm Website

The library chart was introduced at the beginning of Helm 3 (please read the article).

Reasons to Use a Library Chart

Reusability: Share common functionalities and configurations across multiple charts, DRY.

Consistency: All created charts follow the same rules: same labels, same selectors, name conventions.

Maintainability: Majority of the fixes and improvements are centralized. In case you have an independent release flow for your library, all your derivatives will use the defined version, making upgrades straightforward and the impact minimal.

Some of my colleagues still avoid Library Charts and try to create some universal Helm chart for all potential cases/microservices/projects. But over time, all of their “universal” charts turned into chthonic creatures, and support and maintenance became a fight with them.

Anyway, if you have a set of very similar applications, you can create some “half-universal” Helm charts for the set, like:

  • backend: ConfigMap (with injection into environment variables), Deployment, Service, HorizontalPodAutoscaler
  • single page application: ConfigMap (mount application configuration), Deployment, Service, HorizontalPodAutoscaler

TL;DR

Helm Chart Library you can use from here: helm-generic

Chart.yaml
1
2
3
4
5
6
7
8
9
10
apiVersion: v2
name: example-chart
description: Example
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: common
repository: https://kharkevich.github.io/helm-generic/
version: 2.1.0

Helm Chart with the Library under the hood: mlflow-tracking-server

Defining a Library Chart

To define the chart as a library, we need to do the following. In Chart.yaml, you need to set type: library like this:

Chart.yaml
1
2
3
4
5
6
7
apiVersion: v2
name: common
description: Common helm library
type: library
appVersion: 1.0.0
version: 1.0.0
home: https://github.com/kharkevich/helm-generic

To follow Helm concepts, all your helpers in the library chart should begin with an underscore (_) to avoid rendering and output as a Kubernetes manifest file.

So, your templates folder will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_affinity.yaml
_chartref.yaml
_configmap.yaml
_container.yaml
_deployment.yaml
_deployment_strategy.yaml
_envvar.yaml
_fullname.yaml
_healthcheck.yaml
_hpa.yaml
_metadata.yaml
_metadata_annotations.yaml
_metadata_labels.yaml
_name.yaml
_persistentvolumeclaim.yaml
_podmonitor.yaml
_resources.yaml
_secret.yaml
_security_context.yaml
_service.yaml
_serviceaccount.yaml
_servicemonitor.yaml
_tolerations.yaml
_util.yaml
_volume.yaml

Function common.util.merge is one of the key and remarkable functions from the library which allows us to create highly customizable Helm charts with our library. Let’s take a look at the _util.yaml file, where this helper function is located:

_util.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{- /*
common.util.merge will merge two YAML templates and output the result.

This takes an array of three values:
- the top context
- the template name of the overrides (destination)
- the template name of the base (source)

*/ -}}
{{- define "common.util.merge" -}}
{{- $top := first . -}}
{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}}
{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}}
{{- toYaml (merge $overrides $tpl) -}}
{{- end -}}

To demonstrate how this function works, let’s check how to use it to define a ConfigMap, for example.

configmap.yaml
1
2
3
4
5
6
7
8
9
{{- define "common.configmap.tpl" -}}
apiVersion: v1
kind: ConfigMap
{{ template "common.metadata" . }}
data: {}
{{- end -}}
{{- define "common.configmap" -}}
{{- template "common.util.merge" (append . "common.configmap.tpl") -}}
{{- end -}}

An empty ConfigMap will be rendered after using this helper in your Helm chart.

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
data:
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: api
app.kubernetes.io/instance: demo-service
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: helm-demo
app.kubernetes.io/version: "1.0"
helm.sh/chart: helm-demo-1.0.0
name: demo-service

To extend this ConfigMap we can do the following:

  • set some default required parameters
  • add additional code to give us flexibility and customization via values files.

For example

ConfigMapconfigmap.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{- template "common.configmap" (list . "application.env_configmap") -}}
{{- define "application.env_configmap" -}}
data:
{{- if .Values.oidc.enabled }}
SECRET_KEY: {{ required "Required for OIDC configuration" .Values.oidc.secret_key | quote }}
OIDC_REDIRECT_URI: {{ required "Required for OIDC configuration" .Values.oidc.redirect_uri | quote }}
OIDC_DISCOVERY_URL: {{ required "Required for OIDC configuration" .Values.oidc.discovery_url | quote }}
...
REDIS_PORT: {{ required "Required for Redis cache" .Values.oidc.redis_port | quote }}
{{- end }}
{{- if .Values.oidc.group_detection_plugin }}
OIDC_GROUP_DETECTION_PLUGIN: {{ .Values.oidc.group_detection_plugin | quote }}
{{- end }}
{{- end }}
{{- with .Values.config.data }}
{{- range $key, $value := . }}
{{ $key | upper }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end -}}

Conclusion

Library Charts in Helm provide a powerful way to manage and reuse common configurations across multiple charts. By using Library Charts, you can ensure consistency, improve maintainability, and save time by avoiding repetitive tasks. Explore the helm-generic library to get started with creating your own Library Charts.