第 8 章:持续部署

本章内容概览:

  • 使用 GitHub Actions 创建自动化工作流
  • 创建 CI 流水线以运行微服务的自动化测试
  • 创建自动化部署流水线将微服务部署到 Kubernetes

本章将展示我们在前两章学到的技能的应用。在第 6 章和第 7 章中,我们探讨了如何使用代码构建基础设施,以及如何手动部署微服务到该基础设施中。

在本章中,我们将探索如何自动化部署过程。你将学习如何利用 GitHub Actions 构建一个自动化的持续部署(CD)流水线来管理微服务。这种自动化是实现微服务成功的关键。为了简化学习过程,我们将专注于单一微服务的部署,这为将来将流程应用于所有微服务奠定了基础。

本章可能会感觉相当挑战性,因为我们将深入探讨高级主题。虽然构建自动化工作流并不复杂,但第三个示例将涉及到前面章节学到的 Docker 和 kubectl 的知识。如果你对这些工具还不太熟悉,完成本章的学习可能会有一定难度。

如果你更倾向于手动部署你的微服务,你可以选择跳过本章,待到以后再学习。只有经历过手动部署的繁琐和单调,你才能真正体会到拥有一个全自动部署系统的便利。

本章的学习将是实践和尝试的过程,通过这些,你将能够仅通过推送代码到 GitHub 就能触发微服务的部署。

8.1 新工具

本章将介绍 GitHub Actions(见表 8.1),这是一个基于云的服务,可以在代码库中的操作触发时运行自动化工作流。本章中,我们将利用 GitHub Actions 自动测试和部署我们向代码库主分支推送的更新。

表 8.1 第 8 章的新工具

工具版本用途
GitHub ActionsN/A用于运行由特定事件触发的自动化工作流的云服务。

8.2 获取代码

与本书其他章节的代码结构不同,本章的示例代码分布在不同的代码库中。这种设计方便你直接复制并尝试每个 GitHub Actions 示例。若你不熟悉如何复制代码库,不必担心,很快你就会掌握。

本章有三个示例项目,从这里开始: http://mng.bz/RmWv 。你可以通过更改网址中的数字序号,例如 example-2example-3 来访问其他示例。

有关克隆代码库的帮助,请参考第 2 章。如果你在代码上遇到问题,请在 GitHub 的相关代码库中提交 issue。

8.3 在本章运行示例

与前几章不同,本章中的项目示例代码将在云环境而非本地运行。为了尝试 GitHub Actions,你需要复制每个示例代码库的副本。复制代码库意味着你会拥有一份代码的副本,可以随时对其进行修改。

首先,如果你还没有 GitHub 账户,请在 https://github.com/ 注册一个。登录后,可以在示例代码库页面点击 Fork 按钮,如图 8.1 所示。

图 8.1 复制示例代码库。你需要这样做,以便可以尝试 GitHub Actions
图 8.1 复制示例代码库。你需要这样做,以便可以尝试 GitHub Actions

按照页面上的指南操作,这将会在你的 GitHub 账户中创建代码库的副本。这意味着你现在可以修改这些代码,并通过推送更改到你的版本库来触发 GitHub Actions 工作流。启动这些工作流的步骤将在后面解释。在启动工作流之前,你需要在你的代码库页面中的 Actions 选项卡下启用工作流,如图 8.2 所示。

当你克隆本章示例代码时,确保克隆的是你自己账户下的副本,使用以下命令:

git clone [email protected]:<your-name>/chapter-8-example-1.git
图 8.2 复制仓库后,转到 Actions 选项卡,并单击启用仓库的工作流。
图 8.2 复制仓库后,转到 Actions 选项卡,并单击启用仓库的工作流。

8.4 什么是持续集成?

在深入持续部署(CD)之前,我们首先讨论持续集成(CI)。CI 是实现 CD 的基础,而且通常,建立 CI 流水线比构建 CD 流水线更为简单。实际上,CD 流水线往往基于 CI 流水线,并添加部署功能。因此,我们将首先学习如何为持续集成创建 GitHub Actions 工作流。

CI 流水线是一个自动化过程,用于监测代码库中的变更,并对代码进行各种检查和验证,确保其正常运行。这通常包括构建代码和运行代码检查器,但大多数情况下,CI 流水线的主要目的是执行自动化测试。你将在第 9 章学习如何创建自动化测试,但在本章中,你将看到如何在每次向 GitHub 推送代码更改时自动执行这些测试。

对于一个多开发者共同工作的团队来说,CI 流水线尤为重要。它们被称为持续集成流水线,因为它们的目的是测试从多个开发者处合并来的代码。在一个忙碌的开发团队中,保证代码持续运行可能是一个挑战,但 CI 流水线能够在问题出现时即刻发现。

图 8.3 展示了这一过程。多个开发者各自在本地代码库中提交代码。他们在某个时间点将代码与其他开发者的更改合并,然后推送到托管在 GitHub 上的代码库中。推送代码或提交拉取请求(pull request)会触发我们在 GitHub Actions 中设置的 CI 流水线。通常,CI 流水线会对代码执行各种检查和自动化测试。如果所有测试都通过,则该工作流运行被标记为成功。如果有任何测试失败,则会标记为失败,并通常会通过电子邮件通知团队有关问题。

图 8.3 一个 CI 流水线整合多个开发者的代码,并运行自动化测试和其他验证
图 8.3 一个 CI 流水线整合多个开发者的代码,并运行自动化测试和其他验证

8.5 持续部署简介

既然我们已经掌握了持续集成(CI)的概念,现在是时候了解持续部署(CD)了。CD 是一种自动化技术,通过它,我们可以频繁且自动地将代码部署到生产或测试环境中。简单来说,任何代码的更新都将自动触发新的软件部署。CD 流水线是在 CI 流水线的基础上建立的,它在保留原有功能的同时,增加了将代码部署到生产环境的额外步骤。

为了实现自动化部署,我们需要编写可以自动执行且在云环境中无需人工干预的部署脚本。编写的部署脚本需要尽可能健壮,经过严格的测试以确保其可靠性,因为我们期望部署过程中没有失败。部署脚本应尽量简单,减少组件数量,并进行充分的测试,以防止在生产环境中出错,因为调试云中的错误会比在本地更加困难。

在实际应用到生产环境之前,我们会在本地开发机器上对部署脚本进行测试。因此,本章的一部分内容将是在本地创建和测试部署流水线,尽管最终目标是让这些代码在 GitHub Actions 下的云计算机上自动运行。

图 8.4 展示了 CD 流水线的运作过程。与先前的图 8.3 相似,图中展示了多名开发者将代码整合到托管在 GitHub 上的代码库中。他们对代码库的更改触发了在 GitHub Actions 中设置的 CD 流水线。与图 8.3 中的 CI 流水线执行类似的操作:检查代码和执行测试。图 8.4 中新增的是,CD 流水线还会将微服务部署到我们的 Kubernetes 集群中。这意味着任何对微服务的代码更改都会自动触发它的部署到生产环境。当然,这是基于微服务的构建和测试成功的前提。如果开发者引入的更改破坏了构建或导致自动化测试失败,CD 流水线将中断并标记为失败,部署不会进行,并且团队将通过电子邮件接收到失败的详细信息。

图 8.4 一个 CD 流水线响应代码更改自动将微服务部署到生产环境
图 8.4 一个 CD 流水线响应代码更改自动将微服务部署到生产环境

尽管构建一个功能完整的 CD 流水线可能具有挑战性,但理解其概念并实现基本功能并不困难。实际上,构建 CD 流水线的难度并不比编写一个 Shell 脚本复杂得多。在本章中,我们会反复强调这一点。

我通常将 CD 比喻为在云中自动运行一个 Shell 脚本,尽管这是一种过于简化的比喻,但它有助于阐述基本概念。事实上,编写一个可靠的 Shell 脚本通常是创建 CD 流水线中最困难的部分。将你的 CD 流水线实现为 Shell 脚本,这不仅有助于简化流程,还使得在本地机器上测试变得更容易。而且,使用 Shell 脚本是达到这一目的的一种非常实用的方式。

在本章的示例中,我们将使用 Shell 脚本调用 Docker 来构建和发布我们的微服务,并使用 kubectl 将微服务部署到 Kubernetes。

持续交付与持续部署

在行业中,人们常常使用“持续交付”和“持续部署”这两个术语。这两个术语听起来非常相似,意义也十分接近,这可能导致一些混淆,因为它们都缩写为 CD,并且概念非常相似,使得人们难以记清它们各自的含义(有时甚至我也会混淆)。以下是一个简化的解释:

  • 持续交付意味着我们随时准备将我们的软件部署到生产环境,但部署可以是自动的也可以是手动的。
  • 持续部署则意味着我们对软件所做的每个更改都会自动部署到生产环境(前提是通过了测试和其他检查点)。

8.6 自动化部署的优势

为什么我们要着力于部署的自动化呢?自动化部署提供了诸多好处:

  • 手动部署不仅枯燥无味,还容易出错,耗费时间。通过自动化,我们可以减少时间消耗并降低出错的风险。
  • 自动化部署为快速交付产品功能给客户提供了便捷的途径,使我们能够迅速作出更改并获得及时的反馈。对于任何正在开发的新产品,优先实现自动化部署是关键,因为这是将产品呈现给客户的方式(当然,这取决于你的产品是什么)。
  • 当部署过程自动化、可靠且反应迅速时,它就像魔法一样,无需我们关注,使我们能专注于为客户提供有价值的功能,而不必分心于复杂、繁琐或容易出错的部署过程。
  • 自动化部署的记录可以作为一种审计轨迹,显示了谁在什么时间做了哪些更改以及为什么进行这些更改。

总的来说,如果你觉得自动化部署的成本难以承受,那么使用微服务的成本可能也是难以承受的。随着微服务应用的扩展,手动部署的工作量会迅速增加。微服务的成功最终取决于我们是否拥有一个健壮的自动化部署流水线。

有时候,我会思考,围绕微服务的不安是否源自于人们未能成功自动化微服务部署的糟糕体验,或者他们的自动化部署流水线实施得很差,导致频繁的故障(无论出于何种原因)。我相信,自动化部署的成功与否可能是我们与微服务体验好坏的决定性因素。这就是它至关重要的原因。

幸运的是,自动化部署相对并不复杂。如果你对 Docker 和 Kubernetes 已经有了一定的了解,那么构建一个可靠的部署流水线不应该是一个难题。这正是首先使用 Docker 和 Kubernetes 的主要优势之一:这些技术设计之初就是为了简化自动化部署。

8.7 初探 GitHub Actions 自动化

本节将为你提供一个简单的入门指南,帮助你使用 GitHub Actions 创建自动化工作流。如果你已经熟悉 GitHub Actions,可以跳过这一部分,直接前往第 8.8 节。

8.7.1 为何选用 GitHub Actions?

GitHub Actions 提供了一种创建自动化工作流的优秀方式,这些工作流可响应代码库中的事件,如代码推送或拉取请求。尽管市面上有许多类似服务,例如 Bitbucket 和 GitLab 都为其托管的代码库提供相似功能,另外还有专门的自动化服务如 Travis CI 和 CircleCI。

这些服务大多通过 YAML 文件进行配置(与 GitHub Actions 类似),主要功能是自动响应特定事件执行任务。所有这些服务的核心功能非常相似,一旦你掌握了一个,便能轻松上手其他服务。

GitHub Actions 的独特之处在于其与 GitHub 的无缝集成。作为软件行业的领军者,GitHub 提供了一个完美的环境,让代码和自动化流水线可以并行工作。这种集成使得从代码库事件中触发工作流变得异常简单,且便于工作流访问代码库的内容。

作为一个成熟且具可扩展性的平台(可以添加自定义操作),GitHub Actions 已经被广泛应用于生产环境中的各种项目。由于 GitHub 和 GitHub Actions 大部分服务是免费的,这使得你可以在免费账户的限制内进行广泛的试验和使用。

8.7.2 工作流是什么?

虽然我已经多次提到了“工作流”,但究竟什么是工作流呢?工作流,顾名思义,是一系列按特定顺序执行的任务或步骤。这与我所说的“流水线”相似,但“流水线”通常被视为一种传输工具。在持续部署的背景下,流水线是一个将工作代码传送至生产环境的通道。

在 GitHub Actions 中,自动化流水线被称为工作流。如果你查阅 GitHub Actions 的官方文档(这是我推荐的),你会频繁遇到“工作流”这一术语。

GitHub Actions 中的工作流通过一个 YAML 文件定义,并由一系列任务和步骤构成。每个步骤可以执行一个或多个命令,或者调用包含所需命令的 shell 脚本。图 8.5 描述了工作流的构成。

图 8.5 说明 GitHub Actions 工作流的各个部分
图 8.5 说明 GitHub Actions 工作流的各个部分

8.7.3 创建新的工作流

在 GitHub 上为代码库创建新的工作流最简单的方法是切换到“Actions”标签页。如果你之前在第 8.3 节复制了 chapter-8-example-1 代码库,那么你可以直接跳至该页面并按照指引操作。

当你访问一个尚未设定工作流的代码库时,GitHub 会展示一个“开始使用 GitHub Actions”界面。如果你访问的是已设定一个或多个工作流的代码库(如之前复制的 chapter-8-example-1),你会看到一个“新建工作流”按钮,点击后进入选择工作流的界面,类似于图 8.6 所示的界面。

图 8.6 GitHub Actions 为许多语言、技术和框架提供模板工作流,使得为你的项目创建 CI 或 CD 流水线非常容易。
图 8.6 GitHub Actions 为许多语言、技术和框架提供模板工作流,使得为你的项目创建 CI 或 CD 流水线非常容易。

在此界面,你可以找到许多适合作为新工作流起点的模板。流行的语言和框架都有对应的模板自动列出,也可以基于关键词搜索。尝试搜索“Node.js”来查看一些用于构建、测试和部署 Node.js 项目的模板。你可以选择任何一个模板来创建工作流,以便查看它们的功能。当然,你可能并不使用 Node.js —— 无论你使用的是 C#、Python、Go、Rust 还是其他语言,都可以进行搜索以找到适合你项目的新工作流模板。

8.7.4 示例 1 概述

我们从本章的第一个示例开始,初探 GitHub Actions。示例代码可从此处获取: http://mng.bz/RmWv

这个示例展示了 GitHub Actions 工作流的基础,其功能只是简单地打印“hello world”。尽管功能基础,但足以演示如何通过推送更改至代码库来触发工作流。

图 8.7 展示了这个基本项目的结构,关键文件只有两个:一个是用于打印“hello world”的 shell 脚本,另一个是工作流的配置文件。

图 8.7 示例 1 项目的概览
图 8.7 示例 1 项目的概览

8.7.5“Hello World”shell 脚本

核心工作流命令包含在名为 index.sh 的 shell 脚本中。这是一个极其简单的脚本,如清单 8.1 所示,它的功能是将“hello world”打印至终端。你可以在终端尝试如下命令:

cd chapter-8-example-1
./index.sh

如果你使用的是 Windows 系统,你需要在 WSL2 下的 Linux 终端中运行这个脚本。

清单 8.1“Hello World”shell 脚本

echo "Hello world!" # 打印“Hello world!”

通常在执行 shell 脚本前,我们需要将其标记为可执行:chmod +x ./index.sh

在本例中,这一步骤已由 Git 仓库设置完成。当你添加自己的脚本到代码仓库时,可能需要执行以下命令来标记脚本为可执行:

git update-index --chmod=+x <path-to-the-script-file>

将你的脚本标记为可执行后,记得添加、提交并推送更改到你的托管代码仓库。

虽然我们可以不使用 shell 脚本直接在工作流中执行命令,我选择使用脚本是为了强调一个观点:如果你能编写 shell 脚本,那么创建 CD 流水线并不难。

8.7.6“Hello World”工作流

清单 8.2 展示了如何在 GitHub Actions 中运行我们的“hello world”shell 脚本的工作流。这个 YAML 文件配置了一个非常简单的工作流,虽然实际上工作流可以更为复杂。

我们看到工作流可以通过代码推送或手动通过 GitHub Actions UI 触发。工作流的步骤首先是检出代码仓库,然后执行打印“hello world”的 shell 脚本。由于运行器是新的,它需要明确地检出代码,以使其内容在工作流运行器中可用。

清单 8.2 GitHub Actions 的“Hello World”工作流

name: Hello world  # 命名工作流

on:
  push:
    branches:
      - main  # 在 main 分支的推送触发此工作流

  workflow_dispatch:  # 允许从 Actions 标签页手动触发此工作流

jobs:
  hello-world:  # 工作流包含名为“hello-world”的单个任务
    runs-on: ubuntu-latest  # 工作流运行于最新版 Ubuntu Linux,也可选择 Windows 或 macOS

    steps:
      - uses: actions/checkout@v3  # 检出代码仓库至运行器
      - name: Run the shell script  # 步骤名称
        run: ./index.sh  # 执行 shell 脚本

8.7.7 内联调用命令

正如前面所示,我们可以简化工作流,通过直接在工作流中调用命令,而非使用 shell 脚本。对于“hello world”示例,这可能看起来如下:

steps:
  - uses: actions/checkout@v3
  - name: Prints hello world
    run: echo "Hello world!" # 直接在工作流中运行命令

我们还可以使用不同的语法来顺序调用多个命令:

steps:
  - uses: actions/checkout@v3
  - name: Prints hello world
    run: |  # 使用竖线开始多行命令块
        <command1>  # 多个命令跨越几行
        <command2>
        <等等>

这种方式可以创建包含多个步骤的工作流,每个步骤都执行自己的命令集:

steps:
  - uses: actions/checkout@v3
  - name: First step
    run: |  # 顺序执行的工作流步骤
        <command1>
        <command2>
        <等等>
  - name: Second step
    run: |
        <command1>
        <command2>
        <等等>

尽管直接在工作流中调用命令完全可行,我在示例 1 中使用 shell 脚本主要是为了演示其用途;实际上我们并不需要它。但很快,我们将探讨真正需要使用 shell 脚本来分离命令与工作流文件的实际场景。

8.7.8 触发工作流的代码更改

我们已经创建了一个简单的工作流,并为其配置了一个基础任务,即打印“hello world”。现在,让我们看看这个工作流的实际运行情况。

正如第 8.3 节所述,要尝试本章中的示例,你需要复制每个代码库。如果你还没有这样做,现在可以通过以下链接复制示例 1: http://mng.bz/RmWv

之后,克隆你的代码库副本。转到你的代码库的“Actions”标签页,并启用工作流(如果尚未启用)。由于我们还未触发任何工作流,因此历史记录应为空。

现在,尝试进行一些修改,例如在文件 index.sh 中将“hello world”更改为“hello computer”。提交这些更改到你的代码库,并执行 git push 将更改推送到 GitHub。这将触发我们之前在清单 8.2 中定义的工作流。

在执行 git push 后,尽管我们仍在终端操作,但似乎没有什么明显的变化。要查看实际的执行结果,你需要返回 GitHub 并查看工作流的历史记录。

8.7.9 查看工作流历史

你可以通过访问你的 GitHub 代码库并切换到“Actions”标签来查看工作流历史。图 8.8 展示了我所创建示例 1 的工作流历史。你可以看到,我触发了两次工作流运行,第一次运行失败了,而第二次(在顶部)成功了。稍后我将解释第一次为什么会失败。

你的示例 1 副本的界面会有所不同,但你应该能看到工作流运行的记录。如果你刚刚推送了你的第一次代码更改,你应该会看到一次运行记录。如果你还没有触发你的工作流,列表将显示为空。

图 8.8 GitHub Actions 中示例 1 的工作流运行历史
图 8.8 GitHub Actions 中示例 1 的工作流运行历史

如果你没有时间复制代码库,或者只是想快速了解其样貌,你可以通过以下链接查看我的示例 1 版本的工作流历史: http://mng.bz/27xa

现在,进入其中一个运行记录。图 8.9 中,我点击了最上面的一次成功运行。你可以看到输出中工作流如何调用我们的 shell 脚本 index.sh,以及它如何将“Hello world!”打印到工作流的输出中。这是一个简单但基础的示例,但在下一节中,我们将开始将其提升到更高的级别。

图 8.9 在 GitHub Actions 中查看“Hello world”工作流成功的输出
图 8.9 在 GitHub Actions 中查看“Hello world”工作流成功的输出

8.7.10 通过 UI 触发工作流

如我前面所述,我们的工作流可以通过两种方式触发。我们已尝试了第一种方式:推送代码更改。但你可能注意到在清单 8.2 中,我们还添加了一个事件来触发工作流,即手动调用。这意味着我们可以在 GitHub Actions UI 中点击一个按钮来触发工作流,而无需进行任何代码更改。这样做的操作是选择工作流(在这种情况下是“Hello World”工作流),点击运行工作流的下拉菜单,然后点击运行工作流按钮。图 8.10 展示了这个操作的界面。

这将执行与我们推送代码更改时相同的操作。能够手动触发工作流非常有用,它简化了测试过程。如果我们需要修改配置并希望重新运行工作流以应用新配置,这也显得尤为重要。当我们开始使用环境变量配置我们的微服务部署工作流时,我将再次提及此功能。

图 8.10 在 GitHub Actions 中手动触发工作流
图 8.10 在 GitHub Actions 中手动触发工作流

8.7.11 我们实现了什么?

到目前为止,我们已经为 GitHub Actions 创建了一个简单且基础的工作流。虽然这个工作流不太实用,但它展示了构建 CI 和 CD 流水线的基本步骤并非难事。如果你能编写简单的 YAML 文件和 shell 脚本,你基本上已经掌握了构建这些流水线的核心技能。但我们不会止步于此。接下来,我们将学习如何将构建和部署微服务的命令集成到工作流中。这些命令并不新鲜;我们在第 3 章中已经构建并发布了我们的第一个微服务,并在第 6 章将其部署到 Kubernetes。你很快就会了解如何将这些命令集成到 GitHub Actions 工作流中,从而自动化部署我们的第一个微服务。

8.8 实施持续集成

在我们创建持续部署(CD)流水线之前,让我们先构建一个持续集成(CI)流水线。CI 本身就是非常有用的,并且为我们铺垫了进入 CD 的道路。在本节中,我们将尝试进行自动化测试。尽管我们将在第 9 章更详细地讨论自动化测试,但在这里,我们将看到如何在 CI 流水线中触发自动化测试。

8.8.1 示例 2 概述

继续到本章的示例 2,代码可从此链接获取: http://mng.bz/1Jxq

这个示例项目包含了我们一直在使用的 Node.js 视频流微服务,现在增加了自动化测试部分。

项目的布局如图 8.11 所示。这个示例中没有 shell 脚本。通常,CI 流水线可以通过一两个命令实现,所以没有必要使用 shell 脚本,因为在 CI 流水线中运行的命令不够复杂,而且本地测试它们已经足够简单。当然,在接下来的部分,当我们实施 CD 流水线并增加部署命令时,由于复杂性的增加,使用 shell 脚本来封装这些命令将变得很理想。

图 8.11 示例 2 项目的概览
图 8.11 示例 2 项目的概览

8.8.2 自动化测试的工作流

清单 8.3 展示了如何在 GitHub Actions 中运行我们的自动化测试的工作流。这里有两个命令值得注意。第一个是 npm ci,它类似于 npm install,但是专为无人监控的 CI 流水线设计(如果你好奇,npm ci 中的 “ci” 实际上指的是 “clean install”)。它仅安装 package-lock.json 中列出的特定依赖项(忽略 package.json),以获取确定性和稳定的依赖集,从而实现更可靠和可重复的部署流水线。

第二个命令是 npm test,这是 Node.js 中用来运行自动化测试的标准命令。你将在下一章更详细地了解如何实施这个命令。

这个示例专为 Node.js 设置,但你可以通过改变几个命令来轻松适配任何其他语言或框架。实际上,你甚至不需要费心去查找——只需搜索一个工作流模板作为起点,就像我们在 8.7.3 节中讨论的那样。GitHub Actions 为每种语言和框架都准备了现成的模板。

清单 8.3 Node.js 微服务的自动化测试工作流

name: CI  # 命名工作流

on:
  push:
    branches:
      - main  # 当推送到 main 分支时触发此工作流

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3  # 获取代码库的副本

      - uses: actions/setup-node@v3  # 设置 Node.js 环境
        with:
          node-version: '18.x'  # 指定 Node.js 版本
          cache: 'npm'

      - run: npm ci  # 安装依赖
      - run: npm test  # 运行自动化测试

要亲自尝试这个示例,你需要复制并克隆代码库。确保进入 GitHub Actions 标签并启用运行工作流。然后尝试更改代码并推送你的代码。这将触发工作流并运行自动化测试。你可能想尝试破坏自动化测试来看看失败的工作流是什么样的。这是在受控环境中故意制造问题,了解失败工作流行为的好方法。

8.8.3 我们实现了什么?

到目前为止,我们已经为 GitHub Actions 创建了一个实用的工作流。每次我们将代码推送到 GitHub 时,这个工作流就可以自动测试微服务。我们可以轻松看到一个自动化测试流水线如何在每次代码更改时进行测试,从不错过。如果我们推送了有问题的代码,我们的 CI 流水线会迅速发现问题并通知我们,使我们能够迅速纠正问题。在下一章,你将学习如何创建自动化测试。

8.9 持续部署微服务

我们现在进入本书中的核心内容之一——使用 GitHub Actions 实现自动化部署。本节中,我们将设置一个工作流,该工作流将在我们向 GitHub 推送代码更改时自动将我们的视频流微服务部署到 Kubernetes。

8.9.1 示例 3 概述

这一部分的内容涉及本章的示例 3,你可以在这里获取相关代码: http://mng.bz/PR9R

项目的结构如图 8.12 所示。这与我们之前在图 8.11 中看到的示例 2 类似,但请注意此处有新增的文件,这些文件包括用于构建、发布和部署我们的微服务的各种 shell 脚本。此外,还有一个 YAML 文件用于将微服务部署到 Kubernetes,以及一个 Dockerfile 用于创建微服务的生产镜像。

图 8.12 示例 3 项目概览
图 8.12 示例 3 项目概览

8.9.2 模板化我们的部署配置

在我们设置微服务项目的自动化部署之前,我们首先需要配置部署流水线的方法。我们采取这种做法是因为我们希望避免将与部署相关的各种细节硬编码到我们的项目中。我们需要一种方法来参数化我们的部署配置文件,以便我们可以在 CD 流水线执行期间填充某些值。

在之前的章节中,我们已经使用环境变量来配置我们的微服务,并且已证明环境变量是配置我们的 CD 流水线的有效方法(同时也是简单的方法)。硬编码细节,如容器仓库地址和版本号到代码中是不理想的。我们可能希望将来更改容器仓库的位置,如果其 URL 在众多不同的微服务代码库中被硬编码,那将非常麻烦。

微服务的版本号也不能硬编码,因为它会频繁变更。如果我们硬编码了它,就意味着我们需要定期更新我们的代码以保持版本号的最新状态,这既繁琐又可能阻碍快速部署。这只是一些我们不想在微服务项目中硬编码的示例,但任何实际项目都可能有更多我们希望从代码中分离出来的可配置值。

因此,我们将使用环境变量来替代硬编码这些值,如容器仓库和版本号。我们需要将这些值插入到我们的 Kubernetes 部署配置文件中,但我们如何做到这一点呢?有几种方法可以实现这一点,但幸运的是,我们可以使用一个简单的命令行工具 envsubst,该工具是 Linux 系统中的标配。由于我们在工作流文件中定义的 CD 流水线运行在 Ubuntu Linux 上,我们可以依靠 envsubst 将环境变量中的值注入到我们的配置文件中。

这在图 8.13 中有所说明,展示了我们的 Kubernetes 部署配置文件 deploy.yaml 的摘录(完整文件请见 GitHub)。注意我们如何设置了 VERSIONCONTAINER_REGISTRY 的环境变量,并调用 envsubst 命令将这些值注入到配置文件 deploy.yaml 中。

图 8.13 使用 <strong>envsubst</strong> 命令将参数注入我们的部署配置文件 deploy.yaml
图 8.13 使用 envsubst 命令将参数注入我们的部署配置文件 deploy.yaml

在将任何工具包含进自动化部署流水线之前,我们应该熟悉如何使用它,并已经在本地机器上练习过其使用。除非我们已经知道如何手动操作它,否则我们无法自动化它!因此,如果你之前没有使用过 envsubst,现在是直接尝试使用它的好时机,然后再将其整合到我们的工作流中。只需打开终端并尝试一下。如果你在使用 Windows,你将需要在 WSL2 下的 Linux 终端中进行操作。

请首先设置以下环境变量:

export CONTAINER_REGISTRY=<your-registry-url>
export VERSION=1

你需要使用你自己的容器仓库 URL。你可以沿用之前创建的容器仓库,或者按照我们在第 3 章的 3.9.1 节介绍的方式通过用户界面 (UI) 简易创建一个新的容器仓库,又或者按照我们在第 7 章的 7.9.2 节介绍的方式通过 Terraform 脚本创建,这种方式相对更为复杂。

现在,请在 chapter-8-example-3 项目中执行以下命令,使用 envsubst 命令处理 deploy.yaml 文件:

cd chapter-8-example-3
envsubst < ./scripts/kubernetes/deploy.yaml

在输出结果中,你会看到 deploy.yaml 文件中的参数已被 CONTAINER_REGISTRYVERSION 环境变量的值所替代。

注意 想要深入了解 envsubst 的更多信息,你可以在网上找到许多相关的教程、指南和视频。

如果你正在从示例 3 的代码库中执行 shell 脚本,应该已经设置了正确的权限。如果未设置,你需要在运行之前更改脚本的权限:

chmod +x ./scripts/build-image.sh

在执行其他 shell 脚本前,请确保也为它们设置了相应的权限。接下来,让我们查看清单 8.5,示例 chapter-8-example-3/scripts/push-image.sh 中的脚本,这个脚本展示了如何将镜像推送到容器仓库。

清单 8.5 发布镜像的 shell 脚本

echo $REGISTRY_PW | docker login $CONTAINER_REGISTRY \
  --username $REGISTRY_UN --password-stdin
docker push $CONTAINER_REGISTRY/video-streaming:$VERSION

请继续设置以下额外的环境变量:

export REGISTRY_UN=<registry-username>
export REGISTRY_PW=<registry-password>

你需要输入自己容器仓库的用户名和密码。

然后执行以下命令来运行 shell 脚本:

./scripts/push-image.sh

这会将镜像发布到你的容器仓库。如果你想进行测试,请按照第 3 章的 3.9.3 节的指南来启动仓库中发布的镜像。

部署微服务的 shell 脚本

现在已将镜像推送到容器仓库,下一步需要一个 shell 脚本来将我们的微服务部署到 Kubernetes。清单 8.6 展示了部署微服务的脚本。

清单 8.6 部署微服务的 shell 脚本

export

或者按照第 3 章节 3.9.1 节介绍的方法通过 UI 界面轻松创建一个新的容器仓库,亦或如第 7 章节 7.9.2 所述,通过 Terraform 脚本来实现更为复杂的创建方式。

接下来,在项目 chapter-8-example-3 中运行 envsubst 命令,使用 deploy.yaml 文件作为输入:

cd chapter-8-example-3
envsubst < ./scripts/kubernetes/deploy.yaml

执行后,你将看到输出中的 deploy.yaml 文件中的参数已经被 CONTAINER_REGISTRYVERSION 环境变量的值所替换。

注意 想要深入了解 envsubst 工具,你可以在网络上找到众多相关的教程、指导和视频资源。

若你是在示例 3 的代码库中运行 shell 脚本,脚本权限应已正确配置。如果未配置,你需要在执行前将其设置为可执行状态:

chmod +x ./scripts/build-image.sh

这一设置同样适用于未来可能使用的其他 shell 脚本。现在,让我们查看清单 8.5(源自 chapter-8-example-3/scripts/push-image.sh),你可以看到将镜像发布到容器仓库的命令。该 shell 脚本从我们预设的环境变量中获取输入,并加入 REGISTRY_UNREGISTRY_PW,这是用于认证的用户名和密码。

清单 8.5 发布镜像的 shell 脚本

echo $REGISTRY_PW | docker login $CONTAINER_REGISTRY \
  --username $REGISTRY_UN --password-stdin
docker push $CONTAINER_REGISTRY/video-streaming:$VERSION

接下来,你需要设置额外的环境变量以继续操作:

export REGISTRY_UN=<registry-username>
export REGISTRY_PW=<registry-password>

请确保你填入自己容器仓库的用户名和密码。

然后自行运行 shell 脚本:

./scripts/push-image.sh

此操作会将镜像发布到你的容器仓库。如果你想测试这一过程,请按照第 3 章节 3.9.3 的指南启动我们已发布到仓库的镜像所支持的微服务。

部署微服务的 shell 脚本

在将镜像成功发布到容器仓库后,我们需要一个 shell 脚本来将微服务部署到 Kubernetes。清单 8.6(源自 chapter-8-example-3/scripts/deploy-service.sh)展示了该脚本的内容。

此脚本利用之前提到的 envsubst 工具将环境变量的值插入到 Kubernetes 部署配置文件中,然后使用 kubectl 命令将配置应用到 Kubernetes 集群。

清单 8.6 部署微服务的 shell 脚本

export KUBECONFIG=~/.kube/config  # 指定 Kubernetes 配置文件的位置
envsubst < ./scripts/kubernetes/deploy.yaml | kubectl apply -f -

设置以下环境变量:

export KUBECONFIG=~/.kube/config  # 确保你已经配置了 kubectl 以访问你的 Kubernetes 集群
export NAMESPACE=<kubernetes-namespace>  # 使用你自己的 Kubernetes 命名空间

你需要输入你自己的 Kubernetes 命名空间作为这些环境变量的值。

现在自己运行 shell 脚本:

./scripts/deploy-service.sh

这将会把微服务部署到你的 Kubernetes 集群中。

8.9.4 自动化部署流程

我们已经准备好了构建、发布和部署微服务的相关 shell 脚本。现在,让我们将它们集成到 GitHub Actions 工作流中,以实现自动化部署。清单 8.7(源自 chapter-8-example-3/.github/workflows/deploy.yaml)展示了这一自动化部署流程。

清单 8.7 自动化部署的 GitHub Actions 工作流

name: Deploy to Kubernetes

on:
  push:
    branches:
      - main  # 当推送到 main 分支时触发此工作流

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3  # 检出代码库
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      - name: Log in to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Build and push Docker image
        run: |
          export CONTAINER_REGISTRY=${{ secrets.CONTAINER_REGISTRY }}
          export VERSION=${{ github.sha }}
          ./scripts/build-image.sh
          ./scripts/push-image.sh
      - name: Deploy to Kubernetes
        run: |
          export KUBECONFIG=${{ secrets.KUBECONFIG }}
          export NAMESPACE=${{ secrets.K8S_NAMESPACE }}
          export CONTAINER_REGISTRY=${{ secrets.CONTAINER_REGISTRY }}
          export VERSION=${{ github.sha }}
          ./scripts/deploy-service.sh

在此工作流中,我们首先检出代码库,然后配置 Docker Buildx,并登录到 DockerHub。接下来,我们构建并推送 Docker 镜像,并最终将微服务部署到 Kubernetes 集群。

为使用此工作流,你需在 GitHub 仓库的 Secrets 中配置以下秘密变量:

  • DOCKER_USERNAME:你的 DockerHub 用户名
  • DOCKER_PASSWORD:你的 DockerHub 密码
  • CONTAINER_REGISTRY:你的容器仓库 URL
  • KUBECONFIG:用于访问 Kubernetes 集群的配置
  • K8S_NAMESPACE:你的 Kubernetes 命名空间

配置这些秘密变量后,每当你将代码推送至 main 分支,此工作流便会自动运行,完成微服务的构建、发布和部署。

8.9.5 验证部署结果

验证部署是否成功是流程的最后一步。你可以通过执行以下命令来查看你的微服务是否正常运行:

kubectl get pods -n <your-namespace>

请检查 Pod 的状态,确保它们显示为 Running

至此,你已成功地构建了一个自动化的 CI/CD 流水线。每当你向 GitHub 推送代码时,该流水线将自动进行构建、发布和部署操作。

8.9.6 安装和配置 kubectl

接下来的问题是如何在工作流的运行器中安装和配置 kubectl。请参考清单 8.7 中的以下代码行:

- uses: tale/kubectl-action@v1  # 在运行器中安装 kubectl
  with:
    base64-kube-config: ${{ secrets.KUBE_CONFIG }}  # 从 GitHub Secrets 导入 kubectl 配置
    kubectl-version: v1.24.2  # 指定 kubectl 的版本

这段代码演示了如何在我们的工作流中使用自定义操作来安装 kubectl。GitHub Marketplace 上有众多有用的自定义操作可供选择(访问 GitHub Marketplace: https://github.com/marketplace?type=actions )。

这个操作不仅安装 kubectl,并且可以指定版本(v1.24.2),同时从 GitHub Secrets 中加载配置文件。

注意 有关 kubectl-action 的更多信息,请参阅 GitHub 文档 http://mng.bz/JdOZ

8.9.7 从 GitHub Secrets 提取环境变量

GitHub Secrets 是一个用于存储敏感信息(如 Kubernetes 的配置细节)的服务,并可以在工作流中使用这些信息。

Secrets 主要用于配置环境变量。例如,在清单 8.7 中,我们如下设置了环境变量:

env:
  VERSION: ${{ github.sha }}  # 根据 Git 提交的 SHA 哈希自动设置版本号
  CONTAINER_REGISTRY: ${{ secrets.CONTAINER_REGISTRY }}  # 设置容器仓库的 URL
  REGISTRY_UN: ${{ secrets.REGISTRY_UN }}  # 从 GitHub Secrets 获取容器仓库的用户名
  REGISTRY_PW: ${{ secrets.REGISTRY_PW }}  # 从 GitHub Secrets 获取容器仓库的密码

这里,我们从 GitHub Secrets 提取各种信息并配置为环境变量,以供 shell 脚本使用。这种方式确保敏感信息的安全和保密。

注意 更多信息可参考 GitHub 文档 http://mng.bz/wjd5 关于如何在工作流中设置环境变量。

8.9.8 使用 GitHub 上下文变量

在清单 8.7 中,VERSION 环境变量的设置并非来源于 Secrets,而是使用了所谓的上下文变量:

VERSION: ${{ github.sha }}

这里的 github.sha 代表触发工作流的提交哈希,提供了一种简单有效的方式来自动生成版本号,避免了手动递增的需求。

使用上下文变量可以获取关于工作流、环境、代码库等的详细信息,这些信息可以作为环境变量输入到 shell 脚本中。

注意 有关上下文变量的更多信息,请访问 GitHub 文档 http://mng.bz/qj8x

8.9.9 设置 GitHub Secrets

最后一步是配置所需的 GitHub Secrets。如果你在配置完成前就触发了工作流,可能会导致失败。

我们可以向组织添加 Secrets(适用于多个微服务项目),或者仅添加到特定的仓库。向 GitHub 仓库的“设置”标签中添加 Secrets 很简单,操作如图 8.14 所示。首次访问时显示“这个仓库没有 Secrets。”添加后,你将看到已添加的 Secrets 列表。

图 8.14 GitHub Actions 的 Secrets 页面
图 8.14 GitHub Actions 的 Secrets 页面

图 8.14 显示了如何添加一个新的 Secret:

图 8.15 向代码仓库添加新的 GitHub Actions Secrets 以在工作流中使用
图 8.15 向代码仓库添加新的 GitHub Actions Secrets 以在工作流中使用

完成后,点击“添加秘密”按钮保存。一旦保存,就无法再查看这些值了。因此,建议你使用安全的方式记录这些信息,例如使用密码管理器。

注意 如需详细了解 GitHub Secrets,请参阅 GitHub 文档 http://mng.bz/7v7Q

8.9.10 调试你的部署流程

部署流程首次正确执行通常很具挑战性,因此,当你的部署流程首次失败时,请不要感到沮丧。可能是因为遗漏了某些环境变量或认证细节导致部署失败。有时,可能需要经历多次尝试才能成功。在生产环境中进行调试的过程是缓慢的,这也是为什么在本地彻底测试我们的流程并完全理解必须设置的所有环境变量是非常重要的。

调试失败的工作流的第一步是查看 GitHub Actions 工作流的历史并阅读错误消息。在图 8.16 中,你可以看到我在节 8.7 中较早时部署的“Hello world”工作流失败了。是的,连我都会犯错(这或许能给你带来一些安慰,或者至少你会觉得这很有趣;别担心——我在写这个的时候也在笑),我的基本部署流程的第一次尝试就失败了!

错误消息是“权限拒绝”,涉及到名为 index.sh 的 shell 脚本。如果我之前没有多次见过这个错误,这对我来说可能是一个极其令人困惑的问题。解决这个问题需要在 Git 仓库中将 shell 脚本标记为可执行(正如我在节 8.7.5 中提到的)。作为软件开发人员,我们将不断地遇到新的错误消息,因为我们总是在引入新的问题。如果我们不知道错误消息的含义,我们就必须研究它(提示:从 Google 或 ChatGPT 开始),以帮助找出问题所在并学习如何解决它。

图 8.16 GitHub Actions 中失败的工作流输出
图 8.16 GitHub Actions 中失败的工作流输出

解决部署代码中的问题与解决生产代码中的任何问题类似。我们需要问自己一个问题:是什么导致了这个错误?找出答案可能需要一些侦探工作和实验。对于我们无法立即解决的问题,我们是否能在本地重现这个错误?如果代码在本地运行正常,那么我们就需要尝试理解本地环境与生产环境之间的差异。我们是否检查了本地环境变量与工作流中使用的变量?GitHub Secrets 中是否添加了所有必需的值?

对于那些难以解决的问题,能够在本地重现错误为我们提供了寻找原因并解决问题的最佳机会。弄清楚如何在本地计算机上复制生产配置可能很复杂,但这也可以帮助我们解决最棘手的问题。

此外,如果你还不知道,你可以在你的工作流中使用任何 Linux 命令(这取决于你为你的运行程序使用的操作系统,但在我们的示例中我们使用的是 Ubuntu Linux)。这为我们在生产环境中调试工作流提供了广泛的工具集,即使我们无法创建到运行程序的交互式 shell。

例如,尝试在你的工作流中添加 lspwd 命令,以显示运行程序的当前目录和它包含的文件列表。然后,可以使用 cd 更改目录,并使用 cat 打印文件的内容。即使你的工作流运行正常,也值得在工作流中试验性地使用这些命令。这可以帮助你理解正在发生的事情,并认识到这些命令是调试工作流的有价值技术。调试本身就是一门艺术,很难在我们拥有的空间内做到公正,但我们将在第 11 章再次探讨调试。

在本地运行工作流

遇到在 GitHub Actions 下运行复杂工作流的问题吗?

act 是一个允许你在本地运行工作流的工具。当你的工作流需要在 Linux 下运行时(就像这本书中的示例一样),它工作得最好。

你可以按文件名触发特定的工作流,如下所示:

cd my-repo
act -W .github/workflows/my-workflow.yml

在这里了解更多: https://github.com/nektos/act

8.9.11 直接部署到生产环境的风险

请注意,直接从代码的主分支部署到生产环境是非常危险的。主分支通常是集成发生的地方,开发团队的代码变更在这里合并,并且在向最终用户推出之前,这些变更需要进行彻底的测试。

因此,我们不应该直接从主分支部署,但为了本章的简单性,我们确实这样做了。然而,在第 12 章,我们将改变这种做法。我们将讨论使用分支策略,其中主分支触发我们的 CI 管道(自动测试合并到其中的代码),并有一个单独的生产分支(可能启用了分支保护——我们稍后将讨论这一点)触发我们的 CD 管道。保持这些分支的独立性为我们提供了一个门槛,可以在将代码合并到生产环境并发布给用户之前,手动测试和验证我们的开发中代码。这样的策略有助于防止尴尬的错误(甚至恶意更改)进入生产环境。

8.9.12 我们实现了什么?

从我们的“Hello world”工作流开始,我们已经走了很长的路。我们自动化了单个微服务的部署。当我们更改这个微服务的代码时,它会自动进行测试然后部署到生产环境。这可能看起来像魔法,但现在你已经了解了它是如何工作的,它其实并不神秘。

我们介绍了如何为代码配置制作模板的基础,并且你可以进一步采用这种方式,使你的部署流程(例如,在示例 3 中的 scripts 目录中的所有内容)更易于在你未来创建的每个其他微服务中重用。这是一个可以在许多微服务中扩展的方案,我们将在第 12 章中进一步讨论。

8.10 继续学习

一如既往,有很多内容需要学习,我们无法在这里全部涵盖。如果你想深入了解自动化部署,这里有一本完整的书可以参考:

  • 《Grokking Continuous Delivery》作者:Christie Wilson(Manning,2022 年)

我还非常推荐阅读 GitHub Actions 的文档,特别是“Quickstart”和“Understanding GitHub Actions”的页面: https://docs.github.com/en/actions

总结

  • GitHub Actions 是 GitHub 提供的一项服务,我们可以使用它来响应代码库中的各种动作运行自动化工作流。
  • 持续集成(CI)管道在我们向代码库推送变更时自动运行针对我们代码的自动化测试(和其他检查)。它就像是一个警告系统,用于在开发团队合并代码变更时发现代码库中出现的问题。
  • 持续部署(CD)管道在我们推送代码或提交拉取请求时自动将我们的微服务部署到生产环境。
  • 自动化部署是将我们的代码传送给客户的通道,使发布代码变更并快速获得反馈变得容易。创建 CI 或 CD 管道并不比创建一个 YAML 文件(配置我们的管道)和一个 shell 脚本(运行命令,尽管其中的命令可能相当复杂)复杂得多。
  • 成功实现微服务非常依赖于可靠的自动化部署。如果你的部署是手动的或不可靠的,那么扩展微服务将变得越来越困难。
  • 使用 shell 脚本封装我们的部署过程使其便于在本地测试以确保正常工作,然后再在云中运行,因为在云中调试更加困难。
  • 环境变量用于配置我们在自动化工作流中运行的 shell 脚本。
  • 可以从 GitHub Secrets 设置环境变量,这是一项用于存储秘密和敏感值并保持其安全的服务。
  • 有许多有用的 GitHub 上下文变量,例如我们用来为工作流创建的镜像设置版本号的 github.sha
  • 我们可以使用 envsubst 将环境变量填充到部署配置中,从而对部署配置进行模板化。

文章导航

独立页面

这是书籍中的独立页面。

书籍首页

评论区