Kubernetes 通过 Ingress 实现灰度发布以及 CI 流程

ingress-nginx 在 0.22 添加了灰度发布的功能,可以通过简单的配置实现。这篇文章主要讲解如何配置以及如何和 CI 流程结合。
PS:简单说明下,我司的发布流程是通过 Gitlab 和 Kubernetes 实现的。在 Gitlab - Operations - Kubernetes 添加 Kubernetes 相关配置,在 .gitlab-ci.yml 配置 CI 流程,在 Gitlab CI Variables 里配置敏感信息。

Ingress 配置

Ingress 需要增加的配置比较简单,只需要添加几个 annotation 就可以。

1
2
3
# ingress annotation
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"

上面的配置表示开启 ingress canary 功能,设置的流量为 20%。除了基本的配置之外,还可以根据 Header、Cookie 进行流量配置,可以参考官方文档

接下来,只要发布的服务和原来的服务 Ingress host 保持一致就可以。现在发往 http://example.beta.com 的请求,有 80% 的流量发往原服务,有 20% 的流量发往新的服务。

1
2
3
4
5
6
7
8
9
10
11
12
# .gitlab-ci.yml
deploy_beta:
<<: *DEPLOY
environment:
name: beta
url: http://example.beta.com

deploy_beta_canary:
<<: *DEPLOY
environment:
name: beta-canary
url: http://example.beta.com

CI 流程的改造

上面只是说明了如何进行配置,但如何与原来的 CI 流程结合是一个需要解决的问题,比如环境变量的配置、灰度发布流程。接下来一一讨论。

环境变量的配置

由于我司 deployment 的 release 是由项目名以及 Gitlab environment 组成的,我这边通过区分灰度发布的 environment 使得 deployment 的 release 不同。但这会导致原先在 Gitlab CI 上配置的环境变量失效,需要对原来的配置进行修改。在原来的环境后添加通配符 *,可以使得灰度发布的服务也能使用之前的环境变量配置。

当然也可以通过更改 deployment 的 release 规则达到同样的效果,不需要修改 Gitlab 环境变量的配置。

发布流程

考虑到灰度发布后,需要进行正常的发布,我个人倾向于使用同一分支进行灰度发布和正常发布的操作,而不是使用一个例外的分支进行灰度发布,这样可以避免重复构建,也能减少对原来 CI 流程的影响。

这是 master 分支在添加了灰度发布功能后的 pipeline,添加了 deploy_production_canary,stop_production_canary 等 manual action,通过点击就可执行灰度发布、撤销灰度发布的功能。

如果都在同一分支上进行灰度发布和正常发布,那么是如何区分这两个操作的?

可以通过在 Gitlab CI 的灰度发布上添加 CANARY_RELEASE 表明是灰度发布,然后在 CI 发布脚本中根据该变量添加相关的 ingress 配置。以下是 .gitlab-ci.yml 代码示例。

因为灰度发布的流量是由 CANARY_WEIGHT 控制的,当要修改灰度流量比例时,我们只需要在 CI Variables 更改 CANARY_WEIGHT 的值,然后再次点击 deploy。

可以通过执行 kubectl describe ingress ${ingress name} 来确认 CANARY_WEIGHT 是否生效。

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
# .gitlab-ci.yml

.deploy: &DEPLOY
<<: *DEPLOY_BASE
script:
# 在 CANARY_RELEASE 为 true 时,向 ingress 添加 nginx.ingress.kubernetes.io/canary=true 以及 nginx.ingress.kubernetes.io/canary-weight=$CANARY_WEIGHT 的配置。
- if [ $CANARY_RELEASE == 'true' ]; then export HELM_ARGS="$HELM_ARGS,ingress.annotations.nginx\.ingress\.kubernetes\.io/canary=true,ingress.annotations.nginx\.ingress\.kubernetes\.io/canary-weight=$CANARY_WEIGHT"; fi

.deploy_canary: &DEPLOY_CANARY
<<: *DEPLOY
variables:
<<: *DEPLOY_BASE_VARIABLES
# CANARY_RELEASE 表示为灰度发布。CANARY_WEIGHT 为配置灰度发布的流量比例。
CANARY_RELEASE: 'true'
CANARY_WEIGHT: '10'
when: manual

deploy_production:
<<: *DEPLOY
environment:
name: production
url: http://example.internal-k8s.com
when: manual
only:
- master
tags:
- production

deploy_production_canary:
<<: *DEPLOY_CANARY
environment:
name: production-canary
url: http://example.internal-k8s.com
only:
- master
tags:
- production

stop_production_canary:
<<: *STOP_DEPLOY
environment:
name: production-canary
action: stop
only:
- master
tags:
- production

在灰度发布一段时间后,需要全量发布时,步骤如下:

  1. 执行 deploy_production
  2. 待 production 的 pod 发布完毕后,执行 stop_production_canary,将灰度发布的 pod 删除。此时线上的情况就跟直接执行 deploy_production 的情况相同。

当你第一次 deploy_production_canary 时,可能会发现没有部署到生产环境,而是部署到开发环境的 Kubernetes。那你需要更改 Gitlab - Operations - Kubernetes 的配置,将生产环境 Kubernetes cluster 的 Environment scope 从 production 改为 production*。

监控

当线上有两个版本的服务存在时,我们需要能区分这个请求到底是哪个版本的服务。可以通过在灰度发布的版本上添加特殊的 Header 来区分的。Spring 的示例代码如下。

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
// values.yaml
CANARY_RELEASE: false

// application.properties
spring.account-service.canary-release=${CANARY_RELEASE:false}


@Configuration
@ConditionalOnProperty(name = "spring.account-service.canary-release")
public class CanaryConfig {

@Bean
public CanaryFilter canaryFilter() {
return new CanaryFilter();
}
}

public class CanaryFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
response.addHeader("From-Canary", "true");
filterChain.doFilter(request, response);
}
}

可以在 Grafana 添加图表使各个 Pod 接收的请求量可视化,Query 如下。

1
sum(increase(http_server_requests_seconds_count{kubernetes_pod_name=~"$app_name-$env.+"}[2m])) by (kubernetes_pod_name)

分支策略

灰度发布不走单独的分支,按原来的分支流程走。

参考