Higress 或许是目前最好的云原生网关?

共 72346字,需浏览 145分钟

 ·

2024-04-21 19:46

Higress 是基于阿里内部的 Envoy Gateway 实践沉淀、以开源 Istio + Envoy 为核心构建的云原生 API 网关,实现了流量网关 + 微服务网关 + 安全网关三合一的高集成能力,深度集成 Dubbo、Nacos、Sentinel 等微服务技术栈,能够帮助用户极大的降低网关的部署及运维成本;在标准上全面支持 Ingress 与 Gateway API,积极拥抱云原生下的标准 API 规范;同时,Higress Controller 也支持 Nginx Ingress 平滑迁移,帮助用户零成本快速迁移到 Higress。

行业中通常把网关分为两个大类:流量网关与业务网关,流量网关主要提供全局性的、与后端业务无关的策略配置,例如阿里内部的的统一接入网关 Tengine 就是典型的流量网关;业务网关顾名思义主要提供独立业务域级别的、与后端业务紧耦合策略配置,随着应用架构模式从单体演进到现在的分布式微服务,业务网关也有了新的叫法 - 微服务网关。

在虚拟化时期的微服务架构下,业务通常采用流量网关 + 微服务网关的两层架构,流量网关负责南北向流量调度和安全防护,微服务网关负责东西向流量调度和服务治理,而在容器和 K8s 主导的云原生时代,Ingress 成为 K8s 生态的网关标准,赋予了网关新的使命,使得流量网关 + 微服务网关合二为一成为可能。

作为面向南北向的公网网关,使用 Waf 防护异常流量是很常规的需求,而且随着互联网环境变得越来越复杂,用户对防护的诉求是持续增强的,常规做法是将流量先接入 Waf 安全网关,过滤后再将流量转发给流量网关,最后到达微服务网关;Higress 希望通过内置 Waf 模块,使得用户的请求链接只经过 Higress 就可以同时完成 Waf 防护、流量分发、微服务治理,既可以降低链路 RT,也可以降低网关的运维复杂度。因此 Higress 实现了流量网关 + 微服务网关 + 安全网关三合一的高集成能力。

使用场景

Higress 支持多种功能特性,适用于多种场景:

  • Kubernetes Ingress 网关:

    Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。

    支持 Gateway API 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。

  • 微服务网关:

    Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。

    并且深度集成了 Dubbo, Nacos, Sentinel 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。

  • 安全防护网关:

    Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。

核心优势

  • 生产等级

    脱胎于阿里巴巴 2 年多生产验证的内部产品,支持每秒请求量达数十万级的大规模场景。

    彻底摆脱 reload 引起的流量抖动,配置变更毫秒级生效且业务无感。

  • 平滑演进

    支持 Nacos/Zookeeper/Eureka 等多种注册中心,可以不依赖 K8s Service 进行服务发现,支持非容器架构平滑演进到云原生架构。

    支持从 Nginx Ingress Controller 平滑迁移,支持平滑过渡到 Gateway API,支持业务架构平滑演进到 ServiceMesh。

  • 兼容性强

    兼容 Nginx Ingress Annotation 80%+ 的使用场景,且提供功能更丰富的 Higress Annotation 注解。

    兼容 Ingress API/Gateway API/Istio API,可以组合多种 CRD 实现流量精细化管理。

  • 便于扩展

    提供 Wasm、Lua、进程外三种插件扩展机制,支持多语言编写插件,生效粒度支持全局级、域名级,路由级。

    插件支持热更新,变更插件逻辑和配置都对流量无损。

架构

整体上 Higress 网关由控制面组件 higress-controller 和数据面组件 higress-gateway 组成。higress-gateway 负责承载数据流量,higress-controller 负责管理配置下发。

Higress

数据面组件 higress-gateway 是基于 Envoy 开发的网关组件,负责接收和处理流量,支持 HTTP/1.1、HTTP/2、gRPC 等协议,支持 TLS、mTLS、WAF、限流、熔断、重试、负载均衡、路由、转发、重定向、跨域等功能,也就是说真正的流量处理都是在 higress-gateway 中完成的。

控制面组件 higress-controller 负责管理配置下发,支持 Ingress API、Gateway API、Istio API,支持多种注册中心,支持多种认证鉴权策略,支持多种插件扩展机制,支持多种 CRD 实现流量精细化管理,也就是说所有的配置都是通过 higress-controller 下发到 higress-gateway 中的。

安装

Higress 的安装非常简单,只需要通过 Helm 安装即可,如果想要根据自己的需求进行定制,可以通过修改 Helm 的参数来实现,完整参数介绍可以查看运维参数说明,常用的一些可定制参数如下所示:

参数名 参数说明 默认值
全局参数

global.local 如果要安装至本地 K8s 集群(如 Kind、Rancher Desktop 等),请设置为 true false
global.ingressClass 用于过滤被 Higress Controller 监听的 Ingress 资源的 IngressClass。 在集群内部署了多个网关时,可以使用这一参数来区分每个网关的职责范围。 IngressClass 有一些特殊的取值: 1. 如果设置为“nginx”,Higress Controller 将监听 Ingress 为 nginx 或为空的 Ingress 资源。 2. 如果设为空,Higress Controller 将监听 K8s 集群内的全部 Ingress 资源。 higress
global.watchNamespace 如果值不为空,Higress Controller 将只会监听指定命名空间下的资源。 当基于 K8s 命名空间进行业务系统隔离时,若需要对每个命名空间部署一套独立的网关,可以通过这一参数来限制 Higress 监听指定命名空间内的 Ingress。 ""
global.disableAlpnH2 是否在 ALPN 中禁用 HTTP/2 协议 true
global.enableStatus 若为true, Higress Controller 将会更新 Ingress 资源的 status 字段。 为避免从 Nginx Ingress 迁移过程中,覆盖 Ingress 对象的 status 字段,可以将这一参数设置为false,这样 Higress 默认就不会将入口 IP 写入 Ingress 的 status 字段。 true
global.enableIstioAPI 若为true,Higress Controller 将同时监听 istio 资源 false
global.enableGatewayAPI 若为true,Higress Controller 将同时监听 Gateway API 资源 false
global.onlyPushRouteCluster 若为true,Higress Controller 将会只推送被路由关联的服务 true
核心组件参数

higress-core.gateway.replicas Higress Gateway 的 pod 数量 2
higress-core.controller.replicas Higress Controller 的 pod 数量 1
控制台参数

higress-console.replicaCount Higress Console 的 pod 数量 1
higress-console.service.type Higress Console 所使用的 K8s Service 类型 ClusterIP
higress-console.web.login.prompt 登录页面上显示的提示信息 ""
higress-console.o11y.enabled 若为 true,将同时安装可观测性套件(Grafana + Promethues) false
higress-console.pvc.rwxSupported 标识目标 K8s 集群是否支持 PersistentVolumeClaim 的 ReadWriteMany 操作方式。 true

默认情况下 ingress-gateway 会通过一个 LoadBalancer Service 暴露出来,如果你的集群不支持 LoadBalancer 类型的 Service,可以通过修改 higress-core.gateway.service.type 参数来修改 Service 类型,或者通过修改 higress-core.gateway.hostNetwork 参数来直接使用宿主机网络,比如我们这里就使用 hostNetwork 来直接使用宿主机网络:

helm repo add higress.io https://higress.io/helm-charts
helm upgrade --install higress -n higress-system higress.io/higress --set higress-core.gateway.hostNetwork=true,higress-core.gateway.service.type=ClusterIP --create-namespace

另外 Higress 也支持 Istio CRD(可选),但是集群里需要提前安装好 Istio 的 CRD,如果不希望安装 Istio,也可以只安装 Istio 的 CRD:

helm repo add istio https://istio-release.storage.googleapis.com/charts
helm install istio-base istio/base -n istio-system --create-namespace

这种模式下,需要更新 Higress 的部署参数:

helm upgrade higress -n higress-system --set global.enableIstioAPI=true higress.io/higress --reuse-values

同样 Higress 也支持 Gateway API CRD(可选),一样我们也需要提前安装好 Gateway API 的 CRD:https://github.com/kubernetes-sigs/gateway-api/releases,比如我们这里安装 1.0.0 版本的 CRD,具体版本可以根据实际情况进行选择:

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml

这种模式下,需要更新 Higress 的部署参数:

helm upgrade higress -n higress-system --set global.enableGatewayAPI=true higress.io/higress --reuse-values

安装完成后我们可以查看 Higress 的部署情况:

$ kubectl get pods -n higress-system
NAME                                 READY   STATUS    RESTARTS   AGE
higress-console-796544b9df-9v5hn     1/1     Running   0          4m57s
higress-controller-8d4bb7456-4nx6l   2/2     Running   0          4m57s
higress-gateway-596f4f6d9-d46wg      1/1     Running   0          4m57s
higress-gateway-596f4f6d9-mbhp9      1/1     Running   0          4m57s
$ kubectl get svc -n higress-system
NAME                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                                                    AGE
higress-console      ClusterIP   10.96.2.141   <none>        8080/TCP                                                   9m35s
higress-controller   ClusterIP   10.96.0.98    <none>        8888/TCP,15051/TCP,15010/TCP,15012/TCP,443/TCP,15014/TCP   9m35s
higress-gateway      ClusterIP   10.96.3.17    <none>        80/TCP,443/TCP                                             9m35s

接下来我们可以创建一个如下所示的 Ingress 对象来暴露 higress-console 服务:

# higress-console.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: higress-console
  namespace: higress-system
spec:
  ingressClassName: higress
  rules:
    - host: higress.k8s.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: higress-console
                port:
                  number: 8080

直接应用上面的 Ingress 对象即可:

$ kubectl apply -f higress-console.yaml
$ kubectl get ingress -n higress-system
NAME              CLASS     HOSTS               ADDRESS   PORTS   AGE
higress-console   higress   higress.k8s.local             80      15s

由于我们这里将 higress-gateway 设置成了 hostNetwork 模式,所以我们只需要将 higress.k8s.local 解析到 higress-gateway 所在的任一节点即可,这样我们就可以通过 higress.k8s.local 来访问 higress-console 服务了。

初始化管理员

基本使用

当我们第一次访问 higress-console 的时候需要初始化管理员账号,初始化完成后我们就可以通过管理员账号登录到 higress-console 控制台了。

higress console

然后接下来就可以通过 higress-console 控制台来配置我们的网关了,包括创建路由规则、服务、证书等等。

比如现在在 default 命名空间下有如下所示的一个 foo 服务:

# foo.yaml
kind: Pod
apiVersion: v1
metadata:
  name: foo-app
  labels:
    app: foo
spec:
  containers:
    - name: foo-app
      image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/http-echo:0.2.4-alpine
      args:
        - "-text=foo"
---
kind: Service
apiVersion: v1
metadata:
  name: foo-service
spec:
  selector:
    app: foo
  ports:
    # Default port used by the image
    - port: 5678

现在我们希望创建一个对应 http://foo.bar.com/foo 的路由来指向该服务。这里我们有两种方法可以实现,一种是通过 higress-console 控制台来创建,另一种是通过 YAML 文件来创建。

通过控制台创建

首先点击左侧域名管理导航栏,然后点击页面右侧的创建域名按钮,按照下图所示内容填写表单并点击确定按钮。

创建域名

然后点击左侧路由管理导航栏,然后点击页面右侧的创建路由按钮,按照下图片所示内容填写表单并点击确定按钮(要注意选择目标服务)。

创建路由

接下来同样我们只需要将域名 foo.bar.com 解析到 higress-gateway 所在的任一节点即可,这样我们就可以通过 foo.bar.com 来访问 foo-service 服务了。

$ curl http://foo.bar.com/foo
foo

通过 YAML 文件创建

同样我们也可以通过 YAML 文件来创建路由(其实就是一个 Ingress 对象),首先我们需要创建一个 foo-route.yaml 文件,然后使用下方 YAML 来创建我们需要的路由配置。

# foo-route.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: foo
spec:
  ingressClassName: higress
  rules:
    - host: foo.bar.com
      http:
        paths:
          - pathType: Prefix
            path: "/foo"
            backend:
              service:
                name: foo-service
                port:
                  number: 5678

然后我们只需要应用上面的 YAML 文件即可(记得先将控制台中添加的域名和路由删除):

$ kubectl apply -f foo-route.yaml
$ kubectl get ingress
NAME   CLASS     HOSTS         ADDRESS   PORTS   AGE
foo    higress   foo.bar.com             80      4s

同样现在我们也可以通过 foo.bar.com 来访问 foo-service 服务了。

$ curl foo.bar.com/foo
foo

但是需要注意的是通过 YAML 文件管理的路由配置并不会同步到控制台中去,所以在管理路由的时候需要注意。

高级用法

标准的 K8s Ingress 资源只能处理简单场景下的 HTTP(S)流量路由,无法处理流量切分,超时重试,Header 控制和跨域等问题。因此,不同的 Ingress Controller 利用自定义的 Ingress Annotation 增强 Ingress 能力。常见的 Nginx Ingress Controller 引入了 100 多个 Annotation 对 Ingress 在流量治理和安全防护上进行了扩展实现。目前,Higress 已经全面兼容了大部分 Nginx Ingress Annotation,方便用户从 Nginx Ingress 无缝迁移至 Higress,所以我们可以继续使用 Nginx Ingress 的 Annotation 来配置 Higress。

我们可以根据使用习惯继续使用 Nginx Ingress 的 Annotation 前缀 nginx.ingress.kubernetes.io ,或者使用 H igress Ingress 的 Annotation 前缀 higress.io,两者是等价的,下面我们就通过几个简单的例子来演示如何使用 Annotation 来配置 Higress。

跨域

跨域资源共享 CORS 是指允许 Web 应用服务器进行跨域访问控制,从而实现跨域数据安全传输,我们可以通过下面的 Annotation 来配置 Higress 的跨域策略:

  • higress.io/enable-corstrue 或者 false,用于开启或关闭跨域。
  • higress.io/cors-allow-origin:允许的第三方站点,支持泛域名,逗号分隔;支持通配符。默认值为 *,即允许所有第三方站点。
  • higress.io/cors-allow-methods:允许的请求方法,如 GET、POST,逗号分隔;支持通配符 *。默认值为 GET, PUT, POST, DELETE, PATCH, OPTIONS。
  • higress.io/cors-allow-headers:允许的请求头部,逗号分隔;支持通配符 *。默认值为 DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization。
  • higress.io/cors-expose-headers:允许的响应头部,逗号分隔。
  • higress.io/cors-allow-credentialstrue 或者 false。是否允许携带凭证信息。默认允许。
  • higress.io/cors-max-age:预检结果的最大缓存时间,单位为秒;默认值为 1728000。

比如我们现在有一需求是跨域请求被限制为只能来自 example.com 域的请求,并且 HTTP 方法只能是 GET 和 POST,允许的请求头部为 X-Foo-Bar,不允许携带凭证信息,那么我们可以通过下面的 Annotation 来配置 Higress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/enable-cors: "true"
    higress.io/cors-allow-origin: "example.com"
    higress.io/cors-allow-methods: "GET,POST"
    higress.io/cors-allow-headers: "X-Foo-Bar"
    higress.io/cors-allow-credentials: "false"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /hello
            pathType: Exact

Rewrite

在请求转发给目标后端服务之前,Rewrite 重写可以修改原始请求的路径(Path)和主机域(Host),我们可以通过下面的 Annotation 来配置 Higress 的 Rewrite 重写策略:

  • higress.io/rewrite-target:重写 Path,支持捕获组(Capture Group)。
  • higress.io/upstream-vhost:重写 Host。

Rewrite 重写 Path

将请求 example.com/test 在转发至后端服务之前,重写为 example.com/dev

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/rewrite-target: "/dev"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

将请求 example.com/v1/app 在转发至后端服务之前,去掉 Path 前缀 /v1

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/rewrite-target: "/$2"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /v1(/|$)(.*)
            pathType: ImplementationSpecific

将请求 example.com/v1/app 在转发至后端服务之前,将 Path 前缀 /v1 更改为 /v2

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/rewrite-target: "/v2/$2"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /v1(/|$)(.*)
            pathType: ImplementationSpecific

Rewrite 重写 Host

将请求 example.com/test 在转发至后端服务之前,重写为 test.com/test

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/upstream-vhost: "test.com"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

重定向

通过重定向可以将原始客户端请求更改为目标请求。

配置 HTTP 重定向至 HTTPS

我们可以使用下面的 Annotation 来配置 Higress 将 HTTP 请求重定向至 HTTPS:

  • higress.io/ssl-redirect:HTTP 重定向到 HTTPS
  • higress.io/force-ssl-redirect: HTTP 重定向到 HTTPS

Higress 对于以上两个注解不区分对待,都是强制将 HTTP 重定向到 HTTPS。

比如我们需要将请求 http://example.com/test 重定向为 https://example.com/test,我们可以通过下面的 Annotation 来配置 Higress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/ssl-redirect: "true"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

永久重定向

  • higress.io/permanent-redirect:永久重定向的目标 url,必须包含 scheme(http or https)。
  • higress.io/permanent-redirect-code:永久重定向的 HTTP 状态码,默认为 301。

比如将请求 http://example.com/test 永久重定向为 http://example.com/app,我们可以通过下面的 Annotation 来配置 Higress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/permanent-redirect: "http://example.com/app"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

临时重定向

  • higress.io/temporal-redirect:临时重定向的目标 url,必须包含 scheme(http or https)。

需要将请求 http://example.com/test 临时重定向为 http://example.com/app,我们可以通过下面的 Annotation 来配置 Higress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/temporal-redirect: "http://example.com/app"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

配置后端服务协议

Higress 默认使用 HTTP 协议转发请求到后端业务容器。当您的业务容器为 HTTPS 协议时,可以通过使用注解 higress.io/backend-protocol: "HTTPS";当您的业务容器为 GRPC 服务时,可以通过使用注解 higress.io/backend-protocol: "GRPC"

相比 Nginx Ingress 的优势,如果您的后端服务所属的 K8s Service 资源中关于 Port Name 的定义为 grpchttp2,您无需配置注解 higress.io/backend-protocol: "GRPC",Higress 会自动使用 GRPC 或者 HTTP2。

比如我们需要请求 example/test 转发至后端服务使用 HTTPS 协议,我们可以通过下面的 Annotation 来配置 Higress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/backend-protocol: "HTTPS"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /
            pathType: Exact

如果需要请求 example/test 转发至后端服务使用 GRPC 协议,第一种做法就是通过注解,如下所示:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/backend-protocol: "GRPC"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

第二种做法是通过指定 Service Port Name 为 grpc即可,如下所示:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /order
            pathType: Exact
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  ports:
    - name: grpc # 指定 Service Port Name 为 grpc,Higress 会自动使用 GRPC 协议
      port: 80
      protocol: TCP
  selector:
    app: demo-service

超时

Higress 提供路由级别的超时设置,与 nginx ingress 不同,没有区分连接/读写超时,而是面向的接口处理总延时进行配置,在未进行配置时默认不限制,例如后端未返回应答,网关将无限等待。

设置超时时间为 5s:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/timeout: "5"
  name: demo
spec:
  ingressClassName: higress
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              service:
                name: demo-service
                port:
                  number: 80
            path: /test
            pathType: Exact

灰度发布

Higress 提供复杂的路由处理能力,和 Ingress Nginx 类似,Higress 也支持基于 Header、Cookie 以及权重的灰度发布功能。灰度发布功能可以通过设置注解来实现,为了启用灰度发布功能,需要设置注解 higress.io/canary: "true"。通过不同注解可以实现不同的灰度发布功能。

当多种方式同时配置时,灰度方式选择优先级为:基于 Header > 基于 Cookie > 基于权重(从高到低)。

下面我们通过一个示例应用来对灰度发布功能进行说明。

第一步. 部署 Production 应用

首先创建一个 production 环境的应用资源清单:

# production.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: production
  labels:
    app: production
spec:
  selector:
    matchLabels:
      app: production
  template:
    metadata:
      labels:
        app: production
    spec:
      containers:
        - name: production
          image: mirrorgooglecontainers/echoserver:1.10
          ports:
            - containerPort: 8080
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
  name: production
  labels:
    app: production
spec:
  ports:
    - port: 80
      targetPort: 8080
      name: http
  selector:
    app: production

然后创建一个用于 production 环境访问的 Ingress 资源对象:

# production-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: production
spec:
  ingressClassName: higress
  rules:
    - host: echo.k8s.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: production
                port:
                  number: 80

直接创建上面的几个资源对象:

☸ ➜ kubectl apply -f production.yaml
☸ ➜ kubectl apply -f production-ingress.yaml
☸ ➜ kubectl get pods -l app=production
NAME                          READY   STATUS    RESTARTS   AGE
production-64cfc46b65-j6krb   1/1     Running   0          56s
☸ ➜ kubectl get ingress production
NAME         CLASS     HOSTS            ADDRESS   PORTS   AGE
production   higress   echo.k8s.local             80      9s

应用部署成功后即可正常访问应用:

☸ ➜ curl http://echo.k8s.local


Hostname: production-64cfc46b65-j6krb

Pod Information:
        node name:      node1
        pod name:       production-64cfc46b65-j6krb
        pod namespace:  default
        pod IP: 10.0.1.249

Server values:
        server_version=nginx: 1.13.3 - lua: 10008

Request Information:
        client_address=10.0.1.162
        method=GET
        real path=/
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://echo.k8s.local:8080/

Request Headers:
        accept=*/*
        host=echo.k8s.local
        original-host=echo.k8s.local
        req-start-time=1713508680651
        user-agent=curl/7.87.0
        x-b3-sampled=0
        x-b3-spanid=5ec284bec822750c
        x-b3-traceid=5c48c3cc4192ddc85ec284bec822750c
        x-envoy-attempt-count=1
        x-envoy-decorator-operation=production.default.svc.cluster.local:80/*
        x-envoy-internal=true
        x-forwarded-for=192.168.0.112
        x-forwarded-proto=http
        x-request-id=d42c8455-5b2e-471d-811c-65f80210ccec

Request Body:
        -no body in request-

第二步. 创建 Canary 版本

参考将上述 Production 版本的 production.yaml 文件,再创建一个 Canary 版本的应用。

# canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary
  labels:
    app: canary
spec:
  selector:
    matchLabels:
      app: canary
  template:
    metadata:
      labels:
        app: canary
    spec:
      containers:
        - name: canary
          image: mirrorgooglecontainers/echoserver:1.10
          ports:
            - containerPort: 8080
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
  name: canary
  labels:
    app: canary
spec:
  ports:
    - port: 80
      targetPort: 8080
      name: http
  selector:
    app: canary

接下来就可以通过配置 Annotation 规则进行流量切分了。

基于 Header 灰度发布

当我们需要通过 Header 来进行灰度发布时,我们可以通过下面的 Annotation 来配置 Higress 的灰度发布策略:

  • 只配置 higress.io/canary-by-header:基于 Request Header 的名称进行流量切分。当请求包含该 Header 并其值为 always 时,请求流量会被分配到灰度服务入口;其他情况时,请求流量不会分配到灰度服务。
  • 同时配置 higress.io/canary-by-headerhigress.io/canary-by-header-value:基于 Request Header 的名称和值进行流量切分。当请求中的 header 的名称和 header 的值与该配置匹配时,请求流量会被分配到灰度服务;其他情况时,请求流量不会分配到灰度服务。

比如现在当请求 Header 为 higress:always 时将访问灰度服务 canary;其他情况将访问正式服务 production,那么我们可以创建如下的 Ingress 对象:

# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/canary: "true"
    higress.io/canary-by-header: "higress"
  name: canary
spec:
  ingressClassName: higress
  rules:
    - host: echo.k8s.local
      http:
        paths:
          - backend:
              service:
                name: canary
                port:
                  number: 80
            path: /
            pathType: Prefix

直接应用上面的 YAML 文件即可:

☸ ➜ kubectl apply -f canary.yaml
☸ ➜ kubectl apply -f canary-ingress.yaml
☸ ➜ kubectl get pods -l app=canary
NAME                      READY   STATUS    RESTARTS   AGE
canary-7d97679b67-sh2r8   1/1     Running   0          12m
☸ ➜ kubectl get ingress canary
NAME     CLASS     HOSTS            ADDRESS   PORTS   AGE
canary   higress   echo.k8s.local             80      37s

更新上面的 Ingress 资源对象后,我们在请求中加入不同的 Header 值,再次访问应用的域名来查看效果。

☸ ➜ for i in $(seq 1 10); do curl -s echo.k8s.local | grep "Hostname"; done
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb

这里我们在请求的时候没有配置任何 Header,所以请求没有发送到 Canary 应用中去,如果设置为其他值呢:

☸ ➜ for i in $(seq 1 10); do curl -s -H "higress: always" echo.k8s.local | grep "Hostname"; done
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8

由于我们请求设置的 Header 值为 higress: always,所以请求全都路由到了 canary 的服务,如果是其他的值则不会路由到 canary 的服务。

这个时候我们可以在上一个 annotation (即 canary-by-header)的基础上添加一条 higress.io/canary-by-header-value: user-value 这样的规则,就可以将指定 Header 值的请求路由到 Canary Ingress 中指定的服务了。

annotations:
  higress.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
  higress.io/canary-by-header: "higress" # 基于header的流量切分
  higress.io/canary-by-header-value: "user-value"

同样更新 Ingress 对象后,重新访问应用,当 Request Header 满足 higress: user-value时,所有请求就会被路由到 Canary 版本:

☸ ➜ for i in $(seq 1 10); do curl -s -H "higress: user-value" echo.k8s.local | grep "Hostname"; done
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8

基于 Cookie 灰度发布

与基于 Request Header 的 annotation 用法规则类似。例如在 A/B 测试场景下,需要让地域为北京的用户访问 Canary 版本。那么当 cookie 的 annotation 设置为 higress.io/canary-by-cookie: "users_from_Beijing",此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置 cookie users_from_Beijing 的值为 always,这样就可以确保北京的用户仅访问 Canary 版本。

基于 Cookie 的灰度发布不支持自定义设置 Key 对应的值,只能是 always。

比如我们现在请求的 Cookie 为 users_from_Beijing=always 时将访问灰度版本服务 canary;其他情况将访问正式服务 production,则我们可以创建如下的 Ingress 对象:

# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/canary: "true"
    higress.io/canary-by-cookie: "users_from_Beijing"
  name: canary
spec:
  ingressClassName: higress
  rules:
    - host: echo.k8s.local
      http:
        paths:
          - backend:
              service:
                name: canary
                port:
                  number: 80
            path: /
            pathType: Prefix

更新上面的 Ingress 资源对象后,我们在请求中设置一个 users_from_Beijing=always 的 Cookie 值,再次访问应用的域名。

☸ ➜ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=always" echo.k8s.local | grep "Hostname"; done
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8
Hostname: canary-7d97679b67-sh2r8

我们可以看到应用都被路由到了 Canary 版本的应用中去了,如果我们将这个 Cookie 值设置为其他值,则不会路由到 Canary 应用中。

基于权重灰度发布

基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为 0 或 100 来实现。例如,可将 Green 版本设置为主要部分,并将 Blue 版本的入口配置为 Canary。最初,将权重设置为 0,因此不会将流量代理到 Blue 版本。一旦新版本测试和验证都成功后,即可将 Blue 版本的权重设置为 100,即所有流量从 Green 版本转向 Blue。

在 Higress 中,我们可以通过下面的 Annotation 来配置基于权重的灰度发布策略:

  • higress.io/canary-weight:设置请求到指定服务的百分比(值为 0~100 的整数)
  • higress.io/canary-weight-total:设置权重总和,默认为 100

比如我们配置灰度服务 canary 的权重为 20%,配置正式服务 production 的权重为 80%,则我们可以创建如下的 Ingress 对象:

# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    higress.io/canary: "true"
    higress.io/canary-weight: "20" # 分配20%流量到当前Canary版本
  name: canary
spec:
  ingressClassName: higress
  rules:
    - host: echo.k8s.local
      http:
        paths:
          - backend:
              service:
                name: canary
                port:
                  number: 80
            path: /
            pathType: Prefix

更新上面的 Ingress 对象,接下来我们在命令行终端中来不断访问这个应用,观察 Hostname 变化:

☸ ➜ for i in $(seq 1 10); do curl -s echo.k8s.local | grep "Hostname"; done
Hostname: production-64cfc46b65-j6krb
Hostname: canary-7d97679b67-sh2r8
Hostname: production-64cfc46b65-j6krb
Hostname: canary-7d97679b67-sh2r8
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb
Hostname: production-64cfc46b65-j6krb

由于我们给 Canary 版本应用分配了 20% 左右权重的流量,所以上面我们访问 10 次有 3 次(不是一定)访问到了 Canary 版本的应用,符合我们的预期。

到这里我们就通过 Higress 的灰度发布功能实现了流量的切分,可以根据不同的场景选择不同的灰度发布方式。

插件

Higress 提供 Wasm、Lua、进程外三种插件扩展机制,可以通过插件机制实现自定义的功能扩展。支持多语言编写插件,生效粒度支持全局级、域名级,路由级。插件支持热更新,变更插件逻辑和配置都对流量无损。

要为 Higress 启用插件,一共有两种方法,一是通过 Higress 控制台进行配置,另外还可以通过 Higress WasmPlugin CRD 进行配置。

通过 Higress 控制台进行配置

Higress 控制台提供了 3 个入口进行插件配置:

  • 全局配置:插件市场 -> 选择插件进行配置
  • 域名级配置:域名管理 -> 选择域名 -> 点击策略-> 选择插件进行配置
  • 路由级配置: 路由配置 -> 选择路由 -> 点击策略 -> 选择插件进行配置

这三个配置的生效优先级是: 路由级 > 域名级 > 全局,也就是对于没有匹配到具体路由或域名的请求才会生效全局配置。

对于一般的插件,包括自定义插件在内,路由/域名级的配置字段和全局配置字段是完全一样的;对于认证类插件(Key 认证、HMAC 认证、Basic 认证、JWT 认证等)则不同,全局配置仅做 Consumer 凭证配置,以及是否开启全局认证,而在路由/域名级通过 allow 字段配置允许访问的 Consumer 列表。

比如我们要给前面的 foo.bar.com 服务配置一个 Basic Auth 插件,首先需要在全局配置启用 Basic Auth 插件,然后在域名级(或者路由级)配置 Basic Auth 插件。

在控制台中切换到插件市场,找到 Basic Auth 插件,点击配置,然后在数据编辑器中通过 consumers 字段配置用户凭证,如下所示:

global_auth: false
consumers: # 配置两个用户凭证
  - name: consumer1
    credential: admin:123456
  - name: consumer2
    credential: guest:abc

要记得选择开启状态,然后点击保存。

配置 Basic 认证

接下来切换到域名管理页面,添加一个域名 foo.bar.com,然后点击策略,选择 Basic Auth 插件进行配置,。

在数据编辑器中通过 allow 字段配置允许访问的用户,如下所示:

allow:
  - consumer1

要记得选择开启状态,然后点击保存。这里我们配置的是只允许 consumer1 用户访问 foo.bar.com 服务,其他调用者不允许访问。

现在我们可以访问下 foo.bar.com 服务进行验证:

$ curl -v foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
> GET /foo HTTP/1.1
> Host: foo.bar.com
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< www-authenticate: Basic realm=MSE Gateway
< date: Sat, 20 Apr 2024 06:05:15 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host foo.bar.com left intact

可以看到返回了 401 Unauthorized,因为我们没有提供用户凭证,接下来我们提供用户凭证进行访问:

$ curl -v -u admin:123456 foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
* Server auth using Basic with user 'admin'
> GET /foo HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic YWRtaW46MTIzNDU2
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< x-app-name: http-echo
< x-app-version: 0.2.4
< date: Sat, 20 Apr 2024 06:06:10 GMT
< content-length: 4
< content-type: text/plain; charset=utf-8
< req-cost-time: 1
< req-arrive-time: 1713593170671
< resp-start-time: 1713593170673
< x-envoy-upstream-service-time: 1
< server: istio-envoy
<
foo
* Connection #0 to host foo.bar.com left intact

可以看到我们提供了用户凭证后成功访问了服务,如果我们提供的用户凭证不在允许访问的用户列表中,则会返回 403 Forbidden 错误。

$ curl -v -u guest:abc foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
* Server auth using Basic with user 'guest'
> GET /foo HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic Z3Vlc3Q6YWJj
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< www-authenticate: Basic realm=MSE Gateway
< date: Sat, 20 Apr 2024 06:06:54 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host foo.bar.com left intact

通过 Higress WasmPlugin 进行配置

除了通过控制台可以配置插件,还可以通过 Higress WasmPlugin CRD 进行配置,Higress WasmPlugin CRD 是 Istio WasmPlugin CRD 的扩展,新增了一些配置字段。

字段名称 数据类型 填写要求 描述
defaultConfig object 选填 插件默认配置,全局生效于没有匹配具体域名和路由配置的请求
matchRules array of object 选填 匹配域名或路由生效的配置

matchRules中每一项的配置字段说明:

字段名称 数据类型 填写要求 配置示例 描述
ingress array of string ingressdomain中必填一项 ["default/foo","default/bar"] 匹配 ingress 资源对象,匹配格式为: 命名空间/ingress名称
domain array of string ingressdomain中必填一项 ["example.com","*.test.com"] 匹配域名,支持泛域名
config object 选填 - 匹配后生效的插件配置

同样如果要通过 WasmPlugin 来配置实现 Basic Auth 插件,首先需要创建一个 WasmPlugin CRD 对象,如下所示:

# basic-auth-plugin.yaml
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: basic-auth
  namespace: higress-system
spec:
  url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/basic-auth:1.0.0 # 插件镜像地址
  defaultConfig:
    consumers:
      - name: consumer1
        credential: admin:123456
      - name: consumer2
        credential: guest:abc
  matchRules:
    - domain:
        - foo.bar.com
      config:
        allow:
          - consumer2

将上面通过控制台配置的 Basic Auth 插件配置重置还原,不启用 Basic Auth 插件。

在上面的 WasmPlugin 对象中我们通过 defaultConfig 字段配置了用户凭证,这部分其实就对应于控制台中的全局配置,然后通过 matchRules 字段配置了域名级的配置,这部分对应于控制台中的域名级配置,当然除了 domain 之外还可以配置 ingress 字段,这部分对应于控制台中的路由级配置。比如上面的配置表示只允许 consumer2 用户访问 foo.bar.com 服务。

直接创建上面的 WasmPlugin 对象:

☸ ➜ kubectl apply -f basic-auth-plugin.yaml

然后再次访问 foo.bar.com 服务进行验证:

☸ ➜ curl -v foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
> GET /foo HTTP/1.1
> Host: foo.bar.com
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< www-authenticate: Basic realm=MSE Gateway
< date: Sat, 20 Apr 2024 06:23:48 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host foo.bar.com left intact

可以看到返回了 401 Unauthorized,因为我们没有提供用户凭证,接下来我们提供用户凭证进行访问:

☸ ➜ curl -v -u guest:abc foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
* Server auth using Basic with user 'guest'
> GET /foo HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic Z3Vlc3Q6YWJj
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< x-app-name: http-echo
< x-app-version: 0.2.4
< date: Sat, 20 Apr 2024 06:24:49 GMT
< content-length: 4
< content-type: text/plain; charset=utf-8
< req-cost-time: 1
< req-arrive-time: 1713594289845
< resp-start-time: 1713594289847
< x-envoy-upstream-service-time: 0
< server: istio-envoy
<
foo
* Connection #0 to host foo.bar.com left intact

上面我们在 WasmPlugin 中配置了只允许 consumer2 用户访问 foo.bar.com 服务,所以只有提供了 consumer2 用户凭证的请求才能访问服务,所以这里我们提供的 guest:abc 用户凭证成功访问了服务,如果提供的用户凭证不在允许访问的用户列表中,则会返回 403 Forbidden 错误。

☸ ➜ curl -v -u admin:123456 foo.bar.com/foo
*   Trying 192.168.0.116:80...
* Connected to foo.bar.com (192.168.0.116) port 80 (#0)
* Server auth using Basic with user 'admin'
> GET /foo HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic YWRtaW46MTIzNDU2
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< www-authenticate: Basic realm=MSE Gateway
< date: Sat, 20 Apr 2024 06:26:00 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host foo.bar.com left intact

这里我们就实现了通过 Higress WasmPlugin CRD 对象配置 Basic Auth 插件,实现了用户凭证认证功能。除了 Basic Auth 插件,Higress 还提供了很多其他插件,比如 Key Auth 插件、HMAC Auth 插件、JWT Auth 插件等,可以根据实际需求选择不同的插件进行配置,每种插件的配置属性可以参考控制台中的插件市场的配置说明。

自定义插件

除了 Higress 提供的内置插件外,有时候我们可能会有一些特殊的需求,这时候我们可以通过自定义插件来实现自定义的功能扩展。我们可以使用多种编程语言来编写插件,生效粒度支持全局级、域名级,路由级,此外插件还支持热更新,变更插件逻辑和配置都对流量无损。下面我们就用一个简单的示例来说明下如何编写一个自定义插件。

首先需要安装 Golang 和 TinyGo 两个程序。Golang 要求 1.18 版本以上,官方指引链接:https://go.dev/doc/install;TinyGo 要求 0.28.1 版本以上,官方指引链接:https://tinygo.org/getting-started/install/。

☸ ➜ go version
go version go1.20.14 darwin/arm64
☸ ➜ tinygo version
tinygo version 0.30.0 darwin/amd64 (using go version go1.20.14 and LLVM version 16.0.1)

接下来首先我们初始化工程目录,新建一个名为 wasm-demo-go 的目录,在所建目录下执行以下命令,进行 Go 工程初始化

go mod init wasm-demo-go

国内环境需要设置下载依赖包的代理

go env -w GOPROXY=https://proxy.golang.com.cn,direct

然后下载构建插件的依赖:

go get github.com/higress-group/proxy-wasm-go-sdk
go get github.com/alibaba/higress/plugins/wasm-go@main
go get github.com/tidwall/gjson

接下来我们来编写一个简单的插件,实现一个简单的功能,当插件配置中有 mockEnable: true 时直接返回 hello world 应答;未做插件配置,或者设置 mockEnable: false 时给原始请求添加 hello: world 请求 Header 头。

package main

import (
    "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
    "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
    "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
    "github.com/tidwall/gjson"
)

func main() {
    wrapper.SetCtx(
        // 插件名称
        "my-plugin",
        // 为解析插件配置,设置自定义函数
         wrapper.ParseConfigBy(parseConfig),
        // 为处理请求头,设置自定义函数
        wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
    )
}

// 自定义插件配置
type MyConfig struct {
    mockEnable bool
}

// 在控制台插件配置中填写的yaml配置会自动转换为json,此处直接从json这个参数里解析配置即可
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
    // 解析出配置,更新到config中
    config.mockEnable = json.Get("mockEnable").Bool()
    return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
    proxywasm.AddHttpRequestHeader("hello""world")
    if config.mockEnable {
        proxywasm.SendHttpResponse(200nil, []byte("hello world"), -1)
    }
    return types.ActionContinue
}

在网关控制台中的插件配置为 yaml 格式,下发给插件时将自动转换为 json 格式,所以例子中的 parseConfig 可以直接从 json 中解析配置。

上面代码中通过 wrapper.ProcessRequestHeadersBy 将自定义函数 onHttpRequestHeaders 用于 HTTP 请求头处理阶段处理请求,除此之外,还可以通过下面方式,设置其他阶段的自定义处理函数。

HTTP 处理阶段 触发时机 挂载方法
HTTP 请求头处理阶段 网关接收到客户端发送来的请求头数据时 wrapper.ProcessRequestHeadersBy
HTTP 请求 Body 处理阶段 网关接收到客户端发送来的请求 Body 数据时 wrapper.ProcessRequestBodyBy
HTTP 应答头处理阶段 网关接收到后端服务响应的应答头数据时 wrapper.ProcessResponseHeadersBy
HTTP 应答 Body 处理阶段 网关接收到后端服务响应的应答 Body 数据时 wrapper.ProcessResponseBodyBy

上面示例代码中的 proxywasm.AddHttpRequestHeaderproxywasm.SendHttpResponse 是插件 SDK 提供的两个工具方法,主要的工具方法见下表:

分类 方法名称 用途 可以生效的 HTTP 处理阶段
请求头处理 GetHttpRequestHeaders 获取客户端请求的全部请求头 HTTP 请求头处理阶段

ReplaceHttpRequestHeaders 替换客户端请求的全部请求头 HTTP 请求头处理阶段

GetHttpRequestHeader 获取客户端请求的指定请求头 HTTP 请求头处理阶段

RemoveHttpRequestHeader 移除客户端请求的指定请求头 HTTP 请求头处理阶段

ReplaceHttpRequestHeader 替换客户端请求的指定请求头 HTTP 请求头处理阶段

AddHttpRequestHeader 新增一个客户端请求头 HTTP 请求头处理阶段
请求 Body 处理 GetHttpRequestBody 获取客户端请求 Body HTTP 请求 Body 处理阶段

AppendHttpRequestBody 将指定的字节串附加到客户端请求 Body 末尾 HTTP 请求 Body 处理阶段

PrependHttpRequestBody 将指定的字节串附加到客户端请求 Body 的开头 HTTP 请求 Body 处理阶段

ReplaceHttpRequestBody 替换客户端请求 Body HTTP 请求 Body 处理阶段
应答头处理 GetHttpResponseHeaders 获取后端响应的全部应答头 HTTP 应答头处理阶段

ReplaceHttpResponseHeaders 替换后端响应的全部应答头 HTTP 应答头处理阶段

GetHttpResponseHeader 获取后端响应的指定应答头 HTTP 应答头处理阶段

RemoveHttpResponseHeader 移除后端响应的指定应答头 HTTP 应答头处理阶段

ReplaceHttpResponseHeader 替换后端响应的指定应答头 HTTP 应答头处理阶段

AddHttpResponseHeader 新增一个后端响应头 HTTP 应答头处理阶段
应答 Body 处理 GetHttpResponseBody 获取客户端请求 Body HTTP 应答 Body 处理阶段

AppendHttpResponseBody 将指定的字节串附加到后端响应 Body 末尾 HTTP 应答 Body 处理阶段

PrependHttpResponseBody 将指定的字节串附加到后端响应 Body 的开头 HTTP 应答 Body 处理阶段

ReplaceHttpResponseBody 替换后端响应 Body HTTP 应答 Body 处理阶段
HTTP 调用 DispatchHttpCall 发送一个 HTTP 请求 -

GetHttpCallResponseHeaders 获取 DispatchHttpCall 请求响应的应答头 -

GetHttpCallResponseBody 获取 DispatchHttpCall 请求响应的应答 Body -

GetHttpCallResponseTrailers 获取 DispatchHttpCall 请求响应的应答 Trailer -
直接响应 SendHttpResponse 直接返回一个特定的 HTTP 应答 -
流程恢复 ResumeHttpRequest 恢复先前被暂停的请求处理流程 -

ResumeHttpResponse 恢复先前被暂停的应答处理流程 -

接下来我们需要编译生成 WASM 文件:

go mod tidy
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer" ./main.go

编译成功会在当前目录下生成文件 main.wasm,要在 Higress 中配合 Wasmplugin CRD 或者 Console 的 UI 交互配置该插件,需要将该 wasm 文件打包成 oci 或者 docker 镜像。

在项目根目录下面创建一个 Dockerfile 文件,内容如下:

FROM scratch
COPY main.wasm plugin.wasm

然后执行以下命令构建并推送镜像:

docker build -t cnych/higress-plugin-demo:1.0.0 .
docker push cnych/higress-plugin-demo:1.0.0

这样我们就将自定义插件打包成了一个 Docker 镜像。然后在网关控制台的插件市场中创建一个自定义插件,填入上面构建出的 Wasm 镜像地址即可。

添加插件

创建完成后,点击插件卡片的配置按钮,填入插件的配置 mockEnable: true,打开开启开关就生效了。如果插件逻辑发生了变更,可以构建一个新的镜像,并使用不同的镜像 tag,点插件卡片右上方菜单中的编辑按钮,将 Wasm 镜像地址修改为新版本的地址即可。

不过需要注意的是开启后会对所有请求生效,所以在生产环境中需要谨慎使用,可以通过域名级或者路由级配置插件来控制插件的生效范围。

除了通过控制台配置插件,还可以通过 Higress WasmPlugin CRD 对象配置插件,这样可以更灵活的控制插件的生效范围。

# wasm-plugin-demo.yaml
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: plugin-demo
  namespace: higress-system
spec:
  url: oci://docker.io/cnych/higress-plugin-demo:1.0.0
  defaultConfig:
    mockEnable: false
  matchRules:
    - domain:
        - foo.bar.com
      config:
        mockEnable: true

直接应用上面的 WasmPlugin 对象:

☸ ➜ kubectl apply -f wasm-plugin-demo.yaml

然后我们再次访问 foo.bar.com 服务进行验证:

☸ ➜ curl foo.bar.com/foo
hello world

可以看到我们提供了插件配置 mockEnable: true 后直接返回了 hello world 应答,如果我们将插件配置设置为 mockEnable: false,则会给原始请求添加 hello: world 请求 Header 头。

这样我们就通过 Higress WasmPlugin CRD 对象配置了自定义插件,实现了自定义的功能扩展。

这里对插件的生效机制简单做个说明:

  1. 用户将代码编译成 wasm 文件
  2. 用户将 wasm 文件构建成 docker 镜像
  3. 用户将 docker 镜像推送至镜像仓库
  4. 用户创建 WasmPlugin 资源
  5. Istio watch 到 WasmPlugin 资源的变化
  6. Higress Gateway 中的 xDS proxy 进程从 Istio 获取到配置,发现插件的镜像地址
  7. xDS proxy 从镜像仓库拉取镜像
  8. xDS proxy 从镜像中提取出 wasm 文件
  9. Higress Gateway 中的 envoy 进程从 xDS proxy 获取到配置,发现 wasm 文件的本地路径
  10. Envoy 从本地文件中加载 wasm 文件
插件生效

这里 envoy 获取配置并加载 wasm 文件使用到了 ECDS (Extension Config Discovery Service)的机制,实现了 wasm 文件更新,直接热加载,不会导致任何连接中断,业务流量完全无损。

除了本文介绍的 Higress 功能之外,还有很多其他功能和最佳实践方案,更多内容可以参考 Higress 官方文档。

参考文档:https://higress.io


浏览 2313
18点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
18点赞
评论
收藏
分享

手机扫一扫分享

分享
举报