Blog

Améliorer la qualité et la fiabilité grâce à l'intégration continue

par Ranjib Dey 14 avril 2014 | 8 min de lecture

L'intégration continue (CI) est une pratique de développement logiciel dans laquelle les membres fusionnent fréquemment leur travail pour réduire les problèmes et les conflits. Chaque poussée est prise en charge par une construction (et un test) automatisés pour détecter les erreurs. En se vérifiant fréquemment les uns les autres, les équipes peuvent développer des logiciels plus rapidement et de manière plus fiable. En substance, l'intégration continue consiste à vérifier la qualité du code pour s'assurer qu'aucun bug n'est introduit dans l'environnement de production. Si des bugs sont détectés lors des tests, la source est facilement découverte et corrigée. En testant fréquemment le code après chaque validation, vous pouvez réduire l'enquête autour de la source du bug à une période de temps plus courte. Mais tester manuellement le code est pénible et redondant. De nombreux tests peuvent être réutilisés, nous avons donc créé plusieurs tests automatisés pour faciliter les tests fréquents. De plus, comme ces tests sont itératifs, une fois qu'un bug est détecté, nous créons un test qui le recherche dans les futures révisions de code afin que les anciens bugs ne soient plus jamais introduits.

Avant une construction automatisée

DVCS Chez PagerDuty, après avoir décidé de ce que nous devons créer, un ticket JIRA est créé pour collaborer facilement et tenir les membres informés de l'état d'avancement. À l'intérieur du ticket, nous incluons des informations sur ce que cette fonctionnalité ou ce correctif fera et quels sont les impacts connus. Ensuite, nous créons des branches locales à partir de notre dépôt Git pour la fonctionnalité que nous voulons développer ou le problème/bug que nous voulons corriger et nous lui donnons le même nom que le ticket JIRA. Git est un système de contrôle de version distribué (DVCS), il n'y a donc pas de référentiel source unique d'où nous prenons le code, mais il existe plusieurs copies de travail. Cela évite d'avoir un point de défaillance unique dans les référentiels à source unique traditionnels qui reposent sur une seule machine physique. Nous sommes tous pour la redondance ici chez PagerDuty (plusieurs bases de données, plusieurs fournisseurs d'hébergement, plusieurs fournisseurs de contact pour plusieurs méthodes de contact, etc.), donc avoir un DVCS nous permet de développer plus facilement localement même en cas de problèmes. Bazaar et Mercury sont quelques autres DVCS que vous voudrez peut-être consulter.

Écrire d'abord les tests

Bien qu'il serait bien d'avoir des tests automatisés pour tout ce que nous créons, leur création prend du temps. Nos tests sont créés avant que le code ne soit écrit afin que nous puissions les utiliser pour régir nos conceptions et éviter d'écrire du code difficile à tester. Ce développement piloté par les tests (TDD) améliore la conception des logiciels et nous aide à maintenir le code plus facilement. Nous priorisons les critères de test dans l'ordre ci-dessous car ils ont le plus grand impact sur la fiabilité et les ressources.

1. Sécurité – Les bugs critiques qui bloquent nos flux de travail entrent dans cette catégorie. Si le correctif modifie un chemin de code critique pour l'entreprise, nous voulons nous assurer que tout est testé.

2. Stratégique – Réorganisation du code à grande échelle, ajout de nouvelles fonctionnalités. Ces tests ont tendance à ajouter des spécifications correspondantes dans notre suite de tests. Cela permet de traiter à la fois les scénarios de chemin heureux et les régressions connues. Par exemple, l'ajout de différents types de services/microservices (un nouveau magasin persistant) ou un nouvel outil (qui automatise un travail manuel répétitif et de longue durée).

3. Cohérence – En tant qu’équipe en pleine croissance, nous devons nous assurer que le code créé est facile à comprendre pour une personne novice. Cet exercice est une bonne pratique établie pour la qualité du code, la gestion des erreurs et l’identification des problèmes de performances. Toute personne connaissant Chef devrait être capable de comprendre notre base de code. Par exemple, l’isolation de nos personnalisations et leur capture sous forme de correctifs/bibliothèques distincts, puis l’envoi de ces correctifs aux projets en amont. Dans ces scénarios, nous écrivons des spécifications pour la couche d’intégration (c’est-à-dire la partie colle qui relie les extensions aux bibliothèques externes, comme les livres de recettes communautaires, les gemmes, les outils, etc.).

4. Connaissances partagées – Chaque fonctionnalité a des hypothèses valides ou un certain domaine. Nous utilisons des tests pour établir quels sont ces domaines afin de connaître les limites d'une fonctionnalité. Ceux-ci sont très spécifiques à notre propre infrastructure, à ses dépendances et à sa topologie globale. Un exemple est la façon dont nous générons des fichiers de configuration dynamiques pilotés par la recherche pour différents services (comme nous trions toujours les résultats de recherche avant de les consommer). Nous écrivons des tests pour valider et appliquer ces hypothèses, qui sont également exploitées par les chaînes d'outils en aval (comme les conventions de dénomination sur les serveurs, les environnements, etc.).

Notre suite de tests

Les tests que nous écrivons se répartissent en 5 catégories. Tout le code créé doit réussir les tests dans l'ordre ci-dessous, à l'exception des tests de charge, avant d'être déployé pour maintenir la qualité et la fiabilité.

Tests sémantiques : Nous utilisons des contrôles Lint pour la sémantique globale du code et les meilleures pratiques courantes et Rubocop pour le peluchage du rubis et Critique gastronomique pour le linting spécifique au chef. Ce sont des outils sensibles au code, donc selon le langage dans lequel vous écrivez, ces outils peuvent ou non fonctionner pour vous. Les outils de linting sont appliqués globalement après chaque validation et nous n'avons pas besoin d'écrire de code supplémentaire pour cela.

Il existe plusieurs cas où les tests de lint ont détecté des bugs réels en plus de signaler des erreurs de style. Par exemple, foodcritic peut détecter les ressources de chef qui n'envoient pas de notification lors de la mise à jour.

Tests unitaires : Nous écrivons des tests unitaires pour presque chaque élément de code. Si nous développons des recettes de chef, les tests chefspec sont écrits en premier. Si nous écrivons des bibliothèques Ruby brutes, les tests rspec sont écrits en premier. Les tests Lint et unitaires ne recherchent pas de fonctionnalités. Ils testent si le code a une bonne ou une mauvaise conception.

Une bonne conception permet aux autres membres de récupérer facilement le code et de le comprendre rapidement. De plus, ces tests montrent à quel point il est facile de découpler le code. La technologie évolue constamment et le code doit être flexible. Si Ubuntu ou Nginx publie un correctif pour des raisons de sécurité, est-il facile d'accepter ce changement ?

Tests fonctionnels : Ces tests ont pour but de vérifier la fonctionnalité dans son ensemble, sans aucune connaissance d'implémentation, sans se moquer ou stubber d'un sous-composant. Nous nous efforçons également de rendre les spécifications fonctionnelles aussi lisibles que possible par l'homme, en anglais simple, sans aucune construction spécifique au langage de programmation.

Ces tests aident à :

  • nouveau provisionnement du serveur

  • démontage du serveur existant

  • un provisionnement de cluster entier

  • si une séquence d'opérations fonctionne ou non

Nous utilisons Concombre et aruba pour écrire des tests fonctionnels. Ces tests ne se préoccupent pas de la manière dont le code est écrit mais seulement de son bon fonctionnement. Cucumber est un outil BDD qui permet d'écrire des spécifications de manière lisible (en utilisant gherkin), tandis qu'aruba est une extension de concombre qui permet de tester des applications en ligne de commande. Étant donné qu'une grande majorité de nos outils fournissent une interface de ligne de commande (CLI), nous trouvons ces outils de test très pratiques et faciles à utiliser.

Tests d'intégration : Ces tests permettent de s'assurer que tout fonctionne lorsqu'il est combiné avec tous les autres services au sein d'une topologie de type production par rapport à un modèle de trafic de type production. Cela nous aide également à déterminer si notre suite d'automatisation du système fonctionnera parfaitement avec différents services et par rapport à toutes les modifications apportées à ceux-ci ou à d'autres services tiers que nous consommons.

Tests de charge : Cela nous aidera à déterminer à quelle échelle de trafic nous pouvons gérer. Et à identifier rapidement les principaux goulots d'étranglement des performances. Nous effectuons une série de tâches de configuration pour nous assurer que nous disposons d'un volume de données de type production. En général, ces tests prennent du temps et nécessitent beaucoup de ressources, c'est pourquoi ils sont effectués périodiquement sur un ensemble de modifications de code (traitement par lots). Les modifications de code pour lesquelles nous pensons que les performances ne sont pas un problème (modifications de configuration, ajustements de l'interface utilisateur) contournent parfois ces tests.

Automatisation du déploiement et de la vérification de l'intégrité

HipChat_continuous_integration Une fois que le code a passé tous les tests, nous le transmettons à un autre membre de l'équipe pour qu'il effectue un contrôle de cohérence avant de le publier. Nous effectuons un examen manuel du code pour obtenir un deuxième avis afin de garantir qu'aucun bug n'est introduit dans la production. L'examen du code par les pairs permet de garantir qu'aucune exigence n'a été oubliée et que le code répond aux normes de conception.

Nous suivons un déploiement semi-automatique, où l'intégration continue aide à tester et à projeter des outils spécifiques (comme Capistrano et Chef) et aide au déploiement, mais le processus de déploiement réel est déclenché manuellement. L'outil de déploiement lui-même enverra un message dans la salle HipChat de PagerDuty pour informer tout le monde lorsqu'un élément est en cours de déploiement. Il envoie ensuite des notifications avant et après le déploiement (sous forme de messages de service de verrouillage et de déverrouillage). Cela nous aide à comprendre ce qui est déployé et également à éviter les déploiements simultanés.

Avec l'intégration continue, nous créons une qualité de base de logiciel qui doit être respectée et maintenue, ce qui réduit les risques liés à nos versions.