[REX] Architecture orientée microservices avec Netflix OSS et Spring – Article #3

Article #3 – Zuul, gatekeeper et filter loader depuis Cassandra

Désormais, nos services savent se retrouver et communiquer entre eux en passant par Eureka. Cet élément central de notre architecture étant mis en place, il s’agit de pouvoir contrôler les requêtes entrantes et d’utiliser tout le potentiel d’un tel reverse proxy, et plus encore.

Rappel : Cet article s’inscrit dans une série d’articles formant un retour d’expérience sur la création d’une architecture orientée microservices.

Zuul, mais pourquoi faire ?

Zuulest un service qualifiable de “point d’entrée” permettant de faire du proxy inverse au sein d’une application. Il se place donc en entrée de l’architecture et permet de réaliser des opérations avant de transmettre la requête aux services et sur leur retour. Zuul fait partie de la stack Netflix OSS et utilise en interne certaines autres applications de la stack, comme Ribbon et Hystrix, par exemple.

Si Zuul a été choisi parmi d’autres solutions capables de fournir la même panoplie de services, c’est parce que la solution de Netflix possède un argument de poids : il permet la gestion de filtres dynamiques, qui peuvent être stockés en base. Pour l’instant, seule la base de données NoSQL Cassandra est supportée mais il est très simple d’implémenter un connecteur pour tout autre système de persistence. Cette fonctionnalité permet de modifier le comportement de Zuul vis-à-vis des requêtes au runtime, sans avoir à relancer le serveur. Les transactions opérées en base sont réalisées en utilisant Astyanax, un client Cassandra pour Java développé aussi par Netflix.

Mise en place du serveur

Afin de configurer notre serveur Zuul, nous allons utiliser Spring Boot et Spring Cloud et les annotations qui ont été créées : @EnableZuulProxy et @EnableZuulServer. Ces annotations sont présentes dans le module spring-cloud-starter-zuul, et pour la partie chargement de filtres dynamiques, il faut utiliser le module Zuul de Netflix zuul-netflix. Voici à quoi ressemble le main d’un serveur Zuul :

@EnableAutoConfiguration
@EnableZuulProxy
public class ZuulServerApplication {

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

ZuulServerApplication.java

Il faut aussi le configurer en utilisant un fichier application.yml pour lui indiquer où se situe le serveur Eureka, et surtout, indiquer les routes à utiliser pour tel ou tel service. Le point d’entrée “fonctionnel” de notre application étant le service Product, il faut indiquer à Zuul quelle route utiliser pour requêter Product.

server:
  port: 8765

eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8761/eureka/
  instance:
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${server.port}:${random.value}}

zuul:
  routes:
    product-service:
      path: /products/**
      serviceId: product-service
      stripPrefix: false

application.yml

Le serveur Zuul apparait désormais dans la liste des services inscrits dans le registre des services d’Eureka. Désormais, toute requête vers Product passera par Zuul en utilisant l’URL suivante : http://ip_de_zuul:port_de_zuul/products.

Chargement dynamique des filtres

Maintenant que le serveur est en place, nous pouvons utiliser Astyanax pour nous connecter à notre base Cassandra, et ainsi prétendre à utiliser le système de chargement de filtres dynamiques. Nous aurions pu utiliser le driver Datastax, ce qui a été fait du côté des “sous-services” Details et Pricing pour requêter la base de données. Dans ce cas précis de filtres dynamiques, Netflix a préféré utiliser Astyanax. Voici comment se connecter à une base Cassandra et récupérer un Keyspace :

public void connectToCassandra() {
		LOG.info("Connecting to Cassandra database...");
		ctx = new AstyanaxContext.Builder()
				.forCluster(CLUSTER)
				.forKeyspace(KEYSPACE)
				.withConnectionPoolConfiguration(
						new ConnectionPoolConfigurationImpl("productcpc")
								.setSeeds(CASSANDRA_DB_IP)
								.setPort(CASSANDRA_DB_PORT)
								.setAuthenticationCredentials(
										new SimpleAuthenticationCredentials(CASS_USER, CASS_PASSWORD))
								.setMaxConnsPerHost(1))
				.withAstyanaxConfiguration(
						new AstyanaxConfigurationImpl()
								.setDiscoveryType(NodeDiscoveryType.NONE)
								.setCqlVersion(CQL_VERSION)
								.setTargetCassandraVersion(CASSANDRA_VERSION))
				.buildKeyspace(ThriftFamilyFactory.getInstance());

		ctx.start();
		LOG.info("Connected to Cassandra database " + CASSANDRA_DB_IP + ". Cluster : " + CLUSTER);
		ks = ctx.getClient();
		LOG.info("Keyspace : " + ks.getKeyspaceName());
	}

Astyanax example

Cet objet Keyspace va nous permettre d’utiliser ZuulFilterDAOCassandra. En combinaison avec ZuulFilterPoller, le mécanisme automatique de récupération des filtres en base sera lancé. Cependant, ces filtres ont une certaine particularité qui découle de leur dynamisme : ils doivent être écrits en Groovy. Aussi, ces filtres doivent hériter de la classe abstraite ZuulFilter et implémenter certaines méthodes qui décriront le comportement du filtre, comme par exemple : son ordre d’exécution, son type (pre, post ou route), etc.

dao = new ZuulFilterDAOCassandra(ks);
...
ZuulFilterPoller.start(dao);

Poller sample

Note : Il est aussi possible de charger les filtres depuis le FileSystem, à défaut de vouloir utiliser une base de données.

Après avoir mis en place le chargement dynamique, il s’agit de créer son propre filtre pour un cas d’utilisation en particulier. Ces filtres peuvent être utilisés pour établir des règles de sécurité, de faire de l’A/B testing ou du canary testing, voire de rajouter des paramètres à vos requêtes, dans un header par exemple. Les possibilités sont très nombreuses ce qui fait de Zuul un outil important et très pratique pour mieux gérer votre application.

La prochaine et dernière partie de ce REX se penchera sur l’utilisation d’Hystrix, application permettant de faire de la “fault tolerance”, de son dashboard et de la stack ELK pour la concentration et l’analyse des logs.