Aller au contenu

TP : Conteneurisation de votre application Node.js et intégration dans GitLab CI

Ce TP a pour objectif de vous faire pratiquer la création d'images Docker pour une application Node.js et l'automatisation de sa construction dans GitLab CI

Objectifs du TP

  • Rédiger un Dockerfile pour une application Node.js
  • Optimiser une image Docker avec le multi-stage build
  • Comprendre et utiliser le Docker-in-Docker (DinD) ou Kaniko dans GitLab CI
  • Automatiser la création de l'image Docker dans le pipeline CI

Étape 1 : Création du Dockerfile basique

Nous allons créer une image Docker pour l'application Node.js développée au TP précédent

  1. À la racine de votre projet, créez un fichier nommé Dockerfile
  2. Utilisez l'image de base node:20-alpine (plus légère que l'image node standard)
  3. Définissez le répertoire de travail dans le conteneur avec WORKDIR (par exemple /usr/src/app)
  4. Copiez les fichiers package.json et package-lock.json ou yarn.lock
  5. Exécutez la commande d'installation des dépendances avec RUN npm ci
  6. Copiez le reste de votre code source dans le conteneur
  7. Exposez le port 3000 utilisé par votre application avec EXPOSE
  8. Définissez la commande de démarrage de votre application avec CMD
  9. N'oubliez pas d'ajouter un fichier .dockerignore contenant node_modules et .git pour ne pas alourdir le contexte de build

Étape 2 : Test de l'image localement (Optionnel)

Si vous disposez de Docker sur votre machine, vous pouvez vérifier que l'image se construit correctement avant de l'envoyer sur GitLab

  1. Construisez votre image en lui donnant un nom avec la commande docker build -t mon-app-node .
  2. Lancez un conteneur à partir de cette image avec docker run -p 3000:3000 mon-app-node
  3. Vérifiez dans votre navigateur que l'application répond sur http://localhost:3000

Étape 3 : Optimisation avec le Multi-stage Build

Pour avoir une image de production la plus légère possible, nous allons séparer l'étape de build de l'étape d'exécution

  1. Modifiez votre Dockerfile pour introduire une première étape nommée builder (ex: FROM node:20-alpine as builder)
  2. Dans cette étape builder, installez toutes les dépendances et générez les fichiers nécessaires au bon fonctionnement de votre application
  3. Créez une seconde étape en utilisant à nouveau node:20-alpine
  4. Définissez la variable d'environnement NODE_ENV à production
  5. Utilisez l'instruction COPY --from=builder pour ne récupérer que le code compilé et les dossiers nécessaires depuis l'étape précédente
  6. Pour des raisons de sécurité (principe du moindre privilège), configurez le conteneur pour s'exécuter en tant qu'utilisateur non-privilégié en ajoutant la directive USER node juste avant l'instruction CMD (l'utilisateur node est déjà présent dans l'image de base Alpine)
  7. Observez la réduction de taille de l'image finale si vous la buildez localement

Étape 4 : Automatisation de la construction de l'image sur GitLab CI

Maintenant que le Dockerfile est prêt, nous allons demander à GitLab CI de construire notre image Docker à chaque modification du code

Nous utiliserons la méthode Kaniko qui est plus sécurisée et recommandée pour les environnements Kubernetes

  1. Dans votre fichier .gitlab-ci.yml, ajoutez une étape build_image (ou similaire) à vos stages existants
  2. Créez un job build_docker_image rattaché à cette nouvelle étape
  3. Utilisez l'image gcr.io/kaniko-project/executor:debug pour ce job avec l'entrypoint vide [""]
  4. Dans la section script, ajoutez la commande pour configurer l'authentification au registre GitLab via un echo vers /kaniko/.docker/config.json
  5. Lancez l'exécutable Kaniko pour construire l'image en lui fournissant le chemin du Dockerfile ($CI_PROJECT_DIR/Dockerfile), le contexte ($CI_PROJECT_DIR) et la destination (par exemple $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA)
  6. Commitez vos changements et vérifiez dans l'interface GitLab que le pipeline réussit et que l'image est bien poussée dans le Container Registry de votre projet

Étape 5 : Validation de la qualité et de la sécurité (Linting)

Pour garantir la qualité et la sécurité de notre Dockerfile dans un cadre industriel, nous allons ajouter une étape de validation automatique dans notre pipeline CI. Trois approches courantes sont possibles (vous pouvez choisir l'une d'entre elles) :

Option A : Hadolint (Linter historique et spécialisé)

  1. Dans votre .gitlab-ci.yml, ajoutez une étape lint au tout début de vos stages existants
  2. Créez un job nommé lint_hadolint rattaché à cette étape
  3. Utilisez l'image officielle hadolint/hadolint:latest-alpine
  4. Dans la section script, lancez la commande hadolint Dockerfile

Option B : Trivy (Scanner de vulnérabilités et de conformité)

Trivy permet de scanner le Dockerfile pour y détecter des failles de sécurité ou des non-conformités. 1. Créez un job nommé lint_trivy rattaché à l'étape lint 2. Utilisez l'image aquasec/trivy:latest 3. Dans la section script, lancez la commande trivy config Dockerfile (ou trivy config . pour scanner tout le projet)

Option C : Docker build --check (Validation native)

Docker permet de vérifier la syntaxe et le respect des bonnes pratiques directement via la CLI sans construire l'image. 1. Créez un job nommé lint_docker_check rattaché à l'étape lint 2. Utilisez l'image docker:latest avec le service docker:dind 3. Définissez la variable DOCKER_HOST à tcp://docker:2375 4. Dans la section script, lancez la commande docker build --check .


Exercice : Choisissez et configurez l'une des méthodes ci-dessus, puis poussez vos modifications et vérifiez le résultat dans GitLab CI. Si des alertes sont remontées, corrigez votre Dockerfile en conséquence

Étape 6 : Optimisation du cache Kaniko et stratégie de Tagging

Dans un contexte professionnel, il est crucial de réduire le temps d'exécution des pipelines et d'adopter un nommage clair et structuré de nos images Docker

  1. Modifiez le script de votre job Kaniko pour y ajouter l'argument de cache --cache=true ainsi que la destination du cache --cache-repo=$CI_REGISTRY_IMAGE/cache
  2. Lancez le pipeline deux fois de suite sans modifier le Dockerfile et observez la réduction du temps d'exécution lors du second passage
  3. Modifiez la destination du build pour pousser deux tags différents à chaque build : l'image avec le SHA du commit ($CI_REGISTRY_IMAGE:$CI_COMMIT_SHA) et une image avec le nom de la branche ($CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG) (Kaniko permet de spécifier plusieurs arguments --destination)
  4. Optionnel : Ajoutez une règle de pipeline (rules) pour que l'image soit tagguée avec la version de la release ($CI_REGISTRY_IMAGE:$CI_COMMIT_TAG) uniquement lorsqu'un tag Git (ex: v1.0.0) est créé

Étape 7 : Publication sur un registre externe privé (Docker Hub)

Dans de nombreux projets industriels, les images ne sont pas stockées sur le registre GitLab intégré, mais sur un registre externe d'entreprise (Docker Hub, Harbor, AWS ECR, etc.). Nous allons configurer notre pipeline pour pousser sur Docker Hub.

  1. Créez un compte gratuit sur Docker Hub si vous n'en avez pas déjà un
  2. Générez un jeton d'accès personnel (Personal Access Token - PAT) dans les paramètres de votre compte Docker Hub (onglet Security > Access Tokens)
  3. Dans GitLab, allez dans Settings > CI/CD > Variables et ajoutez deux variables masquées :
  4. DOCKER_HUB_USER : Votre nom d'utilisateur Docker Hub
  5. DOCKER_HUB_TOKEN : Le jeton d'accès généré (ne jamais utiliser votre mot de passe en clair dans la CI)
  6. Modifiez votre job Kaniko (ou créez-en un nouveau) pour qu'il génère le fichier /kaniko/.docker/config.json avec les informations d'authentification de Docker Hub (l'URL du registre par défaut est https://index.docker.io/v1/)
  7. Modifiez le paramètre --destination de Kaniko pour cibler votre dépôt Docker Hub : docker.io/<votre_utilisateur_dockerhub>/<nom_du_depot>:$CI_COMMIT_SHA
  8. Lancez le pipeline et vérifiez que l'image est bien présente sur votre compte Docker Hub

Étape 8 : Bonus - Exploration d'une alternative moderne (Buildah ou BuildKit)

Pour dépasser les limites de sécurité de DinD et préparer l'après-Kaniko, essayez de configurer un job de build utilisant l'un des standards modernes de l'industrie

Option A : Utilisation de Buildah (Recommandé)

  1. Créez un job build_buildah dans votre .gitlab-ci.yml
  2. Utilisez l'image quay.io/buildah/stable:latest
  3. Dans la section script, connectez-vous au registre GitLab avec buildah login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  4. Lancez la construction de l'image avec buildah bud -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
  5. Poussez l'image avec buildah push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Option B : Utilisation de BuildKit Rootless

  1. Créez un job build_buildkit dans votre .gitlab-ci.yml
  2. Utilisez l'image moby/buildkit:latest-rootless avec l'entrypoint vide [""]
  3. Définissez la variable BUILDKITD_FLAGS à --oci-worker-no-process-sandbox
  4. Configurez le fichier d'authentification ~/.docker/config.json avec vos identifiants de registre
  5. Lancez la commande de build sans démon : buildctl-daemonless.sh build --frontend dockerfile.v0 --local context=. --local dockerfile=. --output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true

Option C : Utilisation de Cloud Native Buildpacks (Zéro Dockerfile)

Les Buildpacks permettent de construire une image sans écrire ni maintenir de Dockerfile, en détectant automatiquement la pile technologique (Node.js). 1. Créez un job build_buildpack dans votre .gitlab-ci.yml 2. Utilisez l'image buildpacksio/pack:latest et déclarez le service docker:dind (la CLI pack a besoin d'un démon Docker pour lancer le build) 3. Définissez la variable DOCKER_HOST à tcp://docker:2375 4. Dans la section script, connectez-vous au registre GitLab avec docker login (comme vu précédemment) 5. Lancez la construction et la publication automatique avec la commande : pack build $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --builder paketo-buildpacks/builder-jammy-base --publish