Kong Gatewayカスタムプラグインをテストする　～Part2: Pongoでテストする～

# はじめに

「[Kong Gatewayカスタムプラグインをテストする　～Part1: Pongoを実行する～](https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405)」ではカスタムプラグインを Pongo で実行する方法を書きました。

Pongo は Kong Gateway のプラグインをテストするためのツールであり、Pongo だけでをテストする環境をそろえることができます。

今回は手動でのテストの方法と Pongo を使ったテストの方法を説明し、比べてみたいと思います。

プラグインについてはPart1の記事で用意したものを使用します。

https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405#kong-gateway%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E9%96%8B%E7%99%BA

Service / Route / Consumer など Kong Gateway で使われる用語については以下の記事を参照してください。

https://qiita.com/m5takeda/items/d7c74681c66692144ccc

# Pongo でのテスト

### 1. Pongo のインストール
Pongo のインストール方法は[Part1](https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405#pongo-%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)に書きましたので割愛します。

### 2. テストを書く
プラグインの動作や設定を、Kong Gawteway あるいは管理 API 呼び出して、結果が期待(仕様)通りかどうかをチェックするためのシナリオを記述します。
Part1の記事のプラグインは[固定文字列をHTTPレスポンスヘッダに加える単純なもの](https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%A8%E3%81%AF)ですので、リクエストヘッダとしては追加されていないこととレスポンスヘッダには期待通りの値が設定されていることをテストしています。

```lua: 01-simple_spec.lua
local helpers = require "spec.helpers"
local PLUGIN_NAME = "plugin-example"
local CUSTOM_HEADER_NAME = "My-Plugin-Header"

-- データストア毎にテストしますが、Cassandra をデータストアとして利用する場合を除外します. 
for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then
  describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function()
    local client

    -- setup - テスト実行準備.
    lazy_setup(function()

      local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME })

      -- テスト用の Route を追加します.
      -- テストではリクエストをそのまま返すモックアップ(*1)を Service としてデフォルトで設定済みなので、Route のみの設定(*2)でOKです.
      local route1 = bp.routes:insert({
        -- 仮想ホスト　test1.example.com へのリクエストを対象とします.
        hosts = { "test1.example.com" },
      })

      -- テスト用の Route にプラグインを追加します(*2).
      bp.plugins:insert {
        name = PLUGIN_NAME,
        route = { id = route1.id },
        config = {},
      }

      -- テスト用 Kong を実行します.
      assert(helpers.start_kong({
        -- データストア種別.
        database   = strategy,
        -- ローカルモックサーバを作成するためにカスタムテンプレートを使用します.
        -- (Kong 本体に含まれています).
        nginx_conf = "spec/fixtures/custom_nginx.template",
        -- カスタムプラグインを設定します.
        -- 通常、環境変数で設定する文字列ですが、テストシナリオではここで指定します.
        plugins = "bundled," .. PLUGIN_NAME,
        -- 'strategy=off' (DB-lessモード) の場合の yaml ファイルを書きこんでロードします.
        declarative_config = strategy == "off" and helpers.make_yaml_file() or nil,
      }))
    end)

    -- teardown - テスト終了処理 (テスト用 Kong を停止します).
    lazy_teardown(function()
      helpers.stop_kong(nil, true)
    end)

    -- テストケース開始毎にクライアントオブジェクトを作成します.
    before_each(function()
      client = helpers.proxy_client()
    end)

    -- テストケース終了ごとにクライアントオブジェクトの終了手続きを実行します.
    after_each(function()
      if client then client:close() end
    end)


    -- リクエストヘッダが期待通りかどうかをテストします.
    describe("request", function()
      it("gets no custom header", function()
        -- テスト対象の API (http://test1.example.com/request) をコールします.
        local r = client:get("/request", {
          headers = {
            host = "test1.example.com"
          }
        })
        -- HTTPステータスコードが正常(200)であることを確認します.
        assert.response(r).has.status(200)
        -- リクエストヘッダに 'My-Plugin-Header' が存在しないことを確認します.
        assert.request(r).has.no.header(CUSTOM_HEADER_NAME)
      end)
    end)

    -- レスポンスヘッダが期待通りかどうかをテストします.
    describe("response", function()
      it("gets a custom header", function()
        -- テスト対象の API (http://test1.example.com/request) をコールします.
        local r = client:get("/request", {
          headers = {
            host = "test1.example.com"
          }
        })
        -- HTTPステータスコードが正常(200)であることを確認します.
        assert.response(r).has.status(200)
        -- レスポンスヘッダに 'My-Plugin-Header' が存在することを確認します.
        local header_value = assert.response(r).has.header(CUSTOM_HEADER_NAME)
        -- レスポンスヘッダの値が期待通りであることを確認します。
        assert.equal("example", header_value)
      end)
    end)

  end)

end end
```
:::note
Pongo 上で使用するのは busted というテストフレームワークです。
詳細については Kong および busted のドキュメントを参照してください。
:::
https://docs.konghq.com/gateway/latest/plugin-development/tests

https://github.com/lunarmodules/busted

#### モックアップ
Kong Gateway 実行のためには、 HTTPクライアントの他に、アップストリームの API サーバが必要です。
テストフレームワークでは外部APIをモック化するものですが、 Kong Gateway でもアップストリームの API サーバをモック化してテスト中に Service として設定することができます。
シナリオのコメント *1 の部分になります。

#### Service / Route / プラグインへの設定
Kong Gateway の実行には、アップストリームの API を Serviceに、API振り分け条件を Route や場合によっては Consumer 単位に、そしてプラグインに対して設定を行う必要があります。
これらを管理画面や管理API直接呼出しではなく、シナリオとして事前に定義しておくことで実現します。
シナリオのコメント *2 の部分になります。

### 3. テストの実施
プラグインプロジェクトフォルダ上で `pongo init` を一度実行した後は、プラグインまたはテストシナリオを修正する度に `pongo run` を実行するだけです。

```shell-session: テスト実行
$ pongo run
[pongo-INFO] auto-starting the test environment, use the 'pongo down' action to stop it
Kong version: 3.8.0


Stopping after installing dependencies for plugin-example 0.0.1-1

[==========] Running tests from scanned files.
[----------] Global test environment setup.
[----------] Running tests from /kong-plugin/spec/plugin-example/01-simple_spec.lua
[ RUN      ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:62: plugin-example: (access) [#postgres] request gets no custom header
[       OK ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:62: plugin-example: (access) [#postgres] request gets no custom header (3.16 ms)
[ RUN      ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:78: plugin-example: (access) [#postgres] response gets a custom header
[       OK ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:78: plugin-example: (access) [#postgres] response gets a custom header (0.73 ms)
[ RUN      ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:62: plugin-example: (access) [#off] request gets no custom header
[       OK ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:62: plugin-example: (access) [#off] request gets no custom header (25.58 ms)
[ RUN      ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:78: plugin-example: (access) [#off] response gets a custom header
[       OK ] /kong-plugin/spec/plugin-example/01-simple_spec.lua:78: plugin-example: (access) [#off] response gets a custom header (1.48 ms)
[----------] 4 tests from /kong-plugin/spec/plugin-example/01-simple_spec.lua (24579.80 ms total)

[----------] Global test environment teardown.
[==========] 4 tests from 1 test file ran. (24580.28 ms total)
[  PASSED  ] 4 tests.
$ 
```

# 手動でのテスト
Pongo を使わなくてもプラグインのテスト・動作確認は可能です。
Pongo を使わない場合はどのような感じになるのかを示します。

[公式の Docker Compose](https://github.com/Kong/docker-kong/tree/master/compose) を使うとサクッと実行環境を作ることができます。

:::note
テストで使う一時的な環境なので、Pongo と同様に Docker を使用します。
:::

:::note warn
Compose ではバインドするIPアドレスなど少し設定を上書きしてあります。公式のデフォルト設定では管理APIが localhost のみにバインドされていて、公開用のIPアドレス経由ではアクセスできないようになっているためです。
:::

Kong の実行については Qiita の他の記事が参考になります。

https://qiita.com/m5takeda/items/f8e851ddf1ed8bb1606e

https://qiita.com/caunu-s/items/636aff1d04778fbbd779

### 1. プラグインを含んだ実行環境の準備
単に Kong Gateway を実行しただけではプラグインの動作確認ができません。
動作確認には以下が必要になります。

- プラグインを含む Kong Gateway のコンテナ
- アップストリームの API (モックアップ)
- Kong Gateway およびプラグインの設定

[Part1のプラグインプロジェクト](https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E9%85%8D%E7%BD%AE)に対してファイルを加えて `docker compose` で実行できるようにします。

```
kong-plugin-example/
+-- kong/
|   +-- plugins/
|       +-- plugin-example/
|           +-- handler.lua
|           +-- schema.lua
+-- spec/
|   +-- plugin-example/
|       +-- 01-simple_spec.lua
+-- compose.yaml
+-- kong.dockerfile
+-- json-server.dockerfile
+-- pgpassword
+-- plugin-example-0.0.1-1.rockspec
```
:::note
以下のファイルを追加しています。
- compose.yaml
- kong.dockerfile
- json-server.dockerfile
- pgpassword
:::
#### プラグインを含む Kong Gateway の準備

公式 Docker イメージに対してプラグインを追加したイメージを作成します。

```dockerfile: kong.dockerfile
FROM kong:3.8.0

# 一時 root に変更
USER root

# イメージにプラグインを追加
COPY ./kong/plugins/plugin-example /usr/local/share/lua/5.1/kong/plugins/plugin-example

# 環境変数でプラグインを追加
ENV KONG_PLUGINS=bundled,plugin-example

# 実行ユーザーを kong に戻す
USER kong

# Kong 実行
#ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 8000 8443 8001 8444
STOPSIGNAL SIGQUIT
HEALTHCHECK --interval=10s --timeout=10s --retries=10 CMD kong health
CMD ["kong", "docker-start"]
```

:::note
この dockerfile を使ってテスト対象のコンテナをビルドします。
ビルドと実行については[2.テスト対象の実行](#2-テスト対象の実行)を参照してください。
:::

#### モックアップの準備

ここでは手軽な [json-server](https://github.com/typicode/json-server) を使用してアップストリームの APIサーバとします。
```dockerfile: json-server.dockerfile
FROM node:lts-alpine3.20

# モックAPI の戻り値のIDには特に意味はありません
# /mock/9c0f9095-1336-4bf9-978c-96c221699781 への GET リクエストで
#  {"id": "9c0f9095-1336-4bf9-978c-96c221699781"}
# が返ります
RUN mkdir -p /opt/json-server && \
    cd /opt/json-server && \
    npm install json-server --save-dev && \
    echo '{"mock": [{"id": "9c0f9095-1336-4bf9-978c-96c221699781"}]}'>db.json && \
    chmod a+w db.json

WORKDIR /opt/json-server

CMD ["npx", "json-server", "./db.json"]

```

:::note
この dockerfile を使って モックAPI をビルドします。
ビルドと実行については[テスト対象の実行](#2-テスト対象の実行)を参照してください。
:::

### 2. テスト対象の実行

docker compose を使ってテスト対象を実行します。

<details><summary>compose.yaml と pgpassword の内容</summary>

```yaml: compose.yaml
---
services:
  postgres:
    image: "postgres:16.4-alpine"
    hostname: postgres
    environment:
      POSTGRES_HOST: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: kong
      POSTGRES_PASSWORD_FILE: /run/secrets/pgpassword
    secrets:
      - pgpassword
    ports:
      - 5432:5432
    volumes:
      - kong-volume:/var/lib/postgresql/data:cached
      - ./pgpassword:/run/secrets/pgpassword

  kong-migrations:
    image: "kong-customized"
    environment:
      KONG_PG_HOST: postgres
      KONG_PG_USER: postgres
      KONG_PG_PASSWORD_FILE: /run/secrets/pgpassword
    secrets:
      - pgpassword
    command:
      - kong
      - migrations
      - bootstrap
    volumes:
      - ./pgpassword:/run/secrets/pgpassword    
    profiles:
      - initialize
    depends_on:
      - postgres

  kong:
    #image: "kong:3.8.0-ubuntu"
    image: "kong-customized"
    build:
      context: .
      dockerfile: kong.dockerfile
      args:
        http_proxy: ${http_proxy}
        https_proxy: ${https_proxy}
        no_proxy: ${no_proxy}
    ports:
      - "8000:8000"
      - "8001:8001"
      - "8002:8002"
    environment:
      KONG_ADMIN_LISTEN: "0.0.0.0:8001"
      KONG_ADMIN_GUI_LISTEN: "0.0.0.0:8002"
      KONG_PG_HOST: postgres
      KONG_PG_USER: postgres
      KONG_PG_PASSWORD_FILE: /run/secrets/pgpassword
    secrets:
      - pgpassword
    volumes:
      - ./pgpassword:/run/secrets/pgpassword    
    depends_on:
      - postgres

  mock-api:
    build:
      dockerfile: json-server.dockerfile
      args:
        http_proxy: ${http_proxy}
        https_proxy: ${https_proxy}
        no_proxy: ${no_proxy}
    hostname: mock-api
    ports:
      - "3000:3000"

volumes:
  kong-volume:
    driver: local

secrets:
  pgpassword:
    file: ./pgpassword

```
:::note
インターネットアクセスにHTTPプロキシが不要な場合 http_proxy, https_proxy, no_proxy を設定する必要はありません。
:::


```: pgpassword
password
```
:::note
文字列は推奨値ではなく、単なる実行サンプルです。念のため。
:::


</details>

#### ビルド
```shell-session: コンテナのビルド
$ docker compose build
[+] Building 1.4s (16/16) FINISHED                                                                           docker:default
 => [kong internal] load build definition from kong.dockerfile                                                         0.0s
 => => transferring dockerfile: 520B                                                                                   0.0s
 => [mock-api internal] load build definition from json-server.dockerfile                                              0.0s
 => => transferring dockerfile: 358B                                                                                   0.0s
 => [kong internal] load metadata for docker.io/library/kong:3.8.0                                                     1.2s
 => [mock-api internal] load metadata for docker.io/library/node:lts-alpine3.20                                        1.2s
 => [kong internal] load .dockerignore                                                                                 0.0s
 => => transferring context: 2B                                                                                        0.0s
 => [mock-api internal] load .dockerignore                                                                             0.0s
 => => transferring context: 2B                                                                                        0.0s
 => [kong internal] load build context                                                                                 0.0s
 => => transferring context: 222B                                                                                      0.0s
 => [kong 1/2] FROM docker.io/library/kong:3.8.0@sha256:616b2ab5a4c7b6c14022e8a1495ff34930ced76f25f3d418e76758717fec3  0.0s
 => [mock-api 1/3] FROM docker.io/library/node:lts-alpine3.20@sha256:b64ced2e7cd0a4816699fe308ce6e8a08ccba463c757c00c  0.0s
 => CACHED [mock-api 2/3] RUN mkdir -p /opt/json-server &&     cd /opt/json-server &&     npm install json-server --s  0.0s
 => CACHED [mock-api 3/3] WORKDIR /opt/json-server                                                                     0.0s
 => CACHED [kong 2/2] COPY ./kong/plugins/plugin-example /usr/local/share/lua/5.1/kong/plugins/plugin-example          0.0s
 => [mock-api] exporting to image                                                                                      0.0s
 => => exporting layers                                                                                                0.0s
 => => writing image sha256:408cc71d649ba3d8cdd1382409af3d85f08fa152c9c3be96173443886ba42e1e                           0.0s
 => => naming to docker.io/library/kong-plugin-example-mock-api                                                        0.0s
 => [kong] exporting to image                                                                                          0.0s
 => => exporting layers                                                                                                0.0s
 => => writing image sha256:c856a250ac2445bef5bb18fdf7526316df9e2d48fd0d28a647c5160fe85bf7f6                           0.0s
 => => naming to docker.io/library/kong-customized                                                                     0.0s
 => [mock-api] resolving provenance for metadata file                                                                  0.0s
 => [kong] resolving provenance for metadata file                                                                      0.0s
$ 
```

#### 実行
```shell-session: 実行
$ docker compose up -d postgres
[+] Running 3/3
 ✔ Network kong-plugin-example_default       Created                                                                   0.2s 
 ✔ Volume "kong-plugin-example_kong-volume"  Created                                                                   0.0s 
 ✔ Container kong-plugin-example-postgres-1  Started                                                                   0.4s 
$ docker compose up kong-migrations
[+] Running 2/0
 ✔ Container kong-plugin-example-postgres-1         Running                                                            0.0s 
 ✔ Container kong-plugin-example-kong-migrations-1  Created                                                            0.0s 
Attaching to kong-migrations-1
kong-migrations-1  | Bootstrapping database...
kong-migrations-1  | migrating core on database 'kong'...
kong-migrations-1  | core migrated up to: 000_base (executed)
kong-migrations-1  | core migrated up to: 003_100_to_110 (executed)
kong-migrations-1  | core migrated up to: 004_110_to_120 (executed)
kong-migrations-1  | core migrated up to: 005_120_to_130 (executed)
kong-migrations-1  | core migrated up to: 006_130_to_140 (executed)
kong-migrations-1  | core migrated up to: 007_140_to_150 (executed)
kong-migrations-1  | core migrated up to: 008_150_to_200 (executed)
kong-migrations-1  | core migrated up to: 009_200_to_210 (executed)
kong-migrations-1  | core migrated up to: 010_210_to_211 (executed)
kong-migrations-1  | core migrated up to: 011_212_to_213 (executed)
kong-migrations-1  | core migrated up to: 012_213_to_220 (executed)
kong-migrations-1  | core migrated up to: 013_220_to_230 (executed)
kong-migrations-1  | core migrated up to: 014_230_to_270 (executed)
kong-migrations-1  | core migrated up to: 015_270_to_280 (executed)
kong-migrations-1  | core migrated up to: 016_280_to_300 (executed)
kong-migrations-1  | core migrated up to: 017_300_to_310 (executed)
kong-migrations-1  | core migrated up to: 018_310_to_320 (executed)
kong-migrations-1  | core migrated up to: 019_320_to_330 (executed)
kong-migrations-1  | core migrated up to: 020_330_to_340 (executed)
kong-migrations-1  | core migrated up to: 021_340_to_350 (executed)
kong-migrations-1  | core migrated up to: 022_350_to_360 (executed)
kong-migrations-1  | core migrated up to: 023_360_to_370 (executed)
kong-migrations-1  | migrating acl on database 'kong'...
kong-migrations-1  | acl migrated up to: 000_base_acl (executed)
kong-migrations-1  | acl migrated up to: 002_130_to_140 (executed)
kong-migrations-1  | acl migrated up to: 003_200_to_210 (executed)
kong-migrations-1  | acl migrated up to: 004_212_to_213 (executed)
kong-migrations-1  | migrating acme on database 'kong'...
kong-migrations-1  | acme migrated up to: 000_base_acme (executed)
kong-migrations-1  | acme migrated up to: 001_280_to_300 (executed)
kong-migrations-1  | acme migrated up to: 002_320_to_330 (executed)
kong-migrations-1  | acme migrated up to: 003_350_to_360 (executed)
kong-migrations-1  | migrating ai-proxy on database 'kong'...
kong-migrations-1  | ai-proxy migrated up to: 001_360_to_370 (executed)
kong-migrations-1  | migrating basic-auth on database 'kong'...
kong-migrations-1  | basic-auth migrated up to: 000_base_basic_auth (executed)
kong-migrations-1  | basic-auth migrated up to: 002_130_to_140 (executed)
kong-migrations-1  | basic-auth migrated up to: 003_200_to_210 (executed)
kong-migrations-1  | migrating bot-detection on database 'kong'...
kong-migrations-1  | bot-detection migrated up to: 001_200_to_210 (executed)
kong-migrations-1  | migrating hmac-auth on database 'kong'...
kong-migrations-1  | hmac-auth migrated up to: 000_base_hmac_auth (executed)
kong-migrations-1  | hmac-auth migrated up to: 002_130_to_140 (executed)
kong-migrations-1  | hmac-auth migrated up to: 003_200_to_210 (executed)
kong-migrations-1  | migrating http-log on database 'kong'...
kong-migrations-1  | http-log migrated up to: 001_280_to_300 (executed)
kong-migrations-1  | migrating ip-restriction on database 'kong'...
kong-migrations-1  | ip-restriction migrated up to: 001_200_to_210 (executed)
kong-migrations-1  | migrating jwt on database 'kong'...
kong-migrations-1  | jwt migrated up to: 000_base_jwt (executed)
kong-migrations-1  | jwt migrated up to: 002_130_to_140 (executed)
kong-migrations-1  | jwt migrated up to: 003_200_to_210 (executed)
kong-migrations-1  | migrating key-auth on database 'kong'...
kong-migrations-1  | key-auth migrated up to: 000_base_key_auth (executed)
kong-migrations-1  | key-auth migrated up to: 002_130_to_140 (executed)
kong-migrations-1  | key-auth migrated up to: 003_200_to_210 (executed)
kong-migrations-1  | key-auth migrated up to: 004_320_to_330 (executed)
kong-migrations-1  | migrating oauth2 on database 'kong'...
kong-migrations-1  | oauth2 migrated up to: 000_base_oauth2 (executed)
kong-migrations-1  | oauth2 migrated up to: 003_130_to_140 (executed)
kong-migrations-1  | oauth2 migrated up to: 004_200_to_210 (executed)
kong-migrations-1  | oauth2 migrated up to: 005_210_to_211 (executed)
kong-migrations-1  | oauth2 migrated up to: 006_320_to_330 (executed)
kong-migrations-1  | oauth2 migrated up to: 007_320_to_330 (executed)
kong-migrations-1  | migrating opentelemetry on database 'kong'...
kong-migrations-1  | opentelemetry migrated up to: 001_331_to_332 (executed)
kong-migrations-1  | migrating post-function on database 'kong'...
kong-migrations-1  | post-function migrated up to: 001_280_to_300 (executed)
kong-migrations-1  | migrating pre-function on database 'kong'...
kong-migrations-1  | pre-function migrated up to: 001_280_to_300 (executed)
kong-migrations-1  | migrating rate-limiting on database 'kong'...
kong-migrations-1  | rate-limiting migrated up to: 000_base_rate_limiting (executed)
kong-migrations-1  | rate-limiting migrated up to: 003_10_to_112 (executed)
kong-migrations-1  | rate-limiting migrated up to: 004_200_to_210 (executed)
kong-migrations-1  | rate-limiting migrated up to: 005_320_to_330 (executed)
kong-migrations-1  | rate-limiting migrated up to: 006_350_to_360 (executed)
kong-migrations-1  | migrating response-ratelimiting on database 'kong'...
kong-migrations-1  | response-ratelimiting migrated up to: 000_base_response_rate_limiting (executed)
kong-migrations-1  | response-ratelimiting migrated up to: 001_350_to_360 (executed)
kong-migrations-1  | migrating session on database 'kong'...
kong-migrations-1  | session migrated up to: 000_base_session (executed)
kong-migrations-1  | session migrated up to: 001_add_ttl_index (executed)
kong-migrations-1  | session migrated up to: 002_320_to_330 (executed)
kong-migrations-1  | 66 migrations processed
kong-migrations-1  | 66 executed
kong-migrations-1  | Database is up-to-date
kong-migrations-1 exited with code 0
$ docker compose up -d 
[+] Running 3/3
 ✔ Container kong-plugin-example-postgres-1  Running                                                                   0.0s 
 ✔ Container kong-plugin-example-mock-api-1  Started                                                                   0.5s 
 ✔ Container kong-plugin-example-kong-1      Started                                                                   0.6s 
$ 
```
:::note
以下の順に実行しています。
1. postgresql 起動
1. kong-migrations を実行
1. コンテナを全部起動 (Kong Gateway, json-server)
:::

### 3. Kong Gateway およびプラグインの設定
CLIツール curl で管理APIを呼び出し設定をします。

- Kong Gateway の設定
管理APIで Service と Route を設定します。

```shell-session: Service の設定
$ cat service.json
{
    "host": "mock-api",
    "name": "json-server",
    "path": "/mock",
    "port": 3000,
    "protocol": "http"
}
$ curl -d@service.json -H 'Content-Type: application/json' http://localhost:8001/services
{"created_at":1732626400,"updated_at":1732626400,"retries":5,"write_timeout":60000,"port":3000,"enabled":true,"tls_verify":null,"tls_verify_depth":null,"name":"json-server","id":"d3221947-dee9-4f1a-91ac-893ec30aa402","client_certificate":null,"protocol":"http","host":"mock-api","tags":null,"ca_certificates":null,"path":"/mock","connect_timeout":60000,"read_timeout":60000}
$ 
```
:::note
ここで発行された service の ID (d3221947-dee9-4f1a-91ac-893ec30aa402) は Route の設定で必要になります。
:::
```shell-session: Route の設定
$ cat route.json
{
    "name": "mock-route",
    "paths": [
        "/mock-route/"
    ],
    "protocols": [
        "http"
    ],
    "service": {
        "id": "d3221947-dee9-4f1a-91ac-893ec30aa402"
    }
}
$ curl -d@route.json -H 'Content-Type: application/json' http://localhost:8001/routes
{"methods":null,"updated_at":1732626750,"preserve_host":false,"strip_path":true,"service":{"id":"d3221947-dee9-4f1a-91ac-893ec30aa402"},"https_redirect_status_code":426,"snis":null,"headers":null,"paths":["/mock-route/"],"request_buffering":true,"response_buffering":true,"name":"mock-route","id":"2a62ff11-66d2-4b92-b89c-682b749090e7","protocols":["http"],"created_at":1732626750,"sources":null,"path_handling":"v0","regex_priority":0,"tags":null,"destinations":null,"hosts":null}
$ 
```
:::note
ここで発行された Route の ID (2a62ff11-66d2-4b92-b89c-682b749090e7) に対してカスタムプラグインを有効化させます。
:::

- プラグインの設定
指定した Route に対してプラグインを有効化します。
```shell-session: プラグインの有効化
$ curl -d '{"name": "plugin-example"}' -H 'Content-Type: application/json' http://localhost:8001/routes/2a62ff11-66d2-4b92-b89c-682b749090e7/plugins
{"route":{"id":"2a62ff11-66d2-4b92-b89c-682b749090e7"},"created_at":1732627474,"updated_at":1732627474,"instance_name":null,"name":"plugin-example","consumer":null,"config":{},"service":null,"tags":null,"enabled":true,"id":"5e2356ff-acd7-493c-a35e-a5d511b56cd7","protocols":["grpc","grpcs","http","https"]}
$ 
```

### 4. テストの実施
ヘッダの変更を見るだけなので、ここでも curl で動作を確認します。
```shell-session: Kong Gateway 経由のモックAPIアクセス
$ curl -v http://localhost:8000/mock-route/
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /mock-route/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 60
< Connection: keep-alive
< My-Plugin-Header: example
< X-Powered-By: tinyhttp
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, HEAD, PUT, PATCH, POST, DELETE
< Access-Control-Allow-Headers: content-type
< Date: Tue, 26 Nov 2024 13:26:53 GMT
< Server: kong/3.8.0
< X-Kong-Upstream-Latency: 1
< X-Kong-Proxy-Latency: 1017
< Via: 1.1 kong/3.8.0
< X-Kong-Request-Id: 433d4132bd4ba5051fc5ae597c738015
< 
[
  {
    "id": "9c0f9095-1336-4bf9-978c-96c221699781"
  }
* Connection #0 to host localhost left intact
]
$ 
```
:::note
レスポンスヘッダに `My-Plugin-Header: example` が追加されていることを確認します。
:::

:::note warn
プラグインのソースコードを修正したときは

- ソースコード変更後のファイル保存
- コンテナの再ビルド
- コンテナの再実行

のうちどれか一つが欠けても修正が反映されません。コードの修正には注意が必要になります。
:::

# Pongoノススメ

Kong Gateway のプラグインは、クライアントの要求を受け取り、Upsteram の API サーバにデータを送り、APIサーバから応答電文を受け取ってクライアントに応答する、のように動作します。テストをするには、アップストリームの API サーバと何らかのクライアントツール、そして当然ですが Kong Gateway 本体が必要です。
また、プラグインを Kong に入れただけではプラグインが入ったことが管理API(または管理画面)に示されるだけですから、実行前に設定を行う必要があります。

手動の動作確認では、本番での動作と同じように Kong Gateway を準備し、プラグインを追加し、設定し、クライアントツールで API コールをしなければなりません。

一方、 Pongo を使うと、テストシナリオで Kong Gateway およびプラグインの設定とテスト用 API サーバを準備できます。一度書いたシナリオで常に同じ条件でプラグインのテストができます。

また、 Pongo は、条件がそろっているなら(そして最近のコンピュータでは大抵そろっているので)簡単にインストールでき、Java での maven / gradle でユニットテストを CLI 実行するのと同じ位の手軽さで、実装・修正の繰り返しが可能です。

Pongo を使って、テストのためのインストール・設定・実行の手順書の作成の作業とテスト実行の負担をなるべく増やさずにカスタムプラグインのテストをすることをお勧めします。

### Pongo を使うメリットとデメリット

| | Pongo メリット | Pongo デメリット | 手動 メリット | 手動 デメリット |
| --- | --- | --- | -- | --- |
| 環境 | Pongo とテストシナリオで完結 | basted でのテストの学習が必要 | 本番に近い構成 | Kong 実行環境の構成要素が多めで、テスト実行の手順が複雑になりがち |
| 実装 | 一回書けば何度も実行できる<br/>再利用できる<br/>プラグイン・テストシナリオを修正したら、すぐに `pongo run` で結果を得られるため、開発速度が上がる | 慣れは必要 | RESTful API を把握しているなら、平易に理解できる | Service / Route など依存関係を解決して設定する必要があり、煩雑 |
| 実行 | シンプル<br/>プラグイン・シナリオを書いたら `pongo run`でOK | 管理画面は使えない | RESTful API に慣れていれば API 実行自体は楽<br/>管理画面も併用できる | 実行手順が複雑になりがち |
| 保守 | プラグインプロジェクトごとバージョン管理すれば良い | 想定しているバージョン管理は Git のみ | CLI 実行または管理画面からの設定により、手順化はし易い<br/>バージョン管理の選択は自由 | Kong 実行環境のバージョン管理、モックアップのフォロー・バージョン管理、管理APIコマンドの管理・実行手順の管理と、煩雑 |
| 自動化 | パイプラインジョブで Docker Compose が使えるならテストステージの保守の負荷が低い | パイプラインジョブで Docker Compose が使えない場合は busted 実行環境が必要 | RESTful API のテストとして考えれば、特殊ということはない | 可能だが構成が複雑でテストの維持コストが高い |

# おまけ

## Visual Studio Code の利用
Pongo 実行中は、プラグインのファイルを修正すると自動的にリロードされます。  
また、 Pongo はプラグインファイルをホストコンピュータからコンテナにマウントして実行します。
ですので、 Visual Studio Code (以後 VSCode) などの高機能エディタを使ってプロジェクトを開けば、 `pongo shell` 上ですぐに編集内容を確認することができて便利です。  

WSL2 や 別ホストの Linux で開発する場合、VSCode の [Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) の利用をお勧めします。  

:::note warn
[リモートエクステンションパック](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack)を入れるのが簡単ですが、 Windows 上でこのエクステンションを追加する時点で Windows 側に Docker クライアントが入っていないと Docker Desktop for Windows をインストールしてしまいます。会社で使う際は規約違反にならないかどうかに注意してください([参考](https://www.docker.com/ja-jp/pricing/))。Docker Desktop が利用できない場合は、 Remote-SSH, Remote-WSL エクステンションを個別にインストールした方が良いでしょう。
:::

#### 利用イメージ
WSL2 上での実行はこんな感じになります。

![リモートエクステンションでのPongo利用イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935352/7159d42b-ad4a-ab14-6af5-81b094692818.png)

ファイルを保存すると即反映されますので、ターミナルから API を叩いて修正内容をすぐに確認できます。

#### ログの参照
プラグインのログの出力先はファイルになります。
Pongo で実行する場合は、 `ponto init` を実行したフォルダ内に servroot/logs/ フォルダが作成されます。  
ここに作成される error_log ファイルを VSCode のエディタで参照すると楽ちんです。

![リモートエクステンションでPongoのログを参照している](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935352/172643cf-fcc5-a7f4-3dbe-690016260bae.png)

## rockファイルの作成
rockspec ファイルを記述し、`pongo pack` を実行すると rockファイルが作成されます。

このファイルがあれば、luarocks コマンドを使って、プラグインをテストで実行した Kong 以外の Kong Gateway にインストールすることができます。

```lua: plugin-example-0.0.1-1.rockspec
-- パッケージ情報は schema.lua に合わせます
local plugin_name = "plugin-example"
local package_name = plugin_name
local package_version = "0.0.1"
local rockspec_revision = "1"

-- この変数を展開して得られる GitHub のURLは存在しません
local github_account_name = "openstandia"
local github_repo_name = "kong-plugin-example"
local git_checkout = package_version == "main" and "develop" or package_version


package = package_name
version = package_version .. "-" .. rockspec_revision
supported_platforms = { "linux" }
source = {
  url = "git+https://github.com/"..github_account_name.."/"..github_repo_name..".git",
  branch = git_checkout,
}

description = {
  summary = "Kong Plugin example for qiita.",
  homepage = "https://qiita.com/drafts/b6e272ac5fcba113e405",
  license = "Apache 2.0",
}


dependencies = {
}


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",
  }
}
```

```shell-session: パッケージ作成の様子
$ pwd
/home/ubuntu/kong-plugin-example
$ pongo pack
Kong version: 3.8.0


Stopping after installing dependencies for plugin-example 0.0.1-1


plugin-example 0.0.1-1 is now installed in /usr/local (license: Apache 2.0)

Packed: /kong-plugin/plugin-example-0.0.1-1.all.rock
$ ls -l plugin-example*.rock 
-rw-r--r-- 1 root root 10803 Nov 27 14:01 plugin-example-0.0.1-1.all.rock
$ 

```
:::note
luarocks は Lua のパッケージマネージャです。
:::

https://luarocks.org/

## HTTPプロキシを必要とする環境でのテスト

テストシナリオを書くとき、インターネット上でサービス中のサーバへのアクセスが必要になり、しかし、本番リリースまではHTTPプロキシ越しでないとアクセスできないということがしばしばあります。

これには次のような解決案があります。

#### [Forward Proxy Advanced](https://docs.konghq.com/hub/kong-inc/forward-proxy/)
Kong Gateway Enterprise / Kong Konnect で使えるプラグインです。
テスト対象に限らず、XML Schema, JSON Schema のような設定用URLのアクセス先に対してこのプラグインを適用した Route を設定し、テストするときには実ホストではなく別途 Kong Gateway を経由することでHTTPプロキシ越しにアクセスすることが可能です。

#### [Forward Proxy](https://github.com/tfabien/kong-forward-proxy)
公開されているシンプルなHTTPプロキシプラグインです。
このソースコードを参考にしてテスト時だけHTTPプロキシを使うことで解決できそうですが、確認はしていません。

#### Reverse Proxy をたてる
[TinyProxy](https://tinyproxy.github.io/) のように、Forward Proxy にも対応した Reverse Proxy を立て、開発時にはアクセス先をそちらに向けるという方法もあります。
Kong Gateway と Forward Proxy Advanced を使っても同じですが、構成が複雑になりがちな開発環境をシンプルにできるという利点があります。

https://qiita.com/Syuparn/items/85176c64300217d7537e

https://isabelcmdcosta.medium.com/how-i-set-up-tinyproxy-as-a-forward-proxy-and-reverse-proxy-2a5dc1ed64e4

# おわりに

Pongo を使うと、環境構築にそれほど手間をかけることなくカスタムプラグインのテストをシンプルに実行することができます。
[Pongo 自体のインストールも簡単です](https://qiita.com/jirokichi2017/items/b6e272ac5fcba113e405#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)。

Kong Gateway のカスタムプラグイン作成・テストにお役立ていただけると思います。