KongのRate Limiting Advanced PluginとRedis Sentinelを組み合わせて使う

Kong Gatewayで流量制限をする際には[Rate Limiting Plugin](https://docs.konghq.com/hub/kong-inc/rate-limiting/)を使うが、これはOSS版とEnterprise版（[Rate Limiting Advanced Plugin](https://docs.konghq.com/hub/kong-inc/rate-limiting-advanced/)）で機能差がある。
機能比較した結果を一部抜粋したものが以下となる。

| 機能                           | Rate Limiting Plugin | Rate Limiting Advanced Plugin |
|--------------------------------|----------------------|-------------------------------|
| **ライセンス**                 | OSS       | Konnect PremiumかEnterpriseのライセンス要    |
| **アルゴリズム**           | 固定ウィンドウ         | スライディングウィンドウ、固定ウィンドウ          |
| **カウントポリシー**                   | local, cluster, redis(*) | local, cluster, redis |

*：Redis単体のみ。ClusterやSentinelは未サポート。

アルゴリズムの説明は[こちらのブログ](https://konghq.com/blog/engineering/how-to-design-a-scalable-rate-limiting-algorithm)が詳しいので、詳細が必要な場合は参考にするとよい。
ポリシーについては以下[公式ドキュメント](https://docs.konghq.com/hub/kong-inc/rate-limiting/#strategies)の記述を意訳したものとなる。

| 種類   | 長所  | 短所                                                  |
|--------|-------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `local`  | パフォーマンスへの影響は最小限 | 精度は低くなる。ローカルで持つため、複数ノードでDataPlaneを構成した場合、LBで均等に分散しないと偏りが生じるリスクあり |
| `cluster`| ControlPlaneのDBを使うため追加コンポーネントなしで正確にカウントできる | 各リクエスト毎にDBへのRead/Writeが発生するため、性能影響大（ただし`sync_rate`で調整可）。また**DB-Less構成では利用不可**   |
| `redis`  | 正確かつ`cluster`よりも性能影響低 | Redis のインストールが必要。`local`と比べると性能影響大|

上記より以下が言える。
- 流量制御で正確さが求められ、かつ高トラフィックを捌くようなケースではRedisが最適
- 上記のようなケースでは必然的にRedisの冗長性も求められるため、Redis Sentinelなどが必要

ということで、厳し目の流量制御が必要な場合はRate Limiting Advanced PluginでRedis Sentinel or Clusterが必要となる。
前置きが長くなったが、今回は硬くて正確な流量制限が必要な場合に取る構成となる、Rate Limiting Advanced PluginとRedis Sentinelを組み合わせた構成の作り方を確認する。
（SentinelにするかClusterにするかは迷ったが、[こちら](https://qiita.com/ono-soic/items/919797be056b5c348ed6)を参考にSentinelの方が良さげだったのでSentinelを選択した）

## 検証環境
今回は以下の環境で検証した。
- EKS
- Kong Gateway Enterprise 3.6 (Helmで構築済み)

RedisはKong Gatewayと同じクラスタ内に構築し、`Service`でアクセスするようにする。

## 検証
### Redis Sentinelの構築
BitnamiにあるHelm Chartが使えそうなのでこれを使う。
最初にBitnamiのリポジトリを追加する。
```sh
helm repo add bitnami https://charts.bitnami.com/bitnami
```
インストールする。
```sh
helm upgrade -i redis bitnami/redis --set sentinel.enabled=true -n redis --create-namespace --wait --debug
```
インストールが終わるとStatefulSetでRedis Podが展開されていることが分かる。
```sh
$ kubectl get pod -n redis
NAME           READY   STATUS    RESTARTS   AGE
redis-node-0   2/2     Running   0          40m
redis-node-1   2/2     Running   0          40m
redis-node-2   2/2     Running   0          39m
```

動作確認のため、試しに繋いでみる。
`redis-cli`を使うためにredisのPodを起動する。
```sh
kubectl run redis --image redis -n default
```
Redisのパスワードを取得する。
```sh
REDIS_PASSWORD=$(kubectl get secret -n redis redis -o jsonpath={.data.redis-password} | base64 -d)
```
Redis Sentinelに対し、マスターがどれかを聞いてみる。
```sh
kubectl exec -it -n default redis -- redis-cli -a $REDIS_PASSWORD -h redis.redis.svc -p 26379 sentinel get-master-addr-by-name mymaster
```


以下のような出力が得られれば接続に成功している。
```sh
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
1) "redis-node-0.redis-headless.redis.svc.cluster.local"
2) "6379"
```

### Service/Routeの作成
Kong GatewayのService/Routeを設定する。
ここではバックエンドのサービスは[httpbin.org](https://httpbin.org/)とし、Proxyにパス`/myhttpbin`でアクセス出来るようにする。
構築はKong ManagerからUIで行ってもいいが、ここでは[`deck`](https://docs.konghq.com/deck/latest/)コマンドを利用するものとする。
まず`deck`用のYAMLを作成する。
```yaml
cat <<EOF > ./svc-rt.yaml
_format_version: "3.0"
_workspace: redis-test
services:
- host: httpbin.org
  name: httpbin-svc
  port: 443
  protocol: https
  routes:
  - name: httpbin-rt
    paths:
    - /myhttpbin
    preserve_host: false
    protocols:
    - http
    - https
    request_buffering: true
    response_buffering: true
    strip_path: true
EOF
```
ここでは便宜上新しく`Workspace`として`redis-test`を作成している。
作成したYAMLを適用する。
```sh
deck gateway sync ./svc-rt.yaml
```
念の為動作確認として`httpbin.org/user-agent`にProxy経由でアクセスしてみる。
```sh
PROXY=<Kong GatewayのProxyのアドレス>
curl $PROXY/myhttpbin/user-agent
```
問題なければ以下のような出力が得られる。
```json
{
  "user-agent": "curl/8.6.0"
}
```

### Rate Limit Advanced Pluginの設定
ここでは作成したRouteに対してRate Limit Advanced Pluginを設定する。
Pluginの設定はこちらでも[`deck`](https://docs.konghq.com/deck/latest/)コマンドを利用して設定する。
`deck`コマンドでPluginを設定する場合、先程のService/RouteのYAMLに追記する形で書くか、[`deck file add-plugins`](https://docs.konghq.com/deck/latest/reference/deck_file_add-plugins/)というコマンドで既存の設定に別途Pluginだけ追加する方法がある。
ここでは`deck file add-plugins`を使うやり方で設定する。
Pluginの設定ファイルを以下で作成する。
```yaml
cat <<EOF > ./plugin-rate-limiting-adv.yaml
_format_version: "1.0"
add-plugins:
- selectors:
  - $..routes[*]
  overwrite: true
  plugins:
  - name: rate-limiting-advanced
    config:
      limit:
      - 5
      window_size:
      - 30
      identifier: ip
      sync_rate: 0
      namespace: redis
      strategy: redis
      redis:
        sentinel_role: master
        sentinel_addresses:
        - redis.redis:26379
        sentinel_master: mymaster
        sentinel_password: $REDIS_PASSWORD
        password: $REDIS_PASSWORD
EOF
```
細かい書き方については[公式ドキュメント](https://docs.konghq.com/deck/latest/reference/deck_file_add-plugins/)を参照して欲しい。
Rate Limitingの設定として、ウィンドウサイズを30秒、1つのウィンドウのアクセスの上限を5回とし、同期のタイミング(`sync_rate`)を0に設定して常時同期するようにした。
Redisに関する設定は`sentinel_role`、`sentinel_addresses`、`sentinel_master`が必須パラメータであり、Helmでデフォルトで入れると認証も必要となるのでパスワードもあわせて設定した。
Service/Routeに足す形で作成したPluginの設定を適用する。
```sh
deck file add-plugins -s svc-rt.yaml plugin-rate-limiting-adv.yaml | deck gateway sync -
```
コマンドを補足すると、`-s svc-rt.yaml`でPluginを追加するソースのファイルを指定し、`deck file add-plugins`はデフォルトで標準出力にYAMLを吐くので、それを`deck gateway sync`に渡している。（最後の`-`はパイプで渡された標準出力をファイルとして渡している）

## 動作確認
実際にRedisに記録されることを確認する。
Rate Limiting Advanced Pluginを設定したパスにリクエストをいくつか発行する。
```sh
for((i=0; i<20; i++)); do curl -i $PROXY/myhttpbin/user-agent; sleep .5; done
```
Redisの中を確認する。
```
kubectl exec -it -n default redis -- redis-cli -a $REDIS_PASSWORD -h redis.redis.svc -p 6379 keys "*"
```
以下のようにRedis内にキーが作られていることが確認できる。
```
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
1) "1720000830:30:redis"
```
これにより、アクセスの状況がRedisに残っている事が確認できた。