Comment tester fonctionnellement un projet legacy
Travailler sur un projet fraîchement démarré, c’est hype !
Mais beaucoup d’entre nous n’ont pas cette chance. Au travers d’une mission, j’ai dû mettre en place un système d’intégration continue sur un projet considéré par certains comme « legacy ». Le projet comporte plusieurs dizaines de milliers de lignes de code, une base de données MySQL d’environ 8Go et une autre base MongoDB de plus de 1Go.
Ce projet a déjà quelques tests unitaires, qui sont lancés par les développeurs en local, mais aucun test fonctionnel.
Section intitulée premiere-etape-ci-et-tests-unitairesPremière étape : CI et tests unitaires
Chez JoliCode, nous utilisons et aimons beaucoup Docker. Mon choix s’est naturellement porté vers celui-ci pour industrialiser la CI.
La Dockerisation de la stack (assez classique : PHP, Nginx, Yarn, MySQL, MongoDB, Redis) fut très rapide. Nous avons un « starter-kit » en interne que j’ai pu utiliser pour mettre les différents services dans des containers.
Vu que le client utilise déjà Gitlab, l’utilisation de GitLab CI était une évidence. Nous avons monté une nouvelle machine pour le runner, car c’est une très mauvaise idée de mettre le runner sur la même machine que GitLab (pourtant prévenu par la doc, on a essayé, on n’aurait pas dû). L’installation d’un runner est vraiment simple, cependant il faut choisir parmi certains types. Je ne voulais pas faire de Docker In Docker pour certaines raisons donc mon choix s’est porté sur un shell executor
.
La première version de la CI ressemblait à ça:
tests:
before_script:
- fab ci build
- fab ci install
script:
- fab ci tests
after_script:
- fab ci down
Tout est géré par fabric. Nous utilisons fabric comme wrapper de commandes docker-compose. Cependant, il n’est pas possible de lancer plusieurs builds en même temps sans invoquer le chaos.
Nous avons mis à jour nos scripts pour être capables de préfixer tous les objets Docker par un numéro :
- local('docker-compose -p project %s %s' % (...))
+ local('docker-compose -p project_%s %s %s' % (os.environ.get('CI_PIPELINE_ID'), ...))
Et voilà ! Les tests sont maintenant lancés à chaque push et de manière concurrente.
Section intitulée deuxieme-etape-tests-fonctionnelsDeuxième étape : Tests fonctionnels
La homepage du site a besoin de MySQL, MongoDB et Redis. Mais avoir une base de données minimale (via des fixtures par exemple) était impossible. Les développeurs travaillent tous avec une copie anonymisée de la production.
Je me suis dit que la solution la plus rapide pour mettre en place des tests fonctionnels était de coder des tests contre un snapshot de la DB (anonymisée) de production. Comme les tests peuvent altérer l’état de la base de données, il faut l’isoler pour chaque build. Les containers de base de données utilisent des volumes pour stocker les données. Il faut donc pré-remplir le volume avec les données de production. Via quelques commandes Fabric / Docker, nous les avons dans la CI et en dev :
env.job_id = os.environ.get('CI_PIPELINE_ID') or ''
@task
def volume_mysql_snapshot_restore(source=None):
"""
Restore a database filesystem snapshot
"""
docker_compose('stop mysql')
if source == None:
source = env.root_dir + '/docker-volume-mysql-snapshot'
if not os.path.exists(source):
raise Exception('The source "%s" does not exist.' % source)
cmd = 'docker run --rm '
cmd += '--volume project%s_mysql-data:/destination '
cmd += '--volume %s:/source '
cmd += 'alpine '
cmd += '/bin/sh -c "rm -rf /destination/* && cp -r /source/. /destination"'
local(cmd % (env.job_id, source))
docker_compose('up -d mysql')
Et voilà ! Il est maintenant possible d’écrire des tests fonctionnels.
Cependant, copier 8Go pour MySQL et 1Go pour MongoDB, c’est lent. Environ 2 minutes de perdu à chaque push.
Section intitulée btrfs-a-la-rescousseBTRFS à la rescousse
BTRFS est un système de fichiers qui permet de faire du Copy-On-Write.
À la place de copier les données dans un volume Docker, pourquoi ne pas utiliser un point de montage qui est un volume BTRFS ? C’est tout à fait possible et simple. Comme la CI n’avait pas de disque disponible, j’ai utilisé un fichier qui fait office de block device. Voici les étapes nécessaires :
# On contruit un FS (en fait un simple fichier de 12Go)
dd if=/dev/zero of=/var/lib/btrfs bs=1M count=12288
# On crée un FS BTRFS
mkfs.btrfs /var/lib/btrfs
# On crée un dossier pour monter ce FS
mkdir /mnt/btrfs
# On mount ce FS
echo '/var/lib/btrfs /mnt/btrfs btrfs defaults,compress 0 1' >> /etc/fstab
mount -a
Ensuite, il faut créer le volume qui contient les données de référence, puis copier le snapshot dedans :
btrfs subvolume create /mnt/btrfs/project-docker-volume-mysql-snapshot-v1
cp -ra /path/to/docker-volume-mysql-snapshot /mnt/btrfs/project-docker-volume-mysql-snapshot-v1
Puis, il faut mettre à jour la configuration GitLab pour utiliser ce volume.
Dans before_script
, GitLab fait un snapshot du système de fichier utilisé par la base de données. Cette étape prend environ … quelques milli-secondes.
Dans after_script
, GitLab va supprimer ce snapshot.
tests:
before_script:
- sudo btrfs subvolume snapshot /mnt/btrfs/project-docker-volume-mysql-snapshot-v1/ /mnt/btrfs/project-docker-volume-mysql-snapshot-${CI_PIPELINE_ID}
- fab ci build
- fab ci install
script:
- fab ci tests
after_script:
- fab ci down
- sudo btrfs subvolume delete /mnt/btrfs/project-docker-volume-mysql-snapshot-${CI_PIPELINE_ID}
Le script fabric met à jour la valeur de env.mysql_volume
(uniquement dans l’environement de CI) pour utiliser le snapshot BTRFS.
# default value
env.mysql_volume = "mysql-data"
@task
def ci():
env.mysql_volume = '/mnt/btrfs/project-docker-volume-mysql-snapshot-' + env.job_id
def docker_compose(command_name):
local('MYSQL_VOLUME=%s docker-compose -p project%s %s %s' % (
env.mysql_volume,
#...
)
Le fichier docker-compose
utilise maintenant cette variable pour choisir le volume à utiliser pour les data MySQL.
# docker-compose:
volumes:
mysql-data: {}
services:
mysql:
build: ../services/mysql
volumes:
- "${MYSQL_VOLUME}:/var/lib/mysql"
Section intitulée conclusionConclusion
GitLab-CI est vraiment agréable à utiliser et configurer. En ajoutant Docker à la stack, l’intégration entre les deux fût très rapide et simple.
Finalement, l’ajout de BTRFS pour gérer les snapshots de MySQL et MongoDB a permis d’économiser beaucoup de temps et de reduire notre empreinte carbone 😎
Commentaires et discussions
Ces clients ont profité de notre expertise
Ouibus a pour ambition de devenir la référence du transport en bus longue distance. Dans cette optique, les enjeux à venir de la compagnie sont nombreux (vente multi-produit, agrandissement du réseau, diminution du time-to-market, amélioration de l’expérience et de la satisfaction client) et ont des conséquences sur la structuration de la nouvelle…
Nous avons entrepris une refonte complète du site, initialement développé sur Drupal, dans le but de le consolider et de jeter les bases d’un avenir solide en adoptant Symfony. La plateforme est hautement sophistiquée et propose une pléthore de fonctionnalités, telles que la gestion des abonnements avec Stripe et Paypal, une API pour l’application…
LOOK Cycle bénéficie désormais d’une nouvelle plateforme eCommerce disponible sur 70 pays et 5 langues. La base technique modulaire de Sylius permet de répondre aux exigences de LOOK Cycle en terme de catalogue, produits, tunnel d’achat, commandes, expéditions, gestion des clients et des expéditions.