kagamihogeの日記

kagamihogeの日記です。

Spring Cloud Gatewayさわる

https://spring.io/projects/spring-cloud-gateway をさわる。はじめてさわるのでpredicateとかfilterとかをいくつか使ってみる。使い方わからなかったやつはさわってない。個人の日記レベルのさわってみた程度なんで、ちゃんとした情報はリファレンスを参照願います。

やってみる

build.gradle

https://start.spring.io/gatewayと入力すればorg.springframework.cloud:spring-cloud-starter-gatewayが入る。以下はそれで生成したもののコピペ。

plugins {
  id 'org.springframework.boot' version '2.2.6.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}

repositories {
  mavenCentral()
}
ext {
  set('springCloudVersion', "Hoxton.SR4")
}
dependencies {
  implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
  implementation 'org.springframework.boot:spring-boot-devtools'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

gatewayのアプリケーション

gatewayのアプリケーションを作る。以下は2つのrouteを定義している。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("path_google", r -> r.path("/search")
            .uri("https://www.google.co.jp"))
        .route("host_yahoo", r -> r.host("localhost:8080").and().path("/hotentry/all")
            .uri("https://b.hatena.ne.jp"))
        .build();
  }
  
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

どちらもrouteの引数は識別子なので、適当な一意の値を振る。

  1. 1つ目はhttp://localhost:8080/searchにアクセスするとhttps://www.google.co.jp/searchに飛ぶ。http://localhost:8080/search?q=hogeとかをブラウザでアクセスするとそれっぽい検索結果が表示される*1。なお、本来の使い方はAPIのプロキシ的な使い方なので、https://www.google.co.jpに飛ばすのは意味のある例ではないが、適当なAPI用意するの面倒なのでこのようにしている。動作確認できれば良いや、という判断*2
  2. 2つ目はホストがlocalhost:8080かつリクエストパスが/hotentry/allであればhttps://b.hatena.ne.jpに飛ばす。http://localhost:8080/hotentry/allにブラウザでアクセスするとそれっぽいホットエントリが表示される。

上記は設定ファイル(application.yamlとか)で書くこともできる。が、このエントリではそちらはやらない。

predicate

before/after/between

あるZonedDateTimeの、以前・以降・期間、にマッチしたときだけルーティングする。

      .route("path_google", r -> r.before(ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
          .uri("https://www.google.co.jp"))
      .route("path_google", r -> r.after(ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
          .uri("https://www.google.co.jp"))
      .route("path_google", r -> r.between(
          ZonedDateTime.of(2020, 5, 1, 0, 0, 0, 0, ZoneId.systemDefault()),
          ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
          .uri("https://www.google.co.jp"))

cookie

cookieが指定の値を持ってるかどうか。正規表現使用可能なので下はvalueとかにマッチする。

      .route("path_google", r -> r.cookie("hogehoge", "va..e")
          .uri("https://www.google.co.jp"))

header

headerがあるか or headerが指定の値を持ってるかどうか、をチェックする。

      .route("path_google", r -> r.header("X-Hoge", "va..e")
          .uri("https://www.google.co.jp"))

host

hostが指定の値かどうか。正規表現使えるので、ドメイン名で何らかの分岐するときに使う感じか。

method

GETとかPOSTとかをチェックする。

      .route("path_google", r -> r.method(HttpMethod.GET)
          .uri("https://www.google.co.jp"))

path

上で触れたので省略。

query

上で触れたので省略。

remoteAddress

CIDRで条件をかける。

      .route("path_google", r -> r.remoteAddr("192.168.1.1/24")
          .uri("https://www.google.co.jp"))

weight

重み付け。たとえば、以下は8:2の重み付けでgoogletwitterにアクセスが飛ぶ。

        .route("path_route_high", r -> r.weight("group1", 8)
            .uri("https://www.google.com"))
        .route("path_route_low", r -> r.weight("group1", 2)
            .uri("https://twitter.com"))

filter

gatewayにリクエストが来たときと、ルーティング先から戻ってきたときに、何らかの処理を挟みたい場合にfilterを使う。

AddRequestHeader

gatewayで何らかのリクエストヘッダーを追加する。

        .route("path_google", r -> r.path("/{hoge}")
            .filters(f -> f.rewritePath("/.*", "/tools/request_headers.html").addRequestHeader("X-Hoge", "{hoge}"))
            .uri("https://uchy.me/"))

動作確認にはHTTPリクエストヘッダー表示ツール https://uchy.me/tools/request_headers.html を使わせて頂いた。http://localhost:8080/sample-valueとかするとX-Hoge sample-valueヘッダーが送信される。上記のように、pathのURLの変数を{hoge}にしてaddRequestHeaderでその値を使うことができる。

AddRequestParameter

gatewayで何らかのリクエストパラメータを追加する。

        .route("path_google", r -> r.path("/{hoge}")
            .filters(f -> f.rewritePath("/.*", "/search").addRequestParameter("q", "{hoge}"))
            .uri("https://www.google.com/"))

上はhttp://localhost:8080/search-wordにアクセスするとhttps://www.google.com/search?q=search-wordに行く。

AddResponseHeader

gatewayのクライアントに返すレスポンスにヘッダーを追加する。

        .route("path_google", r -> r.path("/search")
            .filters(f -> f.addResponseHeader("X-Hoge", "hoge"))
            .uri("https://www.google.com/"))

DedupeResponseHeader

試してない。

Hystrix GatewayFilter

リファレンスにこんなことが書いてある。なのでSpring Cloud CircuitBreaker GatewayFilterを使うのが良いと思われる。

Netflix has put Hystrix in maintenance mode. We suggest you use the Spring Cloud CircuitBreaker Gateway Filter with Resilience4J, as support for Hystrix will be removed in a future release.

Spring Cloud CircuitBreaker GatewayFilter

CircuitBreakerの詳細な説明は省略(俺自身ちゃんと理解してないので)して、とりあえず使ってみる。

まずCircuitBreakerの実体としてresilience4jを依存性に追加する。

  implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'

とりあえず使ってみる。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class DemoApplication {
  
  @GetMapping("/forward")
  public String forward() {
    return "forward";
  }

  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("path_google", r -> r.path("/")
            .filters(f -> f.circuitBreaker(
                c -> c.setName("myCircuitBreaker")
                .setFallbackUri("forward:/forward")))
            .uri("http://asdasdasdf:8080/"))
        .build();
  }
  
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

上のコードは、http://localhost:8080/にアクセスすると存在しないhttp://asdasdasdf:8080/に飛ぼうとするので、fallback-uriforward:/forwardに飛ぶ。

FallbackHeaders

CircuitBreakerと組み合わせて使うもののようだけど、よくわからない……

MapRequestHeader

あるリクエストヘッダーを別の名前としても送信する。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.rewritePath("/.*", "/tools/request_headers.html").mapRequestHeader("X-FROM", "X-TO"))
            .uri("https://uchy.me/"))

上記のようにすると、リクエストヘッダーX-FROMにプラスでX-TOも送信される。

PrefixPath

ルーティング前にリクエストパスにプレフィクスを追加する。

        .route("path_google", r -> r.path("/{date}")
            .filters(f -> f.prefixPath("/hotentry/all/"))
            .uri("https://b.hatena.ne.jp/"))

たとえばhttp://localhost:8080/20200504とかするとhttps://b.hatena.ne.jp/hotentry/all/20200504にアクセスが行く。

PreserveHostHeader

試してない。

RequestRateLimiter

なんかむつかしそうなので試してない。

RedirectTo

リダイレクトする。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.redirect(302, "https://b.hatena.ne.jp/hotentry/all"))
            .uri("forward:/forward"))

RemoveRequestHeader

任意のリクエストヘッダーを削除する。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.removeRequestHeader("X-Hoge"))
            .uri("forward:/forward"))

RemoveResponseHeader

なんかエラーになる。追加で設定が必要ぽい?

java.lang.UnsupportedOperationException: null
    at org.springframework.http.ReadOnlyHttpHeaders.remove(ReadOnlyHttpHeaders.java:131) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
 Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 

RemoveRequestParameter

リクエストパラメータを削除する。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.removeRequestParameter("hoge"))
            .uri("forward:/forward"))

たとえばhttp://localhost:8080/?hoge=hogeとかするとhogeパラメータが削除される。

RewritePath

        .route("path_google", r -> r.path("/**")
            .filters(f -> f.rewritePath("path-is-(?<segment>.*)", "${segment}"))
            .uri("https://www.google.com"))

http://localhost:8080/path-is-searchとかするとhttps://www.google.com/searchにアクセスがいく。

Spring Cloud Gatewayとは直接関係ないが(?<name>X)という記法は https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/Pattern.html にある通り「名前付きの前方参照を行う正規表現グループ」というもの。

RewriteLocationResponseHeader

試してない。

RewriteResponseHeader

なんかうまく動いてくれない。

        .route("path_google", r -> r.path("/**")
            .filters(f -> f.rewriteResponseHeader("X-Hoge", ".*", "foo"))
            .uri("http://localhost:8080/"))

SaveSession

Spring SessionとかSpring Securityとかが云々って書いてあるので試してない。

SecureHeaders

試してない。

SetPath

        .route("path_google", r -> r.path("/{date}")
            .filters(f -> f.setPath("/hotentry/all/{date}"))
            .uri("https://b.hatena.ne.jp/"))

http://localhost:8080/20200501とかするとhttps://b.hatena.ne.jp/hotentry/all/20200501にアクセスがいく。

SetRequestHeader

addでなくてreplaceする、とリファレンスに書いてあるけどイマイチ違いがわからん。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.setRequestHeader("X-Hoge", "hogeValue"))
            .uri("forward:/forward"))

SetResponseHeader

なんか実行時例外になる。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.setResponseHeader("X-Hoge-Hoge", "hogeValue"))
            .uri("forward:/forward"))

SetStatus

gatewayクライアントに返すときにステータスを変更する。

        .route("path_google", r -> r.path("/")
            .filters(f -> f.setStatus(999))
            .uri("https://b.hatena.ne.jp/"))

上にするとステータスコード999が返ってくる。

StripPrefix

リクエストのパスを前から指定数分削除する。

        .route("path_google", r -> r.path("/**")
            .filters(f -> f.stripPrefix(2))
            .uri("https://b.hatena.ne.jp/"))

http://localhost:8080/strip1/strip2/hotentry/allとするとhttps://b.hatena.ne.jp/hotentry/allに行く。

Retry

リトライ。

        .route("path_google", r -> r.path("/**")
            .filters(f -> f.retry(3))
            .uri("http://localhost:8081/"))

動作確認用に、以下のような適当な5xxを返すエンドポイントを用意して(ポートは8081)、

@RestController
@SpringBootApplication
public class Main {
  
  @ResponseStatus(value = HttpStatus.BAD_GATEWAY)
  @GetMapping("/return-5xx")
  public String return5xx(@RequestHeader Map<String, String> map) {
    System.out.println("5xx");
    return "";
  }

  public static void main(String[] args) {
    SpringApplication.run(Main.class, args);
  }

}

http://localhost:8080/return-5xxとすると3回リトライする。

GatewayFilterSpec.retry(int retries)javadocによると、デフォルトでは5xxかつGETのときリトライ、と書いてある。細かく挙動を指定したい場合は別のメソッドを使う。他に指定可能な条件としては、例外とかbackoffとかがある。

RequestSize

リクエストのサイズに制限をかける。

        .route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
            .filters(f -> f.setRequestSize(DataSize.ofBytes(1)))
            .uri("https://b.hatena.ne.jp/"))

上のようにするとmax1byte制限になるので、適当なサイズのPOSTをすると413Request Entity Too Largeが返される。

ModifyRequestBody

リクエストボディを書き換える。

        .route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
            .filters(f -> f.modifyRequestBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase())))
            .uri("http://httpbin.org/"))

上はPOSTの中身を大文字に書き換えている。

ModifyResponseBody

レスポンスボディを書き換える。

        .route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
            .filters(f -> f.modifyResponseBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase())))
            .uri("http://httpbin.org/"))

上はgateway先から返ってきたレスポンスボディを大文字に書き換えている。

*1:gatewaygoogleにアクセスした結果のhtmlを返すのでブラウザで表示すると色々表示がおかしくなる

*2:http://httpbin.org の存在はこのエントリの後半に気づいた……