In the Part 3 of these article series (http://nicolasduminil.blogspirit.com/archive/2018/06/08/basic-of-microservices-part-3-3106892.html), we demonstrated how to load-balance between micro-services instances, using he Netfkix Eureka service. This way we achieved one of the most important goals of the Service Orientated Architecture: the services virtualization and their location transparency. The Part 4 (http://nicolasduminil.blogspirit.com/archive/2018/06/14/microservices-basics-part-4-the-netflix-hystrix-service-3107106.html) showed how to use the Netflix Hystrix service in order to improve microservices resilience.
Now it is time to consider some cross-cutting concerns like security, logging and tracing. While it is possible to implement these cross-cutting concerns individually, for each microservice, it would be much better if we would have a unique place where we could do it, only once, for all our microservices. Enter the Netflix Zuul.
Netflix Zuul is a so-called API Gateway, i.e. an intermediary component sitting between micrsoervices and their consumers.
Until now, while testing our microservices, we have either called them directly or via the Eureka discovery service.
A microservices gateway is a mediator between the microservices and their consumers. This way we have a single URL that the consumers call and this provides us with one of the Graal of the SOA which is the location transparency. Should the location of our services change, and this is something which happens more often then we might think it does, the consumers will consider to work as expected, which wouldn't have been the case if the consumers would have consumed the services directly, without a gateway.
Another considerable advantage when using a microservices gateway is that it acts as a single, central, policy enforcement point. Hence, should we want to secure the access to the microservices, or should we want to perform advanced logging or tracing, this is the point where we can do it without affecting the global design.
Spring Cloud integrates with the Zuul Netflix project. This is an open source project providing a microservices gateway such the one we described above. It is very simple to set it up via Spring Cloud annotations, as follows.
Our sample microservice was initially consumed directly (in the Part 1 of the article series). In this scenario, our consumer used the microservice effective URL in order to invoke it. Later, in the Part 3, we added a 2nd instance of our sample microservice and then we couldn't consume them directly as, for doing that, our consumer would have to "know" that there are several instances of the microservice and to choose between these instances the one to be called. And as this cannot be an option, we introduced the Eureka autodiscovery service. This service is designed, as seen in the Part 3, such that to inspect the running services and, by interacting with them, to build a services map with their associated status. Hence, consumers just invoke the Eureka autodiscovery service URL, by specifying the generic name of the microservice to be called and the requests are forwarded to the most appropriated running instance of this microservice.
Using the Eureka autodiscovery service provided us with things like services virtualization and location transparency. But we need more then that. We need a single and centralized policy enforcing point where we could apply security or logging/tracing policies, without touching the microservcices.
In order to do that, we modified our project such that to add a new maven module, named ms-routing. Netflix Zuul is completely integrated with Spring Cloud and, in order to use this integration, we need to include the following dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
Next, we implemented the Zuul gateway service as a Spring Boot application which code is presented below:
package fr.simplex_software.micro_services.routing;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.context.config.annotation.*;
import org.springframework.cloud.netflix.eureka.*;
import org.springframework.cloud.netflix.zuul.*;
import org.springframework.context.annotation.*;
import org.springframework.web.client.*;
@SpringBootApplication
@EnableZuulProxy
@RefreshScope
public class RoutingServerApplication
{
@LoadBalanced
@ Bean
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
public static void main(String[] args)
{
SpringApplication.run(RoutingServerApplication.class, args);
}
}
That’s all we need in order to define a gateway service. This code will run an embedded tomcat container having deployed in it a microservice named RoutingServerApplication. This microservice is our gateway service, the one that the consumers will call in order to get the target endpoints. Here is how:
ms-routing:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
- ms-discovery
container_name: ms-routing
links:
- ms-config:ms-config
- ms-discovery:ms-discovery
volumes:
- ../../ms-routing/src/main/docker:/usr/local/share/hml
ports:
- "9090:9090"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-routing
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
DISCOVERY_PORT: "8761"
CONFIGSERVER_LABEL: "routing"
SERVER_PORT: "9090"
DISCOVERYSERVER_URI: "http://ms-discovery:8761/eureka/"
The fragment above comes from the docker-compose.yml file, the one which describes the docker containers chain. Here we spin on a docker container running the Alpine Linux distribution with Java 8. On the top of it we run a Tomcat 8 embedded servlet engine which, in turn, will run our Spring Boot microservice. By simply annotating this service with the @EnableZuulProxy annotation, this service becomes our service gateway.
The container has the name of ms-routing and depends, of course, of the config and discovery services. It mounts a writable volume and listens of the 9090 TCP port number. It defines environment variables such that to identify the config server and the discovery server and, upon start-up, it runs the run.sh script which code follows:
#!/bin/sh
echo "********************************************************"
echo "Waiting for the configuration server to start on port $CONFIGSERVER_PORT"
echo "********************************************************"
while ! `nc -z ms-config $CONFIGSERVER_PORT `; do sleep 3; done
echo ">>>>>>>>>>>> Configuration Server has started"
echo "********************************************************"
echo "Waiting for the discovery server to start on port $DISCOVERY_PORT"
echo "********************************************************"
while ! `nc -z ms-discovery $DISCOVERY_PORT `; do sleep 3; done
echo ">>>>>>>>>>>> Discovery Server has started "
echo "********************************************************"
echo "Starting Routing Service with config on $CONFIGSERVER_URI"
echo "********************************************************"
java -Dserver.port=$SERVER_PORT -Dspring.cloud.config.uri=$CONFIGSERVER_URI -Deureka.client.serviceUrl.defaultZone=$DISCOVERYSERVER_URI -Dspring.profiles.active=$PROFILE -Dspring.cloud.config.label=$CONFIGSERVER_LABEL -jar /usr/local/share/hml/ms-routing.jar
As we can see, before it starts the gateway service is waiting for the configuration service and the discovery service to start. The Zuul gateway may be used without the the Eureka discovery service but, in this case, the architecture is less flexible. Here is the configuration of the Zuul gateway (bootstrap.yml):
spring:
application:
name: hml-routing
management:
security:
enabled: false
eureka:
instance:
preferIpAddress: "true"
The Zuul gateway service is configured such that to interact with the Eureka discovery service in order to have the microservices instances access details.
The only thing that we still have to do is to modify our test consumer such that to call the gateway service instead of the discovery one, as shown below:
ResponseEntity resp = restTemplate.postForEntity("http://hml-routing/hml-core/api/subscribe/", request, Void.class);
assertEquals(resp.getStatusCode(), HttpStatus.ACCEPTED);
HttpEntity<HmlEvent> request2 = new HttpEntity<>(new HmlEvent("subscriptionName", "messageId", "payload"));
HmlEvent hmle = restTemplate.postForObject("http://hml-routing/hml-core/api/publish/", request2, HmlEvent.class);
assertThat(hmle, notNullValue());
assertEquals(hmle.getMessageId(), "messageId");
assertEquals(hmle.getPayload(), "payload");
Here the new URL that the consumer uses is based on the Zuul gateway URI (hml-routing), that the discovery service assign to it, followed by the generic URI of the sample microservice, assigned also by the discovery service. At the runtime, the Zuul gateway service is responsible of chosing the effective instance that will be called in order to satisfy the request. Please compare the resulting URL with the one used in Part 3 and 4.
In order to build and run the whole stuff you need to proceed as you already did previously. First clone the GIT repository, as follows:
mkdir ms-core-config-discovery-resilience-routing
cd ms-core-config-discovery-resilience-routing
git clone https://github.com/nicolasduminil/micro-services.git
git checkout routing
Then build the project:
mvn -DskipTests clean install
Run the docker containers:
docker-compose -f docker/common/docker-compose.yml up
and run the test:
mvn --pl ms-core test
Congratulations, just by running this simple docker-compose command, you got a very complex environment, running six docker containers, each one hosting a full Linux OS. Enjoy !