Introduction
Dans le monde du développement logiciel, la réussite d’un projet repose souvent sur des processus bien orchestrés, de la conception initiale à la livraison finale. Au cœur de cette dynamique, des pratiques telles que l’utilisation de l’approche MVP (Minimum Viable Product) et les pratiques d’intégration et de déploiement continus (CI/DI) émergent comme des éléments clés pour accroître l’efficacité et la cohérence des livraisons logicielles.
Quel est le lien entre MVP et CI/CD ?
L’approche MVP privilégie l’adaptation plutôt que la prédiction, se traduisant concrètement par de petites mises à jour déployées régulièrement en production. Cependant, cette approche incrémentale ou agile peut engendrer une certaine bureaucratie.
C’est à ce stade que les pratiques d’intégration et de déploiement continus (CI/CD) ne sont plus une simple commodité, mais une nécessité. Elles permettent d’automatiser l’ensemble des tâches administratives, libérant ainsi les développeurs des aspects administratifs afin qu’ils puissent se consacrer exclusivement au développement de nouvelles fonctionnalités.
Dans cet article, nous allons partir de 6 exigences fonctionnelles afin de définir un workflow efficace visant à faciliter la production de livrable, son versionnement, et son déploiement dans divers environnements.
En plongeant dans ces pratiques, nous découvrirons comment elles contribuent à améliorer la flexibilité, la qualité et la cohérence des produits logiciels tout au long de leur cycle de vie. Bienvenue dans un voyage au cœur de l’optimisation du développement logiciel, où l’innovation et l’efficacité se rencontrent pour créer des solutions logicielles de premier plan.
Prérequis
- Comprendre docker et la terminologie utilisée
- Automatiser le Versionnement avec Semantic Release
- MVP et l’Amélioration Continue : Un Duo Gagnant pour Votre Projet de Startup
Use case
– [exigence 1] Le système doit favoriser l’adaptation plutôt que la prédiction, conformément à la méthodologie MVP (Minimum Viable Product). Il est impératif dés lors de mettre en place un système d’intégration et de déploiement continus (CI/CD) pour faciliter la libération fréquente de nouvelles versions du logiciel. L’objectif principal de ce système est de réduire le délai de mise sur le marché (time to market), assurant ainsi une réactivité optimale face aux besoins changeants des utilisateurs.
– [exigence 2] Le système doit intégrer Semantic-release pour automatiser le versionnement du code source et des livrables. Il est impératif que chaque version du code source génère automatiquement un livrable associé portant la même version, assurant ainsi une synchronisation précise et une cohérence stricte entre le logiciel en développement et sa version distribuable.
– [exigence 3] Le logiciel doit être distribué sous forme d’image Docker pour faciliter le processus de déploiement. L’emploi d’une image Docker doit assurer une encapsulation complète de l’application ainsi que de ses dépendances, garantissant ainsi une portabilité optimale et une facilité de mise en œuvre sur divers environnements.
– [exigence 4] Le logiciel doit être déployé sur trois environnements distincts pour répondre aux besoins spécifiques de chaque phase du cycle de vie logiciel. Ces environnements comprennent un environnement de développement (dev) destiné aux tests internes, un environnement de recette (staging) dédié aux tests client, et un environnement de production (prod) destiné à la mise à disposition des utilisateurs finaux.
– [exigence 5] Le système doit garantir qu’une seule et même image Docker, ayant été soumise avec succès aux tests automatisés et aux validations manuelles par les développeurs, soit utilisée pour le déploiement en environnement de recette, puis en production si la recette du client est concluante. Cette approche vise à assurer que l’image Docker testée et validée dans un environnement donné puisse être promue (et non qu’une nouvelle image soit générée) de manière fiable vers des environnements plus critiques, garantissant ainsi la cohérence et la fiabilité du déploiement sur l’ensemble des phases du cycle de vie du logiciel.
– [Exigence 6] En cas de détection d’un bug au niveau de la production, le système mis en place doit prévoir la possibilité d’effectuer une mise à jour d’urgence (hotfix) de la production sans nécessiter le passage par les environnements de développement et de recette. Il doit également offrir la possibilité de revenir à une version antérieure (Rollback) dans le cas où la résolution du bug prendrait du temps.
Choisir un style de Workflow git
Pour entamer notre projet, la première étape consiste à sélectionner le workflow Git qui convient le mieux à nos besoins. Cette décision sera guidée par deux principaux critères : la facilité d’adoption par notre équipe de développeurs et la flexibilité nécessaire pour répondre à nos exigences fonctionnelles. La plupart des workflows Git existants reposent généralement sur le principe du Feature Branch Workflow, où chaque nouvelle fonctionnalité ou tâche est développée dans une branche dédiée. C’est le cas de gitlab workflow, github workflow et encore de gitflow workflow. Ce dernier introduit même des branches spécifiques pour les environnements d’execution du logiciel, comme develop, integration, master, etc.
Cependant, il est important de noter que ces approches peuvent parfois s’avérer complexes à gérer. Les demandes de fusion (merge request ou pull request) d’une branche à l’autre, la résolution de conflits, et les processus de validation peuvent ralentir le rythme de l’équipe de développement.
En réalité, cette organisation complexe est souvent nécessaire lorsque le logiciel en cours de développement est destiné à une installation individuelle chez chaque client, avec la possibilité d’ajouter de nouvelles fonctionnalités. Dans le cas des logiciels open source avec des contributeurs de divers horizons, qui ne sont pas forcément très familiers avec le projet, il est crucial de surveiller la qualité du code. Dans de tels cas, la gestion de plusieurs branches simultanées ainsi que les merge requests peuvent devenir nécessaire. Par exemple, une branche pourrait être dédiée à la maintenance de la sécurité sur une ancienne version, une autre branche à la préparation de la prochaine version, révisant l’ensemble de la logique du système, et encore une autre pour les versions beta, alpha, etc.
Cependant, généralement, pour un logiciel en tant que service (SaaS) dont nous maîtrisons entièrement le processus d’industrialisation, toute cette complexité devient souvent superflue. Dans ces situations, la gestion simplifiée d’une branche principale peut suffire, car nous avons une compréhension complète du cycle de développement, de la mise en production, et de l’évolution du logiciel.
Face à cette réalité, notre choix se tourne vers la trunk-based development. Cette approche se distingue par sa focalisation sur une seule branche principale, généralement nommée main ou trunk. La trunk-based encourage les développeurs à publier leurs fonctionnalités directement sur cette branche principale, mettant ainsi l’accent sur la livraison continue (CD) et la simplicité du processus, en opposition à une multiplicité de branches.
Cependant, il convient de noter que dans la trunk-based development, la création de nouvelles branches n’est pas exclue. Toutefois, au sein de ce système, cette pratique est généralement perçue comme l’exception plutôt que comme une composante intégrée à son mode de fonctionnement.
Trunk-based development
Dans un environnement de travail agile, en conformité avec les principes du MVP (Minimum Viable Product), il est vivement recommandé de publier de nouvelles versions aussi fréquemment que possible, même si elles sont de petite envergure. Cette pratique permet au business de recevoir rapidement des retours d’utilisateurs (feedback), facilitant ainsi la réorientation du développement (pivot) en fonction des besoins émergents.
Ce principe est en parfaite adéquation avec la trunk-based development, car grâce à des intégrations fréquentes, les risques de conflits majeurs sont réduits car les développeurs travaillent sur des parties du code plus petites et se synchronisent plus régulièrement.
En trunk-based development, nous sommes constamment à la recherche de stratégies pour diviser les grandes fonctionnalités qui pourraient rendre l’application instable pendant un certain temps en plusieurs petites fonctionnalités. Prenons l’exemple du remplacement d’une composante A du logiciel par une composante B. Pour atteindre cet objectif, nous pourrions envisager plusieurs étapes afin de minimiser l’impact sur la stabilité de l’application.
Une approche possible serait d’introduire progressivement la composante B tout en maintenant la stabilité globale du système. Par exemple, nous pourrions commencer par mettre en place une abstraction devant la composante A, permettant ainsi de créer un point d’interface standardisé. Ensuite, nous pourrions initier le processus de redirection d’une partie des connexions de cette composante vers la nouvelle composante B, sans compromettre la fonctionnalité globale de l’application.
Ce processus itératif peut être poursuivi de manière incrémentale, chaque étape étant soigneusement évaluée pour garantir la cohérence fonctionnelle et la stabilité de l’application. Progressivement, nous évoluerions vers le remplacement complet de la composante A par la composante B, tout en minimisant les interruptions et en permettant aux développeurs de travailler sur des portions spécifiques du changement sans perturber le fonctionnement général du système. Cette approche méthodique dans la mise en œuvre de changements majeurs garantit une transition fluide tout en maintenant une expérience utilisateur stable.
Intégration continue et déploiement continu en dev
Comment gérer le déploiement d’une version au niveau de l’environnement de développement (dev) ?
Pour répondre à l’exigence 1, nous avons opté pour l’utilisation de GitLab afin de mettre en place une pipeline d’intégration et de déploiement continu (CI/CD). Lorsqu’un développeur pousse (push) une nouvelle version du code source dans la branche principale (main), cela déclenche automatiquement notre pipeline d’intégration continue (CI). Cette pipeline comprend les phases de build, de test, et de publication de notre image Docker dans le registre. Suite à cela, une phase de livraison continue (CD) est enclenchée, déployant automatiquement la nouvelle version dans l’environnement de développement.
stages: - build - test - push - deploy variables: DOCKER_IMAGE_NAME: "registry.kata.ovh/kata/kata-api" DEV_ENV: "dev" build: stage: build image: docker:latest services: - docker:dind script: - docker build -t $DOCKER_IMAGE_NAME:lastest-$DEV_ENV . test: stage: test image: docker:latest services: - docker:dind script: - docker run $DOCKER_IMAGE_NAME:lastest-$DEV_ENV npm test push: stage: push image: docker:latest services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker push $DOCKER_IMAGE_NAME:lastest-$DEV_ENV deploy_dev: stage: deploy image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $DEV_PRIVATE_KEY script: - ssh -i $DEV_PRIVATE_KEY $DEV_USER@$DEV_SERVER_IP "cd $DEV_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME" environment: name: development url: https://dev.kata.ovh
Le code ci-dessus (gitlab-ci.yml) représente ce qui a été décrit à l’étape précédente. Cependant, il a pris la liberté de faire des choix par défaut afin de proposer une implémentation concrète de la solution retenue.
Comment dois-je procéder pour tester ce code ?
Comme vous l’avez remarqué, notre fichier gitlab-ci.yml fait référence à un projet Node.js. Nous allons donc créer un environnement afin de pouvoir exécuter notre pipeline correctement.
Réussir le stage Build
Écrivons le minimum de code pour faire passer notre premier stage, à savoir l’étape Build. Commençons par initialiser un projet Node.js de base.
# Création du dossier
mkdir kata-api
# Déplacement dans le dossier
cd kata-api
# Cette commande crée un fichier package.json avec des valeurs par défaut.
npm init -y
# Installer un framework d'applications Web Node.js minimaliste
npm install express --save
Créez un fichier app.js à la racine
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello, Docker World!'); }); app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); });
Créez le fichier Dockerfile qui est utilisé au niveau de notre stage Build.
# Utilisation de l'image officielle Node.js FROM node:18 WORKDIR /app # Copie de tous les fichiers du projet COPY . . # Installation des dépendances RUN npm install EXPOSE 3000 CMD ["node", "app.js"]
Vérifiez que l’application démarre bien en exécutant la commande ci-dessous :
node app.js
Vérifiez également que la construction de l’image Docker s’est déroulée avec succès. Exécutez cette série de commandes, puis accédez à l’URL http://localhost:4200 dans votre navigateur.
# Construire une image Docker nommée "kata-api" à partir du Dockerfile.
docker build -t kata-api .
# Afficher la liste des images Docker
# filtrer celles qui ont le nom "kata-api".
docker images | grep kata-api
# Exécuter un conteneur à partir de l'image "kata-api"
# En mappant le port 4200 de l'hôte au port 3000 du conteneur.
docker run -p 4200:3000 kata-api
Réussir le stage Test
Pour faire passer l’étape test, nous allons installer Vitest dans notre projet.
# installer vitest
npm install vitest --save-dev
Pour exécuter des tests avec Vitest, nous devons ajouter un script à notre fichier package.json
{
"name": "express",
"version": "1.0.0",
"scripts": {
"test": "vitest"
}
//...
}
Vitest par defaut recherchera par défaut des fichiers se terminant par .test.js ou .test.ts. Créons un fichier appelé math.test.ts.
import { expect, test } from "vitest"; test("Math.sqrt()", () => { expect(Math.sqrt(4)).toBe(2); expect(Math.sqrt(144)).toBe(12); })
Vérifiez que les tests fonctionne bien en exécutant la commande ci-dessous :
npm test
Reconstruire notre image puis exécutez les tests à partir du conteneur Docker.
# Construire l'image à nouveau pour inclure les changements
docker build -t kata-api .
# Exécution des tests
docker run kata-api npm test
Réussir le stage Push
Pour faire passer l’étape (stage) de push, assurez-vous d’avoir une instance fonctionnelle de GitLab avec gitlab runner. Créez un dépôt (repository) dans votre instance GitLab. Nous allons supposer que notre instance GitLab est hébergée sur gitlab.kata.ovh, le registre est sur registry.kata.ovh et le projet créé est sur gitlab.kata.ovh/kata/kata-api.git. Dés lors pour créer un projet se rendre sur https://gitlab.kata.ovh/projects/new
Remarquez qu’au niveau de notre étape (stage) de Push, notre fichier gitlab-ci.yml utilise les variables $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD et CI_REGISTRY pour se connecter au registre Docker. Nous n’avons pas besoin de fournir manuellement les valeurs de ces variables, car GitLab les renseigne automatiquement pour nous.
Créer un fichier un fichier .gitignore à la racine du projet sur votre ordinateur afin d’indiquer à Git de ne pas suivre les fichiers et répertoires spécifiés lors des opérations de commit.
# Node.js node_modules/ npm-debug.log
Configurez votre projet Node.js dans ce dépôt et soyez prêt à pousser les modifications.
# Initialisation du dépôt Git avec une branche principale définie sur "main"
git init --initial-branch=main
# Ajout du dépôt distant (remote) nommé "origin" avec l'URL de votre dépôt GitLab
git remote add origin https://gitlab.kata.ovh/kata/kata-api.git
# Ajout de tous les fichiers au suivi de Git
git add .
# Commit des fichiers avec un message de commit
git commit -m "Initial commit"
# Pousser les modifications vers le dépôt distant (GitLab)
# L'option -u pour configurer le suivi
# git push -u origin main
Pour tester votre registre depuis votre ordinateur vous pouvez saisir les commandes ci-dessous :
# Assurez-vous d'être connecté à GitLab Container Registry
# Suivez les instruction du prompt en renseignant votre email / password gitlab
docker login registry.kata.ovh
# Tag de l'image pour qu'elle corresponde à l'emplacement du registre GitLab
# Par defaut l'uri du registre est l'uri du repository sans le ".git" à la fin
docker tag kata-api registry.kata.ovh/kata/kata-api
# Pousser votre image vers le registre GitLab
docker push registry.kata.ovh/kata/kata-api
Vérifiez que l’image a bien été ajoutée au registre en naviguant vers https://gitlab.kata.ovh/kata/kata-api/container_registry.
Réussir le stage deploy
Pour réussir l’étape de déploiement, un serveur avec une clé SSH pour la connexion est nécessaire, et Docker doit également être installé sur le serveur.
Créer un serveur sur DigitalOcean (optionnel)
Si vous n’avez pas de serveur disponible, vous pouvez en créer un en utilisant DigitalOcean. Commencer par vous inscrire si vous n’êtes pas inscrit ensuite suivez le lien ci-après (https://cloud.digitalocean.com/droplets/new) pour créer votre serveur (droplet). Choisir une image pré-installée avec Docker au niveau de la Marketplace . Lors du choix du type d’authentification, sélectionnez l’option SSH Key car notre script se connecte à l’aide s’une clé SSH.
Pour créer une clé SSH exécutez la commande ci-dessous au niveau de votre ordinateur puis suivez les instructions.
# generer une clé ssh ssh-keygen # Puis suivez les instructions en renseignant le chemin où enregistrer la clé Enter file in which to save the key : /Users/mamadou/Documents/id_rsa_kata_api # laissez le mot de passe vide en appuyant sur entrée Enter passphrase (empty for no passphrase): # Appuyez encore sur entrée Enter same passphrase again:
Renseigner le contenu de id_rsa_kata_api.pub dans DigitalOcean. Puis appuyez sur create droplet.
Créer notre infrastructure de déploiement
Il s’agit d’une infrastructure minimaliste pour passer notre étape de déploiement avec succès. Si vous êtes intéressé par une infrastructure plus complète, suivez ce lien : Infrastructure pour déployer Strapi 4 dans un serveur Ubuntu avec Docker Compose
# Connexion sécurisée au serveur distant en utilisant la clé privée spécifiée
ssh -i id_rsa_kata_api root@[ip_du_droplet]
# Naviguer vers le répertoire /home sur le serveur distant
cd /home
# Créer un nouveau répertoire appelé 'kata-api'
mkdir kata-api
# Accéder au répertoire nouvellement créé
cd kata-api
# Ouvrir l'éditeur de texte 'nano' pour créer le fichier deploy.sh
nano deploy.sh
Placez le code ci-dessous dans le fichier deploy.sh
#!/bin/bash # Vérifie que le chemin complet de l'image Docker est fourni en tant que paramètre if [ -z "$1" ]; then echo "Usage: $0 <full_docker_image_path>" exit 1 fi # Charger les variables d'environnement depuis le fichier .env if [ -f .env ]; then source .env else echo "Erreur: Le fichier .env n'est pas trouvé." exit 1 fi # Variable DOCKER_IMAGE_PATH=$1 APP_NAME=kata_api # connexion au registre docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY # Arrêt et suppression de l'ancien conteneur s'il existe docker stop $APP_NAME || true docker rm $APP_NAME || true # Pull de la dernière version de l'image depuis le chemin spécifié docker pull $DOCKER_IMAGE_PATH # Démarrage du nouveau conteneur docker run -d --name $APP_NAME -p 80:3000 $DOCKER_IMAGE_PATH echo "L'application a été déployée avec succès."
Saisissez Ctrl + O puis appuyer sur la touche “Entrée” pour sauvegarder, puis Ctrl + X pour quitter.
Ensuite, créez le fichier .env nano .env
sur le serveur pour renseigner les informations permettant au serveur de se connecter au registre et de récupérer notre image. Pour la variable REGISTRY_PASSWORD
Il est recommandé d’utiliser un jeton d’accès plutôt que votre mot de passe. Rendez-vous sur votre GitLab privé, puis accédez à “Settings” > “Access Tokens”. Cochez le droit “read_registry”, renseignez le champ “Token name”, puis appuyez sur le bouton “Create project access token”.
[email protected] REGISTRY_PASSWORD=glpat-R9k3iYc75FGQbWQLg7F REGISTRY=registry.kata.ovh
Fournir à Gitlab les accès aux serveurs
Maintenant que notre infrastructure de déploiement est prête, il est nécessaire de rendre les variables $DEV_SERVER_IP, $DEV_PRIVATE_KEY, $DEV_USER et $DEV_APP_PATH
disponibles dans notre environnement CI/CD. En effet, notre script gitlab-ci.yml les utilise pour se connecter au serveur.
Pour ce faire, rendez-vous sur GitLab > Settings > CI/CD, puis appuyez sur Variables. Ensuite, cliquez sur “Add variable”, et dans notre cas, nous avons donné les valeurs suivantes aux variables :
DEV_SERVER_IP=165.22.95.140 # Ici il faut selectionner comme type file # Puis coller le contenu de la clé privé id_rsa_kata_api (le fichier sans extension .pub) DEV_PRIVATE_KEY=-----BEGIN OPENSSH PRIVATE KEY-----***** DEV_USER=root DEV_APP_PATH=/home/kata-api
Vérifions le bon fonctionnement de notre script de déploiement en effectuant un lancement manuel.
# rendre deploy.sh exécutable chmod +x deploy.sh # Déployer l'image pousser dans le registre précédemment ./deploy.sh registry.kata.ovh/kata/kata-api
Promotion en recette et en production
Pour une compréhension approfondie, examinons le workflow depuis le début. Lorsqu’un développeur effectue un commit sur la branche principale (main), cela déclenche automatiquement une série d’étapes comprenant la construction (build), les tests, le push du build dans le registre et le déploiement sur l’environnement de développement (dev).
Le déploiement en recette est réalisé manuellement. Une fois que les tests de développement sont satisfaisants, il est possible de déclencher manuellement la promotion du build vers l’étape de recette. Cela entraînera la promotion de l’image en changeant son tag, suivi du déploiement de cette image en recette.
De manière similaire au déploiement en recette, le déploiement en production est également réalisé manuellement une fois que les tests client confirment la satisfaction au niveau de l’environnement de recette.
L’activation de la promotion de l’image de recette vers la production déclenchera également la mise en production avec cette image. Cette approche manuelle à chaque étape offre un contrôle précis, garantissant que seules les versions validées progressent d’une phase à l’autre, de la recette à la production.
stages: - build - test - push - dev - staging - prod variables: DOCKER_IMAGE_NAME: "registry.kata.ovh/kata/kata-api" DEV_ENV: "dev" STAGING_ENV: "staging" PROD_ENV: "prod" # Les autres stages ici # ... promote_staging: stage: staging image: docker:latest only: - main services: - docker:dind script: - docker pull $DOCKER_IMAGE_NAME:latest-$DEV_ENV - docker tag $DOCKER_IMAGE_NAME:latest-$DEV_ENV $DOCKER_IMAGE_NAME:latest-$STAGING_ENV - docker push $DOCKER_IMAGE_NAME:latest-$STAGING_ENV when: manual deploy_staging: stage: staging image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $STAGING_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $STAGING_PRIVATE_KEY script: - ssh -i $STAGING_PRIVATE_KEY $STAGING_USER@$STAGING_SERVER_IP "cd $STAGING_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:latest-$STAGING_ENV" environment: name: staging url: https://staging.kata.ovh needs: - job: promote_staging artifacts: true promote_prod: stage: prod image: docker:latest only: - main services: - docker:dind script: - docker pull $DOCKER_IMAGE_NAME:latest-$STAGING_ENV - docker tag $DOCKER_IMAGE_NAME:latest-$STAGING_ENV $DOCKER_IMAGE_NAME:latest-$PROD_ENV - docker push $DOCKER_IMAGE_NAME:latest-$PROD_ENV when: manual deploy_prod: stage: prod image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $PROD_PRIVATE_KEY script: - ssh -i $PROD_PRIVATE_KEY $PROD_USER@$PROD_SERVER_IP "cd $PROD_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:latest-$PROD_ENV" environment: name: production url: https://prod.kata.ovh needs: - job: promote_prod artifacts: true
Ce script constitue un exemple concret de mise en œuvre du workflow décrit précédemment. Son objectif est de faciliter le déploiement progressif de l’application, débutant par l’environnement de développement, suivi de la recette, et enfin, la production. Les étapes de déploiement en recette et en production sont précédées par une phase de promotion, au cours de laquelle l’image à déployer est étiquetée avec le préfixe correspondant à l’environnement cible (staging, prod).
![](https://applize.io/wp-content/uploads/2023/12/integration-continue-step0-1024x200.png)
![](https://applize.io/wp-content/uploads/2023/12/deploiement-continu-step0-1024x189.png)
Versionnement Semantic-release
Jusqu’à présent, les images de notre projet étaient versionnées en utilisant des étiquettes (tag) telles que “latest-dev”, “latest-staging” ou “latest-prod”, en fonction des environnements où ces images étaient promues pour le déploiement. Désormais, nous allons mettre en place une gestion de version correcte en intégrant semantic-release. L’objectif de semantic-release est d’être exécuté au sein d’une pipeline d’intégration continue. Il effectue ainsi le calcul du numéro de version en analysant les commits effectués dans le projet depuis la dernière version publiée. Cette analyse est rendue possible grâce aux commits conventionnels, un format permettant de standardiser les messages de commit.
Ensuite, Semantic Release exécute un ensemble d’actions essentielles pour simplifier le processus de versionnement. Ces actions comprennent la mise à jour automatique de fichiers ou la version est présentes tels que package.json, yarn.lock…, la génération d’un fichier de changelog détaillant les modifications, ainsi que la création et la publication d’une nouvelle version du projet.
La mise en place de Semantic Release dans notre workflow présente deux défis :
- Le mode de fonctionnement par défaut de Semantic Release repose sur la définition de branches spécifiques pour les releases ou les pré-releases. Les versions générées dans une branche de release ressemble à v1.0.0 et celle générées dans les branches de pré-releases à v1.0.0-beta.1. Quand semantic-release est invoquée dans une branche non configuré elle ne génère pas de version. Notre architecture utilise une seule branche principale. Il devient donc impératif de déterminer le moment opportun pour générer une version. Nous devons définir clairement à quel moment la version est produite dans ce contexte mono-branche.
- La seconde problématique concerne la nécessité que la release soit déclenchée uniquement après la réussite des phases de build et de tests. Dans notre cas, pour respecter nos exigences fonctionnelles, nous devons aligner le numéro de version de notre code source avec les versions des livrables. Il faut savoir que cette manière de faire est dépréciée par semantic-release car il juge que la version ne doit être connue qu’après ces étapes de build et test.
À quel moment alors créer la release ?
La réalisation d’une release doit s’effectuer dans un environnement stable, excluant ainsi immédiatement l’environnement de développement. Il est important de noter que nous évoluons dans un workflow à branche unique, ce qui signifie que nous sommes encore dans notre environnement de développement jusqu’à ce que nous appuyions sur le bouton “promote_recette” pour la transition vers l’environnement de recette.
La décision de générer une release lors du passage à l’environnement de recette apparaît comme une solution judicieuse. En effet, cette approche restreint le nombre de releases publiées et les confine à un environnement spécifique, offrant ainsi la possibilité de fournir une documentation claire de la version actuellement en cours de validation au client. Cette démarche renforce la traçabilité en associant explicitement une version particulière à l’étape de recette, simplifiant ainsi le suivi des versions, des correctifs, et des modifications spécifiques qui ont été validés par le client.
Comment aligner la version de notre image Docker avec le code source qui a permis de la produire ?
Cela nécessite de récupérer le numéro de version de la future release sans la publier, car la publication de la release ne doit s’effectuer qu’en cas de réussite des phases de build et de test. Étant donné que cette approche est désormais dépréciée, il sera nécessaire de mettre en place une solution alternative (hack), pour atteindre nos objectifs.
Pour optimiser la représentation, le haut de notre fichier gitlab-ci.yml ressemblerait à ceci. Le code complet sera fourni à la fin de l’article :
stages: - version - build - test - push - dev - release - staging - prod variables: DOCKER_IMAGE_NAME: "registry.kata.ovh/kata/kata-api" DEV_ENV: "dev" STAGING_ENV: "staging" PROD_ENV: "prod" version: stage: version image: node:18 only: - staging before_script: - npm install script: - npx semantic-release --generate-notes false --dry-run artifacts: paths: - VERSION.txt build: stage: build image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker build -t $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV . test: stage: test image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker run $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV npm test push: stage: push image: docker:latest services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV deploy_dev: stage: dev image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $DEV_PRIVATE_KEY - RC_VERSION=$(cat VERSION.txt) script: - ssh -i $DEV_PRIVATE_KEY $DEV_USER@$DEV_SERVER_IP "cd $DEV_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV" environment: name: development url: https://dev.kata.ovh release: stage: release image: node:18 before_script: - npm install script: - npx semantic-release when: manual artifacts: paths: - VERSION.txt promote_staging: stage: staging image: docker:latest only: - main services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker pull $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV - docker tag $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV - docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV needs: - job: release artifacts: true
À partir de là notre pipeline ressemble à ceci :
![](https://applize.io/wp-content/uploads/2023/12/gitlab-integration-continue-1024x148.png)
![](https://applize.io/wp-content/uploads/2023/12/gitlab-deploiement-continu-1024x153.png)
Comment faire passer le stage version et release qu’on vient d’ajouter ?
Afin d’implémenter cette fonctionnalité, vous devrez installer Semantic Release sur votre projet Node.js précédent et le configurer pour qu’il renvoie la version calculée dans un fichier VERSION.txt. C’est ce fichier que le stage “Version” renvoie en tant qu’artefact.
Pour ajouter semantic-release dans le projet executez les commandes ci dessous :
# installer Semantic Release et ses dépendances
npm install @semantic-release/gitlab @semantic-release/exec @semantic-release/changelog @semantic-release/git --save-dev
Créer le fichier release.config.js
à la racine du projet et ajoutez le contenu ci-dessous :
module.exports = { branches: ['main'], plugins: [ '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/changelog', [ '@semantic-release/git', { assets: ['CHANGELOG.md', 'package.json', 'yarn.lock'], } ], '@semantic-release/gitlab', [ "@semantic-release/exec", { verifyReleaseCmd: "echo ${nextRelease.version} > VERSION.txt", }, ] ], };
Vérifier le fonctionnement de semantic-release en exécutant les commandes ci-dessous :
# Commencer par ecrire un commit conventionnel
git add .
git commit -am "chore: Ajoût de semantic-release"
# Lancer semantic-release en mode dry
# si vous obtenez l'erreur suivante c'est que la configuration est correcte.
# must be created and set in the GL_TOKEN or GITLAB_TOKEN environment...
npx semantic-release --dry-run
Semantic Release a été conçu pour opérer dans un environnement d’intégration continue. Afin de lui permettre d’effectuer les tâches de publication des versions, il est nécessaire de lui fournir un jeton d’accès. Vous pouvez procéder comme suit :
- Accédez à “GitLab” > “Settings” > “Access Tokens” dans votre projet GitLab.
- Cochez les droits “write_registry” et “api”.
- Renseignez le champ “Token name”.
- Appuyez sur le bouton “Create project access token”.
Ensuite, allez dans “GitLab” > “Settings” > “CI/CD” et accédez à la section “Variables”. Cliquez sur “Add variable”, attribuez le nom de variable GL_TOKEN, et utilisez le jeton d’accès que vous venez de créer comme valeur. Cette configuration permettra à Semantic Release d’utiliser ce jeton d’accès lors de ses opérations de publication des releases.
Gestion des bugs critiques (hotfix)
Un hotfix est une correction de bug critique ou une modification urgente apportée à un logiciel en production. Il s’agit d’une intervention rapide destinée à résoudre un problème spécifique qui nécessite une correction immédiate, souvent en dehors du cycle de développement normal.
Les hotfix sont généralement nécessaires pour traiter des problèmes graves qui affectent la stabilité ou la sécurité d’une application, et qui ne peuvent pas attendre la prochaine version planifiée. Ils sont peu utilisés en trunk-based development où les versions sont déployées fréquemment.
Le processus de gestion des hotfix implique généralement la création d’une branche spéciale (hotfix branch) à partir du tag de la version en production. Les corrections critiques sont apportées cependant dans la branche principale (main), puis elles sont introduit dans la branche hotfix par cherry pick.
Los du deploiement de la branche hotfix en production, Il est crucial de générer une nouvelle version pour des raisons de traçabilité. Nous devrons donc à nouveau faire appel à semantic-release. Cette librairie gère les hotfix sous forme de branches de maintenance. Afin de déployer la correction d’un bug dans une version de maintenance avec semantic-release, nous devons créer la branche hotfix/1.0.x à partir de la version (tag) v1.0.0. Par exemple, si la version en production est v1.5.2, alors nous devrons créer la branche hotfix/1.5.x.
Nous allons informer semantic-release
via son fichier de configuration release.config.js
que nos branches de maintenance concernent toutes les branches qui commencent par hotfix/
.
module.exports = { branches: [ //Branche de release 'main', //Branche de maintenance { "name": "hotfix/+([0-9])?(.{+([0-9]),x}).x", "range": "${name.replace(/^hotfix\\//g, '')}" } ], plugins: [ '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/changelog', [ '@semantic-release/git', { assets: ['CHANGELOG.md', 'package.json', 'yarn.lock'], } ], '@semantic-release/gitlab', [ "@semantic-release/exec", { verifyReleaseCmd: "echo ${nextRelease.version} > VERSION.txt", }, ] ], };
À l’heure ou j’écris ces lignes, semantic-release ne peut pas générer une nouvelle version à partir de la branche hotfix dans le cas où la branche hotfix a été créée à partir de la dernière version (tag) de la branche de release (main).
Si on est dans cette situation là c’est uniquement en créant une nouvelle version dans main que semantic-release parvient à générer la version de maintenance au niveau de la branche hotfix. Vous pouvez suivre l’évolution de cette problématique dans le ticket (issue) ici.
Auparavant, nous avions conditionné notre déploiement en recette à l’exécution manuelle de notre étape release. Nous allons désormais transférer cette condition vers notre étape promote_staging, afin de pouvoir générer une release au besoin sans déployer automatiquement notre application en recette. Ceci nous permettra de contourner notre problème avec semantic-release en attendant sa résolution.
Lorsque nous publions une version à partir de la branche principale, une mise en ligne automatique s’effectue sur le serveur de développement, tandis que les déploiements en recette et en production nécessitent des actions manuelles. Nous avons donc mis en place une intégration continue et une livraison qui n’est pas entièrement continue, car des actions manuelles sont nécessaires après le push du code pour effectuer les déploiements dans les environnements de recette et de production. Ce mode de fonctionnement nous convient.
Cependant, pour le déploiement d’un hotfix, nous allons opter pour une intégration continue et un déploiement continu. Cela signifie que dès que le code est poussé, la mise en production s’effectue sans nécessiter d’actions supplémentaires. Vous trouverez ci-dessous les adaptations apportées à notre fichier gitlab-ci.yml
pour atteindre cet objectif. Certains stages ont été omis pour simplifier l’affichage.
stages: - version - build - test - push - dev - release - staging - prod variables: DOCKER_IMAGE_NAME: "registry.kata.ovh/kata/kata-api" DEV_ENV: "dev" STAGING_ENV: "staging" PROD_ENV: "prod" version: stage: version image: node:18 before_script: - npm install script: - npx semantic-release --generate-notes false --dry-run artifacts: paths: - VERSION.txt build: stage: build image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker build -t $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV . test: stage: test image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker run $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV npm test push: stage: push image: docker:latest only: - /^hotfix\/*/ - main services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker push "$DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV" release: stage: release image: node:18 rules: - if: '$CI_COMMIT_BRANCH =~ /^hotfix\//' when: always - if: '$CI_COMMIT_BRANCH == "main"' when: manual before_script: - npm install script: - npx semantic-release artifacts: paths: - VERSION.txt promote_staging: stage: staging image: docker:latest only: - main services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker pull $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV - docker tag $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV - docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV when: manual # Exécution manuelle ajoutée needs: - job: release artifacts: true promote_prod: stage: prod image: docker:latest rules: - if: '$CI_COMMIT_BRANCH =~ /^hotfix\//' when: always - if: '$CI_COMMIT_BRANCH == "main"' when: manual services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - | SOURCE_ENV=$STAGING_ENV TARGET_ENV=$PROD_ENV if [[ "$CI_COMMIT_REF_NAME" = "hotfix/"* ]]; then SOURCE_ENV=$DEV_ENV fi docker pull $DOCKER_IMAGE_NAME:$RC_VERSION-$SOURCE_ENV docker tag $DOCKER_IMAGE_NAME:$RC_VERSION-$SOURCE_ENV $DOCKER_IMAGE_NAME:$RC_VERSION-$TARGET_ENV docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$TARGET_ENV needs: - job: release artifacts: true deploy_prod: stage: prod image: alpine only: - main - /^hotfix\/*/ before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $PROD_PRIVATE_KEY - RC_VERSION=$(cat VERSION.txt) script: - ssh -i $PROD_PRIVATE_KEY $PROD_USER@$PROD_SERVER_IP "cd $PROD_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:$RC_VERSION-$PROD_ENV" environment: name: production url: https://prod.kata.ovh needs: - job: promote_prod artifacts: true
Comment tester le déploiement d’un hotfix ?
Une branche de hotfix est créée à partir du tag de la version en production créée par semantic-release.
# Récupérer tous les tags depuis le dépôt distant
git fetch --tags
# Afficher la liste des tags locaux
git tag
# Créer une nouvelle branche 'hotfix/v1.0.x' basée sur le tag 'v1.0.0'
git checkout -b hotfix/v1.0.x v1.0.0
Cependant la correction du bug est effectuée au niveau de la branche principale (main).
git checkout main
git commit -m "Fix: bug critique"
Et ensuite appliquées à la branche de hotfix par cherry-picking.
# Identifier le hash du commit
git log --oneline --decorate
# Se deplacer dans la branche de hotfix
git checkout hotfix/v1.0.0
# Appliquer le commit de la branche principale
git cherry-pick
# Appliquer les changement sans commit
# Permet de faire des vérification supplémentaire avant de commit
git cherry-pick af02e0b --no-commit
Cette branche de hotfix est conservé jusqu’a ce que la production hérite d’une nouvelle version provenant de main.
Retour à une version antérieure (Rollback)
Examinez les pipelines existants dans GitLab afin d’identifier la version antérieure que vous souhaitez déployer. Une méthode efficace pour remonter à la version appropriée consiste à consulter les tags générés par semantic-release ainsi que les noms de branches associés à chaque pipeline.
Utilisez l’option de réexécution pour relancer le job deploy_prod associé à la version choisie. Cela peut se faire en cliquant sur le bouton de réexécution dans l’interface GitLab.
![](https://applize.io/wp-content/uploads/2023/12/rollback-gitlab-1024x219.png)
Conclusion
Voici l’intégralité du code source de notre fichier gitlab-ci.yml :
stages: - version - build - test - push - dev - release - staging - prod variables: DOCKER_IMAGE_NAME: "registry.kata.ovh/kata/kata-api" DEV_ENV: "dev" STAGING_ENV: "staging" PROD_ENV: "prod" version: stage: version image: node:18 before_script: - npm install script: - npx semantic-release --generate-notes false --dry-run artifacts: paths: - VERSION.txt build: stage: build image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker build -t $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV . test: stage: test image: docker:latest services: - docker:dind before_script: - RC_VERSION=$(cat VERSION.txt) script: - docker run $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV npm test push: stage: push image: docker:latest only: - /^hotfix\/*/ - main services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker push "$DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV" deploy_dev: stage: dev image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $DEV_PRIVATE_KEY - RC_VERSION=$(cat VERSION.txt) script: - ssh -i $DEV_PRIVATE_KEY $DEV_USER@$DEV_SERVER_IP "cd $DEV_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV" environment: name: development url: https://dev.kata.ovh release: stage: release image: node:18 rules: - if: '$CI_COMMIT_BRANCH =~ /^hotfix\//' when: always - if: '$CI_COMMIT_BRANCH == "main"' when: manual before_script: - npm install script: - npx semantic-release artifacts: paths: - VERSION.txt promote_staging: stage: staging image: docker:latest only: - main services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - docker pull $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV - docker tag $DOCKER_IMAGE_NAME:$RC_VERSION-$DEV_ENV $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV - docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV when: manual needs: - job: release artifacts: true deploy_staging: stage: staging image: alpine only: - main before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $STAGING_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $STAGING_PRIVATE_KEY - RC_VERSION=$(cat VERSION.txt) script: - ssh -i $STAGING_PRIVATE_KEY $STAGING_USER@$STAGING_SERVER_IP "cd $STAGING_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:$RC_VERSION-$STAGING_ENV" environment: name: staging url: https://staging.kata.ovh needs: - job: promote_staging artifacts: true promote_prod: stage: prod image: docker:latest rules: - if: '$CI_COMMIT_BRANCH =~ /^hotfix\//' when: always - if: '$CI_COMMIT_BRANCH == "main"' when: manual services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - RC_VERSION=$(cat VERSION.txt) script: - | SOURCE_ENV=$STAGING_ENV TARGET_ENV=$PROD_ENV if [[ "$CI_COMMIT_REF_NAME" = "hotfix/"* ]]; then SOURCE_ENV=$DEV_ENV fi docker pull $DOCKER_IMAGE_NAME:$RC_VERSION-$SOURCE_ENV docker tag $DOCKER_IMAGE_NAME:$RC_VERSION-$SOURCE_ENV $DOCKER_IMAGE_NAME:$RC_VERSION-$TARGET_ENV docker push $DOCKER_IMAGE_NAME:$RC_VERSION-$TARGET_ENV needs: - job: release artifacts: true deploy_prod: stage: prod image: alpine only: - main - /^hotfix\/*/ before_script: - apk add --no-cache openssh - mkdir -p ~/.ssh - ssh-keyscan -t rsa $DEV_SERVER_IP >> ~/.ssh/known_hosts - chmod 600 $PROD_PRIVATE_KEY - RC_VERSION=$(cat VERSION.txt) script: - ssh -i $PROD_PRIVATE_KEY $PROD_USER@$PROD_SERVER_IP "cd $PROD_APP_PATH && ./deploy.sh $DOCKER_IMAGE_NAME:$RC_VERSION-$PROD_ENV" environment: name: production url: https://prod.kata.ovh needs: - job: promote_prod artifacts: true
L’automatisation du versionnement avec Semantic Release a été abordée comme une composante cruciale pour maintenir la cohérence entre le code source et les livrables, renforçant ainsi la traçabilité tout au long du cycle de vie logiciel. La gestion des environnements de développement, de recette, et de production a été détaillée, soulignant l’importance d’une approche manuelle à chaque étape pour assurer un contrôle précis et une progression fiable des versions validées.
En explorant la trunk-based development, nous avons mis en lumière sa pertinence dans un contexte où la complexité des branches peut devenir superflue, notamment pour les logiciels en tant que service (SaaS).
Enfin, la gestion des hotfix a été abordée, soulignant la nécessité d’une intégration continue et d’un déploiement continu pour les corrections de bugs critiques. Bien que certaines adaptations soient nécessaires, notamment en attendant la résolution d’une problématique avec Semantic Release, cette approche garantit une réactivité maximale pour résoudre les problèmes de production de manière urgente.
En résumé, en adoptant ces pratiques et en les adaptant judicieusement à chaque contexte, les équipes de développement peuvent améliorer la flexibilité, la qualité, et la cohérence de leurs produits logiciels, répondant ainsi aux impératifs d’un marché en perpétuelle évolution. Le mariage entre l’innovation apportée par l’approche MVP et l’efficacité assurée par les pratiques CI/CD s’affirme comme un duo gagnant pour mener à bien des projets de développement logiciel.
Architecte logiciel, Développeur d'application diplomé d'ETNA, la filière d'alternance d'Epitech, j'ai acquis une expertise solide dans le développement d'applications, travaillant sur des projets complexes et techniquement diversifiés. Mon expérience englobe l'utilisation de divers frameworks et langages, notamment Symfony, Api Platform, Drupal, Zend, React Native, Angular, Vue.js, Shell, Pro*C...