Présentation avancée de Gitlab CI/CD
Référence : sur la base de la formation Octo Academy.
Quelques concepts généraux
Tests
La plupart des bugs et régressions sont introduit pour l’essentiel au cours du développement. Les identifier le plus tôt possible permet d’éviter des coûts de correction excessifs.
Les tests d’intégration devraient être écrit, ou du moins géré par les PO (en même temps que la User Story) : les membres fonctionnel de l’équipe doivent supporter les fonctions techniques (ce peut être fournir la description des tests, qui seront ensuite codés par les développeurs ou les membres de l’équipe de qualification).
- tester la qualité du code (lint, analyse du code, sécurité, performance)
- tests unitaires
- tests d’intégration
- tests de bout en bout.
Dev / Ops
Cette pratique favorise la collaboration et parallélisation des tâches entre les développeurs et les opérations :
- planification : les opérations, de même que le produit, fournissent des entrées à prendre en compte pour de nouveaux développements.
- code
- build
- tests
- release
- déploiement
- exploitation
- contrôle : les opérations peuvent être à l’origine de retours à destination des développeurs
L’intégration continue consiste à couvrir toutes les phases de la planification aux tests, par les pratiques de DevOps, tandis que le déploiement continue pousse le concept sur l’ensemble du processus.
Les pratiques DevOps reposent sur 4 piliers
- L’architecture et les patterns de développement doivent être pensés à la fois au bénéfice des développeurs et des opérateurs de l’application.
- Penser et utiliser les outils d’automatisation pour construire et maintenir l’infrastructure.
- Constuire et déployer l’application en continu au moyen d’outils et de processus spécifiques.
- Et enfin, une culture, des méthodes et une organisation adéquate favorisent le dialogue entre les développeurs et les opérateurs de l’application.
Vision pipeline
Les premières étapes seront généralement automatisés, tandis que les dernières sont soit conditionnelles, soit des tâches manuelles complémentaires.
Trop de jobs peuvent nuir au suivi (lisibilité) et à la performance.
On y retrouve :
- les test automatisés, le coverage (qualité)
- la construction de l’artéfact, puis
- sa livraison,
- la documentation (construction et publication).
On peut également automatiser des mécanismes de notification dans la pipeline.
La manière de déclencher une pipeline ou un job doit être décidé en fonctionné de l’organisation de l’équipe. Les outils comme Gitlab mettent à disposition différents moyens (dépendance, déclencheur, action manuelle, etc.), mais leur choix dépend des besoins et de l’organisation spécifique de l’équipe.
L’intérêt de la pipeline est de pouvoir produire un artefact réutilisable : par les développeurs, pour la qualification, pour le déploiement en production, etc.
Un mécanisme mono pipeline permet de suivre de bout en bout le déclenchement qui a suivi un commit ou une merge request. par contre, on peut rapidement de retrouver avec une pipeline longue et complexe à suivre (notamment si on ajoute des étapes conditionnelles, manuelles, etc.).
Le multi pipeline répondra généralement à des contraintes de sécurité ou d’environnement. Par contre, cela nuira à la lisibilité du cheminement et rajouter la complexité à la mise en œuvre.
- Exécuter les tests les plus rapides en premier : permet d’avoir des retours rapides
- La pipeline peut (doit) évoluer, et s’adapter en fonction de l’évolution du projet et des différents projets (également des parties prenantes intervenant sur la pipeline).
- Les tests ont un rôle extrêmement importants, dans la pipeline.
Gitlab CI/CD
Les principales fonctionnalités
- Il s’agit d’une approche déclarative (via le fichier yaml) et intégrée à Gitlab.
- Gitlab permet l’exécution de jobs et pipelines en parallèle via les runners.
Gitlab CI permet de découper une pipeline en stages, au sein desquels ont peu paralléliser les jobs. Un système de dépendance et entre les jobs et de conditions permet de fixer un ordre d’exécution entre plusieurs jobs.
Le fichier de configuration
Le fichier gitlab-ci.yml permet de déclarer les stages et les jobs. Pour les jobs, on y décrit :
- environnement d’exécution (nom, image Docker, etc.),
- le stage auquel le job est rattaché,
- tâches pré/post traitement (permettant de surcharger des variables d’environnements, des actions avant ou après le traitement, etc. le post-traitement est exécuté quoi qu’il arrive), et
- le traitement à exécuter (une série de commandes à exécuter).
Remarque : Gitlab CI défini deux stages cachés, qui sont “.pre” et “.post”, respectivement exécutés avant et après la pipeline.
Le fichier permet également de déclarer des variables d’environnement, au niveau de l’ensemble du projet, d’un stage, ou d’un job. À noter que les variables au niveau du projet peuvent également être déclarés via l’interface web du projet (l’un des principaux avantages est la possibilité de masquer les valeurs de certaines variables).
Les runners
On peut voir 4 grandes familles de runners.
- Exécution locale
- Docker
- Exécution ssh (sur une ou plusieurs instances distantes)
- Docker machine (combinaison des deux précédents : on exécute les containers sur des instances externes).
Dans le cas de Docker, on peut se retrouver à faire du “Docker in Docker” pour certaintes tâches. L’outil le plus courant et recommandé pour celà est Kaniko.
À noter qu’on peut aussi partager la socket de Docker avec un container, mais ce n’est pas une solution propre (peu de souplesse, ni de stabilité et présente de gros problèmes de sécurité).
On peut cibler un runner spécifique via des labels associés d’un côté aux runners, et de l’autre côté aux jobs dans leur déclaration. Le choix des tags dépendra de la stratégie sur l’utilisation des runners. Par exemple, pour une application multiplateforme, on pourra choisir un runner en fonction de l’environnement à tester. Ce peut également être en fonction des besoin d’un stage (test, build, déploiement, etc.). Enfin, Gitlab CI met à disposition des runners partagés qui pourrons être ciblés de la même façon.
Factorisation de code dans les jobs
Ancres
Il est possible d’utiliser les ancres YAML. Leur syntaxe est la suivante :
# Le '.' au début indique que le job est un template.
# Il ne sera pas exécuté par Gitlab CI. C'est d'ailleurs un moyen pour commenter
# un job.
.example_anchor: &example_anchor |
echo "This comes from an anchor"
test:
stage: test
script:
- *example_anchor
- etc.
Une autre syntaxe est celle du “map merging” qui permet de fusionner les propriétés de deux objets YAML.
Pour aller plus loin : les ancres sont un élément de la syntaxe de YAML ici exploitées par Gitlab CI.
Ce mécanisme n’est pas compatible avec les imports (cf. include plus bas) d’un fichier YAML dans un autre.
Les ancres peuvent être utiles pour des besoins de factorisation dans des cas relativement simples. Lorsqu’on a des structures plus complexes à manipuler, il vaut mieux privilégier les templates.
Templates
On peut également utiliser le système d’extends. Il consiste (là aussi) à déclarer un job “template” (i.e. qui commence par un point), et à utiliser le mot clé “extends” dans un job avec le nom du template en valeur. Pour le reste, l’extends fonctionne comme de l’héritage en programmation orientée objet.
Il est possible d’extendre un job avec plusieurs templates.
Il est théoriquement possible d’aller jusqu’à 10 niveaux d’héritage, mais il est fortement déconseillé d’allée au delà de 2~3 niveaux pour des raisons de lisibilité et maintenance des fichiers YAML.
Les extends sont plus flexibles, lisible, et performant que les ancres. De plus, ce mécanisme est compatible avec les imports de fichier YAML dans un autre.
À noter qu’on ne peut pas utiliser “extends” sur un job (qui n’est pas un template).
Includes
Il consiste à inclure un fichier YAML dans un autre à l’aide du mot clé “include”. Gitlab CI permet d’inclure des fichiers YAML d’un autre dossier du projet, d’un projet vers un autre, en précisant (ou non) une branche ou un tag donné, etc. Gitlab propose d’ailleurs des templates (cf. Auto-DevOps), voir même via une URL de fichier YAML présent sur internet (via un HTTP(S) GET).
Dans la mesure du possible, il vaut mieux éviter de mettre des jobs dans les templates pour des raisons de lisibilité et de maintenabilité. Les templates devarient être, dans la mesure du possible, réservés aux templates.
Default
Il s’agit d’un bloc pouvant être placé au début du fichier .gitlab-ci.yml pour définir un comportement par défaut des jobs pour les before_script, after_script, etc.
Autoriser l’échec
Un job sera par défaut en échec si l’une des commandes exécutées est en échec (i.e. code de retour différent de 0). Ce comportement peut être modifié en précisant l’attribut “allow_failure” à “true” dans la déclaration du job. Celui-ci apparaîtra alors en “warning” à l’exécution de la pipeline.
Les conditions
Il est possible de conditionner l’exécution d’un job ou d’un stage en fonction d’événements survenu sur un job précédent, le contenu de variables, la présence ou non de fichier, etc.
- Only / Except : exécuter le job seulement si la condition est respectée / ou inversement.
- When : au succès (comportement par défaut), à l’échec, toujours, ou encore manuellement (i.e. c’est “la” manière de déclarer un job manuel, idem pour le déclenchement à l’échec; “always” peut être utile pour faire le ménage à la fin de la pipeline).
- rules : solution beaucoup plus souple et complexe que les précédents.
Autres fonctionnalités avancées de Gitlab CI
Il est possible d’utiliser un système de cache pour stocker des dépendances entre plusieurs jobs d’un pipeline, ou à l’ensemble du projet, etc. Ce système supporte le fait d’avoir plusieurs runners (il faut toutefois penser à un système de cache distribué, comme via un stockage objet).
Gitlab CI permet de propager les artefacts générés par un job dans un ou plusieurs autres jobs (ils doivent être déclarés, car par défaut, Gitlab ne les conserves pas).
Gitlag CI permet d’associer un container de service à côté du container sur lequel le job est exécuté. C’est typiquement utile pour les tests nécessitant un service externe (e.g. une base de donéee) à l’application que l’on est en train de tester.
Une bloc “default” en haut du fichier .gitlab-ci.yml permet de fournir un comportement par défaut à l’ensemble des jobs du fichier (e.g. un “before_script par défaut, etc.). Maintenant, pour ce genre de cas, Gitlab CI a prévu des templates, c’est-à-dire d’autres fichiers YAML qui peuvent être importés et utilisés dans notre fichier .gitlab-ci.yml (avec le mot clé “extends”).
Il est possible de déclencher une pipeline depuis une précédente pipeline. Il peut s’agir d’une pipeline du même, ou d’un autre projet.
Gitlab CI est également accessible par API, de la même façon que les autres fonctionnalités de Gitlab. On peut visualiser, déclencher, manipuler, etc. les pipelines.
Gitlab CI propose encore bien d’autres fonctionnatliés avancées que l’on peut découvrir dans la documentation officielle.
Idées pour l’utilisation de Gitlab CI
- à la fin d’une pipeline de validation du code sur develop (post-MR ou nightly), on peut ajouter un job manuel permettant de déclencher la release (par exemple en taguant et poussant le commit correspondant).
- Faire en sorte que chaque développeur register sa machine comme runner de ses propres pipelines lorsqu’il se connecte pour travailler sur le projet (de sorte à ne pas dépendre des runners partagés de Gitlab, ou éviter de dépenser sur un compte dans le Cloud).
- Ship / Show / Ask - Modern Branching Strategy : https://martinfowler.com/articles/ship-show-ask.html
Une pipeline type prend la forme suivante :
- validation de la qualité du code (lint, analyse statique du code type “bandit” pour Python, etc.)
- tests unitaires
- build (d’une image docker) pour obtenir un paquet de l’application exécutable;
- exécution des tests d’intégration à partir de l’exécutable;
- Publication de l’artefact;
- Déploiement de l’artefact en production (ou alors pour une qualification manuelle, avant mise en production finale).
Quelques pistes pour améliorer les processus, sur la qualification de l’application, d’un point de vue produit :
- Exécuter tout les tests automatisés en parallèle du développement : la plateforme des développeurs est une plateforme d’intégration (et automatiser ce qui est automatisable).
- Intégrer la Q.A. dès le discovery, ou du moins dès les premiers travaux sur la solution pour leur permettre de planifier au mieux la qualification de l’application.
- Tester la sécurité et la performance au plus tôt.