In most microservice implementations, internal microservice endpoints are not exposed outside. They are kept as private services. A set of public services will be exposed to the clients using an API gateway. There are many reasons to do this:
Zuul is a simple gateway service or edge service that suits these situations well. Zuul also comes from the Netflix family of microservice products. Unlike many enterprise API gateway products, Zuul provides complete control for the developers to configure or program based on specific requirements:
The Zuul proxy internally uses the Eureka server for service discovery, and Ribbon for load balancing between service instances.
The Zuul proxy is also capable of routing, monitoring, managing resiliency, security, and so on. In simple terms, we can consider Zuul a reverse proxy service. With Zuul, we can even change the behaviors of the underlying services by overriding them at the API layer.
Unlike the Eureka server and the Config server, in typical deployments, Zuul is specific to a microservice. However, there are deployments in which one API gateway covers many microservices. In this case, we are going to add Zuul for each of our microservices: Search, Booking, Fare, and Check-in:
The project structure for search-apigateway
is shown in the following diagram:
search-apigateway.property
file with the contents given next, and commit to the Git repository.This configuration also sets a rule on how to forward traffic. In this case, any request coming on the /api
endpoint of the API gateway should be sent to search-service
:
spring.application.name=search-apigateway zuul.routes.search-apigateway.serviceId=search-service zuul.routes.search-apigateway.path=/api/** eureka.client.serviceUrl.defaultZone:http://localhost:8761/eureka/
search-service
is the service ID of the Search service, and it will be resolved using the Eureka server.
bootstrap.properties
file of search-apigateway
as follows. There is nothing new in this configuration—a name to the service, the port, and the Config server URL:spring.application.name=search-apigateway server.port=8095 spring.cloud.config.uri=http://localhost:8888
Application.java
. In this case, the package name and the class name are also changed to com.brownfield.pss.search.apigateway
and SearchApiGateway
respectively. Also add @EnableZuulProxy
to tell Spring Boot that this is a Zuul proxy:@EnableZuulProxy @EnableDiscoveryClient @SpringBootApplication public class SearchApiGateway {
CommandLineRunner
as well as BrownFieldSiteController
to make use of the API gateway:Flight[] flights = searchClient.postForObject("http://search-apigateway/api/search/get", searchQuery, Flight[].class);
In this case, the Zuul proxy acts as a reverse proxy which proxies all microservice endpoints to consumers. In the preceding example, the Zuul proxy does not add much value, as we just pass through the incoming requests to the corresponding backend service.
Zuul is particularly useful when we have one or more requirements like the following:
Zuul also provides a number of filters. These filters are classified as pre filters, routing filters, post filters, and error filters. As the names indicate, these are applied at different stages of the life cycle of a service call. Zuul also provides an option for developers to write custom filters. In order to write a custom filter, extend from the abstract ZuulFilter
, and implement the following methods:
public class CustomZuulFilter extends ZuulFilter{ public Object run(){} public boolean shouldFilter(){} public int filterOrder(){} public String filterType(){}
Once a custom filter is implemented, add that class to the main context. In our example case, add this to the SearchApiGateway
class as follows:
@Bean public CustomZuulFilter customFilter() { return new CustomZuulFilter(); }
As mentioned earlier, the Zuul proxy is a Spring Boot service. We can customize the gateway programmatically in the way we want. As shown in the following code, we can add custom endpoints to the gateway, which, in turn, can call the backend services:
@RestController class SearchAPIGatewayController { @RequestMapping("/") String greet(HttpServletRequest req){ return "<H1>Search Gateway Powered By Zuul</H1>"; } }
In the preceding case, it just adds a new endpoint, and returns a value from the gateway. We can further use @Loadbalanced RestTemplate
to call a backend service. Since we have full control, we can do transformations, data aggregation, and so on. We can also use the Eureka APIs to get the server list, and implement completely independent load-balancing or traffic-shaping mechanisms instead of the out-of-the-box load balancing features provided by Ribbon.
Zuul is just a stateless service with an HTTP endpoint, hence, we can have as many Zuul instances as we need. There is no affinity or stickiness required. However, the availability of Zuul is extremely critical as all traffic from the consumer to the provider flows through the Zuul proxy. However, the elastic scaling requirements are not as critical as the backend microservices where all the heavy lifting happens.
The high availability architecture of Zuul is determined by the scenario in which we are using Zuul. The typical usage scenarios are:
In some cases, the client may not have the capabilities to use the Eureka client libraries, for example, a legacy application written on PL/SQL. In some cases, organization policies do not allow Internet clients to handle client-side load balancing. In the case of browser-based clients, there are third-party Eureka JavaScript libraries available.
It all boils down to whether the client is using Eureka client libraries or not. Based on this, there are two ways we can set up Zuul for high availability.
In this case, since the client is also another Eureka client, Zuul can be configured just like other microservices. Zuul registers itself to Eureka with a service ID. The clients then use Eureka and the service ID to resolve Zuul instances:
As shown in the preceding diagram, Zuul services register themselves with Eureka with a service ID, search-apigateway
in our case. The Eureka client asks for the server list with the ID search-apigateway
. The Eureka server returns the list of servers based on the current Zuul topology. The Eureka client, based on this list picks up one of the servers, and initiates the call.
As we saw earlier, the client uses the service ID to resolve the Zuul instance. In the following case, search-apigateway
is the Zuul instance ID registered with Eureka:
Flight[] flights = searchClient.postForObject("http://search-apigateway/api/search/get", searchQuery, Flight[].class);
In this case, the client is not capable of handling load balancing by using the Eureka server. As shown in the following diagram, the client sends the request to a load balancer, which in turn identifies the right Zuul service instance. The Zuul instance, in this case, will be running behind a load balancer such as HAProxy or a hardware load balancer like NetScaler:
The microservices will still be load balanced by Zuul using the Eureka server.
In order to complete this exercise, add API gateway projects (name them as *-apigateway
) for all our microservices. The following steps are required to achieve this task:
application.properties
to bootstrap.properties
, and add the required configurations.@EnableZuulProxy
to Application.java
in each of the *-apigateway
projects.@EnableDiscoveryClient
in all the Application.java
files under each of the *-apigateway
projects.In the end, we will have the following API gateway projects:
chapter5.fares-apigateway
chapter5.search-apigateway
chapter5.checkin-apigateway
chapter5.book-apigateway