Mise en place d'un mécanisme de déploiement continue

Disclaimer

L’article qui suit fait appel à des concepts relativement avancés sur le fonctionnement de Kubernetes. Une connaissance raisonnable de l’outil est nécessaire afin de bien appréhender les concepts abordés.

Il s’agit également de la suite d’un premier article. N’hésitez pas à aller à l’adresse suivante. avant de commencer.

Introduction

Dans un article précédent, j’abordais une technique permettant d’éteindre des environnements de développement notamment en supprimant les nœuds d’un cluster Kubernetes lorsqu’ils ne sont plus nécessaires.

Toutefois, le besoin de démarrage d’environnement de développement n’est pas le seul contexte dans lequel on peut appliquer cette technique. On peut le retrouver notamment dans les cas suivants :

  • Construction d’artefacts applicatifs (images de conteneurs, librairie ou application Java, pages web statiques, etc).
  • Lancement d’environnements éphémères afin de lancer une série de tests
  • Lancement de traitements longue durée (batchs)

Cette liste n’est bien sûr pas exhaustive !

Dans tous les cas, le système lance une série de traitements durant un laps de temps avant de rendre les ressources disponibles. Elles peuvent alors être réaffectées à d’autres usages.

Si pendant un temps donné, l’activité n’est plus significative, on peut même procéder à une suppression de la puissance souscrite afin de réaliser des économies. En poursuivant le raisonnement plus loin, il est même possible d’imaginer de ne démarrer les machines que lorsque le besoin s’en fait sentir.

Le but de cet article est de mettre en œuvre un gestionnaire de construction et de voir comment l’intégrer avec Gitlab. La suite des instructions se fera sur l’instance Gitlab publique (gitlab.com). Néanmoins, les différentes indications sont tout à fait transposables dans le cas d’une instance dédiée.

Gestion de la construction des livrables

Présentation de l’architecture initiale

Pour la suite de l’article, un agent Gitlab Runner va être ajouté au système afin de prendre en charge la construction des livrables.

Dans l’article précédent, deux groupes de machines ont été créés :

  • Un groupe en charge de l’hébergement de production (ou par défaut)
  • L’hébergement d’environnement de développement

Afin de préserver au maximum l’environnement de production, le groupe de machine chargé de l’environnement de développement va être utilisé pour la partie construction.

Ci-dessous un schéma résumant ces différents éléments :

État du système initiale

Au démarrage des environnements, l’agent Gitlab Runner démarre dans son espace de nom. À noter que ce dernier ne consomme que très peu de ressources, il peut donc démarrer sur le même nœud que l’environnement de développement.

En revanche, la partie construction est relativement coûteuse en ressources et déclenchera très certainement la création de nœuds de traitement dans Kubernetes.

Installation de l’agent Gitlab Runner dans Kubernetes

Rattachement de l’agent à Gitlab

L’agent Gitlab Runner s’installe sous forme de chart Helm. Afin de l’associer à une instance Gitlab, il est nécessaire de spécifier deux informations :

  • L’URL de l’instance Gitlab
  • Le token d’accès de l’instance Gitlab

En plus de ces deux paramètres, le chart Helm va se charger de créer le compte de service ainsi que l’attribution des droits nécessaires à la création des pods de build.

La création du token se définit au niveau du projet dans le contexte du Projet dans l’écran Settings →Runners → New project runner. Dans cet écran, entrez un nom de Runner (ex : kube-cluster) et cliquez sur le bouton Create runner. Attention de ne pas perdre le token du Runner : il n’est pas possible de le récupérer ultérieurement.

Sur l’écran suivant, conservez bien la valeur du Token. Il sera nécessaire de l’indiquer à Helm dans le champ runnerToken couplé au champ gitlabUrl pour indiquer l’url de l’instance Gitlab.

Ci-dessous la déclaration reprenant ces indications :

gitlabUrl: https://gitlab.com/
runnerToken: glrt-xxxx

rbac:
 create: true

serviceAccount:
 create: true

Sélection des machines, tolérances et affectation de ressources

En plus de cette configuration, il est nécessaire d'indiquer aux pods en charge des constructions d’utiliser les machines de développement et de tolérer les taints évoqués dans l’article précédent.

Ci-dessous les indications correspondantes à injecter dans les pods :

nodeSelector:
 env: dev
tolerations:
 - key: env
   value: dev
   effect: NoSchedule

Ces machines sont basées sur le modèle Discovery d2-8. Elles sont constituées de 2 CPU accompagnées de 8 Go de mémoire. De fait, la quantité de ressources n’est pas infinie et il est important d’indiquer la quantité de CPU et mémoire qui doit être réservée aux constructions réalisées par Gitlab.

À noter que cet outil utilise trois types de traitement :

  • Le job en charge de la construction en lui-même
  • Les conteneurs chargés de lancer les services supplémentaires : base de données PostgreSQL, MongoDB, gestionnaire de messages, etc.
  • Le conteneur helper chargé de faire les “petites manipulations” : communication avec Gitlab, récupération du code source, copie des binaires, etc.

Pour la suite, l’essentiel des ressources seront affectées au conteneur principal tandis que les deux autres conteneurs auront une quantité de ressources plus petites. Afin de laisser le plus de souplesse possible, l’affectation sera dirigée essentiellement par la mémoire. La CPU sera affectée de manière plus “lâche” : une valeur de base sera affectée avec un maximum assez important de manière à laisser de la souplesse dans le lancement de la construction.

Ci-dessous un exemple de définition compatible avec l’utilisation de machine de type Discovery de chez OVH (d2-8 par exemple) :

--- <br>gitlabUrl: https://gitlab.com/
runnerToken: glrt-xxxx

rbac:
 create: true

serviceAccount:
 create: true

runners:
 config: |-
   [[runners]]
     [runners.kubernetes]

       image = "ubuntu:22.04"
       cpu_request = "0.2"
       cpu_limit = "3"
       memory_request = "5Gi"
       memory_limit = "7Gi"
       privileged = true

       # service containers
       service_cpu_request = "0.1"
       service_cpu_limit = "1"
       service_memory_request = "512Mi"
       service_memory_limit = "768Mi"

       # helper container
       helper_cpu_request = "0.05"
       helper_cpu_limit = "0.1"
       helper_memory_request = "50Mi"
       helper_memory_limit = "150Mi"
       [runners.kubernetes.node_selector]
         env = "gitlab"
       [runners.kubernetes.node_tolerations]
         "env=gitlab" = "NoSchedule"

Sauvegardez ce fichier sous le nom de fichier gitlab-runner-values.yaml.

Lancement de l’installation du Runner Gitlab

Tout est prêt. Il faut maintenant installer le runner Gitlab à l’aide de Helm. N’hésitez pas à consulter l’article suivant afin de procéder à son installation.

Ajoutez tout d’abord la source des charts Helm de Gitlab :

helm repo add gitlab https://charts.gitlab.io

Lancez ensuite l’installation du runner Gitlab :

helm upgrade --install gitlab-runner gitlab/gitlab-runner \<br>             --namespace gitlab --create-namespace \<br>             --values gitlab-runner-values.yaml

Ce dernier renvoie un ensemble d’indications sur l’installation. Vérifiez que le pod associé au runner démarre correctement :

kubectl -n gitlab get pods

Ci-dessous le résultat attendu en cas de succès :

NAME                             READY   STATUS    RESTARTS   AGE
gitlab-runner-69db6f4f75-vsvzg   1/1     Running   0          6m

Vérifiez également que le runner apparaît bien dans la liste des runners disponibles pour le projet.

Runner Gitlab disponible dans l'interface une fois correctement démarré.

Profitez-en pour désactiver l’utilisation des runners par défaut depuis l’écran Settings → CI/CD → Runners (bouton Disable group runners et Disable Instance runners for this project). De cette façon, seul le runner associé au projet pourra lancer des travaux de construction.

Tout est prêt, il est temps de tester le système de build !

Création du pipeline de construction

Constitution du livrable de test

Afin d’illustrer un cas concret, une application va être créée à l’aide du CI de Gitlab. Pour faire au plus simple, cette dernière sera constituée d’une image de conteneur Nginx dans lequel sera déposé un fichier texte contenant la version courte du commit courant.

Le plus simple pour arriver à cette solution est de passer par un paramètre renseigné au moment de la construction de l’image. Ce dernier alimentera un fichier commit.txt à l’aide d’une étape RUN.

Ci-dessous le contenu du fichier Dockerfile permettant de remplir l’ensemble de ces indications :

FROM docker.io/library/nginx:1.28

ARG COMMIT
ENV COMMIT=${COMMIT}
RUN echo ${COMMIT} > /usr/share/nginx/html/commit.txt

Sauvegardez ce fichier puis lancez la construction de cette image à l’aide de Docker ou Podman :

# remplacez la commande docker par podman le cas échéant
docker build -t local.io/test --build-arg COMMIT=test .

Lancez maintenant l’image en écoutant sur le port 8080 :

docker run --publish 8080:80 --name test --detach local.io/test

Interrogez l’entrée http://localhost:8080/commit.txt à l’aide de l’instruction curl suivante :

curl http://localhost:8080/commit.txt

La commande renvoie normalement la valeur de l’argument COMMIT utilisé lors de la construction (test par exemple).

Construction de l’image

Les instructions de construction de l’image sont prêtes. Reste à ordonnancer sa construction à l’aide du CI de Gitlab et d’injecter le commit. Kaniko est plutôt bien indiqué pour remplir cette fonction du fait qu’il ne réclame que très peu de privilèges.

Ce dernier est disponible sous forme d’image (gcr.io/kaniko-project/executor:debug) et réclame les paramètres suivants :

  • L’emplacement du fichier Dockerfile (option -f)
  • L’emplacement du répertoire de travail (option -c)
  • Le passage de paramètre COMMIT (option --build-arg)
  • Le nom de l’image (option -d)

Pour la suite, l’image sera référencée sous deux tags :

  • Le tag courant (variable $IMAGE_TAG)
  • Le tag correspondant au commit courant (variable $CI_COMMIT_SHORT_SHA)

Le nom de l’image sera composé de la variable $CI_REGISTRY_IMAGE suivie de ‘/app’.

Il est également important d’alimenter le fichier /kaniko/.docker/config.json avec le contenu des variables suivantes sous forme de fichier JSON :

  • $CI_REGISTRY : adresse du registre de Gitlab
  • $CI_REGISTRY_USER : utilisateur permettant d’accéder au registre d’image
  • $CI_REGISTRY_PASSWORD : token permettant d’accéder au registre d’image

Ci-dessous un exemple de déclaration permettant de remplir l’ensemble de ces indications :

build-app:
 stage: build
 variables:
   IMAGE: $CI_REGISTRY_IMAGE/app
 image:
   name: gcr.io/kaniko-project/executor:debug
   entrypoint: [ "" ]
 before_script:
   - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
   - IMAGE_TAG=`echo $CI_COMMIT_REF_NAME | sed 's/[\/+]/-/g'`
   - echo $IMAGE
 script:
   - /kaniko/executor -f $CI_PROJECT_DIR/Dockerfile 
       -c $CI_PROJECT_DIR --build-arg COMMIT=$CI_COMMIT_SHORT_SHA 
       -d $IMAGE:$IMAGE_TAG -d $IMAGE:$CI_COMMIT_SHORT_SHA

Sauvegardez cette définition dans le fichier .gitlab-ci.yml à la racine du projet (attention de bien conserver le “.” au début du nom de fichier) et commitez-le avec le reste du code source.

Lors de la synchronisation du code source avec Git, Gitlab va procéder à la construction de l’image.

Exemple de pipeline permettant de construire l'image de test.

N’hésitez pas à scruter l’état du job de construction afin de vérifier que tout fonctionne correctement.

Principe de création des machines au fur et à mesure des besoins de traitement.

Cette animation illustre comment réagit le cluster Kubernetes (notamment lorsqu’il n’a pas assez de puissance de disponible) lorsque le projet construit une image.

Déploiement de l’application dans Kubernetes

L’image est prête. Reste à la déployer dans Kubernetes. Pour la suite, ce travail sera fait à l’aide de l’outil Helm.

Première étape : créer un chart Helm dans le répertoire deploy. Pour cela, lancez les instructions suivantes :

mkdir -p deploy
helm create app deploy/app

Le chart est prêt, reste à le déployer. Cette opération va réclamer les instructions suivantes :

  • Le mot clé upgrade (couplé à l’instruction --install afin de gérer l’idempotence)
  • Le nom de la release du Chart suivi de l’emplacement du chart (deploy/app)
  • Le mot clé --wait afin d’attendre que l’application soit correctement déployée
  • Le nom de l’espace de nom à l’aide de l’option --namespace
  • L’instruction --create-namespace là-aussi afin de gérer l’idempotence

En plus de ces informations sur les caractéristiques du déploiement, il est important de renseigner l’emplacement de l’image (variable image.repository) ainsi que son tag associé (variable image.tag).

Ci-dessous l’instruction complète permettant de lancer cette opération localement :

helm upgrade --install app deploy/app --wait \
       --namespace env-develop \
       --create-namespace \
       --set image.repository=$CI_REGISTRY_IMAGE/app \
       --set image.tag=main # À adapter en fonction du nom de la branche

Vérifiez ensuite que le déploiement se passe bien à l’aide de l’instruction suivante :

kubectl -n env-develop get pods

Si tout se passe bien le système doit renvoyer la sortie suivante :

NAME                  READY   STATUS    RESTARTS   AGE
app-6bf5946d5-9cphj   1/1     Running   0          27m

Intégration du déploiement dans Gitlab

L’application est correctement déployée. Reste à intégrer cette opération dans le pipeline de déploiement de Gitlab. Pour la suite, l’image docker.io/alpine/helm:3.18 sera utilisée afin de disposer de l’utilitaire Helm.

Autre aspect : le déploiement de l’environnement sera rattaché à la phase deploy afin de garantir que la construction de l’image soit bien lancée avant.

Ci-dessous un exemple de déclaration de déploiement dans l’espace de nom env-develop et répondant à l’ensemble des indications précédemment évoquées :

dev-deployment:
  image:
    name: docker.io/alpine/helm:3.18
    entrypoint: [ "" ]
  variables:
    K8S_NAMESPACE: env-develop
  stage: deploy
  interruptible: false
  script:
    - helm upgrade --install app deploy/app --wait
       --namespace ${K8S_NAMESPACE}
       --create-namespace
       --set image.repository=$CI_REGISTRY_IMAGE/app
       --set image.tag=$CI_COMMIT_SHORT_SHA

Ajoutez cette définition dans le fichier .gitlab-ci.yml et intégrez-le dans un commit Git afin d’être pris en charge par Gitlab.

Vérifiez l’état du pipeline. Normalement ce dernier devrait tomber en erreur sur l’étape dev-deployment avec le message suivant :

…
$ helm upgrade --install --wait --namespace ${K8S_NAMESPACE} --create-namespace app deploy/app
Error: query: failed to query with labels: secrets is forbidden: User "system:serviceaccount:gitlab:default" cannot list resource "secrets" in API group "" in the namespace "env-develop"
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: command terminated with exit code 1

Ici, le système indique que le ServiceAccount default de l’espace de nom gitlab n’a pas les droits de consulter l’état des objets Secrets dans l’espace de nom env-develop (ce qui est bien sûr attedu !).

Correction problème de droits

Disclaimer : les configurations utilisées ici sont données à titre indicatif. Elles n’ont pas vocation à être reporté en l’état sans adaptation. N’hésitez pas à prendre contact avec un administrateur Kubernetes ou Sécurité afin de valider que ces indications respectent les normes de sécurité en vigueur.

Afin de simplifier au maximum les choses, le compte de service du runner Gitlab va être repris et affecté aux jobs de déploiement.

Cette réaffectation se fait à l’aide du champ service_account associés au champ runner → config du chart Helm.

Ci-dessous l’extrait de configuration correspondant :

runners:
  config: |-
    [[runners]]
      [runners.kubernetes]
        service_account = "gitlab-runner"

En plus de cette association, il est nécessaire d’affecter des droits supplémentaires au compte de service de Gitlab permettant d’administrer le cluster. Cette fois-ci, cette affectation se fait au niveau du champ rbac et notamment avec les champs suivants :

  • Champ clusterWideAccess : affectation de droits au niveau global du cluster
  • Champ rules : liste de droits à affecter au compte de service

Ci-dessous un exemple de configuration possible à utiliser avec le chart Helm :

rbac:
  create: true
  clusterWideAccess: true
  rules:
    - apiGroups: ['*']
      resources: ['*']
      verbs: ['*']

Ajoutez ces indications dans le fichier gitlab-runner-values.yaml puis lancez la mise à jour du runner Gitlab :

helm upgrade --install gitlab-runner gitlab/gitlab-runner \<br>             --namespace gitlab --create-namespace \<br>             --values gitlab-runner-values.yaml

Vérifiez que le pod associé au Runner se lance correctement à l’aide de l’instruction kubectl -n gitlab get pods.

Relancez ensuite le job de déploiement en erreur. Cette fois-ci, le système déploie correctement le chart de l’application dans l’espace de nom env-develop.

Pipeline de construction et de déploiement de l'application.

Déclenchement du CI sur condition

Origine du problème

Le mécanisme de déploiement automatique est en place. Toutefois, aucun test n’est fait pour savoir si on doit réellement déployer ou non. Ainsi la simple création d’une branche procédera au déploiement sur l’environnement.

Dans le cas d’une personne seule, le problème n’a pas trop de conséquences. Toutefois, dès que ce système sera mis à disposition d’une équipe de développeurs, ce comportement devrait rapidement poser problème …

Déclenchement de traitement sur condition

Afin d’empêcher le mécanisme de build de se déclencher à tout bout de champ, des règles vont être introduites. Elles vont spécifier les conditions de déclenchement de nos travaux. Pour la suite, les règles seront les suivantes :

  • Déclenchement des différentes étapes lors de la création d’une Pull Request ou sur la branche develop
  • Déploiement de l’application sur l’espace de nom env-develop lors de modification sur la branche develop (suite à l’inclusion d’une Pull Request)

Dans Gitlab, ce mécanisme se définit à l’aide du champ rules et prend la forme d’un tableau référençant l’ensemble des conditions de déclenchement.

Ci-dessous un exemple de déclaration utilisable pour répondre aux contraintes évoquées précédemment :

rules:
 - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
 - if: '$CI_PIPELINE_SOURCE != "schedule" && $CI_COMMIT_BRANCH == "develop"'

Reste maintenant à injecter ses conditions ad-hoc dans les étapes existantes. Ci-dessous un exemple d’implémentation faisant appel à la notion d’extension (mot clé extends) :

.dev-trigger:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "develop"'

build-app:
  extends: .dev-trigger
  stage: build
  variables:
    IMAGE: $CI_REGISTRY_IMAGE/app
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [ "" ]
  before_script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - IMAGE_TAG=`echo $CI_COMMIT_REF_NAME | sed 's/[\/+]/-/g'`
    - echo $IMAGE
  script:
    - /kaniko/executor -f $CI_PROJECT_DIR/Dockerfile -c $CI_PROJECT_DIR --build-arg COMMIT=$CI_COMMIT_SHORT_SHA -d $IMAGE:$IMAGE_TAG -d $IMAGE:$CI_COMMIT_SHORT_SHA

dev-deployment:
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
  variables:
    K8S_NAMESPACE: env-develop
  image:
    name: docker.io/alpine/helm:3.17
    entrypoint: [ "" ]
  stage: deploy
  script:
    - helm upgrade --install --wait --namespace ${K8S_NAMESPACE} --create-namespace app deploy/app
          --set image.repository=$CI_REGISTRY_IMAGE/app --set image.tag=$CI_COMMIT_SHORT_SHA

On remarque que le déclenchement du déploiement utilise un cas particulier : il n’est déclenché que lors de la construction de la branche develop.

Dorénavant le système ne se déclenche plus que lors de la création de Pull Request et lors de l’intégration sur la branche develop pour déploiement continue.

Quelques réglages supplémentaires

En l’état, le mécanisme fonctionne très bien. Il peut arriver toutefois que lors de la première construction de la journée, le système mette un peu de temps à attribuer des machines au cluster.

Autre aspect, par défaut les jobs de Gitlab ne s’interrompt pas. Or, dans le cas de la préparation d’une PR, il peut arriver que les modifications s’enchaînent ne laissant pas le temps au système de finir sa construction avant d’en lancer d’autres en parallèle.

Dans ce cas là, le mieux est de pouvoir interrompre ces travaux qui n’ont de toute façon plus d’utilité.

Ci-dessous les déclarations correspondantes rattachées à la clé default :

default:
 # Interrupt job if new modifications on current branch/MR/...
 interruptible: true
 retry:
   max: 2
   when:  ["runner_system_failure", "stuck_or_timeout_failure"]

Conclusion

L’approche détaillée dans cet article permet de tirer pleinement parti de la flexibilité offerte par Kubernetes pour orchestrer dynamiquement la construction et le déploiement d’applications via GitLab CI. En exploitant un cluster capable de s’auto-dimensionner et en intégrant un GitLab Runner dédié, on optimise l’utilisation des ressources tout en garantissant l’isolation des environnements critiques.

Au-delà du simple gain de performance ou de coût, cette méthode permet aussi de bâtir un pipeline CI/CD robuste, reproductible, et prêt à évoluer vers une industrialisation plus poussée. Le recours à des outils comme Kaniko pour la construction d’images et Helm pour le déploiement renforce encore l’automatisation et la portabilité des environnements.

Il conviendra toutefois de veiller à l’alignement des droits et des pratiques de sécurité dans Kubernetes, notamment en production. Cette architecture constitue une excellente base pour explorer d’autres scénarios avancés, comme les environnements à la demande par branche, ou l’analyse dynamique des performances en post-déploiement.

Vous l’aurez compris, tout n’a pas encore été exploré. Un prochain article s’attardera sur la gestion des environnements éphémères 😅.