Kubernetes-DevOps-第二版-全-
Kubernetes DevOps 第二版(全)
原文:
annas-archive.org/md5/a27c1d42e0454ca0cca74af8bc3c1f71译者:飞龙
前言
本书解释了实现 DevOps 原则的基本概念和实用技能,重点介绍容器和 Kubernetes。我们的旅程从介绍容器和 Kubernetes 的核心概念开始,接着探索 Kubernetes 提供的各种功能,如为容器持久化状态和数据、不同类型的工作负载、集群网络、集群管理和扩展。为了监督集群活动,我们在 Kubernetes 中实现了监控和日志基础设施。为了更好的可用性和效率,我们还学习了如何自动扩展容器并构建持续交付管道。最后,我们学习了如何操作来自三大主要公共云提供商的托管 Kubernetes 平台。
本书适用人群
本书面向具有一定软件开发经验的 DevOps 专业人员,旨在帮助他们扩展、自动化并缩短软件交付到市场的时间。
本书所涵盖的内容
第一章,DevOps 入门,带你回顾从过去到今天我们所称之为 DevOps 的演变过程,并介绍在这一领域你应该了解的工具。在过去几年中,对具备 DevOps 技能的人才需求急剧增长。DevOps 实践加速了软件开发和交付速度,并帮助了业务的敏捷性。
第二章,容器与 DevOps,帮助你学习容器工作的基本知识。随着向微服务的趋势日益增强,容器成为每个 DevOps 从业者的便捷且必备的工具,因为它们能以统一的方式管理异构服务,提高敏捷性。
第三章,Kubernetes 入门,探讨了 Kubernetes 中的关键组件和 API 对象,以及如何在 Kubernetes 集群中部署和管理容器。
第四章,管理有状态工作负载,描述了不同工作负载的 Pod 控制器,并介绍了用于维护应用状态的卷管理功能。
第五章,集群管理与扩展,引导你了解 Kubernetes 的访问控制功能,并考察了内置的准入控制器,它提供了对集群更细粒度的控制。此外,我们还将学习如何构建自己的自定义资源,以通过自定义功能扩展集群。
第六章,Kubernetes 网络,解释了 Kubernetes 中默认的网络和路由规则如何工作。我们还将学习如何暴露 HTTP 和 HTTPS 路由以供外部访问。在本章的最后,还介绍了网络策略和服务网格功能,以提高弹性。
第七章,监控与日志记录,向你展示如何使用 Prometheus 在应用程序、容器和节点级别监控资源的使用情况。本章还将展示如何通过 Elasticsearch、Fluent-bit/Fluentd 和 Kibana 堆栈收集来自应用程序、服务网格和 Kubernetes 的日志。确保服务正常运行并保持健康是 DevOps 的重要职责之一。
第八章,资源管理与扩展,描述了如何利用 Kubernetes 的核心——调度器,动态扩展应用程序,从而高效地利用集群的资源。
第九章,持续交付,解释了如何使用 GitHub/DockerHub/TravisCI 构建持续交付流水线。它还解释了如何管理更新、消除滚动更新时的潜在影响,并防止可能的失败。持续交付是一种加速市场时间的方法。
第十章,AWS 上的 Kubernetes,带你了解 AWS 的各个组件,并解释如何通过 AWS 托管的 Kubernetes 服务——EKS 来创建集群。EKS 与现有的 AWS 服务有着深度的集成。在本章中,我们将学习如何利用这些功能。
第十一章,GCP 上的 Kubernetes,帮助你学习 GCP 的概念以及如何在 GCP 的 Kubernetes 服务——Google Kubernetes Engine(GKE)中运行你的应用程序。GKE 对 Kubernetes 提供了最原生的支持。在本章中,我们将学习如何管理 GKE。
第十二章,Azure 上的 Kubernetes,描述了基本的 Azure 组件,如 Azure 虚拟网络、Azure 虚拟机、磁盘存储选项等。我们还将学习如何通过 Azure Kubernetes Service 来创建和运行 Kubernetes 集群。
为了从本书中获得最大的收益
本书将指导你通过 Docker 容器和 Kubernetes 在 macOS 和公共云服务(AWS、GCP 和 Azure)上进行软件开发和交付的方法。你需要安装 minikube、AWS CLI、Cloud SDK 和 Azure CLI 来运行本书中的代码示例。
下载示例代码文件
你可以从你在www.packt.com的账户中下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问www.packt.com/support并注册,以便将文件直接通过邮件发送给你。
你可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择 SUPPORT 标签。
-
点击代码下载和勘误表。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
书籍的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition。如果代码有更新,它将会更新在现有的 GitHub 仓库中。
我们还提供来自我们丰富书籍和视频目录的其他代码包,您可以访问github.com/PacktPublishing/查看。快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789533996_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名。例如:“鉴于此,cgroups 在此处用于限制资源使用。”
代码块的呈现方式如下:
ENV key value
ENV key1=value1 key2=value2 ...
当我们希望引起你对某个代码块中特定部分的注意时,相关的行或项目会使用粗体表示:
metadata:
name: nginx-external
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
任何命令行输入或输出如下所示:
$ docker run -p 80:5000 busybox /bin/sh -c \
"while :; do echo -e 'HTTP/1.1 200 OK\n\ngood day'|nc -lp 5000; done"
$ curl localhost
good day
粗体:表示一个新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会像这样出现在文本中。这里有一个例子:“点击创建后,控制台将带我们进入以下视图以供探索。”
警告或重要提示如下所示。
小贴士和技巧以这种方式呈现。
联系我们
我们非常欢迎读者的反馈。
一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com联系我们。
勘误:尽管我们已尽力确保内容的准确性,但错误仍然会发生。如果您在本书中发现错误,我们将不胜感激,若能向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,并输入相关细节。
盗版:如果你在互联网上发现任何形式的非法复制品,请将其地址或网站名称提供给我们。请通过copyright@packt.com联系我们,并提供该材料的链接。
如果你有兴趣成为作者:如果你在某个主题领域拥有专业知识,并且有兴趣编写或参与书籍创作,请访问 authors.packtpub.com。
评价
请留下评论。在您阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在的读者可以通过您的客观意见来做出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一章:DevOps 简介
过去几年中,软件交付周期变得越来越快,同时应用程序部署也变得越来越复杂。这增加了所有涉及发布周期的角色的工作负担,包括软件开发人员、质量保证(QA)团队和IT 运维人员。为了应对快速变化的软件系统,2009 年提出了一个名为DevOps的新概念,旨在帮助整个软件交付流水线演进,使其更加快速和稳健。
本章涵盖以下主题:
-
软件交付方法论发生了哪些变化?
-
什么是微服务架构?为什么人们选择采用这种架构?
-
什么是 DevOps?它如何使软件系统更具弹性?
软件交付挑战
软件开发生命周期(SDLC),即我们构建应用程序并将其交付到市场的方式,随着时间的推移发生了显著变化。在本节中,我们将重点讨论所做的变化以及原因。
瀑布式和静态交付
回到 1990 年代,软件是以静态方式交付的——通过实体软盘或 CD-ROM。由于将应用程序重新交付到市场并不容易,SDLC 每个周期通常需要几年时间。
当时,主要的软件开发方法之一是瀑布模型。它由多个阶段组成,如下图所示:

一旦某个阶段开始,就很难回到前一个阶段。例如,在开始实施阶段后,我们将无法返回设计阶段来修复技术扩展性问题,因为任何更改都会影响整体进度和成本。所有内容都难以更改,因此新的设计将推迟到下一个发布周期。
瀑布方法必须与每个部门精确协调,包括开发、物流、营销和分销商。瀑布模型和静态交付有时需要几年时间,并且需要巨大的努力。
敏捷和数字交付
几年后,随着互联网的广泛使用,软件交付方法从实体转变为数字化,采用在线下载等方式。因此,许多软件公司(也称为互联网公司)试图弄清楚如何缩短 SDLC 过程,以交付能够击败竞争对手的软件。
许多开发人员开始采用新的方法论,如增量式、迭代式或敏捷模型,希望这些方法能够帮助缩短市场时间。这意味着,如果发现新错误,这些新方法可以通过电子交付向客户提供补丁。从 Windows 98 开始,Microsoft Windows 更新也通过这种方式发布。
在敏捷或数字化模型中,软件开发人员编写的是相对较小的模块,而不是整个应用程序。每个模块会交付给 QA 团队,而开发者则继续开发新的模块。当所需的模块或功能完成时,它们将按以下图示发布:

这种模型使得软件开发生命周期(SDLC)和软件交付变得更快速、易于调整。该周期从几周到几个月不等,足够短以便在必要时进行快速修改。
尽管当时大多数人偏爱这种模型,但应用软件交付意味着软件二进制文件(通常是 EXE 程序)必须安装并运行在客户的个人电脑上。然而,基础设施(如服务器或网络)是非常静态的,必须提前设置。因此,这种模型通常不包括在软件开发生命周期(SDLC)中。
云上的软件交付
几年后,智能手机(如 iPhone)和无线技术(如 Wi-Fi 和 4G 网络)变得流行并广泛应用。应用软件从二进制形式转变为在线服务。网页浏览器成为应用软件的界面,这意味着不再需要安装。基础设施变得非常动态——为了适应快速变化的应用需求,它现在必须能够在容量和性能上进行扩展。
这一切得益于虚拟化技术和软件定义网络(SDN)。如今,云服务,如亚马逊网络服务(AWS)、谷歌云平台(GCP)和微软 Azure,通常被广泛使用。这些服务可以轻松创建和管理按需的基础设施。
基础设施是软件开发交付周期(SDDC)中最重要的组成部分之一。由于应用程序在服务器端安装和运行,而不是在客户端 PC 上,因此软件和服务交付周期通常只需几天到几周。
持续集成
如前所述,软件交付环境在不断变化,而交付周期越来越短。为了实现这种快速交付并提高质量,开发者和 QA 团队最近开始采用自动化技术。其中之一就是持续集成(CI)。它包括各种工具,如版本控制系统(VCS)、构建服务器和测试自动化工具。
VCS(版本控制系统)帮助开发者跟踪软件源代码在中央服务器上的更改。它们保存代码的修订记录,并防止不同开发者覆盖源代码。这使得每个版本的源代码都能保持一致和可管理。集中式构建服务器连接到 VCS,定期或在开发者更新代码到 VCS 时自动获取源代码。然后,它们触发新的构建。如果构建失败,构建服务器会迅速通知开发者。这有助于开发者发现有人将有缺陷的代码提交到 VCS。测试自动化工具也与构建服务器集成。在构建成功后,这些工具会调用单元测试程序,然后将结果通知开发者和 QA 团队。这有助于识别是否有人编写了有 bug 的代码并存储到 VCS 中。
整个 CI 流程如以下图所示:

CI 不仅帮助开发者和 QA 团队提高质量,还缩短了应用程序或模块包周期的过程。在电子交付客户的时代,CI 足以应对。交付给客户意味着将应用程序部署到服务器上。
持续交付
CI 加上部署自动化是服务器端应用程序提供服务给客户的理想过程。然而,仍然存在一些技术挑战需要解决,比如如何将软件部署到服务器上;如何优雅地关闭现有应用程序;如何替换并回滚应用程序;如何升级或替换需要更新的系统库;如果需要,如何修改操作系统中的用户和组设置。
基础设施包括服务器和网络。我们通常有不同的环境来支持不同的软件发布阶段,如开发、QA、预发布和生产。每个环境都有自己的服务器配置和 IP 范围。
持续交付 (CD) 是解决前面提到的挑战的一种常见方法。这是一个结合了 CI、配置管理和编排工具的过程:

配置管理
配置管理工具帮助配置操作系统设置,比如创建用户或组,或者安装系统库。它还充当一个编排工具,确保多个管理的服务器与我们期望的状态保持一致。
它不是一个编程脚本,因为脚本不一定是幂等的。这意味着如果我们执行一个脚本两次,可能会出错,比如如果我们尝试创建同一个用户两次。而配置管理工具则关注状态,所以如果用户已经创建,配置管理工具不会再做任何操作。如果我们不小心或故意删除了用户,配置管理工具会重新创建该用户。
配置管理工具还支持将软件部署或安装到服务器上。我们只需描述需要安装的软件包类型,然后配置管理工具会触发相应的命令,按照要求安装软件包。
此外,如果你告诉配置管理工具停止你的应用程序,下载并替换为新的软件包(如果适用),然后重启应用程序,它将始终保持最新的版本。通过配置管理工具,你还可以轻松执行蓝绿部署。
蓝绿部署是一种技术,准备两套应用栈。只有一个环境(例如,蓝色环境)提供生产服务。当你需要部署新版本的应用时,可以将其部署到另一侧(例如,绿色环境),然后进行最终测试。如果一切正常,你可以更改负载均衡器或路由器设置,将网络流量从蓝色切换到绿色。然后,绿色环境成为生产环境,而蓝色环境变为休眠,等待下一个版本的部署。
基础设施即代码
配置管理工具不仅支持裸机环境或虚拟机(VM),还支持云基础设施。如果你需要在云上创建和配置网络、存储和虚拟机,配置管理工具可以帮助在配置文件中设置云基础设施,如下图所示:

配置管理相较于标准操作程序(SOP)有一些优势。它通过VCS帮助维护配置文件,可以追踪所有版本的历史记录。
它还帮助复制环境。例如,假设我们想在云中创建一个灾难恢复站点。如果你按照传统方法,使用 SOP 手动构建环境,这很难预测和发现人为或操作性错误。另一方面,如果我们使用配置管理工具,我们可以快速且自动地在云中构建环境。
基础设施即代码(Infrastructure as Code)可能包含也可能不包含在持续交付(CD)过程中,因为替换或更新基础设施的成本高于仅仅替换服务器上的应用程序二进制文件。
编排
编排工具是配置管理工具集的一部分。然而,这种工具在配置和分配云资源方面更加智能和动态。编排工具管理多个服务器资源和网络。每当管理员想增加应用程序和网络容量时,编排工具可以确定服务器是否可用,然后自动部署和配置应用程序和网络。尽管编排工具不包括在 SDLC 中,但它有助于 CD 流水线中的容量管理。
总之,SDLC 已经显著发展,使我们能够使用各种过程、工具和方法实现快速交付。现在,软件交付可以随时随地进行,软件架构和设计能够生成大而丰富的应用程序。
微服务趋势
正如前文所述,软件架构和设计基于目标环境和应用程序的体量继续演进。本节将讨论软件设计的历史和演变。
模块化编程
随着应用程序规模的增加,开发者的任务是尝试将其分解为多个模块。每个模块旨在独立和可重用,并由不同的开发团队维护。主应用程序只需初始化、导入和使用这些模块。这使得构建更大的应用程序的过程更加高效。
以下示例显示了在 CentOS 7 上nginx (www.nginx.com) 的依赖项。它表明nginx使用OpenSSL(libcrypt.so.1, libssl.so.10),POSIX thread(libpthread.so.0)库,正则表达式PCRE(libpcre.so.1)库,zlib(libz.so.1)压缩库,GNU C(libc.so.6)库等等:
$ /usr/bin/ldd /usr/sbin/nginx
linux-vdso.so.1 => (0x00007ffd96d79000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fd96d61c000)
libpthread.so.0 => /lib64/libpthread.so.0
(0x00007fd96d400000)
libcrypt.so.1 => /lib64/libcrypt.so.1
(0x00007fd96d1c8000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fd96cf67000)
libssl.so.10 => /lib64/libssl.so.10 (0x00007fd96ccf9000)
libcrypto.so.10 => /lib64/libcrypto.so.10
(0x00007fd96c90e000)
libz.so.1 => /lib64/libz.so.1 (0x00007fd96c6f8000)
libprofiler.so.0 => /lib64/libprofiler.so.0
(0x00007fd96c4e4000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd96c122000)
...
ldd(list dynamic dependencies)命令包含在 CentOS 的glibc-common软件包中。
软件包管理
Java 编程语言以及 Python、Ruby 和 JavaScript 等多种脚本编程语言,都有自己的模块或软件包管理工具。例如,Java 使用 Maven (maven.apache.org),Python 使用pip (pip.pypa.io),Ruby 使用 RubyGems (rubygems.org),JavaScript 使用npm (www.npmjs.com)。
软件包管理工具不仅允许你下载必要的软件包,还能注册你实施的模块或软件包。以下屏幕截图显示了 AWS SDK 的 Maven 仓库:

当你向应用程序添加依赖项时,Maven 会下载必要的软件包。以下屏幕截图显示了当你将aws-java-sdk依赖项添加到应用程序时的结果:

模块化编程有助于加速软件开发速度。然而,现代应用程序已经变得越来越复杂。它们需要不断增加的模块、包和框架,并且新特性和逻辑不断被加入。典型的服务器端应用程序通常使用身份验证方法,如 LDAP,连接到集中式数据库(如 RDBMS),然后将结果返回给用户。开发人员最近发现,必须使用软件设计模式来容纳应用程序中的多个模块。
MVC 设计模式
最流行的应用程序设计模式之一是模型-视图-控制器(MVC)。它定义了三个层次:模型层负责数据查询和持久化,例如将数据加载和存储到数据库;视图层负责用户界面(UI)和输入/输出(I/O);控制器层负责业务逻辑,位于视图和模型之间:

有一些框架可以帮助开发人员简化 MVC 的实现,例如 Struts(struts.apache.org/)、SpringMVC(projects.spring.io/spring-framework/)、Ruby on Rails(rubyonrails.org/)和 Django(www.djangoproject.com/)。MVC 是最成功的软件设计模式之一,是现代 Web 应用程序和服务的基础。
MVC 定义了每一层之间的边界,允许多个开发人员共同开发同一个应用程序。然而,这也会带来一些负面影响。应用程序中的源代码体积不断增大。这是因为数据库代码(模型)、展示代码(视图)和业务逻辑(控制器)都在同一个 VCS(版本控制系统)仓库中。这最终会影响软件开发周期。这种类型的应用程序称为单体应用程序。它包含了大量的代码,构建出一个巨大的 EXE 或 WAR 程序。
单体应用程序
没有一个明确的衡量标准来定义一个应用程序是单体应用程序,但典型的单体应用程序往往有超过 50 个模块或包,超过 50 个数据库表,并且构建需要超过 30 分钟。如果我们需要添加或修改其中的某个模块,所做的更改可能会影响大量代码。因此,开发人员尽量减少应用程序内部的代码更改。这种不情愿可能导致开发人员犹豫不决地维护应用程序代码,然而,如果问题未及时处理,就会出现问题。为了这个原因,开发人员现在倾向于将单体应用程序分割成更小的部分,并通过网络将它们连接起来。
远程过程调用
事实上,将一个应用程序划分为小部分并通过网络连接最早是在 1990 年代进行尝试的,当时 Sun Microsystems 推出了Sun 远程过程调用(SunRPC)。这允许远程使用某个模块。最流行的实现之一是网络文件系统(NFS)。NFS 客户端和 NFS 服务器可以通过网络进行通信,即使服务器和客户端使用不同的 CPU 和操作系统。
一些编程语言也支持 RPC 风格的功能。UNIX 和 C 语言有rpcgen工具,它生成包含一些复杂网络通信代码的存根代码。开发者可以通过网络使用这些代码,以避免困难的网络层编程。
Java 有Java 远程方法调用(RMI),它类似于 Sun RPC,但特定于 Java 语言。RMI 编译器(RMIC)生成连接远程 Java 进程以调用方法并返回结果的存根代码。下图展示了 Java RMI 的过程流程:

Objective C 有分布式对象,而.NET 有远程调用,两者的工作方式相似。大多数现代编程语言都自带 RPC 功能。这些 RPC 设计能够将单一应用分割成多个进程(程序)。各个程序可以有独立的源代码仓库。尽管 RPC 设计在一定程度上有效,但在 1990 年代和 2000 年代初期,机器资源(CPU 和内存)有限。另一个缺点是这些设计通常要求使用相同的编程语言,并且这些设计是为客户端/服务器架构而设计的,而非分布式架构。此外,这些设计在开发时对安全性的考虑较少,因此不推荐在公共网络上使用。
在 2000 年代初,使用SOAP(HTTP/SSL)作为数据传输的Web 服务应运而生。这些服务使用 XML 作为数据呈现,并使用Web 服务描述语言(WSDL)定义服务。随后,通用描述、发现与集成(UDDI)作为服务注册表,用于查找 Web 服务应用。然而,由于当时机器资源不充裕,并且 Web 服务的编程与维护复杂,这一技术未被开发者广泛接受。
如今,gRPC (grpc.io/) 促使编程技术发生了彻底的重新评估,因为 gRPC 是一个简单、安全、支持多语言的工具。
RESTful 设计
在 2010 年代,机器甚至智能手机能够访问大量的 CPU 资源,并且几百 Mbps 的网络带宽到处可见。开发者开始利用这些资源,使应用程序代码和系统结构尽可能简单,从而加快了软件开发周期。
目前,硬件资源已经足够充裕,因此使用 HTTP/SSL 作为 RPC 传输是有意义的。此外,根据经验,开发人员选择以下方式简化这一过程:
-
通过将 HTTP 和 SSL/TLS 作为标准传输协议
-
通过使用 HTTP 方法进行创建/加载/上传/删除(CLUD)操作,例如
GET、POST、PUT或DELETE -
通过使用 URI 作为资源标识符,例如,具有 ID 为
123的用户,其 URI 将是/user/123/ -
通过使用 JSON 作为标准数据表示格式
这些概念被称为表征状态转移(RESTful)设计。它们已经被开发人员广泛接受,并成为分布式应用程序的事实标准。RESTful 应用程序允许使用任何编程语言,因为它们基于 HTTP。例如,可以使用 Java 作为 RESTful 服务器,Python 作为客户端。
RESTful 设计为开发人员带来了自由和机会。它使得代码重构、升级库甚至切换到其他编程语言变得更加容易。它还鼓励开发人员构建由多个 RESTful 应用组成的分布式模块化设计,这些应用被称为微服务。
如果你有多个 RESTful 应用,你可能会想知道如何在 VCS 上管理多个源代码以及如何部署多个 RESTful 服务器。然而,CI 和 CD 自动化使得构建和部署多个 RESTful 服务器应用变得更加容易。因此,微服务设计正在成为 Web 应用开发人员越来越受欢迎的选择。
微服务
尽管微服务的名称中有“微”字,但与 1990 年代或 2000 年代初期的应用程序相比,它们实际上相当“重”。它们使用完整堆栈的 HTTP/SSL 服务器,并包含整个 MVC 层。
微服务设计具有以下优势:
-
无状态:微服务不会存储用户会话,这有助于扩展应用程序。
-
没有共享数据存储:微服务应该拥有自己的数据存储,例如数据库。它们不应该与其他应用共享这些数据存储。这有助于封装后端数据库,从而更容易在单个微服务内重构和更新数据库架构。
-
版本控制与兼容性:微服务可能会更改和更新 API,但它们应定义版本,例如
/api/v1和/api/v2,并保持向后兼容性。这有助于解耦其他微服务和应用程序。 -
集成 CI/CD:微服务应采用 CI 和 CD 流程,以消除管理工作量。
有一些框架可以帮助构建基于微服务的应用程序,例如 Spring Boot(projects.spring.io/spring-boot/)和 Flask(flask.pocoo.org)。然而,也有许多基于 HTTP 的框架,因此开发者可以根据个人偏好选择任何框架或编程语言。这就是微服务设计的魅力所在。
以下图表展示了单体应用设计与微服务设计的对比。它表明,微服务设计与单体设计相同;两者都包含接口层、业务逻辑层、模型层和数据存储层。然而,它们的区别在于,应用程序由多个微服务构成。不同的应用程序可以共享相同的微服务:

开发者可以通过快速的软件交付方法添加必要的微服务,并修改现有的微服务,而不会影响现有的应用程序或服务。这是一个重要的突破。它代表了一个广泛被开发者接受的完整软件开发环境和方法论。
尽管 CI 和 CD 自动化流程有助于开发和部署微服务,但诸如虚拟机、操作系统、库、磁盘卷和网络等资源的数量,无法与单体应用程序相比。仍然有一些工具可以支持这些大型自动化环境在云端的运行。
自动化与工具
如前所述,自动化是实现快速软件交付的最佳方式。它解决了管理微服务的问题。然而,自动化工具不是普通的 IT 或基础设施应用程序,如Active Directory、BIND(DNS)或Sendmail(MTA)。为了实现自动化,我们需要一位既具备开发者技能,特别是在脚本语言方面的编码能力,又具备基础设施操作技能,了解虚拟机、网络和存储操作的工程师。
DevOps 是开发与运维的简称。它指的是使自动化流程(如 CI、基础设施即代码和 CD)得以实现的能力。它使用一些 DevOps 工具来完成这些自动化流程。
持续集成工具
其中一个流行的版本控制工具是 Git(git-scm.com)。开发者经常使用 Git 进行代码的检查和提交。有许多托管 Git 服务,包括 GitHub(github.com)和 Bitbucket(bitbucket.org)。这些服务允许你创建和保存 Git 代码库,并与其他用户通过互联网协作。以下截图展示了 GitHub 上的一个示例拉取请求:

构建服务器有很多种变体。Jenkins (jenkins.io) 是最为成熟的应用程序之一,还有 TeamCity (www.jetbrains.com/teamcity/)。除了构建服务器外,还有托管服务,也称为 软件即服务 (SaaS),例如 Codeship (codeship.com) 和 Travis CI (travis-ci.org)。SaaS 可以与其他 SaaS 工具集成。构建服务器能够调用外部命令,例如单元测试程序,这使得构建服务器成为 CI 流水线中的关键工具。
以下截图展示了使用 Codeship 的示例构建。我们从 GitHub 拉取代码,并调用 Maven 进行构建(mvn compile)和单元测试(mvn test)我们的示例应用程序:

配置管理工具
有多种配置管理工具可供选择。最受欢迎的包括 Puppet (puppet.com)、Chef (www.chef.io) 和 Ansible (www.ansible.com)。
AWS OpsWorks (aws.amazon.com/opsworks/) 提供了一个在 AWS 云上托管的 Chef 平台。以下截图展示了一个 Chef 配方(配置),用于通过 AWS OpsWorks 安装 Amazon CloudWatch Log 代理。AWS OpsWorks 在启动 EC2 实例时自动安装 CloudWatch Log 代理:

AWS CloudFormation (aws.amazon.com/cloudformation/) 有助于实现基础设施即代码。它支持自动化 AWS 操作,允许我们执行以下功能:
-
创建 VPC
-
在 VPC 上创建子网
-
在 VPC 上创建互联网网关
-
创建路由表以将子网与互联网网关关联
-
创建安全组
-
创建虚拟机实例
-
将安全组关联到虚拟机实例
CloudFormation 的配置是通过 JSON 编写的,如下图所示:

CloudFormation 支持参数化,因此使用相同配置的 JSON 文件,可以轻松创建具有不同参数(如 VPC 和 CIDR)的额外环境。它还支持更新操作。如果我们需要更改基础设施的一部分,无需重新创建整个环境。CloudFormation 可以识别配置的差异,并仅代表你执行必要的基础设施操作。
AWS CodeDeploy (aws.amazon.com/codedeploy/) 是另一个有用的自动化工具,专注于软件部署。它允许用户定义部署步骤。你可以在 YAML 文件中执行以下操作:
-
指定从哪里下载和安装应用程序
-
指定如何停止应用程序
-
指定如何安装应用程序
-
指定如何启动和配置应用程序
以下截图是 AWS CodeDeploy 配置文件appspec.yml的示例:

监控和日志记录工具
一旦你开始使用云基础设施管理微服务,就会有多种监控工具帮助你管理服务器。
Amazon CloudWatch是 AWS 的内置监控工具。无需安装代理;它会自动收集 AWS 实例的度量数据,并允许用户可视化这些数据以执行 DevOps 任务。它还支持基于你设定的标准触发警报。以下截图展示了 EC2 实例的 Amazon CloudWatch 度量数据:

亚马逊 CloudWatch 还支持收集应用日志。这需要我们在 EC2 实例上安装一个代理。当你需要开始管理多个微服务实例时,集中日志管理就显得特别有用。
ELK 是一个流行的技术栈组合,代表 Elasticsearch(www.elastic.co/products/elasticsearch)、Logstash(www.elastic.co/products/logstash)和 Kibana(www.elastic.co/products/kibana)。Logstash 聚合应用日志,将其转换为 JSON 格式,然后发送到 Elasticsearch。Elasticsearch 是一个分布式 JSON 数据库。Kibana 可以将存储在 Elasticsearch 中的数据可视化。以下 Kibana 示例展示了一个nginx访问日志:

Grafana(grafana.com)是另一个流行的可视化工具。它曾经与时间序列数据库如 Graphite(graphiteapp.org)或 InfluxDB(www.influxdata.com)连接。时间序列数据库旨在存储扁平化、非规范化的数值数据,如 CPU 使用率或网络流量。与关系型数据库(RDBMS)不同,时间序列数据库做了优化以节省存储空间,并能更快地对历史数值数据执行查询。大多数 DevOps 监控工具都在后台使用时间序列数据库。
以下 Grafana 截图展示了一些消息队列服务器的统计信息:

通讯工具
当你开始使用多个 DevOps 工具时,你需要来回访问多个控制台,以检查 CI 和 CD 管道是否正常工作。特别是,以下事件需要被监控:
-
将源代码合并到 GitHub
-
在 Jenkins 上触发新构建
-
触发 AWS CodeDeploy 部署应用程序的新版本
这些事件需要被跟踪。如果出现任何问题,DevOps 团队需要与开发人员和 QA 团队讨论。然而,这里的沟通可能会成为问题,因为 DevOps 团队需要逐一捕捉每个事件,然后根据情况传递。这是低效的。
有一些通信工具有助于将这些不同的团队集成在一起。它们允许任何人加入以查看事件并进行沟通。Slack (slack.com)和 HipChat (www.hipchat.com)是最流行的通信工具。
这些工具还支持与 SaaS 服务的集成,以便 DevOps 团队可以在一个聊天房间中查看事件。以下截图显示了与 Jenkins 集成的 Slack 聊天房间:

公有云
使用云技术时,CI、CD 和自动化工作可以轻松实现。特别是,公有云 API 帮助 DevOps 团队提出许多 CI 和 CD 工具。像 Amazon Web Services (aws.amazon.com)、Google Cloud Platform (cloud.google.com)和 Microsoft Azure (azure.microsoft.com)这样的公有云提供了一些 API,供 DevOps 团队控制云基础设施。DevOps 团队还可以减少资源浪费,因为你可以按需付费。当需要资源时,可以随时支付。公有云将继续以软件开发周期和架构设计的方式增长。这些都是将你的应用或服务推向成功所必需的。
以下截图显示了 Amazon Web Services 的网页控制台:

Google Cloud Platform 也有一个网页控制台,如下所示:

这里是 Microsoft Azure 控制台的截图:

所有三种云服务都提供免费试用期,DevOps 工程师可以利用这一期尝试并理解云基础设施的好处。
摘要
在本章中,我们讨论了软件开发方法论的历史、编程的演变以及 DevOps 工具。这些方法论和工具支持更快速的软件交付周期。微服务设计也有助于持续的软件更新。然而,微服务增加了环境管理的复杂性。
在第二章,使用容器的 DevOps,我们将描述 Docker 容器技术,它有助于构建微服务应用并以更高效、更自动化的方式管理它们。
第二章:使用容器进行 DevOps
我们现在熟悉了各种各样的 DevOps 工具,这些工具可以帮助我们自动化任务,并在应用程序交付过程中管理配置。然而,随着应用程序变得比以往更加多样化,挑战仍然存在。在本章中,我们将增加工具箱中的另一项技能:容器。特别是,我们将讨论Docker 容器。通过这样做,我们将努力理解以下内容:
-
与容器相关的关键概念
-
运行 Docker 应用程序
-
使用 Dockerfile 构建 Docker 应用程序
-
使用 Docker Compose 编排多个容器
理解容器
容器的一个关键特性是隔离。在本节中,我们将通过查看容器如何实现隔离以及这在软件开发生命周期中的重要性来建立对这一强大工具的适当理解。
资源隔离
当一个应用程序启动时,它消耗 CPU 时间,占用内存空间,链接到其依赖库,写入磁盘,传输数据包,可能还会访问其他设备。它使用的所有资源都是一种资源,这些资源由同一主机上的所有程序共享。为了提高资源利用效率,我们可以尝试在单台机器上运行尽可能多的应用程序。然而,即使我们只想运行两个应用程序,使每个应用程序在容器中有效运行的复杂性也会呈指数增长,更不用说处理大量应用程序和机器了。正因如此,将物理计算单元的资源分隔为独立的部分的想法很快成为行业中的范例。
你可能听说过虚拟机(VMs)、BSD jails、Solaris 容器、Linux 容器、Docker 等术语。所有这些技术都向我们承诺类似的隔离概念,但使用的机制基本上是不同的,因此实际的隔离级别也不同。例如,VM 的实现涉及使用虚拟化技术完全虚拟化硬件层,并通过一个虚拟机监控程序来管理。如果你想在 VM 上运行一个应用程序,你必须从一个完整的操作系统开始。换句话说,资源在运行在同一个虚拟化管理程序下的客户操作系统之间是隔离的。相比之下,Linux 和 Docker 容器是基于 Linux 原生技术构建的,这意味着它们只能在具有这些功能的操作系统上运行。BSD jails 和 Solaris 容器也以类似的方式工作,但在其他操作系统上。下图说明了 Linux 容器与 VM 之间的隔离关系。容器通过操作系统层实现应用程序的隔离,而基于 VM 的隔离则是由底层的虚拟化管理程序或主机操作系统实现的:

Linux 容器
一个 Linux 容器由多个构建模块组成,其中最重要的两个是 命名空间 和 控制组(cgroups)。这两者都是 Linux 内核特性。命名空间提供了对某些系统资源的逻辑划分,例如挂载点(mnt)、进程 ID(PID)和网络(net)。为了更好地理解隔离的概念,我们来看看 pid 命名空间的一些简单示例。以下示例来自 Ubuntu 18.04.1 和 util-linux 2.31.1。
当我们在终端输入 ps axf 时,会看到一长串正在运行的进程:
$ ps axf
PID TTY STAT TIME COMMAND
2 ? S 0:00 [kthreadd]
4 ? I< 0:00 \_ [kworker/0:0H]
5 ? I 0:00 \_ [kworker/u2:0]
6 ? I< 0:00 \_ [mm_percpu_wq]
7 ? S 0:00 \_ [ksoftirqd/0]
...
ps 是一个用于报告系统当前进程的工具。ps axf 提供了一个包含所有进程的树状列表。
现在让我们使用 unshare 进入一个新的 pid 命名空间,unshare 能够将进程资源逐步分离到一个新的命名空间中。接下来,我们再检查一下进程:
$ sudo unshare --fork --pid --mount-proc=/proc /bin/sh
# ps axf
PID TTY STAT TIME COMMAND
1 pts/0 S 0:00 /bin/sh
2 pts/0 R+ 0:00 ps axf
你会发现新命名空间中 shell 进程的 pid 变成了 1,所有其他进程都消失了。这意味着你已经成功创建了一个 pid 容器。接下来,让我们切换到命名空间外的另一个会话,再次列出进程:
$ ps axf ## from another terminal
PID TTY STAT TIME COMMAND
...
1260 pts/0 Ss 0:00 \_ -bash
1496 pts/0 S 0:00 | \_ sudo unshare --fork --pid --mount-proc=/proc /bin/sh
1497 pts/0 S 0:00 | \_ unshare --fork --pid --mount-proc=/proc /bin/sh
1498 pts/0 S+ 0:00 | \_ /bin/sh
1464 pts/1 Ss 0:00 \_ -bash
...
你仍然可以在新命名空间中看到其他进程和你的 shell 进程。由于 pid 命名空间的隔离,不同命名空间中的进程是无法相互看到的。然而,如果某个进程使用了大量的系统资源,比如内存,它可能会导致系统资源耗尽,进而变得不稳定。换句话说,一个隔离的进程仍然可能会干扰其他进程,甚至崩溃整个系统,如果我们没有对其资源使用进行限制的话。
下图展示了 PID 命名空间以及 内存不足(OOM)事件如何影响子命名空间外的其他进程。图中的编号方块是系统中的进程,数字是它们的 PID。带有两个数字的方块表示在子命名空间中创建的进程,其中第二个数字表示它们在子命名空间中的 PID。在图的上半部分,系统仍然有可用的空闲内存。然而,在图的下半部分,子命名空间中的进程耗尽了系统中的剩余内存。由于没有空闲内存,主机内核随后启动 OOM 杀手来释放内存,受害者通常是子命名空间外的进程。这里的示例中,系统中的进程 8 和 13 被杀死:

鉴于此,cgroups 在此被用于限制资源的使用。与命名空间类似,它可以对不同种类的系统资源施加约束。接下来我们继续从 pid 命名空间出发,通过 yes > /dev/null 来生成一些 CPU 负载,然后使用 top 监控它:
## in the container terminal
# yes > /dev/null & top PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2 root 20 0 7468 788 724 R 99.7 0.1 0:15.14 yes
1 root 20 0 4628 780 712 S 0.0 0.1 0:00.00 sh
3 root 20 0 41656 3656 3188 R 0.0 0.4 0:00.00 top
我们的 CPU 负载达到了 100%,正如预期的那样。现在让我们通过 cgroup CPU 来限制它。cgroups 被组织为 /sys/fs/cgroup/ 下的文件夹。首先,我们需要切换到宿主机会话:
## on the host session
$ ls /sys/fs/cgroup
blkio cpu cpuacct cpu,cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids rdma systemd unified
每个文件夹代表它所控制的资源。创建一个 cgroup 并用它来控制进程非常容易:只需在资源类型下创建一个任意名称的文件夹,并将您想要控制的进程 ID 添加到 tasks 中。这里,我们希望限制 yes 进程的 CPU 使用率,因此在 cpu 下创建一个新文件夹,并找出 yes 进程的 PID:
## also on the host terminal
$ ps ax | grep yes | grep -v grep
1658 pts/0 R 0:42 yes
$ sudo mkdir /sys/fs/cgroup/cpu/box && \
echo 1658 | sudo tee /sys/fs/cgroup/cpu/box/tasks > /dev/null
我们刚刚将 yes 加入了新创建的 box CPU 组,但策略仍未设置,进程仍然没有任何限制地运行。通过将所需的数字写入相应的文件来设置限制,然后再次检查 CPU 使用情况:
$ echo 50000 | sudo tee /sys/fs/cgroup/cpu/box/cpu.cfs_quota_us > /dev/null
## go back to namespaced terminal, check stats with topPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2 root 20 0 7468 748 684 R 50.3 0.1 6:43.68 yes
1 root 20 0 4628 784 716 S 0.0 0.1 0:00.00 sh
3 root 20 0 41656 3636 3164 R 0.0 0.4 0:00.08 top
CPU 使用率显著降低,这意味着我们的 CPU 限制工作正常。
前两个例子阐明了 Linux 容器如何隔离系统资源。通过在应用程序中加入更多的限制,我们可以构建一个完全隔离的盒子,包括文件系统和网络,而无需在其中封装操作系统。
容器化交付
运行应用程序的通常方式包括以下步骤:
-
配置机器和相应的基础设施资源
-
安装操作系统
-
安装系统程序和应用程序依赖项
-
部署应用程序
-
维护应用程序的运行状态
整个过程繁琐而复杂,这也是为什么我们通常不希望手动进行。配置管理工具,在第一章《DevOps 简介》中介绍,旨在消除交付过程中大部分的劳动。其模块化和基于代码的配置设计能够有效工作,直到应用程序栈变得复杂和多样。维护庞大的配置库是困难的,尤其是当它是包含各种 hack 的遗留配置时。尽管使用配置管理工具更改配置代码会直接影响生产环境,但配置代码通常比应用程序代码更少受到关注。每当我们想要更新已安装的包时,就必须处理系统和应用程序包之间错综复杂且脆弱的依赖关系。升级与之无关的包时,一些应用程序意外崩溃并不罕见。此外,升级配置管理工具本身也是一项具有挑战性的任务。
为了解决这个问题,推出了使用预先构建的虚拟机镜像进行不可变部署。这意味着每当我们对系统或应用程序包进行更新时,我们将根据更改构建完整的虚拟机镜像并进行部署。这减少了一些复杂性,因为我们可以在推出之前测试更改,并且能够为无法共享相同环境的应用程序定制运行时。然而,使用虚拟机镜像进行不可变部署是昂贵的。启动、分发和运行臃肿的虚拟机镜像的开销远大于部署软件包。
这里的容器是一个与部署需求完美契合的拼图块。容器的表现形式可以在版本控制系统(VCS)中进行管理并构建为一个 Blob 镜像,这个镜像也可以不可变地部署。这使得开发人员能够抽象化实际资源,基础设施工程师则可以避免依赖地狱。此外,由于我们只需要打包应用程序本身及其依赖库,因此其镜像大小会比虚拟机的镜像小得多。因此,分发容器镜像比分发虚拟机镜像更加经济。此外,我们已经知道,在容器内运行一个进程与在其 Linux 主机上运行几乎没有区别,因此几乎不会产生任何开销。总而言之,容器是轻量级的、自包含的,并且几乎不可变的。这为应用程序与基础设施之间的责任区分提供了明确的界限。
由于 Linux 容器共享相同的内核,因此容器运行在其上的时候,内核仍然存在潜在的安全风险。为了解决这个问题,一种新兴的趋势是使虚拟机(VM)的运行像操作系统容器一样简单高效,例如基于unikernel的解决方案或Kata 容器。另一种方法是插入一个中介层,将应用程序与宿主内核分隔开来,例如gVisor。
容器入门
有许多成熟的容器引擎,例如Docker(www.docker.com)或rkt(coreos.com/rkt),它们已经实现了用于生产环境的功能,因此你不需要从头开始构建自己的容器。此外,由容容器行业领导者组成的开放容器倡议(www.opencontainers.org)已经对容器规范进行了标准化。任何标准的实现,无论底层平台如何,都应具有类似的属性,因为 OCI 旨在提供跨各种操作系统使用容器的无缝体验。事实上,Docker 的核心是containerd,它是一个兼容 OCI 的运行时,可以在没有 Docker 的情况下使用。本书中,我们将使用 Docker(社区版)容器引擎来构建我们的容器化应用程序。
为 Ubuntu 安装 Docker
Docker 需要 64 位版本的 Bionic 18.04 LTS、Artful 17.10、Xenial 16.04 LTS 或 Trusty 14.04 LTS。你可以通过apt-get install docker.io来安装 Docker,但其更新通常比 Docker 官方仓库慢。
以下是来自 Docker 的安装步骤(docs.docker.com/install/linux/docker-ce/ubuntu/):
- 确保你已安装允许
apt使用仓库的包;如果没有,可以使用以下命令获取它们:
$ sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common
- 添加 Docker 的
gpg密钥并验证其指纹是否与9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88匹配:
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
- 设置
amd64架构的仓库:
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
- 更新包索引并安装 Docker CE:
$ sudo apt-get update && sudo apt-get install docker-ce
为 CentOS 安装 Docker
运行 Docker 需要 64 位版本的 CentOS 7。你可以通过sudo yum install docker从 CentOS 的仓库获取 Docker 包,但这可能是旧版本。再次,Docker 官方指南中的安装步骤如下:docs.docker.com/install/linux/docker-ce/centos/。
- 安装工具以使
yum能够使用额外的仓库:
$ sudo yum install -y yum-utils
- 设置 Docker 的仓库:
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
- 安装 Docker CE 并启动。如果提示密钥验证,确保它与
060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35匹配:
$ sudo yum install docker-ce
$ sudo systemctl start docker
为 macOS 安装 Docker
Docker 将一个微型 Linux 与虚拟机管理框架结合,在 macOS 上构建本地应用程序,这意味着我们不需要第三方虚拟化工具就能在 Mac 上使用 Docker。为了受益于虚拟机管理框架,必须将 macOS 升级到 10.10.3 或更高版本:
- 下载 Docker 包并安装:
download.docker.com/mac/stable/Docker.dmg。
Docker for Windows 也不需要任何第三方工具。可以在以下链接查找安装指南:docs.docker.com/docker-for-windows/install。
- 你现在进入 Docker 环境。尝试创建并运行你的第一个 Docker 容器。如果你在 Linux 上,可以使用
sudo运行命令:
$ docker run alpine ls
bin dev etc home lib media mnt proc root run sbin srv sys tmp usr var
- 你会发现你现在在
root目录下,而不是当前目录。让我们再次查看进程列表:
$ docker run alpine ps aux
PID USER TIME COMMAND
1 root 0:00 ps aux
如预期的那样,它是隔离的。你现在已经准备好与容器一起工作了。
Alpine 是一个 Linux 发行版。由于其体积非常小,许多人使用它作为构建应用容器的基础镜像。然而需要注意的是,它与主流 Linux 发行版还是有一些差异。例如,Alpine 使用musl libc,而大多数发行版使用glibc。
容器的生命周期
使用容器不像我们习惯使用的大多数工具那样直观,因此我们需要改变工作方式。在本节中,我们将介绍如何使用 Docker,以便能够从容器中受益。
Docker 基础知识
当执行docker run alpine ls时,Docker 会在后台执行以下步骤:
-
它在本地查找
alpine镜像。如果找不到,Docker 会尝试从公共 Docker 仓库中拉取并存储到本地镜像库。 -
它提取镜像并相应地创建一个容器。
-
它执行图像中定义的入口点命令,这些命令是图像名称后的参数。在此示例中,参数是
ls。默认情况下,Linux 基础的 Docker 上的入口点是/bin/sh -c。 -
当入口点进程完成时,容器会退出。
镜像是一个不可变的代码、库、配置等的捆绑包,包含了我们想要放入其中的所有内容。容器是镜像的一个实例,它在运行时执行。你可以使用docker inspect IMAGE和docker inspect CONTAINER命令查看镜像和容器之间的区别。
使用docker run启动的任何内容都会占据前台;-d选项(--detach)允许我们以分离模式运行容器。有时候,我们可能需要进入一个活动的容器,检查其中的镜像或更新某些内容。为此,我们可以使用-i和-t选项(--interactive和--tty)。如果我们想与一个分离的容器进行交互,我们可以使用exec和attach命令:exec命令允许我们在运行中的容器内运行进程,而attach命令则按字面意义工作。以下示例演示了如何使用这些命令:
$ docker run busybox /bin/sh -c "while :;do echo 'meow~';sleep 1;done"
meow~
meow~
...
现在你的终端应该充满了meow~。切换到另一个终端并运行docker ps,这是一个获取容器状态的命令,用来查找容器的名称和 ID。在这里,名称和 ID 都是由 Docker 生成的,你可以使用其中任何一个来访问容器:
$ docker ps
CONTAINER ID IMAGE (omitted) STATUS PORTS NAMES
d51972e5fc8c busybox ... Up 3 seconds agitated_banach
$ docker exec -it d51972e5fc8c /bin/sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh -c while :;do echo 'meow~';sleep 1;done
19 root 0:00 /bin/sh
30 root 0:00 sleep 1
31 root 0:00 ps
/ # kill -s 2 1
## container terminated
为了方便起见,可以在create或run时使用--name标志指定容器的名称。
一旦我们访问容器并检查其进程,我们会看到两个 shell:一个在发出 meow~,另一个是我们所在的位置。使用kill -s 2 1命令终止容器内的第一个 shell,我们会看到整个容器停止,因为入口点进程已经退出。最后,我们使用docker ps -a列出已停止的容器,并通过docker rm CONTAINER_NAME或docker rm CONTAINER_ID清理它们:
$ docker ps -a
CONTAINER ID IMAGE (omitted) STATUS PORTS NAMES
d51972e5fc8c busybox ... Exited (130) agitated_banach
$ docker rm d51972e5fc8c ## "agitated_banach" also works
d51972e5fc8c
$ docker ps -a
CONTAINER ID IMAGE (omitted) STATUS PORTS NAMES
## nothing left now
从 Docker 1.13 开始,docker system prune命令被引入,帮助我们轻松清理已停止的容器和占用的资源。
PID 1进程在类 UNIX 操作系统中非常特殊。无论它是什么类型的进程,它都应该回收已退出的子进程,并且不能接收SIGKILL信号。这就是为什么前面的例子使用了SIGINT(2)而不是SIGKILL的原因。此外,容器中的大多数入口进程并不处理已终止的子进程,这可能会导致系统中出现大量未回收的僵尸进程。如果需要在没有 Kubernetes 的情况下运行 Docker 容器到生产环境,可以在docker run时使用--init标志。这样会在容器中注入一个PID 1进程,它能够正确处理已终止的子进程。
层、镜像、容器和数据卷
我们知道镜像是不可变的,容器是短暂的,并且我们知道如何将镜像作为容器运行。然而,我们仍然缺少一些关于如何打包镜像的信息。
镜像是一个只读的堆栈,由一个或多个层组成,而层是文件系统中的文件和目录集合。为了提高磁盘空间的利用率,层不仅限于一个镜像,而是可以在多个镜像之间共享,这意味着 Docker 只会在本地存储一份基础镜像的副本,无论从它衍生出多少镜像。你可以使用docker history [image]命令来了解一个镜像是如何构建的。例如,如果你用docker history alpine检查 Alpine,你会发现它只有一层。
每当创建一个容器时,它会在基础镜像上添加一个薄的、可写的层。Docker 在这个薄层上采用写时复制(Copy-On-Write, COW)策略。这意味着容器会读取基础镜像中存储目标文件的层,如果文件被修改,它就将文件复制到自己可写的层中。这种方式防止了从同一镜像创建的容器相互干扰。docker diff [CONTAINER]命令显示容器与其基础镜像在文件系统状态上的差异。例如,如果基础镜像中的/etc/hosts被修改,Docker 会将该文件复制到可写层中,并且它将成为docker diff输出中的唯一文件。
以下图示展示了 Docker 镜像的层级结构:

需要注意的是,可写层中的数据会随着容器的删除而被删除。为了持久化数据,你可以通过docker commit [CONTAINER]命令将容器层提交为一个新镜像,或者将数据卷挂载到容器中。
数据卷允许容器执行读写操作,绕过 Docker 的文件系统。它可以位于主机的目录中,也可以位于其他存储介质中,如 Ceph 或 GlusterFS。因此,任何针对卷的磁盘 I/O 操作都可以根据底层存储以原生速度运行。由于数据在容器之外是持久化的,因此可以被多个容器复用和共享。挂载卷是通过在docker run或docker create命令中指定-v(--volume)标志来完成的。以下示例将在容器的/chest目录下挂载一个卷,并在其中留下一个文件。之后,我们使用docker inspect定位数据卷:
$ docker run --name demo -v /chest alpine touch /chest/coins
$ docker inspect demo ## or filter outputs with --format '{{json .Mounts}}'
... "Mounts": [
{
"Type": "volume",
"Name":"(hash-digits)",
"Source": "/var/lib/docker/volumes/(hash-digits)/_data",
"Destination": "/chest",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
...
$ ls /var/lib/docker/volumes/(hash-digits)/_data
coins
Docker CE 在 macOS 上提供的微型 Linux 的默认tty路径可以在以下位置找到:
~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty。
你可以使用screen附加到它。
数据卷的一个使用场景是容器之间共享数据。为此,我们首先创建一个容器并在其上挂载卷,然后在启动其他容器时通过--volumes-from标志引用该卷。以下示例创建一个带有数据卷/share-vol的容器,容器 A 可以将文件放入该卷,容器 B 可以读取它:
$ docker create --name box -v /share-vol alpine nop
7ed7c0c4426df53275c0e41798923121093b67d41bec17d50756250508f7b897
$ docker run --name AA --volumes-from box alpine touch /share-vol/wine
$ docker run --name BB --volumes-from box alpine ls /share-vol
wine
此外,数据卷可以挂载到指定的host路径下,当然其中的数据是持久化的:
$ docker run --name hi -v $(pwd)/host/dir:/data alpine touch /data/hi
$ docker rm hi
$ ls $(pwd)/host/dir
hi
分发镜像
注册表是一个存储、管理和分发镜像的服务。公共服务,如 Docker Hub(hub.docker.com)和 Quay(quay.io),收集了各种流行工具的预构建镜像,如 Ubuntu、nginx,以及其他开发者的自定义镜像。我们已经使用过多次的 Alpine Linux 工具实际上是从 Docker Hub(hub.docker.com/_/alpine)拉取的。你也可以将自己的工具上传到这些服务,并与其他人共享。
如果你需要一个私有注册表,但由于某些原因不想订阅注册服务提供商的付费计划,你始终可以自己搭建一个 Docker Registry(hub.docker.com/_/registry)。其他流行的注册服务提供商包括 Harbor(goharbor.io/)和 Portus(port.us.org/)。
在配置容器之前,Docker 会尝试根据镜像名称中指定的规则来定位指定的镜像。镜像名称由三个部分组成,[registry/]name[:tag],并且会按以下规则解析:
-
如果省略了
registry字段,Docker 会在 Docker Hub 上搜索该名称。 -
如果
registry字段是一个注册服务器,Docker 会在该服务器上搜索名称。 -
一个名称中可以包含多个斜杠。
-
如果省略标签,默认标签为
latest。
例如,像 gcr.io/google-containers/guestbook:v3 这样的图像名称指示 Docker 从 gcr.io 下载 google-containers/guestbook 的 v3 版本。同样,如果你想将图像推送到注册表,请以相同方式标记图像并使用 docker push [IMAGE] 推送它。要列出你当前在本地拥有的图像,可以使用 docker images。你可以使用 docker rmi [IMAGE] 删除一个图像。以下示例展示了如何在不同的注册表之间操作:从 Docker Hub 下载一个 nginx 图像,将其标记为私有注册表路径,并相应地推送。私有注册表是通过 docker run -p 5000:5000 registry 在本地托管的。
这里,我们使用前面提到的注册表和最基本的设置。有关部署的更详细指南,请参阅以下链接:docs.docker.com/registry/deploying/。
请注意,尽管默认标签是 latest,但你必须显式地对其进行标签和 push:
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
802b00ed6f79: Pull complete
...
Status: Downloaded newer image for nginx:latest
$ docker tag nginx localhost:5000/comps/prod/nginx:1.15
$ docker push localhost:5000/comps/prod/nginx:1.15
The push refers to repository [localhost:5000/comps/prod/nginx]
...
8b15606a9e3e: Pushed
1.15: digest: sha256:(64-digits-hash) size: 948
$ docker tag nginx localhost:5000/comps/prod/nginx
$ docker push localhost:5000/comps/prod/nginx
The push refers to repository [localhost:5000/comps/prod/nginx]
...
8b15606a9e3e: Layer already exists
latest: digest: sha256:(64-digits-hash) size: 948
大多数注册表服务在你推送图像时都会要求认证。docker login 就是为此目的设计的。对于一些较旧版本的 Docker,当你尝试拉取图像时,即使图像路径有效,你有时也会收到 image not found 错误。这通常意味着你没有权限访问存储该图像的注册表。
除了通过注册表服务分发的图像外,还有将图像作为 TAR 存档导出并导入回本地仓库的选项:
-
docker commit [CONTAINER]:将容器层的更改提交到一个新的图像中 -
docker save --output [filename] IMAGE1 IMAGE2 ...:将一个或多个图像保存为 TAR 存档 -
docker load -i [filename]:将 TAR 图像加载到本地仓库 -
docker export --output [filename] [CONTAINER]:将容器的文件系统导出为 TAR 存档 -
docker import --output [filename] IMAGE1 IMAGE2:导入导出的 TAR 存档
commit、save 和 export 命令看起来非常相似。主要区别在于,保存的图像即使最终会被删除,仍然会保留层与层之间的文件。而导出的图像则会将所有中间层压缩成一个最终层。另一个区别是,保存的图像会保留元数据,如层历史记录,但导出的图像没有这些信息。因此,导出的图像通常较小。
下图描述了容器与图像之间状态的关系。箭头上的说明是相应的 Docker 子命令:

容器技术与操作系统功能紧密绑定,这意味着为一个平台构建的镜像无法在另一个平台上运行,除非在目标平台上重新编译新的镜像。为了简化这一过程,Docker 引入了镜像清单(Image Manifest),支持多架构构建。我们在本书中不会进一步讨论多架构构建,但你可以通过以下链接找到更多信息:docs.docker.com/edge/engine/reference/commandline/manifest/。
连接容器
Docker 提供了三种网络来管理容器与主机之间的通信,分别是 bridge、host 和 none:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
8bb41db6b13e bridge bridge local
4705332cb39e host host local
75ab6cbbbbac none null local
默认情况下,每个容器在创建时都会连接到桥接网络。在这种模式下,每个容器都会分配一个虚拟接口和一个私有 IP 地址,所有通过该接口的流量会桥接到主机的 docker0 接口。位于同一桥接网络中的容器也可以通过 IP 地址相互连接。让我们运行一个容器,通过 5000 端口发送一条简短消息,并观察其配置。--expose 标志会将指定端口暴露给容器外部:
$ docker run --name greeter -d --expose 5000 busybox \
/bin/sh -c "echo Welcome stranger! | nc -lp 5000"
841138a693d220c92b8634adf97426559fd0ae622e94ac4ee9f295ab088833f5
$ docker exec greeter ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
...
这里,greeter 容器被分配到 IP 地址 172.17.0.2。现在,运行另一个容器,通过这个 IP 地址连接它:
$ docker run -t busybox telnet 172.17.0.2 5000
Welcome stranger!
Connection closed by foreign host
docker network inspect bridge 命令提供了配置详情,比如已连接的容器、子网段和网关信息。
你可以将一些容器分组到一个用户定义的桥接网络中。这是连接单个主机上多个容器的推荐方式。用户定义的桥接网络与默认网络略有不同,主要的区别是你可以通过容器的名称而不是 IP 地址来访问容器。创建网络可以使用 docker network create [NW-NAME] 命令,我们可以通过在创建时添加 --network [NW-NAME] 标志将容器连接到该网络。容器的网络名称默认为其名称,但也可以通过 --network-alias 标志为其指定其他别名:
## create a network called "room"
$ docker network create room
a59c7fda2e636e2f6d8af5918c9cf137ca9f09892746f4e072acd490c00c5e99
## run a container with the name "sleeper" and an alias "dad" in the room
$ docker run -d --network room \
--network-alias dad --name sleeper busybox sleep 60
56397917ccb96ccf3ded54d79e1198e43c41b6ed58b649db12e7b2ee06a69b79
## ping the container with its name "sleeper" from another container
$ docker run --network room busybox ping -c 1 sleeper
PING sleeper (172.18.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.087 ms
--- sleeper ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.087/0.087/0.087 ms
## ping the container with its alias "dad", it also works
$ docker run --network room alpine ping -c 1 dad
PING dad (172.18.0.2): 56 data bytes
...
host 网络如其名所示,每个连接的容器共享主机的网络,但同时失去了隔离特性。none 网络是一个逻辑上与外界隔绝的盒子。无论是进入流量还是出去流量,所有的流量都被隔离在容器内,因为容器没有附加任何网络接口。在这里,我们将一个监听 5000 端口的容器连接到 host 网络,并与其进行本地通信:
$ docker run -d --expose 5000 --network host busybox \
/bin/sh -c "echo im a container | nc -lp 5000"
a6918174c06f8f3f485913863ed69d4ae838fb550d9f4d66e24ef91534c76b3a
$ telnet localhost 5000
im a container
Connection closed by foreign host
如果你使用的是 Docker CE for macOS,主机是运行在虚拟化框架上的微型 Linux。
主机与三种网络模式之间的交互如下图所示。host 和 bridge 网络中的容器连接了适当的网络接口,并与同一网络中的容器以及外部世界进行通信,但 none 网络与主机接口保持隔离:

除了共享主机网络外,-p(--publish) [host]:[container] 标志在创建容器时还允许你将主机端口映射到容器端口。我们不需要同时附加 --expose 标志与 --publish 标志,因为无论如何你都需要打开容器的端口。以下命令将在端口 80 启动一个简单的 HTTP 服务器,你也可以用浏览器查看它:
$ docker run -p 80:5000 busybox /bin/sh -c \
"while :; do echo -e 'HTTP/1.1 200 OK\n\ngood day'|nc -lp 5000; done"
$ curl localhost
good day
使用 Dockerfile
在构建镜像时,无论是使用 docker commit 还是 export,以可管理的方式优化构建结果都是一个挑战,更不用说将其与 CI/CD 流水线集成了。Dockerfile 以代码的形式表示构建任务,这大大减少了构建任务的难度。在这一部分,我们将描述如何将 Docker 命令映射到 Dockerfile,并朝着优化它的方向迈出一步。
编写你的第一个 Dockerfile
Dockerfile 由一系列文本指令组成,指导 Docker 守护进程形成一个镜像,Dockerfile 必须以 FROM 指令开始。例如,我们可能会从以下一行命令构建镜像:
docker commit $( \
docker start $( \
docker create alpine /bin/sh -c \
"echo My custom build > /etc/motd" \
))
这大致相当于以下的 Dockerfile:
FROM alpine
RUN echo "My custom build" > /etc/motd
显然,使用 Dockerfile 构建要更加简洁和精确。
docker build [OPTIONS] [CONTEXT] 命令是唯一与构建任务相关的命令。上下文可以是本地路径、URL 或 stdin,表示 Dockerfile 的位置。一旦触发构建,Dockerfile 以及上下文中的所有内容都会提前发送到 Docker 守护进程,然后守护进程将按顺序执行 Dockerfile 中的指令。每次执行指令都会生成一个新的缓存层,随后的指令将在新的缓存层上执行。由于上下文会被发送到一个不一定是本地路径的地方,且发送太多不相关的文件会浪费时间,因此最好将 Dockerfile、代码、必要的文件和 .dockerignore 文件放在一个 empty 文件夹中,以确保最终生成的镜像只包含所需的文件。
.dockerignore 文件是一个列表,指示在构建时可以忽略同一目录下哪些文件。它通常如下所示:
$ cat .dockerignore
# ignore .dockerignore, .git
.dockerignore
.git
# exclude all *.tmp files and vim swp file recursively
**/*.tmp
**/[._]*.s[a-w][a-z]
# exclude all markdown files except README*.md
!README*.md
通常,docker build 会尝试在上下文中找到一个名为 Dockerfile 的文件来开始构建。然而,有时我们可能希望给它一个不同的名字,可以使用 -f (--file) 标志来指定。另一个有用的标志是 -t (--tag),它可以在镜像构建完成后为镜像添加一个或多个仓库标签。假设我们想在 ./deploy 目录下构建一个名为 builder.dck 的 Dockerfile 并标记当前日期和最新标签,命令如下:
## We can assign the image with more than one tag within a build command
$ docker build -f deploy/builder.dck \
-t my-reg.com/prod/teabreaker:$(date +"%g%m%d") \
-t my-reg.com/prod/teabreaker:latest .
Dockerfile 的语法
Dockerfile 的构建模块由十几个指令组成。它们中的大多数由 docker run/create 标志的功能组成。让我们来看看其中最重要的几个:
-
FROM <IMAGE>[:TAG|[@DIGEST]]:这是告诉 Docker 守护进程当前Dockerfile所基于的镜像。这也是唯一必须出现在Dockerfile中的指令;你可以有一个只包含这一行的Dockerfile。与所有其他与镜像相关的命令一样,如果没有指定标签,则默认为最新版本。 -
RUN:
RUN <commands>
RUN ["executable", "params", "more params"]
RUN 指令会在当前缓存层运行一行命令,并提交其结果。两种形式之间的主要区别在于命令的执行方式。第一种形式被称为 shell 形式,它实际上以 /bin/sh -c <commands> 形式执行命令。另一种形式是 exec 形式,它直接使用 exec 处理命令。
使用 shell 形式类似于编写 shell 脚本,因此通过 shell 操作符和行继续符来连接多个命令、条件测试或变量替换是完全有效的。然而,请记住,命令不是由 bash 处理的,而是由 sh 处理的。
exec 形式被解析为 JSON 数组,这意味着你必须用双引号将文本括起来,并转义保留字符。此外,由于该命令不会由任何 shell 处理,因此数组中的 shell 变量不会被评估。另一方面,如果基础镜像中不存在 shell,你仍然可以使用 exec 形式来调用可执行文件。
CMD:
CMD ["executable", "params", "more params"]
CMD ["param1","param2"]
CMD command param1 param2 ...
CMD 用于设置构建镜像的默认命令,但它不会在构建时运行该命令。如果在执行 docker run 时提供了参数,CMD 配置会被覆盖。CMD 的语法规则几乎与 RUN 相同;前两种形式是 exec 形式,第三种是 shell 形式,它会将 /bin/sh -c 作为前缀加到参数前。还有一个与 CMD 交互的 ENTRYPOINT 指令;当容器启动时,ENTRYPOINT 的参数会添加到三种 CMD 形式前面。在 Dockerfile 中可以有多个 CMD 指令,但只有最后一个会生效。
ENTRYPOINT:
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
这两种形式分别是exec形式和 shell 形式,语法规则与RUN相同。入口点是镜像的默认可执行文件。这意味着当容器启动时,它会运行由ENTRYPOINT配置的可执行文件。当ENTRYPOINT与CMD和docker run参数结合使用时,以不同的形式编写会导致截然不同的行为。以下是关于它们组合的规则:
- 如果
ENTRYPOINT是以 shell 形式出现,则CMD和docker run的参数将被忽略。运行时命令将如下所示:
/bin/sh -c entry_cmd entry_params ...
- 如果
ENTRYPOINT是exec形式且指定了docker run参数,则CMD命令会被覆盖。运行时命令将如下所示:
entry_cmd entry_params run_arguments
- 如果
ENTRYPOINT是exec形式且只配置了CMD,则对于三种形式,运行时命令将变为以下内容:
entry_cmd entry_parms CMD_exec CMD_parms
entry_cmd entry_parms CMD_parms
entry_cmd entry_parms /bin/sh -c CMD_cmd CMD_parms
ENV:
ENV key value
ENV key1=value1 key2=value2 ...
ENV指令为随后的指令和构建的镜像设置环境变量。第一种形式将键设置为第一个空格后的字符串,包括特殊字符,但不包括行继续符。第二种形式允许我们在一行中设置多个变量,变量之间用空格分隔。如果值中包含空格,需用双引号括起来或转义空格字符。此外,使用ENV定义的键也会对同一文档中的变量生效。请参见以下示例以观察ENV的行为:
FROM alpine
# first form
ENV k1 wD # aw
# second form, line continuation character also works
ENV k2=v2 k3=v\ 3 \
k4="v 4"
# ${k2} would be evaluated, so the key is "k_v2" in this case
ENV k_${k2}=$k3 k5=\"K\=da\"
# show the variables
RUN env | grep -Ev '(HOSTNAME|PATH|PWD|HOME|SHLVL)' | sort
在docker build过程中,输出将如下所示:
...
---> Running in c5407972c5f5
k1=wD # aw
k2=v2
k3=v 3
k4=v 4
k5="K=da"
k_v2=v 3 ...
ARG key[=<default value>]:ARG指令可以通过docker build的--build-arg标志,将我们的参数作为环境变量传递到构建容器中。例如,使用docker build --build-arg FLAGS=--static构建以下文件,将导致最后一行出现RUN ./build/dev/run --static:
FROM alpine
ARG TARGET=dev
ARG FLAGS
RUN ./build/$TARGET/run $FLAGS
与ENV不同,每行只能分配一个参数。如果我们将ARG和ENV一起使用,则ARG的值(无论是通过--build-arg传递还是默认值)将被ENV的值覆盖。由于代理环境变量的频繁使用,默认情况下所有这些变量都支持作为参数,包括HTTP_PROXY、http_proxy、HTTPS_PROXY、https_proxy、FTP_PROXY、ftp_proxy、NO_PROXY和no_proxy。这意味着我们可以在不事先在Dockerfile中定义它们的情况下传递这些构建参数。需要注意的一点是,ARG的值将保留在构建机器的 shell 历史记录和镜像的 Docker 历史记录中,这意味着不建议通过ARG传递敏感数据:
-
LABEL key1=value1 key2=value2 ...:LABEL的使用类似于ENV,但标签仅存储在镜像的元数据部分,并由其他主机程序使用,而不是容器中的程序。例如,如果我们在镜像中附加了维护者信息,形式为LABEL maintainer=johndoe@example.com,则可以通过-f(--filter)标志在查询中筛选带有此标签的镜像:docker images --filter label=maintainer=johndoe@example.com。 -
EXPOSE <port> [<port> ...]:此指令与docker run/create中使用的--expose标志相同,暴露由结果镜像创建的容器中的端口。 -
USER <name|uid>[:<group|gid>]:USER指令切换到指定的用户,以执行随后的指令,包括CMD或ENTRYPOINT中的指令。但是,如果镜像中不存在该用户,则无法正常工作。如果要使用不存在的用户运行指令,必须在使用USER指令之前运行adduser。 -
WORKDIR <path>:此指令将工作目录设置为指定的路径。使用ENV设置的环境变量会在该路径上生效。如果路径不存在,系统会自动创建。它的工作方式类似于Dockerfile中的cd,可以接受相对路径和绝对路径,并且可以多次使用。如果在绝对路径后面跟着相对路径,则结果将相对于前一个路径。
WORKDIR /usr
WORKDIR src
WORKDIR app
RUN pwd
# run docker build
...
---> Running in 73aff3ae46ac
/usr/src/app
COPY:
COPY [--chown=<user>:<group>] <src> ... <dest>
COPY [--chown=<user>:<group>] ["<src>", ..., "<dest>"]
该指令将源文件复制到构建容器中的文件或目录中。源文件和目标路径可以是文件或目录。源路径必须位于上下文路径内,且不能被.dockerignore排除,因为只有这些文件会被发送到 Docker 守护进程。第二种形式适用于路径包含空格的情况。--chown标志使我们可以在运行时设置文件的所有者,而无需在容器内运行额外的chown步骤。它还接受数字的用户 ID 和组 ID:
ADD:
ADD [--chown=<user>:<group>] <src > ... <dest>
ADD [--chown=<user>:<group>] ["<src>", ..., "<dest>"]
ADD在功能上与COPY非常相似:它将文件移动到镜像中。主要的区别在于,ADD支持从远程地址下载文件并且可以在一行命令中解压容器中的压缩文件。因此,<src>也可以是一个 URL 或压缩文件。如果<src>是 URL,ADD会将其下载并复制到镜像中;如果<src>是被推断为压缩文件,ADD会将其解压到<dest>路径中:
VOLUME:
VOLUME mount_point_1 mount_point_2 ...
VOLUME ["mount point 1", "mount point 2", ...]
VOLUME指令在给定的挂载点创建数据卷。一旦在构建时声明,该数据卷在随后的指令中所做的任何更改都不会持久化。此外,由于可移植性问题,在Dockerfile或docker build中挂载主机目录不可行:无法保证指定的路径在主机上存在。这两种语法形式的效果是相同的,它们仅在语法解析上有所不同。第二种形式是 JSON 数组,因此像\这样的字符应该被转义。
ONBUILD [Other directives]:ONBUILD允许您将一些指令推迟到后续构建中的派生镜像中执行。例如,假设我们有以下两个 Dockerfile:
--- baseimg.dck ---
FROM alpine
RUN apk add --no-cache git make
WORKDIR /usr/src/app
ONBUILD COPY . /usr/src/app/
ONBUILD RUN git submodule init \
&& git submodule update \
&& make
--- appimg.dck ---
FROM baseimg
EXPOSE 80
CMD ["/usr/src/app/entry"]
当运行docker build时,指令将按以下顺序评估:
$ docker build -t baseimg -f baseimg.dck .
---
FROM alpine
RUN apk add --no-cache git make
WORKDIR /usr/src/app
---
$ docker build -t appimg -f appimg.dck .
---
COPY . /usr/src/app/
RUN git submodule init \
&& git submodule update \
&& make
EXPOSE 80
CMD ["/usr/src/app/entry"]
组织Dockerfile
尽管编写Dockerfile基本上与编写构建脚本相同,但还有一些因素我们应考虑,以构建高效、安全和稳定的镜像。此外,Dockerfile本身也是一个文档。保持其可读性使得它更易于管理。
假设我们有一个应用堆栈,其中包括应用程序代码、数据库和缓存。我们堆栈的初始Dockerfile可能如下所示:
FROM ubuntu
ADD . /proj
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y redis-server python python-pip mysql-server
ADD /proj/db/my.cnf /etc/mysql/my.cnf
ADD /proj/db/redis.conf /etc/redis/redis.conf
ADD https://example.com/otherteam/dep.tgz /tmp/
RUN -zxf /tmp/dep.tgz -C /usr/src
RUN pip install -r /proj/app/requirements.txt
RUN cd /proj/app ; python setup.py
CMD /proj/start-all-service.sh
第一个建议是确保一个容器专注于一件事情,这样我们的系统就能更透明,因为它帮助我们澄清系统中组件之间的边界。此外,我们不鼓励打包不必要的软件包,因为它会增加镜像的大小,这可能会减慢构建、分发和启动镜像所需的时间。我们将在起初的Dockerfile中移除mysql和redis的安装和配置。接下来,使用ADD .将代码移到容器中,这意味着我们很可能会将整个代码库移入容器中。通常情况下,有许多与应用程序无直接关系的文件,包括 VCS 文件、CI 服务器配置甚至构建缓存,我们可能不希望将它们打包到镜像中。因此,建议使用.dockerignore来过滤掉这些文件。最后,在一般情况下,优先使用COPY而不是ADD,除非我们希望在一步中提取一个文件。这是因为使用COPY时更容易预测结果。现在我们的Dockerfile更简单了,如下面的代码片段所示:
FROM ubuntu
ADD proj/app /app
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y python python-pip
ADD https://example.com/otherteam/dep.tgz /tmp/
RUN tar -zxf /tmp/dep.tgz -C /usr/src
RUN pip install -r /app/requirements.txt
RUN cd /app ; python setup.py
CMD python app.py
在构建镜像时,Docker 引擎会尽可能地重用缓存层,这显著减少了构建时间。在我们的Dockerfile中,如果要安装的任何软件包需要更新,我们就必须进行所有更新和依赖安装过程。为了从构建缓存中受益,我们将根据一个经验法则重新排序指令:先运行较少频繁的指令。
此外,正如我们之前所描述的,对容器文件系统所做的任何更改都会生成一个新的镜像层。更具体来说,ADD、RUN和COPY会创建层。即使我们在后续的层中删除了某些文件,这些文件仍然占用镜像层,因为它们仍然存在于中间层中。因此,我们的下一步是通过将多个RUN指令压缩到一起并在RUN的最后清理未使用的文件,来最小化镜像层。此外,为了保持Dockerfile的可读性,我们倾向于使用行续符号\来格式化压缩后的RUN指令。尽管ADD可以从远程位置获取文件到镜像中,但这仍然不是一个好主意,因为这仍然会占用一个层来存储下载的文件。使用RUN和wget/curl下载文件更为常见。
除了处理 Docker 的构建机制外,我们还希望编写一个可维护的Dockerfile,使其更加清晰、可预测和稳定。以下是一些建议:
-
使用
WORKDIR代替内联的cd,并为WORKDIR指定绝对路径 -
明确暴露所需的端口
-
为基础镜像指定标签
-
将软件包逐行分开并排序
-
使用
exec形式来启动应用程序
前四个建议非常直接,旨在消除歧义。最后一个建议涉及到应用程序的终止方式。当 Docker 守护进程向正在运行的容器发送停止请求时,主进程(PID 1)将接收到停止信号(SIGTERM)。如果在一定时间内该进程没有停止,Docker 守护进程将发送另一个信号(SIGKILL)来强制杀死容器。exec形式和 shell 形式在这里有所不同。在 shell 形式中,PID 1进程是/bin/sh -c,而不是应用程序本身。此外,不同的 shell 处理信号的方式不同。有些会将停止信号转发给子进程,而有些则不会。Alpine Linux 中的 shell 就不会转发信号。因此,为了正确停止并清理我们的应用程序,推荐使用exec形式。
结合这些原则,我们得到了以下的Dockerfile:
FROM ubuntu:18.04
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
curl \
python3.6 \
python-pip=9.* \
&& curl -SL https://example.com/otherteam/dep.tgz \
| tar -zxC /usr/src \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["python"]
CMD ["entry.py"]
EXPOSE 5000
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /app
我们可以遵循其他实践来优化我们的Dockerfile,包括使用专用的、更小的基础镜像,而不是通用的发行版,使用非root用户以提高安全性,以及在RUN命令中删除不必要的文件。
多阶段构建
到目前为止我们讨论的原则,都是关于如何加快构建速度、如何在保持 Dockerfile 可维护性的同时减少最终镜像的体积。与其努力优化一个 Dockerfile,不如写一个用来构建所需制品的 Dockerfile,然后将它们移到另一个仅包含运行时依赖的镜像中,这样能更容易理清不同的逻辑。在构建阶段,我们可以不必太关心减少层数,以便高效地重用构建缓存;而在发布镜像时,我们可以遵循之前推荐的技巧来保持镜像干净且小巧。在 Docker CE 17.05 之前,我们需要写两个 Dockerfile 来实现这种构建模式。现在,Docker 已经内建支持在一个 Dockerfile 中定义不同的阶段。
以 golang 构建为例:这需要很多依赖和一个编译器,但最终的产物可能只是一个单一的二进制文件。我们来看下面的例子:
FROM golang:1.11 AS builder
ARG GOOS=linux
ARG GOARCH=amd64
ARG CGO_ENABLED=0
WORKDIR /go/src/app
COPY main.go .
RUN go get .
RUN go build -a -tags netgo -ldflags '-w -s -extldflags "-static"'
FROM scratch
COPY --from=builder /go/src/app/app .
ENTRYPOINT ["/app"]
CMD ["--help"]
不同阶段的分隔符是 FROM 指令,我们可以在镜像名称后使用 AS 关键字为阶段命名。在 builder 阶段,Docker 启动一个 golang 基础镜像,然后像往常一样构建目标二进制文件。之后,在第二次构建中,Docker 使用 --from=[stage name|image name] 从 builder 容器复制二进制文件到一个 scratch 镜像——一个完全空白的镜像名称。由于结果镜像中只有一个二进制文件和一个层,它的体积比 builder 镜像小得多。
阶段的数量不限于两个,COPY 指令的源可以是之前定义的阶段或构建的镜像。ARG 指令作用于 FROM,这也是唯一一个可以写在 FROM 指令之前的例外,因为它们属于不同的阶段。为了使用它,ARG 指令必须在 FROM 之前声明,如下所示:
ARG TAGS=latest
FROM ubuntu:$TAGS ...
多容器编排
随着我们将越来越多的应用程序打包进独立的容器中,我们很快就会意识到,需要一种工具帮助我们同时管理多个容器。在这一部分,我们将从启动单个容器逐步过渡到编排多个容器。
堆积容器
现代系统通常作为由多个组件构成的堆栈来构建,这些组件分布在网络上,比如应用服务器、缓存、数据库和消息队列。每个组件也是一个自包含的系统,拥有许多子组件。而且,微服务的兴起进一步增加了这些系统间错综复杂关系的复杂性。因此,尽管容器技术在部署任务上为我们提供了一定的帮助,但在一个系统中协调各个组件仍然是一个难题。
假设我们有一个简单的应用程序叫做kiosk,它连接到redis来管理我们当前拥有的票据数量。一旦有票据被售出,它就会通过redis频道发布一个事件。记录器订阅了这个redis频道,并在收到任何事件时,将时间戳日志写入 MySQL 数据库。
对于kiosk和recorder,你可以在这里找到代码以及它们的 Dockerfile:github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter2。架构如下:

我们知道如何单独启动这些容器并将它们连接在一起。基于之前讨论的内容,我们将首先创建一个桥接网络,并在其中运行容器:
$ docker network create kiosk
$ docker run -d --network-alias lcredis --network=kiosk redis
$ docker run -d --network-alias lmysql -e MYSQL_ROOT_PASSWORD=$MYPS \
--network=kiosk mysql:5.7
$ docker run -d -p 5000:5000 \
-e REDIS_HOST=lcredis --network=kiosk kiosk-example
$ docker run -d -e REDIS_HOST=lcredis -e MYSQL_HOST=lmysql \
-e MYSQL_ROOT_PASSWORD=$MYPS -e MYSQL_USER=root \
--network=kiosk recorder-example
因为我们的kiosk和recorder比数据库轻量,所以我们的应用程序很可能会比数据库先启动。在这种情况下,如果有任何连接请求更改数据库或 Redis,kiosk可能会失败。换句话说,我们必须在启动脚本中考虑启动顺序。我们还必须处理诸如如何应对随机组件崩溃、如何管理变量、如何扩展某些组件以及如何管理每个运动部件的状态等问题。
Docker compose 概述
Docker compose 是一个让我们轻松运行多个容器的工具。它是 Docker CE 发行版中的内置工具。它所做的只是读取docker-compose.yml(或.yaml)文件以运行定义的容器。docker-compose文件是基于 YAML 的模板,通常如下所示:
version: '3'
services:
hello-world:
image: hello-world
启动它相当简单:将模板保存为docker-compose.yml,然后使用docker-compose up命令来启动它:
$ docker-compose up
Creating network "user_default" with the default driver
Pulling hello-world (hello-world:)...
...
Creating user_hello-world_1 ... done
Attaching to user_hello-world_1
hello-world_1 |
hello-world_1 | Hello from Docker!
hello-world_1 | This message shows that your installation appears to be working correctly.
...
user_hello-world_1 exited with code 0
让我们来看一下当执行up命令时,docker-compose做了什么。
Docker compose 基本上是多个 Docker 功能的组合。例如,docker build的对应命令是docker-compose build;前者构建一个 Docker 镜像,后者则构建docker-compose.yml中列出的 Docker 镜像。请记住,docker-compose run命令并不对应于docker run;它实际上用于从docker-compose.yml中的配置运行特定的容器。实际上,与docker run最接近的命令是docker-compose up。
docker-compose.yml文件包含了不同的卷、网络和服务配置。应该有一个版本定义来指明应使用哪个版本的docker-compose格式。通过对模板结构的理解,我们可以很清楚地看到之前的hello-world示例是做了什么;它创建了一个名为hello-world的服务,使用hello-world:latest镜像。
由于没有定义网络,docker-compose 将创建一个新的网络并使用默认驱动程序,将服务连接到该网络,如示例输出开始部分所示。
容器的网络名称将是服务的名称。你可能会注意到,在控制台中显示的名称与 docker-compose.yml 中的原始名称稍有不同。这是因为 Docker compose 尝试避免容器名称之间的冲突。因此,Docker compose 会使用其生成的名称运行容器,并用服务名称创建一个网络别名。在这个示例中,hello-world 和 user_hello-world_1 都能在同一网络中的其他容器内解析。
Docker compose 是在单台机器上运行多个容器的最简便选项,但它并非设计用来跨网络编排容器。其他主要的容器编排引擎,如 Kubernetes、Docker Swarm、Mesos(结合 Marathon 或 Aurora)或 Nomad,是跨多个节点运行容器的更好选择。
组合容器
由于 Docker compose 在许多方面与 Docker 相同,因此通过示例学习如何编写 docker-compose.yml 要比从 docker-compose 语法开始更有效。现在,让我们回到之前查看过的 kiosk-example,从 version 定义和四个 services 开始:
version: '3'
services:
kiosk-example:
recorder-example:
lcredis:
lmysql:
kiosk-example 的 docker run 参数非常简单,包括一个发布端口和一个环境变量。在 Docker compose 部分,我们相应地填写源镜像、发布端口和环境变量。由于 Docker compose 能处理 docker build,如果本地找不到镜像,它可以构建镜像。我们希望利用这一点来减少镜像管理的工作量:
kiosk-example:
image: kiosk-example
build: ./kiosk
ports:
- "5000:5000"
environment:
REDIS_HOST: lcredis
将 recorder-example 和 redis 的 Docker run 以相同方式转换,我们得到了如下模板:
version: '3'
services:
kiosk-example:
image: kiosk-example
build: ./kiosk
ports:
- "5000:5000"
environment:
REDIS_HOST: lcredis
recorder-example:
image: recorder-example
build: ./recorder
environment:
REDIS_HOST: lcredis
MYSQL_HOST: lmysql
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: mysqlpass
lcredis:
image: redis
ports:
- "6379"
对于 MySQL 部分,MySQL 需要一个数据卷来保存其数据及配置。除了 lmysql 部分外,我们还需要在 services 层级添加 volumes 并声明一个名为 mysql-vol 的空映射,以申请一个数据卷:
lmysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: mysqlpass
MYSQL_DATABASE: db
MYSQL_USER: user
MYSQL_PASSWORD: pass
volumes:
- mysql-vol:/var/lib/mysql
ports:
- "3306"
volumes:
mysql-vol: {}
这其中的一个好处是,我们可以通过 depends_on 管理组件之间的启动顺序。它的作用是保持顺序;但它无法检测所依赖的组件是否已准备好。这意味着我们的应用程序仍然可能在数据库准备好之前连接并写入数据库。总的来说,由于我们的程序是一个分布式系统的一部分,包含许多移动组件,因此让它对依赖项的变化具有一定的弹性是一个好主意。
结合之前的所有配置,包括 depends_on,我们得到最终模板,如下所示:
version: '3'
services:
kiosk-example:
image: kiosk-example
build: ./kiosk
ports:
- "5000:5000"
environment:
REDIS_HOST: lcredis
depends_on:
- lcredis
recorder-example:
image: recorder-example
build: ./recorder
environment:
REDIS_HOST: lcredis
MYSQL_HOST: lmysql
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: mysqlpass
depends_on:
- lmysql
- lcredis
lcredis:
image: redis
ports:
- "6379"
lmysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: mysqlpass
MYSQL_DATABASE: db
MYSQL_USER: user
MYSQL_PASSWORD: pass
volumes:
- mysql-vol:/var/lib/mysql
ports:
- "3306"
volumes:
mysql-vol: {}
该文件被放置在项目的 root 文件夹中。相应的文件树如下所示:
├── docker-compose.yml
├── kiosk
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
└── recorder
├── Dockerfile
├── process.py
└── requirements.txt
最后,运行 docker-compose up 来检查一切是否正常。我们可以使用 kiosk 检查每个组件是否已正确链接:
$ curl localhost:5000
Gooday!
Import tickets with "curl -XPOST -F 'value=<int>' /tickets"
Purchase a ticket with "curl -XPOST /buy
Get current tickets with "curl -XGET /tickets"
$ curl -XGET localhost:5000/tickets
0
$ curl -XPOST -F 'value=10' localhost:5000/tickets
SUCCESS
$ curl -XGET localhost:5000/tickets
10
$ curl -XPOST localhost:5000/buy
SUCCESS
$ docker exec chapter2_lmysql_1 mysql -u root -pmysqlpass \
-e "select * from kiosk.sellinglog;"
id ts
1 1536704902204
编写一个 Docker Compose 的模板是如此简单。我们现在能够轻松地在堆栈中运行一个应用程序。
总结
从 Linux 容器和 Docker 工具栈的最基本元素开始,我们探讨了容器化应用程序的方方面面,包括打包和运行 Docker 容器、为基于代码的不可变部署编写Dockerfile,以及使用 Docker Compose 操作多个容器。然而,我们在本章中获得的能力仅限于在同一主机内运行和连接容器,这限制了我们构建更大应用程序的能力。
在第三章,《Kubernetes 入门》中,我们将介绍 Kubernetes,释放容器的力量,突破规模的限制。
第三章:Kubernetes 入门
到目前为止,我们已经了解了容器可以带来的好处,但如果我们需要扩展我们的服务以满足业务需求该怎么办?有没有一种方法可以在不处理繁琐的网络和存储设置的情况下,在多个机器上构建服务?有没有一种简便的方式来管理并推出具有不同服务周期的微服务?这就是 Kubernetes 发挥作用的地方。在本章中,我们将讨论以下概念:
-
理解 Kubernetes
-
Kubernetes 组件
-
Kubernetes 资源及其配置文件
-
如何使用 Kubernetes 启动自助服务应用程序
理解 Kubernetes
Kubernetes 是一个用于跨多个主机管理容器的平台。它为面向容器的应用提供了许多管理功能,如自动扩展、滚动部署、计算资源和存储管理。像容器一样,它设计为可以在任何地方运行,包括裸机、我们的数据中心、公共云,甚至是混合云中。
Kubernetes 满足大多数应用容器操作需求。其亮点包括以下内容:
-
容器部署
-
持久化存储
-
容器健康监控
-
计算资源管理
-
自动扩展
-
通过集群联邦实现高可用性
使用 Kubernetes,我们可以轻松管理容器化应用。例如,通过创建 Deployment,我们可以使用单一命令推出、滚动或回滚选定的容器(第九章,持续交付)。容器被视为临时的。如果我们只有一个主机,我们可以将主机卷挂载到容器中以保持数据。但在集群环境中,容器可能会被调度到集群中的任何主机上运行。那么,如何在不指定主机的情况下挂载卷呢?Kubernetes 卷 和 持久化卷 被引入来解决这个问题(第四章,管理有状态工作负载)。
容器的生命周期可能很短;当它们超出资源限制时,它们可能随时被杀死或停止。我们如何确保我们的服务始终可用,并由一定数量的容器提供服务?在 Kubernetes 中,部署操作确保一定数量的容器组正常运行。Kubernetes 还支持 存活探针,以帮助定义和监控应用程序的健康状况。为了更好地管理资源,我们可以为 Kubernetes 节点定义最大容量和每组容器(也称为 Pods)的资源限制。Kubernetes 调度器将选择满足资源条件的节点来运行容器。我们将在第八章,资源管理与扩展中进一步了解此内容。Kubernetes 还提供了一个可选的水平 Pod 自动缩放功能,可以按核心或自定义指标水平扩展 Pod。Kubernetes 还设计具有 高可用性(HA)。我们可以创建多个主控节点以防止单点故障。
Kubernetes 组件
Kubernetes 包括两个主要组件:
-
主控节点:主控节点是 Kubernetes 的核心;它控制并安排集群中的所有活动。
-
节点:节点是运行我们容器的工作节点
主控节点组件
主控节点包括 API 服务器、控制器管理器、调度器 和 etcd。所有组件可以在不同的主机上运行,并支持集群化。但在本例中,我们将所有组件都运行在同一节点上,如下图所示:

主控节点组件
API 服务器(kube-apiserver)
API 服务器提供 HTTP/HTTPS 服务器,为 Kubernetes 主控节点中的组件提供 RESTful API。例如,我们可以使用 GET 获取资源状态或使用 POST 创建新资源。我们还可以监听资源的更新。API 服务器将对象信息存储到 etcd 中,这是 Kubernetes 的后端数据存储。
控制器管理器(kube-controller-manager)
控制器管理器是一组控制循环,负责监视 API 服务器中的更改,并确保集群处于期望的状态。例如,部署控制器确保整个部署在所需数量的容器上运行。节点控制器在节点宕机时响应并清除 Pod。端点控制器用于创建服务和 Pod 之间的关系。服务账户和令牌控制器用于创建默认账户和 API 访问令牌。
为了适应来自不同云提供商的不同开发速度和发布周期,从 Kubernetes 版本 1.6 开始,特定于云提供商的逻辑从 kube-controller-manager 移动到了云控制器管理器 (cloud-controller-manager)。这在 1.11 版本中升级为 beta 版本。
etcd
etcd 是一个开源分布式键值存储系统(coreos.com/etcd)。Kubernetes 在这里存储所有的 RESTful API 对象。etcd 负责存储和复制数据。
调度器(kube-scheduler)
调度器决定哪些节点是 pod 运行的合适候选节点。它不仅考虑节点的资源容量和资源利用率平衡,还会考虑节点亲和性、污点和容忍度。更多信息请参见第八章,资源管理与扩展。
节点组件
每个节点上都会提供并运行节点组件,这些组件将 pod 的运行时状态报告给 master:

节点组件
Kubelet
Kubelet 是节点中的一个重要进程。它定期向 kube-apiserver 报告节点活动,包括 pod 健康状态、节点健康状态以及存活探针。如前图所示,它通过容器运行时(如 Docker 或 rkt)来运行容器。
代理(kube-proxy)
代理负责 pod 负载均衡器(也称为 service)与 pods 之间的路由,它还提供从外部互联网到服务的路由。有三种代理模式:userspace、iptables 和 ipvs。userspace 模式通过切换内核空间和用户空间带来较大的开销。而 iptables 模式是最新的默认代理模式,它通过修改 Linux 中的 iptables 网络地址转换(NAT: en.wikipedia.org/wiki/Network_address_translation)来实现跨所有容器的 TCP 和 UDP 包路由。IP 虚拟服务器(IPVS)在 Kubernetes 1.11 中正式发布(GA),旨在解决在集群中运行超过 1,000 个服务时的性能下降问题。它运行在主机上并充当负载均衡器,将连接转发到真实服务器。在某些情况下,IPVS 模式会回退到 iptables;有关更多详细信息,请参阅 github.com/kubernetes/kubernetes/tree/master/pkg/proxy/ipvs。
Docker
如第二章《使用容器进行 DevOps》中所述,Docker 是一种容器运行时实现。Kubernetes 使用 Docker 作为默认的容器引擎,也支持其他容器运行时,例如 rkt(coreos.com/rkt/)和 runc(github.com/opencontainers/runc)。
Kubernetes master 与节点之间的交互
正如我们在下图中看到的,客户端使用 kubectl(一个命令行界面)向 API 服务器 发送请求。API 服务器 作为主节点组件之间的中枢,将响应客户端请求,并从 etcd 中推送和拉取对象信息。如果创建了一个新任务,如运行 pod,调度器将决定将任务分配给哪个节点。控制器管理器 监控运行中的任务,并在出现任何不期望的状态时做出响应。
API 服务器 通过 kubelet 获取 pod 的日志:

主节点与节点之间的交互
Kubernetes 入门
在本节中,我们将学习如何设置单节点集群。接着,我们将学习如何通过 Kubernetes 的命令行工具 kubectl 进行交互。我们将通过所有重要的 Kubernetes API 对象及其 YAML 格式的表达式,了解它们作为 kubectl 输入的作用。然后,我们将看到 kubectl 如何向 API 服务器发送请求,以相应地创建所需的对象。
环境准备
首先,需要安装 kubectl。在主要的 Linux 发行版(如 Ubuntu 或 CentOS)中,你可以通过包管理器搜索并安装名为 kubectl 的包。在 macOS 中,我们可以选择使用 Homebrew (brew.sh/) 来安装它。Homebrew 是 macOS 中一个非常实用的包管理工具。我们可以通过执行 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 命令轻松安装 Homebrew。然后,我们可以通过 brew install kubernetes-cli 使用 Homebrew 安装 kubectl。
现在让我们开始配置一个 Kubernetes 集群。最简单的方法是运行 minikube (github.com/kubernetes/minikube),它是一个在单节点本地运行 Kubernetes 的工具。它可以在 Windows、Linux 和 macOS 上运行。在接下来的示例中,我们将在 macOS 上运行。Minikube 将启动一个虚拟机,并安装 Kubernetes。我们随后可以通过 kubectl 与其交互。
请注意,minikube 不适用于生产环境或任何重载环境。由于它是单节点的,存在一些限制。我们将在第十章,Kubernetes on AWS,第十一章,Kubernetes on GCP,以及第十二章,Kubernetes on Azure中学习如何运行真实的集群。
在安装 minikube 之前,我们需要先安装一些依赖项。Minikube 的官方 GitHub 仓库 (github.com/kubernetes/minikube) 列出了不同平台的依赖项和驱动程序。在我们的案例中,我们在 macOS 上使用 VirtualBox (www.virtualbox.org/) 作为驱动程序。你可以自由选择其他驱动程序;请访问上面的 minikube GitHub 链接查找更多选项。
下载并安装 VirtualBox 后,我们就准备好了。我们可以通过 brew cask install minikube 安装 minikube:
// install minikube
# brew cask install minikube
==> Tapping caskroom/cask
==> Linking Binary 'minikube-darwin-amd64' to '/usr/local/bin/minikube'.
...
minikube was successfully installed!
安装 minikube 后,我们可以通过 minikube start 命令启动集群。这将启动一个本地 Kubernetes 集群。在撰写本文时,minikube v0.30.0 支持的默认 Kubernetes 版本是 v.1.12.0。你也可以添加 --kubernetes-version 参数来指定你想运行的特定 Kubernetes 版本。例如,假设我们想运行版本为 v1.12.1 的集群:
// start the cluster (via minikube v0.30.0)
#
Starting local Kubernetes v1.12.1 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Downloading kubeadm v1.12.1
Downloading kubelet v1.12.1
Finished Downloading kubeadm v1.12.1
Finished Downloading kubelet v1.12.1
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.
这将启动一个名为 minikube 的虚拟机,并通过 kubeadm(一个 Kubernetes 配置工具)设置集群。它还将设置 kubeconfig,这是一个配置文件,用于定义集群的上下文和身份验证设置。通过 kubeconfig,我们能够通过 kubectl 命令切换到不同的集群。我们可以使用 kubectl config view 命令查看 kubeconfig 中的当前设置:
apiVersion: v1
# cluster and certificate information
clusters:
- cluster:
certificate-authority: /Users/chloelee/.minikube/ca.crt
server: https://192.168.99.100:8443
name: minikube
# context is the combination of cluster, user and namespace
contexts:
- context:
cluster: minikube
user: minikube
name: minikube
current-context: minikube
kind: Config
preferences: {}
# user information
users:
- name: minikube
user:
client-certificate: /Users/chloelee/.minikube/client.crt
client-key: /Users/chloelee/.minikube/client.key
这里,我们当前使用的是minikube上下文。上下文是身份验证信息和集群连接信息的组合。如果你有多个上下文,可以使用kubectl config use-context $context 强制切换上下文。
kubectl
kubectl 是用于管理 Kubernetes 集群的命令行工具。kubectl 最常见的用法是检查集群的 version:
// check Kubernetes version
# kubectl version
Client Version: version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.0", GitCommit:"0ed33881dc4355495f623c6f22e7dd0b7632b7c0", GitTreeState:"clean", BuildDate:"2018-10-01T00:59:42Z", GoVersion:"go1.11", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.3", GitCommit:"a4529464e4629c21224b3d52edfe0ea91b072862", GitTreeState:"clean", BuildDate:"2018-09-09T17:53:03Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
然后,我们知道我们的服务器版本是最新的,即撰写本文时的最新版本——版本 1.12.0。kubectl 的一般语法如下:
kubectl [command] [type] [name] [flags]
command 表示你想执行的操作。如果你在终端输入 kubectl help,它会显示支持的命令。type 表示资源类型。我们将在下一节学习主要的资源类型。name 是我们如何命名资源的。最好始终使用清晰且具有信息性的名称。对于 flags,如果你输入 kubectl options,stdout 将显示你可以传递的所有标志。
我们可以随时添加 --help 来获取关于特定命令的更详细信息,如以下示例所示:
// show detailed info for logs command
kubectl logs --help
Print the logs for a container in a pod or specified resource. If the pod has only one container, the container name is optional.
Aliases:
logs, log
Examples:
# Return snapshot logs from pod nginx with only one container
kubectl logs nginx
# Return snapshot logs for the pods defined by label app=nginx
kubectl logs -lapp=nginx ...
Options
...
然后,我们可以在 kubectl logs 命令中获得示例和支持的选项。
Kubernetes 资源
Kubernetes 对象是集群中的条目,存储在 etcd 中。它们表示集群的期望状态。当我们创建一个对象时,我们通过 kubectl 或 RESTful API 向 API 服务器发送请求。API 服务器会检查请求是否有效,将状态存储在 etcd 中,并与其他主控组件交互以确保对象的存在。Kubernetes 使用命名空间将对象虚拟隔离,因此我们可以为不同的团队、用途、项目或环境创建不同的命名空间。每个对象都有自己的名称和唯一 ID。Kubernetes 还支持标签和注解,让我们可以给对象打标签。特别是标签,可以用来将对象分组。
Kubernetes 对象
对象的 spec 描述了 Kubernetes 对象的期望状态。大多数时候,我们编写对象的 spec 并通过 kubectl 发送给 API 服务器。Kubernetes 会尽力实现该期望的 state 并 update 对象的状态。
对象的 spec 可以用 YAML (www.yaml.org/) 或 JSON (www.json.org/) 编写。在 Kubernetes 中,YAML 更为常见。我们将在本书的其余部分使用 YAML 来编写对象 spec。以下代码块展示了一个 YAML 格式的 spec 片段:
apiVersion: Kubernetes API version
kind: object type
metadata:
spec metadata, i.e. namespace, name, labels and annotations
spec:
the spec of Kubernetes object
命名空间
Kubernetes 命名空间允许我们实现多个虚拟集群的隔离。不同命名空间中的对象互相不可见。当不同的团队或项目共享同一个集群时,这非常有用。大多数资源都属于某个命名空间(这些被称为命名空间资源);然而,一些通用资源,如节点或命名空间本身,并不属于任何命名空间。Kubernetes 有三个命名空间:
-
default -
kube-system -
kube-public
如果我们没有明确为命名空间资源指定命名空间,它将位于当前上下文的命名空间中。如果我们从未添加新命名空间,则将使用默认命名空间。
Kube-system 命名空间由 Kubernetes 系统创建的对象使用,如 addon,这些是实现集群功能的 pods 或服务,如仪表盘。Kube-public 命名空间是在 Kubernetes 版本 1.6 中引入的,用于由 beta 控制器管理器(BootstrapSigner:kubernetes.io/docs/admin/bootstrap-tokens)使用,将签名的集群位置信息放入 kube-public 命名空间。此信息可以被认证或未认证的用户查看。
在接下来的部分中,所有命名空间资源都位于默认命名空间中。命名空间对于资源管理和角色也非常重要。我们将在第八章中提供更多信息,资源管理与扩展。
让我们看看如何创建一个命名空间。命名空间是一个 Kubernetes 对象。我们可以指定类型为命名空间,就像其他对象一样。以下是如何创建一个名为 project1 的命名空间的示例:
// configuration file of namespace
# cat 3-2-1_ns1.yml
apiVersion: v1
kind: Namespace
metadata:
name: project1 // create namespace for project1
# kubectl create -f 3-2-1_ns.yaml
namespace/project1 created // list namespace, the abbreviation of namespaces is ns. We could use `kubectl get ns` to list it as well.
# kubectl get namespaces
NAME STATUS AGE
default Active 1d
kube-public Active 1d
kube-system Active 1d
project1 Active 11s
现在我们尝试通过在 project1 命名空间中进行部署来启动两个 nginx 容器:
// run a nginx deployment in project1 ns
# kubectl run nginx --image=nginx:1.12.0 --replicas=2 --port=80 --namespace=project1
deployment.apps/nginx created
当我们通过 kubectl get pods 列出 pods 时,集群中将不会显示任何内容。这是因为 Kubernetes 使用当前上下文来决定哪个命名空间是当前的。如果我们没有在上下文或 kubectl 命令行中明确指定命名空间,默认将使用 default 命名空间:
// We'll see the Pods if we explicitly specify --namespace
# kubectl get pods --namespace=project1
NAME READY STATUS RESTARTS AGE
nginx-8cdc99758-btgzj 1/1 Running 0 22s
nginx-8cdc99758-xpk58 1/1 Running 0 22s
你可以使用 --namespace <namespace_name>,--namespace=<namespace_name>,-n <namespace_name> 或 -n=<namespace_name> 来指定命令的命名空间。要列出跨命名空间的资源,使用 --all-namespaces 参数。
另一种做法是将当前上下文更改为指向所需的命名空间,而不是default命名空间。
名称
Kubernetes 中的每个对象都有自己独特的名称,在相同的命名空间内是唯一的。Kubernetes 将对象名称作为 API 服务器资源 URL 的一部分,因此它必须是小写字母、数字、短横线和点的组合,并且长度不得超过 254 个字符。除了对象名称,Kubernetes 还为每个对象分配了一个唯一 ID(UID),用于区分历史上类似实体的发生。
标签和选择器
标签是附加到对象上的键/值对集合。它们旨在提供有关对象的有意义的标识信息。常见的用途包括指示微服务名称、层级、环境和软件版本。用户可以定义有意义的标签,后续可以与选择器一起使用。对象spec中标签的语法如下:
labels:
$key1: $value1
$key2: $value2
与标签一起使用,标签选择器用于过滤对象集合。多个要求用逗号分隔,将通过AND逻辑运算符连接。过滤有两种方式:
-
基于相等的要求
-
基于集合的要求
基于相等的要求支持以下运算符:=、==和!=。以以下图示为例:如果选择器是chapter=2, version!=0.1,则结果为对象 C。如果要求是version=0.1,则结果为对象 A和对象 B:

选择器示例
如果我们在支持的对象spec中编写要求,它将如下所示:
selector:
$key1: $value1
基于集合的要求支持in、notin和exists(仅适用于key)。例如,如果要求是chapter in (3, 4), version,则返回对象 A。如果要求是version notin (0.2), !author_info,则结果将是对象 A和对象 B。以下示例展示了使用基于集合要求的spec对象:
selector:
matchLabels:
$key1: $value1
matchExpressions:
- {key: $key2, operator: In, values: [$value1, $value2]}
matchLabels和matchExpressions的要求是结合在一起的。这意味着过滤的对象必须同时满足两个要求。
注解
注解是一组用户指定的键值对,用于指定非标识性的元数据。注解的作用类似于普通的标签,例如,用户可以在注解中添加时间戳、提交哈希或构建号。某些kubectl命令支持--record选项,用于记录对对象进行更改的命令。注解的另一个用例是存储配置信息,例如 Kubernetes 部署(kubernetes.io/docs/concepts/workloads/controllers/deployment)或关键附加组件 Pod(coreos.com/kubernetes/docs/latest/deploy-addons.html)。注解的语法如下:
annotations:
$key1: $value1
$key2: $value2
命名空间、名称、标签和注释位于对象spec的元数据部分。选择器位于支持选择器的资源的spec部分,例如 pod、service、ReplicaSet 和 deployment。
Pods
Pod 是 Kubernetes 中最小的可部署单元。它可以包含一个或多个容器。大多数情况下,我们每个 pod 只需要一个容器。在一些特殊情况下,多个容器可能会被包含在同一个 pod 中,例如 sidecar 容器(blog.kubernetes.io/2015/06/the-distributed-system-toolkit-patterns.html)。同一个 pod 中的容器共享一个上下文,在同一节点上运行,共享网络命名空间和共享卷。Pod 的设计也是“易死”的。当 pod 因某种原因死掉时,比如被 Kubernetes 控制器因为资源不足而杀死,它不会自行恢复。相反,Kubernetes 使用控制器为我们创建并管理 pod 的期望状态。
我们可以使用kubectl explain <resource>通过命令行获取资源的详细描述。这将显示该资源支持的字段:
// get detailed info for `pods`
# kubectl explain pods
KIND: Pod
VERSION: v1
DESCRIPTION:
Pod is a collection of containers that can run on a host. This resource is
created by clients and scheduled onto hosts.
FIELDS:
apiVersion <string>
APIVersion defines the versioned schema of this representation of an
object. Servers should convert recognized schemas to the latest internal
value, and may reject unrecognized values. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#resources
kind <string>
Kind is a string value representing the REST resource this object
represents. Servers may infer this from the endpoint the client submits
requests to. Cannot be updated. In CamelCase. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds
metadata <Object>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata
spec <Object>
Specification of the desired behavior of the pod. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status
status <Object>
Most recently observed status of the pod. This data may not be up to date.
Populated by the system. Read-only. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status
在接下来的示例中,我们将展示如何在一个 pod 中创建两个容器,并演示它们如何互相访问。请注意,这既不是一个有意义的也不是经典的 sidecar 模式示例。它只是展示了我们如何在同一个 pod 内访问其他容器的一个例子:
// an example for creating co-located and co-scheduled container by pod
# cat 3-2-1_pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
containers:
- name: web
image: nginx
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do curl http://localhost:80/; sleep 10; done"]
以下图示展示了Pod中容器之间的关系。它们共享相同的网络命名空间:

Pod 内的容器可以通过 localhost 访问
这个 spec 将创建两个容器,web和centos。Web 是一个nginx容器(hub.docker.com/_/nginx/)。容器端口80默认被暴露。由于centos与 nginx 共享相同的上下文,当在http://localhost:80/中使用curl时,它应该能够访问 nginx。
接下来,使用kubectl create命令来启动 pod。-f参数允许我们将配置文件传递给kubectl命令,并创建文件中指定的所需资源:
// create the resource by `kubectl create` - Create a resource by filename or stdin
# kubectl create -f 3-2-1_pod.yaml
pod "example" created
如果在创建资源时,我们在kubectl命令的末尾添加--record=true,Kubernetes 会在创建或更新资源时记录最新的命令。因此,我们不会忘记哪些资源是由哪个 spec 创建的。
我们可以使用kubectl get <resource>命令来获取对象的当前状态。在这个例子中,我们使用kubectl get pods命令:
// get the current running pods
# kubectl get pods
NAME READY STATUS RESTARTS AGE
example 0/2 ContainerCreating 0 1s
添加--namespace=$namespace_name允许我们访问不同命名空间中的对象。以下是如何检查kube-system命名空间中 pods 的示例,该命名空间用于系统类型的 pods:
**// 列出 kube-system 命名空间中的 pods**
**# kubectl get pods --namespace=kube-system**
**NAME READY STATUS RESTARTS AGE**
**coredns-99b9bb8bd-p2dvw 1/1 正在运行 0 1 分钟**
**etcd-minikube 1/1 正在运行 0 47 秒**
**kube-addon-manager-minikube 1/1 正在运行 0 13 秒**
**kube-apiserver-minikube 1/1 正在运行 0 38 秒**
**kube-controller-manager-minikube 1/1 正在运行 0 32 秒**
**kube-proxy-pvww2 1/1 正在运行 0 1 分钟**
**kube-scheduler-minikube 1/1 正在运行 0 26 秒**
**kubernetes-dashboard-7db4dc666b-f8b2w 1/1 正在运行 0 1 分钟**
**storage-provisioner 1/1 正在运行 0 1 分钟**
我们示例中的 pod 状态是ContainerCreating。在此阶段,Kubernetes 已接受请求,并正在尝试调度 pod 并拉取镜像。当前没有容器在运行。
大多数对象都有简短的名称,这在我们使用kubectl get <object>列出它们的状态时非常有用。例如,pod 可以叫做po,服务可以叫做svc,部署可以叫做deploy。输入kubectl get可以查看更多信息。或者,kubectl api-resources命令可以列出所有资源及其简短名称和属性。
等待一会儿后,我们可以再次获取状态:
// get the current running pods
# kubectl get pods
NAME READY STATUS RESTARTS AGE
example 2/2 Running 0 3s
我们可以看到两个容器当前正在运行,运行时间为三秒。使用kubectl logs <pod_name> -c <container_name>可以获取该容器的stdout,这类似于docker logs <container_name>:
// get stdout for centos
# kubectl logs example -c centos
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
pod 中的centos与 nginx 通过 localhost 共享相同的网络。Kubernetes 会与 pod 一起创建一个网络容器。网络容器的一个功能是转发 pod 内容器之间的流量。我们将在第六章中了解更多内容,Kubernetes 网络。
如果我们在 pod 规格中指定标签,我们可以使用kubectl get pods -l <requirement>命令获取符合要求的 pod,例如,kubectl get pods -l 'tier in (frontend, backend)'。此外,如果我们使用kubectl pods -o wide,它会列出哪些 pod 在何种节点上运行。
我们可以使用kubectl describe <resource> <resource_name>来获取资源的详细信息:
// get detailed information for a pod
# kubectl describe pods example
Name: example
Namespace: default
Priority: 0
PriorityClassName: <none>
Node: minikube/10.0.2.15
Start Time: Sun, 07 Oct 2018 15:15:36 -0400
Labels: <none>
Annotations: <none>
Status: Running
IP: 172.17.0.4
Containers: ...
此时,我们知道这个 pod 运行在哪个节点上。在minikube中,我们只有一个节点,因此它没有什么区别。在真实的集群环境中,知道 pod 运行在哪个节点上对于故障排除非常有用。我们还没有为它关联任何标签、注解或控制器:
web:
Container ID: docker://d8284e14942cbe0b8a91f78afc132e09c0b522e8a311e44f6a9a60ac2ca7103a
Image: nginx
Image ID: docker-pullable://nginx@sha256:9ad0746d8f2ea6df3a17ba89eca40b48c47066dfab55a75e08e2b70fc80d929e
Port: <none>
Host Port: <none>
State: Running
Started: Sun, 07 Oct 2018 15:15:50 -0400
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-bm6vn (ro)
在容器部分,我们将看到这个 pod 包含两个容器。我们可以看到它们的状态、源镜像、端口映射和重启次数:
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
一个 pod 具有 PodStatus,其中包含以 PodConditions 数组表示的映射。PodConditions 的可能类型有 PodScheduled、Ready、Initialized、Unschedulable 和 ContainersReady。其值可以是 true、false 或未知。如果 pod 没有按预期创建,PodStatus 将简要概述哪个部分出现了故障。在前面的示例中,我们在每个阶段成功启动了 pod,且没有出现任何错误:
Volumes:
default-token-bm6vn:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-bm6vn
Optional: false
一个 pod 与服务账户相关联,服务账户为运行该 pod 的进程提供身份。它由服务账户和 API 服务器中的令牌控制器进行管理。
它将在 pod 中的每个容器下挂载一个只读卷,路径为 /var/run/secrets/kubernetes.io/serviceaccount,该卷包含一个用于 API 访问的令牌。Kubernetes 创建了一个默认的服务账户。我们可以使用 kubectl get serviceaccounts 命令列出服务账户:
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
我们还没有为这个 pod 分配任何选择器。容忍度用于限制节点可以使用的 pod 数量。我们将在第八章 资源管理和扩展中进一步学习这一点:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m22s default-scheduler Successfully assigned default/example to minikube
Normal Pulling 2m21s kubelet, minikube pulling image "nginx"
Normal Pulled 2m8s kubelet, minikube Successfully pulled image "nginx"
Normal Created 2m8s kubelet, minikube Created container
Normal Started 2m8s kubelet, minikube Started container
Normal Pulling 2m8s kubelet, minikube pulling image "centos"
Normal Pulled 93s kubelet, minikube Successfully pulled image "centos"
Normal Created 92s kubelet, minikube Created container
Normal Started 92s kubelet, minikube Started container
通过查看事件,我们可以识别 Kubernetes 运行节点所需的步骤。首先,调度器将任务分配给一个节点,这里叫做 minikube。然后,kubelet 开始拉取第一个镜像并相应地创建一个容器。之后,kubelet 拉取第二个容器并启动容器。
ReplicaSet
pod 并不是自愈的。当 pod 遇到故障时,它不会自动恢复。这时,ReplicaSet(RS)发挥作用。ReplicaSet 确保集群中始终有指定数量的副本 pod 正在运行。如果一个 pod 因为任何原因崩溃,ReplicaSet 会发送请求来启动一个新的 pod。
ReplicaSet 类似于旧版本 Kubernetes 中使用的 ReplicationController(RC)。与 ReplicaSet 使用基于集合的选择器要求不同,ReplicationController 使用基于等式的选择器要求。现在,ReplicationController 已被完全替代为 ReplicaSet。
让我们看看 ReplicaSet 是如何工作的:

具有期望数量为 2 的 ReplicaSet
假设我们想要创建一个 ReplicaSet 对象,期望数量为 2。这意味着我们将始终在服务中拥有两个 pod。在编写 ReplicaSet 的 spec 之前,我们必须首先确定 pod 模板。这类似于 pod 的 spec。在 ReplicaSet 中,标签在元数据部分是必需的。ReplicaSet 使用 pod 选择器来选择它管理的 pod。标签使得 ReplicaSet 能够区分是否所有与选择器匹配的 pod 都在正常运行。
在这个示例中,我们将创建两个 pod,每个 pod 都带有 project、service 和 version 标签,如前图所示:
// an example for RS spec
# cat 3-2-2_rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx
spec:
replicas: 2
selector:
matchLabels:
project: chapter3
matchExpressions:
- {key: version, operator: In, values: ["0.1", "0.2"]}
template:
metadata:
name: nginx
labels:
project: chapter3
service: web
version: "0.1"
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
// create the RS
# kubectl create -f 3-2-2_rs.yaml
replicaset.apps/nginx created
然后,我们可以使用 kubectl 来获取当前的 RS 状态:
// get current RSs
# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx 2 2 2 29s
这表明我们期望有两个 Pod,当前有两个 Pod,并且两个 Pod 都已准备就绪。现在我们有多少 Pod 呢?让我们通过kubectl命令检查一下:
// get current running pod
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-l5mdn 1/1 Running 0 11s
nginx-pjjw9 1/1 Running 0 11s
这表明我们有两个 Pod 正在运行。如前所述,ReplicaSet 管理所有匹配选择器的 Pod。如果我们手动创建一个具有相同标签的 Pod,理论上,它应该匹配我们刚刚创建的 RS 的 Pod 选择器。让我们试试看:
// manually create a pod with same labels
# cat 3-2-2_rs_self_created_pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: our-nginx
labels:
project: chapter3
service: web
version: "0.1"
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
// create a pod with same labels manually
# kubectl create -f 3-2-2_rs_self_created_pod.yaml
pod "our-nginx" created
让我们看看它是否正常运行:
// get pod status
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-l5mdn 1/1 Running 0 4m
nginx-pjjw9 1/1 Running 0 4m
our-nginx 0/1 Terminating 0 4s
它已调度,ReplicaSet 捕捉到了这个变化。Pod 的数量变为三个,超过了我们期望的数量。该 Pod 最终被终止:
// get pod status
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-l5mdn 1/1 Running 0 5m
nginx-pjjw9 1/1 Running 0 5m
以下图示展示了我们自创建的 Pod 是如何被驱逐的。标签与 ReplicaSet 匹配,但期望数量为 2。因此,额外的 Pod 被驱逐掉了:

ReplicaSet 确保 Pod 处于期望的状态
如果我们想根据需求进行扩展,可以简单地使用kubectl edit <resource> <resource_name>来更新规格。在这里,我们将副本数量从2更改为5:
// change replica count from 2 to 5, default system editor will pop out.
Change `replicas` number
# kubectl edit rs nginx
replicaset.extensions/nginx edited
让我们检查 RS 的相关信息:
// get RS information
# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx 5 5 5 5m
我们现在有五个 Pod。让我们检查一下 RS 是如何工作的:
// describe RS resource `nginx`
# kubectl describe rs nginx Name: nginx
Namespace: default
Selector: project=chapter3,version in (0.1,0.2)
Labels: project=chapter3
service=web
version=0.1
Annotations: <none>
Replicas: 5 current / 5 desired
Pods Status: 5 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: project=chapter3
service=web
version=0.1
Containers:
nginx:
Image: nginx
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 3m34s replicaset-controller Created pod: nginx-l5mdn
Normal SuccessfulCreate 3m34s replicaset-controller Created pod: nginx-pjjw9
Normal SuccessfulDelete 102s replicaset-controller Deleted pod: our-nginx
Normal SuccessfulCreate 37s replicaset-controller Created pod: nginx-v9trs
Normal SuccessfulCreate 37s replicaset-controller Created pod: nginx-n95mv
Normal SuccessfulCreate 37s replicaset-controller Created pod: nginx-xgdhq
通过描述命令,我们可以了解 RS 的规格和事件。当我们创建nginx RS 时,它根据规格启动了两个容器。然后,我们根据另一种规格手动创建了一个名为our-nginx的 Pod。RS 检测到该 Pod 与其 Pod 选择器匹配。随着数量超过我们期望的数量,它将该 Pod 驱逐掉。然后,我们将副本数扩展到五个。RS 检测到它没有达到我们期望的状态,并启动了三个 Pod 来填补空缺。
如果我们想删除一个 RC,只需使用kubectl命令:kubectl delete <resource> <resource_name>。由于我们手头有配置文件,我们也可以使用kubectl delete -f <configuration_file>来删除文件中列出的资源:
// delete a rc
# kubectl delete rs nginx
replicaset.extensions/nginx deleted
// get pod status
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-pjjw9 0/1 Terminating 0 29m
Deployments
在 Kubernetes 1.2 版本之后,Deployments 是管理和部署软件的最佳基础构件。它们允许我们部署 Pod,执行滚动更新,并回滚 Pod 和 ReplicaSets。我们可以使用 Deployments 声明性地定义期望的软件更新,Deployments 会逐步为我们执行这些操作。
在 Deployments 之前,ReplicationController 和 kubectl rolling-update 是实现软件滚动更新的主要方式。这些方法更加命令式且速度较慢。Deployment 现在是用于管理我们应用程序的主要高级对象。
让我们看看它是如何工作的。在本节中,我们将体验一下如何创建 Deployment,如何进行滚动更新和回滚。第九章,持续交付,提供了更多有关如何将 Deployments 集成到我们的持续交付管道中的实际例子。
首先,我们使用kubectl run命令为我们创建deployment:
// using kubectl run to launch the Pods
# kubectl run nginx --image=nginx:1.12.0 --replicas=2 --port=80
deployment "nginx" created
// check the deployment status
# kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx 2 2 2 2 4h
在 Kubernetes 1.2 之前,kubectl run命令会创建 Pod。
有两个 pod 是由 deployment 部署的:
// check if pods match our desired count
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-2371676037-2brn5 1/1 Running 0 4h
nginx-2371676037-gjfhp 1/1 Running 0 4h
以下是一个关于 Deployments、ReplicaSets 和 pods 之间关系的图示。一般来说,Deployments 管理 ReplicaSets,而 ReplicaSets 管理 pods。请注意,我们不应该直接操作由 Deployments 管理的 ReplicaSets,就像如果 pods 是由 ReplicaSets 管理的,我们也没有理由直接改变它们一样:

Deployments、ReplicaSets 和 pods 之间的关系
如果我们删除其中一个 pod,替换的 pod 会立即被调度并启动。这是因为 Deployments 在后台创建了一个 ReplicaSet,它会确保副本的数量与我们期望的数量一致:
// list replica sets
# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-2371676037 2 2 2 4h
我们也可以通过 kubectl 命令暴露端口以供部署:
// expose port 80 to service port 80
# kubectl expose deployment nginx --port=80 --target-port=80
service "nginx" exposed
// list services
# kubectl get services
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes 10.0.0.1 <none> 443/TCP 3d
nginx 10.0.0.94 <none> 80/TCP 5s
Deployments 也可以通过 spec 来创建。之前通过 kubectl 启动的 Deployments 和 Service 可以转换为以下 spec:
// create deployments by spec
# cat 3-2-3_deployments.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
template:
metadata:
labels:
run: nginx
spec:
containers:
- name: nginx
image: nginx:1.12.0
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: nginx
labels:
run: nginx
spec:
selector:
run: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
// create deployments and service
# kubectl create -f 3-2-3_deployments.yaml
deployment "nginx" created
service "nginx" created
为了执行滚动更新,我们需要添加滚动更新策略。此过程中有三个参数用来控制流程:
| 参数 | 描述 | 默认值 |
|---|---|---|
minReadySeconds |
这是热身时间,表示新创建的 pod 被认为可用的时间。默认情况下,Kubernetes 假设应用程序一旦成功启动即认为其可用。 | 0 |
maxSurge |
这表示在执行滚动更新过程中,允许有多少个 pod 被提升。 | 25% |
maxUnavailable |
这表示在执行滚动更新过程中,允许有多少个 pod 不可用。 | 25% |
minReadySecond 是一个重要的设置。如果我们的应用程序在 pod 启动时并未立即可用,pods 将过快滚动,且没有适当的等待。尽管所有新 pods 都已启动,但应用程序可能仍在热身;这时可能会发生服务中断。以下示例中,我们将在 Deployment.spec 部分添加该配置:
// add to Deployments.spec, save as 3-2-3_deployments_rollingupdate.yaml
minReadySeconds: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
这表示我们允许在任何时刻只有一个 pod 不可用,并且在滚动更新 pod 时每次只能启动一个 pod。进入下一操作之前的热身时间是三秒钟。我们可以使用 kubectl edit deployments nginx(直接编辑)或 kubectl replace -f 3-2-3_deployments_rollingupdate.yaml 来更新策略。
假设我们想要模拟一个新的软件发布,从 nginx 1.12.0 更新到 1.13.1。我们仍然可以使用前面提到的两个命令来更改镜像版本,或者使用 kubectl set image deployment nginx nginx=nginx:1.13.1 来触发更新。如果我们使用 kubectl describe 来查看发生了什么,我们会看到 Deployments 已通过删除/创建 pods 触发了对 ReplicaSets 的滚动更新:
// list rs
# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-596b999b89 2 2 2 2m
// check detailed rs information
# kubectl describe rs nginx-596b999b89
Name: nginx-596b999b89
Namespace: default
Selector: pod-template-hash=1526555645,run=nginx
Labels: pod-template-hash=1526555645
run=nginx
Annotations: deployment.kubernetes.io/desired-replicas: 2
deployment.kubernetes.io/max-replicas: 3
deployment.kubernetes.io/revision: 1
Controlled By: Deployment/nginx
Replicas: 2 current / 2 desired
Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: pod-template-hash=1526555645
run=nginx
Containers:
nginx:
Image: nginx:1.12.0
Port: 80/TCP
Host Port: 0/TCP
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 3m41s replicaset-controller Created pod: nginx-596b999b89-th9rx
Normal SuccessfulCreate 3m41s replicaset-controller Created pod: nginx-596b999b89-2pp7b
以下是滚动更新在 Deployment 中如何工作的图示:

Deployment 示意图
上图展示了一个 Deployment 的示意图。在某一时刻,我们希望的副本数是 2,并且有一个 maxSurge pod。在启动每个新 pod 后,Kubernetes 会等待三秒(minReadySeconds),然后执行下一步操作。
如果我们使用 kubectl set image deployment nginx nginx=nginx:1.12.0 命令回滚到先前的版本 1.12.0,Deployment 将为我们执行回滚。
服务
Kubernetes 中的 Service 是将流量路由到一组逻辑 pods 的抽象层。通过 Service,我们不需要追踪每个 pod 的 IP 地址。Service 通常使用标签选择器来选择它们需要路由到的 pod,在某些情况下,Service 会故意不使用选择器。Service 抽象非常强大,它实现了解耦并使微服务之间的通信成为可能。目前,Kubernetes 的 Service 支持 TCP、UDP 和 SCTP。
Service 不关心我们如何创建 pod。就像 ReplicaSet 一样,它只关心 pod 是否匹配它的标签选择器,因此 pod 可以属于不同的 ReplicaSets:

Service 通过标签选择器映射 pods
在上图中,所有的 pods 都匹配 service 选择器,project=chapter3, service=web,因此 Service 将负责将流量分发到所有这些 pods,而无需明确分配。
有四种类型的 Service:ClusterIP、NodePort、LoadBalancer 和 ExternalName:

LoadBalancer 包括 NodePort 和 ClusterIP 的特性
ClusterIP
ClusterIP 是默认的 Service 类型。它在集群内部 IP 上公开 Service。集群中的 pods 可以通过 IP 地址、环境变量或 DNS 访问 Service。在下面的示例中,我们将学习如何使用本地 Service 环境变量和 DNS 来访问集群中 Service 后面的 pods。
在启动 Service 之前,我们希望创建两个不同版本标签的 RS,具体如下:
// create RS 1 with nginx 1.12.0 version
# cat 3-2-3_rs1.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-1.12
spec:
replicas: 2
selector:
matchLabels:
project: chapter3
service: web
version: "0.1"
template:
metadata:
name: nginx
labels:
project: chapter3
service: web
version: "0.1"
spec:
containers:
- name: nginx
image: nginx:1.12.0
ports:
- containerPort: 80
// create RS 2 with nginx 1.13.1 version
# cat 3-2-3_rs2.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-1.13
spec:
replicas: 2
selector:
matchLabels:
project: chapter3
service: web
version: "0.2"
template:
metadata:
name: nginx
labels:
project: chapter3
service: web
version: "0.2"
spec:
containers:
- name: nginx
image: nginx:1.13.1
ports:
- containerPort: 80
然后,我们可以创建我们的 pod 选择器,目标是 project 和 service 标签:
// simple nginx service
# cat 3-2-3_service.yaml
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
selector:
project: chapter3
service: web
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
// create the RSs
# kubectl create -f 3-2-3_rs1.yaml
replicaset.apps/nginx-1.12 created
# kubectl create -f 3-2-3_rs2.yaml
replicaset.apps/nginx-1.13 created
// create the service
# kubectl create -f 3-2-3_service.yaml
service "nginx-service" created
由于 Service 对象可能会创建 DNS 标签,因此服务名称必须由字母数字字符和连字符组成。标签的开头或结尾不能有连字符。
然后我们可以使用 kubectl describe service <service_name> 来检查 Service 的信息:
// check nginx-service information
# kubectl describe service nginx-service
Name: nginx-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: project=chapter3,service=web
Type: ClusterIP
IP: 10.0.0.188
Port: http 80/TCP
Endpoints: 172.17.0.5:80,172.17.0.6:80,172.17.0.7:80 + 1 more...
Session Affinity: None
Events: <none>
一个 Service 可以暴露多个端口。只需在 service 规范中扩展 .spec.ports 列表。
我们可以看到这是一个 ClusterIP 类型的 Service,它分配的内部 IP 地址是 10.0.0.188。端点显示我们在 Service 后面有四个 IP。通过 kubectl describe pods <pod_name> 命令可以找到 pod 的 IP 地址。Kubernetes 会与 service 对象一起创建一个 endpoints 对象,将流量路由到匹配的 pods。
当服务创建时使用了选择器,Kubernetes 会创建相应的端点条目并保持更新,这将指示服务路由的目标:
// list current endpoints. Nginx-service endpoints are created and pointing to the ip of our 4 nginx pods.
# kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 10.0.2.15:8443 2d
nginx-service 172.17.0.5:80,172.17.0.6:80,172.17.0.7:80 + 1 more... 10s
ClusterIP 可以在集群内定义,尽管大多数情况下我们不会显式使用 IP 地址来访问集群。使用 .spec.clusterIP 可以为我们完成这项工作。
默认情况下,Kubernetes 会为每个服务暴露七个环境变量。在大多数情况下,前两个变量使我们能够使用 kube-dns 插件来进行服务发现:
-
${SVCNAME}_SERVICE_HOST -
${SVCNAME}_SERVICE_PORT -
${SVCNAME}_PORT -
${SVCNAME}_PORT_${PORT}_${PROTOCAL} -
${SVCNAME}_PORT_${PORT}_${PROTOCAL}_PROTO -
${SVCNAME}_PORT_${PORT}_${PROTOCAL}_PORT -
${SVCNAME}_PORT_${PORT}_${PROTOCAL}_ADDR
在下面的示例中,我们将在另一个 pod 中使用 ${SVCNAME}_SERVICE_HOST 来检查是否能够访问我们的 nginx pod:

通过环境变量和 DNS 名称访问 ClusterIP
然后,我们将创建一个名为 clusterip-chk 的 pod 来通过 nginx-service 访问 nginx 容器:
// access nginx service via ${NGINX_SERVICE_SERVICE_HOST}
# cat 3-2-3_clusterip_chk.yaml
apiVersion: v1
kind: Pod
metadata:
name: clusterip-chk
spec:
containers:
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do curl
http://${NGINX_SERVICE_SERVICE_HOST}:80/; sleep 10; done"]
我们可以通过 kubectl logs 命令查看 cluserip-chk pod 的 stdout:
// check stdout, see if we can access nginx pod successfully
# kubectl logs -f clusterip-chk
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 612 100 612 0 0 156k 0 --:--:-- --:--:-- --:--:-- 199k
...
<title>Welcome to nginx!</title>
...
这种抽象层解耦了 pod 之间的通信。Pod 是短暂的。通过 RS 和服务,我们可以构建稳健的服务,而无需担心一个 pod 是否会影响所有微服务。
启用 DNS 服务器后,位于同一集群和命名空间中的 pod 可以通过其 DNS 记录访问服务。
CoreDNS GA 在 Kubernetes 1.11 中引入,现在是 Kubernetes 的默认选项。在此之前,kube-dns 插件负责基于 DNS 的服务发现。
DNS 服务器通过监视 Kubernetes API 为新创建的服务创建 DNS 记录。集群 IP 的 DNS 格式是 $servicename.$namespace,端口是 _$portname_$protocal.$servicename.$namespace。clusterip_chk pod 的规格将与环境变量类似。在我们之前的示例中,将 URL 更改为 http://nginx-service.default:_http_tcp.nginx-service.default/,它们的工作方式应该完全相同。
NodePort
如果服务设置为 NodePort,Kubernetes 将在每个节点的特定端口范围内分配一个端口。任何发送到该端口的流量都会被路由到服务端口。端口号可以由用户指定。如果没有指定,Kubernetes 将在 30,000 到 32,767 之间随机选择一个不会发生冲突的端口。另一方面,如果指定了端口,用户应自行负责管理端口冲突。NodePort 包含 ClusterIP 功能,Kubernetes 会为服务分配一个内部 IP。
在下面的示例中,我们将看到如何创建一个 NodePort 服务并使用它:
// write a nodeport type service
# cat 3-2-3_nodeport.yaml
kind: Service
apiVersion: v1
metadata:
name: nginx-nodeport
spec:
type: NodePort
selector:
project: chapter3
service: web
ports:
- protocol: TCP
port: 80
targetPort: 80
// create a nodeport service
# kubectl create -f 3-2-3_nodeport.yaml
service "nginx-nodeport" created
然后你应该能够通过http://${NODE_IP}:80访问该服务。节点可以是任何节点。kube-proxy会监视服务和端点的任何更新,并相应地更新 iptables 规则(如果使用默认的iptables代理模式)。
如果你使用的是 minikube,可以通过minikube service [-n NAMESPACE] [--url] NAME命令访问服务。在这个例子中,这是minikube service nginx-nodeport。
LoadBalancer
这种类型仅在云服务提供商支持下使用,例如 Amazon Web Services(第十章,Kubernetes on AWS),Google Cloud Platform(第十一章,Kubernetes on GCP),以及 Azure(第十二章,Kubernetes on Azure)。如果我们创建一个 LoadBalancer 服务,Kubernetes 会由云服务提供商为该服务提供负载均衡器。
ExternalName(kube-dns 版本>= 1.7)
有时,我们在云中使用不同的服务。Kubernetes 足够灵活,能够支持混合环境。我们可以使用 ExternalName 为集群中的外部端点创建 CNAME。
没有选择器的服务
服务使用选择器来匹配 Pod 并引导流量。然而,有时你需要实现一个代理作为 Kubernetes 集群与另一个命名空间、另一个集群或外部资源之间的桥梁。在以下示例中,我们将展示如何在集群中实现一个代理来访问www.google.com。这只是一个示例;在你的情况下,代理的来源可能是你数据库的端点或云中的其他资源:

没有选择器的服务如何工作
配置文件与之前的类似,只是没有选择器部分:
// create a service without selectors
# cat 3-2-3_service_wo_selector_srv.yaml
kind: Service
apiVersion: v1
metadata:
name: google-proxy
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
// create service without selectors
# kubectl create -f 3-2-3_service_wo_selector_srv.yaml
service "google-proxy" created
不会创建 Kubernetes 端点,因为没有选择器。Kubernetes 不知道如何路由流量,因为没有选择器可以匹配 Pod。我们必须手动创建端点。
在Endpoints对象中,源地址不能是 DNS 名称,因此我们将使用nslookup从域名查找当前的 Google IP,并将其添加到Endpoints.subsets.addresses.ip:
// get an IP from google.com
# nslookup www.google.com
Server: 192.168.1.1
Address: 192.168.1.1#53
Non-authoritative answer:
Name: google.com
Address: 172.217.0.238
// create endpoints for the ip from google.com
# cat 3-2-3_service_wo_selector_endpoints.yaml
kind: Endpoints
apiVersion: v1
metadata:
name: google-proxy
subsets:
- addresses:
- ip: 172.217.0.238
ports:
- port: 80
// create Endpoints
# kubectl create -f 3-2-3_service_wo_selector_endpoints.yaml
endpoints "google-proxy" created
让我们在集群中创建另一个 Pod 来访问我们的 Google 代理:
// pod for accessing google proxy
# cat 3-2-3_proxy-chk.yaml
apiVersion: v1
kind: Pod
metadata:
name: proxy-chk
spec:
containers:
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do curl -L http://${GOOGLE_PROXY_SERVICE_HOST}:80/; sleep 10; done"]
// create the pod
# kubectl create -f 3-2-3_proxy-chk.yaml
pod "proxy-chk" created
让我们检查 Pod 的stdout:
// get logs from proxy-chk
# kubectl logs proxy-chk
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 219 100 219 0 0 2596 0 --:--:-- --:--:-- --:--:-- 2607
100 258 100 258 0 0 1931 0 --:--:-- --:--:-- --:--:-- 1931
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en-CA">
...
太好了!现在我们可以确认代理正常工作了。流量会被路由到我们指定的端点。如果它不起作用,请确保你在外部资源的网络中添加了正确的入站规则。
端点不支持 DNS 作为来源。我们可以使用 ExternalName,它也没有选择器。这需要 kube-dns 版本>= 1.7。
在某些使用场景中,用户不需要负载均衡或代理功能。此时,我们可以将 CluterIP = "None" 设置为所谓的无头服务。有关更多信息,请参见 kubernetes.io/docs/concepts/services-networking/service/#headless-services。
卷
容器是临时的,其磁盘也是临时的。我们可以使用 docker commit [CONTAINER] 命令,或将数据卷挂载到容器中(第二章,容器化 DevOps)。在 Kubernetes 领域,卷管理至关重要,因为 Pod 可能会运行在任何节点上。此外,确保同一 Pod 中的容器能够共享相同的文件变得极为困难。这是 Kubernetes 中的一个重要话题。第四章,有状态工作负载的管理,介绍了卷管理。
秘密
如其名所示,秘密是一个存储以键值对格式存储机密信息的对象,供 Pod 提供敏感信息。它可能是一个密码、访问密钥或令牌。秘密不会存储在磁盘中,而是存储在每个节点的 tmpfs 文件系统中。节点上的 Kubelet 将创建一个 tmpfs 文件系统来存储该秘密。由于存储管理的考虑,秘密并不设计用来存储大量数据。当前单个秘密的大小限制为 1 MB。
我们可以通过启动 kubectl 创建一个秘密命令或通过规范,根据文件、目录或指定的字面值来创建一个秘密。秘密的格式有三种类型:通用类型(或编码后为不透明类型)、docker 注册表类型和 TLS 类型。
我们将在应用程序中使用通用或不透明类型。docker 注册表类型用于存储私有 Docker 注册表的凭证。TLS 秘密用于存储用于集群管理的 CA 证书包。
docker-registry 类型的秘密也叫做 imagePullSecrets,用于通过 kubelet 在拉取镜像时传递私有 Docker 注册表的密码。这意味着我们不需要为每个配置的节点输入 docker login。命令如下:kubectl create secret docker-registry <registry_name> --docker-server =<docker_server> --docker-username=<docker_username> --docker-password=<docker_password> --docker-email=<docker_email>。
我们将从一个通用示例开始,展示它是如何工作的:
// create a secret by command line
# kubectl create secret generic mypassword --from-file=./mypassword.txt
secret "mypassword" created
基于目录和字面值创建秘密的选项与基于文件的选项非常相似。如果我们在 --from-file 后指定一个目录,目录中的文件将会被遍历。如果文件名是合法的秘密名称,则该文件名将作为秘密键。非普通文件,如子目录、符号链接、设备或管道将被忽略。另一方面,--from-literal=<key>=<value> 是一个选项,用于直接从命令中指定纯文本,例如 --from-literal=username=root。
在这里,我们从mypassword.txt文件中创建一个名为mypassword的密钥。默认情况下,密钥的键是文件名,这相当于--from-file=mypassword=./mypassword.txt选项。我们也可以添加多个--from-file实例。我们可以使用kubectl get secret -o yaml命令来查看密钥的更多详细信息:
// get the detailed info of the secret
# kubectl get secret mypassword -o yaml
apiVersion: v1
data:
mypassword: bXlwYXNzd29yZA==
kind: Secret
metadata:
creationTimestamp: 2017-06-13T08:09:35Z
name: mypassword
namespace: default
resourceVersion: "256749"
selfLink: /api/v1/namespaces/default/secrets/mypassword
uid: a33576b0-500f-11e7-9c45-080027cafd37
type: Opaque
我们可以看到,密钥的类型变成了Opaque,因为文本已经通过 kubectl 加密,它是base64编码的。我们可以使用一个简单的bash命令来解码它:
# echo "bXlwYXNzd29yZA==" | base64 --decode
mypassword
Pod 获取密钥有两种方式。第一种是通过文件,第二种是通过环境变量。第一种方法通过卷实现。语法涉及在容器规范中添加containers.volumeMounts,并在卷部分中添加密钥配置。
通过文件获取密钥
让我们先看看如何从 Pod 内的文件读取密钥:
// example for how a Pod retrieve secret
# cat 3-2-3_pod_vol_secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-access
spec:
containers:
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do cat /secret/password-example; sleep 10; done"]
volumeMounts:
- name: secret-vol
mountPath: /secret
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: mypassword
items:
- key: mypassword
path: password-example
// create the pod
# kubectl create -f 3-2-3_pod_vol_secret.yaml
pod "secret-access" created
密钥文件将被挂载到/<mount_point>/<secret_name>,无需在 Pod 中指定items``key、path或/<mount_point>/<path>。在这种情况下,文件路径是/secret/password-example。如果我们描述 Pod,我们会发现这个 Pod 中有两个挂载点:一个只读卷存储我们的密钥,另一个存储与 API 服务器通信的凭证,该凭证由 Kubernetes 创建和管理。我们将在第六章,Kubernetes 网络中了解更多内容:
# kubectl describe pod secret-access
...
Mounts:
/secret from secret-vol (ro)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-jd1dq (ro)
...
我们可以使用kubectl delete secret <secret_name>命令删除一个密钥。
描述 Pod 后,我们可以发现一个FailedMount事件,因为卷不再存在:
# kubectl describe pod secret-access
...
FailedMount MountVolume.SetUp failed for volume
"kubernetes.io/secret/28889b1d-5015-11e7-9c45-080027cafd37-secret-vol" (spec.Name: "secret-vol") pod "28889b1d-5015-11e7-9c45-080027cafd37" (UID: "28889b1d-5015-11e7-9c45-080027cafd37") with: secrets "mypassword" not found
...
如果 Pod 在密钥创建之前生成,Pod 也会遇到失败。
我们现在将学习如何使用命令行创建密钥。我们将简要介绍它的 spec 格式:
// secret example
# cat 3-2-3_secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mypassword
type: Opaque
data:
mypassword: bXlwYXNzd29yZA==
由于 spec 是纯文本,我们需要使用自己的echo -n <password> | base64命令来编码密钥。请注意,此时类型变为Opaque。这应该和我们通过命令行创建的方式相同。
通过环境变量获取密钥
或者,我们也可以使用环境变量来获取密钥,这对于短期凭证(如密码)来说更灵活。应用程序能够使用环境变量来获取数据库密码,而无需处理文件和卷:
// example to use environment variable to retrieve the secret
# cat 3-2-3_pod_ev_secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-access-ev
spec:
containers:
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do echo $MY_PASSWORD; sleep 10; done"]
env:
- name: MY_PASSWORD
valueFrom:
secretKeyRef:
name: mypassword
key: mypassword
// create the pod
# kubectl create -f 3-2-3_pod_ev_secret.yaml
pod "secret-access-ev" created
密钥应该始终在需要它的 Pod 之前创建。否则,Pod 将无法成功启动。
声明在spec.containers[].env[]下。我们需要密钥的名称和键的名称。在本例中,两者都是mypassword。这个示例应该和我们之前看的那个相同。
ConfigMap
ConfigMap 是一种资源,它允许你将配置留在 Docker 镜像外部。它将配置数据作为键值对注入到 Pod 中。它的特性类似于 secrets,但是,虽然 secrets 用于存储敏感数据(如密码),ConfigMap 用于存储不敏感的配置信息。
类似于 secrets,ConfigMap 可以基于文件、目录或指定的字面值。它们的语法与 secrets 相似,但使用kubectl create configmap:
// create configmap
# kubectl create configmap example --from-file=config/app.properties --from-file=config/database.properties
configmap "example" created
由于两个config文件位于同一个文件夹config中,我们可以传递整个config文件夹,而不是逐一指定文件。在这种情况下,等效的命令是kubectl create configmap example --from-file=config。
如果我们描述该 ConfigMap,它将显示当前信息:
// check out detailed information for configmap
# kubectl describe configmap example
Name: example
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
app.properties:
----
name=DevOps-with-Kubernetes
port=4420
database.properties:
----
endpoint=k8s.us-east-1.rds.amazonaws.com
port=1521
我们可以使用kubectl edit configmap <configmap_name>来更新创建后的配置。
我们也可以使用literal作为输入。前面示例的等效命令是kubectl create configmap example --from-literal=app.properties.name=name=DevOps-with-Kubernetes。当我们在应用程序中有许多配置时,这种方式并不总是很实用。
让我们看看如何在 Pod 内部使用这个功能。将 ConfigMap 用于 Pod 内部有两种方式:通过卷或环境变量。
通过卷使用 ConfigMap
与Secrets子章节中的示例类似,我们使用configmap语法挂载一个卷,并在容器模板内添加volumeMounts。在centos中的命令将循环执行cat ${MOUNTPOINT}/$CONFIG_FILENAME:
cat 3-2-3_pod_vol_configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-vol
spec:
containers:
- name: configmap
image: centos
command: ["/bin/sh", "-c", "while : ;do cat /src/app/config/database.properties; sleep 10; done"]
volumeMounts:
- name: config-volume
mountPath: /src/app/config
volumes:
- name: config-volume
configMap:
name: example
// create configmap
# kubectl create -f 3-2-3_pod_vol_configmap.yaml
pod "configmap-vol" created
// check out the logs
# kubectl logs -f configmap-vol
endpoint=k8s.us-east-1.rds.amazonaws.com
port=1521
然后我们可以使用这种方法将我们的非敏感配置注入到 Pod 中。
通过环境变量使用 ConfigMap
要在 Pod 内部使用 ConfigMap,你需要在env部分使用configMapKeyRef作为值来源。这将把整个 ConfigMap 键值对填充到环境变量中:
# cat 3-2-3_pod_ev_configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-ev
spec:
containers:
- name: configmap
image: centos
command: ["/bin/sh", "-c", "while : ;do echo $DATABASE_ENDPOINT; sleep 10; done"]
env:
- name: DATABASE_ENDPOINT
valueFrom:
configMapKeyRef:
name: example
key: database.properties
// create configmap
# kubectl create -f 3-2-3_pod_ev_configmap.yaml
pod/configmap-ev created
// check out the logs
# kubectl logs configmap-ev
endpoint=k8s.us-east-1.rds.amazonaws.com port=1521
Kubernetes 系统本身也使用 ConfigMap 来进行一些认证。通过在kubectl describe configmap命令中添加--namespace=kube-system,可以查看系统的 ConfigMap。
多容器编排
在这一部分,我们将重新审视我们的售票服务:一个作为前端的自助服务机 web 服务,提供获取/放置票据的接口。这里有一个 Redis 作为缓存,管理我们拥有的票据数量。Redis 还充当发布/订阅通道。一旦票据售出,自助服务机将发布一个事件到该通道。订阅者叫做 recorder,它会写入时间戳并将其记录到 MySQL 数据库中。有关详细的 Dockerfile 和 Docker Compose 实现,请参考第二章最后一节,容器中的 DevOps。我们将在 Kubernetes 中使用Deployment、Service、Secret、Volume和ConfigMap对象来实现这个示例。源代码可以在以下链接找到:github.com/DevOps-with-Kubernetes/examples/tree/master/chapter3/3-3_kiosk。
使用 Kubernetes 资源的服务架构如下图所示:

Kubernetes 世界中的一个自助服务机示例
我们需要四种类型的 pods。Deployment 是管理或部署 pods 的最佳选择。由于其部署策略功能,这将减少我们未来执行部署时所需的工作量。由于自助服务机、Redis 和 MySQL 将被其他组件访问,我们将为它们的 pods 关联服务。MySQL 作为数据存储,对于简单起见,我们将挂载一个本地卷到 MySQL。请注意,Kubernetes 提供了许多选择。有关详细信息和示例,请查看第四章,管理有状态工作负载。我们希望将敏感信息,如 MySQL 的 root 用户和密码存储在 secrets 中。其他不敏感的配置,如数据库名称或用户名,我们将交给 ConfigMap 来处理。
我们首先启动 MySQL,因为记录器依赖于它。在创建 MySQL 之前,我们需要先创建相应的secret和ConfigMap。要创建一个secret,我们需要生成base64加密数据:
// generate base64 secret for MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD
# echo -n "pass" | base64
cGFzcw==
# echo -n "mysqlpass" | base64
bXlzcWxwYXNz
然后,我们就可以创建 secret 了:
# cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-user
type: Opaque
data:
password: cGFzcw==
---
# MYSQL_ROOT_PASSWORD
apiVersion: v1
kind: Secret
metadata:
name: mysql-root
type: Opaque
data:
password: bXlzcWxwYXNz
// create mysql secret
# kubectl create -f secret.yaml --record
secret "mysql-user" created
secret "mysql-root" created
接下来,我们来看看我们的 ConfigMap。这里,我们以数据库用户和数据库名称为例:
# cat config.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: mysql-config
data:
user: user
database: db
// create ConfigMap
# kubectl create -f config.yaml --record
configmap "mysql-config" created
然后是时候启动 MySQL 及其服务了:
# cat mysql.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: lmysql
spec:
replicas: 1
selector:
matchLabels:
tier: database
version: "5.7"
template:
metadata:
labels:
tier: database
version: "5.7"
spec:
containers:
- name: lmysql
image: mysql:5.7
volumeMounts:
- mountPath: /var/lib/mysql
name: mysql-vol
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-root
key: password
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: mysql-config
key: database
- name: MYSQL_USER
valueFrom:
configMapKeyRef:
name: mysql-config
key: user
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-user
key: password
volumes:
- name: mysql-vol
hostPath:
path: /mysql/data
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
---
kind: Service
apiVersion: v1
metadata:
name: lmysql-service
spec:
selector:
tier: database
ports:
- protocol: TCP
port: 3306
targetPort: 3306
name: tcp3306
我们可以通过在文件中添加三个短横线来将多个规格放在一个文件中。在这里,我们将hostPath /mysql/data挂载到 pods 的路径/var/lib/mysql。在环境部分,我们使用secretKeyRef和configMapKeyRef语法来引用 secret 和 ConfigMap。
在创建 MySQL 之后,Redis 将是下一个最佳候选项,因为其他服务依赖于它,但它没有任何先决条件:
// create Redis deployment
# cat redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: lcredis
spec:
replicas: 1
selector:
matchLabels:
tier: cache
version: "3.0"
template:
metadata:
labels:
tier: cache
version: "3.0"
spec:
containers:
- name: lcredis
image: redis:3.0
ports:
- containerPort: 6379
minReadySeconds: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
---
kind: Service
apiVersion: v1
metadata:
name: lcredis-service
spec:
selector:
tier: cache
ports:
- protocol: TCP
port: 6379
targetPort: 6379
name: tcp6379
// create redis deployements and service
# kubectl create -f redis.yaml
deployment "lcredis" created
service "lcredis-service" created
然后是启动自助服务机的好时机:
# cat kiosk-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kiosk-example
spec:
replicas: 5
selector:
matchLabels:
tier: frontend
version: "3"
template:
metadata:
labels:
tier: frontend
version: "3"
annotations:
maintainer: cywu
spec:
containers:
- name: kiosk-example
image: devopswithkubernetes/kiosk-example
ports:
- containerPort: 5000
env:
- name: REDIS_HOST
value: lcredis-service.default
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
---
kind: Service
apiVersion: v1
metadata:
name: kiosk-service
spec:
type: NodePort
selector:
tier: frontend
ports:
- protocol: TCP
port: 80
targetPort: 5000
name: tcp5000
// launch the spec
# kubectl create -f kiosk-example.yaml
deployment "kiosk-example" created
service "kiosk-service" created
在这里,我们将lcredis-service.default暴露给 kiosk pods 作为环境变量。这是 kube-dns 为Service对象创建的 DNS 名称(在本章中称为 Services)。因此,kiosk 可以通过环境变量访问 Redis 主机。
最终,我们将创建一个录制器。这个录制器不会向外暴露任何接口,因此不需要Service对象:
# cat recorder-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: recorder-example
spec:
replicas: 3
selector:
matchLabels:
tier: backend
version: "3"
template:
metadata:
labels:
tier: backend
version: "3"
annotations:
maintainer: cywu
spec:
containers:
- name: recorder-example
image: devopswithkubernetes/recorder-example
env:
- name: REDIS_HOST
value: lcredis-service.default
- name: MYSQL_HOST
value: lmysql-service.default
- name: MYSQL_USER
value: root
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-root
key: password
minReadySeconds: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
// create recorder deployment
# kubectl create -f recorder-example.yaml
deployment "recorder-example" created
录制器需要同时访问 Redis 和 MySQL。它使用通过密钥注入的 root 凭据。Redis 和 MySQL 的两个端点通过服务的 DNS 名称<service_name>.<namespace>进行访问。
然后我们可以检查Deployment对象:
// check deployment details
# kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kiosk-example 5 5 5 5 1h
lcredis 1 1 1 1 1h
lmysql 1 1 1 1 1h
recorder-example 3 3 3 3 1h
如预期的那样,我们有四个Deployment对象,且每个对象的期望 pod 数量不同。
当我们将 kiosk 暴露为 NodePort 时,我们应该能够访问它的服务端点,并检查它是否正常工作。假设我们有一个节点,它的 IP 地址是192.168.99.100,Kubernetes 分配的 NodePort 是30520。
如果你正在使用 minikube,minikube service [-n NAMESPACE] [--url] NAME可以帮助你通过默认浏览器访问服务的 NodePort:
**// 打开 kiosk 控制台**
**# minikube service kiosk-service**
**正在默认浏览器中打开 kuberenetes 服务 default/kiosk-service...**
这将允许我们找到 IP 和端口。
然后,我们可以使用POST和GET /tickets创建并获取票据:
// post ticket
# curl -XPOST -F 'value=100' http://192.168.99.100:30520/tickets
SUCCESS
// get ticket
# curl -XGET http://192.168.99.100:30520/tickets
100
总结
在本章中,我们学习了 Kubernetes 的基本概念。我们了解到,Kubernetes 主节点有 kube-apiserver 来处理请求,而控制器管理器是 Kubernetes 的控制中心。它们确保我们期望的容器数量得到满足,控制 pods 与服务之间的关联端点,并且控制 API 访问令牌。我们还学习了 Kubernetes 节点,它们是用来托管容器的工作节点,接收来自主节点的信息,并根据配置路由流量。
然后,我们使用 minikube 演示了基本的 Kubernetes 对象,包括 pods、ReplicaSets、Deployments、Services、secrets 和 ConfigMaps。最后,我们演示了如何将我们学到的所有概念结合起来构建我们的 kiosk 应用。
正如我们之前提到的,容器中的数据会在容器被删除时消失。因此,在容器世界中,卷(volume)是非常重要的,它能够持久化数据。在第四章《管理有状态工作负载》中,我们将学习卷是如何工作的,以及如何使用持久卷。
第四章:管理有状态的工作负载
在第三章,《Kubernetes 入门》中,我们介绍了 Kubernetes 的基本功能。一旦你开始使用 Kubernetes 部署容器,你就需要考虑应用程序的数据生命周期以及 CPU/内存资源管理。
在本章中,我们将讨论以下主题:
-
容器与卷的行为
-
介绍 Kubernetes 的卷功能
-
Kubernetes 持久卷的最佳实践与陷阱
-
提交短生命周期的应用程序作为 Kubernetes 作业
Kubernetes 卷管理
Kubernetes 和 Docker 默认使用本地磁盘。Docker 应用程序可能将任何数据存储到磁盘并加载,例如日志数据、临时文件和应用数据。只要主机有足够的空间,且应用程序具有必要的权限,数据将在容器存在期间保持存在。换句话说,当容器终止、退出、崩溃或重新分配到另一个主机时,数据将丢失。
容器卷生命周期
为了理解 Kubernetes 的卷管理,你需要了解 Docker 卷的生命周期。以下示例展示了当容器重新启动时 Docker 如何与卷交互:
//run CentOS Container
$ docker run -it centos
# ls
anaconda-post.log dev home lib64 media opt root sbin sys usr
bin etc lib lost+found mnt proc run srv tmp var
//create one file (/I_WAS_HERE) at root directory
# touch /I_WAS_HERE
# ls /
I_WAS_HERE bin etc lib lost+found mnt proc run srv tmp
var
anaconda-post.log dev home lib64 media opt root sbin sys usr
//Exit container
# exit
exit
//re-run CentOS Container
# docker run -it centos
//previous file (/I_WAS_HERE) was disappeared
# ls /
anaconda-post.log dev home lib64 media opt root sbin sys usr bin etc lib lost+found mnt proc run srv tmp var
在 Kubernetes 中,你还需要注意 pod 重启的情况。在资源不足时,Kubernetes 可能会停止一个容器,然后在同一个或另一个 Kubernetes 节点上重启该容器。
以下示例展示了当资源不足时 Kubernetes 的行为。收到内存不足错误时,一个 pod 会被终止并重新启动:
//there are 2 pod on the same Node
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
Besteffort 1/1 Running 0 1h
guaranteed 1/1 Running 0 1h
//when application consumes a lot of memory, one Pod has been killed
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
Besteffort 0/1 Error 0 1h
guaranteed 1/1 Running 0 1h
//clashed Pod is restarting
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
Besteffort 0/1 CrashLoopBackOff 0 1h
guaranteed 1/1 Running 0 1h
//few moment later, Pod has been restarted
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
Besteffort 1/1 Running 1 1h
guaranteed 1/1 Running 0 1h
在同一 pod 中共享容器之间的卷
第三章,《Kubernetes 入门》中提到,同一 Kubernetes pod 中的多个容器可以共享相同的 pod IP 地址、网络端口和 IPC。因此,应用程序可以通过 localhost 网络进行通信。然而,文件系统是隔离的。
以下图示展示了 Tomcat 和 nginx 在同一个 pod 中。这些应用程序可以通过 localhost 进行互相通信。然而,它们无法访问彼此的 config 文件:

一些应用程序不会影响这些场景和行为,但有些应用程序可能有一些使用案例需要它们使用共享目录或文件。因此,开发人员和 Kubernetes 管理员需要了解无状态和有状态应用程序的不同类型。
无状态和有状态的应用程序
无状态应用程序不需要在磁盘卷上保留应用程序或用户数据。尽管无状态应用程序在容器存在期间可能会将数据写入文件系统,但从应用程序生命周期的角度来看,这些数据并不重要。
例如,tomcat容器运行一些 Web 应用程序。它还会在/usr/local/tomcat/logs/下写入应用程序日志,但如果丢失log文件,它不会受到影响。
但是,如果你需要持久化一个应用程序日志用于分析或审计呢?在这种情况下,Tomcat 仍然可以是无状态的,但可以与另一个容器(如 Logstash)共享/usr/local/tomcat/logs存储卷(www.elastic.co/products/logstash)。Logstash 将把日志发送到选择的分析存储,如 Elasticsearch(www.elastic.co/products/elasticsearch)。
在这种情况下,tomcat容器和logstash容器必须在同一个 Kubernetes Pod 中,并共享/usr/local/tomcat/logs存储卷,如下所示:

上图展示了 Tomcat 和 Logstash 如何使用 Kubernetes 的emptyDir存储卷共享log文件(kubernetes.io/docs/concepts/storage/volumes/#emptydir)。
Tomcat 和 Logstash 没有通过 localhost 使用网络,但它们通过 Kubernetes 的emptyDir卷共享了文件系统,Tomcat 容器中的/usr/local/tomcat/logs与logstash容器中的/mnt共享:
$ cat tomcat-logstash.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat
spec:
replicas: 1
selector:
matchLabels:
run: tomcat
template:
metadata:
labels:
run: tomcat
spec:
containers:
- image: tomcat
name: tomcat
ports:
- containerPort: 8080
env:
- name: UMASK
value: "0022"
volumeMounts:
- mountPath: /usr/local/tomcat/logs
name: tomcat-log
- image: logstash
name: logstash
args: ["-e input { file { path => \"/mnt/localhost_access_log.*\" } } output { stdout { codec => rubydebug } elasticsearch { hosts => [\"http://elasticsearch-svc.default.svc.cluster.local:9200\"] } }"]
volumeMounts:
- mountPath: /mnt
name: tomcat-log
volumes:
- name: tomcat-log
emptyDir: {}
让我们创建tomcat和logstash Pod,然后看看 Logstash 是否能在/mnt下看到 Tomcat 应用程序日志:
//create Pod
$ kubectl create -f tomcat-logstash.yaml
deployment.apps/tomcat created
//check Pod name
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tomcat-7d99999565-6pm64 2/2 Running 0 1m
//connect to logstash container to see /mnt directory
$ kubectl exec -it tomcat-7d99999565-6pm64 -c logstash /bin/bash
root@tomcat-7d99999565-6pm64:/# ls /mnt
catalina.2018-09-20.log localhost.2018-09-20.log manager.2018-09-20.log
host-manager.2018-09-20.log localhost_access_log.2018-09-20.txt
在这种情况下,Elasticsearch 在最终目的地必须是有状态的,这意味着它使用持久化存储。即使容器被重启,Elasticsearch 容器也必须保留数据。此外,你无需将 Elasticsearch 容器配置在与 Tomcat/Logstash 相同的 Pod 中。因为 Elasticsearch 应该是一个集中式日志数据存储,它可以与 Tomcat/Logstash Pod 分开,并独立扩展。
一旦你确定应用程序需要持久化存储,就可以查看不同类型的存储卷以及管理持久化存储的不同方法。
Kubernetes 的持久化存储和动态供给
Kubernetes 支持多种持久化存储,例如,公共云存储,如 AWS EBS 和 Google 持久磁盘。它还支持网络(分布式)文件系统,如 NFS、GlusterFS 和 Ceph。此外,它还可以支持块设备,如 iSCSI 和光纤通道。根据环境和基础设施,Kubernetes 管理员可以选择最佳匹配的持久化存储类型。
以下示例使用 GCP 持久磁盘作为持久存储。第一步是创建一个 GCP 持久磁盘,并命名为gce-pd-1。
如果使用 AWS EBS、Google 持久磁盘或 Azure 磁盘存储,Kubernetes 节点必须位于同一云平台上。此外,Kubernetes 对每个节点的最大卷数有限制。请参阅 Kubernetes 文档 kubernetes.io/docs/concepts/storage/storage-limits/。

然后,在 Deployment 定义中指定名称 gce-pd-1:
$ cat tomcat-pv.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat
spec:
replicas: 1
selector:
matchLabels:
run: tomcat
template:
metadata:
labels:
run: tomcat
spec:
containers:
- image: tomcat
name: tomcat
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /usr/local/tomcat/logs
name: tomcat-log
volumes:
- name: tomcat-log
gcePersistentDisk:
pdName: gce-pd-1
fsType: ext4
这将把来自 GCE 持久磁盘的持久磁盘挂载到 /usr/local/tomcat/logs,从而保持 Tomcat 应用程序的日志持久化。
通过持久卷声明抽象化卷层
在配置文件中直接指定持久卷会与特定基础设施紧密耦合。例如,在前面的示例(tomcat-log 卷)中,pdName 是 gce-pd-1,卷类型是 gcePersistentDisk。从容器管理的角度来看,pod 定义不应该与特定环境绑定,因为基础设施可能会根据环境不同而有所变化。理想的 pod 定义应该是灵活的,或抽象出实际的基础设施,只指定卷名称和挂载点。
因此,Kubernetes 提供了一个抽象层,将 pod 与持久卷关联,这个抽象层被称为 持久卷声明(PVC)。这使我们能够与基础设施解耦。Kubernetes 管理员只需提前预分配持久卷的大小。然后,Kubernetes 会将持久卷和 PVC 绑定如下:

以下示例是使用 PVC 的 pod 定义;我们先使用之前的示例(gce-pd-1)在 Kubernetes 中进行注册:
$ cat pv-gce-pd-1.yml
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
name: pv-1
spec:
storageClassName: "my-10g-pv-1"
capacity:
storage: "10Gi"
accessModes:
- "ReadWriteOnce"
gcePersistentDisk:
fsType: "ext4"
pdName: "gce-pd-1"
$ kubectl create -f pv-gce-pd-1.yml
persistentvolume/pv-1 created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-1 10Gi RWO Retain Available my-10g-pv-1 11s
请注意,我们将 storageClassName 设置为 my-10g-pv-1,作为 PVC 可以通过指定相同名称来绑定的标识符。
接下来,我们创建一个与持久卷(pv-1)关联的 PVC。
storageClassName 参数允许 Kubernetes 使用静态供应。这是因为一些 Kubernetes 环境,如 Google 容器引擎(GKE),已经配置了动态供应。如果不指定 storageClassName,Kubernetes 将分配一个新的 PersistentVolume,然后绑定到 PersistentVolumeClaim。
//create PVC specify storageClassName as "my-10g-pv-1"
$ cat pvc-1.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-1
spec:
storageClassName: "my-10g-pv-1"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
$ kubectl create -f pvc-1.yml
persistentvolumeclaim/pvc-1 created
//check PVC status is "Bound"
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-1 Bound pv-1 10Gi RWO my-10g-pv-1 7s
//check PV status is also "Bound"
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-1 10Gi RWO Retain Bound default/pvc-1 my-10g-pv-1 2m
现在,tomcat 设置已经与 GCE 持久卷解耦,并绑定到抽象卷 pvc-1:
$ cat tomcat-pvc.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat
spec:
replicas: 1
selector:
matchLabels:
run: tomcat
template:
metadata:
labels:
run: tomcat
spec:
containers:
- image: tomcat
name: tomcat
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /usr/local/tomcat/logs
name: tomcat-log
volumes:
- name: tomcat-log
persistentVolumeClaim:
claimName: "pvc-1"
动态供应和 StorageClass
PVC 提供了持久卷管理的灵活性。然而,预分配一些持久卷池可能不是成本效益高,特别是在公共云环境中。
Kubernetes 还通过支持持久卷的动态配置来帮助解决这种情况。Kubernetes 管理员定义持久卷的供应者,称为StorageClass。然后,PVC 请求StorageClass动态分配持久卷,并将其与 PVC 关联,如下所示:

在以下示例中,AWS EBS 作为StorageClass使用。在创建 PVC 时,StorageClass动态创建一个 EBS,然后将其注册为持久卷(PV),并将其附加到 PVC:
$ cat storageclass-aws.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: aws-sc
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
$ kubectl create -f storageclass-aws.yml
storageclass "aws-sc" created
$ kubectl get storageclass
NAME. TYPE
aws-sc. kubernetes.io/aws-ebs
一旦StorageClass成功创建,接下来创建一个没有 PV 的 PVC,但需要指定StorageClass名称。在这个示例中,名称应为aws-sc,如下所示:
$ cat pvc-aws.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-aws-1
spec:
storageClassName: "aws-sc"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
$ kubectl create -f pvc-aws.yml
persistentvolumeclaim/pvc-aws-1 created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-03557eb8-bc8b-11e8-994f-42010a800085 10Gi RWO Delete Bound default/pvc-aws-1 aws-sc 1s
以下截图展示了提交到 StorageClass 创建 PVC 后的 EBS。AWS 控制台显示一个由 StorageClass 创建的新 EBS:

请注意,像 Amazon EKS (aws.amazon.com/eks/)、Google Kubernetes Engine (cloud.google.com/container-engine/) 和 Azure Kubernetes Service (azure.microsoft.com/en-us/services/kubernetes-service/) 这样的托管 Kubernetes 服务会默认创建StorageClass。例如,Google Kubernetes Engine 将默认存储类设置为 Google Cloud 持久磁盘。有关更多信息,请参考第十章,Kubernetes 在 AWS 上,第十一章,Kubernetes 在 GCP 上,以及第十二章,Kubernetes 在 Azure 上:
//default Storage Class on GKE
$ kubectl get sc
NAME TYPE
standard (default) kubernetes.io/gce-pd
关于短暂性和持久性卷设置的问题
你可能会将你的应用程序视为无状态的,因为datastore功能由另一个 Pod 或系统处理。然而,这里面有一些陷阱,有时应用程序会存储一些重要文件,而你未曾意识到。例如,Grafana (grafana.com/grafana) 连接了时间序列数据源,如 Graphite (graphiteapp.org) 和 InfluxDB (www.influxdata.com/time-series-database/),所以人们可能误认为 Grafana 是一个无状态应用。
事实上,Grafana 本身也使用数据库来存储用户、组织和仪表板元数据。默认情况下,Grafana 使用 SQLite3 组件,并将数据库存储为/var/lib/grafana/grafana.db。因此,当容器重新启动时,Grafana 的设置将会全部被重置。
以下示例展示了 Grafana 在使用短暂性卷时的行为:
$ cat grafana.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
run: grafana
template:
metadata:
labels:
run: grafana
spec:
containers:
- image: grafana/grafana
name: grafana
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: grafana
spec:
ports:
- protocol: TCP
port: 3000
nodePort: 30300
type: NodePort
selector:
run: grafana
接下来,进入 Grafana web 控制台创建名为kubernetes org的 Grafana Organizations,如下所示:

然后,查看 Grafana 目录。这里有一个数据库文件(/var/lib/grafana/grafana.db),其时间戳在创建 Grafana Organizations 后已更新:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
grafana-6bf966d7b-7lh89 1/1 Running 0 3m
//access to Grafana container
$ kubectl exec -it grafana-6bf966d7b-7lh89 /bin/bash
grafana@grafana-6bf966d7b-7lh89:/$ ls -l /var/lib/grafana/
total 404
-rw-r--r-- 1 grafana grafana 401408 Sep 20 03:30 grafana.db
drwxrwxrwx 2 grafana grafana 4096 Sep 7 14:59 plugins
drwx------ 4 grafana grafana 4096 Sep 20 03:30 sessions
当 Pod 被删除时,Deployment 将启动一个新 Pod,并检查是否存在 Grafana Organizations:
grafana@grafana-6bf966d7b-7lh89:/$ exit
//delete grafana pod
$ kubectl delete pod grafana-6bf966d7b-7lh89
pod "grafana-6bf966d7b-7lh89" deleted
//Kubernetes Deployment made a new Pod
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
grafana-6bf966d7b-wpdmk 1/1 Running 0 9s
//contents has been recreated
$ kubectl exec -it grafana-6bf966d7b-wpdmk /bin/bash
grafana@grafana-6bf966d7b-wpdmk:/$ ls -l /var/lib/grafana
total 400
-rw-r--r-- 1 grafana grafana 401408 Sep 20 03:33 grafana.db
drwxrwxrwx 2 grafana grafana 4096 Sep 7 14:59 plugins
看起来 sessions 目录消失了,grafana.db 也被 Docker 镜像重新创建了。如果你访问 Web 控制台,Grafana 的 organization 也会消失:

那么,直接将一个持久化卷挂载到 Grafana 上怎么样?你很快会发现,将持久化卷挂载到一个由 Deployment 控制的 Pod 上并不能很好地扩展,因为每个新创建的 Pod 都会尝试挂载相同的持久化卷。在大多数情况下,只有第一个 Pod 能挂载持久化卷。其他 Pod 会尝试挂载该卷,如果无法挂载,它们将放弃并冻结。这种情况发生在持久化卷只支持 RWO(只读写一次;仅一个 Pod 可以写入)时。
在以下示例中,Grafana 使用持久化卷挂载 /var/lib/grafana;然而,由于 Google 持久化磁盘是 RWO,导致无法扩展:
$ cat grafana-pv.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
run: grafana
template:
metadata:
labels:
run: grafana
spec:
containers:
- image: grafana/grafana:3.1.1
name: grafana
ports:
- containerPort: 3000
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-data
volumes:
- name: grafana-data
gcePersistentDisk:
pdName: gce-pd-1
fsType: ext4
$ kubectl create -f grafana-pv.yml
deployment.apps/grafana created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
grafana-6cf5467c9d-nw6b7 1/1 Running 0 41s
//can't scale up, becaues 3 Pod can't mount the same volume
$ kubectl scale deploy grafana --replicas=3
The Deployment "grafana" is invalid: spec.template.spec.volumes[0].gcePersistentDisk.readOnly: Invalid value: false: must be true for replicated pods > 1; GCE PD can only be mounted on multiple machines if it is read-only
即使持久化卷具有 RWX 能力(读写多次;多个 Pod 可以同时挂载进行读写),比如 NFS,当多个 Pod 尝试绑定相同的卷时,它也不会报错。然而,我们仍然需要考虑是否可以让多个应用实例使用相同的文件夹/文件。例如,如果将 Grafana 复制到两个或更多 Pod,它们将发生冲突,因为多个 Grafana 实例尝试写入相同的 /var/lib/grafana/grafana.db,从而可能导致数据损坏,如下图所示:

在这种情况下,Grafana 必须使用像 MySQL 或 PostgreSQL 这样的后端数据库,而不是 SQLite3,如下所示。它允许多个 Grafana 实例正确地读写 Grafana 元数据:

由于 RDBMS 基本上支持通过网络连接多个应用实例,这种场景非常适合多个 Pod 使用。请注意,Grafana 支持使用 RDBMS 作为后端元数据存储;然而,并不是所有应用程序都支持 RDBMS。
有关使用 MySQL/PostgreSQL 的 Grafana 配置,请参阅在线文档:
docs.grafana.org/installation/configuration/#database。
因此,Kubernetes 管理员需要仔细监控应用程序与卷的交互行为,并理解在某些使用场景下,仅使用持久化卷可能无法解决问题,因为在扩展 Pod 时可能会出现问题。
如果多个 Pod 需要访问集中存储卷,那么可以考虑使用前面所示的数据库(如果适用)。另一方面,如果多个 Pod 需要各自的存储卷,可以考虑使用 StatefulSet。
使用 StatefulSet 复制带有持久化存储的 Pod
StatefulSet 在 Kubernetes 1.5 中引入;它将 Pod 与持久化存储卷绑定。当扩展一个 Pod 时,无论是增加还是减少,Pod 和持久化存储卷会一起创建或删除。
此外,Pod 创建过程是串行的。例如,当请求 Kubernetes 扩展两个额外的 StatefulSet 时,Kubernetes 会首先创建 持久化存储声明 1 和 Pod 1,然后再创建 持久化存储声明 2 和 Pod 2,但不会同时进行。这有助于管理员在应用程序启动时,如果应用程序需要注册到某个注册表:

即使某个 Pod 宕机,StatefulSet 也会保留该 Pod 的位置(Kubernetes 元数据,如 Pod 名称)和持久化存储卷。然后,它会尝试重新创建一个容器,将其重新分配给相同的 Pod,并挂载相同的持久化存储卷。
如果你使用 StatefulSet 运行无头服务,Kubernetes 还会为 Pod 分配并保留完全限定的域名(FQDN)。
无头服务将在 第六章Kubernetes 网络 中详细描述。
这有助于通过 Kubernetes 调度器跟踪和维护 Pod/持久化存储卷的数量,确保应用程序保持在线:

带有持久化存储的 StatefulSet 需要动态提供和 StorageClass,因为 StatefulSet 可以进行扩展。Kubernetes 需要知道如何在添加更多 Pod 时提供持久化存储。
向 Kubernetes 提交任务
通常,应用程序设计为长期运行,就像守护进程一样。典型的长期应用程序会打开网络端口并保持运行。它要求应用程序保持运行。如果失败,你需要重启以恢复状态。因此,使用 Kubernetes 部署是长期应用程序的最佳选择。
另一方面,一些应用程序设计为短生命周期的,例如命令脚本。即使成功执行,也预期应用程序会退出以完成任务。因此,Kubernetes 部署并不是合适的选择,因为部署会尝试保持进程运行。
不用担心,Kubernetes 也支持短生命周期的应用程序。你可以将容器提交为 任务 或 定时任务,Kubernetes 会将其调度到适当的节点并执行你的容器。
Kubernetes 支持几种类型的任务:
-
单一任务
-
可重复任务
-
并行任务
-
定时任务
最后一个也被称为CronJob。Kubernetes 支持这些不同类型的 Jobs,它们与 pods 的使用方式不同,以便更好地利用你的资源。
向 Kubernetes 提交单个 Job
类似 Job 的 pod 适用于运行批处理程序,如收集数据、查询数据库、生成报告等。尽管这被称为短生命周期,但花费多少时间并不重要。这可能需要几秒钟,甚至几天才能完成。它最终会退出应用程序,这意味着它有一个结束状态。
Kubernetes 能够将短生命周期的应用程序作为 Job 进行监控,在失败的情况下,Kubernetes 会为该 Job 创建一个新的 pod,尝试完成你的应用程序。
为了向 Kubernetes 提交 Job,你需要编写一个 Job 模板,指定 pod 配置。以下示例演示了如何检查 Ubuntu Linux 中安装的 dpkg:
$ cat job-dpkg.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: package-check
spec:
activeDeadlineSeconds: 60
template:
spec:
containers:
- name: package-check
image: ubuntu
command: ["dpkg-query", "-l"]
restartPolicy: Never
Job 的定义与 pod 定义类似,但重要的设置是 activeDeadlineSeconds 和 restartPolicy。activeDeadlineSeconds 参数设置 pod 运行的最大时间。如果超时,pod 将被终止。restartPolicy 参数定义了在失败情况下 Kubernetes 的行为。例如,当 pod 崩溃时,如果你指定 Never,Kubernetes 不会重启;如果你指定 OnFailure,Kubernetes 会尝试重新提交 Job,直到成功完成。
使用kubectl命令提交一个 Job,查看 Kubernetes 如何管理 pod:
$ kubectl create -f job-dpkg.yaml
job.batch/package-check created
因为这个 Job(dpkg-query -l 命令)是短生命周期的,它最终会exit()。因此,如果dpkg-query命令顺利完成,即使没有活动的 pod,Kubernetes 也不会尝试重启。因此,当你输入kubectl get pods时,pod 状态将在完成后显示为 completed:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
package-check-7tfkt 0/1 Completed 0 6m
虽然没有活动的 pod 存在,但你仍然可以通过输入 kubectl logs 命令来查看应用程序日志,如下所示:
$ kubectl logs package-check-7tfkt
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture Description
+++-=======================-======================-============-========================================================================
ii adduser 3.116ubuntu1 all add and remove users and groups
ii apt 1.6.3ubuntu0.1 amd64 commandline package manager
ii base-files 10.1ubuntu2.2 amd64 Debian base system miscellaneous files
ii base-passwd 3.5.44 amd64 Debian base system master password and group files
...
提交一个可重复的 Job
用户还可以决定单个 Job 中应完成的任务数量。这对解决一些随机抽样问题非常有帮助。让我们重用之前的模板,并添加spec.completions来查看差异:
$ cat repeat-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: package-check-repeat
spec:
activeDeadlineSeconds: 60
completions: 3
template:
spec:
containers:
- name: package-check-repeat
image: ubuntu
command: ["dpkg-query", "-l"]
restartPolicy: Never
$ kubectl create -f repeat-job.yaml
job.batch/package-check-repeat created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
package-check-7tfkt 0/1 Completed 0 52m
package-check-repeat-vl988 0/1 Completed 0 7s
package-check-repeat-flhz9 0/1 Completed 0 4s
package-check-repeat-xbf8b 0/1 Completed 0 2s
如你所见,三个 pod 被创建来完成这个 Job。如果你需要在特定时间反复运行程序,这非常有用。然而,正如你从前面的结果中的 Age 列看到的,这些 pods 是依次运行的。前面的结果显示年龄分别为 7 秒、4 秒和 2 秒。这意味着第二个 Job 是在第一个 Job 完成后启动的,第三个 Job 是在第二个 Job 完成后启动的。
如果某个 Job 运行时间较长(例如几天),但 1(st)、2(nd) 和 3^(rd) 个 Job 之间没有相关性,那么依次运行它们就没有意义了。在这种情况下,可以使用并行 Job。
提交并行作业
如果你的批处理作业之间没有状态或依赖关系,你可以考虑并行提交作业。为此,类似于 spec.completions 参数,作业模板有一个 spec.parallelism 参数,用于指定要并行运行的作业数量:
$ cat parallel-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: package-check-parallel
spec:
activeDeadlineSeconds: 60
parallelism: 3
template:
spec:
containers:
- name: package-check-parallel
image: ubuntu
command: ["dpkg-query", "-l"]
restartPolicy: Never
//submit a parallel job
$ kubectl create -f parallel-job.yaml
job.batch/package-check-parallel created
//check the result
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
package-check-7tfkt 0/1 Completed 0 1h
package-check-parallel-k8hpz 0/1 Completed 0 4s
package-check-parallel-m272g 0/1 Completed 0 4s
package-check-parallel-mc279 0/1 Completed 0 4s
package-check-repeat-flhz9 0/1 Completed 0 13m
package-check-repeat-vl988 0/1 Completed 0 13m
package-check-repeat-xbf8b 0/1 Completed 0 13m
从 kubectl get pods 命令的 AGE 列可以看出,表示这三个 Pod 是同时运行的。
在这种设置下,Kubernetes 可以将任务调度到可用节点上运行应用程序,从而轻松扩展你的作业。如果你想运行像工人应用程序这样的东西,将多个 Pod 分配到不同的节点,这会非常有用。
最后,如果你不再需要检查作业结果,可以使用 kubectl delete 命令删除资源,示例如下:
$ kubectl get jobs
NAME DESIRED SUCCESSFUL AGE
package-check 1 1 1h
package-check-parallel <none> 3 9m
package-check-repeat 3 3 23m
// delete a job one by one
$ kubectl delete jobs package-check-parallel
job.batch "package-check-parallel" deleted
$ kubectl delete jobs package-check-repeat
job.batch "package-check-repeat" deleted
$ kubectl delete jobs package-check
job.batch "package-check" deleted
//there is no pod
$ kubectl get pods
No resources found.
使用 CronJob 调度运行作业
如果你熟悉 UNIX CronJob 或 Java Quartz (www.quartz-scheduler.org),那么 Kubernetes CronJob 是一个非常直观的工具,你可以用它定义一个特定的时间来重复运行 Kubernetes 作业。
调度格式非常简单;它指定了以下五个项目:
-
分钟(0 到 59)
-
小时(0 到 23)
-
每月的日期(1 到 31)
-
月份(1 到 12)
-
星期几(0:星期日到 6:星期六)
例如,如果你只想每年 11 月 12 日早上 9:00 运行你的作业,给我发送生日祝福,那么调度格式将是 0 9 12 11 *
你也可以使用斜杠(/)来指定步长值。以五分钟为间隔运行前述作业的调度格式将是:
*/5 * * * *
以下模板使用 CronJob 每五分钟运行 package-check 命令:
$ cat cron-job.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: package-check-schedule
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: "Forbid"
jobTemplate:
spec:
template:
spec:
containers:
- name: package-check-schedule
image: ubuntu
command: ["dpkg-query", "-l"]
restartPolicy: Never
$ kubectl create -f cron-job.yaml
cronjob.batch/package-check-schedule created
你可能会注意到,模板格式与这里的作业模板略有不同。不过,我们需要关注一个参数:spec.concurrencyPolicy。通过这个参数,你可以指定如果上一个作业没有完成,而下一个作业的调度时间临近时的行为。这将决定下一个作业如何运行。你可以设置以下值:
-
允许:允许执行下一个作业
-
禁止:跳过下一个作业的执行
-
替换:删除当前作业,然后执行下一个作业
如果你设置了 允许,则可能会有积累一些未完成作业的风险。因此,在测试阶段,你应该设置 禁止 或 替换 来监控作业的执行和完成情况。
几秒钟后,作业会在你期望的时间触发——在这种情况下是每五分钟一次。然后你可以使用 kubectl get jobs 和 kubectl get pods 命令查看作业条目,如下所示:
$ kubectl get jobs
NAME DESIRED SUCCESSFUL AGE
package-check-schedule-1537169100 1 1 8m
package-check-schedule-1537169400 1 1 3m
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
package-check-schedule-1537169100-mnhxw 0/1 Completed 0 8m
package-check-schedule-1537169400-wvbgp 0/1 Completed 0 3m
CronJob 将一直存在,直到你删除它。这意味着每五分钟,CronJob 将创建一个新的作业条目,相关的 pods 也会持续创建。这将影响 Kubernetes 资源的消耗。因此,默认情况下,CronJob 会保留最多三个成功的作业(通过 spec.successfulJobsHistoryLimit)和一个失败的作业(通过 spec.failedJobsHistoryLimit)。你可以根据需求更改这些参数。
总的来说,CronJob 允许作业根据预定时间自动在你的应用程序中运行。你可以利用 CronJob 来运行报告生成作业、每日或每周批处理作业等。
摘要
在本章中,我们讨论了使用持久化卷的有状态应用程序。与短暂卷(ephemeral volumes)相比,在应用程序重启或 pod 扩展时,它们可能会遇到一些问题。此外,Kubernetes 上的持久化卷管理也得到了改进,使其更加便捷,正如你可以从 StatefulSet 和动态供给等工具中看到的那样。
此外,Jobs 和 CronJobs 是用于 pods 的特殊工具。与部署/副本集(deployment/ReplicaSets)相比,它们有一个期望的 pod 数量在运行,而这与作业的理想情况相反,作业的理想状态是当 pods 完成任务后应该被删除。这种短生命周期的应用程序也可以由 Kubernetes 管理。
在第五章,集群管理与扩展,我们将讨论集群管理,例如身份验证、授权和准入控制。我们还将介绍自定义资源定义(CRD),以及如何通过你自己的代码控制 Kubernetes 对象。
第五章:集群管理与扩展
在前面的章节中,我们已经熟悉了基本的 DevOps 技能和 Kubernetes 对象。这包括了多个方面,如如何将我们的应用容器化,并将容器化软件部署到 Kubernetes 中。现在是时候深入了解 Kubernetes 集群管理了。
在本章中,我们将学习以下内容:
-
利用命名空间设置管理边界
-
使用 kubeconfig 在多个集群之间切换
-
Kubernetes 身份验证
-
Kubernetes 授权
-
动态准入控制
-
Kubernetes 自定义资源定义 (CRD) 和控制器
虽然 minikube 是一个相对简单的环境,但在本章中我们将使用 Google Kubernetes Engine (GKE)。有关 GKE 中集群部署的更多信息,请参考 第十一章,在 GCP 上使用 Kubernetes。
Kubernetes 命名空间
我们在 第三章,Kubernetes 入门 中已经了解了 Kubernetes 命名空间,它们用于将集群中的资源划分为多个虚拟集群。命名空间使得每个组可以共享同一个物理集群,并且具有隔离性。每个命名空间提供以下功能:
-
名称的范围;每个命名空间中的对象名称是唯一的
-
确保可信身份验证的策略
-
设置资源管理的资源配额的能力
现在,让我们学习如何使用上下文在不同的命名空间之间切换。
上下文
上下文 是集群信息、用于身份验证的用户和命名空间的组合。例如,以下是我们在 GKE 中一个集群的上下文信息:
- context:
cluster: gke_devops-with-kubernetes_us-central1-b_cluster
user: gke_devops-with-kubernetes_us-central1-b_cluster
name: gke_devops-with-kubernetes_us-central1-b_cluster
我们可以使用 kubectl config current-context 命令来列出当前的上下文:
# kubectl config current-context
gke_devops-with-kubernetes_us-central1-b_cluster
要列出所有配置的详细信息,包括上下文,您可以使用 kubectl config view 命令。要查看当前正在使用的上下文,可以使用 kubectl config get-contexts 命令。
创建一个上下文
下一步是创建一个上下文。像前面示例中一样,我们需要为上下文设置用户和集群名称。如果没有指定,这些值将会为空。创建上下文的命令如下:
$ kubectl config set-context <context_name> --namespace=<namespace_name> --cluster=<cluster_name> --user=<user_name>
在同一集群中可以创建多个上下文。以下是如何在我的 GKE 集群 gke_devops-with-kubernetes_us-central1-b_cluster 中为 chapter5 创建上下文的示例:
// create a context with my GKE cluster
# kubectl config set-context chapter5 --namespace=chapter5 --cluster=gke_devops-with-kubernetes_us-central1-b_cluster --user=gke_devops-with-kubernetes_us-central1-b_cluster
Context "chapter5" created.
切换当前上下文
我们可以使用 use-context 子命令来切换上下文:
# kubectl config use-context chapter5
Switched to context "chapter5".
切换上下文后,通过 kubectl 调用的每个命令都会在 chapter5 上下文下执行。列出 pod 时无需显式指定命名空间。
Kubeconfig
Kubeconfig 是一个文件,你可以通过切换上下文来切换多个集群。我们可以使用 kubectl config view 查看设置,使用 kubectl config current-context 命令查看当前正在使用的上下文。以下是一个 GCP 集群在 kubeconfig 文件中的示例:
# kubectl config view
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://35.0.0.200
name: gke_devops-with-kubernetes_us-central1-b_cluster
contexts:
- context:
cluster: gke_devops-with-kubernetes_us-central1-b_cluster
user: gke_devops-with-kubernetes_us-central1-b_cluster
name: gke_devops-with-kubernetes_us-central1-b_cluster
current-context: gke_devops-with-kubernetes_us-central1-b_cluster
kind: Config
preferences: {}
users:
- name: gke_devops-with-kubernetes_us-central1-b_cluster
user:
auth-provider:
config:
access-token: XXXXX
cmd-args: config config-helper --format=json
cmd-path: /Users/devops/k8s/bin/gcloud
expiry: 2018-12-16T02:51:21Z
expiry-key: '{.credential.token_expiry}'
token-key: '{.credential.access_token}'
name: gcp
正如我们之前所学,我们可以使用 kubectl config use-context CONTEXT_NAME 来切换上下文。我们还可以根据 $KUBECONFIG 环境变量指定 kubeconfig 文件,以确定使用哪些 kubeconfig 文件。通过这种方式,可以合并配置文件。例如,以下命令将合并 kubeconfig-file1 和 kubeconfig-file2:
export KUBECONFIG=$KUBECONFIG: kubeconfig-file1: kubeconfig-file2
我们还可以使用 kubectl config --kubeconfig=<config file name> set-cluster <cluster name> 来指定目标 kubeconfig 文件中的目标集群。
默认情况下,kubeconfig 文件位于 $HOME/.kube/config。如果没有设置前面提到的任何设置,将会加载这个文件。
服务帐户
在 Kubernetes 中,有两种类型的用户帐户:服务帐户和用户帐户。所有发送到 API 服务器的请求都来自服务帐户或用户帐户。服务帐户由 Kubernetes API 管理,而用户帐户则不在 Kubernetes 中管理或存储。以下是服务帐户和用户帐户的简单对比:
| 服务帐户 | 用户帐户 | |
|---|---|---|
| 作用域 | 命名空间级 | 全局 |
| 使用者 | 进程 | 普通用户 |
| 创建者 | API 服务器或通过 API 调用 | 管理员,而非通过 API 调用 |
| 由谁管理 | API 服务器 | 集群外部 |
默认情况下,Kubernetes 集群会为不同的用途创建不同的服务帐户。在 GKE 中,已经创建了一些服务帐户:
// list service account across all namespaces
# kubectl get serviceaccount --all-namespaces
NAMESPACE NAME SECRETS AGE
default default 1 5d
kube-public default 1 5d
kube-system namespace-controller 1 5d
kube-system resourcequota-controller 1 5d
kube-system service-account-controller 1 5d
kube-system service-controller 1 5d
chapter5 default 1 2h
...
Kubernetes 会在每个命名空间中创建一个默认的服务帐户,如果在创建 Pod 时没有在 Pod 规范中指定服务帐户,则会使用该默认服务帐户。我们来看一下默认服务帐户在 chapter5 命名空间中的作用:
# kubectl describe serviceaccount/default
Name: default
Namespace: default
Labels: <none>
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: default-token-52qnr
Tokens: default-token-52qnr
Events: <none>
我们可以看到,服务帐户基本上是使用可挂载的密钥作为令牌。让我们深入了解令牌中的内容:
// describe the secret, the name is default-token-52qnr here
# kubectl describe secret default-token-52qnr
Name: default-token-52qnr
Namespace: chapter5
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 6bc2f108-dae5-11e8-b6f4-42010a8a0244 Type: kubernetes.io/service-account-token
Data
====
ca.crt: # the public CA of api server. Base64 encoded.
namespace: # the name space associated with this service account. Base64 encoded
token: # bearer token. Base64 encoded
服务帐户的密钥将自动挂载到 /var/run/secrets/kubernetes.io/serviceaccount 目录。当 Pod 访问 API 服务器时,API 服务器将检查证书和令牌进行认证。
在服务帐户或 Pod 规范中指定 automountServiceAccountToken: false 可以禁用自动挂载服务帐户密钥。
认证和授权
认证和授权是 Kubernetes 中的重要组件。认证用于验证用户,并检查用户是否是他们所声称的人。而授权则检查用户拥有的权限级别。Kubernetes 支持不同的认证和授权模块。
以下是一个示意图,展示了 Kubernetes API 服务器在收到请求时如何处理访问控制:

Kubernetes API 服务器中的访问控制
当请求到达 API 服务器时,首先通过验证客户端证书与 API 服务器中的证书颁发机构(CA)建立 TLS 连接。API 服务器中的 CA 通常位于 /etc/kubernetes/,客户端的证书通常位于 $HOME/.kube/config。握手完成后,进入身份验证阶段。在 Kubernetes 中,身份验证模块是链式的,可以使用多个身份验证模块。当接收到请求时,Kubernetes 会依次尝试所有身份验证器,直到成功。如果所有身份验证模块都失败,请求将被拒绝并返回 HTTP 401 Unauthorized 错误。如果成功,通过其中一个身份验证器验证用户身份,且请求被认证。此时,Kubernetes 的授权模块开始发挥作用。
授权模块验证用户是否具有足够的权限来执行他们请求的操作。授权模块也是链式的。授权请求需要通过每个模块,直到成功。如果所有模块都失败,请求者将收到 HTTP 403 Forbidden 响应。
准入控制是一组可配置的插件,在 API 服务器中决定请求是否被接受或拒绝。在此阶段,如果请求未能通过某个插件的检查,将立即被拒绝。
身份验证
默认情况下,服务账户是基于令牌的。当你创建一个服务账户或带有默认服务账户的命名空间时,Kubernetes 会创建令牌,将其作为一个密钥进行 Base64 编码存储,并将该密钥作为卷挂载到 pod 中。然后,pod 内的进程就可以与集群进行通信。另一方面,用户账户代表的是一个普通用户,可能会使用 kubectl 直接操作资源。
服务账户令牌身份验证
当我们创建一个服务账户时,Kubernetes 服务账户准入控制插件会自动创建一个签名的持有令牌。我们可以使用该服务账户令牌来验证用户身份。
让我们尝试在 chapter5 命名空间中创建一个名为 myaccount 的服务账户:
// the configuration file of service account object
# cat service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: myaccount
namespace: chapter5
// create myaccount
# kubectl create -f service-account.yaml
serviceaccount/myaccount created
在第九章,持续交付中,我们展示了如何部署my-app,我们创建了一个名为cd的命名空间,并使用get-sa-token.sh脚本(github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/blob/master/chapter9/9-2_service-account-for-ci-tool/utils/push-cd/get-sa-token.sh)为我们导出了令牌:
// export ca.crt and sa.token from myaccount in namespace chapter5
# sh ./chapter9/9-2_service-account-for-ci-tool/utils/push-cd/get-sa-token.sh -n chapter5 -a myaccount
然后,我们通过kubectl config set-credentials <user> --token=$TOKEN命令创建了一个名为mysa的用户:
// CI_ENV_K8S_SA_TOKEN=`cat sa.token`
# kubectl config set-credentials mysa --token=${K8S_SA_TOKEN}
接下来,我们设置上下文以与用户和命名空间绑定:
// Here we set K8S_CLUSTER=gke_devops-with-kubernetes_us-central1-b_cluster
# kubectl config set-context myctxt --cluster=${K8S_CLUSTER} --user=mysa
最后,我们将myctxt上下文设置为默认上下文:
// set the context to myctxt
# kubectl config use-context myctxt
当我们发送请求时,令牌将由 API 服务器进行验证,API 服务器会检查请求者是否符合资格并且确实是其声称的身份。让我们看看是否可以使用这个令牌列出默认命名空间中的 pods:
# kubectl get po
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:chapter5:myaccount" cannot list pods in the namespace "default"
看起来好像出了点问题!这是因为我们还没有为这个服务账户授予任何权限。稍后我们将在本章中学习如何使用Role和RoleBinding来做这件事。
用户账户认证
用户帐户认证有多种实现方式,包括客户端证书、承载令牌、静态文件以及 OpenID 连接令牌等。你可以选择多种方式作为认证链。在这里,我们将演示客户端证书的工作原理。
在第九章,持续交付,以及在本章早些时候,我们学习了如何为服务账户导出证书和令牌。现在,让我们学习如何为用户执行此操作。假设我们仍然在chapter5命名空间中,并且我们想为我们的新 DevOps 成员 Linda 创建一个用户,她将帮助我们为my-app进行部署:
- 首先,我们将通过 OpenSSL 生成一个私钥(
www.openssl.org):
// generate a private key for Linda
# openssl genrsa -out linda.key 2048
- 接下来,我们将为 Linda 创建一个证书签署请求(
.csr):
// making CN as your username
# openssl req -new -key linda.key -out linda.csr -subj "/CN=linda"
- 现在,
linda.key和linda.csr应该位于当前文件夹中。为了让 API 服务器验证 Linda 的证书,我们需要找到集群的 CA。
在 minikube 中,根 CA 位于~/.minikube/下。对于其他自托管解决方案,通常位于/etc/kubernetes/下。如果你使用kops来部署集群,路径通常位于/srv/kubernetes,你可以在/etc/kubernetes/manifests/kube-apiserver.manifest文件中找到该路径。在 GKE 中,集群的根 CA 是不可导出的。
假设我们在当前文件夹下有ca.crt和ca.key;通过使用它们,我们可以为用户生成新的 CA。通过使用-days参数,我们可以定义过期日期:
// generate the cert for Linda, this cert is only valid for 30 days.
# openssl x509 -req -in linda.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out linda.crt -days 30
Signature ok
subject=/CN=linda
Getting CA Private Key
在我们的集群签署证书后,我们可以在集群中设置一个用户,如下所示:
# kubectl config set-credentials linda --client-certificate=linda.crt --client-key=linda.key
User "linda" set.
记住上下文的概念:它是集群信息、用于认证的用户和命名空间的组合。现在,我们将在kubeconfig中设置一个上下文条目。记得在以下示例中替换集群名称、命名空间和用户:
# kubectl config set-context devops-context --cluster=${K8S_CLUSTER} --namespace=chapter5 --user=linda
Context "devops-context" modified.
现在,Linda 应该没有任何权限:
// test for getting a pod
# kubectl --context=devops-context get pods
Error from server (Forbidden): User "linda" cannot list pods in the namespace "chapter5". (get pods)
现在,Linda 可以通过认证阶段,因为 Kubernetes 知道她是 Linda。然而,为了给予 Linda 部署的权限,我们需要在授权模块中设置策略。
授权
Kubernetes 支持多个授权模块。在编写本书时,Kubernetes 支持以下模块:
-
ABAC
-
RBAC
-
节点授权
-
Webhook
-
自定义模块
基于属性的访问控制(ABAC)是基于角色的访问控制(RBAC)引入之前的主要授权模式。节点授权由 kubelet 用于向 API 服务器发出请求。Kubernetes 支持 Webhook 授权模式,以与外部 RESTful 服务建立 HTTP 回调。每当面临授权决策时,它将进行 POST 请求。另一种常见的做法是通过实现自定义模块,遵循预定义的授权接口。有关更多实现信息,请参考kubernetes.io/docs/admin/authorization/#custom-modules。在本节中,我们将介绍如何在 Kubernetes 中利用和使用 RBAC。
基于角色的访问控制(RBAC)
从 Kubernetes 1.6 开始,RBAC 默认启用。在 RBAC 中,管理员创建多个Roles或ClusterRoles,这些角色定义了细粒度的权限,指定一组资源和操作(动词),这些角色可以访问和操作这些资源。之后,管理员通过RoleBinding或ClusterRoleBindings将Role权限授予用户。
如果您正在运行 minikube,在使用minikube start时添加--extra-config=apiserver.Authorization.Mode=RBAC。如果您通过 kops 在 AWS 上运行自托管集群,请在启动集群时添加--authorization=rbac。Kops 会将 API 服务器作为 Pod 启动;使用kops edit cluster命令可以修改容器的spec。EKS 和 GKE 原生支持 RBAC。
角色和集群角色
Kubernetes 中的Role绑定在一个命名空间内。另一方面,ClusterRole是集群级别的。以下是一个Role的示例,它可以执行所有操作,包括get、watch、list、create、update、delete和patch,这些操作适用于deployments、replicasets和pods资源:
// configuration file for a role named devops-role
# cat 5-2_rbac/5-2_role.yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: chapter5
name: devops-role
rules:
- apiGroups: ["", "extensions", "apps"]
resources:
- "deployments"
- "replicasets"
- "pods"
verbs: ["*"]
// create the role
# kubectl create -f 5-2_rbac/5-2_role.yaml
role.rbac.authorization.k8s.io/devops-role created
在 GKE 中,管理员默认没有创建角色的权限。相反,您必须通过以下命令授予用户此权限:kubectl create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user ${USER_ACCOUNT}。
在 apiGroups 中,空字符串 [""] 表示核心 API 组。API 组是 RESTful API 调用的一部分。核心表示原始 API 调用路径,如 /api/v1。较新的 REST 路径包含了组名和 API 版本,例如 /apis/$GROUP_NAME/$VERSION。要查找你想要使用的 API 组,可以查看 API 参考文档 kubernetes.io/docs/reference。在 resources 下,你可以添加想要授予访问权限的 resources,在 verbs 下,你可以列出该角色可以执行的一组操作。让我们来看看一个更高级的 ClusterRoles 示例,我们在 第九章 持续交付 中使用过这个概念:
// configuration file for a cluster role
# cat 5-2_rbac/5-2_clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cd-role
rules:
- apiGroups: ["extensions", "apps"]
resources:
- deployments
- replicasets
- ingresses
verbs: ["*"]
- apiGroups: [""]
resources:
- namespaces
- events
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources:
- pods
- services
- secrets
- replicationcontrollers
- persistentvolumeclaims
- jobs
- cronjobs
verbs: ["*"]
// create the cluster role
# kubectl create -f 5-2_rbac/5-2_clusterrole.yaml
clusterrole.rbac.authorization.k8s.io/cd-role created
ClusterRole 是集群范围的。一些资源不属于任何命名空间,例如节点,只能通过 ClusterRole 控制。它可以访问的命名空间取决于它与 ClusterRoleBinding 关联的 namespaces 字段。在前面的示例中,我们授予了该角色在扩展和应用程序组中读写 deployments、replicasets 和 ingresses 的权限。在核心 API 组中,我们仅授予了命名空间和事件的访问权限,以及其他资源(如 Pods 和服务)的所有权限。
RoleBinding 和 ClusterRoleBinding
RoleBinding 将 Role 或 ClusterRole 绑定到一组用户或服务账户。如果 ClusterRole 被 RoleBinding 而不是 ClusterRoleBinding 绑定,它将仅在 RoleBinding 指定的命名空间内获得权限。
以下是 RoleBinding 规范的示例:
// configuration file for RoleBinding resource
# cat 5-2_rbac/rolebinding_user.yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: devops-role-binding
namespace: chapter5
subjects:
- kind: User
name: linda
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: devops-role
apiGroup: rbac.authorization.k8s.io
// create a RoleBinding for User linda
# kubectl create -f 5-2_rbac/rolebinding_user.yaml
rolebinding.rbac.authorization.k8s.io/devops-role-binding created
在这个例子中,我们通过 roleRef 将一个 Role 与用户绑定。这样,Linda 就获得了我们在 devops-role 中定义的权限。
另一方面,ClusterRoleBinding 用于在所有命名空间中授予权限。在这里,我们将使用 第九章 持续交付 中的相同概念。首先,我们创建了一个名为 cd-agent 的服务账户,然后创建了一个名为 cd-role 的 ClusterRole,并为 cd-agent 和 cd-role 创建了一个 ClusterRoleBinding。然后我们使用 cd-agent 代我们进行部署:
// configuration file for ClusterRoleBinding
# cat 5-2_rbac/cluster_rolebinding_user.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cd-agent
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cd-role
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: system:serviceaccount:cd:cd-agent
// create the ClusterRoleBinding
# kubectl create -f 5-2_rbac/cluster_rolebinding_user.yaml
clusterrolebinding.rbac.authorization.k8s.io/cd-agent created
cd-agent 通过 ClusterRoleBinding 与 ClusterRole 绑定,因此它可以跨命名空间获得在 cd-role 中指定的权限。由于服务账户是在一个命名空间中创建的,我们需要指定它的完整名称,包括其 namespace:
system:serviceaccount:<namespace>:<serviceaccountname>
ClusterRoleBinding 还支持 Group 作为主体。
现在,让我们尝试通过 devops-context 再次获取 Pods:
# kubectl --context=devops-context get pods
No resources found.
我们不再收到禁止访问的响应。那么,如果 Linda 想列出命名空间——这允许吗?
# kubectl --context=devops-context get namespaces
Error from server (Forbidden): User "linda" cannot list namespaces at the cluster scope. (get namespaces)
答案是否定的,因为 Linda 没有被授予列出命名空间的权限。
准入控制
在 Kubernetes 处理请求之前,且认证和授权通过后,会进行准入控制。通过添加--admission-control参数启动 API 服务器时启用该功能。Kubernetes 建议在集群版本大于或等于 1.10.0 时,集群中应包含以下插件:
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota
以下章节介绍了这些插件及其必要性。有关支持的准入控制插件的最新信息,请访问官方文档:kubernetes.io/docs/admin/admission-controllers。
NamespaceLifecycle
如前所述,当一个命名空间被删除时,所有该命名空间中的对象也会被驱逐。此插件确保在终止或不存在的命名空间中无法创建新对象。它还防止 Kubernetes 原生命名空间被删除。
LimitRanger
此插件确保 LimitRange 能正常工作。通过 LimitRange,我们可以在命名空间中设置默认的请求和限制,在启动 pod 时,如果未指定请求和限制,将使用这些默认值。
ServiceAccount
如果使用服务账户对象,必须添加服务账户插件。有关 ServiceAccount 的更多信息,请回顾本章节的 Service account token authentication 部分。
PersistentVolumeLabel
PersistentVolumeLabel 会根据底层云提供商提供的标签,为新创建的 PV 添加标签。自 1.8 版本以来,该准入控制器已被弃用。
DefaultStorageClass
该插件确保如果 PVC 中没有设置 StorageClass,默认的存储类能按预期工作。不同的云提供商会实现自己的 DefaultStorageClass(例如 GKE 使用 Google Cloud Persistent Disk)。确保启用了此功能。
ResourceQuota
就像 LimitRange 一样,如果你使用 ResourceQuota 对象来管理不同级别的 QoS,则必须启用此插件。ResourceQuota 应始终放在准入控制插件列表的末尾。正如我们在 ResourceQuota 部分提到的,ResourceQuota 用于限制每个命名空间的资源使用。将 ResourceQuota 控制器放在准入控制器列表的末尾,可以防止在请求最终被后面的控制器拒绝之前,过早地增加配额使用。
DefaultTolerationSeconds
DefaultTolerationSeconds 插件用于设置没有任何容忍设置的 pod。它会为 notready:NoExecute 和 unreachable:NoExecute 污点应用默认容忍,持续 300 秒。如果你不希望集群中发生此行为,可以禁用此插件。更多信息,请参阅 第八章 中的污点和容忍部分,资源管理和扩展。
PodNodeSelector
这个插件用于将node-selector注解设置到命名空间。当启用此插件时,使用--admission-control-config-file命令传递一个配置文件,格式如下:
podNodeSelectorPluginConfig:
clusterDefaultNodeSelector: <default-node-selectors-
labels>
namespace1: <namespace-node-selectors-labels-1>
namespace2: <namespace-node-selectors-labels-2>
这样,node-selector注解将应用于该namespace。该命名空间中的 Pod 将运行在匹配的节点上。
AlwaysPullImages
拉取策略定义了 kubelet 拉取镜像时的行为。默认的拉取策略是IfNotPresent,即如果镜像在本地不存在,则会拉取该镜像。如果启用了此插件,默认的拉取策略将变为Always,即始终拉取最新的镜像。如果集群由不同团队共享,这个插件还带来另一个好处:每当调度一个 Pod 时,它将始终拉取最新镜像,无论镜像是否已存在本地。这样,我们可以确保 Pod 创建请求始终通过对镜像的授权检查。
DenyEscalatingExec
这个插件可以防止任何kubectl exec或kubectl attach命令使 Pod 升级到特权模式。处于特权模式中的 Pod 可以访问主机命名空间,这可能会成为安全风险。
其他 Admission 控制插件
我们还可以使用许多其他 Admission 控制插件,例如NodeRestriction,用于限制 kubelet 的权限,ImagePolicyWebhook,用于建立 Webhook 来控制镜像访问,和SecurityContextDeny,用于控制 Pod 或容器的权限。有关其他插件的更多信息,请参阅官方文档:kubernetes.io/docs/admin/admission-controllers。
动态 Admission 控制
在 Kubernetes 1.7 之前,Admission controllers 是与 Kubernetes API 服务器一起编译的,因此它们只能在 API 服务器启动之前进行配置。动态 Admission 控制旨在打破这一限制。实现自定义动态 Admission 控制有两种方法:通过初始化器和 Admission webhook。初始化器 Webhook 可以监视未初始化的工作负载,并检查是否需要对其采取任何操作。
Admission webhook 拦截请求并检查其配置中的预设规则,然后决定是否允许请求。初始化器和 Admission webhook 都可以在某些操作上允许和修改资源请求,因此我们可以利用它们强制执行策略或验证请求是否满足组织的要求。故障的初始化器和 Admission webhook 可能会阻止所有目标资源的创建。然而,Admission webhook 提供了一种失败策略,当 Webhook 服务器没有按预期响应时,可以解决此问题。
在撰写本书时,Admission webhook 已经升至 beta 版本,但 Initializer 仍处于 alpha 阶段。在本节中,我们将实现一个简单的 Admission webhook 控制器,该控制器将在 pod 创建期间验证 {"chapter": "5"} 注解是否已设置到 podSpec 中。如果设置了注解,请求将继续;如果没有设置,请求将失败。
Admission webhook
实现 Admission webhook 控制器有两个主要组件:一个 webhook HTTP 服务器,用于接收资源生命周期事件,以及一个 ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration 资源配置文件。请参考 github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter5/5-3_admission-webhook/sample-validating-admission-webhook 获取我们的示例 Admission webhook 的源代码。
让我们看看如何编写 ValidatingWebhookConfiguration。正如我们在以下代码中看到的,像普通对象一样,ValidatingWebhookConfiguration 有一个 API 版本、一个类型和带有名称和标签的元数据。重要的部分是名为 webhooks 的部分。在 webhooks 中,需要定义一个或多个规则。在这里,我们定义了一个规则,该规则在任何 pod 创建请求时触发(operations=CREATE, resources=pods)。failurePolicy 用于确定在调用 webhook 时出现错误时的处理方式。failurePolicy 的选项为 Fail 或 ignore (Fail 表示使请求失败,而 ignore 表示忽略 webhook 错误)。clientConfig 部分定义了 webhook 服务器的端点。这里,我们利用了一个名为 sample-webhook-service-svc 的 Kubernetes 服务。如果是外部服务器,可以直接指定 URL,而不是使用服务。caBundle 用于验证 webhook 服务器的证书。如果未指定,默认情况下会使用 API 服务器的系统信任根证书。
要从 Kubernetes 服务导出 caBundle,请使用 kubectl get configmap -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d '\n' 命令,并按以下方式替换 ${CA_BUNDLE} 字段:
# cat chapter5/5-3_admission-webhook/sample-validating-admission-webhook/validatingwebhookconfiguration.yaml
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: sample-webhook-service
labels:
app: sample-webhook-service
webhooks:
- name: devops.kubernetes.com
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
failurePolicy: Fail
clientConfig:
service:
name: sample-webhook-service-svc
namespace: default
path: "/"
caBundle: ${CA_BUNDLE}
为了创建一个 webhook HTTP 服务器,我们在 Node.js 中创建了一个简单的 express Web 应用程序(expressjs.com/)。该应用的主要逻辑是接收 pod 创建事件并发送 admissionResponse (github.com/kubernetes/api/blob/master/admission/v1beta1/types.go#L81) 响应。这里,我们将返回一个带有 allowed 字段的 admissionResponse,该字段指示请求是否被允许或拒绝:
function webhook(req, res) {
var admissionRequest = req.body;
var object = admissionRequest.request.object;
var allowed = false;
if (!isEmpty(object.metadata.annotations) && !isEmpty(object.metadata.annotations.chapter) && object.metadata.annotations.chapter == "5") {
allowed = true;
}
var admissionResponse = {
allowed: allowed
};
for (var container of object.spec.containers) {
console.log(container.securityContext);
var image = container.image;
var admissionReview = {
response: admissionResponse
};
console.log("Response: " + JSON.stringify(admissionReview));
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(admissionReview));
res.status(200).end();
};
};
在之前的函数中,我们检查了object.metadata.annotations.chapter是否在 pod 中被注解,并且该章节是否等于5。如果是,webhook 服务器将通过请求。webhook 和 API 服务器需要建立相互信任。为了做到这一点,我们将通过请求certificates.k8s.io API 进行证书签名,从而为 webhook 服务器生成证书。流行的服务网格实现 Istio(istio.io/)提供了一个有用的工具(raw.githubusercontent.com/istio/istio/41203341818c4dada2ea5385cfedc7859c01e957/install/kubernetes/webhook-create-signed-cert.sh):
# wget https://raw.githubusercontent.com/istio/istio/41203341818c4dada2ea5385cfedc7859c01e957/install/kubernetes/webhook-create-signed-cert.sh
// create the cert
# sh webhook-create-signed-cert.sh --service sample-webhook-service-svc --secret sample-webhook-service-certs --namespace default
server-key.pem和server-cert.pem将在默认的temp文件夹下通过脚本生成。复制它们并将其放入我们示例 webhook HTTP 服务器中的src/keys文件夹内。现在,使用docker build -t $registry/$repository:$tag .通过 docker 构建应用程序,并将目标 docker 镜像推送到注册表(在此,我们使用了devopswithkubernetes/sample-webhook-service:latest)。完成此操作后,我们可以启动 web 服务器:
# cat chapter5/5-3_admission-webhook/sample-validating-admission-webhook/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-webhook-service
labels:
app: sample-webhook-service
spec:
replicas: 1
selector:
matchLabels:
app: sample-webhook-service
template:
metadata:
labels:
app: sample-webhook-service
spec:
containers:
- name: sample-webhook-service
image: devopswithkubernetes/sample-webhook-service:latest
imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: sample-webhook-service-svc
labels:
app: sample-webhook-service
spec:
ports:
- port: 443
targetPort: 443
selector:
app: sample-webhook-service
# kubectl create -f chapter5/5-3_admission-webhook/sample-validating-admission-webhook/deployment.yaml
deployment.apps/sample-webhook-service created
service/sample-webhook-service-svc created
在确认 pod 已启动并运行后,我们可以创建一个ValidatingWebhookConfiguration资源:
# kubectl create -f chapter5/5-3_admission-webhook/sample-validating-admission-webhook/validatingwebhookconfiguration.yaml
validatingwebhookconfiguration.admissionregistration.k8s.io/sample-webhook-service
created
# kubectl get validatingwebhookconfiguration
NAME CREATED AT
sample-webhook-service 2018-12-27T21:04:50Z
让我们尝试部署两个没有任何注解的nginx pod:
# cat chapter5/5-3_admission-webhook/sample-validating-admission-webhook/test-sample.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
#annotations:
# chapter: "5"
spec:
containers:
- name: nginx
image: nginx
# kubectl create -f chapter5/5-3_admission-webhook/sample-validating-admission-webhook/test-sample.yaml
Hdeployment.apps/nginx created
这应该会创建两个 pod;然而,我们没有看到任何nginx pod 被创建。我们只能看到我们的 webhook 服务 pod:
# kubectl get po
NAME READY STATUS RESTARTS AGE
sample-webhook-service-789d87b8b7-m58wq 1/1 Running 0 7h
如果我们检查相应的ReplicaSet,并使用kubectl describe rs $RS_NAME来检查事件,我们将得到以下结果:
# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-78f5d695bd 2 0 0 1m
sample-webhook-service-789d87b8b7 1 1 1 7h
# kubectl describe rs nginx-78f5d695bd
Name: nginx-78f5d695bd
Namespace: default
Selector: app=nginx,pod-template-hash=3491825168
Labels: app=nginx
pod-template-hash=3491825168
Annotations: deployment.kubernetes.io/desired-replicas: 2
deployment.kubernetes.io/max-replicas: 3
deployment.kubernetes.io/revision: 1
Controlled By: Deployment/nginx
Replicas: 0 current / 2 desired
Pods Status: 0 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=nginx
pod-template-hash=3491825168
Containers:
nginx:
Image: nginx
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
ReplicaFailure True FailedCreate
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 28s (x15 over 110s) replicaset-controller Error creating: Internal error occurred: admission webhook "devops.kubernetes.com" denied the request without explanation
从这里我们可以看出,准入 webhook 拒绝了请求。通过使用前述代码块中的uncomment注解来删除并重新创建部署:
// uncomment this annotation.
#annotations:
# chapter: "5"
如果我们这么做,应该能够看到nginx pod 被相应创建:
# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-978c784c5-v8xk9 0/1 ContainerCreating 0 2s
nginx-978c784c5-wrmdb 1/1 Running 0 2s
sample-webhook-service-789d87b8b7-m58wq 1/1 Running 0 7h
请求已通过认证、授权和准入控制,包括我们的 webhook 服务。pod 对象已相应创建并调度。
请记住,在测试动态准入控制器后要进行清理。它可能会阻止未来实验中的 pod 创建。
自定义资源
自定义资源首次在 Kubernetes 1.7 中引入,旨在作为一个扩展点,让用户创建自定义 API 对象,并作为本机 Kubernetes 对象使用。这样做是为了让用户能够扩展 Kubernetes,支持其应用程序或特定用例的自定义对象。自定义资源可以动态注册和注销。有两种方式可以创建自定义资源:使用 CRD 或聚合 API。CRD 要容易得多,而聚合 API 需要在 Go 中进行额外的编码。在本节中,我们将学习如何从零开始编写一个 CRD。
自定义资源定义
创建自定义资源定义(CRD)对象包括两个步骤:CRD 注册和对象创建。
让我们首先创建一个 CRD 配置:
# cat chapter5/5-4_crd/5-4-1_crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: books.devops.kubernetes.com
spec:
group: devops.kubernetes.com
version: v1alpha1
scope: Namespaced
names:
plural: books
singular: book
kind: Book
shortNames:
- bk
validation:
openAPIV3Schema:
required: ["spec"]
properties:
spec:
required: ["name", "edition"]
properties:
name:
type: string
minimum: 50
edition:
type: string
minimum: 10
chapter:
type: integer
minimum: 1
maximum: 2
使用CustomResourceDefinition,我们可以为自定义对象定义自己的规格。首先,我们需要决定 CRD 的名称。CRD 的命名规则必须是spec.names.plural+"."+spec.group。接下来,我们将定义组、版本、范围和名称。范围可以是Namespaced或Cluster(非命名空间)。在 Kubernetes 1.13 之后,我们可以添加验证部分,通过 OpenAPI v3 模式验证自定义对象(github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject)。我们可以定义所需字段以及每个字段的规格和验证条件。在前面的示例中,我们创建了一个名为books.devops.kubernetes.com的自定义对象,它有三个属性:name、edition 和 chapter。name和edition在对象创建时是必需的。让我们通过kubectl命令创建 CRD。我们还可以通过kubectl get crd命令列出所有的 CRD:
# kubectl create -f chapter5/5-4_crd/5-4-1_crd.yaml
customresourcedefinition.apiextensions.k8s.io/books.devops.kubernetes.com created
# kubectl get crd
NAME CREATED AT
backendconfigs.cloud.google.com 2018-12-22T20:48:00Z
books.devops.kubernetes.com 2018-12-28T16:14:34Z
scalingpolicies.scalingpolicy.kope.io 2018-12-22T20:48:30Z
接下来,我们将根据需要创建一个对象。在spec中,name和edition是必需的。apiVersion将是我们在前面的 CRD 配置中定义的<group>/<version>:
# cat chapter5/5-4_crd/5-4-2_objectcreation.yaml
apiVersion: devops.kubernetes.com/v1alpha1
kind: Book
metadata:
name: book-object
spec:
name: DevOps-with-Kubernetes
edition: second
# kubectl create -f chapter5/5-4_crd/5-4-2_objectcreation.yaml
book.devops.kubernetes.com/book-object created
如果我们将edition设置为second,将抛出一个错误,如下所示:
spec.edition in body must be of type string: "integer"
spec.edition in body should be greater than or equal to 10
现在,我们应该能够像普通 API 对象一样获取和描述它:
# kubectl get books
NAME AGE
book-object 3s
# kubectl describe books book-object
Name: book-object
Namespace: default
Labels: <none>
Annotations: <none>
API Version: devops.kubernetes.com/v1alpha1
Kind: Book
Metadata:
/apis/devops.kubernetes.com/v1alpha1/namespaces/default/books/book-object
UID: c6912ab5-0abd-11e9-be06-42010a8a0078
Spec:
Edition: second
Name: DevOps-with-Kubernetes
Events: <none>
注册 CRD 后,可能需要一个自定义控制器来处理自定义对象操作。自定义控制器需要额外的编程工作。社区中也有多种工具可以帮助我们创建骨架控制器,例如以下工具:
-
Kubebuilder:
github.com/kubernetes-sigs/kubebuilder
使用示例控制器(由 Kubernetes 提供),将一组ResourceEventHandlerFuncs添加到EventHandler中,用于处理对象生命周期事件,如AddFunc、UpdateFunc和DeleteFunc。
Kubebuilder和Operator都可以简化前面的步骤。Kubebuilder 提供支持通过 CRD、控制器和准入 webhook 构建 API。Operator 是由 CoreOS 引入的应用程序特定控制器,它是通过 CRD 实现的。社区中已有许多操作器被实现,并可以在github.com/operator-framework/awesome-operators找到。我们将介绍如何利用 Operator 框架中的 operator SDK 来构建一个简单的控制器,并使用相同的书籍 CRD。
首先,我们需要安装操作员 SDK(github.com/operator-framework/operator-sdk)。在以下示例中,我们使用的是 v.0.3.0 版本:
// Check the prerequisites at https://github.com/operator-framework/operator-sdk#prerequisites
# mkdir -p $GOPATH/src/github.com/operator-framework
# cd $GOPATH/src/github.com/operator-framework
# git clone https://github.com/operator-framework/operator-sdk
# cd operator-sdk
# git checkout master
# make dep
# make install
// check version
# operator-sdk --version
# operator-sdk version v0.3.0+git
让我们通过以下命令创建一个名为devops-operator的新操作员:
// operator-sdk new <operator_name>
# operator-sdk new devops-operator
INFO[0076] Run git init done
INFO[0076] Project creation complete.
操作员初始化后,我们可以开始向其中添加组件。让我们添加api来创建 API 对象,并添加controller来处理对象操作:
// operator-sdk add api --api-version <group>/<version> --kind <kind>
# operator-sdk add api --api-version devops.kubernetes.com/v1alpha1 --kind Book
INFO[0000] Generating api version devops.kubernetes.com/v1alpha1 for kind Book.
INFO[0000] Create pkg/apis/devops/v1alpha1/book_types.go
INFO[0000] Create pkg/apis/addtoscheme_devops_v1alpha1.go
INFO[0000] Create pkg/apis/devops/v1alpha1/register.go
INFO[0000] Create pkg/apis/devops/v1alpha1/doc.go
INFO[0000] Create deploy/crds/devops_v1alpha1_book_cr.yaml
INFO[0000] Create deploy/crds/devops_v1alpha1_book_crd.yaml
INFO[0008] Running code-generation for Custom Resource group versions: [devops:[v1alpha1], ]
INFO[0009] Code-generation complete.
INFO[0009] Api generation complete.
# operator-sdk add controller --api-version devops.kubernetes.com/v1alpha1 --kind Book
INFO[0000] Generating controller version devops.kubernetes.com/v1alpha1 for kind Book.
INFO[0000] Create pkg/controller/book/book_controller.go
INFO[0000] Create pkg/controller/add_book.go
INFO[0000] Controller generation complete.
有多个文件需要修改。第一个是 API 规范。在之前的 CRD 示例中,我们在book资源中添加了三个自定义属性:name、edition和chapter。我们也需要在这里的 spec 中添加它。这可以在pkg/apis/devops/v1alpha1/book_types.go中找到:
// in pkg/apis/devops/v1alpha1/book_types.go
type BookSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
Name string `json:"name"`
Edition string `json:"edition"`
Chapter int32 `json:"chapter"`
}
修改文件后,运行operator-sdk generate k8s,如前面的代码所示。接下来,我们将向控制器逻辑中添加一些自定义逻辑。这部分位于pkg/controller/book/book_controller.go的Reconcile函数中。
在框架创建的现有示例中,控制器将接收podSpec。这正是我们需要的,我们只需从Spec中获取name和edition,并将其打印到busybox的stdout中:
// in pkg/controller/book/book_controller.go
func newPodForCR(cr *devopsv1alpha1.Book) *corev1.Pod {
labels := map[string]string{
"app": cr.Name,
}
name := cr.Spec.Name
edition := cr.Spec.Edition
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name + "-pod",
Namespace: cr.Namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "busybox",
Image: "busybox",
Command: []string{"echo", "Please support", name, edition, "Edition :-) "},
Stdin: true,
},
},
},
}
}
然后,我们可以运行operator-sdk build devopswithkubernetes/sample-operator来构建 Docker 镜像并将其推送到镜像仓库。这里,我们将其推送到公共的 Docker Hub,docker push devopswithkubernetes/sample-operator。
操作员已完成!之后,我们可以开始研究如何部署它。部署脚本会自动创建在deploy文件夹中。我们需要修改的文件是operator.yaml,它指定了操作员容器镜像。找到image: REPLACE_IMAGE行并更新,使其指向你的镜像仓库(这里,我们将其指向devopswithkubernetes/sample-operator)。现在,可以进行部署了:
# kubectl create -f deploy/service_account.yaml
# kubectl create -f deploy/role.yaml
# kubectl create -f deploy/role_binding.yaml
# kubectl create -f deploy/crds/app_v1alpha1_appservice_crd.yaml
# kubectl create -f deploy/operator.yaml
现在,当列出 Pod 时,你应该能够看到一个操作员 Pod:
# kubectl get po
NAME READY STATUS RESTARTS AGE
devops-operator-58476dbcdd-s5m5v 1/1 Running 0 41m
现在,让我们创建一个Book资源。你可以修改当前文件夹中的deploy/crds/devops_v1alpha1_book_cr.yaml,或者重用我们仓库中的5-4_crd/5-4-2_objectcreation.yaml:
# kubectl create -f deploy/crds/devops_v1alpha1_book_cr.yaml
book.devops.kubernetes.com/book-object created
然后,我们应该能够看到 CRD 创建了另一个 Pod,我们也可以查看它的日志:
# kubectl get po
NAME READY STATUS RESTARTS AGE
book-object-pod 0/1 Completed 0 2s
devops-operator-58476dbcdd-s5m5v 1/1 Running 0 45m
# kubectl logs book-object-pod
Please support DevOps-with-Kubernetes second Edition :-)
万岁!一切看起来都很不错。这里,我们展示了一个非常简单的示例。当然,我们可以利用这个概念,发展出更复杂的逻辑和处理器。
应用 CRD:
一个容器化的应用可能包含多个 Kubernetes 资源,例如部署、服务、ConfigMaps、secrets,以及自定义 CRD。在github.com/kubernetes-sigs/application中实现了一个应用 CRD,它提供了一个桥梁,使应用的元数据可以被描述。它还具有应用级别的健康检查,这样用户就不需要在部署后列出所有资源并检查应用是否正确部署。相反,用户只需列出应用 CRD 并检查其状态。
摘要
在本章中,我们学习了namespace和context的概念,了解了它们的工作原理,以及如何通过设置上下文在物理集群和虚拟集群之间切换。接着,我们学习了一个重要的对象——服务账户,它提供了识别在 pod 内运行的进程的能力。然后,我们熟悉了如何控制 Kubernetes 中的访问流。我们了解了认证与授权的区别,以及它们在 Kubernetes 中是如何工作的。我们还学习了如何利用 RBAC 为用户提供细粒度的权限。此外,我们还了解了一些 admission controller 插件和动态准入控制,它们是访问控制流中的最后一道防线。最后,我们学习了 CRD 的概念,并通过操作员 SDK (github.com/operator-framework/operator-sdk) 在操作员框架中实现了它和它的控制器。
在第六章,Kubernetes 网络,我们将继续深入学习集群网络。
第六章:Kubernetes 网络
在第三章,Kubernetes 入门中,我们学习了如何部署具有不同资源的容器,还了解了如何使用卷来持久化数据、动态配置、不同的存储类和 Kubernetes 中的高级管理。在本章中,我们将学习 Kubernetes 如何路由流量以实现这一切。网络在软件世界中始终扮演着重要角色。我们将一步一步地了解 Kubernetes 网络,探讨容器在单一主机、多主机和集群内的通信。
本章将涵盖以下主题:
-
Kubernetes 网络
-
Docker 网络
-
入口流量
-
网络策略
-
服务网格
Kubernetes 网络
在 Kubernetes 中实现网络有很多选择。Kubernetes 本身并不关心你如何实现网络,但你必须满足其三个基本要求:
-
所有容器应该能够相互访问,而无需进行 NAT,无论它们位于哪个节点上。
-
所有节点应该能够与所有容器通信
-
IP 容器应该像外部看到它一样看待自己。
在深入了解 Kubernetes 网络之前,我们首先检查一下默认容器网络是如何工作的。
Docker 网络
现在,让我们回顾一下 Docker 网络是如何工作的,再进入 Kubernetes 网络。对于容器网络,有不同的模式:bridge、none、overlay、macvlan 和 host。我们在第二章,DevOps 与容器中了解了主要的网络模式。Bridge 是默认的网络模式。Docker 会创建并附加一个虚拟以太网设备(也叫做 veth),并为每个容器分配一个网络命名空间。
网络命名空间 是 Linux 中的一个特性,它逻辑上是另一个网络栈的副本。它有自己的路由表、ARP 表和网络设备。这是容器网络的基本概念。
Veth 总是成对出现;一个在网络命名空间中,另一个在桥接中。当流量进入主机网络时,它会被路由到桥接中。数据包会被转发到它的 veth,然后进入容器内部的命名空间,如下图所示:

让我们仔细看看这个问题。在以下示例中,我们将使用 minikube 节点作为 docker 主机。首先,我们必须使用 minikube ssh 登录到节点,因为我们还没有使用 Kubernetes。进入 minikube 节点后,我们将启动一个容器来与我们互动:
// launch a busybox container with `top` command, also, expose container port 8080 to host port 8000.
# docker run -d -p 8000:8080 --name=busybox busybox top
737e4d87ba86633f39b4e541f15cd077d688a1c8bfb83156d38566fc5c81f469
让我们来看一下容器内的出站流量实现。docker exec <container_name 或 container_id> 可以在运行中的容器中执行命令。我们使用 ip link list 来列出所有的网络接口:
// show all the network interfaces in busybox container
// docker exec <container_name> <command>
# docker exec busybox ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: sit0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1
link/sit 0.0.0.0 brd 0.0.0.0
53: eth0@if54: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN>
mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:07 brd ff:ff:ff:ff:ff:ff
我们可以看到在 busybox 容器内有三个接口。其中一个接口的 ID 为 53,名称为 eth0@if54。if 后面的数字是配对接口的 ID,在这个例子中,配对 ID 为 54。如果我们在主机上运行相同的命令,可以看到主机上的 veth 正指向容器内的 eth0:
// show all the network interfaces from the host
# ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
state UNKNOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
pfifo_fast state UP mode DEFAULT group default qlen
1000
link/ether 08:00:27:ca:fd:37 brd ff:ff:ff:ff:ff:ff
...
54: vethfeec36a@if53: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu 1500 qdisc noqueue master docker0 state UP mode
DEFAULT group default
link/ether ce:25:25:9e:6c:07 brd ff:ff:ff:ff:ff:ff link-netnsid 5
我们在主机上有一个名为 vethfeec36a@if53 的 veth。它与容器网络命名空间中的 eth0@if54 配对。veth 54 被附加到 docker0 桥接网络,最终通过 eth0 访问互联网。如果我们查看 iptables 规则,可以找到 docker 为出站流量创建的伪装规则(也称为 SNAT),该规则将使容器能够访问互联网:
// list iptables nat rules. Showing only POSTROUTING rules which allows packets to be altered before they leave the host.
# sudo iptables -t nat -nL POSTROUTING
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
...
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
...
另一方面,关于入站流量,docker 在 prerouting 上创建了一个自定义过滤链,并在 DOCKER 过滤链中动态创建转发规则。如果我们暴露一个容器端口 8080,并将其映射到主机端口 8000,我们可以看到我们正在监听任何 IP 地址(0.0.0.0/0)上的端口 8000,该流量将被路由到容器端口 8080:
// list iptables nat rules
# sudo iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
...
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
...
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
...
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
...
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8000 to:172.17.0.7:8080
...
现在我们知道了数据包是如何进出容器的,接下来我们来看看 Pod 内的容器如何相互通信。
用户自定义的自定义桥接网络:
除了默认的桥接网络外,docker 还支持用户自定义的桥接网络。用户可以动态创建自定义桥接网络。这提供了更好的网络隔离,支持通过内置 DNS 服务器进行 DNS 解析,并且可以在运行时附加或分离容器。更多信息,请参考以下文档:docs.docker.com/network/bridge/#manage-a-user-defined-bridge。
容器间通信
Kubernetes 中的 Pod 拥有各自的真实 IP 地址。Pod 内的容器共享网络命名空间,因此它们相互视为 localhost。默认情况下,这是通过 网络容器 实现的,网络容器充当每个容器的流量分发桥梁。让我们通过以下示例来看看这是如何工作的。我们使用 第三章 中的第一个示例,Kubernetes 入门,其中包含一个 Pod 内的两个容器 nginx 和 centos:
#cat 6-1-1_pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
containers:
- name: web
image: nginx
- name: centos
image: centos
command: ["/bin/sh", "-c", "while : ;do curl http://localhost:80/; sleep 10; done"]
// create the Pod
#kubectl create -f 6-1-1_pod.yaml
pod/example created
然后,我们将描述该 Pod 并查看其 Container ID:
# kubectl describe pods example
Name: example
Node: minikube/192.168.99.100
...
Containers:
web:
Container ID: docker:// d9bd923572ab186870284535044e7f3132d5cac11ecb18576078b9c7bae86c73
Image: nginx
...
centos:
Container ID: docker: //f4c019d289d4b958cd17ecbe9fe22a5ce5952cb380c8ca4f9299e10bf5e94a0f
Image: centos
...
在这个例子中,web 的容器 ID 是 d9bd923572ab,而 centos 的容器 ID 是 f4c019d289d4。如果我们进入 minikube/192.168.99.100 节点并使用 docker ps,我们可以检查 Kubernetes 实际启动了多少个容器,因为我们处于 minikube 环境,它启动了许多其他集群容器。通过查看 CREATED 列,我们可以看到有三个刚刚启动的容器:
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f4c019d289d4 36540f359ca3 "/bin/sh -c 'while : " 2 minutes ago Up 2 minutes k8s_centos_example_default_9843fc27-677b-11e7-9a8c-080027cafd37_1
d9bd923572ab e4e6d42c70b3 "nginx -g 'daemon off" 2 minutes ago Up 2 minutes k8s_web_example_default_9843fc27-677b-11e7-9a8c-080027cafd37_1
4ddd3221cc47 gcr.io/google_containers/pause-amd64:3.0 "/pause" 2 minutes ago Up 2 minutes
还有一个额外的容器,4ddd3221cc47,它已被启动。在深入了解这个容器之前,让我们先查看 web 容器的网络模式。我们会发现,示例 Pod 中的容器都在映射容器模式下运行:
# docker inspect d9bd923572ab | grep NetworkMode
"NetworkMode": "container:4ddd3221cc4792207ce0a2b3bac5d758a5c7ae321634436fa3e6dd627a31ca76",
4ddd3221cc47 容器在本例中是所谓的网络容器。它持有网络命名空间,使得 web 和 centos 容器可以连接。处于同一网络命名空间中的容器共享相同的 IP 地址和网络配置。这是 Kubernetes 实现容器间通信的默认方式,映射到第一个要求。
Pod 到 Pod 的通信
Pod 的 IP 地址可以从其他 Pod 访问,无论它们位于哪个节点上。这符合第二个要求。我们将在接下来的章节中描述同一节点内以及跨节点的 Pod 通信。
同一节点内的 Pod 通信
同一节点内的 Pod 通信默认通过桥接进行。假设我们有两个 Pod,各自拥有自己的网络命名空间。当 Pod 1 想要与 Pod 2 通信时,数据包首先通过 Pod 1 的命名空间,传递到相应的 veth 对 **vethXXXX**,最终到达桥接。然后,桥接会广播目标 IP,帮助数据包找到路径。vethYYYY 响应广播。数据包随后到达 Pod 2:

现在我们了解了数据包在单个节点中的传输方式,接下来我们将讨论当 Pods 在不同节点上时,流量是如何路由的。
跨节点的 Pod 通信
根据第二个要求,所有节点必须能够与所有容器通信。Kubernetes 将实现委托给容器网络接口(CNI)。用户可以选择不同的实现方式,如 L2、L3 或覆盖网络。覆盖网络,也称为数据包封装,是最常见的解决方案之一。它在数据包离开源节点之前进行封装,传送后在目标节点解封。这样会导致覆盖网络增加网络延迟和复杂性。只要所有容器能够在不同节点之间互相访问,你可以自由选择任何技术,比如 L2 邻接或 L3 网关。关于 CNI 的更多信息,请参考其规范(github.com/containernetworking/cni/blob/master/SPEC.md):

假设我们有一个数据包从 Pod 1 发送到 Pod 4。数据包离开容器接口,经过veth对,然后通过桥接器和节点的网络接口。在第 4 步中,网络实现开始发挥作用。只要数据包能够被路由到目标节点,你可以自由选择任何选项。在以下的例子中,我们将启动 minikube 并使用--network-plugin=cni选项。启用 CNI 后,参数将通过节点中的 kubelet 传递。Kubelet 有一个默认的网络插件,但你可以在启动时探测任何支持的插件。在启动minikube之前,如果它已经启动,你可以先使用minikube stop停止,或者使用minikube delete完全删除整个集群,然后再进行其他操作。虽然minikube是一个单节点环境,可能无法完全代表我们将遇到的生产环境,但它可以帮助你大致了解这些工作的原理。我们将在第九章《持续交付》和第十章《AWS 上的 Kubernetes》中了解网络选项在实际部署中的应用:
// start minikube with cni option
# minikube start --network-plugin=cni
...
Loading cached images from config file.
Everything looks great. Please enjoy minikube!
当我们指定network-plugin选项时,minikube会在启动时使用--network-plugin-dir中指定的目录来加载插件。在 CNI 插件中,默认的插件目录是/opt/cni/net.d。集群启动后,我们可以通过minikube ssh登录到节点,查看 minikube 中的网络接口配置:
# minikube ssh
$ ifconfig
...
mybridge Link encap:Ethernet HWaddr 0A:58:0A:01:00:01
inet addr:10.1.0.1 Bcast:0.0.0.0
Mask:255.255.0.0
...
我们会发现节点中新增了一个桥接器,如果我们再次使用5-1-1_pod.yml创建示例 Pod,我们会发现 Pod 的 IP 地址变成了10.1.0.x,并附加到mybridge上,而不是docker0:
# kubectl create -f 6-1-1_pod.yaml
pod/example created
# kubectl describe po example
Name: example
Namespace: default
Node: minikube/10.0.2.15
Start Time: Sun, 23 Jul 2017 14:24:24 -0400
Labels: <none>
Annotations: <none>
Status: Running
IP: 10.1.0.4
这是因为我们已经指定使用 CNI 作为网络插件,并且docker0将不会被使用(也叫做容器网络模型,或libnetwork)。CNI 会创建一个虚拟接口,将其连接到基础网络,设置 IP 地址,并最终将其路由并映射到 Pod 的命名空间。我们来看看位于/etc/cni/net.d/k8s.conf的配置文件,这在 minikube 中:
# cat /etc/cni/net.d/k8s.conf
{
"name": "rkt.kubernetes.io",
"type": "bridge",
"bridge": "mybridge",
"mtu": 1460,
"addIf": "true",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1",
"routes": [
{
"dst": "0.0.0.0/0"
}
]
}
}
在这个例子中,我们使用了 bridge CNI 插件来重用 L2 桥接器用于 Pod 容器。如果数据包来自10.1.0.0/16,并且其目的地是任何地方,它将通过这个网关。就像我们之前看到的图示一样,我们可以有另一个启用了 CNI 的节点,使用10.1.2.0/16子网,这样 ARP 数据包就可以发送到目标 Pod 所在节点的物理接口。这样就实现了跨节点的 Pod 对 Pod 通信。
我们来检查一下iptables中的规则:
// check the rules in iptables
# sudo iptables -t nat -nL
...
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
KUBE-POSTROUTING all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
CNI-25df152800e33f7b16fc085a all -- 10.1.0.0/16 0.0.0.0/0 /* name: "rkt.kubernetes.io" id: "328287949eb4d4483a3a8035d65cc326417ae7384270844e59c2f4e963d87e18" */
CNI-f1931fed74271104c4d10006 all -- 10.1.0.0/16 0.0.0.0/0 /* name: "rkt.kubernetes.io" id: "08c562ff4d67496fdae1c08facb2766ca30533552b8bd0682630f203b18f8c0a" */
所有相关的规则已经切换为10.1.0.0/16的 CIDR。
Pod 与服务的通信
Kubernetes 是动态的,pod 会不断创建和删除。Kubernetes 服务是一种抽象,用于通过标签选择器定义一组 pod。我们通常使用服务来访问 pod,而不是明确指定一个 pod。当我们创建服务时,会创建一个endpoint对象,描述该服务中标签选择器所选择的一组 pod IP。
在某些情况下,endpoint对象在服务创建时不会自动创建。例如,缺少选择器的服务将不会创建相应的endpoint对象。有关更多信息,请参考第三章中的没有选择器的服务部分,Kubernetes 入门。
那么,流量是如何从 pod 传输到服务后面的 pod 的呢?默认情况下,Kubernetes 使用iptables来实现这个过程,并通过kube-proxy来执行。下面的图示解释了这一点:

让我们重用来自第三章的3-2-3_rc1.yaml和3-2-3_nodeport.yaml示例,Kubernetes 入门,来观察默认行为:
// create two pods with nginx and one service to observe default networking. Users are free to use any other kind of solution.
# kubectl create -f chapter3/3-2-3_Service/3-2-3_rs1.yaml
replicaset.apps/nginx-1.12 created
# kubectl create -f chapter3/3-2-3_Service/3-2-3_nodeport.yaml
service/nginx-nodeport created
让我们观察一下iptables规则,看看它是如何工作的。正如下面的代码所示,我们的服务 IP 是10.0.0.167,底下两个 pod 的 IP 地址分别是10.1.0.4和10.1.0.5:
# kubectl describe svc nginx-nodeport
Name: nginx-nodeport
Namespace: default
Selector: project=chapter3,service=web
Type: NodePort
IP: 10.0.0.167
Port: <unset> 80/TCP
NodePort: <unset> 32261/TCP
Endpoints: 10.1.0.4:80,10.1.0.5:80
...
让我们通过使用minikube ssh进入 minikube 节点,并检查其iptables规则:
# sudo iptables -t nat -nL
...
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-SVC-37ROJ3MK6RKFMQ2B tcp -- 0.0.0.0/0 10.0.0.167 /* default/nginx-nodeport: cluster IP */ tcp dpt:80
KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
Chain KUBE-SVC-37ROJ3MK6RKFMQ2B (2 references)
target prot opt source destination
KUBE-SEP-SVVBOHTYP7PAP3J5 all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-nodeport: */ statistic mode random probability 0.50000000000
KUBE-SEP-AYS7I6ZPYFC6YNNF all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-nodeport: */
Chain KUBE-SEP-SVVBOHTYP7PAP3J5 (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.1.0.4 0.0.0.0/0 /* default/nginx-nodeport: */
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-nodeport: */ tcp to:10.1.0.4:80
Chain KUBE-SEP-AYS7I6ZPYFC6YNNF (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.1.0.5 0.0.0.0/0 /* default/nginx-nodeport: */
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-nodeport: */ tcp to:10.1.0.5:80
...
关键点在于,服务将集群 IP 暴露给来自KUBE-SVC-37ROJ3MK6RKFMQ2B的外部流量,后者链接到两个自定义链,KUBE-SEP-SVVBOHTYP7PAP3J5和KUBE-SEP-AYS7I6ZPYFC6YNNF,并且其统计模式随机概率为 0.5。意味着iptables会生成一个随机数,并根据 0.5 的概率分布调节该数值到目标。这两个自定义链的DNAT目标被设置为相应的 pod IP。DNAT目标负责改变数据包的目标 IP 地址。默认情况下,conntrack 会启用,跟踪连接的目标和源地址。当流量到达服务时,iptables会随机选择一个 pod 进行路由,并将目标 IP 从服务 IP 修改为真实的 pod IP。然后,它接收响应,并对回复数据包进行反DNAT操作,发送回请求者。
基于 IPVS 的 kube-proxy:
在 Kubernetes 1.11 中,基于 IPVS 的 kube-proxy 功能正式发布。这可以解决 iptables 在处理数万个服务时的扩展问题。IP 虚拟服务器(IPVS)是 Linux 内核的一部分,可以将 TCP 或 UDP 请求引导到实际的服务器上。如果你的应用包含大量服务,ipvs 代理非常适合。然而,在某些特定情况下,它会回退到 iptables。有关更多信息,请参考 kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/。
外部到服务的通信
将外部流量引入 Kubernetes 是至关重要的。Kubernetes 提供了两个 API 对象来实现这一目标:
-
Service:外部网络 LoadBalancer 或 NodePort(L4)
-
Ingress:HTTP(S) LoadBalancer(L7)
我们将在下一节中深入学习 ingress。现在,我们将重点讨论 L4 服务。基于我们对跨节点 pod 之间通信的理解,数据包在服务和 pod 之间进出。以下图示展示了这一过程。假设我们有两个服务:服务 A 有三个 pod(pod a、pod b 和 pod c),而服务 B 只有一个 pod(pod d)。当流量从 LoadBalancer 进入时,数据包会被调度到某个节点。大多数 LoadBalancer 本身并不了解 pod 或容器,它只知道节点。如果节点通过健康检查,那么它就会成为目标节点的候选节点。
假设我们想要访问服务 B;目前它只有一个 pod 在一个节点上运行。然而,LoadBalancer 将数据包发送到另一个没有运行我们需要的 pod 的节点。在这种情况下,流量路由将如下所示:

数据包的路由过程如下:
-
LoadBalancer 会选择一个节点将数据包转发出去。在 GCE 中,它基于源 IP 和端口、目标 IP 和端口、协议的哈希值选择实例;在 AWS 中,负载均衡基于轮询算法。
-
在这里,路由目标将被更改为 pod d(DNAT),并将数据包转发到另一个节点,类似于跨节点的 pod 到 pod 通信。
-
然后是服务到 pod 的通信。数据包到达 pod d 后,pod d 会返回响应。
-
Pod 到服务的通信也通过
iptables来操作。 -
数据包将被转发回原节点。
-
源和目标将被取消 DNAT,返回给 LoadBalancer 和客户端,并一路发送回请求者。
从 Kubernetes 1.7 开始,这项服务中新增了一个属性 externalTrafficPolicy。在这里,你可以将其值设置为 local,然后,流量进入节点后,Kubernetes 会尽可能地将流量路由到该节点上的 pod,如下所示:
kubectl patch $service_name nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'
入口
Kubernetes 中的 Pods 和服务有自己的 IP 地址。然而,这通常不是你提供给外部互联网的接口。尽管有一个配置了节点 IP 的服务,但节点 IP 中的端口在各个服务之间不能重复。决定哪个端口与哪个服务对应是很麻烦的。此外,节点是动态变化的;向外部服务提供静态的节点 IP 并不是一个明智的选择。
入口定义了一组规则,允许入站连接访问 Kubernetes 集群服务。它在 L7 层将流量引入集群,并在每个虚拟机上分配并转发一个端口到服务端口。如下图所示,我们定义一组规则并将其作为源类型的入口发送到 API 服务器。当流量进入时,入口控制器将根据入口规则来执行和路由入口。如下图所示,入口用于通过不同的 URL 将外部流量路由到 Kubernetes 端点:

现在,我们将通过一个示例来看看这个如何工作。在这个示例中,我们将创建两个名为nginx和echoserver的服务,并配置入口路径/welcome和/echoserver。我们可以在minikube中运行这个。旧版本的minikube默认没有启用入口;我们需要先启用它:
// start over our minikube local
# minikube delete && minikube start
// enable ingress in minikube
# minikube addons enable ingress
ingress was successfully enabled
// check current setting for addons in minikube
# minikube addons list
- registry: disabled
- registry-creds: disabled
- addon-manager: enabled
- dashboard: enabled
- default-storageclass: enabled
- kube-dns: enabled
- heapster: disabled
- ingress: enabled
在minikube中启用入口将创建一个nginx入口控制器(github.com/kubernetes/ingress-nginx)以及一个ConfigMap来存储nginx配置,还有一个Deployment和服务作为默认的 HTTP 后端,用于处理未映射的请求。我们可以通过在kubectl命令中添加--namespace=kube-system来观察这些资源。接下来,让我们创建后端资源。以下是我们的nginx Deployment和Service:
# cat chapter6/6-2-1_nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
template:
metadata:
labels:
project: chapter6
service: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: nginx
spec:
type: NodePort
selector:
project: chapter6
service: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
// create nginx RS and service
# kubectl create -f chapter6/6-2-1_nginx.yaml
deployment.apps/nginx created
service/nginx created
然后,我们将创建另一个带有Deployment的服务:
// another backend named echoserver
# cat chapter6/6-2-1_echoserver.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echoserver
spec:
replicas: 1
template:
metadata:
name: echoserver
labels:
project: chapter6
service: echoserver
spec:
containers:
- name: echoserver
image: gcr.io/google_containers/echoserver:1.4
ports:
- containerPort: 8080
---
kind: Service
apiVersion: v1
metadata:
name: echoserver
spec:
type: NodePort
selector:
project: chapter6
service: echoserver
ports:
- protocol: TCP
port: 8080
targetPort: 8080
// create RS and SVC by above configuration file
# kubectl create -f chapter6/6-2-1_echoserver.yaml
deployment.apps/echoserver created
service/echoserver created
接下来,我们将创建入口资源。有一个名为nginx.ingress.kubernetes.io/rewrite-target的注解。如果service请求来自根 URL,则这是必需的。如果没有重写注解,我们将收到 404 响应。有关更多支持的注解,请参见github.com/kubernetes/ingress-nginx/tree/master/docs/examples/rewrite:
# cat chapter6/6-2-1_ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-example
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: devops.k8s
http:
paths:
- path: /welcome
backend:
serviceName: nginx
servicePort: 80
- path: /echoserver
backend:
serviceName: echoserver
servicePort: 8080
// create ingress
# kubectl create -f chapter6/6-2-1_ingress.yaml
ingress.extensions/ingress-example created
在一些云提供商中,支持服务 LoadBalancer 控制器。这可以通过配置文件中的status.loadBalancer.ingress语法与入口集成。有关更多信息,请参阅github.com/kubernetes/contrib/tree/master/service-loadbalancer。
由于我们的主机设置为 devops.k8s,只有从该主机名访问时才能返回。你可以在 DNS 服务器中配置 DNS 记录,或修改本地主机文件。为了简单起见,我们只需在主机文件中添加一行,格式为 ip 主机名:
// normally host file located in /etc/hosts in linux
# sudo sh -c "echo `minikube ip` devops.k8s >> /etc/hosts"
然后,我们应该能够直接通过 URL 访问我们的服务:
# curl http://devops.k8s/welcome
...
<title>Welcome to nginx!</title>
...
// check echoserver
# curl http://devops.k8s/echoserver
CLIENT VALUES:
client_address=172.17.0.4
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://devops.k8s:8080/
Pod ingress 控制器根据 URL 路径分发流量。路由路径类似于外部到服务的通信。数据包在节点和 pod 之间跳跃。Kubernetes 是可插拔的;有很多第三方实现正在进行。即使 iptables 只是默认的常见实现,我们这里只是触及了表面。每个版本的发布都会使网络发生很大变化。在撰写本文时,Kubernetes 刚刚发布了 1.13 版本。
网络策略
网络策略充当 pod 的软件防火墙。默认情况下,每个 pod 都可以互相通信,没有任何边界。网络策略是你可以对这些 pod 应用的隔离之一。它定义了通过命名空间选择器和 pod 选择器,谁可以访问哪个 pod 的哪个端口。命名空间中的网络策略是累加的,一旦 pod 启用网络策略,它将拒绝任何其他入站流量(也称为拒绝所有)。
目前,有多个网络提供商支持网络策略,如 Calico(www.projectcalico.org/calico-network-policy-comes-to-kubernetes/)、Romana(github.com/romana/romana)、Weave Net(www.weave.works/docs/net/latest/kube-addon/#npc)、Contiv(contiv.github.io/documents/networking/policies.html)和 Trireme(github.com/aporeto-inc/trireme-kubernetes)。用户可以自由选择其中任何一种。不过,为了简单起见,我们将使用 Calico 配合 minikube。为此,我们需要使用 --network-plugin=cni 选项启动 minikube。此时,网络策略在 Kubernetes 中仍然是比较新的功能。我们正在运行 Kubernetes 版本 v.1.7.0,配合 v.1.0.7 minikube ISO 通过自托管解决方案部署 Calico(docs.projectcalico.org/v1.5/getting-started/kubernetes/installation/hosted/)。Calico 可以通过 etcd 数据存储或 Kubernetes API 数据存储进行安装。为了方便起见,我们将在这里演示如何使用 Kubernetes API 数据存储安装 Calico。由于 minikube 中启用了 rbac,我们需要为 Calico 配置角色和绑定:
# kubectl apply -f \
https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yaml
clusterrole.rbac.authorization.k8s.io/calico-node configured
clusterrolebinding.rbac.authorization.k8s.io/calico-node configured
现在,让我们部署 Calico:
# kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml
configmap/calico-config created
service/calico-typha created
deployment.apps/calico-typha created
poddisruptionbudget.policy/calico-typha created
daemonset.extensions/calico-node created
serviceaccount/calico-node created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico....
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
完成此操作后,我们可以列出 Calico pod 并查看它是否成功启动:
# kubectl get pods --namespace=kube-system
NAME READY STATUS RESTARTS AGE
calico-node-ctxq8 2/2 Running 0 14m
让我们在示例中重用 6-2-1_nginx.yaml:
# kubectl create -f chapter6/6-2-1_nginx.yaml
replicaset "nginx" created
service "nginx" created
// list the services
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 36m
nginx NodePort 10.96.51.143 <none> 80:31452/TCP 5s
我们将发现我们的nginx服务的 IP 地址是10.96.51.143。我们来启动一个简单的 bash 并使用wget查看我们是否能够访问nginx:
# kubectl run busybox -i -t --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget --spider 10.96.51.143
Connecting to 10.96.51.143 (10.96.51.143:80)
--spider参数用于检查 URL 是否存在。在这种情况下,busybox可以成功访问nginx。接下来,我们为nginx pods 应用一个NetworkPolicy:
// declare a network policy
# cat chapter6/6-3-1_networkpolicy.yaml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: nginx-networkpolicy
spec:
podSelector:
matchLabels:
service: nginx
ingress:
- from:
- podSelector:
matchLabels:
project: chapter6
我们可以看到一些重要的语法。podSelector用于选择应该匹配目标 pod 标签的 pods。另一个是ingress[].from[].podSelector,它用于定义谁可以访问这些 pods。在这种情况下,所有具有project=chapter6标签的 pods 都有资格访问具有server=nginx标签的 pods。如果我们回到我们的 busybox pod,我们将无法再联系nginx,因为此时nginx pod 上已经有了NetworkPolicy。
默认情况下,它会拒绝所有连接,因此 busybox 将无法与nginx进行通信:
// in busybox pod, or you could use `kubectl attach <pod_name> -c busybox -i -t` to re-attach to the pod
# wget --spider --timeout=1 10.96.51.143
Connecting to 10.96.51.143 (10.96.51.143:80)
wget: download timed out
我们可以使用kubectl edit deployment busybox在 busybox pods 中添加project=chapter6标签。
然后,我们可以再次联系nginx pod:
// inside busybox pod
/ # wget --spider 10.96.51.143
Connecting to 10.96.51.143 (10.96.51.143:80)
借助前面的示例,我们现在有了如何应用网络策略的思路。我们还可以应用一些默认策略来拒绝所有流量或允许所有流量,通过调整选择器来选择没有人或每个人。例如,拒绝所有流量的行为可以通过以下方式实现:
# cat chapter6/6-3-1_np_denyall.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
这样,所有不匹配标签的 pods 将拒绝所有其他流量。或者,我们可以创建一个NetworkPolicy,其入口规则列出所有内容。这样,这个命名空间中的 pods 可以被任何人访问:
# cat chapter6/6-3-1_np_allowall.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
spec:
podSelector: {}
ingress:
- {}
服务网格
服务网格是处理服务到服务通信的基础设施层。特别是在微服务世界中,手头的应用可能包含成百上千个服务。这里的网络拓扑可能非常复杂。服务网格可以提供以下功能:
-
流量管理(如 A/B 测试和金丝雀发布)
-
安全性(如 TLS 和密钥管理)
-
可观察性(例如提供流量可见性。这很容易与监控系统如 Prometheus(
prometheus.io/)、跟踪系统如 Jaeger(www.jaegertracing.io)或 Zipkin(github.com/openzipkin/zipkin)以及日志系统集成)
市场上有两个主要的服务网格实现——Istio(istio.io)和 Linkerd(linkerd.io)。这两者都在应用容器旁边部署网络代理容器(即所谓的 sidecar 容器),并提供 Kubernetes 支持。以下图是服务网格的简化通用架构:

服务网格通常包含一个控制平面,这是网格的大脑。它可以管理和执行路由流量的策略,并收集可以与其他系统集成的遥测数据。它还负责服务或最终用户的身份和凭证管理。服务网格的 sidecar 容器,充当网络代理,与应用容器并排运行。服务之间的通信通过 sidecar 容器传递,这意味着它可以通过用户定义的策略控制流量,通过 TLS 加密来确保流量的安全,进行负载均衡和重试,控制 ingress/egress,收集指标,等等。
在接下来的部分中,我们将以 Istio 为例,但你可以自由选择组织中任何实现。首先,让我们获取最新版本的 Istio。在撰写本文时,最新版本是 1.0.5:
// get the latest istio
# curl -L https://git.io/getLatestIstio | sh -
Downloading istio-1.0.5 from https://github.com/istio/istio/releases/download/1.0.5/istio-1.0.5-osx.tar.gz ...
// get into the folder
# cd istio-1.0.5/
接下来,让我们为 Istio 创建一个 自定义资源定义 (CRD):
# kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml
customresourcedefinition.apiextensions.k8s.io/virtualservices.networking.istio.io created
customresourcedefinition.apiextensions.k8s.io/destinationrules.networking.istio.io created
customresourcedefinition.apiextensions.k8s.io/serviceentries.networking.istio.io created
customresourcedefinition.apiextensions.k8s.io/gateways.networking.istio.io created
...
在下面的示例中,我们将安装带有默认互信 TLS 身份验证的 Istio。资源定义位于 install/kubernetes/istio-demo-auth.yaml 文件中。如果你希望在没有 TLS 身份验证的情况下部署,可以改用 install/kubernetes/istio-demo.yaml:
# kubectl apply -f install/kubernetes/istio-demo-auth.yaml
namespace/istio-system created
configmap/istio-galley-configuration created
...
kubernetes.config.istio.io/attributes created
destinationrule.networking.istio.io/istio-policy created
destinationrule.networking.istio.io/istio-telemetry created
部署完成后,让我们检查服务和 pod 是否已经成功部署到 istio-system 命名空间:
// check services are launched successfully
# kubectl get svc -n istio-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana ClusterIP 10.98.182.66 <none> 3000/TCP 13s
istio-citadel ClusterIP 10.105.65.6 <none> 8060/TCP,9093/TCP 13s
istio-egressgateway ClusterIP 10.105.178.212 <none> 80/TCP,443/TCP 13s
istio-galley ClusterIP 10.103.123.213 <none> 443/TCP,9093/TCP 13s
istio-ingressgateway LoadBalancer 10.107.243.112 <pending> 80:31380/TCP,443:31390/TCP,31400:31400/TCP,15011:32320/TCP,8060:31750/TCP,853:30790/TCP,15030:30313/TCP,15031:30851/TCP 13s
istio-pilot ClusterIP 10.104.123.60 <none> 15010/TCP,15011/TCP,8080/TCP,9093/TCP 13s
istio-policy ClusterIP 10.111.227.237 <none> 9091/TCP,15004/TCP,9093/TCP 13s
istio-sidecar-injector ClusterIP 10.107.43.206 <none> 443/TCP 13s
istio-telemetry ClusterIP 10.103.118.119 <none> 9091/TCP,15004/TCP,9093/TCP,42422/TCP 13s
jaeger-agent ClusterIP None <none> 5775/UDP,6831/UDP,6832/UDP 11s
jaeger-collector ClusterIP 10.110.234.134 <none> 14267/TCP,14268/TCP 11s
jaeger-query ClusterIP 10.103.19.74 <none> 16686/TCP 12s
prometheus ClusterIP 10.96.62.77 <none> 9090/TCP 13s
servicegraph ClusterIP 10.100.191.216 <none> 8088/TCP 13s
tracing ClusterIP 10.107.99.50 <none> 80/TCP 11s
zipkin ClusterIP 10.98.206.168 <none> 9411/TCP 11s
等待几分钟后,检查所有的 pod 是否处于 Running 和 Completed 状态,如下所示:
# kubectl get pods -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-7ffdd5fb74-hzwcn 1/1 Running 0 5m1s
istio-citadel-55cdfdd57c-zzs2s 1/1 Running 0 5m1s
istio-cleanup-secrets-qhbvk 0/1 Completed 0 5m3s
istio-egressgateway-687499c95f-fbbwq 1/1 Running 0 5m1s
istio-galley-76bbb946c8-9mw2g 1/1 Running 0 5m1s
istio-grafana-post-install-8xxps 0/1 Completed 0 5m3s
istio-ingressgateway-54f5457d68-n7xsj 1/1 Running 0 5m1s
istio-pilot-7bf5674b9f-jnnvx 2/2 Running 0 5m1s
istio-policy-75dfcf6f6d-nwvdn 2/2 Running 0 5m1s
istio-security-post-install-stv2c 0/1 Completed 0 5m3s
istio-sidecar-injector-9c6698858-gr86p 1/1 Running 0 5m1s
istio-telemetry-67f94c555b-4mt4l 2/2 Running 0 5m1s
istio-tracing-6445d6dbbf-8r5r4 1/1 Running 0 5m1s
prometheus-65d6f6b6c-qrp6f 1/1 Running 0 5m1s
servicegraph-5c6f47859-qzlml 1/1 Running 2 5m1s
由于我们已经部署了 istio-sidecar-injector,我们可以简单地使用 kubectl label namespace default istio-injection=enabled 来为 default 命名空间中的每个 pod 启用 sidecar 容器注入。istio-sidecar-injector 作为一个变更型准入控制器,如果命名空间被标记为 istio-injection=enabled,它会将 sidecar 容器注入到 pod 中。接下来,我们可以从 samples 文件夹中启动一个示例应用。Helloworld 演示了金丝雀部署的使用 (en.wikipedia.org/wiki/Deployment_environment),它将流量分发到 helloworld-v1 和 helloworld-v2 服务:
// launch sample application
# kubectl run nginx --image=nginx
deployment.apps/nginx created
// list pods
# kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-64f497f8fd-b7d4k 2/2 Running 0 3s
如果我们检查其中一个 pod,会发现 istio-proxy 容器已被注入到其中:
# kubectl describe po nginx-64f497f8fd-b7d4k
Name: nginx-64f497f8fd-b7d4k
Namespace: default
Labels: pod-template-hash=2090539498
run=nginx
Annotations: kubernetes.io/limit-ranger: LimitRanger plugin set: cpu request for container nginx
sidecar.istio.io/status:
{"version":"50128f63e7b050c58e1cdce95b577358054109ad2aff4bc4995158c06924a43b","initContainers":["istio-init"],"containers":["istio-proxy"]...
Status: Running
Init Containers:
istio-init:
Container ID: docker://3ec33c4cbc66682f9a6846ae6f310808da3a2a600b3d107a0d361b5deb6d3018
Image: docker.io/istio/proxy_init:1.0.5
...
Containers:
nginx:
Container ID: docker://42ab7df7366c1838489be0c7264a91235d8e5d79510f3d0f078726165e95665a
Image: nginx
...
istio-proxy:
Container ID: docker://7bdf7b82ce3678174dea12fafd2c7f0726bfffc562ed3505a69991b06cf32d0d
Image: docker.io/istio/proxyv2:1.0.5
Image ID: docker-pullable://istio/proxyv2@sha256:8b7d549100638a3697886e549c149fb588800861de8c83605557a9b4b20343d4
Port: 15090/TCP
Host Port: 0/TCP
Args:
proxy
sidecar
--configPath
/etc/istio/proxy
--binaryPath
/usr/local/bin/envoy
--serviceCluster
istio-proxy
--drainDuration
45s
--parentShutdownDuration
1m0s
--discoveryAddress
istio-pilot.istio-system:15005
--discoveryRefreshDelay
1s
--zipkinAddress
zipkin.istio-system:9411
--connectTimeout
10s
--proxyAdminPort
15000
--controlPlaneAuthPolicy
MUTUAL_TLS
仔细观察,我们可以看到 istio-proxy 容器已根据其控制平面地址、追踪系统地址和连接配置启动。Istio 已经被验证。对于 Istio 流量管理,还有许多内容超出了本书的范围。Istio 提供了许多详细的示例供我们尝试,这些示例可以在我们刚下载的 istio-1.0.5/samples 文件夹中找到。
总结
在本章中,我们学习了容器如何相互通信。我们还介绍了 pod 之间如何进行通信。服务是一种抽象,它将流量路由到其下的任何 pod,只要标签选择器匹配。我们还了解了服务如何通过iptables与 pod 协同工作。我们还熟悉了如何使用 DNAT 和 un-DAT 数据包将外部服务的数据包路由到 pod。此外,我们还研究了新的 API 对象,例如 ingress,它允许我们使用 URL 路径将流量路由到后端的不同服务。最后,我们介绍了另一个 NetworkPolicy 对象,它提供了第二层安全性,并充当软件防火墙规则。通过网络策略,我们可以使特定的 pod 与其他特定 pod 通信。例如,只有数据检索服务才能与数据库容器通信。在最后一节中,我们简要了解了 Istio,这是流行的服务网格实现之一。所有这些功能使 Kubernetes 更加灵活、安全、可靠和强大。
在本章之前,我们已经介绍了 Kubernetes 的基本概念。在第七章,监控与日志记录中,我们将通过监控集群指标以及分析 Kubernetes 的应用程序和系统日志,更清晰地了解集群内部发生了什么。监控和日志记录工具对每个 DevOps 来说都是必不可少的,它们在像 Kubernetes 这样的动态集群中也起着极其重要的作用。因此,我们将深入了解集群的活动,如调度、部署、扩展和服务发现。第七章,监控与日志记录,将帮助你更好地理解在现实世界中操作 Kubernetes 的过程。
第七章:监控和日志记录
监控和日志记录是站点可靠性的关键部分。到目前为止,我们已经学会了如何使用各种控制器来管理我们的应用程序。我们还了解了如何利用服务和 Ingress 一同为我们的 Web 应用程序提供服务,无论是内部还是外部。在本章中,我们将通过以下主题,进一步了解我们的应用程序:
-
获取容器的状态快照
-
Kubernetes 中的监控
-
使用 Prometheus 汇聚 Kubernetes 中的度量
-
与 Kubernetes 中日志记录相关的各种概念
-
使用 Fluentd 和 Elasticsearch 进行日志记录
-
使用 Istio 获取服务之间流量的洞察
检查容器
每当我们的应用程序表现异常时,我们需要找出系统发生了什么。我们可以通过检查日志、资源使用情况、监视器,甚至直接进入正在运行的主机来深入排查问题。在 Kubernetes 中,我们有kubectl get和kubectl describe,它们可以查询我们部署的控制器状态。这帮助我们判断应用程序是否崩溃,或者是否按预期工作。
如果我们想了解应用程序输出的情况,我们可以使用kubectl logs,它将容器的stdout和stderr重定向到我们的终端。对于 CPU 和内存使用情况的统计信息,我们还可以使用类似top的命令——kubectl top。kubectl top node可以概览节点的资源使用情况,而kubectl top pod <POD_NAME>显示每个 Pod 的使用情况:
$ kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node-1 31m 3% 340Mi 57%
node-2 24m 2% 303Mi 51%
$ kubectl top pod mypod-6cc9b4bc67-j6zds
NAME CPU(cores) MEMORY(bytes)
mypod-6cc9b4bc67-j6zds 0m 0Mi
要使用kubectl top,你需要在集群中部署metrics-server或 Heapster(如果你使用的是 1.13 版本之前的 Kubernetes)。我们将在本章稍后讨论这个内容。
如果我们将一些如日志的东西留在容器内而没有发送出去怎么办?我们知道docker exec可以在运行中的容器内执行命令,但每次都访问节点的可能性不大。幸运的是,kubectl也允许我们用kubectl exec命令做同样的事情。它的用法与docker exec类似。例如,我们可以在 Pod 中的容器内运行一个 shell,如下所示:
$ kubectl exec -it mypod-6cc9b4bc67-j6zds /bin/sh
/ #
/ # hostname
mypod-6cc9b4bc67-j6zds
这与通过 SSH 登录主机非常相似。它让我们能够使用熟悉的工具进行故障排除,正如我们之前在没有容器时所做的那样。
如果容器是通过FROM scratch构建的,那么kubectl exec命令可能无法正常工作,因为容器内可能没有核心实用工具,如 shell(sh或bash)。在短命容器出现之前,Kubernetes 并未官方支持这个问题。如果我们恰好在运行的容器内有tar二进制文件,我们可以使用kubectl cp将一些二进制文件复制到容器中进行故障排除。如果我们幸运地拥有对容器运行节点的特权访问权限,我们可以利用docker cp,该命令不需要容器内有tar二进制文件,便能将所需的工具移入容器中。
Kubernetes 仪表盘
除了命令行工具外,还有一个仪表盘,它汇总了我们刚刚讨论的几乎所有信息,并以良好的 Web 用户界面展示数据:

这实际上是一个 Kubernetes 集群的通用图形用户界面,它还允许我们创建、编辑和删除资源。部署它非常简单;我们只需要应用一个模板:
$ kubectl create -f \ https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.0/src/deploy/recommended/kubernetes-dashboard.yaml
许多托管的 Kubernetes 服务,如Google Kubernetes Engine(GKE),提供预部署仪表盘到集群的选项,这样我们就不需要自己安装它了。要确定仪表盘是否存在于我们的集群中,可以使用kubectl cluster-info。如果它已安装,我们将看到消息kubernetes-dashboard is running at ...,如下所示:
$ kubectl cluster-info
Kubernetes master is running at https://192.168.64.32:8443
CoreDNS is running at https://192.168.64.32:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
kubernetes-dashboard is running at https://192.168.64.32:8443/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
使用前述默认模板或由云提供商预部署的仪表盘服务通常是ClusterIP。我们已经学习了多种访问集群内部服务的方法,但在这里我们只使用最简单的内置代理kubectl proxy,来建立我们的终端与 Kubernetes API 服务器之间的连接。一旦代理启动,我们就可以通过http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/访问仪表盘。端口8001是kubectl proxy命令的默认端口。
使用前面模板部署的仪表盘不会出现在kubectl cluster-info输出的服务列表中,因为它不是由插件管理器管理的。插件管理器确保它管理的对象处于活动状态,并且在大多数托管的 Kubernetes 服务中启用,以保护集群组件。有关更多信息,请查看以下仓库:github.com/kubernetes/kubernetes/tree/master/cluster/addons/addon-manager。
身份验证方法在不同的集群配置之间有所不同。例如,允许kubectl访问 GKE 集群的令牌,也可以用来登录仪表盘。它可以在kubeconfig中找到,或者通过下面显示的单行命令获取(假设当前使用的是该上下文):
$ kubectl config view --minify -o \
jsonpath={.users[].user.auth-provider.config.access-token}
如果我们跳过登录过程,仪表盘的服务账户将会被使用。有关其他访问选项,请查看仪表盘项目的 wiki 页面,以选择适合您集群配置的方式:github.com/kubernetes/dashboard/wiki/Access-control#authentication。
与kubectl top一样,要显示 CPU 和内存统计信息,您需要在集群中部署一个指标服务器。
Kubernetes 中的监控
我们现在知道如何在 Kubernetes 中检查我们的应用程序。然而,我们还没有足够的信心回答更复杂的问题,例如我们的应用程序有多健康、从新补丁中 CPU 使用率发生了什么变化、我们的数据库何时会耗尽容量,以及为什么我们的网站会拒绝任何请求。因此,我们需要一个监控系统来收集来自各个来源的度量指标,存储并分析接收到的数据,然后响应异常。在传统的监控系统设置中,我们通常会从至少三个不同的来源收集度量数据,以衡量服务的可用性以及其质量。
监控应用程序
我们关注的数据与我们正在运行的应用程序的内部状态有关。收集这些数据可以让我们更深入了解服务内部的运行情况。这些数据可能与应用程序设计的目标有关,也可能是应用程序本身固有的运行时数据。获取这些数据通常需要我们手动指示程序将内部数据暴露给监控管道,因为只有我们,作为服务的拥有者,知道哪些数据是有意义的,同时因为外部很难获取像缓存服务中记录在内存中的大小等信息。
应用程序与监控系统交互的方式有很大的差异。例如,如果我们需要有关 MySQL 数据库统计信息的数据,我们可以设置一个代理,定期查询信息和性能模式以获取原始数据,如当前时刻的 SQL 查询数量,并将其转换为我们监控系统所需的格式。以 Golang 应用程序为例,我们可能会通过expvar包及其接口暴露运行时信息,然后找到另一种方式将信息传输到监控后端。为了缓解这些步骤可能带来的困难,OpenMetrics(openmetrics.io/)项目致力于为不同应用程序和监控系统之间交换遥测数据提供标准化格式。
除了时间序列度量外,我们还可能希望结合使用分析工具和追踪工具来验证程序的性能。如今,这一点尤为重要,因为一个应用程序可能由数十个分布式服务组成。如果不使用诸如OpenTracing(opentracing.io)之类的追踪工具,找出性能下降背后的原因将变得异常困难。
监控基础设施
这里的“基础设施”一词可能太广泛了,但如果我们简单地考虑应用程序运行的地方以及它如何与其他组件和用户交互,那么我们应该监控的内容就显而易见:应用程序主机和连接的网络。
由于在主机上收集任务是系统监控的常见做法,它通常由监控框架提供的代理执行。代理会提取并发送关于主机的综合指标,例如负载、磁盘、连接或其他进程统计信息,这些有助于我们确定主机的健康状况。
对于网络来说,这些组件可以仅仅是同一主机上的网页服务器软件和网络接口,或者可能还有负载均衡器,甚至是像 Istio 这样的平台。尽管收集关于前述组件的遥测数据的方式取决于它们的实际设置,但一般来说,我们希望衡量的指标是流量、延迟和错误。
监控外部依赖
除了前面提到的两个组件,我们还需要检查依赖组件的状态,例如外部存储的使用情况,或者队列的消耗速率。例如,假设我们有一个订阅了队列作为输入并从该队列执行任务的应用程序。在这种情况下,我们还需要考虑像队列长度和消耗速率这样的指标。如果消耗速率较低,而队列长度不断增长,我们的应用程序可能会遇到问题。
这些原则同样适用于 Kubernetes 上的容器,因为在主机上运行容器几乎与运行进程相同。然而,由于 Kubernetes 上的容器和传统主机上的容器在资源使用方式上的细微区别,我们仍然需要相应地调整我们的监控策略。例如,Kubernetes 上应用程序的容器会分布在多个主机上,并不总是处于同一主机。如果我们仍然采用以主机为中心的监控方式,那么生成一份一致的应用程序记录将会非常困难。因此,我们应该在监控堆栈中添加一个容器层,而不是仅仅观察主机上的资源使用情况。此外,既然 Kubernetes 是我们应用程序的基础设施,我们也应该将这一点考虑在内。
监控容器
由于容器基本上是我们程序和依赖运行时库的一个薄层包装,因此在容器级别收集的指标与在容器主机上收集的指标类似,特别是关于系统资源使用的情况。尽管从容器和其主机上收集这些指标似乎是冗余的,但实际上,它让我们能够解决与监控移动容器相关的问题。这个想法非常简单:我们需要做的是将逻辑信息附加到指标上,例如 pod 标签或其控制器名称。通过这种方式,来自不同主机的容器的指标可以有意义地进行分组。考虑以下图示。假设我们想知道在App 2上传输了多少字节(tx)。我们可以将具有App 2标签的tx指标加起来,这样我们就能得到总计20 MB的数据:

另一个区别是,关于 CPU 限速的指标仅在容器级别报告。如果在某个应用程序遇到性能问题,但主机上的 CPU 资源尚充足,我们可以检查是否由于限速导致问题,并查看相关的指标。
监控 Kubernetes
Kubernetes 负责管理、调度和编排我们的应用程序。一旦应用程序崩溃,Kubernetes 通常是我们首先查看的地方。特别是当新部署后发生崩溃时,相关对象的状态会即时在 Kubernetes 中反映出来。
总结来说,应该监控的组件在下图中进行了说明:

获取 Kubernetes 监控要点
由于监控是服务运营的重要组成部分,我们基础设施中的现有监控系统可能已经提供了从常见来源(如知名的开源软件和操作系统)收集指标的解决方案。至于运行在 Kubernetes 上的应用程序,让我们看看 Kubernetes 及其生态系统提供了什么。
要收集由 Kubernetes 管理的容器的指标,我们不需要在 Kubernetes 主节点上安装任何特殊的控制器,也不需要在容器内部安装任何指标收集器。这基本上是由 kubelet 完成的,它从节点收集各种遥测数据,并在以下 API 端点中暴露出来(截至 Kubernetes 1.13 版本):
-
/metrics/cadvisor:此 API 端点用于 Prometheus 格式的 cAdvisor 容器指标。 -
/spec/:此 API 端点导出机器规格。 -
/stats/:此 API 端点也导出 cAdvisor 容器指标,但以 JSON 格式呈现。 -
/stats/summary:此端点包含 kubelet 聚合的各种数据,也称为 Summary API。
/metrics/路径下的指标与 kubelet 的内部统计数据相关。
Prometheus 格式(prometheus.io/docs/instrumenting/exposition_formats/)是 OpenMetrics 格式的前身,因此在 OpenMetrics 发布后,它也被称为 OpenMetrics v0.0.4。如果我们的监控系统支持这种格式,我们可以配置它从 kubelet 的 Prometheus 端点(/metrics/cadvisor)拉取指标。
要访问这些端点,kubelet 有两个 TCP 端口,10250和10255。端口10250是更安全的,建议在生产环境中使用,因为它是 HTTPS 端点,并且受到 Kubernetes 身份验证和授权系统的保护。10255使用普通 HTTP,应该限制使用。
cAdvisor (github.com/google/cadvisor) 是一个广泛使用的容器级度量收集器。简单来说,cAdvisor 聚合每个在机器上运行的容器的资源使用和性能统计信息。其代码目前已内嵌在 kubelet 中,因此我们无需单独部署它。然而,由于它仅关注某些容器运行时和 Linux 容器,这可能不适应未来 Kubernetes 对不同容器运行时的支持,因此未来的 Kubernetes 版本将不再集成 cAdvisor。除此之外,并非所有 cAdvisor 度量信息目前都由 kubelet 发布。因此,如果我们需要这些数据,就需要自行部署 cAdvisor。请注意,cAdvisor 的部署是按主机而非容器进行的,这对于容器化应用程序更为合理,我们可以使用 DaemonSet 来部署它。
监控管道中的另一个重要组件是度量服务器 (github.com/kubernetes-incubator/metrics-server)。它通过 kubelet 的 summary API 聚合每个节点的监控统计信息,并充当 Kubernetes 其他组件与真实度量源之间的抽象层。更具体地说,度量服务器在聚合层下实现了资源度量 API,因此集群内部的其他组件可以通过统一的 API 路径 (/api/metrics.k8s.io) 获取数据。在这种情况下,kubectl top 和 kube-dashboard 从资源度量 API 获取数据。
以下图示展示了度量服务器如何与集群中的其他组件进行交互:

如果你使用的是旧版本的 Kubernetes,度量服务器的角色将由 Heapster (github.com/kubernetes/heapster) 执行。
大多数 Kubernetes 安装默认会部署度量服务器。如果我们需要手动部署,可以下载度量服务器的 manifest 文件并应用它们:
$ git clone https://github.com/kubernetes-incubator/metrics-server.git
$ kubectl apply -f metrics-server/deploy/1.8+/
虽然 kubelet 的度量关注的是系统度量,但我们还希望在监控仪表板上查看对象的逻辑状态。kube-state-metrics (github.com/kubernetes/kube-state-metrics) 是完成我们监控堆栈的组成部分。它监控 Kubernetes master 节点,并将我们通过 kubectl get 或 kubectl describe 查看对象的状态转换为 Prometheus 格式的度量。因此,我们能够将这些状态抓取到度量存储中,并在发生诸如无法解释的重启次数等事件时发出警报。下载以下模板进行安装:
$ git clone https://github.com/kubernetes/kube-state-metrics.git
$ kubectl apply -f kube-state-metrics/kubernetes
之后,我们可以在集群内部通过 kube-state-metrics 服务查看状态度量:
http://kube-state-metrics.kube-system:8080/metrics
实践中的监控
到目前为止,我们已经学习了创建一个坚固的监控系统所需的一系列原理,这使我们能够构建一个强大的服务。现在是时候实施一个了。由于绝大多数 Kubernetes 组件都会以 Prometheus 格式在常规路径上暴露它们的度量指标,我们可以自由使用任何熟悉的监控工具,只要该工具能够理解这种格式。在这一节中,我们将设置一个使用 Prometheus 的示例。Prometheus 在 Kubernetes 生态系统中的流行,不仅因为它的强大功能,还因为它得到了云原生计算基金会(www.cncf.io/)的支持,该基金会也赞助了 Kubernetes 项目。
了解 Prometheus
Prometheus 框架由多个组件组成,如下图所示:

与所有其他监控框架一样,Prometheus 依赖代理从我们系统的组件中抓取统计信息。这些代理就是图中左侧所示的 exporters。除此之外,Prometheus 采用拉取模型来收集度量指标,也就是说,它并不是被动接收度量指标,而是主动从 exporters 的度量端点拉取数据。如果应用程序暴露了一个度量端点,Prometheus 也能够抓取该数据。默认的存储后端是嵌入式的 TSDB,并且可以切换到其他远程存储类型,例如 InfluxDB 或 Graphite。Prometheus 还负责根据Alertmanager中预配置的规则触发告警,Alertmanager 处理告警任务。它会将接收到的告警分组并发送到实际发送消息的工具,例如电子邮件、Slack(slack.com/)、PagerDuty(www.pagerduty.com/)等。除了告警,我们还希望将收集的度量指标可视化,以便快速了解我们的系统,这时 Grafana 就派上用场了。
除了数据收集,告警是监控中最重要的概念之一。然而,告警更与业务相关,这超出了本章的范围。因此,在这一节中,我们将重点讨论使用 Prometheus 进行度量指标的收集,而不会深入探讨 Alertmanager。
部署 Prometheus
我们为本章准备的模板可以通过以下链接找到:github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter7。
在 7-1_prometheus 下是本节中要使用的组件清单,包括 Prometheus 部署、导出器和相关资源。这些将部署在专用命名空间 monitoring 中,除了那些需要在 kube-system 命名空间中工作的组件。请仔细审查它们。目前,我们按照以下顺序创建我们的资源:
$ kubectl apply -f monitoring-ns.yml
$ kubectl apply -f prometheus/configs/prom-config-default.yml
$ kubectl apply -f prometheus
Prometheus 提供的清单中,资源使用量(如存储和内存)被限制在一个相对较低的水平。如果你希望以更实际的方式使用它们,可以根据实际需求调整参数。在 Prometheus 服务器启动后,我们可以通过 kubectl port-forward 连接到其 Web UI,端口为 9090。如果我们修改相应的服务(prometheus/prom-svc.yml),也可以使用 NodePort 或 Ingress 连接到 UI。进入 UI 后,我们将看到的第一页是 Prometheus 表达式浏览器,我们可以在其中构建查询并可视化指标。在默认设置下,Prometheus 会自动收集指标。所有有效的抓取目标可以在 /targets 路径下找到。要与 Prometheus 交互,我们需要了解其语言:PromQL。
要在生产环境中运行 Prometheus,还可以使用 Prometheus Operator (github.com/coreos/prometheus-operator),该工具旨在通过 CoreOS 简化 Kubernetes 中的监控任务。
使用 PromQL
PromQL 有三种数据类型:瞬时向量、范围向量和标量。瞬时向量是一组数据样本的时间序列;范围向量是包含某个时间范围内数据的时间序列集合;标量是一个数值型浮动值。存储在 Prometheus 中的指标通过指标名称和标签来标识,我们可以通过表达式浏览器中执行按钮旁边的下拉列表找到任何已收集指标的名称。如果我们使用一个指标名称查询 Prometheus,比如 http_requests_total,我们会得到很多结果,因为瞬时向量通常有相同的名称,但具有不同的标签。同样,我们也可以使用 {} 语法查询特定的标签集。例如,查询 {code="400",method="get"} 表示我们想要任何标签 code 和 method 分别为 400 和 get 的指标。将名称和标签结合在查询中也是有效的,如 http_requests_total{code="400",method="get"}。PromQL 使我们能够根据各种不同的参数检查我们的应用程序或系统,只要相关的指标被收集。
除了刚才提到的基本查询,PromQL 还有许多其他功能。例如,我们可以使用正则表达式和逻辑运算符查询标签,使用函数连接和聚合指标,甚至执行不同指标之间的操作。例如,以下表达式给出了 kube-dns pod 在 kube-system 命名空间中消耗的总内存:
sum(container_memory_usage_bytes{namespace="kube-system", pod_name=~"kube-dns-([^-]+)-.*"} ) / 1048576
更详细的文档可以在官方 Prometheus 网站找到(prometheus.io/docs/querying/basics/)。这将帮助你发挥 Prometheus 的强大功能。
在 Kubernetes 中发现目标
由于 Prometheus 仅从它知道的端点拉取指标,我们必须明确告知它我们希望从哪里收集数据。在/config路径下,有一页列出了当前配置的目标以供抓取。默认情况下,会有一个作业针对 Prometheus 本身运行,并可以在常规抓取路径/metrics中找到。如果我们连接到该端点,我们将看到一页非常长的文本,如下所示:
$ kubectl exec -n monitoring <prometheus_pod_name> -- \
wget -qO - localhost:9090/metrics
# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 1.5657e-05
go_gc_duration_seconds{quantile="0.25"} 1.9756e-05
go_gc_duration_seconds{quantile="0.5"} 2.4567e-05
go_gc_duration_seconds{quantile="0.75"} 2.8386e-05
...
这是我们之前提到过的 Prometheus 指标格式。下次我们看到类似的页面时,我们就知道那是一个指标端点。抓取 Prometheus 的作业是默认配置文件中的静态目标。然而,由于 Kubernetes 中的容器是动态创建和销毁的,因此很难找出容器的准确地址,更不用说在 Prometheus 中设置它。在某些情况下,我们可能会利用服务的 DNS 作为静态指标目标,但这仍然无法解决所有问题。例如,如果我们想知道每个 pod 背后的服务收到多少请求,设置一个作业来抓取服务可能会从随机的 pod 中获取结果,而不是从所有 pod 获取结果。幸运的是,Prometheus 通过其发现 Kubernetes 内部服务的能力帮助我们克服了这个问题。
更具体地说,Prometheus 能够查询 Kubernetes 关于正在运行的服务的信息。然后,它可以根据情况将它们添加到或从目标配置中删除。目前支持五种发现机制:
-
node发现模式为每个节点创建一个目标。默认情况下,目标端口将是 kubelet 的 HTTPS 端口(
10250)。 -
service发现模式为每个
Service对象创建一个目标。服务中定义的所有目标端口都会成为抓取目标。 -
pod发现模式与服务发现角色类似;它为每个 pod 创建一个目标,并暴露每个 pod 所定义的所有容器端口。如果在 pod 模板中没有定义端口,它仍然会仅使用地址创建抓取目标。
-
端点模式发现由服务创建的
Endpoint对象。例如,如果一个服务由三个 pod 支持,每个 pod 有两个端口,那么我们将有六个抓取目标。此外,对于一个 pod,不仅仅是暴露给服务的端口,其他声明的容器端口也会被发现。 -
ingress模式为每个 Ingress 路径创建一个目标。由于一个 Ingress 对象可以将请求路由到多个服务,并且每个服务可能有自己的一套指标,这种模式允许我们一次性配置所有这些目标。
下图展示了四种发现机制。左侧是 Kubernetes 中的资源,右侧是 Prometheus 中创建的目标:

一般来说,并不是所有暴露的端口都作为度量端点提供服务,因此我们当然不希望 Prometheus 抓取它在集群中发现的所有内容,而是只收集标记的资源。为了在 Prometheus 中实现这一点,一种常见的方法是利用资源清单上的注解来区分哪些目标需要被抓取,然后我们可以使用 Prometheus 配置中的 relabel 模块过滤掉那些没有注解的目标。请看这个示例配置:
...
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_mycom_io_scrape]
action: keep
regex: true
...
这告诉 Prometheus 只保留具有 __meta_kubernetes_pod_annotation_{name} 标签并且值为 true 的目标。这个标签是从 pod 规格中的注解字段获取的,下面是一个片段:
...
apiVersion: apps/v1
kind: Deployment
spec:
template:
metadata:
annotations:
mycom.io/scrape: "true"
...
请注意,Prometheus 会将所有不在 [a-zA-Z0-9_] 范围内的字符转换为 _,因此我们也可以将之前的注解写为 mycom-io-scrape: "true"。
通过结合这些注解和标签过滤规则,我们可以精确控制需要收集的目标。以下是一些 Prometheus 中常用的注解:
-
prometheus.io/scrape: "true" -
prometheus.io/path: "/metrics" -
prometheus.io/port: "9090" -
prometheus.io/scheme: "https" -
prometheus.io/probe: "true"
这些注解可以在 Deployment 对象(其对应的 pod)和 Service 对象中看到。以下模板片段展示了一个常见的用例:
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/monitoring"
prometheus.io/scheme: "http"
prometheus.io/port: "9090"
通过应用以下配置,Prometheus 将把端点模式下发现的目标转换为 http://<pod_ip_of_the_service>:9090/monitoring:
- job_name: 'kubernetes-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__,__meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
我们可以在 Prometheus 中使用 prometheus.io/probe 注解来表示一个服务是否应该被添加到探测目标中。探测任务将由 Blackbox exporter 执行(github.com/prometheus/blackbox_exporter)。
探测的目的是确定探测器与目标服务之间连接的质量。目标服务的可用性也会被评估,因为探测器可以充当客户。因此,我们放置探测器的位置也是一个需要考虑的因素,如果我们希望探测具有意义的话。
有时,我们可能只想获取某个服务下单个 Pod 的指标,而不是获取该服务下所有 Pod 的指标。由于大多数端点对象不是手动创建的,端点发现模式使用了从服务继承的注解。这意味着,如果我们为服务添加注解,该注解将在服务发现和端点发现模式中同时可见,这使得我们无法区分目标应该是按端点抓取还是按服务抓取。为了解决这个问题,我们可以使用prometheus.io/scrape: "true"来标记需要被抓取的端点,并使用另一个注解如prometheus.io/scrape_service_only: "true"来告诉 Prometheus 仅为该服务创建一个目标。
我们示例仓库中的prom-config-k8s.yml模板包含了一些基本配置,用于为 Prometheus 发现 Kubernetes 资源。按如下方式应用:
$ kubectl apply -f prometheus/configs/prom-config-k8s.yml
因为模板中的资源是一个 ConfigMap,它将数据存储在etcd共识存储中,所以需要几秒钟才能变得一致。之后,我们可以通过向进程发送SIGHUP来重新加载 Prometheus:
$ kubectl exec -n monitoring <prometheus_pod_name> -- kill -1 1
提供的模板基于 Prometheus 官方仓库中的这个示例。你可以通过以下链接了解更多用途,其中还包括针对 Blackbox exporter 的目标发现:github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml。我们也略过了配置中动作是如何实际工作的细节;欲了解更多,请参考官方文档:prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration。
从 Kubernetes 收集数据
在 Prometheus 中实现之前讨论的监控层的步骤现在非常清晰:
-
安装导出器
-
用适当的标签标注它们
-
在自动发现的端点上收集数据
Prometheus 中的主机层监控由节点导出器(github.com/prometheus/node_exporter)完成。它的 Kubernetes 模板可以在本章的示例中找到,并包含一个带有抓取注解的 DaemonSet。按如下方式安装:
$ kubectl apply -f exporters/prom-node-exporter.yml
如果使用示例配置,它在 Prometheus 中的对应目标将通过 Pod 发现角色被发现并创建。
容器层的收集器应为 kubelet。因此,使用节点模式发现它是我们需要做的唯一事情。
Kubernetes 监控由kube-state-metrics完成,之前也介绍过它。它还带有 Prometheus 注解,这意味着我们不需要做任何其他配置。
到目前为止,我们已经基于 Prometheus 设置了强大的监控堆栈。关于应用程序和外部资源监控,Prometheus 生态系统中有广泛的导出器来支持我们系统内各种组件的监控。例如,如果我们需要 MySQL 数据库的统计信息,只需安装 MySQL 服务器导出器(github.com/prometheus/mysqld_exporter),它提供了全面且有用的指标。
除了我们已经描述的指标之外,Kubernetes 组件还有一些其他有用的指标起着重要作用:
-
Kubernetes API 服务器:API 服务器在
/metrics上公开其统计信息,默认情况下启用此目标。 -
kube-controller-manager:此组件在10252端口上公开指标,但在某些托管的 Kubernetes 服务(如 GKE)上不可见。如果您使用自托管集群,应用kubernetes/self/kube-controller-manager-metrics-svc.yml会为 Prometheus 创建端点。 -
kube-scheduler:使用10251端口,在 GKE 集群中也不可见。kubernetes/self/kube-scheduler-metrics-svc.yml用于创建指向 Prometheus 的目标模板。 -
kube-dns:Kubernetes 中的 DNS 由 CoreDNS 管理,其统计信息通过9153端口暴露。相应的模板是kubernetes/self/core-dns-metrics-svc.yml。 -
etcd:etcd集群还在2379端口上提供 Prometheus 指标端点。如果您的etcd集群是自托管且由 Kubernetes 管理的,可以使用kubernetes/self/etcd-server.yml作为参考。 -
Nginx Ingress Controller:Nginx 控制器通过
10254端口发布指标,提供有关 nginx 状态以及 nginx 路由的持续时间、大小、方法和状态码的详细信息。完整的指南可以在这里找到:github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/monitoring.md。
Kubernetes 中的 DNS 由skydns提供,容器还暴露了一个指标路径。在使用skydns的kube-dns pod 中,通常有两个容器,分别是dnsmasq和sky-dns,它们的指标端口分别是10054和10055。如果需要,对应的模板是kubernetes/self/skydns-metrics-svc.yml。
使用 Grafana 可视化指标
表达式浏览器具有内置的图形面板,可帮助我们查看指标,但不适合作为日常例行工作的可视化仪表板。Grafana 是 Prometheus 的最佳选择。我们在第四章《管理有状态工作负载》中讨论了如何设置 Grafana,并在该章节的存储库中提供了模板。
要在 Grafana 中查看 Prometheus 指标,首先必须添加一个数据源。连接到我们的 Prometheus 服务器需要以下配置:
-
类型:
Prometheus -
网址:
http://prometheus-svc.monitoring:9090
一旦连接上,我们可以导入仪表盘。在 Grafana 的共享页面(grafana.com/dashboards?dataSource=prometheus)上,我们可以找到丰富的现成仪表盘。以下截图来自仪表盘#1621:

因为这些图表是由 Prometheus 的数据绘制的,我们可以根据需要绘制任何数据,只要我们掌握 PromQL。
仪表盘的内容可能会有很大差异,因为每个应用程序关注的重点不同。然而,将所有内容放到一个巨大的仪表盘中并不是一个好主意。USE 方法(www.brendangregg.com/usemethod.html)和四个黄金信号(landing.google.com/sre/book/chapters/monitoring-distributed-systems.html)为构建监控仪表盘提供了一个良好的起点。
日志事件
通过对系统状态的定量时间序列进行监控,我们能够快速识别系统中哪些组件发生了故障,但它仍然无法诊断问题的根本原因。我们需要的是一个日志系统,通过关联事件和检测到的异常来收集、持久化并搜索日志。确实,除了排查故障和系统故障后的事后分析,日志系统还有许多其他商业应用场景。
一般来说,日志系统主要由两个组件组成:日志代理和日志后端。前者是程序的抽象层,负责收集、转换并将日志发送到日志后端。日志后端则存储接收到的所有日志。与监控类似,为 Kubernetes 构建日志系统时最具挑战性的部分是确定如何从容器中收集日志并将其发送到集中式日志后端。通常,发送程序日志的方式有三种:
-
将所有内容转储到
stdout/stderr。 -
将日志文件写入文件系统。
-
将日志发送到日志代理或直接记录到后端。Kubernetes 中的程序也能够以相同的方式发出日志,只要我们理解 Kubernetes 中日志流的流动方式。
聚合日志的模式
对于直接向日志代理或后端记录日志的程序,无论它们是否在 Kubernetes 内部运行,实际上都无关紧要,因为它们从技术上讲并不是通过 Kubernetes 发送日志。在其他情况下,我们将使用以下两种日志模式。
每个节点使用日志代理收集日志
我们知道,通过kubectl logs获取的消息是从容器的stdout/stderr重定向来的流,但显然用kubectl logs收集日志并不是一个好主意。实际上,kubectl logs是从 kubelet 获取日志,而 kubelet 会从容器运行时的宿主路径/var/log/containers/聚合日志。日志的命名规则为{pod_name}_{namespace}_{container_name}_{container_id}.log。
因此,我们需要做的是,在每个节点上设置日志代理,并配置它们跟踪并转发路径下的日志文件,如下图所示:

实际上,我们还会配置日志代理来跟踪主节点和节点下/var/log中的系统和 Kubernetes 组件的日志,如下所示:
-
kube-proxy.log -
kube-apiserver.log -
kube-scheduler.log -
kube-controller-manager.log -
etcd.log
如果 Kubernetes 组件由systemd管理,则日志将显示在journald中。
除了stdout/stderr,如果一个应用程序的日志作为文件存储在容器内,并通过hostPath卷持久化,那么一个节点日志代理能够将它们传递到节点上。然而,对于每个导出的日志文件,我们必须在日志代理中自定义它们的相应配置,以便正确地转发它们。此外,我们还需要合理命名日志文件,以避免任何冲突,并确保日志旋转可管理,这使得这种机制不可扩展且难以管理。
运行一个 sidecar 容器来转发写入的日志
修改应用程序以将日志写入标准流而不是日志文件可能会很困难,而且我们通常希望避免将日志写入hostPath卷带来的麻烦。在这种情况下,我们可以运行一个 sidecar 容器来处理 pod 的日志。换句话说,每个应用程序 pod 将有两个共享相同emptyDir卷的容器,以便 sidecar 容器可以跟踪应用程序容器的日志并将它们发送到 pod 外部,如下图所示:

尽管我们不再需要担心管理日志文件,但仍然需要额外的工作,如为每个 pod 配置日志代理并将 Kubernetes 的元数据附加到日志条目上。另一种选择是使用 sidecar 容器将日志输出到标准流,而不是运行专门的日志代理,如以下 pod 示例所示。在这种情况下,应用程序容器持续不断地写入消息到/var/log/myapp.log,而 sidecar 容器则在共享卷中跟踪myapp.log:
---7-2_logging-sidecar.yml---
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- image: busybox
name: application
args:
- /bin/sh
- -c
- >
while true; do
echo "$(date) INFO hello" >> /var/log/myapp.log ;
sleep 1;
done
volumeMounts:
- name: log
mountPath: /var/log
- name: sidecar
image: busybox
args:
- /bin/sh
- -c
- tail -fn+1 /var/log/myapp.log
volumeMounts:
- name: log
mountPath: /var/log
volumes:
- name: log
emptyDir: {}
我们可以通过kubectl logs查看书写日志:
$ kubectl logs -f myapp -c sidecar
Sun Oct 14 21:26:47 UTC 2018 INFO hello
Sun Oct 14 21:26:48 UTC 2018 INFO hello
...
获取 Kubernetes 状态事件
我们在kubectl describe输出中看到的事件消息包含了宝贵的信息,并补充了kube-state-metrics收集的指标。这使我们能够准确地了解我们的 pods 或节点发生了什么。因此,这些事件消息应该成为我们的日志记录基本内容的一部分,与系统日志和应用日志一起使用。为了实现这一点,我们需要一些工具来监控 Kubernetes API 服务器,并将事件聚合到日志存储中。Kubernetes 中的事件对象也存储在etcd中,但访问存储以获取这些事件对象可能需要大量的工作。像 eventrouter(github.com/heptiolabs/eventrouter)这样的项目可以在这种情况下提供帮助。Eventrouter 的工作方式是将事件对象转换为结构化消息,并将其发送到stdout。因此,我们的日志系统可以将这些事件视为普通日志,同时保留事件的元数据。
还有其他一些选择。一个是 Event Exporter(github.com/GoogleCloudPlatform/k8s-stackdriver/tree/master/event-exporter),尽管它仅支持 StackDriver,这是一个 Google Cloud Platform 上的监控解决方案。另一个选择是 eventer,它是 Heapster 的一部分。它支持 Elasticsearch、InfluxDB、Riemann 和 Google Cloud Logging 作为输出目标。如果我们使用的日志系统不被支持,Eventer 还可以直接输出到stdout。然而,由于 Heapster 被 metric server 替代,eventer 的开发也被放弃了。
使用 Fluent Bit 和 Elasticsearch 进行日志记录
到目前为止,我们已经讨论了在现实世界中可能遇到的各种日志记录场景。现在是时候卷起袖子,构建一个日志系统了。日志系统和监控系统的架构在很多方面都非常相似:它们都有收集器、存储和消费者,如 BI 工具或搜索引擎。根据需求,组件可能会有很大的差异。例如,我们可能会实时处理一些日志以提取实时信息,而对于其他日志,我们可能只是将它们存档到持久存储中以供后续使用,比如批量报告或满足合规要求。总的来说,只要我们有办法将日志从容器中发送出去,就可以将其他工具集成到我们的系统中。下图展示了一些可能的使用场景:

在这一节中,我们将设置最基本的日志系统。它的组件包括 Fluent Bit、Elasticsearch 和 Kibana。本节的模板可以在7-3_efk中找到,它们需要部署到logging命名空间。
Elasticsearch 是一个强大的文本搜索和分析引擎,使其成为分析集群中所有运行日志的理想选择。本章节的 Elasticsearch 模板使用了一个非常简单的设置来演示该概念。如果你希望部署一个用于生产的 Elasticsearch 集群,建议使用 StatefulSet 控制器来设置集群,并通过适当的配置调优 Elasticsearch,正如我们在第四章《管理有状态工作负载》中所讨论的那样。我们可以通过以下模板部署一个 Elasticsearch 实例和一个日志命名空间(github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter7/7-3_efk):
$ kubectl apply -f logging-ns.yml $ kubectl apply -f elasticsearch
如果我们从 es-logging-svc:9200 获得响应,那么就说明 Elasticsearch 已经准备好。
Elasticsearch 是一个优秀的文档搜索引擎。然而,在持久化大量日志时,它可能表现得不如预期。幸运的是,有多种解决方案允许我们使用 Elasticsearch 索引存储在其他存储中的文档。
下一步是设置一个节点日志代理。由于我们会在每个节点上运行此代理,我们希望它在节点资源使用上尽可能轻量;这也是我们选择 Fluent Bit 的原因(fluentbit.io/)。Fluent Bit 具有更低的内存占用,使其成为满足我们需求的合适日志代理,即将所有日志从节点中导出。
由于 Fluent Bit 的实现目标是最大限度地减少资源使用,因此它将功能减少到了一个非常有限的集合。如果我们希望在日志层中有更大的自由度来组合解析器和过滤器以适应不同的应用程序,我们可以使用 Fluent Bit 的兄弟项目 Fluentd(www.fluentd.org/),它更加可扩展和灵活,但比 Fluent Bit 消耗更多资源。由于 Fluent Bit 能够将日志转发到 Fluentd,因此一种常见的方法是将 Fluent Bit 用作节点日志代理,将 Fluentd 用作聚合器,就像前面图示的那样。
在我们的示例中,Fluent Bit 被配置为第一个日志模式。这意味着它会通过每个节点的日志代理收集日志,并直接将其发送到 Elasticsearch:
$ kubectl apply -f logging-agent/fluentbit/
Fluent Bit 的 ConfigMap 已经配置为跟踪 /var/log/containers 下的容器日志和 /var/log 下某些系统组件的日志。Fluent Bit 还可以在端口 2020 上以 Prometheus 格式暴露其统计数据,这在 DaemonSet 模板中进行了配置。
由于稳定性问题和对灵活性的需求,使用 Fluentd 作为日志代理仍然很常见。在我们的示例中,可以在 logging-agent/fluentd 下找到模板,或者可以在官方仓库中找到它们:github.com/fluent/fluentd-kubernetes-daemonset。
要使用 Kubernetes 事件,我们可以使用 eventrouter:
$ kubectl apply -f eventrouter.yml
这将开始以 JSON 格式在 stdout 流中打印事件,以便我们可以在 Elasticsearch 中对其进行索引。
要查看发送到 Elasticsearch 的日志,我们可以调用 Elasticsearch 的搜索 API,但还有一个更好的选择:Kibana,它是一个允许我们与 Elasticsearch 交互的 web 界面。在本节的示例中,使用以下命令将所有内容部署到 kibana 下:
$ kubectl apply -f kibana
Grafana 也支持从 Elasticsearch 读取数据:docs.grafana.org/features/datasources/elasticsearch/。
在我们的示例中,Kibana 监听端口 5601。将服务从集群中暴露并使用任何浏览器连接后,您可以开始搜索来自 Kubernetes 的日志。在我们的 Fluent Bit 配置示例中,通过 eventrouter 路由的日志将位于名为 kube-event-* 的索引下,而来自其他容器的日志则可以在名为 kube-container-* 的索引中找到。以下截图展示了 Kibana 中的事件消息样式:

从日志中提取指标
我们围绕 Kubernetes 构建的应用程序监控和日志系统,如下图所示:

日志部分和监控部分看起来像是两条独立的轨道,但日志的价值远不止是短文本的集合。这是结构化数据,通常带有时间戳;因此,如果我们能够从日志中解析信息,并根据时间戳将提取的向量投影到时间维度,它将成为一个时间序列指标,并可用于 Prometheus。
例如,任何一个 web 服务器的访问日志条目可能如下所示:
10.1.5.7 - - [28/Oct/2018:00:00:01 +0200] "GET /ping HTTP/1.1" 200 68 0.002
这些数据包括请求的 IP 地址、时间、方法、处理程序等。如果我们根据日志段的含义划分它们,那么计数的部分可以视为一个指标样本,如下所示:
{ip:"10.1.5.7",handler:"/ping",method:"GET",status:200,body_size:68,duration:0.002}
转换之后,随着时间的推移,追踪日志将更加直观。
为了将日志组织成 Prometheus 格式,诸如 mtail (github.com/google/mtail)、Grok Exporter (github.com/fstab/grok_exporter) 或 Fluentd (github.com/fluent/fluent-plugin-prometheus) 等工具,广泛用于将日志条目提取为指标。
可以说,现在很多应用程序支持直接输出结构化指标,我们总是可以为自己的应用程序加入此类信息的监控。然而,并不是所有的技术栈都能为我们提供方便的方式来获取其内部状态,尤其是操作系统工具,如 ntpd。因此,将此类工具纳入我们的监控栈,帮助提升基础设施的可观察性,仍然是值得的。
集成 Istio 的数据
在服务网格中,每个服务之间的网关是前端代理。因此,前端代理显然是网格内部运行的事物的重要信息来源。然而,如果我们的技术栈中已经有类似的组件,如用于内部服务的负载均衡器或反向代理,那么从它们收集流量数据与服务网格代理有何不同?让我们考虑经典设置:

SVC-A和SVC-B向SVC-C发起请求。从负载均衡器收集到的SVC-C的数据代表了SVC-C的质量。然而,由于我们无法看到客户端到SVC-C的路径,衡量SVC-A或SVC-B与SVC-C之间质量的唯一方式是依赖客户端侧构建的机制,或在客户端所在的网络中放置探针。对于服务网格,看看以下图表:

在这里,我们想要了解SVC-C的质量。在此设置中,SVC-A和SVC-B通过它们的 sidecar 代理与SVC-C通信,因此,如果我们收集所有客户端侧代理向SVC-C发出的请求的指标,我们也可以从服务器端的负载均衡器获得相同的数据,并补充SVC-C与其客户端之间缺失的度量。换句话说,我们可以有一种统一的方式来衡量不仅SVC-C的性能,还包括SVC-C与其客户端之间的质量。这些增强的信息也有助于我们在排查问题时定位故障。
Istio 适配器模型
Mixer 是 Istio 架构中管理遥测的组件。它从与应用容器一起部署的边缘代理获取统计信息,并通过适配器与其他后端组件进行交互。例如,我们的监控后端是 Prometheus,因此我们可以利用 Mixer 的 Prometheus 适配器将从 envoy 代理获取的指标转换为 Prometheus 指标路径。
访问日志如何通过管道流向我们的 Fluentd/Fluent Bit 日志后端,与我们之前构建的管道相同,该管道将日志传送到 Elasticsearch。Istio 组件与监控后端之间的交互在以下图表中有所示:

为现有基础设施配置 Istio
适配器模型使我们能够轻松地从 Mixer 组件中获取监控数据。它需要在接下来的章节中探讨的配置。
Mixer 模板
一个 Mixer 模板定义了 Mixer 应该组织哪些数据,以及数据应以何种形式呈现。为了获取指标和访问日志,我们需要metric和logentry模板。例如,以下模板指示 Mixer 输出包含源和目标名称、方法、请求 URL 等的日志:
apiVersion: config.istio.io/v1alpha2
kind: logentry
metadata:
name: accesslog
namespace: istio-system
spec:
severity: '"info"'
timestamp: request.time
variables:
source: source.workload.name | "unknown"
destination: destination.workload.name | "unknown"
method: request.method | ""
url: request.path | ""
protocol: request.scheme | ""
responseCode: response.code | 0
responseSize: response.size | 0
requestSize: request.size | 0
latency: response.duration | "0ms"
monitored_resource_type: '"UNSPECIFIED"'
每种模板的完整参考可以在这里找到:istio.io/docs/reference/config/policy-and-telemetry/templates/。
处理器适配器
处理器适配器声明了 Mixer 与处理器交互的方式。对于前面的logentry,我们可以有一个如下所示的处理器定义:
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: fluentd
namespace: istio-system
spec:
compiledAdapter: fluentd
params:
address: fluentd-aggegater-svc.logging:24224
从这个代码片段来看,Mixer 知道一个可以接收logentry的目标。每种类型的适配器功能差异很大。例如,fluentd适配器只能接受logentry模板,而Prometheus只能处理metric模板,而 Stackdriver 可以处理metric、logentry和tracespan模板。所有支持的适配器列在这里:istio.io/docs/reference/config/policy-and-telemetry/adapters/。
规则
规则是模板与处理器之间的绑定。如果我们在之前的示例中已经有了accesslog、logentry和fluentd处理器,那么类似这样的规则将这两个实体关联起来:
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: accesslogtofluentd
namespace: istio-system
spec:
match: "true"
actions:
- handler: fluentd
instances:
- accesslog.logentry
一旦规则应用,Mixer 就知道它应该以之前定义的格式将访问日志发送到fluentd,地址为fluentd-aggegater-svc.logging:24224。
部署一个fluentd实例来接收来自 TCP 套接字的输入的示例可以在7_3efk/logging-agent/fluentd-aggregator下找到(github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter7/7-3_efk/logging-agent/fluentd-aggregator),并配置为将日志转发到我们之前部署的 Elasticsearch 实例。访问日志的三个 Istio 模板可以在7-4_istio_fluentd_accesslog.yml下找到(github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/blob/master/chapter7/7-4_istio_fluentd_accesslog.yml)。
现在让我们考虑一下指标。如果 Istio 是通过官方图表部署,并且启用了 Prometheus(默认启用),那么在istio-system命名空间下的集群中将会有一个 Prometheus 实例。此外,Prometheus 会被预配置以收集 Istio 组件的指标。然而,出于各种原因,我们可能希望使用自己的 Prometheus 部署,或者将 Istio 附带的 Prometheus 专门用于收集 Istio 组件的指标。另一方面,我们知道 Prometheus 架构是灵活的,只要目标组件暴露它们的指标端点,我们就可以配置自己的 Prometheus 实例来抓取这些端点。
下面列出了来自 Istio 组件的一些有用端点:
-
<all-components>:9093/metrics:每个 Istio 组件都会在端口9093上暴露其内部状态。 -
<envoy-sidecar>:15090/stats/prometheus:每个 envoy 代理都会在此处打印原始统计数据。如果我们想要监控我们的应用程序,建议先使用 mixer 模板整理指标。 -
<istio-telemetry-pods>:42422/metrics:由 Prometheus 适配器配置并由 mixer 处理的指标将在此处提供。请注意,来自 envoy sidecar 的指标仅在 envoy 上报的 telemetry pod 中可用。换句话说,我们应当使用 Prometheus 的端点发现模式来收集所有 telemetry pod 的指标,而不是直接从 telemetry 服务中抓取数据。
默认情况下,以下指标将被配置并在 Prometheus 路径中提供:
-
requests_total -
request_duration_seconds -
request_bytes -
response_bytes -
tcp_sent_bytes_total -
tcp_received_bytes_total
另一种使 Prometheus 实例(与官方 Istio 发布版一起部署的)收集的指标对我们的 Prometheus 可用的方法是使用联合设置。这涉及到设置一个 Prometheus 实例来抓取存储在另一个 Prometheus 实例中的指标。通过这种方式,我们可以将 Istio 的 Prometheus 看作是所有 Istio 相关指标的收集器。联合功能的路径为 /federate。假设我们想获取所有标签为 {job="istio-mesh"} 的指标,则查询参数如下:
http://<prometheus-for-istio>/federate?match[]={job="istio-mesh"}
结果是,通过添加几行配置,我们可以轻松地将 Istio 指标集成到现有的监控管道中。有关联合的完整参考,请查看官方文档:prometheus.io/docs/prometheus/latest/federation/。
总结
本章开始时,我们描述了如何通过内置函数(如 kubectl)快速获取正在运行的容器状态。然后,我们扩展了讨论,探讨了监控的概念和原理,包括为什么、监控什么以及如何在 Kubernetes 上监控我们的应用程序。之后,我们构建了一个以 Prometheus 为核心的监控系统,并设置了导出器来收集来自我们的应用程序、系统组件和 Kubernetes 单元的指标。还介绍了 Prometheus 的基本原理,如其架构和查询领域特定语言,因此我们现在可以使用指标深入了解我们的集群以及运行其中的应用程序,不仅能回顾性地排查故障,还能检测潜在的故障。之后,我们描述了常见的 日志 模式以及如何在 Kubernetes 中处理这些模式,并部署了 EFK 堆栈来整合日志。最后,我们转向了 Kubernetes 和应用程序之间的另一个重要基础设施组件——服务网格,以便在监控遥测时获得更精细的精度。本章中构建的系统提升了我们服务的可靠性。
在第八章中,资源管理与扩展,我们将利用这些指标来优化我们的服务所使用的资源。
第八章:资源管理与扩展
尽管现在我们通过监控系统对与应用和集群相关的一切有了全面了解,但在处理计算资源和集群的能力时,我们仍然缺乏能力。在本章中,我们将讨论资源,内容包括以下主题:
-
Kubernetes 调度机制
-
资源与工作负载之间的亲和性
-
使用 Kubernetes 平滑扩展
-
安排集群资源
-
节点管理
工作负载调度
调度一词指的是将资源分配给需要执行的任务。Kubernetes 的作用远不止保持容器运行;它会主动监控集群的资源使用情况,并将 Pod 精确调度到可用资源上。这种基于调度器的基础设施是让我们比传统基础设施更高效地运行工作负载的关键。
优化资源利用率
毫不奇怪,Kubernetes 分配 Pod 到节点的方式是基于资源的供需关系。如果一个节点可以提供足够的资源,那么该节点就有资格运行 Pod。因此,集群容量与实际使用之间的差距越小,我们可以获得的资源利用率就越高。
资源类型与分配
有两种核心资源类型参与调度过程,即 CPU 和内存。要查看节点的能力,可以检查其.status.allocatable路径:
# Here we only show the path with a go template.
$ kubectl get node <node_name> -o go-template --template=\
'{{range $k,$v:=.status.allocatable}}{{printf "%s: %s\n" $k $v}}{{end}}'
cpu: 2
ephemeral-storage: 14796951528
hugepages-2Mi: 0
memory: 1936300Ki
pods: 110
如我们所见,这些资源将由调度器分配给任何需要它们的 Pod。但调度器如何知道一个 Pod 会消耗多少资源呢?我们实际上需要指示 Kubernetes 每个 Pod 的请求和限制。语法是 Pod 清单中的 spec.containers[].resources.{limits,requests}.{resource_name}:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
requests:
cpu: 100m
memory: 10Mi
limits:
cpu: 0.1
memory: 100Mi
CPU 资源的单位可以是分数或千分之一 CPU 表达式。一个 CPU 核心(或超线程)等于 1,000 毫核,或在分数表示法中是一个简单的 1.0。请注意,分数表示法是绝对量。例如,如果我们在一个节点上有八个核心,表达式 0.5 表示我们指的是 0.5 个核心,而不是四个核心。从这个角度来看,在之前的例子中,请求的 CPU 数量 100m 和 CPU 限制 0.1 是等价的。
内存以字节为单位表示,Kubernetes 接受以下后缀和符号:
-
十进制:E, P, T, G, M, K
-
二进制:Ei, Pi, Ti, Gi, Mi, Ki
-
科学计数法:e
因此,以下形式大致相同:67108864、67M、64Mi 和 67e6。
除了 CPU 和内存外,Kubernetes 还增加了许多其他资源类型,如 临时存储 和 大页内存。如 GPU、FPGA 和网卡等特定厂商资源可以通过设备插件供 Kubernetes 调度器使用。您还可以将自定义资源类型(如许可证)绑定到节点或集群,并配置 pod 消耗这些资源。有关详细信息,请参阅以下相关文献:
临时存储:
kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/ 设备资源:
kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/ 扩展资源:
kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#extended-resources
如其名称所示,请求是指 pod 可能使用的资源数量,Kubernetes 使用它来选择一个节点调度 pod。对于每种资源类型,节点上所有容器请求的总和永远不会超过该节点可分配的资源。换句话说,每个成功调度的容器都能确保获得它请求的资源量。
为了最大化整体资源利用率,只要 pod 所在节点有剩余资源,pod 就可以超出请求的资源量。但是,如果节点上的每个 pod 都使用超出预期的资源,那么该节点提供的资源可能最终会被耗尽,导致节点不稳定。限制概念解决了这个问题:
-
如果一个 pod 使用的 CPU 超过某个百分比,它将被限制(而不是终止)。
-
如果一个 pod 达到内存限制,它将被终止并重新启动。
这些限制是硬性约束,因此它总是大于或等于相同资源类型的请求。
请求和限制是按容器配置的。如果一个 pod 有多个容器,Kubernetes 会根据所有容器请求的总和来调度该 pod。需要注意的是,如果一个 pod 的总请求超出了集群中最大节点的容量,该 pod 将永远无法被调度。例如,假设集群中最大的节点可以提供 4000 m(四个核心)的 CPU 资源,那么既不能调度请求 4500 m CPU 的单容器 pod,也不能调度请求 2000 m 和 2500 m CPU 的两个容器 pod,因为没有节点能够满足它们的请求。
由于 Kubernetes 基于请求调度 Pods,如果所有 Pods 都没有任何请求或限制怎么办?在这种情况下,由于请求的总和为 0,它总是小于节点的容量,Kubernetes 会继续将 Pods 放置到节点上,直到超过节点的实际能力。默认情况下,节点上的唯一限制是可分配 Pods 的数量。它通过 kubelet 标志 --max-pods 进行配置。在前面的例子中,这是 110。另一个设置资源默认约束的工具是 LimitRange,我们将在本章稍后讨论。
除了 kubelet 的 --max-pods 标志外,我们还可以使用类似的标志 --pods-per-core,它限制每个核心最多可以运行的 Pods 数量。
服务质量(QoS)类
Kubernetes 仅使用请求来调度 Pods,因此所有调度到同一节点的 Pods 的限制总和可能会超过节点的容量。例如,我们可以有一个 1 Gi 内存的节点,但所有 Pods 的总限制可能会达到 1.1 Gi 或更多。这种模式允许 Kubernetes 对节点进行超额订阅,从而提高资源利用率。然而,节点上的可分配资源是有限的。如果一个没有资源限制的 Pod 消耗了所有资源并导致节点发生内存不足事件,Kubernetes 如何确保其他 Pods 仍能获得其请求的资源?Kubernetes 通过按 QoS 类对 Pods 进行排序来解决这个问题。
Kubernetes 中有三种不同的服务类:BestEffort、Burstable 和 Guaranteed。分类取决于 Pod 的请求和限制配置:
-
如果 Pod 中所有容器的请求和限制都为零或未指定,则该 Pod 属于
BestEffort类。 -
如果 Pod 中的任何容器请求至少一种类型的资源,无论数量如何,则该 Pod 为
Burstable。 -
如果 Pod 中所有容器的所有资源的限制都已设置,并且相同类型资源的请求数量等于限制,则该 Pod 被分类为
Guaranteed。
请注意,如果只设置了某个资源的限制,则相应的请求会自动设置为相同的数量。下表展示了一些常见的配置组合及其结果 QoS 类:
| 请求 | 空 | 设置为 0 | 设置 | 设置为一个数字 < 限制 | 无 | 设置 |
|---|---|---|---|---|---|---|
| 限制 | 空 | 设置为 0 | 无 | 设置 | 设置 | 设置 |
| QoS 类 | BestEffort |
BestEffort |
Burstable |
Burstable |
Guaranteed |
Guaranteed |
Pod 创建后,其 .status.qosClass 路径会显示相应的 QoS 类。
每个 QoS 类的示例可以在以下文件中找到:chapter8/8-1_scheduling/qos-pods.yml。你可以在以下代码块中查看配置及其结果类:
$ kubectl apply -f chapter8/8-1_qos/qos-pods.yml
pod/besteffort-nothing-specified created
...
$ kubectl get pod -o go-template --template=\
'{{range .items}}{{printf "pod/%s: %s\n" .metadata.name .status.qosClass}}{{end}}'
pod/besteffort-explicit-0-req: BestEffort
pod/besteffort-nothing-specified: BestEffort
pod/burstable-lim-lt-req: Burstable
pod/burstable-partial-req-multi-containers: Burstable
pod/guranteed: Guaranteed
pod/guranteed-lim-only: Guaranteed
每个类都有其优点和缺点:
-
BestEffort:此类中的 Pods 如果资源可用,可以使用节点上的所有资源。 -
Burstable:这一类的 Pod 被保证获得它们请求的资源,并且如果节点上有可用的额外资源,它们仍然可以使用这些资源。此外,如果有多个BurstablePod 需要比原本请求更多的 CPU 百分比,节点上的剩余 CPU 资源将按所有 Pod 请求的比例分配。例如,假设 Pod A 请求 200 m,Pod B 请求 300 m,而节点上有 1,000 m。在这种情况下,A 最多可以使用 200 m + 0.4 * 500 m = 400 m,而 B 将获得 300 m + 300 m = 600 m。 -
Guaranteed:Pods 被保证获得它们请求的资源,但不能使用超过设置限制的资源。
QoS 类的优先级从高到低依次是 Guaranteed > Burstable > BestEffort。如果节点遇到资源压力,需要立即采取措施回收稀缺资源,那么 Pods 会根据它们的优先级被终止或限流。例如,内存保证的实现是通过操作系统级别的 Out-Of-Memory (OOM) 杀手完成的。因此,通过根据 QoS 类调整 Pods 的 OOM 分数,节点的 OOM 杀手将知道在节点内存压力下,哪个 Pod 可以首先被回收。因此,尽管 Guaranteed Pods 看起来是最受限制的类,但它们也是集群中最安全的 Pods,因为它们的需求会尽可能地得到满足。
带约束的 Pod 放置
大多数时候,我们并不在乎我们的 Pods 运行在哪个节点上,因为我们只希望 Kubernetes 自动为我们的 Pods 安排足够的计算资源。然而,Kubernetes 在调度 Pod 时并不了解节点的地理位置、可用区或机器类型等因素。这种对环境缺乏认知使得在某些情况下很难处理 Pods 需要绑定到特定节点的情况,比如将测试版本部署在一个隔离的实例组中,将 I/O 密集型任务放到带有 SSD 磁盘的节点上,或者尽量将 Pods 安排得尽可能接近。因此,为了完成调度,Kubernetes 提供了不同级别的亲和性,允许我们根据标签和选择器主动将 Pods 分配到特定节点。
当我们输入 kubectl describe node 时,可以看到附加到节点上的标签:
$ kubectl describe node
Name: gke-mycluster-default-pool-25761d35-p9ds
Roles: <none>
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/fluentd-ds-ready=true
beta.kubernetes.io/instance-type=f1-micro
beta.kubernetes.io/kube-proxy-ds-ready=true
beta.kubernetes.io/os=linux
cloud.google.com/gke-nodepool=default-pool
cloud.google.com/gke-os-distribution=cos
failure-domain.beta.kubernetes.io/region=europe-west1
failure-domain.beta.kubernetes.io/zone=europe-west1-b
kubernetes.io/hostname=gke-mycluster-default-pool-25761d35-p9ds
...
kubectl get nodes --show-labels 允许我们仅获取节点的标签信息,而不是所有内容。
这些标签揭示了节点的一些基本信息以及其环境。为了方便起见,大多数 Kubernetes 平台上还提供了常用标签:
-
kubernetes.io/hostname -
failure-domain.beta.kubernetes.io/zone -
failure-domain.beta.kubernetes.io/region -
beta.kubernetes.io/instance-type -
beta.kubernetes.io/os -
beta.kubernetes.io/arch
这些标签的值可能因提供者而异。例如,failure-domain.beta.kubernetes.io/zone 在 AWS 中将是可用区的名称,如 eu-west-1b,在 GCP 中则是类似 europe-west1-b 的区域名称。此外,一些专用平台,如 minikube,并没有所有这些标签:
$ kubectl get node minikube -o go-template --template=\
'{{range $k,$v:=.metadata.labels}}{{printf "%s: %s\n" $k $v}}{{end}}'
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/hostname: minikube
node-role.kubernetes.io/master:
此外,如果你正在使用自托管的集群,可以使用 kubelet 的 --node-labels 标志,在加入集群时为节点附加标签。至于其他托管的 Kubernetes 集群,通常有方法自定义标签,例如在 GKE 上 NodeConfig 中的标签字段。
除了 kubelet 提供的这些预附加标签外,我们还可以手动为节点打标签,方法是更新节点的清单或使用快捷命令 kubectl label。以下示例将 purpose=sandbox 和 owner=alpha 两个标签添加到我们的一个节点上:
## display only labels on the node:
$ kubectl get node gke-mycluster-default-pool-25761d35-p9ds -o go-template --template='{{range $k,$v:=.metadata.labels}}{{printf "%s: %s\n" $k $v}}{{end}}'
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/fluentd-ds-ready: true
beta.kubernetes.io/instance-type: f1-micro
beta.kubernetes.io/kube-proxy-ds-ready: true
beta.kubernetes.io/os: linux
cloud.google.com/gke-nodepool: default-pool
cloud.google.com/gke-os-distribution: cos
failure-domain.beta.kubernetes.io/region: europe-west1
failure-domain.beta.kubernetes.io/zone: europe-west1-b
kubernetes.io/hostname: gke-mycluster-default-pool-25761d35-p9ds
## attach label
$ kubectl label node gke-mycluster-default-pool-25761d35-p9ds \
purpose=sandbox owner=alpha
node/gke-mycluster-default-pool-25761d35-p9ds labeled
## check labels again $ kubectl get node gke-mycluster-default-pool-25761d35-p9ds -o go-template --template='{{range $k,$v:=.metadata.labels}}{{printf "%s: %s\n" $k $v}}{{end}}'
...
kubernetes.io/hostname: gke-mycluster-default-pool-25761d35-p9ds
owner: alpha
purpose: sandbox
通过这些节点标签,我们可以描述各种概念。例如,我们可以指定某一组 Pods 应该只放在同一个可用区内的节点上。这个可以通过 failure-domain.beta.kubernetes.io/zone: az1 标签来表示。目前,我们可以使用两种方式来配置 Pod 的条件:nodeSelector 和 Pod/节点亲和性。
节点选择器
Pod 的节点选择器是最直观的手动放置 Pod 的方式。它类似于服务对象的 Pod 选择器,但它是选择节点——即,Pod 只会被放置在具有匹配标签的节点上。对应的标签键值对映射在 Pod 清单的 .spec.nodeSelector 中设置,格式如下:
...
spec:
nodeSelector:
<node_label_key>: <label_value>
...
可以在选择器中分配多个键值对,Kubernetes 会根据这些键值对的交集来找到符合条件的节点。例如,以下 spec Pod 的片段告诉 Kubernetes,我们希望 Pod 放在具有 purpose=sandbox 和 owner=alpha 标签的节点上:
...
spec:
containers:
- name: main
image: my-app
nodeSelector:
purpose: sandbox
owner: alpha
...
如果 Kubernetes 找不到具有这些标签对的节点,Pod 将无法调度,并且会被标记为 Pending 状态。此外,由于 nodeSelector 是一个映射,我们不能在选择器中分配两个相同的键,否则先前出现的键的值将被后来的值覆盖。
亲和性与反亲和性
尽管 nodeSelector 简单且灵活,但它仍然无法有效表达现实应用中的复杂需求。例如,我们通常不希望将一个StatefulSet的 Pod 放置在同一个可用区,以满足跨区冗余的需求。仅使用节点选择器来配置此类要求可能会很困难。因此,带有标签的约束调度概念已被扩展,以包括亲和性和反亲和性。
亲和性在两种不同的场景下起作用:pod 到节点和 pod 到 pod。它是在 pod 的 .spec.affinity 路径下配置的。第一种选项,nodeAffinity,与 nodeSelector 非常相似,但以更具表现力的方式制定 pod 和节点之间的关系。第二种选项表示 pod 之间的约束,有两种形式:podAffinity 和 podAntiAffinity。对于节点亲和性和 pod 亲和性,有两种不同程度的要求:
-
requiredDuringSchedulingIgnoredDuringExecution -
preferredDuringSchedulingIgnoredDuringExecution
从它们的名称可以看出,这两个要求在调度期间生效,而不是执行期间——也就是说,如果一个 pod 已经被调度到一个节点上,即使该节点的条件变得不适合调度 pod,它仍然会继续执行。至于 required 和 preferred,它们分别表示硬性约束和软性约束。对于满足必需条件的 pod,Kubernetes 会找到一个满足所有要求的节点来运行它;而对于偏好条件,Kubernetes 会尽力找到一个具有最高偏好的节点来运行该 pod。如果没有任何节点符合该偏好,那么该 pod 将不会被调度。偏好的计算是基于与要求的所有条款相关联的可配置 weight。对于已经满足所有其他必需条件的节点,Kubernetes 会遍历所有偏好条款,将每个匹配条款的权重相加作为节点的偏好得分。以下是一个示例:

Pod 对两个键有三种加权偏好:instance_type 和 region。在调度 pod 时,调度器会开始将这些偏好与节点上的标签进行匹配。在此示例中,由于节点 2具有 instance_type=medium 和 region=NA 标签,因此它获得了 15 分,这是所有节点中的最高分。因此,调度器会将 pod 调度到 节点 2 上。
节点亲和性和 pod 亲和性配置之间是有区别的。我们将分别讨论这两者。
节点亲和性
所需语句的描述称为 nodeSelectorTerms,由一个或多个 matchExpressions 组成。matchExpressions 与其他 Kubernetes 控制器(如 Deployment 和 StatefulSets)中使用的 matchExpressions 类似,但在这种情况下,matchExpressions 节点支持以下运算符:In、NotIn、Exists、DoesNotExist、Gt 和 Lt。
一个节点亲和性要求如下所示:
...
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: <key_1>
operator: <In, NotIn, Exists, DoesNotExist. Gt, or Lt>
values:
- <value_1>
- <value_2>
- ...
- key: <key_2>
...
- matchExpressions:
...
...
对于定义了多个nodeSelectorTerms的情况(每个术语都是matchExpression对象),如果满足任何一个nodeSelectorTerm,则所需的语句将被评估为true。但是,对于matchExpression对象中的多个表达式,如果所有matchExpressions都满足,则该术语会被评估为true,例如,如果我们有以下配置及其结果:
nodeSelectorTerms:
- matchExpressions: <- <nodeSelectorTerm_A>
- <matchExpression_A1> : true
- <matchExpression_A2> : true
- matchExpressions: <- <nodeSelectorTerm_B>
- <matchExpression_B1> : false
- <matchExpression_B2> : true
- <matchExpression_B3> : false
在应用前述AND/OR规则后,nodeSelectorTerms的评估结果将为true:
Term_A = matchExpression_A1 && matchExpression_A2
Term_B = matchExpression_B1 && matchExpression_B2 && matchExpression_B3
nodeSelectorTerms = Term_A || Term_B
>> (true && true) || (false && true && false)
>> true
In和NotIn运算符可以匹配多个值,而Exists和DoesNotExist不接受任何值(values: []);Gt和Lt仅接受字符串类型的单个整数值(values: ["123"])。
require语句可以替代nodeSelector。例如,affinity部分和以下nodeSelector部分是等效的:
...
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: purpose
operator: In
values: ["sandbox"]
- key: owner
operator: In
values: ["alpha"]
nodeSelector:
purpose: sandbox
owner: alpha
...
除了matchExpressions,还有另一个术语matchFields,用于选择标签以外的值。从 Kubernetes 1.13 开始,唯一支持的字段是metadata.name,用于选择一个节点,该节点的名称不等于kubernetes.io/hostname标签的值。其语法基本与matchExpression相同:{"matchFields":[{"key": "metadata.name", "operator": "In", "values": ["target-name"]}]}。
首选项的配置类似于所需语句,因为它们共享matchExpressions来表达关系。唯一的区别是,首选项有一个weight字段来表示其重要性,weight字段的范围是1-100:
...
preferredDuringSchedulingIgnoredDuringExecution:
- weight: <1-100>
preference:
- matchExpressions:
- key: <key_1>
operator: <In, NotIn, Exists, DoesNotExist. Gt, or Lt>
values:
- <value_1>
- <value_2>
- ...
- key: <key_2>
...
- matchExpressions:
...
...
如果我们将前一节中使用的图示中指定的条件写入首选项配置,它将如下所示:
...
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 5
preference:
matchExpressions:
- key: instance_type
operator: In
values:
- medium
- weight: 10
preference:
matchExpressions:
- key: instance_type
operator: In
values:
- large
- weight: 10
preference:
matchExpressions:
- key: region
operator: In
values:
- NA
...
Pod 间亲和性
尽管节点亲和性(node affinity)的扩展功能使调度变得更加灵活,但仍然存在一些未涵盖的情况。假设我们有一个简单的需求,比如将一个部署的 Pods 分配到不同的机器上——我们如何实现这个目标呢?这是一个常见的需求,但它并不像看起来那么简单。Pod 间亲和性(Inter-pod affinity)为我们提供了额外的灵活性,减少了解决此类问题所需的工作量。Pod 间亲和性作用于特定节点组中某些正在运行的 Pod 的标签。换句话说,它能够将我们的需求转化为 Kubernetes 的调度要求。我们可以指定,例如,一个 Pod 不应与另一个具有某些标签的 Pod 一起放置。以下是一个 Pod 间亲和性需求的定义:
...
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: <key_1>
operator: <In, NotIn, Exists, or DoesNotExist>
values:
- <value_1>
- <value_2>
...
- key: <key_2>
...
topologyKey: <a key of a node label>
namespaces:
- <ns_1>
- <ns_2>
...
...
其结构几乎与节点亲和性相同。不同之处如下:
-
Pod 间亲和性要求使用有效的命名空间。与节点不同,Pod 是一个有命名空间的对象,因此我们必须告诉 Kubernetes 我们指的是哪个命名空间。如果命名空间字段为空,Kubernetes 会假设目标 Pod 与指定亲和性的 Pod 位于同一命名空间。
-
描述要求的术语
labelSelector与Deployment等控制器中使用的是相同的。因此,支持的操作符为In、NotIn、Exists和DoesNotExist。 -
topologyKey用于定义节点的搜索范围,是必填字段。请注意,topologyKey应该是节点标签的键,而不是 Pod 标签的键。
为了更清晰地理解 topologyKey,请参考下面的示意图:

我们希望 Kubernetes 为我们的新 Pod (Pod 7) 找到一个位置,并且有亲和性要求,不能与其他具有特定标签键值对的 Pod 一起放置,比如 app=main。如果亲和性的 topologyKey 是 hostname,那么调度器会评估 Pod 1 和 Pod 2 的标签,Pod 3 的标签,以及 Pod 4、Pod 5 和 Pod 6 的标签。我们的新 Pod 会被分配到节点 2 或节点 3,这对应于前一个示意图上部的红色勾选。如果 topologyKey 是 az,那么搜索范围会变成 Pod 1、Pod 2 和 Pod 3 的标签,和 Pod 4、Pod 5 和 Pod 6 的标签。因此,唯一可能的节点是 Node 3。
跨 Pod 亲和性和节点亲和性的偏好及其 weight 参数是相同的。以下是使用偏好来尽可能将 Deployment 的 Pod 放得更近的示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: colocate
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- image: busybox
name: myapp
command: ["sleep", "30"]
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 10
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- myapp
topologyKey: "kubernetes.io/hostname"
使得跨 Pod 亲和性与节点亲和性有所不同的另一个因素是反亲和性(podAntiAffinity)。反亲和性是一个声明的评估结果的逆。以之前的共驻部署为例;如果我们将 podAffinity 改为 podAntiAffinity,它将变成一个分布式部署:
...
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 10
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- myapp
topologyKey: "kubernetes.io/hostname"
...
该表达式非常灵活。另一个例子是,如果我们在之前的偏好中使用 failure-domain.beta.kubernetes.io/zone 作为 topologyKey,则部署策略会将 Pod 分配到不同的可用区,而不仅仅是分配到不同的节点上。
请记住,从逻辑上讲,你无法像前面的示例那样,通过 requiredDuringSchedulingIgnoredDuringExecution Pod 亲和性实现共驻部署,因为如果任何节点上没有具有所需标签的 Pod,则不会调度任何 Pod。
然而,自由度的代价是巨大的。Pod 亲和性的计算复杂度非常高。因此,如果我们运行的是一个包含数百个节点和数千个 Pod 的集群,使用 Pod 亲和性的调度速度将显著变慢。同时,topologyKey 也有一些限制,以保持调度性能在合理水平:
-
不允许使用空的
topologyKey。 -
requiredDuringSchedulingIgnoredDuringExecution的 Pod 反亲和性的topologyKey可以通过LimitPodHardAntiAffinityTopology准入控制器限制为仅使用kubernetes.io/hostname。
在调度中优先考虑 Pod
服务质量保证了一个 Pod 可以访问适当的资源,但这种哲学并没有考虑 Pod 的重要性。更准确地说,QoS 只在 Pod 被调度时起作用,而不是在调度过程中。因此,我们需要引入一个正交特性来表示 Pod 的关键性或重要性。
在 1.11 版本之前,通过将 Pod 放入 kube-system 命名空间并使用 scheduler.alpha.kubernetes.io/critical-pod 注解来使 Pod 的关键性对 Kubernetes 可见,该方式将在 Kubernetes 的新版本中被弃用。有关更多信息,请参阅 kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/。
Pod 的优先级由它所属的优先级类定义。优先级类使用一个小于 1e9(一十亿)的 32 位整数来表示优先级。数字越大,优先级越高。大于一十亿的数字保留给系统组件。例如,关键组件的优先级类使用二十亿:
apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
name: system-cluster-critical
value: 2000000000
description: Used for system critical pods that must run in the cluster, but can be moved to another node if necessary.
由于优先级类不是集群范围的(它是无命名空间的),可选的描述字段帮助集群用户了解是否应该使用某个类。如果创建一个 Pod 时没有指定其类,则其优先级将是默认优先级类的值或 0,具体取决于集群中是否存在默认优先级类。默认优先级类通过在优先级类的规范中添加 globalDefault:true 字段来定义。请注意,集群中只能有一个默认优先级类。Pod 的配置对应项在 .spec.priorityClassName 路径下。
优先级特性的原理很简单:如果有等待调度的 Pod,Kubernetes 将首先选择优先级更高的 Pod,而不是按照队列中 Pod 的顺序进行调度。但如果所有节点都无法为新 Pod 提供服务呢?如果集群中启用了 Pod 抢占(从 Kubernetes 1.11 版本开始默认启用),则会触发抢占过程,为更高优先级的 Pod 腾出空间。更具体地说,调度器将评估 Pod 的亲和性或节点选择器,以寻找符合条件的节点。然后,调度器会根据 Pod 的优先级在这些符合条件的节点上找到需要驱逐的 Pod。如果在某个节点上删除 所有 优先级低于待调度 Pod 优先级的 Pod 可以为待调度 Pod 腾出空间,则这些低优先级的 Pod 将会被抢占。
有时移除所有容器组会导致意外的调度结果,同时考虑到容器组的优先级以及它与其他容器组的亲和性。例如,假设节点上有几个正在运行的容器组,同时有一个待调度的容器组叫做 Pod-P。假设 Pod-P 的优先级高于节点上所有其他容器组,它可以抢占目标节点上每个正在运行的容器组。Pod-P 还具有一个要求与节点上某些容器组一起运行的亲和性。结合优先级和亲和性,我们会发现 Pod-P 不会被调度。这是因为优先级较低的容器组也会被考虑进去,即使 Pod-P 并不需要移除所有容器组来在节点上运行。最终,由于移除与 Pod-P 亲和的容器组会破坏亲和性,该节点被视为不适合 Pod-P。
抢占过程并不考虑 QoS 类别。即使一个容器组属于guaranteed QoS 类别,它仍然可能被具有更高优先级的 best-effort 容器组抢占。我们可以通过实验来看抢占如何与 QoS 类别配合工作。在这里,我们将使用minikube进行演示,因为它只有一个节点,这样我们可以确保调度器会尝试将所有容器组运行在同一个节点上。如果你打算在一个有多个节点的集群中做同样的实验,亲和性可能会有所帮助。
首先,我们需要一些优先级类,这些可以在chapter8/8-1_scheduling/prio-demo.yml文件中找到。只需按照以下方式应用该文件:
$ kubectl apply -f chapter8/8-1_scheduling/prio-demo.yml
priorityclass.scheduling.k8s.io/high-prio created
priorityclass.scheduling.k8s.io/low-prio created
接下来,我们来看看我们的minikube节点能提供多少内存:
$ kubectl describe node minikube | grep -A 6 Allocated
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 675m (33%) 20m (1%)
memory 150Mi (7%) 200Mi (10%)
我们的节点大约有 93%的可分配内存。我们可以在低优先级类别中安排两个内存请求为 800 MB 的容器组,以及一个优先级较高的容器组,其内存请求为 80 MB,并且有一定的 CPU 限制。两个部署的示例模板分别可以在chapter8/8-1_scheduling/{lowpods-gurantee-demo.yml,highpods-burstable-demo.yml}中找到。创建这两个部署:
$ kubectl apply -f lowpods-gurantee-demo.yml
deployment.apps/lowpods created
$ kubectl apply -f highpods-burstable-demo.yml
deployment.apps/highpods created
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
highpods-77dd55b549-sdpbv 1/1 Running 0 6s 172.17.0.9 minikube <none>
lowpods-65ff8966fc-xnv4v 1/1 Running 0 23s 172.17.0.7 minikube <none>
lowpods-65ff8966fc-xswjp 1/1 Running 0 23s 172.17.0.8 minikube <none>
$ kubectl describe node | grep -A 6 Allocated
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 775m (38%) 120m (6%)
memory 1830Mi (96%) 1800Mi (95%)
$ kubectl get pod -o go-template --template='{{range .items}}{{printf "pod/%s: %s, priorityClass:%s(%.0f)\n" .metadata.name .status.qosClass .spec.priorityClassName .spec.priority}}{{end}}'
pod/highpods-77dd55b549-sdpbv: Burstable, priorityClass:high-prio(100000)
pod/lowpods-65ff8966fc-xnv4v: Guaranteed, priorityClass:low-prio(-1000)
pod/lowpods-65ff8966fc-xswjp: Guaranteed, priorityClass:low-prio(-1000)
我们可以看到这三个容器组运行在同一个节点上。同时,该节点面临着容量耗尽的风险。两个低优先级的容器组属于guaranteed QoS 类别,而优先级较高的容器组则属于burstable 类别。现在,我们只需要再添加一个高优先级的容器组:
$ kubectl scale deployment --replicas=2 highpods
deployment.extensions/highpods scaled
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
highpods-77dd55b549-g2m6t 0/1 Pending 0 3s <none> <none> minikube
highpods-77dd55b549-sdpbv 1/1 Running 0 20s 172.17.0.9 minikube <none>
lowpods-65ff8966fc-rsx7j 0/1 Pending 0 3s <none> <none> <none>
lowpods-65ff8966fc-xnv4v 1/1 Terminating 0 37s 172.17.0.7 minikube <none>
lowpods-65ff8966fc-xswjp 1/1 Running 0 37s 172.17.0.8 minikube <none>
$ kubectl describe pod lowpods-65ff8966fc-xnv4v
...
Events:
...
Normal Started 41s kubelet, minikube Started container
Normal Preempted 16s default-scheduler by default/highpods-77dd55b549-g2m6t on node minikube
一旦我们添加了一个高优先级的容器组,低优先级的一个容器组就会被终止。从事件消息中,我们可以清楚地看到容器组被终止的原因是容器组被抢占,即使它属于guaranteed类别。需要注意的是,新的低优先级容器组lowpods-65ff8966fc-rsx7j是由其部署启动的,而不是由容器组上的restartPolicy启动的。
弹性扩展
当应用程序达到其容量时,解决问题的最直观方式是通过为应用程序增加更多的计算资源。然而,过度配置资源也是我们想要避免的情况,我们希望将多余的资源分配给其他应用程序。对于大多数应用程序来说,由于物理硬件的限制,扩展横向(scaling out)通常比扩展纵向(scaling up)更为推荐。就 Kubernetes 而言,从服务所有者的角度来看,扩展或缩减可以像增加或减少部署中的 Pod 数量一样简单,且 Kubernetes 内置支持自动执行此类操作,这就是 水平 Pod 自动扩展器(HPA)。
根据你使用的基础设施,你可以通过多种方式扩展集群的容量。还有一个附加组件 集群自动扩展器,可以根据你的需求增加或减少集群的节点数,
github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler,如果你所使用的基础设施支持的话。
另一个附加组件,垂直 Pod 自动扩展器,也可以帮助我们自动调整 Pod 的请求:github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler。
水平 Pod 自动扩展器
HPA 对象会在给定的时间间隔内监控由控制器(Deployment、ReplicaSet 或 StatefulSet)管理的 Pod 的资源消耗,并通过将某些指标的目标值与其实际使用情况进行比较来控制副本数。例如,假设我们有一个初始时包含两个 Pod 的 Deployment 控制器,它们当前平均使用 1000 m 的 CPU,而我们希望每个 Pod 的 CPU 使用量为 200 m。相关的 HPA 将通过 2(1000 m/200 m) = 10* 来计算所需的 Pod 数量,因此它会相应地调整控制器的副本数为 10 个 Pod。Kubernetes 将负责调度其余的八个新 Pod。
默认的评估间隔是 15 秒,其配置在控制器管理器的标志 --horizontal-pod-autoscaler-sync-period 中。
HPA 的清单如下所示:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: someworkload-scaler
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: someworkload
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
.spec.scaleTargetRef 字段指向我们希望通过 HPA 扩展的控制器,并支持 Deployment 和 StatefulSet。minReplicas/maxReplicas 参数设置了一个限制,以防止工作负载过度扩展,从而避免集群中的所有资源被耗尽。metrics 字段告诉 HPA 应该关注哪些指标,以及我们为工作负载设置的目标是什么。指标有四种有效类型,代表不同的来源,分别是 Resource、Pods、Object 和 External。我们将在下一节讨论后面三种指标。
在Resource类型的指标中,我们可以指定两个不同的核心指标:cpu 和 memory。事实上,这两个指标的来源与我们在kubectl top中看到的一样——更具体地说,它们来自资源指标API(metics.k8s.io)。因此,我们需要在集群中部署一个 metrics server 才能利用 HPA。最后,目标类型(.resource.target.*)指定了 Kubernetes 应如何聚合记录的指标。支持的方法如下:
Utilization:pod 的利用率是 pod 实际使用与它在资源上请求的比率。也就是说,如果 pod 没有设置我们在这里指定的资源请求,HPA 将不会做任何操作:
target:
type: Utilization
averageUtilization: <integer>, e.g. 75
AverageValue:这是资源在所有相关 pod 中的平均值。这个量的表示方式与我们指定请求或限制时是一样的:
target:
type: AverageValue
averageValue: <quantity>, e.g. 100Mi
我们还可以在 HPA 中指定多个指标,根据不同的情况扩展 pod。在这种情况下,它的副本数将是所有单独评估目标中最大的数字。
还有一个较旧版本的水平 pod 自动扩展器(autoscaling/v1),它支持的选项比 v2 少得多。使用 HPA 时,请务必小心选择 API 版本。
让我们通过一个简单的例子来看 HPA 如何运行。该部分的模板文件可以在chapter8/8-2_scaling/hpa-resources-metrics-demo.yml中找到。工作负载将从一个 pod 开始,且 pod 将在三分钟内消耗 150 m 的 CPU:
$ kubectl apply -f chapter8/8-2_scaling/hpa-resources-metrics-demo.yml
deployment.apps/someworkload created
horizontalpodautoscaler.autoscaling/someworkload-scaler created
在指标被 metrics server 收集后,我们可以通过使用kubectl describe来查看 HPA 的扩展事件:
$ kubectl describe hpa someworkload-scaler
...(some output are omitted)...
Reference: Deployment/someworkload
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 151% (151m) / 50%
Min replicas: 1
Max replicas: 5
Deployment pods: 1 current / 4 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True SucceededRescale the HPA controller was able to update the target scale to 4
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
ScalingLimited False DesiredWithinRange the desired count is within the acceptable range
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
...
Normal SuccessfulRescale 4s horizontal-pod-autoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target
尽管限制是 150 m,但请求是 100 m,因此我们可以看到测量的 CPU 百分比为 151%。由于我们的目标利用率是 50%,期望的副本数为 ceil(1151/50)=4*,这一点可以在事件消息的底部看到。请注意,HPA 对小数结果应用了向上取整(ceil)。因为我们的工作负载非常“贪心”,即使我们有三个新 pod,平均利用率仍然是 150%。几秒钟后,HPA 决定再次扩展:
$ kubectl describe hpa someworkload-scaler
...
Normal SuccessfulRescale 52s horizontal-pod-autoscaler New size: 5; reason: cpu resource utilization (percentage of request) above target
这次的目标数为 5,它小于估算值,3(150/50) = 9*。当然,这受到maxReplicas的限制,它可以避免破坏集群中其他容器的运行。由于已经过去了 180 秒,工作负载开始进入睡眠状态,我们应该看到 HPA 会逐渐将 pod 数量调整为 1:
$ kubectl describe hpa someworkload-scaler
Normal SuccessfulRescale 14m horizontal-pod-autoscaler New size: 3; reason: All metrics below target
Normal SuccessfulRescale 13m horizontal-pod-autoscaler New size: 1; reason: All metrics below target
整合自定义指标
尽管根据 CPU 和内存使用情况进行 pod 扩展非常直观,但有时它不足以覆盖一些情况,比如基于网络连接、磁盘 IOPS 和数据库事务的扩展。因此,引入了自定义指标 API 和外部指标 API 以便 Kubernetes 组件可以访问那些不被支持的指标。我们提到过,除了Resource之外,HPA 中还有Pods、Object 和 External 类型的指标。
Pods和Object指标指的是由 Kubernetes 内部的对象生成的指标。当 HPA 查询一个指标时,相关的元数据,如 Pod 名称、命名空间和标签,会发送到自定义指标 API。另一方面,External指标指的是集群外的事物,例如来自云服务商的数据库服务的指标,这些指标只通过指标名称从外部指标 API 中获取。它们之间的关系如下所示:

我们知道,metrics server 是一个在集群内部运行的程序,但自定义指标和外部指标 API 服务到底是什么呢?Kubernetes 并不认识所有监控系统和外部服务,因此它提供了 API 接口来集成这些组件。如果我们的监控系统支持这些接口,我们可以将其注册为指标 API 的提供者;否则,我们需要一个适配器来将 Kubernetes 的元数据转换为我们监控系统中的对象。以此类推,我们还需要添加外部指标 API 接口的实现才能使用它。
在第七章中,监控与日志记录,我们使用 Prometheus 构建了一个监控系统,但它不支持自定义和外部指标 API。我们需要一个适配器来连接 HPA 和 Prometheus,例如 Prometheus 适配器(github.com/DirectXMan12/k8s-prometheus-adapter)。
对于其他监控解决方案,这里列出了针对不同监控提供商的适配器:github.com/kubernetes/metrics/blob/master/IMPLEMENTATIONS.md#custom-metrics-api。
如果列出的实现都不支持你的监控系统,仍然有一个 API 服务模板,用于构建自己的适配器来支持自定义和外部指标:github.com/kubernetes-incubator/custom-metrics-apiserver。
要使一个服务在 Kubernetes 中可用,它必须在聚合层下注册为 API 服务。我们可以通过显示相关的apiservices对象来查找哪个服务是 API 服务的后端:
-
v1beta1.metrics.k8s.io -
v1beta1.custom.metrics.k8s.io -
v1beta1.external.metrics.k8s.io
我们可以看到,位于kube-system中的metrics-server服务作为Resource指标的来源:
$ kubectl describe apiservices v1beta1.metrics.k8s.io
Name: v1beta1.metrics.k8s.io
...
Spec:
Group: metrics.k8s.io
Group Priority Minimum: 100
Insecure Skip TLS Verify: true
Service:
Name: metrics-server
Namespace: kube-system
Version: v1beta1
...
基于 Prometheus 适配器部署说明的示例模板可在我们的仓库中找到(chapter8/8-2_scaling/prometheus-k8s-adapter),并已配置为我们在第七章中部署的 Prometheus 服务,监控与日志记录。你可以按以下顺序进行部署:
$ kubectl apply -f custom-metrics-ns.yml
$ kubectl apply -f gen-secrets.yml
$ kubectl apply -f configmap.yml
$ kubectl apply -f adapter.yml
示例中仅配置了默认的指标转换规则。如果你希望将自定义的指标提供给 Kubernetes,你需要根据项目的指引(https://github.com/DirectXMan12/k8s-prometheus-adapter/blob/master/docs/config.md)定制自己的配置。
为了验证安装情况,我们可以查询以下路径,看是否有任何指标从我们的监控后台返回(jq 仅用于格式化结果):
$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/" | jq '.resources[].name'
"namespaces/network_udp_usage"
"pods/memory_usage_bytes"
"namespaces/spec_cpu_period"
...
回到 HPA,非资源类型指标的配置与资源类型指标非常相似。Pods 类型的规格片段如下:
...
metrics:
- type: Pods
pods:
metric:
name: <metrics-name>
selector: <optional, a LabelSelector object>
target:
type: AverageValue or Value
averageValue or value: <quantity>
...
Object 类型指标的定义如下:
...
metrics:
- type: Object
pods:
metric:
name: <metrics-name>
selector: <optional, a LabelSelector object>
describedObject:
apiVersion: <api version>
kind: <kind>
name: <object name>
target:
type: AverageValue or Value
averageValue or value: <quantity>
...
External 类型的指标语法几乎与 Pods 类型的指标相同,唯一的不同之处在于以下部分:
- type: External
external:
假设我们指定一个 Pods 类型的指标,指标名称为 fs_read,并且与之相关联的控制器是 Deployment,该控制器选择 app=worker。在这种情况下,HPA 会向自定义指标服务器发送以下信息:
-
namespace:HPA 的命名空间 -
指标名称:
fs_read -
labelSelector:app=worker
此外,如果我们配置了可选的指标选择器 <type>.metric.selector,它也会传递给后台。前述示例的查询,加上指标选择器 app=myapp,可能如下所示:
/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/fs_read?labelSelector=app=worker&metricLabelSelector=app=myapp
在 HPA 获取到指标的值后,它会通过 AverageValue 或原始的 Value 来聚合该指标,以决定是否进行扩展。请记住,这里不支持 Utilization 方法。
对于 Object 类型的指标,唯一的区别是 HPA 会在查询中附加被引用对象的信息。例如,我们在 default 命名空间中有如下配置:
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gateway
...
metric:
name: rps
selector:
matchExpressions:
- key: app
operator: In
values:
- gwapp
describedObject:
apiVersion: extensions/v1beta1
kind: Ingress
name: cluster-ingress
对监控后台的查询将如下所示:
/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/ingresses.extensions/cluster-ingress/rps?metricLabelSelector=app+in+(gwapp)
请注意,不会传递关于目标控制器的信息,且我们无法引用其他命名空间中的对象。
管理集群资源
随着资源利用率的增加,我们的集群容量更容易耗尽。此外,当大量 Pod 动态地进行独立扩展和收缩时,预测何时向集群添加更多资源可能会变得极其困难。为了防止集群瘫痪,我们可以采取多种措施。
命名空间的资源配额
默认情况下,Kubernetes 中的 Pod 是资源无限制的。运行中的 Pod 可能会耗尽集群中的所有计算或存储资源。ResourceQuota 是一种资源对象,允许我们限制命名空间可使用的资源消耗。通过设置资源限制,我们可以减少噪音邻居现象,确保 Pod 可以持续运行。
Kubernetes 当前支持三种资源配额:
| 计算资源 |
|---|
-
requests.cpu -
requests.memory -
limits.cpu -
limits.memory
|
| 存储资源 |
|---|
-
requests.storage -
<sc>.storageclass.storage.k8s.io/requests -
<sc>.storageclass.storage.k8s.io/persistentvolumeclaims
|
| 对象计数 |
|---|
-
count/<resource>.<group>,例如,以下:-
count/deployments.apps -
count/persistentvolumeclaims
-
-
services.loadbalancers -
services.nodeports
|
计算资源非常直观,限制所有相关对象的给定资源的总和。需要注意的一点是,一旦设置了计算配额,任何没有资源请求或限制的 Pod 创建请求都会被拒绝。
对于存储资源,我们可以在配额中关联存储类。例如,我们可以同时配置两个配额,fast.storageclass.storage.k8s.io/requests: 100G和meh.storageclass.storage.k8s.io/requests: 700G,以区分我们安装的资源类,从而合理地分配资源。
已存在的资源不会受到新创建的资源配额的影响。如果资源创建请求超过了指定的ResourceQuota,则这些资源将无法启动。
创建资源配额
ResourceQuota的语法如下所示。请注意,它是一个命名空间对象:
apiVersion: v1
kind: ResourceQuota
metadata:
name: <name>
spec:
hard:
<quota_1>: <count> or <quantity>
...
scopes:
- <scope name>
...
scopeSelector:
- matchExpressions:
scopeName: PriorityClass
operator: <In, NotIn, Exists, DoesNotExist>
values:
- <PriorityClass name>
只有.spec.hard是必填字段;.spec.scopes和.spec.scopeSelector是可选的。.spec.hard的配额名称为前述表格中列出的名称,且其值仅可为计数或数量。例如,count/pods: 10将 Pod 的数量限制为 10 个,而requests.cpu: 10000m确保不会超过指定的请求量。
这两个可选字段用于将资源配额关联到特定的范围,因此只有在该范围内的对象和使用情况才会被纳入关联配额。目前,.spec.scopes字段有四种不同的范围:
-
Terminating/NotTerminating:Terminating范围匹配.spec.activeDeadlineSeconds >= 0的 Pod,而NotTerminating匹配未设置该字段的 Pod。需要注意的是,Job也有截止日期字段,但不会传递到Job创建的 Pod 中。 -
BestEffort/NotBestEffort:前者适用于处于BestEffortQoS 类的 Pod,后者适用于其他 QoS 类的 Pod。由于在 Pod 上设置请求或限制会将 Pod 的 QoS 类提升为非BestEffort,因此BestEffort范围不适用于计算配额。
另一个范围配置,scopeSelector,用于选择具有更灵活语法的对象,尽管截至 Kubernetes 1.13,仅支持PriorityClass。通过scopeSelector,我们可以将资源配额绑定到特定的优先级类,并使用相应的PriorityClassName。
那么,让我们看看配额是如何在示例中工作的,示例可以在 chapter8/8-3_management/resource_quota.yml 中找到。在该模板中,两个资源配额分别限制了 BestEffort 和其他 QoS 的 pod 数量(quota-pods)和资源请求(quota-resources)。在这种配置下,预期的结果是通过 pod 数量限制没有请求的工作负载,并限制有请求的工作负载的资源量。因此,示例中的两个任务 capybara 和 politer-capybara,它们设置了较高的并行度但在不同的 QoS 类别中,将分别受到两个不同资源配额的限制:
$ kubectl apply -f resource_quota.yml
namespace/team-capybara created
resourcequota/quota-pods created
resourcequota/quota-resources created
job.batch/capybara created
job.batch/politer-capybara created
$ kubectl get pod -n team-capybara
NAME READY STATUS RESTARTS AGE
capybara-4wfnj 0/1 Completed 0 13s
politer-capybara-lbf48 0/1 ContainerCreating 0 13s
politer-capybara-md9c7 0/1 ContainerCreating 0 12s
politer-capybara-xkg7g 1/1 Running 0 12s
politer-capybara-zf42k 1/1 Running 0 12s
如我们所见,即使这两个任务的并行度为 20 个 pod,实际上仅创建了少数几个 pod。它们控制器的消息确认它们已经达到了资源配额:
$ kubectl describe jobs.batch -n team-capybara capybara
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 98s job-controller Created pod: capybara-4wfnj
Warning FailedCreate 97s job-controller Error creating: pods "capybara-ds7zk" is forbidden: exceeded quota: quota-pods, requested: count/pods=1, used: count/pods=1, limited: count/pods=1
...
$ kubectl describe jobs.batch -n team-capybara politer-capybara
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 86s job-controller Error creating: pods "politer-capybara-xmm66" is forbidden: exceeded quota: quota-resources, requested: requests.cpu=25m, used: requests.cpu=100m, limited: requests.cpu=100m
...
我们还可以通过在 Namespace 或 ResourceQuota 上使用 describe 来查看消耗统计数据:
## from namespace
$ kubectl describe namespaces team-capybara
Name: team-capybara
...
Resource Quotas
Name: quota-pods
Scopes: BestEffort
* Matches all pods that do not have resource requirements set. These pods have a best effort quality of service.
Resource Used Hard
-------- --- ---
count/pods 1 1
Name: quota-resources
Scopes: NotBestEffort
* Matches all pods that have at least one resource requirement set. These pods have a burstable or guaranteed quality of service.
Resource Used Hard
-------- --- ---
requests.cpu 100m 100m
requests.memory 100M 1Gi
No resource limits.
## from resourcequotas
$ kubectl describe -n team-capybara resourcequotas
Name: quota-pods
Namespace: team-capybara
...
(information here is the same as above)
请求带有默认计算资源限制的 pod
我们还可以为命名空间指定默认的资源请求和限制。如果我们在创建 pod 时没有指定请求和限制,将使用默认设置。技巧是使用一个 LimitRange 对象,它包含一组 defaultRequest(请求)和 default(限制)。
LimitRange 由 LimitRange 审批控制器插件控制。如果您启动自托管解决方案,请确保启用此插件。更多信息,请查看本章的 Admission Controller 部分。
以下是可以在 chapter8/8-3_management/limit_range.yml 中找到的示例:
apiVersion: v1
kind: LimitRange
metadata:
name: limitcage-container
namespace: team-capybara
spec:
limits:
- default:
cpu: 0.5
memory: 512Mi
defaultRequest:
cpu: 0.25
memory: 256Mi
type: Container
当我们在这个命名空间内启动 pods 时,即使在 ResourceQuota 中设置了总限制,我们也无需每次指定 cpu 和 memory 请求及限制。
我们还可以在 LimitRange 中为容器设置最小和最大 CPU 及内存值。LimitRange 的作用不同于默认值。默认值仅在 pod 规格中不包含请求和限制时使用。最小和最大约束用于验证 pod 是否请求了过多的资源。语法为 spec.limits[].min 和 spec.limits[].max。如果请求超出了最小和最大值,服务器将抛出 forbidden 错误:
...
spec:
limits:
- max:
cpu: 0.5
memory: 512Mi
min:
cpu: 0.25
memory: 256Mi
除了 type: Container 外,LimitRange 还有 Pods 和 PersistentVolumeClaim 类型。对于 Container 类型的限制范围,它单独对 Pods 中的容器进行限制,因此 Pod 类型的限制范围会检查 pod 中所有容器的资源总和。但与 Container 类型限制范围不同,Pods 和 PersistentVolumeClaim 类型的限制范围没有 default 和 defaultRequest 字段,这意味着它们仅用于验证与之关联的资源类型的请求。PersistentVolumeClaim 类型的资源限制范围是 storage。您可以在 chapter8/8-3_management/limit_range.yml 模板中找到完整定义。
除了之前提到的绝对请求和限制约束外,我们还可以通过比例来限制资源:maxLimitRequestRatio。例如,如果我们在 CPU 上设置了 maxLimitRequestRatio:1.2,那么一个请求 CPU 为 requests:50m,限制 CPU 为 limits: 100m 的 Pod 将会被拒绝,因为 100m/50m > 1.2。
与 ResourceQuota 一样,我们可以通过描述 Namespace 或 LimitRange 来查看评估的设置:
$ kubectl describe namespaces <namespace name>
...
Resource Limits
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container memory - - 256Mi 512Mi -
Container cpu - - 250m 500m -
Pod cpu - - - - 1200m
PersistentVolumeClaim storage 1Gi 10Gi - - -
节点管理
无论我们如何仔细分配和管理集群中的资源,节点上总有可能发生资源耗尽的情况。更糟糕的是,从死掉的主机重新调度的 Pod 可能会影响其他节点,导致所有节点在稳定和不稳定状态之间震荡。幸运的是,我们正在使用 Kubernetes,kubelet 有办法处理这些不幸的事件。
Pod 驱逐
为了保持节点稳定,kubelet 会保留一些资源作为缓冲区,以确保在节点的内核采取行动之前可以采取措施。根据不同的目的,有三个可配置的隔离或阈值:
-
kube-reserved:为 Kubernetes 的节点组件保留资源 -
system-reserved:为系统守护进程保留资源 -
eviction-hard:驱逐 Pod 的阈值
因此,节点的可分配资源是通过以下公式计算的:
Allocatable =
[Node Capacity] - <kube-reserved> - <system-reserved> - <eviction-hard>
Kubernetes 和系统预留资源应用于 cpu、memory 和 ephemeral-storage 资源,它们通过 kubelet 标志 --kube-reserved 和 --system-reserved 配置,语法为 cpu=1,memory=500Mi,ephemeral-storage=10Gi。除了资源配置外,还需要手动指定预配置的 cgroups 名称,如 --kube-reserved-cgroup=<cgroupname> 和 --system-reserved-cgroup=<cgroupname>。此外,由于它们是通过 cgroups 实现的,系统或 Kubernetes 组件可能会因为不合适的小资源预留而受到限制。
驱逐阈值在五个关键驱逐信号上生效:
| 驱逐信号 | 默认值 |
|---|
|
-
memory.available -
nodefs.available -
nodefs.inodesFree -
imagefs.available -
imagefs.inodesFree
|
-
memory.available<100Mi -
nodefs.available<10% -
nodefs.inodesFree<5% -
imagefs.available<15%
|
我们可以在节点的 /configz 端点验证默认值:
## run a proxy at the background
$ kubectl proxy &
## pipe to json_pp/jq/fx for a more beautiful formatting
$ curl -s http://127.0.0.1:8001/api/v1/nodes/minikube/proxy/configz | \
jq '.[].evictionHard'
{
"imagefs.available": "15%",
"memory.available": "100Mi",
"nodefs.available": "10%",
"nodefs.inodesFree": "5%"
}
我们还可以检查节点的可分配资源与总容量之间的差异,这应该与我们之前提到的可分配公式一致。minikube 节点默认没有设置 Kubernetes 或系统预留资源,因此内存的差异将是 memory.available 阈值:
**$ VAR=$(kubectl get node minikube -o go-template --template='{{printf "%s-%s\n" .status.capacity.memory .status.allocatable.memory}}') && printf $VAR= && tr -d 'Ki' <<< ${VAR} | bc**
**2038700Ki-1936300Ki=102400**
如果在驱逐阈值中标注的任何资源发生匮乏,系统可能会开始出现异常行为,这可能会危及节点的稳定性。因此,一旦节点状态突破阈值,kubelet 会将节点标记为以下两种状态之一:
-
MemoryPressure:如果memory.available超过其阈值 -
DiskPressure:如果任何nodefs.*/imagefs.*超过其阈值
Kubernetes 调度器和 kubelet 会根据节点的状态调整策略,只要它们感知到该条件。如果节点遇到内存压力,调度器将停止将 BestEffort 类型的 pod 调度到该节点;如果节点正面临磁盘压力条件,调度器则会停止将所有 pod 调度到该节点。kubelet 会立即采取措施回收匮乏的资源,也就是将该节点上的 pod 驱逐。与被杀死的 pod 不同,被驱逐的 pod 如果有足够的容量,将最终被重新调度到其他节点。
除了硬阈值,我们可以为信号配置软阈值,使用 eviction-soft。当达到软阈值时,kubelet 将先等待一段时间(eviction-soft-grace-period),然后它会尝试优雅地移除 pod,最大等待时间为 (eviction-max-pod-grace-period)。例如,假设我们有以下配置:
-
eviction-soft=memory.available<1Gi -
eviction-soft-grace-period=memory.available=2m -
eviction-max-pod-grace-period=60s
在这种情况下,在 kubelet 采取行动之前,如果节点的可用内存小于 1 Gi,但仍符合前述硬驱逐阈值,它将等待两分钟。之后,kubelet 将开始清除节点上的 pod。如果某个 pod 在 60 秒内没有退出,kubelet 会直接杀死它。
pod 的驱逐顺序是根据 pod 在匮乏资源上的 QoS 类别以及 pod 的优先级类别进行排名的。假设 kubelet 现在感知到 MemoryPressure 状态,它会开始比较所有 pod 的属性和实际内存使用情况:
| Pod | Request | Real usage | Above request | QoS | Priority |
|---|---|---|---|---|---|
| A | 100Mi | 50Mi | - | Burstable |
100 |
| B | 100Mi | 200Mi | 100Mi | Burstable |
10 |
| C | - | 150Mi | 150Mi | BestEffort |
100 |
| D | - | 50Mi | 50Mi | BestEffort |
10 |
| E | 100Mi | 100Mi | - | Guaranteed |
50 |
比较从一个 pod 是否使用了超过请求的内存开始,对于 B、C 和 D 是这种情况。需要注意的是,虽然 B 是 Burstable 类别,但由于它过度消耗匮乏的资源,它仍然会被列入第一个驱逐组。接下来需要考虑的是优先级,所以 B 和 D 会被选择。最后,由于 B 在前述请求中的内存使用量大于 D,它将是第一个被杀死的 pod。
大多数情况下,使用其请求范围内资源的 pods(如 A 和 E)不会被驱逐。但是,如果节点的非 Kubernetes 组件耗尽了内存并且必须转移到其他节点,它们将根据优先级类别进行排名。五个 pod 的最终驱逐顺序将是 D、B、C、E,然后是 A。
如果 kubelet 无法在节点的 OOM (内存不足) 杀手作用之前及时释放节点内存,QoS 类别仍然可以通过预先分配的 OOM 分数(oom_score_adj)来保留排名,如我们在本章前面提到的 pods。OOM 分数与进程相关,并且对 Linux OOM 杀手可见。分数越高,进程越有可能被优先杀死。Kubernetes 为 Guaranteed pods 分配分数 -998,为 BestEffort pods 分配 1000 分。Burstable pods 的分数在 2 到 999 之间,根据它们的内存请求而定;请求的内存越多,得到的分数就越低。
污点和容忍设置
节点可以通过污点拒绝 pods,除非 pods 容忍节点上的所有污点。污点应用于节点,而容忍设置则是特定于 pods 的。污点是一个三元组,形式为 key=value:effect,其中 effect 可以是 PreferNoSchedule、NoSchedule 或 NoExecute。
假设我们有一个节点,里面有一些正在运行的 pods,这些运行中的 pod 没有容忍污点 k_1=v_1,不同的效果会导致以下情况:
-
NoSchedule:不会将未容忍k_1=v_1的新 pod 放置到节点上。 -
PreferNoSchedule:调度器会尽量不将新的未容忍k_1=v_1的 pod 放置到该节点上。 -
NoExecute:运行中的 pod 会立即被驱逐,或者在 pod 的tolerationSeconds所指定的时间段后被驱逐。
我们来看一个例子。这里我们有三个 节点:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-mycluster-default-pool-1e3873a1-jwvd Ready <none> 2m v1.11.2-gke.18
gke-mycluster-default-pool-a1eb51da-fbtj Ready <none> 2m v1.11.2-gke.18
gke-mycluster-default-pool-ec103ce1-t0l7 Ready <none> 2m v1.11.2-gke.18
运行一个 nginx pod:
$ kubectl run --generator=run-pod/v1 --image=nginx:1.15 ngx
pod/ngx created
$ kubectl describe pods ngx
Name: ngx
Node: gke-mycluster-default-pool-1e3873a1-jwvd/10.132.0.4
...
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
根据 pod 的描述,我们可以看到它已被放置在 gke-mycluster-default-pool-1e3873a1-jwvd 节点上,并且它具有两个默认的容忍设置。字面意思是,如果节点变得不可用或无法访问,我们必须等待 300 秒,然后 pod 才会被从节点中驱逐。这两个容忍设置是由 DefaultTolerationSeconds 许可控制插件应用的。现在,我们在节点上添加一个 NoExecute 污点:
$ kubectl taint nodes gke-mycluster-default-pool-1e3873a1-jwvd \
experimental=true:NoExecute
node/gke-mycluster-default-pool-1e3873a1-jwvd tainted
由于我们的 pod 不容忍 experimental=true 并且效果是 NoExecute,该 pod 会立即从节点中被驱逐,如果由控制器管理,则会在其他地方重新启动。一个节点也可以应用多个污点。pods 必须匹配所有容忍设置才能在该节点上运行。以下是一个可以通过污点节点的示例:
$ cat chapter8/8-3_management/pod_tolerations.yml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-tolerations
spec:
containers:
- name: web
image: nginx
tolerations:
- key: "experimental"
value: "true"
operator: "Equal"
effect: "NoExecute"
$ kubectl apply -f chapter8/8-3_management/pod_tolerations.yml
pod/pod-with-tolerations created
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
pod-with-tolerations 1/1 Running 0 7s 10.32.1.4 gke-mycluster-default-pool-1e3873a1-jwvd
如我们所见,新 pod 现在可以在被污点化的节点 gke-mycluster-default-pool-1e3873a1-jwvd 上运行。
除了 Equal 操作符外,我们还可以使用 Exists。在这种情况下,我们无需指定值字段。只要节点被指定的键污点化,并且所需的效果匹配,pod 就可以在该污点节点上运行。
根据节点的运行状态,某些污点可能会由节点控制器、kubelet、云服务提供商或集群管理员添加,以便将 pods 从节点上迁移。这些污点如下:
-
node.kubernetes.io/not-ready -
node.kubernetes.io/unreachable -
node.kubernetes.io/out-of-disk -
node.kubernetes.io/memory-pressure -
node.kubernetes.io/disk-pressure -
node.kubernetes.io/network-unavailable -
node.cloudprovider.kubernetes.io/uninitialized -
node.kubernetes.io/unschedulable
如果有任何关键的 pod 即使在这些情况下也需要运行,我们应该显式地容忍相应的污点。例如,由 DaemonSet 管理的 pods 会容忍以下的污点,在 NoSchedule 中:
-
node.kubernetes.io/memory-pressure -
node.kubernetes.io/disk-pressure -
node.kubernetes.io/out-of-disk -
node.kubernetes.io/unschedulable -
node.kubernetes.io/network-unavailable
对于节点管理,我们可以使用 kubectl cordon <node_name> 命令将节点标记为不可调度 (node.kubernetes.io/unschedulable:NoSchedule),并使用 kubectl uncordon <node_name> 命令撤销该操作。另一个命令 kubectl drain 会驱逐节点上的 pods,并将节点标记为不可调度。
总结
在本章中,我们探讨了 Kubernetes 如何管理集群资源并调度工作负载等主题。通过了解服务质量、优先级和节点资源不足处理等概念,我们可以优化资源利用率,同时保持工作负载的稳定性。同时,ResourceQuota 和 LimitRange 在多租户共享资源环境中为运行的工作负载提供了额外的保护层。通过这些保护措施,我们可以放心地依赖 Kubernetes 使用自动扩展器来扩展工作负载,并将资源利用率提升到最大化。
在第九章,持续交付,我们将继续进行并设置一个管道,在 Kubernetes 中持续交付我们的产品。
第九章:持续交付
在本书的开头,我们通过容器化我们的应用程序,使用 Kubernetes 进行编排,持久化数据,并将服务暴露给外界。后来,我们通过设置监控和日志记录来增强对服务的信心,并使它们能够完全自动地进行扩展和缩减。现在,我们希望通过在 Kubernetes 中持续交付最新的功能和改进来推动我们的服务。我们将在本章学习以下内容:
-
更新 Kubernetes 资源
-
设置交付管道
-
如何改进部署过程
更新资源
持续交付(CD),正如我们在第一章《DevOps 简介》中所描述的,是一组操作流程,包括持续集成(CI)和随后的部署任务。CI 流程由版本控制系统、构建、不同级别的验证等元素组成,旨在消除将每个更改集成到主发布线中的工作量。实现这些功能的工具通常位于应用层,可能独立于底层基础设施。尽管如此,谈到部署部分时,理解和处理基础设施仍然是不可避免的。部署任务与我们的应用程序运行平台紧密耦合,无论我们实现的是持续交付还是持续部署。例如,在软件运行在裸机或虚拟机的环境中时,我们会利用配置管理工具、调度器和脚本来部署软件。然而,如果我们将服务运行在像 Heroku 这样的应用平台,甚至是在无服务器模式下,设计部署管道将是完全不同的故事。总的来说,部署任务的目标是确保我们的软件在正确的地方正常运行。在 Kubernetes 中,这意味着知道如何正确更新资源,特别是 Pods。
触发更新
在第三章《Kubernetes 入门》中,我们讨论了部署中 Pods 的滚动更新机制。让我们回顾一下更新过程触发后的发生情况:
-
部署会根据更新后的清单创建一个新的
ReplicaSet,其初始状态为0个 Pods -
新的
ReplicaSet会逐渐扩展,而之前的ReplicaSet会逐渐缩小 -
该过程在所有旧的 Pods 被替换后结束
这一机制由 Kubernetes 自动实现,这意味着我们不需要监督更新过程。触发它所需做的只是通知 Kubernetes 部署的 pod 规格已更新;也就是说,我们修改 Kubernetes 资源的清单。例如,假设我们有一个名为 my-app 的部署(可以参见本节示例目录下的 ex-deployment.yml),我们可以通过 kubectl 的子命令来修改清单,如下所示:
kubectl patch:这会根据输入的 JSON 参数部分地更新对象的清单。如果我们想将my-app的镜像从alpine:3.7更新到alpine:3.8,可以按如下方式操作:
$ kubectl patch deployment my-app -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"alpine:3.8"}]}}}}'
kubectl set:此命令修改对象的某些属性。这是直接更改某些属性的快捷方式。deployment的镜像就是它支持的属性之一:
$ kubectl set image deployment my-app app=alpine:3.8
-
kubectl edit:此命令打开编辑器并显示当前的清单,以便我们可以交互式地进行编辑。修改后的清单将在保存后立即生效。要更改此命令的默认编辑器,可以使用EDITOR环境变量。例如,EDITOR="code --wait" kubectl edit deployments my-app会打开 Visual Studio Code。 -
kubectl replace:此命令用另一个提交的模板文件替换一个清单。如果资源尚未创建,或者包含无法更改的属性,则会报错。例如,在我们的示例模板ex-deployment.yml中有两个资源,分别是部署my-app和其Servicemy-app-svc。我们可以用新的规范文件来替换它们:
$ kubectl replace -f ex-deployment.yml
deployment.apps/my-app replaced
The Service "my-app-svc" is invalid: spec.clusterIP: Invalid value: "": field is immutable
$ echo $?
1
替换后,我们看到错误代码是 1,符合预期,因此我们正在更新的是 deployment 而不是 Service。这种行为在编写 CI/CD 流程的自动化脚本时尤为重要。
kubectl apply:此命令会应用清单文件。换句话说,如果资源已存在于 Kubernetes 中,它将被更新;否则,将会创建资源。当kubectl apply用来创建资源时,它在功能上大致等同于kubectl create --save-config。应用的规格文件会被保存到注释字段kubectl.kubernetes.io/last-applied-configuration中,我们可以通过子命令edit-last-applied、set-last-applied和view-last-applied对其进行操作。例如,我们可以通过以下命令查看之前提交的模板:
$ kubectl apply -f ex-deployment.yml view-last-applied
保存的清单信息将与我们发送的完全相同,区别在于通过 kubectl get <resource> -o <yaml or json> 获取的信息包含对象的实时状态,而不仅仅是规格。
虽然在本节中我们仅关注操作部署,但这些命令同样适用于更新其他所有 Kubernetes 资源,如 service 和 role。
根据 etcd 的汇聚速度,ConfigMap 和 secret 的更改通常需要几秒钟才能传播到 pods。
与 Kubernetes API 服务器交互的推荐方式是使用kubectl。如果你处于受限环境中,或者想实现自己的操作控制器,也可以使用 Kubernetes 的 RESTful API 来操作资源。例如,我们之前使用的kubectl patch命令如下所示:
$ curl -X PATCH -H 'Content-Type: application/strategic-merge-patch+json' --data '{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"alpine:3.8"}]}}}}' 'https://$KUBEAPI/apis/apps/v1/namespaces/default/deployments/my-app'
这里,$KUBEAPI变量是 API 服务器的端点。更多信息请参见 API 参考资料:kubernetes.io/docs/reference/kubernetes-api/。
管理滚动更新
一旦触发部署过程,Kubernetes 会在后台默默完成所有任务。让我们进行一些动手实验。再次提醒,即使我们用之前提到的命令修改了某些内容,滚动更新过程也不会被触发,除非相关 Pod 的规格发生变化。我们准备的示例是一个简单的脚本,它会用其主机名和运行的 Alpine 版本响应任何请求。首先,我们创建deployment并在另一个终端中检查其响应:
$ kubectl apply -f ex-deployment.yml
deployment.apps/my-app created
service/my-app-svc created
$ kubectl proxy &
[1] 48334
Starting to serve on 127.0.0.1:8001
## switch to another terminal, #2
$ while :; do curl http://localhost:8001/api/v1/namespaces/default/services/my-app-svc:80/proxy/; sleep 1; done
my-app-5fbdb69f94-5s44q-v-3.7.1 is running...
my-app-5fbdb69f94-g7k7t-v-3.7.1 is running...
...
现在,我们将其镜像更改为另一个版本,并查看响应:
## go back to terminal#1
$ kubectl set image deployment.apps my-app app=alpine:3.8
deployment.apps/my-app image updated
## switch to terminal#2
...
my-app-5fbdb69f94-7fz6p-v-3.7.1 is running...
my-app-6965c8f887-mbld5-v-3.8.1 is running......
版本 3.7 和 3.8 的消息交替显示,直到更新过程结束。为了立即确定 Kubernetes 中的更新进程状态,而不是轮询服务端点,我们可以使用kubectl rollout来管理滚动更新过程,包括检查正在进行的更新进度。让我们使用status子命令查看当前的rollout状态:
## if the previous rollout has finished,
## you can make some changes to my-app again:
$ kubectl rollout status deployment my-app
Waiting for deployment "my-app" rollout to finish: 3 out of 5 new replicas have been updated...
...
Waiting for deployment "my-app" rollout to finish: 3 out of 5 new replicas have been updated...
Waiting for deployment "my-app" rollout to finish: 3 of 5 updated replicas are available...
Waiting for deployment "my-app" rollout to finish: 3 of 5 updated replicas are available...
Waiting for deployment "my-app" rollout to finish: 3 of 5 updated replicas are available...
deployment "my-app" successfully rolled out
此时,terminal#2的输出应该来自版本 3.6。history子命令允许我们查看之前对deployment所做的更改:
$ kubectl rollout history deployment.app my-app
deployment.apps/my-app
REVISION CHANGE-CAUSE
1 <none>
2 <none>
然而,CHANGE-CAUSE字段并没有显示出任何有助于我们查看修订详情的有用信息。为了利用滚动更新历史功能,在每个导致更改的命令后添加--record标志,如apply或patch。kubectl create也支持record标志。
让我们对deployment做一些更改,例如修改my-app中 Pod 的DEMO环境变量。由于这会导致 Pod 规格发生变化,rollout将立即启动。这种行为使我们能够在不构建新镜像的情况下触发更新。为了简化,我们使用patch来修改变量:
$ kubectl patch deployment.apps my-app -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"DEMO","value":"1"}]}]}}}}' --record
deployment.apps/my-app patched
$ kubectl rollout history deployment.apps my-app
deployment.apps/my-app
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 kubectl patch deployment.apps my-app --patch={"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"DEMO","value":"1"}]}]}}}} --record=true
REVISION 3的CHANGE-CAUSE清楚地记录了提交的命令。只有命令会被记录,这意味着任何在edit/apply/replace中的修改不会被明确标记。如果我们想获取之前版本的清单,可以检索已保存的配置,只要我们的更改是通过apply完成的。
CHANGE-CAUSE字段实际上存储在对象的kubernetes.io/change-cause注释中。
出于各种原因,我们有时希望即使rollout已经成功到一定程度,也能回滚我们的应用。这可以通过undo子命令实现:
$ kubectl rollout undo deployment my-app
整个过程基本上与更新相同——也就是应用之前的清单——并执行滚动更新。我们还可以使用--to-revision=<REVISION#>标志回滚到特定版本,但只有保留的修订版本才能回滚。Kubernetes 根据deployment对象中的revisionHistoryLimit参数来确定保留多少个修订版本。
更新的进度由kubectl rollout pause和kubectl rollout resume控制。顾名思义,它们应该成对使用。暂停一个部署不仅仅是停止正在进行的rollout,还包括冻结任何更新触发,即使规格被修改,除非被恢复。
更新 DaemonSet 和 StatefulSet
Kubernetes 支持多种方式来编排不同类型工作负载的 pods。除了部署外,我们还可以使用DaemonSet和StatefulSet来处理长期运行和非批量工作负载。由于这些 pods 相对于部署的 pods 有更多的约束,因此我们需要注意一些事项,以便正确处理它们的更新。
DaemonSet
DaemonSet是为系统守护进程设计的控制器,正如其名称所示。因此,DaemonSet控制器在每个节点上启动并维护一个 pod;DaemonSet控制器启动的 pod 总数等于集群中的节点数。由于这个限制,更新DaemonSet不像更新部署那样直接。例如,deployment有一个maxSurge参数(.spec.strategy.rollingUpdate.maxSurge),用于控制更新过程中可以创建的超过期望数量的冗余 pod 数量,但我们不能对DaemonSet管理的 pods 使用相同的策略。因为守护进程 pod 通常涉及一些特殊问题,可能会占用主机资源,如端口,如果同一节点上有两个或多个系统 pod 同时存在,可能会导致错误。因此,更新的方式是先在主机上终止旧 pod,再创建新 pod。
Kubernetes 为DaemonSet实现了两种更新策略:
-
OnDelete:Pods 只有在手动删除后才会更新。 -
RollingUpdate:这与OnDelete类似,但 pod 的删除由 Kubernetes 自动执行。有一个可选参数.spec.updateStrategy.rollingUpdate.maxUnavailable,类似于deployment中的参数。默认值为1,意味着 Kubernetes 每次替换一个 pod,逐节点进行。
你可以在github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/blob/master/chapter9/9-1_updates/ex-daemonset.yml找到一个示例,展示了如何编写DaemonSet模板。更新策略设置在.spec.updateStrategy.type路径下,其默认值是RollingUpdate。触发滚动更新的方式与触发部署的方式相同。我们还可以使用kubectl rollout来管理DaemonSet控制器的滚动更新,但pause和resume不受支持。
StatefulSet
StatefulSet和DaemonSet的更新过程基本相同;它们在更新过程中不会创建冗余的 Pod,且更新策略的行为也类似。你可以在9-1_updates/ex-statefulset.yml文件中找到一个用于练习的模板。更新策略的选项设置在.spec.updateStrategy.type路径下:
-
OnDelete:Pod 只有在被手动删除后才会更新。 -
RollingUpdate:像其他控制器的滚动更新一样,Kubernetes 以受控的方式删除和创建 Pod。Kubernetes 知道StatefulSet中的顺序很重要,因此它会逆序替换 Pod。假设我们有三个 Pod:my-ss-0、my-ss-1和my-ss-2,更新顺序将从my-ss-2开始,直到my-ss-0。删除过程不会遵循StatefulSet的 Pod 管理策略;即使我们将 Pod 管理策略设置为Parallel,更新仍然会按顺序一个一个地执行。
RollingUpdate类型的唯一参数是partition(.spec.updateStrategy.rollingUpdate.partition)。如果指定了该参数,则任何顺序编号小于分区号的 Pod 将保持当前版本,并不会更新。例如,如果我们在具有三个 Pod 的StatefulSet中将partition设置为1,那么在滚动更新后,只有 pod-1 和 pod-2 会被更新。这个参数允许我们在一定程度上控制进度,尤其适用于等待数据同步、执行金丝雀测试或进行分阶段更新等场景。
构建交付管道
为容器化应用实现 CD 管道是非常简单的。让我们回顾一下到目前为止学到的关于 Docker 和 Kubernetes 的实践,并将这些实践组织成 CD 管道。假设我们已经完成了代码、Dockerfile 和相应的 Kubernetes 模板。要将这些内容部署到集群中,我们需要按照以下步骤操作:
-
docker build:生成一个可执行且不可变的构件 -
docker run:通过简单的测试验证构建是否有效 -
docker tag:如果构建正常,将其标记为具有有意义的版本 -
docker push:将构建移动到artifacts仓库以供分发 -
kubectl apply:将构建部署到目标环境 -
kubectl rollout status:跟踪部署任务的进度
这就是我们所需的一个简单而可行的交付流水线。
在这里,我们使用“持续交付”而不是“持续部署”这个术语,因为之前描述的步骤之间仍然存在一些差距,可以实现为人工控制或完全自动化的部署。这一考虑可能因团队而异。
选择工具
我们要实现的步骤非常简单。然而,当把它们作为流水线串联起来时,并没有一个适用于所有场景的通用流水线。它可能因组织形式、团队运行的开发工作流或流水线与现有基础设施中其他系统的交互等因素而有所不同。因此,设定目标并选择工具是我们必须考虑的首要事项。
一般来说,为了让流水线持续发布构建,我们至少需要三类工具:版本控制系统、构建服务器和用于存储容器工件的代码库。在本节中,我们将基于之前章节中介绍的 SaaS 工具设置一个参考 CD 流水线:
-
GitHub (
github.com) -
Travis CI (
travis-ci.com) -
Docker Hub (
hub.docker.com)
所有这些工具对于开源项目都是免费的。当然,每个工具都有很多替代方案,例如,GitLab 用于版本控制系统(VCS),Jenkins 托管用于 CI,甚至专门的部署工具如 Spinnaker (www.spinnaker.io/)。除了这些大型构建模块外,我们还可以借助 Helm (github.com/kubernetes/helm)等工具来帮助我们组织模板及其实例化的发布。总而言之,选择最适合您需求的工具由您自己决定。我们将重点讨论这些基础组件如何与 Kubernetes 中的部署进行交互。
交付流水线的端到端演练
以下图表展示了我们基于前述三个服务的 CD 流程:

代码集成的工作流程如下:
-
代码被提交到 GitHub 上的一个代码库。
-
提交触发 Travis CI 上的构建任务:
-
一个 Docker 镜像将被构建。
-
为确保构建质量稳固且准备好进行集成,通常在 CI 服务器上执行不同层次的测试。此外,由于使用 Docker Compose 或 Kubernetes 运行应用程序堆栈比以往任何时候都更加简便,因此在构建任务中运行涉及多个组件的测试也是可行的。
-
-
经验证的镜像会被标记上标识符,并推送到 Docker Hub。
至于 Kubernetes 中的部署,这可以像在模板中更新镜像路径,然后将模板应用于生产集群那样简单,也可以像一系列操作,包括流量分配和金丝雀发布那样复杂。在我们的例子中,发布过程从手动发布一个新的 Git SemVer 标签开始,CI 脚本重复与集成部分相同的流程,直到镜像推送步骤。由于 CI 服务器有时无法触及生产环境,我们在集群内放置了一个代理,来观察并应用配置分支中的更改。
专用的配置仓库是一种常见的模式,用于将应用程序及其基础设施分开。有许多基础设施即代码(IaC)工具帮助我们以可记录在版本控制系统中的方式表达基础设施及其状态。此外,通过在版本控制系统中追踪所有内容,我们可以将对基础设施所做的每个更改转化为 Git 操作。为了简化起见,我们在同一个仓库中使用另一个分支来进行配置更改。
一旦代理检测到更改,它会拉取新的模板并相应地更新对应的控制器。最后,在 Kubernetes 滚动更新过程结束后,交付工作就完成了。
解释步骤
我们的例子 okeydokey 是一个始终对每个请求回显 OK 的 Web 服务,代码和部署文件已提交到 GitHub 上的仓库:github.com/DevOps-with-Kubernetes/okeydokey。
在配置 Travis CI 构建之前,让我们先在 Docker Hub 上创建一个镜像仓库以供后续使用。在登录到 Docker Hub 后,点击右上角的“Create Repository”按钮,然后按照屏幕上的步骤创建一个仓库。okeydokey 的镜像仓库地址是 devopswithkubernetes/okeydokey(hub.docker.com/r/devopswithkubernetes/okeydokey/)。
将 Travis CI 与 GitHub 仓库连接是非常简单的;我们需要做的就是授权 Travis CI 访问我们的 GitHub 仓库,并在设置页面启用其构建该仓库的功能(travis-ci.com/account/repositories)。另一个我们需要的东西是一个 GitHub 访问令牌或具有写权限的部署密钥。这个密钥将被放置在 Travis CI 上,以便 CI 脚本能够将构建好的镜像更新到配置分支。请参考 GitHub 官方文档(developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys)获取部署密钥。
Travis CI 中作业的定义是在一个文件 .travis.yml 中配置的,该文件位于同一个仓库下。这个定义是一个 YAML 格式的模板,包含了一些 Shell 脚本块,指示 Travis CI 在构建过程中应该执行什么操作。
完整的 Travis CI 文档可以在这里找到:docs.travis-ci.com/user/tutorial/。
你可以在以下网址找到我们 .travis.yml 文件块的解释:github.com/DevOps-with-Kubernetes/okeydokey/blob/master/.travis.yml。
env
本节定义了在整个构建过程中可见的环境变量:
DOCKER_REPO=devopswithkubernetes/okeydokey
BUILD_IMAGE_PATH=${DOCKER_REPO}:build-${TRAVIS_COMMIT}
RELEASE_IMAGE_PATH=${DOCKER_REPO}:${TRAVIS_TAG}
在这里,我们设置了一些可能会更改的变量,例如构建的镜像将要存储的 Docker 注册表路径。此外,还有从 Travis CI 传递过来的构建元数据,以环境变量的形式存在,具体文档可参考:docs.travis-ci.com/user/environment-variables/#default-environment-variables。例如,TRAVIS_COMMIT 代表当前提交的哈希值,我们使用它作为标识符来区分不同构建的镜像。
另一个环境变量的来源是手动在 Travis CI 上配置的。由于这些变量在配置后会被隐藏,因此我们把一些敏感数据,如 Docker Hub 和 GitHub 仓库的凭证,存储在这里:

每个 CI 工具都有其处理机密信息的最佳实践。例如,一些 CI 工具还允许我们将变量保存在 CI 服务器中,但这些变量仍然会打印在构建日志中,因此在这种情况下我们不太可能将机密信息存储在这里。
像 Vault(www.vaultproject.io/)这样的密钥管理系统,或云服务商提供的类似服务,如 GCP KMS(cloud.google.com/kms/)、AWS KMS(aws.amazon.com/kms/)和 Azure Key Vault(azure.microsoft.com/en-us/services/key-vault/)都推荐用于存储敏感凭证。
script
本节是我们运行构建和测试的地方:
docker build -t my-app .
docker run --rm --name app -dp 5000:5000 my-app
sleep 10
CODE=$(curl -IXGET -so /dev/null -w "%{http_code}" localhost:5000)
'[ ${CODE} -eq 200 ] && echo "Image is OK"'
docker stop app
由于我们使用的是 Docker,构建过程只需要一行脚本。我们的测试非常简单,主要是启动一个容器并使用构建的镜像,发出一些请求来确定其完整性。在这一阶段,我们可以完成所有操作,包括添加单元测试或运行自动化集成测试,以改进生成的构建成果。
after_success
这个块仅在前一个阶段无错误结束时执行。一旦执行了这个块,我们就准备发布我们的镜像了:
docker login -u ${CI_ENV_REGISTRY_USER} -p "${CI_ENV_REGISTRY_PASS}"
if [[ ${TRAVIS_TAG} =~ ^v.*$ ]]; then
docker tag my-app ${RELEASE_IMAGE_PATH}
docker push ${RELEASE_IMAGE_PATH}
else
docker tag my-app ${BUILD_IMAGE_PATH}
docker push ${BUILD_IMAGE_PATH}
fi
我们的镜像标签使用提交的哈希值用于普通构建,并使用手动标记的版本用于发布。没有绝对的镜像标记规则,但强烈不建议将latest作为业务服务的标签,因为这可能导致版本混乱,例如运行两个名称相同但不同的镜像。最后的条件块用于在特定分支标签上发布镜像,我们希望将构建和发布保持在不同的轨道上。记得在推送镜像之前进行 Docker Hub 身份验证。
Kubernetes 通过imagePullPolicy决定是否拉取镜像,默认值为IfNotPresent:
IfNotPresent:kubelet 在节点上没有镜像时会拉取镜像。如果镜像标签是:latest且策略不是Never,那么 kubelet 将回退到Always。
Always:kubelet 始终拉取镜像。
Never:kubelet 永远不会拉取镜像;它会检查目标镜像是否已经存在于节点上。
因为我们将项目部署设置为仅在发布时部署到实际机器上,所以构建可能会在此时停止并被返回。我们来看一下该构建的日志:travis-ci.com/DevOps-with-Kubernetes/okeydokey/builds/93296022。日志保留了执行的脚本和在 CI 构建过程中每一行脚本的输出:

正如我们所见,我们的构建已经成功,所以镜像被发布在这里:hub.docker.com/r/devopswithkubernetes/okeydokey/tags/。该构建使用的是build-842eb66b2fa612598add8e19769af5c56b922532标签,我们现在可以在 CI 服务器之外运行它:
$ docker run --name test -dp 5000:5000 devopswithkubernetes/okeydokey:build-842eb66b2fa612598add8e19769af5c56b922532
3d93d6505e369286c3f072ef4f04e15db2638f280c4615be95bff47379a70388
$ curl localhost:5000
OK
部署
尽管我们可以实现从端到端的完全自动化流水线,但由于业务原因,我们经常遇到阻碍新构建部署的情况。因此,我们告诉 Travis CI 仅在我们想要发布新版本时才运行部署脚本。
正如我们之前所述,本例中在 Travis CI 上的部署仅仅是将构建好的镜像写回模板以便进行部署。在这里,我们使用脚本提供程序使 Travis CI 运行我们的部署脚本(deployment/update-config.sh),该脚本执行以下操作:
-
定位配置仓库及其相应分支
-
更新了镜像标签
-
提交更新后的模板
在更新后的镜像标签提交到仓库后,Travis CI 上的任务完成。
流水线的另一端是我们集群内部的代理。它负责以下任务:
-
定期监控我们在 GitHub 上配置的变化
-
拉取并应用更新后的镜像到我们的 pod 控制器
前者非常简单,但对于后者,我们必须授予代理足够的权限,以便它能够操作集群内的资源。我们的示例使用一个服务账户cd-agent,位于一个专用的命名空间cd下,用于创建和更新我们的部署,相关的 RBAC 配置可以在本书的chapter9/9-2_service-account-for-ci-tool/cd-agent中找到。
在这里,我们授予服务账户跨命名空间读取和修改资源的权限,包括整个集群的秘密。由于安全问题,通常建议将服务账户的权限限制在实际使用的资源上,否则可能成为潜在的漏洞。
该代理本身仅是一个长期运行的脚本,位于chapter9/9-2_service-account-for-ci-tool/utils/watcher/watcher.sh。为了执行更新,它使用apply和rollout:
...
apply_to_kube() {
kubectl apply -f <template_path> -n <namespace>
kubectl rollout status -f <template_path> -n <namespace> --timeout 5m
}
...
在部署应用之前,让我们先部署agent及其相关的config:
$ kubectl apply -f chapter9/9-2_service-account-for-ci-tool/cd-agent
clusterrole.rbac.authorization.k8s.io/cd-role created
clusterrolebinding.rbac.authorization.k8s.io/cd-agent created
namespace/cd created
serviceaccount/cd-agent created
deployment.apps/state-watcher created
state-watcher部署就是我们的agent,它已配置为监控我们的配置仓库中的环境变量:
$ cat chapter9/9-2_service-account-for-ci-tool/cd-agent/watcher-okeydokey.yml
...
env:
- name: WORK_PATH
value: /repo
- name: TEMPLATE_PATH
value: /repo/deployment
- name: REMOTE_GIT_REPO
value: https://github.com/DevOps-with-Kubernetes/okeydokey.git
- name: WATCH_BRANCH
value: config
- name: RELEASE_TARGET_NAMESPACE
value: default
- name: RELEASE_TARGET_CONTROLLER_TEMPLATE
value: deployment.yml
...
一切准备就绪。让我们看看整个流程是如何运行的。
我们在 GitHub 上发布了一个带有v0.0.3标签的版本(github.com/DevOps-with-Kubernetes/okeydokey/releases/tag/v0.0.3):

Travis CI 在新标签触发后开始构建我们的作业:

如果失败了,我们可以查看构建日志,看看哪里出了问题:travis-ci.com/DevOps-with-Kubernetes/okeydokey/jobs/162862675。幸运的是,我们获得了绿色标志,因此构建的镜像将在稍后推送到 Docker Hub:

此时,我们的agent也应该注意到config的变化并做出相应的处理:
$ kubectl logs -f -n cd state-watcher-69bfdd578-8nn9s -f
...
From https://github.com/DevOps-with-Kubernetes/okeydokey
* branch config -> FETCH_HEAD
* [new branch] config -> origin/config
Tue Dec 4 23:44:56 UTC 2018: No update detected.
deployment.apps/okeydokey created
service/okeydokey-svc created
Waiting for deployment "okeydokey" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "okeydokey" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "okeydokey" rollout to finish: 1 of 2 updated replicas are available...
deployment "okeydokey" successfully rolled out
...
如我们所见,我们的应用已成功部署,应该开始向所有人显示OK:
$ kubectl proxy &
$ curl localhost:8001/api/v1/namespaces/default/services/okeydokey-svc:80/proxy/
OK
我们在本节中构建并演示的流水线是一个经典的 Kubernetes 持续交付流程。然而,随着团队工作方式和文化的不同,为你的团队量身定制一个持续交付流水线可以提高效率。例如,部署的内置更新策略是滚动更新。那些更倾向于使用其他类型部署策略(如蓝绿部署或金丝雀发布)的团队需要调整流水线以满足他们的需求。幸运的是,Kubernetes 非常灵活,我们可以通过组合 Deployment、Service、Ingress 等来实现各种策略。
在本书的上一版中,我们演示了一个类似的流程,但直接从 CI 服务器应用配置。这两种方法各有优缺点。如果你对将集群信息放在 CI 服务器上没有安全顾虑,并且只需要一个非常简单的持续交付流程,那么基于推送的流水线仍然是一个选择。你可以在这里找到导出服务账户令牌的脚本和应用 Kubernetes 配置的另一个脚本:github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter9/9-2_service-account-for-ci-tool/utils/push-cd。
深入理解 Pod
尽管 Pod 的生命周期中的出生与死亡不过是瞬息之间,但它们也是服务最脆弱的点。我们希望避免常见的情况,比如将请求路由到一个尚未就绪的 Pod,或是强制切断与终止中的机器的所有连接。因此,即使 Kubernetes 为我们处理了大多数事务,我们仍然应该知道如何正确配置服务,确保每个功能都能完美交付。
启动 Pod
默认情况下,Kubernetes 在 Pod 启动后立即将其状态设置为 Running。如果 Pod 后面有服务,端点控制器会立即向 Kubernetes 注册一个端点。之后,kube-proxy 会观察端点的变化,并相应地配置主机的 ipvs 或 iptables。来自外部世界的请求现在会发送到 Pod。这些操作发生得非常迅速,因此很有可能请求在应用程序准备就绪之前就到达了 Pod,特别是对于大型软件。如果 Pod 在运行时失败,我们应立即将其从服务的池中移除,以确保没有请求到达坏的端点。
部署和其他控制器的 minReadySeconds 字段不会延迟 Pod 变为就绪,而是延迟 Pod 变为可用。只有当所有 Pod 都可用时,部署才算成功。
存活性和就绪性探针
探测是容器健康状况的指示器。它通过定期执行诊断操作来判断容器健康状态,操作由 kubelet 发起。判断容器状态有两种探测方法:
-
存活 探测:这表示容器是否存活。如果容器在此探测失败,kubelet 会杀死它,并根据 Pod 的
restartPolicy决定是否重启它。 -
就绪探测:这表示容器是否已准备好接收流量。如果服务后面的 Pod 没有准备好,其端点不会被创建,直到 Pod 准备好。
restartPolicy告诉我们 Kubernetes 如何处理 Pod 的失败或终止情况。它有三种模式:Always、OnFailure 或 Never。默认设置为 Always。
可以配置三种操作处理程序来诊断容器:
-
exec:此命令会在容器内执行定义的命令。如果退出码为0,则视为执行成功。 -
tcpSocket:此操作通过 TCP 测试指定端口,如果端口打开,则操作成功。 -
httpGet:这会对目标容器的 IP 地址执行HTTP GET请求。请求中的头部可以自定义。如果状态码满足400 > CODE >= 200,则此探测被认为是健康的。
此外,还有五个参数定义了探测的行为:
-
initialDelaySeconds:Kubelet 在首次探测前需要等待的时间。 -
successThreshold:只有当容器连续多次探测成功超过此阈值时,才认为容器健康。 -
failureThreshold:与之前相同,但定义了失败的条件。 -
timeoutSeconds:单次探测操作的时间限制。 -
periodSeconds:探测操作之间的时间间隔。
以下代码片段演示了如何使用就绪探测。完整模板可以在github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/blob/master/chapter9/9-3_on_pods/probe.yml找到:
...
containers:
- name: main
image: devopswithkubernetes/okeydokey:v0.0.4
readinessProbe:
httpGet:
path: /
port: 5000
periodSeconds: 5
initialDelaySeconds: 10
successThreshold: 2
failureThreshold: 3
timeoutSeconds: 1
command:
...
在此示例中,我们在主应用程序中做了一些处理,设置应用程序的启动时间约为六秒,并在 20 秒后用另一个返回HTTP 500的应用程序替换它。应用程序与就绪探测的交互如下图所示:

上面的时间线表示 Pod 的实际就绪状态,另一个则是 Kubernetes 视角下的就绪状态。第一次探测在 Pod 创建后 10 秒执行,Pod 在连续两次探测成功后被视为就绪。几秒钟后,由于应用程序的终止,Pod 变为不可用,接下来的三次探测失败后,Pod 变为不可就绪。尝试部署前述示例并观察输出:
$ kubectl logs -f my-app-6759578c94-kkc5k
1544137145.530593922 - [sys] pod is created.
1544137151.658855438 - [app] starting server.
1544137155.164726019 - [app] GET / HTTP/1.1
1544137160.165020704 - [app] GET / HTTP/1.1
1544137161.129309654 - [app] GET /from-tester
1544137165.141985178 - [app] GET /from-tester
1544137165.165597677 - [app] GET / HTTP/1.1
1544137168.533407211 - [app] stopping server.
1544137170.169371453 - [500] readiness test fail#1
1544137175.180640604 - [500] readiness test fail#2
1544137180.171766986 - [500] readiness test fail#3
...
在我们的示例文件中,还有另一个 pod,tester,它不断向我们的服务发起请求,并记录日志。/from-tester代表来自测试者的请求。从测试者的活动日志中,我们可以看到,当我们的服务变得不可用时,来自tester的流量停止了(注意时间点1544137180前后两个 pod 的活动):
$ kubectl logs tester
1544137141.107777059 - timed out
1544137147.116839441 - timed out
1544137154.078540367 - timed out
1544137160.094933434 - OK
1544137165.136757412 - OK
1544137169.155453804 -
1544137173.161426446 - HTTP/1.1 500
1544137177.167556193 - HTTP/1.1 500
1544137181.173484008 - timed out
1544137187.189133495 - timed out
1544137193.198797682 - timed out
...
由于我们没有在服务中配置存活探针,因此不健康的容器不会自动重启,除非我们手动杀死它。通常,我们会同时使用两个探针来自动化修复过程。
自定义就绪门控
就绪探针的测试目标始终是容器,这意味着它不能用来通过外部状态禁用 pod 访问服务。由于服务是通过 pod 的标签来选择 pod 的,我们可以通过操作 pod 标签来在一定程度上控制流量。然而,pod 标签也会被 Kubernetes 内部的其他组件读取,因此构建复杂的标签开关可能会导致意想不到的结果。
pod 就绪门控是一个允许我们根据定义的条件标记 pod 是否准备就绪的功能。定义了 pod 就绪门控后,只有当 pod 的就绪探针通过并且与 pod 关联的所有就绪门控状态为True时,pod 才被视为就绪。我们可以通过以下代码片段来定义就绪门控:
...
spec:
readinessGates:
- conditionType: <value>
containers:
- name: main
...
值必须遵循标签键的格式,例如feature_1或myorg.com/fg-2。
当 pod 启动时,我们定义的条件类型会作为条件填充到 pod 的.status.conditions[]路径下,并且我们必须显式地将条件设置为True,以标记 pod 为就绪。对于 Kubernetes 1.13,编辑条件的唯一方式是使用patch API。我们来看一下在github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/blob/master/chapter9/9-3_on_pods/readiness_gates.yml中的示例:
$ cat chapter9/9-3_on_pods/readiness_gates.yml | grep readinessGates -C 1
spec:
readinessGates:
- conditionType: "MY-GATE-1"
$ kubectl apply -f chapter9/9-3_on_pods/readiness_gates.yml
deployment.apps/my-2nd-app created
service/my-2nd-app-svc created
$ kubectl get pod -o custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME IP
my-2nd-app-78786c6d5d-t4564 172.17.0.2
$ kubectl logs my-2nd-app-78786c6d5d-t4564
1544216932.875020742 - [app] starting server.
$ kubectl describe ep my-2nd-app-svc
Name: my-2nd-app-svc
Namespace: default
Labels: app=my-2nd-app
Annotations: <none>
Subsets:
Addresses: <none>
NotReadyAddresses: 172.17.0.2
...
在这里,我们自定义的条件被称为MY-GATE-1,而应用程序是我们在本章中一直使用的那个。正如我们所见,即使 pod 已启动,其地址仍列在NotReadyAddresses中。这意味着该 pod 没有承接任何流量。我们可以使用describe(或wide/json/yaml)命令来验证其状态:
$ kubectl describe pod my-2nd-app-78786c6d5d-t4564
...
Readiness Gates:
Type Status
MY-GATE-1 <none>
Conditions:
Type Status
Initialized True
Ready False
ContainersReady True
PodScheduled True
...
pod 中的容器已就绪,但由于就绪门控的原因,pod 本身并没有就绪。要开启它,我们需要向 API 服务器发送一个JSON Patch负载,目标路径为/api/v1/namespaces/<namespace>/pods/<pod_name>/status:
$ kubectl proxy &
$ curl http://localhost:8001/api/v1/namespaces/default/pods/my-2nd-app-78786c6d5d-t4564/status \
-XPATCH -H "Content-Type: application/json-patch+json" -d \
'[{"op":"add","path":"/status/conditions/-","value":{"type":"MY-GATE-1", "status": "True"}}]'
...
"status": {
"phase": "Running",
"conditions": [
...
{
"type": "MY-GATE-1",
"status": "True",
"lastProbeTime": null,
"lastTransitionTime": null
}
...
我们将看到一条条目被插入到.status.conditions列表中。现在,如果我们检查服务的端点,就可以看到 pod 已经开始提供请求服务:
$ kubectl describe ep my-2nd-app-svc
Name: my-2nd-app-svc
Namespace: default
Labels: app=my-2nd-app
Annotations: <none>
Subsets:
Addresses: 172.17.0.2
...
## also the status of the gates:
$ kubectl describe pod my-2nd-app-78786c6d5d-t4564 | grep -A2 Readiness
Readiness Gates:
Type Status
MY-GATE-1 True
要将 pod 调转过来,我们可以使用 JSON Patch 的replace或remove操作,将条件状态设置为False或<none>:
## we need the index of our gate, and we use jq to query it here:
$ export RG_NAME="MY-GATE-1"
$ kubectl get pod my-2nd-app-78786c6d5d-t4564 -o json | jq --arg R "$RG_NAME" 'foreach .status.conditions[] as $e (-1; .+1; select($e.type == $R))'
0
$ kubectl proxy &
## fill the queried "0" to the path parameter
$ curl http://localhost:8001/api/v1/namespaces/default/pods/my-2nd-app-78786c6d5d-t4564/status \
-XPATCH -H "Content-Type: application/json-patch+json" -d \
'[{"op":"replace","path":"/status/conditions/0","value":{"type":"MY-GATE-1", "status": "False"}}]'
...
$ kubectl describe ep my-2nd-app-svc | grep -B2 NotReadyAddresses
Subsets:
Addresses: <none>
NotReadyAddresses: 172.17.0.2
现在 pod 再次变为未就绪状态。通过就绪门控,我们可以很好地区分切换业务功能和管理标签的逻辑。
init 容器
有时我们需要在应用程序实际运行之前进行初始化,例如为主应用程序准备架构或从其他地方加载数据。由于很难预测初始化可能需要多长时间,我们不能仅仅依赖 initialDelaySeconds 来为此准备工作创建缓冲区,因此 init 容器在这里非常有用。
init 容器是一个或多个在应用容器之前启动并依次运行到完成的容器。如果任何容器失败,它将根据 pod 的 restartPolicy 重新启动,直到所有容器退出并返回代码 0。定义 init 容器与定义常规容器类似:
...
spec:
containers:
- name: my-app
image: <my-app>
initContainers:
- name: init-my-app
image: <init-my-app>
...
它们仅在以下几个方面有所不同:
-
init容器没有就绪探针,因为它们运行到完成为止。 -
在
init容器中定义的端口不会被 pod 前面的服务捕获。 -
资源请求限制是通过计算
max(sum(regular containers))和max(init containers))来得出的,这意味着如果其中一个init容器设置的资源限制高于其他init容器的限制以及所有常规容器资源限制的总和,Kubernetes 会根据该init容器的资源限制来调度 pod。
init 容器的作用不仅仅是阻塞应用容器。例如,我们可以利用 init 容器通过与 init 容器和应用容器共享 emptyDir 卷来配置一个镜像,而不是构建另一个只对基础镜像执行 awk / sed 的镜像。此外,它还使我们能够灵活地为初始化任务和主要应用程序使用不同的镜像。
终止一个 pod
关闭事件的顺序与启动 pod 时的事件类似。在收到删除请求后,Kubernetes 会向即将被删除的 pod 发送 SIGTERM,并且该 pod 的状态变为终止。与此同时,如果该 pod 支持服务,Kubernetes 会移除该 pod 的端点,以停止进一步的请求。偶尔,某些 pod 可能永远不会退出。可能是这些 pod 不遵循 SIGTERM,或者只是因为它们的任务尚未完成。在这种情况下,Kubernetes 会在终止期过后发送 SIGKILL 强制终止这些 pod。终止期的长度在 pod 规格中的 .spec.terminationGracePeriodSeconds 设置。尽管 Kubernetes 仍然有机制回收这些 pod,但我们仍应确保我们的 pod 能够正确关闭。
处理 SIGTERM
优雅终止并不是一个新概念;它是编程中的常见做法。强行终止一个正在工作的 pod 就像突然拔掉正在运行的计算机的电源线,这可能会损坏数据。
实现主要包括三个步骤:
-
注册处理程序来捕获终止信号。
-
在处理程序中完成所有必要的操作,例如释放资源、将数据写入外部持久化层、释放分布式锁或关闭连接。
-
执行程序关闭。我们之前的示例展示了这一思路:在
graceful_exit_handler处理程序中,接收到SIGTERM信号时关闭控制器线程。代码可以在这里找到:github.com/DevOps-with-Kubernetes/okeydokey/blob/master/app.py。
由于 Kubernetes 只能向容器中的 PID 1 进程发送信号,所以有一些常见的陷阱可能会导致我们程序中的优雅处理程序失败。
SIGTERM 没有发送到应用程序进程
在第二章,容器化 DevOps 中,我们学习了在编写 Dockerfile 时,有两种方式调用程序:Shell 表达式和 exec 表达式。在 Linux 容器中,执行 Shell 表达式命令的 Shell 默认是 /bin/sh -c。因此,关于 SIGTERM 是否能被我们的应用程序接收,存在一些相关问题:
-
我们的应用程序是如何被调用的?
-
镜像中使用的是哪种 Shell 实现?
-
Shell 实现是如何处理
-c参数的?
我们逐一分析这些问题。以下示例中使用的 Dockerfile 可以在这里找到:github.com/PacktPublishing/DevOps-with-Kubernetes-Second-Edition/tree/master/chapter9/9-3_on_pods/graceful_docker。
假设我们在 Dockerfile 中使用的是 Shell 表达式命令,CMD python -u app.py,用于执行我们的应用程序。容器的启动命令将是 /bin/sh -c "python3 -u app.py"。当容器启动时,容器内进程的结构如下:
# the image is from "graceful_docker/Dockerfile.shell-sh"
$ kubectl run --generator=run-pod/v1 \
--image=devopswithkubernetes/ch93:shell-sh my-app
pod/my-app created
$ kubectl exec my-app ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/sh -c python3 -u app.py
6 ? S 0:00 python3 -u app.py
7 ? Rs 0:00 ps ax
我们可以看到,PID 1 进程不是我们的应用程序和处理程序,而是 Shell。当我们尝试终止 Pod 时,SIGTERM 会发送到 Shell 而不是我们的应用程序,Pod 会在宽限期结束后被终止。我们可以在删除应用程序时检查日志,看看它是否收到了 SIGTERM:
$ kubectl delete pod my-app &
pod "my-app" deleted
$ kubectl logs -f my-app
$ 1544368565.736720800 - [app] starting server.
rpc error: code = Unknown desc = Error: No such container: 2f007593553cfb700b0aece1f8b6045b4096b2f50f97a42e684a98e502af29ed
我们的应用程序在没有进入代码中的停止处理程序的情况下退出了。正确提升应用程序为 PID 1 的方法有几种。例如,我们可以在 Shell 表达式中显式调用 exec,如 CMD exec python3 -u app.py,这样我们的程序就会继承 PID 1。或者,我们可以选择 exec 表达式,CMD [ "python3", "-u", "app.py" ],直接执行我们的程序:
## shell form with exec
$ kubectl run --generator=run-pod/v1 \
--image=devopswithkubernetes/ch93:shell-exec my-app-shell-exec
pod/my-app-shell-exec created
$ kubectl exec my-app-exec ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 python3 -u app.py
5 ? Rs 0:00 ps ax
## delete the pod in another terminal
$ kubectl logs -f my-app-shell-exec
1544368913.313778162 - [app] starting server.
1544369448.991261721 - [app] stopping server.
rpc error: code = Unknown desc =...
## exec form
$ kubectl run --generator=run-pod/v1 \
--image=devopswithkubernetes/ch93:exec-sh my-app-exec
pod/my-app-exec created
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 python3 -u app.py
5 ? Rs 0:00 ps ax
$ kubectl logs -f my-app-exec
1544368942.935727358 - [app] starting server.
1544369503.846865654 - [app] stopping server.
rpc error: code = Unknown desc =...
无论以哪种方式执行,程序现在都可以正确地接收SIGTERM信号。此外,如果我们需要通过 shell 脚本为程序设置环境,我们应该在脚本中捕获信号并将其传播给程序,或者使用 exec 调用来启动程序,以便应用程序中的信号处理器能够按预期工作。
第二个和第三个问题是关于 shell 的影响:它如何影响我们优雅的处理程序?同样,Linux 中 Docker 容器的默认命令是/bin/sh -c。由于sh在流行的 Docker 镜像中有所不同,因此它处理-c的方式也可能影响信号,特别是在使用 shell 形式时。例如,Alpine Linux 将ash链接到/bin/sh,而 Debian 系列的发行版使用dash。在 Alpine 3.8 之前(或 BusyBox 1.28.0),ash在使用sh -c时会派生出一个新进程,而在 3.8 版本中,它使用exec。我们可以通过ps观察到这一差异,在 3.7 版本中,它会获得PID 6,而在 3.8 版本中则为PID 1:
$ docker run alpine:3.7 /bin/sh -c "ps ax"
PID USER TIME COMMAND
1 root 0:00 /bin/sh -c ps ax
6 root 0:00 ps ax
$ docker run alpine:3.8 /bin/sh -c "ps ax"
PID USER TIME COMMAND
1 root 0:00 ps ax
dash和bash如何处理这些情况?让我们来看看:
## there is no ps inside the official debian image, Here we reuse the one from above, which is also based on the debian:
$ docker run devopswithkubernetes/ch93:exec-sh /bin/sh -c "ps ax"
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/sh -c ps ax
6 ? R 0:00 ps ax
$ docker run devopswithkubernetes/ch93:exec-sh /bin/bash -c "ps ax"
PID TTY STAT TIME COMMAND
1 ? Rs 0:00 ps ax
正如我们所见,它们的结果也不同。我们的应用程序现在可以正确地响应终止事件。然而,还有一件事,如果我们的应用程序以PID 1运行,并且在容器内使用多个进程,这可能会对系统造成潜在的危害。
在 Linux 系统中,如果子进程的父进程没有等待其执行,子进程将变成僵尸进程。如果父进程在子进程结束之前死亡,那么init进程应该收养这些孤儿进程,并清理变成僵尸的进程。系统程序知道如何处理孤儿进程,因此僵尸进程通常不会成为问题。然而,在容器化的环境中,持有PID 1的进程是我们的应用程序,操作系统期望我们的应用程序去清理僵尸进程。由于我们的应用程序并没有被设计成一个合适的init进程,因此处理子进程的状态是不现实的。如果我们忽略这一点,最糟糕的情况是节点的进程表会被僵尸进程填满,我们将无法在节点上启动新的程序。在 Kubernetes 中,如果包含僵尸进程的 Pod 被销毁,那么 Pod 内部的所有僵尸进程将会被清理。另一个可能的场景是,如果我们的应用程序通过脚本频繁在后台执行某些任务,这可能会派生出大量的进程。让我们考虑以下这个简单的例子:
$ kubectl run --generator=run-pod/v1 \
--image=devopswithkubernetes/ch93:exec-sh my-app-exec pod/my-app-exec created ## let's enter our app pod and run sleep in background inside it $ kubectl exec -it my-app-exec /bin/sh
# ps axf
PID TTY STAT TIME COMMAND
5 pts/0 Ss 0:00 /bin/sh
10 pts/0 R+ 0:00 \_ ps axf
1 ? Ss 0:00 python3 -u app.py
# sleep 30 &
# ps axf
PID TTY STAT TIME COMMAND
5 pts/0 Ss 0:00 /bin/sh
11 pts/0 S 0:00 \_ sleep 30
12 pts/0 R+ 0:00 \_ ps axf
1 ? Ss 0:00 python3 -u app.py
## now quit kubectl exec, wait 30 seconds, and check the pod again
$ kubectl exec my-app-exec ps axf
PID TTY STAT TIME COMMAND
23 ? Rs 0:00 ps axf
1 ? Ss 0:00 python3 -u app.py
11 ? Z 0:00 [sleep] <defunct>
sleep 30现在是我们 Pod 中的一个僵尸进程。在第二章《与容器的 DevOps》中,我们提到过docker run --init参数可以为我们的容器设置一个简单的init进程。在 Kubernetes 中,我们可以通过在 Pod 的规格中指定.spec.shareProcessNamespace,使pause容器(一个特殊容器,默默地为我们处理这些事务)出现在 Pod 中:
$ kubectl apply -f chapter9/9-3_on_pods/sharepidns.yml
pod/my-app-with-pause created
$ kubectl exec my-app-with-pause ps ax
1 ? Ss 0:00 /pause
6 ? Ss 0:00 python3 -u app.py
10 ? Rs 0:00 ps ax
pause进程确保丧尸进程被清理,SIGTERM会发送到我们的应用程序进程。请注意,通过启用进程命名空间共享,除了我们的应用程序不再具有PID 1,还有两个关键的区别:
-
同一 pod 中的所有容器共享进程信息,这意味着一个容器可以向另一个容器发送信号。
-
容器的文件系统可以通过
/proc/$PID/root路径进行访问。
如果在init进程仍然需要的情况下,所描述的行为不适合你的应用程序,你可以选择 Tini(github.com/krallin/tini),或者 dump-init(github.com/Yelp/dumb-init),甚至编写一个包装脚本来解决丧尸进程清理问题。
SIGTERM不会触发终止处理程序。
在某些情况下,进程的终止处理程序不会被SIGTERM触发。例如,向nginx发送SIGTERM实际上会导致快速关闭。要优雅地关闭nginx控制器,我们必须通过nginx -s quit发送SIGQUIT。
nginx信号支持的完整操作列表请查看此处:nginx.org/en/docs/control.html。
现在,出现了另一个问题:在删除 pod 时,如何向容器发送除了SIGTERM以外的信号?我们可以修改程序的行为来捕获SIGTERM,但对于像nginx这样的流行工具,我们无法做出任何处理。对于这种情况,我们可以使用生命周期钩子。
容器生命周期钩子
生命周期钩子是某些事件触发的操作,并在容器上执行。它们的工作方式类似于单一的 Kubernetes 探针操作,但在容器生命周期中的每个事件至少会触发一次。目前,支持两个事件:
-
PostStart:在容器创建后执行。由于这个钩子和容器的入口点是异步触发的,所以无法保证钩子会在容器启动之前执行。因此,我们不太可能使用它来初始化容器的资源。 -
PreStop:在发送SIGTERM到容器之前执行。与PostStart钩子的一个区别是,PreStop钩子是同步调用;换句话说,只有在PreStop钩子执行完毕后,SIGTERM才会被发送。
我们可以通过PreStop钩子轻松解决nginx的关闭问题:
...
containers:
- name: main
image: nginx
life cycle:
preStop:
exec:
command: [ "nginx", "-s", "quit" ]
...
钩子的一个重要特性是它们可以以某种方式影响 pod 的状态:只有当PostStart钩子成功退出时,pod 才会运行;pod 在删除时会立即终止,但只有在PreStop钩子成功退出后,SIGTERM才会被发送。因此,我们可以通过PreStop钩子解决 pod 在其代理规则在节点上被移除之前退出的问题。
下图展示了如何使用钩子来消除不必要的间隙:

其实现方式只是添加一个挂钩,让它休眠几秒钟:
...
containers:
- name: main
image: my-app
life cycle:
preStop:
exec:
command: [ "/bin/sh", "-c", "sleep 5" ]
...
处理 Pod 中断
理想情况下,我们希望尽可能保持服务的可用性。然而,总是有许多事件导致支持我们服务的 Pod 出现上下波动,无论是自愿的还是非自愿的。自愿中断包括Deployment的推出、计划中的节点维护,或者通过 API 意外终止 Pod。总体而言,任何经过 Kubernetes 主控的操作都会算作一个事件。另一方面,任何导致服务终止的意外停机都属于非自愿中断的范畴。
在前面的章节中,我们讨论了如何通过使用Deployment和StatefulSet复制 Pod,适当配置资源请求和限制,通过自动扩展器调整应用程序的容量,并使用亲和性和反亲和性将 Pod 分布到多个位置,从而防止非自愿的中断。既然我们已经在服务上付出了很多努力,那在面对这些预期中的自愿中断时,可能会出现什么问题呢?事实上,由于这些事件很可能发生,我们应该更加关注它们。
在Deployment和其他类似对象中,我们可以使用maxUnavailable和maxSurge字段,这些字段帮助我们以受控的方式推出更新。至于其他情况,比如由集群管理员执行的节点维护任务,这些管理员并不了解集群中运行的所有应用程序,服务所有者可以利用PodDisruptionBudget来告知 Kubernetes,服务需要多少 Pod 才能达到服务水平。
一个 Pod 中断预算的语法如下:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: <pdb name>
spec:
maxUnavailable: <desired number or percentage of pods>
minAvailable: <desired number or percentage of pods>
selector:
<matchLabels> or <matchExpressions>
Pod 中断预算中有两个可配置的字段,但它们不能一起使用。选择器与Deployment或其他地方的选择器相同。需要注意的是,Pod 中断预算是不可变的,这意味着它在创建后无法更新。minAvailable和maxUnavailable字段是互斥的,但在某些方面它们是相同的。例如,maxUnavailable:0意味着对任何 Pod 丢失的零容忍,基本上等同于minAvailable:100%,即所有 Pod 必须保持可用。
Pod 中断预算通过驱逐事件来工作,如节点排空或 Pod 优先级抢占。它们不会干扰由控制器如Deployment或StatefulSet执行的滚动更新过程。假设我们想用kubectl drain暂时从集群中移除一个节点,但这会违反某些正在运行的应用程序的 Pod 中断预算。在这种情况下,除非所有的 Pod 中断预算都能得到满足,否则排空操作会被阻止。然而,如果 Kubernetes 调度器将驱逐一个受害 Pod 以满足高优先级的 Pods,调度器只会在可能的情况下尝试满足所有的 Pod 中断预算。如果调度器找不到不违反任何 Pod 中断预算的目标,它仍然会选择优先级最低的 Pod。
概述
在本章中,我们讨论了与构建持续交付流水线相关的主题,以及如何加强我们的部署任务。Pod 的滚动更新是一个强大的工具,它使我们能够以受控的方式进行更新。为了触发滚动更新,我们需要做的就是在支持该滚动更新的控制器中更改 Pod 的规格。此外,尽管更新由 Kubernetes 管理,我们仍然可以通过kubectl rollout在一定程度上控制它。
随后,我们构建了一个可扩展的持续交付流水线,使用了GitHub/DockerHub/Travis-CI。然后,我们继续学习了更多关于 Pod 生命周期的内容,以防止可能的失败,包括使用就绪探针和存活探针来保护 Pod;用init容器初始化 Pod;通过选择合适的入口程序命令和运行它的 Shell 组合来正确处理SIGTERM;使用生命周期钩子来延迟 Pod 的就绪状态,以及其终止,以便 Pod 在正确的时间从服务中移除;以及分配 Pod 中断预算以确保 Pod 的可用性。
在第十章,Kubernetes on AWS,我们将继续学习如何在 AWS(所有公共云提供商中的重要角色)上部署集群的基本要点。
第十章:AWS 上的 Kubernetes
在公共云上使用 Kubernetes 对你的应用来说是灵活且可扩展的。AWS 是公共云行业中最受欢迎的服务之一。在本章中,你将学习什么是 AWS 以及如何在 AWS 上设置 Kubernetes,内容包括以下主题:
-
理解公共云
-
使用和理解 AWS 组件
-
使用 Amazon EKS 在 AWS 上设置 Kubernetes 集群
-
使用 EKS 管理 Kubernetes
AWS 简介
当你在公共网络上运行应用时,你需要基础设施,例如网络、虚拟机(VM)和存储。显然,公司会借用或自建数据中心来准备这些基础设施,然后聘请数据中心工程师和操作员来监控和管理这些资源。
然而,购买和维护这些资产需要巨大的资本支出,以及数据中心工程师/操作员的运营开销。你还需要一定的时间来完全搭建这些基础设施,例如购买服务器、安装到数据中心机架、布设网络电缆,然后进行操作系统的初步配置/安装等。
因此,迅速分配具有适当资源容量的基础设施是决定企业成功的关键因素之一。
为了使基础设施管理更加简便和高效,技术能够为数据中心提供很多帮助,例如虚拟化、软件定义网络(SDN)和存储区域网络(SAN)。但是,将这些技术结合起来会出现一些敏感的兼容性问题,且难以稳定,因此需要聘请行业专家,这最终会增加运营成本。
公共云
有一些公司已经提供了在线基础设施服务。AWS 是提供在线基础设施的知名服务,称为云或公共云。早在 2006 年,AWS 正式推出了虚拟机服务,称为弹性计算云(EC2);在线对象存储服务,称为简单存储服务(S3);以及在线消息队列服务,称为简单队列服务(SQS)。
这些服务足够简单,但从数据中心管理的角度来看,它们减轻了基础设施预分配的压力并缩短了读取时间,因为采用了按需计费模式(按小时或按年向 AWS 付费)。因此,AWS 越来越受欢迎,许多公司已经从自建数据中心转向了公共云。
与公共云相对的,你自己的数据中心被称为本地部署。
API 和基础设施即代码
使用公共云而非本地数据中心的一个独特优势是,公共云提供了控制基础设施的 API。AWS 提供命令行工具(AWS CLI)来控制 AWS 基础设施。例如,在注册 AWS 后(aws.amazon.com/free/),安装 AWS CLI(docs.aws.amazon.com/cli/latest/userguide/installing.html);然后,如果你想启动一个虚拟机(EC2 实例),可以使用 AWS CLI,如下所示:

如你所见,注册 AWS 后,只需几分钟即可访问你的虚拟机。另一方面,如果你从头开始建立自己的本地数据中心会怎样呢?以下图表展示了使用本地数据中心和使用公共云的对比:

如你所见,公共云非常简单快捷;这就是它灵活且方便的原因,不仅适合新兴使用,也适合长期使用。
AWS 组件
AWS 有一些组件用于配置网络和存储。这些对于理解公共云的工作原理以及如何配置 Kubernetes 非常重要。
VPC 和子网
在 AWS 上,首先你需要创建自己的网络。这被称为虚拟私有云(VPC),它使用 SDN 技术。AWS 允许你在 AWS 上创建一个或多个 VPC。每个 VPC 可以根据需要互相连接。当你创建 VPC 时,只需定义一个网络 CIDR 块和 AWS 区域,例如,10.0.0.0/16 CIDR 在us-east-1上。无论你是否可以访问公共网络,你都可以定义任何网络地址范围(从/16到/28的子网掩码范围)。VPC 创建非常快速;一旦创建完成 VPC,你需要在 VPC 内创建一个或多个子网。
在下面的示例中,通过 AWS 命令行创建一个 VPC:
//specify CIDR block as 10.0.0.0/16
//the result, it returns VPC ID as "vpc-0ca37d4650963adbb"
$ aws ec2 create-vpc --cidr-block 10.0.0.0/16
{
"Vpc": {
"CidrBlock": "10.0.0.0/16",
"DhcpOptionsId": "dopt-3d901958",
"State": "pending",
"VpcId": "vpc-0ca37d4650963adbb",
...
子网是一个逻辑网络块。它必须属于一个 VPC 以及一个可用区,例如,vpc-0ca37d4650963adbb VPC 和 us-east-1b。然后,网络 CIDR 必须在 VPC 的 CIDR 范围内。例如,如果 VPC 的 CIDR 是10.0.0.0/16(10.0.0.0–10.0.255.255),那么一个子网的 CIDR 可以是10.0.1.0/24(10.0.1.0–10.0.1.255)。
在下面的示例中,我们将在us-east-1a可用区创建两个子网,并在us-east-1b可用区创建另外两个子网。因此,按照以下步骤,总共会在vpc-0ca37d4650963adbb中创建四个子网:
- 在
us-east-1a可用区创建第一个子网,10.0.1.0/24:
$ aws ec2 create-subnet --vpc-id vpc-0ca37d4650963adbb --cidr-block 10.0.1.0/24 --availability-zone us-east-1a
{
"Subnet": {
"AvailabilityZone": "us-east-1a",
"AvailabilityZoneId": "use1-az6",
"AvailableIpAddressCount": 251,
"CidrBlock": "10.0.1.0/24",
"DefaultForAz": false,
"MapPublicIpOnLaunch": false,
"State": "pending",
"SubnetId": "subnet-09f8f7f06c27cb0a0",
"VpcId": "vpc-0ca37d4650963adbb",
...
- 在
us-east-1b可用区创建第二个子网,10.0.2.0/24:
$ aws ec2 create-subnet --vpc-id vpc-0ca37d4650963adbb --cidr-block 10.0.2.0/24 --availability-zone us-east-1b
{
"Subnet": {
"AvailabilityZone": "us-east-1b",
"AvailabilityZoneId": "use1-az1",
"AvailableIpAddressCount": 251,
"CidrBlock": "10.0.2.0/24",
"DefaultForAz": false,
"MapPublicIpOnLaunch": false,
"State": "pending",
"SubnetId": "subnet-04b78ed9b5f96d76e",
"VpcId": "vpc-0ca37d4650963adbb",
...
- 在
us-east-1b上再次创建第三个子网,10.0."3".0/24:
$ aws ec2 create-subnet --vpc-id vpc-0ca37d4650963adbb --cidr-block 10.0.3.0/24 --availability-zone us-east-1b
{
"Subnet": {
"AvailabilityZone": "us-east-1b",
"AvailabilityZoneId": "use1-az1",
"AvailableIpAddressCount": 251,
"CidrBlock": "10.0.3.0/24",
"DefaultForAz": false,
"MapPublicIpOnLaunch": false,
"State": "pending",
"SubnetId": "subnet-026058e32f09c28af",
"VpcId": "vpc-0ca37d4650963adbb",
...
- 在
us-east-1a上再次创建第四个子网,10.0.4.0/24:
$ aws ec2 create-subnet --vpc-id vpc-0ca37d4650963adbb --cidr-block 10.0.4.0/24 --availability-zone us-east-1a
{
"Subnet": {
"AvailabilityZone": "us-east-1a",
"AvailabilityZoneId": "use1-az6",
"AvailableIpAddressCount": 251,
"CidrBlock": "10.0.4.0/24",
"DefaultForAz": false,
"MapPublicIpOnLaunch": false,
"State": "pending",
"SubnetId": "subnet-08e16157c15cefcbc",
"VpcId": "vpc-0ca37d4650963adbb",
...
让我们将前两个子网设置为公共子网,将最后两个子网设置为私有子网。这意味着公共子网可以从互联网访问,因此它将具有公共 IP 地址。另一方面,私有子网没有互联网访问能力。为此,你需要设置网关和路由表。
互联网网关和 NAT-GW
在大多数情况下,你的 VPC 需要与公共互联网连接。在这种情况下,你需要创建一个 互联网网关 (IGW) 并将其附加到你的 VPC。
在以下示例中,创建了一个 IGW 并将其附加到 vpc-0ca37d4650963adbb:
//create IGW, it returns IGW id as igw-01769bff334dcc035
$ aws ec2 create-internet-gateway
{
"InternetGateway": {
"Attachments": [],
"InternetGatewayId": "igw-01769bff334dcc035",
"Tags": []
}
}
//attach igw-01769bff334dcc035 to vpc-0ca37d4650963adbb
$ aws ec2 attach-internet-gateway --vpc-id vpc-0ca37d4650963adbb --internet-gateway-id igw-01769bff334dcc035
一旦 IGW 被附加,设置指向 IGW 的路由表(默认网关)为子网。如果默认网关指向 IGW,则该子网能够拥有公共 IP 地址并能够访问互联网。因此,如果默认网关不指向 IGW,则该子网被视为私有子网,意味着没有公共访问。
在以下示例中,创建了一个路由表,指向 IGW,并设置为公共子网:
//create route table within vpc-0ca37d4650963adbb
//it returns route table id as rtb-0f45fc46edec61d8f
$ aws ec2 create-route-table --vpc-id vpc-0ca37d4650963adbb
{
"RouteTable": {
"Associations": [],
"PropagatingVgws": [],
"RouteTableId": "rtb-0f45fc46edec61d8f",
...
//then set default route (0.0.0.0/0) as igw-01769bff334dcc035
$ aws ec2 create-route --route-table-id rtb-0f45fc46edec61d8f --gateway-id igw-01769bff334dcc035 --destination-cidr-block 0.0.0.0/0
//finally, update public 2 subnets to use this route table
$ aws ec2 associate-route-table --route-table-id rtb-0f45fc46edec61d8f --subnet-id subnet-09f8f7f06c27cb0a0
$ aws ec2 associate-route-table --route-table-id rtb-0f45fc46edec61d8f --subnet-id subnet-026058e32f09c28af
//public subnet can assign public IP when launch EC2
$ aws ec2 modify-subnet-attribute --subnet-id subnet-09f8f7f06c27cb0a0 --map-public-ip-on-launch
$ aws ec2 modify-subnet-attribute --subnet-id subnet-026058e32f09c28af --map-public-ip-on-launch
另一方面,私有子网不需要公共 IP 地址。然而,私有子网有时需要访问互联网,例如,下载一些软件包和访问 AWS 服务。在这种情况下,我们仍然可以选择连接到互联网。这被称为 网络地址转换网关 (NAT-GW)。
NAT-GW 允许私有子网通过 NAT-GW 访问公共互联网。因此,NAT-GW 必须位于公共子网,并且私有子网的路由表会将 NAT-GW 作为默认网关。请注意,为了能够访问公共网络上的 NAT-GW,它需要附加一个 弹性 IP (EIP) 到 NAT-GW。
在以下示例中,创建了一个 NAT-GW:
//allocate EIP, it returns allocation id as eipalloc-044f4dbafe870a04a
$ aws ec2 allocate-address
{
"PublicIp": "54.161.228.168",
"AllocationId": "eipalloc-044f4dbafe870a04a",
"PublicIpv4Pool": "amazon",
"Domain": "vpc"
}
//create NAT-GW on public subnet (subnet-09f8f7f06c27cb0a0)
//also assign EIP eipalloc-044f4dbafe870a04a
$ aws ec2 create-nat-gateway --subnet-id subnet-09f8f7f06c27cb0a0 --allocation-id eipalloc-044f4dbafe870a04a
{
"NatGateway": {
"CreateTime": "2018-12-09T20:17:33.000Z",
"NatGatewayAddresses": [
{
"AllocationId": "eipalloc-044f4dbafe870a04a"
}
],
"NatGatewayId": "nat-05e34091f53f10172",
"State": "pending",
"SubnetId": "subnet-09f8f7f06c27cb0a0",
"VpcId": "vpc-0ca37d4650963adbb"
}
}
与 IGW 不同,你可以在单个 可用区 (AZ) 部署 NAT-GW。如果你需要高可用性的 NAT-GW,你需要在每个可用区部署 NAT-GW。然而,AWS 会额外收取弹性 IP 和 NAT-GW 的每小时费用。因此,如果你想节省成本,可以像前面的示例一样在单个可用区部署单个 NAT-GW。
创建 NAT-GW 只需几分钟。创建完成后,更新指向 NAT-GW 的私有子网路由表,然后任何 EC2 实例就可以访问互联网;然而,由于私有子网没有公共 IP 地址,因此没有机会从公共互联网访问私有子网的 EC2 实例。
在以下示例中,更新了指向 NAT-GW 的私有子网路由表作为默认网关:
//as same as public route, need to create a route table first
$ aws ec2 create-route-table --vpc-id vpc-0ca37d4650963adbb
{
"RouteTable": {
"Associations": [],
"PropagatingVgws": [],
"RouteTableId": "rtb-08572c332e7e4f14e",
...
//then assign default gateway as NAT-GW
$ aws ec2 create-route --route-table-id rtb-08572c332e7e4f14e --nat-gateway-id nat-05e34091f53f10172 --destination-cidr-block 0.0.0.0/0
//finally update private subnet routing table
$ aws ec2 associate-route-table --route-table-id rtb-08572c332e7e4f14e --subnet-id subnet-04b78ed9b5f96d76e
$ aws ec2 associate-route-table --route-table-id rtb-08572c332e7e4f14e --subnet-id subnet-08e16157c15cefcbc
总体而言,配置了四个子网,其中两个是公共子网,两个是私有子网。每个子网都有一个默认路由,指向 IGW 和 NAT-GW,具体如下。请注意,ID 会有所不同,因为 AWS 会分配唯一标识符:
| 子网类型 | CIDR 块 | 可用区 | 子网 ID | 路由表 ID | 默认网关 | 在 EC2 启动时分配公共 IP |
|---|---|---|---|---|---|---|
| 公有 | 10.0.1.0/24 | us-east-1a | subnet-``09f8f7f06c27cb0a0 |
rtb-``0f45fc46edec61d8f |
igw-``01769bff334dcc035 (IGW) |
是 |
| 私有 | 10.0.2.0/24 | us-east-1b | subnet-``04b78ed9b5f96d76e |
rtb-``08572c332e7e4f14e |
nat-``05e34091f53f10172 (NAT-GW) |
否(默认) |
| 公有 | 10.0.3.0/24 | us-east-1b | subnet-``026058e32f09c28af |
rtb-``0f45fc46edec61d8f |
igw-``01769bff334dcc035 (IGW) |
是 |
| 私有 | 10.0.4.0/24 | us-east-1a | subnet-``08e16157c15cefcbc |
rtb-``08572c332e7e4f14e |
nat-``05e34091f53f10172 (NAT-GW) |
否(默认) |
从技术上讲,你仍然可以为私有子网中的 EC2 实例分配公共 IP,但没有默认的互联网网关(IGW)。因此,公共 IP 将被浪费,并且无法从互联网访问。
现在,如果你在公有子网启动 EC2 实例,它就会变为公有-facing,所以你可以从这个子网提供你的应用程序。
另一方面,如果你在私有子网启动 EC2 实例,它仍然可以通过 NAT-GW 访问互联网,但无法从互联网访问。不过,它仍然可以从公有子网的 EC2 主机进行访问。因此,理想情况下,你可以在私有子网上部署内部服务,如数据库、中间件和监控工具。
安全组
一旦 VPC 和带有相关网关/路由的子网准备好,你就可以创建 EC2 实例了。然而,至少需要事先创建一个访问控制,这就是所谓的安全组。它可以定义入站(传入网络访问)和出站(传出网络访问)防火墙规则。
在以下示例中,创建了一个安全组和一个规则,允许公有子网主机从你的机器 IP 地址进行 SSH 访问,并且允许开放 HTTP(80/tcp)全球访问:
//create one security group for public subnet
$ aws ec2 create-security-group --vpc-id vpc-0ca37d4650963adbb --group-name public --description "public facing host"
{
"GroupId": "sg-03973d9109a19e592"
}
//check your machine's public IP (if not sure, use 0.0.0.0/0 as temporary)
$ curl ifconfig.co
98.234.106.21
//public facing machine allows ssh only from your machine
$ aws ec2 authorize-security-group-ingress --group-id sg-03973d9109a19e592 --protocol tcp --port 22 --cidr 98.234.106.21/32
//public facing machine allow HTTP access from any host (0.0.0.0/0)
$ aws ec2 authorize-security-group-ingress --group-id sg-03973d9109a19e592 --protocol tcp --port 80 --cidr 0.0.0.0/0
接下来,创建一个私有子网主机的安全组,允许从公有子网主机进行 SSH 访问。在这种情况下,指定公有子网的安全组 ID(sg-03973d9109a19e592)而不是 CIDR 块是方便的:
//create security group for private subnet
$ aws ec2 create-security-group --vpc-id vpc-0ca37d4650963adbb --group-name private --description "private subnet host"
{
"GroupId": "sg-0f4058a729e2c207e"
} //private subnet allows ssh only from public subnet host security group
$ aws ec2 authorize-security-group-ingress --group-id sg-0f4058a729e2c207e --protocol tcp --port 22 --source-group sg-03973d9109a19e592
//it also allows HTTP (80/TCP) from public subnet security group
$ aws ec2 authorize-security-group-ingress --group-id sg-0f4058a729e2c207e --protocol tcp --port 80 --source-group sg-03973d9109a19e592
当你为一个公有子网定义安全组时,强烈建议由安全专家进行审查。这是因为,一旦你将 EC2 实例部署到公有子网,它就会有一个公共 IP 地址,届时包括黑客和机器人在内的所有人都能够直接访问你的实例。
总体而言,已经创建了两个安全组,如下所示:
| 名称 | 安全组 ID | 允许 SSH(22/TCP) | 允许 HTTP(80/TCP) |
|---|---|---|---|
| 公有 | sg-03973d9109a19e592 |
你的机器 (98.234.106.21) |
0.0.0.0/0 |
| 私有 | sg-0f4058a729e2c207e |
公共 sg (sg-03973d9109a19e592) |
公共 sg (sg-03973d9109a19e592) |
EC2 和 EBS
EC2 是 AWS 中一项重要服务,您可以使用它在您的 VPC 上启动虚拟机(VM)。根据硬件规格(CPU、内存和网络),AWS 提供了几种类型的 EC2 实例。在启动 EC2 实例时,您需要指定 VPC、子网、安全组和 SSH 密钥对。因此,所有这些必须事先创建。
根据之前的示例,最后一步是 ssh-keypair。让我们创建 ssh-keypair:
//create keypair (aws_rsa, aws_rsa.pub)
$ ssh-keygen -f ~/.ssh/aws_rsa -N ""
//register aws_rsa.pub key to AWS
$ aws ec2 import-key-pair --key-name=my-key --public-key-material "`cat ~/.ssh/aws_rsa.pub`"
{
"KeyFingerprint": "73:89:80:1f:cc:25:94:7a:ba:f4:b0:81:ae:d8:bb:92",
"KeyName": "my-key"
}
//launch public facing host, using Amazon Linux (ami-009d6802948d06e52) on us-east-1
$ aws ec2 run-instances --image-id ami-009d6802948d06e52 --instance-type t2.nano --key-name my-key --security-group-ids sg-03973d9109a19e592 --subnet-id subnet-09f8f7f06c27cb0a0 //launch private subnet host
$ aws ec2 run-instances --image-id ami-009d6802948d06e52 --instance-type t2.nano --key-name my-key --security-group-ids sg-0f4058a729e2c207e --subnet-id subnet-04b78ed9b5f96d76e
几分钟后,在 AWS Web 控制台上检查 EC2 实例的状态;这将显示一个具有公共 IP 地址的 public 子网主机。另一方面,私有子网主机没有公共 IP 地址:

让我们使用您的 SSH 私钥通过 IPv4 公共 IP 地址登录到 EC2 实例,具体步骤如下:
//add private keys to ssh-agent
$ ssh-add ~/.ssh/aws_rsa
//ssh to the public subnet host with -A (forward ssh-agent) option
$ ssh -A ec2-user@54.208.77.168
...
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
1 package(s) needed for security, out of 5 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-0-1-41 ~]$
现在您在公共子网主机(54.208.77.168)上,但该主机也有一个内部(私有)IP 地址,因为它部署在 10.0.1.0/24 子网中,因此私有地址范围必须是 10.0.1.1—10.0.1.254:
[ec2-user@ip-10-0-1-41 ~]$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 9001
inet 10.0.1.41 netmask 255.255.255.0 broadcast 10.0.1.255
inet6 fe80::cf1:1ff:fe9f:c7b2 prefixlen 64 scopeid 0x20<link>
...
让我们在公共主机上安装 nginx Web 服务器,具体步骤如下:
$ amazon-linux-extras |grep nginx
4 nginx1.12 available [ =1.12.2 ]
$ sudo amazon-linux-extras install nginx1.12
$ sudo systemctl start nginx
然后,返回您的机器并检查 54.208.77.168 网站:
[ec2-user@ip-10-0-1-41 ~]$ exit
logout
Connection to 54.208.77.168 closed.
$ curl -I 54.208.77.168
HTTP/1.1 200 OK
Server: nginx/1.12.2
...
此外,在同一 VPC 内,其他可用区之间是可以互相访问的;因此,您可以从公共子网中的 EC2 主机通过 SSH 访问私有子网主机(10.0.2.116)。请注意,我们使用了 ssh -A 选项来转发 ssh-agent,因此无需在 EC2 主机上创建 ~/.ssh/id_rsa 文件:
[ec2-user@ip-10-0-1-41 ~]$ ssh 10.0.2.116
...
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
1 package(s) needed for security, out of 5 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-0-2-116 ~]$
除了 EC2,AWS 还有另一项重要功能,名为磁盘管理。AWS 提供了一个灵活的磁盘管理服务,称为 弹性块存储(EBS)。您可以创建一个或多个持久数据存储,并将其附加到 EC2 实例。从 EC2 的角度来看,EBS 是一种硬盘(HDD/SSD)。一旦您终止(删除)EC2 实例,EBS 及其内容可能会保留,并可以重新附加到另一个 EC2 实例。
在以下示例中,创建了一个容量为 40 GB 的卷,并将其附加到公共子网主机(实例 ID 为 i-0f2750f65dd857e54):
//create 40GB disk at us-east-1a (as same as EC2 public subnet instance)
$ aws ec2 create-volume --availability-zone us-east-1a --size 40 --volume-type standard
{
"CreateTime": "2018-12-09T22:13:41.000Z",
"VolumeType": "standard",
"SnapshotId": "",
"VolumeId": "vol-006aada6fa87c0060",
"AvailabilityZone": "us-east-1a",
"Size": 40,
"State": "creating",
"Encrypted": false
}
//attach to public subnet host as /dev/xvdh $ aws ec2 attach-volume --device xvdh --instance-id i-0f2750f65dd857e54 --volume-id vol-006aada6fa87c0060
{
"State": "attaching",
"InstanceId": "i-0f2750f65dd857e54",
"AttachTime": "2018-12-09T22:15:32.134Z",
"VolumeId": "vol-006aada6fa87c0060",
"Device": "xvdh"
}
在将 EBS 卷附加到 EC2 实例后,Linux 内核会识别 /dev/xvdh,然后您需要进行分区才能使用该设备,具体步骤如下:

在此示例中,我们将一个分区命名为 /dev/xvdh1,因此您可以在 /dev/xvdh1 上创建一个 ext4 格式的文件系统,然后可以将其挂载以在 EC2 实例上使用该设备:

卸载卷后,您可以自由地分离此卷,并在需要时重新附加它:
$ aws ec2 detach-volume --volume-id vol-006aada6fa87c0060
{
"InstanceId": "i-0f2750f65dd857e54",
"VolumeId": "vol-006aada6fa87c0060",
"State": "detaching",
"Device": "xvdh",
"AttachTime": "2018-12-09T22:15:32.000Z"
}
ELB
AWS 提供了一个强大的基于软件的负载均衡器,称为 经典负载均衡器。这个负载均衡器之前被称为 弹性负载均衡器(ELB),它可以将网络流量负载均衡到一个或多个 EC2 实例。此外,ELB 还可以卸载 SSL/TLS 加密/解密,并支持多可用区。
那么,为什么特别是经典负载均衡器(ELB)呢?这是因为 AWS 引入了新的负载均衡器类型:网络负载均衡器(用于 L4)和应用负载均衡器(用于 L7)。因此,ELB 成为了经典负载均衡器。然而,虽然 ELB 稳定且可靠,Amazon EKS 默认使用负载均衡器,所以我们继续使用 ELB。
在下面的示例中,创建了一个 ELB 并与公共子网主机nginx(80/TCP)关联。因为 ELB 也需要一个安全组,所以首先为此创建一个新的安全组:
// Create New Security Group for ELB
$ aws ec2 create-security-group --vpc-id vpc-0ca37d4650963adbb --group-name elb --description "elb sg"
{
"GroupId": "sg-024f1c5315bac6b9e"
}
// ELB opens TCP port 80 for all IP addresses (0.0.0.0/0)
$ aws ec2 authorize-security-group-ingress --group-id sg-024f1c5315bac6b9e --protocol tcp --port 80 --cidr 0.0.0.0/0
// create ELB on public subnets
$ aws elb create-load-balancer --load-balancer-name public-elb --listeners Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80 --subnets subnet-09f8f7f06c27cb0a0 subnet-026058e32f09c28af --security-group sg-024f1c5315bac6b9e
{
"DNSName": "public-elb-1952792388.us-east-1.elb.amazonaws.com"
}
// Register an EC2 instance which runs nginx
$ aws elb register-instances-with-load-balancer --load-balancer-name public-elb --instances i-0f2750f65dd857e54
// You can access to ELB from your laptop
$ curl -I public-elb-1952792388.us-east-1.elb.amazonaws.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 3520
Content-Type: text/html
Date: Mon, 17 Dec 2018 06:05:45 GMT
ETag: "5bbfda61-dc0"
Last-Modified: Thu, 11 Oct 2018 23:18:57 GMT
Server: nginx/1.12.2
Connection: keep-alive
...
总体来说,我们讨论了如何配置 AWS 组件。以下是关于主要组件及其关系的总结和图示:
-
一个具有Internet Gateway(IGW)的 VPC
-
在
us-east-1a上的两个子网(公共和私有) -
在
us-east-1b上的两个子网(公共和私有) -
一个 NAT-GW
-
一个在公共子网中,带有 EBS 的公共 EC2 实例
-
一个私有子网中的私有 EC2 实例
-
ELB 将流量转发到公共 EC2 实例

Amazon EKS
我们讨论了一些 AWS 组件,这些组件非常容易用于设置网络、虚拟机、存储和负载均衡器。因此,有多种方式可以在 AWS 上设置 Kubernetes,例如 kubeadm (github.com/kubernetes/kubeadm),kops (github.com/kubernetes/kops)和 kubespray (github.com/kubernetes-sigs/kubespray)。
此外,自 2018 年 6 月起,AWS 开始提供一个新服务,称为 Amazon Elastic Container Service for Kubernetes(aws.amazon.com/eks/),简称EKS。这类似于 Google Kubernetes Engine(cloud.google.com/kubernetes-engine/)和 Azure Kubernetes Service(docs.microsoft.com/en-us/azure/aks/),这是一个托管的 Kubernetes 服务。
AWS 还提供了另一个容器编排服务,叫做 Amazon Elastic Container Service(ECS)aws.amazon.com/ecs/。AWS ECS 不是 Kubernetes 服务,但它完全与 AWS 组件集成,用于启动你的容器应用程序。
AWS EKS 使用 AWS 组件,如 VPC、安全组、EC2 实例、EBS、ELB、IAM 等,来设置 Kubernetes 集群。这还会管理 Kubernetes 集群,自动修补并替换有问题的组件,全天候运行。由于持续管理,用户将把安装、配置和监控的工作交给 Kubernetes 集群,而只需要按小时付费给 AWS。
对用户来说,AWS 提供了一组经过充分测试的 AWS 组件和 Kubernetes 版本的组合,这非常有益。这意味着用户可以在几分钟内开始在 AWS 上使用生产级的 Kubernetes。
让我们探索 AWS EKS,了解 AWS 如何将 Kubernetes 集成到 AWS 组件中。
深入了解 AWS EKS
AWS EKS 有两个主要组件。它们如下所示:
-
控制平面
-
工作节点
控制平面是由 AWS 管理的 Kubernetes 主节点,其中包括一个 etcd 数据库。AWS 帮助在多个可用区上部署 Kubernetes 主节点。用户可以通过 AWS Web 控制台或 AWS CLI 监控和访问控制平面。此外,用户还可以通过 Kubernetes 客户端(如 kubectl 命令)访问 Kubernetes API 服务器。
截至 2018 年 12 月,AWS 仅为工作节点提供了自定义 Amazon 机器映像 (AMI)。AWS 目前不提供 Web 控制台或 AWS CLI 来创建和配置工作节点。因此,用户需要使用该 AMI 启动 EC2 实例并手动配置工作节点。
亚马逊和 Weaveworks 合作开发了一个名为 eksctl 的开源项目(eksctl.io/)。使用它比使用 AWS CLI 和一些手动步骤部署 EKS 集群更容易。
如果你在理解 AWS 基础知识和 EKS 配置方面有困难,建议使用 eksctl。
幸运的是,AWS 提供了一个易于使用的 CloudFormation 模板来启动和配置工作节点,因此让我们扩展先前的 VPC 示例来设置 Amazon EKS。为此,你需要提前准备以下设置:
- 按如下方式设置 IAM 服务角色(定义哪个 AWS 用户可以创建 EKS 资源):
$ aws iam create-role --role-name eksServiceRole --assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "eks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }'
$ aws iam attach-role-policy --role-name eksServiceRole --policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
$ aws iam attach-role-policy --role-name eksServiceRole --policy-arn arn:aws:iam::aws:policy/AmazonEKSServicePolicy
- 设置安全组(分配给
控制平面,然后工作节点使用此安全组来允许来自控制平面的访问):
$ aws ec2 create-security-group --vpc-id vpc-0ca37d4650963adbb --group-name eks-control-plane --description "EKS Control Plane"
{
"GroupId": "sg-0fbac0a39bf64ba10"
}
- 将此标签添加到私有子网(以告知内部 ELB 这是一个私有子网):
$ aws ec2 create-tags --resources subnet-04b78ed9b5f96d76e --tags Key=kubernetes.io/role/internal-elb,Value=1
$ aws ec2 create-tags --resources subnet-08e16157c15cefcbc --tags Key=kubernetes.io/role/internal-elb,Value=1
启动 EKS 控制平面
EKS 控制平面是一个托管的 Kubernetes 主节点;你只需使用 AWS CLI 指定你的 IAM、子网和安全组。此示例还指定了 Kubernetes 版本为 1.10:
// Note: specify all 4 subnets
$ aws eks create-cluster --name chap10 --role-arn arn:aws:iam::xxxxxxxxxxxx:role/eksServiceRole --resources-vpc-config subnetIds=subnet-09f8f7f06c27cb0a0,subnet-04b78ed9b5f96d76e,subnet-026058e32f09c28af,subnet-08e16157c15cefcbc,securityGroupIds=sg-0fbac0a39bf64ba10 --kubernetes-version 1.10
完成此过程大约需要 10 分钟。你可以通过输入 aws eks describe-cluster --name chap10 检查状态。一旦控制平面状态为 ACTIVE,就可以开始设置 kubeconfig 来访问 Kubernetes API 服务器。
然而,AWS 将 Kubernetes API 访问控制与 AWS IAM 凭证集成。因此,你需要使用 aws-iam-authenticator (github.com/kubernetes-sigs/aws-iam-authenticator) 在运行 kubectl 命令时生成令牌。
这仅仅是下载一个 aws-iam-authenticator 二进制文件,并将其安装到默认命令搜索路径(例如 /usr/local/bin),然后通过以下命令验证 aws-iam-authenticator 是否有效:
$ aws-iam-authenticator token -i chap10
如果看到 authenticator 令牌,请运行 AWS CLI 来生成 kubeconfig,如下所示:
$ aws eks update-kubeconfig --name chap10
如果成功创建 kubeconfig,可以使用 kubectl 命令检查是否可以访问 Kubernetes 主节点,方法如下:
$ kubectl cluster-info
$ kubectl get svc
此时,你不会看到任何 Kubernetes 节点(kubectl get nodes 返回空结果)。所以,你需要进一步添加工作节点(Kubernetes 节点)。
你必须使用与创建控制平面相同的 IAM 用户,并通过 aws-iam-authenticator 访问 API 服务器。例如,如果你通过 AWS 根账户创建了 EKS 控制平面,然后再通过某个 IAM 用户访问 API 服务器,是无法正常工作的。使用 kubectl 时,你可能会看到类似 “必须登录到服务器(未经授权)” 的错误。
添加工作节点
如讨论所述,AWS 不允许 AWS CLI 设置 EKS 工作节点。相反,请使用 CloudFormation。这将为工作节点创建必要的 AWS 组件,如安全组、自动伸缩组和 IAM 实例角色。此外,当工作节点加入 Kubernetes 集群时,Kubernetes 主节点需要一个 IAM 实例角色。强烈推荐使用 CloudFormation 模板启动工作节点。
CloudFormation 执行步骤简单,并遵循 AWS EKS 文档,docs.aws.amazon.com/eks/latest/userguide/launch-workers.html。使用 S3 模板 URL,amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-12-10/amazon-eks-nodegroup.yaml,然后根据以下示例指定参数:
| 参数 | 值 |
|---|---|
Stack name |
chap10-worker |
ClusterName |
chap10 (必须与 EKS 控制平面名称匹配) |
ClusterControlPlaneSecurityGroup |
sg-0fbac0a39bf64ba10 (eks-control-plane) |
NodeGroupName |
chap10 EKS 工作节点(任何名称) |
NodeImageId |
ami-027792c3cc6de7b5b (版本 1.10.x) |
KeyName |
my-key |
VpcId |
vpc-0ca37d4650963adbb |
Subnets |
-
subnet-04b78ed9b5f96d76e (10.0.2.0/24) -
subnet-08e16157c15cefcbc (10.0.4.0/24)
注意:仅限私有子网 |
CloudFormation 执行大约需要五分钟,完成后,你需要从输出中获取 NodeInstanceRole 的值,如下所示:

最后,你可以通过添加 ConfigMap 将这些节点添加到 Kubernetes 集群中。你可以从 amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-12-10/aws-auth-cm.yaml 下载 ConfigMap 模板,然后填写实例角色 ARN,示例如下:
$ cat aws-auth-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
- rolearn: arn:aws:iam::xxxxxxxxxxxx:role/chap10-worker-NodeInstanceRole-8AFV8TB4IOXA
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
$ kubectl create -f aws-auth-cm.yaml
configmap "aws-auth" created
几分钟后,工作节点将注册到你的 Kubernetes 主节点,如下所示:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-2-218.ec2.internal Ready <none> 3m v1.10.3
ip-10-0-4-74.ec2.internal Ready <none> 3m v1.10.3 ...
现在你可以开始在 AWS 上使用自己的 Kubernetes 集群。将你的应用程序部署到集群中查看效果。请注意,基于之前的指示,我们将工作节点部署在私有子网中,因此如果你想部署一个面向互联网的 Kubernetes 服务,你需要使用 type:LoadBalancer。我们将在下一节中探讨这个问题。
EKS 上的云提供商
AWS EKS 将 Kubernetes 云提供商集成到 AWS 组件中,例如弹性负载均衡器和弹性块存储。本节将探讨 EKS 如何集成到 AWS 组件中。
存储类
自 2018 年 12 月起,如果你部署的是 Kubernetes 1.10 版本,EKS 默认不创建存储类。而在 1.11 版本或以上,EKS 会自动创建默认存储类。你可以使用以下命令检查存储类是否存在:
$ kubectl get sc
No resources found.
在这种情况下,你需要创建一个存储类以生成存储类。请注意,AWS EBS 和 EC2 是区域敏感的。因此,EBS 和 EC2 必须位于相同的可用区。因此,建议为每个可用区创建如下的 StorageClass:
//Storage Class for us-east-1a
$ cat storage-class-us-east-1a.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp2-us-east-1a
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4
zone: us-east-1a
// Storage Class for us-east-1b
$ cat storage-class-us-east-1b.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp2-us-east-1b
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4 zone: us-east-1b # only change this
$ kubectl create -f storage-class-us-east-1a.yaml
storageclass.storage.k8s.io "gp2-us-east-1a" created
$ kubectl create -f storage-class-us-east-1b.yaml
storageclass.storage.k8s.io "gp2-us-east-1b" created
// there are 2 StorageClass
$ kubectl get sc
NAME PROVISIONER AGE
gp2-us-east-1a kubernetes.io/aws-ebs 6s
gp2-us-east-1b kubernetes.io/aws-ebs 3s
PersistentVolumeClaim 然后可以指定 gp2-us-east-1a 或 gp2-us-east-1b 存储类来配置持久卷:
$ cat pvc-a.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-a
spec:
storageClassName: "gp2-us-east-1a"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
$ cat pvc-b.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-b
spec:
storageClassName: "gp2-us-east-1b" # only change this
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
$ kubectl create -f pvc-a.yaml
persistentvolumeclaim "pvc-a" created
$ kubectl create -f pvc-b.yaml persistentvolumeclaim "pvc-b" created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-20508a71-0187-11e9-bf51-02a2ca4dacd8 10Gi RWO Delete Bound default/pvc-a gp2-us-east-1a 4m pvc-2235f412-0187-11e9-bf51-02a2ca4dacd8 10Gi RWO Delete Bound 4m
// use AWS CLI to search EBS instances by following command
$ aws ec2 describe-volumes --query "Volumes[*].{Id:VolumeId,AZ:AvailabilityZone,Tags:Tags[?Key=='kubernetes.io/created-for/pv/name'].Value}" --filter "Name=tag-key,Values=kubernetes.io/cluster/chap10"
[
{
"Id": "vol-0fdec40626aac7cc4",
"AZ": "us-east-1a",
"Tags": [
"pvc-20508a71-0187-11e9-bf51-02a2ca4dacd8"
]
},
{
"Id": "vol-0d9ef53eedde70115",
"AZ": "us-east-1b",
"Tags": [
"pvc-2235f412-0187-11e9-bf51-02a2ca4dacd8"
]
}
]
请注意,工作节点有一个 failure-domain.beta.kubernetes.io/zone 标签,因此你可以指定 nodeSelector 将 Pod 部署到所需的可用区:
$ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
ip-10-0-2-218.ec2.internal Ready <none> 4h v1.10.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.small,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1b,kubernetes.io/hostname=ip-10-0-2-218.ec2.internal
ip-10-0-4-74.ec2.internal Ready <none> 4h v1.10.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.small,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1a,kubernetes.io/hostname=ip-10-0-4-74.ec2.internal
// nodeSelector specifies us-east-1a, also pvc-a
$ cat pod-us-east-1a.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
project: devops-with-kubernetes
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- mountPath: /var/log/nginx
name: nginx-log
volumes:
- name: nginx-log
persistentVolumeClaim:
claimName: "pvc-a"
nodeSelector:
failure-domain.beta.kubernetes.io/zone: us-east-1a
$ kubectl create -f pod-us-east-1a.yaml pod "nginx" created
// deploy to 10.0.4.0/24 subnet (us-east-1a)
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE nginx 1/1 Running 0 18s 10.0.4.33 ip-10-0-4-74.ec2.internal
// successfully to mount PVC-a
$ kubectl exec -it nginx /bin/bash
root@nginx:/# df -h /var/log/nginx
Filesystem Size Used Avail Use% Mounted on
/dev/xvdca 9.8G 37M 9.2G 1% /var/log/nginx
负载均衡器
EKS 还将 Kubernetes 服务集成到经典负载均衡器(也称为 ELB)中。当你通过指定 type:LoadBalancer 创建 Kubernetes 服务时,EKS 会创建经典 ELB 实例和安全组,然后自动将 ELB 与工作节点关联。
此外,你可以创建一个面向互联网的负载均衡器(位于公共子网)或内部负载均衡器(位于私有子网)。如果不需要为外部互联网提供流量服务,出于安全考虑,你应该使用内部负载均衡器。
内部负载均衡器
让我们为之前的 nginx Pod 创建一个内部负载均衡器。为了使用内部负载均衡器,你需要添加注释 (service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0),如下所示:
$ cat internal-elb.yaml apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
spec:
ports:
- protocol: TCP
port: 80
type: LoadBalancer
selector:
project: devops-with-kubernetes
app: nginx
$ kubectl create -f internal-elb.yaml
service "nginx" created
然后,EKS 云提供商将创建并配置一个经典 ELB,如下图所示:

由于这是一个内部 ELB,你无法从 AWS 网络外部访问 ELB,例如,从你的笔记本电脑访问。然而,它对于在 VPC 内部将应用程序暴露到 Kubernetes 集群外部是有用的。
AWS 按小时收费 ELB。如果你的 Kubernetes 服务只在 Kubernetes 集群中的 Pods 内部提供服务,你可以考虑使用 type:ClusterIP。
面向互联网的负载均衡器
创建面向互联网的负载均衡器与创建内部负载均衡器的步骤相同,但不需要注释 (service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0):
$ cat external-elb.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-external
spec:
ports:
- protocol: TCP
port: 80
type: LoadBalancer
selector:
project: devops-with-kubernetes
app: nginx
$ kubectl create -f external-elb.yaml
service "nginx-external" created
当你检查 AWS Web 控制台时,你可以看到 Scheme 是面向互联网的,如下所示:

你也可以从你的笔记本电脑访问 ELB:

如前面的截图所示,EKS 云提供商已经集成到 Kubernetes 服务中,后者启动了经典的 ELB。此功能非常强大,可以扩展分发到多个 Pods 的流量量。
EKS 已经开始支持使用 Network Load Balancer (NLB),这是 AWS 中新的 L4 负载均衡器版本。
为了使用 NLB,你需要一个额外的注解。该注解如下:
metadata:
name: nginx-external
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
更新 EKS 上的 Kubernetes 版本
当 Kubernetes 发布新版本时,EKS 会跟进并及时为用户提供最新版本的 Kubernetes。在之前的示例中,我们使用了 Kubernetes 版本 1.10。自 2018 年 12 月以来,EKS 还支持版本 1.11。让我们执行升级,看看 EKS 如何处理集群更新。
典型的升级步骤如下:
-
通过 AWS CLI 升级 Kubernetes 主节点
-
通过 CloudFormation 创建新的工作节点版本
-
向 Kubernetes 集群中添加新的工作节点(此时旧的和新的工作节点共存)
-
将 Pods 从旧的工作节点迁移到新的工作节点
大多数情况下,工作节点的升级需要一些手动步骤。我们将逐步探讨这些步骤。
升级 Kubernetes 主节点
升级 Kubernetes 主节点涉及简单的步骤,即指定你的 EKS 名称和所需的新版本,如下所示。根据情况,这大约需要 30 分钟来完成。同时,访问 Kubernetes API 服务器(通过 kubectl)可能会失败。尽管 Pods 和服务不会受到影响,但你需要留出足够的时间来执行此操作:
$ aws eks update-cluster-version --name chap10 --kubernetes-version 1.11
{
"update": {
"status": "InProgress",
"errors": [],
"params": [
{
"type": "Version",
"value": "1.11"
},
{
"type": "PlatformVersion",
"value": "eks.1"
}
],
"type": "VersionUpdate",
"id": "09688495-4d12-4aa5-a2e8-dfafec1cee17",
"createdAt": 1545007011.285
}
}
如你在前面的代码中看到的,aws eks update-cluster-version 命令返回更新 id。你可以使用这个 ID 来检查升级状态,如下所示:
$ aws eks describe-update --name chap10 --update-id 09688495-4d12-4aa5-a2e8-dfafec1cee17
{
"update": {
"status": "InProgress",
"errors": [],
...
...
一旦状态从 InProgress 更改为 Successful,你就可以看到更新后的 API 服务器版本,如下所示:
$ kubectl version --short
Client Version: v1.10.7
Server Version: v1.11.5-eks-6bad6d
基于旧版和新版 Kubernetes 之间的差异,我们可能需要遵循一些额外的迁移步骤。例如,将 DNS 服务从 kube-dns 更改为 core-dns。如果 AWS EKS 提供了相关说明,你需要按照这些步骤操作。
升级工作节点
升级 Kubernetes 主节点后,你可以开始升级工作节点。然而,AWS CLI 目前还不支持,因此你需要一些手动步骤来升级工作节点:
-
使用与之前相同的步骤通过 CloudFormation 创建新的工作节点。但是,在这里,你需要指定新的 AMI 版本,例如
ami-0b4eb1d8782fc3aea。你可以通过docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html从 AWS 文档中获取 AMI ID 列表。 -
更新旧节点和新节点的安全组,以允许它们之间的网络流量。您可以通过 AWS CLI 或 AWS Web 控制台找到安全组 ID。有关更多详细信息,请访问 AWS 文档:
docs.aws.amazon.com/eks/latest/userguide/migrate-stack.html。 -
更新
ConfigMap以添加(而不是替换)新工作节点实例 ARNs,如以下示例所示:
$ vi aws-auth-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
#
# new version of Worker Nodes
#
- rolearn: arn:aws:iam::xxxxxxxxxxxx:role/chap10-v11-NodeInstanceRole-10YYF3AILTJOS
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
#
# old version of Worker Nodes
#
- rolearn: arn:aws:iam::xxxxxxxxxxxx:role/chap10-worker-NodeInstanceRole-8AFV8TB4IOXA
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
//apply command to update ConfigMap
$ kubectl apply -f aws-auth-cm.yaml
// you can see both 1.10 and 1.11 nodes
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-2-122.ec2.internal Ready <none> 1m v1.11.5
ip-10-0-2-218.ec2.internal Ready <none> 6h v1.10.3
...
- 标记并排空旧节点,将 pod 移动到新节点:
// prevent to assign pod to older Nodes
$ kubectl taint nodes ip-10-0-2-218.ec2.internal key=value:NoSchedule
$ kubectl taint nodes ip-10-0-4-74.ec2.internal key=value:NoSchedule
// move Pod from older to newer Nodes
$ kubectl drain ip-10-0-2-218.ec2.internal --ignore-daemonsets --delete-local-data
$ kubectl drain ip-10-0-4-74.ec2.internal --ignore-daemonsets --delete-local-data
// Old worker node became SchedulingDisabled
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-2-122.ec2.internal Ready <none> 7m v1.11.5 ip-10-0-2-218.ec2.internal Ready,SchedulingDisabled <none> 7h v1.10.3
ip-10-0-4-74.ec2.internal Ready,SchedulingDisabled <none> 7h v1.10.3
- 从集群中移除旧节点,并再次更新
ConfigMap:
$ kubectl delete node ip-10-0-2-218.ec2.internal
node "ip-10-0-2-218.ec2.internal" deleted
$ kubectl delete node ip-10-0-4-74.ec2.internal
node "ip-10-0-4-74.ec2.internal" deleted
$ kubectl edit configmap aws-auth -n kube-system
configmap "aws-auth" edited
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-2-122.ec2.internal Ready <none> 15m v1.11.5
升级 Kubernetes 版本是 Kubernetes 管理员的一项令人头疼的话题。这是由于 Kubernetes 的发布周期(通常每三个月一次)以及需要进行足够的兼容性测试。
EKS 升级过程需要 AWS 的知识和理解。这包含许多步骤,并涉及一定的技术难度,但不应太难。由于 EKS 仍然是 AWS 中较新的服务,它将在未来继续改进,并为用户提供更简便的选项。
总结
本章讨论了公共云。AWS 是最受欢迎的公共云服务,它通过 API 提供了程序化控制 AWS 基础设施的能力。
此外,AWS EKS 使在 AWS 上部署 Kubernetes 变得简单。此外,控制平面采用高可用性设计管理主节点和 etcd,从而减轻了巨大的管理工作量。
另一方面,您需要了解 AWS 的基础知识,例如 pod(EC2)和持久卷(EBS)之间的可用区意识。此外,您还需要具备中级 AWS 知识,如 IAM 凭证,以便访问 API 服务器,并使用工作节点实例角色 ARN 注册集群。
此外,自 2018 年 12 月起,使用 ALB 作为入口控制器是可行的(aws.amazon.com/blogs/opensource/kubernetes-ingress-aws-alb-ingress-controller/),但这也需要额外的配置工作。
尽管 AWS 不断改进功能,开源工具如 eksctl 表明 EKS 仍然需要更多改进,以便更容易地使用 Kubernetes。
在第十一章《Kubernetes 在 GCP 上》中,我们将介绍 Google Cloud Platform 和 Kubernetes 引擎,这是一个为云上托管 Kubernetes 服务而开创的先驱。它比 AWS EKS 更成熟。
第十一章:GCP 上的 Kubernetes
Google Cloud Platform(GCP)是由 Google 提供的公共云服务,越来越受欢迎。GCP 具有与 AWS 类似的概念,如 VPC、计算引擎、持久磁盘、负载均衡和多个托管服务。在本章中,你将了解 GCP,并通过以下主题学习如何在 GCP 上设置 Kubernetes:
-
理解 GCP
-
使用和理解 GCP 组件
-
使用Google Kubernetes Engine(GKE),托管的 Kubernetes 服务
GCP 介绍
GCP 于 2011 年正式推出。与 AWS 不同,GCP 最初提供的是PaaS(平台即服务)。因此,你可以直接部署应用程序,而无需启动虚拟机。之后,GCP 添加了更多服务和功能。
对 Kubernetes 用户来说,最重要的服务是 GKE,它是一个托管的 Kubernetes 服务。因此,你可以免去 Kubernetes 安装、升级和管理的麻烦。它采用按需付费的方式使用 Kubernetes 集群。GKE 也是一个非常活跃的服务,不断提供 Kubernetes 的新版本,并不断推出新的功能和管理工具。
让我们看看 GCP 提供了哪些基础设施和服务,然后再深入探讨 GKE。
GCP 组件
GCP 提供了 Web 控制台和命令行界面(CLI)。这两者都使得控制 GCP 基础设施变得简单直观,但使用时需要 Google 帐号(如 Gmail)。一旦你拥有 Google 帐号,可以前往 GCP 注册页面(cloud.google.com/free/)来设置你的 GCP 帐号。
如果你想通过 CLI 控制 GCP 组件,你需要安装 Cloud SDK(cloud.google.com/sdk/gcloud/),它类似于 AWS CLI,你可以用它列出、创建、更新和删除 GCP 资源。安装 Cloud SDK 后,你需要使用以下命令进行配置,将其与 GCP 帐号关联:
$ gcloud init
VPC
GCP 中的 VPC 与 AWS 的策略有很大不同。首先,你不需要为 VPC 设置 CIDR 前缀。换句话说,你不能为 VPC 设置 CIDR。相反,你只需将一个或多个子网添加到 VPC 中。由于你必须为子网设置特定的 CIDR 块,因此 GCP 的 VPC 被视为子网的逻辑组合,VPC 内的子网可以相互通信。
请注意,GCP 的 VPC 有两种子网模式,分别是 auto 或 custom。如果选择 auto,它将在每个区域创建一些具有预定义 CIDR 块的子网。例如,输入以下命令:
$ gcloud compute networks create my-auto-network --subnet-mode auto
这将创建 18 个子网,如下图所示(因为截至 2018 年 12 月,GCP 有 18 个区域):

自动模式 VPC 可能是一个不错的起点。然而,在自动模式下,你无法指定 CIDR 前缀,而且来自所有区域的 18 个子网可能不适合你的使用场景。例如,连接到本地数据中心通过 VPN。另一个例子是只在特定区域创建子网。
在这种情况下,选择自定义模式 VPC,然后你可以手动创建具有所需 CIDR 前缀的子网。输入以下命令创建一个自定义模式 VPC:
//create custom mode VPC which is named my-custom-network
$ gcloud compute networks create my-custom-network --subnet-mode custom
因为自定义模式 VPC 不会创建任何子网,如下图所示,接下来我们将子网添加到这个自定义模式 VPC:

子网
在 GCP 中,子网总是跨越一个区域内的多个可用区(availability zones)。换句话说,你不能像在 AWS 中那样仅在一个可用区内创建子网。创建子网时,你必须指定整个区域。
此外,与 AWS 不同,GCP 中没有明显的公有和私有子网的概念(在 AWS 中,公有子网的默认路由是 IGW;而私有子网的默认路由是 NAT 网关)。这是因为 GCP 中的所有子网都有通向互联网网关的路由。
GCP 不使用子网级别的访问控制,而是使用主机(实例)级别的访问控制,通过网络标签确保网络安全。接下来的部分会详细描述这一点。
这可能让网络管理员感到紧张;然而,GCP 的最佳实践提供了一个更加简化且可扩展的 VPC 管理方式,因为你可以随时添加子网来扩展整个网络块。
从技术上讲,你可以启动一个虚拟机实例并将其设置为 NAT 网关或 HTTP 代理,然后为私有子网创建一个自定义优先级路由,指向 NAT/代理实例,从而实现类似 AWS 的私有子网。
有关详细信息,请参考以下在线文档:cloud.google.com/compute/docs/vpc/special-configurations。
还有一件事:GCP VPC 的一个有趣且独特的概念是,你可以将不同的 CIDR 前缀网络块添加到同一个 VPC。例如,如果你有一个自定义模式 VPC,那么可以添加以下三个子网:
-
subnet-a(10.0.1.0/24) 来自us-west1 -
subnet-b(172.16.1.0/24) 来自us-east1 -
subnet-c(192.168.1.0/24) 来自asia-northeast1
以下命令将从三个不同的区域创建三个子网,每个子网具有不同的 CIDR 前缀:
$ gcloud compute networks subnets create subnet-a --network=my-custom-network --range=10.0.1.0/24 --region=us-west1
$ gcloud compute networks subnets create subnet-b --network=my-custom-network --range=172.16.1.0/24 --region=us-east1
$ gcloud compute networks subnets create subnet-c --network=my-custom-network --range=192.168.1.0/24 --region=asia-northeast1
结果将是如下所示的 web 控制台。如果你熟悉 AWS VPC,你可能不敢相信这些 CIDR 前缀组合可以在一个 VPC 内使用!这意味着,每当你需要扩展网络时,你可以将另一个 CIDR 前缀分配给这个 VPC:

防火墙规则
如前所述,GCP 防火墙规则对实现网络安全至关重要。然而,GCP 防火墙比 AWS 安全组(SG)更简单、更灵活。例如,在 AWS 中,启动 EC2 实例时,您必须分配至少一个与 EC2 和 SG 紧密耦合的 SG。而在 GCP 中,您不能直接分配任何防火墙规则。相反,防火墙规则和虚拟机实例是通过网络标签松散耦合的。因此,防火墙规则与虚拟机实例之间没有直接关联。
以下图表展示了 AWS 安全组与 GCP 防火墙规则的比较。EC2 需要一个安全组,而 GCP 虚拟机实例只需设置一个标签,无论相应的防火墙是否具有相同的标签:

例如,为公共主机(使用public网络标签)和私有主机(使用private网络标签)创建防火墙规则,如以下命令所示:
//create ssh access for public host
$ gcloud compute firewall-rules create public-ssh --network=my-custom-network --allow="tcp:22" --source-ranges="0.0.0.0/0" --target-tags="public" //create http access (80/tcp for public host)
$ gcloud compute firewall-rules create public-http --network=my-custom-network --allow="tcp:80" --source-ranges="0.0.0.0/0" --target-tags="public" //create ssh access for private host (allow from host which has "public" tag)
$ gcloud compute firewall-rules create private-ssh --network=my-custom-network --allow="tcp:22" --source-tags="public" --target-tags="private"
//create icmp access for internal each other (allow from host which has either "public" or "private")
$ gcloud compute firewall-rules create internal-icmp --network=my-custom-network --allow="icmp" --source-tags="public,private"
这将创建如以下截图所示的四个防火墙规则。让我们创建虚拟机实例,使用public或private网络标签,看看它是如何工作的:

虚拟机实例
在 GCP 中,虚拟机实例与 AWS EC2 相似。您可以从多种机器(实例)类型中选择,这些类型具有不同的硬件配置;您还可以选择基于 Linux 或 Windows 的操作系统,或者您自定义的操作系统。
如在讨论防火墙规则时提到的,您可以指定任意数量的网络标签。标签不一定需要事先创建。这意味着您可以先使用网络标签启动虚拟机实例,即使防火墙规则尚未创建。这样仍然有效,但此时不会应用防火墙规则。然后,您可以创建一个具有网络标签的防火墙规则。最终,防火墙规则将应用于虚拟机实例。这就是为什么虚拟机实例和防火墙规则是松散耦合的,提供了更大的灵活性:

在启动虚拟机实例之前,您需要首先创建一个ssh公钥,方式与 AWS EC2 相同。最简单的方法是运行以下命令来创建并注册一个新的密钥:
//this command create new ssh key pair
$ gcloud compute config-ssh //key will be stored as ~/.ssh/google_compute_engine(.pub)
$ cd ~/.ssh
$ ls -l google_compute_engine*
-rw------- 1 saito admin 1766 Aug 23 22:58 google_compute_engine
-rw-r--r-- 1 saito admin 417 Aug 23 22:58 google_compute_engine.pub
现在,让我们开始在 GCP 上启动虚拟机实例。
在subnet-a和subnet-b上分别部署两个实例作为公共实例(使用public网络标签),然后在subnet-a上启动另一个实例作为私有实例(使用private网络标签):
//create public instance ("public" tag) on subnet-a
$ gcloud compute instances create public-on-subnet-a --machine-type=f1-micro --network=my-custom-network --subnet=subnet-a --zone=us-west1-a --tags=public
//create public instance ("public" tag) on subnet-b
$ gcloud compute instances create public-on-subnet-b --machine-type=f1-micro --network=my-custom-network --subnet=subnet-b --zone=us-east1-c --tags=public
//create private instance ("private" tag) on subnet-a with larger size (g1-small)
$ gcloud compute instances create private-on-subnet-a --machine-type=g1-small --network=my-custom-network --subnet=subnet-a --zone=us-west1-a --tags=private
//Overall, there are 3 VM instances has been created in this example as below
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
public-on-subnet-b us-east1-c f1-micro 172.16.1.2 35.196.228.40 RUNNING
private-on-subnet-a us-west1-a g1-small 10.0.1.2 104.199.121.234 RUNNING
public-on-subnet-a us-west1-a f1-micro 10.0.1.3 35.199.171.31 RUNNING

您可以登录到这些机器,检查防火墙规则是否按预期工作。首先,您需要将ssh密钥添加到您机器上的ssh-agent中:
$ ssh-add ~/.ssh/google_compute_engine
Enter passphrase for /Users/saito/.ssh/google_compute_engine:
Identity added: /Users/saito/.ssh/google_compute_engine
(/Users/saito/.ssh/google_compute_engine)
然后检查是否有 ICMP 防火墙规则可以拒绝来自外部的流量,因为 ICMP 仅允许公共或私有标记主机,因此你机器上的 ping 数据包不会到达公共实例;如以下截图所示:

另一方面,公共 host 允许从你的机器进行 ssh 连接,因为 public-ssh 规则允许任何来源(0.0.0.0/0):

当然,这台主机可以通过私有 IP 地址 ping 和 ssh 连接到 subnet-a 上的私有主机(10.0.1.2),因为有 internal-icmp 和 private-ssh 规则。
让我们 ssh 连接到一台私有主机,然后安装 tomcat8 和 tomcat8-examples 包(这将为 Tomcat 安装 /examples/ 应用程序):

记住,subnet-a 是 10.0.1.0/24 CIDR 前缀,而 subnet-b 是 172.16.1.0/24 CIDR 前缀。然而,在同一个 VPC 中,它们是互通的。这是使用 GCP 的一个巨大优势,你可以根据需要扩展网络地址块。
现在在公共主机(public-on-subnet-a 和 public-on-subnet-b)上安装 nginx:
//logout from VM instance, then back to your machine
$ exit
//install nginx from your machine via ssh
$ ssh 35.196.228.40 "sudo apt-get -y install nginx"
$ ssh 35.199.171.31 "sudo apt-get -y install nginx" //check whether firewall rule (public-http) work or not
$ curl -I http://35.196.228.40/
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Sun, 27 Aug 2017 07:07:01 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Fri, 25 Aug 2017 05:48:28 GMT
Connection: keep-alive
ETag: "599fba2c-264"
Accept-Ranges: bytes
然而,目前即使私有主机拥有公共 IP 地址,你也无法访问 Tomcat。这是因为私有主机尚未设置允许 8080/tcp 的防火墙规则:
$ curl http://104.199.121.234:8080/examples/
curl: (7) Failed to connect to 104.199.121.234 port 8080: Operation timed out
与其仅仅为 Tomcat 创建防火墙规则,我们将在下一节中设置一个负载均衡器,配置 nginx 和 Tomcat。
负载均衡
GCP 提供了几种类型的负载均衡器,如下所示:
-
层 4 TCP 负载均衡器
-
层 4 UDP 负载均衡器
-
层 7 HTTP(S) 负载均衡器
层 4 负载均衡器(TCP 和 UDP)类似于 AWS Classic ELB。另一方面,层 7 HTTP(S) 负载均衡器具有基于内容(上下文)的路由。例如,URL/图像将转发到 instance-a;其他所有内容将转发到 instance-b。因此,它更像是一个应用层负载均衡器。
AWS 还提供 应用负载均衡器(ALB 或 ELBv2),它与 GCP 的层 7 HTTP(S) 负载均衡器非常相似。有关详细信息,请访问 aws.amazon.com/blogs/aws/new-aws-application-load-balancer/。
为了设置负载均衡器,与 AWS ELB 不同,你需要遵循几个步骤来预先配置一些项目:
| 配置项 | 用途 |
|---|---|
| 实例组 | 确定一组虚拟机实例或虚拟机模板(操作系统镜像)。 |
| 健康检查 | 设置健康阈值(间隔、超时等)以确定实例组的健康状态。 |
| 后端服务 | 设置负载阈值(最大 CPU 或每秒请求数)和会话亲和性(粘性会话),并将其与健康检查关联。 |
url-maps(负载均衡器) |
这是一个实际的占位符,代表一个 L7 负载均衡器,它将后台服务与 HTTP(S)代理相关联。 |
| 目标 HTTP(S)代理 | 这是一个连接器,它使前端转发规则与负载均衡器之间建立关系。 |
| 前端转发规则 | 将目标 HTTP 代理与 IP 地址(临时或静态)和端口号关联。 |
| 外部 IP(静态) | (可选)为负载均衡器分配一个静态外部 IP 地址。 |
下图展示了构建 L7 负载均衡器时,所有前面提到的组件的关联:

让我们首先设置一个实例组。在本示例中,需要创建三个实例组:一个用于私有托管的 Tomcat 实例(8080/tcp),另外两个实例组用于每个区域的公共 HTTP 实例。
为了做到这一点,执行以下命令,将它们分组在一起:
//create instance groups for HTTP instances and tomcat instance
$ gcloud compute instance-groups unmanaged create http-ig-us-west --zone us-west1-a
$ gcloud compute instance-groups unmanaged create http-ig-us-east --zone us-east1-c
$ gcloud compute instance-groups unmanaged create tomcat-ig-us-west --zone us-west1-a
//because tomcat uses 8080/tcp, create a new named port as tomcat:8080
$ gcloud compute instance-groups unmanaged set-named-ports tomcat-ig-us-west --zone us-west1-a --named-ports tomcat:8080
//register an existing VM instance to correspond instance group
$ gcloud compute instance-groups unmanaged add-instances http-ig-us-west --instances public-on-subnet-a --zone us-west1-a
$ gcloud compute instance-groups unmanaged add-instances http-ig-us-east --instances public-on-subnet-b --zone us-east1-c
$ gcloud compute instance-groups unmanaged add-instances tomcat-ig-us-west --instances private-on-subnet-a --zone us-west1-a
健康检查
让我们通过执行以下命令来设置标准配置:
//create health check for http (80/tcp) for "/"
$ gcloud compute health-checks create http my-http-health-check --check-interval 5 --healthy-threshold 2 --unhealthy-threshold 3 --timeout 5 --port 80 --request-path /
//create health check for Tomcat (8080/tcp) for "/examples/"
$ gcloud compute health-checks create http my-tomcat-health-check --check-interval 5 --healthy-threshold 2 --unhealthy-threshold 3 --timeout 5 --port 8080 --request-path /examples/
后台服务
首先,我们需要创建一个指定了健康检查的后台服务。然后,我们必须为每个实例组添加阈值:CPU 使用率最高为 80%,HTTP 和 Tomcat 的最大容量都设置为 100%:
//create backend service for http (default) and named port tomcat (8080/tcp)
$ gcloud compute backend-services create my-http-backend-service --health-checks my-http-health-check --protocol HTTP --global
$ gcloud compute backend-services create my-tomcat-backend-service --health-checks my-tomcat-health-check --protocol HTTP --port-name tomcat --global
//add http instance groups (both us-west1 and us-east1) to http backend service
$ gcloud compute backend-services add-backend my-http-backend-service --instance-group http-ig-us-west --instance-group-zone us-west1-a --balancing-mode UTILIZATION --max-utilization 0.8 --capacity-scaler 1 --global
$ gcloud compute backend-services add-backend my-http-backend-service --instance-group http-ig-us-east --instance-group-zone us-east1-c --balancing-mode UTILIZATION --max-utilization 0.8 --capacity-scaler 1 --global
//also add tomcat instance group to tomcat backend service
$ gcloud compute backend-services add-backend my-tomcat-backend-service --instance-group tomcat-ig-us-west --instance-group-zone us-west1-a --balancing-mode UTILIZATION --max-utilization 0.8 --capacity-scaler 1 --global
创建负载均衡器
负载均衡器需要绑定my-http-backend-service和my-tomcat-backend-service。在此场景下,只有/examples和/examples/*的流量会转发到my-tomcat-backend-service。除此之外,所有 URI 的流量都会转发到my-http-backend-service:
//create load balancer(url-map) to associate my-http-backend-service as default
$ gcloud compute url-maps create my-loadbalancer --default-service my-http-backend-service
//add /examples and /examples/* mapping to my-tomcat-backend-service
$ gcloud compute url-maps add-path-matcher my-loadbalancer --default-service my-http-backend-service --path-matcher-name tomcat-map --path-rules /examples=my-tomcat-backend-service,/examples/*=my-tomcat-backend-service //create target-http-proxy that associate to load balancer(url-map)
$ gcloud compute target-http-proxies create my-target-http-proxy --url-map=my-loadbalancer
//allocate static global ip address and check assigned address
$ gcloud compute addresses create my-loadbalancer-ip --global
$ gcloud compute addresses describe my-loadbalancer-ip --global
address: 35.186.192.6
creationTimestamp: '2018-12-08T13:40:16.661-08:00' ...
...
//create forwarding rule that associate static IP to target-http-proxy
$ gcloud compute forwarding-rules create my-frontend-rule --global --target-http-proxy my-target-http-proxy --address 35.186.192.6 --ports 80
如果你没有指定--address选项,则会创建并分配一个临时的外部 IP 地址。
最后,负载均衡器已经创建。然而,还有一个缺失的配置。私有主机没有任何防火墙规则来允许 Tomcat 流量(8080/tcp)。因此,当你查看负载均衡器状态时,my-tomcat-backend-service的健康状态会保持为不正常(0):

在这种情况下,你需要添加一个新的防火墙规则,允许从负载均衡器到私有子网的连接(为此使用private网络标签)。根据 GCP 文档(cloud.google.com/compute/docs/load-balancing/health-checks#https_ssl_proxy_tcp_proxy_and_internal_load_balancing),健康检查的心跳将来自地址范围35.191.0.0/16到130.211.0.0/22:
//add one more Firewall Rule that allow Load Balancer to Tomcat (8080/tcp)
$ gcloud compute firewall-rules create private-tomcat --network=my-custom-network --source-ranges 35.191.0.0/16,130.211.0.0/22 --target-tags private --allow tcp:8080
几分钟后,my-tomcat-backend-service的健康状态将达到(1);现在你可以通过网页浏览器访问负载均衡器。当访问/时,应该会路由到my-http-backend-service,该服务在公共主机上运行nginx应用:

另一方面,如果您使用相同的负载均衡器 IP 地址访问/examples/ URL,它将路由到my-tomcat-backend-service,这是一个托管在私有主机上的 Tomcat 应用,如下图所示:

总的来说,设置负载均衡器需要执行一些步骤,但将不同的 HTTP 应用程序集成到一个负载均衡器中,对于高效地交付服务并以最少的资源使用是非常有用的。
持久磁盘
GCE 还提供了一种存储服务,叫做持久磁盘(PD),它与 AWS EBS 非常相似。您可以在每个可用区分配所需的大小和类型(标准或 SSD),并随时附加/分离 VM 实例。
让我们创建一个 PD,然后将其附加到 VM 实例。请注意,当将 PD 附加到 VM 实例时,二者必须位于相同的可用区。这一限制与 AWS EBS 相同。因此,在创建 PD 之前,请再次检查 VM 实例的位置:
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
public-on-subnet-b us-east1-c f1-micro 172.16.1.2 35.196.228.40 RUNNING
private-on-subnet-a us-west1-a g1-small 10.0.1.2 104.199.121.234 RUNNING
public-on-subnet-a us-west1-a f1-micro 10.0.1.3 35.199.171.31 RUNNING
让我们选择us-west1-a,然后将其附加到public-on-subnet-a:
//create 20GB PD on us-west1-a with standard type
$ gcloud compute disks create my-disk-us-west1-a --zone us-west1-a --type pd-standard --size 20 //after a few seconds, check status, you can see existing boot disks as well
$ gcloud compute disks list
NAME ZONE SIZE_GB TYPE STATUS
public-on-subnet-b us-east1-c 10 pd-standard READY
my-disk-us-west1-a us-west1-a 20 pd-standard READY
private-on-subnet-a us-west1-a 10 pd-standard READY
public-on-subnet-a us-west1-a 10 pd-standard READY //attach PD(my-disk-us-west1-a) to the VM instance(public-on-subnet-a)
$ gcloud compute instances attach-disk public-on-subnet-a --disk my-disk-us-west1-a --zone us-west1-a //login to public-on-subnet-a to see the status
$ ssh 35.199.171.31
Linux public-on-subnet-a 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u3 (2017-08-06) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Aug 25 03:53:24 2017 from 107.196.102.199
saito@public-on-subnet-a:~$ sudo su
root@public-on-subnet-a:/home/saito# dmesg | tail
[ 7377.421190] systemd[1]: apt-daily-upgrade.timer: Adding 25min 4.773609s random time.
[ 7379.202172] systemd[1]: apt-daily-upgrade.timer: Adding 6min 37.770637s random time.
[243070.866384] scsi 0:0:2:0: Direct-Access Google PersistentDisk 1 PQ: 0 ANSI: 6
[243070.875665] sd 0:0:2:0: [sdb] 41943040 512-byte logical blocks: (21.5 GB/20.0 GiB)
[243070.883461] sd 0:0:2:0: [sdb] 4096-byte physical blocks
[243070.889914] sd 0:0:2:0: Attached scsi generic sg1 type 0
[243070.900603] sd 0:0:2:0: [sdb] Write Protect is off
[243070.905834] sd 0:0:2:0: [sdb] Mode Sense: 1f 00 00 08
[243070.905938] sd 0:0:2:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[243070.925713] sd 0:0:2:0: [sdb] Attached SCSI disk
您可能会看到 PD 已附加在/dev/sdb。类似于 AWS EBS,您需要格式化此磁盘。由于这是 Linux 操作系统的操作,步骤与第十章,Kubernetes on AWS中描述的完全相同。
Google Kubernetes Engine(GKE)
总的来说,前面介绍了一些 GCP 的组件。现在,您可以开始使用这些组件在 GCP 的 VM 实例上设置 Kubernetes。您甚至可以使用开源的 Kubernetes 配置工具,如kops和kubespray。
Google Cloud 提供了 GKE On-Prem(cloud.google.com/gke-on-prem/),允许用户在自己的数据中心资源上设置 GKE。从 2019 年 1 月起,这还是一个 alpha 版本,尚未对所有人开放。
然而,GCP 有一个托管的 Kubernetes 服务,叫做 GKE。在后台,它使用了 GCP 的一些组件,如 VPC、VM 实例、PD、防火墙规则和负载均衡器。
当然,像往常一样,您可以使用kubectl命令来控制 GKE 上的 Kubernetes 集群,它包含在 Cloud SDK 中。如果您还没有在计算机上安装kubectl命令,可以通过 Cloud SDK 输入以下命令进行安装:
//install kubectl command
$ gcloud components install kubectl
在 GKE 上设置您的第一个 Kubernetes 集群
您可以使用gcloud命令在 GKE 上设置 Kubernetes 集群。这需要指定多个参数以确定一些配置。一个重要的参数是网络。在这里,您必须指定要部署的 VPC 和子网。尽管 GKE 支持多个可用区进行部署,但您需要为 Kubernetes 主节点指定至少一个可用区。这次,使用以下参数启动 GKE 集群:
| 参数 | 描述 | 值 |
|---|---|---|
--machine-type |
Kubernetes 节点的 VM 实例类型 | f1-micro |
--num-nodes |
Kubernetes 节点的初始数量 | 3 |
--network |
指定 GCP VPC | my-custom-network |
--subnetwork |
如果 VPC 为自定义模式,则指定 GCP 子网 | subnet-c |
--zone |
指定一个单一的可用区 | asia-northeast1-a |
--tags |
将分配给 Kubernetes 节点的网络标签 | private |
在这种情况下,你需要输入以下命令来在 GCP 上启动 Kubernetes 集群。由于在后台会启动多个 VM 实例并设置 Kubernetes 主节点和节点,因此可能需要几分钟才能完成。请注意,Kubernetes 主节点和 etcd 将由 GCP 完全管理。这意味着主节点和 etcd 不会消耗你的 VM 实例:
$ gcloud container clusters create my-k8s-cluster --machine-type f1-micro --num-nodes 3 --network my-custom-network --subnetwork subnet-c --zone asia-northeast1-a --tags private //after a few minutes, check node status
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-default-pool-bcae4a66-mlhw Ready <none> 2m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-tn74 Ready <none> 2m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-w5l6 Ready <none> 2m v1.10.9-gke.5
注意,我们指定了--tags private选项,以便 Kubernetes 节点 VM 实例拥有private网络标签。因此,它的行为与其他具有private标签的普通 VM 实例相同。结果,你无法从公共互联网进行 SSH 连接,也无法从互联网进行 HTTP 访问。不过,你可以从另一个具有public网络标签的 VM 实例进行 ping 和 SSH 连接。
节点池
在启动 Kubernetes 集群时,你可以使用--num-nodes选项指定节点数量。GKE 将 Kubernetes 节点管理为一个节点池。这意味着你可以管理附加到 Kubernetes 集群的一个或多个节点池。
如果需要添加或删除节点怎么办?GKE 允许你通过执行以下命令将 Kubernetes 节点从 3 个增加到 5 个:
//run resize command to change number of nodes to 5
$ gcloud container clusters resize my-k8s-cluster --size 5 --zone asia-northeast1-a
//after a few minutes later, you may see additional nodes
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-default-pool-bcae4a66-j8zz Ready <none> 32s v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-jnnw Ready <none> 32s v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-mlhw Ready <none> 4m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-tn74 Ready <none> 4m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-w5l6 Ready <none> 4m v1.10.9-gke.5
增加节点数量有助于你需要扩展节点容量时。但是,在这种情况下,它仍然使用最小的实例类型(f1-micro,仅有 0.6 GB 内存)。如果单个容器需要超过 0.6 GB 的内存,这可能并没有帮助。在这种情况下,你需要进行扩展,即需要添加更大的 VM 实例类型。
在这种情况下,你必须向集群添加另一个节点池。这是因为,在同一个节点池内,所有的 VM 实例配置都是相同的。因此,你无法在同一个节点池中更改实例类型。
向集群添加一个新的节点池,包含两套新的g1-small(1.7 GB 内存)VM 实例类型。然后,你可以使用不同硬件配置扩展 Kubernetes 节点。
默认情况下,在一个区域内有一些配额会限制 VM 实例的数量(例如,在us-west1区域最多允许 8 个 CPU 核心)。如果你希望增加这个配额,你必须将账户升级为付费账户,然后请求 GCP 更改配额。更多详情,请阅读在线文档:cloud.google.com/compute/quotas 和 cloud.google.com/free/docs/frequently-asked-questions#how-to-upgrade。
运行以下命令以添加一个额外的节点池,该池包含两个g1-small实例:
//create and add node pool which is named "large-mem-pool"
$ gcloud container node-pools create large-mem-pool --cluster my-k8s-cluster --machine-type g1-small --num-nodes 2 --tags private --zone asia-northeast1-a //after a few minustes, large-mem-pool instances has been added
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-default-pool-bcae4a66-j8zz Ready <none> 5m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-jnnw Ready <none> 5m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-mlhw Ready <none> 9m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-tn74 Ready <none> 9m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-bcae4a66-w5l6 Ready <none> 9m v1.10.9-gke.5
gke-my-k8s-cluster-large-mem-pool-66e3a44a-jtdn Ready <none> 46s v1.10.9-gke.5
gke-my-k8s-cluster-large-mem-pool-66e3a44a-qpbr Ready <none> 44s v1.10.9-gke.5
现在你的集群总共有七个 CPU 核心和 6.4 GB 的内存,容量更大。然而,由于硬件类型较大,Kubernetes 调度器可能会优先将 Pod 分配到 large-mem-pool,因为它有足够的内存容量。
然而,你可能希望保留 large-mem-pool 节点,以防某个大型应用需要较大的内存(例如,一个 Java 应用)。因此,你可能想区分 default-pool 和 large-mem-pool。
在这种情况下,Kubernetes 标签 beta.kubernetes.io/instance-type 有助于区分节点的实例类型。因此,可以使用 nodeSelector 来指定 Pod 的目标节点。例如,以下的 nodeSelector 参数将强制使用 f1-micro 节点来运行 nginx 应用:
//nodeSelector specifies f1-micro
$ cat nginx-pod-selector.yml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeSelector:
beta.kubernetes.io/instance-type: f1-micro
//deploy pod
$ kubectl create -f nginx-pod-selector.yml
pod "nginx" created
//it uses default pool
NAME READY STATUS RESTARTS AGE IP NODE
nginx 0/1 ContainerCreating 0 10s <none> gke-my-k8s-cluster-default-pool-bcae4a66-jnnw
如果你想指定一个特定的标签,而不是 beta.kubernetes.io/instance-type,可以使用 --node-labels 选项来创建节点池。这样可以将你想要的标签分配给节点池。更多细节,请阅读以下在线文档:cloud.google.com/sdk/gcloud/reference/container/node-pools/create。
当然,如果你不再需要某个节点池,可以随时将其删除。为此,可以运行以下命令删除 default-pool(f1-micro x 5 实例)。如果 default-pool 上有一些 Pod 在运行,执行此操作时将自动进行 Pod 迁移(终止 default-pool 上的 Pod 并将其重新启动在 large-mem-pool 上):
//list Node Pool
$ gcloud container node-pools list --cluster my-k8s-cluster --zone asia-northeast1-a NAME MACHINE_TYPE DISK_SIZE_GB NODE_VERSION
default-pool f1-micro 100 1.10.9-gke.5
large-mem-pool g1-small 100 1.10.9-gke.5 //delete default-pool
$ gcloud container node-pools delete default-pool --cluster my-k8s-cluster --zone asia-northeast1-a
//after a few minutes, default-pool nodes x 5 has been deleted
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-large-mem-pool-66e3a44a-jtdn Ready <none> 9m v1.10.9-gke.5
gke-my-k8s-cluster-large-mem-pool-66e3a44a-qpbr Ready <none> 9m v1.10.9-gke.5
你可能已经注意到,所有前述操作都发生在同一个区域(asia-northeast1-a)。因此,如果 asia-northeast1-a 区域发生故障,你的集群将会宕机。为了避免区域故障,你可以考虑设置多区域集群。
多区域集群
GKE 支持多区域集群,这允许你在同一地区的多个区域中启动 Kubernetes 节点。在前面的示例中,Kubernetes 节点只在 asia-northeast1-a 区域中提供,因此让我们重新配置一个集群,包含 asia-northeast1-a、asia-northeast1-b 和 asia-northeast1-c 三个区域。
这非常简单;你只需在创建新集群时使用 --node-locations 参数指定三个区域。
让我们删除之前的集群,并使用 --node-locations 选项创建一个新的集群:
//delete cluster first
$ gcloud container clusters delete my-k8s-cluster --zone asia-northeast1-a
The following clusters will be deleted.
- [my-k8s-cluster] in [asia-northeast1-a]
Do you want to continue (Y/n)? y ...
...
//create a new cluster with --node-locations option with 2 nodes per zones
$ gcloud container clusters create my-k8s-cluster --machine-type f1-micro --num-nodes 2 --network my-custom-network --subnetwork subnet-c --tags private --zone asia-northeast1-a --node-locations asia-northeast1-a,asia-northeast1-b,asia-northeast1-c
这个示例将在每个区域创建两个节点(asia-northeast1-a、b 和 c)。因此,总共将添加六个节点:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-default-pool-58e4e9a4-74hc Ready <none> 43s v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-58e4e9a4-8jft Ready <none> 1m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-f7baf2e1-lk7j Ready <none> 1m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-f7baf2e1-zktg Ready <none> 1m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-fa5a04a5-bbj5 Ready <none> 1m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-fa5a04a5-d35z Ready <none> 1m v1.10.9-gke.5
你还可以通过 Kubernetes 标签 failure-domain.beta.kubernetes.io/zone 来区分节点区域,以便指定想要部署 Pod 的区域。
集群升级
一旦你开始管理 Kubernetes,可能会遇到在升级 Kubernetes 集群时的困难。由于 Kubernetes 项目更新非常积极,每大约三个月就会发布一个新版本,例如 1.11.0(2018 年 6 月 27 日发布)、1.12.0(2018 年 9 月 27 日发布)和 1.13.0(2018 年 12 月 3 日发布)。
GKE 还会及时添加对新版本的支持。这使得我们可以通过gcloud命令升级 master 和节点。你可以运行以下命令查看 GKE 支持的 Kubernetes 版本:
$ gcloud container get-server-config
Fetching server config for us-west1-b
defaultClusterVersion: 1.10.9-gke.5
defaultImageType: COS
validImageTypes:
- COS_CONTAINERD
- COS
- UBUNTU
validMasterVersions:
- 1.11.4-gke.8
- 1.11.3-gke.18
- 1.11.2-gke.20
- 1.11.2-gke.18
- 1.10.9-gke.7
- 1.10.9-gke.5
- 1.10.7-gke.13
- 1.10.7-gke.11
- 1.10.6-gke.13
- 1.10.6-gke.11
- 1.9.7-gke.11
validNodeVersions:
- 1.11.4-gke.8
- 1.11.3-gke.18
- 1.11.2-gke.20
- 1.11.2-gke.18
- 1.11.2-gke.15
- 1.11.2-gke.9
- 1.10.9-gke.7
- 1.10.9-gke.5
- 1.10.9-gke.3
- 1.10.9-gke.0
- 1.10.7-gke.13
- 1.10.7-gke.11
...
因此,你可以看到在写作时,最新支持的 Kubernetes 版本是1.11.4-gke.8,在 master 和 node 上都是如此。由于之前安装的版本是1.10.9-gke.5,我们将其更新为1.11.4-gke.8。首先,你需要先升级master:
//upgrade master using --master option
$ gcloud container clusters upgrade my-k8s-cluster --zone asia-northeast1-a --cluster-version 1.11.4-gke.8 --master
Master of cluster [my-k8s-cluster] will be upgraded from version
[1.10.9-gke.5] to version [1.11.4-gke.8]. This operation is
long-running and will block other operations on the cluster (including
delete) until it has run to completion.
Do you want to continue (Y/n)? y
Upgrading my-k8s-cluster...done.
Updated [https://container.googleapis.com/v1/projects/devops-with-kubernetes/zones/asia-northeast1-a/clusters/my-k8s-cluster].
这大约需要 10 分钟,具体时间取决于环境。之后,你可以通过以下命令验证MASTER_VERSION:
//master upgrade has been successfully to done
$ gcloud container clusters list --zone asia-northeast1-a
NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
my-k8s-cluster asia-northeast1-a 1.11.4-gke.8 35.243.78.166 f1-micro 1.10.9-gke.5 * 6 RUNNING
现在你可以开始将所有节点升级到1.11.4-gke.8版本。由于 GKE 尝试执行滚动升级,它将在每个节点上逐一执行以下步骤:
-
从集群中取消注册目标节点
-
删除旧的虚拟机实例
-
提供一个新的虚拟机实例
-
设置节点为
1.11.4-gke.8版本 -
注册到
master
因此,节点的升级比 master 升级需要更长时间:
//node upgrade (not specify --master)
$ gcloud container clusters upgrade my-k8s-cluster --zone asia-northeast1-a --cluster-version 1.11.4-gke.8
All nodes (6 nodes) of cluster [my-k8s-cluster] will be upgraded from
version [1.10.9-gke.5] to version [1.11.4-gke.8]. This operation is
long-running and will block other operations on the cluster (including delete) until it has run to completion.
Do you want to continue (Y/n)? y
在滚动升级过程中,你可以看到节点状态如下;这个例子展示了滚动更新的中途状态。这里,两个节点已经升级到1.11.4-gke.8,一个节点即将升级,剩余的三个节点处于待处理状态:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-k8s-cluster-default-pool-58e4e9a4-74hc Ready <none> 18m v1.11.4-gke.8
gke-my-k8s-cluster-default-pool-58e4e9a4-8jft Ready <none> 19m v1.11.4-gke.8
gke-my-k8s-cluster-default-pool-f7baf2e1-lk7j Ready <none> 19m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-f7baf2e1-zktg Ready <none> 19m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-fa5a04a5-bbj5 Ready,SchedulingDisabled <none> 19m v1.10.9-gke.5
gke-my-k8s-cluster-default-pool-fa5a04a5-d35z Ready <none> 19m v1.10.9-gke.5
Kubernetes 云提供商
GKE 还原生集成了 Kubernetes 云提供商,深度集成了 GCP 基础设施,例如,通过 VPC 路由的覆盖网络(overlay network)、通过持久磁盘(Persistent Disk)的StorageClass、以及通过 L4 负载均衡器的服务(Service)。其中最好的集成之一是由 L7 负载均衡器提供的 ingress 控制器。让我们来看看它是如何工作的。
存储类(StorageClass)
类似于 AWS 上的 EKS,GKE 也默认设置StorageClass;它使用的是持久磁盘(Persistent Disk):
$ kubectl get storageclass
NAME TYPE
standard (default) kubernetes.io/gce-pd $ kubectl describe storageclass standard
Name: standard
IsDefaultClass: Yes
Annotations: storageclass.beta.kubernetes.io/is-default-class=true
Provisioner: kubernetes.io/gce-pd
Parameters: type=pd-standard
AllowVolumeExpansion: <unset>
MountOptions: <none>
ReclaimPolicy: Delete
VolumeBindingMode: Immediate
Events: <none>
因此,当创建持久卷声明(persistent volume claim)时,它会自动分配 GCP 持久磁盘作为 Kubernetes 持久卷。有关持久卷声明和动态供应的更多信息,请参考第四章,管理有状态工作负载:
$ cat pvc-gke.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-gke-1
spec:
storageClassName: "standard"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
//create Persistent Volume Claim
$ kubectl create -f pvc-gke.yml
persistentvolumeclaim "pvc-gke-1" created
//check Persistent Volume
$ kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-bc04e717-8c82-11e7-968d-42010a920fc3 10Gi RWO Delete Bound default/pvc-gke-1 standard 2s
//check via gcloud command
$ gcloud compute disks list
NAME ZONE SIZE_GB TYPE STATUS
gke-my-k8s-cluster-d2e-pvc-bc04e717-8c82-11e7-968d-42010a920fc3 asia-northeast1-a 10 pd-standard READY
L4 负载均衡器
类似于 AWS 云提供商,GKE 也支持使用 L4 负载均衡器为 Kubernetes 服务(Kubernetes Services)提供负载均衡。只需将Service.spec.type指定为 LoadBalancer,GKE 将自动设置和配置 L4 负载均衡器。
请注意,L4 负载均衡器与 Kubernetes 节点之间的相应防火墙规则可以由云提供商自动创建。这非常简单,但足够强大,如果你想快速将应用暴露到互联网上,足够用了:
$ cat grafana.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
project: devops-with-kubernetes
app: grafana
template:
metadata:
labels:
project: devops-with-kubernetes
app: grafana
spec:
containers:
- image: grafana/grafana
name: grafana
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: grafana
spec:
ports:
- port: 80
targetPort: 3000
type: LoadBalancer
selector:
project: devops-with-kubernetes
app: grafana
//deploy grafana with Load Balancer service
$ kubectl create -f grafana.yml
deployment.apps "grafana" created
service "grafana" created
//check L4 Load balancer IP address
$ kubectl get svc grafana NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana LoadBalancer 10.59.244.97 35.243.118.88 80:31213/TCP 1m //can reach via GCP L4 Load Balancer $ curl -I 35.243.118.88
HTTP/1.1 302 Found
Content-Type: text/html; charset=utf-8
Location: /login
Set-Cookie: grafana_sess=186d81bd66d150d5; Path=/; HttpOnly
Set-Cookie: redirect_to=%252F; Path=/; HttpOnly
Date: Sun, 09 Dec 2018 02:25:48 GMT
L7 负载均衡器(ingress)
GKE 也支持 Kubernetes ingress,它可以设置 GCP L7 负载均衡器,根据 URL 将 HTTP 请求调度到目标服务。你只需要设置一个或多个 NodePort 服务,然后创建 ingress 规则指向这些服务。在后台,Kubernetes 会自动创建和配置以下防火墙规则:health check、backend 服务、forwarding rule 和 url-maps。
首先,我们将创建相同的示例,使用 nginx 和 Tomcat 部署到 Kubernetes 集群。这些示例使用的是绑定到 NodePort 的 Kubernetes 服务,而不是 LoadBalancer:

目前,你无法访问 Kubernetes 服务,因为还没有允许从互联网访问的防火墙规则。因此,让我们创建 Kubernetes ingress 来指向这些服务。
你可以使用 kubectl port-forward <pod name> <your machine available port><: service port number> 通过 Kubernetes API 服务器访问 pod。对于上述案例,可以使用 kubectl port-forward tomcat-670632475-l6h8q 10080:8080。之后,打开浏览器并访问 http://localhost:10080/,即可直接访问 Tomcat pod。
Kubernetes ingress 的定义与 GCP 后端服务的定义非常相似,因为它需要指定 URL 路径、Kubernetes 服务名称和服务端口号的组合。在这种情况下,/ 和 /* URLs 指向 nginx 服务,而 /examples 和 /examples/* URLs 则指向 Tomcat 服务,如下所示:
$ cat nginx-tomcat-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-tomcat-ingress
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
- path: /examples
backend:
serviceName: tomcat
servicePort: 8080
- path: /examples/*
backend:
serviceName: tomcat
servicePort: 8080
$ kubectl create -f nginx-tomcat-ingress.yaml
ingress "nginx-tomcat-ingress" created
完整配置 GCP 组件(如health check、forwarding rule、backend 服务和 url-maps)大约需要 10 到 15 分钟:
$ kubectl get ing
NAME HOSTS ADDRESS PORTS AGE
nginx-tomcat-ingress * 107.178.253.174 80 1m
你也可以在 web 控制台上查看状态,如下所示:

完成 L7 负载均衡器设置后,你可以通过访问公共 IP 负载均衡器地址 (http://107.178.253.174/) 查看 nginx 页面。还可以访问 http://107.178.253.174/examples/,你会看到 tomcat example 页面。
在 GKE 完全配置完 L7 负载均衡器之前,GKE 会返回 404 Not Found。
在之前的步骤中,我们为 L7 负载均衡器创建并分配了一个临时 IP 地址。然而,使用 L7 负载均衡器的最佳实践是分配一个静态 IP 地址,因为你还可以将 DNS(FQDN)与静态 IP 地址关联。
为此,更新 ingress 设置,添加名为 kubernetes.io/ingress.global-static-ip-name 的注释,以便将 GCP 静态 IP 地址名称关联起来,如下所示:
//allocate static IP as my-nginx-tomcat
$ gcloud compute addresses create my-nginx-tomcat --global
//check assigned IP address
$ gcloud compute addresses list
NAME REGION ADDRESS STATUS
my-nginx-tomcat 35.186.227.252 IN_USE
//add annotations definition
$ cat nginx-tomcat-static-ip-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-tomcat-ingress
annotations:
kubernetes.io/ingress.global-static-ip-name: my-nginx-
tomcat
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
- path: /examples
backend:
serviceName: tomcat
servicePort: 8080
- path: /examples/*
backend:
serviceName: tomcat
servicePort: 8080
//apply command to update Ingress
$ kubectl apply -f nginx-tomcat-static-ip-ingress.yaml
//check Ingress address that associate to static IP
$ kubectl get ing
NAME HOSTS ADDRESS PORTS AGE
nginx-tomcat-ingress * 35.186.227.252 80 48m
现在,你可以通过静态 IP 地址访问 ingress,比如 http://35.186.227.252/ (nginx) 和 http://35.186.227.252/examples/(Tomcat),而不是通过临时 IP 地址访问。这对用户有好处,并且能保留静态 IP 地址。例如,当你重新创建 ingress 时,IP 地址不会改变。
总结
在本章中,我们讨论了 Google Cloud Platform。其基本概念与 AWS 相似,但一些政策和概念有所不同。尤其是在 Google Container Engine 中,因为这是一个非常强大的服务,适用于将 Kubernetes 用作生产级别的工具。Kubernetes 集群和节点的管理非常容易安装和升级。该云服务提供商也与 GCP 完全集成(特别是入口控制,因为可以通过一个命令配置 L7 负载均衡器)。因此,如果你计划在公有云上使用 Kubernetes,强烈建议尝试 GKE。
第十二章,Kubernetes on Azure,将探索另一个公有云服务——Microsoft Azure,它也提供托管的 Kubernetes 服务。
第十二章:Azure 上的 Kubernetes
与 AWS 和 GCP 类似,Microsoft Azure 的公共云也提供托管服务,即 Kubernetes。Azure Kubernetes Service(AKS)于 2017 年推出。Azure 用户可以在 AKS 上管理、部署和扩展他们的容器化应用,而无需担心底层基础设施。
在本章中,我们将首先介绍 Azure,然后讲解 AKS 使用的主要服务。接着,我们将学习如何启动 AKS 集群并进行实验:
-
Azure 介绍
-
Azure 中的基本服务
-
设置 AKS
-
Azure 云服务提供商
Azure 介绍
与 GCP 一样,Microsoft Azure 提供平台即服务(PaaS)。用户可以将应用部署到 Azure 应用服务,而无需了解详细设置和虚拟机管理。自 2010 年以来,Azure 已为许多用户提供微软软件和第三方软件。每个 Azure 服务提供不同的定价层次。在 Azure 中,这些定价层次也被称为SKU(en.wikipedia.org/wiki/Stock_keeping_unit)。
Azure Kubernetes Service(AKS)于 2017 年宣布,作为对其原有容器编排解决方案Azure Container Service(ACS)的全新支持。自那时起,Azure 的容器解决方案更加关注 Kubernetes 的支持,而不是其他容器编排工具,如 Docker Enterprise 和 Mesosphere DC/OS。作为一个 Kubernetes 云服务提供商,AKS 提供了一些原生支持,例如 Azure Active Directory 用于 RBAC、Azure 磁盘用于存储类、Azure 负载均衡器用于服务、以及 HTTP 应用程序路由用于 Ingress。
资源组
资源组是 Azure 中的一组资源,代表一个逻辑组。您可以一次性部署和删除组内的所有资源。Azure 资源管理器是一个帮助您管理资源组的工具。遵循基础设施即代码的精神(en.wikipedia.org/wiki/Infrastructure_as_code),Azure 提供了一个资源管理器模板,它是一个 JSON 格式的文件,定义了所需资源的配置和依赖关系。用户可以将模板反复且一致地部署到不同环境的多个资源组中。
让我们看看这些内容在 Azure 门户中的表现。首先,您需要拥有一个 Azure 账户。如果没有,请访问azure.microsoft.com/features/azure-portal/并注册免费账户。Azure 免费账户为您提供 12 个月的热门免费服务和 30 天内$200 的信用额度。注册账户时需要信用卡信息,但除非您升级账户类型,否则不会收费。
登录后,点击侧边栏中的创建资源,进入入门指南。我们会看到一个 Web 应用,点击它并输入应用名称。在创建资源时,你需要指定资源组。我们可以使用现有的资源组,也可以创建新的资源组。现在让我们创建一个新的资源组,因为我们还没有资源组。根据需要,修改运行时堆栈为你的应用程序运行时。以下是该过程的截图:

在页面底部,创建按钮旁边有一个自动化选项按钮。如果我们点击它,会看到资源模板自动创建。如果我们点击部署,将显示模板定义的自定义参数。目前,我们直接点击创建。以下是资源模板的截图:

点击创建后,控制台会将我们带到以下视图供我们探索。让我们去查看我们刚刚创建的资源组 devops-app,它位于最近资源标签下:

在这个资源组中,我们可以看到在应用服务中运行着一个应用程序和一个服务计划。我们还可以在侧边栏中看到许多功能。资源组的目的是为用户提供一组资源的全面视图,因此我们无需进入不同的控制台去查找它们:

如果我们点击 devops-app 资源组,它将带我们进入应用服务控制台,这是 Azure 提供的 PaaS 服务:

此时,示例 Web 应用已部署到 Azure 应用服务。如果我们访问 URL 中指定的端点(在此案例中为 devops-app.azurewebsites.net/),我们就能找到它:

我们还可以上传自己的 Web 应用,或者与我们的版本控制软件(如 GitHub 或 Bitbucket)集成,并通过 Azure 构建完整的流水线。有关更多信息,请访问部署中心页面 (docs.microsoft.com/en-us/azure/app-service/deploy-continuous-deployment)。
我们还可以在资源组控制台中轻松删除资源组:

确认后,相关资源将被清理:

Azure 虚拟网络
Azure 虚拟网络(VNet)在 Azure 中创建一个隔离的私有网络段。这个概念类似于 AWS 和 GCP 中的 VPC。用户指定一系列连续的 IP(即 CIDR:en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)和位置(在 AWS 中也称为区域)。我们可以在azure.microsoft.com/global-infrastructure/locations/找到完整的区域列表。我们还可以在虚拟网络中创建多个子网,或者在创建时启用 Azure 防火墙。Azure 防火墙是一个具有高可用性和可扩展性的网络安全服务。它可以使用用户指定的规则来控制和过滤流量,并提供入站 DNAT 和出站 SNAT 支持。根据你使用的平台,你可以通过以下链接的说明安装 Azure CLI(相关文档可以在这里找到:docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest):docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest。另外,你也可以直接使用云壳(shell.azure.com/)。Azure 云壳是一个基于云的管理壳,内置了 Azure CLI,你可以用它来管理你的云资源。
在以下示例中,我们将演示如何使用 Azure CLI 通过 Azure 云壳创建一个 Azure 虚拟网络。只需登录你的帐户并附加云存储以持久化数据。然后,我们就可以开始了:

点击“创建”按钮并等待几秒钟后,云壳控制台将在你的浏览器中启动:

Azure CLI 命令以az作为组名。你可以输入az --help来查看子组列表,或随时使用az $subgroup_name --help来查找有关子组子命令的更多信息。一个子组可能包含多个子组。在命令的末尾是你要对资源执行的操作以及配置参数集。其形式如下:
# az $subgroup1 [$subgroup2 ...] $commands [$parameters]
在以下示例中,我们将创建一个名为devops-vnet的虚拟网络。首先,我们需要创建一个新的资源组,因为在前一部分我们删除了唯一的资源组。现在,让我们在美国中部地区创建一个名为devops的资源组:
# az group create --name devops --location centralus
{
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops",
"location": "centralus",
"managedBy": null,
"name": "devops",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null
}
在前面的命令中,子组名称是group,操作命令是create。接下来,我们将使用network.vnet子组来创建我们的虚拟网络资源,CIDR 为10.0.0.0/8,其余设置保持默认值:
# az network vnet create --name devops-vnet --resource-group devops --subnet-name default --address-prefixes 10.0.0.0/8
{
"newVNet": {
"addressSpace": {
"addressPrefixes": [
"10.0.0.0/8"
]
},
"ddosProtectionPlan": null,
"dhcpOptions": {
"dnsServers": []
},
"enableDdosProtection": false,
"enableVmProtection": false,
"etag": "W/\"a93c56be-6eab-4391-8fca-25e11625c6e5\"",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet",
"location": "centralus",
"name": "devops-vnet",
"provisioningState": "Succeeded",
"resourceGroup": "devops",
"resourceGuid": "f5b9de39-197c-440f-a43f-51964ee9e252",
"subnets": [
{
"addressPrefix": "10.0.0.0/24",
"addressPrefixes": null,
"delegations": [],
"etag": "W/\"a93c56be-6eab-4391-8fca-25e11625c6e5\"",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/default",
"interfaceEndpoints": null,
"ipConfigurationProfiles": null,
"ipConfigurations": null,
"name": "default",
"networkSecurityGroup": null,
"provisioningState": "Succeeded",
"purpose": null,
"resourceGroup": "devops",
"resourceNavigationLinks": null,
"routeTable": null,
"serviceAssociationLinks": null,
"serviceEndpointPolicies": null,
"serviceEndpoints": null,
"type": "Microsoft.Network/virtualNetworks/subnets"
}
],
"tags": {},
"type": "Microsoft.Network/virtualNetworks",
"virtualNetworkPeerings": []
}
}
我们始终可以使用 az 的 list 命令查看我们的设置列表,例如 az network vnet list,或者进入 Azure 门户查看:

网络安全组
网络安全组与虚拟网络关联,并包含一组安全规则。安全规则定义了子网或虚拟机的入站和出站流量的策略。使用网络安全组,用户可以定义是否允许入站流量访问组的资源,以及是否允许出站流量。安全规则包含以下内容:
-
优先级(范围从 100 到 4096;数字越小,优先级越高)
-
一个源或目标(CIDR 块、一组 IP 地址或另一个安全组)
-
一种协议(TCP/UDP/ICMP)
-
一个方向(入站/出站)
-
一个端口范围
-
一项操作(允许/拒绝)
让我们创建一个名为 test-nsg 的网络安全组。请注意,用户定义的规则会在安全组创建后附加:
# az network nsg create --name test-nsg --resource-group devops --location centralus
{
"NewNSG": {
"defaultSecurityRules": [
{
"access": "Allow",
"description": "Allow inbound traffic from all VMs in VNET",
"destinationAddressPrefix": "VirtualNetwork",
"name": "AllowVnetInBound",
"priority": 65000,
"sourceAddressPrefix": "VirtualNetwork",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
},
{
"access": "Allow",
"description": "Allow inbound traffic from azure load balancer",
"destinationAddressPrefix": "*",
"name": "AllowAzureLoadBalancerInBound",
"priority": 65001,
"sourceAddressPrefix": "AzureLoadBalancer",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
},
{
"access": "Deny",
"description": "Deny all inbound traffic",
"destinationAddressPrefix": "*",
"name": "DenyAllInBound",
"priority": 65500,
"sourceAddressPrefix": "*",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
},
{
"access": "Allow",
"description": "Allow outbound traffic from all VMs to all VMs in VNET",
"destinationAddressPrefix": "VirtualNetwork",
"name": "AllowVnetOutBound",
"priority": 65000,
"sourceAddressPrefix": "VirtualNetwork",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
},
{
"access": "Allow",
"description": "Allow outbound traffic from all VMs to Internet",
"destinationAddressPrefix": "Internet",
"name": "AllowInternetOutBound",
"priority": 65001,
"sourceAddressPrefix": "*",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
},
{
"access": "Deny",
"description": "Deny all outbound traffic",
"destinationAddressPrefix": "*",
"name": "DenyAllOutBound",
"priority": 65500,
"sourceAddressPrefix": "*",
...
"type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules"
}
],
"id": "...test-nsg",
"location": "centralus",
"name": "test-nsg",
"networkInterfaces": null,
"provisioningState": "Succeeded",
"resourceGroup": "devops",
"resourceGuid": "9e0e3d0f-e99f-407d-96a6-8e96cf99ecfc",
"securityRules": [],
"subnets": null,
"tags": null,
"type": "Microsoft.Network/networkSecurityGroups"
}
}
我们发现,Azure 为每个网络安全组创建了一组默认规则。默认情况下,它拒绝所有入站流量,并允许出站流量访问。我们可以看到,源和目标不能是 CIDR 块,而必须是类似 VirtualNetwork 这样的服务标签。这些服务标签实际上是一组预定义的 IP 地址前缀。Azure 管理这些服务标签并每周发布。你可以在 Microsoft 下载中心找到已发布的服务标签(www.microsoft.com/en-us/download/details.aspx?id=56519):
| 方向 | 优先级 | 源 | 源端口 | 目标 | 目标端口 | 协议 | 访问 |
|---|---|---|---|---|---|---|---|
| 入站 | 65000 |
VirtualNetwork |
0-65535 |
VirtualNetwork |
0-65535 |
所有 | 允许 |
| 入站 | 65001 |
AzureLoadBalancer |
0-65535 |
0.0.0.0/0 |
0-65535 |
所有 | 允许 |
| 入站 | 65500 |
0.0.0.0/0 |
0-65535 |
0.0.0.0/0 |
0-65535 |
所有 | 拒绝 |
| 出站 | 65000 |
VirtualNetwork |
0-65535 |
VirtualNetwork |
0-65535 |
所有 | 允许 |
| 出站 | 0.0.0.0/0 |
0-65535 |
0-65535 |
Internet |
0-65535 |
所有 | 允许 |
| 出站 | 0.0.0.0/0 |
0-65535 |
0-65535 |
0.0.0.0/0 |
0-65535 |
所有 | 拒绝 |
现在我们可以创建一个接受所有规则并具有最高优先级的网络安全组,并将其附加到我们刚刚创建的 NSG 上:
# az network nsg rule create --name test-nsg --priority 100 --resource-group devops --nsg-name test-nsg
{
"access": "Allow",
"description": null,
"destinationAddressPrefix": "*",
"destinationAddressPrefixes": [],
"destinationApplicationSecurityGroups": null,
"destinationPortRange": "80",
"destinationPortRanges": [],
"direction": "Inbound",
"etag": "W/\"65e33e31-ec3c-4eea-8262-1cf5eed371b1\"",
"id": "/subscriptions/.../resourceGroups/devops/providers/Microsoft.Network/networkSecurityGroups/test-nsg/securityRules/test-nsg",
"name": "test-nsg",
"priority": 100,
"protocol": "*",
"provisioningState": "Succeeded",
"resourceGroup": "devops",
"sourceAddressPrefix": "*",
"sourceAddressPrefixes": [],
"sourceApplicationSecurityGroups": null,
"sourcePortRange": "*",
"sourcePortRanges": [],
"type": "Microsoft.Network/networkSecurityGroups/securityRules"
}
应用安全组
应用安全组是虚拟机 NIC 的逻辑集合,可以作为网络安全组规则中的源或目标。它们使网络安全组更加灵活。例如,假设我们有两个虚拟机,它们将通过 5432 端口访问 PostgreSQL 数据库。我们希望确保只有这些虚拟机可以访问数据库:

我们可以创建两个应用安全组,分别命名为 web 和 db。然后,将虚拟机加入 web 组,将数据库加入 db 组,并创建以下网络安全组规则:
| 方向 | 优先级 | 源 | 源端口 | 目标 | 目标端口 | 协议 | 访问 |
|---|---|---|---|---|---|---|---|
| 入站 | 120 | * | * | db |
0-65535 |
所有 | 拒绝 |
| 入站 | 110 | web |
* | db |
5432 |
TCP | 允许 |
根据此表,第二条规则的优先级高于第一条。只有 web 组可以访问 db 组的端口 5432。所有其他入站流量将被拒绝。
子网
子网可以关联到网络安全组。子网也可以关联到路由表,以便拥有特定的路由。
和 AWS 一样,Azure 也提供路由表资源来进行路由管理。默认情况下,Azure 已经为虚拟网络和子网提供了默认路由。我们在使用 AKS 服务时无需担心路由问题。
创建虚拟网络时,默认会创建一个 default 子网:
# az network vnet subnet list --vnet-name devops-vnet --resource-group devops
[
{
"addressPrefix": "10.0.0.0/24",
"addressPrefixes": null,
"delegations": [],
"etag": "W/\"a93c56be-6eab-4391-8fca-25e11625c6e5\"",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/default",
"interfaceEndpoints": null,
"ipConfigurationProfiles": null,
"ipConfigurations": null,
"name": "default",
"networkSecurityGroup": null,
"provisioningState": "Succeeded",
"purpose": null,
"resourceGroup": "devops",
"resourceNavigationLinks": null,
"routeTable": null,
"serviceAssociationLinks": null,
"serviceEndpointPolicies": null,
"serviceEndpoints": null,
"type": "Microsoft.Network/virtualNetworks/subnets"
}
]
除了默认子网,我们再创建一个子网,前缀为10.0.1.0/24。请注意,子网的 CIDR 需要与所在虚拟网络的 CIDR 前缀相同:
# az network vnet subnet create --address-prefixes 10.0.1.0/24 --name test --vnet-name devops-vnet --resource-group devops
{
"addressPrefix": "10.0.1.0/24",
"addressPrefixes": null,
"delegations": [],
"etag": "W/\"9f7e284f-fd31-4bd3-ad09-f7dc75bb7c68\"",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/test",
"interfaceEndpoints": null,
"ipConfigurationProfiles": null,
"ipConfigurations": null,
"name": "test",
"networkSecurityGroup": null,
"provisioningState": "Succeeded",
"purpose": null,
"resourceGroup": "devops",
"resourceNavigationLinks": null,
"routeTable": null,
"serviceAssociationLinks": null,
"serviceEndpointPolicies": null,
"serviceEndpoints": null,
"type": "Microsoft.Network/virtualNetworks/subnets"
}
现在我们可以列出这个虚拟网络中的子网:
# az network vnet subnet list --vnet-name devops-vnet --resource-group devops | jq .[].name
"default"
"test"
jq (stedolan.github.io/jq/):
jq 是一个 JSON 命令行处理工具,默认安装在云端 shell 中。它是一个非常方便的工具,可以列出 JSON 输出中的所需字段。如果您不熟悉 jq,可以查看以下链接的手册:stedolan.github.io/jq/manual/。
Azure 虚拟机
虚拟机 (VM) 在 Azure 中类似于 Amazon EC2。要启动实例,我们需要知道要启动的 VM 镜像。我们可以使用 az vm image list 命令列出可用的镜像。在以下示例中,我们将使用 CentOS 镜像:
# az vm image list --output table
You are viewing an offline list of images, use --all to retrieve an up-to-date list
Offer Publisher Sku Urn UrnAlias Version
------------- ---------------------- ------------------ -----------------------------------------
CentOS OpenLogic 7.5 OpenLogic:CentOS:7.5:latest CentOS latest
CoreOS CoreOS Stable CoreOS:CoreOS:Stable:latest CoreOS latest
Debian credativ 8 credativ:Debian:8:latest Debian latest
openSUSE-Leap SUSE 42.3 SUSE:openSUSE-Leap:42.3:latest openSUSE-Leap latest
RHEL RedHat 7-RAW RedHat:RHEL:7-RAW:latest RHEL latest
SLES SUSE 12-SP2 SUSE:SLES:12-SP2:latest SLES latest
UbuntuServer Canonical 16.04-LTS Canonical:UbuntuServer:16.04-LTS:latest UbuntuLTS latest
WindowsServer MicrosoftWindowsServer 2019-Datacenter MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest Win2019Datacenter latest
WindowsServer MicrosoftWindowsServer 2016-Datacenter MicrosoftWindowsServer:WindowsServer:2016-Datacenter:latest Win2016Datacenter latest
WindowsServer MicrosoftWindowsServer 2012-R2-Datacenter MicrosoftWindowsServer:WindowsServer:2012-R2-Datacenter:latest Win2012R2Datacenter latest
WindowsServer MicrosoftWindowsServer 2012-Datacenter MicrosoftWindowsServer:WindowsServer:2012-Datacenter:latest Win2012Datacenter latest
WindowsServer MicrosoftWindowsServer 2008-R2-SP1 MicrosoftWindowsServer:WindowsServer:2008-R2-SP1:latest Win2008R2SP1 latest
然后,我们可以使用 az vm create 来启动我们的虚拟机。指定 --generate-ssh-keys 将为您生成一个 SSH 密钥:
# az vm create --resource-group devops --name newVM --image CentOS --admin-username centos-user --generate-ssh-keys
SSH key files '/home/chloe/.ssh/id_rsa' and '/home/chloe/.ssh/id_rsa.pub' have been generated under ~/.ssh to allow SSH access to the VM. If using machines without permanent storage, back up your keys to a safelocation.
- Running ..
{
"fqdns": "",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Compute/virtualMachines/newVM",
"location": "centralus",
"macAddress": "00-0D-3A-96-6B-A2",
"powerState": "VM running",
"privateIpAddress": "10.0.0.4",
"publicIpAddress": "40.77.97.79",
"resourceGroup": "devops",
"zones": ""
}
我们可以看到新创建的虚拟机的 publicIpAddress 是 40.77.97.79。让我们用之前指定的用户名连接它:
# ssh centos-user@40.77.97.79
The authenticity of host '40.77.97.79 (40.77.97.79)' can't be established.
ECDSA key fingerprint is SHA256:LAvnQH94bY7NaIoNgDLM5iMHT1LMRseFwu2HPqicTuo.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '40.77.97.79' (ECDSA) to the list of known hosts.
[centos-user@newVM ~]$
不适合总是允许 SSH 访问实例。让我们来看一下如何解决这个问题。首先,我们需要了解附加到这个虚拟机的网络接口:
# az vm get-instance-view --name newVM --resource-group devops
{
...
"networkProfile": {
"networkInterfaces": [
{
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkInterfaces/newVMVMNic",
"primary": null,
"resourceGroup": "devops"
}
]
},
...
}
找到 NIC 的 id 后,我们可以根据 NIC ID 查找关联的网络安全组:
# az network nic show --ids /subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkInterfaces/newVMVMNic | jq .networkSecurityGroup
{
"defaultSecurityRules": null,
"etag": null,
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkSecurityGroups/newVMNSG",
"location": null,
"name": null,
"networkInterfaces": null,
"provisioningState": null,
"resourceGroup": "devops",
"resourceGuid": null,
"securityRules": null,
"subnets": null,
"tags": null,
"type": null
}
在这里,我们可以看到 NSG 的名称是 newVMNSG。让我们列出附加到这个 NSG 的规则:
# az network nsg rule list --resource-group devops --nsg-name newVMNSG
[
{
"access": "Allow",
"description": null,
"destinationAddressPrefix": "*",
"destinationAddressPrefixes": [],
"destinationApplicationSecurityGroups": null,
"destinationPortRange": "22",
"destinationPortRanges": [],
"direction": "Inbound",
"etag": "W/\"9ab6b2d7-c915-4abd-9c02-a65049a62f02\"",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkSecurityGroups/newVMNSG/securityRules/default-allow-ssh",
"name": "default-allow-ssh",
"priority": 1000,
"protocol": "Tcp",
"provisioningState": "Succeeded",
"resourceGroup": "devops",
"sourceAddressPrefix": "*",
"sourceAddressPrefixes": [],
"sourceApplicationSecurityGroups": null,
"sourcePortRange": "*",
"sourcePortRanges": [],
"type": "Microsoft.Network/networkSecurityGroups/securityRules"
}
]
有一条 default-allow-ssh 规则,ID 为 /subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkSecurityGroups/newVMNSG/securityRules/default-allow-ssh,附加在 NSG 上。我们将删除它:
# az network nsg rule delete --ids "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/networkSecurityGroups/newVMNSG/securityRules/default-allow-ssh"
一旦我们删除规则,就无法通过 SSH 访问虚拟机:
# ssh centos-user@40.77.97.79
...
存储帐户
存储帐户是 Azure 存储解决方案提供的存储对象的容器,如文件、表格和磁盘。用户可以根据使用情况创建一个或多个存储帐户。
存储帐户有三种类型:
-
通用 v2 帐户
-
通用 v1 帐户
-
Blob 存储帐户
v1 是旧版的存储帐户类型,Blob 存储帐户仅允许使用 Blob 存储。v2 帐户是目前 Azure 中最推荐的帐户类型。
负载均衡器
Azure 中负载均衡器的功能类似于其他公有云提供的负载均衡服务,用于将流量路由到后端。它们还会对端点进行健康检查,如果发现后端不健康,会断开连接。Azure 负载均衡器与其他负载均衡器的主要区别在于,Azure 负载均衡器可以拥有多个 IP 地址和多个端口。这意味着当创建新的负载均衡服务时,AKS 不需要创建新的负载均衡器,而是会在负载均衡器中创建一个新的前端 IP。
Azure 磁盘
Azure 磁盘有两种类型:托管磁盘和非托管磁盘。在使用 Azure 托管磁盘之前,用户必须创建存储帐户来使用非托管磁盘。一个存储帐户可能会超出可扩展性目标(docs.microsoft.com/en-us/azure/storage/common/storage-scalability-targets),从而影响 I/O 性能。使用托管磁盘时,我们不需要自己创建存储帐户;Azure 会在后台管理这些帐户。托管磁盘有不同类型的性能级别:标准 HDD 磁盘、标准 SSD 磁盘和高性能 SSD 磁盘。HDD 磁盘(en.wikipedia.org/wiki/Hard_disk_drive)是一种成本效益较高的选择,而 SSD 磁盘(en.wikipedia.org/wiki/Solid-state_drive)性能更佳。对于 I/O 密集型工作负载,高性能 SSD 是最佳选择。使用高性能 SSD 磁盘附加到虚拟机时,虚拟机的磁盘 IOPS 可达到 80,000,磁盘吞吐量可达 2,000 MB/s。Azure 磁盘还提供四种类型的复制:
-
本地冗余存储 (LRS):数据仅保存在一个单独的区域
-
区域冗余存储 (ZRS):数据跨三个区域持久化
-
地理冗余存储 (GRS):跨区域复制
-
读取访问地理冗余存储 (RA-GRS):跨区域复制与读取副本
Azure Kubernetes 服务
Azure Kubernetes 服务是 Azure 中的托管 Kubernetes 服务。一个集群包含一组节点(例如 Azure 虚拟机)。就像普通的 Kubernetes 节点一样,kube-proxy 和 kubelet 会安装在节点上。kube-proxy 与 Azure 虚拟 NIC 通信,管理服务和 Pod 的进出路由。kubelet 接收来自主节点的请求,调度 Pod,并报告指标。在 Azure 中,我们可以将各种 Azure 存储选项(如 Azure 磁盘和 Azure 文件)挂载为 持久卷(PV),用于持久化容器的数据。下面是该示例的说明:

想从头开始构建集群吗?
如果你更倾向于自己搭建一个集群,请务必查看 AKS-engine 项目(github.com/Azure/aks-engine),该项目通过 Azure 资源管理器在 Azure 中构建 Kubernetes 基础设施。
设置你的第一个 Kubernetes 集群在 AKS 上
一个 AKS 集群可以在其自己的 VPC(基本网络配置)中启动,或在现有的 VPC(高级网络配置)中启动;两者都可以通过 Azure CLI 启动。我们可以在创建集群时指定一组参数,其中包括以下内容:
| 参数 | 描述 |
|---|---|
--name |
集群名称。 |
--enable-addons |
启用 Kubernetes 附加组件模块,并以逗号分隔的列表形式提供。 |
--generate-ssh-keys |
如果 SSH 密钥文件不存在,则生成 SSH 密钥文件。 |
--node-count |
节点的数量。默认值为三个。 |
--network-policy |
(预览版)启用或禁用网络策略。默认值为禁用。 |
--vnet-subnet-id |
在 VNet 中部署集群的子网 ID。 |
--node-vm-size |
虚拟机的大小。默认值为 Standard_DS2_v2。 |
--service-cidr |
从中分配服务集群 IP 的 CIDR 表示法 IP 范围。 |
--max-pods |
默认值为 110,或者对于高级网络配置(使用现有的 VNet)为 30。 |
在以下示例中,我们将首先创建一个包含两个节点的集群,并启用用于监控的 addons 以启用 Azure 监控,此外还启用 http_application_routing 以为入口启用 HTTP 应用程序路由:
// create an AKS cluster with two nodes
# az aks create --resource-group devops --name myAKS --node-count 2 --enable-addons monitoring,http_application_routing --generate-ssh-keys
Running...
# az aks list --resource-group devops
[
{
"aadProfile":null,
"addonProfiles":{
"httpApplicationRouting":{
"config":{
"HTTPApplicationRoutingZoneName":"cef42743fd964970b357.centralus.aksapp.io"
},
"enabled":true
},
"omsagent":{
"config":{
"logAnalyticsWorkspaceResourceID":...
},
"enabled":true
}
},
"agentPoolProfiles":[
{
"count":2,
"maxPods":110,
"name":"nodepool1",
"osDiskSizeGb":30,
"osType":"Linux",
"storageProfile":"ManagedDisks",
"vmSize":"Standard_DS2_v2"
}
],
"dnsPrefix":"myAKS-devops-f82579",
"enableRbac":true,
"fqdn":"myaks-devops-f82579-077667ba.hcp.centralus.azmk8s.io",
"id":"/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourcegroups/devops/providers/Microsoft.ContainerService/managedClusters/myAKS",
"kubernetesVersion":"1.9.11",
"linuxProfile":{
"adminUsername":"azureuser",
"ssh":{
"publicKeys":[
{
"keyData":"ssh-rsa xxx"
}
]
}
},
"location":"centralus",
"name":"myAKS",
"networkProfile":{
"dnsServiceIp":"10.0.0.10",
"dockerBridgeCidr":"172.17.0.1/16",
"networkPlugin":"kubenet",
"networkPolicy":null,
"podCidr":"10.244.0.0/16",
"serviceCidr":"10.0.0.0/16"
},
"nodeResourceGroup":"MC_devops_myAKS_centralus",
"provisioningState":"Succeeded",
"resourceGroup":"devops",
"servicePrincipalProfile":{
"clientId":"db016e5d-b3e5-4e22-a844-1dad5d16fec1"
},
"type":"Microsoft.ContainerService/ManagedClusters"
}
]
集群启动后,我们可以使用 get-credentials 子命令来配置我们的 kubeconfig。上下文名称将是集群名称,在此情况下为 myAKS:
// configure local kubeconfig to access the cluster
# az aks get-credentials --resource-group devops --name myAKS
Merged "myAKS" as current context in /home/chloe/.kube/config
// check current context
# kubectl config current-context
myAKS
让我们看看节点是否已加入集群。确保所有节点都处于 Ready 状态:
# kubectl get nodes
NAME STATUS ROLES AGE VERSION
aks-nodepool1-37748545-0 Ready agent 12m v1.9.11
aks-nodepool1-37748545-1 Ready agent 12m v1.9.11
让我们尝试通过我们在 Chapter3 中使用的示例来部署一个 ReplicaSet:
# kubectl create -f chapter3/3-2-3_Service/3-2-3_rs1.yaml
replicaset.apps/nginx-1.12 created
创建一个服务来访问 ReplicaSet。我们将使用 chapter3/3-2-3_Service/3-2-3_service.yaml 文件,并在 spec 中添加 type: LoadBalancer 行:
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
selector:
project: chapter3
service: web
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
然后我们可以查看该服务,直到 EXTERNAL-IP 变为外部 IP 地址。在这里,我们获得了 40.122.78.184:
// watch the external ip from <pending> to IP
# kubectl get svc --watch
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 6h
nginx-service LoadBalancer 10.0.139.13 40.122.78.184 80:31011/TCP 1m
让我们访问这个站点!

在前面的示例中,我们展示了如何将 nginx 服务部署到 AKS 并使用其负载均衡器。如果我们已经在现有的 VNet 中拥有一组资源,并且希望在该 VNet 内启动一个 AKS 集群与现有资源进行通信,该怎么办呢?那么,我们需要在 AKS 中使用高级网络配置。基本网络与高级网络的根本区别在于,基本网络使用 kubenet(github.com/vplauzon/aks/tree/master/aks-kubenet)作为网络插件,而高级网络使用 Azure CNI 插件(github.com/Azure/azure-container-networking/tree/master/cni)。与基本网络相比,高级网络有更多的限制。例如,节点上默认的最大 pod 数量是 30,而不是 110。这是因为 Azure CNI 仅为节点上的 NIC 配置了 30 个附加 IP 地址。Pod IP 是 NIC 上的二级 IP,因此私有 IP 被分配给在虚拟网络内部可访问的 Pods。当使用 kubenet 时,集群 IP 分配给 Pods,这些 IP 不属于虚拟网络,而是由 AKS 管理。集群 IP 无法在集群外部访问。除非你有特殊需求,比如需要从集群外部访问 pods,或希望现有资源与集群之间建立连接,否则基本网络配置应该能够满足大多数场景。
让我们看看如何创建一个具有高级网络配置的 AKS 集群。我们需要为集群指定现有的子网。首先,让我们列出之前创建的devops-vnet文件中的子网 ID:
# az network vnet subnet list --vnet-name devops-vnet --resource-group devops | jq .[].id
"/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/default"
"/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/test"
让我们使用默认子网。请注意,在高级网络配置中,一个子网只能定位一个 AKS 集群。此外,我们指定的服务 CIDR(用于分配集群 IP)不能与集群所在子网的 CIDR 重叠:
# az aks create --resource-group devops --name myAdvAKS --network-plugin azure --vnet-subnet-id /subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourceGroups/devops/providers/Microsoft.Network/virtualNetworks/devops-vnet/subnets/default --service-cidr 10.2.0.0/24 --node-count 1 --enable-addons monitoring,http_application_routing --generate-ssh-keys
- Running ..
{
"aadProfile": null,
"addonProfiles": {
"httpApplicationRouting": {
"config": {
"HTTPApplicationRoutingZoneName": "cef42743fd964970b357.centralus.aksapp.io"
},
"enabled": true
},
"omsagent": {
"config": {
"logAnalyticsWorkspaceResourceID":
},
"enabled": true
}
},
"agentPoolProfiles": [
{
"count": 1,
"maxPods": 30,
"name": "nodepool1",
...
"vmSize": "Standard_DS2_v2"
}
],
"dnsPrefix": "myAdvAKS-devops-f82579",
"enableRbac": true,
"fqdn": "myadveks-devops-f82579-059d5b24.hcp.centralus.azmk8s.io",
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourcegroups/devops/providers/Microsoft.ContainerService/managedClusters/myAdvAKS",
"kubernetesVersion": "1.9.11",
"linuxProfile": {
"adminUsername": "azureuser",
"ssh": {
"publicKeys": [
...
]
}
},
"location": "centralus",
"name": "myAdvAKS",
"networkProfile": {
"dnsServiceIp": "10.0.0.10",
"dockerBridgeCidr": "172.17.0.1/16",
"networkPlugin": "azure",
"networkPolicy": null,
"podCidr": null,
"serviceCidr": "10.2.0.0/24"
},
"nodeResourceGroup": "MC_devops_myAdvAKS_centralus",
"provisioningState": "Succeeded",
"resourceGroup": "devops",
"servicePrincipalProfile": {
"clientId": "db016e5d-b3e5-4e22-a844-1dad5d16fec1",
"secret": null
},
"tags": null,
"type": "Microsoft.ContainerService/ManagedClusters"
}
记得配置kubeconfig:
# az aks get-credentials --resource-group devops --name myAdvAKS
Merged "myAdvAKS" as current context in /home/chloe/.kube/config
如果我们对chapter3/3-2-3_Service/3-2-3_rs1.yaml和chapter3/3-2-3_Service/3-2-3_service.yaml重复执行上述代码,我们应该能够获得相同的结果。
节点池
就像 GKE 一样,节点池是相同大小的虚拟机组。在撰写本书时,支持多个节点池的功能正在开发中。请关注讨论:github.com/Azure/AKS/issues/759,或等待官方公告。
Azure 提供了虚拟机规模集作为其自动扩展组解决方案。在 Kubernetes 1.12 中,VMSS 支持已普遍可用。通过 VMSS,VM 可以根据 VM 指标进行扩展。欲了解更多信息,请查看官方文档:kubernetes.io/blog/2018/10/08/support-for-azure-vmss-cluster-autoscaler-and-user-assigned-identity/。
集群升级
在升级集群之前,确保你的订阅有足够的资源,因为节点将通过滚动部署进行替换。额外的节点将被添加到集群中。要检查配额限制,请使用 az vm list-usage --location $location 命令。
让我们看看当前使用的是哪个 Kubernetes 版本:
# az aks show --resource-group devops --name myAKS | jq .kubernetesVersion
"1.9.11"
Azure CLI 提供了 get-upgrades 子命令,用于检查集群可以升级到哪个版本:
# az aks get-upgrades --name myAKS --resource-group devops
{
"agentPoolProfiles": [
{
"kubernetesVersion": "1.9.11",
"name": null,
"osType": "Linux",
"upgrades": [
"1.10.8",
"1.10.9"
]
}
],
"controlPlaneProfile": {
"kubernetesVersion": "1.9.11",
"name": null,
"osType": "Linux",
"upgrades": [
"1.10.8",
"1.10.9"
]
},
"id": "/subscriptions/f825790b-ac24-47a3-89b8-9b4b3974f0d5/resourcegroups/devops/providers/Microsoft.ContainerService/managedClusters/myAKS/upgradeprofiles/default",
"name": "default",
"resourceGroup": "devops",
"type": "Microsoft.ContainerService/managedClusters/upgradeprofiles"
}
这显示我们可以升级到版本 1.10.8 和 1.10.9。Azure 中不允许跳过小版本,意味着我们不能从 1.9.11 升级到 1.11.x。我们必须先将集群升级到 1.10,然后再升级到 1.11。从 AKS 升级非常简单,就像从 GKE 升级一样。假设我们想要升级到 1.10.9:
# az aks upgrade --name myAKS --resource-group devops --kubernetes-version 1.10.9
操作完成后,我们可以检查集群的当前版本。集群现在已升级到所需的版本:
# az aks show --resource-group devops --name myAKS --output table
Name Location ResourceGroup KubernetesVersion ProvisioningState Fqdn
------ ---------- --------------- ------------------- ------------------- ----------------------------------------------------
myAKS centralus devops 1.10.9 Succeeded myaks-devops-f82579-077667ba.hcp.centralus.azmk8s.io
节点也应该升级到 1.10.9:
# kubectl get nodes
NAME STATUS ROLES AGE VERSION
aks-nodepool1-37748545-2 Ready agent 6m v1.10.9
aks-nodepool1-37748545-3 Ready agent 6m v1.10.9
监控与日志
我们在创建集群时部署了监控插件。这让我们可以通过 Azure 监控轻松观察集群度量标准和日志。让我们访问 Azure 门户中的 Kubernetes 服务。你会看到带有见解、度量标准和日志子标签的“监控”标签。
见解页面显示了集群的一般度量标准,如节点 CPU、内存使用率和节点的总体健康状况:

你还可以从见解中观察到容器信息:

你可以在度量页面找到所有支持的资源度量标准。对于 AKS 集群,它会显示一些核心度量标准(kubernetes.io/docs/tasks/debug-application-cluster/core-metrics-pipeline/)。这很方便,因为它意味着我们无需启动另一个监控系统,如 Prometheus:

在日志页面,你可以在 Azure 日志分析中找到集群日志。你可以通过支持的语法运行查询来搜索日志。欲了解更多信息,可以参考 Azure 监控文档:docs.microsoft.com/en-us/azure/azure-monitor/log-query/log-query-overview:

Kubernetes 云提供商
就像其他云服务提供商一样,Azure 的云控制器管理器(github.com/kubernetes/cloud-provider-azure)实现了与 Kubernetes 的一系列集成。Azure 云控制器管理器与 Azure 交互,并提供无缝的用户体验。
基于角色的访问控制
AKS 通过 OpenID 连接令牌(kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens)与 Azure Active Directory(azure.microsoft.com/en-ca/services/active-directory/)集成。基于角色的访问控制(RBAC)功能只能在集群创建时启用,因此我们从头开始创建一个启用 RBAC 的集群。
在创建集群之前,我们需要先在 Azure Active Directory 中准备两个应用注册。第一个作为用户组成员资格的服务器,第二个作为与 kubectl 集成的客户端。我们先进入 Azure 门户中的 Azure Active Directory 服务,进入“属性”标签并记录“目录 ID”。稍后在创建集群时我们将需要这个 ID:

我们先进入 Azure 门户中的 Azure Active Directory 服务的应用注册页面。侧边标签显示了两个应用注册选项。一个是原始设计,另一个是新控制台的预览版本。它们的功能基本相同。我们首先展示 GA 版本,之后再展示预览版本:

在应用注册页面,点击“新建应用注册”。添加一个名称和任何 URL。
你也可以通过 az ad app create --display-name myAKSAD --identifier-uris http://myAKSAD 尝试操作。
在这里,我们使用 myAKSAD 作为名称。创建后,首先记录 APPLICATION ID。在这里,我们得到 c7d828e7-bca0-4771-8f9d-50b9e1ea0afc。之后,点击清单并将 groupMemebershipClaims 更改为 All,这将使所有属于该组的用户在 JWT 令牌中获得组声明:

保存设置后,前往这些设置页面,然后进入密钥页面创建一个密钥。在这里,我们指定过期时间为一年。这个值是由门户网站直接生成的。稍后在创建集群时,我们将需要这个密码:

接下来,我们将为此注册定义一组应用程序和委派权限。点击“必需的权限”标签,然后点击“添加”,选择 Microsoft Graph。在这里,我们将在应用程序权限类别下选择读取目录数据,在委派权限下选择“登录并读取用户资料”和“读取目录数据”,这样服务器就可以读取目录数据并验证用户。点击“保存”按钮后,我们将在“必需的权限”页面看到以下界面:

作为管理员,我们可以通过点击“授予权限”按钮为该目录中的所有用户授予权限。这将弹出一个窗口,向你再次确认。点击“是”继续。
现在是时候为客户端创建第二个应用程序注册了。我们在这里使用的名称是myAKSADClient。创建后记下它的应用程序 ID。这里,我们获得了b4309673-464e-4c95-adf9-afeb27cc8d4c:

对于必需的权限,客户端只需访问应用程序,搜索委派权限,然后找到你为服务器创建的访问权限的显示名称。完成后别忘了点击“授予权限”按钮:

现在是时候使用 Azure AD 集成创建我们的 AKS 集群了。确保你已记录以下来自先前操作的信息:
| 我们记录的信息 | aks create 中对应的参数 |
|---|---|
| 服务器应用程序 ID | --aad-server-app-id |
| 服务器应用程序密钥(密码) | --aad-server-app-secret |
| 客户端应用程序 ID | --aad-client-app-id |
| 目录 ID | --aad-tenant-id |
现在是时候启动集群了:
# az aks create --resource-group devops --name myADAKS --generate-ssh-keys --aad-server-app-id c7d828e7-bca0-4771-8f9d-50b9e1ea0afc --aad-server-app-secret 'A/IqLPieuCqJzL9vPEI5IqCn0IaEyn5Zq/lgfovNn9g=' --aad-client-app-id b4309673-464e-4c95-adf9-afeb27cc8d4c --aad-tenant-id d41d3fd1-f004-45cf-a77b-09a532556331 --node-count 1 --enable-addons monitoring,http_application_routing --generate-ssh-keys
在 Kubernetes 中,角色绑定或集群角色绑定将角色绑定到一组用户。角色或集群角色定义了一组权限。
在集群成功启动后,为了与 OpenID 集成,我们需要先创建角色绑定或集群角色绑定。在这里,我们将使用集群中现有的集群角色,即cluster-admin。我们将用户绑定到cluster-admin集群角色,以便用户可以通过身份验证并充当集群管理员:
// login as admin first to get the access to create the bindings
# az aks get-credentials --resource-group devops --name myADAKS --admin
// list cluster roles
# kubectl get clusterrole
NAME AGE
admin 2h
cluster-admin 2h
...
对于单个用户,我们需要找到用户名。你可以在 Azure AD 门户中的“用户”页面找到目标用户的对象 ID:

使用用户主体并指定对象 ID 作为名称,如下所示:
# cat 12-3_user-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: azure-user-cluster-admins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: "8698dcf8-4d97-43be-aacd-eca1fc1495b6"
# kubectl create -f 12-3_user-clusterrolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/azure-user-cluster-admins created
之后,我们可以重新开始访问集群资源,如下所示:
// (optional) clean up kubeconfig if needed.
# rm -f ~/.kube/config
// setup kubeconfig with non-admin
# az aks get-credentials --resource-group devops --name myADAKS
// try get the nodes
# kubectl get nodes
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code A6798XUFB to authenticate.
看起来我们需要登录才能列出任何资源。请访问microsoft.com/devicelogin页面并输入提示中的代码:

登录到你的 Microsoft 账户并返回终端,集成看起来非常棒!
# kubectl get nodes
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code A6798XUFB to authenticate.
NAME STATUS ROLES AGE VERSION
aks-nodepool1-42766340-0 Ready agent 26m v1.9.11
而不是指定一组用户,你可以选择通过 Azure AD 组对象 ID 来指定用户。替换集群角色绑定配置中的主题,使该组中的所有用户都能获得集群管理员权限:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: "$group_object_id"
存储类
当我们启动 AKS 时,它会为我们创建两个默认存储类:default 和 managed-premium:
# kubectl get storageclass
NAME PROVISIONER AGE
default (default) kubernetes.io/azure-disk 4h
managed-premium kubernetes.io/azure-disk 11h
如果我们查看这些存储类的详细信息,会发现它们分别是Standard_LRS和Premium_LRS类型:
# kubectl describe storageclass default
Name: default
IsDefaultClass: Yes
Annotations: ...
,storageclass.beta.kubernetes.io/is-default-class=true
Provisioner: kubernetes.io/azure-disk
Parameters: cachingmode=None,kind=Managed,storageaccounttype=Standard_LRS
AllowVolumeExpansion: <unset>
MountOptions: <none>
ReclaimPolicy: Delete
VolumeBindingMode: Immediate
Events: <none>
# kubectl describe storageclass managed-premium
Name: managed-premium
IsDefaultClass: No
Annotations: ...
Provisioner: kubernetes.io/azure-disk
Parameters: kind=Managed,storageaccounttype=Premium_LRS
AllowVolumeExpansion: <unset>
MountOptions: <none>
ReclaimPolicy: Delete
VolumeBindingMode: Immediate
Events: <none>
我们还可以创建自己的存储类来动态创建持久卷。假设我们想要创建一个具有标准和高级类型的双区域冗余存储,我们需要指定位置和skuName:
// create storage class with Premium and Standard ZRS
# cat chapter12/storageclass.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: fastzrs
provisioner: kubernetes.io/azure-disk
parameters:
skuName: Premium_ZRS
location: centralus
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: slowzrs
provisioner: kubernetes.io/azure-disk
parameters:
skuName: Standard_ZRS
location: centralus
// create the storage class
# kubectl create -f chapter12/storageclass.yaml
storageclass.storage.k8s.io/fastzrs created
storageclass.storage.k8s.io/slowzrs created
skuName字段和存储类型的映射如下所示:
| Class | Replication | skuName |
|---|---|---|
| Premium | LRS | Premium_LRS |
| Premium | ZRS | Premium_ZRS |
| Standard | GRS | Standard_GRS |
| Standard | LRS | Standard_LRS |
| Standard | RAGRS | Standard_RAGRS |
| Standard | ZRS | Standard_ZRS |
L4 负载均衡器
当你在基础网络配置中创建 AKS 集群时,Azure 会在一个专用资源组中配置一个名为 kubernetes 的负载均衡器。当我们创建一个类型为 LoadBalancer 的服务时,AKS 会自动配置一个前端 IP 并将其与服务对象关联。这就是我们在本章之前使用外部 IP 访问nginx的方式。你可以在服务中设置一组注解,Azure 云控制器会根据指定的注解来配置它。你可以在这里找到支持的注解列表:github.com/kubernetes/kubernetes/blob/master/pkg/cloudprovider/providers/azure/azure_loadbalancer.go。
入口控制器
Azure 支持将 HTTP 应用程序路由作为附加组件(源代码可以在以下链接找到:github.com/Azure/application-gateway-kubernetes-ingress)。之前,我们使用http_application_routing under --enable-addons argument参数创建了集群。AKS 会部署一组服务以支持入口控制器:
// we could find a set of resources starting with the name addon-http-application-routing-...
# kubectl get pod -n kube-system
NAME READY STATUS RESTARTS AGE
addon-http-application-routing-default-http-backend-6584cdnpq69 1/1 Running 0 3h
addon-http-application-routing-external-dns-7dc8d9f794-4t28c 1/1 Running 0 3h
addon-http-application-routing-nginx-ingress-controller-78zs45d 1/1 Running 0 3h
...
你也可以通过az aks enable-addons命令在集群创建后启用入口控制器。
我们可以复用Chapter6中的入口示例。我们需要做的就是在ingress资源中指定annotationkubernetes.io/ingress.class: addon-http-application-routing,以及 AKS 为你路由 HTTP 应用程序所创建的主机名。你可以在创建或列出集群时,在addonProfiles.httpApplicationRouting.config.HTTPApplicationRoutingZoneName会话中找到主机名,或者通过使用az aks show --query命令进行查找:
// search the routing zone name.
# az aks show --resource-group devops --name myADAKS --query addonProfiles.httpApplicationRouting.config.HTTPApplicationRoutingZoneName
"46de1b27375a4f7ebf30.centralus.aksapp.io"
概述
微软 Azure 是一个强大且面向企业的云计算平台。除了 AKS,它还提供了多个领域的各种服务,如分析、虚拟现实等。在本章中,我们简单介绍了 Azure 虚拟网络、子网和负载均衡。我们还学习了如何在 Azure 中部署和管理 Kubernetes 服务。我们详细了解了 Azure 如何通过云控制器管理器提供 Kubernetes 资源。我们了解到,Azure 的云控制器管理器为 Azure 用户提供了无缝的体验,例如在请求 Kubernetes 中的 LoadBalancer 服务时创建 Azure 负载均衡器,或预先创建 Azure 磁盘存储类。
这是本书的最后一章。在这段 Kubernetes 学习旅程中,我们已经尝试涵盖了基础和更高级的概念。由于 Kubernetes 正在快速发展,我们强烈鼓励你加入 Kubernetes 社区 (kubernetes.io/community/),以获得灵感、进行讨论并做出贡献!


浙公网安备 33010602011771号