Micronaut Discovery Client

Adds Service Discovery Features for Eureka and Consul

Version:

1 Introduction

Using the CLI

If you are creating your project using the Micronaut CLI, supply either of discovery-consul or discovery-eureka features to enable service-discovery in your project:

$ mn create-app my-app --features discovery-consul

Service Discovery enables the ability for Microservices to find each other without necessarily knowing the physical location or IP address of associated services.

There are many ways Service Discovery can be implemented, including:

  • Manually implement Service Discovery using DNS without requiring a third party tool or component.

  • Use a discovery server such as Eureka, Consul or ZooKeeper.

  • Delegate the work to a container runtime, such as Kubernetes.

With that in mind, Micronaut tries to flexible to support all of these approaches. As of this writing, Micronaut features integrated support for the popular Service Discovery servers:

  • Eureka

  • Consul

To include Service Discovery in your application simply the first step is to add the discovery-client dependency to your application:

implementation("io.micronaut.discovery:micronaut-discovery-client")
<dependency>
    <groupId>io.micronaut.discovery</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
</dependency>

The discovery-client dependency provides implementations of the DiscoveryClient interface.

The DiscoveryClient is fairly simple and provides two main entry points:

Both methods return Publisher instances since the operation to retrieve service ID information may result in a blocking network call depending on the underlying implementation.

If you are using Micronaut’s cache module, the default implementation of the DiscoveryClient interface is CachingCompositeDiscoveryClient which merges all other DiscoveryClient beans into a single bean and provides caching of the results of the methods. The default behaviour is to cache for 30 seconds. This cache can be disabled in application configuration:

Disabling the Discovery Client Cache
micronaut.caches.discovery-client.enabled=false
micronaut:
  caches:
    discovery-client:
      enabled: false
[micronaut]
  [micronaut.caches]
    [micronaut.caches.discovery-client]
      enabled=false
micronaut {
  caches {
    discoveryClient {
      enabled = false
    }
  }
}
{
  micronaut {
    caches {
      discovery-client {
        enabled = false
      }
    }
  }
}
{
  "micronaut": {
    "caches": {
      "discovery-client": {
        "enabled": false
      }
    }
  }
}

Alternatively you can alter the cache’s expiration policy:

Configuring the Discovery Client Cache
micronaut.caches.discovery-client.expire-after-access=60s
micronaut:
  caches:
    discovery-client:
      expire-after-access: 60s
[micronaut]
  [micronaut.caches]
    [micronaut.caches.discovery-client]
      expire-after-access="60s"
micronaut {
  caches {
    discoveryClient {
      expireAfterAccess = "60s"
    }
  }
}
{
  micronaut {
    caches {
      discovery-client {
        expire-after-access = "60s"
      }
    }
  }
}
{
  "micronaut": {
    "caches": {
      "discovery-client": {
        "expire-after-access": "60s"
      }
    }
  }
}

See the DiscoveryClientCacheConfiguration class for available configuration options.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Consul Support

Consul is a popular Service Discovery and Distributed Configuration server provided by HashiCorp. Micronaut features a native non-blocking ConsulClient that is built using Micronaut’s support for Declarative HTTP Clients.

Starting Consul

The quickest way to start using Consul is via Docker:

  1. Starting Consul with Docker.

docker run -p 8500:8500 consul

Auto Registering with Consul

To register a Micronaut application with Consul simply add the necessary ConsulConfiguration. A minimal example can be seen below:

Auto Registering with Consul
micronaut.application.name=hello-world
consul.client.registration.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
micronaut:
    application:
        name: hello-world
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[micronaut]
  [micronaut.application]
    name="hello-world"
[consul]
  [consul.client]
    [consul.client.registration]
      enabled=true
    defaultZone="${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
micronaut {
  application {
    name = "hello-world"
  }
}
consul {
  client {
    registration {
      enabled = true
    }
    defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
  }
  consul {
    client {
      registration {
        enabled = true
      }
      defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    }
  },
  "consul": {
    "client": {
      "registration": {
        "enabled": true
      },
      "defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
Using the Micronaut CLI you can quickly create a new service setup with Consul using: mn create-app my-app --features discovery-consul

The consul.client.defaultZone settings accepts a list of Consul servers to be used by default.

You could also simply set consul.client.host and consul.client.port, however ConsulConfiguration allows you specify per zone discovery services for the purpose load balancing. A zone maps onto a AWS availability zone or a Google Cloud zone.

By default registering with Consul is disabled hence you should set consul.client.registration.enabled to true. Note that you may wish to do this only in your production configuration.

Running multiple instances of a service may require an additional configuration param. See below.

If you are running the same applications on the same port across different servers it is important to set the micronaut.application.instance.id property or you will experience instance registration collision.

micronaut.application.name=hello-world
micronaut.application.instance.id=${random.shortuuid}
micronaut:
  application:
    name: hello-world
    instance:
      id: ${random.shortuuid}
[micronaut]
  [micronaut.application]
    name="hello-world"
    [micronaut.application.instance]
      id="${random.shortuuid}"
micronaut {
  application {
    name = "hello-world"
    instance {
      id = "${random.shortuuid}"
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
      instance {
        id = "${random.shortuuid}"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world",
      "instance": {
        "id": "${random.shortuuid}"
      }
    }
  }
}

Customizing Consul Service Registration

The ConsulConfiguration class features a range of customization options for altering how an instance registers with Consul. You can customize the tags, the retry attempts, the fail fast behaviour and so on.

Notice too that ConsulConfiguration extends DiscoveryClientConfiguration which in turn extends HttpClientConfiguration allowing you to customize the settings for the Consul client, including read timeout, proxy configuration and so on.

For example:

Customizing Consul Registration Configuration
micronaut.application.name=hello-world
consul.client.registration.enabled=true
consul.client.registration.tags[0]=hello
consul.client.registration.tags[1]=world
consul.client.registration.meta.some=value
consul.client.registration.meta.instance_type=t2.medium
consul.client.registration.retry-count=5
consul.client.registration.fail-fast=false
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
micronaut:
    application:
        name: hello-world
consul:
  client:
    registration:
      enabled: true
      tags:
        - hello
        - world
      meta:
        some: value
        instance_type: t2.medium
      retry-count: 5
      fail-fast: false
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[micronaut]
  [micronaut.application]
    name="hello-world"
[consul]
  [consul.client]
    [consul.client.registration]
      enabled=true
      tags=[
        "hello",
        "world"
      ]
      [consul.client.registration.meta]
        some="value"
        instance_type="t2.medium"
      retry-count=5
      fail-fast=false
    defaultZone="${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
micronaut {
  application {
    name = "hello-world"
  }
}
consul {
  client {
    registration {
      enabled = true
      tags = ["hello", "world"]
      meta {
        some = "value"
        instance_type = "t2.medium"
      }
      retryCount = 5
      failFast = false
    }
    defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
  }
  consul {
    client {
      registration {
        enabled = true
        tags = ["hello", "world"]
        meta {
          some = "value"
          instance_type = "t2.medium"
        }
        retry-count = 5
        fail-fast = false
      }
      defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    }
  },
  "consul": {
    "client": {
      "registration": {
        "enabled": true,
        "tags": ["hello", "world"],
        "meta": {
          "some": "value",
          "instance_type": "t2.medium"
        },
        "retry-count": 5,
        "fail-fast": false
      },
      "defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
  • tags alters the tags

  • meta alters the metadata

  • retry-count alsters the retry count

  • fail-fast alters the fail fast behavior

Discovery Services from Consul

To discovery other services you could manually interact with the DiscoveryClient, however typically instead you use the Client Annotation to declare how an HTTP client maps to a service.

For example the configuration in the previous section declared a value for micronaut.application.name of hello-world. This is the value that will be used as the service ID when registering with Consul.

Other services can discovery instances of the hello-world service simply by declaring a client as follows:

Using @Client to Discover Services
@Client(id = "hello-world")
interface HelloClient{
    ...
}

Alternatively you can also use @Client as a qualifier to @Inject an instance of HttpClient:

Using @Client to Discover Services
@Client(id = "hello-world")
@Inject
RxHttpClient httpClient;

Consul Health Checks

By default when registering with Consul Micronaut will register a TTL check. A TTL check basically means that if the application does not send a heartbeat back to Consul after a period of time the service is put in a failing state.

Micronaut applications feature a HeartbeatConfiguration which starts a thread using HeartbeatTask that fires HeartbeatEvent instances.

The ConsulAutoRegistration class listens for these events and sends a callback to the /agent/check/pass/:check_id endpoint provided by Consul, effectively keeping the service alive.

With this arrangement the responsibility is on the Micronaut application to send TTL callbacks to Consul on a regular basis.

If you prefer you can push the responsibility for health checks to Consul itself by registering an HTTP check:

Consul HTTP Check Configuration
consul.client.registration.check.http=true
consul:
  client:
    registration:
       check:
         http: true
[consul]
  [consul.client]
    [consul.client.registration]
      [consul.client.registration.check]
        http=true
consul {
  client {
    registration {
      check {
        http = true
      }
    }
  }
}
{
  consul {
    client {
      registration {
        check {
          http = true
        }
      }
    }
  }
}
{
  "consul": {
    "client": {
      "registration": {
        "check": {
          "http": true
        }
      }
    }
  }
}

With this configuration option in place Consul will assume responsibility of invoking the Micronaut applications Health Endpoint.

Controlling IP/Host Registration

Occasionally, depending on the deployment environment you may wish to expose the IP address and not the host name, since by default Micronaut will register with Consul with either the value of the HOST environment variable or the value configured via micronaut.server.host.

You can use the consul.client.registration.prefer-ip-address setting to indicate you would prefer to register with the IP address.

Micronaut will by default perform an IP lookup to try and figure out the IP address, however you can use the consul.client.registration.ip-addr setting to specify the IP address of the service directly.

Consul HTTP Check Configuration
consul.client.registration.ip-addr=<your base container ip>
consul.client.registration.prefer-ip-address=true
consul:
  client:
    registration:
      ip-addr: <your base container ip>
      prefer-ip-address: true
[consul]
  [consul.client]
    [consul.client.registration]
      ip-addr="<your base container ip>"
      prefer-ip-address=true
consul {
  client {
    registration {
      ipAddr = "<your base container ip>"
      preferIpAddress = true
    }
  }
}
{
  consul {
    client {
      registration {
        ip-addr = "<your base container ip>"
        prefer-ip-address = true
      }
    }
  }
}
{
  "consul": {
    "client": {
      "registration": {
        "ip-addr": "<your base container ip>",
        "prefer-ip-address": true
      }
    }
  }
}

This will tell Consul to register the IP that other instances can use to access your service and not the NAT IP it is running under (or 127.0.0.1).

If you use HTTP health checks (see the previous section) then Consul will use the configured IP address to check the Micronaut /health endpoint.

Consul HTTP Check Configuration
consul.client.registration.ip-addr=<your base container ip>
consul.client.registration.prefer-ip-address=true
consul.client.registration.check.http=true
consul:
  client:
    registration:
      ip-addr: <your base container ip>
      prefer-ip-address: true
      check:
        http: true
[consul]
  [consul.client]
    [consul.client.registration]
      ip-addr="<your base container ip>"
      prefer-ip-address=true
      [consul.client.registration.check]
        http=true
consul {
  client {
    registration {
      ipAddr = "<your base container ip>"
      preferIpAddress = true
      check {
        http = true
      }
    }
  }
}
{
  consul {
    client {
      registration {
        ip-addr = "<your base container ip>"
        prefer-ip-address = true
        check {
          http = true
        }
      }
    }
  }
}
{
  "consul": {
    "client": {
      "registration": {
        "ip-addr": "<your base container ip>",
        "prefer-ip-address": true,
        "check": {
          "http": true
        }
      }
    }
  }
}

4 Eureka Support

Netflix Eureka is a popular discovery server deployed at scale at organizations like Netflix.

Micronaut features a native non-blocking EurekaClient as part of the discovery-client module that does not require any additional third-party dependencies and is built using Micronaut’s support for Declarative HTTP Clients.

Starting Eureka

The quickest way to start a Eureka server is to use to use Spring Boot’s Eureka starters.

As of this writing the official Docker images for Eureka are significantly out-of-date so it is recommended to create a Eureka server following the steps above.

Auto Registering with Eureka

The process to register a Micronaut application with Eureka is very similar to with Consul, as seen in the previous section, simply add the necessary EurekaConfiguration. A minimal example can be seen below:

Auto Registering with Eureka (application.yml)
micronaut.application.name=hello-world
eureka.client.registration.enabled=true
eureka.client.defaultZone=${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}
micronaut:
  application:
    name: hello-world
eureka:
  client:
    registration:
      enabled: true
    defaultZone: "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"
[micronaut]
  [micronaut.application]
    name="hello-world"
[eureka]
  [eureka.client]
    [eureka.client.registration]
      enabled=true
    defaultZone="${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"
micronaut {
  application {
    name = "hello-world"
  }
}
eureka {
  client {
    registration {
      enabled = true
    }
    defaultZone = "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
  }
  eureka {
    client {
      registration {
        enabled = true
      }
      defaultZone = "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    }
  },
  "eureka": {
    "client": {
      "registration": {
        "enabled": true
      },
      "defaultZone": "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"
    }
  }
}

Customizing Eureka Service Registration

You can customize various aspects of registration with Eureka using the EurekaConfiguration. Notice that EurekaConfiguration extends DiscoveryClientConfiguration which in turn extends HttpClientConfiguration allowing you to customize the settings for the Eureka client, including read timeout, proxy configuration and so on.

Example Eureka Configuration
eureka.client.readTimeout=5s
eureka.client.registration.asgName=myAsg
eureka.client.registration.countryId=10
eureka.client.registration.vipAddress=myapp
eureka.client.registration.leaseInfo.durationInSecs=60
eureka.client.registration.metadata.foo=bar
eureka.client.registration.retry-count=10
eureka.client.registration.retry-delay=5s
eureka.client.registration.appname=some-app-name
eureka.client.registration.hostname=foo.example.com
eureka.client.registration.ip-addr=1.2.3.4
eureka.client.registration.port=9090
eureka:
  client:
    readTimeout: 5s
    registration:
      asgName: myAsg
      countryId: 10
      vipAddress: 'myapp'
      leaseInfo:
        durationInSecs: 60
      metadata:
        foo: bar
      retry-count: 10
      retry-delay: 5s
      appname: some-app-name
      hostname: foo.example.com
      ip-addr: 1.2.3.4
      port: 9090
[eureka]
  [eureka.client]
    readTimeout="5s"
    [eureka.client.registration]
      asgName="myAsg"
      countryId=10
      vipAddress="myapp"
      [eureka.client.registration.leaseInfo]
        durationInSecs=60
      [eureka.client.registration.metadata]
        foo="bar"
      retry-count=10
      retry-delay="5s"
      appname="some-app-name"
      hostname="foo.example.com"
      ip-addr="1.2.3.4"
      port=9090
eureka {
  client {
    readTimeout = "5s"
    registration {
      asgName = "myAsg"
      countryId = 10
      vipAddress = "myapp"
      leaseInfo {
        durationInSecs = 60
      }
      metadata {
        foo = "bar"
      }
      retryCount = 10
      retryDelay = "5s"
      appname = "some-app-name"
      hostname = "foo.example.com"
      ipAddr = "1.2.3.4"
      port = 9090
    }
  }
}
{
  eureka {
    client {
      readTimeout = "5s"
      registration {
        asgName = "myAsg"
        countryId = 10
        vipAddress = "myapp"
        leaseInfo {
          durationInSecs = 60
        }
        metadata {
          foo = "bar"
        }
        retry-count = 10
        retry-delay = "5s"
        appname = "some-app-name"
        hostname = "foo.example.com"
        ip-addr = "1.2.3.4"
        port = 9090
      }
    }
  }
}
{
  "eureka": {
    "client": {
      "readTimeout": "5s",
      "registration": {
        "asgName": "myAsg",
        "countryId": 10,
        "vipAddress": "myapp",
        "leaseInfo": {
          "durationInSecs": 60
        },
        "metadata": {
          "foo": "bar"
        },
        "retry-count": 10,
        "retry-delay": "5s",
        "appname": "some-app-name",
        "hostname": "foo.example.com",
        "ip-addr": "1.2.3.4",
        "port": 9090
      }
    }
  }
}
  • asgName the auto scaling group name

  • countryId the country id

  • vipAddress The Eureka VIP address

  • durationInSecs The lease information

  • metadata arbitrary instance metadata

  • retry-count How many times to retry

  • retry-delay How long to wait between retries

  • appname (optional) eureka instance application name, defaults to ${micronaut.application.name}

  • hostname (optional) exposed eureka instance hostname, useful in docker bridged network environments

  • ip-addr (optional) exposed eureka instance ip address, useful in docker bridged network environments

  • port (optional) exposed eureka instance port, useful in docker bridged network environments

Eureka Basic Authentication

You can customize the Eureka credentials in the URI you specify to in defaultZone.

For example:

Auto Registering with Eureka
eureka.client.defaultZone=https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761
eureka:
  client:
    defaultZone: "https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761"
[eureka]
  [eureka.client]
    defaultZone="https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761"
eureka {
  client {
    defaultZone = "https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761"
  }
}
{
  eureka {
    client {
      defaultZone = "https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761"
    }
  }
}
{
  "eureka": {
    "client": {
      "defaultZone": "https://${EUREKA_USERNAME}:${EUREKA_PASSWORD}@localhost:8761"
    }
  }
}

The above example externalizes configuration of the username and password Eureka to environment variables called EUREKA_USERNAME and EUREKA_PASSWORD.

Eureka Health Checks

Like Consul, the EurekaAutoRegistration will send HeartbeatEvent instances with the HealthStatus of the Micronaut application to Eureka.

The HealthMonitorTask will by default continuously monitor the HealthStatus of the application by running health checks and the CurrentHealthStatus will be sent to Eureka.

Secure Communication with Eureka

If you wish to configure HTTPS and have clients discovery Eureka instances and communicate over HTTPS then you should set the eureka.client.discovery.use-secure-port option to true to ensure that service communication happens over HTTPS and also configure HTTPS appropriately for each instance.

5 Spring Cloud Config Server Support

Spring Cloud Config Server Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments.

A Micronaut application can be a Spring Cloud Config client to consume Spring Cloud Config Server configurations.

Setup

The quickest way to start a Spring Cloud Config Server is to use to use Spring Boot’s Quick Start.

Loading configurations from config server

The process to consume configurations in a Micronaut application from a Spring Cloud Config Server is very strait forward, simply add the necessary configurations. A complete example can be seen below:

Registering your application as a Spring Cloud Config client
micronaut.application.name=hello-world
micronaut.config-client.enabled=true
spring.cloud.config.enabled=true
spring.cloud.config.uri=http://configserver:9000
spring.cloud.config.name=filename1,filename2
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true
spring:
  cloud:
    config:
      enabled: true
      uri: "http://configserver:9000"
      name: filename1,filename2
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[spring]
  [spring.cloud]
    [spring.cloud.config]
      enabled=true
      uri="http://configserver:9000"
      name="filename1,filename2"
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
spring {
  cloud {
    config {
      enabled = true
      uri = "http://configserver:9000"
      name = "filename1,filename2"
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  spring {
    cloud {
      config {
        enabled = true
        uri = "http://configserver:9000"
        name = "filename1,filename2"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "spring": {
    "cloud": {
      "config": {
        "enabled": true,
        "uri": "http://configserver:9000",
        "name": "filename1,filename2"
      }
    }
  }
}

The field name is optional, if it’s not informed, the value in micronaut.application.name will be used.

6 Repository

You can find the source code of this project in this repository: