Docker-开发者指南-全-
Docker 开发者指南(全)
原文:
annas-archive.org/md5/2786fedb9dab148e2fe09b00520804b3
译者:飞龙
序言
软件工程团队正在迅速采用容器来打包和部署他们的软件。容器提供了一个平台无关的体验,使你能够在各种操作系统镜像下运行应用,并且可以在本地部署、数据中心以及云端部署。为了支持基于容器的应用,供应商们开发了各种各样的工具,从 Docker 和 Google 的 Kubernetes 项目,到 Lyft 的 Envoy 服务网格和 Netflix 的 Spinnaker。不论你是在软件开发、托管和基础设施方面工作,还是在构建 DevOps 流水线,你都需要广泛且深入地理解许多概念,才能有效地管理基于容器的环境。
在 Docker for Developers 中,我们将从 Docker 本地开发容器基础开始,然后转向使用 AWS 部署生产级、云托管系统。如果你有兴趣了解容器编排、部署、监控和安全性等内容,那么我们认为你会喜欢本书。
本书适合谁阅读
Docker for Developers 面向那些希望学习容器基础并在此基础上进一步了解如何在生产环境中使用容器的工程师和 DevOps 人员。本书通过一系列逐步更复杂的部署,展示了 Docker 应用如何通过 CI/CD 流水线进行部署,并在生产级别、云托管环境中进行管理。在处理本书的主题时,具备基本的容器知识将有所帮助,但并非必要。假定本书的读者熟悉 Linux、命令行工具的使用,以及基本的软件工程概念,如版本控制和 Git 的使用。
本书内容简介
第一章,Docker 简介,介绍了 Docker 的一些背景知识,容器及其目的的概述,并为读者提供了本书将讨论的主题的简介。
第二章,使用 VirtualBox 和 Docker 容器进行开发,引导读者通过本地虚拟机进行开发,并与使用 Docker 进行容器化开发项目进行比较。
第三章,通过 Docker Hub 分享容器,向读者介绍 Docker Hub 和预构建容器。接下来,我们将探讨构建专业容器的过程。
第四章,使用容器构建系统,探讨了更复杂的情况,其中多个容器需要共同工作,形成一个完整的系统。此外,我们还将为读者提供 Docker Compose 的概述。
第五章,在生产环境中部署和运行容器的替代方案,帮助读者理解在生产环境中运行容器的选择范围,包括云选项、本地部署和混合解决方案。
第六章,使用 Docker Compose 部署应用程序,讨论了如何在单个主机上使用 Docker Compose 部署生产应用程序,并处理日志记录和监控,同时分析这种简单配置的优缺点。
第七章,使用 Jenkins 进行持续部署,展示了如何使用 Jenkins 进行持续集成(CI)和持续部署(CD)容器,使用 Jenkinsfile 和多个开发分支。
第十八章,将 Docker 应用部署到 Kubernetes,探讨了 Kubernetes 的概念、云分发选项,并展示了如何创建 Amazon Web Services 弹性 Kubernetes 服务(EKS)集群,将 Docker 应用程序部署到 Kubernetes。
第九章,使用 Spinnaker 进行云原生持续部署,在 CI/CD 的基础上,通过将 Netflix 的 Spinnaker 与 Kubernetes 集成并查看自动化测试,进一步发展了相关技能。
第十章,使用 Prometheus、Grafana 和 Jaeger 监控 Docker,解释了如何使用 AWS CloudWatch、Prometheus 和 Grafana 监控基于容器的应用程序。我们介绍了 OpenTracing API,并使用 Jaeger 实现它。
第十一章,扩展和负载测试 Docker 应用程序,探讨了如何通过 Kubernetes 扩展基于 Docker 的应用程序。它介绍了服务网格的概念,并展示了如何使用 Envoy 实现一个简单的服务网格,集成负载均衡、高级流量路由和过滤,包括使用断路器模式。最后,我们展示了如何使用 k6.io 进行负载测试,以证明我们的应用程序能够扩展。
第十二章,容器安全简介,引导读者了解基本的容器安全概念,包括虚拟化和虚拟机监控程序安全模型的工作原理。
第十三章,Docker 安全基础和最佳实践,在上一章的基础上深入探讨 Docker 和安全组件。包括对 Docker 命令及其安全影响的比较。
第十四章,高级 Docker 安全——秘密、秘密命令、标签和标识,涵盖了密码等秘密主题,以及如何在基于容器的环境中安全使用它们。读者还将了解标签和标识的最佳实践。
第十五章,扫描、监控和使用第三方工具,在从其他章节获得的日志记录和监控技能基础上,重新聚焦于这些元素的安全性。在这里,我们还讨论了 AWS、Azure 和 GCP 的用户可以使用哪些选项,以及如何使用 Anchore 扫描容器中的安全问题。
第十六章,结论——旅程的结束,但不是终点,通过回顾我们到目前为止学到的内容来结束本书。最后,我们提供了一些建议,帮助读者进一步探索基于容器的项目。内容包括将 Netflix Chaos Monkey 添加到 CI/CD 流水线中,或在容器中运行 Metasploit。
要充分利用本书
你需要一台能够运行 Docker 的 Windows、Mac 或 Linux 工作站。如果可能,应该使用最新版本。此外,为了完成任何基于云的项目,你需要设置一个云服务提供商的账户。本书示例使用的是亚马逊云服务(AWS),虽然你也可以将许多内容适配到其他云提供商提供的服务:
虽然我们没有明确演示如何将本书中列出的项目部署到 Microsoft Azure 或 Google Cloud Platform,但如果你希望探索这些云平台上可用的某些安全功能,或者尝试在其中运行现有项目,你需要为每个提供商创建一个账户。
如果你使用的是本书的数字版本,我们建议你自己输入代码,或通过 GitHub 仓库(下一节提供链接)访问代码。这样做将帮助你避免与复制粘贴代码相关的潜在错误。
下载示例代码文件
你可以从你的账户在www.packt.com下载本书的示例代码文件。如果你是从其他地方购买的本书,你可以访问www.packtpub.com/support并注册,文件将直接发送到你的邮箱。
你可以按照以下步骤下载代码文件:
-
登录或注册www.packt.com。
-
选择支持标签。
-
点击代码下载。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载完成后,请确保使用以下最新版本的软件解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,链接为 github.com/PacktPublishing/Docker-for-Developers
。如果代码有更新,更新会直接发布到现有的 GitHub 仓库中。
我们还提供其他代码包,来自我们丰富的书籍和视频目录,网址为 github.com/PacktPublishing/
。快来看看吧!
Code in Action
本书的“Code in Action”视频可以在 bit.ly/3kDmrtq
上观看。
下载彩色图片
我们还提供了包含本书所用截图/图表彩色图片的 PDF 文件。你可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789536058_ColorImages.pdf
。
使用的约定
本书中使用了多种文本约定。
文本中的代码
:表示文本中的代码词汇、容器名称、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。这里有一个例子:“这个文件需要添加到主机的conf.d
目录中。”
一块代码或 Dockerfile
设置如下:
FROM ubuntu:bionic
RUN apt-get -qq update && \
apt-get -qq install -y nodejs npm > /dev/null
RUN mkdir -p /app/public /app/server
COPY src/package.json* /app
WORKDIR /app
RUN npm -s install
当我们希望你注意代码块中特定部分时,相关行或项会以粗体显示:
FROM alpine:20191114
RUN apk update && \
apk add nodejs nodejs-npm
RUN addgroup -S app && adduser -S -G app app
RUN mkdir -p /app/public /app/server
ADD src/package.json* /app/
任何命令行输入或输出都以以下方式编写:
$ cp docker_daemon.yaml /path/to/conf.d/
$ vim /path/to/conf.d/conf.yaml
粗体:表示新术语、重要词汇或屏幕上看到的文字。例如,菜单或对话框中的文字会以这样的方式出现在文本中。这里有一个例子:“你可以通过点击立即获取按钮来实现此操作,按钮位于 Azure Marketplace 网站上。”
提示或重要说明
以这种方式显示。
联系我们
我们欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并发送邮件至 customercare@packtpub.com。
勘误表:尽管我们已尽最大努力确保内容的准确性,但难免会出现错误。如果你在本书中发现错误,感谢你向我们报告。请访问 www.packtpub.com/support/errata,选择你的书籍,点击“勘误表提交表单”链接并填写相关信息。
盗版:如果你在互联网上发现任何我们作品的非法复制品,感谢你提供该地址或网站名称。请通过 copyright@packt.com 联系我们,并附上相关内容的链接。
如果你有兴趣成为作者:如果你在某个领域具有专业知识,并且有意撰写或参与撰写书籍,请访问 authors.packtpub.com。
评论
请留下评论。在您阅读并使用本书之后,为什么不在您购买本书的网站上留下评论呢?潜在的读者可以看到并参考您的公正意见来做出购买决策,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packt.com。
第一部分:Docker 简介 – 容器与本地开发
本节介绍了使用 Docker 容器开发应用程序所涉及的技术、技能和步骤。我们首先回顾了托管的历史以及为何我们需要 Docker。接着,我们演示了虚拟化和容器化之间的差异,使用 VirtualBox 创建简单的虚拟机,并利用 Docker 创建一个简单的具有状态的 PHP 应用程序。我们讨论了涉及多个容器协同工作的应用程序(微服务)是使用容器的最终方式,并展示了一个包含多个容器的简单 CRUD 示例,其中一些容器由第三方提供,并通过 Docker Hub 与我们共享。最后,我们介绍了 Docker Compose 工具,作为编排由多个容器组成的完整应用程序的手段,同时提供容器间的私密访问。
本节包含以下章节:
-
第一章**,Docker 介绍
-
第二章**,使用 VirtualBox 和 Docker 容器进行开发
-
第三章**,通过 Docker Hub 分享容器
-
第四章**,使用容器构建系统
第一章:Docker 简介
Docker 是一种技术,允许将整个应用程序及其环境封装在各个容器中。当在一台机器上运行这些容器的多个版本时,它们彼此之间被沙盒化,就像在各自的专用机器上运行一样。
Docker 是开源的,非常适合在容器中运行 Linux,并且有许多开源组件可以帮助构建复杂的系统。它是过去十年甚至更长时间用于托管和后端开发技术的逻辑发展。这一发展经历了从物理托管到逻辑托管的转变,并受到多个需求的驱动。这些需求包括可靠性、可达性、可扩展性和安全性。
本书分为三个部分。第一部分是 Docker 的介绍,重点介绍本地开发。第二部分描述了测试、部署和扩展应用程序的方法论。第三部分详细讨论了使用基于容器的设计时的安全性。
在本章中,我们将回顾托管和后端解决方案的历史,重点讲解 Docker 如何成为广泛使用的技术。
本章将涵盖以下主题:
-
托管服务的起源
-
托管服务类型 – 共置托管
-
托管服务类型 – 自托管
-
数据中心的优势
-
虚拟化是如何工作的
-
数据中心的电力需求
-
虚拟化如何成为数据中心的解决方案以及云计算的发明
-
容器如何为数据中心和托管服务带来更大的优势
Docker 的驱动因素
托管服务的种类最初仅限于自托管服务器、共置服务器托管和共享托管。1994 年和 1995 年,Best Internet Communications 从零起步,在一对 Pentium 服务器上托管超过 18,000 个网站,这些服务器当时是最强大的服务器。Best 还通过共置提供了专用服务器托管、专用宽带连接以及高端优质服务。
Best 托管的大多数网站都是共享托管类型。这些网站共享相同的服务器、相同的硬盘、相同的文件系统、相同的内存、相同的 CPU、相同的网络连接等。
对于这些网站中的任何一个网站被 Slashdotted(即某个非常流行的网站链接到它)并不罕见。这会导致其中一个大约 18,000 个网站的流量激增,而其他网站的性能受到影响。随着网站质量的提高并且对资源的需求增加,它们的管理员通常会转向专用的共置托管或自托管。
共置托管
在共置托管中,客户租用一个位于更大托管设施(数据中心)内的安全笼子:
图 1.1 – 典型的服务器机架,常见于共置托管
客户可以安装和管理自己选择的机器。一些联合托管设施提供额外收费的远程操作服务,客户可以拨打托管公司电话,由公司的工程师执行客户要求的操作。机架被锁定,防止其他客户访问其他客户的设备。
自托管
通过自托管,客户可以在自己选择的物理位置购买一个全时专用的宽带风格连接:
图 1.2 – 印度铁路 139 服务器机房(自托管)
客户最终建立了自己的一种数据中心,并在本地安装和管理服务器及其他设备。
数据中心
专业数据中心的好处是多方面的,最终的趋势是,所有拥有互联网存在的公司中,只有少数几家公司提供数据中心服务,剩下的公司则支付租金来使用专用、共享或高端托管服务。专业数据中心提供丰富的互联网连接(多个提供商、更快的连接),清洁电力,提供 24/7/365 全天候运行的电池备份电力,在长时间停电或断电的情况下提供备用发电机电力,灭火系统,适合设备正常工作温度的控制气候,多个物理位置,专业管理的网络操作中心(NOC)和技术支持,以及通过保安、摄像头、指纹、手印和/或视网膜扫描仪提供的安全保障:
图 1.3 – 欧洲核子研究中心(瑞士)服务器机房
最终建造和运营大多数数据中心的公司包括 Google(Google Cloud Platform)、Microsoft(Azure)、Amazon(Amazon Web Services(AWS))、Yahoo!(曾经)以及一些较小的参与者,包括精品托管公司、区域托管公司和需要比托管公司提供更高安全性的公司(例如银行、金融机构、政府等)。
亚马逊对数据中心有独特的需求。作为全球最大的在线零售商之一,同时也是全球最大的 数据中心开发商/所有者,它们所需的服务器数量、正常运行时间、安全性和覆盖范围促使它们在全国范围内以及全球范围内建设数据中心。
Google 对数据中心也有独特的需求。作为世界上最大的搜索引擎和广告公司,为了保持可访问性,Google 需要在尽可能多的物理位置部署服务器。为了保持高速,Google 需要大量服务器——至少在每个地理位置都有足够的服务器用于分布式搜索索引处理。
像 RackSpace 和 Level 3 这样的公司最初是作为数据中心提供商建立的。它们的专长包括共同托管设施、专用服务器托管、远程服务、网络运营中心(NOCs)、全国性专用光纤骨干网、干净和抗停电电力以及与 AT&T、Verizon 和 Comcast 等其他网络的高度连接。它们发现自己拥有足够的基础设施来跟随虚拟化趋势,并开始提供这些云服务。
提供数据中心服务的最高成本,也传递给客户,最初是带宽。提供商按兆比特支付带宽费用,再加上每月维护物理连接的成本。随着提供商在全球范围内建立自己的私有基础设施来传输数据,带宽费用变成了一种固定费用,对总带宽使用量的大部分提供了优惠。这使得带宽的价格降低到了对托管而言变得微不足道的程度。
这些公司最终建立了一个专门提供专用托管服务的综合基础设施。事实证明,这种基础设施也非常适合虚拟化产品的提供。
使用虚拟化来节约资源使用
虚拟化是将物理机的一部分作为逻辑或虚拟机暴露出来的过程,其行为类似于真实机器,支持安装整个操作系统、文件系统以及运行在操作系统上的软件。例如,一台拥有 64 GB RAM 和 4 个 CPU 的机器可以运行虚拟化软件,伪装成四台各有 16 GB RAM 和 1 个 CPU 的机器。这台机器可以运行四个 Linux 实例。
虚拟化并非一个新概念,IBM 在 1960 年代早期就已实施。它很可能在 1980 年代普及,当时它被用于运行 MS-DOS,后来是 Windows,如原始的 Apple Macintosh (Mac)和 Unix 计算机,如 Sun 和 Silicon Graphics 工作站。
最初的虚拟化软件利用当时 CPU 的可用特性,但通常仅在专业 Unix 工作站的 68000 系列或自定义 CPU 上模拟了 x86 的指令集。SoftPC 是 1980 年代最受欢迎的产品之一。
SoftPC 运行速度相当慢,但能够在 Mac 电脑上运行 Windows 或 MS-DOS 应用程序,使得这些机器在商业和教育环境中得以应用。用户可以运行 Microsoft Office,而不是将所有 Mac 上的程序添加 Microsoft Office 兼容性,以支持 Windows/MS-DOS 用户和 Mac 用户之间的文件交换。
人们看到了它的实际效果,并意识到它的价值。Windows 是家庭和商务中的主流操作系统,为了适应 Windows 在企业环境中的使用,需要类似 SoftPC 的东西。SoftPC 的问题在于它是纯软件模拟,在实际使用中速度非常慢。虚拟化在性能上优于模拟!
整个公司围绕提供消费级或商业虚拟化解决方案而成立。VMWare 成立于 1998 年,是最早的此类公司之一。
Innotek 公司开发了 VirtualBox,并于 2007 年将其发布为开源软件,随后于 2008 年被 Sun Microsystems 收购。然后,Sun 在 2010 年被 Oracle 收购。Parallels 是一个为 Mac 提供的虚拟化解决方案,于 2004 年开发,并在 2006 年成为主流。
虚拟化的价值促使芯片制造商逐渐增加对虚拟化的 CPU 支持。通过 CPU 支持,基于 x86 的系统可以运行虚拟化的机器或软件,接近本地速度,从而更加可接受。这反过来促使工作站公司(如苹果、Sun 和硅谷图形公司)转向 x86 CPU。
虚拟化软件的一个关键组件是虚拟机监控程序(hypervisor)。虚拟机监控程序将虚拟机呈现给所选操作系统,然后管理虚拟机的资源和执行。虚拟机本身是可配置的,至少在内存大小、逻辑 CPU 核心数、显卡内存、作为虚拟硬盘驱动器的主机操作系统磁盘文件、虚拟 CD-ROM 驱动器中的 CD-ROM 挂载和卸载等方面是可配置的。虚拟机监控程序确保这些资源确实可用,并且不会让某个虚拟机占用主机的所有资源,导致其他虚拟机无法获得所需资源。
对于企业来说,要求有所不同。与通过像 Linux 这样的通用主机操作系统提供虚拟机不同,整个操作系统本身可以专门优化为虚拟机监控程序。VMWare 于 2004 年推出了其Elastic Sky X 集成(ESXi)操作系统。剑桥大学计算机实验室在 1990 年代末开发了 Xen 虚拟机监控程序,首个稳定版本于 2003 年发布。Xen 最初是亚马逊为其弹性计算云(Elastic Compute Cloud)提供服务时使用的虚拟机监控程序,后来转向使用 KVM。
KVM 是一个由 Linux 内核直接支持的虚拟化解决方案。内核可以充当 KVM 下的虚拟机监控程序。KVM 还可以模拟与主机本地 CPU 不同的处理器,通常是 x86。这使得 KVM 可以用于模拟像 Raspberry Pi 这样的目标平台。
扩展专用托管网站可能会遇到问题。虽然可以通过升级到更大、更强大的服务器来应对不断增长的流量和服务,但到了一定时点,就没有更大或更强的服务器了!从这个点起,要实现扩展就需要将服务分布到多个服务器上。
应对日益增加的电力需求
虚拟化趋势催生了对新型服务器的需求,这些服务器被部署在数据中心。客户可能曾租用或安装自己的专用服务器,配备 16GB 的内存,而虚拟服务器提供商则可以租用 128GB 内存服务器的一部分,并将该服务器与多个客户共享。这些更大的服务器需要更多的 CPU 核心,以确保虚拟服务器拥有合理的计算能力。
将这些专用服务器放入与较小且功能较弱的专用服务器相同的空间,带来了新的挑战:电力问题。专用服务器可能只需要 400 瓦的电力,而云服务器可能需要 1600 瓦;电力需求将是原来的四倍。除了机器本身的电力需求外,还需要更多的电力来驱动空调冷却机器。
电力成本要求改变了专用托管的计算方式,因此带宽定价几乎是免费的,而服务器的电力需求则以非常高的价格收费。
为了帮助降低电力成本,数据中心已开始自给自足,提供一部分电力。太阳能电池板、靠近能够驱动涡轮机的河流建设、风力涡轮机,以及建设在气候寒冷或凉爽地区都是一些常见的方法。数据中心确实使用电池作为备用电源,同时也使用柴油发电机。
能效是降低电力成本的另一种方式。使用低功率的 CPU 和其他计算机部件是实现这一目标的一种手段。CPU 制造商一直致力于生产低功耗的 CPU,供数据中心和笔记本电脑使用。
托管公司为每个共置机架提供 60 瓦的电源。如果需要超过 60 瓦的电力,可以支付额外费用,为机架增加额外的 60 瓦电力线路。你需要支付建设费用以及每月的电力使用费用。
在这些设施中托管对大多数客户来说是个问题。它需要购买物理机器和其他硬件,设计为提供服务所需的基础设施,时不时需要物理访问机架和硬件,以及可能出现的故障,这意味着停机时间。
服务的增长和流行要求具备可扩展性,或者需要更多、更强大的机器。可以重新利用旧机器,但它们占用空间且耗电。当现有机架填满,且需要更多服务器时,客户成本急剧上升。
下一步,解决这些麻烦的方法就是虚拟化,并在云端运行服务器和服务。
虚拟化和云计算
大多数客户不需要专用服务器。他们真正需要的是一个文件系统的安全性,只有他们的软件可以读取和写入,且 CPU 必须专门用于他们的目的,吞吐量和计算能力是可识别的,并按预期交付。
由像 AWS 这样的公司提供的虚拟服务器的吸引力,促使许多管理员远离专用服务器和自托管。AWS 不断扩展其产品,以便为虚拟主机提供更多价值,让客户受益于亚马逊开发人员的努力。
复制客户设计的基础设施以创建一个与实时/部署应用分开的测试环境相对便宜。随着服务的流行或当服务被Slashdot效应影响时,扩展服务变得更加容易。这个术语描述了当一个非常受欢迎的网站添加了另一个网站的链接,导致更多的流量涌向那个网站,甚至可能比该网站所能处理的流量还要多的现象。
虚拟化基础设施的设计和部署可以在你办公室的舒适环境中完成,无需亲自访问数据中心。如果需要横向扩展,只需启动额外的虚拟机实例。如果需要纵向扩展,只需启动一个更强大的虚拟机,并用它替换掉太慢或过小的虚拟机。
如果云托管设施的硬件发生故障,托管公司员工将安装新的硬件。这一过程对你,作为客户,完全透明。名为Teleport的功能允许托管公司将正在运行的虚拟机迁移到另一台物理机上,且不会中断服务。
除了虚拟服务器,托管公司还可以提供虚拟磁盘、弹性 IP、负载均衡器、DNS、备份解决方案等。虚拟磁盘非常方便,因为你可以通过简单地复制作为映像的文件来备份它们。你还可以从现有的虚拟磁盘启动新的实例,节省在虚拟机上安装操作系统所需的时间。
使用弹性 IP 和虚拟负载均衡器的能力,使得扩展性变得像点击鼠标一样简单。
你可以将弹性 IP 分配给任何虚拟实例或负载均衡器。如果实例被停止,可以将该 IP 重新分配给另一个实例。如果仅通过 DNS 来处理这一问题,DNS 在 ISP 的众多服务器上传播时可能会出现几天的延迟。负载均衡器允许你创建虚拟服务器集群,并在集群中的虚拟服务器之间平衡传入请求。你可以轻松地启动并添加额外的虚拟服务器到负载均衡器中,以便扩展。托管公司甚至可以提供软件触发器,当流量增加时自动启动并添加新的服务器,而当流量减少时则自动关闭并移除这些服务器。
图 1.4 – 硬件虚拟化
AWS 启用公众访问时,当时流行的技术栈是 LAMP,即 Linux、Apache、MySQL 和 PHP 的缩写。典型的设置是在专用的 Linux 服务器上安装这四个软件包。AWS 提供了 RDS,或者等效的 MySQL 专用虚拟服务器,这使得 LAMP 应用程序能够实现卸载和扩展。AWS 还提供了虚拟负载均衡器,它是逻辑以太网交换机,用于在两个或更多的 Web 服务器之间负载平衡流量。AWS 还提供了域名托管和弹性 IP,因此站点的正常运行时间几乎可以是无限的。AWS 继续开发新的软件和服务,以造福其客户。
AWS 及其竞争对手提供了一种成本效益高且动态的方式来随着其受欢迎程度的增加而扩展互联网存在。价格结构在大多数提供商中是常见的。费用基于弹性负载均衡器的数量、虚拟服务器实例的数量、RAM 的大小、虚拟 CPU 的数量、持久存储的大小和带宽。此外,还有可选的附加服务可以增加费用。
虚拟服务器提供了物理服务器的优势,但代价是占用了主机机器上物理 RAM 的专用空间和运行该机器所需的功率。主机机器可能有 64 GB 的 RAM;它可以运行一些虚拟机的组合,所有虚拟机加起来使用掉这些 RAM——例如,四个 16 GB 的虚拟机、两个 32 GB 的虚拟机、两个 16 GB 和一个 32 GB 的虚拟机,依此类推。
虚拟机的一个风险是,当主机机器重启或发生故障时,所有托管在其上的虚拟机都会停止运行。
实现虚拟化的特性和虚拟化在数据中心应用时的限制使得容器化成为一种可行且优选的替代方案。
使用容器进一步优化数据中心资源
Docker 是一种巧妙利用操作系统级虚拟化支持的方式,它允许多个 Docker 容器在一台机器上运行。容器是容器镜像的运行实例。默认情况下,容器与主机机器以及彼此之间是隔离的。
容器可以被配置为暴露资源,例如网络端口,给主机网络(例如,互联网)或彼此之间。下图展示了容器在主机上的基本结构:
图 1.5 – Docker 容器化
容器与主机共享 Linux 内核,因此你无需像虚拟机那样在容器内安装完整的操作系统。容器由 Docker 守护进程管理,后者处理容器及其使用的资源的管理,以及镜像、网络、卷等。
虚拟服务器和容器之间一个重要的区别是,容器直接共享主机的资源,而虚拟服务器需要重复资源。例如,两个相同的容器使用主机的 RAM,而不是在启动虚拟机之前配置的 RAM 块。如果你需要限制容器的资源(如 CPU、内存、交换空间等),是可以做到的,但默认情况下,容器没有资源限制。
与虚拟服务器不同,你处理的是应用镜像,而不是虚拟磁盘。你可以复制镜像进行备份,但没有虚拟磁盘文件可以复制。这些应用镜像是基于其他容器逐步构建的。当你构建容器时,只有那些发生变化的应用镜像部分需要处理。
在设计使用容器的服务时,你不太可能在单个容器中安装很多组件。例如,在运行 LAMP 应用程序的虚拟机中,你可能会在一台虚拟机中安装 Apache、MySQL 和 PHP。而在为容器设计相同的 LAMP 应用时,你可能会将一个容器仅配置为 MySQL,另一个容器用于 Apache 和 PHP。然后,你可以通过运行额外的 Apache 和 PHP 容器,以及在集群配置中运行额外的 MySQL 实例来扩展你的应用程序。
如果我们考虑早前讨论的 LAMP 应用的容器化使用,我们可以将 MySQL 实现为专用容器,而将 Apache 和 PHP 放在另一个容器中;这一切都运行在主机的 Linux 内核之上。为了扩展 LAMP 应用程序,可以启动第二个、第三个、第四个等 Apache/PHP 容器,同样也适用于 MySQL 容器。MySQL 容器可以配置为主从操作。
如果主机操作系统不是基于 Linux 内核的,有两种选择。第一种选择是在主机上运行操作系统本地容器(例如,在 Windows 主机上运行 Windows 容器)。第二种选择是在主机上运行一个 Linux 虚拟机,并在该虚拟机中运行容器。
容器化对于托管公司及其客户来说是一个福音。与虚拟机不同,不再需要为每个容器分配固定数量的 RAM。物理机器的限制仅在于其资源,决定了它可以并发运行的容器数量。容器的定价模型可以帮助客户节省每月费用。因此,容器化是一个巨大的胜利。
在下一章中,我们将学习如何使用虚拟机和 Docker 在本地开发应用程序。稍后的章节中,我们将探讨如何将本地开发的软件部署到公开可访问的互联网/云基础设施。
总结
在本章中,我们看到 Docker 和容器化是自商业互联网开始以来,托管需求进化的自然结果。我们回顾了托管的历史以及我们如何发展到今天的托管配置。你现在应该对虚拟化和容器化之间的区别有了相当的理解。
在下一章中,我们将探讨 VirtualBox 和 Docker。这是一个很好的方式来探索虚拟机和 Docker 容器之间的区别。
进一步阅读
如果你想深入了解目前讨论的某些主题,请参考以下链接:
-
这个链接部分描述了谷歌搜索算法的实现:
www.google.com/search/howsearchworks/
-
这个链接描述了谷歌的搜索基础设施:
netvantagemarketing.com/blog/how-does-google-return-results-so-damn-fast/
-
这个链接也描述了谷歌的搜索基础设施:
www.ctl.io/centurylink-public-cloud/servers/
-
这个链接描述了 IBM 早期的虚拟化支持技术:
en.wikipedia.org/wiki/IBM_CP-40
-
这个链接描述了一个旧程序,它模拟 PC 在非 Windows 主机上运行 Windows:
en.wikipedia.org/wiki/SoftPC
-
这个链接提供了 VMWare 公司的介绍:
en.wikipedia.org/wiki/VMware
-
这个链接描述了 Oracle 的 VirtualBox:
en.wikipedia.org/wiki/VirtualBox
-
这个链接介绍了 Parallels:
en.wikipedia.org/wiki/Parallels_(company)
-
这个链接讨论了虚拟化和容器化中 Hypervisor 的作用:
en.wikipedia.org/wiki/Hypervisor
-
这个链接描述了 VMWare 专门为运行虚拟机设计的独立操作系统:
en.wikipedia.org/wiki/VMware_ESXi
-
这个链接描述了 Xen hypervisor:
15anniversary.xenproject.org/#Intro
-
这个链接描述了亚马逊的 AWS 虚拟机:
en.wikipedia.org/wiki/Amazon_Elastic_Compute_Cloud
-
这个链接描述了支持虚拟化和容器化的内核特性:
en.wikipedia.org/wiki/Kernel-based_Virtual_Machine
-
这个链接描述了如何使用 QEMU 在工作站上模拟树莓派:
azeria-labs.com/emulate-raspberry-pi-with-qemu/
第二章:使用 VirtualBox 和 Docker 容器进行开发
在上一章中,我们介绍了虚拟化和容器化。在本章中,我们将演示如何使用软件,如 VirtualBox,创建虚拟机,并使用 Docker 创建容器。本章的重点是如何在工作站上使用这些技术进行开发。
开发人员在处理多个项目时,一个常见的问题是,随着时间的推移,他们会在工作站上安装大量目前不使用的软件。这可能会变得非常麻烦,甚至让开发人员不得不重装工作站的硬盘和操作系统。
VirtualBox 和 Docker 容器都可以用来解决这个问题。你安装的软件会保留在虚拟机或容器的文件系统中,与工作站的本地文件系统分开。如果你删除虚拟机或容器,其中安装的所有文件都会被删除——包括任何已安装的应用程序或开发软件。
开发人员面临的另一个问题是,工作在特定项目上时需要使用的软件版本。如果开发人员在一个使用 Node.js v12 的项目上工作,而另一个项目使用 Node.js v10,他们就无法在同一工作站上同时运行这两个项目,虽然切换 Node.js 的版本是可行的,但操作起来不太方便。使用虚拟机或容器就没有这个问题——你可以有一个运行 Node.js v12 的虚拟机,另一个运行 Node.js v10,并且同时运行这两个虚拟机。容器也类似,每个版本的 Node.js 可以对应一个容器。
当你需要模拟整个机器时,虚拟化非常有用。如果你的生产系统是虚拟机或物理机,虚拟机是一个很好的方式来模拟该环境。虚拟化非常适合在工作站上运行完整的替代操作系统;也就是说,你可以在 macOS 或 Linux 工作站上通过虚拟机运行 Windows 10。
本章将涵盖以下内容:
-
主机文件系统污染问题
-
使用 VirtualBox 进行虚拟机操作
-
使用 Docker 容器
技术要求
本章的代码可以从以下链接下载:github.com/PacktPublishing/Docker-for-Developers/tree/master/chapter2
查看以下视频,看看代码的实际应用:
主机文件系统污染问题
虚拟化和容器化解决了开发人员面临的某些问题。将服务器类软件系统安装到工作站上没有太大意义——这种软件可以安装在虚拟机或 Docker 容器中。使用这种策略意味着你不必污染工作站的文件系统,不会出现软件版本冲突,并且你可以运行与工作站操作系统不同的操作系统。
污染问题是开发人员的一个实际关注点——他们最终会得到很多不常用的软件或安装的软件,这些软件占用了系统资源。我们将学习使用虚拟化或容器化的方式来安装这些软件,而不是直接安装到宿主的文件系统上。
使用 VirtualBox 进行虚拟机操作
在工作站上运行虚拟机有几个选项。包括 Parallels(适用于 macOS)、KVM/QEMU(适用于 Linux)、VMware(商业版本,支持多个宿主操作系统)和 VirtualBox(一个 Oracle 产品)。我们将使用 VirtualBox,因为它是开源的,且可以免费使用。它还具有跨平台特性,你可以在 Linux、Windows、macOS 和其他宿主操作系统上运行 VirtualBox 和虚拟机。
虚拟化简介
虚拟化利用工作站 CPU 的特殊指令和功能,在宿主机上运行一个通用的伪计算机系统(虚拟机)。在这个虚拟机内,你可以安装各种操作系统,包括不同版本的 Windows Server、Linux、BSD 等等。在虚拟机中运行的操作系统称为来宾操作系统;在工作站上运行的操作系统称为宿主操作系统。
当来宾操作系统执行代码时,它需要进行磁盘和网络访问,执行特权 CPU 指令,并且访问与宿主机共享的资源。虚拟化软件有效地拦截这些来宾操作系统的访问,并将它们转换为宿主操作系统的调用。因此,虚拟机中运行的代码大部分时间都能以全速本地 CPU 速度运行,直到执行这些共享访问的拦截——然后就会有一些额外的开销用于转换为宿主访问。
在安装操作系统之前,可以配置来宾虚拟机。你可以设置使用多少 RAM、一个或多个虚拟磁盘、一个或多个以太网控制器、显卡、要插入虚拟光驱的 ISO 文件(安装介质)等。
通常,你需要为你的来宾操作系统以及你计划在来宾系统内使用的应用程序,设置适当的 RAM、磁盘空间和虚拟 CPU 核心数。例如,如果你要在虚拟机中运行 Windows,你可能希望分配至少 2 个虚拟 CPU 核心、8GB 的 RAM 和 32GB 的磁盘空间。如果你计划在虚拟机中运行一个需要超过 8GB 内存的应用程序,你需要分配更多的 RAM;如果该应用程序需要大量的磁盘空间,你则需要分配更多的磁盘空间。
创建虚拟机
要启动虚拟机,请使用 VirtualBox 程序(用户界面)。当虚拟机启动时,它的表现就像一台物理 PC——对于安装媒体上的安装程序而言,它就是一台物理 PC。安装程序将像在新 PC 上安装或重新安装操作系统一样工作。
虚拟机可以在工作站的桌面上以窗口的形式展示其控制台或桌面,或者它可以是无头的。无头虚拟机类似于服务器机器——你通过 FTP、SSH 等方式访问它。你在不需要操作系统控制台或图形界面的情况下,使用无头虚拟机。无头机器提供你可以远程访问的所有服务器服务。
你从命令行启动无头虚拟机,而不是通过 VirtualBox 用户界面程序。这是通过VBoxManage
命令来完成的,相关文档可以在这里找到:www.virtualbox.org/manual/ch08.html
。不过,你更有可能使用带图形用户界面的来宾操作系统。
一个典型的无头虚拟机可能会用于运行LAMP应用程序——Linux, Apache, MySQL 和 PHP都被整齐地包含在虚拟机内,而不是工作站的文件系统中。你可以通过启动一个无头虚拟机来运行 MySQL,然后启动两个无头虚拟机,分别运行 HTTP 服务器和 PHP 代码,从而模拟一个可扩展的 LAMP 应用程序。
一个典型的图形/桌面虚拟机可能会被用来在你的 Mac 电脑上以窗口形式运行 Windows,在 Mac 电脑上以窗口形式运行 Linux,或在 Windows 机器上以窗口形式运行 Linux,等等。如果你喜欢使用 Linux,但需要运行 Windows 程序,使用虚拟机是一个不错的选择。
非无头安装将提供一些显示选项。整个桌面可以在主机的桌面上以窗口形式显示。这是默认的显示模式。该窗口可以像桌面上的其他窗口一样调整大小。然而,在窗口内部,来宾系统的桌面不会自动调整大小,直到你在来宾系统中安装 VirtualBox 增强功能。
来宾窗口可以设置为全屏模式。这样,来宾操作系统看起来就像是原生运行在工作站上一样。如果你正在运行 macOS,你可以使用 macOS 手势切换桌面,并在全屏的 Windows 和全屏的 macOS 桌面之间来回切换。
对于某些主机操作系统,来宾操作系统可以进入无缝模式,在这种模式下,桌面完全不显示,但虚拟机中运行的任何应用程序会将其窗口显示在主机桌面之上。
结果是虚拟机应用程序窗口和主机操作系统应用程序菜单混合显示在桌面上,如下图所示:
图 2.1 – 在 Linux 主机上的 VirtualBox 中全屏运行 Microsoft Windows 10
如你所见,你可以在虚拟机中运行并管理完整的 Windows 安装,并可以访问主机上的文件和目录,如果你为主机设置了 Samba 文件共享。
顺便提一下,本书的部分内容是使用 Microsoft Word 365 编写的,该程序在 Linux 主机上的 Windows 10 虚拟机中运行。接下来的 Docker 示例是在 Linux 主机上执行的。这是一个很好的例子,说明为什么要运行虚拟机。
注意:
微软允许你购买 Windows 10 许可证,并使用它在虚拟机中激活 Windows 10。
苹果只允许在苹果硬件上运行 macOS 虚拟机。在运行 Windows 或 Linux 的 PC 上运行 macOS 虚拟机违反了它们的许可条款。
Linux 和大多数 BSD 变种通常可以在 PC 或虚拟机中免费使用。
来宾增强功能
对于 Windows 和 Linux 来宾操作系统,你可以安装驱动程序,完全将来宾操作系统与主机操作系统集成。这些驱动程序被称为来宾增强功能,你可以从 VirtualBox 网站下载:virtualbox.org
。它们会像你为 Windows 或 Linux 安装的任何程序一样,安装在虚拟机中。与主机的集成非常有用。
来宾增强功能中的显示驱动程序允许你使用主机屏幕的完整分辨率,并且如果你以窗口模式运行(来宾桌面在主机桌面窗口中),调整窗口大小会导致来宾桌面自动调整大小以适应新窗口大小。如果你想使用无缝窗口功能,则必须安装来宾显示驱动程序。
这些增强功能提供了鼠标指针集成功能。这样你可以自由地在物理屏幕之间移动光标,从来宾窗口切换到主机窗口。否则,鼠标会被虚拟机捕获,以便管理指针事件。
客户端附加功能还可以像共享一个剪贴板一样,分享主机和客户机的剪贴板。你可以在 macOS 主机应用程序中选择并复制文本,然后将复制的文本粘贴到虚拟机中运行的 Windows 应用程序里。
对于 Linux 客户机,附加功能允许你共享主机的文件系统目录和文件。这尤其有用,因为你可以使用主机操作系统的工具和软件来开发主机可见的文件。例如,你可以在 macOS 机器上为你的项目工作目录创建一个共享文件夹。你可以使用 macOS 编辑器来编辑项目中的文件,而在虚拟机中,你可以运行 Linux 本地编译器或工具来执行你的项目。现在我们开始安装 VirtualBox。
安装 VirtualBox
VirtualBox 的网址是 www.virtualbox.org/
。在这里,你可以找到不同主机平台(工作站操作系统)的文档和下载内容,附加组件,查看截图,查看与 VirtualBox 兼容的推荐第三方软件等。
Windows 安装说明
要安装 Windows 版本,前往 VirtualBox 网站的下载页面,下载最新版本的安装程序,然后在下载完成后双击它。接着,按照屏幕上的指示操作。
macOS 安装说明
对于 macOS 安装,你可以使用 Homebrew 或从 VirtualBox 网站下载 .dmg
安装文件并从中安装。使用 Homebrew,你只需输入一条命令:
$ brew cask install virtualbox
Homebrew (brew.sh/
) 是 macOS 缺失的包管理器。它是一个命令行系统,用于从 Homebrew 的仓库中安装软件。它是一个极好的工具,可以增强 macOS 随附的应用软件。这些仓库中的软件更新频率远高于苹果的软件更新。
Linux 安装说明
在 Linux 上安装 VirtualBox 的说明会根据你在工作站上使用的 Linux 发行版而有所不同。由于有很多不同的发行版,我们将以 Ubuntu 为例,给你一个大致的安装步骤,并为你提供安装 VirtualBox 的其他发行版(如 Arch Linux、Fedora 等)的有用提示。
对于 Ubuntu,你可以从 Ubuntu 软件中心安装 VirtualBox,或者从 VirtualBox 网站下载 .deb
文件,或使用 apt
:
$ sudo apt install virtualbox
对于 Arch Linux 及其变种,你可以按照 Arch 维基网站上的说明操作:wiki.archlinux.org/index.php/VirtualBox
。
对于 Fedora 或其他基于 RPM 的 Linux 发行版,请按照 VirtualBox 网站上的说明操作:virtualbox.org
。现在我们来了解如何使用 Docker 容器。
使用 Docker 容器
Docker 通常用于创建容器,这些容器运行你的应用程序,就像在无头虚拟机中一样。实际上,在非 Linux 系统上,Docker 实际上是在虚拟机中运行 Linux,并在该虚拟机中运行你的容器。这是透明进行的。
注意:
你不必自己安装 VirtualBox。Docker 的打包方式使得它能够安装或使用操作系统中已经存在的虚拟化技术(例如虚拟机监控器)。
容器简介
Docker 的早期版本安装了 VirtualBox 来创建虚拟机,但操作系统中实现的更新虚拟化技术使得 Docker 可以使用这些技术,而不必依赖 VirtualBox。
Docker 为 Linux 容器而设计,期望宿主操作系统或虚拟机运行 Linux。容器与宿主共享 Linux 内核。Docker 也可以用于运行 Windows 原生容器,方式与 Linux 容器类似。Windows 内核在宿主和客户端之间共享。为了讨论方便,我们将重点讨论 Linux 宿主和客户端。
Docker 容器通常用于实现类似无头虚拟机的功能。每个应用程序可能创建一个容器,而使用虚拟机的成本较高——你必须为虚拟机保留固定的 RAM 和磁盘空间。在一台 16GB RAM 的 MacBook Pro 上,你大致可以同时运行三个 4GB RAM 的虚拟机。但你还是需要为宿主操作系统保留一些内存。给宿主或虚拟机分配不足的内存会导致它们交换数据,这会极大地降低性能:
图 2.2 – Docker 容器示意图
容器通过宿主操作系统的特性与宿主操作系统分隔开。容器使用 Linux 内核的命名空间功能 (manpages.debian.org/stretch/manpages/namespaces.7.en.html
) 来将容器内的代码相互隔离,并使用 cgroups(参见 manpages.debian.org/stretch/manpages/cgroups.7.en.html
)来限制容器可以使用的资源(包括 RAM 和 CPU)。容器还使用 Linux 的 unionfs
(manpages.debian.org/buster/unionfs-fuse/unionfs.8.en.html
) 文件系统来实现容器在 Docker 下运行时看到的分层文件系统。
从容器内部运行的应用程序的角度来看,容器是一个完整且专用的计算机;它与宿主操作系统没有直接的通信。
容器不需要为每个容器指定虚拟 CPU 数量或专用的 RAM 块。
你的限制仅在于容器所需的 RAM 和主机的 RAM 大小。
容器共享主机的 Linux 内核,而虚拟机则必须安装完整的操作系统!
你可以选择限制容器实例使用的资源,但这不是必须的。
主机资源可以与客容器共享。主机的网络可以与任何容器共享,但这仅对运行需要此功能的应用程序的容器有用。例如,要使用主机的 Bonjour 网络功能,客容器会使用主机的网络。
客户容器可能会将端口暴露给主机和任何可以访问主机的计算机。例如,运行 HTTP 服务器的容器可能会暴露端口 80,当访问主机的端口 80 时,容器会作出响应。
容器推动了微服务的概念。使用微服务架构的应用程序实现了一组可以相互通信并与主机通信的服务。这些服务的实现非常简单——仅需要包含支持服务所需的特定代码。微服务通常在一个源代码文件中实现,代码行数很少。
容器架构具有很高的可扩展性。你可以运行多个容器来运行相同的应用程序(横向扩展),还可以为容器系统分配更多的主机资源(纵向扩展)。例如,你可以创建一个运行 HTTP 服务器的容器;你可以通过实例化任意数量的这些容器来创建一个服务器农场。
使用 Docker 进行开发
使用 Docker 进行开发的一个重要理由是,你不需要在主机上安装除了 Docker 本身以外的任何程序来启用开发。例如,你可以在容器中运行 Apache,而无需在工作站上安装它。
你还可以在容器中混合和匹配不同的软件版本。例如,一个微服务架构可能需要一个容器使用 Node.js 版本 8,另一个容器使用 Node.js 版本 10。这在单个主机上显然是有问题的,但在使用 Docker 时非常简单。一个容器安装并运行版本 8,另一个容器安装并运行版本 10。
在开发过程中,你可以将项目的开发文件与容器共享,这样当你编辑这些文件时,容器就会看到文件的变化。
每个容器都有自己的一组全局环境变量。通常的做法是使用环境变量来配置应用程序,而不是在容器内的源代码或配置文件中进行配置。
当你准备部署或发布一个容器时,可以将其推送到容器托管服务,如 Docker Hub。事实上,Docker Hub 是一个非常好的资源,里面有许多现成的容器,可以帮助你进行项目开发。比如 MongoDB、Node.js(多个版本)、Apache 等等,都有预先制作好的容器镜像。
容器构建实际上是面向对象的。你从一个基础容器继承,并在其上添加所需的功能。你可以在一个从现成的 Node.js 容器开始的容器中创建 Node.js 应用程序,在容器中安装 npm
包,并运行你自定义的代码。
你始终可以开发自己的基础容器。对于这些容器,你可以从现成的 Linux 发行版包开始。Alpine Linux 基础容器很受欢迎,因为它是最轻量级的起始镜像之一。Fedora、Ubuntu、Arch Linux 等都有基础容器。无论你从哪个 Linux 容器开始,你都可以使用该操作系统的安装工具,从该操作系统的官方仓库中添加包;也就是说,Ubuntu 使用 apt
,Fedora 使用 yum
,等等。
将一个原本没有设计为在容器中运行的现有应用程序 Docker 化是一个好主意。你可以为容器选择一个与应用程序兼容的 Linux 发行版和版本,并将应用程序拆分成多个容器镜像,以便未来可以扩展。
例如,你可能有一个旧版的 LAMP 应用程序,需要特定版本的 PHP、MySQL 和 Apache,以及较旧版本的 Ubuntu。你可以将其拆分为一个独立的 MySQL 容器和一个独立的 Apache 加 PHP 容器。你希望你的 Apache+PHP 容器使用共享卷,以便它们都运行相同的最新 PHP 源代码。你可以设置 MySQL 容器使用主从复制。你还可以在另一个容器中设置负载均衡器,将请求平衡到任意数量的 Apache 和 PHP 容器实例上。
是时候进行一个实践示例,使用 Docker 进行开发了。
开始使用 Docker
我们已经创建了一个 GitHub 仓库来共享本书的代码示例。你可以在 github.com/PacktPublishing/Docker-for-Developers
找到该仓库。你应该 fork 这个仓库,然后将其克隆到你的主机上。创建 fork 后,你可以根据自己的需要管理你复制的仓库,无需权限。此部分的相关代码位于 chapter2/
目录下。这里的代码实现了一个小型的 Apache+PHP 应用程序,旨在容器中运行。这里有一些 sh
脚本,用于执行 Docker 命令行,你无需不断输入长串的命令行参数。
在开始编码之前,先确保 Docker 正确安装。docker ps
命令会列出所有正在运行的 Docker 容器。我们可以看到目前没有容器在运行,并且确实可以使用 docker
命令:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
%
Dockerfile 是一个文本文件,用于定义如何构建 Docker 容器镜像。容器本身并不会启动;它只是被创建在磁盘上。构建完成后,你可以启动任意多个容器实例。
通过 sh 脚本自动化 Docker 命令
我们将大量使用 docker cli
命令和 sh
脚本来自动化命令行操作。使用 sh
脚本文件有几个优点。一旦脚本文件创建完成,你不必记住所有命令行参数。脚本正确后,你就不会因为拼写错误或命令行参数不正确而遇到问题。输入脚本文件名更简短,且当你输入文件名的前几个字符并按下 Tab 键时,Shell 会自动补全文件名。最后,脚本的命名具有助记性:build.sh
表示构建容器,run.sh
表示运行容器,等等。
我们提供的 sh
脚本如下:
-
./build.sh
:此脚本用于根据 Dockerfile 构建容器。每当你编辑 Dockerfile 或容器需要重新构建时,应该运行此脚本。 -
./debug.sh
:此脚本以调试模式运行容器。在调试模式下,Apache 会在前台模式运行,你可以按^C
停止容器。 -
./run.sh
:此脚本以守护进程模式运行容器。与./debug.sh
脚本不同,容器将在 Docker 中运行,同时你会返回到命令行提示符。你可以使用此脚本在本地运行容器,模拟生产环境,以测试生产环境下的行为。 -
./stop.sh
:当容器在后台运行时,可以使用此脚本停止容器。 -
./shell.sh
:有时在创建容器和编辑 Dockerfile 时,可能会出现预期之外的问题。你可以使用此脚本在容器内启动 Bash 命令行。通过此命令行,你可以检查并诊断问题。 -
./persist.sh
:此脚本演示如何使用命名卷在容器内持久化应用程序状态。也就是说,使用命名卷后,你可以停止并重启容器,而卷的内容会被持久化。该卷会作为磁盘挂载到容器中。
为了演示如何使用 Dockerfile 构建容器,我们在 GitHub 仓库的 chapter2/
目录下创建了一个文件(文件名为 Dockerfile
):
# we will inherit from the Debian image on DockerHub FROM debian # set timezone so files' timestamps are correct ENV TZ=America/Los_Angeles # install apache and php 7.3 # we include procps and telnet so you can use these with shell.sh prompt RUN apt-get update && apt-get install -y procps telnet apache2 php7.3 # add a user - this user will own the files in /home/app RUN useradd --user-group --create-home --shell /bin/false app # set up and copy files to /home/app ENV HOME=/usr/app WORKDIR /home/app COPY . /home/app # The PHP app is going to save its state in /data so we make a /data inside the container RUN mkdir /data && chown -R app /data && chmod 777 /data
# we need custom php configuration file to enable userdirs COPY php.conf /etc/apache2/mods-available/php7.3.conf # enable userdir and php RUN a2enmod userdir && a2enmod php7.3 # we run a script to stat the server; the array syntax makes it so ^C will work as we want CMD ["./entrypoint.sh"]
让我们逐步看看 Dockerfile 是如何工作的:
-
Dockerfile 继承自 Docker Hub 上的 Debian 镜像。
-
我们将容器的时区设置为与主机的时区匹配;换句话说,确保容器内文件和主机上的文件时间戳一致。这在将主机目录映射到容器的文件系统时非常重要。
-
然后我们安装 Apache 和 PHP 7.3。这些安装在容器的文件系统中,而不是主机的文件系统中。我们避免了安装版本混乱的问题,这样在不进行该项目工作时,主机上的版本不会变得冗余。
-
我们还安装了一些命令行工具,允许我们通过容器内运行的 Bash shell 检查构建容器的状态。
-
默认情况下,在容器中运行项目的用户和组是
root
。为了提供一些典型的 Unix/Linux 安全性,我们希望以实际用户身份运行;在我们的案例中,用户名是app
。因此,我们通过useradd
将该用户添加到容器的环境中。 -
我们将把我们的 PHP 脚本放在
/home/app
目录,并能够将我们的工作目录与主机上的 PHP 脚本映射到/home/app
。 -
我们的演示应用程序将其状态写入
/data
,因此我们需要创建该目录并确保作为用户应用运行的 PHP 脚本可以在其中读写文件。 -
我们创建了一个自定义的 PHP 配置文件,想要在容器内使用,因此我们将其复制到容器的正确文件系统位置。
-
我们需要启用
userdir
和php7.3
模块。这使我们能够通过 Apache 运行 PHP 脚本,并且可以通过如http://localhost/~app/index.php
这样的 URL 访问我们位于/home/app/public_html
的 PHP 脚本。 -
当容器启动时,它需要在容器内运行一些程序或脚本。我们使用一个名为
entrypoint.sh
的sh
脚本,位于/home/app
目录中来启动应用程序。我们可以在开发过程中根据需要编辑这个文件。
我们本可以从多种 Linux 发行版中选择作为起点。我们在这里选择 Debian,因为配置命令对大多数读者来说应该比较熟悉。如果你在虚拟机中安装 Debian,你将使用相同的命令来安装和维护系统。Debian 并不是最小或最轻量的 Linux 镜像;如果你希望容器使用更少的资源,Alpine 是一个不错的选择。如果你选择使用 Alpine,务必阅读如何在 Alpine 中安装包和维护系统。
请注意,无论你从哪个 Linux 镜像开始,它都与主机共享 Linux 内核。在容器内,它才是 Debian —— 你的主机操作系统可能是其他的 Linux 发行版。你在容器内安装的内容不会安装到你的工作站上,只会安装在容器内。显然,你不应该直接在 Arch Linux 工作站上混用,例如 Debian 命令和已安装的包。
当你在实际主机或虚拟机上安装 Apache 时,你会使用 a2enmod
和 a2dismod
命令进行配置,并通过编辑 /etc/apache2
中的各种配置文件来完成配置。我们在这里所做的是在工作站上本地编辑配置文件,然后将该配置文件复制到容器中。
Dockerfile 在容器内使用 apt-get
安装了一些 Debian 应用程序。容器内启动 apt-get
的 RUN
命令使用了 -y
开关来自动回答 apt-get
可能提出的任何问题,使用 -qq
开关让 apt-get
命令输出更简洁,同时使用 >/dev/null
重定向 stdio
以使 Docker 构建(build.sh
)的输出更加简洁。如果没有 -qq
和 stdout
重定向,构建输出将包含下载的每个包和依赖项,以及所有这些包的安装命令。
请注意,Dockerfile 中的最后一行是 CMD,即容器实例化时运行的命令。在我们的案例中,我们使用一个包含单个项 entrypoint.sh
的数组。该数组使得你可以按 Ctrl + C 停止容器。entrypoint.sh
脚本在容器内执行必要的初始化后运行 Apache。还要注意,我们在 Dockerfile 中启用了 userdir
和 php7.3
模块。
现在我们有了 Dockerfile,我们需要能够构建容器,这样我们才能使用它。这时,第一个 .sh
脚本就发挥作用了。
理解 build.sh
build.sh
脚本用于构建容器。你需要至少构建一次容器,这样我们才能编辑主机上的文件,并在容器内看到这些变化。如果你想在生产模式下尝试容器并获得最新版本的文件,你将需要重新构建容器:
#!/bin/sh
# build.sh
# we use the "docker build" command to build a container named "chapter2" from . (current directory)# Dockerfile is found in the current directory, and determines how the conatiner is built.
docker build -t chapter2 .
-t
标志表示将容器命名为 chapter 2
。Dockerfile 位于当前目录。build.sh
脚本的输出非常长,这里省略了。
你可以看到,在构建容器时输出中打印的每一步都对应 Dockerfile 中的一行:
Sending build context to Docker daemon 15.87kB Step 1/11 : FROM debian ---> 67e34c1c9477 Step 2/11 : ENV TZ=America/Los_Angeles ---> Using cache ---> 7bfa02a200a8 Step 3/11 : RUN apt-get update -qq >/dev/null && apt-get install -y -qq procps telnet apache2 php7.3 -qq >/dev/null ---> Running in 98a4e3192e22 debconf: delaying package configuration, since apt-utils is not installed Removing intermediate container 98a4e3192e22 ---> 86aa2b03b3b1 Step 4/11 : RUN useradd --user-group --create-home --shell /bin/false app ---> Running in 917b16b86dc5 Removing intermediate container 917b16b86dc5 ---> ef96ff367f1f Step 5/11 : ENV HOME=/usr/app ---> Running in c9706abf0afd Removing intermediate container c9706abf0afd ---> 4cc08031746b Step 6/11 : WORKDIR /home/app ---> Running in 08c2b9c79204 Removing intermediate container 08c2b9c79204 ---> 9b68722d6776 Step 7/11 : COPY . /home/app ---> d6a7b4a1a4f3 Step 8/11 : RUN mkdir /data && chown -R app /data && chmod 777 /data ---> Running in fe824496056c Removing intermediate container fe824496056c ---> 75996f4d08bc Step 9/11 : COPY php.conf /etc/apache2/mods-available/php7.3.conf ---> c6a3b094a041 Step 10/11 : RUN a2enmod userdir && a2enmod php7.3 ---> Running in 1899c1d01a2e Removing intermediate container 1899c1d01a2e ---> ae6ddd93786c Step 11/11 : CMD ["./entrypoint.sh"] ---> Running in cb0ffeaefca6 Removing intermediate container cb0ffeaefca6 ---> 9c64d1cb6bd3 Successfully built 9c64d1cb6bd3 Successfully tagged chapter2:latest
容器是按增量构建的,如 Dockerfile 所描述的那样。每个步骤都在一个图像层中构建,该层通过哈希值来表示——这些就是打印的十六进制哈希值。当你再次构建容器时,Docker 可以从这些层的 /
哈希值的状态开始,从而减少了每次都从头开始构建容器的需要。每一层仅仅是当前层要求与前一层状态之间的差异。
第一个层是 Debian 镜像。接下来的层是一个中间层,它是 ENV
命令在 Dockerfile 中执行后的结果与原始 Debian 镜像之间的差异。接下来的层是前一个中间层和 apt-get
安装的包之间的差异。注意,我们使用 &&
将一些 apt-get
命令打包成一个层,从而加快了构建过程。随着 Dockerfile 中每个命令的处理,层级继续。
Docker 在缓存和处理层的方式上非常智能。它不需要每次构建时都下载 Debian 镜像;如果它知道之前的步骤没有改变容器到那一点的状态,它可以从之前的中间阶段开始构建。
每当我们需要构建容器时,因为我们对 Dockerfile 做了更改,我们就使用 build.sh
脚本。一旦容器构建完成,我们可以通过几种方式来使用它。debug.sh
脚本可能是你在开发过程中最常用的脚本。
理解 debug.sh
debug.sh
脚本运行的容器镜像不是以守护进程模式运行的。你可以按 Ctrl + C 来停止程序:
#!/usr/bin/env bash
# debug.sh
# run container without making it a daemon - useful to see logging output
docker run \ --rm \ -p8086:80 \ --name="chapter2" \ -v `pwd`:/home/app \ chapter2
docker run
命令有许多可选参数,数量太多,无法在这里一一详述。有关 docker run
的所有可能命令行参数的完整信息,请参阅 Docker 网站上的 docker run
文档:docs.docker.com/engine/reference/run/
。我们只会讲解在脚本中使用的参数。
-
在这里,我们使用
–rm
,它告诉 Docker 在容器退出时进行清理,删除容器和容器的文件系统。 -
-p
标志告诉 Docker 将容器的端口80
(HTTP)映射到主机的端口8086
;你可以通过主机的8086
端口访问容器中的 HTTP 服务器。 -
–name
参数为运行中的容器命名;如果不提供名称,你需要使用docker ps
获取容器的哈希值,然后使用docker stop
停止容器。 -
-v
开关将挂载容器中的卷。卷可以是主机上的目录或文件,也可以是 Docker 为你管理的命名卷。如果你想停止并重启容器,同时保留容器写入文件系统的数据,你必须挂载一个卷,并且容器必须将数据写入该卷。你可以根据需要挂载多个卷。在我们的debug.sh
脚本中,我们将当前目录与源代码挂载到/home/app
,这样我们可以修改源代码,并且容器中的程序会看到文件发生了变化(因为文件时间戳更新了),就像它们位于容器内部一样。对于这个演示,你可以编辑index.php
脚本并重新加载页面,看到更改效果。如果不挂载此卷,容器将访问通过 Dockerfile 和build.sh
脚本复制到/home/app
中的文件;这就是你在生产环境中希望的行为。 -
docker run
的最后一个参数是要启动的容器名称——在我们这个例子中,它是chapter2
,即我们使用build.sh
脚本创建的容器镜像。注意:
我们不会在容器中持久化
/data
。我们可以通过添加-v
开关,将 Docker 卷映射到/data
,这将在persist.sh
脚本中完成。
使用 debug.sh 运行我们的 chapter2 容器
让我们来看看容器的运行情况。我们运行 build.sh
脚本并看到它成功执行。接着,我们使用 debug.sh
脚本以 debug/foreground
模式启动容器。注意,我们并未对容器的主机名进行任何配置,因此 Apache 打印了一个警告信息:
% ./debug.sh
entrypoint.sh
----> Point your browser at http://localhost:8086/~app/index.php
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.5\. Set the 'ServerName' directive globally to suppress this message
在主机上,我们可以用浏览器访问 http://localhost:8086/~app/index.php
。
记住,我们将端口 8086
映射到了容器的端口 80
,并启用了 userdir
模块;在 Dockerfile 中,我们将 index.php
脚本复制到了 /home/app/public_html
(userdir
模块)。
我们本可以在 Dockerfile 和构建过程中配置 Apache 的默认主机,并将文件复制到 /var/www
,这会给我们一个更干净的 URL,并且这是你在实际生产站点中应该做的事情。对于我们的目的,看到 Apache 模块已启用并在容器内正常工作就足够了:
图 2.3 – 浏览器显示我们程序的输出
当我们在浏览器中重新加载页面几次后,可以看到计数器得到了正确的维护:
图 2.4 – 页面在我们重新加载后
请注意,我们还没有生成任何 HTML(尚未)。如果你自己尝试,可以现在编辑 index.php
文件,将 Counterx:
改为 Counter:
并重新加载页面,你将看到页面现在打印了 Counter:
。
我们现在已经为 PHP 开发做好了准备。
如果我们想添加 MySQL 支持,我们需要修改 Dockerfile 来安装 PHP MySQL 模块,并像配置 userdir
和 php
一样启用它。如果我们想添加一个 PHP 框架,我们要么需要通过 Dockerfile 在容器内安装它,要么将其添加到 chapter2/
目录中,然后复制到容器的 /home/app
目录,或者在开发时通过替换 /home/app
来挂载/绑定到容器中。
我们可以使用 docker ps
命令检查容器是否正在运行:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
54925e51e404 chapter2 "./entrypoint.sh" 2 seconds ago Up 1 second 0.0.0.0:8086->80/tcp chapter2
我们可以通过在启动容器的窗口中按下 Ctrl + C 来退出或终止容器。
当我们通过 run.sh
脚本运行容器时,容器没有输出任何内容,甚至没有 Apache 的警告信息:
% ./run.sh
1707b1ff84fabed4d9696aadbcd597cee08063eaa7ad22bfe572c922df 43997e
再次使用 docker ps
查看容器是否正在运行:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1707b1ff84fa chapter2 "./entrypoint.sh" 41 seconds ago Up 39 seconds 0.0.0.0:8086->80/tcp chapter2
在浏览器中加载相同的 URL,我们看到计数器又回到 1
。重新加载几次后,我们看到计数器按设计正常递增。
我们可以使用 docker restart
来重启容器。请注意,容器首次启动是在 3 分钟前,但由于我们重启了它,状态现在是 Up 1 second
:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1707b1ff84fa chapter2 "./entrypoint.sh" About a minute ago Up 1 second 0.0.0.0:8086->80/tcp chapter2
由于容器只是重新启动过,它的文件系统保持不变。重新加载浏览器中的 URL,我们看到计数器继续递增。我们可以使用 docker stop
或 stop.sh
脚本停止容器。docker ps
命令显示没有容器在运行。然后我们再次启动它:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
现在,当我们在浏览器中重新加载时,计数器被重置为 1
。这是因为我们正在写入容器的文件系统,而容器退出时文件系统会消失。
如果我们希望计数器在容器启动/重启之间保持持续性,我们必须将其写入挂载到容器上的卷中。
我们写入 /data/container.txt
,所以我们可以执行以下操作:
-
将我们自己的
container.txt
从主机挂载到容器中的/data/container.txt
。 -
将主机上的目录挂载为容器中的
/data
。 -
让 Docker 为我们创建并维护一个命名或匿名卷。
自从命名卷的出现以来,它们已成为更好的选择。命名卷是使用 docker run
的 -v
开关与仅包含容器中目录名称一起创建和维护的。例如,-v name:/data
。我们有一个脚本 persist.sh
,它旨在简化命名卷的使用。
persist.sh
persist.sh
脚本做的事情与 debug.sh
脚本相同,不同之处在于它向 docker run
命令添加了 -v name:/data
开关:
#!/usr/bin/env bash
# run container without making it a daemon - useful to see logging output # we are adding an anonymous volume for /data in the container so the # counter persists between runs.
docker run \ --rm \ -p8086:80 \ --name="chapter2" \ -v `pwd`:/home/app \ -v name:/data \ chapter2
当我们运行并在浏览器中输入 http://localhost:8086/~app/index.php
时,我们可以看到计数器正常工作,即使我们停止并重启容器。
run.sh
run.sh
脚本以守护进程模式运行容器——你无法看到应用程序的输出,除非使用 docker log
命令。它也不会将主机目录作为卷挂载到容器中。这模拟了生产环境:
#!/usr/bin/env bash
# run.sh
# run the container in the background # /data is persisted using a named container
docker run \ --detach \ --rm \
--restart always \ -p8086:80 \ -v name:/data \ --name="chapter2" \ chapter2
我们再次使用docker run
命令,但参数略有不同:
-
–detach
标志用于 Docker Run,它会使容器在后台运行。 -
使用命名卷,因此数据在容器启动和停止之间得以持久化。
-
开发工作目录被挂载到容器内的
/home/app
。 -
–restart
开关始终告诉 Docker 在系统重启时重新启动容器。这非常方便,因为你不需要在操作系统启动时手动启动容器。
容器只能使用通过 Dockerfile 和 build.sh
脚本复制到其中的文件运行。如果你在主机上编辑文件,你在运行的容器中将看不到这些更改,就像使用 persist.sh
时一样。每次编辑文件并希望在容器中应用这些更改时,你都需要运行 build.sh
脚本,以便在 run.sh
中使用。
我们需要一种方法来停止正在运行的容器。这时 stop.sh
就派上用场了。
stop.sh
stop.sh
脚本将停止你的 chapter2
容器。这在你使用 run.sh
脚本将容器启动到后台时特别有用:
#!/bin/sh
# stop.sh
# stop running container - typing stop.sh is easier than the whole docker command
docker stop chapter2
让我们看看 run.sh
和 stop.sh
的实际效果:
build.sh debug.sh Dockerfile entrypoint.sh install-virtualbox-macos.sh persist.sh php.conf public_html README.md run.sh shell.sh stop.sh % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES % ./run.sh 7d6bc5195a583b3979a2533b50708978d96981d3d9ac59b266055246b6 fad329 % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7d6bc5195a58 chapter2 "./entrypoint.sh" 2 seconds ago Up 1 second 0.0.0.0:8086->80/tcp chapter2 % ./stop.sh chapter2 % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES %
shell.sh
脚本运行容器并启动 Bash shell,这样你可以使用命令行程序来诊断容器在构建时出现的问题:
#!/usr/bin/env bash
# shell.sh
# This script starts a shell in an already built container. Sometimes you need to poke around using the shell # to diagnose problems.
# stop any existing running container ./stop.sh
# fire up the container with shell (/bin/bash)docker run -it --rm --name chapter2 chapter2 /bin/bash
以下代码片段展示了 shell.sh
脚本的作用:
% ./shell.sh Error response from daemon: No such container: chapter2 root@f10092244abe:/home/app# ls -l total 44 -rw-r--r-- 1 root root 871 Dec 13 10:28 Dockerfile -rw-r--r-- 1 root root 808 Dec 5 14:56 README.md -rwxr-xr-x 1 root root 38 Dec 4 12:15 build.sh -rwxr-xr-x 1 root root 197 Dec 4 16:12 debug.sh -rwxr-xr-x 1 root root 411 Dec 13 10:28 entrypoint.sh -rw-r--r-- 1 root root 75 Dec 2 17:31 install-virtualbox-macos.sh -rwxr-xr-x 1 root root 315 Dec 13 10:26 persist.sh -rw-r--r-- 1 root root 860 Dec 4 16:24 php.conf drwxr-xr-x 1 root root 18 Dec 13 10:27 public_html -rwxr-xr-x 1 root root 152 Dec 5 13:01 run.sh -rwxr-xr-x 1 root root 308 Dec 4 17:40 shell.sh -rwxr-xr-x 1 root root 115 Dec 4 17:41 stop.sh root@f10092244abe:/home/app# ls -ldg /data drwxrwxrwx 1 root 0 Dec 13 10:28 /data root@f10092244abe:/home/app# exit %
我们可以看到 /data
被创建并且具有全局写权限。
这几段 sh
脚本足以让你开始开发并使用你自己的容器。随着你使用 Docker,你可能会编写更多自己的脚本!然而,我们将在 第四章《使用容器构建系统》中看到一种无需 sh
脚本即可与 Docker 配合使用的方法。
摘要
在本章中,我们了解了如何使用 VirtualBox 在工作站上创建虚拟机,以及如何利用它在虚拟机中运行 Windows(或 Linux 或其他操作系统)。我们还学习了足够的 Docker 知识,能够用它构建我们的第一个应用程序。
本章是在 Windows 10 操作系统下编写的,运行在 VirtualBox 虚拟机中,并且该虚拟机运行在 Arch Linux 主机上。Windows 中使用了 Microsoft Word,而 Docker 命令和脚本则在 Arch Linux 主机上运行和编辑。
我们演示了如何构建一个类似 LAMP 的应用程序,虽然没有 MySQL,并且如何将其容器化。我们可以将源代码目录从主机挂载到容器中,这样我们可以编辑文件并在容器中即时查看更改。我们还学习了如何持久化数据,这意味着停止和启动容器时,重要文件和状态会被保留。
在下一章中,我们将探索 Docker Hub,并构建一个需要多个容器的更复杂的应用程序。
进一步阅读
-
这个 URL 是官方的 Docker 文档:
docs.docker.com
-
这个 URL 是关于 Dockerfile 参考资料的:
docs.docker.com/engine/reference/builder/
-
这个 URL 是关于 Docker
ps
命令文档的:docs.docker.com/engine/reference/commandline/ps/
-
这个 URL 是关于 Docker 中卷和存储的文档:
docs.docker.com/storage
-
这个 URL 是关于 Docker
run
命令文档的:docs.docker.com/engine/reference/run/
-
这个 URL 是关于 Docker
restart
命令文档的:docs.docker.com/engine/reference/commandline/restart/
-
这个 URL 是关于 Docker
stop
命令文档的:docs.docker.com/engine/reference/commandline/stop/
第三章:使用 Docker Hub 共享容器
在上一章中,我们学习了如何构建一个容器并在工作站上运行它。我们使用了一个 Debian 镜像作为起点,但这个镜像来自哪里呢?答案是,它来自 Docker Hub。Docker Hub 是 Docker 的官方容器镜像库,由为我们带来 Docker 本身的团队运营。
容器库包含了许多程序、服务器、服务等的官方镜像,这些程序你可以安装到自己的容器中。例如,有各种 Linux 发行版的官方镜像,版本的Node.js、MySQL 和 MongoDB 等。
你可以将 Docker Hub 视为类似 GitHub。你可以浏览现有的组织和预制容器,还可以上传自己的容器并创建自己的组织。
我们将演示如何使用 Docker Hub 网站搜索并获取第三方容器的信息,以便在你的应用程序中使用。我们还将演示如何通过命令行使用 Docker Hub 中的第三方容器。我们将使用 Docker Hub 上的官方 MongoDB 容器,该容器由 MongoDB, Inc.发布。
整个后端应用程序可以通过多个 Docker 容器协作来实现。这种应用程序结构允许每个自定义容器的实现保持简单和最小化。我们将应用微服务架构来构建一个简单的应用程序。这展示了容器如何协同工作来创建一个完整的工作应用程序。最后,我们将看到如何使用 Docker Hub 与第三方和开发团队共享你已经准备好的生产环境容器。
在本章中,我们将涵盖以下主题:
-
介绍 Docker Hub
-
实现我们的应用程序的 MongoDB 容器
-
介绍微服务架构
-
实现一个示例微服务应用程序
-
在 Docker Hub 上共享你的容器
技术要求
唯一的技术要求是主机上安装了 Docker,以及一个浏览器,如 Google Chrome、Firefox 或 Microsoft Edge。这是 Docker 的最佳部分之一——你不需要在主机上安装复杂的服务器/服务;我们将它们安装在 Docker 容器中。
我们已准备好可以直接使用而无需修改的示例,这些示例存放在一个公共的 GitHub 仓库中,可以在github.com/PacktPublishing/Docker-for-Developers
找到。
查看以下视频,看看代码是如何在实际中运作的:
介绍 Docker Hub
通常,你会通过命令行或 Dockerfile 与 Docker Hub 进行交互,但你也可以通过 Docker Hub 网站(hub.docker.com
)来搜索任何你知道需要使用的预构建容器。你还可以利用该网站发现一些可能对你有用的预构建容器。
一般来说,你会从 Docker Hub 上的一些预构建 Docker 容器继承,以创建你自己的自定义容器。例如,你可能会从一个 Linux 发行版容器继承,并在该继承的/自定义容器中安装你项目所需的软件。
当你从 Linux 发行版继承时,该发行版的一些基础软件包将会被安装。如果你从 Debian 系列的 Linux 容器继承,你将能够在容器内使用 apt
包管理器来安装软件,就像你在专用机或虚拟机上运行该 Debian 系列 Linux 容器一样。
一些预构建容器继承自某种 Linux 发行版,并提供与该产品特定的软件包。比如,当你从 Node.js 容器继承时,该 Node.js 容器可能继承自某个 Linux 发行版容器,并且已经安装了 Node.js、npm
和 yarn
。
从命令行与 Docker Hub 交互
查看 Docker Hub 和 Docker 协同工作最简单的方法就是运行官方的 hello-world
容器。从 Docker Hub 运行容器的命令是 docker run name-of-container
;我们可以输入 docker run hello-world
:
# docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 1b930d010525: Pull complete Digest: sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e 4194f4ee23e415e1064 Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps: 1\. The Docker client contacted the Docker daemon. 2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3\. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4\. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Docker 没有在本地容器缓存中找到该容器,所以它会自动下载并在 Docker 引擎中运行。容器中的代码很简单——它只是打印出前面的消息。
注意
你可以以相同的方式运行在 Docker Hub 网站上找到的任何容器!
如果你的输出结果与前面的输出不一致,那可能是你的 Docker 安装存在问题,或者从你的主机无法访问 Docker Hub 服务器。一个可能的问题是你的 Docker 安装要求你以 root 或管理员身份运行 docker
命令。
安装说明可以在 docs.docker.com/install/
找到,而 Docker 的安装后说明可以在 docs.docker.com/install/linux/linux-postinstall/
找到。这些安装后说明解释了如何设置 Docker,以便你可以作为非 root 用户管理它。
使用 Docker Hub 网站
让我们去 Docker Hub 找找 hello-world
容器页面—hub.docker.com/_/hello-world
。页面大概会是这个样子:
图 3.1 – Docker Hub 上的 hello-world 镜像页面
这通常是你在 Docker Hub 上看到的大多数共享容器的典型情况。像 MongoDB 这样的特定软件包被封装在容器中,提供了各种版本的软件镜像。这使得你能够处理依赖于特定版本 Docker Hub 包的软件。
MongoDB 在 Docker Hub 上的页面是hub.docker.com/_/mongo
。要找到它,只需在hello-world(或其他任何包)页面顶部的搜索框中输入mongodb
,然后从搜索结果页面中选择它。你可以使用搜索框查找任何你需要的共享镜像。
页面中的简单标签和共享标签部分是重点。MongoDB 的各个版本镜像都使用了简单标签和共享标签进行标记。
例如,3.4-xenial简单标签意味着 MongoDB 3.4 版本的镜像运行在 Ubuntu Xenial 容器中。
3.4共享标签意味着有适用于多个主机操作系统(通常是 Windows Server、Linux 或 macOS)的 MongoDB 3.4 版本的镜像。Docker 守护进程会选择适合主机操作系统的镜像。
截至本文写作时,已有 MongoDB 3.4、3.6、4.0 和 4.2 的主要版本镜像,以及这些主要版本的次要点版本镜像:
图 3.2 – hello-world 的简单标签和共享标签
查找可用的第三方预构建容器的过程是相同的。例如,您可以搜索 Redis,您将得到一个类似的页面,其中包含有关可用 Redis 容器的详细信息。
为我们的应用实现 MongoDB 容器
我们可以通过实现 MongoDB 容器,探索使用 Docker Hub 上预构建的容器。稍后我们将把这个容器作为演示应用程序的一部分,这个应用程序由多个容器组成,并且它们协同工作。
我们将使用 MongoDB 的官方 Docker 镜像,可以在 Docker Hub 网站上找到,网址是hub.docker.com/_/mongo
。我们将创建一个.sh
脚本,在 Docker 中启动我们的镜像,以便启动过程简便且可重复。
我们在第二章《使用 VirtualBox 和 Docker 容器进行开发》中了解到,我们可以将容器的网络端口暴露给主机。这意味着我们可以在 Docker 中运行 MongoDB 容器镜像,并通过访问主机上的 MongoDB 端口,访问容器内正在运行的 MongoDB 服务器。
在这本书的 GitHub 仓库中 (github.com/PacktPublishing/Docker-for-Developers
),有一个 chapter3/
目录,它是本章的配套内容。在这个目录中有一个 shell 脚本,start-mongodb.sh。这个脚本比我们在上一章中使用的简单脚本稍微复杂一点。我们将使用环境变量来配置 MongoDB,并且我们将使用主机上的一个目录来存储 MongoDB 的数据文件—这样备份数据就像将这些文件复制到备份介质上一样简单:
#!/bin/bash
# start-mongodb.sh
SERVICE=mongodb # name of the service
# You can set these in this script (uncomment and edit the lines) or set them in your .zshrc/.bashrc/etc.
# Change this to an EXISTING directory on the HOST where the mongodb database files will be created #!/bin/bash
# start-mongodb.sh
SERVICE=mongodb # name of the service
# Change this to an EXISTING directory on the HOST where the mongodb database files will be created and maintained.
#MONGO_DATADIR="$HOME/data"
# Stop any running MongoDB container, remove previous container, pull newer version
docker stop $SERVICE
docker rm $SERVICE
docker pull mongo:3.4
# Now we run it!
docker run …
你确实需要一个 Dockerfile 来创建容器镜像。然而,如果你使用的是来自 Docker Hub 的预构建容器镜像(如 MongoDB)并且是独立的,那么你就不需要 Dockerfile。MongoDB 的开发者们在将镜像上传到 Docker Hub 之前,会使用 Dockerfile 来生成这些镜像。
事实上,你可以从 MongoDB 在 Docker Hub 页面中的 Supported tags 部分看到,他们生产并支持了不少镜像,包括不同版本的镜像—有些是 Windows 操作系统版的,有些是 Linux 版的,等等。MongoDB 的开发者们肯定有很多 Dockerfile—每个镜像一个!
我们必须为 start-mongodb.sh 提供一个环境变量:MONGO_DATADIR
,这是你工作站上一个现有的目录,用于存储 MongoDB 在容器中的数据文件。设置这个变量有几种方法:
-
你可以将
export MONGODB_DATADIR=/path/to/data/dir
添加到你的 shell 启动文件中(如.zshrc
、.bashrc
等)。 -
你可以在运行脚本之前手动在终端中进行
export
(环境变量)操作。 -
你可以在命令行中运行 start-mongodb.sh 脚本时设置环境变量的值:
# MONGODB_DATADIR=~/data ./
start-mongodb.sh。 -
你可以取消注释 start-mongodb.sh 脚本文件中设置
MONGO_DATADIR
的那一行,并编辑它,将其设置为每次运行脚本时你希望使用的数据目录。
start-mongodb.sh 脚本中的最后一行是一个单独的命令行。行末的反斜杠(\
)字符表示该行正在继续或与下一行连接。这个命令是启动容器的命令。可以想象,如果每次都必须输入这么长的命令来启动 MongoDB 容器,那会非常麻烦。.sh
脚本让这个过程变得相当轻松:
docker run \
--name $SERVICE \
-d \
--restart always \
-e TITLE=$SERVICE \
-p 27017:27017 \
-v "$MONGO_DATADIR":/data/db \
mongo:3.4
让我们来看一下前面命令的不同部分:
-
docker run
命令为正在运行的mongodb
容器命名。 -
-d
开关将容器以分离模式运行。容器将在工作站重启时自动启动。 -
-e
开关允许你将环境变量传递给容器;在这种情况下,我们传递了TITLE=mongodb
环境变量。如果你想传递多个变量,可以使用多个-e
开关。 -
-p
开关将容器中的端口27017
映射到主机上的端口27017
。你可以将容器中的暴露端口重新映射到主机上的不同端口号。如果你已经在容器或主机上运行 MongoDB 服务器,你会这样做。然而,Docker 使我们能够灵活地始终在容器中运行 MongoDB,因此我们永远不必在主机上安装它。我们可能希望在主机上安装 MongoDB 客户端程序,以便使用 MongoDB REPL/shell 访问 MongoDB。一旦主机上暴露了端口
27017
,任何程序都可以访问 MongoDB 数据库,就像它在主机上运行一样。 -
-v
开关将主机上的一个目录映射到容器中 MongoDB 将管理其数据库和其他文件的目录。 -
我们选择从 Docker Hub 下载并运行
mongo:3.4
(标签/版本 3.4)。注意
docker run
命令仅在容器尚未存在于你的工作站中,或者如果 Docker Hub 上的容器镜像较新时,才会从 Docker Hub 下载容器。
你可以用相同的方式运行你在 Docker Hub 上找到的任何容器!
让我们通过以下命令来运行脚本:
# mkdir -p ~/mongodb
# MONGO_DATADIR=~/mongodb ./start-mongodb.sh
以下输出包含一些关于无法停止名为 mongodb
的已运行容器的警告(这是预期的):
# mkdir -p ~/mongodb && MONGO_DATADIR=~/mongodb ./start-mongodb.sh
stopping mongodb Error response from daemon: No such container: mongodb removing old mongodb Error: No such container: mongodb pulling mongodb 3.4: Pulling from library/mongo 976a760c94fc: Pull complete c58992f3c37b: Pull complete 0ca0e5e7f12e: Pull complete …
3757d63ce2b9: Pull complete Digest: sha256:4c7003e140fc7dce5f12817d510b5a9bd265f2 c3bbd6f81d50a60cc11f6395d9 Status: Downloaded newer image for mongo:3.4 docker.io/library/mongo:3.4 e3854f6931e1aa4b64557d5a54e652653123f84a 544fedf39a5cf68d2ee9d0af # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e3854f6931e1 mongo:3.4 "docker-entrypoint.s…" 5 seconds ago Up 3 seconds 0.0.0.0:27017->27017/tcp mongodb #
Docker 拉取了正确的 MongoDB 镜像,并在 Docker 引擎中后台运行。你可以观察到以下内容:
-
MongoDB 镜像由几个已经下载的层组成(
Pull complete
)。 -
工作站上已经有一个现存的(但较旧的)镜像(
Downloaded newer image…
)。 -
容器是通过
docker ps
命令运行的。
如果容器遇到错误,可能会退出并在输出中打印诊断信息。你可以在容器中运行一个 shell 以进行取证诊断。
在容器中运行 shell
通常,你会在容器中运行一个 shell,以便你可以进一步了解容器的环境。例如,可能在你的 Dockerfile 中存在 bug——比如忘记将某个文件复制到容器中。你可以在容器中运行一个 shell,列出目录,你会发现文件缺失。
在 MongoDB 容器的情况下,你可能希望在容器内部运行 MongoDB 客户端命令。MongoDB 容器的 Docker Hub 页面说明我们可以通过简单地连接到正在运行的容器来运行客户端命令(hub.docker.com/_/mongo
)。来自 MongoDB Docker Hub 页面的命令如下:
docker exec -it mongodb bash
该命令的不同部分如下:
-
docker exec
在运行中的容器中执行命令(docs.docker.com/engine/reference/commandline/exec/
)。 -
-it
选项指定 Docker 以交互模式运行容器——这意味着它从键盘获取输入并将输出发送到终端窗口。
在容器内,我们可以使用ls
命令列出目录:
# docker exec -it mongodb bash root@e3854f6931e1:/# ls bin data docker-entrypoint-initdb.d etc js-yaml.js lib64 mnt proc run srv tmp var boot dev entrypoint.sh home lib media opt root sbin sys usr
我们可以通过在容器内使用ps
命令查看 Docker 容器是否正在运行:
root@e3854f6931e1:/# ps -aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND mongodb 1 0.7 0.0 954676 62028 ? Ssl 22:37 0:02 mongod root 40 2.8 0.0 18240 3248 pts/0 Ss 22:41 0:00 bash root 51 0.0 0.0 34420 2848 pts/0 R+ 22:41 0:00 ps -aux root@e3854f6931e1:/#
我们可以在容器内运行命令行 MongoDB 工具。我们不需要在工作站上安装这些工具!在这里,我们运行 MongoDB 命令,然后在 Mongo REPL 内运行show collections
和show databases
命令:
root@e3854f6931e1:/# mongo MongoDB shell version v3.4.23 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.23 Welcome to the MongoDB shell.For interactive help, type "help".For more comprehensive documentation, see http://docs.mongodb.org/Questions? Try the support group http://groups.google.com/group/mongodb-user Server has startup warnings:2019-12-13T22:37:12.342+0000 I CONTROL [initandlisten]2019-12-13T22:37:12.342+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.2019-12-13T22:37:12.342+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.2019-12-13T22:37:12.342+0000 I CONTROL [initandlisten]> show collections > show databases admin 0.000GB local 0.000GB >root@e3854f6931e1:/# exit
一切准备就绪——MongoDB 正在运行,我们可以使用 REPL。show collections
命令没有返回任何集合,因为我们还没有创建任何集合。show databases
命令显示 MongoDB 默认有两个数据库:admin
和 local
。
docker logs
命令显示容器的stdout
和stderr
输出:
# docker logs mongodb 2019-12-13T22:37:09.161+0000 I CONTROL [initandlisten] MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=e3854f6931e1 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] db version v3.4.23 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] git version: 324017ede1dbb1c9554dd2dceb15f8da3c59d0e8 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] OpenSSL version: OpenSSL 1.0.2g 1 Mar 2016 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] allocator: tcmalloc 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] modules: none 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] build environment:2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] distmod: ubuntu1604 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] distarch: x86_64 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] target_arch: x86_64 2019-12-13T22:37:09.162+0000 I CONTROL [initandlisten] options: {}2019-12-13T22:37:09.165+0000 I STORAGE [initandlisten] wiredtiger_open config: create,cache_size=31491M,session_max=20000,eviction=(threads_min=4,threads_m ax=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(w ait=60,log_size=2GB),statistics_log=(wait=0),verbose=(recovery_progress),2019-12-13T22:37:14.335+0000 I INDEX [initandlisten] building index using bulk method; build may temporarily use up to 500 megabytes of RAM 2019-12-13T22:37:14.342+0000 I INDEX [initandlisten] build index done. scanned 0 total records. 0 secs 2019-12-13T22:37:14.344+0000 I COMMAND [initandlisten] setting featureCompatibilityVersion to 3.4 (
…
你可能会使用docker logs
命令查看容器的调试输出。
从我们之前的日志中可以看到,MongoDB 似乎在容器内运行得很好。没有打印任何错误信息。
你可以使用docker logs
命令加上-f
命令行选项来跟踪日志文件。当命令处于跟踪模式时,应用程序运行时写入日志的任何新行都会追加到屏幕显示中。
到目前为止,我们已经探索了如何使用 Docker 运行复杂的服务器应用程序(MongoDB),而无需在工作站上安装 MongoDB。通过 Docker,我们可以访问 MongoDB。
我们可以使用我们的.sh
脚本启动 MongoDB,也可以停止它——我们可以随时这样做,这样就不必始终让 MongoDB 在后台运行。
现在我们知道如何运行 Docker 容器,让我们来看一下如何处理多个协同工作的容器。
介绍微服务架构
Docker 和 Docker Hub 支持使用微服务架构进行开发。该架构强调构建和运行专注于应用程序整体单一方面的容器。当所有容器都在运行时,你就有了完整的后端应用程序。这些容器可以很复杂,比如一个完整的数据库服务器,或者很简单,比如一个简短的 shell 脚本。理想情况下,你为应用程序实现的容器应该是简单的、短小的并且专注的。你编写的每个微服务应该易于调试,因为你不需要很多行代码。
假设我们想开发一个使用 MongoDB 和 Redis 的后端应用程序,并且其应用代码是使用 Node.js 编写的。我们可以选择创建一个 Dockerfile,并从 MongoDB 镜像开始。然后通过使用 apt
安装 Redis,并将我们的程序添加到其中,就像我们在 第二章 中做的那样,使用 VirtualBox 和 Docker 容器进行开发。使用这种方法创建应用程序的问题是,当你因开发原因停止容器时,也会停止正在运行的 MongoDB 和 Redis 服务器。
与其使用一个包含所有安装内容的单体容器,你可以分别运行 MongoDB、Redis 和自定义应用程序容器。你甚至可以将你的自定义应用程序分成多个容器。你所需要的只是一个机制来实现应用程序容器之间的通信。
注意
在你的设计中,最好避免使用单体容器!虽然看起来像 MongoDB 这样的大型复杂程序可能是单体的,但它实际上只是一个你可以作为微服务使用的专用服务。
现在我们对微服务架构有了简要的了解,我们可以检查容器作为微服务的一些好处和要求。
可扩展性
可扩展性几乎总是后端实现中的一个重要考虑因素。例如,如果有足够多的人同时尝试从一个简单的 HTTP/WWW(网页)服务器上获取我们的页面,它可能会停止响应。因此,服务器农场的存在是为了你可以部署两个或更多这样的 HTTP/WWW 服务器,复制我们的页面服务功能。对于一个双服务器农场,你基本上能得到比单个服务器更多的用户在同时访问页面。随着流量的增长——例如,如果网站变得更受欢迎——你可以增加第三台服务器,然后是第四台服务器,以此类推。后端提供页面的能力会随着你的需求增长。
在微服务架构中,我们实现类似的可扩展性方式。我们可以运行多个 MongoDB 容器实例,以提高数据库操作的容量。唯一的诀窍是将 MongoDB 配置为集群或分片,并使应用程序容器使用这种数据库配置。
容器间通信
容器间通信通常涉及一些技术,允许消息从一个容器发送到另一个容器,并将响应或状态返回。能够在运行的容器之间进行通信可以通过几种技术实现,包括以下几种:
-
套接字
-
文件系统
-
数据库记录
-
HTTP
-
MQTT
现在让我们来讨论一下它们。
使用套接字
使用套接字是容器之间通信的一种复杂方法。如果您有五个容器,您可能需要为每个容器创建五个套接字,以提供容器间的通信路径。当您进行扩展时,每个容器需要创建更多的套接字,您实际上希望能够自动化这个过程。这涉及到相当多的业务逻辑。
使用文件系统
使用文件系统的方法涉及在所有容器之间共享某些内容,例如网络驱动器。要发送消息,容器将消息写入文件系统中的文件。要接收消息,容器从文件系统中的文件读取消息。接收方需要轮询或反复检查文件系统,以检测文件是否已被写入。这种方法并不理想,因为我们并不希望以这种方式共享网络驱动器——性能会较慢。
注意
轮询是一种编程技术,指的是持续检查机器状态(例如文件是否发生了变化)。
使用数据库记录
使用数据库记录的方法类似于文件系统方法,不同之处在于要发送的消息仅仅是写入到数据库中的记录,接收方只需轮询数据库记录的变化。一些数据库提供通知机制,告知客户端(接收方)数据库已发生变化。
文件系统和数据库方案都需要相当多的业务逻辑和调试。您必须考虑发送和接收消息的顺序,并避免由于旧消息被数据库或文件系统中的新消息覆盖而导致丢失消息。
使用 HTTP
HTTP 是一种无状态协议,因此您不需要维护一组开放的套接字来进行通信。该协议定义明确,且可供人类阅读(例如文本格式)。要发送消息,您只需向您希望通信的容器发送一个 HTTP 请求,并等待响应。您可以根据 HTTP 协议的规定关闭或保持连接(保持活跃)。此外,为了避免通过 HTTP 轮询消息或状态变化,您可以使用 WebSockets。
使用 MQTT
MQTT 是一个设计良好的消息总线。它的工作方式类似于 IRC 或 Slack,您有房间(主题)和在房间中的人(订阅者)。发送到房间(主题)的消息会被房间中的人(订阅者)接收。人们(订阅者)可以加入多个房间(主题),并接收那些房间(主题)的消息。
对于一个 MQTT 应用,必须有一个可以从其他容器访问的 MQTT 服务器(代理)容器。其他容器无需彼此了解,只需要知道 MQTT 代理的地址。
MQTT 代理接受来自一个或多个客户端的连接。客户端可以订阅一个或多个主题。主题就像 IRC 或 Slack 中的频道/房间名称一样是任意的,通常是字符串。当向 MQTT 代理发送某个特定主题的消息时,代理会将该消息发送给所有订阅了该主题的客户端。
Mosca(hub.docker.com/r/matteocollina/mosca
)是一个用 JavaScript 编写的 MQTT 代理。你可以像运行 MongoDB 或 Redis 一样在容器中运行它。
还有几个其他的 MQTT 代理可供选择,你可以在 Docker Hub 上找到它们。
HTTP 与 MQTT
MQTT 是一种专门为传递键/值对消息设计的协议。它的优势在于其广播能力。每个客户端都负责根据其关心的特定键请求修改值。每个客户端可以确保它们的更新会被任何其他感兴趣的客户端接收。MQTT 还具有保留特定键/值对的功能,因此当新客户端订阅时,它可以收到当前的键/值对(最近发送的那个)。
MQTT 并没有提供请求/响应协议,尽管实现一个很简单。使用 MQTT 进行请求/响应类型事务的缺点是,响应不能保证尽快发生。
HTTP 需要自定义编程才能提供 MQTT 所提供的消息传递服务。你可以实现一个类似消息总线的系统来模拟 MQTT 的功能,但这意味着你需要更多的编程工作,并且未来还会增加维护成本。HTTP 的优势在于它是一个请求/响应协议,因此你通常可以立刻收到响应。缺点是,如果服务器正在维护一组键/值对,你将需要从客户端轮询服务器,查看值是否发生了变化,并将更新的值提交给服务器。轮询会导致服务器消耗 CPU,即使值没有变化,这可能会导致服务器在足够多的客户端频繁轮询时崩溃。你可以使用 WebSockets,但最终你重新发明了 MQTT。
如果你需要比 MQTT 提供的更多功能,HTTP 是一个不错的选择。当然,HTTP 支持 PHP 或Node.js(以及其他语言)后端服务。
HTTP 和 MQTT 是可以结合使用的。使用 HTTP 进行请求/响应类型事务,使用 MQTT 进行状态更新。
MQTT 对于我们的需求来说是一个不错的选择。
附带的 GitHub 仓库中的chapter3/
目录包含一个简单的基于微服务的后端演示应用。它使用 MongoDB、Redis 和 MQTT,并包括一些发布者和订阅者应用程序,你可以在本书的 GitHub 仓库中找到它们(github.com/PacktPublishing/Docker-for-Developers
)。在本章的后面,我们将学习如何通过 Docker Hub 共享我们的订阅者和发布者容器。
实现一个示例的微服务应用
我们可以使用 Mosca、MongoDB 和 Redis 容器,结合一些自定义容器,来实现一个简单但完整的应用:
图 3.3 – 我们的示例微服务应用程序图
发布者和订阅者将通过 MQTT 进行通信。订阅者将监听若干 MQTT 主题,指示它操作或从 MongoDB 和 Redis 数据库中检索信息。发布者将发送这些 MQTT 主题并打印响应。
发布者将基于 Node.js 版本 11,订阅者将基于 Node.js 版本 12。没有 Docker 或虚拟机的情况下,在同一台机器上并行运行两个 Node.js 版本需要使用 Node 版本管理器(nvm)并在工作站上安装多个版本的 Node.js。Docker 容器使得使用所需的多个版本变得简单,并将版本与使用该版本的应用程序一起打包成一个便捷的包(容器)。
发布者和订阅者应用程序位于伴随仓库中 chapter3/
目录下各自的 publisher/
和 subscriber/
子目录中。这些程序各自需要一个 Dockerfile,以便我们可以构建两个独立的容器。它们还各自有自己的辅助 .sh
脚本(debug.sh,run.sh,build.sh 等)。发布者应用只需要一个 MQTT 库。订阅者应用需要 MQTT 库、MongoDB 库和 Redis 库。这些库将通过 npm
(Node.js 包管理器)在容器内安装。
发布者和订阅者应用程序演示了微服务架构如何工作,使用多个 Docker 容器。
订阅者通过 Node.js 包/库连接到 MongoDB 和 Redis 容器,这些包/库通过 npm
在容器中安装。订阅者提供基本的 创建、读取、更新和删除(CRUD)功能,用于在 MongoDB 和 Redis 数据库中添加、列出、删除和获取记录数量。发布者通过 MQTT 消息向订阅者发送请求,以调用这些功能。
我们的主题是基于一种模式派生的字符串:container/command。如果我们想与订阅者通信,模式是 subscriber/command。如果我们想与发布者通信,模式是 publisher/command。这个约定使得每个微服务希望订阅或发布的主题一目了然。
MQTT 主题和消息如下:
-
subscriber/mongo-count
:返回 MongoDB 数据库中记录的数量。 -
subscriber/mongo-add
:将消息内容添加到 MongoDB 数据库。 -
subscriber/mongo-list
:返回一个包含 MongoDB 数据库中记录列表的 JSON 对象。如果消息是一个非零长度的字符串,则用于过滤返回的记录列表。 -
subscriber/mongo-remove
:从 MongoDB 数据库中删除一条记录。消息可以是适合传递给 MongoDB 的collection.deleteOne()
方法的字符串或对象(JSON)。 -
subscriber/mongo-removeall
:删除 MongoDB 数据库中的所有记录。 -
subscriber/redis-count
:返回 Redis 数据库中记录的数量。 -
subscriber/redis-flushall
:删除 Redis 数据库中的所有记录。 -
subscriber/redis-set
:向 Redis 数据库添加一条记录;消息是key=value
形式。 -
subscriber/redis-list
:列出 Redis 数据库中的所有记录,并返回一个记录的 JSON 数组。 -
subscriber/redis-del
:从 Redis 数据库中删除一条记录。 -
subscriber/commands
:返回可用命令(MQTT 主题)的列表。
在chapter3/
目录的根目录下有一些 Shell 脚本,分别启动 Redis(start-redis.sh)、MongoDB(start-mongodb.sh)和 Mosca MQTT 代理(start-mosca.sh),还有一个脚本start-all.sh,可以启动所有三个服务。
我们之前已经详细介绍了start-mongodb.sh脚本的工作原理。start-redis.sh和start-mosca.sh脚本大致相同;只是启动的程序(Redis 和 Mosca)名称有所不同。
需要注意的是,start-mongodb.sh脚本将主机的端口27017
连接到容器的端口27017
。这样,其他容器可以通过默认端口访问 MongoDB。start-mosca.sh脚本将端口1883
和80
连接到主机,以便可以从任意容器使用 MQTT 和 WebSocket 上的 MQTT。start-redis.sh脚本将端口6379
连接到主机,以便容器通过 Redis 的默认端口访问 Redis。当然,主机也可以访问任何容器。
subscriber/start-subscriber.sh
和publisher-start-publisher.sh脚本都在主机上本地运行应用程序,而不是在容器中运行。这允许使用 WebStorm 或其他 IDE,或Node.js调试器进行主机本地调试功能。在下一章中,将详细介绍如何在 Docker 容器内完全开发和调试发布者和订阅者。
注意
要使用start-subscriber.sh和start-publisher.sh脚本,您需要在开发工作站上安装Node.js和yarn
。确保在subscriber/
和publisher/
目录下分别运行yarn install
。
这就是start-subscriber.sh的样子:
#!/bin/sh
# start-subscriber.sh
yarn start
start-publisher.sh脚本与start-subscriber.sh脚本完全相同。发布者目录中的package.json
文件指示yarn start
来启动发布者程序。
HOSTIP
变量必须设置为主机机器的 IP 地址,这个地址对我们的发布者和订阅者可见,并且在我们的 Node.js 程序中用于连接 MQTT 代理、MongoDB 服务器和 Redis 服务器。
在 macOS 上查找 IP 地址(假设你使用192.168.*.*
作为家庭网络 IP 地址范围):
# ifconfig | grep 192
inet 192.168.0.19 netmask 0xffff0000 broadcast 192.168.255.255
此主机的 IP 地址是192.168.0.19
。
在 Linux 上查找 IP 地址,使用以下命令:
$ ip address | grep 192
inet 192.168.0.21/16 brd 192.168.255.255 scope global dynamic enp0s31f6
此主机的 IP 地址是192.168.0.21
。
你将使用以下命令运行start-publisher.sh
脚本:
HOSTIP=192.168.0.19 ./start-publisher.sh
要运行start-subscriber.sh
脚本,使用以下命令:
HOSTIP=192.168.0.19 ./start-subscriber.sh
发布者程序相对简单。它连接到 MQTT 代理并监听以publisher/
开头的主题。接收到的主题和消息会被转换成subscriber/
格式的主题,并发布到 MQTT。订阅者会响应publisher
主题和响应消息。
在发布者和订阅者都运行的情况下,我们使用 MQTT 命令行工具向发布者发送消息。在以下截图中,你可以看到我们如何执行一些订阅者命令。
这两个脚本假设我们已经在主机上安装了 Mosca。我们不需要为 MQTT 代理安装它,但需要它来使用命令行工具。从主机的命令行、.sh
脚本或主机上的 crontab 中发送 MQTT 主题/命令非常有用。你也可以将 Mosca 作为库来实现你自己的Node.js代码中的代理。
注意
对于有好奇心的读者,截图显示的是运行 tmux 的终端窗口,分为三个窗格。tmux是一个终端复用器:它允许在一个屏幕上创建、访问和控制多个终端。tmux 的 GitHub 仓库可以在github.com/tmux/tmux
找到。
在以下截图中,你可以看到我们如何执行一些订阅者命令:
图 3.4 – 三个终端展示发布者和订阅者的协同工作
正如我们所看到的,发布者和订阅者按预期工作,容器与主机之间的数据库查询也正常。我们可以编辑和调试发布者和订阅者程序,直到它们按我们的需求正常工作。
现在我们已经有了正常工作的发布者和订阅者容器,接下来我们希望将它们与开发团队的其他成员共享。
在 Docker Hub 上共享你的容器
为了共享我们的容器,我们将使用 Docker Hub 并发布这两个容器。团队的其他成员可以从 Docker Hub 拉取预构建的容器并使用它们,而无需处理源代码库。他们对这些容器来说只是微服务,就像我们使用 Mosca、MongoDB 或 Redis 容器时并不需要源代码一样。
当然,开发团队需要运行这些脚本。
我们在 Docker Hub 上创建了一个名为 dockerfordevelopers
的组织,我们将用它来发布本书的容器。你无法推送到该组织,但我们可以。为了向 Docker Hub 发布,你需要使用 docker login
命令,并且必须已经在 hub.docker.com/
上创建了帐户。
你也可以在 Docker Hub 上创建自己的组织,以便共享你自己的容器。如果你想使用本章 GitHub 仓库中的示例,你需要编辑脚本,将 dockerfordevelopers
替换为你自己的组织名称。
由于我们正在创建自己的自定义容器,我们需要为每个容器准备一些 .sh
脚本,正如上一章所述。发布者和订阅者各自有一套 .sh
脚本。
用于构建发布者容器的 Dockerfile 与上一章使用的几乎完全相同:
# we will inherit from the NodeJS v12 image on Docker Hub
FROM node:12
# set time zone so files' timestamps are correct
ENV TZ=America/Los_Angeles
# we include procps and telnet so you can use these with shell.sh prompt
RUN apt-get update -qq >/dev/null && apt-get install -y -qq curl procps telnet >/dev/null
# add a user - this user will own the files in /home/app
RUN useradd --user-group --create-home --shell /bin/false app
# set up and copy files to /home/app
ENV HOME=/usr/app
WORKDIR /home/app
COPY . /home/app
# install our NodeJS packages (from package.json)
RUN yarn install
# we run a script to stat the server; the array syntax makes it so ^C will work as we want
CMD ["yarn", "start"]
这个 Dockerfile 与上一章中的一个主要区别是,我们没有安装 Apache 和 PHP,而是从 node:12
继承并安装了我们 Node.js 程序所需的包。
在这个发布者的 Dockerfile 中,我们从 node:12
继承。订阅者的 Dockerfile 完全相同,只不过它是从 node:13
继承。这说明了你如何在同一主机上运行使用不同基础软件版本的容器;如果没有容器,这将是非常不便的。
注意
node:12
和 node:13
容器是从 Docker Hub 拉取并在每次构建容器时进行更新的。
以下是用于构建发布者的 build.sh 脚本:
#!/bin/sh
# build.sh
# we use the "docker build" command to build a container named "dockerfordevelopers/publisher" from . (current directory)
# Dockerfile is found in the current directory, and determines how the container is built.
docker build -t dockerfordevelopers/publisher .
build.sh 脚本非常简短,实际上只包含一行命令。输入 ./
build.sh 比完整输入 docker build -t dockerfordevelopers/publisher .
命令要容易。这也使得整个过程更不容易出错,而且你不必记住命令行选项和格式。
对于订阅者,亦有一个几乎相同的 build.sh 脚本。唯一不同的是构建的容器名称:dockerfordevelopers/subscriber
。
发布者的 build.sh 脚本输出如下:
# ./build.sh
Sending build context to Docker daemon 4.902MB
Step 1/9 : FROM node:12
Step 2/9 : ENV TZ=America/Los_Angeles
Step 3/9 : RUN apt-get update -qq >/dev/null && apt-get install -y -qq curl procps telnet >/dev/null
Step 4/9 : RUN useradd --user-group --create-home --shell /bin/false app
Step 5/9 : ENV HOME=/usr/app
Step 6/9 : WORKDIR /home/app
Step 7/9 : COPY . /home/app
Step 8/9 : RUN yarn install
yarn install v1.16.0
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 1.55s.
Step 9/9 : CMD ["yarn", "start"]
---> Running in f882d870bc6a
Removing intermediate container f882d870bc6a
---> b8f9439e36fa
Successfully built b8f9439e36fa
Successfully tagged dockerfordevelopers/publisher:latest
你可以看到,1/9
、2/9
、3/9
等步骤与我们的 Dockerfile 中的每一行一一对应。我们 Dockerfile 中的第一行是 From Node:12
,而 Step 1/1
行也是 From Node:12
。类似地,Step 2/2
是 Dockerfile 中的第二行。构建过程按 Dockerfile 中的步骤依次执行,最终构建出容器镜像。
输出中的最后一行告诉我们容器的名称是 dockerfordevelopers/publisher:latest
。我们使用这个名称将构建的容器推送到 Docker Hub。
我们使用 push.sh 脚本来执行将发布者容器推送到 Docker Hub 组织的命令:
#!/bin/sh
# push.sh
docker push dockerfordevelopers/publisher
这是另一个方便我们的单行 .sh
脚本。
以下是发布者的 push.sh 脚本的输出:
# ./push.sh
The push refers to repository [docker.io/dockerfordevelopers/publisher]
9502c45a0d0e: Pushed
79b7f0047832: Pushed
bca5484440a2: Pushed
…
6a335755bda7: Pushed
latest: digest: sha256:e408ae01416511ad8451c31e532e3c2c6eb3324 ad43834a966ff161f9062e9ad size: 3056
#
我们在微服务架构项目中,有一种用于与自定义容器协作的模板或模式:
现在,你的同事们可以运行发布者镜像。这是在第二台机器上运行的,例如开发者的工作站:
# docker run --rm dockerfordevelopers/publisher
Unable to find image 'dockerfordevelopers/publisher:latest' locally
latest: Pulling from dockerfordevelopers/publisher
c5e155d5a1d1: Pull complete
221d80d00ae9: Pull complete
4250b3117dca: Pull complete
69df12c70287: Pull complete
…
Digest: sha256:e408ae01416511ad8451c31e532e3c2c6eb3324ad 43834a966ff161f9062e9ad
Status: Downloaded newer image for dockerfordevelopers/publisher:latest
yarn run v1.16.0
$ node ./index.js
当然,在这第二台机器上,开发者已经安装并运行了所需的微服务:Mosca、MongoDB 和 Redis。如果没有所有微服务在 Docker 中运行,应用程序将无法启动。
在你的开发主机上推送到 Docker Hub,并在生产主机上从 Docker Hub 拉取,是一种简单的生产环境容器部署方式。然而,这并不太健壮。我们将在后面的章节中讨论更好的部署方案。
总结
在本章中,我们学会了如何将一个通常会在虚拟机中运行并包含多个服务(MongoDB、Redis 和 Mosca)的应用程序,拆分为基于微服务的架构,并在 Docker 容器中运行。
我们学会了如何浏览 Docker Hub 网站,并找到一些有用的预制 Docker 容器,只需下载并运行它们。
我们还学会了如何将自己的微服务打包为 Docker 容器,并将它们推送到 Docker Hub,以便公共用户或开发团队成员使用。
多个容器被用来启动完整的应用程序,作为通过端口与主机端口映射的微服务进行通信。这并不理想,特别是当你已经在 80
端口上运行 WWW 服务器时;Mosca 也使用 80
端口。
在下一章中,我们将讨论如何使用 Docker Compose 工具设计完整的微服务架构应用程序并运行它们,以便它们拥有一个私有的内部网络,从而不再需要主机端口。
深入阅读
你可以参考以下链接,了解本章中涉及的更多内容:
-
官方 Docker 文档:
docs.docker.com
-
Dockerfile 参考文档:
docs.docker.com/engine/reference/builder/
-
Docker Hub 网站:
hub.docker.com/
-
Docker Hub 的文档:
docs.docker.com/docker-hub/
-
Node.js 容器在 Docker Hub 上的文档:
hub.docker.com/_/node
-
Redis 容器在 Docker Hub 上的文档:
hub.docker.com/_/redis
-
MongoDB 容器在 Docker Hub 上的文档:
hub.docker.com/_/mongo
-
Mosca 容器在 Docker Hub 上的文档:
hub.docker.com/r/matteocollina/mosca
第四章: 使用容器组合系统
在上一章中,我们使用微服务架构创建了一个服务器端应用程序。该应用程序由五个独立的容器组成:三个官方镜像和两个自定义镜像。官方镜像分别用于 MongoDB、Redis 和 Mosca(MQTT)。
在大多数情况下,容器之间的通信是通过 MQTT 消息传递进行的。订阅者容器执行数据库 localhost
(127.0.0.1
)的操作,且订阅者和发布者程序都可以访问位于 localhost
的 Mosca/MQTT。
在本章中,我们将讨论如何组合系统——具体来说是 Docker Compose。我们还将学习如何保持网络访问的私密性,以便服务可以从容器内部访问,但无法从主机访问。我们将了解如何在容器之间共享文件系统中的卷。Docker Compose 也有替代方案,我们将看看其中的一些。
本章将涵盖以下主题:
-
Docker Compose 介绍
-
使用 Docker 本地网络
-
本地卷
-
其他组合工具
总结一下,我们有三个官方 MongoDB、Mosca 和 Redis 的镜像容器。我们还为本书创建了两个额外的容器——发布者和订阅者微服务。
发布者微服务已修改为在 web 浏览器中呈现一个表单。表单中的字段和提交按钮使我们能够执行由订阅者微服务支持的各种操作:
图 4.1 – 我们更新后的发布程序生成的表单
您可以选择执行 CRUD 操作的数据库。您还可以设置一个值,用于列表、计数、添加和移除操作。每个 CRUD 操作都有一个按钮,还有一个刷新按钮,用于从所选数据库中删除所有记录。操作的返回值/结果显示在表单下方的结果标题下。
技术要求
本章的前提软件包括 Docker、Docker Compose(参见 docs.docker.com/compose/install/
)、Git 以及一种 web 浏览器,如 Google Chrome 或 Safari。
Docker 和 Docker Compose 文档使用“服务”这个术语,而我们使用“微服务”这个术语。对于本章来说,这两个术语可以互换使用。
在 GitHub 仓库中 (github.com/PacktPublishing/Docker-for-Developers
),有一个 chapter4/
目录,包含本章的相关内容。该目录包含了上章中使用的微服务架构代码的修改版本。
查看以下视频,了解代码的实际应用:
Docker Compose 介绍
容器的编排系统是一种工具,允许我们在配置文件中描述整个微服务架构程序,然后对所描述的系统进行操作。Docker Compose 就是这样的工具。在我们深入了解 Docker Compose 是什么以及它能做什么之前,让我们先来看一下我们为什么需要这样一个工具。
.sh 脚本的问题
到目前为止,我们一直在使用.sh
脚本来简化我们的微服务应用的工作。我们已经使用了以下脚本:
-
start-mongodb.sh
-
start-redis.sh
-
start-mosca.sh
-
subscriber/
start-subscriber.sh -
publisher/
start-publisher.sh -
subscriber/
build.sh -
publisher/
build.sh -
subscriber/
push.sh -
publisher/
push.sh
我们不再需要将这些命令分别调用,可以通过编写一个单独的 start-all.sh 脚本来依次执行它们:
#!/bin/sh
./start-mosca.sh
./start-mongodb.sh
./start-redis.sh
cd subscriber && ./start-subscriber.sh & cd ..
cd publisher && ./start-publisher.sh & cd ..
注意
start-all.sh 脚本仅供参考。接下来我们将不再使用它!
这个方法是可行的,但关于哪些端口开放以及其他容器特定的访问信息都隐藏在这些.sh
脚本中。例如,mongodb.sh 脚本启动 MongoDB 并将容器的27017
端口绑定到主机的27017
端口。
修改配置可能需要编辑这些.sh
脚本中的每一个,甚至可能需要修改 start-all.sh 脚本本身以及它的对应脚本 stop-sll.sh。我们还有一些额外的脚本,用于构建和发布容器,并执行其他管理任务。这个方法既不方便,也容易出错。
Docker Compose 工具解决了大部分.sh
脚本的问题,尽管我们仍然可能希望使用.sh
脚本来调用带有各种命令行参数的docker-compose
命令。
Docker Compose 配置文件
Docker Compose 的配置通过.yml
文件完成,文件内容是 YAML。YAML 是一种标记语言,允许数据序列化。它类似于 JSON 格式,但语法上对人类更友好。
一个名为docker-compose.yml
的文件是 Docker Compose 的默认配置文件。你可以拥有多个配置文件,并且可以通过命令行参数告诉 Docker Compose 使用哪些配置文件。
让我们看一下仓库中chapter4/
目录下的docker-compose-example.yml
文件。Docker Compose 工具可以取代我们迄今为止使用的 shell 脚本方法:
# Example Docker Compose file for our chapter 4 application
version: '3'
services:
Docker Compose 支持不同版本的docker-compose.yml
格式。新版的版本号较高,并添加了额外的docker-compose
功能。在services
部分,我们描述了要构建和运行的每个容器。
我们在services
部分下有一个redis
容器。image
字段指定我们将使用来自 Docker Hub 的redis
镜像。我们将数据库持久化到/tmp/redis
,以便在容器停止和重新启动时不会丢失数据:
redis:
image: redis
volumes:
- /tmp/redis:/data
ports:
- 6379:6379
我们在主机上暴露6379
端口,这是默认的 Redis 端口。暴露该端口可以让主机和其他容器访问 Redis 服务器。
在 Redis 之后,我们有 MongoDB 容器。我们将使用来自 Docker Hub 的mongo
镜像。我们将数据保存在主机的/tmp/mongo
目录中,这样在停止和重启容器时,数据库的内容可以被保留:
mongodb:
image: mongo
volumes:
- /tmp/mongo:/data/db
ports:
- 27017:27017
MongoDB 的默认 TCP 端口是27017
,我们将其暴露出来,将容器中的27017
端口映射到主机上的27017
端口。主机和容器中的工具可以通过localhost
访问 MongoDB,我们无需在命令行中指定端口,因为默认端口已配置。
接下来是 Mosca 容器。我们使用来自 Docker Hub 的matteocollina/mosca
镜像。我们将容器中的/db
卷设置为主机上的/tmp/mosca
,以持久化 Mosca 的状态:
mosca:
image: matteocollina/mosca
volumes:
- /tmp/mosca:/db
ports:
- 1883:1883
- 80:80
我们将1883
和80
端口暴露为与主机上相同的端口。端口1883
是默认的 MQTT 端口。端口80
用于支持 WebSocket 上的 MQTT,因此您可以在浏览器中的 JavaScript 程序中使用 MQTT。
在我们的publisher
容器中,build:
行告诉docker-compose
我们需要构建位于publisher/
目录中的容器。publisher
目录中的 Dockerfile 用于定义容器的构建方式:
publisher:
build: publisher
environment:
- MQTT_HOST=${HOSTIP}
- REDIS_HOST=${HOSTIP}
- MONGO_HOST=${HOSTIP}
ports:
- 3000:3000
我们暴露3000
端口,以便我们能够使用主机上的 Web 浏览器访问容器中运行的 Web 服务器。
在我们的subscriber
容器中,build:
行告诉docker-compose
我们需要构建位于subscriber/
目录中的容器。subscriber
目录中的 Dockerfile 用于定义容器的构建方式:
subscriber:
build: subscriber
environment:
- MQTT_HOST=${HOSTIP}
- REDIS_HOST=${HOSTIP}
- MONGO_HOST=${HOSTIP}
我们不暴露任何内容——订阅者通过直接的 API 调用来执行所有 I/O 操作,访问 MongoDB 和 Redis,同时通过 MQTT 接受命令并报告状态。
需要注意的几点如下:
-
所有的容器都在单一的配置文件中进行简洁描述。
-
容器仍然暴露与
.sh
脚本相同的端口到主机。 -
容器仍然需要通过
HOSTIP
环境变量找到数据库和 MQTT 经纪人容器。该变量仍然需要像前一章中解释的那样进行设置。
要使用我们的docker-compose-example.yml
脚本启动所有五个微服务,我们使用docker-compose up
命令。-f
选项告诉docker-compose
使用哪个 Docker Compose .yml
文件:
% docker-compose -f docker-compose-example.yml up
默认情况下,docker-compose
以调试模式运行配置文件中的所有容器。它们会按照打印行的顺序将输出打印到终端/控制台。你可能会看到首先是订阅者的输出,接着是发布者的输出,然后再是订阅者的输出。如果按下Ctrl + C,它将终止所有容器并返回到命令提示符。
如果你希望容器以分离模式或守护进程模式运行,可以使用-d
选项:
% docker-compose -f docker-compose-example.yml up -d
在分离模式或守护模式下,容器不会向终端/控制台输出信息,你将立即返回到命令提示符。
为了停止所有五个微服务,我们使用类似的docker-compose
命令:
% docker-compose -f docker-compose-example.yml down
如果我们没有指定要使用的 Docker Compose 配置文件(-f docker-compose-example.yml
),则docker-compose
命令会查找并使用名为docker-compose.yml
的文件。
docker-compose up
/down
命令还允许我们启动和停止一个或多个服务。例如,我们可以只启动mongodb
和redis
容器:
% docker-compose -f docker-compose-example.yml up mongodb redis
现有的mongodb
和/或redis
容器将被停止并启动新的容器。由你的程序来检测这些服务的连接是否被停止,并相应地处理错误。
我们可以使用docker-compose
构建任何或所有的服务:
% docker-compose -f docker-compose-example.yml build publisher
这个命令构建了我们的发布者容器,但不会启动任何容器。
能够指定一个或多个容器(通过名称)的能力,替代了我们许多旧的.sh
脚本。我们不再需要启动脚本,因为可以使用docker-compose up
;我们不再需要停止脚本,因为可以使用docker-compose down
;我们不再需要构建脚本,因为可以使用docker-compose build
;等等!更多详细信息请参见docs.docker.com/compose/reference/
。
我们很可能会为开发和生产环境设置不同的配置,甚至可能有额外的场景。在.sh
脚本中,我们为开发和生产环境分别设置了 debug.sh 和 run.sh 脚本。这个.sh
文件方案的问题在于,我们在每个脚本中几乎有相同的docker run
命令,只有细微的差异。
Docker Compose 有一个继承功能,允许在docker-compose
命令行中指定多个配置文件。
使用多个配置文件进行继承
我们可以实现一个基础的docker-compose.yml
文件,然后使用我们自己的覆盖配置文件来覆盖该文件中的设置。这个功能被称为docker-compose
文件及其设置覆盖功能。
Docker Compose 从命令行上的第一个配置文件开始,然后将第二个文件合并到其中,再将第三个(如果有的话)合并进去,依此类推。合并意味着将第二个(或第三个)配置文件中的设置应用到当前的配置状态中,这最终将被使用。如果第二个配置文件中的设置存在,它们将替换第一个配置文件中的设置;如果不存在,它们将添加新的服务或设置。
让我们看看从现在开始将使用的docker-compose.yml
基础文件:
version: '3'
services:
redis:
image: redis
mongodb:
image: mongo
volumes:
- /tmp/mongo:/data/db
mosca:
image: matteocollina/mosca
volumes:
- /tmp/mosca:/db
publisher:
build: publisher
depends_on:
- "mosca"
- "subscriber"
subscriber:
build: subscriber
depends_on:
- "redis"
- "mongodb"
- "mosca"
这看起来像前一节中的docker-compose-example.yml
文件,但你可能会注意到几个不同点:
-
有两个
depends_on
选项——一个用于发布者,另一个用于订阅者。 -
我们不再将容器的端口暴露或绑定到主机的端口。
接下来我们将详细查看它们。
depends_on
选项
depends_on
选项允许我们控制容器的启动顺序(参考 docs.docker.com/compose/startup-order/
)。此外,depends_on
表示容器之间的相互依赖。更多关于 depends_on
选项的信息,请参考 https://docs.docker.com/compose/compose-file/#depends-on#depends_on。
服务依赖关系会导致以下行为:
-
docker-compose up
按照依赖顺序启动服务。在我们的示例中,redis
、mongo
和mosca
服务会在subscriber
容器之前启动,且mosca
和subscriber
都会在publisher
之前启动。 -
docker-compose up SERVICE
会自动包含SERVICE
下的依赖项。
docker-compose stop
按照依赖顺序停止服务(首先是 mosca
,然后是 mongodb
,再是 redis
,这些服务在我们的 docker-compose.yml
文件中定义)。
启动服务的顺序很重要,因为如果我们在 mosca
启动之前启动 publisher
,publisher
程序中连接 MQTT 代理的逻辑将失败。同样,若在数据库和 MQTT 代理服务启动之前启动 subscriber
,则 subscriber
中的连接数据库和 MQTT 代理的逻辑也可能失败。在 subscriber
启动之前启动 publisher
是没有意义的,因为 publisher
通过 MQTT 发送的任何内容都不会被接收,可以说是“白费力气”。
即使容器已经启动,也不能保证在使用它们的微服务尝试连接时,容器中的程序已经完成初始化。在我们的发布者和订阅者代码中,我们创建了一个 wait_for_services()
方法,确保只有在服务启动并准备好后,我们才尝试连接。
我们在发布者和订阅者程序中首先调用 wait_for_services()
,以确保我们等待足够长的时间,直到依赖服务启动并准备好。
publisher/
index.js 中的 wait_for_services()
方法如下:
/**
* wait_for_services
*
* This method is called at startup to wait for any dependent containers to be running.
*/
const waitOn = require("wait-on"),
wait_for_services = async () => {
try {
await waitOn({ resources: [`tcp:${mqtt_host}:${mqtt_port}`] });
} catch (e) {
debug("waitOn exception", e.stack);
}
};
我们的 publisher
微服务仅连接到 MQTT 代理,因此 wait_for_services()
方法只等待我们的 MQTT 代理的 TCP 端口可访问。
subscriber/
index.js 中的 wait_for_services()
方法稍微复杂一些:
/**
* wait_for_services
*
* This method is called at startup to wait for any dependent containers to be running.
*/
const waitOn = require("wait-on"),
wait_for_services = async () => {
try {
debug(`waiting for mqtt (${mqtt_host}:${mqtt_port})`);
await waitOn({ resources: [`tcp:${mqtt_host}:${mqtt_port}`] });
debug(`waiting for redis (${redis_host}:${redis_port})`);
await waitOn({ resources: [`tcp:${redis_host}:${redis_port}`] });
debug(`waiting for mongo (${mongo_host}:${mongo_port})`);
await waitOn({ resources: [`tcp:${mongo_host}:${mongo_port}`] });
} catch (e) {
debug("***** exception ", e.stack);
}
};
subscriber
微服务需要连接到 MQTT 代理、redis
服务器和 mongo
服务器。我们等待这些服务器的 TCP 端口可访问。
还有其他方法可以等待服务可用,其中包括在容器中安装命令行程序/脚本,并在启动发布者或订阅者服务之前运行它们。例如,您可以使用这个方便的 wait-for-it.sh
脚本,详情请见 github.com/vishnubob/wait-for-it
。
在docker-compose.yml
文件中没有暴露容器端口选项并非疏忽。我们完全可以在重写文件中指定这些选项,从而为现有容器提供选项。
使用重写文件添加端口绑定
在代码库中的chapter4/
目录下,我们有一个docker-compose-simple.yml
文件,它是一个重写文件的示例:
version: '3'
services:
redis:
ports:
- 6379:6379
mongodb:
ports:
- 27017:27017
mosca:
ports:
- 1883:1883
- 80:80
publisher:
environment:
- MQTT_HOST=${HOSTIP}
- REDIS_HOST=${HOSTIP}
- MONGO_HOST=${HOSTIP}
ports:
- 3000:3000
subscriber:
environment:
- MQTT_HOST=${HOSTIP}
- REDIS_HOST=${HOSTIP}
- MONGO_HOST=${HOSTIP}
在这里,我们指定了每个容器的端口。我们正在从docker-compose.yml
文件中继承选项,并添加暴露每个容器端口的选项。
我们没有为subscriber
微服务暴露任何端口,因为它从不将任何端口暴露到主机的端口。
我们还定义了三个环境变量,供发布者和订阅者容器访问MQTT_HOST
(mosca
)、REDIS_HOST
(redis
)和MONGO_HOST
(mongodb
)服务。
使用这两个配置文件(继承)的docker-compose
命令来启动我们的服务,如下所示:
% HOSTIP=192.168.0.21 docker-compose -f docker-compose.yml -f docker-compose-simple.yml up
由于我们没有使用-d
开关,我们的容器没有被分离,而是将其控制台/调试输出打印到终端。直到你按下Ctrl+C,你才能输入更多命令。这样做将按反向depends_on
顺序停止所有容器,并将你带回命令提示符:
% HOSTIP=192.168.0.21 docker-compose -f docker-compose.yml -f docker-compose-simply.yml up -d
添加-d
开关会使所有容器以守护进程模式启动。它们在后台运行,你会立即得到一个命令行提示符。不会再有进一步的输出发送到终端。
如果容器以守护进程模式运行,你可以使用docker-compose down
命令来停止它们:
% HOSTIP=192.168.0.21 docker-compose -f docker-compose.yml -f docker-compose-simple.yml down
我们也可以使用三个或更多配置文件。每个在命令行中指定的附加文件都会进一步扩展容器和选项。
到目前为止,我们实际上是通过继承设置的生产环境。使用这种方式调试特别痛苦,因为你诊断错误的唯一方法就是向发布者和/或订阅者添加debug()
调用,然后重新构建容器,并重新运行整个应用程序。
为了改善我们的开发和调试周期,我们可以将publisher/
和subscriber/
目录绑定/挂载到容器中的/home/app
目录。两个容器的 Dockerfile 使用 nodemon(nodemon.io/
)工具来启动容器内的应用程序。
nodemon 工具做的事情远不止启动我们的程序:
-
它还会监控程序的状态,如果程序停止,nodemon 会重新启动它。这非常有用,因为我们的 Node.js 程序可能会遇到一些无法轻易恢复的错误,然后直接退出,让 nodemon 重启它们。
-
对于开发,nodemon 还会监控代码目录中文件的时间戳,如果任何文件发生变化,它将重新启动程序。
由于我们可以将源代码直接绑定/挂载到容器中,因此我们在主机上使用编辑器或 IDE 对文件进行的任何更改将立即影响容器中的变化。
我们可以创建一个 docker-compose-simple-dev.yml
文件,将我们的绑定/挂载添加到发布者和订阅者:
version: '3'
services:
publisher:
volumes:
- ./publisher:/home/app
subscriber:
volumes:
- ./subscriber:/home/app
我们使用 docker-compose up
命令来运行此命令:
% HOSTIP=192.168.0.21 docker-compose -f docker-compose.yml -f docker-compose-simple.yml -f dockercompose-simple-dev.yml up -d
如果我们在主机上编辑,比如 publisher/
index.js 文件,我们可以看到 nodemon 检测到更改并重新启动发布者程序:
publisher_1 | [nodemon] restarting due to changes...
publisher_1 | [nodemon] starting `node ./index.js`
publisher_1 | 2020-03-30T18:03:39.537Z publisher publisher microservice, about to wait for MQTT host(192.168.0.21, 1883
publisher_1 | 2020-03-30T18:03:39.546Z publisher ---> wait succeeded
publisher_1 | 2020-03-30T18:03:39.587Z publisher publisher connecting to MQTT mqtt://192.168.0.21
publisher_1 | 2020-03-30T18:03:39.591Z publisher connected to 192.168.0.21 port 1883
publisher_1 | 2020-03-30T18:03:39.638Z publisher listening on port 3000
我们现在已经很好地掌握了 docker-compose
,但是我们正在将容器的端口绑定到主机的端口。如果你有一个容器需要绑定主机的 80
端口,而主机上已经在运行一个 Web 服务器或另一个项目的容器,也想绑定到 80
端口,那么这会带来问题。
幸运的是,Docker 提供了一种功能,只将我们的端口暴露给我们的容器!
使用 Docker 本地网络
Docker 和 Docker Compose 都有命令行选项来指定应用将使用的 Docker 本地网络。使用该 Docker 本地网络可以让我们的容器访问另一个容器的端口,而无需将这些端口绑定/暴露到主机的端口上。
使用 .sh 脚本进行网络配置
你可以使用 docker network create
命令来创建一个命名网络,供你的容器彼此私密通信。你可以定义任意数量的这些私有网络——你可能希望同时处理多个不相关的项目,每个项目都需要自己的网络:
% docker network create chapter4
该命令创建了一个名为 chapter4
的网络,我们可以在微服务示例程序中使用它。我们可以使用 docker network rm
命令销毁我们创建的网络:
% docker network rm chapter4
该命令将 chapter4
网络从系统中移除。
start-mongodb.sh、start-redis.sh、start-mosca.sh、publisher/
run.sh 和 subscriber/
run.sh 脚本被 up.sh 脚本用来通过 docker run
命令启动我们的应用容器。
让我们检查一下我们的 up.sh 脚本:
#!/bin/sh
./stop-all.sh
我们运行 docker network create
命令来创建我们的 chapter4
网络:
docker network create chapter4
我们启动我们的三台服务器:
./start-mosca.sh
./start-mongodb.sh
我们还运行 ./
start-redis.sh:
###### SUBSCRIBER
cd subscriber
./run.sh
最后,我们启动发布者:
###### PUBLISHER
# publisher needs to expose port 3000
# so we can access the WWW interface
cd ../publisher
./run.sh
start-mongodb.sh 和 start-redis.sh 脚本大致与 start-mosca.sh 脚本相同。start-mosca.sh 脚本中的相关行是用于 docker run
命令的:
docker run \
--name $SERVICE \
-d \
--restart always \
-e TITLE=$SERVICE \
--network chapter4 \
-v /tmp/mosca:/db \
matteocollina/mosca
只有服务名称、要使用的第三方/Docker Hub 容器以及任何容器到主机目录的绑定是特定于 mongodb
、mosca
或 redis
的。它们都共享 chapter4
网络。
subscriber/
run.sh 脚本中的 docker run
命令如下所示:
docker run \
--name $SERVICE \
-d \
--restart always \
-e TITLE=$SERVICE \
--network chapter4 \
dockerfordevelopers/$SERVICE
我们不再定义 HOSTIP
环境变量,因为 Docker 本地网络系统提供了一个 DNS 功能,允许容器中的程序通过名称查找其他容器。这个名称就是容器的名称,它是在 docker run
命令脚本中通过 –name
命令行选项指定的。
subscriber/
index.js 中的相关代码如下:
const debug = require("debug")("subscriber"),
mongo_host = process.env.MONGO_HOST || "mongodb",
mongo_port = 27017,
mongoUrl = `mongodb://${mongo_host}:${mongo_port}`,
mqtt_host = process.env.MQTT_HOST || "mosca",
mqtt_port = 1883,
mqttUrl = `mqtt://${mqtt_host}`,
redis_host = process.env.REDIS_HOST || "redis",
redis_port = 6379,
redisUrl = `redis://${redis_host}`;
代码设计接受MONGO_HOST
环境变量;否则,它将使用mongodb
容器名称。MQTT_HOST
/mosca
和REDIS_HOST
/redis
也是同样的情况。
注意
我们一直在定义HOSTIP
、MONGO_HOST
、MQTT_HOST
和REDIS_HOST
环境变量,特别是在.sh
脚本示例中。由于我们使用--name
选项在docker run
命令中为容器命名,Docker 的本地 DNS 将在.sh
脚本中工作。也就是说,如果我们给容器命名,就不需要定义这些环境变量。我们仍然需要将容器端口绑定到主机端口,除非我们还添加--network
选项和docker network create
到 Docker 本地网络。
down.sh 脚本停止所有容器并移除chapter4
网络:
#!/bin/sh
docker stop publisher
docker stop subscriber
docker stop redis
docker stop mongodb
docker stop mosca
docker network rm chapter4
我们可以使用这些.sh
脚本,但我们已经了解到,Docker Compose 是管理微服务的更优方法。
使用 Docker Compose 进行网络配置
我们创建的docker-compose.yml
配置文件仍然足够用作执行docker-compose
命令来管理容器的基础。然而,我们不再需要将容器端口暴露或绑定到主机的端口;唯一的例外是,我们会继续绑定端口3000
,以便可以通过主机上的浏览器访问发布者网页。基础的docker-compose.yml
文件并没有绑定端口3000
,因此我们将继续使用覆盖文件来绑定端口。
默认情况下,如果命令行中没有指定配置文件,docker-compose
会查找docker-compose.yml
并使用它,然后查找docker-compose.override.yml
并使用它。
如果需要指定第三个配置文件,必须为每个配置文件使用-f
命令行选项。
我们的docker-compose.override.yml
文件处理我们的生产场景:
version: '3'
services:
redis:
networks:
- chapter4
mongodb:
networks:
- chapter4
mosca:
networks:
- chapter4
publisher:
ports:
- 3000:3000
networks:
- chapter4
subscriber:
networks:
- chapter4
networks:
chapter4:
此文件添加了chapter4
网络,将其分配给每个容器,并将发布者容器的端口3000
绑定到主机上的端口3000
。
我们需要做的就是运行一个简单的docker-compose
命令来使用docker-compose.yml
和docker-compose.override.yml
:
% docker-compose up
几秒钟后,我们的五个容器已经启动并运行,我们可以通过主机上的浏览器访问应用程序。我们可以看到一切正常工作。我们还可以执行以下操作:
-
使用
-d
选项以分离/守护进程模式运行容器。 -
使用
docker-compose
停止和启动一个或多个容器。 -
使用
docker-compose
构建一个或多个容器。 -
使用
docker-compose logs
查看运行在守护进程模式下的任何容器的日志。
我们现在拥有一对适用于生产模式的配置文件。接下来,我们需要一种在开发模式下工作的方式,通过将源代码绑定到容器的主目录。
在容器内绑定主机文件系统
之前,我们使用了第三个 docker-compose
配置文件来指定绑定,以便将源代码目录映射到容器内(替换应用的主目录)。我们将为最新版本的 Docker Compose 设置做同样的事情。
我们首先创建一个 docker-compose-dev.yml
文件:
version: '3'
services:
publisher:
volumes:
- ./publisher:/home/app
subscriber:
volumes:
- ./subscriber:/home/app
这个重载文件只是将发布者和订阅者源代码目录映射到相关容器中的 /home/app
。现在,我们可以自由地在主机上编辑源代码,且得益于 nodemon,我们的更改几乎能立即在运行中的容器内生效。无需停止、重建或重启任何容器。
不幸的是,docker-compose
没有提供通过继承移除选项的功能;我们只能修改现有选项或添加新选项。如果可以移除选项,我们可以在 docker-compose.override.yml
文件中绑定源代码,并在 docker-compose-production.yml
文件中移除它们。这将使我们能够在开发时使用简短的 docker-compose up
命令,并在生产环境中使用带有三个 -f
开关的命令行。这将很方便,因为我们大部分时间都使用开发模式,而很少使用生产模式。
按目前的情况,我们必须指定三个 -f
开关:
% docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose-dev.yml up
卷还有其他用途,我们将进一步探索。
优化我们的容器大小
我们可以使用 docker images
命令查看我们的容器镜像:
% docker images | grep pub
chapter4_publisher latest 15f3a84d348d 24 minutes ago 987MB
如你所见,我们的发布者镜像是 987
兆字节!这只是一个几乎 250 行的 JavaScript 程序。我们可以尝试通过将 node_modules
目录移出容器并放入一个命名卷中来缩小这个大小。这还可以加速容器的构建,因为 node_modules
会在这个命名卷中从构建到构建地被保存,并且使用 yarn
命令安装模块时,只会安装新的模块。
注意
我们将 Dockerfile 重命名为 Dockerfile.chapter3
,并放置在 publisher/
目录下。新 Dockerfile 已被修改为构建一个非常小的镜像。
通过优化我们的 Dockerfile,可以创建一个更小的镜像。我们将构建一个基础镜像和我们的结果镜像。基础镜像将安装 node_modules
。只有当某些内容发生变化并需要重新构建其层时,基础镜像才会重新构建。
让我们看一下发布者的优化 Dockerfile:
FROM node:12-alpine
我们从 Alpine OS 的 Node v12 镜像继承。这个镜像比 Debian 风味的默认 Node 容器要轻得多:
ENV TZ=America/Los_Angeles
WORKDIR /home/app
# add a user - this user will own the files in /home/app
RUN adduser -S app
ENV HOME=/home/app
COPY . /home/app
生成的镜像在构建时不会安装或更新 node_modules
。我们将在另一个步骤中安装这些模块。这使我们避免了每次构建容器时都需要使用 yarn
安装模块:
CMD ["yarn", "start"]
我们使用 yarn start
启动我们的发布应用。
在运行 docker-compose build publisher
后,我们可以看到容器的大小大大减少了!
在优化之前,容器大小为 987
兆字节。优化后,容器为 89.5
兆字节,几乎减少了 900 兆字节:
# docker images | grep pub
chapter4_publisher latest 080efb97e0d3 About a minute ago 89.5MB
我们仍然需要安装我们的 node_modules/
模块,这将在一个命名卷中完成,并在 docker-compose-overrides.yml
文件中定义。只需完成一次,之后如果你向 publisher/
目录下的 packages.json
文件添加包,则需要重新执行:
# docker-compose run publisher yarn install
该命令在发布者容器内使用 yarn install
安装 node_modules/
包。命名卷被正确挂载,因为它在 docker-compose
配置(.yml
)文件中指定。
注意
我们没有优化订阅者的构建。
我们可以通过检查卷的 _data
目录来验证卷是否已创建并包含已安装的 node_modules
模块,在 Linux 上该目录应位于 /var/lib/docker/volumes
:
# cd /var/lib/docker/volumes/
# ls -1 chapter4_node_modules_publisher/_data/
abbrev
accepts
ajv
ansi-align
ansi-regex
ansi-styles
anymatch
对于 macOS,卷的位置有显著不同。你需要使用以下命令获取运行 Docker 的 Linux 虚拟机中的 shell:
# screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
你可能需要按 ^C
几次才能得到 shell 提示符。这个提示符是在虚拟机中运行的 shell。在虚拟机内,容器中 node_modules/
目录的挂载点是 /var/lib/docker/volumes
,就像在 Linux 上的 Docker 一样。
我们可以看到构建的加速效果。在完全删除系统中所有镜像后,发布者的初始构建大约需要 16 秒:
# time docker-compose build publisher
Successfully built e50ec5f4d53b
Successfully tagged chapter4_publisher:latest
docker-compose build publisher 0.36s user 0.09s system 2% cpu 16.187 total
如果没有安装 node_modules
,随后的构建大约需要半秒钟:
# time docker-compose build publisher
Successfully tagged chapter4_publisher:latest
docker-compose build publisher 0.34s user 0.08s system 74% cpu 0.568 total
编辑 index.js 并重新构建后,时间不到 1 秒:
# time docker-compose build publisher
Successfully tagged chapter4_publisher:latest
docker-compose build publisher 0.34s user 0.08s system 49% cpu 0.842 total
正如你所看到的,我们成功地减少了容器的大小和构建时间!
使用 build.sh 脚本
在 GitHub 仓库的 chapter4/
目录中提供了一个 build.sh 脚本。它仅包含几行实际的 shell 命令:
#!/bin/sh
# build.sh
# build publisher and subscriber and install node_modules in each
docker-compose build --force-rm --no-cache
docker-compose run publisher yarn install
docker-compose run subscriber yarn install
build.sh 脚本构建了所有五个容器,并在发布者和订阅者容器中运行 yarn install
,以便在各自的命名卷中安装 node_modules
模块。命令行开关切换到 docker-compose build
命令如下:
-
--force-rm
:强制 Docker 在构建过程中移除所有中间容器镜像 -
--no-cache
:强制 Docker 不使用任何缓存的/已下载的/已构建的版本
你可以省略这两个开关,显著提高构建速度。它们在此提供,目的是演示一种从头强制重建所有内容的方法。
这就是 Docker Compose 的一个不错的概述。它是描述、构建和运行 Docker 应用程序的第一个工具之一,甚至可能是第一个。但是,市场上也有其他的替代工具。
其他组合工具
我们已经看到如何使用 docker-compose
和 .sh
脚本来组合并构建一个多服务应用程序。但还有其他一些你可能想要考虑的选项。
Docker Swarm
Docker Swarm 是一个集群管理系统。它允许你将通过 docker-compose
定义的容器部署到一组节点或服务器上。如果你想使用 Docker Swarm,docker-compose.yml
的使用有一些限制。例如,你不能在 Docker Swarm 中使用卷,并且绑定容器端口到主机时需要仔细规划。
Kubernetes
Kubernetes 是 docker-compose
的一种功能丰富的替代方案。它允许将容器部署到一组 Docker 容器服务器上,并使用类似于 docker-compose.yml
的配置文件格式。
Packer
Packer 是一个生成多种输出格式的工具,包括 Docker 容器。你通过 JSON 文件定义容器,并且该工具从这些文件中读取。Packer 使用构建器来生成输出文件。输出可以是(但不限于)以下内容:
-
Azure 虚拟机镜像
-
DigitalOcean 虚拟机镜像
-
Docker 容器镜像
-
Google 云镜像
-
Parallels(用于 macOS)镜像
-
VirtualBox 镜像
-
VMware 镜像
你选择的编排工具应该使你的工作更轻松。务必选择一个真正适合你需求的工具。Docker Compose 是官方的 Docker 编排工具,其他工具可能更加现代,并解决 Docker Compose 无法解决的附加问题。
总结
在本章中,我们介绍了 Docker Compose 作为管理和运行复杂容器系统的优越管理工具。我们描述了几个有用的 docker-compose
配置文件选项,使我们能够指定需要暴露的端口、本地网络和本地卷。我们还利用了 docker-compose
工具的继承能力。
使用 Docker 的关键部分是开发周期。我们通常在每个周期中编辑、构建、运行和测试——然后重复。可以通过策略性地减少镜像的大小,以及构建、发布和下载镜像的时间,来优化开发过程。
我们还探讨了使用 .sh
脚本和 docker-compose
的一些替代方案。这些是你 Docker 学习过程中自然而然的下一步,因为它们提供了将你的编排部署到生产或测试中服务器群集的功能。
接下来的几章将详细介绍如何部署应用程序以及如何实现持续集成和自动化测试。之后,我们将讨论容器化应用程序的安全性考虑。
进一步阅读
你可以参考以下网址,获取本章所涉及主题的更多信息:
-
官方 Docker 文档:
docs.docker.com
-
官方 Docker Compose 文档:
docs.docker.com/compose/
-
Dockerfile 参考:
docs.docker.com/engine/reference/builder/
-
Docker Hub 网站:
hub.docker.com/
-
Docker Hub 文档:
docs.docker.com/docker-hub/
-
Node.js
容器的文档在 Docker Hub:hub.docker.com/_/node
-
Redis 容器的文档在 Docker Hub:
hub.docker.com/_/redis
-
MongoDB 容器的文档在 Docker Hub:
hub.docker.com/_/mongo
-
Mosca 容器的文档在 Docker Hub:
hub.docker.com/r/matteocollina/mosca
第二部分:在生产环境中运行 Docker
在本节中,您将学习如何在生产环境中选择不同的 Docker 应用运行方案,从单主机配置到在云端的复杂服务器集群,这些集群能够扩展以应对高负载。您将学习如何使用 Docker Compose 部署系统,并学习如何使用 Jenkins 自动化构建和部署简单的环境。随后,我们将深入探讨一个更复杂的设置,在第八章中,将 Docker 应用部署到 Kubernetes,并通过第十一章来了解,扩展和负载测试 Docker 应用,重点讨论 Kubernetes 和亚马逊 Web 服务的使用。您将学习如何手动和使用 Spinnaker 持续部署系统部署应用,并如何使用各种工具监控应用。最后,我们将学习如何使用 Kubernetes 扩展 Docker 应用,利用 Envoy 服务网格和 k6 等工具进行负载测试。我们将使用一个示例应用——名为 ShipIt Clicker 的游戏,逐一展示这些概念。
本节包括以下章节:
-
第五章**, 部署和运行生产环境中容器的替代方案
-
第六章**, 使用 Docker Compose 部署应用
-
第七章**, 使用 Jenkins 进行持续部署
-
第八章**, 将 Docker 应用部署到 Kubernetes
-
第九章**, 使用 Spinnaker 进行云原生持续部署
-
第十章**, 使用 Prometheus、Grafana 和 Jaeger 监控 Docker
-
第十一章**, 扩展和负载测试 Docker 应用
第五章:生产环境中容器的部署与运行替代方案
随着容器技术和云计算的成熟,你部署 Docker 容器的方式也大大增加了。有些选项简单得只需要在单一主机上运行 Docker,而其他的则具备像自动扩展、多云支持等高级功能。你甚至可以将 Docker 容器运行在本地裸金属服务器上,或采用混合云解决方案。
阅读完本章后,你将理解现有的多种选择各自有不同的利弊。你将学会如何构建最小化的可行生产环境。你将能够在不同的云服务提供商及其管理的容器运行时之间做出选择,并阐明将 Docker 运行在本地或混合云环境中的好处。最重要的是,你将能够在多种目标之间权衡,做出关于 Docker 容器部署的生产路径的明智决策。
了解选择的范围将有助于你做出更好的决策。
本章将涵盖以下主要内容:
-
在生产环境中运行 Docker —— 许多路径,明智选择
-
最小化的现实生产环境是什么?
-
托管云服务
-
运行你自己的 Kubernetes 集群 —— 从裸金属服务器到 OpenStack
-
决定合适的 Docker 生产环境设置
技术要求
要完成本章的练习,你需要在本地工作站上安装 Git 和 Docker。对于 Mac 和 Windows 用户,请安装 Docker Desktop (www.docker.com/products/docker-desktop
),因为这是大多数使用 Docker 的人们在本地工作站上使用的方式。在选择生产部署工具之前,你需要了解更多选项。
根据你选择的路径,你可能还需要在 Amazon Web Services、Google Cloud、Microsoft Azure 或 Digital Ocean 等平台上建立账户。这些服务大多提供慷慨的免费套餐,允许你在不花费太多钱的情况下进行实验,尤其是当你只使用这些服务短时间时。在考虑什么样的环境适合你的应用时,拥有多个选项是有帮助的。如果你在云端创建了资源,别忘了删除那些不再使用或不打算保留的资源,否则当你看到账单时可能会大吃一惊。大多数云服务提供商都有计费提醒系统。请考虑设置一个警报,如果你的支出超过预算时能及时通知你。
如果你想探索托管更复杂的本地设置,或使用像 Packet (www.packet.com/
) 这样的裸金属托管服务,你可能需要一台或多台符合 Docker 或 OpenStack 在裸金属硬件上运行要求的服务器。
本章的 GitHub 仓库链接是 github.com/Packt-Publishing/Docker-for-Developers
—— 请查看其中的 chapter5
文件夹。
查看以下视频,看看代码是如何运行的:
示例应用程序 – ShipIt Clicker
本章链接的 GitHub 仓库包含一个在线游戏原型的代码——名为 ShipIt Clicker。在这款游戏中,一只戴着礼帽的松鼠敦促你将容器部署到生产环境;你点击的速度越快,积累的 docker-compose
就越多,从而运行多个容器。游戏涉及一个网页浏览器客户端、一个使用 Express 的 Node.js 服务器和一个 Swagger 驱动的 API,以及一个 Redis NoSQL 数据库,后者用于跟踪分数和其他游戏信息。
你可以通过实验 ShipIt Clicker 来熟悉比前几章更复杂的应用程序。可以自由地调整和改进配置文件和代码,并结合各种工具和服务进行部署,以便学习如何将应用程序部署到生产环境。在接下来的章节中,我们将学习如何以多种不同方式将该应用程序部署到生产环境中,每种方式都提供了越来越多的功能,但在成本、复杂性和可用性方面存在不同的权衡。 在此之前,让我们更深入了解这些替代方案。
在生产环境中运行 Docker —— 多种选择,明智决策。
如果你认为在本地工作站上运行 Docker 提供了很多选择,那么请系好安全带,因为在开发人员和系统管理员使用 Docker 构建应用程序并以一种强大的方式进行部署时,可用的多种方式使得本地开发环境显得简单。世界上一些最大的 IT 公司都使用 Docker(或同类容器技术)来进行大规模运行,而容器编排技术使这一切成为可能。拥有一个自愈集群的承诺,使其能够在面对网络分区和硬件故障时继续运行应用程序,吸引了许多人进入 Docker 领域。当运行一个容错集群的复杂性逐渐显现时,许多人发现他们的热情开始减退。
然而,你不必全程独立完成。多个云服务提供商提供的服务,使得用 Docker 运行应用程序变得更加可管理。大多数大型组织倾向于使用 Kubernetes,这是一个由 Google 赞助的项目,作为一个公开且社区支持的替代品,替代专有的容器编排工具。Kubernetes 从 Google 构建和运营其内部容器编排工具 Borg 中汲取经验,并将这些经验开放给公众。
或者,也许你只需要在尽可能小的设置下运行一个简单的动态网站 – 如果你有一个可以运行 Docker 的联网服务器,你就不需要学习云编排技术来实现这一点。
最小的实际生产环境是什么?
Docker 可以运行在多种硬件和软件上,但你从 Docker 本身或第三方(例如捆绑 Docker 的操作系统发行版)获得的支持水平可能会有所不同。Docker 可以运行在多种操作系统上:Linux、Apple macOS、Microsoft Windows,甚至 IBM S/390x。
最低要求——在一台主机上运行 Docker 和 Docker Compose
鉴于 Docker 在不同环境中的广泛分布,Docker 托管应用程序的最小生产环境是一台主机,无论是物理主机还是虚拟主机,运行支持 Docker 和 Docker Compose 的操作系统。许多流行的主流操作系统和发行版都内置了一些版本的 Docker,包括当前的 长期支持版(LTS)Ubuntu(16.04、18.04 和 20.04)和 CentOS(7 和 8)。其他一些更专业的操作系统,如 CoreOS 和 Container Linux,专注于仅运行容器,尽管这些系统对习惯于主流系统的人来说可能有一定的学习曲线,但它们可能是不错的选择。
你甚至可以在 Windows 或 macOS 上运行 Docker 用于生产系统。根据你的风险承受能力和需求,选择运行 Docker 的平台可能会更让你感到舒适,这取决于是否有支持。选择中总是充满权衡!
Docker 支持
Docker 的社区版获得母公司有限时间的支持——专注于开发者的 Docker 公司(www.docker.com
)每季度发布 社区版(CE)Docker 工具链,并提供 4 个月的滚动支持期。截至 2019 年 11 月 13 日,Docker 的 企业版(EE)已由 Mirantis 生产;详情请见 www.mirantis.com/company/press-center/company-news/mirantis-acquires-docker-enterprise/
。Docker 的 EE 版本提供更长的支持期;支持多种 Linux、Windows 和 macOS 操作系统;以及扩展的支持编排系统;有关 Docker EE 的更多信息,请参见 docs.docker.com/ee/
。Mirantis 宣布将于 2021 年 11 月停止对 Docker EE 中的 Docker Swarm 容器编排工具的支持,但在 2020 年 2 月撤回了这一退役公告。更多详情请见 devclass.com/2020/02/25/mirantis-to-keep-docker-swarm-buzzing-around-pledges-new-features/
。
考虑到这些消息,Kubernetes 似乎在 Docker 容器编排竞争中胜出,尽管 Mirantis 仍然在支持 Docker Swarm。
单主机部署的问题
然而,在单一主机上运行 Docker 存在重大缺点。如果该主机发生重大硬件或软件故障,或者互联网连接受到影响,你的应用程序将面临可用性下降的风险。计算机本质上是不可靠的,即使是具有企业级可用性特性的系统,如冗余磁盘、电源和冷却功能,也可能因环境因素而发生故障。如果你走这条路,明智的做法是增加某种外部监控,并确保你有一个可靠的备份和恢复方案,以减轻这些风险。为了避免这些风险,我们需要考虑更复杂的方法,例如依赖第三方运行的更多容器编排系统。
托管云服务
为了克服在单一主机上部署应用程序的限制,最简单的选择是考虑使用提供容器编排解决方案的托管云服务来运行你的应用程序。一些最流行的解决方案包括以下内容:
-
Google Kubernetes 引擎 (GKE)
-
亚马逊网络服务 弹性 Beanstalk (EB)
-
亚马逊网络服务 弹性容器服务 (ECS)
-
亚马逊网络服务 弹性 Kubernetes 服务 (EKS)
-
微软 Azure Kubernetes 服务 (AKS)
-
DigitalOcean Docker Swarm
这些服务中的大多数支持通过 Kubernetes 运行一组 Docker 容器(kubernetes.io/
),这是一个由 Google 发起的项目。多年来,Google 一直在运行名为 Borg 的容器编排系统(ai.google/research/pubs/pub43438
),并且 Google 以此为灵感,创建了一个适用于外部使用的容器编排系统,并命名为 Kubernetes。
一些托管云服务支持 Docker Swarm,而其他服务(包括 AWS Elastic Beanstalk 和 AWS ECS)则拥有自己定制的编排系统。
所有容器编排系统都允许软件开发人员和系统管理员运行一组服务器,这些服务器能够同时执行多个容器,并通过基于策略的机制在集群中分配多个容器实例。容器编排器负责启动、监控并根据健康检查和扩展限制在主机之间移动容器工作负载。自从 Google 推广了运行这些容器编排系统以来,许多供应商已经制定了托管服务,包括 Google、微软、亚马逊网络服务、Digital Ocean 等,接下来我们将在以下小节中讨论这些供应商。
Google Kubernetes 引擎
Google 提供了一种名为 Google Kubernetes Engine (GKE) (cloud.google.com/kubernetes-engine/
) 的系统,提供在 Google Cloud 内运行的支持的 Kubernetes 集群。如果你使用该服务,你无需自己操作和升级 Kubernetes 集群的主节点;你将根本看不到云控制台中的主节点,因为它们由 Google 直接管理。此外,Google 不向客户收取运行这些 Kubernetes 主节点的费用。这个选项对开发人员具有吸引力,因为它提供了一种运行低成本 Kubernetes 集群的方式。直接获得 Google 支持来运行 Kubernetes 工作负载,也让一些客户对这个系统充满信心。
然而,Google Cloud 不是最大的云服务提供商,甚至连第二大都不是,而且 Google Cloud 提供的其余服务种类没有像 Azure、AWS 或其他云提供商(如阿里巴巴)那样丰富。
如果你已经投资 Google Cloud,或者你希望拥有一个低成本的环境来试验 Kubernetes 或将其投入生产,且不依赖其他云服务提供商的服务,可以考虑评估 GKE 来运行 Docker 和 Kubernetes 负载。
AWS Elastic Beanstalk
亚马逊 Web 服务通过其平台即服务产品 Elastic Beanstalk (aws.amazon.com/elasticbeanstalk/
) 提供了一种运行 Docker 应用程序的方法。你可以运行单个 Docker 容器或支持多个 Docker 容器的设置。如果选择多个容器,Elastic Beanstalk 会在后台使用 ECS。通过 Elastic Beanstalk,开发人员使用命令行工具简化部署到多个环境,同时结合一些简洁的配置文件,隐藏了运行自动扩展集群的一些复杂性。
设置 Elastic Beanstalk 比设置 ECS 或 EKS 更加简单,开发人员如果需要一个轻松的途径以低开销和最小化的配置进入生产环境,可能会考虑使用 Elastic Beanstalk。
AWS ECS 和 Fargate
AWS 还提供了一种容器编排系统,叫做 ECS (aws.amazon.com/ecs/
)。ECS 有两种基本模式:一种是容器运行在由账户拥有者直接管理的 EC2 实例群上,另一种是 AWS 管理容器运行的节点,称为 Fargate (aws.amazon.com/fargate/
)。
如果你在 AWS 上有投入,使用 ECS 与 EC2 或 Fargate 结合可能是合理的。虽然这种路径让你可以在不需要处理 Kubernetes 或 Docker Swarm 的情况下部署容器,但它是一个只有 AWS 支持的专有系统,因此与使用 Kubernetes 或 Docker Swarm 作为编排器相比,你需要做额外的工作才能将系统迁移出去。它有自己的学习曲线,并且需要你承诺将 Docker 工作负载运行在 AWS 上,因为这些接口是 AWS 特定的。
AWS EKS
亚马逊云服务(AWS)提供 EKS,这是一个托管的 Kubernetes 服务,将 Kubernetes 主服务器的维护和配置工作转交给 AWS。EKS 是 AWS 的 Google GKE 等效服务。它与其他 AWS 服务有着强大的集成,尽管在运行 Kubernetes 主节点的成本上不如 GKE 服务经济,但与运行繁忙应用程序的成本相比,基准成本仍然是适中的。自 2018 年以来,AWS 通常通过 EKS 支持 Kubernetes,并且已修复了启动时出现的一些初期问题(例如,缺乏对一些常见自动扩展策略的支持),使 EKS 成为一个强大的 Kubernetes 发行版。2019 年 12 月,AWS 宣布通过 Fargate 运行由 EKS 管理的 Kubernetes 容器的支持,将 AWS 对 EKS 的支持与其提供的托管容器运行时及弹性透明的资源供应结合在一起。
截至 2020 年初,AWS 提供了来自云服务提供商的最大和最全面的服务。如果你在 AWS 上有投资,并且希望走一条许多人已经走过的熟悉道路,可以考虑使用 AWS EKS 作为你的 Kubernetes 主环境。
Microsoft Azure Kubernetes 服务
Microsoft Azure 提供强大的容器部署服务——Azure Kubernetes 服务(AKS)。如果你或你的公司已经在 Microsoft 平台工具上进行了大量投资,包括 Windows、Visual Studio Code 或 Active Directory,这个选项可能特别有吸引力。Microsoft 声称在这些方面提供了强大的支持。微软的开发工具通常比一些其他公司的工具有更平缓的学习曲线。然而,如果你过于依赖 Microsoft 栈中的元素,可能会更难迁移到其他解决方案。
如果你在一个 Microsoft 环境中工作,或者你想要一个紧密集成到 Visual Studio Code 的 Kubernetes 简单入门,可以考虑使用 AKS。
Digital Ocean Docker Swarm
Digital Ocean 提供通过 Docker Swarm 运行一组容器的支持,这是一个相对简单的容器编排系统。与在 Kubernetes 或甚至 AWS ECS 上部署容器相比,这项技术以其易于部署而闻名。Docker 工具本身就支持直接部署到 Docker Swarm。
然而,在 Mirantis 收购后,Docker Swarm 的支持状态被弃用,随后在客户要求持续支持后复兴。鉴于主要供应商对其支持的态度摇摆不定,你应仔细考虑是否应该使用 Docker Swarm 来部署新应用程序。
既然我们已经了解了在生产环境中运行 Docker 应用程序的替代方案,接下来让我们看看使用 Docker 和 Kubernetes 运行应用程序的替代方案。
运行你自己的 Kubernetes 集群 —— 从裸金属到 OpenStack
如果你必须在本地、数据中心运行应用程序,或者需要跨多个云计算提供商运行,你可能需要运行自己的 Kubernetes 集群。一旦你了解了在本地或混合云环境中运行 Docker 和 Kubernetes 的利弊,你应该能够知道何时是适合的解决方案。虽然这些场景比使用托管服务更复杂,但它们可以提供不同的好处,列举如下:
-
按照自己的时间表升级集群软件(或不升级),完全控制今天和明天运行的版本。云服务商可能会滞后于支持的版本,或者通过弃用某些版本带来操作风险。
-
使用许多成熟的 Kubernetes 配置解决方案之一,例如 Kops,这些解决方案有助于在 AWS EC2 上设置 k8s 集群。
-
跨数据中心和云计算环境操作混合云解决方案。虽然一些云服务提供商的解决方案,如 Google Cloud Anthos 或 Azure Arc,支持混合环境,但很多并不支持。
-
在裸金属上运行高性能 Kubernetes 集群,没有虚拟机管理程序的开销。
-
在主要云服务提供商不支持的平台上运行,例如在一群树莓派计算机上运行 Docker 和 Kubernetes。
-
完全控制集群的支持基础设施,并与使用 Kubernetes 作为起点的平台集成,例如 OpenShift 平台。
-
在私有云解决方案上运行,例如 OpenStack 或 VMware Tanzu(前身为 VMware Enterprise PKS)。
-
将 Docker 容器作为一个综合计算平台的一部分运行,该平台拥有超出原生 Kubernetes 的其他主要特性和功能,例如 Red Hat OpenShift 或 Rancher。
实际上,运行这些解决方案要比依赖单主机部署 Docker 或供应商管理的软件即服务 Kubernetes 集群解决方案更为复杂。
决定正确的 Docker 生产环境设置
由于可选择的选项令人眼花缭乱,选择正确的路径将应用程序部署到生产环境中是令人生畏的。你可能需要权衡许多因素,包括以下内容:
-
设置:从本地开发到生产环境的难度如何?
-
特性:部署、测试、监控、警报和成本报告。
-
成本:初始费用和持续的月度费用。
-
支持:是否可以轻松获得来自供应商或社区的支持?
-
弹性:随着负载增加,是否能扩展,并具备自动或手动控制?
-
可用性:该设置能否在丢失服务、主机或网络时仍然存活?
-
粘性:更改部署策略有多困难?
在单一主机上运行 Docker 成本低,设置简单,但在扩展性和可用性方面表现较差。所有主要的支持 Kubernetes 的云编排服务在功能、扩展性和可用性方面都表现均衡,但它们的设置和操作更为复杂。非 Kubernetes 选项比 Kubernetes 选项更加粘性。无论是在云中、裸金属服务器上,还是在混合云环境中运行你自己的集群,虽然可以提供巨大的灵活性,但也增加了复杂性和支持负担。
学习这些系统的相对优缺点,将帮助你判断合适的技术组合,用于部署你的应用程序。以下矩阵展示了我对不同技术选项的快速判断,评分范围为 1 到 5,其中 5 分表示最好。
你可以使用这个矩阵来帮助对替代方案进行排名。通过比较两个或更多选择,你可以更好地了解哪种解决方案更适合。为了评估这个矩阵,你可以建立一个评估表格,将各个替代方案进行对比。如果你用数字对优先级进行排名,其中 5 为最高优先级,1 为最低优先级,你可以将优先级与表 1 中的得分相乘,从而得到一个加权分数。
以下矩阵展示了强调设置简便、成本最小化以及粘性最小化的优先级,同时忽视了高可用性或负载下弹性这类鲁棒性的因素。这一优先级设置与许多现实世界应用程序在初期启动时的优先需求一致——开发人员面临的挑战往往是快速启动应用程序,并且可以在其他因素上做一些妥协。替代方案列中的评分代表将优先级与生产替代方案排名表中的每个替代方案得分相乘的结果。
在这种情况下,替代方案 1,Docker 在单一主机上运行,具有最高的评分,78 分对比 74 分。重要的因素——设置、成本和粘性,结合加权值使其超越了其他替代方案。考虑到这一评分,你应该考虑使用该部署方案。然而,如果优先考虑可用性或弹性,哪怕只高一个等级,另一个替代方案——Google Cloud GKE,可能会成为排名更高的服务。
你可能会发现一个混合解决方案也能满足你的需求,其中多个解决方案同时适用且必要,以解决你的问题。例如,你可能发现日常需求偏向于本地集群,但高峰需求可能需要扩展到云端。
练习——加入 ShipIt Clicker 团队
假设你刚刚加入了 ShipIt Clicker 开发团队。团队中的其他成员已经创建了游戏的基本设计(参见游戏设计文档),并编写了一个原型,只具备构建、测试和使用 Docker 打包应用程序所需的最低功能。
其他团队成员可能是设计、前端或后端开发方面的专家,但他们不确定如何继续进行生产环境部署。此时,你在 Docker 的使用经验上超过了团队中的其他开发者。他们编写的 Dockerfile 和docker-compose.yml
文件是有效的。
在本地工作站上运行专为本章制作的 ShipIt Clicker,帮助更好地理解它是如何搭建起来的。
运行docker-compose up
命令,以便在本地机器上启动容器。这将使你能够评估部署选项,并实验为生产环境使用而准备应用程序的更改。你将看到类似以下的输出;我们将详细解释输出中每组行的含义:
$ docker-compose up
Building shipit-clicker-web
Step 1/11 : FROM ubuntu:bionic
---> 775349758637
Step 2/11 : RUN apt-get -qq update && apt-get -qq install -y nodejs npm > /dev/null
---> Using cache
---> f8a9a6eddb8e
前面的输出显示 Docker 正在使用ubuntu:bionic
镜像,然后安装操作系统包。
Dockerfile 的第 3-5 步通过创建必要的目录并将节点模块的包配置文件复制到适当位置,准备了应用程序安装的容器镜像:
Step 3/11 : RUN mkdir -p /app/public /app/server
---> Using cache
---> f7e56a628e8b
Step 4/11 : COPY src/package.json* /app
---> eede94466dc7
Step 5/11 : WORKDIR /app
---> Running in adcadb6616c2
Removing intermediate container adcadb6616c2
---> 6256f613803e
接下来,Dockerfile 安装了节点模块:
Step 6/11 : RUN npm install > /dev/null
---> Running in 02ae124cf711
npm WARN deprecated superagent@3.8.3: Please note that v5.0.1+ of superagent removes User-Agent header by default, therefore you may need to add it yourself (e.g. GitHub blocks requests without a User-Agent header). This notice will go away with v5.0.2+ once it is released.
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.2.11
npm WARN shipit-clicker@1.0.5 No repository field.
npm WARN shipit-clicker@1.0.5 No license field.
Removing intermediate container 02ae124cf711
---> 64ea4b348ed1
在这之后,Dockerfile 会将更多的配置文件复制到容器镜像中,并将应用程序的源代码复制到容器中的/app
目录下:
Step 7/11 : COPY src/.babelrc src/.env src/.nodemonrc.json /app/
---> 88e88c1bc35d
Step 8/11 : COPY src/public/ /app/public/
---> c9872fccc1c9
Step 9/11 : COPY src/server/ /app/server/
---> f6e76811659a
最后,Dockerfile 告诉 Docker 暴露哪个端口并如何运行应用程序:
Step 10/11 : EXPOSE 3000
---> Running in 75fbd217ef27
Removing intermediate container 75fbd217ef27
---> 03faaa0e8030
Step 11/11 : ENTRYPOINT DEBUG='shipit-clicker:*' npm run dev
---> Running in 0a44ab13b0d3
Removing intermediate container 0a44ab13b0d3
---> ab6e4da773e7
Successfully built ab6e4da773e7
此时,Docker 容器已经构建完成,并且 Docker 应用了latest
标签:
Successfully tagged chapter5_shipit-clicker-web:latest
WARNING: Image for service shipit-clicker-web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
docker-compose up
的强大功能接下来将展示出来,因为我们最初运行的这个命令不仅构建了应用程序的 Docker 容器,还同时启动了所有容器。当它启动容器时,会启动应用程序容器和 Redis 容器。Redis 容器在启动过程中会输出一些详细信息。我们的docker-compose up
命令输出继续显示容器启动的相关信息:
Starting chapter5_redis_1 ... done
Creating chapter5_shipit-clicker-web_1 ... done
Attaching to chapter5_redis_1, chapter5_shipit-clicker-web_1
redis_1 | 1:C 04 Feb 2020 06:15:08.774 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1 | 1:C 04 Feb 2020 06:15:08.774 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1 | 1:C 04 Feb 2020 06:15:08.774 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis_1 | 1:M 04 Feb 2020 06:15:08.776 * Running mode=standalone, port=6379.
redis_1 | 1:M 04 Feb 2020 06:15:08.776 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1 | 1:M 04 Feb 2020 06:15:08.776 # Server initialized
请注意,Redis 在作为 Docker 容器的一部分运行时,并不完全适应没有专门为其调优的 Linux 内核。这是一个使用 Docker 可能无法获得最佳结果的例子,但结果足够好:
redis_1 | 1:M 04 Feb 2020 06:15:08.776 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis_1 | 1:M 04 Feb 2020 06:15:08.776 * DB loaded from disk: 0.000 seconds
redis_1 | 1:M 04 Feb 2020 06:15:08.776 * Ready to accept connections
你可以看到 Redis 现在已经准备好运行。接下来,docker-compose
启动 ShipIt Clicker 容器,使用前面 ENTRYPOINT DEBUG
输出中给出的命令('shipit-clicker:*' npm run dev
):
shipit-clicker-web_1 |
shipit-clicker-web_1 | > shipit-clicker@1.0.5 dev /app
shipit-clicker-web_1 | > nodemon server --exec babel-node --config .nodemonrc.json | pino-pretty
shipit-clicker-web_1 |
shipit-clicker-web_1 | [nodemon] 1.19.4
shipit-clicker-web_1 | [nodemon] to restart at any time, enter `rs`
shipit-clicker-web_1 | [nodemon] watching dir(s): *.*
shipit-clicker-web_1 | [nodemon] watching extensions: js,json,mjs,yaml,yml
shipit-clicker-web_1 | [nodemon] starting `babel-node server`
shipit-clicker-web_1 | [1580796912837] INFO (shipit-clicker/47 on 52e6d59c6121): Redis connection established
shipit-clicker-web_1 | redis_url: "redis://redis:6379"
shipit-clicker-web_1 | [1580796913083] INFO (shipit-clicker/47 on 52e6d59c6121): up and running in development @: 52e6d59c6121 on port: 3000}
一旦完成,你就可以通过浏览器访问 http://localhost:3005/
来玩游戏。在下图中,我们可以看到游戏主菜单的输出,并且有一个指向 API 文档的链接 http://localhost:3005/api-explorer/
:
图 5.1 – ShipIt Clicker 游戏主菜单
一旦你使应用运行起来并探索它,你就可以学习如何以不同的方式部署它。
练习 – 从合理的部署替代方案中选择
本章中的设置用于在本地开发环境中运行游戏。然而,该设置存在一些可能会导致生产部署问题的问题。
该游戏在原型阶段的初步受众如下:
-
你的游戏开发同事和公司管理团队
-
一个全球分布的团队,由报名参加 Alpha 项目的热心人组成
-
一个位于离你生活的地方十二个时区远的专业测试人员团队
管理层希望尽快让原型可以供 Alpha 测试志愿者和专业测试人员使用,但他们想知道支持更强大的部署环境的选项和成本,以便在游戏爆红或投资者批准广告活动以提升订阅量时能够扩展。
你的任务是,根据你对 Docker 以及生产环境部署替代方案的了解,执行以下任务:
-
向管理层建议,首个生产部署应该是什么,在构建生产决策替代方案表格后。
-
向管理层建议,首个部署的一个或多个合理替代方案,这些方案能提高弹性和可用性。
-
根据供应商的最新价格清单,构建一个电子表格模型,展示每个选项在第一年内产生的一次性和持续性成本。
解决方案
将你的决策矩阵与决定正确的 Docker 生产环境设置章节中的示例进行比较,看看你的结果是否有所不同。将成本的电子表格模型和你的决策矩阵展示给同事,询问他们可能会选择什么,并询问他们是否同意你的决定。
练习 – 评估 Dockerfile 和 docker-compose.yml
管理层希望你能多做一些工作,帮助平滑生产部署的路径。它们希望你能识别需要改进的领域:
-
在
Dockerfile
和docker-compose.yml
文件中做出的选择对于该应用来说是否合理? -
为了更好地准备应用程序进行生产部署,应该做出哪些选择?
-
选择商品操作系统发行版时,在
FROM
中选择容器基础镜像会有什么影响?
解决方案
查看github.com/PacktPublishing/Docker-for-Developers/tree/master/chapter6
中的 Dockerfile 和 docker-compose.yml
文件版本,看看你的建议如何与之对接。我们将在第六章《使用 Docker Compose 部署应用程序》中详细探讨这一点。
现在我们已经了解了更多关于将 Docker 容器部署到生产环境的替代方案,并完成了一些实践练习,让我们回顾一下我们学到的内容。
总结
在本章中,我们了解了将基于 Docker 的应用程序部署到生产环境的不同替代方案。我们了解到,许多选择涉及权衡,并且学习了如何构建最小可行的生产环境。我们学习了如何在不同的云服务提供商及其托管的容器运行时之间做出选择,并且了解了在本地或混合云中运行 Docker 的好处。我们还学习了如何根据不同目标选择合适的生产路径来部署 Docker 容器。
基于这些经验教训,你可以应用所学内容来创建一个实际的生产部署。了解技术替代方案的足够背景非常重要——因为不同的策略提供了不同的优缺点。你的公司可能在未来需要一个超级强大的自动扩展部署,但今天可能只需要一个能正常工作的方案。
在下一章中,我们将展示如何创建一个健壮的单主机 Docker 生产部署,同时保持本地开发的能力。
第六章:使用 Docker Compose 部署应用
使用 Docker 部署应用的最简单实用场景是通过在单主机上运行 Docker Compose。你作为开发人员使用的许多命令,例如 docker-compose up -d
,同样适用于在单主机上部署 Docker 应用。
在单主机上运行 Docker 应用比使用更复杂的容器编排系统运行它们更容易理解,因为你可以使用许多与运行非 Docker 应用相同的技术;然而,在性能和可用性方面,它存在一些显著的缺点。
在本章中,你将发现为何这是最简单的实用选项,学习如何为单主机生产环境配置 Docker,并掌握一些高效管理和监控简单部署的技巧。此外,你还将更好地理解在单主机上运行 Docker 的缺点,包括可能面临的问题。
本章将涵盖以下主要主题:
-
为单主机部署选择主机和操作系统
-
为 Docker 和 Docker Compose 准备主机
-
使用配置文件和支持脚本进行部署
-
监控小型部署——日志记录和警报
-
单主机部署的限制
技术要求
要完成本章的练习,你需要在本地工作站上安装 Git 和 Docker,并且需要一台能够运行 Linux 和 Docker 的单主机作为生产服务器,该主机需连接到一个可以通过 SSH 访问且用户能够访问的网络。
本章的 GitHub 仓库可以在 github.com/PacktPublishing/Docker-for-Developers
找到——请参阅 chapter6
文件夹。
查看以下视频,查看代码实际应用:
示例应用 – ShipIt Clicker v2
本章中的 ShipIt Clicker 版本比我们在第五章中使用的版本更加完善,部署和运行生产环境中的容器的替代方案。它具有以下功能:
-
改进的 Dockerfile 和适合基本生产环境使用的
docker-compose.yml
文件 -
在 Redis 中存储游戏状态并与服务器会话绑定,导致不同客户端设备的游戏状态各异
-
改进的视觉和音频资源
我们将使用这个增强版本的 ShipIt Clicker 作为应用,通过 Docker Compose 在单主机上进行部署。
为单主机部署选择主机和操作系统
在单一主机上部署应用是生产环境中最简单的运行方式。从许多方面来看,它类似于使用 Docker 和 Docker Compose 进行本地开发的用户体验。如果你能够通过 docker-compose.yml
文件打包应用的各个部分,那么你已经完成了 70% 的工作。如果你已经具备基本的 UNIX 或 Linux 系统管理技能,那么这将非常简单——这个策略所需的努力最少,你可以在一到两个小时内掌握要点。
单主机部署的要求
为了继续进行部署,你需要一台运行现代 Linux 操作系统且架构与开发系统相同的计算机,且该计算机应具备足够的内存、处理器和存储容量来运行你的应用。如果你是在使用 Docker Community Edition 的 Windows 10 64 位桌面上进行开发,你需要一台使用 x86_64 架构的 Linux 系统。如果你是在运行 Raspbian 的 Raspberry Pi 4 上使用 Docker,你需要一台 ARM 架构的服务器。实际上,你可以使用任何裸金属服务器或虚拟机服务器,无论是在本地还是云端,只要它支持 Docker。
一些云服务提供商,如 Amazon Web Services (AWS),为其最小虚拟机部署提供免费层,至少是第一年。在本章中的示例可以在类似的主机上运行,但如果你的应用较大,你可能需要使用更大、更昂贵的系统。
生产环境中的应用通常需要 247* 不间断运行,且这些应用的用户可能有可靠性方面的担忧。虽然在单一主机上运行 Docker 应用可能是最不可靠的方式,但对于某些应用来说,这可能已经足够。像 HP、Dell 和 IBM 这样的厂商提供的单主机可靠性措施在很多情况下已经足够确保应用的可靠性,前提是你的应用需要这种级别的可靠性。
你需要以下支持 Docker 的 Linux 操作系统发行版之一:
-
Red Hat Enterprise Linux(或 CentOS)7 或 8
-
Ubuntu 16.04 或 18.04 或更新版本
-
Amazon Linux 2
-
Debian Stretch 9
-
Buster 10
为了最小化生产时间并最大化便捷性,选择你已经熟悉的操作系统,或者使用 CentOS 7,后续示例中将使用此版本。
仅当你希望采用更慢且更高级的生产路径时,才选择专注于 Docker 的发行版,如 Container Linux 或 CoreOS,因为在这些环境下,你的系统管理技能可能效果不佳。例如,CoreOS 中的用户管理与主流发行版的方式大不相同。
因为这个策略仅依赖于用户能够访问的主机,你将拥有极大的灵活性。
为 Docker 和 Docker Compose 准备主机
在你配置主机上的软件之前,你应确保它具有一个稳定的 IP 地址。有时这些被称为静态 IP 地址,或者在 AWS 中称为弹性 IP 地址。你可能需要通过提供商特别分配这些 IP 地址,通常可以通过提供商的控制台进行操作,例如 AWS Lightsail 中的网络选项卡,或者 AWS EC2 控制台中的弹性 IP设置。
此外,你应该映射一个地址(例如使用shipitclicker.example.com
而不是原始 IP 地址,如192.2.0.10
)。所有公共云系统都能够管理 DNS 条目——例如,AWS Route 53,大多数虚拟主机系统也具备此功能。
使用操作系统软件包安装 Docker 和 Git
你需要在主机上安装 Docker。对于生产环境,避免使用操作系统发行版中附带的过时 Docker 版本,尽量使用 Docker 为 Docker 社区版发布的操作系统软件包。你可以在 Docker 官网上找到针对各种操作系统的 Docker 社区版安装说明,具体如下:链接
-
CentOS: https://docs.docker.com/install/linux/docker-ce/centos/
-
Debian: https://docs.docker.com/install/linux/docker-ce/debian/
-
Fedora: https://docs.docker.com/install/linux/docker-ce/fedora/
-
Ubuntu: https://docs.docker.com/install/linux/docker-ce/ubuntu/
-
二进制文件: https://docs.docker.com/install/linux/docker-ce/binaries/
对于 CentOS 7 的全新安装,使用以下命令:
$ sudo yum install -y yum-utils
$ sudo yum install -y device-mapper-persistent-data lvm2
$ sudo yum-config-manager --add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum install -y docker-ce docker-ce-cli containerd.io
将你常用的非 root 用户添加到 Docker 用户组,并在当前终端会话中成为该组的成员:
$ sudo usermod -aG docker $USER
$ newgrp docker
确保 Docker 服务已启用,这样它会在启动时自动启动,并且 Docker 服务已经启动:
$ sudo systemctl enable docker
$ sudo systemctl restart docker
按照docs.do
cker.com/compose/install/上的说明安装docker-compose
。截至 2020 年 1 月,1.25.3
是最新版本,但请检查该页面上的版本号,使用最新版本填入以下命令,这应该是单行命令:
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.25.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
现在你已经启动并启用了 Docker 守护进程,并且安装了docker-compose
,你可以部署你的应用程序了。
接下来,通过操作系统的包管理器安装 git
。对于 Red Hat 系列的发行版(如 RHEL、CentOS、Fedora 和 Amazon Linux),请使用以下命令:
$ sudo yum install -y git
对于 Debian 系列的发行版(包括 Ubuntu),请运行以下命令:
$ sudo apt-get update && apt-get install -y git
到这时,主机已准备好部署 Docker 应用程序。为了完成部署,我们将使用一种依赖于 shell 脚本和 Docker 环境配置文件的策略。
使用配置文件和支持脚本进行部署
为了将我们的应用程序部署到生产服务器,我们将使用简单命令和支持脚本的组合来启动或更新正在运行的容器集。让我们首先仔细看看部署所需的两个最重要的文件:Dockerfile
和 docker-compose.yml
。
重新审视初始 Dockerfile
来自 第五章的 Dockerfile,在生产环境中部署和运行容器的替代方案,具有良好的分层,并且在 RUN npm -s install
执行之前和将应用程序的主要部分复制到镜像之前,已经将 package.json
和 package.json.lock
复制到镜像中。然而,它也有一些不足之处,我们将在本章中进行改进,以便为生产部署做好充分准备。首先,让我们看看初始的 Dockerfile:
FROM ubuntu:bionic
RUN apt-get -qq update && \
apt-get -qq install -y nodejs npm > /dev/null
RUN mkdir -p /app/public /app/server
COPY src/package.json* /app
WORKDIR /app
RUN npm -s install
COPY src/.babelrc \
src/.env \
src/.nodemonrc.json \
/app/
COPY src/public/ /app/public/
COPY src/server/ /app/server/
EXPOSE 3000
ENTRYPOINT DEBUG='shipit-clicker:*' npm run dev
前面的 ShipIt Clicker 游戏原型的 Dockerfile 从本地开发的角度来看有很多地方是正确的,但也有一些限制,我们将在本章的 Dockerfile 中解决这些问题。
很多时候,开发人员会从一个基础镜像开始(例如 FROM ubuntu:bionic
),这个镜像通常是他们最熟悉的:传统的 Linux 发行版,通常在工作站上运行。这可能有助于最初调试 Dockerfile,但代价很高,因为基础镜像和生成的镜像都非常大,包含数百兆字节。此外,Ubuntu 的软件包安装过程相当冗长,因此 apt-get install
命令必须将 stdout
重定向到 /dev/null
以防止冗长的输出 占用终端(参见 askubuntu.com/a/1134785
)。
初始 Dockerfile 的其余部分有一些常见的缺陷,您应该避免在生产环境中使用,比如复制所有开发工具的配置文件(参见 COPY
命令,它复制点文件)。初始 Dockerfile 有一个入口点(ENTRYPOINT
),它指向一个更适合开发的服务器,而非生产环境,因为这种方式定义起来快速而简便。真正的生产设置需要一个构建步骤来创建适合分发的资源集,还需要一个不同的 npm
命令来使用这些资源启动应用程序。
本章的 Dockerfile 修正了所有这些问题:
FROM alpine:20191114
RUN apk update && \
apk add nodejs nodejs-npm
RUN addgroup -S app && adduser -S -G app app
RUN mkdir -p /app/public /app/server
ADD src/package.json* /app/
WORKDIR /app
RUN npm -s install
COPY src/public/ /app/public/
COPY src/server/ /app/server/
COPY src/.babelrc /app/
RUN npm run compile
USER app
EXPOSE 3000
ENTRYPOINT npm start
在这个修改后的 Dockerfile 中,我们使用了 Alpine Linux 而不是 Ubuntu,目的是生成更小的镜像,同时我们固定了 Alpine 的版本,以确保构建的一致性。基于 Alpine Linux 的容器镜像体积比 Ubuntu 小 71%:
$ docker images | awk '/chapter._ship/{ print $1 " " $7}'
chapter6_shipit-clicker-web-v2 154MB
chapter5_shipit-clicker-web 524MB
在修改后的 Dockerfile 中,我们还创建了一个app
用户,这样 Docker 就可以以普通的 UNIX 用户身份运行应用程序,而不是root
用户,因为后者可能会加剧安全问题。
在尽可能无声地安装操作系统包和npm
包后,我们可以将应用程序文件和.babelrc
配置文件复制到/app
目录,然后运行RUN npm run compile
来准备生产版本的节点应用程序,我们以app
用户身份运行它,并使用ENTRYPOINT npm start
。
重新审视初始的 docker-compose.yml 文件
前一章中的初始docker-compose.yml
文件完成了启动 Web 和 Redis 容器的任务,但存在一些不足。初始的docker-compose.yml
文件是从 Docker 文档中的简单示例[example改编而来,位于docs.docker.com/compose/
,因此它在生产使用时还有一些缺陷。许多开发者在没有考虑到某些生产部署时需要注意的细微差异的情况下,直接适应这些示例。你可以将它视为起点,而非终点。初始的docker-compose.yml
文件如下:
---
version: '3'
services:
shipit-clicker-web:
build: .
environment:
REDIS_HOST: redis
ports:
- "3005:3000"
links:
- redis
redis:
image: redis
ports:
- "6379:6379"
本章修改后的docker-compose.yml
文件更加健壮。这个文件部分灵感来自于 https://github.com/docker-library/redis/issues/111,尤其是 GitHub 用户@lagden
提供的示例,其中展示了一个支持 Redis 的docker-compose.yml
文件的优秀示例:
---
version: '3'
services:
shipit-clicker-web-v2:
build: .
environment:
- APP_ID=shipit-clicker-v2
- OPENAPI_SPEC=/api/v1/spec
- OPENAPI_ENABLE_RESPONSE_VALIDATION=false
- PORT=3000
- LOG_LEVEL=${LOG_LEVEL:-debug}
- REQUEST_LIMIT=100kb
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- SESSION_SECRET=${SESSION_SECRET:-mySecret-v2}
请注意,我们显式地为应用程序定义了所有环境变量,其中有几个变量使用${VARIABLE_NAME:-default_value}
语法,该语法利用环境变量的值。可以在命令行、常见的配置文件(如:$HOME/.profile
、$HOME/.bashrc
)或者与docker-compose.yml
文件在同一目录中的.env
文件中指定这些变量:
ports:
- "${PORT:-3006}:3000"
networks:
- private-redis-shipit-clicker-v2
links:
- redis
depends_on:
- redis
上述的ports
部分定义了主容器的网络配置;它定义了一个名为private-redis-shipit-clicker-v2
的私有网络,该网络将两个容器连接起来。请注意,这部分使用了depends_on
。这意味着 ShipIt Clicker 容器将在 Redis 容器启动后才会启动。接下来,让我们看看 Redis 容器的定义:
redis:
command: ["redis-server", "--appendonly", "yes"]
image: redis:5-alpine3.10
volumes:
- redis-data-shipit-clicker:/data
networks:
- private-redis-shipit-clicker-v2
volumes:
redis-data-shipit-clicker: {}
networks:
private-redis-shipit-clicker-v2:
这个文件包含许多环境变量条目——例如 LOG_LEVEL
、REDIS_HOST
和 REDIS_PORT
——允许轻松覆盖。这使得重写 Redis 主机设置变得容易,无论是为了更方便的调试,还是为了便于连接到云端 Redis 服务。它通过命令行参数启动 Redis,并启用持久化,同时分配一个 Docker 持久卷来存储 Redis 的仅附加日志文件。否则,每次 Redis 容器重启时,数据都会丢失。它使得 Redis 和 Web 服务器之间的网络通信变为私密。这一点尤其重要,因为 Redis 的默认配置下,Redis 服务器在没有任何身份验证或授权的情况下运行——这意味着任何能够连接的人都可以访问!
在这个简约的、适用于生产环境的 docker-compose.yml
文件中,我们将 Web 服务器直接暴露在 80
端口上供全世界访问。这是可行的,但现代浏览器会对纯 HTTP 内容显示安全警告。虽然这能让你顺利进入生产环境,但许多生产应用程序需要比纯 HTTP 更强的安全保护。你可以通过使用代理或外部负载均衡器,在 443
端口终止 HTTPS,或者通过配置 SSL 证书来绕过这个问题。我们将在后续章节中详细介绍这一点。
docker-compose
v3 配置的一个特点是它设置了容器失败时的默认行为为始终重启。即使主机重新启动,这也应该发生,如果进程由于未处理的异常退出,这种行为也一定会发生。如果你需要更直接地配置应用程序的重启行为,可以参考文档中列出的设置,链接为 docs.docker.com/compose/compose-file/#restart_policy
。
准备生产环境的 .env 文件
克隆仓库并准备配置 docker-compose
:
$ git clone https://github.com/PacktPublishing/Docker-for-Developers.git
$ cd Docker-for-Developers/chapter6
为了配置生产环境中的应用程序,你应该在 docker-compose.yml
文件所在的目录中创建一个名为 .env
的文件。如果你想更改任何默认设置——例如,将生产环境中的调试级别从 info
改为 debug
——你应该通过创建和编辑与生产部署相关的 .env
文件来进行修改。将 env.sample
文件复制为 .env
并根据你的生产需求进行编辑。
处理密钥
这个示例应用程序使用环境变量和 .env
文件来存储密钥。这符合12 因子应用原则(详见 https://12factor.net/config),但这并不是处理密钥的唯一方法,也不是最安全的方法。你可以使用密钥管理系统,例如 HashiCorp Vault 或 Amazon Secrets Manager,来存储和检索密钥。我们将在第八章《将 Docker 应用部署到 Kubernetes》和第十四章《高级 Docker 安全性 - 密钥、密钥命令、标签和标签》详细介绍这一点;但目前,我们先使用环境变量来处理密钥。
你应该用随机的密钥替换环境变量中的 SESSION_SECRET
密钥,并确认是否要将端口 80
暴露给外界。使用你熟悉的编辑器,不论是 vi
、emacs
还是 nano
:
cp env.sample .env
vi .env
设置好环境变量覆盖后,你可以部署应用程序。
首次部署
一旦你把 .env
文件放置好,后台启动服务以部署应用程序:
$ docker-compose up -d
按照以下步骤验证服务是否正在运行:
$ docker-compose ps
Name Command State Ports
-----------------------------------------------------
chapter6_redi docker- Up 6379/tcp
s_1 entrypoint.sh
redis ...
chapter6_ship /bin/sh -c Up 0.0.0.0:80-
it-clicker- npm start >3000/tcp
web-v2_1
检查系统日志是否显示任何错误:
$ docker-compose logs
只要在日志中没有看到一连串的错误信息,你应该能够通过服务器的 IP 地址访问该网站,例如 http://192.0.2.10
,用你的 IP 地址替代。如果你使用 DNS 分配了主机名,你应该可以通过主机名访问它—例如,例如,shiptclicker.example.com
,用这个域名替代原始域名。
排查常见错误
如果你遇到类似的错误,你需要确保主机上没有运行其他的 Web 服务器,例如 Apache HTTPD 或 NGINX:
docker.errors.APIError: 500 Server Error: Internal Server Error ("b'Ports are not available: listen tcp 0.0.0.0:80: bind: address already in use'")
如果你遇到这个问题,你应该卸载主机上运行的 Web 服务器,或者更改它监听请求的端口。你也可以通过更改 .env
文件中的 PORT
变量,改变 ShipIt Clicker 运行的端口。对于 Red Hat 系列系统,监听端口 80
的服务器很可能是 Apache HTTPd,你可以使用以下命令来移除它:
$ yum remove -y httpd
对于 Debian 系列的系统,通常也可能是 Apache,你需要使用以下命令来移除它:
$ apt-get remove -y apache2
可能你的系统上运行着其他 Web 服务器。你可以使用 netstat
查找你 Web 服务器的进程名称:
$ sudo netstat -nap | grep :80
tcp6 0 0 :::80 :::* LISTEN 12037/httpd
你可能不需要进行故障排除就能让应用程序在 Docker 中运行,但在单主机部署场景下,你可以使用系统管理员的故障排除技能来找出可能出错的地方。
一旦应用程序运行起来,您可能会发现您经常运行一些相同的操作,例如在进行更改后重建应用程序。这就是支持脚本派上用场的地方。
支持脚本
在生产环境中运行站点时,您可能经常需要执行一些操作。记住重新启动和更新运行系统或连接到数据库所需的确切 Docker 命令序列变得很烦人。
您应该继续在本地工作站上开发您的应用程序,并使用生产系统将更改部署给您的用户。
在本章节中改进的网络设置中,不再可能通过直接 TCP 端口直接连接到 Redis 容器,因此我们将使用脚本内的docker exec
来执行此操作。
如果你在Docker-for-Developers``/chapter6
目录中,可以通过以下命令将该目录永久添加到PATH
中,以便更方便地运行这些脚本:
$ echo "PATH=$PWD:$PATH" | tee -a "$HOME/.bash_profile"
$ . "$HOME/.bash_profile"
对于此应用程序来说,最常见的操作可能是重新启动应用程序,部署更改和连接到 Redis 进行故障排除。对于这些操作,我们将使用restart.sh
脚本,deploy.sh
脚本和redis-cli.sh
脚本。
重新启动
restart.sh
脚本将重新启动所有容器。在修改配置文件.env
后,您应该运行此命令。您可以简单地运行docker-compose up -d
,但仅此不足以告诉您更改是否生效。此命令还将为您运行docker-compose ps
,以显示更改后您的容器是否正确运行,包括端口映射。在以下示例会话中,我们完全删除.env
文件,然后仅使用PORT=80
设置重新创建它:
[centos@ip-172-26-0-237 chapter6]$ rm .env
[centos@ip-172-26-0-237 chapter6]$ deploy.sh
chapter6_redis_1 is up-to-date
Recreating chapter6_shipit-clicker-web-v2_1 ... done
Name Command State Ports
--------------------------------------------------------------------------------------------------
chapter6_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
chapter6_shipit-clicker-web-v2_1 npm start Up 0.0.0.0:3006->3000/tcp
[centos@ip-172-26-0-237 chapter6]$ echo 'PORT=80' > .env
[centos@ip-172-26-0-237 chapter6]$ restart.sh
chapter6_redis_1 is up-to-date
Recreating chapter6_shipit-clicker-web-v2_1 ... done
Name Command State Ports
------------------------------------------------------------------------------------------------
chapter6_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
chapter6_shipit-clicker-web-v2_1 npm start Up 0.0.0.0:80->3000/tcp
[centos@ip-172-26-0-237 chapter6]$
您可以看到第二次运行restart.sh
时重新创建了chapter6_shipit-clicker-web-v2_1
应用程序,并且服务器现在通过通配符 IPv4 地址0.0.0.0
连接到端口80
。这将允许服务器在 URL 中没有特殊端口号的情况下响应 HTTP 请求。
部署
deploy.sh
脚本从git
上游仓库拉取更改,构建容器,并重新启动需要更新的所有容器。在本地测试完代码并对其进行了测试后,请使用此选项。
Redis
redis-cli.sh
脚本将允许您在命令行中连接到运行的 Redis 服务器。它使用docker exec
命令,在运行的容器中附加并启动新的redis-cli
命令。由于现在 Redis 在一个隔离的网络中运行,甚至从生产主机也不应该能够通过 TCP 套接字访问它。这将帮助您排除后端服务器的任何问题。
这里是展示redis-cli.sh
运行的示例会话:
[centos@ip-172-26-0-237 chapter6]$ ./redis-cli.sh
127.0.0.1:6379> help
redis-cli 5.0.7
To get help about Redis commands type:
"help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit
To set redis-cli preferences:
":set hints" enable online hints
":set nohints" disable online hints
Set your preferences in ~/.redisclirc
127.0.0.1:6379> keys *
1) "example/deploys"
2) "example/nextPurchase"
3) "example/score"
127.0.0.1:6379> get example/score
"209"
127.0.0.1:6379> quit
请注意,即使redis-cli.sh
脚本位于一个私有虚拟网络中(如果您在宿主机上安装了标准的redis-cli
程序,您将无法访问该网络),您仍然可以使用它连接到 Redis 服务器。依赖容器中的工具可以让您深入到应用程序的配置中,即便该应用程序被保护,不直接暴露在互联网上。
练习 – 避免在生产服务器上进行构建
本章的部署脚本执行最简单的更新方式:它在生产服务器上重建容器。然而,这可能导致资源耗尽并使生产服务器宕机。
在第四章《使用容器构建系统》中学到的关于 Docker Hub 的知识基础上,您如何修改应用程序开发的工作流程,以便修改docker-compose.yml
文件和deploy.sh
脚本,避免在生产服务器上构建 Docker 容器?
写下您将使用的工作流程的一两句话,并描述docker-compose.yml
配置文件需要做出哪些更改。
注意:
有多种方法可以实现这些目标,并且没有单一的答案来实现它们。您可以将您的答案与下一章中的docker-compose.yml
文件进行对比,看看您的想法与该章中构建容器的解决方案有何不同。
练习 – 规划如何保护生产站点
假设您从老板那里听到消息,ShipIt Squirrel 代码和生产系统将受到公司首席信息安全官的关注,他将全面审查并寻找潜在的安全漏洞。他担心在急于上线的过程中,采取了太多的快捷方式,他希望您提供更多信息。请回答以下三个问题:
-
如何通过 SSL 加密来保护客户端与服务器之间的通信?以下哪些是您应该做的?
a. 在程序内部终止 SSL。
b. 使用外部负载均衡器终止 SSL。
c. 在宿主机上使用 Web 服务器,但不在 Docker 中,来终止 SSL。
d. 使用 Docker 和 Web 服务器容器终止 SSL。
-
您计划如何定期更新 SSL 证书?
-
您能否找到当前系统中其他的安全弱点,可能存在于 Docker 层或 API 层?
部署应用程序并考虑一些安全增强措施后,您应该学习如何监控部署,以便在用户发现问题之前,先发现故障。
关于如何保护生产站点的答案:
问题 1的四个选项中任何一个都可以使用,但实践中选项b和d最为稳健和稳定。选项a难以正确实现,选项c需要对应用环境进行单独更新。
关于问题 2,您可以从供应商购买 SSL 证书,每年必须更新和重新安装,或者依赖负载均衡器的供应商自动更新您的证书(如果他们提供此选项),或者使用 Let's Encrypt 自动更新证书。有关使用 Let's Encrypt 更新证书以及使用一组 Docker 容器终止 SSL 的更多信息,请参阅下一章的进一步阅读部分。
问题 3是开放式的,但您应该注意的第一件事是chapter6
代码库中的 Web 服务没有内置认证或授权。
监控小型部署 - 日志记录和警报
一个好处是,从小开始,您可能可以依赖于非常简单的机制来处理日志记录和警报。对于在单个主机上使用 Docker 和Docker Compose
部署的任何部署(例如,ShipIt Clicker 的部署),您可以使用一些基本的工具和命令来处理日志记录,并使用第三方提供的各种简单的警报服务来处理警报。
记录
对于日志记录,在许多情况下,只需使用 Docker 内置的日志即可。Docker 捕获其启动的每个进程的标准输出和标准错误文件句柄,并将它们作为每个容器的日志提供。您可以使用以下命令查看自上次容器重启以来启动的所有服务的汇总日志,假设您在包含您的docker-compose.yml
文件的目录中(less -R
将解释logs
命令生成的 ANSI 颜色转义):
$ docker-compose logs 2>&1 | less -R
您还可以执行docker ps
以查找正在运行的容器的名称,以便检索其日志流:
[centos@ip-172-26-0-237 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
e947e7de33ef chapter6_shipit-clicker-web-v2 "npm start" 4 hours ago
Up 4 hours 0.0.0.0:80->3000/tcp chapter6_shipit-clicker-web-v2_1
3f91820e097b redis:5-alpine3.10 „docker-entrypoint.s…" 4 hours ago
Up 4 hours 6379/tcp chapter6_redis_1
一旦您获得了容器的名称,您可以分别检索每个运行容器的单独日志文件。您可以将它们管道传输到less
,或者将日志输出重定向到文件,例如:
[centos@ip-172-26-0-237 ~]$ docker logs chapter6_shipit-clicker-web-v2_1 > shipit.log
[centos@ip-172-26-0-237 ~]$ tail shipit.log
> shipit-clicker@1.0.0 start /app
> node dist/index.js
{"level":30,"time":1580087119723,"pid":16,"hostname":"e947e7de33ef","name":"shipit-clicker-
v2","msg":"Redis connection established","redis_url":"redis://redis:6379","v":1}
{"level":30,"time":1580087119934,"pid":16,"hostname":"e947e7de33ef","name":"shipit-clicker-
v2","msg":"up and running in development @: e947e7de33ef on port: 3000}","v":1}
[centos@ip-172-26-0-237 ~]$
此过程确实要求您登录到生产服务器并在那里运行一些命令,但实际操作中,这是检查运行在单个主机上的应用程序日志的好方法。
警报
要开始监视生产服务器上端口80
上的 HTTP 服务器以确保其保持运行,这就足够了。如果您可以访问公司的网络监控系统,例如 Nagios 或 Icinga 服务器,可以使用该系统。如果系统可以通过互联网访问,您可以使用免费的监控服务,例如uptimerobot.com
,监控服务器。
为了进一步扩展监控,您可能还需要监控内部服务,如 Redis。然而,在像这样简单的设置中,这更加具有挑战性。我们将在第十章中深入讨论高级监控系统,使用 Prometheus、Grafana 和 Jaeger 监控 Docker。
这里的基本思路是,如果系统出现故障,您希望收到电子邮件、短信或两者兼有的通知。
单主机部署的局限性
将 Docker 应用程序部署到单一主机时可能出现什么问题?很多!虽然单主机部署提供了操作上的简便性,但也存在一些重大局限性。接下来我们将详细讨论这些局限性。
没有自动故障转移
如果数据库服务器容器或 Web 服务容器发生故障且无法自动重启,站点将无法访问,并需要人工干预。这可能只是简单地发现您的监控系统显示站点已停机,然后您需要 SSH 进入并重启服务器。但有时,单个服务器的内存会非常低,以至于必须从更高层次的控制台手动重启,甚至手动断电再重启。这通常会导致应用程序长时间停机,无法响应请求。
无法横向扩展以接受更多负载
如果系统的流量超出了当前的容量,会发生什么?在单主机部署中,您可能能够将主机切换到更大的计算机,增加更多内存和处理器,这称为纵向扩展。在云环境中,这要比在需要处理物理硬件(如本地环境或数据中心环境)中要容易得多。而将这些简单的部署技术应用到整个服务器实例群组中——这被称为横向扩展——则要困难得多。
基于错误的主机调优跟踪不稳定行为
根据您的托管提供商、您所使用的基础操作系统以及 Docker 容器的配置,您可能会遇到很难追踪的不稳定性。也许由于提供商的网络检测到不稳定的硬件或网络条件,您的主机会频繁重启。也许您已将操作系统配置为安装自动更新,应用这些更新会导致一段时间的中断。也许应用程序的内存增长到触发某种故障。
为了简化起见,本章中的示例没有在应用程序或容器级别指定内存限制。这意味着,由于缺少在应用程序级主配置文件中的max_memory
设置,Redis 容器可能会消耗主机上所有可用内存。这也意味着,运行 Express Web 应用程序的节点容器可能会泄漏内存,直到操作系统的内存溢出 (OOM)杀手终止它,或者 Docker 守护进程终止。
缓解此问题的一种方法是通过使用交换文件或交换分区来配置主机的虚拟内存,这样系统看起来就像拥有比实际更多的物理内存。如果未在主机上配置交换文件,则可能会发现运行 deploy.sh
脚本时失败。发生这种情况时,控制台可能没有任何信息,但如果检查 /var/log/messages
,你会发现 Linux 内核的 OOM 杀手终止了 npm
安装程序或 Docker 容器构建过程的其他部分。
请参阅 Docker 文档,了解未为容器和操作系统适当配置内存的风险:
docs.docker.com/config/containers/resource_constraints/
单台主机的丢失可能会造成灾难性后果 – 定期备份至关重要
如果你的应用托管在单台物理服务器或虚拟服务器上,应该确保定期备份系统。许多提供商提供镜像备份服务,你可以配置该服务进行每日备份,并将备份保留一段时间,通常需要额外付费。你也可以使用传统方法脚本化备份关键卷,例如使用 TAR 和 SSH 或使用现代备份系统,比如 restic
(参见 restic.readthedocs.io/en/latest/
),将文件和卷备份到云存储系统。
案例研究 – 从 CoreOS 和 Digital Ocean 迁移到 CentOS 7 和 AWS
作者之一 Richard Bullington-McGuire,维护了一个冬季骑行比赛网站,freezingsaddles.org/
,该网站托管在 Digital Ocean 的一个虚拟机上,使用 CoreOS 运行超过一年。这个系统经常在重启后掉线,很难追踪导致周期性宕机的具体问题。由于无法访问 Digital Ocean 控制面板的控制台,且对 CoreOS 不熟悉,系统故障排查变得更加困难。为了确保系统有备份,安装并配置了 restic
,将备份发送到 Amazon S3。经过多次令人沮丧的系统管理经历后,系统迁移到了 AWS,使用 Lightsail,并运行 CentOS 7 作为主机操作系统。为了防止 OOM(内存不足)情况发生,新系统配置了一个与内存大小相等的交换文件。此后,系统停止了每隔几天随机宕机的问题,操作也变得更加顺畅。此外,新系统启用了每日自动快照备份,减少了使用像 restic
这样的应用级工具进行备份的需求。尽管如此,如果系统重启,Web 服务器有时仍无法平稳启动,需要手动干预才能恢复服务。
摘要
将基于 Docker 的应用程序推向生产环境的最简单方式是通过 Docker Compose 将其部署到单个主机上。如果你已正确准备好主机,安装了合适的软件,包括 Docker Compose,那么你可以在生产环境配置下将应用程序部署到该主机。这可以在几小时内完成,并且能高效地服务于低到中等性能和可用性需求的应用。如果你对配置文件做出正确调整,应用程序就可以准备好进行生产部署。通过使用封装长而繁琐命令的 shell 脚本,你可以更轻松地处理应用程序的常规维护和更新。在最简单的情况下,你可以使用外部监控和警报来处理此类应用,并以低成本解决此类问题。
你可以将本章所学的内容应用于提升支持你应用程序的 Dockerfile 和 docker-compose.yml
文件的复杂度。你可以编写简单的 shell 脚本来自动化最常见的应用。你将了解到,你可以依赖外部监控服务(例如 uptimerobot.com
)来提供简单的可用性监控,并且可以使用内建的 Docker 日志功能来深入了解你应用的运行情况。
一旦你部署了应用程序,最好能提高与之相关的自动化水平,特别是在如何构建和部署应用程序方面。在下一章中,我们将看到如何使用 Jenkins —— 一个常见的持续集成系统 —— 来自动化部署和测试。
进一步阅读
-
Docker 食谱(https://www.packtpub.com/free-ebooks/virtualization-and-cloud/docker-cookbook-second-edition/9781788626866)虚拟化与云端/Docker 食谱第二版
-
在生产环境中使用 Compose: https://docs.docker.com/compose/production/
-
开源监控工具: https://geekflare.com/best-open-source-monitoring-software/
-
免费的监控工具: https://www.dnsstuff.com/free-network-monitoring-software
-
docker-compose
是否适用于生产环境?vsupalov.com/docker-compose-production/
-
Docker 提示 2:
COPY
和ADD
在 Dockerfile 中的区别:nickjanetakis.com/blog/docker-tip-2-the-difference-between-copy-and-add-in-a-dockerile
如果您在单台主机上运行真实的生产应用并使用 docker-compose,您应当强烈考虑使用 SSL 来保护您的网站。您可以使用 Let's Encrypt 和一组 Docker sidecar 容器来实现这一点:
-
如何使用 Let's Encrypt、NGINX 和 Docker 为您的网站启用 SSL:
github.com/nginx-proxy/docker-letsencrypt-nginxproxy-companion
-
使用
docker-compose.yml
配置 Let's Encrypt 与 NGINX 和 Docker:github.com/nginx-proxy/docker-letsencryptnginx-proxy-companion/blob/master/docs/Docker-Compose.md
第七章:使用 Jenkins 进行持续部署
为了在生产中可靠地使用 Docker 容器,你需要一个持续构建、测试和部署软件的过程。一个构建非常小型应用的团队,可能会满足于手动运行测试和部署脚本。然而,纪律往往会被打破,导致团队成员之间的协作混乱。这通常会导致构建失败,并且在生产部署前后没有进行测试。结果往往是停机和不满的客户。为了确保我们能够可靠地构建、测试和部署软件,我们可以使用持续集成软件。这类软件能够以一种有纪律且可追溯的方式可靠地构建、测试和部署修订。运行良好的现代项目甚至可以使用这种软件实现持续部署,让即使是最小的变更也能快速推送到测试或生产环境中。
在本章中,我们将展示如何配置 Jenkins,这是一款最受欢迎的持续集成软件系统,以促进部署到前一章展示的最小环境。我们将使用 Jenkins 来管理生产安装和新的暂存环境安装,以测试变更在达到生产环境前的效果。
到本章结束时,你将知道在什么情况下部署 Jenkins 来实现 CI 和 CD 与 Docker 结合是一个好主意。你将学习如何设置一个基本的Jenkinsfile
,帮助 Jenkins 使用 docker-compose
命令更新应用程序。你将发现如何设置 Jenkins 的参数化构建,允许改变和审计配置参数。你将通过增加一个隔离的暂存环境,扩展简单的生产环境设置,使开发人员能够更有信心地进行变更。最后,你将明白何时这种解决方案已达到极限,何时需要采用更复杂的工具。
本章我们将覆盖以下主要内容:
-
使用 Jenkins 促进持续部署
-
Jenkinsfile 和主机连接
-
通过 Jenkins 推动配置变更
-
通过多个分支部署到多个环境
-
通过 Jenkins 扩展部署的复杂性和限制
技术要求
要完成本章的练习,你需要在本地工作站上安装 Git 和 Docker,并且需要根据前一章的描述已经设置好生产环境应用程序。为了完成关于部署到多个环境的练习,你还需要一个主机来运行测试环境,测试环境的规格应与生产主机相似。
您还需要一个 Jenkins 服务器。本章将介绍一些选项来简化 Jenkins 服务器的设置和维护,如果您尚未拥有可用的服务器。如果您的公司已经运行 Jenkins 服务器,您可以使用它—请向系统管理员请求权限。此服务器需要能够通过 SSH 访问您的生产服务器。
您需要能够在您控制的区域内创建 DNS 记录,用于暂存服务器和 Jenkins 服务器。您可以使用与上一章相同的 DNS 区域。
本章的 GitHub 存储库是 github.com/Packt-Publishing/Docker-for-Developers
—请查看其中的 chapter7
文件夹。
查看以下视频以查看代码的实际操作:
示例应用程序 – ShipIt Clicker v3
本章的 ShipIt Clicker 版本与上一章非常相似。我们将使用它来测试通过 Jenkins 到生产环境和暂存环境的部署。
使用 Jenkins 进行持续部署
过去 20 年来,持续集成服务器的世界发展了很长一段路。其中最流行的系统之一是 Jenkins(参见 jenkins.io/
)—因为它是免费的、灵活的,并且提供了大量的集成和插件。背后的公司 CloudBees(www.cloudbees.com
)也通过付费版本提供商业支持。您的公司可能已经在运行 Jenkins,如果是这样,您可能不需要做太多设置来构建和运行您的项目。
我们将使用 Jenkins 2.x 流水线项目类型,在 GitHub 中提交 Jenkinsfile
到源代码控制,并控制 Jenkins 用于构建和部署项目的步骤。
避免这些陷阱
在设置 Jenkins 之前,我们应该确保避免初次设置时人们常遇到的一些常见陷阱。
避免在 Docker 上运行 Jenkins
虽然可以使用 Docker 运行 Jenkins 服务器,但这样做会引入一些最好避免的复杂性,尤其是当您首次尝试运行持续集成服务器时。您可能需要使用名为Docker-in-Docker(dind)的功能或者一个定制的 Jenkins Docker 安装,该安装必须以非常特定的方式从主机映射端口和文件。如果不搞对,可能会遇到问题,例如不能构建 Docker 容器,因为您不能双重挂载联合文件系统。
将 Jenkins 本身作为 Docker 容器运行并解决其中的怪异问题可能需要大量的时间和精力,超出了本书能提供的建议范围。
避免在生产服务器上运行 Jenkins
在前一章节中,我们在云中设置了一个生产服务器来托管应用程序。你可能会被诱使让已经运行的服务器再做双重任务,既运行生产服务器,又运行 Jenkins CI 服务器。这虽然经济,但存在风险,因为生产配置或 Jenkins 服务器中的任何问题都可能同时导致生产服务中断并让 CI 服务器下线。这还会使网络和 Web 托管虚拟主机的配置更加复杂——没有更复杂的编排系统,容易导致这些不同的服务发生冲突。
运行可靠系统的一部分是确保进程和系统之间有足够的隔离,尤其是对于具有不同目的的系统,因此避免将 Jenkins 和生产服务器混用;应该在与生产服务器分开的系统上运行 Jenkins。
避免在本地工作站上运行 Jenkins
你可能也会被诱使直接在本地工作站上安装 Jenkins 进行测试。然而,你会发现这种做法有几个主要的缺点:
-
你的工作站可能没有稳定的 IP 地址,因此需要动态 DNS 解决方案,可能还需要打洞防火墙并设置 NAT 端口重定向。
-
你需要不断地在系统上运行 Jenkins,以便随着提交的推送,它能够处理并构建软件的更改。
-
Jenkins 可能会非常占用资源,无法与完整的开发环境一起运行——而且它可能会显著减慢你的工作站速度。
如果我们不应该将 Jenkins 作为 Docker 容器运行,也不应该在本地工作站上运行它,那么我们应该在哪里运行 Jenkins?让我们来探索一下这些选项。
使用现有的 Jenkins 服务器
如果你有访问正在运行最新版本 Jenkins 2.x 系列的 Jenkins 服务器的权限,就不需要从零开始设置 Jenkins。Jenkins 的新版本对 Docker 提供了极好的支持,前提是运行 Jenkins 构建的主机上已经运行了 Docker。
你需要确保以下 Jenkins 插件已经安装:
-
SSH 凭证
-
Pipeline
-
GitHub
-
GitHub 组织
理想情况下,Jenkins 服务器应该已经安装了 GitHub 组织插件,并且已配置为能够自动管理 GitHub 的 webhooks。如果是这种情况,你可以分叉示例仓库或将其克隆并推送到你的 GitHub 组织中作为新仓库,从那里开始部署。
你需要在 Jenkins 服务器上拥有足够的权限来创建凭证,这些凭证将用于存储构建和部署软件所需的密钥。
设置新的 Jenkins 服务器
简化您需要维护的技术栈的一个方便方法是使用与生产主机相同的基础操作系统和 Docker 设置。这里的说明和脚本是针对 CentOS 7 安装定制的,但您可以遵循相同的基本步骤来安装和维护其他操作系统版本,只需对安装操作系统包的特定命令进行一些修改,例如使用 apt-get
代替 yum
来安装操作系统包。
首先按照上一章中的步骤安装 Docker 和 docker-compose
。完成后,使用 docker run --rm hello-world
命令测试 Docker 是否正常工作,然后安装 Jenkins。如果您使用的是 CentOS 7,可以使用以下脚本 https://github.com/PacktPublishing/Docker-for-Developers/blob/master/chapter7/bin/provision-jenkins.sh 同时安装 Docker 和 Jenkins(将 centos@jenkins.example.com
替换为您新 Jenkins 服务器的用户名和 IP 地址或主机名):
$ ssh centos@jenkins.example.com < bin/provision-jenkins.sh
$ ssh centos@jenkins.example.com
如果您使用的是其他操作系统,请查阅 Jenkins 官方文档获取安装说明:wiki.jenkins.io/display/JENKINS/Installing+Jenkins
为了配置 CentOS 7 允许网络流量到达 Jenkins,您可能需要配置主机防火墙以允许入站流量。
此外,建议让 Jenkins 监听标准端口,如 80
或 443
端口。这可以通过多种方式实现,包括让 Web 服务器充当 Jenkins 的代理,或使用负载均衡器终止 SSL。在 CentOS 7 上允许网络流量到达 Jenkins 的快捷方式如下(如果您使用了 provision-docker.sh
脚本来配置 Jenkins,则此步骤已完成):
$ sudo firewall-cmd --zone=public --permanent --add-masquerade
$ sudo firewall-cmd --permanent --add-service=http
$ sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
$ sudo firewall-cmd --permanent --direct \
--add-rule ipv4 nat OUTPUT 0 \
-p tcp -o lo --dport 80 -j REDIRECT --to-ports 8080
$ sudo firewall-cmd --reload
firewall-cmd
调用将允许您通过端口 80
访问 Jenkins,而不是指定端口 8080
。
安装 Jenkins 后,您必须从其日志中获取密码以连接到服务器:
$ sudo grep -A 3 password /var/log/jenkins/jenkins.log
请记下此命令输出中的密码。如果此方法未立即生效,请等待几分钟再试,因为 Jenkins 可能仍在启动中。
然后,打开 Web 浏览器并输入带有适当端口的 IP 地址,取决于您是否已重定向连接,端口可以是 8080
或 80
。例如,输入 192.2.0.10:8080
访问该站点。
您应该看到一个显示 解锁 Jenkins 的屏幕:
图 7.1 – 解锁 Jenkins
使用 /var/log/jenkins/jenkins.log
文件中的管理员密码首次登录。
下一屏幕会提示您安装插件。请安装推荐的插件:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_07_002.jpg)
图 7.2 – 自定义 Jenkins
如果您的系统内存少于 4 GB,您需要使用交换文件。运行 free
命令查看服务器是否有可用的交换内存。如果没有,请执行以下命令创建一个 1 GB 的交换文件并激活它:
$ free
total used free shared buff/cache available
Mem: 1882296 89008 1533220 8676 260068 1612156
Swap: 2097148 0 2097148
$ sudo dd if=/dev/zero of=/swap bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 2.94343 s, 365 MB/s
[vagrant@localhost ~]$ sudo chmod 0600 /swap
[vagrant@localhost ~]$ sudo mkswap /swap
Setting up swapspace version 1, size = 1048572 KiB
no label, UUID=2bd70cac-3730-45bb-8b77-982425fb7af5
[vagrant@localhost ~]$ echo /swap swap swap defaults 0 0 | sudo tee -a /etc/fstab
/swap swap swap defaults 0 0
[vagrant@localhost ~]$ sudo mount -a
[vagrant@localhost ~]$ free
total used free shared buff/cache available
Mem: 1882296 83120 481244 8668 1317932 1604256
Swap: 2097148 0 2097148
您应该能在 free
命令的输出中看到系统有非零的交换内存。
Jenkins 安全性与 HTTPS
对于生产环境,您应配置 Jenkins 通过 SSL 终端负载均衡器或已配置 SSL 证书的 Web 服务器后端运行,并监听 HTTPS。有关如何使用 HTTPS 安全化 Jenkins,请参考 Jenkins 文档或互联网上的许多教程。您还应该考虑限制可以直接访问 Jenkins 服务器的 IP 地址范围,因为这些服务器是恶意攻击者的常见目标。有关更多关于如何确保 Jenkins 安全的信息,请参阅本章末尾的进一步阅读部分。
要将 Jenkins 与 Docker 一起使用,您需要安装 Docker Pipeline 插件。从 Jenkins 主屏幕,进入 管理 Jenkins | 管理插件 菜单,点击 可用 标签,选择 Docker Pipeline 插件,然后按 立即下载并在重启后安装 按钮。Jenkins 重启后,请重新登录。
现在您已经可以使用 Jenkins 服务器了,可以开始配置它与生产服务器进行通信。
Jenkins 如何支持持续部署
Jenkins 可以从版本控制中检出项目源代码、构建软件、运行测试并执行部署脚本。由于它支持 Docker,它可以构建 Docker 容器,将容器推送到 Docker Hub 或其他容器仓库,然后运行连接到服务器的部署脚本,指示其更新运行中的 Docker 容器。为了支持这些目标,我们必须配置 Jenkins 与生产服务器、版本控制仓库以及 Docker Hub 集成。首先,我们将确保可以使用 Jenkins 连接到生产服务器。
Jenkinsfile 和主机连接
为了确保可重复的构建,我们将使用 Jenkins 脚本来运行构建和部署自动化。Jenkins 支持一种名为 Jenkinsfile
的脚本类型。由于这些脚本使用 Groovy 语言编写(请参见 https://groovy-lang.org/),您可以声明变量、编写函数,并利用该强大语言的许多功能来帮助构建和部署软件。Jenkins 支持自由形式的脚本风格以及使用 Groovy 特殊领域特定语言(DSL)提供更多框架、简洁脚本的结构化声明式脚本风格。
有关如何编写 Jenkinsfile
的更多信息,请参见:www.jenkins.io/doc/book/pipeline/jenkinsfile/
您可以直接将这些脚本输入到 Jenkins 作业定义中,或者将它们存储在版本控制中。如果您将名为Jenkinsfile
的文件放在版本控制仓库的根目录中,Jenkins 可以在配置与版本控制系统(如 GitHub)连接后自动发现这些文件。
使用管道脚本测试 Jenkins 和 Docker
为了测试 Jenkins 和 Docker 是否正常协同工作,我们将首先通过控制台输入脚本。在 Jenkins 的顶层屏幕上,点击Hello Docker
:
图 7.3 – 新建项 – Hello Docker 管道
然后,在chapter7/
的Jenkinsfile-hello-world
文件中(在配套的 GitHub 项目中):
pipeline {
agent { docker { image 'alpine:20191114' } }
stages {
stage('build') {
steps {
sh 'echo "Hello, World (Docker for Developers Chapter 7)"'
}
}
}
}
保存作业并点击立即构建链接,Jenkins 将创建构建#1。点击左侧出现的#1链接,然后点击控制台输出按钮。您应该会看到类似以下内容:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_07_004.jpg)
图 7.4 – 新建项 – Hello Docker 控制台输出
您应该会在 Jenkins 网页的控制台输出中看到Hello, World(Docker for Developers 第七章)
。如果这里出现内存不足错误,请确保您的 Jenkins 服务器上有交换文件。如果看到关于 Docker 不是已知代理类型的错误,请转到管理 Jenkins | 管理插件菜单,安装Docker Pipeline插件。
通过 SSH 连接到生产服务器
接下来,我们将配置 Jenkins 通过 SSH 连接到生产服务器。我们需要这样做,以便控制远程服务器上的 Docker 子系统。我们将为 Jenkins 生成一个 SSH 密钥,并将其添加到生产服务器的授权密钥列表中。
生成 SSH 密钥并将其添加到 Jenkins 凭证
在本地工作站上,执行以下命令以生成一个 2,048 位的 RSA SSH 密钥对并查看它:
ssh-keygen -t rsa -b 2048 -f jenkins.shipit
cat jenkins.shipit
将jenkins.shipit
文件的内容复制到剪贴板,然后转到 Jenkins 主页,在左侧菜单中找到jenkins.shipit
,并输入生产服务器的非 root 用户的用户名(对于 CentOS 7 云服务器,通常为centos
)。点击直接输入,添加密钥并点击确定按钮以保存凭证:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_07_005.jpg)
图 7.5 – 添加凭证 – SSH 密钥
将 SSH 公钥jenkins.shipit.pub
从本地系统复制到生产服务器,并将其追加到~/.ssh/authorized_keys
文件中。通过在本地工作站上输入以下命令,将centos@192.2.0.10
替换为您的生产服务器的用户名和 IP 地址:
prod=centos@192.2.0.10
ssh $prod mkdir -p .ssh
ssh $prod tee -a .ssh/authorized_keys < jenkins.shipit.pub
ssh $prod chmod 700 .ssh ssh $prod chmod 600 .ssh/authorized_keys
通过使用密钥从本地工作站登录,测试 SSH 密钥认证是否正常工作:
$ ssh -i jenkins.shipit $prod
Last login: Mon Mar 2 04:57:35 2020 from gateway.example.net
[centos@ip-172-26-13-202 ~]$
完成此操作后,您可以创建一个测试作业,使用这些凭证通过 SSH 连接到服务器。
使用 Jenkins 管道作业通过 SSH 连接到生产服务器
在 Jenkins Web 控制台中,创建一个新的 Jenkins 任务,选择 SSH to Production
,并选择 Pipeline 任务类型:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_07_006.jpg)
图 7.6 – 创建项目 – SSH 到生产环境
在任务定义表单中,在 centos@192.2.0.10
中填写生产服务器的用户和主机,并保存任务脚本(请参见 chapter7/Jenkinsfile-ssh-proof-of-concept
)并将其存储在配套的 GitHub 项目中:
pipeline {
agent any
stages {
stage('SSH') {
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'jenkins.shipit',
keyFileVariable: 'keyfile')]) {
sh '''
prod=centos@192.2.0.10
cmd="docker ps"
ssh -i "$keyfile" -o StrictHostKeyChecking=no $prod $cmd
'''
}
}
}
}
}
当你点击 Build Now 链接并查看控制台输出时,你应该会看到类似以下的输出:
…
+ ssh -i **** -o StrictHostKeyChecking=no centos@34.238.248.192 docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6c9ef1ca65f6 chapter6_shipit-clicker-web-v2 "npm start" 6 weeks ago Up 6 weeks 0.0.0.0:80->3000/tcp chapter6_shipit-clicker-web-v2_1
…
3f91820e097b redis:5-alpine3.10 "docker-entrypoint.s…" 7 weeks ago Up 7 weeks 6379/tcp chapter6_redis_1
如果你没有看到 docker ps
的输出,请仔细检查用户名、IP 地址和 SSH 密钥。检查 Jenkins 关于 Jenkinsfile
或与 ssh
shell 命令相关的错误信息以进行故障排除。你需要让这个部分正常工作,才能确保下一阶段的顺利进行。
您可以使用 Jenkins 连接到其他主机来运行使用 docker
和 docker-compose
的脚本。但如果需要,您也可以直接在 Jenkins 服务器上运行 docker
和 docker-compose
。我们将在本章后面探讨这一点。
现在我们可以通过 SSH 使用 Jenkins 连接到生产服务器,并通过管道脚本使用该连接对生产服务器进行更改,包括将新的更改部署到服务器上。
通过 Jenkins 驱动配置更改
接下来,我们将学习如何通过运行托管在 Jenkins 中的 Git 仓库中的脚本来对生产系统进行更改。我们可以使用 Jenkins 来构建应用程序的 Docker 容器,并将这些容器部署到生产服务器上。这样,程序、Dockerfile
或 docker-compose.yml
文件的任何更改都可以通过自动化传播到生产系统。
这里有一些关于如何将 Jenkins 与其他系统(包括 GitHub)集成的提示,它们可以让你的工作更加轻松。第一个提示是如何通过将 Jenkinsfile
存储在版本控制系统中,来配置 Jenkins。
使用 Git 和 GitHub 存储你的 Jenkinsfile
在上一节中,我们直接在 Jenkins 任务中使用 Jenkinsfile
进行了一些快速测试。这对于进行探索性工作非常有效,但为了构建和管理更复杂的脚本集,您应该使用 Git 版本控制来存储 Jenkinsfile
,并使用 GitHub 来存储和共享 Git 仓库,因为 GitHub 与 Jenkins 的集成非常顺畅。这将使您能够以受控的方式对程序和部署脚本进行更改。
要了解更多关于为什么你应该将 Git 版本控制系统与 GitHub 一起使用的信息,请参阅这个入门指南:guides.github.com/introduction/git-handbook/
。
我们可以将存储在 GitHub 中的脚本与 Jenkins 中的 Jenkinsfile
(例如本书仓库中的那个)结合,部署示范项目。通过这种对环境变量替换的支持,即使你的生产服务器可能配置了不同的用户和主机,也能使用未改变的 Jenkinsfile
,同时使用你独特的 SSH、Docker Hub 和 GitHub 凭证。
为了进一步推进,你必须确保 Jenkins 拥有 GitHub 用户名和安全令牌凭证,以便你可以使用 Jenkins 检出 GitHub 仓库。
确保 Jenkins 拥有 GitHub 用户名和安全令牌凭证
为了将 Jenkins 与 GitHub 一起使用,你需要保存一个包含 GitHub 个人访问令牌的 Jenkins 凭证。在浏览器中登录 GitHub,访问 github.com/settings/tokens
,并生成一个同时具备 repo
和 admin:repo_hook
权限的令牌。将生成的令牌复制到剪贴板。然后,在另一个浏览器窗口中,访问你的 Jenkins 服务器,导航到凭证部分,进入 Jenkins 全局凭证,并创建一个 github.repo.username
和描述为 username
的凭证,凭证内容为你的实际 GitHub 用户名。点击 OK 按钮保存凭证。
选项 1 – 使用 GitHub 组织项配置 Jenkins
Jenkins 支持定义 项,这些项可能是单独的 Jenkins 作业或相关作业的集合。几种项类型允许你将版本控制系统与 Jenkins 连接,从而自动定义多个 Jenkins 作业。最强大的其中之一就是 GitHub Organization
项目。使用 GitHub Organization
项目,Jenkins 将扫描 GitHub 上每个拥有 Jenkinsfile
的项目,并为在 GitHub 组织中找到 Jenkinsfile
的所有仓库自动设置子项集合。
这是让 Jenkins 管理一组相关项目的最简单方法。如果你正在使用新的 Jenkins 服务器来探索 Docker 开发,并且控制着一个 GitHub 组织,尝试进行这个设置。如果你使用的是公司 Jenkins 服务器,这个设置可能已经完成。
从你的 Jenkins 安装主页,点击 GitHub Organization
。使用标记为 GitHub 仓库凭证(用户名) 的凭证,并确保组织字段中的名称与您的 GitHub 组织名称匹配。
你可以设置一个过滤器,这样它只扫描你想要的包含 Jenkinsfile
的项目。如果你在组织中有大量的仓库和分支,或者你只希望 Jenkins 构建特定的仓库(这些仓库可能与 Jenkins 配合使用),或者如果还有其他 Jenkins 服务器也构建 GitHub 组织中的一部分项目,这可能是个不错的主意。如果你想这么做,可以添加一个类型为 Filter by name (with regular expression)
的 Behavior
,并构建一个正则表达式来匹配你想要包含的仓库名称。
使用 GitHub,将 Docker-for-Developers 仓库(github.com/PacktPublishing/Docker-for-Developers/
)分叉到你的组织中。或者,如果你不想分叉仓库,可以在你的组织中创建一个空的仓库。然后,将你本地的仓库副本推送到新创建的仓库中,进入你创建的 GitHub 组织项,你应该能看到 Docker-for-Developers 项目。
然而,如果你使用的是个人 GitHub 账户,且无法访问 GitHub 组织,这可能不是一个好的选择。你可以改为使用多分支流水线项配置 Jenkins,从一个单一的 GitHub 仓库中提取 Jenkinsfile
。
选项 2 – 使用多分支流水线项配置 Jenkins
使用多分支流水线项将允许 Jenkins 扫描 GitHub 中每个包含 Jenkinsfile
的项目的单一仓库,Jenkins 会自动为每个 Git 分支创建子项,如果该分支中存在 Jenkinsfile
,并且为合并请求设置 Jenkins 作业。
将 Docker-for-Developers 仓库分叉到你的组织中,或在你的账户中创建一个空的仓库,并将你本地的仓库副本推送到 GitHub。在配置多分支流水线之前,你需要完成这一步。
从你的 Jenkins 安装主页,点击 Multibranch Pipeline
。在 Branch Sources 中,选择 GitHub,然后填写 GitHub 表单中的凭据,凭据标签为 GitHub repo credentials (username),并在 Repository HTTPS URL 字段中填写你的 GitHub 仓库 URL。然后,保存该项。Jenkins 会扫描该仓库,并为每个 Git 分支设置独立的 Jenkins 作业。
到此为止,无论你是使用多分支流水线还是 GitHub 组织项类型,你的 Jenkins 中应该已经有了一组分支。
更改所有已检出仓库的来源
此时,你还应该将你的 Git 仓库的 URL 更改为新的仓库 URL,无论是在本地工作站,还是在前一章节中设置的生产服务器上。将 example
替换为你分叉仓库时使用的 GitHub 组织或用户的名称:
git remote set-url origin https://github.com/example/Docker-for-Developers.git
检查你的 GitHub 仓库是否通过 webhook 与 Jenkins 进行通信
GitHub 可以通过 Webhooks 与其他系统通信,这些 Webhooks 是当人们执行某些操作时系统触发的 HTTP 请求,针对另一个系统。
更多关于 GitHub 对 Webhooks 和系统集成的支持信息,请参见:developer.github.com/webhooks/
当我们设置GitHub 组织
项目或多分支流水线
项目时,Jenkins 应该在 GitHub 中设置其中一个 Webhook,以便它可以与 Jenkins 通信。如果没有设置,您可以访问https://jenkins.example.com/github-webhook/
(将jenkins.example.com
替换为您的 Jenkins 服务器)。
现在我们已经配置了 Jenkins 可以与 GitHub 通信,我们希望确保将分支推送到 GitHub 会触发 Jenkins 中的构建。根据您的帐户的 GitHub 权限和 Jenkins 配置,它可能没有自动创建 Webhook。
在 Web 浏览器中,导航至您的 GitHub 存储库,进入设置,然后进入Webhooks,验证是否存在与您的 Jenkins 服务器 URL 相关的 Webhook。
现在 Jenkins 已连接到 GitHub,可以预期发生什么
现在我们已经配置了 Jenkins 以能够检查存储库中是否存在Jenkinsfile
,我们可以继续。Jenkins 将尝试构建您刚刚定义的项目。但是,除非您向 Jenkins 提供额外的变量和凭据,否则构建将失败。
为了将构建的特定配置与您的环境绑定,我们需要使用 Jenkins 设置一些环境变量来存储不太敏感的项,除了使用凭证功能存储加密密钥和密码。
为生产支持创建 Jenkins 环境变量
Jenkins 支持设置环境变量,可以供项目(例如构建和部署作业)引用。对于诸如 SSH 私钥或 Docker Hub API 凭证之类的机密变量,您可以使用我们在上一节中使用的凭证系统来安全存储这些变量。对于不太敏感的值,我们可以使用 Jenkins配置屏幕上可用的环境变量设置:
图 7.7 – Jenkins 配置 – 生产主机的环境变量
为了继续,请与您使用的 DNS 提供商确认,您的生产主机是否具有与其 IP 地址关联的 DNS 名称。在第六章,使用 Docker Compose 部署应用程序中,我们为生产服务器设置了 DNS 名称。拥有 DNS 名称将使配置更易读,并使人们在 Web 浏览器中更容易访问服务器。设置这些密钥和值的变量:
-
shipit_prod_host
:生产服务器 DNS 域名,例如 shipitclicker.example.com -
shipit_prod_user
:生产服务器用户名,例如centos
设置完这些变量后,点击 保存 按钮。当我们运行 Jenkins 作业以更新正在运行的容器时,将使用这些变量。不过,在此之前,我们需要一个容器存放的地方。在之前的章节中,你已经学会了如何将容器镜像推送到 Docker Hub。接下来,我们将自动化这个过程。
构建 Docker 容器并将其推送到 Docker Hub
为了避免在生产服务器上构建容器,我们需要在 Jenkins 上构建容器,然后将它们推送到 Docker 容器注册中心,如 Docker Hub。这可以将构建 Docker 容器与部署分开。如果你尝试在单个小型服务器上同时进行构建和部署,很可能会遇到内存不足或其他系统稳定性问题。而在生产服务器上,你希望最大限度地提高环境的稳定性。
尽管你可以从本地工作站将容器推送到 Docker Hub,使用 Jenkins 的好处之一是可以通过 Jenkins 自动构建并将容器推送到中央仓库。为此,你需要为 Jenkins 提供 Docker Hub 的凭证。
将 Docker Hub 凭证添加到 Jenkins的凭证管理器中
使用你的 Docker 账户登录到 hub.docker.com/
,并为 Jenkins 创建一个 API token,供其使用。API token 可以在 hub.docker.com/settings/security
的安全设置页面找到。复制该 API token 到剪贴板,然后在另一个浏览器标签页中访问 Jenkins 凭证管理器,创建一个新的全局无限制凭证,类型为 用户名和密码
。将其 ID 命名为 shipit.dockerhub.id,并在 用户名
字段中输入你的 Docker 账户用户名,在 密码
字段中输入访问 token,然后保存。
这将允许你使用 Docker Hub 凭证将构建推送到 Docker Hub,并且由于我们已经在 Jenkins 中设置了 SSH 凭证,我们可以使用这些凭证在构建完 Docker 镜像后将其推送到 Docker Hub,然后连接到生产服务器以部署新软件。
确保停止先前的生产环境
如果上一章节中的生产环境正在运行,你需要停止它才能部署新环境。这将确保新生产环境能够绑定到正确的 TCP 端口。
注意
在你拥有真实的生产应用程序并且有宝贵的客户数据的情况下,你会希望将任何数据库和其他持久化存储备份并恢复到新环境中。ShipIt Clicker 应用程序仅使用 Redis 来保存有关生产环境的详细信息。对于 Redis,可以通过 CLI 使用SAVE
命令来完成。然后,你可以将生成的dump.rdb
文件复制到本章 Redis 容器使用的 Docker 卷中。
从你的本地工作站,SSH 到服务器并停止它(将192.0.2.10
替换为你服务器的 IP 地址):
cmd='cd Docker-for-Developers/chapter6; docker-compose stop'
ssh centos@192.2.0.10 "$cmd"
现在,之前的 Docker 容器已经停止,你可以继续使用 Jenkins 构建软件、推送到 Docker Hub 并在生产服务器上部署容器。你只需在从上一章的设置转到本章 Jenkins 管理的环境时执行一次此操作。
接下来,让我们通过 Jenkins 触发生产环境的部署。
推送到 Docker Hub 并触发生产部署
现在我们已经准备好了所有的环境变量和凭证,可以触发 Jenkins 构建了。Jenkins 通常在检测到提交时触发构建,但我们也可以强制 Jenkins 启动构建。进入与存放应用代码的 GitHub 仓库挂钩的 Jenkins 作业页面,选择master
分支,然后点击立即构建。Jenkins 将开始构建作业,并在用户界面中显示构建编号:
图 7.8 – GitHub 组织中的 Jenkins 作业 – 主分支
在检查作业进度之前,让我们先了解它是如何工作的。
Jenkins 运行Jenkinsfile
和脚本chapter7/bin/
dep-ssh.sh的组合,以构建和部署软件。Jenkinsfile
检查仓库、构建 Docker 容器并将其推送到 Docker Hub。以下是Jenkinsfile
的一个片段,展示了管理检出、构建和推送过程的代码:
pipeline {
agent any
stages {
stage('build') {
steps {
checkout scm
script {
docker.withRegistry(registry, 'shipit.dockerhub.id') {
def image = docker.build(
getImageName(appName),
"-f ${dockerfile} --network host ./chapter7"
)
image.push()
}
}
}
}
下一阶段,deploy
阶段,当分支为master
或 staging 时运行,并调用 shell 脚本chapter7/bin/dep-ssh.sh
,通过 SSH 连接到服务器,更新仓库副本,拉取构建的 Docker 容器并重启容器。请参见以下dep-ssh.sh
的片段,了解最重要的部分:
ssh -i "$keyfile" -o StrictHostKeyChecking=no "$targetEnv" <<EOF
set -euo pipefail
cd Docker-for-Developers/chapter7
git fetch
git reset --hard HEAD
git checkout -f origin/"$GIT_BRANCH"
docker pull "$image"
set -a
DOCKER_IMAGE="$image"
PORT="$port"
bin/restart.sh
EOF
现在你已经了解了构建和部署是如何串联在一起的,接下来你应该查看部署到生产环境是否成功。
验证部署是否成功
点击最新的构建,然后点击控制台输出末尾的Finished: Success
。
控制台输出将显示这些基本步骤:
-
正在从 GitHub 克隆 Git 仓库。
-
正在构建 Docker 容器。
-
正在将 Docker 容器推送到 Docker Hub。
-
Jenkins 通过 SSH 连接到生产服务器。
-
脚本
chapter7/bin/ssh-dep.sh
在生产服务器上运行,它从 Docker Hub 拉取镜像并重新启动 Docker 服务。
如果前面的任何步骤失败,Jenkins 任务将会失败。如果发生这种情况,请仔细检查凭据和环境变量是否正确。你可以比较你的测试运行输出与样本输出 chapter7/consoleOutput.txt
,这个样本输出在伴随的 GitHub 仓库中,看看你的 Jenkins 运行是否按预期工作。
如果这已经成功构建,你应该能够访问上一章中使用的相同 URL(例如,shipitclicker.example.com/
或 192.2.0.10/
)来查看应用程序。恭喜你!现在每次推送到主分支,包括合并拉取请求到主分支时,都会部署到生产环境。这是实现持续部署的最简单方法之一。
你可能希望能够在一个独立且稳定的环境中查看你的更改,并且该环境始终可用。这样,如果你做出的更改可能会破坏生产环境,你就可以在隔离的环境中进行测试。在下一节中,我们将学习如何设置一个与生产环境类似的预发布环境,并使用 Jenkins 协调部署到该环境。
通过多个分支部署到多个环境
能够部署到单一的生产环境非常有价值,但为了支持开发和测试,最好拥有至少一个除了生产环境外的其他环境来进行测试。这样,测试软件的人如果没有开发环境,也可以看到你做出的更改的效果,而你不必将这些更改部署到生产环境。
在本章的下一部分,我们将创建第二个环境,一个预发布环境,以便在更改进入生产环境之前进行测试。
创建预发布环境
你将需要另一台主机,规格与运行生产环境的主机类似,用作预发布环境。一旦你能够通过 SSH 连接到该主机,你可以按照上一章关于安装 Docker 和 Git 的说明进行操作。如果你使用的是 CentOS 7,你可以使用以下脚本片段在该系统上快速配置 Docker,并测试其是否正常工作(将 centos@192.2.0.11
替换为你在预发布环境中使用的用户和主机,将 GitHub URL 替换为你组织的项目仓库的 URL):
$ staging=centos@192.2.0.11
$ ssh $staging < bin/provision-docker.sh
$ ssh $staging git clone https://github.com/PacktPublishing/Docker-for-Developers.git
$ ssh $staging docker run --rm hello-world
一旦 Docker 在预发布系统上运行正常,您可以输入 exit
命令回到本地工作站。然后,确保预发布系统具有与生产系统相同的 SSH 公钥。从包含 jenkins.shipit.pub
密钥文件的目录中执行此操作:
$ ssh $staging mkdir -p .ssh
$ ssh $staging tee -a .ssh/authorized_keys < jenkins.shipit.pub
$ ssh $staging chmod 700 .ssh $ ssh $staging chmod 600 .ssh/authorized_keys
现在,预发布服务器已经准备好,具备了正确的 SSH 凭证和运行 Docker 应用所需的基础软件,我们将配置 Jenkins 以支持这个预发布环境。
为支持预发布环境创建 Jenkins 环境变量
为了准备 Jenkins 部署到预发布服务器,我们将返回到 Jenkins 环境变量设置,这些设置可以在 Jenkins 配置页面中找到。为了继续,请确保您的预发布主机已与 DNS 名称关联。为以下键值设置环境变量:
-
shipit_staging_host
:预发布服务器的 DNS 域名,例如shipit-staging.example.com
-
shipit_staging_user
:预发布服务器用户名,例如centos
通过强制推送到预发布分支进行部署
部署脚本会检测正在处理的分支,并将代码部署到正确的环境。这是通过 Jenkinsfile
中的指令和部署脚本使用通过 Jenkinsfile
和 Jenkins 全局配置设置的环境变量的结合实现的。在我们展示如何使用 Git 强制推送之前,我们需要检查 Jenkinsfile
和支持脚本,看看它们如何处理分支名称。
脚本如何知道使用哪个服务器?
Jenkinsfile
只有在分支名称为 master
或 staging
时才会运行部署阶段:
stage('deploy') {
when {
anyOf {
branch 'master'
branch 'staging'
}
}
接下来,我们将展示使用 Jenkinsfile
的一些优势,展示 Groovy 语言的一些特性,如变量插值和调用函数。接下来在 Jenkinsfile
中的步骤定义了环境变量,chapter7/bin/ssh-dep.sh
脚本使用这些变量来帮助选择正确的环境:
steps {
echo "BRANCH_NAME is ${env.BRANCH_NAME}"
echo "Deploying to ${getTarget()}"
withCredentials([sshUserPrivateKey(
credentialsId: 'jenkins.shipit',
keyFileVariable: 'keyfile')]) {
sh """
set -a
target=${getTarget()}
image=${getImageName(appName)}
keyfile=${keyfile}
./chapter7/bin/ssh-dep.sh
"""
这些使用 Jenkins 变量插值表达式来调用用 Groovy 编写的 Jenkins 函数(getTarget()
和 getImageName(appName)
),这些函数设置了 chapter7/bin/ssh-dep.sh
脚本使用的一些环境变量。
getTarget()
函数使用这个三元表达式来决定是选择 prod
环境还是 staging
环境:
def getTarget() {
env.BRANCH_NAME == 'staging' ? 'staging' : 'prod'
}
一旦控制流传递给 chapter7/bin/ssh-dep.sh
脚本,它会使用目标环境变量来选择要部署的环境,并设置变量,以便 SSH 命令能够选择正确的服务器:
port=${port:-80}
prod="${shipit_prod_user}@${shipit_prod_host}"
staging="${shipit_staging_user}@${shipit_staging_host}"
image=${image:-dockerfordevelopers/shipitclicker:latest}
if [[ "$target" = "staging" ]]; then
targetEnv="$staging"
targetHost="$shipit_staging_host"
else
targetEnv="$prod"
targetHost="$shipit_prod_host"
fi
通过这种方式,脚本设置了 targetEnv
,使得接下来的 SSH 命令可以访问正确的服务器:
ssh -i "$keyfile" -o StrictHostKeyChecking=no "$targetEnv" <<EOF
现在,您已经了解了 Jenkinsfile
和 chapter7/bin/ssh-dep.sh
中的变量如何交互,您可以使用 Git 来触发部署到预发布环境。
准备使用 Git 强制推送分支到预发布环境
尽管在 Git 中强制推送分支可能会带来问题,但在某些情况下这样做是合理的。如果你认为 staging
分支是一个特殊的分支,不是你通常会合并到主分支的内容,那么你可以反复将任何分支上的工作进度强制推送到该分支。
在本地工作站上,通过执行命令 git checkout -b experiment
在 Git 仓库中创建一个新的分支 experiment
。编辑 chapter7/src/public/index.html
文件,并将 <h1>
标签中的文本更改为 ShipIt Clicker Experiment
。保存文件并执行 git commit
命令。然后,强制将你分支的 HEAD
推送到 GitHub,命令如下:
$ git push origin HEAD:staging --force
这将把你刚刚提交的代码推送到 GitHub。然后,打开浏览器访问你的 Jenkins 服务器,查看你仓库的相关项。你应该很快看到 Jenkins 创建了一个 staging
分支任务,并将构建软件、推送到 Docker Hub 并部署到预发布环境。观察该 staging
分支任务的 Jenkins 控制台日志,确保它与你从 master
分支部署生产环境时的日志类似。
如果部署成功,使用浏览器检查预发布服务器上应用程序的标题是否为 ShipIt Clicker Experiment
——你更改的文本。
到目前为止,我们已经使用 Jenkins 将一个 Docker 应用部署到了生产和预发布服务器。你可能会想,添加第三个或第四个环境需要做什么,或者这种方法的缺点是什么。非常复杂的脚本和环境可能使得用 Jenkins 部署变得更加困难——让我们更详细地检查一下这个问题。
通过 Jenkins 扩展部署的复杂性和限制
由于 Jenkins 是一个通用的工具,用于构建和脚本化与软件开发相关的过程,它提供了巨大的灵活性,但也带来了复杂性。尽管它几乎可以执行任何与持续集成和部署相关的功能,但它可能需要比其他更为专门构建的系统(如 Spinnaker、CodeFresh 或 WeaveWorks)更多的脚本和配置。一些其他的持续集成和部署系统专门处理专注于 Docker 的工作流。
使用 Jenkins 管理到一两台主机的构建、测试和部署是相当可行的。但当你开始扩展时,可能会变得更加复杂,并且继续使用 Jenkins 来处理构建和部署可能变得更加困难。构建和部署脚本也可能变得过于复杂,难以管理,因为需要使用许多不同的编程语言和方法。让我们从管理多台主机的限制开始检查这些限制。
管理多台主机
本章中展示的脚本处理了两个环境的部署:生产环境和暂存环境。然而,如果我们想要增加四个类似的环境,比如开发、QA、演示和 Beta 环境,我们可能需要启动四个额外的主机,并相应地扩展我们的脚本。很快,这就会变成一个庞大且昂贵的混乱。还要考虑如果某个主机变得太小,无法运行生产站点会发生什么。你可能需要运行一组实例,并确保它们都使用相同的数据库。接着,你将面临如何在不产生停机的情况下更新和部署这组实例的问题。如果你采用蛮力脚本的方法,问题和挑战会变得更大。
如果你打算使用 Jenkins 管理多个主机的大规模部署,你会想要考虑将其与提供额外抽象的服务集成,以处理扩展和部署,例如 AWS EC2 自动扩展组和 AWS CodeDeploy。然而,这些服务都不专注于 Docker 特定的功能。如果你有组织上使用 Jenkins 作为持续集成环境的承诺,你还可以使用 Jenkins 来运行使用 Kubernetes 工具(如 kubectl
或 helm
)的脚本,以便将软件部署到 Kubernetes 集群。
构建脚本的复杂性
Jenkins 最好的地方之一是它允许使用 Groovy 领域特定语言来编写构建脚本;然而,这也可能是最糟糕的地方之一。Groovy 是一种强大且简洁的基于 Java 虚拟机的语言,但它远不如许多其他脚本语言(如 Python、Ruby 和 Bash)那么知名。此外,Jenkins 使用沙箱模型来限制允许使用的 Groovy 语句类型。
这通常意味着实现者必须将构建脚本分割为高层次的协调层,该层使用 Groovy 的 Jenkins pipeline DSL 方言编写,并与其他语言一起使用。这个项目通过结合使用 Groovy Jenkinsfile
和 Bash shell 脚本来实现这一点,这些脚本驱动 Docker 构建和部署。
如何判断何时达到了极限?
有多年经验使用 Jenkins 和手工编写脚本构建和部署软件的人,已经学会识别出一些迹象,表明使用 Jenkins 来完成你的目标已经达到了极限:
-
Jenkins 本身的安装变得脆弱,并且对于项目中新的人来说,快速学习变得太复杂。
-
由于插件的不兼容性,升级 Jenkins 变得困难。
-
构建脚本经常失败,人们忽视这些失败。
-
构建和部署软件的时间开始变得太长,无法满足业务需求。
-
如果你维护多个应用程序,用来构建和维护它们的脚本就变成了一个充满剪切粘贴的意大利面代码的迷宫。
如果你看到以下迹象,可能是时候考虑使用更为专业的工具,比如 Spinnaker、GitLab CI 或 CodeFresh,作为你的 CI 和容器流水线管理工具。
概述
在本章中,你已经学习了如何使用 Docker、Jenkins 和 GitHub 构建持续部署流水线。你学习了如何通过 SSH 建立 Jenkins 服务器与多个主机服务器之间的连接,并通过 Jenkinsfile
脚本化实现。你还学会了如何将这些技术结合起来,利用 Jenkins 推动配置更改和 Docker 部署到生产主机。你还学习了如何设置第二个 staging 环境,并使用 Jenkins 的环境变量和凭证支持,使一套脚本能够部署到多个环境。最后,你了解了使用 Jenkins 管理大规模部署的局限性,以及何时该使用其他工具来管理持续部署。
现在,你已经掌握了使用 Jenkins 在生产和 staging 环境中构建和部署软件的基础知识,你可以将这些应用到自己的项目中。这将帮助你更可靠地构建和部署软件。
在下一章,我们将看到如何使用 Kubernetes 和 Amazon Web Services 弹性 Kubernetes 服务(AWS EKS)来管理更大规模、更强大的服务器集群,这些集群可以托管运行在 Docker 中的应用程序。
进一步阅读
如果你选择使用 Jenkins 来管理基于 Docker 的环境,应该更深入地查看这些资源:
-
使用
Jenkinsfile
:jenkins.io/doc/book/pipeline/jenkinsfile/
-
Jenkins Docker 集成文档:
jenkins.io/doc/book/pipeline/docker/
-
保护 Jenkins:
jenkins.io/doc/book/system-administration/security/
-
使用 Let's Encrypt 和 Apache 配置 Jenkins 的 SSL:https://www.agileana.com/blog/serve-jenkins-over-https-with-apache-as-proxy-and-certbot-letsencrypt-ssl/
-
使用 NGINX 反向代理或 AWS ELB 为 Jenkins 配置 SSL:
wiki.jenkins.io/display/JENKINS/Jenkins+behind+an+NGinX+reverse+proxy
如果你在单台主机上使用 docker-compose
运行真实的生产应用程序,你应该强烈考虑为你的站点配置 SSL。你可以使用 Let's Encrypt 和一系列 Docker sidecar 容器来实现这一点:
-
如何使用 Let's Encrypt、NGINX 和 Docker 为你的站点配置 SSL:
github.com/nginx-proxy/docker-letsencrypt-nginx-proxy-companion
-
使用
docker-compose.yml
配置 Let's Encrypt 与 NGINX 和 Docker:github.com/nginx-proxy/docker-letsencrypt-nginx-proxy-companion/blob/master/docs/Docker-Compose.md
第八章:将 Docker 应用程序部署到 Kubernetes
最近,很多容器编排工具如雨后春笋般涌现,但有一个编排工具准备主导市场:Kubernetes,来自云原生计算基金会。Google 最初发布 Kubernetes 的目的是将其内部使用的 Borg 集群系统的复杂性引入开源容器运行时世界。
我们将首先了解不同的 Kubernetes 发行版以及为什么你可能会选择使用每个版本。我们将从在本地开发工作站上使用 Kubernetes 开始,然后在本地安装一个示例应用程序。
在本章的进展过程中,你将学习如何通过 Elastic Kubernetes Service(EKS)在 Amazon Web Services(AWS)上创建一个 Kubernetes 集群,并将应用程序部署到运行在多个 Elastic Compute Cloud(EC2)节点上的集群中。我们将使用 AWS CloudFormation,这是一个基础设施即代码系统,用于部署 EKS 集群。一旦我们将集群部署到 AWS 后,我们将学习如何使用标签和命名空间来组织我们的应用程序。
运行 Kubernetes 集群比迄今为止介绍的其他方案更为复杂,但它为运行集群化应用程序提供了一个庞大的工具和技术世界,采用供应商中立的云原生方法。Kubernetes 不仅对云部署有用,对本地部署和本地开发也同样适用。
在本章中,我们将涵盖以下主要主题:
-
Kubernetes 本地安装选项
-
部署示例应用程序 – ShipIt Clicker v4
-
选择 Kubernetes 发行版
-
熟悉 Kubernetes 概念
-
使用 CloudFormation 启动 AWS EKS
-
将带有资源限制的应用程序部署到 AWS EKS 上的 Kubernetes
-
在 AWS EKS 上使用 AWS Elastic Container Registry
-
使用标签和命名空间来隔离环境
让我们从在本地工作站上启动 Kubernetes 开始。然后,我们将了解可用的各种 Kubernetes 发行版。
技术要求
对于本章,你需要在本地工作站上设置 Kubernetes,可以通过 Docker Desktop 或安装 Kubernetes 发行版(如 Minikube)。此外,为了将容器部署到 AWS,你需要提前设置好 AWS 账户。
如果你还没有 AWS 账户,可以通过以下网址注册:
本章的代码文件可以从 chapter8
目录下载,网址为 github.com/PacktPublishing/Docker-for-Developers/
。
查看以下视频,观看代码演示:
Kubernetes 本地安装选项
您需要设置一个本地 Kubernetes 安装环境,以便在将 Docker 应用程序部署到云端生产环境之前进行构建、打包和测试。请查阅 Kubernetes 入门指南 文档(kubernetes.io/docs/setup/
)。这份文档将这个本地环境称为 学习环境。可以将本地环境视为在将应用程序推向云端的生产环境之前,用来了解和测试应用程序的一个途径。接下来,我们将继续评估选项,首先从 Docker Desktop 的 Kubernetes 支持开始。
启用 Kubernetes 的 Docker Desktop
对于大多数人来说,这是开始尝试 Kubernetes 的最简单方式。如果您选择这样做,您不需要设置云账户或进行复杂的安装即可开始。要安装 Docker Desktop,请访问 www.docker.com/products/docker-desktop
下载安装。
在最近版本的 Docker Desktop 中,您可以启用 Kubernetes 支持,并在工作站上运行和开发 Kubernetes 应用程序。打开工作站上的 Docker Desktop 应用程序,进入 首选项 菜单,打开 设置 对话框。勾选 启用 Kubernetes 选项,然后点击 应用并重启 按钮:
图 8.1 – 启用 Kubernetes 的示例
这将激活您本地工作站上的单节点 Kubernetes 集群。一旦启用了 Kubernetes,您就可以验证您的本地安装是否正常工作。请参见下文了解如何操作。
Minikube
如果您不想通过 Docker Desktop 运行 Kubernetes,您应该使用 Minikube 来设置一个本地的 Kubernetes 单节点集群环境。此环境适用于 Windows、Macintosh 以及各种 Linux 操作系统发行版。
要安装 Minikube,请根据您的操作系统访问 kubernetes.io/docs/tasks/tools/install-minikube/
上的安装说明,然后按照以下章节中的说明验证您的 Minikube 安装是否正常工作。
验证 Kubernetes 安装是否正常工作
与 Kubernetes 进行交互大多是通过 命令行界面 (CLI) 完成的。您可以执行以下命令来查看您的环境是否正常工作;该命令将显示所有正在运行的 pod,包括系统 pod:
kubectl get pods -A
输出将类似于以下内容:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_08_002.jpg)
图 8.2 – kubectl get pods 输出
现在你已经在本地工作站上运行了 Kubernetes,可以使用 Kubernetes 开发和部署应用程序。你开发并用 Kubernetes 打包的应用程序可以使用你在本地使用的相同工具进行部署——但在云中可以扩展到更大的规模。然而,在将应用程序部署到云端之前,我们应该先展示如何将一个打包的应用程序部署到本地。
部署一个示例应用程序——ShipIt Clicker v4
假设前面章节中介绍的 ShipIt Clicker 应用程序已经上线到生产环境,而负责运维的团队对该应用程序的扩展能力感到担忧,因为它仅部署在一台服务器上。为了将该 Docker 应用程序扩展到多个服务器,团队决定迁移到 Kubernetes,并使用 Helm 包管理器为 Kubernetes 打包软件。接下来,让我们安装 Helm 并进行测试。
安装 Helm
Helm 对 Kubernetes 的作用类似于包管理器对现代操作系统的作用。它允许开发人员指定如何在 Kubernetes 集群中打包和部署应用程序。Helm 不仅是一个包管理器,还是一个用于生成 Kubernetes 配置和以受控方式应用这些配置的模板系统。Helm 允许开发人员定义一整套容器及其相互关联的 Kubernetes 配置。一旦在 Helm 中定义了一个应用程序,安装和更新该应用程序就变得非常简单。
你可以通过以下命令,使用 Homebrew 在 macOS 上轻松安装它:
brew install helm
对于其他操作系统,请按照 helm.sh/docs/intro/install/
上的 Helm 安装说明进行操作。
安装 Helm 后,使用以下命令安装稳定的 Helm 仓库(这样我们就可以安装 Helm 支持的其他软件包,比如 NGINX Ingress 控制器):
helm repo add stable https://kubernetes-charts.storage.googleapis.com/
安装完成后,你可以使用 Helm 从目录中安装应用程序到本地 Kubernetes 实例。你还可以使用 Helm 安装在本地 Helm 图表中定义的应用程序。我们将使用 Helm 将 ShipIt Clicker 部署到 Kubernetes,并结合另一个 Helm 包——NGINX Ingress 控制器。在本章中,我们将首先将 ShipIt Clicker 应用程序部署到本地学习环境的 Kubernetes 集群,稍后我们将把 ShipIt Clicker 部署到 Amazon EKS 云环境中。
在本地部署 NGINX Ingress 控制器和 ShipIt Clicker
让我们使用 Helm 安装一个打包的应用程序——NGINX Ingress 控制器,然后用它来安装 ShipIt Clicker。Ingress 控制器是 Kubernetes 网络代理,允许外部请求访问部署在 Kubernetes 上的应用程序,并提供清晰的接口来帮助连接这些应用程序。稳定的 Helm 仓库包含了 NGINX Ingress 控制器。安装步骤如下:
helm install nginx-ingress stable/nginx-ingress
在本章的后面部分,我们将更详细地探讨 Ingress Controller。暂时知道,这个简单的安装已经足够通过正确的配置将服务暴露到 Kubernetes 集群内部的 localhost
,以便你进行测试。
接下来,我们将构建 ShipIt Clicker Docker 容器、为其打标签,并将其推送到 Docker Hub。Kubernetes 依赖于从 Docker 镜像注册表拉取 Docker 镜像,因此仅仅在本地系统上拥有容器是不够的。执行以下命令,替换 dockerfordevelopers
为你的 Docker Hub 用户名:
$ cd chapter8
$ docker build . -t dockerfordevelopers/shipitclicker:0.4.0
$ docker push dockerfordevelopers/shipitclicker:0.4.0
编辑 shipitclicker/values.yaml
文件,并在此段落中将 dockerfordevelopers
替换为你的 Docker Hub 用户名:
# Default values for shipitclicker.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: dockerfordevelopers/shipitclicker
pullPolicy: IfNotPresent
然后,将 ShipIt Clicker 部署到 Kubernetes 本地环境中。在这种情况下,我们将使用本地 Helm Chart,而不是来自网络 Helm Chart 仓库的 Helm Chart。ShipIt Clicker 的 Helm Chart 位于 GitHub 仓库的 chapter8/shipitclicker
目录中。使用 Helm 安装它,命令如下:
$ helm install shipitclicker shipitclicker NAME: shipitclicker
LAST DEPLOYED: Fri Apr 24 23:21:22 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1\. Get the application URL by running these commands:
http://localhost
访问 http://localhost/
来查看 ShipIt Clicker 应用程序。你应该能看到正在运行的应用程序启动画面。
本地安装故障排除
如果你无法在 http://localhost/
访问应用程序,可能是因为在端口 80
上运行了另一个 Web 服务器,例如 Apache 2。
现在,我们在 Kubernetes 上运行此应用程序,你需要使用 Kubernetes 命令连接到集群内部的服务,而这些服务并未通过 Ingress Controller 暴露。
要从 Kubernetes 集群中暴露 Redis 端口进行测试,使用以下命令:
$ brew install redis
$ kubectl port-forward deployment/shipitclicker 6379 &
$ redis-cli
> keys *
> quit
现在,你已经将 ShipIt Clicker 应用程序部署到本地 Kubernetes 安装中,可以继续将其部署到更大的云环境中,并为生产就绪做配置。
选择 Kubernetes 发行版
那么,如何在不将 Kubernetes 安装在工作站上的情况下托管 Kubernetes 呢?当选择 Kubernetes 发行版时,你会面临许多选择,正如我们在 第五章 生产环境中部署和运行容器的替代方案 中所看到的那样。现在,我们将重新回顾一些最流行的选项,帮助你根据你的云服务提供商或裸机数据中心设置,了解可用的选择,并解释为什么我们选择使用 EKS 来演示将 ShipIt Clicker 示例应用程序迁移到 Kubernetes。
Google Kubernetes Engine
Google Kubernetes Engine (GKE) 是 Google 在基于 Kubernetes 的环境中托管容器的关键服务。GKE(前身为 Google Container Engine)在 2014 年 11 月发布了 Alpha 版本,并于 2015 年 8 月开始面向公众上线。
它目前提供了云服务商提供的最成熟的 Kubernetes 服务之一,包括以下功能:
-
一个单一集群的快速启动选项,用于试用该服务
-
容器漏洞扫描
-
内置数据加密
-
多种升级、修复和发布渠道
-
与 Google 监控服务的集成
-
自动扩展和负载均衡
-
Google 管理的底层硬件
有兴趣的读者可以在 GKE 网站上找到更多文档:cloud.google.com/kubernetes-engine/docs
。
现在我们来与亚马逊的产品进行比较。
AWS EKS
亚马逊针对云中容器服务和管理的解决方案是其 EKS 服务。与 GKE 一样,亚马逊的 Kubernetes 服务 EKS 提供托管服务。不同于 Google 的产品,它是稍晚进入市场的,直到 2018 年初才发布。然而,EKS 在成熟度上有所欠缺,但在功能上却弥补了这一点。
这些特性包括:
-
通过 AWS Fargate 进行无服务器托管 (
aws.amazon.com/fargate/
) -
EC2 上的服务器部署选项
-
零停机升级和补丁
-
自动检测不健康的节点
-
使用 AWS Outposts 的混合托管解决方案 (
aws.amazon.com/outposts/
) -
Kubernetes 批处理作业
你可以在官方网页上阅读更多关于 EKS 的信息:aws.amazon.com/eks/features/
。
本章及后续章节中,我们将更详细地探讨 EKS,主要是因为它是主导云服务商提供的托管 Kubernetes 服务。然而,其他发行版也有其优势,因此我们也将研究一些其他的选择。接下来是 Red Hat OpenShift。
Red Hat OpenShift
OpenShift 是由 Red Hat 开发的一组软件,专为容器化应用架构而设计。像 GKE 和 EKS 一样,OpenShift 以 Kubernetes 为核心;然而,它的不同之处在于,OpenShift 更侧重于构建相关的工件和本地镜像库。
在本书中使用 Jenkins 的项目之后,你现在应该熟悉 kubectl
命令,了解包括机制在内的 CI/CD 功能,这些功能通常需要使用 Jenkins 或 Spinnaker 等软件来实现。这包括创建构建、测试运行和部署的能力。
还有一些其他关键特性,使得 OpenShift 成为一个值得选择的选项:
-
自动化升级和生命周期管理
-
GitHub 上的开源代码库 (
github.com/openshift
) -
可部署于任何云环境、数据中心或本地
-
一个镜像注册库
-
监控和日志聚合
如需了解更多关于 Red Hat OpenShift 的信息,请务必查看 GitHub 上的文档 (github.com/openshift/openshift-docs
) 或访问官方网站 (www.openshift.com/
)。
Microsoft Azure Kubernetes 服务
到目前为止,我们已经介绍了主要的参与者,但当然,在深入之前,必须提到微软对 Kubernetes 生态系统的贡献。对于使用微软云产品的用户,Azure Kubernetes Service(AKS)提供了一种机制,用于在基于 Kubernetes 的环境中提供 Docker 容器服务。
让我们简要浏览一下 AKS 提供的功能:
-
服务的弹性供应
-
与 Azure DevOps 和监控服务的集成
-
使用 Active Directory 进行身份和访问管理
-
故障检测和容器健康监控
-
金丝雀发布
-
日志聚合
如你所见,对于 Azure 用户来说,它提供了一组与 EKS 和 GKE 类似的功能。如果你想了解更多信息,请参考 AKS 文档(docs.microsoft.com/en-us/azure/aks/
)。在这里,你还可以找到一个快速入门指南,帮助你快速体验该服务提供的功能。
在介绍构成 Kubernetes 基础的各个组件之前,我们先简要回顾一下其他可用的选项。
回顾其他相关选项
EKS、OpenShift、GKE 和 AKS 代表了市场上最流行的 Kubernetes 服务。然而,它们并不是唯一的。Digital Ocean 为那些希望尝试托管服务,而不是自己部署 RedShift 基础设施或注册大云服务提供商的用户提供了一个选择。你可以在www.digitalocean.com/products/kubernetes/
阅读更多相关内容。
许多读者应该熟悉 IBM,他们也提供云托管服务。如果你想在他们的云环境中尝试 Kubernetes,可以在他们的网站上找到相关详情,包括如何设置一个免费集群(www.ibm.com/cloud/container-service/
)。
任何熟悉 VMware 的用户可能也想了解他们的 Kubernetes 解决方案——VMware Tanzu Kubernetes Grid——它在构建混合云方面具有优势(tanzu.vmware.com/kubernetes-grid
)。
最后,寻求完全托管 Kubernetes 服务的用户,或者已经是 Rackspace 客户的用户,可以选择查看他们的Kubernetes as a Service(KaaS)服务(www.rackspace.com/managed-kubernetes
)。
这就结束了我们对容器部署托管平台的快速浏览。
本章剩余部分我们将使用 Amazon 的 EKS 服务。如果你还没有创建账户,建议你现在立即注册:
注意
其他云服务提供商的用户可能会发现,如果愿意,他们可以将以下部分内容适配到自己的服务上。
现在,让我们深入探讨 Kubernetes 的核心概念,包括 pods、nodes 和 namespaces。
熟悉 Kubernetes 的概念
现在,您知道了可以在哪些地方部署 Kubernetes,让我们深入了解一些关键概念(包括对象、ConfigMap、Pod、节点、服务、Ingress 控制器、秘密和命名空间)以及它们如何工作。我们先从一个架构图开始,展示系统中各个组件之间的关系:
图 8.3 – Kubernetes 架构图
图 8.3 – Kubernetes 架构图
在 Kubernetes 中,集群由一个控制平面和一组工作节点组成。控制平面管理 Kubernetes 集群的各个方面(包括与云提供商的接口),而工作节点是托管集群中的应用程序的地方。开发人员和集群操作员通过控制平面与 Kubernetes 交互,控制平面中的进程通过 kubelet
进程与各个工作节点上的进程进行通信,工作节点上的进程被组织为 Pod,通过每个节点上运行的 kube-proxy
进程相互通信。
对象
Kubernetes 中最基本的概念是 kubectl
工具,它用于创建、查询和修改各种 Kubernetes 对象,以及配置集群。
kubectl
命令行工具可以接收描述对象的 YAML 格式文件,并使用这些文件来创建和更新系统的状态。这是定义、安装和升级 Kubernetes 应用程序的最基本方式。我们用于安装应用程序的 Helm 工具则进一步提供了模板化和生命周期管理功能。
我们推荐通过 Helm Charts 配置您的应用程序。您在本章开始时简要了解了如何使用 Helm。Helm Chart 只是包含关于您的容器化应用程序信息的一组 YAML 配置文件。
您可以使用以下命令创建一个新的 Helm Chart:
helm create my-chart
这会设置一个 Helm Chart 结构,包含可以定制的模板文件。
ConfigMap
Kubernetes 使用名为 ConfigMap 的概念来处理应用程序配置。接着,我们需要定义容器本身的配置。这通过 ConfigMap 来处理。
ConfigMap 的核心理念是,您可以将重要的配置与镜像本身的内容分离开来。这样做是为了更好地实现微服务和应用程序的可移植性。
ConfigMap 可以通过 kubectl
使用以下命令直接创建:
kubectl create configmap sample-configmap-name
ConfigMap 将包含应用程序使用的信息,以及其他键值对,例如命名空间。以下示例展示了一个应用程序的 ConfigMap 可能是什么样子:
apiVersion: v1
kind: ConfigMap
metadata:
name: shipitclicker-configmap
data:
language: "JavaScript"
node.version: "13.x"
我们刚才演示的 ConfigMap 将存储在 Helm Chart 目录中的 templates 文件夹里——例如,shipitclicker/templates/configmap.yaml
。
在这个基本设置完成后,你可以通过helm install
命令安装你的配置。我们将在本章的后续部分更详细地探讨 ConfigMap 和 Helm Chart 格式的配置。
Pods
Kubernetes 中的 Pod 用于将1到n个容器化组件组合在一起,然后在共享的上下文中运行。它们还包括共享资源,例如 IP 地址、存储和容器如何运行的定义。在 Pod 中一起运行的多个容器可以通过localhost
上的固定端口互相通信,从而大大简化应用配置。
在定义应该在 Pod 中运行的内容时,最好的方法是将其视为包含系统或应用所需所有容器的容器组。然后,可以将多个 Pod 添加到 Kubernetes 中,水平扩展你的应用。这使你能够创建冗余,并帮助应对流量和负载的增加。
Pod 所使用的共享上下文是通过 Linux 概念(如 cgroups 和 namespaces)实现的。在第十二章《容器安全概述》中,我们将深入探讨一些与容器安全相关的概念。
节点
托管 Docker 容器的机器在 Kubernetes 生态系统中被称为节点,尽管你也可能遇到术语minions或workers——它们的意思相同,但节点是官方术语。Kubernetes 支持物理机或虚拟机类型的节点。像 Amazon 的 EKS 这样的服务提供了部署节点基础设施的机制。你将 Kubernetes Pod 部署在节点上;Pod 包括容器和共享资源。
在我们使用的学习环境中,我们的本地开发工作站是集群中唯一的节点。在本章稍后的部分,我们将创建一个由 EKS 管理的 Kubernetes 集群,节点部署在 AWS EC2 上。Kubernetes 节点通过 Pod 和其他 Kubernetes 对象(如 DaemonSets)来运行容器。
替代的容器运行时
Kubernetes 节点可能运行不同的容器运行时。Kubernetes 不仅支持 Docker 容器,还支持其他容器技术,包括 containerd、CRI-O 和 Frakti。由于本书主要讲解 Docker,我们将在示例中专门使用 Docker 运行时。
服务
Kubernetes 服务是一种声明应用如何向外界暴露其接口的方式。它通常定义一个网络端口,其他 Kubernetes Pod 可以使用该端口与应用进行通信。
ShipIt Clicker 的 Helm Chart 会发出一个服务模板,定义一个ClusterIP
服务定义:
$ helm template shipitclicker ./shipitclicker | less
…
# Source: shipitclicker/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: shipitclicker
labels:
helm.sh/chart: shipitclicker-0.1.10
app.kubernetes.io/name: shipitclicker
app.kubernetes.io/instance: shipitclicker
app.kubernetes.io/version: "0.4.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 8008
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: shipitclicker
app.kubernetes.io/instance: shipitclicker
这份声明描述了 ShipIt Clicker 在每个 Pod 上通过8008
端口暴露 HTTP 作为服务的事实。这让其他 Kubernetes 服务能够发现并与其建立连接。
Ingress 控制器
Kubernetes 管理一个内部网络,在该网络中,集群中的应用程序可以通过私有网络相互通信。默认情况下,无法从外部访问运行在 Kubernetes 集群内的应用程序。Ingress Controller 充当代理和连接经纪人。根据你是部署在本地还是云端,不同类型的 Ingress Controller 有不同的用途。例如,在本章前面,我们安装了nginx-ingress
Ingress Controller,以便让我们能够访问本地 Kubernetes 安装上运行的应用程序。当你希望以与供应商无关的方式授予访问 Kubernetes 应用程序的权限时,这个控制器也是很有用的。
其他 Ingress Controllers 允许 Kubernetes 与不同类型的外部负载均衡器(例如aws-alb-ingress-controller
)平滑协作,aws-alb-ingress-controller
使得可以使用k8s-bigip-ctlr
,后者允许使用 F5 BIG-IP 负载均衡器,这些负载均衡器在许多数据中心中都有使用。
你可以使用 Ingress Controllers 将域名和 HTTP 路径映射到 Kubernetes 服务。这使得在不同的 URL 下暴露不同的服务变得非常简单。如果你有一组微服务,你可以使用这种模式将它们暴露在不同的 API 端点上。通过声明一个 Ingress 对象并为你的应用程序广告如何将你的服务连接到外部世界,你可以利用 Ingress Controllers。例如,对于 ShipIt Clicker 示例,我们使用以下方式将服务映射到默认命名空间中的localhost
:
$ helm template shipitclicker ./shipitclicker | less
…
# Source: shipitclicker/templates/ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shipitclicker
labels:
helm.sh/chart: shipitclicker-0.1.10
app.kubernetes.io/name: shipitclicker
app.kubernetes.io/instance: shipitclicker
app.kubernetes.io/version: "0.4.0"
app.kubernetes.io/managed-by: Helm
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
spec:
rules:
- host: "localhost"
http:
paths:
- path: /
backend:
serviceName: shipitclicker
servicePort: 8008
…
Kubernetes 系统使用这个 Ingress Controllers 定义处理外部与集群内托管的应用程序的连接。这意味着,当你首次开发你的应用程序时,你不需要担心它是如何连接到外部世界的。启用 Ingress Controllers 的 Kubernetes 配置也可以通过 Helm Charts 进行管理。
接下来,我们将探讨 Kubernetes 如何处理敏感信息——通过使用密钥。
密钥
每个应用程序都有需要保护的值,从数据库密码到 API 密钥,因此拥有一个安全存储和检索它们的机制是一个重要功能。在 Kubernetes 中,这是通过一个名为“secrets”的机制来处理的。你可以使用配置文件和kubectl
命令的组合来共享和修改需要保护的信息,这些信息与你的 Pod 及其运行的容器相关联。一旦你创建了一个密钥,你可以通过多种机制在你的应用程序中使用它,包括将密钥暴露为环境变量,或者创建一个文件供在 Pod 中运行的容器来检索。
Kubernetes 中与密钥相关的关键操作如下:
-
创建一个密钥
-
描述一个密钥
-
检索一个密钥
-
编辑一个密钥
让我们从创建一个密钥开始,探索这四个概念。
创建一个密钥
我们可以通过几种方法来创建一个机密。可以通过命令行手动添加,或者将其存储在 YAML 模板文件中并从中使用。
要通过命令行添加存储在文本文件中的机密,可以使用以下命令:
$ echo "new-secret" > secret.txt
$ kubectl create secret generic secex --from-file=./secret.txt
如果我们这样做,kubectl
将为我们处理机密的 Base64 编码。
让我们用另一种方式准备一个机密,使用配置文件。为了为这个文件准备一个文本机密,必须进行 Base64 编码。你可以通过以下命令在 macOS 或 Linux 的命令行中完成此操作:
$ echo -n "changed-api-key" | base64
Y2hhbmdlZC1hcGkta2V5
如果我们想将机密存储在配置文件中,并使用 kubectl
将其添加到 Kubernetes 中,我们可以创建以下的 secret-api-token.yaml
文件:
---
apiVersion: v1
kind: Secret
metadata:
name: api-token
namespace: default
type: Opaque
data:
token: "Y2hhbmdlZC1hcGkta2V5"
然后,使用 kubectl apply
命令行选项,我们可以创建机密:
kubectl apply –f ./secret-api-token.yaml
你会注意到,机密的配置文件格式与我们检查的示例 ConfigMap 非常相似。
因为 shipitclicker
使用 Helm 来管理其 Kubernetes 对象,所以它的模板内置了对机密的支持。本章代码中引用的唯一机密与 Node.js 服务器端框架设置有关,特别是样例应用程序中用于处理服务器会话的 Express 框架。这个机密被称为 SESSION_SECRET
,它存储在 chapter8/shipitclicker/templates/secrets.yaml
文件中:
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name}}-secrets
namespace: {{ .Release.Namespace }}
type: Opaque
data:
SESSION_SECRET: "bXlTZWNyZXQtdjQK"
请注意,这里使用了模板表达式来设置 name
和 namespace
,以便与 Helm 转换的其他模板对齐。
我们在本章早些时候使用 helm install
命令安装 shipitclicker
Helm 模板时创建了这个机密。这就是使用 Helm 模板创建机密的方法。
现在我们已经看到几种创建机密的方法,接下来我们将展示如何查询 Kubernetes 知道哪些机密。
描述一个机密
创建机密后,可以使用 kubectl get secrets
命令列出它。这将类似地列出机密:
图 8.4 – 机密列表
要了解更多关于机密的信息,可以使用 kubectl describe
命令:
kubectl describe secrets/shipitclicker-secrets
上述命令的输出将在以下截图中显示:
图 8.5 – kubectl describe
命令输出,显示机密的元数据
你将看到显示的有关机密的元数据,包括机密的键——在此例中为 SESSION_SECRET
。不过,它不会显示机密的值。
检索一个机密
Kubernetes 应用程序检索简单机密的典型方式是将其定义为传递给容器的环境变量,该变量引用了机密。请参见从渲染的 Helm 图表模板中摘录的内容:
# Source: shipitclicker/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: shipitclicker … containers:
- name: shipitclicker
…
env:… - name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: shipitclicker-configmap
key: REDIS_PORT
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: shipitclicker-secrets
key: SESSION_SECRET
你可以看到,映射到 shipitclicker
容器部署的环境变量引用了 configMapKeyRef
和 secretKeyRef
两个条目。
对于更复杂的机密,比如完整的文件(如 SSH 私钥),机制类似。有关更多场景的详细信息,请参阅 Kubernetes 机密文档:kubernetes.io/docs/concepts/configuration/secret/
。
为了故障排除,我们可以通过命令行从 Kubernetes 中检索机密:
$ template='go-template={{index .data "SESSION_SECRET"}}'
$ kubectl get secrets shipitclicker-secrets -o "$template" | base64 -D
mySecret-v4
现在我们已经了解了如何检索一个机密,接下来我们将学习如何编辑机密。
编辑机密
如果您希望在创建后编辑机密,可以使用kubectl edit
命令:
kubectl edit secrets secex
这将打开您的默认编辑器(默认是 vi),您可以编辑机密。您需要准备好 Base64 编码的替换值。它看起来应该像这样:
apiVersion: v1
data:
secret.txt: Y2hhbmdlZC1hcGkta2V5LTI=
kind: Secret
metadata:
creationTimestamp: "2020-04-25T20:54:31Z"
name: secex
namespace: default
resourceVersion: "826562"
selfLink: /api/v1/namespaces/default/secrets/sample-secret
uid: ce8fbf27-33ba-461e-9bb8-1ca31fa3e888
type: Opaque
您可以通过这种方式直接编辑机密。更新机密后,可能需要重新部署您的应用程序,具体取决于它如何使用该机密。手动管理这一过程可能变得复杂,这也是我们使用 Helm 打包应用程序的原因之一。
更新 ShipIt Clicker 会话密钥
对于通过 Helm 部署的应用程序,通常的做法是通过 Helm 模板进行更改,而不是使用原始的kubectl
命令。现在,我们将通过以下步骤,使用 Helm 更改 ShipIt Clicker 的SESSION_SECRET
密钥:
-
使用以下命令生成一个 Base64 编码的机密:
echo -n "new-session-secret" | base64
-
编辑模板
chapter8/shipitclicker/templates/secrets.yaml
文件。 -
使用
openssl
命令输出的值作为新的SESSION_SECRET
值。 -
编辑
chapter8/shipitclicker/Chart.yaml
文件,并增加图表的version
号。 -
每次更新 Helm 图表时,您都需要执行此操作。然后,使用以下命令更新模板:
helm upgrade shipitclicker ./shipitclicker
如您所见,添加和编辑机密的基本命令非常简单。但在我们的应用程序中使用它们稍微复杂一些。这将让您对如何创建机密值以及如何检索相关信息有所了解,以便探索该功能。
注意
欲了解更多关于机密的信息,请查看最新的 Kubernetes 文档:kubernetes.io/docs/concepts/configuration/secret/
。
在第十四章,高级 Docker 安全性——机密、机密命令、标记和标签中,我们探讨了与 Docker Swarm 相关的机密存储和使用。虽然 Docker Swarm 逐渐失宠,许多团队正在转向 Kubernetes,但理解这些概念对于维护遗留系统非常重要。此外,您可能会发现自己需要将系统从 Docker Swarm 迁移到 Kubernetes。本章提供的信息和第十四章,高级 Docker 安全性——机密、机密命令、标记和标签中的内容,将帮助您将一个技术的概念映射到另一个技术。
命名空间
为了在 Kubernetes 中划分资源,我们可以使用一个叫做命名空间的概念。命名空间提供了一种机制,将容器资源分组到不重叠的集合中,这样就可以根据业务需求,在同一个集群中进一步细分 Kubernetes 资源。这可能包括从环境(开发、测试和生产)到微服务组的所有内容。你应该考虑的一个重要因素是,同一命名空间中的应用程序可以读取该命名空间中的任何机密,因此它也代表了一个安全边界。
一旦你了解了这个功能,可能会忍不住想在各处使用它,但 Kubernetes 文档对此做出了警告。主要的命名空间内容页面 (kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
) 中写道:
"对于有少数到几十个用户的集群,你根本不需要创建或考虑命名空间。"
然而,请记住,不同的团队可能希望将应用程序相互隔离,命名空间是一个很好的方法,因为它提供了一个安全边界。在本章后面的使用标签和命名空间来隔离环境部分,我们将探索如何使用这一概念,将应用程序同时部署到 AWS 中的测试环境和生产环境。
接下来,让我们通过 CloudFormation 设置 AWS EKS,以便使用 Kubernetes 将我们的应用程序部署到公有云。
使用 CloudFormation 启动 AWS EKS
现在,我们已经完成了 Kubernetes 本地安装的介绍,并探索了一些云供应商选项,我们将尝试将容器部署到 AWS 托管的 Kubernetes 环境中。这将是我们在本章上一节中简要介绍的 EKS 服务。
为了实现这一目标,我们将描述如何使用 AWS CloudFormation 创建和管理 EKS 集群,CloudFormation 是他们的基础设施即代码服务。有关 CloudFormation 的更多信息,务必查看 AWS 的指南和文档:docs.aws.amazon.com/cloudformation/
。
假设你之前已经创建了 AWS 账户,或者按照本章技术要求部分的说明操作,打开 AWS 云控制台。
要继续,我们需要设置 EKS。获得一个工作中的 EKS 集群有很多方法,每种方法需要不同的工作量:
-
通过 AWS 控制台手动逐步设置所有内容。我们不推荐这种方法,因为它需要深厚的 AWS 知识来正确执行,而且会导致一个难以复制的环境,并且缺乏控制。
-
从零开始编写基础设施即代码模板,使用 AWS CloudFormation 或 Terraform 来控制所需的所有资源。如果你是 CloudFormation 或 Terraform 的专家,并且已经有了在 CloudFormation 或 Terraform 工具上的投资,这种方法可能适合你,但我们不建议初学者使用这种方法。
-
使用
eksctl
工具(见eksctl.io
)通过简单的 CLI 工具创建一个集群。如果你已经熟悉 AWS,并希望将集群放置在特定区域并调整更多集群参数,这个方法可能会很有效。我们仅建议在你已经熟悉 AWS 和 EKS 的情况下使用此方法。 -
研究并采用已经有人编写的基础设施即代码模板。AWS 和许多其他人已经创建了 CloudFormation 和 Terraform 模板。
我们将采用最后一种方法,使用 AWS 快速启动 CloudFormation 模板来创建我们的第一个云 Kubernetes 集群。
介绍 AWS EKS 快速启动 CloudFormation 模板
亚马逊提供了一套便捷的 CloudFormation 模板,称为 Quick Starts,由其专家云架构师构建,用于快速启动 AWS 服务和场景的广泛选择(aws.amazon.com/quickstart/
)。
我们将在本章的下一部分使用 AWS EKS 快速启动模板。
但是,在你部署 EKS 快速启动 CloudFormation 模板之前,请花一点时间为你的 AWS 账户做准备工作。
准备 AWS 账户
如果你刚开始使用 AWS,在继续操作之前有一些关键的事项需要注意,以保护你的账户。这些预防措施和准备工作同样适用于如果你选择了除使用 AWS 快速启动 CloudFormation 模板以外的其他方法来创建 EKS 集群。
如果你已经是经验丰富的 AWS 用户,拥有 AWS us-east-2
区域,并且知道你的公共 IPv4 地址,你可以跳到启动 AWS EKS 快速启动 CloudFormation 模板部分。不过,请避免使用具有管理员权限的假定 IAM 角色来创建 CloudFormation 模板——这样会导致一些子模板进入 UPDATE_ROLLBACK_FAILED
状态,恢复起来非常困难。
使用 IAM 管理员用户而不是根账户用户
首先,确保你不是以根账户用户身份使用 AWS 控制台。这是一个重大的安全风险。你将需要一个具有管理员权限的 AWS IAM 用户账户。如果你刚创建了 AWS 根账户,可以按照 AWS 指南在 docs.aws.amazon.com/IAM/latest/UserGuide/getting-started_create-admin-group.html
上的步骤创建一个。
在按照说明设置好此用户并启用 IAM 用户的账单访问后,前往 console.aws.amazon.com/iam/home#/home
页面,将 IAM 用户的登录链接复制到剪贴板。编辑您的网页浏览器书签,并使用此 URL 创建一个AWS IAM 登录项。您将需要使用此项通过管理员账户登录到您的 AWS 账户,而不是使用根账户。
在您的本地系统中,创建一个 eks-notes.txt
文件,并将登录链接记录在那里。还要从 console.aws.amazon.com/iam/home?region=us-east-2#/users/Administrator
URL 中记录管理员用户的用户 ARN值:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_08_006.jpg)
图 8.6 – 管理员用户的 AWS IAM 用户摘要
这个Amazon 资源名称(ARN)用户是一个字符串,类似于 Web 统一资源标识符(URI),但它是专属于 Amazon 的。现在我们已经设置了一个管理员用户,接下来让我们设置多重身份验证(MFA)来保护根账户和管理员用户。
设置 MFA
我们建议您使用 MFA 来保护根账户和每个具有管理员权限的 IAM 用户账户。如果有人破解了您的根账户,他们可能通过启动昂贵的云资源来产生巨额账单,窃取您的信息,甚至删除您的所有数据。在开始时,我们建议您使用虚拟 MFA 设备和支持软件(如 Google Authenticator、Authy 或 1Password)来启用 MFA。
为了增强安全性,您可以选择使用受支持的硬件令牌解决方案,但虚拟 MFA 也可以正常工作。有关设置 MFA 的更多详情,请参阅 AWS MFA 文档:
aws.amazon.com/iam/features/mfa/
使用 IAM 用户账户登录 AWS 控制台
确保您已退出根账户。然后,使用您的eks-notes.txt
文档中的登录 URL,使用您的管理员 IAM 用户账户登录 AWS 控制台,才可继续操作。
为 IAM 管理员用户创建访问密钥
为了使用 AWS 命令行工具,您需要生成 AWS 访问密钥。您可以在 docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
阅读更多关于访问密钥和其他类型 AWS 凭证的信息。
在 AWS 控制台中,进入 IAM 服务,并在用户部分找到您刚刚创建的管理员用户。然后,导航到安全凭证选项卡,通过点击创建访问密钥按钮创建新的访问密钥:
图 8.7– 管理员用户的 AWS IAM 用户摘要
将这些访问密钥作为 CSV 文件下载到本地系统。你需要打开该文件并检查密钥,以便配置 AWS CLI,接下来我们将进行此操作。
在本地工作站上配置 AWS CLI
为了完成 EKS 集群的配置,你需要在本地工作站上安装一个有效的 AWS CLI。如果你还没有安装,请按照 aws.amazon.com/cli/
上的说明进行安装。
安装完成后,执行 aws configure
命令,并使用你在上节中保存的访问密钥 CSV 文件中的访问 ID 和秘密密钥来配置 CLI,以便使用管理员用户。使用 aws sts get-caller-identity
命令验证是否配置成功。检查输出,确保没有错误消息,然后验证该命令为活动用户发出的 ARN 与在 IAM Web 控制台中显示的管理员用户 ARN 相同。输出应该类似于以下内容:
图 8.8 – aws sts get-caller-identity 输出
在配置 ALB Ingress Controller 时,你需要完成这个设置,详细内容将在本章后面介绍。
为 EKS 集群创建 EC2 密钥对
为了执行 EKS 集群的初始配置,你需要通过 SSH 连接到 CloudFormation 模板设置的 EC2 虚拟服务器,该服务器被称为堡垒主机。选择 us-east-2
区域。以你的 IAM 管理员身份登录,访问 console.aws.amazon.com/ec2
,然后确保从区域选择器中将区域切换到 us-east-2:
图 8.9 – 切换 AWS 区域
然后,在左侧菜单中找到并点击密钥对链接,创建一个名为 ec2-eks
的新密钥对,并下载它。你将在配置 EKS 集群时需要这个密钥对。为此,将该密钥对复制到本地用户主目录下的 .ssh
目录,并设置权限,以便 SSH 可以使用它:
$ mkdir -p ~/.ssh
$ chmod 0700 ~/.ssh
$ cp ~/Downloads/ec2-eks.pem ~/.ssh/
$ chmod 0600 ~/.ssh/ec2-eks.pem
你稍后将需要这个密钥来连接到 EKS 集群的堡垒主机。接下来,确保你知道你的公共 IP 地址。
以 CIDR 表示法记录你的公共 IP 地址
我们将通过限制访问仅限于你当前使用的公共 IPv4 地址来限制来自互联网对 Kubernetes 集群的访问。这将防止恶意黑客和攻击互联网主机的人扫描你的系统。为此,请访问 whatismyip.com/
,并复制你的公共 IPv4 地址(CIDR 格式),即在原始数字地址后附加 /32
。例如,如果是 192.2.0.15
,那么你的 IPv4 地址的 CIDR 格式将是 192.2.0.15/32
。在本地系统中,打开 eks-notes.txt
文件并记录该 CIDR 地址。
启动 AWS EKS 快速启动 CloudFormation 模板
你可以在 aws.amazon.com/quickstart/architecture/amazon-eks/
找到有关 AWS EKS 快速启动 CloudFormation 模板的文档。
要全面了解此快速启动所提供的内容,请阅读 AWS 提供的相关部署指南:
docs.aws.amazon.com/quickstart/latest/amazon-eks-architecture/welcome.html
至少浏览该页面的概要。当你准备好继续部署时,点击 如何部署 部分。你会看到在部署 CloudFormation 模板时有两个选项,如下所示:
-
部署到新 VPC (
fwd.aws/6dEQ7
) -
部署到现有 VPC (
fwd.aws/e37MA
)
在开始之前,如果你仍然使用 root 账户登录 AWS 控制台,请注销并使用你在 eks-notes.txt
文件中记录的 IAM 登录 URL 作为管理员用户登录。
我们建议你从将此基础设施部署到新的 虚拟专用云(VPC)开始。点击该链接或使用前面的 URL 访问 CloudFormation 堆栈创建表单。大部分表单中的项目可以保持默认,但有些必须填写,以完成初始集群配置,并确保不会意外创建不安全的配置。
EKS 快速启动 CloudFormation 创建指南
创建 CloudFormation 堆栈需要你填写一个四页的 CloudFormation 参数表单,可以通过点击前一部分中的 部署到新 VPC 链接来完成。这是该表单的第一页:
图 8.10 – CloudFormation 表单,第 1 页,共 4 页:准备模板
本指南将帮助你完成相关步骤,以便在大约 30 分钟内启动一个可用的 EKS 集群。
创建堆栈 – 前提条件 – 准备模板
保持表单上的所有项目为默认设置,然后点击 下一步 按钮。这将带你进入 指定堆栈详细信息 页面。
指定堆栈详细信息
你几乎可以保持所有项目的默认设置,但需要为以下参数指定项目:
-
us-east-2a
、us-east-2b
和us-east-2c
。 -
192.2.0.15/32
。 -
EKS 集群名称:选择一个简短的集群名称。
-
8
。 -
eks-ec2
。 -
额外的 EKS 管理员 ARN (IAM 角色):如果你有其他 AWS IAM 角色并希望授予访问权限,则填写该字段,否则保持为空。
-
额外的 EKS 管理员 ARN (IAM 用户):如果你有其他 AWS IAM 用户并希望授予访问权限,则填写该字段,否则保持为空。
-
Kubernetes 版本:1.15。
注意
如果你想尝试第九章中描述的 Spinnaker,请不要使用 1.16 或更高版本,因为 Spinnaker 不支持更高版本的 Kubernetes,云原生持续部署使用 Spinnaker。
-
EKS 公共访问终端:已启用。
-
192.2.0.15/32
。 -
ALB Ingress Controller:已启用。
-
集群自动缩放器:已启用。
-
EFS 存储类:已启用。
-
监控堆栈:Prometheus 和 Grafana。
选择这些选项将最终允许你使用kubectl
、helm
和eksctl
工具从本地工作站管理 EKS 集群。一旦指定了这些,点击表单底部的Next按钮。此操作将带你进入Configure Stack Options屏幕。
配置堆栈选项
保持所有默认设置。点击表单底部的Next按钮。此操作将带你进入Review屏幕。
审查
滚动到表单底部,勾选两个复选框,确认这可能会创建具有自定义名称的 IAM 资源,并且可能需要CAPABILITY_AUTO_EXPAND
权限。点击表单底部的Next按钮以创建 CloudFormation 模板。等待大约 30 分钟,并在 CloudFormation 控制台中查看模板的创建状态——它应该会顺利完成。在继续之前,请检查所有 CloudFormation 模板是否已完成。它应该类似于下面的状态:
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/dkr-dev/img/B11641_08_011.jpg)
图 8.11 - CloudFormation 控制台,状态为 CREATE_COMPLETE
现在,你的 EKS 集群已准备好进行初步配置。
配置 EKS 集群
部署 CloudFormation 模板后,你将拥有一个包含以下 AWS 服务的环境:
-
一个作为集群网络基础设施的 VPC
-
由 AWS 管理的 EKS Kubernetes 控制平面
-
用于配置集群的 EC2 堡垒主机
-
Kubernetes 基础设施,包括三个 EC2 实例,作为节点部署在三个 AWS 可用区中
-
一个允许外部访问集群服务的 ALB Ingress 控制器
要获得对集群的初始访问,查看堆栈的 CloudFormation 输出,并记录标记为BastionIP
的 IPv4 地址。然后,使用该地址 SSH 连接到主机,替换192.2.10
为该 IP 地址:
ssh -i ~/.ssh/eks-ec2.pem ec2-user@192.2.0.10
部署完成后,按照 AWS 部署指南验证集群状态:
docs.aws.amazon.com/quickstart/latest/amazon-eks-architecture/step-3.html
使用你已经学到的一些命令,如kubectl get all -A
、kubectl get nodes
和kubectl describe service/kubernetes
,从堡垒主机探索集群配置。
Bastion 节点已经安装了 kubectl
、helm
和 git
,因此你可以选择使用它来执行一些集群维护任务。Helm 安装中甚至已经安装了稳定的图表仓库,你可以通过 helm repo list
命令来验证这一点。
关注 AWS 成本
一旦你部署了 EKS 基础设施,AWS 将开始按小时收费,直到它停止运行。在 EKS 集群和 EC2 服务器运行期间,你将负责所有产生的费用。保持这个 EKS 集群运行可能每天花费高达 $10-20。请访问 Billing & Cost Management 仪表板,网址是 console.aws.amazon.com/billing/home?#/
,以查看你的月度费用和预计费用。我们建议你定期让 AWS 生成费用和使用报告,以帮助你跟踪支出。有关如何启用此功能的信息,请访问 docs.aws.amazon.com/cur/latest/userguide/cur-create.html
。
验证 ALB Ingress Controller 是否正常工作
由于我们在创建 EKS 集群时启用了 ALB Ingress Controller 可选附加组件,我们可以跳过 ALB 用户指南中的详细说明(docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
),无需为 EKS 设置 ALB Ingress Controller。由于 ALB Ingress Controller 已经设置好,集群将在找到正确注解的 Ingress 对象时自动创建新的 Ingress Controller 和应用程序负载均衡器。
作为练习,你可以部署用户指南最后一节中描述的 2048 游戏,来验证 ALB 是否按预期工作。
将带有资源限制的应用程序部署到 AWS EKS 上的 Kubernetes
在 Kubernetes 中,我们可以为应用程序设置资源限制,以防止它占用集群中所有可用的 CPU 和内存资源。这是为了保护系统免受资源枯竭的影响,并确保那些存在内存泄漏或存在导致其 CPU 使用超过预期的 Bug 的应用程序不会导致整个集群崩溃。
为了演示如何设置资源限制,我们将把之前在本章部署示例应用程序部分中部署到本地 Kubernetes 安装的 ShipIt Clicker Docker 容器和 Helm 图表,部署到 EKS 集群。
为了演示如何设置资源限制,我们现在将演示将 ShipIt Clicker 应用程序部署到由 AWS EKS 服务管理的 Kubernetes 上,并启用 CPU 和内存限制。我们还将使用 Ingress Controller 将该应用程序暴露给全世界。
配置资源限制以防止内存泄漏和 CPU 使用过高
既然我们正在部署到 EKS,我们希望确保我们的 pod 容器在集群中是合格的成员。为此,我们将指定资源请求和限制。请求为 Kubernetes 提供关于初始分配给应用程序的每种资源的数量的指导,并帮助调度器在将容器和 pod 放置到节点时做出决策。只有在节点有足够的可用资源来支持请求时,Kubernetes 才会调度 pod 到该节点。限制则为调度器提供了资源分配的硬性最大限制。如果容器超出了其内存限制,其进程将被以 内存溢出 (OOM) 错误终止。
我们将使用 chapter8/shipitclicker-eks/
中的 Helm 模板,进行首次更改,而不是使用我们在本地系统上安装的基础 Helm 模板。
在 chapter8/shipitclicker-eks/values.yaml
中,我们现在指定了容器的 CPU 和内存请求与限制:
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 500m
memory: 512Mi
这些配置适用于 Redis 和 ShipIt Clicker 容器。
为 ShipIt Clicker 添加注解以使用 ALB Ingress Controller
为了确保 Ingress Controller 注解与 EKS 设置兼容,需要对 chapter8/shipitclicker-eks/values.yaml
文件进行一些更改。我们需要调整注解,使其指向 EKS。同时,我们将移除主机限制,并确保路径的配置包含通配符。由于我们使用的是 ClusterIP
服务端点,因此我们还需要为 ALB Ingress Controller 使用 ip
目标类型:
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
hosts:
# - host: "*"
- paths: ['/*']
如果没有这些注解,ALB Ingress Controller 将很难连接到服务。
将 ShipIt Clicker 部署到 EKS
SSH 登录到堡垒主机,克隆仓库,并使用 Helm 部署软件:
$ git clone https://github.com/PacktPublishing/Docker-for-Developers.git
$ cd Docker-for-Developers helm install shipitclicker chapter8/shipitclicker-eks/
在 AWS EC2 控制台中检查是否有弹性负载均衡器正在创建。可能需要几分钟才能变为可用。当它可用时,在浏览器中输入其 DNS 名称,您应该能够看到 ShipIt Clicker 游戏。
如果看不见它,请通过查看 Ingress Controller 日志进行故障排查:
kubectl logs -n kube-system deployment.apps/alb-ingress-controller
现在,我们已将 ShipIt Clicker 应用程序部署到 EKS,并通过 ALB Ingress Controller 向外部暴露,让我们来看看如何隔离环境,以便不同的 Docker 容器可以在不互相干扰的情况下运行。
使用 AWS Elastic Container Registry 与 AWS EKS
使用存储在 Docker Hub 中的公共镜像对于某些应用程序来说是可以的,但对于更敏感的应用程序,您可能希望将 Docker 容器存储在私有 Docker 注册表中。AWS 提供了一个这样的注册表:Elastic Container Registry (ECR)。您可以在主产品网站 aws.amazon.com/ecr/
阅读更多关于 ECR 的基本信息。
为了让 Kubernetes 集群使用来自私有仓库的镜像,你必须为集群配置正确的凭据,以便它能够从仓库中拉取镜像。大多数仓库的过程可以在 Kubernetes 文档中找到,具体请参见kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
。
然而,AWS ECR 使用增强的安全系统,该系统依赖于 AWS IAM 来授予临时访问令牌,这些令牌用于与 ECR 进行身份验证。Kubernetes 内建对该认证过程的支持,具体说明请参见关于使用私有注册表的镜像文档(kubernetes.io/docs/concepts/containers/images/#using-aws-ec2-container-registry
)。
在使用 ECR 与 Kubernetes 时,你需要在 Pod 配置或它们的 Helm 模板中使用 ECR 标识符来指定镜像。你可以通过以下语法指定镜像,而不是使用默认的 Docker Hub 镜像规范:
ACCOUNT.dkr.ecr.REGION.amazonaws.com/imagename:tag
AWS 的 EKS 文档解释了,运行 Pods 的工作节点必须通过 IAM 角色应用正确的 IAM 策略,以便获取认证令牌并检索镜像:
docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_on_EKS.html
幸运的是,我们用来设置 EKS 集群的 AWS CloudFormation 模板会生成具有正确权限的工作节点,使用eksctl
工具设置集群的所有集群也是如此(如果你选择了这种替代路径)。前面 EKS 网页中描述的 ECR 访问控制规则将授予 EKS 节点读取账户中任何 ECR 仓库中镜像的权限。
因此,要将 ECR 与 EKS 一起使用,我们需要确保将容器推送到与 EKS 集群位于同一账户中的 ECR 仓库,并且我们使用 ECR 样式的仓库 URI 作为 Kubernetes Pod 中运行容器的标识符。
接下来,让我们创建一个 ECR 仓库,以便为集成 ECR 和 EKS 做准备。
创建 ECR 仓库
在网页浏览器中,登录到 AWS 控制台。确保切换到us-east-2
区域(与 EKS 集群所在的区域相同),然后点击Services链接,选择Elastic Container Registry。如果你尚未创建任何注册表,请点击Get Started按钮。AWS 控制台会提示你输入命名空间和仓库。
或者,访问以下 URL 开始创建过程:
console.aws.amazon.com/ecr/create-repository?region=us-east-2
无论哪种方式,你都会看到类似这样的内容:
图 8.12 – ECR 创建仓库表单
保持其他设置为默认值。创建仓库后,记下仓库的 URI;你将需要它来推送容器到注册表。你将在类似下面的界面中看到 URI:
图 8.13 – ECR 仓库页面
然后,点击 查看推送命令 按钮。这将为你提供详细的说明,告诉你如何使用 AWS CLI 获取临时凭证,并使用这些凭证完成 Docker 推送到 ECR 仓库。
练习 – 将 ShipIt Clicker 推送到 ECR 仓库
按照在点击 REPO
值后显示的说明操作,使用从 创建 表单生成的 URI 中的 ECR 注册表主机名:
$ cd Docker-for-Developers/chapter8
$ REPO=143970405955.dkr.ecr.us-east-2.amazonaws.com
$ IMAGE=dockerfordevelopers/shipitclicker
$ aws ecr get-login-password --region us-east-2 | \
docker login --username AWS --password-stdin $REPO
$ docker build -t $IMAGE:latest .
$ docker tag $IMAGE:latest $REPO/$IMAGE:latest
$ docker push $REPO/$IMAGE:latest
如果成功,你将看到类似下面的输出:
图 8.14 – 推送 Docker 到 ECR
在下一章中,我们将使用 ECR 存储通过 Jenkins 构建的 Docker 镜像,并通过 Spinnaker 和 Helm 部署。
现在我们已经看到如何将 Docker 容器镜像存储在 ECR 仓库中,接下来我们将探讨如何使用标签和命名空间来隔离环境。
使用标签和命名空间来隔离环境
我们在本章早些时候了解了什么是命名空间。现在,我们将探讨如何使用命名空间和标签,在本地环境和 EKS 集群中创建独立的环境。
本地示例 – 默认命名空间中的标签化环境
假设你正在开发 ShipIt Clicker 应用程序,并希望保持一个稳定的工作环境,以便向他人展示,并将你正在修改的代码中的新行为与稳定的行为进行比较。虽然你可以使用命名空间来隔离应用程序,但使用不同标签的部署重新部署 Helm Chart 会更简单。你可以使用多个具有不同标签的部署,结合一些模板覆盖,在不必处理多个命名空间复杂性的情况下,通过 Helm 完成这一操作。
为此,我们需要执行以下操作:
-
定义一个主机名来访问服务。
-
配置 ShipIt Clicker 的 Ingress Controller 以使用该主机名。
-
在
chapter8/shipitclicker/Chart.yaml
中配置并增加 Chart 版本。 -
使用与已部署的名称不同的名称部署 Helm Chart,例如
shipit-stable
。 -
测试我们是否可以访问备用环境。
让我们逐步完成这些步骤,以便使用命名空间设置这个稳定的环境。
向本地环境添加多个主机名
添加本地环境的替代名称的经得起时间考验的方法是编辑操作系统的 hosts 文件——在类 UNIX 系统中(如 Linux 和 macOS)为/etc/hosts
,在 Windows 系统中为C:\Windows\System32\Drivers\etc\hosts
。不过,你必须以具有管理员权限的用户身份执行此操作。你可能需要向hosts
文件中添加类似127.0.0.1 shipit-stable.internal.
的条目,遵循tools.ietf.org/html/rfc6762#appendix-G
的部分指导,以选择一个不太可能导致操作问题的 TLD。
然而,现在有一种更简单的方法来实现这一点。你可以使用name.A.B.C.D.nip.io
形式的主机名,它会映射到你提供的任何 IP 地址,这得益于免费的nip.io 服务。这使得创建localhost
别名变得非常方便,因为我们可以使用shipit-stable.127.0.0.1.nip.io
以及类似的名称进行本地开发。
临时配置 shipit-stable 环境的 Helm Chart
编辑chapter8/shipitclicker/values.yaml
文件,修改主机名使其匹配shipit-stable.127.0.0.1.nip.io
,并更新图表版本。然后,使用 Helm 通过命令helm install shipit-stable shipitclicker/
部署应用程序。之后,你应该能够通过访问 http://shipit-stable.127.0.0.1.nip.io/在网页浏览器中看到应用程序。
分阶段环境——开发、QA、暂存和生产
在 EKS 环境中,你也可以通过部署带标签的堆栈来实现良好的环境隔离。你可以为堆栈添加前缀或后缀标签,指示它们属于哪个环境。通过 ALB 支持,每个暴露给外界的独立服务都会有自己独特的负载均衡器,无论它们是否在不同的命名空间中。
但是有一些情况你可能仍然需要使用命名空间。例如,如果你在集群中同时托管生产和非生产资源,你可以让非生产资源的命名空间使用配额。有关配额的更多信息,请参考kubernetes.io/docs/concepts/policy/resource-quotas/
。
练习
使用kubectl
创建一个qa
命名空间,并使用 Helm 将 ShipIt Clicker 部署到该命名空间。然后,为该命名空间设置内存配额,确保它不会使用超过 1GB 的 RAM。
对于更高级的命名空间实践,你应该参考最佳实践文档,链接为cloud.google.com/blog/products/gcp/kubernetes-best-practices-organizing-with-namespaces
。
现在我们已经设置了一个通过命名空间隔离的独立环境,我们在部署和管理应用程序时将具有更多的灵活性。接下来,让我们回顾一下本章所学的内容。
总结
在这一章中,我们学习了 Kubernetes 以及在云中托管 Kubernetes 的选项。我们走访了一些市场上的云托管平台,并对 Kubernetes 的关键组件进行了简要概述。
随后,我们开发了一个将 Docker 容器部署到 AWS EKS 的过程,使用 AWS ECR 作为 Docker 容器注册表。在此过程中,你还有机会尝试 Amazon 的 CloudFormation 技术,这是一个开发基础设施即代码的平台。
接下来,我们学习了 Helm 和 Helm Charts,并在 ShipIt Clicker 应用程序的基础上进行了构建。这个应用在 AWS 上部署,并设置了资源限制。
如果你愿意,现在你应该能够轻松地将这个过程重复应用于另一个项目!
现在我们的基础 Kubernetes 设置已经准备好,接下来我们需要解决哪些问题,才能将其用于可扩展的生产项目?我们已经看到了如何使用 Jenkins 进行持续部署,但编写所有脚本来管理一个复杂的 Kubernetes 集群,并将应用程序可靠地部署到其中,仍然会非常繁琐。
本章介绍了一组简化的 Helm Charts,这些 Charts 生成 Kubernetes 配置,从而启动一个运行中的应用程序,但我们需要做一些优化,以确保应用程序适合生产环境,就像我们在之前的章节中使用 Docker Compose 一样。
在下一章中,我们将介绍 Spinnaker,作为一个云原生的 CI/CD 平台,帮助我们为 Kubernetes 实现 CI/CD,以完成这个特定的任务。
深入阅读
这些文章可能有助于你更好地理解一些 Kubernetes 的基本概念:
-
通过这个幽默的指南,轻松介绍 Kubernetes 概念:
www.cncf.io/the-childrens-illustrated-guide-to-kubernetes/
-
另一本由云原生计算基金会提供的 Kubernetes 概念插图指南,主角是 Phippy:
www.cncf.io/phippy-goes-to-the-zoo-book/
-
为什么 Kubernetes 如此流行?请参阅这篇博客文章:
stackoverflow.blog/2020/05/29/why-kubernetes-getting-so-popular/
-
许多应用程序需要你使用私有 Docker 镜像注册表,无论是 Docker Hub、AWS ECR,还是其他平台。阅读这篇文章,了解如何将注册表密钥集成到你的 Kubernetes 配置文件中:
kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
-
虽然这篇文章面向的是使用 Digital Ocean 的 Kubernetes 服务的客户,但它非常出色地解释了 NGINX Ingress 控制器:
www.digitalocean.com/community/tutorials/how-to-set-up-an-nginx-ingress-on-digitalocean-kubernetes-using-helm
-
EKS 用户指南。这里充满了关于运行 EKS 的超详细信息:
docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html
-
部署 Kubernetes 仪表板。这是可选的,但它将为你提供一个漂亮的 web 用户界面,可以查看更多有关集群的信息:
docs.aws.amazon.com/eks/latest/userguide/dashboard-tutorial.html
-
使用 Kubernetes 命名空间的高级配置示例可能包括使用 Kubernetes 基于角色的访问控制 (RBAC) 系统,进一步限制不同命名空间中应用程序的交互方式:
kubernetes.io/docs/reference/access-authn-authz/rbac/
-
了解更多关于 EKS 安装选项的信息,包括使用混合策略(结合 NGINX 和 ALB Ingress Controller 等)进行 Terraform 配置:
medium.com/
@dmaas/setting-up-amazon-eks-what-you-must-know-9b9c39627fbc
第九章:使用 Spinnaker 进行云原生持续部署
将 Docker 容器作为云原生应用程序部署到 Kubernetes 面临着挑战,这些挑战可以通过专门的容器中心持续部署系统来解决。与我们之前在部署到单一主机时通过 Jenkins 运行自定义部署逻辑不同,我们可以使用 Spinnaker 来部署到 Kubernetes。因为 Spinnaker 可以与 Jenkins 配合使用,我们可以继续使用已经设置好的 Jenkins 服务器来构建 Docker 容器并准备部署的 Helm Charts。通过 Spinnaker,我们将使用其内置的 Helm Charts 和 Kubernetes 部署支持来部署应用程序。我们还将探讨 Spinnaker 的一些专门部署策略,看看它们如何应用于以 Kubernetes 为中心的环境。
在本章中,我们将学习在什么情况下以及为什么你会选择除了 Jenkins 之外还使用 Spinnaker。我们将学习如何通过配置 Spinnaker 并将其与 GitHub、Docker Hub 和 Jenkins 集成,来改进你的环境以支持 Kubernetes 应用程序的部署和维护。我们将学习如何通过 Spinnaker 管道和 AWS Elastic Container Registry (ECR)将应用程序部署到 Kubernetes,同时也将了解当你将 Spinnaker 与 Kubernetes 结合使用时,Spinnaker 支持不同部署和测试策略的方式是否适用。
本章将覆盖以下主题:
-
改进你的 Kubernetes 应用程序维护环境
-
Spinnaker —— 你何时以及为什么可能需要更复杂的部署
-
在 AWS EKS 集群中使用 Helm 设置 Spinnaker
-
在 Spinnaker 中使用简单的部署策略部署 ShipIt Clicker
-
了解 Spinnaker 在 Kubernetes 应用程序中支持不同部署和测试策略的情况
让我们先回顾一下本章的技术要求,然后继续学习 Spinnaker 平台。
技术要求
你需要在云中拥有一个可用的 Kubernetes 集群,如前一章所述。你可以复用该集群,或者使用相同的方法或eksctl
为本章设置一个新的集群。请注意,本章所描述的 Spinnaker 版本与 Kubernetes 1.16 及以后的版本不兼容;务必在 Kubernetes 1.15 集群上安装此版本。你还需要在本地工作站上安装当前版本的 AWS kubectl
和helm
3.x,正如前一章所述。本章中的 Helm 命令使用的是helm
3.x 语法。AWS 弹性 Kubernetes 服务(EKS)集群必须配置有工作中的应用负载均衡器(ALB)Ingress Controller。我们还将使用前一章中设置的 AWS ECR Docker 仓库。你还需要具备在第七章中设置的 Jenkins 服务器,因为 Spinnaker 依赖 Jenkins 构建软件制品。
Spinnaker 比本地工作站可用的资源要求更多,我们还需要将其连接到外部服务,如 Jenkins 和 GitHub,这种方式可能无法与本地 Kubernetes 学习环境兼容。
查看以下视频,看看代码的实际应用:
使用更新的 ShipIt Clicker v5
我们将使用以下 GitHub 仓库中的chapter9
目录中的 ShipIt Clicker 版本:
github.com/PacktPublishing/Docker-for-Developers/
这个版本与前一个版本有所不同。它只包含一个 Helm Charts 副本,位于chapter9/shipitclicker
,并包含几个用于集群部署的覆盖 YAML 文件:values-eks.yaml
和values-spin.yaml
。
在前一章中,我们保持了多个冗余的模板和配置文件目录,但在 Helm Charts 中的唯一差异是values
文件中的覆盖内容。本章中的示例采用了更简洁的策略。事实证明,你可以使用多个 YAML 配置文件,这些文件只覆盖每个部署或环境中需要更改的设置。在本章中,我们将把示例应用程序的容器仓库从 Docker Hub 迁移到 ECR,先手动部署一次,然后切换到使用 Spinnaker 部署 ShipIt Clicker。
改进 Kubernetes 应用程序维护的设置
为了部署和维护 Spinnaker,我们需要能够从本地工作站与 Kubernetes 集群进行通信。我们还希望能够使用安全套接字层(SSL)保护的通信与 Kubernetes 托管的资源进行交互。让我们一步步进行,以便为你的本地工作站和 AWS 账户准备更多的高级部署。
从本地工作站管理 EKS 集群
为了更轻松地管理 EKS 集群并与其一起工作,您需要设置本地工作站以与集群通信。在上一章中,我们使用 AWS CLI 配置了 AWS IAM 管理员帐户,并使用它设置了 EKS 集群。在本章中,我们将进一步构建,以确保能够从本地工作站高效管理集群及其内部的应用程序。
要在本地工作站上执行以下操作,以使kubectl
和其他 Kubernetes 实用程序能够与您的 EKS 集群通信,请按照这里的说明进行操作:
aws.amazon.com/premiumsupport/knowledge-center/eks-cluster-connection
在前面的链接中,关于执行从本地工作站发出的aws cli
命令的重要部分涉及。执行此命令以更新 .kube/config
,并添加一个条目,使您能够连接到 EKS 集群,但请用您的 EKS 集群名称替换 EKS-VIVLKQ5X
:
aws eks --region us-east-2 update-kubeconfig --name EKS- VIVLKQ5X
然后,测试您是否能够与集群通信:
kubectl get nodes
如果成功,您将看到组成您的 EKS 集群节点的 EC2 主机列表。
故障排除 kubectl 连接失败
如果前面的aws eks
命令产生错误消息或访问被拒绝消息,或者未能完成操作,则需要先进行故障排除。请按照以下部分的步骤进行操作,并查看 AWS 指南以解决此通信失败问题:
aws.amazon.com/premiumsupport/knowledge-center/eks-cluster-connection/
确保您已激活正确的 AWS CLI 配置文件
如果您有多个 AWS CLI 配置文件,则默认用户可能与预期的用户不匹配。您可以通过--profile
参数显式告知 AWS CLI 使用配置文件,或者在执行aws eks
命令之前通过设置AWS_DEFAULT_PROFILE
变量来强制使用特定配置文件:
export AWS_DEFAULT_PROFILE=my-eks-profile
现在我们已经使用配置文件设置了 AWS CLI,我们必须再次检查我们是否可以通过检查 CloudFormation 模板访问控制列表来到达我们的 EKS 集群。
确保您的 CloudFormation 模板已配置为允许访问
在上一章中,当我们设置 EKS 集群时,我们在 192.2.0.15/32
中输入了我们的 IPv4 地址。请通过 whatismyip.com/
仔细检查您的地址。如果这些设置不正确,请使用这些值更新 CloudFormation 堆栈。
CLI 配置文件必须与您用于创建 EKS 集群的 AWS Quick Start 中使用的 IAM 用户匹配。
这将适当地配置 IAM 和 EKS。
在本地和集群上下文之间切换
当您配置了多个 Kubernetes 上下文时,您可以通过以下命令切换它们:kubectl config get-contexts
和 kubectl config use-context
:
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8 arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8 arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8
docker-desktop docker-desktop docker-desktop
$ kubectl config use-context docker-desktop
Switched to context "docker-desktop".
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
docker-desktop Ready master 21d v1.15.5
$ kubectl config use-context arn:aws:eks:us-east-2:143970405955:cluster/EKS-VIVLKQ5X
Switched to context "arn:aws:eks:us-east-2:143970405955:cluster/EKS-VIVLKQ5X".
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-31-183.us-east-2.compute.internal Ready <none> 2d9h v1.15.10-eks-bac369
ip-10-0-57-2.us-east-2.compute.internal Ready <none> 2d9h v1.15.10-eks-bac369
ip-10-0-90-115.us-east-2.compute.internal Ready <none> 2d9h v1.15.10-eks-bac369
在前面的列表中,我们可以看到所有已定义的上下文。我们还可以看到,当我们使用docker-desktop
上下文时,只会看到一个节点,但当我们使用 EKS 上下文时,我们可以看到多个 EC2 服务器节点。在本章的剩余部分,我们将针对 EKS 上下文执行与 Kubernetes 相关的命令。
验证你是否拥有一个工作正常的 ALB Ingress Controller
在上一章中,我们设置了一个带有 ALB Ingress Controller 的 EKS 集群,以便让外界能够访问 ShipIt Clicker 应用。如果你重新使用那个 EKS 集群,并且 ALB Ingress Controller 正常工作,你可以跳到下一节。
如果你设置了一个新集群,你可以按照上一章的说明来使 ALB Ingress Controller 工作,或者你可以运行本章中提供的其中一个 Shell 脚本,作为快捷方式,如果新集群没有 ALB Ingress Controller。
要使用 ALB Ingress Controller 安装脚本,记下你的 EKS 集群名称,并确保已安装 Helm 和eksctl
。
然后,从你的本地工作站运行deploy-alb-ingress-controller.sh
脚本来设置 ALB Ingress Controller(将EKS-8PWG76O8
替换为你的 EKS 集群名称):
chapter9/bin/deploy-alb-ingress-controller.sh EKS-8PWG76O8
现在你已经安装了 ALB Ingress Controller,可以继续在 AWS 中获取一个域名并生成 SSL 证书。
准备 Route 53 域名和证书
为了确保你的 EKS 集群与外部世界之间的通信安全,我们将使用以下服务来管理域名服务器(DNS)条目和服务器证书:
-
AWS Route 53:
aws.amazon.com/route53/
-
AWS 证书管理器(ACM):
aws.amazon.com/certificate-manager/
在第七章《使用 Jenkins 进行持续部署》中,我们配置了 Jenkins,使用域名映射 ShipIt Clicker 的暂存和生产环境条目。在本章中,我们将使用 Route 53 来管理 DNS 条目,并使用 ACM 管理证书,以帮助确保通信的安全。
你可以将你正在使用的顶级域名转移到 Route 53,或者你可以将你控制的现有域名的子域名(例如eks.example.com
)委托给 Route 53。请参阅 AWS 指南,了解如何将子域名委托给 Route 53:
docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingNewSubdomain.html
一旦你将域名委托给 Route 53,验证你是否能够查看该域名的 SOA 记录(将你的域名替换为 eks.example.com):
$ host -t soa eks.example.com eks.example.com has SOA record ns-1372.awsdns-43.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
如果返回的 SOA 记录与前面的日志类似,那么你已经设置好了。如果出现未找到错误,你需要进一步排查问题。
一旦你的域名解析正常,前往 ACM 控制台 https://us-east-2.console.aws.amazon.com/acm/home?region=us-east-2#/ 并生成包含两个域名的新的公共证书 – *.eks.example.com
和 eks.example.com
(将 example.com
替换为你的域名)。以 *
开头的域名称为通配符证书,因为它匹配所有具有相同域名后缀的域名。使用这个证书可以让我们拥有一个覆盖多个域名的证书。
使用 DNS 验证方法。由于你的域名已经在 Route 53 中进行管理,你可以展开该域名并点击快捷按钮在 Route 53 中创建记录,该按钮应该类似于以下内容:
图 9.1 – 在 ACM 中请求证书
这将向你的 Route 53 区域添加验证记录,从而加速证书的颁发。证书可能需要 5 分钟到 1 小时才能颁发,除非 DNS 验证记录有问题,比如域名没有正确地从上一级的名称服务器委派过来。等待证书颁发,并记下证书的 ARN —— 你稍后会用到它。
构建并部署 ShipIt Clicker v5
为了验证我们是否支持 SSL 保护的网站,我们将把 ShipIt Clicker 部署到 EKS 并启用 ALB 负载均衡器对 HTTPS 的支持。为了演示我们可以使用 AWS ECR 容器注册表,我们还将把容器推送到 ECR,并使用该注册表部署应用程序。
将 chapter9/values-eks.yaml
复制到 chapter9/values.yaml
,然后编辑 values.yaml
文件,如下所示。首先更改文件开头的镜像名称,并将其前缀设置为你的 ECR 容器注册表名称(将 143970405955
替换为你的 AWS 账户 ID,并确保区域——在此为 us-east-2
——与你使用的区域匹配):
---
image:
repository: 143970405955.dkr.ecr.us-east-2.amazonaws.com/ dockerfordevelopers/shipitclicker:0.5.0
请注意,values.yaml
文件中有注释,表明 ALB 应该监听 80
和 443
端口,并且它在 host
设置中有一个完全限定的域名。编辑以下主机条目中的值,以便 shipit-v5.eks.example.com
域名与 ACM 中的通配符 SSL 证书匹配的域名一致:
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443},{"HTTP":80}]'
alb.ingress.kubernetes.io/target-type: ip
hosts:
- host: "shipit-v5.eks.example.com"
paths: ['/*']
现在我们已经准备好 values.yml
文件,我们将构建容器并将其推送到 EKS。
切换到 Docker-for-Developers/chapter9
目录并执行以下命令来构建并部署 ShipIt Clicker 到集群中,以测试 ALB 集成(将 143970405955.dkr.ecr.us-east-2.amazonaws.com
替换为你的 ECR 注册表):
docker build . -t dockerfordevelopers/shipitclicker:0.5.0
docker tag dockerfordevelopers/shipitclicker:0.5.0 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:0.5.0
aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 143970405955.dkr.ecr.us-east-2.amazonaws.com
docker push 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:0.5.0
helm install shipit-v5 -f values.yaml ./shipitclicker
几分钟后,你应该能够验证 Ingress Controller 是否正常工作:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
shipit-v5-shipitclicker shipit-v5.eks.shipitclicker.com 9bbd6f9c-default-shipitv5s-051a-795288134.us-east-2.elb.amazonaws.com 80 90m
如果没有显示,请检查 Ingress Controller 日志,以下是一些排错线索:
kubectl logs -n kube-system deployment.apps/alb-ingress-controller
接下来,我们需要创建一个 DNS 地址映射记录,也就是前面kubectl get ingress
输出中的HOSTS
列。进入 AWS Route 53 控制台,选择你的域名,创建一个类型为 A 的新记录,指向shipit-v5.eks
。将此记录设置为别名记录,并输入kubectl get ingress
输出中 ALB 的HOSTS
列中的 DNS 名称。执行此操作的表单应如下图所示:
图 9.2 – 在 AWS Route 53 中创建作为别名的 A 记录
按下example.com
(使用你自己的域名)来验证你是否可以通过 HTTPS 访问它。
现在,你已经确保可以从本地环境管理 EKS 集群,将演示应用程序的容器推送到 ECR,使用 Helm 将演示应用程序部署到 Kubernetes,并配置 HTTPS 支持以安全地访问一个托管在 EKS 中的服务,你可以继续进行 Spinnaker 的安装。
Spinnaker – 你可能需要更复杂部署的时机和原因
为了可靠地部署你的应用程序,你可以手动编写许多脚本并使用持续集成系统。然而,许多人已经考虑到在 Kubernetes 中部署应用程序所固有的问题。Kubernetes 确实具有强大的部署能力,尤其是当你使用部署控制器时。但这种方法并不适合所有人。一些人开发了专门的系统来减少处理这些任务的复杂性。像 Jenkins-X、Weaveworks、CodeFresh 和 Spinnaker 这样的系统适合这一需求。我们将更详细地研究 Spinnaker,这是一个持续部署工具集(www.spinnaker.io/
)。
我们将首先介绍 Spinnaker 的核心概念,并强调它与其他平台(如 Kubernetes)共享的术语,特别是它们的含义不同之处。
Spinnaker 简介
Spinnaker 是一个持续交付(CD)平台,跨多个云提供商工作,并且是开源的。Netflix 最初编写 Spinnaker 是为了帮助管理他们的多云部署,采用不可变服务器模式(请参见martinfowler.com/bliki/ImmutableServer.html
)。Spinnaker 具有一个镜像工厂功能,涉及将应用程序代码与操作系统镜像及支持库结合,然后保存(烘焙)成一个不可变的机器镜像,例如 AWS 的Amazon 机器镜像(AMI)或 VMware 的虚拟机磁盘(VMDK)镜像,以加速部署并最小化运行时配置。可以阅读更多关于镜像工厂及其在 Spinnaker 中的使用的相关文章:
这种模式在大规模应用中表现良好,但 Docker 和以容器为中心的运行时(如 Kubernetes)的出现,提供了一种不同的方法来实现相同的目标。
Spinnaker 已被调整为支持 Kubernetes 和 Docker,并且支持其原始的部署策略——使用镜像工厂和不可变服务器模式。你可以在官方 GitHub 仓库中找到该平台的源代码及其他项目:
在安装应用程序之前,我们应当熟悉一些该技术的核心概念。我们首先要了解的概念是应用程序管理。
应用程序管理
我们可以使用管理功能来管理和查看我们的云资源。通过 Spinnaker,我们围绕服务器组和集群等概念来建模我们的应用程序。有关这些概念的完整概述,请参阅 Spinnaker 文档:
应用程序是顶级容器,可以部署在 Spinnaker 维护的基础设施上,包括集群和服务器组。每个集群作为组织服务器组的机制。Spinnaker 将在 Kubernetes 中以 Pod 形式运行的 Docker 容器视为服务器组的成员。这些 Docker 镜像可能包含诸如 ShipIt Clicker 之类的服务以及任何相关工具,如在第十五章中介绍的 Datadog 监控代理,扫描、监控和使用第三方工具。
现在我们已经理解了容器化项目在 Spinnaker 中的表现形式,我们应考虑如何通过该框架将其部署到我们 AWS 中的 EKS 集群。
应用程序部署
应用程序部署这一环节在 Spinnaker 用户界面中通过管道图形化呈现。管道可以手动启动,也可以作为其他事件触发的一部分自动启动,比如源代码控制系统的推送。管道会告诉我们完成过程中需要执行的所有步骤(称为阶段)——例如,如何获取 Docker 容器,安装它,并在我们的云环境中对其进行后续更新。
以下截图展示了部署管道及其各个阶段的样子:
图 9.3 – Spinnaker 管道
这个管道中的每个阶段可以视为一个独立的任务。每个任务按照顺序或并行执行,具体取决于管道是否分叉。正如我们接下来会看到的,Spinnaker 提供了多个预定义的阶段,我们可以将它们融入到自定义管道中。
将管道与构建服务器和源代码控制库绑定是有优势的,这样,当你将更改推送到应用程序及其 Helm Charts 时,Spinnaker 可以适当地打包、测试和部署它们。
现在,我们简要介绍了 Spinnaker 的两个主要概念,接下来让我们着手构建一些基础设施和管道,以便更好地理解各个阶段是如何工作的,以及可能的部署策略类型。
使用 Helm 在 AWS EKS 集群中设置 Spinnaker
设置一个生产级的 Spinnaker 集群需要一些精心的规划,但为了学习的目的,我们将使用其中一种简化的方法。完整的 Spinnaker 设置指南可以在 www.spinnaker.io/setup/
找到。
为了演示使用 Spinnaker 的概念验证,我们将使用以下链接中的 Helm Chart 来部署 Spinnaker:
github.com/helm/charts/tree/master/stable/spinnaker
Spinnaker Helm Chart 警告不适合用于生产环境
尽管这个 Helm Chart 声明它不适合用于生产环境,但我们可以使用它来演示构建、测试和部署应用程序的概念验证。Spinnaker 设置指南提供了设置生产级 Spinnaker 系统的指导。最重要的是,这包括将 Spinnaker 安装与托管最终用户消费应用程序的集群分开。为了节省时间和金钱,也为了便于演示,我们将在本章忽略这一建议。如果你打算大规模采用 Spinnaker,请务必认真采纳这一建议,并根据他们的最佳实践文档,在单独的集群中设置 Spinnaker。
确保你连接到正确的 Kubernetes 上下文,目标是你的 EKS 集群,并输入以下命令将 Spinnaker 部署到它自己的命名空间中:
$ kubectl create namespace spinnaker
$ helm install spinnaker stable/spinnaker --namespace spinnaker --version 1.23.3 --timeout 600s
Spinnaker 部署可能需要几分钟才能完成。当它完成时,你应该看到类似以下内容的输出:
图 9.4 – Spinnaker Helm Chart 安装
接下来,我们将连接到刚刚安装的 Spinnaker 系统。
通过 kubectl proxy 连接到 Spinnaker
为了进行初步测试,请注意你从运行 helm install
命令(在上一节中创建端口转发隧道)中收到的输出中的建议。它应该类似于前一节中显示的输出。你应该在本地工作站上设置两个单独的控制台窗口或标签页,然后运行 helm install spinnaker
命令输出中 NOTES
部分列出的命令对,以设置端口转发隧道,每个控制台窗口或标签页执行一对命令。然后,你可以在浏览器中访问 127.0.0.1:9000
来验证 Spinnaker 是否已启动并运行。
通过 ALB Ingress 控制器公开 Spinnaker
将 Spinnaker 与 EKS 集成的说明(www.spinnaker.io/setup/install/providers/kubernetes-v2/aws-eks/
)描述了使用具有 LoadBalancer 注释的服务来公开服务的解决方案。然而,由于我们已经配置了 ALB Ingress 控制器、Route 53 和 ACM,因此最好通过 ALB Ingress 控制器来公开它们。编辑 chapter9/spinnaker-alb-ingress.yaml
文件,并对 spin-deck
和 spin-gate
的 ingress 配置做以下更改(文件中有两组配置):
-
将 eks.example.com 替换为你已使用 ACM 通配符证书配置的域名。
-
将
192.2.0.10/32
替换为你的公共 IP 地址(CIDR 格式)(与你用于锁定 EKS API 的格式相同)。 -
将
192.2.0.200/32
替换为你的 Jenkins 服务器的公共 IP 地址。安全通知
添加前述的 IP 地址限制非常重要,因为默认情况下,Spinnaker 的用户界面以集群管理员身份运行。如果你允许
0.0.0.0/0
(整个互联网)访问,那么有人可能会以集群管理员身份运行进程,并修改或接管你的集群。如果你有动态 IP 地址,你可能需要多次更改,首先从 CloudFormation 模板开始。
然后,应用配置模板以创建 ALB Ingress 控制器:
kubectl apply -n spinnaker -f spinnaker-alb-ingress.yaml
几秒钟后,执行以下命令以验证是否成功(请查找你的域名,而不是 eks.example.com):
$ kubectl get -n spinnaker ingress
NAME HOSTS ADDRESS PORTS AGE
spin-deck spinnaker.eks.example.com 9bbd6f9c-spinnaker-spindec-5f03-917097792.us-east-2.elb.amazonaws.com 80 10m
spin-gate spinnaker-gate.eks.example.com 9bbd6f9c-spinnaker-spingat-712f-2021704484.us-east-2.elb.amazonaws.com 80 10m
该列表中 HOSTS
列下列出的 DNS 名称是我们打算用来调用服务的名称。ADDRESS
列下的 DNS 地址是 ALB Ingress 控制器通过 AWS ALB 创建的实际 DNS 名称。为了将这两个名称连接起来,我们需要在我们的域中创建两个 DNS 记录,以便使用更友好的名称访问 Spinnaker 服务。请注意从该列表中 ADDRESS
列下的 Ingress 控制器的 DNS 名称。然后,进入 AWS Route 53 控制台,为你的域创建两个新的 A 类型 DNS 记录,并将它们设置为别名记录。
将第一个命名为 spinnaker
,并将 spin-deck
条目中 ADDRESS
列显示的值作为其值。
将第二个条目的名称命名为 spinnaker-gate
,并将 spin-gate
条目中 ADDRESS
列显示的值作为其值。
这样做的结果将是两个新的 DNS 记录,类似于以下内容(将你的域名替换为 example.com):
-
spinnaker.eks.example.com
-
spinnaker-gate.eks.example.com
在等待大约 5 分钟,直到 DNS 记录可用并且 ALB 完全激活时,使用 Halyard 配置 Spinnaker,并使用这些 URL 的 HTTPS 版本。
使用 Halyard 配置 Spinnaker
现在我们已经为 Spinnaker 安装分配了友好的 DNS 名称,我们需要配置 Spinnaker 使其理解必须遵守这些名称。从你的本地工作站,连接到 Halyard 维护 pod:
kubectl exec --namespace spinnaker -it spinnaker-spinnaker-halyard-0 bash
一旦你连接到 pod,你将看到 spinnaker@spinnaker-spinnaker-halyard-0:/workdir$
提示符。然后,输入这些命令,将 example.com
替换为你的域名:
$ hal config security api edit --override-base-url https://spinnaker-gate.eks.example.com --cors-access-pattern https://spinnaker.eks.example.com
$ hal config security ui edit --override-base-url https://spinnaker.eks.example.com
$ hal deploy apply
最后的 hal
命令将重新部署 Spinnaker 应用程序。
等待 5 分钟,以便 DNS 记录激活并且 ALB 完全创建。一旦完成,使用其完全合格的域名访问 Spinnaker 网站,将 example.com 替换为你的域名:
你应该被重定向到网站的 HTTPS 版本。
将 Spinnaker 连接到 Jenkins
为了让 Spinnaker 接收来自 Jenkins 的工件,我们必须通过 Jenkins 管理员 API 令牌将其连接。Spinnaker 提供了相关说明,可以在 www.spinnaker.io/setup/ci/jenkins/
上找到。
进入你在前一章节中使用的 Jenkins 服务器。登录后,转到类似于 jenkins.example.com/user/admin/configure
的用户配置页面(将 jenkins.example.com 替换为你的 Jenkins URL)。然后,生成 Spinnaker 的 API 令牌:
图 9.5 – Jenkins API 令牌生成
如 使用 Halyard 配置 Spinnaker 部分所示,从你的本地工作站连接到 hal
维护 pod:
kubectl exec --namespace spinnaker -it spinnaker-spinnaker-halyard-0 bash
然后,在该 pod 的终端中执行这些命令来配置 Jenkins,将 BASEURL
、APIKEY
和 USERNAME
右侧的值替换为你安装的相应值:
$ hal config ci jenkins enable
$ BASEURL=https://jenkins.example.com
$ APIKEY=123456789012345678901234567890
$ USERNAME=admin
$ echo $APIKEY | hal config ci jenkins \
master add my-jenkins-master \ --address $BASEURL --username $USERNAME --password
$ hal deploy apply
现在 Spinnaker 已经配置为与 Jenkins 通信,我们将继续为 Jenkins 配置一组 Spinnaker 将使用的额外构建任务。
设置 Jenkins 与 Spinnaker 和 ECR 的集成
为了运行 Spinnaker 特定的任务并将 Jenkins 与 ECR 集成,我们需要为 Jenkins 配置额外的插件和凭据,以便它能够将容器推送到 AWS ECR,并且还需要设置一个新的多分支流水线项目,以便使用存储在 GitHub 仓库中的此章节的 chapter9/Jenkinsfile
。
在接下来的章节中,我们将进行所有必要的更改,使 Jenkins 能够同时与 ECR 和 Spinnaker 配合使用。
安装 AWS ECR Jenkins 插件
以管理员身份登录到你的 Jenkins 服务器,然后在左侧菜单中进入 ECR
,并在 Filter 框中查看类似这样的内容:
图 9.6 – 通过 Jenkins 插件管理器安装 Amazon ECR 插件
点击 安装 复选框,选择 Amazon ECR 插件旁的 立即下载并在重启后安装 按钮。您将看到如下截图:
图 9.7 – 正在安装 Amazon ECR Jenkins 插件
Jenkins 可能需要 5 到 15 分钟的时间才能重新启动并再次可用。一旦它可用,再次以 Jenkins 管理员用户身份登录。接下来,我们将创建一个具有有限权限的 AWS IAM 用户,并使用这些凭证配置 Jenkins。
为 Jenkins 创建一个受限的 AWS IAM 用户
在上一章中,我们使用 AWS 控制台为账户创建了一个管理员 IAM 用户。这一次,我们将使用 AWS CLI 来创建一个 Jenkins 用户,赋予其比管理员用户更有限的权限,以便它仅能管理 ECR 仓库并将 Docker 镜像推送到这些仓库中。这符合最小权限原则,系统应仅授予所需的最小权限。要创建该用户,请附加适当的策略,创建访问密钥,并执行以下列出的三个 aws iam
命令来设置 Jenkins 用户(您应预期看到的输出与这些命令一致):
$ aws iam create-user --user-name Jenkins
{
"User": {
"Path": "/",
"UserName": "Jenkins",
"UserId": "AIDASDBKOBZBU6ZX6SQ7U",
"Arn": "arn:aws:iam::143970405955:user/Jenkins",
"CreateDate": "2020-05-03T02:45:34Z"
}
}
$ aws iam attach-user-policy --user-name Jenkins --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
$ aws iam create-access-key --user-name Jenkins
{
"AccessKey": {
"UserName": "Jenkins",
"AccessKeyId": "AKIASDBKOBZBYFDCBLMR",
"Status": "Active",
"SecretAccessKey": "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",
"CreateDate": "2020-05-03T02:46:00Z"
}
}
请注意您命令输出中与 AccessKeyId
和 SecretAccessKey
相关的值。您需要这些值来在下一节配置 Jenkins 的 AWS 访问凭证。接下来,让我们使用 AWS 凭证来配置 Jenkins。
使用 AWS 和 ECR 凭证配置 Jenkins
我们需要告诉 Jenkins 我们的 AWS 凭证,以便它可以将构建的 Docker 容器推送到 ECR。此外,我们还需要配置 Jenkins 以知道使用哪个 ECR 注册表。在 第六章,使用 Docker Compose 部署应用程序 中,我们为 Jenkins 配置了 GitHub 和 Docker Hub 的凭证。现在,我们将为 AWS IAM 用户和 ECR 容器注册表配置额外的凭证。
在使用管理员用户登录 Jenkins 服务器时,进入首页,在左侧菜单中找到 shipit.aws.key
ID,ShipIt Clicker AWS API Keys
描述,以及上一节中的访问密钥 ID 和密钥访问密钥。您应该会看到如下的凭证表单:
图 9.8 – 配置 Jenkins 中的 AWS 凭证
完成后,在最后添加一个额外的凭证,引用 dockerfordevelopers/shipitclicker:0.5.0
:
-
作用域:全局
-
143970405955.dkr.ecr.us-east-2.amazonaws.com
-
shipit.ecr.container.id
-
ShipIt Clicker ECR 容器 ID
按下 确定 按钮保存此凭证。
现在我们已经为 Jenkins 配置了连接 AWS 和 ECR 所需的凭证,接下来让我们为本章的代码配置一个新的多分支流水线。
配置 Jenkins 使用多分支流水线进行 Jenkinsfile 配置
接下来,我们将配置 Jenkins 使用一个额外的多分支管道项目,该项目从同一个 GitHub 仓库拉取代码,但配置为使用 chapter9/Jenkinsfile
,而不是仓库根目录下的 Jenkinsfile。登录 Jenkins,并从主页导航到 Spinnaker
,然后使用你的 GitHub 仓库凭证进行配置,类似于以下截图所示(将 PacktPublishing/Docker-for-Developers
替换为你在 第七章 中设置的 GitHub 组织和仓库名称,使用 Jenkins 进行持续部署):
图 9.9 – Jenkins 多分支管道设置
配置完成后,新的项目应能连接到 GitHub 仓库,并将容器构建并推送到 AWS ECR。检查该新项目中的主分支的控制台输出,确保构建成功,并且 Docker 镜像已推送到 AWS ECR 仓库。
现在你已经使用 ECR 插件配置了 Jenkins,创建了一个 Jenkins IAM 用户,配置了该用户的凭证,并且配置了新的凭证以反映 AWS 集成,同时还添加了新的 Jenkins 多分支设置,那么你可以继续将其他服务连接到 Spinnaker。接下来,我们将连接 GitHub。
将 Spinnaker 连接到 GitHub
我们将按照 www.spinnaker.io/setup/artifacts/github/
上的指导,将 Spinnaker 连接到 Jenkins,以便它能够从 GitHub 读取工件。前往你的 GitHub 用户账户,在 Developer Settings 中生成一个带有 repo 范围的访问 token,供 Spinnaker 使用。
从你的本地工作站,连接到 Halyard 维护 pod,如 使用 Halyard 配置 Spinnaker 部分所示,将 GitHub token 放入主目录下的一个文件中,然后执行以下命令(将 xxxx
替换为你的 GitHub token,将 my-github-user
替换为你的 GitHub 用户名):
TOKEN=xxxx
GH_ACCOUNT=my-github-user
TOKEN_FILE=~/.github-token.txt
echo "$TOKEN" > $TOKEN_FILE
hal config artifact github enable
hal config artifact github account add $GH_ACCOUNT --token-file $TOKEN_FILE
hal deploy apply
完成这些操作后,Spinnaker 应该能够与 GitHub 通信。接下来,我们将把 Spinnaker 连接到 Docker Hub。
将 Spinnaker 连接到 Docker Hub
你还需要将 Spinnaker 连接到 Docker Hub,以便它可以读取你的仓库和 library/redis
仓库。将 Spinnaker 集成到 Docker Hub 需要你将所有模板使用的仓库列入白名单。默认的 Docker Hub 集成有一个短小的白名单,包含最常用的库。
我们将按照 www.spinnaker.io/setup/install/providers/docker-registry/
上的指导,将 Docker Hub 添加到 Spinnaker。
登录到你的 Docker Hub 账户,并从 hub.docker.com/settings/security
为 Spinnaker 安装生成一个新的 API token。
从你的本地工作站,连接到 Halyard 维护 pod:
kubectl exec --namespace spinnaker -it spinnaker-spinnaker-halyard-0 bash
然后,执行以下命令(将 xxxx
替换为您的 Docker Hub 令牌,将 my-dockerhub-user
替换为您的 Docker Hub 用户名):
$ ADDRESS=index.docker.io
$ REPOSITORIES="library/redis dockerhub-user/shipitclicker"
$ USERNAME=dockerhub-user
$ PASSWORD=xxxx
$ REPOSITORIES="library/redis dockerhub-user/shipitclicker"
$ echo $PASSWORD | hal config provider docker-registry \
account add my-docker-registry \
--address $ADDRESS \
--repositories $REPOSITORIES \
--username $USERNAME \
--password
$ hal deploy apply
一旦连接了 Docker Hub,您就可以开始在 Spinnaker 中设置应用程序和流水线。但在此之前,让我们谈谈如何排除 Spinnaker 问题。
排除 Spinnaker 问题
如果您在使 Spinnaker 流水线执行工作时遇到任何困难,或者在设置和配置 Spinnaker 方面有其他问题,用户界面具有最少的错误报告能力。它可能看起来不透明且令人生畏。
例如,假设您在其中一个 artifact 定义中有拼写错误 – 例如,gitgub.com 而不是 github.com。由于主机名查找失败,当流水线尝试检索该 artifact 时可能会导致流水线失败。
而不是尝试找出 Spinnaker pods 中可能记录了错误的哪一个,您可以同时查看所有 Spinnaker pods 的所有日志:
kubectl logs -n spinnaker -f -l app=spin --all-containers --max-log-requests 10
如果您在控制台输出中搜索 exception
这个词,您可能会找到一些线索,比如在排除 Spinnaker 问题时找到的这个:
com.netflix.spinnaker.clouddriver.artifacts.exceptions.FailedDownloadException: Unable to determine the download URL of artifact Artifact(type=github/file, customKind=false, name=chapter9/helm.tar.gz, version=staging, location=null, reference=https://api.gitgub.com/repos/PacktPublishing/Docker-for-Developers/contents/chapter9/helm.tar.gz, metadata={id=8ebb0ad7-2d14-4882-9b77-fde3a03e3c45}, artifactAccount=obscurerichard, provenance=null, uuid=null): api.gitgub.com: Try again
分析类似这样的日志文件确实可以帮助您摆脱困境。接下来,我们将使用 Spinnaker 部署 ShipIt Clicker。
使用简单的部署策略在 Spinnaker 中部署 ShipIt Clicker
让我们通过部署我们的 ShipIt Clicker 应用程序来深入了解 Spinnaker。为此,我们将使用 Helm Charts,并使用 chapter9
目录中的应用程序版本。
Spinnaker 需要 Helm 存档文件来运行
为了简化 Helm Charts 的部署,我们已经在 chapter9/helm.tar.gz
中创建了 chapter9/shipitclicker
Helm Chart 目录的归档文件,因为 Spinnaker 期望将此格式的归档文件作为其输入之一。我们也可以将此归档文件输出到 AWS S3 对象,甚至作为 GitHub 发布 artifact,但这超出了本章的范围。如果您更改 chapter9/shipitclicker
目录中的 Helm Charts,请务必更新 helm.tar.gz
归档文件并在使用 Spinnaker 构建之前进行提交和推送。
添加一个 Spinnaker 应用程序
在网页浏览器中转到您的 Spinnaker 安装地址 spinnaker.eks.example.com
(将 example.com 替换为您的域名)。添加一个名为 shipandspin
的应用程序,然后,在 Docker-for-Developers
代码中:
图 9.10 – Spinnaker 中的新应用对话框
当您提交此表单时,它将带您进入一个基础设施定义表单。在此停止,并不要填写或提交基础设施定义表单。此表单适用于其他类型的 Spinnaker 部署,而不是专注于 Kubernetes 的部署。当您部署应用程序时,它将定义 Spinnaker 在 Kubernetes 中理解的基础设施。
添加一个 Spinnaker 流水线
转到 流水线 屏幕:
图 9.11 – Spinnaker 中的 PIPELINES 屏幕示例
创建一个名为shipit-eks-staging
的管道,然后添加两个工件——一个是 Helm Chart,另一个是values-spin.yaml
覆盖。
对于第一个,选择 GitHub 帐户,给它chapter9/helm.tar.gz
Helm 工件,并点击使用默认工件。然后,提供该工件的完整 API URL,并将其更改为匹配你的帐户和仓库名称(提交前请仔细检查是否正确):
api.github.com/repos/PacktPublishing/Docker-for-Developers/contents/chapter9/helm.tar.gz
告诉它使用staging
分支。定义后,它应该看起来像这样:
图 9.12 – Spinnaker 中覆盖工件:Helm Chart 归档
为chapter9/values-spin.yaml
覆盖文件添加另一个工件。设置chapter9/values-spin.yaml
文件路径和values-spin.yaml
显示名称,选择staging
分支(将PacktPublishing/Docker-for-Developers
替换为你在第七章中设置的 GitHub 组织和分叉仓库的名称,使用 Jenkins 进行持续部署):
图 9.13 – Spinnaker 中覆盖工件:Helm Chart 归档
然后,配置build.properties
文件用于属性文件,这是一个 Jenkins 归档文件,Spinnaker 将使用它来获取 Jenkins 构建的容器版本:
图 9.14 – Spinnaker 中 Jenkins 自动触发器屏幕
前往表单底部并保存配置阶段。
现在,让我们添加下一个阶段,创建来自 Helm Charts 的 Kubernetes 清单。
添加 Bake(Manifest)阶段
保存配置阶段后,你仍然会停留在非常长的阶段定义网页表单底部。返回表单顶部,添加一个名为shipit-staging
的额外阶段,并告诉它部署到默认命名空间。为它设置一个模板工件,值为helm.tar.gz。
对于image.repository
名称和${trigger["properties"]["imageName"]}
值,添加一个覆盖键值对,键为ingress.hosts[0].host
,值为 shipit-stage.eks.example.com,将 example.com 替换为你的域名。
我们将在此部署的 Ingress Controller 上设置一个 Route 53 DNS 条目。表单应该类似于以下内容:
图 9.15 – Spinnaker 中 Bake(Manifest)模板渲染器配置屏幕
然后,在表单底部,编辑kube-templates.yaml
并保存表单。它应该看起来像这样:
图 9.16 – Spinnaker 中的 Bake(Manifest)生成工件部分
配置此阶段将设置 Helm 模板渲染过程。然后保存表单。接下来,我们将设置 Deploy (Manifest) 阶段。
添加部署(Manifest)阶段
在保存了上述配置更改之后,再次返回配置表单顶部,添加另一个阶段,kube-templates.yaml
用于 Manifest Artifact 进行部署。不要选择 Rollout Strategy Options 设置,因为这仅适用于只有一个 ReplicaSet 并放弃使用 Deployments 作为 Kubernetes 控制器的情况。它会类似于这样:
图 9.17 – Spinnaker 中部署(Manifest)配置
现在,我们准备触发部署。在屏幕顶部单击 PIPELINES,然后单击 Start Manual Execution 链接。它应该会从 GitHub 获取最新构建,然后使用 Helm Charts 烘焙清单并部署。
因为我们使用 Jenkins 发布了一个 build.properties
文件,并在模板中使用了 image.repository
字段,所以我们将使用 Jenkins 作业连接的触发器构建的特定容器。有关 SPEL 表达式和 Spinnaker 流水线的更多信息,请参考以下链接:
www.spinnaker.io/guides/user/pipeline/expressions/
如果您在某些必需配置中有拼写错误,可能需要解决一些问题。如果一切顺利,它应该看起来类似于这样:
图 9.18 – Spinnaker 中显示已完成作业的流水线
您可以查看 Execution Details 和 INFRASTRUCTURE 面板,因为 Spinnaker 将显示有关正在运行的应用程序的一些信息。它甚至可以显示您运行的 Pod 的日志。
为 Ingress 控制器设置 DNS 记录条目
要从外部查看正在运行的应用程序,您需要设置 DNS 记录。发出 kubectl get ingress
命令以确定 shipit-eks-staging
的 Ingress 控制器的 DNS 别名,然后在 Route 53 中设置 DNS 别名,以匹配您为 shipit-stage.eks.example.com 设置的覆盖(将 example.com 替换为您的域名)。
完成后,您应该能够访问 shipit-stage.eks.example.com/
(将 example.com 替换为您的域名),并查看运行中的 ShipIt Clicker 游戏。
接下来,我们将了解 Spinnaker 对不同类型部署的支持及其在 Kubernetes 部署中的应用(或不适用)。
浏览 Spinnaker 的部署和测试功能
在本章早些时候关于 Spinnaker 的介绍中,我们指出您将有机会了解可用的各种部署方法。现在让我们深入探讨这些概念,包括金丝雀和红/黑部署,并描述它们在使用 Spinnaker 管理 Kubernetes 部署时的相关性。
金丝雀部署
金丝雀部署是一种将应用程序暴露给用户的方法,你可以在新版本的部署中运行一部分流量,同时大部分流量仍然流向当前部署的版本。这可以帮助你测试新版本是否适合生产环境,而无需立即将所有流量都导向新版本。
Kubernetes v2 Spinnaker 提供程序不支持金丝雀部署。
尽管这是 Spinnaker 最受欢迎的特性之一,但 Kubernetes v2 云提供程序不支持金丝雀部署,因此我们不会在 ShipIt Clicker 中使用它。如果我们使用的是非 Kubernetes 云提供程序,例如 AWS、Google Compute Engine 或 Azure 提供程序,那么这将是一个更自然的模式。有关 Spinnaker 云提供程序的完整列表,请参阅 spinnaker.io/setup/install/providers/
。
红/黑部署
现在我们来看看红/黑部署方法是如何工作的。这是另一种为人熟知的蓝/绿部署策略的名称。在红/黑策略中,部署期间会保持两组服务器或容器可用,流量每次只会流向其中一个。假设在部署开始时,红组接收流量。在部署过程中,你将部署到黑组。一旦健康检查通过,你就会将流量切换到黑组,但仍保留红组,以便如果出现问题,你可以将流量切换回红组,而无需重新部署。
Spinnaker 于 2019 年宣布通过 Kubernetes v2 提供程序支持红/黑部署:
blog.spinnaker.io/introducing-rollout-strategies-in-the-kubernetes-v2-provider-8bbffea109a
然而,这有一些显著的限制。它意味着你不能使用 Kubernetes 部署对象,而必须改用低级别的 ReplicaSet 注释。Helm Chart 生成器会生成一个带有部署的框架,它是建立在 ReplicaSets 之上的,因此如果你希望使用 Spinnaker 的红/黑支持与 Kubernetes 配合使用,你将需要对 Helm Charts 进行大幅度的修改。请参考关于 Kubernetes v2 提供程序的建议:
https://www.spinnaker.io/guides/user/kubernetes-v2/traffic-management/#you-must-use-replica-sets
Spinnaker 确实支持的 Kubernetes 部署(仅使用 ReplicaSets)部署策略如下:
-
暗部署:部署到一个新的 ReplicaSet,该 ReplicaSet 不连接到实时负载均衡器。
-
红/黑:部署一个新的 ReplicaSet,并使用 Spinnaker 在新旧两组之间来回切换。
-
Highlander:部署一个新的 ReplicaSet,并在新的 ReplicaSet 开始接收流量时销毁旧的 ReplicaSet(只能有一个 ReplicaSet)。
如果你使用的是 Kubernetes 部署控制器,那么你将获得类似于 Spinnaker Highlander 策略的行为。因此,如果你使用 Kubernetes,可能不需要使用 Spinnaker 支持的高级部署策略。
回滚
那么,如果部署失败会发生什么呢?我们将需要以安全的方式回滚到之前的版本。对于 Spinnaker 管理机器镜像部署的方式,它会协调此回滚。然而,对于 Kubernetes 操作员,它依赖于 Kubernetes 部署机制,使用存活和就绪探针来检查部署是否有效。
Spinnaker 确实支持通过其界面直接撤销一组模板的发布。然而,如果模板中的所有资源没有独立的版本修订(例如,单独版本和标记的 Docker 容器),这可能不起作用。有关 Spinnaker 和 Kubernetes 回滚的更多信息,请参见此处:
www.spinnaker.io/guides/user/kubernetes-v2/automated-rollbacks/
使用 Spinnaker 进行测试
使用 Spinnaker 时,你可以使用手动判断阶段为人员提供时间对应用程序进行手动测试,或者使用脚本化流水线阶段在 Jenkins 中运行自动化测试套件以测试你的应用程序。如果你在多个环境中部署或使用红/黑策略,这能给你更好的机会在将应用程序部署到生产环境或公开给世界之前执行测试。
你可以在各自的 Spinnaker 文档中找到更多关于使用这两种策略进行测试的信息,文档地址为 www.spinnaker.io/guides/tutorials/codelabs/safe-deployments/
和 www.spinnaker.io/setup/features/script-stage/
。
概述
在本章中,我们探讨了如何使用 Spinnaker 框架在 AWS 中进行持续部署的主题。我们首先配置了 Spinnaker,使其与 Jenkins、GitHub、AWS ECR 和 Docker Hub 配合使用。然后,我们使用它将 ShipIt Clicker 应用程序部署到 EKS 上的 Kubernetes,同时为 Spinnaker 和 ShipIt Clicker 应用程序配置了 SSL 安全。
在此之后,我们了解了 Spinnaker 提供的一些高级部署策略,以及在配置基于 Kubernetes 的 Docker 应用程序以利用这些策略时可能需要做出的一些权衡。我们还了解了如何通过 Spinnaker 触发测试的执行(手动或自动)。通过实践中学到的这些章节中的经验,你可以构建使用简单 Jenkins 构建任务和 Spinnaker 流水线将 Docker 应用程序部署到 Kubernetes 的持续部署系统。你所掌握的关于将 Spinnaker 与 Kubernetes 集成的技能也适用于将其他软件包与 Kubernetes 集成。
在下一章中,我们将探讨如何使用 Prometheus、Grafana 和 Jaeger 监控我们的 Docker 容器。
进一步阅读
使用以下资源扩展你对 Spinnaker 和 EKS 的知识:
-
Spinnaker 不是一个构建服务器,以及其他误解:
www.armory.io/blog/spinnaker-is-not-a-build-server-and-other-misconceptions/
-
AWS 的一篇博客文章,描述了如何使用 Jenkins 和 ECR 完全安装 Kubernetes 和 Spinnaker 的部署流水线:
aws.amazon.com/blogs/opensource/deployment-pipeline-spinnaker-kubernetes/
-
一篇关于如何将 Kubernetes 服务暴露给世界的好文章:
medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
-
AWS 官方文档关于 ALB Ingress 控制器:
docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
-
Spinnaker CLI:
www.spinnaker.io/guides/spin/
-
一个 Kubernetes 外部 DNS 提供者,你可以使用它来注释你的模板,以避免手动设置 DNS 别名:
github.com/kubernetes-sigs/external-dns
Spinnaker 不是你应该了解的唯一先进的 Kubernetes 感知 CD 系统;你还应考虑其他替代方案,并在这个领域进行新的研究,因为这个领域变化迅速:
-
Jenkins-X – 一个以 Kubernetes 为中心的 CI/CD 系统的权威性意见:
jenkins-x.io/
-
Argo Project – 工作流、CD 等。作为 2020 年 7 月孵化阶段的 CNCF 项目:
argoproj.github.io/
-
WeaveWorks – 一个使用 Kubernetes 进行 CD 的 GitOps 系统:
www.weave.works/technologies/ci-cd-for-kubernetes/
第十章:使用 Prometheus、Grafana 和 Jaeger 监控 Docker
为了了解应用程序在生产环境中运行时的行为,开发人员和系统运维人员依赖于日志记录、监控和警报系统。这些系统既能洞察应用程序及其环境是否正常运行,又能在故障排除时提供线索。随着系统变得越来越复杂,对应用程序及其支持软件的深入洞察需求也在增加。那些能够在不修改运行系统代码的情况下深入检查所有这些问题的系统,可以被认为具有良好的可观察性特征。
本章将教你如何为你的应用程序及其运行时环境添加监控,以提高整个系统的可观察性。你将学习日志记录、监控和警报的许多方面。具体来说,你将学习如何查看、查询和存储来自 Kubernetes 集群的日志,既可以在集群内查看,也可以存储到 CloudWatch 和 Amazon 简单存储服务(S3)。你将学习如何实现特定于云原生应用程序需求的存活性和就绪性探针,在出现问题时获取警报,并使用 Prometheus 捕获应用程序度量。你还将学习如何使用 Grafana 可视化性能和可用性指标。最后,我们将深入探讨通过 Jaeger 获取应用程序特定的代码和数据库层级的度量。
本章将涵盖以下主题:
-
Docker 日志记录和容器运行时日志记录
-
在 Kubernetes 中使用存活性和就绪性探针
-
使用 Prometheus 收集度量并发送警报
-
使用 Grafana 可视化操作数据
-
使用 Jaeger 进行应用程序性能监控
接下来,让我们确保你准备好测试这些系统,并学习如何将它们协同使用,以实现系统的可观察性。
技术要求
本章侧重于 Kubernetes 与一些 AWS 服务的集成,包括 CloudWatch、Kinesis 和 S3,因此你必须拥有一个具有管理员权限的有效 AWS 账户。你需要在 AWS 中拥有一个有效的 Kubernetes 集群,正如前一章中使用 AWS eksctl
设置的那样。
你还需要在本地工作站上安装当前版本的 AWS CLI、kubectl
和 helm
3.x,如前一章所述。本章中的 helm
命令使用的是 helm
3.x 语法。EKS 集群必须设置有有效的 ALB Ingress Controller。
你可以像前几章中设置的那样使用 Spinnaker 和 Jenkins 来部署本章中的应用程序,但这不是必需的。
查看以下视频,看看代码是如何运行的:
设置演示应用程序 – ShipIt Clicker v7
为了有一个样例应用进行仪表监控,我们将使用以下 GitHub 仓库中 chapter10
目录下的 ShipIt Clicker 版本:
github.com/PacktPublishing/Docker-for-Developers/
本版本的应用相比上一章的版本有一些重要的生产就绪更新。它不再紧密依赖于特定的 Redis 安装,而是使用单独安装的 Redis 服务器。我们需要在安装最新版本的 ShipIt Clicker 之前,先将 Redis 集群部署到 Kubernetes 上。
为了准备我们的 Kubernetes 环境,既包括本地学习环境,也包括 AWS 云中的 EKS 集群,我们首先需要使用 Helm 安装 Redis。
从 Bitnami Helm 仓库安装 Redis
为了部署这个版本,我们将需要将 Redis 服务器独立部署,而不是将其与 ShipIt Clicker pod 一起部署。这代表了比之前章节中 ShipIt Clicker Kubernetes pod 中同时运行 Redis 服务器和无状态应用容器的场景更现实的情况。
我们将使用由 Bitnami 维护的 Redis 版本(bitnami.com/
),它提供了独立的读取和写入端点。首先通过 Helm 部署 Redis,先在本地 Kubernetes 安装中,然后在你的云 Kubernetes 安装中(当你运行以下命令时,将 docker-desktop
和 AWS ARN 替换为你安装的上下文 ID):
$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ kubectl config use-context docker-desktop
$ helm install redis bitnami/redis
$ kubectl config use-context arn:aws:eks:us-east- 2:143970405955:cluster/EKS-8PWG76O8
$ helm install redis bitnami/redis
这将部署一个包含一个接受读写的节点和多个只读的副本节点的 Redis 集群。本章中的 ShipIt Clicker 版本已适配为使用此外部 Redis 服务,该服务通过 Kubernetes secret 存储用于身份验证的密码。
攻击性术语——master 和 slave 被认为是有害的
Bitnami Redis 模板和 Redis 本身使用 master 和 slave 的术语来描述分布式系统中节点的角色。请注意,尽管这些术语在信息技术中很常见,但许多人认为这些术语是过时且冒犯的。其他术语,例如 primary/secondary 或 reader/writer,传达相同的信息,但没有负面的含义。更多关于这个争议性问题的内容请参见这篇文章:
medium.com/@zookkini/masters-and-slaves-in-the-tech-world-132ef1c87504
接下来,让我们构建并安装 ShipIt Clicker 到我们的学习环境中。
本地安装最新版本的 ShipIt Clicker
接下来,我们将构建 ShipIt Clicker Docker 容器,标记它,并将其推送到 Docker Hub,正如我们在前面的章节中所做的那样。执行以下命令,将 dockerfordevelopers
替换为你的 Docker Hub 用户名:
$ docker build . -t dockerfordevelopers/shipitclicker:0.10.0
$ docker push dockerfordevelopers/shipitclicker:0.10.0
$ kubectl config use-context docker-desktop
$ helm install --set image.repository=dockerfordevelopers/shipitclicker:0.10.0 shipit-v7 shipitclicker
使用kubectl get all
检查正在运行的 pods 和服务,以验证 pod 是否正在运行,记下其名称,然后使用kubectl logs
查看启动日志。日志中不应有任何错误。
接下来,让我们在 EKS 上安装这个版本。
通过 ECR 在 EKS 上安装最新版本的 ShipIt Clicker
现在你已经构建了 Docker 容器并在本地安装了它,将其安装到 AWS EKS,通过values.yaml
为其在 Route 53 区域中设置主机名,如shipit-v7.eks.example.com
(将 ECR 引用替换为与你的 AWS 账户和区域对应的引用,并将example.com
替换为你的域名):
$ docker tag dockerfordevelopers/shipitclicker:0.10.0 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:0.10.0
$ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 143970405955.dkr.ecr.us-east-2.amazonaws.com
$ docker push 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:0.10.0
$ kubectl config use-context arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8
$ kubectl config use-context arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8
$ helm install shipit-v7 -f values.yaml --set image.repository=143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:0.10.0 ./shipitclicker
检查 Kubernetes 日志,确保应用程序已成功部署到集群:
kubectl logs services/shipit-v7-shipitclicker
如果部署没有问题,获取 AWS ALB Ingress Controller 的入口地址,如前一章所述,并在 Route 53 控制台中为部署的应用程序创建 DNS 条目,使用 ALB 地址。然后,你应该能够通过类似https://shipit-v7.eks.example.com/
的 URL 访问你的应用程序(将example.com
替换为你的域名)。
本章 Jenkins 和 Spinnaker 的配置
你可能会想知道,是否可以使用你之前为本章设置的 Jenkins 和 Spinnaker 配置。可以,只需对 Jenkins 作业中的Spinnaker
多分支管道项和 Spinnaker 管道定义做一些简单的配置更改。首先修复 Jenkins。编辑作业的配置并更改chapter10/Jenkinsfile
,然后点击保存按钮:
图 10.1 – Spinnaker 多分支管道项的 Jenkins 构建配置设置
Jenkins 将重新扫描仓库,并使用来自chapter10
的文件,而不是chapter9
的文件。
然后,进入 Spinnaker,在配置管道阶段编辑用于暂存环境的管道,并将所有chapter9
的引用更改为chapter10
。
然后,你可以按照前一章中描述的使用git push --force origin HEAD:staging
来触发 Spinnaker 的 Kubernetes 部署。
本章中 ShipIt Clicker 的 Helm 模板已经通过以下命令打包成一个归档文件chapter10/helm.tar.gz
:
$ cd chapter10
$ helm package shipitclicker
Successfully packaged chart and saved it to: /Users/richard/Documents/Docker-for-Developers/chapter10/shipitclicker-0.10.0.tgz
$ mv shipitclicker-*.tgz helm.tar.gz
如果你更改了 Helm Charts 并且正在使用 Spinnaker,确保使用前面的命令重新打包helm.tar.gz
文件,因为 Spinnaker 期望在该特定文件中找到这些 chart。
接下来,让我们详细了解 Docker 容器和容器运行时日志,例如 Kubernetes 控制平面的日志。
Docker 日志和容器运行时日志
当你在排查应用程序问题时,拥有详细的日志对解决问题非常有帮助,包括来自应用程序本身和它运行的任何系统的日志。每个 Docker 容器,无论是在本地运行还是使用像 Kubernetes 这样的云容器运行时管理器运行,都生成自己的日志,你可以查询这些日志。
在前几章中,我们使用了docker logs
命令和kubectl logs
命令,以便在本地工作站和 Kubernetes 云环境中运行演示应用程序时检查日志。这些命令可以为您的系统提供有关事件的关键见解,包括应用程序日志消息以及错误和异常日志。它们仍然是您最常使用的基本工具;但是,特别是当我们需要使用 Kubernetes 扩展应用程序时,我们将需要一种更复杂的方法。
理解 Kubernetes 容器日志
每个在 Kubernetes pod 中运行的 Docker 容器都会产生日志。默认情况下,Kubernetes 运行时会临时存储每个运行中容器的最后 10MB 日志。这使得只需使用kubectl logs
工具,就能对每个运行中的应用程序进行日志采样。当 pod 从节点上驱逐或容器重启时,Kubernetes 将删除这些临时日志文件;它不会自动将日志保存到永久存储中。如果您需要排查问题,尤其是当问题发生在很久之前,导致那些日志已经滚动,且较旧的日志条目无法获取时,这样的做法远非理想。
如前一章所示,您可以使用kubectl
一次检查多个日志,针对显示多个 Spinnaker 容器日志,您还可以使用常见的命令行工具,如grep
、awk
、jq
和less
,对日志进行进一步的基本搜索和过滤。然而,日志滚动的问题会阻碍一些搜索尝试。
考虑到 Kubernetes 系统在日志保留和搜索方面的基本功能限制,明智的做法是探索如何缓解这些问题。接下来,让我们讨论我们希望日志管理系统具备的特点。
理想的日志管理系统特性
理想情况下,您希望使用具有以下某些特征的日志管理系统:
-
可以在中央控制台查看日志消息
-
从日志事件发生到可供搜索的延迟时间较低
-
从多个来源收集日志,包括 Kubernetes 对象,如 pods、节点、部署和 Docker 容器
-
易于使用的搜索界面,能够保存并重复使用临时查询
-
一种可视化搜索结果直方图的方式,支持通过点击和拖动图表来缩放图表(这一功能被称为刷选)
-
根据日志消息内容发送警报的方式
-
配置日志消息保留期限的方式
在过去的 20 年里,各种组织构建了许多优秀的日志存储和分析系统,包括以下第三方日志管理系统:
-
Splunk (
www.splunk.com/
) -
Elasticsearch (
www.elastic.co/
) -
Loggly (
www.loggly.com/
) -
Papertrail (
www.papertrail.com/
) -
New Relic Logs (
newrelic.com/products/logs
) -
Datadog Log Management (
docs.datadoghq.com/logs/
)
云服务提供商还拥有出色的集成日志存储和分析系统,包括以下几种:
-
AWS CloudWatch (
aws.amazon.com/cloudwatch/
) -
Google Cloud Logging (
cloud.google.com/logging
) -
Microsoft Azure Monitor Logs (
docs.microsoft.com/en-us/azure/azure-monitor/platform/data-platform-logs
)
作为开发者或系统运维人员,你可以使用这些系统来存储和搜索日志条目。然而,为了做到这一点,你必须使用 日志传输工具 从日志源提取日志并将其转发到日志管理系统。
我们将很快探讨如何将 Kubernetes 容器日志转发到这些系统中的一个,但首先,让我们研究另一个关键系统方面:Kubernetes 控制平面的日志记录,它负责节点、Pod 以及 Kubernetes 对象家族的协调工作。
使用日志排查 Kubernetes 控制平面问题
如果你自己运行 Kubernetes 集群并管理控制平面服务器,可能会在排查系统级别问题时遇到困难。Kubernetes 故障排除指南提供了关于查看控制平面集群中各个机器日志文件的指导,这可能是一个痛苦的过程:
kubernetes.io/docs/tasks/debug-application-cluster/debug-cluster/
然而,如果你使用的是托管的 Kubernetes 服务,例如 AWS EKS,你将无法直接访问这些系统。你可能会问,我该如何获取这些日志?托管的 Kubernetes 服务提供商都有将这些日志转发到另一个系统的方式,以帮助故障排除。幸运的是,AWS EKS 提供了一个可选的配置设置,允许它将控制平面的日志直接转发到 CloudWatch:
docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html
如果你使用了 第八章 中描述的 AWS EKS 快速启动来创建 EKS 集群,系统会为你自动设置这一配置。你可以前往 us-east-2
区域的 CloudWatch Logs 控制台进行验证:us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logs:
你将看到类似以下的日志组列表:
图 10.2 – 显示 EKS 控制平面日志的 CloudWatch 日志组
主要的 Kubernetes 控制平面日志组将类似于 /aws/eks/EKS-8PWG76O8/cluster
,但名称会包含你的 EKS 集群名称。你可以导航到这里,并通过控制台详细查看日志。
如果你使用 eksctl
创建了 EKS 集群,你可能没有启用 CloudWatch 日志记录。你可以使用这里的说明通过 eksctl
启用 EKS 的 CloudWatch 日志记录:
eksctl.io/usage/cloudwatch-cluster-logging/
现在你已经验证了 EKS 集群控制平面日志已发送到 CloudWatch,并学习了如何基本查看日志,我们将继续在 CloudWatch Logs 中捕获其余的 Kubernetes 日志,并使用 CloudWatch Logs Insights 进行分析。
使用 CloudWatch Logs 存储日志
AWS 提供了一个云规模的服务,处理日志、时间序列度量、数据摄取、存储和分析,称为 CloudWatch。许多 AWS 服务,包括 EKS,都通过 CloudWatch 提供日志集成功能。像许多 AWS 服务一样,你只为实际使用的部分付费。你可以在 aws.amazon.com/cloudwatch/
学习有关 CloudWatch 的基础知识。
我们在上一节中看到,AWS 允许我们配置 EKS 控制平面直接将日志发送到 CloudWatch。这很好,但如果我们要在一个中央位置管理日志,我们应该尽量将 所有 日志都存储在那里。
接下来,我们将了解如何通过 AWS 推荐的解决方案将更多日志发送到 CloudWatch,该解决方案在 EKS 文档中有所描述——Fluent Bit (fluentbit.io/
)。
AWS 提供了一个关于如何在 EKS 上设置 Fluent Bit 的优秀教程,网址是 aws.amazon.com/blogs/containers/kubernetes-logging-powered-by-aws-for-fluent-bit/
。
本章稍后描述的脚本和配置文件灵感来源于并部分借鉴了那篇文章。
接下来,我们将学习如何使用脚本快速且可重复地安装 Fluent Bit 及其支持的 AWS 资源。
安装 Fluent Bit 以将日志发送到 CloudWatch
尽管你可以手动按照之前引用的 AWS 博客中的步骤操作,但为了简化这些操作并使其与 AWS EKS 快速启动更加无缝配合,你可以使用本章中的 install-fluentbit-daemonset.sh
脚本,以 DaemonSet 的形式在 EKS 集群中安装 Fluent Bit,并使用一个将日志发送到 CloudWatch Logs 的配置。将 EKS 集群的 CloudFormation 模板名称作为命令行参数传入:
chapter10/bin/install-fluentbit-daemonset.sh Amazon-EKS
配置 Fluent Bit 以与 AWS 配合使用,比与其他一些云平台配合使用要多一些工作;例如,如果你使用的是 Google Cloud Platform 的 GKE,它会为你自动安装。
一旦你将容器的日志流式传输到 CloudWatch,你可以使用 CloudWatch AWS 控制台查看容器日志以及控制平面日志。
更改 CloudWatch 日志保留周期
默认情况下,CloudWatch 会无限期地存储日志。为了节省日志存储费用,你应该考虑为 CloudWatch 日志设置相对较短的保留期限——例如 60 天。你可以通过控制台或命令行执行此操作,以下命令设置由 install-fluentbit-daemonset.sh
脚本创建的 fluentbit-cloudwatch
日志组的保留周期:
aws logs put-retention-policy --log-group-name fluentbit-cloudwatch --retention-in-days 60 --region us-east-2
你可能会考虑为每个 CloudWatch 日志组进行此操作,即使是由 AWS EKS 快速入门 CloudFormation 模板创建的日志组。
接下来,我们来看一下如何将日志存储到 S3 中。
使用 AWS S3 长期存储日志
为了经济高效地长期存储日志数据,可以使用廉价的云对象存储系统,如 Amazon S3(aws.amazon.com/s3/
)。
如果你有长期保留日志的严重需求——例如,如果你有一个敏感的金融应用程序,其中规定要求所有应用日志必须存储 5 年——S3 可能是一个不错的选择。你可以通过为存储桶设置 S3 生命周期规则,将对象迁移到更便宜的存储层,迁移到 Amazon Glacier(aws.amazon.com/glacier/
),或使旧记录过期并删除,从而使长期存储更加经济实惠。
AWS 发布了一篇博客文章(aws.amazon.com/blogs/opensource/centralized-container-logging-fluent-bit/
),概述了你可以使用 Kinesis Firehose 作为额外的 Fluent Bit 目标将日志流式传输到 S3 的路径。你可以按照博客中 跨集群的日志分析 部分的说明,将日志以这种方式流式传输到 S3,但这可能会比较具有挑战性,因为你需要以多种方式调整脚本,以适应 EKS 快速入门,包括更改 AWS 区域并假设你使用 eksctl
来设置集群。
一个名为 CloudWatch2S3
的项目,灵感来自于那篇博客,可以通过部署一个 CloudFormation 模板来帮助完成这个过程。它的优点是可以将 所有 CloudWatch 日志组发送到 S3,并且你可以通过应用单个 CloudFormation 模板来安装它。它还可以收集来自多个 AWS 账户的 CloudWatch 日志,如果你选择这么做的话。克隆 GitHub 仓库 github.com/CloudSnorkel/CloudWatch2S3
到你的工作站,并按照仓库中的指引来设置将 CloudWatch 日志流式传输到 S3。在继续之前,你可能考虑创建一个 Amazon Key Management Service (KMS) 密钥来加密 Kinesis Firehose 和 S3 存储桶的内容。根据需要,你可以使用 AWS 控制台或 CLI 安装 CloudFormation 模板。
现在我们已经了解了如何在 CloudWatch 和 S3 中存储日志,接下来学习如何查询这些日志会非常有用。
使用 CloudWatch Insights 和 Amazon Athena 分析日志
既然你已经在 CloudWatch 和 S3 中存储了日志,你可以使用 CloudWatch Insights 或 Amazon Athena 来查询这些日志。
使用 CloudWatch Insights 分析存储在 CloudWatch 中的日志
执行查询 AWS 中存储日志的最简单方法是使用 CloudWatch Insights。这个基于 Web 的查询接口提供了一个交互式查询构建器,并且能够以直方图和表格数据格式可视化结果。它具备保存查询管理器的功能,这是一个关键功能,因为它让你能够构建并优化一组可以跨越一个或多个日志组的查询。有关 CloudWatch Insights 的文档可以参考 docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html
。
你可以在 AWS 控制台中探索该服务的示例查询,以更好地了解 CloudWatch Insights 提供的功能。
使用 AWS Athena 分析存储在 S3 中的日志
当日志存储在 S3 中时,你无法以与使用 CloudWatch Insights 或其他日志管理系统完全相同的方式进行查询。然而,仍然有方法可以高效地查询存储在 S3 中的日志。最直接的方法是使用一个名为 Amazon Athena 的查询工具:
Athena 允许你使用类似 SQL 的查询语言查询存储在 S3 存储桶中的半结构化数据。你按照查询付费,费用根据扫描的数据量和所需的处理时间来计算。为了让 Athena 理解你的 S3 数据的结构,你需要使用 AWS Glue 目录配置虚拟表:
docs.aws.amazon.com/athena/latest/ug/glue-athena.html
设置 AWS Glue 和 Athena 的组合相当复杂,超出了本章内容的范围。有关设置 Athena 以便查询存储在 S3 中的数据的更多信息,请参阅本章末尾的 进一步阅读 部分中的链接。
练习 – 查找玩过的 ShipIt 点击器游戏数量
ShipIt 点击器演示应用程序每当启动一个游戏时都会发出如下形式的日志信息:
{"level":30,"time":1591067727743,"pid":17,"hostname":"shipit-staging-shipitclicker-776c589c4f-z9tgg","name":"Shipit-Clicker -shipit-staging","msg":"Game created in Redis","key":"WWoor1SAYT_H98G4DDR-T","value":"OK","v":1}
在 CloudWatch Insights 中创建一个查询,计算已创建的游戏总数。对于 CloudWatch Insights,你需要选择 fluentbit-cloudwatch
日志组。
解决方案
请参考以下文件获取解决方案:
github.com/PacktPublishing/Docker-for-Developers/tree/master/chapter10/cloudwatch-insights.txt
在 Kubernetes 中使用存活性、就绪性和启动探针
Kubernetes 有多种类型的健康检查,称为 探针,用于确保其运行的 Docker 容器能够处理流量。你可以在kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
中详细阅读它们。
探针的类型处理不同的关注点:
-
存活性:确定应用程序是否能够处理请求。
-
就绪性:确定容器是否已准备好接收真实流量,特别是当容器依赖于必须可访问或已连接的外部资源时。
-
启动:确定容器是否准备好开始接收另外两种类型的流量,适用于启动较慢的传统应用程序,给它们时间启动。由于这些探针主要用于传统应用程序,我们将不会详细讨论它们。
你可以配置探针在运行中的容器内执行命令、进行 TCP 端口检查或检查 HTTP 端点。探针有合理的默认超时和检查间隔值——默认情况下,探针每 10 秒检查一次,并在 1 秒内超时失败。默认情况下,探针必须连续失败三次才会进入失败状态,必须成功一次才会进入成功状态。你可以通过模板注解覆盖这些值,例如在 Helm Charts 中的 deployment.yaml
文件中。
如果容器的存活性探针失败足够多次,Kubernetes 将终止容器并重启它。如果一个 Pod 中容器的就绪性探针失败,Kubernetes 将不会将依赖该 Pod 的服务的流量指向该容器。接下来,我们将详细探讨存活性和就绪性探针。
使用存活性探针查看容器是否能够响应
对于像 ShipIt Clicker 这样的服务,一个好的存活检查是,应用程序可以完全依赖内部配置的资源进行响应——例如,依赖于部署在同一 pod 中的容器。在前几章中,应用程序的存活和就绪检查是通过 HTTP 获取 /
资源进行的。本章的存活检查保持不变,因为能够提供一个简单的 HTML 页面是 Express 应用程序的一个不错的存活检查。请查看以下来自 chapter10/shipitclicker/templates/deployment.yaml
的片段:
livenessProbe:
httpGet:
path: /
port: http
这使得 Express 提供 chapter10/src/public/index.html
文件。这个做法提供了一个不错的存活探针,但它并不意味着 pod 已准备好处理需要访问外部资源的请求。为此,我们应该使用就绪探针。
使用就绪探针来确保服务能够接收流量
一些应用程序需要完成一系列初始化操作,比如进行数据库调用和调用外部服务,才能准备好接收流量。对于 ShipIt Clicker,应用程序必须在准备好接收流量之前能够联系到 Redis。接下来,我们将检查 ShipIt Clicker 早期版本中的缺陷,以及为了支持存活和就绪探针所做的修复,因为这些变化是你在应用程序中可能会遇到的更改的典型示例。
更改 ShipIt Clicker 以支持独立的存活和就绪探针
ShipIt Clicker 的早期版本会在连接 Redis 失败时遇到致命异常。只要 src/server/index.js
中的初始化例程加载,这种情况就会发生,因为它加载的模块会实例化位于 src/server/api/services/redis-service.js
的 RedisDatabase
类,而该类会立即连接到 Redis 服务器。这个类缺少 Redis 错误处理程序,因此抛出的错误是致命的,会导致进程终止。
这个故障会立即重复,因为 Kubernetes 尝试启动另一个容器,并触发一系列崩溃,进而激活 Kubernetes 崩溃循环检测器。
chapter10/src/server/api/services/redis.service.js
中 RedisDatabase.init()
方法的新错误处理程序如下所示,它将所有 Redis 错误记录到控制台——因此,也会记录到 Kubernetes 日志系统——以便更容易进行故障排除:
client.on("error", err => l.error({msg: "Redis error", err:err}));
本章的代码还采用了懒加载模式,避免在类实例化时立即连接 Redis。通过懒加载,你将对象或资源的创建推迟到实际需要时。我们通过 RedisDatabase.instance()
方法实现了懒加载,该方法使用了 Redis 客户端连接的单例设计模式:
instance() {
return this._client ? this._client : this._client = this.init();
}
async ping() {
return this.instance().pingAsync();
}
使用懒加载将允许我们推迟连接 Redis 服务器,直到到达一个真正需要它的请求。回想一下,在这个版本的应用程序中,我们将 Redis 服务器从 ShipIt Clicker 服务中拆分出来,并单独运行。鉴于此,准备探针应该连接到 Redis 服务器,并确保 ShipIt Clicker 确实可以与它通信,然后再接受流量。这个版本有一个新的 API 端点 /api/v2/games/ready
,它执行 Redis 的 PING
操作以确保应用程序准备好接收流量:
readinessProbe:
httpGet:
path: /api/v2/games/ready
port: http
如果 Redis 服务器不可用,则此就绪探针将失败,Kubernetes 将从服务中移除未通过健康检查的容器。
练习 – 强制 ShipIt Clicker 失败就绪检查
接下来,我们将进行一个实验,看看当存活探针通过但就绪检查失败时会发生什么。使用 kubectl
切换到本地学习环境的 Kubernetes 上下文。暂时更改 chapter10/shipitclicker/template/configmap.yaml
文件,修改 REDIS_PORT
值为无效数字,例如 1234
,以破坏 Redis 安装。然后,使用 Helm 安装替代名称为 shipit-ready-fail
的 chart。使用 kubectl get pods
验证新 pod 的状态为 RUNNING
,但标记为 READY
的 pod 数量为 0/1
。你的输出应该类似于以下内容:
$ kubectl get pods | grep -E '^NAME|fail'
NAME READY STATUS RESTARTS AGE
shipit-ready-fail-shipitclicker-57c67d76cd-qklh6 0/1 Running 0 3m20s
这个安装版本的 ShipIt Clicker 的准备检查将立即开始失败。如果你描述这个 pod,你会看到它不再是就绪状态。当你完成后,使用 Helm 卸载 shipit-ready-fail
chart,并将 configmap.yaml
文件中的值恢复为原始值。
使用 Prometheus 收集指标并发送警报
Prometheus 是一个主导的基于 Kubernetes 的系统,用于收集集群操作的指标数据。Prometheus 提供了与处理时间序列数据、可视化数据、查询数据和基于指标数据发送警报相关的广泛功能。
这些指标数据可能包括多种时间序列数据,例如节点和 pod 的 CPU 使用情况;存储利用率;由就绪探针定义的应用程序健康状况;以及其他特定于应用程序的指标。Prometheus 使用拉取模型,它会定期轮询端点以获取数值数据。支持 Prometheus 的 Pods、DaemonSets 以及其他 Kubernetes 资源通过注解声明 Kubernetes 应该通过 HTTP 从它们那里抓取指标数据,通常是通过 /metrics
端点。这可能包括来自节点的数据,通过一个名为 node_exporter
的 DaemonSet 来暴露该数据,该 DaemonSet 在每个节点上运行。
它通过将接收到的指标数据与指标名称以及键值对格式的标签集合关联,并附带毫秒分辨率的时间戳来存储这些数据。这种标签方式不仅允许高效存储,还可以在时间序列数据库中查询这些指标。系统管理员和自动化系统可以查询此数据库,以调查系统的健康状况和性能。
它不仅提供了一个时间序列数据库用于存储指标,还提供了一个警报子系统,帮助系统管理员在应用程序遇到问题时主动采取措施。
你可以在prometheus.io/docs/introduction/overview/
了解更多关于 Prometheus 的整体架构和功能。
Prometheus 的历史
虽然 Prometheus 最初是由 SoundCloud 于 2012 年开发的,但它在 2016 年成为了云原生计算基金会(CNCF)的顶级项目,并且它独立于任何单一公司,就像 Kubernetes 本身一样。它的设计灵感来自 Google 的 Borgmon 系统。
通过查询和图形网页界面探索 Prometheus
如果你按照第八章中描述的方式,使用 AWS EKS 快速启动 CloudFormation 模板安装了 EKS 集群,部署 Docker 应用到 Kubernetes,你应该已经有一个可以工作的 Prometheus 应用程序。如果没有,你可以按照此处的说明使用 Helm 安装它:
docs.aws.amazon.com/eks/latest/userguide/prometheus.html
你可以通过使用kubectl
创建一个端口转发代理连接到 Prometheus 服务并开始探索它。你应该按如下方式将prometheus-server
Kubernetes 服务连接到本地工作站(将use-context
后的表达式替换为你的 AWS EKS 集群 ARN):
$ kubectl config use-context arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8
$ kubectl port-forward -n prometheus service/prometheus-server 9090:80
然后,打开一个网页浏览器并访问http://localhost:9090/
,你将看到 Prometheus 查询控制台。
一个很好的初学者查询是使用node_load1
,它显示了底层 Kubernetes 节点的 1 分钟负载平均值。将其输入查询字段并点击执行按钮,然后激活图形选项卡。你将看到一个显示负载平均值的图表。
Prometheus 查询语言叫做PromQL,它与其他时间序列数据库查询语言非常不同。你需要学习更多关于 PromQL 的内容,以便编写自己的查询。在medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085
上阅读更多相关内容。
虽然 Prometheus 可以独立绘制查询结果的图表,但 Kubernetes 用户通常会将 Grafana 与 Prometheus 一起使用,以提供更复杂的图表和仪表盘。我们将在本章后面深入探讨 Grafana。接下来,我们将研究如何将 Prometheus 指标添加到应用程序中。
向应用程序添加 Prometheus 指标
为了将应用程序与 Prometheus 集成,您需要通过 Prometheus 客户端库暴露一个特殊结构的 HTTP API。Prometheus 为多种语言提供官方客户端库,社区也为其他语言创建了许多客户端库。您可以在 Prometheus 文档中阅读更多关于一般过程的内容,链接为 prometheus.io/docs/instrumenting/clientlibs/
。
为了演示这个集成,本章中的 ShipIt Clicker 版本暴露了默认指标集和一个名为 shipitclicker_deployments_total
的自定义计数器指标。为了做到这一点,我们通过 Node.js 集成了用于 JavaScript 应用程序的 Prometheus 客户端:
为了执行集成,我们通过 npm install prom-client --save
命令安装并保存了 prom-client Node 模块,然后根据提供的示例代码,松散地集成了客户端,参考链接为 github.com/siimon/prom-client/blob/master/example/server.js
。
启用指标的 ShipIt Clicker 程序结构
ShipIt Clicker 中的 Prometheus 指标发布代码在结构上遵循了使用 Express 框架编写的 Node 应用程序的常规方式,指标路由被添加到与游戏 API 路由相同的模块化模式中的主路由 chapter10/src/server/routes.js
。主路由导入了 chapter10/src/server/api/controllers/metrics/router.js
,该路由定义了 /metrics
和特殊路由 /metrics/shipitclicker_deployment_total
的 HTTP 路由,并使用 chapter10/src/server/api/controllers/metrics/controller.js
中定义的控制器类。该控制器包含与位于 chapter10/src/server/api/services/prometheus.service.js
中定义的 Prometheus 服务类集成的方法,该服务类与 prom-client
库集成并暴露了默认指标以及自定义的 shipitclicker_deployments_total
指标。请参考以下服务代码摘录,了解我们如何封装 prom-client
库:
import * as client from 'prom-client';
…
export class Prometheus {
…
this.register = client.register;
this.deploymentCounter = new client.Counter({
name: 'shipitclicker_deployments_total',
help: 'Total of in-game deployments in this ShipIt Clicker process',
});
client.collectDefaultMetrics({
timeout: 10000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
});
}
}
export default new Prometheus();
提供指标的控制器类具有适当的异常处理和错误日志记录框架,而 prom-client
的基础示例中缺少这些功能。如果您愿意,您可以轻松地将路由器、控制器和服务类适配到新应用程序中,几乎不需要额外的工作。
为了简化故障排除,度量指标与应用程序的其余部分绑定在同一个 HTTP 端口:端口 3000
。这意味着你可以从任何安装了 ShipIt Clicker 并集成了此代码的版本中获取度量指标——例如,从 shipit-v7.eks.example.com/metrics
(将 example.com
替换为你的域名)。你应该会看到一长串度量指标,以下列内容为开头:
# HELP shipitclicker_deployments_total Total of in-game deployments in this ShipIt Clicker process
# TYPE shipitclicker_deployments_total counter
shipitclicker_deployments_total 0
# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
process_cpu_user_seconds_total 2.5176489999999996
…
现在我们已经看到了原始度量指标,让我们检查一下让 Prometheus 发现演示应用程序的配置是如何工作的。
让 Prometheus 发现 ShipIt Clicker 应用程序
通过 AWS EKS 快速启动 CloudFormation 模板配置的 Prometheus 安装已配置为执行支持 Prometheus 度量指标的 Pod 的服务发现。为了使你的 Kubernetes Pod 被发现,它们必须带有 Prometheus 特定的元数据注释,包括 prometheus.io/scrape: "true"
注释。请参阅 chapter10/shipitclicker/template/deployment.yaml
,了解用于将 ShipIt Clicker 暴露给 Prometheus 的注释:
template:
metadata:
labels:
{{- include "shipitclicker.selectorLabels" . | nindent 8 }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
只要这些注释存在于 Pod 上,Prometheus 就会知道必须抓取 Pod 的 /metrics
端点以获取数据。
现在我们已经看到程序及其配置模板是如何扩展以支持 Prometheus 度量指标的,让我们查询 Prometheus 以获取自定义度量指标。
查询 Prometheus 以获取自定义度量指标
在 shipit-v7.eks.example.com/
上玩游戏一两分钟(将 example.com
替换为你的域名)。然后,使用本章前面解释的端口转发方法连接到 Prometheus 控制台,并发出查询 shipitclicker_deployments_total
,然后切换到 Graph
标签。你应该会看到一张图表,显示随着时间推移,部署数量不断增加。
如果你继续玩游戏并在 Prometheus 控制台中持续重新发出查询,你将看到部署数量增加。Prometheus 使用的默认抓取间隔和目标定义在 prometheus.yml
文件中,该文件嵌入在 prometheus-server
ConfigMap 中,位于 prometheus
命名空间内。默认情况下,抓取间隔设置为 30
秒,因此你不会看到 Prometheus 查询结果的即时变化。
接下来,让我们探索 Prometheus 对警报的支持。
配置 Prometheus 警报
Prometheus 具有定期自我查询的能力,以检测重要的条件——这是警报系统的基础。你可以应用强大的 Prometheus 查询语言来检测系统中具有 Prometheus 度量指标的部分是否过载、响应过慢或不可用。
对于大多数应用程序,基础告警项必须回答的问题是 应用程序是否可用?如果应用程序正在运行,它就准备好为用户请求提供服务。Prometheus 有一个名为 up
的指标,可以帮助回答这个问题 – 如果服务可用,它的值为 1
,如果不可用,则为 0
。如果你查询 Prometheus 中的 up
,你将看到它监控的每个服务的基本可用性状态。如果任何服务的值不是 1
且持续 5 分钟或更长时间,你可能会想要触发告警。这是 Prometheus 文档中给出的告警基础示例(参见 prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
)。接下来,我们将展示如何将文档中的 InstanceDown
示例规则添加到我们的 Prometheus 服务配置中。
AWS EKS 快速启动模板中的 Prometheus 安装最初没有定义告警,因此我们需要自己定义一个或多个告警。如果你在本地工作站上安装了 Prometheus,你将编辑 /etc
目录中的配置文件来完成此操作,然后触发配置文件的重新加载。然而,在 Kubernetes 设置中,必须有另一个机制来允许编辑这些值。
AWS EKS 快速启动 Prometheus 设置使用了一个位于 prometheus
命名空间中的 Kubernetes ConfigMap,名为 prometheus-service
,该 ConfigMap 内嵌了多个 YAML 配置文件,并且在每个 Prometheus 服务器 Pod 中运行一个容器(参见 github.com/jimmidyson/configmap-reload
),该容器监控 ConfigMap 文件的变化,并向 Pod 中运行的 Prometheus 服务器发送 HTTP POST
请求,触发其重新加载更改。ConfigMap 文件每分钟更新一次。编辑告警配置的周期如下所示:
-
使用
kubectl
编辑prometheus-service
ConfigMap。 -
等待 1 分钟,直到 ConfigMap 更改传播到 Pods。
-
通过端口转发的 Prometheus 控制台查看告警,地址为
http://localhost:9090/alerts
。
为了添加监控,我们运行以下命令来编辑 ConfigMap,并在 alerts:
部分下添加规则,如下所示:
kubectl -n prometheus edit configmap/prometheus-server
查看文件顶部,并使alerts:
部分与以下文本匹配:
apiVersion: v1
data:
alerting_rules.yml: |
{}
alerts: |
groups:
- name: Kubernetes
rules:
- alert: InstanceDown
expr: up == 0
for: 5m
labels:
severity: page
annotations:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes."
prometheus.yml: |
编辑完文件后,保存它,文件将在 1 分钟内传播到 Pods。
故障排除提示 – YAML 格式文件要求严格
prometheus-server
Pods 中的大小写和空格问题)– 或者更糟糕的是,无法添加你打算设置的告警。
然后,你应该能在 Prometheus 控制台的 Alerts 部分看到告警定义;点击 InstanceDown,应该能显示告警定义:
图 10.3 – 显示 InstanceDown
的 Prometheus 告警
现在你已经定义了警报,你可以配置 Prometheus 根据该警报发送通知。
使用 Prometheus Alertmanager 发送通知
Prometheus 最强大的功能之一就是它支持通过名为 Alertmanager 的组件发送警报通知。该组件从 Prometheus 获取原始警报信息,进行进一步处理,然后发送通知。你可以在 prometheus.io/docs/alerting/overview/
找到 Prometheus 警报的详细概述。
该警报系统支持多个渠道,包括电子邮件、PagerDuty、Pushover、Slack 等,均通过 webhooks 实现。我们将配置一个 Slack 集成,演示如何发送警报。为此,我们将修改存储在名为 prometheus-alertmanager
的 Kubernetes ConfigMap 中的 Alertmanager 配置。
要添加 Slack 集成,确保你有一个通过浏览器登录的 Slack 帐户,然后访问 api.slack.com/
,为 Slack 创建一个新应用。在 Features 配置屏幕中,配置一个新的 incoming webhook,并选择一个 Slack 渠道接收通知。然后,将 incoming hook 的 URL 复制到剪贴板,并将其存储在本地文本文件中。配置 Alertmanager 时需要用到它。配置你认为相关的其他设置,包括 Slack 集成的图标。然后,使用以下命令编辑 Alertmanager 的 ConfigMap:
kubectl -n prometheus edit configmap/prometheus-alertmanager
ConfigMap 中将会有一个空的 {}
子句用于 global:
部分,我们将删除它,然后添加 slack_api_url
和 slack_configs
部分,具体如下(将单引号中的值替换为来自 Slack 应用的 incoming webhook URL,并将频道替换为 Slack 频道的 hashtag 名称,警报将在该频道中显示):
apiVersion: v1
data:
alertmanager.yml: |
global:
slack_api_url: 'https://hooks.slack.com/services/A/B/C'
receivers:
- name: default-receiver
slack_configs:
- channel: '#docker-book-notices'
route:
这将为你提供一个非常基础的警报设置,你可以在此基础上扩展,获取停机的通知。你可以通过 Prometheus Alertmanager API 发送测试警报来测试 Alertmanager 是否已经连接。首先,将 Alertmanager 服务的端口转发到本地机器:
kubectl -n prometheus port-forward service/prometheus-alertmanager 9093:80
在另一个控制台窗口中,输入以下命令:
curl -d '[{"status": "firing", "labels":{"alertname":"Hello World"}}]' -H "Content-Type: application/json" http://localhost:9093/api/v1/alerts
你应该从 curl
命令中收到 {"status":"success"}
响应,然后你应该在你的 Slack 中看到 Hello World
警报:
图 10.4 – Prometheus 警报在 Slack 中
练习 - 部署一个故障的 ShipIt Clicker,期待 AlertManager 通知
编辑 chapter10/shipitclicker/templates/deployment.yaml
文件,将 Prometheus 探针重定向到端口 3001
,并使用 Helm 部署这个损坏的 ShipIt Clicker 应用程序,以查看警报的实际效果。将应用程序命名为 shipit-broken
。检查 Prometheus 控制台,验证警报进入待处理状态。这应该在 1 分钟以内发生。10 分钟内,您应该会在 Slack 上看到类似 [FIRING:1] (InstanceDown shipit-broken shipitclicker 10.0.87.39:3000 kubernetes-pods default shipit-broken-shipitclicker-6658f47599-pkxwk 6658f47599 page)
的警报。收到警报后,卸载 shipit-broken
Helm Chart,恢复 deployment.yaml
文件的更改,您应该不再收到该特定问题的通知。
一旦收到警报,卸载 shipit-broken
Helm Chart,您应该不再收到关于该特定问题的通知。
深入探索 Prometheus 查询和外部监控
关于如何构建 Prometheus 查询以及如何扩展 Prometheus 以监控外部系统的话题非常深入,超出了本章的范围。请查阅 Prometheus 文档以及本章末尾的 进一步阅读 部分,了解更多关于创建 Prometheus 查询和配置它以使用额外的指标数据源的信息。
接下来,让我们看看如何使用 Grafana 来可视化 Prometheus 收集的数据。
使用 Grafana 可视化操作数据
Prometheus 通常与 Grafana 一起部署(grafana.com/
),以提供复杂的仪表板和更精密的监控 UI。AWS EKS 快速启动中安装的 Kubernetes 包含了 Grafana,并配置了一些仪表板。让我们来探索 Grafana 安装并看看它是如何与 Prometheus 集成的。
获取 Grafana 访问权限
Grafana 安装默认通过 Kubernetes LoadBalancer 暴露,在 EKS 中,它会为实际的 ELB DNS 名称创建一个 AWS EC2-Classic EXTERNAL-IP
字段:
$ kubectl -n grafana get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana LoadBalancer 172.20.5.46 aaa-bbb.us-east-2.elb.amazonaws.com 80:30669/TCP 39d
将该 DNS 地址放入您的网页浏览器中,前缀加上 http://
,您将看到 Grafana 登录界面。您需要从 Kubernetes 密钥中获取管理员用户名和密码才能登录:
$ kubectl -n grafana get secrets/grafana --template='{{index .data "admin-user"}}' | base64 -D
[username redacted]
$ kubectl -n grafana get secrets/grafana --template='{{index .data "admin-password"}}' | base64 -D [password redacted]
使用这些值登录到 Grafana 控制台。然后,您可以探索 UI,包括仪表板和 Prometheus 查询浏览器。某些仪表板可能没有完全填充数据,例如 Kubernetes All Nodes 仪表板,但无需过于担心,因为可以添加社区提供的详细且完全填充集群统计数据的仪表板。查看 Kubernetes Pods 仪表板并选择不同的 Pod,包括 Redis Pod 和 ShipIt Clicker Pod,以了解如何使用仪表板。使用右上角的控件更改时间窗口,以显示一天或一周的数据,然后点击并拖动感兴趣的区域进行缩放。
接下来,我们将添加一些社区提供的仪表板,以便体验系统所能提供的全部功能。
添加社区提供的仪表板
Grafana 提供了一个官方和社区提供的仪表板仓库,地址为 grafana.com/grafana/dashboards
。
这些仪表板包含种类繁多的选项。你应该根据自己的需求,详细探索这些仪表板。
当你添加一个仪表板时,其中一个选项是 Import。选择这个选项后,它会要求你输入社区网站的仪表板 ID 或 URL。
以下是四个值得添加到你的安装中的通用仪表板:
-
Cluster Monitoring for Kubernetes:这个来自 Pivotal Observatory 的紧凑型仪表板让你一眼就能看到哪些 Pod 消耗了最多的 CPU、内存和网络资源 –
grafana.com/grafana/dashboards/10000
。 -
Kubernetes Cluster (Prometheus):一个简洁的仪表板,展示了集群范围内的关键指标 –
grafana.com/grafana/dashboards/6417
。 -
1 Node Exporter for Prometheus Dashboard EN v20191102:一个集群级的复杂仪表板,展示了许多 CPU、磁盘和网络指标 –
grafana.com/grafana/dashboards/11074
。 -
Node Exporter Full:这是一个展示 Prometheus Node Exporter 所有可能指标的仪表板,网站上非常受欢迎,下载量超过两百万次 –
grafana.com/grafana/dashboards/1860
。
添加一个自定义查询的新仪表板
添加自定义查询的新仪表板的步骤如下:
-
在左侧菜单中,点击 + 图标以添加新的仪表板。然后,在 New Panel 区域,点击 Add Query。在 Metrics 旁边的字段中添加以下查询:
shipitclicker_deployments_total
它应该看起来像这样:
图 10.5 – Grafana 自定义仪表板项目定义
-
然后,在
ShipIt Clicker Deployments
中,点击屏幕左上角的左箭头,返回定义小部件的界面。 -
在顶部菜单中,点击带有加号的图表以添加另一个小部件:
图 10.6 – Grafana 添加小部件
-
添加另一个类似的面板,使用以下查询并设置标题为
ShipIt Clicker Deployments Rate
:rate(shipitclicker_deployments_total[2m])
-
然后,点击顶部菜单中的齿轮图标,将仪表板的名称更改为
ShipIt Clicker Dashboard
,然后保存仪表板。 -
接下来,休息一下,玩几分钟的 ShipIt Clicker 游戏。这将生成你可以在图表中看到的流量。停止游戏几分钟后,你的仪表板可能会看起来像这样:
图 10.7 – Grafana 中的 ShipIt Clicker 自定义仪表板
理解速率和计数器
请注意,在停止播放后,速率仪表板会回落到 0,但仅计算总数的仪表板会继续增加并保持不变。在 Prometheus 中选择以 total
结尾的速率查询通常是您想要测量吞吐量的方式。
现在我们已经了解了如何使用 Grafana 绘制应用程序指标并构建仪表板,接下来让我们探索另一个主题:使用 Jaeger 进行应用程序性能监控和分布式追踪。
使用 Jaeger 进行应用程序性能监控
我们现在将简要浏览 Jaeger,了解它如何在微服务架构中用于性能监控。在实现微服务架构与单体应用程序的性能和错误追踪时,面临的一个关键问题是,微服务架构本身就是一个分布式环境。
早期尝试解决这个问题的工具,如 OpenCensus (opencensus.io/tracing/
),由于使用了不同的术语、方法和不兼容的系统,面临了一些问题。为了克服这些问题,性能监控社区创建了 OpenTracing API。
理解 OpenTracing API
OpenTracing 项目 (opentracing.io/
) 旨在允许工程师使用一种非特定厂商的通用 API 规范,将性能监控功能添加到他们的项目中。
实现这个目标的 OpenTracing 的一些关键特性如下:
-
API 规范本身 (
github.com/opentracing/specification
) -
实现 API 规范的框架和库
-
综合文档 (
opentracing.io/docs/
)
现在让我们来看一下这个规范的两个最重要的核心概念:span 和 tracing。
Span
Span 代表一个工作单元,是这种追踪系统的基本构建块。每个 span 包含一个操作名称、开始和结束时间、一个 SpanContext,以及最后的 tags 和 logs 键值对。
您的标签键值对适用于整个 span,并包含诸如 db.type
和 http.url
等信息。常见标签的列表可以在 GitHub 上找到:github.com/opentracing/specification/blob/master/semantic_conventions.md
。
日志键值对用于定义与特定事件或事故相关的日志消息,而不是整个 span。例如,您可以使用这组键值对来记录调试信息。
Span 中的最终概念是 SpanContext,它用于跨进程边界传递数据。它的两个关键组成部分是表示 trace 中特定 span 的状态,以及被称为 baggage items 的概念。这些本质上是跨进程边界的键值对。
你可以在 OpenTracing 网站的文档中阅读更多关于 spans 的内容,链接地址为 opentracing.io/docs/overview/spans/
。
Traces 和 tracers
接下来我们将研究的概念是 traces 和 tracers。
Trace 是将一个或多个 spans 按照一个称为 trace 标识符 的单一标识符进行分组的方式。它可以用来理解分布式系统中的工作流,例如微服务架构。
Tracer 是 OpenTracing API 规范的实际实现,它收集 spans 并发布它们。实现 OpenTracing 的一些 tracer 示例包括 Datadog(我们将在 第十四章, 高级 Docker 安全性 – 密码、秘密命令、标签和标签 中进行探索)、Instana、Lightstep 和 Jaeger。
如果你想阅读更多关于 tracers 和 traces 的内容,可以在官方文档中找到,链接为 opentracing.io/docs/overview/tracers/
。
让我们探索一个实现了 OpenTracing API 的工具 —— Jaeger。
Jaeger 介绍
Jaeger 是一个开源的应用程序追踪框架,允许开发人员和系统操作员收集正在运行的应用程序的信息,并确定应用程序如何消耗时间以及它如何与其他分布式系统组件进行交互,使用的是 OpenTracing API。Jaeger 网站地址为 www.jaegertracing.io/
。
Jaeger 的历史
Jaeger,源自德语中的“猎人”一词,最初由运输公司 Uber 开发。由 Yuri Shkuro 领导的工程师团队在此基础上构建了这个分布式追踪框架。受到 Google 关于其追踪框架 Dapper(research.google/pubs/pub36356/
)以及 Zipkin 追踪框架(zipkin.io/
)的论文启发,他们创造了 Jaeger 作为一个云原生追踪框架。Uber 自 2015 年以来一直在使用 Jaeger,并于 2017 年将其贡献给 CNCF;CNCF 在 2019 年将其晋升为顶级项目。你可以在 Uber 工程博客上阅读更多关于 Jaeger 历史的内容,链接为 eng.uber.com/distributed-tracing/
。
Jaeger 的组件
构成 Jaeger 生态系统的一些重要组件包括以下内容:
-
客户端库,可作为包或直接从 GitHub 获取
-
Jaeger 代理用于监听 spans
-
Collector,负责汇总从代理发送的数据
-
Jaeger 查询,用于通过 UI 分析数据
-
Ingester,它允许我们从 Kafka 主题收集数据,然后将数据写入 AWS Elasticsearch 等服务
让我们测试 Jaeger,看看它在实践中如何运作。
探索 Jaeger UI
要探索 Jaeger,我们可以使用 Docker 运行最新的 all-in-one 镜像:
$ docker run --rm -i -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest
然后,我们可以打开一个网页浏览器并访问http://localhost:16686/
查看 UI 界面。Jaeger 搜索界面本身已被设置为向收集器发送追踪信息,所以一旦看到 UI 界面,刷新页面一次以生成更多的追踪信息,并填充服务下拉框。接着,点击查找追踪按钮。它应该看起来像这样:
图 10.8 – Jaeger UI 搜索界面
探索完成后,通过按下Ctrl + C停止运行中的 Docker 容器。接下来,让我们探索如何通过查看 ShipIt Clicker 与 OpenTracing 和 Jaeger 的集成来为应用程序添加追踪功能。
使用 ShipIt Clicker 探索 Jaeger 客户端
Jaeger 客户端支持多种编程语言。我们的示例将使用 Node.js,但也支持 Go、Java 和 Python 等语言。你可以访问以下网址查看官方客户端文档以了解更多信息:
www.jaegertracing.io/docs/1.18/client-libraries/
ShipIt Clicker v7 已经安装了 Jaeger 客户端、OpenTracing JavaScript Express 中间件和 OpenTracing API 客户端:
-
Jaeger 客户端:
github.com/jaegertracing/jaeger-client-node
-
Express 中间件:
github.com/opentracing-contrib/javascript-express
-
OpenTracing 客户端:
github.com/opentracing/opentracing-javascript
如果你有一个希望与 Jaeger 一起使用的 Express 应用程序,你可以执行以下命令安装相同的库组合:
$ npm install --save jaeger-client express-opentracing opentracing
在 GitHub 仓库中(github.com/PacktPublishing/Docker-for-Developers
),chapter10/src/server/common/jaeger.js
中的 Jaeger 客户端配置示例展示了如何使用环境变量和默认值的混合方式配置 Jaeger 客户端。docker-compose
配置文件和 ShipIt Clicker 的 Helm 模板都已更新,使用一些环境变量来配置 Jaeger,为jaeger.js
提供适当的环境上下文;该文件导入了jaeger-client
模块,进行配置,并导出tracer
对象。我们在chaper10/src/server/common/server.js
文件中的express-opentracing
中间件使用了这个tracer
对象:
import tracer from './jaeger';
import middleware from 'express-opentracing';
…
export default class ExpressServer {
constructor() {
…
app.use(middleware({ tracer: tracer }));
}
使用中间件或其他可以与常见库进程挂钩的软件,能够为我们提供提升,并让我们避免编写样板代码。express-opentracing
中间件对象为 Express 的res
响应对象装饰了一个span
属性,这使我们能够在控制器和请求处理程序中使用 OpenTracing 的跨度。
我们还可以使用更明确的风格,在其中程序化地创建跨度和日志条目:
-
要查看实际操作,请检查 ShipItClicker 的 API 控制器
chapter10/src/server/api/controllers/games/controller.js
:async incrementGameItem(req, res) { const key = `${req.body.id}/${req.body.element}`; const value = req.body.value; const span = tracer.startSpan('redis', { childOf: req.span,
-
下一段代码展示了如何在 span 中创建一个标签,用于保存更详细的跟踪信息:
tags: { [opentracing.Tags.SPAN_KIND]: opentracing.Tags.SPAN_KIND_RPC_CLIENT, 'span.kind': 'client', 'db.type': 'redis', 'db.statement': `INCRBY ${key} ${value}`, }, });
-
上面的代码通过
req.span
使用主 span 初始化了一个子 span,用于 Redis。然后,我们立即调用 Redis,记录结果,并完成 span:try { var redis = await RedisService.incrby(key, value); span.log({ result: redis }).finish();
-
接下来,我们在与父 span 关联的 span 中记录一条消息:
const msg = { msg: 'Game item Redis INCRBY complete', key: key, value: redis, }; req.span.log(msg);
-
现在,我们使用常规日志机制记录消息,并在此请求递增
deploys
元素时更新 Prometheus 自定义指标:l.info(msg); if (req.body.element === 'deploys') { const incr = parseInt(req.body.value, 10); PrometheusService.deploymentCounter.inc(incr); }
-
如果我们到达这里,Redis 请求已经成功,我们可以向客户端返回一个 JSON 响应:
return res.json({ id: req.params.id, element: req.params.element, value: redis, });
-
如果请求失败——例如 Redis 无法访问——我们必须进行错误处理。首先,我们构造一条包含详细错误信息的消息:
} catch (err) { const msg = { key: req.body.id, element: req.body.element, message: err.message, stack: err.stack, };
-
然后,我们将错误记录到 OpenTracing span 和常规错误日志中,并返回一个 404 Not Found HTTP 响应给客户端:
span.log(msg).finish(); l.warn(msg); return res.status(404).json({ status: 404, msg: 'Not Found', }); } }
上面的代码展示了如何使用 tracer 对象在
req.span
中启动一个子 span,并包含记录 Redis 操作结果的日志元素。
为了方便演示 Jaeger 集成,本章提供了一个 Docker Compose 文件chapter10/docker-compose.yml
,它集成了 ShipIt Clicker 容器、Redis 和 Jaeger。你可以通过在chapter10
目录下执行以下命令来运行它们:
docker-compose build && docker-compose up -d
然后,你可以访问http://localhost:3010/
,玩一分钟的 ShipIt Clicker 游戏以生成一些跟踪数据,再访问http://localhost:16686/
查看 Jaeger 查询界面的实际操作。查询shipitclicker-v7
服务,点击图表中的一个跟踪记录,然后展开两个 spans 和其中显示的日志,你应该会看到类似这样的内容:
图 10.9 – 显示 ShipIt Clicker HTTP 事务和 Redis spans 的 Jaeger 跟踪
与第六章《使用 Docker Compose 部署应用程序》中展示的docker-compose.yml
文件相比,本章中的文件是专门为开发环境设置的,而不是作为生产环境的配置。它暴露了 Redis 和 Jaeger 的端口,方便使用,因此在没有额外加固的情况下,不适合用于生产环境。然而,这使得它在调试和开发应用程序时非常方便。你甚至可以通过运行npm run dev
在本地工作站上运行 ShipIt Clicker 应用代码,并让它连接到 Docker 托管的 Redis 和 Jaeger 服务——这可能是尝试更改最快的方式。
你也可以在 Kubernetes 中安装 Jaeger,既可以在本地学习环境中安装,也可以在 AWS EKS Kubernetes 集群中安装。为此,我们将使用 Jaeger Operator。
安装 Jaeger Operator
我们已经了解了如何通过原始的docker
命令和docker-compose
在本地使用 Jaeger。接下来,我们将学习如何将 Jaeger 部署到 Kubernetes。Jaeger 的 Helm Charts(github.com/jaegertracing/helm-charts
)并没有完全得到支持,且在 Helm 3 中可能会出现问题。Jaeger 团队正在积极投资 Jaeger Operator,作为安装和维护该系统的主要方法。Kubernetes Operator是一种特殊类型的资源,它协调整个相关对象和配置的安装和维护,通常涉及复杂的分布式系统。
要部署到 Kubernetes 环境中,我们可以使用以下 GitHub 仓库作为指南:
github.com/jaegertracing/jaeger-operator
使用那里的kubectl
命令集来安装 Operator 命名空间及相关的 Kubernetes 对象。运行不仅是主要的kubectl
命令,还包括一组kubectl
命令,以通过角色绑定赋予 Operator 集群范围的权限。为了使 Jaeger 在所有命名空间中顺利运行,请编辑部署并从WATCH_NAMESPACE
变量中移除值:
kubectl -n observability edit deployment/jaeger-operator
文件中包含WATCH_NAMESPACE
部分的内容应该如下所示:
spec:
containers:
- args:
- start
env:
- name: WATCH_NAMESPACE
- name: POD_NAME
完成此步骤后,你可以安装 Jaeger Operator 实例,它将自动启动 Jaeger 的服务、Pod 和 DaemonSets。一个适用于开发或轻量级生产使用的示例 Operator 定义,使用 DaemonSet 在所有节点上部署 Jaeger 并仅使用内存存储跟踪数据,位于chapter10/jaeger.yaml
文件中。使用kubectl
进行安装:
kubectl apply -n observability -f chapter10/jaeger.yaml
这将安装所有必需的组件,包括一个没有任何注解的jaeger-query
Ingress Controller,因此 EKS 集群将不会将其连接到任何内容。有关具有注解的版本,请参见chapter10/jaeger-ingress.yaml
文件,该版本使用 ALB Ingress Controller 将其连接到互联网。你可以使用与其他 Kubernetes 服务和 Route 53 相同的基本程序,从 Kubernetes 暴露 Jaeger 控制台;或者,你也可以保持不变,仅在需要时通过端口转发连接到 Jaeger 控制台。
如果你在本地的 Kubernetes 学习环境中进行安装,你还可以将 NGINX Ingress Controller 注解添加到 Ingress Controller 中。
为了进一步扩展 Jaeger,你可能考虑添加一个存储后端,如 Cassandra 或 Elasticsearch,这样跟踪数据就可以在 Jaeger Pod 的生命周期之外持久化。我们将在此处暂时停止对 Jaeger 的讨论,但你可以自由深入探讨。
接下来,我们将回顾一下本章所学的内容。
总结
在本章中,你已经了解了关于可观测性的一切——如何使用 Kubernetes 原生方法以及 AWS 服务对 Docker 应用进行日志记录和监控。
你学习了如何将应用程序与常见服务(如 Redis)解耦,以提高生产就绪性。为了帮助故障排除和分析应用程序及系统问题,你学习了如何将日志扩展到 Kubernetes 集群中运行的容器之外,进入 AWS CloudWatch 和 S3,以及如何使用 CloudWatch Insights 和 AWS Athena 查询这些日志存储系统。你还了解到如何为应用程序添加更复杂的 Kubernetes 活跃性和就绪性探针,以及如何使错误处理更加健壮。
接下来,你学习了如何使用 Prometheus 从应用程序和支持系统中收集详细的指标,如何查询这些指标,以及如何使用 Prometheus Alertmanager 设置警报。Prometheus 和 Grafana 是相辅相成的;你发现了如何配置社区提供的 Grafana 仪表板,并如何添加一个显示应用程序特定指标的自定义仪表板。最后,你学习了如何使用 Jaeger 和 OpenTracing API 为应用程序添加追踪,通过使用开源中间件并明确标注应用程序,深入了解应用程序的性能。
在下一章中,我们将探索如何通过自动扩展来扩展应用程序,如何使用 Envoy 和断路器模式保护它免于过载,以及如何使用 k6 进行负载测试。
进一步阅读
你可以探索以下资源,以扩展你对可观测性、Kubernetes 日志、Prometheus 监控、Grafana、Jaeger 以及管理 Kubernetes 集群的知识:
-
可观测性简介:
docs.honeycomb.io/learning-about-observability/intro-to-observability/
。 -
使用 k9s 风格管理你的 Kubernetes 集群 – 一个快速简便的终端界面,类似于 Midnight Commander,是使用
kubectl
查询和控制 Kubernetes 集群的替代方案:k9scli.io/
。 -
Kail – Kubernetes 日志尾部工具:
github.com/boz/kail
。 -
Athena 入门:
docs.aws.amazon.com/athena/latest/ug/getting-started.html
。 -
使用 AWS Athena 查询 S3 文件中的数据:
towardsdatascience.com/query-data-from-s3-files-using-aws-athena-686a5b28e943
。 -
Kubernetes 入门 – 可观测性:你的应用程序健康吗? 活跃性和就绪性探针:
www.alibabacloud.com/blog/getting-started-with-kubernetes-%7C-observability-are-your-applications-healthy_596077
。 -
Kubernetes 存活探针和就绪探针:如何避免自毁前程:
blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/
-
超棒的 Prometheus 告警——不仅适用于 Kubernetes,还适用于 Prometheus 能监控的其他系统,且采用了知识共享许可协议:
awesome-prometheus-alerts.grep.to/rules
。 -
配置 Prometheus Operator Helm Chart 与 AWS EKS 的方法中包含了更多详细的 Alertmanager 配置示例:
medium.com/zolo-engineering/configuring-prometheus-operator-helm-chart-with-aws-eks-c12fac3b671a
。 -
分布式系统监控——来自 Google SRE 书中的内容——特别关注四个黄金信号:
landing.google.com/sre/sre-book/chapters/monitoring-distributed-systems/
。 -
如何在 Kubernetes 中监控黄金信号:
sysdig.com/blog/golden-signals-kubernetes/
。 -
PromQL 初学者教程:
medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085
。 -
理解 Prometheus 告警的延迟:
pracucci.com/prometheus-understanding-the-delays-on-alerting.html
。 -
Kubernetes 在生产环境中的监控——使用 Prometheus 监控资源指标的终极指南:
www.replex.io/blog/kubernetes-in-production-the-ultimate-guide-to-monitoring-resource-metrics
。 -
Kubernetes 与 Prometheus 监控——终极指南(第一部分)——是的,有多篇文章声称自己是终极指南,但这篇确实提供了详细信息,并且第二部分也涉及到 Grafana:
sysdig.com/blog/kubernetes-monitoring-prometheus/
。 -
Kubernetes:使用 Prometheus 监控——exporter、服务发现及其角色。包含一个关于设置 Redis exporter 的章节,您可以通过它更好地探索 ShipIt Clicker 的操作:
itnext.io/kubernetes-monitoring-with-prometheus-exporters-a-service-discovery-and-its-roles-ce63752e5a1
。 -
在 Prometheus 中利用 Deadman's Switch:
jpweber.io/blog/taking-advantage-of-deadmans-switch-in-prometheus/
(结合deadmanssnitch.com/
来创建完整的 Deadman's Switch 警报系统)。 -
在 Amazon CloudWatch 中使用 Prometheus 指标:
aws.amazon.com/blogs/containers/using-prometheus-metrics-in-amazon-cloudwatch/
。 -
通过计划的 Lambda 函数定期将 CloudWatch 日志导出到 S3 的替代方案:
medium.com/searce/exporting-cloudwatch-logs-to-s3-through-lambda-before-retention-period-f425df06d25f
。
第十一章:扩展和负载测试 Docker 应用
像 Google、Facebook、Lyft 和 Amazon 这样的科技巨头部分采用容器编排系统,是为了能够以非常高的利用率运行他们的海量计算资源。为了做到这一点,你必须有一种方法将你的应用扩展到一批服务器上,这些服务器可能是从云服务提供商动态分配的。即使你有一个能够在高流量时扩展、在需求减少时缩减的集群,你仍然可能需要额外的工具来确保其正确运行。你还需要确保如果超出容量限制,服务能够平稳降级。
你可以使用像 Envoy、Istio 或 Linkerd 这样的服务网格来处理这些问题。Envoy 是服务网格领域中较为简单的选项之一;它提供了负载均衡以及先进的流量路由和过滤能力。所有这些能力提供了所需的粘合剂,以便为高需求的用户提供流量。一些更复杂的服务网格使用 Envoy 作为构建块,因为它非常灵活。
为了证明扩展策略有效,你需要进行负载测试。为此,我们将使用 k6.io,一个云原生负载测试和 API 测试工具。
在本章中,你将学习如何使用 Horizontal Pod Autoscaler(水平 Pod 自动扩展器)、Vertical Pod Autoscaler(垂直 Pod 自动扩展器)和 Cluster Autoscaler(集群自动扩展器)来配置你的 Kubernetes 集群,以便实现扩展。你将了解 Envoy 以及为什么你可能需要使用它来提供一个代理层和服务网格,搭建在 Kubernetes 之上。这包括如何在 Kubernetes 集群之上创建一个 Envoy 服务网格,以及如何为其配置断路器。接下来,你将学习如何验证服务网格和自动扩展机制是否按预期工作。最后,你将学习如何使用 k6.io 进行负载测试,并观察当服务面临压力测试时如何失败。
本章将覆盖以下内容:
-
扩展你的 Kubernetes 集群
-
什么是 Envoy,为什么我可能需要它?
-
使用 k6 测试可扩展性和性能
技术要求
你需要拥有一个本地 Kubernetes 学习环境以及一个可用的云端 Kubernetes 集群,如在 第八章 中所述,将 Docker 应用部署到 Kubernetes。你还需要在本地工作站上安装当前版本的 AWS CLI、kubectl
和 helm
3.x,正如前一章所描述的那样。本章中的 Helm 命令使用的是 helm
3.x 语法。
对于本地 Kubernetes 学习环境,你应该配置一个可用的 NGINX Ingress Controller,你可以通过运行 chapter11/bin/
deploy-nginx-ingress.sh 脚本来安装它。你还需要有一个本地 Jaeger 操作符,你可以通过运行 chapter11/bin/deploy-jaeger.sh
脚本来安装它。
对于云托管的集群,您可以重用 AWS 的eksctl
。EKS 集群必须已设置并且工作正常的 ALB Ingress Controller。你还应该拥有一个用于云集群的 deploy-jaeger.sh
脚本。
查看以下视频,了解代码如何运行:
使用更新后的 ShipIt Clicker v8
我们将使用以下 GitHub 仓库中 chapter11
目录提供的 ShipIt Clicker 版本:github.com/PacktPublishing/Docker-for-Developers/
。
您使用的应用程序版本与上一章中的操作类似,在 Kubernetes 中使用时,依赖于通过 bitnami/redis
Helm Charts 安装的外部 Redis 版本。
理解与之前版本 ShipIt Clicker 的区别
在每一章中,我们都对 ShipIt Clicker 进行了增强,以展示与章节内容相关的更改,并且像生产发布过程中的修复一样完善应用程序。
这个版本的 ShipIt Clicker 与上一章中提供的版本类似,但它新增了一个名为/faults/spin
的 API 端点,作为故障注入测试策略的一部分,用于在运行应用程序的节点上诱发 CPU 负载,以测试集群自动扩缩容策略。spin
端点在调用频率增加时会变慢,但如果调用频率减少,它会恢复并变快。这模拟了一个性能较差的应用程序的行为,无需编写复杂的实际低效代码和数据库服务器。它提供了一种人工的 CPU 负载,便于进行基于 CPU 的自动扩缩容测试。请查看chapter11/src/server/common/spin.js
和chapter11/src/server/controllers/faults/controller.js
中的代码,了解其工作原理。
这个版本的 ShipIt Clicker 还增强了与 Prometheus 指标相关的功能:通过配置 Express 监听一个独立的端口,使其暴露这些指标,并提供 /metrics
端点。这样有助于避免暴露包含应用程序信息的指标,普通用户不需要这些信息,并且使得在与 ShipIt Clicker 同一个 Pod 中的多个容器也能暴露 Prometheus 指标。请查看 chapter11/src/server/index.js
文件中的代码,它增加了另一个 HTTP 监听器和一个用于指标的路由。chapter11/shipitclicker/templates/deployment.yaml
中的 Helm 模板也进行了更改,以支持这个新端点。
接下来,我们将把 ShipIt Clicker 构建并安装到本地 Kubernetes 学习环境中。
在本地安装 ShipIt Clicker 的最新版本
在本节中,我们将构建 ShipIt Clicker Docker 容器,给它打标签,并像前几章一样将其推送到 Docker Hub。执行以下命令,将 dockerfordevelopers
替换为你的 Docker Hub 用户名:
$ docker build . -t dockerfordevelopers/shipitclicker:1.11.7
$ docker push dockerfordevelopers/shipitclicker:1.11.7
$ kubectl config use-context docker-desktop
$ helm install --set image.repository=dockerfordevelopers/shipitclicker:1.11.7 shipit-v8 shipitclicker
使用 kubectl get all
检查正在运行的 pod 和服务,以验证 pod 是否正在运行,记下其名称,然后使用 kubectl logs
检查日志以查看启动日志。日志中不应有任何错误。
接下来,我们将在 EKS 中安装此版本。
通过 ECR 在 EKS 中安装 ShipIt Clicker 的最新版本
现在你已经构建了 Docker 容器并在本地安装了它,我们将在 AWS EKS 中通过 ECR 安装它。编辑 chapter11/values.yaml
文件,为其在 Route 53 DNS 区域中设置一个主机名,例如 shipit-v8.eks.example.com(将 ECR 引用替换为与你的 AWS 账户和区域对应的引用,并将 example.com
替换为你的域名):
$ docker tag dockerfordevelopers/shipitclicker:1.11.7 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:1.11.7
$ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 143970405955.dkr.ecr.us-east-2.amazonaws.com
$ docker push 143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:1.11.7
$ kubectl config use-context arn:aws:eks:us-east-2:143970405955:cluster/EKS-8PWG76O8
$ helm install shipit-v8 -f values.yaml --set image.repository=143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:1.11.7 ./shipitclicker
检查 Kubernetes 日志,确保应用程序已成功部署到集群中:
kubectl logs services/shipit-v8-shipitclicker shipitclicker
如果部署一切正常,获取 AWS ALB Ingress 控制器的地址,如 第九章 中所述,使用 Spinnaker 的云原生持续部署,并在 Route 53 控制台中为已部署的应用程序创建 DNS 条目,使用 ALB 地址。然后你应该能够通过类似 shipit-v8.eks.example.com/
的 URL 访问你的应用程序(将 example.com
替换为你的域名)。
扩展你的 Kubernetes 集群
为了支持更多的流量和更多的应用程序,你的 Kubernetes 集群可能需要扩大其初始规模。你可以使用手动方法和动态编程方法来实现这一点,特别是当你在使用基于云的 Kubernetes 集群时。要扩展一个应用程序,你需要控制两个维度:运行特定应用程序的 pod 数量和集群中的节点数量。在具有相同节点数量的集群中,你不能无限扩展 pod 数量;与 CPU、内存和网络相关的实际限制最终会要求集群也要扩展节点数量。
扩展集群的方式会有很大差异,具体取决于云服务提供商和 Kubernetes 分发版。Kubernetes 文档解释了通用流程,并提供了一些针对在 Google 和 Microsoft Azure 云中运行的集群的具体说明:
kubernetes.io/docs/tasks/administer-cluster/cluster-management/
一般来说,你必须启动并配置一台新服务器,该服务器的配置应与现有集群节点类似,然后通过 kubeadm join
命令将其加入集群:
kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-join/
Kubernetes 发行版和云服务提供商通过依赖机器镜像和自动扩展组等机制使这一过程变得更加简便。我们将向您展示如何通过 Amazon EKS 扩展您的集群。在 第八章,将 Docker 应用程序部署到 Kubernetes 中,我们在 使用 CloudFormation 启动 AWS EKS 部分中设置了 EKS 与 AWS Quick Start CloudFormation 模板。以下部分假设您已经使用该方法设置了一个使用自动扩展组的集群。
手动扩展集群
鉴于我们想要增加集群中的节点数量,我们将需要识别并遵循针对我们的 Kubernetes 安装的特定程序。对于 Amazon EKS 集群,请参阅以下文档:
docs.aws.amazon.com/eks/latest/userguide/launch-workers.html
您也可以启动一个全新的节点组,但通常只需要调整一个或两个参数就能增加集群的大小。这种方法用于扩大集群的规模,称为 扩展,而当缩小集群的规模时,称为 缩减。接下来,我们将学习如何调整一个简单的参数,以便扩展集群中的节点数量。
手动扩展节点
为了简化,我们假设您最初使用了 AWS Quick Start 的 EKS CloudFormation 模板来创建您的集群。由于该方法使用 CloudFormation 来管理集群,因此您应该优先使用 CloudFormation 来更新集群的配置。要手动扩展您的集群,请进入 AWS 控制台,更新 CloudFormation 部署,将 节点数量 和 最大节点数量 的默认值从当前值修改为更高的值,如 4 和 8:
图 11.1 – 更新 AWS EKS Quick Start CloudFormation 模板
继续完成 CloudFormation 更新表单并应用更改。查看 CloudFormation 事件以获取更新,并等待几分钟。然后,您可以检查 CloudFormation 模板的更新是否顺利完成。接着,您可以检查自动扩展组的大小,确保其已经增加。
您还可以通过 EC2 控制台更新自动扩展组的大小,分别将最小、期望和最大节点数设置为 4、4 和 8。但是,这会导致您的部署配置与 CloudFormation 模板产生偏差,这是不理想的,因为实际状态将与 CloudFormation 期望的模型不再匹配。有关此问题的更多信息,请参见以下帖子:aws.amazon.com/blogs/aws/new-cloudformation-drift-detection/
。
如果你使用 eksctl
创建了集群,你可以按照 eksctl.io/usage/managing-nodegroups/
中的说明扩展其创建的节点组。
手动扩展节点
你可以逆向操作来缩减集群(减小其规模),但请注意,手动缩减集群规模更加复杂。安全地进行此操作需要一个叫做“驱逐”的过程,详情请参阅以下 Kubernetes 文档:kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/
。仅更改自动扩展组的大小会终止实例,而不允许你选择终止哪个实例或给你机会去驱逐该实例。如果你真的想这样做,你必须完成以下所有步骤:
-
将自动扩展组的最小规模减少一个。
-
使用
kubectl drain
命令驱逐节点。 -
使用 AWS CLI 命令终止节点,该命令会减少期望容量。
调整完自动扩展组的最小规模后,你可以执行以下命令(在每个命令中替换为要终止的节点的节点名称和实例 ID):
$ kubectl drain \
ip-10-0-94-28.us-east-2.compute.internal \
--ignore-daemonsets
$ aws autoscaling terminate-instance-in-auto-scaling-group \
--instance-id i-09c88021d2324e821 \
--should-decrement-desired-capacity
这个过程涉及较多,容易导致手动错误。它还可能导致与 CloudFormation 模板的配置漂移,因此你应该考虑编写脚本,或者依赖自动扩展机制。
通过部署手动扩展 Pod
手动扩展部署或 ReplicaSet 中的 Pod 数量相对简单,只要集群中有足够的资源。你可以使用 kubectl scale
命令来设置副本数量。你可能需要执行几个 kubectl get
命令,直到你看到所有副本都变为就绪状态,如以下转录所示:
$ kubectl get deployment/shipit-v8-shipitclicker
NAME READY UP-TO-DATE AVAILABLE AGE
shipit-v8-shipitclicker 2/2 2 2 57m
$ kubectl scale deployment/shipit-v8-shipitclicker --replicas=4
deployment.apps/shipit-v8-shipitclicker scaled
$ kubectl get deployment/shipit-v8-shipitclicker
NAME READY UP-TO-DATE AVAILABLE AGE
shipit-v8-shipitclicker 2/4 4 1 58m
$ kubectl get deployment/shipit-v8-shipitclicker
NAME READY UP-TO-DATE AVAILABLE AGE
shipit-v8-shipitclicker 4/4 4 4 59m
接下来,我们将探讨如何将程序化扩展应用于集群,既包括节点,也包括 Pod。
动态扩展集群(自动扩展)
现在,经过前面三章的练习,你已经探讨了 Kubernetes 容器编排系统相关的复杂概念,你可能会想:所有这些努力值得吗?在本节中,我们将探讨一个关键特性,它可以让管理这些系统的痛苦变得值得——自动扩展。通过动态扩展集群中的应用程序和集群本身,你可以实现集群资源的高利用率,这意味着你需要更少的计算机(无论是虚拟的还是物理的)来运行你的系统。当你将动态扩展与 Kubernetes 系统的自愈能力结合时,即使在某些领域具有较高的复杂性和学习曲线,这一特性仍然非常吸引人。
Kubernetes 支持几种动态扩展机制,包括集群自动扩展器、水平 Pod 自动扩展器和垂直 Pod 自动扩展器。我们来一一探讨这些机制。
配置 Cluster Autoscaler
kube-system
命名空间使用云 API 来启动和终止节点。
如果您使用 AWS EKS 快速启动 Cloudformation 模板来创建集群并告诉它启用 Cluster Autoscaler,则无需进一步配置。如果您使用 eksctl
或其他方法创建集群,则可能需要使用此处提供的说明进一步配置:docs.aws.amazon.com/eks/latest/userguide/cluster-autoscaler.html
。
您可以通过查询来验证 Cluster Autoscaler 是否正在运行:
$ kubectl -n kube-system get deployments | grep autoscaler
cluster-autoscaler-1592701624-aws-cluster-autoscaler 1/1 1 1
现在我们已经了解了一些有关 Cluster Autoscaler 的信息,让我们发现如何配置应用程序以利用其功能。
配置无状态应用程序以与 Cluster Autoscaler 协同工作
无状态应用程序(例如 ShipIt Clicker)可以容忍启动和停止其任何一个 pod,并且可以在集群中的任何节点上运行。它不需要特殊配置即可与 Cluster Autoscaler 协同工作。需要挂载本地存储和某些其他类别应用程序的有状态应用程序,在可能的情况下必须避免某些扩展操作,并可能需要特殊处理。有关详细信息,请参阅 Autoscaling FAQ:github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md
。
您可以通过 PodDisruptionBudget(PDB)来提示 Cluster Autoscaler 不应缩减超出某一点的 pod 数量,并且它应努力保持一定数量或百分比的健康 pod 可用性:kubernetes.io/docs/tasks/run-application/configure-pdb/
。
我们已经在其 Helm Chart 中使用 PDB 配置了 ShipIt Clicker。有关更多信息,请参见 chapter11/src/shipitclicker/templates/pdb.yaml
。您可以在 chapter11/src/shipitclicker/values.yaml
中找到其默认值。默认情况下,ShipIt Clicker 现在配置为部署两个 pod,并且具有至少一个可用 pod 的 PDB。这为 Cluster Autoscaler 和其他 Kubernetes 应用程序提供了提示,表明即使在进行节点维护时,也应始终保持至少一个 pod 处于运行状态。
接下来,我们将演示 Cluster Autoscaler 的运行情况。
演示 Cluster Autoscaler 的运行情况
为了让 Cluster Autoscaler 更改集群大小,我们可以启动超过其当前处理能力的更多 pod。要观察此过程的执行过程,可以尾随 cluster-autoscaler
服务的日志。打开终端窗口并运行以下命令以尾随服务的日志:
$ service=service/$(kubectl get services -n kube-system \
| awk '/cluster-autoscaler/{ print $1 }')
$ kubectl logs -f -n kube-system "$service"
每 10 秒,您将看到日志条目,指示服务正在寻找不可调度的 pod(这将导致集群扩展节点的数量),以及符合缩放条件的节点。
然后,在另一个终端窗口中,手动将 ShipIt Clicker 的部署扩展到 50
个 pod:
kubectl scale deployment/shipit-v8-shipitclicker --replicas=50
默认 EKS 集群中的每个t3.medium
节点大约可以处理 4 到 16 个 ShipIt Clicker pod,具体取决于每个节点上运行的其他 pod 数量。这会触发集群自动扩展器,并使其至少扩展一个额外的节点。你将会在集群自动扩展器的日志中看到它已发现无法调度的 pod,并且很快,它会完成扩展操作。
要从节点和 pod 的角度查看进度,可以每隔几秒钟执行以下命令:
kubectl get nodes; kubectl get deployments
你将看到节点启动并且越来越多的副本变为就绪状态,直到副本集稳定。当这发生时,将其缩减回一个较低的默认状态:
kubectl scale deployment/shipit-v8-shipitclicker --replicas=2
完成操作后,你可能会注意到节点不会立即缩减,因为在扩展操作完成后,节点会进入一个 10 分钟的冷却状态。然而,在冷却期结束后的一分钟,集群自动扩展器会发现这些节点的 CPU 使用率接近零,然后它会对集群进行缩减,终止那些不再有 pod 可用的节点。集群自动扩展器在执行缩减操作时会遵循 PDB 策略——这允许你在缩减集群中的 pod 和节点数量时,根据需求谨慎操作。
现在你已经学会了如何使用集群自动扩展器来扩展和缩减集群节点,接下来让我们学习如何使用 Horizontal Pod Autoscaler 设置扩展策略。
配置 Horizontal Pod Autoscaler
Horizontal Pod Autoscaler 允许你设置规则,通过考虑 CPU 使用率或其他自定义指标来扩展 Kubernetes pod 集合。这个服务还可以扩展由部署、ReplicaSets 和复制控制器管理的 pod。你可以在这里阅读更多关于它如何工作的理论:kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
。
这是你实现一个可以根据需求自动扩展和缩减的集群之前需要解决的最后一个关键步骤。
你需要安装 Metrics Server,以使 Horizontal Pod Autoscaler 正常工作。我们接下来将进行安装。
安装 Metrics Server
若要在你的 Kubernetes 集群中获取更详细的统计信息,供支持动态扩展的组件使用(包括 Horizontal Pod Autoscaler),你需要运行标准的Metrics Server。它会聚合集群中关于节点的内存、CPU 以及其他资源利用率的统计信息,并以 Kubernetes 各种自动扩展机制可以理解和操作的格式提供这些数据。AWS EKS 的指南中有关于如何安装的介绍,详情见此:
docs.aws.amazon.com/eks/latest/userguide/metrics-server.html
要安装它,确保你的 kubectl config
上下文已设置为你的云集群。然后,从你的本地工作站发出以下命令:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.3.6/components.yaml
安装 Metrics Server 后,验证它是否正在运行:
$ kubectl -n kube-system get deployment metrics-server
NAME READY UP-TO-DATE AVAILABLE AGE
metrics-server 1/1 1 1 6m
接下来,我们将为 ShipIt Clicker 应用启用 Horizontal Pod Autoscaler,以演示它是如何工作的。
启用 Horizontal Pod Autoscaler
AWS EKS 指南展示了安装 Horizontal Pod Autoscaler 所需的步骤:docs.aws.amazon.com/eks/latest/userguide/horizontal-pod-autoscaler.html
。
我们需要安装的主要组件是指标服务。事实证明,Horizontal Pod Autoscaler 已经内置在 Kubernetes 中。我们可以发出如下命令,启用某个部署的 Horizontal Pod Autoscaler:
kubectl autoscale deployment shipit-v8-shipitclicker --cpu-percent=50 --min=2 --max=50
如果你需要编辑这些参数,可以使用以下命令:
kubectl edit hpa/shipit-v8-shipitclicker
你可以通过发出以下命令,详细查看 Horizontal Pod Autoscaler 最近执行的操作:
kubectl describe hpa/shipit-v8-shipitclicker
为了测试 Horizontal Pod Autoscaler 和 Cluster Autoscaler 是否按预期工作,我们需要施加 CPU 负载。这时,/faults/spin
端点就派上用场了。在本章稍后的 使用 k6 测试可扩展性和性能 部分,我们将看到如何为 ShipIt Clicker 应用构建一个真实的负载测试。然而,为了验证自动扩缩容,我们将使用一个暴力方法,通过 Docker 运行 Apache Bench 工具(将 example.com
替换为你的域名):
$ url=https://shipit-v8.eks.example.com/faults/spin
$ docker run --rm jordi/ab -c 50 -t 900 "$url"
使用 kubectl get deployments
、kubectl get pods
、kubectl get nodes
和 kubectl describe hpa
命令反复执行,观察部署副本的增长。或者,使用 Kubernetes 监控工具,例如 k9s(k9scli.io/
)来观察 pod 和节点数在最初 10 分钟内的增长,然后在接下来的 15 分钟内逐渐减少。你也可以查看上一章中描述的 Grafana 仪表盘和 Jaeger 跟踪,以查看集群如何处理负载,或者查看在 EC2 控制台中出现的 CloudWatch 指标,这些是针对活动节点的。
接下来,我们将考虑何时使用 Vertical Pod Autoscaler。
配置 Vertical Pod Autoscaler
Vertical Pod Autoscaler 是一种较新的扩展机制,它观察 pod 请求的内存和 CPU 使用量,以及实际使用的量,从而优化内存和 CPU 请求——它执行右-sizing,以提升集群利用率。这是最适合有状态 Pod 的扩展机制。
然而,Vertical Pod Autoscaler 的文档目前指出,它与 Horizontal Pod Autoscaler 不兼容,因此你应避免配置它以管理相同的 Pods。你可以尝试将它用于你的应用,但请记住文档中关于避免将其与 Horizontal Pod Autoscaler 使用 CPU 指标进行混合的建议。Vertical Pod Autoscaler 的安装过程也比配置其他自动扩展器要复杂,因此我们不会在此详细展示所有步骤——请参考 Vertical Pod Autoscaler 文档获取详细配置说明:github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler
。
在本节中,我们学习了如何使用手动和动态方法来扩展我们的应用程序。在下一节中,我们将了解 Envoy,它是一个服务网格,提供一些关于 Kubernetes 集群中 Pod 通信的高级控制和安全性。
什么是 Envoy,为什么我可能需要它?
Envoy (www.envoyproxy.io/
) 是一个 C++ 开源的 服务网格 和边缘代理,专为微服务部署而设计。由 Lyft 团队开发,它特别适用于开发基于 Kubernetes 托管的应用程序的团队,正如本书中你所看到的应用。
那么,为什么我们需要部署 Envoy 呢?在开发基于云的生产系统时,使用多个容器来托管分布式服务,你会遇到许多与可观察性和网络相关的问题。
Envoy 旨在通过引入一个代理服务来解决这两个问题,该服务提供可运行时配置的网络和指标收集功能,可以作为创建更高级别系统的构建模块,帮助管理这些问题。无论你是在构建一个小型的分布式应用,还是一个围绕服务网格模型设计的大型微服务架构,Envoy 的功能使我们能够以云平台无关的方式抽象出网络这一棘手的问题。
Lyft 团队使用以下概念开发了 Envoy:
-
进程外架构:Envoy 是一个自包含的进程,可以与现有应用程序一起部署。
-
localhost
并且对网络拓扑一无所知。采用 L3/L4 过滤架构用于网络代理。你可以向代理中添加自定义过滤器,以支持诸如 TLS 客户端证书认证等任务。 -
语言无关性:Envoy 支持多种语言,并允许你混合使用不同的应用框架。例如,通过使用 Envoy 的 PHP 和 Python,容器化应用程序可以互相通信。
-
HTTP L7 过滤器和路由:与 L3/L4 过滤器一样,L7 层也支持过滤。这允许开发用于不同任务的插件,从缓冲到与 AWS 服务(如 DynamoDB)交互。Envoy 的路由功能允许你部署一个路由子系统,根据多种标准(如路径和内容类型)重定向请求。
-
负载均衡和前端/边缘代理支持:Envoy 支持高级负载均衡技术,包括自动重试、断路器、健康检查和限流。此外,你可以在网络边缘部署 Envoy 以处理 TLS 终止和 HTTP/2 请求。
-
可观察性和透明性:Envoy 收集统计数据以支持应用层和网络层的可观察性。你可以将 Envoy 与 Prometheus、Jaeger、Datadog 和其他支持指标和追踪的监控平台结合使用。
让我们更详细地探索一下 Envoy 的一些功能,以便更好地理解这些概念。
使用 Envoy 服务网格进行网络流量管理
你应该已经熟悉负载均衡器的概念,它是一种网络流量管理器。但究竟什么是服务网格?为什么你需要使用它?Envoy 如何在这方面帮助我们?
服务网格是一个基础设施层,专门用于处理服务间通信,通常通过代理服务实现。使用服务网格的好处如下:
-
对网络通信的透明性和可观察性。
-
你可以支持网络上的安全连接。
-
指标收集,包括服务失败时重试成功所需的时间。
-
你可以将代理部署为边车。这意味着它们与每个服务并行运行,而不是嵌入其中。反过来,这使我们能够将代理服务与应用程序本身解耦。
四个应用程序的服务网格示例可以如下可视化:
图 11.2 – 一个包含四个微服务和边车代理的服务网格示例
在这里,我们的每个容器化应用程序都有一个相应的边车代理。应用程序与代理进行通信,代理则通过服务网格与我们托管的其他容器化服务进行通信。应用程序并不知道代理的存在,也不需要任何修改即可与代理协作。所有配置都可以通过容器编排系统将端口连接起来,方式对应用程序是不可见的。
现在,让我们动手实际操作,启动并运行 Envoy。
设置 Envoy
由于 Envoy 的架构,你在部署软件时具有灵活性:
-
明确配置为边车容器,使用静态配置文件,与应用容器并排部署
-
动态配置作为服务网格控制平面的一部分,容器可能作为组件被注入到 Kubernetes pod 中,使用如 Istio (
istio.io/
) 或 AWS App Mesh (aws.amazon.com/app-mesh/
) 等软件。
第二种选择提供了更大的功能,但增加了较大的复杂性。
Envoy 的示例配置(参见 www.envoyproxy.io/docs/envoy/latest/start/start#sandboxes
)都属于第一类,具有明确的 Envoy 代理配置。要了解 Envoy,考虑这些明确的配置示例更为简单。本章提供的 ShipIt Clicker 版本已经被修改,可以在 Kubernetes 部署时通过静态配置文件添加一个 Envoy sidecar 容器,采用简化的方式展示 Envoy 的特性。
为 Envoy 配置 ShipIt Clicker
现在,让我们来检查一下为了支持 Envoy 在 ShipIt Clicker 中所需进行的具体更改。应用程序的 JavaScript 代码不需要任何更改;所有更改都在 Helm Charts 中。查看 chapter11/shipitclicker
中的 Helm Charts,并与 chapter10/shipitclicker
中的进行对比;你将看到在 chapter11/shipitclicker/templates/deployment.yaml
中定义了一个新的 Envoy sidecar 容器,使用在 chapter11/shipitclicker/values.yml
中定义的镜像进行配置:
- name: envoy-sidecar
image: "{{ .Values.envoy.repository }}"
imagePullPolicy: {{ .Values.envoy.pullPolicy }}
command: ["/usr/local/bin/envoy"]
args: ["-c", "/etc/envoy-config/config.yaml"]
模板中的前几行使用配置文件 /etc/envoy-config/config.yaml
启动 Envoy 容器,该配置文件在 ConfigMap 中定义。Envoy 需要为其管理或代理的每个服务提供端口定义,包括其管理界面的端口定义:
ports:
- name: envoy-admin
containerPort: 9901
protocol: TCP
- name: envoy-http
containerPort: 4000
protocol: TCP
我们可以查询管理 API,确保 Envoy 处于活动状态并准备好接受流量,符合 Kubernetes 的最佳实践:
livenessProbe:
httpGet:
path: /server_info
port: envoy-admin
readinessProbe:
httpGet:
path: /ready
port: envoy-admin
为了将配置文件暴露给容器,我们使用一个卷挂载来暴露 config.yaml
文件:
volumeMounts:
- name: envoy-config-vol
mountPath: /etc/envoy-config/
volumes:
- name: envoy-config-vol
configMap:
name: {{ .Release.Name }}-envoy-sidecar-configmap
items:
- key: envoy-config
path: config.yaml
config.yaml
文件在 chapter11/shipitclicker/templates/configmap-envoy.yaml
中定义,并为以下内容定义了监听器和集群:
-
一个为 pod 内部的 ShipIt Clicker 容器提供的入口代理。
-
为 Redis Kubernetes 服务配置的出口代理,可以在集群中的
redis-master
处访问。 -
一个入口代理,允许 Prometheus 从 pod 中的 Envoy sidecar 拉取指标。
chapter11/shipitclicker/templates/configmap.yaml
中的 ShipIt Clicker 的 ConfigMap 已被修改,以便它连接到 localhost:6379
的 Redis,Envoy 监听此端口并通过 TCP L4 代理将其代理到 Redis 服务。该服务在集群中的其他地方的 redis-master:6379
处进行监听。
chapter11/shipitclicker/templates/service.yaml
中的 Kubernetes 服务现在调用 envoy-http
端口,而不是直接调用应用程序容器的端口。
为什么不使用 Envoy Redis 协议代理?
此处使用的示例文件使用的是普通的 TCP 代理,而不是 Envoy 的 Redis 协议代理(请参见 www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/redis_proxy/v3/redis_proxy.proto
和 github.com/envoyproxy/envoy/tree/master/examples/redis
)。
这是因为 ShipIt Clicker 应用程序设置了 Redis 密码认证,而该认证与 Envoy 的 Redis 代理不兼容。ShipIt Clicker 被配置为使用从 Kubernetes Secret 中检索的密码,而该密码由 Bitnami Redis Helm Chart 存储。然而,Envoy 不会通过该密码;当配置为使用 Redis 协议代理时,ShipIt Clicker 尝试进行身份验证时,出现了错误信息:警告:Redis 服务器不需要密码,但提供了密码
。事实证明,如果使用 Envoy Redis 协议支持,则必须通过存储在 ConfigMap 中的配置文件为客户端(以及可选的服务器)配置代理本身的密码认证。然而,Bitnami Redis 服务器使用的密码仅作为 Kubernetes 秘密存在,因此重新构建系统以支持这一点会增加复杂性。
作为练习,如果你愿意,可以安装没有密码的 Redis,并从 ShipIt Clicker 的配置中移除密码。如果你这么做了,你还可以将 Redis 实现切换到 Bitnami Redis Cluster Helm Chart(请参见 github.com/bitnami/charts/tree/master/bitnami/redis-cluster
),然后使用 Envoy 支持 Redis 集群来实现读写分离模式。
到目前为止,我们已经看到了如何部署 Envoy 来创建服务网格。接下来,我们将探索电路断路器模式。
配置 Envoy 支持电路断路器模式
电路断路器模式是一种配置失败阈值的机制。其目标是防止故障蔓延到你的微服务平台,并停止持续请求一个未响应的服务。
在 Envoy 上配置该模式相对简单。我们可以通过 circuit_breakers
字段,将电路断路值配置为 Envoy 集群定义的一部分。
要查看其工作原理,请查看以下 ConfigMap 文件,其中包含了电路断路器的定义(chapter11/shipitclicker/templates/configmap-envoy.yaml
):
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: {{ .Values.envoy.maxRequests }}
max_pending_requests: {{ .Values.envoy.maxRequests }}
max_requests: {{ .Values.envoy.maxRequests }}
max_retries: {{ .Values.envoy.maxRetries }}
这个阈值定义指定了 Envoy 将建立的最大连接数和最大并发请求数。在我们的示例中,我们有一个默认优先级阈值配置,以及一个用于高优先级(用于 HTTP 1.1)和最大请求数(用于 HTTP/2)的第二个阈值。如果 Envoy 检测到的流量超过这些阈值,它将抛出错误并拒绝请求,而不会将请求传递到目标服务。请注意,由于我们使用 Helm Charts,我们通过 Helm 模板变量替换指定实际值,这些值来自 chapter11/shipitclicker/values.yaml
或 Helm Chart 值的某个覆盖机制。默认值来自 values.yaml
文件中指定 Envoy 特定值的部分:
envoy:
repository: envoyproxy/envoy:v1.14.2
pullPolicy: IfNotPresent
accessLog: "/dev/null"
maxRequests: 1024
maxRetries: 2
这些默认值适用于此应用程序的生产环境,但我们如何测试断路器是否有效,而不产生巨大的负载呢?我们接下来会展示如何做到这一点。
测试 Envoy 断路器
为了测试 Envoy 断路器是否正常工作,我们将把 ShipIt Clicker 部署到云 Kubernetes 集群中,设置一个人为降低的请求限制,并执行快速负载测试以验证其是否有效。发出 Helm upgrade
命令,随后执行 kubectl rollout restart
命令,类似以下内容,以将最大并发请求设置为 10
(将 image.repository
替换为你的 ECR 仓库引用):
$ helm upgrade shipit-v8 -f values.yaml --set image.repository=143970405955.dkr.ecr.us-east-2.amazonaws.com/dockerfordevelopers/shipitclicker:1.11.7 --set envoy.maxRequests=2 ./shipitclicker
Release "shipit-v8" has been upgraded. Happy Helming!
NAME: shipit-v8
LAST DEPLOYED: Sun Jun 28 22:34:15 2020
NAMESPACE: default
STATUS: deployed
REVISION: 17
NOTES:
1\. Get the application URL by running these commands:
http://shipit-v8.eks.example.com/*
$ kubectl rollout restart deployment/shipit-v8-shipitclicker
deployment.apps/shipit-v8-shipitclicker restarted
接下来,我们将使用 Apache Bench 测试已部署的应用程序,从单个并发请求开始:
$ url=https://shipit-v8.eks.example.com/faults/spin
$ docker run --rm jordi/ab -c 1 -n 400 $url | grep requests:
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Finished 400 requests
Complete requests: 400
Failed requests: 0
在这里,你可以看到当只运行一个并发请求时,所有请求都成功。接下来,我们将把并发增加到 50
个并发连接:
$ docker run --rm jordi/ab -c 50 -n 400 $url | grep requests:
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Finished 400 requests
Complete requests: 400
Failed requests: 72
如果我们将并发设置为 50
个并发请求,其中许多将因断路器触发而失败。我们已经看到如何为我们的集群设置一个具有两个阈值的基本断路器。还有更高级的断路器模式,包括基于延迟和重试的断路器。如果你认为你的应用程序需要,你可以进一步探索这些模式。
现在你已经用低连接阈值测试过了断路器,请将阈值恢复到原始值,并重新部署应用程序,以便为更多负载测试做好准备。
如果我们能很好地衡量每个 Pod 在不失败的情况下能够处理多少真实用户流量,我们可以用它来为断路器设置一个更好的值。然而,Apache Bench 是一个笨重的工具,它不能让我们模拟现实的用户负载。为此,我们需要使用一个更复杂的负载测试框架。现在,我们将看看如何使用 k6 来测试可扩展性,k6 是一个基于 Docker 的负载测试框架。
使用 k6 测试可扩展性和性能
k6 框架(k6.io
)是一个可编程的开源负载测试工具。我们将向你展示如何使用它生成比简单负载生成器(如Apache Bench(ab))更真实的负载模式。
这个框架由于其 Docker 镜像的存在,设置和使用起来相当简单,该镜像可以在 Docker Hub 上找到。你可以在k6.io/docs/getting-started/running-k6
找到快速开始说明。
要使用 k6 创建负载测试,你需要使用 JavaScript 并调用 k6 的库函数。要进行冒烟测试,你的脚本大致需要如下所示:
import http from 'k6/http';
export default function() {
http.get('https://shipit-v8.eks.example.com/');
}
这个脚本大致相当于使用ab
工具对 Web 服务器进行压力测试。创建一个名为hello.js
的文件,使用前面的源代码,并将shipit-v8.eks.example.com
替换为你网站的完全限定域名。
遵循 Docker 最佳实践,你应该确保在 Docker 命令行中添加--rm
标志,以避免在本地安装中积累过时的容器。
$ docker run --rm -i loadimpact/k6 run - < hello.js
这将运行 k6 并获取hello.js
中指定的 URL。
有几个关键概念是你必须了解的:
-
你必须提供一个默认函数。
-
K6 是不是Node.js,它没有事件循环。
-
你的默认函数被称为虚拟用户(VU)。
-
在默认函数外定义的代码会在程序启动时执行一次。
-
默认函数会重复执行,直到测试结束。
-
你可以根据需要使用任意数量的虚拟用户,并运行任意时长。
注意
k6 提供了许多命令行选项,可以用来在一段时间内逐步增加或减少虚拟用户(VU),以及指定测试的运行时长和要模拟的虚拟用户数量。默认设置只有一个虚拟用户(VU),并且只有一个测试迭代。
让我们使用一些选项来用更多的用户并更长时间运行测试:
$ docker run --rm -i loadimpact/k6 run --vus 50 --duration 30s - < hello.js
以这种方式运行 k6 将执行与 Apache Bench 负载测试几乎相同的负载测试,默认并发为50
,持续时间为30
秒。
然而,由于你可以使用 JavaScript 的全部功能,你可以利用多种策略编写更细致的负载测试。
记录并重放网络会话
编写类似hello.js
的脚本的替代方案是使用记录和重放策略。许多负载测试框架,包括 k6,都支持这种范式。为此,使用 Chrome 浏览器及其检查功能。你可以使用调试器的网络选项卡来捕获并保存与应用程序后端之间的网络流量。
你从调试器中的空网络历史记录开始。然后,加载并开始游戏。每次点击都会触发应用程序和后端之间的 API 请求。
当你对录制的内容满意时,右键点击网络面板,选择复制所有内容为 HAR。这将把 HAR 格式的文本放入系统剪贴板:
图 11.3 – Google Chrome 检查器调试控制台 – 复制全部为 HAR
从剪贴板粘贴到一个名为chapter11/src/test/k6/session.har
的文件中。然后,运行一个转换脚本,将 HAR 文件转换为位于chapter11/src/test/k6/har-session.js
的 JavaScript 文件,并运行另一个 shell 脚本,通过 Docker 和正确的参数运行 k6,启动一个 60 秒、1 个用户的测试:
$ chapter11/bin/k6-convert-har.sh
$ chapter11/bin/k6-run-har.sh
k6-run-har.sh
脚本设置了使用环境变量覆盖 VUs 的USERS
变量,并使用DURATION
变量覆盖测试时长。因此,你可以像这样为脚本加上前缀,并运行一个持续300
秒的 10 用户测试:
$ USERS=10 DURATION=300 chapter11/bin/k6-run-har.sh
但是,使用这种播放和记录策略时有一些细节需要注意:该过程是非常字面意义上的,导致生成的文件在请求之间没有延迟。运行该测试会在目标服务上产生巨大的机器速度负载。请求之间的延迟没有随机化,这是你需要做的,以便更真实地模拟一个真实用户会对服务造成的负载。
为了创建一个更真实的测试,我们需要进行一些 JavaScript 编程。
手工制作一个更真实的负载测试
在chapter11/src/tests/k6/
目录下,有一个test.js
脚本,旨在真实地测试 ShipIt Clicker,无论它是部署在本地还是在云端。
这个脚本通过以下策略模拟一个人类玩游戏的过程:
-
获取构成应用程序的 HTML、样式表、图像和 JavaScript 文件
-
执行 HTTP POST 以开始一个新游戏
-
获取初始得分、部署次数和
nextPurchase
值 -
尝试模拟人类玩家点击流的行为
这些 HTTP 请求是通过在浏览器(如 Google Chrome)中玩游戏时,通过其检查功能并查看网络选项卡来识别的,在游戏加载并开始时记录请求。然后,我们编写了一个测试,模拟了一系列请求,尽可能贴近真实用户行为,包括加入了真实的随机延迟。
让我们检查一下chapter11/src/test/k6/test.js
中的代码。这里,我们从 k6 提供的库中导入了http
类和sleep()
方法:
import http from "k6/http";
import { sleep } from "k6";
我们通过环境变量将参数传递给test.js
脚本:
-
DEBUG
环境变量让我们触发更详细的日志记录。 -
MOVES
环境变量包含每个游戏的移动次数。 -
TARGET
环境变量类似于http://192.2.0.10:3011
,用于localhost
开发,其中192.2.0.10
是你的工作站的 IPv4 局域网地址。
这些参数从__ENV
对象中获取,如下所示:
const DEBUG = __ENV.DEBUG;
const MOVES = __ENV.MOVES;
const target = __ENV.TARGET;
ENDPOINTS
数组用于迭代游戏跟踪的三个主要元素:
const ENDPOINTS = ['score', 'deploys', 'nextPurchase'];
deploy()
方法模拟人类点击http.patch()
两次——一次更新部署次数,一次更新得分:
const deploy = id => {
validate(
http.patch(
`${target}/api/v2/games/${id}/deploys`,
JSON.stringify({
id: id,
element: 'deploys',
value: 1,
}),
params
)
);
这个函数还会更新分数:
validate(
http.patch(
`${target}/api/v2/games/${id}/score`,
JSON.stringify({
id: id,
element: 'score',
value: 1,
}),
params
)
);
};
validate()
方法是 deploy()
方法调用的,用于简单验证服务器是否返回有效响应:
validate(
http.patch(
`${target}/api/v2/games/${id}/score`,
JSON.stringify({
id: id,
element: 'score',
value: 1,
}),
params
)
);
};
getStaticAssets()
方法模拟了用户浏览器获取构成游戏的 HTML、CSS、图片和 JavaScript 文件:
const getStaticAssets = () =>
[
target,
`${target}/stylesheet.css`,
`${target}/img/shipit-640x640-lc.jpg`,
`${target}/img/Richard-Cartoon-Headshot-Jaunty-180x180.png`,
`${target}/app.js`,
]
.map(http.get)
.map(validate);
getGameId()
方法模拟了新游戏的开始:
const getGameId = () => {
const uri = `${target}/api/v2/games/`;
const response = validate(http.post(uri, {}, params));
return JSON.parse(response.body).id;
};
getScores()
方法使用 map
函数式编程技术检索现有分数,既用于迭代端点,也用于对 HTTP 响应运行验证函数:
const getScores = id => {
return ENDPOINTS.map(element =>
http.get(`${target}/api/v2/games/${id}/${element}`)
).map(validate);
};
putScores()
方法用于重置所有游戏分数,例如在新游戏开始时:
const putScores = (id, score) => {
return ENDPOINTS.map(element =>
http.put(
`${target}/api/v2/games/${id}/${element}`,
JSON.stringify({
id: id,
element: element,
value: score,
}),
params
)
).map(validate);
};
默认函数是 k6 为每个虚拟用户循环执行的函数:
export default function() {
const startDelay = random_gaussian(6000, 1000) / 1000;
log.debug(`Loading static assets, then wait ${startDelay}s to start game`);
getStaticAssets();
sleep(startDelay);
在这个函数加载静态资源后,它会随机延迟一段时间,模拟用户在启动画面等待的状态:
const gameDelay = random_gaussian(1500, 250) / 1000;
const id = getGameId();
log.debug(
`Game ${id}: Reset game scores, then wait ${startDelay}s to start game`
);
getScores();
putScores(id, 0);
sleep(gameDelay);
在模拟用户看到游戏画面后的另一次延迟之后,测试程序进入一个循环,开始快速模拟点击:
log.info(`Game ${id}: Simulating ${MOVES} moves, starting in ${gameDelay}s`);
for (let i = 0; i < MOVES; i++) {
const moveDelay = random_gaussian(125, 25) / 1000;
请注意,我们使用带有高斯分布的随机生成延迟,其中均值为 125
毫秒,标准差为 25
毫秒。这模拟了约 8 次点击/秒的速率,这是我们在 iPhone 上玩 ShipIt Clicker 时测得的速率——在 1 分钟内,我们记录了 480 次点击:
log.debug(`Game ${id}: move #${i}, then sleep ${moveDelay}s`);
deploy(id);
sleep(moveDelay);
}
log.info(`Game ${id}: Done with ${MOVES} moves`);
}
用于每个虚拟用户的 default
函数会获取与用户浏览器首次加载页面时相同的 URL。请注意所有的随机延迟,这些延迟真实地模拟了一个真实用户所做的延迟。在一个紧凑的循环中,测试模拟了用户尽可能快地点击,点击之间的延迟通过一个正常分布的随机数微妙地随机化,模拟了人类无法像机器人一样精准点击的事实。
chapter11/bin/k6-run.sh
脚本使用与 k6-har-run.sh
脚本相同的环境变量模式覆盖来运行测试,但包含更多变量。它允许你设置以下参数:
-
USERS
: 用户数量 -
DURATION
: 持续时间(秒) -
MOVES
: 游戏中的移动次数 -
STAGES
: 指定一组 k6 阶段,可以随时间变化虚拟用户数量
脚本需要一个命令行参数,这是测试的目标 URL。如前所述,这可能是类似 http://192.2.0.10:80/
的地址,用于测试部署在工作站上的应用程序基础设施。或者,也可以是已部署到云中集群上的应用程序,例如 shipit-v8.eks.shipitclicker.com/
。
运行压力测试
为了运行压力测试,你需要逐步增加应用程序的负载,直到它开始出现故障的迹象。我们可以尝试使用 script.js
k6 程序和 k6-run.sh
测试工具来实现这一点。我们必须指定的关键元素是 STAGES
参数:
$ MOVES=400 STAGES=900s:100 chapter11/bin/k6-run.sh https://shipit-v8.eks.example.com
您可能会发现,使用默认设置下的两个 pod 时,这个初步测试不会显示出任何故障迹象。您可以使用 kubectl
命令,结合 Prometheus、Grafana 和 Jaeger 来监控测试进度,以及集群中的 CPU 和内存利用率,正如前一章所描述的那样。例如,这是前述负载测试后 Grafana 的截图:
图 11.4 – Grafana 仪表板显示负载测试期间 ShipIt Clicker 部署的速率
为了在压力测试过程中让这个部署失败,我们不希望它自动扩展。因此,我们将删除 Horizontal Pod Autoscaler:
kubectl delete hpa/shipit-v8-shipitclicker
我们还希望对单个 pod 进行压力测试,以了解它能承受多少压力,因此我们将部署中的副本数量缩减为仅 1
:
kubectl scale deployment/shipit-v8-shipitclicker --replicas=1
此时,我们可以使用前述的 k9-run.sh
命令重新运行压力测试。观察输出。您可能会看到一些失败的请求,这些请求应以警告的形式记录在 k9 输出中,类似于以下内容:
time="2020-06-29T05:52:31Z" level=info msg="WARNING: PATCH https://shipit-v8.eks.example.com/api/v2/games/t2iAHlWtnhJhbsXfJI3zB/deploys: status 503"
一旦完成压力测试,我们可以重新创建 Horizontal Pod Autoscaler,并将部署的副本数量重置为更高的数字。
到此为止,我们已经学习了如何使用 k6 创建一个现实的负载测试,并用它来对 ShipIt Clicker 进行压力测试。
总结
本章我们探讨了如何使用 Cluster Autoscaler 和 Horizontal Pod Autoscaler 扩展 Kubernetes 集群。随后,我们研究了服务网格的概念,并设置了一个简化的 Envoy 服务网格,以提供代理和透明的网络通信,用于复杂的微服务架构。
接下来,我们探讨了如何使用断路器模式来防止服务被流量压垮。然后,我们使用连接阈值来测试断路器是否有效,并结合简单的负载测试技术,使用 Docker 和 Apache Bench 进行测试。之后,我们学习了使用 k6 时更加复杂的负载测试技巧,包括录制和回放以及精心设计的详细负载测试,旨在模拟真实用户行为。
这标志着我们《生产中运行容器》部分的结束。接下来我们将转向安全性方面的内容。在这一部分中,我们将学习如何将一些技术应用到我们迄今为止在本书中开发的项目和技能中,以改善我们的容器安全性态势。那么,接下来让我们进入 第十二章,容器安全入门。
深入阅读
使用以下资源来扩展您对自动扩展、Envoy 服务网格和负载测试的知识:
-
Lyft 的 Envoy 演示文稿:
www.slideshare.net/datawire/lyfts-envoy-from-monolith-to-service-mesh-matt-klein-lyft
。 -
使用 New Relic 和 JMeter 进行性能修复,这是 Docker for Developers 合著者 Richard Bullington-McGuire 的三部文章系列之一。涵盖负载测试和性能改进基础知识。您可以使用 Prometheus、Grafana、Jaeger 和 k6.io 将这些技术适配到 Kubernetes 中:
moduscreate.com/blog/performance-remediation-using-new-relic-jmeter-part-1-3/
. -
在 Amazon EKS 上使用 Network Load Balancer 和 NGINX Ingress Controller – 对于许多场景来说,这是使用 ALB Ingress Controller 的经济且灵活的替代方案:
aws.amazon.com/blogs/opensource/network-load-balancer-nginx-ingress-controller-eks/
. -
Kubernetes 自动缩放 101:集群自动缩放器、水平 Pod 自动缩放器和垂直 Pod 自动缩放器:
levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231
. -
使用 Velero 备份和恢复您的 Kubernetes 集群。可以备份和恢复整个集群、一个命名空间或按标签过滤的对象:
velero.io/
. -
将 Envoy 的 Prometheus 指标公开为
/metrics
。查看此问题,了解集成到 ShipIt Clicker 的 Envoy 配置中的解决方法,通过添加额外的 Envoy 映射,使 Envoy 的指标可以被 Prometheus 指标抓取器获取:github.com/prometheus/prometheus/issues/3756
. -
使用 Envoy、Istio 和 Kubernetes 进行微服务化:
thenewstack.io/microservicing-with-envoy-istio-and-kubernetes/
. -
使用 Envoy 进行 Jaeger 原生跟踪 – 一种高级跟踪策略:
www.envoyproxy.io/docs/envoy/latest/start/sandboxes/jaeger_native_tracing
. -
Redis 与 Envoy 速查表 – 使用 TLS 和 Redis Auth 设置 Redis 和 Envoy:
blog.salrashid.me/posts/redis_envoy/
. -
现代网络负载均衡和代理简介,来自 Lyft 的 Matt Klein:
blog.envoyproxy.io/introduction-to-modern-network-load-balancing-and-proxying-a57f6ff80236
. -
Matt Klein 谈 Envoy 的成功与服务网格的未来:
thenewstack.io/matt-klein-on-the-success-of-envoy-and-the-future-of-the-service-mesh/
. -
AWS 上 Kubernetes 的成本优化。一旦掌握了扩展操作,下一步就是降低成本。使用 AWS EKS 快速启动 CloudFormation 模板中的默认设置,EKS 集群每天的运行成本可能在$10 到$20 之间:
aws.amazon.com/blogs/containers/cost-optimization-for-kubernetes-on-aws/
。
第三章:Docker 安全性——保护你的容器
在本节中,我们将介绍安全性主题。在这里,你将建立在本书中所学的技能基础上,理解如何采用安全技术来保护基于容器的环境免受恶意攻击者的侵害。从扩大监控的使用到在 DevOps 管道中引入新工具,你将能够开始探索更高级的话题和项目。
本节包含以下章节:
-
第十二章**,容器安全简介
-
第十三章**,Docker 安全基础与最佳实践
-
第十四章**,高级 Docker 安全——机密、机密命令、标签与标签管理
-
第十五章**,扫描、监控与使用第三方工具
-
第十六章**,总结——旅程结束,但不是终点
第十三章:容器安全介绍
在开发技术项目时,安全性应是一个基本关注点。我们生活在一个被安全威胁环绕的世界,从恶意软件和病毒到数据泄露,各种安全威胁层出不穷。成为网络犯罪或信息泄露的受害者可能会带来越来越严重的后果,特别是在欧盟的通用数据保护条例(GDPR)等法规下。
当发生侵犯或妥协时,通过良好的架构实践限制其范围的能力至关重要。这通过所谓的横向移动限制概念来实现。我们指的是利用一个被入侵系统访问另一个系统,从而使攻击者能够在您的系统中穿越,进一步侵害系统并窃取数据。
幸运的是,正确部署的容器化可以通过多种功能帮助您通过本书最后一部分将要探讨的功能提高安全姿态。但是,首先,我们应该探讨 Docker 安全架构的技术基础,以便开始构建。本章中的一些概念将是我们在本书其他地方探讨的想法的总结,并以安全设置的方式呈现。这不仅有助于巩固您的学习过程中的这些概念,还有助于您理解如何保护您的应用开发项目。
在本章中,我们将简要概述容器的安全架构及其与虚拟化的关系和比较,以及 Docker Engine 和 containerd 从安全角度工作的方式及其从 Linux 继承的概念。我们还将查看您可以实施的利用 Docker 安全架构的最佳实践概述。这将为后续章节深入探讨该主题奠定基础。
我们将在本章中涵盖以下主题:
-
虚拟化和 hypervisor 安全模型
-
容器安全模型
-
Docker Engine 和 containerd – Linux 安全功能
-
cgroups 说明
-
最佳实践概述
因此,让我们首先回顾容器和虚拟化的区别,以及安全如何成为两者的基本组成部分。
技术要求
对于本章,您需要访问运行 Docker 的 Linux 机器。我们建议您继续使用本书中迄今为止使用的设置。
如果您已跳转到安全部分作为起点,我们建议您从docs.docker.com/v17.09/engine/installation/
安装 Docker Community Edition。
查看以下视频,了解代码实际操作:
虚拟化和 hypervisor 安全模型
在之前的章节中,我们探讨了 Docker 是如何工作的,以及它与其他技术(如 FreeBSD jail 和虚拟化)的比较。基于我们在这里学到的内容,我们现在将进一步理解支撑 Docker 的安全模型。
首先,让我们来看看虚拟化工具是如何实现安全性的,这样我们就能理解 Docker 与这些工具的相似与不同之处。
虚拟化与保护环
在使用虚拟机(VMs)时,你可能会遇到“超管程序”(hypervisor)这个术语。它是一个协调虚拟机如何在系统上运行以及如何与底层硬件交互的程序。有些超管程序产品被称为类型 1 超管程序,它们直接运行在硬件之上。其他的,如 VirtualBox,是通过现有操作系统安装的,可以让你加载额外的操作系统作为虚拟机。
超管程序如何与底层硬件协作,是由所谓的保护环来决定的。这些保护环决定了特权的层级,实际上决定了计算机系统的软件(例如操作系统、驱动程序和桌面应用程序)能够访问底层硬件的哪些部分。
通常,你会看到保护环被建模为一组同心圆,如下所示:
图 12.1 – 保护环示例
有时设备驱动程序环也可能会显示为两个独立的环(如图中的虚线圆圈所示)。
每种硬件架构在适应保护环模型时会有所不同,因此,运行在其上的操作系统可能也会在不同的层级上运行代码。然而,通常会看到环 0——即位于中心、特权最高的环——被称为内核环(有时也叫做内核空间)。
恶意软件通常会尝试攻击内核,以便获得对系统的完全访问权限并执行低级系统进程。这类软件通常被称为内核模式根套件。因此,保护内核至关重要,同时也要确保如果系统被恶意应用程序、库或软件包攻破,它无法提升特权以获取对内核的访问权限,这是至关重要的。
外层环则可能处理设备驱动程序和应用程序。每个环都有其对应的层级,而包含用户应用程序的外层环通常被称为用户空间。网关处理每个环如何与下层环进行交互。与内核一样,恶意软件有可能感染运行在这些层级上的应用程序,包括运行在第 3 层的用户模式根套件(rootkit)。
考虑到这些威胁,保护环模型有助于防止你在桌面上安装的程序恶意访问底层硬件并绕过内核。因此,恶意软件编写者被迫寻找安全漏洞和其他混淆其攻击手段的方式,比如将他们的代码注入到其他进程中。你可以把这些安全层看作是需要突破的一道道门,而不是攻击者能直接进入并获得底层硬件的访问权限。
这些分层的隔离措施,虽然不是万无一失的,但有助于提供所谓的分层安全方法。其思路是通过在一个安全层上再增加另一个安全层,我们使得攻击变得越来越难以实现。
虚拟化与恶意软件
由于我们关注虚拟化以及虚拟化与 Docker 容器的比较,我们自然会关注虚拟化如何融入这个模型。那么,虚拟化如何防御根套件(rootkits)和其他恶意软件呢?
许多现代硬件架构,如 ARMv7-A,包含一个超管程序(hypervisor)作为我们的环模型中的一个权限级别,这个权限级别比操作系统权限更高。这使得超管程序能够在上面一环的操作系统之间切换。
一些架构还实现了被称为环-1的机制。这允许超管程序运行在一个更深的安全环中,而客操作系统内核运行在环 0。例如,如果你在 x86 平台上运行 VirtualBox,取决于是否支持硬件虚拟化,VirtualBox 将运行在环-1 或环 0。
虚拟机(VM)由于多种原因在进行恶意软件分析时非常有用,其中之一是机器可以被锁定,因此它是一个自包含的环境。一旦分析人员完成对代码及其效果的分析,虚拟机可以被删除,而无需重新安装整台机器的操作系统,或者(如果正确配置)避免恶意软件访问底层硬件的风险。
总结来说,保护环提供了一种机制,用于将软件进行分段,使其只能访问某些特定资源。在虚拟化模型中,超管程序可以运行在权限最高的环中,以便在操作系统之间切换。超管程序可以通过现有的操作系统(如 Windows)安装,或部署在裸机上,如 VMware ESXi 产品。它也可以用来创建一个沙箱环境,防止恶意代码感染底层硬件或操作系统。
那么,这与 Docker 有什么关系,保护环模型如何应用到 Docker 中?
Docker 与保护环
和虚拟机一样,Docker 容器提供了一个隔离的环境,用于在现有操作系统上运行你的代码。这个操作系统可以是虚拟化的,也可以直接安装在裸机上。
那么,这究竟是如何运作的呢?你可能还记得,Docker 容器运行在 Docker Engine 上,而 Docker Engine 又通过一个名为 containerd 的中间组件运行在操作系统之上。这与我们之前讨论的类型 1 虚拟化程序不同,类型 1 虚拟化程序运行在基础设施之上,而客户操作系统则运行在虚拟化程序之上。
因此,Docker 容器都在同一个操作系统上运行,无论它是否被虚拟化。事实上,在某些情况下,例如在 Windows 上运行 Linux 容器时,你可能会注意到它使用了一个中间步骤。这包括运行一个虚拟化版本的 Linux,进而运行 Docker 引擎。在这种情况下,所有容器都运行在同一个虚拟化的 Linux 操作系统上。
注意
Docker Engine 企业版也支持原生 Windows 容器。你可以在www.docker.com/products/windows-containers
阅读更多关于它们的信息。
所有这一切的关键概念是,隔离发生在容器级别,而不是(或除了)虚拟机级别。因此,从基础层面来看,Docker 并不提供与虚拟机本身相同的沙箱功能。
以下图示展示了两者的区别:
图 12.2 – Docker 和虚拟机中的隔离示例
在前面的图示中,如果我们在虚拟机堆栈上运行 Docker,我们将用 Docker Engine/Containerd 和容器层替换 应用程序 层。
正如你可能看到的,这提供了一个额外的安全层,除了由底层主机操作系统提供的安全性之外,或者在适用时由虚拟化层提供的额外安全层。然而,当这个安全层在主机操作系统上运行而不是通过虚拟机时,这意味着如果 Docker Engine 存在安全漏洞,你将面临额外的风险。
因此,Docker 容器访问底层系统/内核是通过引擎进行调节的,而引擎则通过 containerd 发出系统调用(在大多数情况下,是通过 runc
调用的)。
注意
如果你想更详细地了解 containerd 和 runc,可以查看官方网页 containerd.io/
。
在这里,我们为每个容器与底层操作系统和硬件之间提供了一个隔离层。Docker Engine 不运行在 ring 0 或 ring -1,而是运行在 ring 3,这意味着尽管它容易受到其他形式的攻击,但它不像虚拟化程序那样直接访问硬件。
注意
即便采用这种分层的安全方式,过去也曾发现漏洞。你可以在www.twistlock.com/labs-blog/breaking-docker-via-runc-explaining-cve-2019-5736/
了解更多信息。
此外,每个容器都是一个独立的自包含库和应用程序集合,它们只能通过 Docker 引擎相互通信。如我们所提到的,容器默认情况下无法访问 Docker 引擎所在的底层操作系统。事实上,任何对底层操作系统资源的访问调用,必须在设置 Dockerfile 时显式配置。因此,Docker 容器运行在环级 3,即用户空间,并且在此基础上增加了额外的安全层。
现在我们已经了解了环模型以及 Docker 和虚拟化如何与之协作,接下来让我们看看容器安全模型以及它们从 Linux 的最佳实践和技术中继承了什么。
容器安全模型
从硬件层开始,经过虚拟机管理程序和基础操作系统如何调解访问的方式,我们可以开始审视在环级 3 上运行的软件层发生了什么。为了深入了解这一点,我们需要理解 Docker 容器安全模型中的两个关键特性:
-
应用程序与底层宿主系统隔离。
-
容器化应用程序相互隔离。
那么,Docker 是如何实现这些目标的呢?答案正如你所猜测的,通过 Docker 引擎和相关组件,如 containerd。这些组件继承了 Linux 的许多关键特性和概念,并在安全性方面带来了主要好处,包括以下内容:
-
runc:轻量级容器运行时
-
Namespaces:Linux 用于分割内核资源的方法
-
控制组(cgroups):限制 CPU 使用等资源的内核特性
此外,它还允许实现 Linux 内核中发现的其他安全特性,例如:
-
SELinux:用于处理访问控制安全策略的 Linux 内核安全模块
-
AppArmor:用于限制应用程序能力的 Linux 特性
-
TOMOYO:用于处理强制访问控制(MAC)的 Linux 安全模块
-
GRSEC:为 Linux 内核提供的一系列安全增强功能
这些经过验证的最佳实践确保容器能够以安全的方式彼此隔离,并与宿主操作系统隔离。接下来我们将深入探讨 Docker 引擎和 containerd,以更好地理解这些安全特性是如何实现的。
Docker 引擎和 containerd – Linux 安全特性
你之前安装的 Docker 引擎作为所有应用容器的协调者。除了引擎外,还有其他一些关键组件构成了 Docker 生态系统。最初,许多组件被集成到 Docker 引擎中,但随着时间的推移,为了使引擎更小更快,一些组件,如用于管理容器的运行时机制,被拆分为独立的项目。
其中一个例子是 containerd 项目。containerd 实现了 runc
,它支持容器管理,并且被多个与 Docker 相关的项目使用,包括 Kubernetes CRI。
注意
你可以从 GitHub 下载并查看 containerd 的源代码,链接为 github.com/docker/containerd
,以及 runc
的源代码,链接为 github.com/opencontainers/runc
。
containerd 解决了聚合 Linux 内核中多个功能并提供一个抽象层来处理 系统调用 (syscalls) 的问题。因此,Docker 引擎位于其上方,并利用它与底层操作系统进行交互。一个由 Docker 引擎交给它的任务例子是将进程附加到现有容器上。
这种模块化的方法不限于引擎及其与操作系统的交互。例如,容器和引擎不需要位于同一台机器上。因此,可以对托管选项进行拆分。
这种分布式模型的运作方式是,Docker 实现了客户端-服务器模型,容器引擎作为服务器,而每个容器作为客户端。此架构的一些关键特性如下:
-
服务器作为 Linux 守护进程运行 (
man7.org/linux/man-pages/man7/daemon.7.html
)。 -
终端中的 Docker
docker
命令。 -
容器与引擎之间通过 REST API 进行通信。
需要注意的是,容器与引擎之间的通信通道可以通过 SSL/TLS 加密。
SSL/TLS 是加密 Web 端点之间流量的事实标准。当你通过 HTTPS 协议访问网站内容时,可能已经见过它的应用。稍后,我们将探讨如何启用 SSL/TLS 来帮助保护 Docker 守护进程套接字。
Docker 提供了一整套用于配置复杂网络的功能,关于这一点的详细信息可以在 Docker 网站上阅读:docs.docker.com/v17.09/engine/userguide/networking/
。
客户端-服务器架构提供的隔离性,使得主机操作系统与各个容器之间(无论是否位于同一台机器上或分布式)可以基于最小访问原则进行操作。这意味着每个 Docker 容器仅能访问其所需的资源,如磁盘或网络资源,而无法访问其他资源。此外,一个 Docker 容器无法访问另一个容器的进程。
最小访问模型的实现得益于 Linux 命名空间,它用于将进程隔离开来。通过虚拟化的 Linux 环境托管引擎在 Windows 上运行 Docker,是 Windows 用户能够享受此技术的方式之一。
注意
如果你想了解更多关于原生 Windows 容器如何实现进程和 Hyper-V 隔离的信息,可以访问 Windows 容器网站:docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container
。
当 Docker 引擎部署一个容器时,它会生成一些 Linux 命名空间。这些命名空间如下:
-
进程 ID(PID)命名空间
-
挂载(MNT)命名空间
-
网络(NET)命名空间
-
进程间通信(IPC)命名空间
-
Unix 时分共享(UTS)命名空间
-
USER
命名空间
现在我们将更详细地了解这些命名空间,以理解它们的安全影响。
PID 命名空间
正如你所知道的,Linux 操作系统中的每个进程都位于一个树形结构中,并被分配一个称为 PID 的 ID。PID 命名空间允许进程的隔离。通过实现 PID 命名空间,我们可以防止容器查看系统进程。除了安全性上的好处外,它还带来了额外的好处,即可以重用系统 PID,如 PID 1。
如果你希望允许容器访问系统进程,那么你必须在 Dockerfile 中显式地编码这一点。这遵循了前面提到的最小权限原则。因此,在以这种方式实现任何功能之前,请仔细考虑。
MNT 命名空间
MNT 命名空间允许容器访问自己的根目录集合和文件挂载。这种方法使你可以创建一个私有文件系统,从而隔离哪些文件可以被哪些容器访问,减少了被攻破的容器访问不该访问的文件或造成意外文件损坏的风险。
NET 命名空间
Docker,如我们简要讨论过的,提供了多种网络工具。默认情况下,当你部署一个容器时,它将启用网络功能。这将允许容器进行外部连接。默认情况下,容器将使用主机配置的相同 DNS 服务器,并且会为其分配一个 MAC 地址。IPv4 和 IPv6 的 IP 地址可以通过相关标志进行设置。如果你选择通过可用标志覆盖 MAC 地址,你应该知道,系统没有机制自动检查 MAC 地址是否唯一。重复的 MAC 地址可能会导致 MAC 地址冲突。
如果你希望禁用特定容器的网络功能作为安全防护的一部分,可以在执行run
命令时,通过覆盖设置--network
标志来实现。将该标志设置为none
将禁用所有外部访问,仅保留环回地址可访问。
还有许多其他配置选项可用于自定义容器网络设置,这些选项可以通过帮助菜单进行访问。
IPC 命名空间
IPC 命名空间用于提供命名共享内存段的隔离,以及消息队列。
IPC 命名空间被锁定,以防止一个命名空间中的进程访问另一个命名空间中的进程。这个模型的好处是,容器可以安全地部署一组需要使用内存段的服务,例如你在金融科技领域可能会遇到的应用程序类型。
UTS 命名空间
UTS 命名空间允许我们为在该命名空间中运行的进程设置域名和主机名。这个命名空间是默认功能,因此所有容器都有启用,它允许你为每个容器分配不同的主机名。
USER 命名空间
我们将讨论的最后一个命名空间类别是 USER
命名空间。这是一种机制,允许你将用户和用户组映射到容器中。一旦映射,用户可以被分配不同的用户 ID。
从安全角度来看,这个功能的一个非常有用的好处是,它有助于防止容器被用于特权升级攻击。实现这一点的示例不仅包括以非特权用户身份运行应用程序,还包括在 Docker 主机级别将容器内的 root 用户映射到一个较低权限的用户。因此,容器内以 root 身份运行的进程,其特权级别被限制在它们所在的容器内。
关于 cgroups 的说明
Linux cgroups 是一种机制,用于控制可生成的进程数量,从而防止系统出现严重的性能损失,甚至崩溃。
通过使用 cgroups,我们可以限制通过 fork()
和 clone()
操作生成的进程数量。一旦达到限制,就无法在该 cgroup 下生成更多进程。此外,cgroups 支持设置 CPU 和内存限制。你可以在www.man7.org/linux/man-pages/man7/cgroups.7.html
阅读它们的完整选项列表。
使用这个功能可以让你更精细地控制容器所使用的系统资源。在容器遭到入侵的情况下,防止其过度消耗系统资源是一个有用的机制,可以在你修复问题之前限制损害。
在了解了 Docker 引擎和 containerd 如何使用 Linux 最佳实践之后,我们现在来看一下我们可以使用的一些最佳实践,这些实践也实现了我们迄今为止讨论的一些功能。
最佳实践概述
在接下来的章节中,我们将深入探讨确保容器安全的技术。你会高兴地知道,有许多最佳实践可以让你从一开始就确保你在最基本的层面上思考并实施安全性。
首先需要理解的一点是,您可能已经注意到了,Docker 容器与虚拟机相比,提供的安全性并不相同。我们之前举过一个例子,说明虚拟机如何因其沙盒化环境被用来进行恶意软件分析。因此,从安全的角度来看,您应将容器视为一种用于优化系统资源和应用程序开发及交付的机制(内置了一些非常有用的安全性),而不是把它当作微型虚拟机来使用。
记住这一点后,让我们来看一些在使用 Docker 时可以应用的最佳实践。
保持 Docker 更新
与您运行的任何应用程序一样,保持 Docker 更新非常重要。例如,Docker Engine 中未打补丁的安全漏洞可以被恶意行为者利用,在发生安全事件时,通过访问您的某个容器进行攻击。
例如,在 macOS 中,Docker Desktop 应用程序提供了检查更新的选项,并且偏好设置允许您自动检查更新:
图 12.3 – macOS 上检查更新的示例
在实施 Docker 时,您可能还希望根据安全补丁或是否使用 Docker 企业版来手动升级软件。
每个补丁/版本的列表可以在 Docker 网站上找到,包含新增的功能或已解决的问题:
docs.docker.com/engine/release-notes/
您会注意到,这里列出的一些项目带有 CVE 前缀,代表 常见漏洞和暴露。CVE 列表是公开披露的安全问题集合。当 Docker 的安全问题被发现时,它可能会列在 CVE 数据库中,并且在修复后,问题的 CVE ID 会出现在发布说明中。
关于这个话题的最后一点,请记得也要保持 Docker 运行所在的底层操作系统的补丁和加固。
保护 Docker 守护进程套接字
除了确保 Docker 定期打补丁外,我们还需要保护守护进程套接字。这意味着要对其进行锁定,防止攻击者利用它获得对底层主机的 root 权限。Docker 安全文档提供了一个详尽的指南来实现这一点;但在此我们将做一个总结。
注意
要了解更多关于守护进程套接字的信息,请参考 Docker 网站上的官方文档:docs.docker.com/engine/reference/commandline/dockerd/#daemon-socket-option
。
您可以在 Linux 系统中找到域套接字文件,路径为 /var/run/docker.sock
。
该文件应仅通过 root 权限或 Docker 组中的账户访问。
我们现在将设置通过 TLS/SSL 加密访问 Docker 守护进程,以增加额外的保护层。
如您所知,通过使用-H
标志启用未加密的 TCP 套接字,包括 TCP 协议、主机和端口号。按惯例,未加密连接的端口是2375
。往后,如果您一直使用这种方法,建议您停止使用,并改用内建的 TLS/SSL 支持。
在我们能够通过安全通道连接客户端和主机之前,需要生成以下文件:
-
证书授权中心(CA)私钥和公钥
-
服务器密钥
-
服务器证书签名请求(CSR)
-
签名证书
-
客户端密钥
-
客户端 CSR
根据您的操作系统,生成这些 OpenSSL 文件的步骤会有所不同。Docker 网站提供了一个方便的步骤列表。Windows 用户也可以使用 Linux 虚拟机执行这些步骤:
docs.docker.com/engine/security/https/
通过启用 Docker 守护进程使用 CA、服务器证书和服务器密钥,可以以以下方式实现加密。在此示例中,我们将在0.0.0.0
上运行守护进程,并使用端口2376
:
dockerd --tlsverify --tlscacert=tlsca.pem --tlscert=tlsservercert.pem --tlskey=tlsserverkey.pem -H=0.0.0.0:2376
现在,我们可以测试连接。首先,确保客户端证书、密钥和 CA 可用。然后,运行以下命令:
docker --tlsverify --tlscacert=tlsca.pem --tlscert=tlscert.pem --tlskey=tlskey.pem -H=$HOST:2376 version
现在,您应该能够通过加密通道成功连接到 Docker 守护进程。
Docker 无法修复糟糕的代码
Docker 可以做很多事情来帮助缓解安全问题的影响,但它不能修复糟糕的代码。在编写应用程序时,适用于部署在 EC2 实例、VMware 或任何其他平台上的最佳实践同样适用于应用程序开发。
应用程序安全的一个好起点是 OWASP 十大漏洞。除了标准文档,OWASP 还提供了许多有用的备忘单指南,用于应用程序安全开发。
您可以在cheatsheetseries.owasp.org/
找到它们。
始终设置无特权用户
我们简要讨论了USER
命名空间的主题,以及它如何帮助您提高 Docker 配置的安全性。您应该实施的一项实践是确保尽可能配置容器使用无特权用户。从一开始就这样做,将有助于养成良好的习惯。
实现这一点的两种最简单方法如下:
-
向 Dockerfile 中添加用户。
-
运行 Docker 时,在
run
命令中添加--user
标志。
在第一种情况下,可以通过以下方式实现:
FROM alpine
RUN addgroup -S secureusers && adduser -S secureuser -G secureusers
#Execute any root commands prior to needing to switch users
USER secureuser
对于第二种选项,我们可以将标志应用于命令行,如下所示:
docker run --user 5000:500
在这里,我们包含了用户 ID 和组 ID。
现在我们已经掌握了一些基础知识,在进一步深入一些基本原理并动手实践之前,让我们快速回顾一下我们已经学到的内容。
总结
在本章过程中,我们了解了虚拟机和 Docker 如何与底层操作系统、硬件以及彼此协同工作。
接下来,我们探讨了 Docker 从 Linux 到实现的各种功能,以解决安全问题。
最后,我们回顾了一些适用于我们开发的应用程序的最佳实践。现在,让我们进入一些安全基础知识,并在下一章学习 Docker 镜像安全、命令以及构建过程。
第十四章:Docker 安全基础和最佳实践
为了确保我们的容器在开发和生产环境中都能得到加固,我们可以实施许多技术和最佳实践来实现这一目标。在许多情况下,这仅仅是修改你在本书中学到的现有命令或行为,为你的实践添加额外的安全层。
在本章中,我们将基于我们对 Docker 和容器安全的基础知识进行扩展。这将涉及构建和修改容器的实践操作。内容涵盖了诸如通过使用 Docker 命令和签名镜像来保障镜像安全等主题。完成以下练习后,你应该能够在实际的开发和 DevOps 环境中自如地应用这些技能。
在本章中,我们将覆盖以下主要主题:
-
Docker 镜像安全性:在这里,我们将学习镜像安全性,包括使用最小基础镜像、签名和验证过的镜像以及避免数据泄露。
-
在构建 Docker 镜像时使用
COPY
而不是ADD
。 -
构建过程的安全性:在这里,我们将学习构建过程的最佳实践,包括多阶段构建。
让我们通过了解 Docker 镜像安全性和一些最佳实践开始吧。
技术要求
本章需要你有一台运行 Docker 的 Linux 机器。我们建议你使用本书中一直在使用的设置。
此外,你还需要在 Docker Hub 上有一个帐户,以便访问其中的镜像。如果你还没有设置,可以在hub.docker.com
上进行设置。
如果你已经有一个正在运行 SSH 的容器或服务,可以在本章后续部分使用。如果没有,别担心,我们提供了一个来自官方 Docker 文档的示例 Dockerfile,你可以使用它。
查看以下视频,观看代码实践:
Docker 镜像安全性
当你在本书中逐步学习时,你会越来越熟悉镜像。这是 Docker 生态系统中的一个基础构建模块。镜像是文件系统和参数的组合,当 Docker 运行时,它会变成你的容器。
在确保 Docker 本身已经打上补丁并得到加固,确保我们的应用代码健壮,以及确保我们运行容器时它们具有限制权限的情况下,我们还需要确保镜像本身是安全的。
Docker 的一大优点是,像 Docker Hub 这样的服务允许我们共享和重用容器镜像。然而,我们需要小心下载的内容是安全的,且未被恶意方上传:
图 13.1 – Docker Hub 显示示例仓库
然而,即使是合法/官方的网站,你也应该始终保持谨慎。
过去曾有几起恶意镜像被上传到 Docker Hub 的案例,恶意上传者希望这些镜像能被毫无防备的用户下载。恶意代码的例子包括伪装成 tomcat、mysql 和 cron 相关的镜像。例如,包含内核漏洞的被攻击容器可能导致对底层主机的攻击。
Kromtech 安全中心在 2018 年的某一时间段内发现了 Docker Hub 上的 17 个恶意 Docker 镜像。你可以阅读他们的报告《加密劫持侵袭云端:现代容器化趋势如何被攻击者利用》,链接:kromtech.com/blog/security-center/cryptojacking-invades-cloud-how-modern-containerization-trend-is-exploited-by-attackers
。
因此,在任何项目中使用第三方工具和代码时,你的第一步应该是验证这些工件的来源是否可信。保持关注安全警报也很重要,确保你不会不小心下载带有漏洞的镜像。
对于 Docker 镜像,一旦你确认来源的有效性,就可以添加额外的验证过程,检查工件本身是否安全。事实上,你可能已经在使用的其他技术中熟悉这个概念,比如在下载操作系统时验证文件完整性哈希。
一个帮助确保我们下载的源是合法的机制是使用 Docker Hub 提供的签名 Docker 认证镜像。正如我们在 Kromtech 报告中看到的,即使是像 Docker Hub 这样的合法主机,我们也永远不能太小心。这些认证镜像已由主机审查并认证为真实。许多流行的应用环境可以在 Docker Hub 上找到,包括以下内容:
-
Splunk 企业版:
hub.docker.com/_/splunk-enterprise
-
Datadog:
hub.docker.com/_/datadog-agent
-
Dynatrace:
hub.docker.com/_/dynatrace
-
Oracle Java 8 SE(服务器 JRE):
hub.docker.com/_/oracle-serverjre-8
你可以在 Docker Hub 网站上找到更多内容:
hub.docker.com/search?q=&type=image&certification_status=certified
现在让我们采取实际操作的方法来检查镜像的合法性,包括与 Docker Hub 的互动。启动你的命令行工具,然后继续下一部分。
镜像验证
我们需要理解的第一个概念是内容信任。这是 Docker 在镜像上应用的安全模型。
Docker 内容信任 (DCT) 模型的核心是使用数字签名来证明托管在 Docker Hub 等平台上的镜像的完整性。启用 DCT 后,用户可以确保不拉取不可信的镜像(即未签名的镜像),除非他们明确做出例外。
默认情况下,Docker 禁用了 DCT,这允许你在不验证镜像安全性的情况下拉取镜像。这使得你面临下载包含恶意软件或其他安全漏洞的制品的风险。
幸运的是,我们可以使用 DOCKER_CONTENT_TRUST
标志来确保在拉取镜像时进行验证。这通过检查镜像是否已由创建者签名,或我们是否正在使用与镜像相关的显式哈希来工作。要在系统范围内启用它,将其包含在 .bashrc
文件中,如下所示:
$ vim /<path>/<to>/.bashrc
export DOCKER_CONTENT_TRUST=1
:x
$ source /<path>/<to>/.bashrc
如果由于某种原因,你希望与未标签的镜像交互,可以通过在命令中使用 --disable-content-trust
标志临时禁用此设置。
DOCKER_CONTENT_TRUST
标志可以仅限于单个 shell,而不仅仅是系统范围的覆盖。要在新 shell 中快速启用它,输入以下内容:
$ export DOCKER_CONTENT_TRUST=1
只需记住,当你关闭 shell 时,你需要重新启用该标志,或按前述方式在 .bashrc
文件中设置系统范围属性。
实际上,当此设置在系统范围内(或在单个 shell 中)启用时,这意味着与带标签镜像交互的命令行操作需要具备两项内容之一。这些可以是附加到镜像上的内容哈希,或者镜像本身需要是已经通过使用签名密钥预先签名的镜像。
签名密钥
签名密钥是一组用于签署镜像的组件。它们包括一个离线密钥,它是 DCT 信任镜像标签的基础,以及一个用于签署标签的标签密钥,最后是一个用于强制执行安全保证的服务器管理密钥集合。
所以,从实际的角度来看,运行命令时到底会有什么结果?让我们通过一个使用 shipitclicker
镜像的简单示例来看一看。
如果我们希望在启用 DOCKER_CONTENT_TRUST
标志时拉取 shipitclicker
镜像,可以使用 @
符号将哈希附加到镜像。比如以下示例:
$ docker pull dockerfordevelopers/shipitclicker@sha256:b20caa037ac2c36a9845f719ebb12952bbb3e749d4b05fcdcd8d 38201a7de795
只要内容哈希 sha256:b20caa037ac2c36a9845f719ebb12952bbb3e749d4b05fcdcd8d382 01a7de795
存在,命令就会成功。否则,假设我们想拉取该镜像的最新版本或版本号,比如以下示例:
$ docker shipitclicker:v0.1
在这种情况下,我们需要确保镜像已经被签名,否则命令将会失败。pull
命令并不是唯一与可信内容交互的操作,其他包括以下内容:
-
$docker push
-
$docker build
-
$docker create
-
$docker run
我们现在可以进行测试了。我们已提前为你创建了 shipitclicker
镜像,你可以从 Docker Hub 拉取,位于 Packt Docker 书籍仓库中的 hub.docker.com/r/dockerfordevelopers/shipitclicker
。
你可以尝试使用以下命令拉取此镜像:
$ docker pull dockerfordevelopers/shipitclicker:v0.1
你现在应该会看到一个类似于 请求被拒绝 的错误:
Error: remote trust data does not exist for docker.io/ dockerfordevelopers/shipitclicker: notary.docker.io does not have trust data for docker.io/ dockerfordevelopers/shipitclicker
确保在自动化构建过程中启用此标志也是必须的,因为它可以防止未经验证的镜像意外进入你的环境。
这种非常简单的 DCT 使用方法对于确保你避免使用来自 Docker Hub 的不信任内容非常有效。现在,让我们更仔细地看看基础镜像。
使用最小化基础镜像
所以我们知道我们正在拉取签名镜像或特定的哈希值,但我们是否需要考虑我们在容器中使用的镜像类型?答案是 是的。
使用镜像时,你应该问问自己,是否真的需要整个操作系统,包括所有预装的包?在某些情况下,这可能会引入漏洞,因为你可能会在容器中包含未修补的库和其他代码。因此,最好的方法是从简单的镜像开始,然后逐步构建。这将有助于减少整体攻击面。
现在让我们从 Docker Hub 拉取一个最小化的镜像,这样我们可以在本章其余部分进行操作。我们将使用的镜像是 shipitclicker:v0.1
,它刚刚通过 DOCKER_CONTENT_TRUST
进行了测试,并基于 Alpine。
注意
如果你有兴趣查看,并且还没有这样做,Alpine 镜像仅有 5 MB 大小,并且是 Docker Hub 官方镜像计划的一部分。这些镜像是一些仓库,提供所有基本的必要内容,同时确保所有安全补丁定期应用。除此之外,官方 Docker 镜像也是签名的,因此可以保证本章讨论的镜像验证安全措施。
首先,你需要在当前的 shell 中禁用 DOCKER_CONTENT_TRUST
,或者获取镜像的哈希值,以便你现在可以拉取它。如果你希望禁用 DOCKER_CONTENT_TRUST
,可以通过在当前 shell 中执行以下命令来实现:
$ export DOCKER_CONTENT_TRUST=0
只要记住,如果你关闭了 shell 并创建了一个新的,你需要再次运行此命令。我们建议你将标志保留为 1
,而是拉取哈希版本。
你可以在仓库的 Tags 标签下找到哈希值,如以下链接所示:
hub.docker.com/r/dockerfordevelopers/shipitclicker/tags
从这里,选择你感兴趣的版本下显示的摘要值。这将显示 sha256
哈希值,例如以下所示:
DIGEST:sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c 64992ef69804dbf01
以下截图显示了你可以找到用于 docker pull
命令的哈希位置:
图 13.2 – Docker 镜像的信息
包含 sha256
之后的字符串部分可以在拉取请求中使用:
$ docker pull dockerfordevelopers/shipitclicker@ sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef 69804dbf01
你现在应该在终端中看到类似以下内容:
sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef 69804dbf01: Pulling from dockerfordevelopers/shipitclicker
Digest: sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c 4992ef69804dbf01
现在运行 docker images
命令应该能看到它已出现在你的系统中。
在构建自己的镜像时,另一个需要考虑的因素是使用 .dockerignore
文件帮助减少整个容器的大小。
在构建上下文目录中包含 .dockerignore
文件时,文件中列出的任何文件将不会被添加到镜像中。正如你将很快看到的,这还有一个方便的好处。从镜像大小的角度来看,考虑到我们提倡的最佳实践——保持干净,我们可以利用这一点避免像 Python .pyc
文件和类似的二进制文件被意外添加到镜像中。以下示例的 .dockerignore
文件演示了我们如何做到这一点:
# ignore .pyc and .git files/directories
.git
**/*.pyc
这种方法非常简单,如果你习惯使用 .gitignore
文件的话,应该已经非常熟悉了。
现在我们有了最小的基础镜像,我们应该看看在创建容器时限制权限的一些方法,以防止意外的安全漏洞。
限制权限
在上一章中,我们研究了分配用户和组来限制启动镜像时的权限提升。我们可以在此基础上,进一步使用一个有用的参数 no-new-privileges
。
该标志利用了底层 Linux 内核的 no_new_privs
功能。这个功能的基本思想是确保任何进程,包括子进程,在生成时无法获得额外的权限。启用此选项后,应用程序将无法使用 setuid
等功能。
注意
setuid
功能允许用户以提升的权限运行和执行某些程序。这构成了一个安全威胁,因为攻击者可以利用它执行他们通常无法访问的代码和程序。
通过此功能生成的进程也无法取消 no_new_privs
标志,因此防止攻击者通过 setgid
或 setuid
禁用此功能并提升权限。
要在运行容器时启用 no-new-privileges
功能,你需要包括 --security-opt
标志并将其作为参数添加。
让我们试试刚刚下载的镜像:
$ docker run -d -it --security-opt=no-new-privileges dockerfordevelopers/shipitclicker@sha256:39eda93d15866957 feaee28f8fc5adb545276a64147445c64992ef69804dbf01
镜像现在应该已经以这种模式运行。记住,我们可以通过运行以下命令获取容器名称:
$ docker ps -a
禁用容器获取更高权限的能力也有助于防止容器突破。突破这个术语是用来指代一个已被攻破的容器能够访问底层主机上的敏感数据的情况。在容器被利用且漏洞允许攻击者提升权限(例如,如果之前讨论的标志未包含在内)的场景下,他们可能会尝试转移攻击并通过 Docker 进一步攻破其他容器,或者利用主机本身获得其他利益。
正如我们将在本章后面学习的那样,通过限制容器的权限(称为能力),在运行时可以进一步加固我们的系统。
现在我们来看一些可以添加的标志,以及其他一些确保我们使用的数据保持安全的技术。
避免你的镜像数据泄露
在 Linux 中,我们可以实现用户和组的管理,以确保只有需要读取和写入文件的用户才能执行这些操作。这种精细化的访问权限系统对于防止数据泄露非常重要。另一个保护镜像文件系统的方法是将文件系统和所有卷设置为只读状态。
让我们从查看一个可能想要挂载的卷开始。我们将基于shipitclicker
镜像运行一个新容器,并将本地文件系统挂载到其中。为了实现这一点,除了--mount
标志,我们还将在run
命令中加入一个readonly
语句。
首先,在你的本地操作系统上创建一个空文件夹,我们可以用它来挂载文件系统:
$mkdir testfiles
接下来,尝试运行以下命令。它将挂载本地文件夹并运行容器,并尝试向/mnt/testfiles
目录写入一个名为test.file
的文件:
$ docker run --mount source=testfiles,destination=/mnt/testfiles,readonly dockerfordevelopers/shipitclicker@ sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef 69804dbf01 sh -c 'touch /mnt/testfiles/test.file'
现在你应该会看到一个错误,告知你文件系统是只读的:
touch: /mnt/testfiles/test.file: Read-only filesystem
通过这种机制,我们可以读取挂载到容器上的文件,但避免容器能够将文件写回,从而防止意外地将密钥或其他数据写入主机上不应存放的目录。
注意
一个重要的要点是,root 账户可以覆盖任何文件权限,因此可以读取容器中的任何文件。如果有人获得了 root 访问权限,他们可以窃取你的数据!
那么如何保护容器本身的文件系统呢,例如/tmp
目录?幸运的是,Docker 提供了一个简单的方法来实现这一点,通过--read-only
标志。我们可以尝试一下,看看它在实践中的效果。首先,停止我们刚才创建的容器。记住,你可以在运行docker ps -a
命令时获取容器的名称。
一旦你获得了容器名称,停止容器。我们这里使用了nervous_sinoussi
作为示例名称;请用你容器的唯一名称替换它:
$ docker stop nervous_sinoussi
现在,我们将使用--read-only
标志重新创建容器。在run
命令中将包括尝试将名为test
的文件写入/tmp
目录的示例。启用--read-only
标志后,我们应该会看到一个错误,提示此操作不被允许。
让我们删除之前创建的容器,以保持环境的清洁:
$docker container rm nervous_sinoussi
所以,尝试运行以下命令,记得包括你的容器名称:
$ docker run --read-only dockerfordevelopers/shipitclicker@ sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992e f69804dbf01 sh -c 'echo "Testing" > /tmp/test'
你现在应该会看到如下错误:
sh: can't create /tmp/test: Read-only filesystem
检查正在运行的 Docker 进程列表时,你将看到命令已执行并退出。让我们清理这个容器并尝试重新运行不带标志的命令,同时回显我们创建的文件内容:
$ docker run dockerfordevelopers/shipitclicker@sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804 dbf01 sh -c 'echo "Testing" > /tmp/test | echo "File content is: $(cat /tmp/test)"'
现在,通过echo
命令显示文件系统已写入的确认信息,echo
命令会打印/tmp/test
的内容:
File content is: Testing
因此,为了避免文件系统可以被写入的第二种情况,始终包括--read-only
标志。
此外,记得不要在 Dockerfile 中包含敏感信息,如私钥和 API 令牌。你可以使用一些服务来避免这种情况,包括 HashiCorp Vault、Docker Swarm 以及云提供商(如 AWS)内置的服务,例如 SSM。第十四章,高级 Docker 安全——机密、机密命令、标记和标签,将更详细地讲解这些内容。
牢记一些最佳实践后,让我们来看看我们将用来构建自己镜像的命令,以及需要考虑的安全问题。
Docker 命令的安全性
我们很快将探讨构建过程以及如何从安全角度加强这一过程。为了做到这一点,然而,我们首先需要更详细地了解一些我们将要使用的命令,以便知道哪些是安全的,哪些可能构成潜在威胁。让我们从COPY
和ADD
命令开始。
COPY 与 ADD——这其中有什么区别?
当你开始构建镜像时,你需要将文件从主机复制到镜像中。通常,做这件事有两种方法。如果你做过一些在线研究,你可能见过类似“不要使用ADD
命令”的评论。那么,为什么呢?
ADD
命令允许我们递归地将文件复制到镜像中,类似于 Linux 中的cp -r
命令,如果需要,我们还可以通过zip
命令来压缩。简而言之,它扩展归档文件并创建目标位置不存在的目录。
命令的输入是一个 URL,可以引用本地或远程(归档)文件。正如你可以想象的那样,从远程位置拉取时,有很多风险需要考虑。
-
远程主机上的文件是否已被修改并遭到破坏?
-
你知道远程主机上的文件来源吗?
-
关于中间人攻击(MITM)有哪些考虑因素?
这个命令在 Dockerfile 中的使用示例如下:
ADD https://github.com/PacktPublishing/Docker-for-Developers/archive/master.zip /tmp/ch13/
在这种情况下,将下载并解压缩本书 GitHub 账户上托管的仓库的压缩版本到 tmp
目录。
我们之前讨论了使用 .dockerignore
文件来帮助保持镜像大小小巧。除了这个好处,它们还可以防止文件在使用 ADD
命令时被意外添加。例如,你可以确保像配置 .ENV
文件或类似文件不会被复制过去。
COPY
命令的工作方式与 ADD
略有不同。像 ADD
一样,它会递归地复制文件。然而,你必须明确提供源文件夹和目标文件夹。这意味着你必须声明文件的来源和去向。例如,从 A 复制到 B 的 ZIP 文件仍然是一个 ZIP 文件,不会展开,从而避免了任何意外后果。
我们可以看到这个命令语法的示例如下:
COPY master.zip /tmp/ch13
将添加文件的过程分解为多个步骤是更安全的做法,比如先下载文件、扫描文件,然后再复制文件。当访问远程内容时,你还应始终使用 SSL/TLS 连接。这可以通过实现加密安全和经过身份验证的通信路径,防止 MITM 攻击成为问题。
注意
MITM 攻击是指恶意方秘密窃听、转发或篡改两方之间的通信。
我们刚刚看过 COPY
命令如何避免一些 ADD
命令的问题,但递归复制呢?这里是否存在风险?
递归 COPY – 使用时请谨慎
正如你可能知道的,递归复制会将一个位置的内容复制到另一个位置,并包括所有嵌套的子文件夹和文件。
在使用 Docker 的递归复制命令时,有可能不小心将文件复制到镜像中,这是你并不打算复制的文件。
让我们看一个例子。在下面的截图中,我们可以看到一个示例目录,里面包含一个名为 oops
的文件夹和一个 my_secret
文件。这个文件包含了一个假设的秘密,比如一个 API 令牌,它被意外地留在了文件夹中:
图 13.3 – 示例:秘密被意外地留在源代码中
假设我们运行以下命令:
COPY . .
以及所有文件夹所在的父目录,这个秘密文件也会被复制过去,因为命令会递归复制所有内容,包括 oops
目录和我们的嵌套文件。
为了避免这些负面影响,更新你的 .dockerignore
文件,确保排除敏感文件类型总是一个好习惯。
正如我们之前提到的,如果你熟悉 .gitignore
文件,将文件类型添加到 .dockerignore
文件应该很简单。以下是一些需要记住的快捷规则:
# comment – the line is ignored
* # matches anything up to the * e.g. *.txt matches all text files
**# matches any number of folders e.g. **/*.txt matches all text files in build context
! #can be used to exclude a specific file e.g. !id_rsa.pub
tmp? # Any files or folder that start with tmp and include a subsequent character are
#ignored
*/tmp* # Will exclude any directories or files starting with tmp directly below root
*/*/tmp* # Similar to the above however works for two directories below root
使用这些机制,你可以确保各种文件被排除在容器之外,比如 *.pem
和 *.ENV
文件。
因此,如果你确实计划在 Dockerfile 中使用递归复制,请确保 .dockerignore
文件是最新的,并且已经审计过你的应用程序,确保所有被复制的内容都是按预期进行的。
现在,让我们关注构建过程以及如何在这个阶段提升安全性。在这里,我们将看到 COPY
等命令如何作为更大过程的一部分发挥作用。
构建过程的安全性
我们已经看到如何拉取镜像并以安全的方式运行它们。那么,自己构建容器镜像呢?正如你现在已经熟悉的那样,某些命令在加入到 Dockerfile 时会带来额外的风险。在本章的这一部分,我们将探讨如何使用我们迄今为止学到的技巧来确保构建过程的安全。这将包括使用一个最小的基础镜像(shipitclicker
)作为起点,然后在运行容器时,使用我们已对该镜像进行过安全测试的调整。
使用多阶段构建
正如我们之前所说,我们需要小心处理机密信息,确保它们不会被意外泄露。避免这种情况的一种方法是不要将它们包含在 Dockerfile 中。然而,在构建阶段呢?你很可能会不时需要使用私钥来配合构建过程,例如,从一个使用公钥加密保护的远程服务中拉取代码。
使用密钥的一个安全方法是通过使用多阶段构建。这个过程使用一个一次性中间层,确保数据不会意外泄露到最终的构建过程中。让我们看一个简单的示例。如果你希望运行这段代码,你需要运行一个 SSH 服务器并将你的公钥添加到它。
如果你还没有运行 SSH 服务器,可以重复使用位于 docs.docker.com/engine/examples/running_ssh_service/
的 Dockerfile 来构建一个运行 SSH 的容器。
接下来,让我们看一个多阶段构建过程的示例,以及如何将其与访问 SSH 服务结合使用。
将以下代码复制到一个新的 Dockerfile 中,以便你进行操作。在运行 SSH 服务器的容器上,添加一个名为 file.txt
的文件,然后更新 Dockerfile 代码,包含你的用户、IP/主机名,以及你刚创建的文件的路径。
在我们构建之前,先快速了解一下这里发生了什么:
FROM dockerfordevelopers/shipitclicker@sha256:39eda93 d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01 as intermediate
WORKDIR /test
ARG ssh_prv_key
RUN echo "$ssh_prv_key" > /tmp/id_rsa_test
RUN chmod 600 /tmp/*
RUN apk add openssh
RUN scp -i /tmp/id_rsa_test user@server:/path/to/file.txt .
FROM dockerfordevelopers/shipitclicker@sha256:39eda93 d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
WORKDIR /test
COPY --from=intermediate /test .
这段代码做了几件事情。首先,它将我们的 shipitclicker
镜像作为一个中间构建步骤。
接下来,它将 WORKDIR
设置为 test,并创建一个名为 ssh_prv_key
的 ARG
值。这个 ARG
值将允许我们传入连接远程 SSH 服务器所需的 RSA 私钥的路径。
根据我们的输入,我们将其作为文件输出,然后设置该文件的权限为 600
。接着,我们安装 openssh
,以便可以使用 scp
命令行功能。接下来才是有趣的部分。
RUN scp
命令将注入的私钥用于连接远程服务器,以检索名为file.txt
的文件,并将其复制回当前目录。这一步完成了构建的第一阶段。
在第二阶段,我们再次使用shipitclicker
镜像,并使用相同的WORKDIR
,即test
。然而,最后一行才是魔法发生的地方。它从我们在第一阶段完成的中间步骤中复制出从远程服务器检索到的文件,并将其复制到最终的构建阶段。
从结果中可以看到,最终的容器并不包含我们用来从远程 SSH 服务器检索文件的私钥,因此它不会意外地进入最终的容器中。
一旦你有了一个远程位置来复制文件,可以使用以下命令来构建这个 Dockerfile:
$docker build --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa_test)" .
正如你从这个可以猜到的,ssh_prv_key
构建参数实际上只是我们的私钥的值,连接到变量中。
一旦我们构建好了容器,当我们运行它时,我们希望确保它不会消耗超过所需的资源。这有助于在发生不幸的安全漏洞时减轻损害。
最后需要注意的是,多阶段构建还可以帮助保持镜像的小巧,这是一种理想的特性,正如前面所讨论的。接下来让我们来看看如何进一步限制 Docker 中的功能和资源使用。
在部署构建时限制资源和能力
你可以限制容器可用的各种资源,包括 CPU 使用率和内存。这有助于防止拒绝服务攻击。在这种情况下,容器会被利用去消耗主机的底层资源,从而导致整体性能下降,甚至更糟,导致底层主机崩溃。
此外,访问控制机制是确保容器不仅限制使用的资源,还限制权限和访问的重要组成部分。
限制资源
为了避免前面提到的 DOS 攻击类型,我们可以使用一组合适的标志来限制容器可以消耗的底层主机资源。
我们将首先关注的领域是内存。Docker 使我们能够通过硬限制和软限制的结合,限制容器能够使用的内存。
我们可以使用-m
/--memory
标志为容器设置硬限制。这将为指定的内存量预留,并且不允许容器超过此限制。如果容器被攻破,硬限制功能将防止恶意进程不断消耗底层主机的内存。
设置内存限制时,确保它与你的应用程序预期的功能相匹配。内存过少可能会防止在容器被攻破时发生问题,但可能不足以运行你的应用程序。
–memory
标志也可以与 –memory-reservation
标志一起使用。第二个特性允许你指定一个比 –memory
更小的软限制。当 Docker 发现底层宿主机出现问题,例如内存不足时,它会激活此特性。激活后,Docker 将尝试限制容器可用的内存量。
与内存类似,我们也需要意识到被利用的容器可能会消耗比预期更多的 CPU 资源,这反过来可能会对宿主机产生负面影响。
注意
如果你使用的是 Docker 1.12 或更低版本,你需要使用 –cpu-period
和 –cpu-quota
标志,而不是 –cpus
标志。
使用 –cpus
标志,你可以定义容器可访问的 CPU 数量。如果你有多个 CPU(例如,四个),并且将值设置为 –cpus="2"
,容器将被限制为最多只能使用两个 CPU,而不能更多。
我们已经看到了如何使用一些标志来限制容器在运行时可用的资源。接下来,我们来看看一些其他的标志,可以进一步限制运行容器时可能的安全风险。
删除能力
一些可以帮助避免其他风险的技术包括在运行容器时删除能力。能力是 Linux 的一个特性,它将与 root/超级用户账户相关的权限分割成独立的组件。
容器通常具有的能力列表包括chown
、dac_override
、fowner
、fsetid
、kill
、setgid
、setuid
、setpcap
、net_bind_service
、net_raw
、sys_chroot
、mknod
、audit_write
和setfcap
。要了解每项能力的具体功能,请参考 Linux man-pages 文档:man7.org/linux/man-pages/man7/capabilities.7.html
。
要删除像 chown
这样的能力,你可以在运行容器时使用 –cap-drop
标志。请参考以下示例:
$ docker run -d -it --cap-drop=chown --security-opt=no-new-privileges dockerfordevelopers/shipitclicker@sha256:39eda 93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
删除生产容器不需要的强大能力,可以帮助防止攻击者突破容器进行攻击。
本章内容到此结束,关于提高基础安全态势的技术。我们在进入更高级的技术之前,快速回顾一下到目前为止学到的内容。
摘要
在本章中,我们回顾了一些基本步骤,确保在拉取镜像、构建和运行容器时,能够减少攻击面。
我们学习了如何确保从 Docker Hub 拉取的镜像是安全的。此外,我们还了解了如何使用只读权限来防止对文件系统的写入访问。
我们讨论了多阶段构建,以展示如何将容器构建过程拆分为多个步骤,从而确保不会意外地将 SSH 密钥等包含在最终产品中。我们从安全角度简要回顾了 .dockerignore
文件,最后,我们讨论了如何通过移除能力来限制系统资源并实施访问控制。
在下一章中,我们将探讨如何通过使用扫描工具和实施监控来自动化一些安全流程。
第十五章:高级 Docker 安全性——秘密、秘密命令、标记和标签
到目前为止,我们已经看到多个需要使用包含秘密数据的文件的例子。我们可以将“秘密”视为一个通用术语,指的是通常存储在配置文件和环境文件中的敏感数据类型,如数据库访问凭证或 API 令牌。Docker 提供了一种方便的方法来保护这类数据并进行共享。对于使用 Swarm 模式而非 Kubernetes 的遗留系统,了解如何在这些环境中应用安全措施非常重要,因为在你的职业生涯中,可能需要对这些环境进行后期修复。
除了管理秘密数据外,我们还可以使用标签和标记来帮助确保我们在进行安全性工作时考虑到这些因素。你在上一章中已经看到了标记,我们将在本章后续内容中进一步探讨这些。
此外,我们还将探讨如何使用元数据标签提供容器的额外信息,以及如何使用 security.txt
文件。
在本章中,我们将涵盖以下主要主题:
-
在 Docker 中安全存储秘密的介绍
-
什么是秘密以及我们为什么需要它们
-
遍历 Raft 日志文件
-
向 Swarm 中添加、编辑和移除秘密
-
更有效地使用标签,确保我们使用安全的镜像
-
实现元数据标签和 secrets.txt 文件
让我们从了解 Docker 秘密是什么以及它们为什么有益开始。
技术要求
本章,你需要访问一台运行 Docker 的 Linux 机器。我们建议你使用本书中到目前为止所使用的设置。
此外,你还需要一个 Docker Hub 账户,以便访问那里的镜像。如果你还没有设置,可以通过以下链接进行设置:
最后,为了探索使用 Docker 秘密,你需要设置至少两个容器并使用 Docker 的 Swarm 功能。你可以在这里了解更多关于 Swarm 模式的信息:docs.docker.com/engine/swarm/
查看以下视频,看看代码是如何运行的:
在 Docker 中安全存储秘密
处理复杂的网络化软件项目时,不可避免的一部分就是要处理秘密数据。这可以包括像 SSH 访问的私钥、SSL 证书、密码和 API 密钥等各种数据。
为了在多个容器之间安全共享秘密,当然,你需要避免尝试将秘密存储在容器本身中,避免以允许潜在攻击者访问的方式存储秘密。这个抽象层不仅对于根据环境管理不同的凭证集有用,而且还提供了一层额外的安全保障,以防容器以某种方式被破坏。
幸运的是,Docker 提供了一个有用的功能来实现这一目标,名为 Docker secrets。要使用这个功能或 Kubernetes 对应的功能,你需要实现 Swarm 服务或 Kubernetes 本身。正如我们在本书的其他地方所建议的,如果可能的话,你可能希望避免使用 Swarm 服务,转而使用 Kubernetes。然而,在处理使用 Swarm 的遗留系统时,你可能需要与之合作,因此理解在这种情况下如何使用 secrets 是很重要的。考虑到这一点,容器应作为服务运行。
Mirantis 在 2020 年 2 月收购 Docker 后,承诺对 Docker Swarm 提供长期支持(www.mirantis.com/blog/mirantis-will-continue-to-support-and-develop-docker-swarm/
)。你可能已经在 第五章《在生产环境中部署和运行容器的替代方案》中熟悉过这个概念;然而,如果你需要复习,可以按照 Docker 网站上提供的步骤开始使用 Swarm 模式,作为 Kubernetes 的替代方案:
docs.docker.com/engine/swarm/swarm-tutorial/
Swarm 和 Kubernetes 中的机密功能允许你集中管理数据(如密码和 API 密钥),然后将其安全地共享给你选择的容器。这避免了在容器内不安全地硬编码值,或者让所有容器都能访问敏感数据。
此外,当通过 Docker secrets 与 Swarm 中的其他容器共享机密时,这些机密会通过 SSL/TLS 加密的安全连接进行传输。现在,让我们更深入地了解 Docker secrets 如何在基础层面上工作,包括一个名为 Raft 日志的重要功能。
Raft 日志
为了在 Swarm 节点之间共享内容,我们需要确保既有共识又有容错能力。简而言之,这意味着网络中的所有节点都需就某一组值达成一致,以保持一致的状态。
Docker Swarm 使用的算法叫做 Raft。你可以在论文《寻找可理解的共识算法》中阅读更多技术细节,论文可在 Raft 的 GitHub 账户上找到:
Docker Swarm 使用一个称为 Raft 日志文件的文件,作为其实现算法的一部分。这个文件的好处在于它可以用于存储机密数据,这些数据随后需要在 1 到 n 个节点之间共享。当通过 docker secret
命令添加机密时,一个值会被添加到 Raft 日志文件中,然后通过临时文件系统提供,如下所示的示例:
/run/secrets/apikey
本质上,这就是如何在 Docker Swarm 中的多个容器之间共享一个秘密。应用程序读取秘密的方式取决于你使用的语言。例如,如果你在修改 ShipIt Clicker 应用程序,你会使用 JavaScript。如果我们有一个 API 密钥文件之类的秘密,我们可以直接在 JavaScript 源代码中使用 fs
模块访问它,如以下示例所示:
fs.readFile('/run/secrets/apikey', 'utf8')
如你所见,这是一种相当简单的方法。
尽管此文件是加密的,但我们也可以通过锁定提供额外的安全层。
可以使用 --autolock
标志锁定 Swarm,以防止攻击者解密 Raft 日志文件。
请参阅 Docker 文档以获取更多详细信息:
docs.docker.com/engine/swarm/swarm_manager_locking/
现在你已经基本了解了 Docker secrets 功能的工作原理,我们来看看如何使用它。
添加、查看和删除秘密
现在我们将开始探索与 secrets 相关的各种命令。
如果你愿意尝试 Kubernetes 等效的命令,也可以替换本节中的命令。你可以在 kubernetes.io/docs/concepts/configuration/secret/
上找到 kubectl
命令的列表。
或者你可以参考 第八章,将 Docker 应用部署到 Kubernetes,我们在那里通过 kubectl
创建、描述、检索和编辑了秘密。
关于 Docker,我们将首先从创建秘密开始。
创建
create
命令是我们将新秘密添加到 Raft 日志文件中的方法。其基本格式如下:
docker secret create [OPTIONS] SECRET [file|-]
你可能会注意到这与 kubectl
中的命令类似,即 kubectl create secret
。
创建秘密时,我们可以使用 -l
标志为秘密添加标签,如下所示:
docker secret create -l key=val api_key -
这使我们能够为值添加标签,从而知道它们是针对哪个环境的。例如,我们可以为环境添加一个键值对,如质量保证(QA)、开发(DEV)和生产(PROD)。
一个秘密也可以是一个文件。例如,如果我们想添加一个私钥,可能会如下操作:
docker secret create my_key ./id_rsa
如果你希望将秘密添加或更新到正在运行的服务中,你需要在 update
命令上使用 --secret-add
标志。例如,可以按如下方式操作:
docker service update --secret-add <secret> <service>
在添加了一个秘密后,让我们来探讨一下如何查看它。
查看
有很多方法可以用来查看 Docker secrets。要列出添加到 Raft 日志文件中的任何秘密,我们可以使用 ls
命令:
$ docker secret ls.
执行此命令后,当前的秘密将被显示,如下所示:
ID NAME CREATED UPDATED
123345 my_key 2 weeks ago 2 weeks ago
我们可以使用 inspect
命令收集有关该秘密的更多信息。
其格式如下:
docker secret inspect [OPTIONS] SECRET [SECRET...]
所以,使用前面的示例,我们可以如下执行命令:
$ docker secret inspect my_key
然后,这将返回一个包含 ID、版本、创建和更新日期的 JSON 对象,并且包含标签和名称的spec
对象。现在提供了一个该输出的示例:
[
{
"ID": "ae4kfwe6s56sgop7vn1kxap59",
"Version": {
"Index": 10
},
"CreatedAt": "2020-01-26T07:15:29.674382561Z",
"UpdatedAt": "2020-01-26T07:15:29.674382561Z",
"Spec": {
"Name": "my_key",
"Labels": {
"env": "dev",
"rev": "20200126"
}
}
}
]
我们已经添加并检查了机密,现在我们将探索如何在不再需要它们时将其删除。
删除
删除一个机密与添加一个机密一样简单,语法与 Linux 删除文件的命令rm
相同。
命令的格式如下:
docker secret rm SECRET [SECRET...]
在 Kubernetes 中,相应的命令是kubectl delete secret
。
要删除我们之前的示例机密,我们可以运行如下命令:
docker secret rm my_key
如果你希望删除当前服务正在使用的机密,你需要在update
命令中使用--secret-rm
标志,如以下示例所示:
docker service update --secret-rm <secret> <service>
如你所见,添加、删除和检查机密非常简单。现在,让我们尝试使用第十三章中的 SSH 文件,Docker 安全基础与最佳实践,运行前面的命令。
机密操作 – 示例
现在是时候尝试我们刚才回顾过的命令(create
/inspect
/ls
/rm
)了。确保你的设置已经配置为使用 Swarm。你也可以重用上一章中的镜像进行此部分的操作。可以使用以下命令获取该镜像:
$ docker pull docker pull dockerfordevelopers/shipitclicker@ sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef 69804dbf01
重要提示
记住,你可以使用docker swarm init
命令初始化 swarm。也可以使用--advertise-addr
标志,并指定你初始容器的 IP 地址。
之前,我们使用以下命令为 SCP 添加了一个 SSH 私钥,供单个容器使用:
$ docker build --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa_test)" .
要将此密钥添加到我们的 swarm 中,我们将使用以下命令:
$ docker secret create -l env=dev ssh_prv_key ~/.ssh/id_rsa_test
在这里,我们创建了一个与之前使用的构建参数相同名称的新机密,并将我们的私钥内容输出到其中。我们还包含了一个标签,标签的key=val
对表示我们所在的环境。在此情况下,它是开发环境。
现在让我们检查一下我们是否正确添加了它。我们可以通过运行ls
命令来验证:
$ docker secret ls
ID NAME CREATED UPDATED
To5jj... ssh_prv_key 1 minutes ago 1minutes ago
在这里,我们可以看到机密的 ID 和名称。看起来不错!现在我们使用NAME
值在密钥上执行inspect
命令:
$ docker secret inspect ssh_prv_key
现在,你应该能看到一个类似以下内容的 JSON 对象:
[
{
"ID": "to5jjgshjqaddhf56ty89rss42",
"Version": {
"Index": 17
},
"CreatedAt": "2019-11-25T07:11:03.335174723Z",
"UpdatedAt": "2019-11-25T07:11:03.335174723Z",
"Spec": {
"Name": "ssh_prv_key",
"Labels": {
"env": "dev",
"rev": "20181125"
}
}
}
]
如果你在 Swarm 中有多个容器,那么你可以授予它们访问这个机密的权限。以下示例演示了如何将我们刚刚创建的机密发送到一个使用我们示例镜像的新容器:
$ docker service create --name second_container --secret source=ssh_prv_key,target=second_ssh_prv_key,mode=0400 dockerfordevelopers/shipitclicker@ sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef 69804dbf01
在这里,--secret source
值设置为我们创建的 Docker 机密的名称。然后,我们将它存储在目标值中定义的变量里。为了清晰起见,我们将其命名为second_ssh_prv_key
。模式已设置为0400
,以便使机密可访问,并选择我们标记过的镜像作为create
命令的源镜像。
要确认秘密是否可用,我们可以检查我们早些时候讨论过的临时文件系统。为此,您需要获取新容器的容器 ID。您可以使用docker ps
命令来执行此操作。
接下来,使用容器 ID 如下:
$ docker exec -it <id> cat /run/secrets/second_ssh_prv_key
您应该看到秘密的内容与您传递给第一个容器的内容相同,即我们迄今为止一直在测试的私有 SSH 密钥。
其他选项
除了使用本机 Docker 和 Kubernetes 工具外,还存在各种其他选项可用于在基于云的系统中存储秘密。AWS、GCP 和 Azure 提供本机支持,而 HashiCorp 通过 HashiCorp Vault 提供了一种全面的与云无关的秘密管理机制,网址为www.vaultproject.io/
。
现在,我们将进一步了解 Docker 秘密,以及如何使用标签。
Docker 安全标签
我们刚刚看到如何确保在容器集群中安全共享秘密。在第十二章,容器安全介绍,我们学习了如何使用标签结合其他安全功能,确保使用正确的镜像。
现在,我们将看到如何通过使用标签与秘密和标签结合,我们可以注释给定秘密和标签在哪个环境中使用。
作为良好的安全实践,我们应始终在不同环境中使用不同的秘密。例如,在您的开发、演示和生产实例中,数据库访问密码不应相同。通常,在您的开发过程中,与生产环境相比,您可能会在研究、开发和 QA 环境中使用更新版本的容器。
我们可以使用 Docker 标签来帮助确保一旦为开发环境设置了凭据/秘密,我们也会拉取正确的镜像;也就是说,我们打算用于开发目的的镜像与我们创建的开发凭据。使用固定的标签通过不可变性提供了一层安全性,并防止意外地在开发环境之外使用可能包含安全漏洞的实验性镜像。
通常,应采用语义化版本控制(semver.org/
)。这将导致标签使用一种格式,以传达您在使用版本时应期待的变更级别。主要版本号指示了一组不向后兼容的更改。次要版本通常是对现有版本的新功能。最后,我们有了修补程序版本,这可能是一个小的安全修复或类似的内容。一个典型的格式可能如下:
1.1.2
在选择标签时,与您的版本控制系统一致,选择最接近您希望部署的环境的标签。例如,选择:1.1.2-dev
而不是:1
。
在这个例子中,你知道你将要拉取补丁发布。然后,你可以通过docker secret
部署凭证,专门为这个构建和你部署的环境。例如,一种有用的方法是将密钥标签与使用的标签版本配对,如以下代码所示:
$docker secret create --label ver=1.1.2-dev \
--label env=dev \
ssh_prv_key ~/.ssh/id_rsa_test
在这个示例中,已经创建了一个密钥(一个 SSH 密钥),并且我们知道它应该与标签版本1.1.2
一起使用,并且这是一个开发环境。这里,标签提供了注释,给我们提供了密钥的上下文。像这样的简单技巧有助于向工程团队提供更多信息,避免生产凭证被错误地与实验性开发容器一起使用或用于错误的环境。
我们已经看到如何将标签、密钥和标签结合使用。现在让我们看看其他标记选项。
使用标签进行元数据应用
元数据标签是一种在容器中注释额外信息的方式,旨在为开发团队提供有用的信息。当团队中的其他开发者需要了解镜像的关键特性,比如版本号和描述时,这会非常有用。
我们通过docker secrets
命令看到了如何通过命令行添加标签。使用元数据标签,我们还可以将标签添加到 Dockerfile 中,这样在构建新容器时,这些信息就会被内嵌。
标签采用以下格式:
LABEL key=value
在我们之前的示例基础上,我们可以通过 Dockerfile 在容器内设置版本,代码如下:
LABEL "version"="1.1.2-test"
LABEL "description"=" Development environment container for testing the newest security patch. Not for production release yet"
一旦你构建了一个容器,你可以使用docker inspect
命令查看任何你添加的元数据:
"Labels" :{
"version"="1.1.2-test",
"description"=" Development environment container for testing the newest security patch. Not for production release yet"
}
在发布供公众使用的软件时,你还应考虑链接到一个security.txt
文件。就像行为规范或贡献者指南一样,它提供了一种机制,提醒安全研究人员如何负责任地披露他们可能发现的任何安全问题。
你可以从以下网站自动生成一个security.txt
文件:
将此文件保存到你的代码库中,然后通过 Dockerfile 中的LABEL
链接它,如以下示例所示:
LABEL "security.txt"="https://respository.example.com/my_project/security.txt"
这就是我们关于密钥、标签和标签的指南总结。让我们回顾一下到目前为止学到的内容。
总结
在这一章中,我们了解了关于 Docker 密钥的所有内容,它是 Kubernetes 密钥的对等物。我们看到如果需要使用这种技术而不是 Kubernetes 时,如何安全地在容器之间共享敏感数据。我们还了解了这对于基于工作环境划分凭证集有多么有用。最后,我们演示了如何创建、检查和删除这些密钥。
接下来,我们再次回顾了标签,并讨论了如何使用这些标签确保从正确的环境中拉取正确的镜像。我们展示了环境相关的密钥和标签的结合,帮助你进一步保护开发过程。
最后,我们讨论了如何为容器添加元数据标签。这也包括了使用security.txt
文件。
在下一章,我们将探讨如何利用第三方工具帮助我们保护容器,并加强我们迄今为止学到的一些实践方法。
第十六章:扫描、监控和使用第三方工具
到目前为止,我们已经探讨了如何手动配置 Docker 容器,确保安全性优先。在本章中,我们将介绍一些可以自动扫描镜像并监控生产负载的工具。这将为你提供一个出发点,让你能够根据使用的云服务提供商进一步扩展基于 Docker 的项目。
我们将首先查看 DevOps 解决方案,例如 Anchore Engine,用于扫描镜像中的安全漏洞,回顾 docker stats
并了解它的用途,设置 cAdvisor 进行本地监控,并理解如何将 Datadog 作为基于云的解决方案,用于收集容器统计数据。
本章还将简要回顾 AWS 的安全选项,包括用于监控生产环境的 GuardDuty,并介绍微软 Azure 提供的一些功能。你将了解谷歌云平台(GCP)用户可以使用的工具,并将 Datadog Agent 部署到你的容器环境中。
在本章中,我们将涵盖以下主要内容:
-
扫描和监控 – 云端与 DevOps 安全性在容器中的应用
-
使用 AWS 保护你的容器
-
使用 Azure 保护你的容器
-
使用 GCP 保护你的容器
让我们开始了解监控容器、扫描安全问题的技术。
技术要求
对于本章,你需要访问一台运行 Docker 的 Linux 机器。我们建议你继续使用本书中至今为止的设置。
除此之外,你还需要在 Docker Hub 上注册一个账户,以便访问其中的镜像。如果你之前没有设置过账户,可以通过hub.docker.com:
进行注册。
为了使用本章中探讨的许多程序,你需要从网上下载它们。我们将在相关部分提供链接,以便你知道从哪里获取它们。在某些情况下,你可能需要注册账户才能使用某项服务或下载某个工具。
查看以下视频,观看代码的实际应用:
扫描和监控 – 云端与 DevOps 安全性在容器中的应用
在我们开始查看用于监控和扫描容器的具体工具之前,首先需要明确定义在安全环境中我们所说的监控是什么意思。
正如你在本书中所见,容器提供了一种在小型自包含环境中提供应用程序的机制。然而,我们需要确保发布的软件在运行时不会出现性能下降。例如,我们需要知道一个容器是否消耗了大量资源,从而影响我们环境的整体性能。你可能已经从第十章《使用 Prometheus、Grafana 和 Jaeger 监控 Docker》中对这一概念有了一定的了解。
此外,监控使我们能够查找可能表明系统遭受攻击或已被某种方式入侵的异常情况。虽然本书的其他部分集中于确保系统稳定性和性能的监控,但我们将从安全的角度使用这些概念。安全扫描应用程序是任何工具链的重要组成部分,但可能无法发现所有问题,特别是较新的漏洞。因此,查找恶意软件存在的负面副作用是一个重要的防御机制。因此,在发布前进行扫描、发布后进行监控以及事件响应是运行生产容器系统的重要部分。
关于沙箱环境的说明
另一个可能有用的概念是沙箱环境。沙箱为隔离和测试不受信任的代码提供了一个环境。这些环境对于审查可能感染恶意软件的容器非常有用,且无需冒着影响实时系统或团队使用的开发环境的风险。
在本章中,我们将首先了解 CI/CD(DevOps)流水线中的扫描阶段,然后再探讨如何将监控工具与它们结合使用,以保护我们的系统。让我们从使用 Anchore Engine 扫描容器开始。
使用 Anchore Engine 进行扫描
在构建 DevOps 流水线时,扫描我们的容器以查找安全问题是一个重要的考虑因素。典型 CI 过程的最后一步之一是构建容器本身,此前我们已经测试了计划部署到其中的软件。正如你在本书中所见,我们在容器中实验了多种技术。尽管每种语言都有许多安全工具,无论是 JavaScript 还是 PHP(这些在本书的范围之外),但我们不应忽视通过使用自动化工具减少容器级别的手动安全负担。
虽然我们已经看到拉取签名镜像的重要性,但扫描它们也无妨。正如俗话所说,小心驶得万年船!
如果我们发现构建中包含的镜像已被入侵,或某个标签违反了内部工作安全政策或合规性规定,我们就知道整个构建因此容易受到攻击,并可以防止其进入生产环境。
因此,我们可以将安全扫描过程视为以下两个相互关联的步骤:
-
查看我们在
Dockerfile
中包含的镜像,以及Dockerfile
本身的配置。 -
确保容器符合我们可能有的任何内部要求,比如不使用黑名单中的镜像。在这种情况下,镜像可能并非仅因安全原因而被列入黑名单,还可能是出于性能考虑。
为了适应这两个因素,我们需要一个容器扫描工具,它允许我们在标准安全考虑的基础上定义自己的策略。
市场上最受欢迎的开源工具之一,允许我们实现这两个目标的是 Anchore Engine。你可以访问其官方网站:anchore.com/engine/
。
除了我们很快将要探讨的大量功能外,它还是一个开源项目。所以,如果你想为它做出贡献,确保访问 GitHub 仓库:github.com/anchore/anchore-engine
。
Anchore 的核心是一个扫描容器安全问题的引擎。它可以轻松集成到你的 CI 流水线中,在部署前提供漏洞和策略扫描。让我们看看如何安装并对最新的 Alpine 镜像进行基本扫描。
安装 Anchore Engine
安装 Anchore Engine 非常简单。首先,我们需要从产品的引擎部分开始。让我们创建并进入一个名为 aevolume
的新目录:
$ mkdir ~/aevolume
$ cd ~/aevolume
接下来,拉取最新版本的 Anchore Engine:
$ docker pull docker.io/anchore/anchore-engine:latest
现在,我们可以运行 Docker 的 create
命令:
$ docker create --name ae docker.io/anchore/anchore-engine:latest
使用 curl 获取 docker-compose.yaml
你也可以通过 curl 复制 docker-compose.yaml
,使用:curl
docs.anchore.com/current/docs/engine/quickstart/docker-compose.yaml
> docker-compose.yaml
将 docker-compose
文件复制到当前目录,然后删除创建的 ae
文件夹:
$ docker cp ae:/docker-compose.yaml ~/aevolume/docker-compose.yaml
$ docker rm ae
最后,运行 pull
和 up
命令,如下所示:
$ docker-compose pull
$ docker-compose up -d
接下来,我们需要安装一个可以与引擎交互的 CLI。你有几个选择,其中包括 Docker 容器:
$ docker pull anchore/engine-cli:latest
你还可以使用这里列出的一种方法,这将把 CLI 安装到你本地的机器上:github.com/anchore/anchore-cli
。
可以使用以下命令安装 Python 版本的 CLI:
apt-get update
apt-get install python-pip
pip install anchorecli
如果你已经拉取了容器镜像并希望使用默认凭据,请运行以下命令进入 CLI shell:
$ docker run -it anchore/engine-cli
在接下来的部分中,我们将使用 Python 版本的 CLI 来与引擎交互。
你现在可以在容器 shell 内或如果手动安装了 CLI,也可以从 CLI 执行命令。以下示例演示了如何通过 CLI 调用端点,传递凭证和端点,并请求系统状态信息:
$ anchore-cli --u admin --p foobar --url http://localhost:8228/v1/ system status
现在你应该在控制台中看到一些状态结果,表明引擎已启动:
Service analyzer (anchore-quickstart, http://engine-analyzer:8228): up
Service simplequeue (anchore-quickstart, http://engine-simpleq:8228): up
Service policy_engine (anchore-quickstart, http://engine-policy-engine:8228): up
Service apiext (anchore-quickstart, http://engine-api:8228): up
Service catalog (anchore-quickstart, http://engine-catalog:8228): up
Engine DB Version: 0.0.12
Engine Code Version: 0.6.1
现在让我们回顾一下扫描步骤。
添加并扫描镜像
让我们尝试使用 Anchore Engine 扫描最新的 Alpine 容器。你会记得,Alpine 是我们 shipitclicker
镜像版本 0.1 到目前为止使用的基础操作系统。因此,确认它没有问题是一个不错的第一步。
当我们运行扫描时,它会根据一组策略检查镜像。Anchore 中的策略是镜像必须通过的白名单和检查的集合。
启动扫描的过程如下:
-
让我们通过执行以下命令,使用 CLI 添加 Alpine 镜像:
$ anchore-cli --u admin --p foobar --url http://localhost:8228/v1/ image add alpine:latest
-
当此步骤成功完成时,你应该会看到类似以下的内容。这告诉我们镜像已经被添加:
Image Digest: sha256:ddba4d27a7ffc3f86dd6c2f92041af252a1 f23a8e742c90e6e1297bfa1bc0c45 Parent Digest: sha256:ab00606a42621fb68f2ed6ad3c88be54397f 981a7b70a79db3d1172b11c4367d Analysis Status: not_analyzed Image Type: docker Analyzed At: None Image ID: e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29 e8e95d75ca6a99776a Dockerfile Mode: None Distro: None Distro Version: None Size: None Architecture: None Layer Count: None Full Tag: docker.io/alpine:latest Tag Detected At: 2020-02-04T16:22:19Z
-
我们的镜像尚未通过 Anchore 分析。这是我们提取和分类元数据的地方。所以,让我们按以下步骤将镜像移动到这个状态:
$ anchore-cli --u admin --p foobar --url http://localhost:8228/v1/ image wait alpine:latest
-
完成后,我们现在可以使用此命令在 Alpine 镜像上运行漏洞扫描。在这里,我们使用
os
属性检查操作系统级别的包漏洞。除了os
,我们还可以选择检查non-os
(这包括语言特定的包,如 Python PIP 和 Ruby GEM 类型)和all
:$ anchore-cli --u admin --p foobar --url http://localhost:8228/v1/ image vuln alpine:latest os
如果一切顺利且镜像通过,你将不会在屏幕上看到任何漏洞。
如果发现漏洞,它将以以下格式返回:
Vulnerability ID Package Severity Fix Vulnerability URL CVE-1111-1111 package.zip Negligible None https://somewebsite
默认情况下,基本的 Anchore 安装策略将扫描 CVE 问题和 Dockerfile 问题,像我们在前几章中探讨过的那些问题。
现在你已经配置了扫描引擎,可以开始构建自己的策略并进行扫描。更多信息,请参见 Anchor 策略文档:
docs.anchore.com/current/docs/using/cli_usage/policies/
此外,要查看你可以复制和修改的策略示例,请查看 GitHub 上的 Anchore Hub 页面:
无论是定义自定义策略还是重用其他策略,这些 JSON 文件都可以通过 CLI 添加:
$ anchore-cli policy add /path/to/image/policy/bundle.json
一旦添加完成,就可以使用 activate
命令激活它们:
$ anchore-cli policy activate <Policy ID>
如果你需要知道一个策略 ID,可以使用 CLI 中的policy list
命令:
anchore-cli --u admin --p foobar policy list
作为实验,你可能希望在 Docker for Developers Docker Hub 仓库中的其他镜像上运行默认的或你自己的策略:
hub.docker.com/r/dockerfordevelopers/shipitclicker/tags
这涵盖了启动和运行的基础。如果您希望将扫描功能添加到您的 DevOps 流水线中,Anchore 可以与多个 CI/CD 系统集成,包括以下系统:
-
CloudBees
-
GitHub
-
GitLab
-
CircleCI
-
Codefresh
每个平台的集成说明可以在 Anchore 网站上找到:
docs.anchore.com/current/docs/using/integration/ci_cd/
Anchore 还包括一个 Jenkins 插件,因此您可以尝试将其与本书中我们早些时候完成的 Jenkins 设置集成:
plugins.jenkins.io/anchore-container-scanner/
在我们继续了解监控工具之前,先简单提一下另一个工具。
简要提及 Chef InSpec
另一个您可能感兴趣的扫描容器基础设施的工具是 Chef InSpec。
Chef InSpec 是一个开源框架,类似于 Anchore,但它专注于测试和审计所有应用程序和基础设施。这包括对 Docker 进行审计测试。如果您在寻找一个不仅限于容器环境的基础设施一体化解决方案,这可能符合您的需求。
注意
本书的范围不包括 InSpec 的完整操作指南,但如果您想了解更多,可以访问 InSpec 网站的文档门户:www.inspec.io/docs/
。
总结来说,我们可以在部署容器之前对其进行扫描,以检查其安全性。现在让我们继续,了解 Docker stats 用于容器监控。
使用 Docker stats 进行本地原生监控
现在我们已经部署了容器,并相信它们是安全的,我们应考虑使用监控工具来检查性能,并在出现问题时帮助调查。
在探索云中一些复杂且全面的工具之前,我们可以使用 Docker 的原生 stats 工具快速概览容器的健康状况。如果您怀疑某些软件可能在容器中异常消耗资源,例如怀疑 Web 应用程序可能感染了未在 CI 阶段被发现的矿工程序,这时该工具会非常有用,尤其是当您快速测试一个孤立沙箱环境中的容器时。
注意
在虚拟机沙箱中运行容器,并允许您探查性能指标,能够在不冒感染底层机器风险的情况下安全地扫描容器的安全问题。
要访问容器性能数据,您可以执行以下命令:
$ docker stats <container id>
对于每个容器,您将看到 CPU 使用率、内存使用率、内存限制(MEM
)、% NET I/O
和最后的 BLOCK I/O
。以下示例展示了一个典型的输出:
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O
ebb12326ae94 1% 73.63 MiB/490 MiB 15.02% 90.2 MB/275.5 MB 26.8 MB/873.7 MB
虽然 stats
命令在进行本地开发或快速获取系统性能快照时很有用,但收集更全面的指标会更好。实现这一目标的一种方法是使用 Stats API。我们现在简要了解一下它,并考虑它的安全影响。
使用 Stats API
Stats API 是一组更全面的结果,以 JSON 格式返回,并可以通过 Docker 套接字访问:
$ /var/run/docker.sock
你应该还记得在第十二章中的 《守护进程套接字安全》 部分提到,我们需要确保攻击者无法攻破套接字,并利用它获得对底层主机的 root 访问权限。我们可以通过使用 TLS 加密流量来实现这一点。如果你需要帮助设置,可以参考这一章。
Stats API 使用 REST 架构,因此接受 HTTP 请求作为查询。你可以在官方文档站点查看示例:docs.docker.com/engine/api/latest/
。
可以通过命令行使用 netcat 或 curl
发送 API 请求,也可以使用第三方工具如 Postman,或者你可以使用 Python、Bash 等编写脚本来访问 API 端点。
以curl
为例,你可以将命令中的值替换为你自己的,然后执行它:
$ curl -sk <options> https://<ip>:<port>/<rest endpoint> --cert <path/to/cert.pem> --key <path/to/key.pem -cacert <path/to/ca.pem>
你应该看到一个返回的 JSON 对象,包含结果。这些结果比使用 Docker 命令更为全面,如果你想将其保存为 JSON 文件以供进一步分析(例如,如果你认为某个容器可能被攻击时),它们可能会更有用。
除了本地 Docker 工具,谷歌还提供了 容器顾问 (cAdvisor) 用于收集容器的指标。我们现在简要介绍一下它,作为本地监控的第三种选择。
cAdvisor 用于容器监控
cAdvisor 是一个由谷歌管理的软件项目,用于提供容器性能和资源使用情况的洞察。cAdvisor 的源代码可以在 GitHub 上找到,网址如下:
要测试,你可以使用谷歌提供的标准演示容器。只需运行以下命令,从 Google Container Registry 拉取并启动它:
$ sudo docker run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
gcr.io/google-containers/cadvisor:latest
你现在可以在 localhost
的 8080
端口访问 cAdvisor 的 Web 门户。如果该端口上有其他服务运行,例如 Jenkins,你可以在之前的命令中更改 cAdvisor 的端口。
尝试访问 http://localhost:8080/containers/
,你应该能看到以下截图中的仪表板:
图 15.1 – cAdvisor 仪表板
从此仪表盘,你可以探索从文件系统、内存到 CPU 和进程等各种指标。监控这些指标以识别性能问题,可以作为我们在其他地方提到的监控安全问题的有用工具。
例如,如果资源使用量异常高,这可能表明软件无法正常运行,或者是潜在的安全问题,比如恶意软件在容器中运行。
所有这些对于小型本地系统以及可能对潜在被攻破的容器进行快速调查非常有用,但如果我们认为在生产环境中可能存在安全问题,如何监控我们的容器并收集可操作的数据呢?我们可以考虑使用一些市场上流行的第三方工具,来收集指标并构建全面的仪表盘和告警系统。
为了演示这一点,我们将看一下市场上最受欢迎的用于收集 Kubernetes 和 Docker 环境监控数据的工具之一,Datadog。
在云中使用 Datadog 聚合监控数据
对于那些将环境部署到云环境或自建数据中心的商业项目,我们需要一个平台,能够从各种输入源聚合数据,并以你可以操作的方式呈现数据。
Datadog 是一个能够实现这一目标的产品,提供了适用于简单的 Docker 和高级 Kubernetes 环境的插件。它还支持多个平台,包括主要的云服务商,如 AWS。Datadog(www.datadoghq.com/
)提供 14 天免费的试用期,你可以尝试其容器功能,看看是否符合你的需求。你会发现它是一些在前几章中探索过的工具的有力竞争者。
现在,让我们来看看你可以在节点上运行的 Kubernetes 和 Docker 代理,以开始将数据发送回 Datadog。
Docker 和 Kubernetes 的 Datadog 代理
一旦你在www.datadoghq.com/
设置了账户,你就可以在测试节点上安装 Datadog 代理来监控性能。
提示
我们建议在尝试部署到生产环境之前,先从测试环境开始。我们还建议在部署到生产环境之前,先熟悉 Docker 和 Kubernetes 代理文档,网址是:docs.datadoghq.com/agent/docker/?tab=standard
。
以下示例将涵盖安装 Docker 代理和 Kubernetes 代理。每个示例都使用仅包含单个节点的集群进行演示。你可以重复使用第十二章中的 Docker 容器,容器安全简介,或者本书其他地方使用的容器之一。
安装并监控 Docker 代理
你的第一个任务是安装主机上的 Docker 代理。Datadog Docker 代理负责收集度量数据并将其传回你的账户仪表板。
现在安装代理变得非常简单。从你的主机内执行以下 Docker 命令,以包含 Datadog 代理:
$ docker run -d --name dd-agent \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc/:/host/proc/:ro \
-v /path/to/cgroup/:/host/sys/fs/cgroup:ro \
-e DD_API_KEY={API_KEY} \
datadog/docker-dd-agent:latest
根据你的操作系统版本和已安装的代理版本,你可以通过检查此处的命令列表来确认它是否正在运行:
https://docs.datadoghq.com/agent/guide/agent-commands/?tab=agentv6v7#agent-status-and-information
从 Datadog 仪表板中,你现在应该能看到返回的数据。你可以开始探索从容器返回的度量数据,并在出现问题时设置警报:
图 15.2 – Datadog 仪表板显示度量的示例
你可能感兴趣的下一个领域是菜单中的安全选项。选择该项并按照向导设置安全监控。完成后,你可以启用和禁用安全检测规则,如下图所示:
图 15.3 – Datadog 中的检测规则
有关在 Datadog 中为容器设置监视器和警报的更多信息,请参阅此文档:
现在我们来看一下 Kubernetes 代理的对应内容。
安装并监控 Kubernetes 代理
和我们之前的 Docker 示例一样,我们需要先安装代理。为此,我们可以通过 Helm 部署一个 DaemonSet。以下指令使用的是 Helm 3 版本。
提示
如果你还没有运行,记得添加 helm repo add stable,
kubernetes-charts.storage.googleapis.com
,以将 stable 添加到你的仓库。
你可以从 GitHub 下载包含配置的官方 Helm 文件(values.yaml
),地址为(github.com/helm/charts/blob/master/stable/datadog/values.yaml
)。
接下来,你需要从你的账户中获取 API 密钥。使用 API 密钥,我们现在可以完成安装过程。在以下命令中,将{API_KEY}
替换为你自己的密钥:
helm install datadog-agent -f values.yaml --set datadog.apiKey={API KEY} stable/datadog
你应该在终端中看到一个确认信息,表明部署已成功:
图 15.4 – Datadog 代理部署
现在你已经部署了代理,它将开始从 Kubernetes 收集度量数据:
图 15.5 – 仪表板度量示例
在这个安装过程中,kube-state-metrics
Helm 图表也被包含在内。这个 Helm 图表安装了kube-state-metrics
服务(github.com/kubernetes/kube-state-metrics
)。
此服务收集了多种数据,你可以通过 github.com/kubernetes/kube-state-metrics/tree/master/docs
查看暴露的指标。
例如,你可能对与机密相关的指标感兴趣,这样你就可以通过查看 Kubernetes 日志收集文档来了解收集了哪些数据。你还可以通过 Helm 启用日志收集。为此,更新 datadog-values.yaml
文件,将 enabled
和 containerCollectAll
键值对都设置为 true
。完成后,运行 helm upgrade
更新 Datadog Helm 图表。
通过将节点的指标发送回 Datadog 默认的 Kubernetes 仪表盘,你可以开始配置警报和监控,并探索 Datadog 提供的众多功能。
例如,你可以创建一个自定义仪表盘,显示发现的安全信号数量:
图 15.6 – 仪表盘列表
我们已经简要了解了如何使用第三方工具在安全上下文中监控我们的容器。这可以帮助我们对可能表现为性能问题的安全问题发出警报。
现在让我们来看看一些主要云平台提供的工具。我们之前讨论的 Datadog 和 CI/CD 扫描管道可以与以下各节中列出的提供商集成,以提供更加全面的安全防护。
使用 AWS 来保护你的容器
我们可以采取多种方法来保护云中的容器。我们将从 Amazon Web Services(通常称为 AWS)开始。本书的这一部分假设你已经熟悉在 AWS 中托管基于容器的项目。如果你使用的是其他服务,如 Azure 或 GCP,可以跳过并直接阅读 Azure 容器安全 和 Google 容器安全选项 部分。AWS 和容器托管的主题也在 第五章,生产环境中部署和运行容器的替代方案,以及 第八章,将 Docker 应用部署到 Kubernetes 中进行了讨论。让我们看看在 AWS 中用于监控的工具。
使用 GuardDuty 的 AWS 安全警报
AWS 或作为第三方插件存在的多种工具可以用于监控托管容器基础设施的 Amazon 环境。
亚马逊用于监控 VPC 中安全问题的主要工具是 GuardDuty (aws.amazon.com/guardduty/
)。
我们已经看到如何使用 Datadog 监控容器健康状况,但也看到监控支持我们基础设施的环境是多么重要。复杂的生产实例通常会使用 AWS 服务,这些服务位于弹性 Kubernetes 服务(EKS)、弹性容器服务(ECS)和弹性计算云(EC2)之外。例如,您可能在本书早些时候设置 CloudWatch 指标或 S3 存储桶时使用的 IAM 角色。
AWS GuardDuty 提供了一种机制来监控我们的云环境,确保可以追踪到托管容器的 VPC 中发生的任何攻击。这是通过与 CloudWatch 集成实现的,CloudWatch 使我们能够基于我们看到的警报类型触发某些安全操作,例如触发 Lambda 函数,或将事件发送到第三方应用程序或 S3 存储桶进行存储。
如果您希望启用 GuardDuty,您需要一个托管容器的 VPC 设置,例如在第八章中配置的那种,将 Docker 应用部署到 Kubernetes。
在此基础上,您现在可以创建一个规则,允许 CloudWatch 发送 GuardDuty 发现的任何事件。这对于检测容器是否在您的 VPC 中生成可疑的网络流量尤其有用。
使用 AWS CLI,我们现在可以启用 CloudWatch,开始发送之前提到的事件。为此,请执行以下命令:
$ aws events put-rule --name PacktContainerSecurity --event-pattern "{\"source\":[\"aws.guardduty\"]}"
启用这些事件后,您将有多种选择来进行后续操作。例如,您可以附加一个 Lambda 函数,该函数将处理被触发的事件并对其进行操作,或者按照这里的说明,将 CloudWatch GuardDuty 事件与您的 Datadog 设置集成:
github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring
如果您希望将 CloudWatch GuardDuty 事件的结果写入第十章中创建的 S3 存储桶,使用 Prometheus、Grafana 和 Jaeger 监控 Docker部分中的长期存储日志到 AWS S3,那么您可以将 Lambda 函数附加为事件规则:
$ aws events put-targets --rule PacktContainerSecurity --targets Id=1,Arn=arn:aws:lambda:<zone>:<ARN digits>:function:<function>
AWS 提供了一个可以用来将数据写入 S3 存储桶的 Lambda 函数示例,链接如下:
一旦您根据需要修改了这个 Lambda 并将其添加到 <
和 >
括号之间,您可以通过运行以下命令来添加所需的权限:
$ aws lambda add-permission --function-name <function> --statement-id 1 --action 'lambda:InvokeFunction' --principal events.amazonaws.com
这应该作为一个出发点,帮助您更详细地探索 GuardDuty,并扩展您在本书中所创建的设置。
将发现结果存储到 S3 的另一种方式
你也可以使用 AWS 提供的以下步骤,将 GuardDuty 发现结果导出到 S3 存储桶:docs.aws.amazon.com/guardduty/latest/ug/guardduty_exportfindings.html
在 AWS 中,你可能感兴趣的其他安全功能包括:
-
用于分析应用程序安全的 Amazon Inspector:
aws.amazon.com/inspector/
-
用于创建统一安全中心的 AWS Security Hub:
aws.amazon.com/security-hub/
-
用于检测潜在安全问题的 Amazon Detective:
aws.amazon.com/detective/
这些服务都可以通过你的 AWS 网站控制台启用。现在,让我们继续,看看 Microsoft Azure 中可用的一些选项。
使用 Azure 保护你的容器
Azure 是微软的旗舰云服务,提供一系列工具,可以帮助你部署和监控 Docker 容器。本节假设你对 Azure 和 Log Analytics 服务有一定的了解。
Azure 中的容器监控
微软的容器监控解决方案提供了一种机制,可以从一个地方管理 Docker 和 Windows 主机,并支持 Kubernetes 和 Docker Swarm,这两者在本书中都有讨论。
如果你已经在使用微软的 AKS 服务,你可能已经熟悉 AKS 页面上可用的监控服务,但你也可以在 Azure 中监控整个微软基础设施中的容器。
要启用容器监控,你需要先通过将其添加到 Log Analytics 来启用此功能。你可以通过点击 Azure Marketplace 网站上的立即获取按钮来完成:
azuremarketplace.microsoft.com/en-us/marketplace/apps/microsoft.containersoms?tab=overview
完成后,你可以创建一个新的 Log Analytics 工作区。从这个新工作区中,记录你选择的名称,并获取工作区 ID 和密钥。这些信息可以在工作区的高级设置下找到,并位于连接的源 | Linux 服务器选项中。
为了本概述的目的,我们假设一个单主机的环境,就像我们为 Linux 上的 Datadog 所做的那样。在这种情况下,你需要按照以下步骤安装 Log Analytics 代理:
$ wget https://raw.githubusercontent.com/Microsoft/OMS-Agent-for-Linux/master/installer/scripts/onboard_agent.sh && sh onboard_agent.sh -w <workspace_id> -s <workspace_key>
现在,你可以使用以下命令重新启动代理:
$ sudo /opt/microsoft/omsagent/bin/service_control restart [<workspace_id>]
现在,让我们尝试对容器运行监控,如下所示:
$ sudo docker run --privileged -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/containers:/var/lib/docker/containers -e WSID="<workspace_id>" -e KEY="<workspace_key>" -h=`hostname` -p 127.0.0.1:25225:25225 --name="omsagent" --restart=always microsoft/oms
我们可以在 Log Analytics 工作区的数据选项下修改我们收集的事件数据。在这里,我们可以添加 syslog,并启用 Linux 性能计数器。
一旦启用该解决方案,你将看到容器磁贴出现。然后,你可以深入容器仪表盘收集指标。
现在我们已经有了一些监控措施,让我们来看看 Azure 中可用于基于容器平台的一些安全功能。
使用安全中心保护 Azure 中的容器
通过监控到位后,您可以继续查看微软的容器安全工具。在 Azure 中实现这一点的推荐本地工具是安全中心服务。
您可以通过点击azure.microsoft.com/en-us/services/security-center/
上的开启安全中心按钮来注册并将其添加到您的 Azure 帐户中,如果愿意,您还可以同时注册一个 Azure 帐户。
一旦启用此功能,您将看到安全中心提供多个功能,包括以下内容:
-
容器运行时保护
-
漏洞管理
-
环境加固
我们将简要看一下这些内容。
容器运行时保护
安全中心为容器环境提供的运行时保护功能,允许您生成实时的威胁指标,用于规划修复工作。威胁检测机制分为两个核心领域:
-
在主机级别:在此级别,我们可以监控容器是否存在恶意或可疑行为,包括暴露的 Docker 守护进程或容器内运行的特权命令。
-
在 AKS 集群级别:AKS 集群级别的威胁检测会分析 Kubernetes 审计日志中的可疑活动,例如创建高度特权角色或检测到矿工程序。
这两个功能结合使用有助于查看容器堆栈的各个层次并检测可疑活动。
漏洞管理
在这里,您可以使用容器注册表包在推送新镜像时进行扫描。安全中心与第三方安全提供商 Qualys 的集成扫描容器,检测我们在本书中讨论的一些漏洞。
当检测到问题时,它将被记录在仪表板上,并附有推荐的修复步骤。
环境加固
安全中心提供了多种工具来监控容器环境的安全性。最重要的功能之一是运行基准测试,如 CIS Docker 基准测试,如果您的环境配置被削弱,它会发出警告。CIS 控制的一个示例是检查容器之间是否存在不受限制的网络流量交换。
您可以从 CIS 网站免费下载 CIS Docker 基准测试的副本:
learn.cisecurity.org/benchmarks
注意
InSpec 用户可能对下载用于 CIS Docker 基准测试的 InSpec 配置文件感兴趣,链接在此:github.com/dev-sec/cis-docker-benchmark
。
当安全中心发现您的环境存在问题时,它会在仪表板的建议页面上标记出来,以便您可以开始修复该问题。
我们简要地了解了 Azure 中可用的功能。现在,让我们通过快速浏览 GCP 的一些功能来结束本部分。
使用 GCP 保护您的容器
Google 为在 Anthos 和Google Kubernetes Engine(GKE)中监控容器提供了多个工具。
对于不熟悉 Google 产品的人来说,Anthos 是一个设计用于混合云和多云部署的平台,它允许您在其中部署容器导向的平台,如 Kubernetes 等。GKE 是 Google 提供的企业级 Kubernetes 平台,提供于Google Cloud Platform(GCP),可以视为与 Amazon 的 EKS 竞争的对手。Google 的容器注册表是一个用于存储镜像的平台,这些镜像可以在您的项目中重用。
在以下部分中,假设您已具备一定的 GCP 基础知识。如果您想了解更多关于如何开始使用 GCP 的信息,请访问以下链接:
cloud.google.com/gcp/getting-started
我们从 GCP 中的容器安全开始。
GCP 中的容器分析和二进制授权
Google 提供的一个有用功能是容器分析扫描器,用于容器注册表。此功能允许您扫描镜像中的安全问题,并暴露一个 API 供您使用,您可以通过它提取元数据结果。如果您在账户中启用此功能,它将扫描所有推送到注册表的新镜像,但对于现有镜像,您需要重新推送它们以触发扫描。
容器分析的两个核心功能如下:
-
增量扫描:此功能负责扫描新镜像并生成相关的元数据。
-
持续监控:增量扫描生成的元数据会不断进行分析,以查看是否与新的安全漏洞集匹配。
在运行扫描时,会为匹配的安全问题分配一个严重性级别(由 Linux 发行版所有者定义的级别)和常见漏洞评分系统(CVSS)得分。
注意
如果您想了解更多关于 CVSS 的信息,请访问 CVSS 官方网站:www.first.org/cvss/specification-document
。
严重性级别的分类如下:
-
严重
-
高
-
中
-
低
-
最小
这些结果存储在您的容器注册表账户中,并可以从那里查看。此外,它们可以通过 RESTful API 进行检索。有关可用 REST 命令的概述,请参阅容器分析 API 文档:
cloud.google.com/container-registry/docs/reference/rest
要进一步探索容器分析,您可以在账户中启用该功能,并通过将现有镜像推送到注册表来进行测试。例如,您可以使用本书中介绍的shipitclicker
项目之一。执行此操作时,请记得首先为镜像打标签:
$ docker tag <source_image> <hostname>/<project_id>/<image>:<tag>
主机名将是以下四个存储区域之一:
-
gcr.io(美国)
-
us.gcr.io(美国)
-
eu.gcr.io(欧盟)
-
asia.gcr.io(亚洲)
然后,使用docker push
命令,以以下格式推送到注册表:
$ docker push <hostname>/<project_id>/<image>:<tag>
就这么简单,您可以随时拉取容器镜像,并使用容器分析服务。除了对容器进行分析外,我们还可以执行规则,要求使用签名镜像来补充这一点。
Google 构建了一个部署时的安全功能,旨在防止不受信任的容器镜像进入 GKE。这被称为二进制授权(cloud.google.com/binary-authorization
)。
Kritis是二进制授权的核心,它定义了 Kubernetes 应用程序部署授权的规范。您可以在 GitHub 上查看更多关于它的信息:
github.com/grafeas/kritis/blob/master/docs/binary-authorization.md
使用此服务将使您能够执行规则,要求 Docker 镜像必须由可信的授权方签名。这涉及到一个名为“证明”的过程。实际上,每个容器镜像都有一个唯一的哈希值(称为摘要),该哈希值由签署者签名。您可能还记得我们在本书的第十三章中看到过摘要如何使用,Docker 安全基础与最佳实践。
当摘要被签名时,这就是所谓的“证明”。当我们部署容器镜像时,我们可以使用二进制授权验证者来验证证明。这使我们能够防止未经授权的(即未签名的)容器镜像被使用。
如果您有兴趣了解更多,您可以按照这里记录的简单步骤来设置二进制分析:
cloud.google.com/binary-authorization/docs/quickstart
现在,让我们来看看 GCP 的另一个功能——安全指挥中心。
通过安全指挥中心了解您的攻击面
我们将快速了解的最后一个工具是 Google 的安全指挥中心。为此,您需要在 GCP 中设置组织和项目。如果没有,请参考前面的部分,获取 Google 的快速入门指南链接。
要为此新组织和项目启用安全指挥中心,请按照以下步骤操作:
-
登录到 Cloud Console:
console.cloud.google.com
。 -
通过
organizationAdmin
(roles/resourcemanager.organizationAdmin
)和安全中心 | 安全中心管理员的securitycenter.admin
(roles/securitycenter.admin
)角色,添加以下两个角色。 -
保存更改并导航到 Web 控制台中的安全指挥中心页面。
-
从名为组织的下拉列表中选择您在第 2 步中添加的组织。
-
现在将为您展示启用资产发现页面。
-
启用所有当前和未来的项目选项。
-
资产发现现在开始。
一旦安全指挥中心完成扫描您的资源,您就能在仪表板上查看结果。默认情况下,异常检测已启用,然而,Google 提供了许多您可以集成的安全来源,或者您可以插入特定于容器的第三方服务。
您可以在此找到可集成的所有潜在来源列表:
cloud.google.com/security-command-center/docs/how-to-security-sources
配置好这两个基本服务后,您现在可以自由探索集成其他第三方供应商,例如 Twistlock,或尝试这些服务以便熟悉将它们部署到生产环境中的流程。
这就是我们对几个主要云提供商产品的简短介绍。让我们总结一下我们所查看的内容。
总结
在本章中,我们为您提供了一些指引,帮助您决定接下来将云技能应用到哪里。我们讨论了扫描工具如 Anchore,回顾了度量收集平台如 Datadog,并简要了解了主要云提供商提供的一些功能。
这些云平台包括 AWS、Microsoft Azure 和 GCP。每个公司还提供了一些其他基于云的容器基础设施产品,您可能希望进一步探索。
我们希望这个高层次的概述能为您提供一些深思熟虑的见解,帮助您将这些技能应用到自己的项目中。本章中的每个主题应作为一个跳板,帮助您进一步探索每个工具,或者为您提供基础,开始在基于云的容器环境中进行监控实验。对于那些处理本地项目的人,Docker stats 和 cAdvisor 等工具将为您提供便捷的监控容器性能机制。
现在我们将进入最后一章,在这一章中,我们将回顾整个书中的学习内容,并为您总结一些关键要点,帮助您决定接下来要如何进行学习。
进一步阅读
别忘了,您可以访问每个提供商的网站,查看这些额外功能的列表:
-
AWS 上的容器:
aws.amazon.com/containers/services/
-
Azure 中的容器服务:
azure.microsoft.com/en-us/product-categories/containers/
-
GCP 中的容器选项:
cloud.google.com/container-options
第十七章:结论 – 终点站,但不是旅程的结束
你现在已经到达了本书的最后一章。在前面的 15 章中,我们讨论了各种话题。正如你可能已经注意到的,书中的内容被分为三个领域——开发、带有监控的 DevOps,最后是安全性。那么,让我们花些时间回顾一下我们在每个领域所学到的内容,以及接下来我们可以去哪里。
首先,我们将回顾一下本书中学到的内容。接着,将呈现我们在开发方面所获得的技能总结。然后,我们将探索下一步可以去哪里,学习更多关于容器化 DevOps 的知识,并扩展我们新学到的技能。我们的倒数第二次回顾将考虑我们在安全方面学到的内容,以及我们如何保持对安全的关注。最后,我们将以对我们所学内容的总体总结结束。
为了复习这些内容,我们将它们分解为本章中的以下主题:
-
总结 – 让我们开始吧
-
我们学到的开发内容
-
推进 DevOps 知识的下一步
-
关于安全性的总结以及接下来的步骤
准备好你的容器化环境,准备好和我们一起进入 Docker 世界的最后一探。
技术要求
对于本章,你需要有一台运行 Docker 的 Linux 机器。我们建议你使用本书中到目前为止一直使用的设置。这样,你就可以按照本章推荐的一些工具和技术继续进行,如果你愿意的话。
查看以下视频,看看代码在实际中的表现:
总结 – 让我们开始吧
在本书的过程中,我们探讨了容器化的世界。随着这项技术在全球公司和项目中变得越来越普及,掌握容器基础知识以及支持容器的工具集变得愈发有用。
在我们结束本书之前,我们将通过回顾我们在开发方面所学到的内容来做一个总结。之后,我们将讨论可以采取哪些步骤来提升你的 DevOps 技能,最后,我们将快速浏览一些可能感兴趣的安全项目。
你可能希望准备好第九章中的项目,使用 Spinnaker 进行云原生持续部署,以便将本章推荐的一些项目与之结合。
记住,你可以在这里回顾设置该项目的源代码:
github.com/PacktPublishing/Docker-for-Developers/tree/master/chapter9
话虽如此,让我们来回顾一下在基于 Docker 的环境中开发时所学到的内容。
我们学到的开发内容
在本书的第一部分,《Docker 入门——容器与本地开发》中,我们介绍了 Docker 和容器的基础知识,以及它们如何用于开发。
首先,我们介绍了容器化和相关技术,如虚拟化。接下来,我们评估了 Docker 容器和虚拟机之间的差异,以了解它们在开发中的对比。在第三章《通过 Docker Hub 分享容器》中,我们首次体验了如何使用 Docker Hub 从第三方位置存储和检索镜像。最后,在了解了预构建的容器和容器镜像之后,我们探索了多个容器必须协同工作以构建更复杂系统的场景。
本节中的这四章内容,共同提供了本地开发的基础知识,并帮助你理解在该领域成为成功工程师所需的工具。为了进一步拓展这部分知识,理解基于容器的系统的设计模式将是你接下来应当探索的合乎逻辑的步骤。
深入探讨——设计模式
本书的第一部分提供了实践开发的指南。使用容器并不意味着软件开发的架构模式必须被放弃!
所以,如果你是这个领域的新手,你可能会问什么是设计模式。简而言之,设计模式是解决常见架构问题的可重用蓝图。就像建筑行业的工程师和建筑师重复使用可行的建筑模型一样,我们也可以采用类似的方法来构建软件系统。
以下这些面向容器的模式为你在完成本书后进一步探索这一主题提供了一个良好的起点。事实上,你可能会从前面的章节中认识到其中一些模式,这也是我们在此包含它们的原因。现在,让我们简要浏览其中五个模式,并看看本书中哪些服务和项目实现了它们。
单一容器——保持简单
当我们开始本书中的项目时,我们保持简单,使用了单一容器模式。这是你在基于容器的环境中可以采用的最简单模式,ShipIt Clicker 应用就采用了这一模式。
边车设计模式——用于日志记录
我们在整本书中都提到过日志记录,而日志监控系统通常会实现一种被称为边车模式的方式。在最简单的形式中,我们有一个像 ShipIt Clicker 这样的容器,接着是一个带有日志监控工具的第二个容器。这个工具可能是 Grafana、Datadog,或者我们实验过的其他工具。当你开始构建自己的项目时,这种简单的模式是一个很好的起点。将你的应用部署到一个容器中,然后用第二个容器处理日志处理。你还记得我们在探讨 Envoy 时提到的,边车模式在这里被用来创建服务网格,而不需要直接编辑应用来处理复杂的网络问题。
领导者与选举 – 增加冗余
我们已经看到了高可用系统的需求,以及像 Kubernetes 这样的工具如何通过在各个 Pod 之间协调多个容器来帮助我们实现这一目标。与 Kubernetes 配合使用的一种常见设计模式是领导者和选举方法。在这种方法中,数据可以分布到多个节点上以提供冗余;例如,数据可能会在容器之间进行复制。
如果由于某种原因,我们的容器崩溃,其他容器将选举一个新的领导者,Kubernetes 会启动一个新的节点来填补空缺。
使者设计模式 – 一种代理方法
代理是许多系统中的一个重要组成部分,尤其是在微服务架构中。正如你所看到的,在基于 Docker 的环境中,我们可以让多个容器驻留在同一虚拟网络上。每个容器都会分配一个名称,这使得容器之间可以相互通信。
一个可以使用使者模式的例子是,在后端缓存服务(如 Redis)与一组应用程序之间进行通信。在这个例子中,应用程序与一个 Redis 代理节点进行通信,认为它就是 Redis 本身。然而,这个代理节点随后会将流量分发到网络上的其他多个 Redis 节点。
Redis
Redis (redis.io),正如你从早期的章节中可能记得的,它是一个基于内存的开源缓存和消息代理系统。它允许你在内存中存储多种数据结构,例如列表、集合和哈希,并且如果你愿意,还可以将其用作主要数据库(redislabs.com/blog/goodbye-cache-redis-as-a-primary-database/
)。
我们在第十一章《Docker 应用的扩展和负载测试》中分析的工具 Envoy,对于部署使者风格的方法非常有用。如果你有兴趣与 Redis 一起尝试,查看 Dmitry Polyakovsky 的文章《Envoy Proxy 与 Redis》(dmitrypol.github.io/redis/2019/03/18/envoy-proxy.html
)。
Redis 可以从 Docker Hub 获取作为容器 (hub.docker.com/_/redis/
)。在继续之前,让我们看一下最后一个设计模式。
适配器设计模式——解决方案重用
在容器之间有一致的方式来传递信息是非常重要的,尤其是在聚合指标时。例如,如果不同的容器生成不同格式的日志,我们需要能够以统一的格式获取这些数据。这时,适配器模式就派上用场了。我们可以使用这种模式开发统一接口,随后接收来自多个容器的日志文件,对其进行标准化处理,然后将数据存储在集中式监控服务中。
我们在第十章《使用 Prometheus、Grafana 和 Jaeger 监控 Docker》中看到,Prometheus 是一个有用的容器监控工具。然而,Prometheus 需要一个统一的接口来拉取指标,这个接口就是指标 API。当一个应用程序没有暴露与 Prometheus 兼容的端点时,我们可以使用适配器模式部署一个接口,将目标服务容器包装在一组 Prometheus 兼容的端点中。这样,Prometheus 就可以通过中间接口容器无缝地从我们感兴趣的容器中拉取数据。
阅读更多关于设计模式的内容
使用基于容器的设计模式有助于确保为你的系统使用正确的模型,仅在需要时引入适当的复杂性,同时确保系统具有弹性并且更容易管理。
如果你想了解更多关于 Kubernetes 和 Docker 中容器模式的内容,记得查看 Packt 出版的《Kubernetes 设计模式与扩展》一书。
接下来是进一步提升你的 DevOps 知识的步骤
第二部分,在生产环境中运行容器,针对的是 DevOps 实践,如持续集成和持续部署(CI/CD)、使用 Kubernetes 进行容器编排以及使用 Jaeger 等工具进行监控。
一开始,我们考察了在云环境和混合环境中托管容器的选项。接下来,我们探索了一个简单的选项,即通过 Docker Compose 在单一主机上部署应用程序。之后,通过实验 Jenkins,我们第一次接触了 CI/CD 工具,以及这些工具如何与 Docker 配合使用。在掌握了 CD 的概念后,我们进入了 第八章,将 Docker 应用部署到 Kubernetes,这让我们第一次体验了 Kubernetes 容器编排。随后,我们尝试了 Spinnaker 这种容器原生云部署选项,并理解了哪些部署方法对生产环境有用。本书第二部分的倒数第二章探讨了性能监控工具,如 Jaeger、Prometheus 和 Grafana。最后,我们以讨论 Envoy 服务网格、代理和在生产环境中的扩展与负载测试项目结束了这一部分。
本部分的七章提供了丰富的项目,让你了解一些公司在生产环境中托管和服务基于容器的应用程序时所面临的核心概念。然而,仍然有许多有趣的技术和话题需要学习,以便将你的 DevOps 技能提升到一个新的水平。
混沌工程与构建弹性生产系统
在拥有复杂生产系统、容器在云中编排、并且 CD 运行的情况下,我们如何确保系统能够抵御故障和意外崩溃?这就是混沌工程概念发挥作用的地方。
混沌工程是理解代码和基础设施本身复杂性的一种实践,因此我们在进行工程和测试时应考虑到这一点。混沌工程有五个概念,概括如下:
-
围绕稳定状态行为发展假设:在短时间内衡量系统的输出,以收集基线数据。这个基线被称为稳定状态,可能包括错误率、响应时间、延迟时间和流量负载等指标。
-
测试各种现实世界事件:在测试可能影响生产系统的现实世界事件时,可以考虑测试软件故障、损坏的输入、容器崩溃以及其他可能导致性能下降的事件。
-
在生产环境中实验:在生产环境中进行测试可能看起来像是禁忌。然而,每个环境都是不同的,为了获得真实结果,生产环境中的测试是必须的。
-
最小化影响,亦即爆炸半径:在生产环境中进行测试,然而,这并不意味着我们可以忽略确保任何性能下降是暂时性的,并且可以轻松恢复的责任。始终确保你的实验是有良好隔离的。
-
持续运行自动化实验:使用自动化的方法可以减少人工开销,并让测试和实验在一天中的任何时间进行。
Netflix 开发的一个实现这一概念的工具是 Chaos Monkey。Chaos Monkey 是一个平台,你可以将你的基础设施部署到其中,它会随机终止在生产环境中运行的容器。其目标是测试生产系统如何响应/恢复,并允许工程师调优系统以增强其韧性。
你已经看过如何设置 Spinnaker,接下来,你可以将 Chaos Monkey 集成到现有的管道中。Chaos Monkey 也可以与 AWS 和 Kubernetes 一起使用。源代码可以在github.com/Netflix/chaosmonkey
找到。
如果你有兴趣安装 Chaos Monkey 并将其添加到你在第九章中构建的现有 CI/CD Spinnaker 管道中,云原生连续部署使用 Spinnaker,你可以参考官方安装指南netflix.github.io/chaosmonkey/How-to-deploy/
。
一旦它运行起来,你可以在基于 Spinnaker 的容器环境中测试 Chaos Monkey,看看它如何处理终止服务,并观察你的监控工具中显示的相关指标。
如果你有兴趣将 Chaos Monkey 与安全技术结合使用,务必查看 Packt 的视频指南,了解如何使用 Chaos Monkey 进行应用程序的模糊 测试:
什么是模糊测试?
模糊测试是测试应用程序对随机、无效和不兼容的随机数据输入的反应过程。
除了 Chaos Monkey,以下工具还提供了构建和测试弹性系统的机制:
-
Gremlin:一个可以与 Kubernetes、Mesos、ECS 和 Docker Swarm 一起使用的混沌工程平台,详情请见
www.gremlin.com/
。 -
Mangle:VMware 的开源混沌工程平台,支持 Kubernetes 和 Docker,详情请见
vmware.github.io/mangle/
。 -
Chaos Mesh:一个面向 Kubernetes 环境的云原生混沌工程平台。它可以通过 Helm 进行部署,详情请见
github.com/pingcap/chaos-mesh
。
我们简要介绍了混沌工程作为一种概念,你可以从 DevOps 的角度进一步探索。现在,让我们回顾一下我们在安全性方面学习的内容。
关于安全性的总结及下一步该怎么做
本书的最后一部分,Docker 安全 – 保护你的容器,专门讨论了安全主题。首先,我们从安全角度分析了容器如何与底层硬件协同工作。我们研究了容器和虚拟机监控程序的安全模型,并快速了解了安全最佳实践。
随后我们探讨了安全基础和最佳实践,为我们提供了处理 Dockerfile 和构建最小基础镜像的最佳方法。在此之后,我们研究了如何在 Docker Swarm 中处理秘密信息。这为那些可能需要维护遗留系统或从 Swarm 迁移到 Kubernetes 的读者提供了见解。我们还探讨了从安全角度如何使用标签、元数据和标签。
本书倒数第二章,第十五章,扫描、监控和使用第三方工具,为我们提供了 Google、Amazon 和 Microsoft 在云中提供的容器安全功能的简要介绍。我们还安装了 Anchore 进行安全扫描,查看了一些可能有用的额外监控工具,并简要尝试了 Datadog 进行容器监控,而 Datadog 也可以在安全环境下使用。
掌握了这些基础知识后,以下是一些基于这些知识的容器安全项目的下一步思路。
Metasploit – 基于容器的渗透测试
现在我们已经构建了安全容器,并且希望应用程序也足够安全,你可以在基于容器的环境中进行渗透测试,例如通过 Spinnaker 部署的环境。渗透测试是寻找系统中的安全漏洞的过程,这些漏洞可能被利用来获取访问权限、外泄数据、干扰性能,或者将受损系统转变为发起其他攻击的平台。
一个广泛使用的渗透测试工具是Metasploit框架(www.metasploit.com/
)。Metasploit 是一个开源框架,用于开发和部署针对远程目标(例如在你环境中运行的容器)的安全漏洞代码。Metasploit 可以从 Docker Hub 以容器格式获取,网址是hub.docker.com/r/metasploitframework/metasploit-framework
。
在此工具到位后,你可以使用如 Anchore 等工具测试容器中的漏洞。例如,漏洞可能包括容器中安装的旧版软件,这些软件可能容易受到攻击。要获取最新版本,请运行以下代码:
docker pull metasploitframework/metasploit-framework
你可以按如下方式运行容器:
sudo docker run --rm -it metasploitframework/metasploit-framework
加载完成后,你将进入 Metasploit shell,称为msfconsole
:
图 16.1 – Metasploit 容器运行示例
从这里,你可以开始探索可用的命令,并考虑可以在容器内运行的项目。关于使用 Metasploit 的免费课程可以在 Offensive Security 网站找到:www.offensive-security.com/metasploit-unleashed/
。一旦你熟悉了基本命令,可以考虑探索 Metasploit 中的一些其他功能。
无保护的 TCP 套接字漏洞
你还记得我们讨论过的将 Docker 的 TCP 套接字暴露出去可能被攻击者利用的情况。Metasploit 提供了一个如何实现这一点的示例。尝试在第二台机器上通过2375/tcp
运行 Docker,并在我们刚刚设置的 Metasploit 容器中加载docker_daemon_tcp
模块(www.rapid7.com/db/modules/exploit/linux/http/docker_daemon_tcp
)。现在你可以通过此模块针对被攻破的套接字,并在正在运行容器的目标主机上创建一个容器,并将/
路径挂载为具有读写权限。
测试第三方易受攻击的容器 - Apache Struts
以下只是许多可供下载和实验的易受攻击容器中的一个示例。这个由piesecurity
创建的容器,包含了 Apache Struts 的一个易受攻击版本(hub.docker.com/r/piesecurity/apache-struts2-cve-2017-5638/
)。
Apache Struts 是一个流行的 Java 框架,用于开发 Web 应用程序。在 2017 年,发现了该框架中的一个漏洞,攻击者可以利用此漏洞在运行该框架的服务器上远程执行代码。此漏洞的著名受害者之一是 Equifax,该公司遭遇了重大数据泄露事件。
你可以通过 Spinnaker 部署并运行加载了 Struts 的容器,自己测试漏洞。安装完成后,使用 Metasploit 模块struts2_content_type_ognl
(www.rapid7.com/db/modules/exploit/multi/http/struts2_content_type_ognl
)。这将允许你发起攻击,在被攻破的容器上创建反向 Shell,并演示即便在 Kubernetes 和 Docker 中运行,第三方框架内的安全漏洞也可能被利用。
如果你想进一步了解,可以参考 Packt 出版的《高级基础设施渗透测试》一书,书中提供了使用 Metasploit 框架并测试基于容器的环境安全的指导。
总结
希望你喜欢阅读本书。本书旨在提供一份全面的 Docker 开发指南,包括本地开发和云端开发。在这 16 章中,我们的目标不仅是展示如何在容器中开发应用,还包括如何构建、部署、扫描和监控它们。
无论你是打算从零开始构建一个新项目、在 Docker Swarm 上维护遗留系统,还是迁移到基于 Kubernetes 的环境,Docker For Developers 这本书都是你可以随时翻阅来刷新知识或根据需要寻求指导的书籍。
我们希望你在容器世界的旅程中获得的收获,与我们分享这些知识时的喜悦一样多。祝你未来的项目好运!