Kong GatewayのOAS Validation Pluginの使い方

本記事は「Kong Advent Calendar 2024」の10日目のエントリとして、OAS Validation Pluginについて解説する。

APIを開発している時、APIサーバをデプロイしたものの、クライアント側の更新が追いつかず、結果としてサーバ側が期待するリクエストとは異なるリクエストをクライアントが飛ばしてしまうことがある。
これをKongで検知してサーバ側ではなくKongからエラーを返してトラシュを楽にしてくれるのが[OAS Validation Plugin](https://docs.konghq.com/hub/kong-inc/oas-validation/)である。
どんな感じで使えるのが実際に触って確かめてみる。
## 検証
ここでは[チュートリアル](https://docs.konghq.com/hub/kong-inc/oas-validation/#tutorial)を参考にSwagger.ioの[Swagger Petstore](https://petstore.swagger.io/)を使って検証する。

### 設定

Petstoreに繋ぐためのKong GatewayのServiceを作成する。
```sh
curl -X POST http://localhost:8001/services/ \
    --data name='Petstore-Service' \
    --data url='https://petstore.swagger.io/v2'
```
OAS Validation Pluginにはパスのチェックも含まれているため、ServiceとRouteのURLの切り方は少し意識する必要がある。
具体的にはAPIのエンドポイントが
`<Serviceのホスト>/<Routeのパス>`
となっている時、API Specには`<Routeのパス>`だけ含まれている必要がある。
例えば
https://petstore.swagger.io/v2/pet/findByStatus
というアクセス先に対し、API Specで`/pet/findByStatus`が定義されている場合、Serviceは`https://petstore.swagger.io/v2`となる。

次にRouteを作成する。
```sh
curl -X POST http://localhost:8001/services/Petstore-Service/routes \
    --data name='Petstore-Route' \
    --data paths='~/.*' \
    --data "strip_path=false"
```
少し面倒なのが、PluginによるパスチェックはRouteのパスも含まれる。
なので、ここで`paths=~/petstore/.*`などとすると、パスチェックの際に`/petstore/`から始まるパスがAPI Specにあるかどうかをチェックしてしまうので、ここでは`~/*`としてAPI Specのパスから始まるように調整する。
（多分`/petstore/`などの指定もやり方次第で出来ると思うが、今回は確認していない）
また、`strip_path`を無効化しないとパスが消えた状態で渡ってしまうのでエンドポイントから404が返ってくる。
単にチェックしたいだけの目的で使うならそれでもいいが、実際にアクセスしたい場合は今回の設定では`false`にする必要がある。

次にPluginを設定する。
Pluginでは以下のパラメータを指定する。
- `config.api_spec`：リクエストがAPI Specに沿っているかを確認する際に使うAPI Spec（文字列）
- `config.verbose_response`：エラー時にメッセージを詳細化する
- `config.api_spec_encoded` : URIエンコードした文字列を渡す場合は`true`にする

なお、今回はYAMLファイルでAPI Specを渡すが、API SpecをJSONで書いている人もいるかもしれない。
その場合は`api_spec_encoded`は不要になる。

次にValidation対象のAPI Specを用意する。
ここではSwaggerの[Petstore](https://github.com/swagger-api/swagger-petstore)が提供しているAPI Specを利用する。
以下でAPI Specをダウンロードする。
```sh
wget https://petstore3.swagger.io/api/v3/openapi.json -O /tmp/swagger_v3.json
```
今回はYAML形式のSpecを検証するため、JSON形式をYAMLに変換する。
```sh
yq -P < /tmp/swagger_v3.json > ./swagger_v3.yaml
```
PluginをServiceに対して設定する。
```sh
curl -X POST http://localhost:8001/services/Petstore-Service/plugins \
     --data name='oas-validation' \
     --data-urlencode config.api_spec@swagger_v3.yaml \
     --data config.verbose_response=true \
     --data config.api_spec_encoded=true
```
`--data-urlencode`を使うことでYAMLをURIエンコードした状態で送っている。

### 動作確認
今回使う[Petstore](https://petstore.swagger.io/)はペットの情報をAPIで取得できるサンプルのサービスである。
色々なエンドポイントが用意されているが、ここでは`/pet/findByStatus`を触ってみる。
このエンドポイントはstatusのクエリが必須となっている。
![20241115165353.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/eb768636-8ee1-8599-bafe-7953da65fe03.png)
  
API Specでは以下のように定義されている。
```yaml
  /pet/findByStatus:
    get:
      tags:
        - pet
      summary: Finds Pets by status
      description: Multiple status values can be provided with comma separated strings
      operationId: findPetsByStatus
      produces:
        - application/json
        - application/xml
      parameters:
        - name: status
          in: query
          description: Status values that need to be considered for filter
          required: true
          type: array
```
ここにクエリなしでリクエストしてみる。
```sh
$ curl -X GET "http://localhost:8000/pet/findByStatus" -H "accept: application/json"
```
すると即座にレスポンスが返ってきた。
```json
{"message":"query 'status' validation failed with error: 'required parameter value not found in request'"}
```
エラーメッセージには「必須パラメータがリクエストにない」と記載されており、OAS Validation PluginがAPI Specの`required: true`からパラメータの不足を検知して、エンドポイントに飛ばすことなくエラーを検知してくれた。
なお、エラーコードは400となった。
OAS Validation Pluginがない場合はこんな感じ。
```sh
$ curl -X GET "https://petstore.swagger.io/v2/pet/findByStatus" -H "accept: application/xml"
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><pets></pets>
```
エラーコードは200であり、メッセージもエラーではないので原因が特定できない。
このように、OAS Validation Pluginがあればデバッグが少し楽になる。
ちなみにクエリをつけてやると、正常にデータが取得できる。
```json
$ curl -X GET "http://localhost:8000/pet/findByStatus?status=available" -H "accept: application/json" -s | jq . | head -n 5
[
  {
    "id": 9223372016900015390,
    "category": {
      "id": 0,
```
もう少し試してみる。
statusで渡す値がおかしい場合：
```json
$ curl -X GET "http://localhost:8000/pet/findByStatus?status=hoge" -H "accept: application/json"
{"message":"query 'status' validation failed with error: 'failed to validate item 1: matches none of the enum values'"}
```
パスが違う場合：
```json
$ curl -X GET "http://localhost:8000/findByStatus?status=xxx" -H "accept: application/json"
{"message":"validation failed, path not found in api specification"}
```
それぞれ、なぜエラーとなったかを知るためのヒントがメッセージとして表示されたので、やはりトラシュはしやすそうだ。


## 所感
API SpecとあっているかをAPIデプロイ後に簡単に検証できるため、開発時にクライアント側の修正漏れを検知したりするのには便利そう。
一方で、リクエストを止めてAPI Specとリクエストを比較して検証を行うため、性能面に影響がありそうな点は気をつけた方が良さそう。
あくまでも開発時のテスト目的がメインの使用方法と捉え、本番でも使いたい人はしっかり性能試験も行った上で使うのがよいと思われる。