Kongのカスタムプラグイン内からAPIアクセスを行う

## はじめに

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

プラグインはこちらの[Plugin Hub](https://docs.konghq.com/hub/)にリストアップされており、必要なものを選んで導入することができますが、必要な機能がない場合、**Luaという言語でカスタムプラグインを作成することも可能です**。

本記事ではカスタムプラグイン内部でAPIアクセスを行い、その結果を踏まえてUpstreamへリクエストを行うサンプルプラグインを作成します。
記事のサンプルコードは[こちら](https://github.com/Naoto-raimi/kong-api-access-plugin-sample)になります。

また、**OSSのAPI Gateway「Kong」の日本コミュニティ Kong Community, Japan として活動も行っています。ご興味のある方は[コミュニティサイト](https://www.kongjp.org/)をご覧ください。**


## kong.ymlの作成

今回はDBレスモード(yamlファイルでの設定管理)でのKong利用とします。

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

```kong.yml
_format_version: "1.1"

services:
  - name: example_service
    url: http://httpbin.org/post
    routes:
      - name: example_route
        methods:
          - POST
        paths:
          - /httpbin/post
```

ここでは`/httpbin/post`にきたリクエストを`http://httpbin.org/post`に流す設定をしています。

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

ここでは例として、kongから`http://httpbin.org/post`にリクエストを送る前に、規制状態を確認するAPIにリクエストし、その結果を踏まえてレスポンスを返すプラグインを作成します。

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

1. handler.lua

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

2. schema.lua

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

### handler.luaの作成

`plugins/restriction-check`というディレクトリを作成し、`handler.lua`というファイルを作成します。以下が全文です。

```lua: handler.lua
local http = require "resty.http"
local cjson = require "cjson"


local RestrictionCheck = {
  VERSION = "1.0.0",
  PRIORITY = 99999,
}

function RestrictionCheck:access(conf)
  local httpc, err = http.new()
  httpc:set_timeout(5000)

  local response, error = httpc:request_uri(conf.target_url, {
    method = "GET"
  })

  if error then
    return kong.response.exit(
      500,
      {
        message = "Restriction Check API is not working."
      }
    )
  end

  local table_response_body = cjson.decode(response.body)

  if table_response_body[conf.decide_key] then
    return kong.response.exit(
      503,
      {
        message = "This API is under maintenance."
      }
    )
  end
end

return RestrictionCheck
```

ライブラリは以下を使用します

- [lua-resty-http](https://github.com/ledgetech/lua-resty-http)
    - httpクライアントとして使用します。
    - Dockerfileでビルドする際にインストールします。
- [cjson](https://github.com/mpx/lua-cjson)
    - jsonのエンコード/デコードで使用します。
    - kongのイメージに元から入っているのでインストール不要です。

まずは以下のようにバージョンとPriorityを定義します

```lua
local RestrictionCheck = {
  VERSION = "1.0.0",
  PRIORITY = 99999,
}
```

Priorityはプラグインが実行される順番に寄与し、値が大きいほどフェーズ内（Access、Body_filterなど）において先に実行されます。
[Plugin Ordering Reference](https://docs.konghq.com/konnect/reference/plugins/)

ここでは大きめな値`99999`を設定しておきます。

<br>

次に、処理を記載します。

```lua
function RestrictionCheck:access(conf)
  ...
end
```

kongの処理のライフサイクルは[こちら](https://docs.konghq.com/gateway/latest/plugin-development/custom-logic/#available-contexts)のようになっており、リクエストがkongに来た時の処理は`access`フェーズとなるため、その時の処理を記述します。

また引数として、kong.ymlファイルで指定するconfigの設定値（後述するschema.luaで定義します）を受け取ることができます。

<br>

そして、以下のようにAPIアクセスする処理を記載します

```lua
local httpc, err = http.new()
httpc:set_timeout(5000)

local response, error = httpc:request_uri(conf.target_url, {
method = "GET"
})

if error then
return kong.response.exit(
  500,
  {
    message = "Restriction Check API is not working."
  }
)
end

local table_response_body = cjson.decode(response.body)

if table_response_body[conf.decide_key] then
return kong.response.exit(
  503,
  {
    message = "This API is under maintenance."
  }
)
end
```

`lua-resty-http`では、`httpc:request(params)`でHTTPリクエストできます。レスポンスボディは`response.body`に文字列で入っているので、cjsonでluaのテーブルにデコードして使っています。
詳しくは[github](https://github.com/ledgetech/lua-resty-http)を確認してください。

また、APIのurlは`target_url`、規制状態を表す項目は`decide_key`として、kong.ymlの設定値から取得することにします。後述のschema.luaで定義します。

加えて、`kong.response.exit`でkongでレスポンスを返すことができます（[公式ドキュメント](https://docs.konghq.com/gateway/latest/plugin-development/pdk/kong.response/#kongresponseexitstatus-body-headers)）
ここでは、規制状態取得APIへのリクエストでエラーが起きた時、および規制状態が`true`だったときにそれぞれエラーレスポンスを返す実装としています。

### schema.luaの作成

`plugins/restriction-check`に`schema.lua`というファイルを作成します。
`handler.lua`で用いる`target_url`、`decide_key`を定義します。

```lua:schema.lua
local typedefs = require "kong.db.schema.typedefs"

return {
  name = "restriction-check",
  fields = {
    { consumer = typedefs.no_consumer },
    { protocols = typedefs.protocols_http },
    { config = {
        type = "record",
        fields = {
          { target_url = {
            type = "string",
            required = true
          }},
          { decide_key = {
            type = "string",
            required = true
          }}
        }
      },
    },
  }
}
```

ここでは共にString型で必須(`required=true`)として定義します。
記述方法の詳細は公式ドキュメントをご覧ください（[describing-your-configuration-schema](https://docs.konghq.com/gateway/latest/plugin-development/configuration/#describing-your-configuration-schema))

## Dockerfileの作成

### lua-resty-httpのインストール

kongのベースイメージ（ここでは`kong:3.0.0-ubuntu`を使っています）に
今回使用するライブラリ`lua-resty-http`をインストールします。

`luarocks`というluaのパッケージ管理ライブラリを使います。

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

RUN luarocks install lua-resty-http
...
```

### 作成したプラグインの追加

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

| Env Variable       | Value                              | Description        |
| ------------------ | ---------------------------------- | ------------------ |
| KONG_UNTRUSTED_LUA | on                                 | カスタムLuaコードを使えるようにする設定 |
| KONG_PLUGINS  | bundled,restriction-check | bundled(imageに含まれているプラグイン)と今回作成した`restriction-check`を指定する |
| KONG_DATABASE  | off | db-lessモード（kong.ymlで設定値を管理）|
| KONG_DECLARATIVE_CONFIG  | /kong/declarative/kong.yml | kong.ymlのファイルを指定 |

```Dockerfile:Dockerfile
...

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

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
ENV KONG_UNTRUSTED_LUA on
ENV KONG_PLUGINS bundled,restriction-check

COPY plugins/restriction-check /usr/local/share/lua/5.1/kong/plugins/restriction-check 
```

## プラグインの使用

### kongおよびwiremockの設定

まずはkongとwiremockを起動するための`docker-compose.yml`を作成します。

```docker-compose.yml
version: "3.9"
services:
  kong:
    build: .
    container_name: kong
    command: >
      /bin/bash -c "
      kong start"
    environment:
      - KONG_LOG_LEVEL=debug
    restart: always
    ports:
      - 8000:8000
  wiremock:
    image: rodolpheche/wiremock
    container_name: mock
    ports:
      - "8080:8080"
    volumes:
      - ./mock.json:/home/wiremock/mappings/mock.json
    restart: always
```

規制状態を取得するAPIをここではwiremockで作成します。APIのパスは`/restriction`としています。
`mock.json`を作成し、そこにモックレスポンスを記載します。

```mock.json
{
  "request": {
    "url": "/restriction",
    "method": "GET"
  },
  "response": {
    "status": 200,
    "body": "{\"restriction\": true}",
    "headers": {
      "Content-Type": "application/json"
    }
  }
}
```

詳細は[wiremock docker](https://wiremock.org/docs/standalone/docker/)を参照してください。

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

kong.ymlの`/httpbin/post`にrestriction-checkを有効化します。
`schema.yml`で設定したfieldを指定することができます。

以下のように設定することで`http://httpbin.org/post`にリクエストが行く前に、`http://host.docker.internal:8080/restriction`にアクセスし、レスポンスボディの`restriction`の値がtrueの場合は503を返す動きとなります。

```kong.yml
- name: example_service
  url: http://httpbin.org/post
  routes:
   - name: example_route
     methods:
      - POST
     paths:
      - /httpbin/post
  plugins:
   - name: restriction-check
     config:
       target_url: http://host.docker.internal:8080/restriction
       decide_key: restriction
```

### コンテナの起動

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

```sh
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

>>> 
HTTP/1.1 503 Service Temporarily Unavailable
...

{"message":"This API is under maintenance."}
```

503が返ってくることを確認できます。

続いて、規制状態を外してみます。以下のように`mock.json`を書き換えて、コンテナを起動し直します。

```mock.json
{
  ...
  "response": {
    "status": 200,
    "body": "{\"restriction\": false}",
    ...
  }
}
```

コンテナを起動し直し、再度リクエストを送ります。

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

>>>
HTTP/1.1 200 OK
...

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

200で返ってきていることを確認できます。
このようにプラグインからAPIアクセスすることが可能です。