Microservices : CI/CD, environnement, monitoring et logs

CI (Continuous Integration), CD (Continuous Delivery), DevOps, Kubernetes, Monitoring… Cela fait quelques années que l’on voit ces termes fleurir un peu partout. Néanmoins, entre le discours théorique destiné aux décisionnaires, le discours commercial mis en avant par les créateurs de solutions logicielles, et la réalité des faits, il est difficile de se construire une conviction, et surtout de discerner concrètement quoi mettre en place et les écueils à éviter.

Alexandre Brun
neoxia
15 min readFeb 11, 2020

--

Cet article est le deuxième d’une série traitant de l’architecture orientée Microservices rédigée par thomas.ruiz et moi-même(retrouvez le premier article introductif ici). Il aborde un ensemble de notions préliminaires nécessaires à la mise en place de telles architectures et fournit un récapitulatif des étapes à suivre avant de démarrer un projet de mise en place d’un écosystème orienté microservices ou de migration d’un logiciel existant vers une architecture modulaire.

Au travers de cet article, nous apporterons notre éclairage sur les bonnes pratiques, les patterns d’architecture à connaître, l’outillage nécessaire, et les convictions que nous nous sommes forgées. Nous ne prétendons pas détenir une vérité absolue sur la méthode pour mettre en place cette transformation, car il existe une multitude de façons de faire et de penser, mais plutôt vous apporter notre retour d’expérience basé sur les différentes missions que nous menons pour nos clients chez Neoxia.

Retenez votre respiration et suivez donc le lapin blanc au fond du terrier.

Le BUILD

L’importance de la CI/CD

Nous ne vous apprendrons rien en vous expliquant qu’un logiciel est soumis à des évolutions constantes tout au long de son cycle de vie (nouvelles features, bugs, etc.). En revanche, nous vous proposons un éclairage différent — basé sur nos expériences — sur l’importance du déploiement et de l’intégration continue dans un projet d’architecture microservices. Ce travail préparatoire s’articule autour de trois axes que sont les tests, le suivi de version, et la simplicité.

Attention : il s’agit de pratiques qui peuvent sembler chronophages à première vue, mais nous avons acquis la certitude que ces investissements sont nécessaires pour éviter de devoir gérer un incendie incontrôlable au milieu d’un sprint.

Les tests

Nous n’allons pas nous arrêter ici sur les différents types de tests à mettre en place (intégration, unitaires, etc.). Si ces termes vous échappent, creusez d’abord le sujet par le biais des articles sur cette page.

Nous allons nous attarder ici sur une problématique que nous avons rencontrée lorsque nous souhaitions mettre en place des tests dans une architecture microservices. Les services invoquent souvent d’autres services, et nous pouvons vite nous retrouver à lancer des tests dépendant les uns des autres. Ces tests de bout en bout (c’est-à-dire qui s’exécutent sur plusieurs services) sont difficiles, lents, fragiles et coûteux. Imaginez le temps qu’il faut à un développeur pour remonter la trace sur ce type de test, et isoler le problème. D’une part, le dev ne connaît pas forcément l’environnement technique sur lequel son application tournera ; et d’autre part, les points possibles d’échec sont bien trop nombreux pour que le test soit efficace. Afin de diluer cette complexité, nous vous recommandons deux façons de procéder en fonction de votre degré de maturité face aux tests.

Pattern Service Component Test

Comme son nom l’indique, il s’agit d’une suite de tests qui se focalise sur les composants applicatifs. Ce type de test limite la portée du logiciel utilisé à une partie du système testé. Cela permet d’une part de manipuler le système via des interfaces de code internes ; et d’autre part d’utiliser des doublons de test pour isoler le code testé des autres composants. Chez Neoxia, nous avons la conviction qu’il n’est pas nécessaire de communiquer avec des interfaces réelles pour les batteries de tests. Par le biais de contrats, il est possible de définir quels paramètres envoyer à un service, et attendre un format type de retour.

Il est donc tout à fait possible d’implémenter des “mocks” qui répondent à ce contrat. Ces mocks, eux, n’ont pas besoin de connaître l’implémentation sous-jacente des services à implémenter : tout ce qui importe, c’est l’entrée et la sortie des différents appels. C’est un travail qui ne coûte pas grand chose et qui simplifie énormément les développements. Ces mocks doivent être implémentés par service, pour répondre aux besoins spécifiques de celui-ci, et diminuer le couplage. Ainsi, le développeur peut réaliser les tests de son microservice et continuer de travailler sans problème sur celui-ci, en totale isolation du reste du système.

La mise en place de ce pattern de tests peut-être réalisé à l’aide de certains frameworks tels que Pact pour le JavaScript, Ruby, Android, etc., ou bien Spring Cloud Contract pour les environnements Java.

Exemple repris de pact.io

Pattern Service Integration Component Test

Au-delà des tests sur les composants, comment s’assurer que ceux-ci simulent toujours correctement le comportement des services invoqués ? Après avoir testé chaque microservice, nous devons donc tester les communications interservices.

Un test d’intégration de composants vérifie les voies de communication et les interactions entre les composants pour détecter les défauts d’interface. Ce pattern de test rassemble l’ensemble des microservices pour vérifier qu’ils collaborent comme défini dans les contrats, pour épouser le plus fidèlement la logique métier. De plus, ce pattern permet de tester le chemin de communication via un sous-système pour vérifier les hypothèses incorrectes de chaque microservice sur la façon d’interagir avec ses homologues. Par conséquent, les tests d’intégration valident que le système fonctionne ensemble de manière transparente, et que les dépendances entre les services sont implémentées comme prévu.

De la même manière que pour les tests de composants, vous pouvez utiliser Pact provider verifier + Dredd si vous avez la bonne idée d’APIfier votre microservice, et bien sûr un outil de gestion de workflow disponible via les outils d’Atlassian, GitHub, GitLab, etc.

Le suivi de version

Comme nous le rappelions en introduction de la partie Build, un logiciel est soumis à des évolutions constantes. Un microservice ne déroge pas à cette règle et doit pouvoir gérer ces changements.

Certes, notre “architecture orientée microservices” nous permet un découplage entre les services, mais chaque microservice doit pouvoir évoluer tout en restant compatible avec les autres services, qui n’ont pas forcément été modifiés de leur côté. Afin de pouvoir transformer les services, vous devez vous assurer qu’un microservice peut être amélioré pour prendre en charge de nouvelles demandes, tout en causant une rupture minimale avec les autres microservices existants.

Chez Neoxia, nous utilisons différentes approches pour gérer cette complexité.

L’approche versioning/release manager

Les experts Neoxia ont déjà écrit un article au sujet de la gestion des versions dans une stratégie API, que nous vous recommandons de lire avant de continuer la lecture.

Notre opinion à ce sujet est qu’il faut d’utiliser la convention de nommage la plus appropriée. Nous pouvons vous recommander la convention MAJOR.MINOR.PATCH. Il s’agit d’une approche qui a fait ses preuves car elle permet d’avoir une vision globale des versions et des dépendances, mais sans garantir la compatibilité entre les services — sauf si le suivi des versions est confié à un release manager dans vos équipes.

L’approche Tolerant Reader Pattern

Comment certains microservices peuvent-ils continuer à traiter les réponses des autres services lorsque certains contenus sont inconnus ou que les structures de données varient ? Et comment les services peuvent-ils gérer les messages de demande des autres services changeants ? Dans le livre de Chris Evans Domain-Driven Design: Tackling Complexity in the Heart of Software, celui-ci prend pour référence la célèbre maxime de Jon Postel et explique que la loi applicable à la collaboration découplée devrait être la loi de Postel :

“Soyez conservateur dans ce que vous faites, et libéral dans ce que vous acceptez des autres”- Jon Postel

Ainsi, un microservice tolérant doit extraire uniquement ce qui est nécessaire d’un message venant d’un autre appel de service et ignorer le reste. Plutôt que d’implémenter un schéma de validation stricte, le microservice doit poursuivre le traitement des messages lorsque des violations potentielles de schéma sont détectées. Des exceptions ne seront donc levées que lorsque la structure du message empêche le microservice de continuer ou que le contenu viole clairement les règles métiers. Les microservices dits “lecteurs tolérants” ignorent donc les nouveaux éléments de message, l’absence d’éléments facultatifs et les valeurs de données inattendues.

Facilité de déploiement, A/B testing, canary release & autres

Il existe d’autres étapes qui peuvent faciliter la mise en production de votre application dans une optique microservices. À titre d’exemple, l’A/B testing est une méthode issue du marketing permettant de proposer plusieurs variantes d’un même produit à un échantillon de consommateurs afin de déterminer celles qui rencontrent le meilleur résultat. Ici, dans un contexte de programmation, il est possible d’implémenter des variantes de code (orientée interface utilisateur ou performance), avec le même objectif en tête.

Dans la même veine, nous pouvons également mettre en place ce que l’on appelle le canary release. Il s’agit d’une technique pour réduire les potentiels risques du déploiement d’une nouvelle version logicielle en production en distribuant lentement la modification à un petit sous-ensemble d’utilisateurs, avant de la déployer sur l’ensemble de l’infrastructure et de la rendre accessible à tous. Ces approches doivent être abordées au cas par cas, en fonction du contexte du projet et de l’entreprise.

Il est également important de pouvoir rollback un microservice en particulier, si la dernière version déployée engendre des problèmes.

Toutefois, rien de tout cela n’est possible sans une CI extrêmement solide. L’automatisation doit être au centre de vos processus pour garantir un uptime satisfaisant, et une réactivité dans vos nouveaux déploiements. Les bonnes pratiques DevOps doivent être appliquées, et il faut limiter au maximum les écarts qui pourraient être possibles sur les process. À ce titre, nous vous recommandons chaudement l’article de Clément Seguy, Architecte Neoxia, dans lequel il aborde les sujets de gouvernance, de cadre technologique et de pilotage de la transition.

Le RUN

Après ces premières étapes achevées en amont du développement de votre logiciel, nous allons aborder les bonnes pratiques à mettre en place dans la phase aval.

Environnements

Il est toujours important d’avoir plusieurs environnements disponibles pour différents usages, en particulier dans un environnement microservice. La plupart du temps, les trois habituels DEV, QUAL et PROD suffisent. Pour rappel, l’environnement DEV est utilisé par les développeurs de l’application pour tester les nouvelles fonctionnalités et vérifier l’intégration dans le système. L’environnement QUAL (parfois appelé PREPROD) est utilisé par l’équipe qui s’occupe de la recette de l’application. C’est en général un environnement “iso” avec la PROD et branché sur les mêmes systèmes. Quant à la PROD, c’est l’environnement dédié aux utilisateurs finaux.

Il existe également un quatrième environnement moins souvent évoqué, mais qui est possiblement le plus important après la PROD : l’environnement LOCAL. C’est celui que les développeurs doivent gérer sur leur machine pour pouvoir développer et tester l’application. Un bon environnement LOCAL est primordial pour s’assurer d’une livraison rapide et fonctionnelle des applications. Dans le cas des microservices, il est plus difficile d’avoir un environnement LOCAL qui ressemble à celui de la PROD : il faudrait pouvoir lancer tous les services sur une seule machine, qui fait déjà tourner de nombreux processus.

Plusieurs solutions sont possibles, chacune apportant son lot d’inconvénients. Lancer la totalité des services sur la machine du développeur serait évidemment la première solution à envisager ; elle ne se révèle toutefois ni simple, ni efficace… ni même nécessaire ! En effet, lorsqu’on développe des microservices, il faut se rappeler qu’ils doivent par essence être isolés, c’est-à-dire pouvoir fonctionner seuls, sans problème, si tant est qu’ils arrivent à communiquer avec une interface qui répond à un contrat entre les deux services. La bonne pratique est donc de lancer uniquement le service en question. Mais une question nous vient ensuite : avec quelles interfaces le service devra-t-il communiquer ?

Infrastructure

Le choix de l’infrastructure doit se faire impérativement avant le développement des services. Il s’agit du domaine d’expertise de SKALE-5, filiale de Neoxia spécialisée dans la migration et l’infogérance DevOps d’applications dans le cloud. Dans nos projets, nous nous tournons naturellement vers eux pour nous conseiller et valider nos choix sur les infrastructures les plus adéquates. Plusieurs observations reviennent souvent, et une multitude de choix sont toujours possibles, selon les moyens mais surtout les besoins de l’entreprise. Au cours des dernières années, trois technologies se sont démarquées : Docker, Kubernetes et les FaaS (Functions as a Service).

Pour rappel, Docker est une plateforme de virtualisation au niveau du système d’exploitation, qui sert de runtime à des applications sous formes de conteneurs.

Kubernetes, quant à lui, est un système d’orchestration de conteneurs basé sur Docker, d’abord conçu par Google puis repris par la Cloud Native Computing Foundation (CNCF). Kubernetes sert ainsi d’outil de déploiement, de gestion et de scaling de conteneurs.

Enfin, les FaaS sont un type de service rendu possible par le cloud computing, implémenté par beaucoup de grandes plateformes de cloud (AWS avec AWS Lambda, GCP avec Google Cloud Functions, Azure avec Azure Functions). Le principe est simple : déléguer toute la gestion de l’infrastructure à une autre entreprise en “serverless”, pour se concentrer sur l’implémentation de fonctionnalités.

En production, nous n’envisageons plus d’utiliser Docker sans l’associer à un orchestrateur. En effet, un outil comme Kubernetes apporte un gain de temps et donc d’argent non-négligeable, et permet de ne pas avoir à penser à des problématiques comme la scalabilité automatique, la gestion des conteneurs, les rollbacks automatiques, la gestion des configurations, et bien d’autres… Deux solutions sortent donc du lot pour l’infrastructure orientée microservices : Kubernetes, et les FaaS.

Là où Kubernetes permet de personnaliser le système pour qu’il réponde spécifiquement au besoin pour alléger le code, il faut beaucoup apprendre et pratiquer pour avoir un système fiable et stable. Une équipe dédiée à Kubernetes est nécessaire pour ne pas faire d’erreurs et maîtriser correctement l’outil (à l’heure actuelle de nombreux développements sont en cours pour simplifier l’utilisation de l’orchestrateur). Le grand avantage de Kubernetes est qu’il est désormais vraiment complet et très bien intégré aux principaux cloud providers ; ce qui n’était pas le cas ne serait-ce que l’année dernière. Kubernetes est donc devenu la référence en matière de conteneurs et de l’orchestration de ceux-ci : une solution formidable pour les microservices !

En ce qui concerne les FaaS, c’est tout l’inverse de Kubernetes : aucune customisation de cette technologie n’est possible, si ce n’est le choix des ressources allouées au runtime. Le principe est simple : vous définissez une fonction qui recevra un événement dans un format standard, et devra renvoyer la valeur dans un format également bien défini. Ces événements peuvent provenir d’un service intégré au cloud en question, ou être générés par une autre application. L’avantage, c’est qu’il n’y a pas besoin d’une équipe de DevOps pour mettre en place et gérer l’infrastructure ; les développeurs peuvent faire cela en toute autonomie. En revanche, ils doivent également penser à inclure dans leur code de nombreux aspects qui auraient pu être gérées plus simplement dans Kubernetes : la gestion d’erreurs, le tracing, les métriques, etc. Les FaaS peuvent être une bonne entrée en matière pour les start-ups ayant un budget limité, ou qui n’ont pas forcément besoin d’une solution aussi customisable que Kubernetes.

Il existe également des entreprises qui ne souhaitent pas migrer vers un cloud public, pour des questions de tarifs ou de protection de leurs données. Pour ces entreprises, une alternative existe : il est tout à fait envisageable de monter un cloud privé sur des serveurs on-premise, et de gérer un Kubernetes “maison” sur ces serveurs, pour ainsi déployer des microservices. C’est toutefois une tâche considérablement plus complexe, qui demandera beaucoup de temps et d’expertise pour être implémentée correctement.

Monitoring

Une fois l’infrastructure choisie et mise en place, et avant de commencer le développement des microservices, il est nécessaire d’aborder les sujets de surveillance et de mesure de l’activité.

Pour rappel, monitorer un système, c’est recueillir des mesures permettant d’attester de la bonne santé de celui-ci. L’importance de cette activité de surveillance n’est plus à démontrer, et nos conseils s’appliquent aussi bien pour les microservices que pour toute sorte d’architecture logicielle.

Beaucoup de choses peuvent être surveillées, mais il est important de faire le tri dans ce qui importe réellement à votre projet, client, et vision métier. Avez-vous véritablement besoin de connaître le nombre moyen de clics par utilisateur ? Ou le nombre d’erreurs 404 qui ont été renvoyées par votre API ? Ces interrogations se posent en général au cas par cas, mais les différentes mesures peuvent être regroupées comme suit :

  • les mesures de performance (par exemple le temps de réponse) ;
  • les mesures de disponibilité ;
  • les mesures d’intégrité.

Il convient donc de monitorer les bons éléments en fonction des besoins et du contexte, pour ainsi éviter de se retrouver submergé par des informations inutiles. Il existe toutefois des métriques qui vous seront toujours utiles, comme le temps de réponse des requêtes, le nombre de jobs en attente dans vos queues, etc.

Prometheus et Grafana

Prometheus est un excellent outil de monitoring. Nous l’utilisons sur de nombreux projets, et il a toujours su remplir son rôle à merveille. Pour faire simple, il s’agit d’un agrégateur de métriques qui permet de requêter et de stocker des données multi-dimensionnelles de monitoring. Il permet également de lancer des alertes en fonction des métriques reçues. Grafana, quant à lui, est un outil de visualisation de données. Il permet de générer des graphiques et se couple très bien avec Prometheus.

Repris du site prometheus.io

Logs

En matière de gestion des événements, nous recommandons vivement d’établir une politique de gestion des logs, incluant le périmètre et le niveau de détails. Il convient de normaliser tous les logs d’une application pour pouvoir plus facilement les utiliser, et d’éviter de “trop” logger pour ensuite se noyer dans une quantité d’information exagérée. Le stockage des logs a également un coût, et c’est souvent une partie largement sous-estimée d’une telle architecture.

Les logs peuvent être classés en trois catégories :

  • les logs fonctionnels : tous les évènements qui se produisent dans le code de l’application. Dans une approche microservices, il est primordial que chacun des services dispose de son propre système de logs. Néanmoins, nous recommandons la mise en place d’une interface graphique centrale pour agréger et analyser l’intégralité des logs grâce, par exemple, à la stack ELK.
  • les logs de trafic, afin de pouvoir analyser qui fait quoi sur votre application côté utilisateur (nombre de requêtes, nombre d’erreurs…). Dans ce domaine, à vous de décider quel est l’outil le plus adapté, en particulier selon le niveau de détail que vous souhaitez enregistrer.
  • les logs techniques (bases de données, gateway…). Ce dernier type de log sert à déterminer quels appels génèrent des temps de traitement qui peuvent être optimisés.

L’ensemble de cette stack peut paraître fastidieuse à mettre en place, mais elle soutiendra à la fois votre activité de développement mais aussi votre activité data & marketing, en vous permettant de mieux connaître vos utilisateurs et leurs pratiques. Effectivement, une bonne gestion des logs vous permettront de mieux comprendre, tracer et corriger les problèmes mais aussi de connaître et vous comprendre les usages de vos utilisateurs. Par exemple, il n’est pas rare que la faisabilité d’un projet Data dépende en partie de la précision et de la qualité des logs.

Conclusion

La mise en place d’une architecture orientée microservices aborde de nombreux domaines transverses au-delà de la rédaction de code, qu’il s’agisse des tests, de la CI/CD ou encore de l’infrastructure.

La mise en place de ce type d’architecture est délicate à orchester car comme nous venons de le voir, ces briques nécessitent l’implication de toutes les parties prenantes, du développeur au DevOps en passant par le ou la DSI, qui a un vrai rôle de sponsor à jouer.

Concrètement, les atouts de la mise en place d’une architecture orientée microservices s’articulent autour de deux axes :

  • Les enjeux métiers, en préparant les différentes couches en avance pour le BUILD, et en faisant les bons choix pour le RUN, vos équipes de développement pourront livrer un code de grande qualité et avec une fréquence plus régulière, et ce dès les premières semaines. Ainsi, vos équipes métiers n’auront pas à ressentir l’effet de cette transformation. D’autre part, cela renforcera l’évolutivité de votre SI dont les briques logicielles seront moins adhérentes les unes aux autres.
  • Les enjeux SI, en respectant les obligations de robustesse et de coût du SI avec des microservices intégrés dans une logique de tests de bout en bout. Les domaines techniques devront clairement être définis ce qui empêchera l’overlapping de périmètres et l’empilement de briques logicielles.

Par la suite, nous vous recommandons d’organiser vos équipes métiers et techniques autour de domaines fonctionnels bien définis, pour qu’elles puissent travailler indépendamment et sans générer de dépendances bloquantes. Cela revient à créer des Features Teams, sujet que nous vous proposons d’aborder dans le 3e article de notre série autour des Microservices : introduction au Domain-Driven Design. Rendez-vous le 25 février pour la suite de cette série !

--

--