🦍

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

2023/11/21に公開

はじめに

OSSのKongは世界で最も人気のあるAPI Gatewayで、認証認可や流量制御などプラグインを有効化するだけで用意することができます。

プラグインはこちらのPlugin Hubにリストアップされており、必要なものを選んで導入することができますが、必要な機能がない場合、Luaという言語でカスタムプラグインを作成することも可能です

本記事ではカスタムプラグイン内部でAPIアクセスを行い、その結果を踏まえてUpstreamへリクエストを行うサンプルプラグインを作成します。
記事のサンプルコードはこちらになります。

また、OSSのAPI Gateway「Kong」の日本コミュニティ Kong Community, Japan として活動も行っています。ご興味のある方はコミュニティサイトをご覧ください。

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.luaschema.luaになります。

  1. handler.lua

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

  2. schema.lua

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

handler.luaの作成

plugins/restriction-checkというディレクトリを作成し、handler.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
    • httpクライアントとして使用します。
    • Dockerfileでビルドする際にインストールします。
  • cjson
    • jsonのエンコード/デコードで使用します。
    • kongのイメージに元から入っているのでインストール不要です。

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

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

Priorityはプラグインが実行される順番に寄与し、値が大きいほどフェーズ内(Access、Body_filterなど)において先に実行されます。
Plugin Ordering Reference

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


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

function RestrictionCheck:access(conf)
  ...
end

kongの処理のライフサイクルはこちらのようになっており、リクエストがkongに来た時の処理はaccessフェーズとなるため、その時の処理を記述します。

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


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

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を確認してください。

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

加えて、kong.response.exitでkongでレスポンスを返すことができます(公式ドキュメント
ここでは、規制状態取得APIへのリクエストでエラーが起きた時、および規制状態がtrueだったときにそれぞれエラーレスポンスを返す実装としています。

schema.luaの作成

plugins/restriction-checkschema.luaというファイルを作成します。
handler.luaで用いるtarget_urldecide_keyを定義します。

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)

Dockerfileの作成

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

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

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

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
...

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を作成します。

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を作成し、そこにモックレスポンスを記載します。

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

詳細はwiremock dockerを参照してください。

routeへのプラグインの追加

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

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

- 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

コンテナの起動

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

docker-compose up -d --build

動作確認

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

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を書き換えて、コンテナを起動し直します。

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

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

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アクセスすることが可能です。

Discussion