HashiCorp Consul
Service Discovery et Configuration pour microservices
Introduction à HashiCorp Consul
Qu'est-ce que HashiCorp Consul ?
HashiCorp Consul est une solution de service mesh et de service discovery développée par HashiCorp. Elle permet de découvrir, configurer et segmenter les services dans un environnement distribué, offrant des fonctionnalités de découverte de services, de configuration clé-valeur, de segmentation réseau et de sécurité.
graph LR
A[Client
Service A] --> B[Consul
Agent] C[Service B] --> D[Consul
Agent] E[Service C] --> F[Consul
Agent] B --> G[Consul
Server] D --> G F --> G style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#4CAF50,stroke:#388E3C style E fill:#9C27B0,stroke:#4A148C style F fill:#4CAF50,stroke:#388E3C style G fill:#FF5722,stroke:#E64A19
Service A] --> B[Consul
Agent] C[Service B] --> D[Consul
Agent] E[Service C] --> F[Consul
Agent] B --> G[Consul
Server] D --> G F --> G style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#4CAF50,stroke:#388E3C style E fill:#9C27B0,stroke:#4A148C style F fill:#4CAF50,stroke:#388E3C style G fill:#FF5722,stroke:#E64A19
Fonctionnalités principales de Consul :
- Service Discovery : Découverte dynamique des services
- Configuration KV : Stockage clé-valeur distribué
- Health Checking : Surveillance de la santé des services
- Segmentation : Contrôle d'accès et sécurité réseau
- Service Mesh : Gestion du trafic et observabilité
- Multi-Datacenter : Support multi-datacenter
Architecture Consul
Structure de l'architecture
graph TD
A[Client Applications] --> B[Consul Agents Clients]
B --> C[Consul Servers Cluster]
C --> D[Consul Servers Cluster]
C --> E[Consul Servers Cluster]
F[External Services] --> G[Consul Agents Clients]
G --> C
subgraph Datacenter 1
B
C
D
E
end
subgraph Datacenter 2
G
end
style A fill:#FF9800,stroke:#E65100
style B fill:#4CAF50,stroke:#388E3C
style C fill:#2196F3,stroke:#0D47A1
style D fill:#2196F3,stroke:#0D47A1
style E fill:#2196F3,stroke:#0D47A1
style F fill:#9C27B0,stroke:#4A148C
style G fill:#4CAF50,stroke:#388E3C
Composants de Consul :
- Agents Consul : Processus s'exécutant sur chaque nœud
- Serveurs Consul : Maintiennent l'état du cluster
- Clients Consul : Agents légers qui redirigent vers les serveurs
- Services : Applications enregistrées dans Consul
- Catalogue : Registre des services et nœuds
- Key-Value Store : Stockage de configuration distribué
Installation et Configuration de Consul
1 Installation de Consul
Téléchargement et installation :
Sur Linux/macOS :
# Télécharger Consul
wget https://releases.hashicorp.com/consul/1.16.1/consul_1.16.1_linux_amd64.zip
# Décompresser
unzip consul_1.16.1_linux_amd64.zip
# Déplacer dans le PATH
sudo mv consul /usr/local/bin/
# Vérifier l'installation
consul --version
Avec Docker :
# Démarrer un serveur Consul seul (développement)
docker run -d --name=consul -p 8500:8500 -p 8600:8600/udp consul agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0
# Accéder à l'interface web
# http://localhost:8500
# Démarrer un agent client
docker run -d --name=consul-client consul agent -node=client-1 -join=<server-ip>
2 Configuration du Serveur Consul
Fichier de configuration serveur :
consul-config/server.json
{
"server": true,
"node_name": "consul-server-1",
"datacenter": "dc1",
"data_dir": "/opt/consul/data",
"bind_addr": "0.0.0.0",
"client_addr": "0.0.0.0",
"advertise_addr": "192.168.1.100",
"bootstrap_expect": 1,
"ui_config": {
"enabled": true
},
"ports": {
"http": 8500,
"https": 8501,
"dns": 8600,
"serf_lan": 8301,
"serf_wan": 8302,
"server": 8300
},
"connect": {
"enabled": true
},
"enable_central_service_config": true,
"acl": {
"enabled": false,
"default_policy": "allow",
"down_policy": "extend-cache"
}
}
Démarrage du serveur :
# Démarrer le serveur Consul
consul agent -config-file=server.json
# Ou avec plusieurs fichiers de configuration
consul agent -config-dir=./consul-config
# Démarrage en arrière-plan
nohup consul agent -config-file=server.json > consul.log 2>&1 &
3 Configuration du Client Consul
Fichier de configuration client :
consul-config/client.json
{
"server": false,
"node_name": "consul-client-1",
"datacenter": "dc1",
"data_dir": "/opt/consul/data",
"bind_addr": "0.0.0.0",
"client_addr": "127.0.0.1",
"advertise_addr": "192.168.1.101",
"retry_join": ["192.168.1.100"],
"ports": {
"dns": 8600,
"http": 8500,
"https": -1,
"serf_lan": 8301
},
"enable_local_script_checks": true,
"leave_on_terminate": true
}
Démarrage du client :
# Démarrer le client Consul
consul agent -config-file=client.json
# Rejoindre un cluster existant
consul agent -config-file=client.json -join=192.168.1.100
# Vérifier le statut
consul members
Services Backend avec Consul
1 Service A - Utilisateurs
Structure du projet :
serviceA/
├── pom.xml
└── src/main/
├── java/com/example/users/
│ ├── UsersMicroserviceApplication.java
│ ├── controller/
│ │ └── UserController.java
│ ├── model/
│ │ └── User.java
│ └── config/
│ └── ConsulConfig.java
└── resources/
└── application.properties
serviceA/pom.xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Consul -->
<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>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
serviceA/src/main/resources/bootstrap.properties
# === CONFIGURATION CONSUL ===
spring.application.name=service-a
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.service-name=service-a
spring.cloud.consul.discovery.instance-id=service-a-${server.port}
spring.cloud.consul.discovery.prefer-ip-address=true
# === CONFIGURATION KV STORE ===
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.default-context=application
spring.cloud.consul.config.data-key=data
spring.cloud.consul.config.format=KEY_VALUE
# === HEALTH CHECK ===
spring.cloud.consul.discovery.health-check-path=/actuator/health
spring.cloud.consul.discovery.health-check-interval=15s
spring.cloud.consul.discovery.health-check-timeout=5s
serviceA/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8080
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
serviceA/src/main/java/com/example/users/model/User.java
package com.example.users.model;
public class User {
private Long id;
private String name;
private String email;
private String department;
//constructors
// Getters et Setters
serviceA/src/main/java/com/example/users/controller/UserController.java
package com.example.users.controller;
import com.example.users.model.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
@Value("${server.port}")
private int serverPort;
@GetMapping
public List<User> getAllUsers() {
return Arrays.asList(
new User(1L, "John Doe", "john@example.com", "IT"),
new User(2L, "Jane Smith", "jane@example.com", "HR")
);
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return new User(id, "User " + id, "user" + id + "@example.com", "Department " + id);
}
@PostMapping
public User createUser(@RequestBody User user) {
user.setId(System.currentTimeMillis());
return user;
}
@GetMapping("/info")
public String getServiceInfo() {
return "Service A (Users) running on port " + serverPort;
}
}
serviceA/src/main/java/com/example/users/UsersMicroserviceApplication.java
package com.example.users;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // Active la découverte de services via Consul
public class UsersMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(UsersMicroserviceApplication.class, args);
}
}
2 Service B - Produits
serviceB/src/main/resources/bootstrap.properties
# === CONFIGURATION CONSUL ===
spring.application.name=service-b
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.service-name=service-b
spring.cloud.consul.discovery.instance-id=service-b-${server.port}
spring.cloud.consul.discovery.prefer-ip-address=true
# === CONFIGURATION KV STORE ===
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.default-context=application
spring.cloud.consul.config.data-key=data
spring.cloud.consul.config.format=KEY_VALUE
# === HEALTH CHECK ===
spring.cloud.consul.discovery.health-check-path=/actuator/health
spring.cloud.consul.discovery.health-check-interval=15s
spring.cloud.consul.discovery.health-check-timeout=5s
serviceB/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8081
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
serviceB/src/main/java/com/example/products/model/Product.java
package com.example.products.model;
public class Product {
private Long id;
private String name;
private String description;
private Double price;
public Product() {}
public Product(Long id, String name, String description, Double price) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
serviceB/src/main/java/com/example/products/controller/ProductController.java
package com.example.products.controller;
import com.example.products.model.Product;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "*")
public class ProductController {
@Value("${server.port}")
private int serverPort;
@GetMapping
public List<Product> getAllProducts() {
return Arrays.asList(
new Product(1L, "Laptop", "High-performance laptop", 999.99),
new Product(2L, "Smartphone", "Latest smartphone model", 699.99)
);
}
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return new Product(id, "Product " + id, "Description " + id, 99.99 + id);
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
product.setId(System.currentTimeMillis());
return product;
}
@GetMapping("/info")
public String getServiceInfo() {
return "Service B (Products) running on port " + serverPort;
}
}
3 Service C - Commandes
serviceC/src/main/resources/bootstrap.properties
# === CONFIGURATION CONSUL ===
spring.application.name=service-c
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.service-name=service-c
spring.cloud.consul.discovery.instance-id=service-c-${server.port}
spring.cloud.consul.discovery.prefer-ip-address=true
# === CONFIGURATION KV STORE ===
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.default-context=application
spring.cloud.consul.config.data-key=data
spring.cloud.consul.config.format=KEY_VALUE
# === HEALTH CHECK ===
spring.cloud.consul.discovery.health-check-path=/actuator/health
spring.cloud.consul.discovery.health-check-interval=15s
spring.cloud.consul.discovery.health-check-timeout=5s
serviceC/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8082
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
serviceC/src/main/java/com/example/orders/model/Order.java
package com.example.orders.model;
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer quantity;
private Double totalAmount;
//constructors
// Getters et Setters
serviceC/src/main/java/com/example/orders/controller/OrderController.java
package com.example.orders.controller;
import com.example.orders.model.Order;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
@CrossOrigin(origins = "*")
public class OrderController {
@Value("${server.port}")
private int serverPort;
@GetMapping
public List<Order> getAllOrders() {
return Arrays.asList(
new Order(1L, 1L, 1L, 2, 1999.98),
new Order(2L, 2L, 2L, 1, 699.99)
);
}
@GetMapping("/{id}")
public Order getOrderById(@PathVariable Long id) {
return new Order(id, 1L, 1L, 1, 999.99);
}
@PostMapping
public Order createOrder(@RequestBody Order order) {
order.setId(System.currentTimeMillis());
return order;
}
@GetMapping("/info")
public String getServiceInfo() {
return "Service C (Orders) running on port " + serverPort;
}
}
Client avec Consul Discovery
1 Configuration du Client
Dépendances Maven :
client-service/pom.xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Consul Discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- RestTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Configuration Consul :
client-service/src/main/resources/bootstrap.properties
# === CONFIGURATION CONSUL ===
spring.application.name=client-service
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.service-name=client-service
spring.cloud.consul.discovery.instance-id=client-service-${server.port}
spring.cloud.consul.discovery.prefer-ip-address=true
# === HEALTH CHECK ===
spring.cloud.consul.discovery.health-check-path=/actuator/health
spring.cloud.consul.discovery.health-check-interval=15s
spring.cloud.consul.discovery.health-check-timeout=5s
client-service/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8083
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
# === CONFIGURATION FEIGN ===
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=10000
2 Configuration RestTemplate avec LoadBalancer
client-service/src/main/java/com/example/client/config/ConsulConfig.java
package com.example.client.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ConsulConfig {
@LoadBalanced // Active le load balancing via Consul
@Bean
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
3 Clients Feign avec Consul
client-service/src/main/java/com/example/client/feign/UserServiceClient.java
package com.example.client.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient(
name = "service-a", // Nom du service dans Consul
fallback = UserServiceClientFallback.class
)
public interface UserServiceClient {
@GetMapping("/api/users")
List<Map<String, Object>> getAllUsers();
@GetMapping("/api/users/{id}")
Map<String, Object> getUserById(@PathVariable("id") Long id);
@PostMapping("/api/users")
Map<String, Object> createUser(@RequestBody Map<String, Object> user);
@GetMapping("/api/users/info")
String getServiceInfo();
}
client-service/src/main/java/com/example/client/feign/UserServiceClientFallback.java
package com.example.client.feign;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class UserServiceClientFallback implements UserServiceClient {
@Override
public List<Map<String, Object>> getAllUsers() {
Map<String, Object> fallbackUser = new HashMap<>();
fallbackUser.put("id", 0);
fallbackUser.put("name", "Fallback User");
fallbackUser.put("email", "fallback@example.com");
fallbackUser.put("department", "FALLBACK");
return Arrays.asList(fallbackUser);
}
@Override
public Map<String, Object> getUserById(Long id) {
Map<String, Object> fallbackUser = new HashMap<>();
fallbackUser.put("id", id);
fallbackUser.put("name", "Fallback User " + id);
fallbackUser.put("email", "fallback" + id + "@example.com");
fallbackUser.put("department", "FALLBACK");
return fallbackUser;
}
@Override
public Map<String, Object> createUser(Map<String, Object> user) {
Map<String, Object> createdUser = new HashMap<>(user);
createdUser.put("id", System.currentTimeMillis());
return createdUser;
}
@Override
public String getServiceInfo() {
return "Fallback: Service A indisponible";
}
}
client-service/src/main/java/com/example/client/feign/ProductServiceClient.java
package com.example.client.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient(
name = "service-b",
fallback = ProductServiceClientFallback.class
)
public interface ProductServiceClient {
@GetMapping("/api/products")
List<Map<String, Object>> getAllProducts();
@GetMapping("/api/products/{id}")
Map<String, Object> getProductById(@PathVariable("id") Long id);
@PostMapping("/api/products")
Map<String, Object> createProduct(@RequestBody Map<String, Object> product);
@GetMapping("/api/products/info")
String getServiceInfo();
}
4 Services Client
client-service/src/main/java/com/example/client/service/ConsulClientService.java
package com.example.client.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Service
public class ConsulClientService {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
@Qualifier("loadBalancedRestTemplate")
private RestTemplate loadBalancedRestTemplate;
@Autowired
private RestTemplate restTemplate;
// === DISCOVERY CLIENT METHODS ===
public List<String> getAllServices() {
return discoveryClient.getServices();
}
public List<ServiceInstance> getServiceInstances(String serviceName) {
return discoveryClient.getInstances(serviceName);
}
public String getServiceInfo(String serviceName) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
StringBuilder info = new StringBuilder();
info.append("Service: ").append(serviceName).append("\n");
info.append("Instances: ").append(instances.size()).append("\n");
for (ServiceInstance instance : instances) {
info.append(" - ").append(instance.getHost())
.append(":").append(instance.getPort())
.append(" (").append(instance.getInstanceId()).append(")\n");
}
return info.toString();
}
// === LOAD BALANCED REST TEMPLATE METHODS ===
public List<Map<String, Object>> getAllUsers() {
String url = "http://service-a/api/users";
return List.of(loadBalancedRestTemplate.getForObject(url, Map[].class));
}
public Map<String, Object> getUserById(Long id) {
String url = "http://service-a/api/users/" + id;
return loadBalancedRestTemplate.getForObject(url, Map.class);
}
public Map<String, Object> createUser(Map<String, Object> user) {
String url = "http://service-a/api/users";
return loadBalancedRestTemplate.postForObject(url, user, Map.class);
}
public List<Map<String, Object>> getAllProducts() {
String url = "http://service-b/api/products";
return List.of(loadBalancedRestTemplate.getForObject(url, Map[].class));
}
public Map<String, Object> getProductById(Long id) {
String url = "http://service-b/api/products/" + id;
return loadBalancedRestTemplate.getForObject(url, Map.class);
}
public List<Map<String, Object>> getAllOrders() {
String url = "http://service-c/api/orders";
return List.of(loadBalancedRestTemplate.getForObject(url, Map[].class));
}
// === DIRECT REST TEMPLATE METHODS (without load balancing) ===
public String callServiceDirectly(String host, int port, String path) {
String url = "http://" + host + ":" + port + path;
return restTemplate.getForObject(url, String.class);
}
}
5 Application Principale
client-service/src/main/java/com/example/client/ClientServiceApplication.java
package com.example.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient // Active Consul Discovery
@EnableFeignClients // Active OpenFeign
public class ClientServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ClientServiceApplication.class, args);
}
}
Configuration Consul KV Store
1 Utilisation du KV Store
Configuration via l'interface web ou CLI :
Via l'interface web (http://localhost:8500) :
# === CLÉS DE CONFIGURATION ===
# Configuration globale
config/application/data = {"app.name": "my-app", "debug": false}
# Configuration spécifique au service A
config/service-a/data = {"database.url": "jdbc:mysql://db:3306/users", "cache.enabled": true}
# Configuration spécifique au service B
config/service-b/data = {"api.key": "secret-key", "timeout": 5000}
# Configuration spécifique au service C
config/service-c/data = {"notification.email": "orders@example.com", "max-retries": 3}
Via l'API REST :
# === OPÉRATIONS KV STORE ===
# Créer/mettre à jour une clé
curl -X PUT -d 'value1' http://localhost:8500/v1/kv/myapp/key1
# Lire une clé
curl http://localhost:8500/v1/kv/myapp/key1
# Lire une clé avec décodage
curl http://localhost:8500/v1/kv/myapp/key1?raw
# Lister les clés
curl http://localhost:8500/v1/kv/myapp/?keys
# Supprimer une clé
curl -X DELETE http://localhost:8500/v1/kv/myapp/key1
# === CONFIGURATION JSON ===
# Mettre à jour la configuration du service A
curl -X PUT -d '{"database":{"url":"jdbc:mysql://localhost:3306/users","username":"user","password":"pass"}}' \
http://localhost:8500/v1/kv/config/service-a/data
# Mettre à jour la configuration globale
curl -X PUT -d '{"logging":{"level":"INFO"},"features":{"caching":true,"monitoring":false}}' \
http://localhost:8500/v1/kv/config/application/data
2 Configuration Dynamique
serviceA/src/main/java/com/example/users/config/DynamicConfig.java
package com.example.users.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope // Active le rafraîchissement dynamique de la configuration
public class DynamicConfig {
@Value("${app.name:default-app}")
private String appName;
@Value("${database.url:jdbc:h2:mem:testdb}")
private String databaseUrl;
@Value("${cache.enabled:false}")
private boolean cacheEnabled;
@Value("${timeout:30000}")
private int timeout;
// Getters
public String getAppName() {
return appName;
}
public String getDatabaseUrl() {
return databaseUrl;
}
public boolean isCacheEnabled() {
return cacheEnabled;
}
public int getTimeout() {
return timeout;
}
}
serviceA/src/main/java/com/example/users/controller/ConfigController.java
package com.example.users.controller;
import com.example.users.config.DynamicConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/config")
public class ConfigController {
@Autowired
private DynamicConfig dynamicConfig;
@GetMapping
public Map<String, Object> getConfig() {
Map<String, Object> config = new HashMap<>();
config.put("appName", dynamicConfig.getAppName());
config.put("databaseUrl", dynamicConfig.getDatabaseUrl());
config.put("cacheEnabled", dynamicConfig.isCacheEnabled());
config.put("timeout", dynamicConfig.getTimeout());
return config;
}
@GetMapping("/refresh")
public String refreshConfig() {
return "Configuration will be refreshed on next request";
}
}
Health Checks et Monitoring
1 Health Checks Personnalisés
serviceA/src/main/java/com/example/users/health/CustomHealthIndicator.java
package com.example.users.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Vérification personnalisée de la santé du service
boolean isDatabaseUp = checkDatabaseConnection();
boolean isCacheUp = checkCacheConnection();
if (isDatabaseUp && isCacheUp) {
return Health.up()
.withDetail("database", "Connected")
.withDetail("cache", "Available")
.withDetail("service", "Fully operational")
.build();
} else {
return Health.down()
.withDetail("database", isDatabaseUp ? "Connected" : "Disconnected")
.withDetail("cache", isCacheUp ? "Available" : "Unavailable")
.withDetail("service", "Partially operational")
.build();
}
}
private boolean checkDatabaseConnection() {
// Logique de vérification de la base de données
try {
// Simulation de vérification
return Math.random() > 0.1; // 90% de chances d'être up
} catch (Exception e) {
return false;
}
}
private boolean checkCacheConnection() {
// Logique de vérification du cache
try {
// Simulation de vérification
return Math.random() > 0.05; // 95% de chances d'être up
} catch (Exception e) {
return false;
}
}
}
2 Monitoring avec Consul
Via l'interface web Consul :
# === COMMANDES CLI UTILES ===
# Vérifier le statut des membres du cluster
consul members
# Vérifier le statut du leader
consul operator raft list-peers
# Vérifier la santé des services
consul health service service-a
# Vérifier la santé des nœuds
consul health node consul-server-1
# === REQUÊTES API POUR LE MONITORING ===
# Liste des services
curl http://localhost:8500/v1/catalog/services
# Instances d'un service
curl http://localhost:8500/v1/catalog/service/service-a
# Statut de santé d'un service
curl http://localhost:8500/v1/health/service/service-a
# Nœuds du cluster
curl http://localhost:8500/v1/catalog/nodes
# Informations sur un nœud
curl http://localhost:8500/v1/catalog/node/consul-server-1
Sécurité avec Consul
1 Configuration ACL
Activation des ACL :
{
"acl": {
"enabled": true,
"default_policy": "deny",
"down_policy": "extend-cache",
"tokens": {
"master": "b1gs33cr3t"
}
}
}
Création de politiques :
# === CRÉATION DE POLITIQUES ACL ===
# Créer une politique pour les services
consul acl policy create \
-name "service-policy" \
-description "Policy for service discovery" \
-rules 'service_prefix "" { policy = "read" } node_prefix "" { policy = "read" }'
# Créer une politique pour le KV store
consul acl policy create \
-name "kv-policy" \
-description "Policy for key-value store" \
-rules 'key_prefix "config/" { policy = "read" }'
# Créer un token avec les politiques
consul acl token create \
-description "Service token" \
-policy-name service-policy \
-policy-name kv-policy
2 Configuration Sécurisée
serviceA/src/main/resources/bootstrap-secure.properties
# === CONFIGURATION SÉCURISÉE CONSUL ===
spring.application.name=service-a
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
# === ACL TOKEN ===
spring.cloud.consul.config.acl-token=your-service-token-here
spring.cloud.consul.discovery.acl-token=your-service-token-here
# === TLS ===
spring.cloud.consul.tls.enabled=true
spring.cloud.consul.tls.key-store-path=classpath:keystore.jks
spring.cloud.consul.tls.key-store-password=changeit
spring.cloud.consul.tls.certificate-path=classpath:certificate.crt
# === AUTHENTIFICATION ===
spring.cloud.consul.username=consul-user
spring.cloud.consul.password=consul-password
Utilisation et Tests
1 Exemples d'Utilisation
Requêtes et tests :
Tests via curl :
# === TESTS DES SERVICES ===
# Démarrer les services dans l'ordre
# 1. Consul Server
# 2. Service A (port 8080)
# 3. Service B (port 8081)
# 4. Service C (port 8082)
# 5. Client Service (port 8083)
# === REQUÊTES DIRECTES AUX SERVICES ===
# Service A - Utilisateurs
curl -X GET http://localhost:8080/api/users
curl -X GET http://localhost:8080/api/users/1
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Test User","email":"test@example.com","department":"TEST"}'
# Service B - Produits
curl -X GET http://localhost:8081/api/products
curl -X GET http://localhost:8081/api/products/1
# Service C - Commandes
curl -X GET http://localhost:8082/api/orders
curl -X GET http://localhost:8082/api/orders/1
# === REQUÊTES VIA CLIENT SERVICE ===
# Discovery Client
curl -X GET http://localhost:8083/api/discovery/services
curl -X GET http://localhost:8083/api/discovery/instances/service-a
# Load Balanced RestTemplate
curl -X GET http://localhost:8083/api/users
curl -X GET http://localhost:8083/api/users/1
curl -X GET http://localhost:8083/api/products
curl -X GET http://localhost:8083/api/orders
# Feign Client
curl -X GET http://localhost:8083/api/feign/users
curl -X GET http://localhost:8083/api/feign/users/1
curl -X GET http://localhost:8083/api/feign/products
# === TESTS CONSUL ===
# Liste des services
curl -X GET http://localhost:8500/v1/catalog/services
# Instances d'un service
curl -X GET http://localhost:8500/v1/catalog/service/service-a
# Configuration KV
curl -X GET http://localhost:8500/v1/kv/config/application/data?raw
# Health checks
curl -X GET http://localhost:8500/v1/health/service/service-a
2 Tests d'Intégration
client-service/src/test/java/com/example/client/ConsulIntegrationTest.java
package com.example.client;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ConsulIntegrationTest {
@LocalServerPort
private int port;
private final TestRestTemplate restTemplate = new TestRestTemplate();
@Test
void testServiceDiscovery() {
String baseUrl = "http://localhost:" + port;
// Test discovery endpoint
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl + "/api/discovery/services", String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
assertTrue(response.getBody().contains("service-a"));
assertTrue(response.getBody().contains("service-b"));
assertTrue(response.getBody().contains("service-c"));
}
@Test
void testLoadBalancedRequests() {
String baseUrl = "http://localhost:" + port;
// Test load balanced user service
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl + "/api/users", String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
assertTrue(response.getBody().contains("John Doe") || response.getBody().contains("Fallback User"));
}
@Test
void testFeignClient() {
String baseUrl = "http://localhost:" + port;
// Test Feign client
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl + "/api/feign/users", String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
}
@Test
void testHealthEndpoint() {
String baseUrl = "http://localhost:" + port;
// Test health endpoint
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl + "/actuator/health", String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
}
}
Comparaison avec d'autres Solutions
1 Consul vs Eureka
Comparaison détaillée :
| Caractéristique | Consul | Eureka |
|---|---|---|
| Développeur | HashiCorp | Netflix |
| Langage | Go | Java |
| Service Discovery | ✅ Oui | ✅ Oui |
| Configuration KV | ✅ Intégré | ❌ Non |
| Health Checks | ✅ Avancé | ✅ Basique |
| Multi-Datacenter | ✅ Natif | ❌ Limité |
| Sécurité | ✅ ACL, TLS | ✅ Basique |
| Interface Web | ✅ Intégrée | ✅ Intégrée |
| Service Mesh | ✅ Connect | ❌ Non |
| Maintenance | ✅ Active | ⚠️ Limitée |
2 Consul vs Kubernetes
Quand utiliser chaque solution :
Consul est préférable pour :
- Multi-cloud : Déploiement sur plusieurs clouds
- Hybride : Environnements mixtes (on-premise + cloud)
- Service Mesh : Gestion avancée du trafic
- Configuration : Gestion centralisée de la configuration
- Sécurité : Contrôle d'accès fine-grained
Kubernetes est préférable pour :
- Containerisation : Applications conteneurisées
- Orchestration : Gestion complète du cycle de vie
- Scaling : Auto-scaling horizontal
- Ecosystème : Large écosystème d'outils
- Standard : Standard de l'industrie
Bonnes Pratiques et Conseils
1 Configuration Optimale
Configuration recommandée :
# === CONFIGURATION CONSUL OPTIMALE ===
# === DÉCOUVERTE DE SERVICES ===
spring.cloud.consul.discovery.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.health-check-path=/actuator/health
spring.cloud.consul.discovery.health-check-interval=15s
spring.cloud.consul.discovery.health-check-timeout=5s
spring.cloud.consul.discovery.heartbeat.enabled=true
# === CONFIGURATION KV ===
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.default-context=application
spring.cloud.consul.config.data-key=data
spring.cloud.consul.config.format=KEY_VALUE
spring.cloud.consul.config.acl-token=your-token-here
# === TIMEOUTS ET RETRIES ===
spring.cloud.consul.retry.initial-interval=1000
spring.cloud.consul.retry.max-interval=2000
spring.cloud.consul.retry.max-attempts=10
Conclusion
Points Clés à Retenir
graph TB
A[Consul] --> B[Fonctionnalités]
A --> C[Architecture]
A --> D[Configuration]
A --> E[Sécurité]
A --> F[Intégration]
B --> B1[Service Discovery]
B --> B2[KV Store]
B --> B3[Health Checks]
B --> B4[Service Mesh]
C --> C1[Agents]
C --> C2[Serveurs]
C --> C3[Clients]
C --> C4[Cluster]
D --> D1[Bootstrap]
D2[Services]
D3[KV Store]
E --> E1[ACL]
E2[TLS]
E3[Tokens]
F --> F1[Spring Cloud]
F2[Docker]
F3[Kubernetes]
style A fill:#4CAF50,stroke:#388E3C
style B fill:#2196F3,stroke:#0D47A1
style C fill:#FF9800,stroke:#E65100
style D fill:#9C27B0,stroke:#4A148C
style E fill:#FF5722,stroke:#E64A19
style F fill:#8BC34A,stroke:#33691E
Résumé :
- Consul est une solution complète de service discovery et de configuration
- Utilise des agents légers sur chaque nœud pour la découverte
- Fournit un KV store intégré pour la configuration
- Inclut des health checks avancés et des alertes
- Supporte la sécurité avec ACL et TLS
- S'intègre parfaitement avec Spring Cloud
⚠️ Points d'attention :
- Complexité : Plus complexe qu'Eureka pour les cas simples
- Ressources : Nécessite plus de ressources système
- Apprentissage : Courbe d'apprentissage plus raide
- Maintenance : Nécessite une maintenance proactive