Kong Gatewayのカスタムプラグインの様々なインストール方法を確認する

Kong Gatewayでは既存のプラグインで機能が足りない場合、自作したプラグイン（カスタムプラグイン）を導入して必要な機能を追加実装することが出来る。
今回はこのカスタムプラグインのインストール方法を確認する。

Kong Gatewayの展開方法には大きく3種類ある。
- Linux上にパッケージマネージャーを使ってインストールする
- Dockerコンテナを立てる
- Kubernetes上に構築する

カスタムプラグインのインストール方法はKong Gatewayの展開方法によって異なるので、それぞれの展開方法でどうなっているかを確認する。

なおカスタムプラグインのインストール方法について参考となる公式ドキュメントは以下あたりとなる。
- [Installation and Distribution](https://docs.konghq.com/gateway/latest/plugin-development/distribution/)
- [Custom Plugins](https://docs.konghq.com/kubernetes-ingress-controller/latest/plugins/custom/)
- [Kong custom plugin distribution with KongPluginInstallation](https://docs.konghq.com/gateway-operator/latest/guides/plugin-distribution/)

また、インストールの検証を行うにあたりサンプルとなるカスタムプラグインを以下に用意した。
https://github.com/imurata/kong-plugin-echo
これはパラメータ`message`に指定した文字列をKong Gateway側のログに出力するだけのプラグインであり、これを使って動作確認を行っていく。
なお各検証時、事前に以下のコマンドを実行した状態で始めるものとする。
```sh
git clone https://github.com/imurata/kong-plugin-echo
cd kong-plugin-echo
```
またKong Gatewayは構築済みの状態から始めるものとする。

## Linuxのパッケージマネージャーで展開した場合
### カスタムプラグインのインストール
Kong GatewayをLinuxのパッケージマネージャーで展開した場合、公式ドキュメントでは[LuaRocks](https://luarocks.org/)というLua向けのパッケージ管理ツールを使ってプラグインをインストールすることが推奨されているので、こちらを使ってインストールする。
LuaRocksでインストールする場合、[rockspec](https://github.com/luarocks/luarocks/wiki/Creating-a-rock)と呼ばれるインストールの仕様書みたいなものを作ってあげる必要がある。
[Kongが配布しているテンプレート](https://github.com/Kong/kong-plugin/blob/master/kong-plugin-myplugin-0.1.0-1.rockspec)をベースに以下のような感じで作成する。
```lua
cat <<EOF > ./kong-plugin-echo-1.0.0-1.rockspec
local plugin_name = "echo"
local package_name = "kong-plugin-" .. plugin_name
local package_version = "1.0.0"
local rockspec_revision = "1"

package = package_name
version = package_version .. "-" .. rockspec_revision

description = {
  summary = "Write the specified string to log.",
}

source = {
  url = "https://example.com",
}

build = {
  type = "builtin",
  modules = {
    ["kong.plugins."..plugin_name..".handler"] = "kong/plugins/"..plugin_name.."/handler.lua",
    ["kong.plugins."..plugin_name..".schema"] = "kong/plugins/"..plugin_name.."/schema.lua",
  }
}
EOF
```
なんとなく見れば分かると思うので説明は割愛するが、`source.url`については必須パラメータとなっているので仕方なく書いている。

一旦ローカルにインストールする。
```sh
$ luarocks make --local

No existing manifest. Attempting to rebuild...
kong-plugin-echo 1.0.0-1 is now installed in /home/ubuntu/.luarocks
```
インストールされたものをパッケージングする。なおzipコマンドが無いと失敗するので注意。
```sh
$ luarocks pack kong-plugin-echo 1.0.0-1
Packed: /home/ubuntu/CustomPlugin/kong-plugin-echo/kong-plugin-echo-1.0.0-1.all.rock
```
カレントディレクトリに`kong-plugin-echo-1.0.0-1.all.rock`が出来ている。これをKong Gateway環境に持ち込んでインストールする。
```sh
$ sudo luarocks install ./kong-plugin-echo-1.0.0-1.all.rock

kong-plugin-echo 1.0.0-1 is now installed in /usr/local
```
インストールされたLuaRocksパッケージは以下のような感じで確認できる。
```sh
$ luarocks list kong-plugin-echo

Rocks installed for Lua 5.1
---------------------------

kong-plugin-echo
   1.0.0-1 (installed) - /usr/local/lib/luarocks/rocks-5.1
```
### 動作確認
Kongの設定を変更し、カスタムプラグインを認識させる。
```sh
sudo vi /etc/kong/kong.conf
```
エディタを開いた後、`plugins`の項目に作成したプラグインを追加する。
```sh
plugins = bundled,echo
```
`bundled`の指定はなくてもいいが、これを外すと元々バンドルされているプラグインが使えなくなるので、ただカスタムプラグインを追加するだけであれば指定が必要となる（参考：[plugins](https://docs.konghq.com/gateway/latest/reference/configuration/#plugins)）
また、プラグインのログ出力はセベリティを`info`にしているので、`log_level`も変更しておく。
```sh
log_level = info
```
Kong Gatewayを再起動する。
```sh
sudo kong restart
```
Service、Routeを作成し、PluginをRouteに割り当てる。
```sh
curl -i -X POST http://localhost:8001/services \
     -d "name=httpbin-svc" \
     -d "url=https://httpbin.org"
```
```sh
curl -i -X POST http://localhost:8001/services/httpbin-svc/routes \
     -d "name=httpbin-rt" \
     -d "paths[]=/echo"
```
```sh
curl -i -X POST http://localhost:8001/routes/httpbin-rt/plugins \
     --data "name=echo" \
     --data "config.message=Hello world."
```
カスタムプラグインの設定で`config.message=Hello world.`を指定することで、Kong Gatewayのログに`Hello world.`が出てくるようになる。
Routeにアクセスしてログ出力されるか確認する。
```sh
curl localhost:8000/echo
```
ログの内容はこちら。
```sh
$ grep echo /usr/local/kong/logs/error.log
2024/12/15 12:26:35 [info] 10630#0: *1294 [kong] handler.lua:7 [echo] [echo Plugin] Message: Hello world., client: 127.0.0.1, server: kong, request: "GET /echo HTTP/1.1", host: "localhost:8000", request_id: "26929a39f5780062415033374ae07970"
```
上手く動いているようだ。

## Dockerコンテナで展開した場合
### カスタムプラグインのインストール
Kong GatewayをDockerコンテナで展開した場合、カスタムプラグインを使うには2つのやり方がある。
1. **コンテナイメージ内に取り込んで、独自のイメージを作ってしまうケース**
2. **ローカルのカスタムプラグインがあるディレクトリをコンテナ内にマウントして利用するケース**

1のアプローチは複数環境で構築する時は楽が出来るが、Kong Gatewayのベースイメージに変更が入った場合にリビルドが必要となる。
また、カスタムプラグインを開発している最中だとちょっと面倒くさい。
なので今回は2の方法で検証する。

意図したディレクトリを取り込むには[こちらのドキュメント](https://docs.konghq.com/gateway/latest/plugin-development/distribution/#manually)にも記載があるように、`lua_package_path`というパラメータを利用する。
またパッケージマネージャーで展開した時と同様に`plugins`と`log_level`も指定する。
dockerでkong.confのパラメータを渡すにはコンテナ起動時に環境変数に`KONG_`をつけて設定すればいいので、起動コマンドを以下のような感じにして起動しなおす。
```sh
docker run -d --name kong-gateway \
 --network=kong-net \
 -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_USER=kong" -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
 -e "KONG_ADMIN_GUI_URL=http://localhost:8002" \
 -p 8000:8000 \
 -p 8001:8001 \
 -p 8002:8002 \
 -v "${PWD}:/tmp/custom_plugins" \
 -e "KONG_LUA_PACKAGE_PATH=/tmp/custom_plugins/?.lua;;" \
 -e "KONG_PLUGINS=bundled,echo" \
 -e "KONG_LOG_LEVEL=info" \
 kong/kong-gateway:3.9.0.0
```
なお、`lua_package_path`には`kong`という名前のディレクトリがある場所を指定するので、パスはカレントディレクトリまでで問題ない。
コマンド実行後、コンテナが無事立ち上がっていればOK。
パスの指定が誤ってたりしたら、以下のようなエラーが出る。
```sh
2024/12/15 12:58:35 [error] 1#0: init_by_lua error: /usr/local/share/lua/5.1/kong/init.lua:812: error loading plugin schemas: on plugin 'echo': echo plugin is enabled but not installed;
no plugin found
stack traceback:
	[C]: in function 'assert'
	/usr/local/share/lua/5.1/kong/init.lua:812: in function 'init'
	init_by_lua(nginx-kong.conf:58):3: in main chunk
```
### 動作確認
パッケージマネージャーで展開した時と同様にService、Routeを作成し、カスタムプラグインをRouteにあてて動作確認する。
```sh
curl -i -X POST http://localhost:8001/services \
     -d "name=httpbin-svc" \
     -d "url=https://httpbin.org"
```
```sh
curl -i -X POST http://localhost:8001/services/httpbin-svc/routes \
     -d "name=httpbin-rt" \
     -d "paths[]=/echo"
```
```sh
curl -i -X POST http://localhost:8001/routes/httpbin-rt/plugins \
     --data "name=echo" \
     --data "config.message=Hello world."
```
プラグインにログを吐かせるためにRouteにアクセスする。
```sh
curl localhost:8000/echo
```
ログを確認する。
```sh
$ docker logs kong-gateway 2>&1 |grep "echo Plugin"
2024/12/15 14:05:46 [info] 2551#0: *4675 [kong] handler.lua:7 [echo] [echo Plugin] Message: Hello world., client: 172.18.0.1, server: kong, request: "GET /echo HTTP/1.1", host: "localhost:8000", request_id: "8d688c4a81956808d563f568fcdf955b"
```
問題なさそうだ。

## Kubernetes上に展開した場合
### カスタムプラグインのインストール
Kong GatewayをKubernetes上に展開した場合、一番簡単なのは`ConfigMap`にプラグインを設定して読み込ませる方法である。
これに関しては以前「[docker buildなしでKong Gatewayのカスタムプラグインを追加する](https://qiita.com/ipppppei/items/178d1f3bfe799e4f8f04)」で取り上げたのでこちらを参照して欲しい。
今回はKong Gateway Operatorで提供されるカスタムリソース[`KongPluginInstallation`](https://docs.konghq.com/gateway-operator/latest/reference/custom-resources/#kongplugininstallation)を使ってインストールしてみる。
Operatorを使ったKong Gatewayの構築方法については「[Kong Gateway Operatorを使ってKong KonnectをKubernetesのリソースとして管理する](https://qiita.com/ipppppei/items/04e1e8a905b73271a5a5)」で触れてるのでこちらを参考にして欲しい。
ただし、`KongPluginInstallation`を使うにはOperatorインストール時に`ENABLE_CONTROLLER_KONGPLUGININSTALLATION`をtrueに設定する必要があるので注意。
以下のような感じで指定する。
```sh
helm upgrade --install kgo kong/gateway-operator -n kong-system --create-namespace --set image.tag=1.4 \
 --set kubernetes-configuration-crds.enabled=true \
 --set env.ENABLE_CONTROLLER_KONNECT=false \
 --set env.ENABLE_CONTROLLER_KONGPLUGININSTALLATION=true 
```

あと、`KongPluginInstallation`を使う手順ではカスタムプラグインをコンテナイメージから引っ張ってくるため、コンテナレジストリが必要となる。
ここでは[DockerHub](https://hub.docker.com/)を利用する。

最初にディレクトリを移動し、カスタムプラグインのディレクトリを取り込んだコンテナイメージを作成する。
```sh
cd kong/plugins/
cat <<EOF > ./Dockerfile
FROM scratch

COPY echo /
EOF
docker build -t imuratashared/echo:1.0.0 .
```
ベースイメージの`scratch`はDockerHubで提供されている最小コンテナのためのイメージである（[参考](https://docs.docker.jp/engine/userguide/eng-image/baseimages.html#id4)）。
イメージをPushする。
```sh
docker push imuratashared/echo:1.0.0
```
次にカスタムリソースのManifestを作成して適用する。
```yaml
cat <<EOF > ./plugin-install-echo.yaml
kind: KongPluginInstallation
apiVersion: gateway-operator.konghq.com/v1alpha1
metadata:
  name: custom-plugin-echo
  namespace: default
spec:
  image: imuratashared/echo:1.0.0
EOF
kubectl apply -f ./plugin-install-echo.yaml
```
`spec.image`には先程Pushしたイメージを指定する。
上手く適用できたか確認する。
```sh
$ kubectl get kongplugininstallation
NAME                 ACCEPTED
custom-plugin-echo   True
```
Operatorのインストール時に`ENABLE_CONTROLLER_KONGPLUGININSTALLATION`を指定していなかったら`ACCEPTED`の欄が空欄になりTrueにならないので、空欄の場合はOperatorが適切にインストールできているか見直すとよい。
またカスタムプラグインを`ConfigMap`として自動生成するので、そちらも合わせて確認するとよい。
```sh
$ kubectl tree kongplugininstallation custom-plugin-echo
NAMESPACE  NAME                                       READY  REASON  AGE
default    KongPluginInstallation/custom-plugin-echo  -              71m
default    └─ConfigMap/custom-plugin-echo-fpmzh     -              65m
```

次にData Planeにプラグインを読み込ませる。
以下のように`kind: GatewayConfiguration`の`spec.dataPlaneOptions.pluginsToInstall`に作成した`KongPluginInstallation`のリソース名を指定する。
```yaml
cat <<EOF > ./gw-config.yaml
kind: GatewayConfiguration
apiVersion: gateway-operator.konghq.com/v1beta1
metadata:
  name: kong
  namespace: default
spec:
  dataPlaneOptions:
    deployment:
      podTemplateSpec:
        spec:
          containers:
          - name: proxy
            image: kong/kong-gateway:3.9.0.0
            readinessProbe:
              initialDelaySeconds: 1
              periodSeconds: 1
            env:
            - name: KONG_LOG_LEVEL
              value: "info"
    pluginsToInstall:
    - name: custom-plugin-echo
  controlPlaneOptions:
    deployment:
      podTemplateSpec:
        spec:
          containers:
          - name: controller
            image: kong/kubernetes-ingress-controller:3.4.0
            env:
            - name: CONTROLLER_LOG_LEVEL
              value: debug
EOF
kubectl apply -f ./gw-config.yaml
```
ポイントとしてはData Planeのenvに`KONG_LUA_PACKAGE_PATH`や`KONG_PLUGINS`は書かない。
これはカスタムリソース側で吸収してくれてるからである。

次に`kind: Gateway`を作成するが、その前に`GatewayClass`がない場合は以下で作成する。
```yaml
cat <<EOF > ./gwclass.yaml
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
 name: kong
spec:
 controllerName: konghq.com/gateway-operator
 parametersRef:
   group: gateway-operator.konghq.com
   kind: GatewayConfiguration
   name: kong
   namespace: default
EOF
kubectl apply -f ./gwclass.yaml
```
`Gateway`を作成する。
```yaml
cat <<EOF > ./gw.yaml
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
 name: kong
 namespace: default
spec:
 gatewayClassName: kong
 listeners:
 - name: http
   protocol: HTTP
   port: 80
EOF
kubectl apply -f ./gw.yaml
```
Gatewayを作成すると自動的にControl PlaneとData Planeのカスタムリソースも作成される。
```sh
$ kubectl tree gateway kong
NAMESPACE  NAME                                                                 READY  REASON  AGE
default    Gateway/kong                                                         -              10m
default    ├─ControlPlane/kong-zzj5q                                          True   Ready   10m
default    │ ├─Deployment/controlplane-kong-zzj5q-pddhq                      -              10m
default    │ │ └─ReplicaSet/controlplane-kong-zzj5q-pddhq-6556d4b775        -              38s
default    │ │   └─Pod/controlplane-kong-zzj5q-pddhq-6556d4b775-hsr7x       True           38s
default    │ ├─Secret/controlplane-kong-zzj5q-6h9nv                          -              10m
default    │ ├─Secret/controlplane-kong-zzj5q-vbcxc                          -              10m
default    │ ├─Service/controlplane-webhook-kong-zzj5q-xwftw                 -              10m
default    │ │ └─EndpointSlice/controlplane-webhook-kong-zzj5q-xwftw-dljxw  -              10m
default    │ └─ServiceAccount/controlplane-kong-zzj5q-tr5jh                  -              10m
default    ├─DataPlane/kong-5bz8r                                             True   Ready   10m
default    │ ├─ConfigMap/kong-5bz8r-msvbv                                    -              10m
default    │ ├─Deployment/dataplane-kong-5bz8r-6sz4m                         -              10m
default    │ │ └─ReplicaSet/dataplane-kong-5bz8r-6sz4m-7768bd9857           -              2m23s
default    │ │   └─Pod/dataplane-kong-5bz8r-6sz4m-7768bd9857-kp9f9          True           2m23s
default    │ ├─Secret/dataplane-kong-5bz8r-pzhnp                             -              10m
default    │ ├─Service/dataplane-admin-kong-5bz8r-clcx5                      -              10m
default    │ │ └─EndpointSlice/dataplane-admin-kong-5bz8r-clcx5-s96pq       -              10m
default    │ └─Service/dataplane-ingress-kong-5bz8r-k4jmf                    -              10m
default    │   └─EndpointSlice/dataplane-ingress-kong-5bz8r-k4jmf-5lxrx      -              10m
default    └─NetworkPolicy/kong-5bz8r-limit-admin-api-dvz5c                   -              23s
```



### 動作確認
Kong Ingress Controller(KIC)が稼働しているので、IngressかHTTPRouteを作るとKong側でService、Routeが作成される。
ここでは公式ドキュメントで使われている`Deployment`と`Service`を使って宛先となるアプリケーションをデプロイする。
```sh
kubectl apply -f https://docs.konghq.com/assets/kubernetes-ingress-controller/examples/echo-service.yaml
```
次に対応する`HTTPRoute`を作成する。
```yaml
cat << EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httproute-echo
  namespace: default
  annotations:
    konghq.com/strip-path: "true"
    konghq.com/plugins: echo
spec:
  parentRefs:
  - name: kong
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /echo
    backendRefs:
    - name: echo
      kind: Service
      port: 1027
EOF
```
アノテーションに`konghq.com/plugins: echo`を指定しカスタムプラグインを適用するようにしている。
この時点でServiceとRouteは作成され、Gatewayのアドレスを使えばアプリケーションにアクセスできる。
```sh
$ HOST=$(kubectl get gateway kong -o jsonpath={.status.addresses[].value})
$ curl $HOST/echo
Welcome, you are connected to node ip-192-168-2-182.ec2.internal.
Running on Pod echo-57ffc6dfcf-p8khz.
In namespace default.
With IP address 192.168.19.228.
```
ただし、この時点ではカスタムプラグインは適用していないのでカスタムプラグインのログは出ない。
```sh
$ kubectl logs dataplane-kong-5bz8r-6sz4m-7768bd9857-kp9f9 | grep "echo Plugin"
$
```

次にカスタムプラグインを適用し、再度アクセスする。
```yaml
cat <<EOF | kubectl apply -f - 
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: echo
plugin: custom-plugin-echo
config:
  message: "Hello world."
EOF
curl $HOST/echo
```
ログを確認する。
```sh
$ kubectl logs dataplane-kong-5bz8r-6sz4m-7768bd9857-kp9f9 | grep "echo Plugin"
2024/12/15 18:01:33 [info] 2551#0: *3235 [kong] handler.lua:7 [custom-plugin-echo] [echo Plugin] Message: Hello world., client: 192.168.32.243, server: kong, request: "GET /echo HTTP/1.1", host: "ab3d9d6a4fa7749d68f111aca5a4c0e5-1300302864.us-east-1.elb.amazonaws.com", request_id: "b97736531d284f116caa344e6c6ec562"
```
カスタムプラグインのログ出力が確認できた。