apisix~自定義外掛的部署

张占岭發表於2024-05-13

參考

  • 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 提供了兩種方式來新增新的功能。

  1. 修改 Apache APISIX 的原始碼並重新發布 (不推薦)。
  2. 配置 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
  • 覆蓋原有外掛的問題
    https://docs.api7.ai/apisix/how-to-guide/custom-plugins/create-plugin-in-lua

  • 解決這個問題,可以在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"}

目前還不支援:

  1. 兩層以上的巢狀
  2. 陣列中的欄位

透過在 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 等。

相關文章