基于 Kubernetes 的平台工程——服务流水线:构建云原生应用程序

86 阅读47分钟

本章内容包括:

  • 发现交付云原生应用程序所需的各个组件
  • 了解创建和标准化服务流水线的优势
  • 使用 TektonDaggerGitHub Actions 构建云原生应用程序

在上一章中,你安装并体验了一个由四个服务组成的简单分布式会议(Conference)应用程序。本章将介绍如何利用流水线这一交付机制,持续交付每个组件。你将看到并实际操作如何构建、打包、发布以及上线这些服务,使它们能够运行在你组织的环境中。

本章引入了**服务流水线(service pipeline)**的概念。服务流水线涵盖了从源代码构建软件到生成可运行制品的全部步骤。本章分为两个主要部分:

  1. 持续交付云原生应用程序需要哪些条件?

  2. 服务流水线

    • 什么是服务流水线?

    • 服务流水线的实战应用:

      • Tekton:Kubernetes 原生流水线引擎
      • Dagger:用代码定义流水线,并实现跨环境运行
    • 我该使用 Tekton、Dagger 还是 GitHub Actions?

3.1 持续交付云原生应用程序需要哪些条件?

在使用 Kubernetes 时,团队需要承担更多与容器及其在 Kubernetes 中运行方式相关的环节和任务。这些额外任务并非“免费”附送,团队必须学会自动化和优化服务持续运行所需的各个步骤。过去属于运维团队的职责,如今越来越多地由负责开发各个独立服务的团队来承担。

新的工具与方法赋予开发者更多权力,让他们能够开发、运行并维护自己构建的服务。本章将介绍一系列工具,这些工具能够自动化从源代码到 Kubernetes 集群中可运行服务的全过程。

本章还会讲解将软件组件(即我们的应用服务)交付到多个运行环境的机制。但在深入工具之前,我们先快速了解一下面临的主要挑战。

构建和交付云原生应用程序的主要挑战:

  1. 跨团队协作与服务解耦

    • 不同团队在构建应用的不同部分时,需要协调合作,并确保服务的设计不会阻碍其他团队的进度,也不会限制他们持续改进服务的能力。
  2. 服务升级不影响整体运行

    • 如果我们想实现持续交付,服务必须能够独立升级,而不至于导致整个应用宕机。
    • 这要求团队考虑向后兼容性,并且新旧版本能否并行运行以避免一次性“大爆炸式”升级。
  3. 制品的存储与分发

    • 每个服务可能会生成多个制品,这些制品需要能从不同环境(甚至不同地区)访问或下载。
    • 在云环境中,所有服务器都是远程的,制品必须能被这些服务器获取;
    • 在本地部署环境中,团队需要自建、配置并维护制品仓库。
  4. 多环境管理与快速部署

    • 为开发、测试、质量保证(Q&A)、生产等不同目的准备多套环境。
    • 如果想加快开发和测试节奏,团队应当能按需创建这些环境。
    • 环境与生产环境的相似度越高,就越能提前发现问题,减少线上故障。

正如上一章所见,构建云原生应用的最大范式转变在于:应用程序不再是一个单一代码库。团队虽然可以独立开发各自的服务,但这会带来分布式系统的额外复杂性。

如果每次新增服务都让团队感到焦虑、浪费大量时间,那就说明方法不对。端到端的自动化是团队敢于频繁添加或重构服务的前提。这种自动化通常通过我们熟知的**流水线(pipeline)**来实现。

正如图 3.1 所示,流水线定义了构建和运行服务所需的所有步骤,并且通常能够无需人工干预自动执行。

image.png

你甚至可以使用流水线来自动化创建新服务,或向身份管理系统中添加新用户

但是,这些流水线到底在做什么?我们是否需要从零开始创建自己的流水线?该如何在项目中实现它们?要实现这些目标,是需要一条流水线还是多条流水线?

第 3.2 节将重点介绍如何使用流水线构建可复制、可共享、可多次执行且能产生相同结果的解决方案。流水线可以有不同的用途,通常会将它们定义为一系列按顺序执行的步骤,以产生预期的输出结果。根据输出的类型,这些流水线可以被归类为不同的类别。

大多数流水线工具允许你将流水线定义为一组任务(也称为 stepsjobs),每个任务都会运行特定的作业或脚本来完成具体操作。
这些任务可以是任何事情,例如:

  • 运行测试
  • 将代码从一个地方复制到另一个地方
  • 部署软件
  • 配置虚拟机
  • 创建用户
    等等。

流水线定义会由一个称为流水线引擎(pipeline engine)的组件来执行。
流水线引擎会读取流水线定义,创建一个新的
流水线实例(pipeline instance)
,然后依次运行定义中的每个任务。

  • 每个任务执行后,可能会生成数据并传递给下一个任务;
  • 如果流水线中的任何步骤发生错误,流水线会立即停止,并将状态标记为 失败(failed)
  • 如果执行过程中没有错误,则该流水线实例会被标记为 成功(successful)

无论流水线执行成功与否,都应根据定义验证是否产生了预期的输出。

图 3.2中,你可以看到流水线引擎读取流水线定义,并基于不同的参数创建不同的流水线实例:

  • 流水线实例 1:正常完成;
  • 流水线实例 2:未能执行定义中的所有任务,执行失败;
  • 流水线实例 3:仍在运行中。

image.png

翻译:


正如预期的那样,基于这些流水线定义,我们可以创建大量不同的自动化解决方案。常见的情况是,一些工具会在流水线引擎之上构建更具体的解决方案,甚至会屏蔽掉与流水线引擎打交道的复杂性,以简化用户体验。
在接下来的章节中,我们将看看不同类型工具的示例,其中有些工具更底层且灵活,有些则更高层、带有强烈倾向,并且是为了应对非常具体的场景而设计的。

但是,这些概念和工具在交付云原生应用程序时如何应用呢?
对于云原生应用程序,我们对如何构建、打包、发布和分发软件组件(服务)以及部署位置有非常明确的预期。
在云原生应用交付的背景下,我们可以将流水线分为两大类:

  1. 服务流水线(Service pipelines)
    负责构建、单元测试、打包并分发(通常是到制品仓库)软件制品。
  2. 环境流水线(Environment pipelines)
    负责在特定环境(如 staging、测试、生产等)中部署和更新所有服务,通常会从单一可信源(source of truth)获取需要部署的内容。

第 3 章专注于服务流水线,
第 4 章专注于使用 GitOps 这种更具声明式风格的方法来定义环境流水线的工具。

通过将**构建过程(服务流水线)部署过程(环境流水线)**分离,我们可以赋予负责将新版本推向客户的团队更多的控制权。
服务流水线和环境流水线运行在不同的资源之上,且预期目标不同。
下一节将更详细地说明服务流水线中通常定义的步骤;第 4 章将介绍环境流水线的要求。

3.2 服务流水线(Service pipelines)

服务流水线定义并执行构建、打包和分发服务制品所需的所有步骤,使其能够部署到环境中。
服务流水线不负责部署新创建的服务制品,但可以负责通知相关方有新的服务版本可用。

如果你标准化了服务的构建、打包和发布方式,那么不同的服务可以共享同一套流水线定义。
应尽量避免让每个团队都为每个服务定义完全不同的流水线,因为他们很可能会重复发明已经被其他团队定义、测试并优化过的流程。
需要完成的任务相当多,遵循一套既定规范可以显著缩短整个流程所需的时间。

“服务流水线”这个名称的含义是:
应用中的每个服务都有一条流水线,描述该服务所需执行的任务。
如果多个服务技术栈类似,那么它们的流水线看起来也应该相似。
服务流水线的主要目标之一,是具备足够细节使其完全无人干预地运行,实现端到端自动化。

服务流水线还可以作为开发团队(负责创建服务)与运维团队(负责在生产环境运行服务)之间的沟通机制。

  • 开发团队希望流水线运行并在构建代码出错时通知他们;
  • 如果没有错误,他们期望流水线生成一个或多个制品;
  • 运维团队可以在流水线中添加各种检查,确保制品满足生产要求,例如策略与合规检查签名安全扫描等,以确保制品达到生产环境运行的标准。

注意
很容易产生为整个应用(即所有服务集合)创建一条单一流水线的想法,就像单体应用时代那样。
但这会破坏独立更新每个服务的目的。
你应避免为一组服务定义单一流水线,因为这会阻碍独立发布的能力。

3.3 节省时间的规范(Conventions that will save you time)

服务流水线在结构与范围上可以更有主见(opinionated)。
通过遵循这些既定规范,可以避免让团队花费大量时间去定义每一个细节,或通过反复试错去摸索出这些规范。以下做法被证明是有效的:

1. 基于主干的开发(Trunk-based development)
  • 确保代码仓库主分支上的内容始终可发布。
  • 不允许合并会破坏主分支构建与发布流程的更改;
  • 只有准备好发布的更改才能合并。
  • 包括功能分支(feature branches) :开发者可在独立分支上工作,不影响主分支。
  • 功能完成并测试后,通过 Pull Request(变更请求)提交给其他开发者评审并合并。
  • 每次合并到主分支时,可自动创建服务新版本及相关制品,实现持续发布(每个新功能合并后都会产生一次发布)。
  • 由于每个发布都是一致且已测试的,因此可以安全地部署到包含应用所有服务的环境中,使团队能够持续向前推进而无需担心其他服务。
2. 源码与配置管理(Source code and configuration management)

云原生服务的源码与配置管理有两种主要流派:

  • 一种服务 / 一个仓库 / 一条流水线

    • 将服务源码及其构建、打包、发布、部署所需的所有配置放在同一仓库。
    • 团队可独立推进,不受其他服务源码影响。
    • 常见做法:源码仓库中包含 Dockerfile(定义镜像构建方式)和 Kubernetes manifest(定义部署方式),以及构建打包该服务所需的流水线定义。
  • 单一仓库(Mono repository)

    • 使用一个大仓库存放所有服务,不同目录对应不同流水线。
    • 需注意避免团队之间因等待合并 PR 而互相阻塞。
3. 消费者驱动的契约测试(Consumer-driven contract testing)
  • 服务通过契约来对其他服务运行测试。
  • 单元测试不应依赖其他服务的实际运行;
  • 通过消费者驱动的契约,每个服务可针对其他 API 测试其功能;
  • 当下游服务发布新版本时,会向上游服务共享新的契约,上游服务即可针对新版本运行测试。
推荐阅读
  • 持续交付:通过构建、测试与部署自动化实现可靠软件发布》
    (Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation,Jez Humble & David Farley,Addison-Wesley Professional,2010)
  • 深入浅出持续交付》(Grokking Continuous Delivery,Christie Wilson,Manning Publications,2022)

这些书中提到的大部分工具都能帮助你实现高效交付。
结合这些实践与规范,我们可以将服务流水线的职责定义为:

服务流水线将源代码转化为可在某个环境中部署的一个或多个制品。

3.4 服务流水线结构

结合之前的定义,我们来看一下云原生应用(运行在 Kubernetes 上)的服务流水线通常包含哪些任务:

  1. 注册接收源码仓库主分支变更通知

    • (源码版本控制系统,如今一般是 Git 仓库)
    • 如果源码有变更,我们需要创建一个新的发布版本。
    • 新版本发布的方式是触发服务流水线,这通常通过 webhook 或基于轮询的机制(定期检测新提交)来实现。
  2. 克隆源码仓库

    • 构建服务前,需要将源码克隆到一台具备构建/编译工具的机器上,以便将源码编译成可执行的二进制格式。
  3. 为新版本创建标签(Tag)

    • 基于主干开发模式,每次变更都可以创建新的版本发布。
    • 这样有助于明确部署内容,并跟踪每个版本包含的变更。
  4. 构建并测试源码

    • 构建过程中,大多数项目会执行单元测试,如果测试失败则中断构建。
    • 需要的工具取决于技术栈,例如编译器、依赖管理工具、代码静态分析器(linter)等。
    • 可以使用 CodeCov 之类的工具来衡量测试覆盖率,并在覆盖率未达标时阻止变更合并。
    • 还会使用安全扫描工具检测依赖中的漏洞(CVE,Common Vulnerabilities and Exposures),一旦发现新漏洞也可阻止变更。
  5. 将二进制制品发布到制品仓库

    • 需要确保这些二进制文件可供其他系统使用(包括流水线的后续步骤)。
    • 这一步通常是将制品通过网络复制到另一个位置,并与源码仓库中创建的版本标签保持一致,以便从二进制文件追溯到生成它的源码。
  6. 构建容器镜像

    • 构建云原生服务时,必须构建容器镜像。
    • 最常见的方式是使用 Docker 或其他容器构建方案。
    • 该步骤通常需要源码仓库中存在 Dockerfile(定义镜像构建方式)和相应的构建工具(builder)。
    • 一些工具(如 CNCF Buildpacks)可以省去 Dockerfile,自动完成容器构建过程。
    • 必须有合适的构建工具,因为可能需要为不同平台生成多个镜像(如 amd64arm64),本书示例均为这两个平台构建。
  7. 将容器镜像发布到镜像仓库

    • 与发布二进制制品类似,需要将容器镜像推送到集中位置(容器镜像仓库),供其他系统访问。
    • 镜像版本需与源码标签及二进制制品版本一致,便于确认运行容器镜像时所对应的源码版本。
  8. 检查、验证并可选地打包 Kubernetes 部署 YAML 文件(可用 Helm)

    • 在 Kubernetes 中运行容器时,需要管理、存储并版本化部署清单(manifest)。

    • 若使用 Helm 等包管理工具,可将这些包与二进制制品及镜像使用相同版本号。

    • 我的经验法则是:

      如果安装你服务的人很多(如开源项目或全球大型分布式组织),应当打包并版本化 YAML 文件;
      如果仅需应对少量团队和环境,可以直接分发 YAML 文件而无需打包。

  9. (可选)将 Kubernetes manifest 发布到集中位置

    • 如果使用 Helm,可将打包好的 Chart 推送到集中位置,方便其他工具拉取并部署到任意数量的 Kubernetes 集群。
    • 正如第 2 章所述,Helm Chart 现在也可以作为 OCI 容器镜像发布到镜像仓库。
  10. 通知相关方有新版本可用

    • 为了实现从源码到服务运行的全流程自动化,服务流水线可以向等待新版本的各方发送通知。
    • 方式包括向其他仓库提交 Pull Request、向事件总线发送事件、给相关团队发邮件等。
    • 也可以采用拉取模式(pull-based),由代理程序持续监控制品仓库或镜像仓库,一旦发现新版本制品可用即触发部署。

图 3.3 展示了上述步骤的顺序,大多数流水线工具都会有可视化界面,方便查看执行流程。

image.png

该流水线的产出是一组可以部署到某个环境中的制品(artifacts),以便让服务启动并运行。
服务需要以不依赖任何特定环境的方式进行构建和打包。
服务可能依赖其他服务才能在环境中正常工作,比如数据库、消息代理(message broker)或其他下游服务等基础设施组件。

无论你选择什么工具来实现这些流水线,都应该关注以下特性:

  • 基于变更自动运行:如果采用基于主干的开发(trunk-based development),每次主分支发生变更都会创建一个新的流水线实例。
  • 执行结果通知:流水线执行后会用清晰的信息通知成功或失败的状态。包括能方便地找到流水线失败的原因和位置、各步骤耗时等。
  • 唯一执行 ID:每次流水线执行都有唯一的 ID,可用于访问执行日志和运行流水线时使用的参数,从而重现当时的环境以排查问题。通过这个 ID,我们也能访问流水线各步骤的日志。通过查看流水线执行记录,还应该能找到所有生成的制品以及它们的发布位置。
  • 手动触发与参数化执行:流水线也可以手动触发,并使用不同的参数来处理特殊情况,比如测试一个仍在开发中的功能分支。

3.4.1 真实环境中的服务流水线

在真实环境中,每当你将变更合并到代码仓库的主分支时,服务流水线就会运行。
如果你遵循基于主干的开发方法,应该是这样运作的:

  • 当你将变更合并到主分支时,服务流水线会运行,并使用最新的代码基创建一组制品。
  • 如果服务流水线执行成功,这些制品就处于可发布状态。我们要确保主分支始终处于可发布状态,因此运行在主分支上的流水线必须始终成功。
  • 如果这个流水线因为某种原因失败,服务团队需要立即切换重点,尽快修复问题。换句话说,团队不应将会破坏主分支流水线的代码合并到主分支。
  • 我们也必须在功能分支上运行流水线以进行验证。

对于每个功能分支,都应运行一个类似的流水线,以验证该分支上的变更能否在主分支上被成功构建、测试和发布。
在现代开发环境中,通常使用 GitHub Pull Request 来运行这些流水线,以确保在合并任何 PR 之前,流水线会验证这些变更。

通常,在将一组功能合并到主分支后,由于我们知道主分支随时可发布,负责该服务的团队会决定打一个新版本标签(tag)。
在 Git 中,这个标签是指向特定提交(commit)的指针,通常标签名会用来表示流水线要创建的制品的版本号。

图 3.4 展示了针对主分支配置的流水线,以及一个用于在创建 Pull Request 时验证功能分支的通用流水线。
这些流水线可以多次并行运行,不断验证新的变更。

image.png

如图 3.4 所示的服务流水线,展示了每次向主分支合并内容时必须执行的最常见步骤。不过,在不同情况下,你可能还需要运行此流水线的一些变体。不同的事件都可能触发一次流水线执行,我们也可以针对不同的目的设置略有不同的流水线,例如:

  • 验证功能分支的变更
    该流水线可以执行与主分支流水线相同的步骤,但生成的构件(artifact)应包含分支名称,可能作为版本号或构件名称的一部分。每次变更都运行流水线可能成本过高且并非总是必要,因此应根据实际需求决定。
  • 验证拉取请求(PR)/变更请求
    此流水线将验证 PR/变更请求中的修改是否有效,以及是否可以用这些最新变更生成构件。通常,流水线的结果会反馈给负责合并 PR 的用户,并在流水线失败时阻止合并。
    该流水线用于确保合并到主分支的内容是有效且可发布的。验证 PR 和变更请求是避免对功能分支的每次变更都运行流水线的一个好办法。当开发者准备好从构建系统获得反馈时,可以创建一个会触发流水线的 PR。如果开发者在 PR 基础上又提交了新修改,流水线会重新触发。

尽管这些流水线之间可能会有一些细微差别和优化,但它们的运行行为及生成的构件基本相同。这些约定和方法依赖流水线执行足够的测试,以确保生成的服务可以部署到某个环境中。

3.4.2 服务流水线的需求

本节将介绍服务流水线正常运行所需的基础设施要求,以及流水线执行工作所需的源代码仓库内容。

首先是服务流水线运行所需的基础设施要求:

  • 源代码变更通知的 Webhook
    需要能够在存放服务源代码的 Git 仓库中注册 webhook,这样在主分支合并新变更时,就能创建一个新的流水线实例。
  • 可用的构件仓库及推送二进制构件的有效凭据
    一旦构建完成,需要将新生成的构件推送到构件仓库中进行存储。这需要配置一个可用的构件仓库,并具备有效的推送凭据。
  • 容器镜像仓库及推送新镜像的有效凭据
    与推送二进制构件类似,我们还需要分发 Docker 容器镜像,以便 Kubernetes 集群在部署新服务实例时能够拉取这些镜像。此步骤需要配置一个容器镜像仓库并具备有效的推送凭据。
  • Helm Chart 仓库及有效凭据
    Kubernetes 的清单文件可以打包成 Helm Chart 进行分发。如果你使用 Helm,就必须具备一个 Helm Chart 仓库及有效的推送凭据。

如图 3.5 所示,流水线实例会与多个常见的外部系统交互。从 Git 仓库到构件仓库,再到容器镜像仓库,维护这些流水线的团队必须确保凭据正确,并且从流水线运行的环境中(网络可达性)能够访问这些组件。

image.png

为了让服务流水线能够正常工作,存放服务源代码的仓库还需要包含 Dockerfile(或其他能够生成容器镜像的方式)以及部署该服务到 Kubernetes 所需的 Kubernetes 清单文件(manifest)。

图 3.6 展示了一个可能的服务源代码仓库目录结构,其中包括:

  • src 目录:包含所有需要编译成二进制格式的源文件。
  • Dockerfile:用于构建该服务的容器镜像。
  • Helm Chart 目录:包含创建 Helm Chart 所需的全部文件,可以将服务打包分发,以便在 Kubernetes 集群中安装该服务。

你可以选择 每个服务单独一个 Helm Chart,也可以为整个应用的所有服务使用一个统一的 Helm Chart。

图 3.6 展示了包含 Helm Chart 定义的服务目录结构,这有助于独立打包和分发各个服务。只要我们在仓库中包含了构建、打包和运行该服务到 Kubernetes 集群所需的全部内容,那么在主分支每次有变更时,服务流水线就需要运行一次,以创建该服务的新版本发布。

image.png

总而言之,服务流水线 负责构建我们的源代码以及相关制品(artifacts),以便将它们部署到某个环境中。
正如前面提到的,服务流水线并不负责将生成的服务部署到线上环境
部署的工作属于**环境流水线(environment pipeline)**的职责,这将在下一章介绍。

3.4.3 关于服务流水线的看法、限制与取舍

在创建服务流水线时,没有一种 “一刀切” 的通用方案。
在实际工作中,你必须根据自身需求作出权衡和妥协。
在探讨 Tekton、Dagger 和 GitHub Actions 等工具之前,先简单聊聊一些我见过团队经常遇到的实际问题。
以下是设计服务流水线时值得考虑的几点(并非完整清单):

  • 避免用过于严格的规则定义流水线的起止边界
    比如,正如前文提到的,你的服务可能并不需要打包成 Helm Chart。
    如果几乎没有场景需要单独安装该服务(例如,该服务严重依赖其他服务),那么从服务流水线中移除该步骤,以及从服务仓库中移除 Chart 定义,可能是很合理的选择。
  • 理解组件与制品的生命周期
    根据服务的变更频率及其依赖情况,服务流水线可以彼此关联,以便一次性构建一组服务。
    绘制这些依赖关系,并理解运维这些服务的团队的需求,可以帮助你确定合适的流水线粒度。
    例如,你可以让各团队随时发布它们服务的新版本容器镜像,但由另一个团队控制打包所有应用服务的 Helm Chart 的节奏与发布周期。
  • 找到最适合你组织的方案
    根据业务优先级优化端到端自动化。
    如果一个关键服务的发布和部署频繁拖慢节奏,应优先让它的服务流水线完善可用,再考虑覆盖其他服务。
    盲目构建泛用方案可能要花很久,最终才发现组织中 80% 的问题其实只集中在一个服务上
  • 不要提前增加不必要的步骤
    虽然本书多次提到用 Helm 打包和分发 Kubernetes 清单,但我并不是在说这是唯一正确做法。
    Helm 只是一个广泛使用的示例工具,你的情况可能并不需要将 Kubernetes 清单打包分发。
    如果确实没有这个需求,服务流水线里就不该有这一步。
    当需求出现时,再扩展流水线以包含更多步骤即可。

3.5 服务流水线实战

市面上有很多流水线引擎,甚至包括像 GitHub Actionsgithub.com/features/ac… 这样的全托管服务,以及其他一些知名的托管 CI(持续集成)服务,它们可以为你提供大量集成能力,用于构建和打包应用的服务。

接下来我们将看看两个项目:TektonDagger
这两个项目都能帮助你处理云原生应用,并且(正如我们将在第 6 章看到的)能让平台团队打包、分发和复用组织内积累的特定知识。

  • Tektontekton.dev)/) 是为 Kubernetes 设计的流水线引擎,功能通用,可以创建任意类型的流水线。
  • Daggerdagger.io)/) 是一个更新的项目,目标是“能在任何地方运行”。
    稍后我们会将 Tekton 和 Dagger 与 GitHub Actions 进行对比。

3.5.1 Tekton 实战

Tekton 最初由 Google 在 Knative 项目knative.dev)/) 中创建(第 8 章会详细介绍 Knative)。
它最早叫 Knative Build,后来从 Knative 独立出来,成为单独项目。
Tekton 的核心特点是:它是一个为 Kubernetes 设计的云原生流水线引擎

在 Tekton 中有两个核心概念:

  • Task(任务)
  • Pipeline(流水线)

Tekton 的流水线引擎是一组 Kubernetes 组件,它们可以执行 TaskPipeline 这两类 Kubernetes 资源。
像本书中大多数 Kubernetes 项目一样,Tekton 可以直接安装到 Kubernetes 集群中。
官方文档对 Tekton 的价值有很好的解释:tekton.dev/docs/concep…

注意:我在本书对应的仓库中提供了一套 Tekton 的分步教程,可以从安装 Tekton 和 tekton/hello-world/ 示例开始学习:github.com/salaboy/pla…

安装 Tekton 时,会部署一组 自定义资源定义(CRD) ,这些是 Kubernetes API 的扩展,用于定义 TaskPipeline
同时还会安装流水线引擎本身,它能处理这些资源。
此外,你还可以安装 Tekton Dashboard 和命令行工具 tkn

安装完成后,你会在集群中看到一个名为 tekton-pipelines 的命名空间,其中包含:

  • 流水线控制器(流水线引擎)
  • 流水线 Webhook 监听器(用于监听来自外部源的事件,例如 Git 仓库)
Tekton Task 示例

一个 Task 在 Tekton 中就是一个普通的 Kubernetes 资源,例如:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: hello-world-task                     # ① 任务定义名称
spec:
  params:
   - name: name                               # ② 可配置参数
     type: string
     description: who do you want to welcome?
     default: tekton user
  steps:
   - name: echo
     image: ubuntu                            # ③ 使用的容器镜像
     command:
       - echo                                 # ④ 要执行的命令
     args:
       - "Hello World: $(params.name)"        # ⑤ 命令参数,引用了任务参数

该 Task 定义文件可在这里找到:
github.com/salaboy/pla…

定义好 Task 后,用 kubectl apply -f task.yaml 应用到集群,就能让 Tekton 识别它,但这并不会立即运行任务。

运行 Task(TaskRun)

若要运行一个 Task,必须创建 TaskRun 资源,例如:

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: hello-world-task-run-1
spec:
  params:
  - name: name
    value: "Building Platforms on top of Kubernetes reader!"  # ① 指定参数值
  taskRef:
    name: hello-world-task                                    # ② 引用 Task 名称

kubectl apply -f taskrun.yaml 后,流水线引擎会执行该任务。
可以用 kubectl get taskrun 查看 TaskRun 状态,也能用 kubectl get pods 找到相关 Pod 并查看日志,最终会看到输出:

Hello World: Building Platforms on top of Kubernetes reader!

至此,你已经执行了第一个 Tekton TaskRun!🎉
不过单个 Task 还不够有趣——如果我们能将多个 Task 串联起来,就能构建出完整的服务流水线。

3.5.2 Tekton 中的流水线

单个任务(task)可能很有用,但当你使用流水线(pipeline)将这些任务按顺序组合起来时,Tekton 才真正发挥出它的优势。

流水线是按照特定顺序组织的一组任务。下面的这个流水线使用了我们前面定义过的任务,它会先打印一条消息,然后从一个 URL 获取文件,再读取文件内容,并将其传递给我们的 “Hello World” 任务来打印消息。

图 3.7 展示了一个简单的 Tekton 流水线,由三个 Tekton 任务组成。

image.png

在这个简单的流水线中,我们使用了来自 Tekton Hub 的现有任务定义(wget),Tekton Hub 是一个社区仓库,托管通用任务;然后我们在流水线内部定义了 cat 任务以展示 Tekton 的灵活性;最后使用了前一节定义的 “Hello World” 任务。

接下来看看一个在 Tekton 中定义的简单服务流水线(hello-world-pipeline.yaml)。不要害怕,这里会有很多 YAML,我已经提醒过你了。见清单 3.6。

清单 3.6 流水线定义

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: hello-world-pipeline
  annotations:
    description: |
      Fetch resource from internet, cat content and then say hello
spec:
  results:                                            
  - name: message
    type: string
    value: $(tasks.cat.results.messageFromFile)
  params:                                             
  - name: url
    description: resource that we want to fetch
    type: string
    default: ""
  workspaces:                                         
  - name: files
  tasks:
  - name: wget
    taskRef:                                          
      name: wget
    params:
    - name: url
      value: "$(params.url)"
    - name: diroptions
      value:
        - "-P"  
    workspaces:
    - name: wget-workspace
      workspace: files
  - name: cat
    runAfter: [wget]
    workspaces:
    - name: wget-workspace
      workspace: files
    taskSpec:                                         
      workspaces:
      - name: wget-workspace
      results: 
        - name: messageFromFile
          description: the message obtained from the file
      steps:
      - name: cat
        image: bash:latest
        script: |
          #!/usr/bin/env bash
          cat $(workspaces.wget-workspace.path)/welcome.md | tee /tekton/results/messageFromFile
  - name: hello-world
    runAfter: [cat]
    taskRef:
      name: hello-world-task                           
    params:
      - name: name
        value: "$(tasks.cat.results.messageFromFile)"  

spec.results 定义了流水线执行后期望得到的一组结果值,任务可以在执行时设置这些结果值。
② 类似任务参数,流水线可以定义用户在运行流水线时可以设置的参数,这些参数可以传递给各个任务。
③ 流水线和任务允许使用 Tekton Workspaces 来存储持久化信息,用于任务间共享数据。
④ 使用任务引用(taskRef)来引用一个未创建的任务,需要先确保安装该任务定义才能创建 PipelineRun
⑤ 可以在流水线中内联定义任务,这会让流水线文件更复杂,但有时方便将多个任务串联,如本例中用于读取下载文件内容并作为字符串传递给 Hello World 任务。
⑥ 同样需要在集群中安装 “hello-world-task” 定义。可以通过 kubectl get tasks 查看可用任务。
⑦ 使用 Tekton 的强大模板机制提供 Hello World 任务所需的值,这里引用了 cat 任务的结果。

完整流水线定义见:hello-world-pipeline.yaml

在应用流水线定义之前,需要先安装由 Tekton 社区创建和维护的 wget 任务:

kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/wget/0.1/wget.yaml

然后将流水线资源应用到集群:

kubectl apply -f hello-world-pipeline.yaml

如流水线定义所示,spec.tasks 字段包含任务数组,这些任务必须已部署到集群中,流水线定义了任务执行的顺序。这些任务引用可以是你自己的任务,也可以像示例一样来自 Tekton catalog(社区维护的可复用任务仓库)。

同样,由于任务需要 TaskRun 来执行,每次执行流水线时都需要创建 PipelineRun,如下所示。

清单 3.7 PipelineRun 表示流水线实例(执行)

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: hello-world-pipeline-run-1
spec:
  workspaces:                      
    - name: files
      volumeClaimTemplate: 
        spec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 1M 
  params: 
  - name: url                      
    value: "https://raw.githubusercontent.com/salaboy/salaboy/main/welcome.md"
  pipelineRef:
    name: hello-world-pipeline     

① 创建 PipelineRun 时,需要将流水线定义中定义的工作空间绑定到实际存储,本例中创建了 1MB 的 VolumeClaim。
② 流水线参数 url 可以是任何在 PipelineRun 上下文可访问的 URL。
③ 需要提供要执行的流水线定义名称。

PipelineRun 资源见:pipeline-run.yaml

应用此文件后,Tekton 会执行流水线中的所有任务。每个任务会创建一个 Pod 和一个 TaskRun 资源。流水线本质上是对任务的编排,也就是创建 TaskRuns。

查看流水线执行的 TaskRuns:

> kubectl get taskrun
NAME                                   SUCCEEDED  STARTTIME  COMPLETIONTIME
hello-world-pipeline-run-1-cat         True       109s       104s
hello-world-pipeline-run-1-hello-world True       103s       98s
hello-world-pipeline-run-1-wget        True       117s       109s

每个 TaskRun 对应一个 Pod:

> kubectl get pods
NAME                                         READY   STATUS         AGE
hello-world-pipeline-run-1-cat-pod           0/1     Completed      11s
hello-world-pipeline-run-1-hello-world-pod   0/1     Completed      5s
hello-world-pipeline-run-1-wget-pod          0/1     Completed      19s

查看最后一个任务日志:

> kubectl logs hello-world-pipeline-run-1-hello-world-pod
Defaulted container "step-echo" out of: step-echo, prepare (init)
Hello World: Welcome, Internet traveler! Do you want to learn more about Platforms on top of Kubernetes? Check this repository: https://github.com/salaboy/platforms-on-k8s

你也可以在 Tekton Dashboard 中查看 Tasks、TaskRuns、Pipelines 和 PipelineRuns,触发新的任务和流水线执行,并查看每个任务的日志。若已在集群中安装 Tekton Dashboard,可通过以下命令访问:

> kubectl port-forward -n tekton-pipelines services/tekton-dashboard 9097:9097

图 3.8 展示了 Tekton Dashboard 用户界面,可以在其中浏览任务和流水线定义,触发新的任务和流水线执行,并查看每个任务的输出日志。

image.png

如果需要,你可以在以下仓库找到关于如何在 Kubernetes 集群中安装 Tekton 以及如何运行服务流水线的逐步教程:
github.com/salaboy/pla…

在教程的末尾,你会找到我为每个 Conference 应用服务定义的更复杂流水线的链接。这些流水线更复杂,因为它们需要访问外部服务、拥有发布工件和容器镜像的凭据,以及在集群中执行某些特权操作的权限。如果你对更多细节感兴趣,请查看教程的这一部分:
github.com/salaboy/pla…

3.5.3 Tekton 的优势与附加功能

如我们所见,Tekton 非常灵活,允许你创建高级流水线,并且包含其他功能,例如:

  • 输入与输出映射,用于在任务之间共享数据
  • 事件触发器,可以监听事件以触发流水线或任务
  • 命令行工具,可让你从终端轻松与任务和流水线交互
  • 简单的仪表盘,用于监控流水线和任务执行(图 3.9)

image.png

图 3.9 显示了由社区驱动的 Tekton 仪表盘,你可以使用它来可视化流水线的执行情况。请记住,由于 Tekton 是建立在 Kubernetes 之上的,你也可以像操作其他 Kubernetes 资源一样,通过 kubectl 来监控流水线。不过,对于技术水平较低的用户来说,没有什么比图形界面更直观的了。

但是,如果你想用 Tekton 实现一个服务流水线,你将花费相当多的时间来定义任务、流水线、输入输出映射、为 Git 仓库定义合适的事件监听器,以及进一步低层次地定义每个任务使用的 Docker 镜像。创建和维护这些流水线及其相关资源可能成为一项全职工作。为此,Tekton 推出了一个目录(catalog)计划,将任务(未来版本将支持流水线和资源)进行共享。Tekton 目录可在 github.com/tektoncd/ca… 获取。

借助 Tekton 目录,我们可以创建引用目录中已定义任务的流水线。在上一节中,我们使用了从该目录下载的 wget 任务;你可以在 hub.tekton.dev/tekton/task… 查看 wget 任务的完整描述。因此,我们无需自己定义这些任务。你还可以访问 hub.tekton.dev,在那里可以搜索任务定义,并获得关于在流水线中安装和使用这些任务的详细文档(图 3.10)。

Tekton Hub 和 Tekton 目录允许你重用由大量用户和公司创建的任务和流水线。我强烈建议你查看 Tekton 概览页面,该页面总结了使用 Tekton 的优势,包括谁应该使用 Tekton 以及为什么使用:
tekton.dev/docs/concep…

image.png

Tekton 在云原生领域是一个相当成熟的项目,但它也存在一些挑战:

  • 你需要在 Kubernetes 集群中安装并维护 Tekton。你不希望流水线与应用工作负载运行在同一环境,因此可能需要一个独立的集群。
  • 没有简单的方法可以在本地运行 Tekton 流水线。出于开发目的,你必须依赖访问 Kubernetes 集群来手动运行流水线。
  • 你需要了解 Kubernetes 才能定义和创建任务及流水线。
  • 虽然 Tekton 提供了一些条件逻辑,但由于 YAML 的限制以及 Kubernetes 声明式方法的限制,其能力是有限的。

接下来我们将介绍一个名为 Dagger 的项目,它旨在缓解上述一些问题。Dagger 并不是为了替代 Tekton,而是提供了一种不同的方法来解决构建复杂流水线时的日常挑战。

3.5.4 Dagger 实战

Dagger (dagger.io) 的诞生目标很明确:“让开发者能够使用自己喜欢的编程语言构建流水线,并且可以在任何地方运行。”Dagger 仅依赖容器运行时来运行流水线,这些流水线可以使用每位开发者都能编写的代码来定义。Dagger 目前支持 Go、Python、TypeScript 和 JavaScript SDK,但 Dagger 团队正在快速扩展对更多语言的支持。

Dagger 并不仅仅关注 Kubernetes。平台团队必须在保证团队能够利用 Kubernetes 强大且声明式特性的同时,也能让开发团队高效地使用适合的工具完成工作。本小节将对比 Dagger 与 Tekton,说明 Dagger 的适用场景以及如何与其他工具互补。

如果你有兴趣入门 Dagger,可以参考以下资源:

像 Tekton 一样,Dagger 也有一个流水线引擎,但该引擎既可本地运行,也可远程运行,为不同环境提供统一的运行时。Dagger 不直接集成 Kubernetes,这意味着无需涉及 Kubernetes CRD 或 YAML 文件。这对于创建和维护流水线的团队的技能和偏好来说可能非常重要。

在 Dagger 中,我们通过编写代码来定义流水线。由于流水线本质上就是代码,这些流水线可以通过任何代码打包工具进行分发。例如,如果我们的流水线是用 Go 编写的,可以使用 Go modules 导入其他团队编写的流水线或任务;如果使用 Java,则可以使用 Maven 或 Gradle 来打包和分发流水线库,从而促进重用。

图 3.11 显示了开发团队如何使用 Dagger SDK 编写流水线,然后使用 Dagger 引擎在任何 OCI 容器运行时(如 Docker 或 PodMan)中执行这些流水线。无论你是在本地开发环境(如 Docker for Mac 或 Windows)、持续集成环境,还是 Kubernetes 中运行流水线,这些流水线的行为都是一致的。

image.png

Dagger 流水线引擎负责协调流水线中定义的任务,并优化容器运行时执行每个任务时的资源请求。Dagger 流水线引擎的一个显著优势是,它从零开始设计,以优化流水线的运行效率。想象一下,你每天多次构建大量服务。这样不仅会让 CPU 持续高负载,而且反复下载制品的流量开销会非常高——如果你运行在云提供商上,根据消耗收费,成本会更高。

与 Tekton 类似,Dagger 使用容器来执行流水线中的每个任务(步骤)。流水线引擎通过缓存先前执行的结果来优化资源消耗,从而避免重复执行使用相同输入的任务。此外,你可以在本地笔记本/工作站上运行 Dagger 引擎,也可以远程运行,甚至在 Kubernetes 集群内运行。

以我的开发者背景来看,与 Tekton 相比,我更喜欢用熟悉的编程语言来编写流水线的灵活性。开发者创建、版本控制和共享代码都很容易,因为无需学习新的工具。

下面我们来看一个 Dagger 的服务流水线示例,而不是 “Hello World”。使用 Dagger Go SDK 定义的服务流水线如下所示,展示了每个服务要执行的主要目标。请注意 buildServicetestServicepublishService 函数,这些函数编码了如何构建、测试和发布每个服务。它们使用 Dagger 客户端在容器中执行操作,由 Dagger 编排,如清单 3.11 所示。

清单 3.11 使用 Dagger 定义任务的 Go 应用示例

func main() {
  var err error
  ctx := context.Background()
  if len(os.Args) < 2 {
    ...)
  }
  client := getDaggerClient(ctx)
  defer client.Close()
  switch os.Args[1] {
    case "build":
      if len(os.Args) < 3 {
        panic(...)
      }
      _, err = buildService(ctx, client, os.Args[2])
      
    case "test":
      err = testService(ctx, client, os.Args[2])
    case "publish":
      pv, err := buildService(ctx, client, os.Args[2])
      err = publishService(ctx, client, os.Args[2], pv, os.Args[3])
    case "all":
      pv, err := buildService(ctx, client, os.Args[2])
      err = testService(ctx, client, os.Args[2])
      err = publishService(ctx, client, os.Args[2], pv, os.Args[3])
   default:
     log.Fatalln("invalid command specified")
}

你可以在 service-pipeline.go 找到完整定义。

运行

go run service-pipeline.go build notifications-service

Dagger 会使用容器构建 Go 应用源代码,并生成可推送到容器注册表的镜像。查看 buildService 函数(清单 3.12),可以看到它会针对目标平台列表(amd64 和 arm64)构建二进制文件,然后使用 client.Container 创建容器。由于每一步都是程序化定义的,因此还可以定义哪些内容需要缓存以供后续构建使用(通过 client.CacheVolume)。

清单 3.12 使用 Dagger 内置函数的 Go 任务示例

func buildService(ctx context.Context, 
                  client *dagger.Client, 
                  dir string) ([]*dagger.Container, error) {
  srcDir := client.Host().Directory(dir)
  platformVariants := make([]*dagger.Container, 0, len(platforms))
  for _, platform := range platforms {
    ctr := client.Container()
    ctr = ctr.From("golang:1.20-alpine")
    ctr = ctr.WithDirectory("/src", srcDir)
    ctr = ctr.WithMountedCache("/go/pkg/mod", client.CacheVolume("go-mod"))
    ctr = ctr.WithMountedCache("/root/.cache/go-build", client.CacheVolume("go-build"))
    ctr = ctr.WithDirectory("/output", client.Directory())
    ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")
    ctr = ctr.WithEnvVariable("GOOS", "linux")
    ctr = ctr.WithEnvVariable("GOARCH", architecture(platform))
    ctr = ctr.WithWorkdir("/src")
    ctr = ctr.WithExec([]string{"go", "build","-o", "/output/app","."})
    outputDir := ctr.Directory("/output")
    binaryCtr := client.Container(dagger.ContainerOpts{Platform: platform}).
                        WithEntrypoint([]string{"./app"}).
                        WithRootfs(outputDir)
    platformVariants = append(platformVariants, binaryCtr)
  }
  return platformVariants, nil
}

这些流水线使用 Go 构建 Go 应用,但完全可以构建其他语言,只要使用相应工具即可。每个任务只是一个容器。Dagger 与开源社区会提供基础构建模块,而各组织需创建特定领域库以集成第三方或内部/遗留系统。Dagger 专注于赋能开发者,你只需编写可分发的代码库,无需编写插件。

你可以尝试运行某个服务的流水线,或参考教程:github.com/salaboy/pla…。如果运行两次流水线,第二次几乎是瞬时完成,因为大多数步骤都被缓存。

与 Tekton 不同,Dagger 流水线可以在本地运行,而无需 Kubernetes 集群,这带来了优势。例如,无需等待远程反馈,开发者可以在本地使用容器运行时(如 Docker 或 Podman)执行流水线,包括集成测试,然后再提交到 Git 仓库。快速反馈能加快开发节奏。

那么,如果我们想在远程环境运行流水线呢?好消息是,行为一致:远程 Dagger 流水线引擎会执行流水线,无论它运行在 Kubernetes 内部还是作为托管服务,流水线行为和缓存机制都是一致的。图 3.12 展示了在 Kubernetes 中安装 Dagger 流水线引擎并运行相同流水线时的执行流程。

image.png

翻译:当 Dagger 流水线引擎安装在远程环境中(例如 Kubernetes 集群、虚拟机或其他计算资源)时,我们可以连接它并运行流水线。Dagger Go SDK 会从本地环境获取所需的全部上下文,并将其发送给 Dagger 流水线引擎以远程执行任务。我们无需担心将应用源代码发布到线上供流水线使用。

可以参考这个分步教程,了解如何在 Kubernetes 上运行 Dagger 流水线:github.com/salaboy/pla…

如你所见,Dagger 会使用持久化存储(Cache)缓存所有构建和任务,以优化性能并缩短流水线运行时间。负责在 Kubernetes 内部署和运行 Dagger 的运维团队需要根据组织运行的流水线量来跟踪所需存储空间。

在本小节中,我们展示了如何使用 Dagger 创建服务流水线。可以看到,Dagger 与 Tekton 非常不同:你无需使用 YAML 编写流水线,可以使用任何受支持的编程语言编写;可以在本地或远程运行相同的代码流水线;也可以使用与应用相同的工具分发流水线。

从 Kubernetes 的角度来看,使用 Dagger 会失去将流水线作为 Kubernetes 原生资源管理的方式。我认为如果 Dagger 社区获得足够的反馈和需求,将可能朝这个方向扩展。

从平台工程角度来看,你可以创建并分发复杂流水线(及任务),让团队使用并扩展现有工具。这些流水线无论在哪里执行都行为一致,极具灵活性。平台团队可以利用这种灵活性更高效地决定流水线运行的位置(基于成本和资源),而不会增加开发者负担,因为开发者始终可以在本地运行流水线进行开发。

3.5.5 我应该使用 Tekton、Dagger 还是 GitHub Actions?

如你所见,Tekton 和 Dagger 提供了构建非约束性流水线的基本构建模块。换句话说,我们可以用 Tekton 或 Dagger 构建服务流水线,几乎可以构建任何类型的流水线。

使用 Tekton 时,我们采用基于 Kubernetes 资源的方法,享受可扩展性和自愈特性。Kubernetes 原生资源便于将 Tekton 与其他 Kubernetes 工具集成,如资源管理和监控。利用 Kubernetes 资源模型,你可以像管理其他 Kubernetes 资源一样管理 Tekton 流水线及 PipelineRun,并复用现有工具。

使用 Dagger 时,我们可以用熟悉的编程语言和工具定义流水线,并在任何地方运行这些流水线(本地工作站与远程环境行为一致)。这使得 Tekton 与 Dagger 成为平台构建者构建“更有约束性”流水线的理想工具,供开发团队使用。

另一方面,你也可以使用托管服务如 GitHub Actions。例如,你可以查看这里提到的所有项目是如何使用 GitHub Actions 配置服务流水线的。比如,可以查看 notifications 服务流水线:github.com/salaboy/pla…

该 GitHub Actions 流水线使用 ko-build 构建服务,然后将新镜像推送到 Docker Hub。注意,这条流水线没有运行测试,而是使用自定义步骤(github.com/salaboy/pla…)检查服务代码是否有变更;只有在代码变更时才执行构建和推送操作。

使用 GitHub Actions 的优势是,你无需维护运行流水线的基础设施,也无需为执行流水线的机器付费(如果流水量较小)。但如果流水线数量大且数据密集,GitHub Actions 成本会很高。

出于成本或行业监管原因无法在云端运行流水线时,Tekton 和 Dagger 在提供构建和运行复杂流水线的能力方面表现突出。Dagger 已专注于成本和运行时优化,而 Tekton 及其他流水线引擎也在朝这个方向发展。

值得注意的是,你可以将 Tekton 和 Dagger 与 GitHub 集成。例如,可以使用 Tekton Triggers(github.com/tektoncd/tr… GitHub 仓库的提交。你也可以在 GitHub Actions 内运行 Dagger,使开发者能够在本地执行与 GitHub Actions 相同的流水线,这在默认情况下无法轻松实现。

现在,当我们有了准备好部署到多个环境的制品和配置时,让我们来看常用的 GitOps 方法,通过环境流水线实现持续部署。

3.6 回到平台工程

作为平台建设的一部分,你需要帮助团队以自动化方式构建他们的服务。大多数情况下,需要做出决策来标准化服务在各团队间的构建和打包方式。如果平台团队能提供一个解决方案,让各团队可以在本地尝试,或提供合适的环境在推送变更到 Git 仓库前进行测试,这将提高团队的开发速度和反馈循环,使他们能够自信地推进工作。可能还需要一个独立的环境,用于验证 pull request,并在仓库的主分支不可发布时提醒团队。

虽然 GitHub Actions(以及其他托管服务)是一个流行的解决方案,但平台工程团队可能会根据预算或其他平台级决策(如与 Kubernetes API 的对齐)选择不同的工具或服务。

在本书的演示和分步教程(github.com/salaboy/pla…)中,我做出了一些有意识的选择,这些选择可能与你的项目有很大不同。首先,本书呈现的项目复杂度较低;其次,为了保持资源的组织性和版本化以支持未来的修改,所有应用服务的源代码都保存在一个简单的目录结构下。将所有服务源代码放在同一个仓库中的决定,会影响我们服务流水线的设计形式。

所提供的服务流水线(无论是使用 Tekton 还是 Dagger)都接收一个参数,即用户想要构建的仓库目录。如果你设置了 webhook 来在 pull request 时触发流水线,你必须过滤变更的路径,以确定运行哪条服务流水线。这增加了整个设置的复杂性。如前几节所建议的一种替代方法是,每个服务使用一个单独的仓库。这种方式可以为每个服务定义自定义服务流水线(可复用通用任务),并简化 webhook 定义,因为你明确知道在代码变更时需要运行哪些流程。单仓库对应单服务的主要问题在于用户与访问权限管理,因为新增服务意味着你需要创建新的仓库,并确保开发者能够访问。

平台团队还需要在服务流水线的起点和终点做出决策。以这里提供的示例为例,服务流水线从提交变更时开始,到每个服务的容器镜像发布完成时结束。Walking skeleton 服务的服务流水线不会打包和发布单个服务的 Helm Chart。图 3.13 展示了这些示例中定义的服务流水线的职责范围。

image.png

你需要自问,为每个服务单独维护 Helm Chart 是明智之举还是过度设计。你应该清楚地了解谁会使用这些构件。尝试回答以下问题,以找到适合你团队的策略:

  • 你会单独部署各个服务,还是它们总是作为一组一起部署?
  • 服务的变更频率如何?是否有某些服务变更更频繁?
  • 有多少团队会部署这些服务?
  • 你是否在创建一个开源社区会使用的构件,并且许多用户会单独部署这些服务?

对于本章提供的示例,提供了一个独立的应用级流水线,用于打包和发布 Conference 应用的 Helm Chart。

做出这一决定的原因很简单:每位读者都会在集群中安装该应用,我需要一个简单的方式来实现这一点。如果读者不想使用 Helm 在集群中安装应用,他们可以导出执行 helm template 命令的输出,并使用 kubectl 应用这些输出。另一个重要因素是 Helm Chart 与应用服务的生命周期。应用的整体结构变化不大,Helm Chart 的定义可能只在需要添加或移除服务时发生变化。然而,服务的代码会频繁变动,我们希望让负责这些服务的团队能够持续添加更改。

图 3.14 展示了两种互补的服务流水线方法:在开发者环境中运行的服务提供快速反馈循环,而在远程环境中运行的服务生成的构件,则可供团队在不同环境中部署同一应用时使用。

image.png

最后,本书中的示例除了使用 GitHub Actions 关联的示例外,并未提供配置来直接使用 Git 仓库的 webhook。引导读者获取正确的令牌并在多个 Git 提供商中进行配置并不复杂,但如果详细说明,会占用很多篇幅。使用这些机制的团队无需担心管理服务流水线所需的凭据。作为平台团队,为开发(以及其他)团队自动化访问凭据,使其能够直接连接服务,这是加快工作流的基础。

总结

服务流水线定义了如何从源代码生成可在多个环境中部署的构件。遵循 trunk-based 开发和“一服务=一仓库”的实践,有助于团队更高效地标准化软件构建和发布流程。

你需要找到适合团队和应用的方案。没有放之四海皆准的解决方法,需要做出权衡:应用服务变更的频率如何?如何将它们部署到各个环境?回答这些问题有助于定义服务流水线的起点和终点。

Tekton 是专为 Kubernetes 设计的流水线引擎。你可以使用 Tekton 设计自定义流水线,并利用 Tekton Catalog 中公开可用的共享任务和流水线。你现在可以在集群中安装 Tekton 并开始创建流水线。

Dagger 允许你使用熟悉的编程语言编写和分发流水线。这些流水线可以在任何环境中执行,包括开发者的笔记本电脑。

像 GitHub Actions 这样的工具非常有用,但成本可能较高。平台建设者需要寻找能够提供足够灵活性,以构建和分发可被其他团队复用的任务,同时符合公司规范的工具。允许团队在本地运行流水线是一个很大的优势,因为它能提升开发者体验并缩短反馈时间。

如果你按照逐步教程操作过,你已经获得了使用 Tekton 和 Dagger 创建并运行服务流水线的实践经验。