Spring Cloud Consul
- Spring Cloud Consul integrates Consul with Spring Boot applications, facilitating service discovery, configuration management, and distributed system coordination.
- Leverages the open-source Consul platform for managing complex microservice architectures, enhancing service resilience and ease of scaling.
Install and Start Consul
- Download Consul: https://developer.hashicorp.com/consul/install
- Unzip the downloaded file.
- add the location to your PATH for Windows).
- consul agent -dev
- it will run on localhost:8500.
- Access the Consul UI: http://localhost:8500 to access the Consul UI.
Tp1: Service Discovery
1. Client-A Application
Step 2.1: Create a Spring Boot Project for Client-ADependencies:
- Spring Web
- Spring Cloud Starter Consul Discovery
- Spring Boot DevTools (optional for easier development)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
Step 2.2: Configure application.yml for Client-A
Create a file named application.yml in the src/main/resources:
spring:
application:
name: client-a
cloud:
consul:
host: localhost
port: 8500
Step 2.3: Create a REST Controller for Client-A
Create a REST controller that exposes an endpoint:
package com.example.clienta;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ClientAController {
@GetMapping("/greet")
public String greet() {
return "Hello from Client A!";
}
}
Step 2.4: Create the Main Application Class
package com.example.clienta;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ClientAApplication {
public static void main(String[] args) {
SpringApplication.run(ClientAApplication.class, args);
}
}
Run Client A
2. Client-B Application Setup
Create a Spring Boot Project for Client-B
Similar to Client-A, create another Spring Boot project for Client-B with the same dependencies.pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
application.yml for Client-B
Create a file named application.yml in the src/main/resources directory with the following content:
spring:
application:
name: client-b
cloud:
consul:
host: localhost
port: 8500
Create a REST Controller for Client-B
Create a REST controller that calls Client-A:
package com.example.clientb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
public class ClientBController {
@Autowired
private DiscoveryClient discoveryClient;
private final RestTemplate restTemplate = new RestTemplate();
@GetMapping("/call-a")
public String callClientA() {
List<ServiceInstance> instances = discoveryClient.getInstances("client-a");
if (instances != null && !instances.isEmpty()) {
String clientAUrl = instances.get(0).getUri().toString() + "/greet";
return restTemplate.getForObject(clientAUrl, String.class);
}
return "Client A not available";
}
}
- DiscoveryClient: Used for service discovery. It provides information about registered services (such as their URLs) and allows applications to locate other services by name.
- RestTemplate: Used for making HTTP requests. After obtaining the service URL from DiscoveryClient, RestTemplate uses this URL to make RESTful calls to the discovered service.
- Client-B uses DiscoveryClient to retrieve the URL of Client-A from the Consul Server.
- DiscoveryClientcontacts Consul Server to fetch the URL of Client-A.
- RestTemplate then uses the retrieved URL to make HTTP requests to Client-A.
Create the Main Application Class for Client-B
package com.example.clientb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ClientBApplication {
public static void main(String[] args) {
SpringApplication.run(ClientBApplication.class, args);
}
}
Run
- Run Consul
- Run Client-A:
- Run Client-B:
Test the API: Use a tool like Postman or cURL to test the endpoints.
Call Client-A: http://localhost:8080/greet
Call Client-B :that call A :http://localhost:8081/call-a
You should see:
The response from Client-A when hitting the /greet endpoint.
The response from Client-B calling Client-A through service discovery.
Configuration Management
Consul: Acts as a centralized configuration storage where configurations (key-value pairs) for applications are stored.
Spring Boot Application: The application loads configurations from Consul when it starts and injects them into the
@ConfigurationPropertiesclass (e.g.,AppConfig).@RefreshScope: This annotation allows specific beans, such as
AppConfig, to be refreshed with updated values when a configuration refresh is triggered.Actuator
/refreshEndpoint: When a POST request is sent to/actuator/refresh, Spring Cloud reloads the configurations from Consul without restarting the application.User/Admin: Can update configuration values directly in Consul and then trigger the
/refreshendpoint, making the updated configurations immediately available in the application.
Create Configuration
Create Configuration in Consul Key-Value Store- Open the Consul UI at http://localhost:8500.
- Navigate to Key/Value.
- Create a key-value pair :config/application-name/data
- config/client-a/messge/
- use cmd :consul kv put config/client-a/message "client a message"
Create the Spring Boot Application
including the following dependencies:- Spring Web (to create REST endpoints)
- Spring Cloud Starter Consul Config (for Consul integration)
- Spring Boot Actuator (for monitoring and refreshing)
pom.xml
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Configure application.yml for Consul
spring:
application:
name: clienta
cloud:
consul:
host: localhost
port: 8500
discovery:
enabled: true
config:
enabled: true
prefix: config
config:
import: "consul:"
management:
endpoints:
web:
exposure:
include: health,info,refresh
server:
port: 8080
Use Consul Configuration in the Application
package com.example.clienta;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@RefreshScope
@Component
public class AppConfig {
@Value("${message}")
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Create a REST Controller to Display the Configuration
package com.example.clienta;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConfigController {
private final AppConfig appConfig;
public ConfigController(AppConfig appConfig) {
this.appConfig = appConfig;
}
@GetMapping("/message")
public String getMessage() {
return appConfig.getMessage();
}
}
Run
Enable Actuator’s Refresh Endpoint In application.yml, enable the /actuator/refresh endpoint by adding the following configuration:
management:
endpoints:
web:
exposure:
include: refresh
Run
http://localhost:8080/message You should see the value of message as set in Consul (Hello from Consul Config!).Update the Configuration in Consul: Go to http://localhost:8500. Update the message key under config/client-a/data to a new value, like "Hello, updated from Consul!".
Refresh the Configuration: you can
- 1.Restart the application
-
2.send a POST request to the /actuator/refresh endpoint:
http://localhost:8080/messagePOST http://localhost:8080/actuator/refresh
Load balancing
- Load balancing is a technique for distributing network traffic across a server farm.
- It optimizes network performance, reliability, and capacity.
- Reduces latency by evenly distributing demand among multiple servers and resources.
- Uses an appliance (physical or virtual) to identify the best server for client requests in real-time.
- Prevents overwhelming a single server during heavy network traffic.
- Provides failover; if one server fails, workloads are redirected to a backup server.
Client Request:
- The process starts with a client making a request.
Load Balancer:
- The load balancer receives the client request and determines which server is available to handle the request.
Available Servers:
- The load balancer evaluates the pool of available servers (e.g., Server A, Server B, Server C) and chooses one to forward the request.
Forwarding Requests:
- The load balancer forwards the request to the selected server, which processes the request and returns a response.
Response:
- The server processes the request and sends the response back to the client.
Failover Handling:
- If the load balancer detects a failure while processing the request (for example, if the selected server becomes unavailable):
- The load balancer redirects the request to a backup server.
- The backup server processes the request and returns the response to the client.
- If the load balancer detects a failure while processing the request (for example, if the selected server becomes unavailable):
HashiCorp Consul avec Spring Boot
Guide Complet et Détaillé - Explications Approfondies
Installation via developer.hashicorp.com/consul/install - Windows OS
Table des Matières
Introduction à Consul et Spring Boot
Qu'est-ce que HashiCorp Consul ?
HashiCorp Consul est une solution de service mesh et de découverte de services complète qui permet aux services de découvrir, configurer et sécuriser les communications entre eux. Consul offre une plateforme unifiée pour connecter et sécuriser les services dans n'importe quel environnement.
Fonctionnalités Principales de Consul
- Service Discovery : Découverte automatique des services et de leurs emplacements
- Health Checking : Surveillance continue de l'état de santé des services
- Key/Value Store : Stockage distribué de configuration et de métadonnées
- Multi Datacenter : Support natif pour les environnements multi-datacenter
- Service Mesh : Connectivité sécurisée entre les services avec observabilité
- Connect : Communication sécurisée entre services avec mTLS
Architecture de Consul
Architecture typique avec Consul :
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Service A │ │ Service B │ │ Service C │
│ (Consul Client) │ │ (Consul Client) │ │ (Consul Client) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌────────────▼────────────┐
│ Consul Server │
│ (Agent Serveur) │
│ │
│ ┌─────────────────┐ │
│ │ Service Registry│ │
│ │ Health Checks │ │
│ │ KV Store │ │
│ └─────────────────┘ │
└─────────────────────────┘
Spring Boot et Consul
Intégration Spring Boot
Spring Cloud Consul fournit une intégration transparente entre Spring Boot et Consul :
Avantages de l'Intégration
- Auto-configuration : Configuration automatique des clients Consul
- Service Discovery : Découverte automatique des services avec @EnableDiscoveryClient
- Configuration distribuée : Récupération de configuration depuis Consul KV
- Load balancing : Load balancing client-side intégré
- Health checks : Enregistrement automatique des health checks
Cycle de Vie de l'Intégration
- Démarrage : L'application Spring Boot démarre
- Configuration : Auto-configuration de Consul Client
- Enregistrement : Enregistrement du service dans Consul
- Découverte : Découverte d'autres services via DiscoveryClient
- Configuration : Récupération de la configuration depuis KV Store
- Monitoring : Envoi de métriques et health checks
Installation Consul sur Windows
Méthode Officielle via developer.hashicorp.com
Accédez à la page officielle d'installation :
https://developer.hashicorp.com/consul/install
Options d'Installation Windows
- Package ZIP : Méthode recommandée pour Windows
- Chocolatey : Package manager Windows
- Windows Subsystem for Linux (WSL) : Pour environnement Linux-like
- Docker : Pour environnement conteneurisé
Téléchargement via navigateur :
Étapes de Téléchargement
- Allez sur
https://developer.hashicorp.com/consul/install - Sélectionnez Windows comme système d'exploitation
- Choisissez l'architecture AMD64 (64-bit)
- Cliquez sur Download pour télécharger le fichier ZIP
- Le fichier téléchargé sera nommé quelque chose comme :
consul_1.16.1_windows_amd64.zip
Installation manuelle :
# 1. Créez un répertoire pour Consul
mkdir C:\consul
cd C:\consul
# 2. Extrayez le fichier ZIP téléchargé
# Utilisez l'explorateur Windows ou PowerShell :
Expand-Archive -Path "chemin\vers\consul_*.zip" -DestinationPath "C:\consul"
# 3. Vérifiez l'extraction
dir C:\consul
# Vous devriez voir consul.exe
Ajout au PATH système :
# Méthode PowerShell (en tant qu'administrateur)
$env:PATH += ";C:\consul"
[Environment]::SetEnvironmentVariable("PATH", $env:PATH, [EnvironmentVariableTarget]::Machine)
# Ou via l'interface graphique Windows :
# 1. Clic droit sur "Ce PC" → Propriétés
# 2. Paramètres système avancés
# 3. Variables d'environnement
# 4. Sélectionnez "Path" → Modifier
# 5. Ajoutez "C:\consul"
# 6. Redémarrez PowerShell/CMD
Vérification de l'installation :
# Ouvrez une nouvelle invite de commande
consul --version
# Réponse attendue :
# Consul v1.16.1
# Revision 12345678
# Vérification des commandes disponibles
consul --help
Validation
Si la commande consul --version retourne le numéro de version, l'installation est réussie !
Installation via Chocolatey
Installation avec Chocolatey
Chocolatey est un package manager pour Windows qui simplifie l'installation de logiciels.
Installation de Chocolatey (si non installé) :
# Ouvrez PowerShell en tant qu'administrateur
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Vérifiez l'installation
choco --version
Installation de Consul via Chocolatey :
# Installation de Consul
choco install consul
# Mise à jour de Consul
choco upgrade consul
# Désinstallation de Consul
choco uninstall consul
Avantages de Chocolatey
- Mise à jour automatique : Facile de garder Consul à jour
- Gestion des dépendances : Gère automatiquement les dépendances
- Installation silencieuse : Peut être automatisée dans des scripts
- Centralisation : Gère plusieurs logiciels avec une seule interface
Installation via Docker (Windows)
Installation avec Docker Desktop
Docker Desktop pour Windows permet d'exécuter des conteneurs Linux et Windows.
Installation de Docker Desktop :
# Téléchargez Docker Desktop depuis :
# https://www.docker.com/products/docker-desktop
# Installez Docker Desktop en suivant l'assistant d'installation
# Vérifiez l'installation
docker --version
docker-compose --version
Exécution de Consul dans Docker :
# Démarrage de Consul en mode développement
docker run -d --name=consul-dev -p 8500:8500 -p 8600:8600/udp hashicorp/consul:latest agent -dev -client=0.0.0.0
# Vérification
docker ps
# Vous devriez voir le conteneur consul-dev en cours d'exécution
# Accès à l'interface web
# http://localhost:8500
# Arrêt du conteneur
docker stop consul-dev
docker rm consul-dev
Configuration persistante avec Docker :
# Création de volumes pour persistance
docker volume create consul-data
# Démarrage avec volume persistant
docker run -d --name=consul-server \
-p 8500:8500 -p 8600:8600/udp \
-v consul-data:/consul/data \
-v C:\consul\config:/consul/config \
hashicorp/consul:latest agent \
-server -bootstrap-expect=1 -ui -config-dir=/consul/config
# Configuration locale
# C:\consul\config\server.json
{
"server": true,
"node_name": "consul-server-windows",
"datacenter": "dc1",
"data_dir": "/consul/data",
"bind_addr": "0.0.0.0",
"client_addr": "0.0.0.0",
"bootstrap_expect": 1,
"ui_config": {
"enabled": true
}
}
Configuration Spring Boot
Création du Projet Spring Boot
Création via Spring Initializr :
# Accédez à https://start.spring.io/
# Configuration recommandée :
Project: Maven Project
Language: Java
Spring Boot: 2.7.x ou 3.x
Packaging: Jar
Java: 11 ou 17
# Dépendances à inclure :
- Spring Web
- Spring Boot Actuator
- Spring Cloud Consul Discovery
- Spring Cloud Consul Config
- Spring Cloud LoadBalancer (optionnel)
Ajout des dépendances Maven :
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>User Service with Consul</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Cloud Consul Discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- Spring Cloud Consul Config -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<!-- Spring Cloud LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Configuration de la classe principale :
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Explication des Annotations
- @SpringBootApplication :
Combinaison de @Configuration, @EnableAutoConfiguration et @ComponentScan.
- @EnableDiscoveryClient :
Active la découverte de services via Consul Discovery.
Configuration Bootstrap
Configuration bootstrap.yml :
# src/main/resources/bootstrap.yml
spring:
application:
name: user-service
cloud:
consul:
host: localhost
port: 8500
discovery:
instance-id: ${spring.application.name}:${server.port}:${random.value}
service-name: ${spring.application.name}
prefer-ip-address: true
ip-address: ${spring.cloud.client.ip-address}
port: ${server.port}
health-check-path: /actuator/health
health-check-interval: 15s
tags:
- ${spring.profiles.active:default}
- version=1.0.0
metadata:
version: 1.0.0
environment: ${spring.profiles.active:default}
config:
enabled: true
format: YAML
prefix: config
default-context: application
profile-separator: ','
data-key: data
management:
endpoints:
web:
exposure:
include: health,info,refresh
endpoint:
health:
show-details: always
Configuration application.yml :
# src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: user-service
cloud:
consul:
discovery:
enabled: true
register: true
deregister: true
heartbeat:
enabled: true
config:
enabled: true
watch:
enabled: true
delay: 1000
management:
endpoint:
health:
show-details: always
logging:
level:
org.springframework.cloud.consul: DEBUG
com.ecwid.consul: DEBUG
Explication des Propriétés
- spring.application.name :
Nom unique du service dans Consul.
- spring.cloud.consul.host/port :
Coordonnées du serveur Consul.
- prefer-ip-address=true :
Utilise l'IP plutôt que le nom d'hôte pour l'enregistrement.
- health-check-path :
Endpoint pour la vérification de santé.
- health-check-interval :
Fréquence des vérifications de santé.
Premier Démarrage
Démarrage du serveur Consul :
# Démarrage en mode développement
consul agent -dev
# Ou démarrage avec configuration
consul agent -config-dir=C:\consul\config
Construction et démarrage du service :
# Construction du projet
mvn clean package
# Démarrage du service
java -jar target/user-service-0.0.1-SNAPSHOT.jar
# Ou démarrage via Maven
mvn spring-boot:run
Vérification de l'enregistrement :
# Vérification via API
curl http://localhost:8500/v1/agent/services
# Vérification via interface web
# http://localhost:8500/ui/dc1/services
# Vérification des membres
curl http://localhost:8500/v1/agent/members
Validation
Le service devrait apparaître dans l'interface web de Consul avec le statut "passing".
Service Discovery avec Consul
Configuration de Base du Discovery Client
Configuration dans application.yml :
# application.yml
spring:
cloud:
consul:
discovery:
enabled: true
register: true
deregister: true
service-name: ${spring.application.name}
instance-id: ${spring.application.name}:${server.port}:${random.value}
prefer-ip-address: true
ip-address: ${spring.cloud.client.ip-address}
port: ${server.port}
health-check-path: /actuator/health
health-check-interval: 15s
heartbeat:
enabled: true
tags:
- ${spring.profiles.active:default}
- version=1.0.0
metadata:
version: 1.0.0
environment: ${spring.profiles.active:default}
region: us-east-1
# Actuator endpoints
management:
endpoints:
web:
exposure:
include: health,info,refresh
endpoint:
health:
show-details: always
Explication des Propriétés Discovery
- enabled=true :
Active la découverte de services.
- register=true :
Enregistre le service dans Consul au démarrage.
- deregister=true :
Désenregistre le service lors de l'arrêt.
- service-name :
Nom du service dans Consul.
- instance-id :
ID unique de l'instance (utile pour plusieurs instances).
- heartbeat.enabled=true :
Active le heartbeat pour maintenir l'état du service.
Utilisation du DiscoveryClient :
@Service
public class ServiceDiscoveryService {
@Autowired
private DiscoveryClient discoveryClient;
public List<String> getAllServices() {
return discoveryClient.getServices();
}
public List<ServiceInstance> getServiceInstances(String serviceName) {
return discoveryClient.getInstances(serviceName);
}
public ServiceInstance getRandomServiceInstance(String serviceName) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
if (!instances.isEmpty()) {
Random random = new Random();
return instances.get(random.nextInt(instances.size()));
}
return null;
}
}
Appel de Services Distants
Appel via RestTemplate avec LoadBalancer :
@Service
public class RemoteServiceClient {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
public String callRemoteService(String serviceName, String endpoint) {
String url = "http://" + serviceName + endpoint;
return restTemplate.getForObject(url, String.class);
}
public <T> T postToRemoteService(String serviceName, String endpoint, Object requestBody, Class<T> responseType) {
String url = "http://" + serviceName + endpoint;
return restTemplate.postForObject(url, requestBody, responseType);
}
}
Appel via WebClient avec LoadBalancer :
@Service
public class ReactiveServiceClient {
@Autowired
@LoadBalanced
private WebClient.Builder webClientBuilder;
public Mono<String> callRemoteService(String serviceName, String endpoint) {
return webClientBuilder.build()
.get()
.uri("http://" + serviceName + endpoint)
.retrieve()
.bodyToMono(String.class);
}
public <T> Mono<T> postToRemoteService(String serviceName, String endpoint, Object requestBody, Class<T> responseType) {
return webClientBuilder.build()
.post()
.uri("http://" + serviceName + endpoint)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(responseType);
}
}
Contrôleur d'appel de service :
@RestController
@RequestMapping("/api")
public class ServiceCallController {
@Autowired
private RemoteServiceClient remoteServiceClient;
@Autowired
private ReactiveServiceClient reactiveServiceClient;
@GetMapping("/call-service/{serviceName}/{endpoint}")
public ResponseEntity<String> callService(
@PathVariable String serviceName,
@PathVariable String endpoint) {
try {
String result = remoteServiceClient.callRemoteService(serviceName, "/" + endpoint);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error calling service: " + e.getMessage());
}
}
@GetMapping("/call-service-reactive/{serviceName}/{endpoint}")
public Mono<ResponseEntity<String>> callServiceReactive(
@PathVariable String serviceName,
@PathVariable String endpoint) {
return reactiveServiceClient.callRemoteService(serviceName, "/" + endpoint)
.map(ResponseEntity::ok)
.onErrorReturn(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error calling service"));
}
}
Configuration Avancée du Discovery
Configuration avec tags et métadonnées :
# application.yml
spring:
cloud:
consul:
discovery:
tags:
- environment=${spring.profiles.active:default}
- version=${app.version:1.0.0}
- region=${app.region:us-east-1}
- zone=${app.zone:a}
metadata:
version: ${app.version:1.0.0}
environment: ${spring.profiles.active:default}
region: ${app.region:us-east-1}
zone: ${app.zone:a}
team: ${app.team:backend}
query-passing: true
acl-token: ${consul.acl.token:}
prefer-agent-address: false
Filtrage par tags :
@Service
public class TaggedServiceDiscovery {
@Autowired
private DiscoveryClient discoveryClient;
public List<ServiceInstance> getProductionServices(String serviceName) {
return discoveryClient.getInstances(serviceName)
.stream()
.filter(instance -> instance.getMetadata().containsKey("environment") &&
"prod".equals(instance.getMetadata().get("environment")))
.collect(Collectors.toList());
}
public List<ServiceInstance> getServicesByVersion(String serviceName, String version) {
return discoveryClient.getInstances(serviceName)
.stream()
.filter(instance -> instance.getMetadata().containsKey("version") &&
version.equals(instance.getMetadata().get("version")))
.collect(Collectors.toList());
}
public ServiceInstance getNearestService(String serviceName) {
return discoveryClient.getInstances(serviceName)
.stream()
.filter(instance -> instance.getMetadata().containsKey("region") &&
"us-east-1".equals(instance.getMetadata().get("region")))
.findFirst()
.orElse(null);
}
}
Configuration de l'ACL :
# application.yml
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
acl-token: ${CONSUL_ACL_TOKEN:}
scheme: http
config:
acl-token: ${CONSUL_ACL_TOKEN:}
scheme: http
# Ou via variables d'environnement
# SET CONSUL_ACL_TOKEN=your-consul-acl-token
# SET SPRING_CLOUD_CONSUL_DISCOVERY_ACL_TOKEN=your-consul-acl-token
Gestion de Configuration avec Consul
Configuration de Base du Config Client
Configuration dans bootstrap.yml :
# bootstrap.yml
spring:
application:
name: user-service
cloud:
consul:
host: localhost
port: 8500
config:
enabled: true
format: YAML
prefix: config
default-context: application
profile-separator: ','
data-key: data
watch:
enabled: true
delay: 1000
acl-token: ${CONSUL_ACL_TOKEN:}
discovery:
enabled: true
register: true
service-name: ${spring.application.name}
management:
endpoints:
web:
exposure:
include: health,info,refresh
endpoint:
health:
show-details: always
Explication des Propriétés Config
- enabled=true :
Active la récupération de configuration depuis Consul.
- format=YAML :
Format de configuration (YAML, PROPERTIES, FILES).
- prefix=config :
Préfixe des clés de configuration dans Consul KV.
- watch.enabled=true :
Active la surveillance des changements de configuration.
- delay=1000 :
Délai entre les vérifications de changements (ms).
Stockage de configuration dans Consul KV :
# Configuration globale
curl -X PUT -d 'server:
port: 8080
spring:
application:
name: user-service
logging:
level:
root: INFO' \
http://localhost:8500/v1/kv/config/application/data
# Configuration spécifique à l'application
curl -X PUT -d 'app:
name: User Service
version: 1.0.0
database:
url: jdbc:mysql://localhost:3306/userdb
username: user
password: password123
pool-size: 10' \
http://localhost:8500/v1/kv/config/user-service/data
# Configuration par profil
curl -X PUT -d 'app:
name: User Service Dev
version: 1.0.0-SNAPSHOT
database:
url: jdbc:h2:mem:devdb
username: sa
password:
pool-size: 5' \
http://localhost:8500/v1/kv/config/user-service,dev/data
Utilisation de la Configuration
Injection de valeurs de configuration :
@Component
public class AppConfig {
@Value("${app.name:Default App}")
private String appName;
@Value("${app.version:1.0.0}")
private String appVersion;
@Value("${database.url:jdbc:h2:mem:default}")
private String databaseUrl;
@Value("${database.pool-size:10}")
private int databasePoolSize;
// Getters
public String getAppName() { return appName; }
public String getAppVersion() { return appVersion; }
public String getDatabaseUrl() { return databaseUrl; }
public int getDatabasePoolSize() { return databasePoolSize; }
}
@RestController
public class ConfigController {
@Autowired
private AppConfig appConfig;
@Autowired
private Environment environment;
@GetMapping("/config")
public Map<String, Object> getConfig() {
Map<String, Object> config = new HashMap<>();
config.put("appName", appConfig.getAppName());
config.put("appVersion", appConfig.getAppVersion());
config.put("databaseUrl", appConfig.getDatabaseUrl());
config.put("databasePoolSize", appConfig.getDatabasePoolSize());
config.put("activeProfiles", environment.getActiveProfiles());
return config;
}
}
Configuration avec @RefreshScope :
@Service
@RefreshScope
public class RefreshableConfigService {
@Value("${app.refreshable-property:defaultValue}")
private String refreshableProperty;
@Value("${app.timeout:5000}")
private int timeout;
public String getRefreshableProperty() {
return refreshableProperty;
}
public int getTimeout() {
return timeout;
}
}
@RestController
public class RefreshConfigController {
@Autowired
private RefreshableConfigService refreshableConfigService;
@PostMapping("/refresh")
public ResponseEntity<String> refreshConfig() {
// Cette méthode déclenche l'actualisation des beans @RefreshScope
return ResponseEntity.ok("Configuration refreshed at: " + new Date());
}
@GetMapping("/refreshable-config")
public Map<String, Object> getRefreshableConfig() {
Map<String, Object> config = new HashMap<>();
config.put("refreshableProperty", refreshableConfigService.getRefreshableProperty());
config.put("timeout", refreshableConfigService.getTimeout());
return config;
}
}
Configuration de base de données :
@Configuration
public class DatabaseConfig {
@Value("${database.url}")
private String databaseUrl;
@Value("${database.username}")
private String username;
@Value("${database.password}")
private String password;
@Value("${database.pool-size:10}")
private int poolSize;
@Bean
@Primary
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(databaseUrl);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(poolSize);
return new HikariDataSource(config);
}
}
// Utilisation dans un repository
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<User> findAll() {
return jdbcTemplate.query("SELECT * FROM users",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")));
}
}
Configuration Avancée
Configuration hiérarchique :
# Structure du KV store
config/
├── application/
│ ├── data (configuration globale)
│ └── dev/
│ └── data (configuration dev globale)
├── user-service/
│ ├── data (configuration user-service)
│ └── dev/
│ └── data (configuration user-service dev)
└── order-service/
├── data (configuration order-service)
└── prod/
└── data (configuration order-service prod)
# Hiérarchie de priorité (du plus spécifique au plus général) :
# 1. config/{application}/{profile}/data
# 2. config/{application}/data
# 3. config/application/{profile}/data
# 4. config/application/data
Configuration avec différents formats :
# Format YAML
curl -X PUT -d 'app:
features:
- user-management
- authentication
settings:
timeout: 5000
retries: 3' \
http://localhost:8500/v1/kv/config/user-service/data
# Format PROPERTIES
curl -X PUT -d 'app.features[0]=user-management
app.features[1]=authentication
app.settings.timeout=5000
app.settings.retries=3' \
http://localhost:8500/v1/kv/config/user-service/data
# Format JSON
curl -X PUT -d '{
"app": {
"features": ["user-management", "authentication"],
"settings": {
"timeout": 5000,
"retries": 3
}
}
}' \
http://localhost:8500/v1/kv/config/user-service/data
Surveillance des changements :
@Component
public class ConfigChangeEventListener {
private static final Logger logger = LoggerFactory.getLogger(ConfigChangeEventListener.class);
@EventListener
public void handleRefreshEvent(RefreshRemoteApplicationEvent event) {
logger.info("Configuration refresh event received: {}", event);
// Logique de traitement après actualisation
}
@EventListener
public void handleEnvironmentChangeEvent(EnvironmentChangeEvent event) {
logger.info("Environment changed keys: {}", event.getKeys());
// Réagir aux changements spécifiques
event.getKeys().forEach(key -> {
logger.info("Changed property: {}", key);
});
}
}
@RestController
public class ConfigWatchController {
@Autowired
private ConfigurableEnvironment environment;
@GetMapping("/config-values")
public Map<String, Object> getCurrentConfigValues() {
Map<String, Object> config = new HashMap<>();
// Récupérer toutes les propriétés
for (Iterator<PropertySource<?>> it = environment.getPropertySources().iterator(); it.hasNext();) {
PropertySource<?> propertySource = it.next();
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) propertySource;
for (String propertyName : enumerablePropertySource.getPropertyNames()) {
config.put(propertyName, environment.getProperty(propertyName));
}
}
}
return config;
}
}
Health Checks
Configuration de Base des Health Checks
Configuration dans application.yml :
# application.yml
spring:
cloud:
consul:
discovery:
health-check-path: /actuator/health
health-check-interval: 15s
health-check-timeout: 3s
health-check-critical-timeout: 1m
heartbeat:
enabled: true
ttl-value: 30
ttl-unit: s
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
health:
defaults:
enabled: true
Explication des Propriétés Health
- health-check-path :
Endpoint pour la vérification de santé.
- health-check-interval :
Intervalle entre les vérifications (15 secondes).
- health-check-timeout :
Timeout pour chaque vérification (3 secondes).
- health-check-critical-timeout :
Temps avant de marquer le service comme critique (1 minute).
- heartbeat.enabled=true :
Active le heartbeat pour maintenir l'état.
Health checks personnalisés :
@Component
public class CustomHealthIndicator implements HealthIndicator {
private static final Logger logger = LoggerFactory.getLogger(CustomHealthIndicator.class);
@Override
public Health health() {
try {
// Vérification personnalisée (ex: base de données, cache, etc.)
boolean isDatabaseUp = checkDatabaseConnection();
boolean isCacheUp = checkCacheConnection();
if (isDatabaseUp && isCacheUp) {
return Health.up()
.withDetail("database", "connected")
.withDetail("cache", "connected")
.withDetail("timestamp", System.currentTimeMillis())
.build();
} else {
return Health.down()
.withDetail("database", isDatabaseUp ? "connected" : "disconnected")
.withDetail("cache", isCacheUp ? "connected" : "disconnected")
.withDetail("timestamp", System.currentTimeMillis())
.build();
}
} catch (Exception e) {
logger.error("Health check failed", e);
return Health.down()
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
}
private boolean checkDatabaseConnection() {
// Implémentation de vérification de base de données
try {
// Exemple avec JDBC
// Connection connection = dataSource.getConnection();
// return connection.isValid(1);
return true; // Simplifié pour l'exemple
} catch (Exception e) {
return false;
}
}
private boolean checkCacheConnection() {
// Implémentation de vérification de cache
try {
// Exemple avec Redis
// redisTemplate.getConnectionFactory().getConnection().ping();
return true; // Simplifié pour l'exemple
} catch (Exception e) {
return false;
}
}
}
Health Checks Avancés
Health checks réactifs :
@Component
public class ReactiveHealthIndicator implements ReactiveHealthIndicator {
@Autowired
private WebClient.Builder webClientBuilder;
@Override
public Mono<Health> health() {
return webClientBuilder.build()
.get()
.uri("http://external-service/health")
.retrieve()
.bodyToMono(String.class)
.map(response -> Health.up().withDetail("external-service", "reachable").build())
.onErrorResume(throwable ->
Mono.just(Health.down().withDetail("external-service", "unreachable").build()));
}
}
@Component
public class CompositeHealthIndicator implements HealthIndicator {
@Autowired
private List<HealthIndicator> healthIndicators;
@Override
public Health health() {
Health.Builder builder = Health.unknown();
Map<String, Health> details = new HashMap<>();
boolean allUp = true;
boolean anyDown = false;
for (HealthIndicator indicator : healthIndicators) {
try {
Health health = indicator.health();
String name = indicator.getClass().getSimpleName().replace("HealthIndicator", "");
details.put(name, health);
if (health.getStatus() == Status.DOWN) {
anyDown = true;
allUp = false;
} else if (health.getStatus() != Status.UP) {
allUp = false;
}
} catch (Exception e) {
details.put(indicator.getClass().getSimpleName(), Health.down(e).build());
anyDown = true;
allUp = false;
}
}
if (anyDown) {
builder.down();
} else if (allUp) {
builder.up();
} else {
builder.unknown();
}
return builder.withDetails(details).build();
}
}
Configuration des health checks dans Consul :
# Enregistrement de service avec health check HTTP
{
"service": {
"name": "user-service",
"id": "user-service-1",
"address": "192.168.1.100",
"port": 8080,
"checks": [
{
"id": "user-service-http",
"name": "User Service HTTP Check",
"http": "http://192.168.1.100:8080/actuator/health",
"method": "GET",
"interval": "10s",
"timeout": "1s",
"status": "passing"
},
{
"id": "user-service-tcp",
"name": "User Service TCP Check",
"tcp": "192.168.1.100:8080",
"interval": "30s",
"timeout": "1s",
"status": "critical"
}
]
}
}
# Enregistrement via API
curl -X PUT -d @service-registration.json http://localhost:8500/v1/agent/service/register
Monitoring des états de santé :
@RestController
public class HealthMonitoringController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/health-summary")
public Map<String, Object> getHealthSummary() {
Map<String, Object> summary = new HashMap<>();
// Services passing
List<ServiceInstance> passingServices = new ArrayList<>();
List<ServiceInstance> warningServices = new ArrayList<>();
List<ServiceInstance> criticalServices = new ArrayList<>();
List<String> serviceNames = discoveryClient.getServices();
for (String serviceName : serviceNames) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
for (ServiceInstance instance : instances) {
// Vérifier l'état via Consul API
String healthStatus = getConsulHealthStatus(serviceName, instance);
switch (healthStatus.toLowerCase()) {
case "passing":
passingServices.add(instance);
break;
case "warning":
warningServices.add(instance);
break;
case "critical":
criticalServices.add(instance);
break;
}
}
}
summary.put("passing", passingServices.size());
summary.put("warning", warningServices.size());
summary.put("critical", criticalServices.size());
summary.put("total", serviceNames.size());
return summary;
}
private String getConsulHealthStatus(String serviceName, ServiceInstance instance) {
// Implémentation de vérification via Consul HTTP API
try {
String url = "http://localhost:8500/v1/health/checks/" + serviceName;
// Appel HTTP pour récupérer l'état de santé
return "passing"; // Simplifié pour l'exemple
} catch (Exception e) {
return "critical";
}
}
@GetMapping("/health-details/{serviceName}")
public List<Map<String, Object>> getHealthDetails(@PathVariable String serviceName) {
List<Map<String, Object>> details = new ArrayList<>();
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
for (ServiceInstance instance : instances) {
Map<String, Object> instanceHealth = new HashMap<>();
instanceHealth.put("instanceId", instance.getInstanceId());
instanceHealth.put("host", instance.getHost());
instanceHealth.put("port", instance.getPort());
instanceHealth.put("status", getInstanceHealthStatus(instance));
instanceHealth.put("metadata", instance.getMetadata());
details.add(instanceHealth);
}
return details;
}
private String getInstanceHealthStatus(ServiceInstance instance) {
// Implémentation de vérification individuelle
return "passing"; // Simplifié pour l'exemple
}
}
Notifications et Alertes
Système de notification :
@Component
public class HealthNotificationService {
private static final Logger logger = LoggerFactory.getLogger(HealthNotificationService.class);
@EventListener
public void handleInstanceRegisteredEvent(InstanceRegisteredEvent event) {
logger.info("New instance registered: {}", event.getInstance());
// Envoyer notification d'enregistrement
sendNotification("Instance Registered",
"New instance registered: " + event.getInstance().getServiceId());
}
@EventListener
public void handleInstanceDeregisteredEvent(InstanceDeregisteredEvent event) {
logger.info("Instance deregistered: {}", event.getInstance());
// Envoyer notification de désenregistrement
sendNotification("Instance Deregistered",
"Instance deregistered: " + event.getInstance().getServiceId());
}
@EventListener
public void handleHealthChangeEvent(HealthChangeEvent event) {
logger.info("Health status changed: {} -> {}",
event.getOldStatus(), event.getNewStatus());
if (event.getNewStatus() == Status.DOWN || event.getNewStatus() == Status.OUT_OF_SERVICE) {
sendAlert("Service Health Alert",
"Service " + event.getServiceId() + " is now " + event.getNewStatus());
}
}
private void sendNotification(String subject, String message) {
// Implémentation d'envoi de notification
// Email, Slack, SMS, etc.
logger.info("Sending notification - Subject: {}, Message: {}", subject, message);
}
private void sendAlert(String subject, String message) {
// Implémentation d'envoi d'alerte
logger.error("ALERT: {} - {}", subject, message);
// Intégration avec système d'alerte (PagerDuty, OpsGenie, etc.)
}
}
@Component
public class PeriodicHealthChecker {
@Autowired
private DiscoveryClient discoveryClient;
@Scheduled(fixedRate = 30000) // Toutes les 30 secondes
public void checkServiceHealth() {
List<String> services = discoveryClient.getServices();
for (String serviceName : services) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
for (ServiceInstance instance : instances) {
checkInstanceHealth(serviceName, instance);
}
}
}
private void checkInstanceHealth(String serviceName, ServiceInstance instance) {
try {
String healthUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/actuator/health";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(healthUrl, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logger.warn("Health check failed for service {} instance {}", serviceName, instance.getInstanceId());
// Envoyer alerte
}
} catch (Exception e) {
logger.error("Health check exception for service {} instance {}", serviceName, instance.getInstanceId(), e);
// Envoyer alerte critique
}
}
}
Intégration avec systèmes externes :
@Service
public class ExternalNotificationService {
@Value("${notification.slack.webhook.url:}")
private String slackWebhookUrl;
@Value("${notification.email.recipients:admin@example.com}")
private String emailRecipients;
public void sendSlackNotification(String message) {
if (StringUtils.hasText(slackWebhookUrl)) {
try {
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> payload = new HashMap<>();
payload.put("text", message);
restTemplate.postForObject(slackWebhookUrl, payload, String.class);
} catch (Exception e) {
logger.error("Failed to send Slack notification", e);
}
}
}
public void sendEmailNotification(String subject, String message) {
try {
// Implémentation d'envoi d'email
JavaMailSender mailSender = // ... configuration
SimpleMailMessage email = new SimpleMailMessage();
email.setTo(emailRecipients.split(","));
email.setSubject(subject);
email.setText(message);
// mailSender.send(email);
} catch (Exception e) {
logger.error("Failed to send email notification", e);
}
}
}
@RestController
public class NotificationController {
@Autowired
private ExternalNotificationService notificationService;
@PostMapping("/send-notification")
public ResponseEntity<String> sendNotification(@RequestBody Map<String, String> request) {
String type = request.get("type"); // "slack" ou "email"
String message = request.get("message");
String subject = request.get("subject");
switch (type) {
case "slack":
notificationService.sendSlackNotification(message);
break;
case "email":
notificationService.sendEmailNotification(subject, message);
break;
default:
return ResponseEntity.badRequest().body("Unsupported notification type");
}
return ResponseEntity.ok("Notification sent successfully");
}
}
Load Balancing
Configuration de Base du Load Balancer
Configuration dans application.yml :
# application.yml
spring:
cloud:
loadbalancer:
retry:
enabled: true
cache:
enabled: true
ttl: 30s
ribbon:
enabled: false # Désactiver Ribbon, utiliser Spring Cloud LoadBalancer
consul:
discovery:
# Configuration pour le load balancing
prefer-ip-address: true
query-passing: true
# Configuration des retries
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
retryExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
instances:
user-service:
baseConfig: default
Configuration de RestTemplate avec LoadBalancer :
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Configuration des timeouts
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
restTemplate.setRequestFactory(factory);
return restTemplate;
}
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
@Service
public class LoadBalancedServiceClient {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
@Autowired
@LoadBalanced
private WebClient.Builder webClientBuilder;
public String callService(String serviceName, String endpoint) {
String url = "http://" + serviceName + endpoint;
return restTemplate.getForObject(url, String.class);
}
public <T> T callService(String serviceName, String endpoint, Class<T> responseType) {
String url = "http://" + serviceName + endpoint;
return restTemplate.getForObject(url, responseType);
}
public <T> Mono<T> callServiceReactive(String serviceName, String endpoint, Class<T> responseType) {
return webClientBuilder.build()
.get()
.uri("http://" + serviceName + endpoint)
.retrieve()
.bodyToMono(responseType);
}
}
Explication des Propriétés LoadBalancer
- retry.enabled=true :
Active le mécanisme de retry automatique.
- cache.enabled=true :
Active le cache pour améliorer les performances.
- cache.ttl=30s :
Durée de vie du cache (30 secondes).
- ribbon.enabled=false :
Désactive Ribbon pour utiliser Spring Cloud LoadBalancer.
Stratégies de Load Balancing
Configuration du Round Robin (par défaut) :
@Configuration
public class LoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
// Utilisation dans un service
@Service
public class RoundRobinServiceClient {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
public String callService(String serviceName) {
// Le load balancing Round Robin est appliqué automatiquement
return restTemplate.getForObject("http://" + serviceName + "/api/data", String.class);
}
}
Configuration du Random Load Balancer :
@Configuration
public class RandomLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
// Utilisation avec annotation
@LoadBalancerClient(name = "user-service", configuration = RandomLoadBalancerConfiguration.class)
public class UserServiceClient {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
public User getUser(Long userId) {
return restTemplate.getForObject("http://user-service/users/{id}", User.class, userId);
}
}
Load Balancing avec poids :
@Configuration
public class WeightedLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> weightedLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new WeightedResponseTimeLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
@Service
public class WeightedServiceClient {
@Autowired
@LoadBalanced
private WebClient.Builder webClientBuilder;
public Mono<String> callServiceWithWeightedLB(String serviceName, String endpoint) {
return webClientBuilder.build()
.get()
.uri("http://" + serviceName + endpoint)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)));
}
}
// Configuration des poids via metadata dans Consul
// Service registration avec metadata
{
"service": {
"name": "user-service",
"id": "user-service-1",
"address": "192.168.1.100",
"port": 8080,
"meta": {
"weight": "80", // Poids plus élevé = plus de trafic
"version": "1.0.0",
"region": "us-east-1"
}
}
}
Retry et Résilience
Configuration des retries :
# application.yml
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.ResourceAccessException
- java.net.ConnectException
- java.net.SocketTimeoutException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException
instances:
user-service:
baseConfig: default
order-service:
baseConfig: default
maxAttempts: 5
waitDuration: 2000
spring:
cloud:
loadbalancer:
retry:
enabled: true
retryable-status-codes: 503,504,408
# Configuration des timeouts
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
Retry avec Resilience4j :
@Service
public class ResilientServiceClient {
private final Retry retry;
private final RestTemplate restTemplate;
public ResilientServiceClient(RetryRegistry retryRegistry) {
this.retry = retryRegistry.retry("user-service");
this.restTemplate = new RestTemplate();
}
@Retry(name = "user-service", fallbackMethod = "fallback")
public String callService(String serviceName, String endpoint) {
String url = "http://" + serviceName + endpoint;
return restTemplate.getForObject(url, String.class);
}
public String fallback(String serviceName, String endpoint, Exception ex) {
logger.warn("Fallback executed for service: " + serviceName, ex);
return "Service temporarily unavailable";
}
public String callServiceWithManualRetry(String serviceName, String endpoint) {
Supplier<String> retryableSupplier = Retry.decorateSupplier(retry, () -> {
String url = "http://" + serviceName + endpoint;
return restTemplate.getForObject(url, String.class);
});
return Try.ofSupplier(retryableSupplier)
.recover(throwable -> "Service temporarily unavailable: " + throwable.getMessage());
}
}
@RestController
public class ResilientController {
@Autowired
private ResilientServiceClient resilientServiceClient;
@GetMapping("/resilient-call/{serviceName}/{endpoint}")
public ResponseEntity<String> resilientCall(
@PathVariable String serviceName,
@PathVariable String endpoint) {
try {
String result = resilientServiceClient.callService(serviceName, "/" + endpoint);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("Service call failed: " + e.getMessage());
}
}
}
Configuration avancée des timeouts :
@Configuration
public class HttpClientConfig {
@Bean
public CloseableHttpClient httpClient() {
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(5000) // 5 secondes pour la connexion
.setSocketTimeout(10000) // 10 secondes pour la lecture
.setConnectionRequestTimeout(2000) // 2 secondes pour obtenir une connexion
.build();
return HttpClients.custom()
.setDefaultRequestConfig(config)
.setMaxConnTotal(100)
.setMaxConnPerRoute(20)
.build();
}
@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient());
RestTemplate restTemplate = new RestTemplate(factory);
return restTemplate;
}
}
@Service
public class TimeoutAwareServiceClient {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
public String callServiceWithTimeout(String serviceName, String endpoint, int timeoutMs) {
// Configuration dynamique du timeout
HttpComponentsClientHttpRequestFactory factory =
(HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory();
factory.setReadTimeout(timeoutMs);
try {
return restTemplate.getForObject("http://" + serviceName + endpoint, String.class);
} finally {
// Restaurer le timeout par défaut
factory.setReadTimeout(10000);
}
}
}
Load Balancing avec OpenFeign
Configuration OpenFeign :
<!-- Ajout de la dépendance OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
# application.yml
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: basic
requestInterceptors:
- com.example.interceptor.AuthInterceptor
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
compression:
request:
enabled: true
response:
enabled: true
hystrix:
enabled: false # Utiliser Resilience4j à la place
spring:
cloud:
openfeign:
client-config:
user-service:
connectTimeout: 5000
readTimeout: 10000
Interface Feign avec LoadBalancer :
@FeignClient(
name = "user-service",
configuration = UserServiceFeignConfig.class,
fallback = UserServiceFallback.class
)
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@GetMapping("/users")
List<User> getAllUsers();
@PostMapping("/users")
User createUser(@RequestBody User user);
@PutMapping("/users/{id}")
User updateUser(@PathVariable("id") Long id, @RequestBody User user);
@DeleteMapping("/users/{id}")
void deleteUser(@PathVariable("id") Long id);
}
@Configuration
@EnableFeignClients
public class FeignConfig {
// Configuration globale de Feign
}
@Component
public class UserServiceFallback implements UserServiceClient {
private static final Logger logger = LoggerFactory.getLogger(UserServiceFallback.class);
@Override
public User getUser(Long id) {
logger.warn("Fallback for getUser: " + id);
return new User(id, "Fallback User", "fallback@example.com");
}
@Override
public List<User> getAllUsers() {
logger.warn("Fallback for getAllUsers");
return Collections.emptyList();
}
@Override
public User createUser(User user) {
logger.warn("Fallback for createUser");
throw new RuntimeException("User service temporarily unavailable");
}
@Override
public User updateUser(Long id, User user) {
logger.warn("Fallback for updateUser: " + id);
throw new RuntimeException("User service temporarily unavailable");
}
@Override
public void deleteUser(Long id) {
logger.warn("Fallback for deleteUser: " + id);
throw new RuntimeException("User service temporarily unavailable");
}
}
Utilisation du client Feign :
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserServiceClient userServiceClient;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
try {
User user = userServiceClient.getUser(id);
return ResponseEntity.ok(user);
} catch (Exception e) {
logger.error("Error getting user: " + id, e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
}
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
try {
User createdUser = userServiceClient.createUser(user);
return ResponseEntity.ok(createdUser);
} catch (Exception e) {
logger.error("Error creating user", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
}
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
try {
List<User> users = userServiceClient.getAllUsers();
return ResponseEntity.ok(users);
} catch (Exception e) {
logger.error("Error getting all users", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
}
}
}
@Service
public class UserServiceConsumer {
@Autowired
private UserServiceClient userServiceClient;
public User processUser(Long userId) {
// Le load balancing est appliqué automatiquement
User user = userServiceClient.getUser(userId);
// Traitement métier
user.setLastAccessed(new Date());
userServiceClient.updateUser(userId, user);
return user;
}
}
Sécurité et ACL
Configuration de Base des ACL
Activation des ACL dans Consul :
# consul.hcl (serveur)
acl {
enabled = true
default_policy = "deny"
down_policy = "extend-cache"
enable_token_persistence = true
}
# Bootstrap initial
bootstrap_expect = 1
server = true
# Pour les clients
acl {
enabled = true
default_policy = "deny"
down_policy = "fail"
}
# Configuration du token ACL
acl_tokens {
master = "your-master-token"
agent = "your-agent-token"
default = "your-default-token"
}
Bootstrap des ACL :
# Démarrage du serveur avec bootstrap
consul agent -server -bootstrap-expect=1 -data-dir=C:\consul\data -config-dir=C:\consul\config
# Génération du token bootstrap
consul acl bootstrap
# Réponse :
AccessorID: b7a3f4c2-1d8e-4f2a-9b3c-1a2d3e4f5g6h
SecretID: 12345678-1234-1234-1234-123456789012
Description: Bootstrap Token (Global Management)
Local: false
Create Time: 2023-01-01 12:00:00 +0000 UTC
Policies:
00000000-0000-0000-0000-000000000001 - global-management
# Configuration du token
SET CONSUL_HTTP_TOKEN=12345678-1234-1234-1234-123456789012
Création des politiques :
# Création d'une politique pour les services
consul acl policy create -name "service-policy" -description "Service read/write policy" -rules @service-policy.hcl
# service-policy.hcl
service_prefix "web" {
policy = "write"
}
service_prefix "api" {
policy = "read"
}
node_prefix "" {
policy = "read"
}
# Création d'une politique pour le KV store
consul acl policy create -name "kv-policy" -description "KV store policy" -rules @kv-policy.hcl
# kv-policy.hcl
key_prefix "config/app/" {
policy = "write"
}
key_prefix "config/shared/" {
policy = "read"
}
# Création d'une politique pour les agents
consul acl policy create -name "agent-policy" -description "Agent policy" -rules @agent-policy.hcl
# agent-policy.hcl
agent_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}
Gestion des Tokens
Création de tokens :
# Création d'un token pour un service web
consul acl token create -description "Web service token" -policy-name "service-policy" -policy-name "kv-policy"
# Réponse :
AccessorID: c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i
SecretID: 98765432-9876-9876-9876-987654321098
Description: Web service token
Local: false
Create Time: 2023-01-01 12:05:00 +0000 UTC
Policies:
87654321-8765-8765-8765-876543210987 - service-policy
12345678-1234-1234-1234-123456789012 - kv-policy
# Création d'un token local (pour un agent spécifique)
consul acl token create -description "Node agent token" -policy-name "agent-policy" -local
# Création d'un token avec expiration
consul acl token create -description "Temporary token" -policy-name "service-policy" -expiration-ttl=1h
Configuration des agents avec tokens :
# Configuration du serveur
# C:\consul\config\server-acl.json
{
"acl": {
"tokens": {
"agent": "98765432-9876-9876-9876-987654321098",
"default": "98765432-9876-9876-9876-987654321098"
}
}
}
# Configuration du client
# C:\consul\config\client-acl.json
{
"acl": {
"tokens": {
"default": "c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i"
}
}
}
# Ou via variables d'environnement
SET CONSUL_HTTP_TOKEN=c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i
Gestion des tokens :
# Liste des tokens
consul acl token list
# Récupération d'un token spécifique
consul acl token read -id c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i
# Mise à jour d'un token
consul acl token update -id c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i -description "Updated web service token" -policy-name "service-policy"
# Suppression d'un token
consul acl token delete -id c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i
# Clone d'un token
consul acl token clone -id c8b4g5d3-2e9f-5g3b-0c4d-2b3e5f6g7h8i -description "Cloned token"
Sécurité Avancée
Configuration TLS :
# Génération des certificats
# openssl req -newkey rsa:2048 -nodes -keyout consul-agent-key.pem -x509 -days 365 -out consul-agent.pem
# Configuration TLS
# C:\consul\config\tls.json
{
"ca_file": "C:/consul/certs/consul-agent-ca.pem",
"cert_file": "C:/consul/certs/consul-agent.pem",
"key_file": "C:/consul/certs/consul-agent-key.pem",
"verify_incoming": true,
"verify_outgoing": true,
"verify_server_hostname": true,
"auto_encrypt": {
"allow_tls": true
}
}
# Activation HTTPS
ports {
https = 8501
http = -1 # Désactiver HTTP
}
# Configuration dans application.yml
spring:
cloud:
consul:
scheme: https
host: localhost
port: 8501
tls:
key-store: classpath:keystore.jks
key-store-password: changeit
key-password: changeit
Configuration mTLS :
# Configuration mTLS pour Connect
connect {
enabled = true
ca_provider = "consul"
ca_config {
cluster_id = "12345678-1234-1234-1234-123456789012"
}
}
# Configuration des intentions
# consul config write intention.hcl
# intention.hcl
Kind = "service-intentions"
Name = "web"
Sources = [
{
Name = "api"
Action = "allow"
},
{
Name = "*"
Action = "deny"
}
]
# Configuration dans Spring Boot
spring:
cloud:
consul:
connect:
enabled: true
certificate:
path: C:/consul/certs/
key-store: client.jks
key-store-password: password
key-password: password
Audit et logging :
# Configuration du logging
# C:\consul\config\logging.json
{
"log_level": "INFO",
"log_file": "C:/consul/logs/consul.log",
"log_rotate_bytes": 52428800,
"log_rotate_duration": "24h",
"log_rotate_max_files": 10,
"enable_syslog": true,
"syslog_facility": "LOCAL0"
}
# Configuration de l'audit
{
"audit": {
"enabled": true,
"sink": {
"type": "file",
"path": "C:/consul/logs/audit.log",
"mode": "755"
}
}
}
# Surveillance des accès
consul acl token list -format=json | jq '.[] | {Description, CreateTime, Policies}'
# Configuration de sécurité dans Spring Boot
spring:
cloud:
consul:
discovery:
acl-token: ${CONSUL_ACL_TOKEN:}
scheme: https
config:
acl-token: ${CONSUL_ACL_TOKEN:}
scheme: https
# Variables d'environnement
SET CONSUL_ACL_TOKEN=your-secure-token
SET SPRING_CLOUD_CONSUL_DISCOVERY_SCHEME=https
SET SPRING_CLOUD_CONSUL_CONFIG_SCHEME=https
Monitoring et Métriques
Configuration des Métriques
Activation des métriques :
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
metrics:
enabled: true
prometheus:
enabled: true
metrics:
enable:
consul: true
distribution:
percentiles-histogram:
consul: true
percentiles:
consul: 0.5,0.9,0.95,0.99
sla:
consul: 10ms,50ms,100ms,500ms
spring:
cloud:
consul:
discovery:
metrics:
enabled: true
config:
metrics:
enabled: true
# Configuration des métriques spécifiques Consul
management:
metrics:
enable:
spring.cloud.consul: true
distribution:
percentiles-histogram:
spring.cloud.consul.requests: true
percentiles:
spring.cloud.consul.requests: 0.5,0.9,0.95,0.99
sla:
spring.cloud.consul.requests: 10ms,50ms,100ms,500ms
Métriques spécifiques Consul :
# Métriques disponibles
spring.cloud.consul.requests
spring.cloud.consul.discovery.requests
spring.cloud.consul.config.requests
spring.cloud.consul.catalog.requests
spring.cloud.consul.session.requests
# Exemple de configuration
management:
metrics:
enable:
spring.cloud.consul: true
distribution:
percentiles-histogram:
spring.cloud.consul.requests: true
Intégration avec Prometheus et Grafana
Configuration Prometheus :
# prometheus.yml
scrape_configs:
- job_name: 'consul-server'
static_configs:
- targets: ['localhost:8500']
metrics_path: '/v1/metrics'
params:
format: ['prometheus']
scrape_interval: 15s
- job_name: 'spring-boot-apps'
static_configs:
- targets: ['localhost:8080'] # User service
- targets: ['localhost:8081'] # Order service
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
- job_name: 'consul-clients'
consul_sd_configs:
- server: 'localhost:8500'
services: []
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
Dashboard Grafana :
# Panel 1: Taux de requêtes
rate(spring_cloud_consul_requests_seconds_count[5m])
# Panel 2: Latence 95th percentile
histogram_quantile(0.95, sum(rate(spring_cloud_consul_requests_seconds_bucket[5m])) by (le))
# Panel 3: Taux d'erreurs
rate(spring_cloud_consul_requests_seconds_count{status="5xx"}[5m]) /
rate(spring_cloud_consul_requests_seconds_count[5m]) * 100
# Panel 4: Utilisation mémoire
jvm_memory_used_bytes{area="heap"}
# Panel 5: Nombre de services enregistrés
count(consul_catalog_service_node{service="user-service"})
Alertes Prometheus :
# Alerte pour latence élevée
groups:
- name: consul-alerts
rules:
- alert: HighConsulLatency
expr: histogram_quantile(0.95, sum(rate(spring_cloud_consul_requests_seconds_bucket[5m])) by (le)) > 1
for: 1m
labels:
severity: warning
annotations:
summary: "High Consul latency"
description: "Consul latency above 1 second"
- alert: ConsulServiceDown
expr: up{job="consul-server"} == 0
for: 30s
labels:
severity: critical
annotations:
summary: "Consul server is down"
description: "Consul server is not responding"
Événements et Logging
Configuration du logging :
# application.yml
logging:
level:
org.springframework.cloud.consul: DEBUG
com.ecwid.consul: DEBUG
org.springframework.cloud.loadbalancer: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/consul-client.log
# Configuration avancée du logging
logging:
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/consul-client.log
max-size: 10MB
max-history: 30
logback:
rollingpolicy:
max-file-size: 10MB
total-size-cap: 1GB
max-history: 30
Écoute des événements personnalisés :
@Component
public class ConsulEventListener {
private static final Logger logger = LoggerFactory.getLogger(ConsulEventListener.class);
@EventListener
public void onHeartbeatEvent(HeartbeatEvent event) {
logger.debug("Consul heartbeat event: {}", event.getValue());
}
@EventListener
public void onInstanceRegisteredEvent(InstanceRegisteredEvent event) {
logger.info("Service instance registered: {}", event.getInstance().getServiceId());
}
@EventListener
public void onInstanceDeregisteredEvent(InstanceDeregisteredEvent event) {
logger.info("Service instance deregistered: {}", event.getInstance().getServiceId());
}
@EventListener
public void onRefreshEvent(RefreshRemoteApplicationEvent event) {
logger.info("Configuration refresh event: {}", event.getDestinationService());
}
@EventListener
public void onEnvironmentChangeEvent(EnvironmentChangeEvent event) {
logger.info("Environment changed: {}", event.getKeys());
}
}
Patterns d'Architecture
Pattern de Service Mesh
Service Mesh avec Consul Connect
Connect fournit une solution de service mesh intégrée avec mTLS, autorisations et observabilité.
Configuration du service mesh :
# service-defaults.hcl
Kind = "service-defaults"
Name = "web"
Protocol = "http"
# Appliquer la configuration
consul config write service-defaults.hcl
# service-intentions.hcl
Kind = "service-intentions"
Name = "web"
Sources = [
{
Name = "api"
Action = "allow"
},
{
Name = "frontend"
Action = "allow"
},
{
Name = "*"
Action = "deny"
}
]
# Appliquer les intentions
consul config write service-intentions.hcl
Configuration des proxies sidecar :
# web-service.hcl
service {
name = "web"
id = "web-1"
port = 8080
connect {
sidecar_service {
proxy {
upstreams = [
{
destination_name = "api"
local_bind_port = 9102
}
]
config {
protocol = "http"
envoy_access_log_path = "C:/consul/logs/envoy-access-web.log"
envoy_detailed_metrics_enabled = true
}
}
}
}
check {
name = "Web HTTP Check"
http = "http://localhost:8080/health"
interval = "10s"
}
}
Communication sécurisée :
# Dans l'application web
# Appel à l'API via le proxy sidecar
curl http://localhost:9102/api/users
# Le trafic est automatiquement :
# 1. Chiffré avec mTLS
# 2. Authentifié avec les intentions
# 3. Observé avec les métriques
# 4. Routé vers le service api
# Configuration du service api
service {
name = "api"
port = 8081
connect {
sidecar_service {}
}
check {
name = "API HTTP Check"
http = "http://localhost:8081/health"
interval = "10s"
}
}
Pattern de Configuration Distribuée
Gestion de Configuration avec KV Store
Utilisation du KV store pour la configuration distribuée et l'actualisation dynamique.
Structure de configuration :
# Structure du KV store
config/
├── application/
│ ├── database/
│ │ ├── host = "localhost"
│ │ ├── port = "5432"
│ │ └── pool-size = "10"
│ ├── cache/
│ │ ├── redis-host = "localhost"
│ │ ├── redis-port = "6379"
│ │ └── ttl = "300"
│ └── logging/
│ ├── level = "INFO"
│ └── file = "C:/logs/app.log"
├── services/
│ ├── web/
│ │ ├── timeout = "5000"
│ │ └── retries = "3"
│ └── api/
│ ├── timeout = "10000"
│ └── retries = "5"
└── environments/
├── dev/
│ └── database/
│ └── host = "dev-db.local"
└── prod/
└── database/
└── host = "prod-db.company.com"
Application avec configuration dynamique :
# Application Spring Boot avec Consul Config
# bootstrap.yml
spring:
application:
name: web-service
cloud:
consul:
host: localhost
port: 8500
config:
enabled: true
format: YAML
prefix: config
default-context: application
profile-separator: ','
data-key: data
discovery:
enabled: true
# Service de configuration
@Service
@RefreshScope
public class DynamicConfigurationService {
@Value("${config.database.host:localhost}")
private String databaseHost;
@Value("${config.database.port:5432}")
private String databasePort;
@Value("${config.cache.ttl:300}")
private int cacheTtl;
public DatabaseConfig getDatabaseConfig() {
return new DatabaseConfig(databaseHost, databasePort);
}
public int getCacheTtl() {
return cacheTtl;
}
}
# Contrôleur de refresh
@RestController
public class ConfigRefreshController {
@Autowired
private RefreshScope refreshScope;
@PostMapping("/refresh-config")
public ResponseEntity<String> refreshConfiguration() {
refreshScope.refreshAll();
return ResponseEntity.ok("Configuration refreshed");
}
}
Scripts de gestion de configuration :
# config-manager.ps1
param(
[string]$ConsulHost = "localhost:8500",
[string]$ConfigPath = "C:\consul\config"
)
# Fonction pour mettre à jour la configuration
function Update-Config {
param([string]$Key, [string]$Value)
$uri = "http://$ConsulHost/v1/kv/config/$Key"
Invoke-RestMethod -Uri $uri -Method Put -Body $Value
Write-Host "Updated $Key = $Value"
}
# Fonction pour lire la configuration
function Get-Config {
param([string]$Key)
$uri = "http://$ConsulHost/v1/kv/config/$Key?raw"
$value = Invoke-RestMethod -Uri $uri -Method Get
Write-Host "$Key = $value"
return $value
}
# Fonction pour déployer la configuration
function Deploy-Config {
param([string]$Environment, [string]$Service)
$configFile = Join-Path $ConfigPath "$Environment\$Service.yaml"
if (Test-Path $configFile) {
$config = Get-Content $configFile | ConvertFrom-Yaml
foreach ($item in $config.GetEnumerator()) {
$key = "config/$Service/$($item.Key)"
$value = $item.Value
Update-Config -Key $key -Value $value
}
}
}
# Exemple d'utilisation
switch ($args[0]) {
"update" { Update-Config -Key $args[1] -Value $args[2] }
"get" { Get-Config -Key $args[1] }
"deploy" { Deploy-Config -Environment $args[1] -Service $args[2] }
default { Write-Host "Usage: config-manager.ps1 {update|get|deploy} [args...]" }
}
Pattern de Discovery et Load Balancing
Load Balancing Intelligent avec Tags et Meta
Utilisation des tags et métadonnées pour le routage intelligent et le load balancing.
Configuration des services avec tags :
# web-service-primary.hcl
service {
name = "web"
id = "web-primary-1"
tags = ["primary", "v1", "production"]
meta = {
version = "1.2.3"
environment = "production"
zone = "us-east-1a"
weight = "80"
}
port = 8080
address = "192.168.1.100"
check {
name = "Web Primary Health"
http = "http://192.168.1.100:8080/health"
interval = "10s"
}
}
# web-service-secondary.hcl
service {
name = "web"
id = "web-secondary-1"
tags = ["secondary", "v1", "production"]
meta = {
version = "1.2.3"
environment = "production"
zone = "us-east-1b"
weight = "20"
}
port = 8080
address = "192.168.1.101"
check {
name = "Web Secondary Health"
http = "http://192.168.1.101:8080/health"
interval = "10s"
}
}
Client avec load balancing intelligent :
@Service
public class IntelligentServiceClient {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private LoadBalancerClient loadBalancerClient;
public List<ServiceInstance> getAvailableServices() {
return discoveryClient.getInstances("web");
}
public String callPrimaryService() {
// Trouver les services avec le tag "primary"
List<ServiceInstance> primaryInstances = discoveryClient.getInstances("web")
.stream()
.filter(instance -> instance.getMetadata().containsKey("tags") &&
instance.getMetadata().get("tags").contains("primary"))
.filter(instance -> "passing".equals(getServiceHealth(instance)))
.collect(Collectors.toList());
if (!primaryInstances.isEmpty()) {
ServiceInstance selectedInstance = weightedSelection(primaryInstances);
return callService(selectedInstance);
}
throw new RuntimeException("No primary service instances available");
}
private ServiceInstance weightedSelection(List<ServiceInstance> instances) {
// Implémentation du load balancing pondéré
int totalWeight = instances.stream()
.mapToInt(instance -> Integer.parseInt(
instance.getMetadata().getOrDefault("weight", "1")))
.sum();
int randomWeight = new Random().nextInt(totalWeight);
int currentWeight = 0;
for (ServiceInstance instance : instances) {
currentWeight += Integer.parseInt(
instance.getMetadata().getOrDefault("weight", "1"));
if (randomWeight < currentWeight) {
return instance;
}
}
return instances.get(0);
}
private String callService(ServiceInstance instance) {
String url = "http://" + instance.getHost() + ":" + instance.getPort();
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(url, String.class);
}
private String getServiceHealth(ServiceInstance instance) {
// Implémentation de vérification de santé
try {
String healthUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/health";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(healthUrl, String.class);
return response.getStatusCode().is2xxSuccessful() ? "passing" : "critical";
} catch (Exception e) {
return "critical";
}
}
}
Load balancing avec Consul Template :
# nginx-load-balancer.ctmpl
upstream web_backend {
# Services primaires (80% du trafic)
{{range service "web" "passing" | filter "Tag" "primary"}}
server {{.Address}}:{{.Port}} weight=4 max_fails=3 fail_timeout=30s;
{{end}}
# Services secondaires (20% du trafic)
{{range service "web" "passing" | filter "Tag" "secondary"}}
server {{.Address}}:{{.Port}} weight=1 max_fails=3 fail_timeout=30s;
{{end}}
# Configuration avancée
least_conn;
keepalive 32;
}
server {
listen 80;
server_name web.example.com;
location / {
proxy_pass http://web_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Health checks
{{range service "web" "passing"}}
# Service {{.Service}} on {{.Node}}:{{.Address}}:{{.Port}} - Status: {{.Checks.AggregatedStatus}}
{{end}}
}
# Monitoring endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Bonnes Pratiques Consul et Spring Boot
Configuration et Déploiement
Principes Fondamentaux
- Configuration externalisée : Utilisez des fichiers de configuration ou Config Server
- Environnements séparés : Différentes configurations pour dev, test, prod
- Versioning : Versionnez vos configurations avec le code
- Documentation : Documentez clairement les stratégies de résilience
Configuration Recommandée
# Production
spring:
cloud:
consul:
discovery:
health-check-interval: 15s
health-check-timeout: 3s
health-check-critical-timeout: 1m
config:
watch:
enabled: true
delay: 1000
loadbalancer:
retry:
enabled: true
cache:
enabled: true
ttl: 30s
Performance et Optimisation
Optimisations Clés
- Configuration appropriée : Ajustez les seuils selon vos besoins spécifiques
- Timeouts raisonnables : Ni trop courts (erreurs prématurées) ni trop longs (ressources bloquées)
- Retry stratégique : Retry seulement sur les erreurs récupérables
- Monitoring actif : Surveillez les métriques pour ajuster les configurations
Paramètres de Performance Optimisés
| Paramètre | Valeur Recommandée | Raison |
|---|---|---|
| health-check-interval | 15s | Équilibre entre réactivité et charge |
| health-check-timeout | 3s | Détection rapide des pannes réseau |
| retry.max-attempts | 3 tentatives | Évite la surcharge sans abandon prématuré |
| cache.ttl | 30s | Performance optimale avec fraîcheur des données |
Sécurité et Résilience
Considérations de Sécurité
- Validation des entrées : Validez toujours les données avant de les traiter
- Chiffrement des communications : Utilisez HTTPS entre les services
- Rate limiting : Protégez contre les abus et DDoS
- Monitoring des accès : Surveillez les patterns d'accès inhabituels
Pièges Courants à Éviter
- Timeouts trop courts : Causent des erreurs prématurées
- Trop de retries : Amplifient les problèmes de performance
- Pas de health checking : Envoient des requêtes à des services défaillants
- Configuration statique : Difficile d'adapter aux conditions changeantes
- Pas de monitoring : Difficile de diagnostiquer les problèmes
Tests et Validation
Stratégies de Test
- Tests unitaires : Testez les configurations de Consul
- Tests d'intégration : Vérifiez l'interaction avec Consul
- Tests de charge : Simulez des scénarios de haute charge
- Tests de chaos : Simulez des pannes de services
- Tests de performance : Mesurez l'impact sur la latence
// Exemple de test d'intégration
@SpringBootTest
class ConsulIntegrationTest {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private ConfigurableApplicationContext context;
@Test
void testServiceRegistration() {
// Attendre que le service soit enregistré
await().atMost(30, SECONDS).untilAsserted(() -> {
List<String> services = discoveryClient.getServices();
assertThat(services).contains("user-service");
});
}
@Test
void testConfigurationLoading() {
// Vérifier que la configuration est chargée
String appName = context.getEnvironment().getProperty("app.name");
assertThat(appName).isNotNull();
assertThat(appName).isEqualTo("User Service");
}
@Test
void testHealthCheck() {
// Vérifier que le health check fonctionne
HealthEndpoint healthEndpoint = context.getBean(HealthEndpoint.class);
Health health = healthEndpoint.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
}
}
Résumé des Bonnes Pratiques
En suivant ces bonnes pratiques, vous obtiendrez :
- Haute disponibilité : Résilience face aux pannes
- Bonne performance : Distribution optimale de la charge
- Facilité de maintenance : Configuration claire et documentée
- Sécurité : Communications sécurisées et contrôlées
- Observabilité : Monitoring et alerting efficaces