KongのRequest Transformerプラグインをカスタマイズする

## はじめに

OSSの[Kong](https://docs.konghq.com/gateway/latest/)は世界で最も人気のあるAPI Gatewayで、認証認可や流量制御などプラグインを有効化するだけで用意することができます。

プラグインはこちらの[Plugin Hub](https://docs.konghq.com/hub/)にリストアップされており、必要なものを選んで導入することができますが、必要な機能がサポートされていない、といったケースもあるかと思います。
その際には**Kong提供のプラグインをベースにカスタマイズすることも可能**です。

本記事ではリクエスト情報の編集を行う[`Request Transformer Plugin`](https://docs.konghq.com/hub/kong-inc/request-transformer/)への機能追加を例に、Kong提供プラグインのカスタマイズ方法を記載します。
記事のサンプルコードは[こちら](https://github.com/Naoto-raimi/custom-request-transformer)になります。

また、**OSSのAPI Gateway「Kong」の日本コミュニティ Kong Community, Japan として活動も行っています。興味のある方は[slack](https://bit.ly/kongjpslack)にご参加いただければ幸いです。**


## プラグインのコードの入手

`request transformer`のようなKong提供のプラグインはkongのイメージに予め含まれているのでコピーしてきます。
今回はDBレスモード(yamlファイルでの設定管理)でのKong利用とします。

まず、適当なkong.ymlを作成します

```kong.yml
_format_version: "1.1"

services:
  - name: example_service
    url: http://httpbin.org/status/200
    routes:
      - name: example_route
        paths:
          - /example
```

次章以降のためにDockerfileを作っておきます

```Dockerfile:Dockerfile
FROM kong:3.0.0-alpine
USER root

COPY kong.yml /kong/declarative/kong.yml

# DBレスモード
ENV KONG_DATABASE off 
ENV KONG_DECLARATIVE_CONFIG=/kong/declarative/kong.yml
ENV KONG_PROXY_ACCESS_LOG /dev/stdout
ENV KONG_ADMIN_ACCESS_LOG /dev/stdout
ENV KONG_PROXY_ERROR_LOG /dev/stderr
ENV KONG_ADMIN_ERROR_LOG /dev/stderr
```

以下でrequest-transformerのコードを入手します。

```shell
docker build -t custom-kong .
docker run -d --name kong-dbless -p 8000:8000 custom-kong
docker cp kong-dbless:/usr/local/share/lua/5.1/kong/plugins/request-transformer request-transformer
```

以下のフォルダ/ファイルがコピーしたrequest-transformerディレクトリに含まれていればokです

- migrations
- access.lua
- handler.lua
- schema.lua

## Kongプラグインについて

Kongのプラグインは`Lua`という言語で書かれており、主な構成ファイルは`handler.lua`と`schema.lua`になります。

1. handler.lua

   ライフサイクル（request, response, logなど）の各フェーズで実行するプラグインの処理を記載する。

2. schema.lua

   ユーザーが動作を変更するための変数を定義する。


`request-transformer`ではhandler.lua内で処理を記載したaccess.luaを呼び出す構成になっています。
（migrations配下はDBのマイグレーション用）

## request-transformerのカスタマイズ

今回はカスタマイズ例として、**リクエストボディに環境変数をマッピングできるように変更します。**
**UIでは保持できない機密情報などのパラメータをKongでマッピングし、後続のAPIに流すことができます。**

### 必要なライブラリのインストール

環境変数を扱うことのできる[lua-resty-env](https://github.com/3scale/lua-resty-env)を導入します。
Dockerfile内でluarocks installします。

luarocksがない方は`brew install luarocks`でインストールしてください。

```Dockerfile:Dockerfile
FROM kong:3.0.0-alpine
...
# 以下を追加
RUN luarocks install lua-resty-env

ENV KONG_DATABASE off
...
```

### access.luaの書き換え

request-transformerではtemplate文字列(`$(...)`)でヘッダーやクエリ文字列、URI文字列を使用することができます。([Template as a Value](https://docs.konghq.com/hub/kong-inc/request-transformer/#template-as-a-value))
ここでは`$(env.~)`で環境変数を使用できるようにします

`resty.env`を呼び出し、envという文字列に対してenv.list()で環境変数を返すように変更します

```lua:access.lua
local multipart = require "multipart"
local cjson = require "cjson"
local pl_template = require "pl.template"
local pl_tablex = require "pl.tablex"
--- 以下を追加 ---
local resty_env = require "resty.env"
...

local __meta_environment = {
  __index = function(self, key)
    local lazy_loaders = {
      headers = function(self)
        return get_headers() or EMPTY
      end,
      query_params = function(self)
        return get_uri_args() or EMPTY
      end,
      uri_captures = function(self)
        return (ngx.ctx.router_matches or EMPTY).uri_captures or EMPTY
      end,
      shared = function(self)
        return ((kong or EMPTY).ctx or EMPTY).shared or EMPTY
      end,
      --- 以下を追加 ---
      env = function(self) 
        return resty_env.list() or EMPTY
      end
    }
    ...
}

template_environment = setmetatable({
  -- here we can optionally add functions to expose to the sandbox, eg:
  -- tostring = tostring,  -- for example
  -- because headers may contain array elements such as duplicated headers
  -- type is a useful function in these cases. See issue #25.
  type = type,
}, __meta_environment)

local function clear_environment(conf)
  rawset(template_environment, "headers", nil)
  rawset(template_environment, "query_params", nil)
  rawset(template_environment, "uri_captures", nil)
  rawset(template_environment, "shared", nil)
  --- 以下を追加 ---
  rawset(template_environment, "env", nil)
end

...
```

## 変更したプラグインの有効化

### プラグインの追加

今回編集したプラグインを使えるようにDockerfileに記述を追加します
編集したrequest-transformerディレクトリを/usr/local/share/lua/5.1/kong/plugins/にコピーします。
また、以下の環境変数を設定します

| Env Variable       | Value                              | Description        |
| ------------------ | ---------------------------------- | ------------------ |
| KONG_UNTRUSTED_LUA | on                                 | カスタムLuaコードを使えるようにする設定 |

```Dockerfile:Dockerfile
FROM kong:3.0.0-alpine
...

# 以下を追加
ENV KONG_UNTRUSTED_LUA on
COPY request-transformer /usr/local/share/lua/5.1/kong/plugins/request-transformer
```

## 変更したプラグインを使用してみる

### 環境変数の設定

まずはリクエストボディにマッピングする環境変数を定義します。
コンテナに設定した環境変数をlua-resty-envで呼び出すには環境変数名をNginxで指定する必要があります。
以下のように`KONG_NGINX_MAIN_ENV`に環境変数を指定します。
複数指定したい場合は`KONG_NGINX_MAIN_ENV=FOO; env BAR;`と指定します。
（詳しくは[こちらのissue](https://github.com/Kong/kong/issues/7083)に記載があります）

```docker-compose.yml
version: "3"
services:
  kong:
     build: .
     container_name: kong-dbless
     environment:
        - KONG_NGINX_MAIN_ENV=ENV; env SECRET
        - ENV=dev
        - SECRET=secret
     command: >
        /bin/bash -c "
        kong start"
     restart: always
     ports:
      - 8000:8000
```

### routeへのプラグインの追加

kong.ymlにrequest-transformerで環境変数の`SECRET`をリクエストボディにマッピングしてみます

```kong.yml
_format_version: "1.1"

services:
  - name: example_service
    url: http://httpbin.org/post
    routes:
      - name: example_route
        methods:
          - POST
        paths:
          - /httpbin/post
    plugins:
      - name: request-transformer
        config:
          add:
            body:
              - secret:$(env.SECRET)
```

これでコンテナを起動します

```
docker-compose up -d --build
```

### リクエスト

localhost:8000/httpbin/postにリクエストしてみます
今回upstreamに指定している[httpbin.org/post](https://httpbin.org/#/HTTP_Methods/post_post)はリクエストした内容をレスポンスとして返してくれます

```sh
curl -X POST -H "Content-Type: application/json" -d '{"test": "test"}' http://localhost:8000/httpbin/post

>>> 
{
  "args": {}, 
  "data": "{\"secret\":\"secret\",\"test\":\"test\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Content-Length": "33", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.79.1", 
    "X-Forwarded-Host": "localhost", 
    "X-Forwarded-Path": "/httpbin/post", 
    "X-Forwarded-Prefix": "/httpbin/post"
  }, 
  "json": {
    "secret": "secret", 
    "test": "test"
  }, 
  "url": "http://localhost/post"
}
```

ちゃんと`SECRET`の内容がボディにマッピングされていることが確認できます。
上記を応用することで、UIでは保持できない機密情報などのパラメータをKongでマッピングし、後続のAPIに流すことができます。