Lab 1: Routing - Configure & Monitor Gateway Routes

In this first part we look at one of the core features of an API gateway: Routing.

Info: See Spring Cloud Gateway Route Predicate Factories Reference Doc for all details on how to configure a routing predicates.

Lab Contents

Learning Targets

In this lab you will basically learn how the spring cloud gateway works with route predicates and gateway filters.

The Gateway Handler Mapping determines if a request matches the configured route predicates. If a match is found then the Gateway Web Handler runs the request through a filter chain specific to this request. First, in the request all configured pre-filters are executed. Then the proxied service is called. Finally, all configured post-filters are executed for the response.

In lab 1 you will learn how to:

  • Configure route predicates and filters using both,

    • the declarative approach in the application.yml file

    • the functional approach in Java code using a RouteLocatorBuilder and the fluent Java routes API

  • Configure routes from clients to the customer-service and product-service backend microservices

  • Configure basic filters like the RewritePath and RedirectTo filters.

  • How to monitor configured route definitions and corresponding filters using the actuator endpoint

Folder Contents

In the lab 1 folder you find 2 applications:

  • initial: This is the gateway application we will use as starting point for this lab

  • solution: This is the completed reference solution of the gateway application for this lab including all route predicates and filters we introduce during this lab

Start the Lab

Now, let's start with this lab.

Explore the initial application

Starting a new project using the Spring Cloud Gateway is very easy. Usually just go to https://start.spring.io and select the following dependencies:

  • Gateway (This adds all required Spring Cloud and Spring Cloud Gateway components)

  • Spring Boot DevTools (To speed up development cycle times)

  • Spring Boot Actuator (To verify configured gateway routes and provide some metrics)

To speed things further up this project has already been created for you.

Please navigate your Java IDE to the lab1/initial/api-gateway project and explore this project a bit. Then start the application by running the class com.example.apigateway.ApiGatewayApplication inside your IDE or by issuing a mvnw[.sh|.cmd] spring-boot:run command.

If you have not yet seen the sample application architecture we will be building starting with this lab then please look into the sample application architecture.

For this lab we will also need the two provided sample backend services that you can find in the microservices root folder:

  • product-service: Provides a REST API for products

  • customer-service: Provides a REST API for customers

To test if the backend microservice applications works as expected, please run the corresponding spring boot starter classes and check if you can access the following REST API endpoints via the browser or the provided postman collection in /setup/postman:

You may also use a command-line client as well. Here are example requests using httpie and curl.

Httpie:

http localhost:9091/api/v1/customers
http localhost:9092/api/v1/products

Curl:

curl http://localhost:9091/api/v1/customers
curl http://localhost:9092/api/v1/products

To explore the complete APIs provided by the product-service and customer-service you may also check the Swagger UI at http://localhost:9091/swagger-ui.html (customer-service) and http://localhost:9092/swagger-ui.html (product-service).

There is also a machine-readable Open API document available at http://localhost:9091/v3/api-docs (customer-service) and http://localhost:9092/v3/api-docs.


Step 1: Add a route to the product-service

First start using the declarative approach to define routes. Every routes entry in the application.yml has the following attributes:

  • id: Every route requires a unique identifier

  • uri: This is the target Uniform Resource Identifier (URI) for the routing request

  • predicates: This list of route predicates defines how a request is matched against a configured route

  • filters: This list of gateway filters defines pre- and post-filters for the HTTP request and response

Please open the file src/main/resources/application.yml in the project /lab1/initial/api-gateway and add the following entries (please note that you add these below the existing application.name entry):

application.yml:

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: products
          uri: http://localhost:9092
          predicates:
            - Path=/api/v1/products

This defines a route from localhost:9090/api/v1/products (gateway) to localhost:9092/api/v1/products (product-service).

Now (re-)start the api-gateway application and make sure you also have started the product-service microservice located in /microservices/product-service project. Next try to call the new route at http://localhost:9090/api/v1/products using either the web browser or the provided postman collection (corresponding request in the Routing folder)

Step 2: Add a route to the customer-service

Now similar to the previous routing let's add a new route entry for the customer-service microservice. What's different to the previous one is that we need to define a route that matches both API versions provided by the customer-service:

Open the file src/main/resources/application.yml in the /lab1/initial/api-gateway project and add the following entries with id customers after the previous routing entries:

application.yml:

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: products
          uri: http://localhost:9092
          predicates:
            - Path=/api/v1/products

        - id: customers
          uri: http://localhost:9091
          predicates:
            - Path=/api/v1/{segment},/api/v2/{segment}

Maybe you spotted already the difference. Here we introduced the placeholder {segment} for a subsequent segment after the defined root paths. So several possible paths would match this predicate:

  • http://localhost:9090/api/v1/customers

  • http://localhost:9090/api/v2/customers

  • http://localhost:9090/api/v1/products

  • http://localhost:9090/api/v1/test

  • http://localhost:9090/api/v2/dummy

The desired routes that we wanted to achieve are these:

This route also would match the previous route of http://localhost:9090/api/v1/products but as we have already defined a more concrete route for products as the first entry that one will be used. You may also specify the order of routes using the order property.

Now (re-)start the api-gateway application and make sure you also have started the customer-service microservice located in /microservices/customer-service project. Next try to call the new route at http://localhost:9090/api/v1/customers or http://localhost:9090/api/v2/customers using either the web browser or the provided postman collection (corresponding requests in the Routing folder).

Step 3: Add a route for canary testing of customer service

Next we want to add a route to implement canary testing (canary release). This technique is used to test new functionality with a small group before generally rolling out this functionality to all users.

In Spring Cloud Gateway this can be done by using the weight precondition type. With using this you can for example tell the gateway to route 80% of requests to the production version V1 of the customers API and 20% of requests to the new functionality of the V2 customers API.

So let's do this (again in the application.yml file):

application.yml:

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: products
          uri: http://localhost:9092
          predicates:
            - Path=/api/v1/products
        - id: customers
          uri: http://localhost:9091
          predicates:
            - Path=/api/v1/{segment},/api/v2/{segment}

        - id: customers-v1
          uri: http://localhost:9091
          predicates:
            - Weight=customers, 8
            - Path=/customers
          filters:
            - RewritePath=/customers, /api/v1/customers
        - id: customers-v2
          uri: http://localhost:9091
          predicates:
            - Weight=customers, 2
            - Path=/customers
          filters:
            - RewritePath=/customers, /api/v2/customers

Note: We also introduced the RewritePath filter as we need to remap the gateway path to the correct target path of the backend API call.

Now (re-)start the api-gateway application and make sure you still have started the customer-service microservice located in /microservices/customer-service. Next try to call the new route at http://localhost:9090/customers using either the web browser or the provided postman collection (corresponding requests in Routing folder).

You will notice that sometimes you get customers with a corresponding address (V2 API) and in other cases just customers without any address (V1 API). In about 80 percent of cases you should only get the customers without addresses (the V1 API version).

Step 4: Enable route to hidden endpoint of customer service

Next we only want a routing to work if a special cookie is set for the request. There is a hidden API endpoint in the customer-service at http://localhost:9091/api/v1/customers/hidden that only returns some special result when the cookie hidden-api is set to the value of true. The desired behaviour is that the gateway should only enable the routing to this endpoint if this special cookie is set correctly.

So let's do this (again in the application.yml file):

application.yml:

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: products
          uri: http://localhost:9092
          predicates:
            - Path=/api/v1/products
        - id: customers
          uri: http://localhost:9091
          predicates:
            - Path=/api/v1/{segment},/api/v2/{segment}
        - id: customers-v1
          uri: http://localhost:9091
          predicates:
            - Weight=customers, 8
            - Path=/customers
          filters:
            - RewritePath=/customers, /api/v1/customers
        - id: customers-v2
          uri: http://localhost:9091
          predicates:
            - Weight=customers, 2
            - Path=/customers
          filters:
            - RewritePath=/customers, /api/v2/customers

        - id: customers-hidden
          uri: http://localhost:9091
          predicates:
            - Path=/api/v1/customers/hidden
            - Cookie=hidden-api,true

Now (re-)start the api-gateway application and make sure you still have started the customer-service microservice located in /microservices/customer-service. Next try to call the new route at http://localhost:9090/api/v1/customers/hidden using either the web browser or the provided postman collection (corresponding request in folder routing).

You will notice that you get a 404 (not found) HTTP status. Now retry it by setting the cookie hidden-api with the value true (either using developer tools in web browser or the cookies functionality in postman). Please repeat the request. When the cookie is set correctly you should see the result with a 200 (OK) HTTP status.

Step 5: Configure routes with the Fluent Java Routes API

Let's leave the declarative approach behind and continue configuration using the Fluent Java Routes API.

So let's do this. Please create a new Java class called GatewayRoutingConfiguration in the package com.example.apigateway.routing. Then add the following contents to this file.

GatewayRoutingConfiguration.java:

package com.example.apigateway.routing;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayRoutingConfiguration {

  @Bean
  RouteLocator routing(RouteLocatorBuilder routeLocatorBuilder) {
    return routeLocatorBuilder
            .routes()
            .route("http-bin", ps -> ps.path("/get")
                    .filters(f -> f.addResponseHeader("X-MyResponseHeader", "my response")
                            .addRequestHeader("X-MyRequestHeader", "my request"))
                    .uri("https://httpbin.org"))
            .route("redirect-spring-io", ps -> ps.path("/spring")
                    .filters(f -> f.redirect(302, "https://spring.io"))
                    .uri("https://example.org"))
            .build();
  }
}

This configuration is just a standard spring configuration file using the common @Configuration and @Bean annotations.

Here we are using the RouteLocatorBuilder to define routes. Each route() call also contains the route id, the predicate path, the URI and filters. The first route configuration entries define a call to a predefined public API endpoint at https://httbin.org/get with adding a request and a response header. In the second route a redirect is made to the Spring website.

Now (re-)start the api-gateway application again. The customer-service microservice is not required anymore for this step. Next try to call the new routes at http://localhost:9090/get and http://localhost:9090/spring using either the web browser or the provided postman collection (corresponding request in Routing folder) and see what is happening.

That is all for route configuration features of this lab. The Spring Cloud Gateway provides many more routing capabilities that are beyond the scope of this workshop. Please check the Route Predicate Factories reference doc for details.

In the last step we will look at actuator support for Spring Cloud Gteway.

Step 6: Monitor routes and route metrics

In the last step we will look at what the spring cloud gateway contributes to the Spring Boot Actuator. To enable and remotely access the /gateway actuator endpoint, in general you have to add some configuration to the application.yml file.

application.yml:

management:
  endpoint:
    gateway:
      enabled: true
  endpoints:
    web:
      exposure:
        include: gateway

This configuration is already done, so nothing to do here for you. With this configuration you can call the following actuator endpoints for gateway specific information:

Gateway Actuator Information
URL

Global filters that are applied to all routes

http://localhost:9090/actuator/gateway/globalfilters

Route filters that are applied to all routes

http://localhost:9090/actuator/gateway/routefilters

All configured route in the gateway

http://localhost:9090/actuator/gateway/routes

Information about a single route with given id

http://localhost:9090/actuator/gateway/routes/{id}

Metrics for the number of defined routes

http://localhost:9090/actuator/metrics/spring.cloud.gateway.routes.count

These actuator endpoints are not only very helpful for production but also helps to find out why some routes may not work as expected. You may try these using the web browser or using the postman collection (in the Actuator folder).

http://localhost:9090/actuator/gateway/routes:

[
    {
        "predicate": "Paths: [/get], match trailing slash: true",
        "route_id": "http-bin",
        "filters": [
            "[[AddResponseHeader X-MyResponseHeader = 'my response'], order = 0]",
            "[[AddRequestHeader X-MyRequestHeader = 'my request'], order = 0]"
        ],
        "uri": "https://httpbin.org:443",
        "order": 0
    },
    {
        "predicate": "Paths: [/spring], match trailing slash: true",
        "route_id": "redirect-spring-io",
        "filters": [
            "[[RedirectTo 302 = https://springio.net], order = 0]"
        ],
        "uri": "https://example.org:443",
        "order": 0
    },
    {
        "predicate": "Paths: [/api/v1/products], match trailing slash: true",
        "route_id": "products",
        "filters": [
            "[[AddResponseHeader X-My-Default-Response = 'Default-Value'], order = 1]"
        ],
        "uri": "http://localhost:9092",
        "order": 0
    },
    {
        "predicate": "Paths: [/api/v1/{segment}, /api/v2/{segment}], match trailing slash: true",
        "route_id": "customers",
        "filters": [
            "[[AddResponseHeader X-My-Default-Response = 'Default-Value'], order = 1]"
        ],
        "uri": "http://localhost:9091",
        "order": 0
    },
    {
        "predicate": "(Paths: [/api/v1/customers/hidden], match trailing slash: true && Cookie: name=hidden-api regexp=true)",
        "route_id": "customers-hidden",
        "filters": [
            "[[AddResponseHeader X-My-Default-Response = 'Default-Value'], order = 1]"
        ],
        "uri": "http://localhost:9091",
        "order": 0
    },
    {
        "predicate": "(Weight: customers 8 && Paths: [/customers], match trailing slash: true)",
        "route_id": "customers-v1",
        "filters": [
            "[[AddResponseHeader X-My-Default-Response = 'Default-Value'], order = 1]",
            "[[RewritePath /customers = '/api/v1/customers'], order = 1]"
        ],
        "uri": "http://localhost:9091",
        "order": 0
    },
    {
        "predicate": "(Weight: customers 2 && Paths: [/customers], match trailing slash: true)",
        "route_id": "customers-v2",
        "filters": [
            "[[AddResponseHeader X-My-Default-Response = 'Default-Value'], order = 1]",
            "[[RewritePath /customers = '/api/v2/customers'], order = 1]"
        ],
        "uri": "http://localhost:9091",
        "order": 0
    }
]

This ends lab 1. In the next lab 2 you will learn how to configure resilience design patterns like retry, circuit breakers with fallback and rate limiting.

Important Note: If you could not finish this lab, then just use the project lab2/initial/api-gateway as new starting point.


To continue please head over to Lab 2.

Last updated