现代-DevOps-实现指南-全-

现代 DevOps 实现指南(全)

原文:annas-archive.org/md5/504ac02bda9c6c4c7655551815fb7bc6

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

DevOps 是快速高效地部署软件的最新革命。通过一套自动化工具、编排平台和一些流程,公司可以加速其 IT 系统的发布周期,使工程师能够在业务流程中更有效地工作。

本书内容涵盖

第一章,实际世界中的 DevOps,展示了 DevOps 在当前 IT 公司工程部门中的位置,以及如何调整资源以最大化交付潜力。

第二章,云数据中心,比较了不同的云解决方案,用于管理云上的资源(虚拟机、网络、磁盘等)。

第三章,Docker,教授关于 Docker 及其内部工作原理的知识,以便更好地理解容器化技术的运作方式。

第四章,持续集成,讨论了可以用来执行测试以及其他许多操作的持续集成技术,正如我们将在第八章,发布管理 - 持续交付中看到的。

第五章,基础设施即代码,展示了如何以可编程的方式描述我们的基础设施,并应用软件开发生命周期的最佳实践来确保其完整性。

第六章,服务器配置,展示了如何使用 Ansible 来管理远程服务器的配置,以便简化大量服务器的维护工作,即使我们将重点放在 Kubernetes 上,这也是值得了解的。

第七章,Docker Swarm 和 Kubernetes - 集群基础设施,简要介绍了 Docker Swarm,然后引导您关注 Kubernetes,这是最先进的容器编排技术,被世界上最大的公司如 Google 广泛使用。

第八章,发布管理 - 持续交付,展示如何在 Google Cloud Platform 上使用 Kubernetes 和 Jenkins 设置持续交付管道。

第九章,监控,展示了如何监控我们的软件和服务器,以便能够在潜在故障发生前非常快速地发现并修复(可能)而不会影响我们的客户。

您需要准备什么

为了能够跟随本书及其内容,你需要在 Google Cloud Platform 上注册一个试用账户,并安装一个编辑器(我使用的是 Atom,但任何其他编辑器也可以),以及在本地机器上安装 Node.js。你还需要在本地安装 Docker,以便测试不同的示例。我们将在 Kubernetes 示例中使用Google 容器引擎GKE),但如果你想在本地玩 Kubernetes,也可以使用 Minikube,尽管你需要一台相当强大的计算机。

本书适用对象

本书适合那些希望在 DevOps 领域提升技能的工程师,特别是那些想要精通 Kubernetes 和容器的读者。中级技能的读者最为理想。

约定

本书中,你会看到多种文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄以如下方式显示:“下一行代码会读取链接并将其分配给BeautifulSoup函数。” 一段代码块如下所示:

resource "google_compute_address" "my-first-ip" {
 name = "static-ip-address"
}

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

resource "google_compute_address" "my-first-ip" {
 name = "static-ip-address"
}

任何命令行输入或输出都写作如下:

docker commit 329b2f9332d5 my-ubuntu

新术语重要词汇以粗体显示。你在屏幕上看到的词语,例如在菜单或对话框中,文本中会像这样显示:“为了下载新模块,我们将进入 Files | Settings | Project Name | Project Interpreter。”

警告或重要提示以这种方式显示。

提示和技巧以这种方式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对本书的看法——你喜欢或不喜欢的部分。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中受益的书籍。如果你有专业知识并且有兴趣编写或参与编写一本书,请查阅我们的作者指南:www.packtpub.com/authors

客户支持

现在你是 Packt 书籍的骄傲拥有者,我们提供了许多帮助你最大限度利用这本书的资源。

下载示例代码

你可以从你的帐户中下载本书的示例代码文件,网址为:www.packtpub.com。如果你在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件会直接通过电子邮件发送给你。你可以按照以下步骤下载代码文件:

  1. 使用你的电子邮件地址和密码登录或注册我们的官网。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的渠道。

  7. 点击“代码下载”。

文件下载完成后,请确保使用最新版本的以下工具解压或解压文件夹:

  • Windows 版的 WinRAR / 7-Zip

  • Mac 版的 Zipeg / iZip / UnRarX

  • Linux 版的 7-Zip / PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Implementing-Modern-DevOps。我们还提供了其他书籍和视频的代码包,您可以在github.com/PacktPublishing/找到它们。快来看看吧!

下载本书的彩色图片。

我们还为您提供了包含本书中使用的截图/图表的彩色图片的 PDF 文件。这些彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/ImplementingModernDevOps_ColorImages.pdf下载该文件。

勘误表

尽管我们已尽最大努力确保内容的准确性,但错误仍然会发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激您向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择您的书籍,点击勘误提交表格链接,填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误内容将被上传到我们的网站或添加到该书籍的勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书籍名称。所需的信息将在勘误部分显示。

盗版

网络盗版问题在所有媒体上都是一个持续的难题。在 Packt,我们非常重视对版权和许可证的保护。如果您在互联网上发现任何非法的我们作品的副本,请立即提供其位置地址或网站名称,以便我们采取措施。请通过copyright@packtpub.com与我们联系,并提供可疑盗版材料的链接。

我们感谢您帮助我们保护作者权益,并确保我们能够为您提供有价值的内容。

问题

如果您在本书的任何方面遇到问题,欢迎通过questions@packtpub.com联系我们,我们会尽力解决问题。

第一章:现实世界中的 DevOps

在过去的几年里,软件行业发展迅速。我最喜欢的演变例子之一是金融科技(FinTech),这是一个新的领域,其名称源于金融和技术的融合。在这个领域,企业往往以颠覆性的方式构建金融产品,直到威胁到大型传统银行,甚至将它们置于危险之中。

这种情况主要是由于大公司失去了在 IT 系统中保持成本效益的能力,而银行恰恰是这些大公司之一。银行仍然使用 IBM 大型机运行系统,并且不愿意迁移到云端,这一点并不奇怪;同样,银行的核心组件依然是自 90 年代以来没有更新过的 COBOL 应用程序,这也并不奇怪。如果不是因为只有少数具备 AWS 或 Google Cloud Platform 账户的才华横溢的工程师,实际上可以构建出能够替代某些银行产品的服务,比如货币兑换甚至是经纪服务,那这一切或许还不会那么糟糕。

这在过去几年中已成为一种常态,FinTech 中小公司成功的部分原因归功于 DevOps,另一个部分则是其规模。通常,大公司会随着时间推移将 IT 系统商品化,外包给第三方,这些第三方专注于价格,忽视了质量。这是一个非常有效的削减成本的措施,但也有一个副作用:你失去了快速交付价值的能力。

在这一章中,我们将把 DevOps 放到一个更广泛的视角来看,并探索它如何帮助我们创造成本效益高的工作单元,从而在极短的时间内提供大量价值。

什么是 DevOps?

亨利·福特(Ford 公司创始人)有一句著名的名言:

“如果我问人们他们想要什么,他们会说更快的马。”

这就是传统系统管理员角色的变化:人们在尝试解决错误的问题。

所谓“错误的问题”,是指缺乏合适的工具来自动化生产系统中的干预,避免人为错误(这种错误比你想象的还要常见),并导致公司流程中的沟通不连续。

最初,DevOps 是开发、运维和 QA 的交集。DevOps 工程师应该做所有事情,完全参与软件开发生命周期(SDLC),解决传统发布管理中的沟通问题。这是理想的状态,在我看来,这正是全栈工程师应当做的:端到端的软件开发,从需求捕获到部署和维护。

如今,这一定义已经被扭曲到了一个地步,DevOps 工程师基本上是一个使用一套工具来自动化任何公司基础设施的系统工程师。这种 DevOps 的定义并没有错,但请记住,我们正在失去一个非常有竞争力的优势:对系统的端到端视角。一般来说,我不会称这个角色为 DevOps 工程师,而是站点可靠性工程师SRE)。这是谷歌几年前提出的一个术语,因为有时候(尤其是在大公司中),无法为一个工程师提供执行 DevOps 所需的访问权限。我们将在下一节中讨论这个角色,即 SRE 模型。

在我看来,DevOps 更像是一种理念,而不是一套工具或流程:让工程师接触到产品的完整生命周期需要很大的纪律性,但也能让你对所构建的内容拥有巨大的控制力。如果工程师理解问题,他们就能解决它;这正是他们擅长的。

DevOps 的起源

在过去的几年里,我们经历了 IT 行业的革命:它从纯粹的 IT 公司蔓延到各行各业:零售、银行、金融等等。这催生了一批小公司,称为初创企业,基本上是一些有创意的个人,他们付诸实践,并走向市场,通常是为了将产品或服务销售给全球市场。像亚马逊、阿里巴巴,更不用说谷歌、苹果、Stripe 甚至 Spotify,已经从创始人车库中的小公司发展为雇佣数千人的大公司。

这些公司最初的共同点一直是企业效率低下:公司越大,完成简单任务所需的时间就越长

企业效率低下图表示例

这种现象创造了一个独立的市场,需求无法通过传统产品满足。为了提供更敏捷的服务,这些初创公司需要具备成本效益。大银行可以在其货币兑换平台上花费数百万,但如果你是一家正在奋斗的小公司,你与大银行竞争的唯一方式就是通过自动化和更好的流程来降低成本。对于小公司来说,这促使它们采用更好的做事方式,因为每一天的过去都意味着离现金耗尽更近,但更大的驱动力是采用 DevOps 工具:失败。

失败是任何系统发展的自然因素。无论我们投入多少努力,失败总是存在,在某个时刻,它总会发生。

通常,公司非常关注消除失败,但有一条不成文的规则却阻碍了它们的成功:80/20 法则:

  • 你只需要 20%的时间就能实现 80%的目标。剩下的 20%将占用你 80%的时间。

在避免失败上花费大量时间注定会失败,但幸运的是,另一个解决方案是:快速恢复。

到目前为止,在我的工作经验中,我只见过一家公司问“如果凌晨 4 点发生故障,我们该怎么办?”而不是“我们还能做些什么来防止系统崩溃?”,相信我,创建一个恢复系统要比确保我们的系统不会宕机容易得多(特别是有了现代工具)。

所有这些事件(自动化和故障管理)推动了现代自动化工具的发展,使我们的工程师能够:

  • 自动化基础设施和软件

  • 快速从错误中恢复

DevOps 和企业

DevOps 完美契合了小型公司(初创公司)的世界:一些可以访问一切并执行所需命令的个人,能够快速进行系统更改。在这些生态系统中,DevOps 闪耀着光芒。

在大型公司的传统开发模型中,这种访问级别是不可行的。如果您的系统处理的是高度机密的数据,这甚至可能成为法律上的障碍,因为您需要从政府那里为员工获取安全许可才能授予他们对数据的访问权限。

对公司来说,保持一个传统的开发团队将产品交付给运行该产品的工程师,并与开发人员密切合作,以确保沟通不成问题,也会是一个方便的选择。

SRE 也使用 DevOps 工具,但通常他们更多地关注构建和运行一个中间件集群(如 Kubernetes、Docker Swarm 等),为开发人员提供一致性和共同语言,使他们能够抽象化基础设施:他们甚至不需要知道集群部署在哪些硬件上;他们只需要为将要在集群中部署的应用程序创建描述符,并以访问控制和自动化的方式进行,以确保遵循安全政策。

SRE 是一个独立的学科,Google 已经发布了一本免费的电子书,您可以在landing.google.com/sre/book.html找到它。

我建议您阅读这本书,它提供了一个相当有趣的观点。

传统发布管理

多年来,企业将 IT 系统的发展从其核心业务流程中剥离出来:零售商店的业务是零售而非软件,但现实很快就敲响了警钟,像亚马逊或阿里巴巴这样的公司,部分可以归功于将其 IT 系统置于业务核心位置。

几年前,许多公司习惯于将整个 IT 系统外包,试图像外包办公室维护一样将复杂性从主营业务中剥离出去。这种做法在很长一段时间里是成功的,因为相同应用程序或系统的发布周期足够长(每年发布几次),可以将复杂的变更管理链条组织起来,发布就像一次大爆炸事件,所有的内容都要精确到毫米,几乎没有容忍失败的空间。

通常,这类项目的生命周期非常类似于下图所示:

这种模型传统上被称为瀑布模型(你可以看到它的形状),它借鉴了传统工业管道的做法,在这些管道中,事情按非常明确的顺序和阶段发生。在软件行业的最初阶段,工程师们尝试将传统行业的做法应用于软件,尽管这是一个不错的想法,但它也存在一些缺点:

  • 旧问题被带到了一个新领域

  • 软件的无形性优势被削弱

使用瀑布模型时,我们面临一个大问题:没有什么进展是快速的。无论在流程中投入多少努力,它都是为巨大的软件组件设计的,这些组件每年发布的次数很少,甚至只有一年一次。如果你尝试将这种模型应用于较小的软件组件,它会因涉及的参与者数量而失败。很可能,捕获需求的人不会参与应用程序的开发,而且肯定对部署一无所知。

沟通链条

我记得小时候,我们曾经玩一个叫做“疯狂电话”的游戏。有人会编一个充满细节的故事并写在纸上,这个人把故事读给另一个人听,后者需要尽可能多地捕捉信息,并把故事传递给下一个人,直到达到游戏人数的上限。四个人之后,几乎可以肯定,故事不会和最初的版本相似,但还有一个更让人担忧的细节:在第一个人之后,故事就再也不会一样了。细节会被删减或创造出来,但故事肯定会发生变化。

这个游戏正是我们在瀑布模型中试图复制的:那些在需求上工作的人员正在编写一个故事,这个故事将被开发人员转述,而开发人员又会编写另一个故事,交给 QA,以便他们可以测试交付的产品是否符合一个已经经过至少两个人手的故事。

正如你所看到的,这注定会是一场灾难,但等等,我们能做些什么来解决它?如果我们看看传统行业,我们会发现他们从来不会犯错,至少错误率非常低。我认为其中的原因是,他们正在构建的是有形的东西,比如汽车或核反应堆,这些东西可以轻易地进行检查,相信与否,它们通常比软件项目简单。如果你开车,几分钟后你就能发现发动机的问题,但如果你开始使用一个新版本的软件,可能要过几年才能发现安全问题,甚至功能性问题。

在软件中,我们通过创建非常简洁且复杂的图表来缓解这个问题,使用统一建模语言UML),这样我们就可以捕捉唯一的事实来源,始终可以回到它来解决问题或验证我们的工件。尽管这是更好的方法,但它也不是没有问题:

  • 一些细节很难在图表中捕捉

  • 业务相关人员不理解 UML

  • 创建图表需要时间

特别是,业务相关人员不理解 UML 是这里的最大问题。在需求收集后,修改它们或在较低级别(如开发、运维等)提出问题时,通常需要涉及一些人,至少有一个人(即业务相关人员)不理解需求捕捉的语言。如果项目的需求从第一次迭代开始就是完全正确的,这不会成为问题,但你参与过的有多少个项目的需求是静态的?答案是没有。

修复 bug 的成本

一旦我们明确了存在沟通问题,bug 通常会在我们的过程中出现。无论是与需求的不一致,还是需求本身有误,通常都会导致缺陷,这可能会阻止我们将应用程序部署到生产环境并延迟所有进度。

在瀑布模型中,修复 bug 在每一步都变得越来越可能。例如,在需求阶段修复 bug 非常简单:只需更新图表/文档,就完成了。如果同样的 bug 在验证阶段被 QA 工程师捕获,我们需要:

  • 更新文档/图表

  • 创建应用程序的新版本

  • 部署新版本到 QA 环境

如果 bug 在生产环境中被发现,您可以想象修复它涉及多少步骤,更不用说压力,特别是当 bug 影响到公司收入时。

发布新版本

几年前,我曾在一家公司的生产发布步骤中工作,步骤是逐条写在 Microsoft Word 文档中的,并附带解释:

  • 将这个文件复制过去:cp a.tar b.tar

  • 使用以下命令重启服务器xyzsudo service my-server restart

这还加上了一长串发布新版本所需执行的操作。这种情况发生是因为那家公司相当大,IT 部门已经商品化,尽管他们的业务是基于 IT 产品的,但他们并没有将 IT 嵌入到业务的核心。

如你所见,这是一个非常危险的情况。即便是创建版本和部署文档的开发人员在场,仍然有人在生产机器上盲目地按照指示部署新的 WAR 文件(一个打包在文件中的 Java Web 应用)。我记得有一天我问过:如果这个人在没有质疑的情况下执行命令,为什么我们不直接写一个在生产环境中运行的脚本呢?他们说那样太危险了。

他们说得对:风险是我们在部署新版本软件时想要减少的因素,而这些软件一天内会被几十万用户使用。公平地说,正是风险促使我们选择在凌晨 4 点而不是在工作时间进行部署。

我看到的问题是,缓解风险的方法(在早上 4 点当没有人购买我们产品时进行部署)创造了我们在 IT 中所说的单点故障:部署是一种“全有或全无”的事件,受时间的巨大限制,因为在早上 8 点,应用的流量通常会从每小时两次访问增长到每分钟成千上万次,早上 9 点通常是一天中最繁忙的时段。

话虽如此,部署结果有两种可能的结果:新软件要么成功部署,要么没有成功。这给相关人员带来了压力,而你最不希望看到的就是压力重重的人在操作一个数百万美元的商业系统。

让我们看看手动部署背后的数学原理,比如之前提到的:

描述 成功率
从集群中卸载server 1 99.5%
停止server 1上的Tomcat 99.5%
移除旧版本的应用(WAR 文件) 98%
复制新版本的应用(WAR 文件) 98%
更新配置文件中的属性 95%
启动Tomcat 95%
server 1附加到集群 99.5%

这描述了在单台机器上发布新版本的软件所涉及的步骤。整个公司系统有几台机器,因此该过程需要重复进行几次,但为了简化起见,我们假设只在一台服务器上进行部署。

现在有一个简单的问题:整个过程中的失败率是多少?

我们自然倾向于认为,像前面这种链式过程中的失败概率,在每个步骤中是链条上最大的一步:5%。但事实并非如此。公平地说,这是一种非常危险的认知偏差。我们通常会因为对低风险的错误认知而做出非常危险的决定。

让我们用数学来计算失败的概率:

上述列表是依赖事件的列表。如果步骤 4 失败,我们就不能执行第 6 步,因此我们要应用的公式如下:

P(T) = P(A1)*P(A2)…*P(An)

这导致了以下计算:

P(T) = (99.5/100) * (99.5/100) * (98/100) * (98/100) * (95/100) * (95/100) * (99.5/100) = 0.8538

我们只有 85.38%的时间会成功。这转化为部署,这意味着我们在凌晨 4 点唤醒来发布新版本的时候,每 6 次中就会出现 1 次问题,但更大的问题是:如果在刚发布后的生产测试中没有人注意到的错误呢?对这个问题的答案既简单又痛苦:公司需要关闭整个系统以回滚到先前的版本,这可能导致收入和客户流失。

现代发布管理

几年前,当我在凌晨 4 点进行手动部署时,我记得自问:“一定有更好的办法。”工具还不够成熟,而且大多数公司并不认为 IT 是他们业务的核心。然后,一场变革发生了:DevOps 工具开始在开源社区中表现出色,公司开始创建持续交付管道。其中一些取得了成功,但绝大多数失败了,原因有两个:

  • 发布管理流程

  • 组织对齐的失败

我们将在本章后面讨论组织对齐问题。现在,我们将专注于发布管理流程,因为它需要与传统的发布管理完全不同,以促进软件生命周期。

在前面的章节中,我们谈到了不同的阶段:

  • 需求

  • 设计

  • 实施

  • 验证

  • 维护

我们还解释了它在庞大软件中如何运作,我们将功能分组到大版本中,以大爆炸式的方式执行,要么全面部署,要么不部署。

第一次尝试将此流程适应更小的软件组件就是每个人称为敏捷的东西,但没有人真正知道它是什么。

敏捷开发和沟通

在传统的发布管理中,一个大问题是沟通:人们传递信息和消息的链条,正如我们所见,往往收效甚微。

敏捷鼓励更短的沟通链:利益相关者应参与软件开发管理,从需求定义到同一软件的验证(测试)。这有一个巨大的优势:团队永远不会构建不需要的功能。如果需要满足截止日期,工程团队会减少最终产品的规模,牺牲功能而不是质量。

"提前交付并频繁交付"是敏捷的座右铭,基本含义是定义一个最小可行产品MVP),并在它准备好时尽早交付,以便为应用程序的客户提供价值,然后根据需求交付新功能。通过这种方式,我们从第一次发布开始就提供价值,并在产品生命周期的早期就获得反馈。

为了表达这种工作方式,提出了一个新概念:sprint(冲刺)。冲刺是一个时间段(通常为 2 周),在这段时间内会完成一组功能,并在结束时将其交付到生产环境,以实现不同的效果:

  • 客户能够频繁获得价值

  • 反馈每 2 周就会到达开发团队,以便采取纠正措施

  • 团队变得更加可预测,并且能够准确估算

最后这一点非常重要:如果我们在一个季度发布中的估算偏差为 10%,那么意味着我们误差了两周,而在两周的冲刺中,误差只有一天,随着时间推移,随着每次冲刺积累的知识,团队能够根据已有的功能和所花费的时间建立一个数据库,从而进行比较,调整新的功能。

这些功能不叫功能,它们叫做故事。故事的定义是:在冲刺开始之前,为开发团队准备好所有必要信息的、功能清晰的模块,因此当我们开始开发冲刺时,开发者可以专注于技术活动,而不是集中精力解决这些功能中的未知问题。

并非所有的故事大小相同,因此我们需要一个衡量单位:故事点。通常,故事点与时间框架无关,而是与复杂性相关。这让团队能够计算出每个冲刺结束时能交付多少故事点,因此,随着时间的推移,他们能更好地进行估算,每个人的期望都能得到满足。

每个冲刺结束时,团队应该发布已开发、测试并集成到生产环境中的功能,以便进入下一个冲刺。

冲刺的内容从团队正在维护和准备的待办事项列表中选择。

主要目标是通过保持沟通畅通,满足每个人的期望,能够预测交付内容及其交付时间,并明确交付所需的条件。

有多种方法可以在我们的软件产品中实现敏捷方法。前面讲解的叫做Scrum,但如果你研究其他开发方法论,你会发现它们都专注于相同的概念:改善同一团队中不同角色之间的沟通。

如果你对Scrum感兴趣,可以查看更多信息:en.wikipedia.org/wiki/Scrum_(software_development)

发布新版本

如前所述,如果我们遵循 Scrum 方法论,我们应该每 2 周交付一个新版本(在大多数情况下,一个 sprint 的周期),这对消耗的资源有显著影响。让我们来做个数学比较:季度发布与双周发布:

  • 在季度发布中,除了紧急发布以外,我们一年只发布四次,用来修复生产中发现的问题。

  • 在双周发布中,我们每两周发布一次,除此之外还有紧急发布。这意味着每年会发布 26 次(大约 52 周),加上紧急发布。

为了简单起见,我们忽略紧急发布,专注于我们应用程序的日常业务。假设我们需要 10 小时来准备和发布我们的软件:

  • 季度发布:10 x 4 = 40 每年小时

  • 双周发布:10 x 26 = 260 每年小时

到目前为止,发布软件始终是相同的活动,无论我们是每季度发布一次还是每天发布一次。其含义是相同的(大致上),所以我们面临一个大问题:我们的双周发布消耗了大量时间,如果我们需要发布修复 QA 中被忽略的问题,情况会更糟。

解决这个问题只有一个办法:自动化。正如前面提到的,直到 2 年前(大约 2015 年),用于协调自动化部署的工具还不够成熟。Bash 脚本曾经很常见,但并不理想,因为 bash 并不是为了改变生产服务器的状态而设计的。

最早的自动化部署工具是用来管理服务器状态的框架:Capistrano 或 Fabric 包装了 ssh 访问和状态管理,利用 Ruby 和 Python 上的一组命令,允许开发者创建脚本,根据服务器的状态执行不同的步骤来实现一个目标:发布新版本。

这些框架是一个很好的进步,但它们存在更大的问题:跨公司解决方案通常以不同的方式解决相同的问题,这意味着 DevOps(开发+ 运维)工程师需要学会如何在每一个公司中处理这些问题。

真正的变化来自 Docker 和编排平台,如 Kubernetes 或 Docker Swarm。在本书中,我们将探讨如何使用它们,特别是 Kubernetes,将部署时间从 10 小时(或者一般的小时)减少到一次简单的点击,这样我们的每年 260 小时变成了每次发布几分钟。

这也有一个副作用,和我们在本章前面解释的内容有关:从一个非常高风险的发布(记住,85.38%的成功率)伴随大量压力开始,我们正在朝着一个可以在几分钟内修补的发布方式发展,因此发布一个 bug,尽管这不好,但由于我们能够在几分钟内修复或甚至在几秒钟内回滚,它的影响大大降低。我们将在第八章中讨论如何做到这一点,发布管理 – 持续交付

一旦我们与这些实践达成一致,我们甚至可以将单个项目发布到生产环境:一旦某个功能完成,如果部署是自动化的,且只需点击一下,为什么不在功能完成时直接推出这些功能呢?

DevOps 和微服务

微服务是如今的大趋势:小型软件组件,它允许公司在功能的垂直切片上管理他们的系统,单独部署功能,而不是将它们捆绑在一个大应用程序中,这在大团队中可能会导致问题,因为功能之间的交互常常会导致冲突和错误被发布到生产环境,而没人注意到。

一个使用微服务且相当成功的公司例子是 Spotify。不仅在技术层面,甚至在业务层面,他们已经组织好了相关工作,能够协调大量的服务,提供几乎不会失败的顶级音乐流媒体服务,如果失败了,也只是部分失败:

  • 播放列表由一个微服务管理;因此,如果它宕机,只有播放列表会不可用。

  • 如果推荐功能没有生效,用户通常甚至不会注意到。

这带来了巨大的成本:运营开销。将一个应用拆分成多个微服务需要相应的运营量来维持其运行,如果处理不当,这个开销会呈指数增长。我们来看一个例子:

  • 我们的系统由五个应用程序组成:A、B、C、D 和 E。

  • 它们每个都是一个单独部署的微服务,每个月需要大约 5 小时的运维时间(部署、容量规划、维护等)。

如果我们将所有五个应用程序合并成一个大应用程序,我们的维护成本将大幅下降,几乎和之前提到的任何微服务的成本一样。数字非常清晰:

  • 基于微服务架构的系统每月需要 25 小时的维护时间

  • 单体应用每月需要 5 小时的维护时间

这带来了一个问题:如果我们的系统增长到数百个(是的,数百个)微服务,情况就变得难以管理,因为这会消耗我们所有的时间。

唯一的解决方案是自动化。虽然始终会有运营开销,但通过自动化,我们不仅可以节省每个服务每月增加的 5 小时时间,随着时间的推移,这个时间会减少,因为一旦我们自动化了干预,新的服务几乎不会消耗任何时间,一切都会像事件链一样自动发生。

在第八章中,发布管理 - 持续交付,我们将设置一个持续交付流水线,演示这一点是如何实现的,尽管我们会有一些手动步骤以保证稳定性,但完全可以自动化在 Kubernetes 等集群上运行的微服务环境中的所有操作。

一般来说,我不建议任何公司在没有适当自动化的情况下启动基于微服务的项目,特别是如果你确信系统会随着时间的推移增长,那么 Kubernetes 将是一个非常有趣的选择:它提供了其他平台所缺乏的功能,比如负载均衡、路由、入口等。我们将在接下来的章节中深入探讨 Kubernetes。

所有这些活动应该是 DevOps 工程师日常工作的组成部分(其中还有很多其他任务),但首先,我们需要解决一个问题:如何调整我们公司的资源,以便最大限度地发挥 DevOps 工程师的作用。

DevOps:组织对齐

到目前为止,我们已经了解了现代和传统发布生命周期的运作方式。我们还定义了 DevOps 工程师的角色,并说明了他们如何帮助微服务,如前所述,如果没有适当的自动化,它们是不可行的。

除了技术细节,还有一个对 DevOps 文化成功至关重要的因素:组织对齐。

传统的软件开发通常将团队划分为不同的角色:

  • 商业分析师

  • 开发人员

  • 系统管理员

  • QA 工程师

这就是我们所说的横向切片:一个系统管理员团队与开发人员有少数接触点,以便他们获得足够的信息来部署和维护软件。

在现代发布生命周期中,这种方式显然行不通。我们需要的是纵向切片:一个团队应该由每个横向团队的至少一名成员组成。这意味着开发人员、商业分析师、系统管理员和 QA 工程师应该在一起……当然,百分之百的融合并非必需。

在 DevOps 哲学下,这些角色中的一些变得不再重要或需要发展。其理念是一个团队能够独立地构建、部署和运行应用程序,而不依赖外部支持:这被称为跨职能自主管理团队。

在我的职业经验中,跨职能团队是交付高质量可靠产品的最佳组织方式。产品由构建人员管理;因此,他们对产品了如指掌。分析师(取决于业务性质)、开发人员和 DevOps 工程师的组合是将高质量软件交付到生产环境所需的全部人员。有些团队可能还会包括 QA 工程师,但一般来说,由 DevOps 和开发人员创建的自动化测试应该是至高无上的:如果没有良好的代码覆盖率,就不可能以持续交付的方式发布软件。我非常支持分析师亲自进行软件测试,因为他/她是最了解需求的人,因此最适合进行验证。

DevOps 工程师扮演着跨领域的角色:他们需要了解应用是如何构建的(并可能参与开发),但他们的重点与应用的运营相关:安全性、操作准备、基础设施和测试应是他们的日常工作。

我也看到过完全由 DevOps 工程师和分析师组成的团队,没有纯粹的开发人员或 QA。在这种情况下,DevOps 工程师不仅负责基础设施部分,还负责应用开发,这可能会非常具有挑战性,具体取决于系统的复杂性。一般来说,每个案例都需要单独研究,因为 DevOps 并不是一个“通用”的产品。

你可以从本书中期待什么

现在我们已经介绍了 DevOps,接下来是具体说明我们将在本书中学习什么内容。主要将集中在 Google Cloud Platform 及其相关的 DevOps 工具上,原因有多方面:

  • GCP 的试用期足够让你完成整本书的学习。

  • 这是一个非常成熟的产品。

  • Kubernetes 是 GCP 的重要组成部分。

你将学习 DevOps 工具和实践的基础知识,这些内容提供了足够的细节,让你在需要时能够进一步查找额外的信息,同时也能立即在公司中应用所学。

本书将重点关注 DevOps 的运维部分,因为应用开发方面的知识已经足够,且在 DevOps 世界中并未发生变化。不言而喻,我们不会展示如何为你的应用编写测试,这对于确保系统的稳定性是至关重要的活动:没有良好的代码覆盖和自动化测试,DevOps 是无法奏效的。

一般来说,这些示例足够简单,入门级的 DevOps 人员也能轻松跟上,但如果你想深入了解 GCP 的某些方面,可以参考 cloud.google.com/docs/tutorials 上的丰富教程集合。

这本书的结构是渐进式的:首先,在讲解不同云提供商的内容之后,展示 Docker 的基础知识,但在深入讲解配置管理工具(具体是 Ansible)和容器编排平台(主要是 Kubernetes)之前。

最终,我们将设置一个名为 Chronos 的时区时间戳管理系统的持续交付管道,我出于多种原因在讲座中使用它:

  • 它几乎没有业务逻辑。

  • 它基于微服务架构。

  • 它涵盖了几乎所有所需的基础设施。

你可以在以下 GitHub 仓库找到 Chronos 的代码:github.com/dgonzalez/chronos

大多数示例可以通过在本地计算机上使用虚拟化提供商(例如 VirtualBox 和 Kubernetes 示例中的 MiniKube)进行重复,但我鼓励你注册 Google Cloud Platform 的试用版,因为它在写这篇文章时,提供给你 $300 或 1 年的资源供自由使用。

概述

在这一章中,我们已经看到了如何调整资源(工程师)以交付低成本、高影响力的 IT 系统。我们还看到了,沟通不畅如何导致缺陷的发布过程,从而使我们的部署陷入僵局,并且从业务角度来看,系统变得相当低效。在本书的其余部分,我们将探讨能够帮助我们不仅改善这种沟通,还能使我们的工程师以更低的成本交付更高质量功能的工具。

这些工具的第一个系列将在下一章中介绍:云数据中心。这些数据中心使我们能够从其资源池中创建资源(虚拟机、网络、负载均衡器等),以满足我们对特定硬件的需求,且价格合理,灵活性高。现代(以及一些非现代)IT 公司越来越多地采用这种云数据中心,这也促使了一系列自动化基础设施的工具的诞生。

第二章:云数据中心——新现实

在过去几年,云系统逐渐成为主流,它们使得企业能够根据需求以一种简便且低成本的方式扩展。它们还使公司能够利用一种叫做基础设施即代码IAC)的技术,这基本上允许你将之前需要根据需求购买的物理资源(服务器和路由器)视为代码,你可以查看、运行并重新运行这些代码,以使基础设施适应你的需求。

在本章中,我们将介绍主要的云服务提供商,重点分析它们的主要优点和缺点,以便形成清晰的认识,了解它们提供了什么以及我们作为工程师如何利用这些服务。

在市场上的所有提供商中,我们将重点关注这两家:

  • Amazon Web ServicesAWS

  • Google Cloud Platform

我们还将简要讨论以下内容:

  • Heroku

  • Azure

  • DigitalOcean

我们应该保持开放的心态,因为它们每个都可以提供不同且有价值的功能集,这是不容忽视的。

我们将介绍Kubernetes,在我个人看来,它是现代 DevOps 世界中许多问题的答案。

亚马逊网络服务

亚马逊无疑是全球最大的在线零售商,几乎在全球都有业务。每个人都听说过亚马逊,以及这种商店为 21 世纪繁忙社会带来的各种可能性:它们提供几乎任何可以在传统商店购买的商品的送货上门服务。

亚马逊由 Jeff Bezos 于 1994 年创立,从那时起,它每年都在持续增长,提供越来越多的产品和服务,但有一天,它们进入了云计算业务。对于像亚马逊这样的大公司来说,拥有大量的处理能力是有意义的,同时它还具有可靠性,并能够迅速适应业务需求。

起初,云服务是为了满足业务的高可用性需求,并且能够以统一的方式扩展。这使得公司在构建一流的基础设施即服务IaaS)方面积累了大量经验,最终他们意识到这些技术可以出售给客户。

到了 2006 年,市场上没有任何竞争对手,所以它们处于成功起步的有利位置。

我记得当时我还在大学,EC2 和 EC3 两项服务首次在一个会议上推出。

EC2 让你能够在云上创建虚拟机,提供一个通过命令行接口和 Web 界面进行操作的 API,后者可以作为你资源的监控工具。

S3 是一种关键价值存储(某种意义上),允许你以非常低的价格存储大量数据,并且同样可以通过命令行界面进行操作。

这真的是一次革命。它是一个完全的范式转变:现在你可以根据需要请求更多资源。这就像是一个 API 调用,几分钟内就可以得到三台新机器。下面的截图展示了 AWS 上的服务列表:

2017 年 1 月 AWS 服务目录

在过去几年中,亚马逊频繁地添加服务,直到有时很难跟上这种速度。在本章中,我们将介绍一些主要的服务(或我认为最有用的服务),展示它们的功能和应用领域。

EC2 - 计算服务

云系统必须为用户提供的第一个元素是计算能力。EC2代表弹性计算云,它允许你通过几次点击在云端创建机器。

这是 EC2 界面的样子:

EC2 界面

EC2 于 2006 年 8 月 25 日推出(beta 版本),自那时以来已经经历了很多发展。它为用户提供不同大小的机器,并且在全球范围内可用(截至今天,覆盖 11 个不同的区域)。这意味着用户可以在全球不同地区启动机器,以实现高可用性和低延迟,使得你公司工程师能够在不需要跨国协调团队的情况下构建多区域应用。

它们还提供不同类型的实例,针对不同任务进行了优化,使得用户可以根据自己的需求定制基础设施。总共有 24 种不同类型的实例,但它们也按类型进行了分组,我们将在本章后面详细介绍。

让我们看一个如何启动实例的示例。

启动实例

首先,你需要进入 AWS EC2 界面。

  1. 现在点击启动实例按钮,这将带你进入以下界面:

  1. 这是你可以选择运行镜像的地方。如你所见,镜像是将运行在 EC2 实例上的操作系统。在亚马逊的术语中,这个镜像被称为Amazon Machine ImageAMI),你可以创建自己的镜像并保存以便后续使用,这样可以便捷地发布预先构建的软件。现在,选择 Ubuntu Server 16.04 并点击选择。

  2. 下一屏显示的是镜像的大小。AWS 提供了多种大小和类型的镜像。这一参数会显著影响应用程序的性能,包括网络、内存、CPU 性能以及机器的 I/O。

  3. 让我们看一下不同类型的实例:

类型 描述
突发型实例 T2 是用于突发处理的通用型实例。它们提供了一个 CPU 基线水平,以应对处理能力的高峰,但这些高峰是按累积方式提供的:在空闲时,CPU 会累积积分,这些积分可以在需求高峰期使用,一旦这些积分用尽,性能就会恢复到基线水平。
通用型 M3 是一个通用型实例,具有专用资源(没有突发积分)。它在 CPU、内存和网络资源之间提供了良好的平衡,是需要稳定性能的生产应用程序的最小实例。M4 遵循与 M3 相同的理念,但硬件有所更新:亚马逊弹性块存储Amazon EBS)优化、更好的 CPU 以及增强的网络系统是该实例类型的亮点。
计算优化型 AWS 的计算优化型实例包括 C3 和 C4。与 M 系列实例相同,C4 是 C3 的硬件升级版。这些类型的实例适用于需要强大 CPU 性能的工作,如数据处理和分析或高要求的服务器。C4 还配备了增强型网络系统,这对于高网络流量应用程序非常有帮助。
内存优化型 如你所料,AWS 还提供了内存优化型实例,适用于需要高内存使用的应用程序。基于 Apache Spark(或大数据应用)的应用程序、内存数据库等,最能受益于这类实例。在这种情况下,内存优化型实例被划分为两个子系列:X1:这些是大规模企业级实例。X1 可用于企业生态系统中最苛刻的应用程序,它是内存密集型实例的旗舰,仅用于非常大的应用程序。R3/R4:尽管比 X1 更为简朴,但 R 实例完全能够处理大多数日常内存密集型应用程序。缓存系统、内存数据库等是 X 和 R 实例的最佳应用场景。
加速计算实例 一些应用程序,如人工智能AI),具有特定的计算要求,如图形处理单元GPU)处理或可重配置硬件。这些实例被分为三种系列:P2:GPU 计算实例。这些实例经过优化,可执行特定的处理任务,如通过暴力破解密码以及机器学习应用(通常依赖 GPU 算力)。G2:图形处理实例。视频渲染、光线追踪或视频流媒体是这些实例的最佳应用场景。
  1. 如你所见,每种用户需求都有一个相应的实例可用。目前,我们首先选择一个小型实例,因为我们只是测试 AWS,其次是因为 AWS 提供免费套餐,允许你在 1 年内免费使用t2.micro实例,具体如以下截图所示:

  1. 现在我们有两个选项。点击“审核实例启动”或“配置实例详情”。在本例中,我们将点击“审核实例启动”,但通过点击“配置实例详情”,我们可以配置实例的多个元素,例如网络、存储等。

  2. 当你点击“审核实例启动”后,审查页面将出现。点击“启动”,你应该看到类似于下图所示的界面:

  1. 只需为密钥对命名并点击“下载密钥对”按钮,这将下载一个.pem文件,稍后我们将使用该文件通过ssh访问实例。

  2. 一旦你指定了密钥对,点击“启动实例”,如前面的截图所示,就是这么简单。经过几次检查后,你的镜像将准备好安装所需软件(这通常需要几分钟)。

这是在 AWS 中创建运行实例所需的最低配置。正如你所看到的,整个过程在屏幕上有非常详细的解释,一般来说,如果你了解 DevOps 的基础(ssh、网络配置和设备管理),你在创建实例时不需要太多帮助。

关系型数据库服务

在前一部分中,我们展示了可以用来安装所需软件的 EC2 实例。还有一个服务可以让你管理跨区域的高可用数据库(MySQL、PostgreSQL、Maria DB、Aurora,以及 Oracle 和 SQL Server)。这个服务叫做 RDS,代表关系型数据库服务。

关系型数据库的一大难题是高可用性配置:主主配置通常费用较高,小公司往往无法承担。AWS 通过 RDS 提升了标准,提供了可以通过几次点击就能设置的多区域高可用数据库。

AWS 与 EC2 的网络配置

AWS 提供了细粒度的网络控制。与任何物理数据中心一样,你可以定义自己的网络,但 AWS 有一个更高级的抽象概念:虚拟私有云(VPC)。

亚马逊虚拟私有云(Amazon VPC)是 AWS 云的一部分,允许您将资源在子网中分组和隔离,以便按照您的需求组织和规划基础设施。它还允许您在 AWS 和物理数据中心之间创建 VPN,从而扩展后者,增加更多来自 AWS 的资源。同时,当您在 EC2 中创建资源时,您可以选择将资源创建在您自定义的子网中。

在了解 VPC 的结构之前,让我们先解释一下 AWS 如何处理资源的地理分布。AWS 为您提供不同地区的数据中心,例如欧洲、亚洲和美国。以欧盟西部(EU West)为例,它有三个不同的可用区:

在 AWS 中,“区域”概念基本上指的是 AWS 数据中心所在的地理区域。了解这些信息使我们能够构建全球规模的应用程序,通过将流量从最近的数据中心提供服务来提高延迟表现。地理分布的另一个重要原因是许多国家的 数据保护法律。通过选择数据存储的位置,我们可以确保遵守这些法律。

在这些地理区域内,我们有时可以找到可用区。一个可用区基本上是一个物理上分离的数据中心,确保系统的高可用性。例如,在其中一个数据中心发生灾难时,我们可以始终依靠其他可用区进行恢复。

让我们来看一下区域和可用区的分布:

现在我们已经理解了 AWS 从地理角度的运作方式,让我们更深入地了解在区域和可用区层面上 VPC 的概念。

VPC 是 AWS 云中逻辑上隔离的部分,对于用户而言是私有的,可以承载资源,并且跨越 AWS 区域的所有可用区。在这个 VPC 内,我们可以定义不同的子网(不同可用区中的公共和私有子网),并指定哪些机器可以从互联网访问:AWS 允许您创建路由表、Internet 网关、NAT 网关以及其他常见的网络资源,使用户能够构建与物理数据中心中相同的基础设施。

要讨论 AWS 中的网络,仅这一部分就足以写一本完整的书。我们将在本书的后续章节深入探讨一些概念,但如果你真的想深入了解 AWS 的网络部分,你可以访问 docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html 查找更多的数据和示例。

AWS 还提供了一个非常强大的元素:Elastic Load Balancing ELB)。ELB 是经典硬件负载均衡器的现代版本。它使我们能够对资源进行健康检查,只将健康的资源加入池中。此外,AWS 提供两种类型的负载均衡器:经典负载均衡器和应用负载均衡器。第一种版本,顾名思义,是应用负载均衡器,它根据健康检查分配流量,并不理解传输的数据,而应用负载均衡器可以根据请求的信息,基于高级策略来路由流量。ELB 还能够处理完整的 HTTPS 流量,这样我们就可以在负载均衡器中进行 SSL 终止操作,并允许我们的应用将加密/解密任务卸载给负载均衡器。

AWS 和 EC2 中的存储

到目前为止,我们已经展示了如何在 AWS 中创建机器和网络基础设施。在构建应用程序时,一个重要的方面就是数据存储。默认情况下,当我们在 EC2 中启动机器时,根卷中可以关联两种存储类型来运行操作系统:

  • 实例存储支持的镜像

  • Amazon Elastic Block Store Amazon EBS)存储支持的镜像

第一种类型,即实例存储支持的镜像,依赖于与镜像相关联的存储来挂载并运行根卷。这意味着一旦机器终止,存储在镜像中的数据将丢失(这种类型的镜像不支持停止操作,只支持终止操作)。

第二种类型的实例是由 EBS 支持的。Elastic Block Store 是 AWS 提供的存储服务名称。使用 EBS,用户可以根据需要创建和销毁卷(块设备)以及快照:我们可以在进行风险操作之前创建一个正在运行的镜像副本,以便如果发生错误时可以恢复。

存储类型可以根据我们的需求有所不同:你可以从磁性块设备到 SSD 硬盘,也可以使用通用设备来覆盖所有应用中的大多数使用场景。

通常情况下,所有实例都由 EBS 支持,因为存储在逻辑上与计算分离,这使得我们能够进行一些操作,例如调整实例大小(例如,创建更强大的实例),而不会丢失数据。

可以将多个卷挂载到同一个 EC2 实例中,这些卷会像物理设备一样暴露给它,因此如果我们使用的是基于 Linux 的镜像(如 Ubuntu),我们可以使用 mount 命令将设备挂载到文件夹中。

Amazon S3

亚马逊简单存储服务(Amazon S3)正如其名,是一种以非常低的成本,在云端存储大量数据的简单方式,并提供了一套不错的功能。与基于预定义大小的设备存储的 EC2 不同,Amazon S3 实际上是一种键值存储,使我们能够通过键来识别数据。与其他键值存储技术不同,S3 准备好存储从小到非常大的对象(最大可达 5 TB),且响应时间极低,且可以在任何地方访问。

与 EC2 一样,Amazon S3 是一个具有区域概念的功能,但 S3 不理解可用区:S3 服务本身能够将对象存储在不同的设备上,因此您不需要担心这一点。数据存储在一种叫做桶(buckets)的抽象结构中,如果我们尝试将 S3 与文件系统进行比较,它就像一个文件夹,但有一个限制:桶的名称必须在 AWS 账户的所有区域中唯一,因此我们不能在两个不同的区域创建一个名为 Documents 的桶。

S3 的另一个优势是 AWS 提供了一个 REST API,可以非常简单地访问对象,这使得它成为现代 Web 存储的理想选择。

在我的职业生涯中,我遇到的 S3 的最佳使用案例之一是在金融机构中管理大量文档。通常,当公司涉及到资金时,他们必须将客户纳入一个叫做 客户尽职调查CDD)的过程。这个过程确保客户确实是他们声称的人,且资金来源是合法的。根据金融规定,公司还需要保存这些文件至少 6 年。

为了进行这项调查,用户需要将文件发送给公司,而 Amazon S3 正好适合这个需求:客户将文件上传到公司的官网,实际上是将文件推送到 S3 桶(每个客户一个桶),并通过 Amazon S3 的复制功能将文件跨区域复制。此外,S3 为这一模式提供了另一个有趣的功能:有效期内的对象链接。基本上,这使得您可以创建一个仅在特定时间内有效的链接,因此,如果审查文档的人将链接暴露给第三方,S3 会返回错误,确保文件不容易被意外泄露(用户仍然可以下载它)。

S3 另一个有趣的特点是可以与 Amazon 密钥管理系统(Amazon KMS)集成(KMS 是 AWS 提供的另一项功能),因此我们在 S3 中的所有对象都会由保存在 KMS 中的密钥加密,并且该密钥可以定期透明地轮换。

亚马逊 ECR 和 ECS

容器已成为新常态。在过去几年中,我遇到的每个公司都在使用或考虑使用容器来构建软件。这使我们能够在构建软件时考虑微服务的原则(小型独立运行的单个软件组件),因为它提供了从不同应用程序的配置和部署中抽象出一个合理的层次:基本上,所有配置都存储在容器中,我们只需要关心如何运行它。

作为微服务架构的先驱之一,Amazon 创建了自己的镜像注册表和集群(服务)。

正如我们在第三章中将深入讨论的那样,Docker 是围绕两个概念构建的:镜像和容器。镜像是应用程序的定义(配置 + 软件),而容器是运行镜像的实例。镜像通过 Dockerfile(一个带有非常基础脚本语言的镜像描述)构建并存储在注册表中,在这种情况下是 Amazon EC2 容器注册表ECR),我们在 AWS 基础设施中的私有注册表。我们不需要担心可用性或管理资源;我们只需选择容器运行的区域,并将镜像推送到该存储库。

然后,从我们运行 Docker 的主机中,拉取镜像并实例化容器。这既简单又有效,但有一些需要考虑的因素:

  • 当我们的主机没有足够的资源来运行我们想要的容器数量时,会发生什么情况?

  • 如果我们想确保容器的高可用性会发生什么情况?

  • 我们如何确保容器在故障(出于某种原因)时重新启动?

  • 我们如何在不中断服务的情况下为系统添加更多的硬件资源?

这些问题在几年前更加棘手,但现在变得简单了:Amazon EC2 容器服务(Amazon ECS)会为我们处理这些问题。ECS 基本上是一个由资源(EC2 机器)组成的集群,这些资源协同工作,为我们的容器提供运行环境。

在 ECS 中,当创建一个新服务时,我们会指定一些参数,比如应该同时运行多少个容器副本,以及容器应使用什么配置(镜像)。让我们看看它是如何工作的。

创建集群

首先,我们将在 AWS 控制台中创建一个集群,并查看它是如何工作的。

  1. 转到 Amazon ECS 页面并点击“开始使用”按钮(屏幕上唯一的按钮,因为你还没有创建任何资源):

  1. 在继续之前,确保两个复选框都已勾选。我们想将一个示例应用部署到 ECS,并且还希望将镜像存储在 ECR 中。

  2. 下一屏幕至关重要:这里是我们定义镜像存储库的地方,它将决定用于从本地机器通过 Docker 推送镜像的存储库 URI。

  1. 只需使用 devops-test 作为仓库名称,我们的仓库 URI 看起来将与前面截图中的非常相似。

  2. 步骤 2(共 6 步)是 AWS 提供的一系列命令,用于登录 ECR 并推送我们项目的镜像。在本例中,我们将使用一个非常简单的 Node.js 应用:

       var express = require('express');
       var app = express();

       app.get('/', function (req, res) {
        res.send('Hello World!');
      });

        app.listen(3000, function () {
          console.log('Example app listening on port 3000!');
        });
  1. 将之前的代码保存在名为 index.js 的文件中,放在本地机器上名为 devops-test 的文件夹内。由于我们正在使用 express,所以我们需要安装所需的依赖项。只需执行以下命令:
 npm init
  1. 经过几个问题(只需按几次 Enter 就可以了),应该会创建一个名为 package.json 的文件。现在我们需要为程序安装 express:
 npm install --save express
  1. 就这样!我们的 package.json 文件应该包含一行描述所需依赖项的内容:
       {
            "name": "code",
            "version": "1.0.0",
            "description": "",
            "main": "index.js",
        "scripts": {
        "test": "echo "Error: no test specified" 
       && exit 1",
        "start": "node index.js"
       },
       "author": "",
       "license": "ISC",
       "dependencies": {
       "express": "⁴.14.1"
       }
     }
  1. 这个文件允许我们在需要时重新安装依赖项,而无需手动操作;它还允许我们指定一个命令,当我们执行 npm start 时会运行该命令(这是一种使用 npm 运行 Node 应用的标准方式)。按前面的代码中所示添加并突出显示该行,因为我们稍后会需要它(不要忘记上一行的分号)。

  2. 现在我们需要编写我们的 Dockerfile。正如我们将在第三章中看到的,Docker是一个描述我们的 Docker 镜像外观的文件。在这个案例中,我们将重建在 Docker 容器中运行节点应用程序所需的步骤:

    FROM node:latest

    RUN mkdir -p /usr/src/app
    WORKDIR /usr/src/app

    COPY package.json /usr/src/app/
    RUN npm install

    COPY . /usr/src/app

    EXPOSE 3000
    CMD [ "npm", "start" ]
  1. 不要试图理解这个文件;我们将在本书后面深入讲解。只需将其保存为 Dockerfile,并放在之前提到的文件夹 devops-test 中。到目前为止,你的 devops-test 文件夹应该如下所示:

  1. 现在我们准备好按照 ECS 设置中的步骤 2 进行操作。请注意,以下图像与我在 AWS 中的用户相关;你的用户将有不同的参数,因此请使用你自己的参数,而不是复制前面截图中的内容:

完成后,包含你应用程序镜像的新版本应该会安装在你的私有 ECR 中。

  1. 下一步(步骤 3)是创建 AWS 所谓的任务定义,它基本上是我们的容器实例的配置:我们将使用多少内存、运行哪个镜像以及在容器中暴露哪些端口。只需保留默认内存,但将端口更改为 3000,因为这是我们在前面的示例中使用的端口(节点应用程序)。这是典型的 Docker 参数,我们将在下一章中详细了解 Docker。

  2. 一旦准备好,点击下一步,我们将进入第 4 步。此步骤是我们将配置服务的地方。这里的服务指的是我们要保持活跃的容器实例数量,以及如何暴露它们:通过负载均衡器,还是仅通过集群中的 EC2 实例。我们还可以指定用于注册和注销正在运行实例的 IAM(AWS 凭证系统):

  1. 我们只需要保留默认设置,除了两个参数:

    • 所需任务数量:设置为2

    • 在 ELB 部分,我们只需选择 sample-app: 80(或选择不是“No ELB”的选项,这样 AWS 会为我们提供一个 ELB)

  2. 点击下一步,在这里我们将定义我们的集群将如何配置:

    • 节点的数量

    • 节点的大小

  3. 一旦准备好,只需检查并启动实例。几分钟后,我们的集群应该启动并运行,准备好与我们部署的任务一起工作。

你可以通过集群本身提供的负载均衡器,在端口3000上访问我们创建的任务实例。如你所见,ECS 简化了设置容器集群的任务。

在本书中,我们将特别关注 Kubernetes 和 Docker Swarm,主要是因为它们是平台无关的技术,但我相信 Amazon ECS 是构建新容器化系统时值得考虑的一个非常有效的技术。

其他服务

如你所见,AWS 中的服务列表几乎是无止境的。我们已经访问了我认为最重要的服务,接下来的章节中,我们将探讨一些其他也很有趣的服务,但遗憾的是,我们无法深入探讨所有服务。然而,AWS 的文档相当完善,每项服务总是附带了详细的使用说明。

在本节中,我们将简要介绍一些服务,尽管它们非常重要,但并不是本书开发的核心内容。

Route 53

Route 53 是 AWS 中的 DNS 服务。它是一个全球性的可扩展 DNS 服务,允许你执行一些高级操作:

  • 注册域名

  • 从其他注册商转移域名

  • 创建流量路由策略(例如跨区域的故障转移)

  • 监控你应用程序的可用性(并将流量重定向到健康的实例)。

使用 Route 53,我们可以将域名链接到 AWS 资源,如负载均衡器、S3 存储桶和其他资源,使我们能够为在 AWS 实例中创建的资源(主要是虚拟机)公开一个人类可读的名称。

CloudFront

CloudFront 解决了低流量网站在访问量激增时面临的最大问题之一:它提供了缓存,使我们不禁想是否是 AWS 在提供数据,而不是我们的服务器。基本上,CloudFront 拦截对我们主机的请求,渲染页面,并将其缓存长达 24 小时,这样我们的网站就可以将流量卸载到 AWS。它是为提供静态内容而设计的,因为当用户第二次访问相同的 URL 时,将会提供缓存版本,而不是再次访问您的服务器。

强烈建议您使用 CloudFront 为公司的网站提供服务,这样您就可以使用一台非常小的机器处理所有流量,既节省了资源,也能在流量激增时提高站点的正常运行时间。

Amazon ElasticCache

Amazon ElasticCache,顾名思义,是一个分布式和可扩展的内存缓存系统,可以用于在应用程序中存储缓存数据。

它解决了我们在构建依赖缓存存储和检索数据的应用程序时可能面临的最大问题之一:高可用性和一致的临时数据存储。

Amazon RDS

RDS 代表 关系型数据库服务。通过 RDS,您可以通过几次点击配置 DB 实例,用于存储数据:Oracle、MySQL 和 MariaDB 是我们在 RDS 中的几个选择。它利用底层数据库系统的高可用性,这可能是我们依赖 AWS 时面临的一个问题,但通常是可以接受的,因为 SQL 数据库中的高可用性是一个复杂的课题。

DynamoDB

DynamoDB 是一款出色的工程产品。它是一个 NoSQL 数据库,在任何规模下都能将延迟微调到毫秒级。它存储对象而不是行(无法使用 SQL),是存储大量数据的无模式方式的良好选择。从本质上讲,DynamoDB 非常类似于 MongoDB,但有一个基本区别:DynamoDB 是 AWS 提供的服务,只能在 AWS 内运行,而 MongoDB 是一款可以安装在任何地方的软件,包括 AWS。从功能角度来看,MongoDB 的大部分用例同样适用于建模 DynamoDB 数据库。

Google Cloud Platform

Google 一直在技术领域处于领先地位。令人惊讶的是,Google 过去没有提供一个联合的服务层;相反,它提供了各个服务,这在为开发人员提供一个稳固的平台构建应用程序时远非理想。为了解决这个问题,它推出了 Google Cloud Platform,这是一个服务集合(基础设施即服务、平台即服务、容器和大数据,以及许多其他功能),使开发人员和公司能够构建具有高度可靠性和可扩展性的系统,提供一些最先进的功能,如 Kubernetes 和一套独特的机器学习 API。

界面也是 Google Cloud 的主要亮点之一:它为你提供了一个 web 控制台,基本上你可以在这里使用一个与所有服务连接的 ssh 终端,直接操作,而无需在本地机器上做任何配置。界面中的另一个优点是它使用了传统系统管理员领域的术语,使得大多数服务的学习曲线变得容易。

与 AWS 类似,Google Cloud Platform 允许工程师在全球各地的区域和可用区创建资源,以确保我们系统的高可用性,并遵守当地的法律法规。

但真正的皇冠上的明珠是他们的容器引擎。我是容器编排的忠实粉丝。如今,每个人都在朝着基于微服务的系统发展,看到公司遇到微服务系统操作现实的瓶颈并不奇怪:没有编排工具,这几乎是无法管理的。在市场上所有潜在的选择中(Amazon ECS、Docker Swarm 和 DCOS),有一个特别的工具改变了我的生活:Kubernetes。

Kubernetes 是我在撰写第一本书(使用 Node.js 开发微服务)时提出的一个问题的答案:如何通过为开发和运维提供一个共同的基础,有效地自动化微服务环境中的操作?Kubernetes 融合了 Google 多年来在使用容器方面积累的所有专业经验,创造了一个产品,提供了有效管理部署管道所需的所有组件。

在本书中,我们将特别强调 Kubernetes,因为在我看来,它是今天许多团队在成员和资源扩展过程中遇到问题的解决方案。

为了开始使用 GCP,Google 提供了 300 美元的试用信用或 60 天的免费试用,这足以让你了解大部分服务,当然,也足够跟随本书中的示例,玩转我们将要介绍的大多数概念。我建议你激活试用期并开始尝试:一旦信用用完或 60 天试用结束,Google 会要求明确确认启动计费,因此你的账户不会产生额外费用(在撰写本书时是这样的情况)。

Google 计算引擎

Google Compute Engine 相当于 Amazon Web Services 中的 EC2。它允许您以一种我前所未见的简便方式管理机器实例、网络和存储。在与 AWS 上手时,我发现的一个缺点是,他们创建了一些命名抽象,名称并不直观:虚拟私有云、弹性块存储等。虽然这并不是什么大问题,因为 AWS 在市场中非常知名,但 Google 领会了这一点,并且以非常直观的方式命名其资源,使得新用户几乎无需任何努力即可快速上手该平台。

关于机器类型,与 AWS 相比,Google Cloud Platform 提供了一组简化且有限的机器类型,但足够多样化以满足我们的需求。需要注意的 Google Cloud Platform 特性之一是,硬件随着实例大小的增加而提升,这意味着 64 核心的机器比 2 核心的机器拥有更强的 CPU。

Google Cloud Platform 还提供了一个 CLI 工具,可以通过终端与 GCP 资源进行交互。要安装它,只需访问此网址:cloud.google.com/sdk/

然后,根据您的操作系统,按照说明进行操作。

标准机器类型

标准机器是任何应用程序中最常用的类型。它们在 CPU 和内存之间提供平衡,适合所有项目中的大多数任务。这些类型的机器为每个虚拟 CPU 提供 3.75 GB 的内存。让我们看几个示例:

名称 CPU 内存
n1-standard-1 1 3.75 GB
n1-standard-2 2 7.50 GB
n1-standard-64 64 240 GB

如您所见,命名约定非常简单,容易根据规范名称推测机器的内存和 CPU 数量。

高内存机器类型

这些机器经过优化,适用于内存密集型应用程序。每个虚拟 CPU 配备了额外的内存,使您可以在内存方面做到更加出色。

每台高内存类型的机器都配备每个虚拟 CPU 6.5 GB 的内存,以下是一些示例:

名称 CPU 内存
n1-highmem-2 2 13
n1-highmem-8 8 52
n1-highmem-64 64 416

这些机器配备大量内存,非常适合分布式缓存、数据库以及许多其他需要相对较高内存消耗的应用程序。

高 CPU 机器类型

如其名称所示,高 CPU 型机器是具有高 CPU/内存比率的实例,每个虚拟 CPU 配备 0.9 GB 的内存,这意味着它们非常适合在高强度 CPU 任务中节省一些开销(因为我们减少了大量内存)。以下是这些机器的一些示例:

名称 CPU 内存
n1-highcpu-2 2 1.8 GB
n1-highcpu-8 8 7.2 GB
n1-highcpu-64 64 57.6 GB

正如你所看到的,标准机器和高内存机器之间的唯一区别在于,这些机器配置了较少的 RAM,这使得我们在一些应用中可以节省不需要的资源,尤其是当这些应用能够在相同价格下创建更多 CPU 的机器时。高 CPU 机器非常适合需要高 CPU 和低内存消耗的应用,比如数学处理或其他类型的计算。

共享核心机器类型

有时,我们真的不需要为我们的流程提供专用的机器,因此 Google Cloud 提供了可以用作共享机器的选项。在我看来,分享核心的机器不适合生产使用,但它们非常适合用作原型或实验不同资源。以下是两种类型的机器:

名称 CPU 内存
f1-micro 0.2 0.6
g1-small 0.5 1.7

正如你所看到的,这里只有两种选项,它们在 RAM 和 CPU 功率上有所不同。我个人在想要尝试新的软件或 Google Cloud Platform 的新产品时,通常会使用这些机器。

别忘了,这些是突发型机器,只适合短时间内的高强度处理,不适合持续消耗资源,因为 CPU 是在不同应用之间共享的。

自定义机器和 GPU 处理

有时,我们的机器需要额外的资源,这些资源通常不在其他提供商预定义的机器实例中,但在这种情况下,Google Cloud Platform 通过一个令人惊叹的功能来解救我们:自定义机器类型。

在 Google Cloud Platform 上,使用自定义机器类型,我们可以在资源较少的机器上获得大型机器的硬件升级优势,或者创建适合我们需求的特定配置。

我们可以找到自定义机器的一个最佳例子是,当我们希望在我们的配置中加入 GPU 处理时。在 Google Cloud 中,GPU 可以按需附加到任何非共享(f1g1)的机器上。借助创建自定义机器类型的能力,我们可以定义希望在其中提升处理能力的 GPU 数量。

总的来说,当我设计系统时,我尽量坚持使用标准类型,以简化我的设置,但创建自定义机器类型并没有错,唯一的问题是,我们可能会轻易陷入过早优化系统的问题,这也是 IT 工作中最常见的难题之一。

启动实例

在 Google Cloud Platform 中,一切都被组织在项目中。为了创建资源,你需要将它们与项目关联,因此启动实例的第一步是创建一个项目。为此,只需在第一次进入 Google Cloud Platform 界面时选择“新建项目”按钮,或者在已经创建了一个项目后,点击顶部栏的下拉菜单:

  1. 对于本书的示例,我将创建一个名为Implementing Modern DevOps的项目,并用它来运行所有示例:

  1. 一旦我们创建了项目,就可以继续创建新的虚拟机实例。虽然可以创建超过 64 个核心的实例(通过自定义机器类型),但为了节省成本,我们将使用标准机器类型。继续使用默认值创建实例(只需更改名称):

  1. 我非常喜欢 Google Cloud Platform 的两个特点:

    • 他们如何轻松地命名资源,并使一切都变得清晰易懂

    • 他们在定价方面的透明度

  2. 在 Google Cloud Platform 上创建虚拟机时,存在这两个特点:创建机器的表单只有少数几个字段,并且会显示机器每月的费用(所以不会有意外费用)。

  3. 和 AWS 一样,Google Cloud Platform 允许你选择实例将要部署的区域和可用区(记住,这是一个物理隔离的数据中心),以确保系统的高可用性。

  4. 此外(不在前面的图中),它允许你通过勾选两个复选框,只需几个点击即可允许httphttps流量进入实例。这既简单又有效。

  5. 你还可以配置其他内容,如网络、SSH 密钥以及其他我们暂时跳过的参数。只需点击创建按钮(表单底部),然后等待机器完全配置(可能需要几分钟),你应该会看到类似于以下截图的内容:

  1. Google Cloud Platform 最吸引人的特点之一是其用户界面的精心设计。在这种情况下,你可以看到机器描述中有一个名为“连接”的列,允许你通过几种不同的方式连接到机器:

    • SSH

    • gcloud命令(GCP 的命令行工具)

    • 使用另一个 SSH 客户端

  2. 我们将选择 SSH(默认选项),并点击 SSH 按钮。屏幕上应该会弹出一个窗口,几秒钟后,我们应该能看到类似于ssh终端的界面,它是我们机器上的终端:

这是一个非常简洁且实用的功能,它基本上使工程师避免携带一组加密密钥,因为加密密钥总是存在泄露的风险,一旦泄露,你的机器就会暴露。

网络设置

我不能强调 Google Cloud Platform 如何简化概念并使其看起来类似于现实世界的物理数据中心概念这一点。这在网络设置中同样适用:所有的概念和名称都可以一一映射到现实世界的物理网络概念上。

在 Google Cloud 中,我们可以通过几次点击实现任何符合 IP 网络设计原则的需求(与 AWS 相同)。Google Cloud 还提供了一个有趣的功能(与 AWS 等其他提供商一样),即可以通过 VPN 网络将数据中心扩展到云中,既能享受云产品的好处,又能达到最敏感数据所需的安全性。

Google 容器引擎

Google 容器引擎 (GKE) 是 Google 提出的一个容器编排方案,利用市场上最强大的容器集群之一:Kubernetes。

如我们在第七章中将进一步讨论的那样,Docker Swarm 和 Kubernetes——集群基础设施,Kubernetes 是一个功能丰富的集群,专门用于以受控方式部署和扩展基于容器的应用程序,特别强调定义开发和运维之间的共同语言:一个将开发和运维概念融合为共同基础的框架:资源的 YAML(或 JSON)描述。

Kubernetes 的一个大问题是确保高可用性。当你在本地或云提供商上部署集群时,利用计算资源(AWS 中的 EC2 或 GCP 中的 Compute Engine),你需要负责升级集群版本,并随着 Kubernetes 的新版本发布而不断演进。在这种情况下,Google Cloud Platform 通过容器引擎解决了操作问题:GCP 负责保持主节点的更新,而用户在 Kubernetes 发布新版本时升级节点,这使得我们能够制定不同的集群升级程序。

设置集群

在第七章中,Docker Swarm 和 Kubernetes——集群基础设施,你将学习如何操作 Kubernetes,但值得在本章中教你如何在 GKE 中设置集群,以便在深入了解 Kubernetes 的核心概念之前,先展示设置集群的简便性:

  1. 首先,进入 Google Cloud Platform 中的容器引擎:

  1. 如你所见,目前没有设置任何集群,所以我们有两个选择:

    • 创建一个新的容器集群

    • 快速开始

  2. 我们只需点击“创建一个容器集群”,然后按照屏幕上的指示(表单)设置我们的集群:

  1. 确保区域(Zone)靠近你的地理位置(尽管现在这并不重要),并且大小设置为 3。这个参数——大小——将要求 GCP 在计算引擎中创建 3 个实例来设置集群,并由 GCP 自行管理一个主节点。关于镜像,我们在这里有两个选项,gcicontainer-vm。在这种情况下,实际上并不重要,因为这是一个测试集群,但请注意,如果你想使用 NFS 或其他高级文件系统,你将需要使用 container-vm

  2. 点击创建,几分钟后,你应该会看到两样东西:

    • 集群是在 Google 容器引擎部分创建的

    • 三个新的虚拟机在计算引擎部分配置

  3. 这是一个非常智能的设置,因为通过一些使用 Google Cloud Platform 命令工具(gcloud)的命令,我们可以对集群进行扩展或缩减,还可以调整实例的大小以满足我们的需求。如果你探索集群(点击它的名称),你会发现一个“连接到集群”的链接,它会引导你到一个包含连接 Kubernetes 仪表板的说明的页面。

  1. 有时,这些指令会失败,因为 gcloud 配置不当。如果你在配置集群访问时遇到错误,运行以下命令:
 gcloud auth login <your email>
  1. 然后,按照说明操作。假设你已经配置好了 Google Cloud SDK,一切应该正常工作,运行 kubectl proxy 命令后,你应该能够通过 http://localhost:8001/ui 访问 Kubernetes 仪表板。

  2. 为了测试是否一切按预期工作,只需在 Kubernetes 中运行一个简单的镜像(在此案例中是 busybox 镜像):

 kubectl run -i busybox --image=busybox
  1. 如果我们在运行 Kubernetes 代理时(如前所述)刷新仪表板 (http://localhost:8001/ui),我们应该看到与以下图像相似的内容,位于“部署”部分:

这表明部署(一个 Kubernetes 概念,我们将在第七章,Docker Swarm 和 Kubernetes - 集群基础设施中探讨)成功。

其他 Google Cloud Platform 产品

Google Cloud 平台不仅有计算引擎和容器引擎,它还是一系列非常有趣的服务集合,适用于不同的用途。由于涉及的范围有限,我们不会看到大多数服务,只会集中于与 DevOps 相关的服务。

Google App Engine

直到现在,我们一直在使用 DevOps 领域的一个方面,即IaaS。Google Cloud 平台还提供了一种名为平台即服务PaaS)的服务。在 IaaS 模型中,我们不需要担心底层基础设施:配置机器、安装软件、修补软件。使用Google App Engine(或任何其他主要的 PaaS),我们可以忘记基础设施的操作,专注于应用程序的开发,将底层基础设施交给 Google 处理。我们不需要启动机器并安装 Java 来运行基于 Spring Boot 的应用程序,而是只需指定我们希望运行一个 Java 应用程序,GCP 会处理其他一切。

这款产品——Google App Engine,满足了大多数小型到中型项目的需求,但在本书中,我们将重点讨论维护 IaaS 所涉及的 DevOps。

Google App Engine 还为我们提供了诸如用户管理等功能,这是所有应用程序中经常遇到的问题。

机器学习 API

Google 一直以其在技术产品上的创新而闻名。它改变了人们使用电子邮件的方式(通过 Gmail),也改变了人们使用手机的方式(通过 Android)。

关于机器学习,它们也通过一套创新的 API 震撼了世界,用户可以使用这些 API 来处理图像(使用视觉 API)、翻译文档(使用翻译 API),并通过自然语言 API 分析大量文本数据。

我看到的一个关于视觉 API 的最惊人的用法是,一家公司需要为其客户做一定程度的照片 ID 验证。问题是,很多人上传无效图像(随机图像,甚至是部分面部被遮挡或类似的图像),于是我们使用了视觉 API 来识别那些包含面部且没有胡须、帽子或任何配饰(除了眼镜)的图像。

结果是,做身份验证的人只专注于有效的图像,而不必在继续验证之前对其进行有效或无效的分类。

大数据

大数据现在是一个重要的领域。每个人都在努力利用大数据探索新商业领域,或在传统业务中释放其潜力。

Google Cloud Platform 提供了一套大数据 API,使用户能够处理几乎任何大数据任务。通过像 BigQuery 这样的工具,数据分析师可以在几秒钟内对 TB 级别的信息进行查询,而无需设置大规模的基础设施。

一般来说,来自 Google 的大数据 API 被称为 DevOps 世界中的“无操作工具”(no-ops tools):它们不需要用户进行维护,因为这些工具已经被集成到 Google 中。这意味着,如果一个大查询需要大量的处理能力,Google 会负责透明地将这些计算能力提供给用户。

其他云服务提供商

不幸的是,我们在一本书中能讲解的概念有限,因此这次我们将重点介绍 AWS 和 GCP,因为它们是市场上功能最全的云服务提供商。

我总是尝试以开放的心态看待技术,以下是我认为你应该了解的三个云服务提供商:

  • DigitalOcean

  • Heroku

  • Azure

他们提供的服务非常丰富,且都紧跟 DevOps 和安全的新趋势。

Heroku

Heroku 的核心理念就是这句话:构建应用,而非基础设施。这是一个非常有力的信息。基本上,Heroku 全力推动PaaS(平台即服务)概念,让你避免维护底层基础设施:只需指定你想要运行的内容(例如,一个 Node.js 应用程序)以及规模。

通过这一强大的理念,Heroku 让你轻松地通过几个点击部署应用程序实例、数据库或通信总线(例如 Kafka),而无需像使用 DevOps 工具(如 Ansible、Chef 或类似工具)那样进行繁琐的配置。

Heroku 是初创公司首选的云服务提供商之一,因为相比使用 AWS 或 Google Cloud Platform,你可以节省大量时间,因为你只需专注于应用程序,而非基础设施。

DigitalOcean

DigitalOcean 是一个即使不如 AWS 或 GCP 知名,但为中小型组织提供了一个非常有趣的替代方案的云服务提供商。他们开发了一个非常强大的概念:droplet。

基本上,droplet 是一个可以运行你的软件并通过某些配置连接到不同网络(私有或公共)的组件。

为了创建一个 droplet,我们只需要定义几个内容:

  • 镜像(操作系统或一键镜像)

  • 尺寸

  • 区域

一旦你选择了配置,droplet 便开始运行。这非常简单有效,这也是公司通常所追求的。

Azure

Azure 是微软推出的云系统平台,也是过去几年增长最快的云服务提供商之一。正如预期的那样,Azure 是运行基于 Windows 的应用程序的特别优秀平台,但这并不意味着我们可以忽视它运行 Linux 应用程序的能力。

他们的产品目录和 AWS 或 Google Cloud Platform 一样全面,完全没有理由不选择 Azure 作为你的系统云服务提供商。

Azure 也是市场上最年轻的云服务提供商之一(自 2013 年广泛推出),因此它有一个优势,就是能够解决其他提供商所面临的问题。

摘要

到目前为止,我们展示了 AWS 和 GCP 的特点,并介绍了一些在构建系统时非常有趣的其他提供商。市场上有众多竞争者的一个优点是,每个提供商都有自己的强项,我们可以通过使用 VPN 将它们组合起来,在不同的提供商之间创建一个庞大且扩展的虚拟数据中心。

在本书的其余部分,我们将特别关注 AWS 和 GCP,因为它们对于一本 DevOps 书籍来说具有最有趣的特点(当然不能忽视其他云服务提供商,但请记住,篇幅有限)。

我们还将特别关注容器集群,例如 Kubernetes 或 Docker Swarm,因为它们无疑是未来的趋势。

第三章:Docker

多年来,开发和运维之间的接触点一直是将新版本应用程序部署到生产环境时的一个问题源。不同的编程语言生成不同类型的制品(Java 的 war 或 JAR,Node.js 的源代码),这导致了在推出新版本时程序的异构性。

这种异构性导致了定制化的版本发布解决方案,这些解决方案往往像是巫术,带有奇怪的习惯,例如凌晨 4 点发布以避免系统中断,或创建容易出错的 bash 脚本,这些脚本比软件本身更难维护。问题除了复杂性外,还在于新员工需要适应你的系统,这总是引入我们大多数时候未意识到的风险,直到某些事情出错。

Docker 来拯救我们。有了 Docker,我们可以生成一个可部署的制品,这不仅仅是你构建的软件,还有它的运行时。如果你要部署一个 Java 应用程序,通过 Docker,你将打包应用程序和将要运行你的应用程序的 Java 版本。

听起来像是一个梦想:一个受控环境,从开发到 QA,再到生产(有时会在预生产环境进行检查)作为一个制品被推广,这个过程是可重复的,唯一在不同环境间变化的是配置,通常是通过环境变量注入的。这不是梦想;这是 2017 年的现实,在本章中,我们将快速学习如何在 Docker 中运行容器并构建镜像,速度快得如同光速。

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

  • Docker 架构

  • Docker 客户端

  • 构建docker images

  • Docker 注册中心

  • 卷(Volumes)

  • Docker 网络

  • Docker Compose

我们还将介绍docker-compose,这是一个用于组合多个容器的工具,这样我们就可以在开发机器上组成我们的系统,模拟生产配置,或者至少接近组件间的连接,但在此之前,我们还将深入探讨 Docker 网络:我们如何选择最合适的网络来支持我们的系统,以及 Docker 提供的不同网络之间的主要区别是什么?

Docker 的另一个有趣特点是镜像的构建方式:基本上,我们选择一个基础镜像(我们将了解如何构建一个),然后通过一组精简的命令,我们可以构建一个 Docker 文件,这基本上是一个脚本,用于指导 Docker 根据我们需要的配置来构建我们的镜像。

Docker 架构

我学习的一种偏好方式是通过实验。为了说明 Docker 架构,我们将展示一个示例,但首先,我们需要安装 Docker 本身。在这个案例中,我正在使用 Mac,但在docs.docker.com/engine/installation/上,你可以找到适合你需求的发行版,并且有一套非常清晰的安装说明(通常是需要安装的包)。

安装 Docker 后,运行以下命令:

docker run hello-world

一旦完成,输出应该和以下内容非常相似:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
78445dd45222: Pull complete
Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
Status: Downloaded newer image for hello-world:latest
Hello from Docker!

这条消息显示你的安装似乎工作正常。

为了生成这条消息,Docker 执行了以下步骤:

  1. Docker 客户端联系了 Docker 守护进程。

  2. Docker 守护进程从 Docker Hub 拉取了 hello-world 镜像。

  3. Docker 守护进程从该镜像创建了一个新容器,运行生成当前输出的可执行文件。

  4. Docker 守护进程将输出流传输到 Docker 客户端,然后客户端将其发送到你的终端。

如果你想尝试更具挑战性的操作,可以运行一个 Ubuntu 容器,命令如下:

$ docker run -it ubuntu bash

使用免费的 Docker ID,在cloud.docker.com/上分享镜像,自动化工作流,等等。

获取更多示例和想法,访问:docs.docker.com/engine/userguide/

如你所见,hello-world 镜像向你展示了运行前面命令时发生的情况。

这里引入了一些新概念:

  • Docker Hub:这是一个中央仓库,既有公共也有私有部分,用户可以将本地构建的镜像推送到这里。Docker 镜像库用于在不同的部署阶段(甚至在不同系统之间)传输镜像。

  • :Docker 镜像由层组成。层基本上是有序的文件系统差异。Docker 镜像是这些层的堆叠,最终形成镜像。当你更改一个已有镜像中的文件时,会创建一个新层,但镜像的其余层会被复用,因此我们可以节省大量(相信我,真的是很多)空间。

  • Docker 守护进程:Docker 遵循客户端-服务器架构。在这种情况下,Docker 守护进程是服务器端,可以通过表现状态转移REST)API 进行操作。

  • Docker 客户端:Docker 客户端是一个命令行接口CLI),用于操作 Docker 守护进程。它可以是本地的,也可以是远程的。

最后三个概念是草拟 Docker 架构的关键。请查看以下图示:

客户端/服务器架构在软件中占主导地位。你可能认为这种架构对于像 Docker 这样的系统来说是过度设计,但它实际上为你提供了很多灵活性。例如,在前面的图示中,我们可以看到 Docker CLI(客户端)如何管理本地的 Docker 守护进程实例,同时也能通过设置一个名为 DOCKER_HOST 的环境变量与远程守护进程通信,在此案例中,DOCKER_HOST 的值为 62.112.42.57

Docker 的一个关键点是它完全利用了 Linux 内核的虚拟化,使得目前无法在 Windows 或 Mac 上运行 Docker(因为它使用了 Linux 内核的能力)。解决方案是创建一个运行 Linux 的虚拟机来运行 Docker 守护进程,CLI 会与虚拟机通信来执行 Docker 命令。

例如,在 Mac 上,Docker 的旧版本使用名为 Boot2Docker 的分发版来运行 Docker 守护进程,而 Docker 的新版本则使用名为 HyperKit 的轻量级虚拟化解决方案。

Docker for Windows 使用一种与 Mac 上相同的虚拟化技术,因此对于 Mac 所做的所有假设也适用于 Windows。

Docker 内部结构

到目前为止,我们已经了解了 Docker 在整体架构方面的工作原理,但 Docker 守护进程在操作系统层面上发生了什么呢?

粗略地说,Docker 为你的应用程序提供了一个运行时环境:你可以限制容器使用的核心数量和内存量,但归根结底,运行容器的内核和运行宿主机的内核是一样的。

证明这一点的是 Docker 组织镜像的方式:它计算文件系统的差异,并将其打包成可以重用的层。我们来拉取一个相当大的镜像(不是前面示例中的 hello-world):

docker pull ubuntu

这将产生以下输出:

Using default tag: latest
latest: Pulling from library/ubuntu
d54efb8db41d: Pull complete
f8b845f45a87: Pull complete
e8db7bf7c39f: Pull complete
9654c40e9079: Pull complete
6d9ef359eaaa: Pull complete
Digest: sha256:dd7808d8792c9841d0b460122f1acf0a2dd1f56404f8d1e56298048885e45535
Status: Downloaded newer image for ubuntu:latest

如你所见,Docker 已经拉取了五个层次,这基本上告诉我们 Ubuntu 镜像是通过五个步骤构建的(虽然不完全正确,但这是一个很好的方式)。现在,我们将运行一个 Ubuntu 实例。在 Docker 中,镜像的一个实例就是我们所说的容器,镜像和容器之间的主要区别是顶部的可写层(在 Docker 中,层是以只读模式堆叠起来组成镜像的,就像几个补丁文件中的差异)。让我们来演示一下:

docker run -it ubuntu /bin/bash

前面的命令在 Ubuntu 镜像的实例中运行 /bin/bashit 标志让你像使用虚拟机一样使用容器,分配一个虚拟终端(t 标志)并创建交互式会话(i 标志)。现在,你可以看到你的提示符已经变成了如下所示:

root@329b2f9332d5:/#

它不一定完全相同,但应该相似。请注意,你的提示符现在是一个 root 提示符,但不要太兴奋;它仅仅是在容器内。

创建一个文件来更改文件系统:

touch test.txt

现在可以使用exit命令断开与容器的连接。

正如您所看到的,提示符已返回到您的系统提示符,如果您运行docker ps,您会看到没有正在运行的容器,但是如果您运行docker ps -a(显示所有容器,而不仅仅是正在运行的容器),您应该看到类似以下内容:

这是一个已从镜像创建但不再运行的容器。正如我们之前所说,此容器与镜像之间唯一的区别是顶层可写层。为了证明这一点,我们将从几分钟前运行的容器中创建一个新的镜像:

docker commit 329b2f9332d5 my-ubuntu

在这种情况下,我正在使用引用329b。因为它是前面图片(docker ps -a的输出)中显示的,但您需要将哈希更改为您的输出中显示的哈希。公平地说,您不需要全部输入,只需几个字符即可完成任务。如果一切顺利,该命令应输出一个SHA256校验和,并将控制权返回给您。现在运行docker images(列出 Docker 中的镜像),输出应类似于以下内容:

正如您所看到的,这里有一个名为my-ubuntu的新镜像,我们刚刚创建。

现在我们想要检查ubuntu镜像和my-ubuntu镜像之间的差异。为了做到这一点,我们需要检查每个镜像的层并查看差异。我们要使用的命令是docker history,并将镜像名称作为第三个参数。

首先,对于ubuntu镜像:

然后对于my-ubuntu:镜像(刚从ubuntu创建):

非常有启发性。镜像my-ubuntuubuntu完全相同,只是顶层可写层我们刚刚通过登录到机器并创建文件创建的。这非常聪明,因为尽管这两个镜像都使用大约 130 MB 的空间,但第二个镜像额外使用的空间只是在本例中使用的顶层,仅为 5 字节,导致这两个镜像使用的是 130 MB 和 5 字节的空间。这也产生了我们之前讨论过的副作用:容器与镜像完全相同,只是顶层可写层不同,因此运行容器的实例只使用 5 字节的空间。正如您所看到的,创建 Docker 的工程师考虑到了一切!

Docker 如何将镜像存储在硬盘上的方式是存储驱动程序的责任:Docker 可以使用不同的驱动程序以不同的方式(以及位置,例如 AWS 中的 S3)存储镜像,但是最常见的用例,即默认驱动程序,将镜像存储在硬盘上,并为每个层创建一个文件,文件名为层的校验和。

Docker 客户端

我们在前一节中已经使用了 Docker 客户端,但我们需要深入了解 Docker CLI 可以提供的选项。我最喜欢的学习方式是通过实验,而我们将在这一节中从上到下建立概念(更多是拆解,而非构建),所以我建议你按顺序完整阅读这一节,不要跳过任何部分,因为后面的示例将会基于前面的内容。

如果你之前稍微了解过 Docker,你会发现这些命令非常冗长,并不像你想象的那么直观。最常见的用例是以下组合:

docker run -i -t <docker-image>

这个命令基本上做了一件简单的事:它以交互模式运行一个容器并分配伪终端。这使得我们能够与容器交互并执行命令(并非每个镜像都适用,但对于所有 Linux 发行版的基础镜像来说是成立的)。让我们看看这意味着什么:

docker run -i -t ubuntu

这应该会返回一个类似于以下的提示:

root@248ff3bcedc3:/#

刚刚发生了什么?提示符变成了 root,并且主机部分有一个奇怪的数字。我们现在在容器内。基本上,现在我们可以运行将在容器内执行的命令。要退出容器,只需键入exit,控制权将返回到主机的终端,容器会继续在后台运行。

大多数时候,前面的命令就能满足我们的需求,但有时我们希望将容器运行在后台:假设你启动了一个 Jenkins 服务器,并且不想让终端一直挂在它上面。为了做到这一点,我们只需要添加-d选项(守护进程模式),并去掉-i-t选项:

docker run -d jenkins

一旦镜像被拉取并开始运行,控制权就会返回到你的终端。输出的最后一行应该是类似下面的一串长字符:

9f6a33eb6bda4c4e050f3a5dd113b717f07cc97e2fdc5e2c73a2d16613bd540b

这是正在运行的容器的哈希值。如果你执行docker ps,将会产生类似的输出:

注意,截图中CONTAINER ID下的值与前一个命令的哈希值的前几位数字相匹配。

从理论上讲,我们已经运行了一个 Jenkins 实例,如前面的图片所示,它正在监听端口8080和端口50000。让我们尝试用浏览器访问http://localhost:8080。没有反应。基本上,我们的浏览器无法打开该 URL。

这是因为我们没有告诉 Docker 将容器端口绑定到主机机器的本地端口。为了做到这一点,我们需要先停止容器,然后带上一个特殊的参数重新启动它。

现在是时候学习如何停止容器了。我们这里有两个选项:

  • 停止容器:通过停止选项,我们向容器中的主进程发送SIGTERM信号,并等待它完成(有一个宽限期)。然后,我们发送SIGKILL信号。

  • 杀死容器:使用 kill 选项时,我们向容器中的主进程发送 SIGKILL 信号,这会强制容器立即退出,而无法保存状态。

在这种情况下,选择哪个并不重要,但请小心。当你在生产环境中运行时,确保在执行此操作前可以安全地停止容器,因为使用停止选项时,我们允许正在运行的软件保存当前的事务并优雅地退出。在这种情况下,我将要杀死容器:

docker kill 9f6a

Docker 很聪明。我不需要指定完整的容器标识符,仅用几个字符,Docker 就能识别容器(或在其他命令中识别镜像)并将其杀死。

如果你记得之前的例子,当我们杀死一个容器时,会留下一个层,进入一个 dead 状态的容器,我们可以通过添加 -a 选项来探索它,使用 docker ps 命令。对于这个例子,我们也将使用以下命令删除这个层:

docker rm 9f6a

就这样。容器在我们的主机中从未存在过。

现在,回到 Jenkins 的例子,我们希望以一种能够从浏览器访问正在运行实例的方式来运行 Jenkins。让我们修改一下前面的命令并解释原因:

docker run -p 8080:8080 -p 50000 -d jenkins

几秒钟后,如果我们在浏览器中访问 http://localhost:8080,我们应该能看到 Jenkins 的初始配置页面,它会要求输入初始密码才能继续。

让我们先解释一下前面的命令。我们可以看到一个新选项:-p。如你所料,-p 是来自端口。实际上,你可以将 -p 改为 --port,一切照常运行。使用 -p 选项,我们将主机(你的机器)上的端口映射到容器中。在这个例子中,我们将主机的端口 8080 映射到容器的端口 8080,将主机的端口 50000 映射到容器的端口 50000,但如果我们想映射不同的主机端口,该怎么做呢?嗯,这其实非常简单:

docker run -p 8081:8080 -p 50001:50000 -d jenkins

运行上述命令后,我们有两个 Jenkins 实例在运行:

  • 第一个实例暴露在你机器的端口 8080 上。

  • 第二个实例暴露在你机器的端口 80801 上。

请注意,尽管我们没有使用端口 50000,我将其更改为 50001,因为你的机器上的端口 50000 已经被我们之前运行的第一个 Jenkins 实例占用。

如你所见,Jenkins 要求输入密码,而 http://localhost:8080 的初始网页上写明密码可以在日志或文件系统中找到。重点是日志,使用 Docker,我们可以随时提取由守护进程注册的任何容器的日志。让我们尝试一下:

docker logs 11872

在我的情况下,运行在端口 80801 上的 Jenkins 实例的 ID 以 11872 开头。执行上述命令应该能提取 Jenkins 的启动日志,我们可以用来进行故障排除,或者在此情况下,恢复用于初始化 Jenkins 的密码。

Docker 中另一个有趣且常见的选项是将环境变量传递给容器内运行的应用程序。如果你仔细想想,配置 Docker 容器内的应用程序只有三种方式:

  • 环境变量

  • 一个包含数据的卷

  • 从网络获取配置

让我们看一下来自 Docker Hub 的官方 MySQL 镜像:

MySQL 是一个流行的数据库服务器,它也已经被 dockerized(容器化)。如果你稍微浏览一下文档,你会发现其中的一个配置选项是 MySQL 数据库的 root 密码。公平地说,快速入门示例指向了正确的方向:

docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql

这里的新选项是 -e。这个选项允许你将环境变量传递给容器,并指定你希望设置的值,格式为 = 后跟值。运行完前面的命令后,我们将执行另一个命令:

docker inspect caa40cc7d45f

在这个例子中,caa40cc7d45f 是在我的机器上运行 MySQL 时得到的 ID(你可能会得到不同的 ID)。终端中应该会输出一个巨大的 JSON,但其中有一个特别的部分,Config,里面有一个子部分叫做 Env,它应该与以下内容非常相似:

...
"Env": [
   "MYSQL_ROOT_PASSWORD=my-secret-pw",
   "no_proxy=*.local, 169.254/16",
   "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   "GOSU_VERSION=1.7",
   "MYSQL_MAJOR=5.7",
   "MYSQL_VERSION=5.7.17-1debian8"
 ],
...

就是这样。我们之前传递的环境变量 MYSQL_ROOT_PASSWORD 现在可以作为环境变量在容器内访问。

docker inspect 命令中,有很多非常有价值的信息。只要浏览一下,你可能会对其中的大部分信息感到熟悉:它主要是 Linux 术语。

到目前为止,我们已经浏览了截至 2017 年 1 月最常见的命令。如你所知,软件的发展非常迅速,到你阅读这本书时,Docker 已经加入了新的版本(比如 secrets)。检查当前情况的最佳方式是通过 www.docker.com 上的文档,在我看来,它相当全面。你当前 Docker 安装的命令参考也可以通过 docker help 命令获得。

构建 Docker 镜像

在前面的章节中,我们使用 Docker 的 commit 命令构建了一个镜像。虽然这方法可行,但我看到一个大问题:它不可重复。当镜像中的软件由于新漏洞或版本更新被修补时,没有一种简单的方法可以反复重建镜像。

为了解决这个问题,Docker 提供了一种更好的构建镜像的方式:Dockerfile。

Dockerfile 是一个包含一组有序命令的文件,这些命令可以将镜像准备好以供使用。诸如安装软件、升级内核版本以及添加用户等常见操作都可以在 Dockerfile 中进行。让我们来看一个例子:

FROM node:latest

RUN mkdir -p /app/
WORKDIR /app/

COPY package.json /app/
RUN npm install

COPY . /app

EXPOSE 8080
CMD [ "npm", "start" ]

如果你在 IT 领域工作了几年,实际上你不需要解释发生了什么,但我们还是确保大家都在同一页面:

  • 我们是基于最新的 Node.js 镜像创建镜像。

  • /app 中创建了一个新文件夹。我们的应用将安装在那里。

  • 工作目录被设置为这个新文件夹。

  • 复制 package.json 并安装 Node.js 依赖项。记住,我们已经将工作目录设置为 /app,所以 RUN 命令将在 /app 文件夹中执行。

  • 复制剩余的源代码。

  • 将端口 8080 暴露给外部世界。

  • 运行 npm start

一旦做几次就会变得非常简单。需要记住的一点,可能会让初学者抓狂的是:CMDRUN 的区别。

在前面的 Dockerfile 中,我们有时使用 RUN,有时使用 CMD,但它们似乎执行相同的操作:运行一个命令。其实有一个区别:

  • RUN:在构建镜像时运行此命令。

  • CMD:这将在基于生成的镜像启动的容器中运行该命令。

此外,RUN(通常)会创建一个新的层,而 CMD 使用容器的可写层。

现在,是时候测试之前的 Dockerfile 了。在构建镜像之前,我们需要构建一个小的 Node.js 应用程序,这个程序将作为镜像中运行的软件。创建一个新文件夹并添加以下三个文件:

  • package.json

  • index.js

  • Dockerfile(之前的那个)

package.json的内容如下:

{
 "name": "test",
 "version": "1.0.0",
 "description": "Test",
 "main": "index.js",
 "scripts": {
 "start": "node index.js"
 },
 "author": "Test",
 "license": "MIT"
}

index.js 的内容如下:

console.log('Hello world!')

现在,将之前的文件和之前描述的 Dockerfile 放在同一个文件夹中,运行以下命令:

docker build . -t my-node-app

几秒钟后,您的镜像将准备好使用。让我们检查一下。如果您使用docker images命令列出您的镜像,应该可以看到一个名为my-node-app的镜像。现在基于这个镜像创建一个容器:

docker run my-node-app

你应该会看到类似以下的内容:

npm info it worked if it ends with ok
npm info using npm@4.1.2
npm info using node@v7.7.4
npm info lifecycle test@1.0.0~prestart: test@1.0.0
npm info lifecycle test@1.0.0~start: test@1.0.0
> test@1.0.0 start /app
> node index.js
hello world!
npm info lifecycle test@1.0.0~poststart: test@1.0.0
npm info ok

如你所见,在高亮部分,我们的应用程序输出显示在这里。

Dockerfile 参考

如前所述,Dockerfile 非常简单,如果有任何疑问,Dockerfile 语言的官方文档非常详尽。

一般来说,创建 Dockerfile 使用的语言非常类似于几年前 Windows 的批处理语言(.bat 文件)。

让我们看一下最常用的命令:

FROM 该指令用于指定基础镜像。每个 Docker 镜像都是从基础镜像开始创建的(您可以从正在运行的 Linux 发行版创建基础镜像)。
COPY 如你所料,COPY 允许你将文件和文件夹复制到镜像中。例如,我们可以将我们的应用程序、war 文件或任何其他将在镜像分发时一起打包的工件复制进去。

| ADD | 这个指令与 COPY 完全相同,但有三个不同之处:

  • 文件的来源可以是一个在复制之前下载的 URL。

  • 文件的来源可以是一个打包文件(例如 TAR 文件),该文件将在镜像文件系统中解压。

|

RUN 该命令在镜像中运行一个命令。例如,它可以用来在镜像中安装软件。它每次都会在 Docker 镜像中创建一个新的层,因此要小心,并尽量将RUN命令保持在最低限度。
CMD 这是当镜像实例化为容器时运行的默认命令。正如在前面的例子中所看到的,我们使用CMD来执行npm start,该命令会运行node index.js(参考package.json)。它不会创建新层,因为它使用顶部的可写层来存储更改。
ENTRYPOINT ENTRYPOINT类似于CMD,但它会覆盖 docker 镜像中的默认命令/bin/sh -c。为了覆盖指定的入口点,你需要在运行镜像实例时传递--entrypoint标志。ENTRYPOINT非常适合将容器配置为命令行工具,因为你可以将一个相当复杂的命令和复杂的设置打包在一个容器中。
MAINTAINER 使用MAINTAINER,你可以指定镜像的维护者(还可以指定电子邮件)。
EXPOSE 该命令暴露了第一个参数指定的端口,以便容器可以监听该端口。实际上,它并不会在docker客户端主机上暴露该端口,因此用户必须传递-p标志才能访问指定的端口。

使用这些命令,你几乎可以构建任何你想要的内容,尤其是RUN命令,它允许用户在容器内运行任何命令,使我们能够运行脚本(如pythonbashruby)或使用包管理器安装软件。

除了前述指令外,Dockerfile 语言还支持添加环境变量、卷以及其他一些功能,使其功能非常强大。

Docker 仓库

在上一节中,我们创建了一个安装并准备好使用的全新镜像(在这个例子中,是一个非常简单的Hello world Node.js 应用程序)。

现在,我们需要分发这个镜像,以便它可以在我们部署管道的所有阶段安装,或者甚至供其他开发者使用。Docker 不仅适用于运行应用程序,它也是一个非常有趣的选择,用来创建其他开发者也能受益的命令行工具。

为了分发镜像,我们必须依赖导出/导入镜像或使用仓库。仓库基本上是一种软件,允许我们存储和分发 Docker 镜像。仓库有两种类型:

  • 公共仓库

  • 私有仓库

让我们来看看不同的仓库类型。

公共仓库

最著名的公共仓库是 Docker Hub。它是每个 Docker 安装都默认知道的官方仓库。此外,它还提供私有仓库,但最有趣的功能是所有官方镜像都可以在 Docker Hub 上获取。

让我们看看如何使用它。首先,您需要创建一个账户。注册后,创建一个新的仓库:

这个仓库托管了一个名为modern-devops的镜像,我们将向其中推送一个镜像。一旦创建完成,您可以看到 Docker Hub 建议您使用以下命令来拉取镜像:

docker pull dagonzadub/modern-devops

在您的情况下,dagonzadub需要替换成您的用户名。显然,我们不会拉取一个还不存在的镜像,所以我们先推送一个镜像。在前面的部分,我们创建了一个名为my-node-app的镜像。我们将使用这个镜像来测试 Docker Hub。Docker 依赖标签系统来知道将镜像推送到哪里或从哪里拉取。由于我们正在使用default注册表,因此无需指定 URL,但需要指定用户和仓库名称。如果您没有删除之前创建的镜像,请运行以下命令:

docker tag my-node-app dagonzadub/modern-devops

然后,运行这个命令:

docker push dagonzadub/modern-devops

几秒钟后(根据您的上传速度,可能几分钟),您的镜像就会出现在 Docker Hub 上。由于我们将其标记为public,任何人都可以拉取并使用您的镜像。

私有注册表

那么,如果我们想将镜像存储在我们基础设施中的私有注册表里会发生什么呢?

好的,我们有一些选择。如果我们使用云服务提供商,例如 Google Cloud Platform 或 Amazon Web Services,它们提供的 Docker 注册表仅在您的账户内可以访问,您还可以指定镜像所在的区域(记住,我们处理的数据类型可能需要遵守严格的合规规则,规定我们应该存储数据的位置)。

在 AWS 中,容器注册表被称为EC2 容器注册表ECR),在 GCP 中,它被称为容器注册表。如果您的基础设施在这些私有云平台之一,我鼓励您使用它,因为您可以利用平台提供的访问控制。

有时,我们可能会发现自己处于无法使用云服务提供商的情况,因为我们的系统必须部署在本地。这时,我们就需要使用一个私有的本地 Docker 注册表。

现在,市场上有很多选项,但很可能随着越来越多的公司使用 Docker,未来几个月或几年市场会进一步扩大。

在所有的注册表选项中,有三个我特别感兴趣:

Quay:这是目前市场上的一个完整注册表(截至本文写作时)。它有一些有趣的功能,其中可能最有趣的是能够扫描镜像,寻找已安装软件的安全漏洞。它还可以根据您的 git 仓库中的变化构建镜像,因此如果您的 Dockerfile 在 GitHub 中被修改,Quay 会自动触发构建并部署新的镜像版本。Quay 并非免费,使用它需要购买许可证。

注册表:这是一个普通概念的简单名称。它是注册表 API 的官方实现,并以容器的形式打包。它默认没有界面或访问控制,但它完成了工作。它还提供存储管理驱动程序,因此我们可以将镜像部署到 S3 或 Google Cloud Storage 的存储桶中,以及许多其他选项。Registry 是免费的,可以从 Docker Hub 拉取。

Docker Trusted Registry:这是 Docker 企业版的一部分。像几乎所有其他商业注册表一样,它提供静态容器分析以及存储管理驱动程序。Docker Trusted RegistryDTR)不是免费的,因此需要支付许可证费用才能使用。

Docker 数据卷

到目前为止,我们已经看到如何创建镜像,如何将镜像存储到注册表中,以及 Docker 镜像的工作原理(层和容器与镜像的区别)。

任何应用程序的一个重要部分就是存储。通常情况下,Docker 应用程序应该是无状态的,但随着新的编排软件的出现,例如 Kubernetes、Docker Swarm 等,越来越多的工程师正在朝着容器化数据库的方向发展。

Docker 以一种非常优雅的方式解决了这个问题:你可以像使用普通文件夹一样,将本地机器中的文件夹挂载到容器中。

这是一个非常强大的抽象,因为它利用了将数据从容器中推送出来并保存到网络附加存储NAS)或任何其他存储技术的能力(可以使用 Google Cloud Storage 或 S3 中的存储桶作为容器中挂载的数据卷)。

让我们从基础开始。先运行一个 MySQL 数据库:

docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

这有效。它实际上做到了预期的功能:它启动了一个包含 mysql 实例的容器。问题是,所有数据都将写入/var/lib/mysql,而这个文件夹被映射到容器的顶部可写层(记住,在前一节中,我们解释了容器和镜像的区别)。保存数据的唯一方法实际上是提交更改并创建一个新的镜像,但这并不可管理,当然,这不是你想要的做法。想一想:如果你在 Docker 中删除了一个文件,实际上是在顶部层进行的操作,而这个层是唯一可写的,因此实际上你并没有删除文件;你只是把它隐藏了。文件依然存在于某一层中,占用了空间,但不可见。Docker 记录的是差异,每一层本身就是上一层的差异集(想一想 Git 是如何工作的;原理是一样的)。

我们不打算将更改提交到新的镜像中,而是将 docker 主机中的一个文件夹挂载到容器中。让我们稍微修改一下之前的命令:

docker run --name my-mysql-2 -v /home/david/docker/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

现在我们有了一个新的标志-v,后面跟着data:/var/lib/mysql的值。命令的含义非常简单:将/home/david/data文件夹挂载到我的容器的/var/lib/mysql路径下。

如你所料,数据文件夹,在我的例子中是/home/david/data,应该存在于你当前的目录中,因此如果它不存在,请创建它或修改路径以适应你的设置,然后启动容器。这个用例只能通过-v标志实现:将主机中的选定文件夹挂载到容器中。

现在,在数据文件夹中执行ls命令(在 Docker 主机中):

ls /home/david/data

你可以看到mysql实际上已经写入了与启动时创建的数据库对应的数据文件。

Docker 卷不限于每个容器一个,因此你可以根据需要多次使用-v标志,以满足你的需求。

另一种挂载容器与主机之间共享文件夹的方式是直接指定容器内的路径:

docker run --name my-mysql-3 -v /var/lib/myysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

这个命令将会从我们的 Docker 主机挂载一个文件夹到容器中,但在 Docker 主机中的文件夹将由存储驱动程序和docker本身管理:

docker inspect my-mysql-3

输出是熟悉的。我们之前在前面的章节中见过它,但现在我们要寻找不同的信息。我们实际上在寻找一个名为Mounts的部分,类似下面这样(至少类似):

"Mounts": [
 {
 "Type": "volume",
 "Name": "572c2303b8417557072d5dc351f25d152e6947c1129f596f08e7e8d15ea2b220",
 "Source": "/var/lib/docker/volumes/572c2303b8417557072d5dc351f25d152e6947c1129f596f08e7e8d15ea2b220/_data",
 "Destination": "/var/lib/mysql",
 "Driver": "local",
 "Mode": "",
 "RW": true,
 "Propagation": ""
 }
 ]

这也可以通过 Dockerfile 中的VOLUME指令实现。

上面的 JSON 告诉我们哪个本地文件夹将被挂载到容器中(JSON 的Source值),并提供了一个有趣的洞察:卷已经被docker命名(JSON 的Name值)。

这意味着 Docker 跟踪所有(或曾经)挂载在任何容器中的卷,并且可以通过 API 调用列出它们:

docker volume ls

这应该产生类似以下的输出:

DRIVER VOLUME NAME
 local 13b66aa9f9c20c5a82c38563a585c041ea4a832e0b98195c610b4209ebeed444
 local 572c2303b8417557072d5dc351f25d152e6947c1129f596f08e7e8d15ea2b220
 local 695d7cbc47881078f435e466b1dd060be703eda394ccb95bfa7a18f64dc13d41
 local b0f4553586b17b4bd2f888a17ba2334ea0e6cf0776415e20598594feb3e05952

如你所料,我们也可以通过api调用创建卷:

docker volume create modern-devops

这个卷的创建方式与前一个例子相同:由 Docker 来决定将本地机器上的哪个文件夹挂载到容器中的指定路径,但在这种情况下,我们首先创建卷,然后将其挂载到容器。你甚至可以检查这个卷:

docker volume inspect modern-devops

这应该返回类似下面的内容:

[
{
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/modern-devops/_data",
"Name": "modern-devops",
"Options": {},
"Scope": "local"
}
]

现在我们可以使用这个命名资源并将其挂载到我们的容器中,只需引用名称即可:

docker run --name my-mysql-4 -v modern-devops:/var/lib/myysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

最后一个(但同样重要的)有趣用例是在卷中帮助我们在不同的容器间共享配置。想象一下,你有一个相当复杂的设置,导致 Docker 命令非常庞大,带有多个-v。Docker 为我们提供了一个更简单的方式来跨容器共享卷配置:

docker run --name my-mysql-5 --volumes-from my-mysql-4 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

这非常简单直观:my-mysql-5将以my-mysql-4的卷配置启动。

Docker 网络

网络是 Docker 的一个重要部分。默认情况下,Docker 提供了三个网络,我们可以通过执行以下命令来查看它们:

docker network ls

这应该产生类似以下的输出:

让我们解释一下不同的网络:

  • bridge: 这是默认网络。它与主机机器完全不同,具有不同的 IP 范围,在桥接模式下(主机机器作为容器的“路由器”)。创建时未指定网络的容器会附加到默认的桥接网络。

  • host: 在这个网络中,容器与 Docker 主机共享网络堆栈。如果您检查容器中的配置,您会发现它与 Docker 主机中的配置完全相同。

  • none: 这很容易猜到;容器未连接到任何网络,只连接到容器中的回环接口。

现在是看一些示例的时候了。我们将使用 busybox,它是 Docker 映像的“瑞士军刀”。它有几个 Unix 工具,我们可以从中受益,但在这种情况下,我们将从中受益的特性是它是一个功能齐全的 Linux,并且占用空间较小。

让我们运行以下命令:

docker run -it busybox /bin/sh

如果您已经按照前面的部分操作,现在您应该能够理解结果:我们获得了正在运行的容器的 root 访问权限。

接下来的步骤是在容器内部执行 ifconfig 命令。它应该会给我们两个接口:

- eth0 - 172.17.0.2

- lo - 127.0.0.1

IP 可能会改变,但您应该看到这两个接口。将 IP 与 Docker 主机上的 IP 进行比较,我们可以验证容器正在桥接网络中运行,因为 IP 和网络完全不同;在我的情况下,我的 Docker 主机上的 IP 是 10.0.0.12

现在,让我们在另一个终端中使用 busybox 来生成另一个容器:

docker run -it busybox /bin/sh

到目前为止,我们应该有两个正在运行的 busybox 实例,并且它们应该具有连续的 IP 地址,在我的情况下是 172.17.0.2172.17.0.3。如果您回到第一个 busybox 实例的终端,您可以通过 IP 地址对第二个容器进行 ping 测试。这是因为它们都属于(或连接到)相同的网络,即默认的桥接网络。

要在主机网络中运行容器,我们只需将 --network=host 标志传递给 docker run 命令,就可以了;这样我们的容器将与 Docker 主机共享网络堆栈,但是要注意,如果您使用的是 Mac 或 Windows,则 Docker 主机是一个虚拟机,所以不要尝试通过 localhost 访问它;您需要找到运行 docker 的虚拟机的 IP 地址。

用户定义的网络

在 Docker 中也可以创建自定义的隔离网络。从安全角度来看,这是很有意思的,因为它使我们能够在网络层面上隔离不同的容器,从而可以强制执行更高级别的访问控制。

要创建网络,我们只需执行以下命令:

docker network create my-network

就是这样。这是一个简单的方法,但效果如预期。正如您所知,网络是一个复杂的主题,因此 Docker 提供了自定义范围、掩码和其他参数的选项。用户定义的网络类型为桥接。

一旦网络创建完成,你可以在该网络中运行新的容器,如下所示(在一个新终端中):

docker run -it --network=my-network busybox /bin/sh

如预期,这些容器将与其他网络隔离。在这种情况下,这两个容器是启动在 bridge 网络中的。以我的情况为例,第三个容器(刚刚启动的那个)IP 地址是 172.19.0.2,而启动在桥接网络中的两个容器分别是 172.17.0.2172.17.0.3。在不同网络的容器之间执行 ping 命令会导致 100% 丢包。

Docker Compose

大多数时候,Docker 就是微服务的代名词。在 Docker 中运行一个大型单体应用没有太大意义,因为整个 Docker 引擎是为了运行拆分成不同小服务的大型应用程序而设计的。虽然在 Docker 上运行单体应用没有技术限制,但当容器编排软件介入时(在后续章节中介绍),这就真的违背了容器化的目的。

在处理微服务时,开发过程中通常会同时运行多个服务,因为新服务需要依赖现有服务来执行操作。

为了实现这一设置,Docker 提供了一个名为 docker-compose 的工具,通过创建一个定义容器的 YAML 文件,它可以启动一个完整的容器生态系统。

Docker Compose 曾在 Docker 初期非常流行。如今,它仍然被广泛使用,但随着 Kubernetes 容器编排工具接管了生产环境的工作,它的使用范围逐渐缩小至开发阶段。

让我们来看一下 Docker Compose 是如何工作的:

version: '2'
services:
my_app:
build: .
depends_on:
- db
db:
image: postgres

前面的 YAML 文件是一个 docker-compose 定义。如你所见,它包含了两个组件:

  • 一个 web 应用程序(当前文件夹)

  • 一个数据库(postgres)

将文件保存到名为 docker-compose.yaml 的文件夹中。

这是一个应用程序连接数据库的典型案例。为了简化起见,我们的应用程序将只是一个虚拟应用(没有数据库连接),代码如下:

let dns = require('dns')

dns.lookup('db', (err, result) => {
console.log('The IP of the db is: ', result)
})
{
 "name": "modern-devops",
 "version": "1.0.0",
 "description": "Test",
 "main": "index.js",
 "scripts": {
 "start": "node index.js"
 },
 "author": "",
 "license": "ISC"
 }
package.json

而我们的 Dockerfile 非常简单:

FROM node:onbuild

这个 Dockerfile 会安装所需的依赖并在我们的应用文件夹根目录下运行 npm start

如你所见,应用程序非常简单,它仅仅尝试解析名称 db,而不是连接到数据库(公平地说,我们甚至没有指定连接数据库的端口)。我们将演示 docker-compose 如何连接容器。到目前为止,工作文件夹中应该有四个文件:

  • index.js

  • package.json

  • Dockerfile

  • docker-compose.yaml

回到我们的 docker-compose 文件,我们可以看到在 my_app 的定义中,我们要求构建当前文件夹(构建 Dockerfile 描述的镜像),并且我们指定容器本身依赖另一个名为 db 的容器。这使得 Docker 执行操作并连接这两个容器,从而能够通过名称从 my-app 访问 db。为了实现这一点,/etc/hosts 中创建了一个条目,记录了 db 的 IP 地址,这样 my-app 就能解析它。Docker Compose 非常容易理解:它几乎是自解释的,并且使用 YAML 格式使得一切变得更加可读。现在我们需要运行它:

docker-compose up

一旦完成,它应该会输出很长的一段内容,但有些行会显示我们的成功:

my_app_1 | > my_app@1.0.0 start /usr/src/app
my_app_1 | > node index.js
my_app_1 |
my_app_1 | The IP of the db is 172.20.0.2
my_app_1 | npm info lifecycle my_app@1.0.0~poststart:my_app@1.0.0
web_1 | npm info ok

高亮部分告诉我们,my_app 能通过 IP 访问 db,因为它们位于同一个桥接网络上。我们来看看这里发生了什么:

  • Docker 从 Dockerfile 构建了当前文件夹(如 my_app 中指定的)的镜像

  • Docker 从 Docker Hub 拉取了 postgres 镜像

  • Docker 按顺序启动了镜像:首先是 db,其次是 my_app,如依赖关系中所指定的

在本书中,我们将特别强调编排技术,接着我们将在第五章《基础设施即代码》中回到 Docker Compose,届时我们将深入探讨 Docker Swarm,这时 Compose 变得非常有用。

总结

在本章中,我们从内部结构到命令行界面走访了 Docker,学习了如何操作 Docker 主机。现在,我们有足够的知识来理解在生产环境中运行 Docker 的后果和好处。我们没有讨论如何开发 Docker 插件以及不同的存储驱动,因为不幸的是,本书的篇幅有限,无法介绍所有有趣的概念,但我们已经深入探讨了 Docker,足以通过网络上可用的资源(官方文档、视频等)进一步学习。

在下一章中,我们将研究如何围绕我们的软件自动化任务:运行测试、构建镜像以及其他许多不应手动完成的任务。这种自动化被称为 持续集成,因为它允许我们的团队以无缝的方式集成新特性。

第四章:持续集成

在软件构建过程中,质量评估通常会被推到生命周期的末尾,发布之前。当团队在一个六个月的发布周期内工作时,这些缺点不如在发布周期只有几天(甚至几小时!)时那么明显,但根据我的经验,我可以告诉你,在软件开发早期就获取反馈对于将质量提高到我们可以接受的水平至关重要。

软件中存在一种误解,致使许多软件项目面临危险:软件必须是完美的. 这是完全错误的。想想这些真实世界中的系统:你汽车的引擎、核电站、大城市的水净化系统等等;这些系统关乎人命,并且它们也会失败。在这些系统上花费了大量资金,但无法确保完全安全,那你怎么认为你公司编写的软件就能做到完美呢?

与其将资源投入到使软件完美,不如我通过艰难的经验得出结论:更好的做法是将资源投入到构建软件的方式,使工程师能够尽可能快速地修复问题,并能够在有足够信心的情况下缩短发布周期。在本章中,我们将探讨持续集成的关键组成部分:

  • 软件开发生命周期

  • 传统持续集成服务器:

    • Bamboo

    • Jenkins

  • 现代持续集成服务器:

    • Drone

其目标是建立一个有效的持续集成流水线,以确保可靠性,并使我们能够更快地交付产品。

软件开发生命周期

软件开发生命周期是我们作为软件工程师的日常活动图表,等等,这本书是关于 DevOps 的;那我们为什么要谈论软件工程呢?理论上讲,DevOps 是 IT 活动的一部分,涵盖了软件组件的完整生命周期,从初期到发布再到后续的维护。如今,许多公司正在招聘 DevOps 工程师,基于招聘强化版的系统管理员,尽管这种做法有效,但它完全忽视了 DevOps 角色的最大优势:让团队中有人接触软件的所有方面,这样问题就可以快速解决,而无需涉及来自不同团队的人。

在进一步讨论之前,让我们看看软件开发生命周期是如何运作的:

这是 IT 素养中最经典且最研究透彻的软件开发生命周期,几乎每个人在大学里都经历过这一过程,并且即使我们之前没见过,它也作为一种心理模型存在于每个人的脑海中。如今,随着敏捷方法的出现,人们倾向于认为这个模型已经过时。但我认为它仍然是一个非常有效的模型,唯一变化的是规模以及不同利益相关者在之前图示中的参与度。让我们从上到下简要地了解每个步骤的目标:

  • 需求分析:这是我们将遇到大部分问题的地方。我们需要在 IT 外部人员(如会计、市场营销人员、农民等)与 IT 人员之间找到共同的语言,这通常会导致术语不同,甚至商业流程被错误捕捉的问题。

  • 设计:在这一阶段,我们将使用 IT 人员可以立即理解的语言设计流程,这样他们就能够高效地编码。通常,这一阶段会与需求分析重叠(如果利益相关者本身就懂 IT),这也是理想的情况,因为图表是我们正在寻找的完美中介语言。

  • 开发:顾名思义,这就是软件被构建的地方。这是开发人员擅长的事情:构建符合(可能有缺陷的)规范并且能够正常工作的技术制品。这里我们需要聪明:不管我们做什么,我们的软件都不可能完美无缺,我们需要相应地进行规划。当我们在敏捷环境中工作时,“尽早交付,频繁交付”是减少错误规范影响的座右铭,这样利益相关者就能在问题变得过大之前对产品进行测试。我还认为,尽早让利益相关者参与进来是一个不错的策略,但这不是万灵药,所以无论我们做什么,我们的软件必须是模块化的,以便我们可以插拔模块以适应新的需求。为了确保我们模块的功能正常,我们编写可以快速运行的单元测试,以确保代码按照预期的方式执行。

  • 测试:这是持续集成存在的地方。我们的持续集成服务器将在适当时运行测试,并尽快通知我们应用程序可能存在的问题。根据软件的复杂性,在这一阶段我们的测试可能非常广泛,但一般来说,持续集成服务器专注于运行集成测试和验收测试(也就是说,集成服务器通常会运行所有测试,因为单元测试应该是低成本的)。

  • 发布:在这个阶段,我们将软件交付到所谓的生产环境中;人们开始使用软件,无论我们在前期阶段投入多少精力,都难免会出现 Bug,这也是我们设计软件时希望能快速修复问题的原因。在发布阶段,我们可以创建一些稍后将在本书中看到的东西,称为持续交付CD)流水线,它使开发人员能够非常快速地执行构建-测试-部署周期(甚至每天几次)。

  • 维护:维护有两种类型:演化性和修正性。演化性维护是通过添加新功能或改进业务流程来推动软件的发展,以适应业务需求。修正性维护是修复错误和误解的过程。我们希望将后者最小化,但不能完全避免。

测试类型

在上一节中,我们讨论了不同类型的测试:

  • 单元测试:我们所说的白盒测试是模拟依赖项并测试特定代码片段的业务流程。

  • 集成测试:这些测试旨在验证应用程序不同组件之间的集成,而不是广泛地测试业务逻辑。有时,当软件不太复杂时,集成测试被用作单元测试(尤其是在动态语言中),但这并不是最常见的使用场景。

  • 验收测试:这些测试旨在验证业务假设,通常基于我们所称的用户故事原理,描述具有“给定假设”风格的情境。

每个测试有不同的目标,它们协同工作,但请记住以下图示:

这就是我所说的测试金字塔,背后有多年的经验(不仅仅是我的):你的软件应该有大量的单元测试,较少的集成测试,以及一些验收测试。这可以确保大部分业务逻辑被单元测试覆盖,集成测试和验收测试用于更特定的功能。此外,集成测试和验收测试通常更昂贵,因此通常建议最小化它们的使用(但前提是不降低测试覆盖率)。

当与 CI 服务器协作时,通常开发人员会在自己的计算机上运行单元测试,因为它们快速且能够发现大量潜在问题,将集成测试和验收测试留给 CI 服务器,在开发人员进行其他任务时运行这些测试。

传统 CI 服务器

在本节中,我们将介绍最传统的 CI 服务器:

  • Bamboo

  • Jenkins

它们已经存在一段时间了,尽管它们在企业界得到了广泛使用,但在与新的、更现代的 CI 服务器(如 Drone 或 Travis)相比,它们的优势正在逐渐减弱(尽管 Travis 已经存在一段时间,但它已被重新设计以支持云端工作)。

Bamboo

Bamboo是由 Atlassian 开发的专有 CI 服务器。Atlassian 是一家专注于为开发者提供工具的软件公司。像 JIRA 和 Bitbucket 这样的产品由 Atlassian 开发,并在 IT 界广为人知。Bamboo 是他们为 CI 活动提供的解决方案,并且因与其他产品的良好集成而非常受欢迎。

让我们安装它。为此,请访问 Bamboo 的主页:confluence.atlassian.com/bamboo/并按照快速入门指南中的说明操作。如你所见,安装过程非常简单,生成评估许可证并经过一些步骤(快速安装)后,你应该能在本地计算机上运行 Bamboo:

如果你点击标有“创建你的第一个构建计划”的按钮,你会发现设置 Bamboo 中的作业非常简单。在这个例子中,我们可以使用我之前创建的一个开源项目——Visigoth,它是一个带有断路器功能的负载均衡器,用于微服务之间的互联。GitHub 仓库位于github.com/dgonzalez/visigoth

如果你想修改它,可以将其分叉到你的 GitHub 仓库中。Visigoth 是一个单一组件,彼此之间不互相作用,因此只为其创建了单元测试。在相应的字段中输入仓库的克隆 URL,在此情况下为 Git 仓库,然后提交表单。

如果你的 GitHub 账户启用了基于时间的一次性密码TOTP)保护,你可能需要在表单的源部分选择“无认证的 Git 仓库”,而不是选择“GitHub 仓库”来创建测试计划。

一旦你完成了创建计划的步骤,它将要求你为测试计划添加任务,目前,这些任务仅包括从 Git 检出源代码。在这种情况下,Visigoth 是一个 Node.js 应用程序,因此,测试是通过执行npm test命令来运行的。为了执行此命令,我们需要添加两个type命令任务。第一个任务将用于安装应用程序的依赖项,第二个任务将用于运行测试。让我们先添加第一个任务:

如你所见,我通过点击“添加新可执行文件”并指定NPM所在路径来添加了一个可执行文件。你可以通过在安装 Bamboo 的机器的终端执行which npm来找到该路径。

你需要在安装了 Bamboo 的同一台机器上安装 Node.js 才能运行测试。当前的 LTS 版本与之兼容良好,但 Visigoth 是在 Node 6.x 上进行测试的。

现在我们将添加第二个命令,它将执行npm test来运行测试。该命令只会在前两个步骤(检出代码[Checkout Default Repository]和安装依赖[NPM install])成功后执行:

一旦保存了任务,我们就完成了执行 Visigoth 测试所需的所有操作。现在,剩下的唯一任务就是运行该作业:

如果一切正常,你应该会看到一个绿色的徽章和成功的消息。正如你看到的,我的构建在之前的运行中失败了,因为我正在调整 CI 服务器以运行 Visigoth。

你可以查看作业的日志,看看有多少测试成功通过以及其他有用的信息。如果你进一步探索,你还会看到 Bamboo 提供了不同类型的任务,比如 mocha 测试运行器,这使得 Bamboo 能够理解测试结果。目前,在当前配置下,如果任何测试失败,Bamboo 将无法识别失败的是哪个测试。我建议你尝试不同的配置,甚至不同的应用程序,以便熟悉它。正如你自己所看到的,界面非常友好,通常通过创建新任务,达到你期望的配置也相当简单。

默认情况下,Bamboo 会创建一个名为触发器的东西。触发器是一种导致测试计划执行的操作。在这种情况下,如果我们更改了作业创建时使用的 GitHub 仓库,测试计划将被触发以验证更改,确保新代码的持续集成。

另一种有趣的触发器类型是基于时间的触发器。这种类型的触发器允许我们在夜间运行构建,因此如果我们的测试需要几分钟甚至几个小时才能完成,我们可以在没人使用服务器时执行它。此类触发器帮助我避免了由于夏令时调整导致的一些错误,这些错误会导致某些测试失败,因为代码片段无法很好地处理跨时区的时间变化。

总的来说,Bamboo 能够应对各种情况,并且已经适应了现代的需求:我们甚至可以在测试通过后构建 Docker 镜像,并将其推送到远程注册表,以便后续部署。Bamboo 还能够在构建后的阶段采取行动,例如,如果构建在夜间失败,它会通过电子邮件或其他通信渠道通知我们。

Jenkins

我已经使用 Jenkins 一段时间了,我必须说,使用它让我感到非常舒适,因为我知道它是免费的、开源的,并且高度可定制。它有一个强大且文档完善的 API,使用户能够自动化几乎与持续集成相关的所有事务。在第八章,发布管理 – 持续交付,我们将设置一个持续交付管道,使用 Jenkins 以便在测试结果满意时能够透明地发布应用程序的新版本,使我们的团队能够专注于开发,并自动化所有与部署相关的活动。

Jenkins 也是模块化的,这使得开发人员可以编写插件来扩展功能,例如,在构建失败时向 Slack 渠道发送消息,或将 Node.js 脚本作为作业的一部分执行。

在可扩展性方面,Jenkins 像 Bamboo 一样,可以通过主从配置扩展到数百个节点,从而为我们的 CI 服务器增加更多的计算能力,以便并行执行一些任务。

就其本身而言,Jenkins 足以提供几本书的内容,但我们将探讨设置自动化作业以测试应用程序所需的内容。也可以为 Jenkins 编写插件,因此,实际上它的功能没有限制。

现在让我们先关注 Jenkins 的操作方面。为了运行 Jenkins,我们有两个选择:

  • 以 Docker 容器的方式运行

  • 将其作为程序安装在您的 CI 服务器上

现在,我们将使用 Jenkins 的 Docker 镜像进行安装,因为这是运行它的最简单方式,并且适合我们的目的。让我们开始吧。首先要做的是通过命令行运行一个简单的 Jenkins 实例:

docker run -p 8080:8080 -p 50000:50000 jenkins

这将运行 Jenkins,但要注意,所有关于配置和执行的构建信息都将存储在容器中,因此如果丢失容器,所有数据也会丢失。如果您想使用一个卷来存储数据,需要执行的命令如下:

docker run --name myjenkins -p 8080:8080 -p 50000:50000 -v /var/jenkins_home jenkins

这将创建一个可以在升级到新版本的 Jenkins 或甚至重新启动相同容器时重用的卷。运行该命令后,日志将显示类似于以下图示的内容:

这是 Jenkins 的初始密码,设置实例时需要用到它。几秒钟后,容器的日志将停止,这意味着您的 Jenkins 服务器已准备好使用。只需打开浏览器,访问http://localhost:8080/,您将看到类似于以下内容的界面:

这里是您可以输入先前保存的管理员密码,并单击“继续”按钮的位置。接下来的屏幕将询问您是否应安装建议的插件,或者您是否想选择要安装的插件。请选择建议的插件。几分钟后,它将允许您创建用户,就这样。Jenkins 正在一个容器中运行:

现在我们将创建一个新的作业。我们将使用与 Bamboo 相同的代码库,以便我们可以比较这两个集成服务器。让我们点击“创建新项目”。您将看到以下表单:

只需为项目输入名称,并选择第一个选项:自由样式项目。Jenkins 有不同类型的项目。自由样式项目是一种我们可以定义步骤的项目类型,就像我们在 Bamboo 中所做的那样。另一个有趣的选项是流水线类型,我们可以通过一个称为 DSL(即 领域特定语言)的语言定义一组步骤和阶段,创建可以保存为代码的流水线。

接下来的屏幕是我们配置项目的地方。我们将使用 Git,其存储库托管在 github.com/dgonzalez/visigoth.git 上。

如果您之前在 Bamboo 中创建了分支,则可以使用您的分支。您的配置应与以下屏幕截图所示类似:

现在我们需要使用 npm install --development 命令安装 Visigoth 的依赖项,并使用 npm test 命令执行测试,但我们是从容器中运行 Jenkins,而这个容器并未安装 Node.js。我们将利用我们的 Docker 知识来安装它。检查 Docker Hub 中 Jenkins 镜像的 Dockerfile,我们可以验证它基于 Debian Jessie(它基于 OpenJDK,但是它基于 Debian Jessie),因此我们可以在其中安装所需的软件。要安装软件的第一步是获得容器的 root 访问权限。正如您在 第二章 中所学到的,“云数据中心 - 新的现实”,我们可以在正在运行的容器上执行命令。让我们执行以下命令:

docker exec -u 0 -it eaaef41f221b /bin/bash

此命令在 ID 为 eaaef41f221b 的容器中以 UID 和 GID 为 0 的用户(在您的系统中将会有所不同,因为每个容器的 ID 都是唯一的)执行 /bin/bash。我们需要这样做是因为 Jenkins 镜像定义并使用一个名为 jenkins 的新用户,其具有已知的 UID 和 GID,因此如果未传递 -u 0 标志,则 /bin/bash 命令将由用户 jenkins 执行。

一旦我们在容器中获得了 root 权限,继续安装 Node.js:

curl -sL https://deb.nodesource.com/setup_7.x | bash -

一旦上一个命令的执行完成,运行以下命令:

apt-get install -y nodejs build-essentials

就这样。从现在开始,我们的 Jenkins 容器有了 Node.js 的安装,可以运行 Node.js 脚本。也就是说,我们应该避免在生产容器中安装软件。我们的容器应该是不可变的工件,在生命周期内不做更改,因此我们应该将这个镜像的更改提交并标记为新版本,以便将其发布到生产容器中。由于我们没有生产容器,所以我们边做边修改。

我们生产环境中的容器应该是不可变的工件:如果我们需要更改它们的状态,我们会创建新版本的镜像并重新部署,而不是修改正在运行的容器。

一旦安装了 Node.js,我们可以退出容器中的 root shell,然后回到 Jenkins 完成我们的任务。就像我们在 Bamboo 中做的一样,以下是我们运行测试所需要的步骤:

在作业配置的最底部,有一个叫做 post-build 的操作部分。这个部分允许你在作业完成后执行一些操作。这些操作包括发送电子邮件、将提交信息添加到 Git 仓库等。如我们之前提到的,Jenkins 是可扩展的,通过安装新插件,可以添加新的操作。

Jenkins 还可以通过用户输入对构建进行参数化。

一旦你添加了这两个步骤到构建中,点击保存,我们就完成了:现在你已经拥有一个完全功能的 Jenkins 作业。如果我们运行它,它应该能够成功运行 Visigoth 上的测试。

密钥管理

CI 服务器的一个功能是能够与通常依赖某种凭证(如访问令牌或类似物品)来验证用户身份的第三方服务进行通信。暴露这些密钥是不被鼓励的,因为它们可能会对我们的公司造成重大损害。

Jenkins 以非常简单的方式处理此问题:它提供了一种安全存储凭证的方法,可以将这些凭证作为环境变量注入到构建中,以便我们可以使用它们。

我们来看一些示例。首先,我们需要在 Jenkins 中创建密钥。为此,我们需要从首页进入管理 Jenkins 页面。

一旦我们到达那里,你应该看到一个非常相似的界面:

我们使用的是全局凭证存储,因为我们只是想展示其工作原理,但 Jenkins 允许你将凭证封装起来,以便你可以在不同的使用场景下限制访问权限。在 Jenkins 中,凭证除了可以注入到构建上下文外,还可以连接到插件和扩展,以便它们可以对第三方系统进行身份验证。

现在,点击左侧的“添加凭证”按钮:

在继续之前,有些字段需要我们填写,但它们非常基础:

  • 类型:这是我们想要创建的秘密类型。如果打开下拉菜单,会有几种类型,从文件到证书,涵盖用户名和密码等。

  • 范围:这是我们秘密的作用范围。文档不是 100%清晰(至少第一次阅读时不清晰),但它允许我们在某些情况下隐藏秘密。这里有两个选项:全局(Global)和系统(System)。选择全局时,凭据可以暴露给 Jenkins 及其子对象中的任何对象,而选择系统时,凭据只能暴露给 Jenkins 及其节点。

其余字段取决于秘密的类型。接下来,我们将创建一个用户名与密码类型的秘密。只需在下拉菜单中选择它并填写其余细节。创建完成后,它应该会出现在凭据列表中。

下一步是创建一个绑定到这些凭据的任务,以便我们可以使用它们。只需像本节开始时看到的那样创建一个新的自由风格项目,但我们将在配置任务的屏幕上停下来,特别是在构建环境部分:

现在选择用户名和密码(联合)。联合用户名和密码意味着我们在一个变量中获得完整的秘密(用户名和密码),而分开则会将秘密分成两个变量:一个用于用户名,另一个用于密码。

一旦我们选择了它,创建绑定的表单就非常简单:

我们可以选择存储秘密的变量,并且还可以选择具体的秘密。有一个单选按钮可以让你选择“参数表达式”或“特定凭据”,因为我们可以将任务参数化,让用户在触发屏幕中输入内容。为了展示 Jenkins 设计的周全,我们将添加一个构建步骤,通过将秘密回显到日志中来使用它:

点击保存按钮以保存任务并运行它。一旦任务执行完成,转到结果并点击控制台输出。如果你原本期待在这里看到秘密,Jenkins 将给你一个惊喜:

为了防止未授权用户暴露秘密,该秘密已被掩码处理。虽然这种方法并非万无一失,因为有人可能会轻易地从通过 Jenkins 检查出的应用程序中的测试中提取该秘密,但它确实为安全性提供了一定的保障,剩下的则依赖于代码审查和流程。

现代 CI 服务器

在 IT 领域,有一件事是明确的,那就是市场发展非常迅速,每隔几年,就会有一个新的趋势打破曾经被认为是完美解决方案的问题。CI 软件也不例外。在过去的几年中(考虑到本书是 2017 年写的),基础设施即代码(Infrastructure as Code)吸引了大量的关注,而在 CI 中,其对应的概念是流水线即代码(Pipelines as Code)。

Jenkins 和 Bamboo 最近已经添加了对声明式流水线的支持,但它们并不是围绕这个概念构建的。

Drone CI

Drone 可能是市场上最新的 CI 服务器。我决定在本章介绍它,因为当我在 nearForm Ltd. 了解到它时,它对我来说是一个巨大的启示。那时,我已经习惯了 Jenkins,它适用于我在职业生涯中遇到的每一个用例,从 CI 到持续交付,有时甚至作为堡垒主机,使用一种叫做回调 URL 的功能,在这个功能下,工作可以通过向特定 URL 发送 HTTP 请求来触发。

Drone 是基于容器概念构建的。Drone 中的所有内容都是容器,从服务器到测试运行的地方,最有趣的部分是,甚至插件也是容器。这使得编写新的插件以执行自定义操作变得非常容易,唯一的要求是,如果容器成功完成,必须返回 0 作为退出代码,如果不成功,则返回非零退出代码。

对于 Jenkins 或 Bamboo,编写插件需要几个小时的测试和阅读文档。而对于 Drone,我们只需要知道如何构建 Docker 镜像以及我们想要完成的任务。

请注意,Drone 仍然处于 0.5 版本,并且发展非常快速,因此在你阅读本书时,Drone 可能已经发生了显著变化,但我还是决定包括它,因为我认为它是一个非常有前途的软件。

安装 Drone

为了安装 Drone,我们将使用 docker-compose,并将其配置为与 GitHub 一起使用。

Drone 和 Docker 一样,遵循客户端-服务器架构,因此我们可以找到两个不同的组件:服务器和 CLI。我们将首先继续进行服务器部分的配置。请查看以下 docker-compose 文件:

version: '2'

services:
drone-server:
image: drone/drone:0.5
ports:
- 80:8000
volumes:
- ./drone:/var/lib/drone/
restart: always
environment:
- DRONE_OPEN=true
- DRONE_GITHUB=true
- DRONE_GITHUB_CLIENT=${DRONE_GITHUB_CLIENT}
- DRONE_GITHUB_SECRET=${DRONE_GITHUB_SECRET}
- DRONE_SECRET=${DRONE_SECRET}

drone-agent:
image: drone/drone:0.5
command: agent
restart: always
depends_on: [ drone-server ]
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_SERVER=ws://drone-server:8000/ws/broker
- DRONE_SECRET=${DRONE_SECRET}

在前面的 Docker Compose 文件中,运行了两个容器:一个是服务器,另一个是代理。在 0.4 版本之前,Drone 的主节点可以执行构建,但在此之后,需要一个代理来运行构建。在继续之前,我们需要配置一些秘密信息,这些信息通过环境变量传递给 Compose(使用${VAR_NAME}符号):

  • DRONE_GITHUB_CLIENT:如前所述,我们将使用 GitHub 作为源代码的来源,以便进行测试。在 GitHub 上注册一个新的 OAuth 应用时,这个值会提供给你。你可以在 GitHub 的设置部分创建 OAuth 应用。小心,创建 GitHub OAuth 应用时需要的参数之一是回调 URL。在这种情况下,我们将使用http://localhost/authorize,因为我们在本地计算机上工作。

  • DRONE_GITHUB_SECRET:与DRONE_GITHUB_CLIENT相同,这是在 GitHub 上创建新 OAuth 应用时提供的。

  • DRONE_SECRET:这是一个与代理和主服务器共享的任意字符串。你只需创建一个简单的字符串,但在生产环境中运行 Drone 时,确保字符串足够长,以免被猜到。

为了让 Drone 与 GitHub 集成工作,我们需要从 GitHub 接收回调。一旦我们获取到所有的值,我们只需要运行以下命令:

DRONE_GITHUB_CLIENT=your-client DRONE_GITHUB_SECRET=your-secret DRONE_SECRET=my-secret docker-compose up

在一行命令中,我们设置了所需的三个变量,并运行了docker-compose up。如果一切顺利,当你访问http://localhost时,应该能看到类似下面的窗口:

如果点击登录,Drone 应该会将你重定向到 GitHub 进行授权,然后 GitHub 会将你重定向到创建 OAuth 应用时指定的回调 URL,即你的本地 Drone 安装地址http://localhost/authorize。有时可能需要一些调整,但一般来说,非常容易让它工作。如你所见,Drone 利用 GitHub 的身份验证,因此需要一个 GitHub 账号来登录。

现在我们将继续进行 CLI 配置。只需访问readme.drone.io/0.5/install/cli/并选择适合你平台的版本,在我的案例中是 macOS。只需将二进制文件放在路径中,你就可以开始使用了。为了配置 Drone 服务器的位置,你需要指定两个环境变量:

  • DRONE_SERVER:这是你的 Drone 服务器的 URL,在这种情况下是http://localhost

  • DRONE_TOKEN:登录 Drone 后,进入帐户页面并点击显示令牌。这就是你需要的值

设置好这两个变量后,执行以下命令:

drone info

这应该会显示你的 GitHub 用户名和你用来注册的电子邮件。

运行构建

Drone 在运行构建时有不同的理念:它通过触发管道来响应远程仓库中代码的变化。我们来创建一个超级简单的仓库,里面有一个非常简单的 Node.js 应用。我已经在我的 GitHub 账户上创建了这个仓库,为了让一切更加简单:github.com/dgonzalez/node-example-drone/。只需将其 fork 到你自己的账户,就可以开始了。

我们需要做的第一件事是激活你本地 Drone 安装中的项目。只需进入账户,在仓库列表中激活node-example-drone。现在,它应该像下面的截图一样出现在主屏幕上:

现在我们面临一个小问题:Drone 是为了触发通过 GitHub 发送到我们的 Drone 服务器的 webhook 而创建的。由于我们在一个私有网络中工作,我们需要以某种方式将我们的服务器暴露到互联网。在这种情况下,我们将使用一个叫做Ngrok的服务(www.ngrock.com),以便将 Drone 暴露到互联网,而在生产环境中工作时这并不是必要的,因为它应该可以通过互联网访问(或者至少通过代理访问)。只需按照说明操作,一旦在终端运行它,它应该和下面的截图非常相似:

这指定了哪个主机被转发到你的本地地址,在我的例子中是http://852cc48a.ngrok.io。只需在浏览器中打开它,检查是否可以从那里访问 Drone。

还剩下一件事,就是编辑 Drone 在我们激活时安装在 GitHub 仓库中的 webhook。你可以在 GitHub 的仓库设置中找到它。只需编辑 webhook,将 URL 从http://localhost更改为你的 Ngrok URL,在我的例子中是http://852cc48a.ngrok.io

执行管道

现在设置完成,在做任何事情之前,先看一下 forked 仓库中的.drone.yaml文件:

debug: true
pipeline:
 build:
 image: node
 commands:
 - npm install --development
 - npm test

这是我们的管道,正如你所猜测的,它会和我们的代码一起提交到仓库中。当 GitHub 将 webhook 传送到我们的 Drone 安装时,Drone 会执行此管道中的指令。由于 Drone 与容器一起工作,Drone 首先会基于我们指定的 node 创建一个镜像,并执行以下操作:

  • 它会安装依赖

  • 它会运行测试

如果执行这些命令的容器的退出代码是0,那么我们的构建就成功了,你可以通过推送一些更改到你的 GitHub 仓库,并观察 Drone 如何响应来进行测试。

还有一种通过 CLI 接口重新触发构建的方法(不是第一次)。打开终端,在配置好之前提到的环境变量后(如果你还没有做),执行以下命令:

drone build list dgonzalez/node-example-drone

这将返回一个已执行构建的列表。只需将dgonzalez更改为你的用户名,正如你在网页界面中看到的那样。为了重新运行之前的构建,我们可以执行以下命令:

drone build run dgonzalez/node-example-drone 1

这个命令触发了一个已构建的 Drone 构建。这在你怀疑构建因外部因素而失败时特别有用。

有时,webhook 会失败(尤其是我们与 Ngrok 的设置),但 GitHub 允许你在仓库的 webhook 部分调试这个问题。

这是管道的最简单案例。如前所述,Drone 基于插件,这些插件也是 Docker 镜像。插件列表非常全面,可以在github.com/drone-plugins找到。

假设我们想将镜像推送到 Google Cloud 中的 Google Container Registry。我们将使用来自github.com/drone-plugins/drone-gcr的插件 drone-gcr。这是我们的管道:

debug: true
pipeline:
  build:
    image: node
    commands:
      - npm install --development
      - npm test
  publish:
    gcr:
      repo: myrepo/node-example-drone
      token: >
         {
            ...
         }

这里有一个两阶段的管道:它首先执行测试,一旦测试成功,就将镜像部署到 Google Cloud Registry。我们在管道中有不同的阶段可以使用:

  • 构建: 用于构建测试和相关命令

  • 发布: 用于将工件发布到远程仓库

  • 部署: 对于持续集成非常有用,因为它允许我们以持续交付的方式部署软件

  • 通知: 用于通过电子邮件、Slack 或其他任何渠道发送通知

例如,如果我们想发送一个 Slack 通知,我们只需要将以下几行添加到管道中:

 notify:
   image: plugins/slack
   channel: developers 
   username: drone

记住,YAML 对制表符和空格非常敏感,因此 notify 需要与 publishbuild 在同一级别。

其他功能

在撰写本文时,Drone 正在积极开发中,不断添加新功能,并进行一些重大重构。它还提供了其他功能,如秘密管理和支持服务。

通过秘密管理,我们可以注入加密后的秘密,这些秘密存储在数据库中,只有在我们的 Drone CLI 使用来自 Drone 服务器的有效令牌进行加密签名后,才会注入到构建中。

Drone 还提供支持服务,这些服务在测试期间与测试一起运行。当我们的集成测试依赖数据库,或需要启动像 Hashicorp Vault 这样的第三方软件,或者像 Consul 或 Eureka 这样的服务发现基础设施时,这非常有用。

预计未来 Drone 将会有更多功能,但目前它正在经历重大变化,因为它正在积极开发(与像 Jenkins 这样的成熟服务器不同,后者已经存在了一段时间)。

总结

在本章中,我们介绍了三种不同的 CI 工具:

  • Bamboo,一种商业工具

  • Jenkins,一种行业标准的开源工具

  • Drone,一款尖端技术的 CI 服务器

我们在本书中讨论了将来要使用的 Jenkins 的关键特性,但我们也展示了 Drone 如何将容器的概念融入一个非常强大的 CI 系统,尽管它还不够成熟,但我预计在未来几年将成为行业标准。

我们需要注意的重要概念已经解释过了,但总结来说,我们使用集成服务器来为我们运行测试,这样可以让开发人员不需要做这些工作,同时也能在夜间运行测试,以确保每日构建的稳定性。

在下一章,我们将探讨社区所称的 Infrastructure as Code: 基本上是一种将我们的基础设施视为代码来处理的方式,优雅地管理资源。

第五章:基础设施即代码

在前几章中,我们展示了新一代云数据中心如何帮助我们非常轻松地创建在线资源(虚拟机、Docker 仓库、加密密钥),将硬件配置周期从几周(购买、运输和安装新计算机)缩短到几秒钟。我们还看到了市场上有不同的供应商可以提供类似的功能,并且具有各自不同的优势,我们可以在构建系统时加以利用。

你已经学会了如何通过他们提供的网页界面创建资源,但这种方式的可扩展性如何呢?手动创建资源阻止了我们保持自动化的资源清单,这对于安全目的以及像管理软件组件一样管理我们的基础设施是必要的。

在本章中,你将首先学习如何通过云数据中心供应商提供的 SDK 来构建云中的资源,然后通过一个叫做Terraform的软件组件来构建,Terraform是一个管理在线资源的行业标准。我们将专注于Google Cloud Platform,原因有几个:

我个人认为,命令行界面更易于使用。

Google Cloud Platform 的试用版涵盖了大量资源,你可以在本书中使用这些资源进行实验,因为你几乎可以在全套产品中创建任何资源。

在写这篇文章时(2017 年 4 月),在云数据中心方面,Google Cloud Platform 提供了最具性价比的服务。

也就是说,AWS、Azure 或任何其他供应商也提供了非常有趣的试用账户,但遗憾的是,我们不能在一本书中涵盖所有内容。

Google Cloud Platform SDK -  gcloud

Google 为我们提供了一个非常全面的 SDK,它可以用于操作 Google Cloud Platform 以及安装与云操作相关的软件组件。

我们需要做的第一件事是安装gcloud

虽然有 Windows 版安装程序,但通常来说,对于基于 Unix 的系统(主要是 Linux 和 Mac),我们有一个可以从命令行执行的交互式安装程序,并且有一个无人值守模式(用于自动配置)。

不同的选项可以在cloud.google.com/sdk/downloads找到。

为了安装它(在我的例子中是在 Mac 上),我们需要做的第一件事是运行以下命令:

curl https://sdk.cloud.google.com | bash

这将启动在线模式下的交互式安装程序:在安装过程中,我们会被问到一系列问题。

第一个是安装目录。默认情况下,这是用户的主目录,但你可以将其更改为你选择的文件夹。选择文件夹后,它将开始下载并安装所需的基础组件。

问题是,你是否愿意通过收集匿名数据来帮助改进 Google Cloud SDK。请根据你的偏好作答。

现在,Google Cloud SDK 将开始安装核心组件。

如前图所示,Google SDK 安装了一些用于操作 Google Cloud Platform 上基本服务的软件包。安装完成后(无需执行任何操作),它会询问你是否要修改系统的 PATH 变量。只需回复 Y 并按 Enter 键,这样 gcloud 命令就可以在控制台中使用了。它会询问你要在哪个文件中修改 PATH 变量。通常,安装程序提供的默认选项已经足够好。在修改文件之前,Google Cloud SDK 安装程序会创建一个备份文件,文件名与原文件相同,扩展名为 .backup,这样你可以还原更改。

现在完成了。它会提示你启动一个新的 shell 来使更改生效。关闭终端并重新打开,检查 gcloud 命令是否可用。

现在我们已经安装了 Google Cloud SDK,是时候配置认证了。执行以下命令:

gcloud init

它会要求你登录,因此请选择“是”,这将打开一个浏览器窗口,要求你输入 Google 凭证。输入与你的试用账户相关联的凭证(如果你没有注册试用版,请在配置凭证之前进行注册)。如果你在 Google Cloud Platform 上已经创建了项目,它会要求你在控制台中选择要使用的项目。在我的例子中,我已经在第二章中配置了一个项目,云数据中心——新现实,所以我选择了名为 implementing-modern-devops 的项目。

接下来的主题是配置 Google Compute Engine。请选择“是”并选择你的可用性区域。在我的情况下,欧洲的任何地方都适合我。

完成此步骤后,我们就完成了。提示符会告诉我们已经创建了一个名为“default”的配置。这意味着 Google Cloud SDK 可以使用多个凭证,但在本例中,我们将只使用一组凭证和一个项目。

使用 Google Cloud SDK 创建资源

一旦我们设置好,就该开始创建资源了。正如你所猜测的,创建资源的命令可能会非常复杂,也可能非常简单,取决于你的需求。幸运的是,Google 工程师在创建 Google Cloud Platform 的界面时已经考虑到了这一点。

首先,你需要登录到你的 Google Cloud Platform 账户。登录后,进入 Compute Engine 并填写表单以创建新的资源。输入实例名称,选择离你最近的区域(在我这里是欧洲),选择机器类型(默认即可),API 访问(我们不需要这个,但默认设置可以),并勾选允许 HTTP 流量和允许 HTTPS 流量。在点击创建之前,请查看以下截图:

如果你查看最底部,在“创建”按钮下面,有两个链接:

  • REST 等效

  • 命令行

现在,我们将重点关注命令行链接。点击它,你应该会看到一个带有一些命令的窗口。让我们解释一下它们:

gcloud compute --project "implementing-modern-devops" instances create "test-instance" \
               --zone "europe-west1-c" --machine-type "n1-standard-1" --subnet "default" \
               --maintenance-policy "MIGRATE" \
               --service-account "1085359944086-compute@developer.gserviceaccount.com"
               --scopes \                     
                    "https://www.googleapis.com/auth/devstorage.read_only", \
                    "https://www.googleapis.com/auth/logging.write", \
                    "https://www.googleapis.com/auth/monitoring.write", \
                    "https://www.googleapis.com/auth/servicecontrol", \
                    "https://www.googleapis.com/auth/service.management.readonly", \
                    "https://www.googleapis.com/auth/trace.append" \ 
               --tags "http-server","https-server" --image "debian-8-jessie-v20170327" \
               --image-project "debian-cloud" --boot-disk-size "10" --boot-disk-type "pd-standard" \
               --boot-disk-device-name "test-instance"

第一个命令创建了虚拟机。如你所见,没有人能够轻松学会如何创建这个命令,但幸运的是,Google Cloud Platform 为你提供了每个将要创建的资源的命令,因此你可以使用 UI 来生成这些命令。也就是说,前面的命令设置了Google Cloud提供的每一个潜在设置,换句话说,我们能够运行前面的命令,得到相同的结果,无论我们在云账户中更改了什么设置。

还有一个更简短的版本:

gcloud compute --project "implementing-modern-devops" instances create "test-instance"

这个命令与之前那个非常长的命令完全相同,但假设设置是默认的(记住,你已经选择了一些参数,比如默认区域)。

另外两个命令更简单:

gcloud compute --project "implementing-modern-devops" firewall-rules create "default-allow-http" --allow tcp:80 --network "default" --source-ranges "0.0.0.0/0" --target-tags "http-server"

也看看这个:

gcloud compute --project "implementing-modern-devops" firewall-rules create "default-allow-https" --allow tcp:443 --network "default" --source-ranges "0.0.0.0/0" --target-tags "https-server"

正如你所猜测的,这些命令允许 HTTP 和 HTTPS 流量进入我们的主机,正如 UI 表单中所描述的那样。

这些是基础的基础设施即代码。我们可以将这些命令写入 bash 脚本中,然后就可以开始了;我们的基础设施将会自动为我们创建。以同样的方式,如果我们不想依赖 Google Cloud SDK,我们可以选择 REST 选项,它将向我们展示需要向 Google Cloud 发出的 HTTP 请求列表,以便创建相同的资源。如果你熟悉像 Python、JavaScript(Node.js)等编程语言,你就会知道发出 HTTP 请求以创建资源是多么简单,这样你就能像管理代码一样管理基础设施,遵循相同的生命周期。

这是管理云端资源的一大步,但它仍然不完整。想象一下这个情况:你在一家拥有相当复杂设置的公司工作,比如在不同的时区有几台机器,并且网络配置相当复杂。你怎么能一眼看出哪些机器在运行,防火墙规则是什么呢?

答案很简单:根据我们今天所知道的,这种方式不可行。在下一部分,你将学习如何使用来自 HashiCorp 的Terraform来管理不仅是资源的创建,还有不同云提供商上在线资源的完整生命周期。

Terraform

TerraformHashiCorp开发的一款产品。HashiCorp 是一家专注于 DevOps 工具的公司,例如 Consul,一个高度可用的分布式键值存储,或 Vagrant,一个使用与生产环境相同的提供程序来重现开发环境的工具。

Terraform,顾名思义,允许你以声明式的方式在云数据中心创建基础设施,跟踪哪些内容在何处被创建,并允许你从代码角度应用更改:你的基础设施作为代码进行描述,因此,它可以遵循其生命周期。

我们需要做的第一件事是下载并安装 Terraform。只需在浏览器中打开www.terraform.io/downloads.html网址并选择你的平台,在我这里是 Mac。Terraform 是一个压缩成 ZIP 文件的单一二进制文件(据我所知,所有平台都是如此),我解压并将其放在我的路径中,在我的情况下,是/usr/local/bin/terraform

请小心,某些 OSX 配置并不包含/usr/local/bin/PATH环境变量中,因此你可能需要在能够从任何路径执行 Terraform 之前,手动添加它。

一旦安装完成,并且PATH变量中包含/usr/local/bin/作为以分号分隔的值之一,我们可以检查一切是否按预期工作:

terraform version

这应该返回以下输出:

Terraform v0.9.4

这确认了一切都正确无误。另请注意,现如今 DevOps 工具更新非常迅速,它们需要一天比一天做更多的事情。我们将使用最新的版本 0.9.4,但当你读到这本书时,可能会有一个更新的版本,带有新特性甚至可能有一些不兼容的更改。幸运的是,Terraform 附带了一个非常强大的文档。让我们查看所有可用的命令。只需执行这个:

terraform

这应该输出类似以下的内容:

现在,为了显示任何命令的帮助对话框,我们只需要在命令后加上标志-h。例如,我们来查看apply命令的帮助:

terraform apply -h

它将输出该命令在命令提示符下的所有可用选项列表。

创建资源

现在我们已经安装了所有要求的软件包,我们将创建我们的第一个资源,帮助我们理解 Terraform 是如何工作的以及它的强大功能。请在你的计算机上创建一个名为implementing-modern-devops的文件夹,并添加一个名为resources.tf的文件,文件内容如下:

provider "google" {
 credentials = "${file("xxx.json")}"
 project = "implementing-modern-devops"
 region = "europe-west1-b"
}

resource "google_compute_instance" "my-first-instance" {
}

如你所见,前面的片段与 JSON 非常相似,但它实际上被称为 HCL:HashiCorp 配置语言。我们来解释一下这段代码的作用。

第一部分是我们配置凭据的地方。如你所见,Terraform 期望有一个名为xxx.json的文件,而我们现在没有该文件。如果我们查看 Google Cloud Platform 的 Terraform 官方文档,它指出我们需要从 Google Cloud Platform 的 API 管理器部分创建一个服务帐户,如下图所示:

一旦我们选择 JSON 格式创建它,文件将自动保存在我们的计算机上,包含我们操作 Google Cloud Platform 所需的凭证。

请小心。如果泄露了这些凭证,别人可能会代表你创建或销毁资源,这可能会导致重大费用或数据丢失。

将文件复制到之前创建的文件夹 (implementing-modern-devops),并将其重命名为 xxx.json,使其与我们的配置匹配。

第二部分是我们虚拟机的描述,即将在 Google Cloud 中创建的实例。在这种情况下,我们创建了一个名为 my-first-instance 的资源,类型为 google_compute_instance。我们故意没有指定任何配置,因为我想展示如何使用 Terraform 排查问题,鉴于其高质量的错误日志,排查过程非常简单。

让我们看看发生了什么。从我们项目的根目录,即 implementing-modern-devops 文件夹中,运行以下命令:

terraform plan

这个命令将描述在 Google Cloud 中创建基础设施所需的步骤。在这种情况下,它相当简单,因为我们只有一台机器,但这对于学习 Terraform 会非常有帮助。

让我们来看一下发生了什么,以及在输出中是如何解释的:

Errors:

 * google_compute_instance.my-first-instance: "disk": required field is not set
 * google_compute_instance.my-first-instance: "name": required field is not set
 * google_compute_instance.my-first-instance: "machine_type": required field is not set
 * google_compute_instance.my-first-instance: "zone": required field is not set

上述命令失败了。基本上,我们的计算实例需要四个字段,而我们没有指定:machine_typenamezonedisk。在这种情况下,我们可以指定这些字段,但如果你需要检查额外的参数,关于资源 google_compute_instance 的所有文档可以在 www.terraform.io/docs/providers/google/r/compute_instance.html 查阅。

访问它并阅读内容,以便熟悉它。

我们还将指定网络接口(基本上是我们想要连接到机器的网络),因为如果现在不这样做,稍后的 apply 命令将会失败。

现在,我们将解决第一次运行时发现的问题。用以下内容替换 google_compute_instance 块:

resource "google_compute_instance" "my-first-instance" {
 name = "my-first-instance"
 machine_type = "n1-standard-1"
 zone = "europe-west1-b"
 disk {
 image = "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
 }

 network_interface {
   network = "default"
   access_config {
    // Ephemeral IP
   }
 }
}

返回终端并再次执行 terraform plan。输出将类似于以下内容:

+ google_compute_instance.my-first-instance
 can_ip_forward: "false"
 disk.#: "1"
 disk.0.auto_delete: "true"
 disk.0.image: "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
 machine_type: "n1-standard-1"
 metadata_fingerprint: "<computed>"
 name: "my-first-instance"
 network_interface.#: "1"
 network_interface.0.access_config.#: "1"
 network_interface.0.access_config.0.assigned_nat_ip: "<computed>"
 network_interface.0.address: "<computed>"
 network_interface.0.name: "<computed>"
 network_interface.0.network: "default"
 self_link: "<computed>"
 tags_fingerprint: "<computed>"
 zone: "europe-west1-b"

Plan: 1 to add, 0 to change, 0 to destroy.

出于空间考虑,我省略了资源说明之前的解释性文本,但基本上它告诉我们可以将此计划保存在文件中,以便将其作为参数传递给接下来我们将要运行的 apply 命令。

这使我们能够确保执行的内容与我们在计划中看到的相符,以防别人已经在计算需要更改的内容之前修改了在线基础设施,Terraform 会将资源文件中的配置与 Google Cloud 中现有的基础设施进行同步。因此,可能会发生这种情况:我们执行 terraform plan 时,别人通过另一个 Terraform 脚本或手动修改了我们的云基础设施,然后我们的 terraform apply 命令与计算出的计划不同。

一旦确认我们的 Terraform 计划是创建一台虚拟机,执行以下命令:

terraform apply

几秒钟后,脚本应该会完成并展示出已创建、已更改或已销毁的内容:

google_compute_instance.my-first-instance: Creating...
 can_ip_forward: "" => "false"
... (lines omitted: they should match the ones in the plan) ...
 zone: "" => "europe-west1-b"
google_compute_instance.my-first-instance: Still creating... (10s elapsed)
google_compute_instance.my-first-instance: Still creating... (20s elapsed)
google_compute_instance.my-first-instance: Still creating... (30s elapsed)
google_compute_instance.my-first-instance: Creation complete

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

如果一切按计划进行,我们的文件夹中应该会有一个名为 terraform.tfstate 的文件,它是我们在云端创建的虚拟基础设施的状态。我们还会有一个扩展名为 backup 的相同文件,它是我们在执行最后一次 apply 命令之前基础设施的状态。

这个文件非常重要。Terraform 可以根据云端的变更来刷新它,但无法重新构建它。有些人将此文件与 Terraform 脚本一起保存,而其他人则更倾向于使用后端存储此文件并管理 Terraform 状态。

远程状态管理

后端是一种存储我们状态的系统,它将状态存储在一个共享环境中,所有使用相同配置的人都可以快速访问。让我们看看如何使用 Google Cloud Storage 来实现这一点。只需执行以下命令:

terraform remote config -backend=gcs -backend-config="bucket=my-terraform" -backend-config="path=terraform/infrastructure"

以下是一些注意事项:我们需要在 Google Cloud Storage 界面中创建一个名为 my-terraform 的存储桶,并且需要为 Google Cloud 配置应用程序默认凭证。最简单的方法是通过设置名为 GOOGLE_APPLICATION_CREDENTIALS 的环境变量,将其路径指向我们用来在运行基础设施时进行 GCP 认证的 xxx.json 文件。如果你在同一文件夹下,只需运行以下命令:

export GOOGLE_APPLICATION_CREDENTIALS=./xxx.json

一旦完成并且 Terraform 命令执行成功,如果我们检查 Google Cloud Storage 中的存储桶,我们会发现一个新项,它包含我们在本地文件系统中使用的 terraform.tfstate 文件的内容。

现在我们可以通过改变基础设施并查看如何在 Google Cloud Storage 中的存储桶中反映这些变化来测试它是否正常工作。你可以通过运行 terraform destroy 并检查远程状态在 Google Cloud 中的变化来轻松做到这一点。

小心处理状态文件。它们包含关于你公司基础设施的非常重要的信息,可能会被用作攻击的切入点。

这个功能用于在工程团队中共享配置,它由 Terraform 完全管理:你无需手动拉取或推送状态文件,因为 Terraform 会为你处理这些操作。

修改你的基础设施

到目前为止,我们只创建了资源并将云数据中心的状态存储在在线存储桶中。接下来你将学习如何修改已有的基础设施,就像我们之前创建的项目一样。

如你所见,我们从一个非常简单的示例开始:创建一台带有临时 IP 地址的虚拟机(由 Google 分配的默认 IP 地址,非固定)。

现在,我们将创建一个静态 IP 并将其分配给我们的虚拟机,以确保它始终使用相同的 IP 地址。通过 Terraform 来实现这一点的方法是创建一个类型为 google_compute_address 的资源,如下所示:

resource "google_compute_address" "my-first-ip" {
 name = "static-ip-address"
}

现在,我们可以执行 terraform plan 来查看如果应用基础设施更改,哪些内容会发生变化。正如你在新的执行计划中看到的那样,Terraform 识别出我们需要创建一个新的 google_compute_address 类型的资源,但是……我们如何将这个 IP 地址附加到我们的虚拟机呢?让我们重新检查虚拟机的配置:

resource "google_compute_instance" "my-first-instance" {
 name = "my-first-instance"
 machine_type = "n1-standard-1"
 zone = "europe-west1-b"
 disk {
 image = "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
 }

 network_interface {
 network = "default"
 access_config {
 nat_ip = "${google_compute_address.my-first-ip.address}"
 } 
}

在代码的高亮行中,你可以看到将我们的虚拟机与我们将要创建的新地址关联是多么简单:我们创建的资源,即地址,将具有计算属性(在运行时计算的属性),这些属性可以在其他资源中使用。在 Terraform 中,插值值的语法是 ${},其中括号中的值为要插值的属性值,在本例中是名为 my-first-ip 的资源的 IP 地址。

如果你前往 Google Cloud 控制台并打开外部 IP 部分,你会看到类似于以下屏幕截图的内容:

IP 地址已按预期与我们的虚拟机关联。

Terraform 变量

我们之前没有提到的一点是 Terraform 可以与变量一起使用。请看以下定义:

provider "google" {
 credentials = "${file("xxx.json")}"
 project = "implementing-modern-devops"
 region = "europe-west1"
} 

这是我们提供程序的配置。有些字符串很可能会在其他地方使用,比如区域或项目名称。Terraform 有变量的概念,变量是一个可能会变化的值,因此我们可以将其提取到一个单独的文件中。到目前为止,我们已经创建了一个名为 resources.tf 的文件。现在,让我们创建一个名为 vars.tf 的文件,内容如下:

variable "project_name" {
 type = "string"
 default = "implementing-modern-devops"
}

variable "default_region" {
 type = "string"
 default = "europe-west1"
}

现在,我们将把这些变量应用到我们的文件中。默认情况下,Terraform 会查看当前文件夹中所有扩展名为 .tf 的文件,构建所有已描述事实的知识库,并开始根据需要创建我们的基础设施(内部构建一个依赖关系图,可以使用 terraform graph 命令进行检查)。这意味着我们不需要做任何特别的操作,Terraform 就能识别我们的变量文件:

provider "google" {
 credentials = "${file("xxx.json")}"
 project = "${var.project_name}"
 region = "${var.default_region}"
} 

我们可以在几乎任何地方使用变量,以便更轻松地创建我们的基础设施。如你所见,语法与插值时使用的语法相同;实际上,这就是插值。

在变量文件中,我们已经为这些变量指定了默认值,但我们也可能根据环境或调整配置时需要更改这些值。Terraform 还允许你通过三种方式来覆盖变量:

  • 在命令行中

  • 使用名为 terraform.tfvars 的文件

  • 使用环境变量

第一种方式就是简单地在 terraform 命令中添加额外的标志。例如,如果我们想在应用基础设施更改时修改 project_name,只需传递一个额外的标志,并指定变量的值:

terraform apply -var 'project_name=my-new-name' 

就是这样。你可以通过更改项目名称或区域来进行实验,看看terraform plan如何创建新的资源(因为它们在不同的项目中不存在)。

第二种方法是使用包含变量定义的文件。为了测试它,在你的项目根目录下创建一个名为terraform.tfvars的文件,并填入以下内容:

project_name = "my-new-project-name"

现在,如果你运行terraform plan,你将看到 Terraform 计划创建新资源,因为它们在名为my-new-project-name的项目中不存在。文件名不一定是terrafrom.tfvars,但如果你使用不同的名称创建它,Terraform 默认不会加载它,你需要传递-var-file标志来加载它。

在继续之前,别忘了移除terraform.tfvars文件。

覆盖变量的第三种方法是通过环境变量。这种方法特别有趣,因为它可以轻松地通过外部因素来管理不同环境的配置。约定是定义一个与 Terraform 中的变量同名的环境变量,但前缀加上TF_VAR_。例如,对于变量project_name,我们可以执行以下命令:

export TF_VAR_project_name=my-new-project-name

Terraform outputs

到目前为止,我们已经使用 Terraform 来创建我们的基础设施,但我们对我们的云环境(在本例中是 Google Cloud Platform)几乎没有任何了解。HashiCorp 的工程师们也考虑到了这一点,并创建了一个名为输出(output)的元素,允许我们打印脚本创建的资源的值。

到目前为止,我们有两个文件:

  • resources.tf

  • variables.tf

在继续之前,请确保你已经通过运行terraform apply创建了在线基础设施,就像我们之前所做的那样。

现在,我们将创建另一个文件,名为outputs.tf。这并非偶然。在 Terraform 中,这是推荐的项目布局方式,它有助于代码的可读性,并且能够分隔职责。

将以下内容添加到outputs.tf文件中:

output "instance_ip" {
 value = "${google_compute_instance.my-first-instance.network_interface.0.access_config.0.nat_ip}"
}

我们稍后会回到这个命令,但现在我们需要重新运行 apply 命令,以便让 Terraform 为我们创建输出。某些内容已经发生了变化:

google_compute_address.my-first-ip: Refreshing state... (ID: static-ip-address)
google_compute_instance.my-first-instance: Refreshing state... (ID: my-first-instance)

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

instance_ip = 23.251.138.171

即使你没有更改基础设施,Terraform apply也需要运行,才能使你的输出可用。

现在我们可以看到一个新的部分,叫做 outputs,它包含了我们在 outputs 文件中定义的值。如果你想随时查看它,只需运行以下命令:

terraform output instance_ip

或者,直接运行以下命令:

terraform output

第一个将只显示实例的 IP(这对于作为其他命令的输入特别有用)。第二个则显示你在 Terraform 脚本中定义的所有输出。

现在,让我们解释一下输出是如何工作的。在这个例子中,我们使用了以下字符串来识别我们想要输出的内容:

google_compute_instance.my-first-instance.network_interface.0.access_config.0.nat_ip

前两个键(由点分隔)很明确:我们的资源类型和名称。然后,IP 属于network_interface中的acccess_config部分,值存储在nat_ip中,但那里的 0 是怎么回事呢?

很简单;我们可以通过重复network_interface块多次来定义多个网络接口:代码中的第一个接口是0,第二个是1,依此类推...

这个属性路径有时可能很难计算,尽管大多数情况下从配置文件中是相当明显的。如果你在寻找想要输出的内容时遇到问题,下面有一个快捷方式:当你运行terraform apply时,在输出中你会看到类似这样的内容:

这是你可以在输出中显示的所有属性的列表;键是左侧的列。例如,如果我们想显示创建 VM 的区域,就像这样简单:

output "instance_zone" {
 value = "The zone is ${google_compute_instance.my-first-instance.zone}"
}

如你所见,这里的插值也起作用,允许你将字符串与 Terraform 资源的值混合在一起。

摘要

Terraform 是每个 DevOps 工程师必须掌握的基本工具,它允许你像管理代码一样管理基础设施,可以与 Google Cloud Platform 或 AWS 等云服务提供商高效工作,具有生命周期管理和部署基础设施的能力。

在本章中,我们看到了 Terraform 在创建虚拟基础设施方面的最重要内容。你已经学到了足够的知识,可以借助在线文档,创建资源并将它们连接起来,从而创建更大的项目。

即使我们在本章中遵循的示例相当基础,在下一章中,我们将创建更复杂的基础设施并安装所需的软件,以便以自动化方式运行它。

我们还将使用更高级的 Terraform 功能,例如模块,以创建高度可重用的组件,这些组件可以与不同的团队共享,甚至作为开源组件。

第六章:服务器提供

在上一章中,你学习了如何创建将承载我们应用程序的基础设施。正如我们所见,基础设施自动化是一个新兴的领域,我们使用了 Terraform 来实现它。Terraform 的问题在于它只能用于构建基础设施,但为了提供软件,我们需要其他工具。

在本章中,我们将深入探讨 Ansible,因为它与 Puppet 和 Chef 一起,是目前市场上最为流行的服务器提供工具。

本章将涵盖以下主要内容:

  • 服务器提供软件

    • Chef

    • Puppet

    • Ansible

  • Ansible

    • Ansible 配置

    • Ansible 变量

      • 变量

      • 远程事实

      • 模板

    • 流程控制

    • Ansible 角色

  • Ansible Tower

正如你所见,这是一章非常广泛的内容,包含了许多示例,帮助你学习 Ansible 的最重要功能。

在阅读本章时,你需要注意的一点是,无法在单一章节中展示 Ansible 的所有功能。公平地说,掌握所有功能并达到熟练水平可能需要一本书的内容。正如你现在可以猜到的那样,当我需要处理 Ansible 时,第一件事就是打开官方文档,并将其与代码并排放置,这样我可以随时参考它,获取我之前没有接触过或已经很久没有使用过的功能和示例。

我们还将探讨 Ansible Tower 部分,它是一款用于在堡垒主机模式下运行 Ansible playbook 的软件,可以在你的基础设施内运行,而不是在工作站上运行。

服务器提供工具

如前所述,软件提供的选项很少。在本章中,你将学习如何使用 Chef 和 Ansible,重点介绍 Ansible,因为它在许多公司中广泛使用,而且比 Chef 更容易掌握。

市场上还有其他有效且优秀的解决方案,但我们将特别关注 Ansible,因为对我而言,它是所有工具中最容易学习和扩展的。

Chef

Chef 是一款非常有趣的软件,它遵循堡垒主机原则,在我们的服务器上运行配置。堡垒主机是一台放置在私有网络中的服务器,它能够直接或通过代理访问我们的服务器,从而执行必要的操作,将它们设置为所需的状态。这是一个不容忽视的选项,因为服务器提供过程中的最大挑战之一是管理密钥和授权,例如 Ansible 就需要通过第三方软件(如 Red Hat 的 Ansible Tower)来改进这方面的功能。

Chef 使用食谱来配置服务器的各个部分。食谱基本上是一组声明性指令,定义了为了让服务器达到所需状态需要执行的操作。例如,看看这个:

execute "update-upgrade" do
   command "apt-get update && apt-get upgrade -y"
   action :run
end

package "apache2" do
   action :install
end

上面的代码将升级我们的系统,然后安装 Apache2 网络服务器。

一旦完成此食谱,它将从工作站上传到 Chef 服务器,关键点是:在 Chef 中,有三个角色:

  • 服务器

  • 工作站

  • 节点

服务器是存放食谱和配置的地方。在进行任何工作之前需要先安装,安装说明可以在 docs.chef.io/install_server.html 上找到。

Chef 服务器有三种模式:

  • 企业版:这可以安装在你的基础设施内部,并且需要许可证,所以你需要根据管理的节点数量支付费用。

  • 开源:这也可以在你的基础设施中安装,但没有任何支持。它是免费的,必须由你公司配置和维护。它也是企业版 Chef 的简化版本。

  • 托管:Chef 服务器托管在第三方硬件上,你无需担心其维护和升级。根据你公司设置的不同,这可能不是一个可选项。

节点是目标主机。每个节点都在 Chef 服务器上注册,并有一个运行列表:一旦执行 chef-client 命令,这个列表上的食谱将在主机上运行。

工作站是用来配置和上传 Chef 服务器的计算机。此计算机使用一个名为 knife 的软件,可以在 Chef 服务器上执行所有操作:

  • 配置角色

  • 根据角色和其他参数查找虚拟机

  • 配置运行列表

  • 管理机密

Knife 使用加密密钥与 Chef 服务器进行通信,因此所有通信都是以受信方式进行的。

现在,如果我们想把一切都形象化,看起来会像下面的图示:

如你所见,尽管设置相当复杂(你需要设置几个软件组件),但也有明显的好处:我们的 Chef 服务器位于基础设施的防火墙后,在隔离区中,但它通过 CLI 工具进行管理,因此我们所有的机密和配置都安全地保存在我们的基础设施内。

Chef 有一个陡峭的学习曲线,一旦我们度过了初学阶段,就会变得非常熟悉,添加新功能和使用 Ruby 强大的功能扩展 DSL 变得非常容易,且界面设计非常合理。

Puppet

Puppet 已经存在一段时间,并且在 DevOps 世界中被广泛使用。Puppet 有两个版本:

  • 开源

  • 企业版

开源版本原封不动地提供,提供了一套完整的功能,可以让你完全自动化基础设施的配置管理。

企业版除了支持外,还附带一套扩展的功能,使得你公司中的工程师工作更加轻松。

和 Chef 一样,Puppet 也遵循堡垒主机架构:服务器安装在你的基础设施中的非军事区内,节点(你的服务器)通过 Puppet 代理执行指定的任务,以达到所需状态。

Chef 和 Puppet 之间的主要区别在于,Puppet 是声明性的,而 Chef 更像是命令式的:

  • 在 Puppet 中,你指定希望服务器处于什么状态,Puppet 会负责保持它们在该状态。

  • 在 Chef 中,你声明一系列步骤,旨在将服务器引导到所需的状态。

也就是说,Chef 还允许你声明守卫条件,这些是执行步骤的前提条件。

根据我的经验,我发现来自 DevOps 背景的人更容易接受 Puppet,因为它类似于他们多年来做过的事情,而编写 Chef 的食谱更像是软件开发。

Ansible

Ansible 是我们将在本书其余部分开发内容时使用的工具。依我看,它是最容易学习和扩展的。它也容易理解,并提供了一个功能非常全面的开源版本,涵盖了 Ansible 的所有特性。你还可以购买 Ansible Tower(或类似工具)的许可证,以便在堡垒主机配置下运行 Ansible Playbooks,就像 Chef 或 Puppet 一样。

Ansible 基本上是一个领域特定语言DSL),用于在清单中定义的远程主机上执行操作。

Ansible 通过通过 SSH 在目标服务器上运行 playbooks 来工作,因此与 Chef 或 Puppet 不同,我们不需要在远程主机上安装任何东西;只需能够通过 SSH 连接即可。Playbook 基本上是一个另一种标记语言YAML),它包含一系列指令,用于将服务器引导到所需状态,就像我们执行一个 Bash 脚本一样。Playbook 看起来像这样:

---
- hosts: webservers
 vars:
 http_port: 80
 max_clients: 200
 remote_user: root
 tasks:
 - name: ensure apache is at the latest version
 yum: name=httpd state=latest
 - name: write the apache config file
 template: src=/srv/httpd.j2 dest=/etc/httpd.conf
 notify:
 - restart apache
 - name: ensure apache is running (and enable it at boot)
 service: name=httpd state=started enabled=yes
 handlers:
 - name: restart apache
 service: name=httpd state=restarted

通过阅读文件,你会理解如何轻松直观地明白 Playbook 正在做什么。

正如你所看到的,在第二行中,我们指定了希望在名为webservers的主机上运行此 Playbook。这个定义也可以在 Ansible 的另一部分:清单中进行。Ansible 清单基本上是一个包含你基础设施中主机列表的文件,如下所示:

[webservers]
host1
host2

[dbservers]
192.168.0.[1:3]

这个文件非常简洁,但也可能变得相当复杂:

  • 方括号中的名称是组名。

  • 组包含可以用生成器定义的主机,或者它们可以只是列出。

  • 组可以拥有特定于它们的配置,甚至覆盖变量。

在前面的例子中,我们有两个组:webserversdbservers

Web 服务器只有两个主机:

  • Host1

  • Host2

Dbservers 使用生成器,我们有三个主机:

  • 192.168.0.1

  • 192.168.0.2

  • 192.168.0.3

如前所述,我们还可以在清单中定义变量。这些变量可以作用于组和主机。让我们看看以下清单:

[dbservers]
192.168.0.[1:3]

[webservers]
host1 role=master
host2

[dbservers:vars]
timezone=utc

正如你所看到的,我们有两个变量:

  • timezone:这适用于 dbservers 组中的所有主机。

  • role:这适用于 webservers中的主机host1

这个变量可以在 playbooks 中使用,以便为特定主机提供特定的配置,正如我们稍后在本章中看到的那样。

组也可以组合成更大的组:

[dbservers]
192.168.0.[1:3]

[webservers]
host1
host2

[mongoservers]
10.0.0.1
10.0.0.2

[dataservers:child]
mongoservers
dbservers

在前面的清单中,我们可以找到以下内容:

  • dbservers

  • mongoservers

  • webservers

  • dataservers

  • all

  • ungrouped

尽管我们没有指定,但 Ansible 总是有两个默认的组,分别是 allungrouped,这两个组名称具有自描述性:all 是清单中的所有主机,ungrouped 是未在任何组中指定的所有主机。

如前所述,Ansible 不遵循像 Chef 或 Puppet 那样的堡垒主机架构,而是遵循客户端/服务器架构:我们的主机需要能够到达目标主机(清单中的主机)才能工作。

这可能会根据您的基础设施架构带来不便,但可以通过使用 Ansible Tower 或 Rundeck 从您的隔离区内执行 Ansible playbooks 来解决。

在本章中,我们将使用 Ansible 与 Terraform 结合构建真实的生产级示例,以便更好地理解这些工具的实际应用。

Ansible

在本节中,我们将迈出使用 Ansible 进行更全面示例的第一步。现在,我们将安装和配置 NGINX,这是一个非常流行的 Web 服务器,以便展示 Ansible 的主要概念。

首先,我们将使用 Google Cloud Platform 创建一个虚拟机,并为其分配一个静态 IP,以便我们可以从清单中进行定位。我们将使用 Terraform 来完成这个操作。首先,我们来看一下我们的资源文件:

provider "google" {
  credentials = "${file("account.json")}"
  project = "${var.project_name}"
  region = "${var.default_region}"
}

resource "google_compute_instance"
"nginx" {
  name = "nginx"
  machine_type = "n1-standard-1"
  zone = "europe-west1-b"
  disk {
   image = "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
  }
  network_interface {
    network = "default"
    access_config {
      nat_ip = "${google_compute_address.nginx-ip.address}"
    }
  }
}

resource "google_compute_address" "nginx-ip" {
  name = "nginx-ip"
}

现在,我们来看一下我们的 vars 文件:

variable "project_name" {
  type = "string"
  default = "implementing-modern-devops"
}

variable "default_region" {
  type = "string"
  default = "europe-west1"
}

在本例中,我们将重用前一章节的项目,因为一旦完成,我们可以方便地关闭所有内容。现在,我们运行我们的计划,看看将要创建哪些资源:

+ google_compute_address.nginx-ip
 address: "<computed>"
 name: "nginx-ip"
 self_link: "<computed>"

+ google_compute_instance.nginx
 can_ip_forward: "false"
 disk.#: "1"
 disk.0.auto_delete: "true"
 disk.0.image: "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
 machine_type: "n1-standard-1"
 metadata_fingerprint: "<computed>"
 name: "nginx"
 network_interface.#: "1"
 network_interface.0.access_config.#: "1"
 network_interface.0.access_config.0.assigned_nat_ip: "<computed>"
 network_interface.0.access_config.0.nat_ip: "<computed>"
 network_interface.0.address: "<computed>"
 network_interface.0.name: "<computed>"
 network_interface.0.network: "default"
 self_link: "<computed>"
 tags_fingerprint: "<computed>"
 zone: "europe-west1-b"

Plan: 2 to add, 0 to change, 0 to destroy.

到目前为止,一切看起来都正确。我们正在创建两个资源:

  • 静态 IP

  • 虚拟机

现在,我们可以应用我们的基础设施:

google_compute_address.nginx-ip: Creating...
 address: "" => "<computed>"
 name: "" => "nginx-ip"
 self_link: "" => "<computed>"
google_compute_address.nginx-ip: Still creating... (10s elapsed)
google_compute_address.nginx-ip: Creation complete
google_compute_instance.nginx: Creating...
 can_ip_forward: "" => "false"
 disk.#: "" => "1"
 disk.0.auto_delete: "" => "true"
 disk.0.image: "" => "ubuntu-os-cloud/ubuntu-1704-zesty-v20170413"
 machine_type: "" => "n1-standard-1"
 metadata_fingerprint: "" => "<computed>"
 name: "" => "nginx"
 network_interface.#: "" => "1"
 network_interface.0.access_config.#: "" => "1"
 network_interface.0.access_config.0.assigned_nat_ip: "" => "<computed>"
 network_interface.0.access_config.0.nat_ip: "" => "35.187.81.127"
 network_interface.0.address: "" => "<computed>"
 network_interface.0.name: "" => "<computed>"
 network_interface.0.network: "" => "default"
 self_link: "" => "<computed>"
 tags_fingerprint: "" => "<computed>"
 zone: "" => "europe-west1-b"
google_compute_instance.nginx: Still creating... (10s elapsed)
google_compute_instance.nginx: Still creating... (20s elapsed)
google_compute_instance.nginx: Creation complete

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

一切按预期工作。如果我们检查 Google Cloud Platform,我们可以看到我们的虚拟机已创建并关联了一个公共 IP:

在这种情况下,关联的公共 IP 是 35.187.81.127。重要的是验证我们是否可以通过 SSH 访问该服务器。为此,只需点击实例行右侧的 SSH 按钮,它应该会打开一个带有终端访问权限的 Cloud Console 窗口。

如果 SSH 访问失败,您需要在防火墙中为端口22添加一个入站允许规则。以这个例子为例,允许任何 IP 访问任何端口,但在您的实际基础设施中请不要这么做,因为这会带来安全隐患。

一旦一切启动并运行,就可以开始使用 Ansible。首先,我们将创建我们的清单文件:

[nginx-servers]
35.187.81.127

这非常简单:一个包含我们公有 IP 地址的组,该组连接到我们的虚拟机。将文件保存为 inventory,并放置在一个新的文件夹中,例如命名为 ansible-nginx。创建好 inventory 后,我们需要验证所有主机是否可达。Ansible 为此提供了一个工具:

ansible -i inventory all -m ping

如果你执行上述命令,Ansible 将 ping(实际上,它并没有使用 ping 命令,而是尝试发起与服务器的连接)所有在参数 -i 中指定的 inventory 主机。如果你将所有内容替换为一个组的名称,Ansible 只会尝试访问该组中的主机。

让我们看看命令的输出:

35.187.81.127 | UNREACHABLE! => {
 "changed": false,
 "msg": "Failed to connect to the host via ssh: Permission denied (publickey).\r\n",
 "unreachable": true
}

我们在连接远程主机时遇到了问题,原因是我们没有任何密钥可以供主机验证以确认我们的身份。这是预期的,因为我们没有进行配置,但现在,我们将通过创建一个密钥对并使用 Google Cloud SDK 安装到远程主机来解决这个问题:

gcloud compute ssh nginx

这个命令将做三件事:

  • 生成一个新的密钥对。

  • 在我们的远程虚拟机上安装密钥对。

  • 在 GCP 中打开我们的虚拟机 shell。

生成的新密钥可以在 ~/.ssh/ 目录下找到,名称为 google_compute_enginegoogle_compute_engine.pub(私钥和公钥)。

一旦命令执行完成,我们的 shell 应该像这样:

现在我们有一个连接到虚拟机的终端,并且可以执行命令。gcloud 默认配置了一个用户;在我的案例中是 davidgonzalez,该用户可以在不输入密码的情况下使用 sudo。在这种情况下,我们将以 root 用户身份执行 playbook,因此我们需要能够以 root 用户身份登录虚拟机。将 ~/.ssh/authorized_keys 文件复制到 /root/.ssh/authorized_keys 中,这样我们就可以做到这一点。因此,我们已经将之前生成的公钥复制到了 root 用户的授权密钥集中。

一般来说,应尽量避免使用 root 权限,但在这种情况下,为了方便,我们将以 root 用户身份执行 playbook。

为了让 Ansible 能够使用该密钥,我们需要将它添加到服务器上的守护进程中:

ssh-add ~/.ssh/google_compute_engine

这个命令应该输出成功信息,说明身份验证已添加。

现在我们可以再次运行我们的 pin 命令:

ansible -i inventory all -m ping

输出应该会有很大不同:

35.187.81.127 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}

这意味着现在 Ansible 可以访问我们的服务器;因此,它将能够在该服务器上执行 playbook。

现在是时候开始编写我们的第一个 ansible playbook 了。在同一个文件夹 ansible-nginx 中,创建一个名为 tasks.yml 的文件,内容如下:

---
- hosts: all
 user: root
 tasks:
 - name: Update sources
 apt:
 update_cache: yes
 - name: Upgrade all packages
 apt:
 upgrade: dist

这很容易理解:

  • 我们的 playbook 将影响所有主机。

  • 运行 playbook 的用户将是 root。

  • 然后我们将执行两个任务:

    • 更新 apt cache

    • 升级所有软件包。

一旦我们拥有了两个文件(inventory 和 playbook),我们可以运行以下命令:

ansible-playbook -i inventory tasks.yml

我们应该生成类似如下的输出:

在本章中,我们将运行几个 playbook,所以我建议你保持相同的虚拟机处于运行状态,并将它们全部执行,以节省时间和资源。Google Cloud Platform 的试用账户会为你提供足够的空间,可以在几天或几周内运行它们。

让我们解释一下输出:

  • 首先,它指定了我们要在哪个组上执行 playbook。在这个例子中,我们指定了组为all

  • 然后,我们可以看到三个任务被执行。如你所见,描述与tasks.yml中指定的描述相匹配。这在理解你的 playbook 输出时非常有用,尤其是在它们失败时。

  • 然后我们得到一个总结:

    • 执行了三个任务

    • 其中两个在服务器上产生了变化

    • 零个失败

简单有效。这是最接近在服务器上执行脚本的方式:一组指令,一个目标主机及其输出。

在 Ansible 中,动作被封装到模块中,而不是使用简单的 bash 指令。模块是 DSL 的一个组件,允许你执行特殊的操作。在之前的 playbook 中,apt 是一个包含在 Ansible 核心中的模块。有关它的文档可以在docs.ansible.com/ansible/apt_module.html找到。

让我们再看看我们使用apt模块的一个例子:

- name: Update sources
 apt:
 update_cache: yes

如你所猜测,这相当于以下内容:

apt-cache update

因此,在这种情况下,Ansible 提供了一个名为 command 的不同模块,允许我们在我们的清单主机上执行命令。请看下面的yaml

- name: Update sources
 command: apt-cache update

这相当于之前的yaml,两者执行相同的操作:更新apt-cache

一般来说,如果给定任务有模块可用,建议使用它,因为它比执行等效命令更好地处理(或至少能期望它处理)错误和输出。

现在,一旦我们的 playbook 成功执行,我们可以预期我们的系统已更新。你可以通过再次运行 playbook 来检查它:

现在你可以看到,只有一个任务在服务器上产生了变化(更新了 apt 源)。

Ansible 配置

Ansible 的一个特点是能够为每个项目覆盖默认设置。为了实现这一点,我们只需在项目根目录创建一个名为ansible.cfg的文件,Ansible 会读取该文件并应用配置。

有大量可以配置的参数,所有这些参数都可以在官方文档中找到,网址是docs.ansible.com/ansible/intro_configuration.html

如你所见,Ansible 的文档非常好,大多数时候,它能为你的问题提供答案。

让我们看看配置是如何帮助我们的。如果你还记得前面的例子,我们已经指定了-i标志,用于告诉 Ansible 库存文件的位置。Ansible 对此有默认值,即/etc/ansible/hosts。在我们的小项目中,库存文件与代码放在同一文件夹中,为了让 Ansible 知道这一点,我们需要创建一个如下内容的配置文件:

[defaults]
inventory = ./inventory

现在,我们使用以下命令再次运行我们的playbook

ansible-playbook tasks.yml

我们没有指定主机列表,但 Ansible 在读取了ansible.cfg后知道库存文件可以位于./inventory

Ansible 有一套配置优先级的层级结构:

  • ANSIBLE_CONFIG 环境变量

  • ansible.cfg

  • .ansible.cfg

  • /etc/ansible/ansible.cfg

因此,如果我们定义一个名为ANSIBLE_CONFIG的环境变量指向一个文件,Ansible 将从该位置读取配置文件,其他选项将被忽略。这在环境隔离时特别有用:我们的 CI 服务器可以在环境文件中定义自己的配置,而开发者则可以将ansible.cfg文件提交到源代码控制中,以便在团队中共享。

ansible.cfg 文件中可以指定几个部分。各个部分控制 Ansible 的不同方面,如连接。在某些情况下,我们可能需要为 ssh 添加特殊的参数,只需在 ansible.cfg 文件中添加以下几行:

[ssh_connection]
ssh_args=<your args here>

Ansible 变量、远程事实和模板

变量和模板是 Ansible 中非常重要的一部分。它们允许我们覆盖配置中的值(如服务器和剧本),使得我们可以编写通用的剧本,通过少量修改即可在不同的配置中重用。通过模板,我们可以从主机渲染配置文件,这样我们就可以利用 Ansible 来管理远程服务器的配置,几乎不需要任何额外的工作。它还可以用来为不同的主机生成和安装 SSL 证书,且对用户透明。

它们(变量和模板)都使用一个名为 Jinja2 的模板引擎,它允许在我们的配置中嵌入逻辑和插值。

一般来说,有几种方式来定义变量,但我们只会探讨最常见的几种(根据我的标准),否则我们需要几个章节来详细记录这些方法。如果你想深入了解定义变量的不同方式,官方文档提供了一个相当全面的指南,地址是 docs.ansible.com/ansible/playbooks_variables.html

Ansible 变量

变量是最简单的自定义选项。通过变量,我们可以定义将在剧本中替换的值。让我们看看以下的剧本:

---
- hosts: all
  user: root
  tasks:
  - debug:
    msg: "Hello {{ myName }}! I am {{ inventory_hostname }}"

用之前的代码片段替换 tasks.yml 的内容。我们的任务中有两个新的符号。此外,我们的任务是新的:debug 用于在执行 playbook 时将变量的值输出到终端。让我们看看执行情况(我们将使用与之前示例相同的配置):

ansible-playbook -i inventory tasks.yml

它失败了:

失败的原因可以从信息中看到:我们定义了一个名为 name 的变量,但没有为其指定值。如果存在无法插值的值,Ansible 将失败并中止任务的执行。

这里还有一个有趣的信息:Ansible 给了你一个参数,允许你仅在那些未成功的主机上重试 playbook。如果我们只想在失败的主机上重试 playbook,可以运行以下命令:

ansible-playbook -i inventory tasks.yml --limit @/Users/dgonzalez/code/ansible-variables/tasks.retry

新的参数 tasks.retry 是一个包含可重新运行失败主机列表的文件。

回到我们缺失的变量,我们需要定义一个名为 myName 的变量。定义变量的方式有几种;第一种是在命令行中定义:

ansible-playbook -i inventory tasks.yml -e myName=David

你可以看到,现在 playbook 的输出看起来更好了:

如你所见,变量已经被插值,我们可以看到信息 Hello David! I am 35.187.81.127

定义变量的第二种方式是通过库存,如我们之前所见:

[nginx-servers]
35.187.81.127 myName=DavidInventory

如果我们修改库存以匹配前面的代码片段,那么变量的值将是 DavidInventory,我们就不需要在命令行中传递值:

ansible-playbook -i inventory tasks.yml

这将输出信息 Hello DavidInventory! I am 35.187.81.127

在 Ansible 中定义变量的第三种方式是在 playbook 本身中定义。请看下面的 playbook:

---
- hosts: all
 vars:
 myName: David
 user: root
 tasks:
 - debug:
 msg: "Hello {{ myName }}! I am {{ inventory_hostname }}"

听起来很简单,一旦你在 playbook 的 vars 部分定义了变量,它就会变得可用;因此,其他地方不需要指定值。

定义变量的第四种方式是通过文件。Ansible 旨在成为一个自文档化的组件,便于没有太多经验的人理解。Ansible 让理解 playbook 任务变得更容易的一个方法就是可以将每一段配置写入文件。变量也不例外,因此 Ansible 允许你在文件或 playbook 中定义变量。

我们从文件开始。在你工作的同一文件夹中(即包含 playbook 和库存的文件夹)创建一个名为 vars.yml 的文件,并添加以下内容:

myName: DavidFromFile
yourName: ReaderFromFile

现在我们可以运行以下命令来使用变量文件:

ansible-playbook -i inventory playbook.yml -e @vars.yml

如果你查看输出,它将与之前的输出相同。

在这种情况下,我们定义了一个新的变量(yourName),但我们并没有使用它,这也没关系。我只是想向你展示,如果存在未绑定的插值,Ansible 会报错,但如果有未使用的变量,它是不会抱怨的。

在这个例子中,我们通过命令行在剧本中包含了vars.yml,并且在文件名前面加上了@符号,然而在 Ansible 中还有另一种使用变量文件的方式:从剧本内部包含它们。让我们来看一下如何操作:

---
- hosts: all
 user: root
 tasks:
 - name: Include vars
 include_vars:
 file: vars.yml
 - debug:
 msg: "Hello {{ myName }}! I am {{ inventory_hostname }}"

在这个例子中,我们在我们的剧本中使用了include_vars模块。现在,使用以下命令执行剧本:

ansible-playbook -i inventory tasks.yml

你将得到以下输出:

正如你所看到的,有一个额外的任务,它接受一个文件并将变量注入到上下文中。

这个模块非常灵活,有多种方式可以将变量文件包含到我们的剧本中。我们使用的是最直接的一种,但你可以在官方文档中查看其他选项:docs.ansible.com/ansible/include_vars_module.html

另一种将变量文件包含到剧本中的方法是使用剧本中的vars_files指令:

---
- hosts: all
 user: root
 vars_files:
 - vars.yml
 tasks:
 - debug:
 msg: "Hello {{ myName }}! I am {{ inventory_hostname }}"

这将获取vars.yml文件,并将所有定义的变量注入到上下文中,使它们可以在后续使用。

正如你所看到的,Ansible 在定义变量方面非常灵活。

在 Ansible 中,还有一种有趣的设置变量的方式,它帮助我们进一步自定义剧本:set_fact。设置事实允许我们在剧本中动态地设置变量。set_fact 可以与另一个有趣的指令register结合使用。让我们看一个例子:

---
- hosts: all
 user: root
 tasks:
 - name: list configuration folder
 command: ls /app/config/
 register: contents
 - set_fact:
 is_config_empty: contents.stdout == ""
 - name: check if folder is empty
 debug: msg="config folder is empty"
 when: is_config_empty
 - name: installing configuration
 command: <your command here>
 when: is_config_empty

我们在这里所做的基本上是,如果我们应用的配置文件夹为空(假设的配置文件夹),就将变量设置为true,以便只有在它不存在时才会重新生成。这是通过使用when指令来实现的,它允许我们根据条件执行指令。我们将在本章后面回到这个话题。

我们已经介绍了最常见的定义变量的方法,但还有一个问题待解答:不同定义变量方法的优先级是什么?

这是我在使用 playbook 时必须自问的问题,实际上,最终你只会使用几种方法来创建变量,因此它的重要性并不像它应该有的那样高。在我个人的做法中,当不使用角色时,我倾向于创建一个包含变量的文件,如果我需要覆盖某个值,我会在命令行(或环境变量)中进行设置,这也是链条中优先级最高的地方。完整的变量优先级列表可以在docs.ansible.com/ansible/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable找到。

Ansible 远程事实

Ansible 中的远程事实是一种通过显式配置文件或脚本来指定远程主机配置的方式,脚本返回有关服务器的数据。一般来说,这个功能对于执行诸如维护、设置专门标记主机为不在池中的标志等操作非常有用,以确保我们的 playbook 对这些主机没有任何影响。

看一下以下命令(假设之前示例中的清单文件已经在文件夹中,且虚拟机正在 Google Cloud Platform 上运行):

ansible all -m setup -i inventory --user=root

这将输出大量数据(JSON 格式的数据)。这些数据包含了关于远程主机的所有已知事实,例如 CPU 类型、机器 ID、网络接口、内核版本等。它们可以在我们的 playbook 中使用,但也可以扩展,添加更多由远程主机控制的数据,而无需任何本地配置。

为了设置自定义的远程事实,我们有几个选项,但最终,自定义事实默认是通过 JSON 文件在/etc/ansible/facts.d/下定义的。也可以在同一个文件夹中创建一个可执行文件(脚本),这样 Ansible 会执行它并将输出作为事实,添加到事实范围内。来看一下下面的文件:

{
 "my_name": "David Gonzalez"
 }

将它放入远程主机(前面所有示例中使用的主机)中,并在/etc/ansible/facts.d/example.facts中创建一个包含之前内容的文件*。

完成后,运行以下命令:

ansible all -m setup -i inventory --user=root | grep -B 3 -A 3 my_name

它看起来几乎像是魔法一样,但现在你的命令输出应该包括你之前创建的事实:

 },
 "ansible_local": {
 "example": {
 "my_name": "David Gonzalez"
 }
 },
 "ansible_lsb": {

现在它们可以在你的 playbook 中通过ansible_local变量使用,例如,访问my_name*:

---
- hosts: all
 user: root
 tasks:
 - name: Gather my_name fact.
 debug: msg="{{ ansible_local.example.my_name }}"

如前所述,Ansible 还可以从放置在事实路径中的脚本中收集事实。这个脚本应该有x标志,表示它是可执行的,并且应该有.fact扩展名。让我们来看一个我觉得非常有用的有趣技巧。当我尝试诊断我们系统中的故障时,我首先检查的就是 CPU 使用率。大多数时候,我们的系统都是高度可观测的(已经在监控中),因此很容易检查 CPU 负载,但有时候,监控可能并未部署。

首先,进入我们在前面例子中使用的服务器,并在 /etc/ansible/facts.d/cpuload.fact 路径下创建一个文件,内容如下:

#!/bin/bash
CPU_LOAD=`grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage "%"}'`
echo { \"cpu_load\": \"$CPU_LOAD\"}

这是一个简单的脚本,它将输出包含系统 CPU 负载信息的 JSON 格式数据。文件创建后,给它执行权限:

chmod u+x /etc/ansible/facts.d/cpuload.fact

完成了。在断开 SSH 会话之前,确保通过执行脚本来验证它是否按预期工作:

/etc/ansible/facts.d/cpuload.fact

这应该输出类似以下内容:

{ "cpu_load": "0.0509883%"}

现在是时候测试我们编写的事实了。我们将创建一个 playbook,获取 CPU 负载并通过调试消息将其输出到终端。内容如下:

- hosts: all
 user: root
 tasks:
 - name: Get CPU load
 debug: msg="The CPU load for {{ ansible_hostname }} is {{ ansible_local.cpuload.cpu_load }}"

运行之前的 playbook:

ansible-playbook -i inventory tasks.yml

你应该会得到如下类似的输出:

现在,我们有了一个基础工具,可以通过一个简单的命令检查服务器上的 CPU 负载,利用主机组来管理 Ansible。

有一件事我们还没解释,就是 Ansible 在每个 playbook 中输出的第一个任务:收集事实。

这个任务获取我们在本节中提到的所有事实,并为 playbook 执行创建上下文,因此在这种情况下,我们得到的 CPU 负载是执行该任务时收集到的 CPU 负载。

Ansible 模板

模板是 Ansible 的另一项强大工具。它们允许我们渲染配置文件、应用程序属性和任何可以存储在人类可读文件中的内容。

模板高度依赖变量和模板引擎 Jinja2,Ansible 使用 Jinja2 渲染模板。首先,我们将使用一个简单的 playbook 在服务器上安装 nginx

---
- hosts: all
 user: root
 tasks:
 - name: Update sources
 apt:
 update_cache: yes
 - name: Upgrade all packages
 apt:
 upgrade: dist
 - name: Install nginx
 apt:
 name: nginx
 state: present

正如你所看到的,这非常简单:

  • 更新 apt 缓存

  • 升级系统

  • 安装 nginx

现在,只需运行之前创建的 VM 上的 playbook:

ansible-playbook -i inventory tasks.yaml

当 playbook 执行完成后,你应该在远程服务器上看到 nginx 正在运行。为了验证这一点,只需打开浏览器,并使用你的 VM 的 IP 地址作为 URL。你应该能看到 nginx 的欢迎页面。

现在,我们将创建一个包含 nginx 配置的模板,在这个模板中我们可以轻松地添加或移除服务器。请在当前目录(即 playbook 所在的目录)下创建一个名为 nginx-servers 的文件夹,并添加一个名为 nginx.yml 的文件,内容如下:

---
- hosts: all
 user: root
 vars_files:
 - vars.yml
 tasks:
 - name: Update sources
 apt:
 update_cache: yes
 - name: Upgrade all packages
 apt:
 upgrade: dist
 - name: Install nginx
 apt:
 name: nginx
 state: present
 - template:
 src: nginx-servers/nginx-one.conf.j2
 dest: /etc/nginx/sites-enabled/default
 owner: root
 - service:
 name: nginx
 state: reloaded

让我们稍微解释一下这个文件:

  • 系统通过 apt 进行升级。

  • 同样使用 apt 安装,nginx 被成功安装。请注意,Ansible 使用声明式方法来安装软件包:你声明软件包的名称以及执行 playbook 后软件包的目标状态。

  • Playbook 会从名为 nginx-one.conf.j2 的模板渲染虚拟服务器的 nginx 配置。我们稍后会详细讲解这一部分。

  • Playbook 会重新加载 nginx 服务,使新配置生效。

在前面的 playbook 中我们缺少了几个模块。第一个模块是名为 nginx-one.conf.j2 的文件。这个文件是一个模板,用于渲染服务器中虚拟主机的 nginx 配置。让我们来看一下该文件的内容:

server {
   listen {{ server_one_port }} default_server;
   index index.html;
}

创建一个名为 sites-enabled 的文件夹,并将 nginx-one.conf.j2 文件添加到其中,内容如前所述。这个文件是一个标准的 nginx 服务器块,但有一个特殊之处:我们将 server_one_port 作为端口的占位符,以便控制 nginx 虚拟主机暴露的端口。这对我们来说很熟悉:我们正在使用变量来渲染模板。

第二个模块是名为 vars.yml () 的文件,内容如下:

server_one_port: 3000

这非常简单:它只是定义了渲染之前模板所需的变量。使用模板时需要注意的一点是,模板中的所有变量都可以在上下文中访问,从远程服务器收集的事实到各个地方定义的变量。

一旦我们把所有东西准备好(前面提到的两个文件、前面的 playbook 和之前示例中的清单),我们就可以像往常一样运行 playbook,并验证一切是否按预期工作:

ansible-playbook -i inventory nginx.yml

如果一切按预期工作,你应该在 Google Cloud Platform 上的虚拟机中拥有一个完全功能的 nginx 服务器(提供默认页面),并且运行在 3000 端口。

Google Cloud Platform 默认启用拒绝策略以增强安全性,因此你可能需要调整防火墙设置,以允许某些端口的入站流量。

流程控制

在 Ansible 中,可以使用流程控制语句,例如使用变量作为输入的循环或条件语句。这可以用来对某些数据集重复执行任务,或者在未满足某些条件时避免执行某些任务:例如,我们可能希望根据服务器的底层系统使用不同的命令。

在之前的示例中,我们已经看过使用 when 子句的条件语句,但让我们再解释一下:

---
- hosts: all
 user: root
 tasks:
 - command: /bin/false
 register: result
 ignore_errors: True

 - debug: msg="fail!"
 when: result|failed

 - debug: msg="success!"
 when: result|succeeded

上述代码非常易读:执行一个命令(忽略可能出现的错误,以便我们的 playbook 继续执行),并注册一个名为 result 的变量。接下来,我们有两个调试任务:

  • 第一个技巧只有在 /bin/false 命令失败时才会被执行。

  • 第二个技巧只有在 /bin/false 命令成功执行后才会被执行。

在这个 playbook 中,我们使用了两个新技巧:

  • ignore_errors:使用这个子句时,如果任务失败,playbook 将继续执行后续任务。如果我们想测试系统中的假设,比如某些文件是否存在或某个网络接口是否已配置,这个功能非常有用。

  • 管道符号 (|):这个符号叫做 pipe。它是一个 Jinja2 表达式,用于过滤值。在这个例子中,我们使用了 failed 和 succeeded 过滤器,根据命令的执行结果返回 true 或 false。Jinja2 提供了许多过滤器,可以像 Unix 管道一样处理数据。

另一种控制流结构是循环。让我们来看看循环是如何工作的:

---
- hosts: all
 user: root
 vars:
 names:
 - David
 - Ester
 - Elena
 tasks:
 - name: Greetings
 debug: msg="Greetings {{ item }}! live long and prosper."
 with_items: "{{ names }}"

在这里,我们使用了一个我们在解释变量时没有看到的新内容:它们可以具有诸如列表和字典这样的结构。在此示例中,我们定义了一个包含几个名字的列表,并为每个名字输出一条消息。现在是时候运行 playbook 了。将前面的内容保存到名为 loops.yml 的文件中,并执行以下命令:

ansible-playbook -i inventory loops.yml

我们假设库存与前面示例中使用的相同。完成后,你应该在终端中看到类似以下的输出:

也可以使用紧凑版本的声明来定义列表。请查看以下语句:

 names:
 - David
 - Ester
 - Elena

这可以重新定义如下:

names: ['David', 'Ester', 'Elena']

这完全是等效的。

在 Ansible 中也可以定义字典并将其用作变量。它们还可以作为可迭代元素使用,这使得我们能够为数据赋予结构:

---
- hosts: all
 user: root
 vars:
 namesAge:
 - name: David
 age: 33
 - name: Ester
 age: 31
 - name: Elena
 age: 1
 tasks:
 - name: Presentations
 debug: msg="My name is {{ item.name }} and I am {{ item.age }} years old."
 with_items: "{{ namesAge }}"

如果你熟悉软件开发,前面的代码片段应该能让你完全理解:一个结构化数据的列表(对象数组),它包含需要通过键来访问的信息。

在本书的其余部分,我们将使用 Ansible 中更高级的流程控制结构,并在过程中进行解释。如果你想进一步了解,可以参考以下链接:

角色

我们一直在编写一些 Ansible playbook,正如你可以想象的那样,其中有很多内容可以抽象成通用的工作单元。目前,凭借我们对 Ansible 的了解,我们能做的最好的事情就是为 playbook 和文件使用命名约定,以免将它们混淆,但 Ansible 提供了一种更好的方法:角色。

把角色想象成软件中常见的可重用功能模块:一组高度内聚的 playbook、变量和资源,它们共同协作,完成一个特定的目的。例如,如果我们在管理 nginx,将所有相关资源放在一个单独的模块(在此情况下是角色)中,可以提高代码的重用性和清晰度。

一个选择是使用 Ansible 特性来包含 playbook。虽然我们之前没有讲过,但使用 Ansible,可以包含带任务的 YAML 文件来创建依赖关系,如下所示:

---
- include: play-include.yml
- hosts: all
 user: root
 tasks:
 - debug: msg="I am the main playbook"
 - include: tasks-include.yml

让我们解释一下发生了什么。我们可以看到两个包含的文件:

  • 第一个包含的是 Ansible 所谓的 play 包含。它本身就是一个功能完整的 playbook,作为模块被包含到另一个 playbook 中。

  • 第二个包含的是 Ansible 所谓的任务包含。它只包含一组任务列表。

通过查看两个文件的内容,可以轻松地解释这一点。首先,查看 play-include.yml 的内容:

---
- hosts: all
 user: root
 tasks:
 - debug: msg="I am a play include"

其次,查看 tasks-include.yml 的内容:

---
- debug: msg="I am a task include"
- debug: msg="I am a task include again"

现在我们将执行之前的 playbooks,看看输出结果是什么。将第一个 playbook 的内容保存在一个名为 tasks.yml 的文件中,并使用与之前所有示例相同的清单。现在运行以下命令:

ansible-playbook -i inventory tasks.yml

执行完成后,让我们检查输出,应该与以下内容非常相似:

让我们来解释一下:

  1. play 包含(play-include.yml)通过输出其中的调试信息来执行。

  2. 主 playbook 中的调试任务被执行。

  3. 任务包含(tasks-include.yml)通过执行其中包含的两个调试信息来执行。

这并不复杂,但如果多尝试几次 playbooks,就会变得更容易。

尽管之前的示例能够生成一个非常干净且可重用的文件集合,但有一个更好的方法:使用角色。角色是独立的功能集,允许像其他软件组件一样,轻松进行维护。

根据之前的示例,我们可以使用三个角色重新编写它:

  • play 包含(play-include.yml

  • 主要任务(tasks.yml

  • 任务包含(tasks-include.yml

为了开始创建角色,首先创建一个名为 ansible-roles 的新文件夹,并在其中创建一个名为 roles 的文件夹。之前没有提到的一点是,创建一组文件夹来存放 Ansible 资源是一个好习惯:任务文件夹存放任务,文件夹用于存储需要传输到远程主机的所有文件等等。一般来说,我同意这种设置,但为了示例的简单性,我们简化了配置。对于角色,这种设置是必须的。我们需要根据需要创建适当的文件夹。在这种情况下,由于我们只使用任务来演示角色的工作方式,我们将在每个角色内创建任务文件夹,因为如果不这样做,我们就无法执行角色中的任务。

roles 文件夹内,我们将创建另一个名为 play-include 的文件夹,它将是前面示例中 play-include.yml 的等效物,但以角色的形式呈现。

现在是时候创建我们的第一个角色 playbook 了:创建一个名为 main.yml 的文件,并将其放置在 play-include/tasks/ 文件夹中。main.yml 文件的内容如下:

---
- debug: msg="I am a play include"

现在是时候添加第二个角色,名为 main-tasks,方法是创建一个名为 roles 的文件夹,并在 roles/main-tasks/tasks 文件夹中添加一个名为 main.yml 的文件:

---
- debug: msg="I am the main playbook"

我们的第三个也是最后一个角色叫做 tasks-include。像之前一样,创建文件夹(在 roles 文件夹内),并在 tasks 文件夹中添加一个名为 main.yml 的文件:

---
- debug: msg="I am a task include"
- debug: msg="I am a task include again"

就这样,你已经创建了三个可以在不同 Ansible 项目中重用的角色。现在是时候使用它们了。创建一个名为 tasks.yml 的文件,放在你项目的根文件夹中(在我的案例中是 ansible-roles),并添加以下内容:

---
- hosts: all
 user: root
 roles:
 - main-tasks
 - play-include
 - tasks-include

这是在添加了之前所有文件后,你的项目应该呈现的样子:

清单与之前示例中的相同(记住,推荐的是复用相同的虚拟机)。现在是时候运行我们的 playbook 了:

ansible-playbook -i inventory tasks.yml

这将生成类似以下的输出:

如果我们比较之前示例的输出,我们可以看到它几乎相同,唯一的区别是任务的说明,其中指示了任务来自哪个角色。

在角色中,我们还可以定义变量并访问全局作用域中定义的变量以及许多其他功能。正如之前所说,Ansible 足够庞大,完全可以写一本书,因此我们这里只是简单介绍了一些重要部分(根据我的标准)。如常,Ansible 的文档相当不错,如果你想深入了解角色,可以访问 docs.ansible.com/ansible-container/roles/index.html 查阅相关资料。

如果我能给你关于 Ansible 的一些建议,那就是你应该始终尝试使用角色。无论你的项目多大或多简单,你很快就会发现,角色所提供的隔离性和可重用性几乎不花成本,且非常有益。

Ansible Tower

我们已经看到 Ansible 提供了许多非常有用的功能,这些功能对任何希望在 IT 部门自动化任务的 DevOps 工程师来说都非常重要。

使用 Ansible 时有一个设计挑战,那就是 playbooks 是从你自己的计算机运行到远程服务器的,如下图所示:

这可能是一个问题,因为正如你现在所了解的那样,Ansible 使用机密(ansible-vault secrets),并且可能涉及一些敏感信息,这些信息可能会被从工作站上拦截或窃取。在 Chef 或 Puppet 中没有这个问题,因为它们遵循堡垒主机方法,但这可能是一些公司选择 Ansible 时遇到的问题。

其中一个解决方案来自 Red Hat,名为 Ansible Tower。该软件安装在你的 IT 基础设施中(在本例中为 Google Cloud Platform),并提供一个 UI,操作方式类似于 CI 服务器,能够启用角色访问控制到 Ansible 剧本,并且添加了一个在普通 Ansible 中没有的安全层:机密信息保存在基础设施内的一个服务器(Ansible Tower)中,从未离开过该基础设施。

Ansible Tower 提供了 Ansible 中所有的功能,因此你不需要重写任何剧本;只需根据新的基础设施几何形状进行调整。

让我们来看一下以下的图示:

如你所见,现在我们的 Ansible 主机已经在我们的基础设施内部;因此,它可以通过网页界面进行操作,从而增强了我们 IT 操作的安全性。

Ansible Tower 还提供了一个 API,可以用来与我们的软件或 CI 服务器建立集成点。

Ansible Tower 由 Red Hat 授权,因此如果你想在公司中使用它,需要购买许可证。在撰写本文时,市场上还没有太多的替代品,而且现有的替代品在功能上不如 Ansible Tower。而且,它的 UI(如下一张截图所示)非常简洁,虽然这并不是最重要的,但也总是值得考虑的因素。

总结

在本章中,你了解了 Ansible 的主要功能,但显然,我们并没有覆盖所有的可能性,因为要掌握它们需要写好几本书。此外,还有一个需要注意的地方:DevOps 工具正在不断发展。

当你在 IT 世界的 DevOps 领域工作时,你始终需要随时准备学习新知识。

Ansible 最初是为了在云端(以及本地)完全配置虚拟机而创建的,但随着 Kubernetes 或 Docker Swarm 等现代工具逐渐占据市场份额,它正慢慢转向配置管理,将 Docker 整合到持续交付环境中的完整软件开发生命周期中。

在下一章中,你将学习更多关于 Kubernetes 和 Docker Swarm 的内容,因为它们是 DevOps 领域中的下一个重要趋势。特别是 Kubernetes,它是一个我认为将在未来几个月或几年内超越所有其他工具的编排工具,因为它提供了任何 IT 公司所需的所有资源,借助 Google 多年来在容器中运行软件的经验。

我认为,像 Docker 这样的容器引擎即将突破盈亏平衡点,成为全球主要软件公司所有软件组件和架构的常规工具。

第七章:Docker Swarm 和 Kubernetes - 集群基础设施

到目前为止,我们已经看到了 Docker 的强大,但我们还没有释放容器的全部潜力。你已经学会了如何在单一主机上运行容器,并利用本地资源,但没有办法将我们的硬件资源以一种可以将其统一作为一个大主机来使用的方式进行集群。这带来了许多好处,但最明显的一点是,我们为开发人员和运维工程师之间提供了一个中间件,它作为一种通用语言,这样我们就不需要去找运维团队请求指定大小的机器了。我们只需提供我们的服务定义,Docker 集群技术就会处理这些事情。

在本章中,我们将深入探讨如何在 Kubernetes 上部署和管理应用程序,但我们也会看看 Docker Swarm 是如何工作的。

人们通常倾向于将 Kubernetes 和 Docker Swarm 视为竞争对手,但根据我的经验,它们解决的是不同的问题:

  • Kubernetes 专注于先进的微服务拓扑,提供了多年的容器运行经验的全部潜力,源自 Google。

  • Docker Swarm 提供了最直接的集群能力,用于以非常简单的方式运行应用程序。

简而言之,Kubernetes 更适合于高级应用,而 Docker Swarm 是一种增强版的 Docker。

这有一个代价:管理 Kubernetes 集群可能非常困难,而管理 Docker Swarm 集群相对简单。

目前的 DevOps 生态系统中还存在其他集群技术,如 DC/OS 或 Nomad,但不幸的是,我们需要关注那些在我看来最适合 DevOps 的技术,特别是 Kubernetes,我认为它正在吞噬 DevOps 市场。

为什么要集群?

在第一章《现实世界中的 DevOps》中,你学习了组织对齐以及为什么在公司中调整角色以适应 DevOps 工具是如此重要。现在仅仅作为开发者或系统管理员已经不再足够;你现在需要成为一名全栈 DevOps 工程师,才能在任何项目中获得成功。全栈 DevOps 意味着你需要理解企业和组织中使用的技术。想一想,如果你成为了一名土木工程师,而不是 IT 工程师,你必须了解当地的规则(业务),以及用于建造道路和桥梁的工具的商业名称(技术),同时还要能够协调它们的建设(运维)。也许并不是每个工程师都需要知道所有的内容,但他们需要了解整个图景,以确保项目的成功。

回到容器和 DevOps,今天让每个人都能理解的概念是必不可少的。你需要确保项目中的所有工程师都能够追溯软件的整个过程,从构思(需求)到部署(运维),同时也要考虑可预测性,这样那些几乎不懂技术的业务人员也能围绕你所构建的产品规划策略。

实现这里描述的流的关键之一是可预测性,而实现可预测性的方法是对资源进行统一和可重复的使用。正如你之前学到的那样,像 Amazon Web Services 或 Google Cloud Platform 这样的云数据中心为我们提供了一个几乎无限的资源池,可以按照传统方式构建我们的系统:

  • 定义虚拟机的大小

  • 配置虚拟机

  • 安装软件

  • 维护它

或者,如果我们想绘制一个图表,以便更好地理解,它将类似于下图:

这里有几点需要考虑:

  • 开发和运维之间的明确分离(这可能会根据公司规模有所不同)

  • 由开发部门拥有的软件组件以及由运维部门拥有的部署和配置

  • 一些服务器可能相对低效(服务器 1),负载非常低

这是 40 多年来软件开发的情景,如果我们在运行 Docker 容器时仍然是这种情况,但其中有几个问题:

  • 如果在生产环境中组件 3出现问题,谁负责处理?

  • 如果配置不匹配,当开发人员不应该看到生产环境中的情况时,谁来修复它?

  • 服务器 1运行着一个可能一天只调用一次或两次的软件组件(比如工作站的认证服务器);我们需要为它配置一个完整的虚拟机吗?

  • 我们如何以透明的方式扩展我们的服务?

这些问题是可以解答的,但通常它们的答案往往来得太晚,而“隐藏的需求”只有在最糟糕的时刻才会显现出来:

  • 服务发现

  • 负载均衡

  • 自愈基础设施

  • 电路断开

在大学时期,所有不同学科的共同点之一是可重用性和可扩展性。你的软件应该具有可扩展性和可重用性,这样我们就可以创建组件库,形成工程的最佳实践(不仅仅是软件开发):构建一次,到处使用。

在软件开发的运维部分,这一点一直被完全忽视,直到近年来才开始受到重视。如果你在一家公司担任 Java 开发人员,那么有一套被世界上每一个 Java 开发人员接受并使用的实践,这样你几乎可以毫无问题地快速上手(理论上是这样)。现在我们提出一个问题:如果所有的 Java 应用都遵循相同的实践和公共模式,为什么每家公司却以不同的方式部署它们?

在 IT 世界中,持续交付流水线几乎每个公司都需要,但我见过至少三种不同的组织方式,而且这些方式背后有着大量只有一两个人知道的定制“魔法”。

集群在这里拯救我们。让我们重新排列一下之前的图像:

在这种情况下,我们解决了我们的一些问题:

  • 现在,开发和运维通过中间件连接起来:集群。

  • 组件可以被复制(参考组件 1 和组件 1'),无需额外的硬件支持。

  • DevOps 工程师是两个团队(开发和运维)之间的纽带,让事情以更快的节奏推进。

  • 整个系统的稳定性并不依赖于单一服务器(或组件),因为集群是以能够接受一定程度故障的方式构建的,这样可以通过降低性能或关闭较不关键的服务来容忍故障:为了保持公司财务流程的正常运行,牺牲邮件服务是可以接受的。

说到隐藏的需求。这就是我们需要决定使用哪种集群技术的地方,因为它们从不同角度处理服务发现、负载均衡和自动扩展。

Docker Swarm

正如我们在前面的章节中看到的,Docker 是一个非常棒的工具,它遵循了运行作为容器打包的应用的最现代的架构原则。在这种情况下,Docker Swarm 仅运行 Docker 容器,忽略了其他目前不适用于生产环境的技术,比如 Rkt。甚至 Docker 本身也在某种程度上是新兴的,以至于一些公司在将其部署到生产系统时犹豫不决,因为市场上没有太多的专业知识,也存在许多关于安全性或 Docker 工作原理的疑虑。

Docker Swarm 是 Docker 的集群版,它以非常简单的方式解决了上一节中描述的问题:几乎所有你在 Docker 章节中学到的 Docker 命令都可以在 Docker Swarm 中使用,这样我们就可以在不直接管理硬件的情况下联邦化我们的硬件资源。只需将节点添加到资源池中,Swarm 就会管理它们,并充分利用我们构建纯容器系统的方式。

Docker Swarm 不是我们需要单独安装的东西,它是嵌入在 Docker 引擎中的一种模式,而不是一个独立的服务器。

Docker Swarm 正在快速发展,并且随着更多特性的加入,它正在带动 Docker 本身的发展,特别是在 Swarm 模式下使用时。最有趣的部分是,我们如何在没有额外操作的情况下利用我们的 Docker 知识,因为 Docker 引擎的 Swarm 模式会自动处理资源。

这也是一个问题:我们受限于 Docker API,而在 Kubernetes 中(稍后我们会详细讲解),我们不仅不受 Docker API 的限制,还可以扩展 Kubernetes API 来添加新对象,以满足我们的需求。

Docker Swarm 可以通过 docker-compose 进行操作(在一定程度上),它为基础设施即代码提供了一个不错的方案,但当我们的应用程序变得复杂时,它并不是特别全面。

在当前的 IT 市场中,Kubernetes 似乎是编排战斗的明显赢家,因此我们将重点关注它,但如果你想了解更多关于 Docker Swarm 的内容,可以参考官方文档,链接为 docs.docker.com/engine/swarm/

Kubernetes

Kubernetes 是容器编排的皇冠上的明珠。该产品本身由 Google 利用多年的生产容器运行经验进行改进。最初,它是一个内部系统,用于运行 Google 的服务,但在某个时刻,它成为了一个公开的项目。如今,它是一个由少数几家公司(如 Red Hat、Google 等)维护的开源项目,并被成千上万的公司使用。

在撰写本文时,Kubernetes 工程师的需求已经飙升,达到了公司愿意聘用那些虽然在该领域没有专业知识,但有良好学习态度的人来学习新技术的程度。

我认为 Kubernetes 之所以变得如此流行,主要有以下几个原因:

  • 它解决了所有的部署问题

  • 它自动化微服务的运维

  • 它提供了一种通用的语言,将运维和开发连接起来,拥有一个清晰的接口

  • 一旦设置完成,它非常容易操作

如今,许多公司希望缩短交付周期,而其中最大的一个问题就是围绕交付过程积累的繁文缛节。在一个市场中,五名熟练的工程师能够赶超传统银行,因为他们能够消除繁文缛节,简化交付流程,使得他们可以每天发布多次。

我的职业活动之一是参加各种会议(在都柏林的聚会、科克的 RebelCon、多个地方的 Google Developer Groups (GDGs)、Google IO Extended),我在所有的演讲中总是使用相同的话语:发布管理应该不再是一个重大的事件,让整个世界停顿三小时才能发布公司应用的新版本,而应该变成一个无痛的过程,可以随时回滚,从而通过提供管理故障发布的工具来减轻大部分压力。

这(不仅仅是这个,但主要是这个)就是 Kubernetes:一套工具和虚拟对象,它们为工程师提供了一个框架,可以用来简化所有与我们应用相关的操作:

  • 扩展规模

  • 缩减规模

  • 零停机发布

  • 金丝雀发布

  • 回滚

  • 密钥管理

Kubernetes 是以技术无关的方式构建的。Docker 是主要的容器引擎,但所有组件都设计为具有可互换性:一旦 Rkt 就绪,切换到 Rkt 会很容易,这给用户带来了有趣的视角,因为他们不会被某一种特定技术绑定,这样避免供应商锁定就变得更加容易。这同样适用于软件定义网络和其他 Kubernetes 组件。

一个痛点是设置和使用 Kubernetes 的陡峭学习曲线。

Kubernetes 非常复杂,熟练掌握其 API 和操作可能需要几周,甚至几个月的时间,但一旦你精通它,你所节省的时间完全能够弥补所有学习所花费的时间。

同样地,设置集群并不容易,甚至有公司开始将 Kubernetes 作为一种服务出售:它们负责维护集群,你负责使用它。

在我看来(再次强调,这是我的个人看法),Kubernetes 的一个最先进的提供商是 Google 容器引擎 (GKE),这也是我们将在本书中用于示例的提供商。

当我在规划本章内容时,我必须在两个选项之间做出决定:

  • 设置集群

  • 展示如何围绕 Kubernetes 构建应用程序

我考虑了几天,后来我意识到一件事:有大量的信息和大约半打方法来设置集群,而且没有一个是官方的。其中一些方法得到了官方 Kubernetes GitHub 仓库的支持,但在撰写本文时(截至撰写时),并没有官方的、首选的设置 Kubernetes 实例的方法,无论是在本地还是在云端。因此,选择的讲解集群部署的方法可能在本书上市时已经过时。以下是目前最常见的设置 Kubernetes 集群的方式:

  • Kops:这个名字代表 Kubernetes 操作,它是一个用于操作集群的命令行接口:通过几条命令创建、销毁和扩展集群。

  • Kubeadm:目前,Kubeadm 还处于 alpha 阶段,任何时候都可能会有破坏性更新集成到源代码中。它通过在每个我们希望加入集群的节点上执行简单的命令,简化了 Kubernetes 的安装,就像我们使用 Docker Swarm 时的做法一样。

  • Tectonic:Tectonic 是 CoreOS 推出的一个产品,用于在多个云服务提供商(如 AWS、Open Stack、Azure)上轻松安装 Kubernetes。它对于最多九个节点的集群是免费的,我强烈建议你至少尝试一下,以了解集群拓扑结构。

  • Ansible:Kubernetes 官方仓库还提供了一套 playbooks,用于在任何虚拟机提供商或裸机上安装 Kubernetes 集群。

所有这些选项都是从头开始搭建集群的有效方法,因为它们通过隐藏细节和全貌,自动化了 Kubernetes 架构的部分内容。如果你真的想了解 Kubernetes 的内部工作原理,我推荐 Kelsey Hightower 写的一本指南《Kubernetes the hard way》,它展示了如何围绕 Kubernetes 设置一切,从需要在节点间共享信息的 etcd 集群,到用于与 kubectl(Kubernetes 的远程控制工具)通信的证书。你可以在 github.com/kelseyhightower/kubernetes-the-hard-way 找到这本指南。

而且它会随着 Kubernetes 新版本的发布而更新。

正如你从这个解释中可以猜到的,在这一章中,你将学习 Kubernetes 的架构,但主要,我们将专注于如何在 Kubernetes 上部署和操作应用程序,以便在本章结束时,我们能够充分理解如何从一个已经运行的集群中获益。

Kubernetes 逻辑架构

一旦你开始使用 Kubernetes,你会发现第一个问题是如何在脑海中构建一个关于 Kubernetes 中一切如何运行以及如何连接的思维地图。

在这种情况下,我花了几周时间才完全理解这一切是如何连接的,但一旦我脑海中有了这个全貌,我画出了类似于下图所示的结构:

这是 Kubernetes 的一个非常高层次的概述:一个主节点负责协调容器的运行,这些容器被分组在不同节点上的 pods 中运行(它们曾经被称为 minions,但现在已经不再使用这个术语)。

这张思维地图帮助我们理解一切如何连接起来,并引入了一个新的概念:pod。Pod 本质上是一组一个或多个容器,它们在协同操作中执行单一任务。例如,想象一个缓存和一个缓存预热器:它们可以在不同的容器中运行,但在同一个 pod 上,这样缓存预热器就可以作为一个独立应用程序来打包。我们稍后会再讨论这个概念。

通过这张图,我们还能够识别出不同的物理组件:

  • 主节点

  • 节点

主节点是运行所有支持服务的节点,如 DNS(用于服务发现)以及 API 服务器,允许我们操作集群。理想情况下,您的集群应该有多个主节点,但在我看来,能够快速恢复主节点比拥有高可用性配置更重要。毕竟,如果主节点宕机,通常可以保持一切正常运行,直到我们恢复主节点,通常只需生成一个使用与旧主节点相同模板的新 VM(在云上)即可。

也可以运行一个带有 IP Tables 阻止连接到关键端口的主节点,以防它加入集群,并在希望主节点成为集群领导时移除 IP Tables 规则。

节点基本上是工作节点:它们根据指令从主节点部署和维持应用程序的运行,按照指定的配置。它们使用一个名为 Kubelet 的软件,这基本上是 Kubernetes 的代理程序,负责与主节点进行通信。

关于网络,这里有两层网络:

  • 硬件网络

  • 软件网络

硬件网络是我们所熟知的,用于连接集群中的虚拟机(VM)。它在我们的云服务提供商(如 AWS,Google Cloud Platform 等)中定义,并没有什么特别之处,只需记住,理想情况下,这个网络应该是高性能网络(千兆以太网),因为节点间的流量可能会很大。

软件网络(或软件定义网络,SDN)是运行在 Kubernetes 中间件之上的网络,通过etcd与所有节点共享,etcd 基本上是一种分布式键值存储,Kubernetes 用它作为协调点来共享关于多个组件的信息。

这个 SDN 用于连接各个 Pod:IP 地址是虚拟 IP,不在外部网络中实际存在,只有节点(和主节点)知道。它们用于在不同节点之间路由流量,因此,如果节点 1 上的应用程序需要访问节点 3上的 Pod,使用这个网络,应用程序可以通过标准的http/tcp协议栈进行访问。这个网络看起来与下图类似:

让我们简单解释一下这个网络结构:

  • 网络上的地址 192.168.0.0/16 是物理地址。它们用于连接组成集群的虚拟机(VM)。

  • 网络上的地址 10.0.0.0/24 是软件定义网络地址。它们无法从集群外部访问,只有节点能够解析这些地址并将流量转发到正确的目标。

网络是 Kubernetes 中一个相当重要的话题,目前性能瓶颈最常见的原因是节点之间的流量转发(我们将在本章稍后讨论),这会导致额外的节点间流量,从而可能导致 Kubernetes 中运行的应用程序普遍变慢。

一般来说,现阶段我们需要了解的 Kubernetes 架构就是这些。Kubernetes 的核心理念是提供一组统一的资源,这些资源可以作为一个单一的计算单元,支持零停机操作的简便实现。目前,我们还不清楚如何使用它,但重要的是我们已经有了对 Kubernetes 集群整体架构的理解。

在 GCP 上设置集群

我们在 Kubernetes 中要开始操作的第一件事就是集群。虽然有多种选择,但我们将使用 GKE,因为我们已经注册了试用,并且账户里应该有足够的信用额度来完成整个书中的内容。

如果你没有在 GCP 上注册试用,另一个选择是 Minikube。Minikube 是一个开箱即用、易于安装的本地集群,它运行在虚拟机上,是一个非常好的工具,可以在不担心破坏任何东西的情况下尝试新特性。

Minikube 项目可以在 github.com/kubernetes/minikube 上找到。

它的文档相当全面。

为了在 GCP 上创建一个集群,首先我们需要做的是在 GCP 的在线控制台中打开容器引擎,它将显示类似于以下截图的内容:

这意味着你目前没有集群。点击“创建容器集群”按钮,并填写以下表单:

这里有几点需要考虑的事项:

  • 给集群起个全面的名字。在我的例子中,我命名为 testing-cluster

  • 选择一个地理位置上靠近你的区域,在我的例子中是 europe-west1-c

  • 关于集群版本,选择默认版本。这是你希望集群运行的 Kubernetes 版本,之后可以无缝升级。此外,请注意 Kubernetes 每大约两个月发布一个新版本,因此到你读这本书时,很可能会有更现代的版本可用。

  • 机器类型也应该选择标准类型(1 个 vCPU,3.75 GB 的 RAM)。

  • 集群的规模是我们希望在集群中使用的机器数量。三台是测试的一个不错选择,也可以在以后增加或减少。

其他内容应该是默认的。自动升级和自动修复是 Beta 功能,我现在还不建议在生产集群中使用。这两个选项允许 GCP 在有新版本的 Kubernetes 发布或某个节点因某种原因发生故障时自动采取措施。

填写完表单后,点击创建集群,这样就完成了。现在 Google 正在为我们配置一个集群。为了查看发生了什么,打开 GCP 中的 Compute Engine 标签,你应该看到类似下面的截图:

三台带有 "gke-" 前缀的机器已经在计算引擎中创建,这意味着它们属于 GKE,K 代表 Kubernetes。它们是普通机器,除了 Google 已经配置好设置 Kubernetes 节点所需的所有软件外,没什么特别之处,但是主节点在哪里?

在 Google Cloud Platform 上运行 Kubernetes 的有趣之处在于:他们会管理你的主节点,因此你无需担心高可用性或升级问题,这些操作是自动完成的。

我们集群的主节点托管着我们整个集群的关键组件之一:API 服务器。Kubernetes 中的所有操作都是通过 API 服务器与一个名为 kubectl 的组件进行的。kubectl 代表 Kubernetes 控制,是一个终端程序,可以安装在本地机器(或持续集成服务器)上,添加给定集群的配置,然后开始向我们的集群发出命令。

首先,我们将安装 kubectl。在前面的章节中,我们已经安装了 Google Cloud SDK(gcloud 命令),可以通过以下命令来安装 kubectl

gcloud components install kubectl

就这样。现在我们可以像使用任何其他命令一样在系统中使用 kubectl,但我们需要添加我们的集群配置。到目前为止,kubectl 并未配置为操作我们的集群,因此我们首先需要获取所需的配置。Google Cloud Platform 使这变得非常简单。如果你打开 Google Container Engine 标签页,它现在应该类似于下面的界面:

如你所见,屏幕右侧有一个名为“连接”的按钮。点击它后,你将看到如下表单:

表单中的日期会略有不同(因为你的集群和项目的名称会不同),但表单中会显示两个命令:

  • 获取本地机器上 Kubernetes 集群配置的 gcloud 命令

  • 启动代理到 Kubernetes Dashboard UI 的 kubectl 命令

第一个命令很简单。只需执行它:

gcloud container clusters get-credentials testing-cluster --zone europe-west1-c --project david-on-microservices

输出将类似于下面的内容:

Fetching cluster endpoint and auth data.
kubeconfig entry generated for testing-cluster.

所以,发生的情况是 gcloud 获取了配置并将其本地安装,以便我们可以操作集群。你可以通过运行以下命令来尝试:

kubectl get nodes

这将输出集群中节点的列表。Kubectl 是一个功能非常强大的命令行工具。通过它,我们几乎可以在集群内做任何事,正如我们将在本章的其余部分中学习的那样。

前面截图中的第二个命令用于启动 Kubernetes 中的代理:

kubectl proxy

这将输出以下内容:

Starting to serve on 127.0.0.1:8001

让我们来解释一下这里发生了什么。Kubernetes 强烈依赖客户端证书。为了与主节点通信,我们的机器需要代理请求并发送证书来验证它们。

所以,如果我们现在浏览前面截图中的 URL,http://localhost:8001/ui,我们会看到 Kubernetes 仪表盘:

仪表盘基本上是一个很好的方式,用于展示我们运行的集群中的所有信息。虽然也可以通过仪表盘操作集群(到一定程度),但我的建议是精通kubectl,因为它要强大得多。在仪表盘上,我们可以看到很多信息,比如节点的状态、部署到集群中的项目(Pods、Replica Sets、Daemon Sets 等)、命名空间以及其他许多元素。

四处浏览一下,熟悉一下仪表盘,因为它是一个很好的工具,能够让你实际看到集群中发生的事情。

Kubernetes 将工作负载划分为命名空间。命名空间是一个虚拟集群,允许工程师在不同团队之间隔离资源(在一定程度上)。Kubernetes 也利用命名空间来运行其内部组件。这一点非常重要,因为 Kubernetes 将关键组件分布在不同的节点上,以确保高可用性。在这种情况下,我们有三个组件在每个节点上运行:

  • Kubernetes 仪表盘

  • Kubernetes 代理(kube-proxy

  • Kubernetes DNS(kube-dns

Kubernetes 仪表盘就是我们刚刚看到的:一个用户界面,用于展示 Kubernetes 集群中的信息。

Kubernetes 代理是节点用来从 Pod 地址到节点地址解析 SDN 中的 IP 地址的代理,以便集群能够将流量重定向到正确的节点。

Kubernetes DNS 基本上是一个负载均衡和服务发现机制。在接下来的部分中,你将学习如何使用 Kubernetes 中的构建块来部署应用程序。特别是,Services 与此 DNS 服务紧密耦合,为了在 Kubernetes 中定位一个应用程序,我们只需要知道它的名称以及配置该服务的服务配置,从而将组成该应用程序的 Pods 聚合在一起。

我们在每个节点上运行这些组件的事实使得 Kubernetes 能够进入自动驾驶模式,以应对主节点宕机的情况:即使没有主节点,应用程序也能继续工作(在大多数情况下),因此丢失主节点并不是灾难性事件。

一旦我们在机器上配置了kubectl,就可以开始了解 Kubernetes 中的一些构建块,从而构建非常健壮的应用程序。

Kubernetes 构建块

在前面的部分中,您已经了解了集群拓扑,但现在我们需要工具来在其上运行应用程序。我们已经介绍了 Kubernetes 构建块之一:Pod。在本节中,我们将看一些 Kubernetes 提供的最重要的 API 对象(构建块),以便构建我们的应用程序。

当我开始学习 Kubernetes 时,我在第二家公司工作,他们以持续交付方式部署应用程序,我一直心里有一个问题:为什么不同的公司试图以不同的方式解决同样的问题?

后来我意识到:缺失的元素是持续交付的领域特定语言。缺乏共同的标准和良好理解的应用程序推出方式,阻碍了它们有效工作和早期交付价值。每个人都知道什么是负载均衡器或代理,以及在应用程序的新版本部署中涉及的许多其他元素,但问题所在是人们使用它们的方式不尽相同。如果您雇用新工程师,他们以前对持续交付的了解将变得过时,因为他们需要学习您的做事方式。

Kubernetes 通过一组对象(Pod、ReplicaSets、DameonSets 等)解决了这个问题,这些对象在 YAML 文件(或 JSON)中描述。一旦我们完成本节,我们将已经具备足够的知识,可以从定义我们资源的 YAML 或 JSON 文件中构建一个关于系统外观的图表。这些文件和 Docker 镜像足以让 Kubernetes 运行我们的系统,我们将看几个例子。

Pods

Pod 是 Kubernetes API 的最基本元素。一个 Pod 基本上是一组共同工作的容器,以提供服务的全部或部分。Pod 的概念可能会引起误解。我们可以运行多个共同工作的容器,这表明我们应该将应用程序的前端和后端放在单个 Pod 中。尽管我们可以这样做,但我强烈建议您避免这样做。原因是将前端和后端捆绑在一起会丧失 Kubernetes 提供的许多灵活性,如自动缩放、负载均衡或金丝雀部署。

一般情况下,Pod 包含一个单一容器,这是迄今为止最常见的用例,但是多容器 Pod 有一些合法的使用案例:

  • 缓存与缓存预热

  • 预计算和提供 HTML 页面

  • 文件上传与文件处理

正如您所见,所有这些活动都是紧密耦合在一起的,但如果 Pod 内部容器的感觉是它们朝着不同的任务(如后端和前端)工作,可能值得将它们放在不同的 Pods 中。

在 Pod 内部容器之间进行通信有两个选项:

  • 文件系统

  • 本地网络接口

由于 Pod 是不可分割的元素,且运行在同一台机器上,所有挂载在 Pod 内容器中的卷是共享的:在 Pod 中的一个容器创建的文件,可以通过挂载相同卷的其他容器访问。

本地网络接口或回环接口就是我们常说的 localhost。Pod 中的容器共享相同的网络接口;因此,它们可以通过 localhost(或127.0.0.1)在暴露的端口上进行通信。

部署一个 pod

如前所述,Kubernetes 在配置 API 元素时依赖于Yet Another Markup LanguageYAML)文件。为了部署一个 pod,我们需要创建一个 yaml 文件,但首先,创建一个名为deployments的文件夹,在其中创建我们将在本节中创建的所有描述符。创建一个名为pod.yaml(或pod.yml)的文件,内容如下:

apiVersion: v1
kind: Pod
metadata:
 name: nginx
 labels:
 name: nginx
spec:
 containers:
 - name: nginx
 image: nginx
 ports:
 - containerPort: 80
 resources:
 requests:
 memory: "64Mi"
 cpu: "250m"

如你所见,前面的yaml文件相当具有描述性,但有些点需要进一步说明:

  • apiVersion:这是我们将要使用的 Kubernetes API 版本,用来定义我们的资源(在此情况下为 pod)。Kubernetes 是一个快速发展的项目,版本是避免在新版本发布时弃用资源的机制。一般来说,Kubernetes 有三个分支:alpha、beta 和稳定版本。在前述例子中,我们使用的是稳定版。更多信息请参见kubernetes.io/docs/concepts/overview/kubernetes-api/

  • metadata:在这一部分,我们定义了我见过的最强大的发现机制之一:模式匹配。特别是,标签部分将在后续用于将具有特定标签的 pods 暴露给外部。

  • spec:在这里,我们定义我们的容器。在此情况下,我们部署了一个nginx实例,以便我们可以轻松看到一切是如何工作的,而不需要过多关注应用程序本身。正如预期的那样,镜像和暴露的端口已被指定。我们还为该 Pod 定义了 CPU 和内存限制,以防资源消耗失控(注意,YAML 文件请求的资源可能不可用,因此 pod 将使用较低配置的资源运行)。

这是我们可以在 Kubernetes 中创建的最简单配置项。现在是时候将资源部署到我们的集群中了:

kubectl apply -f pod.yml

这将产生类似如下的输出:

pod "nginx" created.

免责声明:创建资源的方式有很多种,但在本书中,我将尽可能使用apply。另一种可能的方式是使用create

kubectl create -f pod.yml

apply相较于create的优势在于,apply会对比以前的版本、当前版本以及你想要应用的更改,进行三方差异比较,并决定如何最好地更新资源。这让 Kubernetes 能够做它最擅长的事:自动化容器编排。

使用 create,Kubernetes 不会保存资源的状态,如果我们希望随后运行 apply 以优雅地更改资源的状态,系统会发出警告:

Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
pod "nginx" configured

这意味着我们可以将系统推向不稳定状态几秒钟,这在某些使用场景中可能是不可接受的。

一旦我们应用了我们的 YAML 文件,我们可以使用kubectl查看 Kubernetes 中的情况。执行以下命令:

kubectl get pods

这将输出我们的 Pod:

我们可以对集群中的其他元素进行类似操作,比如节点:

kubectl get nodes

这将输出以下内容:

kubectl get适用于 Kubernetes 中的所有工作流以及大多数 API 对象。

查看 Kubernetes 中发生的事情的另一种方式是使用仪表盘。现在我们已经创建了一个 Pod,打开http://localhost:8001/ui并在左侧导航到 Pod 部分。

记住,要访问仪表盘,你首先需要在终端执行kubectl proxy

在那里,你将看到当前已部署的 Pod 列表,这里只有 nginx。点击它,屏幕应该与这里展示的非常相似:

在这里,我们可以获取大量信息,从 Pod 正在消耗的内存和 CPU 到它运行的节点,还有一些其他有价值的项目,比如应用到 Pod 上的注解。我们可以使用kubectl describe命令来获取这些信息,如下所示:

kubectl describe pod nginx

注解是一个新概念,它是围绕我们的 API 元素的数据,在这种情况下是我们的 Pod。如果你点击“最后应用的配置”在详情部分,你可以看到来自 YAML 文件的数据,如下图所示:

这与之前解释的三方差异(three-way diff)有关,Kubernetes 使用它来决定在不进入不一致状态的情况下升级资源的最佳方式。

截至目前,我们的 Pod 正在 Kubernetes 中运行,但尚未连接到外部世界;因此,无法从集群外部打开浏览器并导航到 nginx 首页。我们可以做的一件事是打开一个远程会话,进入 Pod 内容器的 bash 终端,方式类似于我们在 Docker 中的操作:

kubectl exec -it nginx bash

我们进入了。实际上,我们已经获得了容器内根终端的访问权限,可以执行任何命令。我们稍后会使用这个功能。

一旦我们了解了 Pod 的工作原理,你可能会有一些关于 Kubernetes 应该做什么的问题:

  • 我们如何扩展 Pod?

  • 我们如何发布应用程序的新版本?

  • 我们如何访问我们的应用程序?

我们将回答所有这些问题,但首先,我们需要了解其他的“构建模块”。

副本集

到目前为止,我们知道如何在 pod 中部署应用。单个 pod 的概念非常强大,但它缺乏鲁棒性。实际上,无法定义扩展策略,甚至无法确保在发生故障(例如节点宕机)时 pod 能够保持存活。在某些情况下这可能没问题,但这里有一个有趣的问题。如果我们已经承担了维护 Kubernetes 集群的开销,为什么不充分利用它的优势呢?

为了做到这一点,我们需要使用Replica Sets。Replica Set 就像一位在满是 pod 的道路上的交通警察:它们确保交通流畅,一切正常运作,避免崩溃,并移动 pod 以便最大化利用道路(在这种情况下是我们的集群)。

Replica Sets 实际上是对更早期的一个项目——Replication Controller 的更新。升级的原因是标签和资源选择的功能,这一点我们将在深入探讨名为 Service 的 API 项目时看到。

让我们来看一下 Replica Set:

apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
 name: nginx-rs
spec:
 replicas: 3
 template:
 metadata:
 labels:
 app: nginx
 tier: frontend
 spec:
 containers:
 - name: nginx
 image: nginx
 resources:
 requests:
 cpu: 256m
 memory: 100Mi
 ports:
 - containerPort: 80

再次提醒,这是一个 YAML 文件,基本上是相当容易理解的,但可能需要一些解释:

  • 在这种情况下,我们使用了v1beta1版本的扩展 API。如果你还记得 pod 部分(前面提到过),Kubernetes 有三个分支:stable、alpha 和 beta。完整的参考可以在官方文档中找到,并且由于 Kubernetes 是一个充满活力且始终在发展的项目,API 很可能经常变化。

  • 在 spec 部分发生了重要的事情:我们为 Replica Set 定义了一组标签,同时还定义了一个 pod(在这个例子中,是一个单容器的 pod),并指定我们希望它有三个实例(replicas:三)。

简单而有效。现在我们定义了一个名为 Replica Set 的资源,它可以根据配置部署一个 pod 并保持它的存活。

让我们来测试一下:

kubectl apply -f replicaset.yml

一旦命令返回,我们应该看到以下消息:

replicaset "nginx-rs" created

让我们用kubectl来验证它:

kubectl get replicaset nginx-rs

根据前面的命令输出,你应该看到 Replica Set 说明有三个预期的 pod,三个已经部署,并且三个准备就绪。注意“current”和“ready”之间的区别:一个 pod 可能已经部署,但仍未准备好处理请求。

我们已指定我们的replicaset应该保持三个 pod 存活。让我们来验证一下:

kubectl get pods

这里没有惊讶:我们的replicaset创建了三个 pod,如下图所示:

我们有四个 pod:

  • 在前面部分创建的一个

  • Replica Set 创建的三个

让我们杀掉一个 pod,看看会发生什么:

kubectl delete pod nginx-rs-0g7nk

现在,查询一下运行了多少个 pod:

轰!我们的replicaset创建了一个新的 Pod(你可以在 AGE 列中看到是哪一个)。这非常强大。我们从一个 Pod(应用程序)被杀掉时,你会在早上 4 点醒来采取措施的世界,转变为一个当我们的应用程序崩溃时,Kubernetes 会为我们恢复它的世界。

让我们看看仪表盘中发生了什么:

正如你所预期的,Replica Set 为你创建了 Pods。你也可以尝试通过界面杀死它们(每个 Pod 最右边的周期图标允许你这么做),但是 Replica Set 会为你重新生成它们。

现在我们要做一些看起来像是来自外太空的事情:我们将通过一个命令来扩展我们的应用程序,但首先,编辑 replicaset.yml 并将 replicas 字段从 3 改为 5。

保存文件并执行以下命令:

kubectl apply -f replicaset.yml

现在再看看仪表盘:

如你所见,Kubernetes 正在根据 Replica Set nginx-rs 的指示为我们创建 Pods。在上面的截图中,我们可以看到一个 Pod,其图标没有变绿,因为它的状态是 Pending,但几秒钟后,它的状态变为 Ready,就像其他任何 Pod 一样。

这也是非常强大的,但有一个问题:如果负载高峰发生在早上 4 点钟,谁来扩展应用程序?好吧,Kubernetes 为此提供了解决方案:水平 Pod 自动扩展器。

让我们执行以下命令:

kubectl autoscale replicaset nginx-rs --max=10

通过上述命令,我们指定 Kubernetes 应该将水平 Pod 自动扩展器附加到我们的 Replica Set。如果你再次浏览仪表盘中的 Replica Set,情况已经发生了显著变化:

让我们解释一下这里发生了什么:

  • 我们已经将水平 Pod 自动扩展器附加到我们的 Replica Set:最小 1 个 Pod,最大 10 个,创建或销毁 Pods 的触发条件是某个 Pod 的 CPU 使用率超过 80%

  • Replica Set 已经缩减到一个 Pod,因为系统没有负载,但如果需要,它会扩展回最多 10 个节点,并在请求高峰期间保持在此状态,之后会缩减到最低所需的资源。

这实际上是任何系统管理员的梦想:无障碍自动扩展和自我修复的基础设施。正如你所看到的,Kubernetes 开始变得合乎逻辑,但在自动扩展部分有一个让人困扰的地方。我们在终端中运行了一个命令,但它并没有被记录下来。那么我们该如何跟踪我们的基础设施呢(是的,水平 Pod 自动扩展器也是基础设施的一部分)?

其实有一个替代方案;我们可以创建一个 YAML 文件来描述我们的水平 Pod 自动扩展器:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
 name: nginx-hpa
spec:
 maxReplicas: 10
 minReplicas: 1
 scaleTargetRef:
 kind: ReplicaSet
 name: nginx-rs
 targetCPUUtilizationPercentage: 80

首先,从仪表盘中删除前面例子中创建的HorizontalPodAutoscaler。然后,将前面的内容写入一个名为horizontalpodautoscaler.yml的文件,并运行以下命令:

kubectl apply -f horizontalpodautoscaler.yml

这应该和autoscale命令有相同的效果,但有两个明显的好处:

  • 我们可以控制更多的参数,比如 HPA 的名称,或者为其添加元数据,例如标签

  • 我们将基础设施作为代码保持在触手可及的地方,这样我们就能知道发生了什么

第二点极为重要:我们正处在基础设施即代码的时代,Kubernetes 利用这个强大的概念来提供可追溯性和可读性。在后续的第八章,发布管理 – 持续交付中,你将学习如何使用 Kubernetes 以一种非常简便的方式创建一个适用于 90%软件项目的持续交付流水线。

一旦前面的命令执行完毕,我们可以在仪表盘上查看,确认我们的 Replica Set 确实根据配置附加了一个 Horizontal Pod Autoscaler。

部署(Deployments)

即使 Replica Set 是一个非常强大的概念,但我们还没有讨论其中的一部分:当我们应用新的配置到 Replica Set 以便升级我们的应用时,会发生什么?它如何处理我们希望应用在 100%的时间内保持运行而不发生服务中断的问题?

好吧,答案很简单:它不会。如果你应用新的配置到一个 Replica Set 并使用了新的镜像版本,Replica Set 将销毁所有 Pods 并创建新的 Pods,且没有任何保证的顺序或控制。为了确保我们的应用始终保持运行并且有最少的资源(Pods)保障,我们需要使用 Deployments。

首先,看看部署(deployment)是怎样的:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: nginx-deployment
spec:
 strategy:
 type: RollingUpdate
 rollingUpdate:
 maxUnavailable: 0
 maxSurge: 1
 replicas: 3
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx
 image: nginx
 resources:
 requests:
 cpu: 256m
 memory: 100Mi
 ports:
 - containerPort: 80

如你所见,它与 Replica Set 非常相似,但有一个新的部分:strategy(策略)。在策略中,我们定义了rollout的工作方式,且有两个选项:

  • RollingUpdate

  • Recreate

RollingUpdate是默认选项,因为它似乎在现代 24/7 应用中最为通用:它协调两个 Replica Set,并在创建新 Replica Set 中的 pod 时,同时关闭旧 Replica Set 中的 pod。这是非常强大的,因为它确保了我们的应用始终保持运行。Kubernetes 决定最好的 pod 重新调度方式,但你可以通过两个参数影响这个决策:

  • maxUnavailable

  • maxSurge

第一个定义了在执行rollout时,我们可以从 Replica Set 中丢失多少个 pod。举个例子,如果我们的 Replica Set 有三个副本,且maxUnavailable值为1,那么 Kubernetes 将在某个时刻允许新 Replica Set 中只有两个 pod 处于Ready状态。在这个例子中,maxUnavailable0;因此,Kubernetes 会始终保持三个 pod 处于存活状态。

MaxSurgemaxUnavailable类似,但它的作用相反:它定义了 Kubernetes 可以调度多少个超过副本数量的 Pod。在前面的示例中,设置了maxSurge1,且有三个副本,这时在我们的rollout中,任何时刻 Pod 的最大数量将是4

通过调整这两个参数以及副本的数量,我们可以实现相当有趣的效果。例如,通过指定三个副本和maxSurge 1以及maxUnavailable 1,我们强制 Kubernetes 以一种非常保守的方式逐一移动 Pod:在rollout过程中,我们可能会有四个 Pod,但我们永远不会低于三个可用的 Pod。

回到策略上,Recreate 基本上是销毁所有的 Pod,并使用新的配置重新创建它们,而不考虑上线时间。在某些情况下,这可能是必要的,但我强烈建议你在可能的情况下使用RollingUpdate(几乎总是这样),因为它可以带来更平滑的部署过程。

也可以像对待副本集一样,向一个部署附加一个水平 Pod 自动扩缩器。

让我们测试一下我们的部署。创建一个名为deployment.yml的文件,并将其应用到我们的集群中:

kubectl apply -f deployment.yml --record

一旦命令返回,我们可以进入 Kubernetes 仪表板(如果代理已激活,地址是localhost:8001/ui),并查看左侧菜单中的 Deployments 部分,检查发生了什么:

我们有一个新的部署叫做nginx-deployment,它已经创建了一个副本集,并且该副本集也包含了指定的 Pod。在前面的命令中,我们传递了一个新的参数:--record。这会将命令保存到我们的部署的rollout历史中,这样我们就可以查询给定部署的rollout历史,以查看它应用了哪些变更。在这种情况下,只需执行以下命令:

kubectl rollout history deployment/nginx-deployment

这将展示所有改变了名为nginx-deployment的部署状态的操作。现在,让我们执行一些更改:

kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1

我们使用kubectlnginx容器的版本改回了 1.9.1 版本(kubectl非常多功能,官方文档为几乎所有操作提供了快捷方式),并且发生了几件事。第一件事是,创建了一个新的副本集,Pod 从旧的副本集转移到了新的副本集中。我们可以在仪表板左侧菜单的 Replica Sets 部分验证这一点:

如你所见,旧的副本集有 0 个 Pod,而接管的新副本集有三个 Pod。所有这些都在你不注意的情况下发生,但这是一种非常巧妙的工作流程,背后有 Kubernetes 社区和相关公司的大量努力。

发生的第二件事是我们在rollout历史中有了一个新的条目。我们来查看一下:

kubectl rollout history deployment/nginx-deployment

这应该会产生类似于以下内容的输出:

现在,我们有两个条目来描述我们对部署所做的更改。

如果你在 IT 行业工作了几年,你现在应该已经得出结论:回滚策略总是必要的,因为无论我们的 QA 有多好,生产环境中都会出现 bug,这是现实。 我是那种喜欢以“部署是无关紧要的事件(从技术角度看)”的方式构建系统的人,就像 Kubernetes 所展示的那样,并且工程师总能在生产环境出现问题时轻松找到解决办法。如果出现问题,部署会提供一个简单的回滚方式:

kubectl rollout undo deployment/nginx-deployment 

执行前面的步骤,然后再次浏览到副本集部分的仪表盘:

没错。在几秒钟内,我们已经从不稳定状态(构建失败)恢复到已知的旧版本,而且没有中断服务,也没有涉及到 IT 部门的大部分人员:一个简单的命令将稳定性带回系统。回滚命令有一些配置,我们甚至可以选择想要跳转到的修订版本。

这就是 Kubernetes 的强大之处,也是我们通过使用 Kubernetes 作为企业中间件让生活变得如此简单的原因:一个现代化的 CD 管道,通过几行配置拼凑而成,在全球所有公司中都能以相同的方式工作,简化了rollouts和回滚操作。就是这样……简单而高效。

现在,我们似乎已经掌握了将应用程序迁移到 Kubernetes 所需的足够知识,但仍然缺少一个重要部分。到目前为止,我们仅运行了未对外暴露的预定义容器。简而言之,无法从集群外部访问我们的应用程序。你将在下一节中学习如何做到这一点。

服务

到目前为止,我们已经能够将容器部署到 Kubernetes 中,并通过使用 Pod、Replica Sets、水平 Pod 自动扩展器以及 Deployments 将它们保持活跃,但到目前为止,你还没有学会如何将应用程序暴露到外部世界,或者如何在 Kubernetes 中使用服务发现和负载均衡。

服务负责上述所有操作。在 Kubernetes 中,服务并不是我们所习惯的元素。服务是一个抽象概念,用来通过模式匹配为一组 Pod 赋予实体,并通过相同的接口将其暴露到不同的通道:一组附加到 Pod 上的标签与选择器(另一组标签和规则)匹配,从而将它们分组。

首先,让我们在上一节中创建的部署基础上创建一个服务:

kind: Service
apiVersion: v1
metadata:
 name: nginx-service
spec:
 selector:
 app: nginx
 ports:
 - protocol: TCP
 port: 80
 targetPort: 80

简单直接,但有一个细节:选择器部分隐藏了一个信息。选择器是 Kubernetes 用来通过模式匹配算法连接组件的机制。让我们解释一下什么是模式匹配。在前面的 Service 中,我们指定了要选择所有具有 app 键和 nginx 值标签的 Pods。如果你回到前一部分,你会明白我们的部署在 pod 规范中有这些标签。这是一个匹配;因此,我们的服务会选择这些 pods。我们可以通过在仪表盘中的服务部分浏览并点击 nginx-service 来检查这一点,但首先,你需要创建 service

kubectl apply -f service.yml

然后,查看仪表盘:

如你所见,选择了三个 pods,它们都属于我们在前一部分创建的 nginx 部署。

不要删除前一部分的部署,否则我们的服务将无法选择 pods。

这个界面有很多有趣的信息。第一条信息是服务有一个 IP:这个 IP 被称为 clusterIP。基本上,它是集群内的一个 IP,可以被我们的 pods 和 Kubernetes 中的其他元素访问。还有一个字段叫 Type,允许我们选择服务类型。有三种类型:

  • ClusterIP

  • NodePort

  • LoadBalancer

ClusterIP 就是我们刚刚创建并解释过的。

NodePort 是另一种服务类型,在云环境中很少使用,但在本地环境中非常常见。它会在所有节点上分配一个端口来暴露我们的应用程序。这使 Kubernetes 能够定义流量进入我们 pods 的入口。由于以下两个原因,这带来了挑战:

  • 它在我们的内部网络中生成额外的流量,因为节点需要将流量转发以到达 pods(想象一下一个有 100 个节点的集群,但只有三个 pods 运行应用程序,几乎不可能击中运行其中一个的节点)。

  • 端口是随机分配的,因此你需要查询 Kubernetes API 以了解分配的端口。

LoadBalancer 是这里的皇冠上的明珠。当你创建一个类型为 LoadBalancer 的服务时,会配置一个云负载均衡器,使客户端应用程序访问负载均衡器,负载均衡器将流量重定向到正确的节点。正如你想象的那样,对于一个在几秒钟内就能创建和销毁基础设施的云环境来说,这是理想的情况。

回到前面的截图,我们可以看到另一条有趣的信息:内部端点。这是 Kubernetes 用来定位我们应用程序的服务发现机制。我们所做的就是将应用程序的 pods 连接到一个名称:nginx-service。从现在开始,不管发生什么,我们的应用程序要想访问我们的 nginx pods,只需要知道有一个名为 nginx 的服务,知道如何定位它们。

为了进行测试,我们将运行一个名为 busybox 的容器实例,它基本上是命令行工具的瑞士军刀。运行以下命令:

kubectl run -i --tty busybox --image=busybox --restart=Never -- sh

上面的命令将为我们提供一个名为busybox的容器的 shell,这个容器运行在 Pod 中,因此我们处于 Kubernetes 集群内部,更重要的是,处于网络内部,这样我们就能看到发生了什么。请注意,前面的命令仅仅运行了一个 Pod:没有创建部署或副本集,因此一旦你退出 shell,Pod 就会结束,资源也会被销毁。

一旦进入 busybox,运行以下命令:

nslookup nginx-service

这应该返回类似如下内容:

Server: 10.47.240.10
Address 1: 10.47.240.10 kube-dns.kube-system.svc.cluster.local

Name: nginx-service
Address 1: 10.47.245.73 nginx-service.default.svc.cluster.local

好吧,这里发生了什么?当我们创建服务时,我们为它指定了一个名称:nginx-service。这个名称已用于将其注册到用于服务发现的内部 DNS 中。如前所述,DNS 服务运行在 Kubernetes 上,所有 Pod 都可以访问,因此它是一个集中式的公共知识库。Kubernetes 工程师还创造了另一种方式来继续服务发现:环境变量。在相同的提示符下,运行以下命令:

env

该命令会输出所有环境变量,但其中有几个与我们刚刚定义的服务相关:

NGINX_SERVICE_PORT_80_TCP_ADDR=10.47.245.73
NGINX_SERVICE_PORT_80_TCP_PORT=80
NGINX_SERVICE_PORT_80_TCP_PROTO=tcp
NGINX_SERVICE_SERVICE_PORT=80
NGINX_SERVICE_PORT=tcp://10.47.245.73:80
NGINX_SERVICE_PORT_80_TCP=tcp://10.47.245.73:80
NGINX_SERVICE_SERVICE_HOST=10.47.245.73

这些由 Kubernetes 在创建时注入的变量,定义了应用程序如何找到我们的服务。这个方法有一个问题:环境变量是在创建时注入的,因此如果我们的服务在 Pod 生命周期中发生变化,这些变量就会变得过时,Pod 必须重新启动以注入新的值。

所有这些魔法都通过 Kubernetes 上的选择器机制发生。在这种情况下,我们使用了等号选择器:标签必须匹配才能选择一个 Pod(或一般对象)。有很多选择器选项,截至本文写作时,这仍在不断发展。如果你想了解更多关于选择器的信息,以下是官方文档:kubernetes.io/docs/concepts/overview/working-with-objects/labels/

正如你所看到的,Kubernetes 中的服务用于将我们的应用程序连接在一起。通过服务连接应用程序,让我们可以构建基于微服务的系统,通过将 API 中的 REST 端点与我们要通过 DNS 到达的服务名称关联起来。

到目前为止,你已经学会了如何将我们的应用程序暴露给集群中的其他部分,但我们如何将应用程序暴露给外部世界呢?你也已经了解了有一种服务类型可以用于此:LoadBalancer。让我们看一下以下定义:

kind: Service
apiVersion: v1
metadata:
   name: nginx-service
spec:
 type: LoadBalancer
   selector:
      app: nginx
 ports:
   - protocol: TCP
      port: 80
      targetPort: 80

前面的定义中有一个变化:服务类型现在是LoadBalancer。解释这一变化的最佳方式是查看仪表盘中的“服务”部分:

如您所见,我们新创建的服务分配了一个外部端点。如果您访问它,成功!nginx 默认页面被呈现出来。

我们创建了两个服务,分别为 nginx-servicenginx-service-lb,类型分别为 ClusterIPLoadBalancer,它们都指向属于某个部署的相同 Pods,并且通过 ReplicaSet 管理。这个概念可能有点混乱,但下面的图表会更好地解释它:

上面的图表完美地解释了我们在本节中所构建的内容。如您所见,负载均衡器位于 Kubernetes 外部,而其他所有内容都作为 API 的虚拟元素位于我们的集群内部。

其他构建块

在前面的章节中,您已经学习了成功将应用程序部署到 Kubernetes 所需的基本知识。我们讨论过的 API 对象如下:

  • Pod

  • ReplicaSet

  • 部署

  • Service

在 Kubernetes 中,还有许多其他构建块可以用来构建更复杂的应用程序;每隔几个月,Kubernetes 的工程师们都会添加新元素来改善或增加功能。

其中一个新增元素是 ReplicaSet,它旨在替代另一个叫做 ReplicationController 的组件。ReplicationController 和 ReplicaSet 的主要区别在于,后者为 Pods 提供了更先进的语义标签选择,而这些 Pods 最近在 Kubernetes 中经过了重新设计。

作为一款新产品,Kubernetes 正在不断变化(事实上,当您阅读这本书时,核心元素可能已经发生变化),因此工程师们会尽力保持不同版本之间的兼容性,以便用户不需要在短时间内强制升级。

其他更高级的构建块示例如下:

  • DaemonSet

  • PetSets

  • Jobs 和 CronJobs

  • CronJobs

若要深入了解 Kubernetes 的全栈,我们需要一本完整的书(甚至更多!)。让我们来看看其中的一些。

Daemon Sets

Daemon Sets 是一个 API 元素,用于 确保 Pod 在所有(或部分)节点上运行。Kubernetes 的一个假设是,Pod 不需要关心在哪个节点上运行,但考虑到这一点,可能会有一种情况,我们希望确保至少有一个 Pod 在每个节点上运行,原因如下:

  • 收集日志

  • 检查硬件

  • 监控

为了实现这一点,Kubernetes 提供了一个名为 Daemon Set 的 API 元素。通过标签和选择器的组合,我们可以定义所谓的 affinity(亲和性),这可以用来在某些节点上运行我们的 Pods(我们可能有特定的硬件需求,只有少数节点能够提供,因此我们可以使用标签和选择器为 Pods 提供提示,指示它们迁移到特定节点)。

Daemon Sets 有多种方式可以被联系,从 DNS 通过无头服务(一个作为负载均衡器工作的服务,而不是分配有集群 IP)到节点 IP,但 Daemon Sets 在作为通信发起者时效果最佳:某个事件发生后,Daemon Set 会发送一个带有该事件信息的事件(例如,节点空间不足)。

PetSets

PetSets 是 Kubernetes 中一个有趣的概念:它们是强命名资源,其名称应该长期保持不变。目前,Pod 在 Kubernetes 集群中没有强实体:你需要创建一个服务来定位 Pod,因为它们是短暂的。Kubernetes 可以在任何时候重新调度它们,且无需提前通知以更改其名称,正如我们之前所见。如果你在 Kubernetes 中运行一个部署并杀死其中一个 Pod,它的名称会从(例如)pod-xyz 改为 pod-abc,这种变化是不可预测的。因此我们无法提前知道应用程序中要连接的名称。

当使用 Pet Set 时,情况完全不同。Pet Set 有一个顺序,因此很容易猜测 Pod 的名称。假设我们部署了一个名为 mysql 的 Pet Set,它定义了运行 MySQL 服务器的 Pod。如果我们有三个副本,则命名方式如下:

  • mysql-0

  • mysql-1

  • mysql-2

因此,我们可以将这些知识内建到我们的应用程序中来访问它们。这虽然不是最优解,但足够好:我们仍然是通过名称来耦合服务(DNS 服务发现存在这个限制),但是在所有情况下它都能正常工作,这是值得付出的牺牲,因为作为回报,我们获得了更多的灵活性。理想的服务发现情况是,我们的系统甚至不需要知道执行工作的应用程序的名称:只需将消息投递到虚空(网络)中,适当的服务器会接收并做出响应。

在 Kubernetes 的后续版本中,Pet Sets 被另一个名为 Stateful Set 的元素所替代。Stateful Set 相比 Pet Set 主要在 Kubernetes 如何管理 主控知识以避免脑裂情况 上有所改进:即两个不同的元素认为它们自己在控制中。

Jobs

在 Kubernetes 中,Job 本质上是一个元素,它生成指定数量的 Pod,并在这些 Pod 完成任务之前等待它们完成其生命周期。当需要运行一次性任务时,比如旋转日志或跨数据库迁移数据,Job 非常有用。

Cron 作业与 Jobs 有相同的概念,但它们是通过时间触发的,而不是一次性过程。

这两者结合起来是非常强大的工具,能够保持任何系统运行。如果你考虑一下如何通过 SSH 在没有 Kubernetes 的情况下旋转日志,那是相当危险的:默认情况下无法控制是谁在做什么,通常也没有审查个人执行的 SSH 操作。

使用这种方法,可以创建一个 Job,并让其他工程师在运行之前进行审查,确保安全性。

密钥和配置管理

一般来说,在 Docker 中,密钥通过环境变量传递到容器中。这是非常不安全的:首先,没有控制谁可以访问什么,其次,环境变量并不是设计用来作为密钥的,很多商业软件(包括开源软件)会将它们输出到标准输出中作为启动过程的一部分。不用说,这非常不方便。

Kubernetes 已经非常优雅地解决了这个问题:它不是通过环境变量将密钥传递给容器,而是将密钥挂载为一个文件(或多个文件)卷,准备供使用。

默认情况下,Kubernetes 会将一些与集群相关的密钥注入到我们的容器中,以便它们能够与 API 等进行交互,但也可以创建你自己的密钥。

有两种方式来创建密钥:

  • 使用kubectl

  • 定义一个类型为密钥的 API 元素,并使用kubectl进行部署

第一种方法相当简单。在你当前的工作文件夹中创建一个名为secrets的文件夹,并在其中执行以下命令:

echo -n "This is a secret" > ./secret1.txt
echo -n "This is another secret" > ./secret2.txt

这将创建两个文件,其中包含两个字符串(目前是简单字符串)。现在是时候使用kubectl在 Kubernetes 中创建密钥了:

kubectl create secret generic my-secrets --from-file=./secret1.txt --from-file=./secret2.txt

就是这样。一旦完成,我们可以使用kubectl查询密钥:

kubectl get secrets

这在我的情况下返回了两个密钥:

  • 集群注入的服务账户令牌

  • 我新创建的密钥(my-secrets

创建密钥的第二种方法是通过在yaml文件中定义它,并通过kubectl进行部署。看一下下面的定义:

apiVersion: v1
kind: Secret
metadata:
 name: my-secret-yaml
type: Opaque
data:
 secret1: VGhpcyBpcyBhIHNlY3JldA==
 secret2: VGhpcyBpcyBhbm90aGVyIHNlY3JldA==

首先,secret1secret2的值似乎是加密的,但实际上它们并没有加密,它们只是被编码为base64

echo -n "This is a secret" | base64
echo -n "This is another secret" | base64

这将返回你在这里看到的值。密钥的类型是 Opaque,这是密钥的默认类型,其余部分看起来相当简单。现在使用 kubectl 创建密钥(将前面的内容保存在一个名为secret.yml的文件中):

kubectl create -f secret.yml

就是这样。如果你再次查询密钥,注意应该会有一个名为my-secret-yaml的新密钥。也可以通过左侧菜单中的 Secrets 链接,在仪表盘中列出并查看密钥。

现在是时候使用它们了。为了使用这个密钥,需要做两件事:

  • 将密钥声明为一个卷

  • 从密钥挂载卷

让我们来看一个使用密钥的Pod

{
   "apiVersion": "v1",
   "kind": "Pod",
   "metadata": {
      "name": "test-secrets",
      "namespace": "default"
   },
   "spec": {
      "containers": [{
         "name": "pod-with-secret",
         "image": "nginx",
         "volumeMounts": [{
            "name": "secrets",
            "mountPath": "/secrets",
            "readOnly": true
         }]
      }],
      "volumes": [{
          "name": "secrets",
          "secret": {
              "secretName": "my-secret"
          }
      }]
   }
}

所以,你在这里学到了一件新事物:kubectl也能理解 JSON。如果你不喜欢 YAML,可以将定义写成 JSON 而不会有任何副作用。

现在,查看 JSON 文件,我们可以看到首先密钥是如何被声明为一个卷,然后密钥是如何挂载到路径/secrets 中的。

如果你想验证这一点,只需在你的容器中运行一个命令来检查:

kubectl exec -it test-secrets ls /secrets

这里应该列出我们创建的两个文件,secret1.txtsecret2.txt,其中包含我们指定的数据。

Kubernetes- 继续前进

在本章中,你已经学习了足够的内容来运行简单的 Kubernetes 应用程序,尽管我们不能自称为专家,但我们已经在成为专家的路上取得了领先一步。Kubernetes 是一个以光速发展的项目,保持最新状态的最佳方法就是在 GitHub 上关注这个项目,链接地址为 github.com/kubernetes

Kubernetes 社区对于用户提出的问题反应迅速,并且非常鼓励大家为源代码和文档做出贡献。

如果你继续使用 Kubernetes,可能会需要一些帮助。官方文档相当完整,尽管有时它似乎需要进行一些重组,但通常它足以让你继续前进。

我发现学习 Kubernetes 最好的方法是在 Minikube(或测试集群)中进行实验,而不是直接投入到更大的项目中。

总结

在本章中,我们探讨了部署 Kubernetes 应用程序所需的一些重要概念。正如之前所提到的,在一章中不可能涵盖所有与 Kubernetes 相关的内容,但通过本章的知识,我们将能够在接下来的章节中设置一个持续交付管道,以实现零停机部署,并避免“大爆炸效应”(即会停顿世界的大规模部署),从而让我们的组织变得更快。

第八章:发布管理 – 持续交付

发布管理一直是软件开发中最无聊的部分。这是一个讨论的过程,来自不同团队的人(运维、管理、开发等)会把所有细节整理在一起,计划如何部署公司某个应用(或多个应用)的新版本。

这通常是一个发生在早上 4 点的大事件,而且是一个二元事件:我们要么成功发布新版本,要么失败并需要回滚。

在这种类型的部署中,压力和紧张是共同的因素,最重要的是,我们在与统计数据作斗争。

在本章中,您将学习如何创建一个持续交付管道,并部署基于微服务的系统来更新它,确保所有服务持续可用。

我们将专门讨论以下主题:

  • 与统计数据作斗争

  • 测试系统

  • 为镜像设置持续交付管道

  • 设置 Jenkins

  • 为您的应用程序设置持续交付

与统计数据作斗争

我们曾多次提到部署就像“大爆炸”事件。这是我在设置新系统时总是尽量避免的事情:发布应该是平稳的事件,可以随时轻松完成,而且应该能够在几分钟内轻松回滚。

这可能是一个庞大的任务,但一旦你为工程师提供了坚实的基础,奇妙的事情就会发生:他们开始变得更加高效。如果你提供一个稳固的基础,使他们确信只需几次点击(或命令)就能将系统恢复到稳定状态,你就解决了任何软件系统中的大部分复杂性。

让我们来谈谈统计数据。当我们创建部署计划时,我们是在创建一个配置为串联的系统:这是一个有限的步骤列表,将导致我们的系统更新:

  • 将 JAR 文件复制到服务器

  • 停止旧的 Spring Boot 应用

  • 复制属性文件

  • 启动新的 Spring Boot 应用

如果任何步骤失败,整个系统都会失败。这就是我们所说的串联系统:任何一个组件(步骤)的失败都会影响整个系统。假设每个步骤的失败率为 1%。1%的失败率似乎是一个可以接受的数字……直到我们将它们串联起来。从前面的例子来看,假设我们有一个包含 10 个步骤的部署。这些步骤的失败率为 1%,即成功率为 99%,或者说是 0.99 的可靠性。将它们串联起来意味着整个系统的可靠性可以表示如下:

(0.99)¹⁰ = 0.9043

这意味着我们的系统成功率为 90.43%,换句话说,失败率为 9.57%。情况发生了巨大变化:每 10 次部署就有 1 次会失败,这与我们之前提到的每个单独步骤的 1%失败率相差甚远。

几乎 10% 的占比对于依赖系统来说已经相当多,这可能是我们不愿承担的风险,那么为什么不努力将这个风险降到一个可接受的水平呢?为什么不把这个风险转移到一个不会影响生产的前置步骤中,并且将部署简化为一个可以随时断开的简单开关(开/关)呢?

这两个概念叫做金丝雀发布(canary)和蓝绿部署(blue green deployments),我们将研究如何在 Kubernetes 中使用它们,从而降低部署失败的风险,减少传统软件开发中部署时发生的 大爆炸事件 带来的压力。

测试系统

为了实现持续交付流水线,我们需要一个可以操作的系统,经过一些讨论和演示,我开发了一个我倾向于使用的系统,因为它几乎没有业务逻辑,并且留给我们很多思考底层基础设施的空间。

我称这个系统为 Chronos,正如你所猜测的,它的目的是与时区和日期格式的管理有关。这个系统非常简单:

我们有三个服务:

  • API 聚合器

  • 一个将时间戳转换为 ISO 格式日期的服务

  • 一个将时间戳转换为 UTC 格式日期的服务

这些服务协调工作,将时间戳转换为不同格式的日期,但它们也可以扩展,我们可以聚合更多服务以添加更多功能,并通过 API 聚合器暴露它们。

每个服务都会打包成一个不同的 Docker 镜像,作为 Kubernetes 中的部署(Deployment)进行部署,并通过服务(包括外部和内部服务)暴露到集群和外部世界(如 API 聚合器)。

ISO 日期和 UTC 日期服务

ISO 日期服务仅仅接收一个时间戳,并返回使用 ISO 格式表示的等效日期。让我们看看它的代码:

const Hapi = require('hapi')
const server = new Hapi.Server()
const moment = require('moment')

server.connection({port: 3000})

server.route({
  method: 'GET',
  path: '/isodate/{timestamp}',
  handler: (request, reply) => {
    reply({date: moment.unix(request.params.timestamp).toISOString()})
  }
})

server.start((err) => {
  if (err) {
    throw err
  }
  console.log('isodate-service started on port 3000')
})

这个服务本身非常简单:它使用一个名为 moment 的库和一个名为 hapi 的框架,通过传递一个 URL 参数中的时间戳来提供等效的 ISO 日期。编写该服务的语言是 Node.js,但你不需要成为该语言的专家,只需能够阅读 JavaScript 即可。像所有 Node.js 应用一样,它配有一个 package.json 文件,用于描述项目及其依赖项:

{
  "name": "isodate-service",
  "version": "1.0.0",
  "description": "ISO Date Service",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "David Gonzalez",
  "license": "ISC",
  "dependencies": {
    "hapi": "¹⁵.2.0",
    "moment": "².15.1"
  }
}

package.json 的某些字段是自定义的,但重要部分是依赖项和脚本部分。

现在,剩下一个重要的文件;Dockerfile:

FROM node:latest

RUN mkdir /app/
WORKDIR /app/

COPY . /app/
RUN npm install
EXPOSE 3000

CMD ["npm", "start"]

为了测试我们的服务,让我们构建 Docker 镜像:

docker build . -t iso-date-service

几秒钟后(或者稍久一些),我们的镜像已经准备好使用。只需运行它:

docker run -it -p 3000:3000 iso-date-service

就这些。为了测试它,可以使用 curl 获取一些结果:

curl http://localhost:3000/isodate/1491231233

这将返回一个 JSON,包含以 ISO 日期格式表示的时间戳,正如你在终端看到的那样。

UTC 日期服务基本相同,只是代码和接口不同:

const Hapi = require('hapi')
const server = new Hapi.Server()
const moment = require('moment')
server.connection({port: 3001})

server.route({
  method: 'GET',
  path: '/utcdate/{timestamp}',
  handler:  (request, reply) => {
    let date = moment.unix(request.params.timestamp).utc().toISOString().substring(0, 19)
    reply({date: date})
  }
})

server.start((err) => {
  if (err) {
    throw err
  }
  console.log('isodate-service started on port 3001')
})

如你所见,做了一些改动:

  • 端口是 3001

  • 返回的日期是 UTC 日期(基本上是没有时区信息的 ISO 日期)

我们还有一个 Dockerfile,和 ISO 日期服务的相同,还有一个 package.json,如下所示:

{
  "name": "utcdate-service",
  "version": "1.0.0",
  "description": "UTC Date Service",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "David Gonzalez",
  "license": "ISC",
  "dependencies": {
    "hapi": "¹⁵.2.0",
    "moment": "².15.1"
  }
}

这些是小的改动(仅是描述和名称)。总的来说,你应该在 UTC 日期服务中拥有以下文件:

  • Dockerfile(与 ISO 日期服务相同)

  • 包含之前代码的 index.js

  • package.json

如果你想让自己的生活更轻松,只需克隆 git@github.com:dgonzalez/chronos.git 仓库,这样你就有了所有准备执行的代码。

现在,为了测试一切是否正确,构建 Docker 镜像:

docker build . -t utc-date-service

然后运行它:

docker run -it -p 3001:3001 utc-date-service

一旦启动,我们应该有我们的服务在 3001 端口上监听。你可以通过执行 curl 来检查,如下所示:

curl http://localhost:3001/utcdate/853123135

以类似于 ISO 日期服务的方式,这应该返回一个 JSON 格式的日期,但这次是 UTC 格式。

聚合服务

aggregator 服务是一个微服务,顾名思义,它聚合了其他两个(或更多)服务,并为消费者提供前端 API,使得所有幕后逻辑得以封装。虽然它并不完美,但这是一个常见模式,因为它允许我们使用断路器的概念并且在专用层中管理错误。

在我们的案例中,服务相当简单。首先,让我们看一下代码:

const Hapi = require('hapi')
const server = new Hapi.Server()
let request = require('request')

server.connection({port: 8080})

server.route({
  method: 'GET',
  path: '/dates/{timestamp}',
  handler:  (req, reply) => {
    const utcEndpoint = `http://utcdate-service:3001/utcdate/${req.params.timestamp}`
    const isoEndpoint = `http://isodate-service:3000/isodate/${req.params.timestamp}`
    request(utcEndpoint, (err, response, utcBody) => {
      if (err) {
        console.log(err)
        return
      }
      request(isoEndpoint, (err, response, isoBody) => {
        if (err) {
          console.log(err)
          return
        }
        reply({
          utcDate: JSON.parse(utcBody).date,
          isoDate: JSON.parse(isoBody).date
        })
      })
    })
  }
})

server.start((err) => {
  if (err) {
    throw err
  }
  console.log('aggregator started on port 8080')
})

为了简化代码的理解,我们没有使用 promises 或 async/await,而是选择了嵌套的 callback(这很容易阅读)。

以下是从前面的代码中需要注意的一些要点:

  • 我们通过名称调用服务(utcdate-serviceisodate-service),利用与 Kubernetes DNS 的通信

  • 在返回之前,aggregator 服务会调用两个服务,并返回一个包含聚合信息的 JSON 对象

为了测试这个服务,我们需要创建指向 isodate-serviceutcdate-service 的 DNS 条目(或主机条目),这比在 Kubernetes 中测试更为复杂,因此我们暂时跳过测试。

与任何 Node 应用一样,aggregator 服务需要一个 package.json 来安装依赖项并控制一些方面:

{
   "name": "aggregator",
   "version": "1.0.0",
   "description": "Aggregator service",
   "main": "index.js",
   "scripts": {
       "start": "node index.js"
   },
   "author": "David Gonzalez",
   "license": "ISC",
   "dependencies": {
       "hapi": "¹⁵.2.0",
       "request": "².75.0"
   }
}

package.json 非常重要,特别是脚本块,它指示我们当 Docker 容器基于 Dockerfile 中定义的镜像执行 npm start 命令时该做什么:

FROM node:latest

RUN mkdir /app/
WORKDIR /app/

COPY . /app/
RUN npm install
EXPOSE 3000

CMD ["npm", "start"]

到目前为止,你应该有三个文件:

  • index.js

  • Dockerfile

  • package.json

使用以下命令构建 docker 容器:

docker build . -t aggregator

检查它是否按预期工作:

docker run -it -p 8080:8080 aggregator

即使服务器无法解析请求,因为它不知道如何与 isodate-serviceutcdate-service 通信,它仍然应该启动。

将镜像推送到 Google Container Registry

到目前为止,我们的本地仓库中已有三个镜像:

  • iso-date-service

  • utc-date-service

  • aggregator

这三个镜像现在存在于你的计算机上,但不幸的是,我们在 GKE 中的 Kubernetes 集群将无法访问它们。解决这个问题的方法是将这些镜像推送到一个 Docker 注册中心,这样我们的集群就能访问它们。谷歌云提供了一个 Docker 注册中心,非常适合与 GKE 配合使用,原因有很多:

  • 数据隔离:数据永远不会离开 Google 网络

  • 集成:GCP 中的服务可以通过隐式认证进行交互

  • 自动化:这与 GitHub 和其他服务集成,使我们能够构建镜像,自动创建持续交付镜像的管道。

在设置与 Git 的持续交付管道之前,我们将手动推送镜像,以便理解其工作原理。Google 容器注册中心 (GCR) 在全球范围内都有复制,因此你需要做的第一件事是选择存储镜像的地方:

  • us.gcr.io在美国托管你的镜像

  • eu.gcr.io在欧盟托管你的镜像

  • asia.gcr.io在亚洲托管你的镜像

在我的情况下,eu.gcr.io是完美的匹配。接下来,我们需要我们的项目 ID。可以通过点击控制台顶部栏中的项目名称找到它:

在我的案例中,项目 ID 是implementing-modern-devops. 现在,第三个组件是我们已经拥有的镜像名称。通过这三个组件,我们可以构建出 Docker 镜像 URL 的名称:

  • eu.gcr.io/isodate-service:1.0

  • eu.gcr.io/utcdate-service:1.0

  • eu.gcr.io/aggregator:1.0

1.0 部分是我们镜像的版本。如果没有指定,默认是 latest,但我们将为镜像打上版本号,以便追溯。

现在是时候为我们的镜像打上合适的标签了。首先是 ISO 日期服务:

docker tag iso-date-service eu.gcr.io/implementing-modern-devops/isodate-service:1.0

然后是 UTC 日期服务:

docker tag utc-date-service eu.gcr.io/implementing-modern-devops/utcdate-service:1.0

最后,我们有了aggregator服务:

docker tag aggregator eu.gcr.io/implementing-modern-devops/aggregator-service:1.0

这是 Docker 用来识别镜像推送地址的机制:Docker 读取我们的镜像名称,并识别出镜像要推送到的 URL。在这种情况下,由于我们使用的是私有注册中心(Google 容器注册中心是私有的),我们需要使用凭据,但使用 gcloud 命令,操作变得相当简单:

gcloud docker -- push eu.gcr.io/implementing-modern-devops/aggregator-service:1.0

现在是时候处理isodate-service了:

gcloud docker -- push eu.gcr.io/implementing-modern-devops/isodate-service:1.0

最后,这里是utcdate-service

gcloud docker -- push eu.gcr.io/implementing-modern-devops/utcdate-service:1.0

小心,项目 ID 会发生变化,因此请根据你的配置自定义命令。

在稍等片刻之后(将镜像推送到 GCR 可能需要几分钟时间),这些镜像应该已经上传到我们私有的 Google 容器注册中心实例中了。

让我们回顾一下我们所做的:

  • 我们已经在本地构建了镜像

  • 我们给镜像打上了适当的标签,以便能够将它们推送到 GCR

  • 我们使用gcloud推送了镜像到 GCR

这非常直接,但如果你以前没有做过,可能会有点棘手。我们所有的镜像都保存在我们的私人容器注册表中,随时可以使用。

设置镜像的持续交付流水线

现在我们已将镜像部署到 GCR,我们需要自动化这个过程,以最小化人工干预。为此,我们将使用 Google 容器注册表中的构建触发器部分。在这种情况下,我们将使用 GitHub,因为它是业界标准的 Git 代码库管理工具。创建一个www.github.com账号(如果你还没有的话),然后创建三个代码库:

  • aggregator

  • isodate-service

  • utcdate-service

这些可以是公开的,但如果你将来要处理私有代码,应该创建私有代码库(需要付费)或选择其他提供商,比如 Google Cloud Platform 中的源代码库。

我们需要做的第一件事是将三个服务的代码推送到代码库中。Github 会提供相关指令,基本上,过程如下:

  1. 克隆代码库

  2. 根据需要添加前述部分的代码

  3. 将代码推送到远程代码库

我的 GitHub 用户名是dgonzalez,推送aggregator代码的命令如下:

git clone git@github.com:dgonzalez/aggregator.git

现在,将aggregator中的代码复制到新创建的文件夹中,使用clone命令并执行(在aggregator文件夹内):

git add .

提交更改:

git commit -m 'Initial commit'

然后将它们push到远程代码库:

git push origin master

在执行这些命令后,你的代码库应该会像下面的截图所示:

我们使用的命令是非常基础的 Git 命令。你可能对 Git 已经有所了解,但如果你不熟悉,我建议你跟随一些教程,比如try.github.io/levels/1/challenges/1

现在我们的代码库已经准备好了,是时候回到 GCP 设置我们流水线的构建触发器了。我们需要做的第一件事是进入 Google Cloud Platform 的容器注册表中的触发器部分:

这个功能允许我们创建触发器,这些触发器会根据事件触发镜像的构建。有几种触发策略。在这种情况下,我们将基于新标签的创建来构建镜像。最常见的做法是监控主分支的变化,但我非常支持版本管理。想一想:容器是不可变的工件:一旦创建,就不应更改,但如果容器内部的代码出现问题怎么办?这个策略是从主分支分支出来,然后创建所谓的热修复构建。通过标签,我们也可以做到这一点,只不过是从标签而不是从主分支进行分支,这有以下好处:

  • 主分支可以更改,而不会触发事件

  • 标签不能被意外创建(因此不会出现意外发布)

  • 版本保存在源代码管理器中,而不是在代码中

  • 你可以将标签与构建的工件关联起来。

也就是说,完全可以将主分支作为参考点并使用其他组合:这里的重要教训是坚持一个程序并让每个人都清楚。

点击创建触发器,然后选择 GitHub。点击“下一步”后,它会让你从列表中选择项目;然后再次点击“下一步”。现在我们会看到一个表单和几个选项:

我们将使用 Dockerfile 而不是cloudbuild.yaml(后者是 GCP 特有的)并设置触发器以便在标签上触发;镜像名称必须与前面章节中创建的仓库相匹配(记住eu.*名称并检查仓库名称)。

创建后,什么也不会发生。我们的仓库没有标签,因此没有构建任何内容。让我们创建一个标签:

git tag -a 1.0 -m "my first tag"

这将创建一个标签,现在我们需要将其推送到服务器:

git push origin 1.0

现在,我们回到 GCP 容器注册表,查看发生了什么:一个新的构建已被触发,将版本 1.0 推送到aggregator镜像的注册表中:

从现在开始,如果我们在仓库中创建新的标签,GCP 将为我们构建镜像,并且可以与 GitHub 中的提交相关联,这样我们就能完全追踪到我们构建的每个环境中的内容。没有比这更好的了。

整个构建和推送过程本可以通过 Jenkins 完成,正如你在第四章(持续集成)中所学到的那样,但我认为如果有人能够以合理的价格处理你的问题,胜过自己去解决它们并在已经复杂的系统中增加更多的可变因素。在这种情况下,注册表、构建流水线和自动化由 Google Cloud Platform 负责。

设置 Jenkins

在前面的部分,我们利用了 Google Cloud Platform 的镜像操作,但现在,我们需要从某个地方以 CI/CD 方式管理 Kubernetes。在这种情况下,我们将使用 Jenkins 来实现这个目的。我们在这里有几种选择:

  • 在 Kubernetes 中部署 Jenkins

  • 在裸机上安装 Jenkins

  • 在 Kubernetes 外部的容器中安装 Jenkins

最近,Jenkins 变得对 Kubernetes 更加友好,提供了一个插件,可以在需要时以容器化的方式启动 Jenkins 的从节点,从而将硬件的配置和销毁交给 Kubernetes 来管理。当你的集群足够大(50 台以上机器)时,这是一种非常有趣的方式,但如果集群较小,这可能会导致邻居噪音问题。

我非常推崇分离原则:CI/CD 应该能够与生产基础设施进行通信,但出于两个原因,它不应与生产硬件共用一台机器:

  • 资源消耗

  • 漏洞

想一想:CI/CD 软件默认情况下对攻击者是脆弱的,因为它需要通过界面执行命令;因此,你实际上是将底层基础设施的访问权限交给了潜在的攻击者。

我的建议是:从简单开始。如果公司规模较小,我会选择将 Jenkins 部署在容器中并挂载卷,逐步发展基础设施,直到集群足够大,可以容纳 Jenkins 而不会对性能造成显著影响;然后将其移入专用的命名空间中。

在第四章(持续集成)中,我们在没有使用任何卷的情况下将 Jenkins 部署在容器中,这可能会带来问题,因为配置可能会在重启后丢失。现在,我们将把 Jenkins 安装在裸机上,这样就有了另一种管理 Jenkins 的方式。

我们需要做的第一件事是创建一台 GCP 机器。

上面的截图是我 Jenkins 机器的配置。以下是几个重要的方面:

  • 使用 Ubuntu 而不是 Debian(我选择了最新的 Ubuntu LTS 版本)

  • 小型实例(我们可以稍后扩展)

  • 可能需要更改防火墙设置,以便能够访问 Jenkins

其他配置都是标准的。我们没有为 Jenkins 配置静态 IP,因为这只是一个演示,但你可能希望配置静态 IP,就像你之前学到的那样,并在 DNS 中添加一个条目,以便为你的 CI 服务器提供一个静态的参考点。

将其以 Terraform 的方式执行也是一个不错的练习,这样你就可以以基础设施即代码IaC)的方式管理基础设施。

一旦机器启动,就可以安装 Jenkins。我们将按照官方指南进行操作,指南链接如下:wiki.jenkins.io/display/JENKINS/Installing+Jenkins+on+Ubuntu

使用 Google Cloud 平台的 Web SSH 终端,打开新创建机器的 shell 并执行以下命令:

wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add -

然后,添加 Jenkins 仓库:

sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'

然后,更新软件包列表:

sudo apt-get update

最后,安装 Jenkins:

sudo apt-get install jenkins

就这样。一旦前面的命令完成,Jenkins 应该已经安装好,可以作为服务启动、停止和重启。为了确保它在运行,执行以下命令:

sudo service jenkins restart

现在,如果我们浏览服务器上的公共 IP 和端口 8080,我们将看到 Jenkins 的初始屏幕。

你可能需要调整防火墙以允许访问该机器上的端口8080

这个初始屏幕很熟悉,我们需要获取密码以初始化 Jenkins。这个密码在日志中:

cat /var/log/jenkins/jenkins.log

输入密码并初始化 Jenkins(建议的插件)。这可能需要一些时间;与此同时,我们还需要设置 Gcloud SDK。首先,切换到 Jenkins 用户:

sudo su jenkins

然后只需执行以下命令:

curl https://sdk.cloud.google.com | bash

安装完成后,你需要打开一个新的 shell 以使更改生效。这样做并安装 kubectl

gcloud components install kubectl

现在我们系统中有了 Kubectl 二进制文件,我们需要将其连接到一个集群,但首先是时候创建一个集群了。正如你在前几章中学到的,只需创建一个包含三台小型机器的集群。创建完成后,如前一章所示,连接 Jenkins 机器到集群,但首先,运行 gcloud init 来配置一个新的 auth 会话(选项 2),使用你的帐户。

完成后,确保 kubectl 可以与集群通信,通过执行以下测试命令:

kubectl get nodes

你应该列出组成你集群的三个节点。现在我们需要让 kubectljenkins 用户可用。只需运行以下命令:

ln -s /root/google-cloud-sdk/bin/kubectl /usr/bin/kubectl

将所有者更改为 Jenkins:

现在,回到 Jenkins,按照以下截图设置管理员用户:

点击保存并完成,就完成了。

在开始创建作业之前,我们需要让 jenkins 用户可以使用二进制文件 kubectl。以 root 用户登录并执行:

ln -s /var/lib/jenkins/google-cloud-sdk/bin/kubectl /usr/bin/kubectl

这将确保 jenkinskubectl 命令指向前面步骤中 jenkins 用户安装的 SDK。

现在,我们有了一切:

  • Jenkins

  • Google Cloud SDK

  • 一个 GKE 集群

  • Jenkins 与 GKE 之间的连接

在继续之前,我们将确保一切按预期工作。进入 Jenkins 创建一个新的自由风格项目,并添加一个构建步骤,使用以下命令:

kubectl get nodes

保存项目并运行。输出应该与以下截图非常相似:

这表示我们可以开始了。

一般来说,Jenkins 和其他 CI 系统绝不应暴露在互联网上。绝不。只需要一个弱密码,如果公开访问,任何人都可以摧毁你的系统。在这个示例中,我们没有配置防火墙,但在你的公司,应该仅允许来自办公室 IP 的访问。

为你的应用程序实现持续交付

到目前为止,我们已经设置了一些元素:

  • 一个包含我们代码的 GitHub 仓库(aggregator

  • 一个在 GCP 中为我们的 Docker 镜像配置的持续交付管道,一旦我们标记代码,就会触发该管道。

  • 一个 Kubernetes 集群

  • Jenkins 连接到前面的集群

现在,我们将设置代码和 Kubernetes 基础设施的持续交付管道。这个管道将由 Jenkins 任务触发,我们将手动触发它。

你可能会认为,所有关于持续交付CD)的内容都是关于透明地将代码发布到生产环境中,而无需任何人工干预,但实际上这里我们有一些需要手动操作的事件,以便启动构建。我曾在一些地方工作过,其中持续交付是通过更改仓库的主分支自动触发的,在经历了几次事故后,我真的相信,手动触发是获得对部署控制的巨大好处的合理代价。

例如,在发布镜像时,通过手动创建标签来构建我们的镜像,我们设置了一个屏障,以防止任何人不小心向主分支提交代码并发布可能不稳定甚至更糟的版本。现在,我们将做类似的事情,但发布代码的任务将在 Jenkins 中手动触发,因此通过控制对 Jenkins 的访问,我们可以追踪谁做了什么,同时我们还可以免费获得基于角色的访问控制。我们可以为团队成员分配角色,防止经验较少的开发人员在没有监督的情况下制造混乱,同时仍然保持足够的灵活性,以便自动化发布代码。

我们首先需要做的是在 GitHub 上创建一个名为aggregator-kubernetes的仓库,用于托管所有 Kubernetes 资源的 YAML 文件。我们将为utcdate-serviceisodate-service做同样的事,但我们先从aggregator开始。

一旦我们创建了仓库,就需要创建 Kubernetes 对象来部署并暴露服务。简而言之,我们的系统将呈现以下图示的样子:

在上面的图片中,我们可以看到我们需要为每个应用程序创建的 Kubernetes 对象(ReplicaSetService)(部署部分省略)。红色部分显示的是应用程序本身。现在,我们聚焦于aggregator,因此我们需要创建一个将由 Deployment 管理的ReplicaSet和一个将通过gcloud负载均衡器将我们的 API 暴露给外界的LoadBalancer类型的 Service。

我们需要的第一个元素是我们的部署:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: aggregator
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: aggregator-service
    spec:
      containers:
      - name: aggregator-service
        image: eu.gcr.io/implementing-modern-devops/aggregator-service:1.0
        ports:
        - containerPort: 8080

这没什么意外的。它是一个简单的部署对象,包含了我们自动构建过程为我们创建的镜像(记住,我们创建了版本 1.0 的标签……也别忘了根据你的项目进行定制)。在我们新的仓库 aggregator-kubernetes 中,将此文件保存在名为 objects 的文件夹下,文件名为 deployment.yaml。现在是时候创建将要暴露我们应用的服务了:

kind: Service
apiVersion: v1
metadata:
   name: aggregator-service
spec:
   ports:
      - port: 80
         targetPort: 8080
   selector:
      app: aggregator-service
   type: LoadBalancer

再次强调,这非常简单:一个服务,通过 Google Cloud 中的负载均衡器将任何带有标签 app: aggregator-service 的内容暴露到外部。将其保存在名为 service.yaml 的文件中,放入 objects 文件夹。现在是时候提交更改并将其推送到你的 GitHub 仓库:

git add .

然后,执行以下操作:

git commit -m 'Initial commit'

最后,看看这个:

git push origin master

到现在为止,你已经把 aggregator 的所有基础设施代码都放在了你的 GitHub 仓库中,并且布局类似于以下内容:

在 objects 文件夹中,你可以找到两个 YAML 文件:deployment.yamlservice.yaml。我们可以使用 kubectl(首先将其连接到集群)在本地运行这些文件,以验证它们是否按预期工作(我推荐你这样做)。

现在是时候设置 Jenkins 作业来协调我们的构建了。在 Jenkins 中创建一个新的自由式项目,并进行如下配置:

首先,查看 GitHub 仓库。如你所见,出现了一个错误,这仅仅是因为 GitHub 需要一个 SSH 密钥来识别客户端。GitHub 在 help.github.com/articles/connecting-to-github-with-ssh/ 中解释了如何生成和配置这些密钥。

一旦你添加了通过生成的私钥进行身份验证的凭证,错误应该会被移除(记住,凭证类型是“带用户名的 SSH 密钥”,你的用户名必须与 GitHub 中的匹配)。

在这里,我们可以尝试很多 Cit 选项,比如在每次构建时创建一个标签,以便追踪系统中发生的事情,或者甚至从标签构建。我们将构建主分支:这次没有标签。

现在,我们将为此任务添加唯一的构建步骤:

正如你在前面的章节中学到的,使用 kubectl apply,我们几乎可以控制一切。在这种情况下,我们将包含 yamls 的文件夹作为参数传递,因此 kubectl 将根据我们即将创建的 YAML 定义在 Kubernetes 上执行操作。

保存任务并运行。一旦完成,它应该会成功,并且日志类似于以下内容:

这个任务可能会失败几次,因为涉及很多组件。目前,你已经具备足够的知识来排查这些组件的集成问题。

就是这样。我们的持续交付(CD)管道已成功运行。从现在开始,如果我们想对aggregator进行更改,我们只需要向我们的代码仓库添加/修改文件,使用新版本进行标记,修改aggregator-kubernetes定义以指向新的镜像,并启动我们的 Jenkins 任务。

还有两个额外的步骤:

  • 创建标签

  • 手动启动任务

这是你为部署中高度控制所付出的代价,但这里有一点“秘密调料”:我们已经为极大的部署灵活性做好了准备,正如我们将在下一节中看到的那样,但首先,你应该为utcdate-serviceisodate-service重复相同的操作,这样我们的完整系统才能运行。如果你想节省很多时间,或者检查自己是否走在正确的路上,可以查看我的仓库:github.com/dgonzalez/chronos

每个服务内部都有一个名为 definitions 的文件夹,里面包含了让一切正常运行的 Kubernetes 对象。

注意服务的命名:aggregator期望能够从 DNS 中解析isodate-serviceutcdate-service,所以你的服务(Kubernetes 对象)应该按此命名。

常规发布

现在一切就绪;如果你已经完成了utcdate-serviceisodate-service的部署,Kubernetes 上应该已经安装了一个完全运行的系统。它的工作原理非常简单:当你通过/dates/{timestamp}路径获取aggregator的 URL 时,将timestamp替换为有效的 UNIX 时间戳,该服务将联系utcdate-serviceisodate-service,并将时间戳转换为 UTC 和 ISO 格式。在我的情况下,Google Cloud Platform 提供的负载均衡器会将 URL 指向:http://104.155.35.237/dates/1111111111

它会有以下响应:

{
   utcDate: "2005-03-18T01:58:31",
   isoDate: "2005-03-18T01:58:31.000Z"
}

你可以玩一会儿,但其实没什么花样:只是一个简单的演示系统,使微服务及其自动化变得容易理解。在这个例子中,我们没有运行任何测试,但对于持续交付管道来说,测试是必须的(我们稍后会讨论这个问题)。

正如本节标题所示,我们将创建一个新版本的应用程序,并使用我们的持续交付管道发布它。

我们的新版本将非常简单,但也很具示范性。在aggregator中,将index.js替换为以下代码:

const Hapi = require('hapi')
const server = new Hapi.Server()
let request = require('request')
server.connection({port: 8080})
server.route({
  method: 'GET',
  path: '/dates/{timestamp}',
  handler: (req, reply) => {
    const utcEndpoint = `http://utcdate-service:3001/utcdate/${req.params.timestamp}`
    const isoEndpoint = `http://isodate-service:3000/isodate/${req.params.timestamp}`
    request(utcEndpoint, (err, response, utcBody) => {
      if (err) {
        console.log(err)
        return
      }
      request(isoEndpoint, (err, response, isoBody) => {
      if (err) {
        console.log(err)
        return
      }
      reply({
        utcDate: JSON.parse(utcBody).date,
        isoDate: JSON.parse(isoBody).date,
        raw: req.params.timestamp
       })
     })
   })
  }
})

server.start((err) => {
  if (err) {
    throw err
  }
  console.log('aggregator started on port 8080')
})

在高亮部分,我们向返回对象中添加了一个新部分,基本上返回原始时间戳。现在是提交更改的时候了,但首先,让我们遵循一个好习惯。创建一个分支:

git checkout -b raw-timestap

这将创建一个名为raw-timestamp的本地分支。现在,提交前面代码中所做的更改:

git add . && git commit -m 'added raw timestamp'

并将分支推送到 GitHub:

git push origin raw-timestamp

如果我们现在访问 GitHub 界面,会注意到一些变化:

系统建议我们创建一个拉取请求。基本上,拉取请求是向代码库添加代码的请求。点击“比较并拉取请求”,然后在新表单中添加描述并点击“创建拉取请求”。这就是结果:

这里有三个选项卡:

  • 会话

  • 提交

  • 文件更改

第一个选项卡显示的是参与者的评论列表。第二个选项卡是他们推送到服务器的提交列表,第三个选项卡则是以差异样式显示的更改列表,包含添加和删除的内容,你可以在此处发表评论,要求更改或建议更好的做法。在大型项目中,主分支通常是锁定的,推送代码到主分支的唯一方法是通过拉取请求,以强制执行代码审查。

一旦你满意,点击合并拉取请求并合并代码。这将把更改推送到主分支(需要确认)。

现在我们准备创建一个标签。可以通过 GitHub 界面完成。如果点击发布链接(位于文件列表上方贡献者数量旁边),它会带你到发布页面:

在这里,你可以看到我们之前从终端创建的标签和一个名为“草拟新发布”的按钮。点击它,会显示一个新的表单:

填写细节,如这里所示,并创建发布。这会创建一个与我们在 Google Cloud Platform 中的容器注册表连接的标签,并且到现在(非常快),我们镜像的新版本应该已经可用:

如你所见,我们可以对进入生产环境的内容(包括注册表和集群)有很好的控制。现在唯一剩下的步骤就是将新版本发布到 Kubernetes。返回到名为aggregator-kubernetes的代码库(我们在前面的部分创建了它),并将deployment.yaml文件中镜像的标签从eu.gcr.io/implementing-modern-devops/aggregator-service:1.0改为eu.gcr.io/implementing-modern-devops/aggregator-service:2.0。请注意,项目需要根据你的配置进行调整。

完成后,提交并推送更改(来自aggregator-kubernetes文件夹):

git add . && git commit -m 'version 2.0 of aggregator' && git push origin master

现在一切都准备好了。我们已经站在悬崖边上。如果我们点击在 Jenkins 中创建的作业中的“运行”,新的软件版本将会在 Kubernetes 中部署,并且实现零停机(取决于你的配置,如之前所见);我们掌控着一切。我们可以决定何时发布,并且我们有一个简单的回滚方法:撤销更改并在 Jenkins 中再次点击“运行”。

一旦你对更改感到满意,运行我们在 Jenkins 中创建的作业(在我的案例中是aggregator-kubernetes)。

如果你访问之前相同的 URL(http://104.155.35.237/dates/1111111111),结果应该有一些变化:

{
   utcDate: "2005-03-18T01:58:31",
   isoDate: "2005-03-18T01:58:31.000Z",
   raw: "1111111111"
}

新版本已上线。正如你所想象的,这是一个非常强有力的理由来采纳 DevOps:以最小的努力透明地将软件发布给用户(创建标签并在 Jenkins 中运行任务)。

在下一部分,我们将执行相同的部署,但使用一种称为蓝绿部署的技术,该技术的核心是将新版本以私有模式运行在生产环境中,以便我们在将其提供给公众之前先进行功能测试。

蓝绿部署

为了执行蓝绿部署,首先,我们需要回滚到 1.0 版本。编辑deployment.yaml文件,在aggregator-kubernetes中调整镜像为1.0标签,并将更改推送到 GitHub。完成后,在 Jenkins 中运行名为aggregator-kubernetes的任务,然后,你就完成了回滚到 1.0 版本。保留 1.0 版本的镜像在注册表中,因为我们将要使用它。

蓝绿部署是一种将软件发布到生产环境的技术,它对公众是不可见的,因此我们可以在将其公开给所有人之前进行测试。Kubernetes 让这一过程变得极其简单:我们需要做的唯一事情是复制aggregator-kubernetes中的资源,并为它们分配不同的名称和标签。例如,这是我们的deployment-bluegreen.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: aggregator-bluegreen
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: aggregator-service-2.0
    spec:
      containers:
      - name: aggregator-service
         image: eu.gcr.io/implementing-modern-devops/aggregator-service:2.0
         ports:
           - containerPort: 8080

这是我们的service-bluegreen.yaml

kind: Service
apiVersion: v1
metadata:
  name: aggregator-service-bluegreen
spec:
  ports:
  - port: 80
     targetPort: 8080
  selector:
     app: aggregator-service-2.0
  type: LoadBalancer

如你所见,我们已经创建了一个具有不同选择器/标签的应用程序垂直切片;因此,我们的原始版本仍在运行,但现在我们有了一个新的服务,名为aggregator-service-bluegreen,它通过负载均衡器提供新版本的应用程序,我们可以通过 Kubernetes 界面(如之前所述使用kubectl proxy命令)检查它:

如果你在两个外部端点之间进行切换,你可以看到差异:新版本返回的是原始有效负载以及日期,日期格式为 ISO 格式并且是 UTC 时区(版本 2.0),而旧版本仅返回日期(版本 1.0)。

我们现在处于我们所称的蓝色状态:我们对我们的发布感到满意,并且确信我们的软件在生产配置下能够正常工作,不会影响任何客户。如果有任何问题,客户是不会注意到的。现在是时候进入绿色阶段了。我们在这里有两个选项:

  • 移除aggregator-bluegreen部署及其所有子项(ReplicaSet 和 Pods,以及aggregator-service-bluegreen服务),并升级我们的基础部署(aggregator)。

  • 更改聚合器服务中的选择器标签,并让它指向新的 Pods

通常来说,我是第一个选项的大粉丝,因为它保持了简单性,但这取决于你;同时也是进行实验的好时机。更改服务中的选择器会立即生效,如果你很急的话,这可能是最简单的路线。

在处理复杂系统时,我总是尽量进行蓝绿部署阶段,以减轻团队的压力。想想看:与其认为一切都很稳定,不如实际验证一切是否按预期工作,没有意外发生,这样发布时不再有不确定性的心理负担。

在下一部分,我们将介绍另一种发布类型,它将一个新的 Pod 引入到正在运行的系统中,从而仅将其暴露给一部分用户。如果有什么问题,它不会破坏系统;它只会产生一些错误。在继续之前,请确保将你的集群恢复到原始状态:只保留一个名为 aggregator 的部署和其 Pod(移除蓝绿部署)。

金丝雀部署

关于这种部署类型的名称有一个有趣的故事。在所有气体探测器之前,矿工们通常会带上一只金丝雀(这是一种鸟)进入矿井,因为金丝雀对有害气体极为敏感。所有人都在正常工作,但会时刻关注着这只鸟。如果鸟死了,大家就会立即离开矿井,以避免中毒甚至被杀害。

这正是我们将要做的:推出我们软件的新版本,如果有任何问题,它将产生错误,因此我们只会影响有限数量的客户。

同样,这是通过 YAMl 文件完成的,使用我们服务所针对的选择器,但应用的是我们新版本的应用程序。在继续之前,请确保只存在一个名为 aggregator 的部署,并且有两个 Pod 正在运行我们应用的 1.0 版本(如“常规发布”部分所示)。

现在,在 aggregator-kubernetes 中创建一个文件(放在 objects 文件夹内),并添加以下内容:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: aggregator-canary
spec:
 replicas: 1
  template:
    metadata:
      labels:
 app: aggregator-service
    spec:
      containers:
      - name: aggregator-service
         image: eu.gcr.io/implementing-modern-devops/aggregator-service:2.0
         ports:
         - containerPort: 8080

这里有一个简单的解释:我们正在创建一个新的部署,只有一个 Pod,并且该 Pod 拥有与原始部署的 Pod(aggregator)相同的标签;因此,aggregator-service 将会指向这个 Pod:总共有三个 Pod。

将更改推送到 GitHub 并运行 aggregator-kubernetes 任务,该任务将此配置应用到我们的集群中。现在打开我们之前用于测试的端点,在我的例子中是 http://104.155.35.237/dates/1111111111,并多次刷新该 URL。大约三分之一的请求应该返回原始时间戳(应用的新版本),其余的应该不返回(1.0 版本)。

你可以通过 Kubernetes 仪表板检查 aggregator-service 来验证一切是否正常:

在这里,您可以看到我们的服务针对新创建的 Pod。我通常会保留这种状态几个小时/几天(取决于发布情况),一旦满意,我就会移除canary部署并将配置应用到aggregator部署中。您还可以根据需要调整副本数量,以改变用户获得新版本的百分比,甚至逐渐增加canaries的数量并减少常规 Pod 的数量,直到完全部署应用程序。

这种策略被像 Google 这样的大公司采用,以极大的成功发布新功能。当系统足够大(运行超过10 +个 Pod)时,我非常喜欢将其作为起点,但在小系统中,我会犹豫不决,因为受影响请求的百分比会太高(在前面的例子中为 33.3%)。

总结

本章非常密集:我们建立了一个持续交付(CD)管道,并讨论了最常见的发布策略,使用 Kubernetes 可以轻松实现。除了故意留下的几个检查点外,一切都是自动化的,这样我们就可以控制系统中正在进行的操作(仅为了安心)。这是本书的高潮:尽管示例很基础,但它们为您提供了足够的工具,使您可以在公司建立类似的设置,以便享受与微服务一起工作的好处,同时减少操作开销,并促进新版本的发布。

在下一章中,我们将学习持续交付的一个重要方面:监控。通过正确的监控,我们可以减少发布中的很多压力,使我们的工程师更加自信,能够及早捕捉错误,从而实现更平稳的部署和更低的生产错误计数。

第九章:监控

介绍

到目前为止,我们已经看到了一大批可以作为 DevOps 工程师在公司中使用的工具,来增强我们的能力。现在我们能够使用 Ansible 配置服务器,在谷歌云平台上创建 Kubernetes 集群,并为我们的微服务设置交付流水线。我们也深入探讨了 Docker 的工作原理,以及我们应该如何组织公司,才能成功交付软件。

在这一章中,我们将讨论 缺失的拼图:监控。通常被忽视,但在我看来,监控是成功的 DevOps 公司中一个关键的组成部分。监控是应对问题的第一道防线。在 第八章,发布管理 – 持续交付 中,我们谈到了如何将焦点转向解决出现的问题,而不是花费大量资源去预防它们:

你 20% 的时间将创造出 80% 的功能。剩下的 20% 将花费你 80% 的时间。

这条非书面规则主宰着世界。通过监控,我们可以利用这一规则,生活得更加舒适,因为我们能够快速识别问题,从而应对 20% 的未知结果。

我们将回顾一些用于监控软件的工具,但我们的重点将放在 Stackdriver 上,因为它是谷歌云平台的监控解决方案,开箱即用,提供了一个相当全面的工具集,帮助我们应对系统中的缺陷。

监控类型

在谷歌的 SRE 书中,定义了两种类型的监控:

  • 黑盒监控

  • 白盒监控

这是大家普遍接受的观点,导致出现了大量明显区分的工具,围绕白盒和黑盒监控。

我听过的最好的白盒与黑盒监控的对比之一是骨折诊断。当你第一次去看医生时,他/她只能接触到你的黑盒指标:

  • 这个区域有任何凸起吗?

  • 移动时是否疼痛?

然后,一旦初步诊断已经做出,下一步就是获取该区域的 X 光片。现在我们可以确认骨头是否断了,如果断了,系统的影响是什么。X 光片就是医生使用的白盒监控。

黑盒监控

白盒监控是从外部观察系统的一种监控方式,而不需要了解系统是如何构建的。这些指标是首先影响用户的,亦是应用或服务器出现问题时的第一外部症状。

在可用于黑盒监控的指标中,我们可以找到以下几种:

  • 延迟

  • 吞吐量

这两种指标是黑盒监控的圣杯。

延迟的定义是系统响应所需的时间。如果我们看一个 HTTP 服务器,从我们第一次发送请求到对方服务器回复的时间,就是我们所理解的延迟。这个指标非常有趣,因为它直接反映了用户如何感知我们的系统:延迟越大,用户体验就越差。

吞吐量与延迟密切相关。基本上,它是我们软件每单位时间可以处理的请求数量,通常以秒为单位。这个指标对容量规划至关重要,而且不建议在运行中的系统中实时测量它,因为它会给系统带来很大的负载,肯定会影响实时用户的响应时间。一般来说,吞吐量是在我们应用程序的性能测试阶段进行衡量的,但这可能会有些棘手:

  • 测试硬件必须与生产环境匹配

  • 数据库的数据集必须与生产环境类似

性能测试步骤通常被许多公司忽视,因为它非常昂贵。因此,通常使用预生产环境进行容量测试,以估算生产环境中所需的硬件数量。现在,随着云基础设施中自动扩展组的出现,这个问题变得不那么严重,因为基础设施在需要时会自动扩展。

如你所见,这些指标非常容易理解,尽管它们在错误响应时间中起着关键作用,但它们可能不是问题的首要指标。

白盒监控

白盒监控,顾名思义,是一种需要了解系统构建方式的监控,以便在应用程序或基础设施内部发生某些事件时发出警报。这些指标非常细粒度(与黑盒监控不同),一旦我们收到警报,它们就是事后分析的主要答案:

  • 问题出在哪里?

  • 问题的原因是什么?

  • 哪些流受到了影响?

  • 我们可以做些什么来避免未来出现这种情况?

这些是非常有趣的一组示例,除了其他指标:

  • 函数执行时间

  • 每单位时间的错误数

  • 每单位时间的请求数

  • 内存使用情况

  • CPU 使用率

  • 硬盘使用情况

  • 每单位时间的 I/O 操作

如你所见,为确保系统的稳定性,有无尽的白盒监控指标可供选择。数量几乎太多,所以我们通常需要选择合适的指标,以避免噪音。

这里一个重要的细节是,当黑盒监控指标给出异常读数时,总会有一个白盒监控指标可以用来诊断,但反过来就不一定成立。服务器可能由于内部问题导致内存使用激增,但这不会影响用户。

白盒监控中最重要的一个工件就是日志文件。这些文件是我们软件中发生事件的有序链条,通常,它们是诊断与软件相关问题的第一道防线。日志文件的主要问题在于,它们存储在生产服务器上,我们不应定期访问这些日志文件,因为这本身就是一种安全威胁。只需要一个被遗忘的终端连接到服务器,就可能给不该访问的人提供权限。

监控第三方工具

监控通常是涉及第三方公司的一个不错选择。它需要系统中有相当多的冗余,以保持监控的持续运行,这对确保我们不会对系统发生的情况视而不见至关重要。

使用第三方应用进行监控的另一个积极方面是,它们不在同一数据中心运行,如果它们确实在同一个数据中心(通常是 AWS),它们的冗余性足以确保稳定性。

在本节中,我们将特别关注三种工具:

  • Pingdom

  • Logentries

  • AppDynamics

这并不意味着它们是市场上最好的或唯一的工具。还有其他一些有趣的替代品(例如,使用 New Relic 代替 AppDynamics),值得探索,但在本节中,我们将重点讨论 Stackdriver,这是 Google Cloud Platform 的监控解决方案,原因有以下几点:

  • 它与 Google Cloud Platform 的集成非常好

  • 它提供一个非常有趣的免费套餐

  • 警报系统是市场上最先进的系统之一

Pingdom

Pingdom 是一个用来衡量我们服务器从世界不同地方的延迟的工具。如你所见,如果你曾在一个 24/7 的公司工作过,你会发现全球各地的延迟差异很大,这取决于我们的客户与数据中心之间的距离。有趣的是,如果我们的服务器在欧洲,来自澳大利亚的用户将会多出 2-3 秒的延迟。

Pingdom 在全球多个地方都有服务器,用来监控用户如何看到系统并采取适当的措施解决问题(例如,建立一个离他们更近的数据中心)。

你可以免费注册 Pingdom,享受 14 天的试用期,但需要提供信用卡信息(不用担心;他们会在试用期结束时提醒你,这样你可以在不继续使用的情况下取消计划)。

请看以下截图:

如你所见,在指定主机之后,Pingdom 将开始向指定的 URL 发出请求,并测量从世界不同地方的响应时间。

最近,Pingdom 加入了一些相当有趣的功能:现在它可以通过端点读取自定义指标,从而监控大量数据:

  • 磁盘上的空闲空间

  • 已使用的 RAM 量

  • 库存水平(是的,您可以将仓库中剩余的任何商品数量发送到 Pingdom)

总体来说,我过去使用 Pingdom 成功地测量了服务器的延迟,并通过在全球范围内战略性地分布数据中心来改善用户体验,缓解了这个问题。Pingdom(以及类似工具)能给您的一些最有趣的见解是,您的网站可能由于互联网网络分裂或某些 DNS 服务器的故障而无法访问(在后一种情况下,网站并未真正宕机,但 Pingdom 和用户将无法访问它)。

Logentries

Logentries 是一家让您在处理大量日志时轻松许多的公司。它基本上解决了一个困扰了几年的问题:它将来自系统的所有日志聚合到一个公共位置,提供访问控制,并且拥有一个非常不错的界面,允许您在大型数据集中快速搜索。

创建账户是免费的,并且提供 30 天的使用期限,带有一些足够进行测试和评估的限制。

访问 logentries.com/ 并创建一个账户。登录后,首个屏幕应与以下截图相似:

如您所见,关于如何配置日志聚合的平台有无尽的解释:您可以从系统到库,经过多个平台(如 AWS、Docker 等)进行监控。

代理通常是一个不错的选择,原因有二:

  • 它们不会在您的应用程序中创建耦合(代理读取日志文件并将其发送到 Logentries 服务器)

  • 它们将复杂性推给了第三方软件

但也有其他有趣的选择,例如手动日志聚合。在本例中,我们将演示如何使用自定义日志记录器将日志从一个非常简单的 Node.js 应用程序发送到 Logentries。创建一个名为 logentries 的文件夹并执行以下命令:

npm init

这假设您的系统已经安装了 Node.js,因此如果没有安装,请从nodejs.org/en/下载任何版本的 Node.js 并安装。

现在我们需要为 Node.js 安装 Logentries 库。Logentries 支持多种平台,但特别支持 Node.js。执行以下命令:

npm install --save le_node

完成后,我们应该已安装所需的库。现在是创建一个简单的 Node.js 程序以演示其工作原理的时候了,但首先,我们需要创建一个服务令牌。在接下来的屏幕上,点击“手动”并填写表单,如下所示:

Logentries 能理解多种不同类型的日志,但它在 JSON 日志方面表现尤为出色。我们不需要为它捕捉日志指定任何日志类型,所以将此选项留空,并为日志和集合命名。点击创建日志令牌后,令牌应该会显示在按钮下方。保存起来,稍后会用到。

现在,如果我们进入主仪表盘,我们应该能看到名为 Testing Set 的日志集:

现在是时候发送一些数据了:

const Logger = require('le_node')
const logger = new Logger({token: '5bffdd28-fb7d-46b6-857a-c3a7dfed5410'})

logger.info('this is a test message')
logger.err('this is an error message')
logger.log('debug', {message: 'This is a json debug message', json: true})

这个脚本足以将数据发送到 Logentries。请注意,指定的 token 必须替换为在前一步中获取的 token。将其保存为index.js并执行几次:

node index.js

执行几次后,返回 Logentries,打开 Testing Set 中的 Test log:

现在你可以看到我们在 Logentries 中的日志被聚合了。Logentries 中有一些随着时间不断改进的有趣功能:

  • 界面非常简洁

  • 搜索机制非常强大

  • Logentries 能够实时流式传输日志(或多或少)

关于搜索机制,Logentries 开发了一种名为LEQL的工具,这基本上是 Logentries 为了通过 JSON 字段或纯文本搜索某些事件而设计的一种语言。你可以在docs.logentries.com/v1.0/docs/search/找到更多信息。

另一个有趣的功能是实时跟踪日志。让我们来测试一下这个功能。创建一个名为livetail.js的新文件,并添加以下代码:

const Logger = require('le_node')

const logger = new Logger({token: '5bffdd28-fb7d-46b6-857a-b3a7dfed5410'})

setInterval(() => {
 logger.info(`This is my timed log on ${Date.now()}`)
}, 500)

这个不需要解释:一个每 500 毫秒执行一次的函数,并将一行日志发送到 Logentries。

执行脚本:

node livetail.js

看起来似乎什么都没发生,但实际上事情正在发生。返回 Logentries 并点击“开始实时跟踪”按钮:

几秒钟后(或更少),日志开始流动。这可以在 Logentries 中存储的任何日志文件上完成,而且这是一个非常有趣的机制,用于调试我们服务器中的问题。

Logentries 还能够向某个邮箱发送警报。你可以配置它向你的团队发送以下警报:

  • 异常

  • 模式

  • 缺少日志

  • 增加的活动

这个警报通常是系统出现问题的第一个指示器,因此,如果你希望对错误做出早期响应,最佳实践是尽量减少噪声,直到警报不会被遗漏,并将假阳性减少到最低限度。

AppDynamics

AppDynamics 曾一度是监控领域的王者(因为它是唯一真正可用的选项)。它是一款非常精心设计的软件,允许你探索软件和服务器的运行情况:异常、单位时间内的请求数、CPU 使用率等是 AppDynamics 能为我们捕捉的众多指标之一。

它还捕捉与第三方端点的交互:如果我们的软件正在使用第三方 API,AppDynamics 将会知道并在类似于下方截图的仪表板中显示这些调用:

AppDynamics 在主动措施方面相当先进。其中一项措施是自动化操作,例如在某些事件发生时重启 Tomcat 服务器或重新加载服务器上运行的服务。例如,如果我们部署了一个新的 Java 应用版本,并且它在 PermGen 空间方面出现问题(在 Java 8+ 中不再是这个问题),这个问题通常非常难以修复,因为它源自 JVM 加载的许多类,并且只有在部署后几个小时才会显现出来。在某些情况下,我们可以指示 AppDynamics 在使用量达到分配总量的 80% 及以上时重新启动应用程序,这样我们就不会完全崩溃并无法为任何客户提供服务,而是每隔几个小时就会有少数掉线,借助气球动作来修复问题。

AppDynamics 采用所谓的代理模型。一个应用程序需要安装在你的服务器上(例如,Tomcat),以便收集指标并将其发送到集中式服务器进行处理,生成相关的仪表板并触发工作流。AppDynamics 的有趣之处在于,如果你不愿意将数据发送给第三方(这通常是处理高敏感数据的公司所要求的安全性),它们提供了一个本地部署版本的仪表板。

Stackdriver

到目前为止,我们已经查看了来自不同第三方的一组工具,但现在我们将来看看 Stackdriver。Stackdriver 是一个云监控解决方案,已被 Google 收购并(未完全)集成到 Google Cloud Platform 中。这对于 GCP 来说是一个重要的步骤,因为提供集成的监控解决方案在如今几乎是必须的。

使用 Stackdriver,我们不仅能够监控应用程序,还能监控 Kubernetes 集群甚至独立的虚拟机。正如我们将看到的那样,集成还不像我们希望的那样无缝(可能在你阅读这篇文章时已经完成),但它足够好,可以被认为是市场中的重要玩家。

监控应用程序

Stackdriver 可以通过捕获指标和日志来监控独立应用程序。它支持主要平台和库,因此我们的技术选择不必担心。在这种情况下,我们将创建一个 Node.js 应用程序,原因有几个:

  • 它易于理解

  • 官方示例在 Node.js 版本上有很好的文档记录。

  • Node.js 正在逐渐成为企业和初创公司中的重要平台。

我们需要做的第一件事是编写一个小的 Node.js 应用程序。创建一个新的文件夹并执行以下命令:

npm init

按照屏幕上的指示操作,你现在应该已经在刚创建的文件夹中得到了 package.json。现在是时候安装依赖项了:

npm install --save @google-cloud/logging-bunyan @google-cloud/trace-agent express bunyan

我们将使用四个库:

  • express:用于处理 HTTP 请求

  • bunyan:用于记录我们应用程序的活动

来自 Google 的两个库用于与 Stackdriver 交互:

  • logging-bunyan:这个库将把 bunyan 的日志发送到 Stackdriver

  • trace-agent:这个库将追踪请求通过我们的应用程序

现在让我们创建一个简单的应用程序:

require('@google-cloud/trace-agent').start()
const express = require('express')
const bunyan = require('bunyan')
const LoggingBunyan = require('@google-cloud/logging-bunyan')
const loggingBunyan = LoggingBunyan()

const log = bunyan.createLogger({
 name: "stackdriver",
 streams: [
 {stream: process.stdout},
 loggingBunyan.stream()
 ],
 level: 'info'
})

const app = express()

app.get('/', (req, res) => {
 log.info(`request from ${req.connection.remoteAddress}`)
 res.send('Hello World!')
})

app.listen(3000, () => {
 console.log('Listening in port 3000')
})

现在是时候解释代码中有趣的部分了:

  • 第一行启用了 Stackdriver 的追踪功能。非常重要的是,这一行必须在其他任何操作之前执行,否则追踪将无法工作。我们将看到追踪功能的强大之处。

  • 为了让 Stackdriver 收集日志,我们需要向 bunyan 日志记录器添加一个流,代码中已展示。

其他一切都很正常:一个 Express.js Node.js 应用程序,处理 URL 请求并返回经典的 Hello World。

还有一个缺失的东西:没有用于访问远程 API 的凭证。这样做是故意的,因为 Google Cloud Platform 有一个非常复杂的凭证管理系统:基本上,它会为你处理这些。

现在,是时候部署我们的应用程序了。首先,在 Google Cloud Platform 创建一个虚拟机,就像我们在前面的章节中看到的几次一样。选择一个小型虚拟机即可,但要确保允许 HTTP 流量。Debian Stretch 是一个不错的操作系统选择。

一旦机器启动,安装 Node.js,如nodejs.org所示。

现在我们需要将代码复制到新创建的机器中。最好的解决方案是创建一个 GitHub 仓库,或者使用我的:github.com/dgonzalez/stackdriver

通过在我们的虚拟机中克隆它(别忘了先通过 apt 安装 Git),我们只需要使用以下命令来安装依赖项:

npm install

我们已经准备好开始了。只需运行以下命令启动应用程序:

node index.js

现在转到机器的外部 IP(在 Google Compute Engine 中显示)并访问 3000 端口。在我的案例中,这是 35.195.151.10:3000/

一旦我们完成,我们应该能在浏览器中看到 Hello World,并在我们的应用日志中看到类似以下内容:

Listening in port 3000
{"name":"stackdriver","hostname":"instance-3","pid":4722,"level":30,"msg":"request from ::ffff:46.7.23.229","time":"2017-09-18T01:50:41.483Z","v":0}

如果没有错误,那么一切正常。为了验证这一点,访问 console.cloud.google.com 并打开 Stackdriver 的日志部分。

Stackdriver 是一个与 Google Cloud Platform 不同的系统;它可能会要求你使用 Google 账户登录。

一旦你到达那里,你应该看到类似于以下截图的内容:

请注意,你必须选择日志部分,在我的情况下是 GCE VM 实例,Instance-3。

这正是从你的应用程序上传到 Google Cloud Platform 的日志,包含了一些非常有趣的信息。你可以尝试使用不同的处理程序来处理其他 URL 和不同的日志事件,但结果是一样的:所有日志都会聚合到这里。

现在,我们也可以使用 Trace 来实现这一点。打开 Google Cloud Platform 中 Stackdriver 下的 trace 部分。

屏幕应类似于以下截图所示(选择 trace 列表选项):

如你所见,提供了很多有用的信息:

  • 调用栈

  • 持续时间

  • 启始和结束时间

你也可以自己玩一玩,发出多个调用,熟悉它是如何工作的。现在,我们将修改我们的程序,调用第三方 API,看看 Stackdriver 如何追踪它:

require('@google-cloud/trace-agent').start()

const express = require('express')
const bunyan = require('bunyan')
const LoggingBunyan = require('@google-cloud/logging-bunyan')
const request = require('request')

const loggingBunyan = LoggingBunyan()

const URL = "https://www.googleapis.com/discovery/v1/apis"

const log = bunyan.createLogger({
 name: "stackdriver",
 streams: [
 {stream: process.stdout},
 loggingBunyan.stream()
 ],
 level: 'info'
})

const app = express()

app.get('/', (req, res) => {
 log.info(`request from ${req.connection.remoteAddress}`)
 res.send('Hello World!')
})
app.get('/discovery', (req, res) => {
 request(URL, (error, response, body) => {
   return res.send(body)
 })
})
app.listen(3000, () => {
 console.log('Listening in port 3000')
})

现在,我们通过执行 HTTP GET 请求访问www.googleapis.com/discovery/v1/apis URL,列出了所有可用的 Google API。将其重新部署到 Google Cloud Platform 上的虚拟机中,然后访问虚拟机的 endpoint/discovery。一个大的 JSON 负载将会显示在你的屏幕上,但真正有趣的部分发生在后台。返回 Stackdriver 的 Trace 列表部分,你会看到一个新的 trace 正在被捕捉:

在这里,你可以看到我们的程序如何联系远程 API,并且它花费了 68 秒才得到回复。

实时获取这种信息非常强大——如果客户响应时间非常长,我们可以几乎实时地看到应用程序内部发生了什么。

监控 Kubernetes 集群

Kubernetes 覆盖了任何软件公司 99%的需求,但它在嵌入式监控方面并不十分出色,因此留给了第三方来填补这一空白。Kubernetes 的主要问题源于 Docker:容器是临时性的,因此常见的做法是将日志转储到标准输出/错误中,并使用syslogd将它们收集到一个集中位置。

使用 Kubernetes,我们遇到了一个额外的问题:Docker 上的调度器需要知道如何获取日志,以便通过 API 或仪表板将其提供,使用户在需要时能够访问它们。但是,随之而来的是另一个问题。通常,日志会根据时间进行轮换并归档,以避免日志溢出,这可能会消耗我们服务器上的所有可用空间,导致应用程序(以及操作系统)无法正常运行。

最佳解决方案是使用外部系统在集群内聚合日志和事件,从而将复杂性转移到一旁,让 Kubernetes 专注于它最擅长的事情:调度容器。

在这种情况下,为了将我们的集群与 Google Cloud Platform 中的 Stackdriver 集成,我们需要做的唯一事情就是在集群创建屏幕上勾选两个复选框:

这将启用跨我们集群中不同节点的监控,并改善我们处理应用程序中出现问题的方式。点击“创建”,允许集群被配置(可能需要几秒钟或几分钟的时间)。集群不需要很大;只需使用小型机器,两个虚拟机就足够了。公平地说,我们可能需要在负载测试期间减少规模,以加速警报部分的处理。

正如我们在前一部分看到的那样,启用 GKE 监控后,Kubernetes 还将日志发送到 Stackdriver 的日志功能,因此您无需连接到节点来获取日志。

一旦创建完成,我们将从 Stackdriver 添加监控。我们需要做的第一件事是打开 Google Cloud Platform 中 Stackdriver 部分的监控功能。这将打开一个新页面,原始的 Stackdriver 页面,外观与以下截图所示的非常相似:

尽管一开始用户界面可能有些令人困惑,但经过一段时间的使用后,我们会发现,Stackdriver 提供的大量功能很难在更好的 UI 中打包展示。默认情况下,我们可以看到一些关于集群的指标(图像右下角部分),但它们并不是非常有用:我们不希望有人整天盯着这些指标来触发警报。让我们来自动化它。我们需要做的第一件事是创建一个组。组基本上是一起工作的资源集合,在这种情况下,就是我们的集群。点击“组”并创建一个新组:

默认情况下,Stackdriver 会为您提供分组建议。在此情况下,在建议的组部分,我们可以看到 Stackdriver 已经建议了我们的集群。虽然可以添加更复杂的标准,但在这种情况下,匹配我们机器名称的开头就足够了,因为 GKE 会根据某些标准命名机器,集群名称会出现在名称的开头。

创建一个组(命名为 GKE,这是默认建议的名称)。创建组后,您可以导航到该组并查看不同的指标,如 CPU,或者甚至配置它们,添加其他如磁盘 I/O 等指标。熟悉一下仪表板:

在我的情况下,我添加了一个用于捕获集群中已用内存的指标。尽管这些可用数据非常有趣,但还有一个更强大的工具:警报策略。

警报策略是我们应该得到警报的标准:高内存使用率、低磁盘空间或高 CPU 利用率等事件,是我们希望了解的,以便尽快采取行动。警报策略的优点是,如果我们正确配置它们,我们就进入我所称的自动驾驶模式:除非收到警报,否则我们无需担心系统的性能,这大大减少了操作系统所需的人员数量。

点击“创建警报策略”按钮,来创建一个警报策略:

如前屏幕所示,我们需要选择一个度量标准。我们将使用“度量阈值”类型,因为它包含在 Stackdriver 的基础包中,所以我们不需要升级到 Premium 订阅。如果我们集群中的任何成员的 CPU 使用率超过 30%,且持续超过一分钟,就会触发警报。

下一步是配置通知。在基础包中,仅包含电子邮件功能,但它足以用来测试系统的运行情况。Stackdriver 还允许你在通知中包含文本。只需在电子邮件旁边输入类似Test alert的内容,并保存你选择名称的警报策略。

正如你所看到的,在 Stackdriver 中创建警报非常简单。这是一个非常简化的示例,但一旦你设置好了集群,下一步是设置一组正常操作的度量标准,并在其中任何一个被违反时接收警报。

现在是时候触发警报,看看会发生什么。为此,我们需要通过多个相同镜像的副本来过载集群,我们将使用 Apache Benchmark 工具给系统施加负载:

kubectl run my-nginx --image=nginx --replicas=7 --port=80

现在暴露名为my-nginx的部署:

kubectl expose my-nginx --type=LoadBalancer

请注意,你首先需要配置kubectl指向你的集群,正如我们在之前的章节中看到的那样。

nginx 部署完成后,接下来就该对其进行压力测试了:

ab -k -c 350 -n 4000000 http://130.211.65.42/

ab工具是一个非常强大的基准测试工具,叫做 Apache Benchmark。我们将创建 350 个并发消费者,它们将发出 400 万个请求。可能需要减少集群的规模来施加压力:如果在基准测试运行时减少规模,Kubernetes 将需要重新调度容器以重新组织资源,这将增加系统的负载。

我建议你进一步探索 Apache Benchmark 工具,因为它对于负载测试非常有用。

一旦任何节点的 CPU 使用超过阈值超过一分钟,我们应该通过电子邮件收到警报,并且它会显示在 Stackdriver 界面中:

在这个案例中,我收到了两个警报,因为两个节点超出了限制。这些警报遵循一个工作流。如果规则仍然被违反,你可以确认它们,一旦确认并且条件消失,它们就会变为已解决状态。在这种情况下,如果你停止 Apache Benchmark 并确认已触发的警报,它们会直接进入已解决状态。

在高级版本中,有更多高级策略,如 Slack 消息或短信,这使得你的团队能够设置一个轮班表来确认事件并管理相关操作。

总结

这是本书的最后一章。在整本书中,我们探讨了 DevOps 工程师使用的最重要的工具,重点介绍了 Google 云平台。在本章中,我们实验了我认为对任何系统都非常重要的一个方面:监控。我认为监控是应对已经影响到生产系统的问题的最佳方法,无论你投入多少努力,这些问题最终都会发生。

接下来是什么?

本书并没有对任何主题进行特别深入的探讨。这样做是有意为之。本书旨在播下大树的种子:DevOps 文化,目的是希望你能获得足够的信息来继续拓展对 DevOps 工具和流程的了解。

保持与最新工具同步是一项全职工作,但如果你想站在技术潮流的前沿,这非常必要。我认为我们很幸运能够参与 DevOps 文化的崛起与辉煌,未来一片光明。自动化和更好的编程语言(如 Golang、Kotlin、Node.js 等)将使我们减少人工干预,提升系统的整体弹性。

如果你回顾五年前并与今天市场上的情况进行对比,你能想象我们在 15 年后的工作会是怎样的吗?

如果你想跟进任何问题或查看我现在正在做的事情,你可以随时通过 LinkedIn 与我联系:

posted @ 2025-06-29 10:40  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报