kong-client-rubyを使用してKongに設定を適用する
はじめに
kong-client-rubyのgemを使用してKong OSS版へServiceなどを登録する際に、いろいろ詰まってしまいました。
そのため、kong-client-rubyの使用方法、使用時の問題と対応について記載します。
kong-client-rubyについて
オリジナルのkong-client-rubyは以下になります。
ただし、本記事執筆時点(2/6)で確認したところ、このgemは2019年でメンテナンスが止まっているらしく、Kongの最新バージョンに追従できていないようです。おそらくKongの1.1に未対応と思われます。
このKong 1.1に対応したgemが、以下フォーク先のリポジトリのkong1dot1ブランチとなっています。
また、フォーク先の作者曰く、このブランチはKong 2系にも対応しているようです。
My fork works with Kong 2.x
本記事では、このmtmail/kong-client-rubyリポジトリのkong1dot1ブランチを使用して、Kongへの登録処理を行います。
なお、このkong1dot1ブランチを使用しても、Kongの最新バージョン(2.7)に未対応の部分があるため、都度自作パッチをあてて対応します。
最初に結論
この項目に記載している構成、コードを使用し、docker compose起動、rubyファイルの実行によって、kong-client-rubyを使用してKongに登録が可能となります。
今回Kongに登録するものは、以下ConsumerからTargetまでの6つとなります。
- Consumer
- Service
- Route
- Plugin
- Upstream
- Target
使用バージョン
Ruby: 2.7.5
Kong: 2.7.0
Docker Desktop: 4.4.2
docker compose: 3.9
構成
.
├── POSTGRES_PASSWORD
├── client
│ ├── Dockerfile
│ ├── Gemfile
│ ├── kong2_compatible.rb
│ └── main.rb
├── config
│ └── kong.yaml
└── docker-compose.yml
本記事では、docker composeを使用し、Kong、Postgresql、kong-client-rubyを使用するclientサービスを立ち上げます。
使用するdocker-compose.yml、POSTGRES_PASSWORD(Postgresqlへの接続のパスワード)、configディレクトリに関しては、公式がテンプレートを提供しているので、そのテンプレートを使用します。
clientディレクトリ配下の各ファイルの中身は以下のとおりです。
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'kong', git: 'https://github.com/mtmail/kong-client-ruby', branch: 'kong1dot1'
# kong-client-rubyのパッチ
module Kong
class Plugin
def create
super
end
end
class Target
def initialize(attributes = {})
super(attributes)
end
def use_upstream_end_point
if self.attributes['upstream'] && self.attributes['upstream'][:id]
self.api_end_point = "/upstreams/#{self.attributes['upstream'][:id]}#{self.class::API_END_POINT}"
end
end
end
end
require 'kong'
require_relative './kong2_compatible.rb'
Kong::Client.api_url = 'http://kong:8001'
# Kongに登録されているサービスなどを全て削除
Kong::Upstream.all.each(&:delete)
Kong::Plugin.all.each(&:delete)
Kong::Route.all.each(&:delete)
Kong::Service.all.each(&:delete)
Kong::Consumer.all.each(&:delete)
# Consumer登録
consumer = Kong::Consumer.new({ username: 'test-user' })
consumer.create
# Service登録
service = Kong::Service.new(
{
name: 'example_service',
protocol: 'https',
host: 'mockbin.org',
path: '/request'
}
)
service.create
# Route登録
route = Kong::Route.new(
{
name: 'mocking',
service: {
id: service.id
},
paths: ['/mock'],
methods: ['GET']
}
)
route.create
# Rate LimitingのPlugin登録
plugin = Kong::Plugin.new(
{
service: {
id: service.id
},
consumer: {
id: consumer.id
},
name: "rate-limiting",
config: {
minute: 20,
hour: 500
}
}
)
plugin.create
# Upstream登録
upstream = Kong::Upstream.new({ name: 'upstream-service'})
upstream.create
# Upstreamに紐づくTarget登録
mockbin_target = Kong::Target.new( {
upstream: {
id: upstream.id
},
target: 'mockbin.org:443'
})
mockbin_target.create
httpbin_target = Kong::Target.new( {
upstream: {
id: upstream.id
},
target: 'httpbin.org:443'
})
httpbin_target.create
# Upstreamを既存のServiceに紐付けて更新
service2 = Kong::Service.new(
{
id: service.id,
host: 'upstream-service'
}
)
service2.update
FROM ruby:2.7.5-alpine
RUN apk add git
RUN gem install bundle
WORKDIR /app
COPY Gemfile .
RUN bundle config path vendor/bundle && bundle install
COPY . .
CMD ["sleep", "infinity"]
version: '3.9'
x-kong-config: &kong-env
KONG_DATABASE: ${KONG_DATABASE:-postgres}
KONG_PG_DATABASE: ${KONG_PG_DATABASE:-kong}
KONG_PG_HOST: db
KONG_PG_USER: ${KONG_PG_USER:-kong}
KONG_PG_PASSWORD_FILE: /run/secrets/kong_postgres_password
volumes:
kong_data: {}
kong_prefix_vol:
driver_opts:
type: tmpfs
device: tmpfs
kong_tmp_vol:
driver_opts:
type: tmpfs
device: tmpfs
networks:
kong-net:
external: false
services:
kong-migrations:
image: "${KONG_DOCKER_TAG:-kong:latest}"
command: kong migrations bootstrap
profiles: ["database"]
depends_on:
- db
environment:
<<: *kong-env
secrets:
- kong_postgres_password
networks:
- kong-net
restart: on-failure
kong-migrations-up:
image: "${KONG_DOCKER_TAG:-kong:latest}"
command: kong migrations up && kong migrations finish
profiles: ["database"]
depends_on:
- db
environment:
<<: *kong-env
secrets:
- kong_postgres_password
networks:
- kong-net
restart: on-failure
kong:
platform: linux/arm64
image: "${KONG_DOCKER_TAG:-kong:latest}"
user: "${KONG_USER:-kong}"
environment:
<<: *kong-env
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_PROXY_LISTEN: "${KONG_PROXY_LISTEN:-0.0.0.0:8000}"
KONG_ADMIN_LISTEN: "${KONG_ADMIN_LISTEN:-0.0.0.0:8001}"
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_PREFIX: ${KONG_PREFIX:-/var/run/kong}
KONG_DECLARATIVE_CONFIG: "/opt/kong/kong.yaml"
secrets:
- kong_postgres_password
networks:
- kong-net
ports:
# The following two environment variables default to an insecure value (0.0.0.0)
# according to the CIS Security test.
- "${KONG_INBOUND_PROXY_LISTEN:-0.0.0.0}:8000:8000/tcp"
- "${KONG_INBOUND_SSL_PROXY_LISTEN:-0.0.0.0}:8443:8443/tcp"
# Making them mandatory but undefined, like so would be backwards-breaking:
# - "${KONG_INBOUND_PROXY_LISTEN?Missing inbound proxy host}:8000:8000/tcp"
# - "${KONG_INBOUND_SSL_PROXY_LISTEN?Missing inbound proxy ssl host}:8443:8443/tcp"
# Alternative is deactivating check 5.13 in the security bench, if we consider Kong's own config to be enough security here
- "127.0.0.1:8001:8001/tcp"
- "127.0.0.1:8444:8444/tcp"
healthcheck:
test: ["CMD", "kong", "health"]
interval: 10s
timeout: 10s
retries: 10
restart: on-failure:5
read_only: true
volumes:
- kong_prefix_vol:${KONG_PREFIX:-/var/run/kong}
- kong_tmp_vol:/tmp
- ./config:/opt/kong
security_opt:
- no-new-privileges
db:
image: postgres:9.5
profiles: ["database"]
environment:
POSTGRES_DB: ${KONG_PG_DATABASE:-kong}
POSTGRES_USER: ${KONG_PG_USER:-kong}
POSTGRES_PASSWORD_FILE: /run/secrets/kong_postgres_password
secrets:
- kong_postgres_password
healthcheck:
test: ["CMD", "pg_isready", "-U", "${KONG_PG_USER:-kong}"]
interval: 30s
timeout: 30s
retries: 3
restart: on-failure
stdin_open: true
tty: true
networks:
- kong-net
volumes:
- kong_data:/var/lib/postgresql/data
client:
build: ./client
volumes:
- ./client:/app
networks:
- kong-net
secrets:
kong_postgres_password:
file: ./POSTGRES_PASSWORD
docker compose起動、Kongへの登録
上記のファイルを使用し、まずPostgresqlを起動します。
> docker compose up db
次に、kong-migrationsサービスを起動しmigrationを実行させます。
> docker compose up kong-migrations
これでmigrationが完了するので、docker compose up
でサービスを起動させます。
> docker compose up
最後にclientサービスに入り、main.rbを実行することで、Kongへの登録が完了します。
> docker compose exec client /bin/sh
> bundle exec ruby main.rb
以降は、各登録時に詰まるポイントと対応について記載します。
Consumer登録
Consumer登録では、kong-client-rubyののREADMEの手順と同じく、以下コードを使用することで登録可能となります。
consumer = Kong::Consumer.new({ username: 'test-user' })
consumer.create
Service登録
こちらもConsumer登録と同じく、READMEの手順と同じ方法で登録できます。
service = Kong::Service.new(
{
name: 'example_service',
protocol: 'https',
host: 'mockbin.org',
path: '/request'
}
)
service.create
Route登録
Route登録では、READMEとコードが少し異なります。
READMEでは以下のようにservice_id
を使用するようになっています。
route = Kong::Route.new({
name: 'Mockbin',
service_id: '5fd1z584-1adb-40a5-c042-63b19db49x21',
uris: ['/someservice'],
methods: ['GET'],
strip_path: false,
preserve_host: false
})
ですがKong 2.7.0に対してこのコードを実行すると、Kongのスキーマにservice_id
がなくunknown fieldとなり、登録ができません。
/app/vendor/bundle/ruby/2.7.0/bundler/gems/kong-client-ruby-19764e083d27/lib/kong/client.rb:229:in `handle_error_response': {"fields":{"service_id":"unknown field"},"name":"schema violation","message":"schema violation (service_id: unknown field)","code":2} (Kong::Error)
Kongのコードを詳しく見たわけではありませんが、serviceのidをKongへ渡す際には、リクエストのボディをservice: {id: xxx}
のjson形式にしないといけないようです。
json形式への変換はkong-client-ruby側で行ってくれるので、client側はハッシュ形式でkong-clent-ruby側に渡す必要があります。
そのため、以下のコードにして実行するとRouteが登録されます。
route = Kong::Route.new(
{
name: 'mocking',
service: {
id: service.id
},
paths: ['/mock'],
methods: ['GET']
}
)
route.create
Plugin登録
特定のConsumerの特定のServiceに対し、Rate Limitingを設定するとします。
READMEでは、以下のようにコード例が記載されています。
plugin = Kong::Plugin.new({
service_id: '5fd1z584-1adb-40a5-c042-63b19db49x21',
consumer_id: 'a3dX2dh2-1adb-40a5-c042-63b19dbx83hF4',
name: 'rate-limiting',
config: {
minute: 20,
hour: 500
}
})
ですが、このコードのまま実行してもエラーになってしまいます。
これは以下2つの問題があるためです。
- 1つ目: Route登録と同様に、
service_id
、consumer_id
がKongになくschema violationとなってしまう。 - 2つ目: 1つ目の問題を対応して
create
しても、schema violationが発生して登録できない。
1つ目に関しては、Route登録の対応と同じように、service: { id: xxx}
、consumer: {id: xxx}
とすることで解決できます。
2つ目に関して、schema violationのエラーは以下のようなエラーとなります。
/app/vendor/bundle/ruby/2.7.0/bundler/gems/kong-client-ruby-19764e083d27/lib/kong/client.rb:229:in `handle_error_response': {"code":2,"name":"schema violation","fields":{"config.hour":"unknown field","config.minute":"unknown field","@entity":["at least one of these fields must be non-empty: 'config.second', 'config.minute', 'config.hour', 'config.day', 'config.month', 'config.year'"]},"message":"3 schema violations (at least one of these fields must be non-empty: 'config.second', 'config.minute', 'config.hour', 'config.day', 'config.month', 'config.year'; config.hour: unknown field; config.minute: unknown field)"} (Kong::Error)
エラーにある通り、config.hour
やconfig.minute
はKongのスキーマになく、unknown fieldとなります。
この理由は、Pluginクラスのcreate
時に、Plugin.new
で与えた引数を修正してリクエストのボディに使用しているためです。
module Kong
class Plugin
include Base
include BelongsToService
・・・略・・・
def create
flatten_config
super
end
・・・略・・・
private
def flatten_config
if attributes['config']
attributes.merge!(Util.flatten(attributes.delete('config'), 'config'))
end
end
end
end
module Kong
module Util
def self.flatten(cursor, parent_key = nil, memo = {})
memo.tap do
case cursor
when Hash
cursor.keys.each do |key|
flatten(cursor[key], [parent_key, key].compact.join('.'), memo)
end
else
memo["#{parent_key}"] = cursor
end
end
end
end
end
plugin.rbで使用されているattributes
には、Plugin.new
で与えた引数のキーの文字列と値が入っています。
Plugin
クラスのcreate
メソッドを実行すると、attributes
のキーがflatten_config
メソッドによって、ドットで結合された形となります。
よって、以下のようなconfig
が引数で渡された場合、attributes
にはconfig.minutes: 20
, config.hour: 500
がセットされます。
config: {
minute: 20,
hour: 500
}
このattribute
をリクエストのボディに渡してリクエストするとエラーになるため、Routeの登録と同じように、config: {minutes: xxx}
のようにリクエストのボディにセットする必要があるようです。
そのため、Plugin
クラスのflatten_config
メソッドを実行させなければ、想定通りのボディの形になるので、Plugin
クラスのcreate
メソッドをオーバーライドします。
module Kong
class Plugin
def create
super
end
end
・・・略・・・
このオーバーライドにより、Pluginを登録できるようになります。
※余談:config.hour
をボディで渡しているのに、config.hour
が無いとエラーが返ってくるのは結構謎でした。
Upstream, Target登録
Upstreamの登録、Upstremに紐づくTargetの登録、ServiceにTargetを紐付けて更新を行います。
Targetでは以下2つのAPIを登録します。
- ドメイン
- mockbin.org
- httpbin.org
※ポートは443を使用
またServiceは前述で作成したServiceを使用するため、ここではServiceの登録は省略します。
Upstreamの登録やTargetの登録についても、READMEと同じ手順を行ってもエラーとなり登録ができません。
これは以下3つの問題のためです。
- 1つ目: Route登録と同じように、
upstream_id
を使用するとschema violationになる。 - 2つ目:
Target.new
時、引数にupstream_id
がセットされていない場合、upstream_id
未使用の例外が発生する。 - 3つ目:
Target.new
時、引数にupstream_id
がセットされていない場合、create
メソッドを実行すると、Target登録用のエンドポイントへリクエストされない。http://kong:8001 へリクエストがされてしまう。
1つ目はRoute登録の対応と同じように、upstream: {id: xxx}
の形式で引数に渡すことで解決できます。
2つ目については、Target
クラスの初期化時に、upstream_id
がないため例外が投げられるためです。
module Kong
class Target
include Base
・・・略・・・
def initialize(attributes = {})
super(attributes)
raise ArgumentError, 'You must specify an upstream_id' unless self.upstream_id
end
・・・略・・・
1つ目の問題を対応してupstream: {id: xxx}
を使用する場合、例外が投げられないようにする必要があります。
そのため以下のようにオーバーライドして、初期化時に例外を投げないようにします。
module Kong
・・・略・・・
class Target
def initialize(attributes = {})
super(attributes)
end
・・・略・・・
end
end
3つ目については、Target登録のエンドポイント作成が、upstream_id
を使用した作成になっているためです。
Target.new
時、以下のようにBase
クラスのinitialize
メソッドが実行されます。
module Kong
module Base
・・・略・・・
##
# @param [Hash] attributes
def initialize(attributes = {})
init_api_end_point
init_attributes(attributes)
end
・・・略・・・
private
def init_attributes(attributes)
@attributes = {}
attributes.each do |key, value|
@attributes[key.to_s] = value
end
use_consumer_end_point if respond_to?(:use_consumer_end_point)
use_api_end_point if respond_to?(:use_api_end_point)
use_service_end_point if respond_to?(:use_service_end_point)
use_upstream_end_point if respond_to?(:use_upstream_end_point)
end
end
end
module Kong
class Target
include Base
ATTRIBUTE_NAMES = %w(id upstream_id target weight).freeze
API_END_POINT = '/targets/'.freeze
・・・略・・・
def use_upstream_end_point
self.api_end_point = "/upstreams/#{self.upstream_id}#{self.class::API_END_POINT}" if self.upstream_id
end
・・・略・・・
initialize
メソッドではinit_api_end_point
メソッドを呼び出します。
このメソッドで、エンドポイントを作成するuse_upstream_end_point
メソッドが子クラスにあれば、そのメソッドを呼び出します。
Target
クラスでは、use_upstream_end_point
メソッドを定義しているため、そのメソッドが呼び出されます。
このuse_upstream_end_point
メソッドがupstream_id
を使用したエンドポイント作成となっているので、use_upstream_end_point
メソッドをオーバーライドして、upstream: {id: xxx}
を使用したエンドポイント作成に修正します。
修正後のコードは以下の通りです。
module Kong
・・・略・・・
class Target
・・・略・・・
def use_upstream_end_point
self.api_end_point = "/upstreams/#{self.attributes['upstream'][:id]}#{self.class::API_END_POINT}" if self.attributes['upstream'] && self.attributes['upstream'][:id]
end
end
end
※補足:attributes['upstream'][:id]
となっている点について
ruby-kong-clientでは、Target
クラス初期化時にincludeしているBase
クラスで、init_attributes
メソッドを呼び出します。
そのメソッド内部で、初期化時に与えた引数(attributes
)から、キーの文字列と値を@attributes
にセットしています。
def init_attributes(attributes)
@attributes = {}
attributes.each do |key, value|
@attributes[key.to_s] = value
end
use_consumer_end_point if respond_to?(:use_consumer_end_point)
use_api_end_point if respond_to?(:use_api_end_point)
use_service_end_point if respond_to?(:use_service_end_point)
use_upstream_end_point if respond_to?(:use_upstream_end_point)
end
そのためupstream: {id: xxx}
を初期化時の引数で与えると、attributes['upstream'][:id]
の形式で保存されるためです。
おわりに
kong-client-rubyを使用したKongへの登録について記載しました。
gemのメンテナンスが止まっているようなので、自分でできる範囲で対応できることをやっていきたいと考えています。
この記事が誰かのお役に立てれば幸いです。
Discussion