Aller au contenu principal

Ecrire son premier chart Helm pour Kubernetes

· 8 minutes de lecture
Stéphane ROBERT

logo helm

Pour ceux qui ne connaissant pas Kubernetes Helm je vous renvoie à mon premier billet décrivant les concepts et l'utilisation de Helm en tant que gestionnaire de package.

Le langage de Helm est aussi complet et complexe par sa syntaxe. La première lecture peut être rebutante. Donc accrochez-vous!

Petite précision, je débute comme vous et je risque donc de compléter cette documentation en fonction de mes découvertes. (Si vous êtes compétent sur le sujet je prends vos remarques !)

Création d'un chart

La commande helm create <nom-du-chart> permet de créer la structure complète d'un chart avec des templates pour instancier un deployment, un service, un ingress, un serviceaccount et un autoscaler.

Structure d'un chart

Créons le chart myfirst-chart :

helm create myfirst-chart
Creating myfirst-chart

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

Nous retrouvons :

  • un dossier charts qui contient d'éventuels charts dépendants
  • un dossier templates qui contient les manifests Kubernetes qui seront générés
  • un fichier Chart.yaml qui contient les metada du chart : nom, description,type, version du chart
  • un fichier values.yaml qui contient les valeurs qui seront utilisé pour construire les manifest kubernetes finaux à partir des templates.
  • fichier .helmignore qui indiquera quels fichiers ou répertoires à ignore lors de la construction de l'artefact. Le pacakge qui sera envoyé dans votre repository helm.

Première installation du chart

Le chart créé est opérationnel et peut être instancié dans votre cluster kubernetes :

kubectl create ns test
kubectl config set-context --current --namespace=test
cd myfirst-chart
helm install myfirst-chart ./
NAME: myfirst-chart

LAST DEPLOYED: Wed Jan 19 13:48:29 2022
NAMESPACE: test
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace test -l "app.kubernetes.io/name=myfirst-chart,app.kubernetes.io/instance=myfirst-chart" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace test $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace test port-forward $POD_NAME 8080:$CONTAINER_PORT

Vérifions ce qu'il a créé :

kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myfirst-chart-7cb78598b9-jjzcm 1/1 Running 0 30s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/myfirst-chart ClusterIP 10.104.106.188 <none> 80/TCP 30s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/myfirst-chart 1/1 1 1 30s

NAME DESIRED CURRENT READY AGE
replicaset.apps/myfirst-chart-7cb78598b9 1 1 1 30s

On a donc un déployment utilisant un pod exposé avec un service de type ClusterIP. Et oui vous avez déja fait tout ça en quelques commandes. Magique non ?

Prise en main du langage de templating Go

Helm étant écrit en Go, il utilise donc le système de templating Go. Pour ceux qui connaissent Jinja vous ne serez pas trop perdu, car il s'en inspire.

Afficher le contenu du fichier templates/service.yaml :

apiVersion: v1
kind: Service
metadata:
name: {{ include "myfirst-chart.fullname" . }}
labels:
{{- include "myfirst-chart.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "myfirst-chart.selectorLabels" . | nindent 4 }}

Contrôler l'indentation de la sortie

Dans un fichier yaml la position du texte à toute son importance. Pour éviter ces problèmes on utilise le filtre/fonction nindent.

    {{- include "myfirst-chart.selectorLabels" . | nindent 4 }}

Pour éviter de se retrouver avec plein de ligne vide, on utilise le n caractère - (chomp) qui se place après {{ ou avant }} :

  • {{- supprime les espaces de la ligne avant le template (left-trim)
  • -}} supprime les espaces de la ligne après le template (right-trim)

Les pipelines et les fonctions

Les variables peuvent être manipulés via des fonctions qui sont appelés via les pipelines :

    {{- include "myfirst-chart.labels" . | nindent 4 }}

Les fonctions les plus courantes dans les fichiers générés par défaut :

  • replace : remplace une chaine par une autre
  • trunc : coupe une chaine à partir du n caractère
  • trimSuffix : retranche un suffixe d'une chaine
  • quote : ajout des doubles quotes à une "chaine"
  • nindent : La fonction nindent est identique à la fonction d'indentation, mais ajoute un retour à la ligne au début de la chaîne
  • title : Première lettre en majuscule

La documentation liste l'ensemble de ces fonctions ici

Accéder aux valeurs définies dans le fichier values.yaml

Pour rappel vous pouvez créer plusieurs fichiers de valeur avec des noms différents, mais lors de l'instanciation du chart il faudra lui indiquer le nom du fichier avec l'option -f : helm install myfirst-chart ./ -f dev-values.yaml

Une valeur définie dans le fichier de valeur est accessible sous la forme : {{ .Values. }} Donc ouvrons le fichier de valeur :

# Default values for myfirst-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

...

service:
type: ClusterIP
port: 80
...

Si on prend la valeur replicaCount elle est bien appelé sous la forme : {{ .Values.replicaCount }} De même une valeur d'un dictionnaire est accessible sous la forme : {{ .Values.service.type }}

Vous pouvez modifier ses valeurs et afficher le manifest sans le déployer avec la commande helm template :

helm template .\

....

spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http

...

Vous pouvez mettre à jour votre Chart déployé avec la commande helm upgrade :

helm list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
myfirst-chart test 1 2022-01-19 13:48:29.437268209 +0000 UTC deployed myfirst-chart-0.1.0 1.16.0

helm upgrade myfirst-chart ./
helm history
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Wed Jan 19 13:48:29 2022 superseded myfirst-chart-0.1.0 1.16.0 Install complete
2 Wed Jan 19 14:06:53 2022 deployed myfirst-chart-0.1.0 1.16.0 Upgrade complete

Une valeur peut être définie par défaut dans le template :

image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

Lors de l'installation du chart, s'il ne trouve pas de valeur définie dans le fichier de valeur pour la valeur image.tag alors il prend la valeur définie dans le fichier de description du chart AppVersion. Ce qui est le cas ici.

helm template ./ |grep image
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
image: busybox

Factorisation des valeurs

Pour éviter de répéter du code, il existe un premier moyen qui est d'affecter une valeur à une variable en utilisant l'affectation $var := val. L'appel de cette variable se fera via le caractère $ placé devant le nom de la variable. Un exemple :

{{- $fullName := include "myfirst-chart.fullname" . -}}
name: {{ $fullName }}

Un autre moyen est d'utiliser le fichier nommé _helpers.tpl qui contient des valeurs construites à partir d'autres et qui peuvent être utilisées ensuite dans tous les templates.

Elles sont appelées cette fois via un include :

  selector:
{{- include "myfirst-chart.selectorLabels" . | nindent 4 }}

On retrouve bien sa définition dans le fichier _helpers.tpl

{{- define "myfirst-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myfirst-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{ Release.Name }} est le nom de la release qui sera instanciée.

  selector:
app.kubernetes.io/instance: myfirst-chart
app.kubernetes.io/name: myfirst-chart

Les Conditions

Comme dans tous les langages on utilise ici le bon vieux {{ if }} :

{{- if .Values.serviceAccount.create }}
{{- default (include "myfirst-chart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}

Un test est négatif s'il retourne :

  • un booléen égale à false
  • une valeur égale à zero
  • une chaine vide
  • une valeur de type empty ou null
  • une collection vide

Donc les autres valeurs sont positives.

Les scopes

Pour simplifier l'écriture on peut changer la portée d'une variable avec l'opérateur with. Prenons un exemple pour comprendre son fonctionnement. Par exemple on a la variable favorite définie avec :

favorite:
drink: coffee
food: pizza

Pour éviter d'écrire à chaque fois .Values.favorite on utilise with :

  {{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}

Sinon nous aurions écrit :

  drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}

Les Boucles

On utilise range et non pas for ou each. Prenons l'exemple suivant. On a un fichier de valeur :

pizzaToppings:
- mushrooms
- cheese
- peppers
- onions

Si on doit boucler sur les valeurs du tableau pizzaToppings on écrit :

  toppings: |-
{{- range .Values.pizzaToppings }}
- {{ . | title | quote }}
{{- end }}

Vous remarquez qu'ici on ne définit pas de nom de boucle, mais on utilise le . !

Ce qui sera converti en :

  toppings: |-
- "Mushrooms"
- "Cheese"
- "Peppers"
- "Onions"

Valider vos templates

Comme beaucoup de langages Helm possède son propre linter. Il s'agit de la commande helm lint :

helm lint
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Chercher de l'inspiration

Je vous conseille de lire des charts de la communauté. Pour cela il suffit de télécharger les charts avec la commande Helm pull :

helm search repo sonarqube
NAME CHART VERSION APP VERSION DESCRIPTION
bitnami/sonarqube 0.2.3 9.2.4 SonarQube is an open source quality management ...

helm pull bitnami/sonarqube --untar
cd sonarqube

A bientôt !