你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

在 Kubernetes 上为微服务构建 CI/CD 管道

Kubernetes 服务
容器注册表

为微服务体系结构创建可靠的 CI/CD 过程可能很困难。 单个团队必须能够快速可靠地发布服务,而不会中断其他团队或破坏整个应用程序。

本文介绍用于将微服务部署到 AKS Azure Kubernetes 服务 () 的示例 CI/CD 管道。 每个团队和项目都是不同的,因此不要将本文视为一组硬性快速的规则。 相反,它应该是设计自己的 CI/CD 过程的起点。

KUBERnetes 托管微服务的 CI/CD 管道的目标汇总如下:

  • Teams可以独立生成和部署其服务。
  • 通过 CI 过程的代码更改会自动部署到类似于生产的环境。
  • 质量入口在管道的每个阶段强制执行。
  • 可以与以前的版本并排部署新版本的服务。

有关更多背景信息,请参阅 微服务体系结构的 CI/CD

假设

对于此示例,下面是有关开发团队和代码库的一些假设:

  • 代码存储库是一个 monorepo,由微服务组织的文件夹。
  • 团队的分库策略以基于主库的开发为基础。
  • 团队使用 发布分支 来管理发布。 为每个微服务创建单独的版本。
  • CI/CD 进程使用Azure Pipelines生成、测试和将微服务部署到 AKS。
  • 每个微服务的容器映像存储在Azure 容器注册表中。
  • 团队使用 Helm 图表打包每个微服务。

这些假设驱动 CI/CD 管道的许多特定详细信息。 但是,此处介绍的基本方法适用于其他流程、工具和服务,例如 Jenkins 或 Docker Hub。

验证生成

假设开发人员正在处理名为传递服务的微服务。 在开发新功能时,开发人员会将代码签入到某个功能分库中。 根据约定,功能分库名为 feature/*

CI/CD workflow

生成定义文件包括按分支名称和源路径筛选的触发器:

trigger:
  batch: true
  branches:
    include:
    # for new release to production: release flow strategy
    - release/delivery/v*
    - refs/release/delivery/v*
    - master
    - feature/delivery/*
    - topic/delivery/*
  paths:
    include:
    - /src/shipping/delivery/

使用此方法,每个团队都可以有自己的生成管道。 仅签入 /src/shipping/delivery 文件夹的代码会触发交付服务的生成。 将提交推送到与筛选器匹配的分支会触发 CI 生成。 在工作流中,CI 生成此时会运行某种最低程度的代码验证:

  1. 生成代码。
  2. 运行单元测试。

目标是缩短生成时间,以便开发人员可以获得快速反馈。 该功能准备好合并到主控形状后,开发人员将打开 PR。 此操作触发另一个执行其他检查的 CI 生成:

  1. 生成代码。
  2. 运行单元测试。
  3. 生成运行时容器映像。
  4. 对映像运行漏洞扫描。

Diagram showing ci-delivery-full in the Build pipeline.

注意

在Azure DevOps Repos中,可以定义用于保护分支的策略。 例如,策略可以要求在合并到主库之前,必须成功完成 CI 生成并由审批人员签署同意书。

完整 CI/CD 版本

有时候,团队可以部署新版传送服务。 发布管理器使用此命名模式从主分支创建分支: release/<microservice name>/<semver> 例如,release/delivery/v1.0.2

Diagram showing ci-delivery-full in the Build pipeline and cd-delivery in the Release pipeline.

创建此分支会触发运行上述所有步骤的完整 CI 生成,以及:

  1. 将容器映像推送到Azure 容器注册表。 该映像标记有版本号(取自分库名称)。
  2. 运行 helm package 以打包服务的 Helm 图表。 图表还标记有版本号。
  3. 将 Helm 包推送到容器注册表。

假设此生成成功,它会使用Azure Pipelines发布管道触发部署 (CD) 过程。 此管道具有以下步骤:

  1. 将 Helm 图表部署到 QA 环境。
  2. 审批者签署同意书,然后包就会转到生产环境。 请参阅通过审批进行发布部署控制
  3. 在 Azure 容器注册表 中为生产命名空间重新标记 Docker 映像。 例如,如果当前标记为 myrepo.azurecr.io/delivery:v1.0.2,则生产标记为 myrepo.azurecr.io/prod/delivery:v1.0.2
  4. 将 Helm 图表部署到生产环境。

即使在 monorepo 中,这些任务也可以限定为单个微服务,以便团队可以高速部署。 此过程有一些手动步骤:批准 PR、创建发布分支以及批准部署到生产群集。 这些步骤是手动的;如果组织愿意,他们可以自动执行这些操作。

环境隔离

你将拥有多个环境,可在其中部署服务,包括用于开发的环境、冒烟测试、集成测试、负载测试,最后部署生产环境。 这些环境需要某种程度的隔离。 在 Kubernetes 中,可以选择物理隔离或逻辑隔离。 物理隔离表示部署到独立的群集。 逻辑隔离使用命名空间和策略,如前所述。

我们建议创建专用的生产群集,并为开发/测试环境创建独立的群集。 使用逻辑隔离来隔离开发/测试群集中的环境。 部署到开发/测试群集的服务不得有权访问保存业务数据的数据存储。

生成过程

如果可能,请将生成过程打包到 Docker 容器中。 此配置允许使用 Docker 生成代码项目,而无需在每个生成计算机上配置生成环境。 通过容器化生成过程,可以通过添加新生成代理轻松横向扩展 CI 管道。 此外,团队中的任何开发人员只需运行生成容器即可生成代码。

通过在 Docker 中使用多阶段生成,可以在单个 Dockerfile 中定义生成环境和运行时映像。 例如,下面是生成 .NET 应用程序的 Dockerfile:

FROM mcr.microsoft.com/dotnet/core/runtime:3.1 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src/Fabrikam.Workflow.Service

COPY Fabrikam.Workflow.Service/Fabrikam.Workflow.Service.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.csproj

COPY Fabrikam.Workflow.Service/. .
RUN dotnet build Fabrikam.Workflow.Service.csproj -c release -o /app --no-restore

FROM build AS testrunner
WORKDIR /src/tests

COPY Fabrikam.Workflow.Service.Tests/*.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.Tests.csproj

COPY Fabrikam.Workflow.Service.Tests/. .
ENTRYPOINT ["dotnet", "test", "--logger:trx"]

FROM build AS publish
RUN dotnet publish Fabrikam.Workflow.Service.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Fabrikam.Workflow.Service.dll"]

此 Dockerfile 定义多个生成阶段。 请注意,命名 base 的阶段使用 .NET 运行时,而命名 build 的阶段使用完整的 .NET SDK。 阶段 build 用于生成 .NET 项目。 但是,最终运行时容器是从 base中生成的,它只包含运行时,并且明显小于完整的 SDK 映像。

生成测试运行程序

另一个最佳做法是在容器中运行单元测试。 例如,下面是生成测试运行程序的 Docker 文件的一部分:

FROM build AS testrunner
WORKDIR /src/tests

COPY Fabrikam.Workflow.Service.Tests/*.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.Tests.csproj

COPY Fabrikam.Workflow.Service.Tests/. .
ENTRYPOINT ["dotnet", "test", "--logger:trx"]

开发人员可以使用此 Docker 文件在本地运行测试:

docker build . -t delivery-test:1 --target=testrunner
docker run delivery-test:1

CI 管道还应在生成验证步骤中运行测试。

请注意,此文件使用 Docker ENTRYPOINT 命令运行测试,而不是 Docker RUN 命令。

  • 如果使用 RUN 该命令,则每次生成映像时都会运行测试。 通过使用 ENTRYPOINT,测试是选择加入的。 它们仅在显式面向 testrunner 阶段时才运行。
  • 失败的测试不会导致 Docker build 命令失败。 这样,就可以区分容器生成失败与测试失败。
  • 测试结果可以保存到装载的卷。

容器最佳做法

下面是用于容器的其他一些最佳做法:

  • 针对要部署到群集中的资源(pod、服务等),定义组织范围的容器标记约定、版本控制和命名约定。 这样,便可以更轻松地诊断部署问题。

  • 在开发和测试周期,CI/CD 过程将生成许多容器映像。 只有其中一些映像是发布的候选映像,然后只有其中一些发布候选项将提升到生产环境。 具有明确的版本控制策略,以便了解当前部署到生产中的映像,并在必要时帮助回滚到以前的版本。

  • 始终部署特定的容器版本标记,而不是 latest

  • 使用Azure 容器注册表中的命名空间将已批准的映像与仍在测试的映像隔离。 在准备好将其部署到生产环境之前,请勿将映像移到生产命名空间中。 如果将这种做法与容器映像的语义版本控制结合使用,则可以减少意外部署尚未批准发布的版本的可能性。

  • 以非特权用户身份运行容器,遵循最低特权原则。 在 Kubernetes 中,可以创建一个 Pod 安全策略,以防止容器以 身份运行。 请参阅 阻止 Pod 使用根权限运行

Helm 图表

考虑使用 Helm 来管理服务的生成和部署。 下面是 Helm 的一些功能,可帮助 CI/CD:

  • 通常,单个微服务由多个 Kubernetes 对象定义。 Helm 允许将这些对象打包到单个 Helm 图表中。
  • 可以使用单个 Helm 命令而不是一系列 kubectl 命令部署图表。
  • 图表已显式版本控制。 使用 Helm 发布版本、查看版本并回滚到以前的版本。 使用语义版本控制以及用于回滚到以前版本的功能来跟踪更新和修订。
  • Helm 图表使用模板避免在多个文件中复制标签和选择器等信息。
  • Helm 可以管理图表之间的依赖关系。
  • 图表可以存储在 Helm 存储库中,例如Azure 容器注册表,并集成到生成管道中。

有关将容器注册表用作 Helm 存储库的详细信息,请参阅将 Azure 容器注册表用作应用程序图表的 Helm 存储库

重要

此功能目前处于预览状态。 需同意补充使用条款才可使用预览版。 在正式版 (GA) 推出之前,此功能的某些方面可能会有所更改。

单个微服务可能涉及多个 Kubernetes 配置文件。 更新服务可能意味着触摸所有这些文件以更新选择器、标签和图像标记。 Helm 将这些包视为名为图表的单个包,并允许你使用变量轻松更新 YAML 文件。 Helm 使用基于 Go 模板 (模板) 编写参数化 YAML 配置文件。

例如,下面是定义部署的 YAML 文件的一部分:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "package.fullname" . | replace "." "" }}
  labels:
    app.kubernetes.io/name: {{ include "package.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  annotations:
    kubernetes.io/change-cause: {{ .Values.reason }}

...

  spec:
      containers:
      - name: &package-container_name fabrikam-package
        image: {{ .Values.dockerregistry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        env:
        - name: LOG_LEVEL
          value: {{ .Values.log.level }}

可以看到部署名称、标签和容器规格都使用在部署时提供的模板参数。 例如,在命令行中:

helm install $HELM_CHARTS/package/ \
     --set image.tag=0.1.0 \
     --set image.repository=package \
     --set dockerregistry=$ACR_SERVER \
     --namespace backend \
     --name package-v0.1.0

尽管 CI/CD 管道可以直接将图表安装到 Kubernetes,但我们建议创建图表存档 (.tgz 文件) 并将图表推送到 Helm 存储库,例如Azure 容器注册表。 有关详细信息,请参阅 Azure Pipelines Helm 图表中的基于 Docker 的应用

请考虑将 Helm 部署到自己的命名空间,并使用基于角色的访问控制 (RBAC) 来限制可以部署到的命名空间。 有关详细信息,请参阅 Helm 文档中的基于角色的访问控制

修订

Helm 图表始终具有版本号,必须使用 语义版本控制。 图表也可以有一个 appVersion。 此字段是可选的,不必与图表版本相关。 某些团队可能希望将应用程序版本与图表的更新分开。 但更简单的方法是使用一个版本号,因此图表版本与应用程序版本之间存在 1:1 的关系。 这样,可以存储每个版本的一个图表,并轻松部署所需的版本:

helm install <package-chart-name> --version <desiredVersion>

另一个好做法是在部署模板中提供更改原因注释:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "delivery.fullname" . | replace "." "" }}
  labels:
     ...
  annotations:
    kubernetes.io/change-cause: {{ .Values.reason }}

这样,便可以使用 kubectl rollout history 命令查看每个修订的更改原因字段。 在前面的示例中,更改原因作为 Helm 图表参数提供。

kubectl rollout history deployments/delivery-v010 -n backend
deployment.extensions/delivery-v010
REVISION  CHANGE-CAUSE
1         Initial deployment

还可以使用 helm list 命令查看修订历史记录:

helm list
NAME            REVISION    UPDATED                     STATUS        CHART            APP VERSION     NAMESPACE
delivery-v0.1.0 1           Sun Apr  7 00:25:30 2020    DEPLOYED      delivery-v0.1.0  v0.1.0          backend

Azure DevOps管道

在Azure Pipelines中,管道分为生成管道发布管道。 生成管道运行 CI 进程并创建生成项目。 对于 Kubernetes 上的微服务体系结构,这些项目是定义每个微服务的容器映像和 Helm 图表。 发布管道运行将微服务部署到群集的 CD 进程。

根据本文前面所述的 CI 流,生成管道可能包含以下任务:

  1. 生成测试运行程序容器。

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        arguments: '--pull --target testrunner'
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        imageName: '$(imageName)-test'
    
  2. 通过调用针对测试运行程序容器运行的 docker 来运行测试。

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        command: 'run'
        containerName: testrunner
        volumes: '$(System.DefaultWorkingDirectory)/TestResults:/app/tests/TestResults'
        imageName: '$(imageName)-test'
        runInBackground: false
    
  3. 发布测试结果。 请参阅 生成映像

    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: 'TestResults/*.trx'
        searchFolder: '$(System.DefaultWorkingDirectory)'
        publishRunAttachments: true
    
  4. 生成运行时容器。

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        includeLatestTag: false
        imageName: '$(imageName)'
    
  5. 将容器映像推送到Azure 容器注册表 (或其他容器注册表) 。

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        command: 'Push an image'
        imageName: '$(imageName)'
        includeSourceTags: false
    
  6. 打包 Helm 图表。

    - task: HelmDeploy@0
      inputs:
        command: package
        chartPath: $(chartPath)
        chartVersion: $(Build.SourceBranchName)
        arguments: '--app-version $(Build.SourceBranchName)'
    
  7. 将 Helm 包推送到Azure 容器注册表 (或其他 Helm 存储库) 。

    task: AzureCLI@1
      inputs:
        azureSubscription: $(AzureSubscription)
        scriptLocation: inlineScript
        inlineScript: |
        az acr helm push $(System.ArtifactsDirectory)/$(repositoryName)-$(Build.SourceBranchName).tgz --name $(AzureContainerRegistry);
    

CI 管道的输出是生产就绪的容器映像和微服务更新的 Helm 图表。 此时,发布管道可以接管。 它执行以下步骤:

  • 部署到开发/QA/过渡环境。
  • 等待审批者批准或拒绝部署。
  • 重新标记容器映像以供发布
  • 将发布标记推送到容器注册表。
  • 升级生产群集中的 Helm 图表。

有关创建发布管道的详细信息,请参阅 发布管道、草稿发布和发布选项

下图显示了本文中所述的端到端 CI/CD 过程:

CD/CD pipeline