本文演示如何编写一个kong的插件,希望帮助大家快速理解插件编写的关键思路。
插件开发参照自2个信息源:
- kong官方文档
- kong官方插件项目模板:https://github.com/Kong/kong-plugin
因为lua语法也是很重要的一部分,所以我也会把一些涉及到的lua语法顺便说一下。
初始化plugin源码
随便在哪里创建1个空目录,然后开始准备3个文件:
- handler.lua:实现kong各个生命期的钩子函数,会被kong加载使用,是我们插件核心逻辑所在。
- schema.lua:定义plugin支持哪些配置,每一个字段的类型,这样kong可以校验。
- .rockspec:luarocks包管理工具的描述文件,kong利用luarocks把plugin源码安装到lua的系统查找路径下面。(当然我们也可以不用.rockspec,手动把源码放到lua系统路径下,以便openresty可以加载到)
我要做的插件有2个功能:
- 为response设置1个http header叫做tag,其值是可以配置的。
- 把request的来源IP记录到日志中。
schema.lua
从官方模板copy过来,我做了最大程度的简化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
return { no_consumer = false, -- this plugin is available on APIs as well as on Consumers, fields = { -- Describe your plugin's configuration's schema here tag = { type = "string", required = true, }, }, self_check = function(schema, plugin_t, dao, is_updating) -- perform any custom verification return true end } |
- schema用于定义插件配置的约束规范,插件配置是向service/route添加plugin时上传的。
- fields中指定了其中有一个字符串类型的配置项,必传,名字叫做tag
- self_check是一个更灵活的配置检查回调函数,这里没有做任何事情,仅作演示用。
handler.lua
从官方模板copy过来,进行了最大程序的精简(保留了必要的官方注释),并且实现了插件的2个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
local plugin = { PRIORITY = 1000, -- set the plugin priority, which determines plugin execution order VERSION = "0.1", } -- constructor function plugin:new() -- do initialization here, runs in the 'init_by_lua_block', before worker processes are forked end --------------------------------------------------------------------------------------------- -- In the code below, just remove the opening brackets; `[[` to enable a specific handler -- -- The handlers are based on the OpenResty handlers, see the OpenResty docs for details -- on when exactly they are invoked and what limitations each handler has. --------------------------------------------------------------------------------------------- -- handles more initialization, but AFTER the worker process has been forked/created. -- It runs in the 'init_worker_by_lua_block' function plugin:init_worker() -- your custom code here end --]] -- runs in the ssl_certificate_by_lua_block handler function plugin:certificate(plugin_conf) -- your custom code here end --]] -- runs in the 'rewrite_by_lua_block' -- IMPORTANT: during the `rewrite` phase neither the `api` nor the `consumer` will have -- been identified, hence this handler will only be executed if the plugin is -- configured as a global plugin! function plugin:rewrite(plugin_conf) -- your custom code here end --]] --- runs in the 'access_by_lua_block' function plugin:access(plugin_conf) -- your custom code here -- ngx.req.set_header("Hello-World", "this is on a request") kong.ctx.plugin.source_ip = kong.client.get_ip() end --]] ---[[ runs in the 'header_filter_by_lua_block' function plugin:header_filter(plugin_conf) -- your custom code here, for example; -- ngx.header["Bye-World"] = "this is on the response" kong.response.set_header("tag", plugin_conf["tag"]) end --]] -- runs in the 'body_filter_by_lua_block' function plugin:body_filter(plugin_conf) -- your custom code here end --]] -- runs in the 'log_by_lua_block' function plugin:log(plugin_conf) -- your custom code here kong.log.debug("my-plugin", kong.ctx.plugin.source_ip) end --]] -- return our plugin object return plugin |
handler.lua定义并导出了一个lua模块,看起来像个class,但其实这是lua字典的语法:
- function plugin:log(plugin_conf)实际就等价于plugin[“log”] = function(plugin_conf),就是字典里定义了一个函数。
- lua中function plugin:log()和function plugin.log()的区别,其实前者会提供一个隐式的self表示字典自己,调用的时候也只需要plugin:log()即可;后者则需要显示定义和传参字典自身。
再说明一下这些出现的概念:
- new:nginx master进程启动后的回调。
- init_worker:nginx worker进程启动后的回调。
- rewrite:request进来后首先经过rewrite,可以改写URL等。
- access:request路由完成之后,进一步处理之前,可以修改request。
- header_filter:response准备返回之前,可以修改header。
- body_filter:response准备返回之前,可以修改body。
整个流程图奉上:
分析一下我的插件逻辑:
- access回调:
- 利用kong.client.get_ip()获取了客户端IP地址字符串;其实kong这个变量是kong框架定义的全局变量,就像代码中ngx.req的ngx全局变量一样;我们也可以知道,其实写kong插件可以使用kong封装的方法kong.xxxx,也可以使用openresty原生方法ngx.xxxx,这块比较灵活。
- 保存IP地址到kong.ctx.plugin.source_ip;其实它底层就是利用的ngx.ctx,提供了跨hook之间的数据传递能力;plugin是一个请求级生命期的字典,因为我要在log回调时候打印source_ip,所以要在access阶段放到ctx里暂存。
- log回调:
- 调用kong.log去打印Nginx日志,其实底层也是利用openresty的能力完成打印。
- 从kong.ctx.plugin上下文中取出source_ip。
.rockspec
最后要把插件代码安装到lua系统路径下,以便kong框架可以通过lua require加载到。
这个插件的源码最终会被kong框架以require “kong.plugins.my.handler”这样的方式引入,所以其最终会安装到路径:/usr/local/share/lua/5.1/kong/plugins/my/下面,其中/usr/local/share/lua/5.1是lua默认的系统查找路径。
但是我们不采用手动方式copy源码到该路径,而是采用kong官方建议的luarocks包管理工具(虽然我觉得没有啥卵用)。
在代码同目录创建.rockspec文件,注意名字必须长下面这样(后面我会说为什么):
1 2 3 4 5 |
[root@localhost my]# ll total 12 -rw-r--r-- 1 root root 2163 Dec 18 15:06 handler.lua -rw-r--r-- 1 root root 369 Dec 18 14:52 kong-plugin-my-0.1.0-1.rockspec -rw-r--r-- 1 root root 473 Dec 18 14:41 schema.lua |
查看rockspec内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[root@localhost my]# cat kong-plugin-my-0.1.0-1.rockspec package = "kong-plugin-my" version = "0.1.0-1" supported_platforms = {"linux", "macosx"} source = { url = "", tag = "0.1.0" } description = { summary = "", homepage = "", license = "" } dependencies = { } build = { type = "builtin", modules = { ["kong.plugins.my.handler"] = "handler.lua", ["kong.plugins.my.schema"] = "schema.lua", } } |
文件名其实是luarocks要求的,必须是文件里的package-version连起来的样子,不匹配就不合法。
build部分其实就是告诉luarocks把每个文件拷贝到/usr/local/share/lua/5.1下面的什么路径下,luarocks根据kong.plugins.my.handler这样的包名就会把代码handler.lua放到正确位置了。
现在执行luarocks完成插件的安装:
1 2 3 |
[root@localhost my]# luarocks make kong-plugin-my 0.1.0-1 is now installed in /usr/local (license: ) |
现在看一下lua系统路径下已经存在代码了:
1 2 3 4 |
[root@localhost my]# ll /usr/local/share/lua/5.1/kong/plugins/my/ total 8 -rw-r--r-- 1 root root 2163 Dec 18 15:28 handler.lua -rw-r--r-- 1 root root 473 Dec 18 15:28 schema.lua |
大家感兴趣也可以读读plugins目录下,kong提供的官方插件是怎么实现的。
测试插件
编辑/etc/kong/kong.conf,调整nginx的日志级别(因为我是用debug级别输出的source_ip):
1 2 |
log_level = debug # Log level of the Nginx server. Logs are # found at `<prefix>/logs/error.log`. |
kong的日志是输出到error.log的,其实这是openresty的能力。
然后注册开启插件:
1 |
plugins = bundled,my |
bundled是内置的,my是我们自定义的那个插件,其名字就是包的目录名,kong会去找到它。
然后重启kong:
1 |
kong restart |
然后添加一个service叫做my-service(指向baidu):
1 |
curl -X POST http://localhost:8001/services/ --data 'name=my-service' --data 'url=https://www.baidu.com' |
添加对应的Route叫做my-route(域名为my-baidu.com):
1 |
curl -X POST --url http://localhost:8001/services/my-service/routes --data 'name=my-route' --data 'hosts[]=my-baidu.com' |
然后验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[root@localhost my]# curl localhost:8000 -H 'Host:my-baidu.com' -I HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 277 Connection: keep-alive Accept-Ranges: bytes Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform Date: Wed, 18 Dec 2019 08:57:06 GMT Etag: "575e1f59-115" Last-Modified: Mon, 13 Jun 2016 02:50:01 GMT Pragma: no-cache Server: bfe/1.0.8.18 X-Kong-Upstream-Latency: 18 X-Kong-Proxy-Latency: 1 Via: kong/1.4.1 |
可以得到来自百度bfe的应答。
然后添加插件到route对象:
1 |
curl -X POST --url http://localhost:8001/routes/my-route/plugins --data 'name=my' --data 'config.tag=owenliang' |
然后再次请求接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[root@localhost my]# curl localhost:8000 -H 'Host:my-baidu.com' -I HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 277 Connection: keep-alive Accept-Ranges: bytes Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform Date: Wed, 18 Dec 2019 09:01:42 GMT Etag: "575e1f59-115" Last-Modified: Mon, 13 Jun 2016 02:50:01 GMT Pragma: no-cache Server: bfe/1.0.8.18 tag: owenliang X-Kong-Upstream-Latency: 15 X-Kong-Proxy-Latency: 11 Via: kong/1.4.1 |
可以看到tag:owenliang已经出现在response header中。
另外,我们查看一下nginx日志(默认路径:/usr/local/kong/logs/error.log):
1 |
2019/12/18 17:01:42 [debug] 11649#0: *24143 [kong] handler.lua:71 [my] my-plugin127.0.0.1 |
成功。
关于kong插件开发的核心概念就说到这里,如果你有openresty的开发经验,那么kong对你来说应该没有什么难于理解的地方。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

1