Microservices Registration and Discovery using Spring Cloud, Eureka

This blog post drives though an implementation pattern and a working setup with multiple microservices where they get registered with Eureka and the client can discover them and invoke them

What is Eureka

Eureka is a REST-based service that provides support for discovering other services so that clients can use the Eureka service to discover services and calling those services.

Why we need Discovery Service

In a Microservices architecture where we have multiple services running, we need a mechanism to know what are the services and how to invoke them. Each service typically will have a different URL or IP address to reach out to. Also, clients of those microservices need to know when a new


microservice is added or an existing microservice was taken down for maintenance. Discovery service provides that mechanism, where services can register themself in the discovery service and clients, can query the discovery service to know more about the available microservices.

Project achievement

In this project, we will go through an end to end use case where we will have a client microservice calling another microservice utilizing Eureka discovery service. Essentially we want to find existing services by looking up in discovery service and then invoking the target service without knowing about their whereabouts like URL, or IP or port, etc. For that, we will build a discovery server one service microservice and one client microservice.
Project Contents
  • Eureka Discovery Service: Provides the registration and discovery
  • Article Microservice: Provides REST services for Article CRUD operation
  • Client Microservice: Invokes the article microservice



Above pictures show what we are going to achieve in very high level
1) We will deploy a Discovery service
2) We will deploy Article Service which registers itself with discovery service with a unique name.
3) We will deploy a consumer application which is another spring boot application which will invoke the
     Article Service by its name.

Technology Used

Java 11
Apache Maven 3.5.0
Spring Boot 2.2.6
Spring Cloud Hoxton.SR1
Netflix Ribbon 2.3.0
Open Feign
JSON

Discovery Server

The following section highlights the important code related to Eureka Server. The details can be looked into in the git repo. Mainly we need to add the maven dependency for Eureka Discovery Server, in the spring boot properties file add the server port and defaultZone and annotate the SpringBoot Application class with @EnableEurekaServer.
We need to include the Netflix Eureka Server dependency in the maven pom file.
pom.xml
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
application.properties
server.port=8761

##Eureka Related
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
eureka.client.healthcheck.enabled=true
The Eureka Discovery Server is a Spring Boot application that is very simple. We need to annotate the application with @EnableEurekaServer. @EnableEurekaServer allows this application to run as a Eureka server, aided by Spring Boot to instantiate Eureka-related beans based on configuration properties.
ServiceDiscovery.java
package com.bootng.service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class ServiceDiscovery {
  public static void main(String[] args) {
    SpringApplication.run(ServiceDiscovery.class, args);
  }
}
Now when we build and start the discovery server and access the server through http://localhost:8761 we would see something like bellow. Notice the following screenshots shows no entries under applications because no other services are up yet.


Discovery Service UI


Article Service

Now we will build and deploy the Article microservice which is another spring boot application that registers itself with the discovery service. The name with which this service is registered is "article-service" and is defined in bootstrap.properties file.
pom.xml
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.properties
## Server Related
server.port=9051
## Eureka Related
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=false
eureka.vipAddress=article-service
bootstrap.properties
spring.application.name=article-service
Following is the main application class representing Article Microservice.
ArticleMicroservice
@ComponentScan({ "com.siddb" })
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ArticleMicroservice {
	public static void main(String args[]) {
		SpringApplication.run(ArticleMicroservice.class, args);
	}
}
This Spring Boot Application exposes the following REST endpoints through the ArticleController.java class. And registers itself with the service discovery with the name "article-service"
ArticleController.java
@RestController
@RequestMapping("/api")
public class ArticleController {
  private static final Logger log = LoggerFactory.getLogger(ArticleController.class);
  @Autowired
  ArticleService articleService;

  @RequestMapping(value = {"/articles"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<List<Article>> getArticles() throws AppException {
    List<Article> articles = articleService.getArticles();
    return new ResponseEntity<List<Article>>(articles, HttpStatus.OK);
  }

  @RequestMapping(value = {"/articles/{id}"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Article> getArticle(@PathVariable(value = "") String id)
      throws AppException, NotFoundException {
    Article articles = articleService.getByID(id);
    return new ResponseEntity<Article>(articles, HttpStatus.OK);
  }
 //other methods for POST and Deleted removed from here for readability.
}
REST Endpoints
  • GET http://localhost:9051/api/articles
  • GET http://localhost:9051/api/articles/ARTICLE_ID
  • POST http://localhost:9051/api/articles PAYLOAD
  • DELETE http://localhost:9051/api/articles/ARTICLE_ID

Sample Response

GET http://localhost:9051/api/articles
[
  {
    "id": "America-Travel",
    "name": "America Travel",
    "description": "Places to travel in AmericaLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum",
    "category": "Travel"
  }
]
GET http://localhost:9051/api/articles
Sample Response: [ { "id": "America-Travel", "name": "America Travel", "description": "Places to travel in AmericaLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "category": "Travel" } ]
[ { "id": "America-Travel", "name": "America Travel", "description": "Places to travel in AmericaLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "category": "Travel" } ]

Article Client Service

Now we will build and deploy the Client Service application which is another Spring Boot application that consumes the REST services deployed by Article Service. Most interesting part
pom.xml
<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-loadbalancer</artifactId>
	</dependency>
	<!--ribbon related -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-openfeign</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
</dependencies>
application.properties
hostname=localhost
logging.level.org.springframework=info
logging.level.root=info
spring.main.banner-mode=off
server.port=9053

##Management
management.endpoint.health.show-details
management.health.key.ping=enabled
management.endpoints.web.exposure.include=info,health,mappings

##Eureka Related
eureka.instance.leaseRenewalIntervalInSeconds=1
eureka.instance.leaseExpirationDurationInSeconds=90
eureka.instance.hostname=${hostname}
eureka.instance.hostname.metadataMap.instanceId=${spring.application.name}:${server.port}
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
eureka.client.healthcheck.enabled=true

##Eureka Ribbon
article-service-consumer.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
article-service-consumer.ribbon.DeploymentContextBasedVipAddresses=article-service
Following is the Consumer Application Class. We have annotated it with @EnableDiscoveryClient so that it can discover other services registered with the Discovery Server.
ConsumerApplication
@ComponentScan({"com.siddb", "com.siddb.controller"})
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {
  public static void main(String args[]) {
    SpringApplication.run(ConsumerApplication.class, args);
  }
}
Following is the main controller which uses the discovery service internally and invokes services on the service article-service using ribbon. From the ribbon configuration in (application.properties), it knows article-service-consumer is tied to a service with VIP Address (name) article-service.
RibbonController.java
@RestController
public class RibbonController {

  @Autowired
  private RestTemplate restTemplate;

  /**
   * This method calls the microservice GET article-service/api/articles using Ribbon to get a list of
   * Articles and returns the same. it uses RestTemplate to make the API calls.
   * @return
   * @throws Exception
   */
  @RequestMapping(value = {"render"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<String> renderArticles() throws Exception {
    JsonNode repos =
        restTemplate.getForObject("http://article-service-consumer/api/articles", JsonNode.class);

    int counter = 1;
    StringBuilder result = new StringBuilder("\n List of Articles");
    if (repos.isArray()) {
      for (JsonNode jsonNode : repos) {
        result.append("\n Repo ").append(counter++).append("::");
        result.append(jsonNode.get("name").asText());
      }
    }
    return new ResponseEntity<String>(result.toString(), HttpStatus.OK);
  }
}
The Consumer Controller uses the ribbon configuration that we have added in the property file to invoke the article service by the ribbon name "article-service-consumer". This is where the Consumer controller will discover the actual API for the corresponding service name. And call GET http://article-service-consumer/api/articles will be actually to http://localhost:9051/api/articles. This is how using the Eureka Discovery service the client becomes independent of the service (IP address, port, etc.). The client simply can refer to the service with its published name.

Start Microservices

Now the fun part, start all the microservices and invoke the /render REST API. We need to start all services (discovery service, article service, consumer service). For that, we first need to check out the repo from git and build/run each project.
1) git clone https://github.com/siddharthagit/spring-boot-sdc.
2) go to the root folder that contains this repo.
3) open 3 Terminals one for each subproject (eureka-service-discovery, ms-article-service, ms-consumer-service).
4) build each project and start the services.
mvn clean install
mvn spring-boot:run
5) Check the status of each service on the discovery server http://localhost:8761
6)Invoke the REST API on consumer service http://localhost:9053/render
This API internally will use the discovery server to resolve the service and will route the call to http://localhost:9051/api/articles.
http://localhost:9053/render
List of Articles Repo 1::America Travel Repo 2::Asia Travel Repo 3::Europe Travel Repo 4::Road Trip Repo 5::Winter Travel Repo 6::Japan Travel

Discovery Server UI with all services running


Conclusion

In this article, we've covered how to use Spring Cloud Eureka for service discovery in the microservice/cloud environment. We created two simple REST services that communicate with each other without hardcoding any hostname/port while making REST calls. Though in this article we have shown how the client can invoke the article service with ribbon, the git repo also contains a Feign client that uses Feign and will be covered in a different article.

References