Skip to content

Niveau 2 : Intégration continue avec Gitlab CI

Auteurs

Assemblé et rédigé par Martin Souchal

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Introduction

L'intégration continue est une méthode de développement de logiciel DevOps avec laquelle les développeurs intègrent régulièrement leurs modifications de code à un référentiel centralisé, suite à quoi des opérations de création et de test sont automatiquement menées. L'intégration continue désigne souvent l'étape de création ou d'intégration du processus de publication de logiciel, et implique un aspect automatisé (un service d'IC ou de création) et un aspect culturel (apprendre à intégrer fréquemment). Les principaux objectifs de l'intégration continue sont de trouver et de corriger plus rapidement les bogues, d'améliorer la qualité des logiciels et de réduire le temps nécessaire pour valider et publier de nouvelles mises à jour de logiciels. AWS

Le but de ce TP est de mettre en place un mécanisme d'intégration continue sous Gitlab dans le but d'automatiser la création d'un conteneur pour une application Django, de générer de la documentation et d'effectuer des tests de qualité sur le code.

Création d'une application Django

Commencez par créer un répertoire vide.

mkdir django-tp

On va ensuite créer un environnement virtuel python dans ce répertoire :

python -m venv django-venv

Maintenant activez cet environnement virtuel :

source django-venv/bin/activate

Votre prompt doit maintenant commencer par (django-venv).

Pour vérifier que votre environnement est bien actif, la commande which python doit renvoyer un chemin dans le dossier actuel.

Si tout est bon, on peut maintenant installer Django :

python -m pip install django

On va maintenant initialiser un projet Django, ici on va l'appeller tp :

django-admin startproject tp
python tp/manage.py makemigrations

Si tout s'est bien passé, vous devez maintenant avoir un répertoire tp qui contient un fichier manage.py. Tapez la commande python tp/manage.py runserver pour lancer Django. Vous pouvez maintenant ouvrir un navigateur sur l'adresse http://127.0.0.1:8000/ et admirer la fusée Django.

On va ensuite personnaliser un peu le code pour afficher le message "Hello, World!". Commencez par créer une application, que l'on va appeller hello dans le projet Django :

cd tp
python manage.py startapp hello

Django à automatiquement crée un répertoire nommé hello qui va contenir le code de notre application. On va ajouter les fichiers suivants :

django-tp/tp/hello/urls.py

1
2
3
4
5
6
7
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='home')
]
et

django-tp/tp/hello/views.py

1
2
3
4
5
6
from django.shortcuts import render
from django.http import HttpResponse


def index(request):
    return HttpResponse('Hello, World!')

et enfin

django-tp/tp/tp/urls.py

"""tp URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('hello.urls')),
]

Maintenant, si tout va bien, vous pouvez lancer le serveur Django qui doit afficher un magnifique "Hello, World!" sur la page http://127.0.0.1:8000/ :

python manage.py runserver

Nous avons maintenant un code en python, utilisant le framework django en local. La prochaine étape, c'est de versionner ce code via git, et ensuite de l'envoyer sur le serveur GitLab.

Gitlab

Commencez par vous connecter sur Gitlab puis créez un nouveau dépot. Ajoutez ensuite votre code dans ce dépôt.

Ajout du code Django dans git

Pour ajouter un répertoire existant dans gitlab, il faut d'abord transformer le répertoire en dépôt git, puis l'envoyer sur gitlab :

1
2
3
4
5
6
7
cd django-tp
git init
# Remplacer "user" par votre nom d'utilisateur GitLab
git remote add origin git@gitlab.in2p3.fr:user/django-tp.git
git add .
git commit -m "Initial commit"
git push -u origin master

Mise en place de l'intégration continue

CICD

Dans Gitlab, la configuration de l'intégration continue se fait dans un fichier texte qui doit impérativement être nommé .gitlab-ci.yml et doit être situé à la racine du code. Ce fichier contient la liste des actions à mener à chaque push sur le dépot git et doit être rédigé dans le langage YAML (attention, ce langage est particulièrement sensible à l'indentation du code). Ces actions seront executées sur un "gitlab-runner", c'est à dire un serveur connecté à Gitlab et qui offre au moins le service docker. L'ensemble des actions définies dans ce fichier constitue un pipeline. En général un pipeline se décompose en trois étapes :

  1. build
  2. test
  3. deploy

Il n'est cependant pas obligatoire d'utiliser ces trois étapes. Dans l'interface Gitlab il est possible de voir l'état des différentes étapes d'un pipeline, ainsi que les logs d'execution. Si il n'y a aucune erreur dans un pipeline, il apparait en vert. Dans le cas contraire, il sera en rouge avec une croix. En cas d'erreur, il est utile de consulter les logs dans Gitlab.

Commencons par créer un fichier .gitlab-ci.yml avec le contenu suivant :

hello-world:
  script: echo "Hello World"

Ajoutez ensuite le fichier dans Git :

git add .gitlab-ci.yml

Puis on commit et on push les modifs dans gitlab :

git commit -m "gitlab CI"
git push

Rendez vous ensuite dans Gitlab, il doit y avoir un pipeline qui s'est lancé automatiquement suite au push. Pour vérifier, allez dans "CI/CD" dans le menu de gauche, puis dans "Pipelines". Il doit y avoir un pipeline correspondant à votre commit. Cliquez sur le statu du pipeline pour accèder aux logs d'execution. Le log devrait ressembler à cela :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Running with gitlab-runner 13.1.1 (6fbc7474)
on ccosvms0239@gitlab.in2p3.fr 96238d4c
Preparing the "docker" executor
00:09
Using Docker executor with image docker:latest ...
Pulling docker image docker:latest ...
Using docker image sha256:81f5749c9058a7284e6acd8e126f2b882765a17b9ead14422b51cde1a110b85c for docker:latest ...
Preparing environment
00:04
Running on runner-96238d4c-project-9960-concurrent-0 via ccosvms0239...
Getting source from Git repository
00:07
Fetching changes with git depth set to 50...
Initialized empty Git repository in /builds/souchal/django-tp/.git/
Created fresh repository.
Checking out 1b7c97ef as master...
Skipping Git submodules setup
Executing "step_script" stage of the job script
00:04
$ echo "Hello World"
Hello World
Job succeeded

Si vous faites un nouveau commit et un nouveau push, sans modifier le fichier .gitlab-ci.yml vous aurez le même résultat : en effet nous nous contentons d'afficher un message, sans lien avec notre code gitlab.

Automatiser la création d'un conteneur

Un des interêts majeurs de l'intégration continue est de permettre d'automatiser la création de conteneurs Docker. En effet, une fois l'image Docker configurée, seul le code de l'application change : l'idée est donc de régenerer l'image Docker dans un dépot a chanque nouvelle modification de code. Ca tombe bien : Gitlab embarque son propre dépot Docker.

Nous allons dans un premier temps écrire un Dockerfile qui servira à créer un conteneur Docker qui sera en mesure de faire tourner notre application. Là encore Gitlab peut nous aider : allez dans le dossier tp dans l'interface web Gitalb et cliquez sur ajouter un nouveau fichier (c'est le petit plus a coté de la branche en cours dans le panneau de droite), ensuite choisissez le template "Dockerfile", puis "Python". Vous aurez alors un fichier avec ce contenu :

# This file is a template, and might need editing before it works on your project.
FROM python:3.13

# Edit with mysql-client, postgresql-client, sqlite3, etc. for your needs.
# Or delete entirely if not needed.
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        postgresql-client \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app

COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /usr/src/app

# For Django
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

# For some other command
# CMD ["python", "app.py"]

Le Dockerfile généré contient le minimum necessaire pour faire tourner une application Django : nous avons juste besoin d'un fichier requirements.txt pour pouvoir installer les dépendances python. Pour générer ce fichier, lancez la commande suivante depuis votre environnement virtuel python, toujours dans le repertoire tp (qui contient le fichier manage.py :

pip freeze > requirements.txt

Ensuite on peut passer à la création de l'image Docker :

docker build --tag=django .

Si le build s'est bien passé, il devrait y avoir le message suivant :

1
2
Successfully built c3fc2e5e363d
Successfully tagged django:latest

Pour tester le conteneur, il faut lancer l'image que l'on vient de créer :

docker run -d -p 8000:8000 django

Véifiez que le message "Hello World" s'affiche bien dans un navigateur à l'adresse http://127.0.0.1:8000/

Votre application est conteneurisée !

Maintenant, nous allons faire la même chose, mais de manière automatique pour que lors de chaque modification du code, Gitlab construise lui-même le conteneur. Pour cela nous avons besoin de modifier à nouveau le fichier .gitlab-ci.yml :

docker-build: # Nom de l'action
  image: docker:latest # Nom de l'image docker que l'on va utiliser pendant cette étape
  stage: build # type d'action
  services:
    - docker:dind # On sépcifie que l'on veut utiliser docker dans un conteneur docker
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY # On se connecte a notre registre docker privé avant de lancer notre script
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" tp/  # On lance la commande de build sur le fichier Dockerfile que l'on a écrit avant
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" # On envoie l'image dans le registre

Avec cet exemple nous avons défini une étape appellée docker-build dans laquelle nous allons utiliser docker dans un conteneur docker (dind pour docker in docker afin de pouvoir construire notre image (docker build) et l'envoyer sur le registre Gitlab de notre projet (docker push). Comme ce projet est privé, nous avons besoin de nous authentifier au préalable (docker login).

Les variables en majuscules sont des variables d'environnement Gitlab prédéfinies, comme le nom d'utilisateur ou le mot de passe gitlab. Pour avoir la liste de ces variables, consultez la documentation.

Si le pipeline se termine sans erreur, votre conteneur docker est maintenant disponible dans le registre du projet : allez vérifier dans "Packages & Registries" -> "Container Registry".

Vous pouvez maintenant lancer votre conteneur sur n'importe quelle machine avec la commande pull :

docker run -p 8000:8000 gitlab-registry.in2p3.fr/user/django-tp:master

Avec cette commande, docker va rapatrier l'image depuis gitlab et la lancer sur votre machine. Vérifiez que votre application est bien accessible à l'adresse http://127.0.0.1:8000/

Testons notre pipeline en modfiant le code : par exemple, changez le texte dans tp/hello/views.py, faites un commit et push. Une fois le pipeline terminé, relancez la commande docker run, en faisant un pull d'abord pour mettre a jour le conteneur.

Générer et publier la documentation

Ici nous allons générer une documentation au format HTML et la publier sur gitlab pages pour pouvoir y accéder via un navigateur web. Pour générer la documentation, nous allons utiliser le logiciel doxygen qui va analyser le code source python et automatiquement générer de la documentation.

Pour commencer, il faut créer un fichier de configuration pour Doxygen. Ce fichier pré-rempli est disponible dans le dépôt git : files/niveau2/Doxyfile : copiez le dans un dossier doxygen à la racine de votre projet django.

On va ensuite ajouter une étape dans le pipeline CI/CD de gitlab en modifiant le fichier .gitlab-ci.yml. Comme le but est de générer des fichiers html, on va utiliser le mot clé pages pour spécifier qu'on utilise le plugin "gitlab pages" qui permet de publier des pages html statiques sur un serveur web intégré dans Gitlab.

Comme dans la première partie, l'idée est de spécifier une image docker et d'installer le logiciel Doxygen dans cette image afin de générer les fichiers html. Appellons cette étape deploy :

pages:
  image: alpine
  stage: deploy
  script:
    - apk update && apk add doxygen
    - doxygen doxygen/Doxyfile
    - mv doxygen/documentation/html/ public/ # On déplace les fichiers html dans le répertoire public qui sera la racine du serveur web intégré
  artifacts: # Un artefact est un fichier ou un ensemble de fichier qui peuvent être téléchargés ou archivés dans gitlab.
    paths:
      - public

Le fichier .gitab-ci.yml doit maintenant ressembler à cela :

docker-build: 
  image: docker:latest
  stage: build
  services:
    - docker:dind 
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" tp/
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 

pages:
  image: alpine
  stage: deploy
  script:
    - apk update && apk add doxygen
    - doxygen doxygen/Doxyfile
    - mv doxygen/documentation/html/ public/
  artifacts:
    paths:
      - public

Nous avons maintenant un pipeline en deux étapes qui construit automatiquement une image docker et ensuite génère une dcoumentation en ligne. Pour visualiser cette documentation, une fois que le pipeline s'est exécuté avec succès, il faut aller dans l'interface web Gitlab et dans le menu de gauche -> Settings -> Pages. Cliquez sur le lien pour voir votre documentation.

Il est également possible de télécharger les fichiers html générés via l'artifact défini dans .gitab-ci.yml. Pour cela il faut aller dans l'interface Gitlab puis dans jobs.

Effectuer des test

La dernière partie de notre pipeline va effectuer des tests sur l'application ; c'est à dire que l'on va vérifier automatiquement si notre application réponds bien à son cahier des charges : ici notre application doit afficher un texte en html à la racine du serveur. C'est un cas simple à tester avec django.

Configuration des tests : éditez le fichier hello/tests.py comme ceci.

1
2
3
4
5
6
7
8
from django.test import TestCase

# Create your tests here.

class HelloTestCase(TestCase):
    def test(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

Ce code va vérifier que le code réponse http soit bien 200 lorsque'on fait une requête sur la racine (/) du serveur web. Pour lancer ce test, la commande django est la suivante :

python manage.py test

Vous pouvez vérifier que le test tourne en local sur votre machine.

Nous allons modifier notre fichier .gitab-ci.yml pour ajouter cette dernière étape :

test:
  image: python:latest # On utilise une image docker python pour executer notre code django
  stage: test
  before_script: # avant de faire tourner les tests on installe les dépendances de notre projet django
    - pip install -r tp/requirements.txt
    - pip install coverage # On installe également la commande coverage qui renvoie la couverture du code
  script:
    - cd tp && python manage.py makemigrations # On crée les migrations necessaires pour faire tourner l'application
    - python manage.py test # on lance les tests
    - coverage run --source='.' manage.py test # on vérifie la couverture du code
    - coverage report # on affiche la couverture du code

Le fichier .gitab-ci.yml doit maintenant ressembler à cela :

docker-build: 
    docker-build: 
docker-build: 
  image: docker:latest
  stage: build
  services:
    - docker:dind 
        - docker:dind 
    - docker:dind 
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" tp/
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 
        - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 

pages:
  image: alpine
  stage: deploy
  script:
    - apk update && apk add doxygen
    - doxygen doxygen/Doxyfile
    - mv doxygen/documentation/html/ public/
  artifacts:
  paths:
    - public

test:
  image: python:latest
  stage: test
  before_script:
    - pip install -r tp/requirements.txt
    - pip install coverage
  script:
    - cd tp && python manage.py makemigrations
    - python manage.py test
    - coverage run --source='.' manage.py test
    - coverage report

Pour afficher le résultat des tests il faut se rendre dans l'interface web gitlab, et ensuite dans l'étape de test du pipeline. Il est également possible d'afficher le résultat des tests ou le coverage dans des badges gitlab.

Si les tests sont vérifiés, cela veut dire que l'application répond bien à son cahier des charges, elle peut être déployée automatiquement. Cela permet de pousser les modifications en production dès qu'elles sont implémentées et ainsi de gagner du temps pour proposer directement aux utilisateurs les nouvelles fonctionnalités. Il est important de tester tout le code, ainsi que la sécurité du code lorsque que l'on travaille ainsi.

Au travers de ce TP nous avons vu des cas simple de pipelines. Pour aller plus loin et connaitre toutes les possibilités qui sont proposées il vous est conseillé de visiter la documentation officielle de Gitlab.

Pour aller plus loin

Sur des pipelines très long, il devient nécessaire d'utiliser des conditions pour ne pas lancer tout ou partie des jobs à chaque commit. Par exemple, pour ne lancer les tests que lors d'un commit sur la branche master on utilise le mot clé only :

test:
  image: python:latest
  stage: test
  before_script:
    - pip install -r tp/requirements.txt
    - pip install coverage
  script:
    - cd tp && python manage.py makemigrations
    - python manage.py test
    - coverage run --source='.' manage.py test
    - coverage report
  only:
    - master

Un autre moyen de limiter les jobs est de lancer les jobs manuellement. Ainsi, a chaque commit l'utilisateur aura le choix de lancer, ou non, un ou plusieurs jobs. Testons avec le job de test :

test:
  image: python:latest
  stage: test
  before_script:
    - pip install -r tp/requirements.txt
    - pip install coverage
  script:
    - cd tp && python manage.py makemigrations
    - python manage.py test
    - coverage run --source='.' manage.py test
    - coverage report
  when: manual

Il faut ensuite aller dans l'interface Gitlab, puis dans Pipelines pour lancer le job.

Sinon il est également possible de mettre les mots clés [ci skip] ou [skip ci] dans un message de commit pour ne pas lancer la CI du tout.