手把手教你实现一个Kong网关插件

前言

Kong Gateway 是一个轻量、快速、灵活的基于Nginx开发云原生 API 网关。在云原生领域,Kong Gateway 越来受欢迎。

Kong提供了插件化能力,在对后台业务服务代码无侵入的条件下,可以在接入层方便地引入认证鉴权、安全防护、流量控制都能功能。这也是其受欢迎的原因之一。

Kong Gateway 官方已经提供了一系列常用的插件,但是业务开发中有时需要定制自己的插件。本文将介绍如何编写 Kong 的自定义插件,以及如何将插件集成到 Kong 网关中

搭建开发环境

1. 自定义Docker网络

docker network create kong-net

2. 启动 PostgreSQL 数据库容器

docker run -d --name kong-database \
  --network=kong-net \
  -p 5432:5432 \
  -e "POSTGRES_USER=kong" \
  -e "POSTGRES_DB=kong" \
  -e "POSTGRES_PASSWORD=kongpass" \
  -v /data/home/windealli/workspace/kong/postgres/data:/var/lib/postgresql/data \
  postgres:13

3. 准备数据库

docker run --rm --network=kong-net \
 -e "KONG_DATABASE=postgres" \
 -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_PASSWORD=test" \
kong/kong-gateway:3.4.0.0 kong migrations bootstrap

4. 启动Kong网关容器

网关容器Kong-Gateway的启动包含三个步骤

  • 第一步: 不容目录映射启动容器
  • 第二步: 拷贝关键配置目录
  • 第三步: 删除就容器,使用目录映射启动新容器。

第三步是为了方便后面配置我们自定义的插件,第一、二步是服务与第三步的,否则容器无法启动。

1) 不带目录映射启动容器

docker run -d --name kong-gateway \
 --network=kong-net \
 -e "KONG_DATABASE=postgres" \
 -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_USER=kong" \
 -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
 -e "KONG_ADMIN_ACCE_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://localhost:8002" \
 -e "KONG_PLUGIN_PATH=/kong/plugins/?.lua;;" \
 -e "KONG_LUA_PACKAGE_PATH=/kong/plugins/my-first-plugin/?.lua;;" \
 -e "KONG_PLUGINS=bundled,my-first-plugin" \
 -e "KONG_CUSTOM_PLUGINS=my-first-plugin" \
 -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
 -e "KONG_LOG_LEVEL=debug" \
 -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/kong-gateway:3.4.0.0

2) docker cp 把容器中需要修改的配置文件拷贝到宿主机

mkdir etc lua_kong
docker cp ${container_id}:/etc/kong etc/kong # ${container_id} 换成容器ID,下同
docker cp ${container_id}:/usr/local/share/lua/5.1/kong lua_kong/kong  # 

3)使用目录映射,重新启动Kong-Gateway容器

先删除旧容器

docker ps -a # 得到容器ID
docker stop ${container_id}
docker rm ${container_id}

启动带目录映射的Kong-gateway新容器

docker run -d --name kong-gateway \
 --network=kong-net \
 -e "KONG_DATABASE=postgres" \
 -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_USER=kong" \
 -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
 -e "KONG_ADMIN_ACCE_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://localhost:8002" \
 -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
 -e "KONG_LOG_LEVEL=info" \
 -v "/data/home/windealli/workspace/kong/my-plugins:/kong/plugins" \
 -v "/data/home/windealli/workspace/kong/etc/kong:/etc/kong" \
 -v "/data/home/windealli/workspace/kong/lua_kong/kong:/usr/local/share/lua/5.1/kong" \
 -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/kong-gateway:3.4.0.0

5. 安装UI管理工具Konga

docker run -p 1337:1337 \
   --network=kong-net \
   --name=konga \
   -e "NODE_ENV=development" \
   pantsel/konga

安装后访问http://localhost:1337 即可访问konga

第一次访问需要创建用户。 创建完后登录。

之后需要配置与Kong-Gateway的连接,注意因为konga和kong-gateway在不同容器中,因此配置http://locolhost:8001 是无法连接的,需要使用本机的IP。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果连接建立成功,可以看到如下控制台。

在这里插入图片描述

编写插件

Kong网关插件由 Lua 模块组成,Kong网关提供了一套用于插件开发的**Plugin Development Kit**(PDK) ,通过PDK可以与请求/响应对象或流进行交互,实现任意逻辑。PDK 是一组 Lua 函数。

插件的文件结构

Kong插件需要以特定的文件结构组织。

一个最简单的插件必须包含下面两个文件:

  • handler.lua: 插件的核心。它是一个要实现的接口,其中每个函数将在请求/连接生命周期中的所需时刻运行。
  • schema.lua: 定义插件的运行规则,定义插件所需要的配置结构(以便用户在使用时配置)
[my-first-plugin]$ tree
.
├── handler.lua  # 插件的核心,逻辑实现。
└── schema.lua   # 定义配置结构和运行规则。

如果需要与Kong进行更深入的交互,实现更加复杂的插件。比如需要与数据库交互,需要公开管理API的endpoint。 这时就需要更加复杂的文件结构。

complete-plugin
├── api.lua        # 定义管理 API 中可用的端点列表
├── daos.lua       # 定义 DAO(数据库访问对象)列表
├── handler.lua    # 要实现的接口。
├── migrations     # 数据库迁移(例如创建表)
│   ├── init.lua
│   └── 000_base_complete_plugin.lua
└── schema.lua     # 保存插件配置的架构,

本文主要介绍简单插件开发和调试流程,高级插件的开发后面有空会另开一文介绍。

生命周期和切入点

本文只介绍HTTP请求的生命周期和切入点。

Kong中的**HTTP Module** 定义了如下接口用于为HTTP请求编写插件

函数名语义适用协议描述
init_workerhttps://github.com/openresty/lua-nginx-module#init_worker_by_lua_block*Nginx worker进程启动时执行
certificatehttps://github.com/openresty/lua-nginx-module#ssl_certificate_by_lua_blockhttps, grpcs, wssSSL握手时执行
rewritehttps://github.com/openresty/lua-nginx-module#rewrite_by_lua_block*用于在Kong收到客户端请求时的重写阶段执行。在此阶段Server和Consumer还没被识别,因此只能用于全局插件。
accesshttps://github.com/openresty/lua-nginx-module#access_by_lua_blockhttp(s), grpc(s), ws(s)在收到请求后,转发给upstream前执行.
ws_handshakehttps://github.com/openresty/lua-nginx-module#access_by_lua_blockws(s)在每个WebSocket请求完成 WebSocket 握手前执行.
responsehttps://github.com/openresty/lua-nginx-module#access_by_lua_blockhttp(s), grpc(s)从upstream收到相应后,转发给客户端前执行。
header_filterhttps://github.com/openresty/lua-nginx-module#header_filter_by_lua_blockhttp(s), grpc(s)从upstream收到所有应答header时执行。
ws_client_framehttps://github.com/openresty/lua-nginx-module#content_by_lua_blockws(s)从客户端收到WebSocket Message时执行。
ws_upstream_framehttps://github.com/openresty/lua-nginx-module#content_by_lua_blockws(s)从upstream收到WebSocket Message时执行。
body_filterhttps://github.com/openresty/lua-nginx-module#body_filter_by_lua_blockhttp(s), grpc(s)每收到upstream应答的chunk时执行. 由于响应被流式传输回客户端,因此它可能会超出缓冲区大小并被逐块流式传输。 这个函数在一个请求中可能会被多次执行。
loghttps://github.com/openresty/lua-nginx-module#log_by_lua_blockhttp(s), grpc(s)最后一个response返回给客户端后执行。
ws_closehttps://github.com/openresty/lua-nginx-module#log_by_lua_blockws(s)WebSocket关闭时执行。

简单插件的编码示例

这里开发一个不带配置的建议插件,并在HTTP请求常见切入点做一些处理逻辑。

目录结构:

my-plugins/
└── my-first-plugin
    ├── handler.lua
    ├── schema.lua

schema.lua:

//schema.lua
local typedefs = require "kong.db.schema.typedefs"
  

local schema = {
    name = "my-first-plugin",
    fields = {
        {
            consumer = typedefs.no_consumer,
        },
        {
            -- 插件只在nginx http模块中生效
            protocols = typedefs.protocols_http,
        },
        {
            config = {
                type = "record",
                fields = {
               },
            },
        },
    },
}

return schema

handler.lua:

//handler.lua
local MyFirstHandler = {
    -- 插件的优先级,决定了插件的执行顺序;数字越大,优先级越高,越早执行
    PRIORITY = 1101,
    -- 插件的版本号
    VERSION = "0.1.0-1",
}

-- 在Nginx worker启动时执行
function MyFirstHandler:init_worker()
        kong.log("data:init_worker") -- 用来确认是否加载成功的日志
end

-- 收到请求,还没进入server处理时执行, 
-- 此处判断路径如果不是/sayHello和/sayBye直接返回字符串"only support /sayHello and /sayBye"
function MyFirstHandler:rewrite()
        kong.log("MyFirstHandler:rewrite")
        local rawPath = kong.request.get_raw_path() -- 使用PDK获取请求URL
        kong.log("rewrite rawpath: " .. rawPath)
        if rawPath ~= "/sayHello" and rawPath ~= "/sayBye" then
                kong.log("not support rawPath: " .. rawPath)
                return kong.response.exit(404, "only support /sayHello and /sayBye")
        end
        kong.log("rewrite finish")
end

function MyFirstHandler:access()
        kong.log("access")
        kong.service.request.set_header("req-key", "plugin-header-value")
end

-- 注意,即使rewrite中使用了kong.response.exit, 这里也会执行
function MyFirstHandler:header_filter()
        kong.log("header_filter")
        local header = kong.service.response.get_header("rsp-key")
        if header ~= nil then
          kong.log(header)
          kong.response.set_header("rsp-key", header .. " modify by plugin")
        end
end

function MyFirstHandler:body_filter()
        kong.log("body_filter")
end

return MyFirstHandler

测试验证

测试准备:upstream服务Demo

这里使用python的flask框架搭建了一个web服务,接受两个路由 sayHellosayByes

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json

from flask import Flask, request

app = Flask(__name__)

@app.route("/sayHello")
def sayHello():
    req_header = request.headers.get("req-key")
    print(req_header)

    name = request.args.get("name")
    return "Hello " + name, 200, [('rsp-key', 'rsp-head-by-server')]

@app.route("/sayBye")
def sayBye():
    req_header = request.headers.get("req-key")
    print(req_header)
    name = request.args.get("name")
    return "Bye " + name, 200, [('rsp-key', 'rsp-head-by-server')]

app.run(host="0.0.0.0", port=5000, debug=True)

并未这个服务配置 upstream、server、router

确保在未配置插件是,可以正常通过 http://{kong网关IP}:8000/sayHello 访问

安装插件

新建/修改 /etc/kong/kong.conf

[windealli@VM-52-29-tencentos kong]$ cat etc/kong/kong.conf
plugins = bundled,my-first-plugin  # 指定了要加载的插件
lua_package_path = /kong/plugins/?.lua;; # 指定了自定义插件的目录
[windealli@VM-52-29-tencentos kong]$

修改 lua_kong/kong/constants.lua

如果不修改constants.lua,会出现konga的info页面可以看到插件my-first-plugin,但是在添加插件页面却找不到。

# 在plugins中添加我们的插件
local plugins = {
  "my-first-plugin",
	...
}

使用插件

直接将插件配置到全局

在这里插入图片描述

验证

1) 验证rewrite

在这里插入图片描述

  1. 验证完整链路

在这里插入图片描述

查看服务端日志打印了插件添加的header

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值