Using Spring Cloud Gateway to Hide Microservices behind an Edge Server
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:
- Adding an edge server to our system landscape
- Setting up Spring Cloud Gateway, including configuring routing rules
- Trying out the edge server
Technical requirements
For instructions on how to install the tools used in this book and how to access the source code for this book, see:
- Chapter 21 for macOS
- Chapter 22 for Windows
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
.
Adding an edge server to our system landscape
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
Setting up Spring Cloud Gateway as an edge server is straightforward and can be done with the following steps:
- Create a Spring Boot project using Spring Initializr as described in Chapter 3, Creating a Set of Cooperating Microservices – refer to the Using Spring Initializr to generate skeleton code section.
- Add a dependency on
spring-cloud-starter-gateway
. - To be able to locate microservice instances through Netflix Eureka, also add the
spring-cloud-starter-netflix-eureka-client
dependency. - Add the edge server project to the common build file,
settings.gradle
:include ':spring-cloud:gateway'
- Add a
Dockerfile
with the same content as for the microservices; seeDockerfile
content in the folder$BOOK_HOME/Chapter10/microservices
. - Add the edge server to our three Docker Compose files:
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 of512
MB is applied to the edge server, in the same way as we have done for the other microservices. - Since the edge server will handle all incoming traffic, we will move the composite health check from the product composite service to the edge server. This is described in the Adding a composite health check section next.
- Add configuration for routing rules and more. Since there is a lot to configure, it is handled in a separate section below, Configuring a Spring Cloud Gateway.
You can find the source code for the Spring Cloud Gateway in $BOOK_HOME/Chapter10/spring-cloud/gateway
.
Adding a composite health check
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:
- The
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! - The main application class,
GatewayApplication
, declares aWebClient.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.
Configuring a 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:
- Since Spring Cloud Gateway will use Netflix Eureka to find the microservices it will route traffic to, it must be configured as a Eureka client in the same way as described in Chapter 9, Adding Service Discovery Using Netflix Eureka – refer to the Configuring clients to the Eureka server section.
- Configure Spring Boot Actuator for development usage as described in Chapter 7, Developing Reactive Microservices – refer to the Adding a health API section:
management.endpoint.health.show-details: "ALWAYS" management.endpoints.web.exposure.include: "*"
- Configure log levels so that we can see log messages from interesting parts of the internal processing in the Spring Cloud Gateway, for example, how it decides where to route incoming requests to:
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
.
Routing rules
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:
- Predicates, which select a route based on information in the incoming HTTP request
- Filters, which can modify both the request and/or the response
- A destination URI, which describes where to send a request
- An ID, that is, the name of the route
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.
Routing requests to the product-composite API
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 isproduct-composite
.uri: lb://product-composite
: If the route is selected by its predicates, the request will be routed to the service that is namedproduct-composite
in the discovery service, Netflix Eureka. The protocollb://
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)"
Routing requests to the Eureka server's API and web page
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:
- Requests sent to the edge server with the path starting with
/eureka/api/
should be handled as a call to the Eureka API - Requests sent to the edge server with the path starting with
/eureka/web/
should be handled as a call to the Eureka web page
API 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:
- When running the services on the same host without using Docker, for example, for debugging purposes, the property will be translated to
localhost
using thedefault
profile. - When running the services as Docker containers, the Netflix Eureka server will run in a container with the DNS name
eureka
. Therefore, the property will be translated intoeureka
using thedocker
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
Routing requests with predicates and filters
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:
- Calls to the
i.feel.lucky
host should return200 OK
- Calls to the
im.a.teapot
host should return418 I'm a teapot
- Calls to all other hostnames should return
501 Not Implemented
To 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:
- To make calls to
http://i.feel.lucky:8080/headerrouting
return200 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
- To make calls to
http://im.a.teapot:8080/headerrouting
return418 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
- Finally, to make calls to all other hostnames return
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!
Trying out the edge server
To try out the edge server, we perform the following steps:
- First, build the Docker images with the following commands:
cd $BOOK_HOME/Chapter10 ./gradlew clean build && docker-compose build
- Next, start the system landscape in Docker and run the usual tests with the following command:
./test-em-all.bash start
- Expect output similar to what we have seen in previous chapters:
Figure 10.2: Output from test-em-all.bash
- From the log output, note the second to last test result,
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:
- Examining what is exposed by the edge server outside of the system landscape running in the Docker engine
- Trying out some of the most frequently used routing rules as follows:
- Use URL-based routing to call our APIs through the edge server
- Use URL-based routing to call the Swagger UI through the edge server
- Use URL-based routing to call Netflix Eureka through the edge server, both using its API and web-based UI
- Use header-based routing to see how we can route requests based on the hostname in the request
Examining what is exposed outside the Docker engine
To understand what the edge server exposes to the outside of the system landscape, perform the following steps:
- Use the
docker-compose ps
command to see which ports are exposed by our services:docker-compose ps gateway eureka product-composite product recommendation review
- As we can see in the following output, only the edge server (named
gateway
) exposes its port (8080
) outside the Docker engine:Figure 10.3: Output from docker-compose ps
- If we want to see what routes the edge server has set up, we can use the
/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 ajq
filter. In the following example, theid
of the route and theuri
the request will be routed to are selected:curl localhost:8080/actuator/gateway/routes -s | jq '.[] | {"\(.route_id)": "\(.uri)"}' | grep -v '{\|}'
- This command will respond with the following:
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!
Trying out the routing rules
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.
Calling the product composite API through the edge server
Let's perform the following steps to call the product composite API through the edge server:
- To be able to see what is going on in the edge server, we can follow its log output:
docker-compose logs -f --tail=0 gateway
- Now, in a separate terminal window, make the call to the product composite API through the edge server:
curl http://localhost:8080/product-composite/1
- Expect the normal type of response from the product composite API:
Figure 10.5: Output from retrieving the composite product with Product ID 1
- We should be able to find the following information in the log output:
Figure 10.6: Log output from the edge server
- From the log output, we can see the pattern matching based on the predicate we specified in the configuration, and we can see which microservice instance the edge server selected from the available instances in the discovery server – in this case, it forwards the request to
http://b8013440aea0:8080/product-composite/1
.
Calling the Swagger UI through the edge server
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!
Calling Eureka through the edge server
To call Eureka through an edge server, perform the following steps:
- First, call the Eureka API through the edge server to see what instances are currently registered in the discovery server:
curl -H "accept:application/json"\ localhost:8080/eureka/api/apps -s | \ jq -r .applications.application[].instance[].instanceId
- Expect a response along the lines of the following:
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. - Next, open the Eureka web page in a web browser using the URL
http://localhost:8080/eureka/web
:Figure 10.9: Eureka listing the edge server, gateway, in the web UI
- From the preceding screenshot, we can see the Eureka web page reporting the same available instances as the API response in the previous step.
Routing based on the host header
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:
- To call for the
i.feel.lucky
hostname, use this code:curl http://localhost:8080/headerrouting -H "Host: i.feel.lucky:8080"
- Expect the response
200 OK
. - For the hostname
im.a.teapot
, use the following command:curl http://localhost:8080/headerrouting -H "Host: im.a.teapot:8080"
- Expect the response
418 I'm a teapot
. - Finally, if not specifying any
Host
header, uselocalhost
as theHost
header:curl http://localhost:8080/headerrouting
- Expect the response
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.
Summary
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.
Questions
- What are the elements used to build a routing rule in Spring Cloud Gateway called?
- What are they used for?
- How can we instruct Spring Cloud Gateway to locate microservice instances through a discovery service such as Netflix Eureka?
- In a Docker environment, how can we ensure that external HTTP requests to the Docker engine can only reach the edge server?
- How do we change the routing rules so that the edge server accepts calls to the
product-composite
service on thehttp://$HOST:$PORT/api/product
URL instead of the currently usedhttp://$HOST:$PORT/product-composite
?