Book Image

Microservices with Spring Boot and Spring Cloud - Second Edition

By : Magnus Larsson
3.5 (2)
Book Image

Microservices with Spring Boot and Spring Cloud - Second Edition

3.5 (2)
By: Magnus Larsson

Overview of this book

Want to build and deploy microservices, but don’t know where to start? Welcome to Microservices with Spring Boot and Spring Cloud. This edition features the most recent versions of Spring, Java, Kubernetes, and Istio, demonstrating faster and simpler handling of Spring Boot, local Kubernetes clusters, and Istio installation. The expanded scope includes native compilation of Spring-based microservices, support for Mac and Windows with WSL2, and an introduction to Helm 3 for packaging and deployment. A revamped security chapter now follows the OAuth 2.1 specification and makes use of the newly launched Spring Authorization Server from the Spring team. You’ll start with a set of simple cooperating microservices, then add persistence and resilience, make your microservices reactive, and document their APIs using OpenAPI. Next, you’ll learn how fundamental design patterns are applied to add important functionality, such as service discovery with Netflix Eureka and edge servers with Spring Cloud Gateway. You’ll deploy your microservices using Kubernetes and adopt Istio, then explore centralized log management using the Elasticsearch, Fluentd, and Kibana (EFK) stack, and then monitor microservices using Prometheus and Grafana. By the end of this book, you'll be building scalable and robust microservices using Spring Boot and Spring Cloud.
Table of Contents (6 chapters)

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:

  1. 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.
  2. Add a dependency on spring-cloud-starter-gateway.
  3. To be able to locate microservice instances through Netflix Eureka, also add the spring-cloud-starter-netflix-eureka-client dependency.
  4. Add the edge server project to the common build file, settings.gradle:
    include ':spring-cloud:gateway'
    
  5. Add a Dockerfile with the same content as for the microservices; see Dockerfile content in the folder $BOOK_HOME/Chapter10/microservices.
  6. 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 of 512 MB is applied to the edge server, in the same way as we have done for the other microservices.

  7. 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.
  8. 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:

  1. 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!

  2. The main application class, 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.

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:

  1. 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.
  2. 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: "*"
    
  3. 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:

  1. Predicates, which select a route based on information in the incoming HTTP request
  2. Filters, which can modify both the request and/or the response
  3. A destination URI, which describes where to send a request
  4. 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 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)"
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:

  1. When running the services on the same host without using Docker, for example, for debugging purposes, the property will be translated to localhost using the default profile.
  2. 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 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
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 return 200 OK
  • Calls to the im.a.teapot host should return 418 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:

  1. To make calls to 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
    
  2. To make calls to 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
    
  3. 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:

  1. First, build the Docker images with the following commands:
    cd $BOOK_HOME/Chapter10
    ./gradlew clean build && docker-compose build
    
  2. Next, start the system landscape in Docker and run the usual tests with the following command:
    ./test-em-all.bash start
    
  3. Expect output similar to what we have seen in previous chapters:

    Figure 10.2: Output from test-em-all.bash

  4. 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:

  1. 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
    
  2. 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

  3. 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 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 '{\|}'
    
  4. 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:

  1. 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
    
  2. 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
    
  3. Expect the normal type of response from the product composite API:

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

  4. We should be able to find the following information in the log output:

    Figure 10.6: Log output from the edge server

  5. 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:

  1. 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
    
  2. 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.

  3. 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

  4. 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:

  1. To call for the i.feel.lucky hostname, use this code:
    curl http://localhost:8080/headerrouting -H "Host: i.feel.lucky:8080"
    
  2. Expect the response 200 OK.
  3. For the hostname im.a.teapot, use the following command:
    curl http://localhost:8080/headerrouting -H "Host: im.a.teapot:8080"
    
  4. Expect the response 418 I'm a teapot.
  5. Finally, if not specifying any Host header, use localhost as the Host header:
    curl http://localhost:8080/headerrouting
    
  6. 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

  1. What are the elements used to build a routing rule in Spring Cloud Gateway called?
  2. What are they used for?
  3. How can we instruct Spring Cloud Gateway to locate microservice instances through a discovery service such as Netflix Eureka?
  4. In a Docker environment, how can we ensure that external HTTP requests to the Docker engine can only reach the edge server?
  5. How do we change the routing rules so that the edge server accepts calls to the product-composite service on the http://$HOST:$PORT/api/product URL instead of the currently used http://$HOST:$PORT/product-composite?