Aller au contenu

Mon premier chart Helm

Mes premières commandes Helm

Helm est le gestionnaire de paquet de Kubernetes. Les paquets helm peuvent se trouver soit dans un dépot de charts, soit en local.

Pour ajouter un dépot. On peut les trouver sur Artifact Hub -- un catalogue des charts.

helm repo add stable https://charts.helm.sh/stable

Listez les charts que vous pouvez installer :

helm search repo stable
NAME                                    CHART VERSION   APP VERSION             DESCRIPTION                                       
stable/acs-engine-autoscaler            2.2.2           2.1.1                   DEPRECATED Scales worker nodes within agent pools 
stable/aerospike                        0.3.5           v4.5.0.5                DEPRECATED A Helm chart for Aerospike in Kubern...
stable/airflow                          7.13.3          1.10.12                 DEPRECATED - please use: https://github.com/air...
stable/ambassador                       5.3.2           0.86.1                  DEPRECATED A Helm chart for Datawire Ambassador   
stable/anchore-engine                   1.7.0           0.7.3                   Anchore container analysis and policy evaluatio...
...

Installez un chart d'exemple :

helm repo update &&\
helm install my-example-mysql stable/mysql --set resources.limits.memory=500Mi

Verifiez que l'installation a créée les ressources dans le namespace tuto :

k get all

NAME                                   READY   STATUS    RESTARTS      AGE
pod/jupyter-74bfb8cc64-g78rs           1/1     Running   1 (65m ago)   66m
pod/my-example-mysql-6cb4f55c5-hmcqn   1/1     Running   0             83s

NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/jupyter            ClusterIP   10.43.202.0     <none>        8888/TCP   7d1h
service/my-example-mysql   ClusterIP   10.43.197.116   <none>        3306/TCP   83s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/jupyter            1/1     1            1           7d1h
deployment.apps/my-example-mysql   1/1     1            1           83s

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/jupyter-74bfb8cc64           1         1         1       66m
replicaset.apps/my-example-mysql-6cb4f55c5   1         1         1       83s

Helm permet de configurer les options de déploiement d'application. Ici nous avons configuré la limite de la mémoire de conteneur. Pour voir les valeurs par défaut :

helm show values stable/mysql

Et les valeurs appliquées :

helm get values my-example-mysql

Pour voir ce qui a été déployé :

helm ls

NAME                NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
my-example-mysql    tuto        1           2025-03-12 18:09:16.987606589 +0100 CET deployed    mysql-1.6.9 5.7.30     

Et désinstaller une release :

helm uninstall my-example-mysql

Création de chart d'exemple

Maintenant, nous allons créer le chart Helm à partir des manifests que nous avions déjà créés.

Créer un projet de chart avec :

helm create jupyter-chart
tree jupyter-chart
jupyter-chart
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   ├── service.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

4 directories, 10 files
  • un fichier values.yaml contient les paramètres ajustables
  • un fichier Chart.yaml contient metadata (nom, version, description ...)
  • un dossier charts peut contenir les charts dont ce chart dépendre
  • un dossier templates contient des modèles des manifests (écrits avec langage des templates de Go)
  • _helpers.tpl fournit des fonctions utiles
  • NOTES.txt est une modèle de ce que helm affiche lors d'installation de chart
  • un dossier tests contient des définitions de Pod/Job qui peuvent être utilisés pour tester l'installation

Creation du chart Jupyter

Nous allons remplaces les manifests de base pour créer notre propre charte d'application Jupyter.

cp configmap.yaml jupyter-chart/templates/.
cp ingress.yaml jupyter-chart/templates/.
cp jupyter-claim0-persistentvolumeclaim.yaml jupyter-chart/templates/pvc.yaml
cp jupyter-deployment.yaml jupyter-chart/templates/deployment.yaml
cp jupyter-service.yaml jupyter-chart/templates/service.yaml
cp secret.yaml jupyter-chart/templates/.
rm jupyter-chart/templates/serviceaccount.yaml
rm jupyter-chart/templates/hpa.yaml
rm -r jupyter-chart/tests
echo '' > jupyter-chart/templates/NOTES.txt
tree jupyter-chart
tree jupyter-chart/
jupyter-chart/
├── Chart.yaml
├── templates
│   ├── _helpers.tpl
│   ├── configmap.yaml
│   ├── insgress.yaml
│   ├── pvc.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── secret.yaml
└── values.yaml

2 directories, 9 files

Modifions le metadata du chart :

Chart.yaml
apiVersion: v2
name: jupyter
description: My first helm chart for Jupyter

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "4.0.7" # <-- on a mis la version de jupyterlab ici

Même qu'il ne soit pas configurable, c'est déjà un chart Helm valide et installable.

Effacons tous de Namespace tuto :

kubectl delete ns tuto
kubectl create ns tuto
kubectl-ns tuto

et installons le chart :

helm install my-jupyter jupyter-chart/
NAME: my-jupyter
LAST DEPLOYED: Wed Mar 12 19:38:02 2025
NAMESPACE: tuto
STATUS: deployed
REVISION: 1
TEST SUITE: None
helm ls

NAME        NAMESPACE   REVISION    UPDATED                                 STATUS      CHART             APP VERSION
my-jupyter  tuto        1           2025-03-12 19:38:02.264727617 +0100 CET deployed    jupyter-0.1.0       4.0.7     

Le chart a ce stade est disponible ici.

Variables de chart Helm

Nous allons ajouter des variables aux modèles des manifests.

deployment.yaml
...
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      io.kompose.service: jupyter
  strategy:
    type: {{ .Values.strategy }}
  template:
    metadata:
      labels:
        io.kompose.service: jupyter
    spec:
      containers:
        - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          name: jupyter
          resources:
            requests:
              cpu: {{ .Values.resources.requests.cpu }}
              memory: {{ .Values.resources.requests.memory }}
            limits:
              cpu: {{ .Values.resources.limits.cpu }}
              memory: {{ .Values.resources.limits.memory }}
...
service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    io.kompose.service: jupyter
  name: jupyter
spec:
  type: {{ .Values.service.type }}
  ports:
    - name: jupyterlab
      port: {{ .Values.service.externalPort }}
      targetPort: 8888
  selector:
    io.kompose.service: jupyter
ingress.yaml
...
    port:
        number: {{ .Values.service.externalPort }}
pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jupyter-claim0
spec:
  storageClassName: {{ .Values.storage.storageClass }}
  accessModes:
    - {{ .Values.storage.accessMode }}
  resources:
    requests:
      storage: {{ .Values.storage.size }}

Et aussi ajoutons les valeurs par défaut au fichier values.yaml :

values.yaml
replicaCount: 1
strategy: Recreate
image:
  repository: jupyter/base-notebook
  tag: lab-4.0.7
resources:
  requests:
    cpu: 500m
    memory: 500Mi
  limits:
    cpu: "1"
    memory: 1024Mi
service:
  type: ClusterIP
  externalPort: 8888
storage:
  storageClass: local-path
  accessMode: ReadWriteOnce
  size: 100Mi

Creez l'autre fichier values.yaml dehors du dossier de chart. Il sera utilisé pour remplacer des valeurs par défaut :

values.yaml
resources:
  limits:
    cpu: "2"

Le chart modifié et le fichier de valeurs sont fourni au repo

Appliquez la nouvelle version du chart avec des valeurs remplacées :

helm upgrade my-jupyter jupyter-chart --values values.yaml

Voir l'histoire des installations :

helm history my-jupyter
REVISION        UPDATED                         STATUS          CHART             APP VERSION     DESCRIPTION     
1               Wed Mar 12 23:16:04 2025        superseded      jupyter-0.1.0     4.0.7           Install complete
2               Wed Mar 12 23:16:11 2025        deployed        jupyter-0.1.0     4.0.7           Upgrade complete

Instructions de contrôle de flux et fonctions

Le langage des modèles Helm est une combinaison du Go template language, des fonctions additionnelles et des wrappers pour exposer les objets spécifiques de Helm. Les docs complets sont accessibles sur https://helm.sh/docs/chart_template_guide/getting_started/.

Fonctions

La syntaxe des fonctions est {{ functionName arg1 arg2 }}. Il est possible d'utiliser l'opérateur pipe | pour passer l'argument de la fonction, {{ functionName arg }} est {{ arg | functionName }} sont équivalents. Si la fonction accepte plusieurs arguments, le dernier est passé par |.

Pour illustrer, faisons le mot de passe configurable par valeurs

secret.yaml
apiVersion: v1
data:
  password: {{ .Values.auth.password | b64enc }}
kind: Secret
metadata:
  creationTimestamp: null
  name: jupyter-pwd

et ajoutons dans values.yaml dans le chart:

auth:
  password: password

Appliquons le chart :

helm upgrade my-jupyter jupyter-chart --values values.yaml

Le mot de passe n'est pas changé après installation de la nouvelle version. C'est parce que le manifest de déploiement n'été pas changé et il n'a pas redémarré. Alors, comme le mot de passe est injecté par conteneur d'init, il garde sa valeur. Il existe une astuce pour déclencher le redémarrage de pod lors du changement de Configmap/Secret. Ajoutons l'annotation dans déploiement:

deployment.yaml
kind: Deployment
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
...

En appliquons le chart modifié nous allons voir que le pod redémarre.

Blocs conditionnels

Le langage des modèles Helm support les blocs conditionnels if/else et les boucles range. Nous allons rendre des composants de l'installation être optionnels (- sert à supprimer les espaces (y compris retour à la ligne) avant/après le bloc "moustache") :

ingress.yaml
{{ if .Values.ingress.enabled | default true -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jupyter 
  annotations:
    ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: jupyter 
            port:
              number: {{ .Values.service.externalPort }}
{{ end }}

et ajoute le défaut aussi dans values.yaml

...
ingress:
  enabled: true

Question

Modifiez le chart pour que la création du volume persistante soit configurable. Notez que ça exige plusieurs modifications, dans le fichier pvc.yaml ainsi que dans deployment.yaml.

Vous pouvez vérifier que les modèles sont lisibles et produisent des yaml valides :

helm lint jupyter-chart/ -f values.yaml

et le chart peut être installé :

helm upgrade my-jupyter jupyter-chart/ -f values.yaml --dry-run
Success
pvc.yaml
{{ if .Values.storage.enabled | default false -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jupyter-claim0
spec:
  storageClassName: {{ .Values.storage.storageClass }}
  accessModes:
    - {{ .Values.storage.accessMode }}
  resources:
    requests:
      storage: {{ .Values.storage.size }}
{{ end }}
deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          name: jupyter
          volumeMounts:
            {{- if .Values.storage.enabled | default false }}
            - mountPath: /home/jovyan/work/local
              name: jupyter-claim0
            {{- end }}
            - name: jupyter-confdir
              mountPath: /home/jovyan/.jupyter
...
      volumes:
        {{- if .Values.storage.enabled | default false }}
        - name: jupyter-claim0
          persistentVolumeClaim:
            claimName: jupyter-claim0
        {{- end }}
        - name: initscript
          configMap:
            name: initscript
        - name: jupyter-confdir
          emptyDir: {}
...

Nous appliquons le chart :

helm upgrade my-jupyter jupyter-chart/ -f values.yaml

et vérifions que le volume n'est plus là : kubectl get pvc.

Le chart de référence à ce stade est disponible

Boucles

Instruction range permet de créer les boucles type for each. La syntaxe de base est comme :

{{ range .ListVariable }}
- {{ . | quote }}
{{ end }}

À l'intérieur de la boucle le contexte change, alors {{ . }} fait référence à l'élément actuel.

Nous allons profiter d'une syntaxe plus verbeuse qui nous permet d'utiliser les indices des éléments. Cette syntaxe utilise des variables.

Pour essayer les boucles, nous allons ajouter la possibilité de créer plusieurs volumes persistants. D'abord, modifions le fichier values.yaml (celui dans le directoire de chart) :

replicaCount: 1
strategy: Recreate
image:
  repository: jupyter/base-notebook
  tag: lab-4.0.7
resources:
  requests:
    cpu: 500m
    memory: 500Mi
  limits:
    cpu: "1"
    memory: 1024Mi
service:
  type: ClusterIP
  externalPort: 8888
storage: [] # <--changé
auth:
  password: password
ingress:
  enabled: true

Contrairement à la configuration que nous utilisions avant, .Values.storage sera une liste des dictionnaires, pas un dictionnaire.

Dans deployment.yaml :

deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          name: jupyter
          volumeMounts:
            {{- if .Values.storage | default false -}}
            {{- range $index, $volume := .Values.storage }}
            - mountPath: {{ $volume.mountPath }}
              name: jupyter-claim{{ $index }}
            {{- end -}}
            {{- end }}
            - name: jupyter-confdir
              mountPath: /home/jovyan/.jupyter
...
      volumes:
        {{- if .Values.storage | default false }}
        {{- range $index, $volume := .Values.storage }}
        - name: jupyter-claim{{ $index }}
          persistentVolumeClaim:
            claimName: jupyter-claim{{ $index }}
        {{- end -}}
        {{- end }}
        - name: initscript
          configMap:
            name: initscript
        - name: jupyter-confdir
          emptyDir: {}

Question

Changez le fichier pvc.yaml pour créer les PVCs correspondants. Notez que plusieurs ressources peuvent être places dans le même fichier en utilisant le séparateur ---. Le séparateur se répétera en boucle.

Vérifiez qu'aucun PVC n'est créé avec la valeur de storage par défaut (une liste vide). Ajoutez deux volumes au fichier values.yaml (celui dehors directoire de chart) :

storage:
- storageClass: local-path
  accessMode: ReadWriteOnce
  size: 100Mi
  mountPath: /home/jovyan/work/volume100
- storageClass: local-path
  accessMode: ReadWriteOnce
  size: 50Mi
  mountPath: /home/jovyan/work/volume50

Verifiez que deux volumes sont crééz et montés lors d'installation helm upgrade my-jupyter jupyter-chart/ -f values.yaml.

Success
pvc.yaml
{{- if .Values.storage | default false -}}
{{ range $index, $volume := .Values.storage -}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jupyter-claim{{ $index }}
spec:
  storageClassName: {{ $volume.storageClass }}
  accessModes:
    - {{ $volume.accessMode }}
  resources:
    requests:
      storage: {{ $volume.size }}
{{ end }}
{{ end }}

Le chart de référence à ce stade est disponible

Helpers

Le fichier _helpers.yaml contient des définitions utilitaires. Nous allons les utiliser pour que les noms des ressources correspondent au nom du release Helm (il est fixé par instant) et pour remplacer des étiquettes de kompose.

Nous avons besoin d'ajouter quelques valeurs par défaut qui sont utilisées dans _helpers.tpl :

jupyter-chart/values.yaml
nameOverride: ""
fullnameOverride: ""
serviceAccount:
  create: false

En déploiement (diff) :

diff -u step[34]/jupyter-chart/templates/deployment.yaml
--- step3/jupyter-chart/templates/deployment.yaml   2025-03-25 18:13:11.522701853 +0100
+++ step4/jupyter-chart/templates/deployment.yaml   2025-03-25 18:31:24.339726299 +0100
@@ -1,14 +1,14 @@
 apiVersion: apps/v1
 kind: Deployment
 metadata:
+  name: {{ include "jupyter-chart.fullname" . }}
   labels:
-    io.kompose.service: jupyter
-  name: jupyter
+    {{- include "jupyter-chart.labels" . | nindent 4 }}
 spec:
   replicas: {{ .Values.replicaCount }}
   selector:
     matchLabels:
-      io.kompose.service: jupyter
+      {{- include "jupyter-chart.selectorLabels" . | nindent 6 }}
   strategy:
     type: {{ .Values.strategy }}
   template:
@@ -16,7 +16,7 @@
       annotations:
         checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
       labels:
-        io.kompose.service: jupyter
+        {{- include "jupyter-chart.labels" . | nindent 8 }}
     spec:
       containers:
         - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
@@ -67,20 +67,21 @@
             - name: JUPYTER_PWD
               valueFrom:
                 secretKeyRef:
-                  name: jupyter-pwd
+                  name: {{ include "jupyter-chart.fullname" . }}-pwd
                   key: password
       restartPolicy: Always
       volumes:
         {{- if .Values.storage | default false }}
+        {{ $basename := include "jupyter-chart.fullname" . }}
         {{- range $index, $volume := .Values.storage }}
         - name: jupyter-claim{{ $index }}
           persistentVolumeClaim:
-            claimName: jupyter-claim{{ $index }}
+            claimName: {{ $basename }}-claim{{ $index }}
         {{- end -}}
         {{- end }}
         - name: initscript
           configMap:
-            name: initscript
+            name: {{ include "jupyter-chart.fullname" . }}-initscript
         - name: jupyter-confdir
           emptyDir: {}

Info

Notez l'utilisation de variable $basename. Dans la boucle, le contexte change alors la variable .Values n'est plus disponible (et cela est utilisé par modèle jupyter-chart.fullname).

Question

Appliquez les mêmes changements aux autres manifests. Vous pouvez suivre ceux créés par la commande helm create <name> ou consulter le chart dans le repositoire.