Post-function PluginでWebページの表示を書き換える


何かしらのWebサービスを展開している際、メンテナンス前に通知表示を加えたいが本番環境に手を加えたくないといったケースがあると思う。
API GatewayであるKon GatewayのPluginを使うと、エンドポイントのサービスに手を加えずレスポンスのBodyを変更することもできるので、今回は[Post-function Plugin](https://docs.konghq.com/hub/kong-inc/post-function/)を使ってWebサーバの表示の書き換えを検証してみる。

## Post-function Pluginとは
[Post-function Plugin](https://docs.konghq.com/hub/kong-inc/post-function/)は用意したLuaの関数を実行するプラグインであり、リクエスト・レスポンスの書き換えやログの追加など、かなり色々なことがプログラマブルに出来るようになっている。
以下はログ出力を追加するLua実装の例で、これをPluginのインストール時に併せて渡してあげることで、リクエストやレスポンスがKong Gatewayを通った際に任意の関数が実行できるようになる。
```lua
-- this runs once on the first request
local count = 0

return function()
  -- this runs on each request
  count = count + 1
  ngx.log(ngx.ERR, "hello world: ", count)
end
```
実装に関してはKongの[Plugin Development Kit(PDK)](https://docs.konghq.com/gateway/latest/plugin-development/pdk/)が利用できるため、例えばレスポンスの内容などは`kong.response.get_headers()`などを使って取得する事が出来る

このプラグインは基本的には全ての処理の中で最後に動作するプラグインであり、[実行優先度](https://docs.konghq.com/konnect/reference/plugins/#plugin-execution-order)は-1000とプラグインの中で最低に設定されている。

[Pluginのパラメータ](https://docs.konghq.com/hub/kong-inc/post-function/configuration/#parameters)についてはどこでLuaのコードを動かすかを定義するもので、[Openresty Reference](https://openresty-reference.readthedocs.io/en/latest/Directives/)のPhaseやWebSocketの状況に併せて指定する。

なお、このプラグインを利用するとプラグインを登録できる人は任意のコードを実行できてしまうので、セキュリティを気にする場合はこれを使わず[Custom Plugin](https://docs.konghq.com/gateway/latest/plugin-development/)で実装してPost/Pre-function Pluginは無効化しておいた方がよい。

## 準備
検証用のWebページを参照するためにnginxを起動する。
```sh
docker run --name nginx --network=kong-net -d nginx
```

Kong GatewayにアクセスするためのServiceとRouteを作成する。
まずはServiceを作るためにAdminAPI(localhost:8001)をcurlで叩く。
```sh
curl -i -X POST http://localhost:8001/services/ \
  --data name=nginx-service \
  --data url=http://nginx:80
```
次にRouteを作成する。
```sh
curl -i -X POST http://localhost:8001/services/nginx-service/routes \
  --data name=nginx-route \
  --data paths[]=/nginx
```
これで`http://localhost:8000/nginx`にアクセスすることでKong GatewayのProxy経由でnginxコンテナにアクセスできる。
![20240809103425.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/63c62bef-dabf-42fe-0f60-64167cf2e8e2.png)


## Post-function Pluginの検証
ここでは想定シナリオとして、何かしらのメンテナンスの通知を元のWebページを改変せずに追加出力することを想定する。
具体的には、先程の動作確認で立てたnginxのデフォルトページの"Welocome to nginx!"の上にメンテナンスの告知を追加で表示するようにしてみる。

以下、Post-function Pluginに渡すLuaのコードとなる。
```lua:modify_html.lua
return function()
  local body = kong.response.get_raw_body()
  if body then
    local position = string.find(body, "<h1>Welcome to nginx!</h1>")

    if position then
      body = string.sub(body, 1, position - 1) ..
             [[<p style="color:red;">メンテナンスを9/1に実施するため、終日サービスが停止します</p>]] ..
             string.sub(body, position)
    end
    kong.response.set_raw_body(body)
  end
end
```
このコードはレスポンスを受け取るタイミングで動作するものとする。
PDKが利用できるため、最初に`kong.response.get_raw_body()`でレスポンスのBodyを取得し、`string.find()`で文字列を検索する。
ターゲットとしている文字列("Welcome to nginx!")が見つかったら、その前にメンテナンスに関する通知のための文字列を追加し、`kong.response.set_raw_body(body)`で文字列をbodyに保存して終了する。

Luaのコードを用意したら、先程作成したRouteに適用してみる。
```sh
curl -X POST http://localhost:8001/routes/nginx-route/plugins \
  -F "name=post-function"  \
  -F "config.body_filter=@modify_html.lua"
```
今回はBodyを修正するので`body_filter`でコードを実行するようにした。
適用したらnginxの表示が変わるはずだ。
実際にKong Gateway経由でアクセスしてみる。
![20240810094149.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/6b485ffa-cd75-2dd8-9ffe-cd8cf37e7437.png)
Luaで実装した、メンテナンスに関する告知が確認できた。
これにより、nginxコンテナには一切手を加えずにWebページの表示を変更することが確認出来た。

## まとめ
Post-function Pluginを使うと稼働しているサービスに手を加えず表示を変更したりすることが出来ることが確認できた。
今回検証したような感じで本番環境など簡単にサービスを変更できないような環境を改変するのはもちろん、クライアント側の仕様変更を吸収するのに使ったり、用途は色々考えられると思う。
一方で、何でも出来てしまう＝セキュリティリスクを伴うので、利用シーンは適切に選択した上で利用し、必要なければ無効化も検討したほうが良さそうだ。

## おまけ：Kong Gatewayの起動
今回の検証用に使ったKong Gatewayの構築は以下を実行すれば構築できる。
最初にコンテナ間通信のためにDockerのネットワークを作成する。
```sh
docker network create kong-net
```
次にKong Gatewayで使うDBを用意する。
```sh
docker run -d --name kong-database \
  --network=kong-net \
  -p 5432:5432 \
  -e "POSTGRES_USER=kong" \
  -e "POSTGRES_DB=kong" \
  -e "POSTGRES_PASSWORD=kongpass" \
  postgres:13
```
DBを初期化する。
```sh
docker run --rm --network=kong-net \
 -e "KONG_DATABASE=postgres" \
 -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_PASSWORD=test" \
kong/kong-gateway:3.7.1.2 kong migrations bootstrap
```
Kong Gatewayを起動する。
```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 \
 kong/kong-gateway:3.7.1.2
```