In this lab, our objective is to eliminate the hard-coding of the fortune service url from the configuration of the greeting application, or for that matter, from any other client of the fortune service. We want services to be discoverable. This is especially relevant in cloud environments where, unlike traditional server environments, service instances come and go and their location does not necessarily have a fixed url.

We’re going to use the Spring Cloud Netflix Eureka project.

In our scenario, there will be three actors:

  1. The fortune application will be the service that needs to be discovered

  2. We’ll introduce a new Spring Cloud service, the Eureka server, to enable discovery

  3. The greeting application will be known as the client

1. Create and Configure the Server

Eureka servers are intended to run as a cluster, where each member of the cluster is a client of the other members, or peers. All peers register with each other and send each other heartbeats. In this way, if one of the servers goes down, we don’t lose any of the instance registrations. And so, even Eureka servers have client configurations.

In this lab we’re going to run a single Eureka server. This is called Standalone mode, as opposed to peer awareness mode. In order to run our server in standalone mode, we’ll need to instruct the server not to do what it would naturally do: seek out and register with other server instances:

server:
  port: 8761 (1)

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false (2)
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/ (3)
1 Eureka server will listen on port 8761, the default port number for Eureka
2 To run in standalone mode, this server should not attempt to register with or fetch registry information from peers.
3 The url of the Eureka server

Armed with this information, begin to setup the Eureka server:

  1. Visit the Spring Initializr and configure yourself a spring boot project as shown below

    initializr eureka
  2. Unzip the downloaded file, load the project into your IDE

  3. Replace the application.properties file with an application.yml file whose contents match the yaml shown at the beginning of this section (customize port, configure eureka for standalone mode)

  4. Open the main Application class, EurekaServerApplication, and annotate the class with @EnableEurekaServer:

    @@ -2,8 +2,10 @@ package io.pivotal.training.springcloud.eurekaserver;
    
     import org.springframework.boot.SpringApplication;
     import org.springframework.boot.autoconfigure.SpringBootApplication;
    +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    
     @SpringBootApplication
    +@EnableEurekaServer
     public class EurekaServerApplication {
    
       public static void main(String[] args) {
  5. Start up the server as usual, with gradle bootRun or using your IDE

Once the server starts up, visit its home page at http://localhost:8761/. Here’s a screenshot:

eureka dashboard init

Notice that, as expected, at present there are no instances registered with Eureka. It’s worth noting another endpoint that serves a "raw" xml version of the registry at http://localhost:8761/eureka/apps.

We now have a running eureka server; so far so good. Next, let’s turn our attention to Eureka clients.

2. Terminology: Instance vs Client

In eureka, there’s a clear distinction between the terms instance and client: the instance registers with eureka so it can be discovered, whereas the client is doing the discovery, and contacts the eureka server so it can lookup the urls of registered instances.

Eureka is highly configurable. In this lab, we’ll stick to the defaults, but it’s important to illustrate how the terms instance and client organize the configuration properties. Here are some examples:

  • To configure under what hostname an instance should register with eureka, we use eureka.instance.hostname

  • To configure the frequency a client fetches a new copy of the registry, we use eureka.client.registryFetchIntervalSeconds

  • To configure the frequency an instance sends a heartbeat to the server, we use eureka.instance.leaseRenewalIntervalInSeconds

Here is a complete list of configuration options for the client, and for the instance.

3. Configuring a Client

If not configured, clients will by default look for a eureka server on localhost port 8761. This will be our strategy in this lab. For the curious, here’s an example of a client that is explicitly configured with the eureka server’s url:

application.yml
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

The above semantics require explanation..

Eureka was developed by Netflix to operate in AWS, across availability zones.

Each instance usually contacts the eureka server that is present in its availability zone. In the absence of a mapping of instances to availability zones, instances will look for the eureka server in the default availability zone. So, by specifying a defaultZone, we’re essentially saying: here is the url you should connect to when doing either registration or discovery.

4. Configuring fortune service

Eureka makes things easy here: instead of having to code the client ourselves, eureka provides the code necessary for an instance to register with a eureka server. The task boils down to adding the spring-cloud-starter-eureka dependency to the project. Because this is a spring cloud dependency, we must also add the spring cloud maven bom (bill of materials) to the build, as follows:

Add eureka client dependency to fortune service’s build.gradle
@@ -28,9 +28,14 @@ repositories {
   mavenCentral()
 }

+ext {
+  springCloudVersion = 'Edgware.SR2'
+}
+
 dependencyManagement {
   imports {
     mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${contractVerifierVersion}"
+    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
   }
 }

@@ -43,6 +48,8 @@ dependencies {
   compile 'org.springframework.boot:spring-boot-starter-web'
   compile 'org.springframework.boot:spring-boot-starter-actuator'

+  compile 'org.springframework.cloud:spring-cloud-starter-eureka'
+
   runtime 'org.springframework.boot:spring-boot-devtools'

   compileOnly 'org.projectlombok:lombok'
@@ -50,3 +57,4 @@ dependencies {
   testCompile 'org.springframework.boot:spring-boot-starter-test'
   testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
 }

As with the server, we must also explicitly enable eureka discovery with an annotation:

Enable eureka discovery in the fortune service
@@ -2,8 +2,10 @@ package io.pivotal.training.fortune;

 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

 @SpringBootApplication
+@EnableDiscoveryClient
 public class FortuneApplication {

   public static void main(String[] args) {

⇒ Start up the fortune service application

At this point, it’s worth reading through the startup log for fortune service. Below are the relevant eureka-related log messages:

Setting initial instance status as: STARTING
Initializing Eureka in region us-east-1
Using JSON encoding codec LegacyJacksonJson
Using JSON decoding codec LegacyJacksonJson
Using XML encoding codec XStreamXml
Using XML decoding codec XStreamXml
Resolving eureka endpoints via configuration
Disable delta property : false
Single vip registry refresh property : null
Force full registry fetch : false
Application is null : false
Registered Applications size is zero : true
Application version is -1: true
Getting all instance registry info from the eureka server (1)
The response status is 200
Starting heartbeat executor: renew interval is: 30 (2)
InstanceInfoReplicator onDemand update allowed rate per min is 4
Discovery Client initialized at timestamp 1504488800985 with initial instances count: 0
Registering application fortune with eureka with status UP
Saw local status change event StatusChangeEvent [timestamp=1504488801017, current=UP, previous=STARTING]
DiscoveryClient_FORTUNE/eitans-mbp:fortune:8081: registering service... (3)
DiscoveryClient_FORTUNE/eitans-mbp:fortune:8081 - registration status: 204
1 Fetch the registry from the eureka server
2 The application dedicates a thread to sending heartbeats to the eureka server, to renew registrations every 30 seconds
3 Request sent to register the fortune service with eureka

On the server side, you should see a corresponding log message showing that the fortune app has contacted it and has been registered:

Registered instance FORTUNE/eitans-mbp:fortune:8081 with status UP (replication=false)

Note how discovery clients here act as both client and instance.

  • Re-visit the eureka dashboard at http://localhost:8761/

    There should now be one instance of the FORTUNE application registered.

  • Re-visit the raw xml view of the registry: http://localhost:8761/eureka/apps

    This view should allow you to see all of the metadata associated with the registered application instance. The one we care about here primarily is homePageUrl. But note how there’s also a heathCheckUrl, set to the application’s actuator healthcheck endpoint.

The most important thing to note is that the spring application’s name (literally, the value of the configuration property spring.application.name) is the handle used to look up information about all corresponding service instances.

5. Configuring the greeting application

Make a similar set of edits for the greeting application: add the dependency to the build file, and add the annotation to the main application class.

greeting-app/build.gradle
@@ -31,6 +31,7 @@ dependencies {
   compile 'org.springframework.boot:spring-boot-starter-freemarker'

   compile 'org.springframework.cloud:spring-cloud-starter-hystrix'
+  compile 'org.springframework.cloud:spring-cloud-starter-eureka'

   runtime 'org.springframework.boot:spring-boot-devtools'

+++ greeting-app/src/main/java/io/pivotal/training/GreetingApplication.java
@@ -3,11 +3,13 @@ package io.pivotal.training;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 import org.springframework.context.annotation.Bean;
 import org.springframework.web.client.RestTemplate;

 @SpringBootApplication
 @EnableCircuitBreaker
+@EnableDiscoveryClient
 public class GreetingApplication {

   public static void main(String[] args) {

More interestingly, we can now remove the hard-coding of the fortune service url from the application’s configuration

Greeting application’s application.yml
@@ -9,5 +9,3 @@ management:
 greeting:
   displayFortune: true

-fortuneService:
-  baseUrl:  http://localhost:8081

..and replace it with a eureka API lookup:

Greeting application’s FortuneServiceClient.java
@@ -1,5 +1,7 @@
 package io.pivotal.training.greeting;

+import com.netflix.appinfo.InstanceInfo;
+import com.netflix.discovery.EurekaClient;
 import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
@@ -12,22 +14,27 @@ import java.util.Map;
 @Slf4j
 public class FortuneServiceClient {
   private RestTemplate restTemplate;
+  private EurekaClient eurekaClient;

-  @Value("${fortuneService.baseUrl}")
-  private String baseUrl;
-
-  public FortuneServiceClient(RestTemplate restTemplate) {
+  public FortuneServiceClient(RestTemplate restTemplate, EurekaClient eurekaClient) {
     this.restTemplate = restTemplate;
+    this.eurekaClient = eurekaClient;
   }

   @HystrixCommand(fallbackMethod = "defaultFortune")
   public String getFortune() {
+    String baseUrl = lookupUrlFor("FORTUNE");
     Map<String,String> result = restTemplate.getForObject(baseUrl, Map.class);
     String fortune = result.get("fortune");
     log.info("received fortune '{}'", fortune);
     return fortune;
   }

+  private String lookupUrlFor(String appName) {
+    InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka(appName, false);
+    return instanceInfo.getHomePageUrl();
+  }
+
   public String defaultFortune() {
     log.info("Default fortune used.");
     return "Your future is uncertain";

The above diff speaks for itself. We remove the wiring of the configuration value, and replace it with autowiring of a EurekaClient, through which we can obtain any registered instance’s information, including the homePageUrl.

Finally, stand up the greeting application and make sure that it still works: will it be able to fetch fortunes from the fortune service without knowing its url in advance?

You should see a very similar startup sequence: registration of greeting application with eureka, the fetching of the registry. It’s important to point out that each client keeps a cache of that registry. At lookup time, the application does not need to make a call to eureka.

At this point, go ahead and shutdown all three applications: the greeting application, the fortune service, and the eureka server.

6. One last thing..

Before we proceed to the next lab, make sure that all tests still pass:

  • cd to fortune-service and run gradle clean test

  • cd to greeting-app and run gradle clean test

Does everything pass?

The greeting application’s FortuneServiceClientTests should be failing. I get the following assertion failure:

org.junit.ComparisonFailure:
Expected :"a random fortune"
Actual   :"Your future is uncertain"

The above message is actually a great testament to the fact that hystrix is working: the test is unable to contact the fortune service, and so the hystrix logic falls back to the default fortune.

The reason of course for this behavior is that, although we have a running stub, it’s certainly not registered with eureka and even if it did, the test has no running eureka server to talk to.

A simple solution here is to mock the response from the eureka server, which is surprisingly easy to accomplish (imports omitted below):

+++ greeting-app/src/test/java/io/pivotal/training/greeting/FortuneServiceClientTests.java
@@ -17,8 +26,18 @@ public class FortuneServiceClientTests {

   @Autowired private FortuneServiceClient fortuneServiceClient;

+  @MockBean EurekaClient eurekaClient;
+  @Mock InstanceInfo instanceInfo;
+
   private static final String ExpectedFortune = "a random fortune";

+  @Before
+  public void setup() {
+    initMocks(FortuneServiceClientTests.class);
+    when(instanceInfo.getHomePageUrl()).thenReturn("http://localhost:8081/");
+    when(eurekaClient.getNextServerFromEureka(anyString(), anyBoolean())).thenReturn(instanceInfo);
+  }
+
   @Test
   public void shouldReturnAFortune() {
     assertThat(fortuneServiceClient.getFortune()).isEqualTo(ExpectedFortune);

We’re basically instructing the test: when we lookup the address of the fortune service in eureka, eureka responds with the address of our fortune service stub.

In subsequent labs, we won’t be using the eureka client directly. Instead the logic will be wrapped behind a more generic Spring Cloud API, or a RestTemplate call. In these cases, Spring Cloud Contract will automatically do this eureka mocking on our behalf.

7. Congratulations

We’ve covered a lot of ground here: we’ve introduce a eureka server and now have the ability to dynamically look up the address of running instances of any service instance that’s registered with eureka.

This is extremely powerful, and it makes our system even more resilient than before: we can introduce a new service instance and decommission another, all without disrupting the operation of clients. This foundation also sets the stage for our next lab: giving clients the ability to directly load-balance against multiple running service instances.

Next up, let’s explore load balancing with the Spring Cloud Netflix Ribbon library.