helmfile is a very nice and powerful tool to manage multiple Helm charts declaratively. However, there is one area in which I find it suboptimal: the handling of values / environment values.
Let's go over how it works, and see how we can make it better. If you don't like to read, skip to My tip on using values in helmfile (or read the TL;DR in the repo linked below).
For a full example, check out this code !
👉 ✨ https://github.com/derlin/helmfile-intuitive-values-handling ✨ 👈
Values in umbrella charts (pure Helm)
Coming from the Helm world, I am used to using umbrella charts, where all the default values for my charts are defined in one single values.yaml
:
# globals are available to all sub-charts
# using .Values.global.*
global:
domain: dev.example.com
foo:
# default values passed to the sub-chart called 'foo'
image: nginx
tag: latest
bar:
# default values passsed to the sub-chart called 'bar'
mode: local
...
When some values need to be overridden per environment, I simply create a file <env>.yaml
and pass it to helm using --values
. For example:
# in environments/prod.yaml
global:
domain: prod.example.com # override the domain for all
foo:
image:
tag: 1.19 # use a stable docker image
...
To deploy to prod:
helm install my-umbrella-name . \
--values environments/prod.yaml
With helmfile though, there is no easy way to reproduce this behavior (well, there is actually, keep reading 😉).
Values in helmfile
In helmfile, one defines default values for a chart using the releases.<name>.values
:
releases:
- name: foo
...
values:
- image:
repository: nginx
tag: latest
To add global values, there is an equivalent environments.default.values
, but this only makes values available to the templates... It doesn't attach those values automatically. In other words, the following does nothing:
releases:
- name: foo
...
environments:
prod:
values:
- prod: true
To make it work, we need to add some values template to release foo
(and all other releases), for example:
releases:
- name: foo
...
values:
- {{ toYaml .Values | nindent 8 }}
Now, prod: true
will be passed to foo
upon helmfile -e prod
...
The environment values are passed to all release templates, not releases! That is, they can be used inside gotmpl
templates/files listed under release.<name>.values
, but are not attached directly ...
This is already too complex to follow.
My tip on using values in helmfile
Instead of trying to understand how all those values work (and creating specific .gotmpl
files for each release), here is how I managed to mimic the umbrella chart behavior regarding values with helmfile (one default value file + one file per environment, with global
section and <release-name>
sections).
First, create a folder called environments
. In it, create a default.yaml
file, and specify the default values for each release and the globals using the "umbrella chart syntax":
global:
# ... values passed to all releases
foo:
# ... values passed to release foo
bar:
# ... values passed to release bar
Next, create as many files as you have environments (environment prod
→ environments/prod.yaml
) and override only what needs to be overridden (compared to default).
In the helmfile, configure each environment to read from default.yaml
and the specific environment values:
# in helmfile.yaml
environments:
default:
values:
- environments/default.yaml
prod:
values: # apply default first, then prod
- environments/default.yaml
- environments/prod.yaml
Now, here is the trick.
Create a magic gotmpl
file that will extract both the global
section and the release-specific section of the values:
{{/* in env-magic.gotmpl */}}
{{/*
extract both global and <release-name> sections from
.Values, and merge them (giving precedence to release
specific values.
Note: missing entries are fine.
*/}}
{{ merge (.Values | get .Release.Name dict) (.Values | get "global" dict) | toYaml }}
And attach this magic file to all releases in the helmfile:
# in helmfile.yaml
releases:
- name: foo
...
values:
- &env env-magic.gotmpl # use a YAML anchor for DRYness
- name: bar
...
values:
- *env # reference the anchor
...
That's it! Now, you can simply edit the files in environments/
, and don't have to think about (or touch) values in helmfile anymore.
Example
A complete example (and a different explanation) is available here:
derlin / helmfile-intuitive-values-handling
How to manage values (globals, environment, release-specific) intuitively within helmfile
How to handle helmfile values nicely
helmfile is a very powerful tool, but his way of handling release values is daunting for the beginners (and experts). I propose here a simple pattern to handle values, that is completely generic, intuitive and works in all situations.
Read the article !
TL;DR
This repo reproduces the way values are handled in umbrella charts (Helm Charts with sub-charts).
If you don't like to read but want to experiment instead:
- clone this repo,
- customize the different release values by editing
environments/default.yaml
, - override values per environment by editing
environments/<envName>.yaml
(available environements arelocal
andprod
), - see for yourself how your changes work by running:
helmfile -e <env> write-values
and see the output.
To apply this in your helmfile:
- ensure you reference
env-magic.gotmpl
under all release values (releases.<releaseName>.values
) and, - ensure…
Written with ❤ by derlin
Top comments (7)
Super helpfull!
Comming from helm umberlla chart this one was preatry hard for me.
in my setup iv change the command to
This will allow the values that spesified for the release to take persidens.
And using
deepCopy
will also merge the maps.This one solved my issue were the image nested object will not be merge.
my map look like this
Glad you found it helpful!
Care to explain better what the deepCopy solved? I am not sure I understand, and I am curious (the merge should deep merge dictionaries, deepCopy or not... I am missing something?)
The merge is fine, but what happens if you would also like to deep merge a map.
Think of the .image value that usually is a map that contains the registry and the version. When you are not using deepCopy, this values are not going to be merged and you are going to have inconsistency.
I'm on mobile so it's hard to get the docs.. but check it out, it's all in the merge function docs
Great post! One question though. Isn't "(.Values | get "global" dict)" leads to a behaviour, which is different from the helm umbrella?
To fully mimic it I'd suggest '(set . "global" (.Values | get "global" dict))' or, simply '(set . "global" .Values.global)' or '(pick .Values "global")
Thank you 😊
Not sure I understand your point. The
.
refers to helmfile values, and won't be passed to helm, so assigning it doesn't really matter. pick and get both return the globals dict (sole difference is the copy) and merge will anyways don't touch the second (globals) dict. Finally, in umbrella charts children don't have access to parent's values (only what is in globals and under their own name). Or am I missing something?Can you give me an example of values where the behavior would differ with my original solution?
Hi Lucy. Say we have global.domain value. With the (.Values | get "global" dict) it will be accessible as .Values.domain. However with the helm umbrella it would be .Values.global.domain, isn't it?
Therefore
{{ merge (.Values | get .Release.Name dict) (pick .Values "global") | toYaml }}
Hello Victor ! Sorry for the late reply.
Yes you are correct. If you really want the exact behavior of the umbrella chart, though, it should be easy to change the magic script.