跳到内容

a

CI/CD 简介

在这一部分中,你将从练习11.2开始,建立一个强大的部署管道到一个现成的实例项目。你将fork这个例子项目,这将为你创建一个仓库的个人副本。在最后两个练习中,你将为一些你自己的先前创建的应用构建另一个部署管道

这一部分有21个练习,你需要完成每个练习才能完成课程。练习是通过提交系统提交的,就像前几部分一样,但与第0至7部分不同的是,提交到一个不同的 "课程实例"。

这一部分将依赖于课程前几部分所涉及的许多概念。建议你在开始这一部分之前,至少要完成第0至5部分。

与本课程的其他部分不同,你在这部分不会写很多行代码,它更多的是关于配置。调试代码可能很难,但调试配置就更难了,所以在这一部分,你需要大量的耐心和纪律性!

Getting software to production

编写软件是件好事,但没有什么是存在于真空中的。最终,我们需要将软件部署到生产中,也就是说,将它交给真正的用户。在那之后,我们需要维护它,发布新的版本,并与其他人合作来扩展该软件。

我们已经使用GitHub来存储我们的源代码,但当我们在一个有更多开发者的团队中工作时,会发生什么?

当几个开发人员参与时,可能会出现许多问题。该软件可能在我的电脑上工作得很好,但也许其他一些开发者使用的是不同的操作系统或不同的库版本。一个代码在一个开发者的机器上工作得很好,但另一个开发者甚至无法启动它,这种情况并不罕见。这通常被称为 "在我的机器上工作 "的问题。

也有一些更复杂的问题。如果两个开发者都在做修改,而他们还没有决定如何部署到生产中,那么谁的修改会被部署?怎样才能防止一个开发者的修改覆盖另一个开发者的修改?

在这一部分中,我们将讨论如何共同工作,并以严格定义的方式构建和部署软件,以便清楚地知道在任何特定情况下会发生什么。

Some useful terms

在这部分中,我们将使用一些你可能不熟悉的术语,或者你可能没有很好的理解。我们将在这里讨论其中一些术语。即使你熟悉这些术语,也要读一读这部分,这样当我们在这部分使用这些术语时,我们就会在同一页上。

Branches

Git允许代码的多个副本、流或版本共存而不互相覆盖。当你第一次创建一个仓库时,你会看到主分支(通常在git中,我们称之为mastermain,但这在老项目中确实有所不同)。如果一个项目只有一个开发者,而且这个开发者每次只做一个功能,那么这就很好。

当这种环境变得更加复杂时,分支就很有用。在这种情况下,每个开发人员可以有一个或多个分支。每个分支实际上是主分支的一个副本,其中的一些变化使其与主分支相背离。一旦分支中的功能或变化准备就绪,就可以合并回到主分支中,有效地使该功能或变化成为主软件的一部分。通过这种方式,每个开发者都可以在自己的修改集上工作,在修改准备好之前不影响其他开发者。

但是,一旦一个开发者将他们的修改合并到主分支,其他开发者的分支会发生什么?他们现在正从主分支的一个较早的副本中分化出来。后面那个分支的开发者如何知道他们的修改是否与主分支的当前状态兼容?这就是我们在这一部分要回答的基本问题之一。

你可以从这里阅读更多关于分支的信息。

Pull request

在GitHub中,将一个分支合并到软件的主干分支,通常是通过一个叫做pull request的机制实现的,在这个机制中,做了一些修改的开发者要求将这些修改合并到主干分支。一旦提出了拉动请求,也就是通常所说的PR,或者打开了,另一个开发者就会检查是否一切正常,然后合并PR。

如果你对本课程的材料提出了修改意见,你就已经提出了一个拉动请求!

Build

术语 "构建 "在不同语言中有不同的含义。在一些解释型语言中,如Python或Ruby,实际上根本就不需要构建步骤。

一般来说,当我们谈论构建时,我们指的是准备软件在它要运行的平台上运行。这可能意味着,例如,如果你用TypeScript编写了你的应用,而你打算在Node上运行它,那么构建步骤可能是将TypeScript转译成JavaScript。

在C和Rust这样的编译语言中,这个步骤要复杂得多(而且需要),因为代码需要被编译成可执行文件。

第7章节中,我们看了一下webpack,它是目前构建React或任何其他前端JavaScript或TypeScript代码库的生产版本的事实工具。

Deploy

部署指的是把软件放在最终用户需要使用的地方。在库的情况下,这可能只是意味着将一个npm包推送到一个包存档(如npmjs.com),其他用户可以找到它并将其包含在他们的软件中。

部署一个服务(如网络应用)的复杂程度可能不同。在第三章节中,我们的部署工作流程涉及手动运行一些脚本,并将版本库代码推送到Heroku托管服务。

在这一部分中,我们将开发一个简单的 "部署管道",将你每次提交的代码自动部署到Heroku,如果提交的代码没有破坏任何东西。

部署可以明显地更复杂,特别是如果我们增加了诸如 "软件在部署期间必须一直可用"(零停机部署)的要求,或者如果我们必须考虑到诸如数据库迁移的事情。在这一部分中,我们不会涉及像那些复杂的部署,但知道它们的存在是很重要的。

What is CI?

CI(持续集成)的严格定义和该术语在业界的使用方式有很大不同。一个有影响力但相当早的(2006年)关于这个话题的讨论是在Martin Fowler's blog

严格来说,CI指的是经常将开发人员的修改合并到主分支中,维基百科甚至帮助性地建议。维基百科甚至建议:"每天数次"。这通常是正确的,但是当我们在行业中提到CI时,我们通常在谈论实际合并发生后的情况。

我们可能想做其中的一些步骤。

  • 提示:保持我们的代码清洁和可维护。

  • 构建:将我们所有的代码整合成软件

  • 测试:以确保我们不会破坏现有的功能

    -打包。把它全部放在一个容易移动的批次中

  • 上传/部署。将它提供给全世界

我们将在后面更详细地讨论这些步骤中的每一个(以及它们何时适合)。要记住的是,这个过程应该被严格定义。

通常,严格的定义是对创造力/开发速度的一种限制。然而,对于CI来说,这通常不应该是真的。这种严格性应该以允许更容易开发和合作的方式来设置。使用一个好的CI系统(比如我们将在这部分介绍的GitHub Actions)将使我们能够自动地完成这些工作。

Packaging and Deployment as a part of CI

可能值得注意的是,打包和特别是部署有时不被认为是属于CI的范畴。我们将它们添加到这里,因为在现实世界中,将它们放在一起是有意义的。部分原因是它们在流程和管道的背景下是有意义的(我想把我的代码交给用户),部分原因是这些实际上是最有可能发生故障的点。

包装往往是CI中出现问题的一个领域,因为这不是通常在本地测试的东西。在CI工作流程中测试项目的包装是有意义的,即使我们不对产生的包做任何事情。在某些工作流程中,我们甚至可以测试已经建立的包。这就保证了我们已经测试了与将被部署到生产中的代码相同的形式。

那部署呢?我们将在接下来的章节中详细讨论一致性和可重复性,但我们在这里要提到的是,无论我们是在开发分支还是在主分支上运行测试,我们都希望有一个看起来相同的过程。事实上,这个过程可能实际上是一样的,只是在最后进行检查,以确定我们是否在主分支上,并需要进行部署。在这种情况下,将部署包括在 CI 过程中是有意义的,因为我们在进行 CI 工作的同时,也在维护它。

Is this CD thing related?

术语连续交付连续部署(两者的首字母缩写都是CD)经常被用于谈论也负责部署的CI。我们不会用确切的定义来烦扰你(你可以使用例如Wikipedia另一篇Martin Fowler的博文),但一般来说,我们把CD称为主分支始终保持可部署的做法。一般来说,这也经常与由合并到主分支所引发的自动部署结合起来。

CI和CD之间的模糊区域如何处理?例如,如果我们在任何新代码被合并到主干分支之前必须运行测试,那么这是CI,因为我们经常合并到主干分支,还是CD,因为我们要确保主干分支总是可以部署的?

所以,有些概念经常跨越CI和CD之间的界限,正如我们上面所讨论的,部署有时是有意义的,可以将CD视为CI的一部分。这就是为什么你会经常看到用CI/CD来描述整个过程。在这一部分,我们将交替使用 "CI "和 "CI/CD "这两个术语。

Why is it important?

上面我们谈到了 "在我的机器上工作 "的问题和多个变化的部署,但其他问题呢?如果 Alice 直接提交到主分支怎么办?如果Bob使用了一个分支,但在合并前没有费心去运行测试,那该怎么办?如果Charlie试图为生产构建软件,但用了错误的参数,怎么办?

通过使用持续集成和系统化的工作方式,我们可以避免这些。

  • 我们可以不允许直接提交到主分支上

  • 我们可以让CI流程在所有针对主分支的拉动请求(PR)上运行,只有在满足我们所需的条件时才允许合并,如测试通过。

  • 我们可以在CI系统的已知环境中为生产构建我们的包。

扩展这个设置还有其他好处。

  • 如果我们在每次合并到主分支时都使用CD与部署,那么我们就知道它在生产中总是有效的。

  • 如果我们只允许在该分支与主分支保持一致的时候进行合并,那么我们就可以确保不同的开发者不会互相覆盖对方的修改。

注意,在这部分中,我们假设主分支(名为mastermain)包含了正在生产中运行的代码。人们可以使用git的许多不同的工作流程,例如,在某些情况下,可能是一个特定的release分支包含了正在生产中运行的代码。

Important principles

重要的是要记住,CI/CD不是目标。目标是更好、更快的软件开发,减少可预防的错误和更好的团队合作。

为此,CI应该始终根据手头的任务和项目本身进行配置。最终目标应始终铭记在心。你可以把CI看成是这些问题的答案。

  • 如何确保在所有将要部署的代码上运行测试?

  • 如何确保主分支在任何时候都是可部署的?

  • 如何确保构建是一致的,并且总是在它要部署的平台上工作?

  • 如何确保这些变化不会相互覆盖?

  • 如何在点击按钮时进行部署,或者在一个分支合并到主分支时自动部署?

甚至有科学证据表明CI/CD的使用有很多好处。根据《加速》一书中报告的一项大型研究。,CI/CD的使用与组织的成功有很大关系(例如,提高利润率和产品质量,增加市场份额,缩短上市时间)。CI/CD甚至通过减少开发人员的倦怠率而使他们更快乐。书中总结的结果在科学文章中也有报道,如这个

Documented behavior

有一个老笑话说,错误只是一个 "未记录的功能"。我们想避免这种情况。我们希望避免任何我们不知道确切结果的情况。例如,如果我们依靠PR上的标签来定义某个东西是 "主要"、"次要 "还是 "补丁 "版本(我们将在后面介绍这些术语的含义),那么我们必须知道,如果开发者忘记在他们的PR上贴标签会发生什么。如果他们在构建/测试过程开始后才贴上标签呢?如果开发者在中途改变了标签会怎样,哪一个才是真正的发布?

你有可能覆盖所有你能想到的情况,但仍然有差距,即开发者会做一些你没有想到的 "创造性 "的事情,所以在这种情况下,让程序安全失败是很重要的。

例如,如果我们有上面提到的情况,标签在构建过程中发生变化。如果我们事先没有想到这一点,那么如果发生了我们没有预料到的事情,最好是让构建失败并提醒用户。另一种情况是,我们还是部署了错误的版本,这可能会导致更大的问题,所以失败并通知开发者是解决这种情况的最安全的方法。

Know the same thing happens every time

我们可能对我们的软件进行了可以想象的最好的测试,这些测试可以抓住每一个可能的问题。这很好,但如果我们不在代码部署前运行它们,它们就没有用。

我们需要保证这些测试能够运行,并且我们需要确保它们能够针对实际部署的代码运行。例如,如果测试只针对Alice的分支运行,而在合并到主分支后会失败,那就没有用。我们要从主干分支部署,所以我们需要确保测试是针对主干分支的副本进行的,并将 Alice 的修改合并进来。

这给我们带来了一个关键的概念。我们需要确保每次都发生同样的事情。或者说,所需的任务都是按照正确的顺序进行的。

Code always kept deployable

拥有始终可部署的代码会使生活更轻松。当主分支包含在生产环境中运行的代码时,这一点尤其正确。例如,如果发现了一个需要修复的错误,你可以从主分支拉出一份副本(知道它是在生产环境中运行的代码),修复这个错误,并向主分支提出拉取请求。这是很直接的做法。

另一方面,如果主干分支和生产分支差别很大,而且主干分支不能部署,那么你就必须找出在生产中运行的代码,拉出一份副本,修复错误,想办法推回,然后再想办法部署那个特定的提交。这不是很好,而且必须是一个与正常部署完全不同的工作流程。

Knowing what code is deployed (sha sum/version)

知道在生产中实际运行的是什么往往很重要。理想情况下,正如我们上面所讨论的,我们会在生产中运行主分支。这并不总是可行的。有时我们打算让主分支在生产中运行,但构建失败了,有时我们把几个变化批在一起,想让它们一次全部部署。

在这些情况下,我们所需要的(一般来说也是个好主意)是确切地知道哪些代码在生产中运行。有时这可以用版本号来完成,有时将提交的SHA和(git中特定提交的唯一标识哈希值)附在代码上也很有用。我们将进一步讨论版本问题在本章节稍后

如果我们把版本信息和所有版本的历史结合起来,就更有用了。例如,如果我们发现某个特定的提交引入了一个bug,我们就可以准确地找出这个bug是什么时候发布的,有多少用户受到影响。当这个bug在数据库中写入坏数据时,这就特别有用。我们现在就可以根据时间来追踪这些坏数据的去向。

Types of CI setup

为了满足上面列出的一些要求,我们想用一个单独的服务器来运行持续集成的任务。有一个单独的服务器用于此目的,可以最大限度地减少其他东西干扰CI/CD过程并导致其不可预测的风险。

有两种选择:托管我们自己的服务器或使用云服务。

Jenkins (and other self-hosted setups)

在自我托管的选项中,Jenkins是最受欢迎的。它非常灵活,而且

有几乎任何东西的插件(除了你想做的那件事情)。这对许多应用来说是一个很好的选择,使用自我托管的设置意味着整个环境在你的控制之下,资源的数量可以被控制,秘密(我们将在这一部分的后面部分详细说明安全问题)永远不会暴露给其他人,你可以在硬件上做任何你想做的事情。

不幸的是,也有一个坏处。Jenkins的设置相当复杂。它非常灵活,但这意味着通常需要相当多的模板/模版代码来实现构建工作。具体到Jenkins,这也意味着CI/CD必须用Jenkins自己的特定领域语言进行设置。还有硬件故障的风险,如果设置被大量使用,这可能是一个问题。

对于自我托管的选项,计费通常是基于硬件的。你为服务器付费。你在服务器上做什么并不改变计费。

GitHub Actions and other cloud-based solutions

在云托管的设置中,环境的设置不是你需要担心的事情。它就在那里,你所要做的就是告诉它该做什么。这样做通常包括在你的版本库中放置一个文件,然后告诉CI系统读取该文件(或者检查你的版本库中的特定文件)。

基于云的选项的实际CI配置通常要简单一些,至少如果你保持在被认为是 "正常 "的使用范围内。如果你想做一些更特别的事情,那么基于云的选项可能会变得更加有限,或者你会发现很难完成云平台不适合的特定任务。

在这一部分,我们将看看一个相当正常的使用案例。更复杂的设置可能,例如,利用特定的硬件资源,例如,GPU。

除了上面提到的配置问题外,基于云的平台上通常有资源限制。在自我托管的设置中,如果构建速度慢,你可以得到一个更大的服务器,并在它身上投入更多资源。在基于云的选项中,这可能是不可能的。例如,在GitHub Actions中,你的构建将运行在2个vCPUs和8GB内存的节点上。

基于云的选项通常也是按构建时间计费的,这是需要考虑的问题。

Why pick one over the other

一般来说,如果你有一个中小型的软件项目,没有任何特殊的要求(例如需要一个图形卡来运行测试),基于云的解决方案可能是最好的。配置很简单,你不需要为建立你自己的系统而费心费力。特别是对于较小的项目,这应该是比较便宜的。

对于需要更多资源的大型项目,或者在有多个团队和项目需要利用它的大公司,自我托管的CI设置可能是最好的方式。

Why use GitHub Actions for this course

对于本课程,我们将使用GitHub Actions。这是一个明显的选择,因为我们无论如何都要使用GitHub。我们可以立即得到一个强大的CI解决方案,而不需要设置服务器或配置第三方的云服务。

除了易于使用,GitHub Actions在其他方面也是一个不错的选择。它可能是目前最好的基于云的解决方案。自2019年11月首次发布以来,它已经获得了很多人气。