Skip to content

Configuration de CI/CD

TECHNIQUE

Introduction

Ce document à pour but de détailler la configuration d'un pipeline CI/CD Gitlab pour déployer une application Angular et une application NestJS sur un serveur VPS de manière automatisée.

Nous verrons deux solutions, la première sans Docker et la deuxième avec Docker -> Solution 2 CI/CD GitLab (avec Docker)

Génération de la clé SSH

Pour que gitlab puisse se connecter au vps nous utiliserons un utilisateur nommé "root", l'ip du serveur qui sera "1.1.1.1" et une clé SSH pour se connecter sans mot de passe.

Pensez à bien installer toutes les dépendances nécessaires (vues dans les autres cours) sur le vps.

Sur votre serveur, générez une clé SSH

ssh-keygen -t ed25519 -C "your_email@example.com"

Ajoutez la clé privé en tant que variable d'environnement dans GitLab

Pour l'afficher ->

cat ~/.ssh/id_ed25519

"$SSH_PRIVATE_KEY" est une variable d'environnement que nous définirons sur GITLAB dans CI/CD Config -> Variables -> Add new variable

Ensuite ajouter la clé publique dans le fichier autorized_keys du serveur

cat ~/.ssh/id_ed25519.pub > ~/.ssh/authorized_keys

Solution 1 (sans Docker)

Configuration du pipeline

Créer un fichier .gitlab-ci.yml à la racine du projet sur gitlab

Info

Il faut savoir que Gitlab nous met à disposition des runners pour exécuter les jobs de notre pipeline, mais il est aussi possible d'utiliser des runners personnalisés, c'est à dire des machines que nous possédons et sur lesquelles nous avons installé un runner Gitlab.

En d'autre terme, tout ce qui se passe dans le pipeline se passe sur les serveurs de gitlab.

stages: # Les différents stages du pipeline
    - build
    - deploy

build:
    stage: build # lancé quand le stage build est appelé
    image: node:latest # Image docker à utiliser (image publique)
    before_script:
        - npm install -g @nestjs/cli
        - npm install -g @angular/cli
    script:
        - echo "BUILD FRONT"
        - cd front
        - npm ci
        - npm install
        - ng build

        - echo "BUILD BACK"
        - cd ../server
        - npm install
        - nest build

    artifacts: # Les fichiers à conserver pour les stages suivants
        paths:
            - front/

Ici, nous avons un pipeline avec un seul stage, le stage build. Ce stage est composé d'un job build. Quand le job est est lancé construit l'application Angular et l'application NestJS. Les fichiers générés par ces builds sont ensuite conservés pour le stage suivant, le stage deploy.

Info

Imaginez que vous ayiez un back express.js, il n'y à pas de build possible, cependant pour tout type d'applications (front ou back) qui peut se build, il est très recommandé de lancer la pipeline de build à chaque push, peu importe la branche, pour voir des erreurs que le simple run start ne vous renverrait pas.

Maintenant, nous allons créer le stage le job deploy.

deploy:
    stage: deploy
    only: # Les branches sur lesquelles le pipeline doit se déclencher
    - master
    dependencies: # Les stages sur lesquels ce stage dépend
    - build 
    before_script:
        # Tout ce qui est nécessaire pour se connecter au serveur grâce à SSH sans mot de passe
        - "command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )" # Installe ssh-agent si ce n'est pas
        - eval $(ssh-agent -s) # Lance ssh-agent
        - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null # Ajoute la clé privée SSH
        - mkdir -p ~/.ssh  # Crée le dossier .ssh
        - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config # Désactive la vérification de l'empreinte SSH

    script:
        - echo "DEPLOY FRONT"
        # remplacez root et ipVPS par votre utilisateur(root est user sudo de base) et ip de votre serveur
        # ssh root@192.13.145.21 "sudo rm -rf /var/www/html/angular/*"
        - ssh root@ipVPS "sudo rm -rf /var/www/html/angular/*" # Supprime le build existant
        - scp -r -p front/dist/* root@1.1.1.1:/var/www/html/angular/ # Copie le build de la CI sur le serveur
        - ssh root@ipVPS "sudo nginx -t" # Vérifie la syntaxe de la configuration Nginx
        - ssh root@1.1.1.1 "sudo systemctl restart nginx" # Redémarre Nginx

        - echo "DEPLOY BACK"
        - ssh root@ipVPS "sudo rm -rf /server/*" # Supprime le build existant
        - scp -r -p server/* root@1.1.1.1:/server/ # Copie le server de la CI sur le serveur
        - ssh root@ipVPS "cd /server && npm install" # Installe les dépendances
        - ssh root@ipVPS "cd /server && npm run build:env" # Build le projet
        - ssh root@ipVPS "cd /server/dist && pm2 start main.js --name nomDeVotreApi" # Démarre le serveur avec pm2
Documention -> pm2

Dans ce stage, nous avons un job deploy. Ce job est lancé uniquement sur la branche master. Il dépend du stage build. Avant de lancer le script, nous devons nous connecter au serveur grâce à SSH sans mot de passe. Pour cela, nous utilisons les variables d'environnement que nous avons définies dans Gitlab. Ensuite, nous supprimons les builds existants sur le serveur, nous copions les builds de la CI (ceux du stage build) sur le serveur, nous vérifions la syntaxe de la configuration Nginx et nous redémarrons Nginx (pour le front) et nous lançons l'api.

Normalement, si tout est bien configuré, à chaque push sur la branche master, le pipeline se déclenchera et le build de l'application Angular sera copié sur le serveur, la syntaxe de la configuration Nginx sera vérifiée et Nginx sera redémarré. Vous pouvez garder la configuration nginx qu'on a vue précedemment Votre application Angular est maintenant en ligne.

Ensuite le build de l'application NestJS sera copié sur le serveur et l'api sera redémarré. Nous copions aussi le dossier node_modules car il peut être nécessaire pour le bon fonctionnement de l'application nest.

Il existe de nombreuses configuration de pipeline gitlab, je vous invite à consulter la documentation officielle pour plus d'informations.

Solution 2 CI/CD GitLab (avec Docker)

Configuration du pipeline

Info

Pour cette solution, nous allons utiliser Docker pour construire l'image d'une application NestJS et la publier sur le registre Docker de Gitlab. Ensuite, nous allons déployer cette image sur le serveur VPS. Nous allons faire ce procédé uniquement pour une applications nestJs pour simplifier les choses.

Nous pouvons garder le même build que précédemment, mais nous allons ajouter deux stages, un pour le build de l'image Docker et un pour le déploiement de l'image Docker sur le serveur.

Créer un fichier .gitlab-ci.yml à la racine du projet sur gitlab

stages: # Les différents stages du pipeline
    - build
    - publish
    - deploy

variables: 
  IMAGE_TAG: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}

build:
  stage: build
  image: node:latest
  before_script:
    - npm install -g @nestjs/cli
  script:
    - echo "BUILD BACK"
    - npm install
    - nest build
  artifacts:
    paths:
      - dist/
      - node_modules/
    expire_in: 2 hours

Dans ce stage, nous avons un job build. Ce job est lancé à chaque push sur n'importe quelle branche. Il construit l'application NestJS. Les fichiers générés par ce build sont ensuite conservés pour les stages suivants.

Info

Comme vous pouvez le voir, nous avons ajouté une variable IMAGE_TAG. Cette variable est utilisée pour taguer l'image Docker que nous allons construire. Elle sera disponible dans tous les stages du pipeline. Pour générer cette variable, nous utilisons des variables prédéfinies par Gitlab. ${CI_REGISTRY_IMAGE} est l'URL du registre Docker de Gitlab et ${CI_COMMIT_REF_SLUG} est le nom de la branche sur laquelle le pipeline est déclenché.

Nous avons aussi ajouté une expiration pour les fichiers générés par le build, sinon gitlab conserverait ces fichiers pour toujours.

Avant de continuer nous devons créer un fichier Dockerfile à la racine du projet.

FROM node:20.9

WORKDIR /srv/[YOUR_APP]

COPY node_modules ./node_modules
COPY dist ./

EXPOSE 3000

CMD [ "node", "main.js" ]

Comme vous pouvez le voir, nous utilisons l'image node:20.9 comme image de base. Nous copions les fichiers node_modules et dist générés par le build dans le conteneur. Nous exposons le port 3000 et nous lançons l'application NestJS.

Ce Dockerfile sera utilisé un petit peu plus tard.

Maintenant, nous allons créer le stage publish ->

publish:
  stage: publish
  image: docker:24.0.5
  dependencies: # Les stages sur lesquels ce stage dépend
    - build
  services: # Les services nécessaires pour ce stage
    - docker:dind 
  before_script: 
    - echo "${CI_REGISTRY_PASSWORD}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY} 
  script:
    - echo "PUBLISH BACK"
    - docker build -t ${IMAGE_TAG} .
    - docker push ${IMAGE_TAG}
  only:
    - master

La commande dans le before_script permet de se connecter au registre Docker de Gitlab. ${CI_REGISTRY_PASSWORD} et ${CI_REGISTRY_USER} sont des variables prédéfinies par Gitlab. Elles sont utilisées pour se connecter au registre Docker de Gitlab. ${CI_REGISTRY} est l'URL du registre Docker de Gitlab.

La commande docker build -t ${IMAGE_TAG} . construit une image Docker pour votre projet en utilisant le Dockerfile situé dans le répertoire courant du projet (indiqué par .) La commande docker push ${IMAGE_TAG} publie l'image Docker sur le registre Docker de Gitlab.

Note

Dans ce stage, nous avons un job publish. Ce job est lancé uniquement sur la branche master. Il dépend du stage build. Avant de lancer le script, nous devons nous connecter au registre Docker de Gitlab. Ensuite, nous construisons l'image Docker de l'application NestJS et nous la publions sur le registre Docker de Gitlab.

Maintenant, nous allons créer le stage deploy ->

deploy:
  stage: deploy
  image: docker:latest
  before_script:
    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
  script:
    - echo "DEPLOY BACK"
    - ssh ${VPS_USER}@${VPS_IP} "docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} && docker pull ${IMAGE_TAG} && docker stop nest-app || true && docker rm nest-app || true && docker run -d --name nest-app --network degree-dealers -p 3000:3000 -e PORT=${API_PORT} -e DATABASE_TYPE=${DB_TYPE} -e POSTGRES_HOST=${DB_HOST} -e POSTGRES_USERNAME=${DB_USERNAME} -e POSTGRES_PASSWORD=${DB_PASSWORD} -e POSTGRES_DBNAME=${DB_NAME} ${IMAGE_TAG}"
  only:
    - master

Toutes les commandes présentes dans le before_script sont nécessaires pour se connecter au serveur grâce à SSH sans mot de passe.

La commande dans le script permet d'abord de se connecter au serveur grâce à SSH sans mot de passe.

Ensuite sur notre serveur nous nous connectons au registre Docker de Gitlab avec docker login ..., nous téléchargeons l'image Docker de l'application NestJS avec docker pull ..., nous arrêtons et supprimons le conteneur existant avec docker stop ... puis docker rm ... (même s'il n'existe pas encore), nous lançons un nouveau conteneur avec la nouvelle image Docker avec docker run ....

Info

Il aurait tout à fait été possible de créer un fichier docker-compose.yml pour lancer le conteneur, mais pour simplifier les choses nous avons utilisé une commande docker run.

Dans ce stage, nous avons un job deploy. Ce job est lancé uniquement sur la branche master. Il dépend du stage publish. Avant de lancer le script, nous devons nous connecter au serveur. Ensuite, nous nous connectons au registre Docker de Gitlab (sur notre serveur), nous téléchargeons l'image Docker de l'application NestJS, nous arrêtons et supprimons le conteneur existant, nous lançons un nouveau conteneur avec la nouvelle image Docker.

Noubliez pas

Rappelez vous que dans le Dockerfile, il y avait cette ligne CMD [ "node", "main.js" ] qui permet de lancer l'application NestJS. Nous n'avons pas besoin de lancer l'application manuellement, elle se lancera donc automatiquement quand le conteneur sera lancé.

Résumé

Pour faire court, à chaque push sur la branche master, le pipeline se déclenchera. Un job nommé build sera lancé. Il construira l'application NestJS. Ensuite -> Un job nommé publish sera lancé. Il construira l'image Docker de l'application NestJS et la publiera sur le registre Docker de Gitlab. Pour finir -> Un job nommé deploy sera lancé. Il se connectera à votre serveur, d'ici il se connectera au registre Docker de Gitlab, téléchargera l'image Docker de l'application NestJS, arrêtera et supprimera le conteneur existant, lancera un nouveau conteneur avec la nouvelle image Docker.

Bravo

Vous avez maintenant un pipeline CI/CD Gitlab qui construit l'application NestJS, publie l'image Docker de l'application NestJS sur le registre Docker de Gitlab et déploie cette image sur votre serveur.

Il existe de nombreuses configuration de pipeline gitlab, je vous invite à consulter la documentation officielle pour plus d'informations.