Dependencies and Umbrella Charts

Declaring subcharts, overriding their values from the parent, what global actually shares — and why one umbrella release means one failure domain.

intermediate 20 min lesson hands-on task included

A chart can depend on other charts. That gives you the packaging benefit — “install Postgres” is a line of YAML — and a coupling cost that is easy to miss: everything under one umbrella becomes one release, with one revision history and one failure.


Topic 1: Declaring Dependencies

AN UMBRELLA CHART OWNS ITS SUBCHARTS' VALUES platform (umbrella) Chart.yaml: dependencies api local: file://../api postgresql repo: bitnami 15.x condition: postgresql.enable redis oci://ghcr.io/… condition: redis.enabled common type: library no templates of its own OVERRIDING A SUBCHART, FROM THE PARENT postgresql: ← the subchart NAME (or alias) auth: database: checkout global: ← visible to EVERY subchart imageRegistry: ghcr.io/acme THE PARTS THAT BITE · charts/ is VENDORED — dependency update rewrites it · Chart.lock pins the resolved versions. Commit it. · A subchart cannot read its parent's values, only global · alias lets you install the same chart twice · One release: a failed subchart fails the whole upgrade
Values flow downward from the parent, and `global` is the only channel that reaches every subchart. The right-hand panel is the list of behaviours that surprise people the first time.
# Chart.yaml
apiVersion: v2
name: platform
version: 1.2.0
dependencies:
  - name: postgresql
    version: "15.5.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

  - name: redis
    version: "19.x.x"
    repository: oci://registry-1.docker.io/bitnamicharts
    condition: redis.enabled
    tags: [cache]

  - name: api
    version: "0.4.x"
    repository: "file://../api"          # a local path, for a monorepo

  - name: common
    version: "1.2.0"
    repository: oci://ghcr.io/acme/charts # a library chart
helm dependency update .     # resolve, download into charts/, write Chart.lock
helm dependency build .      # install exactly what Chart.lock says — use this in CI
helm dependency list .

update versus build matters in a pipeline: update re-resolves version ranges and may pull a newer patch release; build installs exactly what the lock file pins. CI should use build, so a dependency does not change under you between the commit and the deploy.

Commit Chart.lock. Do not commit charts/. The lock file pins resolved versions and is what makes builds reproducible; the charts/ directory is downloaded output and belongs in .gitignore. (The exception is an air-gapped environment where vendoring is the point — then commit both and say so in the README.)

Version ranges follow semver: "15.5.x" allows patch updates, "~15.5.0" is equivalent, "^15.0.0" allows minor updates, "15.5.2" pins exactly. For anything stateful, pin tightly — a minor version of a database chart can change a StatefulSet’s storage layout.


Topic 2: Overriding a Subchart’s Values

Subchart values live under the subchart’s name in the parent’s values:

# platform/values.yaml
postgresql:                  # the subchart NAME (or its alias)
  auth:
    database: checkout
    username: checkout
  primary:
    persistence:
      size: 20Gi
    resources:
      requests: { cpu: 500m, memory: 1Gi }

redis:
  architecture: standalone
  auth:
    enabled: true

api:
  replicaCount: 3
  image:
    tag: "1.6.0"

Two rules that are not obvious:

  • A subchart cannot read the parent’s values. It sees its own values plus global. Any coupling must be explicit.
  • The parent always wins on keys it sets, because the parent’s values have higher precedence than the subchart’s own defaults.

global is the shared channel, and it is the only one:

global:
  imageRegistry: ghcr.io/acme
  imagePullSecrets: [regcred]
  storageClass: fast-ssd

Every subchart sees .Values.global.* — but only if the subchart’s templates actually read it. Most well-known charts support global.imageRegistry and global.storageClass; a chart that does not read them ignores yours silently. Check helm show values before assuming.

Keep global small. It is a namespace shared by every chart in the tree, including third-party ones, and a collision produces behaviour nobody intended.


Topic 3: Conditions, Tags and Aliases

condition points at a boolean in values and enables or disables the dependency:

dependencies:
  - name: postgresql
    condition: postgresql.enabled      # first existing path wins
postgresql:
  enabled: false        # use a managed database in prod instead

This is the standard pattern for “bundled dependency for dev, external service in production”, and it is why most public charts ship with an embedded database that you turn off.

tags enable groups of dependencies at once:

dependencies:
  - name: redis
    tags: [cache]
  - name: memcached
    tags: [cache]
tags:
  cache: false          # both off

condition beats tags when both are present.

alias installs the same chart more than once:

dependencies:
  - name: postgresql
    version: "15.5.x"
    repository: https://charts.bitnami.com/bitnami
    alias: postgresql-orders
  - name: postgresql
    version: "15.5.x"
    repository: https://charts.bitnami.com/bitnami
    alias: postgresql-billing

Values then go under the alias, not the chart name — postgresql-orders: and postgresql-billing:. This is how one umbrella runs two databases without forking anything.

import-values pulls values up from a child into the parent’s namespace, which is occasionally the cleanest way to let a chart expose computed configuration to its siblings. It is rarely needed; reach for global first.


Topic 4: The Coupling Cost

An umbrella chart makes everything one release. That has consequences worth deciding on deliberately rather than discovering:

  • One revision history. Upgrading the API bumps the whole release, including revisions for subcharts nothing changed in.
  • One failure domain. If the Postgres subchart’s upgrade fails, the whole helm upgrade fails — and with --atomic, your API rolls back too.
  • One rollback. You cannot roll back only the API.
  • One set of hooks, ordered by weight across every chart in the tree.
  • Rendering order is by kind, not by chart. Helm sorts all resources from all subcharts into its install order; a subchart does not “complete” before the next begins. If the API needs the database ready, that is an init container or a probe — not an ordering assumption.

When an umbrella is the right shape: components that are genuinely deployed and versioned together, and a dev-environment convenience stack. When separate releases are better: anything with an independent release cadence, anything stateful you would rather not couple to an application rollback, and anything a different team owns.

A middle path many teams settle on: separate releases per component, orchestrated by GitOps (Argo CD Applications or Flux HelmReleases), which gives you per-component rollback with declarative ordering — covered in the delivery lesson.


Topic 5: Library Charts

A library chart (type: library) contains only named templates, has no templates of its own, and cannot be installed. It is how an organisation shares helper logic without copying _helpers.tpl between repositories.

# common/Chart.yaml
apiVersion: v2
name: common
type: library
version: 1.2.0
{{/* common/templates/_labels.tpl */}}
{{- define "common.labels" -}}
app.kubernetes.io/name: {{ include "common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end }}

Consumers add it as a dependency and include its templates. One place to fix the label convention or the security-context defaults across every chart you own.

The trade-off is version coordination: changing a library template requires a version bump and a helm dependency update in each consumer. Worth it at around five charts; overhead at two.


Topic 6: Auditing What You Depend On

A chart from a public repository runs in your cluster with whatever permissions its manifests request. Read it before installing:

helm show chart bitnami/postgresql
helm show values bitnami/postgresql | head -80
helm pull bitnami/postgresql --version 15.5.0 --untar    # read the templates
helm template test ./postgresql | grep -E 'kind:|image:|serviceAccountName|privileged'

Three checks that take two minutes and are worth doing for anything going into production:

  • What images does it pull, and from where? A chart pointing at an unfamiliar registry is a supply-chain decision.
  • What RBAC does it create? grep -A5 'kind: ClusterRole' in the rendered output — a chart requesting cluster-wide privileges should have a reason.
  • Is it maintained? Check the repository’s last release. Abandoned charts pin abandoned images.

Pin dependencies to exact versions in production, and update deliberately with a diff:

helm diff upgrade platform . -f values/prod.yaml

Try it yourself: add a dependency with a condition, install once with it enabled and once disabled, and diff the two rendered outputs. Seeing exactly which resources appear and disappear makes the mechanism concrete — and it is how you verify a chart’s enabled flag actually does what it claims.

Common mistake: committing the charts/ directory along with Chart.lock, then editing a vendored subchart directly to fix something. The next helm dependency update overwrites the edit without warning, the fix vanishes, and nobody remembers it was there. Fork the chart properly, or override through values — never edit vendored output.