Consul and Micronaut - Microservices service discovery

Use Consul service discovery to expose your Micronaut apps.

Authors: Sergio del Amo

Micronaut Version: 2.5.0

1. Getting Started

In this guide, we are going to create three microservices and register them with Consul Service discovery.

Consul is a distributed service mesh to connect, secure, and configure services across any runtime platform and public or private cloud

You will discover how Micronaut eases Consul integration.

2. What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

3. Solution

We recommend that you follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

4. Writing the App

Let’s describe the microservices you are going to build through the tutorial.

  • bookcatalogue - It returns a list of books. It uses a domain consisting of a book name and isbn.

  • bookinventory - It exposes an endpoint to check whether a book has sufficient stock to fulfil an order. It uses a domain consisting of a stock level and isbn.

  • bookrecommendation - It consumes previous services and exposes an endpoint which recommends book names which are in stock.

Initially we are going to hardcode the addresses where the different services are in the bookcatalogue service.

hardcoded

As shown in the previous image, the bookcatalogue hardcodes references to its collaborators.

In the second part of this tutorial we are going to use a discovery service.

About registration patterns

We will use a self‑registration pattern. Thus, each service instance is responsible for registering and deregistering itself with the service registry. Also, if required, a service instance sends heartbeat requests to prevent its registration from expiring.

Services register when they start up:

discovery service registration

We will use client‑side service discovery, clients query the service registry, select an available instance, and make a request.

discovery service flow

4.1. Catalogue Microservice

Create the bookcatalogue microservice:

mn create-app example.micronaut.bookcatalogue --build=maven --lang=groovy

The previous command creates a folder named bookcatalogue and a Micronaut app inside it.

Create a BooksController class to handle incoming HTTP requests into the bookcatalogue microservice:

bookcatalogue/src/main/groovy/example/micronaut/BooksController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@CompileStatic
@Controller("/books") (1)
class BooksController {

    @Get("/") (2)
    List<Book> index() {
        [
            new Book("1491950358", "Building Microservices"),
            new Book("1680502395", "Release It!"),
            new Book("0321601912", "Continuous Delivery:"),
        ]
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 The @Get annotation is used to map the index method to an HTTP GET request on /books.

The previous controller responds a List<Book>. Create the Book POJO:

bookcatalogue/src/main/groovy/example/micronaut/Book.groovy
package example.micronaut

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@TupleConstructor
@EqualsAndHashCode
@CompileStatic
class Book {
    String isbn
    String name
}

Write a test:

bookcatalogue/src/test/groovy/example/micronaut/BooksControllerSpec.groovy
package example.micronaut

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest (1)
class BooksControllerSpec extends Specification {

    @Inject
    @Client("/")
    HttpClient client (2)

    void "it is possible to retrieve books"() {
        when:
        HttpRequest request = HttpRequest.GET("/books") (3)
        List books = client.toBlocking().retrieve(request, Argument.listOf(Book)) (4)

        then:
        books.size() == 3
        books.contains(new Book("1491950358", "Building Microservices"))
        books.contains(new Book("1680502395", "Release It!"))
    }
}
1 Annotate the class with @MicronautTest to let Micronaut starts the embedded server and inject the beans. More info: https://micronaut-projects.github.io/micronaut-test/latest/guide/index.html.
2 Inject the HttpClient bean in the application context.
3 It is easy to create HTTP requests with a fluid API.
4 Parse easily JSON into Java objects.

Edit application.yml

bookcatalogue/src/main/resources/application.yml
micronaut:
  application:
    name: bookcatalogue (1)
  server:
    port: 8081 (2)
1 Configure the application name. The app name will be use by the discovery service.
2 Configure the app to listen at port 8081

Create a file named application-test.yml which is used in the test environment:

bookcatalogue/src/test/resources/application-test.yml
micronaut:
  server:
    port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

Run the unit test:

bookcatalogue $ ./mvnw test

4.2. Inventory Microservice

Create the bookinventory microservice:

mn create-app example.micronaut.bookinventory --build=maven --lang=groovy

The previous command creates a folder named bookinventory and a Micronaut app inside it.

Create a Controller:

bookinventory/src/main/groovy/example/micronaut/BooksController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces

import javax.validation.constraints.NotBlank

@CompileStatic
@Controller("/books") (1)
class BooksController {

    @Produces(MediaType.TEXT_PLAIN) (2)
    @Get("/stock/{isbn}") (3)
    Boolean stock(@NotBlank String isbn) {
        bookInventoryByIsbn(isbn).map { bi -> bi.getStock() > 0 }.orElse(null)
    }

    private Optional<BookInventory> bookInventoryByIsbn(String isbn) {
        if (isbn == "1491950358") {
            return Optional.of(new BookInventory(isbn, 4))

        } else if (isbn == "1680502395") {
            return Optional.of(new BookInventory(isbn, 0))
        }
        Optional.empty()
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 By default, Content-Type of Micronaut’s response is application/json : override this with text/plain since we are returning a String, not a JSON object.
3 The @Get annotation is used to map the index method to an HTTP GET request on /books/stock/{isbn}.

Create the POJO used by the controller:

bookinventory/src/main/groovy/example/micronaut/BookInventory.groovy
package example.micronaut

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@CompileStatic
@TupleConstructor
@EqualsAndHashCode
class BookInventory {
    String isbn
    Integer stock
}

Write a test:

bookinventory/src/test/groovy/example/micronaut/BooksControllerSpec.groovy
package example.micronaut

import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest
class BooksControllerSpec extends Specification {

    @Inject
    @Client("/")
    HttpClient client

    void "for a book with inventory true is returned"() {
        when:
        Boolean hasStock = client.toBlocking().retrieve(HttpRequest.GET("/books/stock/1491950358"), Boolean)

        then:
        noExceptionThrown()
        hasStock
    }

    void "for a book without inventory false is returned"() {
        when:
        Boolean hasStock = client.toBlocking().retrieve(HttpRequest.GET("/books/stock/1680502395"), Boolean)

        then:
        noExceptionThrown()
        hasStock == Boolean.FALSE
    }

    void "for an invalid ISBN 404 is returned"() {
        when:
        client.toBlocking().retrieve(HttpRequest.GET("/books/stock/XXXXX"))

        then:
        def e = thrown(HttpClientResponseException)
        e.response.status == HttpStatus.NOT_FOUND
    }
}

Edit application.yml

bookinventory/src/main/resources/application.yml
micronaut:
  application:
    name: bookinventory (1)
  server:
    port: 8082 (2)
1 Configure the application name. The app name will be used later in the tutorial.
2 Configure the app to listen at port 8082

Create a file named application-test.yml which is used in the test environment:

bookinventory/src/test/resources/application-test.yml
micronaut:
  server:
    port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

Run the unit test:

bookinventory $ ./mvnw test

4.3. Recommendation Microservice

Create the bookrecommendation microservice:

mn create-app example.micronaut.bookrecommendation --build=maven --lang=groovy

The previous command creates a folder named bookrecommendation and a Micronaut app inside it.

Create an interface to map operations with bookcatalogue, and a Micronaut Declarative HTTP Client to consume it.

bookrecommendation/src/main/groovy/example/micronaut/BookCatalogueOperations.groovy
package example.micronaut

import io.reactivex.Flowable

interface BookCatalogueOperations {
    Flowable<Book> findAll()
}
bookrecommendation/src/main/groovy/example/micronaut/BookCatalogueClient.groovy
package example.micronaut

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
import io.micronaut.retry.annotation.Recoverable;

@Client("http://localhost:8081") (1)
@Recoverable(api = BookCatalogueOperations.class)

interface BookCatalogueClient extends BookCatalogueOperations {

    @Get("/books")
    Flowable<Book> findAll()
}
1 Use @Client to use declarative HTTP Clients

The client returns a POJO. Create it in the bookrecommendation:

bookrecommendation/src/main/groovy/example/micronaut/Book.groovy
package example.micronaut

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@CompileStatic
@TupleConstructor
@EqualsAndHashCode
class Book {
    String isbn
    String name
}

Create an interface to map operations with bookinventory, and a Micronaut Declarative HTTP Client to consume it.

bookrecommendation/src/main/groovy/example/micronaut/BookInventoryOperations.groovy
package example.micronaut

import io.reactivex.Maybe
import javax.validation.constraints.NotBlank

interface BookInventoryOperations {
    Maybe<Boolean> stock(@NotBlank String isbn)
}
bookrecommendation/src/main/groovy/example/micronaut/BookCatalogueClient.groovy
package example.micronaut

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
import io.micronaut.retry.annotation.Recoverable;

@Client("http://localhost:8081") (1)
@Recoverable(api = BookCatalogueOperations.class)

interface BookCatalogueClient extends BookCatalogueOperations {

    @Get("/books")
    Flowable<Book> findAll()
}
1 Use @Client to use declarative HTTP Clients

Create a Controller which injects both clients.

bookrecommendation/src/main/groovy/example/micronaut/BookController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.reactivex.Flowable

@CompileStatic
@Controller("/books") (1)
class BookController {

    private final BookCatalogueOperations bookCatalogueOperations
    private final BookInventoryOperations bookInventoryOperations

    BookController(BookCatalogueOperations bookCatalogueOperations,
                   BookInventoryOperations bookInventoryOperations) { (2)
        this.bookCatalogueOperations = bookCatalogueOperations
        this.bookInventoryOperations = bookInventoryOperations
    }

    @Get("/") (3)
    Flowable<BookRecommendation> index() {
        return bookCatalogueOperations.findAll()
            .flatMapMaybe { b ->
                bookInventoryOperations.stock(b.isbn)
                    .filter { hasStock -> hasStock == Boolean.TRUE }
                    .map { rsp -> b }
            }.map { book -> new BookRecommendation(book.name) }
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 Clients are injected via constructor injection
3 The @Get annotation is used to map the index method to an HTTP GET request on /books.

The previous controller returns a Flowable<BookRecommendation>. Create the BookRecommendation POJO:

bookrecommendation/src/main/groovy/example/micronaut/BookRecommendation.groovy
package example.micronaut

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@TupleConstructor
@CompileStatic
@EqualsAndHashCode
class BookRecommendation {
    String name
}

BookCatalogueClient and BookInventoryClient will fail to consume the bookcatalogue and bookinventory during the tests phase.

Using the @Fallback annotation you can declare a fallback implementation of a client that will be picked up and used once all possible retries have been exhausted

Create @Fallback alternatives in the test classpath.

bookrecommendation/src/test/groovy/example/micronaut/BookInventoryClientStub.groovy
package example.micronaut

import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.reactivex.Maybe
import io.micronaut.retry.annotation.Fallback
import javax.inject.Singleton
import javax.validation.constraints.NotBlank

@Requires(env = Environment.TEST) (1)
@Fallback
@Singleton
class BookInventoryClientStub implements BookInventoryOperations {

    @Override
    Maybe<Boolean> stock(@NotBlank String isbn) {
        if (isbn == "1491950358") {
            return Maybe.just(Boolean.TRUE) (2)

        } else if (isbn == "1680502395") {
            return Maybe.just(Boolean.FALSE) (3)
        }
        Maybe.empty() (4)
    }
}
1 Make this fallback class to be effective only when the micronaut environment TEST is active
2 Here we arbitrarily decided that if everything else fails, that book’s stock would be true
3 Similarly, we decided that other book’s stock method would be false
4 Finally, any other book will have their stock method return an empty value
bookrecommendation/src/test/groovy/example/micronaut/BookCatalogueClientStub.groovy
package example.micronaut

import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.reactivex.Flowable
import io.micronaut.retry.annotation.Fallback
import javax.inject.Singleton

@Requires(env = Environment.TEST)
@Fallback
@Singleton
class BookCatalogueClientStub implements BookCatalogueOperations {

    @Override
    Flowable<Book> findAll() {
        Book buildingMicroservices = new Book("1491950358", "Building Microservices")
        Book releaseIt = new Book("1680502395", "Release It!")
        Flowable.just(buildingMicroservices, releaseIt)
    }
}

Write a test:

bookrecommendation/src/test/groovy/example/micronaut/BookControllerSpec.groovy
package example.micronaut

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest
class BookControllerSpec extends Specification {

    @Inject
    @Client("/")
    HttpClient client

    void "retrieve books"() {
        when:
        HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/books"), Argument.listOf(BookRecommendation))

        then:
        response.status() == HttpStatus.OK
        response.body().size() == 1
        response.body().get(0).name == "Building Microservices"
    }
}

Edit application.yml

bookrecommendation/src/main/resources/application.yml
micronaut:
  application:
    name: bookrecommendation (1)
  server:
    port: 8080 (2)
1 Configure the application name. The app name will be used later in the tutorial.
2 Configure the app to listen at port 8080

Create a file named application-test.yml which is used in the test environment:

bookrecommendation/src/test/resources/application-test.yml
micronaut:
  server:
    port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

Run the unit test:

bookinventory $ ./mvnw test

4.4. Running the app

Run bookcatalogue microservice:

To run the application execute ./mvnw mn:run.

...
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

Run bookinventory microservice:

To run the application execute ./mvnw mn:run.

...
14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 506ms. Server Running: http://localhost:8082

Run bookrecommendation microservice:

To run the application execute ./mvnw mn:run.

...
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080

You can run a cURL command to test the whole application:

$ curl http://localhost:8080/books
[{"name":"Building Microservices"}]

5. Consul and Micronaut

5.1. Install Consul via Docker

The quickest way to start using Consul is via Docker:

$ docker run -p 8500:8500 consul

The following screenshots show how to install/run Consul via Kitematic; graphical user interface for Docker.

kitematic consul 1

Configure ports:

kitematic consul 2

5.2. Book Catalogue

Modify your build file to add the discovery-client feature.

pom.xml
<dependency>
    <groupId>io.micronaut.discovery</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
    <scope>compile</scope>
</dependency>

Append to bookcatalogue service application.yml the following snippet:

bookcatalogue/src/main/resources/application.yml
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

This configuration registers a Micronaut app with Consul with minimal configuration. Discover a more complete list of Configuration options at ConsulConfiguration.

Disable consul registration in tests:

bookcatalogue/src/test/resources/application-test.yml
consul:
  client:
    registration:
      enabled: false

5.3. Book Inventory

Modify your build file to add the discovery-client feature.

pom.xml
<dependency>
    <groupId>io.micronaut.discovery</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
    <scope>compile</scope>
</dependency>

Also, modify the application.yml of the bookinventory application with the following snippet:

bookinventory/src/main/resources/application.yml
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

Disable consul registration in tests:

bookinventory/src/test/resources/application-test.yml
consul:
  client:
    registration:
      enabled: false

5.4. Book Recommendation

Modify your build file to add the discovery-client feature.

pom.xml
<dependency>
    <groupId>io.micronaut.discovery</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
    <scope>compile</scope>
</dependency>

Also, append to bookrecommendation.application.yml the following snippet:

bookrecommendation/src/main/resources/application.yml
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

Modify BookInventoryClient and BookCatalogueClient to use the service id instead of a harcoded ip.

bookrecommendation/src/main/groovy/example/micronaut/BookCatalogueClient.groovy
package example.micronaut

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
import io.micronaut.retry.annotation.Recoverable;

@Client(id = "bookcatalogue") (1)
@Recoverable(api = BookCatalogueOperations.class)

interface BookCatalogueClient extends BookCatalogueOperations {

    @Get("/books")
    Flowable<Book> findAll()
}
1 Use the configuration value micronaut.application.name used in bookcatalogue as service id.
bookrecommendation/src/main/groovy/example/micronaut/BookInventoryClient.groovy
package example.micronaut

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Maybe
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Consumes
import javax.validation.constraints.NotBlank
import io.micronaut.retry.annotation.Recoverable;

@Client(id = "bookinventory") (1)
@Recoverable(api = BookInventoryOperations.class)

interface BookInventoryClient extends BookInventoryOperations {

    @Get("/books/stock/{isbn}")
    @Consumes(MediaType.TEXT_PLAIN)
    Maybe<Boolean> stock(@NotBlank String isbn)
}
1 Use the configuration value micronaut.application.name used in bookinventory as service id.

Disable consul registration in tests:

bookrecommendation/src/test/resources/application-test.yml
consul:
  client:
    registration:
      enabled: false

5.5. Running the App

Run bookcatalogue microservice:

bookcatalogue $ ./gradlew run
...
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081
14:28:34.084 [nioEventLoopGroup-1-3] INFO  i.m.d.registration.AutoRegistration - Registered service [bookcatalogue] with Consul

Run bookinventory microservice:

bookinventory $ ./gradlew run
...
14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 506ms. Server Running: http://localhost:8082
14:31:13.154 [nioEventLoopGroup-1-3] INFO  i.m.d.registration.AutoRegistration - Registered service [bookinventory] with Consul

Run bookrecommendation microservice:

...
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080
14:31:57.439 [nioEventLoopGroup-1-3] INFO  i.m.d.registration.AutoRegistration - Registered service [bookrecommendation] with Consul

Consul comes with a HTML UI. Open http://localhost:8500/ui in your browser.

You will see the services registered in Consul:

consului

You can run a cURL command to test the whole application:

$ curl http://localhost:8080/books
[{"name":"Building Microservices"}]

6. Next steps

Read more about Consul support inside Micronaut.

7. Help with Micronaut

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.