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

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