基于 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 需要 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 的 url、channel、user 填入上面的命令中。这里设置的是全局通知,集群中的 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: podinfonamespace: testlabels:app: podinfoannotations:kubernetes.io/ingress.class: "nginx"spec:rules:- host: app.example.comhttp:paths:- backend:serviceName: podinfoservicePort: 80
将以上内容另存为 podinfo-ingress.yaml,然后应用:
$ kubectl apply -f ./podinfo-ingress.yaml
创建一个 Canary 资源:
apiVersion: flagger.app/v1beta1kind: Canarymetadata:name: podinfonamespace: testspec:provider: nginx# deployment referencetargetRef:apiVersion: apps/v1kind: Deploymentname: podinfo# ingress referenceingressRef:apiVersion: networking.k8s.io/v1beta1kind: Ingressname: podinfo# HPA reference (optional)autoscalerRef:apiVersion: autoscaling/v2beta1kind: HorizontalPodAutoscalername: podinfo# the maximum time in seconds for the canary deployment# to make progress before it is rollback (default 600s)progressDeadlineSeconds: 60service:# ClusterIP port numberport: 80# container port number or nametargetPort: 9898analysis:# 时间间隔 (默认 60s)interval: 10s# 回滚前的最大失败指标检查次数threshold: 10# 路由到金丝雀副本的最大流量百分比# 百分比 (0-100)maxWeight: 50# 金丝雀每次递增的百分比# 百分比 (0-100)stepWeight: 5# NGINX Prometheus checksmetrics:- name: request-success-rate# minimum req success rate (non 5xx responses)# percentage (0-100)thresholdRange:min: 99interval: 1m# testing (optional)webhooks:- name: acceptance-testtype: pre-rollouturl: http://flagger-loadtester.test/timeout: 30smetadata:type: bashcmd: "curl -sd 'test' http://podinfo-canary/token | grep token"- name: load-testurl: http://flagger-loadtester.test/timeout: 5smetadata: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 29sNAME 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 29sNAME 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 控制,在部署新版本后,Flagger 自动将流量按照比例切换到新版本上,同时监控性能指标,例如 HTTP 请求的成功率、请求的平均持续时间和 pod 运行状态,经过分析后提升流量或者回滚,并通知到 Slack。
通过更新镜像版本触发金丝雀部署:
$ 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: 5Conditions:Last Transition Time: 2020-07-02T07:21:26ZLast Update Time: 2020-07-02T07:21:26ZMessage: New revision detected, progressing canary analysis.Reason: ProgressingStatus: UnknownType: PromotedFailed Checks: 0Iterations: 0Last Applied Spec: c8bdf98d5Last Transition Time: 2020-07-02T07:22:05ZPhase: ProgressingTracked 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 generationWarning Synced 10m flagger podinfo-primary.test not ready: waiting for rollout to finish: 0 of 2 updated replicas are availableNormal Synced 10m (x3 over 10m) flagger all the metrics providers are available!Normal Synced 10m flagger Initialization done! podinfo.testNormal Synced 41s flagger New revision detected! Scaling up podinfo.testWarning Synced 31s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 1 updated replicas are availableWarning Synced 21s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 2 updated replicas are availableWarning Synced 11s flagger canary deployment podinfo.test not ready: waiting for rollout to finish: 1 of 2 updated replicas are availableNormal Synced 1s flagger Starting canary analysis for podinfo.testNormal Synced 1s flagger Pre-rollout check acceptance-test passedNormal 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 2020NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIMEtest podinfo Progressing 45 2020-07-02T07:23:25Z
开始部署时的 Slack 通知:
页面上也能看出变化,访问到新版本的概率会越来越高,以蓝色和绿色的圆代表新版本和老版本:
发布成功后,会收到 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 2020NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIMEtest podinfo Failed 0 2020-07-02T07:45:16Z
发布失败,也会收到 Slack 通知:
A/B 测试
除了加权路由,Flagger 还可以根据 HTTP 匹配条件将流量路由到新版本(当然,这个 Nginx-Ingress 的功能,Flagger 只是简化了操作)。可以根据 HTTP header 和 cookie 来定位用户并细分受众,对于需要关联会话的前端应用十分有用。
修改 Canary 资源:
apiVersion: flagger.app/v1beta1kind: Canarymetadata:name: podinfonamespace: testspec:provider: nginx# deployment referencetargetRef:apiVersion: apps/v1kind: Deploymentname: podinfo# ingress referenceingressRef:apiVersion: networking.k8s.io/v1beta1kind: Ingressname: podinfo# HPA reference (optional)autoscalerRef:apiVersion: autoscaling/v2beta1kind: HorizontalPodAutoscalername: podinfo# the maximum time in seconds for the canary deployment# to make progress before it is rollback (default 600s)progressDeadlineSeconds: 60service:# ClusterIP port numberport: 80# container port number or nametargetPort: 9898analysis:interval: 1mthreshold: 10iterations: 10match:# 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-ratethresholdRange:min: 99interval: 1mwebhooks:- name: load-testurl: http://flagger-loadtester.test/timeout: 5smetadata: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 通知:
正常访问,还是访问到老的 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"}
在浏览器中访问也能得到相同的结果:
结语
最早了解 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/
