Dynamic OpenAPIs with
Spring Cloud Gateway
Iván López
@ilopmar
@ilopmar
Iván López
Who am I?
● Iván López (@ilopmar)
● JVM Developer
● Staff Software Engineer at VMware
● @MadridGUG Coordinator
● Speaker: GeeCon, Codemotion, Devoxx, SpringOne,
RigaDevDays, CommitConf, Spring IO,...
🇪🇸🇮🇹🇬🇧🇦🇹🇨🇦🇧🇪🇨🇿🇺🇦🇩🇰🇸🇪🇺🇸🇷🇺🇪🇪🇱🇻🇭🇷🇵🇱🇹🇷🇷🇴🇧🇬
OpenAPI
@ilopmar
Iván López
OpenAPI Specification
● Old Swagger Specification
● API description format for REST APIs
● Endpoints, operations, parameters, authentication,...
● Programming language agnostic
● YAML or JSON
@ilopmar
Iván López
Swagger
● Set of opensource tools build around OpenApi Spec
● Swagger Editor
● Swagger UI
● Swagger Codegen
● Swagger Core, Parser,...
@ilopmar
Iván López
Why use OpenAPI?
● Standard widely used
● Huge userbase
● Stable implementation
● Swagger Codegen to generate server stub and
client libraries!
● Integrations with many languages, libraries and
frameworks
Spring Cloud
Gateway
@ilopmar
Iván López
What is an API Gateway?
● API proxy between an API Client and API Server
● Single entry point for the backend API and services
● Cross-cuttings concerns: security, rate-limiting,
monitoring, logging, metrics,...
@ilopmar
Iván López
Spring Cloud Gateway
● API Gateway for the Spring Ecosystem
● Built on top Spring Boot, WebFlux and Reactor
● Dynamic routing
● Route matching: Path, Method, Header, Host,...
● Filters
● Rate Limiting, Circuit Breaker
● Path Rewriting
@ilopmar
Iván López
Spring Cloud Gateway Basics
● Route, Predicate and Filter
● Route Predicates: Cookie, Header, Host, Method,
Path,...
● Gateway Factories: Add/Remove
Request/Response Header/Parameter, Rewrite
path
● Global and custom filters
● YAML configuration & Programmatic API
@ilopmar
Iván López
Spring Cloud Gateway examples
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Foo, some-value
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/?(?<segment>.*), /${segment}
The problem
“I just want to expose
the OpenAPI of my
application”
@ilopmar
Iván López
Current status
● Spring Cloud Gateway in front of all different services
● All services expose OpenAPI
● API First?
– Code -> OpenAPI
– OpenAPI -> Code
● Internal endpoints vs Public endpoints
● Manual configuration in SCG
@ilopmar
Iván López
Requirements
● Expose product Public OpenAPI in Gateway
● Aggregate endpoints from different services
● Public endpoints defined on every service
● Optionally rewrite some endpoints
● Expose only public schemas and not all of them
● Rewrite and group tags
● Update Public OpenAPI and routing must be dynamic
@ilopmar
Iván López
My approach
● I did a 1 week spike
● We use SCG OSS
● The feature is available in SCG Commercial: Only
Tanzu Application Service and Kubernetes
● We deploy on K8S but wanted to keep everything
agnostic: local development, on-premise,...
● Libraries to read/write OpenAPI specifications
● SCG Programmatic API
Solution
@ilopmar
Iván López
Every service defines...
● Which endpoint is made public
paths:
/pipelines:
post:
x-vmw-public: true
summary: Create a new pipeline definition
description: Given a pipeline, it creates an execution
graph and prepares it to be run
@ilopmar
Iván López
Every service defines...
● How an endpoint is rewritten
paths:
/reports:
get:
x-vmw-public: true
x-vmw-rewrite: /vulnerabilities/reports
summary: Get all reports, optionally filtered by artifact version
description: It returns a collection of report metadata...
@ilopmar
Iván López
Every service defines...
● How tags are rewritten
tags:
- name: products
description: Using these endpoints you can manage the products...
x-vmw-rewrite: inventory
- name: artifacts
description: Using these endpoints you can manage the artifacts...
x-vmw-rewrite: inventory
- name: artifact-versions
description: Using these endpoints you can manage the artifact versions...
x-vmw-rewrite: inventory
- name: inventory
description: Using these endpoints you can see the products, artifacts and
artifact versions and their relationships in your organization inventory
@ilopmar
Iván López
Every service defines...
● How tags are rewritten
tags:
- name: products
description: Using these endpoints you can manage the products...
x-vmw-rewrite: inventory
- name: artifacts
description: Using these endpoints you can manage the artifacts...
x-vmw-rewrite: inventory
- name: artifact-versions
description: Using these endpoints you can manage the artifact versions...
x-vmw-rewrite: inventory
- name: inventory
description: Using these endpoints you can see the products, artifacts and
artifact versions and their relationships in your organization inventory
@ilopmar
Iván López
OpenAPI creation
● Gateway polls services every 5 minutes
● Filter, transform and combine all the OpenAPI specs
● Creates Public OpenAPI specification on the fly
@ilopmar
Iván López
Expose OpenAPI
@GetMapping(value = "/v1/api.yml", produces = APPLICATION_X_YAML)
public ResponseEntity<String> v1() {
return openApiService.generateOpenApi()
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@ilopmar
Iván López
Spring Cloud Gateway routes
@Bean
@RefreshScope
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) {
RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes();
List<GatewayRoute> gatewayRoutes = routeService.findGatewayRoutes();
for (GatewayRoute gatewayRoute : gatewayRoutes) {
routeLocator.route(gatewayRoute.routeId(), r -> r
.order(gatewayRoute.order())
.path(gatewayRoute.gatewayPath())
.and().method(gatewayRoute.openApiRoute().method())
.filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(),
gatewayRoute.rewritePath())
)
.uri(gatewayRoute.openApiRoute().uri()));
}
return routeLocator.build();
}
● Routes refreshed dynamically
@ilopmar
Iván López
● Routes refreshed dynamically
Spring Cloud Gateway routes
@Bean
@RefreshScope
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) {
RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes();
List<GatewayRoute> gatewayRoutes = routeService.findGatewayRoutes();
for (GatewayRoute gatewayRoute : gatewayRoutes) {
routeLocator.route(gatewayRoute.routeId(), r -> r
.order(gatewayRoute.order())
.path(gatewayRoute.gatewayPath())
.and().method(gatewayRoute.openApiRoute().method())
.filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(),
gatewayRoute.rewritePath())
)
.uri(gatewayRoute.openApiRoute().uri()));
}
return routeLocator.build();
}
@ilopmar
Iván López
Gateway Routes
public List<GatewayRoute> findGatewayRoutes() {
return openApiService.findOpenApiRoutes()
.stream()
.map(RouteConverter::convertOpenApiRouteToGatewayRoute)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
}
@ilopmar
Iván López
Example: Route conversion (I)
OpenApiRoute[
originalPath=/v1/reports
newPath=/v1/vulnerabilities/reports
method=GET
uri=https://vulnerability-service.xxxxxxxxxxxxx
]
GatewayRoute[
routeId=GET__/v1/vulnerabilities/reports
gatewayPath=/v1/vulnerabilities/reports
regexp=/v1/vulnerabilities/reports
rewritePath=/v1/reports
order=0
]
@ilopmar
Iván López
Example: Route conversion (II)
OpenApiRoute[
originalPath=/v1/target-platforms/{target_platform_id}
newPath=/v1/tp/{target_platform_id}
method=GET
uri=https://provisioning-service.xxxxxxxxxxxxx
]
GatewayRoute[
routeId=GET__/v1/tp/{target_platform_id}
gatewateyPath=/v1/tp/?*
regexp=/v1/tp/(?<id1>.*)
rewritePath=/v1/target-platforms/${id1}
order=10
]
@ilopmar
Iván López
Spring Cloud Gateway routes
@Bean
@RefreshScope
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) {
RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes();
List<GatewayRoute> openApiRoutes = routeService.findOpenApiRoutes();
for (GatewayRoute gatewayRoute : openApiRoutes) {
routeLocator.route(gatewayRoute.routeId(), r -> r
.order(gatewayRoute.order())
.path(gatewayRoute.gatewayPath())
.and().method(gatewayRoute.openApiRoute().method())
.filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(),
gatewayRoute.rewritePath())
)
.uri(gatewayRoute.openApiRoute().uri()));
}
return routeLocator.build();
}
● Routes refreshed dynamically
@ilopmar
Iván López
Example: Gateway Routes (I)
OpenApiRoute[
originalPath=/v1/reports
newPath=/v1/vulnerabilities/reports
method=GET
uri=https://vulnerability-service...
]
GatewayRoute[
routeId=GET__/v1/vulnerabilities/reports
gatewateyPath=/v1/vulnerabilities/reports
regexp=/v1/vulnerabilities/reports
rewritePath=/v1/reports
order=0
]
routeLocator.route("GET__/v1/vulnerabilities/reports", r -> r
.order(0)
.path("/v1/vulnerabilities/reports")
.and().method("GET")
.filters(s -> s.rewritePath("/v1/vulnerabilities/reports", "/v1/reports"))
.uri("https://vulnerability-service.xxxxxxxxxxxxxxxxxx"));
@ilopmar
Iván López
Example: Gateway Routes (II)
OpenApiRoute[
originalPath=/v1/target-platforms/{target_platform_id}
newPath=/v1/tp/{target_platform_id}
method=GET
uri=https://provisioning-service...
]
GatewayRoute[
routeId=GET__/v1/tp/{target_platform_id}
gatewateyPath=/v1/tp/?*
regexp=/v1/tp/(?<id1>.*)
rewritePath=/v1/target-platforms/${id1}
order=10
]
routeLocator.route("GET__/v1/tp/{target_platform_id}", r -> r
.order(10)
.path("/v1/tp/?*")
.and().method("GET")
.filters(s -> s.rewritePath("/v1/tp/(?<id1>.*)", "/v1/target-platforms/${id1}"))
.uri("https://provisioning-service.xxxxxxxxxxxxxxx"));
Demo
@ilopmar
Iván López
Summary
Solved the problem
in one week
+1 year in Production
without problems
Reuse existing
OSS libraries
Platform agnostic Spring Cloud Gateway
is awesome
Public and Dynamic
OpenAPI
Thank you!
Questions?
Iván López
@ilopmar
lopez.ivan@gmail.com
https:/
/github.com/ilopmar
https:/
/bit.ly/springio-dynamic-apis

Spring IO 2023 - Dynamic OpenAPIs with Spring Cloud Gateway

  • 1.
    Dynamic OpenAPIs with SpringCloud Gateway Iván López @ilopmar
  • 2.
    @ilopmar Iván López Who amI? ● Iván López (@ilopmar) ● JVM Developer ● Staff Software Engineer at VMware ● @MadridGUG Coordinator ● Speaker: GeeCon, Codemotion, Devoxx, SpringOne, RigaDevDays, CommitConf, Spring IO,... 🇪🇸🇮🇹🇬🇧🇦🇹🇨🇦🇧🇪🇨🇿🇺🇦🇩🇰🇸🇪🇺🇸🇷🇺🇪🇪🇱🇻🇭🇷🇵🇱🇹🇷🇷🇴🇧🇬
  • 3.
  • 4.
    @ilopmar Iván López OpenAPI Specification ●Old Swagger Specification ● API description format for REST APIs ● Endpoints, operations, parameters, authentication,... ● Programming language agnostic ● YAML or JSON
  • 5.
    @ilopmar Iván López Swagger ● Setof opensource tools build around OpenApi Spec ● Swagger Editor ● Swagger UI ● Swagger Codegen ● Swagger Core, Parser,...
  • 6.
    @ilopmar Iván López Why useOpenAPI? ● Standard widely used ● Huge userbase ● Stable implementation ● Swagger Codegen to generate server stub and client libraries! ● Integrations with many languages, libraries and frameworks
  • 7.
  • 8.
    @ilopmar Iván López What isan API Gateway? ● API proxy between an API Client and API Server ● Single entry point for the backend API and services ● Cross-cuttings concerns: security, rate-limiting, monitoring, logging, metrics,...
  • 9.
    @ilopmar Iván López Spring CloudGateway ● API Gateway for the Spring Ecosystem ● Built on top Spring Boot, WebFlux and Reactor ● Dynamic routing ● Route matching: Path, Method, Header, Host,... ● Filters ● Rate Limiting, Circuit Breaker ● Path Rewriting
  • 10.
    @ilopmar Iván López Spring CloudGateway Basics ● Route, Predicate and Filter ● Route Predicates: Cookie, Header, Host, Method, Path,... ● Gateway Factories: Add/Remove Request/Response Header/Parameter, Rewrite path ● Global and custom filters ● YAML configuration & Programmatic API
  • 11.
    @ilopmar Iván López Spring CloudGateway examples spring: cloud: gateway: routes: - id: method_route uri: https://example.org predicates: - Method=GET,POST spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Foo, some-value spring: cloud: gateway: routes: - id: rewritepath_route uri: https://example.org predicates: - Path=/foo/** filters: - RewritePath=/foo/?(?<segment>.*), /${segment}
  • 12.
  • 13.
    “I just wantto expose the OpenAPI of my application”
  • 14.
    @ilopmar Iván López Current status ●Spring Cloud Gateway in front of all different services ● All services expose OpenAPI ● API First? – Code -> OpenAPI – OpenAPI -> Code ● Internal endpoints vs Public endpoints ● Manual configuration in SCG
  • 15.
    @ilopmar Iván López Requirements ● Exposeproduct Public OpenAPI in Gateway ● Aggregate endpoints from different services ● Public endpoints defined on every service ● Optionally rewrite some endpoints ● Expose only public schemas and not all of them ● Rewrite and group tags ● Update Public OpenAPI and routing must be dynamic
  • 16.
    @ilopmar Iván López My approach ●I did a 1 week spike ● We use SCG OSS ● The feature is available in SCG Commercial: Only Tanzu Application Service and Kubernetes ● We deploy on K8S but wanted to keep everything agnostic: local development, on-premise,... ● Libraries to read/write OpenAPI specifications ● SCG Programmatic API
  • 17.
  • 18.
    @ilopmar Iván López Every servicedefines... ● Which endpoint is made public paths: /pipelines: post: x-vmw-public: true summary: Create a new pipeline definition description: Given a pipeline, it creates an execution graph and prepares it to be run
  • 19.
    @ilopmar Iván López Every servicedefines... ● How an endpoint is rewritten paths: /reports: get: x-vmw-public: true x-vmw-rewrite: /vulnerabilities/reports summary: Get all reports, optionally filtered by artifact version description: It returns a collection of report metadata...
  • 20.
    @ilopmar Iván López Every servicedefines... ● How tags are rewritten tags: - name: products description: Using these endpoints you can manage the products... x-vmw-rewrite: inventory - name: artifacts description: Using these endpoints you can manage the artifacts... x-vmw-rewrite: inventory - name: artifact-versions description: Using these endpoints you can manage the artifact versions... x-vmw-rewrite: inventory - name: inventory description: Using these endpoints you can see the products, artifacts and artifact versions and their relationships in your organization inventory
  • 21.
    @ilopmar Iván López Every servicedefines... ● How tags are rewritten tags: - name: products description: Using these endpoints you can manage the products... x-vmw-rewrite: inventory - name: artifacts description: Using these endpoints you can manage the artifacts... x-vmw-rewrite: inventory - name: artifact-versions description: Using these endpoints you can manage the artifact versions... x-vmw-rewrite: inventory - name: inventory description: Using these endpoints you can see the products, artifacts and artifact versions and their relationships in your organization inventory
  • 22.
    @ilopmar Iván López OpenAPI creation ●Gateway polls services every 5 minutes ● Filter, transform and combine all the OpenAPI specs ● Creates Public OpenAPI specification on the fly
  • 23.
    @ilopmar Iván López Expose OpenAPI @GetMapping(value= "/v1/api.yml", produces = APPLICATION_X_YAML) public ResponseEntity<String> v1() { return openApiService.generateOpenApi() .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); }
  • 24.
    @ilopmar Iván López Spring CloudGateway routes @Bean @RefreshScope public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) { RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes(); List<GatewayRoute> gatewayRoutes = routeService.findGatewayRoutes(); for (GatewayRoute gatewayRoute : gatewayRoutes) { routeLocator.route(gatewayRoute.routeId(), r -> r .order(gatewayRoute.order()) .path(gatewayRoute.gatewayPath()) .and().method(gatewayRoute.openApiRoute().method()) .filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(), gatewayRoute.rewritePath()) ) .uri(gatewayRoute.openApiRoute().uri())); } return routeLocator.build(); } ● Routes refreshed dynamically
  • 25.
    @ilopmar Iván López ● Routesrefreshed dynamically Spring Cloud Gateway routes @Bean @RefreshScope public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) { RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes(); List<GatewayRoute> gatewayRoutes = routeService.findGatewayRoutes(); for (GatewayRoute gatewayRoute : gatewayRoutes) { routeLocator.route(gatewayRoute.routeId(), r -> r .order(gatewayRoute.order()) .path(gatewayRoute.gatewayPath()) .and().method(gatewayRoute.openApiRoute().method()) .filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(), gatewayRoute.rewritePath()) ) .uri(gatewayRoute.openApiRoute().uri())); } return routeLocator.build(); }
  • 26.
    @ilopmar Iván López Gateway Routes publicList<GatewayRoute> findGatewayRoutes() { return openApiService.findOpenApiRoutes() .stream() .map(RouteConverter::convertOpenApiRouteToGatewayRoute) .filter(Optional::isPresent) .map(Optional::get) .toList(); }
  • 27.
    @ilopmar Iván López Example: Routeconversion (I) OpenApiRoute[ originalPath=/v1/reports newPath=/v1/vulnerabilities/reports method=GET uri=https://vulnerability-service.xxxxxxxxxxxxx ] GatewayRoute[ routeId=GET__/v1/vulnerabilities/reports gatewayPath=/v1/vulnerabilities/reports regexp=/v1/vulnerabilities/reports rewritePath=/v1/reports order=0 ]
  • 28.
    @ilopmar Iván López Example: Routeconversion (II) OpenApiRoute[ originalPath=/v1/target-platforms/{target_platform_id} newPath=/v1/tp/{target_platform_id} method=GET uri=https://provisioning-service.xxxxxxxxxxxxx ] GatewayRoute[ routeId=GET__/v1/tp/{target_platform_id} gatewateyPath=/v1/tp/?* regexp=/v1/tp/(?<id1>.*) rewritePath=/v1/target-platforms/${id1} order=10 ]
  • 29.
    @ilopmar Iván López Spring CloudGateway routes @Bean @RefreshScope public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder, RouteService routeService) { RouteLocatorBuilder.Builder routeLocator = routeLocatorBuilder.routes(); List<GatewayRoute> openApiRoutes = routeService.findOpenApiRoutes(); for (GatewayRoute gatewayRoute : openApiRoutes) { routeLocator.route(gatewayRoute.routeId(), r -> r .order(gatewayRoute.order()) .path(gatewayRoute.gatewayPath()) .and().method(gatewayRoute.openApiRoute().method()) .filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath(gatewayRoute.regexp(), gatewayRoute.rewritePath()) ) .uri(gatewayRoute.openApiRoute().uri())); } return routeLocator.build(); } ● Routes refreshed dynamically
  • 30.
    @ilopmar Iván López Example: GatewayRoutes (I) OpenApiRoute[ originalPath=/v1/reports newPath=/v1/vulnerabilities/reports method=GET uri=https://vulnerability-service... ] GatewayRoute[ routeId=GET__/v1/vulnerabilities/reports gatewateyPath=/v1/vulnerabilities/reports regexp=/v1/vulnerabilities/reports rewritePath=/v1/reports order=0 ] routeLocator.route("GET__/v1/vulnerabilities/reports", r -> r .order(0) .path("/v1/vulnerabilities/reports") .and().method("GET") .filters(s -> s.rewritePath("/v1/vulnerabilities/reports", "/v1/reports")) .uri("https://vulnerability-service.xxxxxxxxxxxxxxxxxx"));
  • 31.
    @ilopmar Iván López Example: GatewayRoutes (II) OpenApiRoute[ originalPath=/v1/target-platforms/{target_platform_id} newPath=/v1/tp/{target_platform_id} method=GET uri=https://provisioning-service... ] GatewayRoute[ routeId=GET__/v1/tp/{target_platform_id} gatewateyPath=/v1/tp/?* regexp=/v1/tp/(?<id1>.*) rewritePath=/v1/target-platforms/${id1} order=10 ] routeLocator.route("GET__/v1/tp/{target_platform_id}", r -> r .order(10) .path("/v1/tp/?*") .and().method("GET") .filters(s -> s.rewritePath("/v1/tp/(?<id1>.*)", "/v1/target-platforms/${id1}")) .uri("https://provisioning-service.xxxxxxxxxxxxxxx"));
  • 32.
  • 33.
    @ilopmar Iván López Summary Solved theproblem in one week +1 year in Production without problems Reuse existing OSS libraries Platform agnostic Spring Cloud Gateway is awesome Public and Dynamic OpenAPI
  • 34.