Kongのプラグインを書いてみる

はじめに

Kongはマルチクラウド対応されたAPIゲートウェイです。Kongはlua-nginx-moduleLuaと呼ばれる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
  • handler.lua
    • プラグインの実態です。個々に任意のロジックを記述することができます。この中で定義されるファンクションはリクエストとコネクションのライフサイクルの中で任意のタイミングで実行させることができます
  • schema.lua
    • プラグインに対してAdminAPI等を通した設定変更を行なう際に、ユーザによって入力されなければならない設定値などのルールを定義します

上記の構成は基本的なものでよりKongの実装に深く入り込むようなPluginの記述はadvanced-plugin-modules) を作成することによって可能となるようですが、この記事ではまずは基本的なPluginを書いてみてそれを適用してみることをゴールとしているので、一旦は深く触れないで起きます

Pluginの適用方法

KongにPluginとして認識させるために以下の3つのルールに従う必要があります。

  • Luaパッケージの命名規約
  • LuaのPackage Pathに配置する
  • kong.conf等の設定値pluginsプラグイン名を記述する

Luaパッケージの命名規約

KongのPluginにはパッケージの命名規約があり、以下のルールにしたがって書かれるPluginを認識し、適用します。

kong.plugins.<plugin_name>.<module_name>

LuaのPackage Pathに配置する

作成したPluginは、Luapackage.pathに配置してやる必要があります、これはLUA_PATH環境変数によって指定することが可能です。
LUA_PATHのデフォルト値は以下のようになります。

  • ./?.lua;./?/init.lua;

kong.conf等の設定値plugins変数へ記述する

KongでPluginを認識するために、kong.conf等の設定値であるpluginsプロパティにプラグイン名を追記してやる必要があります。この設定は以下のようにコンマで区切られたリスト形式で記述できます。

plugins = bundled,my-custom-plugin

上記の記述の場合bundledmy-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.luahandler.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.luaLua 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を記述すれば結構いろんなことができそうだと感じました。