Dans le premier article, nous avions poser les bases de nos apis et mis en place la découverte de ces services. Nous avions eu l’occasion de voir comment Spring Cloud pouvait nous aider à les référencer via Eureka. Mais Spring Cloud ne se limite pas aux registres de services.

Dans cette seconde partie nous allons voir comment mettre en place le point d’entrée de l’architecture. Cela se fera avec Zuul, reverse proxy proposé dans la stack Netflix. Ce deuxième article sera également l’occasion de voir de quelle façon les fichiers de configuration peuvent être externalisés.

Mise en place de Zuul.

Zuul en quelques mots

Zuul est donc un reverse proxy intégré à la stack Netflix OSS. Il s’agit donc du même type de brique que peuvent l’être Spring Cloud Gateway ou Nginx. Mais Zuul a un gros avantage sur ses « concurrents » : il permet la gestion de filtres (pouvant être enregistrés en base). En somme, ces filtres peuvent être utilisés pour réaliser des traitements sur les requêtes entrantes avant de les transmettre aux services sous-jacents. Le fait de pouvoir les stocker dans une base de données permet de modifier ces filtres sans devoir relancer le serveur (seule Cassandra semble supportée pour le moment).

Lancement du serveur

On ajoute un nouveau module a notre projet. Les dépendances suivantes devront figurer dans le pom.xml :

<dependencies>
 <dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zuul</artifactId>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>
 <dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
 </dependency>
</dependencies>

On complète la classe « main » du module comme suit :

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayServiceApplication {
 public static void main(String[] args) throws MalformedURLException {
  SpringApplication.run(GatewayServiceApplication.class, args);
 }
}

L’annotation EnableZuulProxy indiquera qu’il s’agit du serveur Zuul, tout simplement.

Pour terminer le fichier application.properties devra contenir la configuration suivante :

spring.application.name=service-gateway
server.port=8762
eureka.instance.preferIpAddress=true
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
management.security.enabled=false
eureka.serviceurl.defaultzone=http://localhost:8761/eureka/
zuul.routes.api-authors.path=/authors/**
zuul.routes.api-authors.serviceId=api-authors
zuul.routes.api-categories.path=/categories/**
zuul.routes.api-categories.serviceId=api-categories
zuul.routes.api-news.path=/news/**
zuul.routes.api-news.serviceId=api-news

On y précise le port d’écoute du proxy, mais aussi l’url du registre de service (Eureka), le nom de l’application (ici service-gateway) ainsi que la ou les routes à utiliser pour joindre les différents services.

Les paramètres de routing zuul.routes.api-xxx.path et zuul.routes.api-xxx.serviceIdsont facultatifs. En effet si ceux ci ne sont pas spécifiés, alors Zuul contactera Eureka pour déduire des règles de routing par défaut (par exemple le service api-authors sera mappé à l’url http://localhost:8762/api-authors). 

Au lancement de l’application, lorsqu’on se rend dans l’interface d’Eureka nous pouvons constater l’apparition de service-gateway.

Une petite vérification sur http://localhost:8762/routes nous assurera que Zuul a bien pris en compte nos routes personnalisées, et un appel à http://localhost:8762/authors devrait nous afficher la liste des auteurs (notre service est donc joignable via le suffixe “authors” et non plus “api-authors”).

Mise en place des règles de filtrage

Dans l’introduction de l’article, nous disions que les filtres pouvaient être enregistrés en base NoSql (Cassandra). Ce n’est pas la seule méthode de stockage supportée. En effet, il est tout à fait possible de charger les filtres depuis le FileSystem. C’est l’option retenue ici.

Les filtres peuvent donc être de simples classes Java qui vont étendre ZuulFilter. Nous pouvons donc créer un package “Filters” au sein de service-gateway qui contiendra un sous-package par type de filtre :

  • pre : contiendra les filtres exécutés avant le routage d’une requête (règles de pré-routage).
  • post : contiendra les filtres exécutés après le routage d’une requête (règles de post-routage).
  • error : contiendra les filtres exécutés si une erreur survient lors d’une traitement d’une requête.
  • routing : contiendra les filtres exécutés lors du routage de la requête.

Nous ne sommes pas obligés de respecter cette hiérarchie, néanmoins elle permet de structurer proprement nos filtres (ce qui est utile dans le cas de gros projets avec une multitude de filtres).

Chaque filtre étendra ZuulFilter, et surchargera les méthodes :

  • filterType qui permet de spécifier le type de filtre pre/post/route.
  • filterOrder la priorité à appliquer au filtre, et donc l’ordre dans lequel les filtres seront appliqués.
  • shouldFilter qui retournera True si run doit être invoquée.
  • run dans laquelle seront décrites les opérations appliquées par le filtre.

L’exemple suivant donne un squelette de filtre possible :

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.Debug;
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.context.RequestContext;

public class FilterExample extends ZuulFilter {

 @Override
 public String filterType() {
   return "pre";
 }

 @Override
 public int filterOrder() {
   return 10000;
 }

 @Override
 public boolean shouldFilter() {
   return Debug.debugRequest();
 }

 @Override
 public Object run() {
   HttpServletRequest req = RequestContext.getCurrentContext().getRequest();
   Debug.addRequestDebug("Requete:" + req.getScheme() + " Adresse:" + req.getRemoteAddr() + " Port:" + req.getRemotePort());
   return null;
 }
}

Ce filtre ne fait pas grand chose à part capturer les requêtes en entrée et afficher en debug quelques informations sur celles – ci, mais il illustre bien la simplicité avec laquelle il est possible de customiser les filtres de Zuul.

Pour utiliser le filtre, il ne reste plus qu’a ajouter le code suivant dans GatewayServiceApplication

@Bean
public FilterExample filterExample() {
   return new FilterExample();
}

Voilà ce n’est pas plus compliqué que ça 😉


Le point d’entrée de l’architecture est maintenant défini, voyons maintenant comment centraliser les configurations au sein d’un repository git.

Centralisation des configurations.

Pourquoi externaliser les configurations sur Git ?

Tout simplement pour faciliter le changement de paramètre. Imaginons qu’il soit nécessaire de modifier les informations de connexion à une base de données, le fait de devoir changer l’ensemble des paramètres définis en dur dans le code serait fastidieux, là où une simple modification dans un fichier json ou yaml (référencé dans les classes de notre projet) est beaucoup plus rapide.

Mais pourquoi centraliser ce type de fichier sur un repository Git ? Premier avantage, plus besoin de recompiler le projet en cas de modification du fichier de configuration.

Pourquoi ne pas se contenter de préciser les informations dans des variables d’environnements ? Second avantage de la solution, qui dit Git (ou tout autre outil de scm) dit historique des commits. En cas de modification d’une configuration, un simple push suffit et l’ensemble des modifications est historisé (donc facilement réversible).

Un service pour gérer la configuration

Une fois de plus, Spring Cloud nous mâche le travail et fournit une annotation pour simplifier la récupération des configurations depuis Git : @EnableConfigServer. 

Deux solutions s’offrent nous, utiliser directement cette annotation dans le code des apis déjà créées (ApiNewsApplication, ApiCategoriesApplication, ApiAuthorsApplication) ou créer un service dédié à la récupération des configurations. On optera pour la seconde solution (afin de ne pas refaire la même manipulation à chaque ajout d’api). Nous créons donc une nouvelle api nommée service-configuration, et nous ajoutons la classe ConfigurationServiceApplication :

@SpringBootApplication
@EnableConfigServer
public class ConfigurationServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(ConfigurationServiceApplication.class, args);
  }
}

Il s’agit donc d’une application Spring Boot somme toute classique, avec en plus l’annotation @EnableConfigServer.


NOTE : pour la suite de l’article, nous partirons du principe qu’un repository GitHub a été créé afin de centraliser les fichiers application.properties de nos apis. Pour les lecteurs n’ayant pas de compte GitHub, il est tout à fait possible de suivre le tutoriel en utilisant un repository Git local. A titre d’exemple voici les commandes à exécuter afin de créer un tel repository :

$ mkdir configuration-repository
$ cd configuration-repository
$ git init .
$ echo server.port: 8080 > application.properties
$ git add application.properties
$ git commit -m "Init application.properties"

Pour préciser depuis quel repository Git notre service doit télécharger les fichiers de configuration, nous ajoutons dans le fichier application.properties les informations suivantes :

server.port=8888
spring.cloud.config.server.git.uri=https://github.com/ineat-demo-spring-cloud/configuration-repository

La seconde ligne est la plus importante. Dans le cas le plus simple il suffit d’entrer l’uri pour contacter notre repository.
Ici nous avons opté pour un repository GitHub, mais on pourrait très bien passer par une autre solution comme GitLab ou même par un repository local auquel cas l’uri aurait été file://${user.home}/configuration-repository


NOTE : il sera certainement nécessaire de s’authentifier soit via login/password, soit via une clé ssh (en fonction de la configuration du repo). En partant du principe que notre repository se base sur une authentification classique par login/password, on utilisera le duo spring.cloud.config.server.git.uri.login/spring.cloud.config.server.git.uri.password.


Il est évident que dans le cas d’une architecture multi-apis comme la notre nous ne pouvons pas nous contenter d’un repo dans lequel les configurations seront placées à la racine. Le repository devra donc avoir un sous – répertoire par api, contenant les fichiers application.properties.

Quelques options utiles

  • Filtrer les sous-répertoires avec searchPath

Si nous souhaitons utiliser des sous-répertoires (comme précisé sur le schéma précédent) il faut ajouter une option dans l’application.properties : spring.cloud.config.server.git.searchPath. Celle ci pourra prendre comme valeur un ou plusieurs noms de répertoire (séparés par des virgules) voir même un simple pattern. Par exemple pour scruter le sous-répertoire de notre repository, nous pourrions le paramétrer de deux façons :

  • En précisant les noms des répertoires sous la forme d’une liste -> spring.cloud.config.server.git.searchPath=api-authors,api-news,api-categories
  • En utilisant un pattern -> spring.cloud.config.server.git.searchPath=api*

Dans le deuxième cas, l’ensemble des sous-répertoires commençant par “api” sera utilisé.

  • Cloner le repository au démarrage de l’application

Par défaut, Spring Cloud va cloner le repository lors de la première sollicitation des configurations. Encore une fois il possible de modifier se comportement en ajoutant l’option cloneOnStart, qui prendra comme valeur “true” (si on souhaite cloner le repo au démarrage du serveur) ou “false” (dans ce cas c’est le comportement par défaut qui s’appliquera).

  • Gérer la configuration de plusieurs environnements

Jusqu’ici, nous partions du principe que notre application fonctionnerait toujours dans le même environnement, mais dans la vraie vie ça ne fonctionne pas comme ça ! Il faut jongler avec les configurations des plateformes de production, d’intégration, de développement, … tout cela pourrait vite devenir un casse tête si nous ne disposions pas d’options permettant de gérer divers repo.

Spring Cloud offre la possibilité de définir des profils permettant de pointer automatiquement vers tel ou tel repository Git. On pourrait donc imaginer avoir un repository par type d’environnement (contenant les configurations pour la production ou l’intégration par exemple).

server.port=8888
spring.cloud.config.server.git.uri=https://github.com/ineat-demo-spring-cloud/default/configuration-repository

spring.cloud.config.server.git.repos.development.pattern=development
spring.cloud.config.server.git.repos.development.uri=https://github.com/ineat-demo-spring-cloud/development/configuration-repository
spring.cloud.config.server.git.repos.production.pattern=production
spring.cloud.config.server.git.repos.production.uri=https://github.com/ineat-demo-spring-cloud/production/configuration-repository

Dans l’exemple précédent, on définit deux repository supplémentaires (pour la plateforme de production et pour celle de développement). En somme, lorsque le profil “production” sera utilisé, les configurations seront chargées depuis https://github.com/ineat-demo-spring-cloud/production/configuration-repository

Si le profil utilisé n’est pas défini dans le fichier application.properties alors c’est la valeur de spring.cloud.config.server.git.uri qui fait foi et qui sera utilisée pour récupérer les fichiers de configurations.

Câblage du service avec les autres apis

Pour que nos apis puissent bénéficier de la configuration exposée par le service de gestion de la configuration, deux choses sont à ajouter :

  • dans les fichiers application.properties de nos apis, on ajoute la ligne
    spring.cloud.config.uri=http://localhost:8888

    Celle ci permet de préciser l’url à appeler pour contacter le service.

  • dans les controllers des projets d’api, on ajoute l’annotation @RefreshScope, qui permettra de forcer le rafraîchissement de la configuration de chaque composant (c’est à dire récupérer les mises à jour de la configuration depuis service-configuration et déclencher un événement d’actualisation).

Cela conclu ce deuxième article. L’architecture mise en place est relativement simpliste mais facilement extensible, et fourni un bon point de départ pour démarrer un projet plus complexe nécessitant d’ajouter rapidement de nouveaux services. D’autres articles feront suite a celui-ci, et porteront notamment sur l’intégration de Consul (en vue de remplacer Eureka).

D’ici là, quelques liens utiles pour les lecteurs souhaitant aller plus loin dans leur découverte de Zuul :