本文主要介紹如何配置nginx ingress和云效Appstack來實現灰度發布。
背景信息
灰度發布能降低部署風險,提升服務穩定性,尤其適用于快速迭代的軟件研發。相比 k8s 的滾動部署,基于流量特征的灰度發布更精準,風險更低。在云原生場景下,基于 nginx ingress 的灰度發布廣泛使用。該方案在流量入口調配灰度和正常流量,將灰度流量導入新版本服務,驗證通過后再全量部署,驗證不通過時可及時回退,確保用戶服務不間斷。
基本原理
對于常見的 web 服務,ingress 灰度發布的基本邏輯是:將指向同一入口(HOST)的請求,根據特征或流量比例,部分流量被路由到灰度服務中,通過監控和驗證灰度流量,判斷灰度服務是否符合上線標準
根據流量路徑,我們可以繪制 ingress 流量灰度在 k8s 上的架構圖(如下圖):
流量從入口的 HOST 進來后,根據 nginx-ingress 的流量標識(定義在 ingress 的 annotation 中),將符合灰度標識的請求路由到灰度環境的 service,再進入工作負載 Pod。
每個環境內部的 service 和 deployment 并不主動感知灰度標識,通常情況下,內部的 rpc 流量不攜帶灰度標識。因此,這種架構主要解決外部流量灰度的問題,適合服務數量不多、服務間調用簡單的場景。
操作步驟
接下來,我們結合云效 Appstack,來看下如何在阿里云 ACK 集群上進行應用的 ingress 灰度發布。
我們首先導入 ACK 集群并創建應用。
環境管理
新建環境
在應用交付AppStack首頁,進入目標應用,分別新建灰度環境與生產環境,兩個環境共享同一個 k8s 集群,將灰度環境命名為 grey,生產環境命名為 ack-prod。
編排配置
單擊
,定義部署編排。灰度發布生效的關鍵是 Ingress 中的 canary 注解,需要查看您的 nginx-ingress-controller 版本,確定可以支持的注解。測試的集群上 nginx-ingress-controller 的版本是 0.30,采用 header 來標識灰度流量。為了做到只在灰度環境中開啟灰度的路由配置,使用了編排模板的條件語句,參見下面的編排示例:
--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ .AppStack.appName }}-{{ .AppStack.envName }} namespace: {{ .Values.namespace }} {{ if eq .AppStack.envName "grey" }} annotations: # 開啟Canary。 nginx.ingress.kubernetes.io/canary: "true" # 請求頭為_env。 nginx.ingress.kubernetes.io/canary-by-header: "_env" # 請求頭_env的值為grey時,請求才會被路由到新版本服務中。 nginx.ingress.kubernetes.io/canary-by-header-value: "grey" {{ end }} spec: rules: - host: {{ .Values.host }} http: paths: - path: / pathType: Prefix backend: service: name: {{ .AppStack.appName }}-{{ .AppStack.envName }} port: number: 80
可以看到在 Ingress 的編排中,僅名稱為“grey”的環境,會開啟 canary 的 annotaion,并且將灰度標識設置為通過 _env: grey 的 header 標簽。
同時在 Ingress 的 rules 配置中,通過變量 host 來指定路由關聯的 HOST,這里 ACK 灰度環境和 ACK 生產環境分別關聯灰度環境變量組和生產環境變量組,這兩個變量組的 host 變量采用相同的值。
示例:
# namespace = demo-pre # host = go.demo.prod
# namespace = demo-prod # host = go.demo.prod
除了 Ingress 外,還需要配置對應的 Service 和 Deployment。
--- apiVersion: v1 kind: Service metadata: name: {{ .AppStack.appName }}-{{ .AppStack.envName }} namespace: {{ .Values.namespace }} spec: selector: run: {{ .AppStack.appName }}-{{ .AppStack.envName }} ports: - protocol: TCP port: 80 targetPort: 8080
--- apiVersion: apps/v1 kind: Deployment metadata: name: {{ .AppStack.appName }}-{{ .AppStack.envName }} labels: run: {{ .AppStack.appName }}-{{ .AppStack.envName }} namespace: {{ .Values.namespace }} spec: replicas: {{ .Values.replicas }} selector: matchLabels: run: {{ .AppStack.appName }}-{{ .AppStack.envName }} template: metadata: labels: run: {{ .AppStack.appName }}-{{ .AppStack.envName }} spec: containers: - name: main image: {{ .AppStack.image.backend }} ports: - containerPort: 8080 resources: limits: cpu: {{ .Values.cpuLimit }} memory: {{ .Values.memoryLimit }} requests: cpu: {{ .Values.cpuRequest }} memory: {{ .Values.memoryRequest }}
準備示例代碼并關聯到應用中
可從 atomgit 上下載實例代碼,路徑為:https://atomgit.com/feiyuw/demo-go-echo.git
將代碼導入到云效代碼管理 Codeup,然后關聯到應用中。
定義灰度發布的流程
接下來,我們通過云效 AppStack 的研發流程,定義灰度發布的整個過程,見下圖:
在該流程中,每次執行,我們都會從 master 分支拉取代碼進行鏡像構建,構建完成后會轉交運維進行審批,審批通過后即開始部署過程。
部署過程包含灰度部署、灰度驗證、生產部署和灰度清理四個步驟。
灰度部署步驟會將前面構建出來的鏡像更新到 ACK 灰度環境,此時通過在請求中攜帶 _env:grey 的 header 就可進行灰度驗證。
灰度驗證是一個人工卡點,用于對灰度環境的觀測和驗證,如果驗證通過即自動進行生產環境的部署,如果驗證失敗,則跳過生產部署,執行灰度清理。
生產部署步驟會將鏡像更新到 ACK 生產環境,此時新的服務版本將對普通用戶可見。注意,這里為了降低風險,生產部署的策略被設置為了分批,且首批暫停的模式,保證線上仍然是逐步放量的,且有機會快速回退。
灰度清理步驟主要是清理灰度環境的資源,一方面節約資源,另一方面避免灰度驗證不通過時對線上的影響。
在 云效 AppStack 上相關應用的設置中,進行研發流程設置(如果僅是體驗灰度發布,可僅配置生產階段)。
上述流程可以參考下面的流水線 YAML 來定義,請注意將其中的acr_docker_build_step 步驟的鏡像地址和服務連接修改為實際的值,并把grey_validate 和 ops_validate 包含的 userId 替換為自己的阿里云賬號 ID。
---
stages:
build:
name: "構建"
jobs:
go_build:
name: "Go 鏡像構建"
steps:
golang_build_step:
name: "Golang 構建"
step: "GolangBuild"
with:
goVersion: "1.20.x"
run: |
export GOPROXY=https://goproxy.cn
make build
upload_step:
step: "ArtifactUpload"
name: "構建物上傳"
with:
serviceConnection: "wtdbdh89rrfdsod6"
repo: "flow_generic_repo"
artifact: "demo-go-echo"
version: "prod-${CI_COMMIT_ID}.${DATETIME}"
filePath:
- "demo-go-echo"
- "deploy.sh"
acr_docker_build_step:
name: "鏡像構建并推送至阿里云鏡像倉庫個人版"
step: "ACRDockerBuild"
with:
artifact: "image"
dockerfilePath: "Dockerfile"
dockerRegistry: "registry.cn-zhangjiakou.aliyuncs.com/docker007/demo-go-echo"
dockerTag: "prod-${CI_COMMIT_ID}.${DATETIME}"
region: "cn-zhangjiakou"
serviceConnection: "<connectionId>"
approve:
name: "部署審核"
jobs:
ops_validate:
name: "運維審批"
component: "ManualValidate"
with:
validatorType: "users"
validators:
- <userId>
grey:
name: "灰度驗證"
jobs:
grey_deploy_job:
name: "ACK灰度部署"
component: "AppStackFlowDeploy"
with:
application: "demo-go-echo"
environment: "grey"
artifacts:
- label: "backend"
value: "$[stages.build.go_build.acr_docker_build_step.artifacts.image.dockerUrl]"
autoDeploy: true
grey_validate:
name: "灰度驗證"
component: "ManualValidate"
needs:
- "grey_deploy_job"
with:
validatorType: "users"
validators:
- <userId>
deploy:
name: "部署"
jobs:
ack_deploy_job:
name: "ACK生產部署"
component: "AppStackFlowDeploy"
condition: |
succeed('grey.grey_validate')
with:
application: "demo-go-echo"
environment: "ack-prod"
artifacts:
- label: "backend"
value: "$[stages.build.go_build.acr_docker_build_step.artifacts.image.dockerUrl]"
autoDeploy: true
cleanup:
name: "清理環境"
jobs:
cleanup_grey_env_job:
name: "清理灰度環境"
component: "AppStackCleanEnv"
needs:
- "grey.grey_validate"
- "deploy.ack_deploy_job"
condition: |
failed('grey.grey_validate') || succeed('deploy.ack_deploy_job')
with:
application: "demo-go-echo"
environment: "grey"
deleteEnv: "cleanEnv"
驗證灰度發布流程
我們假設研發流程僅包含生產階段,先運行一次生產階段,將應用部署到灰度和生產環境中,注意:環境第一次部署可能需要手動創建部署單。
修改代碼,將 routes.go 里面的版本號修改為新的值,再次執行生產階段,直到灰度驗證步驟,此時生產環境與灰度環境運行不同的版本。
通過 kubectl get ing -A 獲取 Ingress 的出口 IP,在本地/etc/hosts 中將其綁定到 go.demo.prod 中,如:
127.0.0.1 go.demo.prod # 將127.0.0.1修改為正確的出口IP
打開終端,通過 httpie 或者 curl 請求/version 接口,以 httpie 為例,請求命令為:
http -v http://go.demo.prod/version # 請求正式環境
http -v http://go.demo.prod/version _env:grey # 請求灰度環境
常見問題
有了灰度環境,生產環境部署的時候還需要分批嗎?
建議在生產環境部署的時候仍然開啟分批,因為灰度環境雖然驗證通過了,但受到數據量等影響,生產環境仍然不建議全量一起上線,通過分批,可以把風險控制在小范圍內,避免大的故障的發生。
如何在研發流程上整合配置變更和數據變更?
可以將配置變更和數據變更作為研發階段的步驟,編排到研發流程的流水線 YAML 中,通常建議在應用部署前執行數據變更,部署后執行配置變更。同時,對應于 K8s 上的灰度環境,如果采用了類似 Nacos 這樣的配置中心,也應當由對應的灰度 namespace,從而避免直接修改生產配置。
灰度環境包含多個應用,如何保證其內部服務間調用也是走的灰度環境?
完整的方案建議參考 MSE 等產品。
如果應用數量比較少,鏈路比較簡單,且接受基于 K8s 的簡單方案,可以為每個應用都定義一個灰度環境,共享同一個集群和 namespace,應用間通過 Service 進行調用。此時,由于 namespace 的隔離,灰度環境內的應用互相調用只會調用同 namespace 下的其它應用的灰度版本。
這種方案需要保證各應用灰度環境的長期可用,因此研發流程最后的清理環境步驟需要被移除。
如何在流程中關聯其它類型的發布如函數計算?
可以在函數計算的相關步驟編排到研發流程的流水線 YAML 中,從而實現雙方的聯動。