OnCallReady

Lesson 25.29 · CI/CD Pipelines & Helm · 13 min read

Dependencies, repositories, OCI and versioning

In plain words

Imagine building a model railway. You buy a station kit from one shop and a bridge kit from another, and your instructions say "use the bridge kit, version 1.3-something". The first time, you write down the exact kit you bought, "bridge 1.3.2", on a list, so next year you can buy the very same one. You tell the bridge what colour to be by writing on a sticky note labelled "bridge".

Helm dependencies work the same. Chart.yaml lists subcharts with version ranges like ~1.3.0; helm dependency update resolves them and writes exact versions to Chart.lock; helm dependency build downloads exactly those in CI. Values for a subchart go under its name, like java-service:. Charts live in classic repositories with index.yaml or, increasingly, in OCI registries next to images.

The problem

Twenty services each writing their own Deployment, probes and security settings means twenty slightly different versions of the same thing. Better: a platform team writes one good base chart, and each service only supplies values. That needs chart dependencies, a place to publish charts, and version numbers that tell users when an update is safe.

What you need to know already: 25.19 (charts, version), 25.21 (values and their layers), 25.2 (semantic versioning), 25.11 (immutable tags and digests), 10.54 (registries).

Words for this lesson

Dependencies

A chart can depend on other charts. They are declared in Chart.yaml:

dependencies:
  - name: java-service              # the chart's name in the repository
    version: "~1.3.0"               # a SemVer RANGE: >=1.3.0 <1.4.0
    repository: https://charts.lab  # or "@platform" (a repo added with helm repo add), or oci://...
  - name: redis
    version: "20.x"
    repository: https://charts.lab
    condition: redis.enabled        # a values path: false = the subchart is skipped entirely
    alias: cache                    # optional: install it under another name

Redis is a popular in-memory key-value store, often used as a cache.

Ranges: ~1.3.0 = patch updates only, ^1.3.0 = anything below 2.0.0, 1.3.x, >=1.2 <2. Pin to a range in Chart.yaml and let Chart.lock hold the exact versions:

helm dependency update ./chart   resolve the ranges, download to charts/, WRITE Chart.lock
helm dependency build ./chart    download exactly what Chart.lock says (CI uses this)
helm dependency list ./chart     name, version, repository, status (ok / missing / wrong version)

update picks the newest versions the ranges allow and records them; build reproduces exactly the recorded ones. Chart.lock goes into git, charts/*.tgz usually does not (the pipeline runs dependency build). A helm install of a chart whose charts/ is missing fails with found in Chart.yaml, but missing in charts/ directory: java-service.

Values for subcharts

The parent passes values to a subchart under the subchart's name (or alias):

# refunds-worker/values.yaml
java-service:              # everything here is .Values inside the java-service subchart
  image:
    repository: registry.lab/pay/payments-api
    tag: "1.5.1"
  replicas: 2
redis:
  enabled: true            # read by the parent (condition) AND visible to the subchart
global:
  environment: prod        # global: is copied into every subchart as .Values.global

This is exactly the shape that broke stage in the incident (25.23) - valid for an umbrella chart, silently ignored for a normal one. A subchart cannot see the parent's values except global, and the parent cannot read a subchart's defaults unless the subchart exports them (import-values, rarely worth it).

Values precedence with subcharts: subchart's values.yaml < parent's section for it < -f files < --set java-service.replicas=3.

Library charts and platform base charts

type: library charts contain only named templates (25.24); they are dependencies that other charts include from (the common chart pattern). The more common setup in banks is an application base chart owned by the platform team: every service depends on java-service (or uses it directly with only values), so probes, security context, labels and resource defaults are the same everywhere and a platform fix ships by bumping one dependency. The price: a breaking change in the base chart (replicas renamed replicaCount in 2.0.0) breaks every service that bumps it without reading the changelog - which is what SemVer is for.

Where charts live

Classic repositories are an HTTP server with index.yaml listing every chart version and its .tgz. helm repo add NAME URL remembers the repository under a short name, repo update downloads its index again, search repo lists what it has, show values prints a chart's default values, pull --untar downloads and unpacks it:

helm repo add platform https://charts.lab
helm repo update                       # refresh the cached index.yaml
helm search repo platform/java-service --versions
helm show values platform/java-service --version 1.3.2
helm pull platform/java-service --version 1.3.2 --untar

OCI registries store charts as OCI artifacts next to images (ACR, Harbor, GitLab, Artifactory/Nexus). No index, no repo add. helm package makes the .tgz, helm registry login logs in (password from standard input), helm push uploads, and every command takes an oci:// reference instead of a repo name:

helm package ./refunds-worker                              # refunds-worker-0.1.0.tgz (version from Chart.yaml)
helm registry login registry.lab -u ci-bot --password-stdin
helm push refunds-worker-0.1.0.tgz oci://registry.lab/charts
helm show chart oci://registry.lab/charts/refunds-worker --version 0.1.0
helm upgrade --install refunds-worker oci://registry.lab/charts/refunds-worker --version 0.1.0

The push prints the chart's digest; like images, a chart version is meant to be immutable - most registries can enforce it, and a re-pushed 0.1.0 with different content is the chart equivalent of the mutable-tag incident. The pipeline that promotes an image by digest should promote the chart by version (or digest) the same way: build and push once, deploy the same package everywhere.

Versioning a chart

The chart version follows SemVer and describes the chart's interface (its values and what it renders):

patch  0.4.0 -> 0.4.1   template fix, new appVersion, no values change
minor  0.4.1 -> 0.5.0   new optional values (extraEnv), new templates, defaults unchanged
major  0.5.0 -> 1.0.0   renamed/removed values, changed selectors, anything that needs
                        users to change their values or reinstall

Changing a Deployment's selector labels is always a major change: the selector is immutable, and the upgrade fails with spec.selector: Invalid value: ...: field is immutable. The fix then is delete-and-recreate (--force-replace, with downtime) or a new release name - both things you do on purpose, not by accident.

helm lint --strict and helm template in CI catch most chart mistakes; helm diff (a Helm plugin - an add-on command; next mission) catches the rest before prod.

What you can now do

Why it helps

In a bank, most services will not write their own chart; they depend on a platform base chart such as java-service. When stage breaks because values under java-service: were given to a normal chart, silently ignored, you will recognise the umbrella-chart shape. When a service bumps the base chart from 1.x to 2.0.0 and every deployment breaks because replicas became replicaCount, you know that is what major versions warn about.

CI failures like found in Chart.yaml, but missing in charts/ directory mean someone forgot helm dependency build. And when you own a base chart, pushing it to oci://registry.lab/charts with immutable versions and bumping SemVer correctly is your daily work. Selector changes as major bumps and chart immutability are good interview material.

FAQ

What is the difference between helm dependency update and helm dependency build?

helm dependency update resolves the version ranges in Chart.yaml against the repositories, downloads the matching charts into charts/ and rewrites Chart.lock with the exact versions. helm dependency build reads Chart.lock and downloads exactly those versions, without resolving anything. Developers run update when they want newer dependencies; CI runs build for reproducible results. Commit Chart.lock; usually do not commit charts/*.tgz.

How do I pass values to a subchart?

Under a key named after the subchart, or its alias, in the parent's values: java-service: { replicas: 2 } becomes .Values.replicas inside the subchart. A subchart cannot see the parent's other values, except those under global:, which is copied into every subchart. Precedence: the subchart's own defaults, then the parent's section, then -f files, then --set java-service.replicas=3.

What is the difference between a classic Helm repository and an OCI registry?

A classic repository is an HTTP server with an index.yaml listing all charts and versions; you helm repo add it and helm repo update refreshes the cached index. An OCI registry stores charts as OCI artifacts alongside images (ACR, Harbor, GitLab, Artifactory): no index, no repo add, you reference oci://registry.lab/charts/name --version 0.1.0 directly, and helm registry login handles auth. OCI is where the ecosystem is moving.

What do ~1.3.0 and ^1.3.0 mean?

They are SemVer ranges. ~1.3.0 allows patch updates only: at least 1.3.0 and below 1.4.0. ^1.3.0 allows anything below the next major: at least 1.3.0 and below 2.0.0. 1.3.x is like tilde. Ranges let dependency update pick up fixes; Chart.lock then pins the exact version so builds are reproducible until someone updates on purpose.

When is a chart change a major version bump?

When users must change something to upgrade: renamed or removed values, changed defaults that alter behaviour, changed selector labels, anything needing a reinstall. Selector changes are always major because spec.selector of a Deployment is immutable and the upgrade fails with field is immutable. New optional values or templates are minor; template fixes and a new appVersion without values changes are patch.

In an interview Mid

How would you design and version a platform base chart used by many services?

The base chart (java-service) holds the Deployment, probes, security context, labels and resource defaults; each service declares it as a dependency in Chart.yaml with a version range (~1.3.0 = patch updates only) and supplies only values, under the subchart's name. A platform fix then ships by bumping one dependency.

Reproducible builds: helm dependency update resolves the ranges and writes Chart.lock - committed to git; CI runs helm dependency build to fetch exactly those versions.

Distribution: push packaged charts (helm package, helm push) to an OCI registry next to the images, install with oci://registry.lab/charts/... --version. Chart versions immutable, promoted like image digests.

Versioning = the chart's interface (SemVer): patch for a template fix or new appVersion; minor for new optional values with unchanged defaults; major for renamed or removed values or changed selector labels (immutable - the upgrade fails). Teams read the changelog before a major bump.

Guardrails: values.schema.json, helm lint --strict and helm template in CI, helm diff before prod.

Also asked: What is Chart.lock and why should it be committed? · What is the difference between a classic Helm chart repository and an OCI registry? · When is a change to a chart a major version?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.