使用 Ingress-Nginx 进行灰度(金丝雀)发布
1. Ingress-Nginx Canary介绍
Nginx Ingress Controller 作为项目对外的流量入口和项目中各个服务的反向代理。
官方文档概述:Annotations - Ingress-Nginx Controller (kubernetes.github.io)
Nginx Annotations 的几种 Canary 规则:
| Annotation | 说明 |
|---|---|
| nginx.ingress.kubernetes.io/canary | 必须设置该Annotation值为true,否则其它规则将不会生效。取值:true:启用canary功能。false:不启用canary功能。 |
| nginx.ingress.kubernetes.io/canary-by-header | 表示基于请求头的名称进行灰度发布。请求头名称的特殊取值: always:无论什么情况下,流量均会进入灰度服务。 never:无论什么情况下,流量均不会进入灰度服务。 若没有指定请求头名称的值,则只要该头存在,都会进行流量转发。 |
| nginx.ingress.kubernetes.io/canary-by-header-value | 表示基于请求头的值进行灰度发布。 需要与canary-by-header头配合使用。 |
| nginx.ingress.kubernetes.io/canary-by-header-pattern | 表示基于请求头的值进行灰度发布,并对请求头的值进行正则匹配。 需要与canary-by-header头配合使用。 取值为用于匹配请求头的值的正则表达式。 |
| nginx.ingress.kubernetes.io/canary-by-cookie | 表示基于Cookie进行灰度发布。例如,nginx.ingress.kubernetes.io/canary-by-cookie: foo。 Cookie内容的取值: always:当foo=always,流量会进入灰度服务。 never:当foo=never,流量不会进入灰度服务。 只有当Cookie存在,且值为always时,才会进行流量转发。 |
| nginx.ingress.kubernetes.io/canary-weight | 表示基于权重进行灰度发布。 取值范围:0~权重总值。 若未设定总值,默认总值为100。 |
| nginx.ingress.kubernetes.io/canary-weight-total | 表示设定的权重总值。 若未设定总值,默认总值为100。 |
注意:不同灰度方式的优先级 由高到低 为:
canary-by-header –> canary-by-cookie –> canary-weight
2. ingress-nginx Canary实现
2.1. 基于客户端请求的流量切分场景
需求:
假设当前线上环境,您已经有一套服务Service V1对外提供7层服务,此时上线了一些新的特性,需要发布上线一个新的版本Service V2。
希望将请求头中包含foo=bar或者Cookie中包含foo=bar的客户端请求转发到Service V2服务中。
待运行一段时间稳定后,可将所有的流量从Service V1切换到Service V2服务中,再平滑地将Service V1服务下线。

通过上面的annotation来实现灰度发布,其 思路如下:
-
在集群中部署两套系统,一套是stable版本(old-nginx),一套是canary版本(new-nginx),两个版本都有自己的service;
-
定义两个ingress配置,一个正常提供服务,一个增加canary的annotation;
-
待canary版本无误后,将其切换成stable版本,并且将旧的版本下线,流量全部接入新的stable版本
old-nginx 创建Deployment、Service、Ingress。
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4name: old-nginx
5spec:
6replicas: 2
7selector:
8 matchLabels:
9 run: old-nginx
10template:
11 metadata:
12 labels:
13 run: old-nginx
14 spec:
15 containers:
16 - image: registry.cn-hangzhou.aliyuncs.com/acs-sample/old-nginx
17 imagePullPolicy: Always
18 name: old-nginx
19 ports:
20 - containerPort: 80
21 protocol: TCP
22 restartPolicy: Always
23---
24apiVersion: v1
25kind: Service
26metadata:
27name: old-nginx
28spec:
29ports:
30- port: 80
31 protocol: TCP
32 targetPort: 80
33selector:
34 run: old-nginx
35sessionAffinity: None
36type: ClusterIP
37---
38apiVersion: networking.k8s.io/v1
39kind: Ingress
40metadata:
41name: nginx-ingress
42namespace: default
43spec:
44ingressClassName: nginx
45rules:
46 - host: nginx.kubernets.cn
47 http:
48 paths:
49 - pathType: Prefix
50 backend:
51 service:
52 name: old-nginx
53 port:
54 number: 80
55 path: /
测试验证:(预期输出:old)
1$ curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
灰度发布新版本服务:
new-nginx 创建Deployment、Service。
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4name: new-nginx
5spec:
6replicas: 1
7selector:
8 matchLabels:
9 run: new-nginx
10template:
11 metadata:
12 labels:
13 run: new-nginx
14 spec:
15 containers:
16 - image: registry.cn-hangzhou.aliyuncs.com/acs-sample/new-nginx
17 imagePullPolicy: Always
18 name: new-nginx
19 ports:
20 - containerPort: 80
21 protocol: TCP
22 restartPolicy: Always
23---
24apiVersion: v1
25kind: Service
26metadata:
27name: new-nginx
28spec:
29ports:
30- port: 80
31 protocol: TCP
32 targetPort: 80
33selector:
34 run: new-nginx
35sessionAffinity: None
36type: ClusterIP
设置访问新版本服务的路由规则。
- 设置满足特定规则的客户端才能访问新版本服务。以下示例仅请求头中满足foo=bar的客户端请求才能路由到新版本服务。
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 # 修改名称
5name: new-nginx-ingress
6namespace: default
7annotations:
8 # 开启Canary。
9 nginx.ingress.kubernetes.io/canary: "true"
10 # 请求头为foo。
11 nginx.ingress.kubernetes.io/canary-by-header: "foo"
12 # 请求头foo的值为bar时,请求才会被路由到新版本服务new-nginx中。
13 nginx.ingress.kubernetes.io/canary-by-header-value: "bar"
14spec:
15ingressClassName: nginx
16rules:
17 - host: nginx.kubernets.cn
18 http:
19 paths:
20 - pathType: Prefix
21 backend:
22 service:
23 # 选择新pod的svc
24 name: new-nginx
25 port:
26 number: 80
27 path: /
按照header头信息转发流量:
1# 正常访问:
2$ curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
3old
4
5# 访问新的服务:
6$ curl -H "Host: nginx.kubernets.cn" -H "foo: bar" http://nginx.kubernets.cn
7new
可以看到,仅请求头中满足foo=bar的客户端请求才能路由到新版本服务。
需求:请求头中满足foo=bar的客户端请求,若不包含该请求头,再将50%的流量路由到新版本服务中。
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4name: new-nginx-ingress
5namespace: default
6annotations:
7 # 开启Canary。
8 nginx.ingress.kubernetes.io/canary: "true"
9 # 请求头为foo。
10 nginx.ingress.kubernetes.io/canary-by-header: "foo"
11 # 请求头foo的值为bar时,请求才会被路由到新版本服务new-nginx中。
12 nginx.ingress.kubernetes.io/canary-by-header-value: "bar"
13 # 在满足上述匹配规则的基础上仅允许50%的流量会被路由到新版本服务new-nginx中。
14 nginx.ingress.kubernetes.io/canary-weight: "50"
15spec:
16ingressClassName: nginx
17rules:
18 - host: nginx.kubernets.cn
19 http:
20 paths:
21 - pathType: Prefix
22 backend:
23 service:
24 name: new-nginx
25 port:
26 number: 80
27 path: /
测试验证:(几乎可以达到50%请求分布)
1# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
2new
3# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
4old
5# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
6old
7# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
8old
9# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
10new
11# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
12new
13# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
14old
15# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
16old
系统运行一段时间后,当新版本服务已经稳定并且符合预期后,需要下线老版本的服务 ,仅保留新版本服务在线上运行。
为了达到该目标,需要将旧版本的Service指向新版本服务的Deployment,并且删除旧版本的Deployment和新版本的Service。
1apiVersion: v1
2kind: Service
3metadata:
4name: old-nginx
5spec:
6ports:
7- port: 80
8 protocol: TCP
9 targetPort: 80
10selector:
11 # 指向新版本服务。
12 run: new-nginx
13sessionAffinity: None
14type: ClusterIP
预期输出:
1# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
2new
3. 金丝雀发布的高级功能
如上只简单介绍了一些ingress开源默认支持的Annotation。
日常工作中基于开源ingress-nginx实线的高级功能:
通过修改nginx.ingress.kubernetes.io/configuration-snippet配置,并且配置正则实现:
-
当header头中有关键字(foo 或 new)字段的时候,自动将流量转发至new-nginx;
-
nginx.ingress.kubernetes.io/configuration-snippet (用于插入 location 块代码段);
-
nginx.ingress.kubernetes.io/server-snippet (用于插入 server 块中的代码段);
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4name: new-nginx-ingress
5namespace: default
6annotations:
7 nginx.ingress.kubernetes.io/configuration-snippet: |
8 if ($http_name ~ "^.*foo$|^.*new$") {
9 proxy_pass http://new-nginx.default:80;
10 break;
11 }
12spec:
13ingressClassName: nginx
14rules:
15 - host: nginx.kubernets.cn
16 http:
17 paths:
18 - path: /
19 backend:
20 service:
21 name: old-nginx
22 port:
23 number: 80
24 pathType: Prefix
25 - path: /
26 backend:
27 service:
28 name: new-nginx
29 port:
30 number: 80
31 pathType: Prefix
测试验证:
1# curl -H "name: new" http://nginx.kubernets.cn
2new
3# curl -H "name: foo" http://nginx.kubernets.cn
4new
5# curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
6old
4. 阿里开源ingress-nginx实现
4.1. 基础理论
Nginx Ingress Controller通过Annotation来支持应用服务的灰度发布机制。
nginx.ingress.kubernetes.io/service-match:该注解用来配置新版本服务的路由匹配规则。
1nginx.ingress.kubernetes.io/service-match: |
2 <service-name>: <match-rule>
3# 参数说明:
4# service-name:服务名称,满足match-rule的请求会被路由到该服务中。
5# match-rule:路由匹配规则
6#
7# 路由匹配规则:
8# 1. 支持的匹配类型
9# - header:基于请求头,支持正则匹配和完整匹配。
10# - cookie:基于cookie,支持正则匹配和完整匹配。
11# - query:基于请求参数,支持正则匹配和完整匹配。
12#
13# 2. 匹配方式
14# - 正则匹配格式:/{regular expression}/,//表明采用正则方式匹配。
15# - 完整匹配格式:"{exact expression}",""表明采用完整方式匹配。
路由匹配规则配置示例:
1# 请求头中满足foo正则匹配^bar$的请求被转发到新版本服务new-nginx中。
2new-nginx: header("foo", /^bar$/)
3# 请求头中满足foo完整匹配bar的请求被转发到新版本服务new-nginx中。
4new-nginx: header("foo", "bar")
5# cookie中满足foo正则匹配^sticky-.+$的请求被转发到新版本服务new-nginx中。
6new-nginx: cookie("foo", /^sticky-.+$/)
7# query param中满足foo完整匹配bar的请求被转发到新版本服务new-nginx中。
8new-nginx: query("foo", "bar")
nginx.ingress.kubernetes.io/service-weight:该注解用来配置新旧版本服务的流量权重。
1nginx.ingress.kubernetes.io/service-weight: |
2 <new-svc-name>:<new-svc-weight>, <old-svc-name>:<old-svc-weight>
3参数说明:
4new-svc-name:新版本服务名称
5new-svc-weight:新版本服务权重
6old-svc-name:旧版本服务名称
7old-svc-weight:旧版本服务权重
8
9配置示例:
10nginx.ingress.kubernetes.io/service-weight: |
11 new-nginx: 20, old-nginx: 60
4.2. 案例
创建老服务的:Deployment、Service。
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4name: old-nginx
5spec:
6replicas: 2
7selector:
8 matchLabels:
9 run: old-nginx
10template:
11 metadata:
12 labels:
13 run: old-nginx
14 spec:
15 containers:
16 - image: registry.cn-hangzhou.aliyuncs.com/acs-sample/old-nginx
17 imagePullPolicy: Always
18 name: old-nginx
19 ports:
20 - containerPort: 80
21 protocol: TCP
22 restartPolicy: Always
23---
24apiVersion: v1
25kind: Service
26metadata:
27name: old-nginx
28spec:
29ports:
30- port: 80
31 protocol: TCP
32 targetPort: 80
33selector:
34 run: old-nginx
35sessionAffinity: None
36type: ClusterIP
Ingress资源创建:
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4name: gray-release
5namespace: default
6spec:
7ingressClassName: nginx
8rules:
9 - host: nginx.kubernets.cn
10 http:
11 paths:
12 - pathType: Prefix
13 backend:
14 service:
15 name: old-nginx
16 port:
17 number: 80
18 path: /
灰度发布新版本服务
发布一个新版本的Nginx服务并配置路由规则。
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4name: new-nginx
5spec:
6replicas: 1
7selector:
8 matchLabels:
9 run: new-nginx
10template:
11 metadata:
12 labels:
13 run: new-nginx
14 spec:
15 containers:
16 - image: registry.cn-hangzhou.aliyuncs.com/acs-sample/new-nginx
17 imagePullPolicy: Always
18 name: new-nginx
19 ports:
20 - containerPort: 80
21 protocol: TCP
22 restartPolicy: Always
23---
24apiVersion: v1
25kind: Service
26metadata:
27name: new-nginx
28spec:
29ports:
30- port: 80
31 protocol: TCP
32 targetPort: 80
33selector:
34 run: new-nginx
35sessionAffinity: None
36type: ClusterIP
设置满足特定规则的客户端才能访问新版本服务。以下示例仅请求头中满足foo=bar的客户端请求才能路由到新版本服务。
- 修改如上步骤创建的Ingress。
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4name: gray-release
5namespace: default
6annotations:
7 # 请求头中满足正则匹配foo=bar的请求才会被路由到新版本服务new-nginx中。
8 nginx.ingress.kubernetes.io/service-match: |
9 new-nginx: header("foo", /^bar$/)
10spec:
11ingressClassName: nginx
12rules:
13 - host: nginx.kubernets.cn
14 http:
15 paths:
16 # 老版本服务
17 - path: /
18 backend:
19 service:
20 name: old-nginx
21 port:
22 number: 80
23 pathType: ImplementationSpecific
24 # 新版本服务
25 - path: /
26 backend:
27 service:
28 name: new-nginx
29 port:
30 number: 80
31 pathType: ImplementationSpecific
测试验证:
执行以下命令,访问当前服务:
1$ curl -H "Host: nginx.kubernets.cn" http://nginx.kubernets.cn
2old
执行以下命令,请求头中满足foo=bar的客户端请求访问服务:
$ curl -H "Host: nginx.kubernets.cn" -H "foo: bar" http://nginx.kubernets.cn
new