The Ops Community ⚙️

Lucy Linder
Lucy Linder

Posted on • Edited on • Originally published at blog.derlin.ch

helmfile: a simple trick to handle values intuitively

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
...
Enter fullscreen mode Exit fullscreen mode

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 
...
Enter fullscreen mode Exit fullscreen mode

To deploy to prod:

helm install my-umbrella-name . \
   --values environments/prod.yaml
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, create as many files as you have environments (environment prodenvironments/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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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:

GitHub logo 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 !
👉 helmfile: a simple trick to handle values intuitively 👈

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 are local and prod),
  • see for yourself how your changes work by running: helmfile -e <env> write-values and see the output.

To apply this in your helmfile:

  1. ensure you reference env-magic.gotmpl under all release values (releases.<releaseName>.values) and,
  2. ensure…

Written with ❤ by derlin

Top comments (7)

Collapse
 
nati profile image
Nati Sayada

Super helpfull!
Comming from helm umberlla chart this one was preatry hard for me.

in my setup iv change the command to

{{ deepCopy (.Values | get "global"  dict) | merge (.Values | get .Release.Name  dict) | toYaml }}
Enter fullscreen mode Exit fullscreen mode

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

global:
  image:
    repository: my.repo.io
    tag: ###-18483-SNAPSHOT
Enter fullscreen mode Exit fullscreen mode
Collapse
 
derlin profile image
Lucy Linder

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?)

Collapse
 
nati profile image
Nati Sayada

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

Collapse
 
fforloff profile image
Victor Orlov • Edited

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")

Collapse
 
derlin profile image
Lucy Linder

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?

Collapse
 
fforloff profile image
Victor Orlov

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 }}

Thread Thread
 
derlin profile image
Lucy Linder

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.