Easily Discover Containerized Spring Boot Microservices using Consul

User Icon ,   Calendar Icon
Containerized Spring Boot Microservices using Consul

Microservices architecture has several advantages over traditional monolithic applications but it presents different challenges as well - service discovery and interaction with other services on-the-fly are very important among them. In this blog I will explain an Easy way to discover containerized Spring Boot Microservices using Consul.

Context

Service registration and discovery are two important aspects of a microservices-based system. Service registration includes registering services and their associated metadata with a service registry, while service discovery includes discovering and connecting to the correct service instances.

Spring Boot applications can use Consul for service discovery by utilizing the Spring Cloud Consul Discovery client. This client allows the application to register itself as a service with Consul and to discover other services registered with Consul. Consul is a powerful service discovery tool that allows developers to easily create, manage and monitor microservices.

We will create two microservices; named 1) clientidgenerator which accepts a REST API GET request to create a hypothetical "Client Id". In this example, the combination of the name of the client (passed as a path parameter in the GET request) and a unique identifier (UUID) forms the "Client Id". To generate a unique Id, clientidgenerator service takes help of another microservice running in parallel named 2) uuidgenerator. Both microservices are discoverable via Consul.

Let’s move to the coding part..

GitHub

uuidgenerator

We will create a microservice uuidgenerator, which exposes only one REST API endpoint (GET http://<hostname>/v1/uuidgenerator). It generates a random unique identifier (UUID) and returns it as a string. uuidgenerator service is discoverable through Consul.

Project dependencies

We use Spring Cloud Consul, which provides an easy integration to Consul for the spring boot applications. For that purpose we require a couple of dependencies; 'spring-cloud-starter-consul-all' which is a starter dependency for Spring Boot applications that provides support for both service discovery and configuration management using Consul, and 'spring-cloud-starter-bootstrap' which helps to load the configuration to register with discovery service, just before the main application configurations are loaded.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-consul-all', version: '4.0.0'
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-bootstrap', version: '4.0.0'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Application configurations

uuidgenerator service will be listening to port 8888.

application:
  api:
    version: v1
server:
  port: 8888
  servlet:
    context-path: /${application.api.version}/uuidgenerator

Bootstrap configuration

In a Spring Boot application that uses Consul for service discovery, the bootstrap.yml file is used to configure the settings for connecting to the Consul server and registering the application as a service. This file is typically located in the src/main/resources directory of your project.

The bootstrap.yml file is read before the application's application.yml or application.properties file, and its properties take precedence over those in the other files. This allows to specify the required Consul-specific configurations before application starts up and registers itself as a service with Consul.

By default the applications look for Consul at port 8500, we just keep that value. We auto-enabled (enabled: true) the discovery of our application.

We added some health checks configurations for the service. By default spring boot adds a health check endpoint which is called periodically and returns 200 OK if service is healthy.

spring:
  application:
    name: uuidgenerator
  cloud:
    consul:
      host: consul
      port: 8500
      discovery:
        enabled: true
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${random.value}
        healthCheckPath: ${management.server.servlet.context-path}/health
        healthCheckInterval: 15s

Application and Controller

The UuidgeneratorApplication class contains a single method generateUuid() which is exposed as a REST endpoint. It's annotated with @GetMapping which maps to the HTTP GET method and with produces = MediaType.TEXT_PLAIN_VALUE, it specifies that the response body will be of plain text format. Inside the method, it generates and returns a random UUID.

We write @EnableDiscoveryClient annotation over Application class, which enables the application to register itself as a service with a service registry (such as Consul) and discover other services that have also registered with the registry.

A @RestController is used to handle HTTP requests and responses. This simple service receives a HTTP GET request, generates a random unique identifier and returns it as a string.

Upon starting, this application registers itself as a service with Consul and listens for incoming HTTP requests on the root path ("/"). When it receives a GET request, it generates a new UUID and returns it as plain text in the response.

@RestController
@RequestMapping(path = "/")
@EnableDiscoveryClient
@SpringBootApplication
public class UuidgeneratorApplication {
	public static void main(String[] args) {
		SpringApplication.run(UuidgeneratorApplication.class, args);
	}

    @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    public String generateUuid() {
        return UUID.randomUUID().toString();
    }
}

Now we can build this application using a Gradle wrapper or any build tool of your choice.

./gradlew clean build

Dockerfile

Create a Dockerfile to build an application docker image. If you wonder what are the meanings of the following lines, then please go to this article where I have explained the basics of the containerization process of docker.

FROM eclipse-temurin:19-jdk-alpine
EXPOSE 8080
ADD build/libs/uuidgenerator-0.0.1-SNAPSHOT.jar /app/uuidgenerator.jar
WORKDIR /app/
CMD java -jar uuidgenerator.jar

We can now build the docker image,

docker build -t uuidgenerator .

clientidgenerator

clientidgenerator service exposes an HTTP GET method, and accepts a /{name} path parameters. It discovers uuidgenerator service on the fly, fetches a UUID and concatenates that UUID with the name of the client and returns as a string.

We repeat most of the steps for clientidgenerator as we did for uuidgenerator.

Project dependencies

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'	
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-consul-all', version: '4.0.0'
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-bootstrap', version: '4.0.0'
	implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Application configurations

clientidgenerator service listens to port 8889.

application:
  api:
    version: v1
  uuidgenerator:
    name: uuidgenerator
    uuid-context-path: /v1/uuidgenerator
server:
  port: 8889
  servlet:
    context-path: /${application.api.version}/clientidgenerator

Bootstrap configuration

spring:
  application:
    name: clientidgenerator
  cloud:
    consul:
      host: consul
      port: 8500
      discovery:
        enabled: true
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${random.value}
        healthCheckPath: ${management.server.servlet.context-path}/health
        healthCheckInterval: 15s

Application

@SpringBootApplication
public class ClientIdGeneratorApplication {
	public static void main(String[] args) {		
              SpringApplication.run(ClientIdGeneratorApplication.class, args);
	}
}

Controller

clientidgenerator service exposes an HTTP GET method and injects ServiceDiscoveryHelper which is a helper class to discover uuidgenerator service and get unique id.

@RestController
public class ClientIdController {

    @Autowired
    private ServiceDiscoveryHelper sdHelper;

    public ClientIdController(ServiceDiscoveryHelper serviceDiscovery) {
        this.sdHelper = serviceDiscovery;
    }
    
    @GetMapping(path = "/{name}", produces = MediaType.TEXT_PLAIN_VALUE)
    public String getHelloMessageString(
        @PathVariable(value = "name") final String name) throws ServiceUnavailableException {

        String msgId = sdHelper.getUuidFromUuidGeneratorService();
        return "Hello " + name + "-" + msgId;
    }
}

ServiceDiscoveryHelper

ServiceDiscoveryHelper is a helper class which 1) resolves the uuidgenerator (the service to discover) application configurations and 2) injects couple of classes into the context,

  • DiscoveryClient: provides API to discover other services
  • RestTemplate: provides client HTTP access.
@Component
public class ServiceDiscoveryHelper {
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    @Value("${application.uuidgenerator.name}")
    private String uuidGeneratorServiceName;

    @Value("${application.uuidgenerator.uuid-context-path}")
    private String uuidGeneratorContextPath;    

    public String getUuidFromUuidGeneratorService() throws ServiceUnavailableException {
        // Discover uuid-generator service from consul and read message key (uuid) from it
        URI service = uuidGeneratorServerUri()
                .map(s -> s.resolve(uuidGeneratorContextPath))
                .orElseThrow(ServiceUnavailableException::new);
        return restTemplate.getForObject(service, String.class);
    }


    private Optional<URI> uuidGeneratorServerUri() {
        return discoveryClient.getInstances(uuidGeneratorServiceName)
            .stream()
            .map(si -> si.getUri()).findFirst();
    }
}

RestTemplate

RestTemplate is a spring class for client side HTTP access. clientidgenerator service uses RestTemplate to call HTTP GET method exposed by uuidgenerator.

@Configuration
public class MessageConfiguration {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

Dockerfile

FROM eclipse-temurin:19-jdk-alpine
EXPOSE 8080
ADD build/libs/clientidgenerator-0.0.1-SNAPSHOT.jar /app/clientidgenerator.jar
WORKDIR /app/
CMD java -jar clientidgenerator.jar

docker-compose

We are ready to run following services, required for this example

  • consul
  • uuidgenerator
  • clientidgenerator
version: '3.9'
services:
  consul:
    image: consul:latest
    ports:
      - "8500:8500"
  uuidgenerator:
    image: uuidgenerator
    ports:
      - "8888:8888"
    depends_on:
      - consul
    links:
      - "consul"
  clientidgenerator:
    image: clientidgenerator
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8889:8889"

Run them using docker-compose,

docker-compose up
docker-images-console

We can also check docker images running in containers in Docker Desktop,

docker-desktop-view

We can check all services status on consul UI, http://localhost:8500

Testing

We fire a HTTP GET request "http://localhost:8889/v1/clientidgenerator/bob" - where "bob" is a name path parameter. clientidgenerator service finds the uuidgenerator service to fetch a unique Id and then return "Hello <client-name>-<id>"

postman-get-request-view
Postman Get request

Conclusion

Hope this article helps you understand and implement the discoverable spring boot microservices. We see how Spring Cloud Consul makes service discovery easy by providing a set of simple, easy-to-use annotations that developers can use to integrate Consul into their Spring Boot applications. This can greatly simplify the process of creating, managing, and monitoring microservices.