vlambda博客
学习文章列表

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布

记得点击上方“云原生之路”关注哦


前言

很久之前我写过一篇介绍使用 Nginx-Ingress 实现蓝绿部署和金丝雀发布的文章,但那篇文章只是介绍了 nginx-ingress 具备这些能力,真正应用还要很多额外的配置和操作,况且现在能实现这些功能的并不只有 nginx-ingress,Service Mesh 工具如:Istio,App Mesh,Linkerd;Ingress Controller 如:Contour,Gloo,NGINX 都能实现,而我们需要的更多是进行金丝雀发布之后指标的监控,流量的调整以及出现问题后的及时回滚。而 Flagger 就是这样一个帮助我们解决上面这些问题的开源工具。

Flagger

Flagger[1] 是一种渐进式交付工具,可自动控制 Kubernetes 上应用程序的发布过程。通过指标监控和运行一致性测试,将流量逐渐切换到新版本,降低在生产环境中发布新软件版本导致的风险。

Flagger 使用 Service Mesh(App Mesh,Istio,Linkerd)或 Ingress Controller(Contour,Gloo,NGINX)来实现多种部署策略(金丝雀发布,A/B 测试,蓝绿发布)。对于发布分析,Flagger 可以查询 Prometheus、Datadog 或 CloudWatch,并使用 Slack、MS Teams、Discord 和 Rocket 来发出告警通知。

本文主要介绍 Flagger 使用 Nginx-Ingress 进行金丝雀发布并监控发布状态,更多内容见官方文档[2]

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
Flagger NGINX Ingress Controller

前提条件

版本要求

安装 Flagger 需要 Kubernetes 版本高于 v1.14,NGINX ingress 版本高于 0.24。

安装 NGINX ingress

$ kubectl create ns ingress-nginx$ helm upgrade -i nginx-ingress stable/nginx-ingress \--namespace ingress-nginx \--set controller.metrics.enabled=true \--set controller.podAnnotations."prometheus\.io/scrape"=true \--set controller.podAnnotations."prometheus\.io/port"=10254

安装部署

Flagger 安装

Flagger 提供了 Hlem 和 Kustomize 两种安装方式,这里使用 Helm 3 安装:

$ helm repo add flagger https://flagger.app$ helm upgrade -i flagger flagger/flagger \--namespace ingress-nginx \--set prometheus.install=true \--set meshProvider=nginx \--set slack.url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \--set slack.channel=flagger \--set slack.user=flagger

值得注意的是这里我选择了 Slack 作为通知软件,需要在自己的 #channel 内新增一个 APP,并将该 APP 的 urlchanneluser 填入上面的命令中。这里设置的是全局通知,集群中的 Flagger 被触发后都会进行通知,当然也可以为单个 Flagger 配置专门的通知,这里就不做过多介绍,详情见官方文档[3]

示例安装

新建测试 namespace:

$ kubectl create ns test

部署示例 deployment 和 horizontal pod autoscaler:

$ kubectl apply -k github.com/weaveworks/flagger//kustomize/podinfo

部署负载测试器,以便在金丝雀发布时进行流量分析:

$ helm upgrade -i flagger-loadtester flagger/loadtester --namespace=test

部署 ingress,这里的 app.example.com 需要改成你自己的域名,如果是在本地进行测试,则修改本机和负载测试器所在节点的 /ect/hosts,将其指向你的 ADDRESS,否则将无法进行流量分析,导致部署失败。

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata: name: podinfo namespace: test labels: app: podinfo annotations: kubernetes.io/ingress.class: "nginx"spec: rules: - host: app.example.com http: paths: - backend: serviceName: podinfo servicePort: 80

将以上内容另存为 podinfo-ingress.yaml,然后应用:

$ kubectl apply -f ./podinfo-ingress.yaml

创建一个 Canary 资源:

apiVersion: flagger.app/v1beta1kind: Canarymetadata: name: podinfo namespace: testspec: provider: nginx # deployment reference targetRef: apiVersion: apps/v1 kind: Deployment name: podinfo # ingress reference ingressRef: apiVersion: networking.k8s.io/v1beta1 kind: Ingress name: podinfo # HPA reference (optional) autoscalerRef: apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler name: podinfo # the maximum time in seconds for the canary deployment # to make progress before it is rollback (default 600s) progressDeadlineSeconds: 60 service: # ClusterIP port number port: 80 # container port number or name targetPort: 9898 analysis: # 时间间隔 (默认 60s) interval: 10s # 回滚前的最大失败指标检查次数 threshold: 10 # 路由到金丝雀副本的最大流量百分比 # 百分比 (0-100) maxWeight: 50 # 金丝雀每次递增的百分比 # 百分比 (0-100) stepWeight: 5 # NGINX Prometheus checks metrics: - name: request-success-rate # minimum req success rate (non 5xx responses) # percentage (0-100) thresholdRange: min: 99 interval: 1m # testing (optional) webhooks: - name: acceptance-test type: pre-rollout url: http://flagger-loadtester.test/ timeout: 30s metadata: type: bash cmd: "curl -sd 'test' http://podinfo-canary/token | grep token" - name: load-test url: http://flagger-loadtester.test/ timeout: 5s metadata: cmd: "hey -z 1m -q 10 -c 2 http://app.example.com/"

将以上内容另存为 podinfo-canary.yaml,然后应用:

$ kubectl apply -f ./podinfo-canary.yaml

目前可以看到示例应用 podinfo 已经安装完毕,并出现了 podinfo 和 podinfo-primary 两个版本,并且 http://app.example.com/ 已经可以访问:

$ kubectl get deploy,svc,ing -n testNAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/flagger-loadtester 1/1 1 1 29hdeployment.apps/podinfo 0/0 0 0 29hdeployment.apps/podinfo-primary 2/2 2 2 29s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/flagger-loadtester ClusterIP 10.43.116.74 <none> 80/TCP 29hservice/podinfo ClusterIP 10.43.155.193 <none> 80/TCP 9sservice/podinfo-canary ClusterIP 10.43.194.226 <none> 80/TCP 29sservice/podinfo-primary ClusterIP 10.43.254.13 <none> 80/TCP 29s
NAME HOSTS ADDRESS PORTS AGEingress.extensions/podinfo app.example.com 192.168.1.129,192.168.4.210 80 5h17mingress.extensions/podinfo-canary app.example.com 80 9s

这个页面会展示 podinfo 的版本已经其正在访问的 pod 名称:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
app.example.com

自动金丝雀发布

现在起发布由 Flagger 控制,在部署新版本后,Flagger 自动将流量按照比例切换到新版本上,同时监控性能指标,例如 HTTP 请求的成功率、请求的平均持续时间和 pod 运行状态,经过分析后提升流量或者回滚,并通知到 Slack。

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
自动金丝雀发布

通过更新镜像版本触发金丝雀部署:

$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.1deployment.apps/podinfo image updated

可以看到初始化完成后已经有 5% 的流量切换到新版本了

$ kubectl -n test describe canary/podinfo...Status: Canary Weight: 5 Conditions: Last Transition Time: 2020-07-02T07:21:26Z Last Update Time: 2020-07-02T07:21:26Z Message: New revision detected, progressing canary analysis. Reason: Progressing Status: Unknown Type: Promoted Failed Checks: 0 Iterations: 0 Last Applied Spec: c8bdf98d5 Last Transition Time: 2020-07-02T07:22:05Z Phase: Progressing Tracked Configs:Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning Synced 10m flagger podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less then desired generation Warning Synced 10m flagger podinfo-primary.test not ready: waiting for rollout to finish: 0 of 2 updated replicas are available Normal Synced 10m (x3 over 10m) flagger all the metrics providers are available! Normal Synced 10m flagger Initialization done! podinfo.test Normal Synced 41s flagger New revision detected! Scaling up podinfo.test Warning Synced 31s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 1 updated replicas are available Warning Synced 21s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 2 updated replicas are available Warning Synced 11s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 1 of 2 updated replicas are available Normal Synced 1s flagger Starting canary analysis for podinfo.test Normal Synced 1s flagger Pre-rollout check acceptance-test passed Normal Synced 1s flagger Advance podinfo.test canary weight 5

使用 watch 也能实时看到部署流量的权重,根据上面的设置,新版本权重大于 50% 就认为部署成功,流量将全部切换到新版本,并完成金丝雀部署:

$ watch kubectl get canaries --all-namespacesEvery 2.0s: kubectl get canaries --all-namespaces guoxudongdeMacBook-Pro.local: Thu Jul 2 15:23:35 2020
NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIMEtest podinfo Progressing 45 2020-07-02T07:23:25Z

开始部署时的 Slack 通知:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
Slack 通知

页面上也能看出变化,访问到新版本的概率会越来越高,以蓝色和绿色的圆代表新版本和老版本:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
金丝雀发布

发布成功后,会收到 Slack 通知:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
Slack 通知

自动回滚

当然,有自动发布就会有自动回滚,下面就通过手动触发状态码 500 异常,演示暂停发布并回滚。

部署一个新版本:

$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.2

触发状态码 500 异常:

$ watch curl http://app.example.com/status/500

等待一会儿,就可以看到部署失败并回滚:

$ watch kubectl get canaries --all-namespacesEvery 2.0s: kubectl get canaries --all-namespaces guoxudongdeMacBook-Pro.local: Thu Jul 2 15:45:24 2020
NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIMEtest podinfo Failed 0 2020-07-02T07:45:16Z

发布失败,也会收到 Slack 通知:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
失败 Slack 通知

A/B 测试

除了加权路由,Flagger 还可以根据 HTTP 匹配条件将流量路由到新版本(当然,这个 Nginx-Ingress 的功能,Flagger 只是简化了操作)。可以根据 HTTP header 和 cookie 来定位用户并细分受众,对于需要关联会话的前端应用十分有用。

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
A/B 测试

修改 Canary 资源:

apiVersion: flagger.app/v1beta1kind: Canarymetadata: name: podinfo namespace: testspec: provider: nginx # deployment reference targetRef: apiVersion: apps/v1 kind: Deployment name: podinfo # ingress reference ingressRef: apiVersion: networking.k8s.io/v1beta1 kind: Ingress name: podinfo # HPA reference (optional) autoscalerRef: apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler name: podinfo # the maximum time in seconds for the canary deployment # to make progress before it is rollback (default 600s) progressDeadlineSeconds: 60 service: # ClusterIP port number port: 80 # container port number or name targetPort: 9898 analysis: interval: 1m threshold: 10 iterations: 10 match: # curl -H 'X-Canary: insider' http://app.example.com - headers: x-canary: exact: "insider" # curl -b 'canary=always' http://app.example.com - headers: cookie: exact: "canary" metrics: - name: request-success-rate thresholdRange: min: 99 interval: 1m webhooks: - name: load-test url: http://flagger-loadtester.test/ timeout: 5s metadata: cmd: "hey -z 1m -q 10 -c 2 -H 'Cookie: canary=always' http://app.example.com/"

从上面的配置可以看到,将 headers 为 X-Canary: insider 或 cookie 为 canary=always 的请求路由到新版本。

部署一个新版本:

$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.3

可以收到 Slack 通知:

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布
A/B 测试 Slack 通知

正常访问,还是访问到老的 v3.1.1 版:

$ curl http://app.example.com{ "hostname": "podinfo-primary-5dc6b76bd5-8sbh8", "version": "3.1.1", "revision": "7b6f11780ab1ce8c7399da32ec6966215b8e43aa", "color": "#34577c", "logo": "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif", "message": "greetings from podinfo v3.1.1", "goos": "linux", "goarch": "amd64", "runtime": "go1.13.1", "num_goroutine": "11", "num_cpu": "6"}

请求添加指定 header,访问到新的 v3.1.3 版:

$ curl -H 'X-Canary: insider' http://app.example.com{ "hostname": "podinfo-58bdd78d6f-m9bsc", "version": "3.1.3", "revision": "7b6f11780ab1ce8c7399da32ec6966215b8e43aa", "color": "#34577c", "logo": "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif", "message": "greetings from podinfo v3.1.3", "goos": "linux", "goarch": "amd64", "runtime": "go1.13.1", "num_goroutine": "10", "num_cpu": "6"}

请求添加指定 cookie,访问到新的 v3.1.3 版:

$ curl -b 'canary=always' http://app.example.com{ "hostname": "podinfo-58bdd78d6f-m9bsc", "version": "3.1.3", "revision": "7b6f11780ab1ce8c7399da32ec6966215b8e43aa", "color": "#34577c", "logo": "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif", "message": "greetings from podinfo v3.1.3", "goos": "linux", "goarch": "amd64", "runtime": "go1.13.1", "num_goroutine": "10", "num_cpu": "6"}

在浏览器中访问也能得到相同的结果:

添加 cookie 在浏览器中访问

结语

最早了解 Flagger 其实是因为其与 Istio 的关系,Flagger 默认的 meshProvider 就是 Istio。但是在深入了解后,发现其对市面上常见的 Service Mesh 和 Ingress Controller 都有较好的支持,通过与 Prometheus 以及负载测试器的配合可以进行细粒度的分析,从而提高了发布质量,同时还降低了人工操作出错的可能性。

最近 OAM 社区[4]也放出了基于 Flagger 的部署 Trait 的示例,相信之后与 OAM 结合使用可以在持续部署和应用管理领域发挥更大的作用。

想了解 OAM 可以查看我之前的文章:。

引用链接

[1] Flagger: https://github.com/weaveworks/flagger
[2] 官方文档: https://docs.flagger.app/
[3] 官方文档: https://docs.flagger.app/usage/alerting
[4] OAM 社区: https://oam.dev/