APISIX
Apache APISIX 是 Apache 软件基金会下的云原生 API 网关,它具有动态、实时、高性能等特点,提供了负载均衡、动态上游、灰度发布(金丝雀发布)、服务熔断、限速、防御恶意攻击、身份认证、可观测性等丰富的流量管理功能。我们可以使用 Apache APISIX 来处理传统的南北向流量,也可以处理服务间的东西向流量。同时,它也支持作为 Kubernetes Ingress Controller 来使用。
APISIX 基于 Nginx 和 etcd,与传统 API 网关相比,APISIX 具有动态路由和热加载插件功能,避免了配置之后的 reload 操作,同时 APISIX 支持 HTTP(S)、HTTP2、Dubbo、QUIC、MQTT、TCP/UDP 等更多的协议。而且还内置了 Dashboard,提供强大而灵活的界面。同样也提供了丰富的插件支持功能,而且还可以让用户自定义插件。
主要具有以下几个特点:
- 多平台支持:APISIX 提供了多平台解决方案,它不但支持裸机运行,也支持在 Kubernetes 中使用,还支持与 AWS Lambda、Azure Function、Lua 函数和 Apache OpenWhisk 等云服务集成。
- 全动态能力:APISIX 支持热加载,这意味着你不需要重启服务就可以更新 APISIX 的配置。请访问为什么 Apache APISIX 选择 Nginx + Lua 这个技术栈?以了解实现原理。
- 精细化路由:APISIX 支持使用 NGINX 内置变量做为路由的匹配条件,你可以自定义匹配函数来过滤请求,匹配路由。
- 运维友好:APISIX 支持与以下工具和平台集成:HashiCorp Vault、Zipkin、Apache SkyWalking、Consul、Nacos、Eureka。通过 APISIX Dashboard,运维人员可以通过友好且直观的 UI 配置 APISIX。
- 多语言插件支持:APISIX 支持多种开发语言进行插件开发,开发人员可以选择擅长语言的 SDK 开发自定义插件。
安装 APISIX
为了简单,我们这里可以直接在本地使用 docker 方式来启动 APISIX,首先 Clone 官方提供的 apisix-docker
仓库:
➜ git clone https://github.com/apache/apisix-docker.git
➜ cd apisix-docker
在项目根目录下面的 example
目录中有启动 APISIX 的 docker-compose 配置文件,如下所示:
version: "3"
services:
apisix-dashboard:
image: apache/apisix-dashboard:3.0.0-alpine
restart: always
volumes:
- ./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml
ports:
- "9000:9000"
networks:
apisix:
apisix:
image: apache/apisix:3.2.0-debian
restart: always
volumes:
- ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
depends_on:
- etcd
ports:
- "9180:9180/tcp"
- "9080:9080/tcp"
- "9091:9091/tcp"
- "9443:9443/tcp"
- "9092:9092/tcp"
networks:
apisix:
etcd:
image: rancher/coreos-etcd:v3.4.15-arm64
user: root
restart: always
volumes:
- ./etcd_data:/etcd-data
environment:
ETCD_UNSUPPORTED_ARCH: "arm64"
ETCD_ENABLE_V2: "true"
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_DATA_DIR: "/etcd-data"
ports:
- "2379:2379/tcp"
networks:
apisix:
web1:
image: nginx:1.19.10-alpine
restart: always
volumes:
- ./upstream/web1.conf:/etc/nginx/nginx.conf
ports:
- "9081:80/tcp"
environment:
- NGINX_PORT=80
networks:
apisix:
web2:
image: nginx:1.19.10-alpine
restart: always
volumes:
- ./upstream/web2.conf:/etc/nginx/nginx.conf
ports:
- "9082:80/tcp"
environment:
- NGINX_PORT=80
networks:
apisix:
networks:
apisix:
driver: bridge
该 compose 里面主要包含了 APISIX 的三个容器:apisix、etcd 以及 apisix-dashboard。现在我们就可以使用 docker-compose 来进行一键启动:
➜ docker-compose -f docker-compose.yml up -d
另外两个 nginx 容器是用于测试的:
请确保其他系统进程没有占用 9000、9080、9091、9092、9180、9443 和 2379 端口。如果启动有错误,可以尝试为 examples 目录设置成 777 权限,保证 etcd 数据有权限写入。
当 APISIX 启动完成后我们就可以通过 curl 来访问正在运行的 APISIX 实例。比如,可以发送一个简单的 HTTP 请求来验证 APISIX 运行状态是否正常。
➜ curl "http://127.0.0.1:9080" --head
HTTP/1.1 404 Not Found
Date: Tue, 21 Mar 2023 07:38:45 GMT
Content-Type: text/plain; charset=utf-8
Connection: keep-alive
Server: APISIX/3.2.0
现在,你已经成功安装并运行了 APISIX !
功能测试
接下来我们来了解下 APISIX 的一些功能。在了解之前我们需要对 APISIX 的几个主要概念和组件简单了解下:
上游
Upstream 也称为上游,上游是对虚拟主机的抽象,即应用层服务或节点的抽象。
上游的作用是按照配置规则对服务节点进行负载均衡,它的地址信息可以直接配置到路由或服务上。当多个路由或服务引用同一个上游时,可以通过创建上游对象,在路由或服务中使用上游的 ID 方式引用上游,减轻维护压力。
路由
Route 也称为路由,是 APISIX 中最基础和最核心的资源对象。
APISIX 可以通过路由定义规则来匹配客户端请求,根据匹配结果加载并执行相应的插件,最后把请求转发给到指定的上游服务。路由中主要包含三部分内容:匹配规则、插件配置和上游信息。
服务
Service 也称为服务,是某类 API 的抽象(也可以理解为一组 Route 的抽象)。它通常与上游服务抽象是一一对应的,Route 与 Service 之间,通常是 N:1
的关系。
消费者
Consumer 是某类服务的消费者,需要与用户认证配合才可以使用。当不同的消费者请求同一个 API 时,APISIX 会根据当前请求的用户信息,对应不同的 Plugin 或 Upstream 配置。如果 Route、Service、Consumer 和 Plugin Config 都绑定了相同的插件,只有消费者的插件配置会生效。插件配置的优先级由高到低的顺序是:Consumer > Route > Plugin Config > Service。
对于 API 网关而言,一般情况可以通过请求域名、客户端 IP 地址等字段识别到某类请求方,然后进行插件过滤并转发请求到指定上游。但有时候该方式达不到用户需求,因此 APISIX 支持了 Consumer 对象。
插件
Plugin 也称之为插件,它是扩展 APISIX 应用层能力的关键机制,也是在使用 APISIX 时最常用的资源对象。插件主要是在 HTTP 请求或响应生命周期期间执行的、针对请求的个性化策略。插件可以与路由、服务或消费者绑定。
如果路由、服务、插件配置或消费者都绑定了相同的插件,则只有一份插件配置会生效,插件配置的优先级由高到低顺序是:消费者 > 路由 > 插件配置 > 服务。同时在插件执行过程中也会涉及 6 个阶段,分别是 rewrite
、access
、before_proxy
、header_filter
、body_filter
和 log
。
Admin API
APISIX 提供了强大的 Admin API 和 Dashboard 供用户使用,Admin API 是一组用于配置 Apache APISIX 路由、上游、服务、SSL 证书等功能的 RESTful API。
我们可以通过 Admin API 来获取、创建、更新以及删除资源。同时得益于 APISIX 的热加载能力,资源配置完成后 APISIX 将会自动更新配置,无需重启服务,具体的架构原理可以查看下面的架构图:
主要分为两个部分:
- APISIX 核心:包括 Lua 插件、多语言插件运行时(Plugin Runner)、Wasm 插件运行时等;
- 功能丰富的各种内置插件:包括可观测性、安全、流量控制等。
APISIX 在其核心中,提供了路由匹配、负载均衡、服务发现、API 管理等重要功能,以及配置管理等基础性模块。除此之外,APISIX 插件运行时也包含其中,提供原生 Lua 插件的运行框架和多语言插件的运行框架,以及实验性的 Wasm 插件运行时等。APISIX 多语言插件运行时提供多种开发语言的支持,比如 Golang、Java、Python、JS 等。
APISIX 目前也内置了各类插件,覆盖了 API 网关的各种领域,如认证鉴权、安全、可观测性、流量管理、多协议接入等。当前 APISIX 内置的插件使用原生 Lua 实现,关于各个插件的介绍与使用方式,后续我们再介绍。
创建路由
下面的示例中我们先使用 Admin API 来创建一个 Route 并与 Upstream 绑定,当一个请求到达 APISIX 时,APISIX 会将请求转发到指定的上游服务中。
以下示例代码中,我们将为路由配置匹配规则,以便 APISIX 可以将请求转发到对应的上游服务:
➜ curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT -d '
{
"methods": ["GET"],
"host": "youdianzhishi.com",
"uri": "/anything/*",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}' -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
# 正常会得到如下所示的结果
{"value":{"create_time":1679392758,"methods":["GET"],"host":"youdianzhishi.com","status":1,"priority":0,"update_time":1679392758,"upstream":{"pass_host":"pass","hash_on":"vars","type":"roundrobin","nodes":{"httpbin.org:80":1},"scheme":"http"},"id":"1","uri":"/anything/*"},"key":"/apisix/routes/1"}
其中的 X-API-KEY
的值在 APISIX 的配置文件中 apisix_config.yaml
中有配置,位于 deployment.admin.admin_key
下面。
该配置意味着,当请求满足下述的所有规则时,请求将被转发到上游服务(httpbin.org:80
):
- 请求的 HTTP 方法为 GET。
- 请求头包含 host 字段,且它的值为
youdianzhishi.com
。 - 请求路径匹配
/anything/*
,*
意味着任意的子路径,例如/anything/foo?arg=10
。
当路由创建完成后,现在我们就可以通过以下命令访问上游服务了:
➜ curl -i -X GET "http://127.0.0.1:9080/anything/foo?arg=10" -H "Host: youdianzhishi.com"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 443
Connection: keep-alive
Date: Tue, 21 Mar 2023 08:25:49 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0
{
"args": {
"arg": "10"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "youdianzhishi.com",
"User-Agent": "curl/7.85.0",
"X-Amzn-Trace-Id": "Root=1-64196a0d-1d2b654b29cbed3f7a9302c7",
"X-Forwarded-Host": "youdianzhishi.com"
},
"json": null,
"method": "GET",
"origin": "172.22.0.1, 221.11.206.200",
"url": "http://youdianzhishi.com/anything/foo?arg=10"
}
该请求将被 APISIX 转发到 http://httpbin.org:80/anything/foo?arg=10
,我们可以和直接访问上游数据进行对比。
使用上游服务创建路由
我们还可以通过以下命令创建一个上游,并在路由中使用它,而不是直接将其配置在路由中:
➜ curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d '
{
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}' -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
# 正常会得到如下所示的输出
{"value":{"type":"roundrobin","create_time":1679392818,"pass_host":"pass","hash_on":"vars","update_time":1679392818,"nodes":{"httpbin.org:80":1},"id":"1","scheme":"http"},"key":"/apisix/upstreams/1"}
该上游配置与上一节配置在路由中的上游相同。同样使用了 roundrobin
作为负载均衡机制,并设置了 httpbin.org:80
为上游服务。为了将该上游绑定到路由,此处需要把 upstream_id
设置为 "1"。
上游服务创建完成后,现在我们可以通过以下命令将其绑定到指定的 /get
路由:
➜ curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT -d '
{
"uri": "/get",
"host": "httpbin.org",
"upstream_id": "1"
}' -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
# 正常会得到如下所示的输出
{"value":{"upstream_id":"1","status":1,"create_time":1679392758,"host":"httpbin.org","update_time":1679392834,"priority":0,"id":"1","uri":"/get"},"key":"/apisix/routes/1"}
我们已经创建了路由与上游服务,现在可以通过以下命令访问上游服务:
➜ curl -i -X GET "http://127.0.0.1:9080/get?foo1=bar1&foo2=bar2" -H "Host: httpbin.org"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 370
Connection: keep-alive
Date: Tue, 21 Mar 2023 08:40:19 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0
{
"args": {
"foo1": "bar1",
"foo2": "bar2"
},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.85.0",
"X-Amzn-Trace-Id": "Root=1-64196d73-165daa124c362e5d4c6bb79d",
"X-Forwarded-Host": "httpbin.org"
},
"origin": "172.22.0.1, 221.11.206.200",
"url": "http://httpbin.org/get?foo1=bar1&foo2=bar2"
}
同样该请求也会被 APISIX 转发到 http://httpbin.org:80/anything/foo?arg=10
。
使用 Dashboard
同样我们还可以使用 APISIX Dashboard 创建和配置类似于上述步骤中所创建的路由。
如果你已经完成上述操作步骤,正常现在我们已经可以通过 localhost:9000
来访问 APISIX Dashboard 了。
默认的用户名和密码均为 admin
,在 examples
目录中 dashboard_conf
下面的 conf.yaml
进行配置:
authentication:
secret: secret
expire_time: 3600
users:
- username: admin
password: admin
- username: user
password: user
上面我们的 docker-compose 中也已经启动了 Grafana,所以登录后我们也可以在首页仪表盘上配置 Grafana,地址为 http://localhost:3000
。
登录后单击侧边栏中的路由,可以查看已经配置的路由列表,可以看到在上述步骤中使用 Admin API 创建的路由。
你也可以通过单击创建按钮并按照提示创建新路由:
如果想利用 APISIX 实现身份验证、安全性、限流限速和可观测性等功能,可通过添加插件实现。
限流限速和安全插件
在很多时候,我们的 API 并不是处于一个非常安全的状态,它随时会收到不正常的访问,一旦访问流量突增,可能就会导致你的 API 发生故障,这个时候我们就可以通过速率限制来保护 API 服务,限制非正常的访问请求。对此,我们可以使用如下方式进行:
- 限制请求速率;
- 限制单位时间内的请求数;
- 延迟请求;
- 拒绝客户端请求;
- 限制响应数据的速率。
APISIX 提供了多个内置的限流限速的插件,包括 limit-conn
、limit-count
和 limit-req
。
limit-conn
插件主要用于限制客户端对服务的并发请求数。limit-req
插件使用漏桶算法限制对用户服务的请求速率。limit-count
插件主要用于在指定的时间范围内,限制每个客户端总请求个数。
这里我们就以 limit-count
插件为例,来说明如何通过限流限速插件保护我们的 API 服务。如下所示。
使用下面的命令首先创建一条路由:
➜ curl -i http://127.0.0.1:9180/apisix/admin/routes/1 -X PUT -d '
{
"uri": "/index.html",
"plugins": {
"limit-count": {
"count": 2,
"time_window": 60,
"rejected_code": 503,
"key_type": "var",
"key": "remote_addr"
}
},
"upstream_id": "1"
}' -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
这里我们直接使用前面已经创建的上游(ID 为 1)来创建/更新一条路由,并且在 plugins
中启用了 limit-count
插件,该插件仅允许客户端在 60 秒内,访问上游服务 2 次,超过两次,就会返回 503 错误码。
上面的指令执行成功后,接下来我们连续使用下面的命令访问三次后,则会出现如下错误。
➜ curl http://127.0.0.1:9080/index.html
➜ curl http://127.0.0.1:9080/index.html
➜ curl http://127.0.0.1:9080/index.html
正常情况下就会出现如下所示的 503 错误,则表示 limit-count
插件已经配置成功。
<html>
<head>
<title>503 Service Temporarily Unavailable</title>
</head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr />
<center>openresty</center>
<p>
<em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em>
</p>
</body>
</html>
缓存响应
当我们在构建一个 API 时,肯定希望他能够尽量保持简单和快速,一旦读取相同数据的并发需求增加,可能会面临一些问题,一般我们直接的办法就是引入缓存,当然我们可以在不同层面去进行缓存的。
- 边缘缓存或 CDN
- 数据库缓存
- 服务器缓存(API 缓存)
- 浏览器缓存
反向代理缓存是另一种缓存机制,通常在 API 网关内实现。它可以减少对你的端点的调用次数,也可以通过缓存上游的响应来改善对你的 API 请求的延迟。如果 API Gateway 的缓存中有所请求资源的新鲜副本,它就会使用该副本直接满足请求,而不是向端点发出请求。如果没有找到缓存的数据,请求就会转到预定的上游服务(后端服务)。
我们这里主要了解的是 API 网关层的缓存,也就是 APISIX 提供的 API 缓存,它也可以和其他插件一起使用,目前支持基于磁盘的缓存,也可以在插件配置中指定缓存过期时间或内存容量等。
比如我们现在有一个 /products
的 API 接口,通常每天只更新一次,而该端点每天都会收到重复的数十亿次请求,以获取产品列表数据,现在我们就可以使用 APISIX 提供的一个名为 proxy-cache
的插件来缓存该接口的响应。
这里我们还是使用前面 ID 为 1 的上游对象,使用 /anything/products
来模拟产品接口,直接执行下面的命令来更新路由的插件:
➜ curl "http://127.0.0.1:9180/apisix/admin/routes/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '{
"name": "Route for API Caching",
"methods": [
"GET"
],
"uri": "/anything/*",
"plugins": {
"proxy-cache": {
"cache_key": [
"$uri",
"-cache-id"
],
"cache_bypass": [
"$arg_bypass"
],
"cache_method": [
"GET"
],
"cache_http_status": [
200
],
"hide_cache_headers": true,
"no_cache": [
"$arg_test"
]
}
},
"upstream_id": "1"
}'
更新完成后现在我们来对该接口发起几次请求:
➜ curl http://localhost:9080/anything/products -i
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 398
Connection: keep-alive
Date: Tue, 21 Mar 2023 10:48:42 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0
Apisix-Cache-Status: MISS
➜ curl http://localhost:9080/anything/products -i
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 398
Connection: keep-alive
Date: Tue, 21 Mar 2023 10:48:42 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0
Apisix-Cache-Status: HIT
正常每次都应该收到 HTTP 200 OK 响应,但是第一次响应中的 Apisix-Cache-Status
显示为 MISS
,这意味着当请求第一次进入路由时,响应还没有被缓存。而后面的几次请求会得到一个缓存的响应,缓存指标变为了 HIT
,表示我们的响应缓存成功了。
APISIX Ingress
前面我们已经学习了 APISIX 的基本使用,同样作为一个 API 网关,APISIX 也支持作为 Kubernetes 的一个 Ingress 控制器进行使用。APISIX Ingress 在架构上分成了两部分,一部分是 APISIX Ingress Controller,作为控制面它将完成配置管理与分发。另一部分 APISIX(代理) 负责承载业务流量。
当 Client 发起请求,到达 Apache APISIX 后,会直接把相应的业务流量传输到后端(如 Service Pod),从而完成转发过程。此过程不需要经过 Ingress Controller,这样做可以保证一旦有问题出现,或者是进行变更、扩缩容或者迁移处理等,都不会影响到用户和业务流量。
同时在配置端,用户通过 kubectl apply
创建资源,可将自定义 CRD 配置应用到 K8s 集群,Ingress Controller 会持续 watch 这些资源变更,来将相应配置应用到 Apache APISIX(通过 admin api)。
从上图可以看出 APISIX Ingress 采用了数据面与控制面的分离架构,所以用户可以选择将数据面部署在 K8s 集群内部或外部。但 Ingress Nginx 是将控制面和数据面放在了同一个 Pod 中,如果 Pod 或控制面出现一点闪失,整个 Pod 就会挂掉,进而影响到业务流量。这种架构分离,给用户提供了比较方便的部署选择,同时在业务架构调整场景下,也方便进行相关数据的迁移与使用。
APISIX Ingress 控制器目前支持的核心特性包括:
- 全动态,支持高级路由匹配规则,可与 Apache APISIX 官方插件 & 客户自定义插件进行扩展使用
- 支持 CRD,更容易理解声明式配置
- 兼容原生 Ingress 资源对象
- 开箱即用的节点健康检查支持
- 支持基于 Pod(上游节点)的负载均衡
- 服务自动注册发现,无惧扩缩容
- 支持 gRPC plaintext 与 TCP 4 层代理等
安装
我们这里在 Kubernetes 集群中来使用 APISIX,可以通过 Helm Chart 来进行安装,首先添加官方提供的 Helm Chart 仓库:
➜ helm repo add apisix https://charts.apiseven.com
➜ helm repo update
由于 APISIX 的 Chart 包中包含 dashboard 和 ingress 控制器的依赖,我们只需要在 values 中启用即可安装 ingress 控制器了:
➜ helm fetch apisix/apisix --untar
新建一个用于安装的 values 文件,内容如下所示:
# apisix-values.yaml
apisix:
enabled: true
gateway:
type: NodePort
http:
enabled: true
servicePort: 80
containerPort: 9080
tls:
enabled: true # 启用 tls
servicePort: 443
containerPort: 9443
etcd:
enabled: true
replicaCount: 1 # 默认为3
persistence:
enabled: true
storageClass: cfsauto
dashboard:
enabled: true
ingress-controller:
enabled: true
config:
apisix:
serviceName: apisix-admin
serviceNamespace: apisix # 指定命名空间,如果不是 ingress-apisix 需要重新指定
APISIX 需要依赖 etcd,默认情况下 Helm Chart 会自动安装一个 3 副本的 etcd 集群,需要提供一个默认的 StorageClass,如果你已经有默认的存储类则可以不用指定。
➜ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
cfsauto com.tencent.cloud.csi.cfs Retain Immediate false 3d5h
local-storage kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 3d7h
然后直接执行下面的命令进行一键安装:
➜ helm upgrade --install apisix ./apisix -f ./apisix-values.yaml -n apisix --create-namespace
Release "apisix" does not exist. Installing it now.
NAME: apisix
LAST DEPLOYED: Thu Mar 23 16:43:26 2023
NAMESPACE: apisix
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
export NODE_PORT=$(kubectl get --namespace apisix -o jsonpath="{.spec.ports[0].nodePort}" services apisix-gateway)
export NODE_IP=$(kubectl get nodes --namespace apisix -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
正常等一小会儿就可以成功部署 apisix ingress 了:
➜ kubectl get pods -n apisix
NAME READY STATUS RESTARTS AGE
apisix-58559d47c8-c5kmh 1/1 Running 0 5m10s
apisix-dashboard-5db89db87-sspph 1/1 Running 0 5m10s
apisix-etcd-0 1/1 Running 0 5m10s
apisix-ingress-controller-986f76ffc-4z8kq 1/1 Running 0 5m10s
➜ kubectl get svc -n apisix
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
apisix-admin ClusterIP 10.97.115.202 <none> 9180/TCP 3m4s
apisix-dashboard ClusterIP 10.101.216.74 <none> 80/TCP 3m4s
apisix-etcd ClusterIP 10.106.173.180 <none> 2379/TCP,2380/TCP 3m4s
apisix-etcd-headless ClusterIP None <none> 2379/TCP,2380/TCP 3m4s
apisix-gateway NodePort 10.101.146.213 <none> 80:31207/TCP,443:32484/TCP 3m4s
apisix-ingress-controller ClusterIP 10.100.82.47 <none> 80/TCP 3m4s
Dashboard
现在我们可以为 Dashboard 创建一个路由规则,新建一个如下所示的 ApisixRoute
资源对象即可:
# apisix-dashboard-route.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: dashboard
namespace: apisix
spec:
http:
- name: root
match:
hosts:
- apisix.youdianzhishi.com
paths:
- "/*"
backends:
- serviceName: apisix-dashboard
servicePort: 80
创建后 apisix-ingress-controller
会将上面的资源对象通过 admin api 映射成 APISIX 中的配置:
➜ kubectl get apisixroute -n apisix
NAME HOSTS URIS AGE
dashboard ["apisix.youdianzhishi.com"] ["/*"] 15s
所以其实我们的访问入口是 APISIX,而 apisix-ingress-controller
只是一个用于监听 crds,然后将 crds 翻译成 APISIX 的配置的工具而已,现在就可以通过 apisix-gateway
的 NodePort 端口去访问我们的 dashboard 了:
当然如果不想在访问的时候域名后面带上端口,在云端环境可以直接将 apisix-gateway
这个 Service 设置成 LoadBalancer 模式,在本地测试的时候可以使用 kubectl port-forward
将服务暴露在节点的 80 端口上。
➜ kubectl port-forward --address 0.0.0.0 svc/apisix-gateway 80:80 443:443 -n apisix
默认登录用户名和密码都是 admin,登录后在路由
菜单下正常可以看到上面我们创建的这个 dashboard 的路由信息:
点击更多
下面的查看
就可以看到在 APISIX 下面真正的路由配置信息:
所以我们要使用 APISIX,也一定要理解其中的路由 Route 这个概念,路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则,路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。
理解了路由后自然就知道了我们还需要一个上游 Upstream 进行关联,这个概念和 Nginx 中的 Upstream 基本是一致的,在上游
菜单下可以看到我们上面创建的 dashboard 对应的上游服务:
其实就是将 Kubernetes 中的 Endpoints 映射成 APISIX 中的 Upstream,然后我们可以自己在 APISIX 这边进行负载。
URL Rewrite
同样我们来介绍下如何使用 APISIX 来实现 URL Rewrite 操作,同样还是以前面测试用过的 Nexus 应用为例进行说明,通过 ApisixRoute
对象来配置服务路由,对应的资源清单如下所示:
# apisix-rewrite.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nexus
labels:
app: nexus
spec:
selector:
matchLabels:
app: nexus
template:
metadata:
labels:
app: nexus
spec:
containers:
- image: cnych/nexus:3.20.1
imagePullPolicy: IfNotPresent
name: nexus
ports:
- containerPort: 8081
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nexus
name: nexus
spec:
ports:
- name: nexusport
port: 8081
targetPort: 8081
selector:
app: nexus
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.youdianzhishi.com
paths:
- "/*"
backends:
- serviceName: nexus
servicePort: 8081
直接创建上面的资源对象即可:
➜ kubectl apply -f nexus.yaml
➜ kubectl get apisixroute nexus
NAME HOSTS URIS AGE
nexus ["ops.youdianzhishi.com"] ["/*"] 10s
➜ kubectl get pods -l app=nexus
NAME READY STATUS RESTARTS AGE
nexus-6f78b79d4c-b79r4 1/1 Running 0 48s
➜ kubectl get svc -l app=nexus
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nexus ClusterIP 10.110.126.128 <none> 8081/TCP 58s
部署完成后,我们根据 ApisixRoute
对象中的配置,只需要将域名 ops.youdianzhishi.com
映射到任何节点,加上 nodePort 即可访问:
同样如果现在需要通过一个子路径来访问 Nexus 应用的话又应该怎么来实现呢?比如通过 http://ops.youdianzhishi.com/nexus
来访问我们的应用,首先我们肯定需要修改 ApisixRoute
对象中匹配的 paths 路径,将其修改为 /nexus
:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.youdianzhishi.com
paths:
- "/nexus*"
backends:
- serviceName: nexus
servicePort: 8081
更新后我们可以通过 http://ops.qikqiak.com/nexus
访问应用:
仔细分析发现很多静态资源 404 了,这是因为现在我们只匹配了 /nexus
的请求,而我们的静态资源是 /static
路径开头的,当然就匹配不到了,所以就出现了 404,所以我们只需要加上这个 /static
路径的匹配就可以了,同样更新 ApisixRoute 对象,新增 /static/*
路径支持:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
backends:
- serviceName: nexus
servicePort: 8081
更新后发现虽然静态资源可以正常访问了,但是当我们访问 http://ops.qikqiak.com/nexus
的时候依然会出现 404 错误。
这是因为我们这里是将 /nexus
路径的请求直接路由到后端服务去了,而后端服务没有对该路径做任何处理,所以也就是 404 的响应了,在之前 ingress-nginx 中我们是通过 url 重写来实现的,而在 APISIX 中同样可以实现这个处理,相当于在请求在真正到达上游服务之前将请求的 url 重写到根目录就可以了,这里我们需要用到 proxy-rewrite 这个插件(需要确保在安装的时候已经包含了该插件),proxy-rewrite
是上游代理信息重写插件,支持对 scheme、uri、host 等信息的重写,该插件可配置的属性如下表所示:
我们现在的需求是希望将所有 /nexus
下面的请求都重写到根路径 /
下面去,所以我们应该使用 regex_uri
属性,转发到上游的新 uri 地址, 使用正则表达式匹配来自客户端的 uri,当匹配成功后使用模板替换转发到上游的 uri, 未匹配成功时将客户端请求的 uri 转发至上游,重新修改后的 ApisixRoute
对象如下所示,新增 plugins
属性来配置插件:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ["^/nexus(/|$)(.*)", "/$2"]
backends:
- serviceName: nexus
servicePort: 8081
这里我们启用一个 proxy-rewrite
插件,并且将所有 /nexus
路径的请求都重写到了 /
跟路径下,重新更新后再次访问 http://ops.youdianzhishi.com/nexus
应该就可以正常访问了:
只有最后一个小问题了,从浏览器网络请求中可以看出我们没有去匹配 /service
这个路径的请求,只需要配置上该路径即可,如下所示:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
- "/service/*"
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ["^/nexus(/|$)(.*)", "/$2"]
backends:
- serviceName: nexus
servicePort: 8081
现在重新访问子路径就完成正常了:
redirect
现在当我们访问 http://ops.youdianzhishi.com/nexus
或者 http://ops.youdianzhishi.com/nexus/
的时候都可以得到正常的结果,一般来说我们可能希望能够统一访问路径,比如访问 /nexus
子路径的时候可以自动跳转到 /nexus/
以 Splash 结尾的路径上去。同样要实现该需求我们只需要使用一个名为 redirect
的插件即可,该插件是 URI 重定向插件,可配置的属性如下所示:
要实现我们的需求直接使用 regex_uri
这个属性即可,只需要去匹配 /nexus
的请求,然后进行跳转即可,更新 ApisixRoute
对象:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
- "/service/*"
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ["^/nexus(/|$)(.*)", "/$2"]
- name: redirect
enable: true
config:
regex_uri: ["^(/nexus)$", "$1/"]
backends:
- serviceName: nexus
servicePort: 8081
我们新启用了一个 redirect
插件,并配置 regex_uri: ["^(/nexus)$", "$1/"]
,这样当访问 /nexus
的时候会自动跳转到 /nexus/
路径下面去。
同样如果我们想要重定向到 https,只需要在该插件下面设置 config.http_to_https=true
即可:
# ... 其他部分省略
- name: redirect
enable: true
config:
http_to_https: true
tls
通过使用上面的 redirect
插件配置 http_to_https
可以将请求重定向到 https 上去,但是我们现在并没有对我们的 ops.youdianzhishi.com
配置 https 证书,这里我们就需要使用 ApisixTls
对象来进行证书管理。
我们先使用 openssl
创建一个自签名的证书,当然你有正规 CA 机构购买的证书的话直接将证书下载下来使用即可:
➜ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=ops.youdianzhishi.com"
然后通过 Secret 对象来引用上面创建的证书文件:
# 要注意证书文件名称必须是 tls.crt 和 tls.key
➜ kubectl create secret tls ops-tls --cert=tls.crt --key=tls.key
然后就可以创建一个 ApisixTls
资源对象,引用上面的 Secret 即可:
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
name: ops-tls
spec:
hosts:
- ops.youdianzhishi.com
secret:
name: ops-tls
namespace: default
同时 APISIX TLS 还可以配置 spec.client
,用于进行 mTLS 双向认证的配置。上面的资源对象创建完成后,即可访问 https 服务了(chrome 浏览器默认会限制不安全的证书,只需要在页面上输入 thisisunsafe
即可访问了):
而且当访问 http 的时候也会自动跳转到 https 上面去,此外我们还可以结合 cert-manager 来实现自动化的 https。
APISIX 认证与自定义插件
身份认证在日常生活当中是非常常见的一项功能,大家平时基本都会接触到。比如用支付宝消费时的人脸识别确认、公司上班下班时的指纹/面部打卡以及网站上进行账号密码登录操作等,其实都是身份认证的场景体现。
如上图,Jack 通过账号密码请求服务端应用,服务端应用中需要有一个专门用做身份认证的模块来处理这部分的逻辑。请求处理完毕子后,如果使用 JWT Token 认证方式,服务器会反馈一个 Token 去标识这个用户为 Jack。如果登录过程中账号密码输入错误,就会导致身份认证失败。
但是每个应用服务模块去开发一个单独的身份认证模块,用来支持身份认证的一套流程处理,当服务量多了之后,就会发现这些模块的开发工作量都是非常巨大且重复的。这个时候,我们可以通过把这部分的开发逻辑放置到 Apache APISIX 的网关层来实现统一,减少开发量。
如上图所示,用户或应用方直接去请求 Apache APISIX,然后 Apache APISIX 通过识别并认证通过后,会将鉴别的身份信息传递到上游应用服务,之后上游应用服务就可以从请求头中读到这部分信息,然后进行后续的逻辑处理。
Apache APISIX 作为一个 API 网关,目前已开启与各种插件功能的适配合作,插件库也比较丰富。目前已经可与大量身份认证相关的插件进行搭配处理,如下图所示。
基础认证插件比如 Key-Auth
、Basic-Auth
,他们是通过账号密码的方式进行认证。复杂一些的认证插件如 Hmac-Auth
、JWT-Auth
,如 Hmac-Auth
通过对请求信息做一些加密,生成一个签名,当 API 调用方将这个签名携带到 Apache APISIX,Apache APISIX 会以相同的算法计算签名,只有当签名方和应用调用方认证相同时才予以通过。其他则是一些通用认证协议和联合第三方组件进行合作的认证协议,例如 OpenID-Connect
身份认证机制,以及 LDAP
认证等。
Apache APISIX 还可以针对每一个 Consumer (即调用方应用)去做不同级别的插件配置。如下图所示,我们创建了两个消费者 Consumer A、Consumer B,我们将 Consumer A 应用到应用 1,则后续应用 1 的访问将会开启 Consumer A 的这部分插件,例如 IP 黑白名单,限制并发数量等。将 Consumer B 应用到应用 2 ,由于开启了 http-log 插件,则应用 2 的访问日志将会通过 HTTP 的方式发送到日志系统进行收集。
总体说来 APISIX 的认证系统功能非常强大,我们非常有必要掌握。
basic-auth
首先我们来了解下最简单的基本认证在 APISIX 中是如何使用的。basic-auth
是一个认证插件,它需要与 Consumer 一起配合才能工作。添加 Basic Auth 到一个 Service 或 Route,然后 Consumer 将其用户名和密码添加到请求头中以验证其请求。
首先我们需要在 APISIX Consumer 消费者中增加 basic auth 认证配置,为其指定用户名和密码,我们这里在 APISIX Ingress 中,可以通过 ApisixConsumer
资源对象进行配置,比如这里我们为前面的 nexus 实例应用添加一个基本认证,如下所示:
# apisix-basic-auth.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
name: nexus-bauth
spec:
authParameter:
basicAuth:
value:
username: admin
password: admin321
ApisixConsumer
资源对象中只需要配置 authParameter
认证参数即可,目前支持 basicAuth
、hmacAuth
、jwtAuth
、 keyAuth
、wolfRBAC
等多种认证类型,在 basicAuth 下面可以通过 value 可直接去配置相关的 username 和 password,也可以直接使用 Secret 资源对象进行配置,比起明文配置会更安全一些。
然后在 ApisixRoute
中添加 authentication,将其开启并指定认证类型即可,就可以实现使用 Consumer 去完成相关配置认证,如下所示:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
- "/service/*"
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ["^/nexus(/|$)(.*)", "/$2"]
- name: redirect
enable: true
config:
regex_uri: ["^(/nexus)$", "$1/"]
- name: redirect
enable: true
config:
http_to_https: true
backends:
- serviceName: nexus
servicePort: 8081
authentication: # 开启 basic auth 认证
enable: true
type: basicAuth
直接更新上面的资源即可开启 basic auth 认证了,在 Dashboard 上也可以看到创建了一个 Consumer:
然后我们可以进行如下的测试来进行验证:
# 缺少 Authorization header
➜ curl -i http://ops.youdianzhishi.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 28 Mar 2023 08:12:01 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
WWW-Authenticate: Basic realm='.'
Server: APISIX/3.2.0
{"message":"Missing authorization in request"}
# 用户名不存在
➜ curl -i -ubar:bar http://ops.youdianzhishi.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 28 Mar 2023 08:12:19 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.2.0
{"message":"Invalid user authorization"}
# 成功请求
➜ curl -i -uadmin:admin321 http://ops.youdianzhishi.com/nexus/
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 9024
Connection: keep-alive
Date: Tue, 28 Mar 2023 08:12:28 GMT
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Last-Modified: Tue, 28 Mar 2023 08:12:28 GMT
Pragma: no-cache
Cache-Control: no-cache, no-store, max-age=0, must-revalidate, post-check=0, pre-check=0
Expires: 0
Server: APISIX/3.2.0
<html>
<head><title>301 Moved Permanently</title></head>
# ......
</html>
consumer-restriction
不过这里大家可能会有一个疑问,在 Route 上面我们并没有去指定具体的一个 Consumer,然后就可以进行 Basic Auth 认证了,那如果我们有多个 Consumer 都定义了 Basic Auth 岂不是都会生效的?确实是这样的,这就是 APISIX 的实现方式,所有的 Consumer 对启用对应插件的 Route 都会生效的,如果我们只想 Consumer A 应用在 Route A、Consumer B 应用在 Route B 上面的话呢?要实现这个功能就需要用到另外一个插件:consumer-restriction。
consumer-restriction
插件可以根据选择的不同对象做相应的访问限制,该插件可配置的属性如下表所示:
其中的 type 字段是个枚举类型,可以设置以下值:
consumer_name
:把 Consumer 的 username 列入白名单或黑名单来限制 Consumer 对 Route 或 Service 的访问。consumer_group_id
: 把 Consumer Group 的 id 列入白名单或黑名单来限制 Consumer 对 Route 或 Service 的访问。service_id
:把 Service 的 id 列入白名单或黑名单来限制 Consumer 对 Service 的访问,需要结合授权插件一起使用。route_id
:把 Route 的 id 列入白名单或黑名单来限制 Consumer 对 Route 的访问。
比如现在我们有两个 Consumer:jack1 和 jack2,这两个 Consumer 都配置了 Basic Auth 认证,配置如下所示:
执行命令 kubectl port-forward --address 0.0.0.0 svc/apisix-admin 9180:9180 -n apisix
暴露 admin 端点。
Conumer jack1
的认证配置:
➜ curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"username": "jack1",
"plugins": {
"basic-auth": {
"username":"jack2019",
"password": "123456"
}
}
}'
Conumer jack2
的认证配置:
➜ curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"username": "jack2",
"plugins": {
"basic-auth": {
"username":"jack2020",
"password": "123456"
}
}
}'
现在我们只想给一个 Route 路由对象启用 jack1 这个 Consumer 的认证配置,则除了启用 basic-auth
插件之外,还需要在 consumer-restriction
插件中配置一个 whitelist
白名单(当然配置黑名单也是可以的),如下所示:
➜ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/index.html",
"upstream": {
"type": "roundrobin",
"nodes": {
"10.244.1.125:8081": 1
}
},
"plugins": {
"basic-auth": {},
"consumer-restriction": {
"whitelist": [
"jack1"
]
}
}
}'
然后我们使用 jack1 去访问我们的路由进行验证:
➜ curl -u jack2019:123456 http://127.0.0.1/index.html -i
HTTP/1.1 302 Found
...
正常使用 jack2 访问就会认证失败了:
➜ curl -u jack2020:123456 http://127.0.0.1/index.html -i
HTTP/1.1 403 Forbidden
Date: Tue, 28 Mar 2023 08:22:38 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.2.0
{"message":"The consumer_name is forbidden."}
所以当你只想让一个 Route 对象关联指定的 Consumer 的时候,记得使用 consumer-restriction
插件。
jwt-auth
在平时的应用中可能使用 jwt 认证的场景是最多的,同样在 APISIX 中也有提供 jwt-auth
的插件,它同样需要与 Consumer 一起配合才能工作,我们只需要添加 JWT Auth 到一个 Service 或 Route,然后 Consumer 将其密钥添加到查询字符串参数、请求头或 cookie 中以验证其请求即可。
当然除了通过 ApisixConsumer
这个 CRD 去配置之外,我们也可以直接通过 Dashboard 页面操作。在 Dashboard 消费者页面点击创建消费者:
点击下一步进入插件配置页面,这里我们需要启用 jwt-auth
这个插件:
在插件配置页面配置 jwt-auth
相关属性,可参考插件文档 https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/:
可配置的属性如下表所示:
然后提交即可创建完成 Consumer,然后我们只需要在需要的 Service 或者 Route 上开启 jwt-auth
即可,比如同样还是针对上面的 nexus 应用,我们只需要在 ApisixRoute
对象中启用一个 jwt-auth
插件即可:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- "/nexus*"
- "/static/*"
- "/service/*"
plugins:
- name: redirect
enable: true
config:
http_to_https: true
- name: redirect
enable: true
config:
regex_uri: ["^(/nexus)$", "$1/"]
- name: proxy-rewrite
enable: true
config:
regex_uri: ["^/nexus(/|$)(.*)", "/$2"]
backends:
- serviceName: nexus
servicePort: 8081
authentication: # 开启 jwt auth 认证
enable: true
type: jwtAuth
重新更新上面的对象后我们同样来测试验证下:
➜ curl -i http://ops.youdianzhishi.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 28 Mar 2023 08:30:02 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.2.0
{"message":"Missing JWT token in request"}
要正常访问我们的服务就需要先进行登录获取 jwt-auth
的 token,首先,你需要为签发 token 的 API 配置一个 Route,该路由将使用 public-api 插件。
➜ curl http://127.0.0.1:9180/apisix/admin/routes/jas \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/apisix/plugin/jwt/sign",
"plugins": {
"public-api": {}
}
}'
{"error_msg":"unknown plugin [public-api]"}
执行上面命令后会出现不识别 public-api
插件,这是因为我们这里使用 Helm Chart 方式安装的 APISIX 默认没有安装该插件,所以我们需要到 Helm Chart 的 values.yaml
文件中在 plugins
属性下面添加上该插件,然后重新更新 APISIX 即可:
➜ helm upgrade --install apisix ./apisix -f ./apisix-values.yaml -n apisix --create-namespace
更新后重新执行命令:
➜ curl http://127.0.0.1:9180/apisix/admin/routes/jas -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/apisix/plugin/jwt/sign",
"plugins": {
"public-api": {}
}
}'
之后就可以通过调用它来获取 token 了。
➜ curl http://127.0.0.1/apisix/plugin/jwt/sign?key=user-key -i
HTTP/1.1 200 OK
Date: Tue, 28 Mar 2023 08:44:45 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.2.0
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODAwNzk0ODUsImtleSI6InVzZXIta2V5In0.n4o_w3AgNC6C1pujEUScSBe0Mzw5vbjIzKpQpbrBhO8
要注意上面我们在获取 token 的时候需要传递创建消费者的标识 key,因为可能有多个不同的 Consumer 消费者,然后我们将上面获得的 token 放入到 Header 头中进行访问:
➜ curl -i http://ops.youdianzhishi.com/nexus/ -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODAwNzk0ODUsImtleSI6InVzZXIta2V5In0.n4o_w3AgNC6C1pujEUScSBe0Mzw5vbjIzKpQpbrBhO8'
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 9024
Connection: keep-alive
Date: Tue, 28 Mar 2023 08:45:24 GMT
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Last-Modified: Tue, 28 Mar 2023 08:45:24 GMT
Pragma: no-cache
Cache-Control: no-cache, no-store, max-age=0, must-revalidate, post-check=0, pre-check=0
Expires: 0
Server: APISIX/3.2.0
<!DOCTYPE html>
<html lang="en">
......
可以看到可以正常访问。同样也可以放到请求参数中验证:
➜ curl -i http://ops.youdianzhishi.com/nexus/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODAwNzk0ODUsImtleSI6InVzZXIta2V5In0.n4o_w3AgNC6C1pujEUScSBe0Mzw5vbjIzKpQpbrBhO8
HTTP/1.1 200 OK
......
此外还可以放到 cookie 中进行验证:
➜ curl -i http://ops.youdianzhishi.com/nexus/ --cookie jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODAwNzk0ODUsImtleSI6InVzZXIta2V5In0.n4o_w3AgNC6C1pujEUScSBe0Mzw5vbjIzKpQpbrBhO8
HTTP/1.1 200 OK
......
自定义插件
除了 APISIX 官方内置的插件之外,我们也可以根据自己的需求去自定义插件,要自定义插件需要使用到 APISIX 提供的 Runner,目前已经支持 Java、Go、Node 和 Python 语言的 Runner,这个 Runner 相当于是 APISIX 和自定义插件之间的桥梁,比如 apache-apisix-python-runner
这个项目通过 Python Runner 可以把 Python 直接应用到 APISIX 的插件开发中,整体架构如下所示:
左边是 APISIX 的工作流程,右边的 Plugin Runner
是各语言的插件运行器,当在 APISIX 中配置一个 Plugin Runner 时,APISIX 会启动一个子进程运行 Plugin Runner,该子进程与 APISIX 进程属于同一个用户,当我们重启或重新加载 APISIX 时,Plugin Runner 也将被重启。
如果你为一个给定的路由配置了 ext-plugin-*
插件,请求命中该路由时将触发 APISIX 通过 Unix Socket
向 Plugin Runner 发起 RPC 调用。调用分为两个阶段:
ext-plugin-pre-req
:在执行 APISIX 内置插件之前ext-plugin-post-req
:在执行 APISIX 内置插件之后ext-plugin-post-resp
:将在请求获取到上游的响应之后执行。
接下来我们就以 Python 为例来说明如何自定义插件,首先获取 apache-apisix-python-runner
项目:
➜ git clone https://github.com/apache/apisix-python-plugin-runner.git
➜ cd apisix-python-plugin-runner
➜ git checkout 0.2.0 # 切换到0.2.0版本
如果是开发模式,则我们可以直接使用下面的命令启动 Python Runner:
➜ APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock python3 bin/py-runner start
启动后需要在 APISIX 配置文件中新增外部插件配置,如下所示:
➜ vim /path/to/apisix/conf/config.yaml
apisix:
admin_key:
- name: "admin"
key: edd1c9f034335f136f87ad84b625c8f1
role: admin
ext-plugin:
path_for_test: /tmp/runner.sock
通过 ext-plugin.path_for_test
指定 Python Runner 的 unix socket 文件路径即可,如果是生产环境则可以通过 ext-plugin.cmd
来指定 Runner 的启动命令即可:
ext-plugin:
cmd: [ "python3", "/path/to/apisix-python-plugin-runner/apisix/bin/py-runner", "start" ]
我们这里的 APISIX 是运行 Kubernetes 集群中的,所以要在 APISIX 的 Pod 中去执行 Python Runner 的代码,我们自然需要将我们的 Python 代码放到 APISIX 的容器中去,然后安装自定义插件的相关依赖,直接在 APISIX 配置文件中添加上面的配置即可,所以我们这里基于 APISIX 的镜像来重新定制包含插件的镜像,在 apisix-python-plugin-runner
项目根目录下新增如下所示的 Dockerfile 文件:
FROM apache/apisix:3.2.0-debian
WORKDIR /apisix-python
ADD . /apisix-python
USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip make && \
rm -rf /var/lib/apt/lists/* && apt-get clean && \
make setup && make install
基于上面 Dockerfile 构建一个新的镜像,推送到 Docker Hub:
➜ docker build -t cnych/apisix:py3-plugin-3.2.0-debian .
# 推送到DockerHub
➜ docker push cnych/apisix:py3-plugin-3.2.0-debian
接下来我们需要使用上面构建的镜像来安装 APISIX,我们这里使用的是 Helm Chart 进行安装的,所以需要通过 Values 文件进行覆盖,如下所示:
# ci/prod.yaml
apisix:
enabled: true
image:
repository: cnych/apisix
tag: py3-plugin-3.2.0-debian
......
extPlugin:
# -- Enable External Plugins. See [external plugin](https://apisix.apache.org/docs/apisix/next/external-plugin/)
enabled: true
# -- the command and its arguements to run as a subprocess
cmd: ["python3", "/apisix-python/bin/py-runner", "start"]
注意这里需要将自定义插件开启,并且将 extPlugin.cmd
配置为 ["/apisix-python/bin/py-runner", "start"]
,因为我们是在 APISIX 的镜像中安装了 Python Runner,所以需要指定 Python Runner 的启动命令。
接着就可以重新部署 APISIX 了:
➜ helm upgrade --install apisix ./apisix -f ./apisix-values.yaml -n apisix --create-namespace
部署完成后在 APISIX 的 Pod 中可以看到会启动一个 Python Runner 的子进程:
在插件目录 /apisix-python/apisix/plugins
中的 .py
文件都会被自动加载,上面示例中有两个插件 stop.py
和 rewrite.py
,我们以 stop.py
为例进行说明,该插件代码如下所示:
from typing import Any
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
from apisix.runner.plugin.core import PluginBase
class Stop(PluginBase):
def name(self) -> str:
"""在runner中注册的插件的名称"""
return "stop"
def config(self, conf: Any) -> Any:
"""解析插件配置"""
return conf
def filter(self, conf: Any, request: Request, response: Response):
"""插件执行的主函数
:param conf:
解析后的插件配置
:param request:
请求参数和信息
:param response:
响应参数和信息
:return:
"""
# 打印插件配置
print(conf)
# 获取请求 nginx 变量 `host`
host = request.get_var("host")
print(host)
# 获取请求体
body = request.get_body()
print(body)
# 设置响应头
response.set_header("X-Resp-A6-Runner", "Python")
# 设置响应体
response.set_body("Hello, Python Runner of APISIX")
# 设置响应状态码
response.set_status_code(201)
实现插件首先必须要继承 PluginBase
类,必须实现 filter
函数,插件执行核心业务逻辑就是在 filter
函数中,该函数只包含 Request
和 Response
类对象作为参数,Request
对象参数可以获取请求信息,Response
对象参数可以设置响应信息,conf
可以获取插件配置信息。
然后我们在前面的 Nexus 应用中新增一个路由来测试我们上面的 stop
插件,在 ApisixRoute
对象中新增一个路由规则,如下所示:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: ext
match:
hosts:
- ops.youdianzhishi.com
paths:
- "/extPlugin"
plugins:
- name: ext-plugin-pre-req # 启用ext-plugin-pre-req插件
enable: true
config:
conf:
- name: "stop" # 使用 stop 这个自定义插件
value: '{"body":"hello"}'
backends:
- serviceName: nexus
servicePort: 8081
直接创建上面的路由即可,核心配置是启用 ext-plugin-pre-req
插件(前提是在配置文件中已经启用该插件,在 Helm Chart 的 Values 中添加上),然后在 config
下面使用 conf
属性进行配置,conf
为数组格式可以同时设置多个插件,插件配置对象中 name
为插件名称,该名称需要与插件代码文件和对象名称一致,value
为插件配置,可以为 JSON 字符串。
创建后同样在 Dashboard 中也可以看到 APISIX 中的路由配置格式:
接着我们可以来访问 http://ops.youdianzhishi.com/extPlugin
这个路径来验证我们的自定义插件:
➜ curl -i http://ops.youdianzhishi.com/extPlugin
HTTP/1.1 201 Created
Date: Tue, 28 Mar 2023 13:10:34 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
x-resp-a6-runner: Python
Server: APISIX/3.2.0
Hello, Python Runner of APISIX
可以看到得到的结果和响应码和我们在插件中的定义是符合的。到这里就完成了使用 Python 进行 APISIX 自定义插件,我们有任何的业务逻辑需要处理直接去定义一个对应的插件即可。