Ingress 的继任者 —— Gateway API?

2021-07-19 10:23:24 浏览数 (1)

在 Kubernetes 集群边缘对外提供网络服务的时候,通常需要借助 Ingress 对象,这个对象提供了暴露 Service 所必须的核心要素,例如基于主机名的路由、对 URL 路径的适配以及 TLS 配置等。但是在实际开放服务的时候,往往会有更多的具体需求,这时 Ingress 对象所提供的核心功能就有些力不从心了,各种 Ingress 控制器往往会使用 metadata.annotations 中的特定注解,来完成对 Ingress 特定行为的控制,完成各自的个性化功能,例如认证、路径变更、黑白名单等,这就让 Ingress 对象变成了一个奇怪的东西:结构化的核心结构,和非结构化的标注结合起来形成各种 Ingress 方言,并且后期还出现了 Traefik Middleware 这样的 CRD 配置,这给 Ingress 功能的集中管理造成了一个较大的困扰;另外 Ingress 中可以随意定制主机名、路径以及后端服务,也给共享集群的用户造成了一定的安全隐患。包括 Cotour、Traefik 在内的 Ingress 控制器后期都提供了各自的基于 CRD 的功能表达,客观上也让 Ingress 世界更为分裂。 例如要移除路径前缀,Nginx Ingress 控制器需要使用 nginx.ingress.kubernetes.io/rewrite-target 注解,而 Traefik 1.7 中则需要使用 traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip 注解。

SIG-Network 基于实际现状和需求,提出了全新的 Gateway API 来作为 Ingress 的继任者,总体来说,相对于 Ingress,Gateway API 有几个显著特点:

  1. 职责分离,运维、开发等不同的角色都能够在适合的边界内完成工作;
  2. 扩展核心能力,并使用更结构化的方式进行表达;
  3. 易于扩展:Gateway API 为各种不同实现的控制器提供了一致的扩展方法。

目前该 API 还处于 Alpha 阶段,也仅有少量控制器提供了早期支持。下面做一些陈述和试验,来看看 Gateway API 有什么不一样。

概念层次

Ingress 中包含了 IngressClass/Ingress 两层概念,而 Gateway API 包含了三层概念:GatewayClass、Gateway 和 Route,其中的 Route 实际是包含了 HTTPRoute、TCPRoute、TLSRoute 和 UDPRoute 在内的一组对象。

GatewayClass

它是一个集群范围内的资源,由云基础设施中的 Gateway API 控制器提供,其职责和原有的 Ingress Class 类似。

Gateway

Gateway 对象是命名空间范围对象,可以视作是 GatewayClass 的一个实例,它通常是由具体机群的运维人员进行维护的,在 Gateway 对象中可以指定该对象负责的主机名称范围,用标签选择器选择对应的 Service,甚至还可以指定该 Gateway 生效的命名空间。这样就给具体应用的对外开放划定了一个范围,防止应用随意占用主机名,并完善命名空间的隔离能力。

Route

前文讲到,Route 对象除了像原有的 Ingress 对象一样提供 HTTP 服务的开放能力之外,还提供了 TCP、TLS 和 UDP 的对应资源,从而缓解了 Nginx、HAProxy Ingress 控制器使用 Configmap 配置 TCP/UDP 的窘境。HTTPRoute 除了提供基础的 Ingress 对象能力之外,还提供了一些“越界”的功能,例如对流量进行复制、分流;更重要的是其中还提供了 Filter 能力,这是一个扩展点,除了自带的核心处理能力之外,底层设施还可以在这里接入自己的 CRD,对流量进行处理,从而为流量处理能力的扩展提供了一个统一入口。UDPRoute 和 TCPRoute 也提供了对流量的判别能力,但是这部分仅提供了扩展点,而没有像 HTTP 一样的成熟能力。

举个栗子

目前 GKE 提供了 Gateway API 的公共预览版可以用于测试,仅限于以下区域的 1.20 以上版本的集群:

  • us-west1
  • us-east1
  • us-central1
  • europe-west4
  • europe-west3
  • europe-west2
  • europe-west1
  • asia-southeast1

不同区域的集群缺省开关可能不一致,注意需要在控制台的网络页面启用 HTTP 负载均衡功能,或者在命令行中的 --addons 参数值里加入 HttpLoadBalancing

使用如下命令部署网关资源:

代码语言:javascript复制
$ kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v0.3.0" 
| kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/backendpolicies.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/gateways.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/httproutes.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/tcproutes.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/tlsroutes.networking.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/udproutes.networking.x-k8s.io created

部署完成之后,网关控制器会被触发,创建两个 GatewayClass

代码语言:javascript复制
$ kubectl get gatewayclasses
NAME          CONTROLLER                  AGE
gke-l7-gxlb   networking.gke.io/gateway   1s
gke-l7-rilb   networking.gke.io/gateway   1s

不难发现,我们使用的是 HTTP 负载均衡,新建的 Gateway Class 也都包含 l7 字样,其实官方文档也明确说明:

注意:GKE Gateway Controller 仅支持 GatewayClass、网关和 HTTPRoute。不支持 TCPRoute、UDPRoute 和 TLSRoute。

这里初始化了两个 GatewayClass,gxlb 用于外部,rilb 用于内部,所以我们要在外网测试,就要用 gxlb 创建网关。

一个简单的工作负载:

代码语言:javascript复制
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: flaskapp-v1
  name: flaskapp-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flaskapp-v1
  template:
    metadata:
      labels:
        app: flaskapp-v1
    spec:
      containers:
      - image: dustise/flaskapp:v0.2.6
        name: flaskapp
        env:
        - name: "VERSION"
          value: "v1"
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: flaskapp-v1
  name: flaskapp-v1
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: flaskapp-v1
  type: ClusterIP

创建 Gateway:

代码语言:javascript复制
kind: Gateway
apiVersion: networking.x-k8s.io/v1alpha1
metadata:
  name: gateway-gen
spec:
  gatewayClassName: gke-l7-gxlb
  listeners:
  - protocol: HTTP
    port: 80
    hostname: "*.microservice.xyz"
    routes:
      kind: HTTPRoute
      selector:
        matchLabels:
          app: flaskapp-v1
      namespaces:
        from: "All"

这个 Gateway 对象中仅定义了一个 Listener 对象,其中规定可以使用的域名为 *.microservice.xyzHTTPRoute,选择器要求使用这个对象的 Route 必须使用 app=flaskapp-v1 的标签,可以在所有命名空间中进行引用。创建之后查看一下状态:

代码语言:javascript复制
$ kubectl describe gateway gateway-gen

...
Spec:
  Gateway Class Name:  gke-l7-gxlb
  Listeners:
    Hostname:  *.microservice.xyz
    Port:      80
    Protocol:  HTTP
    Routes:
      Group:  networking.x-k8s.io
      Kind:   HTTPRoute
      Namespaces:
        From:  All
      Selector:
        Match Labels:
          App:  flaskapp-v1
Status:
  Addresses:
    Type:   IPAddress
    Value:  37.132.121.12
...

看到 Gateway 中已经得到了一个 IP 地址。接下来建立一个对应的 HTTPRoute 对象:

代码语言:javascript复制
kind: HTTPRoute
apiVersion: networking.x-k8s.io/v1alpha1
metadata:
  name: flaskapp-v1
  labels:
    app: flaskapp-v1
spec:
  hostnames:
  - "v1.microservice.xyz"
  - "v1.microservice.rocks"
  rules:
  - forwardTo:
    - serviceName: flaskapp-v1
      port: 80

这个里面我们用了一个多余的域名:v1.microservice.rocks,看看运行结果:

代码语言:javascript复制
$ http http://v1.microservice.xyz/env/VERSION
HTTP/1.1 200 OK
...
Server: nginx/1.15.3
Via: 1.1 google

v1

$ http http://v1.microservice.rocks/env/VERSION
HTTP/1.1 404 Not Found
...
Via: 1.1 google

default backend - 404

这里看到,“超纲”的域名会返回 404,被缺省后端截获。如果更换一个命名空间来创建 HTTPRoute 来引用 Gateway,会发现虽然在 Gateway 中定义了 namespaces.from: "All",但是仍旧会返回 404describe httproute 一下会发现,spec.gateways.allow 缺省被设置为 SameNamespace,因此显式定义 spec.gateways.allow=All,就能正常访问了。

分流

HTTPRoute 的 spec.rules 是一个数组,实际上这是一个分流支持,例如我们如此定义:

代码语言:javascript复制
  rules:
  - forwardTo:
    - serviceName: flaskapp-v1
      port: 80
  - forwardTo:
    - serviceName: flaskapp-v2
      port: 80

然后循环测试,会发现 v1v2 在一定时间内会交替出现。forwardTo 还有一个 weight 属性,这个数字决定了流量在不同转发目标之间的分配比例。

GKE 的分流好像比较弱,一百个请求测试,有时分配也并不明显。

条件分支

多个 Rule 之间还可以使用条件进行分流,例如:

代码语言:javascript复制
  rules:
  - forwardTo:
    - serviceName: flaskapp-v1
      port: 80
  - forwardTo:  
    - serviceName: flaskapp-v2
      port: 80
    matches:
    - headers:
        type: Exact
        values:
          version: v2

测试一下有无特定 Header 的结果:

代码语言:javascript复制
$ http http://v1.microservice.xyz/env/VERSION version:v2
...
v2

$ http http://v1.microservice.xyz/env/VERSION
...
v1

其他

在直接的 forwardTo 之外,Gateway API 还可以通过 Filter 的方式支持扩展能力,这个能够在转发之前进行流量处理的功能分为三种层级:

  • Core:所有实现者都必须实现该能力,例如 RequestHeaderModifier
  • Extended:建议实现这个层级的能力,例如 RequestMirror
  • Custom:实现者可以在这个层级实现各种扩展能力,如果多个厂商都实现了该功能,则可能升级到 Extended 或者 Core。

GKE 的公共 Gateway 并不支持流量复制,现阶段也不提供 TCP/UDP 的支持,可能需要靠其它控制器来实现。

0 人点赞