This is Part 2 of Reducing Helm Chart Boilerplate with Named Templates and Library Charts.
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 type setting 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 definition. 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’s 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 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 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.
Thanks a lot for the article!
What do you think about library charts vs a subchart that’s being called.
Subcharts are great for deploying dependencies alongside your app. Consider the relationship between WordPress and MySQL. WordPress depends on MySQL for persistence, and you can easily deploy that persistence layer by simply using a MySQL subchart.
Library charts are great for capturing smaller pieces of boilerplate, but not necessarily an entire dependency. You may notice boilerplate yaml across several Helm charts. Library charts can help clean that up by allowing you to use common templates.
Great article, Do you have a sample code in a github repo?
Thanks, Johann. I don’t have an example in GitHub right now, but check out Bitnami’s “common” Helm chart https://github.com/bitnami/charts/tree/master/bitnami/common. This is a library chart that Bitnami uses across their other charts. You can see it being referenced by other charts in their repository.
Thanks for the article… There is a big drawback which defeats the purpose of library chart: “named templates are global”… So if an umbrella chart consists of dependencies which in turn rely on same library chart, but different versions, you may run into troubles as you don’t control which named template version will take precedence. Would you have workarounds maybe?
Thanks
Hi Romuale, good observation. I ran a quick test and validated that this would be an issue. The best way to prevent this is to keep your charts up to date with the latest library chart version (assuming that you own the charts in question). If the version can’t be kept up to date, then as a library chart maintainer, it would be best to ensure no breaking changes between versions. This might be good enough depending on context.
Luckily, I haven’t come across a situation like this before in practice, but if you work with umbrella charts frequently, you might eventually see this.
Thanks a lot! You explained this SO much better than the official Helm website!
Thanks, Robert!