GateWay Kong
简介
Kong Gateway 是一个轻量级、快速、灵活的云原生 API 网关。Kong Gateway 是一个运行在 Nginx 中的 Lua 应用程序。
Kong 提供了许多**插件**供在网关部署中使用。您还可以创建自己的自定义插件。
三个组件:
- Kong Server :基于nginx的服务器,用来接收API请求。
- Cassandra/PostgreSQL :用来存储操作数据**(无DB模式/DB模式,无DB模式在使用上会有所限制)**。
- Kong dashboard:官方推荐UI管理工具,当然,也可以使用 restful 方式 管理admin api。
管理 admin api
1.通过kong manager界面管理
2.通过restful管理
# 创建服务Service
$ curl -i -X POST --url http://localhost:8001/services/ \
--data 'name=example-service' \
--data 'url=http://localhost:8762/test1/'
# 创建路由Route
$ curl -i -X POST --url http://localhost:8001/services/example-service/routes \
--data 'hosts[]=example.com'
# 通过kong进行api调用
$ curl -i -X GET --url http://localhost:8000/hello \
--header "Host:example.com"
自定义聚合插件 kong-aggregator
注:所聚合的接口返回内容统一以下格式
{
"data": {},
"msg" : "",
"code": ""
}
插件参数
-
service:插件作用的服务名。约束:service和route至少填一个。
-
route:插件作用的路由名。约束:service和route至少填一个。
-
urls(必填):所聚合的API地址列表。约束:数组类型。
-
result_name(必填):期望每个API返回的结果名称。约束:数组类型,必须与url的地址顺序对应。
-
complex_result(非必填):期望返回的数据格式。不填:API返回结果会默认并列显示;填:API结果按照期望结果显示。**注:**此参数要求返回data内容必须是对象,如果不是,例如true,则此参数不生效,此参数一般用于查询接口,增删改的接口不必要使用。
约束:JSON类型,键名必须和result_name一致,否则会有显示问题,填写时只需填写梗概即可。
# 例子 (不包含service和route的创建过程)
#插件
#url:
http://xxx.xxx.xxx.xxx:xxxx/v1/user,http://xxx.xxx.xxx.xxx:xxxx/v1/userContact
#result_name:
user,contacts
#complex_result(仅在查询生效):
{
"user": {
"contacts": {}
}
}
# 请求gateway url:
http://xxx.xxx.xxx.xxx:8000/aggregator/add
# 请求体:之前文档里的params,现直接使用请求体动态传入,要求保持顺序性,json类型,
{
"user": {
"name": "ccc",
"avatar": "321"
},
"contacts": {
"a": "b",
"name": "333"
}
}
#返回示例
{
"msg": "OK",
"data": {
"user": true,
"contacts": true
},
"code": 200
}
Docker部署参考
# 拉取Docker 镜像
$ docker pull kong/kong-gateway:2.6.0.1-alpine
# 标记
$ docker tag kong/kong-gateway:2.6.0.1-alpine kong:2.6.0.1
# 创建自定义 Docker 网络以允许容器相互发现和通信:
$ docker network create kong-net
# 启动PostgreSQL容器
$ docker run -d --name kong-database \
--network=kong-net \
-p 5432:5432 \
-e "POSTGRES_USER=postgres" \
-e "POSTGRES_PASSWORD=postgres" \
-e "POSTGRES_DB=postgres" \
postgres:9.6
# 准备kong数据库
$ docker run --rm \
--network=kong-net \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_PG_PORT=5432" \
-e "KONG_PG_USER=kong" \
-e "KONG_PG_PASSWORD=kong" \
-e "KONG_PG_DATABASE=kong" \
kong:2.6.0.1 kong migrations bootstrap
# 运行以下命令以使用 Kong Gateway 启动容器 *注*:修改{HOSTNAME}
$ docker run -d --name kong \
--network=kong-net \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_PG_USER=kong" \
-e "KONG_PG_PASSWORD=kong" \
-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" \
-e "KONG_ADMIN_GUI_URL=http://{HOSTNAME}:8002" \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
-p 8002:8002 \
-p 8445:8445 \
-p 8003:8003 \
-p 8004:8004 \
kong:2.6.0.1
# 测试/services使用 Admin API访问端点:
$ curl -i -X GET --url http://{HOSTNAME}:8001/services
# 访问控制界面(kong manager):http://{HOSTNAME}:8002
自定义插件部署
# 进入kong容器
$ docker exec -it kong bash
# 添加自定义插件
# 1.修改constant.lua文件,找到local plugins,添加自定义插件名称,"kong-aggregator",如下:
$ vim /usr/local/share/lua/5.1/kong/constants.lua
------------------------------------
local plugins = {
...
...
"external plugins
"azure-functions",
"zipkin",
"kong-aggregator",
-- XXX EE
"application-registration",
}
-----------------------------------
# 2.添加插件源码,需要添加handler.lua(插件核心功能代码)和schema.lua(插件参数配置项)
$ cd /usr/local/share/lua/5.1/kong/plugins
$ mkdir /kong-aggregator # 将lua文件放置此目录夏
# 3.退出容器并重启容器
$ exit
$ docker restart kong
# 插件使用参照上方简述 创建服务->创建路由->添加插件
相关文件
schema.lua
-- 主要涉及插件的入参配置
local cjson = require "cjson.safe"
local Errors = kong.errors
return {
fields = {
urls = { type = "array", required = true },
--params = { type = "string", required = true, default = '[ {"ssl_verify": false, "headers": {"x-hakuna": "matata", "x-foo": "bar" }, "method": "POST", "body": "a=1&b=2" }, {"ssl_verify": false, "headers": {"content-type": "application/json" } } ]' },
--merge_body = { type = "boolean", required = true, default = false },
result_name = { type = "array", required = true },
complex_result = { type = "string", required = false }
},
self_check = function(schema, plugin_t, dao, is_updating)
print("----------------开始------------")
local params = cjson.decode(plugin_t.params)
local complexResult = cjson.decode(plugin_t.complex_result)
local resultName = plugin_t.result_name
if #params == #plugin_t.urls and #params == #resultName then
return true
else
return false, Errors.schema("'complexResult is not required.'params' and 'complex_result' must be a JSON. The number of objects in 'Params' must match the number of objects in 'urls' and 'result_name'")
end
end
}
handle.lua
-- 插件内部逻辑代码
local BasePlugin = require "kong.plugins.base_plugin"
local plugin_name = ({ ... })[1]:match("^kong%.plugins%.([^%.]+)")
local plugin = BasePlugin:extend()
local responses = kong.response
local utils = require "kong.tools.utils"
local meta = require "kong.meta"
local http = require "resty.http"
local cjson = require "cjson.safe"
local ngx = ngx
local resty_http = require 'resty.http'
local pp = require 'pl.pretty'
local re_match = ngx.re.match
local string_upper = string.upper
local table_concat = table.concat
local body = {}
-- 判断table中是否存在某个value
function k_include(tab, value)
for k, v in pairs(tab) do
if k == value then
return true
end
end
return false
end
-- 发送http请求
local function do_request(url, params, name, httpc)
local params_body = {}
if params ~= nil then
params_body = params
end
kong.log("kong.request.get_query()----------------", kong.request.get_query())
local send_body = cjson.encode(params_body[name])
local send_params = {
headers = kong.request.get_headers(),
method = kong.request.get_method(),
body = send_body,
query = kong.request.get_query(),
keepalive = false
}
local res, err = httpc:request_uri(url, send_params)
if err ~= nil then
ngx.log(ngx.ERR, "tem erro")
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
if res == nil then
ngx.log(ngx.ERR, "nao teve response")
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
--and re_match(string_upper(res.headers["content-type"]), '^APPLICATION/JSON')
if res.status == 200 then
body = cjson.decode(res.body)
return nil, res.status, res.body, res.headers
end
return nil, 400, '{"message": "status is not 200 or content-type is not application/json"}', {}
end
-- 自定义返回参数
local function generate_complex_data(table, data, vtable)
for k, v in pairs(vtable) do
table[k] = data[k]
if v ~= nil then
generate_complex_data(table[k], data, v)
end
end
return table
end
local function set_header(header, value)
if not ngx.header[header] then
ngx.header[header] = value
end
end
local function override_header(header, value)
if ngx.header[header] then
ngx.header[header] = nil
end
ngx.header[header] = value
end
-- 返回内容生成
local function generate_body(content, conf)
local bodyJson = {}
local data = {}
local msg = "OK"
local code = 200
local resultName = conf.result_name
for i = 1, #conf.urls do
local resultJson = cjson.decode(content[i])
local currentName = resultName[i]
local resultTable = {}
if resultJson["data"] ~= nil then
resultTable[currentName] = resultJson["data"]
end
if resultJson["code"] ~= 200 then
code = resultJson["code"]
if msg then
msg = currentName .. ":" .. resultJson["msg"]
end
end
data = utils.table_merge(data, resultTable)
end
bodyJson["msg"] = msg
bodyJson["code"] = code
local complexData = conf.complex_result
if complexData ~= nil then
local jsonTable = cjson.decode(complexData)
local flag = true
for k, v in pairs(data) do
if type(v) ~= "table" then
flag = false
end
end
if flag then
data = generate_complex_data(jsonTable, data, jsonTable)
end
end
bodyJson["data"] = data
bodyJson = cjson.encode(bodyJson)
return bodyJson
end
local function send(content, conf)
ngx.status = 200
body = generate_body(content, conf)
set_header('Content-Length', #body)
override_header('Content-Type', 'application/json')
ngx.say(body)
return ngx.exit(ngx.status)
end
-- 打印table内容,debug用
key = ""
function PrintTable(table, level)
level = level or 1
local indent = ""
for i = 1, level do
indent = indent .. " "
end
if key ~= "" then
print(indent .. key .. " " .. "=" .. " " .. "{")
else
print(indent .. "{")
end
key = ""
for k, v in pairs(table) do
if type(v) == "table" then
key = k
PrintTable(v, level + 1)
else
local content = string.format("%s%s = %s", indent .. " ", tostring(k), tostring(v))
print(content)
end
end
print(indent .. "}")
end
function plugin:new()
plugin.super.new(self, plugin_name)
end
function split(inputString, sep)
if sep == nil then
sep = "%s"
end
local t = {};
i = 1
for str in string.gmatch(inputString, "([^" .. sep .. "]+)") do
t[i] = str
i = i + 1
end
return t
end
--方法集成
function plugin:access(conf)
plugin.super.access(self)
local upstream_uri = ngx.var.upstream_uri == "/" and "" or ngx.var.upstream_uri
local params = cjson.decode(kong.request.get_raw_body())
if params == nil then
params = cjson.decode("[]")
end
local err = {}
local status = {}
local content = {}
local headers = {}
local resultName = conf.result_name
local httpc = resty_http:new()
for i = 1, #conf.urls do
err[i], status[i], content[i], headers[i] = do_request(conf.urls[i] .. upstream_uri, params, resultName[i], httpc)
end
return send(content, conf)
end
plugin.PRIORITY = 750
plugin.VERSION = "0.1-1"
return plugin
)
end
local err = {}
local status = {}
local content = {}
local headers = {}
local resultName = conf.result_name
local httpc = resty_http:new()
for i = 1, #conf.urls do
err[i], status[i], content[i], headers[i] = do_request(conf.urls[i] .. upstream_uri, params, resultName[i], httpc)
end
return send(content, conf)
end
plugin.PRIORITY = 750
plugin.VERSION = "0.1-1"
return plugin