J'ai découvert le concept d'intégration continue (CI) pour la première fois lors du lancement du projet Mozilla. Il comprenait un serveur de construction rudimentaire dans le cadre du processus, ce qui était révolutionnaire à l'époque. Je maintenais un projet C++ qui prenait 2 heures à construire et à lier.
Nous avons rarement traversé une construction propre qui a créé des problèmes aggravants car un mauvais code était engagé dans le projet.
Beaucoup de choses ont changé depuis ces jours anciens. Les produits CI sont partout et en tant que développeurs Java, nous bénéficions d'une richesse de fonctionnalités comme jamais auparavant. Mais je m'avance… Commençons par les bases.
L'intégration continue est une pratique de développement logiciel dans laquelle les changements de code sont automatiquement créés et testés de manière fréquente et cohérente.
L'objectif de CI est d'identifier et de résoudre les problèmes d'intégration dès que possible, réduisant ainsi le risque de bogues et d'autres problèmes glissant dans la production.
CI va souvent de pair avec la livraison continue (CD) qui vise à automatiser l'ensemble du processus de livraison du logiciel, de l'intégration du code au déploiement en production.
L'objectif du CD est de réduire le temps et les efforts nécessaires pour déployer de nouvelles versions et correctifs, permettant aux équipes de fournir de la valeur aux clients plus rapidement et plus fréquemment.
Avec CD, chaque modification de code qui réussit les tests CI est considérée comme prête pour le déploiement, ce qui permet aux équipes de déployer de nouvelles versions à tout moment en toute confiance. Je n'aborderai pas la livraison continue dans cet article, mais j'y reviendrai car il y a beaucoup à discuter.
Je suis un grand fan du concept, mais il y a certaines choses que nous devons surveiller.
Il existe de nombreux outils puissants d'intégration continue. Voici quelques outils couramment utilisés :
Jenkins : Jenkins est l'un des outils CI les plus populaires, offrant une large gamme de plugins et d'intégrations pour prendre en charge divers langages de programmation et outils de construction. Il est open-source et offre une interface conviviale pour la configuration et la gestion des pipelines de construction.
Il est écrit en Java et a souvent été mon "outil de prédilection". Cependant, c'est une douleur à gérer et à mettre en place. Il existe des solutions «Jenkins en tant que service» qui nettoient également son expérience utilisateur, ce qui fait quelque peu défaut.
Remarquez que je n'ai pas mentionné les actions GitHub auxquelles nous reviendrons sous peu. Il y a plusieurs facteurs à considérer lors de la comparaison des outils CI :
En général, Jenkins est connu pour sa polyvalence et sa vaste bibliothèque de plugins, ce qui en fait un choix populaire pour les équipes avec des pipelines de construction complexes. Travis CI et CircleCI sont connus pour leur facilité d'utilisation et leur intégration avec les outils SCM populaires, ce qui en fait un bon choix pour les petites et moyennes équipes.
GitLab CI/CD est un choix populaire pour les équipes utilisant GitLab pour la gestion de leur code source car il offre des fonctionnalités CI/CD intégrées. Bitbucket Pipelines est un bon choix pour les équipes qui utilisent Bitbucket pour la gestion de leur code source, car il s'intègre parfaitement à la plateforme.
L'hébergement des agents est un facteur important à prendre en compte lors du choix d'une solution CI. Il existe deux options principales pour l'hébergement d'agents : basé sur le cloud et sur site.
Lors du choix d'une solution CI, il est important de prendre en compte les besoins et les exigences spécifiques de votre équipe.
Par exemple, si vous avez un pipeline de build volumineux et complexe, une solution sur site telle que Jenkins peut être un meilleur choix, car elle vous donne plus de contrôle sur l'infrastructure sous-jacente.
D'un autre côté, si vous avez une petite équipe avec des besoins simples, une solution basée sur le cloud telle que Travis CI peut être un meilleur choix, car elle est facile à configurer et à gérer.
L'état détermine si les agents conservent leurs données et leurs configurations entre les générations.
Il y a un débat animé parmi les partisans de l'IC concernant la meilleure approche. Les agents sans état fournissent un environnement propre et facile à reproduire. Je les choisis dans la plupart des cas et je pense qu'ils constituent la meilleure approche.
Les agents sans état peuvent également être plus chers car ils sont plus lents à mettre en place. Puisque nous payons pour les ressources cloud, ce coût peut s'additionner. Mais la principale raison pour laquelle certains développeurs préfèrent les agents avec état est la capacité à enquêter.
Avec un agent sans état, lorsqu'un processus CI échoue, vous n'avez généralement aucun moyen d'investigation autre que les journaux.
Avec un agent avec état, nous pouvons nous connecter à la machine et essayer d'exécuter le processus manuellement sur la machine donnée. Nous pourrions reproduire un problème qui a échoué et gagner en compréhension grâce à cela.
Une entreprise avec laquelle j'ai travaillé a choisi Azure plutôt que GitHub Actions car Azure autorisait les agents avec état. C'était important pour eux lors du débogage d'un processus CI défaillant.
Je ne suis pas d'accord avec ça, mais c'est une opinion personnelle. J'ai l'impression d'avoir passé plus de temps à dépanner un mauvais nettoyage d'agent qu'à rechercher un bogue. Mais c'est une expérience personnelle, et certains de mes amis intelligents ne sont pas d'accord.
Les builds répétables font référence à la capacité de produire exactement les mêmes artefacts logiciels chaque fois qu'une build est effectuée, quel que soit l'environnement ou l'heure à laquelle la build est effectuée.
Du point de vue DevOps, il est essentiel de disposer de versions reproductibles pour garantir la cohérence et la fiabilité des déploiements de logiciels.
Les pannes intermittentes sont le fléau de DevOps partout, et elles sont pénibles à suivre.
Malheureusement, il n'y a pas de solution facile. Autant que nous l'aimerions, une certaine flakiness se retrouve dans des projets d'une complexité raisonnable. Il est de notre devoir de minimiser cela autant que possible. Il existe deux bloqueurs aux builds répétables :
Lors de la définition des dépendances, nous devons nous concentrer sur des versions spécifiques. Il existe de nombreux schémas de version, mais au cours de la dernière décennie, la version sémantique standard à trois chiffres a pris le dessus sur l'industrie.
Ce schéma est extrêmement important pour CI car son utilisation peut avoir un impact significatif sur la répétabilité d'une construction, par exemple avec maven, nous pouvons faire :
<dependency> <groupId>group</groupId> <artifactId>artifact</artifactId> <version>2.3.1</version> </dependency>
Ceci est très spécifique et excellent pour la répétabilité. Cependant, cela pourrait devenir obsolète rapidement. Nous pouvons remplacer le numéro de version par LATEST
ou RELEASE
qui obtiendra automatiquement la version actuelle. C'est mauvais car les builds ne seront plus reproductibles.
Cependant, l'approche à trois chiffres codée en dur est également problématique. Il arrive souvent qu'une version de correctif représente un correctif de sécurité pour un bogue. Dans ce cas, nous voudrions mettre à jour jusqu'à la dernière mise à jour mineure, mais pas les versions les plus récentes.
Par exemple, pour ce cas précédent, je voudrais utiliser la version 2.3.2
implicitement et non 2.4.1
. Cela compromet une certaine répétabilité pour les mises à jour de sécurité mineures et les bogues.
Mais une meilleure façon serait d'utiliser le plugin Maven Versions et d'invoquer périodiquement la commande mvn versions:use-latest-releases
. Cela met à jour les versions les plus récentes pour maintenir notre projet à jour.
C'est la partie la plus simple des builds reproductibles. La difficulté réside dans les tests feuilletés. C'est une douleur si courante que certains projets définissent un "nombre raisonnable" de tests échoués, et certains projets réexécutent la construction plusieurs fois avant de reconnaître l'échec.
Une cause majeure de la flakiness des tests est la fuite d'état. Les tests peuvent échouer en raison d'effets secondaires subtils laissés par un test précédent. Idéalement, un test devrait se nettoyer après lui-même afin que chaque test s'exécute de manière isolée.
Dans un monde parfait, nous exécuterions chaque test dans un nouvel environnement complètement isolé, mais ce n'est pas pratique. Cela signifierait que les tests prendraient trop de temps à s'exécuter et que nous aurions besoin d'attendre beaucoup de temps pour le processus CI.
Nous pouvons écrire des tests avec différents niveaux d'isolement ; Parfois, nous avons besoin d'un isolement complet et pouvons avoir besoin de faire tourner un conteneur pour un test. Mais la plupart du temps, nous ne le faisons pas et la différence de vitesse est importante.
Le nettoyage après les tests est très difficile. Parfois, des fuites d'état provenant d'outils externes tels que la base de données peuvent entraîner un échec de test instable. Pour assurer la répétabilité de l'échec, il est courant de trier les cas de test de manière cohérente ; cela garantit que les futures exécutions de la construction s'exécuteront dans le même ordre.
C'est un sujet très débattu. Certains ingénieurs pensent que cela encourage les tests buggés et cache des problèmes que nous ne pouvons découvrir qu'avec un ordre aléatoire des tests. D'après mon expérience, cela a effectivement trouvé des bogues dans les tests, mais pas dans le code.
Mon objectif n'est pas de créer des tests parfaits, et je préfère donc exécuter les tests dans un ordre cohérent, tel que l'ordre alphabétique.
Il est important de conserver des statistiques sur les échecs des tests et de ne jamais appuyer simplement sur Réessayer. En suivant les tests problématiques et l'ordre d'exécution d'un échec, nous pouvons souvent trouver la source du problème.
La plupart du temps, la cause première de l'échec se produit en raison d'un nettoyage défectueux lors d'un test précédent, c'est pourquoi l'ordre est important et sa cohérence est également importante.
Nous sommes ici pour développer un produit logiciel, pas un outil CI. L'outil CI est là pour améliorer le processus. Malheureusement, l'expérience avec l'outil CI est souvent si frustrante que nous finissons par consacrer plus de temps à la logistique qu'à l'écriture de code.
Souvent, je passais des jours à essayer de passer une vérification CI afin de pouvoir fusionner mes modifications. Chaque fois que je me rapproche, un autre développeur fusionnerait d'abord son changement et casserait ma construction.
Cela contribue à une expérience de développeur moins que stellaire, en particulier lorsqu'une équipe évolue et que nous passons plus de temps dans la file d'attente CI qu'à fusionner nos modifications. Il y a beaucoup de choses que nous pouvons faire pour atténuer ces problèmes :
En fin de compte, cela est directement lié à la productivité des développeurs. Mais nous n'avons pas de profileurs pour ce genre d'optimisations. Nous devons mesurer à chaque fois; cela peut être fastidieux.
GitHub Actions est une plateforme d'intégration/livraison continue (CI/CD) intégrée à GitHub. Il est sans état bien qu'il permette dans une certaine mesure l'auto-hébergement d'agents. Je me concentre dessus car il est gratuit pour les projets open-source et dispose d'un quota gratuit décent pour les projets à code source fermé.
Ce produit est un concurrent relativement nouveau dans le domaine, il n'est pas aussi flexible que la plupart des autres outils CI mentionnés précédemment. Cependant, il est très pratique pour les développeurs grâce à son intégration profonde avec GitHub et les agents sans état.
Pour tester GitHub Actions, nous avons besoin d'un nouveau projet que, dans ce cas, j'ai généré à l'aide de JHipster avec la configuration vue ici :
J'ai créé un projet séparé qui illustre l'utilisation des actions GitHub ici. Notez que vous pouvez suivre cela avec n'importe quel projet ; bien que nous incluions des instructions maven dans ce cas, le concept est très simple.
Une fois le projet créé, nous pouvons ouvrir la page du projet sur GitHub et passer à l'onglet actions.
Nous verrons quelque chose comme ceci :
Dans le coin inférieur droit, nous pouvons voir le type de projet Java avec Maven. Une fois que nous avons choisi ce type, nous passons à la création d'un fichier maven.yml
comme indiqué ici :
Malheureusement, le maven.yml par défaut suggéré par GitHub comporte un problème. Voici le code que nous voyons dans cette image :
name: Java CI with Maven on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
Les trois dernières lignes mettent à jour le graphe de dépendance. Mais cette fonctionnalité échoue, ou du moins elle a échoué pour moi. Leur suppression a résolu le problème. Le reste du code est une configuration YAML standard.
Les lignes pull_request
et push
en haut du code déclarent que les builds s'exécuteront à la fois sur une demande d'extraction et sur une poussée vers le maître. Cela signifie que nous pouvons exécuter nos tests sur une demande d'extraction avant de valider. Si le test échoue, nous ne nous engagerons pas.
Nous pouvons interdire la validation avec des tests ayant échoué dans les paramètres du projet. Une fois que nous avons validé le fichier YAML, nous pouvons créer une demande d'extraction et le système exécutera le processus de construction pour nous. Cela inclut l'exécution des tests puisque la cible "package" dans maven exécute les tests par défaut.
Le code qui invoque les tests se trouve dans la ligne commençant par "run" vers la fin. Il s'agit en fait d'une ligne de commande Unix standard. Parfois, il est logique de créer un script shell et de l'exécuter simplement à partir du processus CI.
Il est parfois plus facile d'écrire un bon script shell que de gérer tous les fichiers YAML et les paramètres de configuration des différentes piles CI.
Il est également plus portable si nous choisissons de changer d'outil CI à l'avenir. Ici, nous n'en avons pas besoin puisque maven est suffisant pour nos besoins actuels.
Nous pouvons voir la demande d'extraction réussie ici :
Pour tester cela, nous pouvons ajouter un bogue au code en changeant le point de terminaison “/api”
en “/myapi”
. Cela produit l'échec illustré ci-dessous. Cela déclenche également un e-mail d'erreur envoyé à l'auteur du commit.
Lorsqu'un tel échec se produit, nous pouvons cliquer sur le lien "Détails" sur le côté droit. Cela nous amène directement au message d'erreur que vous voyez ici :
Malheureusement, il s'agit généralement d'un message inutile qui n'aide pas à résoudre le problème. Cependant, le défilement vers le haut montrera l'échec réel qui est généralement mis en évidence pour nous, comme on le voit ici :
Notez qu'il y a souvent plusieurs échecs, il serait donc prudent de faire défiler vers le haut. Dans cette erreur, nous pouvons voir que l'échec était une assertion à la ligne 394
de AccountResourceIT que vous pouvez voir ici, notez que les numéros de ligne ne correspondent pas. Dans ce cas, la ligne 394
est la dernière ligne de la méthode :
@Test @Transactional void testActivateAccount() throws Exception { final String activationKey = "some activation key"; User user = new User(); user.setLogin("activate-account"); user.setEmail("[email protected]"); user.setPassword(RandomStringUtils.randomAlphanumeric(60)); user.setActivated(false); user.setActivationKey(activationKey); userRepository.saveAndFlush(user); restAccountMockMvc.perform(get("/api/activate?key={activationKey}", activationKey)).andExpect(status().isOk()); user = userRepository.findOneByLogin(user.getLogin()).orElse(null); assertThat(user.isActivated()).isTrue(); }
Cela signifie que l'appel assert a échoué. isActivated()
a renvoyé false
et a échoué au test. Cela devrait aider un développeur à affiner le problème et à comprendre la cause première.
Comme nous l'avons mentionné précédemment, CI concerne la productivité des développeurs. Nous pouvons aller bien plus loin que simplement compiler et tester. Nous pouvons appliquer des normes de codage, pelucher le code, détecter les vulnérabilités de sécurité, et bien plus encore.
Dans cet exemple, intégrons Sonar Cloud qui est un puissant outil d'analyse de code (linter). Il détecte les bogues potentiels dans votre projet et vous aide à améliorer la qualité du code.
SonarCloud est une version cloud de SonarQube qui permet aux développeurs d'inspecter et d'analyser en continu leur code pour trouver et résoudre les problèmes liés à la qualité, à la sécurité et à la maintenabilité du code. Il prend en charge divers langages de programmation tels que Java, C #, JavaScript, Python, etc.
SonarCloud s'intègre aux outils de développement populaires tels que GitHub, GitLab, Bitbucket, Azure DevOps, etc. Les développeurs peuvent utiliser SonarCloud pour obtenir des commentaires en temps réel sur la qualité de leur code et améliorer la qualité globale du code.
D'autre part, SonarQube est une plate-forme open source qui fournit des outils d'analyse de code statique aux développeurs de logiciels. Il fournit un tableau de bord qui affiche un résumé de la qualité du code et aide les développeurs à identifier et résoudre les problèmes liés à la qualité, à la sécurité et à la maintenabilité du code.
SonarCloud et SonarQube offrent des fonctionnalités similaires, mais SonarCloud est un service basé sur le cloud et nécessite un abonnement, tandis que SonarQube est une plate-forme open source qui peut être installée sur site ou sur un serveur cloud.
Par souci de simplicité, nous utiliserons SonarCloud mais SonarQube devrait très bien fonctionner. Pour commencer, nous allons sur sonarcloud.io et nous nous inscrivons. Idéalement avec notre compte GitHub. On nous présente alors une option pour ajouter un référentiel pour la surveillance par Sonar Cloud comme indiqué ici :
Lorsque nous sélectionnons l'option Analyser une nouvelle page, nous devons autoriser l'accès à notre référentiel GitHub. L'étape suivante consiste à sélectionner les projets que nous souhaitons ajouter à Sonar Cloud, comme indiqué ici :
Une fois que nous avons sélectionné et procédé au processus de configuration, nous devons choisir la méthode d'analyse. Puisque nous utilisons GitHub Actions, nous devons choisir cette option à l'étape suivante, comme illustré ici :
Une fois cela défini, nous entrons dans la dernière étape de l'assistant Sonar Cloud, comme illustré dans l'image suivante. Nous recevons un jeton que nous pouvons copier (l'entrée 2 est floue dans l'image), et nous l'utiliserons sous peu.
Notez qu'il existe également des instructions par défaut à utiliser avec maven qui apparaissent une fois que vous avez cliqué sur le bouton intitulé "Maven".
Pour en revenir au projet dans GitHub, nous pouvons passer à l'onglet des paramètres du projet (à ne pas confondre avec les paramètres du compte dans le menu du haut). Ici, nous sélectionnons "Secrets et variables" comme indiqué ici :
Dans cette section, nous pouvons ajouter un nouveau secret de référentiel, en particulier la clé et la valeur SONAR_TOKEN que nous avons copiées depuis SonarCloud, comme vous pouvez le voir ici :
GitHub Repository Secrets est une fonctionnalité qui permet aux développeurs de stocker en toute sécurité des informations sensibles associées à un référentiel GitHub, telles que des clés API, des jetons et des mots de passe, qui sont nécessaires pour authentifier et autoriser l'accès à divers services ou plates-formes tiers utilisés par le référentiel. .
Le concept derrière GitHub Repository Secrets est de fournir un moyen sûr et pratique de gérer et de partager des informations confidentielles, sans avoir à exposer publiquement les informations dans le code ou les fichiers de configuration.
En utilisant des secrets, les développeurs peuvent séparer les informations sensibles de la base de code et les empêcher d'être exposées ou compromises en cas de violation de la sécurité ou d'accès non autorisé.
Les secrets du référentiel GitHub sont stockés en toute sécurité et ne sont accessibles qu'aux utilisateurs autorisés qui ont obtenu l'accès au référentiel. Les secrets peuvent être utilisés dans les flux de travail, les actions et d'autres scripts associés au référentiel.
Ils peuvent être passés en tant que variables d'environnement au code afin qu'il puisse accéder et utiliser les secrets de manière sécurisée et fiable.
Dans l'ensemble, GitHub Repository Secrets offre aux développeurs un moyen simple et efficace de gérer et de protéger les informations confidentielles associées à un référentiel, contribuant ainsi à garantir la sécurité et l'intégrité du projet et des données qu'il traite.
Nous devons maintenant intégrer cela dans le projet. Tout d'abord, nous devons ajouter ces deux lignes au fichier pom.xml. Notez que vous devez mettre à jour le nom de l'organisation pour qu'il corresponde au vôtre. Ceux-ci doivent aller dans la section du XML :
<sonar.organization>shai-almog</sonar.organization> <sonar.host.url>https://sonarcloud.io</sonar.host.url>
Notez que le projet JHipster que nous avons créé prend déjà en charge SonarQube qui doit être supprimé du fichier pom avant que ce code ne fonctionne.
Après cela, nous pouvons remplacer la partie "Build with Maven" du fichier maven.yml
par la version suivante :
- name: Build with Maven env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=shai-almog_HelloJHipster package
Une fois que nous aurons fait cela, SonarCloud fournira des rapports pour chaque demande d'extraction fusionnée dans le système, comme indiqué ici :
Nous pouvons voir un rapport qui comprend une liste de bogues, de vulnérabilités, d'odeurs et de problèmes de sécurité. Cliquer sur chacun de ces problèmes nous amène à quelque chose comme ceci :
Notez que nous avons des onglets qui expliquent exactement pourquoi le problème est un problème, comment le résoudre, et plus encore. Il s'agit d'un outil remarquablement puissant qui est l'un des réviseurs de code les plus précieux de l'équipe.
Deux autres éléments intéressants que nous avons vus précédemment sont les rapports de couverture et de duplication. SonarCloud s'attend à ce que les tests aient une couverture de code de 80 % (déclenchent 80 % du code dans une demande d'extraction), ce qui est élevé et peut être configuré dans les paramètres.
Il signale également un code en double qui pourrait indiquer une violation du principe Ne vous répétez pas (DRY).
CI est un vaste sujet avec de nombreuses opportunités pour améliorer le flux de votre projet. Nous pouvons automatiser la détection des bogues. Rationalisez la génération d'artefacts, la livraison automatisée et bien plus encore. Mais à mon humble avis, le principe de base derrière CI est l'expérience du développeur.
Il est là pour nous faciliter la vie.
Lorsqu'il est mal fait, le processus CI peut transformer cet outil incroyable en cauchemar. Passer les tests devient un exercice futile. Nous réessayons encore et encore jusqu'à ce que nous puissions enfin fusionner. Nous attendons des heures pour fusionner à cause des files d'attente lentes et encombrées.
Cet outil censé aider devient notre ennemi juré. Cela ne devrait pas être le cas. CI devrait nous faciliter la vie, et non l'inverse.