Paul Andrieux
le 

Votre workflow sous steroïdes avec Gitlab-CI

13 minutes de lecture

Savez-vous pourquoi un système d'intégration continue est un élément primordial pour un bon workflow ? Connaissez-vous plus particulièrement les possibilités qu'offre Gitlab ? Quelle utilisation peut-on faire d'un CI lorsque l'on bosse avec Symfony et en quoi peut-il nous permettre de franchir un cap en productivité et en qualité ?

Revenons déjà sur ce qu'est l'intégration.

Historiquement, lorsque l’on conçoit une solution, il y a une première étape de conception, où l’on s’efforce de créer la solution qui correspond à la demande.

Après cette phase, qui peut être plus ou moins longue, arrive le moment où tout est prêt, et où il faut rassembler le travail de chacun et connecter la solution au reste du SI. Et c’est le moment de la roulette russe. Soit tout se passe bien et l’intégration va très vite, soit les soucis s'enchaînent et l’intégration peut prendre plus longtemps que la conception de la solution, et même échouer lamentablement.

Dans les années 90, un nouveau paradigme pour le développement est apparu : l’eXtreme Programming. Parmi les différents outils, XP a apporté l’intégration continue pour pallier à ce type de problématiques. Avec cette pratique, il n’est plus question d’attendre la fin du projet pour vérifier sa conformité avec le reste du SI.

Le principe est de d’augmenter la fréquence des tâches d’intégration, afin de les réaliser au moindre changement du code source. Pour cela, il faut :

  • utiliser un outil de contrôle de version
  • automatiser le processus de build
  • rédiger des tests automatisés
  • effectuer les contrôles qualité de la base de code
  • non régression

De plus, le fait d’exécuter les tests de façon automatisée à chaque changement de la base de code permet de détecter les éventuelles régressions, qui étaient une des principales pertes de temps dans un projet informatique. Pour nous aider à automatiser ces tâches, bon nombre d’outils sont apparus, dont Gitlab. Voici les différentes phases que nous avons connu auparavant chez Troopers :

  • 2012 : Bitbucket (projets privés gratis, pas de CI)
  • mid-2012 : ajout de Jenkins (première version : UI dégueu et UX pas top, implémentation de CS et PHPunit)
  • 2014 : Stash (Bitbucket auto-hébergé)
  • mid-2014 : ajout de Bamboo, le CI propulsé par Atlassian (ajout du support des tests Behat)
  • mid-2014 : projets open source sur Github, setup de CircleCI qui permet la parallélisation des tests
  • 2016 : setup de Gitlab CE en auto-hébergé :hearts:

Attardons-nous sur Gitlab.

Gitlab est principalement un outil de contrôle des sources basé sur git. Il permet de collaborer plus efficacement avec le système de Merge Request (équivalent des PR sur GitHub). Pourquoi Gitlab ? En plus des fonctionnalités standards de ce type d’outil, les raisons qui nous ont fait adopter Gitlab sont :

  • le fait que l’on puisse auto-héberger la version CE ;
  • le fait que l’outil soit complètement open source ;
  • l’intégration des process DevOps, notamment au niveau de l’intégration continue.

En effet, là où l’intégration continue passe par des services tiers sur GitHub, Gitlab propose tous les outils pour automatiser cette partie via GitlabCI, avec la notion de pipeline.

Le concept CI de Gitlab est architecturé autour de jobs, qui sont des tâches uniques comme la création d’un build ou l’exécution de tests. Il est possible d’agglomérer des jobs en stages, qui sont un ensemble de jobs qui seront exécutés en parallèle. Une pipeline désigne en fait un ensemble de stages.

A quoi ressemble une pipeline "standard" ? En général on a une première stage de build/install, puis une de test et une dernière de deploy. Ici, par exemple, on a une pipeline basique pour un projet en GO.

Chez Troopers, nous avons décider d'installer Gitlab sur un petit serveur dédié, et d'avoir un gros serveur (256Go RAM, 2 Xéons, 1,5To SSD) secondaire pour lancer un maximum de runners en parallèle. Un runner étant un daemon externe à Gitlab qui pop sur demande et permet de lancer des jobs. Bien sûr, il n'est pas nécessaire de disposer de tout ça pour se lancer avec Gitlab ! La version gratuite permet déjà de lancer des runners, mais il faut savoir être patient car un nombre limité seulement est mis à disposition, et vers 18h, quand les français font leurs commits de fin de journée et les américains leurs commits pré-lunch, il ne faut pas être pressé...

Maintenant, quels outils peut-on utiliser avec Gitlab pour améliorer notre workflow de développement ?

Build 5b589126c79cd.png

Pour commencer, chez Troopers, on travaille beaucoup avec Docker. La première chose que font nos pipelines est de surveiller l’image sur laquelle on construit l’application. Si celle-ci a bougé, alors on la recompile. Comment ? On fait un hash du fichier Dockerfile et on regarde si notre registre connaît une image taguée avec ce hash. Sinon, on va build l’image et la push dans le registry sous ce tag. Ainsi, on a toujours à disposition une image à jour pour notre application.

/.gitlab-ci.yml

1    build:
2        stage: install
3        image: docker:latest
4        services:
5            - docker:dind
6        before_script:
7            - chmod a+x ci/*
8            - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
9            - docker info
10        script:
11            - ci/build_job.sh

/ci/build_job.sh

1    DOCKER_TAG=`md5sum ./Dockerfile | awk '{ print $1 }'`
2
3    docker pull registry.troopers.agency:4567/client/project:$DOCKER_TAG
4
5    if [ $? -eq 0  ]; then
6        exit;
7    fi
8
9    docker build . -t registry.troopers.agency:4567/client/project:$DOCKER_TAG
10    docker push registry.troopers.agency:4567/client/project:$DOCKER_TAG

Contrôle qualité 5b589126c79cd.png

Le second job que l’on va lancer est lié aux contrôles qualité. Chez nous, nous faisons majoritairement du PHP avec le framework Symfony, ainsi que du React pour la partie front. Une des missions les plus faciles à mettre en place dans un CI est de vérifier que les conventions de codage et de qualité sont respectées. En PHP, on peut faire ça avec pas mal d’outils comme PHPMD, php-cs-fixer, php-metrics, etc. Nous avons donc créé une stage spécifique de contrôle qualité qui exécute chacun de ces tests dans un job dédié, et donc exécuté en parallèle. On peut voir si chaque build passe ou non, et nous pouvons mettre à disposition un patch de correctifs fourni par exemple par php-cs-fixer.

/.gitlab-ci.yml

1    phpstan:
2        image: jakzal/phpqa
3        stage: tests
4        script: phpstan analyse --level 2 -c ./ci/phpstan.neon src
5        allow_failure: true
6
7    php-metrics:
8        image: jakzal/phpqa
9        stage: tests
10        script: phpmetrics --report-html=var/php-metrics src
11        artifacts:
12            paths:
13               - var/php-metrics/
14        allow_failure: true
15
16    php-phpmd:
17        image: jakzal/phpqa
18        stage: tests
19        script: phpmd src text ./ci/phpmd.xml
20        artifacts:
21            paths:
22                - var/phpmd.html
23        allow_failure: true
24
25    php-deprecation-detector:
26        image: jakzal/phpqa
27        stage: tests
28        script:
29            - deprecation-detector check src vendor
30        allow_failure: true
31
32    php-cs-fixer:
33        image: jakzal/phpqa
34        stage: tests
35        script:
36            - ci/php-cs-fixer.sh
37        artifacts:
38            paths:
39                - var/patch.diff
40            expire_in: 24 hrs
41            when: on_failure
42        allow_failure: true

Tests 5b589126c79cd.png

Une seconde stage est réservée à l'exécution des tests automatisés. Chez Troopers, nous utilisons beaucoup PHPUnit pour les tests unitaires, et aussi Behat pour les tests comportementaux. PHPUnit est extrêmement rapide, et nous permet d'avoir un retour quasiment instantané sur la validité de nos PRs. Par contre, les tests Behat sont beaucoup plus longs à s'exécuter. Le but étant de tester l'application comme le ferait un utilisateur réel, il faut booter l'application, lui brancher une base de donnée avec un jeu de test, émuler un navigateur pour valider le fonctionnement du javascript, etc.

Pour cela, nous nous basons sur Docker pour être capable de lancer un environnement complet. C'est de toute façon la technologie que l'on utilise pour travailler en local. Il suffit donc d'ajouter un job en début de pipeline pour compiler l'image de base, puis d'y envoyer le code source de la PR qui va être testée. Nous avons un docker-compose.yml dédié à la stack de test, qui embarque les images de Selenium.

/.gitlab-ci.yml

1    phpunit:
2      stage: tests
3      dependencies:
4        - install
5      variables:
6          SYMFONY__DATABASE_TEST_DRIVER: pdo_mysql
7      script:
8        - cp -v app/config/parameters.yml.dist app/config/parameters.yml
9        - service mysql start
10        - mysql -u root -e "CREATE DATABASE db"
11        - bin/console --env=test doctrine:schema:create
12        - php -d memory_limit=2048M bin/console --env=test cache:warmup --no-debug
13        - php -d memory_limit=2048M bin/phpunit -c app/ --testdox-html var/logs/phpunit/report.html
14      artifacts:
15        paths:
16          - var/logs/phpunit/
17        expire_in: 24 hrs
18
19    behat:1:
20      stage: tests
21      dependencies:
22        - install
23      script:
24        - ./ci/services.sh
25        - ./ci/parallelBehat.sh 1
26      artifacts:
27        paths:
28          - var/fails/
29          - var/logs/
30        expire_in: 24 hrs
31        when: on_failure
32
33    behat:2:
34      stage: tests
35      dependencies:
36        - install
37      script:
38        - ./ci/services.sh
39        - ./ci/parallelBehat.sh 2
40      artifacts:
41        paths:
42          - var/fails/
43          - var/logs/
44        expire_in: 24 hrs
45        when: on_failure

./ci/parrallelBehat.sh

1    Xvfb :99 -ac &
2    export DISPLAY=:99
3    nohup java -jar \
4      /selenium-server-standalone-2.53.0.jar \
5      2> /dev/null > /dev/null &
6    bin/console --env=test cache:warmup
7    bin/console --env=test doctrine:database:create
8    bin/console --env=test doctrine:schema:create
9    bin/console --env=test server:run 127.0.0.1:80 &

Push 5b589126c79cd.png

Une fois que les stages précédentes sont passées, alors le code est considéré comme valide, il est donc inclus dans notre image Docker et celle-ci est push dans le registry.

/.gitlab-ci.yml

1    push:
2        stage: build
3        image: docker:latest
4        services:
5            - docker:dind
6        variables:
7            DOCKER_DRIVER: overlay
8        script:
9            - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
10            - DOCKER_TAG=`md5sum ./Dockerfile | awk '{ print $1 }'`
11            - docker create --name $CI_COMMIT_REF_SLUG \
12                registry.troopers.agency:4567/client/project:$DOCKER_TAG
13            - docker cp "$PWD/." $CI_COMMIT_REF_SLUG:/app
14            - docker commit $CI_COMMIT_REF_SLUG \
15                registry.troopers.agency:4567/client/project:$CI_COMMIT_REF_SLUG
16            - docker rm $CI_COMMIT_REF_SLUG
17            - docker run --name="$CI_COMMIT_REF_SLUG" \
18                registry.troopers.agency:4567/client/project:$CI_COMMIT_REF_SLUG ci/init.sh
19            - docker commit $CI_COMMIT_REF_SLUG \
20                registry.troopers.agency:4567/client/project:$CI_COMMIT_REF_SLUG
21            - docker push registry.troopers.agency:4567/client/project:$CI_COMMIT_REF_SLUG
22        only:
23            - branches

Code Reviews et App Reviews

Parmi les bonnes pratiques de développement, il y en a une en particulier qui s’est imposée : les Code Reviews.

Étroitement lié au concept de Pull Request, il s’agit de faire en sorte qu’un développeur tiers relise le travail d’un collègue afin de détecter les points améliorables. Les Codes Reviews sont donc un super outil pour réduire la dette technique et améliorer la qualité globale d’une base de code. De plus, ils permettent de continuer à se former en continu en bénéficiant des retours des autres développeurs.

Pourquoi ne pas pousser ce concept plus loin ?

En effet, le Code Review permet de s’assurer de la qualité du code source livré, mais ne permet pas de vérifier que le changement dans la base de code soit cohérent avec la demande initiale. De plus, ça ne permet pas de vérifier que le design est respecté.

L’impact des Code Reviews est en fin de compte relativement limité. C’est pour ça que je vais vous parler des App Reviews.

Schéma du process de review

Le principe est que, lorsque qu’une PR est ouverte, on puisse relire les changements de code, mais aussi accéder à la version proposée, déployée dans un environnement temporaire.

Le but est de faire intervenir un designer afin qu’il s’assure que les maquettes qu’il a conçu soient respectées et que l’ergonomie corresponde à ce qu’il avait imaginé.

Le product owner peut lui aussi s’impliquer, pour vérifier que la fonctionnalité correspond à l’user story qu’il a rédigé en amont.

Une fois qu’un développeur, un designer et un product owner ont review la PR et accepté celle-ci, on est sûr que la fonctionnalité est conforme, on peut donc merge la PR dans le tronc commun. Cette façon de procéder permet de se passer de recette en fin de sprint, tous les éléments ayant déjà été revus au fil de l’eau.

De pair avec tout cela, on a également le Stop Review qui a pour rôle de détruire la stack lorsque la PR est fermée, afin de libérer le serveur. Ce job est lancé automatiquement.

Maintenant, en quoi Gitlab nous aide à mettre en place ce type de procédé ?

Tout d’abord grâce aux jobs de déploiement. Il est possible de configurer un job dédié au déploiement de notre application. Par exemple chez Troopers, nous utilisons beaucoup Capistrano pour les déploiements.

Nous passons aussi de plus en plus sous docker pour la construction de l’infra. Du coup, il est possible de construire un container contenant notre code source, de le monter pour exécuter les tests, puis de le push dans un registry (par exemple celui fourni par Gitlab), pour le déployer via docker-compose, swarm, ou sur n’importe quel IAAS.

Comment accéder à ce container de l'exterieur ? Gitlab génère un sous-domaine dynamique mais personne ne pointe sur le port du container. On va utiliser un nginx sur lequel on va manipuler les vhost à distance pour activer notre environnement. Et grâce à la notion d’environnements, nous pouvons dire à Gitlab de déclarer un job de déploiement dans la section « environnements » sous la forme d’un nom et d’une url. Ainsi, nous avons accès à un bouton pour redéployer notre préprod/prod ou pour rollback.

/.gitlab-ci.yml

1    deploy_review: 
2        stage: deploy
3        image: ubuntu:16.04
4        dependencies:
5            - build
6        before_script:
7          - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
8          - mkdir -p ~/.ssh
9          - eval $(ssh-agent -s)
10          - '[[ -f /.dockerenv ]] && echo -e "Host *\nStrictHostKeyChecking no\n" > ~/.ssh/config'
11          - ssh-add <(echo "$STAGING_PRIVATE_KEY")
12        script:
13          - ssh $STAGING_USER@$STAGING_IP -o SendEnv="CI_COMMIT_REF_SLUG" "mkdir -p ~/docker/project/$CI_COMMIT_REF_SLUG"
14          - scp docker-compose-deploy.yml $STAGING_USER@$STAGING_IP:~/docker/project/$CI_COMMIT_REF_SLUG
15          - ssh $STAGING_USER@$STAGING_IP -o SendEnv="CI_JOB_TOKEN" -o SendEnv="CI_REGISTRY" \
16              "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
17          - export TAG=$CI_COMMIT_REF_SLUG
18          - ssh $STAGING_USER@$STAGING_IP -o SendEnv="TAG" -o SendEnv="CI_COMMIT_REF_SLUG" "cd ~/docker/project/$CI_COMMIT_REF_SLUG \
19              && TAG=$TAG docker-compose -f docker-compose-deploy.yml pull \
20              && TAG=$TAG docker-compose -f docker-compose-deploy.yml up -d --build --force-recreate"
21          - export PORT=$((ssh $STAGING_USER@$STAGING_IP -o SendEnv="TAG" -o SendEnv="CI_COMMIT_REF_SLUG" \
22              "cd ~/docker/project/$CI_COMMIT_REF_SLUG \
23              && TAG=$TAG docker-compose -f docker-compose-deploy.yml port project 8000") | sed 's/.*://')
24          - scp ci/nginx_proxy.conf $STAGING_USER@$STAGING_IP:/tmp/docker-nginx.conf
25          - ssh $STAGING_USER@$STAGING_IP -o SendEnv="CI_COMMIT_REF_SLUG" -o SendEnv="PORT" \
26            "CI_COMMIT_REF_SLUG=$CI_COMMIT_REF_SLUG PORT=$PORT envsubst \
27            < /tmp/docker-nginx.conf > /etc/nginx/conf-troopers.d/$CI_COMMIT_REF_SLUG.conf"
28          - ssh $STAGING_USER@$STAGING_IP "sudo /usr/sbin/service nginx restart"
29          - ssh $STAGING_USER@$STAGING_IP -o SendEnv="TAG" -o SendEnv="CI_COMMIT_REF_SLUG" \
30              "cd ~/docker/project/$CI_COMMIT_REF_SLUG && TAG=$TAG docker-compose -f docker-compose-deploy.yml ps"
31        environment:
32            name: review/$CI_COMMIT_REF_NAME
33            url: http://$CI_COMMIT_REF_SLUG.my.ci.domain.com
34            on_stop: stop_review
35        only:
36            - branches
37        except:
38            - develop  
39            - master
40
41    stop_review:
42        stage: deploy
43        before_script:
44          - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
45          - mkdir -p ~/.ssh
46          - eval $(ssh-agent -s)
47          - '[[ -f /.dockerenv ]] && echo -e "Host *\nStrictHostKeyChecking no\n" > ~/.ssh/config'
48          - ssh-add <(echo "$STAGING_PRIVATE_KEY")
49        script:
50            - ssh $STAGING_USER@$STAGING_IP -o SendEnv="CI_JOB_TOKEN" -o SendEnv="CI_REGISTRY" \
51                "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
52            - export TAG=$CI_COMMIT_REF_SLUG
53            - ssh $STAGING_USER@$STAGING_IP -o SendEnv="TAG" -o SendEnv="CI_COMMIT_REF_SLUG" \
54                "cd ~/docker/vom/$CI_COMMIT_REF_SLUG \
55                && TAG=$TAG docker-compose -f docker-compose-deploy.yml down"
56        when: manual
57        environment:
58            name: review/$CI_COMMIT_REF_NAME
59            action: stop
60        only:
61            - branches
62        except:
63            - prod
64            - develop

/ ci/nginx_proxy.conf

1    server {
2        listen       80;
3        server_name  $CI_COMMIT_REF_SLUG.my.ci.domain.com;
4        location / {
5            proxy_pass http://127.0.0.1:$PORT;
6        }
7    }

En conclusion, que peut-on dire de Gitlab ?

Pour conclure, Gitlab apporte des outils d'intégration continue assez complets. Tant pour les check qualité, de testing, les apps reviews et de déploiement automatisés.

Si Github répond bien à vos besoins, mon opinion est que le rachat par Microsoft n'est pas une raison suffisante pour en partir. Sachez que la version saas de Gitlab est hébergée sur azure, et que Github, même avant ce rachat, est une solution propriétaire et centralisée. Rien n'a changé.

Maintenant, si vous cherchez quand même à quitter Github, Gitlab est une excellente alternative que l'on utilise depuis plusieurs années et qui nous comble parfaitement chez Troopers.