Kongのプラグインを書いてみる
はじめに
Kongはマルチクラウド対応されたAPIゲートウェイです。Kongはlua-nginx-moduleとLuaと呼ばれるScript言語を利用して、 拡張プラグインを書くことが可能で、その機能を試してみようと思います。
ここでは、以下をゴールとします
- Kong Pluginの基本的な書き方とその適用方法を知る
あくまでKongの基本的なPluginを書いてみることを目的としているため、Luaについては深く触れません。
Kongそのもののエントリーはこちらに書いているのでよかったら読んでみてください。
lua-nginx-moduleとKongとの関係
lua-nginx-moduleはNingxに対してLuaJIT 2.0/2.1を組み、再コンパイルの必要としない拡張ポイントを提供するためのOSSです。
KongはNginxベースのAPIであり、このlua-nginx-moduleが組み込まれており、その恩恵を受けることができます。KongへのプラグインをLua scriptで記述することが可能です。
KongのPluginの記述
KongではPlugin Development Kit(PDK)を提供しており、request/responseオブジェクトやストリームを利用することにより任意のロジックを組み込むことが可能となります。
ちなみに、Kongで言うPluginはこのLua Moludeの集まりで、実際にはModuleを記述して、それらの集まりをPluginとして、Kongに組み込むようなイメージになるようです。
Pluginの基本構成
基本的なPluginの構成は以下のようなLua Moduleから成り立ちます。
simple-plugin ├── handler.lua └── schema.lua
上記の構成は基本的なものでよりKongの実装に深く入り込むようなPluginの記述はadvanced-plugin-modules) を作成することによって可能となるようですが、この記事ではまずは基本的なPluginを書いてみてそれを適用してみることをゴールとしているので、一旦は深く触れないで起きます
Pluginの適用方法
KongにPluginとして認識させるために以下の3つのルールに従う必要があります。
Luaパッケージの命名規約
KongのPluginにはパッケージの命名規約があり、以下のルールにしたがって書かれるPluginを認識し、適用します。
kong.plugins.<plugin_name>.<module_name>
LuaのPackage Pathに配置する
作成したPluginは、Luaのpackage.path
に配置してやる必要があります、これはLUA_PATH
環境変数によって指定することが可能です。
LUA_PATH
のデフォルト値は以下のようになります。
./?.lua;./?/init.lua;
kong.conf
等の設定値plugins
変数へ記述する
KongでPluginを認識するために、kong.conf
等の設定値であるplugins
プロパティにプラグイン名を追記してやる必要があります。この設定は以下のようにコンマで区切られたリスト形式で記述できます。
plugins = bundled,my-custom-plugin
上記の記述の場合bundled
、my-custom-plugin
のネームスペースで定義されたモジュールを読み込みます。
Pluginを書いてみる
一通り、Pluginの書き方と適用方法についてまとめたので、実際にPluginを書いて行こうと思います。
今回はx-custom-header
と行った名前のHTTPヘッダを追加するようなPluginを書いてみたいと思います。
また、Kong本体はDockerを用いてインストールするようにします。
動作環境
動作環境は以下のようになってます。
$ uname -srvmpio Linux 5.3.0-46-generic #38~18.04.1-Ubuntu SMP Tue Mar 31 04:17:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ docker -v Docker version 19.03.8, build afacb8b7f0
下準備
設定ファイルを記述する
最低限必要な設定ファイルのベースを作成しておきます。後ほどこちらに必要な設定を追加していきます。
kong.yml
_format_version: "1.1"
kong.conf
admin_listen = 0.0.0.0:8001
Dockerfileを記述しておく
前述の通りKongのインストールにはDockerを利用します。
今回はいくつかのFROM kong:2.0.3-alpine
をベースにプラグインと設定ファイルを取り込んで、起動するようなDokcerfileを記述して動かそうと思います。
今の段階では、上記で記述した設定ファイルを組み込むだけのものとなっています。
FROM kong:2.0.3-alpine ADD ./kong.conf /etc/kong/ ADD ./kong.yml /usr/local/kong/declarative/ ENV KONG_DATABASE off ENV KONG_DECLARATIVE_CONFIG /usr/local/kong/declarative/kong.yml CMD ["kong", " migrations", "bootstrap"] CMD ["kong", "start", "--v"]
UpStreamを用意する
GoとGinを使って簡単なUpStreamを用意しておきます。 ヘッダーを受け取りその値をレスポンスJsonとして返すようなアプリを作成します。
go-sample.go
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { header := c.GetHeader("x-custom-header") if header == "" { header = "nothing" } c.JSON(200, gin.H{ "got-header": header, }) }) r.Run() }
読めばわかることかもしれませんが、このアプリは、GET /
のアクセスに対し、x-custom-header
が付与されているか否かで、以下の2パターンのレスポンスを返します
$ go build . $ ./go-sample ## ヘッダーが付与されていない場合 $ curl localhost:8080 {"got-header":"noting"} ## カスタムヘッダが付与されている場合 $ curl -H "x-custom-header: hello" localhost:8080 {"got-header":"hello"}
UpStreamをServiceとして追加する
Pluginを作成する前にUpStreamをServiceとして登録しておきます。
先程、用意したkong.ymlに以下の設定を追記します。
_format_version: "1.1" services: - name: kong-service # Goのサンプルはホストマシンで動くため、Docker runの際にgo_sampleのホスト名でIPを登録する url: http://go_sample:8080 routes: - name: kong-route paths: - /
それでは先程のDokcerfileを用いてイメージを作成し、Kongを動かしてみましょう。
# UpStreamであるGoのサンプルを動かしておく $ ./go-sample $ docker build . -t kong-plugin-sample:1.0.0 $ docker run -p 8000:8000 --add-host=go_sample:<hostのIPアドレス> kong-plugin-sample:1.0.0 $ curl localhost:8000 {"got-header":"noting"}
Kongを通した疎通確認が行えました。下準備はここまででOKです。
プラグインを書いてみる
それでは、本題であるPluginの記述に入っていきます。
Pluginの基本構成
のところでも書きましたがKongで基本的なPluginを作成する際にはschema.lua
とhandler.lua
を記述する必要があります。
handlar.luaについて
handler.luaはカスタムなロジックをリクエストとレスポンスやストリームのライフサイクルの中でいくつかのエントリーポイントを利用し組み込むことが可能です。これはbase_plugin.lua
インターフェースを実装することに可能となります。 例えばリクエストをUpStreamにプロキシする前に何らかの処理を組み込みたい場合は以下のように記述します。
// base_pluginインターフェースを利用するための宣言 local BasePlugin = require "kong.plugins.base_plugin" // インターフェースを実装する local CustomHandler = BasePlugin:extend() CustomHandler.VERSION = "1.0.0" // Pluginにプライオリティをつけることができる CustomHandler.PRIORITY = 10 // Pluginのブロック関数ここで、プラグイン名を指定し、ログ出力にその名前が利用される function CustomHandler:new() CustomHandler.super.new(self, "my-plugin") end // accessメソッドを実装することに寄ってリクエストが来た際に前処理として何か挟める function CustomHandler:access(config) CustomHandler.super.access(self) // 任意のロジックを記述する end return CustomHandler
また、base_pluginインターフェースでは以下のような関数が用意されており、それぞれのタイミングで任意の処理をインジェクとすることが可能となっています。
- Http Moduleに対しては以下のようなインジェクのための関数を提供します。
関数名 | 説明 |
---|---|
:init_worker() | NginxのWokerがそれぞれスタートアップするたびに実行されます |
:certificate() | SSHハンドシェイクのSSL認証の間に実行されます |
:rewrite() | すべてのリクエストに対して実行されます。このフェーズではどのServiceやConsumerも指定されません。よってグローバルなプラグインとして設定されたときのみ実行されます |
:access() | リクエストがUpStreamにプロキシされる前に実行されます。 |
:header_filter() | UpStreamからすべてのデータを受け取った後に実行されます |
:body_filter() | ChunkのレスポンスボディをUpStreamから受け取るたびに実行されます。よって、レスポンスサイズが大きい場合は何度も実行されることになります |
:log() | 最後のレスポンスがクライアントに送信された後に実行されます |
- Stream Moduleに対しては以下のような関数を提供します。
関数名 | 説明 |
---|---|
:init_worker() | NginxのWokerがそれぞれスタートアップするたびに実行されます |
:preread() | すべてのコネクションに対して一度実行されます |
:log() | すべてのコネクション終了時に一度実行されます |
また、:init_worker()
以外のすべての関数はLua Tableの引数を受け取り、ユーザから指定された値を保持しています。
handlerのプライオリティ
ハンドラーには実行順序を制御するためにプライオリティを設定することが可能です。
ここでは深く触れませんが気になる人は公式サイト(Plugins execution order)を読んでみてください。
hanlder.luaを書いてみる
適用されたServiceに対するリクエストに対して、x-custom-header
を付与するプラグインを記述してみます。
local BasePlugin = require "kong.plugins.base_plugin" local CustomHandler = BasePlugin:extend() CustomHandler.VERSION = "1.0.0" CustomHandler.PRIORITY = 10 function CustomHandler:new() CustomHandler.super.new(self, "my-plugin") end function CustomHandler:access(config) CustomHandler.super.access(self) ngx.req.set_header("x-custom-header", "hello, kong plugins") end return CustomHandler
リクエストを受け取った際に処理を噛ませたかったので:access()
を実装しました。
また、lua-nginx-module
に寄って提供されるngx.req.set_header()
関数ででヘッダーを追加しています。
schema.luaについて
このモジュールでは、作成したPluginに対してユーザが後から設定変更を行なうためのルールを定義します。
schema.luaはLua Tableを返し、例えば以下のように記述します。
// lua tableを返す return { name = "my-plugin", no_consumer = true, fields = {} }
fields = {}
の中により詳細な条件を記述して聞くことになります。
今回は一旦Pluginを書いて適用することを目的としていますのでSchemaについては深く触れません。上記の設定をそのまま使おうと思います。
もし、より突っ込んだ情報がほしい方は公式サイト(schema.lua specifications)を参照してみてください。
Kongに適用する
記述した、PluginをKongに適用してみましょう。
Kongに適用するためにいくつか設定を書き換える必要があります。
まずはkong.conf
を以下のように書き換えます。
plugins = my-plugin admin_listen = 0.0.0.0:8001
これでKongが${Lua_PATH}/kong/plugins/my-plugin
配下のモジュールをPluginとして読み込みようになります。
次に、kong.ymlを修正し、ServiceにPluginを適用する記述を行います。
_format_version: "1.1" services: - name: kong-service url: http://go_sample:8080 # 以下のPluginの設定を追記する plugins: - name: my-plugin routes: - name: kong-route paths: - /
最後にDockerfileを修正して、Pluginを適切なディレクトリに配置するようにします。
FROM kong:2.0.3-alpine ADD ./kong.conf /etc/kong/ ADD ./kong.yml /usr/local/kong/declarative/ ADD ./plugins/handler.lua ./kong/plugins/my-plugin/ ADD ./plugins/schema.lua ./kong/plugins/my-plugin/ ENV KONG_DATABASE off ENV KONG_DECLARATIVE_CONFIG /usr/local/kong/declarative/kong.yml CMD ["kong", " migrations", "bootstrap"] CMD ["kong", "start", "--v"]
これで、イメージをビルドし直すとPluginが適用されているはずです。
$ docker build . -t kong-plugin-sample:1.0.0 $ docker run -p 8000:8000 --add-host=go_sample:<hostのIPアドレス> kong-plugin-sample:1.0.0
kongにcurlを叩いてみます。
$ curl localhost:8000 {"got-header":"Hello, kong plugin"}
プラグインが適用されて、ヘッダーが追加されていることが確認できました。
今回動かしたやつのソース
今回動かしたもののソースはいかに置いてあります。もし、何かの参考になれば幸いです。また、何かこうした方が良いみたいな意見があればいただけると幸いです。
感想
Pluginを記述すれば結構いろんなことができそうだと感じました。