Reducing code duplication and overhead with Helm Library Charts

by Jens Heremans

At one of our customers we manage several applications composed of many micro-services, around 50 at the time of writing. We deploy these services on Kubernetes using Helm charts. Each services is built and packaged together with its own Helm chart. So each version of the services comes with an identically versioned helm chart. While that approach has served us quite well over the years, we started noticing a few challenges as our applications kept growing.

  • Over 90% of the Helm templating code across all charts is identical.
  • Any change to a Helm chart requires building a new version of the micro-service.
  • Structural changes to the Helm templates must be propagated to every repository.

Given these challenges, we decided it was time to explore whether our setup could be improved.

What We’re Trying to Achieve

In an effort to make our setup more efficent we focused on 3 main objectives:

1. Reduce Code Duplication

Most of our Helm charts shared the same templating logic. This not only bloated our code-base but also made it harder to maintain consistency. We wanted a centralized solution that could provide reusable building blocks for our charts.

2. Lower the Maintenance Overhead

Every structural change in the Helm templates required pushing changes to every micro-service repository and rebuilding each one. Our goal was to reduce the operational burden on developers.

3. Ensure Consistency Across Charts

Despite best efforts, differences between individual service charts started creeping in, especially when teams made service-specific tweaks. These inconsistencies made debugging deployments harder and introduced potential for human error. We aimed to standardize chart structure across services, making deployments more predictable and easier to manage.

Evaluating Our Options

Before jumping into a new solution, we explored a few common patterns for managing Helm charts at scale. Ultimately, we rejected two approaches in favor of one that better fit our needs.

❌ Monolithic Helm Chart

One option was to use a single monolithic Helm chart that could deploy all services. However, this approach separates the chart from the application code, making the setup less intuitive for developers. It also would have required significant changes to our CI/CD pipelines to support this new structure, something we wanted to avoid.

❌ Centralized Reusable Chart

Another approach we considered was maintaining a single, centralized Helm chart and referencing it from each micro-service repository with just a unique values.yaml file. While this would reduce duplication, it proved too rigid. We often need to define service-specific templates (like custom config maps or jobs), and this approach didn’t allow that very easily.

✅ Helm Library Chart

The solution we ultimately chose was to use a Helm library chart. This approach provided the best balance between reusability and flexibility:

  • Enables us to define and maintain a shared set of reusable templates.
  • Allows services to extend or override templates as needed.
  • Requires no changes to our existing CI/CD pipelines.
  • Supports global sane defaults, reducing the need for repetitive values in each chart.

We can maintain a clean and DRY chart structure, while still giving individual teams the power to customize where needed.

Using Library charts

With Helm library charts, we can centralize and reuse common template logic across all our service charts. Instead of duplicating the same boilerplate across 50+ Helm charts, we define reusable snippets, called definitions, in a shared library chart. Then, individual charts can reference these definitions directly, keeping them minimal and consistent.

Now how does this all work?

1. Initial setup

Define a Library Chart

Start by creating a new Helm chart with type: library. This chart won’t produce any Kubernetes objects on its own.t’s meant to be imported and used by other charts.

# my-library-chart/Chart.yaml
apiVersion: v2
name: my-library-chart
description: A library of Helm templates to build our application charts
type: library
version: 0.1.0

Prefix each template file with an underscore. This is required to prevent Helm from rendering yaml within the library itself.
The folder structure should look like this:

my-library-chart/
├── templates/
│   ├── _deployment.tpl
│   ├── _service.tpl
│   └── ...
├── Chart.yaml
└── values.yaml

Add the Library Chart as a Dependency

In your micro-service Helm chart, include the library chart as a dependency:

# example-service/Chart.yaml
apiVersion: v2
name: example-service
description: A Helm chart for our microservice
version: 0.1.0
appVersion: "1.0"
dependencies:
  - name: my-library-chart
    version: 0.1.0
    repository: link-to-your-repository.com

Then, run helm dependency update to pull in the library chart.

💡 NOTE: During testing you can also point to the folder on your filesystem containing the library chart so that you don’t need to push to your repository every time. For example: repository: file:///path/to/your/library/chart

2. Creating Library Definitions

As mentioned before, the template filenames should have an underscore (_) prefix. This tells Helm not to render them directly, but to make them available for inclusion.

We can then use the define function to create definitions.

Here’s an example of a simple deployment resource:

# my-library-chart/templates/_deployment.yaml
{{- define "my-library-chart.defaultDeployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.name }}
spec:
  replicas: {{ .Values.replicaCount | default 1 }}
  selector:
    matchLabels:
      app: {{ .Values.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.name }}
    spec:
      containers:
        - name: {{ .Values.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
{{- end }}

Helm will not render YAML from this template directly. Instead, you can reference the my-library-chart.defaultDeployment definition from another template file, without the underscore prefix, to include and render it as part of that chart.

You can create as many template files and definitions as needed. As well as multiple resources of the same kind.

3. Referencing Library Templates

In your application chart, you can now reference those library definitions using the include function:

# example-service/templates/deployment.yaml
{{- include "my-library-chart.defaultDeployment" . }}

Given the following values file provided in your application helm chart:

# example-service/values.yaml
name: example-service
replicaCount: 2
image:
  repository: mycompany/example-service
  tag: v1.2.3
service:
  port: 8080

Will render the example deployment definition as:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: example-service
  template:
    metadata:
      labels:
        app: example-service
    spec:
      containers:
        - name: example-service
          image: "mycompany/example-service:v1.2.3"
          ports:
            - containerPort: 8080

4. Optional: Setting Default Values

In order to reduce the amount of code in each application chart you can also set default values. These are optionally imported and can be overridden by the local chart’s values.

To configure defaults inside the library chart, define them under the top-level exports key.

# my-library-chart/values.yaml
exports:
  global:
    replicaCount: 1
    service:
      port: 80

Then, in your application chart, import these values using the import-values field in the Chart.yaml file. You can selectively import values by specifying the key you want to include. This allows you to group and reuse sets of default values while keeping imports minimal and relevant.

For example, to import the global values from the snippet above:

# example-service/Chart.yaml
...
dependencies:
  - name: my-library-chart
    version: 0.1.0
    repository: link-to-your-repository.com
    import-values:
      - global

These values will be imported with global as the root, so your application chart will inherit them as if they were defined locally:

replicaCount: 1
service:
    port: 80

Meaning you can reference these values in your templates as normal, e.g. {{ .Values.service.port }}, and override them in any values file passed to Helm.

You can find more information on default values in Helm’s docs here.

5. Optional: Shared Helpers

To keep things even more DRY, you can also define reusable helper functions in a .tpl file in your library chart:

# my-library-chart/templates/_helpers.tpl
{{- define "common.name" -}}
{{ .Chart.Name }}
{{- end }}

{{- define "common.fullname" -}}
{{ printf "%s-%s" .Release.Name .Chart.Name }}
{{- end }}

You can then reference these helpers in any template using the include function. Extending the previous example deployment:

# my-library-chart/templates/_deployment.yaml
{{- define "my-library-chart.defaultDeployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "common.fullname" . }}
spec:
  replicas: {{ .Values.replicaCount | default 1 }}
  selector:
    matchLabels:
      app: {{ include "common.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "common.name" . }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
{{- end }}

Shared helpers can include many useful functions, such as:

  • Standard sets of annotations or labels
  • Common metadata blocks
  • Reusable container definitions
  • Logic for parsing values to generate labels or names

💡 NOTE: The Helm documentation on library charts includes a more advanced helper function called mylibchart.util.merge. It allows you to override parts of your library definitions from within the application chart.

6. Optional: Creating “base” Templates

As your library chart grows, you may find yourself repeatedly combining the same helper functions, default values, and templates for similar types of applications. To make reuse even easier, you can group these into base templates. Instead of having each application chart include multiple individual library chart definitions (e.g. deployment, service, labels, annotations, etc.), you can define a single base definition that includes everything needed for that type of service. This way, the application chart only needs to reference one top-level template.

One way to use this in practice is to create a base template for each application type like a java spring-boot miscro-service or a nodejs webapp.

To keep it manageable you could structure your folder like this:

my-library-chart/
├── values.yaml
├── Chart.yaml
├── templates/
│   ├── _helpers.tpl
│   ├── java/
│   │   ├── _base.yaml
│   │   ├── _configmap.yaml
│   │   ├── _deployment.yaml
│   │   ├── _service.yaml
│   │   ├── ...
│   ├── nodejs/
│   │   ├── _base.yaml
│   │   ├── _configmap.yaml
│   │   ├── _deployment.yaml
│   │   ├── _service.yaml
│   └── ...

Make sure all your definitions have unique names. For example you could define your java deployment as my-library-chart.java.deployment and your nodejs deployment as my-library-chart.nodejs.deployment.

Then we can create our base java template:

# my-library-chart/templates/java/_base.yaml
{{- define "my-library-chart.base.java" -}}
{{ include "my-library-chart.java.deployment" . }}
{{ include "my-library-chart.java.configmap" . }}
{{ include "my-library-chart.java.service" . }}
{{- end }}

Your application chart can then include just the base template in its custom chart template:

# example-service/templates/base.yaml
{{ include "my-library-chart.base.java" . }}

And that’s it! just a single line to create your entire helm chart.

💡 NOTE: If needed, you can always add any other template files that do not reference any of your libraries inside your application helm chart. Some resources might be unique and not required anywhere else, so there’s little benefit in including those to your library.

Conclusion

Getting started with Helm Library Charts definitely came with a learning curve, but in the end, it proved well worth the effort. We’ve made significant improvements over our previous setup.

Results after switching to library charts:

  • Reduced code duplication by eliminating over 25,000 lines of code across all repositories
  • Drastically lowered the overhead of making changes to Helm charts
  • Improved consistency across charts, making debugging easier and reducing the risk of human error
  • Maintained flexibility, charts are still easy to extend when needed

There’s certainly more we could explore with Helm libraries, but for now, we’re satisfied with where we’ve landed. You can go quite deep with this pattern, but we recommend keeping it opinionated. You likely don’t need to accommodate every edge case, so KISS (Keep It Simple, Stupid).

If you’re looking to get started with your own library chart and want some examples, check out Helmet or the (deprecated but still useful) Common Helm Helper Chart.

References

Helm Docs – https://helm.sh/docs

Common Helm Helper Chart – https://github.com/helm/charts/tree/master/incubator/common

Helmet (example library) – https://github.com/companyinfo/helm-charts/tree/main/charts/helmet

Menu