Helmfile basics: get your Helm flow organized

There are no doubts that Helm is extremely popular tool, perhaps the most popular tool in the whole Kubernetes ecosystem. But are there any ways how to streamline deployments of complex applications?

I’m not big fan of Helm when it comes to delivery of applications. When used with inappropriate amount of vigilance, it can generate a huge mess (technical debt). I’ve seen this couple of times in different places, a good example is huge Helm release used for all the services.

This might be working for smaller applications, but when you grow and adding more services, this can become a major blocker of your deployment velocity:

  • you always need to collect information for all the services so you are able to populate Helm values properly
  • when using --atomic flag, unimportant small application can break and revert the whole deployment
  • bigger release means longer execution time of the deployment (more things can go wrong in that time period)
  • small change in monolithic Helm chart might cause butterfly effect

That’s not complete list of course. But yeah, that’s the reality, Helm is here and it won’t go away any time soon. So let’s try to come with some solution!

How we got here

First we need to think about how we got here. Why do we use monolithic Helm charts? I’d say it’s mainly due to fear of repetition while installing components of the application (services). That’s a good point, when installing application with 10 services, we essentially need to run helm upgrade ten times.

Or we can create all-in-one chart that simplifies this a bit. Or we can create all-in-one chart that simplifies this a bit. Alternatively we can create an envelope chart and use separate charts for all the applications.

So this is how we got here. All the stuff that I’ve mentioned before applies here.

Enter the Helmfile

A few weeks ago I’ve discovered Helmfile which is basically wrapper for Helm that can simplify installation of multiple charts. But there’s a difference: Helmfile does not combine everything to the single Helm release.

Let’s check quick and dirty example of Helmfile for the application composed from two services.

helmDefaults:
  atomic: true
  wait: true
  timeout: 3600
releases:

  - name: backend
    namespace: backend-production
    createNamespace: true
    labels:
      app: backend
    chart: ./charts/backend
    missingFileHandler: Error
    set:
      - name: image.tag
        value: {{ env "BACKEND_VERSION" }}
    values:
      - "./environments/{{ .Environment.Name }}/backend-values.yaml"
      - "./environments/{{ .Environment.Name }}/values.yaml"
    secrets:
      - "environments/{{ .Environment.Name }}/backend-secrets.enc.yaml"
      - "environments/{{ .Environment.Name }}/secrets.enc.yaml"

  - name: frontend
    namespace: frontend-production
    createNamespace: true
    labels:
      app: frontend
    chart: ./charts/frontend
    missingFileHandler: Error
    set:
      - name: image.tag
        value: {{ env "FRONTEND_VERSION" }}
    values:
      - "./environments/{{ .Environment.Name }}/frontend-values.yaml"
      - "./environments/{{ .Environment.Name }}/values.yaml"
    secrets:
      - "environments/{{ .Environment.Name }}/frontend-secrets.enc.yaml"
      - "environments/{{ .Environment.Name }}/secrets.enc.yaml"

environments:
  default:
  production:

You can see a couple of things there:

  • global flags for Helm can be defined there
  • releases can be installed to different namespaces
  • helmfile somehow distinguishes between values and secrets (we will talk about this later on)
  • Helmfile is able to handle different environments
  • releases can have labels
  • we can even use environment variables!

Now, let’s go through some basic features that can simplify deployment process. I’m gonna create extra topic for each feature that I find interesting for this purpose.

Labels

There are situations when we want to install all the releases at the same time. Deployment to production is not one of them. But let’s go back to the valid situations. How about review environments? That’s a good example, right? In such case, you can simple run helmfile apply.

helmfile apply

End of story. Now we have Helm releases backend in the namespace backend-production and frontend in the namespace frontend-production.

Now let’s focus on the production scenario when we want to install only on release at time.

helmfile --environment production --selector app=backend diff

This command installs only one release: backend to the namespace backend-production.

Environments

In the previous sections you may notice --environment flag. That’s Helmfile internal functionality that introduces some templating functions directly to the helmfile.yaml specification.

With this feature you can load different values for different environments:

    values:
      - "./environments/{{ .Environment.Name }}/backend-values.yaml"
      - "./environments/{{ .Environment.Name }}/values.yaml"

And guess what, you can use it even for namespaces:

releases:
  - name: backend
    namespace: backend-{{ .Environment.Name }}

With this feature we just need to create a good directory structure and that’s it:

.
├── charts
│   ├── backend
│   └── frontend
├── environments
│   ├── default
│   │   ├── backend-secrets.enc.yaml
│   │   ├── backend-values.yaml
│   │   ├── secrets.enc.yaml
│   │   └── values.yaml
│   └── production
│       ├── backend-secrets.enc.yaml
│       ├── backend-values.yaml
│       ├── secrets.enc.yaml
│       └── values.yaml
├── helmfile.yaml
└── README.md

Environment variables

The typical use case for environment variables is updating of service versions. In Kubernetes terminology we’re basically switching images.

Traditionally we process the version in the orchestration tool and then we provide it as --set flag to Helm. With Helmfile, you can skip this part and you can use environment variables as the reference directly in helmfile.yaml.

    set:
      - name: image.tag
        value: {{ env "BACKEND_VERSION" }}

This feature is rather for machines (automation engines) but still I find this as a really useful one.

Secrets management

Helmfile integrates with Helm secrets plugin for Helm. With this plugin you have an ability to store encrypted secrets in git repository using SOPS tool from Mozilla engineering team.

I don’t want to go soo deep to SOPS internals but here are the key takeaways:

  • secrets can be encrypted with managed solutions like AWS KMS or GCP KMS
  • SOPS is encrypting values only, this means that keys are visible and you can see the context while doing reviews for changes

Here’s the example for the second takeaway:

global:
    secrets:
        GLOBAL_SECRET: ENC[AES256_GCM,data:LuHVrAE6nkFH,iv:EHm8cPw8dfaHQSyqN0YKEpK/53gv1ljL5zYWACqUP2E=,tag:Rx9bcfZ3XCJph50wWKYAew==,type:str]

Now let’s just connect this SOPS stuff with Helmfile. All you need to know is that Helmfile is handling decryption process and then it merges secrets with values.

All you need to know is that Helmfile is handling decryption process and then it merges secrets with values. There’s no magic there, all you need to do is refer values as usual:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}
type: Opaque
data:
  {{- range $key, $value := .Values.global.secrets }}
  {{ $key }}: {{ $value | b64enc | quote }}
  {{- end }}

Wrap

Is it all you can do with Helmfile? Hell no! Helmfile has a lot of features that I did not cover in this blog post. However I was mainly focusing on release cycle of applications with Helm so that’s why I’ve named here only few features.

Let’s wrap this blog post with my advice: try to create not so complex Helm charts and if you need to compose more things together, use rather some orchestration tool without adding more complexity to charts. Helmfile can serve you well for this purpose and it also does not create any lock-in so you can switch back to plain Helm anytime.