Introduction
Vous avez lu toute la théorie sur les architectures micro services, mais vous ne savez toujours pas comment en implémenter une ? Dans ce cas cet article est probablement pour vous.
Il faut savoir qu’ont peut réaliser ça de plusieurs façons différentes, tout dépends des cas d’utilisation, technologie, plateforme cible ….
c’est pourquoi dans cet article on va essayer d’implémenter une architecture microservices basique, mais qui pourra évoluer selon vos besoins, on va utiliser principalement le language java avec le framework spring boot ainsi que différentes librairies de spring cloud.
Code source
Vous pouvez accéder au code source de cet exemple ici.
Architecture minimale
Voici l’architecture qu’on va implémenter :
chaque brique présentée ci-dessus est un webservice à part, donc un projet spring boot par webservice.
On aura des webservices de « support » (nécéssaires au fonctionnement de l’architecture), et des webservices « métier » qui vont répondre aux besoins des cas d’utilisation métier des utilisateurs finaux.
faisons un tour des différentes briques:
Registre Service:
Chaque webservices dans notre architecture est un projet spring boot à part, donc chaque webservice aura sa propre url.
Le registre service sert à enregistrer et tracker l’ensemble de nos webservices dans leurs permettre de communiquer entre eux.
il va donc garder pour chaque service un ensemble de méta-data comme :
- URI du service
- nom
- état ( up, down)
…
ce registre sera sollicité par le reste des services, qu’ils soient de type « support » ou « métier ».
Passerelle:
La passerelle va jouer le rôle de reverse proxy devant l’ensemble de nos webservices, elle va donc:
- isoler les webservices du monde extérieur: tout nos services seront accessibles via la passerelle, un utilisateur final ne devrait pas connaitre les URI des différents webservices qu’il veut utiliser, il aura juste besoin de connaître la passerelle, cette dernière va se charger de router ces requêtes aux services concernés.
-
jouer le rôle de load balancer: dans le cas ou on a plusieurs instances d’un même webservice, la passerelle va pouvoir décider à quel instance router une requête HTTP entrante selon la disponibilité de chaque instance.
La passerelle va se baser sur le registre service pour son fonctionnement.
Serveur de configuration
Ce serveur gère la configuration des différentes instances des webservices dans notre système, il va vous permettre par exemple de mettre à jour la configuration de toutes les instances d’un webservice donnée, et sans être obligé de redémarrer les instances.
Vos webservices
Les webservices à l’intérieur de notre architecture devront obligatoirement s’enregistrer dans le registre service, ne seront accessible que derrière la Passerelle et pourront optionellement charger leur configuration depuis le serveur de configuration durant la séquence de démarrage.
Gérer l’authentification et authorisation
Vous avez plein d’options pour gérer l’authentification des utilisateur et l’authorisation d’applications (clients) à accèder à votre système, dans notre exemple je vais utiliser oauth2 pour gérer l’authorisation des clients (web, mobile …) et spring security pour authentifier les utilisateurs.
un utilisateur va pouvoir s’authentifier sur notre serveur si il viens d’une application authorisée par notre serveur oauth, dans ce cas le serveur va lui donner un token qui va lui permettre d’exploiter les webservices dans notre architecture.
Exemple d’architecture
On va maintenant implémenter l’architecture suivante :
on est presque dans la même architecture présentée plus haut, on ajoute un serveur d’authorisation, et 2 webservices métier:
Gestion des utilisateurs : pour gérer la création de compte utilisateurs, modification d’infos personelles …
Catalogue produit : Gestion des produits d’un magasin par exemple
Implémentation de l’exemple
Je ne vais expliquer ici que les parties les plus importantes du projet, mais vous pouvez voir l’ensemble du code sur github.
1. Registre des webservices avec Eureka
On va Commencer le registre service nommé discovery dans notre exemple, on va utiliser eureka ici, pour utiliser eureka on doit suivre les étapes suivantes :
Ajouter la dépendance dans build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}
Ajouter l’annotation @EnableEurekaServer pour activer le serveur eureka.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryApplication.class, args);
}
}
configuration dans : src/main/resources/application.yml
server:
port: 9761
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
waitTimeInMsWhenSyncEmpty: 0
cette configuration va principalement dire au serveure eureka de ne pas s’enregistrer dans son propre registre, et d’écouter le port 9761.
2. Serveur de configuration
Le serveur de configuration a besoin d’une source de configurations à servir aux webservices concernés, dans notre exemple on va stocker les configurations dans un repo git.
Créez un repo git pour vos configurations ou clonez juste le repo d’exemple
git clone url-repo-example
Après cela on crée le projet configServer :
a. Dépendance dans build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
Remarquez qu’on ajoute la dépendance eureka-client, le serveur de configuration sera donc enregistré dans le registre services, ainsi les autres webservices de l’application sauront ou chercher leurs configurations durant leurs phase de démarrage.
b. Configuration du serveur
server:
port: 9889
spring:
application:
name: configserver
cloud:
config:
server:
git:
uri: {CONFIG_SERVICE_GIT_URL:file://<path to config repo>}
eureka:
instance:
hostname: localhost
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone:{EUREKA_SERVICE_URL:http://localhost:9761}/eureka/
ici on dit au serveur ou se trouver les fichiers de configurations des différents webservices, puis on configure le chemin vers le registre service.
c. Ajouter les annotations pour activer le serveur de configuration
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigserverApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigserverApplication.class, args);
}
}
on va ajouter les annotations @EnableEurekaClient et @EnableConfigServer pour activer le client eureka et le serveur de configuration.
3. Passerelle avec spring gateway
La passerelle est la porte d’entrée de notre système, elle va se baser sur l’url pour choisir à quel webservice passer la requête entrante.
on va configurer dans notre exemple les routes suivantes:
- /api/users-management —> webservice users
- /api/products-catalog —> webservice product
- /oauth —> service d’authorisation et d’authentification
on va maintenant créer le projet:
a. Dépendances dans build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
la passerelle doit utiliser le registre eureka pour trouver le chemin des webservices, on va donc utiliser encore eureka client.
on charge aussi la dépendance spring-gateway pour gérer la partie reverse proxy.
b. Configuration du serveur
spring:
cloud:
gateway:
x-forwarded:
enabled: true
for-enabled: true
for-append: true
proto-enabled: false
host-enabled: false
port-enabled: false
routes:
- id: api_users_route
uri: lb://users
predicates:
- Path=/api/users-management/**
- id: api_products_route
uri: lb://products
predicates:
- Path=/api/products-catalog/**
- id: api_oauth_route
uri: lb://oauth
predicates:
- Path=/oauth/**
eureka:
instance:
hostname: localhost
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: ${EUREKA_SERVICE_URL:http://localhost:9761}/eureka/
c. Code Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Pas besoin d’ajouter d’annotation pour la passerelle ici, la dépendance est suffisante, mais il faut quand même ajouter l’annotation EnableEurekaClient pour donner accès aux registre services.
4. Serveur d’authorisation oauth
On va gérer ici l’authentification des utilisateurs, et l’authorisation des applications (clients dans la terminologie oauth).
Ce service devra se connecter à une base de donnée postgres qui contient les utilisateurs, on va aussi utiliser spring-cloud-oauth2 pour gérer le flow oauth ainsi que security-jwt pour gérer les token jwt.
a. Dépendances dans build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-security'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2'
implementation 'org.springframework.security:spring-security-jwt'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation 'org.postgresql:postgresql:42.2.18'
}
b. Configuration du service
server:
servlet:
context-path: /oauth
health:
config:
enabled: false
# ===============================
# = DATABASE STUFF
# ===============================
spring:
jpa.database-platform: org.hibernate.dialect.PostgreSQLDialect
datasource:
url: jdbc:postgresql://localhost:5432/mydatabase
username: postgres
password: password
driverClassName: org.postgresql.Driver
initialization-mode: always
# ===============================
# = Key to sign jwt tokens
# ===============================
signing:
key: somekeyhere
c. sécuriser le serveur d’authorisation
On va ici sécuriser les endpoints d’oauth qui permettent de fournir des tokens pour accéder à l’application,
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean(name = "passwordEncoder")
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
DefaultRedirectStrategy authenticationSuccessHandlerRedirectStrategy = new DefaultRedirectStrategy();
authenticationSuccessHandlerRedirectStrategy.setContextRelative(true);
SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
authenticationSuccessHandler.setDefaultTargetUrl("/");
authenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true);
authenticationSuccessHandler.setRedirectStrategy(authenticationSuccessHandlerRedirectStrategy);
http.anonymous().and()
.authorizeRequests()
.antMatchers("/", "/public/**", "/oauth/authorize", "/authorize", "/oauth/login", "/login").permitAll()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler)
.permitAll()
.and()
.logout()
.permitAll().and().anonymous();
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
d. Configurer le service d’authorisation
On doit configurer maintenant un client oauth, le client dans ce cas représente l’application (front) que l’utilisateur va utiliser, et c’est cette application front qui va accéder à l’api.
on peut donc dire que le token représente un utilisateur U autorisant l’application App à utiliser ces données dans l’api API.
un webservice métier
Maintenant qu’on a mis en place les webservices « techniques », on va s’attaquer maintenant aux webservices orienté métier.
On va commencer par le webservices gestion des utilisateurs, ce webservice devra donc s’enregistrer sur le registre services, et n’accepter que les requêtes HTTP contenant un token signé par notre serveur oauth.
a. Dépendances dans build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2'
implementation 'org.springframework.security:spring-security-jwt'
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation 'org.liquibase:liquibase-core:3.10.3'
implementation 'org.postgresql:postgresql:42.2.18'
}
b. Configuration du webservice
server:
servlet:
context-path: /api/users-management
health:
config:
enabled: false
# ===============================
# = DATABASE Stuff
# ===============================
spring:
jpa.database-platform: org.hibernate.dialect.PostgreSQLDialect
datasource:
url: jdbc:postgresql://localhost:5432/mydatabase
username: postgres
password: password
driverClassName: org.postgresql.Driver
initialization-mode: always
liquibase:
change-log: classpath:/db/changelog/db.changelog-master.yaml
enabled: true
contexts: dev
Le webservice gestion utilisateurs va partager la base de données avec le service oauth, cependant c’est bien au niveau de la gestion utilisateurs qu’on va créer la table pour les utilisateurs.
on va gérer les modifications dans la base de données (création table, ajout de colonne …) avec liquibase.
c. Configuration niveau Java
@SpringBootApplication
@EnableEurekaClient
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UsersApplication {
public static void main(String[] args) {
SpringApplication.run(UsersApplication.class, args);
}
}
l’annotation @EnableResourceServer va faire en sorte que le webservice ne soit accessible qu’avec un token.
remarquez qu’on a encore une fois utilisé l’annotation @EnableEurekaClient pour permettre au webservice de s’enregistrer sur le registre service.
Lancer le projet en local
Vous pouvez lancer l’exemple présenté dans cet article en suviant les étapes suivantes:
1. Cloner le projet
git clone git@github.com:bmarwane/microservices-project-example.git
2. Démarrer la base de données
pas la peine d’installer postgres, il suffit de lancer un container contenant notre base postgres avec la commande :
docker-compose up -d
2. Démarrer les services
la seule règle à respecter est de toujours démarrer le registre service et le service de configuration avant tout le reste, cependant pour pouvoir utiliser rapidement les services je conseil de démarrer les service selon l’ordre suivant:
- discovery ( registre service )
- config server
- oauth
- services metiers
- gateway service
Tester le fonctionnement
j’ai ajouté un service pour jouer le rôle du serveur front end, sachant qu’on est pas obligé de servire du code front end statique de cette manière, c’est fait comme ça dans cet exemple pour faire simple.
Scenario de test
On va supposer que notre application expose une api pour explorer le catalogue de produits, pour y accéder on devra passer par notre passerelle gateway.
voici l’url cible :
http://localhost:9080/api/product-catalog/somecategory/somesubcategory
dans cet exemple la passerelle est exposée sur le port : 9080
quand on test l’api via postman (ou autre outil) le serveur nous renvoi l’erreur suivante:
on constate que le serveur a besoin qu’on soit authentifié, c’est normal car on a pas ajouté de token prouvant qu’on est authentifié auprès du service d’authentification dans cette requête.
on devra donc demander un au service oauth, là aussi on devra passer par la gateway.
dans un navigateur ouvrez l’URL suivante :
http://localhost:9080/oauth/authorize?response_type=token&client_id=app&redirect_uri=http://localhost:9080/front-end/callback
utiliser les identifiants / mot de passe suivants :
login: admin
mot de passe: admin
si tout va bien vous devriez voir cette page :
On doit maintenant prendre le token généré pour le rajouter à notre requête dans postman, pour ce faire on ajoute le paramètre Authorization dans les headers de la requête, ce paramètre va contenir le token qui prouve qu’on est authentifié sur le service d’authentification.
voici le format du paramètre à rajouter :
clé: Authorization
valeur: Bearer <votre token ici>
Exemple sur postman :
Remarquez que l’api nous a bien répondu.
Recap du scénario de test
Comme on a vu dans notre exemple, on a pu accéder à une api « métier » (catalogue produits) en utilisant un token d’authentification venant du service d’authentification, et toutes nos requêtes ont circulé via la passerelle (gateway), ce qui nous a permis de cacher toute l’architecture de notre système dérière la seule url de la passerelle.
Conclusion
Implémenter une architecture micro services peut s’avérer lourd à monter et à maintenir, cette architecture ajoute une complexité par rapport à une architecture monolithique classique, cependant cette complexité viens aussi avec des avantages comme:
- pouvoir scaller facilement voir même automatiquement certains services.
- gestion de la configuration des services plus facile.
- pouvoir développer et déployer chaque webservice individuellement sans impacter le reste des services.
Au vu des avantages de ce genre d’architectures, on peut constater qu’elle est surtout adaptée pour les grosses applications, c’est bien de savoir comment implémenter une architecture microservices, mais je conseil quand même d’éviter de partir sur ce genre d’archi systèmatiquement.