Kong插件开发向导
简介
在进一步讨论之前,这里先有必要简要阐述一下 Kong 是如何构建的,特别是它如何与Nginx集成,以及它与Lua脚本之间的关系
使用 lua-nginx-module 模块可以在 Nginx 中启用 Lua 脚本功能,Kong 与 OpenResty 一起发布,OpenResty 中已经包含了 lua-nginx-module 模块,OpenResty 不是 Nginx 的分支,而是一组扩展 Nginx 功能的模块
因此,Kong 是一个 Lua 应用程序,旨在加载和执行 Lua 模块(我们通常称之为“插件”),并且Kong还为此提供了整套开发环境,包括 SDK,数据库抽象、数据迁移等等
插件由 Lua 模块组成,用户可以使用插件开发包(又称PDK),通过调用请求响应或者流交互实现各种功能 ,PDK 是一组 Lua 方法,插件可以使用它来促进 Kong 核心模块(或其他组件)与插件本身的交互
这篇向导将详细描述插件的结构,它们的扩展点,以及如何发布和安装它们,有关PDK的详情,请查阅插件开发工具包向导
文件结构
简介
插件其实是一组 Lua 模块,本章中描述的每个文件都可以视为一个单独的模块,如果它们的命名遵循某个约定,Kong 就会检测并加载插件模块:
kong.plugins.<plugin_name>.<module_name>
用户定义的插件模块需要通过 package.path 变量访问到,用户可以更改 lua_package_path 配置调整这个值,然而,安装插件的首选方法是通过 LuaRocks,它与 Kong 天然集成,有关 LuaRocks 安装插件的详情,请参考后面的章节
为了让 Kong 意识到哪些插件需要安装,用户必须将它们添加到配置文件中的 plugins 属性中,格式是以逗号分隔的列表,例如:
plugins = bundled,my-custom-plugin # your plugin name here
或者,用户不想加载任何预捆绑的插件:
plugins = my-custom-plugin # your plugin name here
现在,Kong会试图从下列命名空间中加载Lua模块
kong.plugins.my-custom-plugin.<module_name>
其中一些模块是必需的(例如 handler.lua),有些是可选的,以允许插件实现一些额外的功能(例如 api.lua 可以扩展 Admin API 端点)
现在我们将详细描述用户可以实现的模块以及它们的用途
基础插件模块
最基础的插件,必须包含两个模块:
simple-plugin
├── handler.lua
└── schema.lua
- handler.lua:插件的核心,它是一个需要实现的接口,其中每个方法会在请求/连接的生命周期中运行
- schema.lua:插件可能需要保留一些用户输入的配置,此模块定义一些规则保存配置的模式,以便用户只能输入有效的配置项
高级插件模块
有些插件与 Kong 之间有更深入地集成,比如在数据库中存数据,在 Admin API 中公开端点等等,每个插件都可以通过向插件添加新模块来完成,插件的结构大致如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── init.lua
│ └── 000_base_complete_plugin.lua
└── schema.lua
以下是完整的模块列表,以及其简要说明:
模块名 | 是否必须 | 描述 |
---|---|---|
api.lua | 否 | 定义 Admin API 中也用的端点列表,与插件自定义的实体进行交互 |
daos.lua | 否 | 定义数据库访问对象列表 |
handler.lua | 是 | 一个需要实现的接口,其中每个方法会在请求/连接的生命周期中运行 |
migrations/*.lua | 否 | 数据源迁移,只有当用户的插件有自定义实体时才需要 |
schema.lua | 是 | 保存插件的配置项,以便用户只能输入有效的配置值 |
Key-Auth 插件实现了整套完整的插件接口,可以查看源码了解细节
实现自定义逻辑
简介
Kong 的插件允许用户在整个生命周期的几个切点加入自定义逻辑,为此,必须实现 base_plugin.lua
接口中的一些方法,这些方法在 kong.plugins.<plugin_name>.handler
模块中实现
模块
kong.plugins.<plugin_name>.handler
可用的上下文
插件接口允许用户覆盖 handler.lua 文件中的以下任何方法,在Kong的执行生命周期的各个切点实现自定义逻辑:
- HTTP Module:为 HTTP / HTTPS 请求编写的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() |
init_worker | 每个 Nginx worker 进程启动时执行 |
:certificate() |
ssl_certificate | 在 SSL 握手提供证书时执行 |
:rewrite() |
rewrite | 从客户端接收到请求,进入 rewrite 段执行,注意,在这个阶段没有识别服务,也没有消费者介入,只有配置成全局插件才会执行此处理程序 |
:access() |
access | 从客户端接收到请求到被代理到 upstream service 之前执行 |
:header_filter() |
header_filter | 从 upstream service 接收到所有响应头时执行 |
:body_filter() |
body_filter | 针对从 upstream service 接收到的响应体块执行,由于响应以流的形式返回给客户端,超过缓冲区大小的按块进行传输,因此,如果响应体很大,会多次调用这个方法 |
:log() |
log | 最后一个响应字节发送到客户端时执行 |
- Stream Module:为 TCP 流连接编写的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() |
init_worker | 每个 Nginx worker 进程启动时执行 |
:preread() |
preread | 每个连接执行一次 |
:log() |
log | 每个连接中断执行一次 |
除了 :init_worker()
方法,每个方法都会携带一个参数,这个参数由Kong给出,即插件的配置,这个参数的类型是 Lua table,包含了用户定义的值,格式根据用户定义的插件 schema 格式
handler.lua 格式
handler.lua
文件需要返回一个 table,里面包含了用户希望执行的方法,为了方便起见,这里有一个示例模块,实现了模块中所有的可用方法:
-- Extending the Base Plugin handler is optional, as there is no real
-- concept of interface in Lua, but the Base Plugin handler's methods
-- can be called from your child implementation and will print logs
-- in your `error.log` file (where all logs are printed).
local BasePlugin = require "kong.plugins.base_plugin"
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 10
-- Your plugin handler's constructor. If you are extending the
-- Base Plugin handler, it's only role is to instantiate itself
-- with a name. The name is your plugin name as it will be printed in the logs.
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:init_worker()
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.init_worker(self)
-- Implement any custom logic here
end
function CustomHandler:preread(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.preread(self)
-- Implement any custom logic here
end
function CustomHandler:certificate(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.certificate(self)
-- Implement any custom logic here
end
function CustomHandler:rewrite(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.rewrite(self)
-- Implement any custom logic here
end
function CustomHandler:access(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.access(self)
-- Implement any custom logic here
end
function CustomHandler:header_filter(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.header_filter(self)
-- Implement any custom logic here
end
function CustomHandler:body_filter(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.body_filter(self)
-- Implement any custom logic here
end
function CustomHandler:log(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.log(self)
-- Implement any custom logic here
end
-- This module needs to return the created table, so that Kong
-- can execute those functions.
return CustomHandler
插件本身的逻辑可以写在另一个模块,然后在处理程序模块中调用:
local BasePlugin = require "kong.plugins.base_plugin"
-- The actual logic is implemented in those modules
local access = require "kong.plugins.my-custom-plugin.access"
local body_filter = require "kong.plugins.my-custom-plugin.body_filter"
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 10
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:access(config)
CustomHandler.super.access(self)
-- Execute any function from the module loaded in `access`,
-- for example, `execute()` and passing it the plugin's configuration.
access.execute(config)
end
function CustomHandler:body_filter(config)
CustomHandler.super.body_filter(self)
-- Execute any function from the module loaded in `body_filter`,
-- for example, `execute()` and passing it the plugin's configuration.
body_filter.execute(config)
end
return CustomHandler
插件开发工具包
在插件开发过程中,需要与请求/响应对象或其他核心组件交互,Kong 为此提供了一个插件开发工具包,插件可以使用里面的函数和变量来执行各种网关操作,并且插件开发工具包是向前兼容的
如果用户尝试实现一些与 Kong 交互的逻辑时(例如检索请求头、生成响应、记录错误或调试信息),可以参考插件开发工具包
插件执行顺序
某些插件可能依赖其他插件来执行某些操作,例如,依赖消费者身份的插件必须在身份验证插件之后运行,考虑到这一点,Kong在插件执行期间定义了优先级,以确保插件执行顺序,用户可以定义这个属性值来配置插件优先级:
CustomHandler.PRIORITY = 10
优先级越高,插件执行的越早(例如 :access()
、:log()
方法),预绑定插件的优先级如下:
插件 | 优先级 |
---|---|
pre-function | +inf |
zipkin | 100000 |
ip-restriction | 3000 |
bot-detection | 2500 |
cors | 2000 |
session | 1900 |
kubernetes-sidecar-injector | 1006 |
jwt | 1005 |
oauth2 | 1004 |
key-auth | 1003 |
ldap-auth | 1002 |
basic-auth | 1001 |
hmac-auth | 1000 |
request-size-limiting | 951 |
acl | 950 |
rate-limiting | 901 |
response-ratelimiting | 900 |
request-transformer | 801 |
response-transformer | 800 |
aws-lambda | 750 |
azure-functions | 749 |
prometheus | 13 |
http-log | 12 |
statsd | 11 |
datadog | 10 |
file-log | 9 |
udp-log | 8 |
tcp-log | 7 |
loggly | 6 |
syslog | 4 |
request-termination | 2 |
correlation-id | 1 |
post-function | -1000 |
插件配置
简介
大多数情况下,插件的配置可以满足用户的需求,插件的配置存储在数据库中,当插件运行时,Kong在数据库中检索出它们,并将其传递给 handler.lua 方法
配置在 Kong 中由 Lua table 组成,我们称之为 schema,用户通过 Admin API 启用插件时,以键值对的形式输入参数,Kong提供了验证用户插件配置的方法,当用户向 Admin API 发送请求启用或更新给定 Service、Route 或 Consumer 上的插件时,Kong 会根据用户定义的 schema 来验证插件配置,举例,用户执行如下请求:
curl -X POST http://kong:8001/services/<service-name-or-id>/plugins -d "name=my-custom-plugin" -d "config.foo=bar"
如果配置对象的所有属性都验证有效,API 会返回 201 Created
,插件将和配置一起存储在数据库中:
{
foo = "bar"
}
如果配置验证不通过,API 会返回 400 Bad Request
和错误信息
模块
kong.plugins.<plugin_name>.schema
schema.lua 格式
这个模块返回一个 Lua table,其中包含了用户可以配置插件哪些属性,可用的属性包含:
属性名 | 数据类型 | 描述 |
---|---|---|
name |
string | 插件名称,比如 key-auth
|
fields |
table | 字段定义数组 |
entity_checks |
function | 校验条件数组 |
所有插件都默认继承的属性:
属性名 | 数据类型 | 描述 |
---|---|---|
id |
string | 自动生成的插件 Id |
name |
string | 插件名称,比如 key-auth
|
created_at |
number | 插件配置时间 |
route |
table | 绑定的路由 |
service |
table | 绑定的服务 |
consumer |
table | 绑定的消费者 |
run_on |
string | 插件运行在服务网格上的哪个节点 |
protocols |
table | 插件运行的协议 |
enabled |
boolean | 插件是否生效 |
tags |
table | 插件的标签 |
大多数情况下,用户可以使用默认值,或者让用户在启用插件时指定值,以下是一份示例 schema.lua
文件:
local typedefs = require "kong.db.schema.typedefs"
return {
name = "<plugin-name>",
fields = {
{
-- this plugin will only be applied to Services or Routes
consumer = typedefs.no_consumer
},
{
-- this plugin will only be executed on the first Kong node
-- if a request comes from a service mesh (when acting as
-- a non-service mesh gateway, the nodes are always considered
-- to be "first".
run_on = typedefs.run_on_first
},
{
-- this plugin will only run within Nginx HTTP module
protocols = typedefs.protocols_http
},
{
config = {
type = "record",
fields = {
-- Describe your plugin's configuration's schema here.
},
},
},
},
entity_checks = {
-- Describe your plugin's entity validation rules
},
}
描述配置 schema
schema.lua
文件中的 config.fields
属性描述了插件配置的 schema,例如:
{
name = "<plugin-name>",
fields = {
config = {
type = "record",
fields = {
{
some_string = {
type = "string",
required = false,
},
},
{
some_boolean = {
type = "boolean",
default = false,
},
},
{
some_array = {
type = "array",
elements = {
type = "string",
one_of = {
"GET",
"POST",
"PUT",
"DELETE",
},
},
},
},
},
},
},
}
这里罗列了一些常用的属性规则:
规则 | 描述 |
---|---|
type |
属性的类型 |
required |
属性是否是必须的 |
default |
属性的默认值 |
elements |
array 或 set 格式的元素类型 |
keys |
map 格式的 key 元素类型 |
values |
map 格式的 value 元素类型 |
fields |
record 格式的元素类型 |
另外还有一些:
规则 | 描述 |
---|---|
between |
校验输入是否在约定的范围之内 |
eq |
校验输入是否等于约定值 |
ne |
校验输入是否不等于约定值 |
gt |
校验输入是否大于约定值 |
len_eq |
校验输入字符串长度是否等于约定值 |
len_min |
校验输入字符串长度是否大于约定值 |
len_max |
校验输入字符串长度是否小于约定值 |
match |
校验输入字符串是否匹配约定正则表达式 |
not_match |
校验输入字符串是否不匹配约定正则表达式 |
match_all |
校验输入字符串是否全部匹配约定正则表达式列表 |
match_none |
校验输入字符串是否全部不匹配约定正则表达式列表 |
match_any |
校验输入字符串是否匹配约定正则表达式列表中的一个 |
starts_with |
校验输入字符串是否以约定值开头 |
one_of |
校验输入字符串是否是约定值列表中的一个 |
contains |
校验输入字符串列表是否包含约定值 |
is_regex |
校验输入字符串是否是合法的正则表达式 |
custom_validator |
校验输入是否是标准的 Lua 方法 |
例子
这是 key-auth 插件的 schema.lua
文件:
-- schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "key-auth",
fields = {
{
consumer = typedefs.no_consumer
},
{
run_on = typedefs.run_on_first
},
{
protocols = typedefs.protocols_http
},
{
config = {
type = "record",
fields = {
{
key_names = {
type = "array",
required = true,
elements = typedefs.header_name,
default = {
"apikey",
},
},
},
{
hide_credentials = {
type = "boolean",
default = false,
},
},
{
anonymous = {
type = "string",
uuid = true,
legacy = true,
},
},
{
key_in_body = {
type = "boolean",
default = false,
},
},
{
run_on_preflight = {
type = "boolean",
default = true,
},
},
},
},
},
},
}
访问数据库
简介
Kong 通过 Dao 层与数据层交互,本章将详细介绍与数据层交互的的 API,Kong支持两类数据库:Cassandra 3.x.x 和 PostgreSQL 9.5+
kong.db
Kong 中所有的实体可以表现为:
- 一份描述与实体相关联的表结构,表结构中有对字段的约束,如外键,非控约束等,这个 schema 是在插件配置章节中描述的
- 一份 Dao 层实体,与现正使用的数据库作映射,Dao 层使用 schema,并暴露公共方法增删改查实体
Kong 中的核心实体包括:服务、路由、消费者和插件,所有这些都可以作为数据访问对象(DAOs),通过 kong.db 全局单例访问:
-- Core DAOs
local services = kong.db.services
local routes = kong.db.routes
local consumers = kong.db.consumers
local plugins = kong.db.plugins
Kong 的核心实体和插件自定义的实体都可以通过 kong.db.* 获取
Dao 层 Lua API
Dao 层负责在操作存储在数据库中的数据,所有底层支持的数据库(目前是 Cassandra 和 Postgres)都遵循相同的接口,这样 Dao 与所有这些数据库都兼容,插入服务和插件非常简单,例如:
local inserted_service, err = kong.db.services:insert({
name = "mockbin",
url = "http://mockbin.org",
})
local inserted_plugin, err = kong.db.plugins:insert({
name = "key-auth",
service = inserted_service,
})
存储自定义实体
简介
虽然并非所有插件都需要它,但有些插件中,用户可能需要在数据库中存储多于其配置的数据,在这种情况下,Kong 会在数据层提供抽象,允许用户存储自定义实体
如上一章节所述,Kong 将与数据层交互的类称为 DAO 层,可以通过使用 DAO Factory 单例访问,本章将描述如何为用户自定义的实体提供抽象
模块
kong.plugins.<plugin_name>.daos
kong.plugins.<plugin_name>.migrations.init
kong.plugins.<plugin_name>.migrations.000_base_<plugin_name>
kong.plugins.<plugin_name>.migrations.001_<from-version>_to_<to_version>
kong.plugins.<plugin_name>.migrations.002_<from-version>_to_<to_version>
创建迁移目录
定义完模型之后,用户必须创建迁移模块,当 Kong 启动时会创建表结构,用来存储实体记录,如果用户的插件需要同时支持 Cassandra 和 Postgres,那需要写两个迁移模块
如果用户的插件还没有这个模块,可以添加一个 <plugin_name>/migrations
目录,然后创建 init.lua
文件,这是引用插件所有迁移信息的地方,初始版本的 migrations/init.lua
文件指向单个迁移,这里,我们称之为 000_base_my_plugin
-- `migrations/init.lua`
return {
"000_base_my_plugin",
}
这意味着 <plugin_name>/migrations/000_base_my_plugin.lua
文件中包含了一份初始迁移文件,用户马上可以看到具体的工作原理
在现有插件上添加迁移
有时需要在发布插件的新版本中引入更改,添加新功能,数据库里的数据也可能需要更改,当发生这种情况时,用户需要创建一个新的迁移文件,发布插件后,用户严禁修改原有的迁移文件,虽然没有严格的规则来命名迁移文件,但有一个约定,即初始的前缀为000
,下一个为前缀为001
,依次类推
继我们之前的示例,现在用户想要发布新版本插件,需要修改数据库(例如,需要一个名为 foo 的表),我们可以添加一个文件加入它,文件名为 <plugin_name>/migrations/001_100_to_110.lua
,并且在初始迁移文件中引入它(其中100是插件的先前版本1.0.0,110是插件现在的版本1.1.0)
-- `<plugin_name>/migrations/init.lua`
return {
"000_base_my_plugin",
"001_100_to_110",
}
迁移文件语法
虽然 Kong 的核心迁移同时支持 Postgres 和 Cassandra,自定义插件可以选择全部支持,或者只支持其中一个,迁移文件是一个 Lua 文件,它返回一个表,结构如下:
-- `<plugin_name>/migrations/000_base_my_plugin.lua`
return {
postgresql = {
up = [[
CREATE TABLE IF NOT EXISTS "my_plugin_table" (
"id" UUID PRIMARY KEY,
"created_at" TIMESTAMP WITHOUT TIME ZONE,
"col1" TEXT
);
DO $$
BEGIN
CREATE INDEX IF NOT EXISTS "my_plugin_table_col1"
ON "my_plugin_table" ("col1");
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]],
},
cassandra = {
up = [[
CREATE TABLE IF NOT EXISTS my_plugin_table (
id uuid PRIMARY KEY,
created_at timestamp,
col1 text
);
CREATE INDEX IF NOT EXISTS ON my_plugin_table (col1);
]],
}
}
-- `<plugin_name>/migrations/001_100_to_110.lua`
return {
postgresql = {
up = [[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" ADD "cache_key" TEXT UNIQUE;
EXCEPTION WHEN DUPLICATE_COLUMN THEN
-- Do nothing, accept existing state
END;
$$;
]],
teardown = function(connector, helpers)
assert(connector:connect_migrations())
assert(connector:query([[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" DROP "col1";
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]])
end,
},
cassandra = {
up = [[
ALTER TABLE my_plugin_table ADD cache_key text;
CREATE INDEX IF NOT EXISTS ON my_plugin_table (cache_key);
]],
teardown = function(connector, helpers)
assert(connector:connect_migrations())
assert(connector:query("ALTER TABLE my_plugin_table DROP col1"))
end,
}
}
如果插件仅支持 Postgres 或 Cassandra 中的一个,策略中只需要写一部分,每个策略包含两段,up
段和 teardown
段:
-
up
:可选的 SQL/CQL 语句,当Kong执行迁移时,执行这些语句 -
teardown
:可选的 Lua 方法,接收一个连接器参数,此类连接器可以调用查询方法执行 SQL/CQL 查询,在Kong迁移完成后执行
建议在 up
段中执行非破坏性操作(例如创建新表、添加新纪录);在 teardown
段中执行破坏性操作(例如删除数据、更改行类型
添加新数据),在编写 SQL/CQL 语句时,推荐可以重复使用,比如使用 DROP TABLE IF EXISTS
,而不是 DROP TABLE
;使用 CREATE INDEX IF NOT EXIST
,而不是 CREATE INDEX
,这样当某个原因导致迁移失败时,只需修复问题,重新运行迁移即可
Postgres 支持 NOT NULL、UNIQUE、FOREIGN KEY 之类的约束,Cassandra 本身并不支持,但是如果在定义模型 schema 时加入此类约束,Kong 就会支持这些功能,所以对于 Postgres 和 Cassandra,这两类模式是相同的,可以完全将 Cassandra 当做纯 SQL 模式使用,请注意:如果在 schema 中使用了 unique
约束,Cassandra 会强制执行,Postgres 需要在迁移中设置此约束
定义 Schema
在自定义插件中使用自定义实体的第一步是定义一个或多个 schema,schema 格式是 Lua 表,其中描述实体的信息,包括实体的不同字段如何命名以及数据类型,与插件描述配置的字段类似,与插件配置 schema 相比,自定义实体 schema 需要额外的元数据(比如实体主键),schema 在该模块中定义:
kong.plugins.<plugin_name>.daos
这意味着插件文件夹中需要有一个名为 <plugin_name>/daos.lua
的文件,daos.lua
文件返回一个表,其中包含了一个或多个 schema,例如:
-- daos.lua
local typedefs = require "kong.db.schema.typedefs"
return {
-- this plugin only results in one custom DAO, named `keyauth_credentials`:
keyauth_credentials = {
name = "keyauth_credentials", -- the actual table in the database
endpoint_key = "key",
primary_key = { "id" },
cache_key = { "key" },
generate_admin_api = true,
fields = {
{
-- a value to be inserted by the DAO itself
-- (think of serial id and the uniqueness of such required here)
id = typedefs.uuid,
},
{
-- also interted by the DAO itself
created_at = typedefs.auto_timestamp_s,
},
{
-- a foreign key to a consumer's id
consumer = {
type = "foreign",
reference = "consumers",
default = ngx.null,
on_delete = "cascade",
},
},
{
-- a unique API key
key = {
type = "string",
required = false,
unique = true,
auto = true,
},
},
},
},
}
daos.lua
示例文件的属性描述如下:
名称 | 类型 | 描述 |
---|---|---|
name | string,必须的 | 用于确定 DAO 的名称(kong.db.[name]) |
primary_key | table,必须的 | 实体主键,大多数情况下 Kong 使用 UUID 的 id 作为主键,也可以使用复合主键 |
endpoint_key | string,可选的 | 在 Admin API 中作为备用标志符的字段,在上面的示例中,endpoint_key 是 key ,这意味着 id = 123 和 key = "foo" 的凭证可以通过 /keyauth_credentials/123 和 /keyauth_credentials/foo 这两条路径获取 |
cache_key | table,可选的 | 生成 cache_key 的字段 |
generate_admin_api | boolean,可选的 | 是否自动生成 Admin API,默认情况下,会为所有 DAOS 生成 Admin API,包括自定义的 DAO,如果要为 DAO 创建完全自定义的 Admin API,或者想要完全禁用自动生成功能,将此选项设置为 false
|
admin_api_name | boolean,可选的 | 启用 generate_admin_api 时,使用 name 属性自动生成 Admin API |
admin_api_nested_name | boolean,可选的 | 类似于 admin_api_name
|
fields | table | 定义了字段属性的描述 |
许多字段属性编码验证规则,在使用 DAO 插入或更新实体时,将检查这些验证,如果输入不符合这些验证,则会返回错误,typedefs
变量(通过 kong.db.schema.typedefs
获得)是一个包含大量实用类型定义和别名的表,包括 typedefs.uuid
(主键的常用类型)和 typedefs.auto_timestamp_s
(用于 created_at
字段),它们在定义字段时被广泛使用,下面是一些字段属性的解释:
属性名 | 类型 | 描述 |
---|---|---|
type | string | 支持以下标量类型:string 、integer 、number 、boolean ;还支持以下复合类型:array 、record 、set ,除此之外,type 还可以取 foreign ,表示外键关系,type 是所有字段必需的属性 |
default | any,与 type 指定的类型保持一致 | 默认值,始终通过 Lua 设置,而不是由底层数据库设置,因此建议不要在迁移中的字段上设置任何默认值 |
required | boolean | 是否必需,如何设置 true,当输入时缺少该字段会抛出错误 |
unique | boolean | 是否唯一,如果设置 ture,当另一个实体存在时会抛出错误,在使用 PostgreSQL 时,必需在迁移中将该字段声明为 UNIQUE ;Cassandra 在插入数据前会检查 Lua,因此不需要任何特殊处理 |
auto | boolean | 是否自动填充,当 type == "uuid" 时,该字段将填充 UUID;当 type == "string" 时,该字段将填充随机字符串;如果字段名为 created_at 或 updated_at ,该字段将在插入/更新时填充当前时间 |
reference | string | 当 type 是 foreign 时是必须的 |
on_delete | string | 当 type 是 foreign 时,定义了外键删除的逻辑,在 Cassandra 中,这是用纯 Lua 代码处理的,但在 PostgreSQL 中,在迁移时要将引用声明为 <font ON DELETE CASCADE/NULL/RESTRICT` |
需要了解表结构的更多信息,可以参考:
- typedefs.lua 的源代码,用于了解默认值
- 核心表结构
- 预绑定插件的 daos.lua 文件
自定义 DAO
schema 不直接和数据库交互,DAO层通过 kong.db
接口与数据库交互
查询实体
local entity, err, err_t = kong.db.<name>:select(primary_key)
在数据库中查询实体并返回,可能会有三种结果:
- 如果找到对应实体,将作为普通 Lua table 返回;
- 发生错误,例如数据连接丢失,第一个参数返回 nil,第二个参数返回描述错误的字符串,最后一个参数返回相同错误,数据格式是 table
- 没有错误,但找不到实体,直接返回 nil;
示例:
local entity, err = kong.db.keyauth_credentials:select({
id = "c77c50d2-5947-4904-9f37-fa36182a71a9"
})
if err then
kong.log.err("Error when inserting keyauth credential: " .. err)
return nil
end
if not entity then
kong.log.err("Could not find credential.")
return nil
end
遍历实体
for entity, err on kong.db.<name>:each(entities_per_page) do
if err then
...
end
...
end
这个方法通过创建分页请求有效地迭代数据库中的所有实体,entities_per_page
参数(默认100),控制每页返回的实体数,每次迭代时都会返回一个新实体,当有错误时,err
参数会填充错误,迭代的推荐方法是首先检查错误,然后假设实体存在,例如:
for credential, err on kong.db.keyauth_credentials:each(1000) do
if err then
kong.log.err("Error when iterating over keyauth credentials: " .. err)
return nil
end
kong.log("id: " .. credential.id)
end
示例中迭代了1000个元素的凭证,并记录它们的ID,发生错误时,打印错误日志
插入实体
local entity, err, err_t = kong.db.<name>:insert(<values>)
在数据库中插入实体,返回值包括插入实体的副本或 nil、错误消息(字符串)和错误表(table 形式),插入成功后,返回的实体包含默认和自动生成的填充值,以下示例使用 keyauth_credentials
DAO 为给定的消费者插入凭证,将 key
设置为 secret
,注意此处引用外键的语法:
local entity, err = kong.db.keyauth_credentials:insert({
consumer = { id = "c77c50d2-5947-4904-9f37-fa36182a71a9" },
key = "secret",
})
if not entity then
kong.log.err("Error when inserting keyauth credential: " .. err)
return nil
end
假设没有发生错误,返回的实体将包含自动填充的字段,如 id
和 created_at
更新实体
local entity, err, err_t = kong.db.<name>:update(primary_key, <values>)
更新实体的前提是提供可以找到它的主键和一组值,返回的内容是更新后的实体,或者是 nil + 错误信息 + 错误表,以下示例是在给定凭证 ID 的情况下修改现有凭证的 font color=red>key` 字段:
local entity, err = kong.db.keyauth_credentials:update({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" },
{ key = "updated_secret" },
})
if not entity then
kong.log.err("Error when updating keyauth credential: " .. err)
return nil
end
注意此处指定主键的语法与之前指定外键的语法相类似
插入或更新实体
local entity, err, err_t = kong.db.<name>:upsert(primary_key, <values>)
upsert
是 insert
和 update
的结合:
- 当提供的
primary_key
可以标识,与更新实体类似 - 当提供的
primary_key
不可以标识,与插入实体类似
local entity, err = kong.db.keyauth_credentials:upsert({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" },
{ consumer = { id = "a96145fb-d71e-4c88-8a5a-2c8b1947534c" } },
})
if not entity then
kong.log.err("Error when upserting keyauth credential: " .. err)
return nil
end
删除实体
local ok, err, err_t = kong.db.<name>:delete(primary_key)
local ok, err = kong.db.keyauth_credentials:delete({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" }
})
if not ok then
kong.log.err("Error when deleting keyauth credential: " .. err)
return nil
end
缓存自定义实体
有时,每个请求/响应都需要访问自定义实体,每个都会触发数据库的查询,这样效率非常低,因为查询数据库会增加延迟并降低请求/响应速度,并且数据库的负载增加会影响数据库本身性能,从而影响其他 Kong 节点,当每个请求/响应都需要访问自定义实体时,最好利用 Kong 提供的缓存API将其缓存在内存中,下一章将重点描述如何缓存自定义实体,并在数据库内容变化时使它们失效
缓存自定义实体
简介
有时,用户的插件在每个请求/响应都需要访问自定义实体(前一章有所描述),通常第一次加载它们,之后将它们缓存在内存中会显著提供性能,同时可以防止数据库因负载增加而受到压力
想象一下 api-key 鉴权插件需要在每个请求上验证 api-key,从而每次都从数据库中读取自定义的凭证对象,然后根据情况阻断请求或者检索到消费者 ID 识别用户,每个请求都是如此,这会相当低效:
- 查询数据库会增加每个请求的延迟,使请求处理速度变慢
- 数据库负载会增加,速度会变慢或者崩溃,这样会反过来影响 Kong 节点
为了避免每次都查询数据库,我们可以在节点上以内存形式缓存自定义实体,这样频繁的实体查询不会每次都触发数据库查询,而是发生在内存中,这比查询数据库更快更可靠(特别在重负载下)
模块
kong.plugins.<plugin_name>.daos
缓存自定义实体
用户可以使用插件开发工具包提供的 kong.cache
模块将自定义实体缓存在内存中:
local cache = kong.cache
缓存有两层:
- L1: Lua 缓存 - Nginx worker 进程中,可以存储任何类型的 Lua 值
- L2: 共享缓存(SHM)- Nginx 节点中,在 worker 进程中共享,只能保存标量值,更复杂的类型比如 table,需要序列化
从数据库提取数据后,数据会同时存储在两个缓存中,如果同一个 worker 进程再次请求数据,Kong会从 Lua 缓存中检索之前反序列化的数据;如果同一个节点的另一个 worker 进程请求该数据,Kong 会从 SHM 中找到该数据,并对其反序列化(存储在当前进程的缓存中),然后将其返回
该模块公开以下方法:
方法名 | 描述 |
---|---|
value, err = cache:get(key, opts?, cb, ...) | 从缓存中检索值,如果缓存没有值(未命中),则在保护模式下调用 cb ,cb 会且仅会返回一个缓存的值,这个方法也会抛出错误,这些错误会被Kong捕获,并记录为 ngx.ERR 级别,这个方法会缓存 nil,因此必须通过第二个参数检查可能的错误 |
ttl, err, value = cache:probe(key) | 检查是否有缓存值,如果有,返回 ttl;如果没有,返回 nil,缓存值可以是 nil,第三个返回值是缓存的内容 |
cache:invalidate_local(key) | 在节点中删除一个缓存 |
cache:invalidate(key) | 在集群中删除一个缓存 |
cache:purge() | 在节点中删除所有缓存 |
回到鉴权插件,当使用特定的 api-key 查找凭证时,会这样写:
-- handler.lua
local BasePlugin = require "kong.plugins.base_plugin"
local kong = kong
local function load_credential(key)
local credential, err = kong.db.keyauth_credentials:select_by_key(key)
if not credential then
return nil, err
end
return credential
end
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 1010
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:access(config)
CustomHandler.super.access(self)
-- retrieve the apikey from the request querystring
local key = kong.request.get_query_arg("apikey")
local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
-- We are using cache.get to first check if the apikey has been already
-- stored into the in-memory cache. If it's not, then we lookup the datastore
-- and return the credential object. Internally cache.get will save the value
-- in-memory, and then return the credential.
local credential, err = kong.cache:get(credential_cache_key, nil,
load_credential, credential_cache_key)
if err then
kong.log.err(err)
return kong.response.exit(500, {
message = "Unexpected error"
})
end
if not credential then
-- no credentials in cache nor datastore
return kong.response.exit(401, {
message = "Invalid authentication credentials"
})
end
-- set an upstream header if the credential exists and is valid
kong.service.request.set_header("X-API-Key", credential.apikey)
end
return CustomHandler
在上面的示例中,我们使用插件开发工具包中的各种组件与请求、缓存模块进行交互,甚至在插件中自定义了响应,现在,有了上述机制,一旦消费者携带 API key 发送请求,缓存就被预热了,后续请求不会再触发数据库查询,缓存在 Key-Auth 插件的多个地方使用
更新或删除自定义实体
每次在数据库中更新或删除缓存过的自定义实体时(比如使用 Admin API),都会造成数据库中的数据与Kong内存中缓存的数据不一致,为了避免这种情况,用户需要在内存中删除缓存的实体,并强制Kong再次从数据库中查询它,我们称这个过程为缓存失效
实体缓存失效
如果用户希望通过 CRUD 操作使实体失效,而不是等它们到达 TTL 时间,需要执行几个步骤,对于大多数实体,这个过程会自动执行,但有些需要手动订阅某些 CRUD 事件使具有复杂关系的实体失效
自动失效
在用户实体的 schema 中设置 cache_key
可以直接启用缓存失效功能,例如:
local typedefs = require "kong.db.schema.typedefs"
return {
-- this plugin only results in one custom DAO, named `keyauth_credentials`:
keyauth_credentials = {
name = "keyauth_credentials", -- the actual table in the database
endpoint_key = "key",
primary_key = { "id" },
cache_key = { "key" },
generate_admin_api = true,
fields = {
{
-- a value to be inserted by the DAO itself
-- (think of serial id and the uniqueness of such required here)
id = typedefs.uuid,
},
{
-- also interted by the DAO itself
created_at = typedefs.auto_timestamp_s,
},
{
-- a foreign key to a consumer's id
consumer = {
type = "foreign",
reference = "consumers",
default = ngx.null,
on_delete = "cascade",
},
},
{
-- a unique API key
key = {
type = "string",
required = false,
unique = true,
auto = true,
},
},
},
},
}
如果 cache_key
是这样生成的,并在实体的 schema 中指定,那么缓存失效过程是自动的:每个有关 key 的 CRUD 操作都会影响到 cache_key
,并会广播到集群上的其他节点,以便在缓存中清除这个值,再下一个请求中从数据库中重新获取
当父实体执行 CRUD 操作,Kong 会对父实体和子实体同时执行缓存失效机制
手动失效
在某些情况下,实体 schema 的 cache_key
属性不够灵活,必须手动使缓存失效,在这些情况,用户需要手动在Kong的失效通道注册订阅,并执行自定义失效流程,要监听 Kong 内部的失效通道,需要在 init_worker
段中实现以下内容:
function MyCustomHandler:init_worker()
-- listen to all CRUD operations made on Consumers
kong.worker_events.register(function(data)
end, "crud", "consumers")
-- or, listen to a specific CRUD operation only
kong.worker_events.register(function(data)
kong.log.inspect(data.operation) -- "update"
kong.log.inspect(data.old_entity) -- old entity table (only for "update")
kong.log.inspect(data.entity) -- new entity table
kong.log.inspect(data.schema) -- entity's schema
end, "crud", "consumers:update")
end
一旦上述监听器适用于所需的实体,用户就可以根据需要对插件缓存的任何实体手动执行失效,例如:
kong.worker_events.register(function(data)
if data.operation == "delete" then
local cache_key = data.entity.id
kong.cache:invalidate("prefix:" .. cache_key)
end
end, "crud", "consumers")
扩展 Admin API
简介
用户可以使用称为 Admin API 的 REST 接口配置 Kong,插件可以通过添加自己的端点,管理插件中的自定义实体,典型的例子是增删改查
Admin API 是一个 Lapis 应用程序,Kong 提供了抽象,用户可以轻松添加端点
模块
kong.plugins.<plugin_name>.api
在 Admin API 上添加端点
如果用户以这个格式定义模块,Kong 会检测并加载端点:
"kong.plugins.<plugin_name>.api"
这个模块必须返回一个 table,结构如下:
{
["<path>"] = {
schema = <schema>,
methods = {
before = function(self) ... end,
on_error = function(self) ... end,
GET = function(self) ... end,
PUT = function(self) ... end,
...
}
},
...
}
其中:
-
<path>
:表示一个路径,路由中可以包含差值参数 -
<schema>
:结构定义,核心或者自定义插件实体的 schema 可以通过kong.db.<entity>.schema
获取,schema 用于判断字段根据什么类型解析,默认情况下,表单字段的类型都是 string -
methods
:包含了一系列方法,索引是字符串
-
before
键是可选的,可以定义一个方法,如果存在,则在调用任何其他方法之前,都会执行这个方法 - 可以使用 HTTP 名称(如
GET
或PUT
)作为索引,当匹配对应的路径和 HTTP 方法时,将执行索引对应的方法,如果在路径上存在 before 方法,则首先执行该方法,注意,before 方法可以使用kong.response.exit
提前退出,这样可以跳过原有的 HTTP 方法 -
on_error
键是可选的,可以定义一个方法,如果存在,当其他方法抛出错误时会执行该方法;如果不存在,Kong会使用默认错误处理程序返回错误
例如:
local endpoints = require "kong.api.endpoints"
local credentials_schema = kong.db.keyauth_credentials.schema
local consumers_schema = kong.db.consumers.schema
return {
["/consumers/:consumers/key-auth"] = {
schema = credentials_schema,
methods = {
GET = endpoints.get_collection_endpoint(
credentials_schema, consumers_schema, "consumer"),
POST = endpoints.post_collection_endpoint(
credentials_schema, consumers_schema, "consumer"),
},
},
}
上面这端代码将在 /consumers/:consumers/key-auth
路径上创建两个 Admin API 端点,用来获取(GET)和创建(POST)绑定在消费者上的凭证,此示例中,方法由 kong.api.endpoints
库提供,如果想要查看更完整的示例,并在方法中使用自定义代码,请查看 key-auth 插件中的 api.lua 文件
端点模块中当前包含了Kong中最常用的 CRUD 操作的默认实现,此模块为用户提供了增删改查的帮助程序,并执行对应的 DAO 层操作,使用响应的 HTTP 状态码回应,它还提供了从路径中检索参数的功能,例如服务名称或 ID,消费者用户名或ID
如果端点提供的功能不够,可以使用常规的 Lua 方法:
-
endpoints
模块提供的方法 - PDK 提供的所有方法
-
self
参数,Lapis 的请求对象 - 用户可以引入需要的 Lua 模块
local endpoints = require "kong.api.endpoints"
local credentials_schema = kong.db.keyauth_credentials.schema
local consumers_schema = kong.db.consumers.schema
return {
["/consumers/:consumers/key-auth/:keyauth_credentials"] = {
schema = credentials_schema,
methods = {
before = function(self, db, helpers)
local consumer, _, err_t = endpoints.select_entity(self, db, consumers_schema)
if err_t then
return endpoints.handle_error(err_t)
end
if not consumer then
return kong.response.exit(404, { message = "Not found" })
end
self.consumer = consumer
if self.req.method ~= "PUT" then
local cred, _, err_t = endpoints.select_entity(self, db, credentials_schema)
if err_t then
return endpoints.handle_error(err_t)
end
if not cred or cred.consumer.id ~= consumer.id then
return kong.response.exit(404, { message = "Not found" })
end
self.keyauth_credential = cred
self.params.keyauth_credentials = cred.id
end
end,
GET = endpoints.get_entity_endpoint(credentials_schema),
PUT = function(self, db, helpers)
self.args.post.consumer = { id = self.consumer.id }
return endpoints.put_entity_endpoint(credentials_schema)(self, db, helpers)
end,
},
},
}
在上面的示例中,/consumers/:consumers/key-auth/:keyauth_credentials
路径定义了三个方法:
-
before
方法是一个自定义的 Lua 方法,其中使用了endpoints
提供的方法(endpoints.handle_error)和 PDK 中的方法(kong.response.exit),它也填充了self.consumer
参数,以供后续方法调用 -
GET
方法完全使用endpoints
,这是合理的,before
方法已经预先准备了东西,比如self.consumer
-
PUT
方法在调用endpoints
提供的put_entity_endpoint
方法前,先填充了self.args.post.consumer
参数
测试用例
简介
如果用户认真对待自己的插件,需要为它编写测试用例,单元测试 Lua 脚本很简答,也有很多测试框架可以选择,如果用户还想编写集成测试,Kong 也提供了这样的功能
编写集成测试用例
Kong 首选的测试框架是 busted,与 resty-cli 解释器一起运行,如果用户愿意,也可以使用其他的,在 Kong 的仓库中,用户可以找到 busted 的可执行文件 bin/busted
Kong 提供了一个帮助程序 spec.helpers,可以在测试套件中启停 Lua 脚本,这个帮助程序还能在运行测试之前在数据中插入或删除数据,以及其他各种帮助
如果用户想在自己的仓库中编写插件,可以复制以下文件:
local helpers = require "spec.helpers"
for _, strategy in helpers.each_strategy() do
describe("my plugin", function()
local bp = helpers.get_db_utils(strategy)
setup(function()
local service = bp.services:insert {
name = "test-service",
host = "httpbin.org"
}
bp.routes:insert({
hosts = { "test.com" },
service = { id = service.id }
})
-- start Kong with your testing Kong configuration (defined in "spec.helpers")
assert(helpers.start_kong( { plugins = "bundled,my-plugin" }))
admin_client = helpers.admin_client()
end)
teardown(function()
if admin_client then
admin_client:close()
end
helpers.stop_kong()
end)
before_each(function()
proxy_client = helpers.proxy_client()
end)
after_each(function()
if proxy_client then
proxy_client:close()
end
end)
describe("thing", function()
it("should do thing", function()
-- send requests through Kong
local res = proxy_client:get("/get", {
headers = {
["Host"] = "test.com"
}
})
local body = assert.res_status(200, res)
-- body is a string containing the response
end)
end)
end)
end
注意:当使用测试环境的 Kong 配置文件时,Kong代理监听9000和9443端口,Admin API 监听9001端口
安装/卸载插件
简介
Kong 的自定义插件由Lua源文件组成,这些源文件需要安装在 Kong 节点的文件系统中,本章将逐步说明,使 Kong 节点可以理解用户的自定义插件
这些步骤将作用于 Kong 集群的每个节点,以确保每个节点上都有用户的自定义插件
打包源码
用户可以使用常规打包策略(比如 tar),也可以使用 LuaRocks 包管理器来做执行这项工作,我们推荐使用 LuaRocks,因为它已经携带在官方发布的Kong安装包中
当使用 LuaRocks,用户必须创建一个 rockspec
文件来指定包内容,有关示例可以参考Kong的插件模板,更多信息可以参考 LuaRocks 的文档,用户可以使用以下命令打包文件:
# install it locally (based on the `.rockspec` in the current directory)
luarocks make
# pack the installed rock
luarocks pack <plugin-name> <version>
假设用户插件的 rockspec 文件叫 kong-plugin-my-plugin-0.1.0-1.rockspec
,命令行为:
luarocks pack kong-plugin-my-plugin 0.1.0-1
LuaRocks 的 pack
指令创建了一个 .rock
文件(这是一个包含安装 rock 所需内容的 zip 文件)
如果用户选择不使用 LuaRocks,可以使用 tar
指令将包含的 .lua
文件打包 .tar.gz
存档中
存档的内容类似如下格式:
tree <plugin-name>
<plugin-name>
├── INSTALL.txt
├── README.md
├── kong
│ └── plugins
│ └── <plugin-name>
│ ├── handler.lua
│ └── schema.lua
└── <plugin-name>-<version>.rockspec
安装插件
要使 Kong 节点能够使用自定义插件,必须在主机的文件系统上安装自定义插件的 Lua 源,有多种方法可以达成:通过 LuaRocks,或手动,选择其中一个,然后直接跳转到第3部分:
- 通过 LuaRocks 创建 rock
.rock 文件是一个自包含的软件包,可以在本地安装,也可以从远程服务器安装,如果用户的系统中安装了 LuaRocks,可以在 LuaRocks 树中安装 rock,安装指令如下:
luarocks install <rock-filename>
- 如果用户的系统中已经安装了 luarocks,用户可以将当前目录修改为插件存档的目录,其中 rockspec 文件是:
cd <plugin-name>
luarocks make
这将在用户系统中的 LuaRocks 树中安装 kong/plugins/<plugin-name>
的源文件
- 一个更保险的安装方式是避免污染 LuaRocks 树,而是将 Kong 指向包含它们的目录,这通过调整
lua_package_path
属性完成,如果你熟悉它,这个属性是 Lua VM 中LUA_PATH
变量的别名,这个属性包含以分号分隔的目录列表,用于搜索 Lua 源,配置大致如下:
lua_package_path = /<path-to-plugin-location>/?.lua;;
其中:
/<path-to-plugin-location>
:包含提取存档的目录的路径
?
:占位符,在 Kong 尝试加载插件时,会被 kong.plugins.<plugin-name>
替换,不要修改它
;;
:默认 Lua 路径的占位符,不要修改它
例如:
something
这个插件的 handler 文件在文件系统中这个位置:
/usr/local/custom/kong/plugins/<something>/handler.lua
Kong的目录是 /usr/local/custom
,因此,正确的路径可以设置为:
lua_package_path = /usr/local/custom/?.lua;;
多个插件:如果用户希望通过这个方法安装多个插件,可以这样设置变量:
lua_package_path = /path/to/plugin1/?.lua;/path/to/plugin2/?.lua;;
;
:多个目录之间的分隔符
;;
:依旧表示默认 Lua 路径的占位符
加载插件
用户必须将自定义插件的名称添加到Kong配置中的插件列表(每个节点都需要):
plugins = bundled,<plugin-name>
或者,用户可以不添加绑定的插件:
plugins = <plugin-name>
如果用户需要使用多个插件,可以用逗号分隔:
plugins = bundled,plugin1,plugin2
或
plugins = plugin1,plugin2
注意,用户还可以通过环境变量 KONG_PLUGINS
来设置此属性,不要忘记更新Kong集群中每个节点的 plugins
指令,插件重启会生效
kong restart
如果用户不希望Kong停机并加载上插件,可以这样:
kong prepare
kong reload
验证加载插件
现在用户可以正常启动 Kong 了,为了确保插件被 Kong 加载,可以使用调试
日志级别启动 Kong:
log_level = debug
或者
KONG_LOG_LEVEL=debug
然后,用户可以看到加载的每个插件
[debug] Loading plugin <plugin-name>
删除插件
删除插件通常有3个步骤:
- 先从 Kong 的服务或路由配置中删除插件,确保该插件不再作用域全局,也不作用任何服务、路由或消费者,对于整个 Kong 集群,只需执行一次整个操作,不需要执行 restart 或 reload 指令,这个步骤只是让集群不再使用该插件,但它仍然可以再被启用
- 从
plugins
指令删除插件,确保在执行此操作之前已完成步骤1,在此步骤之后,任何人都不能将插件重新应用在服务、路由、消费者或者全局中,此步骤需要执行 restart 或 reload 指令才能生效 - 要彻底删除插件,要在每个 Kong 节点删除与插件相关的文件,在删除文件之前,确保已完成步骤2,包括重启 Kong,如果用户之前使用 LuaRocks 安装插件,可以使用
luarocks remove <plugin-name>
指令来删除
发布插件
首选的方法是使用 LuaRocks,Lua 模块的包管理器,我们称这些模块是 rock
,用户不必将模块存储在 Kong 的仓库中,如果用户希望维持Kong的设置,则需要这样做
通过在 rockspec 文件中定义模块(及其依赖项),用户可以通过 LuaRocks 在平台上安装模块,用户也可以使用 LuaRocks 上传模块给其他人使用
排除故障
由于以下几个原因,配置错误的自定义插件可能无法启动:
-
plugin is in use but not enabled
:用户在其他节点配置了自定义插件,并且数据库中已经保存了插件的配置,但是当前节点的plugins
指令中没有找到该插件,要解决此问题,需要将每个节点都添加plugins
指令 -
plugin is enabled but not installed
:插件已添加在plugins
指令中,但Kong无法从文件系统中加载handler.lua
源文件,要解决此问题,确保正确设置 lua_package_path 指令来加载 Lua 源文件 -
no configuration schema found for plugin
:插件已安装,但Kong无法从文件系统中加载schema.lua
源文件,要解决此问题,确保schema.lua
与handler.lua
文件一起存在