# Ingress 的继任者 —— GatewayAPI

在 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`。

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

```plaintext
$ 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`：

```plaintext
$ 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` 创建网关。

一个简单的工作负载：

```yaml
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：

```yaml
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.xyz` 的 `HTTPRoute`，选择器要求使用这个对象的 Route 必须使用 `app=flaskapp-v1` 的标签，可以在所有命名空间中进行引用。创建之后查看一下状态：

```plaintext
$ 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 对象：

```yaml
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`，看看运行结果：

```plaintext
$ 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"`，但是仍旧会返回 `404`，`describe httproute` 一下会发现，`spec.gateways.allow` 缺省被设置为 `SameNamespace`，因此显式定义 `spec.gateways.allow=All`，就能正常访问了。

### 分流

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

```yaml
  rules:
  - forwardTo:
    - serviceName: flaskapp-v1
      port: 80
  - forwardTo:
    - serviceName: flaskapp-v2
      port: 80
```

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

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

### 条件分支

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

```yaml
  rules:
  - forwardTo:
    - serviceName: flaskapp-v1
      port: 80
  - forwardTo:  
    - serviceName: flaskapp-v2
      port: 80
    matches:
    - headers:
        type: Exact
        values:
          version: v2
```

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

```plaintext
$ 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 的支持，可能需要靠其它控制器来实现。
