-
Book Overview & Buying
-
Table Of Contents
Microservices with Spring Boot and Spring Cloud - Second Edition
By :
In this chapter, we will learn how to use Spring Cloud Gateway as an edge server, to control what APIs are exposed from our microservices-based system landscape. We will see how microservices that have public APIs are made accessible from the outside through the edge server, while microservices that have private APIs are only accessible from the inside of the microservice landscape. In our system landscape, this means that the product composite service and the discovery server, Netflix Eureka, will be exposed through the edge server. The three core services, product, recommendation, and review, will be hidden from the outside.
The following topics will be covered in this chapter:
For instructions on how to install the tools used in this book and how to access the source code for this book, see:
The code examples in this chapter all come from the source code in $BOOK_HOME/Chapter10.
If you want to view the changes applied to the source code in this chapter, that is, see what it took to add Spring Cloud Gateway as an edge server to the microservices landscape, you can compare it with the source code for Chapter 9, Adding Service Discovery Using Netflix Eureka. You can use your favorite diff tool and compare the two folders, $BOOK_HOME/Chapter09 and $BOOK_HOME/Chapter10.
In this section, we will see how the edge server is added to the system landscape and how it affects the way external clients access the public APIs that the microservices expose. All incoming requests will now be routed through the edge server, as illustrated by the following diagram:

Figure 10.1: Adding an edge server
As we can see from the preceding diagram, external clients send all their requests to the edge server. The edge server can route the incoming requests based on the URL path. For example, requests with a URL that starts with /product-composite/ are routed to the product composite microservice, and a request with a URL that starts with /eureka/ is routed to the discovery server based on Netflix Eureka.
To make the discovery service work with Netflix Eureka, we don't need to expose it through the edge server. The internal services will communicate directly with Netflix Eureka. The reasons for exposing it are to make its web page and API accessible to an operator that needs to check the status of Netflix Eureka, and to see what instances are currently registered in the discovery service.
In Chapter 9, Adding Service Discovery Using Netflix Eureka, we exposed both the product-composite service and the discovery server, Netflix Eureka, to the outside. When we introduce the edge server in this chapter, this will no longer be the case. This is implemented by removing the following port declarations for the two services in the Docker Compose files:
product-composite:
build: microservices/product-composite-service
ports:
- "8080:8080"
eureka:
build: spring-cloud/eureka-server
ports:
- "8761:8761"
With the edge server introduced, we will learn how to set up an edge server based on Spring Cloud Gateway in the next section.
Setting up Spring Cloud Gateway as an edge server is straightforward and can be done with the following steps:
spring-cloud-starter-gateway.spring-cloud-starter-netflix-eureka-client dependency.settings.gradle:
include ':spring-cloud:gateway'
Dockerfile with the same content as for the microservices; see Dockerfile content in the folder $BOOK_HOME/Chapter10/microservices.gateway:
environment:
- SPRING_PROFILES_ACTIVE=docker
build: spring-cloud/gateway
mem_limit: 512m
ports:
- "8080:8080"
From the preceding code, we can see that the edge server exposes port 8080 to the outside of the Docker engine. To control how much memory is required, a memory limit of 512 MB is applied to the edge server, in the same way as we have done for the other microservices.
You can find the source code for the Spring Cloud Gateway in $BOOK_HOME/Chapter10/spring-cloud/gateway.
With an edge server in place, external health check requests also have to go through the edge server. Therefore, the composite health check that checks the status of all microservices has been moved from the product-composite service to the edge server. See Chapter 7, Developing Reactive Microservices – refer to the Adding a health API section for implementation details for the composite health check.
The following has been added to the edge server:
HealthCheckConfiguration class has been added, which declares the reactive health contributor:
@Bean
ReactiveHealthContributor healthcheckMicroservices() {
final Map<String, ReactiveHealthIndicator> registry =
new LinkedHashMap<>();
registry.put("product", () ->
getHealth("http://product"));
registry.put("recommendation", () ->
getHealth("http://recommendation"));
registry.put("review", () ->
getHealth("http://review"));
registry.put("product-composite", () ->
getHealth("http://product-composite"));
return CompositeReactiveHealthContributor.fromMap(registry);
}
private Mono<Health> getHealth(String baseUrl) {
String url = baseUrl + "/actuator/health";
LOG.debug("Setting up a call to the Health API on URL: {}",
url);
return webClient.get().uri(url).retrieve()
.bodyToMono(String.class)
.map(s -> new Health.Builder().up().build())
.onErrorResume(ex ->
Mono.just(new Health.Builder().down(ex).build()))
.log(LOG.getName(), FINE);
}
From the preceding code, we can see that a health check for the product-composite service has been added, instead of the health check used in Chapter 7, Developing Reactive Microservices!
GatewayApplication, declares a WebClient.Builder bean to be used by the implementation of the health indicator as follows:
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
From the preceding source code, we see that WebClient.builder is annotated with @LoadBalanced, which makes it aware of microservice instances registered in the discovery server, Netflix Eureka. Refer to the Service discovery with Netflix Eureka in Spring Cloud section in Chapter 9, Adding Service Discovery Using Netflix Eureka, for details.
With a composite health check in place for the edge server, we are ready to look at the configuration that needs to be set up for the Spring Cloud Gateway.
When it comes to configuring a Spring Cloud Gateway, the most important thing is setting up the routing rules. We also need to set up a few other things in the configuration:
management.endpoint.health.show-details: "ALWAYS"
management.endpoints.web.exposure.include: "*"
logging:
level:
root: INFO
org.springframework.cloud.gateway.route.
RouteDefinitionRouteLocator: INFO
org.springframework.cloud.gateway: TRACE
For the full source code, refer to the configuration file, src/main/resources/application.yml.
Setting up routing rules can be done in two ways: programmatically, using a Java DSL, or by configuration. Using a Java DSL to set up routing rules programmatically can be useful in cases where the rules are stored in external storage, such as a database, or are given at runtime, for example, via a RESTful API or a message sent to the gateway. In more static use cases, I find it more convenient to declare the routes in the configuration file, src/main/resources/application.yml. Separating the routing rules from the Java code makes it possible to update the routing rules without having to deploy a new version of the microservice.
A route is defined by the following:
For a full list of available predicates and filters, refer to the reference documentation: https://cloud.spring.io/spring-cloud-gateway/single/spring-cloud-gateway.html.
If we, for example, want to route incoming requests where the URL path starts with /product-composite/ to our product-composite service, we can specify a routing rule like this:
spring.cloud.gateway.routes:
- id: product-composite
uri: lb://product-composite
predicates:
- Path=/product-composite/**
Some points to note from the preceding code:
id: product-composite: The name of the route is product-composite.uri: lb://product-composite: If the route is selected by its predicates, the request will be routed to the service that is named product-composite in the discovery service, Netflix Eureka. The protocol lb:// is used to direct Spring Cloud Gateway to use the client-side load balancer to look up the destination in the discovery service.predicates: - Path=/product-composite/** is used to specify what requests this route should match. ** matches zero or more elements in the path.To be able to route requests to the Swagger UI set up in Chapter 5, Adding an API Description Using OpenAPI, an extra route to the product-composite service is added:
- id: product-composite-swagger-ui
uri: lb://product-composite
predicates:
- Path=/openapi/**
Requests sent to the edge server with a URI starting with /openapi/ will be directed to the product-composite service.
When the Swagger UI is presented behind an edge server, it must be able to present an OpenAPI specification of the API that contains the correct server URL – the URL of the edge server instead of the URL of the product-composite service itself. To enable the product-composite service to produce a correct server URL in the OpenAPI specification, the following configuration has been added to the product-composite service:
server.forward-headers-strategy: framework
For details, see https://springdoc.org/index.html#how-can-i-deploy-springdoc-openapi-ui-behind-a-reverse-proxy.
To verify that the correct server URL is set in the OpenAPI specification, the following test has been added to the test script, test-em-all.bash:
assertCurl 200 "curl -s http://$HOST:$PORT/
openapi/v3/api-docs"
assertEqual "http://$HOST:$PORT" "$(echo $RESPONSE
| jq -r .servers[].url)"
Eureka exposes both an API and a web page for its clients. To provide a clean separation between the API and the web page in Eureka, we will set up routes as follows:
/eureka/api/ should be handled as a call to the Eureka API/eureka/web/ should be handled as a call to the Eureka web pageAPI requests will be routed to http://${app.eureka-server}:8761/eureka. The routing rule for the Eureka API looks like this:
- id: eureka-api
uri: http://${app.eureka-server}:8761
predicates:
- Path=/eureka/api/{segment}
filters:
- SetPath=/eureka/{segment}
The {segment} part in the Path value matches zero or more elements in the path and will be used to replace the {segment} part in the SetPath value.
Web page requests will be routed to http://${app.eureka-server}:8761. The web page will load several web resources, such as .js, .css, and .png files. These requests will be routed to http://${app.eureka-server}:8761/eureka. The routing rules for the Eureka web page look like this:
- id: eureka-web-start
uri: http://${app.eureka-server}:8761
predicates:
- Path=/eureka/web
filters:
- SetPath=/
- id: eureka-web-other
uri: http://${app.eureka-server}:8761
predicates:
- Path=/eureka/**
From the preceding configuration, we can take the following notes. The ${app.eureka-server} property is resolved by Spring's property mechanism depending on what Spring profile is activated:
localhost using the default profile.eureka. Therefore, the property will be translated into eureka using the docker profile.The relevant parts in the application.yml file that define this translation look like this:
app.eureka-server: localhost
---
spring.config.activate.on-profile: docker
app.eureka-server: eureka
To learn a bit more about the routing capabilities in Spring Cloud Gateway, we will try out host-based routing, where Spring Cloud Gateway uses the hostname of the incoming request to determine where to route the request. We will use one of my favorite websites for testing HTTP codes: http://httpstat.us/.
A call to http://httpstat.us/${CODE} simply returns a response with the ${CODE} HTTP code and a response body also containing the HTTP code and a corresponding descriptive text. For example, see the following curl command:
curl http://httpstat.us/200 -i
This will return the HTTP code 200, and a response body with the text 200 OK.
Let's assume that we want to route calls to http://${hostname}:8080/headerrouting as follows:
i.feel.lucky host should return 200 OKim.a.teapot host should return 418 I'm a teapot501 Not ImplementedTo implement these routing rules in Spring Cloud Gateway, we can use the Host route predicate to select requests with specific hostnames, and the SetPath filter to set the desired HTTP code in the request path. This can be done as follows:
http://i.feel.lucky:8080/headerrouting return 200 OK, we can set up the following route:
- id: host_route_200
uri: http://httpstat.us
predicates:
- Host=i.feel.lucky:8080
- Path=/headerrouting/**
filters:
- SetPath=/200
http://im.a.teapot:8080/headerrouting return 418 I'm a teapot, we can set up the following route:
- id: host_route_418
uri: http://httpstat.us
predicates:
- Host=im.a.teapot:8080
- Path=/headerrouting/**
filters:
- SetPath=/418
501 Not Implemented, we can set up the following route:
- id: host_route_501
uri: http://httpstat.us
predicates:
- Path=/headerrouting/**
filters:
- SetPath=/501
Okay, that was quite a bit of configuration, so now let's try it out!
To try out the edge server, we perform the following steps:
cd $BOOK_HOME/Chapter10
./gradlew clean build && docker-compose build
./test-em-all.bash start

Figure 10.2: Output from test-em-all.bash
http://localhost:8080. That is the output from the test that verifies that the server URL in Swagger UI's OpenAPI specification is correctly rewritten to be the URL of the edge server.With the system landscape including the edge server up and running, let's explore the following topics:
To understand what the edge server exposes to the outside of the system landscape, perform the following steps:
docker-compose ps command to see which ports are exposed by our services:
docker-compose ps gateway eureka product-composite product recommendation review
gateway) exposes its port (8080) outside the Docker engine:
Figure 10.3: Output from docker-compose ps
/actuator/gateway/routes API. The response from this API is rather verbose. To limit the response to information we are interested in, we can apply a jq filter. In the following example, the id of the route and the uri the request will be routed to are selected:
curl localhost:8080/actuator/gateway/routes -s | jq '.[] | {"\(.route_id)": "\(.uri)"}' | grep -v '{\|}'

Figure 10.4: Spring Cloud Gateway routing rules
This gives us a good overview of the actual routes configured in the edge server. Now, let's try out the routes!
In this section, we will try out the edge server and the routes it exposes to the outside of the system landscape. Let's start by calling the product composite API and its Swagger UI. Next, we'll call the Eureka API and visit its web page. Finally, we'll conclude by testing the routes that are based on hostnames.
Let's perform the following steps to call the product composite API through the edge server:
docker-compose logs -f --tail=0 gateway
curl http://localhost:8080/product-composite/1

Figure 10.5: Output from retrieving the composite product with Product ID 1

Figure 10.6: Log output from the edge server
http://b8013440aea0:8080/product-composite/1.To verify that we can reach the Swagger UI introduced in Chapter 5, Adding an API Description Using OpenAPI, through the edge server, open the URL http://localhost:8080/openapi/swagger-ui.html in a web browser. The resulting Swagger UI page should look like this:

Figure 10.7: The Swagger UI through the edge server, gateway
Note the server URL: http://localhost:8080; this means that the product-composite API's own URL, http://product-service:8080/ has been replaced in the OpenAPI specification returned by the Swagger UI.
If you want to, you can proceed and actually try out the product-composite API in the Swagger UI as we did back in Chapter 5, Adding an API Description Using OpenAPI!
To call Eureka through an edge server, perform the following steps:
curl -H "accept:application/json"\
localhost:8080/eureka/api/apps -s | \
jq -r .applications.application[].instance[].instanceId

Figure 10.8: Eureka listing the edge server, gateway, in REST call
Note that the edge server (named gateway) is also present in the response.
http://localhost:8080/eureka/web:
Figure 10.9: Eureka listing the edge server, gateway, in the web UI
Let's wrap up by testing the route configuration based on the hostname used in the requests!
Normally, the hostname in the request is set automatically in the Host header by the HTTP client. When testing the edge server locally, the hostname will be localhost – that is not so useful when testing hostname-based routing. But we can cheat by specifying another hostname in the Host header in the call to the API. Let's see how this can be done:
i.feel.lucky hostname, use this code:
curl http://localhost:8080/headerrouting -H "Host: i.feel.lucky:8080"
200 OK.im.a.teapot, use the following command:
curl http://localhost:8080/headerrouting -H "Host: im.a.teapot:8080"
418 I'm a teapot.Host header, use localhost as the Host header:
curl http://localhost:8080/headerrouting
501 Not Implemented.We can also use i.feel.lucky and im.a.teapot as real hostnames in the requests if we add them to the file /etc/hosts and specify that they should be translated into the same IP address as localhost, that is, 127.0.0.1. Run the following command to add a row to the /etc/hosts file with the required information:
sudo bash -c "echo '127.0.0.1 i.feel.lucky im.a.teapot' >> /etc/hosts"
We can now perform the same routing based on the hostname, but without specifying the Host header. Try it out by running the following commands:
curl http://i.feel.lucky:8080/headerrouting
curl http://im.a.teapot:8080/headerrouting
Expect the same responses as previously, 200 OK and 418 I'm a teapot.
Wrap up the tests by shutting down the system landscape with the following command:
docker-compose down
Also, clean up the /etc/hosts file from the DNS name translation we added for the hostnames, i.feel.lucky and im.a.teapot. Edit the /etc/hosts file and remove the line we added:
127.0.0.1 i.feel.lucky im.a.teapot
These tests of the routing capabilities in the edge server end the chapter.
In this chapter, we have seen how Spring Cloud Gateway can be used as an edge server to control what services are allowed to be called from outside of the system landscape. Based on predicates, filters, and destination URIs, we can define routing rules in a very flexible way. If we want to, we can configure Spring Cloud Gateway to use a discovery service such as Netflix Eureka to look up the target microservice instances.
One important question still unanswered is how we prevent unauthorized access to the APIs exposed by the edge server and how we can prevent third parties from intercepting traffic.
In the next chapter, we will see how we can secure access to the edge server using standard security mechanisms such as HTTPS, OAuth, and OpenID Connect.
product-composite service on the http://$HOST:$PORT/api/product URL instead of the currently used http://$HOST:$PORT/product-composite?Change the font size
Change margin width
Change background colour