Plateforme d’intégration continue élastique avec Gitlab CI
Début 2019, j’ai déployé et peaufiné pour le projet libre Orfeo Toolbox une plateforme d’intégration continue élastique qui mérite bien un petit retour d’expérience.
Une once de vocabulaire
Pour commencer, mettons-nous d’accord sur les mots. Par « élastique », j’entends plateforme qui se dimensionne dynamiquement et sans intervention humaine pour répondre à son besoin instantané en ressources. Lorsqu’elle n’est pas sollicitée, une telle plateforme est réduite à son strict minimum, ici l’orchestrateur et les systèmes qui ne peuvent pas, pour une raison ou une autre, être virtualisés ou instanciés à la demande. Au fur et à mesure que son besoin en ressources s’accroit, la plateforme instancie des machines virtuelles auxquelles elle distribue les tâches d’intégration. Lorsque ces machines ont terminé leur travail et ne sont plus utiles, elles sont détruites.
Le projet et son état des lieux
Venons-en maintenant au projet. Orfeo Toolbox (OTB pour les intimes) est un ensemble de bibliothèques et d’outils libres en C++, de traitement d’images pour la télédétection. Autrement dit, la bibliothèque OTB permet de traiter les lourdes images issues de satellites ou de prises de vue aériennes et d’en extraire des informations (occupation du sol, densité de la végétation, détection d’aéronefs ou de navires…).
Jusqu’en 2018, la validation du code d’OTB reposait sur des nightly builds réalisés par une ferme de serveurs hébergés par CS GROUP – France et le CNES. Cette validation prenait une bonne partie de la nuit. Les développeurs avaient donc un retour sur leur travail le lendemain, soit parfois 24 heures après avoir poussé leur code sur la forge Gitlab, et bien souvent, ils étaient passés à autre chose entre temps. Ce retour tardif nuisait sérieusement à la fluidité du développement. En outre, le contrôle qualité était sommaire.
En 2018, le CNES – établissement qui finance le développement d’OTB, principalement assuré par CS – a souhaité qu’une réelle intégration continue soit mise en place et qu’une attention particulière soit portée à la qualité du code. Voici le constat que nous avons alors fait :
-
OTB exploitait sa propre instance de la forge Gitlab, tournant sur un serveur loué à OVH, comme tous les autres services collaboratifs du projet, à l’exception notable de la plateforme de nightly builds.
-
La validation du code exigeait d’importantes ressources matérielles :
-
Selon les cibles et les options, la compilation d’OTB et l’exécution des 1900 tests prenait entre 90 et 120 minutes sur un serveur musclé.
-
Cette lourdeur était décuplée par le caractère multiplatefome (GNU/Linux, MS-Windows, MacOS) d’OTB et le support de différentes configurations sur chaque plateforme.
-
-
L’accès à la plateforme exécutant les nightly builds posait problème :
-
Entre pannes et opérations de maintenance préventive des serveurs, des réseaux et des installations électriques, la disponibilité de l’infrastructure laissait à désirer.
-
Les serveurs utilisés appartenant à CS et au CNES et étant déployés sur leur réseau interne, il était hors de question qu’un contributeur tiers participe à leur administration.
-
-
OTB étant un projet libre, au développement ouvert, nous recevions des contributions de tiers, les plus modestes et néanmoins précieuses étant des signalements de bogues. La forge était donc volontairement ouverte, tout le monde pouvait y ouvrir un compte.
-
Nous utilisions le service en ligne Coverity Scan, qui nous avait rendu de fiers services en détectant moult bogues larvés, mais cet outil était propriétaire et il ne répondait pas à tous nos besoins d’assurance qualité. Pour le reste, notre approche de la qualité était artisanale et nous ne la considérions que de manière ponctuelle.
Identification des besoins
Ce constat a guidé notre réflexion :
-
La plateforme d’intégration continue devait renvoyer les résulats au plus tôt aux développeurs. Au plus tôt signifiait « ne pas devoir attendre le lendemain », mais aussi « en moins de 90 minutes ».
-
Pour améliorer la disponibilité de la plateforme et permettre à des tiers fortement investis dans le projet de l’opérer, nous devions l’externaliser. Utilisant déjà les services d’OVH, le recours au cloud d’OVH était naturel. Mais nous devions comparer l’offre d’OVH à celles d’Amazon, de Google et de Microsoft.
-
Étant pleinement satisfaits par la forge Gitlab, nous appuyer sur Gitlab CI pour l’intégration continue tombait sous le sens, mais nous devions aussi envisager le recours aux plateformes mutualisées telles que Travis CI, CircleCI ou AppVeyor, qui pouvaient s’avérer plus attractives (au prix d’un passage obligé par le miroir d’OTB sur Github).
-
Le recours à une plateforme externe impliquait un cout récurrent et potentiellement élevé. Nous devions donc soigneusement évaluer le modèle de facturation de chaque fournisseur envisagé et nous doter d’une plateforme d’intégration continue qui optimise l’allocation des ressources.
-
Le caractère résolument public et ouvert de la plateforme allait l’exposer aux abus et autres détournements (transformation de la plateforme en ferme de minage de Bitcoin ou en botnet). Il fallait anticiper les risques et leur trouver une parade.
Les solutions et stratégies retenues
Voici vers quelles solutions nous a dirigés notre analyse :
-
Disqualification des plateformes d’intégration continue mutualisées : Ces plateformes ne pouvaient pas nous fournir les serveurs musclés dont nous avions besoin et la plupart d’entre elles étaient conçues pour fonctionner exclusivement avec Github. Or, si le projet OTB disposait d’un miroir sur Github (né du fol espoir d’accroitre la visibilité du projet et le nombre de contributions), l’équipe était attachée à Gitlab. Nous contenter de serveurs plus modestes aurait en outre allongé les temps de traitement, ce qui était à l’opposé du but poursuivi.
-
Gitlab CI : Cet outil nous a rapidement séduit de par son ergonomie, ses choix techniques (intégration parfaite à Gitlab, YAML, registre Docker intégré), l’éventail de ses exécuteurs (shell, Docker, Docker Machine, Kubernetes), sa remarquable documentation (quel bonheur pour qui a dû administrer et configurer pendant des années des instances de Jenkins !) et sa fonction autoscale qui adressait parfaitement notre besoin de dimensionnement dynamique de l’infrastructure. Pour faire bref, cette fonction délègue à Docker Machine l’instanciation des machines virtuelles dans un cloud et le déploiement du service Docker sur ces machines. La fonction autoscale reprend ensuite la main et distribue les tâches d’intégration continue aux machines fraichement instanciées. Lorsqu’elle n’a plus besoin des machines virtuelles, elle demande à Docker Machine de les supprimer.
-
Public Cloud OVH : Nous avons opté pour le cloud d’OVH car, si son modèle de facturation simple et à granularité horaire semblait de prime abord moins intéressant que celui de ses concurrents, il se révélait parfaitement prédictible. Une machine virtuelle coutait X centimes par heure, qu’elle soit allumée ou éteinte, qu’elle ne fasse rien ou exploite au maximum les ressources allouées. À contrario, les géants du cloud tels qu’Amazon proposaient une facturation à la seconde. Mais outre l’instance, ils facturaient l’utilisation effective des ressources : charge du processeur et de la mémoire, espace de stockage consommé et nombre d’entrées/sorties générées sur cet espace, consommation de bande passante sur le réseau, tout faisait ventre ! Nous n’aurions pu connaitre le cout effectif d’un traitement qu’en l’exécutant au moins une fois sur le cloud de ces fournisseurs. Au final, le risque de dérapage financier était bien réel pour un projet tel qu’OTB, grand consommateur de toutes les ressources mises à sa disposition. Or, CS devait s’engager auprès du CNES sur le cout de la plateforme et annoncer un budget. Notons que nous avons pu choisir le cloud d’OVH car il est géré via OpenStack (tout au moins le « Public Cloud ») et Docker Machine dispose d’un connecteur pour OpenStack.
-
Centre d’hébergement de Varsovie : Lors de nos essais préliminaires, il s’est avéré que le délai de fourniture effective des machines virtuelles était d’au moins 5 minutes, qu’il atteignait fréquemment 10 minutes, avec un record à plus de 30 minutes. De tels délais anéantissaient nos efforts d’optimisation. Supposant une charge excessive du centre d’hébergement (data center) que nous utilisions (GRA3), nous avons évalué plusieurs centres d’hébergement d’OVH, pour finalement jeter notre dévolu sur celui de Varsovie (WAW1), qui délivrait les machines en 60 à 90 secondes. Ce choix fait il y a un an est toujours pertinent à cette heure (je dois être fou pour partager une telle information).
Notez que la fourniture de machines en plus de 10 minutes (ce qui est exceptionnel avec WAW1, mais arrive tout de même) induit un sérieux problème. Après 10 minutes d’attente, Docker Machine considère que sa requête s’est perdue dans les limbes et il réitère sa demande de création de machine sans annuler la première demande puisqu’aucune référence ne lui a été communiquée. La première requête finit pourtant par être exaucée, mais la machine virtuelle livrée n’existe pas pour Docker Machine. Il ne demande donc jamais sa suppression. Or, cette machine est facturée au projet tant qu’elle existe. Inutile de vous faire un dessin ! Il est donc primordial de surveiller cet écart entre les ressources connues et les ressources allouées, et de supprimer ce que j’appelle « les machines orphelines ». J’ai confié cette tâche à un script Python (que je finirai bien par publier un jour) qui, toutes les 20 minutes, interroge OpenStack et Docker Machine, et demande la destruction immédiate de toute machine connue du premier, mais pas du second.
-
Machines virtuelles MS-Windows persistantes : Nous avons eu notre première déconvenue en constatant qu’à l’instanciation, les outils d’OVH réinitialisaient le système des machines virtuelles MS-Windows que nous avions préalablement préparé sur une image personnalisée, au point de nous contraindre à nous connecter sur la machine pour saisir le numéro de licence de MS-Windows et finaliser l’installation à la main. Il nous était donc impossible d’instancier à la demande des systèmes MS-Windows prêts à l’emploi, et nous nous sommes résignés à utiliser des machines virtuelles persistantes, perdant un peu de l’élasticité espérée.
-
Serveurs MacOS auto-hébergés : Faisant une entorse à l’externalisation de la plateforme, nous avons choisi de conserver le serveur MacOS déployé sur le réseau de CS. Ce choix a été guidé par le fait que nous venions de le renouveler et que l’offre « cloud MacOS » était restreinte en juin 2018 (date à laquelle ce choix a été acté). Ce serveur n’avait pas besoin d’être exposé sur le net puisque, par conception, l’exécuteur Gitlab Runner est toujours à l’initiative de la connexion avec l’orchestrateur Gitlab CI, qui utilise ensuite ce lien pour commander l’exécuteur. Mais ce serveur compilant du code et exécutant des instructions en provenance de l’extérieur, potentiellement de contributeurs tiers inconnus, il s’est tout de même avéré nécessaire de le mettre dans une enclave l’isolant du réseau interne de CS.
-
SonarQube : Sans surprise, nous avons choisi SonarQube pour mettre en place un véritable suivi qualimétrique du code. Les greffons communautaires pour les langages C et C++ supportent les dernières versions de ces langages et fonctionnent bien. Les greffons Sonar GitLab et Sonar Auth GitLab ont facilité son intégration avec la forge, et le greffon Sonarqube Community Branch est arrivé à point nommé pour activer le support des branches dans la version communautaire de Sonarqube (nous disposons ainsi d’un suivi qualimétrique par branche).
-
Accès aux exécuteurs accordé au cas par cas : Face au risque d’utilisation abusive et de détournement, nous ne pouvions laisser de parfaits inconnus exploiter les ressources de la plateforme d’intégration continue. Nous avons donc choisi de réserver l’accès aux exécuteurs aux projets « connus de nos services », mais nous savons ouvrir les vannes au cas par cas. Dans le même ordre d’idées, seul un nombre restreint de projets ont à publier des paquets binaires sur le site web et cette publication nécessite un accès privilégié. Pour éviter toute publication illégitime, nous avons décidé de la confier à un exécuteur dédié, disposant des clés nécessaires et tournant sur le serveur de la forge. Seuls les projets ayant à publier des paquets ont accès à cet exécuteur.
-
Contrôle des images utilisées : Toujours dans le souci d’éviter les usages illégitimes, nous avons créé sur la forge un projet dédié à la génération des images Docker nécessaires au projet OTB et aux projets connexes (projet otb-build-env). Ce projet génère les images via Kaniko, un outil de génération alternatif à Docker, performant et qui présente l’énorme avantage de ne pas tourner sous l’identité root et de ne pas nécessiter de conteneur privilégié. Les images générées sont publiées dans le registre Docker intégré à la forge (encore un service remarquable de Gitlab). Nous avons ensuite configuré Gitlab Runner pour qu’il n’accepte d’utiliser que les images Docker issues de ce registre. Comme toutes les protections, celle-ci a ses limites, mais en multipliant les barrières, on finit par rendre le contournement vraiment laborieux.
-
Pipeline à géométrie variable : Dans le jargon de Gitlab CI, le « pipeline » est la séquence de tâches (regroupées en étapes) qui constitue le processus d’intégration continue. S’il est nécessaire qu’à l’occasion de points clés (par exemple, une merge request ou une fusion sur une branche de release), le code soit soigneusement validé sur toutes les plateformes et que la qualité soit conforme au niveau attendu, au quotidien, les développeurs gagnent plus à avoir un retour rapide sur leur travail qu’à avoir une validation exhaustive. Gitlab offrant toutes les fonctions nécessaires, nous avons décidé de limiter l’exécution de nombreuses tâches à ces points clés, afin de fournir un retour sommaire, mais très rapide aux développeurs le reste du temps (compilation et tests sur une seule plateforme, selon un mode appelé « fast-build »).
-
Optimisation de la compilation et des tests : À proprement parler, ce point n’a rien à voir avec l’intégration continue, mais la mise en place de cette plateforme et le fait que plus l’exécution du pipeline prendrait du temps, plus le cout de la plateforme serait élevé, ont amené l’équipe à effectuer un gros travail d’optimisation des tests et de la compilation. Elle a obtenu des résultats remarquables et comme c’est toujours le cas en pareilles circonstances, on se demande après coup pourquoi ce travail n’avait pas été effectué plus tôt. ;)
Le bilan
Aujourd’hui, cette plateforme ronronne et il est possible de dresser le bilan de ce projet :
-
Je vais enfoncer une porte ouverte, mais il est toujours agréable de voir se confirmer ce que l’on sait : passer du nightly build à une véritable intégration continue, procurant un retour rapide aux développeurs, prenant en charge la publication des paquets et de la documentation, et assurant le suivi qualimétrique a changé la façon de travailler de l’équipe. Son efficacité s’est accrue, comme la qualité et la fiabilité du code. Ce constat justifie à lui seul la migration.
-
Nous avons fait les bons choix techniques. La plateforme d’intégration continue apporte entière satisfaction aux utilisateurs et sa disponibilité est quasiment irréprochable. J’y pense, vous ai-je déjà dit tout le bien que je pensais de Gitlab, de Gitlab CI et de Gitlab Runner ? ;) Grâce à Docker, nous testons plus de configurations qu’auparavant et les personnes aptes à modifier ces configurations ou à en ajouter sont plus nombreuses (elles n’ont pas besoin d’être admin. sys. des machines).
-
Le cout est conforme à celui annoncé. Nous l’avions estimé à 700 €/mois. Il oscille entre 600 et 670 €/mois, et de récents ajustements vont l’abaisser d’environ 110 €/mois. Pour le garder sous contrôle en détectant au plus tôt d’éventuels dérapages, j’ai développé un greffon pour Munin que je vais lui aussi publier un de ces jours.
-
Le cloud, c’est moins magique qu’on nous le vend, mais qui en aurait douté ? Le temps de mise à disposition des machines a été une réelle déconvenue, tout comme l’écrasement de la configuration du système MS-Windows ou les machines orphelines. Entre essais infructueux, investigations, mise au point de palliatifs et changements de stratégie, cette partie s’est révélée plus laborieuse (et chronophage) que prévu. Mais l’expérience a été assimilée et capitalisée, nous serons plus efficaces et pertinents la prochaine fois.
-
Sécuriser une plateforme publique et ouverte pour éviter son utilisation abusive et son détournement n’est pas une sinécure. À tous les niveaux, nous avons dû imaginer les fourberies possibles et nous en prémunir. Par contre, force est de reconnaitre que je n’ai jamais eu à hacker les outils que nous avions choisis pour introduire les contrôles et les restrictions qui s’imposaient. Tout était prévu ! Chapeau bas à leurs équipes de développement, qui ont de la bouteille et un sacré savoir-faire industriel.
-
Lorsque la plateforme est montée en charge, nous avons réalisé qu’il était nécessaire de brider la création et de retarder la destruction des machines virtuelles par la plateforme. En effet, en l’état actuel, le pipeline d’OTB peut requérir jusqu’à huit machines en parallèle et il arrive que deux ou trois développeurs soumettent du code à la forge en l’espace de 90 minutes (temps d’exécution du pipeline dans sa version exhaustive). Pour leur fournir un retour dans les meilleurs délais, la plateforme pourrait donc instancier jusqu’à 24 machines simultanément (et même plus de manière exceptionnelle, j’en suis certain). Et c’est là qu’il faut avoir à l’esprit le mode de facturation de ces machines. Même si elles n’exécutent qu’une seule tâche, traitée en quelques secondes, et si elles sont ensuite détruites par Docker Machine, OVH les facture une heure pleine. Pire, si Docker Machine instancie dans la foulée une nouvelle machine à la demande de Gitlab CI, nous paierons deux heures pleines pour quelques minutes d’utilisation effective qu’une seule machine aurait pu cumuler. Dans le souci d’optimiser l’usage des ressources sur une heure pleine, quitte à retarder un peu le retour fait aux développeurs, nous avons décidé de :
-
Limiter Gitlab Runner à 8 exécuteurs en parallèle.
-
Demander à Docker Machine d’attendre 10 minutes d’inactivité avant de détruire une machine. Ce faisant, nous augmentons la probabilité de réutilisation d’une machine lorsque les tâches s’enchainent, tout en limitant le risque de dépassement de l’heure d’existence, puisque les tâches les plus longues prennent 45 minutes.
-
Demander à OVH l’ajustement de nos quotas au niveau nécessaire pour disposer de 8 instances B2-30 (gabarit de machine virtuelle que nous utilisons).
-
Et maintenant ?
Diverses améliorations sont envisageables :
-
Utilisation de conteneurs MS-Windows pour multiplier les configurations sur ce système, comme nous le faisons sur GNU/Linux.
-
Utilisation d’un cluster Kubernetes en lieu et place de Docker Machine.
-
Publication des paquets binaires sur un serveur d’artéfacts externe. En fonction du besoin exact, il sera possible d’utiliser un outil dédié tel que Sonatype Nexus Repository OSS ou un serveur de type « object storage » S3, tel que Minio.
-
Exécution optimisée du pipeline grâce à un ordonnancement des tâches par un graphe de dépendances acyclique.
Mise à jour le 30/01/2020 : C’est fait ! Cf. mon article Exécution « désordonnée » de jobs dans Gitlab CI.
-
Généralisation des caches de compilation. Nous utilisons un cache généré quotidiennement par CCache pour accélérer les compilations récurrentes (tâche fast-build). Cette stratégie s’avère très efficace, il serait judicieux de la généraliser.
Tout finit par arriver, même la fin de cet article…
J’espère que ce retour d’expérience vous sera utile. Vous trouverez les instructions de déploiement d’une telle plateforme d’intégration continue dans la généreuse documentation de Gitlab, enrichie par les articles en anglais et en français que d’autres personnes ont déjà publiés et qu’il me semblait inutile de paraphraser ici.
Quelques liens pour la route
- Projet OTB sur la forge Gitlab
- Suivi qualimétrique
- Pipelines :
- Historique des pipelines du projet (12700 machines virtuelles cumulées en 12 mois)
- Un exemple de pipeline rapide
- Un exemple de pipeline complet
- Le forum du projet, déployé au même moment en remplacement des listes de diffusion qui n’étaient plus au gout du jour