How to Reduce Helm Chart Boilerplate with Library Charts
By: Austin Dewey
Tags: DevOps, Containers, Helm, Kubernetes

In Part 1 of this blog series, I talked about how you can use named templates to reduce boilerplate throughout a single Helm chart. Now, we’re going to expand on that idea by discussing how you can use library charts to reduce boilerplate across multiple Helm charts. To do this, we will focus on the concept of library charts.

Understanding Library Charts

A library chart is a Helm chart consisting entirely of named templates. As you can recall from Part 1, a named template is a reusable template that contains boilerplate common to your chart resources. The named templates we described in Part 1 were defined in a file called _helpers.tpl and were only visible to the Helm chart that it belonged to.

Library charts expand on this idea by containing named templates that capture boilerplate common across all of your Helm charts. Your Helm charts can use these named templates by declaring the library chart as a dependency.

Note that library charts do not deploy anything to your Kubernetes cluster. Instead, they are imported by application charts to simplify chart development and maintenance.

Now that we have introduced library charts, let’s look at how you can create one.

Creating Library Charts

Creating a library chart is similar to creating a regular application chart. Here’s the basic structure of a library chart:

library-chart/

Chart.yaml

templates/

Pretty simple! Notice that library charts do not contain values.yaml files, since it’s the responsibility of the application chart to provide that instead.

One gotcha is to be sure the typesetting in Chart.yaml is library to differentiate this from a normal application chart:

apiVersion: v2

name: library-chart

version: 1.0.0

type: library

Your named templates will go under the templates/ folder. You could structure this by using a single _helpers.tpl file like this:

templates/

_helpers.tpl

However, I think library charts are easier to maintain when you split your named templates into multiple files, naming each tpl file based on its purpose. For example, you may have a structure like this:

templates/

_name.tpl # For setting resource names

_labels.tpl # For adding labels to each resource

_volumes.tpl # For adding volumes and volumeMounts

...

The contents of each tpl file contain one or more named template definitions. Let’s say the _labels.tpl file is used for creating a set of common templates across each of your Helm charts. In this file you can write the following template:

{{- define "library-chart.labels" -}}

{{- if .Chart.AppVersion }}

app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}

{{- end }}

app.kubernetes.io/managed-by: {{ .Release.Service }}

app.kubernetes.io/instance: {{ .Release.Name }}

{{- end }}

Then, in your application chart, you can include this named template to generate the boilerplate that it contains. Let’s get more in-depth with this in the next section.

Using Library Charts

To leverage the named templates defined in your library chart, you need to import it by declaring it as a dependency in your application chart’s Chart.yaml file. You’ll provide your library chart’s name, version, and repository under the dependencies section. Here’s an example:

apiVersion: v2

name: nginx

version: 1.0.0

dependencies:

- name: library-chart

version: 1.0.0

repository: https://my-chart-repo.example.com/

Sometimes, it’s more convenient to reference the library chart as a local file path, especially if your library chart is part of a monorepo with the rest of your application charts. In that case, the below declaration might be easier to maintain:

apiVersion: v2

name: nginx

version: 1.0.0

dependencies:

- name: library-chart

version: 1.0.0

repository: file://../library-chart

Once you have declared your library chart as a dependency, be sure to run the helm dependency update command to import the chart:

→ helm dependency update nginx

Hang tight while we grab the latest from your chart repositories...

...

Update Complete. ⎈Happy Helming!⎈

Saving 1 charts

Deleting outdated charts

You can use each of the named templates defined in your library chart once the library chart is imported. Here’s an example of using the “library-chart.labels” template from above in the example nginx chart:

apiVersion: apps/v1

kind: Deployment

metadata:

name: {{ .Release.Name }}

labels:

{{- include "library-chart.labels" . | indent 4 }}

spec:

replicas: 1

selector:

matchLabels:

{{- include "library-chart.labels" . | indent 6 }}

template:

metadata:

labels:

{{- include "library-chart.labels" . | indent 8 }}

spec:

containers:

- name: {{ .Release.Name }}

image: {{ .Values.image }}:{{ .Values.tag }}

As you can see, using a library chart helps reduce chart boilerplate, and it also provides a single place for you to manage each of your chart configurations. Take the labels example from above. By using a library chart, you can easily manage each of the labels that get added to your Kubernetes resources. If you need to change these labels, you can easily update multiple Helm charts by making a quick change in your library chart.

We’ve talked about how you can use named templates and library charts to help capture small boilerplate pieces. In the next section, I want to discuss how you can use library charts to encapsulate the entire Kubernetes resources.

Encapsulating Kubernetes Resources with Library Charts

Consider a set of Helm charts that all have one deployment resource, and imagine you want to reduce as much deployment boilerplate as possible. One option you can consider is to write named templates to cover configs that we discussed in Part 1, such as:

Labels

Environment variables

Volumes and volume mounts

Health checks

However, if you proceed with this option, you still end up with deployment boilerplate across your Helm charts since these named templates don’t cover everything.

A second option that will help you reduce more boilerplate is to write a named template that encapsulates an entire deployment. Here’s an example:

{{- define "library-chart.deployment" -}}

apiVersion: apps/v1

kind: Deployment

metadata:

name: {{ .Release.Name }}

labels:

{{- include "library-chart.labels" . | indent 4 }}

spec:

replicas: {{ .Values.replicas }}

selector:

matchLabels:

{{- include "library-chart.labels" . | indent 6 }}

template:

metadata:

labels:

{{- include "library-chart.labels" . | indent 8 }}

spec:

containers:

- name: {{ .Release.Name }}

image: {{ .Values.image.name }}:{{ .Values.image.tag }}

{{- end }}

Then, your application charts can easily create a deployment with a single “include” invocation:

{{ include "library-chart.deployment" . }}

And that’s it! Since the deployment boilerplate was captured in your library chart, you only needed one line to create an entire deployment. All that’s required now is for either the user to provide the “replicas”, “image.name”, and “image.tag” values or for you to set defaults for these in your chart’s values.yaml file.

One challenge you may encounter with library charts is understanding the values that each named template requires, especially as they grow larger and potentially more complex. In the next section, I want to present a solution that I like to use to simplify library chart usage, which is writing your library charts to be contract-based.

Writing Contract-Based Library Charts

The concept of contract-based library charts (as I call it) is similar to the idea behind Java interfaces. In a Java interface, you define each of the methods that the inheriting class must implement. To describe this in terms of Helm, your library chart should require each of the values it expects, and the inheriting application chart should either provide defaults in values.yaml or expect users to provide them on their own.

Let’s go through an example, using the same deployment named template from earlier but using the required function throughout to require input.

{{- define "library-chart.deployment" -}}

apiVersion: apps/v1

kind: Deployment

metadata:

name: {{ .Release.Name }}

labels:

{{- include "library-chart.labels" . | indent 4 }}

spec:

replicas: {{ required "value 'replicas' is required" .Values.replicas }}

selector:

matchLabels:

{{- include "library-chart.labels" . | indent 6 }}

template:

metadata:

labels:

{{- include "library-chart.labels" . | indent 8 }}

spec:

containers:

- name: {{ .Release.Name }}

image: {{ required "value 'image.name' is required" .Values.image.name }}:{{ required "value 'image.tag' is required" .Values.image.tag }}

{{- end }}

As you can see, this named template now requires the “replicas”, “image.name”, and “image.tag” values. If someone tries to implement this named template in their application chart but don’t have these values present, they’ll get this error:

Error: execution error at (nginx/templates/deployment.yaml:1:3): value 'replicas' is required

And similar errors for each value that is not implemented. By adding these requirements, you have created a contract between your library chart and all charts that want to implement its named templates.

From there, the application chart maintainer can make a decision. They can either default each of the required values in their values.yaml file, or they can leave them unimplemented and force the user to have to provide them on their own. In this case, let’s say you want to default each of them in your application chart’s values.yaml file, so you can add each of these values to your values file:

replicas: 1

image:

name: nginx

tag: “1.19”

For more information on using the “required” function, check out my blog post, Helm Tricks: Input Validation With ‘Required’ And ‘Fail’.

Thanks For Reading!

That ends part two of Reducing Helm Chart Boilerplate with Named Templates and Library Charts. In this post, we expanded on the idea of named templates and learned about how to create library charts to reduce boilerplate across multiple different Helm charts. You also learned how you can create contract-based Helm charts to make it easier for chart maintainers to use library charts that you create.