Master Expert Technologie de l'information EPITECH 2020.
Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)
Formation PSPO-1 Agile Scrum 2022.
Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.
Qu'est-ce qu'une CI / CD ?
Bonus - Mise en place d'une CI / CD avec Github Actions
Mise en place d'une CI / CD avec Gitlab
Mise en place du CD avec Terraform
Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.
Après la théorie, nous verrons alors la pratique en réalisant des exercices sur un repository gitlab.
Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.
Pour faciliter le rendu final des TPs et exercices, vous allez créer un dépôt sur Github avec le nom cicd en publique et le cloner sur votre machine.
Si vous préférez le garder en privé, vous devez m'ajouter avec les droits pour voir votre dépôt avec le nom d'utilisateur : ValentinMontagne
Intégration fréquente des changements au code source pour éviter les régressions.
Automatiser pour éviter les erreurs humaines et la pénibilité de tâche répétitive.
Utilise une pipeline avec différentes étapes pour vérifier la qualité des changements.
La régression est un type de bug, une fonctionnalité déjà présente et fonctionnel dans la solution n'est maintenant plus utilisable.
Un autre type de régression que la régression fonctionnelle existe, on l'appelle la régression de performance. Plus précisément, la fonctionnalité consomme maintenant bien plus de ressources pour fonctionner.
C'est un principe fondamental du développement d'éviter les bugs et les régressions.
Par exemple en Agilité, chaque itération du produit doit développer le produit, jamais diminuer involontairement sa qualité ou ses fonctionnalités.
Itérer rapidement, apprendre rapidement.
Permettre le Lean et l'Agile
Garder un produit fonctionnel car le déploiement est automatisable avec le Cloud as a service.
Lean, Agile, etc.
Beaucoup de méthode de gestion d'entreprise, de projet ou de produit se base sur une itération continue où l'apprentissage est la clé.
Google Design Sprint
Grâce au Sprint Design, Google est capable de tester une nouvelle fonctionnalité en 5 jours ouvrés.
Course à l'itération !
Pour cela, les équipes ont besoin de méthodes pour éviter les régressions et augmenter la rapidité des livraisons.
Scientifiquement prouvé
Corrélation entre les performances et le bien-être au travail avec le CI / CD et le Lean
Computers perform repetitive tasks; People solve problems.
Key Indicators
Protéger l'entreprise d'incidents techniques
Créer un lien de confiance et garder les utilisateurs
Augmenter la productivité et le bien-être des équipes techniques
Réduire le temps et le coût de la validation des changements.
Les tâches pénibles et répétitives sont facilement ignorées.
Éviter les régressions à la source au lieu de les corriger après déploiement.
Créé par Kohsuke Kawaguchi en 2011 avec Oracle car réalisé en Java.
Open source, s'interface avec des systèmes de versions comme Git.
Automatise le build, les tests et le déploiement.
Créé par Gitlab Inc. en 2014, open source avec une version Entreprise.
Solution tout-en-un du CI / CD avec gestion des issues et pipelines liés aux projets.
Permet d'héberger une version de Gitlab directement sur les serveurs de l'entreprise.
Après l'impulsion du rachat par Microsoft, créations de la partie Actions de Github en 2018.
Leader dans le stockage de dépôt de code et développement de projet Open Source, grande communauté.
Plus limité que Gitlab CI sur la partie DevOps / Docker.
Les hooks sont des scripts qui s'exécutent automatiquement à des moments précis du Workflow Git, par exemple lors d'un commit, pull ou push des modifications d'un dépôt. Ces scripts peuvent être utilisés pour effectuer diverses tâches, comme la validation du code, le formatage des fichiers ou même l'envoi de notifications.
Il existe deux types de hooks Git :
Côté client : Ils sont exécutés sur votre machine locale avant d'effectuer des modifications.
Côté serveur : Ils sont exécutés sur le serveur distant lorsque vous transférez des modifications.
Nous allons utiliser cet outil côté client pour éviter qu'un développeur ne commit du code qui n'est pas validé par notre CI.
Le code est déployé après chaque modification.
Au vu de la fréquence, on automatise alors le déploiement.
Plusieurs stratégies de déploiement existent. (Blue / Green, Feature Flag, Canary...)
Gérer correctement les différents environnements de la solution.
Identifier et corriger rapidement les bugs et régressions.
Améliorer la stabilité de la solution et revenir rapidement à la version d'avant.
Externaliser son infrastructure
Automatiser et monitorer le déploiement des applications
Définir toute son infrastructure via des fichiers de configuration.
Automatiser la création, l'édition et la suppression des ressources de l'infrastructure dans le cloud.
Suivre les différentes versions de l'infrastructure en fonction de la solution.
Automatise le déploiement sur tous les grands Clouds.
Est capable de gérer des grands cluster avec Kubernetes.
S'intègre facilement dans les pipelines CI / CD.
Terraform a été mis en open-source en 2014 sous la Mozilla Public License (v2.0).
Puis, le 10 août 2023, avec peu ou pas de préavis, HashiCorp a changé la licence pour Terraform de la MPL à la Business Source License (v1.1), une licence non open source.
OpenTofu est un fork de la version Open source de Terraform et est géré par la fondation Linux. C'est donc une bonne alternative à Terraform aujourd'hui.
La migration à OpenTofu est extrêmement simple car il n'y a pas de différence de fonctionnement avec Terraform.
Blue / Green
Feature Flag
Canary
1. Comment déployer fréquemment sans interrompre les utilisateurs ?
2. Comment déployer fréquemment sans bugs ou régressions pour les utilisateurs ?
3. Comment tester les changements avec de vrais utilisateurs ?
On crée deux environnements distincts mais identiques. Un environnement (Blue) exécute la version actuelle de l'application et un environnement (Green) exécute la nouvelle version de l'application.
Gère les fonctionnalités dans le code source. Une condition dans le code permet d'activer ou de désactiver une fonctionnalité pendant l'exécution. Voir Unleash.
Redirige le trafic d'une portion des utilisateurs en fonction de critères ou d'un simple pourcentage vers la nouvelle version. Après un certain temps ou d'une condition, l'ensemble du trafic est rédirigé.
– Conrad Anker
Gitlab utilise le fichier à la racine de votre dépôt .gitlab-ci.yml qui contient toutes les instructions pour votre pipeline CI / CD. Lors d'évènement comme un commit ou une pull request (ou merge request), la pipeline s'execute grâce à des runners.
Les runners peuvent être hébergé par l'équipe (personnalisé) ou directement par Gitlab (partagé). Ils s'executent sur des machines physiques, virtuelles ou des conteneurs Docker.
# .gitlab-ci.yml File
# Configure the global image and cache for every stage
image: node:latest
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
# Define all the stages used for the pipeline
stages:
- validate
- test
- build
- release
- deploy
# Add first job using the '.pre' stage defined by Gitlab, could be a defined stage too
# .pre is always run as the first stage
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
– Conrad Anker
# The configuration for the `remote` backend.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.aws_region
}
# AWS EC2 instance
resource "aws_instance" "example" {
ami = var.ec2_ami
instance_type = var.ec2_instance_type
key_name = var.ec2_key_name
tags = {
Name = "ExampleInstance"
}
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
# Pull and run your Docker image here
docker run -d -p 80:80 valentinmontagne/nginx-web-example:${var.docker_image_version}
EOF
}Gitlab CI
Création du compte Gitlab
Si vous n'avez pas encore de compte Gitlab, je vous invite pour la suite du TP et des exercices à créer un compte avec votre adresse étudiante.
Récupérer le projet de base
Maintenant que vous avez un compte Gitlab, vous pouvez "Fork" directement le projet ou le cloner sur votre machine et créer un nouveau projet vide pour récupérer le contenu du projet de base pour réaliser ce TP :
https://gitlab.com/vm-marvelab/ci-cd
(Tous les fichiers sont à mettre à la racine de votre dépôt.)
Le projet de base est une API très simple en NodeJS, elle utilise Express pour lancer un serveur qui écoute les requêtes sur des routes définies.
N'hésitez pas à lire le README pour comprendre comment fonctionne la route /auth.
Utilisez la commande pour installer les dépendances du projet :
npm install
Ensuite utilisez la commande pour lancer l'API :
npm start
Maintenant que notre projet de base fonctionne, nous allons commencer à intégrer notre pipeline CI.
Pour cela, créez à la racine du projet le fichier .gitlab-ci.yml
N'hésitez pas à installer une extension sur votre IDE pour vous aider avec le format du fichier.
Passons à la prochaine étape.
Nous devons spécifier avec quel image notre Runner doit s'executer pour lancer notre pipeline CI, étant une API en nodejs, nous allons utiliser une image nodejs.
Nous allons ajouter des informations sur le cache pour éviter qu'à chaque Job de chaque Stage que nous soyons obliger de réinstaller les dépendances.
# .gitlab-ci.yml file
image: node:latest
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
Comme vu ensemble, un Job est une étape spécifique du processus, comme exécuter des tests, compiler du code, ou déployer une application. Un job peut être configuré avec des scripts, des environnements d'exécution, et des conditions spécifiques (par exemple, quand il doit être exécuté). Plusieurs jobs peuvent s'exécuter en parallèle ou en séquence, en fonction des dépendances et de la structure du pipeline.
Un Stage est un groupe de Job et permet d'organiser la pipeline, ils s'exécutent dans l'ordre dont ils sont définis.
Définissons nos Stages et réalisons notre premier Job qui sera l'installation des dépendances à la suite du fichier :
stages:
- validate
- test
- build
- release
- deploy
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
Ici le stage .pre est un Stage déjà créé par Gitlab, il est obligatoirement le premier à être lancé.
Nos Stages sont les étapes habituelles d'une pipeline de CI / CD.
Une fois ajouté, réaliser un git commit et un git push pour envoyer les changements à votre dépôt sur Gitlab.
Une fois fait, allez dans la section Build dans la barre à gauche du dépôt sur gitlab.com et cliquez sur Pipelines, il n'y a pas encore de Pipeline lancée, c'est parce que nous n'avons pas encore de Job hors installation.
Passons à l'étape suivante pour réaliser notre Stage validate.
Ajoutons maintenant le Stage de validation, pour cela nous allons ajouter les outils Eslint pour vérifier la qualité de notre code NodeJS :
npm init @eslint/config@latest
Choisissez les options "Style et problems", puis "CommonJS", "None of these", "No" pour Typescript et cochez "Node".
Un nouveau fichier eslint.config.mjs est apparu, passons à l'étape suivante.
Changez la configuration du fichier pour ignorer les fichiers de tests et ajouter des règles comme ici ne pas avoir de variables inutilisés, de variable non défini ou de console.log dans le code.
// eslint.config.mjs
import globals from "globals";
import pluginJs from "@eslint/js";
export default [
{
ignores: ["**/*.test.js"],
files: ["**/*.js"],
languageOptions: { sourceType: "commonjs" },
},
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
{
rules: {
"no-unused-vars": "error",
"no-undef": "error",
"no-console": "error",
},
},
];
Ajoutons maintenant le script "lint" au package.json pour pouvoir lancer la commande :
npm run lint
// package.json
// ...
"scripts": {
"start": "node src/index.js",
"lint": "eslint src --max-warnings=0"
},
// ...
Ajoutons maintenant un console.log que l'on a "oublié" dans une route de notre API dans le fichier index.js qui affiche la variable secret et qui ne devrait pas être dans nos logs !
const express = require("express");
const auth = require("./modules/authentication");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/auth/:secret", (req, res) => {
const { secret } = req.params;
const response = auth(secret);
console.log(secret);
res.status(response.status).send(response.message);
});
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`Example app listening on http://localhost:${port}`);
});
Lancez la commande npm run lint vous devriez maintenant avoir une erreur.
Ajoutons maintenant un Job dans notre pipeline CI pour lancer cette commande automatiquement. A la suite du fichier .gitlab-ci.yml, ajoutez le Job lint dans le Stage validate.
// .gitlab-ci.yml
// ...
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
lint:
stage: validate
script:
- npm run lint
Faites un nouveau commit et faites un push sur la branch main à nouveau et retournez voir les Pipelines de votre dépôt pour voir le résultat.
La même erreur est détecté automatiquement par votre CI maintenant. Enlevez le console.log et faites de nouveau un push pour corriger le problème.
Mais cela serait plus fiable si l'on détectait ce genre de problème avant qu'il n'arrive sur notre pipeline non ?
Mettons en place notre Git hooks à l'étape suivante.
Maintenant nous allons installer Husky à notre projet, qui permet de mettre en place les Git hooks pour nous permettre d'executer notre validation ou nos tests avant chaque commit. Cela rentre totalement dans l'intégration continue pour éviter de commit du code qui n'est pas fonctionnel ou de mauvaise qualité.
Lancez la commande :
npm install --save-dev husky && npx husky init
Husky génère alors un dossier .husky, ouvrez le fichier pre-commit dans ce dossier et changez npm test par npm run lint.
Essayez de commit, vous allez maintenant avoir npm run lint qui s'execute automatiquement. Essayez d'oublier à nouveau un console.log dans le code, lors de la tentative de commit vous devriez être bloqué par l'echec de la commande npm run lint.
Maintenant ajoutons les tests à notre pipeline à l'étape suivante.
Pour réaliser l'étape des tests, nous allons installer Vitest :
npm install -D vitest
Ensuite, ajoutons dans package.json la commande "test".
// package.json
// ...
"scripts": {
"start": "node src/index.js",
"lint": "eslint src --max-warnings=0",
"prepare": "husky",
"test": "vitest run"
},
"devDependencies": {
// ...
Vérifiez que tout est fonctionnel avec la commande :
npm test
Ajoutez ensuite cette commande au Git hooks.
Ensuite, ajoutons notre Job unit-test qui sera dans notre Stage test.
// .gitlab-ci.yml
// ...
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
lint:
stage: validate
script:
- npm run lint
unit-test:
stage: test
script:
- npm test
Faites un push à nouveau et vérifiez que le job apparait bien dans votre pipeline et qu'il lance les tests.
Nous allons maintenant permettre de release une version de notre API via notre Pipeline. Ce Job doit alors être manuel pour nous laisser la possibilité d'activer la release au bon moment.
Par sécurité nous allons ajouter des Rules pour éviter que le Job Release se lance sur d'autres branches que main.
Pour créer une nouvelle version nous allons utiliser Release-it :
npm init release-it
Choisissez les réponses "Yes" et "package.json" à l'installation.
Ajoutez dans le package.json dans le champ "release-it" les informations "git" suivantes :
Voir à l'étape suivante l'ajout au .gitlab-ci.yml du Stage Release.
// package.json
// ...
"dependencies": {
"express": "^4.18.2"
},
"release-it": {
"$schema": "https://unpkg.com/release-it/schema/release-it.json",
"gitlab": {
"release": true
},
"git": {
"commitMessage": "chore: release v${version}"
}
}
// ...// .gitlab-ci.yml
// ...
unit-test:
stage: test
script:
- npm test
release:
stage: release
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"
&& $CI_COMMIT_TAG == null
&& $CI_COMMIT_TITLE !~ /^chore: release/'
before_script:
- git config user.email $GITLAB_USER_EMAIL
- git config user.name $GITLAB_USER_NAME
- git remote set-url origin
"https://gitlab-ci-token:$GITLAB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git"
- git checkout $CI_COMMIT_BRANCH
- git pull origin $CI_COMMIT_BRANCH --rebase
script:
- npx --yes release-it --ci// Permet de spécifier que ce Job sera activé manuellement.
when: manual
// Permet d'ajouter des rules au Job pour l'afficher ou non grâce au if.
// Ici le commit doit être sur la branche main, cela ne doit pas être un tag
// et le title du commit doit être différent de ce format.
rules:
- if: '$CI_COMMIT_BRANCH == "main"
&& $CI_COMMIT_TAG == null
&& $CI_COMMIT_TITLE !~ /^chore: release/'Quelques explications :
Ensuite, pour fonctionner, Release-it a besoin d'une variable d'environnement GITLAB_TOKEN. Pour cela nous allons devoir l'ajouter manuellement, mettez votre souris sur Settings et cliquez sur CI / CD, ensuite ouvrez le menu Variables. Ouvrez sur une autre page vos Access Tokens en mettant votre souris sur votre profil en haut à gauche, puis en cliquant sur Préférences, là vous devez cliquer sur Access Tokens et générer un nouveau Token avec les droits api.
Ajoutez ce token dans une nouvelle variable CI / CD de votre projet GITLAB_TOKEN.
Faites un push et lancez la release.
Appelez-moi quand vous avez terminé pour que l'on valide ensemble le TP.
Nous avons un problème, ici après la release, deux pipelines se lancent, cela est inutile.
Vous devez retirer les étapes validate et test en ajoutant des rules aux Jobs pour qu'ils ne se lancent pas lorsqu'un tag est créé ou que le commit est le commit de release "chore: release".
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Préparons un nouveau Job e2e-test dans le Stage test, pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello E2E !"
Il doit être visible uniquement dans les Merge Requests (Pull Requests).
Appelez-moi pour que l'on puisse valider ensemble.
Préparons un nouveau Job only-canary dans le Stage validate, qui ne doit se lancer que quand l'on lance la pipeline pour l'environnement Canary, pour cela, nous allons créer une variable d'environnement "ENV_TARGET" qui doit être égale à "canary" pour que le Job se lance.
Pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello Only Canary !"
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Préparons un nouveau Job integration-test dans le Stage test, qui ne doit se lancer que quand le Job unit-test réussi, de même pour e2e-test qui ne doit maintenant se lancer que quand integration-test réussi.
Pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello Integration !"
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Durant les derniers exercices, nous avons souvent appliqué le même comportement à plusieurs Jobs. Pour éviter la duplication de code et que le fichier soit plus maintenable, utilisons les Anchors en YAML pour pouvoir faire hériter à nos Jobs une configuration.
Appelez-moi pour que l'on puisse valider ensemble.
Maintenant que vous avez terminé l'intégration continue, nous allons maintenant voir le côté déploiement continu.
Pour cela, nous allons rajouter les étapes de builds pour l'image Docker et du déploiement de celle-ci.
Vous devez réaliser un fichier Dockerfile pour build le projet.
Ajouter l'étape suivante build-image :
# If your language needs to build you can create another build job before
# Add docker job to build the image
# Only for testing purpose, you can add a docker run + command to check health
# Gitlab has a lot of variables to use in the pipeline
# Here we are using $CI_REGISTRY_IMAGE and $CI_COMMIT_SHORT_SHA
build-image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- docker image build -t $IMAGE_TAG .
Ici, nous utilisons un service spécial docker:20.10.16-dind pour permettre l'utilisation de Docker dans un container Docker.
On pourrait ici build plusieurs images si besoin durant cette étape.
Vérifiez que cette étape fonctionne en réalisant un push sur votre branche.
Ajoutez ensuite l'étape deploy-image :
deploy-image:
needs:
- release
image: docker:20.10.16
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null && $CI_COMMIT_TITLE !~ /^chore: release/'
services:
- docker:20.10.16-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker image build --platform=linux/amd64 -t $IMAGE_TAG -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE --all-tags
environment: productionVérifiez après avoir push à nouveau les changements que votre image est bien ajoutée dans votre Container Registry en allant dans le menu Deploy sur votre dépôt Gitlab. C'est terminé !
Voici un exemple pour déployer sur un serveur distant, ceci n'est pas la meilleure méthode mais elle est très simple.
deploy:
needs:
- deploy-image
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null && $CI_COMMIT_TITLE !~ /^chore\(release\): publish/ && $CI_COMMIT_TITLE !~ /^chore\(pre-release\): publish/'
before_script:
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
script:
- ssh-add <(echo "$SSH_PRIVATE")
- ssh -o StrictHostKeyChecking=no "$SSH_SERVER" 'which docker || (sudo yum update -y && sudo yum install -y docker && sudo service docker start)'
- <SAME> "sudo docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY"
- <SAME> "(sudo docker rm -f example || true) && sudo docker run -p 80:3000 -d --name example registry.gitlab.com/vm-marvelab/docker-cicd:$CI_COMMIT_SHORT_SHA"
environment: production– Conrad Anker
Découvrons ensemble Github Actions, pour cela nous allons recommencer ce TP en utilisant uniquement Github.
Je vous invite à créer un compte Github si ce n'est pas déjà le cas pour la suite du TP.
name: Node.js CI
on:
push:
branches: ['main']
pull_request:
branches: ['main']
jobs:
build:
runs-on:
ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install
run: npm ci --cache .npm --prefer-offline
- name: Validate
run: npm run lint
- name: Test
run: npm test
- name: Build for production
run: npm run build
- uses: actions/upload-artifact@master
with:
name: web
path: ./distGithub Actions
Maintenant que vous êtes plus à l'aise avec l'intégration continue, vous serez en autonomie, suivez la documentation https://docs.github.com/fr/actions pour réaliser le même TP qu'avec Gitlab CI, vous devez retrouver via la documentation de Github Actions comment refaire les étapes qu'on a réalisé avec Gitlab.
N'hésitez pas à m'appeler en cas de problème.
– Conrad Anker