基于nginx ingress+云效Appstack实现灰度发布

本文主要介绍如何配置nginx ingress和云效Appstack来实现灰度发布。

背景信息

灰度发布能降低部署风险,提升服务稳定性,尤其适用于快速迭代的软件研发。相比 k8s 的滚动部署,基于流量特征的灰度发布更精准,风险更低。在云原生场景下,基于 nginx ingress 的灰度发布广泛使用。该方案在流量入口调配灰度和正常流量,将灰度流量导入新版本服务,验证通过后再全量部署,验证不通过时可及时回退,确保用户服务不间断。

基本原理

对于常见的 web 服务,ingress 灰度发布的基本逻辑是:将指向同一入口(HOST)的请求,根据特征或流量比例,部分流量被路由到灰度服务中,通过监控和验证灰度流量,判断灰度服务是否符合上线标准

根据流量路径,我们可以绘制 ingress 流量灰度在 k8s 上的架构图(如下图):

image

流量从入口的 HOST 进来后,根据 nginx-ingress 的流量标识(定义在 ingress 的 annotation 中),将符合灰度标识的请求路由到灰度环境的 service,再进入工作负载 Pod。

每个环境内部的 service 和 deployment 并不主动感知灰度标识,通常情况下,内部的 rpc 流量不携带灰度标识。因此,这种架构主要解决外部流量灰度的问题,适合服务数量不多、服务间调用简单的场景。

操作步骤

接下来,我们结合云效 Appstack,来看下如何在阿里云 ACK 集群上进行应用的 ingress 灰度发布。

我们首先导入 ACK 集群并创建应用。

高的 - 2024-12-16T163041.584.png

环境管理

  1. 新建环境

    应用交付AppStack首页,进入目标应用,分别新建灰度环境与生产环境,两个环境共享同一个 k8s 集群,将灰度环境命名为 grey,生产环境命名为 ack-prod。

    高的 - 2024-12-16T163427.089.png

  2. 编排配置

    单击设置 > 编排配置,定义部署编排。灰度发布生效的关键是 Ingress 中的 canary 注解,需要查看您的 nginx-ingress-controller 版本,确定可以支持的注解。测试的集群上 nginx-ingress-controller 的版本是 0.30,采用 header 来标识灰度流量。

    高的 - 2024-12-16T163940.407.png

    为了做到只在灰度环境中开启灰度的路由配置,使用了编排模板的条件语句,参见下面的编排示例:

    ---
    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 变量采用相同的值。

    示例:

    高的 - 2024-12-16T164017.469.png

    # 
    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,然后关联到应用中。

高的 - 2024-12-16T164053.254.png

定义灰度发布的流程

接下来,我们通过云效 AppStack 的研发流程,定义灰度发布的整个过程,见下图:

image.jpeg

在该流程中,每次执行,我们都会从 master 分支拉取代码进行镜像构建,构建完成后会转交运维进行审批,审批通过后即开始部署过程。

部署过程包含灰度部署、灰度验证、生产部署和灰度清理四个步骤。

  • 灰度部署步骤会将前面构建出来的镜像更新到 ACK 灰度环境,此时通过在请求中携带 _env:grey 的 header 就可进行灰度验证。

  • 灰度验证是一个人工卡点,用于对灰度环境的观测和验证,如果验证通过即自动进行生产环境的部署,如果验证失败,则跳过生产部署,执行灰度清理。

  • 生产部署步骤会将镜像更新到 ACK 生产环境,此时新的服务版本将对普通用户可见。注意,这里为了降低风险,生产部署的策略被设置为了分批,且首批暂停的模式,保证线上仍然是逐步放量的,且有机会快速回退。

  • 灰度清理步骤主要是清理灰度环境的资源,一方面节约资源,另一方面避免灰度验证不通过时对线上的影响。

在 云效 AppStack 上相关应用的设置中,进行研发流程设置(如果仅是体验灰度发布,可仅配置生产阶段)。

高的 - 2024-12-16T164130.200.png

上述流程可以参考下面的流水线 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    # 请求灰度环境

常见问题

  1. 有了灰度环境,生产环境部署的时候还需要分批吗?

    建议在生产环境部署的时候仍然开启分批,因为灰度环境虽然验证通过了,但受到数据量等影响,生产环境仍然不建议全量一起上线,通过分批,可以把风险控制在小范围内,避免大的故障的发生。

  2. 如何在研发流程上整合配置变更和数据变更?

    可以将配置变更和数据变更作为研发阶段的步骤,编排到研发流程的流水线 YAML 中,通常建议在应用部署前执行数据变更,部署后执行配置变更。同时,对应于 K8s 上的灰度环境,如果采用了类似 Nacos 这样的配置中心,也应当由对应的灰度 namespace,从而避免直接修改生产配置。

  3. 灰度环境包含多个应用,如何保证其内部服务间调用也是走的灰度环境?

    完整的方案建议参考 MSE 等产品。

    如果应用数量比较少,链路比较简单,且接受基于 K8s 的简单方案,可以为每个应用都定义一个灰度环境,共享同一个集群和 namespace,应用间通过 Service 进行调用。此时,由于 namespace 的隔离,灰度环境内的应用互相调用只会调用同 namespace 下的其它应用的灰度版本。

    这种方案需要保证各应用灰度环境的长期可用,因此研发流程最后的清理环境步骤需要被移除。

  4. 如何在流程中关联其它类型的发布如函数计算?

    可以在函数计算的相关步骤编排到研发流程的流水线 YAML 中,从而实现双方的联动。

其他相关