Kubernetes - Ecrire son premier chart Helm
Publié le : 19 janvier 2022 | Mis à jour le : 27 juin 2023Pour ceux qui ne connaissant pas 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 de 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 autretrunc
: coupe une chaine à partir du n caractèretrimSuffix
: retranche un suffixe d’une chainequote
: 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înetitle
: 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’affecteur
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 !