Kubernetes-DevOps-手册(全)

Kubernetes DevOps 手册(全)

原文:zh.annas-archive.org/md5/55C804BD2C19D0AE8370F4D1F28719E7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书将带您学习 DevOps、容器和 Kubernetes 的基本概念和有用技能的旅程。

本书涵盖的内容

第一章《DevOps 简介》带您了解了从过去到今天我们所说的 DevOps 的演变以及您应该了解的工具。近几年对具有 DevOps 技能的人的需求一直在迅速增长。它加速了软件开发和交付速度,也帮助了业务的敏捷性。

第二章《使用容器进行 DevOps》帮助您学习基本概念和容器编排。随着微服务的趋势,容器已成为每个 DevOps 的便捷和必要工具,因为它具有语言不可知的隔离性。

第三章《使用 Kubernetes 入门》探讨了 Kubernetes 中的关键组件和 API 对象,以及如何在 Kubernetes 集群中部署和管理容器。Kubernetes 通过许多强大的功能(如容器扩展、挂载存储系统和服务发现)简化了容器编排的痛苦。

第四章《存储和资源管理》描述了卷管理,并解释了 Kubernetes 中的 CPU 和内存管理。在集群中进行容器存储管理可能很困难。

第五章《网络和安全》解释了如何允许入站连接访问 Kubernetes 服务,以及 Kubernetes 中默认网络的工作原理。对我们的服务进行外部访问对业务需求是必要的。

第六章《监控和日志记录》向您展示如何使用 Prometheus 监视应用程序、容器和节点级别的资源使用情况。本章还展示了如何从您的应用程序以及 Kubernetes 中收集日志,以及如何使用 Elasticsearch、Fluentd 和 Kibana 堆栈。确保服务正常运行和健康是 DevOps 的主要责任之一。

第七章,持续交付,解释了如何使用 GitHub/DockerHub/TravisCI 构建持续交付管道。它还解释了如何管理更新,消除滚动更新时可能的影响,并防止可能的失败。持续交付是加快上市时间的一种方法。

第八章,集群管理,描述了如何使用 Kubernetes 命名空间和 ResourceQuota 解决前述问题,以及如何在 Kubernetes 中进行访问控制。建立管理边界和对 Kubernetes 集群进行访问控制对 DevOps 至关重要。

第九章,AWS 上的 Kubernetes,解释了 AWS 组件,并展示了如何在 AWS 上部署 Kubernetes。AWS 是最受欢迎的公共云。它为我们的世界带来了基础设施的灵活性和敏捷性。

第十章,GCP 上的 Kubernetes,帮助您了解 GCP 和 AWS 之间的区别,以及从 Kubernetes 的角度来看在托管服务中运行容器化应用的好处。GCP 中的 Google 容器引擎是 Kubernetes 的托管环境。

第十一章,接下来是什么?,介绍了其他类似的技术,如 Docker Swarm 模式、Amazon ECS 和 Apache Mesos,您将了解哪种方法对您的业务最为合适。Kubernetes 是开放的。本章将教您如何与 Kubernetes 社区联系,学习他人的想法。

本书所需内容

本书将指导您通过 macOS 和公共云(AWS 和 GCP)使用 Docker 容器和 Kubernetes 进行软件开发和交付的方法论。您需要安装 minikube、AWSCLI 和 Cloud SDK 来运行本书中的代码示例。

本书的受众

本书适用于具有一定软件开发经验的 DevOps 专业人士,他们愿意将软件交付规模化、自动化并缩短上市时间。

惯例

在本书中,您将找到许多区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。

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

任何命令行输入或输出都是这样写的:

$ sudo yum -y -q install nginx
$ sudo /etc/init.d/nginx start
Starting nginx: 

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中出现,就像这样:"本书中的快捷键基于 Mac OS X 10.5+方案。"

警告或重要提示会出现在这样的地方。提示和技巧会出现在这样的地方。

第一章:DevOps 简介

软件交付周期变得越来越短,而另一方面,应用程序的大小却变得越来越大。软件开发人员和 IT 运营商面临着找到解决方案的压力。有一个新的角色,称为DevOps,专门支持软件构建和交付。

本章涵盖以下主题:

  • 软件交付方法论如何改变?

  • 什么是微服务,为什么人们采用这种架构?

  • DevOps 如何支持构建和交付应用程序给用户?

软件交付挑战

构建计算机应用程序并将其交付给客户已经被讨论并随着时间的推移而发展。它与软件开发生命周期SDLC)有关;有几种类型的流程、方法论和历史。在本节中,我们将描述其演变。

瀑布和物理交付

回到 20 世纪 90 年代,软件交付采用了物理方法,如软盘或 CD-ROM。因此,SDLC 是一个非常长期的时间表,因为很难(重新)交付给客户。

那时,一个主要的软件开发方法论是瀑布模型,它具有如下图所示的需求/设计/实施/验证/维护阶段:

在这种情况下,我们不能回到以前的阶段。例如,在开始或完成实施阶段后,不可接受返回到设计阶段(例如查找技术可扩展性问题)。这是因为它会影响整体进度和成本。项目倾向于继续并完成发布,然后进入下一个发布周期,包括新设计。

它完全符合物理软件交付,因为它需要与物流管理协调,压制并交付软盘/CD-ROM 给用户。瀑布模型和物理交付过去需要一年到几年的时间。

敏捷和电子交付

几年后,互联网被广泛接受,然后软件交付方法也从物理转变为电子,如在线下载。因此,许多软件公司(也被称为点 com 公司)试图找出如何缩短 SDLC 流程,以交付能够击败竞争对手的软件。

许多开发人员开始采用增量、迭代或敏捷模型等新方法,以更快地交付给客户。即使发现新的错误,现在也更容易通过电子交付更新并交付给客户。自 Windows 98 以来,微软 Windows 更新也被引入。

在这种情况下,软件开发人员只编写一个小的逻辑或模块,而不是一次性编写整个应用程序。然后,交付给质量保证,然后开发人员继续添加新模块,最终再次交付给质量保证。

当所需的模块或功能准备就绪时,将按照以下图表释放:

这种模式使得软件开发生命周期和交付变得更快,也更容易在过程中进行调整,因为周期从几周到几个月,足够小以便进行快速调整。

尽管这种模式目前受到大多数人的青睐,但在当时,应用软件交付意味着软件二进制,如可安装并在客户 PC 上运行的 EXE 程序。另一方面,基础设施(如服务器和网络)非常静态并且事先设置。因此,软件开发生命周期并不倾向于将这些基础设施纳入范围之内。

云端软件交付

几年后,智能手机(如 iPhone)和无线技术(如 Wi-Fi 和 4G 网络)得到了广泛的接受,软件应用也从二进制转变为在线服务。Web 浏览器是应用软件的界面,不再需要安装。另一方面,基础设施变得动态起来,因为应用需求不断变化,容量也需要增长。

虚拟化技术和软件定义网络(SDN)使服务器机器变得动态。现在,云服务如亚马逊网络服务(AWS)和谷歌云平台(GCP)可以轻松创建和管理动态基础设施。

现在,基础设施是重要组成部分之一,并且在软件开发交付周期的范围内,因为应用程序安装并在服务器端运行,而不是在客户端 PC 上运行。因此,软件和服务交付周期需要花费几天到几周的时间。

持续集成

正如之前讨论的,周围的软件交付环境不断变化;然而,交付周期变得越来越短。为了实现更高质量的快速交付,开发人员和质量保证人员开始采用一些自动化技术。其中一种流行的自动化技术是持续集成CI)。CI 包含一些工具的组合,如版本控制系统VCS)、构建服务器测试自动化工具

VCS 帮助开发人员将程序源代码维护到中央服务器上。它可以防止覆盖或与其他开发人员的代码冲突,同时保留历史记录。因此,它使得源代码保持一致并交付到下一个周期变得更容易。

与 VCS 一样,有一个集中的构建服务器,它连接 VCS 定期检索源代码,或者当开发人员更新代码到 VCS 时自动触发新的构建。如果构建失败,它会及时通知开发人员。因此,当有人将有问题的代码提交到 VCS 时,它有助于开发人员。

测试自动化工具也与构建服务器集成,构建成功后调用单元测试程序,然后将结果通知给开发人员和质量保证人员。它有助于识别当有人编写有错误的代码并存储到 VCS 时。

CI 的整个流程如下图所示:

CI 不仅有助于开发人员和质量保证人员提高质量,还有助于缩短应用程序或模块包的归档周期。在电子交付给客户的时代,CI 已经远远不够了。然而,因为交付给客户意味着部署到服务器。

持续交付

CI 加上部署自动化是为服务器应用程序提供服务给客户的理想流程。然而,还有一些技术挑战需要解决。如何将软件交付到服务器?如何优雅地关闭现有应用程序?如何替换和回滚应用程序?如果系统库也需要更改,如何升级或替换?如果需要,如何修改操作系统中的用户和组设置?等等。

由于基础设施包括服务器和网络,一切都取决于诸如 Dev/QA/staging/production 之类的环境。每个环境都有不同的服务器配置和 IP 地址。

持续交付CD)是一种可以实现的实践;它是 CI 工具、配置管理工具和编排工具的组合:

配置管理

配置管理工具帮助配置操作系统,包括用户、组和系统库,并管理多个服务器,使其与期望的状态或配置保持一致,如果我们替换服务器。

它不是一种脚本语言,因为脚本语言执行基于脚本逐行执行命令。如果我们执行脚本两次,可能会导致一些错误,例如尝试两次创建相同的用户。另一方面,配置管理关注状态,所以如果用户已经创建,配置管理工具就不会做任何事情。但是如果我们意外或有意删除用户,配置管理工具将再次创建用户。

它还支持将应用程序部署或安装到服务器。因为如果您告诉配置管理工具下载您的应用程序,然后设置并运行应用程序,它会尝试这样做。

此外,如果您告诉配置管理工具关闭您的应用程序,然后下载并替换为新的软件包(如果有的话),然后重新启动应用程序,它将保持最新版本。

当然,一些用户只希望在需要时更新应用程序,比如蓝绿部署。配置管理工具也允许您手动触发执行。

蓝绿部署是一种技术,它准备了两套应用程序堆栈,然后只有一个环境(例如:蓝色)提供生产服务。然后当您需要部署新版本的应用程序时,部署到另一侧(例如:绿色),然后进行最终测试。然后如果一切正常,更改负载均衡器或路由器设置,将网络流从蓝色切换到绿色。然后绿色成为生产环境,而蓝色变为休眠状态,等待下一个版本的部署。

基础设施即代码

配置管理工具不仅支持操作系统或虚拟机,还支持云基础架构。如果您需要在云上创建和配置网络、存储和虚拟机,就需要进行一些云操作。

但是配置管理工具还可以通过配置文件自动设置云基础架构,如下图所示:

配置管理在维护操作手册(SOP)方面具有一些优势。例如,使用 Git 等版本控制系统维护配置文件,可以追踪环境设置的变化历史。

环境也很容易复制。例如,您需要在云上增加一个额外的环境。如果按照传统方法(即阅读 SOP 文档来操作云),总是存在潜在的人为错误和操作错误。另一方面,我们可以执行配置管理工具,快速自动地在云上创建一个环境。

基础设施即代码可能包含在持续交付过程中,因为基础设施的替换或更新成本比仅仅在服务器上替换应用程序二进制文件要高。

编排

编排工具也被归类为配置管理工具之一。然而,当配置和分配云资源时,它更加智能和动态。例如,编排工具管理多个服务器资源和网络,然后当管理员想要增加应用程序实例时,编排工具可以确定一个可用的服务器,然后自动部署和配置应用程序和网络。

尽管编排工具超出了 SDLC 的范围,但在需要扩展应用程序和重构基础设施资源时,它有助于持续交付。

总的来说,SDLC 已经通过多种流程、工具和方法演变,以实现快速交付。最终,软件(服务)交付需要花费几个小时到一天的时间。与此同时,软件架构和设计也在不断演进,以实现大型和丰富的应用程序。

微服务的趋势

软件架构和设计也在不断演进,基于目标环境和应用程序规模的大小。

模块化编程

当应用程序规模变大时,开发人员尝试将其分成几个模块。每个模块应该是独立和可重用的,并且应该由不同的开发团队维护。然后,当我们开始实施一个应用程序时,应用程序只需初始化并使用这些模块来高效地构建一个更大的应用程序。

以下示例显示了 Nginx(www.nginx.com)在 CentOS 7 上使用的库。它表明 Nginx 使用了OpenSSLPOSIX 线程库、PCRE正则表达式库、zlib压缩库、GNU C库等。因此,Nginx 没有重新实现 SSL 加密、正则表达式等:

$ /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命令包含在 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依赖项时所得到的结果:

模块化编程有助于提高软件开发速度并减少重复劳动,因此现在是开发软件应用程序的最流行方式。

然而,随着我们不断添加功能和逻辑,应用程序需要越来越多的模块、软件包和框架的组合。这使得应用程序变得更加复杂和庞大,特别是服务器端应用程序。这是因为它通常需要连接到诸如关系型数据库(RDBMS)之类的数据库,以及诸如 LDAP 之类的身份验证服务器,然后通过适当的设计以 HTML 形式将结果返回给用户。

因此,开发人员采用了一些软件设计模式,以便在应用程序中开发一堆模块。

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 定义了每一层之间的边界线,允许许多开发人员共同开发同一个应用程序。然而,这也会带来副作用。也就是说,应用程序中的源代码大小不断增加。这是因为数据库代码(模型)、展示代码(视图)和业务逻辑(控制器)都在同一个版本控制系统存储库中。最终会对软件开发周期产生影响,使其变得更慢!这被称为单片式,其中包含了构建巨大的 exe/war 程序的大量代码。

单片式应用程序

单片式应用程序的定义没有明确的衡量标准,但通常具有超过 50 个模块或包,超过 50 个数据库表,然后需要超过 30 分钟的构建时间。当需要添加或修改一个模块时,会影响大量代码,因此开发人员试图最小化应用程序代码的更改。这种犹豫会导致更糟糕的影响,有时甚至会导致应用程序因为没有人愿意再维护代码而死掉。

因此,开发人员开始将单片式应用程序分割成小的应用程序片段,并通过网络连接起来。

远程过程调用

实际上,将应用程序分成小块并通过网络连接已经尝试过了,早在 1990 年代。Sun Microsystems 推出了Sun RPC远程过程调用)。它允许您远程使用模块。其中一个流行的 Sun RPC 实现者是网络文件系统NFS)。因为它们基于 Sun RPC,NFS 客户端和 NFS 服务器之间的 CPU 操作系统版本是独立的。

编程语言本身也支持 RPC 风格的功能。UNIX 和 C 语言都有rpcgen工具。它帮助开发人员生成存根代码,负责网络通信代码,因此开发人员可以使用 C 函数风格,免除了困难的网络层编程。

Java 有Java 远程方法调用RMI),它类似于 Sun RPC,但对于 Java,RMI 编译器rmic)生成连接远程 Java 进程以调用方法并获取结果的存根代码。下图显示了 Java RMI 的过程流程:

Objective C 也有分布式对象,.NET 有远程调用,因此大多数现代编程语言都具有开箱即用的远程过程调用功能。

这些远程过程调用设计的好处是将应用程序分成多个进程(程序)。各个程序可以有单独的源代码存储库。尽管在 1990 年代和 2000 年代机器资源(CPU、内存)有限,但它仍然运行良好。

然而,它的设计意图是使用相同的编程语言,并且设计为客户端/服务器模型架构,而不是分布式架构。此外,安全性考虑较少;因此,不建议在公共网络上使用。

在 2000 年代,出现了一个名为web 服务的倡议,它使用SOAP(HTTP/SSL)作为数据传输,使用 XML 作为数据呈现和服务定义的Web 服务描述语言WSDL),然后使用通用描述、发现和集成UDDI)作为服务注册表来查找 web 服务应用程序。然而,由于机器资源不丰富,以及 Web 服务编程和可维护性的复杂性,它并未被开发人员广泛接受。

RESTful 设计

进入 2010 年代,现在机器性能甚至智能手机都有大量的 CPU 资源,加上到处都有几百 Mbps 的网络带宽。因此,开发人员开始利用这些资源,使应用程序代码和系统结构尽可能简单,从而加快软件开发周期。

基于硬件资源,使用 HTTP/SSL 作为 RPC 传输是一个自然的决定,但是根据开发人员对 Web 服务困难的经验,开发人员将其简化如下:

  • 通过将 HTTP 和 SSL/TLS 作为标准传输

  • 通过使用 HTTP 方法进行创建/加载/上传/删除(CLUD)操作,例如GET/POST/PUT/DELETE

  • 通过使用 URI 作为资源标识符,例如:用户 ID 123 作为/user/123/

  • 通过使用 JSON 作为标准数据呈现

它被称为RESTful设计,并且已被许多开发人员广泛接受,成为分布式应用程序的事实标准。RESTful 应用程序允许任何编程语言,因为它基于 HTTP,因此 RESTful 服务器是 Java,客户端 Python 是非常自然的。

它为开发人员带来了自由和机会,易于进行代码重构,升级库甚至切换到另一种编程语言。它还鼓励开发人员通过多个 RESTful 应用构建分布式模块化设计,这被称为微服务。

如果有多个 RESTful 应用程序,就会关注如何在 VCS 上管理多个源代码以及如何部署多个 RESTful 服务器。然而,持续集成和持续交付自动化使构建和部署多个 RESTful 服务器应用程序变得更加容易。

因此,微服务设计对 Web 应用程序开发人员变得越来越受欢迎。

微服务

尽管名称是微服务,但与 20 世纪 90 年代或 2000 年代的应用程序相比,它实际上足够复杂。它使用完整的 HTTP/SSL 服务器并包含整个 MVC 层。微服务设计应关注以下主题:

  • 无状态:这不会将用户会话存储到系统中,这有助于更容易地扩展。

  • 没有共享数据存储:微服务应该拥有数据存储,比如数据库。它不应该与其他应用程序共享。这有助于封装后端数据库,使单个微服务内的数据库方案易于重构和更新。

  • 版本控制和兼容性:微服务可能会更改和更新 API,但应定义一个版本,并且应具有向后兼容性。这有助于解耦其他微服务和应用程序之间的关系。

  • 集成 CI/CD:微服务应采用 CI 和 CD 流程来消除管理工作。

有一些框架可以帮助构建微服务应用程序,比如 Spring Boot (projects.spring.io/spring-boot/))和 Flask (flask.pocoo.org))。然而,有许多基于 HTTP 的框架,因此开发人员可以随意尝试和选择任何喜欢的框架甚至编程语言。这就是微服务设计的美妙之处。

下图是单块应用程序设计和微服务设计的比较。它表明微服务(也是 MVC)设计与单块设计相同,包含接口层、业务逻辑层、模型层和数据存储。

但不同的是,应用程序(服务)由多个微服务构成,不同的应用程序可以共享相同的微服务。

开发人员可以使用快速软件交付方法添加必要的微服务并修改现有的微服务,而不会再影响现有应用程序(服务)。

这是对整个软件开发环境和方法论的突破,现在得到了许多开发人员的广泛接受。

尽管持续集成和持续交付自动化流程有助于开发和部署多个微服务,但资源数量和复杂性,如虚拟机、操作系统、库和磁盘容量以及网络,无法与单块应用程序相比。

因此,有一些工具和角色可以支持云上的大型自动化环境。

自动化和工具

如前所述,自动化是实现快速软件交付的最佳实践,并解决了管理许多微服务的复杂性。然而,自动化工具并不是普通的 IT/基础架构应用程序,比如Active DirectoryBIND(DNS)和Sendmail(MTA)。为了实现自动化,需要一名工程师具备开发人员的技能集,能够编写代码,特别是脚本语言,以及基础设施操作员的技能集,比如虚拟机、网络和存储。

DevOps 是开发运维的缩合词,可以具有使自动化流程成为可能的能力,例如持续集成、基础设施即代码和持续交付。DevOps 使用一些 DevOps 工具来实现这些自动化流程。

持续集成工具

其中一种流行的版本控制工具是 Git(git-scm.com)。开发人员始终使用 Git 来签入和签出代码。有一些托管 Git 服务:GitHub(github.com))和 Bitbucket(bitbucket.org)。它允许您创建和保存您的 Git 存储库,并与其他用户协作。以下截图是 GitHub 上的示例拉取请求:

构建服务器有很多变化。Jenkins(jenkins.io)是一个成熟的应用程序之一,与 TeamCity(www.jetbrains.com/teamcity/))相同。除了构建服务器,您还可以使用托管服务,如 Codeship(codeship.com))和 Travis CI(travis-ci.org))等软件即服务(SaaS)。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/)提供了一个托管的 Chef 平台。以下截图是使用 AWS OpsWorks 安装 Amazon CloudWatch 日志代理的 Chef 配方(配置)。它在启动 EC2 实例时自动安装 CloudWatch 日志代理:

AWS CloudFormation (aws.amazon.com/cloudformation/)) 帮助实现基础架构即代码。它支持 AWS 操作的自动化,例如执行以下功能:

  1. 创建一个 VPC。

  2. 在 VPC 上创建一个子网。

  3. 在 VPC 上创建一个互联网网关。

  4. 创建路由表以将子网与互联网网关关联。

  5. 创建一个安全组。

  6. 创建一个 VM 实例。

  7. 将安全组与 VM 实例关联。

CloudFormation 的配置是通过 JSON 编写的,如下截图所示:

它支持参数化,因此可以使用具有相同配置的 JSON 文件轻松创建具有不同参数(例如 VPC 和 CIDR)的附加环境。此外,它支持更新操作。因此,如果需要更改基础架构的某个部分,无需重新创建。CloudFormation 可以识别配置的增量并代表您执行必要的基础架构操作。

AWS CodeDeploy (aws.amazon.com/codedeploy/)) 也是一个有用的自动化工具。但专注于软件部署。它允许用户定义。以下是一些操作到 YAML 文件上:

  1. 在哪里下载和安装。

  2. 如何停止应用程序。

  3. 如何安装应用程序。

  4. 安装后,如何启动和配置应用程序。

以下截图是 AWS CodeDeploy 配置文件appspec.yml的示例:

监控和日志工具

一旦您开始使用云基础架构管理一些微服务,就会有一些监控工具帮助您管理服务器。

Amazon CloudWatch 是 AWS 上内置的监控工具。不需要安装代理;它会自动从 AWS 实例中收集一些指标并为 DevOps 可视化。它还支持根据您设置的条件设置警报。以下截图是 EC2 实例的 Amazon CloudWatch 指标:

Amazon 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 使用率和网络流量。与关系型数据库不同,时间序列数据库对于节省数据空间和更快地查询数字数据历史具有一些优化。大多数 DevOps 监控工具在后端使用时间序列数据库。

以下示例是一个显示消息队列服务器统计信息的 Grafana:

沟通工具

一旦您开始像我们之前看到的那样使用多个 DevOps 工具,您需要来回访问多个控制台,以检查 CI 和 CD 流水线是否正常工作。例如,请考虑以下几点:

  1. 将源代码合并到 GitHub。

  2. 在 Jenkins 上触发新构建。

  3. 触发 AWS CodeDeploy 部署应用程序的新版本。

这些事件需要按时间顺序跟踪,如果出现问题,DevOps 需要与开发人员和质量保证讨论处理情况。然而,由于 DevOps 需要逐个捕捉事件然后解释,可能通过电子邮件,因此存在一些过度沟通的需求。这并不高效,同时问题仍在继续。

有一些沟通工具可以帮助集成这些 DevOps 工具,任何人都可以加入以查看事件并相互评论。Slack(slack.com)和 HipChat(www.hipchat.com)是最流行的沟通工具。

这些工具支持集成到 SaaS 服务,以便 DevOps 可以在单个聊天室中查看事件。以下截图是与 Jenkins 集成的 Slack 聊天室:

公共云

当与云技术一起使用时,CI CD 和自动化工作可以很容易实现。特别是公共云 API 帮助 DevOps 提出许多 CI CD 工具。亚马逊云服务(aws.amazon.com))和谷歌云平台(cloud.google.com))提供一些 API 给 DevOps 来控制云基础设施。DevOps 可以摆脱容量和资源限制,只需在需要资源时按需付费。

公共云将像软件开发周期和架构设计一样不断增长;它们是最好的朋友,也是实现应用/服务成功的重要关键。

以下截图是亚马逊云服务的网页控制台:

谷歌云平台也有一个网页控制台,如下所示:

这两种云服务都有一个免费试用期,DevOps 工程师可以使用它来尝试和了解云基础设施的好处。

总结

在本章中,我们讨论了软件开发方法论的历史,编程演变和 DevOps 工具。这些方法和工具支持更快的软件交付周期。微服务设计也有助于持续的软件更新。然而,微服务使环境管理变得复杂。

下一章将描述 Docker 容器技术,它有助于以更高效和自动化的方式组合微服务应用程序并进行管理。

第二章:使用容器的 DevOps

我们已经熟悉了许多 DevOps 工具,这些工具帮助我们自动化任务并在应用程序交付的不同阶段管理配置,但随着应用程序变得更加微小和多样化,仍然存在挑战。在本章中,我们将向我们的工具箱中添加另一把瑞士军刀,即容器。这样做,我们将寻求获得以下技能:

  • 容器概念和基础知识

  • 运行 Docker 应用程序

  • 使用Dockerfile构建 Docker 应用程序

  • 使用 Docker Compose 编排多个容器

理解容器

容器的关键特性是隔离。在本节中,我们将详细阐述容器是如何实现隔离的,以及为什么在软件开发生命周期中这一点很重要,以帮助建立对这个强大工具的正确理解。

资源隔离

当应用程序启动时,它会消耗 CPU 时间,占用内存空间,链接到其依赖库,并可能写入磁盘,传输数据包,并访问其他设备。它使用的一切都是资源,并且被同一主机上的所有程序共享。容器的理念是将资源和程序隔离到单独的盒子中。

您可能听说过诸如 para-virtualization、虚拟机(VMs)、BSD jails 和 Solaris 容器等术语,它们也可以隔离主机的资源。然而,由于它们的设计不同,它们在根本上是不同的,但提供了类似的隔离概念。例如,虚拟机的实现是为了使用 hypervisor 对硬件层进行虚拟化。如果您想在虚拟机上运行应用程序,您必须首先安装完整的操作系统。换句话说,在同一 hypervisor 上的客户操作系统之间的资源是隔离的。相比之下,容器是建立在 Linux 原语之上的,这意味着它只能在具有这些功能的操作系统中运行。BSD jails 和 Solaris 容器在其他操作系统上也以类似的方式工作。容器和虚拟机的隔离关系如下图所示。容器在操作系统层隔离应用程序,而基于 VM 的分离是通过操作系统实现的。

Linux 容器概念

容器由几个构建模块组成,其中最重要的两个是命名空间控制组cgroups)。它们都是 Linux 内核的特性。命名空间提供了对某些类型的系统资源的逻辑分区,例如挂载点(mnt)、进程 ID(PID)、网络(net)等。为了解释隔离的概念,让我们看一些关于 pid 命名空间的简单示例。以下示例均来自 Ubuntu 16.04.2 和 util-linux 2.27.1。

当我们输入 ps axf 时,会看到一个长长的正在运行的进程列表:

$ ps axf
 PID TTY      STAT   TIME COMMAND
    2 ?        S      0:00 [kthreadd]
    3 ?        S      0:42  \_ [ksoftirqd/0]
    5 ?        S<     0:00  \_ [kworker/0:0H]
    7 ?        S      8:14  \_ [rcu_sched]
    8 ?        S      0:00  \_ [rcu_bh]

ps 是一个报告系统上当前进程的实用程序。ps axf 是列出所有进程的命令。

现在让我们使用 unshare 进入一个新的 pid 命名空间,它能够逐部分将进程资源与新的命名空间分离,并再次检查进程:

$ 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   COMMAND
  ...
  25744 pts/0 \_ unshare --fork --pid --mount-proc=/proc    
  /bin/sh
 25745 pts/0    \_ /bin/sh
  3305  ?     /sbin/rpcbind -f -w
  6894  ?     /usr/sbin/ntpd -p /var/run/ntpd.pid -g -u  
  113:116
    ...

在新的命名空间中,您仍然可以看到其他进程和您的 shell 进程。

通过 pid 命名空间隔离,不同命名空间中的进程无法看到彼此。然而,如果一个进程占用了大量系统资源,比如内存,它可能会导致系统内存耗尽并变得不稳定。换句话说,一个被隔离的进程仍然可能干扰其他进程,甚至导致整个系统崩溃,如果我们不对其施加资源使用限制。

以下图表说明了 PID 命名空间以及一个内存不足OOM)事件如何影响子命名空间外的其他进程。气泡代表系统中的进程,数字代表它们的 PID。子命名空间中的进程有自己的 PID。最初,系统中仍然有可用的空闲内存。后来,子命名空间中的进程耗尽了系统中的所有内存。内核随后启动了 OOM killer 来释放内存,受害者可能是子命名空间外的进程:

鉴于此,cgroups 在这里被用来限制资源使用。与命名空间一样,它可以对不同类型的系统资源设置约束。让我们从我们的 pid 命名空间继续,用 yes > /dev/null 来压力测试 CPU,并用 top 进行监控:

$ yes > /dev/null & top
$ PID USER  PR  NI    VIRT   RES   SHR S  %CPU %MEM    
TIME+ COMMAND
 3 root  20   0    6012   656   584 R 100.0  0.0  
  0:15.15 yes
 1 root  20   0    4508   708   632 S   0.0  0.0                   
  0:00.00 sh
 4 root  20   0   40388  3664  3204 R   0.0  0.1  
  0:00.00 top

我们的 CPU 负载达到了预期的 100%。现在让我们使用 CPU cgroup 来限制它。Cgroups 组织为/sys/fs/cgroup/下的目录(首先切换到主机会话):

$ ls /sys/fs/cgroup
blkio        cpuset   memory            perf_event
cpu          devices  net_cls           pids
cpuacct      freezer  net_cls,net_prio  systemd
cpu,cpuacct  hugetlb  net_prio 

每个目录代表它们控制的资源。创建一个 cgroup 并控制进程非常容易:只需在资源类型下创建一个任意名称的目录,并将您想要控制的进程 ID 附加到tasks中。这里我们想要限制yes进程的 CPU 使用率,所以在cpu下创建一个新目录,并找出yes进程的 PID:

$ ps x | grep yes
11809 pts/2    R     12:37 yes

$ mkdir /sys/fs/cgroup/cpu/box && \
 echo 11809 > /sys/fs/cgroup/cpu/box/tasks

我们刚刚将yes添加到新创建的 CPU 组box中,但策略仍未设置,进程仍在没有限制地运行。通过将所需的数字写入相应的文件来设置限制,并再次检查 CPU 使用情况:

$ echo 50000 > /sys/fs/cgroup/cpu/box/cpu.cfs_quota_us
$ PID USER  PR  NI    VIRT   RES   SHR S  %CPU %MEM    
 TIME+ COMMAND
    3 root  20   0    6012   656   584 R  50.2  0.0     
    0:32.05 yes
    1 root  20   0    4508  1700  1608 S   0.0  0.0  
    0:00.00 sh
    4 root  20   0   40388  3664  3204 R   0.0  0.1  
    0:00.00 top

CPU 使用率显着降低,这意味着我们的 CPU 限制起作用了。

这两个例子阐明了 Linux 容器如何隔离系统资源。通过在应用程序中增加更多的限制,我们绝对可以构建一个完全隔离的盒子,包括文件系统和网络,而无需在其中封装操作系统。

容器化交付

为了部署应用程序,通常会使用配置管理工具。它确实可以很好地处理模块化和基于代码的配置设计,直到应用程序堆栈变得复杂和多样化。维护一个庞大的配置清单基础是复杂的。当我们想要更改一个软件包时,我们将不得不处理系统和应用程序软件包之间纠缠不清和脆弱的依赖关系。经常会出现在升级一个无关的软件包后一些应用程序意外中断的情况。此外,升级配置管理工具本身也是一项具有挑战性的任务。

为了克服这样的困境,引入了使用预先烘焙的虚拟机镜像进行不可变部署。也就是说,每当系统或应用程序包有任何更新时,我们将根据更改构建一个完整的虚拟机镜像,并相应地部署它。这解决了一定程度的软件包问题,因为我们现在能够为无法共享相同环境的应用程序定制运行时。然而,使用虚拟机镜像进行不可变部署是昂贵的。从另一个角度来看,为了隔离应用程序而不是资源不足而配置虚拟机会导致资源利用效率低下,更不用说启动、分发和运行臃肿的虚拟机镜像的开销了。如果我们想通过共享虚拟机来消除这种低效,很快就会意识到我们将遇到进一步的麻烦,即资源管理。

容器在这里是一个完美适应部署需求的拼图块。容器的清单可以在版本控制系统中进行管理,并构建成一个 blob 图像;毫无疑问,该图像也可以被不可变地部署。这使开发人员可以抽象出实际资源,基础设施工程师可以摆脱他们的依赖地狱。此外,由于我们只需要打包应用程序本身及其依赖库,其图像大小将明显小于虚拟机的。因此,分发容器图像比虚拟机更经济。此外,我们已经知道,在容器内运行进程基本上与在其 Linux 主机上运行是相同的,因此几乎不会产生额外开销。总之,容器是轻量级的、自包含的和不可变的。这也清晰地划定了应用程序和基础设施之间的责任边界。

开始使用容器。

有许多成熟的容器引擎,如 Docker(www.docker.com)和 rkt(coreos.com/rkt),它们已经实现了用于生产的功能,因此您无需从头开始构建一个。此外,由容器行业领导者组成的Open Container Initiativewww.opencontainers.org)已经制定了一些容器规范。这些标准的任何实现,无论基础平台如何,都应具有与 OCI 旨在提供的类似属性,以便在各种操作系统上无缝体验容器。在本书中,我们将使用 Docker(社区版)容器引擎来构建我们的容器化应用程序。

为 Ubuntu 安装 Docker

Docker 需要 Yakkety 16.10、Xenial 16.04LTS 和 Trusty 14.04LTS 的 64 位版本。您可以使用apt-get install docker.io安装 Docker,但它通常更新速度比 Docker 官方存储库慢。以下是来自 Docker 的安装步骤(docs.docker.com/engine/installation/linux/docker-ce/ubuntu/#install-docker-ce):

  1. 确保您拥有允许apt存储库的软件包;如果没有,请获取它们:
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common 
  1. 添加 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 
  1. 设置amd64架构的存储库:
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 
  1. 更新软件包索引并安装 Docker CE:
 $ sudo apt-get update 
 $ sudo apt-get install docker-ce

在 CentOS 上安装 Docker

需要 CentOS 7 64 位才能运行 Docker。同样,您可以通过sudo yum install docker从 CentOS 的存储库获取 Docker 软件包。同样,Docker 官方指南(docs.docker.com/engine/installation/linux/docker-ce/centos/#install-using-the-repository)中的安装步骤如下:

  1. 安装实用程序以启用yum使用额外的存储库:
    $ sudo yum install -y yum-utils  
  1. 设置 Docker 的存储库:
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 
  1. 更新存储库并验证指纹是否匹配:

060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35

    $ sudo yum makecache fast   
  1. 安装 Docker CE 并启动它:
$ sudo yum install docker-ce
$ sudo systemctl start docker 

为 macOS 安装 Docker

Docker 使用微型 Linux moby 和 Hypervisor 框架来在 macOS 上构建本机应用程序,这意味着我们不需要第三方虚拟化工具来开发 Mac 上的 Docker。要从 Hypervisor 框架中受益,您必须将您的 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 发行版。由于其体积非常小,许多人使用它作为构建应用程序容器的基础图像。

容器生命周期

使用容器并不像我们习惯的工具那样直观。在本节中,我们将从最基本的想法开始介绍 Docker 的用法,直到我们能够从容器中受益为止。

Docker 基础知识

当执行 docker run alpine ls 时,Docker 在幕后所做的是:

  1. 在本地找到图像 alpine。如果找不到,Docker 将尝试从公共 Docker 注册表中找到并将其拉取到本地图像存储中。

  2. 提取图像并相应地创建一个容器。

  3. 使用命令执行图像中定义的入口点,这些命令是图像名称后面的参数。在本例中,它是 ls。在基于 Linux 的 Docker 中,默认的入口点是 /bin/sh -c

  4. 当入口点进程退出时,容器也会退出。

图像是一组不可变的代码、库、配置和运行应用程序所需的一切。容器是图像的一个实例,在运行时实际上会被执行。您可以使用 docker inspect IMAGEdocker inspect CONTAINER 命令来查看区别。

有时,当我们需要进入容器检查镜像或在内部更新某些内容时,我们将使用选项-i-t--interactive--tty)。此外,选项-d--detach)使您可以以分离模式运行容器。如果您想与分离的容器进行交互,execattach命令可以帮助我们。exec命令允许我们在运行的容器中运行进程,而attach按照其字面意思工作。以下示例演示了如何使用它们:

$ docker run alpine /bin/sh -c "while :;do echo  
  'meow~';sleep 1;done"
meow~
meow~
...

您的终端现在应该被“喵喵喵”淹没了。切换到另一个终端并运行docker ps命令,以获取容器的状态,找出喵喵叫的容器的名称和 ID。这里的名称和 ID 都是由 Docker 生成的,您可以使用其中任何一个访问容器。为了方便起见,名称可以在createrun时使用--name标志进行分配:

$ docker ps
CONTAINER ID    IMAGE    (omitted)     NAMES
d51972e5fc8c    alpine      ...        zen_kalam

$ 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
  27 root       0:00 /bin/sh
  34 root       0:00 sleep 1
  35 root       0:00 ps
  / # kill -s 2 1
  $ // container terminated

一旦我们进入容器并检查其进程,我们会看到两个 shell:一个是喵喵叫,另一个是我们所在的位置。在容器内部使用kill -s 2 1杀死它,我们会看到整个容器停止,因为入口点已经退出。最后,让我们使用docker ps -a列出已停止的容器,并使用docker rm CONTAINER_NAMEdocker rm CONTAINER_ID清理它们。自 Docker 1.13 以来,引入了docker system prune命令,它可以帮助我们轻松清理已停止的容器和占用的资源。

层、镜像、容器和卷

我们知道镜像是不可变的;容器是短暂的,我们知道如何将镜像作为容器运行。然而,在打包镜像时仍然缺少一步。

镜像是一个只读的堆栈,由一个或多个层组成,而层是文件系统中的文件和目录的集合。为了改善磁盘使用情况,层不仅被锁定在一个镜像上,而且在镜像之间共享;这意味着 Docker 只在本地存储基础镜像的一个副本,而不管从它派生了多少镜像。您可以使用docker history [image]命令来了解镜像是如何构建的。例如,如果您键入docker history alpine,则 Alpine Linux 镜像中只有一个层。

每当创建一个容器时,它会在基础镜像的顶部添加一个可写层。Docker 在该层上采用了写时复制COW)策略。也就是说,容器读取存储目标文件的基础镜像的层,并且如果文件被修改,就会将文件复制到自己的可写层。这种方法可以防止从相同镜像创建的容器相互干扰。docker diff [CONTAINER] 命令显示容器与其基础镜像在文件系统状态方面的差异。例如,如果基础镜像中的 /etc/hosts 被修改,Docker 会将文件复制到可写层,并且在 docker diff 的输出中也只会有这一个文件。

以下图示了 Docker 镜像的层次结构:

需要注意的是,可写层中的数据会随着容器的删除而被删除。为了持久化数据,您可以使用 docker commit [CONTAINER] 命令将容器层提交为新镜像,或者将数据卷挂载到容器中。

数据卷允许容器的读写绕过 Docker 的文件系统,它可以位于主机的目录或其他存储中,比如 Ceph 或 GlusterFS。因此,对卷的磁盘 I/O 可以根据底层存储的实际速度进行操作。由于数据在容器外是持久的,因此可以被多个容器重复使用和共享。通过在 docker rundocker create 中指定 -v--volume)标志来挂载卷。以下示例在容器中挂载了一个卷到 /chest,并在其中留下一个文件。然后,我们使用 docker inspect 来定位数据卷:

$ docker run --name demo -v /chest alpine touch /chest/coins
$ docker inspect demo
...
"Mounts": [
 {
    "Type": "volume",
     "Name":(hash-digits),
     "Source":"/var/lib/docker/volumes/(hash- 
      digits)/_data",
      "Destination": "/chest",
      "Driver": "local",
      "Mode": "",
       ...
$ ls /var/lib/docker/volumes/(hash-digits)/_data
      coins

Docker CE 在 macOS 上提供的 moby 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
c53e3e498ab05b19a12d554fad4545310e6de6950240cf7a28f42780f382c649
$ docker run --name A --volumes-from box alpine touch /share-vol/wine
$ docker run --name B --volumes-from box alpine ls /share-vol
wine

此外,数据卷可以挂载在给定的主机路径下,当然其中的数据是持久的:

$ 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)中拉取的。当然,你也可以将你的工具上传到这样的服务并与所有人分享。

如果你需要一个私有注册表,但出于某种原因不想订阅注册表服务提供商的付费计划,你总是可以使用 registry (hub.docker.com/_/registry)在自己的计算机上设置一个。

在配置容器之前,Docker 将尝试在图像名称中指示的规则中定位指定的图像。图像名称由三个部分[registry/]name[:tag]组成,并根据以下规则解析:

  • 如果省略了registry字段,则在 Docker Hub 上搜索该名称

  • 如果registry字段是注册表服务器,则在其中搜索该名称

  • 名称中可以有多个斜杠

  • 如果省略了标记,则默认为latest

例如,图像名称gcr.io/google-containers/guestbook:v3指示 Docker 从gcr.io下载google-containers/guestbookv3版本。同样,如果你想将图像推送到注册表,也要以相同的方式标记你的图像并推送它。要列出当前在本地磁盘上拥有的图像,使用docker images,并使用docker rmi [IMAGE]删除图像。以下示例显示了如何在不同的注册表之间工作:从 Docker Hub 下载nginx图像,将其标记为私有注册表路径,并相应地推送它。请注意,尽管默认标记是latest,但你必须显式地标记和推送它。

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
ff3d52d8f55f: Pull complete
...
Status: Downloaded newer image for nginx:latest

$ docker tag nginx localhost:5000/comps/prod/nginx:1.14
$ docker push localhost:5000/comps/prod/nginx:1.14
The push refers to a repository [localhost:5000/comps/prod/nginx]
...
8781ec54ba04: Pushed
1.14: 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 a repository [localhost:5000/comps/prod/nginx]
...
8781ec54ba04: Layer already exists
latest: digest: sha256:(64-digits-hash) size: 948

大多数注册表服务在你要推送图像时都会要求进行身份验证。docker login就是为此目的而设计的。有时,当尝试拉取图像时,你可能会收到image not found error的错误,即使图像路径是有效的。这很可能是你未经授权访问保存图像的注册表。要解决这个问题,首先要登录:

$ docker pull localhost:5000/comps/prod/nginx
Pulling repository localhost:5000/comps/prod/nginx
Error: image comps/prod/nginx:latest not found
$ docker login -u letme -p in localhost:5000
Login Succeeded
$ docker pull localhost:5000/comps/prod/nginx
Pulling repository localhost:5000/comps/prod/nginx
...
latest: digest: sha256:(64-digits-hash) size: 948

除了通过注册表服务分发图像外,还有将图像转储为 TAR 存档文件,并将其导入到本地存储库的选项:

  • docker commit [CONTAINER]:将容器层的更改提交为新镜像

  • docker save --output [filename] IMAGE1 IMAGE2 ...:将一个或多个镜像保存到 TAR 存档中

  • docker load -i [filename]:将tarball镜像加载到本地存储库

  • docker export --output [filename] [CONTAINER]:将容器的文件系统导出为 TAR 存档

  • docker import --output [filename] IMAGE1 IMAGE2:导入文件系统tarball

commit命令与saveexport看起来基本相同。主要区别在于保存的镜像即使最终将被删除,也会保留层之间的文件;另一方面,导出的镜像将所有中间层压缩为一个最终层。另一个区别是保存的镜像保留元数据,例如层历史记录,但这些在导出的镜像中不可用。因此,导出的镜像通常体积较小。

以下图表描述了容器和镜像之间状态的关系。箭头上的标题是 Docker 的相应子命令:

连接容器

Docker 提供了三种网络类型来管理容器内部和主机之间的通信,即bridgehostnone

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1224183f2080        bridge              bridge              local
801dec6d5e30        host                host                local
f938cd2d644d        none                null                local

默认情况下,每个容器在创建时都连接到桥接网络。在这种模式下,每个容器都被分配一个虚拟接口和一个私有 IP 地址,通过该接口传输的流量被桥接到主机的docker0接口。此外,同一桥接网络中的其他容器可以通过它们的 IP 地址相互连接。让我们运行一个通过端口5000发送短消息的容器,并观察其配置。--expose标志将给定端口开放给容器外部的世界:

$ docker run --name greeter -d --expose 5000 alpine \
/bin/sh -c "echo Welcome stranger! | nc -lp 5000"
2069cbdf37210461bc42c2c40d96e56bd99e075c7fb92326af1ec47e64d6b344 $ docker exec greeter ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
...

在这里,容器greeter被分配了 IP172.17.0.2。现在运行另一个连接到该 IP 地址的容器:

$ docker run alpine 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标志给它另一个别名:

$ docker network create room
b0cdd64d375b203b24b5142da41701ad9ab168b53ad6559e6705d6f82564baea
$ docker run -d --network room \
--network-alias dad --name sleeper alpine sleep 60
b5290bcca85b830935a1d0252ca1bf05d03438ddd226751eea922c72aba66417
$ docker run --network room alpine ping -c 1 sleeper
PING sleeper (172.18.0.2): 56 data bytes
...
$ docker run --network room alpine ping -c 1 dad
PING dad (172.18.0.2): 56 data bytes
...

主机网络按照其名称的字面意思工作;每个连接的容器共享主机的网络,但同时失去了隔离属性。none 网络是一个完全分离的盒子。无论是入口还是出口,流量都在内部隔离,因为容器上没有网络接口。在这里,我们将一个监听端口5000的容器连接到主机网络,并在本地与其通信:

$ docker run -d --expose 5000 --network host alpine \
/bin/sh -c "echo im a container | nc -lp 5000"
ca73774caba1401b91b4b1ca04d7d5363b6c281a05a32828e293b84795d85b54
$ telnet localhost 5000
im a container
Connection closed by foreign host

如果您在 macOS 上使用 Docker CE,主机指的是 hypervisor 框架上的 moby Linux。

主机和三种网络模式之间的交互如下图所示。主机和桥接网络中的容器都连接了适当的网络接口,并与相同网络内的容器以及外部世界进行通信,但 none 网络与主机接口保持分离。

除了共享主机网络外,在创建容器时,标志-p(--publish) [host]:[container]还允许您将主机端口映射到容器。这个标志意味着-expose,因为您无论如何都需要打开容器的端口。以下命令在端口80启动一个简单的 HTTP 服务器。您也可以用浏览器查看它。

$ docker run -p 80:5000 alpine /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 守护程序形成一个 Docker 镜像。通常,Dockerfile是以指令FROM开头的,后面跟着零个或多个指令。例如,我们可以从以下一行指令构建一个镜像:

docker commit $(   \
docker start $(  \
docker create alpine /bin/sh -c    \
"echo My custom build > /etc/motd" \
 ))

它大致相当于以下Dockerfile

./Dockerfile:
---
FROM alpine
RUN echo "My custom build" > /etc/motd
---

显然,使用Dockerfile构建更加简洁和清晰。

docker build [OPTIONS] [CONTEXT]命令是与构建任务相关的唯一命令。上下文可以是本地路径、URL 或stdin;表示Dockerfile的位置。一旦触发构建,Dockerfile以及上下文中的所有内容将首先被发送到 Docker 守护程序,然后守护程序将开始按顺序执行Dockerfile中的指令。每次执行指令都会产生一个新的缓存层,随后的指令会在级联中的新缓存层上执行。由于上下文将被发送到不一定是本地路径的地方,将Dockerfile、代码、必要的文件和.dockerignore文件放在一个空文件夹中是一个良好的做法,以确保生成的镜像仅包含所需的文件。

.dockerignore文件是一个列表,指示在构建时可以忽略同一目录下的哪些文件,它通常看起来像下面的文件:

./.dockerignore:
---
# ignore .dockerignore, .git
.dockerignore 
.git
# exclude all *.tmp files and vim swp file recursively
/*.tmp
/[._]*.s[a-w][a-z]
...
---

通常,docker build将尝试在context下找到一个名为Dockerfile的文件来开始构建;但有时出于某些原因,我们可能希望给它另一个名称。-f--file)标志就是为了这个目的。另外,另一个有用的标志-t--tag)在构建完镜像后能够给一个或多个仓库标签。假设我们想要在./deploy下构建一个名为builder.dckDockerfile,并用当前日期和最新标签标记它,命令将是:

$ docker build -f deploy/builder.dck  \
-t my-reg.com/prod/teabreak:$(date +"%g%m%d") \
-t my-reg.com/prod/teabreak: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 -cENTRYPOINTCMD交互的另一个指令;实际上,三种CMD形式在容器启动时都会被ENTRYPOINT所覆盖。在Dockerfile中可以有多个CMD指令,但只有最后一个会生效。

  • ENTRYPOINT
ENTRYPOINT ["executable", "param1", "param2"] ENTRYPOINT command param1 param2

这两种形式分别是执行形式和 shell 形式,语法规则与RUN相同。入口点是图像的默认可执行文件。也就是说,当容器启动时,它会运行由ENTRYPOINT配置的可执行文件。当ENTRYPOINTCMDdocker 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以执行形式存在,并且只配置了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
    ENV key wD # aw
    ENV k2=v2 k3=v\ 3 \
        k4="v 4"
    ENV k_${k2}=$k3 k5=\"K\=da\"

    RUN echo key=$key ;\
       echo k2=$k2 k3=$k3 k4=$k4 ;\
       echo k_\${k2}=k_${k2}=$k3 k5=$k5

在 Docker 构建期间的输出将是:

    ...
    ---> Running in 738709ef01ad
    key=wD # aw
    k2=v2 k3=v 3 k4=v 4
    k_${k2}=k_v2=v 3 k5="K=da"
    ...
  • LABEL key1=value1 key2=value2 ...LABEL的用法类似于ENV,但标签仅存储在镜像的元数据部分,并由其他主机程序使用,而不是容器中的程序。它取代了以下形式的maintainer指令:
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指令切换用户以运行随后的指令。但是,如果用户在镜像中不存在,则无法正常工作。否则,在使用USER指令之前,您必须运行adduser

  • WORKDIR <path>:此指令将工作目录设置为特定路径。如果路径不存在,路径将被自动创建。它的工作原理类似于Dockerfile中的cd,因为它既可以接受相对路径也可以接受绝对路径,并且可以多次使用。如果绝对路径后面跟着一个相对路径,结果将相对于前一个路径:

    WORKDIR /usr
    WORKDIR src
    WORKDIR app
    RUN pwd
    ---> Running in 73aff3ae46ac
    /usr/src/app
    ---> 4a415e366388

此外,使用ENV设置的环境变量会影响路径。

  • COPY:
COPY <src-in-context> ... <dest-in-container> COPY ["<src-in-context>",... "<dest-in-container>"]

该指令将源复制到构建容器中的文件或目录。源可以是文件或目录,目的地也可以是文件或目录。源必须在上下文路径内,因为只有上下文路径下的文件才会被发送到 Docker 守护程序。此外,COPY利用.dockerignore来过滤将被复制到构建容器中的文件。第二种形式适用于路径包含空格的情况。

  • ADD
ADD <src > ... <dest >
ADD ["<src>",... "<dest >"]

ADD在功能上与COPY非常类似:将文件移动到镜像中。除了复制文件外,<src>也可以是 URL 或压缩文件。如果<src>是一个 URL,ADD将下载并将其复制到镜像中。如果<src>被推断为压缩文件,它将被提取到<dest>路径中。

  • VOLUME
VOLUME mount_point_1 mount_point_2 VOLUME ["mount point 1", "mount point 2"]

VOLUME指令在给定的挂载点创建数据卷。一旦在构建时声明了数据卷,后续指令对数据卷的任何更改都不会持久保存。此外,在Dockerfiledocker build中挂载主机目录是不可行的,因为存在可移植性问题:无法保证指定的路径在主机中存在。两种语法形式的效果是相同的;它们只在语法解析上有所不同;第二种形式是 JSON 数组,因此需要转义字符,如"\"

  • ONBUILD [其他指令]ONBUILD允许您将一些指令推迟到派生图像的后续构建中。例如,我们可能有以下两个 Dockerfiles:
    --- baseimg ---
    FROM alpine
    RUN apk add --no-update git make
    WORKDIR /usr/src/app
    ONBUILD COPY . /usr/src/app/
    ONBUILD RUN git submodule init && \
              git submodule update && \
              make
    --- appimg ---
    FROM baseimg
    EXPOSE 80
    CMD ["/usr/src/app/entry"]

然后,指令将按以下顺序在docker build中进行评估:

    $ docker build -t baseimg -f baseimg .
    ---
    FROM alpine
    RUN apk add --no-update git make
    WORKDIR /usr/src/app
    ---
    $ docker build -t appimg -f appimg .
    ---
    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 . /app
RUN apt-get update 
RUN apt-get upgrade -y
RUN apt-get install -y redis-server python python-pip mysql-server
ADD db/my.cnf /etc/mysql/my.cnf
ADD db/redis.conf /etc/redis/redis.conf
RUN pip install -r /app/requirements.txt
RUN cd /app ; python setup.py
CMD /app/start-all-service.sh

第一个建议是创建一个专门用于一件事情的容器。因此,我们将在这个Dockerfile的开头删除mysqlredis的安装和配置。接下来,代码将被移入容器中,使用ADD,这意味着我们很可能将整个代码库移入容器。通常有许多与应用程序直接相关的文件,包括 VCS 文件、CI 服务器配置,甚至构建缓存,我们可能不希望将它们打包到镜像中。因此,建议使用.dockerignore来过滤掉这些文件。顺便说一句,由于ADD指令,我们可以做的不仅仅是将文件添加到构建容器中。通常情况下,使用COPY更为合适,除非确实有不这样做的真正需要。现在我们的Dockerfile更简单了,如下面的代码所示:

FROM ubuntu
COPY . /app
RUN apt-get update 
RUN apt-get upgrade -y
RUN apt-get install -y python python-pip
RUN pip install -r /app/requirements.txt
RUN cd /app ; python setup.py
CMD python app.py

在构建镜像时,Docker 引擎将尽可能地重用缓存层,这显著减少了构建时间。在我们的Dockerfile中,只要存储库有任何更新,我们就必须经历整个更新和依赖项安装过程。为了从构建缓存中受益,我们将根据一个经验法则重新排序指令:首先运行不太频繁的指令。

另外,正如我们之前所描述的,对容器文件系统的任何更改都会导致新的镜像层。即使我们在随后的层中删除了某些文件,这些文件仍然占用着镜像大小,因为它们仍然保存在中间层。因此,我们的下一步是通过简单地压缩多个RUN指令来最小化镜像层。此外,为了保持Dockerfile的可读性,我们倾向于使用行继续字符“\”格式化压缩的RUN

除了与 Docker 的构建机制一起工作之外,我们还希望编写一个可维护的Dockerfile,使其更清晰、可预测和稳定。以下是一些建议:

  • 使用WORKDIR而不是内联cd,并为WORKDIR使用绝对路径。

  • 明确公开所需的端口

  • 为基础镜像指定标签

  • 使用执行形式启动应用程序

前三个建议非常直接,旨在消除歧义。最后一个建议是关于应用程序如何终止。当来自 Docker 守护程序的停止请求发送到正在运行的容器时,主进程(PID 1)将接收到一个停止信号(SIGTERM)。如果进程在一定时间后仍未停止,Docker 守护程序将发送另一个信号(SIGKILL)来终止容器。在这里,exec 形式和 shell 形式有所不同。在 shell 形式中,PID 1 进程是"/bin/sh -c",而不是应用程序。此外,不同的 shell 处理信号的方式也不同。有些将停止信号转发给子进程,而有些则不会。Alpine Linux 的 shell 不会转发它们。因此,为了正确停止和清理我们的应用程序,建议使用exec形式。结合这些原则,我们有以下Dockerfile

FROM ubuntu:16.04
RUN apt-get update && apt-get upgrade -y  \
&& apt-get install -y python python-pip
ENTRYPOINT ["python"]
CMD ["entry.py"]
EXPOSE 5000
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /app 

还有其他一些实践可以使Dockerfile更好,包括从专用和更小的基础镜像开始,例如基于 Alpine 的镜像,而不是通用目的的发行版,使用除root之外的用户以提高安全性,并在RUN中删除不必要的文件。

多容器编排

随着我们将越来越多的应用程序打包到隔离的容器中,我们很快就会意识到我们需要一种工具,能够帮助我们同时处理多个容器。在这一部分,我们将从仅仅启动单个容器上升一步,开始编排一组容器。

堆叠容器

现代系统通常构建为由多个组件组成的堆栈,这些组件分布在网络上,如应用服务器、缓存、数据库、消息队列等。同时,一个组件本身也是一个包含许多子组件的自包含系统。此外,微服务的趋势为系统之间纠缠不清的关系引入了额外的复杂性。由于这个事实,即使容器技术在部署任务方面给了我们一定程度的缓解,启动一个系统仍然很困难。

假设我们有一个名为 kiosk 的简单应用程序,它连接到 Redis 来管理我们当前拥有的门票数量。一旦门票售出,它会通过 Redis 频道发布一个事件。记录器订阅了 Redis 频道,并在接收到任何事件时将时间戳日志写入 MySQL 数据库。

对于kioskrecorder,你可以在这里找到代码以及 Dockerfiles:github.com/DevOps-with-Kubernetes/examples/tree/master/chapter2。架构如下:

我们知道如何分别启动这些容器,并将它们连接在一起。基于我们之前讨论的内容,我们首先会创建一个桥接网络,并在其中运行容器:


$ docker network create kiosk
$ docker run -d -p 5000:5000 \
    -e REDIS_HOST=lcredis --network=kiosk kiosk-example 
$ docker run -d --network-alias lcredis --network=kiosk redis
$ docker run -d -e REDIS_HOST=lcredis -e MYSQL_HOST=lmysql \
-e MYSQL_ROOT_PASSWORD=$MYPS -e MYSQL_USER=root \
--network=kiosk recorder-example
$ docker run -d --network-alias lmysql -e MYSQL_ROOT_PASSWORD=$MYPS \ 
 --network=kiosk mysql:5.7 

到目前为止一切都运行良好。然而,如果下次我们想再次启动相同的堆栈,我们的应用很可能会在数据库之前启动,并且如果有任何传入连接请求对数据库进行任何更改,它们可能会失败。换句话说,我们必须在启动脚本中考虑启动顺序。此外,脚本还存在一些问题,比如如何处理随机组件崩溃,如何管理变量,如何扩展某些组件等等。

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 "cwd_default" with the default driver
Creating cwd_hello-world_1
Attaching to cwd_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.
...
cwd_hello-world_1 exited with code 0

让我们看看docker-composeup命令后面做了什么。

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将使用默认驱动程序创建一个新网络,并将服务连接到与示例输出中的 1 到 3 行相同的网络。

此外,容器的网络名称将是服务的名称。您可能会注意到控制台中显示的名称与docker-compose.yml中的原始名称略有不同。这是因为 Docker Compose 尝试避免容器之间的名称冲突。因此,Docker Compose 使用生成的名称运行容器,并使用服务名称创建网络别名。在此示例中,hello-worldcwd_hello-world_1都可以在同一网络中解析到其他容器。

组合容器

由于 Docker Compose 在许多方面与 Docker 相同,因此更有效的方法是了解如何使用示例编写docker-compose.yml,而不是从docker-compose语法开始。现在让我们回到之前的kiosk-example,并从version定义和四个services开始:

version: '3'
services:
 kiosk-example:
 recorder-example:
 lcredis:
 lmysql:

kiosk-exampledocker run参数非常简单,包括发布端口和环境变量。在 Docker Compose 方面,我们相应地填写源镜像、发布端口和环境变量。因为 Docker Compose 能够处理docker build,如果本地找不到这些镜像,它将构建镜像。我们很可能希望利用它来进一步减少镜像管理的工作量。

kiosk-example:
 image: kiosk-example
 build: ./kiosk
 ports:
  - "5000:5000"
  environment:
    REDIS_HOST: lcredis

以相同的方式转换recorder-exampleredis的 Docker 运行,我们得到了以下模板:

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 部分,它需要一个数据卷来保存数据以及配置。因此,除了lmysql部分之外,我们在services级别添加volumes,并添加一个空映射mysql-vol来声明一个数据卷:

 lmysql:
 image: mysql:5.7
   environment:
     MYSQL_ROOT_PASSWORD: mysqlpass
   volumes:
   - mysql-vol:/var/lib/mysql
   ports:
   - "3306"
  ---
volumes:
  mysql-vol:

结合所有前述的配置,我们得到了最终的模板,如下所示:

docker-compose.yml
---
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"
 lmysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: mysqlpass
    volumes:
    - mysql-vol:/var/lib/mysql
    ports:
    - "3306"
volumes:
 mysql-vol: 

该文件放在项目的根文件夹中。相应的文件树如下所示:

├── docker-compose.yml
├── kiosk
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
└── recorder
 ├── Dockerfile
 ├── process.py
 └── requirements.txt  

最后,运行docker-compose up来检查一切是否正常。我们可以通过发送GET /tickets请求来检查我们的售票亭是否正常运行。

编写 Docker Compose 的模板就是这样简单。现在我们可以轻松地在堆栈中运行应用程序。

总结

从 Linux 容器的最原始元素到 Docker 工具栈,我们经历了容器化应用的每个方面,包括打包和运行 Docker 容器,为基于代码的不可变部署编写Dockerfile,以及使用 Docker Compose 操作多个容器。然而,本章获得的能力只允许我们在同一主机上运行和连接容器,这限制了构建更大应用的可能性。因此,在下一章中,我们将遇到 Kubernetes,释放容器的力量,超越规模的限制。

第三章:开始使用 Kubernetes

我们已经了解了容器可以为我们带来的好处,但是如果我们需要根据业务需求扩展我们的服务怎么办?有没有一种方法可以在多台机器上构建服务,而不必处理繁琐的网络和存储设置?此外,是否有其他简单的方法来管理和推出我们的微服务,以适应不同的服务周期?这就是 Kubernetes 的作用。在本章中,我们将学习:

  • Kubernetes 概念

  • Kubernetes 组件

  • Kubernetes 资源及其配置文件

  • 如何通过 Kubernetes 启动 kiosk 应用程序

理解 Kubernetes

Kubernetes 是一个用于管理跨多台主机的应用容器的平台。它为面向容器的应用程序提供了许多管理功能,例如自动扩展、滚动部署、计算资源和卷管理。与容器的本质相同,它被设计为可以在任何地方运行,因此我们可以在裸机上、在我们的数据中心、在公共云上,甚至是混合云上运行它。

Kubernetes 考虑了应用容器的大部分操作需求。重点是:

  • 容器部署

  • 持久存储

  • 容器健康监控

  • 计算资源管理

  • 自动扩展

  • 通过集群联邦实现高可用性

Kubernetes 非常适合微服务。使用 Kubernetes,我们可以创建Deployment来部署、滚动或回滚选定的容器(第七章,持续交付)。容器被视为临时的。我们可以将卷挂载到容器中,以在单个主机世界中保留数据。在集群世界中,容器可能被调度在任何主机上运行。我们如何使卷挂载作为永久存储无缝工作?Kubernetes 引入了VolumesPersistent Volumes来解决这个问题(第四章,使用存储和资源)。容器的生命周期可能很短。当它们超出资源限制时,它们可能随时被杀死或停止,我们如何确保我们的服务始终为一定数量的容器提供服务?Kubernetes 中的ReplicationControllerReplicaSet将确保一定数量的容器组处于运行状态。Kubernetes 甚至支持liveness probe来帮助您定义应用程序的健康状况。为了更好地管理资源,我们还可以为 Kubernetes 节点定义最大容量和每组容器(即pod)的资源限制。Kubernetes 调度程序将选择满足资源标准的节点来运行容器。我们将在第四章,使用存储和资源中学习这一点。Kubernetes 提供了一个可选的水平 pod 自动缩放功能。使用此功能,我们可以按资源或自定义指标水平扩展 pod。对于那些高级读者,Kubernetes 设计了高可用性(HA)。我们可以创建多个主节点来防止单点故障。

Kubernetes 组件

Kubernetes 包括两个主要组件:

  • 主节点:主节点是 Kubernetes 的核心,它控制和调度集群中的所有活动

  • 节点:节点是运行我们的容器的工作节点

Master 组件

Master 包括 API 服务器、控制器管理器、调度程序和 etcd。所有组件都可以在不同的主机上进行集群运行。然而,从学习的角度来看,我们将使所有组件在同一节点上运行。

Master 组件

API 服务器(kube-apiserver)

API 服务器提供 HTTP/HTTPS 服务器,为 Kubernetes 主节点中的所有组件提供 RESTful API。例如,我们可以获取资源状态,如 pod,POST 来创建新资源,还可以观察资源。API 服务器读取和更新 etcd,这是 Kubernetes 的后端数据存储。

控制器管理器(kube-controller-manager)

控制器管理器在集群中控制许多不同的事物。复制控制器管理器确保所有复制控制器在所需的容器数量上运行。节点控制器管理器在节点宕机时做出响应,然后会驱逐 pod。端点控制器用于关联服务和 pod 之间的关系。服务账户和令牌控制器用于控制默认账户和 API 访问令牌。

etcd

etcd 是一个开源的分布式键值存储(coreos.com/etcd)。Kubernetes 将所有 RESTful API 对象存储在这里。etcd 负责存储和复制数据。

调度器(kube-scheduler)

调度器根据节点的资源容量或节点上资源利用的平衡来决定适合 pod 运行的节点。它还考虑将相同集合中的 pod 分散到不同的节点。

节点组件

节点组件需要在每个节点上进行配置和运行,向主节点报告 pod 的运行时状态。

节点组件

Kubelet

Kubelet 是节点中的一个重要进程,定期向 kube-apiserver 报告节点活动,如 pod 健康、节点健康和活动探测。正如前面的图表所示,它通过容器运行时(如 Docker 或 rkt)运行容器。

代理(kube-proxy)

代理处理 pod 负载均衡器(也称为服务)和 pod 之间的路由,它还提供了从外部到服务的路由。有两种代理模式,用户空间和 iptables。用户空间模式通过在内核空间和用户空间之间切换来创建大量开销。另一方面,iptables 模式是最新的默认代理模式。它改变 Linux 中的 iptables NAT以实现在所有容器之间路由 TCP 和 UDP 数据包。

Docker

正如第二章中所述,使用容器进行 DevOps,Docker 是一个容器实现。Kubernetes 使用 Docker 作为默认的容器引擎。

Kubernetes 主节点与节点之间的交互

在下图中,客户端使用kubectl向 API 服务器发送请求;API 服务器响应请求,从 etcd 中推送和拉取对象信息。调度器确定应该分配给哪个节点执行任务(例如,运行 pod)。控制器管理器监视运行的任务,并在发生任何不良状态时做出响应。另一方面,API 服务器通过 kubelet 从 pod 中获取日志,并且还是其他主节点组件之间的中心。

与主节点和节点之间的交互

开始使用 Kubernetes

在本节中,我们将学习如何在开始时设置一个小型单节点集群。然后我们将学习如何通过其命令行工具--kubectl 与 Kubernetes 进行交互。我们将学习所有重要的 Kubernetes API 对象及其在 YAML 格式中的表达,这是 kubectl 的输入,然后 kubectl 将相应地向 API 服务器发送请求。

准备环境

开始的最简单方法是运行 minikube (github.com/kubernetes/minikube),这是一个在本地单节点上运行 Kubernetes 的工具。它支持在 Windows、Linux 和 macOS 上运行。在下面的示例中,我们将在 macOS 上运行。Minikube 将启动一个安装了 Kubernetes 的虚拟机。然后我们将能够通过 kubectl 与其交互。

请注意,minikube 不适用于生产环境或任何重负载环境。由于其单节点特性,存在一些限制。我们将在第九章 在 AWS 上运行 Kubernetes和第十章 在 GCP 上运行 Kubernetes中学习如何运行一个真正的集群。

在安装 minikube 之前,我们必须先安装 Homebrew (brew.sh/)和 VirtualBox (www.virtualbox.org/)。Homebrew 是 macOS 中一个有用的软件包管理器。我们可以通过/usr/bin/ruby -e "$(curl -fsSL [raw.githubusercontent.com/Homebrew/install/master/install)](https://raw.githubusercontent.com/Homebrew/install/master/install))"命令轻松安装 Homebrew,并从 Oracle 网站下载 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 后,我们现在可以启动集群了:

// start the cluster
# minikube start
Starting local Kubernetes v1.6.4 cluster...
Starting VM...
Moving files into cluster...
Setting up certs...
Starting cluster components...
Connecting to cluster...
Setting up kubeconfig...
Kubectl is now configured to use the cluster.

这将在本地启动一个 Kubernetes 集群。在撰写时,最新版本是v.1.6.4 minikube。继续在 VirtualBox 中启动名为 minikube 的 VM。然后将设置kubeconfig,这是一个用于定义集群上下文和认证设置的配置文件。

通过kubeconfig,我们能够通过kubectl命令切换到不同的集群。我们可以使用kubectl config view命令来查看kubeconfig中的当前设置:

apiVersion: v1

# cluster and certificate information
clusters:
- cluster:
 certificate-authority-data: REDACTED
 server: https://35.186.182.157
 name: gke_devops_cluster
- 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: gke_devops_cluster
 user: gke_devops_cluster
 name: gke_devops_cluster
- context:
 cluster: minikube
 user: minikube
 name: minikube
current-context: minikube
kind: Config
preferences: {}

# user information
users:
- name: gke_devops_cluster
user:
 auth-provider:
 config:
 access-token: xxxx
 cmd-args: config config-helper --format=json
 cmd-path: /Users/chloelee/Downloads/google-cloud-sdk/bin/gcloud
 expiry: 2017-06-08T03:51:11Z
 expiry-key: '{.credential.token_expiry}'
 token-key: '{.credential.access_token}'
 name: gcp

# namespace info
- name: minikube
user:
 client-certificate: /Users/chloelee/.minikube/apiserver.crt
 client-key: /Users/chloelee/.minikube/apiserver.key

在这里,我们知道我们当前正在使用与集群和用户名称相同的 minikube 上下文。上下文是认证信息和集群连接信息的组合。如果您有多个上下文,可以使用kubectl config use-context $context来强制切换上下文。

最后,我们需要在 minikube 中启用kube-dns插件。kube-dns是 Kuberentes 中的 DNS 服务:

// enable kube-dns addon
# minikube addons enable kube-dns
kube-dns was successfully enabled

kubectl

kubectl是控制 Kubernetes 集群管理器的命令。最常见的用法是检查集群的版本:

// check Kubernetes version
# kubectl version
Client Version: version.Info{Major:"1", Minor:"6", GitVersion:"v1.6.2", GitCommit:"477efc3cbe6a7effca06bd1452fa356e2201e1ee", GitTreeState:"clean", BuildDate:"2017-04-19T20:33:11Z", GoVersion:"go1.7.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"6", GitVersion:"v1.6.4", GitCommit:"d6f433224538d4f9ca2f7ae19b252e6fcb66a3ae", GitTreeState:"clean", BuildDate:"2017-05-30T22:03:41Z", GoVersion:"go1.7.3", Compiler:"gc", Platform:"linux/amd64"} 

我们随后知道我们的服务器版本是最新的,在撰写时是最新的版本 1.6.4。 kubectl的一般语法是:

kubectl [command] [type] [name] [flags] 

command表示您要执行的操作。如果您只在终端中键入kubectl help,它将显示支持的命令。type表示资源类型。我们将在下一节中学习主要的资源类型。name是我们命名资源的方式。沿途始终保持清晰和信息丰富的命名是一个好习惯。对于flags,如果您键入kubectl options,它将显示您可以传递的所有标志。

kubectl非常方便,我们总是可以添加--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 

  # Return snapshot of previous terminated ruby container logs   
  from pod web-1 
  kubectl logs -p -c ruby web-1 
... 

然后我们得到了kubectl logs命令中的完整支持选项。

Kubernetes 资源

Kubernetes 对象是集群中的条目,存储在 etcd 中。它们代表了集群的期望状态。当我们创建一个对象时,我们通过 kubectl 或 RESTful API 向 API 服务器发送请求。API 服务器将状态存储到 etcd 中,并与其他主要组件交互,以确保对象存在。Kubernetes 使用命名空间在虚拟上隔离对象,根据不同的团队、用途、项目或环境。每个对象都有自己的名称和唯一 ID。Kubernetes 还支持标签和注释,让我们对对象进行标记。标签尤其可以用于将对象分组在一起。

Kubernetes 对象

对象规范描述了 Kubernetes 对象的期望状态。大多数情况下,我们编写对象规范,并通过 kubectl 将规范发送到 API 服务器。Kubernetes 将尝试实现该期望状态并更新对象状态。

对象规范可以用 YAML(www.yaml.org/)或 JSON(www.json.org/)编写。在 Kubernetes 世界中,YAML 更常见。在本书的其余部分中,我们将使用 YAML 格式来编写对象规范。以下代码块显示了一个 YAML 格式的规范片段:

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 系统创建的对象使用,例如插件,这些插件是实现集群功能的 pod 或服务,例如仪表板。kube-public 命名空间是在 Kubernetes 1.6 中新引入的,它被一个 beta 控制器管理器(BootstrapSigner kubernetes.io/docs/admin/bootstrap-tokens)使用,将签名的集群位置信息放入kube-public命名空间,以便认证/未认证用户可以看到这些信息。

在接下来的章节中,所有的命名空间资源都将位于默认命名空间中。命名空间对于资源管理和角色也非常重要。我们将在第八章《集群管理》中介绍更多内容。

名称

Kubernetes 中的每个对象都拥有自己的名称。一个资源中的对象名称在同一命名空间内是唯一标识的。Kubernetes 使用对象名称作为资源 URL 到 API 服务器的一部分,因此它必须是小写字母、数字字符、破折号和点的组合,长度不超过 254 个字符。除了对象名称,Kubernetes 还为每个对象分配一个唯一的 ID(UID),以区分类似实体的历史发生。

标签和选择器

标签是一组键/值对,用于附加到对象。标签旨在为对象指定有意义的标识信息。常见用法是微服务名称、层级、环境和软件版本。用户可以定义有意义的标签,以便稍后与选择器一起使用。对象规范中的标签语法是:

labels: 
  $key1: $value1 
  $key2: $value2 

除了标签,标签选择器用于过滤对象集。用逗号分隔,多个要求将由AND逻辑运算符连接。有两种过滤方式:

  • 基于相等性的要求

  • 基于集合的要求

基于相等性的要求支持===!=运算符。例如,如果选择器是chapter=2,version!=0.1,结果将是对象 C。如果要求是version=0.1,结果将是对象 A对象 B。如果我们在支持的对象规范中写入要求,将如下所示:

selector: 
  $key1: $value1 

选择器示例

基于集合的要求支持innotinexists(仅针对键)。例如,如果要求是chapter in (3, 4),version,那么对象 A 将被返回。如果要求是version notin (0.2), !author_info,结果将是对象 A对象 B。以下是一个示例,如果我们写入支持基于集合的要求的对象规范:

selector: 
  matchLabels:  
    $key1: $value1 
  matchExpressions: 
{key: $key2, operator: In, values: [$value1, $value2]} 

matchLabelsmatchExpressions的要求被合并在一起。这意味着过滤后的对象需要在两个要求上都为真。

我们将在本章中学习使用 ReplicationController、Service、ReplicaSet 和 Deployment。

注释

注释是一组用户指定的键/值对,用于指定非标识性元数据。使用注释可以像普通标记一样,例如,用户可以向注释中添加时间戳、提交哈希或构建编号。一些 kubectl 命令支持 --record 选项,以记录对注释对象进行更改的命令。注释的另一个用例是存储配置,例如 Kubernetes 部署(kubernetes.io/docs/concepts/workloads/controllers/deployment)或关键附加组件 pods(coreos.com/kubernetes/docs/latest/deploy-addons.html)。注释语法如下所示,位于元数据部分:

annotations: 
  $key1: $value1 
  $key2: $value2 

命名空间、名称、标签和注释位于对象规范的元数据部分。选择器位于支持选择器的资源的规范部分,例如 ReplicationController、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 
DESCRIPTION: 
Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts. 

FIELDS: 
   metadata  <Object> 
     Standard object's metadata. More info: 
     http://releases.k8s.io/HEAD/docs/devel/api- 
     conventions.md#metadata 

   spec  <Object> 
     Specification of the desired behavior of the pod. 
     More info: 
     http://releases.k8s.io/HEAD/docs/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: 
     http://releases.k8s.io/HEAD/docs/devel/api-
     conventions.md#spec-and-status 

   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: 
     http://releases.k8s.io/HEAD/docs/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: 
     http://releases.k8s.io/HEAD/docs/devel/api-
     conventions.md#types-kinds 

在以下示例中,我们将展示如何在一个 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 中的容器可以通过 localhost 进行访问

此规范将创建两个容器,webcentos。Web 是一个 nginx 容器 (hub.docker.com/_/nginx/)。默认情况下,通过暴露容器端口 80,因为 centos 与 nginx 共享相同的上下文,当在 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 将在创建或更新此资源时添加最新的命令。因此,我们不会忘记哪些资源是由哪个规范创建的。

我们可以使用 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

kubectl 命令的末尾添加 --namespace=$namespace_name 可以访问不同命名空间中的对象。以下是一个示例,用于检查 kube-system 命名空间中的 pod,该命名空间由系统类型的 pod 使用:

# kubectl get pods --namespace=kube-system

NAME READY STATUS RESTARTS AGE

kube-addon-manager-minikube 1/1 Running 2 3d

kube-dns-196007617-jkk4k 3/3 Running 3 3d

kubernetes-dashboard-3szrf 1/1 Running 1 3d

大多数对象都有它们的简称,在我们使用 kubectl get <object> 列出它们的状态时非常方便。例如,pod 可以称为 po,服务可以称为 svc,部署可以称为 deploy。输入 kubectl get 了解更多信息。

我们示例 pod 的状态是 ContainerCreating。在这个阶段,Kubernetes 已经接受了请求,尝试调度 pod 并拉取镜像。当前没有容器正在运行。等待片刻后,我们可以再次获取状态:

// 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 通过 localhost 与 nginx 共享相同的网络!Kubernetes 会在 pod 中创建一个网络容器。网络容器的功能之一是在 pod 内部的容器之间转发流量。我们将在 第五章 中了解更多,网络和安全

如果我们在 pod 规范中指定了标签,我们可以使用kubectl get pods -l <requirement>命令来获取满足要求的 pod。例如,kubectl get pods -l 'tier in (frontend, backend)'。另外,如果我们使用kubectl pods -owide,它将列出哪个 pod 运行在哪个节点上。

我们可以使用kubectl describe <resource> <resource_name>来获取资源的详细信息:

// get detailed information for a pod
# kubectl describe pods example
Name:    example
Namespace:  default
Node:    minikube/192.168.99.100
Start Time:  Fri, 09 Jun 2017 07:08:59 -0400
Labels:    <none>
Annotations:  <none>
Status:    Running
IP:    172.17.0.4
Controllers:  <none>
Containers:  

此时,我们知道这个 pod 正在哪个节点上运行,在 minikube 中我们只有一个节点,所以不会有任何区别。在真实的集群环境中,知道哪个节点对故障排除很有用。我们没有为它关联任何标签、注释和控制器:

web:
 Container ID:    
 docker://a90e56187149155dcda23644c536c20f5e039df0c174444e 0a8c8  7e8666b102b
   Image:    nginx
   Image ID:    docker://sha256:958a7ae9e56979be256796dabd5845c704f784cd422734184999cf91f24c2547
   Port:
   State:    Running
      Started:    Fri, 09 Jun 2017 07:09:00 -0400
   Ready:    True
   Restart Count:  0
   Environment:  <none>
   Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from 
      default-token-jd1dq (ro)
     centos:
     Container ID:  docker://778965ad71dd5f075f93c90f91fd176a8add4bd35230ae0fa6c73cd1c2158f0b
     Image:    centos
     Image ID:    docker://sha256:3bee3060bfc81c061ce7069df35ce090593bda584d4ef464bc0f38086c11371d
     Port:
     Command:
       /bin/sh
       -c
       while : ;do curl http://localhost:80/; sleep 10; 
       done
      State:    Running
       Started:    Fri, 09 Jun 2017 07:09:01 -0400
      Ready:    True
      Restart Count:  0
      Environment:  <none>
      Mounts:
          /var/run/secrets/kubernetes.io/serviceaccount from default-token-jd1dq (ro)

在容器部分,我们将看到这个 pod 中包含了两个容器。它们的状态、镜像和重启计数:

Conditions:
 Type    Status
 Initialized   True
 Ready   True
 PodScheduled   True

一个 pod 有一个PodStatus,其中包括一个表示为PodConditions的数组映射。PodConditions的可能键是PodScheduledReadyInitializedUnschedulable。值可以是 true、false 或 unknown。如果 pod 没有按预期创建,PodStatus将为我们提供哪个部分失败的简要视图:

Volumes:
 default-token-jd1dq:
 Type:  Secret (a volume populated by a Secret)
 SecretName:  default-token-jd1dq
 Optional:  false

Pod 关联了一个 service account,为运行在 pod 中的进程提供身份。它由 API Server 中的 service account 和 token controller 控制。

它将在包含用于 API 访问令牌的 pod 中,为每个容器挂载一个只读卷到/var/run/secrets/kubernetes.io/serviceaccount下。Kubernetes 创建了一个默认的 service account。我们可以使用kubectl get serviceaccounts命令来列出它们:

QoS Class:  BestEffort
Node-Selectors:  <none>
Tolerations:  <none>

我们还没有为这个 pod 分配任何选择器。QoS 表示资源服务质量。Toleration 用于限制可以使用节点的 pod 数量。我们将在第八章中学到更多,集群管理

Events:
 FirstSeen  LastSeen  Count  From      SubObjectPath    Type     
  Reason    Message
  ---------  --------  -----  ----      -------------    ------ 
  --  ------    -------
  19m    19m    1  default-scheduler        Normal    Scheduled  
  Successfully assigned example to minikube
  19m    19m    1  kubelet, minikube  spec.containers{web}  
  Normal    Pulling    pulling image "nginx"
  19m    19m    1  kubelet, minikube  spec.containers{web}  
  Normal    Pulled    Successfully pulled image "nginx"
  19m    19m    1  kubelet, minikube  spec.containers{web}  
  Normal    Created    Created container with id 
  a90e56187149155dcda23644c536c20f5e039df0c174444e0a8c87e8666b102b
  19m    19m    1  kubelet, minikube  spec.containers{web}   
  Normal    Started    Started container with id  
 a90e56187149155dcda23644c536c20f5e039df0c174444e0a8c87e86 
 66b102b
  19m    19m    1  kubelet, minikube  spec.containers{centos}  
  Normal    Pulling    pulling image "centos"
  19m    19m    1  kubelet, minikube  spec.containers{centos}  
  Normal    Pulled    Successfully pulled image "centos"
  19m    19m    1  kubelet, minikube  spec.containers{centos}  
  Normal    Created    Created container with id 
 778965ad71dd5f075f93c90f91fd176a8add4bd35230ae0fa6c73cd1c 
 2158f0b
  19m    19m    1  kubelet, minikube  spec.containers{centos}  
  Normal    Started    Started container with id 
 778965ad71dd5f075f93c90f91fd176a8add4bd35230ae0fa6c73cd1c 
 2158f0b 

通过查看事件,我们可以了解 Kubernetes 在运行节点时的步骤。首先,调度器将任务分配给一个节点,这里它被命名为 minikube。然后 minikube 上的 kubelet 开始拉取第一个镜像并相应地创建一个容器。然后 kubelet 拉取第二个容器并运行。

ReplicaSet (RS) 和 ReplicationController (RC)

一个 pod 不会自我修复。当一个 pod 遇到故障时,它不会自行恢复。因此,ReplicaSetRS)和ReplicationControllerRC)就发挥作用了。ReplicaSet 和 ReplicationController 都将确保集群中始终有指定数量的副本 pod 在运行。如果一个 pod 因任何原因崩溃,ReplicaSet 和 ReplicationController 将请求启动一个新的 Pod。

在最新的 Kubernetes 版本中,ReplicationController 逐渐被 ReplicaSet 取代。它们共享相同的概念,只是使用不同的 pod 选择器要求。ReplicationController 使用基于相等性的选择器要求,而 ReplicaSet 使用基于集合的选择器要求。ReplicaSet 通常不是由用户创建的,而是由 Kubernetes 部署对象创建,而 ReplicationController 是由用户自己创建的。在本节中,我们将通过示例逐步解释 RC 的概念,这样更容易理解。然后我们将在最后介绍 ReplicaSet。

带有期望数量 2 的 ReplicationController

假设我们想创建一个ReplicationController对象,期望数量为两个。这意味着我们将始终有两个 pod 在服务中。在编写 ReplicationController 的规范之前,我们必须先决定 pod 模板。Pod 模板类似于 pod 的规范。在 ReplicationController 中,元数据部分中的标签是必需的。ReplicationController 使用 pod 选择器来选择它管理的哪些 pod。标签允许 ReplicationController 区分是否所有与选择器匹配的 pod 都处于正常状态。

在这个例子中,我们将创建两个带有标签projectserviceversion的 pod,如前图所示:

// an example for rc spec
# cat 3-2-2_rc.yaml
apiVersion: v1
kind: ReplicationController
metadata:
 name: nginx
spec:
 replicas: 2
 selector:
 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
 ports:
 - containerPort: 80
// create RC by above input file
# kubectl create -f 3-2-2_rc.yaml
replicationcontroller "nginx" created  

然后我们可以使用kubectl来获取当前的 RC 状态:

// get current RCs
# kubectl get rc
NAME      DESIRED   CURRENT   READY     AGE
nginx     2         2         2         5s  

它显示我们有两个期望的 pod,我们目前有两个 pod 并且两个 pod 已经准备就绪。现在我们有多少个 pod?

// get current running pod
# kubectl get pods
NAME          READY     STATUS    RESTARTS   AGE
nginx-r3bg6   1/1       Running   0          11s
nginx-sj2f0   1/1       Running   0          11s  

它显示我们有两个正在运行的 pod。如前所述,ReplicationController 管理所有与选择器匹配的 pod。如果我们手动创建一个具有相同标签的 pod,理论上它应该与我们刚刚创建的 RC 的 pod 选择器匹配。让我们试一试:

// manually create a pod with same labels
# cat 3-2-2_rc_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_rc_self_created_pod.yaml 
pod "our-nginx" created  

让我们看看它是否正在运行:

// get pod status
# kubectl get pods
NAME          READY     STATUS        RESTARTS   AGE
nginx-r3bg6   1/1       Running       0          4m
nginx-sj2f0   1/1       Running       0          4m
our-nginx     0/1       Terminating   0          4s  

它已经被调度,ReplicationController 捕捉到了它。pod 的数量变成了三个,超过了我们的期望数量。最终该 pod 被杀死:

// get pod status
# kubectl get pods
NAME          READY     STATUS    RESTARTS   AGE
nginx-r3bg6   1/1       Running   0          5m
nginx-sj2f0   1/1       Running   0          5m  

ReplicationController 确保 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 rc nginx
replicationcontroller "nginx" edited  

让我们来检查 RC 信息:

// get rc information
# kubectl get rc
NAME      DESIRED   CURRENT   READY     AGE
nginx     5         5         5         5m      

我们现在有五个 pods。让我们来看看 RC 是如何工作的:

// describe RC resource `nginx`
# kubectl describe rc nginx
Name:    nginx
Namespace:  default
Selector:  project=chapter3,service=web,version=0.1
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
 Environment:  <none>
 Mounts:    <none>
 Volumes:    <none>
Events:
 FirstSeen  LastSeen  Count  From      SubObjectPath  Type      
  Reason      Message
---------  --------  -----  ----      -------------  --------  ------      -------
34s    34s    1  replication-controller      Normal    SuccessfulCreate  Created pod: nginx-r3bg6 
34s    34s    1  replication-controller      Normal    SuccessfulCreate  Created pod: nginx-sj2f0 
20s    20s    1  replication-controller      Normal    SuccessfulDelete  Deleted pod: our-nginx
15s    15s    1  replication-controller      Normal    SuccessfulCreate  Created pod: nginx-nlx3v
15s    15s    1  replication-controller      Normal    SuccessfulCreate  Created pod: nginx-rqt58
15s    15s    1  replication-controller      Normal    SuccessfulCreate  Created pod: nginx-qb3mr  

通过描述命令,我们可以了解 RC 的规范,也可以了解事件。在我们创建 nginx RC 时,它按规范启动了两个容器。然后我们通过另一个规范手动创建了另一个 pod,名为 our-nginx。RC 检测到该 pod 与其 pod 选择器匹配。然后数量超过了我们期望的数量,所以它将其驱逐。然后我们将副本扩展到了五个。RC 检测到它没有满足我们的期望状态,于是启动了三个 pods 来填补空缺。

如果我们想要删除一个 RC,只需使用 kubectl 命令 kubectl delete <resource> <resource_name>。由于我们手头上有一个配置文件,我们也可以使用 kubectl delete -f <configuration_file> 来删除文件中列出的资源:

// delete a rc
# kubectl delete rc nginx
replicationcontroller "nginx" deleted
// get pod status
# kubectl get pods
NAME          READY     STATUS        RESTARTS   AGE
nginx-r3bg6   0/1       Terminating   0          29m  

相同的概念也适用于 ReplicaSet。以下是 3-2-2.rc.yaml 的 RS 版本。两个主要的区别是:

  • 在撰写时,apiVersionextensions/v1beta1

  • 选择器要求更改为基于集合的要求,使用 matchLabelsmatchExpressions 语法。

按照前面示例的相同步骤,RC 和 RS 之间应该完全相同。这只是一个例子;然而,我们不应该自己创建 RS,而应该始终由 Kubernetes deployment 对象管理。我们将在下一节中学到更多:

// RS version of 3-2-2_rc.yaml 
# cat 3-2-2_rs.yaml
apiVersion: extensions/v1beta1
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

部署

在 Kubernetes 1.2 版本之后,部署是管理和部署我们的软件的最佳原语。它支持优雅地部署、滚动更新和回滚 pods 和 ReplicaSets。我们通过声明性地定义我们对软件的期望更新,然后部署将逐渐为我们完成。

在部署之前,ReplicationController 和 kubectl rolling-update 是实现软件滚动更新的主要方式,这更加命令式和较慢。现在部署成为了管理我们应用的主要高级对象。

让我们来看看它是如何工作的。在这一部分,我们将体验到部署是如何创建的,如何执行滚动更新和回滚。第七章,持续交付有更多关于如何将部署集成到我们的持续交付流水线中的实际示例信息。

首先,我们可以使用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:

// 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  

部署、ReplicaSets 和 pod 之间的关系

如果我们删除一个 pod,替换的 pod 将立即被调度和启动。这是因为部署在幕后创建了一个 ReplicaSet,它将确保副本的数量与我们的期望数量匹配。一般来说,部署管理 ReplicaSets,ReplicaSets 管理 pod。请注意,我们不应该手动操作部署管理的 ReplicaSets,就像如果它们由 ReplicaSets 管理,直接更改 pod 也是没有意义的:

// 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  

部署也可以通过 spec 创建。之前由 kubectl 启动的部署和服务可以转换为以下 spec:

// create deployments by spec
# cat 3-2-3_deployments.yaml
apiVersion: apps/v1beta1
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%

minReadySeconds是一个重要的设置。如果我们的应用程序在 pod 启动时不能立即使用,那么没有适当的等待,pod 将滚动得太快。尽管所有新的 pod 都已经启动,但应用程序可能仍在热身;有可能会发生服务中断。在下面的示例中,我们将把配置添加到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来检查发生了什么,我们将看到部署已经通过删除/创建 pod 来触发了 ReplicaSets 的滚动更新:

// check detailed rs information
# kubectl describe rs nginx-2371676037 
Name:    nginx-2371676037 
Namespace:  default
Selector:  pod-template-hash=2371676037   ,run=nginx
Labels:    pod-template-hash=2371676037 
 run=nginx
Annotations:  deployment.kubernetes.io/desired-replicas=2
 deployment.kubernetes.io/max-replicas=3
 deployment.kubernetes.io/revision=4
 deployment.kubernetes.io/revision-history=2
Replicas:  2 current / 2 desired
Pods Status:  2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
 Labels:  pod-template-hash=2371676037 
 run=nginx
Containers:
nginx:
Image:    nginx:1.13.1
Port:    80/TCP
...
Events:
FirstSeen  LastSeen  Count  From      SubObjectPath  Type    Reason      Message
---------  --------  -----  ----      -------------  --------  ------      -------
3m    3m    1  replicaset-controller      Normal    SuccessfulCreate  Created pod: nginx-2371676037-f2ndj
3m    3m    1  replicaset-controller      Normal    SuccessfulCreate  Created pod: nginx-2371676037-9lc8j
3m    3m    1  replicaset-controller      Normal    SuccessfulDelete  Deleted pod: nginx-2371676037-f2ndj
3m    3m    1  replicaset-controller      Normal    SuccessfulDelete  Deleted pod: nginx-2371676037-9lc8j

部署的示例

上图显示了部署的示例。在某个时间点,我们有两个(期望数量)和一个(maxSurge)pod。在启动每个新的 pod 后,Kubernetes 将等待三个(minReadySeconds)秒,然后执行下一个操作。

如果我们使用命令kubectl set image deployment nginx nginx=nginx:1.12.0 to previous version 1.12.0,部署将为我们执行回滚。

服务

Kubernetes 中的服务是将流量路由到一组逻辑 pod 的抽象层。有了服务,我们就不需要追踪每个 pod 的 IP 地址。服务通常使用标签选择器来选择它需要路由到的 pod(在某些情况下,服务是有意地创建而不带选择器)。服务抽象是强大的。它实现了解耦,并使微服务之间的通信成为可能。目前,Kubernetes 服务支持 TCP 和 UDP。

服务不关心我们如何创建 pod。就像 ReplicationController 一样,它只关心 pod 是否匹配其标签选择器,因此 pod 可以属于不同的 ReplicationControllers。以下是一个示例:

服务通过标签选择器映射 pod

在图中,所有的 pod 都匹配服务选择器,因此服务将负责将流量分发到所有的 pod,而无需显式分配。

服务类型

服务有四种类型:ClusterIP、NodePort、LoadBalancer 和 ExternalName。

LoadBalancer 包括 NodePort 和 ClusterIP 的功能

ClusterIP

ClusterIP 是默认的服务类型。它在集群内部 IP 上公开服务。集群中的 pod 可以通过 IP 地址、环境变量或 DNS 访问服务。在下面的示例中,我们将学习如何使用本地服务环境变量和 DNS 来访问集群中服务后面的 pod。

在启动服务之前,我们想要创建图中显示的两组 RC:

// create RC 1 with nginx 1.12.0 version
# cat 3-2-3_rc1.yaml
apiVersion: v1
kind: ReplicationController
metadata:
 name: nginx-1.12
spec:
 replicas: 2
 selector:
 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 RC 2 with nginx 1.13.1 version
# cat 3-2-3_rc2.yaml
apiVersion: v1
kind: ReplicationController
metadata:
 name: nginx-1.13
spec:
 replicas: 2
 selector:
 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 选择器,以定位项目和服务标签:

// 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 RCs 
# kubectl create -f 3-2-3_rc1.yaml
replicationcontroller "nginx-1.12" created 
# kubectl create -f 3-2-3_rc2.yaml
replicationcontroller "nginx-1.13" created

// create the service
# kubectl create -f 3-2-3_service.yaml
service "nginx-service" created  

由于service对象可能创建一个 DNS 标签,因此服务名称必须遵循字符 a-z、0-9 或-(连字符)的组合。标签开头或结尾的连字符是不允许的。

然后我们可以使用kubectl describe service <service_name>来检查服务信息:

// 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>

一个服务可以公开多个端口。只需在服务规范中扩展.spec.ports列表。

我们可以看到这是一个 ClusterIP 类型的服务,分配的内部 IP 是 10.0.0.188。端点显示我们在服务后面有四个 IP。可以通过kubectl describe pods <pod_name>命令找到 pod IP。Kubernetes 为匹配的 pod 创建了一个endpoints对象以及一个service对象来路由流量。

当使用选择器创建服务时,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 pods:

通过环境变量和 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 是有寿命的。有了 RC 和 service,我们可以构建健壮的服务,而不必担心一个 pod 可能影响所有微服务。

启用kube-dns插件后,同一集群和相同命名空间中的 pod 可以通过服务的 DNS 记录访问服务。Kube-dns 通过监视 Kubernetes API 来为新创建的服务创建 DNS 记录。集群 IP 的 DNS 格式是$servicename.$namespace,端口是_$portname_$protocal.$servicename.$namespaceclusterip_chk pod 的规范将与环境变量相似。只需在我们之前的例子中将 URL 更改为http://nginx-service.default:_http_tcp.nginx-service.default/,它们应该完全相同地工作!

NodePort

如果服务设置为 NodePort,Kubernetes 将在每个节点上分配一个特定范围内的端口。任何发送到该端口的节点的流量将被路由到服务端口。端口号可以由用户指定。如果未指定,Kubernetes 将在 30000 到 32767 范围内随机选择一个端口而不发生冲突。另一方面,如果指定了,用户应该自行负责管理冲突。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访问服务。Node 可以是任何节点。kube-proxy会监视服务和端点的任何更新,并相应地更新 iptables 规则(如果使用默认的 iptables 代理模式)。

如果你正在使用 minikube,你可以通过minikube service [-n NAMESPACE] [--url] NAME命令访问服务。在这个例子中,是minikube service nginx-nodeport

LoadBalancer

这种类型只能在云提供商支持的情况下使用,比如谷歌云平台(第十章,GCP 上的 Kubernetes)和亚马逊网络服务(第九章,AWS 上的 Kubernetes)。通过创建 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文件系统来存储秘密。由于存储管理的考虑,秘密并不设计用于存储大量数据。一个秘密的当前大小限制为 1MB。

我们可以通过启动 kubectl 创建秘密命令或通过 spec 来基于文件、目录或指定的文字值创建秘密。有三种类型的秘密格式:通用(或不透明,如果编码)、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 <secret_name> -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", "cat /secret/password-example; done"] 
    volumeMounts: 
      - name: secret-vol 
        mountPath: /secret 
        readOnly: true 
  volumes: 
    - name: secret-vol 
      secret: 
        secretName: mypassword 
        # items are optional 
        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>中,而不指定items``keypath,或者在 Pod 中的/<mount_point>/<path>中。在这种情况下,它位于/secret/password-example下。如果我们描述 Pod,我们可以发现这个 Pod 中有两个挂载点。第一个是只读卷,存储我们的秘密,第二个存储与 API 服务器通信的凭据,这是由 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 也会遇到失败。

现在我们将学习如何通过命令行创建秘密。接下来我们将简要介绍其规范格式:

// secret example # cat 3-2-3_secret.yaml 
apiVersion: v1 
kind: Secret 
metadata:  
  name: mypassword 
type: Opaque 
data:  
  mypassword: bXlwYXNzd29yZA==

由于规范是纯文本,我们需要通过自己的echo -n <password> | base64来对秘密进行编码。请注意,这里的类型变为Opaque。按照这样做,它应该与我们通过命令行创建的那个相同。

通过环境变量检索秘密

或者,我们可以使用环境变量来检索秘密,这样更灵活,适用于短期凭据,比如密码。这样,应用程序可以使用环境变量来检索数据库密码,而无需处理文件和卷:

秘密应该始终在需要它的 Pod 之前创建。否则,Pod 将无法成功启动。

// 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 

声明位于spec.containers[].env[]下。在这种情况下,我们需要秘密名称和密钥名称。两者都是mypassword。示例应该与通过文件检索的示例相同。

ConfigMap

ConfigMap 是一种能够将配置留在 Docker 镜像之外的方法。它将配置数据作为键值对注入到 pod 中。它的属性与 secret 类似,更具体地说,secret 用于存储敏感数据,如密码,而 ConfigMap 用于存储不敏感的配置数据。

与 secret 相同,ConfigMap 可以基于文件、目录或指定的文字值。与 secret 相似的语法/命令,ConfigMap 使用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 内利用它。在 pod 内使用 ConfigMap 也有两种方式:通过卷或环境变量。

通过卷使用 ConfigMap

与 secret 部分中的先前示例类似,我们使用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: config-ev
spec:
 containers:
 - name: centos
 image: centos
 command: ["/bin/sh", "-c", "while : ;do echo $DATABASE_ENDPOINT; sleep 10;    
   done"]
 env:
 - name: MY_PASSWORD
 valueFrom:
 secretKeyRef:
 name: mypassword
 key: mypassword

// 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 来进行一些认证。例如,kube-dns 使用它来放置客户端 CA 文件。您可以通过在描述 ConfigMaps 时添加--namespace=kube-system来检查系统 ConfigMap。

多容器编排

在这一部分,我们将重新审视我们的售票服务:一个作为前端的售票机网络服务,提供接口来获取/放置票务。有一个作为缓存的 Redis,用来管理我们有多少张票。Redis 还充当发布者/订阅者通道。一旦一张票被售出,售票机将向其发布一个事件。订阅者被称为记录器,它将写入一个时间戳并将其记录到 MySQL 数据库中。请参考第二章中的最后一节,使用容器进行 DevOps,了解详细的 Dockerfile 和 Docker compose 实现。我们将使用DeploymentServiceSecretVolumeConfigMap对象在 Kubernetes 中实现这个例子。源代码可以在github.com/DevOps-with-Kubernetes/examples/tree/master/chapter3/3-3_kiosk找到。

Kubernetes 世界中售票机的一个例子

我们将需要四种类型的 pod。使用 Deployment 来管理/部署 pod 是最好的选择。它将通过部署策略功能减少我们在未来进行部署时的痛苦。由于售票机、Redis 和 MySQL 将被其他组件访问,我们将为它们的 pod 关联服务。MySQL 充当数据存储,为了简单起见,我们将为其挂载一个本地卷。请注意,Kubernetes 提供了一堆选择。请查看第四章中的详细信息和示例,使用存储和资源。像 MySQL 的 root 和用户密码这样的敏感信息,我们希望它们存储在秘钥中。其他不敏感的配置,比如数据库名称或数据库用户名,我们将留给 ConfigMap。

我们将首先启动 MySQL,因为记录器依赖于它。在创建 MySQL 之前,我们必须先创建相应的secretConfigMap。要创建secret,我们需要生成 base64 加密的数据:

// generate base64 secret for MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD
# echo -n "pass" | base64
cGFzcw==
# echo -n "mysqlpass" | base64
bXlzcWxwYXNz

然后我们可以创建秘钥:

# 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 及其服务的时候:

// MySQL Deployment
# cat mysql.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: lmysql
spec:
 replicas: 1
 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
---
kind: Service
apiVersion: v1
metadata:
 name: lmysql-service
spec:
 selector:
 tier: database
 ports:
 - protocol: TCP
 port: 3306
 targetPort: 3306
 name: tcp3306  

我们可以通过添加三个破折号作为分隔,将多个规范放入一个文件中。在这里,我们将hostPath /mysql/data挂载到具有路径/var/lib/mysql的 pod 中。在环境部分,我们通过secretKeyRefconfigMapKeyRef利用秘钥和 ConfigMap 的语法。

创建 MySQL 后,Redis 将是下一个很好的候选,因为它是其他的依赖,但它不需要先决条件:

// create Redis deployment
# cat redis.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: lcredis
spec:
 replicas: 1
 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  

然后现在是启动 kiosk 的好时机:

# cat kiosk-example.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: kiosk-example
spec:
 replicas: 5
 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 pod 的环境变量,这是 kube-dns 为Service对象(在本章中称为 service)创建的 DNS 名称。因此,kiosk 可以通过环境变量访问 Redis 主机。

最后,我们将创建录音机。录音机不向其他人公开任何接口,因此不需要Service对象:

# cat recorder-example.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: recorder-example
spec:
 replicas: 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。它使用通过秘密注入的根凭据。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

在默认浏览器中打开 kubernetes 服务默认/kiosk-service...

然后我们可以知道 IP 和端口。

然后我们可以通过POSTGET /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 的控制中心,例如,它确保我们期望的容器数量得到满足,控制关联 pod 和服务的端点,并控制 API 访问令牌。我们还有 Kubernetes 节点,它们是承载容器的工作节点,接收来自主节点的信息,并根据配置路由流量。

然后,我们使用 minikube 演示了基本的 Kubernetes 对象,包括 pod、ReplicaSets、ReplicationControllers、deployments、services、secrets 和 ConfigMap。最后,我们演示了如何将我们学到的所有概念结合到 kiosk 应用程序部署中。

正如我们之前提到的,容器内的数据在容器消失时也会消失。因此,在容器世界中,卷是非常重要的,用来持久保存数据。在下一章中,我们将学习卷是如何工作的,以及它的选项,如何使用持久卷等等。

第四章:处理存储和资源

在第三章 开始使用 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,因此,应用程序可以通过本地网络相互通信;但是,文件系统是分隔的。

以下图表显示了Tomcatnginx在同一个 pod 中。这些应用程序可以通过本地主机相互通信。但是,它们无法访问彼此的config文件:

一些应用程序不会影响这些场景和行为,但有些应用程序可能有一些使用案例,需要它们使用共享目录或文件。因此,开发人员和 Kubernetes 管理员需要了解不同类型的无状态和有状态应用程序。

无状态和有状态的应用程序

就无状态应用程序而言,在这种情况下使用临时卷。容器上的应用程序不需要保留数据。虽然无状态应用程序可能会在容器存在时将数据写入文件系统,但在应用程序的生命周期中并不重要。

例如,tomcat容器运行一些 Web 应用程序。它还在/usr/local/tomcat/logs/下写入应用程序日志,但如果丢失log文件,它不会受到影响。

但是,如果您开始分析应用程序日志呢?需要出于审计目的保留吗?在这种情况下,Tomcat 仍然可以是无状态的,但可以将/usr/local/tomcat/logs卷与 Logstash 等另一个容器共享(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之间共享文件系统:

让我们创建tomcatlogstash pod,然后看看 Logstash 是否能在/mnt下看到 Tomcat 应用程序日志:

在这种情况下,最终目的地的 Elasticsearch 必须是有状态的。有状态意味着使用持久卷。即使容器重新启动,Elasticsearch 容器也必须保留数据。此外,您不需要在同一个 pod 中配置 Elasticsearch 容器和 Tomcat/Logstash。因为 Elasticsearch 应该是一个集中的日志数据存储,它可以与 Tomcat/Logstash pod 分开,并独立扩展。

一旦确定您的应用程序需要持久卷,就有一些不同类型的卷和不同的管理持久卷的方法。

Kubernetes 持久卷和动态配置

Kubernetes 支持各种持久卷。例如,公共云存储,如 AWS EBS 和 Google 持久磁盘。它还支持网络(分布式)文件系统,如 NFS,GlusterFS 和 Ceph。此外,它还可以支持诸如 iSCSI 和光纤通道之类的块设备。根据环境和基础架构,Kubernetes 管理员可以选择最匹配的持久卷类型。

以下示例使用 GCP 持久磁盘作为持久卷。第一步是创建一个 GCP 持久磁盘,并将其命名为gce-pd-1

如果使用 AWS EBS 或 Google 持久磁盘,则 Kubernetes 节点必须位于 AWS 或 Google 云平台中。

然后在Deployment定义中指定名称gce-pd-1

它将从 GCE 持久磁盘挂载到/usr/local/tomcat/logs,可以持久保存 Tomcat 应用程序日志。

持久卷索赔抽象层

将持久卷直接指定到配置文件中,这将与特定基础架构紧密耦合。在先前的示例中,这是谷歌云平台,也是磁盘名称(gce-pd-1)。从容器管理的角度来看,pod 定义不应该锁定到特定环境,因为基础架构可能会根据环境而不同。理想的 pod 定义应该是灵活的,或者抽象出实际的基础架构,只指定卷名称和挂载点。

因此,Kubernetes 提供了一个抽象层,将 pod 与持久卷关联起来,称为持久卷索赔PVC)。它允许我们与基础架构解耦。Kubernetes 管理员只需预先分配必要大小的持久卷。然后 Kubernetes 将在持久卷和 PVC 之间进行绑定:

以下示例是使用 PVC 的 pod 的定义;让我们首先重用之前的例子(gce-pd-1)在 Kubernetes 中注册:

然后,创建一个与持久卷(pv-1)关联的 PVC。

请注意,将其设置为storageClassName: ""意味着它应明确使用静态配置。一些 Kubernetes 环境,如Google 容器引擎GKE),已经设置了动态配置。如果我们不指定storageClassName: "",Kubernetes 将忽略现有的PersistentVolume,并在创建PersistentVolumeClaim时分配新的PersistentVolume

现在,tomcat设置已经与特定卷“pvc-1”解耦:

动态配置和 StorageClass

PVC 为持久卷管理提供了一定程度的灵活性。然而,预先分配一些持久卷池可能不够成本效益,特别是在公共云中。

Kubernetes 还通过支持持久卷的动态配置来帮助这种情况。Kubernetes 管理员定义了持久卷的provisioner,称为StorageClass。然后,持久卷索赔要求StorageClass动态分配持久卷,然后将其与 PVC 关联起来:

在下面的例子中,AWS EBS 被用作StorageClass,然后,在创建 PVC 时,StorageClass动态创建 EBS 并将其注册到 Kubernetes 持久卷,然后附加到 PVC:

一旦StorageClass成功创建,就可以创建一个不带 PV 的 PVC,但要指定StorageClass的名称。在这个例子中,这将是"aws-sc",如下面的截图所示:

然后,PVC 要求StorageClass在 AWS 上自动创建持久卷,如下所示:

请注意,诸如 kops(github.com/kubernetes/kops)和 Google 容器引擎(cloud.google.com/container-engine/)等 Kubernetes 配置工具默认会创建StorageClass。例如,kops 在 AWS 环境上设置了默认的 AWS EBS StorageClass。Google 容器引擎在 GKE 上设置了 Google Cloud 持久磁盘。有关更多信息,请参阅第九章,在 AWS 上使用 Kubernetes和第十章,在 GCP 上使用 Kubernetes

//default Storage Class on AWS
$ kubectl get sc
NAME            TYPE
default         kubernetes.io/aws-ebs
gp2 (default)   kubernetes.io/aws-ebs

//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 在临时卷上的行为:

让我们创建一个名为kubernetes org的 Grafana organizations,如下所示:

然后,看一下Grafana目录,有一个数据库文件(/var/lib/grafana/grafana.db)的时间戳,在创建 Grafana organization之后已经更新:

当 pod 被删除时,ReplicaSet 将启动一个新的 pod,并检查 Grafana organization是否存在:

看起来sessions目录已经消失,grafana.db也被 Docker 镜像重新创建。然后,如果您访问 Web 控制台,Grafana organization也会消失:

仅使用持久卷来处理 Grafana 呢?但是使用带有持久卷的 ReplicaSet,它无法正确地复制(扩展)。因为所有的 pod 都试图挂载相同的持久卷。在大多数情况下,只有第一个 pod 可以挂载持久卷,然后另一个 pod 会尝试挂载,如果无法挂载,它将放弃。如果持久卷只能支持 RWO(只能有一个 pod 写入),就会发生这种情况。

在以下示例中,Grafana 使用持久卷挂载/var/lib/grafana;但是,它无法扩展,因为 Google 持久磁盘是 RWO:

即使持久卷具有 RWX(多个 pod 可以同时挂载以读写),比如 NFS,如果多个 pod 尝试绑定相同的卷,它也不会抱怨。但是,我们仍然需要考虑多个应用程序实例是否可以使用相同的文件夹/文件。例如,如果将 Grafana 复制到两个或更多的 pod 中,它将与尝试写入相同的/var/lib/grafana/grafana.db的多个 Grafana 实例发生冲突,然后数据可能会损坏,如下面的截图所示:

在这种情况下,Grafana 必须使用后端数据库,如 MySQL 或 PostgreSQL,而不是 SQLite3。这样可以使多个 Grafana 实例正确读写 Grafana 元数据。

因为关系型数据库基本上支持通过网络连接多个应用程序实例,因此,这种情况非常适合多个 pod 使用。请注意,Grafana 支持使用关系型数据库作为后端元数据存储;但是,并非所有应用程序都支持关系型数据库。

对于使用 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 首先创建持久卷索赔 1Pod 1,然后创建持久卷索赔 2Pod 2,但不是同时进行。如果应用程序在应用程序引导期间注册到注册表,这将有助于管理员:

即使一个 Pod 死掉,StatefulSet 也会保留 Pod 的位置(Pod 名称、IP 地址和相关的 Kubernetes 元数据),并且持久卷也会保留。然后,它会尝试重新创建一个容器,重新分配给同一个 Pod 并挂载相同的持久卷。

使用 Kubernetes 调度程序有助于保持 Pod/持久卷的数量和应用程序保持在线:

具有持久卷的 StatefulSet 需要动态配置和StorageClass,因为 StatefulSet 可以进行扩展。当添加更多的 Pod 时,Kubernetes 需要知道如何配置持久卷。

持久卷示例

在本章中,介绍了一些持久卷示例。根据环境和场景,Kubernetes 管理员需要正确配置 Kubernetes。

以下是使用不同角色节点构建 Elasticsearch 集群以配置不同类型的持久卷的一些示例。它们将帮助您决定如何配置和管理持久卷。

Elasticsearch 集群场景

Elasticsearch 能够通过使用多个节点来设置集群。截至 Elasticsearch 版本 2.4,有几种不同类型的节点,如主节点、数据节点和协调节点(www.elastic.co/guide/en/elasticsearch/reference/2.4/modules-node.html)。每个节点在集群中有不同的角色和责任,因此相应的 Kubernetes 配置和持久卷应该与适当的设置保持一致。

以下图表显示了 Elasticsearch 节点的组件和角色。主节点是集群中唯一管理所有 Elasticsearch 节点注册和配置的节点。它还可以有一个备用节点(有资格成为主节点的节点),可以随时充当主节点:

数据节点在 Elasticsearch 中保存和操作数据存储。协调节点处理来自其他应用程序的 HTTP 请求,然后进行负载均衡/分发到数据节点。

Elasticsearch 主节点

主节点是集群中唯一的节点。此外,其他节点需要指向主节点进行注册。因此,主节点应该使用 Kubernetes StatefulSet 来分配一个稳定的 DNS 名称,例如es-master-1。因此,我们必须使用 Kubernetes 服务以无头模式分配 DNS,直接将 DNS 名称分配给 pod IP 地址。

另一方面,如果不需要持久卷,因为主节点不需要持久化应用程序的数据。

Elasticsearch 有资格成为主节点的节点

有资格成为主节点的节点是主节点的备用节点,因此不需要创建另一个Kubernetes对象。这意味着扩展主 StatefulSet 分配es-master-2es-master-3es-master-N就足够了。当主节点不响应时,在有资格成为主节点的节点中进行主节点选举,选择并提升一个节点为主节点。

Elasticsearch 数据节点

Elasticsearch 数据节点负责存储数据。此外,如果需要更大的数据容量和/或更多的查询请求,我们需要进行横向扩展。因此,我们可以使用带有持久卷的 StatefulSet 来稳定 pod 和持久卷。另一方面,不需要有 DNS 名称,因此也不需要为 Elasticsearch 数据节点设置 Kubernetes 服务。

Elasticsearch 协调节点

协调节点是 Elasticsearch 中的负载均衡器角色。因此,我们需要进行横向扩展以处理来自外部来源的 HTTP 流量,并且不需要持久化数据。因此,我们可以使用带有 Kubernetes 服务的 Kubernetes ReplicaSet 来将 HTTP 暴露给外部服务。

以下示例显示了我们在 Kubernetes 中创建所有上述 Elasticsearch 节点时使用的命令:

此外,以下截图是我们在创建上述实例后获得的结果:

在这种情况下,外部服务(Kubernetes 节点:30020)是外部应用程序的入口点。为了测试目的,让我们安装elasticsearch-headgithub.com/mobz/elasticsearch-head)来可视化集群信息。

将 Elasticsearch 协调节点连接到安装elasticsearch-head插件:

然后,访问任何 Kubernetes 节点,URL 为http://<kubernetes-node>:30200/_plugin/head。以下 UI 包含集群节点信息:

星形图标表示 Elasticsearch 主节点,三个黑色子弹是数据节点,白色圆形子弹是协调节点。

在这种配置中,如果一个数据节点宕机,不会发生任何服务影响,如下面的片段所示:

//simulate to occur one data node down 
$ kubectl delete pod es-data-0
pod "es-data-0" deleted

几分钟后,新的 pod 挂载相同的 PVC,保留了es-data-0的数据。然后 Elasticsearch 数据节点再次注册到主节点,之后集群健康状态恢复为绿色(正常),如下面的截图所示:

由于 StatefulSet 和持久卷,应用程序数据不会丢失在es-data-0上。如果需要更多的磁盘空间,增加数据节点的数量。如果需要支持更多的流量,增加协调节点的数量。如果需要备份主节点,增加主节点的数量以使一些主节点有资格。

总的来说,StatefulSet 的持久卷组合非常强大,可以使应用程序灵活和可扩展。

Kubernetes 资源管理

第三章,开始使用 Kubernetes提到 Kubernetes 有一个调度程序来管理 Kubernetes 节点,然后确定在哪里部署一个 pod。当节点有足够的资源,如 CPU 和内存时,Kubernetes 管理员可以随意部署应用程序。然而,一旦达到资源限制,Kubernetes 调度程序根据其配置行为不同。因此,Kubernetes 管理员必须了解如何配置和利用机器资源。

资源服务质量

Kubernetes 有资源 QoS服务质量)的概念,它可以帮助管理员通过不同的优先级分配和管理 pod。根据 pod 的设置,Kubernetes 将每个 pod 分类为:

  • Guaranteed pod

  • Burstable pod

  • BestEffort pod

优先级将是 Guaranteed > Burstable > BestEffort,这意味着如果 BestEffort pod 和 Guaranteed pod 存在于同一节点中,那么当其中一个 pod 消耗内存并导致节点资源短缺时,将终止其中一个 BestEffort pod 以保存 Guaranteed pod。

为了配置资源 QoS,您必须在 pod 定义中设置资源请求和/或资源限制。以下示例是 nginx 的资源请求和资源限制的定义:

$ cat burstable.yml  
apiVersion: v1 
kind: Pod 
metadata: 
  name: burstable-pod 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    resources: 
      requests: 
        cpu: 0.1 
        memory: 10Mi 
      limits: 
        cpu: 0.5 
        memory: 300Mi 

此示例指示以下内容:

资源定义类型 资源名称 含义
requests cpu 0.1 至少 10%的 1 个 CPU 核心
memory 10Mi 至少 10 兆字节的内存
limits cpu 0.5 最大 50%的 1 个 CPU 核心
memory 300Mi 最大 300 兆字节的内存

对于 CPU 资源,可接受的值表达式为核心(0.1、0.2……1.0、2.0)或毫核(100m、200m……1000m、2000m)。1000m 相当于 1 个核心。例如,如果 Kubernetes 节点有 2 个核心 CPU(或 1 个带超线程的核心),则总共有 2.0 个核心或 2000 毫核,如下所示:

如果运行 nginx 示例(requests.cpu: 0.1),它至少占用 0.1 个核心,如下图所示:

只要 CPU 有足够的空间,它可以占用最多 0.5 个核心(limits.cpu: 0.5),如下图所示:

您还可以使用kubectl describe nodes命令查看配置,如下所示:

请注意,它显示的百分比取决于前面示例中 Kubernetes 节点的规格;如您所见,该节点有 1 个核心和 600 MB 内存。

另一方面,如果超出了内存限制,Kubernetes 调度程序将确定该 pod 内存不足,然后它将终止一个 pod(OOMKilled):


//Pod is reaching to the memory limit
$ kubectl get pods
NAME            READY     STATUS    RESTARTS   AGE
burstable-pod   1/1       Running   0          10m

//got OOMKilled
$ kubectl get pods
NAME            READY     STATUS      RESTARTS   AGE
burstable-pod   0/1       OOMKilled   0          10m

//restarting Pod
$ kubectl get pods
NAME            READY     STATUS      RESTARTS   AGE
burstable-pod   0/1       CrashLoopBackOff   0   11m 

//restarted
$ kubectl get pods
NAME            READY     STATUS    RESTARTS   AGE
burstable-pod   1/1       Running   1          12m  

配置 BestEffort pod

BestEffort pod 在资源 QoS 配置中具有最低的优先级。因此,在资源短缺的情况下,该 pod 将是第一个被终止的。使用 BestEffort 的用例可能是无状态和可恢复的应用程序,例如:

  • Worker process

  • 代理或缓存节点

在资源短缺的情况下,该 pod 应该将 CPU 和内存资源让给其他优先级更高的 pod。为了将 pod 配置为 BestEffort pod,您需要将资源限制设置为 0,或者不指定资源限制。例如:

//no resource setting
$ cat besteffort-implicit.yml 
apiVersion: v1
kind: Pod
metadata:
 name: besteffort
spec:
 containers:
 - name: nginx
 image: nginx

//resource limit setting as 0
$ cat besteffort-explicit.yml 
apiVersion: v1
kind: Pod
metadata:
 name: besteffort
spec:
 containers:
 - name: nginx
 image: nginx
 resources:
 limits:
      cpu: 0
      memory: 0

请注意,资源设置是由namespace default设置继承的。因此,如果您打算使用隐式设置将 pod 配置为 BestEffort pod,如果命名空间具有以下默认资源设置,则可能不会配置为 BestEffort:

在这种情况下,如果您使用隐式设置部署到默认命名空间,它将应用默认的 CPU 请求,如request.cpu: 0.1,然后它将变成 Burstable。另一方面,如果您部署到blank-namespace,应用request.cpu: 0,然后它将变成 BestEffort。

配置为 Guaranteed pod

Guaranteed 是资源 QoS 中的最高优先级。在资源短缺的情况下,Kubernetes 调度程序将尽力保留 Guaranteed pod 到最后。

因此,Guaranteed pod 的使用将是诸如任务关键节点之类的节点:

  • 带有持久卷的后端数据库

  • 主节点(例如 Elasticsearch 主节点和 HDFS 名称节点)

为了将其配置为 Guaranteed pod,明确设置资源限制和资源请求为相同的值,或者只设置资源限制。然而,再次强调,如果命名空间具有默认资源设置,可能会导致不同的结果:

$ cat guaranteed.yml 
apiVersion: v1
kind: Pod
metadata:
 name: guaranteed-pod
spec:
 containers:
   - name: nginx
     image: nginx
     resources:
      limits:
       cpu: 0.3
       memory: 350Mi
      requests:
       cpu: 0.3
       memory: 350Mi

$ kubectl get pods
NAME             READY     STATUS    RESTARTS   AGE
guaranteed-pod   1/1       Running   0          52s

$ kubectl describe pod guaranteed-pod | grep -i qos
QoS Class:  Guaranteed

因为 Guaranteed pod 必须设置资源限制,如果您对应用程序的必要 CPU/内存资源不是 100%确定,特别是最大内存使用量;您应该使用 Burstable 设置一段时间来监视应用程序的行为。否则,即使节点有足够的内存,Kubernetes 调度程序也可能终止 pod(OOMKilled)。

配置为 Burstable pod

Burstable pod 的优先级高于 BestEffort,但低于 Guaranteed。与 Guaranteed pod 不同,资源限制设置不是强制性的;因此,在节点资源可用时,pod 可以尽可能地消耗 CPU 和内存。因此,它适用于任何类型的应用程序。

如果您已经知道应用程序的最小内存大小,您应该指定请求资源,这有助于 Kubernetes 调度程序分配到正确的节点。例如,有两个节点,每个节点都有 1GB 内存。节点 1 已经分配了 600MB 内存,节点 2 分配了 200MB 内存给其他 pod。

如果我们创建一个请求内存资源为 500 MB 的 pod,那么 Kubernetes 调度器会将此 pod 分配给节点 2。但是,如果 pod 没有资源请求,结果将在节点 1 或节点 2 之间变化。因为 Kubernetes 不知道这个 pod 将消耗多少内存:

还有一个重要的资源 QoS 行为需要讨论。资源 QoS 单位的粒度是 pod 级别,而不是容器级别。这意味着,如果您配置了一个具有两个容器的 pod,您打算将容器 A 设置为保证的(请求/限制值相同),容器 B 是可突发的(仅设置请求)。不幸的是,Kubernetes 会将此 pod 配置为可突发,因为 Kubernetes 不知道容器 B 的限制是多少。

以下示例表明未能配置为保证的 pod,最终配置为可突发的:

// supposed nginx is Guaranteed, tomcat as Burstable...
$ cat guaranteed-fail.yml 
apiVersion: v1
kind: Pod
metadata:
 name: burstable-pod
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
     limits:
       cpu: 0.3
       memory: 350Mi
     requests:
       cpu: 0.3
       memory: 350Mi
  - name: tomcat
    image: tomcat
    resources:
      requests:
       cpu: 0.2
       memory: 100Mi

$ kubectl create -f guaranteed-fail.yml 
pod "guaranteed-fail" created

//at the result, Pod is configured as Burstable
$ kubectl describe pod guaranteed-fail | grep -i qos
QoS Class:  Burstable

即使改为仅配置资源限制,但如果容器 A 只有 CPU 限制,容器 B 只有内存限制,那么结果也会再次变为可突发,因为 Kubernetes 只知道限制之一:

//nginx set only cpu limit, tomcat set only memory limit
$ cat guaranteed-fail2.yml 
apiVersion: v1
kind: Pod
metadata:
 name: guaranteed-fail2
spec:
 containers:
  - name: nginx
    image: nginx
    resources:
      limits:
       cpu: 0.3
  - name: tomcat
    image: tomcat
    resources:
      requests:
       memory: 100Mi

$ kubectl create -f guaranteed-fail2.yml 
pod "guaranteed-fail2" created

//result is Burstable again
$ kubectl describe pod |grep -i qos
QoS Class:  Burstable

因此,如果您打算将 pod 配置为保证的,必须将所有容器设置为保证的。

监控资源使用

当您开始配置资源请求和/或限制时,由于资源不足,您的 pod 可能无法被 Kubernetes 调度器部署。为了了解可分配资源和可用资源,请使用 kubectl describe nodes 命令查看状态。

以下示例显示一个节点有 600 MB 内存和一个核心 CPU。因此,可分配资源如下:

然而,这个节点已经运行了一些可突发的 pod(使用资源请求)如下:

可用内存约为 20 MB。因此,如果您提交了请求超过 20 MB 的可突发的 pod,它将永远不会被调度,如下面的截图所示:

错误事件可以通过 kubectl describe pod 命令捕获:

在这种情况下,您需要添加更多的 Kubernetes 节点来支持更多的资源。

总结

在本章中,我们已经涵盖了使用临时卷或持久卷的无状态和有状态应用程序。当应用程序重新启动或 pod 扩展时,两者都存在缺陷。此外,Kubernetes 上的持久卷管理已经得到增强,使其更容易,正如您可以从 StatefulSet 和动态配置等工具中看到的那样。

此外,资源 QoS 帮助 Kubernetes 调度器根据优先级基于请求和限制将 pod 分配给正确的节点。

下一章将介绍 Kubernetes 网络和安全性,这将使 pod 和服务的配置更加简单,并使它们更具可扩展性和安全性。

第五章:网络和安全

我们已经学会了如何在 Kubernetes 中部署具有不同资源的容器,在第三章 开始使用 Kubernetes中,以及如何使用卷来持久化数据,动态配置和不同的存储类。接下来,我们将学习 Kubernetes 如何路由流量,使所有这些成为可能。网络在软件世界中始终扮演着重要角色。我们将描述从单个主机上的容器到多个主机,最终到 Kubernetes 的网络。

  • Docker 网络

  • Kubernetes 网络

  • 入口

  • 网络策略

Kubernetes 网络

在 Kubernetes 中,您有很多选择来实现网络。Kubernetes 本身并不关心您如何实现它,但您必须满足其三个基本要求:

  • 所有容器应该彼此可访问,无需 NAT,无论它们在哪个节点上

  • 所有节点应该与所有容器通信

  • IP 容器应该以其他人看待它的方式看待自己

在进一步讨论之前,我们首先会回顾默认容器网络是如何工作的。这是使所有这些成为可能的网络支柱。

Docker 网络

在深入研究 Kubernetes 网络之前,让我们回顾一下 Docker 网络。在第二章 使用容器进行 DevOps中,我们学习了容器网络的三种模式,桥接,无和主机。

桥接是默认的网络模型。Docker 创建并附加虚拟以太网设备(也称为 veth),并为每个容器分配网络命名空间。

网络命名空间是 Linux 中的一个功能,它在逻辑上是网络堆栈的另一个副本。它有自己的路由表、arp 表和网络设备。这是容器网络的基本概念。

Veth 总是成对出现,一个在网络命名空间中,另一个在桥接中。当流量进入主机网络时,它将被路由到桥接中。数据包将被分派到它的 veth,并进入容器内部的命名空间,如下图所示:

让我们仔细看看。在以下示例中,我们将使用 minikube 节点作为 docker 主机。首先,我们必须使用minikube ssh来 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 or 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@if54if后面的数字是配对中的另一个接口 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 在预路由上创建自定义过滤器链,并动态创建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 中的容器如何相互通信。

容器间通信

Kubernetes 中的 Pod 具有自己的真实 IP 地址。Pod 中的容器共享网络命名空间,因此它们将彼此视为localhost。这是默认情况下由网络容器实现的,它充当桥接口以为 pod 中的每个容器分发流量。让我们看看以下示例中的工作原理。让我们使用第三章中的第一个示例,开始使用 Kubernetes,其中包括一个 pod 中的两个容器,nginxcentos

#cat 5-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 5-1-1_pod.yaml
pod "example" created  

然后,我们将描述 pod 并查看其容器 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 是 d9bd923572abcentos 的容器 ID 是 f4c019d289d4。如果我们使用 docker ps 进入节点 minikube/192.168.99.100,我们可以检查 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 容器在这种情况下被称为网络容器,它持有网络命名空间,让 webcentos 容器加入。在同一网络命名空间中的容器共享相同的 IP 地址和网络配置。这是 Kubernetes 中实现容器间通信的默认方式,这也是对第一个要求的映射。

Pod 间的通信

无论它们位于哪个节点,Pod IP 地址都可以从其他 Pod 中访问。这符合第二个要求。我们将在接下来的部分描述同一节点内和跨节点的 Pod 通信。

同一节点内的 Pod 通信

同一节点内的 Pod 间通信默认通过桥接完成。假设我们有两个拥有自己网络命名空间的 pod。当 pod1 想要与 pod2 通信时,数据包通过 pod1 的命名空间传递到相应的 veth 对 vethXXXX,最终到达桥接设备。桥接设备然后广播目标 IP 以帮助数据包找到它的路径,vethYYYY 响应。数据包然后到达 pod2:

然而,Kubernetes 主要是关于集群。当 pod 在不同的节点上时,流量是如何路由的呢?

节点间的 Pod 通信

根据第二个要求,所有节点必须与所有容器通信。Kubernetes 将实现委托给容器网络接口CNI)。用户可以选择不同的实现,如 L2、L3 或覆盖。覆盖网络是常见的解决方案之一,被称为数据包封装。它在离开源之前包装消息,然后传递并在目的地解包消息。这导致覆盖增加了网络延迟和复杂性。只要所有容器可以跨节点相互访问,您可以自由使用任何技术,如 L2 邻接或 L3 网关。有关 CNI 的更多信息,请参阅其规范(github.com/containernetworking/cni/blob/master/SPEC.md):

假设我们有一个从 pod1 到 pod4 的数据包。数据包从容器接口离开并到达 veth 对,然后通过桥接和节点的网络接口。网络实现在第 4 步发挥作用。只要数据包能够路由到目标节点,您可以自由使用任何选项。在下面的示例中,我们将使用--network-plugin=cni选项启动 minikube。启用 CNI 后,参数将通过节点中的 kubelet 传递。Kubelet 具有默认的网络插件,但在启动时可以探测任何支持的插件。在启动 minikube 之前,如果已经启动,您可以首先使用minikube stop,或者在进一步操作之前使用minikube delete彻底删除整个集群。尽管 minikube 是一个单节点环境,可能无法完全代表我们将遇到的生产场景,但这只是让您对所有这些工作原理有一个基本的了解。我们将在第九章的在 AWS 上的 Kubernetes和第十章的在 GCP 上的 Kubernetes中学习网络选项的部署。

// start minikube with cni option
# minikube start --network-plugin=cni
...
Kubectl is now configured to use the cluster.  

当我们指定network-plugin选项时,它将在启动时使用--network-plugin-dir中指定的目录中的插件。在 CNI 插件中,默认的插件目录是/opt/cni/net.d。集群启动后,让我们登录到节点并通过minikube ssh查看内部设置:

# 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 5-1-1_pod.yaml
pod "example" created
# kubectl describe po example
Name:       example
Namespace:  default
Node:       minikube/192.168.99.100
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/的配置:

# 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"
      }
 ]
 }
}

在这个例子中,我们使用桥接 CNI 插件来重用用于 pod 容器的 L2 桥接。如果数据包来自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 到 service 的通信

Kubernetes 是动态的。Pod 不断地被创建和删除。Kubernetes 服务是一个抽象,通过标签选择器定义一组 pod。我们通常使用服务来访问 pod,而不是明确指定一个 pod。当我们创建一个服务时,将创建一个endpoint对象,描述了该服务中标签选择器选择的一组 pod IP。

在某些情况下,创建服务时不会创建endpoint对象。例如,没有选择器的服务不会创建相应的endpoint对象。有关更多信息,请参阅第三章中没有选择器的服务部分,开始使用 Kubernetes

那么,流量是如何从一个 pod 到 service 后面的 pod 的呢?默认情况下,Kubernetes 使用 iptables 通过kube-proxy执行这个魔术。这在下图中有解释。

让我们重用第三章中的3-2-3_rc1.yaml3-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 3-2-3_rc1.yaml
replicationcontroller "nginx-1.12" created
# kubectl create -f 3-2-3_nodeport.yaml
service "nginx-nodeport" created  

让我们观察 iptables 规则,看看它是如何工作的。如下所示,我们的服务 IP 是10.0.0.167,下面的两个 pod IP 地址分别是10.1.0.410.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-SVVBOHTYP7PAP3J5KUBE-SEP-AYS7I6ZPYFC6YNNF,统计模式为随机概率 0.5。这意味着,iptables 将生成一个随机数,并根据概率分布 0.5 调整目标。这两个自定义链的DNAT目标设置为相应的 pod IP。DNAT目标负责更改数据包的目标 IP 地址。默认情况下,当流量进入时,启用 conntrack 来跟踪连接的目标和源。所有这些都导致了一种路由行为。当流量到达服务时,iptables 将随机选择一个 pod 进行路由,并将目标 IP 从服务 IP 修改为真实的 pod IP,并取消 DNAT 以返回全部路由。

外部到服务的通信

为了能够为 Kubernetes 提供外部流量是至关重要的。Kubernetes 提供了两个 API 对象来实现这一点:

  • 服务: 外部网络负载均衡器或 NodePort(L4)

  • 入口: HTTP(S)负载均衡器(L7)

对于入口,我们将在下一节中学到更多。我们先专注于 L4。根据我们对节点间 pod 到 pod 通信的了解,数据包在服务和 pod 之间进出的方式。下图显示了它的工作原理。假设我们有两个服务,一个服务 A 有三个 pod(pod a,pod b 和 pod c),另一个服务 B 只有一个 pod(pod d)。当流量从负载均衡器进入时,数据包将被分发到其中一个节点。大多数云负载均衡器本身并不知道 pod 或容器。它只知道节点。如果节点通过了健康检查,那么它将成为目的地的候选者。假设我们想要访问服务 B,它目前只在一个节点上运行一个 pod。然而,负载均衡器将数据包发送到另一个没有我们想要的任何 pod 运行的节点。流量路由将如下所示:

数据包路由的过程将是:

  1. 负载均衡器将选择一个节点来转发数据包。在 GCE 中,它根据源 IP 和端口、目标 IP 和端口以及协议的哈希选择实例。在 AWS 中,它基于循环算法。

  2. 在这里,路由目的地将被更改为 pod d(DNAT),并将其转发到另一个节点,类似于节点间的 pod 到 pod 通信。

  3. 然后,服务到 Pod 的通信。数据包到达 Pod d,响应相应地。

  4. Pod 到服务的通信也受 iptables 控制。

  5. 数据包将被转发到原始节点。

  6. 源和目的地将被解除 DNAT 并发送回负载均衡器和客户端。

在 Kubernetes 1.7 中,服务中有一个名为externalTrafficPolicy的新属性。您可以将其值设置为 local,然后在流量进入节点后,Kubernetes 将路由该节点上的 Pod(如果有)。

Ingress

Kubernetes 中的 Pod 和服务都有自己的 IP;然而,通常不是您提供给外部互联网的接口。虽然有配置了节点 IP 的服务,但节点 IP 中的端口不能在服务之间重复。决定将哪个端口与哪个服务管理起来是很麻烦的。此外,节点来去匆匆,将静态节点 IP 提供给外部服务并不明智。

Ingress 定义了一组规则,允许入站连接访问 Kubernetes 集群服务。它将流量带入集群的 L7 层,在每个 VM 上分配和转发一个端口到服务端口。这在下图中显示。我们定义一组规则,并将它们作为源类型 ingress 发布到 API 服务器。当流量进来时,ingress 控制器将根据 ingress 规则履行和路由 ingress。如下图所示,ingress 用于通过不同的 URL 将外部流量路由到 kubernetes 端点:

现在,我们将通过一个示例来看看这是如何工作的。在这个例子中,我们将创建两个名为nginxechoserver的服务,并配置 ingress 路径/welcome/echoserver。我们可以在 minikube 中运行这个。旧版本的 minikube 默认不启用 ingress;我们需要先启用它:

// 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 中启用 ingress 将创建一个 nginx ingress 控制器和一个ConfigMap来存储 nginx 配置(参考github.com/kubernetes/ingress/blob/master/controllers/nginx/README.md),以及一个 RC 和一个服务作为默认的 HTTP 后端,用于处理未映射的请求。我们可以通过在kubectl命令中添加--namespace=kube-system来观察它们。接下来,让我们创建我们的后端资源。这是我们的 nginx DeploymentService

# cat 5-2-1_nginx.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: nginx
spec:
 replicas: 2
 template:
 metadata:
 labels:
 project: chapter5
 service: nginx
 spec:
 containers:
 - name: nginx
 image: nginx
 ports:
 - containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
 name: nginx
spec:
 type: NodePort
  selector:
 project: chapter5
 service: nginx
 ports:
 - protocol: TCP
 port: 80
 targetPort: 80
// create nginx RS and service
# kubectl create -f 5-2-1_nginx.yaml
deployment "nginx" created
service "nginx" created

然后,我们将创建另一个带有 RS 的服务:

// another backend named echoserver
# cat 5-2-1_echoserver.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: echoserver
spec:
 replicas: 1
 template:
 metadata:
 name: echoserver
 labels:
 project: chapter5
 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: chapter5
 service: echoserver
 ports:
 - protocol: TCP
 port: 8080
 targetPort: 8080

// create RS and SVC by above configuration file
# kubectl create -f 5-2-1_echoserver.yaml
deployment "echoserver" created
service "echoserver" created  

接下来,我们将创建 ingress 资源。有一个名为ingress.kubernetes.io/rewrite-target的注释。如果服务请求来自根 URL,则需要此注释。如果没有重写注释,我们将得到 404 作为响应。有关 nginx ingress 控制器中更多支持的注释,请参阅github.com/kubernetes/ingress/blob/master/controllers/nginx/configuration.md#annotations

# cat 5-2-1_ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: ingress-example
 annotations:
 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 5-2-1_ingress.yaml
ingress "ingress-example" created

在一些云提供商中,支持服务负载均衡器控制器。它可以通过配置文件中的status.loadBalancer.ingress语法与 ingress 集成。有关更多信息,请参阅github.com/kubernetes/contrib/tree/master/service-loadbalancer

由于我们的主机设置为devops.k8s,只有在从该主机名访问时才会返回。您可以在 DNS 服务器中配置 DNS 记录,或者在本地修改 hosts 文件。为简单起见,我们将在主机文件中添加一行,格式为ip hostname

// 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.7 版本。

网络策略

网络策略作为 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.yaml (github.com/projectcalico/calico/blob/master/v2.4/getting-started/kubernetes/installation/hosted/calico.yaml))文件来创建 Calico 节点和策略控制器。需要配置etcd_endpoints。要找出 etcd 的 IP,我们需要访问 localkube 资源。

// find out etcd ip
# minikube ssh -- "sudo /usr/local/bin/localkube --host-ip"
2017-07-27 04:10:58.941493 I | proto: duplicate proto type registered: google.protobuf.Any
2017-07-27 04:10:58.941822 I | proto: duplicate proto type registered: google.protobuf.Duration
2017-07-27 04:10:58.942028 I | proto: duplicate proto type registered: google.protobuf.Timestamp
localkube host ip:  10.0.2.15  

etcd 的默认端口是2379。在这种情况下,我们将在calico.yaml中修改etcd_endpoint,从http://127.0.0.1:2379改为http://10.0.2.15:2379

// launch calico
# kubectl apply -f calico.yaml
configmap "calico-config" created
secret "calico-etcd-secrets" created
daemonset "calico-node" created
deployment "calico-policy-controller" created
job "configure-calico" created

// list the pods in kube-system
# kubectl get pods --namespace=kube-system
NAME                                        READY     STATUS    RESTARTS   AGE
calico-node-ss243                           2/2       Running   0          1m
calico-policy-controller-2249040168-r2270   1/1       Running   0          1m  

让我们重用5-2-1_nginx.yaml作为示例:

# kubectl create -f 5-2-1_nginx.yaml
replicaset "nginx" created
service "nginx" created
// list the services
# kubectl get svc
NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
kubernetes   10.0.0.1     <none>        443/TCP        47m
nginx        10.0.0.42    <nodes>       80:31071/TCP   5m

我们将发现我们的 nginx 服务的 IP 是10.0.0.42。让我们启动一个简单的 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.0.0.42 
Connecting to 10.0.0.42 (10.0.0.42:80)  

--spider参数用于检查 URL 是否存在。在这种情况下,busybox 可以成功访问 nginx。接下来,让我们将NetworkPolicy应用到我们的 nginx pod 中:

// declare a network policy
# cat 5-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: chapter5  

我们可以在这里看到一些重要的语法。podSelector用于选择 pod,应该与目标 pod 的标签匹配。另一个是ingress[].from[].podSelector,用于定义谁可以访问这些 pod。在这种情况下,所有具有project=chapter5标签的 pod 都有资格访问具有server=nginx标签的 pod。如果我们回到我们的 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.0.0.42
Connecting to 10.0.0.42 (10.0.0.42:80)
wget: download timed out  

我们可以使用kubectl edit deployment busybox将标签project=chaper5添加到 busybox pod 中。

如果忘记如何操作,请参考第三章中的标签和选择器部分,开始使用 Kubernetes

之后,我们可以再次联系 nginx pod:

// inside busybox pod
/ # wget --spider 10.0.0.42 
Connecting to 10.0.0.42 (10.0.0.42:80)  

通过前面的例子,我们了解了如何应用网络策略。我们还可以通过调整选择器来应用一些默认策略,拒绝所有或允许所有。例如,拒绝所有的行为可以通过以下方式实现:

# cat 5-3-1_np_denyall.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
 name: default-deny
spec:
 podSelector:  

这样,所有不匹配标签的 pod 将拒绝所有其他流量。或者,我们可以创建一个NetworkPolicy,其入口列表来自任何地方。然后,运行在这个命名空间中的 pod 可以被任何其他人访问。

# cat 5-3-1_np_allowall.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
 name: allow-all
spec:
 podSelector:
 ingress:
 - {}  

总结

在这一章中,我们学习了容器之间如何进行通信是至关重要的,并介绍了 pod 与 pod 之间的通信工作原理。Service 是一个抽象概念,可以将流量路由到任何匹配标签选择器的 pod 下面。我们学习了 service 如何通过 iptables 魔术与 pod 配合工作。我们了解了数据包如何从外部路由到 pod 以及 DNAT、un-DAT 技巧。我们还学习了新的 API 对象,比如ingress,它允许我们使用 URL 路径来路由到后端的不同服务。最后,还介绍了另一个对象NetworkPolicy。它提供了第二层安全性,充当软件防火墙规则。通过网络策略,我们可以使某些 pod 只与某些 pod 通信。例如,只有数据检索服务可以与数据库容器通信。所有这些都使 Kubernetes 更加灵活、安全和强大。

到目前为止,我们已经学习了 Kubernetes 的基本概念。接下来,我们将通过监控集群指标和分析 Kubernetes 的应用程序和系统日志,更清楚地了解集群内部发生了什么。监控和日志工具对于每个 DevOps 来说都是必不可少的,它们在 Kubernetes 等动态集群中也扮演着极其重要的角色。因此,我们将深入了解集群的活动,如调度、部署、扩展和服务发现。下一章将帮助您更好地了解在现实世界中操作 Kubernetes 的行为。

第六章:监控和日志记录

监控和日志记录是站点可靠性的重要组成部分。我们已经学会了如何利用各种控制器来管理我们的应用程序,以及如何利用服务和 Ingress 一起为我们的 Web 应用程序提供服务。接下来,在本章中,我们将学习如何通过以下主题跟踪我们的应用程序:

  • 获取容器的状态快照

  • Kubernetes 中的监控

  • 通过 Prometheus 汇总 Kubernetes 的指标

  • Kubernetes 中日志记录的概念

  • 使用 Fluentd 和 Elasticsearch 进行日志记录

检查一个容器

每当我们的应用程序表现异常时,我们肯定会想知道发生了什么,通过各种手段,比如检查日志、资源使用情况、进程监视器,甚至直接进入运行的主机来排查问题。在 Kubernetes 中,我们有kubectl getkubectl describe可以查询部署状态,这将帮助我们确定应用程序是否已崩溃或按预期工作。

此外,如果我们想要了解应用程序的输出发生了什么,我们还有kubectl logs,它将容器的stdout重定向到我们的终端。对于 CPU 和内存使用统计,我们还可以使用类似 top 的命令kubectl topkubectl top node提供了节点资源使用情况的概览,kubectl top pod <POD_NAME>显示了每个 pod 的使用情况:

# kubectl top node
NAME        CPU(cores)   CPU%      MEMORY(bytes)  MEMORY% 
node-1      42m          4%        273Mi           12% 
node-2      152m         15%       1283Mi          75% 

# kubectl top pod mypod-name-2587489005-xq72v
NAME                         CPU(cores)   MEMORY(bytes) 
mypod-name-2587489005-xq72v   0m           0Mi            

要使用kubectl top,您需要在集群中部署 Heapster。我们将在本章后面讨论这个问题。

如果我们遗留了一些日志在容器内而没有发送到任何地方怎么办?我们知道有一个docker exec在运行的容器内执行命令,但我们不太可能每次都能访问节点。幸运的是,kubectl允许我们使用kubectl exec命令做同样的事情。它的用法类似于 Docker。例如,我们可以像这样在 pod 中的容器内运行一个 shell:

$ kubectl exec -it mypod-name-2587489005-xq72v /bin/sh
/ # 
/ # hostname
mypod-name-2587489005-xq72v  

这与通过 SSH 登录主机几乎相同,并且它使我们能够使用我们熟悉的工具进行故障排除,就像我们在非容器世界中所做的那样。

Kubernetes 仪表板

除了命令行实用程序之外,还有一个仪表板,它在一个体面的 Web-UI 上汇总了我们刚刚讨论的几乎所有信息:

实际上,它是 Kubernetes 集群的通用图形用户界面,因为它还允许我们创建、编辑和删除资源。部署它非常容易;我们所需要做的就是应用一个模板:

$ kubectl create -f \ https://raw.githubusercontent.com/kubernetes/dashboard/v1.6.3/src/deploy/kubernetes-dashboard.yaml  

此模板适用于启用了RBAC(基于角色的访问控制)的 Kubernetes 集群。如果您需要其他部署选项,请查看仪表板的项目存储库(github.com/kubernetes/dashboard)。关于 RBAC,我们将在第八章中讨论,集群管理。许多托管的 Kubernetes 服务(例如 Google 容器引擎)在集群中预先部署了仪表板,因此我们不需要自行安装。要确定仪表板是否存在于我们的集群中,请使用kubectl cluster-info

如果已安装,我们将看到 kubernetes-dashboard 正在运行...。使用默认模板部署的仪表板服务或由云提供商提供的服务通常是 ClusterIP。为了访问它,我们需要在我们的终端和 Kubernetes 的 API 服务器之间建立代理,使用kubectl proxy。一旦代理启动,我们就能够在http://localhost:8001/ui上访问仪表板。端口8001kubectl proxy的默认端口。

kubectl top一样,您需要在集群中部署 Heapster 才能查看 CPU 和内存统计信息。

Kubernetes 中的监控

由于我们现在知道如何在 Kubernetes 中检查我们的应用程序,所以我们应该有一种机制来不断地这样做,以便在第一次发生任何事件时检测到。换句话说,我们需要一个监控系统。监控系统从各种来源收集指标,存储和分析接收到的数据,然后响应异常。在应用程序监控的经典设置中,我们至少会从基础设施的三个不同层面收集指标,以确保我们服务的可用性和质量。

应用程序

我们在这个层面关心的数据涉及应用程序的内部状态,这可以帮助我们确定服务内部发生了什么。例如,以下截图来自 Elasticsearch Marvel(www.elastic.co/guide/en/marvel/current/introduction.html),从版本 5 开始称为监控,这是 Elasticsearch 集群的监控解决方案。它汇集了关于我们集群的信息,特别是 Elasticsearch 特定的指标:

此外,我们将利用性能分析工具与跟踪工具来对我们的程序进行仪器化,这增加了我们检查服务的细粒度维度。特别是在当今,一个应用可能以分布式方式由数十个服务组成。如果不使用跟踪工具,比如 OpenTracing(opentracing.io)的实现,要识别性能问题可能会非常困难。

主机

在主机级别收集任务通常是由监控框架提供的代理完成的。代理提取并发送有关主机的全面指标,如负载、磁盘、连接或进程状态等,以帮助确定主机的健康状况。

外部资源

除了上述两个组件之外,我们还需要检查依赖组件的状态。例如,假设我们有一个消耗队列并执行相应任务的应用;我们还应该关注一些指标,比如队列长度和消耗速率。如果消耗速率低而队列长度不断增长,我们的应用可能遇到了问题。

这些原则也适用于 Kubernetes 上的容器,因为在主机上运行容器几乎与运行进程相同。然而,由于 Kubernetes 上的容器和传统主机上利用资源的方式之间存在微妙的区别,当采用监控策略时,我们仍需要考虑这些差异。例如,Kubernetes 上的应用的容器可能分布在多个主机上,并且也不总是在同一主机上。如果我们仍在采用以主机为中心的监控方法,要对一个应用进行一致的记录将会非常困难。因此,我们不应该仅观察主机级别的资源使用情况,而应该在我们的监控堆栈中增加一个容器层。此外,由于 Kubernetes 实际上是我们应用的基础设施,我们绝对应该考虑它。

容器

正如前面提到的,容器级别收集的指标和主机级别得到的指标基本上是相同的,特别是系统资源的使用情况。尽管看起来有些多余,但这正是帮助我们解决监控移动容器困难的关键。这个想法非常简单:我们需要将逻辑信息附加到指标上,比如 pod 标签或它们的控制器名称。这样,来自不同主机上的容器的指标可以被有意义地分组。考虑下面的图表;假设我们想知道App 2上传输的字节数(tx),我们可以对App 2标签上的tx指标求和,得到20 MB

另一个区别是,CPU 限制的指标仅在容器级别上报告。如果在某个应用程序遇到性能问题,但主机上的 CPU 资源是空闲的,我们可以检查是否受到了相关指标的限制。

Kubernetes

Kubernetes 负责管理、调度和编排我们的应用程序。因此,一旦应用程序崩溃,Kubernetes 肯定是我们想要查看的第一个地方。特别是在部署新版本后发生崩溃时,相关对象的状态将立即在 Kubernetes 上反映出来。

总之,应该监控的组件如下图所示:

获取 Kubernetes 的监控要点

对于监控堆栈的每一层,我们总是可以找到相应的收集器。例如,在应用程序级别,我们可以手动转储指标;在主机级别,我们会在每个主机上安装一个指标收集器;至于 Kubernetes,有用于导出我们感兴趣的指标的 API,至少我们手头上有kubectl

当涉及到容器级别的收集器时,我们有哪些选择?也许在我们的应用程序镜像中安装主机指标收集器可以胜任,但我们很快就会意识到,这可能会使我们的容器在大小和资源利用方面变得过于笨重。幸运的是,已经有了针对这种需求的解决方案,即 cAdvisor(github.com/google/cadvisor),这是容器级别的指标收集器的答案。简而言之,cAdvisor 汇总了机器上每个运行容器的资源使用情况和性能统计。请注意,cAdvisor 的部署是每个主机一个,而不是每个容器一个,这对于容器化应用程序来说更为合理。在 Kubernetes 中,我们甚至不需要关心部署 cAdvisor,因为它已经嵌入到 kubelet 中。

cAdvisor 可以通过每个节点的端口4194访问。在 Kubernetes 1.7 之前,cAdvisor 收集的数据也可以通过 kubelet 端口(10250/10255)进行收集。要访问 cAdvisor,我们可以通过实例端口4194或通过kubectl proxyhttp://localhost:8001/api/v1/nodes/<nodename>:4194/proxy/访问,或直接访问http://<node-ip>:4194/

以下截图是来自 cAdvisor Web UI。一旦连接,您将看到类似的页面。要查看 cAdvisor 抓取的指标,请访问端点/metrics

监控管道中的另一个重要组件是 Heapster(github.com/kubernetes/heapster)。它从每个节点检索监控统计信息,特别是处理节点上的 kubelet,并在之后写入外部接收器。它还通过 REST API 公开聚合指标。Heapster 的功能听起来与 cAdvisor 有些多余,但在实践中它们在监控管道中扮演不同的角色。Heapster 收集集群范围的统计信息;cAdvisor 是一个主机范围的组件。也就是说,Heapster 赋予 Kubernetes 集群基本的监控能力。以下图表说明了它如何与集群中的其他组件交互:

事实上,如果您的监控框架提供了类似的工具,也可以从 kubelet 中抓取指标,那么安装 Heapster 就不是必需的。然而,由于它是 Kubernetes 生态系统中的默认监控组件,许多工具都依赖于它,例如前面提到的 kubectl top 和 Kubernetes 仪表板。

在部署 Heapster 之前,请检查您正在使用的监控工具是否作为此文档中的 Heapster sink 支持:github.com/kubernetes/heapster/blob/master/docs/sink-configuration.md

如果没有,我们可以使用独立的设置,并通过应用此模板使仪表板和 kubectl top 工作:

$ kubectl create -f \
    https://raw.githubusercontent.com/kubernetes/heapster/master/deploy/kube-config/standalone/heapster-controller.yaml  

如果启用了 RBAC,请记得应用此模板:

$ kubectl create -f \ https://raw.githubusercontent.com/kubernetes/heapster/master/deploy/kube-config/rbac/heapster-rbac.yaml

安装完 Heapster 后,kubectl top 命令和 Kubernetes 仪表板应该正确显示资源使用情况。

虽然 cAdvisor 和 Heapster 关注物理指标,但我们也希望在监控仪表板上显示对象的逻辑状态。kube-state-metrics (github.com/kubernetes/kube-state-metrics) 是完成我们监控堆栈的重要组成部分。它监视 Kubernetes 主节点,并将我们从 kubectl getkubectl describe 中看到的对象状态转换为 Prometheus 格式的指标 (prometheus.io/docs/instrumenting/exposition_formats/)。只要监控系统支持这种格式,我们就可以将状态抓取到指标存储中,并在诸如无法解释的重启计数等事件上收到警报。要安装 kube-state-metrics,首先在项目存储库的 kubernetes 文件夹中下载模板(github.com/kubernetes/kube-state-metrics/tree/master/kubernetes),然后应用它们:

$ kubectl apply -f kubernetes

之后,我们可以在其服务端点的指标中查看集群内的状态:

http://kube-state-metrics.kube-system:8080/metrics

实际监控

到目前为止,我们已经学到了很多关于在 Kubernetes 中制造一个无懈可击的监控系统的原则,现在是时候实施一个实用的系统了。因为绝大多数 Kubernetes 组件都在 Prometheus 格式的传统路径上公开了他们的仪表盘指标,所以只要工具理解这种格式,我们就可以自由地使用我们熟悉的任何监控工具。在本节中,我们将使用一个开源项目 Prometheus(prometheus.io)来设置一个示例,它是一个独立于平台的监控工具。它在 Kubernetes 生态系统中的流行不仅在于其强大性,还在于它得到了Cloud Native Computing Foundationwww.cncf.io/)的支持,后者也赞助了 Kubernetes 项目。

遇见 Prometheus

Prometheus 框架包括几个组件,如下图所示:

与所有其他监控框架一样,Prometheus 依赖于从系统组件中抓取统计数据的代理,这些代理位于图表左侧的出口处。除此之外,Prometheus 采用了拉取模型来收集指标,这意味着它不是被动地接收指标,而是主动地从出口处拉取数据。如果一个应用程序公开了指标的出口,Prometheus 也能够抓取这些数据。默认的存储后端是嵌入式 LevelDB,可以切换到其他远程存储,比如 InfluxDB 或 Graphite。Prometheus 还负责根据预先配置的规则发送警报给Alert managerAlert manager负责发送警报任务。它将接收到的警报分组并将它们分发给实际发送消息的工具,比如电子邮件、Slack、PagerDuty 等等。除了警报,我们还希望可视化收集到的指标,以便快速了解我们的系统情况,这时 Grafana 就派上用场了。

部署 Prometheus

我们为本章准备的模板可以在这里找到:

github.com/DevOps-with-Kubernetes/examples/tree/master/chapter6

在 6-1_prometheus 下是本节的清单,包括 Prometheus 部署、导出器和相关资源。它们将在专用命名空间monitoring中安装,除了需要在kube-system命名空间中工作的组件。请仔细查看它们,现在让我们按以下顺序创建资源。

$ kubectl apply -f monitoring-ns.yml
$ kubectl apply -f prometheus/config/prom-config-default.yml
$ kubectl apply -f prometheus  

资源的使用限制在提供的设置中相对较低。如果您希望以更正式的方式使用它们,建议根据实际要求调整参数。在 Prometheus 服务器启动后,我们可以通过kubectl port-forward连接到端口9090的 Web-UI。如果相应地修改其服务(prometheus/prom-svc.yml),我们可以使用 NodePort 或 Ingress 连接到 UI。当进入 UI 时,我们将看到 Prometheus 的表达式浏览器,在那里我们可以构建查询和可视化指标。在默认设置下,Prometheus 将从自身收集指标。所有有效的抓取目标都可以在路径/targets下找到。要与 Prometheus 交流,我们必须对其语言PromQL有一些了解。

使用 PromQL

PromQL 有三种数据类型:即时向量、范围向量和标量。即时向量是经过采样的数据时间序列;范围向量是一组包含在一定时间范围内的时间序列;标量是一个数值浮点值。存储在 Prometheus 中的指标通过指标名称和标签进行识别,我们可以通过表达式浏览器旁边的下拉列表找到任何收集的指标名称。如果我们使用指标名称,比如http_requests_total,我们会得到很多结果,因为即时向量匹配名称但具有不同的标签。同样,我们也可以使用{}语法仅查询特定的标签集。例如,查询{code="400",method="get"}表示我们想要任何具有标签codemethod分别等于400get的指标。在查询中结合名称和标签也是有效的,比如http_requests_total{code="400",method="get"}。PromQL 赋予了我们检查应用程序或系统的侦探能力,只要相关指标被收集。

除了刚才提到的基本查询之外,PromQL 还有很多其他内容,比如使用正则表达式和逻辑运算符查询标签,使用函数连接和聚合指标,甚至在不同指标之间执行操作。例如,以下表达式给出了kube-system命名空间中kube-dns部署消耗的总内存:

sum(container_memory_usage_bytes{namespace="kube-system", pod_name=~"kube-dns-(\\d+)-.*"} ) / 1048576

更详细的文档可以在 Prometheus 的官方网站找到(prometheus.io/docs/querying/basics/),它肯定会帮助您释放 Prometheus 的力量。

在 Kubernetes 中发现目标

由于 Prometheus 只从它知道的端点中提取指标,我们必须明确告诉它我们想要从哪里收集数据。在路径/config下是列出当前配置的目标以进行提取的页面。默认情况下,会有一个任务来收集有关 Prometheus 本身的当前指标,它位于传统的抓取路径/metrics中。如果连接到端点,我们会看到一个非常长的文本页面:

$ kubectl exec -n monitoring prometheus-1496092314-jctr6 -- \
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"} 2.4032e-05
go_gc_duration_seconds{quantile="0.25"} 3.7359e-05
go_gc_duration_seconds{quantile="0.5"} 4.1723e-05
...

这只是我们已经多次提到的 Prometheus 指标格式。下次当我们看到这样的页面时,我们会知道这是一个指标端点。

抓取 Prometheus 的默认作业被配置为静态目标。然而,考虑到 Kubernetes 中的容器是动态创建和销毁的事实,要找出容器的确切地址,更不用说在 Prometheus 上设置它,真的很麻烦。在某些情况下,我们可以利用服务 DNS 作为静态指标目标,但这仍然不能解决所有情况。幸运的是,Prometheus 通过其发现 Kubernetes 内部服务的能力帮助我们克服了这个问题。

更具体地说,它能够查询 Kubernetes 有关正在运行的服务的信息,并根据情况将其添加或删除到目标配置中。目前支持四种发现机制:

  • 节点发现模式为每个节点创建一个目标,默认情况下目标端口将是 kubelet 的端口。

  • 服务发现模式为每个service对象创建一个目标,并且服务中定义的所有端口都将成为抓取目标。

  • pod发现模式的工作方式与服务发现角色类似,也就是说,它为每个 pod 创建目标,并且对于每个 pod,它会公开所有定义的容器端口。如果在 pod 的模板中没有定义端口,它仍然会只创建一个带有其地址的抓取目标。

  • 端点模式发现了由服务创建的endpoint对象。例如,如果一个服务由三个具有两个端口的 pod 支持,那么我们将有六个抓取目标。此外,对于一个 pod,不仅会发现暴露给服务的端口,还会发现其他声明的容器端口。

以下图表说明了四种发现机制:左侧是 Kubernetes 中的资源,右侧是 Prometheus 中创建的目标:

一般来说,并非所有暴露的端口都作为指标端点提供服务,因此我们当然不希望 Prometheus 抓取集群中的所有内容,而只收集标记的资源。为了实现这一点,Prometheus 利用资源清单上的注释来区分哪些目标应该被抓取。注释格式如下:

  • 在 pod 上:如果一个 pod 是由 pod 控制器创建的,请记住在 pod 规范中设置 Prometheus 注释,而不是在 pod 控制器中:

  • prometheus.io/scrapetrue表示应该拉取此 pod。

  • prometheus.io/path:将此注释设置为公开指标的路径;只有在目标 pod 使用除/metrics之外的路径时才需要设置。

  • prometheus.io/port:如果定义的端口与实际指标端口不同,请使用此注释进行覆盖。

  • 在服务上:由于端点大多数情况下不是手动创建的,端点发现使用从服务继承的注释。也就是说,服务上的注释同时影响服务和端点发现模式。因此,我们将使用prometheus.io/scrape: 'true'来表示由服务创建的端点应该被抓取,并使用prometheus.io/probe: 'true'来标记具有指标的服务。此外,prometheus.io/scheme指定了使用http还是https。除此之外,路径和端口注释在这里也起作用。

以下模板片段指示了 Prometheus 的端点发现角色,但选择在9100/prom上创建目标的服务发现角色。

apiVersion: v1 
kind: Service 
metadata: 
  annotations: 
    prometheus.io/scrape: 'true' 
    prometheus.io/path: '/prom' 
... 
spec: 
  ports: 
 - port: 9100 

我们的示例存储库中的模板prom-config-k8s.yml包含了为 Prometheus 发现 Kubernetes 资源的配置。使用以下命令应用它:

$ kubectl apply -f prometheus/config/prom-config-k8s.yml  

因为它是一个 ConfigMap,需要几秒钟才能变得一致。之后,通过向进程发送SIGHUP来重新加载 Prometheus:

$ kubectl exec -n monitoring ${PROM_POD_NAME} -- kill -1 1

提供的模板基于 Prometheus 官方存储库中的示例;您可以在这里找到更多用法:

github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml

此外,文档页面详细描述了 Prometheus 配置的工作原理:

prometheus.io/docs/operating/configuration/

从 Kubernetes 中收集数据

现在,实施之前在 Prometheus 中讨论的五个监控层的步骤已经非常清晰:安装导出器,使用适当的标签对其进行注释,然后在自动发现的端点上收集它们。

Prometheus 中的主机层监控是由节点导出器(github.com/prometheus/node_exporter)完成的。它的 Kubernetes 清单可以在本章的示例中找到,其中包含一个带有抓取注释的 DaemonSet。使用以下命令安装它:

$ kubectl apply -f exporters/prom-node-exporter.yml

其相应的配置将由 pod 发现角色创建。

容器层收集器应该是 cAdvisor,并且已经安装在 kubelet 中。因此,发现它并使用节点模式是我们需要做的唯一的事情。

Kubernetes 监控是由 kube-state-metrics 完成的,之前也有介绍。更好的是,它带有 Prometheus 注释,这意味着我们无需进行任何额外的配置。

到目前为止,我们已经基于 Prometheus 建立了一个强大的监控堆栈。关于应用程序和外部资源的监控,Prometheus 生态系统中有大量的导出器来支持监控系统内部的各种组件。例如,如果我们需要我们的 MySQL 数据库的统计数据,我们可以安装 MySQL Server Exporter(github.com/prometheus/mysqld_exporter),它提供了全面和有用的指标。

除了已经描述的那些指标之外,还有一些来自 Kubernetes 组件的其他有用的指标,在各种方面起着重要作用:

  • Kubernetes API 服务器:API 服务器在/metrics上公开其状态,并且此目标默认启用。

  • kube-controller-manager:这个组件在端口10252上公开指标,但在一些托管的 Kubernetes 服务上是不可见的,比如Google Container EngineGKE)。如果您在自托管的集群上,应用"kubernetes/self/kube-controller-manager-metrics-svc.yml"会为 Prometheus 创建端点。

  • kube-scheduler:它使用端口10251,在 GKE 集群上也是不可见的。"kubernetes/self/kube-scheduler-metrics-svc.yml"是创建一个指向 Prometheus 的目标的模板。

  • kube-dns:kube-dns pod 中有两个容器,dnsmasqsky-dns,它们的指标端口分别是1005410055。相应的模板是kubernetes/self/ kube-dns-metrics-svc.yml

  • etcd:etcd 集群也在端口4001上有一个 Prometheus 指标端点。如果您的 etcd 集群是自托管的并由 Kubernetes 管理,您可以将"kubernetes/self/etcd-server.yml"作为参考。

  • Nginx ingress controller:nginx 控制器在端口10254发布指标。但是这些指标只包含有限的信息。要获取诸如按主机或路径计算的连接计数等数据,您需要在控制器中激活vts模块以增强收集的指标。

使用 Grafana 查看指标

表达式浏览器有一个内置的图形面板,使我们能够看到可视化的指标,但它并不是设计用来作为日常例行工作的可视化仪表板。Grafana 是 Prometheus 的最佳选择。我们已经在第四章中讨论了如何设置 Grafana,与存储和资源一起工作,我们还为本章提供了模板;这两个选项都能胜任工作。

要在 Grafana 中查看 Prometheus 指标,我们首先必须添加一个数据源。连接到我们的 Prometheus 服务器需要以下配置:

  • 类型:"Prometheus"

  • 网址:http://prometheus-svc.monitoring:9090

  • 访问:代理

一旦连接上,我们就可以导入一个仪表板来看到实际的情况。在 Grafana 的共享页面(grafana.com/dashboards?dataSource=prometheus)上有丰富的现成仪表板。以下截图来自仪表板#1621

因为图形是由 Prometheus 的数据绘制的,只要我们掌握 PromQL,我们就能绘制任何我们关心的数据。

记录事件

使用系统状态的定量时间序列进行监控,能够迅速查明系统中哪些组件出现故障,但仍然不足以诊断出症候的根本原因。因此,一个收集、持久化和搜索日志的日志系统对于通过将事件与检测到的异常相关联来揭示出出现问题的原因是非常有帮助的。

一般来说,日志系统中有两个主要组件:日志代理和日志后端。前者是一个程序的抽象层。它收集、转换和分发日志到日志后端。日志后端存储接收到的所有日志。与监控一样,为 Kubernetes 构建日志系统最具挑战性的部分是确定如何从容器中收集日志到集中的日志后端。通常有三种方式将日志发送到程序:

  • 将所有内容转储到stdout/stderr

  • 编写log文件

  • 将日志发送到日志代理或直接发送到日志后端;只要我们了解日志流在 Kubernetes 中的流动方式,Kubernetes 中的程序也可以以相同的方式发出日志

聚合日志的模式

对于直接向日志代理或后端记录日志的程序,它们是否在 Kubernetes 内部并不重要,因为它们在技术上并不通过 Kubernetes 输出日志。至于其他情况,我们将使用以下两种模式来集中日志。

每个节点使用一个日志代理收集日志

我们知道通过kubectl logs检索到的消息是从容器的stdout/stderr重定向的流,但显然使用kubectl logs收集日志并不是一个好主意。实际上,kubectl logs从 kubelet 获取日志,kubelet 将日志聚合到主机路径/var/log/containers/中,从容器引擎下方获取。

因此,在每个节点上设置日志代理并配置它们尾随和转发路径下的log文件,这正是我们需要的,以便汇聚运行容器的标准流,如下图所示:

在实践中,我们还会配置一个日志代理来从系统和 Kubernetes 的组件下的/var/log中尾随日志,比如在主节点和节点上的:

  • kube-proxy.log

  • kube-apiserver.log

  • kube-scheduler.log

  • kube-controller-manager.log

  • etcd.log

除了stdout/stderr之外,如果应用程序的日志以文件形式存储在容器中,并通过hostPath卷持久化,节点日志代理可以将它们传递给节点。然而,对于每个导出的log文件,我们必须在日志代理中自定义它们对应的配置,以便它们可以被正确分发。此外,我们还需要适当命名log文件,以防止任何冲突,并自行处理日志轮换,这使得它成为一种不可扩展和不可管理的日志记录机制。

运行一个旁路容器来转发日志

有时修改我们的应用程序以将日志写入标准流而不是log文件是困难的,我们也不想面对使用hostPath卷带来的麻烦。在这种情况下,我们可以运行一个旁路容器来处理一个 pod 内的日志。换句话说,每个应用程序 pod 都将有两个共享相同emptyDir卷的容器,以便旁路容器可以跟踪应用程序容器的日志并将它们发送到他们的 pod 外部,如下图所示:

虽然我们不再需要担心管理log文件,但是配置每个 pod 的日志代理并将 Kubernetes 的元数据附加到日志条目中仍然需要额外的工作。另一个选择是利用旁路容器将日志输出到标准流,而不是运行一个专用的日志代理,就像下面的 pod 一样;应用容器不断地将消息写入/var/log/myapp.log,而旁路容器则在共享卷中跟踪myapp.log

---6-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
Tue Jul 25 14:51:33 UTC 2017 INFO hello
Tue Jul 25 14:51:34 UTC 2017 INFO hello
...

摄取 Kubernetes 事件

我们在kubectl describe的输出中看到的事件消息包含有价值的信息,并补充了 kube-state-metrics 收集的指标,这使我们能够了解我们的 pod 或节点发生了什么。因此,它应该是我们日志记录基本要素的一部分,连同系统和应用程序日志。为了实现这一点,我们需要一些东西来监视 Kubernetes API 服务器,并将事件聚合到日志输出中。而 eventer 正是我们需要的事件处理程序。

Eventer 是 Heapster 的一部分,目前支持 Elasticsearch、InfluxDB、Riemann 和 Google Cloud Logging 作为其输出。Eventer 也可以直接输出到stdout,以防我们使用的日志系统不受支持。

部署 eventer 类似于部署 Heapster,除了容器启动命令,因为它们打包在同一个镜像中。每种 sink 类型的标志和选项可以在这里找到:(github.com/kubernetes/heapster/blob/master/docs/sink-configuration.md)。

我们为本章提供的示例模板还包括 eventer,并且它已配置为与 Elasticsearch 一起工作。我们将在下一节中进行描述。

使用 Fluentd 和 Elasticsearch 进行日志记录

到目前为止,我们已经讨论了我们在现实世界中可能遇到的日志记录的各种条件,现在是时候动手制作一个日志系统,应用我们所学到的知识了。

日志系统和监控系统的架构在某些方面基本相同--收集器、存储和用户界面。我们将要设置的相应组件是 Fluentd/eventer、Elasticsearch 和 Kibana。此部分的模板可以在6-3_efk下找到,并且它们将部署到前一部分的命名空间monitoring中。

Elasticsearch 是一个强大的文本搜索和分析引擎,这使它成为持久化、处理和分析我们集群中运行的所有日志的理想选择。本章的 Elasticsearch 模板使用了一个非常简单的设置来演示这个概念。如果您想要为生产使用部署 Elasticsearch 集群,建议使用 StatefulSet 控制器,并根据我们在第四章中讨论的适当配置来调整 Elasticsearch。让我们使用以下模板部署 Elasticsearch (github.com/DevOps-with-Kubernetes/examples/tree/master/chapter6/6-3_efk/):

$ kubectl apply -f elasticsearch/es-config.yml
$ kubectl apply -f elasticsearch/es-logging.yml

如果从es-logging-svc:9200收到响应,则 Elasticsearch 已准备就绪。

下一步是设置节点日志代理。由于我们会在每个节点上运行它,因此我们肯定希望它在节点资源使用方面尽可能轻量化,因此选择了 Fluentd(www.fluentd.org)。Fluentd 具有较低的内存占用,这使其成为我们需求的一个有竞争力的日志代理。此外,由于容器化环境中的日志记录要求非常专注,因此有一个类似的项目,Fluent Bit(fluentbit.io),旨在通过修剪不会用于其目标场景的功能来最小化资源使用。在我们的示例中,我们将使用 Fluentd 镜像用于 Kubernetes(github.com/fluent/fluentd-kubernetes-daemonset)来执行我们之前提到的第一个日志模式。

该图像已配置为转发容器日志到/var/log/containers下,以及某些系统组件的日志到/var/log下。如果需要,我们绝对可以进一步定制其日志配置。这里提供了两个模板:fluentd-sa.yml是 Fluentd DaemonSet 的 RBAC 配置,fluentd-ds.yml是:

$ kubectl apply -f fluentd/fluentd-sa.yml
$ kubectl apply -f fluentd/fluentd-ds.yml  

另一个必不可少的日志记录组件是 eventer。这里我们为不同条件准备了两个模板。如果您使用的是已部署 Heapster 的托管 Kubernetes 服务,则在这种情况下使用独立 eventer 的模板eventer-only.yml。否则,考虑在同一个 pod 中运行 Heapster 和 eventer 的模板:

$ kubectl apply -f heapster-eventer/heapster-eventer.yml
or
$ kubectl apply -f heapster-eventer/eventer-only.yml

要查看发送到 Elasticsearch 的日志,我们可以调用 Elasticsearch 的搜索 API,但有一个更好的选择,即 Kibana,这是一个允许我们与 Elasticsearch 交互的 Web 界面。Kibana 的模板是elasticsearch/kibana-logging.yml,位于github.com/DevOps-with-Kubernetes/examples/tree/master/chapter6/6-3_efk/下。

$ kubectl apply -f elasticsearch/kibana-logging.yml  

在我们的示例中,Kibana 正在监听端口5601。在将服务暴露到集群外并使用任何浏览器连接后,您可以开始从 Kubernetes 搜索日志。由 eventer 发送的日志的索引名称是heapster-*,而由 Fluentd 转发的日志的索引名称是logstash-*。以下截图显示了 Elasticsearch 中日志条目的外观。

该条目来自我们之前的示例myapp,我们可以发现该条目已经在 Kubernetes 上附带了方便的元数据标记。

从日志中提取指标

我们在 Kubernetes 上构建的围绕我们应用程序的监控和日志系统如下图所示:

监控部分和日志部分看起来像是两条独立的轨道,但日志的价值远不止一堆短文本。它们是结构化数据,并像往常一样带有时间戳;因此,将日志转换为时间序列数据的想法是有前途的。然而,尽管 Prometheus 非常擅长处理时间序列数据,但它无法在没有任何转换的情况下摄取文本。

来自 HTTPD 的访问日志条目如下:

10.1.8.10 - - [07/Jul/2017:16:47:12 0000] "GET /ping HTTP/1.1" 200 68

它包括请求的 IP 地址、时间、方法、处理程序等。如果我们根据它们的含义划分日志段,计数部分就可以被视为一个指标样本,如下所示:"10.1.8.10": 1, "GET": 1, "/ping": 1, "200": 1

诸如 mtail(github.com/google/mtail)和 Grok Exporter(github.com/fstab/grok_exporter)之类的工具会计算日志条目并将这些数字组织成指标,以便我们可以在 Prometheus 中进一步处理它们。

摘要

在本章的开始,我们描述了如何通过内置函数(如kubectl)快速获取运行容器的状态。然后,我们扩展了对监控的概念和原则的讨论,包括为什么需要进行监控,要监控什么以及如何进行监控。随后,我们以 Prometheus 为核心构建了一个监控系统,并设置了导出器来收集来自 Kubernetes 的指标。还介绍了 Prometheus 的基础知识,以便我们可以利用指标更好地了解我们的集群以及其中运行的应用程序。在日志部分,我们提到了日志记录的常见模式以及在 Kubernetes 中如何处理它们,并部署了一个 EFK 堆栈来汇聚日志。本章中构建的系统有助于我们服务的可靠性。接下来,我们将继续在 Kubernetes 中建立一个持续交付产品的流水线。

第七章:持续交付

到目前为止,我们讨论的主题使我们能够在 Kubernetes 中运行我们的服务。通过监控系统,我们对我们的服务更有信心。我们接下来想要实现的下一件事是如何在 Kubernetes 中持续交付我们的最新功能和改进我们的服务,并且我们将在本章的以下主题中学习它:

  • 更新 Kubernetes 资源

  • 建立交付流水线

  • 改进部署过程的技术

更新资源

持续交付的属性就像我们在第一章中描述的那样,是一组操作,包括持续集成CI)和随后的部署任务。CI 流程包括版本控制系统、构建和不同级别的自动化测试等元素。实现 CI 功能的工具通常位于应用程序层,可以独立于基础架构,但是在实现部署时,由于部署任务与我们的应用程序运行的平台紧密相关,理解和处理基础架构是不可避免的。在软件运行在物理或虚拟机上的环境中,我们会利用配置管理工具、编排器和脚本来部署我们的软件。然而,如果我们在像 Heroku 这样的应用平台上运行我们的服务,甚至是在无服务器模式下,设计部署流水线将是完全不同的故事。总之,部署任务的目标是确保我们的软件在正确的位置正常工作。在 Kubernetes 中,这涉及如何正确更新资源,特别是 Pod。

触发更新

在第三章中,开始使用 Kubernetes,我们已经讨论了部署中 Pod 的滚动更新机制。让我们回顾一下在更新过程触发后会发生什么:

  1. 部署根据更新后的清单创建一个新的ReplicaSet,其中包含0个 Pod。

  2. 新的ReplicaSet逐渐扩展,同时先前的ReplicaSet不断缩小。

  3. 所有旧的 Pod 被替换后,该过程结束。

Kubernetes 会自动完成这样的机制,使我们免于监督更新过程。要触发它,我们只需要通知 Kubernetes 更新 Deployment 的 pod 规范,也就是修改 Kubernetes 中一个资源的清单。假设我们有一个 Deployment my-app(请参阅本节示例目录下的ex-deployment.yml),我们可以使用kubectl的子命令修改清单如下:

  • kubectl patch:根据输入的 JSON 参数部分地修补对象的清单。如果我们想将my-app的镜像从alpine:3.5更新到alpine:3.6,可以这样做:
$ kubectl patch deployment my-app -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"alpine:3.6"}]}}}}'
  • kubectl set:更改对象的某些属性。这是直接更改某些属性的快捷方式,其中支持的属性之一是 Deployment 的镜像:
$ kubectl set image deployment my-app app=alpine:3.6
  • kubectl edit:打开编辑器并转储当前的清单,以便我们可以进行交互式编辑。修改后的清单在保存后立即生效。

  • kubectl replace:用另一个提交的模板文件替换一个清单。如果资源尚未创建或包含无法更改的属性,则会产生错误。例如,在我们的示例模板ex-deployment.yml中有两个资源,即 Deployment my-app及其 Service my-app-svc。让我们用一个新的规范文件替换它们:

$ kubectl replace -f ex-deployment.yml
deployment "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-appliedset-last-appliedview-last-applied来操作它。例如,我们可以查看之前提交的模板,无论ex-deployment.yml的实际内容如何。
$ kubectl apply -f ex-deployment.yml view-last-applied

保存的清单信息将与我们发送的完全相同,不同于通过kubectl get -o yaml/json检索的清单,后者包含对象的实时状态,以及规范。

尽管在本节中我们只关注操作部署,但这里的命令也适用于更新所有其他 Kubernetes 资源,如 Service、Role 等。

ConfigMap 和 secret 的更改通常需要几秒钟才能传播到 pods。

与 Kubernetes 的 API 服务器进行交互的推荐方式是使用 kubectl。如果您处于受限制的环境中,还可以使用 REST API 来操作 Kubernetes 的资源。例如,我们之前使用的 kubectl patch 命令将变为如下所示:

$ curl -X PATCH -H 'Content-Type: application/strategic-merge-patch+json' --data '{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"alpine:3.6"}]}}}}' 'https://$KUBEAPI/apis/apps/v1beta1/namespaces/default/deployments/my-app'

这里的变量 $KUBEAPI 是 API 服务器的端点。有关更多信息,请参阅 API 参考:kubernetes.io/docs/api-reference/v1.7/

管理部署

一旦触发了滚动更新过程,Kubernetes 将在幕后默默完成所有任务。让我们进行一些实际的实验。同样,即使我们使用了之前提到的命令修改了一些内容,滚动更新过程也不会被触发,除非相关的 pod 规范发生了变化。我们准备的示例是一个简单的脚本,它会响应任何请求并显示其主机名和其运行的 Alpine 版本。我们首先创建 Deployment,并在另一个终端中不断检查其响应:

$ kubectl apply -f ex-deployment.yml
deployment "my-app" created
service "my-app-svc" created
$ kubectl proxy
Starting to serve on 127.0.0.1:8001
// switch to another terminal #2
$ while :; do curl localhost:8001/api/v1/proxy/namespaces/default/services/my-app-svc:80/; sleep 1; 

done
my-app-3318684939-pwh41-v-3.5.2 is running...
my-app-3318684939-smd0t-v-3.5.2 is running...
...

现在我们将其图像更改为另一个版本,看看响应是什么:

$ kubectl set image deployment my-app app=alpine:3.6
deployment "my-app" image updated
// switch to terminal #2
my-app-99427026-7r5lr-v-3.6.2 is running...
my-app-3318684939-pwh41-v-3.5.2 is running...
...

来自版本 3.5 和 3.6 的消息在更新过程结束之前交错显示。为了立即确定来自 Kubernetes 的更新进程状态,而不是轮询服务端点,有 kubectl rollout 用于管理滚动更新过程,包括检查正在进行的更新的进度。让我们看看使用子命令 status 进行的滚动更新的操作:

$ kubectl rollout status deployment my-app
Waiting for rollout to finish: 3 of 5 updated replicas are available...
Waiting for rollout to finish: 3 of 5 updated replicas are available...
Waiting for rollout to finish: 4 of 5 updated replicas are available...
Waiting for rollout to finish: 4 of 5 updated replicas are available...
deployment "my-app" successfully rolled out

此时,终端 #2 的输出应该全部来自版本 3.6。子命令 history 允许我们审查 deployment 的先前更改:

$ kubectl rollout history deployment my-app
REVISION    CHANGE-CAUSE
1           <none>
2           <none>  

然而,CHANGE-CAUSE 字段没有显示任何有用的信息,帮助我们了解修订的详细信息。为了利用它,在导致更改的每个命令之后添加一个标志 --record,就像我们之前介绍的那样。当然,kubectl create 也支持记录标志。

让我们对部署进行一些更改,比如修改my-app的 pod 的环境变量DEMO。由于这会导致 pod 规范的更改,部署将立即开始。这种行为允许我们触发更新而无需构建新的镜像。为了简单起见,我们使用patch来修改变量:

$ kubectl patch deployment my-app -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"DEMO","value":"1"}]}]}}}}' --record
deployment "my-app" patched
$ kubectl rollout history deployment my-app
deployments "my-app"
REVISION    CHANGE-CAUSE
1           <none>
2           <none>
3           kubectl patch deployment my-app --
patch={"spec":{"template":{"spec":{"containers":
[{"name":"app","env":[{"name":"DEMO","value":"1"}]}]}}}} --record=true  

REVISION 3CHANGE-CAUSE清楚地记录了提交的命令。尽管如此,只有命令会被记录下来,这意味着任何通过edit/apply/replace进行的修改都不会被明确标记。如果我们想获取以前版本的清单,只要我们的更改是通过apply进行的,我们就可以检索保存的配置。

出于各种原因,有时我们希望回滚我们的应用,即使部署在一定程度上是成功的。可以通过子命令undo来实现:

$ kubectl rollout undo deployment my-app
deployment "my-app" rolled back

整个过程基本上与更新是相同的,即应用先前的清单,然后执行滚动更新。此外,我们可以利用标志--to-revision=<REVISION#>回滚到特定版本,但只有保留的修订版本才能回滚。Kubernetes 根据部署对象中的revisionHistoryLimit参数确定要保留多少修订版本。

更新的进度由kubectl rollout pausekubectl rollout resume控制。正如它们的名称所示,它们应该成对使用。部署的暂停不仅意味着停止正在进行的部署,还意味着冻结任何滚动更新,即使规范被修改,除非它被恢复。

更新 DaemonSet 和 StatefulSet

Kubernetes 支持各种方式来编排不同类型的工作负载的 pod。除了部署外,还有DaemonSetStatefulSet用于长时间运行的非批处理工作负载。由于它们生成的 pod 比部署有更多的约束,我们应该了解处理它们的更新时的注意事项

DaemonSet

DaemonSet是一个专为系统守护程序设计的控制器,正如其名称所示。因此,DaemonSet在每个节点上启动和维护一个 Pod,也就是说,DaemonSet的总 Pod 数量符合集群中节点的数量。由于这种限制,更新DaemonSet不像更新 Deployment 那样直接。例如,Deployment 有一个maxSurge参数(.spec.strategy.rollingUpdate.maxSurge),用于控制更新期间可以创建多少超出所需数量的冗余 Pod。但是我们不能对DaemonSet的 Pod 采用相同的策略,因为DaemonSet通常占用主机的资源,如端口。如果在一个节点上同时有两个或更多的系统 Pod,可能会导致错误。因此,更新的形式是在主机上终止旧的 Pod 后创建一个新的 Pod。

Kubernetes 为DaemonSet实现了两种更新策略,即OnDeleterollingUpdate。一个示例演示了如何编写DaemonSet的模板,位于7-1_updates/ex-daemonset.yml。更新策略设置在路径.spec.updateStrategy.type处,默认情况下在 Kubernetes 1.7 中为OnDelete,在 Kubernetes 1.8 中变为rollingUpdate

  • OnDelete:只有在手动删除 Pod 后才会更新。

  • rollingUpdate:它实际上的工作方式类似于OnDelete,但是 Kubernetes 会自动执行 Pod 的删除。有一个可选参数.spec.updateStrategy.rollingUpdate.maxUnavailable,类似于 Deployment 中的参数。其默认值为1,这意味着 Kubernetes 会逐个节点替换一个 Pod。

滚动更新过程的触发与 Deployment 的相同。此外,我们还可以利用kubectl rollout来管理DaemonSet的滚动更新。但是不支持pauseresume

DaemonSet的滚动更新仅适用于 Kubernetes 1.6 及以上版本。

StatefulSet

StatefulSetDaemonSet的更新方式基本相同——它们在更新期间不会创建冗余的 Pod,它们的更新策略也表现出类似的行为。在7-1_updates/ex-statefulset.yml中还有一个模板文件供练习。更新策略的选项设置在路径.spec.updateStrategy.type处:

  • OnDelete:只有在手动删除 Pod 后才会更新。

  • rollingUpdate:像每次滚动更新一样,Kubernetes 以受控的方式删除和创建 Pod。但是 Kubernetes 知道在StatefulSet中顺序很重要,所以它会按照相反的顺序替换 Pod。假设我们在StatefulSet中有三个 Pod,它们分别是my-ss-0my-ss-1my-ss-2。然后更新顺序从my-ss-2开始到my-ss-0。删除过程不遵守 Pod 管理策略,也就是说,即使我们将 Pod 管理策略设置为Parallel,更新仍然会逐个执行。

类型rollingUpdate的唯一参数是分区(.spec.updateStrategy.rollingUpdate.partition)。如果指定了分区,任何序数小于分区号的 Pod 将保持其当前版本,不会被更新。例如,在具有 3 个 Pod 的StatefulSet中将其设置为 1,只有 pod-1 和 pod-2 会在发布后进行更新。该参数允许我们在一定程度上控制进度,特别适用于等待数据同步、使用金丝雀进行测试发布,或者我们只是想分阶段进行更新。

Pod 管理策略和滚动更新是 Kubernetes 1.7 及更高版本中实现的两个功能。

构建交付流水线

为容器化应用程序实施持续交付流水线非常简单。让我们回顾一下到目前为止我们对 Docker 和 Kubernetes 的学习,并将它们组织成 CD 流水线。假设我们已经完成了我们的代码、Dockerfile 和相应的 Kubernetes 模板。要将它们部署到我们的集群,我们需要经历以下步骤:

  1. docker build:生成一个可执行的不可变构件。

  2. docker run:验证构建是否通过了一些简单的测试。

  3. docker tag:如果构建成功,为其打上有意义的版本标签。

  4. docker push:将构建移动到构件存储库以进行分发。

  5. kubectl apply:将构建部署到所需的环境中。

  6. kubectl rollout status:跟踪部署任务的进展。

这就是一个简单但可行的交付流水线。

选择工具

为了使流水线持续交付构建,我们至少需要三种工具,即版本控制系统、构建服务器和用于存储容器构件的存储库。在本节中,我们将基于前几章介绍的 SaaS 工具设置一个参考 CD 流水线。它们是GitHub (github.com)、Travis CI (travis-ci.org)和Docker Hub (hub.docker.com),它们都对开源项目免费。我们在这里使用的每个工具都有许多替代方案,比如 GitLab 用于 VCS,或者托管 Jenkins 用于 CI。以下图表是基于前面三个服务的 CD 流程:

工作流程始于将代码提交到 GitHub 上的存储库,提交将调用 Travis CI 上的构建作业。我们的 Docker 镜像是在这个阶段构建的。同时,我们经常在 CI 服务器上运行不同级别的测试,以确保构建的质量稳固。此外,由于使用 Docker Compose 或 Kubernetes 运行应用程序堆栈比以往任何时候都更容易,我们能够在构建作业中运行涉及许多组件的测试。随后,经过验证的镜像被打上标识并推送到公共 Docker Registry 服务 Docker Hub。

我们的流水线中没有专门用于部署任务的块。相反,我们依赖 Travis CI 来部署我们的构建。事实上,部署任务仅仅是在镜像推送后,在某些构建上应用 Kubernetes 模板。最后,在 Kubernetes 的滚动更新过程结束后,交付就完成了。

解释的步骤

我们的示例my-app是一个不断回显OK的 Web 服务,代码以及部署文件都提交在我们在 GitHub 上的存储库中:(github.com/DevOps-with-Kubernetes/my-app)。

在配置 Travis CI 上的构建之前,让我们首先在 Docker Hub 上创建一个镜像存储库以备后用。登录 Docker Hub 后,点击右上角的 Create Repository,然后按照屏幕上的步骤创建一个。用于推送和拉取的my-app镜像注册表位于devopswithkubernetes/my-app (hub.docker.com/r/devopswithkubernetes/my-app/)。

将 Travis CI 与 GitHub 存储库连接起来非常简单,我们只需要授权 Travis CI 访问我们的 GitHub 存储库,并在个人资料页面启用 Travis CI 构建存储库即可(travis-ci.org/profile)。

Travis CI 中作业的定义是在同一存储库下放置的.travis.yml文件中配置的。它是一个 YAML 格式的模板,由一系列告诉 Travis CI 在构建期间应该做什么的 shell 脚本块组成。我们的.travis.yml文件块的解释如下:(github.com/DevOps-with-Kubernetes/my-app/blob/master/.travis.yml)

env

这个部分定义了在整个构建过程中可见的环境变量:

DOCKER_REPO=devopswithkubernetes/my-app     BUILD_IMAGE_PATH=${DOCKER_REPO}:b${TRAVIS_BUILD_NUMBER}
RELEASE_IMAGE_PATH=${DOCKER_REPO}:${TRAVIS_TAG}
RELEASE_TARGET_NAMESPACE=default  

在这里,我们设置了一些可能会更改的变量,比如命名空间和构建图像的 Docker 注册表路径。此外,还有关于构建的元数据从 Travis CI 以环境变量的形式传递,这些都在这里记录着:docs.travis-ci.com/user/environment-variables/#Default-Environment- Variables。例如,TRAVIS_BUILD_NUMBER代表当前构建的编号,我们将其用作标识符来区分不同构建中的图像。

另一个环境变量的来源是在 Travis CI 上手动配置的。因为在那里配置的变量会被公开隐藏,所以我们在那里存储了一些敏感数据,比如 Docker Hub 和 Kubernetes 的凭据:

每个 CI 工具都有自己处理密钥的最佳实践。例如,一些 CI 工具也允许我们在 CI 服务器中保存变量,但它们仍然会在构建日志中打印出来,所以在这种情况下我们不太可能在 CI 服务器中保存密钥。

脚本

这个部分是我们运行构建和测试的地方:

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,所以构建只需要一行脚本。我们的测试也很简单——使用构建的图像启动一个容器,并对其进行一些请求以确定其正确性和完整性。当然,在这个阶段我们可以做任何事情,比如添加单元测试、进行多阶段构建,或者运行自动化集成测试来改进最终的构件。

成功后

只有前一个阶段没有任何错误结束时,才会执行这个块。一旦到了这里,我们就可以发布我们的图像了:

docker login -u ${CI_ENV_REGISTRY_USER} -p "${CI_ENV_REGISTRY_PASS}"
docker tag my-app ${BUILD_IMAGE_PATH}
docker push ${BUILD_IMAGE_PATH}
if [[ ${TRAVIS_TAG} =~ ^rel.*$ ]]; then
 docker tag my-app ${RELEASE_IMAGE_PATH}
 docker push ${RELEASE_IMAGE_PATH}
fi

我们的镜像标签在 Travis CI 上简单地使用构建编号,但使用提交的哈希或版本号来标记镜像也很常见。然而,强烈不建议使用默认标签latest,因为这可能导致版本混淆,比如运行两个不同的镜像,但它们有相同的名称。最后的条件块是在特定分支标签上发布镜像,实际上并不需要,因为我们只是想保持在一个单独的轨道上构建和发布。在推送镜像之前,请记得对 Docker Hub 进行身份验证。

Kubernetes 决定是否应该拉取镜像的imagePullPolicykubernetes.io/docs/concepts/containers/images/#updating-images

因为我们将项目部署到实际机器上只在发布时,构建可能会在那一刻停止并返回。让我们看看这个构建的日志:travis-ci.org/DevOps-with-Kubernetes/my-app/builds/268053332。日志保留了 Travis CI 执行的脚本和脚本每一行的输出:

正如我们所看到的,我们的构建是成功的,所以镜像随后在这里发布:

hub.docker.com/r/devopswithkubernetes/my-app/tags/

构建引用标签b1,我们现在可以在 CI 服务器外运行它:

$ docker run --name test -dp 5000:5000 devopswithkubernetes/my-app:b1
72f0ef501dc4c86786a81363e278973295a1f67555eeba102a8d25e488831813
$ curl localhost:5000
OK

部署

尽管我们可以实现端到端的完全自动化流水线,但由于业务原因,我们经常会遇到需要暂停部署构建的情况。因此,我们告诉 Travis CI 只有在发布新版本时才运行部署脚本。

在 Travis CI 中从我们的 Kubernetes 集群中操作资源,我们需要授予 Travis CI 足够的权限。我们的示例使用了一个名为cd-agent的服务账户,在 RBAC 模式下代表我们创建和更新部署。后面的章节将对 RBAC 进行更多描述。创建账户和权限的模板在这里:github.com/DevOps-with-Kubernetes/examples/tree/master/chapter7/7-2_service-account-for-ci-tool。该账户是在cd命名空间下创建的,并被授权在各个命名空间中创建和修改大多数类型的资源。

在这里,我们使用一个能够读取和修改跨命名空间的大多数资源,包括整个集群的密钥的服务账户。由于安全问题,始终鼓励限制服务账户对实际使用的资源的权限,否则可能存在潜在的漏洞。

因为 Travis CI 位于我们的集群之外,我们必须从 Kubernetes 导出凭据,以便我们可以配置我们的 CI 任务来使用它们。在这里,我们提供了一个简单的脚本来帮助导出这些凭据。脚本位于:github.com/DevOps-with-Kubernetes/examples/blob/master/chapter7/get-sa-token.sh

$ ./get-sa-token.sh --namespace cd --account cd-agent
API endpoint:
https://35.184.53.170
ca.crt and sa.token exported
$ cat ca.crt | base64
LS0tLS1C...
$ cat sa.token
eyJhbGci...

导出的 API 端点、ca.crtsa.token 的对应变量分别是 CI_ENV_K8S_MASTERCI_ENV_K8S_CACI_ENV_K8S_SA_TOKEN。客户端证书(ca.crt)被编码为 base64 以实现可移植性,并且将在我们的部署脚本中解码。

部署脚本(github.com/DevOps-with-Kubernetes/my-app/blob/master/deployment/deploy.sh)首先下载 kubectl,并根据环境变量配置 kubectl。之后,当前构建的镜像路径被填入部署模板中,并且模板被应用。最后,在部署完成后,我们的部署就完成了。

让我们看看整个流程是如何运作的。

一旦我们在 GitHub 上发布一个版本:

github.com/DevOps-with-Kubernetes/my-app/releases/tag/rel.0.3

Travis CI 在那之后开始构建我们的任务:

一段时间后,构建的镜像被推送到 Docker Hub 上:

在这一点上,Travis CI 应该开始运行部署任务,让我们查看构建日志以了解我们部署的状态:

travis-ci.org/DevOps-with-Kubernetes/my-app/builds/268107714

正如我们所看到的,我们的应用已经成功部署,应该开始用 OK 欢迎每个人:

$ kubectl get deployment
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
my-app    3         3         3            3           30s
$ kubectl proxy &
$ curl localhost:8001/api/v1/namespaces/default/services/my-app-svc:80/proxy/
OK

我们在本节中构建和演示的流水线是在 Kubernetes 中持续交付代码的经典流程。然而,由于工作风格和文化因团队而异,为您的团队设计一个量身定制的持续交付流水线将带来效率提升的回报。

深入了解 pod

尽管在 pod 的生命周期中,出生和死亡仅仅是一瞬间,但它们是服务最脆弱的时刻。在现实世界中,常见的情况,如将请求路由到未准备就绪的盒子,或者残酷地切断所有正在进行的连接到终止的机器,都是我们要避免的。因此,即使 Kubernetes 为我们处理了大部分事情,我们也应该知道如何正确配置它,以便在部署时更加自信。

启动一个 pod

默认情况下,Kubernetes 在 pod 启动后立即将其状态转换为 Running。如果 pod 在服务后面,端点控制器会立即向 Kubernetes 注册一个端点。稍后,kube-proxy 观察端点的变化,并相应地向 iptables 添加规则。外部世界的请求现在会发送到 pod。Kubernetes 使得 pod 的注册速度非常快,因此有可能在应用程序准备就绪之前就已经发送请求到 pod,尤其是在处理庞大软件时。另一方面,如果 pod 在运行时失败,我们应该有一种自动的方式立即将其移除。

Deployment 和其他控制器的minReadySeconds字段不会推迟 pod 的就绪状态。相反,它会延迟 pod 的可用性,在部署过程中具有意义:只有当所有 pod 都可用时,部署才算成功。

活跃性和就绪性探针

探针是对容器健康状况的指示器。它通过定期对容器执行诊断操作来判断健康状况,通过 kubelet 进行:

  • 活跃性探针:指示容器是否存活。如果容器在此探针上失败,kubelet 会将其杀死,并根据 pod 的restartPolicy可能重新启动它。

  • 就绪性探针:指示容器是否准备好接收流量。如果服务后面的 pod 尚未准备就绪,其端点将在 pod 准备就绪之前不会被创建。

retartPolicy指示 Kubernetes 在失败或终止时如何处理 pod。它有三种模式:AlwaysOnFailureNever。默认设置为Always

可以配置三种类型的操作处理程序来针对容器执行:

  • exec:在容器内执行定义的命令。如果退出代码为0,则被视为成功。

  • tcpSocket:通过 TCP 测试给定端口,如果端口打开则成功。

  • httpGet:对目标容器的 IP 地址执行HTTP GET。要发送的请求中的标头是可定制的。如果状态码满足:400 > CODE >= 200,则此检查被视为健康。

此外,有五个参数来定义探针的行为:

  • initialDelaySeconds:第一次探测之前 kubelet 应等待多长时间。

  • successThreshold:当连续多次探测成功通过此阈值时,容器被视为健康。

  • failureThreshold:与前面相同,但定义了负面。

  • timeoutSeconds:单个探测操作的时间限制。

  • periodSeconds:探测操作之间的间隔。

以下代码片段演示了就绪探针的用法,完整模板在这里:github.com/DevOps-with-Kubernetes/examples/blob/master/chapter7/7-3_on_pods/probe.yml

...
 containers:
 - name: main
 image: devopswithkubernetes/my-app:b5
 readinessProbe:
 httpGet:
 path: /
 port: 5000
 periodSeconds: 5
 initialDelaySeconds: 10
 successThreshold: 2
 failureThreshold: 3 
 timeoutSeconds: 1
 command:
...

探针的行为如下图所示:

上方时间线是 pod 的真实就绪情况,下方的另一条线是 Kubernetes 视图中的就绪情况。第一次探测在 pod 创建后 10 秒执行,经过 2 次探测成功后,pod 被视为就绪。几秒钟后,由于未知原因,pod 停止服务,并在接下来的三次失败后变得不可用。尝试部署上述示例并观察其输出:

...
Pod is created at 1505315576
starting server at 1505315583.436334
1505315586.443435 - GET / HTTP/1.1
1505315591.443195 - GET / HTTP/1.1
1505315595.869020 - GET /from-tester
1505315596.443414 - GET / HTTP/1.1
1505315599.871162 - GET /from-tester
stopping server at 1505315599.964793
1505315601 readiness test fail#1
1505315606 readiness test fail#2
1505315611 readiness test fail#3
...

在我们的示例文件中,还有另一个名为tester的 pod,它不断地向我们的服务发出请求,而我们服务中的日志条目/from-tester是由该测试人员引起的。从测试人员的活动日志中,我们可以观察到从tester发出的流量在我们的服务变得不可用后停止了:

$ kubectl logs tester
1505315577 - nc: timed out
1505315583 - nc: timed out
1505315589 - nc: timed out
1505315595 - OK
1505315599 - OK
1505315603 - HTTP/1.1 500
1505315607 - HTTP/1.1 500
1505315612 - nc: timed out
1505315617 - nc: timed out
1505315623 - nc: timed out
...

由于我们没有在服务中配置活动探针,除非我们手动杀死它,否则不健康的容器不会重新启动。因此,通常情况下,我们会同时使用这两种探针,以使治疗过程自动化。

初始化容器

尽管initialDelaySeconds允许我们在接收流量之前阻塞 Pod 一段时间,但仍然有限。想象一下,如果我们的应用程序正在提供一个从其他地方获取的文件,那么就绪时间可能会根据文件大小而有很大的不同。因此,在这里初始化容器非常有用。

初始化容器是一个或多个在应用容器之前启动并按顺序完成的容器。如果任何容器失败,它将受到 Pod 的restartPolicy的影响,并重新开始,直到所有容器以代码0退出。

定义初始化容器类似于常规容器:

...
spec:
 containers:
 - name: my-app
 image: <my-app>
 initContainers:
 - name: init-my-app
 image: <init-my-app>
...

它们只在以下方面有所不同:

  • 初始化容器没有就绪探针,因为它们会运行到完成

  • 初始化容器中定义的端口不会被 Pod 前面的服务捕获

  • 资源的请求/限制是通过max(sum(regular containers), max(init containers))计算的,这意味着如果一个初始化容器设置了比其他初始化容器以及所有常规容器的资源限制之和更高的资源限制,Kubernetes 会根据初始化容器的资源限制来调度 Pod

初始化容器的用处不仅仅是阻塞应用容器。例如,我们可以利用它们通过在初始化容器和应用容器之间共享emptyDir卷来配置一个镜像,而不是构建另一个仅在基础镜像上运行awk/sed的镜像,挂载并在初始化容器中使用秘密而不是在应用容器中使用。

终止一个 Pod

关闭事件的顺序类似于启动 Pod 时的事件。在接收到删除调用后,Kubernetes 向要删除的 Pod 发送SIGTERM,Pod 的状态变为 Terminating。与此同时,如果 Pod 支持服务,Kubernetes 会删除该 Pod 的端点以停止进一步的请求。偶尔会有一些 Pod 根本不会退出。这可能是因为 Pod 不遵守SIGTERM,或者仅仅是因为它们的任务尚未完成。在这种情况下,Kubernetes 会在终止期间之后强制发送SIGKILL来强制杀死这些 Pod。终止期限的长度在 Pod 规范的.spec.terminationGracePeriodSeconds下设置。尽管 Kubernetes 已经有机制来回收这些 Pod,我们仍然应该确保我们的 Pod 能够正确关闭。

此外,就像启动一个 pod 一样,这里我们还需要注意一个可能影响我们服务的情况,即在 pod 中为请求提供服务的进程在相应的 iptables 规则完全删除之前关闭。

处理 SIGTERM

优雅终止不是一个新的想法,在编程中是一个常见的做法,特别是对于业务关键任务而言尤为重要。

实现主要包括三个步骤:

  1. 添加一个处理程序来捕获终止信号。

  2. 在处理程序中执行所有必需的操作,比如返回资源、释放分布式锁或关闭连接。

  3. 程序关闭。我们之前的示例演示了这个想法:在graceful_exit_handler处理程序中关闭SIGTERM上的控制器线程。代码可以在这里找到(github.com/DevOps-with-Kubernetes/my-app/blob/master/app.py)。

事实上,导致优雅退出失败的常见陷阱并不在程序方面:

SIGTERM 不会转发到容器进程

在第二章 使用容器进行 DevOps中,我们已经学习到在编写 Dockerfile 时调用我们的程序有两种形式,即 shell 形式和 exec 形式,而在 Linux 容器上运行 shell 形式命令的默认 shell 是/bin/sh。让我们看看以下示例(github.com/DevOps-with-Kubernetes/examples/tree/master/chapter7/7-3_on_pods/graceful_docker):

--- Dockerfile.shell-sh ---
FROM python:3-alpine
EXPOSE 5000
ADD app.py .
CMD python -u app.py

我们知道发送到容器的信号将被容器内的PID 1进程捕获,所以让我们构建并运行它。

$ docker run -d --rm --name my-app my-app:shell-sh
8962005f3722131f820e750e72d0eb5caf08222bfbdc5d25b6f587de0f6f5f3f 
$ docker logs my-app
starting server at 1503839211.025133
$ docker kill --signal TERM my-app
my-app
$ docker ps --filter name=my-app --format '{{.Names}}'
my-app

我们的容器还在那里。让我们看看容器内发生了什么:

$ docker exec my-app ps
PID   USER     TIME    COMMAND
1     root      0:00  /bin/sh -c python -u app.py
5     root      0:00  python -u app.py
6     root      0:00  ps  

PID 1进程本身就是 shell,并且显然不会将我们的信号转发给子进程。在这个例子中,我们使用 Alpine 作为基础镜像,它使用ash作为默认 shell。如果我们用/bin/sh执行任何命令,实际上是链接到ash的。同样,Debian 家族的默认 shell 是dash,它也不会转发信号。仍然有一个转发信号的 shell,比如bash。为了利用bash,我们可以安装额外的 shell,或者将基础镜像切换到使用bash的发行版。但这两种方法都相当繁琐。

此外,仍然有解决信号问题的选项,而不使用bash。其中一个是以 shell 形式在exec中运行我们的程序:

CMD exec python -u app.py

我们的进程将替换 shell 进程,从而成为PID 1进程。另一个选择,也是推荐的选择,是以 EXEC 形式编写Dockerfile

CMD [ "python", "-u", "app.py" ] 

让我们再试一次以 EXEC 形式的示例:

---Dockerfile.exec-sh---
FROM python:3-alpine
EXPOSE 5000
ADD app.py .
CMD [ "python", "-u", "app.py" ]
---
$ docker run -d --rm --name my-app my-app:exec-sh
5114cabae9fcec530a2f68703d5bc910d988cb28acfede2689ae5eebdfd46441
$ docker exec my-app ps
PID   USER     TIME   COMMAND
1     root       0:00  python -u app.py
5     root       0:00  ps
$ docker kill --signal TERM my-app && docker logs -f my-app
my-app
starting server at 1503842040.339449
stopping server at 1503842134.455339 

EXEC 形式运行得很好。正如我们所看到的,容器中的进程是我们预期的,我们的处理程序现在正确地接收到SIGTERM

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

因此,我们的 nginx 关闭问题可以通过PreStop钩子轻松解决:

...
 containers:
 - name: main
 image: nginx
 lifecycle:
 preStop:
 exec:
 command: [ "nginx", "-s", "quit" ]
... 

此外,钩子的一个重要属性是它们可以以某种方式影响 pod 的状态:除非其PostStart钩子成功退出,否则 pod 不会运行;在删除时,pod 立即设置为终止,但除非PreStop钩子成功退出,否则不会发送SIGTERM。因此,对于我们之前提到的情况,容器在删除之前退出,我们可以通过PreStop钩子来解决。以下图示了如何使用钩子来消除不需要的间隙:

实现只是添加一个休眠几秒钟的钩子:

...
 containers:
 - name: main
 image: my-app
 lifecycle:
 preStop:
 exec:
 command: [ "/bin/sh", "-c", "sleep 5" ]
...

放置 pod

大多数情况下,我们并不真的关心我们的 pod 运行在哪个节点上,因为调度 pod 是 Kubernetes 的一个基本特性。然而,当调度 pod 时,Kubernetes 并不知道节点的地理位置、可用区域或机器类型等因素。此外,有时我们希望在一个隔离的实例组中部署运行测试构建的 pod。因此,为了完成调度,Kubernetes 提供了不同级别的亲和性,允许我们积极地将 pod 分配给特定的节点。

pod 的节点选择器是手动放置 pod 的最简单方式。它类似于服务的 pod 选择器。pod 只会放置在具有匹配标签的节点上。该字段设置在.spec.nodeSelector中。例如,以下 pod spec的片段将 pod 调度到具有标签purpose=sandbox,disk=ssd的节点上。

...
 spec:
 containers:
 - name: main
 image: my-app
 nodeSelector:
 purpose: sandbox
 disk: ssd
...

检查节点上的标签与我们在 Kubernetes 中检查其他资源的方式相同:

$ kubectl describe node gke-my-cluster-ins-49e8f52a-lz4l
Name:       gke-my-cluster-ins-49e8f52a-lz4l
Role:
Labels:   beta.kubernetes.io/arch=amd64
 beta.kubernetes.io/fluentd-ds-ready=true
 beta.kubernetes.io/instance-type=g1-small
 beta.kubernetes.io/os=linux
 cloud.google.com/gke-nodepool=ins
 failure-domain.beta.kubernetes.io/region=us-  
          central1
 failure-domain.beta.kubernetes.io/zone=us-
          central1-b
 kubernetes.io/hostname=gke-my-cluster-ins- 
          49e8f52a-lz4l
... 

正如我们所看到的,我们的节点上已经有了标签。这些标签是默认设置的,默认标签如下:

  • 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

如果我们想要标记一个节点以使我们的示例 pod 被调度,我们可以更新节点的清单,或者使用快捷命令kubectl label

$ kubectl label node gke-my-cluster-ins-49e8f52a-lz4l \
 purpose=sandbox disk=ssd
node "gke-my-cluster-ins-49e8f52a-lz4l" labeled
$ kubectl get node --selector purpose=sandbox,disk=ssd
NAME                               STATUS    AGE       VERSION
gke-my-cluster-ins-49e8f52a-lz4l   Ready     5d        v1.7.3

除了将 pod 放置到节点上,节点也可以拒绝 pod,即污点和容忍,我们将在下一章学习它。

摘要

在本章中,我们不仅讨论了构建持续交付流水线的话题,还讨论了加强每个部署任务的技术。pod 的滚动更新是一个强大的工具,可以以受控的方式进行更新。要触发滚动更新,我们需要更新 pod 的规范。虽然更新由 Kubernetes 管理,但我们仍然可以使用kubectl rollout来控制它。

随后,我们通过GitHub/DockerHub/Travis-CI创建了一个可扩展的持续交付流水线。接下来,我们将学习更多关于 pod 的生命周期,以防止任何可能的故障,包括使用就绪和存活探针来保护 pod,使用 Init 容器初始化 pod,通过以 exec 形式编写Dockerfile来正确处理SIGTERM,利用生命周期钩子来延迟 pod 的就绪以及终止,以便在正确的时间删除 iptables 规则,并使用节点选择器将 pod 分配给特定的节点。

在下一章中,我们将学习如何在 Kubernetes 中使用逻辑边界来分割我们的集群,以更稳定和安全地共享资源。

第八章:集群管理

在之前的章节中,我们学习了 Kubernetes 中大部分基本的 DevOps 技能,从如何将应用程序容器化到通过持续部署将我们的容器化软件无缝部署到 Kubernetes。现在,是时候更深入地了解如何管理 Kubernetes 集群了。

在本章中,我们将学习:

  • 如何利用命名空间设置管理边界

  • 使用 kubeconfig 在多个集群之间切换

  • Kubernetes 身份验证

  • Kubernetes 授权

虽然 minikube 是一个相当简单的环境,但在本章中,我们将以Google 容器引擎GKE)和 AWS 中的自托管集群作为示例,而不是 minikube。有关详细设置,请参阅第九章,AWS 上的 Kubernetes,以及第十章,GCP 上的 Kubernetes

Kubernetes 命名空间

Kubernetes 具有命名空间概念,将物理集群中的资源划分为多个虚拟集群。这样,不同的组可以共享同一个物理集群并实现隔离。每个命名空间提供:

  • 一组名称范围;每个命名空间中的对象名称是唯一的

  • 确保受信任身份验证的策略

  • 设置资源配额以进行资源管理

命名空间非常适合同一公司中的不同团队或项目,因此不同的组可以拥有自己的虚拟集群,这些集群具有资源隔离但共享同一个物理集群。一个命名空间中的资源对其他命名空间是不可见的。可以为不同的命名空间设置不同的资源配额,并提供不同级别的 QoS。请注意,并非所有对象都在命名空间中,例如节点和持久卷,它们属于整个集群。

默认命名空间

默认情况下,Kubernetes 有三个命名空间:defaultkube-systemkube-publicdefault命名空间包含未指定任何命名空间创建的对象,而kube-system包含由 Kubernetes 系统创建的对象,通常由系统组件使用,例如 Kubernetes 仪表板或 Kubernetes DNS。kube-public是在 1.6 中新引入的,旨在定位每个人都可以访问的资源。它现在主要关注公共 ConfigMap,如集群信息。

创建新的命名空间

让我们看看如何创建一个命名空间。命名空间也是 Kubernetes 对象。我们可以像其他对象一样指定种类为命名空间。下面是创建一个命名空间project1的示例:

// configuration file of namespace
# cat 8-1-1_ns1.yml
apiVersion: v1
kind: Namespace
metadata:
name: project1

// create namespace for project1
# kubectl create -f 8-1-1_ns1.yml
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 

当我们通过kubectl get pods列出 pod 时,我们会在我们的集群中看不到任何内容。为什么?因为 Kubernetes 使用当前上下文来决定哪个命名空间是当前的。如果我们在上下文或kubectl命令行中不明确指定命名空间,则将使用default命名空间:

// We'll see the Pods if we explicitly specify --namespace
# kubectl get pods --namespace=project1
NAME                     READY     STATUS    RESTARTS   AGE
nginx-3599227048-gghvw   1/1       Running   0          15s
nginx-3599227048-jz3lg   1/1       Running   0          15s  

您可以使用--namespace <namespace_name>--namespace=<namespace_name>-n <namespace_name>-n=<namespace_name>来指定命令的命名空间。要列出跨命名空间的资源,请使用--all-namespaces参数。

另一种方法是将当前上下文更改为指向所需命名空间,而不是默认命名空间。

上下文

上下文是集群信息、用于身份验证的用户和命名空间的组合概念。例如,以下是我们在 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中为project1创建上下文的示例:

// create a context with my GKE cluster
# kubectl config set-context project1 --namespace=project1 --cluster=gke_devops-with-kubernetes_us-central1-b_cluster --user=gke_devops-with-kubernetes_us-central1-b_cluster
Context "project1" created.  

切换当前上下文

然后我们可以通过use-context子命令切换上下文:

# kubectl config use-context project1
Switched to context "project1".  

上下文切换后,我们通过kubectl调用的每个命令都在project1上下文下。我们不需要明确指定命名空间来查看我们的 pod:

// list pods
# kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nginx-3599227048-gghvw   1/1       Running   0          3m
nginx-3599227048-jz3lg   1/1       Running   0          3m  

资源配额

在 Kubernetes 中,默认情况下,pod 是无限制的资源。然后运行的 pod 可能会使用集群中的所有计算或存储资源。ResourceQuota 是一个资源对象,允许我们限制命名空间可以使用的资源消耗。通过设置资源限制,我们可以减少嘈杂的邻居症状。为project1工作的团队不会耗尽物理集群中的所有资源。

然后我们可以确保其他项目中工作的团队在共享同一物理集群时的服务质量。Kubernetes 1.7 支持三种资源配额。每种类型包括不同的资源名称(kubernetes.io/docs/concepts/policy/resource-quotas)。

  • 计算资源配额(CPU,内存)

  • 存储资源配额(请求的存储、持久卷索赔)

  • 对象计数配额(pod、RCs、ConfigMaps、services、LoadBalancers)

创建的资源不会受到新创建的资源配额的影响。如果资源创建请求超过指定的 ResourceQuota,资源将无法启动。

为命名空间创建资源配额

现在,让我们学习ResourceQuota的语法。以下是一个例子:

# cat 8-1-2_resource_quota.yml
apiVersion: v1
kind: ResourceQuota
metadata:
 name: project1-resource-quota
spec:
 hard:# the limits of the sum of memory request
 requests.cpu: "1"               # the limits of the sum   
   of requested CPU
   requests.memory: 1Gi            # the limits of the sum  
   of requested memory 
   limits.cpu: "2"           # the limits of total CPU  
   limits
   limits.memory: 2Gi        # the limits of total memory 
   limit 
   requests.storage: 64Gi    # the limits of sum of 
   storage requests across PV claims
   pods: "4"                 # the limits of pod number   

模板与其他对象一样,只是这种类型变成了ResourceQuota。我们指定的配额适用于处于成功或失败状态的 pod(即非终端状态)。支持几种资源约束。在前面的例子中,我们演示了如何设置计算 ResourceQuota、存储 ResourceQuota 和对象 CountQuota。随时,我们仍然可以使用kubectl命令来检查我们设置的配额:kubectl describe resourcequota <resource_quota_name>

现在让我们通过命令kubectl edit deployment nginx修改我们现有的 nginx 部署,将副本从2更改为4并保存。现在让我们列出状态。

# kubectl describe deployment nginx
Replicas:         4 desired | 2 updated | 2 total | 2 available | 2 unavailable
Conditions:
 Type                  Status      Reason
 ----                  ------      ------
 Available             False MinimumReplicasUnavailable
 ReplicaFailure  True  FailedCreate  

它指示一些 pod 在创建时失败。如果我们检查相应的 ReplicaSet,我们可以找出原因:

# kubectl describe rs nginx-3599227048
...
Error creating: pods "nginx-3599227048-" is **forbidden**: failed quota: project1-resource-quota: must specify limits.cpu,limits.memory,requests.cpu,requests.memory  

由于我们已经在内存和 CPU 上指定了请求限制,Kubernetes 不知道新期望的三个 pod 的默认请求限制。我们可以看到原来的两个 pod 仍在运行,因为资源配额不适用于现有资源。然后我们使用kubectl edit deployment nginx来修改容器规范如下:

在这里,我们在 pod 规范中指定了 CPU 和内存的请求和限制。这表明 pod 不能超过指定的配额,否则将无法启动:

// check the deployment state
# kubectl get deployment
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx     4         3         2            3           2d  

可用的 pod 变成了四个,而不是两个,但仍然不等于我们期望的四个。出了什么问题?如果我们退一步检查我们的资源配额,我们会发现我们已经使用了所有的 pod 配额。由于部署默认使用滚动更新部署机制,它将需要大于四的 pod 数量,这正是我们之前设置的对象限制:

# kubectl describe resourcequota project1-resource-quota
Name:             project1-resource-quota
Namespace:        project1
Resource          Used  Hard
--------          ----  ----
limits.cpu        900m  4
limits.memory     900Mi 4Gi
pods              4     4
requests.cpu      300m  4
requests.memory   450Mi 16Gi
requests.storage  0     64Gi  

通过kubectl edit resourcequota project1-resource-quota命令将 pod 配额从4修改为8后,部署有足够的资源来启动 pod。一旦Used配额超过Hard配额,请求将被资源配额准入控制器拒绝,否则,资源配额使用将被更新以确保足够的资源分配。

由于资源配额不会影响已创建的资源,有时我们可能需要调整失败的资源,比如删除一个 RS 的空更改集或者扩展和缩小部署,以便让 Kubernetes 创建新的 pod 或 RS,这将吸收最新的配额限制。

请求具有默认计算资源限制的 pod

我们还可以为命名空间指定默认的资源请求和限制。如果在创建 pod 时不指定请求和限制,将使用默认设置。关键是使用LimitRange资源对象。LimitRange对象包含一组defaultRequest(请求)和default(限制)。

LimitRange 由 LimitRanger 准入控制器插件控制。如果启动自托管解决方案,请确保启用它。有关更多信息,请查看本章的准入控制器部分。

下面是一个示例,我们将cpu.request设置为250mlimits设置为500mmemory.request设置为256Milimits设置为512Mi

# cat 8-1-3_limit_range.yml
apiVersion: v1
kind: LimitRange
metadata:
 name: project1-limit-range
spec:
 limits:
 - default:
 cpu: 0.5
 memory: 512Mi
 defaultRequest:
 cpu: 0.25
 memory: 256Mi
 type: Container

// create limit range
# kubectl create -f 8-1-3_limit_range.yml
limitrange "project1-limit-range" created  

当我们在此命名空间内启动 pod 时,即使在 ResourceQuota 中设置了总限制,我们也不需要随时指定cpumemory请求和limits

CPU 的单位是核心,这是一个绝对数量。它可以是 AWS vCPU,GCP 核心或者装备了超线程处理器的机器上的超线程。内存的单位是字节。Kubernetes 使用字母或二的幂的等价物。例如,256M 可以写成 256,000,000,256 M 或 244 Mi。

此外,我们可以在 LimitRange 中为 pod 设置最小和最大的 CPU 和内存值。它与默认值的作用不同。默认值仅在 pod 规范不包含任何请求和限制时使用。最小和最大约束用于验证 pod 是否请求了太多的资源。语法是spec.limits[].minspec.limits[].max。如果请求超过了最小和最大值,服务器将抛出 forbidden 错误。

limits: 
   - max: 
      cpu: 1 
      memory: 1Gi 
     min: 
      cpu: 0.25 
      memory: 128Mi 
    type: Container 

Pod 的服务质量:Kubernetes 中的 pod 有三个 QoS 类别:Guaranteed、Burstable 和 BestEffort。它与我们上面学到的命名空间和资源管理概念密切相关。我们还在第四章中学习了 QoS,使用存储和资源。请参考第四章中的最后一节使用存储和资源进行复习。

删除一个命名空间

与其他资源一样,删除一个命名空间是kubectl delete namespace <namespace_name>。请注意,如果删除一个命名空间,与该命名空间关联的所有资源都将被清除。

Kubeconfig

Kubeconfig 是一个文件,您可以使用它来通过切换上下文来切换多个集群。我们可以使用kubectl config view来查看设置。以下是kubeconfig文件中 minikube 集群的示例。

# kubectl config view
apiVersion: v1
clusters:  
- cluster:
 certificate-authority: /Users/k8s/.minikube/ca.crt
 server: https://192.168.99.100:8443
 name: minikube
contexts:
- context:
 cluster: minikube
 user: minikube
 name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
 user:
 client-certificate: /Users/k8s/.minikube/apiserver.crt
 client-key: /Users/k8s/.minikube/apiserver.key

就像我们之前学到的一样。我们可以使用kubectl config use-context来切换要操作的集群。我们还可以使用kubectl config --kubeconfig=<config file name>来指定要使用的kubeconfig文件。只有指定的文件将被使用。我们还可以通过环境变量$KUBECONFIG指定kubeconfig文件。这样,配置文件可以被合并。例如,以下命令将合并kubeconfig-file1kubeconfig-file2

# export KUBECONFIG=$KUBECONFIG: kubeconfig-file1: kubeconfig-file2  

您可能会发现我们之前没有进行任何特定的设置。那么kubectl config view的输出来自哪里呢?默认情况下,它存在于$HOME/.kube/config下。如果没有设置前面的任何一个,将加载此文件。

服务账户

与普通用户不同,服务账户是由 pod 内的进程用来联系 Kubernetes 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
project1      default                      1         2h
...  

Kubernetes 将在每个命名空间中创建一个默认的服务账户,如果在创建 pod 时未指定服务账户,则将使用该默认服务账户。让我们看看默认服务账户在我们的project1命名空间中是如何工作的:

# kubectl describe serviceaccount/default
Name:       default
Namespace:  project1
Labels:           <none>
Annotations:      <none>
Image pull secrets:     <none>
Mountable secrets:      default-token-nsqls
Tokens:                 default-token-nsqls  

我们可以看到,服务账户基本上是使用可挂载的密钥作为令牌。让我们深入了解令牌中包含的内容:

// describe the secret, the name is default-token-nsqls here
# kubectl describe secret default-token-nsqls
Name:       default-token-nsqls
Namespace:  project1
Annotations:  kubernetes.io/service-account.name=default
              kubernetes.io/service-account.uid=5e46cc5e- 
              8b52-11e7-a832-42010af00267
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 服务器将检查证书和令牌进行认证。服务账户的概念将在接下来的部分中与我们同在。

认证和授权

从 DevOps 的角度来看,认证和授权非常重要。认证验证用户并检查用户是否真的是他们所代表的身份。另一方面,授权检查用户拥有哪些权限级别。Kubernetes 支持不同的认证和授权模块。

以下是一个示例,展示了当 Kubernetes API 服务器收到请求时如何处理访问控制。

API 服务器中的访问控制

当请求发送到 API 服务器时,首先,它通过使用 API 服务器中的证书颁发机构CA)验证客户端的证书来建立 TLS 连接。API 服务器中的 CA 通常位于/etc/kubernetes/,客户端的证书通常位于$HOME/.kube/config。握手完成后,进入身份验证阶段。在 Kubernetes 中,身份验证模块是基于链的。我们可以使用多个身份验证和授权模块。当请求到来时,Kubernetes 将依次尝试所有的身份验证器,直到成功。如果请求在所有身份验证模块上失败,将被拒绝为 HTTP 401 未经授权。否则,其中一个身份验证器验证用户的身份并对请求进行身份验证。然后 Kubernetes 授权模块将发挥作用。它将验证用户是否有权限执行他们请求的操作,通过一组策略。授权模块也是基于链的。它将不断尝试每个模块,直到成功。如果请求在所有模块上失败,将得到 HTTP 403 禁止的响应。准入控制是 API 服务器中一组可配置的插件,用于确定请求是否被允许或拒绝。在这个阶段,如果请求没有通过其中一个插件,那么请求将立即被拒绝。

身份验证

默认情况下,服务账户是基于令牌的。当您创建一个服务账户或一个带有默认服务账户的命名空间时,Kubernetes 会创建令牌并将其存储为一个由 base64 编码的秘密,并将该秘密作为卷挂载到 pod 中。然后 pod 内的进程有能力与集群通信。另一方面,用户账户代表一个普通用户,可能使用kubectl直接操作资源。

服务账户身份验证

当我们创建一个服务账户时,Kubernetes 服务账户准入控制器插件会自动创建一个签名的令牌。

在第七章,持续交付中,在我们演示了如何部署my-app的示例中,我们创建了一个名为cd的命名空间,并且我们使用了脚本get-sa-token.shgithub.com/DevOps-with-Kubernetes/examples/blob/master/chapter7/get-sa-token.sh)来为我们导出令牌。然后我们通过kubectl config set-credentials <user> --token=$TOKEN命令创建了一个名为mysa的用户:

# kubectl config set-credentials mysa --token=${CI_ENV_K8S_SA_TOKEN}  

接下来,我们将上下文设置为与用户和命名空间绑定:

# kubectl config set-context myctxt --cluster=mycluster --user=mysa  

最后,我们将把我们的上下文myctxt设置为默认上下文:

# kubectl config use-context myctxt  

当服务账户发送请求时,API 服务器将验证令牌,以检查请求者是否有资格以及它所声称的身份是否属实。

用户账户认证

有几种用户账户认证的实现方式。从客户端证书、持有者令牌、静态文件到 OpenID 连接令牌。您可以选择多种身份验证链。在这里,我们将演示客户端证书的工作原理。

在第七章,持续交付中,我们学习了如何为服务账户导出证书和令牌。现在,让我们学习如何为用户做这件事。假设我们仍然在project1命名空间中,并且我们想为我们的新 DevOps 成员琳达创建一个用户,她将帮助我们为my-app进行部署。

首先,我们将通过 OpenSSL(www.openssl.org)生成一个私钥:

// generate a private key for Linda
# openssl genrsa -out linda.key 2048  

接下来,我们将为琳达创建一个证书签名请求(.csr):

// making CN as your username
# openssl req -new -key linda.key -out linda.csr -subj "/CN=linda"  

现在,linda.keylinda.csr应该位于当前文件夹中。为了批准签名请求,我们需要找到我们 Kubernetes 集群的 CA。

在 minikube 中,它位于~/.minikube/。对于其他自托管解决方案,通常位于/etc/kubernetes/下。如果您使用 kops 部署集群,则位置位于/srv/kubernetes下,您可以在/etc/kubernetes/manifests/kube-apiserver.manifest文件中找到路径。

假设我们在当前文件夹下有ca.crtca.key,我们可以通过我们的签名请求生成证书。使用-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-devops.net --namespace=project1 --user=linda
Context "devops-context" modified.  

现在,琳达应该没有任何权限:

// test for getting a pod 
# kubectl --context=devops-context get pods
Error from server (Forbidden): User "linda" cannot list pods in the namespace "project1". (get pods)  

琳达现在通过了认证阶段,而 Kubernetes 知道她是琳达。但是,为了让琳达有权限进行部署,我们需要在授权模块中设置策略。

授权

Kubernetes 支持多个授权模块。在撰写本文时,它支持:

  • ABAC

  • RBAC

  • 节点授权

  • Webhook

  • 自定义模块

基于属性的访问控制ABAC)是在基于角色的访问控制RBAC)引入之前的主要授权模式。节点授权被 kubelet 用于向 API 服务器发出请求。Kubernetes 支持 webhook 授权模式,以与外部 RESTful 服务建立 HTTP 回调。每当面临授权决定时,它都会进行 POST。另一种常见的方式是按照预定义的授权接口实现自己的内部模块。有关更多实现信息,请参阅kubernetes.io/docs/admin/authorization/#custom-modules。在本节中,我们将更详细地描述 ABAC 和 RBAC。

基于属性的访问控制(ABAC)

ABAC 允许管理员将一组用户授权策略定义为每行一个 JSON 格式的文件。ABAC 模式的主要缺点是策略文件在启动 API 服务器时必须存在。文件中的任何更改都需要使用--authorization-policy-file=<policy_file_name>命令重新启动 API 服务器。自 Kubernetes 1.6 以来引入了另一种授权方法 RBAC,它更灵活,不需要重新启动 API 服务器。RBAC 现在已成为最常见的授权模式。

以下是 ABAC 工作原理的示例。策略文件的格式是每行一个 JSON 对象。策略的配置文件类似于我们的其他配置文件。只是在规范中有不同的语法。ABAC 有四个主要属性:

属性类型 支持的值
主题匹配 用户,组
资源匹配 apiGroup,命名空间和资源
非资源匹配 用于非资源类型请求,如/version/apis/cluster
只读 true 或 false

以下是一些示例:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*"}} 
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"linda", "namespace": "project1", "resource": "deployments", "apiGroup": "*", "readonly": true}} 
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"linda", "namespace": "project1", "resource": "replicasets", "apiGroup": "*", "readonly": true}} 

在前面的例子中,我们有一个名为 admin 的用户,可以访问所有内容。另一个名为linda的用户只能在命名空间project1中读取部署和副本集。

基于角色的访问控制(RBAC)

RBAC 在 Kubernetes 1.6 中处于 beta 阶段,默认情况下是启用的。在 RBAC 中,管理员创建了几个RolesClusterRoles,这些角色定义了细粒度的权限,指定了一组资源和操作(动词),角色可以访问和操作这些资源。之后,管理员通过RoleBindingClusterRoleBindings向用户授予Role权限。

如果你正在运行 minikube,在执行minikube start时添加--extra-config=apiserver.Authorization.Mode=RBAC。如果你通过 kops 在 AWS 上运行自托管集群,则在启动集群时添加--authorization=rbac。Kops 会将 API 服务器作为一个 pod 启动;使用kops edit cluster命令可以修改容器的规范。

角色和集群角色

在 Kubernetes 中,Role绑定在命名空间内,而ClusterRole是全局的。以下是一个Role的示例,可以对部署、副本集和 pod 资源执行所有操作,包括getwatchlistcreateupdatedeletepatch

# cat 8-5-2_role.yml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
 namespace: project1
 name: devops-role
rules:
- apiGroups: ["", "extensions", "apps"]
 resources:
 - "deployments"
 - "replicasets"
 - "pods"
 verbs: ["*"]

在我们写这本书的时候,apiVersion仍然是v1beta1。如果 API 版本发生变化,Kubernetes 会抛出错误并提醒您进行更改。在apiGroups中,空字符串表示核心 API 组。API 组是 RESTful API 调用的一部分。核心表示原始 API 调用路径,例如/api/v1。新的 REST 路径中包含组名和 API 版本,例如/apis/$GROUP_NAME/$VERSION;要查找您想要使用的 API 组,请查看kubernetes.io/docs/reference中的 API 参考。在资源下,您可以添加您想要授予访问权限的资源,在动词下列出了此角色可以执行的操作数组。让我们来看一个更高级的ClusterRoles示例,我们在上一章中使用了持续交付角色:

# cat cd-clusterrole.yml
apiVersion: rbac.authorization.k8s.io/v1beta1
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: ["*"]

ClusterRole是集群范围的。一些资源不属于任何命名空间,比如节点,只能由ClusterRole控制。它可以访问的命名空间取决于它关联的ClusterRoleBinding中的namespaces字段。我们可以看到,我们授予了该角色读取和写入 Deployments、ReplicaSets 和 ingresses 的权限,它们分别属于 extensions 和 apps 组。在核心 API 组中,我们只授予了对命名空间和事件的访问权限,以及对其他资源(如 pods 和 services)的所有权限。

RoleBinding 和 ClusterRoleBinding

RoleBinding用于将RoleClusterRole绑定到一组用户或服务账户。如果ClusterRoleRoleBinding绑定而不是ClusterRoleBinding,它将只被授予RoleBinding指定的命名空间内的权限。以下是RoleBinding规范的示例:

# cat 8-5-2_rolebinding_user.yml  
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
 name: devops-role-binding
 namespace: project1
subjects:
- kind: User
 name: linda
 apiGroup: [""]
roleRef:
 kind: Role
 name: devops-role
 apiGroup: [""]

在这个例子中,我们通过roleRefRole与用户绑定。Kubernetes 支持不同类型的roleRef;我们可以在这里将Role的类型替换为ClusterRole

roleRef:
kind: ClusterRole
name: cd-role
apiGroup: rbac.authorization.k8s.io 

然后cd-role只能访问project1命名空间中的资源。

另一方面,ClusterRoleBinding用于在所有命名空间中授予权限。让我们回顾一下我们在第七章中所做的事情,持续交付。我们首先创建了一个名为cd-agent的服务账户,然后创建了一个名为cd-roleClusterRole。最后,我们为cd-agentcd-role创建了一个ClusterRoleBinding。然后我们使用cd-agent代表我们进行部署:

# cat cd-clusterrolebinding.yml
apiVersion: rbac.authorization.k8s.io/v1beta1
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  

cd-agent通过ClusterRoleBindingClusterRole绑定,因此它可以跨命名空间拥有cd-role中指定的权限。由于服务账户是在命名空间中创建的,我们需要指定其完整名称,包括命名空间:

system:serviceaccount:<namespace>:<serviceaccountname> 

让我们通过8-5-2_role.yml8-5-2_rolebinding_user.yml启动RoleRoleBinding

# kubectl create -f 8-5-2_role.yml
role "devops-role" created
# kubectl create -f 8-5-2_rolebinding_user.yml
rolebinding "devops-role-binding" created  

现在,我们不再被禁止了:

# 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 处理请求之前,经过身份验证和授权之后。在启动 API 服务器时,通过添加--admission-control参数来启用它。如果集群版本>=1.6.0,Kubernetes 建议在集群中使用以下插件。

--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,ResourceQuota  

以下介绍了这些插件的用法,以及为什么我们需要它们。有关支持的准入控制插件的更多最新信息,请访问官方文档kubernetes.io/docs/admin/admission-controllers

命名空间生命周期

正如我们之前所了解的,当命名空间被删除时,该命名空间中的所有对象也将被驱逐。此插件确保在终止或不存在的命名空间中无法发出新的对象创建请求。它还防止了 Kubernetes 本机命名空间的删除。

限制范围

此插件确保LimitRange可以正常工作。使用LimitRange,我们可以在命名空间中设置默认请求和限制,在启动未指定请求和限制的 Pod 时将使用这些设置。

服务帐户

如果使用服务帐户对象,则必须添加服务帐户插件。有关服务帐户的更多信息,请再次查看本章中的服务帐户部分。

PersistentVolumeLabel

PersistentVolumeLabel根据底层云提供商提供的标签,为新创建的 PV 添加标签。此准入控制器已从 1.8 版本中弃用。

默认存储类

如果在持久卷索赔中未设置StorageClass,此插件确保默认存储类可以按预期工作。不同的云提供商使用不同的供应工具来利用DefaultStorageClass(例如 GKE 使用 Google Cloud 持久磁盘)。请确保您已启用此功能。

资源配额

就像LimitRange一样,如果您正在使用ResourceQuota对象来管理不同级别的 QoS,则必须启用此插件。资源配额应始终放在准入控制插件列表的末尾。正如我们在资源配额部分提到的,如果使用的配额少于硬配额,资源配额使用将被更新,以确保集群具有足够的资源来接受请求。将其放在准入控制器列表的末尾可以防止请求在被以下控制器拒绝之前过早增加配额使用。

默认容忍秒

在介绍此插件之前,我们必须了解污点容忍是什么。

污点和容忍

污点和忍受度用于阻止一组 Pod 在某些节点上调度运行。污点应用于节点,而忍受度则指定给 Pod。污点的值可以是NoScheduleNoExecute。如果在运行一个带有污点的节点上的 Pod 时没有匹配的忍受度,那么这些 Pod 将被驱逐。

假设我们有两个节点:

# kubectl get nodes
NAME                            STATUS    AGE       VERSION  
ip-172-20-56-91.ec2.internal Ready 6h v1.7.2
ip-172-20-68-10.ec2.internal Ready 29m v1.7.2

现在通过kubectl run nginx --image=nginx:1.12.0 --replicas=1 --port=80命令运行一个 nginx Pod。

该 Pod 正在第一个节点ip-172-20-56-91.ec2.internal上运行:

# kubectl describe pods nginx-4217019353-s9xrn
Name:       nginx-4217019353-s9xrn
Node:       ip-172-20-56-91.ec2.internal/172.20.56.91
Tolerations:    node.alpha.kubernetes.io/notReady:NoExecute for 300s
node.alpha.kubernetes.io/unreachable:NoExecute for 300s  

通过 Pod 描述,我们可以看到有两个默认的忍受度附加到 Pod 上。这意味着如果节点尚未准备好或不可达,那么在 Pod 从节点中被驱逐之前等待 300 秒。这两个忍受度由 DefaultTolerationSeconds 准入控制器插件应用。我们稍后会谈论这个。接下来,我们将在第一个节点上设置一个 taint:

# kubectl taint nodes ip-172-20-56-91.ec2.internal experimental=true:NoExecute
node "ip-172-20-56-91.ec2.internal" tainted  

由于我们将操作设置为NoExecute,并且experimental=true与我们的 Pod 上的任何忍受度不匹配,因此 Pod 将立即从节点中删除并重新调度。可以将多个 taints 应用于一个节点。Pod 必须匹配所有忍受度才能在该节点上运行。以下是一个可以通过的带污染节点的示例:

# cat 8-6_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"  

除了Equal运算符,我们也可以使用Exists。在这种情况下,我们不需要指定值。只要键存在并且效果匹配,那么 Pod 就有资格在带污染的节点上运行。

DefaultTolerationSeconds插件用于设置那些没有设置任何忍受度的 Pod。然后将应用于taints的默认忍受度notready:NoExecuteunreachable: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注释将应用于命名空间。然后该命名空间上的 Pod 将在这些匹配的节点上运行。

AlwaysAdmit

这总是允许所有请求,可能仅用于测试。

AlwaysPullImages

拉取策略定义了 kubelet 拉取镜像时的行为。默认的拉取策略是IfNotPresent,也就是说,如果本地不存在镜像,它将拉取镜像。如果启用了这个插件,那么默认的拉取策略将变为Always,也就是说,总是拉取最新的镜像。这个插件还带来了另一个好处,如果你的集群被不同的团队共享。每当一个 pod 被调度,它都会拉取最新的镜像,无论本地是否存在该镜像。这样我们就可以确保 pod 创建请求始终通过对镜像的授权检查。

AlwaysDeny

这总是拒绝所有请求。它只能用于测试。

DenyEscalatingExec

这个插件拒绝任何kubectl execkubectl attach命令升级特权模式。具有特权模式的 pod 具有主机命名空间的访问权限,这可能会带来安全风险。

其他准入控制器插件

还有许多其他的准入控制器插件可以使用,比如 NodeRestriciton 来限制 kubelet 的权限,ImagePolicyWebhook 来建立一个控制对镜像访问的 webhook,SecurityContextDeny 来控制 pod 或容器的权限。请参考官方文档(kubernetes.io/docs/admin/admission-controllers))以了解其他插件。

总结

在本章中,我们学习了命名空间和上下文是什么以及它们是如何工作的,如何通过设置上下文在物理集群和虚拟集群之间切换。然后我们了解了重要的对象——服务账户,它用于识别在 pod 内运行的进程。然后我们了解了如何在 Kubernetes 中控制访问流程。我们了解了认证和授权之间的区别,以及它们在 Kubernetes 中的工作方式。我们还学习了如何利用 RBAC 为用户提供细粒度的权限。最后,我们学习了一些准入控制器插件,它们是访问控制流程中的最后一道防线。

AWS 是公共 IaaS 提供商中最重要的参与者。在本章中,我们在自托管集群示例中经常使用它。在下一章第九章,在 AWS 上使用 Kubernetes,我们将最终学习如何在 AWS 上部署集群以及在使用 AWS 时的基本概念。

第九章:在 AWS 上的 Kubernetes

在公共云上使用 Kubernetes 对您的应用程序来说是灵活和可扩展的。AWS 是公共云行业中受欢迎的服务之一。在本章中,您将了解 AWS 是什么,以及如何在 AWS 上设置 Kubernetes,以及以下主题:

  • 了解公共云

  • 使用和理解 AWS 组件

  • kops 进行 Kubernetes 设置和管理

  • Kubernetes 云提供商

AWS 简介

当您在公共网络上运行应用程序时,您需要像网络、虚拟机和存储这样的基础设施。显然,公司会借用或建立自己的数据中心来准备这些基础设施,然后雇佣数据中心工程师和运营商来监视和管理这些资源。

然而,购买和维护这些资产需要大量的资本支出;您还需要为数据中心工程师/运营商支付运营费用。您还需要一段时间来完全设置这些基础设施,比如购买服务器,安装到数据中心机架上,连接网络,然后进行操作系统的初始配置/安装等。

因此,快速分配具有适当资源容量的基础设施是决定您业务成功的重要因素之一。

为了使基础设施管理更加简单和快速,数据中心有许多技术可以帮助。例如,对于虚拟化、软件定义网络(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 区域。例如,在us-east-1上的 CIDR 10.0.0.0/16。无论你是否有访问公共网络,你都可以定义任何网络地址范围(在/16 到/28 的掩码范围内)。VPC 的创建非常快速,一旦创建了 VPC,然后你需要在 VPC 内创建一个或多个子网。

在下面的例子中,通过 AWS 命令行创建了一个 VPC:

//specify CIDR block as 10.0.0.0/16
//the result, it returns VPC ID as "vpc-66eda61f"
$ aws ec2 create-vpc --cidr-block 10.0.0.0/16
{
 "Vpc": {
 "VpcId": "vpc-66eda61f", 
   "InstanceTenancy": "default", 
   "Tags": [], 
   "State": "pending", 
   "DhcpOptionsId": "dopt-3d901958", 
   "CidrBlock": "10.0.0.0/16"
  }
}

子网是一个逻辑网络块。它必须属于一个 VPC,并且另外属于一个可用区域。例如,VPC vpc-66eda61fus-east-1b。然后网络 CIDR 必须在 VPC 的 CIDR 内。例如,如果 VPC CIDR 是10.0.0.0/1610.0.0.0 - 10.0.255.255),那么一个子网 CIDR 可以是10.0.1.0/2410.0.1.0 - 10.0.1.255)。

在下面的示例中,创建了两个子网(us-east-1aus-east-1b)到vpc-66eda61f

//1^(st) subnet 10.0."1".0/24 on us-east-1"a" availability zone
$ aws ec2 create-subnet --vpc-id vpc-66eda61f --cidr-block 10.0.1.0/24 --availability-zone us-east-1a
{
 "Subnet": {
    "VpcId": "vpc-66eda61f", 
    "CidrBlock": "10.0.1.0/24", 
    "State": "pending", 
    "AvailabilityZone": "us-east-1a", 
    "SubnetId": "subnet-d83a4b82", 
    "AvailableIpAddressCount": 251
  }
} 

//2^(nd) subnet 10.0."2".0/24 on us-east-1"b"
$ aws ec2 create-subnet --vpc-id vpc-66eda61f --cidr-block 10.0.2.0/24 --availability-zone us-east-1b
{
   "Subnet": {
    "VpcId": "vpc-66eda61f", 
    "CidrBlock": "10.0.2.0/24", 
    "State": "pending", 
    "AvailabilityZone": "us-east-1b", 
    "SubnetId": "subnet-62758c06", 
    "AvailableIpAddressCount": 251
   }
}

让我们将第一个子网设置为面向公众的子网,将第二个子网设置为私有子网。这意味着面向公众的子网可以从互联网访问,从而允许它拥有公共 IP 地址。另一方面,私有子网不能拥有公共 IP 地址。为此,您需要设置网关和路由表。

为了使公共网络和私有网络具有高可用性,建议至少创建四个子网(两个公共和两个私有位于不同的可用区域)。

但为了简化易于理解的示例,这些示例创建了一个公共子网和一个私有子网。

互联网网关和 NAT-GW

在大多数情况下,您的 VPC 需要与公共互联网连接。在这种情况下,您需要创建一个IGW互联网网关)并附加到您的 VPC。

在下面的示例中,创建了一个 IGW 并附加到vpc-66eda61f

//create IGW, it returns IGW id as igw-c3a695a5
$ aws ec2 create-internet-gateway 
{
   "InternetGateway": {
      "Tags": [], 
      "InternetGatewayId": "igw-c3a695a5", 
      "Attachments": []
   }
}

//attach igw-c3a695a5 to vpc-66eda61f
$ aws ec2 attach-internet-gateway --vpc-id vpc-66eda61f --internet-gateway-id igw-c3a695a5  

一旦附加了 IGW,然后为指向 IGW 的子网设置一个路由表(默认网关)。如果默认网关指向 IGW,则该子网可以拥有公共 IP 地址并从/到互联网访问。因此,如果默认网关不指向 IGW,则被确定为私有子网,这意味着没有公共访问。

在下面的示例中,创建了一个指向 IGW 并设置为第一个子网的路由表:

//create route table within vpc-66eda61f
//it returns route table id as rtb-fb41a280
$ aws ec2 create-route-table --vpc-id vpc-66eda61f
{
 "RouteTable": {
 "Associations": [], 
 "RouteTableId": "rtb-fb41a280", 
 "VpcId": "vpc-66eda61f", 
 "PropagatingVgws": [], 
 "Tags": [], 
 "Routes": [
 {
 "GatewayId": "local", 
 "DestinationCidrBlock": "10.0.0.0/16", 
 "State": "active", 
 "Origin": "CreateRouteTable"
 }
 ]
 }
}

//then set default route (0.0.0.0/0) as igw-c3a695a5
$ aws ec2 create-route --route-table-id rtb-fb41a280 --gateway-id igw-c3a695a5 --destination-cidr-block 0.0.0.0/0
{
 "Return": true
}

//finally, update 1^(st) subnet (subnet-d83a4b82) to use this route table
$ aws ec2 associate-route-table --route-table-id rtb-fb41a280 --subnet-id subnet-d83a4b82
{
 "AssociationId": "rtbassoc-bf832dc5"
}

//because 1^(st) subnet is public, assign public IP when launch EC2
$ aws ec2 modify-subnet-attribute --subnet-id subnet-d83a4b82 --map-public-ip-on-launch  

另一方面,尽管第二个子网是一个私有子网,但不需要公共 IP 地址,但是私有子网有时需要访问互联网。例如,下载一些软件包和访问 AWS 服务。在这种情况下,我们仍然有一个连接到互联网的选项。它被称为网络地址转换网关NAT-GW)。

NAT-GW 允许私有子网通过 NAT-GW 访问公共互联网。因此,NAT-GW 必须位于公共子网上,并且私有子网的路由表将 NAT-GW 指定为默认网关。请注意,为了在公共网络上访问 NAT-GW,需要将弹性 IPEIP)附加到 NAT-GW。

在以下示例中,创建了一个 NAT-GW:

//allocate EIP, it returns allocation id as eipalloc-56683465
$ aws ec2 allocate-address 
{
 "PublicIp": "34.233.6.60", 
 "Domain": "vpc", 
 "AllocationId": "eipalloc-56683465"
}

//create NAT-GW on 1^(st) public subnet (subnet-d83a4b82
//also assign EIP eipalloc-56683465
$ aws ec2 create-nat-gateway --subnet-id subnet-d83a4b82 --allocation-id eipalloc-56683465
{
 "NatGateway": {
 "NatGatewayAddresses": [
 {
 "AllocationId": "eipalloc-56683465"
 }
 ], 
 "VpcId": "vpc-66eda61f", 
 "State": "pending", 
 "NatGatewayId": "nat-084ff8ba1edd54bf4", 
 "SubnetId": "subnet-d83a4b82", 
 "CreateTime": "2017-08-13T21:07:34.000Z"
 }
}  

与 IGW 不同,AWS 会对弹性 IP 和 NAT-GW 收取额外的每小时费用。因此,如果希望节省成本,只有在访问互联网时才启动 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-66eda61f
{
 "RouteTable": {
 "Associations": [], 
 "RouteTableId": "rtb-cc4cafb7", 
 "VpcId": "vpc-66eda61f", 
 "PropagatingVgws": [], 
 "Tags": [], 
 "Routes": [
 {
 "GatewayId": "local", 
 "DestinationCidrBlock": "10.0.0.0/16", 
 "State": "active", 
 "Origin": "CreateRouteTable"
 }
 ]
 }
}

//then assign default gateway as NAT-GW
$ aws ec2 create-route --route-table-id rtb-cc4cafb7 --nat-gateway-id nat-084ff8ba1edd54bf4 --destination-cidr-block 0.0.0.0/0
{
 "Return": true
}

//finally update 2^(nd) subnet that use this routing table
$ aws ec2 associate-route-table --route-table-id rtb-cc4cafb7 --subnet-id subnet-62758c06
{
 "AssociationId": "rtbassoc-2760ce5d"
}

总的来说,已经配置了两个子网,一个是公共子网,一个是私有子网。每个子网都有一个默认路由,使用 IGW 和 NAT-GW,如下所示。请注意,ID 会有所不同,因为 AWS 会分配唯一标识符:

子网类型 CIDR 块 子网 ID 路由表 ID 默认网关 EC2 启动时分配公共 IP
公共 10.0.1.0/24 subnet-d83a4b82 rtb-fb41a280 igw-c3a695a5 (IGW)
私有 10.0.2.0/24 subnet-62758c06 rtb-cc4cafb7 nat-084ff8ba1edd54bf4 (NAT-GW) 否(默认)

从技术上讲,您仍然可以为私有子网的 EC2 实例分配公共 IP,但是没有通往互联网的默认网关(IGW)。因此,公共 IP 将被浪费,绝对无法从互联网获得连接。

现在,如果在公共子网上启动 EC2 实例,它将成为公共面向的,因此可以从该子网提供应用程序。

另一方面,如果在私有子网上启动 EC2 实例,它仍然可以通过 NAT-GW 访问互联网,但无法从互联网访问。但是,它仍然可以从公共子网的 EC2 实例访问。因此,您可以部署诸如数据库、中间件和监控工具之类的内部服务。

安全组

一旦 VPC 和相关的网关/路由子网准备就绪,您可以创建 EC2 实例。然而,至少需要事先创建一个访问控制,这就是所谓的安全组。它可以定义一个防火墙规则,即入站(传入网络访问)和出站(传出网络访问)。

在下面的例子中,创建了一个安全组和一个规则,允许来自您机器的 IP 地址的公共子网主机的 ssh,以及全球范围内开放 HTTP(80/tcp):

当您为公共子网定义安全组时,强烈建议由安全专家审查。因为一旦您将 EC2 实例部署到公共子网上,它就有了一个公共 IP 地址,然后包括黑客和机器人在内的所有人都能直接访问您的实例。


//create one security group for public subnet host on vpc-66eda61f
$ aws ec2 create-security-group --vpc-id vpc-66eda61f --group-name public --description "public facing host"
{
 "GroupId": "sg-7d429f0d"
}

//check your machine's public IP (if not sure, use 0.0.0.0/0 as temporary)
$ curl ifconfig.co
107.196.102.199

//public facing machine allows ssh only from your machine
$ aws ec2 authorize-security-group-ingress --group-id sg-7d429f0d --protocol tcp --port 22 --cidr 107.196.102.199/32

//public facing machine allow HTTP access from any host (0.0.0.0/0)
$ aws ec2 authorize-security-group-ingress --group-id sg-d173aea1 --protocol tcp --port 80 --cidr 0.0.0.0/0  

接下来,为私有子网主机创建一个安全组,允许来自公共子网主机的 ssh。在这种情况下,指定公共子网安全组 ID(sg-7d429f0d)而不是 CIDR 块是方便的:

//create security group for private subnet
$ aws ec2 create-security-group --vpc-id vpc-66eda61f --group-name private --description "private subnet host"
{
 "GroupId": "sg-d173aea1"
}

//private subnet allows ssh only from ssh bastion host security group
//it also allows HTTP (80/TCP) from public subnet security group
$ aws ec2 authorize-security-group-ingress --group-id sg-d173aea1 --protocol tcp --port 22 --source-group sg-7d429f0d

//private subnet allows HTTP access from public subnet security group too
$ aws ec2 authorize-security-group-ingress --group-id sg-d173aea1 --protocol tcp --port 80 --source-group sg-7d429f0d

总的来说,以下是已创建的两个安全组:

名称 安全组 ID 允许 ssh(22/TCP) 允许 HTTP(80/TCP)
公共 sg-7d429f0d 您的机器(107.196.102.199 0.0.0.0/0
私有 sg-d173aea1 公共 sg(sg-7d429f0d 公共 sg(sg-7d429f0d

EC2 和 EBS

EC2 是 AWS 中的一个重要服务,您可以在您的 VPC 上启动一个 VM。根据硬件规格(CPU、内存和网络),AWS 上有几种类型的 EC2 实例可用。当您启动一个 EC2 实例时,您需要指定 VPC、子网、安全组和 ssh 密钥对。因此,所有这些都必须事先创建。

由于之前的例子,唯一的最后一步是 ssh 密钥对。让我们创建一个 ssh 密钥对:

//create keypair (internal_rsa, internal_rsa.pub)
$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/saito/.ssh/id_rsa): /tmp/internal_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /tmp/internal_rsa.
Your public key has been saved in /tmp/internal_rsa.pub.

//register internal_rsa.pub key to AWS
$ aws ec2 import-key-pair --key-name=internal --public-key-material "`cat /tmp/internal_rsa.pub`"
{
 "KeyName": "internal", 
   "KeyFingerprint":  
 "18:e7:86:d7:89:15:5d:3b:bc:bd:5f:b4:d5:1c:83:81"
} 

//launch public facing host, using Amazon Linux on us-east-1 (ami-a4c7edb2)
$ aws ec2 run-instances --image-id ami-a4c7edb2 --instance-type t2.nano --key-name internal --security-group-ids sg-7d429f0d --subnet-id subnet-d83a4b82

//launch private subnet host
$ aws ec2 run-instances --image-id ami-a4c7edb2 --instance-type t2.nano --key-name internal --security-group-ids sg-d173aea1 --subnet-id subnet-62758c06  

几分钟后,在 AWS Web 控制台上检查 EC2 实例的状态;它显示一个具有公共 IP 地址的公共子网主机。另一方面,私有子网主机没有公共 IP 地址:

//add private keys to ssh-agent
$ ssh-add -K /tmp/internal_rsa
Identity added: /tmp/internal_rsa (/tmp/internal_rsa)
$ ssh-add -l
2048 SHA256:AMkdBxkVZxPz0gBTzLPCwEtaDqou4XyiRzTTG4vtqTo /tmp/internal_rsa (RSA)

//ssh to the public subnet host with -A (forward ssh-agent) option
$ ssh -A ec2-user@54.227.197.56
The authenticity of host '54.227.197.56 (54.227.197.56)' can't be established.
ECDSA key fingerprint is SHA256:ocI7Q60RB+k2qbU90H09Or0FhvBEydVI2wXIDzOacaE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '54.227.197.56' (ECDSA) to the list of known hosts.

           __|  __|_  )
           _|  (     /   Amazon Linux AMI
          ___|\___|___|

    https://aws.amazon.com/amazon-linux-ami/2017.03-release-notes/
    2 package(s) needed for security, out of 6 available
    Run "sudo yum update" to apply all updates.

现在您位于公共子网主机(54.227.197.56),但是这台主机也有一个内部(私有)IP 地址,因为这台主机部署在 10.0.1.0/24 子网(subnet-d83a4b82)中,因此私有地址范围必须是10.0.1.1 - 10.0.1.254

$ ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 0E:8D:38:BE:52:34 
          inet addr:10.0.1.24  Bcast:10.0.1.255      
          Mask:255.255.255.0

让我们在公共主机上安装 nginx web 服务器如下:

$ sudo yum -y -q install nginx
$ sudo /etc/init.d/nginx start
Starting nginx:                                            [  OK  ]

然后,回到您的机器上,检查54.227.197.56的网站:

$ exit
logout
Connection to 52.227.197.56 closed.

//from your machine, access to nginx
$ curl -I 54.227.197.56
HTTP/1.1 200 OK
Server: nginx/1.10.3
...
Accept-Ranges: bytes  

此外,在同一个 VPC 内,其他可用区域也是可达的,因此您可以从这个主机 ssh 到私有子网主机(10.0.2.98)。请注意,我们使用了ssh -A选项,它转发了一个 ssh-agent,因此不需要创建~/.ssh/id_rsa文件:

[ec2-user@ip-10-0-1-24 ~]$ ssh 10.0.2.98
The authenticity of host '10.0.2.98 (10.0.2.98)' can't be established.
ECDSA key fingerprint is 1a:37:c3:c1:e3:8f:24:56:6f:90:8f:4a:ff:5e:79:0b.
Are you sure you want to continue connecting (yes/no)? yes
    Warning: Permanently added '10.0.2.98' (ECDSA) to the list of known hosts.

           __|  __|_  )
           _|  (     /   Amazon Linux AMI
          ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.03-release-notes/
2 package(s) needed for security, out of 6 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-0-2-98 ~]$ 

除了 EC2,还有一个重要的功能,即磁盘管理。AWS 提供了一个灵活的磁盘管理服务,称为弹性块存储EBS)。您可以创建一个或多个持久数据存储,可以附加到 EC2 实例上。从 EC2 的角度来看,EBS 是 HDD/SSD 之一。一旦终止(删除)了 EC2 实例,EBS 及其内容可能会保留,然后重新附加到另一个 EC2 实例上。

在下面的例子中,创建了一个具有 40GB 容量的卷,并附加到一个公共子网主机(实例 IDi-0db344916c90fae61):

//create 40GB disk at us-east-1a (as same as EC2 host instance)
$ aws ec2 create-volume --availability-zone us-east-1a --size 40 --volume-type standard
{
    "AvailabilityZone": "us-east-1a", 
    "Encrypted": false, 
    "VolumeType": "standard", 
    "VolumeId": "vol-005032342495918d6", 
    "State": "creating", 
    "SnapshotId": "", 
    "CreateTime": "2017-08-16T05:41:53.271Z", 
    "Size": 40
}

//attach to public subnet host as /dev/xvdh
$ aws ec2 attach-volume --device xvdh --instance-id i-0db344916c90fae61 --volume-id vol-005032342495918d6
{
    "AttachTime": "2017-08-16T05:47:07.598Z", 
    "InstanceId": "i-0db344916c90fae61", 
    "VolumeId": "vol-005032342495918d6", 
    "State": "attaching", 
    "Device": "xvdh"
}

将 EBS 卷附加到 EC2 实例后,Linux 内核会识别/dev/xvdh,然后您需要对该设备进行分区,如下所示:

在这个例子中,我们将一个分区命名为/dev/xvdh1,所以你可以在/dev/xvdh1上创建一个ext4格式的文件系统,然后可以挂载到 EC2 实例上使用这个设备:

卸载卷后,您可以随时分离该卷,然后在需要时重新附加它:

//detach volume
$ aws ec2 detach-volume --volume-id vol-005032342495918d6
{
    "AttachTime": "2017-08-16T06:03:45.000Z", 
    "InstanceId": "i-0db344916c90fae61", 
    "VolumeId": "vol-005032342495918d6", 
    "State": "detaching", 
    "Device": "xvdh"
}

Route 53

AWS 还提供了一个托管 DNS 服务,称为Route 53。Route 53 允许您管理自己的域名和关联的 FQDN 到 IP 地址。例如,如果您想要一个域名k8s-devops.net,您可以通过 Route 53 订购注册您的 DNS 域名。

以下屏幕截图显示订购域名k8s-devops.net;可能需要几个小时才能完成注册:

注册完成后,您可能会收到来自 AWS 的通知电子邮件,然后您可以通过 AWS 命令行或 Web 控制台控制这个域名。让我们添加一个记录(FQDN 到 IP 地址),将public.k8s-devops.net与公共面向的 EC2 主机公共 IP 地址54.227.197.56关联起来。为此,获取托管区域 ID 如下:

$ aws route53 list-hosted-zones | grep Id
"Id": "/hostedzone/Z1CTVYM9SLEAN8",   

现在您得到了一个托管区域 ID,即/hostedzone/Z1CTVYM9SLEAN8,所以让我们准备一个 JSON 文件来更新 DNS 记录如下:

//create JSON file
$ cat /tmp/add-record.json 
{
 "Comment": "add public subnet host",
  "Changes": [
   {
     "Action": "UPSERT",
     "ResourceRecordSet": {
       "Name": "public.k8s-devops.net",
       "Type": "A",
       "TTL": 300,
       "ResourceRecords": [
         {
          "Value": "54.227.197.56"
         }
       ]
     }
   }
  ]
}

//submit to Route53
$ aws route53 change-resource-record-sets --hosted-zone-id /hostedzone/Z1CTVYM9SLEAN8 --change-batch file:///tmp/add-record.json 

//a few minutes later, check whether A record is created or not
$ dig public.k8s-devops.net

; <<>> DiG 9.8.3-P1 <<>> public.k8s-devops.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18609
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;public.k8s-devops.net.       IN    A

;; ANSWER SECTION:
public.k8s-devops.net.  300   IN    A     54.227.197.56  

看起来不错,现在通过 DNS 名称public.k8s-devops.net访问 nginx:

$ curl -I public.k8s-devops.net
HTTP/1.1 200 OK
Server: nginx/1.10.3
...  

ELB

AWS 提供了一个强大的基于软件的负载均衡器,称为弹性负载均衡器ELB)。它允许您将网络流量负载均衡到一个或多个 EC2 实例。此外,ELB 可以卸载 SSL/TLS 加密/解密,并且还支持多可用区。

在以下示例中,创建了一个 ELB,并将其与公共子网主机 nginx(80/TCP)关联。因为 ELB 还需要一个安全组,所以首先为 ELB 创建一个新的安全组:

$ aws ec2 create-security-group --vpc-id vpc-66eda61f --group-name elb --description "elb sg"
{
  "GroupId": "sg-51d77921"
} 
$ aws ec2 authorize-security-group-ingress --group-id sg-51d77921 --protocol tcp --port 80 --cidr 0.0.0.0/0

$ aws elb create-load-balancer --load-balancer-name public-elb --listeners Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80 --subnets subnet-d83a4b82 --security-groups sg-51d77921
{
   "DNSName": "public-elb-1779693260.us-east- 
    1.elb.amazonaws.com"
}

$ aws elb register-instances-with-load-balancer --load-balancer-name public-elb --instances i-0db344916c90fae61

$ curl -I public-elb-1779693260.us-east-1.elb.amazonaws.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 3770
Content-Type: text/html
...  

让我们更新 Route 53 DNS 记录public.k8s-devops.net,指向 ELB。在这种情况下,ELB 已经有一个A记录,因此使用指向 ELB FQDN 的CNAME(别名):

$ cat change-to-elb.json 
{
 "Comment": "use CNAME to pointing to ELB",
  "Changes": [
    {
      "Action": "DELETE",
      "ResourceRecordSet": {
        "Name": "public.k8s-devops.net",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
           "Value": "52.86.166.223"
          }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "public.k8s-devops.net",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [
          {
           "Value": "public-elb-1779693260.us-east-           
1.elb.amazonaws.com"
          }
        ]
      }
 }
 ]
}

$ dig public.k8s-devops.net

; <<>> DiG 9.8.3-P1 <<>> public.k8s-devops.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10278
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;public.k8s-devops.net.       IN    A

;; ANSWER SECTION:
public.k8s-devops.net.  300   IN    CNAME public-elb-1779693260.us-east-1.elb.amazonaws.com.
public-elb-1779693260.us-east-1.elb.amazonaws.com. 60 IN A 52.200.46.81
public-elb-1779693260.us-east-1.elb.amazonaws.com. 60 IN A 52.73.172.171

;; Query time: 77 msec
;; SERVER: 10.0.0.1#53(10.0.0.1)
;; WHEN: Wed Aug 16 22:21:33 2017
;; MSG SIZE  rcvd: 134

$ curl -I public.k8s-devops.net
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 3770
Content-Type: text/html
...  

S3

AWS 提供了一个有用的对象数据存储服务,称为简单存储服务S3)。它不像 EBS,没有 EC2 实例可以挂载为文件系统。相反,使用 AWS API 将文件传输到 S3。因此,AWS 可以实现可用性(99.999999999%),并且多个实例可以同时访问它。它适合存储非吞吐量和随机访问敏感的文件,如配置文件、日志文件和数据文件。

在以下示例中,从您的计算机上传文件到 AWS S3:

//create S3 bucket "k8s-devops"
$ aws s3 mb s3://k8s-devops
make_bucket: k8s-devops

//copy files to S3 bucket
$ aws s3 cp add-record.json s3://k8s-devops/
upload: ./add-record.json to s3://k8s-devops/add-record.json 
$ aws s3 cp change-to-elb.json s3://k8s-devops/
upload: ./change-to-elb.json to s3://k8s-devops/change-to-elb.json 

//check files on S3 bucket
$ aws s3 ls s3://k8s-devops/
2017-08-17 20:00:21        319 add-record.json
2017-08-17 20:00:28        623 change-to-elb.json  

总的来说,我们已经讨论了如何配置围绕 VPC 的 AWS 组件。以下图表显示了一个主要组件和关系:

在 AWS 上设置 Kubernetes

我们已经讨论了一些 AWS 组件,这些组件非常容易设置网络、虚拟机和存储。因此,在 AWS 上设置 Kubernetes 有多种方式,例如 kubeadm(github.com/kubernetes/kubeadm)、kops(github.com/kubernetes/kops)和 kubespray(github.com/kubernetes-incubator/kubespray)。在 AWS 上配置 Kubernetes 的推荐方式之一是使用 kops,这是一个生产级的设置工具,并支持大量配置。在本章中,我们将使用 kops 在 AWS 上配置 Kubernetes。请注意,kops 代表 Kubernetes 操作。

安装 kops

首先,您需要将 kops 安装到您的机器上。Linux 和 macOS 都受支持。Kops 是一个单一的二进制文件,所以只需将kops命令复制到/usr/local/bin中,如推荐的那样。之后,为 kops 创建一个处理 kops 操作的 IAM 用户和角色。有关详细信息,请参阅官方文档(github.com/kubernetes/kops/blob/master/docs/aws.md)。

运行 kops

Kops 需要一个存储配置和状态的 S3 存储桶。此外,使用 Route 53 来注册 Kubernetes API 服务器名称和 etcd 服务器名称到域名系统。因此,在前一节中创建的 S3 存储桶和 Route 53。

Kops 支持各种配置,例如部署到公共子网、私有子网,使用不同类型和数量的 EC2 实例,高可用性和叠加网络。让我们使用与前一节中网络类似的配置来配置 Kubernetes,如下所示:

Kops 有一个选项可以重用现有的 VPC 和子网。但是,它的行为很棘手,可能会根据设置遇到一些问题;建议使用 kops 创建一个新的 VPC。有关详细信息,您可以在github.com/kubernetes/kops/blob/master/docs/run_in_existing_vpc.md找到一份文档。

参数 意义
- --name my-cluster.k8s-devops.net k8s-devops.net域下设置my-cluster
- --state s3://k8s-devops 使用 k8s-devops S3 存储桶
- --zones us-east-1a 部署在us-east-1a可用区
- --cloud aws 使用 AWS 作为云提供商
- --network-cidr 10.0.0.0/16 使用 CIDR 10.0.0.0/16 创建新的 VPC
- --master-size t2.large 为主节点使用 EC2 t2.large实例
- --node-size t2.medium 为节点使用 EC2 t2.medium实例
- --node-count 2 设置两个节点
- --networking calico 使用 Calico 进行叠加网络
- --topology private 设置公共和私有子网,并将主节点和节点部署到私有子网
- --ssh-puglic-key /tmp/internal_rsa.pub 为堡垒主机使用/tmp/internal_rsa.pub
- --bastion 在公共子网上创建 ssh 堡垒服务器
- --yes 立即执行

因此,运行以下命令来运行 kops:

$ kops create cluster --name my-cluster.k8s-devops.net --state=s3://k8s-devops --zones us-east-1a --cloud aws --network-cidr 10.0.0.0/16 --master-size t2.large --node-size t2.medium --node-count 2 --networking calico --topology private --ssh-public-key /tmp/internal_rsa.pub --bastion --yes

I0818 20:43:15.022735   11372 create_cluster.go:845] Using SSH public key: /tmp/internal_rsa.pub
...
I0818 20:45:32.585246   11372 executor.go:91] Tasks: 78 done / 78 total; 0 can run
I0818 20:45:32.587067   11372 dns.go:152] Pre-creating DNS records
I0818 20:45:35.266425   11372 update_cluster.go:247] Exporting kubecfg for cluster
Kops has set your kubectl context to my-cluster.k8s-devops.net

Cluster is starting.  It should be ready in a few minutes.  

在看到上述消息后,完全完成可能需要大约 5 到 10 分钟。这是因为它需要我们创建 VPC、子网和 NAT-GW,启动 EC2,然后安装 Kubernetes 主节点和节点,启动 ELB,然后更新 Route 53 如下:

完成后,kops会更新您机器上的~/.kube/config,指向您的 Kubernetes API 服务器。Kops 会创建一个 ELB,并在 Route 53 上设置相应的 FQDN 记录为https://api.<your-cluster-name>.<your-domain-name>/,因此,您可以直接从您的机器上运行kubectl命令来查看节点列表,如下所示:

$ kubectl get nodes
NAME                          STATUS         AGE       VERSION
ip-10-0-36-157.ec2.internal   Ready,master   8m        v1.7.0
ip-10-0-42-97.ec2.internal    Ready,node     6m        v1.7.0
ip-10-0-42-170.ec2.internal   Ready,node     6m        v1.7.0

太棒了!从头开始在 AWS 上设置 AWS 基础设施和 Kubernetes 只花了几分钟。现在您可以通过kubectl命令部署 pod。但是您可能想要 ssh 到 master/node 上查看发生了什么。

然而,出于安全原因,如果您指定了--topology private,您只能 ssh 到堡垒主机。然后使用私有 IP 地址 ssh 到 master/node 主机。这类似于前一节中 ssh 到公共子网主机,然后使用 ssh-agent(-A选项)ssh 到私有子网主机。

在以下示例中,我们 ssh 到堡垒主机(kops 创建 Route 53 条目为bastion.my-cluster.k8s-devops.net),然后 ssh 到 master(10.0.36.157):

Kubernetes 云服务提供商

在使用 kops 设置 Kubernetes 时,它还将 Kubernetes 云服务提供商配置为 AWS。这意味着当您使用 LoadBalancer 的 Kubernetes 服务时,它将使用 ELB。它还将弹性块存储EBS)作为其StorageClass

L4 负载均衡器

当您将 Kubernetes 服务公开到外部世界时,使用 ELB 更有意义。将服务类型设置为 LoadBalancer 将调用 ELB 创建并将其与节点关联:

$ cat grafana.yml 
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: grafana
spec:
 replicas: 1
 template:
 metadata:
 labels:
 run: 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:
 run: grafana

$ kubectl create -f grafana.yml 
deployment "grafana" created
service "grafana" created

$ kubectl get service
NAME         CLUSTER-IP       EXTERNAL-IP        PORT(S)        AGE
grafana      100.65.232.120   a5d97c8ef8575...   80:32111/TCP   11s
kubernetes   100.64.0.1       <none>             443/TCP        13m

$ aws elb describe-load-balancers | grep a5d97c8ef8575 | grep DNSName
 "DNSName": "a5d97c8ef857511e7a6100edf846f38a-1490901085.us-east-1.elb.amazonaws.com",  

如您所见,ELB 已经自动创建,DNS 为a5d97c8ef857511e7a6100edf846f38a-1490901085.us-east-1.elb.amazonaws.com,因此现在您可以在http://a5d97c8ef857511e7a6100edf846f38a-1490901085.us-east-1.elb.amazonaws.com访问 Grafana。

您可以使用awscli来更新 Route 53,分配一个CNAME,比如grafana.k8s-devops.net。另外,Kubernetes 的孵化项目external-dnsgithub.com/kubernetes-incubator/external-dns))可以自动更新 Route 53 在这种情况下。

L7 负载均衡器(入口)

截至 kops 版本 1.7.0,它尚未默认设置 ingress 控制器。然而,kops 提供了一些插件(github.com/kubernetes/kops/tree/master/addons)来扩展 Kubernetes 的功能。其中一个插件 ingress-nginx(github.com/kubernetes/kops/tree/master/addons/ingress-nginx)使用 AWS ELB 和 nginx 的组合来实现 Kubernetes 的 ingress 控制器。

为了安装ingress-nginx插件,输入以下命令来设置 ingress 控制器:

$ kubectl create -f https://raw.githubusercontent.com/kubernetes/kops/master/addons/ingress-nginx/v1.6.0.yaml
namespace "kube-ingress" created
serviceaccount "nginx-ingress-controller" created
clusterrole "nginx-ingress-controller" created
role "nginx-ingress-controller" created
clusterrolebinding "nginx-ingress-controller" created
rolebinding "nginx-ingress-controller" created
service "nginx-default-backend" created
deployment "nginx-default-backend" created
configmap "ingress-nginx" created
service "ingress-nginx" created
deployment "ingress-nginx" created

之后,使用 NodePort 服务部署 nginx 和 echoserver 如下:

$ kubectl run nginx --image=nginx --port=80
deployment "nginx" created
$ 
$ kubectl expose deployment nginx --target-port=80 --type=NodePort
service "nginx" exposed
$ 
$ kubectl run echoserver --image=gcr.io/google_containers/echoserver:1.4 --port=8080
deployment "echoserver" created
$ 
$ kubectl expose deployment echoserver --target-port=8080 --type=NodePort
service "echoserver" exposed

// URL "/" point to nginx, "/echo" to echoserver
$ cat nginx-echoserver-ingress.yaml 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: nginx-echoserver-ingress
spec:
 rules:
 - http:
 paths:
 - path: /
 backend:
 serviceName: nginx
 servicePort: 80
 - path: /echo
 backend:
 serviceName: echoserver
 servicePort: 8080

//check ingress
$ kubectl get ing -o wide
NAME                       HOSTS     ADDRESS                                                                 PORTS     AGE
nginx-echoserver-ingress   *         a1705ab488dfa11e7a89e0eb0952587e-28724883.us-east-1.elb.amazonaws.com   80        1m 

几分钟后,ingress 控制器将 nginx 服务和 echoserver 服务与 ELB 关联起来。当您使用 URI "/"访问 ELB 服务器时,它会显示 nginx 屏幕如下:

另一方面,如果你访问相同的 ELB,但使用 URI "/echo",它会显示 echoserver 如下:

与标准的 Kubernetes 负载均衡器服务相比,一个负载均衡器服务会消耗一个 ELB。另一方面,使用 nginx-ingress 插件,它可以将多个 Kubernetes NodePort 服务整合到单个 ELB 上。这将有助于更轻松地构建您的 RESTful 服务。

StorageClass

正如我们在第四章中讨论的那样,有一个StorageClass可以动态分配持久卷。Kops 将 provisioner 设置为aws-ebs,使用 EBS:

$ kubectl get storageclass
NAME            TYPE
default         kubernetes.io/aws-ebs 
gp2 (default)   kubernetes.io/aws-ebs 

$ cat pvc-aws.yml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: pvc-aws-1
spec:
 storageClassName: "default"
 accessModes:
 - ReadWriteOnce
 resources:
 requests:
 storage: 10Gi

$ kubectl create -f pvc-aws.yml 
persistentvolumeclaim "pvc-aws-1" created

$ kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM               STORAGECLASS   REASON    AGE
pvc-94957090-84a8-11e7-9974-0ea8dc53a244   10Gi       RWO           Delete          Bound     default/pvc-aws-1   default                  3s  

这将自动创建 EBS 卷如下:

$ aws ec2 describe-volumes --filter Name=tag-value,Values="pvc-51cdf520-8576-11e7-a610-0edf846f38a6"
{
 "Volumes": [
    {
      "AvailabilityZone": "us-east-1a", 
    "Attachments": [], 
      "Tags": [
       {
...
     ], 
    "Encrypted": false, 
    "VolumeType": "gp2", 
    "VolumeId": "vol-052621c39546f8096", 
    "State": "available", 
    "Iops": 100, 
    "SnapshotId": "", 
    "CreateTime": "2017-08-20T07:08:08.773Z", 
       "Size": 10
       }
     ]
   }

总的来说,AWS 的 Kubernetes 云提供程序被用来将 ELB 映射到 Kubernetes 服务,还有将 EBS 映射到 Kubernetes 持久卷。对于 Kubernetes 来说,使用 AWS 是一个很大的好处,因为不需要预先分配或购买物理负载均衡器或存储,只需按需付费;这为您的业务创造了灵活性和可扩展性。

通过 kops 维护 Kubernetes 集群

当您需要更改 Kubernetes 配置,比如节点数量甚至 EC2 实例类型,kops 可以支持这种用例。例如,如果您想将 Kubernetes 节点实例类型从t2.medium更改为t2.micro,并且由于成本节约而将数量从 2 减少到 1,您需要修改 kops 节点实例组(ig)设置如下:

$ kops edit ig nodes --name my-cluster.k8s-devops.net --state=s3://k8s-devops   

它启动了 vi 编辑器,您可以更改 kops 节点实例组的设置如下:

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
 creationTimestamp: 2017-08-20T06:43:45Z
 labels:
 kops.k8s.io/cluster: my-cluster.k8s-devops.net
 name: nodes
spec:
 image: kope.io/k8s-1.6-debian-jessie-amd64-hvm-ebs-2017- 
 05-02
 machineType: t2.medium
 maxSize: 2
 minSize: 2
 role: Node
 subnets:
 - us-east-1a  

在这种情况下,将machineType更改为t2.small,将maxSize/minSize更改为1,然后保存。之后,运行kops update命令应用设置:

$ kops update cluster --name my-cluster.k8s-devops.net --state=s3://k8s-devops --yes 

I0820 00:57:17.900874    2837 executor.go:91] Tasks: 0 done / 94 total; 38 can run
I0820 00:57:19.064626    2837 executor.go:91] Tasks: 38 done / 94 total; 20 can run
...
Kops has set your kubectl context to my-cluster.k8s-devops.net
Cluster changes have been applied to the cloud.

Changes may require instances to restart: kops rolling-update cluster  

正如您在前面的消息中看到的,您需要运行kops rolling-update cluster命令来反映现有实例。将现有实例替换为新实例可能需要几分钟:

$ kops rolling-update cluster --name my-cluster.k8s-devops.net --state=s3://k8s-devops --yes
NAME              STATUS     NEEDUPDATE  READY MIN   MAX   NODES
bastions          Ready       0           1     1     1     0
master-us-east-1a Ready       0           1     1     1     1
nodes             NeedsUpdate 1           0     1     1     1
I0820 01:00:01.086564    2844 instancegroups.go:350] Stopping instance "i-07e55394ef3a09064", node "ip-10-0-40-170.ec2.internal", in AWS ASG "nodes.my-cluster.k8s-devops.net".  

现在,Kubernetes 节点实例已从2减少到1,如下所示:

$ kubectl get nodes
NAME                          STATUS         AGE       VERSION
ip-10-0-36-157.ec2.internal   Ready,master   1h        v1.7.0
ip-10-0-58-135.ec2.internal   Ready,node     34s       v1.7.0  

总结

在本章中,我们已经讨论了公共云。AWS 是最流行的公共云服务,它提供 API 以编程方式控制 AWS 基础设施。我们可以轻松实现自动化和基础架构即代码。特别是,kops 使我们能够从头开始快速设置 AWS 和 Kubernetes。Kubernetes 和 kops 的开发都非常活跃。请继续监视这些项目,它们将在不久的将来具有更多功能和配置。

下一章将介绍Google Cloud PlatformGCP),这是另一个流行的公共云服务。Google Container EngineGKE)是托管的 Kubernetes 服务,使使用 Kubernetes 变得更加容易。

第十章:GCP 上的 Kubernetes

Google Cloud Platform(GCP)在公共云行业中越来越受欢迎,由 Google 提供。GCP 与 AWS 具有类似的概念,如 VPC、计算引擎、持久磁盘、负载均衡和一些托管服务。在本章中,您将了解 GCP 以及如何通过以下主题在 GCP 上设置 Kubernetes:

  • 理解 GCP

  • 使用和理解 GCP 组件

  • 使用 Google Container Engine(GKE),托管的 Kubernetes 服务

GCP 简介

GCP 于 2011 年正式推出。但与 AWS 不同的是,GCP 最初提供了 PaaS(平台即服务)。因此,您可以直接部署您的应用程序,而不是启动虚拟机。之后,不断增强功能,支持各种服务。

对于 Kubernetes 用户来说,最重要的服务是 GKE,这是一个托管的 Kubernetes 服务。因此,您可以从 Kubernetes 的安装、升级和管理中得到一些缓解。它采用按使用 Kubernetes 集群的方式付费。GKE 也是一个非常活跃的服务,不断及时提供新版本的 Kubernetes,并为 Kubernetes 提供新功能和管理工具。

让我们看看 GCP 提供了什么样的基础设施和服务,然后探索 GKE。

GCP 组件

GCP 提供了 Web 控制台和命令行界面(CLI)。两者都很容易直接地控制 GCP 基础设施,但需要 Google 账户(如 Gmail)。一旦您拥有了 Google 账户,就可以转到 GCP 注册页面(cloud.google.com/free/)来创建您的 GCP 账户。

如果您想通过 CLI 进行控制,您需要安装 Cloud SDK(cloud.google.com/sdk/gcloud/),这类似于 AWS CLI,您可以使用它来列出、创建、更新和删除 GCP 资源。安装 Cloud SDK 后,您需要使用以下命令将其配置到 GCP 账户:

$ gcloud init

VPC

与 AWS 相比,GCP 中的 VPC 政策有很大不同。首先,您不需要为 VPC 设置 CIDR 前缀,换句话说,您不能为 VPC 设置 CIDR。相反,您只需向 VPC 添加一个或多个子网。因为子网总是带有特定的 CIDR 块,因此 GCP VPC 被识别为一组逻辑子网,VPC 内的子网可以相互通信。

请注意,GCP VPC 有两种模式,即自动自定义。如果您选择自动模式,它将在每个区域创建一些具有预定义 CIDR 块的子网。例如,如果您输入以下命令:

$ gcloud compute networks create my-auto-network --mode auto

它将创建 11 个子网,如下面的屏幕截图所示(因为截至 2017 年 8 月,GCP 有 11 个区域):

自动模式 VPC 可能是一个很好的起点。但是,在自动模式下,您无法指定 CIDR 前缀,而且来自所有区域的 11 个子网可能无法满足您的用例。例如,如果您想通过 VPN 集成到您的本地数据中心,或者只想从特定区域创建子网。

在这种情况下,选择自定义模式 VPC,然后可以手动创建具有所需 CIDR 前缀的子网。输入以下命令以创建自定义模式 VPC:

//create custom mode VPC which is named my-custom-network
$ gcloud compute networks create my-custom-network --mode custom  

因为自定义模式 VPC 不会像下面的屏幕截图所示创建任何子网,让我们在这个自定义模式 VPC 上添加子网:

子网

在 GCP 中,子网始终跨越区域内的多个区域(可用区)。换句话说,您无法像 AWS 一样在单个区域创建子网。创建子网时,您总是需要指定整个区域。

此外,与 AWS 不同(结合路由和 Internet 网关或 NAT 网关确定为公共或私有子网的重要概念),GCP 没有明显的公共和私有子网概念。这是因为 GCP 中的所有子网都有指向 Internet 网关的路由。

GCP 使用网络标签而不是子网级别的访问控制,以确保网络安全。这将在下一节中进行更详细的描述。

这可能会让网络管理员感到紧张,但是 GCP 最佳实践为您带来了更简化和可扩展的 VPC 管理,因为您可以随时添加子网以扩展整个网络块。

从技术上讲,您可以启动 VM 实例设置为 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 中,您不能直接分配任何防火墙规则。相反,防火墙规则和 VM 实例是通过网络标签松散耦合的。因此,防火墙规则和 VM 实例之间没有直接关联。以下图表是 AWS 安全组和 GCP 防火墙规则之间的比较。EC2 需要安全组,另一方面,GCP VM 实例只需设置一个标签。这与相应的防火墙是否具有相同的标签无关。

例如,根据以下命令为公共主机(使用网络标签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"

它创建了四个防火墙规则,如下图所示。让我们创建 VM 实例,以使用publicprivate网络标签,看看它是如何工作的:

VM 实例

GCP 中的 VM 实例与 AWS EC2 非常相似。您可以选择各种具有不同硬件配置的机器(实例)类型。以及 Linux 或基于 Windows 的 OS 镜像或您定制的 OS,您可以选择。

如在讨论防火墙规则时提到的,您可以指定零个或多个网络标签。标签不一定要事先创建。这意味着您可以首先使用网络标签启动 VM 实例,即使没有创建防火墙规则。这仍然是有效的,但在这种情况下不会应用防火墙规则。然后创建一个防火墙规则来具有网络标签。最终,防火墙规则将应用于 VM 实例。这就是为什么 VM 实例和防火墙规则松散耦合的原因,这为用户提供了灵活性。

在启动 VM 实例之前,您需要首先创建一个 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 上启动一个 VM 实例。

subnet-asubnet-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,如下面的屏幕截图所示:

另一方面,公共主机允许来自您的机器的 ssh,因为 public-ssh 规则允许任何(0.0.0.0/0)。

当然,这台主机可以通过私有 IP 地址在subnet-a10.0.1.2)上 ping 和 ssh 到私有主机,因为有internal-icmp规则和private-ssh规则。

让我们通过 ssh 连接到私有主机,然后安装tomcat8tomcat8-examples包(它将在 Tomcat 中安装/examples/应用程序)。

请记住,subnet-a10.0.1.0/24的 CIDR 前缀,但subnet-b172.16.1.0/24的 CIDR 前缀。但在同一个 VPC 中,它们之间是可以互相连接的。这是使用 GCP 的一个巨大优势,您可以根据需要扩展网络地址块。

现在,在公共主机(public-on-subnet-apublic-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  

然而,此时,您无法访问私有主机上的 Tomcat。即使它有一个公共 IP 地址。这是因为私有主机还没有任何允许 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 经典 ELB。另一方面,第 7 层 HTTP(S)负载均衡器具有基于内容(上下文)的路由。例如,URL /img 将转发到实例 a,其他所有内容将转发到实例 b。因此,它更像是一个应用级别的负载均衡器。

AWS 还提供了应用负载均衡器ALBELBv2),它与 GCP 第 7 层 HTTP(S)负载均衡器非常相似。有关详细信息,请访问aws.amazon.com/blogs/aws/new-aws-application-load-balancer/

为了设置负载均衡器,与 AWS ELB 不同,需要在配置一些项目之前进行几个步骤:

配置项 目的
实例组 确定 VM 实例或 VM 模板(OS 镜像)的组。
健康检查 设置健康阈值(间隔、超时等)以确定实例组的健康状态。
后端服务 设置负载阈值(最大 CPU 或每秒请求)和会话亲和性(粘性会话)到实例组,并关联到健康检查。
url-maps(负载均衡器) 这是一个实际的占位符,用于表示关联后端服务和目标 HTTP(S)代理的 L7 负载均衡器
目标 HTTP(S)代理 这是一个连接器,它建立了前端转发规则与负载均衡器之间的关系
前端转发规则 将 IP 地址(临时或静态)、端口号与目标 HTTP 代理关联起来
外部 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-servicemy-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

//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),健康检查心跳将来自地址范围130.211.0.0/2235.191.0.0/16

//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 130.211.0.0/22,35.191.0.0/16 --target-tags private --allow tcp:8080  

几分钟后,my-tomcat-backend-service的健康状态将变为正常(1);现在您可以从 Web 浏览器访问负载均衡器。当访问/时,它应该路由到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 操作系统操作,步骤与第九章中描述的完全相同,在 AWS 上的 Kubernetes

Google 容器引擎(GKE)

总的来说,一些 GCP 组件已经在前几节中介绍过。现在,您可以开始在 GCP VM 实例上使用这些组件设置 Kubernetes。您甚至可以使用在第九章中介绍的 kops,在 AWS 上的 Kubernetes也是如此。

然而,GCP 有一个名为 GKE 的托管 Kubernetes 服务。在底层,它使用了一些 GCP 组件,如 VPC、VM 实例、PD、防火墙规则和负载均衡器。

当然,像往常一样,您可以使用kubectl命令来控制 GKE 上的 Kubernetes 集群,该命令包含在 Cloud SDK 中。如果您尚未在您的机器上安装kubectl命令,请输入以下命令通过 Cloud SDK 安装kubectl

//install kubectl command
$ gcloud components install kubectl  

在 GKE 上设置您的第一个 Kubernetes 集群

您可以使用gcloud命令在 GKE 上设置 Kubernetes 集群。它需要指定几个参数来确定一些配置。其中一个重要的参数是网络。您必须指定将部署到哪个 VPC 和子网。虽然 GKE 支持多个区域进行部署,但您需要至少指定一个区域用于 Kubernetes 主节点。这次,它使用以下参数来启动 GKE 集群:

参数 描述
--cluster-version 指定 Kubernetes 版本 1.6.7
--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 --cluster-version 1.6.7 --machine-type f1-micro --num-nodes 3 --network my-custom-network --subnetwork subnet-c --zone asia-northeast1-a --tags private

Creating cluster my-k8s-cluster...done. 
Created [https://container.googleapis.com/v1/projects/devops-with-kubernetes/zones/asia-northeast1-a/clusters/my-k8s-cluster].
kubeconfig entry generated for my-k8s-cluster.
NAME            ZONE               MASTER_VERSION  MASTER_IP      MACHINE_TYPE  NODE_VERSION  NUM_NODES  STATUS
my-k8s-cluster  asia-northeast1-a  1.6.7           35.189.135.13  f1-micro      1.6.7         3          RUNNING

//check node status
$ kubectl get nodes
NAME                                            STATUS    AGE       VERSION
gke-my-k8s-cluster-default-pool-ae180f53-47h5   Ready     1m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-6prb   Ready     1m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-z6l1   Ready     1m        v1.6.7  

请注意,我们指定了--tags private选项,因此 Kubernetes 节点 VM 实例具有private网络标记。因此,它的行为与具有private标记的其他常规 VM 实例相同。因此,您无法从公共互联网进行 ssh,也无法从互联网进行 HTTP。但是,您可以从具有public网络标记的另一个 VM 实例进行 ping 和 ssh。

一旦所有节点准备就绪,让我们访问默认安装的 Kubernetes UI。为此,使用kubectl proxy命令作为代理连接到您的计算机。然后通过代理访问 UI:

//run kubectl proxy on your machine, that will bind to 127.0.0.1:8001
$ kubectl proxy
Starting to serve on 127.0.0.1:8001

//use Web browser on your machine to access to 127.0.0.1:8001/ui/
http://127.0.0.1:8001/ui/

节点池

在启动 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    AGE       VERSION
gke-my-k8s-cluster-default-pool-ae180f53-47h5   Ready     5m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-6prb   Ready     5m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-f8ps   Ready     30s       v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-qzxz   Ready     30s       v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-z6l1   Ready     5m        v1.6.7  

增加节点数量将有助于扩展节点容量。但是,在这种情况下,它仍然使用最小的实例类型(f1-micro,仅具有 0.6 GB 内存)。如果单个容器需要超过 0.6 GB 内存,则可能无法帮助。在这种情况下,您需要进行扩展,这意味着您需要添加更大尺寸的 VM 实例类型。

在这种情况下,您必须将另一组节点池添加到您的集群中。因为在同一个节点池中,所有 VM 实例都配置相同。因此,您无法在同一个节点池中更改实例类型。

因此,添加一个新的节点池,该节点池具有两组新的g1-small(1.7 GB 内存)VM 实例类型到集群中。然后,您可以扩展具有不同硬件配置的 Kubernetes 节点。

默认情况下,您可以在一个区域内创建 VM 实例数量的一些配额限制(例如,在us-west1最多八个 CPU 核心)。如果您希望增加此配额,您必须将您的帐户更改为付费帐户。然后向 GCP 请求配额更改。有关更多详细信息,请阅读来自cloud.google.com/compute/quotascloud.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    AGE       VERSION
gke-my-k8s-cluster-default-pool-ae180f53-47h5     Ready     13m       v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-6prb     Ready     13m       v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-f8ps     Ready     8m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-qzxz     Ready     8m        v1.6.7
gke-my-k8s-cluster-default-pool-ae180f53-z6l1     Ready     13m       v1.6.7
gke-my-k8s-cluster-large-mem-pool-f87dd00d-9v5t   Ready     5m        v1.6.7
gke-my-k8s-cluster-large-mem-pool-f87dd00d-fhpn   Ready     5m        v1.6.7  

现在您的集群中总共有七个 CPU 核心和 6.4GB 内存的容量更大。然而,由于更大的硬件类型,Kubernetes 调度器可能会首先分配部署 pod 到large-mem-pool,因为它有足够的内存容量。

但是,您可能希望保留large-mem-pool节点,以防一个大型应用程序需要大的堆内存大小(例如,Java 应用程序)。因此,您可能希望区分default-poollarge-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
$ kubectl get pods nginx -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP           NODE
nginx     1/1       Running   0          7s        10.56.1.13   gke-my-k8s-cluster-default-pool-ae180f53-6prb

如果您想指定一个特定的标签而不是beta.kubernetes.io/instance-type,请使用--node-labels选项创建一个节点池。这将为节点池分配您所需的标签。

有关更多详细信息,请阅读以下在线文档:

cloud.google.com/sdk/gcloud/reference/container/node-pools/create

当然,如果您不再需要它,可以随时删除一个节点池。要做到这一点,请运行以下命令删除default-poolf1-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.6.7
large-mem-pool  g1-small      100           1.6.7

//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    AGE       VERSION
gke-my-k8s-cluster-large-mem-pool-f87dd00d-9v5t   Ready     16m       v1.6.7
gke-my-k8s-cluster-large-mem-pool-f87dd00d-fhpn   Ready     16m       v1.6.7  

您可能已经注意到,所有前面的操作都发生在一个单一区域(asia-northeast1-a)中。因此,如果asia-northeast1-a区域发生故障,您的集群将会宕机。为了避免区域故障,您可以考虑设置一个多区域集群。

多区域集群

GKE 支持多区域集群,允许您在多个区域上启动 Kubernetes 节点,但限制在同一地区内。在之前的示例中,它只在asia-northeast1-a上进行了配置,因此让我们重新配置一个集群,其中包括asia-northeast1-aasia-northeast1-basia-northeast1-c,总共三个区域。

非常简单;在创建新集群时,只需添加一个--additional-zones参数。

截至 2017 年 8 月,有一个测试功能支持将现有集群从单个区域升级到多个区域。使用以下测试命令:

$ gcloud beta container clusters update my-k8s-cluster --additional-zones=asia-northeast1-b,asia-northeast1-c

要将现有集群更改为多区域,可能需要安装额外的 SDK 工具,但不在 SLA 范围内。

让我们删除之前的集群,并使用--additional-zones选项创建一个新的集群:

//delete cluster first
$ gcloud container clusters delete my-k8s-cluster --zone asia-northeast1-a

//create a new cluster with --additional-zones option but 2 nodes only
$ gcloud container clusters create my-k8s-cluster --cluster-version 1.6.7 --machine-type f1-micro --num-nodes 2 --network my-custom-network --subnetwork subnet-c --zone asia-northeast1-a --tags private --additional-zones asia-northeast1-b,asia-northeast1-c  

在此示例中,它将在每个区域(asia-northeast1-abc)创建两个节点;因此,总共将添加六个节点:

$ kubectl get nodes
NAME                                            STATUS    AGE       VERSION
gke-my-k8s-cluster-default-pool-0c4fcdf3-3n6d   Ready     44s       v1.6.7
gke-my-k8s-cluster-default-pool-0c4fcdf3-dtjj   Ready     48s       v1.6.7
gke-my-k8s-cluster-default-pool-2407af06-5d28   Ready     41s       v1.6.7
gke-my-k8s-cluster-default-pool-2407af06-tnpj   Ready     45s       v1.6.7
gke-my-k8s-cluster-default-pool-4c20ec6b-395h   Ready     49s       v1.6.7
gke-my-k8s-cluster-default-pool-4c20ec6b-rrvz   Ready     49s       v1.6.7  

您还可以通过 Kubernetes 标签failure-domain.beta.kubernetes.io/zone区分节点区域,以便指定要部署 Pod 的所需区域。

集群升级

一旦开始管理 Kubernetes,您可能会在升级 Kubernetes 集群时遇到一些困难。因为 Kubernetes 项目非常积极,大约每三个月就会有一个新版本发布,例如 1.6.0(2017 年 3 月 28 日发布)到 1.7.0(2017 年 6 月 29 日发布)。

GKE 还会及时添加新版本支持。它允许我们通过gcloud命令升级主节点和节点。您可以运行以下命令查看 GKE 支持的 Kubernetes 版本:

$ gcloud container get-server-config

Fetching server config for us-east4-b
defaultClusterVersion: 1.6.7
defaultImageType: COS
validImageTypes:
- CONTAINER_VM
- COS
- UBUNTU
validMasterVersions:
- 1.7.3
- 1.6.8
- 1.6.7
validNodeVersions:
- 1.7.3
- 1.7.2
- 1.7.1
- 1.6.8
- 1.6.7
- 1.6.6
- 1.6.4
- 1.5.7
- 1.4.9  

因此,您可能会看到此时主节点和节点上支持的最新版本都是 1.7.3。由于之前的示例安装的是 1.6.7 版本,让我们升级到 1.7.3。首先,您需要先升级主节点:

//upgrade master using --master option
$ gcloud container clusters upgrade my-k8s-cluster --zone asia-northeast1-a --cluster-version 1.7.3 --master
Master of cluster [my-k8s-cluster] will be upgraded from version 
[1.6.7] to version [1.7.3]. 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 upgrade has been successfully to done
$ gcloud container clusters list --zone asia-northeast1-a
NAME            ZONE               MASTER_VERSION  MASTER_IP       MACHINE_TYPE  NODE_VERSION  NUM_NODES  STATUS
my-k8s-cluster  asia-northeast1-a  1.7.3           35.189.141.251  f1-micro      1.6.7 *       6          RUNNING  

现在您可以将所有节点升级到 1.7.3 版本。因为 GKE 尝试执行滚动升级,它将按照以下步骤逐个节点执行:

  1. 从集群中注销目标节点。

  2. 删除旧的 VM 实例。

  3. 提供一个新的 VM 实例。

  4. 设置节点为 1.7.3 版本。

  5. 注册到主节点。

因此,它比主节点升级需要更长的时间:

//node upgrade (not specify --master)
$ gcloud container clusters upgrade my-k8s-cluster --zone asia-northeast1-a --cluster-version 1.7.3 
All nodes (6 nodes) of cluster [my-k8s-cluster] will be upgraded from 
version [1.6.7] to version [1.7.3]. 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.7.3,一个节点正在升级,三个节点处于挂起状态):

NAME                                            STATUS                        AGE       VERSION
gke-my-k8s-cluster-default-pool-0c4fcdf3-3n6d   Ready                         37m       v1.6.7
gke-my-k8s-cluster-default-pool-0c4fcdf3-dtjj   Ready                         37m       v1.6.7
gke-my-k8s-cluster-default-pool-2407af06-5d28   NotReady,SchedulingDisabled   37m       v1.6.7
gke-my-k8s-cluster-default-pool-2407af06-tnpj   Ready                         37m       v1.6.7
gke-my-k8s-cluster-default-pool-4c20ec6b-395h   Ready                         5m        v1.7.3
gke-my-k8s-cluster-default-pool-4c20ec6b-rrvz   Ready                         1m        v1.7.3  

Kubernetes 云提供商

GKE 还集成了 Kubernetes 云提供商,可以深度整合到 GCP 基础设施;例如通过 VPC 路由的覆盖网络,通过持久磁盘的 StorageClass,以及通过 L4 负载均衡器的服务。最好的部分是通过 L7 负载均衡器的入口。让我们看看它是如何工作的。

StorageClass

与 AWS 上的 kops 一样,GKE 也默认设置了 StorageClass,使用持久磁盘:

$ 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
Events:           <none>  

因此,在创建持久卷索赔时,它将自动将 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 服务提供支持。只需将Service.spec.type指定为 LoadBalancer,然后 GKE 将自动设置和配置 L4 负载均衡器。

请注意,L4 负载均衡器到 Kubernetes 节点之间的相应防火墙规则可以由云提供商自动创建。如果您想快速将应用程序暴露给互联网,这种方法简单但足够强大:

$ cat grafana.yml 
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: grafana
spec:
 replicas: 1
 template:
 metadata:
 labels:
 run: 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:
 run: grafana

//deploy grafana with Load Balancer service
$ kubectl create -f grafana.yml 
deployment "grafana" created
service "grafana" created

//check L4 Load balancer IP address
$ kubectl get svc grafana
NAME      CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
grafana   10.59.249.34   35.189.128.32   80:30584/TCP   5m

//can reach via GCP L4 Load Balancer
$ curl -I 35.189.128.32
HTTP/1.1 302 Found
Location: /login
Set-Cookie: grafana_sess=f92407d7b266aab8; Path=/; HttpOnly
Set-Cookie: redirect_to=%252F; Path=/
Date: Wed, 30 Aug 2017 07:05:20 GMT
Content-Type: text/plain; charset=utf-8  

L7 负载均衡器(入口)

GKE 还支持 Kubernetes 入口,可以设置 GCP L7 负载均衡器,根据 URL 将 HTTP 请求分发到目标服务。您只需要设置一个或多个 NodePort 服务,然后创建入口规则指向服务。在幕后,Kubernetes 会自动创建和配置防火墙规则、健康检查、后端服务、转发规则和 URL 映射。

首先,让我们创建相同的示例,使用 nginx 和 Tomcat 部署到 Kubernetes 集群。这些示例使用绑定到 NodePort 而不是 LoadBalancer 的 Kubernetes 服务:

此时,您无法访问服务,因为还没有防火墙规则允许从互联网访问 Kubernetes 节点。因此,让我们创建指向这些服务的 Kubernetes 入口。

您可以使用kubectl port-forward <pod name> <your machine available port><: service port number>通过 Kubernetes API 服务器访问。对于前面的情况,请使用kubectl port-forward tomcat-670632475-l6h8q 10080:8080.

之后,打开您的 Web 浏览器到http://localhost:10080/,然后您可以直接访问 Tomcat pod。

Kubernetes Ingress 定义与 GCP 后端服务定义非常相似,因为它需要指定 URL 路径、Kubernetes 服务名称和服务端口号的组合。因此,在这种情况下,URL //* 指向 nginx 服务,URL /examples/examples/* 指向 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  

大约需要 10 分钟来完全配置 GCP 组件,如健康检查、转发规则、后端服务和 URL 映射:

$ 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 示例页面。

在前面的步骤中,我们为 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 地址访问入口,如http://35.186.227.252/(nginx)和http://35.186.227.252/examples/(Tomcat)。

摘要

在本章中,我们讨论了 Google Cloud Platform。基本概念类似于 AWS,但一些政策和概念是不同的。特别是 Google Container Engine,作为一个非常强大的服务,可以将 Kubernetes 用作生产级别。Kubernetes 集群和节点管理非常容易,不仅安装,还有升级。云提供商也完全集成到 GCP 中,特别是 Ingress,因为它可以通过一个命令配置 L7 负载均衡器。因此,如果您计划在公共云上使用 Kubernetes,强烈建议尝试 GKE。

下一章将提供一些新功能和替代服务的预览,以对抗 Kubernetes。

第十一章:接下来是什么

到目前为止,我们已经讨论了在 Kubernetes 上进行 DevOps 任务的各种主题。然而,在实际情况下实施知识总是具有挑战性,因此您可能会想知道 Kubernetes 是否能够解决您目前面临的特定问题。在本章中,我们将学习以下主题来解决挑战:

  • 高级 Kubernetes 特性

  • Kubernetes 社区

  • 其他容器编排框架

探索 Kubernetes 的可能性

Kubernetes 每天都在不断发展,以每季度发布一个主要版本的速度发展。除了每个新 Kubernetes 发行版都带有的内置功能之外,社区的贡献也在生态系统中发挥着重要作用,我们将在本节中对它们进行一番探讨。

精通 Kubernetes

Kubernetes 的对象和资源分为三个 API 跟踪,即 alpha、beta 和 stable,以表示它们的成熟度。每个资源开头的apiVersion字段表示它们的级别。如果一个功能有类似 v1alpha1 的版本,它属于 alpha 级 API,beta API 以相同的方式命名。alpha 级 API 默认禁用,并且可能会在不通知的情况下发生变化。

beta 级别的 API 默认启用;经过充分测试,被认为是稳定的,但模式或对象语义也可能会发生变化。其余部分是稳定的,通常可用的部分。一旦 API 进入稳定阶段,就不太可能再发生变化。

尽管我们已经广泛讨论了 Kubernetes 的概念和实践,但仍然有许多重要特性尚未提及,涉及各种工作负载和场景,使 Kubernetes 变得非常灵活。它们可能适用于某些人的需求,但在特定情况下并不稳定。让我们简要地看一下流行的特性。

作业和定时作业

它们还是高级的 Pod 控制器,允许我们运行最终会终止的容器。作业确保一定数量的 Pod 成功完成运行;CronJob 确保在指定时间调用作业。如果我们需要运行批量工作负载或定时任务,我们会知道内置控制器会发挥作用。相关信息可以在以下网址找到:kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/

Pod 和节点之间的亲和性和反亲和性

我们知道可以使用节点选择器手动将 Pod 分配给某些节点,并且节点可以拒绝带有污点的 Pod。然而,在更灵活的情况下,也许我们希望一些 Pod 共存,或者我们希望 Pod 在可用性区域之间均匀分布,通过节点选择器或节点污点来安排我们的 Pod 可能需要很大的努力。因此,亲和性旨在解决这种情况:kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity

Pod 的自动扩展

几乎所有现代基础设施都支持自动扩展运行应用程序的实例组,Kubernetes 也是如此。Pod 水平扩展器(PodHorizontalScaler)能够根据 CPU/内存指标在诸如 Deployment 的控制器中扩展 Pod 副本。从 Kubernetes 1.6 开始,该扩展器正式支持基于自定义指标(例如每秒事务数)的扩展。更多信息可以在kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/找到。

预防和减轻 Pod 中断

我们知道 Pod 是不稳定的,它们会在集群的扩展和收缩过程中在节点之间被终止和重新启动。如果一个应用程序的太多 Pod 同时被销毁,可能会导致服务水平下降甚至应用程序失败。特别是当应用程序是有状态的或基于法定人数的时候,它可能几乎无法容忍 Pod 的中断。为了减轻中断,我们可以利用PodDisruptionBudget来告知 Kubernetes 我们的应用程序在任何给定时间内可以容忍多少个不可用的 Pod,以便 Kubernetes 能够在了解其上的应用程序的情况下采取适当的行动。有关更多信息,请参阅kubernetes.io/docs/concepts/workloads/pods/disruptions/

另一方面,由于PodDisruptionBudget是一个受管理的对象,它仍然无法排除 Kubernetes 之外的因素引起的中断,例如节点的硬件故障,或者由于内存不足而被系统杀死的节点组件。因此,我们可以将诸如 node-problem-detector 之类的工具纳入我们的监控堆栈,并适当配置节点资源的阈值,以通知 Kubernetes 开始排空节点或驱逐过多的 Pod,以防止情况恶化。有关 node-problem-detector 和资源阈值的更详细指南,请参阅以下主题:

Kubernetes 联邦

联邦是一组集群。换句话说,它由多个 Kubernetes 集群组成,并且可以从单个控制平面访问。在联邦上创建的资源将在所有连接的集群中同步。截至 Kubernetes 1.7,可以联邦的资源包括 Namespace、ConfigMap、Secret、Deployment、DaemonSet、Service 和 Ingress。

联邦的能力为我们构建混合平台带来了另一个灵活性水平,例如,我们可以将部署在本地数据中心和各种公共云中的集群联合起来,通过成本分配工作负载,并利用特定于平台的功能,同时保持灵活性以便移动。另一个典型的用例是将分散在不同地理位置的集群联合起来,以降低全球客户的边缘延迟。此外,支持 5,000 个节点的单个 Kubernetes 集群(在版本 1.6 上)可以保持 API 响应时间的 p99 小于 1 秒。如果需要具有数千个节点或更多的集群,我们可以通过联合集群来实现。

联邦指南可以在以下链接找到:kubernetes.io/docs/tasks/federation/set-up-cluster-federation-kubefed/

集群附加组件

集群附加组件是旨在增强 Kubernetes 集群的程序,被认为是 Kubernetes 的固有部分。例如,我们在第六章中使用的 Heapster,监控和日志,是其中一个附加组件,我们之前提到的 node-problem-detector 也是如此。

由于集群附加组件可能用于一些关键功能,一些托管的 Kubernetes 服务(如 GKE)部署了附加组件管理器,以保护附加组件的状态不被修改或删除。托管的附加组件将在 pod 控制器上部署一个标签addonmanager.kubernetes.io/mode。如果模式是Reconcile,对规范的任何修改都将回滚到其初始状态;EnsureExists模式只检查控制器是否存在,但不检查其规范是否被修改。例如,默认情况下,在 1.7.3 GKE 集群上部署以下部署,并且它们都受到Reconcile模式的保护:

如果您想在自己的集群中部署附加组件,可以在以下链接找到它们:github.com/kubernetes/kubernetes/tree/master/cluster/addons

Kubernetes 和社区

在选择要使用的开源工具时,我们肯定会想知道在开始使用后它的支持性如何。支持性包括项目的领导者是谁,项目是否持有意见,项目的受欢迎程度等因素。

Kubernetes 起源于 Google,现在由Cloud Native Computing FoundationCNCFwww.cncf.io)支持。在 Kubernetes 1.0 发布时,Google 与 Linux 基金会合作成立了 CNCF,并捐赠了 Kubernetes 作为种子项目。CNCF 旨在推动容器化、动态编排和面向微服务的应用程序的发展。

由于 CNCF 下的所有项目都是基于容器的,它们肯定可以与 Kubernetes 无缝配合。Prometheus、Fluentd 和 OpenTracing,我们在第六章中展示和提到的监控和日志,都是 CNCF 的成员项目。

Kubernetes 孵化器

Kubernetes 孵化器是支持 Kubernetes 项目的过程:

github.com/kubernetes/community/blob/master/incubator.md.

毕业项目可能成为 Kubernetes 的核心功能、集群附加组件,或者是 Kubernetes 的独立工具。在整本书中,我们已经看到并使用了许多这样的项目,包括 Heapster、cAdvisor、仪表板、minikube、kops、kube-state-metrics 和 kube-problem-detector,所有这些都让 Kubernetes 变得更好。您可以在 Kubernetes(github.com/kubernetes)或孵化器(github.com/kubernetes-incubator)下探索这些项目。

Helm 和图表

Helm(github.com/kubernetes/helm)是一个软件包管理器,简化了在 Kubernetes 上运行软件的 day-0 到 day-n 操作。它也是孵化器中的一个毕业项目。

正如我们在第七章中学到的,持续交付,将容器化软件部署到 Kubernetes 基本上就是编写清单。尽管如此,一个应用可能由数十个 Kubernetes 资源构建而成。如果我们要多次部署这样的应用程序,重命名冲突部分的任务可能会很繁琐。如果我们引入模板引擎的概念来解决重命名的困扰,我们很快就会意识到我们应该有一个地方来存储模板以及渲染后的清单。因此,Helm 旨在解决这些烦人的琐事。

Helm 中的一个包被称为 chart,它是运行应用程序的配置、定义和清单的集合。社区贡献的图表发布在这里:github.com/kubernetes/charts。即使我们不打算使用它,我们仍然可以在那里找到特定包的经过验证的清单。

使用 Helm 非常简单。首先通过运行官方安装脚本获取 Helm:raw.githubusercontent.com/kubernetes/helm/master/scripts/get

在获取 Helm 二进制文件后,它会获取我们的 kubectl 配置以连接到集群。我们需要在 Kubernetes 集群中有一个名为Tiller的管理器来管理 Helm 的每个部署任务:

$ helm init
$HELM_HOME has been configured at /Users/myuser/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!  

如果我们想要在不将 Tiller 安装到我们的 Kubernetes 集群中的情况下初始化 Helm 客户端,我们可以在helm init中添加--client-only标志。此外,一起使用--skip-refresh标志可以让我们离线初始化客户端。

Helm 客户端能够从命令行搜索可用的图表:

$ helm search
NAME                          VERSION     DESCRIPTION
stable/aws-cluster-autoscaler 0.2.1       Scales worker nodes within autoscaling groups.
stable/chaoskube              0.5.0       Chaoskube periodically kills random pods in you...
...
stable/uchiwa                 0.2.1       Dashboard for the Sensu monitoring framework
stable/wordpress              0.6.3       Web publishing platform for building blogs and ...  

让我们从存储库安装一个图表,比如最后一个wordpress

$ helm install stable/wordpress
NAME:   plinking-billygoat
LAST DEPLOYED: Wed Sep  6 01:09:20 2017
NAMESPACE: default
STATUS: DEPLOYED
...  

在 Helm 中部署的图表被称为发布。在这里,我们有一个名为plinking-billygoat的发布已安装。一旦 pod 和服务准备就绪,我们就可以连接到我们的站点并检查结果。

释放发布也只需要一行命令:

$ helm delete plinking-billygoat
release "plinking-billygoat" deleted 

Helm 利用 ConfigMap 来存储发布的元数据,但使用helm delete删除发布不会删除其元数据。要完全清除这些元数据,我们可以手动删除这些 ConfigMaps,或者在执行helm delete时添加--purge标志。

除了在我们的集群中管理软件包,Helm 带来的另一个价值是它被确立为共享软件包的标准,因此它允许我们直接安装流行的软件,比如我们安装的 Wordpress,而不是为我们使用的每个软件重写清单。

朝着未来基础设施的发展趋势。

很难判断一个工具是否合适,特别是在选择一个支撑业务任务的集群管理软件时,因为每个人面临的困难和挑战都不同。除了性能、稳定性、可用性、可扩展性和可用性等客观问题外,实际情况也占据了决策的重要部分。例如,选择一个堆栈来开发全新项目和在庞大的传统系统之上构建额外层次的观点可能是不同的。同样,由高度内聚的 DevOps 团队和以老式方式工作的组织来操作服务也可能导致不同的选择。

除了 Kubernetes,还有其他平台也具有容器编排功能,并且它们都提供了一些简单的入门方式。让我们退一步,对它们进行概述,找出最合适的选择。

Docker swarm 模式

Swarm 模式(docs.docker.com/engine/swarm/)是自 Docker 1.12 版本以来集成在 Docker 引擎中的本地编排器。因此,它与 Docker 本身共享相同的 API 和用户界面,包括使用 Docker Compose 文件。这种程度的集成被认为是优势,也被认为是劣势,取决于一个人是否习惯于在一个堆栈上工作,其中所有组件都来自同一个供应商。

一个 swarm 集群由管理者和工作者组成,其中管理者是共识组的一部分,用于维护集群的状态并保持高可用性。启用 swarm 模式非常容易。大致上,这里只有两个步骤:使用docker swarm init创建一个集群,然后使用docker swarm join加入其他管理者和工作者。此外,Docker 提供的 Docker Cloud(cloud.docker.com/swarm)帮助我们在各种云提供商上引导一个 swam 集群。

Swarm 模式带来的功能是我们在容器平台中所期望的,也就是说,容器生命周期管理,两种调度策略(复制和全局,分别类似于 Kubernetes 中的部署和 DaemonSet),服务发现,秘密管理等。还有一个类似于 Kubernetes 中 NodePort 类型服务的入口网络,但如果我们需要 L7 层负载均衡器,我们将不得不启动类似 nginx 或 Traefik 的东西。

总的来说,Swarm 模式提供了一个选项,可以在开始使用 Docker 后立即使用的编排容器化应用程序。与此同时,由于它与 Docker 使用相同的语言和简单的架构,它也被认为是所有选择中最简单的平台。因此,选择 Swarm 模式来快速完成某些工作是合理的。然而,它的简单性有时会导致缺乏灵活性。例如,在 Kubernetes 中,我们可以通过简单地操作选择器和标签来使用蓝/绿部署策略,但在 Swarm 模式中没有简单的方法来实现这一点。由于 Swarm 模式仍在积极开发中,例如在 17.06 版本中引入了存储配置数据的功能,这类似于 Kubernetes 中的 ConfigMap,我们确实可以期待 Swarm 模式在保持简单性的同时在未来变得更加强大。

亚马逊 EC2 容器服务

EC2 容器服务(ECS,aws.amazon.com/ecs/)是 AWS 对 Docker 激增的回应。与谷歌云平台和微软 Azure 提供的开源集群管理器(如 Kubernetes,Docker Swarm 和 DC/OS)不同,AWS 坚持采用自己的方式来满足容器服务的需求。

ECS 将 Docker 作为其容器运行时,并且还接受语法版本 2 的 Docker Compose 文件。此外,ECS 和 Docker Swarm 模式的术语基本相同,比如任务和服务的概念。然而,相似之处止步于此。尽管 ECS 的核心功能简单甚至基本,作为 AWS 的一部分,ECS 充分利用其他 AWS 产品来增强自身,比如用于容器网络的 VPC,用于监控和日志记录的 CloudWatch 和 CloudWatch Logs,用于服务发现的 Application LoadBalancer 和 Network LoadBalancer 与 Target Groups,用于基于 DNS 的服务发现的 Lambda 与 Route 53,用于 CronJob 的 CloudWatch Events,用于数据持久性的 EBS 和 EFS,用于 Docker 注册表的 ECR,用于存储配置文件和秘密的 Parameter Store 和 KMS,用于 CI/CD 的 CodePipeline 等等。还有另一个 AWS 产品,AWS Batch(aws.amazon.com/batch/),它是建立在 ECS 之上用于处理批处理工作负载的。此外,AWS ECS 团队的开源工具 Blox(blox.github.io)增强了定制调度的能力,这些能力在 ECS 中没有提供,比如类似 DaemonSet 的策略,通过将 AWS 产品进行耦合。从另一个角度来看,如果我们将 AWS 作为一个整体来评估 ECS,它确实非常强大。

建立 ECS 集群很容易:通过 AWS 控制台或 API 创建 ECS 集群,并使用 ECS 代理将 EC2 节点加入集群。好处在于,主控端由 AWS 管理,因此我们无需时刻警惕主控端。

总的来说,ECS 很容易上手,特别是对于熟悉 Docker 和 AWS 产品的人来说。另一方面,如果我们对目前提供的基本功能不满意,我们必须通过其他 AWS 服务或第三方解决方案来完成一些手工工作,这可能会导致在这些服务上产生不必要的成本,并且需要配置和维护来确保每个组件能够良好地协同工作。此外,ECS 仅在 AWS 上可用,这也可能是人们认真对待它的一个关注点。

Apache Mesos

Mesos(mesos.apache.org/))在 Docker 引发容器潮流之前就已经存在,其目标是解决集群中资源管理的困难,同时支持各种工作负载。为了构建这样一个通用平台,Mesos 利用两层架构来划分资源分配和任务执行。因此,执行部分理论上可以扩展到任何类型的任务,包括编排 Docker 容器。

尽管我们在这里只谈到了 Mesos 这个名字,但实际上它基本上负责一层工作,执行部分是由其他组件称为 Mesos 框架来完成的。例如,Marathon(mesosphere.github.io/marathon/)和 Chronos(mesos.github.io/chronos/)是两个流行的框架,分别用于部署长时间运行和批处理任务,并且两者都支持 Docker 容器。因此,当提到 Mesos 这个术语时,它指的是一个堆栈,比如 Mesos/Marathon/Chronos 或 Mesos/Aurora。事实上,在 Mesos 的两层架构下,也可以将 Kubernetes 作为 Mesos 框架来运行。

坦率地说,一个组织良好的 Mesos 堆栈和 Kubernetes 在能力方面基本上是一样的,只是 Kubernetes 要求在其上运行的一切都应该是容器化的,无论是 Docker、rkt 还是虚拟化容器。另一方面,由于 Mesos 专注于其通用调度,并倾向于保持其核心精简,一些基本功能需要单独安装、测试和操作,这可能需要额外的努力。

Mesosphere 发布的 DC/OS(dcos.io/)利用 Mesos 构建了一个全栈集群管理平台,这在功能上更类似于 Kubernetes。作为建立在 Mesos 之上的每个解决方案的一站式商店,它捆绑了一些组件来驱动整个系统,Marathon 用于常见工作负载,Metronome 用于定期作业,Mesos-DNS 用于服务发现等等。尽管这些构建块似乎很复杂,但 DC/OS 通过 CloudFormation/Terraform 模板和其软件包管理系统 Mesosphere Universe 大大简化了安装和配置工作。自 DC/OS 1.10 以来,Kubernetes 已正式集成到 DC/OS 中,并可以通过 Universe 安装。托管的 DC/OS 也可在一些云提供商上使用,如 Microsoft Azure。

以下截图是 DC/OS 的 Web 控制台界面,汇总了来自每个组件的信息:

到目前为止,我们已经讨论了 DC/OS 的社区版本,但一些功能仅在企业版中可用。它们主要涉及安全性和合规性,列表可以在mesosphere.com/pricing/找到。

摘要

在本章中,我们简要讨论了适用于特定更具体用例的 Kubernetes 功能,并指导了如何利用强大的社区,包括 Kubernetes 孵化器和软件包管理器 Helm。

最后,我们回到起点,概述了实现相同目标的其他三种流行替代方案:编排容器,以便让您选择下一代基础架构的结论留在您的脑海中。

posted @ 2024-05-06 18:35  绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报