开发自定义 Lua 插件
自定义 Lua 插件可以在请求/响应生命周期的不同阶段执行自定义逻辑,从而扩展 API7 网关功能。本指南将带你 创建一个简单的 Lua 插件,在 API7 网关中注册该插件,并将其应用到路由。
前提条件
- 具备 Lua 编程语言基础知识。
- 熟悉 OpenResty 和 NGINX 请求生命周期。
- 能够访问 API7 网关实例及其 Dashboard。
- 从 Dashboard 创建的 Token。
插件结构
Lua 插件是一个导出 Table 的模块,其中包含配置 Schema、优先级和阶段处理函数。
标准字段
每个插件都应定义以下字段:
name:插件的唯一名称。version:插件版本。priority:决定执行顺序,优先级越高越先执行。schema:定义插件配置参数的 JSON Schema。
执行阶段
API7 网关按以下顺序执行插件:
rewrite:在路由匹配前修改请求,例如更改 URI。access:主要处理阶段,例如认证和限流。before_proxy:在 access 阶段之后、请求代理到上游之前执行。header_filter:修改响应头。body_filter:修改响应体。log:响应发送给客户端后记录日志。
分步开发
1. 创建插件文件
为插件新建 Lua 文件。本示例使用名为 custom-header 的插件。
2. 定义 Schema 和优先级
使用 apisix.core 根据 JSON Schema 校验插件配置。
local core = require("apisix.core")
local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}
local plugin_name = "custom-header"
local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
3. 实现阶段处理函数
在适当阶段实现逻辑。若要在请求到达上游前添加请求头,请使用 access 阶段。
function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end
4. 完整插件示例
以下是 custom-header插件的完整代码:
local core = require("apisix.core")
local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}
local plugin_name = "custom-header"
local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end
return _M
在 API7 网关中注册插件
在 API7 网关中,自定义插件由控制面管理。准备好 Lua 源文件后,通过 Dashboard 上传并部署,使其出现在路由、服务、消费者或全局规则的插件选择器中。
1. 在 Dashboard 中添加插件
在 Dashboard 中进入 Gateway Settings,打开 Custom Plugin 标签页,然后点击 Add Custom Plugin。
上传 Lua 源文件并提供所需插件元数据:
- Plugin Source Code File:上传
custom-header.lua。 - Plugin Description:简要说明插件功能。
- Plugin Documentation Link:可选,填写插件文档链接。
- Plugin Author:标明插件所有者。
- Deployed Gateway Group:选择可使用该插件的网关组。为安全发布,最初只选择测试网关组,详见跨网关组发布。
文件上传成功后,API7 网关会解析插件,并在添加前于对话框显示检测到的 name 和 version。
添加插件后,可以在已部署网关组的路由和服务 Add Plugin 对话框中选择它。
有关产品操作流程,请参阅自定义插件。
2. 确认插件可用
在已部署网关组中打开路由或服务,确认 Add Plugin 对话框中出现 custom-header。
添加依赖库
从 Dashboard 上传的自定义插件会注册为独立、完整的插件模块。控制面将源代码分发到网关,并直接加载到运行时。该文件不会写入网关的 Lua 模块 搜索路径,因此其他插件无法可靠地通过 require() 解析它。请勿把共享库作为自定义插件上传供其他插件require。这种做法最初可能有效,但网关重启后会失效,因为无法保证依赖先于引用它的插件加载。
有些插件会共享辅助代码,或依赖第三方 Lua 模块或原生 C 库。请将依赖安装到网关的包路径,并像往常一样在插件中 require。
将依赖放到 Lua 或 C 路径
网关通过两类搜索路径解析模块:
- Lua 模块通过
lua_package_path解析。可使用require("myorg.utils")加载<dir>/myorg/utils.lua。 - 原生 Lua/C 模块通过
lua_package_cpath解析。可使用require("myorg.native")加载<dir>/myorg/native.so。
可以使用网关内置路径或自定义目录:
-
内置路径:将文件放到网关内置搜索路径(例如
/usr/local/apisix/),并与require的模块名保持一致。 -
额外路径(推荐):将代码与运行时分离,并在数据面
config.yaml中添加自定义目录:apisix:
extra_lua_path: "/data/lua/?.lua" # 扩展 lua_package_path
extra_lua_cpath: "/data/clib/?.so" # 扩展 lua_package_cpath
通过 LuaJIT FFI 的 ffi.load() 加载的原生库由系统动态链接器解析,而不是 lua_package_cpath。请将这类库放到系统库路径(例如 /usr/local/lib),或在网关上设置 LD_LIBRARY_PATH。
让网关能够访问文件
网关启动前,每个网关节点上都必须存在依赖文件:
- Kubernetes / Helm:使用卷(如 ConfigMap 或持久卷)把文件挂载到网关 Pod,并让
extra_lua_path/extra_lua_cpath指向挂载路径。 - Docker / Docker Compose:将目录绑定挂载到网关容器。
上传的自定义插件由控制面动态分发,而包路径上的库在网关 Worker 启动时加载。添加或修改后,请重新加载或重启网关使变更生效。
示例
将共享辅助函数放入网关路径上的普通 Lua 模块,例如 /data/lua/myorg/base.lua:
local _M = {}
function _M.verify(token)
-- 多个插件共用的逻辑
return token ~= nil
end
return _M
将目录添加到数据面配置:
apisix:
extra_lua_path: "/data/lua/?.lua"
然后在任意自定义插件中 require:
local core = require("apisix.core")
local base = require("myorg.base")
-- ...
function _M.access(conf, ctx)
if not base.verify(core.request.header(ctx, "Authorization")) then
return 401
end
end
由于模块位于包路径,require("myorg.base") 在每个 Worker 上的解析方式相同,网关重启后仍然有效。
测试插件
插件源代码已在网关节点上可用且插件已注册到 API7 网关后,可以使用 Admin API 或 ADC 在特定路由上启用它。
- 创建启用该插件的路由:
- Admin API
- ADC
curl -k "https://localhost:7443/apisix/admin/services/custom-header-service?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-service",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [
{
"host": "httpbin.org",
"port": 80,
"weight": 100
}
]
}
}'
curl -k "https://localhost:7443/apisix/admin/routes/custom-header-route?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-route",
"methods": ["GET"],
"paths": ["/get"],
"service_id": "custom-header-service",
"plugins": {
"custom-header": {
"header_name": "X-Trace-ID",
"header_value": "docs-test"
}
}
}'
services:
- name: custom-header-service
upstream:
nodes:
- host: httpbin.org
port: 80
weight: 1
routes:
- name: custom-header-route
uris:
- /get
plugins:
custom-header:
header_name: X-Trace-ID
header_value: docs-test
adc sync -f adc.yaml
- 发送请求并 验证上游响应体中的请求头:
curl -i "http://localhost:9080/get"
httpbin 响应的 headers 中应包含 X-Trace-ID: docs-test,这表明插件在代理前修改了请求。
如果路由创建后代理立即返回 404 Route Not Found,请等待几秒后重试。
跨网关组发布
自定义插件按网关组部署。每个网关组都是隔离的配置范围:部署到一个组的插件无法供其他组的路由或服务使用。借助这种隔离,可在插件影响生产流量前先在非生产组中验证。
推荐的发布流程如下:
- **部署到测试网关组。**添加插件时,仅在 Deployed Gateway Group 下选择测试或预发布网关组。插件只对该组可用。
- **验证。**按照测试插件在测试组的路由上启用插件并确认行为符合预期。生产组尚无该插件,因此迭代期间不受影响。
- **发布到生产环境。**验证完成后,编辑自定义插件并将生产网关组添加到 Deployed Gateway Group。控制面会立即把插件分发到新选中组的网关。
要更改部署范围,请进入 Gateway Settings,打开 Custom Plugin 标签页和目标插件,更新 Deployed Gateway Group 并保存。
请注意:
- **更新会同时应用到所有已部署组。**上传新版本会同时更新所有网关组中的插件,不支持按组维护版本。因此测试优先流程非常适合新插件。若要验证已在生产环境运行的插件变更,请将它作为名称唯一的独立插件上传,并仅部署到测试组;验证后再将变更应用到生产插件。
- **移除网关组会撤回插件。**如果该组中的路由仍引用插件,可能会出现故障。只有确认不存在依赖后才能移除。
- **插件名称是全局的。**自定义插件是带有一组部署网关组的单一命名对象,而不是每个组各有一份副本。发布过程中请保持名称唯一且稳定。
调试提示
- 检查错误日志:日志默认位于
/usr/local/apisix/logs/error.log。 - 语法错误:上传前使用
luac -p custom-header.lua检查 Lua 语法。 - 插件可见性:如果 Dashboard 插件列表中未出现该插件,请确认它已部署到正确的网关组。
- Admin API 载荷结构:API7 网关创建路由时使用
paths和service_id,而不是 APISIX 风格的顶层uri和内联upstream。 - 名称冲突:确保插件名称唯一且不与内置插件冲突。
后续步骤
- 插件开发最佳实践 — 编写正确、高效且易维护的插件代码。
- Serverless 函数或自定义插件 — 为用例选择合适的扩展机制。
- 插件 — 了解插件执行顺序和作用域。