Accueil Nos publications Blog Intégration continue en environnement distribué avec Jenkins

Intégration continue en environnement distribué avec Jenkins


Jenkins CI

Lorsque que l’on aborde le sujet des architectures distribuées, l’intégration continue n’est généralement pas le premier exemple auquel on pense. Pourtant, de nombreux avantages peuvent en être retirés et c’est ce qu’il m’a été donné de découvrir lors d’une présentation faite par Kohsuke Kawaguchi lors du premier barcamp organisé par la société SFEIR le 30 mai 2011.

Pour situer le cadre de cette présentation, Kohsuke Kawaguchi est tout simplement un (si ce n’est le) membre fondateur du projet Hudson, un outil d’intégration continue dont la popularité n’a cessé de croire ces dernières années. Kohsuke se concentre aujourd’hui sur le projet Jenkins qui n’est autre qu’un fork d’Hudson. L’origine de ce fork tient au rachat de Sun (le supporter initial du projet) par Oracle et aux incertitudes planant sur la licence d’Hudson. Un cas de figure identique s’est posé pour OpenOffice toujours suite au rachat de Sun.

Le but de cet article n’est pas de faire une présentation en détail de l’outil Jenkins mais seulement de faire un point de focus sur certaines fonctionnalités que l’on ne penserait pas trouver de prime abord sur un outil d’intégration continue mais qui mériteraient d’être davantage connues.

L’architecture maître/esclave ou comment faire travailler les autres pour soi.

Le rôle du maître.

La machine maîtresse est typiquement la machine sur laquelle est installée Jenkins mais dans le cas d’une architecture distribuée elle servira principalement à définir la typologie de la grappe d’esclaves à contrôler. C’est aussi à ce niveau que le workflow de la campagne de test sera défini notamment par le biais de tâches plus ou moins complexes appelées jobs.

Enfin cette machine jouera le rôle d’agrégateur en regroupant les résultats des différents tests ainsi que les rapports d’utilisation (charge) de la grappe d’esclaves.

Le rôle des esclaves.

Les esclaves ont pour rôle principal la réalisation des jobs dispatchés par la machine maître. Faire d’une machine un esclave pour Jenkins requiert uniquement de configurer un jar client (le slave agent) au niveau de la machine et de s’assurer que la communication est possible avec la machine désignée comme maître.

Le postulat de base pour les machines esclaves est que ces dernières ne sont pas fiables dans le temps et qu’il faut être capable de substituer une machine par une autre à tout moment. Pour permettre cette substitution, Kohsuke commença donc sa présentation avec un plugin de base de Jenkins à savoir l’outil de déploiement et d’installation automatique.

Déployer les esclaves avec Jenkins ? Il y a un plugin pour cela ! 🙂

Dans de nombreux projets, disposer de plusieurs serveurs de test et d’intégration se limite bien souvent à cloner un même environnement et à les laisser évoluer de manière indépendante en confiant aux intégrateurs le soin de gérer les besoins spécifiques comme la présence de tel ou tel navigateur ou encore de différentes versions d’une même librairie.

Cette gestion des environnements montre rapidement ses limites dès que le nombre de chaînes d’intégration s’accroit. De plus si un de ces environnements vient à tomber en panne, les délais pour réinstaller une plateforme vous privent d’une ressource pour un certain moment.

Pour réduire les coûts inhérents aux opérations de déploiement et de configuration Jenkins dispose de deux plugins permettant des installations et des déploiements automatiques.

Le premier, connu sous le nom de PXE-Plugin, permet au démarrage d’un esclave d’aller chercher une image disque sur le réseau via le protocole PXE et d’utiliser cette image pour réaliser une installation non interactive de la machine.

Le second, l’Automated Tool Installation, permet l’installation de composants supplémentaires tels que le JDK, Ant ou encore Maven.

Certains d’entre vous pourraient me faire remarquer que ces deux plugins perdent de leur intérêt si les machines employées ne sont pas strictement identiques. Par exemple, comment arriver à tester le comportement d’une application en environnement Windows si tous les esclaves mis à disposition sont des machines Unix à la base ?

Et bien, tout simplement en gardant les mêmes modules car, cerise sur le gâteau, le plugin PXE sait aussi gérer la virtualisation et rien ne vous empêche alors de créer le sous-environnement voulu au sein même de la machine esclave.

Vous l’aurez donc compris, Jenkins met à disposition deux outils qui dépassent du cadre de l’intégration continue mais qui finalement structurent les problématiques d’intégration et de déploiement. Le travail n’en est pas fini pour autant car une fois les esclaves installés il faut encore les organiser.

L’organisation des esclaves ou comment coller des étiquettes

Pour commencer, chaque machine esclave est déclarée au niveau de la machine maître. Néanmoins cette seule déclaration ne permet pas une gestion fine des tâches à accomplir. Pour remédier à cela Jenkins propose d’utiliser différents niveaux d’organisation.

Les labels

Les labels ne sont rien d’autre que des étiquettes regroupant une ou plusieurs machines selon un critère défini par l’utilisateur. Ces critères peuvent être :

  • Matériels :
    • x86 vs. Arm
    • 32 bits vs. 64 bits
  • Logiciels :
    • Windows vs. Linux vs. MacOs
    • Firefox vs. IE vs. Chrome
  • Organisationnels
    • Groupe d’esclaves alloués à un projet particulier et/ou un groupe d’utilisateurs
  • Géographiques
    • Groupe d’esclaves dans des endroits distincts (partage de plateforme pour le développement offshore par ex.)
  • Libres
    • Partition arbitraire des machines selon un discriminant choisi par l’utilisateur

Exemple de labels[/caption]

Les labels sont cumulatifs et manipulables via de simples expressions logiques permettant ainsi d’assigner la réalisation d’un job à une population donnée d’esclaves. Nous reviendrons sur ce point un peu plus tard.

Kohsuke insiste sur l’importance de choisir la bonne granularité des labels pour des raisons de lissage de la charge entre tous les esclaves mais aussi de compréhension. Trop de labels complexifient l’administration et la maintenance.

Au final, bien que présentant une certaine modularité, l’utilisation des labels n’est pas l’outil le plus avancé de Jenkins pour gérer la répartition des jobs. Au-dessus des labels se trouvent ce que l’on appelle les Matrix Projects ou matrices de configurations.

Les Matrix Projects

Le deuxième outil présenté par Kohsuke découle directement des labels sur lesquels il s’appuie.

Introduites récemment au sein de Jenkins, les matrices de configurations partent d’un postulat assez simple. Le plus souvent, les activités de test et d’intégration consistent à répéter la même suite d’opérations mais avec de légères variations à chaque itération. Typiquement, tester la requête R sur les SGBD X, Y et Z en la déclenchant à partir des navigateurs A, B et C.

Kohsuke compare d’ailleurs cette phase à un ensemble de boucles foreach imbriquées ce qui n’est finalement pas très éloigné de la réalité.

Plutôt que de devoir assigner manuellement les taches en faisant varier les conditions d’exécution, l’équipe de Jenkins suggère la création d’axes pour résumer les conditions du test. Dans notre exemple on peut distinguer deux axes à savoir l’axe « Navigateur » et l’axe « SGBD ».

L’axe « Navigateur » sera constitué de trois labels A, B et C définissant le navigateur disponible pour un esclave et l’axe « SGBD » fera de même en ce qui concerne la base de données.

Il suffira ensuite de définir un job « Requête R » et d’associer ce job à un projet contenant les deux axes précédents.

Jenkins se propose alors de réaliser un ensemble de tests afin d’obtenir la couverture complète de la requête R selon les deux axes. Par défaut, c’est à dire sans filtrage sur les axes, la matrice obtenue est le produit cartésien entres les valeurs des axes impliqués. Cette même matrice sera en charge de sélectionner les esclaves répondant aux deux axes combinés. L’avantage réside ici dans une plus grande souplesse dans la création de campagnes de test et dans la sélection des esclaves à impliquer.

Éviter de tirer la couverture à soi.

Néanmoins, il faut apporter un petit bémol à ce que nous venons de voir. Comme vous l’aurez compris toutes les combinatoires vont être testées afin de permettre une couverture à 100 %. Pour autant, peut-on dire que cette méthode est efficiente ? Non car il faut au préalable s’interroger sur la validité des combinaisons formées.

Certaines combinaisons seront impossibles techniquement comme par exemple utiliser IE6 alors que l’esclave tourne sous Windows Seven. Ce dernier ne faisant pas partie des systèmes supportés par le dit navigateur, pourquoi continuer à mobiliser des ressources ? En poussant cet exemple encore plus loin il est totalement inutile de vouloir tester IE sur une plateforme Linux, ce navigateur n’ayant jamais existé sur de tels systèmes !

En plus de ces situations assez triviales d’autres combinaisons techniquement possibles peuvent se révéler peu représentatives. Je pense ici à l’emploi de Safari. Sur une plateforme Apple tester ce navigateur est pertinent car c’est un composant par défaut du système mais sur une plateforme Windows ? Rapporté au nombre d’utilisateurs réels et en se basant sur le fait que Safari ne représente pas plus de 5% des navigateurs toutes plateformes confondues vous risquez fort de d’investir des ressources pour une faible portion de vos utilisateurs cibles.

matrix_configuration[/caption]

Vouloir une couverture à 100 % des axes peut alors handicaper votre campagne de test en créant des situations difficilement reproductibles voire impossibles. Au pire ces situations peuvent invalider une campagne entière si vous employez des métriques du type « X % des tests doivent être réussis avant de passer à la campagne suivante ». Sans parler du fait que la réalisation de ces jobs vous coûtera autant en ressources, si ce n’est plus, qu’un job aux paramètres plus réalistes.

Le temps et les ressources allouées à obtenir un taux de couverture trop élevé sont autant d’éléments que vous auriez pu allouer à d’autres jobs ou à d’autres configurations statistiquement plus courantes.

Il est aisé de rétorquer qu’en faisant ce choix, certains cas seront passés à la trappe mais il faut bien comprendre que c’est à vous de définir ce qui est acceptable en terme de périmètre non couvert et que vous risquez de payer bien cher les derniers pourcents manquants à votre campagne.

Matrice de configuration réduite[/caption]

Cette gestion des ressources nous amène au dernier concept développé par Kohsuke à savoir le « Build Promotion »

Build promotion ou l’ascenseur social pour vos builds

Utiliser un outil d’intégration continue avec une architecture distribuée met en avant le besoin d’optimiser la gestion des ressources à disposition.

Faire passer un ensemble de tests à un build reste un processus couteux qui est amplifié par le nombre d’esclaves et la complexité croissante des fonctionnalités testées.

Pour répondre à cette problématique de gestion des coûts, le concept de build promotion propose d’organiser la procédure de qualification sous la forme d’un pipeline dans lequel les tests deviendraient de plus en plus recherchés et où la réussite de l’étape courante deviendrait le prérequis pour passer à la prochaine.

L’idée n’est pas révolutionnaire en soi mais prend une autre dimension grâce aux concepts précédemment évoqués.

Huggy les bons tuyaux.

Pour servir d’exemple au concept de pipeline de builds je vais utiliser la situation suivante :

  • Soit le module java M1, doté d’un build Ant B1. M1 est un module indépendant.
  • Soit le module java M2, doté d’un build Ant B2. M2 dépend de M1 dont il importe les packages. De part sa taille la compilation, M2 prend beaucoup plus temps que M1. Il en va de même pour la récupération des sources.

Au niveau de Jenkins les jobs ci-après sont créés :

  • Le job J1 dédié au module M1 et constitué des sous-jobs :
    • J1.0 qui récupère sur l’esclave les sources de M1 à partir de SVN
    • J1.1 qui lance le build B1 pour la compilation de M1
  • Le job J2 dédié au module M2 et constitué des sous-jobs :
    • J1.0 (cf. ci-dessus)
    • J1.1 (cf. ci-dessus)
    • J2.0 qui récupère les sources de M2
    • J2.1 qui lance le build B2 pour la compilation de M2

Nous disposons aussi de 10 esclaves E1 à E10. Les machines E1 à E8 sont peu puissantes et ont du mal à compiler le module M2.

Notre première stratégie sera de lancer les jobs J1 et J2 sur tous les esclaves en parallèle avec les versions 1.0 de M1 et M2. Par malchance la version 1.0 de M2 ne compile pas et va entraîner l’échec du job J2.

Pour les machines E9 et E10 le job J2 se terminera rapidement mais vous devez attendre que les autres esclaves finissent ce même job avant de déclarer l’échec de M2 v1.0. Vous vous retrouvez donc avec 8 machines occupées pour un petit moment. Mais, dans l’intervalle, une version 1.1 de M1 est arrivée. Que faire ? Seulement deux esclaves sont disponibles pour à la fois chercher les causes d’erreur de J2 et refaire passer le job J1 à M1 v1.1.

Vous venez donc de surcharger votre grappe d’esclaves en réalisant un test trop coûteux vis-à-vis de vos ressources. A moins de pouvoir multiplier vos machines comme des petits pains et d’avoir un budget extensible cette congestion est là pour durer si vous gardez la même répartition des jobs. Une seule solution s’offre à vous à moyens égaux, il faut revoir votre stratégie.

De l’art de la plomberie.

Pour ce nouvel essai nous allons répartir nos jobs différemment.

  • Le job J1 sera exclusivement réalisé par les esclaves E1 à E8.
  • Le job J2 sera confié quant à lui aux esclaves E9 et E10 et ne sera réalisé que si et seulement si le job J1 est un succès sur les autres esclaves.

Tester le job J1 sur les esclaves E1 à E8 prendra le même temps que lors de la stratégie précédente mais, déchargés du job J2, les mêmes esclaves deviennent disponibles immédiatement lors de l’arrivée du module M1 v1.1.

Vous voilà donc en train de tester simultanément la version 1.1 du module M1 pour le job J1 et la version 1.0 du module M2 pour le job J2.

Grâce à ce changement de stratégie vous testez donc une version supplémentaire de M1 au prix d’une légère réduction du nombre d’esclaves. Si cette réduction ne s’est pas faite au détriment du périmètre de couverture vous êtes forcement gagnant car vous avez parallélisé votre campagne tout en lissant l’utilisation des machines esclaves.

Pour aller un peu plus loin dans le lissage de la charge du plan de test il faut mentionner qu’il serait possible de raffiner la définition du job J2 en lui demandant non plus d’exécuter les sous-jobs J1.0 et J1.1 mais plutôt de récupérer les artéfacts produits par le job J1 à l’étape précédente. En effet, que représente le coup d’un déplacement ou d’une recopie de fichier(s) comparé à celui d’une synchronisation avec un outil de gestion de configuration ajouté au temps de compilation des sources ?

Enfin, en plus de cette réorganisation de l’enchaînement des tests, les conditions de passage d’un job à un autre permettent aussi d’épargner des ressources.

Prenons un exemple pour illustrer cela :

Une requête de suppression en base doit être testée sur 5 SGBD différents. Cette requête est gérée par du code Java. Imaginons qu’une erreur se situe au niveau de ce code. Vous avez alors toutes les chances que la requête s’échoue de la même manière sur chacun des SGBD. Partant de ce constat, est-il encore utile de vouloir réaliser le test sur chacun d’entre eux ? Bien évidemment non.

On a donc tout intérêt ici de conditionner le passage des tests en appliquant une règle du type « Si le test est réussi sur les SGBD A et B lancer le test courant sur les SGBD C, D et E »

Ainsi vous économisez le coût d’exécution sur 3 environnements différents

Et au final j’en retiens quoi ?

A la suite de cette présentation un premier ensemble de practices se détachent si l’on souhaite mettre en place Jenkins sous la forme d’une architecture distribuée :

  • Définir une division cohérente de vos esclaves via les labels
  • Ne pas rechercher une couverture complète mais privilégier une matrice de test utilisant les configurations les plus courantes vis-à-vis de vos utilisateurs quitte à utiliser une autre matrice pour les cas les plus marginaux.
  • Analyser les opérations réalisées pour chaque fonctionnalité testée afin d’identifier les blocs mutualisables et les artéfacts réutilisables entre chaque job.
  • Mettre en place un système de promotion de builds en appliquant une politique de détection d’échec au plus tôt car le coût d’un test qui échoue se répercute à un moment ou un autre sur vos autres tests.

Bibliographie et sources