IstioのGlobal Rate LimitをOpenShift ServiceMeshで試してみる

# この記事は

Istioの「[Enabling Rate Limits using Envoy](https://istio.io/latest/docs/tasks/policy-enforcement/rate-limit/)」をOpenShift Service Meshで試してみた時のメモです。Istioの`EnvoyFilter`というカスタムリソースを利用して、サイドカー（Envoy）へのカスタイマイズを実装します。


# 背景（大事）

最初にお断りを入れておくと、**`EnvoyFilter`は明示的に文書化された使い方を除き、OpenShift Service Meshではサポートされていない、とのことです。**

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.13/html-single/service_mesh/index#ossm-envoy-filters_ossm-vs-istio

このドキュメントを知る前に、「へ～Istioで流量制御できるんだ～じゃあOpenShiftでもできるか試してみよう」という話が持ち上がり、試してしまいました。~~事前に軽くドキュメントは読みましょう~~

せっかく試したし、今後もしかしたらサポートされるかもしれない・・ということで軽くメモをまとめておきます。

# Rate Limitについてざっくりと

Istio上のサービスへのリクエスト数を制限する機能です。

[Istioのサンプルアプリ](https://istio.io/latest/docs/examples/bookinfo/)でいえば、「`/productpage`へのアクセスを1分間に10回までとする」のような制限をかけることができます。

制限をかける個所としては、~~メッシュの入り口であるIngress Gatewayと、個々のサービスに対して可能です。前者はGlobal Rate Limit、後者はLocal Rate Limitと呼ぶそうです。~~

（2024年9月10日）微妙に勘違いしていたので修正します。。あるサービス全体のリクエスト数制限をGlobal Rate Limit、あるサービスの個々のインスタンス（すなわちPod）のリクエスト数制限をLocal Rate Limitと呼ぶそうです。

Rate Limitを実現する要素としては大きく2つあります。

- Rate Limitサービス
    - クォータ管理（ざっくり言うとどの通信が何回きたか）を行うサービス。Envoyから問合せが来る。
- EnvoyFilter
    - Envoy（サイドカー）のカスタマイズをするリソース。Envoyが通信を受け取ったときの処理を色々いじれると思われる。
    - https://istio.io/latest/docs/reference/config/networking/envoy-filter/

Rate Limitサービスをデプロイし、Rate LimitをかけたいサービスにEnvoyFilterを適用していくのがおおまかな流れとなります。

# 検証の流れ

## 前提

- Red Hat OpenShift on IBM Cloud (ROKS)
- OpenShift 4.11
- すでにOpenShift Service Meshは導入済
    - [ドキュメント](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.11/html/service_mesh/installing-ossm)に従って導入。設定値はデフォルトのまま。
- ServiceMeshControlPlaneも作成済
    - [ドキュメント](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.11/html/service_mesh/ossm-create-smcp)に従って作成。こちらも設定値はデフォルトのまま。

## 作業用Projectの作成

```
$ oc new-project rate-limit-01
Now using project "rate-limit-01" on server "https://c100-e.jp-tok.containers.cloud.ibm.com:xxxxx".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app rails-postgresql-example

to build a new example application in Ruby. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=k8s.gcr.io/e2e-test-images/agnhost:2.33 -- /agnhost serve-hostname

```

## ServiceMeshMemberRollへの追加

予めdefaultというSMMRをistio-system Projectに作成していたので、`.spec.members[]`に作業用Projectを追加します。

```
$ oc edit smmr -n istio-system
servicemeshmemberroll.maistra.io/default edited
```

## Bookinfoアプリケーションのデプロイ

istio恒例のBookinfoアプリケーションをデプロイします。

アプリケーション本体（Deployment, Service, ServiceAccount）は、[OpenShiftドキュメントのチュートリアル](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.11/html/service_mesh/ossm-create-mesh#ossm-tutorial-info-install_ossm-create-mesh)をそのまま使用しました。

Gateway、VirtualServiceについては同チュートリアルのマニフェストの`hosts`に、以下のようなホスト名を追加しました。
```
rate-limit-sample.ixxxxxxxxxxxxxxxxxx0.jp-tok.containers.appdomain.cloud
```

Destination Ruleも[チュートリアル](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.11/html/service_mesh/ossm-create-mesh#ossm-tutorial-info-adding-destination-rules_ossm-create-mesh)のものをそのまま使用しました。

必要なリソース群を配置したディレクトリで`oc apply`を実行します。

```
$ oc apply -f ./
```

## Rate Limit周りのリソース作成

以降のリソースは、[Global Rate Limit](https://istio.io/latest/docs/tasks/policy-enforcement/rate-limit/#global-rate-limit)のチュートリアルを参考に作成します。

### ConfigMap

どの`Descriptor`についてクォータ設定をするか定めます。以下の書き方で、`/productpage`へのアクセスは1分間に10回まで、メッシュ全体へのアクセスは1分間に1000回までとする設定を表します。

```yaml
piVersion: v1
kind: ConfigMap
metadata:
  name: ratelimit-config
data:
  config.yaml: |
    domain: productpage-ratelimit
    descriptors:
      - key: PATH
        value: "/productpage"
        rate_limit:
          unit: minute
          requests_per_unit: 10
      - key: PATH
        rate_limit:
          unit: minute
          requests_per_unit: 1000
```

### Rate Limitサービス

Rate Limitサービスをデプロイします。あわせてデプロイするRedisは、OpenShiftの場合defaultのServiceAccountでは権限不足で正常に稼働しないので、Redis用のServiceAccountを作成し権限を付与します。

#### SevriceAccount

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: null
  name: redis
```
上記の内容でServiceAccountを作成し、anyuidを付与します。

```
$ oc adm policy add-scc-to-user anyuid -z redis
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "redis"
```

#### Rate Limitサービス+Redis

以下の内容で作成します（長いので折り畳み）。ほぼほぼ参考にしたチュートリアルどおりですが、前述のServiceAccountをRedis Deploymentで使うようにしています。

<details><summary>マニフェスト</summary>

```yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
spec:
  ports:
  - name: redis
    port: 6379
  selector:
    app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - image: redis:alpine
        imagePullPolicy: Always
        name: redis
        ports:
        - name: redis
          containerPort: 6379
      restartPolicy: Always
      serviceAccountName: redis
---
apiVersion: v1
kind: Service
metadata:
  name: ratelimit
  labels:
    app: ratelimit
spec:
  ports:
  - name: http-port
    port: 8080
    targetPort: 8080
    protocol: TCP
  - name: grpc-port
    port: 8081
    targetPort: 8081
    protocol: TCP
  - name: http-debug
    port: 6070
    targetPort: 6070
    protocol: TCP
  selector:
    app: ratelimit
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ratelimit
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ratelimit
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: ratelimit
    spec:
      containers:
      - image: envoyproxy/ratelimit:9d8d70a8 # 2022/08/16
        imagePullPolicy: Always
        name: ratelimit
        command: ["/bin/ratelimit"]
        env:
        - name: LOG_LEVEL
          value: debug
        - name: REDIS_SOCKET_TYPE
          value: tcp
        - name: REDIS_URL
          value: redis:6379
        - name: USE_STATSD
          value: "false"
        - name: RUNTIME_ROOT
          value: /data
        - name: RUNTIME_SUBDIRECTORY
          value: ratelimit
        - name: RUNTIME_WATCH_ROOT
          value: "false"
        - name: RUNTIME_IGNOREDOTFILES
          value: "true"
        - name: HOST
          value: "::"
        - name: GRPC_HOST
          value: "::"
        ports:
        - containerPort: 8080
        - containerPort: 8081
        - containerPort: 6070
        volumeMounts:
        - name: config-volume
          mountPath: /data/ratelimit/config
      volumes:
      - name: config-volume
        configMap:
          name: ratelimit-config
```
</details>

### EnvoyFilter

2つ作成します。1つ目のEnvoyFilterではどのRate Limitサービスと連携するか指定する箇所があるので、デプロイしたRate Limitサービスを指定します。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: filter-ratelimit
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
                name: "envoy.filters.http.router"
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.ratelimit
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
            # 前述のConfigMap名に一致させる必要あり
            domain: productpage-ratelimit
            failure_mode_deny: false
            timeout: 10s
            rate_limit_service:
              grpc_service:
                envoy_grpc:
                  # namespace名に注意（ratelimitをデプロイした名前空間にする）
                  cluster_name: outbound|8081||ratelimit.rate-limit-01.svc.cluster.local
                  authority: ratelimit.rate-limit01.svc.cluster.local
              transport_api_version: V3

```

2つ目のEnvoyFilterでは、リクエストヘッダーにpathがあった場合、PATHというDescriptorを作成し、その値にpathの中身を入れる設定が記載されています。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: filter-ratelimit-svc
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: VIRTUAL_HOST
      match:
        context: GATEWAY
        routeConfiguration:
          vhost:
            name: ""
            route:
              action: ANY
      patch:
        operation: MERGE
        value:
          rate_limits:
            - actions:
              - request_headers:
                  header_name: ":path"
                  descriptor_key: "PATH"
```

## リクエストを繰り返し送信

Bookinfoの`/prodcutpage`に向かって、毎秒リクエスト送信してみます。

どうもx分00秒～(x+1)分00秒の間でリクエスト数をカウントしているらしく、その時間の中で10回送信されるとステータスコード429（Too Many Requests）が返ってきました。

<details>
<summary>リクエスト送信時の出力</summary>

```
$ while true;do date; curl -I http:///rate-limit-sample.ixxxxxxxxxxxxxxxx0.jp-tok.containers.appdomain.cloud/productpage -o /dev/null -w '%{http_code}\n' -s; echo ""; sleep 1; done
Tue Jun 27 22:48:13 JST 2023
200

Tue Jun 27 22:48:14 JST 2023
200

Tue Jun 27 22:48:15 JST 2023
200

Tue Jun 27 22:48:16 JST 2023
200

Tue Jun 27 22:48:18 JST 2023
200

Tue Jun 27 22:48:19 JST 2023
200

Tue Jun 27 22:48:20 JST 2023
200

Tue Jun 27 22:48:21 JST 2023
200

Tue Jun 27 22:48:22 JST 2023
200

Tue Jun 27 22:48:23 JST 2023
200

Tue Jun 27 22:48:24 JST 2023
429

Tue Jun 27 22:48:25 JST 2023
429

Tue Jun 27 22:48:26 JST 2023
429

Tue Jun 27 22:48:27 JST 2023
429

Tue Jun 27 22:48:29 JST 2023
429

Tue Jun 27 22:48:30 JST 2023
429

Tue Jun 27 22:48:31 JST 2023
429

Tue Jun 27 22:48:32 JST 2023
429

Tue Jun 27 22:48:33 JST 2023
429

Tue Jun 27 22:48:34 JST 2023
429

Tue Jun 27 22:48:35 JST 2023
429

Tue Jun 27 22:48:36 JST 2023
429

Tue Jun 27 22:48:37 JST 2023
429

Tue Jun 27 22:48:38 JST 2023
429

Tue Jun 27 22:48:39 JST 2023
429

Tue Jun 27 22:48:40 JST 2023
429

Tue Jun 27 22:48:41 JST 2023
429

Tue Jun 27 22:48:43 JST 2023
429

Tue Jun 27 22:48:44 JST 2023
429

Tue Jun 27 22:48:45 JST 2023
429

Tue Jun 27 22:48:46 JST 2023
429

Tue Jun 27 22:48:47 JST 2023
429

Tue Jun 27 22:48:48 JST 2023
429

Tue Jun 27 22:48:49 JST 2023
429

Tue Jun 27 22:48:50 JST 2023
429

Tue Jun 27 22:48:51 JST 2023
429

Tue Jun 27 22:48:52 JST 2023
429

Tue Jun 27 22:48:53 JST 2023
429

Tue Jun 27 22:48:54 JST 2023
429

Tue Jun 27 22:48:55 JST 2023
429

Tue Jun 27 22:48:56 JST 2023
429

Tue Jun 27 22:48:58 JST 2023
429

Tue Jun 27 22:48:59 JST 2023
429

Tue Jun 27 22:49:00 JST 2023
200

Tue Jun 27 22:49:01 JST 2023
200

Tue Jun 27 22:49:02 JST 2023
200

Tue Jun 27 22:49:03 JST 2023
200

Tue Jun 27 22:49:04 JST 2023
200

Tue Jun 27 22:49:05 JST 2023
200

Tue Jun 27 22:49:06 JST 2023
200

Tue Jun 27 22:49:07 JST 2023
200

Tue Jun 27 22:49:09 JST 2023
200

Tue Jun 27 22:49:10 JST 2023
200

Tue Jun 27 22:49:11 JST 2023
429

Tue Jun 27 22:49:12 JST 2023
429

Tue Jun 27 22:49:13 JST 2023
429

Tue Jun 27 22:49:14 JST 2023
429

Tue Jun 27 22:49:15 JST 2023
429

Tue Jun 27 22:49:16 JST 2023
429

Tue Jun 27 22:49:17 JST 2023
429

Tue Jun 27 22:49:18 JST 2023
429

Tue Jun 27 22:49:19 JST 2023
429

Tue Jun 27 22:49:20 JST 2023
429

Tue Jun 27 22:49:22 JST 2023
429

Tue Jun 27 22:49:23 JST 2023
429

Tue Jun 27 22:49:24 JST 2023
429

Tue Jun 27 22:49:26 JST 2023
429

Tue Jun 27 22:49:27 JST 2023
429

Tue Jun 27 22:49:28 JST 2023
429

Tue Jun 27 22:49:29 JST 2023
429

Tue Jun 27 22:49:30 JST 2023
429

Tue Jun 27 22:49:31 JST 2023
429

Tue Jun 27 22:49:32 JST 2023
429

Tue Jun 27 22:49:33 JST 2023
429

Tue Jun 27 22:49:34 JST 2023
429

Tue Jun 27 22:49:35 JST 2023
429

Tue Jun 27 22:49:36 JST 2023
429

Tue Jun 27 22:49:38 JST 2023
429

Tue Jun 27 22:49:39 JST 2023
429

Tue Jun 27 22:49:40 JST 2023
429

Tue Jun 27 22:49:41 JST 2023
429

Tue Jun 27 22:49:42 JST 2023
429

Tue Jun 27 22:49:43 JST 2023
429

Tue Jun 27 22:49:44 JST 2023
429

Tue Jun 27 22:49:45 JST 2023
429

Tue Jun 27 22:49:46 JST 2023
429

Tue Jun 27 22:49:47 JST 2023
429

Tue Jun 27 22:49:48 JST 2023
429

Tue Jun 27 22:49:50 JST 2023
429

Tue Jun 27 22:49:51 JST 2023
429

Tue Jun 27 22:49:52 JST 2023
429

Tue Jun 27 22:49:53 JST 2023
429

Tue Jun 27 22:49:54 JST 2023
429

Tue Jun 27 22:49:55 JST 2023
429

Tue Jun 27 22:49:56 JST 2023
429

Tue Jun 27 22:49:57 JST 2023
429

Tue Jun 27 22:49:58 JST 2023
429

Tue Jun 27 22:49:59 JST 2023
429

Tue Jun 27 22:50:00 JST 2023
200

Tue Jun 27 22:50:01 JST 2023
200

Tue Jun 27 22:50:03 JST 2023
200

Tue Jun 27 22:50:05 JST 2023
200

Tue Jun 27 22:50:06 JST 2023
200

Tue Jun 27 22:50:07 JST 2023
200

Tue Jun 27 22:50:08 JST 2023
200

Tue Jun 27 22:50:09 JST 2023
200

Tue Jun 27 22:50:10 JST 2023
200

Tue Jun 27 22:50:11 JST 2023
200

Tue Jun 27 22:50:13 JST 2023
429

Tue Jun 27 22:50:14 JST 2023
429

Tue Jun 27 22:50:15 JST 2023
429

```

</details>

# まとめ

OpenShiftのドキュメントではサポートしていないとしつつも、とりあえず動くことや必要なリソースは確認できました。