Shared Configuration Across Teams¶
Assume you have two or more teams, each work for a different internal or external service, like:
- Product 1
- Product 2
- Observability
The simplest helmfile.yaml that declares the whole cluster that is composed of the three services would look like the below:
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: product2-api
chart: saas-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
This works, but what if you wanted to a separate cluster per service to achieve a smaller blast radius?
Let’s start by creating a helmfile.yaml for each service.
product1/helmfile.yaml:
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
product2/helmfile.yaml:
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
You will (of course!) notice this isn’t DRY.
To remove the duplication of observability stack between the two helmfiles, create a “sub-helmfile” for the observability stack.
observability/helmfile.yaml:
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring helmfiles.
product1/helmfile.yaml:
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
product2/helmfile.yaml:
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
Using sub-helmfile as a template¶
You can go even further by generalizing the product related releases as a pair of api and web:
shared/helmfile.yaml:
releases:
- name: product{{ env "PRODUCT_ID" }}-api
chart: product{{ env "PRODUCT_ID" }}-charts/api
# snip
- name: product{{ env "PRODUCT_ID" }}-web
chart: product{{ env "PRODUCT_ID" }}-charts/web
# snip
Then you only need one single product helmfile
product/helmfile.yaml:
helmfiles:
- ../observability/helmfile.yaml
- ../shared/helmfile.yaml
Now that we use the environment variable PRODUCT_ID to as the parameters of release names, you need to set it before running helmfile, so that it produces the differently named releases per product:
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
Inheriting parent configuration with inherits:¶
By default a sub-helmfile is independent: it does not see the repositories:,
helmDefaults:, environments:, etc. declared in the parent that includes it. To
share configuration you either extract it into a separate file referenced via
bases: from each sub-helmfile, or — more concisely — let each sub-helmfile
opt into inheriting specific categories from its parent with inherits:.
# parent helmfile.yaml
repositories:
- name: release-charts
url: registry.example.com/release/helm-charts
oci: true
helmfiles:
- path: myapp.yaml
inherits:
- repositories
- environments
myapp.yaml now sees the release-charts repository and the parent’s resolved
environment values, without having to re-declare them.
Allowed values¶
inherits: accepts a list. Each entry must be one of:
| Key | What is inherited |
|---|---|
repositories |
Parent repository definitions (appended; child entry wins on name conflict). |
helmDefaults |
Parent helm defaults; parent fills the child’s unset sub-fields, child’s set sub-fields win (see caveat below). |
commonLabels |
Parent common labels; child’s keys win on conflict. |
apiVersions |
Parent API versions (appended and de-duplicated). |
kubeVersion |
Parent kube version, only when the child left it empty. |
templates |
Parent templates; child’s template wins on name conflict. |
environments |
The parent’s resolved environment values (see note below). |
Precedence¶
Child wins, parent fills gaps — consistent with bases:. Slices like
repositories accumulate (parent first); maps (commonLabels, templates) are
unioned with the child’s keys winning; helmDefaults is deep-merged so the child
overrides individual sub-fields it sets.
Caveat: helmDefaults and zero values¶
helmDefaults is deep-merged field-by-field, and Go value types cannot
distinguish an explicitly-set zero value from “unset”. So a child that sets a
sub-field to its zero value (for example atomic: false to disable an inherited
atomic: true) will still see the parent’s non-zero value fill in. To override
such a field, set it to a non-zero value, or omit helmDefaults from inherits:
and declare the block in full on the child. The common case — a child that omits
helmDefaults entirely and inherits the parent’s — is unaffected.
Note on environments¶
environments inherits the parent’s already-resolved values (including CLI
overrides and decrypted secrets), not the environments: declaration block. This
avoids ambiguity around the directory that relative values files resolve against
(the parent resolves them once). The child’s own environments: block, if any,
still takes precedence per key.
Transitive inheritance¶
Inheritance is opt-in at every level but the values propagate. If a parent
inherits repositories and the child in turn has its own helmfiles: entries
declaring inherits: [repositories], the grandchild receives the accumulated set.
inherits: vs bases:¶
bases: |
inherits: |
|
|---|---|---|
| Model | Pull — each file lists files to merge into itself | Push — the parent pushes its config to the child |
| Where the shared config lives | Must live in a separate file | Can live inline in the parent |
| Repetition | bases: must be repeated in every sub-helmfile |
Declared once, per sub-helmfile entry |
They are complementary: use bases: for cross-team reusable building blocks
(environment defaults, shared helm defaults), and inherits: to let a sub-helmfile
consume the parent’s inline configuration without duplication.
Footgun warning¶
If a sub-helmfile references a repository that the parent declares but the child
does not (and repositories is not inherited), helmfile prints a warning suggesting
inherits: [repositories] instead of failing later with a confusing
repo not found (see #1495).