L'une des questions essentielles qu'on se pose quand on conçoit une nouvelle application concerne l'infrastructure où sera hébergée cette application.
Et le choix évident aujourd'hui pour déployer une application web est un cluster Kubernetes.
Kubernetes en quelques mots
Définir Kubernetes en quelques mots peut se résumer à :
Kubernetes est un système pour manager et orchestrer des applications conteneurisées. Il permet d'automatiser le déploiement et la mise à l'échelle d'une application.
Un cluster Kubernetes repose sur un ensemble de "noeuds" qui concrètement sont des machines virtuelles ou physiques qui exécutent les conteneurs.
Les interactions avec Kubernetes se font via des API, ce qui facilite l'intégration dans une chaîne de CI/CD par exemple. En quelques fichiers yaml on peut décrire le déploiement d'une application de A à Z. Il suffit ensuite de fournir ce fichier à Kubernetes via la CLI (kubectl).
Voici par exemple comment déployer rapidement un serveur nginx sur un cluster Kubernetes à partir d'un Yaml fourni dans l'entrée standard d'un shell.
<pre><code>kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: frontend
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
EOF</code></pre>
Déployer sur plusieurs environnements
La majorité du temps on va déployer notre application de la même façon d'un environnement à l'autre. Donc notre première idée est de se dire qu'on va pouvoir copier/coller les fichiers d'un environnement à l'autre (avouons-le notre premier réflexe de développeur est bien souvent le copier/coller !).
En analysant un peu les fichiers, on se rend vite compte qu'il y a un certain nombre de paramètres qui vont changer d'un environnement à l'autre. Voici une liste d'exemples de paramètres qui peuvent changer d'un environnement à l'autre:
- La version de l'image Docker à déployer
- L' URL d'accès à l'application
- Le nombre d'instances en parallèle de l'application
- Les informations de connexion à la BDD
- Les URL d'accès à des API externes à notre application
- ...
Au final gagner du temps sur le développement en dupliquant les fichiers de déploiement d'un environnement à l'autre ne représente pas un gros gain. Car on va devoir maintenir autant de versions des fichiers de déploiement qu'on a d'environnement pour gérer les différents paramètres.
Pour autant, changer quelques paramètres dans des fichiers ne représente pas un gros effort.
Avec un script shell et à grands coups de sed, awk ou de jq, on peut faire ça. Mais un script Shell ça ne fait pas rêver grand monde, on en fait de moins en moins et c'est donc plus complexe à écrire. Ce n'est pas très robuste car basé sur des expressions régulières ou placeholder. Bref plein de bonnes raisons de ne pas aller vers cette solution...
Après quelques recherches, on se rend compte qu'il existe des outils plus adaptés et plus user-friendly comme Kustomize ou Helm qui est le but de cet article.
Helm
Le projet Helm a démarré en 2015 et a été présenté au grand public lors du KubeCon de 2015. Il a été certifié en tant que projet de la Cloud Native Computing Foundation (CNCF) en 2020.
Helm est un outil permettant de gérer des applications Kubernetes sous forme de packages appelés chart. Il est basé sur des fichiers templates dans lesquels on va pouvoir intégrer les paramètres spécifiques à nos environnements.
Avec Helm il est possible de versionner nos charts de déploiements, de les compresser et les envoyer vers un registry à l'image d'une image Docker ou d'une librairie.
Les charts Helm peuvent également inclure des dépendances vers d'autres charts. Par exemple, si votre application fonctionne avec un cache Redis vous pouvez facilement inclure la dépendance à votre chart. Ainsi au déploiement de votre chart, Helm déploiera le cache Redis nécessaire à votre application.
De par ses caractéristiques Helm se rapproche des gestionnaires de packages qu'on peut connaître sur Linux (yum, apt) ou Windows (chocolatey, homebrew).
Helm CLI
Création de chart
Une fois installée, la CLI (Command-Line Interface) Helm offre un ensemble de commande qui permettent de manipuler des charts. Par exemple, pour créer un chart il faut taper la commande <span class="css-span">helm create younup-chart</span>. La commande va créer la structure du chart avec tous les fichiers requis.
On y retrouve les éléments obligatoires suivants:
- <span class="css-span">Chart.yaml</span>: Le fichier contenant les métadonnées du chart (nom, description, type, version...)
- <span class="css-span">charts/</span>: Le répertoire qui contient les éventuelles dépendances vers d'autres charts
- <span class="css-span">templates/</span>: Le répertoire qui contient les templates qui combinées aux valeurs du fichier values.yaml produiront des manifest Kubernetes
- <span class="css-span">values.yaml</span>: La configuration par défaut du chart.
Installation d'un package Helm
Par exemple, pour déployer le chart Helm de la dernière version de Drupal sur son cluster Kubernetes il suffit de lancer la commande <span class="css-span">helm install my-release oci://registry-1.docker.io/bitnamicharts/drupal</span>
Templating
Helm utilise un système de templating pour remplacer les placeholders des templates par sa valeur dans le fichier de valeurs spécifiques à l'environnement dans lequel on souhaite déployer.
Pour illustrer le système de templating, ce projet Github permet de déployer un Chart Helm qui instancie une image Docker qui affiche un simple message. Ce message valorisé par défaut avec "Hello World !" peut-être surchargé par une variable d'environnement injectée dans le conteneur Docker.
Voilà le contenu du fichier template deployment.yaml permettant de générer un manifest Kubernetes pour déployer l'application :
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hello-kubernetes.name" . }}
labels:
{{- include "hello-kubernetes.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.deployment.replicaCount }}
selector:
matchLabels:
{{- include "hello-kubernetes.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "hello-kubernetes.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "hello-kubernetes.name" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.deployment.container.image.repository }}:{{ .Values.deployment.container.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.deployment.container.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.deployment.container.port }}
protocol: TCP
env:
{{- if .Values.message }}
- name: MESSAGE
value: "{{ .Values.message }}"
{{- end }}</code></pre>
Et le fichier values.yaml avec les valeurs spécifiques à notre environnement
<pre><code># Provide a custom message
message: ""
service:
type: LoadBalancer
port: 80
deployment:
replicaCount: 2
container:
image:
repository: "paulbouwer/hello-kubernetes"
tag: "" # uses chart appVersion if not provided
pullPolicy: IfNotPresent
port: 8080</code></pre>
Le fichier deployment.yaml contient un ensemble de directives (par exemple <span class="css-span">{{ .Values.message }}</span>) qui seront suivies par le moteur de template pour produire le manifest Kubernetes à partir des valeurs spécifiques qu'on retrouve dans le fichier values.yaml.
La directive <span class="css-span">{{- if .Values.message }</span> permet d'ajouter une variable <span class="css-span">MESSAGE</span> aux variables d'environnement du conteneur si la variable <span class="css-span">message</span> est bien renseignée dans le fichier <span class="css-span">values.yaml</span>. Par défaut, en l'absence de valeur pour la variable, le message "Hello World !" est affiché par le conteneur.
Pour déployer le chart sur notre cluster sans surcharger la variable <span class="css-span">MESSAGE</span> il suffit de lancer la commande <span class="css-span">helm upgrade -i hello-younup ./hello-kubernetes</span> (l'option -i permet de faire une installation si la release hello-younup n'a pas encore été déployée sur le cluster).
Si on part du principe que la variable <span class="css-span">message</span> sert à afficher le nom de l'environnement qui a été déployé, il nous faut surcharger la variable par environnement.
Pour cela, rien de plus simple il nous faut créer autant de copies du fichier <span class="css-span">values.yaml</span> qu'on a d'environnements et surcharger les variables nécessaires avec la valeur spécifique de l'environnement. Donc, dans notre exemple on va simplement mettre le nom de l'environnement dans la variable message. Les autres valeurs ne changeant pas en fonction de l'environnement, elles peuvent être centralisées et rester dans le fichier <span class="css-span">values.yaml</span>.
Pour déployer sur chaque environnement, il n'y a plus qu'à préciser le fichier de variables de l'environnement concerné avec l'option -f et le tour est joué : <span class="css-span">helm upgrade -i hello-younup ./hello-kubernetes -f hello-kubernetes/environment-config/values-recette.yaml</span>.
Contenu du fichier de configuration de recette <span class="css-span">hello-kubernetes/environment-config/values-recette.yaml</span> :
<pre><code>message: "RECETTE"</code></pre>
History & Rollback
A chaque fois qu'on installe ou qu'on met à jour une release, Helm met également à jour l'historique de déploiement de la release.
La commande <span class="css-span">helm history [release]</span> nous permet de consulter l'historique de déploiement de notre release et de voir ce qui a été fait à chaque révision (dans une limite pas défaut de 256).
Quand nous déployons une release en production et qu'on s'aperçoit qu'une anomalie bloquante y a été embarquée et n'est pas corrigeable rapidement, grâce à cet historique nous allons pouvoir revenir en une seule ligne de commande à la version précédente.
Avec <span class="css-span">helm rollback [release] [revision]</span> nous pouvons revenir à la révision souhaitée. Si on ne précise pas le numéro de version, on revient à la release précédente par défaut.
Synthèse
Ainsi, avec Helm nous avons répondu à la majorité des problèmes exposés au début de cet article.
Nous avons donc une solution qui nous permet de déployer tous nos environnements avec la même procédure d'installation (et donc de pouvoir tester la procédure sur d'autres environnements) et une configuration applicative externalisée.
Retour d'expérience
Cela fait quelques années maintenant que j'utilise Helm, et un tel outil est vraiment indispensable dans un environnement de run d'application sur un cluster Kubernetes. Tant que l'on fait des choses très simples avec, comme de la configuration applicative par environnement, l'outil reste très simple à utiliser. Quand on commence à faire des choses plus avancées et que l'on commence à faire trop d'algorithmie (instruction if/else par exemple) dans nos templates, ils deviennent très complexes à développer, à comprendre et à maintenir. C'est le bémol principal que je donnerai à Helm.