Partie 1 : Go, Docker et agents de compilation auto-hébergés
Qu’est-ce qu’Azure DevOps ?
Azure DevOps est une plateforme web de Microsoft qui fournit des outils pour divers domaines dans l’environnement des projets informatiques :
- Azure Boards pour la gestion de projet
- Azure Pipelines pour CI/CD
- Azure Repos pour la gestion du code source
- Azure Test Plans pour les tests manuels
- Azure Artifacts pour la gestion des artefacts
Les outils s’interconnectent, par exemple, les éléments de travail d’Azure Boards peuvent être liés aux demandes d’extraction dans Azure Repos. Avant qu’une demande d’extraction ne puisse être fusionnée, un pipeline dans Azure Pipelines doit confirmer l’exactitude du code et finalement, il télécharge un artefact vers Azure Artifacts.
Dans cette série d’articles, nous n’utiliserons qu’Azure Repos et Azure Pipelines.
Le premier pipeline CI – Go et Docker
Notre premier cas d’utilisation est un microservice en Go, qui doit être installé via Docker. Nous allons créer un pipeline CI qui devra faire ce qui suit :
- Construire et tester le microservice Go
- Construire une image Docker
- Téléverser l’image Docker dans un registre Docker
Azure DevOps offre deux possibilités pour créer des pipelines : via une interface graphique ou via des fichiers YAML, qui sont enregistrés dans le dépôt Git. Habituellement, ce fichier est validé dans le répertoire racine du dépôt Git sous le nom azure-pipelines.yaml (mais le nom est librement choisi). Comme nous développons nos pipelines en équipe, que nous voulons les documenter, suivre les changements et réutiliser des sections, nous optons pour la variante avancée avec les fichiers YAML.
Le microservice a une structure très simple : il démarre simplement un serveur HTTP avec un point de terminaison REST :
Maintenant, passons à la partie passionnante : le pipeline CI. Dans Azure DevOps, la partie exécutable d’un pipeline se compose d’étapes (Stages), une étape se compose de tâches (Jobs) et une tâche se compose d’étapes (Steps). Pour notre cas simple, une étape avec une tâche est tout à fait suffisante. La fonctionnalité d’une étape est décrite au moyen d’une tâche (Task), par exemple, il existe une tâche Go, une tâche Docker et une tâche de vérification Git. Avec les tâches Bash ou PowerShell, il est possible d’exécuter ses propres scripts. Pour des cas d’utilisation plus complexes, il est également possible de développer ses propres tâches en TypeScript. Comme le processus de construction est déjà entièrement défini dans le Dockerfile, nous n’avons besoin que de docker build et docker push comme étapes de construction. Pour cela, nous utilisons la tâche Docker.
En plus du processus de construction proprement dit, nous pouvons définir dans le pipeline
- quels événements déclencheront automatiquement le pipeline.
- quelles variables et groupes de variables doivent être utilisés.
- si le pipeline doit être paramétré.
- si d’autres dépôts Git doivent être vérifiés.
Tous ces paramètres peuvent également être omis, dans ce cas le pipeline sera automatiquement déclenché pour chaque Push Git, n’aura pas de variables ni de paramètres et seul le propre dépôt Git sera vérifié. C’est aussi le comportement que nous voulons pour notre pipeline.
Au sein du pipeline, nous avons accès à certaines variables prédéfinies qui nous renseignent, entre autres, sur le nom, l’organisation et le commit vérifié du dépôt Git. Une liste de toutes les variables prédéfinies est disponible ici. Bien entendu, il est également possible de définir ses propres variables lors de l’exécution et ainsi de transmettre des données entre les étapes. Nous utilisons
- $(Build.SourcesDirectory), le chemin sur l’agent de construction où le dépôt Git est vérifié comme préfixe de chemin pour l’accès aux fichiers.
- $(Build.Repository.Name), le nom du dépôt Git comme nom pour l’image Docker.
- $(Build.SourceVersion), le hash de commit en tant que balise pour l’image Docker
La définition finale du pipeline se présente donc comme suit :
Nous avons spécifié « docker-hub » comme ContainerRegistry cible. Il s’agit d’une référence à ce que l’on appelle une connexion de service, qui décrit de manière générale une connexion à un service externe dans Azure DevOps. Ainsi, le pipeline peut l’utiliser sans qu’aucune information d’identification ne doive être stockée directement dans le pipeline. Pour créer une nouvelle connexion de service, nous allons dans les « Paramètres du projet » et là, sous « Pipelines » –> « Connexions de service ». Nous créons une connexion à un registre Docker dans Docker Hub. La condition préalable est un compte Docker (gratuit). Bien entendu, nous pourrions également utiliser n’importe quel autre registre Docker.
Ensuite, nous devons saisir nos informations d’identification pour le compte Docker Hub. Important : un jeton d’accès doit être créé au préalable dans Docker Hub comme mot de passe.
Après avoir commité tous les fichiers, il ne nous reste plus qu’à créer le pipeline dans l’interface d’Azure DevOps et à faire référence à notre azure-pipelines.yaml. Pour ce faire, nous cliquons sur « Créer un pipeline » sous « Pipelines » :
Tests unitaires et couverture de code
Tout bon pipeline comprend également des tests automatisés. C’est pourquoi nous allons maintenant étendre notre projet Go avec un test unitaire et inclure une étape dans le pipeline qui exécute les tests. Si les tests échouent, le pipeline doit s’arrêter et l’image Docker ne doit être ni construite ni poussée.
Notre premier test lance une requête HTTP au serveur autonome et vérifie la réponse :
Le test fonctionne déjà localement :
Nous allons maintenant ajouter les tests au pipeline. Azure DevOps propose deux tâches prédéfinies pour les projets Go : GoTool et Go. Avec GoTool, nous sélectionnons la version de Go pour le pipeline, avec Go, nous pouvons ensuite exécuter n’importe quelle commande Go. Avant d’exécuter les tests, nous construisons notre projet Go. Même si ce n’est pas absolument nécessaire, cela aide à déterminer si une erreur se produit déjà lors de la construction (erreur syntaxique) ou seulement lors de l’exécution des tests (erreur sémantique). Deux étapes sont nécessaires pour la construction : go mod download pour télécharger les bibliothèques et go build pour compiler. Les tests sont ensuite exécutés avec go test. Nous étendons donc le pipeline comme suit :
Pour vérifier précisément quel test a échoué et pourquoi, nous devons consulter le journal. Avec un seul test, ce n’est pas un problème, mais si nous avons des centaines de tests, nous n’avons pas le temps de faire défiler des milliers de lignes ici pour trouver les tests qui ont échoué. De plus, nous ne voyons pas immédiatement quel pourcentage de tests a échoué. Heureusement, Azure DevOps offre ici une interface pour fournir les résultats des tests au format XML JUnit. Pour pouvoir l’utiliser, nous devons cependant convertir la sortie de go test dans ce format.
Fort heureusement, quelqu’un d’autre a déjà fait ce travail pour nous et a écrit un outil Go correspondant : https://github.com/jstemmer/go-junit-report. Nous nous intéressons également à la couverture des tests. Ici aussi, il existe une interface d’Azure DevOps et des outils prêts à l’emploi pour la conversion au bon format.
Pour ce processus complexe dans son ensemble, nous créons une tâche Bash qui accomplira les actions suivantes : Tout d’abord, elle téléchargera les outils nécessaires, puis exécutera les tests, tout en mémorisant le code de retour pour une utilisation ultérieure. En effet, nous souhaitons utiliser le code de retour de go test comme code de retour pour l’ensemble de l’étape, afin qu’Azure DevOps puisse déterminer si l’étape a échoué ou non. Cependant, avant cela, nous devons encore préparer le rapport et la couverture, tant en cas de succès que d’échec. Ensuite, nous ajouterons les deux tâches PublishTestResults et PublishCodeCoverageResults. Il est important d’ajouter ici la condition: succeededOrFailed(). Normalement, les étapes suivantes ne sont pas exécutées si une étape échoue (c’est-à-dire que la valeur par défaut est condition: succeeded()), mais avec condition: succeededOrFailed(), elles sont exécutées même si les étapes précédentes ont échoué, contrairement à condition: always(), mais pas si le pipeline a été annulé manuellement.
Note en marge, si les builds doivent s’exécuter sur un agent de build auto-hébergé : la tâche PublishCodeCoverageResults nécessite qu’un runtime .NET soit installé sur l’agent de build.
Voici maintenant le pipeline finalisé :
Conclusion
Azure DevOps offre une excellente opportunité de créer rapidement et confortablement des pipelines. L’interface graphique est épurée et facile à comprendre, ce qui la rend particulièrement recommandable pour les débutants dans le domaine du DevOps. À cet effet, la création de pipelines via l’interface graphique Drop est particulièrement appropriée. La connexion à des services externes est également simple et rapide à réaliser. Néanmoins, grâce au passage à la syntaxe YAML pour les pipelines, il convient également aux cas d’utilisation plus complexes. Ainsi, il est possible de définir des pipelines d’une complexité presque illimitée. Dans cet article, nous n’avons examiné jusqu’à présent qu’une fraction des fonctionnalités d’Azure DevOps.
Dans la partie 2, nous allons :
- Intégrer un autre projet Go, y compris un pipeline, en tant que dépendance dans notre premier projet Go.
- Créer un modèle de pipeline réutilisable pour de futurs pipelines.
- Intégrer un algorithme de versionnement intelligent selon le Semantic Versioning dans le pipeline.