Gitlab-CICD-流水线的自动化-DevOps-全-

Gitlab CICD 流水线的自动化 DevOps(全)

原文:annas-archive.org/md5/cbd71031099b6c72f4f0b5b172eb1be9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

近年来,组织已经认识到更具协作性和迭代性的开发方法的好处。传统模式下,开发和运维团队各自为政,导致公司难以快速、可靠地交付新功能。DevOps 的推广解决了这一挑战,通过促进开发与运维之间的集成和沟通。而工具和自动化工作流的广泛应用,进一步提高了软件质量和稳定性。

DevOps 的一个关键要素是持续集成与持续交付(或部署)(CI/CD)。CI/CD 是将开发人员的贡献定期集成到共享代码库中,并自动构建、测试和发布应用程序的实践。其目标是最小化部署软件更新所需的周期时间,同时减少过程中的人为错误。

本书介绍并教授如何使用 GitLab 作为一个统一的 DevOps 平台来管理软件开发生命周期的各个阶段。内容主要聚焦于概念和示例,而不拘泥于可能随着时间变化的功能特性和 UI 流程。在完成本书后,您将能够使用 GitLab 管理几乎任何软件项目,同时了解适用于其他 DevOps 工作流和 CI/CD 工具的最佳实践。

本书适合的人群

本书面向所有参与软件开发生命周期的人,无论项目大小。如果您觉得这个范围很广,那是因为它确实很广!GitLab 提供的功能适用于各种角色的人员。传统的 GitLab 用户包括开发人员、QA、安保测试人员、性能测试人员、产品负责人、项目经理、UX 设计师、技术写作人员、发布工程师,以及那些属于模糊概念“DevOps”和“DevSecOps”范畴的各种角色。所以,如果您参与了软件的规划、编写、测试、安全、构建、打包或部署,或者管理任何相关任务,并且正在考虑如何自动化您当前手动执行的缓慢且容易出错的任务,那么本书几乎肯定能为您提供使用 GitLab 和其自动化 CI/CD 管道来改善工作的思路。

本书假设读者具备一定的软件开发生命周期主要阶段的基础知识。我们预计每位读者会参与到生命周期的不同部分,因此本书将重点介绍与读者相关的内容。这是一种很好的学习方式,尽管我们建议每个读者都先阅读构成第一部分的四章内容,因为它们解释了所有 GitLab 用户都需要了解的背景概念和术语。

本书内容

第一章**,理解 DevOps 之前的生活,简要回顾了主要的软件开发生命周期阶段,以及在手动执行时可能遇到的问题。

第二章**,实践基本的 Git 命令,介绍了 Git,这一强大的版本控制系统,GitLab 正是基于它构建的。

第三章**,理解 GitLab 组件,介绍了每个 GitLab 用户都需要熟悉的 GitLab 主要组件。

第四章**,理解 GitLab CI/CD 管道结构,解释了 GitLab CI/CD 管道的目的和结构:它们如何工作,如何配置它们,以及如何查看其结果。

第五章**,安装和配置 GitLab Runner,探讨了支持 GitLab CI/CD 管道的关键工具。

第六章**,验证你的代码,介绍了 GitLab CI/CD 管道功能,专门用于确保你的代码质量高且功能正确。

第七章**,保护你的代码,讨论了 GitLab CI/CD 管道扫描器,这些工具能识别你代码中的安全漏洞。

第八章**,打包和部署你的代码,解释了如何使用 GitLab CI/CD 管道自动化使用常见的构建和打包工具,使你的代码成为可部署的形式。

第九章**,提高 CI/CD 管道的速度和可维护性,介绍了一些加速 GitLab CI/CD 管道并使其易于阅读和维护的技巧。

第十章**,扩展 CI/CD 管道的应用范围,解释了如何使用 GitLab CI/CD 管道发现代码中的性能问题,如何在 GitLab 仪表板中启用或禁用产品功能,以及如何使用 GitLab 开发移动应用程序。

第十一章**,端到端示例,演示了一个端到端的示例,将你学到的许多 GitLab 技术整合成一个单一、现实的软件开发工作流。

第十二章**,排除故障及 GitLab 的未来发展,提供了一些 GitLab CI/CD 管道故障排除的技巧,同时也分享了使用 GitOps 来管理基础设施的思路,以及 GitLab 未来可能的发展方向。

为了最大化地利用本书

如果您在gitlab.com(即软件即服务实例)或自托管实例上有 GitLab 账户,您将能从本书中获得最大收益。若能访问已安装 Git 的 Linux、macOS 或 Windows 终端,也将非常有帮助。对软件开发生命周期的主要阶段有所了解会更有助于理解本书内容。书中不假设您具有编程知识,但对 YML 或其他结构化数据格式的基本了解将使书中的许多部分更易于理解。

本书中的代码示例是使用 GitLab 15.x 版本测试的,应该也适用于未来的版本。所有截图也来自 GitLab 15.x 版本。未来的版本可能会在 GUI 上产生一些差异,但基本概念和操作应该保持不变。

书中涵盖的 软件/硬件 操作系统 要求
GitLab 15+ Linux、macOS 或 Windows
Git Linux、macOS 或 Windows

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块如下所示:

employee_name = get_user_input() 
sql = "SELECT salary FROM employee_records WHERE employee_name = $employee_name" ENTERcall_database(sql) 

当我们希望引起您对代码块中特定部分的注意时,相关的行或项目会用粗体显示:

deploy-to-staging: 
stage: staging 
script: ./deploy-staging.sh 
tags: 
- windows 
- staging 

任何命令行输入或输出如下所示:

$ git --version 
git version 2.25.1

粗体:表示新术语、重要单词或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇通常以粗体显示。举个例子:“从管理面板中选择系统信息。”

提示或重要说明

以这种方式出现。

联系我们

我们总是欢迎读者的反馈。

常见反馈:如果您对本书的任何内容有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。

勘误表:尽管我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/errata并填写表单。

盗版:如果您在互联网上发现我们作品的任何非法复制品,我们将非常感激您提供相关的地址或网站名称。请通过版权@packt.com 与我们联系,并附上该材料的链接。

如果您有意成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与书籍编写,请访问 authors.packtpub.com。

分享您的想法

阅读完《使用 GitLab CI/CD 管道自动化 DevOps》后,我们很想听听您的想法!请点击此处直接前往 Amazon 评论页面并分享您的反馈。

您的评价对我们以及技术社区非常重要,将帮助我们确保提供优质的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但无法随身携带印刷书籍吗?您的电子书购买是否无法与您选择的设备兼容?

不用担心,现在每本 Packt 书籍都能免费获得无 DRM 的 PDF 版本。

随时随地在任何设备上阅读。可以从您最喜欢的技术书籍中直接搜索、复制并粘贴代码到您的应用程序中。

特权并不止于此,您还可以获得独家折扣、新闻简报以及每天发送到您邮箱的精彩免费内容

按照这些简单的步骤来获取好处:

  1. 扫描二维码或访问下面的链接

packt.link/free-ebook/9781803233000

  1. 提交您的购买证明

  2. 就是这样!我们会将您的免费 PDF 和其他好处直接发送到您的邮箱。

第一部分 开始使用 DevOps、Git 和 GitLab

在本书的这一部分,您将了解在 GitLab 出现之前,软件开发生命周期为何如此缓慢且容易出错,这将帮助您理解 GitLab 所解决的问题。您还将了解 Git 版本控制系统的基础知识,并将接触到 GitLab 的基本概念和组件。最后,您将首次接触 GitLab CI/CD 管道,它将是本书其余部分的主要内容。

本部分包括以下章节:

  • 第一章理解 DevOps 之前的生活

  • 第二章练习基本的 Git 命令

  • 第三章理解 GitLab 组件

  • 第四章理解 GitLab CI/CD 管道结构

第一章:理解 DevOps 之前的生活

为了理解 GitLab CI/CD 管道DevOps 方法的强大功能,我们必须了解在 GitLab 等工具出现之前,软件是如何构建的。尽管在本章你不会学到任何实际操作,但你将了解 GitLab CI/CD 管道所起源的世界,并清楚地看到它们解决了哪些问题。理解这些内容将帮助你理解 GitLab CI/CD 管道为何如此运作,并使你对它们在软件开发生命周期中带来的强大力量大开眼界。简而言之,理解现在的工作方式,最好的方法就是理解过去的艰难!

本章将介绍一个虚构但真实的网络应用程序,名为Hats for Cats+,它销售——你猜对了——猫咪的头饰。你将快速了解如何将“猫咪帽子”从一个创意转变为一个写得好、经过测试并且部署的网络应用程序。你将看到,在没有 GitLab CI/CD 管道的世界里,这些任务是如何完成的,这样当你在后续章节中学习 GitLab 时,其优势将更加明显。

本章将涵盖以下主要主题:

  • 介绍“猫咪帽子”网络应用

  • 手动构建和验证代码

  • 手动进行安全测试代码

  • 手动打包和部署代码

  • 手动软件开发生命周期实践的问题

  • 通过 DevOps 解决问题

介绍“猫咪帽子”网络应用

“猫咪帽子”是一个虚拟的网络应用,用于销售棒球帽、牛仔帽和礼帽,专为你喜爱的毛茸茸的小伙伴们设计。假设它是一个标准的在线商店,就像你使用过的成百上千个商店一样。它允许人们浏览帽子目录,加入购物车并输入账单和配送信息。

本书不涉及“猫咪帽子”的用户体验或图形设计。它所基于的网络应用框架也不重要,甚至它所使用的编程语言也不重要。我再说一遍,因为这是一个重要但可能让你惊讶的点:本书不依赖特定语言。它将包括多种编程语言的示例,以增加你至少能理解某些示例的可能性。但无论你的应用程序——或者是“猫咪帽子”应用——是用 Java、JavaScript、Python、Ruby 或任何其他语言编写的,都不重要。本书所描述的 GitLab CI/CD 原则适用于所有情况。

重要的是你需要采取的一般步骤,以确保代码具有高质量、按预期行为运行、安全、性能足够、打包合理,并在正确的时间部署到合适的环境中。本书的重点是 GitLab CI/CD 管道如何使软件开发生命周期(SDLC)中的各个步骤变得更轻松、更快速且更可靠。它不会教你如何编写“Hats for Cats” web 应用程序。假设所有编码工作都在后台完成,之后你将学习如何构建、验证、安全性、打包和发布这些代码。

牢记这一点,让我们一步步走过你需要遵循的高层次步骤,以便在 GitLab 出现之前将你的代码准备好供用户使用。这些都是 GitLab CI/CD 管道能够自动为你完成的手动等价过程。但理解手动过程的局限性,以及执行这些过程所涉及的痛苦和繁琐,将帮助你理解 GitLab 的真正强大之处。

手动构建和验证代码

在 GitLab CI/CD 管道出现之前,你需要手动构建和验证代码。这通常是一次糟糕的、让人心力交瘁的体验,原因我们将在这里讨论。

手动构建代码

构建代码取决于你使用的语言。如果你使用的是像 Python 或 Ruby 这样的解释型语言,那么可能根本不需要构建。但是,如果你编写的是编译型语言,那么你需要通过编译源代码来构建你的应用程序。

想象一下你正在使用 Java。以下是将 Java 源代码编译成可执行 Java 类的几种不同方式:

  • 你可以使用随 Java 开发工具包一起提供的 javac Java 编译器

  • 你可以使用 Maven 构建工具

  • 你可以使用 Gradle 构建工具

有很多原因说明这个手动构建过程是繁琐且令人烦恼的,大多数开发人员都希望抛弃它:

  • 它容易出错:你有多少次忘记了是否需要将 javac 指向你类所在的顶级包,或者指向单独的类文件?

  • 它很慢,可能需要几秒钟到几分钟,具体取决于你的应用程序有多大。这可能会导致大量的停机时间。

  • 它很容易被忘记,导致混乱,当你不小心执行了行为与预期不同的旧代码时。

  • 编写不当的代码可能无法编译,导致每个人浪费时间,构建工程师将代码返回给开发人员进行修复,并等待修复的到来。

手动验证代码

一旦你构建好了代码,你需要验证它是否正确运行。测试有无数种形式,且测试的种类比我们在本书中能描述的还要多。但这里是一些你可能希望对代码进行的最常见的测试形式:

图 1.1 – 验证代码的测试

图 1.1 – 验证代码的测试

功能测试

你的程序做的事情是否符合预期?这是功能测试所要回答的问题。大多数编程项目都从一份描述软件应如何行为的规格说明开始:给定某个输入,应该提供什么输出?开发人员只有在他们编写的代码符合这些规格时,才算完成工作。如何知道代码是否符合规格?这就是功能测试的作用所在。

就像一般有很多种测试形式一样,也有许多子类别的测试,它们共同构成了功能测试。

如果将2 + 2输入到计算器,它应该返回4!顺畅路径测试看起来像是最重要的测试类型,因为它们检查用户在使用软件时最有可能遇到的行为。但实际上,你通常可以通过少数几个顺畅路径测试来覆盖最常见的用例。那些涵盖不寻常或意外情况的测试通常要多得多。

说到不寻常的情况,56 ÷ 209(这些值处于计算器可以接受的值范围的中间位置)比输入0 + 0999,999 – 999,999(因为这些值位于范围的边界)更具挑战性。边界测试确保在接受范围的最远端的输入值不会导致软件崩溃。你能创建一个由单个字母组成的用户名吗?你能订购 9,999 本书吗?你能向银行账户存入 1 美分吗?如果规格说明说你的软件应该能处理这些边界情况,那你最好确保它真的能!

如果边界测试确保你的软件能够处理接近接受范围边缘的输入值,那么极限测试则验证软件是否能够处理两个或更多的同时出现的边界情况。可以把它当作加速版的边界测试,挑战你的软件,把它置于更加不舒服(但仍然有效)的情境中。例如,你的银行应用程序是否允许你在最远的有效日期安排提款,且金额为最小有效货币金额?极限测试不仅限于两个输入值:如果你的软件一次接受三个、十个或一百个输入值,你就需要确保它在每个输入值都被推到有效范围的极端值时仍然能正常工作。

这处理了软件接收有效值的情况。但你是否也需要确保它在接收无效值时能正确行为?当然需要!这种测试形式有时被称为负 6美元。当你查询 2020 年 2 月 31 日的汇率时,你的货币转换软件应该给出一个合理的错误消息。

由于通常有更多方式将错误数据输入应用程序而非正确数据,开发人员通常专注于正确处理预期数据,但未能考虑用户可能输入的意外、格式错误或超出范围的数据类型。程序需要预测并优雅地处理所有类型的数据——无论是好数据还是坏数据。编写完整的“顺利路径”和“不顺利路径”测试集是确保开发人员编写的代码无论用户输入什么数据都能表现良好的最佳方法。

这些是一些涉及有效数据和无效数据的行为类型,测试可以检查这些内容。但是,还有另一个维度可以用来分类测试:测试所针对的代码块的大小

在大多数情况下,测试可以检查的最小代码片段是一个单独的方法或函数。例如,您可能想要测试一个名为alphabetize的函数,该函数接受任意数量的字符串作为输入,并返回这些相同的字符串,按字母顺序排列。为了测试这个函数,您可能会使用一种叫做单元测试的测试方法。它测试代码的单个单元,在这个情况下,一个单元就是一个函数。您可以有多个单元测试集合,每个测试以不同的方式覆盖这个函数:

  • 一些测试可能覆盖顺利路径。例如,它们可以传递dogcatmouse字符串作为输入。

  • 一些测试可能覆盖边缘或极端情况。例如,它们可以将一个空字符串传递给函数,或者传递仅由数字组成的字符串,或已按字母顺序排列的字符串。

  • 一些测试可能覆盖不顺利路径。例如,它们可以传递一个意外的数据类型,如布尔值,而不是预期的数据类型字符串。

要验证较大代码片段的行为,您可以使用集成测试。这些测试不会关注单个函数,而是关注函数组之间如何相互作用。例如,假设您的货币转换应用有四个函数:

  • get_input,该函数从用户那里获取输入,包括源货币、源金额和目标货币。

  • convert,该函数将源货币的金额转换为正确的目标货币金额。

  • print_output,该函数告诉用户转换后产生的目标货币数量。

  • main,这是您的应用的主要入口点。当使用您的应用时,调用的就是这个函数。它会调用另外三个函数,并将每个函数的输出作为输入传递给下一个函数。

为了确保这些功能能够和谐地协同工作——也就是说,检查它们是否良好集成——你需要编写集成测试来调用main,而不是编写调用get_inputconvertprint_output的单元测试。这让你能够在更高的抽象层次上进行测试,也就是更接近真实用户使用应用程序的方式。毕竟,用户不会单独调用get_input,他们会调用main,而main则会调用其他三个函数并协调它们之间的值传递。编写一个独立工作正常的函数很容易,但让多个函数协作构建更大的逻辑则更为困难。集成测试能够发现这种问题,而纯粹的单元测试则无法做到。

测试人员常常将各种测试形式看作一个金字塔。根据这一模型,单元测试占据了金字塔的宽大低端:它们是低级的,因为它们测试的是代码的基本单元,而且数量众多。集成测试则位于金字塔的中间:它们的抽象层次高于单元测试,且数量较少。金字塔的顶部是第三类测试,我们接下来将讨论的——用户测试

图 1.2 – 测试金字塔

图 1.2 – 测试金字塔

最后一种测试类型是用户测试,它模拟用户的行为,并以用户的方式使用软件。例如,如果用户通过输入源货币、该源货币的金额和目标货币,然后期望看到目标货币的输出金额,那么这正是用户测试所做的事情。这可能意味着它使用应用程序的图形用户界面(GUI),通过点击按钮和在字段中输入值来操作。或者,它可能通过调用应用程序的 REST API 接口,传入输入值并检查输出结果。然而,它与应用程序进行交互,并且尽可能以最接近真实用户的方式进行。与单元测试和集成测试一样,用户测试可以包括常规路径测试、边缘和极限情况测试,以及异常路径测试,以覆盖软件规格所描述的所有场景,以及测试设计人员可能构思的任何其他场景。

到目前为止,我们已经解释了单元测试、集成测试和用户测试的不同目的,但我们还没有描述另一个基本差异。单元测试和集成测试几乎总是自动化的。也就是说,它们是测试其他计算机程序的计算机程序。虽然用户测试在可能的情况下也是自动化的,但是由于编写可靠且可重现的测试与应用程序 GUI 交互的困难足够多,因此许多用户测试必须手动运行。Web 应用程序因为在加载时间周围的行为不可预测、页面渲染不完整、缺失或未完全加载的 CSS 文件以及网络拥塞而出名难以测试。这意味着,虽然软件开发团队通常尝试自动化 Web 应用程序的用户测试,但更常见的情况是他们最终采用自动化与手动用户测试的混合方法。正如您可能已经猜到的那样,手动用户测试在时间和测试人员士气方面成本极高。

性能测试

在对功能测试进行高级导览之后,您可能会认为我们已经涵盖了所有测试基础。但实际上我们才刚刚开始。您可能还需要测试应用程序的另一个方面:其性能。它是否能够快速执行用户期望的功能,以避免用户感到沮丧?它是否符合开发人员在编码之前所得到的性能规格?它的性能是否明显优于或劣于竞争对手的性能?这些是性能测试旨在回答的一些问题。

性能测试因其设计和执行的复杂性而声名狼藉。在评估应用程序运行速度时需要考虑很多变量:

  • 在测试期间应该运行在什么样的环境中?创建与生产环境完全相同的环境通常代价高昂,但在测试环境中可以削减哪些方面而不会严重影响性能测试结果?

  • 您的性能测试应使用哪些输入值?根据应用程序的不同,某些输入值的处理时间可能显著长于其他输入值。

  • 如果您的应用程序是可配置的,应该使用什么配置设置?如果没有大多数用户都使用的标准配置,则尤其重要。

即使您可以弄清如何设计有用的性能测试,这些测试通常需要很长时间才能运行,并且在某些情况下可能会产生不一致的结果。这导致团队经常重新运行性能测试,这会导致测试所需时间更长。因此,性能测试是所有测试类型中最关键且成本较高的测试之一。

负载测试

性能测试有一个密切相关的“亲戚”叫做负载测试。性能测试评估的是软件在执行单一操作时的速度(例如单次货币兑换、单次银行存款或单次数学问题),而负载测试评估的是软件在多用户同时交互时的表现。负载测试面临着与性能测试相似的设计难题,可能会产生类似不一致的结果。它们的设置也更加昂贵,因为需要模拟成百上千的用户。

持续测试

当应用程序运行数小时或数天后,它是否分配了永远不会回收的内存?它是否因为过度记录日志而占用了大量磁盘空间?它是否启动了永远不会关闭的后台进程?如果出现这些资源“泄漏”问题,应用程序可能会因为内存、磁盘空间或 CPU 周期不足而失去性能,甚至崩溃。这些问题可以通过持续测试发现,持续测试是在一段时间内对软件进行持续的测试,同时监控其稳定性和性能。显然,持续测试在时间和硬件资源上非常昂贵,需要大量的投入。

模糊测试

一种被低估但非常强大的测试方法叫做模糊测试。这种方法会向软件发送有效但异常的数据输入,目的是揭露传统功能测试可能遗漏的错误。可以把它想象成醉酒状态下的正常路径测试。所以,不是尝试用“Sam”作为用户名创建账户,而是尝试用包含 1000 个字母的用户名,或者尝试创建一个全为空格的用户名,或者在收货地址中包含克林贡字母表的 Unicode 字符。

模糊测试引入了强大的随机性:它发送到软件的输入值要么是完全随机生成的,要么是已知不会对代码产生问题的输入值的随机排列。例如,如果你的代码将 PDF 文件转换成 HTML 文件,模糊测试可能会从发送稍微调整过的有效 PDF 文件开始,然后逐步要求软件转换完全随机的字符串,这些字符串与 PDF 文件完全没有关系。由于模糊测试需要发送成千上万的随机输入值,直到找到一个导致崩溃或其他错误的输入值,因此模糊测试必须自动化进行。手动执行模糊测试实在是太繁琐了。

静态代码分析

另一种严格自动化的测试形式是静态代码分析。与我们讨论过的其他测试试图在代码运行时找到问题不同,静态代码分析是在不执行代码的情况下检查你的源代码。它可以查找各种不同的问题,但通常它会检查你是否遵循了公认的编码最佳实践和语言习惯。这些规范可能是由你的团队、语言本身的开发者或其他编程权威制定的。

例如,静态代码分析可以注意到你声明了一个变量,但从未给它赋值。或者它可能指出你已经给一个变量赋值,但之后从未引用过该变量。它可以识别无法访问的代码、使用已知比其他功能等效模式更慢的编码模式,或者使用不规范的空白字符方式的代码。这些都是可能不会导致代码完全崩溃,但会使你的代码在可读性、可维护性或速度方面远不如预期的做法。

验证代码的更多挑战

到目前为止,我们只描述了一些你可能希望验证代码行为、性能和质量的方式。但一旦你完成了所有这些不同类型的测试,你将面临一个可能比较棘手的问题,即如何解析、处理和报告结果。如果你幸运的话,你的测试工具会生成标准格式的报告,你可以将其集成到自动更新的仪表板中。但你很可能会发现自己至少需要使用一个无法嵌入你正常报告结构的测试工具或框架,并且需要手动扫描、清理并调整格式,使其易于阅读和传播。

我们已经提到过,特别是性能测试通常需要反复进行。但事实上,所有这些类型的测试都需要反复执行,以捕捉回归或平滑所谓的“闪烁”测试,闪烁测试是指根据网络状况、服务器负载或其他无数不可预测因素,有时通过有时失败的测试。这意味着无论是手动执行测试,还是管理和触发自动化测试,所需的工作量远远大于最初看起来的情况。如果你需要反复执行测试,你需要搞清楚何时以及多频繁地进行测试,你需要确保在合适的时间有适当的硬件或测试环境可用,而且你还需要有足够的灵活性来根据条件的变化或管理层要求更为实时的结果时调整你的测试频率。关键是,测试很难、耗时且容易出错,每次需要人工干预以确保测试在正确的时间和正确的方式进行时,这些困难都会被放大。

尽管我们刚才说过测试应该通常执行并反复执行,但还有另一个相反的力量在起作用。由于执行测试既昂贵又困难,人们往往倾向于尽可能少地运行测试。这种倾向受到一种常见开发模式的鼓励:即开发者完成一个功能(有时是整个产品)后,再将代码扔到墙的另一边交给质量保证QA)团队进行验证。代码构建与测试之间的这种严格分工意味着许多团队只会在开发周期的最后阶段才运行测试——无论是在两周的冲刺结束时、一年项目的结束时,还是介于两者之间的某个时刻。

不频繁或延迟测试的做法会导致一个巨大的问题:当开发者交付一大批代码进行测试——这些代码是由不同的人在几周或几个月的时间里使用不同的编码风格和习惯开发的,可能有成千上万行——那么诊断测试发现的 bug 的根本原因会变得极其困难。反过来,这意味着修复这些 bug 也变得困难。就像大草堆比小草堆更能有效地隐藏针一样,大批量的代码让我们很难发现、理解和修正其中的 bug。开发团队在将代码交给 QA 团队之前等待的时间越长,这个问题就越严重。

这就是我们快速浏览功能测试、负载测试、浸泡测试、模糊测试和静态代码分析的全部内容。此外,我们还解释了进行这些不同种类测试时所涉及的一些隐藏难题。你可能会想知道我们为什么要讨论测试。原因在于,理解测试的挑战——了解验证代码的方式有多少种,测试的不同形式有多么重要,设置测试环境的时间成本有多高,手动进行无法自动化的用户测试有多麻烦,处理和报告测试结果有多复杂,以及在庞大的代码包中发现并修复潜藏的 bug 有多么困难——是理解在 DevOps 出现之前软件开发有多艰难的一个重要部分。在本书的后面,你会看到 GitLab CI/CD 管道如何简化运行各种测试并查看其结果的过程,以及当你理解早期和频繁运行的测试如何使问题更容易被发现和更便宜地修复时,你可以回顾这些繁琐的测试程序,并对那些在 GitLab 出现之前不得不经历这部分 SDLC 的开发者产生同情。GitLab 时代的生活真是好多了!

手动进行安全测试代码

我们提到过功能测试只是测试的一种形式。另一个重要的形式是安全测试。它非常重要且非常难以做到完美,因此通常由与传统 QA 部门分开的专门团队执行。安全测试有许多不同的方法,但大多数都可以归结为以下三类之一:

  • 检查源代码

  • 与运行中的代码互动

  • 检查项目使用的第三方依赖

此外,安全测试可以查找不同种类的问题。乍一看,这些问题中的一些可能看起来不属于安全问题的范畴,但它们都会导致潜在的数据丢失或软件被恶意操控:

  • 非标准编码实践

  • 不安全的编码实践

  • 含有已知漏洞的源代码依赖

让我们看一下安全测试的几种具体类型,看看它们如何使用不同的技术来寻找不同类型的问题。

静态代码分析

你通常可以通过请安全专家审查你的源代码来发现不安全的编码实践。例如,如果你请求用户输入,然后使用该输入查询数据库,聪明的用户可能会通过在输入中加入数据库命令来发起所谓的SQL 注入攻击。称职的代码审查员会立即发现这种问题,并且通常能提出易于实施的解决方案。

例如,以下伪代码接受用户输入,但在将其用于 SQL 语句之前没有验证输入。一个聪明的用户可以输入恶意值,如Smith OR (0 = 0),从而导致暴露出比开发者预期更多的信息:

employee_name = get_user_input()
sql = "SELECT salary FROM employee_records WHERE employee_name = $employee_name" ENTERcall_database(sql)

代码审查还可以识别那些可能看起来不明显不安全的代码,但这些代码使用了非标准的习惯用法、异常的格式或尴尬的程序结构,导致代码更难被其他团队成员(甚至原作者)阅读和维护。这可能间接使代码未来更容易遭受安全问题,或者至少使得未来的安全问题更难被代码审查人员发现。

例如,以下 Python 函数接受一个异常多的参数,然后忽略其中大部分。这两种特征都被认为是糟糕的编程实践,即使它们都不会威胁到代码的行为或安全性:

def sum(i, j, k, l, m, n, o, p, q, r):
    return i + j

静态代码分析有时会自动进行。许多集成开发环境(IDE)将静态代码分析作为内置功能:它们会在检测到任何非标准或不安全的代码时,在其下方绘制红色警告线。这可能是一个很大的帮助,但最好将其视为对手动代码审查的补充,而不是完全的替代品。

秘密检测

你可以将秘密检测视为静态代码分析的一种特殊形式。你希望从软件源代码中排除的敏感数据有很多类型。很容易想到一些例子:

  • 密码

  • 部署密钥

  • 公共 SSH 或 GPG 密钥

  • 美国社会安全号码

  • 其他国家使用的独特个人识别号码

就像静态代码分析扫描源代码以查找编程或安全问题一样,秘密检测扫描源代码以寻找应当删除并存储在更安全位置的秘密。例如,以下 Java 代码包含一个社会安全号码,任何具有读取权限的人都可以看到:

String bethSSN = "555-12-1212";
if (customerSSN.equals(bethSSN))) {
     System.out.println("Welcome, Beth!");
}

动态分析

查看源代码很有用,但有许多类别的软件缺陷通过与执行中的代码进行交互更容易被发现。这种交互可能表现为像人类一样使用应用程序的 GUI、向 REST API 端点发送请求,或在 Web 应用程序的不同 URL 上使用不同的请求查询字符串值。

例如,您的 Web 服务器可能配置为在每个响应的头部中包含其版本号。这看起来可能是无害的信息,但它可能为恶意攻击者提供有关哪些针对 Web 服务器的漏洞可能对您的站点有效的线索,以及您的 Web 服务器可能免疫于哪些漏洞。

举个例子,您代码中的复杂逻辑可能掩盖了您通过输入一组特定的输入值来触发未处理的除零错误的事实。如前所述,这类问题一开始可能不会被认为是安全风险,但聪明的黑客通常能找到利用简单漏洞的方式,从而暴露数据、导致数据丢失或引发拒绝服务攻击。

例如,以下 Ruby 代码在运行时可能会产生一个 ZeroDivisionError 实例,这可能导致程序崩溃:

puts 'how many hats do you have?'
num_hats = gets.to_i
puts 'how many cats do you have?'
num_cats = gets.to_i
puts "you have #{num_hats / num_cats} hats per cat"

依赖扫描

依赖扫描是将您产品的每个依赖项的名称和版本号与已知漏洞数据库进行比较,并识别出哪些依赖项应该升级到更高版本或完全删除,以提高软件安全性的实践。如今几乎每一款非平凡的软件都依赖于数十、数百或数千个第三方开源库。这些最受欢迎的库的源代码被黑帽黑客仔细研究,寻找可能的漏洞。这些漏洞通常会被库的维护者迅速修复,但如果您的项目使用的是旧版、未打补丁的库,依赖扫描将告知您,您的代码可能会受到这些已知漏洞的攻击。

一个完美的例子,说明了这种类型的安全扫描的必要性,正是当时新闻中的话题。许多 Java 项目依赖于一个名为 Log4j 的开源 Java 库,该库提供了一种便捷的方式来记录信息、警告或错误消息。最近发现了一个漏洞,允许黑客远程执行命令或在任何运行 Log4j 的计算机上安装恶意软件。这是一个巨大的问题!幸运的是,这正是依赖扫描能够发现的问题。任何更新的依赖扫描工具都会让你知道你的软件是否依赖于未修补版本的 Log4j,无论是直接依赖还是通过其他依赖关系,并会建议你升级到哪个版本的 Log4j。

容器扫描

如今,许多软件产品以Docker镜像的形式交付。最简单的描述就是,Docker 镜像是一个已经安装了你的应用程序的 Linux 发行版,并将其打包成一种可以由 Docker 或类似工具执行的镜像格式。如果你构建的 Docker 镜像包含了一个过时的 Linux 发行版,并且该发行版存在安全漏洞,那么你的应用程序将不如它应该有的那样安全。

容器扫描检查你的Docker 化应用程序所安装的基础 Linux 镜像,并检查已知安全漏洞的数据库,查看你的打包应用程序是否可能容易受到漏洞攻击。例如,由于 CentOS 6 在 2020 年停止维护,它包含的许多库存在严重的安全漏洞。容器扫描会提醒你这个问题,并建议你考虑将应用程序的 Docker 镜像升级为使用 CentOS 7 或更高版本作为基础镜像。

手动安全测试总结

到此为止,我们已经讨论了多种旨在检测安全漏洞或安全相关问题的测试方法,例如未遵循编码最佳实践。虽然在将一个简单的 Web 应用投入生产之前可能需要经过很多不同的步骤,但如今窃取信息或关闭服务的方式越来越多,且没有迹象表明这种趋势会很快改变。所以,无论你愿意与否,负责任的开发者都需要考虑并可能实施所有这些不同的安全测试:

图 1.3 – 各种安全测试类型的一部分

图 1.3 – 各种安全测试类型的一部分

有些测试必须手动执行,其他一些则有自动化工具来帮助。但自动化测试依然繁琐:你仍然需要安装安全测试工具或框架,配置测试工具,更新测试框架和依赖项,设置并维护测试环境,处理报告,并以某种集成的方式展示报告。如果你试图通过将其中一些任务外包给外部公司或软件即服务SaaS)工具来简化问题,你将需要为每个工具学习不同的图形用户界面(GUI),为每个服务维护不同的用户帐户,管理多个许可证,并进行一系列其他任务,以保持测试的顺利进行。

本节展示了在 GitLab 出现之前,开发团队在工作中面临的更多困难。正如你将在接下来的章节中学到的,GitLab 的 CI/CD 流水线用快速、自动化的安全扫描工具取代了之前描述的繁琐、多步骤的安全测试过程,配置一次后,你可以在继续开发软件项目的过程中持续受益。我们稍后将更详细地讨论这个话题。

手动打包和部署代码

现在你的软件已经构建完成、验证无误并且安全,接下来是时候考虑如何打包和部署它了。就像我们之前讨论的其他步骤一样,手动进行这一过程时可能会变得很麻烦。如何将应用程序打包成可部署的状态,不仅取决于它所使用的计算机语言,还取决于你使用的构建管理工具。例如,如果你使用 Maven 工具管理 Java 产品,你需要运行一组与使用 Gradle 工具时不同的命令。将 Ruby 代码打包成 Ruby gem 则需要完全不同的过程。打包通常涉及收集数十个、数百个或成千上万个文件,使用适合该语言的工具将它们打包,再三检查文档和许可证文件是否完整并放置在正确的位置,可能还需要通过加密签名打包的代码,以证明它来自一个可信的源。

我们已经提到过指定代码发布许可证的任务。这引出了在将代码部署到生产环境之前需要进行的另一种测试:许可证 合规性扫描

许可证合规性扫描

大多数开源的第三方库都是根据特定的软件许可证发布的。开发者可以选择的许可证种类繁多,但大多数开源库使用的仅仅是其中几种,包括 MIT 许可证、GNU 通用公共许可证GPL)和 Apache 许可证。了解你的依赖项使用哪些许可证至关重要,因为如果你的项目使用的许可证与某个依赖项的许可证不兼容,那么你就无法合法地使用该依赖项。

什么会导致两个许可证不兼容呢?一些许可证,例如《和平开源许可证》,明确禁止军方使用该软件。另一个常见的许可证冲突原因是所谓的Copyleft许可证与专有许可证之间的冲突。像 GPL 这样的 Copyleft 许可证规定,任何使用了 GPL 所涵盖库的软件,必须也使用 GPL 许可证。Copyleft 许可证有时被称为病毒许可证,因为它们将许可证的限制传递给任何使用了这些许可证所涵盖依赖项的软件。

由于法律要求你确保主许可证与你使用的任何第三方库的许可证兼容,你需要在打包和部署工作流中添加一个许可证扫描步骤。无论是手动进行还是使用自动化工具,你都必须识别并替换任何你不能使用的依赖项。

部署软件

一旦你的软件已打包完毕,并且你已经仔细检查了依赖项的许可证,你就面临着将代码在正确的时间部署到正确位置的难题。

大多数开发团队都有多个环境来部署代码。每个组织的设置方式不同,但一个典型(尽管是最小化的)环境结构可能如下所示:

  • 一个或多个测试环境

  • 一个预发布环境生产前环境,其配置尽可能与生产环境相似,但通常规模要小得多。

  • 生产环境

我们稍后会更详细地讨论这些不同环境的使用,但现在你只需要了解每个环境在基本部署工作流程中的作用。当代码开发时,通常会将其部署到测试环境,以便质量保证(QA)团队或发布工程师确保它按预期工作,并与现有代码无缝集成,不会引起任何问题。当新代码被宣布准备好添加到生产代码库时,传统上会将其部署到预发布环境,以便进行最终测试,确保新代码与最终运行环境之间没有不兼容的情况。如果这些测试顺利通过,代码最终将部署到生产环境,真实用户可以从新代码带来的功能、修复或其他改进中受益。

正如你所想,确保正确的代码在正确的时间被部署到正确的环境中是一项复杂但至关重要的工作。而部署仅仅是战斗的一半!另一半是确保各个环境都可用且健康。它们必须运行在正确类型和规模的硬件上,必须配置正确的用户账户,必须正确设置网络和安全策略,还必须安装正确版本的操作系统、工具和其他基础设施软件。当然,还有维护任务、升级和其他系统重新配置工作,这些都必须计划、执行,并在出错时修复。这些任务的范围和复杂性令人难以置信,这也是大公司拥有完整发布工程师团队的原因,确保一切顺利运行,并在出现问题时紧急排查。

这完成了我们对在你提交新代码后,最常见的 SDLC 任务的介绍:

  1. 构建代码。

  2. 通过各种测试验证代码的功能、性能、资源使用等。

  3. 通过使用更多的测试,确保代码没有安全漏洞。

  4. 将代码打包成可部署的格式。

  5. 寻找并修复任何与许可证不兼容的问题。

  6. 将代码部署到适当的环境中。

到现在,你应该已经感觉到一个主题:在 GitLab 之前的生活是复杂的、容易出错且缓慢的。这些形容词确实适用于在 SDLC 接近尾声时发生的软件包、许可证扫描和发布任务。但正如你将在后续章节中详细了解的那样,GitLab CI/CD 流水线为你处理了这些工作的最繁琐部分。通过让流水线处理那些枯燥和重复的工作,你可以专注于编写软件中更具创意和满足感的部分。

手动软件开发生命周期实践中的问题

现在,你已经对软件在开发者完成编写到用户能够使用之间发生的事情有了大致的了解,你可以开始理解这个过程有多么困难。沿着这条将安全、可工作的代码交付给用户的路径,许多任务需要发生:

图 1.4 – SDLC 中的主要任务

图 1.4 – SDLC 中的主要任务

这些任务中的一些通常是手动完成的,而其他任务则可以部分或完全自动化。但这两种方法都存在相关问题,使得每个任务都成为潜在的痛点。

手动执行这些任务有哪些困难?让我们来看看:

  • 它们需要时间。即使你过去有过手动执行它们的经验,它们通常也比你预期的花费更多的时间。执行这些任务时可能出错的方式有无数种,而每种错误都需要耗时的故障排除和修复。即使一切顺利,每个任务本身也涉及大量的工作。记住物理学家道格拉斯·霍夫施塔特在 1979 年提出的定律:即使你考虑到 霍夫施塔特定律,它总是比你预期的要花费更长的时间。

  • 它们容易出错。因为你依赖的是人类来执行这些任务——而这些人类可能疲惫、无聊或分心——因此它们容易出现配置错误、数据输入错误,或者某些步骤被遗忘或顺序错误,仅举几例,人的错误可能导致任务失败。

  • 它们对员工士气有害。没有人喜欢做例行的重复性工作,尤其是当任务至关重要并且必须做对的时候。每两周第 20 次执行标准的两小时手动测试,许多 QA 工程师都曾怀疑,自己是不是当初选择了错误的职业道路。

  • 它们有很高的沟通或报告错误的潜力。当手动测试人员完成了他们枯燥的两小时测试套件后,他们是否还能剩下足够的脑细胞准确记录哪些工作了,哪些没工作?如果我们无法依赖结果准确记录,那么所有的测试都是没有意义的,但任何执行过复杂手动测试计划的人都知道,结果中可能存在多少不确定性,可能会有多少意外情况扭曲测试结果,而要知道如何向依赖这些报告的人解释这些因素是多么困难。更不用说即使结果毫不含糊,仅仅是记录错误的结果的可能性有多大了。

出于所有这些原因,你可以看出,手动任务在时间、金钱和员工善意方面的开销是多么巨大

但是,如果我们所描述的一些任务能够自动化,是否能消除我们在手动任务中面临的问题?嗯,这将解决一些问题。但将一系列自动化工具添加到 SDLC 中,也会引入一整套新的问题。考虑到这样做所涉及的额外努力和费用,以及构建自定义工具链所需要承担的所有任务:

  • 研究和选择适用于每个可自动化任务的工具

  • 购买和续订每个工具的许可证

  • 为每个工具选择托管解决方案

  • 为每个工具配置用户

  • 学习每个工具的不同图形用户界面(GUI)

  • 管理每个工具的数据库和其他基础设施

  • 将每个工具与 SDLC 中的其他工具进行集成

  • 如果可能的话,弄清楚如何在一个集中位置显示每个工具的状态和结果

  • 处理那些有缺陷、被弃用或随着市场上更好替代品的出现而变得不那么吸引人的工具

即使手动或自动化任务的问题得到处理,使用这种模式的团队仍然面临一个无法避免的重大问题:它是一个顺序工作流。步骤是一个接一个地进行的。一支团队编写软件,然后把代码扔到墙那边,交给另一支负责构建软件的团队。那支团队又将代码交给负责验证软件的第三支团队。当他们完成后,他们通常会将代码交给另一个安全测试小组。最后,发布团队会接手代码,确保它被部署到正确的位置。这个过程可能会有很多偏离基本描述的方式,但一个接一个地执行步骤,在前面的步骤完成后才将代码传递给下一个步骤,这个特征是许多软件开发团队的工作流共有的特点。

到目前为止,可能还不太明显为什么顺序工作流会带来问题,所以让我们详细说明一下。由于手动执行这些步骤的困难,或者保持自动化步骤在多个工具中顺利且可靠运行的麻烦,这种工作流通常只是偶尔发生。代码通过这些步骤的执行频率因团队而异,但涉及的时间和费用意味着代码更改通常会在几天、几周,甚至有时几个月后才被正确地构建、验证、加固和部署。而这反过来意味着,在这个过程中检测到的问题修复成本高昂。如果功能测试失败,安全测试检测到漏洞,或者集成测试显示代码在所有部署到同一环境后无法协同工作,确定是哪个代码导致了问题,就像在一个巨大的干草堆中找针一样。如果 5,000 行代码跨越 25 个类发生了变化,16 个依赖被升级到了更新的版本,Java 版本从 16 升级到 17,而且测试环境运行的是不同版本的 Ubuntu,那么在追踪问题源并找出解决方法时,有这么多的变量需要调查。

到此为止,你已经了解了传统的、开发前的软件开发方式,我们可以用一句话总结它面临的最大问题:涉及手动任务或由不同工具执行的自动化任务的顺序工作流会导致开发变慢,发布不频繁,并且结果软件的质量低于它 本应达到的标准

不过有个好消息:DevOps 的诞生就是为了应对这些问题。而 GitLab CI/CD 流水线的发明则是为了让 DevOps 更易于使用。接下来我们将一起探讨这两者。

使用 DevOps 解决问题

我们所说的DevOps是什么意思?尽管这个术语在软件界已经使用了至少 10 年(第一个devopsdays会议——现在是最大的以 DevOps 为主题的会议——于 2009 年举办),但至今仍没有一个所有人都认同的统一标准定义。

当 GitLab 谈论 DevOps 时,它指的是一种重新思考软件开发生命周期(SDLC)的方法,专注于四个方面:

  • 自动化

  • 协作

  • 快速反馈

  • 迭代改进

让我们更详细地看一下每个方面。

DevOps 的主要关注点是自动化尽可能多的软件开发任务。这可以消除与手动构建、测试、保护和发布相关的挑战。但如果它只是将这些挑战交换为组装一系列手动工具的麻烦和费用,那它的用途就非常有限。我们稍后会看到 GitLab 如何解决这个问题,但现在只需要理解,一个合适的 DevOps 工作流是完全自动化的。

通过促进所有参与编写软件的团队之间的协作,以及每个团队成员之间的协作,DevOps 有助于消除每次代码从一个团队转移到另一个团队时出现的摩擦点和潜在问题。如果没有“墙”可以把代码抛过去——如果过程中的每个步骤对所有参与编写和交付软件的人都是透明的——那么每个人都会对代码的整体质量感到责任重大,并且觉得自己是同一个团队的一员。不同的人仍然对特定任务负主要责任,但整体文化趋向于代码的共同所有权和共同的目标。

快速反馈可能是 DevOps 最关键和革命性的元素。可以将其视为我们之前讨论的两个概念的结果:并行工作流和左移。当你停下来思考时,这两个术语归结为同一件事:对于每一批开发人员提交的代码,尽早并同时完成所有构建、验证和保护任务。以并行方式而非顺序方式执行它们,以确保它们出现在软件开发时间线的最左端。而且对于每一块新代码,无论多么微小,都要立即运行所有这些任务。通过早期且频繁地运行这些任务,你可以最小化测试的代码变化的大小,这使得排除任何由测试发现的软件漏洞、配置问题或安全漏洞变得更便宜和容易。

如果你能快速发现并修复问题,就能够更频繁地将软件发布给客户。通过更早地向他们提供新特性和修复的 bug,你在帮助他们从你产品的迭代改进中获益。通过以更短的间隔发布较小的代码更改,这样的发布风险较低,不容易出错,也无需回滚,你就实现了“让发布变得无聊”这一口号。在这种情况下,无聊是件好事:大多数客户更愿意接受频繁、小规模的升级,因为它们的风险小,而不愿意接受那些不频繁、巨大改变的升级,后者更有可能造成严重问题并需要回退。

通过利用自动化、协作、快速反馈和迭代改进,DevOps 实践能够生成更高质量、开发成本更低、且更频繁交付给用户的代码。

如何通过 GitLab 实现 DevOps

GitLab 是一个可以支持我们讨论的所有软件开发任务的工具,它使用了我们刚刚概述的 DevOps 原则。GitLab 最重要的特点是它是一个单一工具,能够将 SDLC 中的所有步骤统一在一个平台下。

还记得从手动过程转向自动化过程时,解决了一些问题,但也带来了与自动化相关的新问题吗?GitLab 的单一工具方法同样解决了这些问题。考虑一下拥有以下统一工具链方法的好处:

  • 一个许可证需要购买(除非你的团队使用 GitLab 的免费版本,该版本功能受限,在这种情况下不需要购买许可证)

  • 一个应用程序进行维护和升级

  • 一套用户账户进行配置

  • 一个数据库进行管理

  • 一个 GUI 需要学习

  • 一个查看的地方——可以说是一个雷达屏幕——用来查看所有构建、验证、安全、打包和部署步骤的报告和状态

因此,GitLab 作为一个单一工具,解决了使用不同自动化工具所带来的问题。更重要的是,它使用单一的组件和实体,这些组件和实体相互之间能够良好沟通并且互相了解,这种设计促进并鼓励了 DevOps 中至关重要的协作、并发、透明度和共享所有权。一旦你拥有并发任务,你就能获得快速反馈。而这又进一步推动了通过无聊发布进行迭代改进。

本书的大部分内容将讨论 GitLab 如何运用这些 DevOps 原则进行实践:CI/CD 流水线。我们暂时不会定义这个术语的含义,但你将在未来的章节中详细了解它。现在,你只需要知道 CI/CD 流水线是 GitLab 如何将单一工具模型用于执行所有构建、验证、安全、打包和部署的地方,它们是你代码需要经过的过程。

我们不得不提的是,GitLab 的一个重要部分是帮助你规划、分配和管理工作。但这与 CI/CD 管道是分开的,因此超出了本书的范围。我们会时不时提及一些相关话题,因为 GitLab 中的所有内容都是相互关联的,不可能完全局限于 CI/CD 管道的范畴。但本书的大部分内容将解释 GitLab 管道的功能以及如何使用它们。

摘要

不从事软件公司工作的人可能不了解,编写软件不仅仅是……编写软件。在代码提交后,必须遵循一系列冗长复杂的步骤,包括构建、验证、安全性检查、打包和部署,才能让用户使用这些代码。所有这些步骤都可以手动完成,或者在特定条件下,部分步骤可以自动化。然而,无论是手动方式还是自动化方式,都存在问题。

DevOps 是一种相对较新的方法,用于完成这些步骤。它将自动化、协作、快速反馈和迭代改进结合起来,使团队能够更好、更快、更便宜地开发软件。

GitLab 是一个 DevOps 工具,它将所有这些任务整合在一个平台下,使得软件开发团队可以通过单一工具、使用单一界面来完成所有任务,所有测试结果和部署状态都显示在一个地方。它专注于自动化,解决了手动流程中出现的问题。它的单一工具模型则解决了自动化流程中出现的问题。GitLab 通过使用 CI/CD 管道将所有 DevOps 原则付诸实践,这将是本书余下部分的主要内容。

但在我们讨论 CI/CD 管道之前,我们需要快速绕道介绍 Git,这是 GitLab 构建的基础工具。如果没有扎实的 Git 基础,你很可能会觉得 GitLab 的许多概念和术语令人困惑。因此,准备好,拿起你最喜欢的含咖啡因饮料,让我们一起跳入 Git 的世界吧。

第二章:练习基本的 Git 命令

GitLab 产品是围绕一个名为 Git 的独立工具构建的。GitLab 使 Git 更易于使用,并为你提供了一个中央位置来存储 Git 所管理的所有文件,此外还提供了许多与 Git 无关的功能。我们喜欢将 GitLab 看作是 Git 的一个包装器,使其使用起来更愉快,也更强大。

虽然 GitLab 和 Git 是不同的工具,但 GitLab 借鉴了 Git 的许多概念。这意味着,要理解 GitLab,你需要理解 Git。幸运的是,你只需要掌握 Git 的基础知识即可。我们之所以说“幸运”,是因为 Git 是一个庞大且复杂的工具,学习它的所有细节需要付出巨大的努力。但相信我们:如果你理解 Git 的前 10%,你就能有效地使用 GitLab。那 10% 正是我们将在本章中介绍给你的内容。

首先,我们将向你展示为什么像 Git 这样的版本控制系统是软件开发中如此有用的一部分。然后,我们将解释如何将你的代码存储在 Git 中,包括你或你的团队成员对该代码所做的任何编辑。我们还将向你展示如何在一个叫做分支的安全空间中开发代码,该分支与其他团队成员隔离开来。这确保了你不会干扰到他人的工作,也不会覆盖他们的代码。你将学习如何标记代码的特定版本,以便以后可以轻松引用它或将其发布给客户。最后,你将了解如何在远程位置存储代码。你将学习如何同步本地和远程文件的副本,并且你将理解这种架构如何使整个团队能够同时在同一个代码库上工作。

在本章中,我们将涵盖以下主要主题:

  • 为什么使用 Git?

  • 提交代码以保持其安全

  • 为了标记提交以识别代码版本

  • 为了在一个隔离空间中开发代码进行分支

  • 同步本地和远程仓库的副本

  • 学习 Git 的额外资源

技术要求

对于本章,你需要在本地计算机上安装 Git。Git 可以在 Linux、macOS、Windows 以及许多 Unix 变种系统上运行。在 git-scm.com/downloads 上有易于遵循的安装指南,适用于这些操作系统中的任何一个。如果在安装过程中要求设置配置选项,接受所有默认值是安全的。

要输入你将在本章中看到的 Git 命令,请在 Linux 或 macOS 上使用你喜欢的终端应用程序。如果你是 Windows 用户,你可以在命令提示符、PowerShell 或 Git Bash 中输入这些命令。在 Windows 上安装 Git 时,默认的配置选项应该会使 Git 在这些 Windows 终端中可用,而且它们在运行 Git 命令时应产生相同的结果。

本书中你将看到的 Git 示例是操作系统无关的:Git 无论在什么地方运行,表现都一样

要查看 Git 是否已安装,或者验证是否正确安装了 Git,打开适合你操作系统的终端并运行以下命令。如果输出显示版本号而不是错误信息,说明 Git 已正确安装在你的计算机上:

$ git --version
git version 2.25.1

不必担心看到特定的版本号;几乎任何版本的 Git 都能在我们在本书中使用的简单命令上表现一致。

在使用 Git 之前,你必须告诉 Git 你的名字和电子邮件地址。这些信息会被添加到你存储在 Git 中的每一条编辑记录里,以便其他团队成员知道哪些编辑是你负责的。

首先,检查 Git 是否已经配置了这些信息:

$ git config --list

如果输出中包含 user.emailuser.name 的条目,那么你已经配置好了,可以跳过接下来的两个命令。否则,通过运行这两个一次性的命令告诉 Git 你的身份,替换掉电子邮件地址和名字为你的信息:

$ git config --global user.email "george.spelvin@example.com"
$ git config --global user.name "George Spelvin"

一个可选但推荐的步骤是将 Git 配置为在新项目中使用 main 作为默认分支的名称,而不是 master。我们还没有讨论什么是分支,所以这可能现在看起来没有太大意义。现在,你只需要知道,许多软件公司正在将 main 作为项目稳定代码库所在位置的名称。你会在现实中(甚至在本书中)看到这两个术语的使用,但如果你想将你的计算机配置为新项目使用 main,可以运行以下命令一次:

$ git config --global init.defaultBranch main

所有技术要求都完成后,让我们开始吧!

为什么要使用 Git?

就像在自动化工具如 GitLab CI/CD 管道出现之前,了解我们是如何构建软件的(如在第一章中讨论的那样),了解在 Git 或类似工具出现之前,团队如何协调编辑同一文件的复杂过程也是很有帮助的。

这些工具旨在解决开发人员面临的许多问题,但我们这里只看其中一个。假设你和你的队友伊丽莎白在同一个代码库上工作,并且你们都想编辑一些相同的文件。进一步假设这是在 Git 或任何其他版本控制系统VCS)出现之前的时代。那时编写软件的唯一方法是你编辑文件,然后通过电子邮件发送,或者把文件放在共享的网络驱动器上,或者复制到便携磁盘中。然后,你必须让伊丽莎白知道她可以开始编辑这个文件。她以某种方式(也许是通过在电子表格中添加一项条目,表明她控制了文件,或者通过其他机制)“签出”文件,并且只要她需要,就一直保留文件的控制权。如果你有了新的想法,想再次编辑文件,你需要让她停止编辑并将文件转交给你。当她做到了,你需要浏览整个文件,查看她所做的更改,以确保这些更改不会与您想做的更改冲突。然后,你需要为你们两个人正在编辑的每一个文件重复这个过程,每当你们中的任何一个想要编辑这些文件时。你可以想象这个过程有多么缓慢和繁琐,以及在所有文件所有权转移过程中出错的机会有多少!

通过了解过去的工作方式,我们可以看看 VCS 是什么,它是如何解决这个问题的,以及它还以其他方式简化了开发人员的工作。

什么是版本控制系统?

VCS(版本控制系统)是一种工具,旨在让一个或多个开发人员更方便地处理一组文件。它通过在特定时间对项目中的所有文件进行快照,允许你查看、比较和恢复不同快照中的文件来实现这一目标。

每个 VCS 的功能略有不同,但以下是大多数 VCS 提供的一些功能:

  • 提供文件备份,以防当前版本丢失或被意外覆盖。

  • 显示文件内容随时间的变化。

  • 显示了谁在什么时候对哪些文件进行了哪些更改。

  • 为未来参考标记某些文件快照。

  • 提供每一组更改的人类可读描述,以便团队成员理解更改的原因。

  • 允许开发人员以与其他开发人员同时编辑相同文件的方式进行文件编辑。

多年来,有许多不同的 VCS 出现了,包括开源和专有的版本。其中一些最著名的例子包括微软的 Visual SourceSafe、CVS、Apache Subversion,现在还有 Git。由于一些原因,我们将在稍后解释,Git 已经在很大程度上接管了 VCS 领域,并成为任何没有公司规定使用 Git 竞争对手的团队的默认 VCS。换句话说,Git 在 VCS 的竞争中已经获胜,在这样的胜利是可能的情况下。

VCS 可以与任何计算机语言一起使用。例如,你可以使用同一个 VCS 来管理独立的 Java、Python 和 Ruby 项目中的文件。虽然我们通常认为 VCS 主要用于帮助处理计算机语言中的源代码文件,但它们可以与软件项目中的 任何 文件一起使用,包括(但不限于)以下内容:

  • 文档,例如 Markdown 或 PDF 文件

  • 配置文件,例如 JSON 或 YAML 文件

  • 测试代码和数据

  • 集成开发环境IDE)的元数据或配置信息

  • 其他项目资产,如图片、视频或音频文件

不必将 VCS 限于软件项目!你可以使用 Git 或任何 VCS 来管理诗集中的诗歌、食谱书中的食谱,或者小说中的章节。VCS 对于任何涉及计算机文件的项目都很有用。

版本控制系统(VCS)解决了哪些问题?

现在你了解了像 Git 这样的 VCS 提供的功能,你的脑海里可能充满了各种可能性,想象着 VCS 如何解决软件开发者日常遇到的问题。以下是一些场景,但你无疑能想到更多。

为什么要修改这段代码?

你某天早晨在文本编辑器中打开源代码,发现一个你熟悉的方法现在使用了完全不同的算法。为什么要修改它?旧算法是坏掉了吗?新算法更快吗?实现新算法的代码是否更短或更易读?通过查看 VCS 的 提交信息,你可以阅读到为什么进行这次更改的描述。这些信息的完整程度因开发者的细心程度而异,但你通常能大致了解这次更改的动机。

这段代码是什么时候修改的?

想象一下,你重新访问一个几个月没看过的 Java 类,发现它添加了一些新功能,也删除了一些旧的功能。这些变化是什么时候发生的?更重要的是,它们是在上次部署到生产环境之前还是之后发生的?你的 VCS 的提交日志会告诉你每次该类被修改的时间,甚至每次编辑时修改了哪些行。这样,你就能准确地定位哪些变化是何时做出的,从而知道当前客户使用的是该类的哪个版本。

是谁添加了这段有 bug 的代码?

Git 有一个名为 blame 的功能,它会告诉你是哪位开发者编辑了文件中的哪些行。当你发现一些新加入的代码有 bug 或者运行缓慢时,这个功能很有用,因为你知道应该找谁修复它!但是它也有一个积极的用途:如果你发现了一段特别聪明的代码,VCS 会告诉你该向谁表示赞赏,并且希望你能从中学习。所以,blame 功能为改善开发者之间的职业关系和增强团队士气提供了一个很好的方式。

我需要恢复我的 Foo.java 文件的副本

我敢肯定从来没有在工作一整天后不小心删除过文件,但我们确实有过这种经历。我也敢肯定非常小心地为这种情况做备份,但我们可没有。但是,由于我们总是使用 VCS,恢复丢失的文件变得非常简单:每个 VCS 都提供了一种简单的方式来查看和恢复它所管理的任何文件的最后一个版本。

我想恢复今天早上测试目录中所有文件的版本

你不仅可以恢复文件的最新版本;你可以恢复任何版本的文件,无论它有多旧,只要你添加了包含该版本的快照。例如,假设你花了几个小时重写自动化测试,以使它们运行得更快,却发现你的新测试要么更慢,要么根本无法工作。你的 VCS 会让你替换单个文件、目录中的所有文件,或者项目中的所有文件,恢复为任何旧版本。尽管可以随意编辑任何文件,但只要你小心定期将更改提交到 VCS,就不必担心丢失工作或在新代码不可用时恢复到旧代码。

我和同事想同时编辑 Foo.java

VCS 最常用的功能可能是它们能够安全地划分你在文件中所做的修改,以防止它们覆盖其他人在同一文件中所做的工作。每个开发人员都有一个分支,可以在其中编辑任何想编辑的文件,即使其他人也在他们的分支上编辑相同的文件。当每个开发人员完成工作后,他们会将自己的分支合并到项目的稳定代码库中。通过这种方式,多个开发人员可以同时编辑同一个文件,而不会丢失任何工作,也不需要协调文件的所有权。

我需要将上周五的代码版本部署到生产环境

VCS(版本控制系统)允许你标记特定版本的文件,这样就可以轻松查看或恢复这些版本。例如,你可以在进行大规模重构项目之前标记整个代码库,这样如果重构没有成功,你就可以轻松恢复到已知的良好状态。更常见的是,开发团队通常会标记代码的特定版本,以便准确知道某个发布版本中部署的是哪一版代码。例如,你可以给你部署的产品版本 6.1.0 的代码打上version-6-1-0标签。当有人报告该版本的产品存在 bug 时,你就知道该检查产品文件的哪个版本。

我希望我的所有同事都能访问我编辑过的代码

当你编辑一个文件时,你的团队成员必须知道你已经编辑了它,并且能够看到你的编辑。VCS 让你可以轻松地将编辑内容推送到集中式位置。然后,其他团队成员可以将这些更改拉取到他们的本地计算机,确保整个团队保持同步。

为什么 Git 这么受欢迎

我们已经提到过 Git 成为了主流的版本控制系统。那是为什么呢?不同的 Git 用户会给出不同的解释,但以下是一些可能帮助它登顶的特点。

血统

Git 是由 Linus Torvalds 发明的,用于存储和管理 Linux 内核的源代码。Git 最初用于存储像 Linux 内核这样高知名度、成功且广泛采用的代码,这无疑赋予了它即时的信誉和声望:如果它足够强大和可靠,能满足 Linus 和 Linux 的需求,那么它也足够适合你。

顺便提一下,一个程序员负责启动两个重要的软件项目:Linux 和 Git,真是令人惊讶。就好像莎士比亚发明了铅笔,只是为了让他的剧本更容易写一样。

简单的分支管理

正如你很快就会了解到的,分支是任何版本控制系统中最重要的组成部分之一。Git 从一开始就设计得非常简单,方便创建、使用和合并分支。开发者可以轻松地使用分支,这促使他们使用大量的分支,从而促进安全和快速的开发工作流程。

速度

Git 非常快速。添加新文件、提交更改、恢复旧代码以及同步文件以纳入同事的编辑——这些操作都在几秒钟内完成,即使是大型项目。特别是创建、使用和合并分支的操作速度非常快,这也是开发者如此喜爱使用 Git 的关键原因之一。

可靠性

你可能认为,可靠性应该是任何版本控制系统的基本要求:如果 VCS 丢失了你的文件或编辑,它就无法履行其职责。但令人吃惊的是,许多版本控制系统多年来并不是 100% 可靠的。我们在 2000 年代初期曾参与的一个一百人的开发团队使用了当时主流的专有版本控制系统,尽管它被认为是这一类工具中的最佳,但它经常丢失或弄乱我们的编辑。

Git 以其可靠性而闻名。它是一个复杂的工具,如果你不完全理解如何使用它的命令,可能会因为人为错误而丢失数据。但 Git 出现技术故障导致数据丢失的情况几乎闻所未闻。全球无数怀疑的程序员团队信任它,而这种信任是经过长期实践验证的。

分布式架构

在 Git 之前,许多版本控制系统(VCS)采用了集中式架构。这意味着要在文件上工作,你需要从中央服务器获取文件的最新版本,进行编辑,然后再将文件重新提交到中央服务器,以便其他团队成员能够访问。

集中式架构存在一些问题。首先,一些(但不是所有)集中式 VCS 会锁定你已签出的文件,因此在你编辑这些文件时,其他人无法对它们进行修改。这会导致很多“嘿,你做完Foo.java了吗?”这样的对话,这会创造出一种尴尬、不便且令人烦恼的工作流程。

使用集中式架构的 VCS 的第二个问题是,它要求你每次需要签出文件或提交编辑时都必须连接到那个中央服务器。没有网络连接,你无法高效工作。尽管这种问题不如以前那么严重,但仍然会有一些时刻,你处于热点之间,仍然想继续工作。

集中式架构也会创建一个单点故障。如果服务器出现故障,所有开发人员的工作都会陷入停滞。如果数据丢失或服务器被物理摧毁,恢复数据或重建硬件可能需要几天时间。

最后,随着开发团队的增长,集中式架构的扩展性往往较差。依赖性能不足的 VCS 服务器的快速增长的团队,可能会在排队访问该服务器时被阻塞。

幸运的是,Git 所依赖的分布式架构解决了所有这些问题。当你在不同的计算机上拥有项目文件的多个副本时,这些问题便消失了。在使用分布式 VCS 时,每个开发人员的本地计算机上都有整个项目的副本。包括所有文件、编辑历史、标签、提交消息以及其他元数据,这些都使得开发人员可以在没有连接到中央服务器的情况下继续工作。

这种策略如何帮助解决集中式版本控制系统所面临的问题呢?首先,如果每个开发者都有项目中所有文件的本地副本,就不存在锁定你正在编辑的文件的概念:任何人都可以随时编辑他们本地的任何文件。其次,不需要联系中央服务器来检出文件,也不需要联系服务器来提交修改。你可以在本地工作,直到你愿意。确实,最终你需要将你的修改同步到服务器上,以便同事能看到你的修改(而你也能看到他们的修改),但你可以根据团队的需要,决定同步的频率。第三,由于每个开发者的机器上都有整个项目文件的副本,所以不再有单一的故障点。如果你用来同步更改的中央服务器出现故障,你可以将任何开发者的机器指定为临时中央服务器,同时重建原始服务器。最后,因为使用分布式版本控制系统的开发者与中央服务器同步代码的频率远低于使用集中式版本控制系统的开发者,所以分布式版本控制系统在扩展性方面比集中式版本控制系统更具优势。大多数使用 Git 的团队在增加新成员时不会遇到版本控制相关的扩展问题。

Git 的缺点

记得我们提到过 Git 之所以有信誉,是因为它是由 Linus Torvalds 发明的吗?不幸的是,这里有一个问题:它是为 Linus 的思维方式设计的,而不是为你的。这意味着它的命令可能不一致、令人困惑,并且违反直觉。举个例子,让我们看看一个命令如何通过三种不同的方式来修改它的行为:

  • git branch 列出所有可用的分支。

  • git branch foo 创建一个名为foo的新分支。

  • git branch --delete foo 删除一个名为foo的分支。

你可能会期待这些命令应该是这样的:

  • git branch --list(这有效,但不必要,也没有人使用它)

  • git branch --``create foo

  • git branch --``delete foo

但情况并非如此;你必须记住不同的选项语法。仅仅是一个命令就需要这样做。

Git 的另一个大问题是它太庞大了。它有很多功能、选项和可配置的设置,可能让人觉得不知从何入手。官方参考文档,《Pro Git》一书,长达 511 页!当你刚开始使用这个工具时,很容易产生一种感觉,觉得自己永远也学不够 Git 的概念和命令,无法有效使用它,你可能会想,怎么会有人能够掌握这么复杂的东西。

幸运的是,你不需要了解 Git 的所有不一致性和语法复杂性,也不需要知道 Git 提供的所有功能。你只需要掌握一些常用的命令及其变种,就能完成 95% 需要的 Git 相关任务。大多数 Git 用户学习大约 20 个常见操作,随着时间的推移将其记住,其他 Git 操作的细节只在需要时查阅。所以,不要慌张,也不要试图学习并记住所有的 Git 知识。 如果你对本章描述的简单命令和概念感到舒服,你已经具备了使用 Git 做实际工作的能力。这可能就是你从这个工具中所需的全部。

这就是对 Git 的介绍,现在是时候查看一些实际的命令了。

提交代码以保持它的安全。

为了享受前面所描述的所有优势,你需要知道如何将文件添加到 Git 中。你该如何做到呢?

首先,让我们讨论一下仓库的概念,它通常简写为repo。仓库是 Git 存储项目文件和所有文件更改历史的地方。它是一个保险库,用来保存文件以确保其安全。

创建仓库有两种主要方式。第一种方式是将 Linux、macOS 或 Windows 文件系统中的普通目录转换为 Git 仓库。这非常简单:在该目录中使用 git init 命令,完成后它就变成了一个 Git 仓库。然后,你可以使用 git status 命令来证明它是一个仓库。

让我们使用这些命令为我们的“猫帽子”项目创建一个新的仓库。首先,创建一个新的目录,将其变成一个仓库,并进入该目录(本章中的示例使用的是 Linux 终端,如果你使用的是其他终端或操作系统,提示符和输出可能会稍有不同,但概念是相同的):

$ mkdir hats-for-cats
$ cd hats-for-cats

证明它还不是一个 Git 仓库:

$ git status
fatal: not a git repository (or any of the parent directories): .git

使用 git init 命令将该目录转变为 Git 仓库:

$ git init

现在,观察 git status 如何没有明确告诉我们我们是否在一个仓库中,但它确实提供了只有在目录被转变为仓库时才有意义的信息。它告诉我们我们在 main 分支上,Git 没有跟踪仓库中的任何文件,并且我们没有编辑任何可能希望 Git 管理的文件:

$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

创建仓库的第二种方式,可能也是更常见的一种方式,是使用git clone命令从另一台计算机复制现有的仓库。在本章之前,我们提到过 Git 的分布式架构意味着每个团队成员都在自己的计算机上拥有项目 Git 仓库的完整副本。克隆是从其他计算机下载该仓库副本到你计算机的方式。稍后我们会在讨论远程仓库时详细介绍这一过程。目前,理解作为软件开发团队的一员,你大多数时候会从其他计算机克隆仓库,而不是仅在自己的计算机上创建新仓库。

现在我们已经有了一个仓库,接下来我们来添加一个文件。以这个例子为例,添加一个待办事项列表。首先,使用touch命令或文本编辑器在hats-for-cats/目录中创建一个名为todo.txt的文件。可以在文件中填写任何内容,或者让它保持为空。Git 可以很好地管理空文件:

$ touch todo.txt

你的目录中包含一个文件,但 Git 还没有跟踪该文件,因为你还没有正式将其添加到仓库中。这个过程分为两步:

  1. 将一个或多个文件移动到称为暂存区的地方。

  2. 提交暂存区中的所有文件。

理解这一点很重要:文件在 Git 中并不存储,意味着 Git 不会管理该文件的版本,直到你将它暂存并提交

还记得我们谈到过版本控制系统(VCS)如何在特定时间对文件状态进行“快照”吗?这个暂存和提交过程就是你用 Git 创建新快照的方式。这意味着你完全控制何时创建快照,以及每个快照包含哪些文件。如果你正在编辑foo.pybar.py,但此时只想对foo.py进行快照,那么只需将该文件添加到暂存区并提交。当你准备好对bar.py进行编辑并创建快照时,将该文件添加到暂存区,当然如果你希望记录自上次创建快照以来对foo.py所做的任何更改,也可以将foo.py一并添加,然后提交暂存区中的文件。

你可能已经注意到,我们讨论过暂存和提交新文件以及编辑现有文件。之所以这样,是因为无论是新文件还是修改文件,你都使用相同的暂存和提交过程。你可以将暂存和提交看作是用来捕捉文件的任何更改,而更改可以有四种不同的形式:

  • 创建一个新文件

  • 编辑现有文件的内容

  • 重命名或移动文件(就文件系统而言,这两者是相同的操作)

  • 删除文件

无论你执行哪种操作,当你将一个或多个文件添加到暂存区并提交时,你实际上是在为暂存区中的文件创建一个新的 Git 快照。

你可能会想,为什么暂存和提交的步骤需要分开?Git 难道不能提供一个命令来直接创建文件的快照吗?这样做的原因是为了让你能够在一个提交中包含多个文件。例如,你可能编辑了一个源代码文件、修改了一些位于不同文件中的关联测试代码,并最终编辑了一个文档文件,这些都是修复一个 bug 的一部分。你会希望把这三个文件都包含在一个提交中,因为对所有文件的修改在逻辑上是关联的。

首先,让我们使用 git add 命令来暂存你的新 todo.txt 文件。这并不会移动或复制文件,而是给文件添加了一个看不见的标签,表示它已经进入暂存区:

$ git add todo.txt

如果你再次运行 git status,你会看到 todo.txt 已经包含在待提交更改的列表中,这意味着它已经被添加到暂存区,但还没有提交:

$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   todo.txt

现在,你可以使用 git commit 命令提交它了。但在此之前,关于提交信息,有几点需要说明。每个提交的重要组成部分就是提交者提供的可读提交信息。最简单的添加方法是作为 git commit 命令的一个参数,使用 --message 选项:

$ git commit --message "add list of tasks to do"

如果你再次运行 git status,你会看到没有文件等待提交,这意味着你的待办事项列表已经安全地存储在仓库中,且所有未来的修改都将被 Git 跟踪。恭喜你,你刚刚完成了第一次使用 Git 的操作!

让我们暂停一下,谈谈 Git 每次提交时包含的信息。你已经了解到,提交包含了提交文件的内容和描述提交目的的信息,但它还包含其他一些信息。以下是完整的列表:

  • 对文件内容(或文件名或文件系统中的位置)所做的任何更改。

  • 一个由 40 个十六进制字符组成的字符串,称为安全哈希算法SHA),该代码根据提交文件的内容生成这个字符串。一个提交的 SHA 唯一标识该提交;你可以把它看作是该提交的唯一名称。SHA 不是按顺序排列的:每个 SHA 都与前一个提交的 SHA 完全不同。

  • 提交者的姓名和电子邮件。

  • 提交时间戳。

  • 描述为什么进行这次提交的易读信息。

  • 指向当前分支上前一个提交(或父提交)的指针。我们将在下一节介绍分支的概念,并进一步讨论这个指针。

git log 命令会显示当前分支上所有提交的信息(我们稍后会更详细地解释分支)。假设你已经进行了两次提交:一次是创建一个空的待办事项列表,另一次是往列表中添加了一项内容。运行 git log 会输出类似以下内容:

$ git log
commit 7d4c98438ade780531e1baa283b3239c21943171 (HEAD -> master)
Author: George Spelvin <george.spelvin@example.com>
Date:   Tue Jan 4 09:57:37 2022 -0800
    add first item to list
commit 63ea581d1bc693dac159c146fa10d1cbfa4e6366
Author: George Spelvin <george.spelvin@example.com>
Date:   Sun Jan 2 12:31:27 2022 -0800
    add list of tasks to do

此输出包括您的两个提交的信息,最近的提交位于顶部。您能找到每个提交的 SHA、作者信息、时间戳和提交信息吗?如果您在自己的电脑上运行这些命令,您将看到每个提交的不同细节,但输出的格式将保持一致。

在这个例子中,所有提交信息都来自您自己的提交。这仅仅是因为只有您一个人向这个分支添加了提交。如果其他人也向同一个分支提交了内容,您将看到他们的提交信息也会出现在此输出中。

从仓库中排除文件

到此为止,您可能会认为将项目中的所有文件都暂存并提交是明智的做法。但实际上,有几类文件您通常不希望存储在 Git 或任何其他版本控制系统中。这些文件包括但不限于以下几类:

  • 从其他文件生成的文件

  • 极大的文件

  • 包含敏感信息的文件

第一类文件包括从源代码编译而来的可执行文件,或从存储在 Markdown 中的源文本生成的 PDF 文件。由于您可以随时从源文件重新生成这些文件,因此无需将它们存储在 Git 中。此外,将它们放入 Git 可能会引入漂移问题,即源文件和生成的文件不同步。例如,源文件可能包含上周的代码,而编译后的可执行文件可能包含上个月的代码。这会导致各种意想不到的问题。

大文件提出了另一种问题。如果您添加了一个 5 GB 的 ISO 文件或一个 10 GB 的数据集,那么任何将您的项目仓库复制到本地的用户都将不得不下载该文件。我们之前提到过,Git 很快,但它无法解决拥挤或缓慢的网络问题。由于复制仓库在 Git 用户中是一个相当常见的操作,因此通常希望避免在仓库中包含巨大的文件。将这些文件放在 Git 外部,例如共享驱动器或其他可访问的数据存储系统上,通常是更合适的做法。即使您删除了一个已经添加的大文件,也无法解决问题。因为 Git 会记录每个添加到仓库中的文件的完整编辑历史,所以该大文件会永远留在您的仓库中,每当有人将仓库复制到本地时,都会给您和您的同事带来困扰。

最后,像部署密钥、SSH 私钥或密码这样的机密通常不应该存储在 Git 中。正如前面所述,任何你提交到仓库的机密信息将永远存在。如果你的 IT 和 Git 管理员不小心限制了仓库的权限,这意味着任何有权限访问你仓库的人都可以查看它们。因此,这些内容通常不是存储在 Git 中,而是存储在专门设计的系统中,这些系统专门用于保护敏感数据的安全。

这是一个重要的话题,因此让我们通过一个具体的例子进一步探讨。假设你的 hats-for-cats/ 目录中包含一个名为 personal-notes.txt 的文件和一个名为 .ide-config/ 的目录。前者是你记录项目功能创意的地方,而后者包含一些你的 IDE 用来在本地计算机上配置项目的文件。你可能不想与开发团队的其他成员共享这些内容,因为这些创意只是供你个人参考,而配置文件仅适用于你的计算机设置。

你可以通过从不使用 git add 命令将它们添加到项目的 Git 仓库来保持这个文件和目录的私密性。这是可行的,但这种方法有个问题。每当你运行 git status 查找是否有需要添加和提交的编辑过的文件时,Git 总是会指出 personal-notes.txt.ide-config/ 不在仓库中:

$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .ide-config/
        personal-notes.txt

这个问题有一个简单的解决方案:在项目根目录下创建一个名为 .gitignore 的新文件。在这个文件中,添加你希望 Git 忽略的任何文件或目录的名称。你可以通过在行首添加井号字符来添加解释性注释。下面的 .gitignore 文件中的代码指示 Git 排除我们之前提到的文件:

# notes to myself that are not for public consumption personal-notes.txt # configuration files for my IDE .ide-config/

你需要将这个 .gitignore 文件添加到仓库中,就像你想让 Git 跟踪的任何其他文件一样:

$ git add .gitignore
$ git commit --message "exclude notes file and IDE config directory"

多亏了 .gitignore,当你运行 git status 时,Git 不再警告你关于未提交的文件和目录:

$ git status
On branch main
nothing to commit, working tree clean

对于大型项目来说,.gitignore 文件中最终可能会有几十个甚至几百个条目,我们鼓励你使用这个功能来保持 Git 仓库的整洁,避免 git status 输出杂乱无章。

让我们回顾一下你在本节中学到的命令和概念,关于通过提交代码到 Git 仓库来确保代码安全:

  • 使用 git initgit clone 在你的计算机上创建一个 Git 仓库。

  • 使用 git add 将一个或多个文件添加到 Git 的暂存区。

  • 使用 git commit --message "<MESSAGE>" 来创建一个新的 Git 提交(或快照),包括所有在暂存区中的文件。

  • 使用 git status 查看你是否有编辑过的文件,等待被移到暂存区,或者是否有已暂存的文件,等待被提交。

  • 使用 git log 查看当前分支中所有提交的相关信息。

  • 使用 .gitignore 文件排除仓库中的某些文件或目录。

现在,让我们学习如何标记你在 Git 中精心提交的文件的特定版本,以便你将来可以轻松查看或恢复到该版本。

为了标记提交,识别代码版本

现在你已经理解了开发者如何以及为什么将代码提交到 Git,我们可以解释标记。标记很简单:它是一种给提交添加永久标签的方式。有很多原因会用到标记,但最常见的两种原因是标记发布给客户的代码的确切版本,以及在需要回滚大量更改时,能够方便地返回到特定版本的代码。我们来看看这两种情况的例子。

假设《Cats for Hats》已经准备好发布版本,你可以使用 git tag 命令为分支上的最新提交添加一个表示版本号的标签:

$ git tag version-1-0-beta

如果你运行 git log,你会看到 Git 已经将标签应用到你的最新提交上。

作为一个单独的例子,假设你决定进行一次大规模重构,将你的类改为使用更复杂的类继承结构。你知道这种大规模更改很容易搞砸,所以你希望有一种简单的方式在一切出问题时返回到今天的重构前版本。标记正是解决方案:

$ git tag before-class-reorganization

标记有很多选项,但其中两个最有用的是 --delete--list,它们的功能和你预期的一样。试试它们,看看效果:

$ git tag --list
$ git tag --delete version-1-0-beta

这就是标记的概述。和大多数 Git 命令一样,标记功能有很多其他操作可以做,但这部分内容涵盖了基础。幸运的是,现在你对标记的理解已经和大多数 Git 用户一样,所以除非你需要更多的灵活性或功能来标记文件的版本,否则不必担心进一步探索 git tag

你现在已经掌握了将文件保存在 Git 中以供保存、排除不想保存在 Git 中的文件,并为某些提交加上标签以便将来参考的基本知识。现在,是时候学习如何编辑、存储和标记文件,同时避免与其他可能在相同文件上工作的开发者发生冲突。是时候了解 Git 分支了!

为了在隔离的空间中开发,进行代码分支

提交后,分支可能是 Git 中最重要的概念。严格来说,你并不需要了解分支和分支操作就能使用 Git,尤其如果你是一个独立开发者。但任何涉及多人合作的软件开发工作通常都会大量使用分支。让我们来弄清楚它们是什么,以及为什么我们需要它们。

分支只是有序的提交序列。记得我们曾说过每个提交都会包括一个指向前一个提交的向后指针吗?如果你从最新的提交开始,沿着这些向后指针一直追溯到首次提交,这样你就描述了你的提交所在的分支。再强调一次,分支只是由一系列按特定顺序排列的提交构成,且通过 向后指向的箭头链接在一起*。

每当你在 Git 代码库中处理文件时,你正处于某个分支上。这意味着你只能看到该分支上文件的版本,任何提交都会被添加到该分支上。更准确地说,你在该分支上进行的每个提交都会包含一个指向上一个提交的向后箭头,这个箭头将提交与同一分支上的前一个提交联系起来。

你可以将处于某个分支类比为在一条街道上行驶。想象一个只有两条街道的小镇。主街(Main Street)从南到北延伸,第一街(First Street)从主街分出来,平行于主街延伸几条街区,最后再与主街合并。你的车在任何时刻必须正好处于这两条街道中的一条。为了进一步延伸这个类比,假设有一架直升机,配备了一个巨大磁铁,可以随时将你的车从主街和第一街之间移动。再将这个类比变得更加离奇一点,假设在你车的后座有一个小孩,他每隔一段距离就会从车窗扔出弹珠到街上。在这个类比中,街道代表分支,车代表开发者,弹珠代表提交,是开发者在他们所处的分支上进行的添加,而直升机则代表Git 命令(我们很快会学习的),它允许开发者随时在任意分支之间切换:

图 2.1 – 街道和弹珠,代表分支与提交

图 2.1 – 街道和弹珠,代表分支与提交

每个代码库至少有一个分支,通常被称为默认分支。分支有正式名称,默认分支几乎总是被正式命名为mainmaster,其中前者现在通常更为推荐。

如前所述,分支图通常会包括从每个提交到该分支上前一个提交的箭头。换句话说,箭头指向时间的过去,从后来的分支指向早期的分支。因此,如果你的代码库有一个名为main的分支,并且该分支上有三个提交(分别叫做ABC),而另一个名为branch-a的分支上有两个提交(分别叫做DE),并且branch-a是从main分出来的,你可以像这样画出代码库的状态:

图 2.2 – 分支与提交

图 2.2 – 分支与提交

为什么需要分支?它们让你在代码准备好被加入到稳定的代码库之前,将开发中的代码与稳定的代码库隔离开来。以下是一个典型的工作流程,展示了这一过程是如何发生的:

  1. 你的产品代码存储在一个 Git 仓库中。该代码的稳定版本存储在该仓库中的一个名为main的分支里。

  2. 你被指派为你的产品编写一个让用户能够登录的功能。

  3. 你创建了一个名为login-feature的分支。

  4. 你切换到login-feature分支。

  5. 你编辑文件并向该分支添加一个或多个提交。

  6. 其他团队成员审核这些提交中的编辑内容并给你反馈。

  7. 你添加了另一个提交来合并反馈。

  8. 你的团队负责人批准了你的工作,声明登录功能已正确实现。你的 QA 团队也可能会签署你的工作。

  9. 你将login-feature分支合并main分支中。这意味着你对login-feature分支所做的所有提交,现在也成为了main分支的一部分。你的登录功能现在已经成为产品主代码库的一部分。

  10. 由于login-feature分支已经合并且不再有任何用途,你可以安全地删除它。

在这个工作流程中,重要的是要注意,在你开发登录功能的过程中,你的部分完成的代码并未出现在主代码库中(即main分支)。你的不完整代码与产品的稳定代码安全地隔离开来,避免了它破坏稳定性。登录功能直到经过正式审查、批准并通过所有测试后,才会被加入到主代码库中。这就是我们所说的分支让你在隔离的环境中开发代码,远离其他开发者的工作和稳定的代码库。

这个例子只涉及一个开发者,但通常,多个开发者会同时在不同的分支上工作。例如,两个开发者可能会在各自的分支上工作以添加新功能,另外两个开发者可能会在各自的分支上工作以修复 bug。某个开发者可能在一天内在多个分支之间切换,因为他们的工作重点从一个任务转移到另一个任务。重点是,分支之间是彼此独立的——当你在一个分支上时,无法查看或修改其他分支上的代码,直到所有分支最终合并到main分支。

用于管理分支的 Git 命令

让我们学习一下你需要用到的 Git 命令,以便按照之前描述的工作流程操作。

下面是如何创建一个名为login-feature的新分支:

$ git branch login-feature

这个命令会显示该仓库中所有分支的列表,当前所在的分支会用星号标记:

$ git branch

你可以使用两条不同的命令来切换到另一个分支。它们执行相同的操作,你会看到两者都有使用。在这种情况下,它们会将你切换到login-feature分支:

$ git checkout login-feature
$ git switch login-feature

合并一个分支(称为源分支)到另一个分支(称为目标分支)需要两个命令,这样源分支上的所有提交就会成为目标分支的一部分。要将login-feature中的所有提交合并到main,首先,确保你在目标分支上:

$ git checkout main

然后,在指定分支的情况下执行合并:

$ git merge login-feature

一些组织喜欢将所有分支保留下来作为历史记录的一部分,但大多数组织会要求你在将分支合并到main后删除该分支。以下是删除login-feature分支的方法:

$ git branch -–delete login-feature

如果你在尝试删除分支之前没有合并该分支,Git 会警告你并不会删除该分支。你可以强制删除该分支(例如,如果你创建了一个实验性分支并希望在不合并的情况下删除它),方法如下:

$ git branch -–delete -–force experimental-branch

处理合并冲突

当你尝试将一个分支合并到另一个分支时,有时会遇到所谓的合并冲突。这意味着其他人已经编辑了与你相同的文件中的相同行,并且他们已经在你合并自己的分支到main之前将他们的分支合并到了main。当你尝试合并你的分支时,Git 不确定是保留其他开发者所做的修改,还是保留你正在尝试合并的修改。

要继续合并,你首先需要解决合并冲突。解决冲突有几种方法。许多人发现使用专门的 Git GUI 工具,如 Sourcetree(适用于 macOS 和 Windows)或 Sublime Merge(适用于 Linux、macOS 和 Windows),是处理合并冲突最简单和最直观的方式。其他人则喜欢手动解决合并冲突,使用 Git 终端命令和文本编辑器。GitLab 用户还有一个选择:可以使用 GitLab 内置的图形化合并冲突解决工具。无论你采取哪种方法,你都需要以某种方式告诉 Git 接受哪些更改、丢弃哪些更改,并继续合并。

让我们看看 GitLab 的合并冲突解决工具是如何工作的。假设你正在new-login-message分支上编辑login.py文件。在为该分支创建了合并请求并提交了一些编辑后,你返回到合并请求页面,准备进行合并,却发现合并请求被合并冲突阻止:

图 2.3 – 被合并冲突阻止的合并请求

图 2.3 – 被合并冲突阻止的合并请求

由于你只编辑了一个文件,你知道一定是其他人也编辑了同一个文件的相同行,并在你有机会编辑之前已经将他们的修改合并到了 main 分支。现在,Git 理解上有些困惑,不知道是应该接受你的修改、他们的修改,还是某种混合修改。在这一问题解决之前,它无法将你的 new-login-message 分支合并到 main

GitLab 给你提供了两个选择:你可以点击 解决冲突,使用内置的 GUI 工具解决合并冲突,或者你可以点击 本地解决,手动指示 Git 如何处理合并,通过在你计算机上的仓库副本中编辑文件,然后将这些修改推送回 GitLab。第二种策略超出了本书的范围,因此我们将专注于第一种策略:使用 GitLab 解决合并冲突。请注意,这个选项只会出现在图形界面中,适用于像本例中的简单合并冲突。如果你需要通过复杂的编辑来合并两个独立提交的部分,你可能需要在本地解决合并冲突,然后将解决结果推送回 GitLab。

在这个例子中,你会想使用 GitLab 内置的合并冲突工具,因此你必须点击 解决冲突。你将被带到一个页面,显示出哪些行存在冲突修改。你会看到,只有一行存在冲突修改,你的修改用绿色突出显示,其他人已经做的修改用蓝色突出显示。假设你把“我们很高兴你在这里”这一行改成了“我们真的很高兴你在这里”,而现有的修改将这一行改成了“我们超级高兴你在这里!”。

图 2.4 – 在 GitLab 中解决合并冲突

图 2.4 – 在 GitLab 中解决合并冲突

你决定更喜欢自己的修改,因此你点击解决合并冲突的 new-login-message 分支。这个新的提交触发了你分支上的管道运行。管道完成后,你就可以像平常一样合并合并请求了。恭喜——合并冲突已成功解决,合并完成!

图 2.5 – 合并冲突已解决,合并请求被解除阻塞

图 2.5 – 合并冲突已解决,合并请求被解除阻塞

假装所有的合并冲突都像这个例子一样容易解决是不负责任的,但无论问题有多复杂,解决合并冲突的一般方法是相同的:你告诉 GitLab 哪些修改需要保留,哪些修改需要丢弃,以及当你想混合不同的修改时,如何合并同一行代码或文本上的修改。许多第三方图形界面(GUI)Git 工具比 GitLab 提供的合并冲突解决工具更强大,因此如果你发现需要更多功能或只是需要不同的视角来查看发生了什么,别怕尝试其他工具。

这部分讲解了如何创建、提交和合并分支。希望现在你已经了解了为什么分支是 Git 最重要和最受欢迎的功能之一!但是,当你与团队中的其他开发者共享分支,并能访问他们正在开发的分支时,分支的功能会变得更强大。在接下来的章节中,你将学习如何与团队协作使用分支。

同步本地和远程仓库副本

Git 对单独开发者来说是一个有用的工具,但它最常用于开发者团队中。正如我们之前所讨论的,Git 的分布式架构意味着团队中的每个开发者都有项目仓库的完整副本,包括所有的提交、提交信息、分支,以及仓库中包含的所有其他数据和元数据。保持这些仓库的同步,让它们都包含相同的信息,是至关重要的。如果我的仓库副本和你的仓库副本包含不同的文件,或者对同一文件进行了不同的编辑,那么我就无法看到你所做的工作,反之亦然。而且,如果我的仓库副本没有你正在添加提交的分支,我就无法审查和批准你的工作。同步仓库不是一个自动的过程:它需要开发者的积极参与。本节将解释如何实现这一点。

“黄金”仓库

在向你展示同步仓库的命令之前,我们需要部分撤回我们之前的说法。还记得我们解释过 Git 的分布式架构的一个优势是没有一个所有开发者都需要连接的中央服务器吗?其实那并不完全正确。Git 的分布式架构确实要求每个开发者都拥有项目仓库的完整副本,但团队需要指定其中一个仓库副本作为我们所说的 黄金 仓库。这个仓库被认为包含 最新的稳定代码库版本,也是你的团队发布软件的仓库。我可能在我的电脑上一个分支上开发一个功能,你可能在你的电脑上一个分支上修复一个 bug,我们可能已经完成了完成这些任务所需的所有编辑,但直到我们将我们的编辑提交到黄金仓库,它们才正式成为项目稳定代码库的一部分。由于这个黄金仓库的特殊角色,我通常把它看作是“群英之中的首位”或者“记录仓库”。

黄金库是托管在 GitLab 实例上的仓库副本。如果你的 20 个开发者团队使用 GitLab,那么你的项目仓库至少会有 21 个副本:每个开发者的电脑上一个副本,GitLab 实例上的黄金副本。如果托管 GitLab 实例的计算机由于某些原因不可用,你可以暂时将任何开发者计算机上的仓库指定为团队的黄金仓库。但一旦 GitLab 实例的副本恢复可用,你应该尽快恢复使用 GitLab 实例的副本作为黄金仓库。

当我们谈论在不同副本之间同步修改时,这种同步总是通过 GitLab 实例进行的。换句话说,如果我在本地计算机上的仓库做了提交,并且想让同事们也能看到这些提交,我不会直接将这些提交发送到他们每一台计算机上。相反,我会将我的提交推送到黄金仓库,然后每个开发者从黄金仓库拉取我的提交。

下图展示了一个工作流,允许一位开发者通过推送和拉取包含提交内容的分支与另一位开发者共享他们的提交,推送和拉取的目标是黄金库:

图 2.6 – 通过黄金库与同事共享提交

图 2.6 – 通过黄金库与同事共享提交

配置远程仓库

在你能将你的仓库与黄金仓库同步之前,Git 需要知道黄金仓库的存在。任何不在你本地计算机上的仓库副本都叫做“远程仓库”,因此同步提交的一个重要前提是配置远程仓库。

你可以使用以下命令查看 Git 知道的所有远程仓库的名称和 URL 列表:

git remote --verbose

前面我们描述了将仓库获取到你电脑上的两种方式:git initgit clone。如果你在一个使用 git init 创建的仓库中,并要求 Git 给你列出远程仓库,它将不会返回任何输出。它还不知道任何远程仓库,因为你还没有告诉它任何信息。但如果你使用 git clone 将仓库复制到你的机器上,那么 Git 已经知道一个远程仓库:你克隆仓库时的那个副本。

在我们深入讨论之前,先来讲一下如何克隆一个仓库。正如前面提到的,克隆是获取仓库到你计算机上的最常见方式,因此熟悉这个操作是非常重要的。

你可以从你的本地计算机可以联网的计算机克隆一个代码库,但我们这里重点讲解从 GitLab 实例克隆代码库的过程。本例假设该代码库托管在位于 www.gitlab.com 的 GitLab 实例上。如果你使用的是自托管版本的 GitLab,而不是我们在本例中使用的软件即服务SaaS)版本,克隆过程是相同的,只是你所克隆的代码库地址会稍有不同。

要克隆一个托管在 GitLab 上的代码库,你需要知道该代码库的地址。这个地址可以有几种形式,但最常见的两种形式使用的是 HTTPS 协议或 SSH 协议。虽然这两种协议都能工作,但通常推荐使用 SSH 协议。它要求你先配置 SSH 密钥,但之后在与该远程仓库交互时,你无需再输入凭据:密钥基础设施会自动处理所有认证。另一方面,使用 HTTPS 协议时,每次使用 Git 命令与远程交互时,都需要输入用户名和密码。

你已经看到如何使用 git init 创建我们 Hats for Cats 项目的代码库,但现在,让我们换个角度,假设 GitLab 实例上已经有一个 Hats for Cats 的代码库,你希望将该代码库克隆到你的计算机上。

首先,你需要使用 ssh-keygen 命令在计算机终端中创建公钥和私钥,然后将公钥上传到你的 GitLab 实例。GitLab 提供了非常详细的文档,指导如何完成此过程,文档地址为 docs.gitlab.com/ee/ssh,因此我们推荐你查看该文档,而不是在这里重复说明。请注意,这个过程只需要做一次,之后你在同一台本地计算机上与该 GitLab 实例的任何项目交互时,都会自动认证。如果你换到其他 GitLab 实例或其他本地计算机,则需要重新进行此过程。

现在,你需要找到该项目的 SSH 地址。登录 GitLab 并导航到该项目。我们还没有正式介绍 GitLab 的图形界面(GUI),因此接下来的指令和截图可能看起来有些神秘,但等你更熟悉 GitLab 的各个组成部分和导航时,这些内容会更加清晰。

在你项目的主页上,如 图 2.7 所示,点击 克隆 按钮。在弹出的下拉菜单中,复制 Clone with SSH 标签旁边的地址:

图 2.7 – 从 GitLab 克隆代码库

图 2.7 – 从 GitLab 克隆代码库

现在,你已经设置了一个密钥对,并且将项目的 SSH 地址复制到了剪贴板上,导航到本地文件系统中你想包含仓库的目录,并使用git clone和仓库的 SSH 地址将远程仓库克隆到你的计算机上。你的 SSH 地址将与此处显示的不同:

$ cd ~/code
$ git clone git@gitlab.com:cwcowell/hats-for-cats.git

克隆的过程为你定义了一个远程仓库:它让你的仓库的本地副本知道,一个远程副本存在于你从中克隆的任何 URL。你可以在git remote --verbose的输出中看到此远程副本列表:

$ git remote --verbose
origin  git@gitlab.com:cwcowell/hats-for-cats.git (fetch)
origin  git@gitlab.com:cwcowell/hats-for-cats.git (push)

这里定义了两个远程:第一个用于获取其他人的提交,而另一个用于发送你的提交。不必担心这两者之间的区别;你可以将它们视为单个远程。你还会在输出中看到“origin”这个词。任何仓库的黄金副本通常被称为“origin”,其原因只有 Git 设计者才知道。

如果你是使用git init而不是git clone创建仓库,你需要在你的 GitLab 实例上创建一个 Git 项目和仓库作为远程仓库;然后,你可以使用git remote add命令让你的本地仓库知道这个远程仓库的存在。因为这比使用git clone要少见得多,所以如果你有需要,我们会让你自己查找此命令的语法。

推送

到目前为止,我们使用“发送”一词来描述将本地提交复制到黄金仓库的过程。但官方 Git 术语是“push”。推送提交的命令是git push

让我们回到你在本地仓库上创建了一个名为login-feature的分支的场景。想象一下,你已经向该分支添加了几个提交,现在你想将该分支推送到 GitLab 实例上的黄金仓库,以便你的队友可以查看该分支及你对其所做的提交。要做到这一点,请确保你在要推送的分支上,然后推送它。

在我们展示命令之前,我们应该解释一下,当你第一次将一个分支推送到远程仓库时,有一个小问题:你必须告诉远程仓库如何命名它的分支副本。几乎总是希望远程分支的名称与本地分支的名称相同。在黄金仓库上,你的分支的远程副本有时被非正式地称为“上游”分支。要告诉远程仓库将上游分支命名为login-feature,切换到要推送的分支,然后向git push传递几个额外的选项:

$ git switch login-feature
$ git push --set-upstream origin login-feature

请注意,这些选项中包含了origin这个词。这告诉 Git 要将更改推送到哪个远程仓库,即使此时你只定义了一个远程仓库。此外,理解你只需要设置一次上游分支的名称也很重要。从那时起,你只需运行git push,而无需任何额外选项,就能将当前分支的提交推送到远程的黄金仓库副本。

获取

到目前为止,你已经了解了如何将自己的编辑推送到黄金仓库,以便同事们可以查看。那么,如何将他们的编辑获取到本地仓库呢?你需要知道的第一个命令是git fetch。这个命令会与黄金仓库通信,查看是否有人将新的分支或提交推送到黄金仓库的现有分支上。然后,它会将这些信息传递给你的本地仓库。它不会更新你本地计算机上的任何分支或文件。它只是收集关于黄金仓库中已做更改的元数据,并将这些信息告知你的本地仓库,而不会对你的本地仓库做任何修改。这意味着,git fetch是一个非破坏性的完全安全的命令,你可以随时运行,频繁运行也没有问题。

这个命令非常有用,因为它可以让你知道你的分支是否“落后”于黄金仓库的该分支副本:你的分支是否缺少黄金仓库副本中存在的提交?这可以帮助你了解如果你尝试将更改推送到黄金仓库时,是否会遇到合并冲突。如果其他人已经在黄金仓库的相同分支上修改了相同的文件,除非你使用git fetch来获取有关该处更改的信息,否则你无法知道这些更改。因此,如果你想知道自己是否拥有分支上的最新编辑,或者你想知道是否有人向黄金仓库添加了任何新的分支,git fetch就是获取这些信息的方式。

值得再次强调的是,git fetch提供了关于黄金仓库中分支状态的最新信息,但不会更新你本地计算机上的任何文件。要更新本地文件,你需要了解最后一个 Git 命令。

拉取

要用黄金仓库中该分支副本的更新版本替换本地分支上的所有文件,切换到适当的分支,然后拉取更改:

$ git switch login-feature
$ git pull

如果自上次运行git pull以来该分支有任何编辑,Git 将会用黄金仓库该分支副本的更新文件替换本地分支上的任何文件。

记住,git pull一次只操作一个分支。也就是说,它只会从黄金仓库中获取你当前所在分支的更新文件。没有办法通过一次使用git pull来获取所有分支的所有编辑,但也没有明显的理由说明你会需要这么做。

让我们将所有与同步相关的 Git 命令结合成一个工作流程,向你展示如何将项目仓库的本地副本与仓库的黄金副本保持同步:

  1. 在本地计算机上,使用 git switch 切换到你想要进行工作的分支。

  2. 使用 git fetch 获取有关黄金仓库中分支副本是否有新编辑的信息。

  3. 运行 git status 查看你的本地分支是否比黄金仓库中的同一分支“落后”。

  4. 如果是,请使用 git pull 更新你的本地文件。

  5. 对本地分支副本进行编辑并提交更改。

  6. 使用 git push 将你本地分支的所有提交推送到分支的黄金副本。

完成了这个关于同步仓库分支的解释后,你现在已经掌握了使用 Git 做实际工作的所有基本知识!但如果你发现你需要学习我们这里没有涵盖的概念和命令,或者你想看到不同的解释和示例来说明这些相同的主题,你可以参考许多其他地方来获取更多信息。现在,让我们来看一些最好的替代资源。

学习 Git 的其他资源

在这一点上需要提醒一下。你只看到了本章中介绍的 Git 命令的最基本用法。实际上,这些命令有很多选项可以用来改变它们的行为,而且在不同情况下正确使用这些命令也有很多不同的细节和技巧。我们之前提到过,解决合并冲突这个重要概念超出了本次快速介绍 Git 的范围,但在日常使用 Git 时,其他一些重要的概念和实践可能会出现,而我们这里没有足够的空间讲解,包括变基和选择快进合并提交合并之间的区别。此外,我们也无法描述在使用 Git 时遇到文件处于不希望的或不熟悉的状态时,常见的故障排除过程。然而,我们可以做的是为你推荐一些其他资源,帮助你继续扩展 Git 知识,并丰富你的 Git 命令和实践经验。

鉴于此,以下是我们最喜欢的 Git 参考资料和学习材料:

  • GitLab 提供了一份非常好的四页 PDF 备忘单,涵盖了最常见的 Git 概念和命令。每当我们需要回忆某个基本命令的用法时,这是我们首先查阅的地方:about.gitlab.com/images/press/git-cheat-sheet.pdf

  • Ry’s Git 教程,由 Ryan Hodson 编写,是一个非常棒的 Git 学习教程,适合学习 Git 或刷新你对其命令和概念的记忆。它仅作为免费的 Kindle 电子书在 Amazon 上提供。

  • Pro Git,由 Scott Chacon 和 Ben Straub 编写,内容过于密集和枯燥,不适合作为教程,但它是一本很好的参考书。当我们需要查找一个不常用的命令或想知道一个常用命令(如 git commit)的所有选项时,我们就会去查阅它。它是一本可以在 Git 官网免费下载的电子书:git-scm.com/book/en/v2

  • Dangit Git!?! 是一个网站,展示了如何摆脱一些常见的 Git 难题。当我们在使用 Git 命令时犯错,需要修复我们破坏的内容时,我们会在这里寻求帮助:dangitgit.com/

记住,你不可能学会或记住所有 Git 的命令和选项。我们认为,最好的方法是学习一些你每天使用的命令,然后在需要进行更复杂操作时查找额外的命令和语法。像 Pro Git 这样的参考书籍或 Git 的手册页会让你眼花缭乱,展示出你可以配置这些命令的不同方式,以及 Git 提供的所有额外命令,用来解锁工具的高级功能。但只有在你愿意的情况下,才深入了解这些内容;没有必要理解所有可用的命令或选项来有效地使用 Git。即使你能够学会所有 Git 的内容,所需的努力也远超于收益递减的临界点。熟悉这里呈现的概念和命令,使用临时仓库进行练习,根据需要查阅参考资料,你就能够成为一个快乐且高效的 Git 用户。

总结

停下来,深呼吸,并为自己鼓掌。你在非常短的时间内学到了很多关于一个复杂工具的知识。

你现在明白了程序员如何以及为什么使用版本控制系统(VCS)来处理各种日常任务和问题,以及为什么 Git 的特性和架构使其成为首选的 VCS。你还了解了最常用的 Git 概念和命令。

现在你已经完成了本章,你可以使用 git initgit clone 创建新的 Git 仓库;使用 git addgit commit 添加文件编辑;使用 git tag 为提交打标签以便日后参考;使用 git branch 列出、创建或删除分支;使用 git merge 合并分支并解决合并冲突;使用 git pushgit fetchgit pull 同步本地分支和黄金仓库中的分支。

你也知道在哪里可以进一步学习 Git,无论你需要教程步骤、参考资料,还是故障排除帮助。

在掌握了 Git 的基础知识后,接下来是从 Git 过渡到 GitLab。在下一章中,你将学习 GitLab 如何使 Git 更加简单且更强大。我们将为你提供 GitLab 组件和图形用户界面(GUI)的基本理解,以便向你介绍 GitLab CI/CD 管道这一强大的概念。

第三章:理解 GitLab 组件

GitLab 是一个庞大而复杂的 Web 应用程序,旨在成为软件开发生命周期中每个步骤的“一站式商店”:它帮助你规划、创建、测试、保障和部署软件。这些仅仅是它涵盖的大任务!它还帮助你通过多种工作流跟踪进展、记录项目、创建发布说明、存储 Docker 镜像或其他类型的软件包、托管静态网页、监控已部署应用的性能,以及监控 Kubernetes 集群中的可疑网络流量。这个列表可以更长,但你明白了:GitLab 有助于完成标准软件开发生命周期中的大部分任务

第一章中,我们阐明了 GitLab 旨在解决的关键软件开发生命周期问题。现在,我们将向你介绍一些关键的 GitLab 概念和组件,这些是你有效使用 GitLab 时需要熟悉的内容。一旦你理解了这些基础,你将准备好开始设置 CI/CD 流水线,我们将在第四章中开始讨论这个话题。

在本章中,我们将讨论项目、小组、问题、分支和合并请求。接着,我们将通过向你展示如何在应用程序的 GUI 中创建、管理和使用这些概念,使这些 GitLab 组件栩栩如生。当你熟悉了这些基础后,你将学习如何使用 GitLab flow,这是 GitLab 开发人员推荐的一种最佳实践工作流,用于在编写、测试、保障和部署软件时有效地结合工具的各个组成部分。

本章的主要主题如下:

  • 强调“为什么”而非“如何”

  • 介绍 GitLab 平台

  • 将工作组织成项目和小组

  • 使用问题跟踪工作

  • 使用提交、分支和合并请求安全地编辑文件

  • 使用 GitLab flow 启用 DevOps 实践

技术要求

如果你能通过登录到 GitLab 实例上的账户来跟随本章内容,你将从中受益最大。这个账户可以是托管在 gitlab.com 上的实例账户(也称为 软件即服务 (SaaS) 实例),或者是托管在你公司上的实例账户(称为自管理实例、自托管实例或本地实例)。你甚至可以将 GitLab 托管在你自己的硬件上,或者通过 AWS EC2、Google Cloud Platform 或 Microsoft Azure 等服务在云中的虚拟机上托管。

由于 GitLab 的硬件要求非常低——你甚至可以在树莓派上托管 GitLab——并且有多种包含所有 GitLab 实例所需内容的“Omnibus”Linux 安装包,因此托管自己的实例并不像听起来那么不可思议。如果你想走这条路,我们将引导你查看about.gitlab.com/install上的 GitLab 安装文档,获取更多信息。

如果你更愿意让别人来为你处理安装、管理和升级任务,可以访问gitlab.com并在他们的 SaaS 平台上注册一个免费账户。虽然 SaaS 版和自托管版 GitLab 之间有一些小的功能差异,但这些差异非常微小,因此我们在本书中不会讨论它们。从实际用途来看,SaaS 版 GitLab 和自托管版 GitLab 的功能集是相同的。

截至 2023 年初,GitLab 有三个产品套餐:免费版、Premium 版和 Ultimate 版。第一个套餐是开源的,任何人都可以免费使用,但它的功能最为有限。Premium 版需要付费许可证,但增加了一些额外功能。Ultimate 版的价格高于 Premium 版,但解锁了完整的 GitLab 功能集。这些套餐适用于 SaaS 版和自托管版的 GitLab。

本书将讨论一些在免费套餐中提供的功能,也会介绍一些仅在 Premium 和 Ultimate 套餐中可用的功能,还有一些功能只有在 Ultimate 许可证下才能解锁。如果你的预算有限,不用担心。即使在较低的套餐中,GitLab 也有足够的功能来提升你作为软件开发者的工作效率。许多人发现免费套餐已足够满足他们的需求,尤其是如果他们主要使用 GitLab 来做个人爱好项目的话。

强调“为什么”而非“如何”

在我们开始之前,有一句警告。大多数情况下,本书不会逐步引导你点击 GitLab GUI 中的每个操作步骤。

首先,大多数操作的指令已经在 GitLab 官方文档中得到了很好的覆盖,文档内容清晰且详尽。

其次,由于 GitLab 正处于快速开发中,其图形用户界面(GUI)经常发生变化。这些变化通常不是激烈的、破坏工作流的变化,但它们足够显著,以至于截图或如何执行操作的逐条指令很容易变得过时。这意味着本书中的具体指令可能会变得让人困惑或无法执行,甚至可能导致数据丢失,因为随着时间推移 GitLab 的 GUI 会发生变化。为了避免这个问题,我们将主要关注为什么你可能想要使用不同的 GitLab 功能。虽然我们会给你一个如何使用这些功能的整体框架,但通常不会提供每个配置选项或工作流的详细操作指引。

介绍 GitLab 平台

什么是 GitLab?

叫做 GitLab 的公司生产一个单一的产品:一个也叫 GitLab 的 Web 应用程序。幕后,GitLab Web 应用程序是一个复杂的工具、数据库、队列和粘合代码的集合,将一切串联在一起,但就用户而言,它只是一个用于构建软件的单一 Web 工具。

“GitLab”的不同含义

本书中的“GitLab”一词指的是工具本身,而非公司,除非我们明确说明。

正如我们在第一章中所讨论的,GitLab 的单一工具模型比任何由多个、更专注的工具组成的集合更容易安装、管理和升级。它只需要每个用户一套凭据。它为所有功能提供一致的 GUI。它集成了所有软件开发生命周期工具,允许数据在一个功能到另一个功能之间流动顺畅且无丢失或失真。它提供了一个单一的位置,让你在规划、构建、测试、安全和部署软件时,了解软件的状态。更棒的是,它比购买多个单独工具的许可证便宜得多。如果你发现 GitLab 的任何单独功能不能提供你所需要的灵活性或功能,你几乎总是可以将其他工具与 GitLab 集成,以使其符合你的技术需求和偏好工作流。

GitLab 解决了什么问题?

GitLab 的目标——它旨在解决的问题——随着时间的推移发生了变化并且更加广泛。GitLab 于 2011 年创建时,关注点比较狭窄:它希望使 Git 更加易用且更强大。那时候,它不过是一个基于 Web 的 GUI 封装工具,作为一个集中的地方来存储项目的主 Git 仓库。

从那时起,GitLab 的范围得到了扩展。它现在不仅仅针对 Git,而是针对整个软件开发生命周期。

要了解其使命如何发展,首先需要理解 GitLab 在软件开发生命周期中的“阶段”概念。GitLab 识别出 10 个阶段:

  • 管理:创建审计和合规报告,并限制对资源的访问。

  • 计划:将工作分解为可操作的任务,便于优先级排序、加权和分配给团队成员。

  • 创建:提交、审核和批准文件编辑,无论它们包含的是代码、配置信息还是其他资产。

  • 验证:运行自动化测试,确保软件按预期执行。

  • 打包:将软件打包成可部署格式。

  • 安全:查找软件或其依赖项中的任何安全漏洞。

  • 发布:部署软件,选择性地使用功能标志和金丝雀发布等复杂技术。

  • 配置:设置代码将要部署的环境。

  • 监控:报告性能指标、事件或错误。

  • 保护:检测部署环境中潜在的安全问题,如 Kubernetes 集群。

请注意,软件开发生命周期分为各个阶段并没有什么神奇之处。另一家公司可能将其分为 9 或 13 个阶段,并可能在阶段之间的边界上有所不同。但 GitLab 的分阶段方法对于任何参与过软件开发的人来说, probably seems reasonable。

让我们回到 GitLab 旨在解决的具体问题上。自从作为解决使用 Git 的难题的方案起步(也就是说,早期的 GitLab 只专注于之前提到的“创建”阶段),现在它已经能够解决软件开发生命周期所有 10 个阶段中的问题。由于不同的阶段呈现出不同的问题,难以用简洁的语言描述 GitLab 旨在解决的单一问题。事实上,考虑到 GitLab 现在解决的是来自 10 个阶段的各种问题,这个问题可能根本无法简明扼要地回答。我们能给出的最简洁、最好的回答就是:它帮助人们更高效、更少风险地编写更好的软件

我们首先要承认,GitLab 目前在解决所有 10 个阶段的问题上还没有达到同等的效果。换句话说,GitLab 提供的解决方案中,有些功能比其他功能更成熟、更强大。正如你可能预期的那样,GitLab 存在时间最长的功能(如与 Git 相关的功能)通常是最成熟的,而较新开发的功能(如在 Kubernetes 集群中保护你的应用免受可疑流量攻击)则显得相对简陋。但 GitLab 对自己对各个阶段问题的解决方案的相对成熟度评估非常透明,并且明确表示其将在近期专注于哪些功能的开发和改进。因此,如果你特别关心 GitLab 对某一特定软件开发生命周期问题的解决方案是否足够完善,快速搜索“GitLab maturity”可能会为你提供所有必要的信息,让你可以做出是否 GitLab 提供足够的功能和灵活性来解决你最关心的问题的明智决定。

在这一点上,你可能会想,GitLab 如何与专注于软件开发生命周期某一阶段的专业工具竞争。毕竟,单一工具真的能替代 10 个独立工具的组合吗?每个工具都被认为是在各自领域中“最佳”的解决方案。

首先,你可能会发现,你并不像最初认为的那样需要那么多功能或那么强大的性能。本书的其中一位作者曾参加过一整天的 Java 性能分析工具培训课程。培训结束时,他脑袋一片混乱,因为这款产品提供了许多惊人的功能,并且他很快就能向经理展示详细的性能瓶颈报告。但最终事实证明,他的公司只需要该工具 2% 的功能,完全可以使用一个更简单且更便宜的替代工具。这个故事的寓意是什么?无论你关心的是哪个软件开发生命周期阶段,GitLab 可能会提供你所需的所有功能

其次,GitLab 的一些功能是通过集成独立开发的开源工具实现的,这些工具在解决其所面临的问题时,确实是最优秀的工具。例如,许多 GitLab 的安全漏洞扫描工具都是备受推崇的开源工具。确实,你可以在 GitLab 之外下载并使用这些工具,但 GitLab 使得在工作流中启用这些工具变得非常简单,并且将它们的输出集成到现有的 GitLab 仪表板中,呈现出一种熟悉的、易于阅读的格式,且与 GitLab 其他报告一致。

最后,如果你确实发现 GitLab 提供的功能不足以解决某个特定的软件开发生命周期问题,你几乎总能找到将外部工具集成到 GitLab 工作流中的方法。其中一些集成是 GitLab 明确支持的,因此能够实现几乎无缝的效果。其他集成可能需要你更多的操作。但几乎任何可以从操作系统命令行运行的工具,都可以与 GitLab 集成。结果可能有所不同,但无法与 GitLab 对接的工具几乎可以忽略不计。

验证、安全和发布阶段

既然我们已经确认 GitLab 在帮助解决软件开发生命周期的所有 10 个阶段方面有雄心勃勃的目标,我们就稍微缩小一下视野。本章重点介绍 SDLC 的中间阶段:验证安全发布阶段。这些是编写软件时最常用的阶段,也是最具挑战性的阶段,正如 第一章中所描述的那样,存在许多问题。幸运的是,这也是 GitLab 最为有效的阶段,而 GitLab 用来解决这些阶段问题的功能,正如你可能猜到的,是 CI/CD 流水线。现在你应该明白为什么本书有这么多内容专注于这一主题了!

为了理解 GitLab 如何帮助解决这三个阶段中发现的问题,你需要了解一些概念、术语和 GitLab 组件。幸运的是,本章接下来要学习的内容将对许多其他 GitLab 阶段也具有相关性和实用性。因此,一旦你理解了如何使用这些概念,你不仅能够继续理解 GitLab CI/CD 管道,还能更好地理解 GitLab 如何解决本书未讨论的其他 SDLC 阶段。

本章的其余部分将重点介绍这些概念、术语和组件。让我们开始吧,首先介绍一个 GitLab 组件——项目

将工作组织为项目和组

项目是 GitLab 的基本构建块。一个 GitLab 项目代表你正在处理的单个软件产品或单个非软件项目。项目是你存储文件的地方,也是你导航 GitLab 不同功能的起点。简而言之,项目是你作为 GitLab 用户大部分时间所在的地方。

以下是一些典型项目的示例,以及可能使用它们的人员:

  • 用于寻找附近洗车店的手机应用,开发团队 #1 使用

  • 同一个洗车应用的桌面版,开发团队 #2 使用

  • 技术写作团队使用的文档

  • 即将举行的会议,用于事件规划团队

  • 新员工的入职任务,整个公司使用

正如你所看到的,这些示例中有一些与软件相关,但其他的则与软件无关。你可以使用项目来规划、管理和跟踪任何类型工作的进展。虽然大多数 GitLab 项目确实专注于软件开发,但你的公司可能会发现许多与技术无关的项目用途。

没有硬性规定如何将工作划分为项目。例如,一家公司可能决定将所有文档放入一个独立的、专门用于文档的项目中,正如前面的示例所描述的那样。另一家公司可能会将每个软件产品的文档文件包含在为这些产品创建的项目中。使用最适合你的结构。通常,这需要一些反复试验,因此不要害怕重新调整你的项目使用方式。

了解项目是什么,最简单的方式是看一张项目的图片。你不会感到惊讶,GitLab 本身就是使用 GitLab 工具开发的。这里是 GitLab 开源部分的项目:

图 3.1 – GitLab 开源代码的项目

图 3.1 – GitLab 开源代码的项目

如你所见,文件列表占据了项目屏幕的最大部分。从截图中可能不明显的是,这些文件实际上是一个 Git 仓库。

您可以将一个 GitLab 项目视为一个“包装器”,它围绕着一个 Git 仓库。此外,您还可以将存储在 GitLab 中的仓库视为“黄金”副本,就像在第二章中讨论的那样。由于项目包含 Git 仓库,项目还让您访问 Git 仓库通常包含的所有其他内容,包括 Git 提交、Git 标签和 Git 分支。我们将在本章后面讲解如何查看这些组件。

有时,您会发现自己拥有一系列以某种方式关联的项目。以下是一些典型示例:

  • 都属于同一团队的项目

  • macOS 和 Windows 版本的同一软件的项目

  • 所有与数据库管理相关的项目

当这种情况发生时,您可以使用GitLab 组来聚集这些相关的项目,以便它们都集中存在于 GitLab 的一个地方。您可以将 GitLab 组视为类似于目录或文件夹,用于存储一组项目。

GitLab 组不仅限于保存 GitLab 项目:它们还可以包含其他 GitLab 组。您可以在 GitLab 组内最多拥有 20 级子组。我们鼓励您根据需要使用这些子组,将项目组织成相关的集合。

这是一个组、子组和项目的示例结构,可能有助于您理解这三者之间的关系。假设有一个名为 Acme Anvils 的公司,其 IT 团队负责开发销售铁砧的软件。它还开发用于内部目的的软件,例如库存管理。它们的组层级结构可能如下所示:

图 3.2 – 一个示例的组和项目层级结构

图 3.2 – 一个示例的组和项目层级结构

组不仅仅是收集相关项目的方式。您还可以用它们来建立角色和权限。使用组,您可以执行以下操作:

  • 邀请其他 GitLab 用户成为组的成员。

  • 为他们分配组内的角色。

  • 授予用户、用户组或角色查看或编辑该组内任何项目的权限。

因此,组提供了一种简单的方式,可以一次性管理多个用户的访问控制。

一个组还会汇总该组内所有项目的组件。例如,您可以进入单一界面查看该组内所有项目的所有问题。

但是,组不一定要复杂,您也不必使用它们的所有功能。它们是一个很好的方式,可以简单地将相关项目收集到一个地方。

关于项目、组和子组的理论讲解到此为止,现在是时候看看这些概念如何在实践中应用了。

示例 – 组织您的“猫咪帽子”工作

回想一下第一章,我们介绍了您构思的“猫咪帽子”网店。现在是时候认真设置 GitLab,帮助您开发软件了。

假设你决定“猫帽子”需要以三种不同的形式存在:Web 应用、iOS 应用和 Android 应用。你决定,尽管这三种产品之间的某些逻辑会相似,但它们之间有足够的实现差异,因此每个产品都应该有自己的项目。

(提醒:本章不会告诉你如何创建、编辑或查看项目或组。正如本章开头所提到的,官方 GitLab 文档是你获取逐步操作指导的最佳资源,适用于 GitLab 的任何组件或使用 GUI。本章——实际上整个书籍——专注于为什么,而非如何。)

由于 iOS 和 Android 都是移动平台,你决定将它们收集到一个专门用于移动开发的单一组中。然后,你决定将这个组与 Web 应用项目一起,收集到一个包含整个“猫帽子”概念的总组下。最后,你决定提供一个完全与平台无关的在线文档:它应该适用于 iOS、Android 和 Web 版本的应用。因为它不与任何现有项目相关联,所以你希望创建一个新的项目来专门保存文档。

在 GitLab 中实现这种结构时,通常从上往下工作最为简便,先从组开始,最后创建项目。你从登录 GitLab 并创建顶级Hats for Cats组开始,使用所有默认设置。(如果你想跟着做,请查看官方 GitLab 文档,了解如何操作的详细步骤。)完成后,GitLab 会将你带到该组的主页。现在,你决定在Hats for Cats组内创建一个名为Mobile的子组。

现在是时候创建项目了。(再次说明,GitLab 文档可以详细解释此过程,但幸运的是,这非常简单。)假设你进行以下操作以开始进行“猫帽子”项目的工作:

  1. Hats for Cats组内创建一个名为Documentation的项目。

  2. Hats for Cats组内创建一个名为Web的项目。

  3. Mobile子组内创建一个名为iOS的项目。

  4. Mobile子组内创建一个名为Android的项目。

完成后,你将得到一个看起来像这样的组和项目结构:

图 3.3 – “猫帽子”组和项目层级

图 3.3 – “猫帽子”组和项目层级

在继续之前,先快速复习一下:

  • 一个项目会为你提供一个 Git 仓库,你可以在其中存储代码。

  • 一个项目也是你在 GitLab 中进行大多数工作的核心位置。

  • 可以将多个相关项目收集到一个组中。

  • 组可以包含项目、其他组或两者。

  • 通过在组内组织项目,甚至可能是子组,你可以保持项目的良好组织,方便查找,并且你还能在组级别分配权限给其他团队成员,并使这些权限应用到该组内的所有项目。

现在该进入 GitLab 的下一个基本构建块:问题

使用问题跟踪工作

如果一个 GitLab 项目是单一产品或计划所在的地方,那么 GitLab 的问题就是单一工作项所在的地方。如果你曾使用过其他工具进行工作规划和跟踪,可能会遇到像“故事”或“工单”这样的术语,它们用于描述与 GitLab 问题类似的组件。

问题存在于 GitLab 项目中,每个问题仅属于一个项目(尽管它们可以在项目之间移动)。除了与项目相关联外,问题还与大量其他 GitLab 组件相关联,正如我们在介绍这些组件时所看到的那样。事实上,这些关联是 GitLab 在 SDLC 的所有 10 个阶段中发挥作用的关键因素之一。

GitLab 问题的结构

GitLab 问题由多个部分组成,其中这四个部分是最重要的:

  • 标题

  • 一个描述

  • 几个可选的元数据字段

  • 一个有线程的讨论,团队成员可以在其中对问题进行评论。

让我们更详细地看一下这些问题组件。

标题是对问题内容的简短描述。例如,添加 FAQ 页面修复 bug #12提高页面加载性能 20%都是合理的问题标题。你不需要在标题中提供所有关于某个功能的细节;那是描述字段的作用。

描述字段可以包含你想要的任意多或少的文本。它可以包含截图或链接,并充分利用 Markdown 的格式化功能。随着更多信息的揭示或问题方向的变化,它也可以在后续编辑。

问题有几个元数据字段。我们不会逐一介绍所有字段,但这里是一些最重要的:

  • 指派人:此字段标识一个或多个负责此问题的人,意味着他们负责推动问题的进展,并作为联系人,解答那些不想添加到问题讨论区的提问或评论。

  • 截止日期:GitLab 中有几种方式可以使用截止日期,但最直接的方式是将截止日期直接分配给问题。

  • 标签:我们稍后会更详细地讨论这些标签,但它们的作用是优先级排序、路线指定或报告问题进度等。

  • 工作量:该字段描述你预期问题所需的工作量。如果你熟悉 Scrum 项目管理方法,那么你也使用过类似的“故事点”概念。为一个问题分配工作量时,你可以使用具体的度量单位(例如人时),也可以使用更抽象的度量单位(例如,小任务为一分,中等任务为两分,大任务为三分)。每个团队都有自己关于这方面的哲学,通常是通过时间和经验积累出来的。

除了这些明确的元数据字段,还有一个关于问题的重要元数据:它是开放还是关闭的。每个问题开始时的状态是开放。当某人完成了问题所需的工作后,通常会将其状态更改为已关闭

最后,每个问题都有一个讨论区,允许人们参与线程式讨论,就像你在 Facebook 或 Instagram 上看到的那样。由于是线程式的,参与者可以回复个别消息,也可以添加全新的消息。讨论可以包括表情符号、链接或图片。

既然一张图片胜过千言万语,这里有一个来自你们“猫咪帽子”项目的示例问题:

图 3.4 – 示例问题

图 3.4 – 示例问题

大多数项目包含许多问题,这些问题的状态有开放已关闭。GitLab 使得查看项目中所有问题的列表变得容易,并且可以聚焦到列表中的任何问题,查看该问题的完整详情。你也可以从组级别而不是项目级别查看问题列表。这种视图会显示属于该组内任何项目的所有问题列表。所以,如果你想了解“猫咪帽子”应用的 iOS 和 Android 版本还有多少问题需要处理,你可以从各个独立项目中升到上一级,查看移动端组中所有开放状态的问题列表。

问题能代表的任务类型

你可能认为问题仅用于捕捉与软件开发相关的工作,但那只是问题冰山的一角。让我们来看看可以用 GitLab 问题描述和追踪的广泛任务范围:

  • 增加一个功能。

  • 修复一个 bug。

  • 编写自动化测试。

  • 设置一个数据库。

  • 配置一个全团队使用的工具。

  • 研究技术选项。

  • 头脑风暴,解决一个问题。

  • 计划一个活动。

  • 对团队进行调查,了解编码标准的偏好。

  • 报告并管理安全事件。

  • 提出一个新产品或新功能的想法。

  • 提出任何人都可以发表意见的问题。

  • 请求为即将到来的公司聚会设计 T 恤。

当然,问题的用途远不止于此,远比这个简短的列表要多。如你所见,问题可以用于技术性或非技术性的工作,既可以由个人使用,也可以由整个公司使用。

再举一个可能意外的例子,GitLab 的每位新员工都会被分配一个问题,其中包含欢迎加入公司文本以及一长串要完成和检查的入职任务。在员工的前几周,员工的经理和公司的人力资源部门将监控此问题,以查看他们在入职流程中的进展情况。稍后,当入职流程完成并关闭该问题后,员工可以将该问题用作公司政策和流程的参考来源。

标签

在解释如何实际使用问题之前,我们需要向您介绍标签。这些是带有短文本的彩色标签。您可以将标签应用于问题或其他 GitLab 组件,例如合并请求(稍后我们会讨论),并在它们不再有用时删除它们。您可以为您的项目或组定义任何需要的标签,并始终可以添加更多标签或删除现有标签。然后,您可以将一个或多个标签应用于问题,以“标记”该问题的标签内容。

这里是一些通常创建的标签示例:

  • High Priority:指示需要立即处理的问题

  • QA:指示由质量保证团队负责的问题

  • Status::Healthy:指示按计划进行的问题

  • Status::At Risk:指示已落后并需要额外资源分配的问题

请注意,最后两个标签在其描述文本中有双冒号。双冒号具有特殊含义:它们将这些标签转换为作用域标签,这意味着它们是互斥的。也就是说,一个问题可以被应用为Status::Healthy标签或Status::At Risk标签,但不能同时存在。不带双冒号的非作用域标签可以以任何组合应用于任何问题。例如,您可以将Front-endDB标签同时应用于需要前端开发人员和数据库管理员工作的问题。

GitLab 使用数百个问题来优先考虑、路由、分配责任并跟踪工作,以开发 GitLab 产品本身,因此请勇于制作和应用您所需的任何问题;它们可以免费创建并易于管理。

问题工作流程

与许多 GitLab 组件一样,对于每个团队在每种情况下都有效的问题使用工作流并不存在单一的解决方案。鼓励您进行实验,并发现根据您的需求和团队文化最佳使用问题的方法。但是,我们可以提供一个问题的典型工作流示例,您可以将其用作探索与 GitLab 问题合作的可能性的起点。

这是您的一项名为"Hats for Cats"项目的样本工作流程:

  1. 构思需要完成的工作,并弄清楚这项工作属于哪个项目。例如,作为《Hats for Cats》iOS 项目的一部分,你需要研究 Objective-C 和 Swift 编程语言,以决定使用哪种语言来编写 iOS 应用。

  2. 在该项目中创建问题并描述问题中的工作内容。你创建了一个标题为Research languages for iOS的问题,并添加了关于可能的编程语言及你初步选择哪个语言的看法的描述。

  3. 为问题添加权重。你决定使用预期的总人天数作为度量标准,并为该问题分配一个权重值为二。

  4. 为问题设置截止日期。你将问题的截止日期设置为 3 天后。

  5. 分配标签以优先处理并指引问题。你将iOS高优先级标签分配给该问题。前者确保合适的人员进行监控,后者则表明该问题需要立即开始处理。

  6. 讨论问题。参与开发《Hats for Cats》iOS 应用的人员分享他们在不同 iOS 语言方面的经验。其他人提出澄清性问题。有些人提供了讨论 Swift 和 Objective-C 的外部博客链接。你还从一个面向开发者的网站上添加了一个语言对比表的截图。

  7. 分配问题。在讨论中,你询问最有经验的开发者是否愿意处理这个任务。当他们同意后,你将问题分配给他们,这样每个人都知道他们负责处理这个问题并更新进展。

  8. 更新标签。随着工作的进展,你分配问题的人员更新问题的标签。例如,当工作开始时,他们可能会移除高优先级标签,并在意识到自己可能无法在问题的截止日期前完成研究时,添加状态::风险中的标签。

  9. 完成后关闭问题。被分配该问题的开发者完成了他们的研究,并将结果发布到问题的讨论区。然后他们关闭了该问题,表示该问题已无进一步工作需要完成。

这就是你对 GitLab 问题的介绍。你已经了解了在问题中可以描述的工作内容、问题中包含的数据以及在处理问题时可能采用的工作流程。没有创建和使用大量问题,很难成为一个高效的 GitLab 用户,因此在熟悉 GitLab 的过程中,练习创建、查看和编辑问题是一个很好的方法。

安全地编辑文件,包括提交、分支和合并请求

在上一章节中,你了解了如何使用 Git 中的分支和提交,其中分支是一系列提交,而提交是由一个或多个文件的编辑所组成的快照。因为从某些角度看,GitLab 是 Git 仓库的一个封装(尽管它远不止如此),所以分支和提交也是使用 GitLab 的重要组成部分。在 GitLab 中,还有一个你会频繁使用的相关概念:合并请求(通常简称为MR)。在本节中,我们将解释什么是 MR,并向你展示如何在 GitLab 中操作这三者。

GitLab 通常提供多种方法来完成同一件事,在操作提交和分支时也不例外。你可以通过在终端中输入命令,或者使用 GitLab 图形界面(GUI)来执行你需要的大多数操作。由于 MR(合并请求)是 GitLab 特有的概念,而不是 Git 的一部分,你会发现 MR 需要使用 GitLab GUI。

在你能将编辑提交到分支之前,你需要先创建该分支。回顾上一章节,你会记得你可以使用 git branch <BRANCH-NAME> 命令创建分支,随后使用某种形式的 git push 命令将分支复制到项目仓库的“金本副本”中。或者,你也可以通过 GitLab 的 GUI 在 GitLab 中创建一个新分支,然后使用 git fetch 和某种形式的 git pull 将该分支复制到你本地的仓库副本中(前提是你有本地副本)。由于确切的命令依赖于你的情况,你应该参考你最喜爱的 Git 专门书籍来获得完整的信息。

尽管通常我们不会详细指导你如何使用 GitLab GUI,但创建分支、提交和 MR 是有效使用 GitLab 的基础,因此我们将概述如何通过 GUI 操作这些内容。

让我们从创建一个分支开始。因为分支是 Git 仓库的一部分,而 GitLab 项目不过是一个附加了大量额外功能的 Git 仓库,所以在 GitLab 项目中创建分支是合乎逻辑的。例如,假设你想为《Hats for Cats》安卓应用添加一个allow-password-change分支,以便你的开发人员能够添加一个让用户管理密码的功能。

下面是如何添加分支的方法:

  1. 在你的组结构中导航并打开Android项目。

  2. 在页面左侧的导航面板中,点击仓库 > 分支。这将带你进入项目仓库中存在的分支列表。

  3. 点击新建分支按钮,填写分支名称,点击创建分支按钮,完成:

图 3.5 – 项目中的分支列表

图 3.5 – 项目中的分支列表

创建分支后,你可以提交编辑。再次提醒,这有两种方式可以做到。你已经了解了在终端中使用的三条命令:git add <FILE-NAME>,然后是git commit --message "<MESSAGE>",再接着是git push。不过,如果你更愿意在 GitLab 内操作,下面是你需要做的:

  1. 通过点击左侧导航栏中的Repository > Files,导航到项目的代码库。

  2. 确保你正在正确的分支上工作,方法是点击页面左上方的分支名称下拉菜单:

图 3.6 – 在项目中选择分支

图 3.6 – 在项目中选择分支

  1. 在代码库的文件列表中,点击你想编辑的文件名,这样就会显示该文件的内容。

  2. 点击Edit in Web IDE以打开浏览器中的编辑器,并对文件进行必要的更改。

  3. 如果你想编辑此提交中的更多文件,点击页面左侧文件浏览器中的下一个文件名,并对其内容进行任何你想要的编辑。

  4. 当你完成更改后,点击Commit…并输入提交信息。如果你还没有为该分支创建合并请求,通常建议勾选Start a new merge request复选框。点击Commit,你就完成了。

  5. 如果你已经将代码库克隆到本地机器上,你可能想使用git checkout <BRANCH-NAME>,然后执行git pull,将你刚才做的提交复制到本地代码库,但你通常可以选择偶尔这样做,而不是每次提交后都这么做。

图 3.7 – 提交你的编辑

图 3.7 – 提交你的编辑

直接在 GitLab 中编辑文件并将更改提交到你项目的代码库,而不需要触碰终端,这是使用 GitLab 的一大乐趣,因此值得多加练习,直到它成为你正常工作流程的一部分。

创建分支对你没什么帮助,除非你能在它们之间切换,这样你可以查看或编辑任何你想要的分支内容。你已经学会了如何使用命令行通过git checkout <BRANCH-NAME>git switch <BRANCH-NAME>来完成这件事。在图形用户界面中切换分支同样简单:只需找到许多页面左上角存在的分支下拉菜单,切换到你喜欢的分支即可。有时容易忘记你当前所在的分支,因此养成时常检查此下拉菜单的习惯是个好主意,帮助自己保持方向感。

提示

这可能不言而喻,但我们还是要说一下:使用终端命令切换分支只会改变你本地仓库中所在的分支,而不会改变你在 GitLab 上托管的仓库中的分支。同样,在 GitLab 图形界面中切换分支只会改变你在 GitLab 托管的仓库中所在的分支,而不会改变你在本地仓库中的分支。因此,不要在一个位置切换分支后就认为你在另一个位置也切换了;本地和远程仓库中的分支是完全独立的。

提交历史记录

在 Git 仓库中最常见的操作之一是查看特定分支上提交的历史记录,如图 3.8所示。正如你在 第二章中学到的那样,在终端中运行git log会显示你所在分支的所有提交的反向时间顺序历史记录,包括每次提交的作者、时间戳、安全哈希算法SHA)和提交信息。你也可以在 GitLab 图形界面中做同样的事情,只需导航到项目的主页,选择你感兴趣的分支(从分支下拉菜单中),然后点击历史记录按钮:

图 3.8 – 分支上的提交列表

图 3.8 – 分支上的提交列表

历史记录是该分支上提交的列表,最新的提交在列表的顶部。这个列表包含与git log命令相同的每个提交信息。使用这样的图形界面一个很好的附加功能是,你可以点击列表中的任何提交,查看每个文件中所做的所有编辑,以易于阅读的并排格式显示(如果你觉得这种格式更容易查看,也可以切换为内联格式)。当然,你也可以在终端中使用git diff命令获得相同的信息,但终端输出远没有 GitLab 图形界面中的输出那么容易阅读。

合并一个 Git 分支到另一个分支

在 GitLab 图形界面中,将一个分支合并到另一个分支是与终端中的操作显著不同的第一个操作。如你所记得,你可以通过命令行使用git checkout main,然后执行git merge branch-a,将branch-a合并到main(以两个示例分支名称为例)。但是,在 GitLab 图形界面中执行相同的操作需要一个合并请求(MR)。这是 GitLab 中最重要且最常用的部分之一,因此理解并练习这一操作非常关键。需要理解的是,合并请求是从 GitLab 图形界面中将一个分支合并到另一个分支的唯一方法。以下是你某个 Hats for Cats 项目中的示例合并请求:

图 3.9 – 示例合并请求

图 3.9 – 示例合并请求

合并请求

合并请求就是字面意思:GitLab 中的一个组件,表示某人(可能是你,也可能是别人)请求将一个分支合并到 GitLab 实例的仓库金本副本中的另一个分支。合并请求看起来很像一个问题,它包含了许多相同的字段,包括标题、描述、受指派人以及线程式讨论。

但是,合并请求会添加一些在问题中不存在的额外字段。这些字段包括 branch-amain,其中源分支为 branch-a,目标分支为 main

合并请求还会显示源分支上的 Git 提交,并将每个提交的所有修改收集到合并请求中的单个页面内。这让你能够精确看到将源分支合并到目标分支时会如何影响目标分支中的文件。

合并请求的另一个特点是,它们包含一个特殊面板,显示 CI/CD 管道对分支中的代码进行的自动化测试和扫描结果。我们稍后会更详细地讨论这个面板,但你可以将其视为显示你正在开发的代码总体状态的“一站式商店”。它是否按预期工作?是否存在安全漏洞?是否引入了使用不被接受的软件许可证的依赖?简而言之,通过查看这个面板,你可以快速判断你在该分支上进行的工作是让整体软件产品变得更好还是更差。这无疑是一个非常实用的功能!

最后,合并请求中包含一个大而显眼的 Merge 按钮。合并请求的任务是将一个分支的提交合并到另一个分支,所以如果没有提供执行合并的方式,这个按钮就没有意义。这个按钮可能因为许多原因变成灰色并无法点击,从而阻止合并的进行。

以下是一些合并请求被阻止的原因示例。请注意,许多这些阻止行为是可以配置的,因此你可以决定最适合你团队的配置:

  • 合并请求标题以 Draft 开头。这表明与合并请求关联的分支仍在进行中,开发者不打算立即合并。

  • GitLab 的许可证扫描器(稍后章节会详细介绍)会检测到合并请求引入了一个与整体项目许可证不兼容的依赖。

  • 自动化测试未通过与合并请求关联的分支最近一次提交的测试。

  • 合并请求中的一个或多个讨论线程未解决。

  • 合并请求没有获得足够的批准,或者没有得到合适人员的批准,无法满足审批规则。我们将在接下来的内容中更详细地描述这一点。

审查和批准合并请求中的代码

由于合并请求有修改目标分支中文件的权限,而目标分支几乎总是 mainmaster 或包含稳定、可生产的代码的分支,因此每个合并请求都必须经过开发团队成员的严格审查。幸运的是,合并请求有多个功能来支持代码审查:

  • 你可以将评论直接链接到文件中的一行或多行,这样当你提出改进建议(或表扬时),就能清楚地表明你指的是哪些代码行。

  • 你可以在讨论区直接提议替代代码。这些提议甚至包括一个按钮,允许原作者通过图形用户界面一键接受你的建议。

  • 你可以指定评审人员来自你的团队。他们会通过电子邮件收到通知,告诉他们你希望他们审查合并请求中的修改。这些评审人员会被列在合并请求的元数据字段中,团队中的每个人都知道是谁被要求审查文件。

  • 团队成员可以批准你的代码。这是一个与审查代码不同的概念。审查通常是一个重复的过程,包括审查、原作者根据审查进行修复、再次审查、再次修复,以此类推。批准是一个单一的、一次性的“点赞”,意味着批准者认为你的修改已经准备好合并。

  • 你可以创建谁必须批准你的合并请求的规则,才能使其被合并。这些规则可能变得相当复杂,涉及多个团队或人员。以下是三个示例规则,规定了谁必须批准合并请求才能解除阻止:

    • 规则 1:你的团队的技术负责人或架构师

    • 规则 2:你的开发团队经理

    • 规则 3:质量保证团队的任何一位成员,加上安全团队的两位成员中的一位,再加上三位架构师中的两位

如你所见,合并请求对于让你的修改得到审查、批准并合并到正确的分支至关重要。

在提交代码之前创建合并请求

创建合并请求有多种方式,包括 GitLab 在一些可能出乎意料(但很有帮助)的位置提供的快捷方式。正如我们在描述其他 GitLab 组件时所做的,我们会要求你查看官方的 GitLab 文档,以获取如何使用图形用户界面创建、查看和管理合并请求的最新指南。

然而,我们确实需要为你提供一些关于何时创建合并请求的指导。这个听起来可能有些奇怪或违反直觉,但我们建议你在创建分支后立即创建合并请求,而在你提交任何代码之前

如果你曾使用过像 GitHub 这样的工具,这些建议可能听起来特别奇怪。GitHub 的合并请求(称为拉取请求)通常是在你提交了所有计划放到分支上的代码之后才创建的。毕竟,如果合并请求的目的是将源分支的修改合并到目标分支,那么当源分支上没有任何代码可以合并时,打开合并请求又有什么意义呢?

将合并请求作为代码的仪表盘

在工作流程中尽早创建合并请求的做法在 GitLab 用户中之所以被广泛接受,原因有两个。首先,合并请求充当“仪表盘”,让你了解你正在向分支添加的代码的整体质量。这个仪表盘回答了以下类似的问题:

  • 自动化测试是否通过?

  • 你的代码是否满足性能要求?

  • 你的代码是否引入了任何安全漏洞?

  • 如果它添加了任何新的第三方依赖项,它们的许可证是否与整体项目许可证兼容?

  • 你的代码是否满足风格和质量要求?

  • 如果你将应用程序作为 Docker 镜像发布,那么你的代码所打包的基础 Docker 镜像中是否存在已知的安全漏洞?

这些结果在 GitLab 界面的其他部分也可以查看,但在合并请求中整齐地呈现所有结果非常方便。

更重要的是,当你在合并请求中查看结果时,你会看到这些结果的“差异”视图。换句话说,合并请求中的结果将告诉你该合并请求关联分支的测试和扫描结果与对默认分支进行相同测试和扫描的结果有何不同。这一点非常宝贵,因为它能让你知道你贡献到分支上的代码是否朝着正确的方向发展。简单来说,你提交到分支的代码是让软件变得更好还是更糟?

当然,如果你等到所有编码完成后再创建合并请求,你就无法从这种关于你提交是否走在正确轨道上的持续指导中受益。单凭这一点,就足以成为在提交任何代码到分支之前创建合并请求的一个重要理由。

合并请求改善协作

但是,在开发流程中尽早创建合并请求(MR)还有另一个重要的原因:合并请求鼓励团队成员之间的协作。通过提供一个讨论区域,它们让你的同事可以审查并评论你所做的每一个提交。你是否引入了微妙的 bug?你的同事可以在它们还容易修复的时候发现并提醒你。你是否开始编写一个不如其他方案高效的算法?有人可以在你投入大量时间和代码之前告诉你。你是否在编程语言中误用了某些习惯用法?如果一位资深开发人员能在过程早期指出这一点,你就可以在将更多代码提交到分支之前调整你的风格。

这些场景有一个共同的主题:通过早期并频繁的协作,通过查看在单个提交中到达的小段代码,问题变得更容易发现,也更便宜修复。MR 正是这种协作发生的地方。

任何被要求审核一个包含 3,000 行代码的已完成特性的人,都会有一种不知从何开始的沉重感觉。或者,当你意识到开发人员误解了规格,未能构建产品负责人期望的功能时,所感到的绝望。又或者,当你指出开发人员在整个功能代码中犯了数百次编程或风格错误时,那种尴尬的感觉。这些情况都可以通过频繁的查看小段代码来避免。而且,这只有在你准备好合并请求并在第一次提交前就已准备好时才有可能。

合并请求所促成的频繁协作,不仅帮助审查代码的人,也帮助了代码的作者。就像自动化测试失败时,只有导致失败的小段代码才容易排查和修复一样,当代码审查员在 12 行代码中发现样式问题、次优算法或 bug 时,比在由 3,000 行代码构成的已完成特性或 bug 修复中发现这些问题要容易得多。早期调整编码实践远比在你认为代码已经完成后,再回去进行潜在复杂或甚至破坏性修复要好得多,也容易得多。

这些原则同样适用于安全漏洞。软件开发中的一条常识是,必须在整个开发流程中考虑安全;你不能仅仅在最后加上安全措施。在合并请求中频繁出现的安全扫描结果帮助我们遵循这一原则,通过从开发流程一开始就“融入”安全,确保了安全性得到重视。经验丰富的开发人员或安全团队成员进行的代码审查也有助于实现这一目标,而 MR 讨论窗格正是进行这种代码审查的地方。我们在本书中早些时候提到的“向左移动”原则——尤其在安全方面显得尤为重要。因为安全问题有时需要对软件的基本架构决策进行广泛的重新思考和修改。此类修复在代码库较小且简单时会更加容易、便宜且不那么具有破坏性,而这正是新特性开发刚开始时的情况。

三个好伙伴——问题、分支和合并请求

你现在已经了解了很多关于问题、分支和合并请求的知识。重要的是要理解,当这三个 GitLab 组件用来规划和完成编程任务时,它们是密切相关的。

GitLab 推荐这三部分的特定工作流。他们建议,在确定需要完成的工作后,首先创建一个问题。问题一旦分配给开发人员,开发人员应该立即创建一个分支来进行工作,然后为该分支创建一个合并请求。问题、分支和合并请求应该有相似的(或有时甚至是相同的)标题,以表明它们相互关联。例如,如果你看到这些组件,你就可以从它们的标题中知道它们都在处理同一任务:

  • simplify the login process

  • simplify-login-process

  • simplify the login process

因为问题、分支和合并请求是密切相关的,并且在使用 GitLab 完成任何编码工作时通常需要这三者,因此有时会将它们称为三剑客。如果你不确定如何开始一个编程任务,一个好的经验法则是,在写任何代码之前,确保你已经准备好三剑客。

当两个剑客就足够时

然而,也值得提到的是,并不是每个任务都必须有三个组成部分。如果一个任务不需要你编辑任何存储库中的文件,那么就不需要创建分支,这也就意味着不需要合并请求。你可能还记得,我们之前提到过,一个问题的可能用例是为即将到来的公司活动征集 T 恤设计。这些 T 恤设计可以直接添加到问题的讨论区。你不需要编辑任何文件来满足该任务的要求,因此你可以不创建分支,也不创建相关的合并请求。事实上,创建分支和合并请求问题可能会让你的同事感到困惑,因为它暗示着你确实期望在完成这个工作时编辑文件。

类似地,有些情况下你只需要一个分支和合并请求,但不需要问题。例如,假设你需要修复代码中的一个小错误。你可以创建一个问题来描述这个问题,但对于如此小的编辑来说,这可能显得有些多余。在这种情况下,直接创建一个分支和合并请求,然后提交一个修复错误的提交,再请求审查和批准合并请求,似乎更加合适。大多数 GitLab 用户可能会同意,在这种情况下不需要问题(尽管创建一个问题也不会有什么坏处)。话虽如此,一些组织可能会决定每个合并请求都需要一个相关的问题,这也是完全可以接受的政策,尽管偶尔会带来一些额外的工作。

问题和合并请求有什么不同?

你可能会想知道问题和合并请求有何不同。我们已经讨论了合并请求包含的额外信息,但这两个组件之间还有一个哲学上的区别,记住这一点会很有帮助。可以把问题看作是用来呈现和讨论想法的地方。另一方面,合并请求是你呈现和讨论代码的地方。如果你用这个概念来区分二者,它将帮助你理解什么时候只需要其中一个,什么时候又需要两个都存在。这也有助于你理解是否你的任何评论更适合在问题中的讨论部分(如果你在谈论工作的总体想法)或合并请求中的讨论部分(如果你在谈论开发者提交的具体代码)。

问题和合并请求之间的另一个区别在于它们可以具有不同的状态值。问题可以是打开关闭的,而合并请求可以是打开关闭已合并的。事实上,关闭的合并请求相对较少,因为该状态仅在你放弃合并请求而不是完成合并时使用。尽管确实会发生这种情况,但合并请求更常见的结果是,一旦其关联的分支被合并,它就会转变为已合并状态。

现在,你已经学会了如何使用 GitLab GUI 来处理诸如提交、分支和合并请求等基本概念。你还了解到,问题和合并请求乍一看可能很相似,但在 GitLab 中扮演着重要而不同的角色。你明白了在合并任何修改之前创建合并请求的重要性,以及合并请求如何支持团队成员之间的紧密、频繁的合作。最后,你了解了问题、分支和合并请求这三个“好朋友”,并且明白它们是如何协作帮助你规划工作、完成工作以及合并由此产生的代码变更的。换句话说,即使你仍然不清楚 GitLab 在你编写完软件后如何验证、保障、打包和部署软件,你已经接触到了使用 GitLab 编写软件的所有基本构建模块。但我们很快就会涉及到这些内容,保证!

使用 GitLab flow 启用 DevOps 实践

让我们通过一个实际的例子来结束本章,看看问题、分支和合并请求是如何结合在一起的。这展示了 GitLab 推荐的最佳实践,说明如何在大多数情况下使用你已经接触过的所有组件,形成一个顺畅的工作流程。事实上,这个工作流程被如此强烈推荐,并且经过时间的验证,GitLab 甚至为这个工作流程命名了:GitLab flow。像往常一样,鼓励你将这个工作流程作为你开发自己流程和程序的起点;根据你的团队、产品和组织文化,随时可以根据需要进行调整。

在开发《Hats for Cats》网页应用时,你决定添加一个功能,让用户可以根据猫的品种来筛选帽子。毕竟,给大头的缅因猫戴上牛仔帽,可能会淹没掉德文雷克斯猫那娇小的脑袋。以下是 GitLab flow 中规定的所有步骤,用以实现这个功能:

  1. Filter hats by breed 中,保留所有元数据字段为空。

  2. 在问题的讨论区,你提到两个人,认为他们可能对这个功能是否是一个好主意有看法。

  3. 那两个人在讨论区添加了回复。一个只是留下了一个点赞表情符号。另一个表达了对这个想法的支持,但询问是否应该根据其他标准进行过滤。

  4. 你认为根据其他标准进行过滤是一个好主意,但你不确定应该选择哪些标准。于是,你创建了另一个问题,标题为 Question: what criteria should we use when filtering hats? 你将这个问题暂时搁置,继续集中精力处理 Filter hats by breed 问题,因为你确信品种应该是筛选标准之一。

  5. 在一次全体团队的规划会议中,团队决定为这个问题分配一个权重值 8,对你们团队来说,这意味着预计这是一个为期 1 周的任务。你将问题指派给名为 Elizabeth 的后端开发人员,并将其截止日期设置为从今天起 2 周后。

  6. Elizabeth 给问题添加了一个有范围的 Status::In Progress 标签和一个无范围的 Back-end 标签。这将帮助团队跟踪问题是否按计划进行,并了解谁对其负责。

  7. Elizabeth 创建了一个名为 filter-hats-by-breed 的临时分支来保存她的提交。

  8. Elizabeth 创建了一个标题为 Draft: Filter hats by breed 的合并请求。她将队友 Alice 和 Bob 指派为合并请求的审阅者。因为 Elizabeth 还没有在 MR 的分支中添加任何代码,所以他们暂时没有什么事情可做。

  9. 现在,Elizabeth 已经设置好了问题、分支和合并请求这三个要素,她开始编码。

  10. 在完成一个小的、可测试的代码块后,她将其提交到自己的分支。

  11. Elizabeth 查看了 MR,查看自动化测试、代码质量扫描、许可证扫描和安全扫描的结果,这些测试和扫描针对她的第一次提交进行了检查。结果没有报告任何问题,因此她用一杯加糖的阿萨姆茶庆祝。

  12. Alice 和 Bob 收到了电子邮件通知,得知 Elizabeth 已经向 MR 的分支提交了代码。他们查看了 MR 并审阅了她的更改。两人都添加了一些评论,说明他们喜欢她代码的哪些部分,以及哪些部分可以改进。

  13. Elizabeth 认为其中一些建议不正确,因此她在 MR 的讨论区添加了评论,解释她的观点。她继续讨论,直到大家达成一致,决定如何继续。Elizabeth 添加了一个包含达成共识后修复内容的新提交。

  14. 伊丽莎白再次查看 MR,看她最后一次提交的自动化测试和扫描结果。安全扫描指出了一个她无意间引入的漏洞。她迅速添加了一个修复漏洞的新提交。这次修复的代码再次运行扫描,这次一切顺利。

  15. 伊丽莎白得到了爱丽丝和鲍勃对她迄今为止提交的所有代码的点赞审核,所以她的工作完成了。她移除了后端标签,添加了前端标签,并重新将问题分配给了名为乔治的前端工程师。

  16. 乔治编写了一些前端代码,并向伊丽莎白使用的相同filter-by-breed分支添加了一些提交。每个提交触发一次自动化测试和扫描的新运行,并由爱丽丝和鲍勃审核。

  17. 乔治意识到工作进度落后,所以他给原始问题添加了有风险标签。开发经理通过指派另一名名为海伦的前端开发者来回应。

  18. 提交-审核-检查-自动化测试和扫描结果的周期继续进行几轮,直到乔治和海伦完成该功能。他们移除了有风险标签。爱丽丝和鲍勃对代码感到满意,并都在讨论中添加了点赞表情。

  19. 乔治从问题标题中移除了Draft:,表明他认为代码已准备好合并。

  20. 乔治在 MR 的讨论中提到安全和质量保证团队,以便他们可以批准。在他们批准之前,Web 项目的批准规则将阻止 MR 合并。

  21. 安全团队的一名成员和质量保证团队的两名成员将 MR 标记为“已批准”。这重新激活了 MR 的合并按钮。乔治感到非常高兴和成就感,移除了前端状态::进行中标签,并点击了 MR 的合并按钮。

  22. 整个团队一起去酒吧庆祝,吃了大量的披萨,感觉不舒服。

这里有一个流程图展示这个流程,但为了清晰起见,省略了一些步骤:

图 3.10 – GitLab 流程的主要步骤

图 3.10 – GitLab 流程的主要步骤

总结

你在本章节中接触到了大量的概念和术语,所以让我们进行一个快速回顾。

GitLab 是一个网络应用程序,其使命是解决与 SDLC 的 10 个阶段中的任何一个相关的许多问题。因此,GitLab 不仅解决一个问题,而是解决了软件开发许多不同方面存在的许多问题。

使用 GitLab 主要发生在一个 GitLab 项目中,该项目代表一个软件产品、组织结构的一部分或一个倡议。具有类似主题的项目可以在 GitLab 组内进行收集,而组还可以包含子组。

每个单独的任务或工作块都记录在 GitLab 问题中。问题描述了需要完成的工作,允许团队成员参与关于该问题的讨论,并包括多个字段以存储关于该问题的元数据。问题通常代表与软件相关的任务,但也可以(而且应该)用于描述、规划和跟踪非技术性的工作。

你可以创建作用域无作用域标签,以突出显示问题的状态或健康状况,或指示哪个人或团队负责处理该问题。你可以将标签分配给合并请求以及问题。

GitLab 合并请求是你用来在 GitLab 图形界面中将一个分支合并到另一个分支的组件。每个合并请求列出了源分支和目标分支,合并该请求将使所有仅存在于源分支上的提交被添加到目标分支中。合并请求看起来与问题类似,但它们的用途不同:前者用于描述、讨论和合并代码,后者用于描述和讨论想法及任务。

你还可以使用 GitLab 图形界面(GUI)执行许多常见的与 Git 相关的任务,除了管理合并请求。例如,你可以使用 GUI 创建分支、添加提交、显示分支上的提交列表或为提交分配标签。

GitLab 流程是使用所有 GitLab 组件以一种经过验证且可靠的方式构建软件的最佳实践工作流。你不需要强制使用 GitLab 流程,但它是一个很好的起点,帮助你确定哪种工作流和政策最适合你的组织或团队。

到目前为止,我们一直在绕着 GitLab 流程的一个核心部分转圈:CI/CD 流水线,它在你将代码提交到仓库后,运行无数不同的检查。在下一章中,我们将直接解决这一部分,深入探讨可能是 GitLab 最强大且最有帮助的功能。

第四章:理解 GitLab 的 CI/CD 管道结构

到现在为止,你已经掌握了足够的 Git 和 GitLab 概念,能够理解开发者如何在 软件开发生命周期SDLC)的 Create 阶段,使用这些工具创建、审查和存储代码。你也已经了解了在 Create 阶段之后紧接着的 VerifyPackageRelease 阶段所带来的一些问题。现在,是时候深入了解本书的核心内容了:GitLab 的 CI/CD 管道如何帮助解决那些 Verify、Package 和 Release 阶段的问题。

在本章中,你将学习什么是 持续集成CI)和 持续交付CD),以及它们如何融入 GitLab Flow。你将学习如何描述管道的不同部分,包括阶段和任务。你将看到这些部分是如何结合在一起的,以及代码是如何流动的。你将学会如何查看管道的整体状态,以及组成它的各个阶段和任务的状态。你将了解 GitLab 可以通过不同方式触发管道,以及为什么你可能希望限制管道的运行频率。最后,你将学习如何为你的 "Hats for Cats" 软件配置一个简单的 Hello World 风格管道。

一旦你对这些概念和实践有了足够的了解,你将打开通往强大 GitLab 功能的大门,这些功能通过管道进行启用和配置。当你到达这一点时,很可能你会成为一个忠实的 GitLab 用户,无法想象再回到其他 DevOps 工具。

在本章中,我们将涵盖以下主要内容:

  • 定义“管道”、“CI”和“CD”这几个术语

  • 管道的组成部分——阶段、任务和命令

  • 运行 GitLab CI/CD 管道

  • 阅读 GitLab CI/CD 管道状态

  • 配置 GitLab CI/CD 管道

技术要求

与上一章一样,如果你有一个 GitLab 实例的账户(无论是自托管还是 软件即服务SaaS)),并且能够登录并用来实践和实验本章讨论的概念,那么你将从本章中获得最大收益。

定义管道、CI 和 CD 这几个术语

由于 GitLab 的强大功能很大一部分来自于配置 CI/CD 管道来对你的代码进行各种操作,因此理解管道到底是什么至关重要。所以,讨论这个话题的一个显而易见的起点就是弄清楚我们所说的管道、CI 和 CD 到底指的是什么。我们还不会开始创建管道——那将在后面的章节中讲解。

理解什么是管道

GitLab CI/CD 管道是一系列在每次提交编辑到 GitLab 托管的仓库副本时执行的步骤。这句话包含了很多内容,所以让我们仔细看看它的每一部分。

“一系列步骤”是什么意思?你可以将这些步骤理解为对文件执行的任务。例如,你可能想对文件运行各种测试或扫描工具,以确保代码书写良好、没有安全漏洞、依赖项使用了合适的许可证,并且满足所有功能或性能要求。你可能还想将代码打包成某种可部署的格式,无论是 Ruby Gem、可安装的 Red Hat 包、Docker 镜像,还是其他任何类型的包。当然,你可能还需要一个步骤将代码部署到适当的环境中,无论是测试环境、预生产环境,还是项目的实际生产环境。

“你的文件”是什么意思?从技术角度讲,GitLab CI/CD 管道可以对 GitLab 项目代码库中包含的任何文件执行我们刚才描述的步骤:源代码文件、配置文件、README 文件以及测试数据文件。简而言之,你可以配置管道来检查、测试、打包、部署或以其他方式操作项目中的任何文件。管道步骤最常见的目标文件类型是源代码文件,但重要的是要记住,你可以配置管道对 GitLab 项目代码库中的几乎任何文件执行几乎任何任务。

为什么我们规定管道在“每次提交编辑时运行”?因为在绝大多数情况下,添加 Git 提交正是触发管道对文件运行的原因。GitLab 带来的 SDLC(软件开发生命周期)优势,只有在管道频繁运行且每次只针对少量文件变更时,才能得到实现。为了实现这一点,GitLab 的默认行为是在每次提交编辑后的文件到 GitLab 托管的项目代码库副本时运行完整的管道。提交并不是触发管道的唯一方式,尽管它是最常见的方式。在本章后面,你将学习如何手动启动管道运行,并了解如何防止管道在提交后运行。

最后,为什么指定管道仅适用于 GitLab 托管的代码库副本很重要?这是因为管道是 GitLab 的概念,而不是 Git 的概念。这意味着管道只能访问 GitLab 所知道的文件,也就是说,它只能访问存储在 GitLab 实例上的项目代码库副本中的文件。换句话说,如果你在本地代码库副本中编辑文件,GitLab 是看不到这些文件版本的(至少,在你通过git push同步它们到 GitLab 之前是如此),因此它不能对这些文件版本执行管道。再说一遍:管道只能作用于存储在你的 GitLab 实例上的文件版本

每个项目定义一个管道

每个项目只定义一个 GitLab CI/CD 流水线。然而,流水线中究竟发生了什么——它包含的任务——可能取决于各种因素,这导致流水线的不同运行之间“看起来不同”。例如,针对编辑后的源代码运行的流水线可能包括许多自动化测试和将代码打包成 Docker 镜像,而针对编辑后的文档运行的流水线可能涉及拼写检查和部署到 Web 服务器。但在这两种情况下,它依然是同一个流水线。只是这个单一流水线的某些“特性”可以根据不同的代码修改类型被开启或关闭。很容易看到来自同一个项目的两个流水线运行输出差异巨大,并认为它们是两个完全不同的流水线,但事实并非如此。每个 GitLab 项目只有一个 CI/CD 流水线。只是该流水线执行的内容在每次运行时可能会有所不同。

理解“流水线”一词的不同用法

“流水线”一词有时被使用得比较宽泛。最准确的理解方式是把项目的流水线看作是应用于项目文件的步骤系列的蓝图或配方。执行这些步骤从技术上来说并不是流水线,而是称为“流水线运行”或“流水线实例”。但人们经常将单个运行称为流水线。我们在本章中也会这样做:我们将经常使用更简短的“流水线”一词,尽管“流水线运行”或“流水线实例”从技术上讲会更准确。无论我们是在讨论流水线的蓝图还是流水线的单个实例,都应该从上下文中清楚地看出来。

一个项目可以同时运行多个流水线实例。如果你在几秒钟内进行两次提交,并且流水线步骤需要几分钟才能完成,那么你可能会有两个流水线实例在同时运行,分别针对这两个不同的提交。每个流水线运行将执行相同的步骤,但针对不同版本的文件。

查看流水线列表

使用 GitLab 构建软件时,你最常做的事情之一就是查看正在运行和已完成的流水线实例列表。按照前几章中建立的模式,我们将不太关注如何做到这一点(因为 GUI 可能会发生变化,而且官方的 GitLab 文档会始终提供最新的说明),而更多关注你为什么可能想要这样做。

GitLab 的流水线列表不仅告诉你所有流水线运行的通过/失败状态,还让你知道是否有任何流水线因某种原因“卡住”或无法运行。它显示每个流水线正在运行的代码版本、触发该流水线的提交或 Git 标签的提交信息、是谁进行了提交或标签、流水线何时开始以及运行了多久(如果它已完成)。

管道列表还提供了一个 GUI 控制项,允许你在管道运行过程中取消管道。某些复杂的管道可能需要几分钟(甚至可能需要几个小时)才能运行完成,如果你的项目可用的管道时间有限,你可能希望取消因一些微不足道的文件更改触发的管道,以节省时间。

最后,管道列表还提供了 GUI 控制项,允许重新运行任何管道。如果某个管道因你怀疑是间歇性网络问题而失败,你可能想要重新运行该管道。

这是一个包含 Hats for Cats 项目的管道列表——包括正在运行和已完成的管道。表格按时间倒序列出管道,最近的管道排在最上面。你可以看到,有两个管道运行已完成并显示“通过”状态,一个管道已完成并显示“失败”状态,另外两个最新的管道仍在运行。别担心每一行中显示的图标,它们表示管道各部分的状态,我们会在本章稍后详细讲解:

图 4.1 – Hats for Cats 项目管道运行列表

图 4.1 – Hats for Cats 项目管道运行列表

你已经了解了管道是什么,术语如何被宽泛使用,每个项目如何只定义一个管道,以及你可以从管道列表中获得哪些信息。现在,让我们解析一下 CI 和 CD 这两个术语,看看它们在应用于管道时意味着什么。

CI – 查明你的代码是否良好

尽管每个 GitLab 项目最多只能定义一个管道,但几乎每个管道都由两个部分组成:CI 部分和 CD 部分。管道的 CI 部分包含专门用于回答这个问题的步骤:你的 代码好吗?

CI 代表 持续集成。这不仅是一个 GitLab 术语,它是一个标准术语,定义为大多数软件公司所理解和认可。你可以将其视为管道的一部分,它确保你正在编辑的任何文件与你项目的稳定代码库能良好集成。换句话说,当你将功能或修复分支合并到默认分支时,是否会出现新的问题?在你工作时尽早发现这些问题,而不是等到合并后再发现,可以增加轻松解决这些问题的几率。

管道的 CI 部分通过在每次你将代码提交到 GitLab 托管的项目 Git 仓库时,运行测试、扫描和其他检查来完成这个任务。至少,这是 GitLab 管道的默认行为。你可以覆盖这种行为,以便管道不会在每个提交时运行,而且有一些合理的原因你可能希望这样做,我们稍后会讨论这些原因。

你已经看到一些与管道的 CI 相关步骤执行的测试和扫描有关的参考内容,作为复习,这里列出一个部分的可能的 CI 专注的管道步骤:

  • 功能测试:您的软件是否按预期工作?这一类别包括质量保证QA)团队花费大量时间编写的回归测试。

  • 安全扫描:您的代码是否引入了任何安全漏洞?

  • 代码质量扫描:您的代码是否遵循最佳实践,如类的长度、空格的使用和其他样式相关的考虑因素?

  • 性能测试:您的代码是否符合性能预期?

  • 许可证扫描:您所有代码的依赖项是否使用与主项目许可证兼容的软件许可证?

  • 模糊测试:通过传递异常长的字符串、超出预期范围的数字或其他奇怪或极端的数据作为输入,能否触发代码崩溃或异常错误?

因为 GitLab 为这些类型的检查提供了一流的支持,所以它们可以轻松启用在 CI 管道中。但是,您几乎可以将任何工具、扫描或检查集成到 GitLab 管道中。我们稍后会学习如何操作,但现在,您只需要知道的是,任何可以从命令行运行的工具——无论是商业的、开源的还是自制的——都可以添加到 GitLab 管道中。

CI 的好处

正如您所看到的,CI 的一个大优势是它实现了我们之前讨论的“向左转移”理念。您运行测试的越早,发现问题的时间就越早。问题发现得越早,修复它们的负担就越轻。将尽可能多的软件开发任务移到时间线的最前端,能带来巨大的回报。

CI 的另一个优势,特别是与 GitLab 透明的“单一视图”开发生命周期监控方法结合使用时,就是它促进了协作。例如,当安全测试频繁运行时,整个团队可以掌握项目安全状态。我们是否增加了不期望的漏洞,需要分配时间和人力来修复?开发人员是否需要调整他们的编码方法或架构,以减少下个月添加新功能时引入更多漏洞的可能性?如果整个团队——包括管理人员、开发人员、QA、UX、技术写作人员以及所有与产品相关的人员——都能看到项目的安全状态,他们就可以直接帮助解决问题,或根据自己的工作角色调整工作,来修复现有的安全问题或防止未来的安全问题。

这一协作原则不仅适用于安全问题,还适用于功能问题、性能问题、可用性问题或任何其他衡量软件质量的标准。CI 为团队中的每个人提供了对软件状态的了解,使得每个人都能根据自己的角色以适当的方式参与软件的构建或修复。

理解短语 CI 管道和 CD 管道

在本书中,我们有时会提到CI 流水线CD 流水线,但请记住,一个 GitLab 项目并没有单独的 CI 和 CD 流水线——它只有一个包含一些 CI 相关步骤和一些 CD 相关步骤的单一流水线。“CI 流水线”和“CD 流水线”这些表达方式仅仅是“项目单一流水线中的 CI 部分”和“项目单一流水线中的 CD 部分”这两个更加繁琐(但更准确)的短语的简化版。

CD – 找出你的代码应该去哪里(并将其放到那里)

CI这一术语有一个所有公司都使用的标准定义,CD的定义则更为模糊。GitLab 使用该术语来指代持续交付持续部署。稍后我们会讨论这两个术语之间的区别,但这两者都与决定代码应该部署到哪个环境,并实际进行该部署有关。

如果我们稍微谈一谈环境的概念,这将更加清楚。大多数软件开发团队都会设置多个环境来部署代码。这些环境有不同的目的:有些用于进行软件功能测试,有些用于性能测试,还有些模拟生产环境,以便在生产环境中出现问题之前识别并修复任何集成错误。当然,每个项目都会有一个生产环境,用于托管真实用户互动的代码。

使用 GitLab 开发的软件同样使用环境,GitLab CI/CD 流水线中的 CD 部分负责决定代码应该部署到哪个环境,然后将其部署到该环境中。根据你配置的项目流水线,执行此任务的操作会在决定部署位置时考虑多种因素。流水线最常用的因素是它们是在 Git 分支还是 Git 标签下运行,如果是分支,则考虑该分支的名称。

不同的公司使用不同的 Git 分支命名方案,但以下是 GitLab 流水线的 CD 部分决定项目代码部署位置的典型示例。请记住,尽管这是一个现实的示例,但它绝不是配置 CD 流水线的唯一方式:

  • 如果一个流水线在类似add-login-featurefix-password-bugremediate-cross-site-scripting-vuln的分支上运行,则将代码部署到审查环境进行测试(更多信息请参见下一节)。

  • 如果一个流水线在main分支上运行,则将该代码部署到暂存环境(有时称为预生产环境)。

  • 如果一个流水线在production分支上的 Git 标签下运行,则将代码部署到version-1-0version-12-2,并将其部署到所有计划向用户部署的提交。

理解审查环境

任何非平凡的软件项目至少需要一个测试环境。这是一个软件可以在开发过程中部署到的机器,QA 团队可以在一个安全的、沙盒式的环境中使用软件,以确保它满足功能需求。有些团队还有额外的、专门的测试环境,专门用于性能测试、负载测试、可扩展性测试或其他类型的测试。GitLab 为这些测试环境起了一个特别的名字:审查环境。每个非默认的 Git 分支都有一个专门为该分支提供的审查环境。一旦该分支合并到包含稳定代码库的默认分支中,GitLab 就会销毁不再需要的审查环境。

审查环境是 GitLab 最神奇的功能之一。你无需自己设置这些环境。每次你在 GitLab 托管的项目仓库中创建一个分支时,审查环境会神奇地出现,准备好供你的 CI/CD 流水线进行部署。当你完成分支的工作并删除它或将其合并到稳定的代码库中时,审查环境会神奇地消失。这真的是 GitLab 最棒、最有帮助的功能之一。

持续交付

我们已经说过,GitLab 对 CD 一词的含义之一是 持续交付。这意味着 GitLab 的 CI/CD 流水线会根据你配置的因素,自动将你的代码部署到正确的环境中。但是,有一个重要的例外:在持续交付的情况下,GitLab 不会自动将你的代码部署到生产环境中。相反,它提供了一个图形界面控制,要求一个人(通常是发布工程师)手动批准并触发部署到生产环境。这是一个最终的保护机制,防止你的团队将有缺陷的代码或错误版本的代码部署到实际用户那里。这是 GitLab 用户最常见的 CD 形式。

持续部署

CD 另一个含义是 持续部署。这与持续交付相同,唯一的区别是:它去除了最终的手动保护机制。持续部署会完全自动地将你的代码发送到生产环境,就像它将代码部署到任何其他环境一样。去除人为因素可能会被一些组织视为风险,但如果你拥有一个成熟、经过验证、值得信赖的 CI 流水线部分,你可能会觉得,任何通过测试、扫描和其他检查的代码都足够好,可以直接部署给客户。这对于那些对其 CI 流水线有高度信任的公司来说,可能是一个很好的节省时间的做法。

使用 CD 打包和部署代码

CD 管道——无论是实现持续交付还是持续部署——有时需要将项目的代码打包成可部署的形式才能进行部署。稍后我们会更具体地讨论这一点。现在,只需知道管道中的 CD 阶段可能涉及将 Java 代码打包成 WAR 或 EAR 文件,将 Ruby 代码打包成 Gem,将 C 代码打包成 Docker 镜像,收集所有项目文件成一个tarball,或者根据项目的语言和部署策略,将项目代码打包成任何最适合的形式。

当然,也有一些情况不需要打包。一些具有简单部署策略的项目可以部署一组松散的、未打包的文件。

无论你的 CD 管道是否打包项目的代码,它总是需要将软件发送到某个地方。这可能表现为将 Docker 镜像推送到一个仓库(无论是公共仓库还是由 GitLab 托管的仓库,我们稍后会了解更多),使用命令行工具将代码部署到 AWS 环境,或使用任何其他特定环境的部署技术。通常,这是项目管道中 CD 部分的最后一项任务(有时是唯一的任务)。

CD 的好处

回顾一下,CD 的目的是“让发布变得无聊”。如果你的 CD 管道每次提交代码时都会部署——无论是部署到审查环境、暂存环境还是生产环境——这有助于你更频繁地将代码发布给客户,减少变动,并降低风险。

当然,客户看不到你的 CD 管道部署到审查环境或暂存环境中的代码,但通过将代码部署到这些环境并在其中测试软件,你的团队可以在商业合理时更加自信地将代码发布到生产环境。这些非生产环境的部署可以看作是实际发布的试运行,帮助你频繁且小范围地向客户发布版本。这种方法让你能更快地向客户交付功能,让客户更早地对这些功能提供反馈,并减少因意外问题导致的回滚风险。

GitLab Runner

现在你已经理解了管道、持续集成和持续交付的高层概念,是时候简要介绍一个新且至关重要的概念,它使得管道成为可能:GitLab Runner。

正如你将在下一节中学到的那样,管道最终归结为一系列自动执行的 shell 命令,几乎不需要人工干预。了解这一点对于学习 CI/CD 管道至关重要,因此我们将用不同的表述重复这一点:CI/CD 管道只是由机器人执行的一系列命令,这些命令执行与构建、验证和部署软件相关的任务。

GitLab Runners 是执行这些命令的机器人。从技术上讲,GitLab Runner 是一个小程序,它接受来自 GitLab 实例的命令并执行。我们将在下一章讨论 GitLab Runners 在 CI/CD 管道中的角色,并解释如何安装和配置它们——以及如何判断你是否需要它们。我们不会在这里深入探讨,但理解 GitLab Runners 是管道的核心部分至关重要:它们是将你的 CI/CD 配置代码转化为实际执行任务的地方,这些任务包括构建、验证、安全性检查和部署代码。

为了激发你的兴趣,下面是 GitLab Runners 如何融入 GitLab CI/CD 架构的简要介绍。可以把管道看作依赖于三个组件:定义管道任务的 CI/CD 配置文件、在某个环境中执行这些任务的 GitLab Runner,以及管理和协调管道所有方面,并最终展示管道任务结果的 GitLab 实例。你可以将它们组合在一起如下所示:

图 4.2 – GitLab CI/CD 管道架构图

图 4.2 – GitLab CI/CD 管道架构图

你将在下一章学到更多关于 GitLab Runners 的各个方面,但这个简短的介绍应该足以帮助你理解本章其余的内容。

通过这些,我们可以总结出管道、CI、CD 和 GitLab Runner 的定义和讨论。正如你所看到的,CI/CD 管道是一系列自动执行的步骤,作用于你项目的 Git 仓库中的文件,每当你编辑这些文件时。这些管道通过频繁运行、针对小的文件变更,GitLab 使得早期发现问题、低成本修复问题,并频繁且低风险地将新代码部署到客户端变得更加容易。除了需要一些时间来处理外,管道几乎全是好处,没有什么理由不去运行它们。它们是你 GitLab 工作流中的关键部分,也是使用 GitLab 开发软件比不使用它时更容易、更高效的一个重要原因。

现在,让我们继续探讨管道的构成。管道由哪些组件组成,它们如何组合在一起,GitLab 又是如何展示管道中发生的事情的?

管道的组成部分——阶段、任务和命令

这就是 GitLab CI/CD 管道的总体框架——CI 部分和 CD 部分如何在同一个管道中有所不同,以及为什么管道在 SDLC 中占有如此重要的地位。让我们稍微放大一下,详细了解管道的结构。特别是,管道是如何通过阶段 和任务组合在一起的?

阶段

每个流水线由一个或多个阶段组成。阶段是与某一主题相关的流水线任务的集合。例如,这三个可能是最常用的阶段:

  • 构建:此阶段包含将源代码编译和/或打包成可部署格式的任务。

  • 测试:此阶段包含运行自动化测试、代码质量扫描和代码检查工具的任务,并可能包括安全扫描。

  • 部署:此阶段将你的代码发送到适当的环境,具体取决于流水线运行时所使用的 Git 分支或 Git 标签(以及其他可能的因素)。

这三个阶段使用得非常普遍,以至于 GitLab 默认会将它们添加到你的流水线中。当然,你可以通过添加、移除或替换阶段来覆盖此默认设置。不论最终使用哪些阶段,我们建议你始终明确地定义你的阶段,即使你使用的是三个默认阶段。这样做可能显得有些冗长,但我们发现它有助于提高可读性,帮助故障排除,并且避免未来的混淆。

你可以定义任意数量的阶段。对于非常简单的项目,你甚至可以只创建一个单一的阶段,构建一个简化的流水线。你可以根据自己的意愿命名阶段,并且可以在阶段名称中包含空格和其他几种标点符号。由于 GitLab 的 GUI 有时会截断过长的阶段名称,因此我们建议尽量将名称保持简短,同时不牺牲清晰度。

GitLab 没有办法检查阶段中包含的任务是否在主题上相关联。这是你的责任。这意味着你可以根据需要创建非常糟糕、凌乱的阶段。例如,你可以在一个名为部署文档的阶段中运行自动化回归测试,并且可以在一个名为准备测试环境的阶段中部署文档。你如何将流水线划分为不同阶段,以及在每个阶段中放置哪些任务,完全由你决定。然而,这种自由并非没有代价:偶尔审查你的阶段结构,并在需要时进行重构,以保持清晰和一致性,被视为一种最佳实践。

在 GitLab GUI 中查看阶段

记得我们在图 4.1中看到的流水线运行列表吗?如果你翻回去查看那张截图,你会注意到流水线运行列表中的每一行都有图标,显示每个阶段的通过/失败状态。以下是“猫咪帽子”流水线中构建、测试和部署阶段状态的放大视图:

图 4.3 – 流水线实例阶段的状态图标

图 4.3 – 流水线实例阶段的状态图标

在这个流水线实例中,前两个阶段通过了,第三个阶段失败了。从这个视图中,你无法看到阶段的名称,但如果你将鼠标悬停在某个阶段的状态图标上,阶段名称会显示出来。

如果管道中的 所有 阶段都通过,则管道的总体状态为 通过。在本例中,由于最后一个阶段失败,管道的总体状态为 失败

如果你需要了解每个阶段发生的更多细节,可以点击管道实例行左侧的状态图标(在本例中是红色的失败图标)。这会将你带到管道的放大视图,提供有关每个阶段的更多细节。以下是该视图的样子:

图 4.4 – 三个管道阶段的详细信息

图 4.4 – 三个管道阶段的详细信息

你可以看到 BuildTestDeploy 阶段被表示为列,每个阶段内发生的任务列在该阶段的列中。那么这些任务到底是什么呢?这正好为下一个话题做了一个完美的过渡:作业。

作业

在本章中,我们一直在讨论管道中发生的“任务”。现在,是时候介绍这些任务的正式名称了:GitLab 称它们为 作业。每个作业必须有一个描述其执行任务的名称。

你可以把作业看作是 GitLab CI/CD 管道中构建模块的下一级(即低于阶段)。每个阶段包含一个或多个作业,每个作业都包含在某个阶段中。

如果你再次查看前面的截图,你会看到 Build 阶段包含一个名为 build-job 的作业,Test 阶段包含一个名为 test-job 的作业,Deploy 阶段包含一个名为 deploy-job 的作业。

正如你从这些作业名称中可能猜到的那样,每个作业通常执行一个任务。例如,一个作业可能将你所有的 Java 源代码编译成类。另一个作业可能重置测试数据库中的数据。还有一个作业可能将 Docker 镜像推送到注册表。但和 GitLab 不验证你的阶段是否包含主题相关的作业一样,它也不会验证作业是否执行作业名称所暗示的任务。换句话说,你可以创建一个名为 compile-java 的作业,它删除自动化测试生成的多余文件,或者一个名为 deploy-to-production 的作业,它运行安全扫描器。因此,要小心命名作业,并定期检查作业名称,确保它们仍然准确且可读。

GitLab 无法验证的另一件事是每个作业是否执行单一任务。这意味着没有任何限制阻止你创建一个名为 test 的作业,该作业运行九个不同的自动化测试套件、三个性能测试和五个安全扫描器。当然,这违反了每个作业只执行一个任务的最佳实践,因此 GitLab 允许你创建范围广泛或狭窄的作业。

命令

让我们来讨论一下我们之前只是略微提及的一个话题:作业是如何执行任务的?答案就是 GitLab CI/CD 管道中的最终构建块:命令。每个作业包含一个或多个命令,允许作业 执行某些操作。

一个作业包含的命令,实际上就像人类可能在终端中输入的命令。只不过你可以把作业想象成一个机器人,它将命令输入到 Linux bash shell、macOS Zsh shell 或 Windows PowerShell 中,就像真人一样。以下是一些可能包含在 GitLab CI/CD 管道作业中的命令示例:

  • javac *.java 命令用于编译 Java 类

  • docker build --tag my_app:1.2 命令用于创建 Docker 镜像

  • mvn test 命令用于使用 Maven 构建工具触发自动化 Java 单元测试

再次强调,这些命令可以由人输入,也可以由 GitLab CI/CD 管道作业执行;结果是相同的。如果一个人使用管道作业中包含的所有相同命令,你将得到一个完全相同的管道。唯一的区别是人工运行的管道会慢得多(并且可能更容易出错)。

一个作业可以包含多个命令,以完成其任务。例如,如果一个作业负责清理环境,删除测试过程中生成的临时文件,它可能包含三个独立的命令:

  • rm -``f tmp/

  • rm *.tmp

  • rm -``f /tmp/test_files/

你也可以创建三个独立的作业,可能叫做 remove-files-1remove-files-2remove-files-3,但由于这些命令是紧密相关的,通常一起运行,大多数 GitLab 用户更倾向于将这三个命令包含在一个作业中。

将管道组件拼接在一起

现在你已经了解了阶段、作业和命令的基本概念,接下来我们来复习一下它们是如何结合在一起的:

  • 每个 GitLab CI/CD 管道由至少一个 阶段组成。阶段代表管道必须执行的任务类别。

  • 每个 阶段包含至少一个 作业。作业代表管道必须执行的单个任务。

  • 每个 作业 包含至少一个 命令,命令就是人类在 shell 中输入的,执行管道任务所需的具体内容。

很明显,不同的项目可能会定义非常不同的流水线阶段、任务和命令。但如果你查看足够多的项目流水线,你会注意到一些反复出现的模式。正如我们之前提到的,大多数流水线至少包含构建测试部署阶段,而且每个阶段中的任务通常是相似的(至少对于使用相同语言和构建工具的项目来说)。虽然这些核心阶段和任务相当常见,但大多数非平凡的软件项目会定义大量独特的任务,有时甚至是独特的阶段。其他项目可能会有类似或相同的需求,但通过不同的命令或将相同命令组织到不同的任务和/或阶段中来实现。看到团队设置 CI/CD 流水线的各种方式,正是使用 GitLab 的乐趣之一。

运行 GitLab CI/CD 流水线

每当一个项目的流水线运行时,它是在某个版本的项目文件上运行的。这意味着在流水线的 CI 部分,它仅对文件的一个版本进行自动化测试和扫描。然后,在 CD 部分,它会将该文件的相同版本部署到适当的环境中。你还会看到,这通常被描述为流水线“运行在”项目文件的某个版本上。

流水线的目的是检查你的代码状态——然后部署该代码——每次你对代码进行更改时。所以,在昨天版本的代码上运行项目的流水线可能会产生一组结果,而在今天的代码版本上运行流水线可能会产生非常不同的结果,尽管流水线包含相同的阶段、任务和命令。在昨天和今天之间,你可能已经添加了新的自动化测试,或者通过添加有缺陷的产品代码引入了新的测试失败,或者添加了一个存在安全漏洞的依赖。如果出现任何这种情况,两次流水线的运行将会产生关于代码质量的不同报告。

分支流水线

运行流水线最常见的方式是提交更改到一个分支。每当你这么做时,GitLab 会自动在该提交所包含的项目文件版本上运行流水线。在流水线实例列表中,你会看到该流水线实例的条目,其中显示了(除了其他信息外)分支名称和该分支中最新提交的 SHA。以下是一个示例:

图 4.5 – 运行在不同分支上的流水线,结果不同(分支名称已高亮显示)

图 4.5 – 运行在不同分支上的流水线,结果不同(分支名称已高亮显示)

在这个例子中,最近的流水线运行在add-login-feature分支上,倒数第二个运行在fix-password-bug分支上。这些分支可能在相同文件中包含非常不同的内容,或者一个分支可能包含另一个分支尚未拥有的新文件。这就解释了为什么在针对add-login-feature运行的流水线中测试阶段失败,而在针对fix-password-bug运行的流水线中测试阶段没有失败。

GitLab 还允许你手动对任何 Git 分支运行流水线,即使它不是你最后提交的分支。触发针对任意分支的流水线很简单:访问流水线列表,点击运行流水线按钮,选择你想要运行流水线的分支,然后点击下一个运行流水线按钮,如下图所示。在这个例子中,我们即将对add-login-feature分支运行流水线,但如果我们展开该分支名称的下拉框,你会看到它列出了 GitLab 托管的项目仓库中所有的分支:

图 4.6 – 手动对特定分支运行流水线(分支名称和触发按钮已被高亮显示)

图 4.6 – 手动对特定分支运行流水线(分支名称和触发按钮已被高亮显示)

Git 标签流水线

记得在第二章中,你可以为发布给客户的产品版本 3.1 的提交添加一个名为version-3-1的 Git 标签吗?GitLab 也允许你针对任意的 Git 标签(比如这个标签)运行流水线,即使该标签并不指向分支上的最后一次提交。只需告知 GitLab 使用与指向特定分支的手动触发流程相同的方式,运行一个针对任意标签的流水线。列出可用分支的下拉菜单中也会包含所有 Git 标签的条目,如下所示:

 图 4.7 – 手动对特定标签运行流水线(标签的名称已被高亮显示)

图 4.7 – 手动对特定标签运行流水线(标签的名称已被高亮显示)

其他类型的流水线

你刚刚看到如何对分支或 Git 标签运行流水线。还有三种其他类型的流水线你应该了解,虽然它们的使用频率低于分支或标签流水线:

  • 合并请求流水线会针对合并请求的源分支运行,每当对该分支进行提交时。

  • 合并结果管道 是特殊类型的合并请求管道。每当对源分支进行提交时,合并结果管道会针对合并请求的源分支临时合并到其目标分支上运行。请注意,这种类型的管道实际上并不合并这两个分支;它只是针对可能产生的文件集合运行管道。这是确保您的分支能够很好集成到稳定代码库中的一个很好的方法。

  • 合并列车 是一种特殊的合并结果管道。合并列车将多个合并请求排队,然后对队列中每个合并请求执行单独的并行合并结果管道。但是,与仅对一个合并请求的源分支和目标分支执行临时合并不同,合并列车对队列中当前合并请求之前的每个合并请求的源分支执行临时合并。这是确保多个分支在合并到快速变化的目标分支时能够很好集成的好方法。

这些备用管道类型不像标准的分支和标签管道那样经常使用。因为它们在概念上更难理解,并且因为它们需要您的一些额外配置,我们建议您查阅官方 GitLab 文档,以了解它们是否对您的项目有用,以及如何使它们运行起来。

跳过管道

尽管 GitLab 的 CI/CD 管道非常棒、强大,对于构建软件的任何人来说都是巨大的帮助,但有时不运行管道可能更合理。以下是一些例子:

  • 使用 GitLab 的 SaaS 版本(即托管在 gitlab.com 上的实例)的团队每月有限的计算时间分钟数来运行管道。如果分钟数不足,他们可能只想在最重要的提交上运行管道。

  • 如果您进行了一个您知道不会影响任何管道测试或扫描的微不足道的更改,并且您不需要立即部署它,可能不需要为该提交运行管道。此类情况包括向您的代码添加注释,轻微编辑 README 文件或修复 GUI 文本中的微小拼写错误。

  • 当您即将对同一分支进行几个小提交,并且认为所有提交都是低风险时,您可能希望等到所有提交都提交后再针对所有提交运行管道。但这应该谨慎使用:通过增加变更范围,您放弃了 GitLab 提供的“向左移动”的一些好处。

幸运的是,阻止提交触发管道运行很容易。只需在提交消息中的任何位置包含以下两个短语之一,GitLab 将在不对其运行管道的情况下执行提交:

  • [skip ci]

  • [ci skip]

这个管道暂停只适用于单个提交。下次你在该分支上提交时,如果没有包括两个跳过消息中的任何一个,管道将在新提交上恢复(当然,这将包括你在没有管道提交上所做的任何编辑)。

你现在已经了解了管道如何针对 Git 分支或 Git 标签进行运行,并且你还学习了其他一些更为专门且不常用的管道,它们是针对临时合并的分支运行的。你也明白了如何在提交编辑过的文件时自动触发管道,或者在任何时候手动触发,以便重新运行你的代码版本的扫描和检查。你甚至知道如何告诉 GitLab 跳过特定提交的管道,从而节省时间和计算资源。你可能不会在工作中使用所有这些触发技术和管道变体,但了解在特殊需求出现时可以使用的选项是很有益的。

当然,如果你无法找到管道的结果或不了解它报告的内容,管道就无法为你提供帮助。因此,在下一节中,我们将探讨如何查看和解释完成的管道提供的信息。

阅读 GitLab CI/CD 管道状态

每个管道实例不仅有一个通过/失败的状态,而且管道实例中的每个阶段也有一个通过/失败的状态,任何阶段中的每个作业也有一个通过/失败的状态。除了通过失败,还有更多可用的状态。以下是一些最常见的值:

  • 正在运行:管道、阶段或作业正在进行中。

  • 待处理:等待资源可用以启动作业。

  • 已跳过:当早期阶段失败时,所有后续阶段默认都会被跳过。

  • 已取消:用户可以在作业或管道运行时取消它。

图 4.3中,你看到了管道实例列表不仅显示每个管道实例的状态,还显示每个管道中的各个阶段的状态。在图 4.4中,你看到了如何放大查看单个管道实例,以查看管道各阶段内所有作业的状态。GitLab 允许你进一步放大,查看作业内包含的单个命令的输出,通过点击图 4.4中显示的其中一个作业图标。这种视图展示了 GitLab 在执行该作业时输入的命令以及这些命令产生的输出。

例如,以下截图显示了运行一系列 Python 单元测试的作业的命令和输出。你可以从输出中看到,两个测试通过了,一个失败了。通常,我们会向作业中添加更多逻辑,使其上传单元测试结果,这样 GitLab 就可以在其 GUI 中显示这些结果。但为了简化起见,本示例省略了这一步:

图 4.8 – 运行 Python 单元测试的作业(作业的命令和输出被突出显示)

图 4.8 – 运行 Python 单元测试的任务(任务的命令和输出已突出显示)

在 GitLab 的图形界面(GUI)中有多个地方可以查看管道、阶段和任务的状态。此外,还有几种不同的图形表示方式,展示了项目的管道结构及其中各个元素的状态。在你浏览 GitLab 时,这些图标和图示非常容易辨认,而且大多数图标可以通过悬停或点击查看更多关于该元素的信息。

到目前为止,在本章中我们已经介绍了什么是管道、它们如何帮助软件开发团队、它们的结构、如何运行管道以及如何解读结果。但是你可能已经注意到,我们还没有解释如何创建和配置管道。这是一个大话题,本书的后续章节将详细讲解。然而,我们也意识到,你现在可能有些迫不及待:既然你已经了解了这么多关于管道的知识,你可能迫切想在自己的 GitLab 实例中试一试!

不必担心。我们发现,学习 GitLab 最有效的方式是多次在不同的上下文中介绍它的功能和组件,并且每次都有不同的背景知识。考虑到这一点,这是一个非常好的机会,能让你快速了解如何为 "Hats for Cats" 设置一个简单的 CI/CD 管道。我们会快速浏览这些内容,但别担心——你将在后续章节中多次遇到这些概念。

配置 GitLab CI/CD 管道

我们提到过,你可以配置项目的 CI/CD 管道来定义它的阶段、任务和命令。那么,如何做呢?所有的 CI/CD 管道配置都在一个名为 .gitlab-ci.yml 的文件中进行,这个文件位于项目仓库的根目录下。浏览任何一个公开的 GitLab 项目,你一定会看到一个这个名字的文件,它决定了该项目管道中的操作。

每个 .gitlab-ci.yml 文件使用的是一种领域特定语言,包含关键字、值和一些语法结构。一些关键字用于定义阶段和阶段中的任务。其他关键字则配置任务在管道中执行不同的操作。还有一些关键字用于设置变量、指定 Docker 镜像或以其他方式影响整个管道。这个领域特定语言足够丰富,能让你在 CI/CD 管道中做几乎任何事情,但又不会过于复杂(至少当你有了一些编写和阅读 CI/CD 配置文件的经验后,应该不会感到困惑)。

.gitlab-ci.yml 文件中有大约 30 个可用的关键字。与其尝试记住每个关键字的详细信息和配置选项,我们建议你集中精力了解 CI/CD 管道的总体功能,然后根据需要学习相关关键字的细节。官方的 GitLab 文档是获取这些关键字信息的最佳来源,尤其是因为这些关键字会随着时间的推移而有所变化。

本书的其余部分将花费大量篇幅演示你可以通过这些关键字完成的一些关键 CI/CD 管道任务,因此现在是时候通过查看一个简洁的 .gitlab-ci.yml 文件,来浅尝 CI/CD 管道配置的水了。这个文件的内容将驱动一个实际的管道,尽管是一个简单的管道。让我们逐行分析它,并解释每一行的作用。

由于 .gitlab-ci.yml 文件使用 YAML 格式来存储结构化数据,因此现在是学习或复习非常简单的 YAML 语法的好时机。关于 YAML 的维基百科文章是获取相关信息的一个好地方。我们会在这里等你,直到你对使用 YAML 感到自信。

现在这一部分完成了,我们可以开始了。大多数 CI/CD 配置文件首先通过定义管道的阶段来开始。如果你没有定义任何阶段,管道将默认拥有 buildtestdeploy 阶段。如果你定义了阶段,这些阶段将替代 —— 而不是增加 —— 这三个默认阶段。对于这个简单的管道,我们只需要 buildtest 阶段,因此我们将在 hats-for-cats 项目仓库的根目录下,创建一个名为 .gitlab-ci.yml 的新文件来明确地定义它们:

stages:  - build   - test

在这个管道中,我们将有两个作业,每个作业位于我们刚才定义的两个阶段中的一个。假设这个项目是基于 Python 的,所以两个作业都将使用与 Python 相关的工具。在下一章中,我们将进一步解释 GitLab Runners 如何在 Docker 容器内运行作业。现在,你需要知道的是,我们可以在 CI/CD 配置文件中指定一个 Docker 镜像,以便作业在其中运行。在这种情况下,我们的两个作业都需要访问 Python 工具,因此我们将告诉管道为所有作业使用 Python Docker 镜像:

image: python:3.10

我们的第一个作业将运行 mypy,它是一个工具,用来确保 Python 源代码在函数和变量中使用正确的数据类型。这个任务可以合理地放在 buildtest 阶段,但为了让我们在这个阶段至少有一个作业,我们将它放在 build 阶段。下面是我们定义这个作业的方式:

data-type-check:  stage: build   script:    - pip install mypy     - mypy src/hats-for-cats.py

由于第一行的第一个单词不是 GitLab 识别的关键字,GitLab 假设它是一个新的作业名称要定义。这个名称可以使用空格而不是连字符,如果你更喜欢的话,但有时候这样做可能会让解析变得更加困难。

下一行将这个作业分配到 build 阶段。

第三行以script关键字开头,告诉 GitLab 我们即将列出该任务的命令。接下来的两行正是做了这件事:第一行运行命令,使用pip包管理器将mypy包安装到任务运行所在的 Python Docker 容器中。第二行运行刚刚安装的mypy命令,检查src/目录中的所有文件。如果mypy发现我们的代码在使用数据类型时存在问题,它将使该任务失败,从而导致所在的build阶段失败,进而导致整个流水线实例失败。

现在,让我们定义一个任务来运行自动化单元测试:

unit-tests:  stage: test   script:    - pip install pytest     - pytest test/ --junitxml=unit_test_results.xml   artifacts:    reports:      junit: unit_test_results.xml     when: always

由于第一行不是一个已知的关键字,GitLab 知道这是我们正在定义的新任务的名称。

第二行将该任务分配到test阶段。

script关键字后,我们为任务定义了两个命令。第一个命令安装了pytest包,第二个命令在test/目录中的任何单元测试上运行刚刚安装的pytest工具。此外,它还指定pytest应将单元测试结果输出到一个名为unit_test_results.xml的文件,该文件将采用 JUnit XML 格式。

artifacts关键字开头的部分允许 GitLab 在任务完成时保存单元测试结果文件,而不是将其丢弃。在 GitLab 术语中,任何由任务生成并随后保存的文件都称为制品理解这一点很重要:任何由任务生成但未声明为制品的文件会在任务完成后立即被删除。

这个示例中的artifacts部分使用的确切语法并不是特别重要,因为在需要时可以很容易地查阅 GitLab 文档。但在这里,我们告诉 GitLab,这个制品包含了 JUnit XML 格式的单元测试结果,这是一个行业标准格式,GitLab 需要这种格式才能在流水线详情页面的测试选项卡中导入并显示测试结果。

artifacts部分的最后一行告诉 GitLab,即使unit-tests任务失败,也要将结果文件保存为制品。如果有任何测试失败,任务的状态将为失败,但我们希望每次该任务运行时都显示测试结果,即使(或者特别是如果!)存在任何测试失败。

将之前列出的所有配置代码结合起来,完整的.gitlab-ci.yml文件如下所示:

stages:  - build   - test image: python:3.10 data-type-check:  stage: build   script:    - pip install mypy     - mypy src/hats-for-cats.py unit-tests:  stage: test   script:    - pip install pytest     - pytest test/ --junitxml=unit_test_results.xml   artifacts:    reports:      junit: unit_test_results.xml     when: always

以下截图展示了这个流水线完成后的详情页面。不要担心unit-tests任务的失败状态。只要它运行的任何测试失败,这种情况是预期的:

图 4.9 – 完成的流水线详情页面,验证 Python 数据类型并运行单元测试

图 4.9 – 完成的流水线详情页面,验证 Python 数据类型并运行单元测试

总结

现在你已经对 GitLab CI/CD 管道的目的和结构有了很好的理解,接下来让我们回顾一下本章中涵盖的概念。

管道是一系列在项目的 Git 仓库中的代码上执行的步骤。每个项目只有一个管道,尽管构成项目管道的各个步骤可以根据正在运行的 Git 分支或 Git 标签来执行或抑制。“管道”一词有时用来指代将在项目代码上执行的整体任务集合,而有时则指在特定版本的仓库文件上运行该管道的单个实例或运行。

管道中的 CI(持续集成)部分回答了这个问题,代码好吗? 它通常由一些自动化测试、安全扫描、许可证合规检查和代码质量检查的组合组成。管道中的 CI 步骤支持 向左移动 的理念,即确保在问题还容易且便宜修复时就能被发现。它还促进了软件开发团队所有成员之间的协作。

管道中的 CD(持续交付/部署)部分回答了这个问题,代码应该部署到哪个环境? 它还负责将代码实际部署到该环境中。将代码打包成可部署格式的过程也可以看作是 CD 部分管道的一部分。管道中的 CD 步骤促进了频繁、可预测、低风险的功能和 bug 修复版本发布到客户。

每个项目的管道由一个或多个阶段组成,其中一个阶段是执行一组共享相似主题的任务的集合,例如构建、测试或部署代码。每个阶段由一个或多个作业组成,每个作业由单个工作单元组成,例如编译 Java 类、运行自动化单元测试或将应用程序打包成 Docker 镜像。每个作业由一个或多个命令组成,这些命令是开发人员在终端中手动执行与管道作业相同工作时输入的 Shell 命令。最终,管道由 GitLab 自动输入一系列终端命令,记录这些作业的输出,并将结果展示给用户。

每次提交代码到你的仓库时,管道通常都会运行,因此无论是在特性分支、修复分支还是默认分支上,你总能获取到代码的最新状态。管道可以运行在 Git 分支或 Git 标签上,并可以自动或手动触发。还有一些更为特殊的管道形式,例如运行一个管道来检查如果你将一个分支合并到另一个分支时所产生的代码。

每个管道实例都有一个通过/失败状态(或其他几种不常见的状态之一)。管道中的每个阶段也有一个通过/失败状态,每个阶段中的每个作业也如此。任何管道、阶段或作业的状态都可以在 GitLab 图形界面中查看。

管道几乎可以执行任何人通过终端输入的任务。每个项目都使用一种特殊的领域特定语言,在一个名为.gitlab-ci.yml的文件中配置组成其管道的任务。

本书的其余部分大部分都用于解释你可以配置管道执行哪些任务,以及你可以使用哪些语法和关键字来做到这一点。但首先,我们需要介绍一个执行管道工作、或作为“机器人”输入你在 CI/CD 管道配置文件中定义的终端命令的工具。换句话说,现在是时候深入了解 GitLab Runners 了。

第二部分 使用 GitLab CI/CD 管道自动化 DevOps 阶段

本部分是本书的核心内容:你将学习如何使用 GitLab CI/CD 管道将软件开发生命周期中最常见的手动步骤替换为自动化的等效步骤。通过本部分的学习,你将了解如何设置基础设施以支持管道。你还将能够自信地配置管道,执行多个关键任务:通过运行质量扫描和功能测试验证代码,运行安全扫描以保护你的代码及其依赖项,通过自动运行标准构建和打包工具打包代码,并将代码自动部署到适当的环境中。

本节包含以下章节:

  • 第五章安装和配置 GitLab Runners

  • 第六章验证你的代码

  • 第七章保护你的代码

  • 第八章打包和部署你的代码

第五章:安装与配置 GitLab Runner

第四章 中,你了解了 GitLab CI/CD 的基本概念。我们定义并介绍了与 CI/CD 流水线相关的词汇和概念,其中包括 CI/CD 流水线的组成部分、不同的流水线类型、如何在 GitLab UI 中观察和与流水线交互,以及如何使用 .gitlab-ci.yml 文件编写流水线配置。几段文字还介绍了 GitLab Runner 作为 GitLab CI/CD 的关键组件,它实际上运行流水线任务并将结果报告回 GitLab。

本章的唯一重点将是 GitLab Runner。你将在本章中了解到,GitLab Runner 在 CI/CD 过程中充当了“肌肉”的角色。Runner 是一些与 GitLab 主应用程序分开安装的小程序,它们的目的是接收 GitLab 发布的新的 CI/CD 任务,并按照 .gitlab-ci.yml 文件中指定的指令执行任务。Runner 可以安装并配置为与多种类型的基础设施一起使用,包括独立服务器、虚拟机、容器等。

我们将首先介绍 Runner 架构,并将 GitLab Runner 与你可能熟悉的其他工具进行比较。接下来,我们将描述如何安装和配置 Runner,使其能够与 GitLab 配对以运行 CI/CD 任务。最后,我们将讨论在不同情况下使用不同 Runner 类型的最佳实践。

一旦你学会了如何安装、配置、使用和维护 GitLab Runner,你就能顺利管理应用程序的构建、测试和部署的端到端生命周期。本章将介绍以下主题:

  • 定义 GitLab Runner 及其与 CI/CD 的关系

  • Runner 架构与支持的平台

  • 安装 Runner 代理

  • 配置并注册 Runner 到 GitLab

  • 理解何时以及为何使用不同类型的 Runner 和执行器

技术要求

与之前的章节一样,如果你在 GitLab 实例的软件即服务SaaS)或自托管环境中有一个账户,你将从本章中获得更多收益。此外,安装 GitLab Runner 代理需要一台计算机(Windows、Mac 或 Linux),在该计算机上安装 Runner 二进制文件。个人笔记本电脑也可以使用——Runner 轻量且系统要求较低。如果你使用的是 GitLab.com,CI/CD 流水线也可以通过 GitLab 的 SaaS Runner 来运行,不过请注意可能会产生使用费用。

定义 GitLab Runner 及其与 CI/CD 的关系

回想一下,GitLab CI/CD 是一系列针对你项目中的代码执行的任务,这些任务通常包括构建、测试和部署工作的组合。重要的是,CI/CD 流水线并不是在 GitLab 应用程序内部运行的,因为每个任务通常需要某些特定的平台和工具集才能成功运行。

注意

GitLab runners 是从 GitLab 接受 CI/CD 任务、在适当的执行环境中运行任务,并将结果报告回 GitLab 的程序。

GitLab Runner 是一个用 Go 语言编写的开源应用程序。

GitLab Runner 应用程序的官方仓库托管在 GitLab.com 上,项目名为 gitlab-runner。写作时,您可以访问 gitlab.com/gitlab-org/gitlab-runner 来查看该项目的开发和源代码。与主要的 GitLab 应用程序一样,GitLab Runner 遵循每月发布一次的版本发布周期。GitLab Runner 的最新版本通常与 GitLab 的主版本号和次版本号相同,但这并不总是保证的。与大多数 GitLab 一样,GitLab Runner 是开源的,并且根据 MIT 许可证进行分发。

查阅前述 URL 中的代码库,我们可以看到 GitLab Runner 是用 Go 编程语言编写的。该程序与大多数主要的计算机架构(x86、AMD64、ARM 等)和操作系统(Windows、macOS 和 Linux)兼容——实际上,只要支持安装 Go 二进制文件的地方,几乎都可以运行。我们很快会看到,安装 runner 可执行文件非常简单,且所需依赖很少。

GitLab Runner 执行在 .gitlab-ci.yml 文件中指定的 CI/CD 任务。

回顾上一章,GitLab CI/CD 流水线由在 .gitlab-ci.yml 文件中定义的阶段和任务组成。每个任务包含一组指令,通常是按顺序执行的 shell 风格命令。默认情况下,每当对一个定义了 .gitlab-ci.yml 文件的分支进行新的提交时,就会启动一个新的流水线运行。这意味着 .gitlab-ci.yml 中的任务将按照配置中指定的顺序和逻辑进行调度。

在 CI/CD 流水线运行过程中,当某个任务到达其“轮次”时,该任务将被分配给一个可用的 GitLab runner,该 runner 能够执行任务的指令。每个任务会分配给一个 runner。在 runner 从 GitLab 接收到任务后,它将首先执行的操作之一是获取启动流水线的提交记录,以便拥有相关的代码库快照。然后,runner 可能会执行编译构建、运行单元测试、安全扫描,或者将应用程序部署到某个环境中等步骤。请记住,runner 只是按照 .gitlab-ci.yml 文件中的任务指令进行操作。一旦 runner 完成任务中指定的工作,它将结果报告回 GitLab。报告几乎总是包括返回通过或失败状态,以及在任务执行过程中生成或修改的任何工件。

如在第四章(参见查看管道列表部分)开头讨论的那样,可以通过 GitLab 用户界面实时监控管道状态和作业执行。Runner 持续与 GitLab 通信,从用户界面可以查看可用的 Runner、修改 Runner 设置、干预管道和作业执行、查看上传的工件等。一种理解 GitLab CI/CD 组件之间关系的方式是将 Runner 看作是执行 .gitlab-ci.yml 指令的肌肉(大脑),而 GitLab 则像是神经系统,协调大脑(管道配置)和肌肉(执行 CI/CD 作业的 Runner)之间的通信。

Runner 架构和支持的平台

在进一步详细讨论 Runner 组件、安装和配置之前,值得澄清一些术语。到目前为止,本章中可能使用了GitLab Runnerrunner这两个词交替出现。然而,这两个词之间有些微妙的差别。GitLab Runner是指安装在计算机上的应用程序。安装 GitLab Runner 应用程序后,它尚未与 GitLab 通信或执行 CI/CD 作业。为了连接到 GitLab 并运行 CI/CD 作业,管理员需要执行一个 GitLab Runner 命令,该命令将单个runner注册到 GitLab,并指定这些 runner 将使用的执行环境。每个注册的 runner 将成为一个独立的进程,定期与 GitLab 联系并运行 CI/CD 作业。

这种设置一开始可能会显得有些混乱,因为单个 GitLab Runner 应用程序通常会支持在安装它的计算机上注册多个 Runner 进程。此外,这些 Runner 进程可能会使用不同的执行环境来运行它们的作业。例如,考虑一个单独的裸机 Linux 服务器。管理员可能会安装 GitLab Runner 应用程序,然后使用它注册以下内容:

  • 一个 Runner 进程,在服务器操作系统的 shell 会话中执行作业

  • 一个第二个 Runner 进程,在 Docker 容器中执行作业

  • 一个第三方 Runner 进程,将作业的命令通过 SSH 管道传送到另一台服务器

也就是说,安装在单台计算机上的一个 GitLab Runner 应用程序可以注册多个 Runner。GitLab Runner 应用程序负责启动、停止和管理各个 Runner 进程,并从 GitLab 获取 CI/CD 作业。图 5.1 总结了 GitLab、Runner 和执行作业负载的环境之间的信息流:

图 5.1 – GitLab Runner 通信与作业执行

图 5.1 – GitLab Runner 通信与作业执行

为了本章的目的,不必太担心此流程的细节。只需理解 runner 与 GitLab 通信,以便在关联的执行环境中接收和运行 CI/CD 作业。

GitLab Runner 支持大多数平台和架构

在撰写本书时,GitLab Runner 可以安装在每个主要的 Linux 发行版和架构上,以及 FreeBSD、Windows、macOS、Docker 和 Kubernetes 上。GitLab 还为那些因法律或内部合规要求需要它的组织提供了 FIPS 140-12 版 runner 二进制文件。

表 5.1 总结了截至 GitLab Runner 15.3 支持的架构和操作系统平台:

官方支持的计算机架构 官方支持的操作系统
x86 Debian
AMD64 Ubuntu
ARM CentOS
ARM64 Red Hat Enterprise Linux
s390x Fedora
ppx64le Linux Mint
Microsoft Windows
macOS
FreeBSD

表 5.1 – GitLab Runner 支持的平台

这张表列出了 GitLab 根据其文档明确支持的架构和操作系统平台。对于列出的 Linux 发行版,GitLab 提供官方的 GitLab Runner 包,可以通过该发行版的本地包管理器进行管理。然而,即使你的 Linux 发行版没有出现在表中,只要该系统具有兼容的计算机架构,通常也可以手动安装 GitLab Runner 二进制文件。

如前所述,GitLab Runner 还可以托管在容器或容器编排系统中,即分别是 Docker 和 Kubernetes。请注意,这指的是托管 GitLab Runner 代理本身,而不是其用于运行作业的执行器或执行环境。当我们稍后在本章详细讨论执行器时,我们将了解到,runner 可以被指示使用 Docker 或 Kubernetes 作为其执行器,无论 GitLab Runner 代理安装在哪里,只要它能够访问相关的容器工具。

Runners 可以是特定的、组级的或共享的

第三章 中,我们了解到 GitLab 中的工作是按项目和组来组织的。项目和组代表组织边界,如团队或产品线。项目通常(但不总是)托管一个包含源代码的 Git 仓库。组是容器,用来存放项目和其他组,类似于文件系统中文件夹的组织方式。

第四章随后介绍了 CI/CD 流水线。我们了解到 CI/CD 流水线是在项目内运行,并针对该项目的代码。那么,如何在 GitLab 中组织和分配跑者,使它们可供 CI/CD 流水线运行作业呢?事实证明,我们可以像组织其他 GitLab 资源一样组织跑者的可用性:通过将其设置为项目级、群组级或实例级。

特定跑者启用供个别项目使用

项目所有者和维护者可以选择仅为他们的项目注册跑者。特定跑者被分配给特定项目,仅会接收并运行该项目中 CI/CD 流水线的作业。

使用特定跑者有几个优势。首先是特定跑者使项目所有者和开发者能够设置他们所需的跑者基础设施,而无需更改他们所在项目之外的任何内容。例如,开发者可能会在他们的本地笔记本电脑上安装 GitLab Runner,并将特定跑者注册到他们作为主要贡献者的项目中。这样,开发者就不需要请求 IT 或平台所有者进行全局变更管理。项目特定的跑者可以在项目设置下进行管理,如图 5.2所示:

图 5.2 – 项目级跑者设置

图 5.2 – 项目级跑者设置

特定跑者的另一个优势是为个别项目提供专用或定制化的工具。特定跑者使得项目级资源使用的会计工作变得更简单。此外,安全和合规政策可能要求某些项目使用与组织其他部分隔离的专用基础设施。注册到特定项目的跑者只会运行该项目内部的流水线代码,GitLab 其他部分的流水线无法访问它。

群组跑者可供群组内所有项目使用

我们了解到,GitLab 中有些资源仅在项目中可用,有些资源仅在群组中可用,还有一些资源可以同时在项目和群组中使用。跑者就是这第三类资源的一个例子。在群组级注册跑者使得该跑者可以供群组内所有项目及其子群组的流水线使用。图 5.3 显示群组跑者可以在群组的 CI/CD 设置中注册:

图 5.3 – 群组跑者设置

图 5.3 – 群组跑者设置

群组所有者可以创建并管理群组跑者,群组跑者会按先进先出FIFO)顺序接收并运行 CI/CD 作业。群组跑者对于那些希望共享资源或运行跨多个项目的 CI/CD 流水线的团队非常有用,但这些团队仍然需要管理自己的跑者以满足会计或合规要求。

共享跑者可供 GitLab 中所有项目使用

GitLab 实例的管理员可以选择注册运行器,这些运行器可以从 GitLab 实例中任何项目的任何组获取 CI/CD 作业。这使平台所有者可以将运行器管理从开发人员或项目经理中抽象出来。实例管理员还可以在全局级别配置 CI/CD 配额,限制各个项目在可用的共享运行器上使用的 CI/CD 流水线分钟数。

共享运行器仅适用于自管理的 GitLab

管理员只能在自管理的 GitLab 实例上配置共享运行器。GitLab.com的客户可以选择使用 GitLab 提供的 SaaS 运行器,也可以注册自己的组或特定运行器。每个 GitLab 许可级别包含一定数量的 SaaS 运行器流水线分钟,额外的分钟可以购买。使用您自己的组或特定运行器永远不收费。

基于容器的平台,如 Kubernetes,是共享运行器常见的执行器选择,旨在提供可以快速扩展的临时资源。与基于 FIFO 原则挑选作业的组运行器不同,共享运行器通过公平使用队列进行操作。使用共享运行器的 CI/CD 作业较少的项目优先于作业更活跃的项目。这有助于确保来自单个项目的大规模流水线不会占用整个共享运行器基础设施。

每个运行器都有一个定义的执行器

让我们回顾一下到目前为止提到的一些组件:

  • GitLab 应用程序,调度和协调 CI/CD 流水线

  • GitLab Runner,安装在计算机上的二进制文件

  • 独立运行器,它们是运行 CI/CD 作业的进程,由 GitLab Runner 代理管理

如果你回顾图 5.1,你会注意到一个执行器组件,它接收作业负载并返回作业输出和状态。执行器指的是运行接收到的 CI/CD 作业的运行器进程所使用的环境。运行器的执行器在运行器首次注册到 GitLab 时指定。回想一下,可以从安装了 GitLab Runner 代理的单台计算机注册多个运行器进程,每个进程都有自己的执行器。表 5.2总结了支持的运行器执行器:

官方支持的 GitLab Runner 执行器
Docker
Shell
VirtualBox
Parallels
Kubernetes
Docker Machine
SSH
Custom

表 5.2 – GitLab Runner 支持的执行器

我们将依次描述每个执行器。

Docker 执行器

使用 Docker 执行器的运行器在从指定 Docker 镜像启动的 Docker 容器中运行 CI/CD 作业。这提供了一个可复现的环境,包含运行 CI/CD 作业所需的工具。使用 Docker 执行器要求在与 GitLab Runner 相同的计算机上安装 Docker 引擎。

注意

Docker 是 GitLab 用户中最常用的执行器。Docker 容器也是 GitLab.com 上共享 SaaS 运行器使用的默认环境。

Docker 执行器使得确保 CI/CD 任务拥有成功运行所需的工具变得容易。这些工具通过容器镜像提供,运行器被指示在任务中使用该镜像。任务使用的镜像可以在几个不同的位置进行指定:

  • .gitlab-ci.yml 文件中的任务定义内部

  • .gitlab-ci.yml 文件中全局设置,这样它会被用于流水线中的所有任务

  • 如果 .gitlab-ci.yml 文件中没有指定要使用的镜像,则作为运行器使用 Docker 执行器时的默认镜像

运行器使用的镜像可以位于本地的 GitLab 容器注册表、其他外部注册表,或像 Docker Hub 这样的公共容器注册表。例如,如果你的 CI/CD 任务需要一个包含 Python 工具的环境,你可以指示运行器从 Docker Hub 拉取 python:3.10 镜像,然后从该镜像启动一个容器来运行任务。任务完成后,运行器会删除该容器,直到收到新的任务,这时运行器会在一个新的容器中运行该任务。

Shell 执行器

Shell 执行器直接在安装了 GitLab Runner 的机器上通过 shell 会话运行任务。每个任务定义中的 .gitlab-ci.yml 文件中的 script 关键字的内容会像用户在终端中输入命令一样执行。Shell 执行器的主要优点是,因其使用了 GitLab Runner 安装所在的本地 shell 和文件系统,因此上手简单。

然而,Shell 执行器有一些挑战,使其难以扩展:

  • 首先,你需要在服务器上预先安装 CI/CD 任务所需的构建、测试或部署工具,以便 Shell 执行器能够访问这些工具。或者你需要在 script 关键字中指定步骤来安装所需的依赖项。

  • 第二个挑战是缺乏干净的环境来执行 CI/CD 任务。由于任务将在服务器的文件系统中直接执行,而不是在如容器这样的可重现环境中,因此容易留下构建和测试的残留物。

因此,尽管 Shell 执行器可能是你初次搭建流水线时最好的选择,但对于更复杂的构建环境,建议使用其他执行器。

VirtualBox 执行器

VirtualBox 执行器是一种为 CI/CD 作业提供可重现环境的方法,这些作业可能仍然需要完整的操作系统资源。该执行器只能在安装了 VirtualBox 虚拟化程序的计算机上使用。当您将 Runner 注册到 VirtualBox 执行器时,指定一个 虚拟机VM)模板,Runner 将使用该模板运行 CI/CD 作业。当 Runner 获取到一个作业时,它会从基本模板启动一个新的虚拟机,在该虚拟机上通过 shell 会话执行作业,将结果返回到 GitLab,然后销毁虚拟机。

虚拟机执行器虽然在确保清洁环境方面很有用,但除非作业需要访问运行在 Type-2 虚拟化程序上的操作系统,否则可能不需要使用。若您希望在没有虚拟机开销的情况下实现标准化,考虑使用 Docker 或 Kubernetes 执行器。

Parallels 执行器

Parallels 执行器的配置和运行方式与 VirtualBox 执行器相同,不同之处在于它使用 Parallels 虚拟化平台而非 VirtualBox。这样,您就可以在运行 macOS 的主机机器上运行 Windows 虚拟机中的 CI/CD 作业。

Kubernetes 执行器

当一个 Runner 注册到 Kubernetes 执行器时,它会在 Kubernetes 集群中的 Pod(即一个或多个容器的组合)中运行 CI/CD 作业。这自然需要您已设置好 Kubernetes 集群,并通过 Kubernetes API 让 Runner 与之连接。

在写作本文时,GitLab Runner 连接到 Kubernetes 集群的方式有几种不同的选择:

  • GitLab 提供了一个官方的 Helm chart,用于将 Runner 代理部署到集群中。

  • GitLab 还包括一种更广泛的方法,将 Kubernetes 集群连接到 GitLab 实例,称为 GitLab Agent for Kubernetes。一旦 GitLab 实例连接到集群,您就可以使用代理将带有 Kubernetes 执行器的 Runner 部署到集群中。

  • GitLab 正在积极开发一个名为 GitLab Operator 的工具,进一步自动化在 Kubernetes 中配置 GitLab 资源,使用如 Red Hat OpenShift 等容器管理平台。Operator 将提供另一种方法,将 Kubernetes 执行器与集群部署在一起。虽然目前尚不推荐在生产环境中使用,Operator 可用于有效地管理开发和测试环境中的资源。有关更多详细信息,请参考 GitLab 文档。

最终,成功地使用容器编排需要对网络、存储和安全有很高的知识和经验。如果您或您的团队拥有 Kubernetes 专业知识,Kubernetes 执行器可以成为实现和扩展云原生 CI/CD 工作流的强大方式。如果没有,最好还是坚持使用之前提到的执行器,如 Docker。

Docker Machine 执行器

Docker 执行器为每个 CI/CD 作业提供单独的 Docker 容器,而 Docker Machine 执行器则为整个拥有 Docker 引擎的主机(VMs)提供服务。这些主机本身支持启动 Docker 容器。Docker Machine 通常与具有自动扩展功能的云提供商一起使用,因此您可以根据需求快速灵活地启动与容器兼容的主机。

注意

Docker(公司)已不再积极开发 Docker Machine,而是支持 Docker Desktop。GitLab 维护 Docker Machine 的分支以继续支持 Docker Machine 执行器。

您可以将 Docker Machine 执行器视为 VirtualBox/Parallels 执行器和 Docker 执行器的组合,还包括额外的自动扩展支持。Docker Machine 还可以用于确保每个作业的隔离资源,通过确保容器在其专用的 VM 上运行。事实上,GitLab 在其自身的 Linux SaaS Runner 上使用 Docker Machine,为用户提供可扩展且在多租户平台上适当隔离的 Runner。

SSH 执行器

有时,您可能希望在一个基础设施上运行 CI/CD 作业,在该基础设施上,由于技术或合规性原因,无法安装 GitLab Runner。如果该基础设施支持从能够安装 GitLab Runner 的计算机进行 SSH 访问,您可以使用 SSH 执行器在远程主机上运行 CI/CD 作业。

当您使用 SSH 执行器注册 GitLab Runner 时,还将指定运行 CI/CD 作业的远程主机以及用于连接到该主机的 SSH 身份文件。当 Runner 接收到 CI/CD 作业时,它将通过 SSH “管道” 命令,以便在远程主机上执行它们。尽管 SSH 执行器目前仅支持 Bash 命令和脚本,但如果您不希望在每台希望运行 CI/CD 作业的机器上安装 GitLab Runner 程序,则可能会发现它很有用。

到目前为止,我们已经描述了 GitLab Runner 的主要组件:GitLab Runner 代理,其各自注册的 Runner 进程,以及每个 Runner 进程可能使用的执行器来运行其作业。还有一个值得讨论的 Runner 配置元素,那就是 Runner 标签。

Runner 标签限制了可以接收哪些作业的 Runner

apacherhelios。标签还可以表示 Runner 在 CI/CD 过程中某个阶段的预期用途,例如 buildstagingprod。当您在 CI/CD 作业定义中指定一个或多个标签时,可以确保 Runner 具有运行该作业所需的适当工具和环境。

例如,在 .gitlab-ci.yml 文件中的以下作业示例:

deploy-to-staging:
    stage: staging
    script: ./deploy-staging.sh
    tags:
        - windows
        - staging

在 CI/CD 作业定义中包含的 windowsstaging 标签,确保 deploy-to-staging 作业只会分配给同时具有 windowsstaging 标签的 runner。默认情况下,具有标签的 runner 不会运行没有标签的作业——即没有标签与某个特定标签 runner 匹配的作业。这个默认设置可以在 runner 的设置中覆盖,你可以允许带标签的 runner 运行没有标签的作业,因此也不关心它们运行在哪里。

Runner 标签 ≠ Git 标签

在 GitLab 中,“标签”这个词可能会让人感到困惑,因为这个术语在不同的上下文中都有使用。在本讨论中,“标签”仅仅是一个附加在 runner 上的标签,用于将其与具有相同标签的 CI/CD 作业匹配。这些标签与 Git 提交中使用的 Git 标签不同,后者是描述性标签,放置在 Git 提交中,也可以在 GitLab 中找到。Runner 标签与 Git 版本控制中使用的标签无关。

到目前为止,我们已经涵盖了有关 GitLab runner 的所有基本信息,包括它们的工作原理以及支持的不同平台和执行器。现在是时候开始讲解 runner 安装过程了。

安装 Runner 代理

如果你一边跟着操作,一边在自己的计算机上安装和注册 runner,那么本节内容将最为有用。你会发现,安装步骤会根据你的系统类型略有不同:Windows、macOS、具有支持的包管理器的 Linux 或通用 Linux 系统。无论平台如何,都是相同的两步流程:

  1. 安装 GitLab Runner 代理。

  2. 在 GitLab 中注册一个 runner。

安装 GitLab Runner

如前所述,安装方法会根据你的操作系统略有不同。对于主要的 Linux 发行版,文档(docs.gitlab.com/runner/install/linux-repository.html)会指导你将 runner 仓库添加到系统中,然后使用本地包管理器安装 gitlab-runner 包。对于 Windows、macOS 和其他 Linux 发行版,你将使用 curl 直接从 GitLab 获取程序,设置可执行权限,然后安装并启动 runner 代理。

以一台具有 x86_64 架构和基于 RPM 包管理系统的 Red Hat Linux Enterprise 服务器为例。GitLab 文档指导我们首先下载并执行一个 shell 脚本,该脚本会将 gitlab-runner 仓库添加到系统的包管理器中:

sudo curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash

如果你查看该 shell 脚本的内容,会发现它的作用只是检测操作系统平台,然后运行相关的包管理命令来添加 gitlab-runner 仓库。你可以通过检查可用仓库列表(在 RHEL 中使用 sudo dnf repolist)来验证此步骤是否完成。你应该能在列表中看到 gitlab-runner,与主要操作系统仓库一起列出。

我们已经添加了运行器库,但尚未安装 GitLab Runner。我们可以通过安装gitlab-runner包轻松完成此操作:

sudo dnf install –y gitlab-runner

安装完成后,GitLab Runner 应该会自动在后台启动(如果你在 Windows 或 macOS 上安装了 GitLab Runner,或者在 Linux 上手动安装了它,你需要手动启动 GitLab Runner)。你可以通过以下命令验证 GitLab Runner 代理是否已启动并正在运行:

sudo gitlab-runner status

你应该看到代理正在运行的确认信息,同时也会看到目前还没有任何运行器注册到 GitLab。注册运行器将是我们接下来的步骤。

在 GitLab 中注册运行器

到目前为止,我们已经在计算机上安装了 GitLab Runner 代理,并且它作为后台服务运行。但是,目前还没有任何运行器与 GitLab 进行通信。我们将通过注册一个或多个运行器来设置它们与 GitLab 进行通信并运行 CI/CD 任务。

回顾之前讨论的共享、特定和组运行器。当你在 GitLab 中注册运行器时,你将运行器绑定到整个 GitLab 实例(共享运行器)、一个组(组运行器)或一个项目(特定运行器)。注册说明和注册后的运行器设置将在你注册运行器的 GitLab 相应部分(实例、组或项目)中出现。例如,回顾图 5.2,我们可以在Hats for Cats项目中的设置 | CI/CD | 运行器下找到这些详细信息:

图 5.4 – 项目级运行器设置

图 5.4 – 项目级运行器设置

此外,请注意图 5.4中显示的注册令牌。该运行器注册令牌由 GitLab 生成,并用于运行器身份验证,确保它注册到 GitLab 中的正确区域:

  1. 如果你在演示系统中操作,请将注册令牌复制到剪贴板,因为在我们从安装了 GitLab Runner 的计算机注册运行器时需要使用它。

  2. 接下来,返回到安装了 GitLab Runner 的计算机。从终端会话中运行一个基于提示的运行器注册脚本:

    sudo gitlab-runner register
    
  3. GitLab 首先会提示你输入 GitLab 应用实例的 URL。对于 SaaS 版本,这将是gitlab.com。否则,它将是你用来访问自托管实例的 URL。在本示例中,我们将使用gitlab.com

    Enter the GitLab instance URL (for example, https://gitlab.com):
    
    https://gitlab.com
    
  4. 接下来,脚本将要求输入运行器注册令牌。此令牌显示在图 5.2中,并根据你注册运行器的项目或组而有所不同。换句话说,注册令牌用于验证运行器与 GitLab 的连接,确保它注册到正确的项目或组:

    Enter the registration token:
    
    GR1348941Xi_koNdj8AjJMjSzQyYY
    
  5. 然后,你可以提供一个可选的描述,该描述将在 GitLab UI 中显示在运行器的元数据中。此示例假设运行器是一个 Linux 服务器,开发人员可以在上面构建和测试他们的代码:

    Enter a description for the runner:
    
    [localhost] Linux dev server
    
  6. 下一个消息提示你输入任何可选的 runner 标签。回想一下,标签是你分配给 runner 的标签元数据。标签将 runner 宣传为能够接收具有相同标签的 CI/CD 作业。例如,构建作业可能会包含 rhel 标签,表示该作业需要由 Red Hat Linux 提供的工具。只有具有该标签的 runner 才能接收此作业。标签可以在 runner 注册时分配,如此处所示,也可以通过 GitLab UI 中的 runner 设置进行修改:

    Enter tags for the runner (comma-separated):
    
    dev,rhel
    

可选的维护备注是另一种描述性元数据,不会配置 runner 的行为:

Enter optional maintenance note for the runner
<leave blank in this example>

在脚本的这一点,新创建的 runner 进程将与 GitLab 通信,以确认它能够进行通信和身份验证:

Registering runner... succeeded                     runner=GR1348941Xi_koNdj
  1. 最后,runner 会询问它应使用哪个执行环境来运行 CI/CD 作业。另外,请记住,执行器取决于是否具备必要的工具;例如,选择 Docker 需要在服务器上安装并可用 Docker 引擎。在此示例中,我们将选择 Shell,因为它是最容易入门的执行器,且不需要依赖项:

    Enter an executor: parallels, docker-ssh+machine, kubernetes, custom, docker, docker-ssh, docker+machine, shell, ssh, virtualbox:
    
    Shell
    
  2. 脚本最终会确认 runner 注册成功。现在,runner 进程正在计算机上运行,由本地 GitLab Runner 代理管理,并准备从 GitLab 实例接收 CI/CD 作业:

    Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
    
    Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"
    

我们可以通过几种方式验证注册和通信是否成功。在安装 GitLab Runner 的地方,可以使用以下命令查看已配置的 runner:

sudo gitlab-runner list
Runtime platform                                    arch=amd64 os=linux pid=30148 revision=32fc1585 version=15.2.1
Listing configured runners                          ConfigFile=/etc/gitlab-runner/config.toml
Linux dev server                                    Executor=shell Token=ZLXwxp4KWrQp2jjZRxjj URL=https://gitlab.com

列出了描述为 Linux dev server 的 runner,以及其执行器、注册令牌和 runner 注册的 GitLab 实例。

我们还可以从 GitLab 端验证 runner 是否正确注册。我们刚刚注册的 runner 已经注册到我们的Hats for Cats项目中。图 5.5 显示,如果我们返回项目,通过 Linux dev runner 注册并可用于该项目:

图 5.5 – 特定 runner 成功注册到 GitLab

图 5.5 – 特定 runner 成功注册到 GitLab

GitLab UI 显示了我们特定 runner 的一些其他有趣特性。devrhel 标签显示在 runner ID 和描述旁边。runner ID 旁边的锁图标表示该 runner 已锁定到特定项目,不能分配给其他项目。铅笔图标将带我们进入 GitLab UI 中可调整的 runner 设置,暂停按钮则会“暂停”该 runner。暂停 runner 会使其保持在 GitLab 中注册,但会阻止它在暂停时接受新作业。

我们可以通过点击超链接的运行器 ID 来查看现有的运行器设置和统计信息。图 5.6 显示了我们之前注册的运行器信息,可以在 GitLab UI 中查看。这些信息包括运行器的架构和网络详情、活动状态,以及可分配的属性,如运行器标签、是否受保护、描述和是否可以分配给其他项目:

图 5.6 – GitLab UI 中的运行器信息

图 5.6 – GitLab UI 中的运行器信息

如果我们返回到运行器设置页面(如图 5.5所示),并选择编辑(铅笔)图标,我们将进入一个页面,显示额外的运行器详情以及我们可以从 UI 设置的属性。这些设置包括保护运行器、修改描述和运行器标签,如图 5.7所示。

最大作业超时字段默认情况下告诉运行器在一定时间后报告作业失败。使用此选项时要小心;如果设置了该选项,请确保其值大于你期望一个构建所需的最大时间。

图 5.7 – GitLab UI 中的运行器设置

图 5.7 – GitLab UI 中的运行器设置

此时,您的运行器已注册、处于活动状态,并准备开始接收 CI/CD 作业。接下来我们将讨论基于你或你组织需求的运行器配置和执行器的相关考虑。

关于各种运行器类型和执行器的考虑因素

我们了解到,存在许多类型的运行器、配置选项和执行环境。在本节中,我们将讨论一些性能、安全性和监控方面的考虑,以帮助你在决定使用哪些运行器以及何时使用它们时作出决策。

性能考虑

作为开发人员或运维人员,你希望确保管道尽可能高效地运行。关于 CI/CD 作业执行的关键性能考虑因素包括运行器的可用性和资源、代码库的大小,以及如何处理作业和应用程序的依赖关系。

运行器可用性

请考虑本章前面讨论的三种运行器作用域:

  • 在此实例级别配置的共享运行器(如果使用自托管的 GitLab),可供该实例中的所有项目使用

  • 群组运行器可供群组及其子群组中的所有项目使用

  • 仅注册到指定项目的特定运行器

每种类型的 runner 处理 CI/CD 作业的方式会影响管道效率和执行时间。特定 runner 是一个相对直接的例子。当你知道需要为某个项目分配专用资源时,应使用特定 runner。也就是说,你可能会在提高管道效率的同时牺牲资源的高效利用。广泛使用特定 runner 可能导致服务器空闲时间增加。此外,将资源整合到组和共享 runner 中,也能让你利用云服务提供的自动扩展功能,这在使用特定 runner 时可能在经济上不可行。因此,如果你的应用资源使用和需求是可预测的,应该使用特定 runner。如果你预计资源需求会波动,则可以考虑使用组 runner 或共享 runner。

关于组和共享 runner,它们在实际操作中可能看起来差不多,尤其是当你的 GitLab 实例有一个顶级组来存储所有项目时。然而,组 runner 和共享 runner 在获取各自作业的方式上有很大不同。组 runner 按照 FIFO(先进先出)原则处理作业。这意味着一个资源密集型的管道可能会“占用”一组组 runner,因为它的作业在队列中排队,尤其是当一个阶段有很多作业时。

另一方面,特定 runner 通过公平使用队列操作。也就是说,GitLab 实例中作业最少的项目将优先使用共享 runner。这在某些情况下可能是期望的,因为它意味着你在 GitLab 中的所有项目将得到更公平的管道执行。然而,实际上,某些项目可能比其他项目更为重要,你会希望这些项目得到优先执行,避免因为其他项目的作业已经运行而导致待处理作业被排到队尾。在这种情况下,组 runner 按 FIFO 分配可能是最好的选择。

代码库大小

GitLab 文档将“大型”代码库定义为工作树中包含超过 50,000 个文件的代码库(即,已检出的文件集合)。当代码库较大时,管道中的限速步骤可能是 runner 克隆或拉取项目代码库。

GitLab Runner 已经对拉取项目文件的时间和资源进行了优化。如果项目已经被克隆到 runner 的执行环境中,runner 会执行增量拉取,这样就不需要为每个作业都拉取整个代码库。此外,默认情况下,runner 会执行浅克隆,只复制项目的最新 20 次提交(这个设置可以通过 .gitlab-ci.yml 文件中的 GIT_DEPTH 变量进行调整)。

对于一些更高级的配置,你可以在 .gitlab-ci.yml 文件中使用 pre_clone_script 关键字,设置在 runner 克隆代码库之前执行的 Git 配置命令。

缓存依赖

依赖关系和工件缓存的考虑概念上类似于大仓库的讨论。其核心思想是,我们希望尽量减少 runner 重复下载相同文件的需求,只下载当前分配给 runner 的 CI/CD 作业所需的文件。

.gitlab-ci.yml 中的 cache 关键字是指定应在作业之间保留在 runner 上的文件路径。我们建议将 cache 关键字与 runner 标签结合使用,这样像工具链之类的作业可以分配给那些已经预缓存这些依赖的 runner。

默认情况下,每个 runner 还会下载该管道中之前运行的每个作业的所有工件。你可以使用 dependencies 关键字来选择需要下载哪些作业的工件。例如,如果你有分别针对 Windows 和 Linux 的构建作业,并且有独立的 Windows 和 Linux 构建的测试作业,那么测试作业应该只下载各自构建作业的工件。

最后,如果你的管道是基于容器的,你可能会消耗大量的网络资源,从公共注册表拉取容器镜像来运行任务。GitLab 有一个名为 Dependency Proxy 的功能,允许你配置本地注册表来缓存 Docker 镜像,这样 runner 执行器就不需要在每次运行时从公共源拉取。相反,runner 会从本地注册表拉取,只有在更新缓存中的容器版本时才需要从公共源拉取。

安全性考虑

有关安全性和 GitLab 的内容远远超出了本书所能涵盖的范围。然而,你在安装和配置 runners 时会涉及到两个直接相关的考虑因素。它们是选择 runner 执行器,以及你如何在 CI/CD 管道中处理机密信息。

你选择的 runner 执行器

请记住,CI/CD 管道本质上是在远程主机上执行命令——也就是说,在安装了 GitLab Runner 的主机上。因此,你面临的固有风险不仅是对源代码进行操作,还包括对承载 runner 的底层基础设施进行操作。

通常来说,一些 runner 执行器可以被认为比其他执行器“更安全”。尽管 shell 执行器使用起来方便,但它暴露了服务器的文件系统给 runner,而对文件系统进行的操作可能会跨作业持续存在。例如,项目 A 的 CI/CD 管道作业可能会访问项目 B 的文件,如果项目 B 最近使用了相同的 runner。gitlab-runner 用户将在注册 runner 的用户权限下运行。如果注册时使用了 sudo,这意味着 runner 将具有完整的 root 权限。因此,我们建议仅在你信任的项目中的特定 runner 上使用 shell 执行器。

Docker 执行器可以被认为是相对更安全的,因为容器为主机系统提供了一个额外的抽象层。运行器会克隆项目代码并在隔离的容器中运行作业命令,随后在将结果报告回 GitLab 后销毁该容器。至关重要的是,确保容器以非特权模式运行。也就是说,作业必须由非 root 用户运行,以确保作业执行过程中不会涉及主机系统的访问。

VirtualBox/Parallels 执行器可以被认为是最安全的运行器之一,因为作业在具有隔离操作系统和文件系统的临时虚拟机中运行。CI/CD 作业无法访问底层的虚拟化管理程序,并且作业之间不太可能意外共享信息。如果你对保护 shell 或 Docker 环境的能力没有信心,可以考虑使用这些执行器。

秘密管理

管理密钥并避免它们被意外泄露是一个范围较广的主题,超出了本章的重点。然而,由于运行器从 GitLab 获取仓库信息和环境变量,因此理解糟糕的秘密管理可能导致运行器成为泄露敏感信息的途径至关重要。

无论在任何情况下,都不要将密钥(密码、云凭证、部署密钥等)硬编码到项目仓库中。Git 版本控制的特性是,一旦信息被提交到项目中,它就成为仓库不可变快照历史的一部分。仅仅通过未来的提交删除该密钥并不会将其从之前的提交中移除。由于运行器会以一定深度的历史快照克隆项目仓库,意外遗留在项目中的密钥会被传播到运行器基础设施中。

第七章将介绍如何使用 GitLab 的秘密检测工具对你的仓库进行历史扫描,以查找可能硬编码的密钥。最终,解决“我不小心提交了敏感信息”问题的办法是考虑该信息已不可挽回地暴露,因此需要重置或轮换受影响的凭证或密钥。

CI/CD 变量也会出现类似情况。GitLab CI/CD 中使用的变量作为环境变量导出到运行器的 shell 会话中。未掩码和未保护的存储密钥的变量存在被执行不可信构建的运行器泄露的风险。为了防止这种情况,应该将变量存储为掩码和受保护的变量,且仅能在受保护分支上使用,并确保它们的值不会在运行器上暴露。

监控考虑

记住,GitLab Runner 作为一个独立的应用程序运行在 GitLab 实例之外的基础设施上。监控你的运行器和管道的数据对于确保正确的安全性和资源利用率至关重要。

监控 runner 时需要考虑的三个关键领域如下:

  • GitLab UI 中的分析

  • Runner 日志系统

  • 由 runner 本身产生的可导出指标

自托管的 GitLab 提供了深入的日志记录和监控功能。大部分监控可以由内置的 Prometheus 服务器管理,日志也可以导出到诸如Splunk之类的聚合系统。同样,GitLab Runner 生成的日志条目可以在本地操作系统中进行管理,也可以由外部或基于 Web 的工具进行管理。

GitLab UI 分析

一般的 CI/CD 分析可以在项目级别的Analytics | CI/CD下找到。图 5.8展示了托管在GitLab.com上的公共 GitLab 项目的统计数据:

图 5.8 – GitLab 项目的 CI/CD 分析

图 5.8 – GitLab 项目的 CI/CD 分析

这里在 UI 中显示的指标仅反映了一般的流水线趋势,即整体流水线的成功和失败率。然而,这些数据可以作为一个有用的起点,帮助你判断流水线失败是由于源代码中的逻辑错误,还是由于底层基础设施(即你的 runner)的问题。如果 runner 是罪魁祸首,你可以进一步深入到 runner 特定的日志和指标,正如接下来几段所讨论的。

Runner 日志

GitLab Runner 没有专用的日志文件。相反,消息会发布到常规系统日志文件中。在 Debian 系列操作系统(如 Ubuntu)中,这通常是/var/log/syslog,在 Fedora 系列操作系统(如 Red Hat Linux)中,是/var/log/messages。关于 runner 服务、其配置或与 GitLab 通信能力的错误会被记录在这些文件中。

确认 runner 配置有效且能够与 GitLab 正常通信的一个有用方法是运行gitlab-runner verify命令,如下所示:

$ sudo gitlab-runner verify
Runtime platform             arch=amd64 os=linux pid=84209 revision=32fc1585 version=15.2.1
Running in system-mode.
Verifying runner... is alive                        runner=ZLXwxp4K

在上面的输出中,你可以验证 runner 的架构、版本、进程 ID、注册 ID 以及与 GitLab 通信的能力。

Runner Prometheus 指标

自托管的 GitLab 提供了深入的日志记录和监控功能。大部分监控可以由内置的 Prometheus 服务器管理。同样,GitLab Runner 包括一个嵌入式 HTTP 服务器,可以将其指标公布到一个可用的 Prometheus 服务器。

为了暴露 runner 的指标,你首先需要编辑 runner 的主配置文件config.toml,该文件通常可以在/etc/gitlab-runner找到。添加listen_address参数来告知指标服务器监听哪个端口。一个添加了listen_address参数的config.toml文件示例如下:

concurrent = 1
check_interval = 0
listen_address = "localhost:9252"
[session_server]
  session_timeout = 1800
. . .

编辑配置文件后,使用sudo gitlab-runner restart重启 GitLab Runner 服务。然后,合适的 Prometheus 服务器将能够读取并记录 runner 的指标。

总结

在本章中,我们描述了在 GitLab CI/CD 流水线中运行者的角色。我们了解到,运行者可以被视为执行 .gitlab-ci.yml 中指定的作业的“肌肉”。运行者可以作为独立程序安装在大多数计算机平台上,并且可以与 GitLab 中的所有项目或仅特定项目或组共享。此外,无论在何处安装了运行者,您都可以选择它用于运行 CI/CD 任务的执行环境。

无论您的角色如何,了解运行者设置如何影响流水线性能、应用程序安全性以及开发生命周期中的可观察性都是有帮助的。我们鼓励您练习安装和注册运行者的过程,即使维护运行者基础设施并不是您日常职责的一部分。对运行者架构和工作流程的自信了解将使您成为更好的软件实践者,因为您继续您的 GitLab 之旅。

下一章将在我们所学的基础上进行扩展,并向您介绍如何使用 GitLab CI/CD 构建强大的测试基础设施。应用程序验证以及稍后的安全性将作为核心主题,覆盖这些内容将使您能够不断提高 DevOps 工作流的效率、可持续性和安全性。

第六章:验证你的代码

对于大多数项目,GitLab CI/CD管道应该首先做的事情是验证代码。不同的项目会依赖不同的任务来执行这一关键步骤,但通常涉及检查代码质量和运行自动化功能测试的某种组合。作为某些类型验证的先决条件,有些项目需要先构建代码。 本章将重点介绍构建然后验证代码。

我们将首先讨论构建代码是否必要,如果必要的话,如何配置 GitLab CI/CD 管道来执行此任务。接着,我们将讨论如何使用管道运行 GitLab 内置的代码质量扫描工具。然后,我们将解释如何在管道中运行自动化功能测试。接下来,我们将介绍一种非常有趣的自动化测试,称为模糊测试,它可以发现传统自动化功能测试可能遗漏的问题。我们还将涉及 GitLab 的可访问性测试,确保你的代码能够被广泛的人群使用。最后,我们将简要提及几种其他验证代码的方法,尽管我们没有足够的篇幅来详细描述它们。到本章结束时,你将拥有一系列工具,确保你的代码编写良好且能够按预期执行。

本章的主要内容包括:

  • 在 CI/CD 管道中构建代码

  • 在 CI/CD 管道中检查代码质量

  • 在 CI/CD 管道中运行自动化功能测试

  • 在 CI/CD 管道中进行模糊测试

  • 在 CI/CD 管道中检查可访问性

  • 验证代码的其他方法

技术要求

和之前的章节一样,如果你在一个 GitLab 实例(自托管软件即服务)上有一个帐户,并可以登录并用来练习和实验本章讨论的概念,你将从本章获得最大的收益。

在 CI/CD 管道中构建代码

在简化运行软件时发生的后台机制的风险下,我们可以大致将解释型编程语言,如 Python 或 Ruby,视为直接执行原始源代码,而编译型语言,如 Java、C 或 C#,则必须通过编译将源代码转换为可运行的形式,然后执行编译后的程序版本。

这是配置管道以验证代码时需要牢记的一个重要区别,因为这意味着如果你的项目中包含任何用编译语言编写的代码(即使它只是你整体项目中的一小部分),你可能需要在任何验证任务之前,在管道中包括一个构建任务。我们说可能是因为一些通常在管道验证阶段运行的任务(例如,代码质量检查)直接查看源代码,而另一些则与运行中的代码进行交互。所以,如果你的管道只使用关注源代码的验证扫描,无论你使用什么语言,都可以省略构建步骤。如果你希望在管道中包含自动化功能测试或模糊测试,你需要先构建你的代码,所以请继续阅读!

每种编程语言都有不同的构建方式,使用不同的工具。即使是在同一种语言中,有时也会有多种工具或技术来构建代码。让我们来看两种编译 Java 代码的方法以及一种编译 C 代码的方法。

这些示例旨在让你了解如何在 GitLab CI/CD 管道中构建代码的全貌。它们并非所有实现这一任务方式的全面示例。因为有太多不同的语言和工具,我们只能给出一些基本的示例,然后让你根据自己的语言、工具、约束和偏好进行调整和扩展。

使用 javac 编译 Java 代码

除了简单的训练应用程序,现实世界中的 Java 项目很少使用javac编译器将 Java 源代码(即.java扩展名的文件)转换为编译后的 Java 类(即.class扩展名的文件)。当你只处理几个文件时,使用javac工具是有效的,但随着项目复杂度的增加,这种方法可能变得繁琐。然而,就像花生酱果酱三明治是烹饪入门的好方式,尽管它们永远不会出现在白金汉宫的正式晚宴上,javac也是将新 GitLab 用户引入使用 CI/CD 管道编译 Java 代码的好方法。

添加你的 Java 应用程序

让我们保持简单,创建一个单文件应用程序和一个名为com.hatsforcats的 Java 包。你可以使用 GitLab 的 Web IDE 编辑器创建一个名为src/com/hatsforcats的目录来存储你的源代码。在该目录下,使用 Web IDE 创建一个名为Login.java的文件。将以下简单的 Hello World 风格的 Java 代码添加到该文件:

package com.hatsforcats;
class Login {
    public static void main(String [] args) {
        System.out.println("Welcome to Hats for Cats!");
    }
}

配置你的管道

现在你的应用已被添加到项目中,是时候配置你的管道了。从项目仓库根目录下的空.gitlab-ci.yml文件开始,并使用stages关键字为你的管道定义一个build阶段:

stages:
    - build

接下来,你将定义一个位于构建阶段并运行javac的管道任务。为了这个例子,让我们设定一些额外的要求:

  • 所有的 Java 源代码文件都属于com.hatsforcats Java 包。

  • 您团队的编码标准要求将所有源代码放在项目根目录中的src/目录下。

  • 编译后的文件应保存在项目根目录中的target/目录下。

为了在满足这些标准的同时编译代码,您需要在.gitlab-ci.yml中定义一个任务来完成这项工作。给它起个明显的名字,并将其放入build阶段:

compile-java-with-javac:
    stage: build

在该任务定义中,您需要指定任务运行的 Docker 镜像。任务需要访问javac编译器,因此一个好的选择是使用最新版本的openjdk。将其添加到任务定义中(记得调整缩进):

    image: openjdk:latest

最后,任务需要调用 Java 编译器。在script关键字下列出的任何命令将在管道执行该任务时运行:

    script:
        - javac src/com/hatsforcats/*.java -d target/

毋庸置疑,您能根据之前给出的要求理解javac命令的语法,但如果不能,随时可以参考 Java 编译器的文档。

信不信由你,这就是在 GitLab CI/CD 管道中编译 Java 代码所需要的一切!

为了展示任务按预期工作,接下来让我们在compile-java-with-javac任务的script部分添加更多行。第一行将在javac执行完毕后显示target/目录的内容。如果编译器工作正常,该命令将在任务运行时显示编译后的 Java 源文件版本:

        - ls target/com/hatsforcats

接下来的几行将执行您编译后的Login.class代码,以证明它已正确编译。通常,您不会在专门构建代码的任务中运行代码,但在这种情况下,您只是为了演示编译确实发生了:

        - cd target
        - java com.hatsforcats.Login

这是您已经组装好的.gitlab-ci.yml完整文本。如果您跟着操作,请确保该文件的版本正好包含以下内容:

stages:
    - build
compile-java-with-javac:
    stage: build
    image:openjdk:latest
    script:
        - javac src/com/hatsforcats/*.java -d target/
        - ls target/com/hatsforcats
        - cd target
        - java com.hatsforcats.Login

提交此文件,并转到项目中的管道列表。缩放到由您的提交自动触发的管道运行,缩放到compile-java任务,并查看是否能在任务输出的末尾找到类似于此代码段的文本:

$ javac src/com/hatsforcats/*.java -d target/
$ ls target/com/hatsforcats
Login.class
$ cd target
$ java com.hatsforcats.Login
Welcome to Hats for Cats!
Cleaning up project directory and file based variables
Job succeeded

您可以看到javac命令在没有出现错误的情况下运行,ls命令显示了编译后的Login.java版本,并且该类在执行时产生了预期的输出。成功!

使用 Maven 编译 Java

让我们尝试一种稍微复杂一点的方法,尽管它可能更符合实际情况,用来编译您在上一节中设置的相同 Java 项目。我们不直接使用 Java 编译器,而是将*.java文件编译成*.class文件。

配置 Maven

Maven 通过一个名为pom.xml的特殊文件进行配置。这里无需深入讨论该文件的结构或内容,但如果你对每个部分的作用感到好奇,Maven 文档可以为你提供所有细节。将以下简洁的内容复制到你项目根目录中的新pom.xml文件中:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.hatsforcats</groupId>
  <artifactId>login</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
</project>

添加你的 Java 应用程序

如果你重新使用前面 javac 示例中的相同项目,你已经将一个 Java 程序添加到项目的仓库中。如果你使用的是新项目,请将以下 Java 代码添加到一个名为Login.java的新文件中,并将该文件放置在新的 src/com/hatsforcats/ 目录中:

package com.hatsforcats;
class Login {
    public static void main(String [] args) {
        System.out.println("Welcome to Hats for Cats!");
    }
}

配置你的管道

你可以在根目录下创建一个新的 .gitlab-ci.yml 文件,或者用这段配置代码替换你现有的 .gitlab-ci.yml 文件中的所有内容:

stages:
  - build
compile-java-with-maven:
  stage: build
  image: maven:latest
  script:
    - mvn compile
    - ls target/classes/com/hatsforcats
    - cd target/classes
    - java com.hatsforcats.Login

你会注意到,Maven 驱动的构建的管道配置代码与 Java 编译器驱动的构建配置代码类似,但有几个关键的不同之处:

  • image 关键字后面的不同值意味着 GitLab Runner 将在基于 Maven 的 Docker 镜像中执行任务,而不是在基于 Java 的 Docker 镜像中。

  • 编译代码的命令使用的是 mvn,而不是 javac

  • 默认情况下,Maven 将编译后的类放在与源代码不同的目录中,因此你不需要像使用 javac 时那样明确告诉它这么做(尽管请注意,它的默认目录与使用 javac 时指定的目录并不完全相同)。

提交该配置代码后,你可以查看自动触发的管道的详细信息,并放大查看compile-java-with-maven任务。你应该会在输出的末尾看到类似于这个片段的内容:

[INFO] Compiling 1 source file to /builds/cwcowell/hats-for-cats/target/classes
[INFO] ---------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ---------------------------------------------------
[INFO] Total time:  4.069 s
[INFO] Finished at: 2022-04-11T21:22:28Z
[INFO] ---------------------------------------------------
$ ls target/classes/com/hatsforcats
Login.class
$ cd target/classes
$ java com.hatsforcats.Login
Welcome to Hats for Cats!
Cleaning up project directory and file based variables
Job succeeded

Maven 驱动的编译正常工作,编译后的类出现在你预期的位置,运行时该类会输出预期的结果。你将再也不需要手动运行mvn compile命令!

使用 Gnu Compiler Collection (GCC) 编译 C 代码

让我们来看看基于 C 编程语言的项目构建。通常,你会使用像 Make 这样的工具来构建 C 项目,就像使用 Maven 构建 Java 项目一样。但为了尽可能简化这个例子,你将依赖于经典的 GCC 来直接编译一些 C 代码。

如果你在家中跟着做,你可以为你的 C 程序创建一个新项目,或者你也可以重新使用之前为两个 Java 示例使用的项目。

添加你的 C 应用程序

在 GitLab GUI 中导航到你的项目仓库,在根目录添加一个名为login.c的新文件,并将这段简单的 C 代码粘贴进去:

#include <stdio.h>
int main(void) {
    puts("Welcome to Hats for Cats!");
    return 0;
}

配置你的管道

设置一个管道以使用 GCC 编译 C 代码与 Java 示例中的设置差别不大。以下是主要区别:

  • 该任务将在包含 GCC 工具的 Docker 镜像中运行。

  • 作业定义中的 script 关键字指定使用 gcc 而不是 mvnjavac 来构建你的代码。

  • 该作业直接运行已编译的代码,而不是通过 java 命令调用 JVM。

构建和运行一个使用 GCC 编译的 C 程序的基本 CI/CD 配置代码可能如下所示:

stages:
  - build
compile-c:
  stage: build
  image: gcc:latest
  script:
    - gcc login.c -o login
    - ./login

重复我们之前提到的内容,通常你不会在构建代码的同一作业中运行它——实际上,在流水线中你可能根本不运行它。但你在这里运行它仅仅是为了展示编译是否按预期工作。

这是该作业输出的一个片段,显示你的 C 程序已经正确编译和运行:

$ gcc login.c -o login
$ ./login
Welcome to Hats for Cats!
Cleaning up project directory and file based variables
Job succeeded

将构建的代码作为工件存储

在你可以继续进行测试你刚刚构建的代码的流水线阶段之前,还有一个你需要了解的关键字:artifacts 关键字。

流水线作业创建的任何文件——包括在构建作业过程中生成的编译文件——都会在作业完成后立即被删除。这与命令行中的构建工具工作方式非常不同。如果你在终端中输入 javac MyApp.java,生成的 MyApp.class 文件会一直保留在你的文件系统中,直到你删除它。但是在 GitLab CI/CD 流水线中,每个作业都在自己的独立环境中运行。这意味着如果你在 build-java 作业中编译了一些文件,然后在后续阶段的 test-java 作业中测试它们,test-java 作业将无法看到你之前精心构建的文件。

幸运的是,artifacts 关键字提供了一个解决方法。这个关键字允许你指定 GitLab 应该从一个作业中保留并在所有后续作业中提供的某些文件或目录。例如,要保留在 compile-c 作业中生成的可执行 login 文件,你可以将以下两行添加到 compile-c 作业定义的底部:

  artifacts:
    paths:
      - login

你可以指定多个文件进行保存,并且除了任何单独的文件外,你还可以指定一个或多个目录进行保存。你还可以指定要从工件列表中排除的子目录或文件。例如,要保存 Maven 放置编译文件的目录中的所有内容,但排除任何子目录中以 Test 开头的文件,你可以将以下代码添加到之前 compile-java-with-maven 作业的底部:

  artifacts:
    paths:
      - target/classes/
    exclude:
      - target/classes/**/Test*

artifacts 关键字是配置 CI/CD 流水线时最重要的关键字之一,忘记在需要时使用它是一个常见的错误。如果你的流水线没有按照预期工作,应该尝试的第一个故障排除步骤是检查是否在所有生成你想在后续作业中访问的文件的作业中指定了工件。

现在你已经学会了在 CI/CD 流水线中何时以及如何构建代码,接下来让我们继续讨论通常是下一个流水线步骤的内容:检查你的代码质量

在 CI/CD 流水线中检查代码质量

GitLab 提供的众多扫描工具之一是一个特殊功能,它确保你的项目代码符合特定的质量标准。GitLab 将这个功能称为,毫不意外的,代码质量。如果你之前使用过任何类型的 linting 工具,可以将这个功能看作是一个加速版的 linter。

代码质量功能依赖于一个名为 Code Climate 的外部服务。虽然该服务可以扫描所有主要计算机语言编写的代码,但它并不能处理所有的语言。你可以参考 Code Climate 的官方文档,查看支持的语言列表,但可以放心,它与 Java、Python、Ruby、JavaScript 以及大多数其他常用语言完全兼容。

代码质量功能查找哪些类型的问题?它关注的主要类别包括性能、风格、复杂性、安全性以及臭味(即,指示高风险错误的模式)。它检测的具体违规行为因语言而异,但以下是一些它能够识别的质量违规实例:

  • 参数过多的函数

  • 具有过多退出点的函数

  • 过长的函数或类

  • 过于复杂的逻辑表达式

  • 过多或过少的垂直空白

  • 重复的代码

此外,如果你的编程语言有一套既定的风格规范——比如 Python 的 PEP-8 标准,或者 Ruby 的 Rubocop 规则集——可以将代码质量功能配置为包含这些规则。

启用代码质量

将 Code Quality 添加到 CI/CD 流水线中再简单不过了:

  1. 确保你的流水线已经定义了 test 阶段(提示:它几乎肯定已经有这个阶段了,所以你可能不需要做任何更改)。

  2. 包含一个 GitLab 提供的模板(即,包含额外 CI/CD 配置代码的文件),名为 Code-Quality.gitlab-ci.yml,它将 Code Quality 作业添加到你的流水线中。

步骤 1 在你项目的 .gitlab-ci.yml 文件中看起来是这样的:

stages:
  - test

步骤 2 将是这样的:

include:
  template: Code-Quality.gitlab-ci.yml

请注意,如果你已经定义了其他阶段,在 步骤 1 中,你只需将 test 阶段添加到现有阶段中——你不会删除任何现有阶段。类似地,如果你的流水线配置代码中已经包含了其他模板,在 步骤 2 中,你应当将这个新模板添加到现有模板中,而不是替换它们。

代码质量功能足够智能,能够检测到你 GitLab 项目中使用的所有编程语言,并为每种语言运行相应的扫描器。然而,理解这一点非常重要:因为这些扫描器都是由 GitLab 以外的不同人或团队开发的,所以不能保证这些扫描器在所有支持的语言中都能发现完全相同的问题。例如,某一种语言的扫描器可能特别擅长检测重复的代码片段,而另一种语言的扫描器可能特别擅长发现应该简化的复杂代码。

查看代码质量结果

让我们看一个具体的代码质量示例。假设你在项目的根目录下有一个名为 hats-for-cats.py 的文件,包含以下代码:

def register(username, password, phone, city, state, zip):
    # TODO finish this code

这段代码存在两个你期望代码质量能够捕捉到的问题:该函数的参数太多,并且 TODO 注释应该被处理并移除。

如果你在项目的流水线中启用了代码质量功能,然后运行流水线,流水线详情页面将包含一个名为代码质量的新标签,显示代码质量扫描的结果:

图 6.1 – 流水线详情页面中的代码质量结果

图 6.1 – 流水线详情页面中的代码质量结果

你可以在另一个地方看到相同的信息:合并请求中。然而,合并请求中的报告与流水线详情页面中的报告在一个重要方面有所不同。流水线详情报告显示的是在流水线运行所在分支上发现的所有代码质量问题,而合并请求报告则显示的是合并请求源分支与目标分支之间的代码质量问题差异。由于目标分支几乎总是你项目的默认分支(即,main 或 master),合并请求报告会告诉你,相比于稳定的代码库,你的源分支上的工作是新增了代码质量问题,还是修复了旧的代码质量问题。换句话说,它展示了你分支上的提交是让项目的代码变得更好,还是更糟。

为了说明这一点,假设你创建了一个分支,为该分支创建了一个合并请求,并且提交了一个修改,删除了 TODO 注释并添加了一个新的 FIXME 注释。你会期望合并请求中的代码质量报告显示,一个旧的问题(TODO)已被修复,一个新问题(FIXME)被添加了。而这正是合并请求报告中显示的内容:

图 6.2 – 合并请求中的代码质量结果

图 6.2 – 合并请求中的代码质量结果

两个报告位置——管道详细信息页面和合并请求——都为每个检测到的问题提供了条目。这些条目会告诉你每个问题的名称、文件名和问题发生的行号。这些信息应该足够让你决定是修复还是忽略每个代码质量问题。你可以决定忽略一些问题,可能是因为它们是假阳性,或者是因为它们是一些过于微小、不值得修复的实际问题。

代码质量是 GitLab 最优秀、最有价值的 CI/CD 功能之一。它是保持代码可读性和可维护性的一个重要工具,GitLab 甚至将其作为产品所有许可层级的标准功能,包括免费版。它运行快速、可靠且高效。实际上没有理由不在所有项目中使用它。

在 CI/CD 管道中运行自动化功能测试

在 CI/CD 管道中,最常见的任务之一是运行自动化功能测试,以确保你的代码按预期工作。例如,你可能希望使用pytest框架来运行一系列用 Python 编写的单元测试,测试基于 Python 的“Hats for Cats”应用。让我们看看如何在 GitLab 中做到这一点。

注意

如果你不熟悉pytest,不用担心。pytest单元测试的语法非常简单,即使是对编写自动化测试有一点经验的人也能理解。

启用自动化功能测试

假设你已经编写了三个基于pytest的单元测试,以确保“Hats for Cats”应用的登录功能按预期工作。你可能会有一个名为test/test_login.py的文件,内容如下:

def test_login():
    # add code that tries to log in with good credentials
    assert True
def test_login_bad_password():
    # add code that tries to log in with a bad password
    assert True
def test_login_no_password():
    # add code that tries to log in with no password
    assert False

显然,这些示例测试包含了占位符代码,强制前两个测试通过,第三个测试失败。实际的测试会有实际的逻辑,以多种方式验证登录功能,但这些简化的示例使得 GitLab 的自动化测试功能更容易演示。

为了在你的管道中运行这些自动化测试,添加一个作业来触发它们,就像你在命令行中执行一样:

unit-tests:
  stage: test
  image: python:3.10
  script:
    - pip install pytest
    - pytest test/

这个作业定义指定该作业属于test阶段,并且必须在安装了 Python 3.10 版本的 Docker 容器中运行。它首先运行的命令通过pip包管理器安装pytest包,然后调用新安装的pytest命令,运行test/目录中的所有单元测试。

在添加此任务并运行管道后,你可以检查任务的输出,看到测试确实已经运行。你甚至可以看到每个测试的通过/失败结果。但是,任务的输出很难解析,且有些晦涩。如果自动化测试的结果能以易于阅读的表格形式出现在 GitLab 的 GUI 中,那该多好?幸运的是,GitLab 确实可以做到这一点。你只需要稍微调整任务定义,以便将单元测试的输出以特定格式存储,然后将该结果文件保存为 GitLab 的构件。将以下代码添加到现有的unit-tests任务定义末尾即可实现:

  artifacts:
    reports:
      junit: unit_test_results.xml
    when: always

这段代码指示 GitLab 保存pytest框架生成的unit_test_results.xml文件。它还将此文件指定为报告,该报告包含以 JUnit 格式存储的测试结果,JUnit 是 GitLab 可以读取和显示的行业标准格式。最后,它指示 GitLab 在测试失败时仍然保留该文件。最后一步很重要,因为一个失败的测试会使整个unit-tests任务处于失败状态,通常这会导致构件被丢弃。但我们希望看到结果,即使——也许尤其是——当其中某些测试失败时。

查看自动化功能测试结果

在添加我们刚刚描述的附加代码并运行新的管道实例后,管道详情页面上将出现一个标记为Tests的新标签。看看,点击该标签会显示自动化测试的通过和失败概览:

图 6.3 – 自动化测试结果概览

图 6.3 – 自动化测试结果概览

该表格显示了每个触发自动化测试的任务一行。点击任何一行可以进一步细分结果,因此你可以准确看到哪些测试通过或失败:

图 6.4 – 单个自动化测试结果

图 6.4 – 单个自动化测试结果

正如你可能预料到的,位于每个测试旁边的查看详情按钮会显示该测试的更多信息,包括生成失败断言的代码行,以及该测试过去失败的历史记录。这些信息有助于你调试产品代码——或者如果问题出在测试本身,则调试你的测试代码。

图 6.5 – 单个自动化测试详情

图 6.5 – 单个自动化测试详情

在管道详细页面查看功能测试结果,可以看到该管道运行的分支代码的所有测试结果。有时这正是你想要的。其他时候,你可能想知道与项目默认分支上的代码相比,某个分支上的代码是否出现了新的测试失败(或修复了某些失败的测试)。换句话说,功能分支是在修复坏代码,是在破坏曾经有效的代码,还是两者都有?

幸运的是,出现在合并请求中的自动化功能测试报告正好为你提供了这些信息。假设你正在一个分支上工作,并且你成功修复了默认分支上失败的一个测试,破坏了默认分支上通过的一个测试,添加了一个通过的新测试,并且添加了一个失败的新测试。该分支的合并请求将呈现如下报告:

图 6.6 – 合并请求的自动化功能测试结果的增量视图

图 6.6 – 合并请求的自动化功能测试结果的增量视图

这显示有两个测试失败,包括你在这个分支上破坏的旧测试和你添加到这个分支上的新测试中的一个。它还显示默认分支上的一个测试失败,但在此分支上已修复。合并请求报告没有提到你添加的通过的新测试,除了在五个总测试的计数中包括它。这是因为你通常更关心的是哪些测试失败,而不是哪些测试通过。如果你确实想查看所有测试的状态——包括通过和失败的测试——查看完整报告按钮将为你提供这些信息。

运行自动化测试通常是开发团队配置新管道时要执行的第一个任务。如果你仅仅做到这一点,你仍然可以从 GitLab CI/CD 管道中获得巨大的价值。但你可以通过管道验证代码的方式还有很多!接下来让我们看看模糊测试。

CI/CD 管道中的模糊测试

模糊测试是一种替代性的、非传统的寻找代码缺陷的方法。简而言之,这种高级测试技术向代码的函数发送半随机数据,试图触发错误。虽然它的设置比其他扫描器需要更多的工作,但通过发现你可能使用其他方法永远找不到的错误,它是值得的。

关于 GitLab 版本和功能的提示

模糊测试,就像本书中讨论的许多其他功能一样,只有在你使用 GitLab Ultimate 许可证时才可用。你可以通过查看 GitLab 官方文档中的相关功能,了解你的许可证层级是否包括特定的功能。功能通常会在被高层级限制几年后,才会在较低层级中提供。

在 GitLab 中进行模糊测试有两种方式:覆盖引导的模糊测试Web API 模糊测试。在本书中,我们将只讨论前者,但这两种技术足够相似,如果你理解了其中一种,你可以很容易通过 GitLab 的文档学习另一种。从现在开始,每当我们提到模糊测试时,我们指的就是覆盖引导的变体。

模糊测试的架构和工作流程

要使用覆盖引导的模糊测试,你需要理解四个架构组件:被测试的代码CI/CD 作业模糊引擎模糊目标。让我们看看每个组件,然后再看看它们如何在模糊测试工作流程中协同工作。

被测试的代码

模糊测试针对代码中的单个函数。该函数可以用 GitLab 的模糊测试工具支持的任何语言编写,并且长度不限。它必须至少接受一个参数,但没有参数个数的上限。该函数可以调用其他函数,如果在调用栈中的任何地方触发了 bug,模糊测试工具会报告它。

请考虑这个 Python 函数作为你测试中的代码。假设它位于名为 name_checker.py 的文件中:

def is_bob(name: str) -> bool:
  if len(name) == 0:
    return False
return name[0] == 'b' and name[1] == 'o' and name[2] == 'b'

这个简单的函数将一个字符串作为参数。如果字符串为空,它立即返回 False。否则,如果字符串是 bob,则返回 True,否则返回 False

当然,这对于这个简单任务来说是一个糟糕的算法,但我们会请你抑制住对这段代码作者发泄不满的冲动,为了演示效果配合一下。假装这是一个刚上班的实习生写的,他完全是吓坏了。

这位实习生不仅在设计算法方面很糟糕,而且也不是一个很好的程序员。你可能已经发现了函数的明显 bug:它没有验证传入的字符串是否至少有三个字符。因此,如果字符串只有一个字符且该字符是 b,当函数尝试读取字符串中不存在的第二个字符时,它将抛出一个意外的 IndexError。同样,如果字符串中只有两个字符 bo,当函数尝试读取第三个字符时,它会抛出 IndexError

开发人员或 QA 团队负责编写测试用例时,可能会忘记测试这些情况。让我们看看模糊测试是否能通过找到这个 bug 来拯救局面。

一个 CI/CD 作业

接下来,你需要在 CI/CD 流水线中定义一个专门用于模糊测试被测代码的作业。你可以在一个流水线中模糊测试多个不同的函数,但每个函数需要一个单独的流水线作业来进行测试。在这个例子中,你的被测试代码只有一个函数,因此你只需要定义一个 CI/CD 作业。

在我们定义作业之前,我们应该解释一下模糊测试作业必须扩展一个名为.fuzz_base的作业,该作业在 GitLab 提供的模板中定义。在定义作业之前,您需要通过向.gitlab-ci.ymlincludes:部分添加一行来包含该模板:

  - template: Coverage-Fuzzing.gitlab-ci.yml

我们将扩展的.fuzz_base作业预期在一个名为fuzz的新阶段中运行,这个阶段必须在build阶段之后运行,以便它可以对已编译的可运行代码执行模糊测试。让我们把它添加到我们的阶段列表中。假设我们已经定义了buildtest阶段,.gitlab-ci.ymlstages:部分将如下所示:

stages:
  - build
  - test
  - fuzz

现在我们准备在.gitlab-ci.yml中添加一个作业定义,用于为我们的测试代码启动模糊测试:

fuzz-test-is-bob:
  image: python:latest
  extends: .fuzz_base
  script:
    - pip install --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple pythonfuzz
    - ./gitlab-cov-fuzz run --engine pythonfuzz -- is_bob_fuzz_target.py

这个名为fuzz-test-is-bob的作业首先指定它应该在包含最新版本 Python 的 Docker 镜像中运行。这是必要的,因为模糊引擎、模糊目标和测试代码都是用 Python 编写的。

接下来,它从名为.fuzz_base的父作业继承工作配置细节。这个父作业由 GitLab 提供,你不需要知道或关心它为你的作业提供了哪些配置细节。

然后,您的作业指定了两个要运行的命令。第一个命令从 GitLab 托管的软件包注册表安装基于 Python 的模糊引擎。第二个命令运行一个名为gitlab-cov-fuzz的二进制文件,将其指向正确的模糊引擎和模糊目标。这个二进制文件实际上启动了模糊测试。当我们在下一节中查看整个模糊测试工作流程时,您将更好地了解模糊测试如何进行。

模糊引擎

模糊引擎是 GitLab 提供的一个二进制文件,它向模糊目标发送随机字节流。这些字节作为输入数据的基础,模糊目标将把这些数据提供给正在测试的代码——但更多关于这个主题的信息将在下一节中介绍。

实话实说,将这些字节称为半随机比称其为随机更准确。这是因为模糊引擎查看了上一轮数据使用的哪些代码行,并试图以这样一种方式变异数据,使得当变异后的数据作为下一组随机字节使用时,它将执行测试代码中的不同代码行。因此,它是随机的,但也受先前使用的随机数据集的影响。这就是所谓的覆盖率引导的含义:模糊测试器使用代码覆盖数据来影响其如何生成发送给测试代码的随机数据。

模糊目标

模糊目标是一小段您必须使用与测试代码相同的语言编写的代码。它充当模糊引擎和测试代码之间的翻译器或中介。模糊目标有两个任务:

  • 将模糊引擎发送给它的随机字节转换为待测试代码期望接收的输入参数的数据类型。例如,它可能需要将字节转换为一个整数数组、一个字符串或一个类的实例。

  • 调用待测试代码中的函数,将转换后的随机字节传递给它。

在这个示例中,模糊目标需要将模糊引擎发送的随机字节转换为一个字符串,然后将该字符串传递给name_validator.py中的is_bob函数。你可以将模糊目标所在的文件命名为任何你喜欢的名字,但为了让模糊引擎能够调用它,你必须包含一定量的样板代码。假设你将你的模糊目标文件命名为is_bob_fuzz_target.py,并在文件中包含以下内容:

from name_checker import is_bob
from pythonfuzz.main import PythonFuzz
@PythonFuzz
def fuzz(random_bytes):
    try:
        random_bytes_as_string = str(random_bytes, 'UTF-8')
        is_bob(random_bytes_as_string)
    except UnicodeDecodeError:
        pass
if __name__ == '__main__':
    fuzz()

让我们看看这里发生了什么。第一行使得待测试的代码可用,以便模糊目标可以将随机数据传递给它。

接下来的两行声明了一个名为fuzz的函数,它接受随机字节作为输入。这是必需的样板代码:你必须包含这些行。

接下来,模糊目标将模糊引擎发送给它的随机字节尝试转换为字符串,这是待测试代码期望作为输入的数据类型。对于许多(实际上是大多数!)传递给模糊目标的随机字节集,由于至少有一个字节超出了映射到字母、数字、标点符号和其他符号的值范围,这种转换将失败。tryexcept语句处理了这个问题:如果任何字节无法转换,模糊目标将直接返回,而不会调用待测试的代码。

如果字节成功地转换为字符串,模糊目标通过将新生成的字符串传递给is_bob函数来测试待测试的代码。

最后的两行是更多的样板代码,任何基于 Python 的模糊目标都必须包含这些代码。

记住,模糊目标必须使用与待测试代码相同的编程语言编写。尽管非 Python 模糊目标中使用的概念与这里展示的非常相似,但样板代码和数据转换代码在其他语言中可能会有所不同。

一个模糊测试工作流

以下是这四个组件如何协同工作,每当你运行项目的流水线时执行模糊测试:

  1. 名为fuzz-test-is-bob的 CI/CD 作业在fuzz阶段触发。它下载gitlab-cov-fuzz二进制文件和基于 Python 的模糊引擎。然后,它运行gitlab-cov-fuzz二进制文件,将其指向 Python 模糊引擎和位于is_bob_fuzz_target.py中的模糊目标。

  2. 模糊引擎生成一系列随机字节,并将它们传递给is_bob_fuzz_target.py中的fuzz函数。

  3. 模糊目标将随机字节转换为字符串,因为is_bob函数(即待测试代码)期望接收字符串作为输入。

  4. 模糊测试目标将字符串传递给is_bob

  5. 如果is_bob能够优雅地处理随机字符串——即没有崩溃或抛出任何意外异常——模糊引擎会查看上一系列随机字节所触发的代码行,并生成一系列新的随机字节,旨在触发is_bob中不同的代码行。这个过程会持续进行,每次循环时模糊引擎都会生成新的字节。

  6. 另一方面,如果随机字符串导致is_bob崩溃或抛出意外异常,模糊引擎会将此报告给fuzz-test-is-bob CI/CD 任务,该任务报告模糊测试在被测试代码中发现了一个错误。成功!当然,至少成功地触发了一个失败。

查看模糊测试结果

当模糊测试发现一个错误时,它会在三个地方显示这些信息:

  • 漏洞报告,你可以通过点击左侧导航窗格中的Security & Compliance | Vulnerability Report访问。此报告仅展示模糊测试在项目的默认分支上发现的问题。

  • 管道详细信息页面上的Security标签。这展示了模糊测试在该管道实例运行的任何分支上发现的问题。

  • 在合并请求中。这展示了模糊测试在默认分支上发现的问题与在合并请求的源分支上发现的问题之间的差异。如果默认分支和源分支之间没有任何变化,合并请求将报告模糊测试未发现任何问题,无论两个分支上实际存在多少问题。

尽管报告的具体错误会根据你查看的报告类型有所不同,但每个报告提供的详细信息类型几乎是相同的。例如,以下是漏洞报告中的一页,展示了模糊测试在被测试的is_bob代码中发现的错误的详细信息:

图 6.7 – 模糊测试错误报告

图 6.7 – 模糊测试错误报告

请注意,这个报告包含了一个堆栈跟踪,显示了抛出的错误(IndexError),以及是哪一行抛出的(带有return语句的那一行)。报告还告诉你是哪个随机字节——也叫做“样本”——触发了这个问题。在这种情况下,模糊引擎生成了一个单一的字节:62。事实证明,62 在 UTF-8 中对应字母b的小写字母。如果你查看被测试代码中的is_bob函数,你应该能够清楚地看到为什么由单个字母b组成的输入字符串会暴露这个错误。当像模糊测试器这样复杂的系统按预期准确运行时,是不是感觉特别满足?

模糊测试时的额外注意事项

与 GitLab 提供的其他验证代码的方式的可预测、逻辑性相比,模糊测试就像你那个怪异的叔叔,他会穿着不匹配的袜子出现在家庭聚会中,说一些神秘的评论,这些评论可能是深刻的,也可能是完全无稽之谈,这取决于那天的情况。模糊测试的随机性质意味着它的结果是不可预测的。你可能在不同的两天运行相同的模糊测试,第一天在 10 秒内就发现了一个 bug,而第二天运行 10 分钟后什么也没发现。你永远不知道模糊测试会带来什么,甚至可能什么也没有发现。这不必担心,因为每次运行项目的管道时都会进行新的模糊测试;即使今天没有发现问题,明天也许会找到一个重要的问题。

记住,模糊测试运行时间越长,它发现问题的机会就越大,有些团队选择异步运行模糊测试,而不是作为正常的管道作业,这样不会阻塞管道中的后续阶段。这个技术超出了本书的范围,但如果你想尝试,GitLab 的文档会解释如何设置。

与其他测试或扫描工具不同,模糊测试的另一种方式是,它会在发现一个问题后立即停止,而其他工具通常会继续运行,直到找到并报告它们能够挖掘出的所有问题。通常这不是问题,因为大多数项目会在开发过程中运行模糊测试数十次、数百次或数千次。但了解这一点很重要,模糊测试今天发现了一个 bug,并不意味着在后续的运行中,代码中还有其他隐藏的 bug 等待被发现。

记住,虽然你可以对代码中的任何函数进行模糊测试,但每个函数必须创建一个单独的 CI/CD 作业和一个单独的模糊测试目标。当你开始进行模糊测试时,这可能会增加不少开销。幸运的是,一旦所有设置完毕,模糊测试按预期工作后,通常不需要更改作业或模糊测试目标。

带语料库的模糊测试

模糊测试有一个特殊的可选功能,叫做语料库。这是一系列随机字节,模糊测试工具可以用来实现两个目的。首先,如果某一系列随机字节在测试代码时导致了 bug 或崩溃,而你的团队已修复了该 bug,那么将这些随机字节用于未来的模糊测试可以确保代码没有回归。换句话说,一旦你的团队修复了 bug,将这些有问题的字节加入语料库是一个很好的安全措施,可以确保 bug 不会复发。如果你将这些有问题的字节加入语料库,那么所有未来的模糊测试运行都将使用这系列字节作为发送到测试代码中的值之一。

语料库的第二个用途是帮助模糊测试工具更快地发现 bug。当它生成真正随机的字节作为输入并测试代码时,可能需要很长时间才能找到 bug——如果能找到的话。但如果你向语料库中加载一个或多个构成有效输入的字节序列(即代码可以优雅处理的输入),那么模糊测试可以改变这些有效数据并将其作为下一组输入传递给代码进行测试。变异有效数据往往能比完全依赖随机字节作为输入更快地找到触发 bug 的数据。

设置语料库可能有些复杂,特别是如果你想利用 GitLab 的一个巧妙功能,该功能会在每次模糊测试发现 bug 时自动更新语料库。如果你认为语料库可能有用,GitLab 文档会引导你完成此过程。我们确实建议你尝试这一可选功能,因为它可以大大增强模糊测试的效果。

接下来,我们将从强大且有些特殊的 bug 发现工具——模糊测试,转向一种重要但常被忽视的代码质量检查方式:可访问性测试

在 CI/CD 管道中检查可访问性

并非所有应用程序都包含网页界面,但每当你编写网页应用时,我们强烈建议你使用 GitLab CI/CD 管道来确保你的界面对各种残障人士可访问。幸运的是,GitLab 使得测试你的网站是否符合网页内容可访问性指南WCAG)变得非常简单,WCAG 是由万维网联盟制定的标准。

这些指南涵盖了可能导致可访问性问题的网站特征。以下是 WCAG 涵盖的一些内容:

  • 需要同时进行纵向和横向滚动的页面

  • 例如 <H1> 这样的 HTML 标题标签没有文本内容

  • 文本与背景对比度不足

  • 缺少替代文本描述的图片

  • 没有可供屏幕阅读器使用的按钮控件名称

你可能会对这个扫描工具在你的网页界面中找到的可访问性问题数量感到惊讶,也可能会对修复这些问题的难易程度感到惊讶。如果它在你的网站上发现了几个可访问性 bug,不要感到沮丧;试试把扫描工具指向任何一个流行网站,你很可能会惊讶于它展示出来的基本可访问性违规问题的数量!

启用可访问性测试

要在你的管道中添加可访问性测试,首先需要在 .gitlab-ci.yml 文件中创建一个名为 accessibility 的新阶段:

stages:
  - accessibility

很可能你的项目已经定义了 stages 部分,在这种情况下,你只需将新的 accessibility 阶段添加到现有的部分,而不是重新定义整个 stages 部分(这会导致 .gitlab-ci.yml 文件格式错误)。

接下来,包括由 GitLab 提供的包含与辅助性工作相关的作业定义的模板:

include:
  - template: "Verify/Accessibility.gitlab-ci.yml"

正如我们之前所说,如果您已经定义了一个include部分,只需将此模板添加到其中,而不是定义一个新的include部分。

最后,设置一个全局变量,告诉辅助性扫描器要检查哪个网站。这可以是您的 Web 应用程序在其生产环境中或任何预生产、暂存或审查环境。指向辅助性扫描器的任何网站,即使它不是您拥有的网站,也可能是有趣的(并且有教育意义)。在这里,我们将指向一个假的 URL,Hats for Cats 网站在其生产环境中运行:

variables:
    a11y_urls: «https://www.hats-for-cats.com»

再次强调,如果你已经有一个全局variables部分,只需将这个新变量添加到其中,而不是创建第二个variables部分。

信不信由你,这就是你需要做的全部。要在管道中启用辅助性测试,获取辅助性扫描器并运行它。辅助性扫描器没有提供任何其他配置选项,这使得设置非常简单。

查看辅助性测试结果

辅助性扫描器不会像您在自动功能测试结果中看到的那样将其结果集成到 GitLab 仪表板中。但它确实会生成一个易于阅读的 HTML 页面,描述其发现的所有较轻微问题(称为警告)和更严重问题(称为错误)。

要查看此页面,请运行启用辅助性测试的管道并访问管道详细信息页面。您将看到一个名为a11y的作业,这是运行辅助性扫描器的作业。单击该作业以查看作业的终端输出。您可以忽略该输出,但在右侧的作业工件窗格中,您将看到一个浏览作业产生的任何工件的按钮:

图 6.8 – 查找辅助性扫描工具的工件

图 6.8 – 查找辅助性扫描工具的工件

单击此按钮将显示辅助性扫描器生成的 JSON 和 HTML 报告。这些报告都包含有关目标网站上发现的任何辅助性违规的相同信息。可以下载 JSON 输出,解析并集成到您设置的任何其他仪表板中。HTML 报告可在浏览器中以人类可读的方式查看,让您的团队成员了解可能需要拆分成问题以进行跟踪和管理的辅助性相关工作。

有另一种方法可以查看辅助性扫描工具的发现,而不是查看其两个工件之一。记得代码质量报告和自动功能测试报告的合并请求版本展示了默认分支上的代码质量或测试结果与合并请求源分支上的代码质量或测试结果之间的差异吗?辅助性违规的合并请求报告工作方式完全相同。

如果你有一个与合并请求相对应的分支,合并请求将显示在针对该分支运行的最新管道中发现的任何可访问性违规,前提是这些违规没有出现在该分支创建时针对默认分支运行的管道中。换句话说,合并请求会告诉你管道的分支是让你的项目代码变得更好(通过修复默认分支上的可访问性问题),还是更差(通过引入默认分支上没有的新的可访问性问题)。如果你在一个功能分支上工作,并且希望确保你的老板不会因为你增加了比修复更多的问题而责备你,这个报告非常有用!

验证代码的其他方法

我们已经介绍了一些验证代码的常见方法。GitLab 还提供了更多功能,帮助你进一步测试代码。由于篇幅限制,我们无法详细介绍所有方法,但这里简要描述了你可以用来测试代码的三种额外方法。启用和配置这些工具的详细信息,可以在官方的 GitLab 文档中找到。

代码覆盖率

自动化功能测试确保你的代码按预期工作。测试的存在是每个软件开发项目的关键部分,但如果你不知道你的测试覆盖了多少代码库,看到所有测试都通过,可能会让你产生一种虚假的信心。毕竟,如果所有测试都执行了应用程序代码的同一个 5%,那么即使有 100 个测试通过,也对你帮助不大。

代码覆盖率报告让你对测试结果的价值有信心。你可以配置 GitLab,使用适当的、语言特定的代码覆盖工具来精确确定哪些产品代码行被你的测试执行。这个报告集成在 GitLab 的图形界面中,所以在编写新测试时,你很容易知道应该关注哪些代码行。

浏览器性能测试

由于如今许多应用程序都运行在浏览器中,而且基于浏览器的应用程序通常比传统的桌面应用程序慢得多,因此,跟踪网站各个页面的加载速度非常重要,并且要知道你对代码所做的更改是否使加载时间变得更快或更慢。

GitLab 可以测量页面加载时间,并在合并请求中显示结果,开发人员可以了解他们所提出的代码更改如何影响他们的 Web 应用性能。它甚至可以在性能下降到特定的用户可配置阈值时,发出特别警报。这个报告让开发人员在代码合并到稳定的代码库之前,修复任何由其代码引入的性能问题。

加载性能测试

虽然浏览器性能测试可以告诉你网页应用前端 GUI 加载的速度,GitLab 的负载性能测试帮助你跟踪应用程序后端代码的性能。尽管这个功能可以以多种方式对你的应用进行测试,但最常见的做法是针对应用程序的 API。例如,它可以通过数十、数百或数千个并发请求,向一个或多个应用的 REST API 端点发起测试,然后监控应用对这些请求的响应速度。你还可以使用这个工具执行长期的浸泡测试,以检查你的应用是否在长时间运行后出现内存泄漏或其他问题。

负载性能测试功能在合并请求中展示其结果,以便开发人员理解与该合并请求相关的分支上的任何代码变更如何影响其应用程序的后端性能。

总结

再次强调,在这一章中你覆盖了大量内容。你学会了如何在 GitLab CI/CD 管道中构建代码,使用了多种不同的方法和语言。虽然这并没有涵盖你可以用来编译或构建代码的所有可能方法——我们仅仅触及了这个话题的皮毛——但无论你使用什么语言或工具,你应该对涉及的基本步骤有一个清晰的了解。你还学到了一些代码验证工具需要先构建代码,因为它们会在代码运行时与之交互。其他测试则不需要这一步,因为它们只扫描你的源代码,而不运行它。

接下来,你了解了如何在 GitLab 的管道中使用代码质量功能,以确保你的代码遵循最佳的编码风格实践,遵循常见的编码约定,避免不必要的复杂性,并且没有出现任何代码异味,这些异味可能表明存在潜在的错误或异常行为。

然后,你学习了如何将自动化功能测试集成到 GitLab CI/CD 管道中。你不仅了解了如何从管道作业中触发这些测试,还学习了如何确保结果可以在 GitLab 界面的两个不同报告中查看。你还发现了如何使用合并请求中的增量视图查看测试结果,以了解该合并请求的分支上的代码是否有助于提高产品自动化测试的通过率,或是降低了通过率。

接下来是模糊测试,GitLab 最复杂但也许是最有趣的找错功能。你了解了构成模糊测试架构的四个不同组件,并看到如何通过这些组件将随机数据传递到下一个组件,试图触发代码错误或导致崩溃或异常。你熟悉了模糊测试的各种独特性,并学会了如何适应它们。最后,你了解了如何使用语料库,不仅可以捕获代码中的功能回归,还能加速模糊测试,并增加找到问题的可能性。

你最后看到的工具是 GitLab 的无障碍测试功能。这个功能帮助你确保你的网页应用可以被不同残障人士使用,从而最大化你的潜在用户群体。

这些工具在验证你的软件项目时是一个很好的起点,但 GitLab 提供了几种额外的方法来更深入地检查你的代码。你已经快速了解了代码覆盖工具、浏览器性能测试和负载性能测试。所有这些内容都值得通过使用 GitLab 的官方文档和你自己的实验进行进一步探索。

一旦你的代码通过验证,你可以将其部署到生产环境供客户使用,对吧?不对。你首先需要确保它不包含任何安全漏洞,这也是我们在下一章中要探讨的内容。

第七章:保护你的代码

现在你已经知道如何配置 GitLab CI/CD 管道,以验证项目代码是否满足其要求,构建管道的下一步是添加检查安全漏洞的任务。这是一个可选步骤,但由于 GitLab 使得将安全扫描添加到管道变得简单,而且除了增加几分钟的管道运行时间外几乎没有任何缺点,我们建议你启用所有与 项目相关的 安全扫描器。

我们将从概述 GitLab 在使用安全扫描器方面的总体策略开始本章;在开始了解各个扫描器之前,理解安全扫描的几个方面是很有帮助的。接下来,我们将解释 GitLab 提供的七种安全测试类型的目的:静态应用安全测试SAST)、密钥检测动态应用安全测试DAST)、依赖扫描容器扫描许可证合规性基础设施即代码IaC扫描。我们将向你展示如何在管道中启用每种类型的扫描器,然后讨论一些配置选项和技术,你可以用来调整它们的行为,以最适合你的需求。最后,我们将介绍 GitLab 的三个附加功能,使安全扫描器更易用、更强大:阅读扫描报告、通过漏洞管理跟踪扫描结果,以及集成外部安全扫描器。

本章结束时,你将掌握一些关键技能,帮助你保持代码安全并保护数据安全。你将了解如何识别哪些 GitLab 提供的安全扫描器与项目相关。你将知道如何将它们添加到 CI/CD 管道中,并配置它们的行为以满足你的需求。你将牢牢掌握 GitLab 提供的不同类型的安全报告。你还将能够跟踪团队在修复安全漏洞方面的进展。最后,你将了解如何将第三方安全扫描器添加到管道中。简而言之,你会对自己的代码安全性充满信心。

本章将涵盖以下主题:

  • 了解 GitLab 的安全扫描策略

  • 使用 SAST 扫描源代码中的漏洞

  • 使用密钥检测来查找存储库中的私人信息

  • 使用 DAST 查找 Web 应用程序中的漏洞

  • 使用依赖扫描来查找依赖项中的漏洞

  • 使用容器扫描来查找 Docker 镜像中的漏洞

  • 使用许可证合规性来管理依赖项的许可证

  • 使用 IaC 扫描来查找基础设施配置文件中的问题

  • 了解不同类型的安全报告

  • 管理安全漏洞

  • 集成外部安全扫描器

GitLab 对所有安全扫描器使用相同的一些配置技巧。为了避免重复,我们将在专门讨论第一个扫描器类型(SAST)的章节中详细讨论这些技巧。讨论其他扫描器的配置技巧时,我们会参考 SAST 章节。

在讨论扫描器配置时,重要的是要理解这些扫描器大多数提供 许多 配置选项,而在本书中讨论所有这些选项——甚至大多数选项——是不可能的。相反,我们会向你展示如何以一种相对简单的方式启动并运行每个扫描器,给你一个关于每个扫描器存在的配置选项的示例,并将你指向官方 GitLab 文档,作为每种扫描器类型配置设置的最新信息的最佳来源。幸运的是,关于这个主题的文档既清晰又全面。

最后,请注意,这些扫描器中的许多仅对拥有 GitLab Ultimate 许可证的用户可用。然而,GitLab 有将仅限 Ultimate 用户使用的扫描器在后续版本中提供给 Premium 或 Free 许可证用户的历史。截至撰写本文时,SAST、Secret Detection、容器扫描和基础设施即代码扫描对所有用户开放,无论其许可证级别如何,尽管有时这些功能会以功能受限(但仍然有用)的形式提供。因此,如果你发现自己喜欢的扫描器尚未对你的许可证开放,那么即使你没有升级许可证,它在未来可能会变得可用。

技术要求

和前几章一样,如果你拥有一个可以登录并用于实践和实验的 GitLab 实例账户(无论是自托管的还是 软件即服务 (SaaS)),你会从本章中获得最大的收获。

了解 GitLab 的安全扫描策略

在学习每个扫描器的功能之前,有一些 GitLab 安全扫描器的基本原则你需要了解,这些原则会对你有所帮助。我们现在就来看看这些原则。

GitLab 使用开源扫描器

你可能会感到惊讶,了解到本章讨论的所有安全扫描器都是 第三方开源工具;其中没有任何一个是 GitLab 内部开发的。例如,基础设施即代码扫描是由开源工具 Keeping Infrastructure as Code Secure (KICS) 执行的,依赖性扫描则由开源工具 Gemnasium 处理。

这并不意味着这些第三方扫描器在任何方面都比 GitLab 开发的软件差。它们都经过 GitLab 的严格研究和审查,才被采用为官方 GitLab 扫描器。此外,GitLab 还会定期审查新的开源安全扫描器,看看它们是否应当替代或补充现有产品的扫描器。因此,不用担心——这些扫描器都是你管道中的一流补充,尽管它们的代码不是由 GitLab 开发者编写的。

由专注于安全的组织或公司开发的安全扫描器,通常比那些由非专注于安全的公司开发的专有软件出现的漏洞更少。正如关于开源代码的说法:“只要有足够多的眼睛,所有的漏洞都浅显易懂。” 使漏洞变浅——然后修复它们——对于与安全相关的工具尤其重要:使用设计不良的安全扫描器,给你一个错误的印象,让你以为代码是安全的,比完全不了解产品安全更糟糕。

由于这些扫描器是开源软件,难道没有什么可以阻止你自己下载它们并独立于 GitLab 运行它们吗?没有!但很难理解为什么你会想这样做。GitLab 认证的扫描器非常容易集成到 GitLab CI/CD 管道中,而且 GitLab 会自动更新它们,以确保你的管道始终运行最新版本(除非你另行指定),而你无需采取任何行动。如果你已经在 GitLab 中设置了 CI/CD 管道,并且你的 GitLab 许可证级别允许你访问所需的扫描器,我们建议你在 GitLab 内使用这些工具,而不是独立运行它们。如果你自行下载并运行它们,你不会获得任何好处,反而会因为额外的系统管理和维护而失去不少。

GitLab 的安全扫描器支持哪些语言?

要查看每种类型的 GitLab 安全扫描器支持的所有语言的列表,以及所使用的开源工具的名称,请参阅官方 GitLab 文档(docs.gitlab.com/ee/user/application_security/sast/#supported-languages-and-frameworks)。请记住,这些细节会不时发生变化,因此建议定期重新查看文档,了解不同扫描器类型支持的新语言。

扫描器被打包为 Docker 镜像

当安全扫描器在 GitLab CI/CD 管道中运行时,它会在 Docker 容器内运行。对于大多数 GitLab 用户来说,这一点无关紧要,但有三个影响你应该知道。

第一点,因为运行扫描器的流水线任务需要拉取该扫描器的 Docker 镜像,所以这会增加任务运行时间大约一分钟左右。当然,具体的延迟取决于你的网络速度,以及镜像是否已经被缓存。这通常不是大问题,因为许多扫描器即使在其 Docker 镜像下载后,仍需要几分钟才能完成扫描。此外,大多数复杂的流水线任务是以分钟为单位运行的,而不是秒,所以你可能不会注意到因拉取安全扫描器 Docker 镜像而产生的短暂延迟。

第二,任何安全扫描任务必须在使用 Docker 或 Kubernetes 执行器的 GitLab Runner 上运行。如果你不确定这是什么意思,可以参考前一章节来回顾 GitLab Runner 执行器的相关内容。如果你的组织使用 GitLab 的 SaaS 版本(即你使用的是 gitlab.com 上的实例),那么这个问题已经为你解决:所有 SaaS 提供的 GitLab Runners 都使用这两种执行器之一。如果你使用的是自托管的 GitLab 实例,那么你可能有一个 GitLab 管理员负责设置你团队所需的所有 GitLab Runners。只需要确保他们明白,至少一些 Runner 必须使用 Docker 或 Kubernetes 执行器,如果你打算在流水线中添加安全扫描的话。

第三,因为你的任务每次运行时都会下载安全扫描器的 Docker 镜像,所以你无需担心更新安全扫描器。GitLab 会确保每个扫描器的最新版本包含在每次任务拉取的 Docker 镜像中。这意味着你少了一项需要跟踪的维护任务。

一些扫描器会为不同的编程语言使用不同的分析器。

一些扫描器,如 SAST 和依赖扫描,依赖于不同的开源工具来扫描用不同编程语言编写的代码。GitLab 称这些语言特定的工具为分析器。例如,当你在一个仅包含 Go 代码的项目中启用 SAST 时,GitLab 会运行一个开源的、支持 Go 的 SAST 分析器,名为Semgrep。但当你在一个基于 Ruby 的项目中启用 SAST 时,GitLab 会运行另一个名为Brakeman的开源分析器,它能够扫描 Ruby 代码。

你无需告诉 GitLab 运行哪些分析器——它会自动检测项目中的编程语言,并仅运行与这些语言兼容的分析器。GitLab 通过查找 .py 文件来推测项目包含 Python 代码,并为你启用的任何安全扫描器运行基于 Python 的分析器。它还会查找某些传统上与各种语言一起使用的配置文件,如 Ruby 项目中的 GemfileGemfile.lock,Python 项目中的 requirements.txt,以及使用 Maven 构建工具的 Java 项目中的 pom.xml。大多数情况下,你不需要担心让 GitLab 容易检测到你的编程语言——它几乎能自动正确处理所有项目。但如果你发现 GitLab 无法正确识别语言,你可以在 GitLab 官方文档中查找每种安全扫描器类型的触发文件列表,并确保项目中每种语言至少有一个触发文件。

在同一个项目中使用多种编程语言是完全没问题的。GitLab 会检测项目仓库中的所有语言,并为每种语言运行相应的分析器,前提是该扫描器类型和语言都有可用的分析器。

对于某些扫描器类型和语言的组合,GitLab 提供了多个可用的分析器。在这种情况下,它会运行所有相关的分析器。例如,如果你在一个包含 Python 代码的项目上启用 SAST,它会同时运行 SemgrepBandit 分析器。如果这两个分析器检测到相同的问题,你可能会在扫描报告中看到重复的结果,每个分析器都有一条结果。虽然这可能会让报告稍显杂乱,但宁可多做一些也不愿疏漏任何问题。此外,由于每个开源分析器都是由不同的开发团队编写,具有不同的关注点或专注领域,并且不同的分析器在全面性或成熟度上有所不同,运行多个分析器是发现更多漏洞的好方法。

如果在运行针对特定语言的多个分析器时产生了过多的噪声,你可以配置大多数扫描器来禁用某些单独的分析器。例如,你可以告诉 GitLab 在对 Python 代码执行 SAST 扫描时,仅运行 Bandit 分析器,而不运行 Semgrep 分析器。然而,我们通常建议你保持尽可能多的分析器启用,以减少漏洞漏网的机会。对于不在项目中的语言,也没有必要禁用分析器:GitLab 足够智能,只会运行支持其检测到的语言的分析器。

并非所有针对特定扫描器类型的分析器都会发现相同的问题。例如,如果你在 Go 代码中有一个除以零的漏洞,而在 Ruby 代码中有相同的漏洞,其中一个分析器可能会将其报告为潜在问题,而另一个分析器可能会忽略它。同样,这是因为不同的开源分析器是由不同的开发团队编写和维护的。

漏洞不会阻止流水线继续运行

对于大多数 GitLab 作业,默认行为是当包含失败作业的阶段完成时,中止流水线。毕竟,如果你的测试在早期阶段失败,那么在后续阶段就没有必要部署代码。GitLab 的安全扫描器在某种程度上遵循这个标准……但也有不遵循的地方。让我们来解释一下这意味着什么。

每个成功运行的安全扫描器都会将其流水线作业标记为通过,无论它是否检测到任何漏洞。换句话说,看到安全扫描器的流水线作业状态为通过,只是意味着扫描器已完成运行——它并不告诉你扫描器是否发现了漏洞。因此,即使流水线中的扫描器在你的代码中发现了大量漏洞,它们的作业状态仍会被标记为通过,并且流水线会继续进入后续阶段。

这可能看起来不太符合直觉。检测到漏洞不就像自动化测试失败一样吗?嗯,是的,也不是。漏洞通常是团队应该仔细检查的内容,但正如你将在后面了解的,当我们讨论 GitLab 的漏洞管理功能时,你可能会决定不修复这个漏洞。GitLab 不想假设你会在将代码部署到生产环境之前修复所有漏洞,因此即使发现漏洞,流水线仍会继续运行。这种做法为开发团队提供了关于代码安全的所有信息,而不要求工具评估代码是否适合部署。

发现结果出现在三个不同的报告中

GitLab 的安全扫描器发现的任何漏洞或其他问题,都会出现在整合了所有不同扫描器结果的报告中。GitLab 界面中有三个这样的报告,分别位于不同的位置。每个报告显示的信息略有不同,理解每个报告的目的非常重要,这样你就不会误解结果。我们将在本章后面详细讨论这个话题,但现在,我们先简单介绍一下漏洞报告,你可以通过点击左侧导航栏中的Security & Compliance,然后点击Vulnerability report来查看。当我们在本章的其余部分讨论安全扫描器时,我们会展示每个扫描器的示例结果截图,它们会出现在漏洞报告中。

流水线可以使用非 GitLab 提供的扫描器

尽管 GitLab 内置的扫描工具可能为你提供所需的所有安全漏洞保护,但你仍然可以通过向管道中添加许多其他第三方扫描工具来补充安全测试。我们将在本章末尾的专门部分(集成外部安全扫描器)中讨论如何配置这种集成。

现在你已经理解了一些 GitLab 安全扫描器的基本概念,让我们来看一下每个扫描器,了解它们能够发现哪些问题以及如何使用它们。

使用 SAST 扫描源代码中的漏洞

让我们从SAST开始介绍扫描工具。即使你不打算使用 SAST,也建议你仔细阅读本节内容,因为使用 SAST 所涉及的许多原则和实践同样适用于其他扫描工具。了解如何使用 SAST 可以让你在启用、配置和解读其他扫描工具的结果时占得先机。

理解 SAST

SAST 查看的是项目的源代码,而不是在代码运行时与其交互。有时,这种方法被称为白盒扫描,意味着扫描器查看应用程序的内部以检查其代码,而不是待在应用程序外部仅分析其行为。

该扫描器会查找不良编码实践、反模式或设计不良、结构不佳的代码特征,这些有时被称为代码异味,可能会导致可被利用的安全问题。例如,考虑以下这一行 Python 代码:

temp_dir = '/tmp'

它看起来无害,但非 Python 程序员可能会惊讶地发现,它被认为是一个安全漏洞。Python 的最佳实践是使用语言内置的 tempfile 模块来创建和管理临时目录和文件。通过该模块创建的任何目录或文件在程序执行完毕后会自动删除,确保没有敏感数据被意外地留在计算机的文件系统中。创建用于存放临时文件的目录是危险的,因为很容易忘记在程序不再需要时删除该目录。这正是 SAST 设计用来检测的问题。

正如上一节所提到的,不同计算机语言的 SAST 分析器会寻找不同的安全漏洞。GitLab 针对其他语言的 SAST 分析器可能无法检测出该语言代码中的类似问题,这可能是因为该语言中的代码不被认为是漏洞,或者分析器的成熟度和稳定性不如 GitLab 的 Python SAST 分析器。

启用 SAST

有两种方法可以在 GitLab 项目的流水线中启用 SAST:手动启用或通过 GitLab GUI 启用。它们最终是一样的,因为它们都会导致向 .gitlab-ci.yml 文件中添加几行配置 CI/CD 流水线的内容。让我们来看一下这两种方法。

手动启用 SAST

要手动启用 SAST,你需要对项目的 .gitlab-ci.yml 文件进行两项操作:

  • 确保流水线已经定义了 test 阶段

  • 包含名为 Security/SAST.gitlab-ci.yml 的模板

代码如下所示:

stages:
  - test
include:
  - template: Security/SAST.gitlab-ci.yml

这些步骤中的第一步很简单,因为流水线通常在你必须添加安全扫描时,已经定义了 test 阶段。但如果你在添加任何其他测试相关的作业之前就要添加安全扫描,只需添加前面片段中的前两行。当然,如果你已经定义了其他阶段,在添加 test 阶段时不要删除它们——只需将 test 添加到现有的阶段中。

注意

请记住,列出阶段的顺序很重要,因为 GitLab 会按照这个顺序在流水线中执行它们。

第二步是将一个模板添加到你的 CI/CD 配置中。模板是 GitLab 提供的文件,包含定义新作业定义或添加其他功能的 CI/CD 代码。通过在你的 CI/CD 配置文件中包含一个模板,你可以添加执行任务(例如 SAST 扫描)的作业定义,而无需了解这些作业的工作原理。

在这种情况下,模板为每个 GitLab SAST 分析器添加了一个作业定义。它还添加了检测项目中存在哪些语言的逻辑,以便它知道要运行哪些与 SAST 相关的作业。如果你假设你的 hats-for-cats 项目仅包含 Python 代码,你会期望此模板在项目的流水线中运行两个新作业:一个是 Bandit,另一个是 Semgrep,它们是 GitLab 的两个 Python SAST 分析器。

结果果然如此,如果你将此更改提交到 .gitlab-ci.yml 文件,并查看由该提交触发的流水线的详细信息,你会看到这些作业现在已经包含在你的流水线中,位于 test 阶段下:

图 7.1 – 流水线详细信息页面上的 Python SAST 作业

图 7.1 – 流水线详细信息页面上的 Python SAST 作业

请记住,如果你的项目包含 Python 以外的其他语言的代码,你将在此页面上看到不同的作业名称,并可能会看到不同数量的作业。如果你的项目同时包含 Python 和其他语言的代码,你将看到每种语言的 SAST 分析器的作业。

通过 GitLab GUI 启用 SAST

奇怪的是,通过 GUI 向流水线添加 SAST 扫描比手动添加要复杂:

  1. 通过导航到左侧面板中的 安全与合规性 选项,然后选择 配置,开始这个过程。这将引导你进入一个控制面板,用于启用和配置 GitLab 的大部分(但不是全部)安全扫描器。

  2. 精确的 GUI 控件有时会随着 GitLab 版本更新而变化,但会有一个按钮让你启用 SAST。点击该按钮会带你到一个新的页面用于配置 SAST。

  3. 通常,你可以将所有选项保持为默认值,然后点击底部的按钮来创建合并请求。这将带你到一个合并请求创建页面。

  4. 再次提醒,你通常可以将所有字段保持为默认值,然后点击底部的按钮来创建合并请求。

  5. 导航到合并请求并进行合并以完成过程。如果你的 .gitlab-ci.yml 文件之前没有包含 test 阶段,现在它应该包括该阶段,并包含前一节中描述的 SAST 模板。你可以看到,手动编辑该文件可能会更加简单!

一旦你通过 GUI 启用了 SAST,你应该会看到与手动启用 SAST 时相同的结果:每次运行管道时,将会添加两个新任务,分别对应 GitLab 支持的两个基于 Python 的分析器。

配置 SAST

现在,你已经知道如何将 SAST 添加到你的 GitLab 管道中。但如果你想改变 SAST 的默认行为怎么办?用于配置 SAST 的技术也适用于配置大多数其他安全扫描器。我们将在这里详细讨论这些技术,并在介绍其他扫描器时,直接参考本节,而不是重复这些信息。

配置 SAST 或其他任何安全扫描器的方式有三种:

  • .gitlab-ci.yml 中,设置全局变量

  • .gitlab-ci.yml 中,重写由你包含的模板原本添加的任务定义,并为该任务设置一个任务范围的变量。

  • 使用 GitLab GUI

你使用哪种技术来配置 SAST 或其他扫描器,取决于你想要设置的配置选项。不幸的是,你无法选择使用哪种技术来设置特定的配置选项——你必须参考 GitLab 文档,查看你感兴趣的配置选项需要使用哪种技术或哪几种技术。

让我们看一些配置 SAST 的示例。

使用全局变量配置 SAST

首先,假设你想禁用 Python 的 Semgrep SAST 分析器。你可以通过在 .gitlab-ci.yml 中设置一个全局变量来做到这一点:

variables:
  SAST_EXCLUDED_ANALYZERS: "semgrep"

如果你将此变量添加到你的 CI/CD 配置文件中并重新运行管道,你会注意到在前面截图中看到的 semgrep-sast 任务现在已经消失了。

当然,如果你的 CI/CD 配置文件已经有 variables 部分,你应该将这个新变量添加到现有的变量中,而不是创建一个全新的 variables 部分。

通过重写任务定义并设置任务范围变量来配置 SAST

接下来,假设你希望 Bandit SAST 分析器不扫描某些目录,比如包含测试代码的目录。也许你知道你的测试中充满了安全漏洞,但你不在乎,因为客户永远不会使用这些代码。你可以通过编辑 .gitlab-ci.yml 来设置这个配置选项,以覆盖触发 Bandit 的作业定义,并在新的作业定义中设置一个作业范围的变量:

bandit-sast:
  variables:
    SAST_BANDIT_EXCLUDED_PATHS: "*/my_tests/*"

不要被变量的奇怪值吓到。这个特殊的变量期望一个使用稍微不同的 fnmatch 语法写成的值。这就是你在查阅 GitLab 文档以了解更多关于这个和其他扫描器的各种配置选项时学到的细节。

如果你将这段代码添加到你的 CI/CD 配置文件并重新运行管道,Bandit 分析器将停止报告在 my_tests 目录中发现的任何漏洞。我们还没有讨论如何查看这些分析器的结果,但别担心 —— 这些内容将在本章稍后部分介绍。

使用 GUI 配置 SAST

最后,你可以使用 GitLab 的 GUI 设置某些扫描器的配置选项。例如,在同一 GUI 页面上,你可以配置 SAST 使用来自其他 Docker 镜像的分析器、更改它运行的管道阶段,或更改它在检测项目中的语言时搜索的目录深度。如果你发现某些特定语言的 SAST 分析器没有帮助,或者生成的结果与其他分析器重复,你也可以在该 GUI 页面上禁用这些分析器。

与其他扫描器类型相比,SAST 提供了异常多的配置选项,可以通过图形用户界面(GUI)进行设置。GitLab 文档可以为你提供更多关于其他扫描器在 GUI 中可用选项的信息,以及哪些选项必须通过编辑 .gitlab-ci.yml 文件来设置。

查看 SAST 的发现

一旦启用了 SAST,并且可以选择进行配置,它将根据项目中检测到的语言运行相应的分析器,并在 GitLab 的三个安全报告中显示其结果。例如,这里是 GitLab 漏洞 报告 区域中描述的与临时目录相关的漏洞的发现:

图 7.2 – SAST 发现

图 7.2 – SAST 发现

这就是我们对 SAST 的总结。接下来,让我们讨论一个相关但独立的安全扫描器:秘密检测(Secret Detection)。

使用秘密检测在你的代码库中查找私人信息

你可以将秘密检测(Secret Detection)看作是 SAST(静态应用安全测试)的一种特殊、专注的版本,专门用于发现那些意外出现在源代码中的秘密,例如美国社会保障号码或 AWS 部署密钥。它的工作方式与 SAST 相同——也就是通过扫描源代码,而不是通过与正在运行的应用程序进行交互。

秘密检测曾经是 GitLab SAST 功能的一部分,但最终被单独分离出来,成为一个独立的一级安全扫描器。我们提到这一点是为了避免你在遇到旧文档或博客文章中的引用时感到困惑,因为这些资料可能会提到秘密检测是由 GitLab 的 SAST 扫描器执行的。

理解秘密检测

秘密检测会寻找各种各样的字符串,这些字符串代表了那些通常不应存储在 Git 仓库文件中的秘密。除了前面提到的社会保障号码和 AWS 部署密钥,下面是它寻找的 50 多种秘密中的一些:

  • 短期和长期有效的 Dropbox API 令牌

  • GitLab 个人访问令牌

  • Heroku API 密钥

  • 私有 SSH 密钥

  • Stripe 访问令牌

例如,秘密检测应该能找到并报告以下 Python 代码片段中包含的所有三个秘密:

MY_SSN = '123-45-6789'
MY_GITLAB_ACCESS_TOKEN = 'glpat-txQxy1frpAJodkxJYL8U'
MY_PRIVATE_SSH_KEY = '''
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzbmUAAAAEbm9uZAwAAAAtzc2gt==
-----END OPENSSH PRIVATE KEY-----
'''

秘密检测基于正则表达式(或regexes):扫描器为每种类型的字符串创建了一个正则表达式,用于检测并报告任何与这些正则表达式匹配的字符串字面量。

正则表达式的使用意味着秘密检测完全与编程语言无关。由于它只是扫描文件,看是否有任何字符串与正则表达式匹配,它并不关心你的仓库中使用的是哪种计算机语言。这意味着,与 SAST 不同,秘密检测不需要针对不同语言的单独分析器:它使用一个分析器来处理所有源代码文件。秘密检测甚至不依赖于文件类型:它可以扫描配置文件、README 文件、纯文本文件以及仓库中的任何其他非二进制文件。它可以像在 Go 源代码文件中查找字符串一样,轻松地在 JSON 配置文件中查找匹配正则表达式的字符串。

虽然正则表达式是一个强大的工具,但秘密检测依赖它们也意味着扫描器有一个重要的限制:它无法检测密码。这个限制一开始可能让人惊讶,但一旦你意识到一个设计良好的密码应该很难用正则表达式捕捉到,这就能理解了。很难设计一个能够捕捉任何可能的密码的正则表达式,而不会同时匹配到像文档中的一句话或图形用户界面元素中的一系列词语这样的非秘密文本。除了这个特定的情况,秘密检测在发现那些应该存储在比 Git 仓库更安全位置的字符串方面表现出色。

有一个极好的功能是 Secret Detection 是唯一提供的 GitLab 扫描器功能:历史模式。如果启用此模式,Secret Detection 将扫描您仓库中的所有提交,查看是否曾经有任何秘密被提交到其中。

为什么历史模式很重要?由于 Git 等版本控制系统的目标之一是允许您回到任何时刻文件的状态,因此即使秘密在下一个提交中被立即删除,也可以很容易地看到任何曾经被提交的秘密。一旦秘密进入 Git 仓库,它始终是可恢复的。因此,每当 Secret Detection 发现一个秘密时,它在任何安全扫描报告中创建的条目总是会提到该秘密不仅应该被删除,还应该被 撤销。任何进入 Git 仓库的秘密都应该被认为已经暴露给了全世界,应该不再使用。

这是一个非常重要的观点!如果 Secret Detection 找到一个密码,您应该立即停用该密码并设置一个新密码(当然,您不应该将新密码提交到 Git)。如果它检测到一个部署密钥,您应该取消该密钥并创建一个新的。这一原则适用于任何类型的秘密。如果 Secret Detection 发现了它,单纯从仓库中删除它是不够的。它应该被认为不再可用,并应尽快更换。

启用和配置 Secret Detection

由于 Secret Detection 以前是 SAST 的一部分,因此您可以使用相同的手动或基于 GUI 的方法来启用这两个扫描器,这一点并不令人惊讶。

要手动启用 Secret Detection,请确保在您的管道中定义了一个 test 阶段。然后,包含 GitLab 提供的模板,其中包含与 Secret Detection 相关的作业定义:

stages:
  - test
include:
  - template: Security/Secret-Detection.gitlab-ci.yml

就是这么简单!下次触发管道时,您会注意到一个 Secret Detection 作业现在在 test 阶段下运行。

要通过图形界面启用 Secret Detection,您可以使用与启用 SAST 相同的过程:点击 .gitlab-ci.yml 文件,包含之前描述的 Secret Detection 模板。如果您合并请求并触发新的管道运行,您会发现 Secret Detection 已经在您的管道中启用。与 SAST 一样,许多人发现这个过程比直接手动编辑 .gitlab-ci.yml 更繁琐,但您的体验可能有所不同。

与大多数 GitLab 安全扫描器一样,Secret Detection 有多个可配置选项。您可以在 GitLab 文档中了解所有选项及其设置方法,但这里有两个特别值得强调。

首先,您可能想要启用特定的 test/ 目录,并将假的部署密钥存储在 docs/ 目录中。您可能希望通过排除这些目录,防止 Secret Detection 将它们标记为安全漏洞。

您可以通过覆盖由秘密检测 CI/CD 模板提供的 secret_detection 作业定义,然后设置作业范围的变量来设置这两个配置选项。

secret_detection:
  variables:
    SECRET_DETECTION_HISTORIC_SCAN: "true"
    SECRET_DETECTION_EXCLUDED_PATHS: "tests,docs"

已配置并启用秘密检测,现在让我们查看其发现。

查看秘密检测的发现

一旦您根据喜好启用和配置了秘密检测,并且它在流水线中成功运行,您可以像在 SAST 结果中一样在漏洞报告区域看到结果。例如,这些是在之前提供的 Python 代码上运行秘密检测生成的结果:

图 7.3 - 秘密检测发现

图 7.3 - 秘密检测发现

现在您已经掌握了秘密检测,是时候看看 GitLab 兵器库中的下一个安全扫描器:DAST。

使用 DAST 查找 Web 应用程序中的漏洞

让我们继续下一个类型的安全扫描器:DAST。此扫描器与您的代码交互运行,而不是查看您的源代码。如果 SAST 和秘密检测是“白盒”测试的例子 - 它们查看您的应用程序内部的工作方式 - 那么 DAST 是“黑盒”测试的一种形式 - 它只发送输入并查找输出中的潜在问题或安全漏洞,而不知道您的应用程序如何执行输入到输出的转换。

理解 DAST

DAST 测试Web 应用程序 URLWeb API 端点。如果您向 DAST 提供网站主页的 URL,它将访问该页面,识别页面上的任何链接或可点击的 GUI 元素,跟随这些链接或点击这些元素,并重复此过程。它将继续执行此“爬行”过程,直到访问您的应用程序内可达到的每个页面。在每个步骤中,它检查 Web 应用程序返回的结果,以查看是否存在任何问题。以下是它寻找的问题类型的三个示例:

  • 暴露私人个人信息

  • 缺少跨站请求伪造令牌

  • 通过查询字符串接受敏感信息,如密码

如果您告诉 DAST 扫描 Web API 端点,它会向端点发送信息并分析响应,寻找相同类型的问题。

无论是针对 URL 还是 Web API 端点,DAST 都可以在 被动主动 模式下运行。每次 DAST 运行时,它都会执行被动扫描,这意味着它会发送类似真实用户请求的良性、无害的请求。如果你想对你的 Web 应用进行更深入的分析,可以启用所谓的 完整扫描,它会在通常发送的被动请求基础上添加主动攻击。这些主动攻击更具攻击性,如果它们被指向你不拥有的网站或 Web API,可能会被视为恶意攻击。然而,这些攻击非常宝贵,因为它们模拟了实际黑客可能使用的攻击类型,因此揭示了许多可能被恶意行为者利用的弱点。

启用和配置 DAST

DAST 提供了大量的配置选项。我们将介绍一些最常用的选项,这些足以帮助你快速开始使用 DAST。像往常一样,GitLab 官方文档提供了关于如何以非默认方式配置 DAST 的完整细节。

使用 GUI 启用和配置 DAST 是最简单的。这个过程的具体细节可能会在未来的 GitLab 版本中有所变化,因此我们将介绍高层次的概念,而不深入探讨具体的细节。

你可以通过访问你为 SAST 和秘密检测使用的相同安全扫描器配置页面来开始启用和配置 DAST:点击左侧导航栏中的 安全性与合规性 选项,并选择 配置。从那里,你将能够点击一个按钮来启用 DAST,尽管该按钮首先会将你引导到另一个页面,让你设置 DAST 需要通过的一些配置选项。

首先,你需要设置一个 扫描器配置文件。该配置文件告诉 DAST 是否只使用被动扫描,还是同时执行主动扫描。你还可以设置超时值,以限制 DAST 在爬取网站时的时间。GitLab 会让你为此配置文件命名,以便以后可以将同一个配置文件用于多个不同的 URL 或 Web API 目标。

其次,GitLab 会引导你创建一个 站点配置文件。该配置文件包含你想要扫描的网站首页或 Web API 端点的 URL。如果你扫描的是一个网站,你可以选择性地向站点配置文件添加身份验证凭据。这允许 DAST 像用户一样登录到网站,通常会暴露出更多需要扫描的 URL。

在你创建了这两个配置文件后,GUI 会呈现一个代码片段,你可以将其复制并粘贴到项目的 .gitlab-ci.yml 文件中。这与通过 GUI 启用 SAST 或秘密检测的合并请求驱动工作流稍有不同,但结果是一样的:你只需向 CI/CD 配置文件中添加几行代码,指示 DAST 在你的流水线中运行。

你可以通过 DAST 目标哪些 URL?

尽管你可以使用任何网站或 Web API 的 URL 创建站点配置文件,但我们强烈建议你仅针对自己拥有和管理的网站以及 API 端点进行扫描。如果你使用的是 DAST 的全面扫描选项,该选项会进行更具侵略性的扫描,尤其如此。此外,我们建议你仅在应用程序处于审查、暂存或预生产环境中时运行 DAST。将 DAST 用于生产版本的应用程序可能会导致其不稳定,降低真实用户的性能,甚至可能完全使其崩溃。

如果你更喜欢手动启用和配置 DAST,你需要在管道中deploy阶段之后添加一个dast阶段。你还必须包括 DAST 模板,设置一个包含你希望 DAST 扫描的 URL 的全局变量,并设置任何需要修改 DAST 行为的其他全局或作业范围的变量:

stages:
  - deploy
  - dast
include:
  - template: DAST.gitlab-ci.yml
variables:
  DAST_WEBSITE: https://example.com
  DAST_FULL_SCAN_ENABLED: "true"

你可能已经注意到,DAST 是我们遇到的第一个需要在专用阶段运行的扫描器。之所以如此,一旦你记住 DAST 扫描的是执行中的代码而不是源代码,这个原因就变得显而易见:它不能在代码构建和部署之前执行你的代码。这些任务通常发生在builddeploy阶段,因此 DAST 必须在这些阶段之后的一个阶段中进行。

由于 DAST 有时需要很长时间才能遍历网站的所有页面,而且你通常不希望在 DAST 执行时阻塞管道,因此 GitLab 还允许你按需运行 DAST 扫描或根据计划运行。触发按需扫描或创建扫描计划的过程非常简单:在左侧导航窗格的安全与合规选项中,选择按需扫描,然后让图形界面向导引导你完成整个过程。

如前所述,DAST 提供了种类繁多的可配置选项。这里无法一一列举,但到目前为止我们讨论的选项已经足够让你在大多数情况下获得有用的漏洞发现。如果你需要为蜘蛛过程设置超时值、禁用特定的漏洞检查、为目标网站设置登录凭证,或以其他方式调整 DAST 的行为,你可以在官方 GitLab 文档中找到所有需要的信息。

查看 DAST 的发现

DAST 扫描的结果会显示在漏洞报告区域,就像 SAST 和秘密检测的结果一样。由于我们没有完整的Hats for Cats应用可供运行 DAST,这里是运行 DAST 被动扫描 example.com 的一些示例结果。你可以看到,所有的结果都涉及标题字段,这是被动扫描最常见的发现类型:

图 7.4 – DAST 发现

图 7.4 – DAST 发现

这就是你对 DAST 的介绍。现在,让我们转换一下话题,研究一下依赖扫描,这是一个安全扫描类型,它检查你从外部来源导入到项目中的代码。

使用依赖扫描来发现依赖中的漏洞

为什么要自己编写函数,而别人已经编写、测试并文档化了一个库,可以完美地完成你所需的功能呢?通常,找到第三方的 Python 模块、Ruby gem、Java JAR 或其他开源软件包是很容易的,它们能加速项目的开发。不幸的是,这些第三方依赖可能包含安全漏洞,如果你将它们引入到项目中,你也会继承这些问题。GitLab 的依赖扫描功能就是在这个时候发挥作用——它确保你使用的任何依赖都是没有已知漏洞的。

理解依赖扫描

像 SAST 一样,依赖扫描支持许多语言——包括你期待的所有主流语言——但并不是所有语言都支持。你可以查阅 GitLab 文档,查看支持的语言的最新列表。

依赖扫描知道如何解析每种支持语言的包管理器所使用的配置文件,并利用这些信息来确定项目依赖哪些库。例如,它可能会扫描 Ruby 项目的 Gemfile.lock、Python 项目的 requirements.txtrequirements.pip,或者使用 Maven 构建工具的 Java 项目的 pom.xml 文件。正如你从我们提到的两个不同的 Python 配置文件中看到的那样,扫描器足够智能,知道某些语言使用多个文件来列出它们的配置,它可以解析每种语言中最常用的文件。就像 SAST 一样,依赖扫描能够处理包含多种不同计算机语言的项目。它会解析它检测到的任何语言的依赖配置文件,并在所有这些文件中寻找漏洞。

例如,如果你的网站 “Hats for Cats” 是建立在旧版本的 Django Web 框架上的,项目中可能会有一个 requirements.txt 文件,其中只有一行内容:

django==4.0

扫描器不仅会查找项目显式声明的依赖的名称和版本号,还会查找这些依赖的任何子依赖的名称和版本号。换句话说,它会递归地遍历依赖树,检测传递性依赖以及直接依赖。它会报告在依赖树中的任何依赖中发现的漏洞,这意味着你可能会看到一些你甚至不知道项目使用的依赖中存在的漏洞。

一旦依赖扫描知道了项目中每个依赖的名称和版本号,它会在数据库中查找每个依赖的名称和版本号,查看该版本的特定库是否存在已知漏洞。需要理解的是,依赖扫描并不会像 SAST 那样扫描依赖代码——也就是说,它不会分析依赖中的代码,试图发现新的漏洞。相反,它采用了一种更加直接的策略。它只是确定数据库中是否包含有关该版本依赖库中已知漏洞的信息。这听起来可能是一个不太复杂的方法,但事实证明它非常有用,并且在揭示常用库中存在的问题方面表现得相当出色。

依赖扫描有一个特殊功能,虽然并不总是可用,但在可用时可以节省不少时间。如果扫描器检测到库的旧版本中存在漏洞,并且知道该漏洞在同一库的后续版本中已被修复,它有时会提供创建一个合并请求的选项,重新编写项目的依赖配置文件,使其使用后续版本的已修复库。这只在某些情况下和某些语言中发生,但在提供此选项时,值得利用。

启用和配置依赖扫描

您可以像在 SAST、Secret Detection 或 DAST 中那样,通过图形界面或手动方式将依赖扫描添加到项目的流水线中。通过图形界面启用它,只需点击.gitlab-ci.yml文件,该文件启用扫描器。合并合并请求,操作完成。

手动启用更为简单。只需确保您的流水线已定义test阶段,并将依赖扫描模板添加到.gitlab-ci.yml中:

stages:
  - test
include:
  - template: Security/Dependency-Scanning.gitlab-ci.yml

与其他扫描器一样,依赖扫描有多个可配置选项。同样,像其他扫描器一样,这些选项可以通过在.gitlab-ci.yml中设置全局变量,或通过重写作业定义并在该文件中设置作业范围的变量来控制。

例如,以下代码设置了一个作业范围的变量,告诉依赖扫描的 Python 分析器查找一个非标准名称的依赖配置文件,而不是传统的requirements.txt文件:

gemnasium-python-dependency_scanning:
  variables:
    PIP_REQUIREMENTS_FILE: "hats-for-cats-requirements.txt"

查看依赖扫描结果

漏洞报告显示了在项目依赖中检测到的任何潜在安全问题。例如,以下是来自《Hats for Cats》项目中依赖于旧版本 Django 库的五个关键和高严重性的漏洞,正如示例中的requirements.txt文件所指定的那样:

图 7.5 – 依赖扫描发现

图 7.5 – 依赖扫描发现

到现在为止,你应该已经牢牢掌握了依赖扫描的功能和使用方法。接下来,我们来看看一个以 Docker 为中心的安全扫描工具:容器扫描。

使用容器扫描查找 Docker 镜像中的漏洞

容器扫描对 Docker 镜像的作用类似于依赖扫描对项目依赖的作用:它检查你的项目在构建 Docker 镜像时使用的特定版本的 Linux 发行版中已知的漏洞。

理解容器扫描

如果你将应用程序打包并部署为 Docker 镜像——或者更严格地说,是一个符合开放容器倡议(OCI)标准的镜像——你应该使用 GitLab 的容器扫描功能来查找镜像构建所基于的基础 Linux 发行版中的已知漏洞。

如果你之前没有使用过 Docker 镜像,这听起来可能有些神秘,但其实并不复杂。可以把 Docker 镜像想象成类似于虚拟机的东西。它有一个特殊的文件叫做Dockerfile,它作为创建该虚拟机的“配方”。这个Dockerfile文件指定了使用哪个 Linux 发行版作为虚拟机的操作系统,应该在 Linux 上安装哪些额外的软件包来支持你的应用程序,以及最终要在虚拟机上安装的应用程序是什么。操作系统、依赖项和你的应用程序组成了整个 Docker 镜像。

容器扫描会寻找漏洞,既包括在基础 Linux 操作系统中默认安装的软件包,也包括你在Dockerfile中指定的任何额外软件包。正如你所料,Linux 发行版越旧,安装的依赖越多,容器扫描可能发现的问题就越多。

尽管容器扫描不能找到所有 Linux 发行版的所有版本中的漏洞包,但它支持最常用的发行版的最后两个或三个版本。除非你使用了一个非常特殊的发行版作为应用程序 Docker 镜像的基础,否则应该能够使用容器扫描。如果你想确保你的镜像是可以扫描的,GitLab 文档中有一个支持的发行版列表。

容器扫描有一个默认禁用的可选功能:它还可以查找“语言包”中的漏洞,语言包是由语言的包管理器添加的库。例如,你可能使用 Ruby 的bundler工具安装Ruby on Rails gem,或者使用 Python 的pip工具安装Flask模块。你可能会注意到,这个功能与 GitLab 的依赖扫描功能覆盖了相同的领域——因此,通常会产生重复的结果。因为这个原因,许多 GitLab 用户更倾向于使用依赖扫描,而不是启用容器扫描中的这个功能。

尽管容器扫描可以查找任何它能够通过网络访问的 Docker 镜像中的问题,但其默认行为是扫描在你项目的容器注册表中找到的任何镜像。容器注册表是 GitLab 项目提供的一项功能,允许你将 Docker 镜像存储在一个安全的、受访问控制的位置,而不是存储在像 Docker Hub 这样的公开站点,或像 Artifactory 这样的工具实例中。要使用容器扫描检查容器注册表中的镜像,你需要让你的管道构建一个 Docker 镜像并将其推送到注册表。我们将在下一章讨论有关部署策略的内容。

启用和配置容器扫描

你可以通过手动编辑 .gitlab-ci.yml 或使用 GitLab 图形界面来启用容器扫描。若手动启用,确保你的管道包含一个 test 阶段,并添加容器扫描模板:

stages:
  - test
include:
  - template: Security/Container-Scanning.gitlab-ci.yml

要使用图形界面配置容器扫描,你可以使用与目前为止讨论的其他扫描器相同的技术:点击左侧导航栏中的安全与合规选项,选择配置,然后找到启用容器扫描的控件。这样会生成一个合并请求(MR),将前述模板添加到你的 CI/CD 配置文件中。合并这个 MR 后,你就完成了。

如果你希望容器扫描检查项目的容器注册表中的 Docker 镜像,那么这些手动或图形界面启用扫描器的方法就是你需要执行的唯一步骤。但是,如果你需要更改其默认行为,你可以使用你已用于配置其他扫描器的两种方法:设置全局变量或覆盖作业定义并设置作业作用域变量。此扫描器的一些可配置选项包括将扫描器指向存储在你项目容器注册表以外位置的 Docker 镜像、启用语言包扫描,或设置漏洞必须具备的最低严重性级别,才能包含在容器扫描的结果中。

查看容器扫描的结果

在你构建的 Docker 镜像中,容器扫描发现数十个漏洞并不罕见,尤其是当你使用的基础镜像是较旧的 Linux 发行版时。考虑到每个 Linux 发行版默认安装的大量软件包,以及在开源软件包中发现漏洞的速度,这并不令人惊讶。

例如,Alpine Linux 作为一个小巧的主要发行版而著名,这意味着它安装的包比其他流行发行版(如 Ubuntu 或 Debian)少。这使得它成为 Docker 镜像的基础发行版之一。如果您基于 Alpine Linux 版本 3.14.1(在撰写本文时仅有 10 个月的历史)构建 Docker 镜像,容器扫描会发现其默认包中至少有 30 个漏洞。您可以看到在基于该发行版构建的 Docker 镜像中发现的一些最高严重性漏洞,如漏洞 报告区域所显示:

图 7.6 – 容器扫描结果

图 7.6 – 容器扫描结果

现在我们已经介绍了容器扫描的基础知识,让我们来看看另一个安全扫描工具:许可证合规性。

使用许可证合规性来管理依赖项的许可证

很容易忽视项目中各种依赖项所使用的软件许可证,也容易忘记哪些许可证与项目的总体许可证兼容,哪些许可证因各种原因应被排除在外。在这方面,GitLab 的许可证合规性功能可以提供帮助。

理解许可证合规性

大多数开源库都发布在某种软件许可证下。虽然有数百种许可证可供选择,但通常只有大约 20 种常用许可证,而且每种许可证的法律细节差异较大。如果在项目中使用了第三方库,必须确保该库的许可证与您打算发布软件的许可证兼容。如果它们不兼容,您必须将该依赖项替换为使用更友好许可证的替代库。

当我们谈论许可证时,什么是“兼容”的意思?大多数许可证被认为是宽松的,这意味着您可以几乎出于任何目的使用该许可证下发布的软件。两种广为人知的宽松许可证例子是 MIT 许可证和 BSD 许可证。如果您使用的是宽松许可证的依赖项,通常不会遇到兼容性问题。其他许可证则是保护性的,而非宽松的,这意味着它们限制了您如何使用根据这些许可证发布的软件。以下是保护性许可证所施加的一些限制示例:

  • 一些开源许可证,如 GPL 或 AGPL,属于一种非正式的类别,通常称为反向版权。根据这些许可证发布的软件可以作为依赖项在其他项目中使用,但前提是这些其他项目本身也必须使用相同的反向版权许可证。例如,如果您的《猫帽子》应用使用了一个基于 GPL 许可证发布的开源 Python 排序库,那么整个《猫帽子》项目也必须使用 GPL 许可证。如果您打算销售软件而不是将其作为开源发布,这可能会成为一个问题。

使用一些更具争议性的术语,copyleft(版权左)许可证有时被称为“病毒性”许可证,因为它们可以被认为是“感染”父项目。这个疾病隐喻可能不公平,许多优秀的软件都是通过 copyleft 许可证发布的,但事实依然是,你需要小心使用这些许可证。

  • 一些许可证明确禁止某些行业使用,比如军事行业。例如,基于军事排除许可证发布的代码不能作为导弹制导软件的依赖项使用。

  • 一些许可证可能排除某些国家/地区使用该软件。这种情况并不常见,但许可证包含此类限制是合法的。

如你所见,理解你的项目依赖项使用的许可证非常重要,这样你就可以意识到这些许可证可能带来的任何限制,并且避免使用那些发布在限制你以预期方式使用或销售软件的许可证下的依赖项。

GitLab 的 License Compliance 功能分为三个阶段:

  • 扫描器会查看项目的依赖项,并生成所有使用的许可证的列表。

  • 软件开发团队或公司法务部门会创建许可证政策,明确允许或拒绝项目依赖项中找到的每个许可证。或者,如果你已经知道哪些许可证与你的主项目的整体许可证不兼容,可以在添加任何依赖项之前预先创建许可证政策。

  • 如果开发者在分支上引入了一个新的依赖项,而该依赖项使用了一个被拒绝的许可证,那么 License Compliance 功能会阻止该分支的合并请求,直到移除许可证或解除阻止。

从这个工作流中,你可以看出,创建许可证政策是使用 License Compliance 功能的重要部分。要查看、创建、删除或编辑许可证政策,请点击左侧导航窗格中的安全与合规性,然后点击许可证合规性。以下是当你批准了两个许可证并拒绝了两个其他许可证时,该屏幕的显示效果:

图 7.7 – 许可证政策

图 7.7 – 许可证政策

工作流描述提到,License Compliance(许可证合规性)具有阻止任何引入依赖项且使用被许可证政策明确拒绝的许可证的合并请求(MR)的能力。为了阻止 MR,License Compliance 会停用或隐藏合并按钮,使其无法点击。以下是一个被阻止的 MR,显示了在 MR 分支上引入的新依赖项(及其依赖项)所使用的许可证列表:

图 7.8 – 被拒绝许可证阻止的合并请求

图 7.8 – 被拒绝许可证阻止的合并请求

使用 GitLab 规则集解除 MR 阻止

除了您刚刚看到的情况,GitLab 还有几个其他触发器,导致合并请求(MR)被阻止,直到采取某种纠正措施为止。举一个例子,当开发者编写新产品代码时,如果没有为该代码编写自动化测试,GitLab 可以配置为注意到开发者分支上的“代码覆盖率”下降,并且该分支的 MR 会被阻止,直到开发者添加自动化测试来测试他们的新代码。

任何被自动阻止的 MR 都可以通过 GitLab 的 审批规则 功能解锁。这些规则允许特定人员仅通过批准该 MR 来解锁。每个审批规则都有一个名称;允许您覆盖由被拒绝许可证引起的阻止的规则称为 License-Check。我们没有足够的篇幅在这里详细介绍审批规则,但它们的设置和使用非常简单。GitLab 文档可以为您提供更多细节。

启用和配置许可证合规性

您无法使用 GUI 启用许可证合规性,但启用它的手动方法现在应该很熟悉了。只需确保在您的流水线中定义了 test 阶段,并包含许可证合规性模板:

stages:
  - test
include:
  - template: Security/License-Scanning.gitlab-ci.yml

配置许可证合规性可以像配置其他扫描工具一样完成:编辑 .gitlab-ci.yml 文件,设置全局变量或覆盖作业定义并设置作业范围的变量。

如前所述,配置许可证合规性的重要部分是创建许可证政策,规定哪些开源许可证可以或不可以用于项目的依赖项。要创建或编辑这些政策,请导航到左侧导航窗格中的安全与合规性选项,点击许可证合规性,然后点击政策标签。在那里,您可以添加政策来允许或拒绝 GitLab 识别的数百种许可证中的任何一种。无论项目是否包含这些许可证的依赖项,您都可以为许可证创建政策。在项目开发开始之前,您的法务团队可能需要为任何被拒绝的许可证创建政策,这样如果合并请求引入了使用被拒绝许可证的依赖项,就会立即被阻止。

编辑许可证政策的先决条件

GitLab 并没有很好地记录这部分内容,但要添加、删除或编辑许可证政策,您必须首先运行至少一个包含许可证扫描作业的流水线实例。这样做会“解锁”允许和拒绝的许可证列表,并允许您编辑政策。

查看许可证合规性的发现

与我们迄今为止查看的其他扫描工具不同,许可证合规性不会在 漏洞报告 窗口中显示其发现的内容。要查看它在项目默认分支的依赖项中识别出的许可证,点击左侧导航窗格中的安全与合规性选项,然后点击许可证合规性

要查看依赖项在功能或 bug 修复分支上的许可证,请导航到该分支上运行的流水线详情页并点击 Licenses 标签。此视图还会显示哪些许可证是允许的,哪些是被拒绝的,哪些尚未分类。

现在你已经具备了将许可证合规性应用于项目的能力。是时候处理 GitLab 提供的最后一个安全扫描工具:基础设施即代码扫描(IaC)。

使用 IaC 扫描来查找基础设施配置文件中的问题

在过去十年左右,“把硬件当作牲畜,而非宠物”这句话被用来描述一种新的计算机管理方法。通过将硬件视为一种可替代的商品,而不是一堆特殊的“雪花”计算机,开发和运维团队可以摆脱仔细配置和维护用于托管部署环境、运行数据库、提供 Web 应用或进行软件开发与部署中涉及的其他无数任务的计算机的困扰。通过使用像 Ansible 或 Terraform 这样的 IaC 工具来配置和维护硬件(无论是实际的还是虚拟的,本地的还是基于云的)上的配置状态,系统管理员可以调整服务器容量、创建新环境或实验硬件配置,而无需担心如果出现问题或实验失败,恢复系统的难度。要恢复正常,他们只需抹掉旧机器,并使用已经证明有效的设置通过 IaC 工具自动重新配置这些机器。这样为开发团队节省的时间和获得的自由是巨大的。

但这种新自由带来了新的漏洞类型。很容易创建会导致安全漏洞的 IaC 工具配置文件,这些漏洞会影响使用这些文件配置的机器。GitLab 的 IaC 扫描工具正是用来发现这种类型的漏洞。一旦识别出这些漏洞,团队可以在配置文件中修复它们,然后轻松地重新配置机器,使用更新且更安全的设置。

理解 IaC 扫描

GitLab 的 IaC 扫描是一种专门的 SAST 形式。它会浏览项目的仓库,查看是否能找到任何来自支持的 IaC 工具的配置文件(GitLab 文档可以为你提供这个扫描工具支持的 IaC 工具的最新列表)。然后,它会识别这些配置文件中的漏洞或不良编程实践。

下面是一个简单的 Terraform 配置文件,用于创建一个 S3 存储桶:

resource "aws_s3_bucket" "testBucket" {
    bucket = "myBucket"
    acl = "authenticatedRead"
}

这段代码看起来足够简单,但它引入了几个安全漏洞,并未遵循 Terraform 配置文件的一些最佳实践,正如你将在本章后面的 IaC 扫描报告中看到的那样。

启用和配置 IaC 扫描

与许多其他扫描器一样,你可以通过 GUI 或手动编辑 .gitlab-ci.yml 来启用 IaC 扫描。要通过 GitLab GUI 启用,可以使用你启用其他扫描器时相同的技巧:在左侧导航栏的 安全与合规 选项下,点击 配置,告诉 GUI 创建合并请求并合并该请求。

要手动启用 IaC 扫描,如果项目的管道中尚未包含 test 阶段,请添加一个,并包含扫描器模板:

stages:
  - test
include:
  - template: SAST-IaC.latest.gitlab-ci.yml

当前,IaC 扫描并不提供任何配置选项,但未来可能会提供。

查看 IaC 扫描的结果

前面展示的用于创建 S3 桶的 Terraform 配置在短短几行代码中就包含了不少问题。存在一些安全漏洞以及在创建 S3 资源时没有遵循最佳实践。漏洞报告中展示的发现对帮助你了解需要在哪里加强 Terraform 代码,消除这些问题非常有价值:

图 7.9 – IaC 扫描结果

图 7.9 – IaC 扫描结果

通过查看漏洞报告中工具栏的发现,你会看到所有 IaC 扫描的发现都列为来自 SAST,而不是 IaC 扫描器。这是因为 IaC 扫描器在 GitLab 中被归类为 SAST 工具组的一部分。要在漏洞报告中仅查看 IaC 发现,你需要在工具下拉菜单中选择 SAST——即使这样,你仍然会看到来自 SAST 工具以及任何你已集成到 SAST 工具组中的第三方扫描器的发现。

这就结束了我们对 GitLab 七大安全扫描器的概览。接下来,让我们讨论 GitLab 提供的两个功能,这些功能能让其安全扫描器更易于使用、更强大:报告和漏洞管理。

理解不同类型的安全报告

所有 GitLab 安全扫描器都会将其结果显示在三个独立的报告中。因为每个报告都显示了所有扫描器的结果,所以无需在 GitLab GUI 中来回跳转,收集来自不同扫描器的信息。然而,这三个报告位置展示的扫描结果略有不同。了解这三份报告的区别非常重要,下面我们逐一了解:

  • mainmaster。如果你想了解你稳定代码库的安全性,请查看漏洞报告。它不会告诉你任何关于功能或 bugfix 分支的安全状态——仅仅是默认分支的情况。

  • 每个运行的流水线的流水线详细信息页面会告诉你该流水线运行所在分支上存在的安全问题。因此,如果流水线运行在默认分支上,它的流水线详细信息页面将包含与漏洞报告相同的信息。但对于运行在功能或 bug 修复分支上的流水线,其流水线详细信息页面会提醒你该分支上存在的漏洞,无论它们是否也存在于默认分支上。如果启用了许可证合规性并且检测到具有许可证的依赖项,该页面将显示一个名为许可证的标签;如果该页面有其他扫描器的检测结果,它将显示一个名为安全性的标签。流水线详细信息页面上显示的信息类型与漏洞报告中显示的相同,尽管格式稍有不同。

  • 每个合并请求都会显示来自最近一次运行于 MR 源分支的流水线的扫描结果。然而,MR 报告与漏洞报告和流水线详细信息报告有一个重要区别:MR 报告只显示最近一次在 MR 源分支上运行的流水线所发现的漏洞与 MR 目标分支上漏洞之间的差异。换句话说,它显示的是差异视图,而不是该分支上所有漏洞的完整列表。

让我们通过一个示例来说明这三种报告之间的区别:

  1. 假设 Hats for Cats 的main分支上有一个 SAST 漏洞和一个被 Secret Detection 发现的问题。

  2. 开发人员从main分支创建了一个名为make-hats-sortable的分支。

  3. 遵循 GitLab Flow 的最佳实践,开发人员创建了一个 MR,make-hats-sortable作为源分支,main作为目标分支。

  4. 开发人员向make-hats-sortable分支提交了代码。假设此次提交修复了 SAST 漏洞,但引入了一个新的 DAST 漏洞。

以下是目前每个报告所显示的内容:

  • main 分支。

  • make-hats-sortable分支(即合并请求的源分支)仅显示 Secret Detection 和 DAST 发现的漏洞,因为 SAST 漏洞已在该分支上修复。

  • make-hats-sortablemain分支。

这就是我们关于不同类型 GitLab 安全报告的讨论。阅读这些报告并了解你的安全漏洞所在之后,如何追踪解决这些漏洞的进展呢?这是下一个章节的主题。

管理安全漏洞

每当除许可证合规性之外的任何扫描器发现漏洞时,它会将该漏洞标记为需要分类状态。此状态会出现在漏洞报告中的漏洞条目和流水线详细信息页面报告中。

你应当决定对每个具有该状态的漏洞打算如何处理,并相应地更改其状态。以下是可能的状态值:

  • 已忽略表示你不打算修复这个漏洞。也许你已经确定这是一个误报,或者你认为这是一个真实的问题,但不值得修复,或者你意识到这个漏洞与你的产品或用户无关。

  • 已确认表示这是一个真实的问题,并且你确实打算修复它。在将问题设置为此状态后,你通常会创建一个问题来跟踪进展,以便团队在修复漏洞时进行追踪。GitLab 在报告的图形界面中提供了一些快捷方式,甚至会自动填充问题的标题和描述,从发现中提取信息,以便尽可能简化修复过程。

  • 已解决意味着你已经修复了该问题,因此它不再出现在你的项目中。此状态需要手动设置。GitLab 不会自动解决漏洞。这是因为它不希望不小心解决仍然存在的问题,从而给你带来虚假的安全感。

GitLab 的漏洞管理功能基本上就是设置漏洞的状态,然后可选地使用 GitLab 问题来跟踪修复该漏洞的进展。一个典型的漏洞管理工作流可能是这样的:

  1. 扫描器报告一个漏洞,将其状态设置为需要 处理

  2. 开发团队评估漏洞并决定不修复它,这时你将其状态设置为已忽略,并停止流程。

或者,团队对漏洞进行优先级评估,决定它确实需要修复,在这种情况下,你可以将其状态设置为已确认

  1. 可选择创建一个包含漏洞信息以及可能的修复指南的问题。这个问题会像其他问题一样被讨论、添加到冲刺中并分配给开发者。

  2. 分配给该问题的开发者创建一个分支,为该分支创建一个合并请求,并在该分支上修复问题。

  3. 合并请求的“增量”安全报告显示该问题存在于默认分支中,但在开发者的分支中不再存在。

  4. 你的团队合并了合并请求。漏洞现在在默认分支中已得到修复。

  5. 你关闭该问题。

  6. 在漏洞报告中,你必须将漏洞的状态设置为已解决

当你看到这个过程按步骤展示时,可能会觉得有些繁琐,但大多数人很快会习惯这种流程,并开始欣赏它给项目的安全状态提供的可见性。

现在,你知道如何跟踪团队修复代码中的安全漏洞。该章节的最后一个话题是:使用 GitLab 提供以外的安全扫描器。

集成外部安全扫描器

许多团队致力于使用一个或多个不属于 GitLab 安全产品的安全扫描器。别担心——通常可以将外部扫描器集成到您的 GitLab CI/CD 管道中。

集成分为两个部分。首先,您需要告诉您的管道触发外部扫描器。这很简单,只要您的扫描器以 Docker 镜像的形式打包,并且可以通过命令行运行:

  1. test阶段创建一个新的管道作业(除非有理由在其他地方运行它)。

  2. 在作业定义中使用images关键字来指定包含您希望添加到管道中的扫描器的 Docker 镜像的位置。

  3. 在作业定义的script部分,使用您手动运行扫描器时所使用的 CLI 命令来触发扫描器。您可能需要传递一些选项给 CLI 命令,以控制它生成结果文件的位置以及使用什么格式。

  4. 在您的作业定义中添加allow_failure: true,这样即使外部扫描器发现漏洞,管道仍然会继续运行。

集成外部扫描器的第二部分是告诉 GitLab 如何将扫描器的结果包含到我们在本章讨论的三个标准 GitLab 安全报告中。如果结果文件写入 JSON 文件,并符合特定的 JSON 模式,GitLab 才能将这些结果整合进去。每种扫描器类型(SAST、DAST 等)都有一个单独的模式。每种扫描器类型的文档提供了有关这些模式的更多信息。

集成生成非标准结果文件的第三方扫描器

如果您的第三方扫描器无法生成与适当模式验证的结果文件,您需要编写一个简短的脚本来解析结果并创建一个符合模式的新结果文件。您需要在扫描器运行后在管道中的某个地方触发此脚本。

在您为第三方扫描器创建的作业定义中,您必须声明扫描器的结果文件为一个工件,特别是包含某种类型安全报告结果的工件。例如,如果您正在集成一个额外的 SAST 扫描器,并且它生成名为my_scanner/results.json的结果文件,您需要在运行该扫描器的作业定义中包含以下代码:

  artifacts:
    reports:
      sast: my_scanner/results.json

这个高级别的描述可能是您集成第三方扫描器所需要的全部内容,但如果您需要更详细的配置说明——包括有关结果模式的指导、命名管道作业和结果文件的最佳实践等——GitLab 官方文档有一篇非常详尽的页面专门介绍这个话题。

总结

安全是本书中最复杂且最重要的话题之一,因此恭喜您完成了这一部分!让我们回顾一下本章所学内容。

首先,我们介绍了所有 GitLab 安全扫描器的基本原理。我们讨论了所有扫描器都是由 GitLab 外部开发的开源工具,以及这一点为什么是好事。我们看到一些扫描器使用不同的分析器来支持不同的计算机语言,尽管所有扫描器都支持最常用的语言,有些扫描器则完全不依赖语言。我们了解了将扫描器打包为 Docker 镜像的含义。我们看到扫描器在发现漏洞时不会停止 CI/CD 流水线,我们还了解到,如果 GitLab 提供的扫描器不足以满足需求,通常可以将外部扫描器集成到流水线中。

然后,我们逐一了解了 GitLab 安全扫描器的功能,学习了每个扫描器查找的漏洞类型,如何通过 GUI 或手动启用它们,如何配置它们的行为,以及如何查看它们的发现结果。我们看到如下内容:

  • 静态应用安全测试(SAST)查找源代码中的漏洞

  • 秘密检测(Secret Detection)查找不应存储在 Git 仓库中的敏感信息

  • DAST(动态应用安全测试)发现运行中的 Web 应用或 Web API 中的漏洞

  • 依赖项扫描(Dependency Scanning)发现你项目中第三方库的已知漏洞

  • 容器扫描(Container Scanning)发现构成你 Docker 镜像基础的 Linux 发行版中的已知问题

  • 许可证合规(License Compliance)识别与整体项目许可证不兼容的依赖项

  • 基础设施即代码扫描(IaC Scanning)查找可能会将漏洞引入你管理的计算机的基础设施配置文件

最后,我们探讨了与扫描器本身相关的三个话题:GitLab 提供的三种不同安全报告之间的区别、管理扫描器识别出的安全漏洞,以及将外部扫描器集成到你的流水线中,以便为需要更多保护的项目提供帮助。

不幸的是,安全问题已经成为软件开发中一个庞大且重要的部分。但正如我们在本章中所看到的,GitLab 提供的一整套检测和修复安全漏洞的工具是其最强大、最有价值的功能之一。我们可以稍微松一口气,因为现在很多潜在问题可以在开发过程早期被识别出来,这时仍然有足够的时间在生产环境中出现尴尬、昂贵或损害声誉的安全漏洞之前解决它们。

到此为止,你的代码已经编写完成、验证无误并且得到了安全保障。软件开发生命周期的下一步是打包和部署它。我们将在下一章中处理这些任务。

第八章:打包和部署代码

在前几章中,你已经学习了如何使用 GitLab 进行源代码管理,并设置 CI/CD 管道来构建、测试和对已提交的代码进行安全扫描。你现在应该已经自信地理解了 GitLab CI/CD 周围的基础设施以及用于编写管道的语法。

本章中,我们将继续软件开发阶段的旅程,现在专注于打包和部署代码。我们将结合使用 GitLab 的内置功能和常见的行业工具,将我们的代码部署到端点或环境。目标是回答这个问题,我们如何使已经构建和测试的应用程序可供我们的用户使用?

本章将介绍新词汇,扩展我们对 GitLab CI/CD 语法的理解。还会提到并使用第三方工具,如 Docker,以及云服务提供商,如 Google Cloud Platform。由于本书的核心重点是 GitLab 的功能,我们没有空间对每个可以与 GitLab 配合使用的工具进行技术性深入探讨,因此你可能对本章中提到的某些技术不太熟悉。因此,我们将尽力使用行业标准语言,重点介绍每个工具与 GitLab 的集成,并提供可以复制的示例,不要求读者拥有进一步的知识。完成本章后,你将掌握使用 GitLab 原生软件包和容器注册表功能来托管已完成构建所需的技能。你还将扩展对 GitLab CI/CD 的知识,包括使用传统或云原生基础设施将代码部署到评审和生产环境。

以下是本章将涵盖的主题:

  • 将代码存储在 GitLab 的软件包注册表中,以备后续重用

  • 将代码存储在 GitLab 的容器和软件包注册表中,以备后续部署

  • 使用 GitLab Flow 部署到不同环境

  • 部署到评审应用进行测试

  • 部署到实际生产环境

  • 部署到 Kubernetes 集群

技术要求

与前几章一样,如果你有一个 GitLab 实例账户(SaaS 或自托管),你将能最大限度地从本章中受益。此外,本章的主题和示例将越来越侧重于部署到 GitLab 之外的环境。提到的基础设施工具包括:

  • 托管 GitLab 的服务器(或 GitLab.com)

  • 自托管运行器(或托管在 GitLab.com 上的 SaaS 运行器)

  • Docker(也可在托管在 GitLab.com 上的 SaaS 运行器上使用)

  • Kubernetes

  • 云平台服务(如 Amazon Web Services、Google Cloud Platform 或 Heroku)

如果您希望最小化需要安装和维护的工具量,我们建议使用 GitLab.com 和 SaaS 运行器。如果您希望练习将应用部署到实时或复杂的基础设施,我们也建议在云服务平台上创建一个帐户。只要注意使用这些服务时可能产生的费用。

将代码存储在 GitLab 的软件包注册表中以供后续重用

作为作为完整 DevOps 平台的目标之一,GitLab 在每个项目中都包含启用软件包和容器注册表的选项。软件包注册表支持以多种格式托管软件包和语言包,容器注册表作为专用容器镜像的仓库。这些功能使团队能够方便地托管、组织和版本控制完成的构建,并与源代码一起使用。我们将依次讨论软件包和容器注册表。

定位 GitLab 的容器和软件包注册表

GitLab 项目技术上支持三种内置注册表。它们是软件包、容器和基础设施注册表,可用于存储完成的代码,无论是供终端用户使用还是供其他软件项目使用。本书将重点讨论软件包和容器注册表;基础设施注册表是 GitLab 最近添加的功能,专门用于托管 Terraform 模块。GitLab 还支持将构件推送到外部或第三方注册表,尽管这超出了我们在此讨论的范围。

图 8.1 显示了在项目和组级别下,GitLab 的软件包和容器注册表的位置,在左侧导航窗格中的 软件包和注册表 下。

图 8.1 – 左侧边栏中的软件包和注册表

图 8.1 – 左侧边栏中的软件包和注册表

当你将鼠标悬停在 软件包和注册表 上时,你会看到软件包、容器和基础设施注册表都有各自的页面可以导航,如 图 8.2 所示。

图 8.2 – 软件包和注册表子菜单项

图 8.2 – 软件包和注册表子菜单项

如果我们还没有配置或推送任何构件到注册表,当我们访问注册表页面时,会看到一条适当的提示信息。图 8.3 显示了我们在 软件包注册表 页面上看到的提示信息,并附带了使用软件包注册表的 GitLab 文档链接。

图 8.3 – 没有软件包的项目软件包注册表

图 8.3 – 没有软件包的项目软件包注册表

图 8.4 向我们展示了一个未填充的 容器注册表。在这里,GitLab 提示我们可以使用 Docker 命令构建应用程序的容器化版本,然后将其推送到容器注册表。

图 8.4 – 没有容器镜像的项目容器注册表

图 8.4 – 没有容器镜像的项目容器注册表

我们很快会看到,将内容推送到 GitLab 注册表的典型方式是配置执行构建工件的命令的 CI/CD 作业,对其进行身份验证并将工件推送到注册表,通常使用 GitLab 的 API。

开始使用软件包注册表

在 GitLab 中,软件包注册表默认启用,尽管管理员可以在实例级别禁用它。在项目设置中,你还可以选择在 设置 | 常规 | 可见性、项目功能、权限 下启用或禁用软件包注册表功能。如果将 Packages 选项关闭,则左侧边栏将无法显示该功能。图 8.5 显示了在项目设置中启用或禁用软件包注册表的位置。

图 8.5 – 在项目设置中启用软件包注册表

图 8.5 – 在项目设置中启用软件包注册表

现在问题来了,我们如何使用软件包注册表?也就是说,我们如何将软件填充到其中?填充软件包注册表有两个主要步骤:首先是对注册表进行身份验证,然后是通过 CI/CD 作业构建并上传软件包。但在深入了解这些步骤之前,我们先列出 GitLab 支持的软件包格式。

支持的软件包格式

截至本书写作时,以下软件包管理器格式通常可用并已完全支持 GitLab 的软件包注册表:

  • 通用软件包

  • Maven

  • npm

  • NuGet

  • PyPI

以下软件包格式要么是受功能标志控制,要么使用时存在已知问题:

  • Composer

  • Conan

  • Debian

  • Go

  • Helm

  • Ruby Gems

每个软件包管理器都有自己的配置格式和语法。为了保持一致性,并专注于 GitLab 软件包的核心概念,我们将使用通用的软件包格式来展示示例。我们将从对 GitLab 软件包注册表进行身份验证开始。

对注册表进行身份验证

我们在本章前面提到过,使用 GitLab 的注册表主要通过 CI/CD 作业进行。身份验证、上传到注册表和使用注册表中的软件包的操作应该通过 GitLab CI/CD 配置中定义的作业任务来表示。一般来说,你可以通过以下四种凭据之一来验证 GitLab 的软件包注册表:

  • 个人访问令牌,用于使用用户权限进行身份验证

  • 项目部署令牌,用于访问项目中的所有软件包

  • 一个组部署令牌,用于访问组内或其子组中所有项目的所有软件包

  • 一个作业令牌,用于访问定义 CI/CD 作业的项目中的软件包

部署令牌和作业令牌与用户无关

与个人访问令牌不同,部署令牌和作业令牌是特殊类型的 GitLab 凭证,它们与特定用户无关,而是可以作为一种无需指定用户凭证的替代身份验证方式。部署令牌和作业令牌通常用于以编程方式访问代码库,或者在我们的案例中,用于读取和写入包注册表。

在我们的示例中,我们将在“Hats for Cats”项目中创建一个项目级部署令牌。图 8.6显示了我们可以在项目设置 | 代码库 | 部署令牌下创建新的部署令牌的位置。在这里,我们可以为令牌指定一个易于识别的名称、一个可选的过期日期,以及一个可选的与令牌相关的用户名(如果不输入用户名,GitLab 将自动为我们生成一个)。

最后,我们选择要授予令牌的权限。权限名称可能略显模糊,但read_registrywrite_registry仅指容器注册表的读取和写入,而read_package_registrywrite_package_registry则指包注册表的读取和写入。

图 8.6 – 创建新的部署令牌

图 8.6 – 创建新的部署令牌

一旦我们创建了部署令牌,GitLab 将提供一个密码,我们需要将其保存在安全的地方(见图 8.7)。我们将使用部署令牌密码对命令进行身份验证,以访问包或容器注册表。

图 8.7 – 部署令牌密码

图 8.7 – 部署令牌密码

别忘了保存您的部署令牌!

一定要把部署令牌的密码保存在安全的地方!一旦离开页面,您将无法恢复密码,因为 GitLab 不会永久存储凭证。

完成创建部署令牌并保存密码后,GitLab 将在活跃的部署令牌下显示该令牌及其详细信息,如图 8.8所示。如果不再需要使用该令牌,项目维护者或所有者可以选择撤销该令牌。

图 8.8 – 活跃的部署令牌

图 8.8 – 活跃的部署令牌

现在,我们可以使用我们创建的令牌进行注册表身份验证。我们用于身份验证的确切语法将取决于我们的包管理器,即我们正在创建或使用的包类型。在最简单的情况下,我们可以将凭证包含在curl命令的头部,如下所示:

curl --user "hfc-generic-build:<deploy token password>" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/stable-releases/0.0.1/my_app.tar.gz

当在 CI/CD 作业中运行时,前述命令会对运行管道的项目的包注册表进行身份验证。身份验证凭证由--user标志提供,我们在其中提供部署密钥的用户名和密码(这里显示为占位符)。该命令的输出将下载my_app.tar.gz包。

使用部署密钥是一种从注册表拉取或下载的编程方式。下载包文件也可以通过 UI 完成。图 8.9 显示了一个 stable_releases 包的示例,版本为 0.0.1,已被添加到包注册表中。

图 8.9 – 包注册表中的条目

图 8.9 – 包注册表中的条目

如果我们点击 stable_releases,我们可以在 Assets 标题下看到可下载的应用文件(见 图 8.10)。我们可以看到文件名(通过扩展名可以识别文件类型)、大小和创建日期。选择文件名会将文件下载到本地计算机。在 History 部分,我们可以看到它何时发布到注册表、哪个管道构建推送了包、哪个提交启动了管道,以及包的名称和版本。

图 8.10 – 包文件和构建历史

图 8.10 – 包文件和构建历史

现在我们已经了解了如何进行身份验证并访问包注册表中的包,接下来我们将使用 CI/CD 作业来构建、推送和更新注册表中的包。

构建和推送包到包注册表

正如我们已经提到的几次,GitLab 推荐使用 CI/CD 作业来构建和发布到注册表。从技术上讲,写入注册表可以通过 API 调用进行,因此 GitLab CI/CD 并不是严格必要的。然而,构建和产物管理非常适合 GitLab 流程,并且将此过程标准化,与其他软件开发工作流类似,是很有帮助的。出于这个原因,我们将创建一个 CI/CD 管道来构建并推送到注册表。

精确的作业语法将取决于您用来构建软件的编程语言和包管理器。GitLab 文档提供了用于使用 Python、Maven 和其他工具进行身份验证、读取和写入注册表的特定语法示例。为了保持概念性,并且因为我们没有空间展示每种受支持的语言或工具,我们将使用通用包作为示例,并且我们的命令将是 curl 风格的 API 调用。

一个 CI/CD 配置可能如下所示:

stages:
  - build
  - publish
build_app:
  stage: build
  script:
    - make my_app
    - tar -czvf my_app.tar.gz .
  artifacts:
    paths:
      - my_app.tar.gz
publish_to_registry:
  stage: publish
  script:
    - $PACKAGE_FILE=$(ls | grep *.tar.gz)
    - curl --user "hfc-generic-build:<deploy token password>" --upload-file my_app.tar.gz "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/stable_releases/0.0.1/$PACKAGE_FILE"
  dependencies:
    - build_app

前面的示例 .gitlab-ci.yml 内容包含两个阶段:一个 build 阶段,它编译一个 C 风格的应用程序,另一个是 publish 阶段,它将构建产物推送到包注册表中。build 阶段包含一个作业来构建我们的代码。然后,我们创建一个包含完成的构建和支持文件的归档文件。该归档文件在构建作业中作为产物指定,以便下游的 publish 作业可以访问它。

publish_to_registry作业对注册表进行身份验证并上传构建产物。注意curl命令中的一些动态 CI/CD 变量(即以CI_开头的术语)。这些变量是引用项目中正确的注册表 URL 和路径的便捷方式。还要注意,我们将构建分类为通用包,属于“稳定发布”版本 0.0.1 的一部分。

我们还可以添加一个 Unix 时间戳,以区分注册表中的不同构建。我们可以按如下方式修改构建作业,以在产物文件名中包含时间戳:

build_app:
  stage: build
  script:
    - make my_app
    - TIMESTAMP=$(date +%s)
    - tar -czvf my_app_$TIMESTAMP.tar.gz .
  artifacts:
  paths:
    - ./*.tar.gz

图 8.11 显示了注册表中更新的文件资产。注意在构建脚本中的tar命令文件名中的时间戳。归档文件的大小和创建日期也会显示。开发人员可以点击文件名以下载该归档文件,然后可以在本地系统上解压。

图 8.11 – 带有时间戳的包注册表中的文件

图 8.11 – 带有时间戳的包注册表中的文件

构建并推送包到容器注册表

GitLab 还提供了用于存储 Docker 镜像的容器注册表。与包注册表类似,可以通过 CI/CD 作业创建并推送 Docker 镜像到注册表。身份验证的概念与包注册表相似,但你需要使用容器平台工具(如 Docker)来进行身份验证、构建、推送和拉取容器镜像。

以下 Docker 命令可用于使用我们之前的部署令牌对容器注册表进行身份验证。我们将使用类似的 Docker 命令来构建我们应用程序的容器化版本,并将其推送到注册表:

docker login -–username "hfc-generic-build" --password "eBYj_qvLTFSMcFrS-4tA" $CI_REGISTRY

我们的 CI/CD 作业用于“容器化”我们的应用程序并将其推送到注册表,可能如下所示:

publish_to_container_registry:
  stage: publish
  image: docker:stable
  services:
    - docker:dind
  variables:
    IMAGE: $CI_REGISTRY_IMAGE/my_app/0.0.1
  script:
    - docker login -u "hfc-generic-build" -p "<deploy token password>" $CI_REGISTRY
    - docker build -t $IMAGE .
    - docker push $IMAGE

让我们一步步解释前述 CI/CD 作业中发生的事情:

  1. 我们将作业命名为publish_to_container_registry,作为发布阶段的一部分。

  2. 然后,我们在 CI/CD 作业的运行时环境中包含两个容器:一个包含官方 Docker 工具的容器(image: docker:stable),以及一个带有“Docker-in-Docker”工具的服务容器(docker:dind)。后者容器类型允许我们在已经容器化的 CI/CD 作业环境中构建容器(因此被称为 Docker-in-Docker)。

  3. 接下来,我们定义一个名为IMAGE的变量,用于指定我们的容器镜像的名称,以及其在容器注册表中的目标端点。IMAGE变量将在docker命令中作为参数引用。

  4. 最后,我们的 CI/CD 作业脚本部分包含三个命令:

    • 第一个命令,docker login,使用我们的部署令牌对容器注册表进行身份验证。

    • 第二个命令,docker build,构建我们应用程序的容器化版本。

    • 最后的命令,docker push,将新构建的容器镜像推送到容器注册表。

在使用 Docker-in-Docker 构建容器时要小心。

虽然这很简单直接,但 Docker-in-Docker 默认使用--docker-privileged标志,这可能会使运行容器的服务获得对主机机器的 root 访问权限。如果这是一个安全问题,GitLab 还提供了使用名为kaniko的构建工具从 Dockerfile 构建容器镜像的说明。请参阅 GitLab 文档以获取更多详情。

结果发现,如果我们仅将前面的任务添加到我们的 CI/CD 管道配置中并运行管道,那么任务将失败。这是因为我们缺少构建容器镜像的另一个必要组件。我们的代码库中有一个名为Dockerfile的文件,它作为容器镜像构建的“食谱”,规定了容器镜像的构建方式及其内容。通常,Dockerfile 会包含一些指令,例如应该安装的依赖项、需要启动的服务或需要开放的端口。在这个例子中,我们将 Dockerfile 保持得非常简单。一个非常简单的 Dockerfile 可能包含以下内容:

FROM alpine:latest
# copy all of the files in this project into the Docker image
RUN mkdir public-app/
ADD . public-app/
WORKDIR public-app

上面的 Dockerfile 执行了以下操作:

  • 它使用一个名为 Alpine Linux 的最小化 Linux 发行版作为我们构建容器的基础操作系统

  • 它在容器中创建了一个名为public-app/的目录

  • 它将我们所有的代码库文件放在public-app/目录中

  • 它在容器启动时将public-app/设置为工作目录

图 8**.12显示了在更新.gitlab-ci.yml文件并创建 Dockerfile 后,我们的项目代码库可能的样子。

图 8.12 – 我们的代码库与 Dockerfile

图 8.12 – 我们的代码库与 Dockerfile

现在,当我们运行 CI/CD 管道时,可以在publish_to_container_registry任务日志中看到 Docker 命令,如下图所示(图 8**.13)。注意,docker build命令的输出显示它遵循我们在Dockerfile中指定的食谱。任务脚本最后确认容器镜像已推送到项目的容器注册表。

图 8.13 – 推送到容器注册表的任务日志

图 8.13 – 推送到容器注册表的任务日志

检查容器注册表时,实际上可以看到我们在 CI/CD 任务中指定的容器名称的引用(图 8**.14)。

图 8.14 – 容器注册表中的镜像库

图 8.14 – 容器注册表中的镜像库

如果我们点击容器名称,就可以看到构建本身(图 8**.15)。

图 8.15 – 项目容器注册表中的最新工件

图 8.15 – 项目容器注册表中的最新工件

到目前为止,我们讨论的内容涵盖了查找、验证以及写入 GitLab 的包和容器注册表。在本章的下一节中,我们将实际使用和部署我们创建并上传的资产。

将代码存储在 GitLab 的容器和包注册表中,以便以后部署

GitLab 的包和容器注册表不仅有助于将软件提供给用户下载,还用于存储包和库,以便在 CI/CD 管道中使用,或部署到环境中。在本节中,我们将讨论如何通过 CI/CD 作业以编程方式与注册表进行交互。

使用来自容器注册表的镜像

在上一节中,我们构建了一个容器化的应用程序版本,并将镜像推送到 GitLab 的容器注册表。回想一下,我们用来构建容器镜像的 CI/CD 作业本身是在容器中运行的,因此提到了诸如 docker:stabledocker:dnd 这样的术语。在这个例子中,我们是从公共容器注册表(即 Docker Hub)中拉取镜像。

然而,我们也可以拉取我们推送到 GitLab 容器注册表的容器镜像,并将其作为运行 CI/CD 作业的基础。我们可以像使用公共容器镜像一样,在管道中使用它们,使用 image 关键字,如下片段所示:

use_container_from_registry:
  stage: run
  image: registry.gitlab.com/nlotz1/pet-fashion/hats-for-cats/my_app/0.0.1:latest
  script:
    - ls –al      # Show the current directory as our container's working directory

图 8.16 显示了当我们从本地注册表拉取容器并运行作业时的作业输出。

.

图 8.16 – 在 CI/CD 作业中使用本地容器

图 8.16 – 在 CI/CD 作业中使用本地容器

请注意,作业日志显示了 GitLab 从容器注册表中拉取镜像的过程,以及容器文件系统的内容(即我们在第一次构建容器时包含的文件)。

使用包管理器注册表中的包

使用包管理器注册表中的包遵循与发布包到注册表类似的主题。从注册表拉取包的确切步骤和语法将取决于包的类型。通常,你需要指定包所在的组和项目(即 GitLab 中的命名空间)、包名以及包版本。与之前的示例一致,我们可能会使用 CI/CD 作业从我们的注册表中拉取并运行一个通用包:

use_package_from_registry:
  stage: run_package
  script:
    - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/stable_releases/0.0.1/my_app_1665515895.tar.gz'
    - tar -xvf my_app_1665515895.tar.gz
    - ./my_app

图 8.17 中的作业日志显示了通过 GitLab 的 API 认证并下载包文件的输出。在这个例子中,包被从其压缩文件中提取并运行。

图 8.17 – 在 CI/CD 作业中使用来自注册表的包

图 8.17 – 在 CI/CD 作业中使用来自注册表的包

GitLab 的文档展示了为不同包管理器认证并下载包的不同步骤,但原则保持一致。

现在我们已经理解了如何在 CI/CD 作业的背景下发布和下载包,接下来我们将把这些步骤融入到典型的 GitLab 开发工作流中。

使用 GitLab Flow 部署到不同的环境

到目前为止,我们已经讨论了如何通过 GitLab 的包和容器注册表发布和拉取我们的完成代码。现在,我们将学习 GitLab 的一些功能,用于组织将代码部署到特定环境。

GitLab 有两个术语,环境部署,用于描述和分类已部署应用的位置和版本。环境通过名称和 URL 表示,作为 GitLab 中的组织标签。每当应用通过 CI/CD 部署到该环境时,GitLab 会创建并将其分类为新的部署。

环境是通过testing创建的,且该环境将应用部署到名为production的通用应用中:

use_package_from_registry:
  stage: run_package
  script:
    - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/stable_releases/0.0.1/my_app_1665515895.tar.gz'
    - tar -xvf my_app_1665515895.tar.gz
    - ./my_app
  environment:
    name: testing
    url: https://test.example.com
use_container_from_registry:
  stage: run_container
  image: registry.gitlab.com/nlotz1/pet-fashion/hats-for-cats/my_app/0.0.1:latest
  script:
    - ls -la
  environment:
    name: production
    url: https://prod.example.com

图 8.18 显示了环境在 GitLab UI 中如何在部署 | 环境下进行表示。

图 8.18 – GitLab UI 中的环境

图 8.18 – GitLab UI 中的环境

每当一个新的提交创建一个 CI/CD 管道并部署到该环境时,环境就会被更新。GitLab UI 会显示最新部署的提交 SHA、触发该提交的 GitLab 用户、触发提交的分支以及部署到该环境的 CI/CD 作业。(https://prod.example.com)。

我们可以看到,通过将环境集成到 GitLab CI/CD 中,我们可以设置更高级的工作流,例如使用 CI/CD 关键字如规则,以确保只有在特定分支上代码更改时,某些环境才会被更新。本章的下一部分将展示这样的示例。

部署到审查应用进行测试

GitLab 有一个名为.gitlab-ci.yml的功能,它创建了一个作业,该作业部署到审查应用环境。

图 8.19 – 启用审查应用

图 8.19 – 启用审查应用

在作业内容中有两个关键点需要注意。第一个是使用预定义的 CI/CD 变量来指定环境名称和 URL。审查环境的名称将根据触发的分支动态更新,URL 也将根据动态环境名称进行更新。这对于在开发过程中快速启动临时的即席环境非常有用。

在审查应用 CI/CD 作业中需要注意的第二个点是其only/except关键字。审查应用环境在 Git 分支上触发管道时才会被部署,除非是主分支。其逻辑是,分支代表一个开发线,而主分支可能是一个更稳定的暂存或生产环境,是静态的而非动态的。

审查应用的另一个有用元素是它们可以通过合并请求轻松访问。如果合并请求的管道已定义触发分支的审查应用作业,则可以从合并请求页面访问审查应用的 URL 链接(见图 8.20)。

图 8.20 – 从合并请求查看审查应用

图 8.20 – 从合并请求查看评审应用程序

你也可以在 CI/CD 作业日志中看到指向评审环境的链接(见图 8.21)。

图 8.21 – 评审应用程序 CI/CD 作业日志

图 8.21 – 评审应用程序 CI/CD 作业日志

总结来说,评审应用程序只是扩展了我们对环境的理解,涵盖了动态创建和更新。它们最适合用于表示快速变化的开发环境,且开发人员和评审人员可以轻松访问,用于预览应用程序的最新版本。

现在我们已经了解了如何通过评审应用程序预览变更,最后让我们来讨论一下如何将应用程序部署到生产环境。

部署到实际的生产环境

到目前为止,我们展示的例子表明,环境仅仅是 GitLab 中的组织类别,具有相关的名称和网址。然而,在现实世界中,环境代表的是实际的基础设施,无论是你自己的计算机还是其他人的计算机。这意味着要认识到可用资源的限制,以及适当的安全性和访问控制的重要性。

在当今时代,使用 AWS、Microsoft Azure 和 Google Cloud Platform 等云服务提供商来托管应用程序变得越来越普遍。这些服务不仅外包了基础设施管理的需求,还提供了用于管理环境的程序化接口。

此外,随着云服务提供商的兴起,管理这些资源的开发工具套件也应运而生,除了云供应商提供的工具外,像 Terraform、Ansible 和 Chef 等软件可以声明式地管理云资源。也就是说,它们让你以文本形式将基础设施的描述存储在 Git 仓库中。因此,管理基础设施变更的工作流可以与应用程序开发管理的工作流类似。

利用云服务的内容远超本章所能覆盖的范围。不过,值得指出的是 GitLab 如何在应用环境管理的一个重要方面提供帮助:秘密管理。我们已经解释了 CI/CD 变量的概念,以及它们如何在 CI/CD 配置中使用并导出到作业运行的环境中。对于敏感性质的变量,例如部署密钥,GitLab 在组级或项目级设置中提供了一个特殊的地方,以确保这些值不会暴露在项目的代码库中。

比如说,你希望部署到 AWS 托管的环境中。你需要提供凭据来验证 AWS,通常是 AWS 访问密钥和秘密密钥的形式。然而,你不希望将这些值直接输入到 CI/CD 部署脚本中,因为这会将凭据泄露到代码库及其版本历史中。

你可以像处理其他变量一样,导航到项目的.gitlab-ci.yml文件,而无需将其值硬编码到配置中。

图 8.22 – 项目级 CI/CD 变量

图 8.22 – 项目级 CI/CD 变量

值得注意的是,以这种方式创建变量为其使用和暴露提供了额外的门控保护。除了设置变量的名称和值外,你还可以限制其可用的环境。你还可以选择将变量限制在受保护的分支和标签中(即,对贡献或修改它们的人有限制的分支和标签)。而图 8.23显示,你还可以将变量进行屏蔽,这意味着如果有尝试泄漏其值,作业日志中只会显示星号或[MASKED]

图 8.23 – 项目级 CI/CD 变量设置

图 8.23 – 项目级 CI/CD 变量设置

这些解释仅仅触及了实际环境中部署考虑的表面。然而,在如今这个高度关注安全的时代,我们发现最好专注于秘密管理这一关键领域,以展示 GitLab 如何使部署过程既顺畅又安全。在接下来的最后一部分,我们将学习 GitLab 与 Kubernetes 的集成,支持云原生部署。

部署到 Kubernetes 集群

我们之前讨论了云服务作为自托管基础设施的替代方案的普遍性。类似地,Kubernetes 等容器编排系统也越来越受欢迎,成为手动管理裸金属服务器或容器主机的替代方案。

使用 GitLab CI/CD 部署到 Kubernetes 在概念上与我们迄今为止讨论的工作流相似。你可以设置一个带有 Kubernetes 执行器的 GitLab runner,该执行器通过 Kubernetes API 与集群进行通信。或者,GitLab 还提供了一个名为GitOps 工作流的附加方法,它并不完全依赖于 CI/CD 流水线。我们将依次总结它们。

CI/CD 工作流

利用迄今为止的概念,你可以使用正常的 CI/CD 设置将容器化应用程序部署到 Kubernetes。这需要一个已注册 Kubernetes 执行器的 runner。在 runner 注册过程中,会提供集群主机和认证信息等内容。然后,CI/CD 脚本会包含集群中的直接 API 调用,指定构建、测试或部署代码的命令。

由于这个工作流涉及 runner 向 Kubernetes 集群发送命令,因此我们称之为基于推送的工作流。它的方便之处在于无需额外的依赖即可与 Kubernetes 交互。然而,如果集群或部署在 CI/CD 工作流之外发生任何变化,可能会遇到问题,导致集群处于与 CI/CD 作业预期不同的状态,从而产生潜在的问题。

因此,GitLab 越来越推荐一种与 Kubernetes 合作的不同方法,称为 GitOps 工作流。

一个 GitOps 工作流

GitLab 提供了一种替代的 Kubernetes 集成方法,不直接使用 CI/CD。相反,它使用安装在 Kubernetes 集群上的代理与 GitLab 实例进行通信。你可以通过导航到基础设施 | Kubernetes 集群,然后选择连接集群,按照提供的说明创建并注册新的代理(见图 8.24)。

图 8.24 – 将 Kubernetes 集群连接到 GitLab

图 8.24 – 将 Kubernetes 集群连接到 GitLab

一旦建立连接,代理会在配置和部署文件在代码库中更新时自动检测变化,并相应地更改集群状态。这是由于集群代理与 GitLab 服务器之间的双向流式传输。实际上,代理启动所有通信,以绕过任何妨碍 Kubernetes 集群的网络限制(见图 8.25)。

图 8.25 – GitLab 代理用于 Kubernetes 架构

图 8.25 – GitLab 代理用于 Kubernetes 架构

因此,GitLab 将 GitOps 工作流称为拉取式方法,这种方法正越来越受到 CI/CD 流水线传统推送方法的青睐。

总结

本章中,我们描述了打包和部署代码的各种方式,包括利用 GitLab 的包和容器注册表、CI/CD 环境以及与云原生基础设施交互的方法。

即使你可能在日常工作中不会使用所有讨论的功能或服务,我们希望本章为 GitLab 特性提供了一个有用的桥梁,帮助你理解更多实际、现实世界中的应用场景。

下一章将在我们已学到的基础上进一步发展。它还将介绍提高 CI/CD 速度和性能的高级功能,提高开发者生产力,并优化快速构建和交付软件的能力。

第三部分 使用 GitLab 改进应用程序的下一步

本书的这一部分将带你深入了解高级 CI/CD 流水线主题,包括使用有向无环图加速流水线、将第三方工具集成到流水线中、利用流水线发现代码中的性能问题以及其他许多话题。你还将有机会通过一个端到端的示例,回顾你迄今为止学到的所有内容,展示如何使用 GitLab 将一个真实的软件项目带入整个软件开发生命周期。最后,你将学习如何解决 GitLab 流水线中常见的问题,并提前了解 GitLab 未来的发展方向。

本部分包括以下章节:

  • 第九章提高 CI/CD 流水线的速度和可维护性

  • 第十章扩展 CI/CD 管道的应用范围

  • 第十一章端到端示例

  • 第十二章GitLab 的故障排除与未来发展

第九章:提升 CI/CD 管道的速度和可维护性

本章将介绍你可以利用的不同工具和方法,来提升 CI/CD 管道的速度和可维护性。本章的目标是介绍三种主要的方法来加速你的管道。我们不会涵盖 GitLab 中加速 CI 管道的所有方法,而是专注于对管道速度影响最大的几种方法。

本章将讨论以下内容:

  • 使用有向无环图(DAG)和父子架构加速管道

  • 为多种架构构建代码

  • 何时以及如何利用缓存或工件

  • 使用锚点和扩展减少重复的配置代码

  • 通过结合多个管道并利用父子管道提升可维护性

  • 使用专用容器保障和加速任务

使用有向无环图(DAG)和父子架构加速管道

GitLab 支持使用有向无环图DAG)模式构建 CI 管道。在 GitLab 的正常使用中,每个阶段代表一系列需要完成的任务。每个阶段由多个并行执行的任务组成。当一个阶段完成后,下一个阶段开始,直到整个管道完成。这是 GitLab 用于完成 CI 管道的典型处理循环。

然而,完全可以创建一个内部循环的 CI 任务,这些任务按特定顺序执行,但不会形成循环。这个模式叫做DAG,即有向无环图。有向指的是操作的顺序,无环指的是这个过程只会发生一次,则表示步骤的顺序。

如果正确使用,DAG 模式可以显著减少管道完成所需的时间。这是因为你在 CI 管道中创建了处理循环,并且明确地定义了 CI 任务的执行顺序,而这些任务位于阶段之外。一个好的例子是,如果一个任务的执行依赖于另一个任务的执行,你可以使用needs:关键字来确保它们一个接一个地执行,而不受阶段顺序的影响。这样就创建了一个简单的 DAG 过程。

如何在 GitLab CI 中创建 DAG

以下是一个正常 GitLab CI 管道的示例。以下是我们在介绍中描述的示例的 YAML 实现:

Job1:
  stage: A
Job2:
  stage: A
Job3:
  stage: B
Job4:
  stage: B

在传统管道中,如下所示,stage: B中定义的任务仅在所有stage: A中的任务完成后才会开始执行。以下是设置为使用 DAG 的相同管道示例:

Job1:
  stage: A
Job2:
  stage: A
Job3:
  stage: B
  needs: ["Job1"]
Job4:
  stage: B

通过将 needs 关键字添加到 Job3,我们在 Job1Job3 之间建立了依赖关系。所以,一旦 Job1 完成,Job3 将开始执行。这种依赖关系超出了常规的阶段顺序。如果未定义 needs,则会遵循阶段顺序。如我们在 Job4 中所见,要让 Job4 开始执行,stage: A 必须先完成。然而,如果定义了 needs: [] 且为空,则 GitLab 会在管道开始时立即执行该作业,并且不会分配给任何阶段执行。

为多个架构构建代码

GitLab CI 使你能够一次为多个架构构建工件。这可以轻松地加速软件构建,速度提升可达两到三倍,因为在多个架构下,通常需要为每个架构多次构建软件。在这里,我们将使用 CI 作业和管道同时执行多个架构的软件构建。

在为多个架构构建软件时,有一些特殊要求。第一个要求是,必须在每个架构运行的机器上安装并配置 GitLab Runners。对于这一部分,我们将以三个平台作为示例:x86_64arch64powerpc。在这种情况下,预期是你需要为这三种架构中的每一种准备一台机器,并在其上安装 GitLab Runner。该 GitLab Runner 还需要为它所运行的架构分配一个标签。

第二个要求是,你用来构建软件的工具链必须能够支持多架构构建。为了演示,我们将使用 GCC 和多架构的 Docker 镜像。我们还将使用 GitLab CI 的 parallel:matrix: 关键字。

parallel: 关键字旨在允许 CI 作业并行运行多次。例如,如果你在 CI 作业中设置 parallel: 5,该 CI 作业将在管道中并行执行 5 次。

matrix: 关键字旨在与 parallel: 关键字一起使用,在同一时间启动多个 CI 作业,并为每个作业分配不同的变量。以下是当在多架构构建的管道中使用时的示例:

my-multiarch-ci-job:
  stage: build
  image: multiarch/crossbuild
  script:
    - make helloworld
  parallel:
    matrix:
      - ARCH: x86
        CROSS_TRIPLE: " "
      - ARCH: arch64
        CROSS_TRIPLE: "arch64-linux-gnu"
      - ARCH: powerpc
        CROSS_TRIPLE: "powerpcle-linux-gnu"
  tags:
    - $ARCH

上述示例将执行三个关键操作:

  • 首先,通过使用 parallel:matrix:,GitLab 将启动三个独立的作业 —— 每个作业对应我定义的 ARCHCROSS_TRIPLE 的变量对。

  • 其次,每个作业将会分配一个标签,反映 ARCH: 中定义的值。这将导致该作业分配到与标签对应的适当 runner。

  • 第三,GitLab 将把 CROSS_TRIPLE: 环境变量暴露给 multiarch/crossbuild 容器。这个环境变量用于容器正确配置 GCC 工具链,使其为目标架构做好构建准备。

当容器启动并执行 CI 作业时,make helloworld 命令将被执行。该命令将调用预配置的 GCC 工具链,并使其开始构建我们的应用程序二进制文件。生成的二进制文件将被构建为支持指定架构的版本。

通过这种方式构建多架构二进制文件,我们简化了多架构构建的复杂性。我们使得这些多架构构建变得可重复且易于理解。我们还将这些构建并行运行,这样它们就不会相互等待,我们可以快速看到每个架构构建的结果。

重要提示

parallel:matrix: 关键字可以在任何需要多个具有相同配置但变量不同的 CI 作业的情况下使用。matrix: 关键字还可以接受数组作为环境变量的值。然而,环境变量的键不能是数组。

何时以及如何利用缓存或工件

在使用 GitLab 时,关于缓存和工件的使用经常会引起困惑。许多用户都很想知道在什么时候使用哪种功能。本章的目标是为您澄清这两者的区别,并提供实现这两种方法的工具,同时解释每种模式的优缺点。

缓存应该被视为一种保存 CI 作业或阶段中常用项目的方法。它不应被视为在阶段或作业之间传递项目的手段——这正是工件的作用。这个区别很重要,因为每种方式的实现和配置不同。缓存并不是为了作为一种在 CI 作业之间移动项目的方法而构建或设计的。因此,未来对缓存的任何功能或更改都将围绕该功能进行调整。

工件是支持将 CI 作业创建的项目永久存储,并将其作为依赖项传递给其他 CI 作业的主要方式。使用工件时,您可以了解存储了哪些内容,并且可以将具有硬依赖关系的 CI 作业关联起来,以确保工件得以共享。与缓存不同,缓存只有软链接。使用缓存的作业即使缓存不存在也能继续工作;然而,使用工件的作业则无法继续执行。

缓存和工件的下一个考虑因素是它们的存储方式以及涉及的网络调用。缓存包由 GitLab Runner 处理,GitLab Runner 的配置决定了缓存的存储位置。在默认配置参数下,缓存会存储在运行器所在的机器上。对于容器而言,缓存会被销毁。通过为 GitLab Runner 添加 S3 配置,Runner 会将所有缓存包推送到 S3 存储。在 CI 作业开始之前,任何使用该缓存的 CI 作业都会从 S3 存储中拉取缓存。

对于工件,Runner 在其处理过程中不发挥作用。每个工件包都会直接上传到 GitLab 实例。然后,GitLab 的配置决定该工件何时以及存储在何处。最常见的配置也是 S3 存储提供商。任何需要此工件的未来 CI 作业都会在 CI 作业开始之前下载它。像缓存包一样,工件包会被解压到工作目录中以供使用。

重要提示

在本章中,我们将讨论两种类型的依赖关系。第一种是 CI 作业依赖关系,应通过工件进行处理。CI 作业依赖关系本质上将一个 CI 作业的结果链接到另一个作业。第二种依赖关系类型是工具链依赖关系,即应用程序的依赖项——通常是第三方库。这些依赖项由多个 CI 作业或多个 CI 管道使用,因此应该利用缓存。

缓存特性

这些是区分缓存与工件的特点:

  • 你可以通过使用cache关键字为每个作业定义缓存。否则,缓存将被禁用。

  • 后续管道可以使用缓存。

  • 如果依赖项相同,同一管道中的后续作业可以使用缓存。

  • 不同的项目不能共享缓存。

  • 默认情况下,受保护的分支和非受保护的分支不会共享缓存。但是,你可以更改此行为。

工件特性

这些是区分工件与缓存的特点:

  • 你可以为每个作业定义工件。

  • 同一管道后续阶段的作业可以使用工件。

  • 不同的项目不能共享工件。

  • 工件默认在 30 天后过期。你可以定义自定义的过期时间。

  • 如果启用了保持最新工件,则最新工件不会过期。

  • 你可以使用依赖关系来控制哪些作业获取工件。

使用缓存

利用缓存的第一步是将配置添加到你的 GitLab CI 文件中。以下是如何执行此操作的代码示例。如果你查看此示例,你会看到我们定义了一个名为MyCIJob的 CI 作业。在此基础上,我们定义了一个cache块,并添加了一个path参数,列出了我们希望缓存的目录和文件。这是将项目缓存作为 CI 作业的一部分所需的最小有效代码:

MyCIJob:
  cache:
    paths:
      - theDirectoryToSave/*
      - myFileToSave.js

现在,每次此 CI 作业启动时,它都会使用相同的缓存包。这在你有每次 CI 作业运行时都会下载的依赖项时非常有用。通过将这些依赖项放入缓存中,它们就不需要每次都下载。相反,GitLab 会在作业开始时插入这些依赖项。这大大缩短了 CI 作业的时间,最终减少了 CI 管道完成的时间。

现在,我们将进一步深入。假设你有四个独立的 CI 作业,其中每两个 CI 作业需要共享一个缓存。在前面的示例中,我们创建了一个通用的缓存,可以用于所有作业。但是在这个示例中,我们希望在两个 CI 作业之间链接一个缓存。这可以通过添加 key 值来完成:

MyCIJob:
  cache:
    key: cache1
    paths:
      - theDirectoryToSave/*
      - myFileToSave.js

通过添加 key 值,GitLab 将拥有一个标识符,用来确定何时以及下载哪个缓存包。这个 key 值可以是一个字符串、一个变量或两者的任意组合。许多用户最终会使用 GitLab 预定义的变量作为 key,这样 GitLab 就可以更有效地管理缓存。例如,如果你使用 $CI_PIPELINE_ID 变量,那么这个缓存包只会在 ID 与 $CI_PIPELINE_ID 匹配的流水线中被使用。这意味着每个流水线都会有自己全新的缓存包。

你可以利用缓存的最终配置是改变缓存包的上传和下载的时机和方式:

MyCIJob:
  cache:
    key: cache1
    policy: pull
    paths:
      - theDirectoryToSave/*
      - myFileToSave.js

如你所见,缓存的 GitLab CI 对象有一个名为 policy 的值。默认情况下,这个值设置为 push-pull,这意味着缓存的包会在作业开始时被拉取,在作业结束时被推送。然而,你也可以将其配置为仅推送或仅拉取。设置为推送时,它会推送缓存对象,但从不拉取;而设置为拉取时,它会拉取缓存,但不会推送更新到包中。

使用构件

在前一部分中,我们讨论了缓存。缓存的许多相同概念将应用于构件,但构件具有更丰富的功能集。由于构件用于报告、作业之间传递构件和作业依赖管理等,因此它们需要更深层次的功能集。让我们从一个基本的构件对象开始,并从中构建一个更复杂的构件:

MyArtifactJob:
  artifacts:
    paths:
      - theDirectoryToSave/*
      - myFileToSave.js

从前面的示例中,你可以看到它的核心与缓存非常相似。我们添加了一个构件作业,分配了一个 artifacts: 对象给它,向其中添加了一个 paths: 对象,并列出了我们希望进行构件化的文件。从这里开始,当 MyArtifactJob 执行时,流水线运行结束时,它将把 paths: 中列出的所有内容发送到 GitLab 并进行存储。

类似于缓存的 policy 选项,它定义了如何处理缓存,构件有一个 when 对象,允许你指定当构件创建时会发生什么。其选项为 on_successon_failurealways。第一个选项 on_success 只有在作业成功执行时才会上传构件。第二个选项 on_failure 只有在作业失败时才会上传构件。最后,always 会在任何结果下始终上传构件。应用到我们的作业时,结果作业将如下所示:

MyArtifactJob:
  artifacts:
    when: 'on_success'
    paths:
      - theDirectoryToSave/*
      - myFileToSave.js

总结一下,前面的任务只有在其他 CI 任务成功时才会执行。然后,它将把对象存储在theDirectoryToSave/文件夹中的myFileToSave.js文件里。

利用产物作为任务依赖

现在我们已经介绍了产物的基础知识,让我们通过在实际场景中将任务与产物链接在一起,来进一步应用它们。通过在 CI 任务块中使用dependencies关键字,我们可以从另一个任务中提取产物,并在两个独立的 CI 任务之间建立内在的联系。

我们的应用程序将由MyBuildJob任务构建,该任务将在稍后定义。你可能会从之前的章节中认出很多语法。这个第一个任务是一个构建基于 Node.js 的应用程序的示例。首先,我们使用npm install命令来拉取构建此应用所需的任何依赖项。为了保持这个示例的简洁,我们没有使用依赖项缓存,但在生产环境中应该使用缓存。

其次,我们运行npm build命令来构建 Node.js 应用程序。所有 Node.js 项目的通用标准是,它们的构建文件都会生成在一个名为dist的文件夹中。Node.js 的依赖项通常存储在node_modules文件夹中。

最后,我们有一个产物定义,列出了node_modulesdist文件夹作为需要归档的内容。现在,当这个任务执行并完成时,GitLab 会存储这两个文件夹中所有的项目:

MyBuildJob:
  image: nodejs:latest
  script:
    - npm install
    - npm build
  artifacts:
    when: 'on_success'
    paths:
      - node_modules/*
      - dist/*

以下 CI 任务,定义为MyDependentJob,被构建用来从我们之前的任务中提取构建产物。然后,它将这些产物用于 Dockerfile 的构建。dependencies关键字是连接这两个任务的桥梁:

MyDependentJob:
  image: docker:latest
  dependencies:
    - MyBuildJob
  script:
    - docker build

正如我们所看到的,通过使用dependencies关键字,我们指示 GitLab 确保MyDependentJob拥有执行所需的一切。如果没有这个功能,MyDependentJob将需要自己拉取这些产物,这将需要额外的时间和配置。

使用锚点和扩展来减少重复的配置代码

所有 GitLab CI 管道文件必须是有效的 YAML。这也意味着它们支持 YAML 语言支持的各种模板和可重复的代码模式。为每个 CI 任务编写多个 CI 任务定义可能会耗时,并且会导致严重的可维护性问题。如果你在管道中多个地方使用相同的变量,最好只定义一次,然后多次引用。这样,当你需要更改变量时,只需在一个地方进行更改,而不是在多个地方进行更改。这不仅更容易操作,而且更安全,因为在大型管道中,多次替换变量可能会导致错误。

GitLab 和 YAML 提供的创建可重用 CI 管道的三种方法是extends:关键字和!reference标签。在接下来的章节中,我们将解释这三种方法的优点和使用方式。每种方法在功能上都有优缺点。

锚点

我们要回顾的第一种方法是锚点。YAML 锚点允许你将一个 CI 作业的属性复制或继承到另一个作业。使用锚点,你可以定义整个 GitLab CI 作业或几个属性,然后重复使用它们。

这里,我们将使用一个基本的 YAML 锚点。正如你所看到的,&job_definition是我们将要设置的锚点。然后,我们将在jobOnejobTwo中使用它,以引入来自&job_definition的内容:

.job_definition: &job_definition
  image: node:latest
  services:
    - postgres
jobOne:
  <<: *job_definition
  script:
    - npm build
jobTwo:
  <<: *job_definition
  script:
    - npm test

一旦 GitLab 处理了 CI 文件,结果将类似于下面的样子。在这里,&job_definition的内容已合并到我们的两个 CI 作业中:

jobOne:
  image: node:latest
  services:
    - postgres
  script:
    - npm build
jobTwo:
  image: node:latest
  services:
    - postgres
  script:
    - npm test

在前面的示例中,我们定义了一个以点(.)开头的作业。这个点意味着该 CI 作业不会作为 CI 作业执行,而只是作为一个引用存在。这个作业定义为.job_definition:。在这个定义之后,我们添加了一个 YAML 锚点。这里是&job_definition语句,您可以看到它。所有在&符号之后的内容都定义了锚点的名称。接着,我们定义了一个正常的作业应该是什么样子。

之后,我们定义了两个 CI 作业,每个作业都有不同的script:块。然而,我们使用<<:关键字告诉 YAML 处理器,我们希望将.job_definition:的属性和关键字与此作业合并。*字符后跟锚点名称,引用了我们用&符号定义的锚点。

结果是合并后的代码块中包含了这两个作业。

重要提示

YAML 锚点只能在定义它们的同一个 GitLab CI 文件中使用。这意味着,如果你正在利用includes:关键字将 CI 文件拆分成多个文件,你必须使用extends:!reference,而不是 YAML 锚点。

extends: 关键字

在 GitLab CI 文件中复用 CI 作业和配置的第二种方式是通过extends:关键字。extends:关键字和 YAML 锚点在操作方式上非常相似。一个主要的区别是,YAML 锚点通常用于在整个 CI 管道文件中复制单个值或属性,而extends:则更常用于复用整个配置块。在前面的锚点示例中,extends:更适合使用。

在下面的示例中,我们定义了一个.rules_definition块。然后,我们将其包含在.job_definition块中,并在jobOnejobTwo中使用.job_definition块。任何以点(.)开头的作业定义不会被 GitLab 作为实际的作业处理。相反,它们被当作模板来处理:

.rule_definition:
  rules:
    - if: $CI_PIPELINE_SOURCE =="push"
.job_definition:
  extends: .rule_definition
  image: node:latest
  services:
    - postgres
jobOne:
  extends: .job_definition
  script:
    - npm build
jobTwo:
  extends: .job_definition
  script:
    - npm test

在 GitLab 处理完前面的 CI 文件后,最终合并的结果如下所示。在这里,.rule_definition.job_definition的内容(这两个内容只定义了一次)现在包含在jobOnejobTwo中:

jobOne:
  image: node:latest
  services:
    - postgres
  script:
    - npm build
  rules:
    - if: $CI_PIPELINE_SOURCE =="push"
jobTwo:
  image: node:latest
  services:
    - postgres
  script:
    - npm test
  rules:
    - if: $CI_PIPELINE_SOURCE =="push"

正如你所看到的,这个示例与我们用锚点的示例略有不同。这是因为锚点和extends:之间的另一个主要区别是,extends:可以从多个 CI 作业定义中继承配置。

因此,你可以看到合并后的作业定义增加了一个rules:属性。这个属性是通过.job_definition CI 作业继承自.rules_definition CI 作业的。

引用标签

复用 CI 文件配置的第三种方法是使用!reference标签。!reference标签是自定义的 YAML 标签,用于从其他 CI 作业部分选择关键字配置,并在当前部分中重用它们。它们的使用方式与 YAML 锚点类似,但你可以在多个 CI 文件中使用引用标签。另一方面,YAML 锚点只能在定义它们的同一个文件中使用。让我们来看一个例子。

创建一个看起来像这样的Build.gitlab-ci.yml文件:

.build-node:
  stage: deploy
  before_script:
    - npm install
  script:
    - npm build

创建一个看起来像这样的.gitlab-ci.yml文件:

include:
  - local: Build.gitlab-ci.yml
Build-My-App:
  stage: build
  script:
    - !reference [.build-node, script]
    - echo "Application is Built"

在 GitLab 处理这两个文件后,结果应该是这样的:

Build-My-App:
  stage: build
  script:
    - npm build
    - echo "Application is Built"

在前面的示例中,我们在Build.gitlab-ci.yml中创建了一个作业定义。然后,我们将这个Build CI 文件包含到主.gitlab-ci.yml文件中。之后,我们使用!reference关键字直接从.build-node作业定义中提取脚本块。通过利用!reference而不是extends:,我们可以只从该作业定义中提取我们想要的配置,而不是整个作业定义。如果我们使用了extends:,我们还会将该 CI 作业定义的stage:before_script:属性一并引入。

通过合并多个流水线和利用父子流水线来提高可维护性

大多数 GitLab 用户只使用一个.gitlab-ci.yml文件来配置他们的流水线。这种方法完全可以接受,但在许多情况下,这个文件中的代码量可能会变得非常庞大且难以维护。GitLab 引入了将多个gitlab-ci文件作为一个文件包含在一起的功能。在本节中,我们将讨论如何将一个.gitlab-ci.yml文件拆分为多个部分。接下来,我们将讲解如何使用第二个.gitlab-ci.yml文件并执行一个子流水线,然后讨论为什么你可能会这样做的原因。

利用包含来提高可维护性

创建一个看起来像这样的.gitlab-ci.yml文件:

"Build Application":
  stage: build
  script:
    - code here
"Build Container":
  stage: build
  script:
    - code here
"Deploy Container":
  stage: deploy
  script:
    - code here
"Deploy Production":
  stage: deploy
  script:
    - code here

上述代码展示了一个传统的.gitlab-ci.yml文件的示例。在这个示例中,我们包含了四个作业——两个构建作业和两个部署作业。在普通的.gitlab-ci.yml文件中,这可能会包含数十个作业,并且它们之间有大量的逻辑,代码行数可能会达到数百行。这是完全可以接受的做法,但它难以维护和管理。与所有源代码形式一样,我们希望确保我们编写的代码在第一眼就能被理解,以保证其可维护性。

为了拆分这段代码并提高可维护性,我们可以利用 GitLab CI 语法中的 include: 关键字。该关键字用于告诉 GitLab 的 YAML 处理器何时将多个 YAML 文件合并为一个单一的上下文。让我们使用这个工具将我们的 CI 文件拆分成多个独立的 CI 文件,以便于重用。

创建一个 Build.gitlab-ci.yml 文件,其内容如下:

"Build Application":
  stage: build
  script:
    - code here
"Build Container":
  stage: build
  script:
    - code here

创建一个 Deploy.gitlab-ci.yml 文件,其内容如下:

"Deploy Container":
  stage: deploy
  script:
    - code here
"Deploy Production":
  stage: deploy
  script:
    - code here

创建一个 .gitlab-ci.yml 文件,其内容如下:

include: "Build.gitlab-ci.yml"
include: "Deploy.gitlab-ci.yml"

在这里,我们将各个作业根据阶段拆分到不同的文件中。然后,我们将这些文件包含在 .gitlab-ci.yml 文件中。当 GitLab 的 CI 处理器遍历 .gitlab-ci.yml 文件时,它会将每个 YAML 文件从上到下合并成一个单一的上下文。这意味着,如果我在 Build.gitlab-ci.ymlDeploy.gitlab-ci.yml 中都写了相同的作业,因为 Deploy.gitlab-ci.yml 最后被包含,它将覆盖 Build CI 文件中的内容。这是一个简单的示例和方法,用于将 CI 文件拆分以提高可维护性。接下来,我们将结合之前的示例与这种方法进行讲解。

利用包含以提高重用性

之前,你已经学习了如何使用包含来帮助维护性。在本节中,我们将结合使用 include: 和本章中关于锚点和扩展的知识,向你展示如何创建可重用的流水线。我们将从一个示例开始。

创建一个 Templates.gitlab-ci.yml 文件,其内容如下:

.npm-build:
  stage: build
  variables:
    NPM_CLI_OPTS: ""
  before_script:
    - npm install
  script:
    - npm rebuild $NPM_CLI_OPTS

在前面的 GitLab CI 文件中,我们创建了一个非常简单的 CI 作业定义。由于该 CI 作业的名称以 . 开头,因此这个作业不会单独运行。我们包含了一个 variables: 块,并插入了一个空的 NPM_CLI_OPTS 变量作为占位符。然后,我们在 script: 块中使用了这个变量,当我们执行 rebuild 命令时。设置这个默认变量的原因是,我们希望在使用该作业时,能够有一个合理的默认值。

我们将这个 CI 文件命名为 Templates.gitlab-ci.yml,以表明它包含的是 CI 作业模板,而不是实际的 CI 作业定义。然而,我们希望在 CI 流水线中利用它。以下示例展示了如何实现这一点。

创建一个 .gitlab-ci.yml 文件,其内容如下:

include: Templates.gitlab-ci.yml
My-NPM-Job:
  extends: .npm-build
  variables:
    NPM_CLI_OPTS: '--global'

在这里,我们包含了来自 Templates.gitlab-ci.yml 文件的所有作业定义,并基于这些定义启动了我们自己的作业。然而,在 variables: 块中,我们添加了自己的变量来控制模板 CI 作业定义的执行方式。

这种将 CI 作业模板化,然后使用变量来暴露配置选项的方法,类似于传统软件开发中的组件化方式。尽管语法不同,但它遵循相同的目的、规则和目标。

来自远程区域的包含

include: 关键字并不局限于单个项目或仓库。你还可以从开放的互联网中包含 GitLab CI 文件。以下是一些远程包含的示例:

  • 包括来自不同项目和分支:

    include:
    
      - project: "my-group/my-project"
    
        ref: my-branch
    
        file: 'Templates.gitlab-ci.yml
    
  • 包括来自远程位置:

    include:
    
      - remote: 'https://www.google.com/Templates.gitlab-ci.yml'
    
  • 从 GitLab 实例的模板中包含(lib/gitlab/ci/templates):

    include:
    
      - template: 'Templates.gitlab-ci.yml'
    

重要提示

你可以从 CI 流水线启动者有访问权限的任何地方包含 CI 文件。前提是启动 CI 流水线的人具有该文件的读取权限,流水线将会成功。如果启动者没有访问权限,流水线将会失败。

利用父子流水线

现在我们已经讨论了如何包含多个 YAML 文件,并且如何将它们模板化以便重用,接下来让我们谈谈如何使用trigger:关键字来创建子流水线。

子流水线是由另一个流水线触发的流水线。触发流水线的流水线称为父流水线。一旦父流水线触发子流水线,父流水线的执行将等待子流水线完成后才会继续。这是构建多个流水线来支持单体仓库,或将一个大型复杂流水线拆分为更小、更易管理流水线的强大工具。

要触发子流水线,只需将trigger:关键字添加到include:语句的父级。以下示例将会执行build.gitlab-ci.yml文件作为子流水线:

My-Child-CI-Job:
    stage: build
    trigger:
      include:
        - project: "my-group/my-project"
          ref: my-branch
          file: 'Build.gitlab-ci.yml'

在前面的示例中,在 CI 流水线视图中,你会看到一个名为My-Child-CI-Job的 CI 作业。该 CI 作业将会附带另一个名为Downstream Pipeline的流水线。在这个视图中,你将能够看到来自子流水线的所有作业及其执行状态。

重要提示

被触发的子流水线接受所有正常的 CI 作业属性和关键字。像rules:这样的关键字可以决定何时触发子流水线。environment:关键字还可以将子流水线的触发与审批规则或环境跟踪关联起来。

使用专用容器保障和加速作业

GitLab 在正确配置的情况下,会在容器中运行流水线的所有 CI 作业。这意味着整个构建操作都发生在容器中。因此,容器的管理非常重要。如果 CI 作业发生在不安全的容器中,那么整个 CI 作业和流水线都将不安全。如果 CI 作业使用性能不佳的容器,那么该作业和流水线将会花费更多时间才能完成,导致显示结果的时间大大延长。在所有可衡量的方面,CI 作业使用的容器是流水线中最重要的部分。

重要提示

要快速设置或识别某个 CI 作业使用的容器,查找 CI 作业中的image:属性。该属性将定义容器镜像的来源,以及正在使用的具体容器镜像。

查找此容器镜像的第二个位置是在 CI 作业日志的顶部。那里会有一条消息,指示当前使用的容器镜像。

我们的目标是通过一种称为专用容器的实践来解决这些问题。这些容器的整个设计都是用于在 CI 流水线中使用。在这里,我们将概述这些容器的一些特性,并解释如何构建它们。在为 CI 流水线构建容器时,请尽可能遵循这些指导原则。即使您无法实现此处提到的每一项,尽可能遵循这些指导原则将使您的 CI 流水线容器更安全、更高效和更易于维护。

考虑的第一个项目是容器的文件大小。用于 GitLab CI 的容器应仅包含运行和执行其任务所需的最少组件。此容器还应只执行一组任务。这意味着您会为每个工具链单独创建一个容器 - 也就是说,一个用于 Java,一个用于 Node.js。您不希望混合多个工具链,因为这会增加容器的文件大小。很少有情况需要将多个工具链放在同一个容器中执行,因为它们通常在单独的 CI 作业中执行。一个好的遵循规则是,如果 CI 作业不需要某些东西,那么容器中就不应该包含该东西。

考虑的第二个项目是确保您的容器是多用途的。您仍然希望将工具链保持在单独的容器中;但是,您应避免将配置嵌入到只能用于多个 CI 作业或多个流水线的容器中。一个好的例子是包括加密证书,以便容器可以与其需要的任何资源通信。不应包括的示例是仅与单个 CI 作业或流水线相关的任何配置或设置。这两个示例之间的区别在于,第一个示例(证书)用于使容器能够正常工作,而第二个示例(配置或设置)将限制容器可以运行的 CI 作业和流水线。

考虑的第三个项目是防止容器在正常使用时运行。专为 GitLab CI 构建的容器应有效地成为僵尸或外壳。容器执行除了 CI 作业指示其执行的任何内容之外的情况都不应该存在。这可以通过确保容器的入口点为空来实现。如果 Docker 容器在 GitLab CI 中启动时执行任何操作,可能会导致冲突,并且启动时间也会更长。

第四个需要考虑的事项是避免在 Docker 容器中添加不必要的层。每当 Dockerfile 中使用RUNADD命令时,Docker 都会创建一个新层。不必要的层可能会显著增加 Docker 容器的大小,从而违反第一个考虑事项。当你在 Dockerfile 中运行命令时,应充分使用&操作符将多个命令链接在一起。我们在 Dockerfile 示例中提供了这种用法的例子。

最后一个需要考虑的事项是权限。在 GitLab CI 中运行的 Docker 容器通常不需要提升的权限来运行和执行操作。在 OpenShift 和一些 Kubernetes 平台上,任何具有提升权限的 Docker 容器可能都不允许运行和执行。创建容器时设置随机用户 ID 有助于防止这些平台赋予容器任何形式的特权。如果需要为文件或文件夹提供权限,应将权限授予组,而不是单个用户。

一个定制容器示例

在这里,我们提供了一个定制容器的示例。该示例遵循了我们之前列出的所有考虑事项。这是一个基于alpine:3.12.0基础容器构建的 Docker 镜像。在此基础上,我们有一个RUN命令,该命令将多个命令合并为一行。它利用&操作符将多个 APK 包管理器命令连接在一起。这减少了 Docker 文件中的层数。在该命令行的末尾,我们将一个文件夹分配给运行用户的组。通过这样做,我们保留了操作该文件夹的能力,但防止它通过错误的组分配获得任何特权:

FROM alpine:3.12.0
RUN apk update && apk add –no-cache nodejs npm && mkdir ~/.npm && chmod -R g=u ~/.npm
USER 1001
CMD ["echo", "This is a purpose built container. It is meant to be used in a pipeline and not executed."]

对于第一个考虑事项,你可以看到它通过apk add –no-cache nodejs npm来满足。Node.js 是该容器中唯一安装的工具链。

对于第二个考虑事项,你可以看到 Dockerfile 中没有嵌入任何配置。这意味着容器将从 CI 作业的配置中获取所有配置。

对于第三个考虑事项,你可以查看以CMD ["echo"开头的那一行。该行防止容器在 CI 作业之外运行。它显示错误信息并终止容器。

对于第四个考虑事项,请查看以RUN apk开头的那一行。该行将多个命令通过&连接在一起。每个容器中的RUN命令都会创建一个新的层。这里,我们尽量使用尽可能少的RUN命令。

对于第五个考虑事项,我们使用USER 1001关闭容器。这一行强制容器中的所有命令以随机用户 ID 运行。这意味着没有任何命令会以任何形式的提升权限运行。

总结

在本章中,我们学习了许多可以与 GitLab CI 一起使用的工具,用于创建快速且可重用的流水线。我们从 DAG(有向无环图)开始,它可以使流水线执行得更快。接着,我们学习了如何为多种架构构建代码。随着主流 ARM 平台的出现,这一点将随着时间的推移变得越来越重要。随后,我们学习了在流水线中何时利用缓存或工件进行依赖管理。

我们讨论的最后两个主题可能是最重要的。我们学习了三种不同的方法来构建可重用的流水线定义,这样你就不必在多个地方重复编写相同的逻辑。最后,我们了解了一个名为专用流水线的概念,它使你能够构建快速、安全且稳定的容器来执行你的 CI 工作负载。

在下一章中,你将学习如何扩展你的 CI/CD 流水线的覆盖范围。

第十章:扩展 CI/CD 流水线的应用范围

本章旨在扩展 CI/CD 流水线在常见自动化用例中的应用。到本章结束时,你应该对 CI/CD 流水线的可能性有所了解。你会发现,它们不仅仅用于构建和部署任务,还能用于自动化任务,从而让工程师的工作更加轻松、可重复和可靠。CI/CD 流水线的目标始终是减轻工程师的工作负担,使他们能够专注于更具创新性和重要性的任务。

本章将涵盖以下主题:

  • 使用 CI/CD 流水线发现性能问题

  • 将第三方工具集成到 CI/CD 流水线中

  • 使用 CI/CD 流水线开发移动应用

使用 CI/CD 流水线发现性能问题

没有比在自动化的 CI/CD 流水线中进行性能测试更合适的时机了。对于性能测试,测试应该是例行的,并且针对稳定、不变的环境或部署。如果你对一个不断变化的环境或部署进行性能测试,你将无法获得可靠的结果。没有可靠的结果,性能测试的整个意义就丧失了。你希望了解你的更改如何影响性能;没有稳定的结果,你就无法得出这种理解。

使用 GitLab,有多种方式可以运行性能测试。对于基于 Web 或 API 的部署,GitLab 包含一个原生的性能测试工具,本章将介绍该工具并讲解其集成方式。不过,作为最终用户,你可以更进一步,涵盖更多的指标,如 CPU/内存/存储使用情况,并将其融入 CI/CD 流水线中。我们本章不会讲解如何收集这些指标,因为这对每个用户的环境都是独特的。然而,我们强烈建议你将这些指标作为 CI/CD 流水线的一部分进行捕捉。

GitLab 针对 API 和基于 Web 的部署提供的原生集成性能测试工具,将检查活跃部署的众多指标。这些指标包括页面加载时间、首屏渲染时间和总阻塞时间。在合并请求中(见下图),你可以看到代码更改如何影响 Web/API 部署的性能:

图 10.1:合并请求中的性能指标视图

图 10.1:合并请求中的性能指标视图

始终仔细检查你的测试标准

你的测试结果将直接依赖于你的测试标准、测试设置和环境。你应该始终质疑你的测试标准、设置和环境是否准确,绝不能假设它们是准确的。如果它们不准确,你的测试结果也永远不会准确。

如何集成浏览器性能测试

集成浏览器性能测试非常简单——只需将 GitLab 模板作为 CI/CD 管道的一部分,并指示 GitLab 如何访问您的 API 或 Web 部署。请参见以下示例。

include:
  - template: Verify/Browser-Performance.gitlab-ci.yml
browser_performance:
  variables:
    URL: https://myWebOrApiURL.local/

在前面的作业模板中,我们调用了 GitLab 中的 browser_performance: 模板。然后,我们重写了作业的 URL 变量。这个 URL 告诉性能测试套件去哪里访问并扫描我们的应用程序的性能。通过这个配置,GitLab 将检查我们的应用程序,并将结果报告到我们的合并请求中。

如何将负载性能测试与 k6 集成

GitLab CI/CD 默认使用的第二种测试类型是负载测试,使用的是由 Grafana 公司提供的名为 k6 的工具。GitLab 还提供了一个模板,可以将该工具作为 CI/CD 管道的一部分运行。然而,创建并指定一个 k6 配置文件来执行适当的 k6 负载测试是一个额外的步骤。

我们将在这里讲解这个过程。在此之前,请注意绝对不要在生产环境中运行这些负载测试。生产环境的负载测试应在一个与生产环境完全相同的非生产环境中运行。为了获得准确的负载测试结果,负载测试工具应该是与您的环境通信的唯一项目。事不宜迟,让我们开始集成负载测试。将以下代码添加到您的 .gitlab-ci.yml 文件中:

include:
  - template: Verify/Load-Performance-Testing.gitlab-ci.yml
load_performance:
  variables:
    K6_TEST_FILE: '<PATH TO FILE>' #.gitlab/tests/k6.js

上面的代码将作为我们管道的一部分执行 k6。不过,现在我们需要告知 k6 如何测试我们的应用程序,并为 GitLab 提供一个测试文件来运行。我们假设这个文件位于 .gitlab/test/k6.js 路径。接下来我们将一步步讲解如何构建这个文件。

以下代码将加载影响我们负载测试的库和函数。如果没有这些,负载测试文件的其余部分将会失败。接下来,我们将添加我们的设置:

import { check, group, sleep } from 'k6';
import http from 'k6/http';

以下代码将定义我们测试的选项。在 5 分钟内,我们将逐步增加到 100 个用户访问网站。然后,我们将保持 100 个用户访问 10 分钟。最后,在 5 分钟内,我们将逐步减少到 0 个用户。这意味着此测试将总共运行 20 分钟:

export const options = {
  stages: [
    { duration: '5m', target: 100 },
    { duration: '10m', target: 100 },
    { duration: '5m', target: 0 },
  ],
  thresholds: {
    'http_req_duration': ['p(99)<1500']
  },
};

然后,我们必须设置失败的阈值。我们希望看到 99% 的所有请求在 1500 毫秒内响应。任何超出此范围的请求都将失败。接下来,我们将执行实际的负载测试。

以下代码将使用 k6 执行我们的网站性能测试。现在这个文件已经成功构建,我们可以执行我们的管道并进行可重复的负载测试:

export default() => {
  const myResponse = http.get('<MY URL or ENV VAR>').json();
  check(myResponse, { 'retrieved url: (obj) => obj.length > 0 });
  sleep(1);
};

在设置好性能测试后,我们将把注意力转向如何利用功能标志在部署后启用和禁用应用程序的部分功能。

使用功能标志来支持以业务为驱动的发布决策

GitLab 提供了在 UI 中设置功能标志的功能。它基于第三方 unleash 库。一旦在 GitLab UI 中设置了功能标志,您需要配置应用程序与 GitLab 通信以检查功能标志。我们将介绍这两个步骤的示例,但 GitLab 不会为您完成这项工作:这项工作需要由您的应用程序开发人员来做。

一旦在 UI 中配置了功能标志,就需要通过应用程序发出 API 调用来检查标志,并相应地更改应用程序的逻辑。让我们来看看如何在 UI 中设置功能标志:

  1. 在项目中,打开左侧的导航窗格。点击部署 | 功能标志。这将带您进入主要的功能标志部分。在右上角,点击新建功能标志。这将带您进入一个新的视图:

图 10.2:GitLab 导航窗格中的功能标志

图 10.2:GitLab 导航窗格中的功能标志

  1. 您将看到一个表单。填写表单并根据需要规划策略。您可以根据环境、用户列表和用户 ID 设置多个策略,并为不同的组分配百分比。提交后,界面将如下所示:

图 10.3:主要功能标志视图

图 10.3:主要功能标志视图

  1. 功能标志在推出新功能时极其强大。通过将新功能放在功能标志后面,您可以通过切换标志来关闭应用程序的一部分。这有助于防止回滚发布或紧急支持工单:

图 10.4:功能标志详细视图

图 10.4:功能标志详细视图

图 10.4中,您可以看到我们新创建的功能标志。在左侧的状态下,您可以看到一个复选框,用于启用或禁用该标志。

如何为功能标志配置您的应用程序

本节将介绍一个检查应用程序中的功能标志的用例。以下是一个 Ruby 代码示例,它将检查功能标志并根据设置的标志切换代码路径。您的团队应用程序的开发人员需要根据他们的工具链和语言使用情况设置应用程序:

require 'unleash'
require 'unleash/context'
unleash = Unleash::Client.new({
  url: 'http://gitlab.com/api/v4/feature_flags/unleash/42',
  app_name: 'production',
  instance_id: '29QmjsW6KngPR5JNPMWx'
})
unleash_context = Unleash::Context.new
unleash_context.user_id = "123"
if unleash.is_enabled?("my_feature_name", unleash_context)
  puts "Feature enabled"
else
  puts "Feature disabled!"
end

在这个 Ruby 示例中,首先设置 GitLab 信息,包括 GitLab 实例的 URL、应用程序的名称(与 GitLab 中的环境对应)以及实例 ID(GitLab 项目的数字 ID)。

user_id 参数是向 GitLab 传递信息的示例。在这个例子中,我们传递了一个用户 ID,GitLab 会将其与配置好的功能标志策略进行匹配。下一步是调用 unclear.is_enabled,它接受功能名称作为变量,然后联系 GitLab 确定您是否属于已启用标志的组。接着,将执行启用的代码路径。

现在我们已经介绍了功能标志的使用和目的,我们将继续讲解如何将第三方工具集成到 CI/CD 管道中。

将第三方工具集成到 CI/CD 管道中

在本节中,我们将介绍如何将第三方工具集成到 CI/CD 管道中。将第三方工具集成到 CI/CD 管道中的首选方法是将其容器化,创建一个使用该容器的 CI/CD 作业,然后在该作业中调用我们的工具。在许多情况下,这是必需的,并且是设置集成的第一步。

工具格式要求

本节假设你想集成的工具已经编译好并准备好集成到你的管道中。如果它尚未准备好,你可以在此处提到的作业之前添加 CI 作业,以编译或组装该工具。然后,你可以调用本节中的 CI 作业和步骤。

创建我们的工具容器的 Dockerfile

在上一章中,我们讨论了如何构建专用容器。我们将在这里使用这种方法来集成你的工具。如果你还没有阅读上一章中的使用专用容器保护和加速作业部分,强烈建议你先阅读它,然后再回来。

这个过程的第一步是为你的工具和容器设置一个新的 GitLab 项目。一旦设置好后,将二进制文件、配置文件和你希望放入容器中的其他文件提交到该项目。下一步是构建容器并将这些内容放入其中。在你的仓库中创建一个 Dockerfile,并添加以下示例代码。$mybinary是你的二进制文件名的占位符;myTool将是你的工具的名称。

如以下代码所示,我们已经创建了一个从alpine:3.13.0派生的新容器。我们更新了容器,然后为我们的工具创建了一个目录。在创建该目录后,我们将仓库中的所有文件添加到该文件夹,并赋予它们宽泛的权限,同时将我们的二进制文件设置为可执行:

FROM alpine:3.13.0
RUN apk update && mkdir /opt/myTool
ADD . /opt/myTool
RUN chmod 777 -R /opt/myTool && chmod +x /opt/myTool/$mybinary
USER 1001
CMD ["echo", "This is a purpose-built container. It is meant to be used in a pipeline and not executed."]

这是一个简化的集成第三方工具的示例。你应该始终将权限值自定义为运行工具所需的最小权限。你还应该只将工具运行所需的文件放入容器中。在这个示例中,我们采取了宽泛的方式,包含了所有文件,并赋予了宽泛的权限。

自动化容器构建

现在我们有了一个 Dockerfile,我们需要构建容器。作为额外的操作,我们还将启用容器扫描。如果你还没有创建.gitlab-ci.yml文件,请在你的仓库中创建一个。接下来,我们将用以下代码填充该.gitlab-ci.yml文件:

Container_Build:
  stage: build
  image: docker:20.10:16
  services:
    - docker:20.10:16-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_VERIFY: 0
  script:
    - docker login -u $CI_REGISTRY_USER –p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:latest

避免每次提交时触发管道运行

在进行多次管道更改时,你可能不希望每次修改时都启动一个管道运行。如果你在提交信息开头添加[CI SKIP],GitLab 将不会为该提交启动管道。

上述代码是 Docker-in-Docker 构建的最基本示例。首先,我们利用 image: docker:20.10.16 来定义我们希望构建的 Docker 版本。然后,我们定义要使用的 Docker 服务——即 dind 容器。接着,我们设置 DOCKER_HOSTDOCKER_TLS_VERIFY 变量,以便 Docker 和 dind 容器能够互相通信。最后,我们调用 docker build 命令来构建我们的容器,使用 docker push 命令将其上传到 GitLab 的容器注册表中。

GitLab Runner 对 Docker 的要求

构建一个 Docker 容器通常需要使用能够执行 Docker-in-Docker 容器构建的 GitLab Runner。GitLab.com 提供的共享 Runner 已预先配置了此功能。如果你使用的是自托管 Runner,可能需要重新配置它。请参考 GitLab 文档了解如何实现这一点。

容器扫描

GitLab 在容器构建过程中提供容器扫描功能。我们希望利用这一点来识别容器中的任何依赖关系或其他漏洞。启用此功能非常简单:你只需要在 .gitlab-ci.yml 文件中定义一个测试阶段。然后,只需将以下代码块添加到 .gitlab-ci.yml 文件的顶部:

include:
  - template: Jobs/Container-Scanning.gitlab-ci.yml

调用第三方工具

到这个步骤,我们应该已经有了一个构建好的第三方工具容器,经过扫描并存放在 GitLab 容器注册表中。现在剩下的就是调用我们的工具。我们可以通过创建一个指向我们容器并在容器内调用可执行文件的 CI/CD 任务来实现:

Test_Job:
  stage: test
  image: path/to/my/container
  script:
    - /opt/myTool/$myBinary

到目前为止,我们已经讲解了如何构建包含工具的容器,如何扫描容器以及如何通过 CI/CD 任务调用该工具。这是一个基本示例,向你展示了可能实现的艺术。通过这个过程,你可以克隆第三方工具、自定义脚本、自定义配置或任何其他你想在 CI/CD 管道中包含的内容。

这样做有很多重大好处。例如,任何作为 CI/CD 管道一部分运行的容器都可以访问 GitLab API 和 GitLab 仓库。这意味着你可以将 CI/CD 任务作为管道的一部分运行,用于度量收集或配置验证等。以这种方式构建的 CI/CD 任务几乎可以自动化任何操作。

如果你构建并容器化了一个自定义工具,务必与社区共享它。

在下一部分,我们将讨论如何使用 GitLab CI/CD 和 Fastlane 构建移动应用。

使用 CI/CD 管道开发移动应用

在本节中,我们将讨论如何在 GitLab 中为移动应用程序开发设置 CI/CD 管道。自动化移动开发过程的打包阶段有很多好处,最显著的是,打包移动应用程序涉及多个证书、授权和配置文件,在打包应用程序时需要花费大量时间来组装这些文件。除此之外,移动应用程序的测试过程可能是手动且繁琐的。通过自动化跨多个设备的截图等操作,我们可以大大减少开发人员的工作量。

本节假设您已经配置并使下列所有要求正常工作。我们不会讨论如何进行移动开发,而是如何使用 Fastlane 和 GitLab 自动化您的移动开发实践。

要求

对于本节,您将需要以下内容:

  • 一台运行最新版本 OS X 的 macOS 设备或虚拟机,并已安装 Fastlane

  • 在您的 macOS 设备上安装 GitLab Runner

  • 一个 Apple 开发者账户

  • 一个 Google 开发者账户

  • 一款可以在您的 macOS 设备上成功构建的应用程序

在继续之前请阅读此内容

如果您无法满足这些要求,您应停止并等待能够满足这些要求后再继续。本指南假设您已经拥有一台可用且配置好的 macOS 设备,并且能够在设备上构建移动应用程序。

Fastlane

在本章中,我们将使用 Fastlane CLI 工具来自动化我们的测试、构建和部署。Fastlane 是一款同时支持 Android 和 iOS 构建流程的工具。它是一个开源工具,可以在 macOS 和 Windows 平台上免费安装。您可以在 docs.fastlane.tools/getting-started/ios/setup/ 阅读更多关于它的文档。

我们假设您已经在机器上安装了 Fastlane。如果没有,请参考之前链接的 Fastlane 文档,选择适合您的平台进行安装。此过程的第一步是打开您的移动应用程序项目,并创建一个名为 Fastfile 的 Fastlane 配置文件。Fastfile 是 Fastlane 使用的主要配置文件。我们将使我们的 Fastfile 看起来像这样:

lane :beta do
  build_app(Scheme: "MyApp") # For iOS
  gradle( task: 'assemble', build_type: 'Release') # For Android
end

要为 iOS 和 Android 构建我们的移动应用程序,我们只需运行 fastlane beta 命令。假设您的 iOS 和 Android 应用程序已经可以在 macOS 设备上构建,那么此命令应自动化它们的构建。我们可以将其放入 GitLab CI 文件中,如下所示:

"Build Mobile Applications":
  stage: build
  tags:
    - my-osx-runner
  script:
    - fastlane beta

此 GitLab CI 作业将通过 GitLab 管道自动化 Fastlane 过程。我们利用 tags: 关键字确保此构建发生在特定的 macOS 设备上。如果没有这个关键字,构建可能会发生在任何 GitLab Runner 上。

Fastlane – 部署

你可以将移动应用程序部署到 Android 和 Apple 商店。然而,在设置 Fastlane 之前,你需要先为生产环境配置所有移动证书。当这些证书过期时,必须有人在 macOS 设备上更新它们。不过,如果这些证书已经配置好,你可以像下面这样修改你的 Fastfile

lane :appstore do
  sync_code_signing(type: "appstore")
  build_app(scheme: "MyApp")
  upload_to_app_store
end
lane :playstore do
  gradle(task: 'assemble', build_type: 'Release')
  upload_to_play_store
end

之前对 Fastfile 的修改将构建你的应用程序并发布到相应的应用商店。你只需运行 Fastlane appstoreFastlane playstore 命令。相应的 GitLab CI/CD 配置文件如下所示:

"Deliver Mobile Applications":
  stage: release
  tags:
    - my-osx-runner
  script:
    - fastlane appstore
    - fastlane playstore

Fastlane – 自动化测试

假设你已经为项目设置了单元测试,Fastlane 也可以自动化这些测试。它不会自动设置测试,但如果你已经将它们作为项目配置的一部分,Fastlane 就可以调用它们。这个过程与之前的每一步类似 – 我们从修改 Fastfile 开始,如下所示:

lane :iosTest do
  run_tests( devices: ["iPhone 6s", "iPad Air"], scheme: "MyAppTests")
end
lane :androidTests do
  gradle(task: "test")
end

就像之前一样,我们可以修改我们的 GitLab CI/CD 配置文件,加入自动化测试:

"Test Mobile Applications":
  stage: test
  tags:
    - my-osx-runner
  script:
    - fastlane iOSTests
    - fastlane androidTests

有时,使用 Fastlane 可能会让你感觉像是在作弊。一旦你在设备上建立了一个移动项目,Fastlane 就能接管剩下的工作。在 Fastlane 出现之前,移动开发是一个繁琐且手动的过程。CI/CD 管道充满了大量的代码和逻辑,用来生成构建并进行部署。

在本节中,我们讨论了如何构建 Fastfile 来配置 Fastlane,如何运行 Fastlane 命令,以及最重要的,如何将它们添加到 GitLab CI/CD 配置文件中。我们之前提到过这一点,但最后再次强调:Fastlane 和使用 GitLab CI/CD 管道自动化移动开发最适合已经配置并设置好以构建移动应用程序的 macOS 设备。从那里开始,然后使用 GitLab CI 和 Fastlane 处理其余部分。

总结

在本章中,我们讨论了在 CI/CD 管道中加入性能检查的好处。我们还讨论了如何包含 GitLab 的本地性能测试工具。接下来,我们介绍了功能标志的好处,以及它们如何保护你的部署并防止耗时的回滚。然后,我们介绍了如何将第三方工具集成到你的 CI/CD 管道中,并如何将它们容器化以供使用。最后,我们讲解了如何使用 Fastlane 自动化创建和部署移动应用程序。

在下一章,我们将介绍一个端到端的示例,涵盖本书中所学到的所有内容。

第十一章:端到端示例

到目前为止,你已经了解了 GitLab 的各个部分如何帮助你编写、审查、验证、保护、打包和部署软件。这些功能已经一一介绍过了,现在让我们把它们全部结合起来,展示一个完整的端到端示例,让你能一次性看到整个过程。在本章中不会有任何新的内容,所以这是一个复习之前章节内容的好机会,看看它们如何在一个扩展的工作流程中整合在一起。

本章分为多个子章节,每个章节展示如何使用 GitLab 来帮助软件开发生命周期中的不同部分:

  • 设置你的开发环境

  • 编写代码

  • 建立流水线基础设施

  • 验证你的代码

  • 保护你的代码

  • 改进你的流水线

  • 将你的代码交付到正确的环境

技术要求

如果你想跟随我们一起开发一款使用 GitLab 的软件,你只需要拥有一个 GitLab 实例的账户,无论是软件即服务(SaaS)还是自托管实例。

设置你的开发环境

在你开始编写任何代码之前,需要整理一些初步事项。具体来说,你需要创建一个 GitLab 项目来存储你的代码并运行你的流水线,你需要创建一些 GitLab 问题来帮助规划和跟踪你的工作,此外你还需要将项目的代码仓库克隆到本地计算机,这样你就可以使用自己喜欢的 IDE 编写代码,而不是完全依赖 GitLab 的图形界面。

创建一个 GitLab 项目

让我们回到创建 Hats for Cats 网页应用的初衷。我们知道我们想为我们的在线猫咪配饰商店开发一个网页应用,所以让我们首先创建一个 GitLab 项目,既容纳网页应用的代码,也容纳我们在开发过程中将使用的其他 GitLab 组件。

关于图形用户界面的说明

在本章中,我们将继续展示代码片段,但通常不会展示使用 GitLab 图形界面的步骤。这样做是为了避免随着 GitLab 图形界面在未来版本中发生变化,截图内容会失去实用性。

我们本可以直接在 GitLab 账户的顶级命名空间中创建一个 Hats for Cats 项目,但考虑到我们未来可能会创建更多项目——也许是其他网页应用,也许是针对不同平台的同一网页应用的不同版本——因此,首先我们应该创建一个 GitLab 组来容纳所有项目。假设我们的公司名为 Acme Software,我们将从一个名为 Acme Software 的组开始:

图 11.1 – Acme Software 组

图 11.1 – Acme Software 组

在这个组内,我们可以添加一个新的项目来托管 Hats for Cats 网页应用。我们将其命名为 Hats for Cats,并选择其 Git 仓库中的 README.md 文件,但不选择其他文件。创建后的新项目如下所示:

图 11.2 – Hats for Cats 项目

图 11.2 – Hats for Cats 项目

正如你已经知道的,这个项目不仅包含我们的网页应用程序的代码,还包括我们的 CI/CD 管道配置文件、与代码相关的分支和合并请求MRs)、准备部署的代码包,以及帮助我们规划和跟踪工作的各类问题。这自然引出了我们工作流的下一步:创建 GitLab 问题。

将所有项目放置在一个小组中

出于技术原因,本章其余部分的截图显示的是Hats for Cats项目直接在用户账户下,而不是在Acme Software小组中。然而,最佳实践是将公司或组织的所有项目都创建在一个主小组下。

使用 GitLab 问题规划工作

使用 GitLab 问题来规划和跟踪工作是编写 GitLab 软件的一个可选但强烈推荐的步骤。当然,你可以使用其他工具,如 Jira 或 Trello 来规划工作,但许多开发人员发现,GitLab 问题可以为标准项目管理任务提供所需的所有功能。

GitLab 项目对于一些复杂的软件可能包含数十个、数百个,甚至在极端情况下,包含成千上万的问题——为了教学目的,让我们只为我们的项目创建四个问题。为了构建我们的网页应用程序的最小可行产品MVP),我们将创建以下标题的问题:

  • 允许用户 登录

  • 允许用户 搜索库存

  • 允许用户购买 一顶帽子

  • 允许用户 登出

一旦这些准备好后,我们就可以考虑 GitLab 标签了。一些团队会在每个问题上使用多个标签,以指示归属、分配优先级或执行其他管理功能。让我们创建并分配一个无范围的安全标签,表示登录和登出的问题需要我们的安全团队额外关注。我们还将创建带有范围的priority::highpriority::low标签,指示哪些功能应优先处理,哪些可以稍后再做。目前,只需将priority::high分配给登录问题——我们稍后会作为团队决定其他问题的优先级标签。

接下来,让我们处理登录问题的元数据,这是我们希望首先开发的功能。我们打开该问题,将其分配给开发人员,将Weight字段设置为5(经过讨论,我们认为这是一个中等大小任务的合理权重),使用快速操作估算完成它需要 15 小时的工作时间,并设置一个截止日期。在创建问题的同时设置元数据是许多使用看板工作流的团队的常见做法;如果我们使用的是 Scrum 工作流,可能会等到下一次积压梳理会议时再分配截止日期、权重或估算工时。

在创建了这四个问题、为它们创建并分配了适当的标签,并填写了其中一个问题的元数据后,点击左侧导航栏中的问题 | 列表,应该会显示类似这样的内容。请注意,元数据直接显示在问题列表中,这非常方便:

图 11.3 – Hats for Cats 问题和问题元数据

图 11.3 – Hats for Cats 问题和问题元数据

你已经完成了 GitLab 环境的设置:你创建了一个 GitLab 组,在其中创建了一个 GitLab 项目,并创建了一些需要解决的问题。现在,你已经准备好开始编写代码,并将其存储在该项目中。

设置本地 Git 版本库

尽管我们可以在 GitLab 实例内完成所有的开发工作,但那样会使得编写代码变得困难,测试也无法进行。相反,让我们将版本库克隆到本地计算机上,这样我们就可以使用我们最熟悉的桌面 IDE 或其他工具来进行开发。

首先,我们需要确保已经在工作站上生成了公钥/私钥对,并将公钥上传到 GitLab 帐户。这项工作对于任何我们正在使用的 GitLab 实例只需做一次,因此我们假设这部分已经完成。GitLab 文档中有关于此过程的更多信息。

接下来,我们需要获取用于克隆Hats for Cats项目版本库的地址。由于我们使用基于密钥的安全性,而不是在每个 Git 命令中手动输入认证凭据,因此我们通过图形界面复制项目的 SSH 地址,然后通过在本地机器上运行以下命令来克隆它(如果你在家里跟着操作,你会看到一个略有不同的地址,基于你的 GitLab 帐户名):

git clone git@gitlab.com:acme-software/hats-for-cats.git

然后,我们可以进入克隆操作创建的目录并查看。如果一切正常,我们应该会看到与 GitLab 托管版本库中相同的文件的本地副本——此时的副本只有一个README.md文件:

~$ cd hats-for-cats
~/hats-for-cats$ ls
README.md

现在,我们已经准备好进行本地开发,将任何修改推送到 GitLab 上版本库的副本,并拉取同事们推送上来的修改。

编写代码

你已经接近可以开始编码的阶段。首先,你需要一个 Git 分支来提交代码。然后,你需要一个 MR,这样你就可以看到针对这段代码运行的管道任务的结果,并且最终将代码合并到main分支。让我们先了解这些步骤,然后进行第一次提交并推送。

创建一个用于开发的 Git 分支

现在我们已经准备好添加第一个分配问题所要求的登录功能,我们需要一个 Git 分支来提交代码。我们不妨将分支命名为我们正在处理的那个问题的标题:

git branch add-login-feature

切换到新的分支,这样任何新的提交都将进入该分支,而不是main分支:

git checkout add-login-feature

在这一点上,我们不妨将这个分支推送到 GitLab 托管的代码库副本中,这样它就能同时存在于两个地方。对于第一次推送,我们需要使用一个稍微长一些的命令:

git push --set-upstream origin add-login-feature

对于后续的推送,在我们添加了一些提交之后,我们可以依赖一个更简单的命令:

git push

如果我们查看 GitLab 中项目的代码库,可以看到它现在在项目的分支下拉菜单中列出了add-login-feature

创建 MR

让我们遵循 GitLab 最佳实践,立即为我们刚创建的分支创建一个 MR。导航到左侧导航窗格中的Ns,点击New merge request。选择add-login-feature作为源分支,main作为目标分支,然后点击Compare branches and continue

给 MR 起个标题 Draft: add login feature,并在描述字段中添加Closes #X,其中X替换为描述中的#对应的数字,图形界面会为你展示一个下拉菜单,显示你创建的所有问题,这样你就不需要手动查找问题编号了)。现在,我们已经创建了问题、分支和 MR 这“三个好朋友”,可以开始开发我们的功能了。

提交和推送代码

该是写代码的时候了!我们不打算写一个包含真实 GUI 元素和逻辑的实际登录页面,而是用我们本地的计算机在print("Welcome to Hats for Cats!")的根目录下创建一个名为login.py的文件。因为我们是在本地计算机上操作,而不是在 GitLab 上,所以我们需要使用 Git 终端命令或者 Git 的图形化工具(如SourcetreeTower,或内置于 IDE(如IntelliJ IDEAVisual Studio Code)中的 Git 工具)将新文件添加到 Git 暂存区,然后提交一个适当的提交信息。可以通过命令行进行这些操作:

git add login.py
git commit -m "add initial version of log in page"

在这一点上必须重申,本地提交不会触发 CI/CD 流水线。GitLab 实例并不知道这个提交的存在,即使知道,它也无法查看该提交中包含的代码。然而,一旦我们将任何本地提交推送到 GitLab,它会在自己副本中的代码库中检测到这些提交,并针对我们推送的任何提交运行流水线。

现在让我们推送add-login-feature分支:

git push

经常推送非常重要!

我们本可以等到对这个分支做了多个本地提交后再推送,但由于 GitLab CI/CD 流水线的强大功能正是来自于它们频繁地对小的代码变化进行运行,通常在每次本地提交后推送是最明智的选择。

这只是一个如此简单的编辑——添加一个包含一行代码的文件——你可能会想,为什么我们不直接在 GitLab GUI 中进行编辑并提交更改,而要通过本地编辑、提交再推送到 GitLab 的麻烦。说实话,对于这样一个小更改,我们完全可以直接在 GitLab 上进行编辑。如果这是一个真实项目,那正是我们会建议你这么做,但由于大多数开发工作远比这里包含的占位符代码更为复杂,我们认为展示一下典型的本地编辑并提交然后推送工作流会对你有所帮助,这是你在大多数开发工作中会用到的流程。

建立管道基础设施

你已经将一些代码存储在仓库中,所以现在是时候设置一个管道,执行一系列任务来构建、验证和保护你的代码。在某些情况下,你可能还想设置一个 GitLab Runner 来执行这些管道任务,尽管这个任务通常由你的 GitLab 管理员或 GitLab SaaS 平台为你处理。

创建管道

向 GitLab 推送提交,以便它可以在我们的新代码上运行 CI/CD 管道,只有在我们定义了管道要执行的任务后,这一操作才会生效。我们将在本章接下来的部分中向Hats for Cats项目的管道中添加多个任务,但现在,让我们先设置一个基础的管道。

就像我们在最初的文件中所做的那样,我们可以在本地创建.gitlab-ci.yml管道配置文件,提交并推送到 GitLab,但由于 GitLab GUI 提供了一个专门用于编写和调试管道配置文件的方便编辑器,通常更有意义在 GitLab 上进行编辑。因为我们无法在本地机器上运行管道,所以.gitlab-ci.yml的本地副本并不重要。

在左侧导航面板中,选择.gitlab-ci.yml。此时,我们并不确定在管道中需要哪些阶段。可以合理地认为,在你真正需要阶段之前没必要定义它们,但我们相当确信我们将需要buildtestdeploy这三项基本任务,因此为了完整展示这个例子,我们现在就添加这三项:

stages:
    - build
    - test
    - deploy

由于 Python 是一种解释型语言,而非编译型语言,我们在build阶段还没有任何需要定义的任务。我们也还没有编写任何自动化测试,因此没有必要添加执行测试的任务。我们希望运行一些安全扫描并进行其他验证任务,但这些任务将在稍后定义。我们还需要将代码部署到多个环境中,但这也将稍后处理。现在,让我们只添加一个虚拟任务,这样 GitLab 就不会因为管道中缺少任务定义而抱怨(如果我们不这样做,GitLab 实际上会认为管道配置文件格式不正确,并且位于专用编辑器顶部的代码检查器会给我们警告)。将这个内容粘贴到stages:部分下方:

# temporary job that we'll delete later
job1:
    stage: build
    script:
        - echo "in job1"

在提交此更改后,我们导航到.gitlab-ci.yml—并且它在共享运行器上成功执行了job1,没有任何问题。当然,如果你使用的是没有提供共享运行器的 GitLab 实例,你需要为该项目创建自己的运行器,才能使管道运行。幸运的是,这正是我们工作流中的下一个步骤。

创建一个运行器

如你所知,GitLab 的 SaaS 版本用户可以在 GitLab Runners 上运行他们的管道,这些 Runners 是软件订阅的一部分,但如果你使用的是自托管版本的 GitLab,或者如果你想在自己的硬件上运行一些管道,以避免耗尽订阅中的 GitLab Runner 分钟数,你需要设置一个或多个你自己的 GitLab Runners。让我们为我们的Hats for Cats项目创建一些专用的运行器。

我们决定在一台闲置的 Linux 机器上创建运行器。考虑到 GitLab Runner 二进制文件在主要 Linux 发行版的仓库中通常是几个版本之前的版本,我们查阅了 GitLab 文档,了解如何将官方 GitLab 仓库添加到我们的 Linux 机器的包管理系统中,下载最新的 GitLab Runner 二进制文件,将其作为服务安装,并确保它在运行。由于这个过程因操作系统和 Linux 发行版而异,我们在这里不会提供详细的操作步骤。

GitLab Runner 版本

尽管 GitLab Runners 通常在与 GitLab 实例的版本相差几个小版本时仍然能正常工作(例如 15.0 对 15.3),但如果你保持这两个版本同步,它们将最可靠地运行。

一旦gitlab-runner二进制文件安装在将托管运行器的计算机上,我们需要通过注册它们来创建运行器。在此之前,我们需要收集一些信息。我们已经决定运行器将专门用于abc123

我们决定注册两个运行器。我们可以随意给它们命名,但我们最终选择了最明显的命名方案:Hats for Cats 1Hats for Cats 2

接下来,我们选择在两个运行器上都使用 Docker 执行器,因为这样可以为它们提供最大的灵活性:它们可以处理任何 CI/CD 流水线作业,因为它们可以在已经安装了所有必需工具的 Docker 镜像中执行作业。我们决定指定 alpine:latest 作为运行器将用于未指定镜像的作业的默认 Docker 镜像,因为这是最小的功能完备的 Linux 发行版,因此下载速度最快。最后,我们决定不向运行器添加任何标签,因为我们不打算使任何一个运行器成为特定用途。

当然,除非托管运行器的机器上安装并运行 Docker,否则我们无法使用 Docker 执行器注册运行器。Docker 的安装说明偶尔会有所变化,并且根据操作系统而异,因此官方 Docker 文档是您在此步骤中的最佳信息来源。

一旦 Docker 在与运行器相同的主机上启动并运行,我们可以通过 gitlab-runner register 以交互方式注册单个运行器,但在这种情况下,让我们通过将所有细节作为选项传递给单个终端命令来非交互式地注册运行器。我们使用这条命令在将托管运行器的 Linux 服务器上注册第一个运行器(根据需要更改 --url--registration-token 值):

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.hats-for-cats.com/" \
  --registration-token "abc123" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "Hats for Cats 1"

我需要使用 sudo 吗?

检查 GitLab 文档,了解在您的操作系统上 gitlab-runner 二进制文件是否需要 sudo 或管理员权限;命令在不同平台上的行为有所不同。

我们可以运行相同的命令,更改 --description 选项的值,以创建第二个运行器。

现在两个运行器都已注册,请确保它们都已启动并运行:

~$ sudo gitlab-runner verify
Runtime platform         arch=amd64 os=linux pid=6365
revision=bbcb5aba version=15.3.0
Running in system-mode.
Verifying runner... is alive             runner=LuKAFv53
Verifying runner... is alive             runner=Rtq7yC5e

最后,请刷新我们通过 设置 | CI/CD | Runners 进入的 GitLab 页面,并确保两个运行器能够连接到我们的 GitLab 实例并声明自己已准备好接受来自 Hats for Cats 项目的作业。这是该屏幕相关部分的内容:

图 11.4 – Hats for Cats 项目的特定运行器

图 11.4 – Hats for Cats 项目的特定运行器

在继续之前,您可能希望查阅 GitLab 文档,以了解注册运行器的主配置文件中 concurrentcheck_interval 选项的更多信息。有时,调整这两个选项的值可以帮助运行器更快地接收作业。这个配置文件在将 gitlab-runner 二进制文件作为 root 用户运行的 Linux 系统上是 /etc/gitlab-runner/config.toml,但当 gitlab-runner 不作为 root 运行或在其他操作系统上运行时,可能存在其他位置。运行 gitlab-runner list(根据您注册运行器的方式,可能需要或不需要 sudo)应该会显示该文件在您系统上的位置。

可选:禁用共享运行器

如果你正在自己的 GitLab 账户中操作,可能需要前往 Settings | CI/CD | Runners 并禁用所有 Hats for Cats 项目的共享 Runner。这样可以确保该项目的所有 CI/CD 流水线任务会被分配到我们刚刚注册的两个 Runner 之一。

在配置好基础的 CI/CD 流水线配置文件并注册了两个 Runner 后,我们已完成项目基本流水线基础设施的设置。现在,我们需要开始向流水线中添加任务,以便它可以运行所有的测试和扫描,从而让 GitLab 流水线成为一个强大的软件开发工具。

验证你的代码

让我们配置你的流水线,使其能够通过运行功能测试、代码质量扫描和模糊测试来验证你的代码。

向流水线添加功能测试

许多团队通过向流水线添加任务来运行自动化功能测试,确保他们的代码按预期的方式运行。在之前的章节中,你已经学到有许多不同种类的功能测试。在这个例子中,我们将添加一些使用 pytest 框架编写的基础自动化单元测试。我们的项目代码尚不复杂到需要真正的单元测试,但为了本示例,我们可以添加一些虚拟测试,以便 GitLab 可以运行它们并显示结果。

在添加任何测试之前,让我们先使登录代码变得 稍微 复杂一点,通过添加一个函数供测试调用,并在其中加入一个“待办”注释,不仅提醒我们稍后完善这个占位符函数,还给代码质量扫描器提供了检测的目标。无论是在本地(在这种情况下,你需要进行提交并推送)还是在 GitLab 图形界面中,都将以下简单代码添加到 login.py 文件中:

def log_user_in(username, password):
    return (username == "Dana") and (password == "p@ssw0rd")
# TODO: replace this placeholder code with real logic

我们还需要声明对 pytest 框架的依赖,这样 GitLab 才能在运行自动化测试之前安装它。由于这是一个 Python 项目,我们将在项目仓库的顶层(仍然在add-login-feature分支上)声明这个依赖,并创建一个新的 requirements.txt 文件,文件内容为一行:

pytest==7.1.3

有多种方法可以在文件和目录中对自动化单元测试进行分组,但为了简单起见,我们在仓库的根目录下添加一个名为test_login.py的文件,放在我们一直在使用的add-login-feature分支中。

我们将在这个文件中添加三个测试:一个用于检查使用正确凭据的登录功能,一个用于测试使用错误用户名登录,另一个用于测试使用错误密码登录。我们还需要导入被测试的函数,以便单元测试能够调用它。将以下代码添加到 test_login.py

from login import log_user_in
def test_login_good_credentials():
    assert log_user_in("Dana", "p@ssw0rd")
def test_login_bad_username():
    assert not log_user_in("foo", "p@ssw0rd")
def test_login_bad_password():
    assert not log_user_in("Dana", "foo")

扩展你的自动化测试

这些单元测试示例比你在真实项目中通常使用的要简单。大多数单元测试框架——无论测试的是哪种语言——都提供了多种选项和附加功能,帮助你使测试更全面、更强大。我们建议你彻底了解你选择的测试框架,因为自动化测试是编写高质量代码的重要组成部分。

让我们在流水线的test阶段添加一个任务,安装pytest库(如requirements.txt中列出),并运行测试。该任务应要求执行器在一个包含最新版本 Python 的 Docker 容器中执行命令。pytest测试框架会自动识别包含测试的文件,因此我们不需要指定执行哪个测试文件。我们需要告诉pytest生成一个junit格式的输出文件,这是 GitLab 能够解析和显示的测试结果格式。将此任务定义添加到你当前在add-login-feature分支上的.gitlab-ci.yml文件中:

unit-tests:
    stage: test
    image: python:3.10
    script:
        - pip install -r requirements.txt
        - pytest --junit-xml=unit_test_results.xml

既然我们已经在 CI/CD 流水线中定义了一个真实的任务,如果你想简化流水线配置文件,可以删除临时job1任务的定义。

当由此提交触发的流水线完成后,你会注意到,即使unit-tests任务成功运行,流水线详情页面仍然没有显示任何测试结果。这是因为我们没有告诉 GitLab 将结果保留为构件。让我们通过将以下代码添加到unit-tests任务定义的末尾来修复这个问题,确保所有行都正确缩进,以便 GitLab 将此代码理解为现有任务定义的一部分:

    artifacts:
        reports:
            junit: unit_test_results.xml
        when: always

这样,我们应该能够使单元测试正常运行并通过,这是确保代码符合设计规范的一大步。如果你一直在跟进,应该可以在最新的流水线详情页面的测试选项卡中看到类似的输出,显示所有三个单元测试都在运行并通过:

图 11.5 – 流水线详情页面上的测试结果

图 11.5 – 流水线详情页面上的测试结果

一旦自动化测试通过,你就可以确信你的代码做到了它应该做的事情。然而,仅仅正常运行还不够:你的代码还需要编写得很好。这有助于确保代码具有可读性和可维护性,并且在你将来为其扩展新功能时,不太可能出现 bug。因此,接下来,让我们来看看如何确保你的代码质量高。

将代码质量扫描添加到流水线

让我们在流水线中添加一个名为代码质量扫描的任务,这将帮助我们评估代码的质量。与 GitLab 的所有扫描器一样,代码质量扫描仅适用于某些计算机语言。不过——如 GitLab 文档中对该功能的描述——它支持所有常见的编程语言,包括 Python。

我们通过在项目的 CI/CD 配置文件末尾包含其模板来启用扫描器,使用的分支是add-login-feature,这是我们完成所有工作的分支:

include:
    - template: Code-Quality.gitlab-ci.yml

如果你检查由提交此更改到.gitlab-ci.yml触发的流水线,你会注意到代码质量扫描任务失败了。别慌!这个失败源于代码质量扫描在 GitLab 的扫描工具中有些特殊。它使用了一种名为runner for code quality的技术,通过在托管其他 runner 的同一台机器上输入此命令来实现。就像我们注册那些 runner 时一样,你需要将--url--registration-token的值替换为你系统中适当的值:

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.hats-for-cats.com/" \
  --registration-token "abc123" \
  --executor "docker" \
  --docker-image "docker:stable" \
  --description "runner for code quality" \
  --tag-list "code-quality-capable " \
  --builds-dir "/tmp/builds" \
  --docker-volumes "/cache"\
  --docker-volumes "/tmp/builds:/tmp/builds" \
  --docker-volumes "/var/run/docker.sock:/var/run/docker.sock"

我们用一个code-quality-capable标签配置了这个新 runner,表示它能够处理代码质量任务。为了确保我们的任务分配给这个特定的 runner,我们需要覆盖代码质量扫描器的任务定义,并给它分配相同的标签。在调整流水线配置文件时,让我们同时禁用一个由某些 runner 用于处理这个任务的服务,但我们的 runner 并不需要它。将这个任务定义覆盖添加到你的.gitlab-ci.yml文件末尾:

code_quality:
    tags:
        - code-quality-capable
    services: []    # disable all services

一旦我们提交最后一次更改,GitLab 会启动流水线并在流水线详细页面的一个新代码质量标签页中展示代码质量结果:

图 11.6 – 流水线详细页面上的代码质量结果

图 11.6 – 流水线详细页面上的代码质量结果

代码质量扫描器提醒我们处理早些时候在login.py中添加的“待办事项”注释。这个建议不错,但现在让我们忽略它,继续进行模糊测试。

向流水线添加模糊测试

让我们设置一个模糊测试,看看log_user_in函数是否有任何未被自动化单元测试捕获的 bug。第六章详细描述了模糊测试中涉及的架构元素,如果你需要回顾每个代码片段的角色,可以参考这一章。

提醒

模糊测试,和本书中讨论的其他一些功能一样,仅在 GitLab Ultimate 许可下可用。每个功能的 GitLab 文档都会告知你该功能需要哪个许可等级。本书没有提及哪些功能需要哪些许可,是因为 GitLab 在较高级别的许可测试过一段时间后,通常会将一些功能开放给较低级别的许可。

这次模糊测试将测试login.py文件中的log_user_in函数。如果你还记得第六章,这段代码被称为待测代码。按照现在的写法,这个函数足够简单,我们通过查看代码就能知道模糊测试不会发现任何问题。换句话说,对于如此简单的函数来说,模糊测试是“杀鸡用牛刀”,但我们可以想象这个函数未来可能会变得更加复杂,随着复杂度的增加,出现 bugs 的可能性也会增大。因此,现在就为这段简单的待测代码创建一个模糊测试是一个好主意,这样如果我们以后用更复杂、更容易出错的代码重写这个函数,它就可以发现新的 bugs。

我们将log_in_user_fuzz_target.py文件放在仓库的add-login-feature分支的根目录下。该文件应包含以下 Python 代码:

from login import log_user_in
from pythonfuzz.main import PythonFuzz
@PythonFuzz
def fuzz(bytes):
    try:
        string = str(bytes, 'UTF-8')
        divider = int(len(string) / 2)
        username = string[:divider]
        password = string[divider:]
        log_user_in(username, password)
    except UnicodeDecodeError:
        pass
if __name__ == '__main__':
    fuzz()

这些代码大部分是从 GitLab 文档中的示例模糊目标复制过来的。需要我们发挥编程创意的部分是fuzz()方法中的逻辑。这段代码将模糊引擎发送的随机字节转换成随机字符串,使用字符串的前半部分作为用户名,后半部分作为密码,并将用户名和密码传递给待测代码。

接下来,我们需要将 GitLab 模糊测试模板包含到add-login-feature分支中的.gitlab-ci.yml文件里。在我们已经添加的代码质量模板后,在现有的include:部分内添加Coverage-Fuzzing.gitlab-ci.yml模板。完整的include:部分应该如下所示:

include:
    - template: Code-Quality.gitlab-ci.yml
    - template: Coverage-Fuzzing.gitlab-ci.yml

因为这个模板声明了模糊测试任务将在它们自己的fuzz阶段运行,我们必须在add-login-feature分支的.gitlab-ci.yml文件顶部的stages:关键字下添加该阶段。完成后,完整的阶段定义部分应该如下所示:

stages:
    - build
    - test
    - deploy
    - fuzz

最后,让我们创建一个管道任务,用来触发对待测代码的模糊测试。将此任务定义添加到.gitlab-ci.yml文件的末尾,位于add-login-feature分支中:

fuzz-test-for-log-user-in:
    image: python:3.10
    extends: .fuzz_base
    script:
        - pip install --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple pythonfuzz
        - ./gitlab-cov-fuzz run --engine pythonfuzz -- log_user_in_fuzz_target.py

如果你查看由此提交触发的管道的管道详情页面,你应该会看到单元测试、代码质量和现在的模糊测试任务。如果你点击模糊测试任务查看其输出,你会注意到它正在不断地将随机数据集发送给待测代码,看似没有停止的迹象。然而,正如我们在第六章中知道的,它会在找到 bug 或者超时后停止。因为这个管道是在非默认分支上运行的,所以超时时间设置为 60 分钟(默认分支有 10 分钟的超时)。虽然这对于复杂代码中深层次 bug 的发现来说可能是合理的时间,但对于我们这段简单的登录代码来说,实在是有些过头了。

通过配置作业,加快管道速度,向正在测试的函数发送 1,000 组随机字节,并在没有发现任何错误时停止。当然,如果我们认为更复杂的登录代码会受益于更彻底的模糊测试,我们可以随时增加随机数据集的最大数量。

为了限制模糊测试执行的次数,请取消当前正在运行的管道(无需等待其在 60 分钟后超时),并确保这些行被正确缩进,以便定义一个作业范围的变量而不是全局变量:

    variables:
        COVFUZZ_ARGS: '--runs=1000'

同样在fuzz-test-for-log-user-in作业定义中,用以下内容替换script:部分的第二行(请注意,这是一行很长的代码):

        - ./gitlab-cov-fuzz run --engine pythonfuzz --additional-args $COVFUZZ_ARGS -- log_user_in_fuzz_target.py

如果你查看这些更改后运行的模糊测试作业的输出,你会发现它在找到第一个 1,000 次失败尝试后停止。您还会注意到管道详细信息页面上没有显示安全选项卡,因为模糊测试没有发现任何问题(尽管其旨在查找错误而不是安全漏洞,但 GitLab 认为模糊测试是一种安全扫描器而不是代码质量扫描器)。

我们的管道真的很顺利!让我们通过添加一些安全扫描器来保持这种势头。

保护您的代码

对于此示例用例,您将向管道添加四个扫描器:静态应用程序安全性测试SAST)、秘密检测、依赖关系扫描和许可合规性。您还将学习如何添加第三方扫描器。

将 SAST 添加到管道中

通常情况下,向管道添加由 GitLab 提供的安全扫描器是一个简单的过程。为了启用 SAST 并确保我们的“Hats for Cats”源代码不包含安全漏洞,我们只需要在add-login-feature分支的.gitlab-ci.yml中的现有include:部分包含一个新的模板。确保正确缩进,可以在现有的任何位置添加此行:

    - template: Security/SAST.gitlab-ci.yml

这样就启用了 SAST,但我们还希望配置它,以便它不扫描我们的自动化测试文件或我们的模糊目标文件。GitLab 文档告诉我们设置哪个变量以实现此目的。在.gitlab-ci.yml的末尾添加一个新的部分,以设置正确的全局变量:

variables:
    SAST_EXCLUDED_PATHS: "test_login.py,log_user_in_fuzz_target.py"

注意

此代码仅有两行:第二行足够长,可能会以令人困惑的方式换行。

等待管道完成并查看其详细信息页面。在login.py下。出于演示进行时的考虑,我们将忽略此漏洞,但看到 SAST 已经启动运行还是令人欣慰的:

图 11.7 – 管道详细信息页面上的 SAST 结果

图 11.7 – 管道详细信息页面上的 SAST 结果

将秘密检测添加到管道中

让我们将 SAST 的“表亲”秘密检测添加到我们的 CI/CD 流水线中。在add-login-feature分支,包含一个新的模板到.gitlab-ci.yml的现有include:部分。仔细检查缩进,确保一切就绪:

    - template: Security/Secret-Detection.gitlab-ci.yml

我们应该考虑一些秘密检测的配置选项。由于我们的代码库只有少数几个提交,并且我们确信到目前为止没有添加任何秘密,因此不需要启用“历史”模式。然而,告诉秘密检测不要扫描我们的测试文件是有意义的,因为其中的任何秘密都是为了测试目的而虚构的秘密。将来,我们可能会把所有与测试相关的代码放入tests/目录,但由于我们目前还没有该目录,我们将明确排除单独的文件。通过覆盖适当的作业定义并设置作业范围的变量来做到这一点:

secret_detection:
    variables:
        SECRET_DETECTION_EXCLUDED_PATHS: "login_test.py,log_user_in_fuzz_target.py"

注意

这段代码的最后一行实际上是一行,因换行问题而看起来很尴尬,所以一定要作为一行粘贴进去。

最后,让我们给秘密检测提供一个秘密来检测。假设我们的实习生 Carl 不小心将一个 AWS 访问令牌粘贴到了login.py中。在add-login-feature分支的文件末尾添加以下一行:

AWS_access_token = 'AKIAABCDEFGH12345678'

当结果流水线完成时,检查其流水线详情页上的安全标签,查看是否报告了来自秘密检测的安全漏洞:

图 11.8 – 流水线详情页上的秘密检测结果

图 11.8 – 流水线详情页上的秘密检测结果

将依赖扫描添加到流水线

到目前为止,我们的项目声明的唯一依赖是pytest自动化测试框架。由于该依赖不会在生产中使用,我们可能不关心它是否有安全漏洞,但将依赖扫描添加到我们的流水线中是明智的,这样我们就可以在将来添加任何依赖时,及时收到安全问题的提醒。

include:部分:

    - template: Security/Dependency-Scanning.gitlab-ci.yml

现在依赖扫描已经启用,让我们给它一些内容来检测。假设“Cats for Cats”网页应用将基于 Python 的 Django 网页框架构建,我们决定使用一个我们已经熟悉的旧版本 Django。在add-login-feature分支的现有requirements.txt文件末尾添加这一新行,并注意它应该是左对齐的,而不是缩进的:

django==3.2

由于此版本显著落后于当前的 4.1.1 版本,我们可能会发现依赖扫描中存在安全漏洞。果不其然,如果我们触发一个流水线并查看流水线详情页上的安全标签,我们会看到至少 15 个潜在的依赖问题!你可能会觉得使用严重性工具过滤器来减少干扰,专注于最重要的漏洞会很有帮助:

图 11.9 – 管道详情页上的严重性为“关键”的依赖扫描结果

图 11.9 – 管道详情页上的严重性为“关键”的依赖扫描结果

将许可证合规性添加到管道中

由于我们为 add-login-feature 分支声明了一些软件依赖项,请在 include: 部分添加一个新模板,并仔细检查缩进:

    - template: Security/License-Scanning.gitlab-ci.yml

在由此次编辑触发的管道详情页上,点击新的 pytest 库及其依赖项。有趣的是,许可证合规性扫描器未能确定 Django 本身使用的许可证,因此它被列为未知。通常,你会与法务团队协商,决定是否明确允许或拒绝这些许可证,可以通过点击页面上的管理许可证按钮来进行处理,但为了保持演示的连贯性,我们将跳过这一步骤。

图 11.10 – 项目依赖项使用的许可证

图 11.10 – 项目依赖项使用的许可证

一些扫描器被排除在本演示之外

你可能已经注意到,我们没有将 DAST、容器扫描或基础设施即代码扫描添加到我们的管道中。我们排除了这些扫描器,部分原因是 Hats for Cats 演示 Web 应用没有足够的功能代码来打包成 Docker 镜像并以普通用户的方式进行交互,另一个原因是我们不需要使用基础设施即代码工具来配置任何新的机器作为 Hats for Cats 项目的一部分。同样,我们还想展示并非所有扫描器都与所有项目相关。你应该只启用和配置那些对你特定项目有意义的扫描器;添加不必要的扫描器会让你的 CI/CD 管道配置文件变得复杂,并且拖慢管道速度,毫无益处。

将第三方安全扫描器集成到管道中

假设有一个我们过去使用过的第三方安全扫描器,现在我们希望将其添加到 gui-proofreader 中,它是一种 SAST 扫描形式,检查面向用户的文本中的拼写错误。假设它作为一个 Docker 镜像在 Docker Hub 上提供,你可以通过将仓库克隆到 gui-proofreader Docker 容器中,然后在该容器内运行 proofread-my-gui.sh 脚本来对仓库中的所有代码进行扫描。

让我们在 CI/CD 管道中添加一个任务,完成这个功能。在 add-login-feature 分支的 .gitlab-ci.yml 文件末尾添加以下任务定义:

proofread:
    stage: test
    image: gui-proofreader:latest
    script:
        - ./proofread-my-gui.sh

这已经足以触发第三方扫描器,但我们仍然需要将其输出集成到 GitLab 安全报告中。这个步骤很简单:将扫描器的输出声明为包含 SAST 报告的工件(我们可以将其分配给几种报告类型,但这个特定的扫描器类似于 SAST,所以我们选择使用 SAST)。将以下代码添加到 proofread 任务定义的底部:

    artifacts:
        reports:
            sast: gui-proofreader-report.json

当然,我们需要调整作业定义中指定的结果文件名,以匹配gui-proofreader扫描器实际生成的文件名。此外,GitLab 只有在文件符合 GitLab 的官方 SAST 扫描器 JSON 架构时,才能解析并显示这些结果(每种类型扫描器的安全扫描结果架构细节可以在 GitLab 文档中找到)。如果gui-proofreader扫描器无法使用该架构生成输出,我们需要编写一个小脚本,将扫描器的输出转换为符合适当架构的 JSON 文件,在后续管道阶段的独立作业中运行该脚本,并将工件声明从proofread作业移到新脚本的作业中。

由于这个作业按现有写法无法工作——因为 Docker Hub 上没有叫做gui-proofeader的 Docker 镜像——最好的做法是将这个作业定义从.gitlab-ci.yml文件中排除,或者注释掉它。然而,如果你将来确实想集成第三方扫描器,这是一个可以遵循的模型。

改进你的管道

你已经设置了一个管道,确保你的代码质量高且没有安全漏洞。在许多情况下,你可以到此为止。然而,对于这个示例用例,你将更进一步,探讨使用 DAG 来加速管道。你还将看到是否值得将管道的配置代码拆分成多个文件,以提高可读性和可维护性。

使用 DAG 加速管道

我们的管道还没有复杂到需要转换成 DAG,但如果我们继续添加更多作业,最终会因为性能原因希望对其中一些或所有作业使用 DAG。现在,让我们通过使用needs关键字来预览一下,给我们的管道添加一些 DAG 元素。

首先,假设我们希望code_quality作业仅在unit-tests作业通过之后运行。毕竟,我们可能认为我们的代码在关心美观和可维护性之前,应该先确保它能正确工作。我们可以通过将code_quality放在比unit-tests晚的阶段来实现这一点,但由于它们在概念上都属于code_quality作业定义:

    needs: ["unit-tests"]

现在已经设置好了一个迷你 DAG,让我们再创建一个。也许我们对在所有其他扫描器完成之后运行的模糊测试作业不满意。这是因为模糊测试作业在它自己的fuzz-test-for-log-user-in中:

    needs: []

当我们运行包含这些编辑的管道时,我们会看到我们所期望的行为:fuzz-test-for-log-user-in立即运行,code_qualityunit-tests完成之前暂停:

图 11.11 – DAG 导致作业没有按照阶段顺序运行

图 11.11 – DAG 导致作业没有按照阶段顺序运行

我们可以通过点击流水线详细页面中的作业依赖关系,并打开显示依赖关系来再次检查对流水线流程的理解。流水线运行结束后,我们可以看到哪些作业按什么顺序运行,得益于“需求”关系:

图 11.12 – 完成的流水线作业依赖关系视图

图 11.12 – 完成的流水线作业依赖关系视图

将流水线拆分为多个文件

既然我们已经设置好了项目的 CI/CD 流水线来验证和确保代码的安全性,那么让我们看看如何保持流水线配置的整洁和易读。当前的文件比现实项目中的 .gitlab-ci.yml 文件要简单得多,通常我们会建议当前的配置文件已经足够干净,不需要重构。然而,为了展示如何在配置变得更加复杂时进行维护,接下来我们还是将它分成两个文件。

假设我们决定在我们团队的所有项目中标准化安全扫描过程。实现这一目标的好方法是将所有与安全相关的作业定义分离到一个不同的 CI/CD 流水线配置文件中,然后将其包含到每个项目的 CI/CD 配置文件中。

add-login-feature 分支的根目录下创建一个名为 security-jobs.yml 的新文件。将以下行添加到新文件中:

include:

将这些行从 .gitlab-ci.yml 文件的 include: 部分剪切并粘贴到 security-jobs.yml 文件的 include: 部分:

    - template: Security/SAST.gitlab-ci.yml
    - template: Security/Secret-Detection.gitlab-ci.yml
    - template: Security/Dependency-Scanning.gitlab-ci.yml
    - template: Security/License-Scanning.gitlab-ci.yml

最后,通过在 .gitlab-ci.ymlinclude: 部分的任何位置添加此行,将新配置文件包含到原始配置文件中:

    - local: security-jobs.yml

当你在做出这些更改后重新运行流水线时,你会发现它的行为与我们重构配置文件之前完全相同。这展示了如何将一个长而复杂的 CI/CD 流水线配置文件拆分为两个或更多的子文件,每个文件如果需要,可以在多个项目中复用。

注意,由于 security-jobs.yml.gitlab-ci.yml 在同一个项目中,我们使用了 include: 关键字和 local: 子关键字来指向它。如果我们要从其他项目中包含它,就需要使用 include:file: 关键字。GitLab 文档中有更多关于如何在不同情况下使用不同形式的 include: 的信息,非常值得查阅。

将你的代码部署到正确的环境

你的代码已经编写、验证并确保安全。剩下的唯一步骤就是部署它。

部署代码

我们流水线的最后任务是使用 rules:if: 关键字来控制应该运行的三个作业中的哪一个,具体取决于流水线运行所在的 Git 分支。

为了保持示例简单,我们仅讲解如何将其部署到生产环境。我们假设每当我们在生产分支上运行流水线时,都会发生此操作。

正如你在第八章中学到的那样,部署代码有无数种方法。选择哪种方法在很大程度上取决于你要部署的环境:AWS EC2 虚拟机、Kubernetes 集群、裸金属机器或其他什么东西。对于这个例子,我们假设我们正在将代码部署到一台 IP 地址为192.168.0.1的机器上,该机器运行着一个 Apache Web 服务器,托管着“Hats for Cats”网站。此外,假设要部署我们网页应用程序的新版本,我们只需要将文件复制到 Apache 主机上的正确目录,并发出命令重启 Apache。

这个部署过程非常简单。将这个任务定义添加到add-login-feature分支上的.gitlab-ci.yml文件中:

deploy-to-production:
    stage: deploy
    image: registry.hats-for-cats.com/ubuntu-with-deploy-key:latest
    rules:
        - if: $CI_COMMIT_REF_NAME == "production"
    script:
        - scp -r . root@192.168.0.1:/home/hats-for-cats/production
        - ssh root@192.168.0.1 service apache2 restart

你可能会对这个任务定义中指定的镜像感到疑惑。我们可以通过几种方式来设置scp命令所依赖的公/私钥对,其中一种方法是制作我们自己的 Docker 镜像,并将其存储在内部 Docker 容器注册表中。当然,我们也可以将这个镜像存储在这个项目或其他项目的容器注册表中,但现在假设我们已经设置了一个单独的、公司范围的 Docker 容器注册表。这个镜像包含一个带有 OpenSSH 库的 Linux 发行版(提供scp命令),并且已经生成了一个公/私钥对。然后,我们将 Apache 主机计算机配置为接受来自该 Docker 镜像的私钥。搭建好这个基础设施后,GitLab Runner 可以在特殊的 Docker 镜像内执行scp,并通过密钥对完成对 Apache 主机的身份验证。

这个任务定义中的rules:if:关键字会防止任务运行,除非流水线在deploy-to-reviewdeploy-to-staging任务上运行,这两个任务会使用不同的逻辑来指定它们应该在哪些分支上运行。

由于我们的deploy-to-production任务不会在我们提交已编辑的.gitlab-ci.yml文件的add-login-feature分支上运行,因此在提交触发新的流水线时我们不会看到它运行。然而,我们确实希望确保它能正确部署代码,那么我们该如何测试这个任务呢?

首先,我们需要将add-login-feature分支合并到Draft:,从标题开头开始,使其可以合并,然后点击Merge按钮。由于我们将这个 MR 与允许用户登录问题关联,合并 MR 将自动关闭该问题。

这个合并将我们所有的流水线配置细节添加到了deploy-to-production任务运行中,因为流水线并没有在该任务上运行。当然,我们不应指望它实际通过测试,因为我们还没有创建任务定义中指定的 Docker 镜像,而且我们也没有在192.168.0.1上实际运行生产环境,但至少我们可以看到任务已运行,这也是我们此时能够现实测试的全部内容。宣布胜利,并打开一杯冰饮料,庆祝一下。这标志着Hats for Cats项目的示例工作流结束。

概述

在本章中,我们创建了一个组和项目来存放我们的代码及其他相关组件,并创建了任务来规划和跟踪我们的工作。接着,我们将项目的仓库克隆到本地工作站,这样我们就可以使用自己喜欢的桌面工具编写代码。然后,我们创建了一个分支来提交我们的工作,并为该分支创建了一个合并请求(MR),将其与相关任务关联,并提交并推送了新软件功能的代码。我们设置了一个简单的 CI/CD 流水线,可以在其中添加各种任务,并为该项目的流水线注册了特定的执行器。我们向流水线添加了自动化单元测试,以确保代码满足设计规范,同时进行了代码质量扫描,并为该扫描器注册了一个专用执行器。我们还向流水线添加了模糊测试,以便在关键功能中查找漏洞,并加入了 SAST(静态应用安全测试)以发现代码中的安全漏洞。我们还向流水线添加了秘密检测,以查找任何意外提交到仓库中的秘密信息,并加入了依赖扫描,以了解我们项目所依赖的第三方库中的任何安全问题。接着,我们将许可证合规性检查添加到流水线中,以排除使用与我们项目许可证不兼容的软件许可证的第三方库,并将一个第三方扫描器集成到流水线中,该扫描器会自动触发并将其结果整合到现有的 GitLab 仪表板和报告中。我们重写了流水线的一部分,将其改为 DAG(有向无环图)以提升性能,并将流水线配置代码拆分为多个文件,以提高其可读性和可维护性,并添加了逻辑,使得正确分支上的代码能自动部署到生产环境。

尽管我们覆盖了很多内容,但请记住,这只是一个示例工作流。我们只使用了 GitLab CI/CD 流水线提供的无数功能中的一部分,而且几乎没有探讨这些功能的不同配置选项。在流水线中,通常有多种方式完成相同的任务,而且你可以将这些任务以无限种方式组织成阶段和作业,所以不要觉得这个示例是使用 GitLab CI/CD 流水线的唯一正确方法。要有创意,进行实验,享受探索哪些流水线功能对你的项目最有用,并发现哪些功能的配置设置最适合你和你的团队。

在下一章,我们将学习 GitLab 的故障排除以及未来的发展。

第十二章:GitLab 故障排除与未来展望

到目前为止,我们已经涵盖了 GitLab CI/CD 从规划、构建、测试到交付软件的端到端使用过程。你应该对 GitLab CI/CD 流水线和 Runner 基础设施的术语有所了解,并且有信心使用 GitLab 开发和部署基本应用程序。

CI/CD 在软件行业中占据着一个不断变化的空间。今天的最佳实践和工具在五年后可能已经过时,甚至更早。本书中我们有意识地努力保持在概念和工具之间的平衡。我们的目标是通过强调 CI/CD 和 DevOps 基础知识来保持内容的相关性,同时即便语法和一些工具在未来几年有所变化,依然为你提供实践和跟随的机会。

本章的目的是综合我们所学的内容,并引导你迈向 DevOps 旅程的下一步。我们将首先讨论一些常见的故障排除场景和可能遇到的“陷阱”,这些问题通常出现在使用 CI/CD 时。接下来,我们将讨论如何在运维领域使用 GitLab CI/CD,将软件开发工作流和版本控制应用于基础设施。最后,我们将讨论行业未来的变化趋势,并总结本书的关键要点。

本章将按照以下方式覆盖这些主题:

  • 常见流水线问题的故障排除与最佳实践

  • 使用 GitOps 管理你的运维基础设施

  • 未来行业趋势

  • 结论与下一步

技术要求

与前几章一样,建议在 GitLab 实例(SaaS 或自托管)上拥有一个帐户。本章中的一些内容更偏向于概念性讨论,而非基于实例的内容,但如果你希望实践 使用 GitOps 管理运维基础设施 部分讨论的概念,建议使用 基础设施即代码IaC)工具,如 Terraform 和 Ansible。如果是这种情况,建议在云服务提供商(如 AWS 或 Microsoft Azure)中注册一个账户,以便使用你选择的 IaC 工具进行基础设施的配置与管理。

接下来,我们将识别并排除在使用 GitLab CI/CD 时常见的问题。

常见流水线问题的故障排除与最佳实践

在 GitLab CI/CD 流水线中遇到的问题大致可以分为两类。第一类是由 .gitlab-ci.yml 文件中的语法和逻辑引起的错误或意外行为,这是主要的 CI/CD 配置文件。第二类涉及用于运行 CI/CD 作业的 Runner 基础设施中的限制或配置错误。我们将依次解决这两类问题。

排查 CI/CD 语法和逻辑问题

当 GitLab CI/CD 的错误可以归结为 .gitlab-ci.yml 文件内容时,故障排除的第一步是确定问题是由于语法错误或不支持的语法,还是有效 YAML 文件的配置错误(或误解)。

.gitlab-ci.yml 中的语法错误

最容易排查的情况之一(因此也是首先检查问题的好地方)是 .gitlab-ci.yml 中的语法或格式错误。一个典型的例子是由于遗漏必需的关键字而导致的错误。GitLab CI/CD 至少要求至少有一个阶段(使用 stages 关键字定义),每个作业必须分配到一个阶段,然后每个作业必须执行某些操作(通常使用 scripttrigger 关键字定义)。以下示例展示了一个非常基础的配置,定义了两个阶段和两个作业。然而,compile_assets 作业缺少阶段分配。因此,build 阶段没有至少一个作业分配给它:

stages:
  - build
  - deploy
compile_assets:
  script:
    - echo "Run build scripts here"
publish_application:
  stage: deploy
  script:
    - echo "Run deployment scripts here"

图 12**.1 显示了我们如果导航到 CI/CD | Pipelines 时看到的管道错误:

图 12.1 – 显示在管道页面上的配置错误信息

图 12.1 – 显示在管道页面上的配置错误信息

请注意,当出现 YAML 语法错误时,GitLab 甚至不会尝试运行管道(即,不会将作业分配给执行者)。我们看到错误信息提到 compile_assets 作业的阶段不存在。

作业的默认阶段分配

图 12**.1 中显示的错误实际上有些微妙。严格来说,如果 CI/CD 作业没有在配置中显式分配到某个阶段,GitLab 会自动将其分配到 test 阶段。然而,在这个示例中,我们没有定义 test 阶段。因此,GitLab 报告“选择的”阶段(即 GitLab 本应将 compile_assets 作业分配到的阶段)不存在,因为我们只定义了 builddeploy 阶段。

如果配置不符合正确的 YAML 格式,GitLab 也会产生错误。我们尝试通过将 compile_assets 作业添加到 build 阶段来修复之前的错误:

stages:
  - build
  - deploy
compile_assets:
stage: build
  script:
    - echo "Run build scripts here"
publish_application:
  stage: deploy
  script:
    - echo "Run deployment scripts here"

当我们再次查看管道图时,我们看到另一个错误,这次是更通用的错误,指出配置没有有效的 YAML 语法(见 图 12**.2)。尽管错误信息比较模糊,但 GitLab 确实包含了一个 CI 语法检查工具的链接,可以帮助找出问题所在:

图 12.2 – CI/CD 管道的通用语法错误信息

图 12.2 – CI/CD 管道的通用语法错误信息

跟随链接会带我们到一个叫做 .gitlab-ci.yml 文件的工具。在这种情况下,图 12**.3 显示了一个错误描述,提到一个未知的映射值。最终,问题出在 YAML 的缩进要求上。stage 关键字必须至少缩进两个空格,才能成为 compile_assets 作业定义中的有效元素。

图 12.3 – 显示无效 CI/CD 配置的 Pipeline 编辑器

图 12.3 – 显示无效 CI/CD 配置的 Pipeline 编辑器

在正确缩进后,图 12.4 现在显示了描述有效 CI/CD 语法的 Pipeline 编辑器。GitLab 现在将根据此配置创建一个可运行的管道:

图 12.4 – 显示有效 CI/CD 配置的 Pipeline 编辑器

图 12.4 – 显示有效 CI/CD 配置的 Pipeline 编辑器

因此,对于 CI/CD 语法问题的主要建议是利用 Pipeline 编辑器,这样您可以实时检查 CI/CD 配置中的逻辑和风格错误。编辑器对于识别拼写错误、遗漏的必需关键字和 YAML 格式问题非常有价值。

CI/CD 配置逻辑和作业顺序

CI/CD 排错的另一个关键领域是理解作业定义和顺序的逻辑。随着管道的复杂化,您的 .gitlab-ci.yml 文件可能会开始充满 rules 关键字,用于管理作业的执行方式,且主配置可能会调用其他配置作为模板或下游管道。

请考虑以下 CI/CD 配置:

stages:
    - build
    - test
    - deploy
build_app:
  stage: build
  script:
    - echo "Run build on all branches"
static_tests:
  stage: test
  script:
    - echo "Run static tests only on feature branches"
  rules:
    - if: '$CI_COMMIT_REF_NAME == "main"'
      when: never
    - when: always
  needs: []
deploy_app:
  stage: deploy
  script:
    - echo "Run deploy only on main branch"
  only:
    - main
  needs:
    build_app

在这里,我们的配置中混合了逻辑关键字和作业顺序。test_app 作业使用 rules 关键字,以确保它在 main 分支上从不执行;否则,它会在所有其他分支上运行。与此同时,deploy_app 作业使用 only 关键字,指定它只在 main 分支上执行(而不是其他分支)。最后,deploy_app 作业需要 build_app 作业先通过,才会在 main 分支上运行,而 static_tests 作业则在功能分支上独立运行,无需等待其他作业(如 build_app)。

随着管道的增长,逻辑和不同 CI/CD 变量的混合可能会变得难以解析。在 GitLab 用户界面中有一些功能可以帮助您理清作业的逻辑和顺序。作业依赖(见 图 12.5)和 Needs 标签在 CI/CD | Pipelines | Pipeline ID 中,允许您显示哪些作业依赖于其他作业:

图 12.5 – 显示有效 CI/CD 配置的 Pipeline 编辑器

图 12.5 – 显示有效 CI/CD 配置的 Pipeline 编辑器

Pipeline 编辑器还具有 main 分支:

图 12.6 – 在 Pipeline 编辑器中进行管道验证

图 12.6 – 在 Pipeline 编辑器中进行管道验证

我们强烈建议您在开发和部署过程中使用这些功能,以组织和可视化您的管道。

排错管道操作和 Runner 分配

除了语法和逻辑错误,注意用于 CI/CD 执行的 Runner 也是很重要的,确保您的管道配置能够得到 Runner 基础设施的支持。可能出现的潜在问题包括 Runner 标签管理不当以及缺少或配置错误的容器支持。

正确管理运行器标签

tags CI/CD 关键字(不要与已标记的 Git 提交混淆)告诉 GitLab 只将任务分配给包含相同标签的运行器。为了确保 CI/CD 任务标签被正确理解和使用,需考虑以下几个方面。

当在 CI/CD 任务中指定多个标签时,这意味着 GitLab 只会将任务分配给同时拥有所有这些标签的运行器。例如,以下任务将仅分配给拥有 ubuntu 标签、python3 标签和 amd64 标签的运行器:

deploy_to_linux:
  stage: deploy
  script:
    - ./deploy_script.py
  tags:
    - ubuntu
    - python3
    - amd64

另一方面,接收任务的运行器也可以具有其他描述性标签,只要这些标签至少包括 ubuntupython3amd64 标签。如果任务没有被预期的运行器接收,请检查运行器的分配标签,在 设置 | CI/CD | 运行器 中查看。如果不同的运行器配置在 GitLab 的不同部分——即项目、组或实例级别,记住这一点。

管理容器化的 CI/CD 流水线

基于容器的环境,如 Docker 和 Kubernetes,越来越频繁地被软件团队用于开发、测试和部署。image 关键字是 CI/CD 任务中的关键字,用于指定任务必须运行的 Docker 容器类型。然而,容器化 CI/CD 任务的正确执行有许多依赖因素,除正确的配置语法外,还需要考虑其他问题。请看以下任务定义:

launch_web_services:
  stage: deploy
  image: node:19
  services:
    - postgres:latest
  script:
    - npm start

任务的指令是在从 Node.js Docker 镜像启动的容器内启动 Web 服务,同时运行一个支持的 Postgres 数据库。让我们思考任务成功执行所需的条件:

  • 支持 Docker 容器的环境中的运行器。

  • 非容器运行器不会无意间接收任务。

  • 运行器可以连接到像 Docker Hub 这样的容器注册表,以拉取基础镜像。

这只是一个简单的例子,要求开发者思考他们的任务执行所使用的基础设施。解决第二个问题(非容器运行器接收任务)的一种方法是为基于容器的运行器专门定义一个 CI/CD 标签,并在任何使用 image 关键字的任务中包含该标签。

需要同时管理操作基础设施和应用程序逻辑可能会让开发者感到有些吃力。在接下来的部分,我们将介绍 GitOps 的概念,以帮助将开发与运维统一到最佳实践的工作流中。

使用 GitOps 管理你的操作基础设施

开发不能脱离执行代码的基础设施。我们已经清晰地看到,从将 GitLab 用作源代码管理工具,到定义 CI/CD 流水线来定义源代码如何构建和部署,再到运行器(基础设施)作为 CI/CD 的一个重要组成部分的过程中,基础设施的重要性。

正确理解的 DevOps 是一种将开发和运维统一起来的文化。那么,我们如何将配置和管理我们的操作基础设施纳入到开发人员已经使用并理解的 GitLab 流程中呢?答案在于实施 GitOps。GitOps 是一种属于 DevOps 的开发实践,鼓励使用与开发团队已经使用的相似的迭代变更管理模型。正如这个术语所暗示的那样,Git 是工作流的核心部分。基础设施应该像应用程序源代码一样,受到版本控制。对基础设施的更改是通过对代码库进行修改来实现的,这反过来会触发 CI/CD 管道,更新我们的环境以适应这些提交的更改。

GitOps 通常意味着将基础设施纳入软件版本控制之下

本章中描述的 GitOps 比我们之前讨论的 Kubernetes 集群的特定 GitOps 工作流要广泛。在这里,GitOps 仅仅意味着将 Git 开发工作流应用于任何形式的基础设施管理。本章使用 Terraform 和 Ansible 作为参考示例,这些示例可以跨多个不同的配置管理工具进行应用。

回想一下来自 第三章图 3.10(这里再次展示),当时我们介绍了 GitLab 流程作为一种迭代分支模型,让开发人员以结构化且高效的方式进行更改:

图 12.7 – GitLab 流程中的主要步骤

图 12.7 – GitLab 流程中的主要步骤

这一工作流的核心动机是消除开发过程中的摩擦。对 Git 版本化代码库中的文件进行文本更改,会触发自动化管道的构建、测试和部署到相应环境。审查和批准已经集成到合并请求功能中,确保我们的所有更改都是有意的,而版本控制确保我们可以在需要时回滚。

那么,如何将这个模型应用于基础设施管理呢?就像开发人员使用编程语言来开发应用程序一样,现今行业中有大量基于文本的工具,可以用于描述、提供和配置操作环境。这些工具被称为“基础设施即代码”,因为它们旨在将基础设施(如服务器、网络、存储、数据库、身份和访问控制)置于与传统软件应用程序相似的编程模型之下。

基础设施即代码(IaC)是一个快速发展的领域,新的工具不断取代旧的工具。由于快速的创新可能使当今常见的工具过时,决定在此类书籍中介绍哪种软件可能会很困难。然而,截止到本文写作时,我们已经看到一些 IaC 工具在 GitOps 和 GitLab 中被广泛使用。我们在这里展示它们,并理解在基础设施工具中,技术状态正在迅速变化。话虽如此,我们经常看到的工具是Terraform用于基础设施配置和Ansible用于配置管理。

使用 Terraform 部署和更新基础设施状态

Terraform 是一个 IaC 工具,用于描述你的基础设施应如何呈现。核心项目是开源的,企业版本由 HashiCorp 开发并销售。Terraform 包括一种语言和语法来描述基础设施状态,以及一套命令行工具,用于验证和更新该基础设施。使用的配置模型是声明式的。我们描述我们的计算、网络、存储等应如何呈现,软件则执行必要的最小更改,以使我们的基础设施反映该配置。

让我们看一个非常基础的示例,使用 Terraform 在 AWS 中配置一个对象存储资源。Terraform 要求你在以 .tf 结尾的状态文件中描述配置。Terraform 语法的细节超出了本书的范围,但以下语法展示了如何在 AWS 中配置一个 S3 桶。这个“代码”可能会被存储在名为 bucket.tf 的文件中:

resource "aws_s3_bucket" "coreSite" {
    bucket = "travelBlog"
    acl = "private"
}

这个简单的状态文件在 AWS 中指定了一个 S3 桶资源,包括名称和访问权限。接下来,由 Terraform 来“实现这一目标”。在 GitOps 和 GitLab 的背景下,"实现这一目标"是通过 CI/CD 管道完成的。该管道将运行必要的 Terraform 命令来初始化、验证并将配置应用到你的 AWS 账户中。

安装和配置 Terraform 可能需要一些工作。幸运的是,GitLab 将 Terraform 的工具封装在一个 Docker 镜像中,可以通过 .gitlab-ci.yml 中的 image 关键字来启动。GitLab 维护了一个公共示例项目,里面有一个示例 .gitlab-ci.yml 文件(gitlab.com/gitlab-org/configure/examples/gitlab-terraform-aws/-/blob/master/.gitlab-ci.yml),该文件包含了应用或更新 AWS 中 Terraform 配置的相关作业和命令。我们建议你浏览这个页面,并在你自己的 AWS 账户中测试这些示例。注意,preparevalidatebuilddeploy 阶段及其相关作业在概念上类似于典型应用开发中的 buildtestdeploy 阶段。

Terraform 是一个主要用于部署或配置基础设施的工具。我们将讨论的第二个工具,主要用于配置已部署的资源。

使用 Ansible 管理资源配置

Ansible 也是一个开源的 IaC 工具,Red Hat 提供了付费版本。与 Terraform 类似,Ansible 通过在文件中声明资源配置来使用,文件内容随后通过运行命令转化为配置变更。这些 Ansible 资源配置文件称为 playbooks,并且采用 YAML 格式编写。一个极其基础的 playbook 内容,安装并启动 Apache,可能如下所示:

---
- hosts: web
  become: yes
  tasks:
   - name: ensure Apache is installed
     yum:
       name: apache2
       state: latest
   - name: ensure Apache is enabled and started
     service:
       name: apache2
       enabled: yes
       state: started

与 Terraform 一样,我们使用声明性语法来描述我们的环境应该是什么样的,并依赖工具(在本例中为 Ansible)来实现这一目标。在这里,我们指定一个名为 web 的主机集合应该安装 apache2 包,并且相应的服务应该被启用并启动。然后,Ansible 会连接到相关主机,并在连接后安装并启动 apache2

同样像 Terraform 一样,我们会定义 CI/CD 作业,运行工具的命令以应用配置更改。在 Ansible 的情况下,GitLab 没有官方的带有 Ansible 工具的 Docker 镜像。因此,我们可能会告诉 GitLab 使用一个我们知道是 Ansible 主机的 runner 来运行我们的 playbook:

ansible-run:
    stage: run
    tags:
        - ansible
    script:
        - ansible-playbook apache-playbook.yml --key-file $REMOTE_HOST_CREDENTIALS

上述作业被分配给一个带有 ansible 标签的 runner(我们可以假设这是一个已安装并配置了 Ansible 的 runner)。作业脚本随后会运行 playbook,并可能通过 CI/CD 变量传递凭据,以便 Ansible 可以对它所配置的服务器进行身份验证。

有关将 Ansible 与 GitLab 一起使用的更多细节,包括在运行 Ansible 命令时如何解释和解析 CI/CD 作业输出,我们推荐您参考 GitLab 发布的这篇文章(about.gitlab.com/blog/2019/07/01/using-ansible-and-gitlab-as-infrastructure-for-code/),作为更深入的参考。请同时参考官方的 Terraform 和 Ansible 文档站点,以获取有关设置和系统要求的更多详细信息。最终,本节的目标是向您介绍 GitOps 概念,作为将 IaC 工具整合到我们迄今为止学习的 CI/CD 工作流中的一种范式。

未来行业趋势

不言而喻,学习任何新事物时,都有一个期望,那就是知识将来仍然相关。展望未来,持续集成和持续交付在软件领域中有望作为关键实践持续存在,并将继续经历演变变化。我们可以识别出未来几年软件开发生命周期可能发生变化的三种趋势。它们是:自动化的增加、抽象化的增加和开发周期时间的缩短。我们还可以预期 GitLab 作为平台将继续发挥重要作用。前几章中展示的概念、示例和工具将为你现在成为更高效的开发者打下坚实的基础,同时也提供适应行业不可避免变化所需的信心。

自动化将以更大规模创造更多软件

现在,问任何一个组织什么是运营中最昂贵的部分,他们很可能会回答是人力。熟练的人工劳动力,包括技术人员,尤其昂贵。优秀的工程师难以找到,更难以替代,且留住他们非常昂贵。此外,这些工程师们已经设计了软件和工具,使得他们自己的工作更加轻松高效。组织们现在正在利用这些工具,以便用更少的人力创造和部署更多的软件。

想想 20 年前甚至 10 年前,公司所雇佣的手工质量保证测试团队。现在,考虑像 GitLab 这样的工具在允许单个开发人员自动化软件发布过程(从概念化到部署)中所起的作用。而且,现在云服务提供商几乎为任何拥有信用卡的开发人员提供无限量的基础设施——不再需要专门的团队来进行服务器的安装和管理。

然而,除了奇点的预测之外,人类在短期内不会去哪里。自动化的普及需要能够理解和维护这些工具的人。你能说懂自动化的语言,以及像 GitLab 这样推动这一趋势的平台,你就能更好地参与到自动化革命中。

抽象化将引领万物皆代码的商业模型

2011 年,Marc Andreessen 写下了他那篇如今广为人知的文章《为何软件正在吞噬这个世界》(a16z.com/2011/08/20/why-software-is-eating-the-world/)。Andreessen 预测,下一代互联网公司正在引领一场广泛的经济变革,在这一变革中,软件将成为大多数,甚至所有行业的语言和引擎。十年后的 2021 年,Jon Eckhardt 写了一篇后续文章,标题为《软件正在吞噬吞噬世界的软件》(eiexchange.com/content/software-is-eating-the-software-thats-eating-the-world)。Eckhardt 认识到,不仅传统行业被软件颠覆,而且一代新的工具也在颠覆现有的软件模型。这些工具可以被看作是强大且更易使用的程序,使得开发者能够在更高的抽象层次上工作。也就是说,开发者可以专注于业务逻辑,而工具则负责处理更细微的细节。

一个增加抽象层次的好例子是过去 30 年基础设施的变化。行业从大型主机转向了服务器模型。接着,又转向了虚拟机,因此我们可以开始将操作系统视为灵活的软件定义资源,运行在共享主机上。然后,配置管理帮助我们维护 IT 资源,从而使我们能够更多地关注“做什么”,而不必过多关注“怎么做”。之后,云服务迅速发展,以至于没人需要亲自踏入数据中心。现在,像容器和函数这样的额外工具,使开发者能够专注于他们的代码,而无需考虑它可能运行的计算机和网络。

我们预计这一抽象化趋势将会持续下去。像自动化一样,我们完全不认为人类会变得不再重要。相反,我们预期将会有更多的整合和专业化。代码总会在某人的计算机上运行,无论在哪个地方,我们都需要基础设施专家来构建和维护这些环境。但我们也预计像 GitLab 这样的工具将继续普及,提供自文档化的程序化模型,能够观察到业务的所有层次。

减少周期时间将帮助团队更快发布更好的软件

DevOps 哲学主要是文化层面的,但它也代表着一种受精益制造革命启发的工作系统。基于自动化和抽象趋势,并结合如敏捷和 Scrum 等项目管理方法论,团队现在以一种方式开发软件,即早期频繁发布,融入小的改进,并在需要时回滚。

像 Eric Ries 的《精益创业》这样的书籍引入了这一开发模型的语言,许多工具也应运而生,允许开发人员以非常快速但受控的方式构建、测试和发布软件。GitLab 就是这些核心平台之一。源代码管理和版本控制确保了与共享代码库的持续集成,管道则在每次小规模提交后,允许执行一套共同的构建、测试和部署任务。这使得快速、迭代式的开发成为可能,并能够不断适应业务需求。

自动化、抽象化以及更小的发布周期的结合将要求开发人员适应迭代式的项目管理和设计方法。不要害怕失败,因为快速的小变更不可避免地会导致一些代码出现问题或有缺陷。采用持续改进的思维方式,专注于开发更好的软件,而不是追求完美的软件。

结论与下一步

本书引导你通过使用 GitLab 作为平台来管理你的软件开发生命周期。在讨论了 DevOps 作为一种文化和方法论出现之前行业的现状后,你接着学习了 Git 版本控制和 GitLab 的项目管理组件的基础知识。然后,GitLab CI/CD 管道被介绍为组织、设计和自动化开发工作流的核心功能。

你了解到,GitLab CI/CD 包含三个组件:基于文本的管道配置文件、执行管道任务的 runner,以及协调配置与 runner 代理之间的 GitLab 主应用程序。接着,我们展示了如何利用管道来验证、加固、打包和部署代码。你了解到,你可以将 GitLab CI/CD 作为一个统一的工具,执行软件生命周期中这些通常是离散的步骤。

在讨论完支持的 CI/CD 特性和工作流后,我们转向了一些高级话题和最佳实践。你学到了如何提高管道的速度和可维护性,以及如何将 GitLab CI/CD 应用于其他可能并不立即显现的工作流中。

最后,第十一章向你介绍了一个端到端的示例,使用了之前讨论过的所有概念和功能。该章节的目标是展示一个基础但真实的 GitLab 使用案例,支持规划、构建和部署应用程序的整个工作流。我们随后以这一章作为总结,提供了一些最终的故障排除技巧、使用 GitLab 进行下一代基础设施管理的指南,并展望了未来几年行业可能的发展方向。

我们希望你与 GitLab、CI/CD 和 DevOps 的旅程不会随着本书的结束而告一段落。GitLab 本身是开源核心,而 GitLab 公司在软件的管理和演进方面也保持透明。我们鼓励你继续关注 GitLab 的演变和产品路线图,通过关注核心产品的 issue 跟踪系统,并参考产品的在线手册页面,这些页面记录了 GitLab 各项功能的类别发展方向。

最终,作为技术人员,我们知道学习永无止境。我们希望本书能帮助你了解 GitLab 作为一个软件平台,它所要解决的问题,以及你如何使用它来应对自己组织中的挑战。我们祝愿你在继续你的 DevOps 旅程时一切顺利。

posted @ 2025-06-26 15:33  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报