Kong GatewayのAdmin APIにエンドポイントを追加する

Kong Gatewayを操作するためのAdmin APIはLuaのフレームワークである[Lapis](https://leafo.net/lapis/)で実装されており、Lapisの作りに従ってAdmin APIにAPIエンドポイントを追加することが出来る。

エンドポイントの追加はカスタムプラグインの追加と同じ要領で追加できるので、実際にやってみる。

## エンドポイントの基礎
Kongは`kong.plugins.<plugin_name>.api`にエンドポイントの定義があれば検出して読み込む。
そのため、エンドポイントを追加する際はカスタムプラグインの要領でプラグインを追加し、その中に`api.lua`を用意して読み込ませることになる。
`api.lua`の基本構造は以下となる。（[Add endpoints to the Admin API](https://docs.konghq.com/gateway/latest/plugin-development/admin-api/#add-endpoints-to-the-admin-api)より抜粋）
```lua
return {
  ["<path>"] = {
     schema = <schema>,
     methods = {
       before = function(self) ... end,
       on_error = function(self) ... end,
       GET = function(self) ... end,
       PUT = function(self) ... end,
       ...
     }
  },
  ...
}
```
`<path>`部分はAPIへのアクセスパスであり、例えば`["/my_endpoint"]`と指定すると`<Admin APIのURL>/my_endpoint`
でアクセス出来るようになる。
パスの中に`:consumers`のようにコロンを含んだ値がある場合、これは動的に変更する部分でアクセス時に`self.params.<:を除いた名前>`に格納される（参考：[Routes&URL Patterns](https://leafo.net/lapis/reference/actions.html#routes-and-url-patterns)）。
UUIDなど外部から変数を渡したい時に利用できる。

`schema`についてはKongのエンティティのスキーマを指定し、フィールドの解析などに利用する。エンティティのスキーマは`kong.db.<エンティティ名>.schema`のような形で指定する。

`methods`の中は実際にAPIを呼び出した際の処理を記述する。
- `before`：前処理的なものを行うための呼び出し。全ての関数の前に実行される（指定は任意）
- `on_error`：他の関数がエラーとなった時にエラーをハンドルする関数。指定しない場合はKongのデフォルトのエラーハンドリングが行われる。
- `GET等`：HTTPメソッドごとの処理を記述する。

ちなみに`api.lua`を持ってるKongの公式プラグインは数が少ないが、実装する際の参考になるので参考までにリンクを貼っておく。

- [ACL](https://github.com/Kong/kong/blob/master/kong/plugins/acl/api.lua)
- [ACME](https://github.com/Kong/kong/blob/master/kong/plugins/acme/api.lua)
- [Prometheus](https://github.com/Kong/kong/blob/master/kong/plugins/prometheus/api.lua)



## 検証
APIエンドポイントを含むプラグインのファイル構造については[Advanced plugin modules](https://docs.konghq.com/gateway/latest/plugin-development/file-structure/#advanced-plugin-modules)に説明がある。
ファイル構造の説明に従うと以下のファイルが必要となる。
```sh
kong/plugins/<プラグイン名>/
├── api.lua
├── handler.lua
└── schema.lua
```
以降、それぞれのファイルを作成して検証する。

### api.lua
ここでは以下の仕様のAPIエンドポイントをお試しで作ってみる。
- `/my_endpoint`
  - GETでアクセスすると接続元と接続先のIPと内部で持ってる文字列を返す
- `/my_endpoint/<任意の文字列>`
  - POSTでアクセスするとパスで指定した任意の文字列を返す

実装は以下のような感じ。
```lua:api.lua
return {
  ["/my_endpoint"] = {
    GET = function(self)
      local client_ip = kong.client.get_ip()
      local server_ip = ngx.var.server_addr
      local msg = "Hello from Kong"
      return kong.response.exit(200, { 
        message = msg,
        ip = {
          client_ip = client_ip, 
          server_ip = server_ip,  
        }
      })
    end,
  },
  ["/my_endpoint/:my_endpoint"] = {
    POST = function(self)
      local msg = "params: " .. self.params.my_endpoint
      return kong.response.exit(200, { 
        message = msg
      })
    end,
  },
}
```
KongのPDKは問題なく呼び出せるので、IPの取得やレスポンスを返す時に利用している。

### handler.luaとschema.luaの作成
`handler.lua`と`schema.lua`については今回はプラグインではないので、中身がないものを適当に用意しておけばOK。
```lua:handler.lua
local CustomEndpoints = {
    PRIORITY = 1000,
    VERSION = "1.0.0",
}

return CustomEndpoints
```
```lua:schema.lua
return {
    name = "custom-endpoints",
    fields = {
        { config = {
            type = "record",
            fields = {}
        }}
    }
}
```

### 導入
ここではKubernetes上のKong Gatewayに導入する。
なお検証環境はHybridモードで展開しているが、APIのエンドポイント追加はAdmin APIがいるControl Planeにのみ影響するため、作成したカスタムプラグインの導入はControl PlaneのみでOK。
またカスタムプラグインの導入方法はここでは`ConfigMap`を利用する。
最初に`ConfigMap`を作成する。3つのluaファイルは`./custom-endpoints`に置いているものとする。
```sh
kubectl create cm kong-plugin-endpoint \
  --from-file=./custom-endpoints/ \
  -n kong \
  --dry-run=client -o yaml \
  | kubectl apply -f -
```
次にControl PlaneのHelmの`values.yaml`に以下を追記し、エンドポイントを追加したプラグインを読み込むようにする。
```yaml
env:
  plugins: bundled,custom-endpoints
plugins:
  configMaps:
  - name: kong-plugin-endpoints
    pluginName: custom-endpoints
```
修正した`values.yaml`を使ってControl Planeを再デプロイする。
```sh
helm upgrade -i kong-aws-cp kong/kong -f ./values.yaml -n kong --wait --debug
```
以上で追加したAPIエンドポイントが有効化された。
なお、カスタムプラグインと異なり、特にプラグインを有効化せずともエンドポイントは有効化されているので、プラグイン導入直後からアクセス出来るようになっている。
最初に`/my_endpoint`の方にGETでアクセスしてみる。
```json
$ curl -sX GET https://adminapi.mydomain.info/my_endpoint | jq .
{
  "message": "Hello from Kong",
  "ip": {
    "client_ip": "192.168.34.31",
    "server_ip": "192.168.53.23"
  }
```
意図したレスポンスが取得できた。
次に`/my_endpoint/<任意の文字列>`へのPOSTも確認する。
```json
$ curl -sX POST https://adminapi.mydomain.info/my_endpoint/hoge | jq .
{
  "message": "params: hoge"
}
```
こちらも確認できた。

## まとめ
カスタムプラグインと同じ感覚でエンドポイントを追加できた。
カスタムプラグイン作成時に独自のエンドポイントを持たせて機能追加したい場合などには有用だと思う。
特にカスタムエンティティなどを実装し始めると使い道も増えそうなので、カスタムプラグイン実装時には覚えておくとよさそう。

## 余談
`daos.lua`を作成している場合、`generate_admin_api`を使うとエンドポイントが自動で追加され、GET等のメソッドに対応する動作も自動で用意される。
カスタムエンティティを使っている場合で`api.lua`の開発・保守が面倒なら`generate_admin_api`の利用も検討するとよいと思う。