Kong Gatewayで評価式を使ってRouteを設定する

Kong Gatewayでは3.0以降、Routeの表現方法に[Expressions](https://docs.konghq.com/gateway/latest/key-concepts/routes/expressions/)というものが追加されている。
Routeを設定する時、従来はPathsやHeadersなど各項目ごとに値を設定して、条件に一致するものがRouteのパスとして評価されていたが、Expressionsの場合は複数の項目ではなく1つの評価式だけでRouteのパスを設定するような硬派な設定方法である。

従来の設定方法は以下のような感じ。
![20241030090506.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/5c4ee729-6cef-ae9b-9f7f-f64bdb8b8d50.png)

Expressionsによる設定だと以下のような感じ。
![20241030090756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e46a2947-79ad-7240-b888-523c79d86ed7.png)

Expressionsで設定する場合、以下のメリット・デメリットがある。
- メリット：
  - プラグインなしで許可しないヘッダなどの指定がブラックリスト的な書き方などが出来たり、表現方法が広がる
  - 評価式の優先順位をコントロールしたり、正規表現を避けながら複雑な式が書けるため、性能チューニングが可能
- デメリット：
  - 評価式の書き方の学習コストが発生する
  - 従来のRouteをコンバートする方法がなく、既存のRouteでExpressionに切り替える場合は書き直しが必要（`deck file openapi2kong`でも不可）

[ドキュメント](https://docs.konghq.com/gateway/latest/key-concepts/routes/#how-requests-are-routed)にも以下のように書いてあり、
> It is recommended that new deployments use the expressions router as it is more powerful and expressive.
（意訳：より強力で表現力が豊かなため、新規導入の場合はExpressionsのRouteを使用することを推奨する。）

使い方は覚えておいて損はないだろう。
ということで、今回はExpressionsの使い方をざっくり紹介する。

## Expressionsを利用するための設定

kong.confに[`router_flavor`](https://docs.konghq.com/gateway/latest/reference/configuration/#router_flavor)というパラメータがある。
デフォルトでは`traditional_compatible`となっていて、内部的にはExpressionsは利用するのだが、外部から見えず指定できない設定となっている。
Expressionsを利用する場合はこれを`expressions`に変更してKong Gatewayを起動する必要がある。
Dockerで起動する場合は`docker run`の引数に`-e "KONG_ROUTER_FLAVOR=expressions" `を追加する。

起動に成功すると、Routeの作成画面に`Expressions`のタブが表示されるようになる。
![20241030094019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/1ef76db0-abac-80fe-0727-74a8435521a8.png)

## Expressionsの前提知識
Expressionsで指定する評価式のフォーマットは[Language References](https://docs.konghq.com/gateway/latest/reference/expressions-language/language-references/)に記載がある。

最低限これだけ知っておけば良さそうな演算子（Operator）だけここで紹介する。
|演算子|意味|
|---|---|
|`==`|完全一致|
|`!=`|一致しない|
|`~`|正規表現と一致|
|`^=`|前方一致(value*)|
|`=^`|後方一致(*value)|
|`&&`|And条件|
|`\|\|`|Or条件|
|`(評価式)`|所謂算数のカッコ。評価する対象をグループ化|
|`!`|カッコの前でのみ利用でき、結果を否定する|

シェルスクリプトや一般的な開発言語の演算子とかとそれほど差はないので、コードを書いたことがある人からするとそれほど敷居は高くないと思われる。
更に細かい使い方は[Language References](https://docs.konghq.com/gateway/latest/reference/expressions-language/language-references/)の方を参考にして欲しい。

なお、正規表現については[Rustの正規表現](https://docs.rs/regex/latest/regex/#syntax)に従う。
なので、例えば「barのあとに1桁の数字が続く」というパターンの場合は`r#"bar\d"#`となる。

あと、ホストやパス、ヘッダなどの指定は[Maching fields](https://docs.konghq.com/gateway/latest/key-concepts/routes/expressions/#matching-fields)を参考にして欲しい。
1つ注意点として、**ヘッダ名は小文字とアンダースコアだけで定義する必要がある。**
例えばFoo-BaRというヘッダ名の場合はfoo_barと指定する。

## 動作検証
検証にあたり、Local PCにDockerでKong Gatewayを起動した。
また以下の手順で事前にServiceを作成している。

```sh
SVC_NAME="httpbin-service"
curl -s -X POST localhost:8001/services \
  -H "Content-Type: application/json" \
  -d '{
    "name":"'"${SVC_NAME}"'",
    "url":"https://httpbin.konghq.com"
    }' 
```
### 基本ケース：パスのみの指定
最初にパスのみを設定したRouteを作成して動作確認を行う。
ここではcurlで設定する。
curlで設定する際、Expressionの指定は`Content-Type: multipart/form-data`で渡す必要がある。
curlで指定する場合は`--form-string`で評価式を渡せばMIME typeを`mulitpart/form-data`にしてくれるので、パスを`/httpbin`としてRouteを作成する場合は以下のような式で作成できる。

```sh
curl -X POST \
 http://localhost:8001/services/${SVC_NAME}/routes \
 --form-string name=httpbin-route \
 --form-string \
   'expression=(http.path == "/httpbin")'
```
作成するとKong Managerでは以下のように評価式が確認できる。
![20241030103335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/71e789a4-9527-cd98-f1e0-c0e0cb340c9f.png)

`deck gateway dump`だと以下のようになる。
```yaml
  routes:
  - expression: (http.path == "/httpbin")
    https_redirect_status_code: 426
    name: httpbin-route
    path_handling: v0
    preserve_host: false
    priority: 0
    protocols:
    - http
    - https
    regex_priority: 0
    request_buffering: true
    response_buffering: true
    strip_path: true
```
実際にアクセスするとHTMLが取得できる。
```sh
$ curl -s  localhost:8000/httpbin | head
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
：（省略）
```
ただし、先程はパスの指定を`==`で指定したため、パスが完全一致しないとルーティングしてくれない。
なので、`/httpbin/ip`などにアクセスしても「no Route」とエラーになる。
```sh
$ curl -s  localhost:8000/httpbin/ip
{
  "message":"no Route matched with those values",
  "request_id":"cec3864c95a8cb452a0f99285f32b759"
}
```
パスの指定を前方一致の`^=`に変えてみる。

```sh
curl -X PATCH \
 http://localhost:8001/routes/httpbin-route \
 --form-string \
   'expression=(http.path ^= "/httpbin")'
```
前方一致に変えると問題なくアクセスできた。
```sh
$ curl -s  localhost:8000/httpbin/ip
{
  "origin": "192.168.127.1"
}
```


### 複雑なケース：ヘッダによる指定
先程作成したRouteにヘッダによる条件も足してみる。
ここでは少し複雑な条件として、以下の時にルーティングするよう設定する。
- `my-header: foo`または`my-header: bar`が指定されてたら許可
- `my-bad-header`が指定されていたら不許可
```sh
curl -X PATCH \
 http://localhost:8001/routes/httpbin-route \
 --form-string \
   'expression=(http.path ^= "/httpbin")
      && ((http.headers.my_header == "foo" || http.headers.my_header == "bar" ) 
      && !(http.headers.my_bad_header ~ r#".*"#))'
```
ヘッダ名は前述の通りハイフンはアンダースコアに変更する必要があるので変換している。
また`my-bad-header`を弾く部分は`~`で正規表現を指定し、`r#".*"#`で何か文字列が指定していたら条件が合致する、とし、この評価式の先頭に`!`をつけることで否定することでヘッダが指定されていたら弾くようにしている。

実際に実行した結果は以下となる。
ヘッダなしの場合は以下。
```sh
$ curl -s  localhost:8000/httpbin/ip
{
  "message":"no Route matched with those values",
  "request_id":"850e8d007d405e2ad6e8e63b4502e72f"
}
```
許可されたヘッダを指定した場合は以下。
```sh
$ curl -s  localhost:8000/httpbin/ip -H "my-header: bar"
{
  "origin": "192.168.127.1"
}
```
許可されたヘッダ＋禁止ヘッダを指定した場合は以下。
```sh
$curl -s  localhost:8000/httpbin/ip -H "my-header: bar" -H "my-bad-header: true"
{
  "message":"no Route matched with those values",
  "request_id":"94f9cd786de3f270c7d763580090615d"
}
```

ということで、従来のRoute設定だと難しい複雑な設定を簡単に設定することが出来た。


## Router Playground
最後にRouter Playgroundという機能についても触れておきたい。
Kong ManagerでRoute設定する際、ExpressionsのタブをクリックしてExpressionを記入する時に`Test with Router Playground`という項目が確認できる。
これをクリックすると以下のような画面に飛び、作成した評価式に問題がないかを確認する事が出来る。
![20241030130031.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e0eba6f1-faf5-c402-354e-f148c4c759ef.png)

例えば、ヘッダにアンダースコアではなくハイフンを指定すると、以下のように枠全体が赤くなり、またエラー箇所にアンダーラインが走ってエラーを教えてくれる。
![20241030130139.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e39ba235-2236-bf3d-ae4d-54a0df077674.png)
マウスカーソルをアンダーラインに合わせるとエラーの理由も教えてくれる。（ただ、このケースだと`parsing error: expected binary_operator`と少し分かりにくいエラーメッセージになるが。。)
`Add`から接続先を書けば、`http.host`などの評価の時にハイライトしてくれたりもする。
評価式を作成する際にはこれで事前にチェックするとよさそうだ。

## まとめ
Expressionを使った場合、少しプログラミングっぽい書き方が必要で利用者の敷居を高くする反面、自由度が高くプラグインなしでも様々な条件を設定できることが確認できた。
また今回は検証しなかったが、評価式が評価される順番や正規表現で引っかかる回数などを意識して評価式を書けば性能チューニングも出来、細かく厳しい要件があるようなシステムでは使い勝手がよさそうだ。
なお、Routeの書き方を一度Expressionを選ぶと従来のTraditionalな設定は選べなくなり、またその逆もしかりなので、利用する際はその点は注意した方がよい。
あとAPIOps的なところでAPI Specから変換する場合もExpressionが利用できないようなので、パイプラインでExpressionのRouteを自動で作る場合は工夫が必要な点も気をつけておきたい。