Transformer Pluginいらず！jq Pluginの活用方法

本記事は「Kong Advent Calendar 2024」の18日目のエントリとして、[jq Plugin](https://docs.konghq.com/hub/kong-inc/jq/)について解説する。

Kong Gatewayでリクエスト/レスポンスの書き換えを行おうと思った場合、真っ先に出てくるのがRequest/Response Transformer Pluginではないだろうか。
確かにリクエストの変換には便利なプラグインであるが、単純なCRUDにはいいけど条件式みたいなのが書けなかったり少し複雑な事をしようとすると使い勝手が悪かったりする。
Luaの拡張もあるが、これもLuaの知識が必要だったり適用範囲が限られていたりと使いづらい。
そこで今回はTransformer Pluginよりも使い勝手がよいと思ってる、[jq Plugin](https://docs.konghq.com/hub/kong-inc/jq/)を紹介したい。

## jq Pluginとは
簡単に言うと、リクエストやレスポンスに対して[jqコマンド](https://jqlang.github.io/jq/)のようなことが出来るプラグインである。
jqはJSONを表示・加工するためのコマンドで、例えば以下のような感じでJSONを編集できる。
```json:元データ
[
  { "id": 1, "name": "Alice", "age": 25 },
  { "id": 2, "name": "Bob", "age": 30 },
  { "id": 3, "name": "Charlie", "age": 35 }
]
```
30歳以上の人の名前に`Updated: `をつける。
```json:コマンド例
cat data.json | jq 'map(if .age >= 30 then .name = "Updated: \(.name)" else . end)'
```
```json:実行結果
[
  { "id": 1, "name": "Alice", "age": 25 },
  { "id": 2, "name": "Updated: Bob", "age": 30 },
  { "id": 3, "name": "Updated: Charlie", "age": 35 }
]
```
このような変換をリクエスト/レスポンスに対して行うのがjq Pluginとなる。
なお、OSS版では利用できないので注意。

Pluginのパラメータとしては以下が[用意されている](https://docs.konghq.com/hub/kong-inc/jq/configuration/)。
- `request_jq_program`
- `request_jq_program_options`
- `request_if_media_type`
- `response_jq_program`
- `response_jq_program_options`
- `response_if_media_type`
- `response_if_status_code`

`request_`と`response_`でそれぞれ加工対象がリクエストかレスポンスを指定し、`_jq_program`がついているものでjqの評価式を指定する。
`_program_options`はjqコマンドのオプションにそのまま対応し、`if_media_type`はヘッダのメディアタイプが指定した値と合致する場合のみ動作する（デフォルトは`application/json`）。
`if_status_code`はレスポンスのステータスコードを見ており、これが合致する場合のみ動作する（デフォルトは`200`）。
これらのパラメータを組み合わせて様々な条件で様々な方法で動作させる事が出来る。

## 検証の前提
ここではDockerでLocalPCにKong Gatewayを立てて、そこに対してPlugin設定などを行う。
検証開始時、Kong Gatewayは既に起動済みとし、ここでは起動方法については触れない。

## 検証
送ったリクエストの中身を確認できる[httpbin.org](https://httpbin.org/)がKongでも[ホスティングされている](https://httpbin.konghq.com/)のでこれを使ってリクエストやレスポンスが書き換わるかを確認する。
最初にServiceとRouteを作成する。
```sh
ADMINAPI=http://localhost:8001
URI=https://httpbin.konghq.com/
SVC_NAME="httpbin-kong"
curl -s -X POST ${ADMINAPI}/services \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"${SVC_NAME}\",\"url\":\"$URI\"}" 
```
```sh
RT_NAME=jq-test
RT_PATH=/jq-test
curl -s -X POST ${ADMINAPI}/services/${SVC_NAME}/routes \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"${RT_NAME}\",\"paths\":[\"${RT_PATH}\"]}" 
```
これで`localhost:8000/jq-test`にアクセスすると`https://httpbin.konghq.com/`に転送されるようになった。
```json
$ curl localhost:8000/jq-test/user-agent
{
  "user-agent": "curl/8.7.1"
}
```

前準備は終わったので、Pluginの設定に移る。
今回は冒頭で伝えた、年齢が30歳以上だったら文字列を追加するような事が出来るかをまず確認してみる。
ここでは便宜上、環境変数に評価式を設定し、それをcurlで登録する。
```sh
JQ='map(if .age >= 30 then .name = \"Updated: \\(.name)\" else . end)'
```
curlの-dに渡す時用にエスケープ記号が必要なので注意
なお、curlを使って登録するためにエスケープなどを実施しているが、UIからだとjqの評価式がそのまま貼れるので、検証する際はそちらの方がオススメ。

次にServiceにjq Pluginを設定する。リクエストに差し込みたいので`request_jq_program`にjqの評価式を設定する。
```sh
curl -X POST ${ADMINAPI}/services/${SVC_NAME}/plugins \
     -H "Content-Type: application/json" \
     -d '{
       "name": "jq",
       "config": {
         "request_jq_program": "'"$JQ"'"
       }
     }'
```
deckを使うと適切に設定されていることが分かる。エスケープ記号が適切に反映されたかはUIからか、deckから確認しておくとよい。
```sh
$ deck gateway dump | grep -A1 -w request_jq_program
      request_jq_program: 'map(if .age >= 30 then .name = "Updated: \(.name)" else
        . end)'
```
動作確認してみる。
以下のような感じで冒頭でjqの動作確認に使ったJSONをProxyに投下する。
```sh
curl -X POST http://localhost:8000/${RT_PATH}/anything \
     -H "Content-Type: application/json" \
     -d '[
         { "id": 1, "name": "Alice", "age": 25 },
         { "id": 2, "name": "Bob", "age": 30 },
         { "id": 3, "name": "Charlie", "age": 35 }
     ]'
```
結果は以下となった（抜粋）。
```json
  "data": "[{\"id\":1,\"name\":\"Alice\",\"age\":25},{\"id\":2,\"name\":\"Updated: Bob\",\"age\":30},{\"id\":3,\"name\":\"Updated: Charlie\",\"age\":35}]\n",
```
ちなみにProxyを通さず直で叩いた場合はこちら。
```json
  "data": "[\n         { \"id\": 1, \"name\": \"Alice\", \"age\": 25 },\n         { \"id\": 2, \"name\": \"Bob\", \"age\": 30 },\n         { \"id\": 3, \"name\": \"Charlie\", \"age\": 35 }\n     ]",
```
書き換えたい箇所が変わっていることが確認できた。

次にレスポンスでも試してみる。
今度はヘッダの書き換えを試してみる。
User-Agentにcurlという文字列が含まれたら"Firefox"に置換するjqの評価式を以下で環境変数に設定する。
```sh
JQ='.headers |= with_entries(if .value | contains(\"curl\") then .value = \"Firefox\" else . end)'
```
Pluginを適用する。
```sh
curl -X POST ${ADMINAPI}/services/${SVC_NAME}/plugins \
     -H "Content-Type: application/json" \
     -d '{
       "name": "jq",
       "config": {
         "response_jq_program": "'"$JQ"'"
       }
     }'
```
curlで動作確認する。
```sh
$ curl -s -X GET http://localhost:8000/jq-test/anything -H "Content-Type: application/json" | jq . |grep -i user-agent
    "User-Agent": "Firefox",
```
書き換わったことが確認できた。

こんな感じでjq Pluginを使うとリクエストやレスポンスを簡単に書き換えることが出来る。
また嬉しいのはjqコマンドの評価式がそのまま使えるので、Luaのようなものを書いてトラシュする必要もなく、手元で作った評価式を貼り付けるだけで動くのもありがたい。
ということで、リクエストやレスポンスの書き換えにはTransformer Plugin以外にも強力な選択肢があることを理解していただければ幸いである。