前回の記事では、「HRMOSタレントマネジメント」内にあった複数の認証基盤を統合させるための大まかな流れを紹介しました。API Gatewayパターンの実装には複数の選択肢がありますが、我々はSpring WebFluxベースのフレームワークであるSpring Cloud Gatewayを使用しました。本記事では、Spring Cloud Gatewayを選択した背景、実際にSpring Cloud Gatewayを使用して得られた知見を紹介します。
何故Spring Cloud Gatewayを選択したのか
API Gatewayパターンの実装では、Amazon API Gateway等のマネージドサービス、Kong等のミドルウェアの使用、独自に実装するといった選択肢が挙げられます。その中で何故Spring Cloud Gatewayを使用して独自に実装したのか説明します。
「HRMOS」シリーズは製品が日々増えており、これらの変化にAPI Gatewayは順応が必要です。このような環境でマネージドサービスの使用は料金面での見通しが立てにくいため採用を見送りました。また、ミドルウェアの使用についても、将来増えるであろう要件に耐えうるか不明であったため採用を見送りました。
独自に実装する場合、フレームワークを使用するのか、使用するのであれば何を使用するのか検討が必要です。「HRMOS」ではKotlinを使用したSpring Bootで開発したサービスを運用しており、Springエコシステムに存在するAPI GatewayのフレームワークSpring Cloud Gatewayは既存の知識や知見を流用できると考え、使用候補に挙がりました。 Spring Cloud Gatewayで使用しているSpring WebFluxは、Netty等のノンブロッキングサーバー上で実行されているため、性能面での期待もできます。また、Spring Cloud Gatewayは機能の拡張も容易に行える機構になっているため、要件の本質的な実装に時間をかけることができると判断して採用しました。
Spring Cloud Gatewayを使用した実装例
ここでは、Spring Cloud Gatewayを使用した具体的な実装例について紹介します。
Spring Cloud Gatewayでは、多くの処理をapplication.yaml
に定義して設定できます。下記はAPI GatewayへのリクエストのHostヘッダーがservice.example
であった場合にhttp://service-backend.example
へリクエストをプロキシする設定例です。
application.yaml
の設定例:
|
|
簡単な設定でAPI Gatewayへのリクエストをプロキシできることが分かると思います。また、アプリケーション内のコードで同様の設定が行えます。
Kotlinでの実装例:
|
|
複数Hostに対するプロキシも下記のように簡単に行なえます。
application.yaml
の設定例:
|
|
Spring Cloud GatewayはSpring WebfluxのWebFilter
と、図1のFilterに該当するSpring Cloud Gateway内のGatewayFilter
, GlobalFilter
を使用してリクエストに対して独自の処理が行えます。
画像は公式ドキュメントより引用1
GatewayFilter
を用いて独自の処理を行いたい場合、Spring Cloud Gatewayで用意されているAbstractGatewayFilterFactory
で実現できます。
プロキシ時に、Cookieのセッション情報からIDトークンを生成しヘッダーに付与する例を下記に示します。
Kotlinでの実装例:
|
|
作成したTokenTranslationFitlerFactory
は次のようにプロキシの設定と紐づけできます。
application.yaml
の設定例:
|
|
Kotlinでの実装例:
|
|
このように、Spring Cloud Gatwayではプロキシや独自の処理を容易に実装できるため、採用を決定しました。
実際に使用して得られたSpring Cloud Gatewayの知見
ここからは、実際に使用して得られたSpring Cloud Gatewayの知見を紹介します。
1. Spring Cloud GatewayのCORS設定がAPI GatewayのすべてのHostに対して同一のパスにしか行えない
Spring Cloud GetewayではCORS処理をするための設定が用意されています。下記にapplication.yaml
での設定例を示します。
application.yaml
の設定例:
|
|
設定例に[/service_1/**]
とあるように、API Gatewayのパス毎にCORS処理の設定が可能です。しかし我々は、リクエストのHostを元にしてAPI Gatewayから各サービスのプロキシを行なっていたため、Host毎にCORS処理の設定を行いたかったのですが、パス毎の設定しか行えませんでした。
この設定を行った場合、Spring Cloud Gatewayの内部ではベースになっているSpring Webluxのクラスに対して設定値を渡し、CORS処理を行っています。このSpring WebfluxのCORS処理は、外部からの差し替えが難しい構造になっており、Host毎にCORS処理の設定をすることは容易ではありませんでした。
具体的には、CORS処理の設定を行うとSpring Cloud Gatewayの内部でSpring Webluxのクラス(AbstractHandlerMapping
)に設定値を渡し、CORS処理を行うクラスCorsProcessor
がAbstractHandlerMapping
内でCORS処理を行います。そのため独自のCORS処理をしたい場合、AbstractHandlerMapping
内のCorsProcessor
を独自のCORS処理を実装したクラスへの差し替えが必要です。しかし、CorsProcessor
はAbstractHandlerMapping
内で初期化されており、SpringのBeanといった機構で差し替えることができません。そのため我々は、AbstractHandlerMapping
を管理しているDispatcherHandler
の処理をoverrideすることにより差し替えました。
Kotlinでの実装例:
|
|
また、この問題に関連してSpring Cloud GatewayがCORSのプリフライトリクエストで使用するOPTIONSリクエストをプロキシできない問題Issue(#830), Issue(#2472)があります。Spring Cloud GatewayのCORS処理がSpring Webfluxに依存しているため、改善が難しいこともあり、現状(2022/12時点)で改善は進められていないので注意が必要です。
2. 不正なURLクエリのリクエストにプロキシを行わずにHTTP 400 Bad Requestを返却する
不正なURLクエリのリクエストに対してAPI Gatewayがプロキシを行わずにHTTP 400 Bad Requestを返却する事象が発生しました。また、INFO
以上のログを出力するようにしている場合、ログには何も出力せずにHTTP 400 Bad Requestを返却していたため、プロキシを行わずにレスポンスしている事に気づくのが困難でした。
具体的な再現条件としては、?param1=true?param2=false
のような?
が続いた不正なクエリを与えたhttp://localhost:8080/example?param1=true?param2=false
にリクエストがあると発生し、ログレベルをDEBUG
にすることで下記の様なログが出力されます。
|
|
この処理はSpring WebのHttpWebHandlerAdapter内で行われており、この振る舞いを無効にするためには多くの処理の差し替えが必要です。そのため我々のチームでは、org.springframework.web.server.adapter.HttpWebHandlerAdapter
のログレベルをDEBUG
に変更することで発生を検知できるようにしました。
3. Connection has been closedの発生
reactor.netty.channel.AbortedException: Connection has been closed
が、API Gatewayの開発中に発生しました。
「HRMOSタレントマネジメント」ではAPI Gatewayからプロキシ先の各サービスへAWSのALB(Application Load Balancer)を介して接続しており、ALBには一定時間(デフォルトは60秒)データが送受信されなかった場合にコネクションが切断されるアイドルタイムアウトが設定されています。一方で、Spring Cloud Gatewayは一度接続したコネクションを使い回すように設計されており、デフォルトではアイドルタイムアウトに近い値は設定されていません。
そのためALBは、一定時間経過後にAPI Gatewayとの接続をアイドルタイムアウトにより切断していましたが、API Gateway側ではそのコネクションを再利用しようとしてConnection has been closed
が発生していました。
Spring Cloud Gatewayにも 一定時間データが送受信されなかった場合に接続を切断する設定spring.cloud.gateway.httpclient.pool.max-idle-time
が存在するため、この値をALBのアイドルタイムアウトより小さい値を設定することでこの問題を解決しました。
application.yaml
の設定例:
|
|
他のタイムアウトの値も設定できるようになっているため、必要に応じて設定をしておくべきです。
4. 環境によって Filter のOrder が変化する
API GatewayがローカルとAWS環境では正常に動作するものの、GitHub Actions上でビルドしたローカル開発向けのDocker Imageから起動すると期待通りの振る舞いを行わない問題が発生しました。
Spring Cloud Gatewayを使用した実装例で紹介したように、Spring Cloud GatewayではFilter
を用いて独自の処理を行うことができ、Filter
の適用順はOrder
を設定して変更できます。下記にGlobalFilter
の適用順を設定する例を示します。
Kotlinでの実装例:
|
|
Order
を設定していない場合や同じOrder
の値が使用されている場合、Filter
はjarファイル内のクラスの順に適用します。このクラスの順序は、jarをビルドする環境毎に変化する可能性があるため注意が必要です。
我々はGithub Actions上でAWS環境向けのDockerイメージとローカル開発向けのDockerイメージをビルドしていましたが、ローカル開発向けのDockerイメージはamd/arm向けのマルチプラットフォームイメージを作成しており、そのイメージのみjarファイル内のクラスの順序が変化してしまいました。
従って、順序が大切な処理は明示的にOrderを指定する必要があり、Orderが重複しないように注意が必要です。
まとめ
「HRMOSタレントマネジメント」では、Spring Cloud Gatewayを用いてAPI Getewayを構築しました。Spring Cloud Gatewayはプロキシや独自の処理を容易に実装できるようになっているため、要件の本質的な実装に時間をかけることができます。その反面、「実際に使用して得られたSpring Cloud Gatewayの知見」で紹介した1, 2のようなSpring Cloud GatewayがSpring Webfluxベースで作られているために発生する問題や、3, 4のようなSpring, Spring Cloud Gateway固有の問題にも注意が必要です。この記事が今後Spring Cloud Gatewayを用いてAPI Gatewayを構築される方の参考になれば幸いです。
-
Spring Cloud Gateway, https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html, (参照 2022-10-05). ↩︎