1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kong Gatewayのカスタムエンティティを作成する

Last updated at Posted at 2025-01-02

Kong Gatewayでカスタムプラグインを作成する際、プラグインが扱うデータをどこかしらに保存したい時がある。
例えば鍵認証のようなものの場合、プラグイン設定時に指定した鍵情報をリクエスト送信時に参照して一致するか照合するようなケースである。
このようなケースでは外部のDBを用いる方法も考えられるが、Kong GatewayのDBに格納することも出来る。
この独自に格納するデータをカスタムエンティティと呼ぶが、これを今回はこれの使い方を検証する。

基礎知識

Kong GatewayではDBへのアクセスはPDKを使ってDAO(Data Access Object)として抽象化されたものでアクセスできる

カスタムプラグインでこのDAOを実装しようとした場合、DAOを含めたカスタムプラグインの構造は以下となる(参考)。

complete-plugin
├── daos.lua
├── handler.lua
├── migrations
│   ├── init.lua
│   └── 000_base_complete_plugin.lua
└── schema.lua

schema.luahadnler.luaがカスタムプラグインの実装にあたり、カスタムエンティティの実装はdaos.luaで行い、DBの初期化はmigrations以下のコードで行う。
migrations以下のファイルは以下のようになる。

  • init.lua:DBの初期化で利用するファイルの定義
  • 000_xxx.lua:実際に初期化で使うコード

000_xxx.luaのファイル名については厳密な命名規則はないが、先頭に3桁の数字をつけ、実行順にあわせて000_...001_...と昇順のプリフィックスをつけるようになっている。
また以下の慣習的な命名規則がある。

  • 最初のファイルは000_base_<プラグイン名>
  • 以降のファイルは00x_<旧バージョン>_to_<新バージョン>

例えばKey Auth Pluginの場合は

000_base_key_auth.lua
002_130_to_140.lua
003_200_to_210.lua

のように1.3.0から1.4.0での変更、2.0.0から2.1.0の変更を130_to_140200_to_210のようにファイル名で表現している。

カスタムエンティティの実装についてはKey Auth Pluginのコードも参考になるので、必要に応じて参照したい。

実装

ここでは以下のような仕様のカスタムプラグインを作成する。

  • ヘッダに"mykey-add: value"をつけてアクセスするとDBに以下を格納する。
    • UUID (※自動生成)
    • 作成時刻 (※自動生成)
    • value (カラム名はmykeyとする)
  • ヘッダに"mykey-del: UUID"をつけてアクセスするとテーブルに指定したUUIDと合致する項目があれば削除する
  • バージョンアップを想定し、DBの初期化では最初に利用しないカラム(col1)を作成した後、そのカラムを削除してmykeyを作成する

また、今回はカスタムプラグインのテンプレートであるkong-pluginを使って実装し、カスタムプラグインのテストツールであるPongoを使って動作確認する。
Pongoについては@jirokichi2017さんが書いた「Kong Gatewayカスタムプラグインをテストする ~Part1: Pongoを実行する~」がよく纏まってるのでそちらを参考にして欲しい。

最初にテンプレートをcloneする。

git clone https://github.com/Kong/kong-plugin
cd kong-plugin

テンプレートでは以下のようにdaos.luamigrationsは含まれていないので、それぞれを作っていく。

kong/plugins/myplugin/
├── handler.lua
└── schema.lua

テーブルの作成

先にDBのテーブルを作っていく。
まず格納先ディレクトリを作成する。

mkdir kong/plugins/myplugin/migrations

最初にDBの初期化の際にどのファイルを使うかを定義するinit.luaから作成する。

kong/plugins/myplugin/migrations/init.lua
cat <<'EOF' > ./kong/plugins/myplugin/migrations/init.lua
return {
  "000_base_my_plugin",
  "001_100_to_110",
}
EOF

init.luaではDB初期化時に読み込ませるファイルの一覧を記載する。
検証の冒頭で述べたように、バージョンアップを想定して初期ファイルである000_base_my_plugin.luaとバージョンアップで更新する001_100_to_110.luaを用意する。

次にinit.luaに記載した000_base_my_plugin.lua001_100_to_110.luaを作成する。
ここでは公式ドキュメントのMigration file syntaxにあるコードを参考にした(※公式の記述だと'25/1時点で記載ミスがあり、コピペだと動かないので注意)。
000_base_my_plugin.luaではidcreated_atcol1というカラムを持つmy_plugin_tableというテーブルを作成し、col1というカラムに基づくインデックスmy_plugin_table_col1を作成する。

kong/plugins/myplugin/migrations/000_base_my_plugin.lua
cat <<'EOF' > ./kong/plugins/myplugin/migrations/000_base_my_plugin.lua
return {
 postgres = {
   up = [[
     CREATE TABLE IF NOT EXISTS "my_plugin_table" (
       "id"           UUID                         PRIMARY KEY,
       "created_at"   TIMESTAMP WITHOUT TIME ZONE,
       "col1"         TEXT
     );

     DO $$
     BEGIN
       CREATE INDEX IF NOT EXISTS "my_plugin_table_col1"
                               ON "my_plugin_table" ("col1");
     EXCEPTION WHEN UNDEFINED_COLUMN THEN
       -- Do nothing, accept existing state
     END$$;
   ]],
 }
}
EOF

001_100_to_110.luaはプラグインのバージョンアップ時に追加されたファイルと想定し、以下の変更をDBに行う。

  • 新しいカラムmykeyを追加
  • 古いカラムcol1を削除
kong/plugins/myplugin/migrations/001_100_to_110.lua
cat <<'EOF' > ./kong/plugins/myplugin/migrations/001_100_to_110.lua
return {
 postgres = {
   up = [[
     DO $$
     BEGIN
       ALTER TABLE IF EXISTS ONLY "my_plugin_table" ADD "mykey" TEXT UNIQUE;
     EXCEPTION WHEN DUPLICATE_COLUMN THEN
       -- Do nothing, accept existing state
     END$$;
   ]],
   teardown = function(connector, helpers)
     assert(connector:connect_migrations())
     assert(connector:query([[
       DO $$
       BEGIN
         ALTER TABLE IF EXISTS ONLY "my_plugin_table" DROP "col1";
       EXCEPTION WHEN UNDEFINED_COLUMN THEN
         -- Do nothing, accept existing state
       END$$;
     ]]))
   end,
 }
}
EOF

up =で記載されている箇所はkong migrations upで動作し、teardown =で記載されている箇所はkong migrations finishで動作する。
そのため、以下のような使い分けが推奨されている。

  • 非破壊的な操作:upで記述
  • 破壊的な操作:teardownで記述

カスタムエンティティの作成

カスタムエンティティのためのスキーマはdaos.luaによって定義できる。
設定できる項目の説明についてはDefine a schemaを参照。

なお、上記サイトには載っていないが、workspaceableをつけるとWorkspace単位でエンティティが存在するのかどうかも指定できる。(今回はPongoをWorkspaceがないOSS版で起動していたので、有効化はしていない)
workspaceabletrueエンティティはグローバルエンティティ(≠グローバルスコープ)となり、Workspaceを跨いで参照できる(mTLSで使う証明書やGroups、Licenseなどが該当)。

ここでは以下のように作成する。

kong/plugins/myplugin/daos.lua
cat <<'EOF' > ./kong/plugins/myplugin/daos.lua
local typedefs = require "kong.db.schema.typedefs"

return {
   {
   name          = "my_plugin_table",
   primary_key   = { "id" },
   -- workspaceable = true,
   fields = {
     { id = typedefs.uuid },
     { created_at = typedefs.auto_timestamp_s },
     { mykey = { type = "string", required  = true }}
   }
   }
}
EOF

先ほど作成したmigrations以下のDBの構成に合わせたフィールドを定義する。
また、作成したDAOはkong.db.<指定したname>でアクセスできるようになる。

カスタムプラグインの作成

DBにアクセスするカスタムプラグインを作成する。
schema.luaについては今回検証時にパラメータは渡さないので、空に近いものを用意する。

kong/plugins/myplugin/schema.lua
cat <<'EOF' > ./kong/plugins/myplugin/schema.lua
local PLUGIN_NAME = "myplugin"
local schema = {
  name = PLUGIN_NAME,
  fields = {
    { config = {
        type = "record",
        fields = {}
      }}
  }
}
return schema
EOF

handler.luaは以下のようにした。

kong/plugins/myplugin/handler.lua
cat <<'EOF' > ./kong/plugins/myplugin/handler.lua
local plugin = {
  PRIORITY = 1000,
  VERSION = "0.1",
}
  
function plugin:access(conf)
  local add_value = kong.request.get_header("mykey-add")
  local del_value = kong.request.get_header("mykey-del")

  if add_value then
    local ok, err = kong.db.my_plugin_table:insert({mykey = add_value})
    if not ok then
      kong.log.err("Failed to insert into my_plugin_table: ", err)
    end
  elseif del_value then
    local entity, err = kong.db.my_plugin_table:select({id = del_value})
    if not entity then
      kong.log.err("Failed to find entity with name: ", del_value, " Error: ", err)
      return kong.response.exit(404, { message = "Entity not found" })
    end

    local ok, err = kong.db.my_plugin_table:delete({ id = entity.id })
    if not ok then
      kong.log.err("Failed to delete entity: ", err)
      return kong.response.exit(500, { message = "Internal Server Error" })
    end
  end

end
return plugin
EOF

:access()を指定することで、リクエスト送信時にこの関数が動作するようにしている。
DAOの利用箇所は以下となる。

  • kong.db.my_plugin_table:insert({mykey = add_value}):DBにエンティティをInsert
  • kong.db.my_plugin_table:select({id = del_value}):DB内のプライマリキー(UUID)と一致するエンティティを取得
  • kong.db.my_plugin_table:delete({ id = entity.id }):UUIDが一致するエンティティを削除

以上でカスタムプラグインの準備は終了となる。

動作確認

ここではPongoを使って動作確認する。
Pongoを起動し外部からアクセスできるようにする。

pongo up
pongo expose

shellをアタッチし、Kong Gatewayを起動する。

pongo shell
kms

この段階でPostgres内に定義したテーブルが作成される。

$ docker exec -it pongo-91c2117c-postgres-1 psql -U kong -d kong_tests -c "SELECT * FROM my_plugin_table;"
 id | created_at | mykey
----+------------+------
(0 rows)

Kong Gateway起動後、Service/Route/Pluginを作成する。
ここではhttpコマンドで作成した。

http POST :8001/services \
    name=example-service \
    url=http://httpbin.org
http POST :8001/services/example-service/routes \
    name=example-route \
    paths:='["/echo"]'
http POST :8001/services/example-service/plugins \
    name=myplugin 

動作確認する。
ヘッダにmykey-add:testしたものとmykey-add:test2を付加したもので2回アクセスする。

http GET :8000/echo/ip \
    mykey-add:test
http GET :8000/echo/ip \
    mykey-add:test2

テーブルを確認する。

                  id                  |     created_at      | mykey
--------------------------------------+---------------------+-------
 96244c26-4778-47aa-a52e-958a43aa1f97 | 2025-01-02 06:45:06 | test
 656c428f-04ee-4851-b8bf-b29e0423f1dd | 2025-01-02 07:00:13 | test2
(2 rows)

正常にエンティティが追加されている。
また削除も試してみる。

http GET :8000/echo/ip \
    mykey-del:96244c26-4778-47aa-a52e-958a43aa1f97

テーブルの内容は以下となった。

                  id                  |     created_at      | mykey
--------------------------------------+---------------------+-------
 656c428f-04ee-4851-b8bf-b29e0423f1dd | 2025-01-02 07:00:13 | test2
(1 row)

問題なく操作できた。

まとめ

無事カスタムプラグインでDAOを定義してエンティティを追加することが出来た。
参考となる文献が少ないのがややネックだが、公開されているKongのPluginのソースを参考にすれば実装で悩むことはあまりないと思われる。
データを残したいようなケースでいちいちDBを用意したくない時とかなどに使ってみると良さそうだ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?