參考
- https://docs.api7.ai/apisix/how-to-guide/custom-plugins/create-plugin-in-lua
- https://apisix.apache.org/docs/apisix/next/plugin-develop/
- https://apisix.apache.org/docs/apisix/next/plugins/prometheus/
- https://apisix.apache.org/blog/2022/02/16/file-logger-api-gateway/
此文檔是關於 lua 語言的插件開發,其他語言請看:external plugin。
插件放置路徑#
路徑的相對路徑是固定的,必須是apisix/plugins,例如extra_lua_path是/path/to/example,那你真實的lua文件應該放到/path/to/example/apisix/plugins/下面
Apache APISIX 提供了兩種方式來添加新的功能。
- 修改 Apache APISIX 的源代碼並重新發布 (不推薦)。
- 配置 extra_lua_path 和 extra_lua_cpath 在 conf/config.yaml 以加載你自己的代碼文件。你應該給自己的代碼文件起一個不包含在原來庫中的名字,而不是使用相同名稱的代碼文件,但是如果有需要,你可以使用這種方式覆蓋內置的代碼文件。
比如,你可以創建一個目錄目錄結構,像下面這樣:
├── example
│ └── apisix
│ ├── plugins
│ │ └── 3rd-party.lua
│ └── stream
│ └── plugins
│ └── 3rd-party.lua
如果你需要自定義插件的目錄,請在該目錄下創建 /apisix/plugins 的子目錄。
接着,在 conf/config.yaml 文件中添加如下的配置:
apisix:
...
extra_lua_path: "/path/to/example/?.lua"
plugins:
- 3rd-party
-
覆蓋原有插件的問題
-
解決這個問題,可以在values.yaml的plugins節點,添加現有的默認插件
-
默認插件列表獲取方式:/apisix/admin/plugins/list
插件命名,優先級和其他#
給插件取一個很棒的名字,確定插件的加載優先級,然後在 conf/config.yaml 文件中添加上你的插件名。例如 example-plugin 這個插件, 需要在代碼裏指定插件名稱(名稱是插件的唯一標識,不可重名),在 apisix/plugins/example-plugin.lua 文件中可以看到:
local plugin_name = "example-plugin"
local _M = {
version = 0.1,
priority = 0,
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema,
}
注:新插件的優先級(priority 屬性)不能與現有插件的優先級相同,您可以使用 control API 的 /v1/schema 方法查看所有插件的優先級。另外,同一個階段裏面,優先級 ( priority ) 值大的插件,會優先執行,比如 example-plugin 的優先級是 0,ip-restriction 的優先級是 3000,所以在每個階段,會先執行 ip-restriction 插件,再去執行 example-plugin 插件。這裏的“階段”的定義,參見後續的 確定執行階段 這一節。對於你的插件,建議採用 1 到 99 之間的優先級。
在 conf/config-default.yaml 配置文件中,列出了啓用的插件(都是以插件名指定的):
plugins: # plugin list
- limit-req
- limit-count
- limit-conn
- key-auth
- prometheus
- node-status
- jwt-auth
- zipkin
- ip-restriction
- grpc-transcode
- serverless-pre-function
- serverless-post-function
- openid-connect
- proxy-rewrite
- redirect
...
注:先後順序與執行順序無關。
特別需要注意的是,如果你的插件有新建自己的代碼目錄,那麼就需要修改 Makefile 文件,新增創建文件夾的操作,比如:
$(INSTALL) -d $(INST_LUADIR)/apisix/plugins/skywalking
$(INSTALL) apisix/plugins/skywalking/*.lua $(INST_LUADIR)/apisix/plugins/skywalking/
_M 中還有其他字段會影響到插件的行爲。
local _M = {
...
type = 'auth',
run_policy = 'prefer_route',
}
run_policy 字段可以用來控制插件執行。當這個字段設置成 prefer_route 時,且該插件同時配置在全局和路由級別,那麼只有路由級別的配置生效。
如果你的插件需要跟 consumer 一起使用,需要把 type 設置成 auth。詳情見下文。
配置描述與校驗#
定義插件的配置項,以及對應的 JSON Schema 描述,並完成對 JSON 的校驗,這樣方便對配置的數據規格進行驗證,以確保數據的完整性以及程序的健壯性。同樣,我們以 example-plugin 插件爲例,看看他的配置數據:
{
"example-plugin": {
"i": 1,
"s": "s",
"t": [1]
}
}
我們看下他的 Schema 描述:
local schema = {
type = "object",
properties = {
i = {type = "number", minimum = 0},
s = {type = "string"},
t = {type = "array", minItems = 1},
ip = {type = "string"},
port = {type = "integer"},
},
required = {"i"},
}
這個 schema 定義了一個非負數 i,字符串 s,非空數組 t,和 ip 跟 port。只有 i 是必需的。
同時,需要實現 check_schema(conf) 方法,完成配置參數的合法性校驗。
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
注:項目已經提供了 core.schema.check 公共方法,直接使用即可完成配置參數校驗。
另外,如果插件需要使用一些元數據,可以定義插件的 metadata_schema ,然後就可以通過 Admin API 動態的管理這些元數據了。如:
local metadata_schema = {
type = "object",
properties = {
ikey = {type = "number", minimum = 0},
skey = {type = "string"},
},
required = {"ikey", "skey"},
}
local plugin_name = "example-plugin"
local _M = {
version = 0.1,
priority = 0, -- TODO: add a type field, may be a good idea
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema,
}
你可能之前見過 key-auth 這個插件在它的模塊定義時設置了 type = 'auth'。 當一個插件設置 type = 'auth',說明它是個認證插件。
認證插件需要在執行後選擇對應的 consumer。舉個例子,在 key-auth 插件中,它通過 apikey 請求頭獲取對應的 consumer,然後通過 consumer.attach_consumer 設置它。
爲了跟 consumer 資源一起使用,認證插件需要提供一個 consumer_schema 來檢驗 consumer 資源的 plugins 屬性裏面的配置。
下面是 key-auth 插件的 consumer 配置:
{
"username": "Joe",
"plugins": {
"key-auth": {
"key": "Joe's key"
}
}
}
你在創建 Consumer 時會用到它。
爲了檢驗這個配置,這個插件使用瞭如下的 schema:
local consumer_schema = {
type = "object",
properties = {
key = {type = "string"},
},
required = {"key"},
}
注意 key-auth 的 check_schema(conf) 方法和 example-plugin 的同名方法的區別:
-- key-auth
function _M.check_schema(conf, schema_type)
if schema_type == core.schema.TYPE_CONSUMER then
return core.schema.check(consumer_schema, conf)
else
return core.schema.check(schema, conf)
end
end
-- example-plugin
function _M.check_schema(conf, schema_type)
return core.schema.check(schema, conf)
end
加密存儲字段#
指定參數需要被加密存儲(需要 APISIX 版本不小於 3.1)
有些插件需要將參數加密存儲,比如 basic-auth 插件的 password 參數。這個插件需要在 schema 中指定哪些參數需要被加密存儲。
encrypt_fields = {"password"}
如果是嵌套的參數,比如 error-log-logger 插件的 clickhouse.password 參數,需要用 . 來分隔:
encrypt_fields = {"clickhouse.password"}
目前還不支持:
- 兩層以上的嵌套
- 數組中的字段
通過在 schema 中指定 encrypt_fields = {"password"},可以將參數加密存儲。APISIX 將提供以下功能:
- 通過 Admin API 來新增和更新資源時,對於 encrypt_fields 中聲明的參數,APISIX 會自動加密存儲在 etcd 中
- 通過 Admin API 來獲取資源時,以及在運行插件時,對於 encrypt_fields 中聲明的參數,APISIX 會自動解密
如何開啓該功能?
在 config.yaml 中開啓 data_encryption:
apisix:
data_encryption:
enable: true
keyring:
- edd1c9f0985e76a2
- qeddd145sfvddff4
keyring 是一個數組,可以指定多個 key,APISIX 會按照 keyring 中 key 的順序,依次嘗試用 key 來解密數據(只對在 encrypt_fields 聲明的參數)。如果解密失敗,會嘗試下一個 key,直到解密成功。
如果 keyring 中的 key 都無法解密數據,則使用原始數據。
確定執行階段#
根據業務功能,確定你的插件需要在哪個階段執行。key-auth 是一個認證插件,所以需要在 rewrite 階段執行。在 APISIX,只有認證邏輯可以在 rewrite 階段裏面完成,其他需要在代理到上游之前執行的邏輯都是在 access 階段完成的。
注意:我們不能在 rewrite 和 access 階段調用 ngx.exit、ngx.redirect 或者 core.respond.exit。如果確實需要退出,只需要 return 狀態碼和正文,插件引擎將使用返回的狀態碼和正文進行退出。例子
APISIX 的自定義階段#
除了 OpenResty 的階段,我們還提供額外的階段來滿足特定的目的:
- delayed_body_filter
function _M.delayed_body_filter(conf, ctx)
-- delayed_body_filter 在 body_filter 之後被調用。
-- 它被 tracing 類型插件用來在 body_filter 之後立即結束 span。
end
編寫執行邏輯#
在對應的階段方法裏編寫功能的邏輯代碼,在階段方法中具有 conf 和 ctx 兩個參數,以 limit-conn 插件配置爲例。
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"id": 1,
"plugins": {
"limit-conn": {
"conn": 1,
"burst": 0,
"default_conn_delay": 0.1,
"rejected_code": 503,
"key": "remote_addr"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
conf 參數#
conf 參數是插件的相關配置信息,您可以通過 core.log.warn(core.json.encode(conf)) 將其輸出到 error.log 中進行查看,如下所示:
function _M.access(conf, ctx)
core.log.warn(core.json.encode(conf))
......
end
conf:
{
"rejected_code": 503,
"burst": 0,
"default_conn_delay": 0.1,
"conn": 1,
"key": "remote_addr"
}
ctx 參數#
ctx 參數緩存了請求相關的數據信息,您可以通過 core.log.warn(core.json.encode(ctx, true)) 將其輸出到 error.log 中進行查看,如下所示:
function _M.access(conf, ctx)
core.log.warn(core.json.encode(ctx, true))
......
end
註冊公共接口#
插件可以註冊暴露給公網的接口。以 jwt-auth 插件爲例,這個插件爲了讓客戶端能夠簽名,註冊了 GET /apisix/plugin/jwt/sign 這個接口:
local function gen_token()
-- ...
end
function _M.api()
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
}
}
end
注意,註冊的接口將不會默認暴露,需要使用public-api 插件來暴露它。
註冊控制接口#
如果你只想暴露 API 到 localhost 或內網,你可以通過 Control API 來暴露它。
Take a look at example-plugin plugin:
local function hello()
local args = ngx.req.get_uri_args()
if args["json"] then
return 200, {msg = "world"}
else
return 200, "world\n"
end
end
function _M.control_api()
return {
{
methods = {"GET"},
uris = {"/v1/plugin/example-plugin/hello"},
handler = hello,
}
}
end
如果你沒有改過默認的 control API 配置,這個插件暴露的 GET /v1/plugin/example-plugin/hello API 只有通過 127.0.0.1 才能訪問它。通過以下命令進行測試:
curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello"
查看更多有關 control API 介紹
註冊自定義變量#
我們可以在 APISIX 的許多地方使用變量。例如,在 http-logger 中自定義日誌格式,用它作爲 limit-* 插件的鍵。在某些情況下,內置的變量是不夠的。因此,APISIX 允許開發者在全局範圍內註冊他們的變量,並將它們作爲普通的內置變量使用。
例如,讓我們註冊一個叫做 a6_labels_zone 的變量來獲取路由中 zone 標籤的值。
local core = require "apisix.core"
core.ctx.register_var("a6_labels_zone", function(ctx)
local route = ctx.matched_route and ctx.matched_route.value
if route and route.labels then
return route.labels.zone
end
return nil
end)
此後,任何對 $a6_labels_zone 的獲取操作都會調用註冊的獲取器來獲取數值。
注意,自定義變量不能用於依賴 Nginx 指令的功能,如 access_log_format。
編寫測試用例#
針對功能,完善各種維度的測試用例,對插件做個全方位的測試吧!插件的測試用例,都在 t/plugin 目錄下,可以前去了解。 項目測試框架採用的 test-nginx 。 一個測試用例 .t 文件,通常用 DATA 分割成 序言部分 和 數據部分。這裏我們簡單介紹下數據部分, 也就是真正測試用例的部分,仍然以 key-auth 插件爲例:
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER)
if not ok then
ngx.say(err)
end
ngx.say("done")
}
}
--- request
GET /t
--- response_body
done
--- no_error_log
[error]
一個測試用例主要有三部分內容:
- 程序代碼:Nginx location 的配置內容
- 輸入:http 的 request 信息
- 輸出檢查:status,header,body,error_log 檢查
這裏請求 /t,經過配置文件 location,調用 content_by_lua_block 指令完成 lua 的腳本,最終返回。 用例的斷言是 response_body 返回 "done",no_error_log 表示會對 Nginx 的 error.log 檢查, 必須沒有 ERROR 級別的記錄。
附上 test-nginx 執行流程#
根據我們在 Makefile 裏配置的 PATH,和每一個 .t 文件最前面的一些配置項,框架會組裝成一個完整的 nginx.conf 文件, t/servroot 會被當成 Nginx 的工作目錄,啓動 Nginx 實例。根據測試用例提供的信息,發起 http 請求並檢查 http 的返回項, 包括 http status,http response header,http response body 等。