Learn Helm Chart - Library Charts
Episode 19 of 30

Learn Helm Chart - Library Charts

Managing shared template logic across many charts: the library chart type concept, building reusable helpers with _helpers.tpl, using them as dependencies in application charts, real-world label and security context examples, and best practices for library chart versioning and testing.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After episode 18, where we moved chart distribution to OCI registries — the helm push/helm pull flow, Cosign signing, and the migration strategy from HTTP — in this episode we enter a design pattern that solves a different problem: not how to ship a chart, but how to keep dozens of charts consistent. The answer is library charts.

A story very familiar in the real world: a platform team manages 40 application charts — one per microservice. Every chart has an almost identical _helpers.tpl: standard labels, a fullname generator, a security context. Then one day the security policy changes: every pod must run with runAsNonRoot: true and a specific seccompProfile. Now 40 charts must be changed, tested, and released one by one — and by chart 23, someone misses one. That's not just tedious work; it's the most efficient way to produce drift between what the security team approved and what's actually running in production.

Library charts are the answer. They're charts that can't be installed on their own — there are no templates/*.yaml files producing resources — because their contents are only ready-to-include template definitions. When a library chart is bumped, all application charts consuming it receive label fixes, security contexts, or new helpers all at once. In this episode we dissect the concept, how to build and use them, real examples you can use immediately, and the best practices that make a library chart safe to use as a foundation.

Main Discussion

The Library Chart Concept — Code That Never Runs on Its Own

In Chart.yaml there's a type field determining a chart's role: application (the default) or library. A library chart is a collection of named templates designed to be included by other charts — like a code module with no entry point. The crucial differences:

  • An application chart is installed and produces a release; it has templates/ that render resources to the API server.
  • A library chart can't be installed — running helm install on it fails with a message that a library chart contains no manifests. It has no resources of its own; it only has template definitions callable via include.

Compare with a _helpers.tpl in an ordinary chart (episode 10): every chart can indeed have local helpers, but those helpers can't be used across charts unless copied. Library charts eliminate that copy-paste: logic is written once, distributed via a dependency, and used anywhere. It's Helm's answer to the DRY (Don't Repeat Yourself) principle that ordinary libraries have long had.

What are library charts used for in practice:

  • Standardization: a single source of truth for labels, annotations, and resource naming across the whole org.
  • Shared policies: security contexts, resource defaults, and image pull policies required in all charts.
  • Complex logic: intricate helper functions — like building resource names that satisfy the 63-character label limit — written once, tested once.

Building a Library Chart — Chart.yaml and _helpers.tpl

Building a library chart starts with Chart.yaml — note the type: library:

charts/myorg-common/Chart.yaml
apiVersion: v2
name: myorg-common
description: Shared template helpers for myorg application charts.
type: library
version: 1.2.0
appVersion: ""

Its main content is templates/_helpers.tpl — a file defining named templates with define, which other charts will later call with the library name as a prefix. The agreed naming convention: every define begins with <library-name>.<helper-name>, for example myorg-common.labels. This isn't just style — it prevents name collisions when several library charts or application charts define helpers with the same name.

templates/_helpers.tpl
{{- define "myorg-common.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
 
{{- define "myorg-common.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
 
{{- define "myorg-common.labels" -}}
app.kubernetes.io/name: {{ include "myorg-common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOf | default "myorg" }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end -}}
 
{{- define "myorg-common.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myorg-common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
 
{{- define "myorg-common.securityContext" -}}
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
capabilities:
  drop: ["ALL"]
seccompProfile:
  type: RuntimeDefault
{{- end -}}
Every define starts with the library chart name prefix

There's a decisive technical detail: how . (root context) is passed through. When an application chart calls include "myorg-common.labels" ., the . value entering is the application chart's context — with the application chart's .Release, .Chart, and .Values. This is what makes a helper like myorg-common.labels work: it reads .Release.Name and .Chart.Name from the caller, not from the library chart. A library chart must write helpers as functions of the given context, not of its own context — the same concept as scope in Go templates we covered in episode 8.

Note

Note that the _helpers.tpl above doesn't access library-chart-specific .Values — it depends on values coming from the caller. If you need internal default values, define them in the library chart's values.yaml... but remember: values from a library chart's values.yaml are not automatically merged into the application chart. The common pattern is to add explicit configuration in the application chart's values.yaml (for example, partOf) and call it with default. Don't assume library chart values are available on the caller's side.

A library chart may also contain other templates/, a Chart.lock, and even dependencies — but since it isn't installed, nothing is rendered to the cluster. Everything is just a source of definitions to include.

Using a Library Chart — Dependency and include

The consumer side is where a library chart's power is felt. An application chart declares the library chart as an ordinary dependency:

myapp/Chart.yaml
apiVersion: v2
name: myapp
description: myapp application chart
type: application
version: 2.0.0
appVersion: "1.27.3"
dependencies:
  - name: myorg-common
    version: 1.2.0
    repository: https://charts.myorg.example.com

Because a library chart is an ordinary dependency, the episode 11 workflow applies fully — including the Chart.lock from episode 16:

Pulling a library chart as a dependency
helm dependency update ./myapp
# ... pulls myorg-common-1.2.0 into charts/
helm lint ./myapp
helm package ./myapp

Then inside the application chart's templates, helpers are called with include — and here's the difference from local helper usage: the template name is called with the library prefix:

myapp/templates/deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myorg-common.fullname" . }}
  labels:
    {{- include "myorg-common.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myorg-common.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myorg-common.selectorLabels" . | nindent 8 }}
    spec:
      securityContext:
        {{- include "myorg-common.securityContext" . | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- include "myorg-common.securityContext" . | nindent 12 }}
Library helpers included with the library chart name prefix

Notice the consistent pattern: include "myorg-common.fullname" . calls a helper from the library chart, and the result is nindent-ed to the correct indentation position in the YAML structure — exactly the whitespace management technique we mastered in episode 10. No more helper copy-paste: myapp now uses the same label and security context logic as 39 other charts in the org.

One detail that often surprises: a library chart dependency doesn't automatically "become visible" — the application chart's templates aren't rendered together with the dependency. include works because Helm renders all named templates from every chart bundled in (the application chart plus its dependencies) into a single namespace. The consequence: helpers from a library chart are only accessible via include "myorg-common.helperName" . — not called directly as part of subchart rendering, because a library chart indeed produces no manifests. If you want to test a helper before using it in all charts, render via helm template ./myapp --show-only templates/deployment.yaml and observe the include results in the output.

Real-world Example — Labels, Names, and Security Context

Let's look at one end-to-end pattern that's often the real reason people build library charts: a changing security policy. Say the security team decides all workloads must run with readOnlyRootFilesystem: true. With the copy-paste approach, the team has to edit 40 charts. With a library chart:

The change lives only in the library chart
{{- define "myorg-common.securityContext" -}}
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
  drop: ["ALL"]
seccompProfile:
  type: RuntimeDefault
{{- end -}}
One file changed, all consumers receive the change

Bump the library chart's version to 1.3.0, then each application chart bumps its dependency and re-releases at its own pace. One change, one place, zero drift. This is the clearest example of why library charts aren't just "for tidiness" — they're a policy enforcement mechanism.

Other common real-world patterns found in library charts:

  • Resource name helpers — a fullname that normalizes label length, handles nameOverride, and satisfies the 63-character limit.
  • Standard annotations — helpers for prometheus.io/scrape, checksum/config (restart the pod when a ConfigMap changes), or consistent owner annotations.
  • Image configuration — a helper building the repository:tag string with a default pull policy from chart values.
  • Environment helpers — mapping standard env (like NODE_ENV or DD_SERVICE) from shared values.

Library Chart Best Practices

Building a foundation used by dozens of charts means a mistake here is amplified across the entire org. Five practices must be held:

Conservative versioning. Library charts must follow strict semver (episode 16). Adding a new helper = MINOR; changing the behavior of an existing helper = MAJOR. Because helpers are used by many charts, a MAJOR change means every consumer must adapt — always make sure a helper is removed or behavior-changed only with the appropriate bump.

Backward compatibility at all costs. A helper already used by other charts is a public API. Don't remove old helpers; if you need new behavior, add a new helper and leave the old one with a deprecation notice. The same rule as code libraries: removing a function others are using is a breaking change.

Strict documentation. Every helper — the parameters it needs from context, the values it produces, and its edge-case behavior — must be documented (episode 15). Because these helpers are used across teams, documentation is the only way users know how to call them without reading the implementation.

Serious testing. A library chart must be tested like code: helm-unittest (episode 14) can test include against a fabricated context, ensuring helpers produce the expected output for various inputs. Because mistakes here propagate, library helper test coverage deserves stricter treatment than ordinary chart helpers.

One chart, one responsibility. Don't turn a library chart into a dumping ground for every helper. Separate by domain: myorg-common for labels & naming, myorg-security for security contexts, myorg-observability for monitoring annotations. Consumers take only what they need, and bumping one domain doesn't force everyone to accept another domain's changes.

Warning

Don't build a library chart before two or three charts genuinely share the same pattern. Premature abstraction — extracting helpers only used by one chart — adds cost without savings. The right pattern emerges from repetition; extract into a library chart when the third copy appears, not the first.

Conclusion

In this episode 19 we mastered the pattern that changes how large teams keep charts consistent. We understood the library chart concept as a type: library chart that can't be installed, containing named templates included by other charts — Helm's answer to the DRY principle. We built a library chart with a library-typed Chart.yaml and a _helpers.tpl where every define is prefixed with the library name to prevent collisions, then used it in an application chart via a helm dependency update dependency and include "myorg-common.labels" . calls. We saw its power through the real case of a security policy change needing only one place to change, and closed with best practices: conservative semver, backward compatibility, documentation, testing, and one-chart-one-responsibility.

The core takeaways:

  • A library chart = template code that never runs on its own; type: library enforces that.
  • Prefix every define with the library chart name to avoid helper collisions.
  • Library helpers read the caller's context (.Release, .Chart, .Values from the application chart).
  • One change in a library chart propagates to all consumers — power and responsibility in one.
  • Extract into a library chart when a pattern repeats, not on the first chart.

With this, the chart-building phase is complete: from templating, dependencies, testing, documentation, packaging, distribution, to patterns for sharing logic. In the next episode, episode 20, we raise the standard to a non-negotiable domain: chart security and best practices — least privilege, security context, RBAC, secret management without hardcoding, image security, supply chain, and policy enforcement with admission controllers. Keep your spirits up!

Learn Helm Chart - Library Charts | Learn Helm Chart