API网关Kong实践笔记

Kong插件开发向导

2019-10-03  本文已影响0人  fossilman

简介

在进一步讨论之前,这里先有必要简要阐述一下 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

高级插件模块

有些插件与 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的执行生命周期的各个切点实现自定义逻辑:

方法名 段信息 描述
: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 最后一个响应字节发送到客户端时执行
方法名 段信息 描述
: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 中所有的实体可以表现为:

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 段中执行非破坏性操作(例如创建新表、添加新纪录);在 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 = 123key = "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 支持以下标量类型:stringintegernumberboolean;还支持以下复合类型:arrayrecordset,除此之外,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_atupdated_at,该字段将在插入/更新时填充当前时间
reference string 当 type 是 foreign 时是必须的
on_delete string 当 type 是 foreign 时,定义了外键删除的逻辑,在 Cassandra 中,这是用纯 Lua 代码处理的,但在 PostgreSQL 中,在迁移时要将引用声明为 <font ON DELETE CASCADE/NULL/RESTRICT`

需要了解表结构的更多信息,可以参考:

自定义 DAO

schema 不直接和数据库交互,DAO层通过 kong.db 接口与数据库交互

查询实体

local  entity,  err,  err_t  =  kong.db.<name>:select(primary_key)

在数据库中查询实体并返回,可能会有三种结果:

示例:

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

假设没有发生错误,返回的实体将包含自动填充的字段,如 idcreated_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>)

upsertinsertupdate 的结合:

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.plugins.<plugin_name>.daos

缓存自定义实体

用户可以使用插件开发工具包提供的 kong.cache 模块将自定义实体缓存在内存中:

local cache = kong.cache

缓存有两层:

  1. L1: Lua 缓存 - Nginx worker 进程中,可以存储任何类型的 Lua 值
  2. L2: 共享缓存(SHM)- Nginx 节点中,在 worker 进程中共享,只能保存标量值,更复杂的类型比如 table,需要序列化

从数据库提取数据后,数据会同时存储在两个缓存中,如果同一个 worker 进程再次请求数据,Kong会从 Lua 缓存中检索之前反序列化的数据;如果同一个节点的另一个 worker 进程请求该数据,Kong 会从 SHM 中找到该数据,并对其反序列化(存储在当前进程的缓存中),然后将其返回
该模块公开以下方法:

方法名 描述
value, err = cache:get(key, opts?, cb, ...) 从缓存中检索值,如果缓存没有值(未命中),则在保护模式下调用 cbcb 会且仅会返回一个缓存的值,这个方法也会抛出错误,这些错误会被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,
       ...
     }
  },
  ...
}

其中:

  1. before 键是可选的,可以定义一个方法,如果存在,则在调用任何其他方法之前,都会执行这个方法
  2. 可以使用 HTTP 名称(如 GETPUT)作为索引,当匹配对应的路径和 HTTP 方法时,将执行索引对应的方法,如果在路径上存在 before 方法,则首先执行该方法,注意,before 方法可以使用 kong.response.exit 提前退出,这样可以跳过原有的 HTTP 方法
  3. 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 方法:

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 路径定义了三个方法:

测试用例

简介

如果用户认真对待自己的插件,需要为它编写测试用例,单元测试 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部分:

  1. 通过 LuaRocks 创建 rock
    .rock 文件是一个自包含的软件包,可以在本地安装,也可以从远程服务器安装,如果用户的系统中安装了 LuaRocks,可以在 LuaRocks 树中安装 rock,安装指令如下:
luarocks install <rock-filename>
  1. 如果用户的系统中已经安装了 luarocks,用户可以将当前目录修改为插件存档的目录,其中 rockspec 文件是:
cd <plugin-name>
luarocks make

这将在用户系统中的 LuaRocks 树中安装 kong/plugins/<plugin-name> 的源文件

  1. 一个更保险的安装方式是避免污染 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个步骤:

  1. 先从 Kong 的服务或路由配置中删除插件,确保该插件不再作用域全局,也不作用任何服务、路由或消费者,对于整个 Kong 集群,只需执行一次整个操作,不需要执行 restart 或 reload 指令,这个步骤只是让集群不再使用该插件,但它仍然可以再被启用
  2. plugins 指令删除插件,确保在执行此操作之前已完成步骤1,在此步骤之后,任何人都不能将插件重新应用在服务、路由、消费者或者全局中,此步骤需要执行 restart 或 reload 指令才能生效
  3. 要彻底删除插件,要在每个 Kong 节点删除与插件相关的文件,在删除文件之前,确保已完成步骤2,包括重启 Kong,如果用户之前使用 LuaRocks 安装插件,可以使用 luarocks remove <plugin-name> 指令来删除

发布插件

首选的方法是使用 LuaRocks,Lua 模块的包管理器,我们称这些模块是 rock,用户不必将模块存储在 Kong 的仓库中,如果用户希望维持Kong的设置,则需要这样做
通过在 rockspec 文件中定义模块(及其依赖项),用户可以通过 LuaRocks 在平台上安装模块,用户也可以使用 LuaRocks 上传模块给其他人使用

排除故障

由于以下几个原因,配置错误的自定义插件可能无法启动:

上一篇下一篇

猜你喜欢

热点阅读