基于Ingress-Nginx 实现灰度发布

Grayscale publishing based on ingress-nginx

Posted by caojiele on March 15, 2020

灰度发布,是指在黑与白之间,能够平滑过渡的一种发布方式。通俗来说,即让产品的迭代能够按照不同的灰度策略对新版本进行线上环境的测试,灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以对新版本进行测试、发现和调整问题,以保证其影响度。

而KubeSphere在实现灰度发布中提供两种方式,一种是在 Bookinfo 微服务的灰度发布示例中,KubeSphere 基于 Istio 对 Bookinfo 微服务示例应用实现灰度发布。另一种则是使用应用路由 (Ingress) 和项目网关 (Ingress Controller) 实现灰度发布。

Ingress-Nginx (0.21.0 版本) 中,引入了一个新的 Canary 功能,可用于为网关入口配置多个后端服务,还可以使用指定的 annotation 来控制多个后端服务之间的流量分配。 KubeSphere 在 2.0.2 的版本 中,升级了项目网关 (Ingress Controller) 版本至 0.24.1,支持基于 Ingress-Nginx 的灰度发布。

说明: 本文用到的示例 yaml 源文件及代码已在KubeSphere企业空间中demo-workspace实现,后期版本控制发布可以在其基础上拓展实现。

### Ingress-Nginx Annotation 简介

KubeSphere 基于 Nginx Ingress Controller 实现了项目的网关,作为项目对外的流量入口和项目中各个服务的反向代理。而 Ingress-Nginx 支持配置 Ingress Annotations 来实现不同场景下的灰度发布和测试,可以满足金丝雀发布、蓝绿部署与 A/B 测试等业务场景。

Nginx Annotations 支持以下 4 种 Canary 规则:

  • nginx.ingress.kubernetes.io/canary-by-header:基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always 时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never 时,请求不会被发送到 Canary 入口;对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他金丝雀规则进行优先级的比较。
  • nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值,必须与上一个 annotation (即:canary-by-header) 一起使用。
  • nginx.ingress.kubernetes.io/canary-weight:基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求,权重为 100 意味着所有请求都将被发送到 Canary 入口。
  • nginx.ingress.kubernetes.io/canary-by-cookie:基于 cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的cookie。当 cookie 值设置为 always 时,它将被路由到 Canary 入口;当 cookie 值设置为 never 时,请求不会被发送到 Canary 入口;对于任何其他值,将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。

注意:金丝雀规则按优先顺序进行如下排序:

canary-by-header - > canary-by-cookie - > canary-weight

把以上的四个 annotation 规则可以总体划分为以下两类:

  • 基于权重的 Canary 规则

基于权重的 Canary 规则

  • 基于用户请求的 Canary 规则

基于用户请求的 Canary 规则

### 前提条件

  • 使用 project-admin 登陆创建 ingress-demo 项目
  • 使用 admin 用户登陆,在KubeSphere 右下角的工具箱打开 web kubectl

#### 第一步:创建项目和 Production 版本的应用

1.1. 在 KubeSphere 中创建一个企业空间 (workspace) 和项目 (namespace) ,可参考 多租户管理快速入门。如下已创建了一个示例项目。

web kubectl

1.2. 本文为了快速创建应用,使用集群的 admin 账号登录,在项目中创建工作负载和服务时可通过 编辑 yaml 的方式,或使用 KubeSphere 右下角的工具箱打开 web kubectl 并使用以下命令和 yaml 文件创建一个 Production 版本的应用并暴露给集群外访问。如下创建 Production 版本的 deploymentservice

web kubectl

1
2
3
  $ kubectl apply -f production.yaml -n ingress-demo
  deployment.extensions/production created
  service/production created

其中用到的 yaml 文件如下:

production.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: production
    labels:
      app: production
  spec:
    replicas: 1
    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
      protocol: TCP
      name: http
    selector:
      app: production

1.3. 创建 Production 版本的应用路由 (Ingress)。

1
2
  $ kubectl apply -f production.ingress -n ingress-demo
  ingress.extensions/production created

其中用到的 yaml 文件如下:

production.ingress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  apiVersion: extensions/v1beta1
  kind: Ingress
  metadata:
    name: production
    annotations:
      kubernetes.io/ingress.class: nginx
  spec:
    rules:
    - host: kubesphere.io
      http:
        paths:
        - backend:
            serviceName: production
            servicePort: 80

#### 第二步:访问 Production 版本的应用

2.1. 此时,在 KubeSphere UI 的企业空间 demo-workspace 下,可以看到 ingress-demo 项目下的所有资源。

Deployment

工作负载

Service

服务

Route (Ingress)

应用路由

2.2. 访问 Production 版本的应用需确保当前项目已开启了网关,在外网访问下打开网关,类型为 NodePort。

外网访问

2.3. 如下访问 Production 版本的应用,注意以下命令需要在 SSH 客户端中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  $ curl --resolve kubesphere.io:31740:192.168.100.210 kubesphere.io:31740
  # 注意,加上 --resolve 参数则无需在本地配置 /etc/hosts 中的 IP 与域名映射,否则需要预先在本地配置域名映射,其中 192.168.100.210 是项目内的网关地址。
  
  	
  Hostname: production-7946d7969c-pf8gq
  
  Pod Information:
          node name:      k8snode1
          pod name:       production-7946d7969c-pf8gq
          pod namespace:  ingress-demo
          pod IP: 10.101.249.5
  
  Server values:
          server_version=nginx: 1.13.3 - lua: 10008
  
  Request Information:
          client_address=10.101.249.7
          method=GET
          real path=/
          query=
          request_version=1.1
          request_scheme=http
          request_uri=http://kubesphere.io:8080/
  
  Request Headers:
          accept=*/*
          host=kubesphere.io:31740
          user-agent=curl/7.66.0
          x-forwarded-for=10.101.16.128
          x-forwarded-host=kubesphere.io:31740
          x-forwarded-port=80
          x-forwarded-proto=http
          x-original-uri=/
          x-real-ip=10.101.16.128
          x-request-id=2da8fdbddfa5cfeda25e1ae1c5189b35
          x-scheme=http
  
  Request Body:
          -no body in request-	

#### 第三步:创建 Canary 版本

参考将上述 Production 版本的 production.yaml 文件,再创建一个 Canary 版本的应用,包括一个 Canary 版本的 deploymentservice (为方便快速演示,仅需将 production.yaml 的 deployment 和 service 中的关键字 production 直接替换为 canary,实际场景中可能涉及业务代码变更)。

#### 第四步:Ingress-Nginx Annotation 规则

基于权重 (Weight)

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

4.1. 使用以下 canary.ingress 的 yaml 文件再创建一个基于权重的 Canary 版本的应用路由 (Ingress)。

注意:要开启灰度发布机制,首先需设置 nginx.ingress.kubernetes.io/canary: "true" 启用 Canary,以下 Ingress 示例的 Canary 版本使用了基于权重进行流量切分的 annotation 规则,将分配 30% 的流量请求发送至 Canary 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  apiVersion: extensions/v1beta1
  kind: Ingress
  metadata:
    name: canary
    annotations:
      kubernetes.io/ingress.class: nginx
      nginx.ingress.kubernetes.io/canary: "true"
      nginx.ingress.kubernetes.io/canary-weight: "30"
  spec:
    rules:
    - host: kubesphere.io
      http:
        paths:
        - backend:
            serviceName: canary
            servicePort: 80

应用路由

4.2. 访问应用的域名(在 SSH 中执行以下命令)。

说明:应用的 Canary 版本基于权重 (30%) 进行流量切分后,访问到 Canary 版本的概率接近 30%,流量比例可能会有小范围的浮动。

1
  $ for i in $(seq 1 10); do curl -s --resolve kubesphere.io:31740:192.168.100.210 kubesphere.io:31740 | grep "Hostname"; done

Weight

### 基于 Request Header

4.3. 基于 Request Header 进行流量切分的典型应用场景即灰度发布或 A/B 测试场景。参考以下截图,在 KubeSphere 给 Canary 版本的应用路由 (Ingress) 新增一条 annotation nginx.ingress.kubernetes.io/canary-by-header: canary (这里的 annotation 的 value 可以是任意值),使当前的 Ingress 实现基于 Request Header 进行流量切分。

说明:金丝雀规则按优先顺序 canary-by-header - > canary-by-cookie - > canary-weight 进行如下排序,因此以下情况将忽略原有 canary-weight 的规则。

Request Header

4.4. 在请求中加入不同的 Header 值,再次访问应用的域名。

说明: 举两个例子,如开篇提到的当 Request Header 设置为 neveralways 时,请求将不会一直被发送到 Canary 版本;

对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他 Canary 规则进行优先级的比较(如下第二次请求已将基于 30% 权重作为第一优先级)。

1
  $ for i in $(seq 1 10); do curl -s -H "canary: never" --resolve kubesphere.io:31740:192.168.100.210 kubesphere.io:31740 | grep "Hostname"; done

never

1
  $ for i in $(seq 1 10); do curl -s -H "canary: always" --resolve kubesphere.io:31740:192.168.100.210 kubesphere.io:31740 | grep "Hostname"; done

always

1
  $ for i in $(seq 1 10); do curl -s -H "canary: other-value" --resolve kubesphere.io:31740:192.168.100.210  kubesphere.io:31740 | grep "Hostname"; done

other-value

4.5. 此时可以在上一个 annotation (即 canary-by-header)的基础上添加一条 nginx.ingress.kubernetes.io/canary-by-header-value: user-value 。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。

canary-by-header

4.6. 如下访问应用的域名,当 Request Header 满足此值时,所有请求被路由到 Canary 版本(该规则允许用户自定义 Request Header 的值)。

1
  $ for i in $(seq 1 10); do curl -s -H "canary: user-value" --resolve kubesphere.io:31740:192.168.100.210 kubesphere.io:31740 | grep "Hostname"; done

user-value

4.7. 如果改成other-value,则匹配的原则不是user-value值,所以会匹配到第三优先级Ingress规则(weight),即权重规则。

1
  $ for i in $(seq 1 10); do curl -s -H "canary: other-value" --resolve kubesphere.io:31740:192.168.100.210  kubesphere.io:31740 | grep "Hostname"; done

other-value

### 基于 Cookie

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

### 总结

灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以对新版本进行测试、发现和调整问题,以保证其影响度。本文通过多个示例演示和说明了基于 KubeSphere 使用应用路由 (Ingress) 和项目网关 (Ingress Controller) 实现灰度发布,并详细介绍了 Ingress-Nginx 的四种 Annotation,在未使用 Istio 的情况下,也能借助 Ingress-Nginx 轻松实现灰度发布。

### 参考


☛小礼物走一走,来Github关注我☚

-->