为微服务体系结构创建可靠的持续集成/持续交付 (CI/CD) 过程可能颇具挑战性。 各个团队必须能够快速可靠地发布服务,且不干扰其他团队或破坏整个应用程序的稳定性。
本文介绍一个用于将微服务部署到 Azure Kubernetes 服务 (AKS) 的示例 CI/CD 管道。 每个团队和项目各不相同,因此请不要将本文看作一成不变的规则, 而应该将其看作设计你自己的 CI/CD 过程的起点。
Kubernetes 托管微服务的 CI/CD 管道目标汇总如下:
- 团队可以独立生成和部署其服务。
- 通过 CI 过程的代码更改将自动部署到类似于生产的环境中。
- 在管道的每个阶段都强制执行质量控制。
- 新服务版本可以连同前一版本一起部署。
有关更多背景信息,请参阅微服务体系结构的 CI/CD。
假设
对于本示例而言,下面是有关开发团队和代码库的一些假设:
- 代码存储库为单存储库,文件夹按微服务进行组织。
- 团队的分库策略以基于主库的开发为基础。
- 团队使用发布分支来管理发布。 为每个微服务创建单独的发布。
- CI/CD 过程使用 Azure Pipelines 生成、测试微服务并将其部署到 AKS。
- 每个微服务的容器映像存储在 Azure 容器注册表中。
- 团队使用 Helm 图表来打包每个微服务。
- 使用推送部署模型,其中的 Azure Pipelines 和关联的代理通过直接连接到 AKS 群集来执行部署。
这些假设促成了 CI/CD 管道的许多具体细节。 但是,可根据其他过程、工具和服务(例如 Jenkins 或 Docker Hub)改编此处所述的基本方法。
备选方法
下面是客户在选择用于 Azure Kubernetes 服务的 CI/CD 策略时可能使用的常见替代方案:
- 除了将 Helm 用作包管理和部署工具外,还可使用 Kustomize,它是 Kubernetes 原生的配置管理工具,其中引入了一种用于自定义和参数化应用程序配置的无模板方法。
- 除了将 Azure DevOps 用于 Git 存储库和管道外,还可将 GitHub 存储库用于专用和公共 Git 存储库,将 GitHub Actions 用于 CI/CD 管道。
- 除了使用推送部署模型外,还可使用 GitOps(拉取部署模型)大规模管理 Kubernetes 配置,其中的群集内 Kubernetes 操作员可以根据 Git 存储库中存储的配置同步群集状态。
验证生成
假设某个开发人员正在开发一个名为 Delivery Service 的微服务。 在开发新功能时,开发人员会将代码签入到某个功能分库中。 根据约定,功能分库名为 feature/*
。
生成定义文件包含一个触发器,用于按分支名称和源路径进行筛选:
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
文件夹的代码才会触发 Delivery Service 的生成。 将提交内容推送到与筛选器匹配的分支会触发 CI 生成。 在工作流中,CI 生成此时会运行某种最低程度的代码验证:
- 构建代码。
- 运行单元测试。
目标是保持较短的生成时间,使开发人员可以获得快速反馈。 在该功能准备好合并到主分支后,开发人员会创建一个 PR。 此操作触发另一个 CI 生成来执行其他一些检查:
- 构建代码。
- 运行单元测试。
- 生成运行时容器映像。
- 在映像上运行漏洞扫描。
注意
在 Azure DevOps 存储库中,可以定义策略来保护分支。 例如,策略可以要求在合并到主库之前,必须成功完成 CI 生成并由审批人员签署同意书。
完整 CI/CD 生成
有时候,团队可以部署新版传送服务。 发布经理使用以下命名模式从主分支创建一个分支:release/<microservice name>/<semver>
。 例如,release/delivery/v1.0.2
。
创建此分支会触发完整 CI 生成,从而运行前面所述的所有步骤,加上以下步骤:
- 向 Azure 容器注册表推送容器映像。 该映像标记有版本号(取自分库名称)。
- 运行
helm package
以打包服务的 Helm 图表。 该图表还标有版本号。 - 将 Helm 包推送到容器注册表。
假设此生成成功,它会使用 Azure Pipelines 发布管道触发部署 (CD) 过程。 此管道包含以下步骤:
- 将 Helm 图表部署到 QA 环境。
- 审批者签署同意书,然后包就会转到生产环境。 请参阅通过审批进行发布部署控制。
- 在 Azure 容器注册表中为生产命名空间重新标记 Docker 映像。 例如,如果当前标记为
myrepo.azurecr.io/delivery:v1.0.2
,则生产标记为myrepo.azurecr.io/prod/delivery:v1.0.2
。 - 将 Helm 图表部署到生产环境。
即使在单存储库中,也可将这些任务的范围限定为单个微服务,这样团队就能快速进行部署。 该过程包含一些手动步骤:审批 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 安全策略来防止容器以 root 身份运行。
Helm 图表
考虑使用 Helm 来管理服务的生成和部署。 下面是可帮助实现 CI/CD 的一些 Helm 功能:
- 通常,单个微服务由多个 Kubernetes 对象定义。 Helm 允许将这些对象打包到单个 Helm 图表中。
- 可以使用单个 Helm 命令而不是一系列 kubectl 命令来部署图表。
- 图表的版本显式受控。 使用 Helm 发布版本、查看发布和回滚到以前的版本。 使用语义版本控制以及用于回滚到以前版本的功能来跟踪更新和修订。
- Helm 图表使用模板来避免在多个文件之间复制标签和选择器等信息。
- Helm 可以管理图表之间的依赖关系。
- 图表可以存储在 Azure 容器注册表等 Helm 存储库中,并可集成到生成管道中。
有关将容器注册表用作 Helm 存储库的详细信息,请参阅将 Azure 容器注册表用作应用程序图表的 Helm 存储库。
单个微服务可能涉及多个 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 中将基于 Docker 的应用打包到 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 流程,生成管道可能包含以下任务:
使用
Docker
任务生成测试运行程序容器。通过针对测试运行器容器调用 docker run 来运行测试。 这使用
Docker
任务。使用
PublishTestResults
任务发布测试结果。 请参阅生成映像。使用本地 docker 生成和
Docker
任务或使用Azure 容器注册表生成和AzureCLI
任务生成运行时容器。使用
Docker
或AzureCLI
任务将容器映像推送到Azure 容器注册表(或其他容器注册表)。使用
HelmDeploy
任务打包 Helm 图表。使用
HelmDeploy
任务将 Helm 包推送到 Azure 容器注册表(或其他 Helm 存储库)。
CI 管道的输出是一个生产就绪的容器映像,以及微服务的已更新 Helm图表。 此时,发布管道可以接管工作。 每个微服务都有一个独特的发布管道。 发布管道将配置为使用一个触发源,该源设置为发布了工件的 CI 管道。 通过此管道,可以独立部署每个微服务。 发布管道执行以下步骤:
- 将 Helm 图表部署到开发/QA/过渡环境。
helm upgrade
命令可与--install
标志结合使用,以支持首次安装和后续升级。 - 等待审批者批准或拒绝部署。
- 重新标记容器映像以供发布
- 将发布标记推送到容器注册表。
- 在生产群集中部署 Helm 图表。
有关创建发布管道的详细信息,请参阅发布管道、草稿发布和发布选项。
下图显示了本文中所述的端到端 CI/CD 过程: