Kong插件开发指南

What is Kong

OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
Kong 核心基于OpenResty构建, 拥有强大的插件扩展功能, 可通过Restful API对API,Service,Upstream等进行管理

本文将讲述如何编写kong插件

编写插件

插件目录结构

Kong所有的插件都位于kong/plugins目录下, 如下在plugins目录我们创建一个名为custom-plugin, 目录结构及推荐命名如下

custom-plugin
├── api.lua            # 用于扩展Admin API 
├── daos.lua           # 数据访问层
├── handler.lua        # (必需)包含请求的生命周期, 提供接口来实现插件逻辑
├── migrations         # 插件的表结构定义语句
│   ├── cassandra.lua  
│   └── postgres.lua
└── schema.lua         # (必需)插件配置参数定义, 可加入自定义校验函数

插件入口文件

下面我们来看插件的入口文件handler.lua, 你可以重写以下方法用以在kong的生命周期内执行想要的自定义逻辑;

-- 继承BasePlugin
local BasePlugin = require "kong.plugins.base_plugin"
local CustomHandler = BasePlugin:extend()

-- 插件构造函数
function CustomHandler:new()
  CustomHandler.super.new(self, "my-custom-plugin")
end

function CustomHandler:init_worker()
  CustomHandler.super.init_worker(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:certificate(config)
  CustomHandler.super.certificate(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:rewrite(config)
  CustomHandler.super.rewrite(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:access(config)
  CustomHandler.super.access(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:header_filter(config)
  CustomHandler.super.header_filter(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:body_filter(config)
  CustomHandler.super.body_filter(self)
  -- 在这里实现自定义的逻辑
end

function CustomHandler:log(config)
  CustomHandler.super.log(self)
  -- 在这里实现自定义的逻辑
end

return CustomHandler

如下表格, 为kong插件中支持重写的函数列表

函数名 Lua-Nginx-Module 上下文 描述
:init_worker() init_worker_by_lua 在每个Nginx Worker启动时执行
:certificate() ssl_certificate_by_lua_block 在SSL握手的SSL证书服务阶段执行
:rewrite() rewrite_by_lua_block 每个请求中的rewrite阶段执行
:access() access_by_lua 在被代理至上游服务前执行
:header_filter() header_filter_by_lua 从上游服务器接收所有Response headers后执行
:body_filter() body_filter_by_lua 从上游服务接收的响应主体的每个块时执行。 由于响应被流回客户端,因此它可以超过缓冲区大小并按块进行流式传输。 因此,如果响应很大,则会多次调用此方法
:log() log_by_lua 当最后一个响应字节输出完毕时执行

另外还有一点, 插件的执行有时是依赖于特定的顺序的, 比如认证的插件应当先于部分业务插件执行, 可以通过CustomHandler.PRIORITY = 10在handler.lua中配置

插件配置

-- schema.lua
return {
  no_consumer = true, -- this plugin will only be applied to Services or Routes,
  fields = {
    kafka_brokers = {type="array"},
    kafka_topic = {type = "string"}
  },
  self_check = function(schema, plugin_t, dao, is_updating)
    -- 自定义的验证函数
    return true
  end
}
  • no_consumer: 如果为true, 则插件只能被应用于Service和Routes
  • fields: 一个field数组, field中可定义type,required,unique,default,immutable,enum,regex 等属性
  • self_check: 在安装时, 执行的自定义校验函数

PDK(Plugin Development Kit)

kong 的插件开发套件包含了一些常用的Lua函数和变量, 如kong.client,kong.request,kong.log,kong.table 等

数据访问

Kong自身存储可选PostgreSQL和Cassandra, 在开发插件时, kong提供了一个数据库抽象层用于存储自定义的实体, 也就是dao层.

要完成数据访问, 需要两步:

  1. 编写Migration文件, 用于数据库DDL操作, 在kong migrations up时执行
  2. 编写daos.lua, 用于映射你的数据表

如下为PostgreSQL示例:

-- 步骤一:postgres.lua
return {
  {
    name = "2015-07-31-172400_init_keyauth",
    up = [[
      CREATE TABLE IF NOT EXISTS keyauth_credentials(
        id uuid,
        consumer_id uuid REFERENCES consumers (id) ON DELETE CASCADE,
        key text UNIQUE,
        created_at timestamp without time zone default (CURRENT_TIMESTAMP(0) at time zone 'utc'),
        PRIMARY KEY (id)
      );
    ]],
    down = [[
      DROP TABLE keyauth_credentials;
    ]]
  },
  .....
}

postgres.lua 由一个Migration数组组成, 每个Migration包含三个字段:name,up,down, 其中name须唯一, up和down分别代表升级和降级时执行的SQL语句.

-- 步骤二:daos.lua
local SCHEMA = {
  primary_key = {"id"},
  table = "keyauth_credentials", -- 数据库表名
  fields = {
    id = {type = "id", dao_insert_value = true}, 
    created_at = {type = "timestamp", immutable = true, dao_insert_value = true}, 
    consumer_id = {type = "id", required = true, foreign = "consumers:id"}, 
    key = {type = "string", required = false, unique = true} 
  }
}

return {keyauth_credentials = SCHEMA}  --keyauth_credentials即为在DAO中加入的自定义schema

完成以上两步后, 即可通过如下所示代码在我们的插件中对数据库进行操作

local singletons = require "kong.singletons"
singletons.dao.keyauth_credentials.find_all({key="ravenzz"})
singletons.dao.keyauth_credentials.insert(...)
singletons.dao.keyauth_credentials.update(...)
....

缓存

作为网关代理层, 缓存一定是必不可少的一环, 在kong的PDK中, 封装了lua-resty-mlcache.
kong的缓存分为两级:

  • L1: Lua memory cache - 在nginx worker中共享, 可以存储任何Lua值
  • L2: Shared memory cache (SHM) - 在nginx node的所有worker中共享. 可以存储任何标量值, 但它需要序列化和反序列化, 所以性能会有所下降

注: 从数据库中提取数据后,它将同时存储于上述两级缓存中。现在,如果同一个工作进程再次请求数据,它将从Lua内存缓存检索数据。 如果同一个Nginx节点中的不同工作者请求该数据,它将在SHM中找到数据,对其进行反序列化(并将其存储在自己的Lua内存缓存中),然后将其返回。

一个典型的使用方式如下:

-- 通过一个唯一值获取Cache Key
cache_key = singletons.dao.keyauth_credentials:cache_key("api_key")
value, err = singletons.cache:get(cache_key ,nil, load_from_db_func, ....)

函数: value, err = cache:get(key, opts?, cb, ...) , 如果cache没有值(miss), 则会调用函数cb, cb 必须返回一个返回值, 可返回需要缓存的值或者nil, 需要注意的是返回值为nil时, get函数依旧会执行缓存, 但我们可以通过cache:get()的第二个参数控制缓存的TTL和negative TTL. 如下代码即代表有数据时, 我们缓存600s, 没有数据时, 缓存nil结果40s

value, err = singletons.cache:get(cache_key ,{
            ttl = 600, --如果有数据, 则缓存的时间, 单位:秒
            neg_ttl = 40 -- 如果没有数据, 单位:秒
        }, load_from_db_func, ....)

通过上述API开发者可轻松实现缓存懒加载功能

Docker化

这里没有用kong官方提供的image, 而是通过源码安装方式自定义安装kong, 将修改的代码放入custom目录, 编译时, 将custom目录覆盖至kong目录, 这样做的好处在于无需修改kong目录的代码, 可轻松保持kong为最新社区版本

MY-PROXY
├── custom         # 自定义的代码及配置(包含插件), 用于覆盖kong的代码及配置文件
├── kong           # git clone [email protected]:Kong/kong.git

修改后的Dockerfile如下

ARG RESTY_IMAGE_BASE="centos"
ARG RESTY_IMAGE_TAG="7"

FROM ${RESTY_IMAGE_BASE}:${RESTY_IMAGE_TAG}

LABEL maintainer="RavenZZ "

RUN yum update -y 
RUN yum install -y pcre-devel openssl openssl-devel gcc curl perl make unzip gettext git

ARG RESTY_VERSION="1.13.6.2"
ARG RESTY_LUAROCKS_VERSION="2.4.4"

# 安装OpenResty和LuaRocks
RUN cd /tmp \
    && curl -LO https://openresty.org/download/openresty-${RESTY_VERSION}.tar.gz \
    && tar -xf openresty-${RESTY_VERSION}.tar.gz \
    && cd /tmp/openresty-${RESTY_VERSION} \
    && ./configure \
    --prefix=/usr/local/openresty \
    --with-pcre-jit \
    --with-http_ssl_module \
    --with-http_realip_module \
    --with-http_stub_status_module \
    --with-http_v2_module \
    -j2 \
    && make -j2 \
    && make install \
    && curl -fSL https://github.com/luarocks/luarocks/archive/${RESTY_LUAROCKS_VERSION}.tar.gz -o luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
    && tar xzf luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
    && cd luarocks-${RESTY_LUAROCKS_VERSION} \
    && ./configure \
    --prefix=/usr/local/openresty/luajit \
    --with-lua=/usr/local/openresty/luajit \
    --lua-suffix=jit-2.1.0-beta3 \
    --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1 \
    && make build \
    && make install \
    && cd /tmp \
    && rm -rf luarocks-${RESTY_LUAROCKS_VERSION} luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz  \
    && rm -rf /tmp/openresty-${RESTY_VERSION} /tmp/openresty-${RESTY_VERSION}.tar.gz

ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
ENV LUA_PATH="/usr/local/openresty/site/lualib/?.ljbc;/usr/local/openresty/site/lualib/?/init.ljbc;/usr/local/openresty/lualib/?.ljbc;/usr/local/openresty/lualib/?/init.ljbc;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/?/init.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua"
ENV LUA_CPATH="/usr/local/openresty/site/lualib/?.so;/usr/local/openresty/lualib/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so"

# 编译kong
COPY kong /kong
RUN cd /kong \
    && make install 


# 使用custom覆盖/kong目录后, 再次编译
COPY custom /kong
RUN cd /kong \
    && make install 

ENV PATH=$PATH:/kong/bin

RUN ln -sf /dev/stdout /usr/local/openresty/nginx/logs/access.log \
    && ln -sf /dev/stderr /usr/local/openresty/nginx/logs/error.log

COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
EXPOSE 8000 8443 8001 8444
STOPSIGNAL SIGTERM

CMD ["kong", "docker-start"]

运行

在首次运行或更新代码时, 执行kong migrations up 对数据库进行DDL操作, 完成后启动

# 执行Migration
docker run --rm \
    --network=kong-net \
    -e "KONG_DATABASE=postgres" \
    -e "KONG_PG_HOST=xxxxxxx" \
    -e "POSTGRES_DB=kong" \
    -e "KONG_PG_USER=kong_user" \
    -e "KONG_PG_PASSWORD=kong_pwd" \
    ravenzz/kong:latest kong migrations --vv up

# 启动kong
docker run -d --rm --name kong \
    --network=kong-net \
    -e "KONG_DATABASE=postgres" \
    -e "KONG_PG_HOST=xxxxxxx" \
    -e "KONG_PG_USER=kong_user" \
    -e "KONG_PG_PASSWORD=kong_pwd" \
    -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
    -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
    -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
    -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
    -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
    -p 8000:8000 \
    -p 8443:8443 \
    -p 8001:8001 \
    -p 8444:8444 \
    ravenzz/kong:latest

在生产环境时, 若使用docker部署, 避免对性能产生影响, --network参数需设置为host

Kong GUI

Konga
kong-dashboard

参考资料

  • Kong Plugin Development
  • Docker OpenResty
  • Docker kong
  • Github kong
  • lua-resty-mlcache

你可能感兴趣的:(Kong插件开发指南)