Skip to content

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 容器是用于测试的:

img

请确保其他系统进程没有占用 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 个阶段,分别是 rewriteaccessbefore_proxyheader_filterbody_filterlog

Admin API

APISIX 提供了强大的 Admin API 和 Dashboard 供用户使用,Admin API 是一组用于配置 Apache APISIX 路由、上游、服务、SSL 证书等功能的 RESTful API。

我们可以通过 Admin API 来获取、创建、更新以及删除资源。同时得益于 APISIX 的热加载能力,资源配置完成后 APISIX 将会自动更新配置,无需重启服务,具体的架构原理可以查看下面的架构图:

img

主要分为两个部分:

  • 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,我们可以和直接访问上游数据进行对比。

img

使用上游服务创建路由

我们还可以通过以下命令创建一个上游,并在路由中使用它,而不是直接将其配置在路由中:

➜ 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 了。

img

默认的用户名和密码均为 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

img

登录后单击侧边栏中的路由,可以查看已经配置的路由列表,可以看到在上述步骤中使用 Admin API 创建的路由。

img

你也可以通过单击创建按钮并按照提示创建新路由:

img img

如果想利用 APISIX 实现身份验证、安全性、限流限速和可观测性等功能,可通过添加插件实现。

限流限速和安全插件

在很多时候,我们的 API 并不是处于一个非常安全的状态,它随时会收到不正常的访问,一旦访问流量突增,可能就会导致你的 API 发生故障,这个时候我们就可以通过速率限制来保护 API 服务,限制非正常的访问请求。对此,我们可以使用如下方式进行:

  • 限制请求速率;
  • 限制单位时间内的请求数;
  • 延迟请求;
  • 拒绝客户端请求;
  • 限制响应数据的速率。

APISIX 提供了多个内置的限流限速的插件,包括 limit-connlimit-countlimit-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(代理) 负责承载业务流量。

img

当 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 了:

img

当然如果不想在访问的时候域名后面带上端口,在云端环境可以直接将 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 的路由信息:

img

点击更多下面的查看就可以看到在 APISIX 下面真正的路由配置信息:

img

所以我们要使用 APISIX,也一定要理解其中的路由 Route 这个概念,路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则,路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。

理解了路由后自然就知道了我们还需要一个上游 Upstream 进行关联,这个概念和 Nginx 中的 Upstream 基本是一致的,在上游菜单下可以看到我们上面创建的 dashboard 对应的上游服务:

img

其实就是将 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 即可访问:

img

同样如果现在需要通过一个子路径来访问 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 访问应用:

img

仔细分析发现很多静态资源 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 错误。

img

这是因为我们这里是将 /nexus 路径的请求直接路由到后端服务去了,而后端服务没有对该路径做任何处理,所以也就是 404 的响应了,在之前 ingress-nginx 中我们是通过 url 重写来实现的,而在 APISIX 中同样可以实现这个处理,相当于在请求在真正到达上游服务之前将请求的 url 重写到根目录就可以了,这里我们需要用到 proxy-rewrite 这个插件(需要确保在安装的时候已经包含了该插件),proxy-rewrite 是上游代理信息重写插件,支持对 scheme、uri、host 等信息的重写,该插件可配置的属性如下表所示:

img

我们现在的需求是希望将所有 /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 应该就可以正常访问了:

img

只有最后一个小问题了,从浏览器网络请求中可以看出我们没有去匹配 /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

现在重新访问子路径就完成正常了:

img

redirect

现在当我们访问 http://ops.youdianzhishi.com/nexus 或者 http://ops.youdianzhishi.com/nexus/ 的时候都可以得到正常的结果,一般来说我们可能希望能够统一访问路径,比如访问 /nexus 子路径的时候可以自动跳转到 /nexus/ 以 Splash 结尾的路径上去。同样要实现该需求我们只需要使用一个名为 redirect 的插件即可,该插件是 URI 重定向插件,可配置的属性如下所示:

img

要实现我们的需求直接使用 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 即可访问了):

img

而且当访问 http 的时候也会自动跳转到 https 上面去,此外我们还可以结合 cert-manager 来实现自动化的 https。

APISIX 认证与自定义插件

身份认证在日常生活当中是非常常见的一项功能,大家平时基本都会接触到。比如用支付宝消费时的人脸识别确认、公司上班下班时的指纹/面部打卡以及网站上进行账号密码登录操作等,其实都是身份认证的场景体现。

img

如上图,Jack 通过账号密码请求服务端应用,服务端应用中需要有一个专门用做身份认证的模块来处理这部分的逻辑。请求处理完毕子后,如果使用 JWT Token 认证方式,服务器会反馈一个 Token 去标识这个用户为 Jack。如果登录过程中账号密码输入错误,就会导致身份认证失败。

但是每个应用服务模块去开发一个单独的身份认证模块,用来支持身份认证的一套流程处理,当服务量多了之后,就会发现这些模块的开发工作量都是非常巨大且重复的。这个时候,我们可以通过把这部分的开发逻辑放置到 Apache APISIX 的网关层来实现统一,减少开发量。

img

如上图所示,用户或应用方直接去请求 Apache APISIX,然后 Apache APISIX 通过识别并认证通过后,会将鉴别的身份信息传递到上游应用服务,之后上游应用服务就可以从请求头中读到这部分信息,然后进行后续的逻辑处理。

Apache APISIX 作为一个 API 网关,目前已开启与各种插件功能的适配合作,插件库也比较丰富。目前已经可与大量身份认证相关的插件进行搭配处理,如下图所示。

img

基础认证插件比如 Key-AuthBasic-Auth,他们是通过账号密码的方式进行认证。复杂一些的认证插件如 Hmac-AuthJWT-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 的方式发送到日志系统进行收集。

img

总体说来 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 认证参数即可,目前支持 basicAuthhmacAuthjwtAuthkeyAuthwolfRBAC 等多种认证类型,在 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:

img

然后我们可以进行如下的测试来进行验证:

# 缺少 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 插件可以根据选择的不同对象做相应的访问限制,该插件可配置的属性如下表所示:

img

其中的 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 消费者页面点击创建消费者:

img

点击下一步进入插件配置页面,这里我们需要启用 jwt-auth 这个插件:

img

在插件配置页面配置 jwt-auth 相关属性,可参考插件文档 https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/:

img

可配置的属性如下表所示:

img

然后提交即可创建完成 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 的插件开发中,整体架构如下所示:

img

左边是 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 的子进程:

img

在插件目录 /apisix-python/apisix/plugins 中的 .py 文件都会被自动加载,上面示例中有两个插件 stop.pyrewrite.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 函数中,该函数只包含 RequestResponse 类对象作为参数,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 中的路由配置格式:

img

接着我们可以来访问 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 自定义插件,我们有任何的业务逻辑需要处理直接去定义一个对应的插件即可。