Blog : le CI/CD avec Gitlab CI
- blog
- gitlab ci
- ci/cd
- devops
Introduction
Pour ce quatrième article sur le blog nous allons aborder la CI/CD avec Gitlab CI.
Ayant déjà travaillé un peu avec Jenkins pour faire du pipeline déclenché par webhook gitlab, je me suis posé la question de l'outil à utiliser pour la CI/CD, et j'ai choisi Gitlab CI.
Les avantages que j'ai vu avec Gitlab CI :
- ayant déjà un serveur Gitlab, je n'avais rien besoin d'installer (mis à part un runner, et un peu de configuration)
- naturellement bien intégré à Gitlab (pas besoin de webhook)
- bonne intégration avec Docker
- nouvelle techno à apprendre
Je tiens à dire que le blog saltycrane et notamment l'article Next.js GitLab CI/CD Docker multi-stage example m'ont beaucoup inspiré pour cette partie.
Ci-dessous un schéma qui représente les stages du pipeline CI/CD opérés par Gitlab CI :
Modèle de branche
Le modèle de branche est basique avec deux branches :
- la branche "dev" qui est la branche de travail pour le développement et de la rédaction de nouveaux articles. La branche dev est la branche par défaut dans Gitlab
- la branche "production" qui représente l'état du blog en production
Les étapes :
- on crée la branche "dev" à partir de la branche "production"
- on développe ou on rédige un article sur la branche dev, puis on push le change et le pipeline de CI/CD se déclenche
- quand les étapes build, test, release et deploy sont terminées avec succès, la dernière étape du pipeline consiste à merge "dev" dans "production". La branche dev est maintenue après le merge
Gitlab CI
Gitlab CI runner
Pour utiliser Gitlab CI il faut disposer à minima d'un runner. Le runner est le composant qui va prendre en charge l'exécution du pipeline. Plusieurs types de runner existent et il est possible de tager un runner pour pouvoir ensuite le sélectionner dans les stages du pipeline. J'ai donc choisi d'installer un runner linux avec executor Docker (cf doc officielle) car mon application est conteneurisée, et c'est bien plus pratique d'utiliser du Docker 😉.
Variables
Les variables permettent de stocker des données sensibles en dehors du fichier de pipeline .gitlab-ci.yml, et se trouvent au niveau du projet dans Settings > CI/CD > Variables.
Key | Description |
---|---|
AWS_ACCESS_KEY_ID | Nécessaire pour l'accès programmatique à AWS |
AWS_DEFAULT_REGION | Région du compte AWS utilisé |
AWS_ECR_REGISTRY | Identifiant du registry ECR |
AWS_SECRET_ACCESS_KEY | Nécessaire pour l'accès programmatique à AWS |
CI_PRIVATE_KEY | Clé privée du runner |
CI_PUBLIC_KEY | Clé publique du runner |
GITLAB_PUBLIC_KEY | Clé publique de gitlab |
Pipeline
Le pipeline est décri dans le fichier ".gitlab-ci.yml" qui doit être à la racine du projet. Un pipeline est composé de stages qui sont composés de job.
variables:
# enable docker buildkit. Used with `BUILDKIT_INLINE_CACHE=1` below
DOCKER_BUILDKIT: 1
# DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TEST: $CI_REGISTRY_IMAGE/test:latest
# IMAGE_CYPRESS: $CI_REGISTRY_IMAGE/cypress:latest
IMAGE_DEPLOY: $CI_REGISTRY_IMAGE/deploy:latest
stages:
- build
- test
- release
- deploy
.dockerbase:
image: docker:latest
tags:
- shared
only:
- dev
services:
- docker:dind
before_script:
- docker --version
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
.alpinebase:
image: alpine:latest
tags:
- shared
only:
- dev
before_script:
- cat /etc/os-release
build:buildapp:
extends: .dockerbase
tags:
- shared
stage: build
only:
- dev
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $IMAGE_TEST --target app-builder -t $IMAGE_TEST .
- docker push $IMAGE_TEST
test:eslint:
extends: .dockerbase
tags:
- shared
stage: test
needs: ["build:buildapp"]
script:
- docker run $IMAGE_TEST npm run lint
release:buildimage:
extends: .dockerbase
tags:
- shared
stage: release
only:
- dev
needs: ["test:eslint"]
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $IMAGE_DEPLOY --cache-from $IMAGE_TEST -t $IMAGE_DEPLOY .
- docker push $IMAGE_DEPLOY
release:releaseimage:
extends: .dockerbase
tags:
- shared
stage: release
only:
- dev
needs: ["release:buildimage"]
script:
- docker pull $IMAGE_DEPLOY
- apk add --no-cache curl jq python3 py3-pip
- pip install awscli --break-system-packages
- aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
- aws configure set aws_secret_access_key AWS_SECRET_ACCESS_KEY
- aws configure set region $AWS_DEFAULT_REGION
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login $AWS_ECR_REGISTRY -u AWS --password-stdin
- docker tag $IMAGE_DEPLOY $AWS_ECR_REGISTRY:latest
- docker push $AWS_ECR_REGISTRY:latest
deploy:deployaws:
extends: .alpinebase
tags:
- shared
stage: deploy
only:
- dev
needs: ["release:releaseimage"]
script:
- apk add --no-cache curl jq python3 py3-pip
- pip install awscli --break-system-packages
- aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
- aws configure set aws_secret_access_key AWS_SECRET_ACCESS_KEY
- aws configure set region $AWS_DEFAULT_REGION
- aws ecs update-service --cluster ECS-Cluster --service Service-ledevdanslesnuages --task-definition task-ledevdanslesnuages --force-new-deployment
deploy:gitmerge:
extends: .alpinebase
tags:
- shared
stage: deploy
only:
- dev
needs: ["deploy:deployaws"]
script:
- apk add --no-cache git openssh-client
- mkdir ~/.ssh
- echo "$CI_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- echo "$CI_PUBLIC_KEY" > ~/.ssh/id_rsa.pub
- echo "$GITLAB_PUBLIC_KEY" > ~/.ssh/known_hosts
- git clone git@gitlab.blr:root/ledevdanslesnuages-blog.git
- cd ledevdanslesnuages-blog/
- git checkout production
- git merge dev
- git push
- variables : section pour définir des variables d'environnement :
DOCKER_BUILDKIT: 1
: active le docker buildkit qui optimise les performances de buildIMAGE_TEST: $CI_REGISTRY_IMAGE/test:latest
: correspond à l'image pour les tests sur le registryIMAGE_DEPLOY: $CI_REGISTRY_IMAGE/deploy:latest
: correspond à l'image pour le deploiement sur le registry
- stages : liste les stages du pipeline. Cette liste sera représentée graphiquement dans l'interface Gitlab lors de l'exécution du pipeline
- .dockerbase : définition de l'image de base Docker. Cette base servira pour les opérations Docker (pull, build, tag, push, login)
- en service
docker:dind
: Docker in Docker (dind) permet comme son nom l'indique d'utiliser Docker dans Docker 😄 - en before_script
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
: pour se connecter au registry Gitlab Container Registry
- en service
- .alpinebase : définition de l'image de base alpine. Cette base servira pour les opérations non Docker
- concernant les jobs qui suivent :
- ils sont réalisés séquentiellement, c'est à dire que chaque job a comme prérequis le job précédent via la propriété "needs"
- ils sont bloquants, ce qui signifie que si le job failed, l'exécution du pipeline s'arrête et son statut est "failed"
- ils ont le tag "shared" qui correspond au tag du runner
- la propriété "extends" permet d'utiliser l'image de base souhaitée
- la propriété "only" permet de cibler uniquement la branche "dev"
Stage Build
- build:buildapp : dans ce job on build l'application en ciblant la target "app-builder" du Dockerfile. On parle ici de build au sens NextJS et SSG ("next export"). On a donc en sortie de ce build les fichiers statiques du blog à servir sur le serveur web.
Stage Test
- test:eslint : dans ce job on vient tester l'application buildée. Au moment où cet article est rédigé, cette partie n'est pas aboutie car on vient faire un
npm run lint
alors que le lint est déjà fait implicitement lors du build à l'étape précédente. Il faudrait ici faire un vrai test end to end, typiquement avec Cypress. To be continued...
Stage Release
- release:buildimage : ici on vient builder l'image Docker qui correspond à l'application finale sous nginx, et pousser l'image sur le Gitlab Container Registry
- release:releaseimage : dans ce job on récupère l'image précédemment buildée, on la tag et la push dans le registry ECR
Stage Deploy
- deploy:deployaws : ce job met à jour le service ECS avec la nouvelle image déposée dans ECR lors du job précédent. La mise à jour du service ECS va redéployer le container
- deploy:gitmerge : dans la dernière étape du pipeline, on vient merger la branche "dev" dans "production" pour prendre en compte dans le repo Git l'état du blog
Le mot de la fin
Certaines parties de ce pipeline restent a compléter comme les tests (avec Cypress) ou à améliorer comme le temps d'exécution (le pipeline actuel dure ~5min), mais il est vraiment plaisant d'envoyer un nouvel article en faisant un push sur le repo Git, puis de voir le pipeline prendre le relais et mettre à jour le blog automatiquement.