Service Discovery & Registration deals with solving the problems that can occur when Microservices talk to eachother, i.e perform API calls.
In traditional network topology, applications have static network locations. Hence IP addresses of relevant external locations can be read from a configuration file, as these addresses rarely change.
In modern microservices architecture, knowing the right network location of an application is a much more complex problem for the clients as service instances might have dynamically assigned IP addresses. Moreover, the number may vary due to autoscaling and failures.
Microservice Service Discovery & Registration is a way for applications and microservices to locate eachother on a network. This includes:
- A central server (or servers) that maintain a global view of addresses :check:
- Microservices/clients that connect to the central server to register their address when they start & are ready. :check:
- Microservices/clients send their heartbeats at regular intervals to the central server to inform on their health. :check:
Spring Cloud makes Service Discovery & Registry easy with the help of the below components:
-
Spring Cloud Netflix's Eureka service which will act as a service discovery agent*
-
Spring Cloud Load Balancer library for client-side load balancing**
-
Netflix Feign client to look up services between microservices.
- We use Eureka because it's widely used, but there are other popular service registries such as Consul, Apache Zookeeper, and etcd
** Netflix Ribbon used to be the go-to fo load balacning, but it is currently in maintenence mode, so we will use Spring Cloud Load Balancer.
-
Generate a new Spring Starter Project in the same IDE as your other services > name it
eurekaserver
> group:com.revature
> package:com.revature.eurekaserver
-
Add these dependencies:
- Spring Boot Actuator
- Eureka Server
- Spring Cloud Config Client
- Modify the
pom.xml
to exclude Netflx Ribbon. Add the<exclusions>
featured below within the Spring Cloud Eureka Server dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</exclusion>
<exclusion>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</exclusion>
</exclusions>
</dependency>
- Under the Maven plugin, add the
<image>
tag to auto-generate a Docker file with Buildpacks:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>sophiagavrila/${project.artifactId}</name>
</image>
</configuration>
</plugin>
</plugins>
</build>
- Add the
@EnableEurekaServer
annotation abovecom.revature.eurekaserver.EurekaServerAlication.java
/**
* @EnableEurekaServer makes our Spring Boot microservice
* act as a Service Discovery agent
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaserverApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}
- In
application.properties
, add the following:
# Properties necessary for Config Server
# This is the name used to identify this service in configserver
spring.application.name=eurekaserver
spring.config.import=optional:configserver:http:https://localhost:8071
# Add this to confirm that we are not using Ribbon which is deprecated
spring.cloud.loadbalancer.ribbon.enabled=false
-
Add the
eurekaserver
proeprties to the Config Server Git Repository here ~ or you can just call my git repository as your config repo -
Test it! Start the
configserver
first, then starteurekaserver
> Go tohttp:https://localhost:8070/
(fwded port from config)
You will see a dashboard of all instances of a service that's up (none right now)
Each microservice can register itself to Eureka service discovery and send a heartbeat.
-
Start with
accounts
> openpom.xml
and add the following dependencies:- Eureka Discovery Client
- OpenFeign
-
Go to
application.properties
and add the properties to connect witheurekaserver
:
# In the case that the IP address for this container changes
eureka.instance.preferIpAddress = true
# Go ahead and register with Eureka
eureka.client.registerWithEureka = true
# Fetch all registry details
eureka.client.fetchRegistry = true
eureka.client.serviceUrl.defaultZone = http:https://localhost:8070/eureka/
## Configuring info endpoint for actuator
info.app.name=Accounts Microservice
info.app.description=Bank Accounts Application
info.app.version=1.0.0
# Enable the service to shutdown gracefully
# Expose this endpoint to acutator
endpoints.shutdown.enabled=true
management.endpoint.shutdown.enabled=true
-
Start the
configserver
, theneurkekaserver
, thehnaccounts
> Go tolocalhost:8080/acutator/info
and you will see the properties that you have set asinfo
endpoints inaccounts
application.properties
. -
Go to
localhost:8070
> you should see your account instnace is up! -
Do the same for
cards
andloans
microservices.- Add Eureka Discovery Client (not OpenFeign) dependency to pom.xml
- Add acutator end points + eureka config info to
application.properties
-
run config > eureka > all microservices. Additioanlly you can navigate to
localhost:8070/eureka/apps/__(service)__
to view info about that instanceAdditionally you can change the request header to
Accepts = "application/json"
in Postman. -
Similarly, you can deregister instances by calling it's port + actuator + shutdown +
localhost:9000/actuator/shutdown
Run all apps, kill the eureka server (you will see that all microservices are trying to send a heartbeat every 30 seconds but are unable to find an active Discovery service).
This is how microservices use Eureka data and Netflix Open Feign Client to communicate with other microservices. You must follow client-side load balancing if possible. Feign Client allows microservices to talk with eachother without knowing exact location details of eachother.
We will build a new API path inside accounts
which will be exposed to UI application. This myCustomerDetails
path will provide a single view of all cards, loans, and accounts. It will use Open Feign Client to invoke cards and loans.
-
Open Feign dependency is only in
accounts
app because it will be invoking info from the other services -
Insert
@EnableFeignClients
annotation ontop ofAccountsApplication
class:
@SpringBootApplication
@EnableFeignClients
public class AccountsApplication {
public static void main(String[] args) {
SpringApplication.run(AccountsApplication.class, args);
}
}
- In
accoutns
create a new package calledcom.revature.accounts.service.client
> Here we will make 2 classes:CardsFeignClient
&LoansFeignClient
. These are interfacesWe use these to invoke
cards
&loans
busines logic withinaccounts
:
CardsFeignClient.java
/**
* This interface allows accounts to invoke a controller
* method within the cards microservice.
*/
@FeignClient("cards") // use the application name that's registered in Eureka Server
public interface CardsFeignClient {
// Indicate the path that you want to invoke (within cards' controller)
@RequestMapping(method = RequestMethod.POST, value = "myCards", consumes = "application/json")
List<Cards> getCardDetails(@RequestBody Customer customer); // pass a customer obj, extract id, retrieve cards details
}
LoansFeignCLient.java
@FeignClient("loans")
public interface LoansFeignClient {
@RequestMapping(method = RequestMethod.POST, value = "myLoans", consumes = "application/json")
List<Loans> getLoansDetails(@RequestBody Customer customer);
}
-
Go to accounts controller. Autowire both
FeignClient
interfaces to the controller. -
Write a method that exposes an API path called
/myCustomerDetails
which passes a Customer as the Request Body and invokes bothcards
andloans
client to return information. YourAccountsController
should now look like this:
@RestController
public class AccountsController {
@Autowired
private AccountsRepository accountsRepository;
@Autowired
AccountsServiceConfig accountsConfig;
@Autowired
LoansFeignClient loansFeignClient;
@Autowired
CardsFeignClient cardsFeignClient;
/**
* Passes customer object as parameter in HTTP Request body and returns Account
* object based on account found by that cusomter's ID.
*/
@PostMapping("/myAccount")
public Accounts getAccountDetails(@RequestBody Customer customer) {
Accounts accounts = accountsRepository.findByCustomerId(customer.getCustomerId());
if (accounts != null) {
return accounts;
} else {
return null;
}
}
/**
* This method will return all properties configured for this service from the
* auto-wired AccountsServiceConfig in JSON format to the client.
*/
@GetMapping("/account/properties")
public String getPropertyDetails() throws JsonProcessingException {
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
Properties properties = new Properties(accountsConfig.getMsg(), accountsConfig.getBuildVersion(),
accountsConfig.getMailDetails(), accountsConfig.getActiveBranches());
String jsonStr = ow.writeValueAsString(properties);
return jsonStr;
}
/**
* Passes Customer object as localhost:8080/myCustomerDetails.
* Customer obj is passed as method to both cards and loans controllers
* by using FeignClient to invoke other microservices and return details.
*/
@PostMapping("/myCustomerDetails")
public CustomerDetails myCustomerDetails(@RequestBody Customer customer) {
Accounts accounts = accountsRepository.findByCustomerId(customer.getCustomerId());
List<Loans> loans = loansFeignClient.getLoansDetails(customer);
List<Cards> cards = cardsFeignClient.getCardDetails(customer);
CustomerDetails customerDetails = new CustomerDetails();
customerDetails.setAccounts(accounts);
customerDetails.setLoans(loans);
customerDetails.setCards(cards);
return customerDetails;
}
}
It is the responsibility of Eureka Server to find the loans
and cards
services > It will get instance details of loans
and cards
when we send the first request and cache locally. It will also do load balancing through Spring Cloud LoadBalancing
- Start configserver > eurekaserver > accounts > cards > loans. With Postman, make a POST request to
localhost:8080/myCustomerDetails
with{"customerId" : 1}
as the Request Body > this will return all details.
-
Go to root folder where all services are present > open terminal. We have to generate a Dockerfile for
accounts
the old way. Open a terminal withinaccounts
-
First we need an
accounts
JAR file. Tell maven you're not unit testing. Run:mvn clean install -Dmaven.test.skip=true
-
Run:
docker build . -t sophiagavrila/accounts
. -
Generate one for
cards
andloans
, but faster:cd
intocards
and runmvn spring-boot:build-image -Dmaven.test.skip=true
> do the same inloans
. -
configserver
is up to date > we just need to generate a Docker image foreurekaserver
> run:mvn spring-boot:build-image -Dmaven.test.skip=true
-
Run
docker images
and do some cleanup to remove old images (docker rmi <image-id> -f
)
- Push all images with
docker push sophiagavrila/accounts
(repeat for all 4 images - not configserver)
-
Create a
eurekaserivce
service underconfigserver
. -
In the
accounts
service section, addeurekaserver
as a dependency, and add it's environment variable. -
Add
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http:https://eurekaserver:8070/eureka/
to all services declared in coker compose file.
docker-compose.yml
should look like this:
version: "3.8"
services:
configserver:
image: sophiagavrila/configserver:latest
mem_limit: 700m
ports:
- "8071:8071"
networks:
- bank
eurekaserver:
image: sophiagavrila/eurekaserver:latest
mem_limit: 700m
ports:
- "8070:8070"
networks:
- bank
depends_on:
- configserver
# Incase config server is not started, set a restart policy and try again
deploy:
restart_policy:
condition: on-failure
delay: 15s
max_attempts: 3
window: 120s
environment:
SPRING_PROFILES_ACTIVE: default
# Tells docker where the config server location is that we can connect
SPRING_CONFIG_IMPORT: configserver:http:https://configserver:8071/
accounts:
image: sophiagavrila/accounts
mem_limit: 700m
ports:
- "8080:8080"
networks:
- bank
# Docker Compose will ensure that config is started first
depends_on:
- configserver
- eurekaserver
# Deploy configurations delays accounts before it makes requests to configserver
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
# Here we are overriding application.properties of the service
environment:
SPRING_PROFILES_ACTIVE: default
# Make sure we connect to configserver even if it is not on localhost
SPRING_CONFIG_IMPORT: configserver:http:https://configserver:8071/
# Tell docker where Eureka is so it can register it
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http:https://eurekaserver:8070/eureka/
loans:
image: sophiagavrila/loans
mem_limit: 700m
ports:
- "8090:8090"
networks:
- bank
depends_on:
- configserver
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
environment:
SPRING_PROFILES_ACTIVE: default
SPRING_CONFIG_IMPORT: configserver:http:https://configserver:8071/
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http:https://eurekaserver:8070/eureka/
cards:
image: sophiagavrila/cards
mem_limit: 700m
ports:
- "9000:9000"
networks:
- bank
depends_on:
- configserver
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
environment:
SPRING_PROFILES_ACTIVE: default
SPRING_CONFIG_IMPORT: configserver:http:https://configserver:8071/
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http:https://eurekaserver:8070/eureka/
networks:
bank:
- Do the same to
dev
andprod
docker-compose files.
-
Open a terminal in
accounts/docker-compose/default
> run:docker compose up -d
-
You can also navigate to
localhost:8070
to check eureka's registry of instances -
Send a POST request in Postman to
localhot:8080/myCustomerDetails
to confirm Feign Client.
-
Add one more accounts serve, append a 1 to it's name, add 12s delay for eureka, add 30s delay for accounts 1
-
and change the port exposed to your local system in
docker-compose.yml
(start withdefault
) and replicate this in all env settings.
Add this under the first acccount service:
accounts:
image: sophiagavrila/accounts
mem_limit: 700m
ports:
- "8080:8080"
networks:
- bank
# Docker Compose will ensure that config is started first
depends_on:
- configserver
- eurekaserver
# Deploy configurations delays accounts before it makes requests to configserver
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
# Here we are overriding application.properties of the service
environment:
SPRING_PROFILES_ACTIVE: default
# Make sure we connect to configserver even if it is not on localhost
SPRING_CONFIG_IMPORT: configserver:http:https://configserver:8071/
# Tell docker where Eureka is so it can register it
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http:https://eurekaserver:8070/eureka/
-
Run
docker compose up
> go tolocalhost:8070
to view the 2 Accounts instances on Eureka -
You will notice that your system is significantly slower - you can go ahead and revert all the changes in your
docker-compose.yml
file so we're only running one instance.