Helm & Kubernetes Packaging

Dependencies & Subcharts

18 min Lesson 6 of 28

Dependencies & Subcharts

Real production applications are rarely self-contained. A backend API needs a database, a cache layer, and possibly a message broker. Helm solves this with a first-class dependency system: you declare what your chart needs, Helm fetches those charts from repositories, and the entire stack is installed and upgraded as a single atomic unit. This lesson teaches you how chart dependencies work, how to share values across parent and child charts through global values, and how to compose a multi-service stack using an umbrella chart — the pattern used by platform teams at companies like Shopify and Lyft to deploy entire environments with a single helm install.

Declaring Dependencies in Chart.yaml

Dependencies are declared in the dependencies block of Chart.yaml. Each entry specifies the chart name, version constraint, and repository URL. After editing this block you must run helm dependency update — Helm resolves, downloads, and packages the dependency charts into a charts/ subdirectory as .tgz archives, and writes a Chart.lock file that pins exact resolved versions (analogous to package-lock.json).

# Chart.yaml for a backend API that depends on PostgreSQL and Redis apiVersion: v2 name: backend-api description: Production API service with managed dependencies type: application version: 1.4.0 appVersion: "2.9.1" dependencies: - name: postgresql version: "15.3.x" # semver range — resolves latest 15.3.z repository: "https://charts.bitnami.com/bitnami" condition: postgresql.enabled # can be disabled at install time - name: redis version: "19.x.x" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled - name: common version: "2.x.x" # internal helpers library chart repository: "https://charts.bitnami.com/bitnami"
# After editing Chart.yaml, always run: helm dependency update ./backend-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 3 charts # Downloading postgresql from repo https://charts.bitnami.com/bitnami # Downloading redis from repo https://charts.bitnami.com/bitnami # Downloading common from repo https://charts.bitnami.com/bitnami # Deleting outdated charts ls backend-api/charts/ # common-2.19.1.tgz postgresql-15.3.4.tgz redis-19.6.2.tgz # Chart.lock is generated — commit it to git so CI reproduces exact versions cat backend-api/Chart.lock
Always commit Chart.lock. Chart.lock pins the exact resolved version of every dependency (and transitive dependency). Without it, two engineers running helm dependency update at different times can resolve different patch versions — producing different deployments from the same commit. Treat it like go.sum or yarn.lock.

Passing Values to Subcharts

Once a dependency is downloaded, you configure it by nesting its values under a key that matches the chart name in your parent values.yaml. Helm routes those values into the subchart automatically — the subchart never sees the parent's top-level values unless you use global values (covered next).

# values.yaml for backend-api replicaCount: 3 image: repository: myregistry.io/backend tag: "2.9.1" # Subchart values: key must match the dependency name in Chart.yaml postgresql: enabled: true auth: username: api_user password: "" # override with --set or a sealed secret database: api_production primary: persistence: size: 50Gi storageClass: gp3 resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2 memory: 4Gi redis: enabled: true auth: enabled: true password: "" replica: replicaCount: 2 master: persistence: size: 8Gi

Inside your parent chart's own templates you reference the subchart's generated Service name to wire the connection. The conventional naming pattern for Bitnami charts is {{ include "common.names.fullname" .Subcharts.postgresql }}.{{ .Release.Namespace }}.svc.cluster.local, but in practice you use a template helper or compute the name via the release name. A safer approach is to expose the hostname as a value your app reads:

# templates/deployment.yaml (parent chart) env: - name: DATABASE_HOST value: "{{ .Release.Name }}-postgresql.{{ .Release.Namespace }}.svc.cluster.local" - name: REDIS_HOST value: "{{ .Release.Name }}-redis-master.{{ .Release.Namespace }}.svc.cluster.local" - name: DATABASE_NAME value: {{ .Values.postgresql.auth.database | quote }}

Global Values

By default, a subchart is isolated: it sees only the values namespaced under its own key. Global values break this isolation intentionally — any value placed under the global: key in the parent is visible to every subchart and to the parent itself, without any namespacing. This is the canonical way to share cross-cutting configuration like image registry, environment name, or cluster region.

Global values flow in parent and subchart Parent Chart: backend-api global: imageRegistry: myregistry.io environment: production storageClass: gp3 clusterRegion: us-east-1 Parent Values replicaCount: 3 image.tag: 2.9.1 postgresql.auth.database: api_prod Subchart: postgresql sees global.imageRegistry sees global.storageClass sees postgresql.auth.* does NOT see replicaCount Subchart: redis sees global.imageRegistry sees global.storageClass sees redis.auth.* does NOT see replicaCount
Global values flow down to every subchart automatically; parent-scoped values stay isolated to the parent.
# values.yaml — global block is injected into every subchart global: imageRegistry: myregistry.io # Bitnami charts honor this key imagePullSecrets: - name: regcred storageClass: gp3 environment: production postgresql: enabled: true image: # Bitnami picks up global.imageRegistry automatically tag: "16.3.0-debian-12-r14" auth: username: api_user database: api_production existingSecret: backend-api-db-creds # reference to an externally managed Secret redis: enabled: true auth: existingSecret: backend-api-redis-creds existingSecretPasswordKey: redis-password
Never put plaintext passwords in values.yaml committed to git. Use existingSecret to reference a Kubernetes Secret managed outside Helm (via Sealed Secrets, External Secrets Operator, or AWS Secrets Manager CSI driver). This pattern separates secret lifecycle from chart lifecycle — a security boundary that matters enormously when charts are stored in public or semi-public repositories.

Umbrella Charts

An umbrella chart (also called a "meta chart" or "app of apps") is a chart whose sole purpose is to group and configure a set of subcharts. It has little or no content in its own templates/ directory — it exists purely to declare dependencies and provide a single values file that configures the entire stack. This is how platform teams at scale deploy entire environments: one helm install platform ./umbrella --values prod.yaml brings up API, worker, database, cache, and monitoring in one atomic operation.

# Structure of an umbrella chart platform-umbrella/ ├── Chart.yaml # declares all service subcharts as dependencies ├── Chart.lock # pinned resolved versions — ALWAYS commit this ├── values.yaml # default values for all subcharts ├── values-dev.yaml # dev environment overrides ├── values-prod.yaml # prod environment overrides ├── charts/ # downloaded .tgz subcharts (gitignored or committed) │ ├── backend-api-1.4.0.tgz │ ├── frontend-2.1.0.tgz │ ├── worker-1.1.3.tgz │ └── postgresql-15.3.4.tgz └── templates/ # usually empty, or contains only NOTES.txt └── NOTES.txt
# platform-umbrella/Chart.yaml apiVersion: v2 name: platform-umbrella description: Full platform stack for the payments product type: application version: 4.2.0 dependencies: - name: backend-api version: "1.4.x" repository: "oci://myregistry.io/helm" # OCI chart repo (ECR/GCR) condition: backend-api.enabled - name: worker version: "1.1.x" repository: "oci://myregistry.io/helm" condition: worker.enabled - name: frontend version: "2.1.x" repository: "oci://myregistry.io/helm" condition: frontend.enabled - name: postgresql version: "15.3.x" repository: "https://charts.bitnami.com/bitnami" condition: postgresql.enabled - name: redis version: "19.x.x" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled - name: kube-prometheus-stack version: "58.x.x" repository: "https://prometheus-community.github.io/helm-charts" condition: monitoring.enabled
# Deploy the entire platform to production in one command helm dependency update ./platform-umbrella helm upgrade --install platform ./platform-umbrella \ --namespace platform-prod \ --create-namespace \ --values platform-umbrella/values.yaml \ --values platform-umbrella/values-prod.yaml \ --set global.imageTag="${CI_COMMIT_SHA}" \ --atomic \ --timeout 10m \ --wait # To disable monitoring in a lightweight dev environment: helm upgrade --install platform ./platform-umbrella \ --values platform-umbrella/values-dev.yaml \ --set monitoring.enabled=false \ --set postgresql.primary.persistence.size=5Gi

Alias and Tags

Sometimes you need to install the same chart twice as two different subcharts — for example, two separate PostgreSQL databases for different services. Use the alias field to give each instance a unique name, which becomes the key you use in values.yaml:

# Chart.yaml — two PostgreSQL instances with aliases dependencies: - name: postgresql alias: userdb version: "15.3.x" repository: "https://charts.bitnami.com/bitnami" - name: postgresql alias: analyticsdb version: "15.3.x" repository: "https://charts.bitnami.com/bitnami" # values.yaml — configure each alias independently userdb: auth: database: users username: user_service primary: persistence: size: 20Gi analyticsdb: auth: database: analytics username: analytics_service primary: persistence: size: 200Gi storageClass: io2 # high-IOPS storage for analytics
Production pitfall — dependency chart cache staleness. The charts/ directory is populated by helm dependency update. If you commit the .tgz files, they can drift from Chart.lock when a teammate updates dependencies but forgets to re-commit the archives. The safest CI pattern is: never commit charts/*.tgz (add it to .helmignore), and always run helm dependency build (faster than update — uses Chart.lock without re-resolving) in your CI pipeline before helm upgrade. This guarantees every build uses exactly the pinned versions.

Putting It Together — CI Pipeline Pattern

A production CI pipeline for an umbrella chart looks like this:

# .github/workflows/deploy.yml (relevant steps only) - name: Add Helm repos run: | helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update - name: Build dependencies (uses Chart.lock, no network resolution) run: helm dependency build ./platform-umbrella - name: Lint chart run: helm lint ./platform-umbrella --values values-prod.yaml - name: Dry-run render (catch template errors before touching the cluster) run: | helm upgrade --install platform ./platform-umbrella \ --values values-prod.yaml \ --set global.imageTag="${{ github.sha }}" \ --dry-run --debug \ --namespace platform-prod \ 2>&1 | tee helm-dry-run.log - name: Deploy run: | helm upgrade --install platform ./platform-umbrella \ --values values-prod.yaml \ --set global.imageTag="${{ github.sha }}" \ --atomic --timeout 10m \ --namespace platform-prod
Use helm dependency build in CI, not helm dependency update. build reads Chart.lock and downloads exactly those versions without hitting repository servers to re-resolve ranges. This is faster, deterministic, and prevents a surprising version bump if an upstream chart releases a new patch between your two CI runs.

With dependencies, global values, and umbrella charts mastered, you have the full compositional toolkit to manage complex production stacks. The next lesson covers hooks and tests — how to run database migrations, smoke tests, and readiness checks as integral parts of the Helm release lifecycle.