CoursesHelmSubcharts & dependencies

Subcharts & dependencies

Compose charts; share values.

Intermediate12 min · lesson 7 of 12

A recipe that calls for two cups of tomato sauce does not make you simmer tomatoes for an hour. You reach for a jar someone already made, trust the label, and get on with the dish. A Helm chart can work the same way. When your application needs a PostgreSQL database (a relational database that stores data in tables) and a Redis cache (an in-memory store that holds data you need to reach fast), you do not rewrite those from raw Kubernetes YAML (Kubernetes is the system that runs your containers across a pool of machines; YAML is the indentation-based text format it reads). You declare them as dependencies, and Helm fetches their charts for you. A chart pulled in this way is called a subchart. The chart doing the pulling is the parent.

Dependencies live in one place: the dependencies list in the parent's Chart.yaml (the file that holds a chart's name, version, and metadata). This is how large applications get assembled out of parts other people maintain, the way an operating system package pulls in the libraries it needs instead of bundling its own copy of everything.

Declaring what your chart depends on

Each entry in the list has a handful of fields, and every one of them matters for security later. name is the subchart's name, and it doubles as the key you use to configure it. version is a version constraint written in semantic versioning (the major.minor.patch scheme), and it can be one exact version like 15.5.38 or a range like 15.5.x that means "any patch of 15.5". repository says where to fetch the chart from, either an HTTP chart repository or an OCI registry (Open Container Initiative, the standard that container registries speak) addressed with an oci:// URL. condition points at a boolean value that switches the whole subchart on or off. tags are named groups, so one switch can flip several subcharts together.

Chart.yaml
apiVersion: v2
name: payments-api
version: 1.4.0
appVersion: "2.1.0"
dependencies:
- name: postgresql
version: "15.5.x" # a range: any patch of 15.5
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # switch the whole subchart on/off
- name: redis
version: "19.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
tags:
- cache # flip every subchart tagged "cache" at once

That condition field is the difference between a chart that only works one way and one you can actually run in more than one place. In development you set postgresql.enabled to true and get a throwaway database inside the cluster (the pool of machines Kubernetes runs your workloads on). In production you set it to false and point the app at a managed database your platform team runs, with backups and patching handled for you. Same chart, two very different deployments, no forked copies.

Two more fields turn up in bigger charts. alias lets you list the same subchart twice under different names, so one parent can run two independent Redis instances side by side. import-values pulls data the other direction, letting a subchart expose some of its computed values up to the parent. You will not reach for either on day one, but they are how the same building blocks compose into genuinely large charts.

Fetching them: update versus build

Think of a shopping list versus the itemized receipt you get at the till. helm dependency update works from the list. It reads Chart.yaml, resolves each version constraint to one concrete release, downloads each subchart as a compressed .tgz archive into the charts/ directory, and writes Chart.lock. That lock file is the receipt: it records the exact versions it settled on plus a SHA-256 checksum (a cryptographic fingerprint) of the whole set.

terminal
$ helm dependency update ./payments-api
output
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 2 charts
Downloading postgresql from repo https://charts.bitnami.com/bitnami
Downloading redis from repo https://charts.bitnami.com/bitnami
Deleting outdated charts

The archives land in charts/, and the versions the ranges resolved to get frozen into Chart.lock. Read the lock file and you can see exactly what was pulled, down to the patch release.

terminal
$ ls charts/
output
postgresql-15.5.38.tgz redis-19.6.4.tgz
Chart.lock
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 15.5.38
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 19.6.4
digest: sha256:3b1c0e7a9f2d4b6c8e0a1f3d5b7c9e1a2f4d6b8c0e2a4f6d8b0c2e4a6f8d0b2c
generated: "2026-07-17T10:22:04.481233+02:00"

helm dependency build works the other way around. It ignores the ranges in Chart.yaml and fetches exactly what Chart.lock already names, refusing to run if the two files disagree. If you have worked in a JavaScript project, this is the same split as npm install (resolve fresh) versus npm ci (obey the lockfile). Use update on your workstation when you deliberately want newer subcharts. Use build in CI (continuous integration, the automation that rebuilds and tests your code every time it changes) so every pipeline run assembles the identical set of charts.

terminal
$ helm dependency list ./payments-api
output
NAME VERSION REPOSITORY STATUS
postgresql 15.5.x https://charts.bitnami.com/bitnami ok
redis 19.x.x https://charts.bitnami.com/bitnami ok

That STATUS column is worth a glance in CI. ok means the archive in charts/ matches the lock, while missing or wrong version means someone changed Chart.yaml and never re-fetched. In that drifted state, build stops rather than guessing, which is exactly what you want a pipeline to do.

terminal
$ helm dependency build ./payments-api # after Chart.yaml was edited but the lock was not
output
Error: the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies

Handing values down to a subchart

Picture the mailboxes in an apartment lobby. Each subchart has one, labelled with its name. Anything you write under the postgresql key in the parent's values file gets delivered into the postgresql subchart and read there as if it were that chart's own top-level configuration. There is also one shared notice board, the global block, that every subchart can read. Those are the only two routes in. A value you drop at the parent's top level, not under a subchart's name and not under global, never reaches any subchart at all.

values.yaml
postgresql: # delivered to the postgresql subchart
enabled: true
auth:
database: payments
existingSecret: pg-credentials # read a Secret you made; do not invent a password
primary:
persistence:
size: 20Gi
redis: # delivered to the redis subchart
enabled: true
architecture: standalone
global: # the only block every subchart can read
imageRegistry: registry.internal
storageClass: fast-ssd

So postgresql.auth.database sets the database name inside the postgresql subchart, and postgresql.auth.existingSecret tells that subchart to read a password from a Kubernetes Secret (the Kubernetes object that holds sensitive values like passwords) you created ahead of time, instead of generating one and writing it back into the release (Helm's name for one installed instance of a chart). global.imageRegistry reaches both subcharts and the parent, which is how you make everything pull images from your own internal registry instead of the public internet in one line.

Which values a subchart can actually read
global.*
parent
can read it
postgresql subchart
can read it
redis subchart
can read it
postgresql.* / redis.*
that named subchart
reads it as its own values
the other subcharts
cannot see it
a top-level parent key
parent templates
read it
every subchart
never sees it
Only two routes reach a subchart: the global block and the key named after that subchart. Everything else stays with the parent.
A subchart only reads its own key and global
Set a value at the parent's top level expecting a subchart to pick it up, and nothing happens, with no error to tell you. A plain imageRegistry: registry.internal at the top of the parent's values does not reach the postgresql subchart. It has to be global.imageRegistry (shared with everyone) or postgresql.image.registry (that subchart's own key). When a subchart ignores a setting you are certain you passed, check the scope of the key before anything else.

Know exactly what a subchart deploys

A subchart is code, not a config knob you turn. It is other people's templates, and installing the parent applies whatever those templates render, using your credentials against your cluster. A community database chart can stand up a StatefulSet (the Kubernetes controller that runs pods with stable names and storage; a pod is the smallest unit Kubernetes schedules, one or more containers that live and die together), its own ServiceAccount (the identity a pod uses to talk to the Kubernetes API) and RBAC rules (role-based access control, which decides what an identity is allowed to do), NetworkPolicies (firewall rules that decide which pods may talk to which), and Secrets, none of which you wrote by hand. Before you trust one in production, render the whole thing and read it. helm template prints every manifest (the YAML description of a single Kubernetes object) the chart would apply, without ever touching the cluster.

terminal
$ helm template payments ./payments-api | grep -E '^kind:' | sort | uniq -c
output
1 kind: ConfigMap
1 kind: Deployment
2 kind: NetworkPolicy
1 kind: Secret
3 kind: Service
2 kind: ServiceAccount
2 kind: StatefulSet

That one line of output is the honest inventory of your "app". Two StatefulSets and a Secret you never authored, ServiceAccounts that will hold real permissions in your cluster, NetworkPolicies that quietly decide what is allowed to talk to what. Read the actual manifests for anything that runs privileged, mounts a host path, or grants broad RBAC. helm template ./payments-api --show-only charts/postgresql/templates/statefulset.yaml narrows the output to a single file when you want to inspect one piece closely.

Pinning is not the same as trusting

Freezing a version stops the code from changing under you. It does not make the code safe, and it does not guarantee the code will still be there tomorrow. Two habits close most of the gap. First, pin exact subchart versions or commit Chart.lock, and run helm dependency build in CI so a deploy never silently picks up a newer subchart. Second, vendor the archives: commit the charts/*.tgz files into your repository so the exact bytes you reviewed are the exact bytes you ship, with no live fetch at deploy time.

That second habit stopped being theoretical in the second half of 2025. Bitnami restructured its public catalog, moving older image versions out of the default location into a separate bitnamilegacy repository and putting its hardened images behind a paid tier. Teams that had pinned old chart versions found the chart still resolved, but the container images those charts pointed at had moved, and pods failed to pull. A version pin in Chart.yaml protects the chart. It says nothing about whether the registry will keep serving what the chart points at.

So build one more habit in. An unpinned subchart pulls whatever the repository serves today, the same moving-target risk as any unpinned dependency, except this one deploys workloads straight into your cluster. Pin exact versions, commit Chart.lock so helm dependency build reproduces the identical set, and for anything you cannot afford to have vanish, vendor the .tgz archives and mirror the images into a registry you control. Review what each subchart deploys before you trust it, and re-review every time you bump the version.

Quick check
01Your CI pipeline runs helm dependency update before every deploy, and Chart.yaml lists postgresql at "15.5.x". Two deploys a week apart end up with different postgresql patch releases. Why?
Incorrect — Helm does honour what you wrote, and it will never cross into 15.6 or 16. What shifts is which 15.5 patch satisfies the range on the day the command runs.
Incorrect — The version field constrains the chart itself. appVersion is separate metadata describing the software packaged inside, and it is not what a dependency entry matches on.
Correct — Every run rewrites Chart.lock from the constraint rather than reading it, which makes any range a moving target. Commit the lock and switch CI to helm dependency build.
Incorrect — Resolution finishes long before install; by then charts/ holds fixed .tgz archives. Chart.lock names a concrete version and a checksum for each subchart, so they pin like anything else.
02Your chart pins postgresql to 15.5.38, Chart.lock is committed, and CI runs helm dependency build. Months later a deploy fails because pods cannot pull the database image. What explains it?
Correct — Bitnami shifted older images into a separate legacy repository in 2025. The chart still resolved from the lock, but the image references inside it pointed somewhere that stopped serving them. Mirror images into a registry you control.
Incorrect — That generated field only records when the resolve happened. There is no expiry, and build compares versions and checksums rather than dates.
Incorrect — build is the command that obeys the lock, and it refuses to run at all when Chart.yaml and Chart.lock disagree. update is the one that re-resolves ranges.
Incorrect — Nothing about the lock weakens the pin. With the two files in agreement, build fetches those exact versions on every run, which is why the chart bytes were never the problem here.
03You put a top-level imageRegistry: registry.internal in the parent's values.yaml, expecting the postgresql subchart to pull from it. The subchart ignores it and Helm reports nothing. What went wrong?
Incorrect — A dependencies entry says which chart to fetch, from where, and under what condition. It has no say in how values are scoped once the chart is in place.
Incorrect — Refetching swaps the archive, not the rules Helm follows when passing values down. The subchart would still find nothing addressed to it.
Incorrect — A bare top-level key stays with the parent's own templates and reaches no subchart at all. Sharing one setting across everything is exactly what the global block is for.
Correct — Write global.imageRegistry when every chart in the release should pull from the same place, or postgresql.image.registry when only that one should. Anywhere else and the setting is silently ignored.

Try this

Run helm dependency update ./payments-api 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: a subchart only reads its own key and global. 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