Introduction à l'utilisation de HelmFile

BrunoBruno
12 min read

Découverte Helmfile

Qu'est ce que Helmfile ?

Helmfile est un wrapper pour les déploiements de charts Helm.

Helmfile ajoute des fonctionnalités supplémentaires à Helm et permet, entre autres, de déployer un ensemble de charts Helm pour créer un artefact de déploiement complet.

Il prend en charge la customisation des déploiements selon l'environnement, la gestion des secrets mais aussi l'utilisation de Kustomize.

HelmFile utilise une approche déclarative, nous allons décrire l'état désiré de notre stack applicative et HelmFile fera le reste.

Le projet est relativement bien suivi par la communauté et les mises à jour sont régulières.

Installation

L'installation peut se faire de plusieurs manières.

Ici, nous allons installer le binaire directement sur notre distrribution Linux.

A la date de l'écriture de cet article, la dernière version stable est la v0.171.0.

$ curl -L https://github.com/helmfile/helmfile/releases/download/v0.171.0/helmfile_0.171.0_linux_amd64.tar.gz | tar -xz helmfile
$ sudo mv helmfile /usr/local/bin/

Vous pouvez vérifier que HelmFile est bien installé sur votre OS.

$ helmfile --version
helmfile version 0.171.0

Je vous renvoie au projet GitHub officiel pour les autres types d'installation:

  • Package Manager

  • Docker

Préparation

Pour cette démo, j'utilise minikube pour pouvoir profiter rapidement d'un cluster Kubernetes fonctionnel.

Le binaire Helm est un prérequis à l'utilisation de HelmFile.
Installons donc helm avant de rentrer dans le vif du sujet.

curl -L https://get.helm.sh/helm-v3.17.1-linux-amd64.tar.gz | tar -xz linux-amd64/helm --strip-components 1
$ sudo mv helm /usr/local/bin/

HelmFile utilise par défaut certains plugins Helm.

Avant la première utilisation, il est donc nécessaire d'initialiser HelmFile pour que ce dernier aille récupérer les plugins Helm nécessaires à son fonctionnement.

$ helmfile init
The helm plugin "diff" is not installed, do you want to install it? [y/n]: y
Install helm plugin diff
Downloading https://github.com/databus23/helm-diff/releases/download/v3.9.14/helm-diff-linux-amd64.tgz
Preparing to install into /home/bruno/.local/share/helm/plugins/helm-diff
Installed plugin: diff

The helm plugin "secrets" is not installed, do you want to install it? [y/n]: y
Install helm plugin secrets
Installed plugin: secrets

The helm plugin "s3" is not installed, do you want to install it? [y/n]: y
Install helm plugin s3
Downloading and installing helm-s3 v0.16.0 ...
Checksum is valid.
Installed plugin: s3

The helm plugin "helm-git" is not installed, do you want to install it? [y/n]: y
Install helm plugin helm-git
Installed plugin: helm-git

helmfile initialization completed!

Utilisation

Démarrons l'utilisation de HelmFile par un projet simple pour poser les bases du sujet.

Mon premier projet HelmFile

Je souhaite installer NGinx.

Supposons une première version du fichier helmfile.yaml qui va représenter l'état désiré de notre premiére release Helm ⬇

repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: nginx
  namespace: default
  chart: bitnami/nginx

Pour synchroniser l'état du cluster K8S avec la déclaration du fichier helmfile.yaml, il suffit de taper la commande helmfile apply en étant dans le dossier où se trouve le fichier déclaratif helmfile.yaml.

$ helmfile apply
Adding repo bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

Comparing release=nginx, chart=bitnami/nginx, namespace=default
********************

    Release was not present in Helm.  Diff will show entire contents as new.

********************
default, nginx, Deployment (apps) has been added:
- 
+ # Source: nginx/templates/deployment.yaml
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: nginx
+   namespace: "default"

[...]

Upgrading release=nginx, chart=bitnami/nginx, namespace=default
Release "nginx" does not exist. Installing it now.
NAME: nginx
LAST DEPLOYED: Thu Mar  6 20:44:24 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: nginx
CHART VERSION: 19.0.1
APP VERSION: 1.27.4

[...]

Listing releases matching ^nginx$
nginx    default      1           2025-03-06 20:44:24.42934593 +0100 CET    deployed    nginx-19.0.1    1.27.4     


UPDATED RELEASES:
NAME    NAMESPACE   CHART           VERSION   DURATION
nginx   default     bitnami/nginx   19.0.1          4s

L'output de la commande est tronquée ici pour des raisons de lisibilité mais, par défaut, HelmFile affiche la différence entre l'état du cluster avant l'installation et après l'éxécution de l'installation de la release du chart Helm NGinx grâce au plugin helm-diff.

Bon, c'est bien beau tout ça mais par défaut, NGinx installe un service en mode LoadBalancer et je n'ai pas de service de type LoadBalancer sur Minikube pour cette démo donc ce dernier reste désespérément sur un status pending.

nginx        LoadBalancer   10.107.63.122   <pending>     80:30309/TCP,443:31536/TCP   24h

Nous allons donc surcharger les values pour demander à ce que notre service se transforme en NodePort.
Jetons un oeil aux values du chart Helm NGinx proposé par Bitnami : https://artifacthub.io/packages/helm/bitnami/nginx?modal=values

Nous allons changer la value service.type ⬇

repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: nginx
  namespace: default
  chart: bitnami/nginx
 values:
    - service:
        type: NodePort

Une commande particulièrement utile, helmfile diff permet de visualiser les changements qui vont être opérés sur le cluster K8S avant son éventuel déploiement.

$ helmfile diff
Adding repo bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

Comparing release=nginx, chart=bitnami/nginx, namespace=default
default, nginx, Service (v1) has changed:
  # Source: nginx/templates/svc.yaml
  apiVersion: v1
  kind: Service
  metadata:
    name: nginx
    namespace: "default"
    labels:
      app.kubernetes.io/instance: nginx
      app.kubernetes.io/managed-by: Helm
      app.kubernetes.io/name: nginx
      app.kubernetes.io/version: 1.27.4
      helm.sh/chart: nginx-19.0.1
    annotations:
  spec:
-   type: LoadBalancer
+   type: NodePort
    sessionAffinity: None
    externalTrafficPolicy: "Cluster"
    ports:
      - name: http
        port: 80
        targetPort: http
      - name: https
        port: 443
        targetPort: https
    selector:
      app.kubernetes.io/instance: nginx
      app.kubernetes.io/name: nginx

Parfait, c'est le résultat attendu.
Passons à l'éxecution.

$ helmfile apply
Adding repo bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

Comparing release=nginx, chart=bitnami/nginx, namespace=default
default, nginx, Service (v1) has changed:
[...]
Upgrading release=nginx, chart=bitnami/nginx, namespace=default
Release "nginx" has been upgraded. Happy Helming!
NAME: nginx
LAST DEPLOYED: Fri Mar  7 21:04:41 2025
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
CHART NAME: nginx
CHART VERSION: 19.0.1
APP VERSION: 1.27.4
[...]

Notre v2 est maintenant installée avec un service NodePort fonctionnel.

$ helm ls
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
nginx   default         2               2025-03-07 21:04:41.076721801 +0100 CET deployed        nginx-19.0.1    1.27.4     

$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
nginx        NodePort    10.107.63.122   <none>        80:30309/TCP,443:31536/TCP   24h

Environnements

Jusque là, l'application de modifications reste basique.
HelmFile est capable de gérer plusieurs configurations selon les environnements que l'on souhaite installer.

Les environnements peuvent être définis dans différents fichiers déclaratifs YAML qui vont représenter les configurations spécifiques qui seront installées pour l'environnement choisi.

Continuons sur notre installation NGinx pour y appliquer une configuration particulière selon sur quel environnement nous souhaitons installer notre release.

Par défaut, HelmFile utilise un environnement qu'il nomme default.

Supposons que, pour notre environnement de production, les métriques soient mises à disposition, là où en hors-production, nous n’en voulons pas.

Créons notre fichier d'environnement que nous nommerons environments.yaml.

environments:
  default:
  prod:
    values:
    - prod-environment-values.yaml
  hprod:
    values:
    - hprod-environment-values.yaml

Laissons de côté l'environnement default pour nous concentrer sur nos 2 autres environnements.
Nous allons donc créer les fichiers de values spécifiques à nos environnements PROD et HPROD.

Le premier, prod-environment-values.yaml sera construit ainsi ⬇

metrics:
  enabled: true

Le second, hprod-environment-values.yaml, vous l'aurez facilement compris sera construit ainsi ⬇

metrics:
  enabled: false

La dernière étape consiste à informer le fichier helmfile.yaml de prendre en compte ces configurations d'environnements spécifiques grâce à la directive suivante:

bases:
- environments.yaml

Notre fichier complet devient donc ⬇

repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

bases:
- environments.yaml

---

releases:
- name: nginx
  namespace: default
  chart: bitnami/nginx
  values:
    - service:
        type: NodePort
    - metrics:
        enabled: {{ .Values.metrics }}

Maintenant, si nous voulons installer une version NGinx avec les métriques activées, il nous suffit de passer la commande suivante ⬇

$ helmfile apply -e prod

Je me retrouve bien avec la version de NGinx qui possède en sidecar le container pour les métriques.

$ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6df97d7679-7qvdb   2/2     Running   0          24m

$ kubectl get pods -o=custom-columns='CONTAINERS:spec.containers[*].name'
CONTAINERS
nginx,metrics

A l'inverse, pour déployer la version hors-production sans l'exposition des métriques Prometheus ⬇

$ helmfile apply -e hprod
$ k get po
NAME                    READY   STATUS    RESTARTS   AGE
nginx-bd777c6bc-fkv45   1/1     Running   0          13s

$ kubectl get pods -o=custom-columns='CONTAINERS:spec.containers[*].name'
CONTAINERS
nginx

La différence se trouve sous la colonne Ready indiquant le nombre de containers que contient le pod.

Il est possible également de pouvoir définir l'environnment à installer au travers de la variable d'environnement HELMFILE_ENVIRONMENT.

Gestion des secrets

HelmFile est capable de pouvoir gérer les secrets en utilisant le plugim helm-secrets ou d'utiliser des coffres-forts distants au travers du packages vals.

Dans cette démo, nous allons stocker notre secret dans un coffre-fort openBao pour le récupérer lors du déploiement de notre release NGinx.

Profitons-en pour ajouter l'installation d'OpenBao au travers de notre fichier helmfile.yaml

repositories:
- name: openbao
  url: https://openbao.github.io/openbao-helm
- name: bitnami
  url: https://charts.bitnami.com/bitnami

bases:
  - environments.yaml

---

releases:
- name: openbao
  namespace: default
  chart: openbao/openbao
- name: nginx
  namespace: default
  chart: bitnami/nginx
  values:
    - values.yaml
    - service:
        type: NodePort
    - metrics:
        enabled: {{ .Values.metrics }}

Appliquons notre modification en envoyant la commande helmfile apply.
Le résultat: Les releases des charts Helm NGinx et OpenBao seront déployés sur notre namespace.

$ helmfile apply -e=prod
[...]

$ kubectl get po
NAME                                      READY   STATUS    RESTARTS   AGE
nginx-6df97d7679-lbcnc                    2/2     Running   0          6m27s
openbao-0                                 1/1     Running   0          6m28s
openbao-agent-injector-7c7589ff5b-k78zb   1/1     Running   0          6m29s

J'ai généré 3 secrets sur OpenBao en amont et je vais m'en servir pour personnaliser le comportement de NGinx et utiliser mes propres certificats et clés TLS.

Nous allons récupérer nos secrets depuis OpenBao dans nos déploiements pour éviter de laisser trainer des secrets (comme la clé privée pour le TLS dans le cas présent) sur les fichiers HelmFile ou Helm.
Créons un fichier default-environment-values.yaml.

service:
  tlscrt: ref+vault://kv/secretsPasswords/#tlscrt
  tlskey: ref+vault://kv/secretsPasswords/#tlskey

Nous devons maintenant informer notre fichier d'environnements que nous souhaitons utiliser ce nouveau fichier default-environment-values.yaml dans nos déploiements PROD et HPROD.

Le fichier environemnts.yaml devient ⬇

environments:
  default:                                                            
  prod:                                            
    values:
    - default-environment-values.yaml
    - prod-environment-values.yaml
  hprod:
    values:
    - default-environment-values.yaml
    - hprod-environment-values.yaml

Nous allons ensuite modifier le fichier helmfile.yaml pour remplacer le certificat, la clé privée et le CA de notre release NGinx.

Attention, le fichier commence à être conséquent.
Il s'agit là d'ajouter un manifeste K8S Secret de type kubernetes.io/tls qui va contenir notre certificat, sa clé privée et l'autorité de certification que nous avons ajouté à OpenBao et récupéré sur les values suivantes :

  • service.tlscrt

  • service.tlskey

NGinx demande à ce que le certificat et la chaine de certification soit concaténé dans un seul et unique fichier. Le fichier tls.crt contient donc le certificat + l’autorité de certification.

Cette étape est possible grâce à la value extraDeploy offerte par le chart Helm NGinx de Bitnami.

  values:
    - extraDeploy:
        - apiVersion: v1
          kind: Secret
          metadata:
            name: custom-tls
          type: kubernetes.io/tls
          data:
            tls.key: {{ .Values.service.tlskey | fetchSecretValue | b64enc }}
            tls.crt: {{ .Values.service.tlscrt | fetchSecretValue | b64enc }}

La seconde modification du fichier helmfile.yaml va créer un bloc de configuration NGinx pour lui informer de l’emplacement de nos certificats et clés et lui demander de faire du TLS.

  - serverBlock: |-
        # HTTPS Server
        server {
            # Port to listen on, can also be set in IP:PORT format
            listen  8443 ssl;
            ssl_certificate      /certs/tls.crt;
            ssl_certificate_key  /certs/tls.key;
            include  "/opt/bitnami/nginx/conf/bitnami/*.conf";
            location /status {
                stub_status on;
                access_log   off;
                allow 127.0.0.1;
                deny all;
            }
        }

La dernière modification du fichier helmfile.yaml va surcharger les values relatives au TLS pour y prendre en compte nos nouveaux fichiers déployés sur le secret K8S custom-tls.

    - tls:
        enabled: true
        existingSecret: custom-tls
        certFilename: tls.crt
        certKeyFilename: tls.key

Le fichier au complet ⬇

repositories:
- name: openbao
  url: https://openbao.github.io/openbao-helm
- name: bitnami
  url: https://charts.bitnami.com/bitnami

bases:
  - environments.yaml

---

releases:
- name: openbao
  namespace: default
  chart: openbao/openbao
- name: nginx
  namespace: default
  chart: bitnami/nginx
  values:
    - service:
        type: NodePort
    - metrics:
        enabled: {{ .Values.metrics }}
    - tls:
        enabled: true
        existingSecret: custom-tls
        certFilename: tls.crt
        certKeyFilename: tls.key
    - extraDeploy:
        - apiVersion: v1
          kind: Secret
          metadata:
            name: custom-tls
          type: kubernetes.io/tls
          data:
            tls.key: {{ .Values.service.tlskey | fetchSecretValue | b64enc }}
            tls.crt: {{ .Values.service.tlscrt | fetchSecretValue | b64enc }}
    - serverBlock: |-
        # HTTPS Server
        server {
            # Port to listen on, can also be set in IP:PORT format
            listen  8443 ssl;
            ssl_certificate      /certs/tls.crt;
            ssl_certificate_key  /certs/tls.key;
            include  "/opt/bitnami/nginx/conf/bitnami/*.conf";
            location /status {
                stub_status on;
                access_log   off;
                allow 127.0.0.1;
                deny all;
            }
        }

Dernière étape, créer les variables d'environnement suivantes pour que HelmFile puisse se connecter à notre coffre-fort:

$ export VAULT_ADDR=<FQDN de votre instance OpenBao>
$ export VQULT_TOKEN=<Votre token OpenBao>

Appliquons la modification et observons l'état de notre déploiement.

$ helmfile apply -e=prod
[...]

$ kubectl port-forward --address 0.0.0.0 svc/nginx 8000:443
$ curl -v https://127.0.0.1:8000
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=n0l1n3ry; C=FR; ST=xxx; L=xxx; O=n0l1n3ry
*  start date: Mar  8 20:03:46 2025 GMT
*  expire date: Mar  6 20:03:46 2035 GMT
[...]

Notre nouveau certificat est bien pris en compte et aucun secret n'est visible sur la configuration HelmFile.

Kustomize

HelmFile gère également certains transformers Kustomize, le strategicMergePatches et le jsonPatches pour jouer et opérer des tranformations aux manifestes K8S d'un projet.

Commençons par y introduire une annotation sur le déploiement NGinx à l'aide du builtin AnnotationTransformer de Kustomize.

Créons un fichier kustomization.yaml. (Le nom ici n'a pas d'importance)

commonAnnotations:
  producedBy: n0l1n3ry

Informons notre fichier principal helmfile.yaml de la présence de notre nouveau fichier en y ajouter une ligne sous la directive values de notre release NGinx.

[...]
releases:
[...]
  chart: bitnami/nginx
  values:
    - kustomization.yaml
[...]

Appliquons la modification et observons l'état de notre déploiement.

$ helmfile apply -e=prod
$ kubectl get deploy nginx -o jsonpath='{.metadata.annotations}' | jq .
{
  "deployment.kubernetes.io/revision": "1",
  "meta.helm.sh/release-name": "nginx",
  "meta.helm.sh/release-namespace": "default",
  "producedBy": "n0l1n3ry"
}

Et comme je n'ai aucune imagination, tentons de tranformer cette annotation commune par une autre sur le déploiement NGinx uniquement.

Pour cela, utilisons jsonPatches.
Ajoutons à la fin du fichier helmfile.yaml la configuration suivante:

 jsonPatches:
    - target:
        version: v1
        kind: Deployment
        name: nginx
      patch:
        - op: replace
          path: /metadata/annotations/producedBy
          value: "n0l1n3ry Corporation"

Le fichier helmfile.yaml au complet ⬇

repositories:
- name: openbao
  url: https://openbao.github.io/openbao-helm
- name: bitnami
  url: https://charts.bitnami.com/bitnami

bases:
  - environments.yaml

---

releases:
- name: openbao
  namespace: default
  chart: openbao/openbao
- name: nginx
  namespace: default
  chart: bitnami/nginx
  values:
    - kustomization.yaml
    - service:
        type: NodePort
    - metrics:
        enabled: {{ .Values.metrics }}
    - tls:
        enabled: true
        existingSecret: custom-tls
        certFilename: tls.crt
        certKeyFilename: tls.key
    - extraDeploy:
        - apiVersion: v1
          kind: Secret
          metadata:
            name: custom-tls
          type: kubernetes.io/tls
          data:
            tls.key: {{ .Values.service.tlskey | fetchSecretValue | b64enc }}
            tls.crt: {{ .Values.service.tlscrt | fetchSecretValue | b64enc }}
    - serverBlock: |-
        # HTTPS Server
        server {
            # Port to listen on, can also be set in IP:PORT format
            listen  8443 ssl;
            ssl_certificate      /certs/tls.crt;
            ssl_certificate_key  /certs/tls.key;
            include  "/opt/bitnami/nginx/conf/bitnami/*.conf";
            location /status {
                stub_status on;
                access_log   off;
                allow 127.0.0.1;
                deny all;
            }
        }
  jsonPatches:
    - target:
        version: v1
        kind: Deployment
        name: nginx
      patch:
        - op: replace
          path: /metadata/annotations/producedBy
          value: "n0l1n3ry Corporation"

Le résultat sur notre déploiement NGinx:

$ kubectl get deploy nginx -o jsonpath='{.metadata.annotations}' | jq .
{
  "deployment.kubernetes.io/revision": "1",
  "meta.helm.sh/release-name": "nginx",
  "meta.helm.sh/release-namespace": "default",
  "producedBy": "n0l1n3ry Corporation"
}

Suppression

Pour supprimer nos releases NGinx et OpenBao, une seule commande suffit ⬇

$ helmfile destroy

Plus aucune ressource ou manifest K8S ne sera présent sur notre namespace.

Conclusion

Il ne s'agit-là que d'un aperçu des possibilités qu'offre HelmFile.
Je ne peux que vous conseillez d'aller voir la documentation officielle du projet pour y découvrir toutes les possibilités de cet outil et la valeur ajouté qu'il peut apporter aux déploiements de vos releases Helm.

0
Subscribe to my newsletter

Read articles from Bruno directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bruno
Bruno

Depuis août 2024, j'accompagne divers projets sur l'optimisation des processus DevOps. Ces compétences, acquises par plusieurs années d'expérience dans le domaine de l'IT, me permettent de contribuer de manière significative à la réussite et l'évolution des infrastructures de mes clients. Mon but est d'apporter une expertise technique pour soutenir la mission et les valeurs de mes clients, en garantissant la scalabilité et l'efficacité de leurs services IT.