Using Spring Cloud Contract in API Gateway Environments
Spring Cloud Contract supports Consumer Driven Contract Testing in distributed environments, where API consumers and providers know each other. This also applies to distributed system environments, where front-end developers need to consume REST-APIs provided by Spring Boot powered micro-services. Often, however, the communication is going through some kind of API gateway, which applies certain request/response modifications, such as URL rewrites, in order to aggregate multiple backend services, for instance.
In this blog post I am going to show you, how to deal with API gateway semantics, when using Spring Cloud Contract.
Challenges
Contract based testing in non-JVM environments such as for front-end development, has its challenges. Spring Cloud Contract addresses the most obvious problem of distributing and hosting REST API stubs locally or within a CI/CD pipeline using its dockerized stub runner (together with WireMock). An API gateway, however, imposes further challenges, which are not addressed out of the box.
Cross-Origin Resource Sharing (CORS)
For security reasons, browsers stop scripts from accessing URLs on different domains. Special CORS requests (effectively HTTP OPTIONS requests) are used, when a browser wants to know, if it is eligible to access such URLs. In production environments most likely both front-end web apps and back-end services are deployed under the same domain, thus avoiding these additional requests. If running on the different domains, the back-end is typically configured to grant those CORS requests for certain front-end domains.
The Spring Cloud Contract stub runner, however, won’t be deployed as part of any runtime environment, nor is it known to any API gateway. Instead it will either be started locally or from within a CI/CD pipeline, before running the consumer contract tests, e.g. using Jest. Hence, it will have a different hostname and/or port, but on the other hand won’t answer CORS requests, thus preventing browser (code) access to the contract stubs.
One could, of course, create dedicated CORS contracts using Spring Cloud Contract. However, it is obvious, that CORS is — by definition — a cross-cutting concern, which should not be addressed by adding even more contracts. This is especially true, if CORS isn’t an issue in the actual live environment, in case front- and back-end share the same domain.
Stub Routing/Aggregation
Front-end applications aren’t necessarily limited to accessing only a single back-end service and its API. Accordingly, for running consumer contract tests, multiple stub artifacts need to be hosted and requests need to be routed to the correct stub accordingly. The Spring Cloud Contract stub runner supports multiple stubs by means of its STUBRUNNER_IDS
environment variable, however, it has to start each of those using a separate WireMock instance using a distinct TCP/IP port. Hence, the front-end code needs to know, which of the (usually dynamically assigned) ports is to be used for which request, in order to properly re-route its requests for testing. While a base URL is typically configurable to enable staged environments, front-end code will (and should) not have such detailed knowledge about the different back-end services, which an API gateway typically takes care of.
Request Modification
By far the most challenging issue, when using contract testing in API gateway environments, is the gateway’s ability to modify requests (and responses). The most popular modification known is URL rewriting, which is often used to aggregate different back-end services and route requests towards them accordingly. In such environments front-end code may access resources using URLs such as /api/persons/1
or /api/orders/4711
regardless, if they are backed by one or two different back-end services.
Even worse, the URLs may be mapped completely different to the individual back-end services, for instance /v1/persons/1
handled by one service and /orders/v2/4711
by another one. A contract for such an API may then look as follows:
Contract.make {
request {
method GET()
url '/v1/persons/1'
}
response {
status OK()
headers {
contentType applicationJson()
}
body([
// ..
])
}
}
However, when run locally using the stub runner, requests towards /api/persons/1
won’t be translated properly.
One might try to circumvent this, by using separate values for consumer and provider within contracts, e.g. as follows:
Contract.make {
request {
method GET()
url $(consumer('/api/persons/1'), producer('/v1/persons/1'))
}
response {
status OK()
headers {
contentType applicationJson()
}
body([
// ..
])
}
}
In this case, the generated stubs will respond to /api/persons/1
while the generated provider tests will request for /v1/persons/1
. It is more than obvious, that this approach has several downsides:
- Contracts are polluted with API gateway semantics, thus introducing infrastructure knowledge into back-end services.
- The alignment between consumer and provider is potentially broken, since the consumer value may simply be wrong or outdated without any test or build noticing it, at least within the back-end service’s CI.
- When combined with (wildcard) regular expression patterns, such as
url $(consumer(regex('/api/persons/.*')), producer('/v1/persons/1'))
, Spring Cloud Contract’s stub generation will explicitly fail, since the regex pattern no longer matches the provider value. This is — by intent — to ensure the alignment between consumer and provider.
Enhancing Spring Cloud Stub Runner
Fortunately, Spring Cloud Stub Runner itself simply is a dockerized Spring Boot application. Thus it can easily be extended to build a customized version, providing additional API gateway semantics.
Spring Cloud Gateway meets Spring Cloud Contract Stub Runner
Spring Cloud Gateway is a library for building API gateways and is often used as stand-alone API gateway solution in distributed environments. It offers configuration properties for defining API routes and request modification rules in a declarative manner using Spring Boot’s environment abstraction. Accordingly, it is the natural fit for overcoming stub routing, aggregation, and request modification challenges.
Let’s start with the following build.gradle.kts
, created via start.spring.io. Notice, that the dependency spring-cloud-starter-contract-stub-runner
has been adapted to implementation
scope, since it needs to be part of the Spring Boot application:
plugins {
java
id("org.springframework.boot") version "3.1.4"
id("io.spring.dependency-management") version "1.1.3"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
extra["springCloudVersion"] = "2022.0.4"
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
implementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Our Spring Boot application class then simply needs to be annotated with @EnableStubRunnerServer
, to enable the stub runner functionality:
@SpringBootApplication
@EnableStubRunnerServer
class GatewayStubRunnerApplication
On top of that an application.yml
needs to be created, providing the gateway configuration for the back-end services and their corresponding stubs, as follows:
server.port: 8750
spring:
cloud:
gateway:
globalcors:
cors-configurations:
"[/**]":
allowedOriginPatterns: "*"
allowedMethods:
- GET
- POST
- PATCH
- PUT
- DELETE
routes:
- id: person_service_route
uri: http://undefined
predicates:
- Path=/api/persons/**
filters:
- RewritePath=/api/persons/(?<segment>/?.*), /v1/persons/$\{segment}
- id: order_service_route
uri: http://undefined
predicates:
- Path=/api/orders/**
filters:
- RewritePath=/api/orders/(?<segment>/?.*), /orders/v2/$\{segment}
The configuration covers:
- the configuration of the default
server.port
in accordance with the default used by Spring Cloud Contract Stub Runner - a gateway CORS configuration enabling all kinds of CORS requests against the configured API routes
- a list of route definitions for down-stream services resembling the API gateway routing semantics of the actual runtime environment (These could be essentially the route definitions from your production environment, in case you were using Spring Cloud Gateway there already.)
Once started, the routing can be tested using cUrl against the well-defined port 8750:
curl http://localhost:8750/api/persons/1
This yields an internal server error, since Spring Cloud Gateway cannot connect to http://undefined
, which will be solved in the next section.
Be aware of the fact, that all other stub runner configurations can be applied using either additional YAML configuration or environment variables. Accordingly, stubs may already be fetched and started, for instance using the
STUBRUNNER_xxx
environment variables.
Entering Spring Cloud Load Balancer
Spring Cloud Load Balancer is a library for client-side load balancing and provides dynamic discovery of target hosts/ports. As such it is is the perfect candidate to resolve the target URIs configured as part of the gateway route definitions (previously set to http://undefined
).
An additional dependency (implementation(“org.springframework.cloud:spring-cloud-starter-loadbalancer”)
) needs to be added or included via start.spring.io.
Spring Cloud Contract is aware of Spring Cloud Load Balancer by default (if present on the class-path), if @AutoConfigureStubRunner
annotation is used on Spring Boot test classes. This enables clients, such as Feign clients, to be redirected to the appropriate running stub server (WireMock). It is therefore sufficient to annotate the Spring Boot application class with the relevant meta- annotations from @AutoConfigureStubRunner
, since the latter one must not be used on Spring Boot application classes directly:
@SpringBootApplication
@EnableStubRunnerServer
@ImportAutoConfiguration(
classes = [
SpringCloudLoadBalancerAutoConfiguration::class,
StubRunnerSpringCloudAutoConfiguration::class,
],
)
class GatewayStubRunnerApplication
It is now possible to use the stub artifact id using the special URL prefix lb://
to configure the gateway routing targets, e.g. as follows:
server.port: 8750
spring:
cloud:
gateway:
globalcors:
cors-configurations:
"[/**]":
allowedOriginPatterns: "*"
allowedMethods:
- GET
- POST
- PATCH
- PUT
- DELETE
routes:
- id: person_service_route
uri: lb://person-service
predicates:
- Path=/api/persons/**
filters:
- RewritePath=/api/persons/(?<segment>/?.*), /v1/persons/$\{segment}
- id: order_service_route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- RewritePath=/api/orders/(?<segment>/?.*), /orders/v2/$\{segment}
The resolution if those URLs is performed lazily, that is it doesn’t really matter, if the stub with the appropriate id was actually loaded or not, as long as no request is executed against the route definition.
Be aware that stub artifact ids need to be unique despite their group ids, for this to work.
Local Development and integration into CI/CD
Our example could be started for local development tests, for instance using a dedicated remote repository and stub Maven coordinates, as follows:
env STUBRUNNER_STUBS_MODE=remote \
STUBRUNNER_REPOSITORY_ROOT='<artifactory url>' \
STUBRUNNER_IDS="com.example:person-service:1.0.0,com.example:order-service:1.2.0" \
./gradlew bootRun
The application could as well be published as Docker image into a Docker Container Registry, such as GitLab. It can then be integrated as helper service into the continuous integration pipeline, for instance using GitLab services, as follows:
contract-test:
stage: test
variables:
CI_DEBUG_SERVICES: "true"
STUBRUNNER_STUBS_MODE: "remote"
STUBRUNNER_REPOSITORY_ROOT: https://your-artifactory
STUBRUNNER_USERNAME: $ARTIFACTORY_USER
STUBRUNNER_PASSWORD: $ARTIFACTORY_PASSWORD
STUBRUNNER_IDS: "<comma separated list of stub artefacts>"
services:
- name: <docker image name of the stub runner>
alias: stubrunner
image:
name: <image to be used for testing>
script:
- <start test execution using http://stubrunner:8750 as target url for backend calls>
Summary
This blog post introduced you to some of the challenges encountered when using contract testing in environments including API gateways, mainly the fact that requests need to be modified on their way from front-end clients to back-end service in one way or another.
Thanks to Spring Cloud Contract Stub Runner being a Spring Boot application itself, it can easily be extended with additional Spring Cloud libraries to resemble those behaviors.
Feel free to respond to this post or follow me on Twitter.