Environments & values layering¶
You set ingressReplicaCount: 4 in environments/prod.yaml, deploy, and nothing changes on the cluster.
That's not a bug — it's a layering mistake, and it's the single most common way to lose an hour with
Helmfile. The course page shows environments: as three blocks (default, staging, prod), each
listing a values: file or two; this page slows down on how that layering actually resolves — the
merge order, what counts as an "environment value" versus a "Helm value", and the failure modes that show
up once a stack has more than one or two environments.
TL;DR — in 30 seconds:
- Environment values (
.Environment.Values) and Helm/release values (.Valuesinside the chart) are two separate trees — setting a key inenvironments/prod.yamldoes nothing until a templated values file reads.Environment.Valuesand passes it into the chart. - Both
environments.<name>.valuesandenvironments.<name>.secretsare lists merged in order, later entries winning — the same strategic-merge rule release values use. - Picking
-e stagingvs-e prodcan change which releases run (viacondition:), not just their config — checkcondition:before assuming a values typo.
1. Two separate value systems, easy to conflate¶
Helmfile has two value trees that look similar in YAML but serve different purposes:
| Tree | Declared under | Consumed as | Purpose |
|---|---|---|---|
| Environment values | environments.<name>.values |
.Environment.Values.* in templates |
Deploy-time knobs: which env, what domain, what replica count — never sent to a chart directly |
| Helm (release) values | a release's values: list |
merged into the chart's values.yaml |
What the chart itself receives — becomes .Values.* inside the chart's templates |
The course page's environments/default.yaml (domain, certManager.enabled, ingressReplicaCount)
is the first tree. It only becomes useful once something reads .Environment.Values.domain — usually
inside a release's templated *.yaml.gotmpl values file, which is where the two trees actually connect.
flowchart LR
EnvFiles["environments.<name>.values<br/>(environment values)"]
EnvCtx[".Environment.Values.*<br/>available in every template"]
Gotmpl["release values/*.gotmpl<br/>reads .Environment.Values"]
Rendered["Rendered Helm values"]
Chart["Chart's values.yaml<br/>(.Values.* inside the chart)"]
EnvFiles --> EnvCtx --> Gotmpl --> Rendered --> Chart
That's the mistake from the top of this page, precisely: setting ingressReplicaCount: 4 in
environments/prod.yaml does nothing to the cluster until some *.gotmpl values file actually reads
.Environment.Values.ingressReplicaCount and assigns it to a chart field the chart understands
(controller.replicaCount). Confusing the two trees is the single most common layering mistake.
2. Environment-values layering is additive, in list order¶
environments.<name>.values is a list, not a single file, and Helmfile merges every entry in the
list — earlier entries first, later entries winning on conflicting keys:
environments:
staging:
values:
- environments/default.yaml # base: applies everywhere
- environments/staging.yaml # overrides: only what staging changes
Same strategic merge rule as release values (scalars/lists replace, maps merge recursively) applies
here. This is why environments/staging.yaml in the course page only needs to contain the handful of keys
that actually differ from default.yaml — everything else falls through from the base file. A staging
override that repeats an unchanged key isn't wrong, just redundant; a common house style is: base file has
every key with its most common value, every non-default environment file has only its deltas.
3. Secrets layer the same way, in a separate list¶
environments.<name>.secrets merges exactly like values, but through helm-secrets/SOPS decryption
first — so an environment can combine a plaintext base with an encrypted override:
environments:
prod:
values:
- environments/default.yaml
- environments/prod.yaml
secrets:
- secrets/prod.yaml # decrypted at run time, merged in after values
secrets: entries are conceptually a second values list decrypted just before use — they merge into the
same .Environment.Values tree as values:, not a separate namespace. A key set in both values: and
secrets: follows the same last-one-wins rule, decided by which list position comes later.
4. Selecting an environment doesn't just swap values — it can swap the release set¶
Because condition: on a release reads .Environment.Values, picking -e staging vs -e prod can
change which releases run, not just their configuration:
releases:
- name: cert-manager
condition: certManager.enabled # environments/staging.yaml may set this false
This is the layering trap worth naming explicitly: two environments can diff not only on replica counts
but on the shape of the deployed stack itself. When triaging "why isn't X running in staging", check
condition: before assuming a values typo.
5. Precedence, top to bottom¶
Putting the pieces from this page together, here is everything that can set a value that ends up inside a chart, lowest priority first:
| # | Source | Scope |
|---|---|---|
| 1 | Chart's own values.yaml defaults |
Whatever the chart author shipped |
| 2 | environments.<name>.values (base file, e.g. default.yaml) |
Broadest environment layer |
| 3 | environments.<name>.values (env-specific file, e.g. prod.yaml) |
Narrower — later in the list |
| 4 | environments.<name>.secrets |
Decrypted, merged after plaintext values |
| 5 | Release values: list (files, then inline maps, in list order) |
Per-release, still .Environment.Values-aware if templated |
| 6 | Release set: |
Last-resort scalar override, highest priority |
flowchart BT
L1["1 · chart values.yaml"]
L2["2 · environment base values"]
L3["3 · environment override values"]
L4["4 · environment secrets"]
L5["5 · release values: list"]
L6["6 · release set:"]
L1 --> L2 --> L3 --> L4 --> L5 --> L6
style L6 stroke-width:3px
6. How this connects¶
- The precedence ladder in §5 is the same shape as two other modules' layering pages: Ansible's
host_vars/group_varsladder (module 08, Ansible, inventory & variables) and mise's config hierarchy (module 11, mise) — different sources, same rule of "later, more specific layer wins." - §3's
secrets:layering is only the merge-order half of the story — what SOPS actually encrypts and howhelm-secretsdecrypts it in flight is this course's next page, Secrets with SOPS. - Once a release's rendered values reach the chart, what the chart does with them (mounting as env vars vs.
a projected volume, Secret immutability) is Kubernetes' Config & Secrets page
(module 04, Kubernetes & Helm) — this page's job stops at producing
.Values, that page picks up from there.
Common gotchas
- Setting a value only in
environments/prod.yamland expecting the chart to pick it up. Fix: it must be read via.Environment.Values.<key>inside a templated release values file and mapped to a field the chart understands — the environment tree never reaches the chart directly. - Repeating every key in every environment override file. Fix: put every key with its most common
value in the base file (
default.yaml) and let each environment file hold only its deltas — repeating an unchanged key isn't wrong, just noise. - Forgetting
condition:when triaging "why isn't X running in staging". Fix: check whether the environment's values disable the release viacondition:before assuming a plain values typo — environments can differ in which releases run, not just their settings.
Check yourself — you set certManager.enabled: false in environments/staging.yaml. What actually changes on the cluster, beyond a config value?
If a release's condition: reads that key (e.g. condition: certManager.enabled), the release itself
stops being deployed to staging — the environment value doesn't just tune the release, it can remove
it from the stack entirely.
Check yourself — in an environment's secrets: list, does a key set there override the same key set in values:, or the reverse?
Whichever list position comes later wins — secrets: entries merge into the same
.Environment.Values tree as values: and are conceptually just a second, SOPS-decrypted values
list, not a separate namespace.
You can defend this when you can explain why setting a key in environments/prod.yaml has no effect
by itself until a templated values file reads .Environment.Values, why a later entry in an environment's
values: list wins over an earlier one, and why switching -e can change which releases run and not just
their configuration.
Go deeper: the full precedence spec, including CLI
--state-values-setand defaults merging, is in the Helmfile docs — this page covers the 80/20 you need for the Do exercise.