CoursesHelmLibrary charts & reuse

Library charts & reuse

Shared templates across charts.

Advanced12 min · lesson 9 of 12

A busy kitchen keeps one binder of prep recipes on a shelf. Nobody cooks the binder, and nobody serves it. Every station pulls recipes out of it, and the day the head chef writes a food-safety rule into one recipe (cook the chicken to 74 degrees Celsius), every dish that follows that recipe inherits the fix at once. A Helm library chart is that binder. Helm is the package manager for Kubernetes (often shortened to k8s), the system that schedules and runs your containers. A container is a self-contained box that holds an app and everything it needs to run. A chart is Helm's unit of packaging: a folder of templated YAML (a plain-text format for configuration files). A library chart ships no runnable output of its own. No Deployment (the Kubernetes object that keeps a set of identical containers running), no Service (the object that hands those containers one stable network address), nothing that ever lands in a cluster (the pool of machines Kubernetes runs everything on).

What it holds instead is reusable named templates. Those are the define blocks you write in helper files: a chunk of YAML given a name, ready to be called by other charts. Twenty microservice charts (each one packaging a small, independently deployed piece of a bigger app) that all want the same labels, the same health probes (the periodic checks Kubernetes runs to confirm a container is alive and ready for traffic), and the same hardened security settings can call one shared template instead of pasting the same forty lines into twenty places. Write it once. Fix it once. Ship the fix everywhere by bumping a version number.

A chart that renders nothing

You turn an ordinary chart into a library with one line in Chart.yaml (the file that describes the chart): type: library. Application charts default to type: application, and those are the ones Helm installs. A library is different on purpose. Helm renders none of its templates on its own, and running helm install against it fails by design. That refusal is a feature. A shared toolbox of templates should never be able to drop something into a cluster by accident; the only way its YAML reaches a cluster is when a real application chart chooses to call it.

common/Chart.yaml
apiVersion: v2
name: common
type: library # not installable; renders nothing on its own
version: 1.0.0

All the logic lives inside define blocks, kept in files whose names start with an underscore. Helm treats underscore-prefixed files as partials: helper files that never render into output on their own. That naming convention is what stops a stray helper from turning into a stray manifest (a manifest is a finished YAML document Kubernetes reads to create an object). Running helm create scaffolds an application chart, so you build a library either by hand or by scaffolding one and switching its type.

common/templates/_deployment.yaml
{{- define "common.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount | default 1 }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- containerPort: {{ .Values.service.port }}
{{- end -}}

Notice the helper reads .Values, .Release, and .Chart. It owns none of them. It borrows them from whichever chart calls it, which is the whole trick. On its own this file produces nothing, because a define block only registers a name. Something else has to invoke that name before any YAML appears.

Why it refuses to install

terminal
helm install demo ./common
output
Error: library charts are not installable

That error is the guardrail working. If you see it while trying to install what you thought was an app chart, you left type: library in a Chart.yaml by mistake, or you pointed helm install at the wrong directory. For a security reviewer it is a reassuring property: a chart marked library cannot, by itself, put a single object into your cluster.

Wiring it into an application chart

A consumer pulls the library in the same way it pulls any dependency (the same mechanics as the subcharts lesson). List it under dependencies in Chart.yaml, point repository at a file:// path on disk or a hosted Helm repository, then run helm dependency update to fetch it into the chart's charts/ folder.

This looks like a subchart, and the plumbing is identical, but the behavior is the opposite. A subchart is a chart nested inside another one. A normal subchart renders its own manifests the moment you install the parent; you get its Deployments whether you asked for them or not. A library renders nothing until your template explicitly calls one of its helpers. A subchart pushes resources at you. A library waits to be asked.

web/Chart.yaml
apiVersion: v2
name: web
version: 0.1.0
dependencies:
- name: common
version: "1.0.0"
repository: "file://../common" # or a hosted repo, or an oci:// reference
web/values.yaml
replicaCount: 1
image:
repository: nginx
tag: "1.27"
service:
port: 8080
terminal
helm dependency update ./web
output
Hang tight while we grab the latest from your chart repositories...
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Deleting outdated charts
terminal
ls web/charts/
output
common-1.0.0.tgz

That tarball (a .tgz compressed archive) is a snapshot of the library, copied into your consumer chart. Hold that thought, because the snapshot is where local iteration usually goes wrong. With the dependency in place, calling the shared template takes one line.

web/templates/deployment.yaml
{{ include "common.deployment" . }}

One line stands in for the whole Deployment. include runs a named template and hands back its text, which you can pipe into indentation helpers when you slot a snippet into a bigger file. (Its cousin template writes output directly into the surrounding text and cannot be piped, so include is the one to reach for.) The . at the end is the part that matters. It passes the consumer's context into the helper, which is why the helper's .Chart.Name resolves to web, not common, and why the same shared template produces a differently named resource in every chart that calls it.

Sometimes you need to feed the helper more than the plain consumer context. The second argument to include can be any value you like, so you can hand it a small dictionary, for example include "common.deployment" (dict "Values" .Values "extra" "sidecar"), and the helper reads whatever you packed in. Passing . is the common case. Passing a custom dict is how you parameterize a shared template beyond straight values.

Rendering the whole resource

terminal
helm template ./web
output
---
# Source: web/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name-web
labels:
app.kubernetes.io/name: web
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: "nginx:1.27"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- containerPort: 8080

Render it before you ship it. helm template expands everything locally without touching a cluster, so you can read exactly what would be applied. Look at what the shared helper baked in: runAsNonRoot, a read-only root filesystem (the container cannot write to its own disk, so an attacker who lands code inside it has nowhere to drop a payload), every Linux capability dropped, and privilege escalation switched off. The platform team wrote that hardening once. The day a new control is needed, say a stricter seccomp profile (a Linux kernel feature that limits which system calls a container may make; the kernel is the core of the operating system that talks to the hardware), they add it to the library, cut a new version, and every consumer that bumps its dependency inherits it. Not twenty pull requests. One. That single point of change is also the single place a defender audits to confirm the baseline is actually in force across the whole fleet.

Your charts/ tarball goes stale
A file:// dependency is copied into the consumer's charts/ folder as a snapshot tarball (common-1.0.0.tgz), not symlinked to the live source. Edit the library after that and helm template shows no change, because Helm is still rendering the cached copy. Teams lose hours here: the fix looks applied in the library folder, but the release never picks it up. Re-run helm dependency update after every library edit for a local path (no version bump needed, it repackages the current source). Version numbers and Helm's name-plus-version caching only matter for hosted or OCI (Open Container Initiative, a registry standard) repositories.

Keep names from colliding

Give every helper a namespace: common.deployment, never a bare deployment. Named templates share one flat global space across the parent chart and all of its subcharts, so two helpers both called deployment would clobber each other, and the last one loaded would silently win. Prefixing every name with the library's own name keeps them apart. This is how well-worn libraries like Bitnami's common chart are built: dozens of small, namespaced helpers (image references, label sets, name builders) that many charts import instead of reinventing them.

How one template reaches every chart
1Write the helper
define "common.deployment" in _deployment.yaml
2Mark type: library
Chart.yaml, so it never installs on its own
3Declare the dependency
web/Chart.yaml lists common under dependencies
4helm dependency update
copies common as a tarball into web/charts/
5include with context
{{ include "common.deployment" . }} passes web's own values
6Rendered manifest
full Deployment named after web, hardening baked in
Quick check
01The only template in your web chart is {{ include "common.deployment" . }}, and helm template ./web prints a Deployment named release-name-web. The helper that built it lives in the common chart. What decided that name?
Incorrect — Helm leaves a name a template already produced alone, so nothing after rendering steps in to fix it up.
Incorrect — common/Chart.yaml does set name: common, and the helper still prints web because it never reads its own chart.
Correct — A named template owns no data; it reads whatever context arrives with the call, and . is the caller's own.
Incorrect — Unpacking is not what picks the name. Feed the same helper a different context and it renders a different one.
02You add a normal subchart and a library chart to the same dependencies list, then run helm dependency update ./web. Both land in web/charts/ as tarballs. What changes once you install the parent?
Correct — That is the split worth remembering: one dependency ships resources by default, the other only when asked.
Incorrect — type: library is not a packaging detail. It decides whether the chart can produce any output at all.
Incorrect — Nothing in a library runs on its own schedule, so there is no first pass for it to make before anything else.
Incorrect — A plain subchart needs no invitation. Installing the parent brings its Deployments along whether you wanted them or not.
03Your library is wired in with repository: "file://../common". You tighten securityContext in common/templates/_deployment.yaml, run helm template ./web, and the output comes back unchanged. What explains it?
Incorrect — That preview is how you inspect a helper before shipping, and it expands included templates in full.
Incorrect — Versions govern hosted and OCI sources. A path on disk is repackaged from whatever bytes are there now.
Incorrect — The shared helper already sets runAsNonRoot and drops every capability, so the block clearly holds that YAML.
Correct — Run helm dependency update again after each library edit so the archive is rebuilt from the current files.

Before you bump a library version that hundreds of charts depend on, render at least one real consumer with helm template and diff the output against the previous version. A one-character change in a shared helper reaches every chart that calls it. That reach is the power of a library chart, and it is also why a careless edit travels exactly as far.

Try this

Run helm install demo ./common on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

The trap worth remembering here: your charts/ tarball goes stale. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related