JHipster permet de générer les descripteurs de déploiement Kubernetes des applications qu'ils génèrent. Dans ce cadre, il propose depuis peu une option pour gérer Istio : le ServiceMesh le plus connu actuellement.
Après une brève introduction à Istio, nous allons voir ici que les architectures microservice associées à ces déploiements peuvent être très différentes suivant les options de génération choisies et étudier un peu leurs caractéristiques.
Rapide introduction à Istio
Istio est un Service Mesh complètement intégré à Kubernetes.
Sans rentrer trop dans le détail qui n’est pas le but de ce billet, son rôle est de gérer l’ensemble des communications entre services au sein de votre architecture microservice.
Cela lui permet en particulier de gérer :
- le load-balancing intelligent entre les différentes instances des services ( dont le circuit breaking et les retry )
- du déploiement blue/green
- le canary testing ou l’A/B testing entre deux versions d’un service
- le monitoring/observabilité des flux et des services
- la sécurité des appels inter-applicatifs
Le point important dans le cadre de ce billet est de comprendre qu’il est capable de remplacer en bonne partie des outils de la stack Netflix tel que : Ribbon, Hystrix, une partie de Zuul, et se passer des services d’un Eureka en bénéficiant directement du service discovery de la plateforme Kubernetes.
Vous trouverez quelques ressources intéressantes sur le sujet ici :
- http://philcalcado.com/2017/08/03/pattern_service_mesh.html (expliquant le concept général de ServiceMesh)
- https://istio.io/docs/concepts/what-is-istio/
Nous parlerons un peu plus de son fonctionnement plus loin dans ce billet.
Rappel de l’architecture microservice classique générée par JHipster
Voici l’architecture classique que JHipster génère dans le cas général :
JHipster s’appuie alors énormément sur Spring Cloud et en particulier Spring Cloud Netflix :
- la JHipster Registry héberge :
- un serveur Eureka ( registre de service )
- et un serveur Spring Cloud Config (centralisation de la configuration applicative)
- chaque instance de microservice
- récupère la configuration centralisée auprès de Spring Cloud
- s’enregistre (via son IP par défaut) auprès d’Eureka.
- la gateway JHipster en frontal :
- permet en particulier le routing des requêtes HTTP vers l’ensemble des microservices générés en se basant sur les contextpath des urls : elle se base pour cela sur Zuul (et Ribbon) pluggué sur Eureka pour découvrir dynamiquement l’emplacement des services disponibles
- c’est aussi elle qui héberge les sources de l’application Web (Angular ou React) ainsi que certains services tels que le référentiel des utilisateurs (suivant les options de génération)
- Éventuellement, chaque microservice peut bénéficier de Eureka et Ribbon pour découvrir puis appeler lui-même les autres microservices de l’architecture
- Hystrix s’assure de la robustesse de ces appels (GW->MS et MS->MS) : en assurant en particulier la fonction de circuit breaking
Cette architecture fonctionne (entre autres) sur un cluster Kubernetes classique (comprendre sans Istio) : les générateurs JHipster s’assurant en particulier de générer la configuration Kubernetes nécessaire à la gestion de JHipster Registry déployé en haute disponibilité avec plusieurs replicas (via un StatefulSet)
L’architecture classique de JHipster sur Kubernetes avec Istio
Le principe de fonctionnement d’Istio est basé sur l’injection d’un proxy (ce proxy est Envoy en l’occurrence) au sein de chaque Pod. ( Chaque Pod hébergera alors une instance d’un microservice donné et son proxy Envoy colocalisé ).
L’ensemble des connections réseau entrantes et sortantes sur chaque Pod est routé vers ce proxy (via des règles iptable) qui est alors en mesure d’appliquer des stratégies d’appels et de routage indépendamment de l’applicatif hébergé.
(extrait de https://istio.io/docs/concepts/what-is-istio/arch.svg )
Ce proxy s’occupe en particulier de router les appels vers les bonnes instances du bon microservice. Ce routage peut s’effectuer selon différents critères, la plupart du temps via un mapping du host de l’url vers un type de service particulier (mais le path de l’url, voire les headers HTTP peuvent aussi être utilisés , …)
Par exemple, au sein d’un cluster Kubernetes dans lequel un VirtualService Istio nommé “productservice” aurait été défini et associé à un microservice Product : le microservice Cart (si on reprend les schémas de ce billet) a uniquement besoin d’appeler une url du type : http://productservice/catalog/112 pour que Istio puisse router cet appel vers une instance disponible du service Product (en gérant lui-même les aspects de timeout, de circuit breaking et de retry)
Cela rentre donc en partie en concurrence avec l’architecture classique de JHipster pour laquelle ces aspects sont gérés par l'écosystème Spring Cloud.
Le générateur est toutefois assez intelligent pour permettre de faire cohabiter les deux mécanismes lorsqu’on lui demande de déployer cette architecture sur Istio :
La principale adaptation consiste à modifier le mode d’enregistrement des microservices dans Eureka :
- le descripteur Kubernetes généré positionne les variables d’environnement de façon à désactiver l’enregistrement par IP dans Eureka pour le remplacer par un enregistrement par nom.
- ce nom est normalement pour Eureka le hostname hébergeant le service. Ici, il sera rempli avec le nom du VirtualService Istio associé au microservice (“productservice” dans mon exemple)
On obtient donc bien au final une architecture équivalente :
Les proxy Envoy ont été représentés en vert sur les appels sortants (mais ils capturent aussi les appels entrants) ; ainsi que la gateway Istio sur l’Ingress (dont le rôle est de router les appels externes au cluster vers les bons services)
On notera qu’ici chaque instance d’un microservice donné s’enregistre auprès de Eureka avec le même nom d’hôte (“productservice” dans mon exemple ci-dessus pour le MS Product). En cas de présence de 2 replica, Ribbon aura donc le choix entre deux instances : http://productservice/ et http://productservice/ … On aura compris ici, qu’il ne jouera pas vraiment son rôle de loadbalancer puisqu’au final ce sera le proxy Envoy qui décidera de router http://productservice/ vers l’un ou l’autre des replica du microservice Product.
Pour les appels inter-services (Cart vers Product dans le schéma), c’est la même chose :
- si le développeur utilise les mécanismes standards mis à disposition par JHipster : Eureka et Ribbon seront mis à contribution mais c’est le proxy Istio qui aura le dernier mot sur le load balancing
- L’utilisation d’un simple client Http aura donc globalement le même effet (aux remarques ci-dessous près)
On notera toutefois que la présence de Ribbon et d’Hystrix peut avoir des effets de bords :
- les mécanismes de retry de Ribbon et ceux de Istio vont s’additionner :
il n’est souhaitable de n’activer que l’un des deux mécanismes pour plus de maîtrise (par défaut JHipster n’active pas le retry Ribbon, mais le retry Istio est activé dans les descripteurs générés) - les timeout d’Hystrix (et/ou de Feign) et d’Istio peuvent eux aussi rentrer en concurrence :
et il faut donc les configurer de manière cohérente : a minima pour que le timeout Hystrix ne se déclenche pas avant le timeout Istio (ou pendant les retry configuré dans Istio)
(le post Spring Cloud Feign et la gestion des erreurs est une bonne source d’information pour la configuration d’Hystrix dans un environnement Spring Cloud, en particulier sur le calcul des timeouts en présence de retry. Il sera utile ici aussi pour se poser les bonnes questions)
Une architecture alternative “Istio-native”
On l’aura compris la configuration précédente a le mérite de permettre d’exécuter l’application sans remise en cause majeure, mais elle n’embrasse pas complètement les mécanismes d’Istio et leur gain sur la simplification du code applicatif (et sa configuration).
Ray Tsang (@saturnism) de Google qui est l’initiateur de travail sur Istio dans JHipster a quant à lui essayé de faire en sorte de générer une architecture plus adaptée à Istio.
En l’état, le changement d’architecture est toutefois assez structurant :
- il consiste en effet à désactiver complètement le concept de Service Discovery lors de la génération de la gateway JHipster et des microservices :
Les configurations Zuul et cliente d’Eureka ne sont alors plus générées dans la gateway Jhipster et dans les microservices.
A la génération des descripteurs Kubernetes / Istio, JHipster prend alors en compte le fait que les éléments applicatifs n’ont pas été générés avec l’option service discovery et s’adapte en conséquence :
- sur la gateway Istio (le point d’entrée depuis le monde extérieur donc), une route est créée pour accéder directement à chaque microservice (en se basant sur le contextPath)
- la “Gateway JHipster” prend la place d’un service comme un autre et ne sert plus qu’à héberger les fichiers statiques de l’application Web javascript et les services transverses tel que le référentiel utilisateur
L’architecture ressemble alors à ceci :
Deepu K Sasidharan (@deepu105) l'un des principaux contributeurs JHispter vient justement tout juste d'illustrer lui aussi les impacts architecturels de l'usage de Istio dans son dernier article : JHipster microservices with Istio service mesh on Kubernetes
Ici plus de stack Netflix donc : les appels externes sont directement routés vers le bon service (tout en bénéficiant du circuit breaking et du retry) et les appels inter-services ont besoin d’un simple client http pour être effectués (là aussi en bénéficiant des services du proxy Istio)
Pour être plus précis, on pourra bien sûr garder Feign (initialement créé par Netflix) pour faire ces appels, mais Hystrix, Ribbon et Eureka ne sont plus nécessaires dans le cas général (dans certains cas particulier Hystrix pourra toutefois encore rendre quelques services). Le lecteur intéressé pourra lire cette comparaison de Envoy et Hystrix
Cette architecture a toutefois deux principaux impacts :
- la gateway JHipster ne fait plus office de passerelle applicative (malgré son nom)
- la suppression du Service Discovery a complètement sorti la JHipster Registry du paysage, et Spring Cloud Config avec donc.
La gateway JHipster en tant que passerelle applicative a plusieurs rôles à jouer dans une architecture microservice : le routage des appels vers les microservices n’étant qu’un de ces rôles.
Suivant les besoins, une passerelle applicative doit être en capacité :
- de filtrer certains headers
- de gérer les entêtes CORS
- de gérer du throttling (cf l’implémentation de RateLimitingFilter fournie par JHipster...)
- de gérer un contexte d’authentification
A noter : le mode d'authentification “JWT” généré par JHipster fonctionne bien ici mais les autres modes (dont UAA qui reste stateless) aura besoin de la gateway. L’intégration d’une authentification JWT plus puissante (avec rafraichissement en particulier) aura elle aussi souvent besoin d’une gateway. - d'appliquer des règles de sécurité transverses
- ...
Différentes stratégies peuvent être mises en place pour traiter tous ces aspects autrement, mais une gateway applicative reste toutefois le moyen le plus simple de les traiter de manière centralisée.
Suivant les besoins, ces deux points peuvent donc être acceptables mais je vous propose ci-dessous une architecture cohérente avec Istio et préservant les fonctionnalités annexes de l’architecture classique de JHipster..
L’architecture idéale ?
Disclaimer : actuellement, JHipster ne sait pas (encore?) générer cette architecture directement. On pourra voir ci-dessous que cela impliquerait probablement des modifications spécifiques sur le code généré au niveau de la gateway et qui n’aurait de sens que pour ce cas particulier.
Le but ici est de remettre la gateway JHipster en frontal des microservices et éventuellement de garder Spring Cloud Config dans le paysage.
La principale adaptation nécessaire sur le code “standard” de JHipster est l’adaptation de la délégation des appels vers les microservices depuis la Gateway JHipster :
- Le routage vers les bons microservices reste une responsabilité que l’on veut déléguer à l’infrastructure Istio : la gateway Jhipster devra donc se contenter d’appeler un service virtuel configuré au niveau d’Istio pour effectuer le routage :
il s'agit quasiment de la même configuration de routage que pour la gateway Istio de l’architecture précédente
Exemple :
- La gateway reçoit une requête : http://gatewayjhipster/product/xxxx,
- après avoir appliqué les filtres adéquats (sécurité, throttling, etc…), elle doit se contenter d’appeler une url de type http://servicerouter/product/xxxx
- au niveau d’Istio un “VirtualService” nommé “servicerouter” aura été défini qui suivant le contextPath (ici “/product” ou “/cart”) sera en capacité de router les appels vers une instance du Microservice Product (ou respectivement du microservice Cart).
Dans l’illustration ci-dessus, j’ai choisi de garder Zuul dans la Gateway JHipster afin de continuer à bénéficier des filtres custom mis à disposition par Spring Cloud (dans Spring Cloud Security par exemple) ou JHipster tel que le RateLimitingFilter
- le filtre de routing Zuul devra par contre être réimplémenté/reconfiguré afin de pouvoir répondre à la stratégie décrite ci-dessus.
- on voudra aussi en particulier ne pas intégrer Hystrix (ni Ribbon) dans la configuration de ce serveur Zuul
( car comme indiqué plus haut, il rentre en concurrence avec Istio et leur configuration pourrait ne pas être cohérente)
Bien sûr, si les filtres en question ne sont pas nécessaires, Zuul pourra être simplement supprimé, et remplacé par un simple composant proxy se contentant de déléguer les appels HTTP vers des urls de la forme http://servicerouter/xxxx.
Conclusion
L’architecture microservice de JHipster est basée sur Spring Cloud et en particulier sur la stack Netflix (bien que des alternatives telles que Consul et Traefik soient aussi disponibles), et cela fait entièrement sens.
Il est donc parfaitement normal que cette architecture appliquée telle quelle sur Istio montre quelques limites.
Comme je l’ai décrit à la fin de ce billet, je pense que l’essence de cette architecture peut rester valable même hébergée sur Istio. Cela nécessiterait toutefois de modifier le code généré sur la gateway JHipster pour traiter explicitement ce cas particulier.
Difficile de contenter tout le monde, JHipster offre déjà une combinatoire de génération très riche et donc complexe à maintenir. En l’état le lecteur intéressé devra donc relever un peu ses manches et implémenter lui même la stratégie décrite.
Rappelons pour finir que la version 1.0 d’Istio n’est sortie que fin Juillet 2018. C’est une technologie très prometteuse mais qui reste jeune et peu connue. Spring Cloud, en particulier dans un contexte où il est bien maîtrisé, répond encore très bien à la plupart des problématiques.