我们正在谈论 CI/CD

文摘   2024-11-01 08:31   上海  

本篇内容是根据2021年10月份#162 We're talkin' CI/CD[1]音频录制内容的整理与翻译

持续集成和持续交付都是我们听说过的术语,但它们的真正含义是什么?如果做得好,CI/CD 会是什么样子?我们可能想避免哪些陷阱?在本集中,《CI/CD with Docker and Kubernetes》[2] 一书的作者 Jérôme 和 Marko 与我们分享他们的想法。


过程中为符合中文惯用表达有适当删改, 版权归原作者所有.

Jon Calhoun: 大家好,欢迎来到Go Time节目。今天我们邀请到了Marko Anastasov。Marko,你想和大家打个招呼吗?

Marko Anastasov: 大家好,谢谢你们邀请我。

Jon Calhoun: 我们还邀请到了Jérôme Petazzoni。Jérôme,你也和大家打个招呼吧。

Jérôme Petazzoni: 大家好。

Jon Calhoun: Marko是Semaphore[3]的联合创始人,Semaphore是一种持续集成/持续部署服务。而Jérôme曾是Docker团队的一员,他会演奏十几种乐器,同时你还教授有关Kubernetes中的容器技术,对吗?

Jérôme Petazzoni: 是的,完全正确。

Jon Calhoun: 另外,我们还有另一位主持人Kris Brandow。Kris,你也和大家打个招呼吧。

Kris Brandow: 大家好。

Jon Calhoun: 好的,今天的嘉宾阵容让我们不言自明,今天我们要讨论的主题是持续集成和持续部署(CI/CD)。那么,为了逐渐展开话题,我们从一个简单的问题开始吧---什么是持续集成和持续部署呢?

Marko Anastasov: 持续集成本质上是一个开发者经常将各自的工作集成到某种中央分支的过程。对很多开发者来说,当你想到持续集成时,首先想到的就是测试,构建和测试代码。为什么会这样呢?因为要经常进行集成,我们需要非常快速地确认我们集成的内容是否有效。所以这也是为什么我们会采用自动化测试的实践。

持续交付则是一个更广泛的软件开发方法,其中包含了一些实践,其中一个就是持续集成。你确保你的代码始终处于可部署的状态。通常,这意味着你的部署流程,继测试之后,也是自动化的,且通常足够简单和稳健。

Jon Calhoun: 我还有一个后续问题,为什么我们总是一起提到这些术语?CI/CD现在几乎变成了一个单一的术语,但听起来它们实际上是两个独立的概念,只是经常被捆绑在一起。

Marko Anastasov: 是的。例如,在我个人的开发者旅程中,我首先接触到的是持续集成,这让我意识到了自动化测试的重要性,并且经常获取反馈。我认为这可能是一个常见的情况。

另一方面,部署---即使你只是在做一个原型,没有测试,甚至没有考虑CI,你可能会通过其他方式进行部署。比如说,也许你做了一个git push,然后它就上线了。

所以在术语上有些混淆,因为这些事情通常是一起做的,特别是在一定规模的团队和代码库中。当你只谈持续交付时,可能会让人觉得它过于模糊,无法理解它也包含了CI。而我认为这只是为了让我们明确我们在谈论什么。

Jon Calhoun: 那么,如果我们在讨论CI/CD,它解决了哪些问题,从而让公司愿意采用它?为什么它最近被如此广泛地采用?

Jérôme Petazzoni: 我认为这完全是为了提升开发者的速度,能够更快地交付东西,从而缩短我在代码编辑器中点击保存到查看代码是否有效的时间。

我记得我还是个青少年的时候,我很幸运,我父亲会写代码。我记得有一次我看到Turbo Pascal的广告,上面写着“它每秒可以编译57,000行代码”。我不记得确切的数字了,因为那是很久以前的事情,但当时我在想“有什么意义呢?一个编译器能够在一秒钟内编译比我一生可能写的代码还要多的代码,为什么这是一个重要的指标?”很多年后,我才意识到,也许这很重要,因为通常当我们编译一个大的代码库时---你知道那个XKCD的笑话,几个人坐着转椅,拿着吸管互相打架,而老板走过来说“你们在干什么?”他们回答说“哦,我们在等代码编译完。”老板就说“哦,好的,没事。”

过去我们总是等待代码编译,现在我们在等待代码验证是否有效。它需要经过构建,可能还要部署到测试环境,然后我们又要等人来进行QA测试。

如果我们可以自动化尽可能多的步骤,就可以节省时间。如果我可以点击保存,推送到某个分支,然后我知道一系列自动化流程会构建我的代码,测试它,部署到某个暂存环境,并通过Slack或其他方式通知我“嘿,你的代码已经部署到这个环境了,现在你可以查看了。”也许是我自己来查看,也许是QA人员,或者是同事,或者是要求我交付这个特定功能的经理。

如果我们能将这个时间缩短,如果从我需要打开一个JIRA工单让别人把我的东西放到生产环境中,到现在自动化完成只需五分钟,那就意味着我可以每五分钟迭代一次,而不是每天一次。我可以每小时进行多次实验,犯多次错误,而不是每天只能一次。所以对我来说,这就是CI/CD的核心:让我可以快速尝试多次,快速失败,修复我的bug,然后再尝试。到一天结束时,我也许可以尝试、失败并最终成功10次、20次甚至50次,而不是只有一次。

Jon Calhoun:  这很有道理。当你提到将代码推送到暂存环境,并进行QA测试时,我不由得想到这些流程通常与大型项目挂钩,而不是只有一两个开发者的小项目。你觉得这种做法是随着团队规模和项目规模的增长而变得更有价值,还是无论团队规模如何,它都值得应用?

Jérôme Petazzoni: 两者皆是。一段时间之前,我也会同意这种看法,觉得“哦,这太复杂了。我不确定我的小项目是否需要这个。”但后来有几件事让我改变了主意。首先是我看到Heroku的出现,那是十多年前,我刚加入dotCloud的时候。Docker最初是一家PaaS公司,和Heroku竞争;而Heroku的能力让我可以直接推送我的代码,而不是推送到一个仓库,而是推送到一个可以构建和部署的地方---这非常棒,而且非常简单。这也是Heroku的全部意义所在,dotCloud模仿了它,并为更多语言提供了支持。这种方式即使对于小项目也能很好地工作。

某种程度上,我几乎要说,尤其是对于小项目。我指的是,如果有人想开始使用Django或Rails,或者是你最喜欢的JavaScript框架,甚至是现在的Go,你必须考虑“我要在哪里以及如何部署它?”当然,如果我只是在部署一个微服务API后端,那只是一个Go服务,没有别的,很简单,不需要问太多问题。但如果我有一个API端点,可能还有一些静态资源,这些资源通过一个小的优化管道处理等等,那么简化这个过程就很有意义。如果我可以推送而不是手动运行一系列命令和脚本,那将降低部署的门槛,让人们可以更轻松地看到并使用它。所以我认为即使是小项目,这也很有意义。

Marko Anastasov: 我认为这是一个非常重要的观点。即使你可能最初没有计划(或者根本不打算)编写测试,设置一个部署管道也是个好主意,假设你正在为其他人类构建东西。想法是,一旦你写完代码,就自动化一切需要发生的事情,直到其他人可以看到或使用它。让这成为一个命令的事情。如果中间有多个步骤,那就是CD管道的任务。

Jon Calhoun: 那么有没有一些情况,你认为使用持续集成或持续部署不太合适?或者说,不是一个坏主意,但可能不会带来太多价值?

Jérôme Petazzoni: 也许在需要花费大量精力的情况下。这是一件好主意的事情,但如果因为设置或其他特殊情况让你跳过非常复杂的环节,浪费了很多时间,那我也会质疑它是否值得。但这不应该成为借口。我们不能总说“哦,我的应用程序很特殊,所以我不能做CI。”我更喜欢“是的,并且……”的思维方式,比如“是的,我应该做CI,但目前因为某些特殊问题无法实现。但一旦解决了这些问题,我就可以做到。”

例如,在Kubernetes生态系统中,我曾有过这样的想法:“哇,我真希望每次可以在一个全新的Kubernetes集群上运行一系列测试。”想象一下你推送代码,然后它会部署一个完整的集群,在集群上测试代码,最后销毁集群。几年前,这种想法虽然不算不可能,但有些荒谬,因为它会占用大量资源和时间。而今天,你可以使用像kind[4]这样的工具,非常轻松快捷地实现这一点,因为技术发展了许多,我们得到了很多贡献和新项目。因此,过去看似极其复杂和昂贵的事情,现在已经变得非常普遍且相对容易。

所以我认为,不要把任何事情定死,而是要接受“我现在不能做是因为X,但一旦解决了X,我就可以做”的思路。

Marko Anastasov: 是的,我还想补充一点,不同的行业可能有不同的CI/CD做法。也许你所在的行业不允许持续部署,或者法规不允许,或者你不希望持续部署一些代码,比如控制飞机或医疗设备的代码。在另一方面,某些复杂代码库没有测试,你持续部署这些代码的风险会非常大。这些团队通常不会真正进行持续部署,但他们意识到风险的存在,通常会有非常复杂的流程;也许每周或每月部署一次,有几个团队成员需要批准。QA团队会反复核查各种场景,确保一切都正常运行。所以每个情况的成熟度水平是不同的。

至于CI,我可能会重新表述一下---“为这个项目编写自动化测试是否有意义”,这样可能更清楚。如果你只是做原型设计,还不知道最终会做什么,那么可能不是编写测试的最佳时机。但一旦你对自己在做什么有了清晰的认识,并且你在朝着某个方向努力,想要让它最终面向某些用户,无论是开发者还是最终用户,只要你和用户之间有共识,认为你编写的东西应该能够正常工作,我认为没有理由不编写至少一些测试。如果是因为缺乏经验或技能,那也没关系。但这是一个不同的话题,比如如何提高这方面的能力。

Jon Calhoun: Marko,你提到在一些情况下法规不允许持续部署,比如部署到飞机上的软件。大多数时候,当我想到CI/CD时,我更多的是在想网络应用程序,但我知道它也可以用于其他场景。你有相关经验吗?可以谈谈这种情况下的CI/CD设置吗?交付在这种场景中意味着什么?

Marko Anastasov: 我想到一些Semaphore的客户,他们在一些非传统的行业工作,至少对大多数开发者来说是这样的。但我现在想不出具体的案例。在大多数情况下,各行各业都在进行数字化转型,大家都在编写某种类型的网络应用程序,或是移动应用程序。

我最近和一些人交谈过,他们在做一些卫星技术,虽然它不是网络应用程序,也不是Linux系统,它是一个实时操作系统。在这种情况下,以及回想我早期职业生涯中做过的一些嵌入式系统项目,在这些项目中,编写测试并不那么普遍。更多情况下是手动QA,然后有一个发布周期,频率比每日发布低得多。

Jon Calhoun: 明白了。

Jérôme Petazzoni: 我想补充一下,当你部署到太空或飞机等场景时,你肯定可以做CI,但CD就不太现实了,因为部署本身不像将代码推送到服务器那样简单和自动化。实际的部署过程涉及一系列工业流程和代码,理想情况下,你可以做一些CI,但往往这也非常复杂,因为你必须模拟很多东西。而CD在这种情况下是不太可能的,因为代码运行在一个与外界隔绝的环境中,甚至可以说是“太空隔绝”的环境。这些都是非常特殊的场景。

Marko Anastasov: 是的,我最近还在研究---有一种叫Verilog[5]的语言,人们用它来编写芯片;你可以用代码定义芯片。令人惊讶的是,居然还有Verilog的TDD(测试驱动开发)框架。所以我觉得,所有领域的技术都在进步。

Kris Brandow: 我认为另一个可能只做CI而不做CD的领域是库开发。如果你不是在构建一个在服务器上运行的产品,而是构建一个供其他人使用的库,那就非常适合做CI。我仍然想运行所有的测试,确保一切正常,但我不会为每次提交或每次关闭issue都发布新版本。

Jon Calhoun: 我见过一些软件,它们会构建二进制文件,然后用这些二进制文件运行测试,模拟一些外部调用,比如Git调用。所以它们几乎是做了持续交付,只不过生成的二进制文件不会真正发布给用户。这是一种奇怪的中间状态,它们做了大部分CI/CD的事情,但不会每两小时发布一个新版本给用户,那样会很糟糕。但你仍然可以获得一些好处。然后最终每周一次,将所有测试通过的代码打包成一个最终的二进制文件发布给用户。

Jon Calhoun: 那么当我们讨论CI和CD时,通常的设置是什么样的?会使用哪些工具,这些工具为什么有用?

Jérôme Petazzoni: 我不确定是否真的有一个典型的设置。对我来说,核心概念是总有一个“管道”的概念,即使它不一定被叫做“管道”---但它是一系列操作的顺序。如果你查看Semaphore、Travis或Jenkins等工具的配置选项和运行方式,基本原理都是一样的。你先准备环境,运行一些操作,进行测试,可能还有一些版本的组合需要测试,收集所有日志,最后得到一个“通过”或“失败”的结果。

关于工具,我看到有一些“老牌”工具,比如Travis或Jenkins,一个是SaaS领域的代表,一个是本地部署的代表。然后随着新技术的兴起,特别是容器的出现,出现了很多新的工具。很多老牌平台最初并不支持容器,或者即使支持也不是一开始就支持,或者支持得不够优雅。这就给了很多新公司机会,它们说“我们从第一天起就支持容器和其他新技术,并且以一种对写Dockerfile并在容器中运行代码的人来说合理的方式支持它。”

所以关于工具,我会说有两个维度,一个是本地部署工具,一个是SaaS工具,虽然许多工具实际上在两者之间都有应用。我个人认为,可以看到一个不太明确的界线,那就是“容器前”和“容器后”的环境。这是非常明显的变化。

Jon Calhoun: 当我第一次接触CI时,使用的大多是像Travis这样的工具,它给人的感觉是你可以直接拿现有的项目,它会神奇地让它工作。然而现在,大多数新产品似乎必须支持容器,并且由于容器的普及,使用这些工具的一个好处是你可以自由选择适合你环境的工具组合。而在过去使用Travis时,大多数情况下它都能神奇地工作,但如果遇到问题,调试就会变得很麻烦,尤其是你需要在服务器上安装一些稀有的软件时。

因此,随着Docker和容器的普及,CI/CD生态系统是否因此发生了变化,在你看来是这样的吗?

Marko Anastasov: 当然,Docker对CI/CD领域产生了颠覆性的影响,因为它引入了一种全新的构建、测试和部署软件的抽象过程。通常,开发者之前不需要处理Docker代表的这些东西,所以对于所有CI来说,比如Semaphore是一个云端服务,这是我最熟悉的。

例如,早期的云端CI服务,如Travis或Semaphore,它们的工作流能力非常有限。基本上,你可以运行一个步骤序列,或许还有一个并行作业序列,但这就是全部。也许有些服务还会有一个独立的部署步骤,但有些甚至没有。

在Docker容器的案例中,即使你没有Jon描述的那种问题,比如你不需要为你的环境定义一个容器,但你需要构建容器,因为这是你要发布的东西。

当你开始构建时,你会构建一个容器,然后也许你有一个相对较大的测试套件,你想并行化它。理想情况下,你会构建一次容器,然后将它“扇出”(fan out)到几个并行的作业中,重复使用该容器,而不是重建五次,而是使用同一个容器五次。而这是早期版本的Semaphore无法做到的。你必须在所有并行作业中重建容器。如果你每天都在处理容器,这显然是不可接受的。那么突然之间,无论之前的CI工具对你有多好和有用,它突然就不再合适了。

但从CI提供商的角度来看,要实现这一新的场景以及其他一些相关的场景,可能不那么明显,但同样重要,需要大量的工作。我们这些做云端CI的人,不得不基本上重新发明我们的解决方案。或者不做,有些公司没有做,或者有些新玩家进入市场。这是行业中的一次重要变革。

Jon Calhoun: 那么当你谈论持续集成(CI)的运行时,你提到即使你不需要一个单独的环境,你也可以基本上把构建任务分散出去---为什么速度如此重要呢?我想这样问会更合适一些:我曾经在一些团队里,他们的持续集成反馈非常快,而在另一些团队里,持续集成是那种你推送代码后,要等15分钟再检查发生了什么的情况。那么你能谈谈这对开发者体验有什么影响吗?

Jérôme Petazzoni: 我觉得这回到了我之前提到的关于更快迭代,以及在一天内能够尝试和实验更多东西的观点。这有点像是追求最快的部署时间;我觉得这几乎就是Ellen Körbes的一个演讲标题的原话,她在Tilt[6]工作,她有一个非常棒的演讲,主题是从按下按钮到将代码运行在Kubernetes集群上的时间可以有多短。我想答案是可以缩短到大约四秒钟,或者类似的时间。当然,在这种情况下我们不是在谈CI;那是一个非常特殊的情况。但这正好说明了对速度的需求。

我认为对于我们编写的大多数代码来说,可能不需要这么快,因为我可以在本地测试。理想情况下,我只需要保存一次构建,然后我测试我的东西,它运行正常……但是如果我在做一些更复杂的事情,比如它与一个很难模拟的环境交互……比如说,你写了一个Kubernetes operator,因为这在最近非常流行,很多人都在做这个,所以你最终会用Go写你的东西,然后你需要在Kubernetes集群上运行它……尤其是在刚学习时---我最近才做过这个,老实说,这是一种你从文档和示例代码中拼凑东西的过程,结合你头脑中对其工作方式的想法……但很多时候,我只是把一行代码放在那里,老实说,我不知道它会做什么;我只是希望它能让我更接近我想要的结果,但我真的别无选择,只能尝试、测试并看看会发生什么。

在这种情况下,当然我不是在做CI,而是希望在某种CD(持续部署)中。如果我可以在本地工作,那很好。但如果我需要与一个有很多pod、容器和负载均衡器的大集群交互,那时我需要部署到也许不是真正的集群,但至少是足够真实的环境来进行所有测试……那时我希望它能快一点。因为再次强调,如果我正处于学习阶段,我可能会做出某些我们不应该做的事情,比如在代码开始处打印日志等等,但有时我们确实得退回到这种做法---那么在这种情况下,我希望构建和部署都能非常快。我愿意为了让它快一点而采取很多捷径,就像我之前举的例子一样。我还没有谈到CI,我只是处于学习阶段,我认为这也是现代CI和CD管道中的一个重要点;即“我们如何可以跳过某些部分?”或者“我们如何让这个过程既适用于本地开发实验,也能尽可能接近CI和CD的形式?”

我很多次都有这种需求。我之前提到的Tilt---它是一个在容器,特别是Kubernetes生态系统中填补了巨大空缺的工具,因为我们仍然没有像在Docker的Compose中那样拥有很好的Kubernetes开发者体验……所以当我看到这个工具Tilt时,我的反应是“哇,这真的很棒”,然后我开始使用它,几乎有点滥用了……然后我开始思考,“我用这个工具描述了我整个开发栈,但现在我想把它转变为一个部署工具。我是否需要重新开始?”结果发现其他人也有类似的想法,我意识到,即使它最初只是一个开发工具,后来有人添加了一些CI命令,所以你可以基本上告诉它,“好吧,现在不要只是启动我所有的服务和容器等,然后在这个开发周期中迭代、修改代码、保存等”,现在你可以更多地以CI的思维方式工作,你运行这个工具来启动所有东西,可能再运行测试,然后关闭所有东西。我认为在这个领域会有很多发展,因为我们有很棒的CI工具、很棒的CD工具、很棒的本地开发工具、很多很棒的工具……但越来越多的需求是工具能够同时干两件事---既能跳萨尔萨舞[7]也能跳探戈,而不仅仅是其中之一。

Jon Calhoun: 我有一个问题是---大多数时候我们谈论CI/CD时,我们假设这是可以在本地运行的东西,然后我们可以部署它,看看它作为发布产品时的表现。但你提到了开发者速度和其他一些不同的使用场景……我一直有个疑问是:“有没有可能CI/CD几乎取代了某人本地运行的需求,如果我们的反馈循环足够快?”有一个例子让我想到这个问题---在之前的一期节目中,我们和Play With Go的创建者聊过,这个项目好像是从Play With Docker衍生出来的,而我相信你很熟悉Play With Docker,Jérôme……

Jérôme Petazzoni: 是的。

Jon Calhoun: 我不记得你是不是这个项目的创建者……是这样吗?

Jérôme Petazzoni: 好吧,它是由两位Docker Captain创建的,我不想念错名字,所以不打算说他们的名字……但他们是Marcos和Jonathan……我在某些时候帮过一点忙,但主要是通过加油和鼓励他们,因为我觉得他们做的东西真的很棒,当时很多类似的工具如雨后春笋般涌现……所以是的,我明白你的意思。

Jon Calhoun:  是的,我在想Play With Go版本---至少它使用了Qlang和其他一些东西,所以当你在写一篇指南时,它会构建并推送这些内容。但至少在目前的状态下,实际编写指南意味着你必须拉取整个项目,在本地运行它,并在本地运行所有脚本……而如果你想降低入门门槛,理想的情况是有人可以直接写脚本,然后有某种CI/CD管道输出一些东西,告诉他们“嗯,这大致上是它的样子”。也许不是完美的,但它允许他们跳过那种---你知道的,我只想写一篇两页的指南,我并不想搞懂如何安装整个系统并设置它。

Jérôme Petazzoni: 是的,完全同意。某种程度上,容器使得在“普通代码”之间做到这一点变得容易,但如果我的代码正在与容器进行交互,那么我如何将它本身放入容器中呢?所以我们有了像Docker-in-Docker[8]这样的项目,类似的东西……或者最近我看到的另一个项目,我认为目前它还没有引起足够的关注,但当人们看到它的功能时,它肯定会火起来……这个项目叫Sysbox[9],它基本上---简化来说,它允许你运行等同于特权容器的东西,但相对安全,至少是更安全,这意味着所有像Docker-in-Docker或Kubernetes-in-Docker等的工作负载,通常你会认为“哦,我需要一个虚拟机”,这些东西现在可以在容器中运行了,这将使很多事情变得可行……就像我之前说的,几年前你可能会说“我做不了这个,因为那看起来不可能”,然后今天,随着新工具、新功能的出现,某些令人惊喜的特性解锁了一些非常有趣的用例。所以,是的,CI和开发---我认为这两者会越来越紧密结合。

Marko Anastasov: 我想补充一下Jon最初的问题---我认为大型Web应用程序,随着时间的推移会发展出庞大的测试套件。你会有很多单元测试,可能在本地运行并不复杂,但通常端到端测试或验收测试是更耗资源的……根据我们的内部经验,以及很多Semaphore用户的反馈,我看到如果你正在开发某种SaaS产品,开发人员通常不会在本地运行整个测试套件;他们只是推送到CI系统,在特性分支上进行测试……因为在CI中,他们可以有非常精细的并行化和优化。如果他们在本地顺序运行所有测试,总时间可能会超过一个小时……但在CI中,他们实际上将时间缩短到了大约10分钟。所以推送代码并等待反馈更方便。

Jon Calhoun: 这也挺好,因为在这种情况下你可以推送代码然后继续工作……而本地运行的话,你至少得开一个新标签页来让它运行,可能还会拖慢你的电脑,取决于你的开发环境。因为我知道有些人使用Chromebook之类的设备,有时运行起来比较困难。

想问一个相关问题,再回到工具选择上---如果你今天要选择工具……假设你有一个Web应用程序---我觉得很多听众都在构建Web应用程序或类似的东西---你想开始使用持续集成/持续交付,你会如何选择工具?如果他们刚开始,想要得到最大收益,你认为他们应该如何思考这个过程?

Jérôme Petazzoni:  这是个好问题。对我来说,我的个人方法是尽量选择最简单但能完成任务的工具。不太简单,否则我做不了我想做的事,但也不要太复杂,因为很容易陷入复杂性的陷阱。

例如,我见过很多人选择Kubernetes或Docker,仅仅因为他们觉得这是应该做的事情,就像是一种时尚,然而当我们查看他们实际运行的内容时,“哦,我们只是有一些Go微服务”,或者也许只是Python。然后当我们仔细看的时候,我们会想“你真的需要Kubernetes、Docker或其他东西吗?”因为也许你不需要那种额外的复杂性。在这种情况下,我很乐意不使用它们。如果只是一些Go构建任务,那实在是很难变得更简单。

所以我不会推荐具体的工具;我不会告诉你“哦,你绝对应该用这个或那个”,而是建议你思考“什么是最简单的、能满足我需求的工具”,并尽量避免复杂化。

Jon Calhoun: Marko,我猜你可能更有偏向。

Marko Anastasov: 是的……

Jon Calhoun: 也许我错了,但……你怎么看Semaphore在其中的定位?你觉得哪些场景最适合人们去尝试使用Semaphore?

Marko Anastasov: 是的。我可能会补充一点Jérôme的观点---如果你是这个领域的新手,可能甚至不需要首先考虑CI和CD。也许先花时间学习测试驱动开发(TDD)。这将提升你设计代码和思考系统的能力,帮助你写出更干净的代码。如果你大体上掌握了这些,那么只要确保你运行测试或从头构建应用程序的方式非常简单。理想情况下,只需要一个命令。如果你能做到这一点,不泄露任何复杂性,而是保持简单,那么选择工具会变得非常容易---你可以在一个下午内搞定。

我可以分享一下公司在评估选择时的思路……通常,他们会看他们今天在构建什么,他们的系统的技术需求是什么,大多数Semaphore的客户都在构建某种SaaS,或者他们是某种技术公司。他们通常有一个相对较大的代码库,在这种情况下,他们从Semaphore中受益最大,因为Semaphore是最快的云端CI服务,任何人都可以查证这一点。

通常这些公司有不同的团队,可能他们在构建移动应用……你知道的,取决于他们使用的框架和语言;一旦你把所有这些都列出来,通常会有一些边缘情况,突然间并不是每个工具都合适。你还需要弄清楚“你能使用基于云的服务吗?”你能把整个过程外包出去,还是有些因素迫使你自己动手?这是一个重要的分叉点。

一旦你通过了这些,如果还有多个选项可选,我会评估用户体验。它是否对开发人员足够友好?或者它是否更像是强迫你需要一个“魔法人物”或团队来处理管道……我认为这并不太好;我认为开发人员应该拥有项目的管道,拥有完全的自主权……然后就是性能。如果有差异---有时云服务之间的差异可以达到2倍,所以如果你在15分钟还是30分钟内得到反馈,这非常重要。

Jon Calhoun:  如果你在等着看某个东西是否工作,那15分钟和30分钟的差异确实非常大。作为开发人员,我能想象这会如何影响生产力,可能会让你的效率提高2倍甚至3倍。

Marko Anastasov: 是的。

Jon Calhoun: Marko,你提到如果你提前做好准备---基本上设置好应用程序,因此你有测试,它们运行起来相对简单……在开始使用CI/CD时,还有哪些常见的陷阱或错误,会导致问题?

Marko Anastasov: 好吧,也许那些以前没有实践过CI的人会做的一件事是---他们在非常长时间的分支上工作;因此他们在特性分支上积累了大量的更改,这使得集成变得更加困难。这是应该避免的。

在对话中,我会使用“特性分支”这个术语,但……我不知道。对我来说,特性分支是你做一个git checkout,然后可能一个小时后就要合并的东西,而不是一个月后。所以你要确保你在小批量的更改中工作;你基本上可以通过简单的if语句隐藏未完成的功能,然后继续逐步合并。我们之前谈到过要避免不必要的复杂性,Jérôme也提到过这个……

Jon Calhoun: 特性分支确实是一个应该牢记的好点子,因为我和你有同样的想法,即使你要在一个特性分支上工作超过一小时,我也尽量让它保持为单一提交,描述所有完成的工作。如果你写了太多代码,那可能就是在说明你在特性分支上工作得太久了。而这并不意味着在开发过程中它会一直保持为一个提交,因为有时我只是想保存我的工作,或者其他……但最终,我会压缩整个分支并合并它,所以我希望它最终成为一个提交,描述一个小特性或者某个特性的部分。

Marko Anastasov: 不稳定的测试……

Jon Calhoun:哦,不稳定的测试?我正想说,这正是我见过最多的情况。CI 对我来说变得没用的原因是,当我在一个项目上工作时,我们会实际部署,但可能有 50% 的时间 CI 会失败……在那时,它不再是有用的反馈,因为你无法分辨“嗯,是出问题了,还是只是一个测试没有正确运行?”这让 CI 变得有点奇怪---你等待十分钟得到反馈,然后想“我现在只需要再运行一次测试,看看它是否真的坏了”。而当我们谈论速度时,这意味着你的一半测试可能需要 20 分钟,因为你要检查它到底对不对。

Jérôme Petazzoni:是的,我们谈论监控和可观测性时也讨论过类似的事情,尤其是当监控系统半夜提醒你时……如果它是一个误报,那就太糟糕了。首先,被机器在半夜提醒已经很糟糕了,尤其是当你知道有一半的几率,甚至只有 10% 的时候,它是误报……这就像狼来了的故事一样,因为监控系统经常骚扰你,所以当它真的重要时,你可能根本不在意。

我觉得你提到的这个测试场景也类似。你提到的行为是负责任的,因为你会“我再运行一次测试”,但有些人可能会想“如果测试不可信,我就不再理会它了”。在这种情况下,我们需要修复这个测试。

还有一点---我非常支持开发人员掌控 CI 和相关流程。不过,我也非常支持短期内引入一些专家团队来帮助你了解需要做什么,如何设置,然后快速向开发人员解释“这是你们如何实现自主的方式”。我曾经为容器项目做过几次这种事情,因为这些生态系统真的太大了。理想情况下,在最好的世界里,我们可以自己做研究并选择解决方案。但有时如果有人能坐下来,听你说你在用什么,看看你试图运行的代码,然后告诉你“我至少可以帮你缩小范围到这个、这个和那个”,那会非常有帮助。如果他们为你做了这些工作,接下来他们可以让你有能力去维护它。

说到我知道的东西---是的,从头开始写第一个 Dockerfile 可能非常困难,特别是如果你想把所有多阶段构建和其他复杂功能都做好。然而,一旦你有了那个 Dockerfile,添加一个额外的依赖项或者改动什么就会简单得多。所以这里有点两面性。

Kris Brandow:我有一个问题,可能与 CI/CD 相关,关于构建系统的。在什么情况下你觉得应该引入比 makefile 或 shell 脚本更好的东西,比如 Bazel、Pants[10](译者注: Python开发的工具) 或 Buck[11] (译者注:meta用Java开发的工具,目前已经归档)等等……这似乎与 CI/CD 管道非常相关。

Jérôme Petazzoni:是的,这超级相关,我很喜欢你提到 Bazel,因为我有个朋友帮我理解了 Bazel 的作用是什么。从外面看,我看到了一些关于容器的例子,因为过去几年我做的全是容器相关的事情……我一直不明白“为什么要为容器使用 Bazel?这看起来很复杂”。然后我的朋友基本上解释说“嗯,如果你有一个 100 到 200 人的团队在不断发布代码,并且你有一个不断增长的测试套件,每次你在代码库的前端修改一行小小的代码,你就需要重新运行所有东西,这种复杂性很快就会爆炸……也许不是指数级,但至少不再是线性增长了。”

很快你就会发现你的测试套件从最开始的几分钟,变成几个小时,然后突然变成几天,你会想“我们不能再这样做了”。而使用 Bazel,你可以以非常优雅的方式表达依赖关系。

对我来说,理解这一点很重要。像 make 和 makefiles 这样的东西可以帮助我只重建我需要的部分,而像 Bazel 这样的工具不仅可以只构建我需要的部分,还可以只测试我需要的部分,只生成我需要的工件等等。我可以把那些令人难以忍受的测试时间缩短到合理的范围,开发人员可以重新在几分钟内看到结果,而不是几天。

当然,工具的复杂性是另一面。我朋友的情况基本上是有一个全职工程师在维护他们的 Bazel 构建系统---如果你谈论的是数百名工程师在后面发布代码,这很合理,因为工具对开发至关重要……但我也见过另一种极端情况,有些人甚至不能轻松编写 Dockerfiles,然后出现了一个人,他带着 Bazel 来了,说“哦,这太棒了,我要把 Bazel 文件放在每个地方”,结果没有人能理解或维护它,事情变得一团糟,因为人们只是运行它然后祈祷,而当他们需要调整某些东西时,就变得非常复杂。

但是的,这是一种连续体。从 makefile、Bazel、容器,到我们现在拥有的所有容器构建系统……虽然我一直在谈论 Dockerfile 等等,但我们现在还有其他东西,所以它们是交织在一起的。

Marko Anastasov:是的,我没有使用 Bazel 的经验;我们还在用 make……

Jon Calhoun:听起来这是一种当你需要更好的工具时,它会变得显而易见的情况,如果事情变得太慢……

Marko Anastasov:是的……

Jon Calhoun:我个人也还没遇到过这样的情况,所以对此很感激……但同时,知道有可用的工具是一件好事。

Marko Anastasov:我想说关于不稳定的测试---我认为大多数人不知道的是,作为一个 CI 提供商,我能看到基本上每个组织都有不稳定的测试,而人们通常为此感到羞愧……所以我只是想告诉大家,你们绝对不是孤军奋战。这只是工作的一部分,复杂性的一部分,关键是你如何处理它……我绝对鼓励人们花点时间来维护他们的测试,就像维护代码一样。测试需要维护和一些打磨。

Jon Calhoun:这确实是值得记住的事情……我认为你可能是对的,我从未见过一个组织最终不引入不稳定的测试。现在,他们可能会更快地移除它,但我认为它们确实会随着时间的推移被引入。

Marko Anastasov:是的。

Jon Calhoun:好吧,我要为大家播放介绍部分了,然后我们可以进入你们的不同观点。

Jon Calhoun: 好的,Jérôme,Marko,你们有没有什么不太受欢迎的观点想要分享?每次我们做这个环节时,Jerod 通常会把你们的观点发在 Twitter 上,进行一个小投票,看看 @GoTimeFM 的粉丝怎么看。我得提醒你们,大多数的听众都是 Go 开发者,所以有时候一些观点在整体上可能不流行,但在这里未必不受欢迎……不过,即使你的观点不算不受欢迎也没关系,我们只是对一些与常规不同的意见感兴趣。

Jérôme Petazzoni:  我的观点是,我们应该停止坚持所有更新等都必须通过 HTTPS 分发。我经常说这个的时候,我所有的安全朋友,甚至不是朋友的人都会说,“不,你不知道你在说什么。这很重要,因为有各种攻击……”然后我解释说,“不,不,不……当然,可以通过 HTTPS 分发元数据,比如包列表、版本、校验和等等。但大部分的实际数据---你可以通过 HTTP、FTP 等方式分发。”因为通过 HTTPS 分发成本很高,不是因为 TLS (传输层安全协议)复杂,而是因为如果你使用 HTTP 或 FTP,你可以让全世界来镜像你的东西。这就是 Debian 和 Slackware 等发行版几十年来用很少的预算运行的方式。

如果你看 Docker Hub---我不会告诉你我在 Docker 工作时的具体数字,因为即使我知道也记不清了……不过根据今年年初 Docker 公布的一些数据,他们说 Docker Hub 上有大约 15 PB(拍字节)的镜像。存储这些数据在 S3 上每月至少要花费 30 万美元,还不包括数据传输费用。传输方面---我根据 Docker 今年年初公布的数字,他们每月有大约 80 亿次拉取请求。我假设每次拉取的平均大小是 10 MB(实际上可能更大),那么每个月的传输费用大约是 400 万美元。仅仅是维持 Docker Hub 的运营,这些估计已经很乐观了。

如果这些大数据可以通过普通的 HTTP、FTP 等方式轻松镜像,只保留元数据通过 TLS 传输,或者可能为一些少见的攻击场景保留一个使用 TLS 的原始副本,我不敢说这会改变 Docker 的命运,但我很好奇如果我们一开始这样做,世界会是什么样子。一个类似 Docker Hub 的服务能够不需要每月花费数百万、数千万美元来运行。

Jon Calhoun: 你觉得这会节省多少成本呢?你认为能节省一半吗,还是更多?

Jérôme Petazzoni: 哦,我认为至少可以节省 99% 的成本,听起来可能有点不可思议,但如果你看看 Linux 发行版,比如 Debian、Slackware、Arch Linux等,几乎没有像“Debian 公司”或“Arch Linux LLC”这样的机构来支付所有的镜像费用。这些镜像通常由公司、大学、实验室、ISP 等自愿维护,因为他们觉得这是公共利益的一部分,是一种公共资源。

我以前在法国运营过一家托管公司,当时我们也维护了一些镜像,首先是为了自己的方便,因为在部署机器时,网络内有个镜像源是非常方便的。此外,公开提供这些资源对其他人也有好处。

所以最终,我认为这种做法可以将成本削减 100 倍甚至 1000 倍。

Marko Anastasov: 我觉得这是一个非常重要的信息,特别是对于那些希望建立下一个类似社区骨干公司的人来说。

Jérôme Petazzoni: 是的。我也在想 npm,不知道他们的成本是多少,但我不敢去想。

Marko Anastasov: 是啊。我还记得大学时下载 Gentoo Linux,通常会选择从我所在大学的镜像下载……不过现在我想大多数人都有更快的网络了。但即便如此,我觉得每个组织都希望从最近的源下载数据。这不仅仅是预算问题,下载速度也会更快、更方便。

Jon Calhoun:  我可以肯定地说,当我在一些公司工作时,他们会镜像某些内容,你可以明显感觉到哪些内容是镜像的,哪些不是,因为速度差别非常大。

Jérôme Petazzoni: 是的。所以,如果当时 Docker 能允许数据通过 HTTP 透明地镜像,我觉得情况会好很多。我真希望当时我能说服我的同事们,让数据部分通过 HTTP 分发,而不是 HTTPS。

Jon Calhoun: 这是不是他们最近做出更改的原因?我猜就是因为成本问题,所以现在下载超过 200 次后必须登录?

Jérôme Petazzoni: 是的,我猜一定是因为成本太高了……特别是我们这些在 CI(持续集成)领域的人也有责任。每次我设置流水线时,我都注意到我每次都会从 Docker Hub 拉取镜像,我会想“有没有办法避免这样做?” 但其实很复杂。

我还记得以前参加过 Linux 安装派对,和一群技术宅朋友一起安装 Linux,很有趣!当时我设置了一个透明代理,所有人都可以通过这个代理拉取包,配置起来非常简单。试着为 Docker Hub 做同样的事吧,你做不到,因为它是通过 HTTPS 传输的。当然,你可以这么做,但操作起来非常麻烦。你需要设置一个透明的 TLS 代理,插入证书,而这会使你通过 TLS 获得的安全性付诸东流,因为你在添加这个缓存代理的后门。所以,这其实挺麻烦的。

Kris Brandow: 这让我想起模块化的中间道路,它保留了安全性,但也允许分发。你觉得这种方式合适吗,还是你认为应该坚持使用 HTTP?

Jérôme Petazzoni: 我想这可能也是一个大小问题;对于容器镜像来说,这个问题被放大了。因为很容易出现 4 GB 的容器镜像,而你甚至还没开始放代码。然后你会有一个流水线,它每次都要下载这 4 GB 的数据,因为流程就是这样。而当没有人为此支付费用时,也没有人有动力去优化它。主要的动力可能是“嗯,也许我可以减少镜像的大小,这样流水线会跑得更快……”

但最终,总有人在为这些买单,而在这种情况下,我猜这个人就是 Docker。

Jon Calhoun: Marko,你有不太受欢迎的观点想分享吗?

Marko Anastasov: 是的,我有一个观点,和今天的主题相关,虽然在写小型 Go 服务时不一定会遇到……我的观点是,如果反馈时间超过 10 分钟,那就不是合格的持续集成了。这其实是在为“什么是足够好”画一条线。

我的意思是,如果作为开发人员,你在等待过程中不会完全失去专注力,那就足够好了。而这个时间大概在 10 分钟左右。基本上,如果你等待的时间超过这个……也许你还能保持专注 15 分钟,但如果再长一些……就真的很糟糕。从开发人员的角度来看,就好像有人把我的键盘抢走了,我没法继续工作,这种感觉非常糟糕。

Jon Calhoun: 大概就是去泡杯咖啡或茶的时间。如果等你回来时还没完成,那就有问题了。

Marko Anastasov: 对,没错。

Jérôme Petazzoni: 是的。

Jon Calhoun: 我觉得这很有道理。向非开发人员解释等待半小时后再回到之前的工作有多让人分心确实有点难……我猜大多数开发人员都曾试图向别人解释这种痛苦,但这是一个真实的痛点。如果等待太久,保持专注真的很困难。

Marko Anastasov: 是的。你可以这样向非开发人员解释:假设等待时间是一个小时,而我们有 12 个人在一个项目上工作……我们每天最多有 8 小时工作时间。这样算下来,理论上我们一天内就不可能所有人都能提交和合并代码。想想这对我们一起工作的影响,还有我们多频繁地进行检查和提交……很快你就会遇到很大的限制。或者你有不稳定的测试,如我们之前提到的,你需要重新运行测试,但其他两个人也在重跑主干分支上的测试,到了下午 3 点,你可能觉得不如干脆提前下班。

Jon Calhoun: 在你描述的情景中,甚至可能会出现第二天早上代码还在跑的情况,这就更糟糕了……因为一旦有人提交了新的代码,你就必须重新测试最新的提交,无法并行化处理,这使得问题更加复杂。

Jérôme Petazzoni: 也许这就是为什么有时候可以选择绕过一些步骤……我在想,如果你在特性分支上添加了新的提交,可能最好取消之前计划在该分支上执行的任务。每次我们在工具上取得进展时,比如引入不同版本的矩阵等功能,我们总能想到一些新的功能,在现有的基础上构建更高层次的东西。

我不知道 10 分钟反馈时间这个观点是否真的不受欢迎,还是因为很难做到,所以人们觉得“不,我不想承诺这个,因为这太难了。”

Marko Anastasov: 是的,这里面可能有很多因素。当我谈论这个话题时,人们通常会有点防御性,说“哦,你不知道我的代码。这是必须的。”

Jon Calhoun: 这就是那种理论上大家都喜欢的东西,但在实践中没有人愿意真正付出努力去确保它实现。

Jérôme Petazzoni: 完全同意。

Jon Calhoun: 我猜 Marko 的意思是,这件事应该足够重要,值得你去确保它真正实现。

Marko Anastasov: 是的,是的。不过如果有合适的工具,可能会让这变得更容易。你不需要立即运行所有测试。你的工具应该让你先运行单元测试,然后高效地进行端到端测试……因为如果你的单元测试出问题,那可能是一个足够基础的问题,端到端测试的结果就没那么重要。所以有些优化可以做……或者如果你有多个项目在同一个代码库中,工具应该让你指定“如果这个目录发生了变化,就执行这个操作,但别做其他事情。”

Kris Brandow: 我觉得这也涉及到代码维护的问题。通常当你的 CI 流水线变得越来越慢,比如 20 分钟甚至一个小时,问题通常是“你没有设计好测试的并行化”,甚至是单元测试的并行化。我也有过这样的经验,就是在写测试时,我没有考虑到并行化问题,结果所有测试都必须顺序执行。然后我可能想“我可以花 10 分钟去修复它,但我现在不想做”,于是几个月后,所有的东西都围绕这种顺序执行的概念构建,现在要移除这个全局状态变成了一个巨大的项目,所以我不太想做,只好继续忍受……而我本可以花 10 到 20 分钟避免这个问题的。这总让我想起那些滑坡效应;一开始的一个小决策最终会带来巨大的麻烦。

Jon Calhoun: 有些问题确实很难避免。我可以举个例子,如果你想在测试中使用真实的数据库,你需要启动一个数据库实例。启动一个 Postgres 数据库进行测试非常容易,但你可能不想同时运行六个测试,因为它们可能会相互干扰。所以很容易就会想,“好吧,我们只用一个数据库。启动四个会有点麻烦,不如不这样做。”

不过有些工具---我记得 Dockertest 可以帮助解决这个问题,好像它可以启动多个 Postgres 副本。我得去确认一下,但我不太记得了。

Jérôme Petazzoni: 这曾是我早期 Docker 演示中的一个环节。我会加载数据到 Postgres 数据库中,然后通过 Docker commit 创建多个容器,它们都有相同的数据。这在演示中看起来很酷……但这也让信息变得有点混乱,因为你不会想把数据库数据直接打包在容器镜像中,除非是这种场景……不过,确实有一些有趣的应用场景。

Jon Calhoun: 好的,Jérôme,Marko,谢谢你们的分享。和你们一起讨论 CI 和 CD 很有收获。希望所有在听的朋友都学到了很多。我们下次 Go Time 再见。

参考资料
[1]

#162 We're talkin' CI/CD: https://changelog.com/gotime/162

[2]

《CI/CD with Docker and Kubernetes》: https://semaphoreci.com/wp-content/uploads/2020/05/CICD_with_Docker_Kubernetes_Semaphore.pdf

[3]

Semaphore: https://semaphoreci.com/

[4]

kind: https://kind.sigs.k8s.io/

[5]

Verilog: https://www.runoob.com/w3cnote/verilog-tutorial.html

[6]

Tilt: https://tilt.dev/

[7]

萨尔萨舞: https://baike.baidu.com/item/%E8%90%A8%E5%B0%94%E8%90%A8%E8%88%9E/1452106

[8]

Docker-in-Docker: https://github.com/jpetazzo/dind

[9]

Sysbox: https://github.com/nestybox/sysbox

[10]

Pants: https://github.com/pantsbuild/pants

[11]

Buck: https://github.com/facebook/buck


旅途散记
记录技术提升