Why does a Helm values override replace a whole list instead of merging into it?
answer
- Not every value merges the same way
- Keys can be matched, positions cannot
- Maps deep-merge, sequences do not
- The higher-precedence list wins whole
basics
~20 sHelm deep-merges maps key by key, but treats a YAML list as one opaque value: an override that supplies a list replaces the chart's list entirely. List elements have no key to match on, so nothing can be merged or appended.
solid answer
~50 sHelm's values merge walks two maps and combines them key by key, recursing whenever both sides hold a map. That is why a small override file can set `resources.limits.cpu` without disturbing the memory limit beside it. A list has no such handle - elements are identified only by position, and position is not identity, so Helm stops recursing and lets the higher-precedence list win as a unit. The consequence is that you cannot add one entry to a list a chart already defaults: your override must restate every element you want, defaults included. `--set targets[0].port=9127` is the same trap in flag form - it builds a one-element list that replaces the chart's list rather than patching element zero. Passing the full list once, in a `-f` file or with `--set-json`, is the honest way to do it.
code
yaml · 13 lines# monitoring-stack/values.yaml - the chart's default
scrapeTargets:
- name: billing
port: 9127
- name: ledger
port: 9128
- name: gateway
port: 9129
# values/prod-extra.yaml - REPLACES all three, does not add a fourth
scrapeTargets:
- name: fraud
port: 9131go deeper
Remember the rule itself: in Helm values, maps merge and lists are replaced whole. If you override a list, write out every element you want, because the chart's entries will not come along.
Explain why the rule exists - map keys give identity, list positions do not - and describe the merge walking two maps and recursing only while both sides are maps. Be able to show that --set with an index replaces the list too.
Show how this bites in production: an override file that reads as an addition silently drops chart defaults, and the release still installs green. Talk about rendering and diffing before applying, and about reviewing any list in an override as a full replacement.
Own the design consequence: values contracts that expose lists push replacement semantics onto every consumer and make chart upgrades a merge burden. Decide when a keyed map is worth the churn, and how your platform reviews override files at scale.
Helm merges values with a recursive table merge. Given a lower-precedence map and a higher-precedence one, it walks the keys of both. ## How the merge decides, key by key - **The key exists on only one side** - that value is taken. - **It exists on both, and both values are maps** - Helm recurses into them and repeats the process. - **It exists on both, and either value is not a map** - the higher-precedence value wins outright. Lists fall into that last case, and everything surprising about list overrides follows from it. ## Why lists cannot merge Merging requires identity: to decide that two things are the same thing seen twice, you need a name for them. Map keys are exactly that. A list gives you only ordinal position, and position is not identity - the second element of the chart's default list and the second element of your override list are not "the same target seen twice", they are two unrelated entries that happen to be second. Any merge rule Helm invented here would be wrong for someone: index-wise merge breaks the moment an author reorders defaults, append breaks anyone trying to shrink a list, and merge-by-some-guessed-field breaks lists of scalars. Replacement is the only rule that is always predictable. ## What it looks like in practice Suppose a monitoring-stack chart's `values.yaml` defaults `scrapeTargets` to three entries - billing on 9127, ledger on 9128, gateway on 9129 - and a platform engineer adds `values/prod-extra.yaml` containing a single fraud-service target. The merged value is a one-element list. The three defaults are gone, and because the render succeeds and the resulting object is valid, nothing fails: the chart installs cleanly and three services quietly stop being scraped. This is the single most common values bug in real charts, and it is invisible in review because the override file looks like an addition. ## The `--set` form is the same trap `--set scrapeTargets[0].port=9127` reads like a patch on element zero, but the flag parser builds a small structure - a list containing one map with one key - and that structure is then merged like any other override. Since it is a list, it replaces. Addressing a later index does not pull the chart's earlier elements along either. If a value is a list, you own all of it or none of it. ## Three honest ways to work with the rule 1. **Restate the full list** in an override file - all the defaults you still want plus your addition - and accept that you now have to track chart-default changes when you upgrade. 2. **Pass the complete list in one `--set-json`**, which lets you write real JSON structure on the command line instead of fighting bracket syntax. 3. **Expose the setting as a map keyed by name** rather than as a list, when you control the chart, so that overrides merge per key and a consumer can add one entry without owning the rest; that is a chart-design decision with its own tradeoffs, but it is the reason many mature charts key such settings by name. ## Empty is not the same as absent | Override | The key in the merged values | What the chart sees | | --- | --- | --- | | `[]` | Present, holding an empty list | `toYaml` renders `[]`, and a `range` over it produces nothing | | `null` | Removed entirely | A chart expression that supplies a fallback when the key is missing takes its other branch | Both are falsey to a plain `if`, which is why the two get conflated, and they differ exactly when the chart is doing something more careful than a plain `if`. ## Catching it Render the chart locally with the exact flags before applying, and read the section that came from the list. In review, treat any override file that contains a list at all as a full replacement of that list and ask whether the author meant to drop what is not written there. A diff of two rendered manifests - the one running now and the one about to ship - turns this from a subtle merge rule into a visible deletion.
- So how do you actually add one entry to a list the chart already defaults?You restate the whole list - the defaults you still want plus your new entry - either in a `-f` file or in a single `--set-json`. There is no append operator. If you own the chart, the durable fix is to expose that setting as a map keyed by name so overrides merge per key and consumers no longer inherit responsibility for the other entries.
- What is the difference between overriding a list with [] and with null?`[]` keeps the key with an empty list, so it still exists in `.Values` and renders as `[]`. `null` deletes the key from the merged values, so it is absent and any fallback the chart applies for a missing key takes effect. Both look false to a plain `if`, which is why they get confused; they diverge whenever the chart checks for presence rather than truth.
- Does the same replacement rule apply between two -f files, or only between a file and the chart defaults?It applies at every layer. The merge is the same operation whether it is combining the chart's defaults with a file, one file with the next, or a file with the `--set` flags. A list in a higher-precedence layer always replaces the list beneath it, so a second override file can silently discard the list the first one carefully assembled.
A map is a labelled drawer unit - you can swap the contents of one drawer and leave the rest alone. A list is a sealed box with no labels; the only thing Helm can do is hand over a different box.
saying these in an interview costs you the question
- Claims override files append their list items to the defaults
- Thinks --set targets[0].port patches only that element
- Says maps and lists merge the same way
- Expects an empty list override to leave defaults in place
- Believes a later -f file replaces the whole values tree
- Assumes Helm merges list elements by matching a name field