Kubernetes-AWS-手册-全-

Kubernetes AWS 手册(全)

原文:zh.annas-archive.org/md5/9CADC322D770A4D3AD0027E7CB5CC592

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Docker 容器承诺彻底改变开发人员和运维在云上构建、部署和管理应用程序的方式。Kubernetes 提供了您在生产环境中实现这一承诺所需的编排工具。

《Kubernetes on AWS》指导您在Amazon Web ServicesAWS)平台上部署一个生产就绪的 Kubernetes 集群。您将了解如何使用 Kubernetes 的强大功能,它是最快增长的生产容器编排平台之一,用于管理和更新您的应用程序。Kubernetes 正在成为云原生应用程序生产级部署的首选。本书从最基本的原理开始介绍 Kubernetes。您将首先学习 Kubernetes 的强大抽象——pod 和 service,这使得管理容器部署变得容易。接着,您将通过在 AWS 上设置一个生产就绪的 Kubernetes 集群的指导之旅,同时学习成功部署和管理自己应用程序的技术。

通过本书,您将在 AWS 上获得丰富的 Kubernetes 实践经验。您还将学习到一些关于部署和管理应用程序、保持集群和应用程序安全以及确保整个系统可靠且具有容错性的技巧。

本书适合对象

如果您是云工程师、云解决方案提供商、系统管理员、网站可靠性工程师或对 DevOps 感兴趣的开发人员,并且希望了解在 AWS 环境中运行 Kubernetes 的详尽指南,那么本书适合您。虽然不需要对 Kubernetes 有任何先前的了解,但具有 Linux 和 Docker 容器的一些经验将是一个优势。

本书涵盖内容

第一章,Google 的基础设施适用于我们其余人,帮助您了解 Kubernetes 如何为您提供谷歌的可靠性工程师使用的一些超能力,以确保谷歌的服务具有容错性、可靠性和高效性。

第二章,启动引擎,帮助您开始使用 Kubernetes。您将学习如何在自己的工作站上启动适用于学习和开发的集群,并开始学习如何使用 Kubernetes 本身。

第三章,《触及云端》,教您如何从头开始构建在 AWS 上运行的 Kubernetes 集群。

第四章,《管理应用程序中的更改》,深入探讨了 Kubernetes 提供的工具,用于管理在集群上运行的 Pod。

第五章,《使用 Helm 管理复杂应用程序》,教您如何使用社区维护的图表将服务部署到您的集群。

第六章,《生产规划》,让您了解在决定在生产环境中运行 Kubernetes 时可以做出的多种不同选择和决策。

第七章,《生产就绪集群》,帮助您构建一个完全功能的集群,这将作为一个基本配置,用于许多不同的用例。

第八章,《抱歉,我的应用程序吃掉了集群》,深入探讨了使用不同的服务质量配置 Pod,以便重要的工作负载能够保证它们所需的资源,但不太重要的工作负载可以在有空闲资源时利用专用资源而无需专门的资源。

第九章,《存储状态》,全都是关于使用 Kubernetes 与 AWS 原生存储解决方案弹性块存储(EBS)的深度集成。

第十章,《管理容器镜像》,帮助您了解如何利用 AWS 弹性容器注册表(ECR)服务以满足所有这些需求存储您的容器镜像。

第十一章,《监控和日志记录》,教您如何设置日志管理管道,并将帮助您了解日志的一些潜在问题和潜在问题。到本章结束时,您将已经设置了一个指标和警报系统。有关本章,请参阅www.packtpub.com/sites/default/files/downloads/Monitoring_and_Logging.pdf

第十二章安全最佳实践,教您如何使用 AWS 和 Kubernetes 网络原语管理 Kubernetes 集群的安全网络。 您还将学习如何保护您的主机操作系统。 有关本章,请参阅www.packtpub.com/sites/default/files/downloads/Best_Practices_of_Security.pdf

为了充分利用本书

您需要访问 AWS 帐户以执行本书中给出的示例。

下载示例代码文件

您可以从您在www.packt.com的帐户中下载本书的示例代码文件。 如果您在其他地方购买了本书,可以访问www.packt.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择 SUPPORT 选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Kubernetes-on-AWS。 如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。 快去看看吧!

使用的约定

本书中使用了许多文本约定。

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

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

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

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都以以下方式编写:

$ mkdir css
$ cd css

粗体:表示一个新术语,一个重要词或者屏幕上看到的词。例如,菜单或对话框中的词会在文本中显示为这样。这是一个例子:“从管理面板中选择系统信息。”

警告或重要提示会显示为这样。提示和技巧会显示为这样。

第一章:谷歌的基础设施服务于我们其他人

Kubernetes 最初是由谷歌的一些工程师构建的,他们负责谷歌内部的容器调度器 Borg。

学习如何使用 Kubernetes 运行自己的基础设施可以让你拥有一些谷歌的可靠性工程师利用的超能力,以确保谷歌的服务具有弹性、可靠和高效。使用 Kubernetes 可以让你利用谷歌和其他公司工程师通过其大规模积累的知识和专业技能。

你的组织可能永远不需要像谷歌这样的公司那样运营。然而,你会发现,许多在操作数万台机器的公司中开发的工具和技术对于运行规模小得多的组织也是适用的。

虽然一个小团队显然可以手动配置和操作数十台机器,但在更大规模上需要的自动化可以让你的生活更简单,你的软件更可靠。如果以后需要从数十台机器扩展到数百甚至数千台,你会知道你正在使用的工具已经在最恶劣的环境中经过了考验。

Kubernetes 的存在本身就是开源/自由软件运动成功的衡量标准和证明。Kubernetes 最初是一个项目,旨在开源谷歌内部容器编排系统 Borg 背后的思想和研究成果。现在它已经有了自己的生命,大部分代码现在都是由谷歌以外的工程师贡献的。

Kubernetes 的故事不仅仅是谷歌看到开源自己的知识间接地为自己的云业务带来好处,而且也是各种基础工具的开源实现成熟的故事。

Linux 容器在某种形式上已经存在了将近十年,但直到 Docker 项目(2013 年首次开源)使它们成为足够多用户广泛使用和理解。虽然 Docker 本身并没有为底层技术带来任何新的东西,但它的创新在于将已经存在的工具打包成一个简单易用的界面。

Kubernetes 也得益于 etcd 的存在,这是一个基于 Raft 一致性算法的键值存储,也是在 2013 年首次发布,用于构建 CoreOS 正在开发的另一个集群调度工具的基础。对于 Borg,Google 使用了基于非常相似的 Paxos 算法的底层状态存储,使 etcd 成为 Kubernetes 的完美选择。

谷歌准备采取主动措施,创建一个开源实现这些知识的项目,这在那个时候对于他们的工程组织来说是一个巨大的竞争优势,因为 Linux 容器由于 Docker 的影响开始变得更加流行。

Kubernetes、Docker、etcd 和许多其他构成 Linux 容器生态系统的工具都是用 Go 编程语言编写的。Go 提供了构建这些系统所需的所有功能,具有出色的并发支持和内置的优秀网络库。

然而,在我看来,语言本身的简单性使其成为开源基础设施工具的绝佳选择,因为如此广泛的开发人员可以在几个小时内掌握语言的基础知识,并开始对项目做出有生产力的贡献。

如果您对了解 Go 编程语言感兴趣,可以尝试查看tour.golang.org/welcome/1,然后花一个小时查看gobyexample.com

我为什么需要一个 Kubernetes 集群?

Kubernetes 的核心是一个容器调度器,但它是一个更丰富和功能齐全的工具包,具有许多其他功能。可以扩展和增强 Kubernetes 提供的功能,就像 RedHat 的 OpenShift 产品所做的那样。Kubernetes 还允许您通过部署附加工具和服务到您的集群来扩展其核心功能。

以下是内置在 Kubernetes 中的一些关键功能:

  • 自愈: Kubernetes 基于控制器的编排确保容器在失败时重新启动,并在它们所在的节点失败时重新调度。用户定义的健康检查允许用户决定如何以及何时从失败的服务中恢复,以及在这样做时如何引导流量。

  • 服务发现:Kubernetes 从根本上设计为使服务发现变得简单,而无需对应用程序进行修改。您的应用程序的每个实例都有自己的 IP 地址,标准的发现机制,如 DNS 和负载均衡,让您的服务进行通信。

  • 扩展:Kubernetes 可以通过按一下按钮实现水平扩展,并提供自动扩展功能。

  • 部署编排:Kubernetes 不仅帮助您管理运行的应用程序,还具有工具来推出对应用程序及其配置的更改。其灵活性使您可以为自己构建复杂的部署模式,或者使用多个附加工具之一。

  • 存储管理:Kubernetes 内置支持管理云提供商的底层存储技术,如 AWS Elastic Block Store 卷,以及其他标准的网络存储工具,如 NFS。

  • 集群优化:Kubernetes 调度程序会根据工作负载的需求自动将其分配到机器上,从而更好地利用资源。

  • 批量工作负载:除了长时间运行的工作负载,Kubernetes 还可以管理批处理作业,如 CI、批处理处理和定期作业。

容器的根源

询问普通用户 Docker 容器是什么,您可能会得到十几种回答之一。您可能会听到有关轻量级虚拟机的内容,或者这种炙手可热的新颠覆性技术将如何革新计算。实际上,Linux 容器绝对不是一个新概念,也并不像虚拟机那样。

1979 年,Unix 的第 7 版中添加了chroot syscall。调用 chroot 会改变当前运行进程及其子进程的根目录。在所谓的 chroot 监狱中运行程序可以防止其访问指定目录树之外的文件。

chroot 的最初用途之一是用于测试 BSD 构建系统,这是大多数现代 Linux 发行版的软件包构建系统所继承的。通过在干净的 chroot 环境中测试软件包,构建脚本可以检测到缺少的依赖信息。

Chroot 也常用于沙箱化不受信任的进程-例如,在共享 FTP 或 SFTP 服务器上的 shell 进程。专门考虑安全性的系统,例如 Postfix 邮件传输代理,利用 chroot 来隔离管道的各个组件,以防止一个组件的安全问题在系统中蔓延。

Chroot 实际上是一个非常简单的隔离工具,它从未旨在提供对文件系统访问以外的任何安全性或控制。对于提供类似构建工具的文件系统隔离的预期目的来说,它是完美的。但是对于在生产环境中隔离应用程序,我们需要更多的控制。

进入容器

试图理解 Linux 容器是什么可能有点困难。就 Linux 内核而言,根本不存在容器这样的东西。内核具有许多功能,允许对进程进行隔离,但这些功能比我们现在所认为的容器要低级和细粒度得多。诸如 Docker 之类的容器引擎使用两个主要的内核特性来隔离进程:

Cgroups

Cgroups,或者控制组,提供了一个控制一个或一组进程的接口,因此得名。它们允许控制组的资源使用的几个方面。资源利用可以通过限制(例如,限制内存使用)来控制。Cgroups 还允许设置优先级,以便为进程提供更多或更少的时间限制资源,例如 CPU 利用率或 I/O。Cgroups 还可以用于快照(和恢复)运行进程的状态。

命名空间

容器隔离的另一部分是内核命名空间。它们的操作方式与我们使用 chroot 系统调用的方式有些相似,即容器引擎指示内核仅允许进程查看系统资源的特定视图。

与仅限制对文件系统内核的访问不同,命名空间限制对许多不同资源的访问。

每个进程可以分配到一个命名空间,然后只能看到与该命名空间连接的资源。可以命名空间化的资源类型如下:

  • 挂载:挂载命名空间控制对文件系统的访问。

  • 用户:每个命名空间都有自己的用户 ID 集。用户 ID 命名空间是嵌套的,因此高级命名空间中的用户可以映射到低级命名空间中的另一个用户。这就允许容器以 root 身份运行进程,而不会给予该进程对根系统的完全权限。

  • PID:进程 ID 命名空间与用户命名空间一样是嵌套的。这就是为什么主机可以在运行容器的系统上检查进程列表时看到容器内运行的进程。然而,在命名空间内部,数字是不同的;这意味着在 PID 命名空间内创建的第一个进程可以被分配为 PID 1,并且可以继承僵尸进程(如果需要)。

  • 网络:网络命名空间包含一个或多个网络接口。该命名空间拥有自己的私有网络资源,如地址、路由表和防火墙。

还有用于 IPC、UTS 和 Cgroups 接口本身的命名空间。

将这些部分组合在一起

容器引擎(如 Docker 或 rkt 等软件)的工作是将这些部分组合在一起,为我们这些凡人创造出可用和可理解的东西。

虽然一个直接暴露 Cgroups 和命名空间所有细节的系统会非常灵活,但理解和管理起来会更加困难。使用诸如 Docker 之类的系统为我们提供了一个简单易懂的抽象,但必然会为我们做出许多关于这些低级概念如何使用的决定。

Docker 在先前的容器技术上取得的根本突破是采用了良好的默认设置来隔离单个进程,并将它们与允许开发人员提供进程运行所需的所有依赖项的镜像格式相结合。

这是非常好的一件事,因为它允许任何人安装 Docker 并快速理解发生了什么。它还使得这种 Linux 容器成为构建更大更复杂系统(如 Kubernetes)的完美基石。

在这里,安排一下...

在其核心,Kubernetes 是一个将工作调度到一组计算机的系统——一个调度器。但是为什么你需要一个调度器呢?

如果你考虑一下你自己的系统,你会意识到你可能已经有了一个调度器,但除非你已经在使用类似 Kubernetes 的东西,否则它可能看起来会非常不同。

也许你的调度程序是一个团队,有关于数据中心每台服务器上运行的服务的电子表格和文档。也许这个团队会查看过去的流量统计数据,试图猜测未来会有重负载的时间。也许你的调度程序依赖于用户在任何时间通知团队成员,如果你的应用程序停止运行。

这本书讨论了这些问题,讨论了我们如何摆脱手动流程和对系统未来使用的猜测。它是关于利用管理系统的人类的技能和经验,将我们的运营知识编码到可以每秒做出关于你的运行系统的决策的系统中,无缝地响应崩溃的进程、失败的机器和增加的负载,而无需任何人为干预。

Kubernetes 选择将其调度程序建模为控制循环,以便系统不断发现集群的当前状态,将其与期望状态进行比较,然后采取行动来减少期望状态和实际状态之间的差异。这在以下图表中总结如下:

典型的控制循环

能够声明我们希望系统处于的状态,然后让系统自己采取必要的行动来实现这种期望状态,是非常强大的。

您以前可能使用了一种命令式工具或脚本来管理系统,甚至可能使用了手动步骤的书面操作手册。这种方法非常像食谱:你一步一步地采取一系列行动,希望最终达到你所期望的状态。

当描述如何首次安装和引导系统时,这种方法效果很好,但当你需要运行你的脚本来管理已经运行的系统时,你的逻辑需要变得更加复杂,因为对于食谱中的每个阶段,你都需要停下来检查在执行之前需要做什么。

当使用像 Kubernetes 这样的声明性工具来管理系统时,你的配置变得简化,更容易理解。这种方法的一个重要副作用是,如果底层故障导致配置偏离你的期望状态,Kubernetes 将修复你的配置。

通过结合控制循环和声明性配置,Kubernetes 允许您告诉它为您做什么,而不是如何做。Kubernetes 赋予您,操作者,建筑师的角色,而 Kubernetes 则扮演建造者的角色。建筑师向建造者提供了详细的建筑计划,但不需要解释如何用砖和灰浆建造墙壁。您的责任是向 Kubernetes 提供应用程序的规范和所需的资源,但您不需要担心它将在哪里以及如何运行的细节。

Kubernetes 的基础知识

让我们开始了解 Kubernetes,首先看一些大部分 Kubernetes 建立在其上的基本概念。清楚地了解这些核心构建块如何组合在一起将有助于我们探索组成 Kubernetes 的多种功能和工具。

如果您没有对 Kubernetes 有任何经验,那么在没有清楚理解这些核心构建块的情况下使用 Kubernetes 可能会有点困惑,因此,在继续之前,您应该花时间了解这些部分如何组合在一起。

Pod

像一群鲸鱼,或者也许是豌豆荚一样,Kubernetes pod 是一组链接的容器。如下图所示,一个 pod 可以由一个或多个容器组成;通常一个 pod 可能只是一个单一的容器:

Pods 是一个或多个容器的逻辑分组

Kubernetes 调度的每个 pod 都被分配了独特的 IP 地址。网络命名空间(因此 pod 的 IP 地址)被每个 pod 中的每个容器共享。

这意味着方便地一起部署几个密切协作的容器。例如,您可以部署一个反向代理与 Web 应用程序一起,以为不本地支持它们的应用程序添加 SSL 或缓存功能。在下面的示例中,我们通过部署一个典型的 Web 应用程序服务器-例如 Ruby on Rails-以及一个反向代理-例如 NGINX 来实现这一点。这个额外的容器提供了可能不被原生应用程序提供的进一步功能。将功能从较小的隔离容器中组合在一起的这种模式意味着您能够更容易地重用组件,并且可以简单地向现有工具添加额外的功能。设置如下图所示:

通过组合多个容器提供额外的功能

除了共享网络命名空间外,Kubernetes 还允许在一个 pod 中的任意数量的容器之间非常灵活地共享卷挂载。这允许出现多种情况,其中几个组件可以协作执行特定任务。

在这个例子中,我们使用了三个容器来协调为使用 NGINX web 服务器构建的静态网站提供服务。

第一个容器使用 Git 从远程 Git 存储库中拉取和更新源代码。该存储库被克隆到与第二个容器共享的卷中。第二个容器使用 Jekyll 框架构建将由我们的 web 服务器提供的静态文件。Jekyll 监视文件系统上的共享目录的更改,并重新生成需要更新的任何文件。

Jekyll 写入生成文件的目录与运行 NGINX 的容器共享,用于为我们的网站提供 HTTP 请求,如下图所示:

我们在这里使用 Jekyll 作为例子,但是你可以使用许多工具来构建静态网站,比如 Hugo、Hexo 和 Gatsby。像这样将应用程序拆分成单独的容器意味着很容易升级单个组件,甚至尝试替代工具。

共享卷挂载的 pod 的另一个用途是支持使用 Unix 套接字进行通信的应用程序,如下图所示。例如,提取转换加载ETL)系统可以被建模为使用 UNIX 套接字进行通信的几个独立进程。如果您能够利用第三方工具来处理管道的一部分或全部内容,或者在各种情况下重用您为内部使用构建的工具,这可能是有益的:

在这个例子中,一个定制的应用程序用于从网页中抓取数据,并通过共享卷中的 Unix 域套接字与 Fluentd 的实例进行通信。使用第三方工具(如 Fluentd)将数据推送到后端数据存储的模式不仅简化了定制工具的实现,还提供了与 Fluentd 选择支持的任何存储兼容的功能。

Kubernetes 为您提供了一些强有力的保证,即 pod 中的容器具有共享的生命周期。这意味着当您启动一个 pod 时,您可以确保每个容器将被调度到同一节点;这很重要,因为这意味着您可以依赖于 pod 中的其他容器将存在并且将是本地的。Pod 通常是将几个不同容器的功能粘合在一起的便捷方式,从而实现常见组件的重用。例如,您可以使用 sidecar 容器来增强应用程序的网络能力,或提供额外的日志管理或监控设施。

给所有东西贴标签

标签是附加到资源(如 pod)的键值对,旨在包含帮助您识别特定资源的信息。

您可以为您的 pod 添加标签,以标识正在运行的应用程序,以及其他元数据,例如版本号、环境名称或与您的应用程序相关的其他标签。

标签非常灵活,因为 Kubernetes 让您自行决定如何为自己的资源打上标签。

一旦您开始使用 Kubernetes,您将发现几乎可以为您创建的每个资源添加标签。

能够添加反映您自己应用程序架构的标签的强大之处在于,您可以使用选择器使用您为资源指定的任何标签组合来查询资源。这种设置如下图所示:

您可以为在 Kubernetes 中创建的许多资源添加标签,然后使用选择器进行查询。

Kubernetes 不强制执行任何特定的模式或布局,用于给集群中的对象打标签;您可以自由地为应用程序打上标签。但是,如果您想要一些结构,Kubernetes 确实对您可能想要应用于可以组合成逻辑应用程序的对象的标签提出了一些建议。您可以在 Kubernetes 文档中阅读更多信息:kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/

副本集

在 Kubernetes 中,ReplicaSet是一个模板化创建 pod 的资源。副本集的定义包含它创建的 pod 的模板定义,副本的期望数量和用于发现其管理下的 pod 的选择器。

ReplicaSet用于确保始终运行所需数量的 pod。如果与选择器匹配的 pod 数量低于所需数量,则 Kubernetes 将安排另一个。

由于 pod 的生命周期与其运行的节点的生命周期相关,pod 可以被视为短暂的。有许多原因可能导致特定 pod 的生命周期结束。也许它被操作员或自动化流程移除了。Kubernetes 可能已经驱逐了 pod 以更好地利用集群的资源或准备节点进行关闭或重启。或者底层节点可能失败了。

ReplicaSet允许我们通过要求集群确保整个集群中运行正确数量的副本来管理我们的应用程序。这是 Kubernetes 在其许多 API 中采用的一种策略。

作为集群操作员,Kubernetes 会帮助用户减少运行应用程序的复杂性。当我决定需要运行我的应用程序的三个实例时,我不再需要考虑底层基础设施:我只需告诉 Kubernetes 执行我的愿望。如果最坏的情况发生,我的应用程序正在运行的底层机器之一失败,Kubernetes 将知道如何自我修复我的应用程序并启动一个新的 pod。不再需要寻呼机的呼叫,也不需要在半夜里尝试恢复或替换失败的实例。

ReplicaSet取代了您可能在旧教程和文档中了解过的ReplicationController。它们几乎完全相同,但在一些细微的方面有所不同。

通常,我们希望更新集群上运行的软件。因此,我们通常不直接使用ReplicaSet,而是使用Deployment对象来管理它们。在 Kubernetes 中,部署用于优雅地推出ReplicaSet的新版本。您将在第四章中了解更多关于部署的内容,管理应用程序的变更

服务

Kubernetes 为我们管理应用程序提供的最后一个基本工具是服务。服务为我们提供了一种方便的方式,在集群内访问我们的服务,通常被称为服务发现

实际上,服务允许我们定义一个标签选择器来引用一组 pod,然后将其映射到我们的应用程序可以使用的内容,而无需修改以查询 Kubernetes API 来收集这些信息。通常,服务将以轮询的方式提供一个稳定的 IP 地址或 DNS 名称,用于访问它所引用的底层 pod。

通过使用服务,我们的应用程序不需要知道它们正在 Kubernetes 上运行-我们只需要正确地配置它们,使用服务的 DNS 名称或 IP 地址。

服务提供了一种让集群中的其他应用程序发现符合特定标签选择器的 pod 的方法。它通过提供一个稳定的 IP 地址,以及可选的 DNS 名称来实现这一点。这个设置如下图所示:

底层

现在我们已经了解了 Kubernetes 为我们提供的功能,让我们深入一点,看看 Kubernetes 用来实现这些功能的组件。Kubernetes 通过具有微服务架构,使我们更容易查看每个组件的功能在一定程度上的隔离。

在接下来的几章中,我们将亲自部署和配置这些组件。但是现在,让我们通过查看以下图表,对每个组件的功能有一个基本的了解:

主节点上的主要 Kubernetes 组件

API 服务器

API 服务器充当 Kubernetes 的中央枢纽。Kubernetes 中的所有其他组件通过读取、监视和更新 Kubernetes API 中的资源来进行通信。这个中央组件用于访问和操作集群当前状态的信息,允许 Kubernetes 在保持高度一致性的同时扩展和增强新功能。

Kubernetes 使用 etcd 存储集群的当前状态。使用 etcd 存储是因为其设计意味着它既能抵抗故障,又能保证其一致性。然而,组成 Kubernetes 的不同组件从不直接与 etcd 交互;相反,它们与 API 服务器通信。对于我们作为集群的操作者来说,这是一个很好的设计,因为它允许我们将对 etcd 的访问限制在 API 服务器组件,提高安全性并简化管理。

虽然 API 服务器是 Kubernetes 架构中的组件,其他所有组件都与其通信以访问或更新状态,但它本身是无状态的,所有存储都被推迟到后端 etcd 集群。对于我们作为集群操作员来说,这再次是一个理想的设计决策,因为它允许我们部署多个 API 服务器的实例(如果我们希望)以提供高可用性。

控制器管理器

控制器管理器是运行实现 Kubernetes 功能的一些核心功能的核心控制循环(或控制器)的服务。这些控制器中的每一个都通过 API 服务器监视集群的状态,然后进行更改,以尝试将集群的状态移动到期望的状态。控制器管理器的设计意味着一次只能运行一个实例;然而,为了简化在高可用配置中的部署,控制器管理器具有内置的领导者选举功能,因此可以并排部署多个实例,但只有一个实际上会在任何时候执行工作。

调度器

调度器可能是使 Kubernetes 成为有用和实用工具的最重要的组件。它会监视处于未调度状态的新 pod,然后分析集群的当前状态,包括运行的工作负载、可用资源和其他基于策略的问题。然后决定最适合运行该 pod 的位置。与控制器管理器一样,调度器的单个实例一次只能工作一个,但在高可用配置中,可以进行领导者选举。

Kubelet

kubelet 是在每个节点上运行的代理,负责启动 pod。它不直接运行容器,而是控制运行时,比如 Docker 或 rkt。通常,kubelet 会监视 API 服务器,以发现已经在其节点上调度的 pod。

kubelet 在PodSpec级别操作,因此它只知道如何启动 pod。Kubernetes API 中的任何更高级的概念都是由控制器实现的,最终使用特定配置创建或销毁 pod。

kubelet 还运行一个名为cadvisor的工具,它收集有关节点上资源使用情况的指标,并使用节点上运行的每个容器,这些信息可以被 Kubernetes 用来做调度决策。

总结

到目前为止,你应该对构建现代容器编排器(如 Kubernetes)的软件栈有基本的了解。

现在你应该理解以下内容:

  • 容器是建立在 Linux 内核的更低级特性之上的,比如命名空间和 Cgroups。

  • 在 Kubernetes 中,pod 是建立在容器之上的强大抽象。

  • Kubernetes 使用控制循环来构建一个强大的系统,允许操作员以声明方式指定应该运行什么。Kubernetes 会自动采取行动,推动系统朝着这个状态发展。这是 Kubernetes 自我修复特性的来源。

  • Kubernetes 中几乎所有的东西都可以被贴上标签,你应该给你的资源贴上标签,以便更简单地管理它们。

在下一章中,你将通过在工作站上运行一个小集群来获得一些使用 Kubernetes API 的实际经验。

第二章:启动你的引擎

在本章中,我们将迈出 Kubernetes 的第一步。您将学习如何在自己的工作站上启动一个适合学习和开发使用的集群,并开始学习如何使用 Kubernetes 本身。在本章中,我们将做以下事情:

  • 学习如何安装和使用 Minikube 来运行 Kubernetes

  • 构建一个在 Docker 容器中运行的简单应用程序

  • 使用 Kubernetes 来运行简单的应用程序

您自己的 Kubernetes

Minikube是一个工具,可以在您的工作站上轻松运行一个简单的 Kubernetes 集群。它非常有用,因为它允许您在本地测试应用程序和配置,并快速迭代应用程序,而无需访问更大的集群。对于我们的目的来说,它是获得一些实际的 Kubernetes 实践经验的理想工具。安装和配置非常简单,您会发现。

安装

您需要一些工具来在您的工作站上运行 Kubernetes:

  • kubectl是 Kubernetes 命令行界面。在本书中,您将使用它与 Kubernetes 进行交互。

在 Kubernetes 社区中,没有人同意如何发音kubectl

尝试这些不同的方法并选择您喜欢的:

    kube-kuttle
    kube-control
    kube-cee-tee-ell
    kube-cuddle
  • minikube是一个在本地机器上管理 Kubernetes 的命令。它处理所有困难的事情,所以您可以立即开始使用 Kubernetes。

  • dockerminikube虚拟机内部运行着 Docker 守护程序,但如果您想直接与其交互,您可能需要在您的工作站上安装 Docker 命令行。

最好与虚拟机一起使用 Minikube,因为像 macOS 和 Windows 这样的平台不本地支持 Linux 容器,即使在 Linux 上,也有助于保持您的环境干净和隔离。根据您的操作系统,您可以使用各种虚拟化工具与minikube一起使用:

  • VirtualBox:它易于使用,可以安装在 macOS、Windows 和 Linux 上。

  • VMware Fusion:这是 macOS 上可用的商业工具。

  • KVM:这是一个众所周知的 Linux 虚拟化工具。

  • xhyve:这是一个利用 macOS 中的本机虚拟化框架的开源项目。它的性能非常好,但安装和使用可能有点困难。

  • Hyper-V:这是 Windows 的本地虚拟化工具。请记住,您可能需要在您的机器上手动启用它并设置其网络。

在本书中,我们将介绍默认选项 VirtualBox,但如果你经常使用 Minikube,你可能想探索一些其他选项,因为如果设置正确,它们可能更高效和可靠。

你可以在git.k8s.io/minikube/docs/drivers.md找到一些关于不同驱动程序的文档。

macOS

在 Mac 上,安装minikubekubectl的最佳方法是使用 Homebrew 软件包管理器。

macOS 的 Homebrew 软件包管理器是安装开发工具的简单方法。你可以在网站上找到如何安装它:brew.sh/

  1. 首先安装 Kubernetes 命令行客户端kubectl
brew install kubernetes-cli
  1. 接下来,安装minikubevirtualbox
    brew cask install minikube virtualbox

Linux

在 Linux 上,最简单的安装方法是下载并安装预构建的二进制文件:

  1. 你应该下载minikubekubectl的二进制文件:
    curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
    curl -LO https://dl.k8s.io/v1.10.6/bin/linux/amd64/kubectl  
  1. 一旦你下载了二进制文件,将它们设置为可执行,并将它们移动到你的路径中的某个位置:
    chmod +x minikube kubectl
    sudo mv minikube kubectl /usr/local/bin/

在 Linux 上安装 VirtualBox 的方法将取决于你的发行版。

请查看 VirtualBox 网站上的说明:www.virtualbox.org/wiki/Linux_Downloads

Windows

在 Windows 机器上安装 Minikube 与在 Linux 或 macOS 上一样简单。

首先安装 VirtualBox。

你可以从www.virtualbox.org/wiki/Downloads下载 VirtualBox 的 Windows 安装程序。

如果你使用 chocolatey 软件包管理器,请执行以下步骤:

  1. 安装minikube
    C:\> choco install minikube
  1. 安装kubectl
    C:\> choco install kubernetes-cli

如果你不使用 chocolatey,你可以手动安装minikubekubectl

  1. storage.googleapis.com/minikube/releases/latest/minikube-windows-amd64.exe下载minikube并将其重命名为minikube.exe。然后将它移动到你路径上的某个位置。下载kubectldl.k8s.io/v1.10.6/bin/windows/amd64/kubectl.exe,然后将它移动到你路径上的某个位置。

启动 Minikube

一旦你安装好了minikube和你选择的虚拟化工具,我们就可以用它来构建和启动本地 Kubernetes 集群。

如果你选择使用minikube工具的默认设置,那么做起来就很简单。只需运行:

    minikube start  

然后,您应该会看到一些输出,类似于以下内容:

    Starting local Kubernetes v1.10.0 cluster...
    Starting VM...
    Getting VM IP address...
    Moving files into cluster...
    Setting up certs...
    Connecting to cluster...
    Setting up kubeconfig...
    Starting cluster components...
    Kubectl is now configured to use the cluster.

minikube start 有许多选项,可用于配置启动的集群。尝试运行minikube help start 以找出您可以自定义的内容。

您可能想要设置--cpus和/或--memory来自定义您的计算机资源用于 Minikube VM 的使用量。

假设一切都如预期那样进行,那就是了;您应该在本地机器上安装并运行了一个集群。

kubectl配置文件(默认情况下在~/.kube/config中找到)定义了上下文。上下文链接到一个集群和一个用户对象。集群定义了如何。

minikube start命令创建了一个指向 Minikube VM 内运行的 API 服务器的kubectl上下文,并且正确配置了一个允许访问 Kubernetes 的用户。

当您阅读本书时,您当然会想要添加额外的上下文,以便连接到您可能设置的远程集群。您应该能够通过运行以下命令随时切换回minikube上下文,以便使用minikube

    kubectl config use-context minikube

使用 kubectl 的第一步

让我们首先验证kubectl是否确实已配置为正确使用您的集群,并且我们可以连接到它:

    kubectl version

您应该会看到类似于这样的输出:

    Client Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.4", GitCommit:"5ca598b4ba5abb89bb773071ce452e33fb66339d", GitTreeState:"clean", BuildDate:"2018-06-18T14:14:00Z", GoVersion:"go1.9.7", Compiler:"gc", Platform:"darwin/amd64"}
    Server Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.0", GitCommit:"fc32d2f3698e36b93322a3465f63a14e9f0eaead", GitTreeState:"clean", BuildDate:"2018-03-26T16:44:10Z", GoVersion:"go1.9.3", Compiler:"gc", Platform:"linux/amd64"}

您的输出可能显示略有不同的版本号,但是假设您从客户端和服务器都看到了一个版本号,那么您就可以连接到集群。

如果您看不到服务器版本,或者看到其他错误消息,请跳转到本章的Minikube 故障排除部分。

让我们开始使用一些在与集群交互时对我们有用的kubectl命令来与集群进行交互。

我们将要探索的第一个命令是get命令。这使我们能够列出有关集群上资源的基本信息。在这种情况下,我们正在获取所有节点资源的列表:

    kubectl get nodes
    NAME       STATUS    AGE       VERSION
    minikube   Ready    20h       v1.10.0

如您所见,在我们的 Minikube 安装中,这并不是很令人兴奋,因为我们只有一个节点。但是在具有许多节点的较大集群上,能够查看有关所有节点(或某些子集)的信息可能非常有用。

下一个命令将允许我们深入研究并查看有关特定资源的更详细信息。尝试运行以下命令来查看您可以发现有关 Minikube VM 的信息:

    $ kubectl describe node/minikube

随着您在本书中的进展,您将发现能够获取和描述 Kubernetes API 公开的各种资源将成为您的第二天性,无论何时您想要发现集群上发生了什么以及为什么。

在我们继续之前,kubectl还有一个技巧可以帮助我们。尝试运行以下命令,以获取集群上可用的每种资源类型的描述和一些示例:

    kubectl describe -h

在集群内构建 Docker 容器

您可能已经在工作站上安装了 Docker,但是当您在应用程序上工作时,将图像构建在托管 Kubernetes 集群的 Minikube VM 内部运行的 Docker 守护程序上可以改善您的工作流程。这意味着您可以跳过将图像推送到 Docker 仓库,然后在 Kubernetes 中使用它们。您只需要构建和标记您的图像,然后在 Kubernetes 资源中按名称引用它们。

如果您的工作站上已经安装了 Docker,那么您应该已经安装了与 Minikube Docker 守护程序交互所需的命令行客户端。如果没有,安装也很容易,可以通过安装适用于您平台的 Docker 软件包,或者如果您只想要命令行工具,可以下载二进制文件并将其复制到您的路径中。

为了正确配置 Docker CLI 与 minikube VM 内部的 Docker 守护进程通信,minikube 提供了一个命令,将返回环境变量以配置客户端:

    minikube docker-env

在 Mac 或 Linux 上,您可以通过运行正确地将这些变量扩展到当前的 shell 环境中:

    eval $(minikube docker-env)

尝试运行一些docker命令来检查一切是否设置正确:

    docker version

这应该向您显示在 Minikube VM 内运行的 Docker 版本。您可能会注意到,在 Minikube VM 中运行的 Docker 服务器版本略落后于最新版本的 Docker,因为 Kubernetes 需要一些时间来测试新版本的 Docker,以确保稳定性。

尝试列出正在运行的容器。您应该注意到一个正在运行 Kubernetes 仪表板的容器,以及 Kubernetes 启动的一些其他服务,如kube-dnsaddon管理器:

    docker ps

在 Minikube 上构建和启动一个简单的应用程序

让我们迈出第一步,在我们的本地 minikube 集群上构建一个简单的应用程序并让它运行。

我们需要做的第一件事是为我们的应用程序构建一个容器映像。这样做的最简单方法是创建一个 Dockerfile 并使用 docker build 命令。

使用您喜欢的文本编辑器创建一个名为 Dockerfile 的文件,内容如下:

Dockerfile 
FROM nginx:alpine 
RUN echo "<h1>Hello World</h1>" > /usr/share/nginx/html/index.html 

要构建应用程序,首先确保您的 Docker 客户端指向 Minikube VM 内的 Docker 实例,方法是运行:

    eval $(minikube docker-env)

然后使用 Docker 构建映像。在这种情况下,我们给映像打了一个标签 hello,但您可以使用任何您想要的标签:

    docker build -t hello:v1 .

Kubectl 有一个 run 命令,我们可以使用它快速在 Kubernetes 集群上运行一个 pod。在后台,它创建了一个 Kubernetes 部署资源,确保我们的 hello 容器的单个实例在一个 pod 中运行(我们稍后会更多地了解这一点):

    kubectl run hello --image=hello:v1 --image-pull-policy=Never \
    --port=80

我们在这里设置 --image-pull-policy=Never 是为了确保 Kubernetes 使用我们刚刚构建的本地映像,而不是默认从远程存储库(如 Docker Hub)拉取映像。

我们可以使用 kubectl get 来检查我们的容器是否已经正确启动:

    $ kubectl get pods
    NAME                     READY     STATUS    RESTARTS   AGE
    hello-2033763697-9g7cm   1/1       Running   0          1m

我们的 hello world 应用程序设置起来足够简单,但我们需要一些方法来访问它,以便我们的实验被认为是成功的。我们可以使用 kubectl expose 命令来创建一个指向刚刚创建的部署中的 pod 的服务:

    kubectl expose deployment/hello --port=80 --type="NodePort" \
    --name=hello 

在这种情况下,我们已将服务类型设置为 NodePort,这样 Kubernetes 将在 Minikube VM 上公开一个随机端口,以便我们可以轻松访问我们的服务。在第六章中,生产规划,我们将更详细地讨论将应用程序暴露给外部世界的问题。

当您创建一个NodePort类型的服务时,Kubernetes 会自动为我们分配一个端口号,以便服务可以在其上公开。在多节点集群中,此端口将在集群中的每个节点上打开。由于我们只有一个节点,因此找出如何访问集群会简单一些。

首先,我们需要发现 Minikube VM 的 IP 地址。幸运的是,我们可以运行一个简单的命令来获取这些信息:

    minikube ip
    192.168.99.100

很可能当minikube VM 在您的机器上启动时,它被分配了一个与我的不同的 IP 地址,所以请记下您自己机器上的 IP 地址。

接下来,为了发现 Kubernetes 已经在哪个端口上公开了我们的服务,让我们在服务上使用 kubectl get

    $ kubectl get svc/hello
    NAME      CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    hello     10.0.0.104   <nodes>       80:32286/TCP   26m

在这种情况下,您可以看到 Kubernetes 已经将容器上的端口80暴露为节点上的端口32286

现在,您应该能够构建一个 URL,在浏览器中访问该应用程序进行测试。在我的情况下,它是http://192.168.99.100:32286

您应该能够使用浏览器访问您的应用程序

刚刚发生了什么?

到目前为止,我们已经成功在 Minikube 实例上构建、运行和暴露了一个单个容器。如果您习惯使用 Docker 执行类似的任务,您可能会注意到,虽然我们所采取的步骤非常简单,但要使一个简单的 hello world 应用程序运行起来还是有一些复杂性的。

很多这些都与工具的范围有关。Docker 提供了一个简单易用的工作流,用于在单个机器上构建和运行单个容器,而 Kubernetes 首先是一个旨在管理多个节点上运行的多个容器的工具。

为了理解 Kubernetes 即使在这个简单的例子中引入的一些复杂性,我们将探索 Kubernetes 在幕后工作以确保我们的应用程序可靠运行的方式。

当我们执行kubectl run时,Kubernetes 创建了一种新的资源:部署。部署是一个更高级的抽象,代表我们管理的底层ReplicaSet。这样做的好处是,如果我们想对应用程序进行更改,Kubernetes 可以管理向正在运行的应用程序滚动发布新配置:

我们简单的 Hello 应用程序的架构

当我们执行 kubectl expose 时,Kubernetes 创建了一个带有标签选择器的服务,该选择器与我们引用的部署管理的 pod 匹配。

滚动发布更改

部署资源的一个关键功能是管理应用程序的新版本的发布。让我们看一个如何执行这个操作的例子。

首先,让我们更新我们的Hello World应用程序的版本 2 的 Dockerfile:

Dockerfile 
FROM nginx:alpine 
COPY index.html /usr/share/nginx/html/index.html 

您可能已经注意到,我们在版本 1 中使用的 HTML 有点不完整,因此我们在Dockerfile中使用COPY命令将index.html文件复制到我们的容器镜像中。

使用文本编辑器创建一个index.html文件,它在视觉上与版本 1 有所区别。我抓住机会添加了一个合适的 DOCTYPE,并且当然,使用 CSS 重新实现了可悲的已经废弃的闪烁标签!由于这不是一本关于网页设计的书,随意进行任何想要的更改:

index.html 
<!DOCTYPE html> 
<html> 
  <head> 
    <style> 
      blink { animation: blink 1s steps(1) infinite; } 
      @keyframes blink { 50% { color: transparent; } } 
    </style> 
    <title>Hello World</title> 
  </head> 
  <body> 
    <h1>Hello <blink>1994</blink></h1> 
  </body> 
</html> 

接下来,使用 Docker 构建您的第 2 版镜像:

    docker build -t hello:v2 .

现在我们可以使用 kubectl 来更新部署资源以使用新的镜像:

    kubectl set image deployment/hello hello=hello:v2

等待几分钟,直到 Kubernetes 启动新的 pod,然后刷新您的浏览器;您应该能看到您的更改。

当我们更新一个部署时,Kubernetes 在幕后创建一个新的副本集,具有新的配置,并处理新版本的滚动部署。Kubernetes 还会跟踪您部署的不同配置。这也使您有能力在需要时回滚部署:

    $ kubectl rollout undo deployment/hello
    deployment "hello" rolled back

弹性和扩展性

能够提供对底层基础设施中的错误和问题具有弹性的服务是我们可能希望使用 Kubernetes 部署我们的容器化应用程序的关键原因之一。

我们将通过我们的Hello World部署来进行实验,以发现 Kubernetes 如何处理这些问题。

第一个实验是看当我们故意删除包含我们的hello容器的 pod 时会发生什么。

为了做到这一点,我们需要找到这个 pod 的名称,我们可以使用kubectl get命令来做到这一点:

    $ kubectl get pods
    NAME                     READY     STATUS    RESTARTS   AGE
    hello-2473888519-jc6km   1/1       Running   0          7m

在我们的 Minikube 集群中,目前只有一个来自我们迄今为止创建的一个部署的运行中的 pod。一旦开始部署更多的应用程序,诸如 kubectl get 之类的命令的输出就会变得更长。我们可以使用-l标志传递一个标签选择器来过滤结果。在这种情况下,我们将使用kubectl get pods -l run=hello来仅显示标签设置为hello的 pod。

然后我们可以使用kubectl delete命令来删除资源。删除一个 pod 也会终止其中的容器内运行的进程,有效地清理了我们节点上的 Docker 环境:

    $ kubectl delete pod/hello-2473888519-jc6km
    pod "hello-2473888519-jc6km" delete

如果然后重新运行get pods命令,您应该注意到我们删除的 pod 已被一个新的带有新名称的 pod 所取代:

    $ kubectl get pod
    NAME                     READY     STATUS    RESTARTS   AGE
    hello-2473888519-1d69q   1/1       Running   0          8s

在 Kubernetes 中,我们可以使用副本集(和部署)来确保尽管出现意外事件,例如服务器故障或管理员误删 pod(就像在这种情况下发生的那样),但 pod 实例仍然在我们的集群中运行。

你应该开始理解作为这个练习的一部分,pod 是一个短暂的实体。当它被删除或者它所在的节点失败时,它将永远消失。Kubernetes 确保缺失的 pod 被另一个替换,从相同的模板中创建。这意味着当 pod 不可避免地失败并被替换时,存储在本地文件系统或内存中的任何状态,pod 本身的身份也会丢失。

这使得 pod 非常适合一些工作负载,不需要在运行之间在本地存储状态,比如 Web 应用程序和大多数批处理作业。如果你正在构建打算部署到 Kubernetes 的新应用程序,通过将状态的存储委托给外部存储,比如数据库或像 Amazon S3 这样的服务,可以使它们更易于管理。

我们将在 Kubernetes 中探索允许我们部署需要存储本地状态和/或保持稳定身份的应用程序的功能,在第九章存储状态中。

当我们测试 Kubernetes 替换被移除的 pod 的能力时,你可能已经注意到一个问题,那就是在短时间内,我们的服务变得不可用。对于这样一个简单的单节点集群上运行的示例服务,也许这并不是世界末日。但我们确实需要一种方式,让我们的应用程序以最小化甚至瞬间的停机时间运行。

答案当然是要求 Kubernetes 运行多个实例来运行我们的应用程序,因此即使一个丢失了,第二个也可以接管工作:

    $ kubectl scale deployment/hello --replicas=2
    deployment "hello" scaled

如果我们现在检查正在运行的 pod,我们可以看到第二个hello pod 已经加入了:

    $ kubectl get pods
    NAME                     READY     STATUS    RESTARTS   AGE
    hello-2473888519-10p63   1/1       Running   0          1m
    hello-2473888519-1d69q   1/1       Running   0          25m

使用仪表板

Kubernetes 仪表板是一个在 Kubernetes 集群内运行的 Web 应用程序,提供了一个替代的、更具图形化的解决方案,用于探索和监视你的集群。

Minikube 会自动安装仪表板,并提供一个命令,可以在你的 Web 浏览器中打开它:

    $ minikube dashboard

Kubernetes 仪表板

仪表板界面非常易于使用,你应该开始注意到与kubectl工作方式有更多相似之处,因为它们都允许你与相同的底层 API 进行交互。

屏幕左侧的导航栏可访问显示特定类型资源列表的屏幕。这类似于kubectl get命令提供的功能:

使用 Kubernetes 仪表板列出当前运行的 pod

在此视图中,我们可以单击看起来像一叠文件的图标,以打开日志查看器,查看从每个容器的标准输出中捕获的日志:

在 Kubernetes 仪表板中查看容器日志

其他资源具有适合其功能的其他选项。例如,部署和副本集具有对话框,用于增加或减少 pod 的数量。

通过单击特定资源的名称,我们可以获得一个显示类似于kubectl describe的信息的视图:

详细屏幕为我们提供了关于 Kubernetes 中的 pod 或其他资源的大量信息:

除了资源的配置和设置概览外,如果您滚动到页面底部,您应该能够看到事件的反馈。如果您正在尝试调试问题,这非常有用,并且将突出显示正在运行的资源的任何错误或问题。

对于 pod,我们有许多其他选项来管理和检查容器。例如,通过单击执行按钮在浏览器中打开终端:

在 Kubernetes 仪表板中使用交互式 shell 调试容器

目前,为了使此功能正常工作,您的容器需要有/bin/bash可用。这在未来版本的仪表板中可能会发生变化,但目前,为了使其工作,请将RUN apk add --no-cache bash添加到您的Dockerfile并部署新构建的映像。

代码配置

在本章中,我们通过使用kubectl提供的命令或 Kubernetes 仪表板与 Kubernetes 进行交互。在实践中,我发现这些工具对于快速在集群中运行容器非常有用。当配置变得更加复杂或者我想要能够将相同的应用程序部署到多个环境时,拥有一个可以提交到集群并存储在版本控制系统中的配置文件非常有用。

kubectl,实际上包括 Kubernetes 仪表板,将允许我们提交 YAML 或 JSON 格式的配置以创建集群上的资源。我们将再次看看如何使用 YAML 格式的文件而不是kubectl run等命令来部署相同的Hello World应用程序。

这个 Kubernetes 配置通常被称为清单,而 YAML 或 JSON 格式的文件被称为清单文件。

让我们首先删除我们用kubectl创建的配置,这样我们就有一个干净的状态来复制相同的配置:

    $ kubectl delete deployment/hello svc/hello
    deployment "hello" deleted
    service "hello" deleted

让我们为hello服务的版本 1 定义一个部署:

deployment.yaml 
apiVersion: apps/v1
kind: Deployment 
metadata: 
  name: hello 
spec: 
  replicas: 2 
  template: 
    metadata: 
      labels: 
        app: hello 
    spec: 
      containers: 
      - name: hello 
        image: hello:v1 
        ports: 
        - containerPort: 80 

现在我们可以使用kubectl将部署提交到 Kubernetes:

    $kubectl apply -f deployment.yaml
    deployment "hello" created  

接下来,让我们为一个服务做同样的事情:

service.yaml 
kind: Service 
apiVersion: v1 
metadata: 
  name: hello 
spec: 
  selector: 
    app: hello 
  type: NodePort 
  ports: 
  - protocol: TCP 
    port: 80 
    targetPort: 80 

使用kubectl提交定义到 Kubernetes:

    $ kubectl apply -f service.yaml
    service "hello" created  

你可以看到,虽然我们牺牲了只需运行一个命令来创建部署的速度和简单性,但通过明确指定我们想要创建的资源,我们可以更好地控制我们的 pod 的配置,并且现在我们可以将这个定义提交到版本控制,并可靠地更新。

在更新资源时,我们可以对文件进行编辑,然后使用kubectl apply命令来更新资源。kubectl会检测到我们正在更新现有资源,并将其更新以匹配我们的配置。尝试编辑deployment.yaml中的图像标记,然后重新提交到集群:

    $ kubectl apply -f deployment.yaml
    deployment "hello" configured 

如果我们只是在本地集群上对资源进行更改,我们可能只是想快速更改一些东西,而无需编辑文件。首先,就像在我们之前的例子中一样,您可以使用kubectl set来更新属性。Kubernetes 实际上并不关心我们如何创建资源,因此我们之前所做的一切仍然有效。进行快速更改的另一种方法是使用kubectl edit命令。假设您已经正确设置了$EDITOR环境变量与您喜欢的文本编辑器,您应该能够打开资源的 YAML,进行编辑,然后保存,而kubectl会无缝地为您更新资源。

故障排除 Minikube

在尝试使用 Minikube 时可能遇到的一个常见问题是,您可能无法访问 VM,因为其网络与您的计算机上配置的另一个网络重叠。如果您正在使用企业 VPN,或者连接到配置了默认情况下 Minikube 使用的192.168.99.1/24 IP 地址范围的另一个网络,这种情况经常会发生。

使用替代 CIDR 启动 Minikube 非常简单,您可以选择任何您想要使用的私有范围;只需确保它不会与本地网络上的其他服务重叠:

    $ minikube start --host-only-cidr=172.16.0.1/24

总结

做得好,能走到这一步真不容易。如果您在本章的示例中跟着做,那么您应该已经在学习如何使用 Kubernetes 来管理自己的应用程序的路上了。您应该能够做到以下几点:

  • 使用 Minikube 在您的工作站上设置单节点 Kubernetes 集群

  • 使用 Docker 构建一个简单的应用程序容器

  • 在 Minikube 集群上运行一个 pod

  • 使用清单文件声明 Kubernetes 配置,以便您可以重现您的设置

  • 设置一个服务,以便您可以访问您的应用程序

第三章:抓住云端

在本章中,我们将学习如何从头开始在亚马逊网络服务上构建一个运行 Kubernetes 集群。为了了解 Kubernetes 的工作原理,我们将手动启动将形成第一个集群的 EC2 实例,并手动安装和配置 Kubernetes 组件。

我们将构建的集群适合您在学习管理 Kubernetes 和开发可以在 Kubernetes 上运行的应用程序时使用。通过这些说明,我们的目标是构建最简单的集群,可以部署到 AWS。当然,这意味着在构建关键任务应用程序的集群时,您可能会有一些不同的需求。但不用担心——在第三部分《准备生产环境》中,我们将涵盖您需要了解的一切,以使您的集群准备好应对最苛刻的应用程序。

在 AWS 上运行 Kubernetes 集群是需要花钱的。根据我们的说明(一个带有一个主节点和一个工作节点的基本集群),目前的费用大约是每月 75 美元。因此,如果您只是用集群进行实验和学习,请记得在一天结束时关闭实例。

如果您已经完成了集群,终止实例并确保 EBS 卷已被删除,因为即使它们所附加的实例已经停止,您也会为这些存储卷付费。

本章旨在成为一个学习体验,因此请在阅读时阅读并输入命令。如果您有本书的电子书版本,请抵制复制粘贴的冲动,因为如果您输入命令并花些时间理解您正在做的事情,您会学到更多。有一些工具可以通过运行一个命令来完成本章所涵盖的一切甚至更多,但是希望通过逐步手动构建您的第一个集群,您将获得一些宝贵的见解,了解构建 Kubernetes 集群所需的一切。

集群架构

本章中我们要建立的集群将由两个 EC2 实例组成——一个将运行 Kubernetes 控制平面的所有组件,另一个是您可以用来运行应用程序的工作节点。

因为我们是从零开始的,本章还将阐述一种在私有网络中隔离 Kubernetes 集群并允许您从自己的工作站轻松访问机器的方法。

我们将通过使用额外的实例作为堡垒主机来实现这一点,该主机将允许来自外部世界的 SSH 连接,如下图所示。如果您的 AWS 账户已经有一些基础设施可以实现这一点,那么请随意跳过本节:

本章中您将设置的集群架构

创建 AWS 账户

如果您还没有 AWS 账户,请前往aws.amazon.com/注册一个。在您的账户中创建资源之前,您需要向您的账户添加信用卡以支付任何费用。

当您首次注册 AWS 账户时,您将在前 12 个月内有资格免费使用一些服务。不幸的是,这个免费层并不能提供足够的资源来运行 Kubernetes,但在本章中,我们已经优化了我们选择的实例,以降低成本,因此您应该能够在不花费太多的情况下跟随示例。

创建 IAM 用户

当您注册 AWS 账户时,您选择的电子邮件地址和密码将用于登录根账户。在开始与 AWS 进行交互之前,最好创建一个 IAM 用户,您将使用该用户与 AWS 进行交互。这样做的好处是,如果您愿意,您可以为每个 IAM 用户提供尽可能多或尽可能少的对 AWS 服务的访问权限。如果您使用根账户,您将自动拥有完全访问权限,并且无法管理或撤销权限。按照以下步骤设置账户:

  1. 登录 AWS 控制台后,通过点击“服务”并在搜索框中输入IAM来进入身份和访问管理仪表板。

  2. 从侧边栏中选择“用户”以查看 AWS 账户中的 IAM 用户。如果您刚刚设置了一个新账户,这里还没有任何用户——根账户不算在内。

  3. 通过点击屏幕顶部的“添加用户”按钮开始设置新用户账户的流程。

  4. 首先选择一个用户名作为您的用户。勾选两个框以启用编程访问(这样您就可以使用命令行客户端)和AWS 管理控制台访问,这样您就可以登录到 Web 控制台,如前面的屏幕截图所示:

  1. 在下一个屏幕上,您可以为用户配置权限。选择直接附加现有策略,然后选择AdministratorAccess策略,如下图所示:

  1. 审查您的设置,然后单击创建用户

  1. 创建用户后,请记下凭据。您将很快需要访问密钥 ID秘密访问密钥来配置 AWS 命令行客户端。还要记下控制台登录链接,因为这是您的 AWS 帐户的唯一链接,如下所示:

  1. 一旦您为自己设置了 IAM 用户,请从浏览器中注销根帐户,并检查您是否可以使用用户名和密码重新登录。

您可能希望为您的 AWS 帐户设置双因素身份验证以获得更高的安全性。请记住,对帐户具有管理员访问权限的任何人都可以访问或删除您帐户中的任何资源。

获取 CLI

您可以使用 Web 控制台控制 AWS,但如果您从 AWS 命令行客户端执行所有操作,您对 AWS 的控制将更加精确。

您应该按照 AWS 提供的说明在您的系统上安装命令行客户端(或者使用系统包管理器),使用以下链接中找到的说明:docs.aws.amazon.com/cli/latest/userguide/installing.html

一旦您安装了命令行客户端,请运行aws configure命令以使用您的凭据配置 CLI。此命令将更新您的主目录中的aws config文件。

在这个阶段,您应该为您的集群选择一个 AWS 区域。对于测试和实验,选择一个距离您位置相对较近的区域是有意义的。这样做将在您使用sshconnect访问您的实例时改善延迟。

设置密钥对

当我们启动 EC2 实例时,我们希望能够通过 SSH 访问它。我们可以在 EC2 控制台中注册一个密钥对,以便在启动实例后登录。

我们可以要求 AWS 为您生成一个密钥对(然后您可以下载)。但最佳做法是在您的工作站上生成一个密钥对,并将公共部分上传到 AWS。这样可以确保您(只有您)控制您的实例,因为您的密钥的私有部分永远不会离开您自己的机器。要设置密钥对,请按照以下步骤进行:

  1. 您可能已经在您的机器上有一个希望使用的密钥对。您可以通过查看.ssh目录中的现有密钥来检查,如下所示:
$ ls -la ~/.ssh
total 128
drwx------    6 edwardrobinson  staff    192 25 Feb 15:49 .
drwxr-xr-x+ 102 edwardrobinson  staff   3264 25 Feb 15:49 ..
-rw-r--r--    1 edwardrobinson  staff   1759 25 Feb 15:48 config
-rw-------    1 edwardrobinson  staff   3326 25 Feb 15:48 id_rsa
-rw-r--r--    1 edwardrobinson  staff    753 25 Feb 15:48 
id_rsa.pub
-rw-r--r--    1 edwardrobinson  staff  53042 25 Feb 15:48 
known_hosts  
  1. 在此示例中,您可以看到我在.ssh目录中有一个密钥对——私钥的默认名称为id_rsa,公钥称为id_rsa.pub

  2. 如果您还没有设置密钥对,或者想要创建一个新的密钥对,那么您可以使用ssh-keygen命令创建一个新的,如下所示:

$ ssh-keygen -t rsa -b 4096 -C "email@example.com"
Generating public/private rsa key pair.  
  1. 此命令使用您的电子邮件地址作为标签创建一个新的密钥对。

  2. 接下来,选择保存新密钥对的位置。如果您还没有密钥对,只需按Enter将其写入默认位置,如下所示:

Enter file in which to save the key (/home/edwardrobinson/.ssh/id_rsa):  
  1. 接下来,系统会要求您输入密码。如果只需按Enter,则密钥将在没有任何密码保护的情况下创建,如下命令所示。如果选择密码,请确保记住它或安全存储,否则您将无法在没有密码的情况下使用 SSH 密钥(或访问实例)。
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/edwardrobinson/.ssh/id_rsa.
Your public key has been saved in /home/edwardrobinson/.ssh/id_rsa.
The key fingerprint is:
SHA256:noWDFhnDxcvFl7DGi6EnF9EM5yeRMfGX1wt85wnbxxQ email@example.com  
  1. 一旦您在您的机器上有了 SSH 密钥对,您可以开始将其导入到您的 AWS 帐户中。请记住,您只需要导入密钥对的公共部分。这将在以.pub扩展名结尾的文件中。

  2. 从 AWS EC2 控制台(单击“服务”,然后搜索 EC2),选择屏幕左侧菜单中的密钥对,如下截图所示:

  1. 从此屏幕中,选择导入密钥对以打开对话框,您可以在其中上传您的密钥对,如下截图所示:

  1. 选择一个在 AWS 中标识您的密钥对的名称(我选择了eds_laptop)。然后,要么导航到密钥的位置,要么只需将其文本粘贴到大文本框中,然后单击导入。导入密钥后,您应该在密钥对页面上看到它列出。

如果您在多个地区使用 AWS,则需要在要启动实例的每个地区导入一个密钥对。

准备网络

我们将在您的 AWS 账户中设置一个新的 VPC。VPC,或虚拟私有云,允许我们拥有一个与 EC2 和互联网上的所有其他用户隔离的私有网络,我们可以在其上启动实例。

它为我们构建集群的安全网络提供了一个安全的基础,如下命令所示:

$ VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query "Vpc.VpcId" --output text)

VpcId将是您的账户唯一的,所以我将设置一个 shell 变量,以便在需要时引用它。您可以使用来自您的帐户的VpcId做同样的事情,或者您可能更喜欢每次需要时将其键入。

本章的其余步骤遵循这种模式,但如果您不明白发生了什么,不要害怕查看 shell 变量,并将 ID 与 AWS 控制台中的资源进行关联,如下所示:

$ echo $VPC_ID  

Kubernetes 根据 AWS 分配给它们的内部 DNS 主机名命名您的实例。如果我们在 VPC 中启用 DNS 支持,那么我们将能够在使用 VPC 内提供的 DNS 服务器时解析这些主机名,如下所示:

$ aws ec2 modify-vpc-attribute \
    --enable-dns-support \
    --vpc-id $VPC_ID
$ aws ec2 modify-vpc-attribute \
    --enable-dns-hostnames \
    --vpc-id $VPC_ID  

Kubernetes 广泛使用 AWS 资源标记,因此它知道可以使用哪些资源,哪些资源由 Kubernetes 管理。这些标记的关键是kubernetes.io/cluster/<cluster_name>。对于可能在几个不同集群之间共享的资源,我们使用shared值。这意味着 Kubernetes 可以利用它们,但永远不会从您的帐户中删除它们。

我们将用于 VPC 等资源。Kubernetes 完全管理生命周期的资源具有owned的标记值,并且如果不再需要,Kubernetes 可能会删除它们。当 Kubernetes 创建资源,如自动缩放组中的实例、EBS 卷或负载均衡器时,通常会自动创建这些标记。

我喜欢在创建的集群之后以计算机科学历史上的著名人物命名。我为本章创建的集群以设计了 COBOL 编程语言的 Grace Hopper 命名。

让我们为我们的新 VPC 添加一个标签,以便 Kubernetes 能够使用它,如下命令所示:

aws ec2 create-tags \
--resources $VPC_ID \
--tags Key=Name,Value=hopper \
  Key=kubernetes.io/cluster/hopper,Value=shared  

当我们创建 VPC 时,一个主路由表会自动创建。我们将在私有子网中使用这个路由表进行路由。让我们先获取 ID 以备后用,如下命令所示:

$ PRIVATE_ROUTE_TABLE_ID=$(aws ec2 describe-route-tables \
    --filters Name=vpc-id,Values=$VPC_ID \
    --query "RouteTables[0].RouteTableId" \
    --output=text) 

现在我们将添加第二个路由表来管理我们 VPC 中公共子网的路由,如下所示:

$ PUBLIC_ROUTE_TABLE_ID=$(aws ec2 create-route-table \
  --vpc-id $VPC_ID \
  --query "RouteTable.RouteTableId" --output text)  

现在我们将为路由表命名,以便以后能够跟踪它们:

$ aws ec2 create-tags \
  --resources $PUBLIC_ROUTE_TABLE_ID \
  --tags Key=Name,Value=hopper-public
$ aws ec2 create-tags \
  --resources $PRIVATE_ROUTE_TABLE_ID \
  --tags Key=Name,Value=hopper-private  

接下来,我们将创建两个子网供我们的集群使用。因为我要在eu-west-1区域(爱尔兰)创建我的集群,我将在eu-west-1a子网中创建这些子网。您应该通过运行aws ec2 describe-availability-zones来选择您正在使用的区域中的可用区来为您的集群选择一个可用区。在第三部分,我们将学习如何创建跨多个可用区的高可用性集群。

让我们首先创建一个只能从我们的私有网络内部访问的实例子网。我们将在 CIDR 块上使用“/20 子网掩码”,如下命令所示;通过这样做,AWS 将为我们提供 4089 个 IP 地址,可供分配给我们的 EC2 实例和 Kubernetes 启动的 pod 使用:

$ PRIVATE_SUBNET_ID=$(aws ec2 create-subnet \
  --vpc-id $VPC_ID \
  --availability-zone eu-west-1a \
  --cidr-block 10.0.0.0/20 --query "Subnet.SubnetId" \
  --output text)

$ aws ec2 create-tags \
  --resources $PRIVATE_SUBNET_ID \
  --tags Key=Name,Value=hopper-private-1a \
    Key=kubernetes.io/cluster/hopper,Value=owned \
    Key=kubernetes.io/role/internal-elb,Value=1  

接下来,让我们在同一个可用区添加另一个子网,如下命令所示。我们将使用这个子网来放置需要从互联网访问的实例,比如公共负载均衡器和堡垒主机:

$ PUBLIC_SUBNET_ID=$(aws ec2 create-subnet \
  --vpc-id $VPC_ID \
  --availability-zone eu-west-1a \
 --cidr-block 10.0.16.0/20 --query "Subnet.SubnetId" \
  --output text)

$ aws ec2 create-tags \
 --resources $PUBLIC_SUBNET_ID \
 --tags Key=Name,Value=hopper-public-1a \
    Key=kubernetes.io/cluster/hopper,Value=owned \
    Key=kubernetes.io/role/elb,Value=1  

接下来,我们应该将这个子网与公共路由表关联,如下所示:

$ aws ec2 associate-route-table \
  --subnet-id $PUBLIC_SUBNET_ID \
  --route-table-id $PUBLIC_ROUTE_TABLE_ID  

为了使我们的公共子网中的实例能够与互联网通信,我们将创建一个互联网网关,将其附加到我们的 VPC,然后在路由表中添加一条路由,将流向互联网的流量路由到网关,如下命令所示:

$ INTERNET_GATEWAY_ID=$(aws ec2 create-internet-gateway \
    --query "InternetGateway.InternetGatewayId" --output text)

$ aws ec2 attach-internet-gateway \
    --internet-gateway-id $INTERNET_GATEWAY_ID \
    --vpc-id $VPC_ID

$ aws ec2 create-route \
    --route-table-id $PUBLIC_ROUTE_TABLE_ID \
    --destination-cidr-block 0.0.0.0/0 \
    --gateway-id $INTERNET_GATEWAY_ID

为了配置私有子网中的实例,我们需要它们能够建立对外部连接,以便安装软件包等。为了实现这一点,我们将在公共子网中添加一个 NAT 网关,然后为互联网出站流量在私有路由表中添加路由,如下所示:

$ NAT_GATEWAY_ALLOCATION_ID=$(aws ec2 allocate-address \
  --domain vpc --query AllocationId --output text)

$ NAT_GATEWAY_ID=$(aws ec2 create-nat-gateway \
  --subnet-id $PUBLIC_SUBNET_ID \
  --allocation-id $NAT_GATEWAY_ALLOCATION_ID \
  --query NatGateway.NatGatewayId --output text)  

在这个阶段,你可能需要等待一段时间,直到 NAT 网关被创建,然后再创建路由,如下命令所示:

$ aws ec2 create-route \
    --route-table-id $PRIVATE_ROUTE_TABLE_ID \
    --destination-cidr-block 0.0.0.0/0 \
    --nat-gateway-id $NAT_GATEWAY_ID  

建立堡垒

我们将使用我们要启动的第一个主机作为堡垒主机,这将允许我们连接到只能从 VPC 网络的私有侧访问的其他服务器。

我们将创建一个安全组,以允许 SSH 流量到这个实例。我们将使用aws ec2 create-security-group命令为我们的堡垒主机创建一个安全组,如下命令所示。安全组是 AWS 提供的一种抽象,用于将相关的防火墙规则分组并应用到主机组上:

$ BASTION_SG_ID=$(aws ec2 create-security-group \
    --group-name ssh-bastion \
    --description "SSH Bastion Hosts" \
    --vpc-id $VPC_ID \
    --query GroupId --output text)  

一旦我们创建了安全组,我们可以附加一个规则以允许端口22上的 SSH 入口,如下命令所示。这将允许您使用 SSH 客户端访问您的主机。在这里,我允许来自 CIDR 范围0.0.0.0/0的入口,但如果您的互联网连接有一个稳定的 IP 地址,您可能希望将访问限制在您自己的 IP 上:

$ aws ec2 authorize-security-group-ingress \
  --group-id $BASTION_SG_ID \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0  

现在我们已经为堡垒主机设置了安全组,我们可以开始启动我们的第一个 EC2 实例。在本章中,我将使用 Ubuntu Linux(一种流行的 Linux 发行版)。在启动实例之前,我们需要发现我们想要使用的操作系统的 AMI(Amazon 机器映像)的 ID。

Ubuntu 项目定期发布更新的映像到他们的 AWS 账户,可以用来启动 EC2 实例。我们可以运行以下命令来发现我们需要的映像的 ID:

$ UBUNTU_AMI_ID=$(aws ec2 describe-images --owners 099720109477 \
  --filters Name=root-device-type,Values=ebs \
            Name=architecture,Values=x86_64 \
            Name=name,Values='*hvm-ssd/ubuntu-xenial-16.04*' \
  --query "sort_by(Images, &Name)[-1].ImageId" --output text)  

我们将为堡垒主机使用一个t2.micro实例(如下命令所示),因为这种实例类型的使用包含在 AWS 免费套餐中,所以在设置 AWS 账户后的第一个 12 个月内,您不必为其付费。

$ BASTION_ID=$(aws ec2 run-instances \
  --image-id $UBUNTU_AMI_ID \
  --instance-type t2.micro \
  --key-name eds_laptop \
  --security-group-ids $BASTION_SG_ID \
  --subnet-id $PUBLIC_SUBNET_ID \
  --associate-public-ip-address \
  --query "Instances[0].InstanceId" \
  --output text)  

请注意,我们正在传递我们选择使用的子网的 ID,我们刚刚创建的安全组的 ID,以及我们上传的密钥对的名称。

接下来,让我们使用Name标签更新实例,这样我们在查看 EC2 控制台时就可以识别它,如下命令所示:

$ aws ec2 create-tags \
  --resources $BASTION_ID \
  --tags Key=Name,Value=ssh-bastion  

一旦实例启动,您应该能够运行aws ec2 describe-instances命令来发现您新实例的公共 IP 地址,如下所示:

$ BASTION_IP=$(aws ec2 describe-instances \
  --instance-ids $BASTION_ID \
  --query "Reservations[0].Instances[0].PublicIpAddress" \
  --output text)  

现在您应该能够使用 SSH 访问实例,如下所示:

$ ssh ubuntu@$BASTION_IP  

当您登录时,您应该会看到以下消息:

Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-1052-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
        http://www.ubuntu.com/business/services/cloud

 0 packages can be updated.
 0 updates are security updates.

 To run a command as administrator (user "root"), use "sudo <command>".
 See "man sudo_root" for details.

 ubuntu@ip-10-0-26-86:~$  

如果您将密钥对保存为除默认的~/.ssh/id_rsa之外的其他内容,您可以使用-i标志传递密钥的路径,如下所示:

**ssh -i ~/.ssh/id_aws_rsa ubuntu@$BASTION_IP**

作为替代,您可以首先将密钥添加到您的 SSH 代理中,方法如下:

**ssh-add ~/.ssh/id_aws_rsa**

sshuttle

只需使用 SSH 就可以将流量从工作站转发到私有网络。但是,我们可以使用sshuttle工具更方便地访问堡垒实例上的服务器。

在您的工作站上安装sshuttle很简单。

您可以使用 Homebrew 在 macOS 上安装它,如下所示:

brew install sshuttle  

如果您在 Linux 上安装了 Python,也可以按照以下方式安装它:

    pip install sshuttle

为了透明地代理私有网络内的实例流量,我们可以运行以下命令:

$ sshuttle -r ubuntu@$BASTION_IP 10.0.0.0/16 --dns
[local sudo] Password:
client: Connected.  

首先,我们传递我们的ubuntu@$BASTION_IP堡垒实例的 SSH 登录详细信息,然后是我们 VPC 的 CIDR(这样只有目的地是私有网络的流量才会通过隧道传输);这可以通过运行aws ec2 describe-vpcs来找到。最后,我们传递--dns标志,以便您的工作站上的 DNS 查询将由远程实例的 DNS 服务器解析。

使用sshuttle需要您输入本地 sudo 密码,以便设置其代理服务器。

您可能希望在单独的终端或后台运行sshuttle,以便您仍然可以访问我们一直在使用的 shell 变量。

我们可以通过尝试使用其私有 DNS 名称登录到我们的实例来验证此设置是否正常工作,方法如下:

$ aws ec2 describe-instances \
  --instance-ids $BASTION_ID \
  --query "Reservations[0].Instances[0].PrivateDnsName"

"ip-10-0-21-138.eu-west-1.compute.internal"

$ ssh ubuntu@ip-10-0-21-138.eu-west-1.compute.internal  

这将测试您是否可以从 AWS 提供的私有 DNS 解析 VPC 内运行的实例的 DNS 条目,并且查询返回的私有 IP 地址是否可达。

如果您遇到任何困难,请检查sshuttle是否有任何连接错误,并确保您已经记得在您的 VPC 中启用了 DNS 支持。

实例配置文件

为了让 Kubernetes 能够利用其与 AWS 云 API 的集成,我们需要设置 IAM 实例配置文件。实例配置文件是 Kubernetes 软件与 AWS API 进行身份验证的一种方式,也是我们为 Kubernetes 可以执行的操作分配细粒度权限的一种方式。

学习 Kubernetes 需要正确运行所需的所有权限可能会令人困惑。您可以设置允许对 AWS 进行完全访问的实例配置文件,但这将以牺牲安全最佳实践为代价。

每当我们分配安全权限时,我们应该致力于授予软件正常运行所需的最低权限。为此,我整理了一组最小的 IAM 策略,这些策略将允许我们的集群正常运行,而不会过度授予权限。

您可以在github.com/errm/k8s-iam-policies查看这些策略,我已经用简要描述记录了每个策略的目的。

存储库包括一个简单的 shell 脚本,我们可以用它来为我们集群中的主节点和工作节点创建 IAM 实例配置文件,如下所示:

$ curl https://raw.githubusercontent.com/errm/k8s-iam-policies/master/setup.sh -o setup.sh
$ sh -e setup.sh
  {
      "InstanceProfile": {
          "Path": "/",
          "InstanceProfileName": "K8sMaster",
          "InstanceProfileId": "AIPAJ7YTS67QLILBZUQYE",
          "Arn": "arn:aws:iam::642896941660:instance-profile/K8sMaster",
          "CreateDate": "2018-02-26T19:06:19.831Z",
          "Roles": []
      }
  }
  {
      "InstanceProfile": {
          "Path": "/",
          "InstanceProfileName": "K8sNode",
          "InstanceProfileId": "AIPAJ27KNVOKTLZV7DDA4",
          "Arn": "arn:aws:iam::642896941660:instance-profile/K8sNode",
          "CreateDate": "2018-02-26T19:06:25.282Z",
          "Roles": []
      }
  }  

Kubernetes 软件

我们将启动一个实例,在该实例中,我们将安装组成我们集群的不同节点所需的所有软件。然后,我们将创建一个 AMI,或 Amazon 机器映像,我们可以用它来启动我们集群上的节点。

首先,我们为这个实例创建一个安全组,如下所示:

$ K8S_AMI_SG_ID=$(aws ec2 create-security-group \
    --group-name k8s-ami \
    --description "Kubernetes AMI Instances" \
    --vpc-id $VPC_ID \
    --query GroupId \
    --output text)

我们需要能够从我们的堡垒主机访问这个实例,以便登录和安装软件,因此让我们添加一条规则,允许来自ssh-bastion安全组中实例的端口22的 SSH 流量,如下所示:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_AMI_SG_ID \
    --protocol tcp \
    --port 22 \
    --source-group $BASTION_SG_ID

我们只是在这里使用一个t2.micro实例,因为我们不需要一个非常强大的实例来安装软件包,如下命令所示:

$ K8S_AMI_INSTANCE_ID=$(aws ec2 run-instances \
    --subnet-id $PRIVATE_SUBNET_ID \
    --image-id $UBUNTU_AMI_ID \
    --instance-type t2.micro \
    --key-name eds_laptop \
    --security-group-ids $K8S_AMI_SG_ID \
    --query "Instances[0].InstanceId" \
    --output text) 

我们添加一个Name标签,这样我们以后可以识别实例,如果需要的话,如下所示:

$ aws ec2 create-tags \
    --resources $K8S_AMI_INSTANCE_ID \
    --tags Key=Name,Value=kubernetes-node-ami

获取实例的 IP 地址,如下所示:

$ K8S_AMI_IP=$(aws ec2 describe-instances \
    --instance-ids $K8S_AMI_INSTANCE_ID \
    --query "Reservations[0].Instances[0].PrivateIpAddress" \
    --output text)

然后使用ssh登录,如下所示:

$ ssh ubuntu@$K8S_AMI_IP  

现在我们准备开始配置实例,安装所有集群中所有节点都需要的软件和配置。首先同步 apt 存储库,如下所示:

$ sudo apt-get update  

Docker

Kubernetes 可以与许多容器运行时一起工作,但 Docker 仍然是最广泛使用的。

在安装 Docker 之前,我们将向 Docker 服务添加一个systemd drop-in 配置文件,如下所示:

/etc/systemd/system/docker.service.d/10-iptables.conf
[Service]
ExecStartPost=/sbin/iptables -P FORWARD ACCEPT  

为了使我们的 Kubernetes pod 对集群中的其他实例可访问,我们需要设置iptables FORWARD链的默认策略,如下命令所示;否则,Docker 将将其设置为DROP,Kubernetes 服务的流量将被丢弃:

$ sudo mkdir -p /etc/systemd/system/docker.service.d/
$ printf "[Service]\nExecStartPost=/sbin/iptables -P FORWARD ACCEPT" |   sudo tee /etc/systemd/system/docker.service.d/10-iptables.conf

Kubernetes 将与 Ubuntu 存储库中包含的 Docker 版本很好地配合,因此我们可以通过安装docker.io软件包来简单地安装它,如下所示:

$ sudo apt-get install -y docker.io  

通过运行以下命令检查 Docker 是否已安装:

$ sudo docker version  

安装 Kubeadm

接下来,我们将安装我们在这个主机上设置 Kubernetes 控制平面所需的软件包。这些软件包在以下列表中描述:

  • kubelet:Kubernetes 用来控制容器运行时的节点代理。这用于在 Docker 容器中运行控制平面的所有其他组件。

  • kubeadm:这个实用程序负责引导 Kubernetes 集群。

  • kubectl:Kubernetes 命令行客户端,它将允许我们与 Kubernetes API 服务器交互。

首先,添加托管 Kubernetes 软件包的 apt 存储库的签名密钥,如下所示:

$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
OK  

接下来,添加 Kubernetes apt 存储库,如下所示:

$ sudo apt-add-repository 'deb http://apt.kubernetes.io/ kubernetes-xenial main'  

然后,重新同步软件包索引,如下所示:

$ sudo apt-get update  

然后,按以下方式安装所需的软件包:

$ sudo apt-get install -y kubelet kubeadm kubectl  

这将安装软件包的最新版本。如果您想固定到特定版本的 Kubernetes,尝试运行apt-cache madison kubeadm来查看不同的可用版本。

我使用 Kubernetes 1.10 准备了这一章节。如果你想安装最新版本的 Kubernetes 1.10,你可以运行以下命令:

**sudo apt-get install kubeadm=1.10.* kubectl=1.10.* kubelet=1.10.***

构建 AMI

现在我们在这个实例上安装软件包完成后,可以关闭它,如下所示:

$ sudo shutdown -h now
Connection to 10.0.13.93 closed by remote host.
Connection to 10.0.13.93 closed.  

我们可以使用create-image命令指示 AWS 对我们的实例的根卷进行快照,并使用它来生成 AMI,如下命令所示(在运行命令之前,您可能需要等待一段时间,直到实例完全停止):

$ K8S_AMI_ID=$(aws ec2 create-image \
 --name k8s-1.10.3-001 \
 --instance-id $K8S_AMI_INSTANCE_ID \
 --description "Kubernetes v1.10.3" \
 --query ImageId \ 
 --output text)

镜像变得可用需要一些时间,但您可以使用describe-images命令来检查其状态,如下所示:

aws ec2 describe-images \
     --image-ids $K8S_AMI_ID \
     --query "Images[0].State"

在构建镜像时,您将看到pending,但一旦准备好使用,状态将变为available

引导集群

现在我们可以为 Kubernetes 控制平面组件启动一个实例。首先,我们将为这个新实例创建一个安全组,如下所示:

$ K8S_MASTER_SG_ID=$(aws ec2 create-security-group \
    --group-name k8s-master \
    --description "Kubernetes Master Hosts" \
    --vpc-id $VPC_ID \
    --query GroupId \
    --output text) 

我们需要能够从我们的堡垒主机访问这个实例,以便登录和配置集群。我们将添加一条规则,允许来自ssh-bastion安全组中实例的端口22上的 SSH 流量,如下所示:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_MASTER_SG_ID \
    --protocol tcp \
    --port 22 \
    --source-group $BASTION_SG_ID 

现在我们可以启动实例,如下所示:

$ K8S_MASTER_INSTANCE_ID=$(aws ec2 run-instances \
    --private-ip-address 10.0.0.10 \
    --subnet-id $PRIVATE_SUBNET_ID \
    --image-id $K8S_AMI_ID \
    --instance-type t2.medium \
    --key-name eds_laptop \
    --security-group-ids $K8S_MASTER_SG_ID \
    --credit-specification CpuCredits=unlimited \
    --iam-instance-profile Name=K8sMaster \
    --query "Instances[0].InstanceId" \
    --output text) 

我们应该给实例命名,并确保 Kubernetes 能够将所有资源与我们的集群关联起来,我们还将添加KubernetesCluster标签,并为此集群命名,如下所示:

$ aws ec2 create-tags \
  --resources $K8S_MASTER_INSTANCE_ID \
  --tags Key=Name,Value=hopper-k8s-master \
    Key=kubernetes.io/cluster/hopper,Value=owned

$ ssh ubuntu@10.0.0.10  

为了确保所有 Kubernetes 组件使用相同的名称,我们应该将主机名设置为与 AWS 元数据服务提供的名称相匹配,如下所示。这是因为元数据服务提供的名称被启用了 AWS 云提供程序的组件使用:

$ sudo hostnamectl set-hostname $(curl http://169.254.169.254/latest/meta-data/hostname)
$ hostnamectl status
   Static hostname: ip-10-0-0-10.eu-west-1.compute.internal  

为了正确配置 kubelet 使用 AWS 云提供程序,我们创建了一个 systemd drop-in 文件,向 kubelet 传递一些额外的参数,如下所示:

/etc/systemd/system/kubelet.service.d/20-aws.conf
[Service]
Environment="KUBELET_EXTRA_ARGS=--cloud-provider=aws --node ip=10.0.0.10"
$ printf '[Service]\nEnvironment="KUBELET_EXTRA_ARGS=--cloud-provider=aws --node-ip=10.0.0.10"' | sudo tee /etc/systemd/system/kubelet.service.d/20-aws.conf 

添加了这个文件后,重新加载 systemd 配置,如下所示:

$ sudo systemctl daemon-reload
$ sudo systemctl restart kubelet  

我们需要为 kubeadm 提供一个配置文件,以便在它启动的每个组件上启用 AWS 云提供程序。在这里,我们还将 tokenTTL 设置为 0,如下所示;这意味着发放给工作节点加入集群的令牌不会过期。这很重要,因为我们计划使用自动扩展组来管理我们的工作节点,新节点可能会在一段时间后加入该组:

kubeadm.config
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
cloudProvider: aws
tokenTTL: "0"  

现在我们只需要运行以下命令来引导主节点:

$ sudo kubeadm init --config=kubeadm.config 
[init] Using Kubernetes version: v1.10.3 .. .
. . .
. . . 
Your Kubernetes master has initialized successfully!
. . .

您应该看到前面的消息,然后是一些设置集群其余部分的说明。记下 kubeadm join 命令,因为我们将需要它来设置工作节点。

我们可以通过按照 kubeadm 给出的指示在主机上设置 kubectl 来检查 API 服务器是否正常运行,如下所示:

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config  

尝试运行 kubectl version。如果 kubectl 能够正确连接到主机,那么您应该能够看到客户端(kubectl)和服务器上 Kubernetes 软件的版本,如下所示:

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.3", GitCommit:"d2835416544f298c919e2ead3be3d0864b52323b", GitTreeState:"clean", BuildDate:"2018-02-07T12:22:21Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.3", GitCommit:"d2835416544f298c919e2ead3be3d0864b52323b", GitTreeState:"clean", BuildDate:"2018-02-07T11:55:20Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}

刚刚发生了什么?

那很容易对吧?我们通过运行一个命令来启动和运行了 Kubernetes 控制平面。

kubeadm 命令是一个很棒的工具,因为它消除了正确配置 Kubernetes 的许多猜测。但是让我们暂时中断设置集群的过程,深入挖掘一下刚刚发生了什么。

查看 kubeadm 命令的输出应该给我们一些线索。

首先,kubeadm做的事情是建立一个私有密钥基础设施。如果你查看/etc/kubernetes/pki目录,你会看到一些ssl证书和私钥,以及一个用来签署每个密钥对的证书颁发机构。现在,当我们向集群添加工作节点时,它们将能够在 kubelet 和apiserver之间建立安全通信。

接下来,kubedam将静态 pod 清单写入/etc/kubernetes/manifests/目录。这些清单就像您将提交给 Kubernetes API 服务器以运行自己的应用程序的 pod 定义一样,但由于 API 服务器尚未启动,定义是由kubelet直接从磁盘读取的。

kubelet被配置为在kubeadmetc/systemd/system/kubelet.service.d/10-kubeadm.conf创建的systemd dropin中读取这些静态 pod 清单。您可以在其他配置中看到以下标志:

--pod-manifest-path=/etc/kubernetes/manifests  

如果您查看/etc/kubernetes/manifests/,您将看到形成控制平面的每个组件的 Kubernetes pod 规范,如下列表所述:

  • etcd.yaml:存储 API 服务器状态的键值存储

  • kube-apiserver.yaml:API 服务器

  • kube-controller-manager.yaml:控制器管理器

  • kube-scheduler.yaml:调度程序

最后,一旦 API 服务器启动,kubeadm向 API 提交了两个插件,如下列表所述:

  • kube-proxy:这个进程在每个节点上配置 iptables,使服务 IP 正确路由。它在每个节点上以 DaemonSet 运行。您可以通过运行kubectl -n kube-system describe ds kube-proxy来查看此配置。

  • kube-dns:这个进程提供了可以被集群上运行的应用程序用于服务发现的 DNS 服务器。请注意,在为您的集群配置 pod 网络之前,它将无法正确运行。您可以通过运行kubectl -n kube-system describe deployment kube-dns来查看kube-dns的配置。

您可以尝试使用kubectl来探索组成 Kubernetes 控制平面的不同组件。尝试运行以下命令:

$ kubectl -n kube-system get pods

$ kubectl -n kube-system describe pods

$ kubectl -n kube-system get daemonsets

$ kubectl -n kube-system get deployments

在继续下一节之前,请注销主实例,如下所示:

$ exit

**注销**

**连接到 10.0.0.10 已关闭。**

从您的工作站访问 API

能够通过工作站上的kubectl访问 Kubernetes API 服务器非常方便。这意味着您可以将您可能一直在开发的任何清单提交到在 AWS 上运行的集群。

我们需要允许来自堡垒服务器访问 API 服务器的流量。让我们向K8S-MASTER安全组添加一条规则来允许此流量,如下所示:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_MASTER_SG_ID \
    --protocol tcp \
    --port 6443 \
    --source-group $BASTION_SG_ID

如果您尚未在工作站上安装 kubectl,请返回到第二章,“启动引擎”,进行学习。

现在我们可以从主实例复制kubeconfig文件。

如果您在本地的~/.kube/config文件中尚未配置任何集群,您可以按照以下步骤从主服务器复制文件:

$ scp ubuntu@10.0.0.10:~/.kube/config ~/.kube/config  

如果您已经配置了一个集群(例如,minikube),那么您可能希望合并您的新集群的配置,或者使用另一个文件并使用--kubeconfig标志将其位置传递给kubectl,或者在KUBECONFIG环境变量中传递。

检查您是否可以使用本地的kubectl连接到 API 服务器,如下所示:

$ kubectl get nodes
NAME               STATUS     AGE       VERSION
ip-10-0-9-172...   NotReady   5m        v1.9.3 

如果您在连接时遇到任何问题,请检查sshuttle是否仍在运行,并且您已经正确允许了从堡垒主机到 k8s-master 安全组的访问。

设置 pod 网络

您可能已经注意到,当运行kubectl get nodes时,NodeStatusNotReady。这是因为我们引导的集群缺少一个基本组件——将允许在我们的集群上运行的 pod 相互通信的网络基础设施。

Kubernetes 集群的网络模型与标准 Docker 安装有些不同。有许多网络基础设施的实现可以为 Kubernetes 提供集群网络,但它们都具有一些共同的关键属性,如下列表所示:

  • 每个 pod 都被分配了自己的 IP 地址

  • 每个 pod 都可以与集群中的任何其他 pod 进行通信,而无需 NAT(尽管可能存在其他安全策略)

  • 运行在 pod 内部的软件看到的内部网络与集群中其他 pod 看到的 pod 网络是相同的,即它看到的 IP 地址相同,并且不进行端口映射

这种网络安排对于集群的用户来说要简单得多(比 Docker 的标准网络方案要简单),Docker 的标准网络方案是将容器内部端口映射到主机上的其他端口。

但是,这需要网络基础设施和 Kubernetes 之间的一些集成。Kubernetes 通过一个名为容器网络接口CNI)的接口来管理这种集成。通过 Kubernetes DaemonSet,可以简单地将CNI插件部署到集群的每个节点上。

如果您想了解更多关于 Kubernetes 集群网络的信息,我建议阅读底层概念的全面文档,网址为kubernetes.io/docs/concepts/cluster-administration/networking/

我们将部署一个名为amazon-vpc-cni-k8s的 CNI 插件,它将 Kubernetes 与 AWS VPC 网络的本地网络功能集成在一起。该插件通过将次要私有 IP 地址附加到形成集群节点的 EC2 实例的弹性网络接口,然后在 Kubernetes 将它们调度到每个节点时分配给 Pod 来工作。然后,流量通过 AWS VPC 网络布线直接路由到正确的节点。

部署此插件与使用kubectl将任何其他清单提交到 Kubernetes API 的过程类似,如以下命令所示:

$ kubectl apply -f https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/master/config/v1.3/aws-k8s-cni.yaml 
daemonset "aws-node" created

您可以通过运行以下命令来监视正在安装和启动的网络插件:

$ kubectl -n kube-system describe pods aws-node  

我们可以通过再次查看节点状态来检查网络是否已正确设置,方法如下:

$ kubectl get nodes
NAME               STATUS    ROLES     AGE       VERSION
ip-172-31-29-230   Ready     master    10m       v1.9.3  

启动工作节点

我们现在将为工作节点创建一个新的安全组,方法如下:

$ K8S_NODES_SG_ID=$(aws ec2 create-security-group \
    --group-name k8s-nodes \
    --description "Kubernetes Nodes" \
    --vpc-id $VPC_ID \
    --query GroupId \
    --output text)  

为了我们能够登录进行调试,我们将允许通过堡垒主机访问工作节点,方法如下:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_NODES_SG_ID \
    --protocol tcp \
    --port 22 \
    --source-group $BASTION_SG_ID

我们希望允许运行在工作节点上的 kubelet 和其他进程能够连接到主节点上的 API 服务器。我们可以通过以下命令来实现这一点:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_MASTER_SG_ID \
    --protocol tcp \
    --port 6443 \
    --source-group $K8S_NODES_SG_ID  

由于 kube-dns 插件可能在主节点上运行,让我们允许来自节点安全组的流量,方法如下:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_MASTER_SG_ID \
    --protocol all \
    --port 53 \
    --source-group $K8S_NODES_SG_ID 

我们还需要主节点能够连接到 kubelet 暴露的 API,以便流式传输日志和其他指标。我们可以通过输入以下命令来实现这一点:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_NODES_SG_ID \
    --protocol tcp \
    --port 10250 \
    --source-group $K8S_MASTER_SG_ID

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_NODES_SG_ID \
    --protocol tcp \
    --port 10255 \
    --source-group $K8S_MASTER_SG_ID

最后,我们需要允许任何节点上的任何 Pod 能够连接到任何其他 Pod。我们可以使用以下命令来实现这一点:

$ aws ec2 authorize-security-group-ingress \
    --group-id $K8S_NODES_SG_ID \
    --protocol all \
    --port -1 \
    --source-group $K8S_NODES_SG_ID  

为了在启动时使工作节点自动注册到主节点,我们将创建一个用户数据脚本。

此脚本在节点首次启动时运行。它进行一些配置更改,然后运行kubeadm join,如下命令所示。当我们初始化主节点时,您应该已经记录了kubeadm join命令。

user-data.sh
#!/bin/bash

set -exuo pipefail
hostnamectl set-hostname $(curl http://169.254.169.254/latest/meta-data/hostname)

cat << EOF $ /etc/systemd/system/kubelet.service.d/20-aws.conf
[Service]
Environment="KUBELET_EXTRA_ARGS=--cloud-provider=aws --node-ip=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)     --node-labels=node-role.kubernetes.io/node="
EOF

systemctl daemon-reload
systemctl restart kubelet

kubeadm join \
  --token fddaf9.1f07b60a8268aac0 \
  --discovery-token-ca-cert-hash sha256:872757bce0df91c2b046b0d8bb5d930bc1ecfa245b14c25ad8a52746cb8b8e8b \
10.0.0.10:6443  

首先,我们使用以下命令创建一个启动配置。这类似于自动缩放组将用于启动我们的工作节点的配置模板。许多参数类似于我们将传递给 EC2 run-instances 命令的参数:

$ aws autoscaling create-launch-configuration \
    --launch-configuration-name k8s-node-1.10.3-t2-medium-001 \
    --image-id $K8S_AMI_ID \ --key-name 
  eds_laptop \    
     --security-groups $K8S_NODES_SG_ID \  
     --user-data file://user-data.sh \    
     --instance-type t2.medium \    
     --iam-instance-profile K8sNode \    
     --no-associate-public-ip-address

创建启动配置后,我们可以创建一个自动缩放组,如下所示:

> aws autoscaling create-auto-scaling-group \
    --auto-scaling-group-name hopper-t2-medium-nodes \
    --launch-configuration-name k8s-node-1.10.3-t2-medium-001 \
    --min-size 1 \
    --max-size 1 \
    --vpc-zone-identifier $PRIVATE_SUBNET_ID \
    --tags Key=Name,Value=hopper-k8s-node \
      Key=kubernetes.io/cluster/hopper,Value=owned \
      Key=k8s.io/cluster-autoscaler/enabled,Value=1  

需要等待一段时间,直到自动缩放组启动节点,并使用kubeadm将其注册到主节点,如下所示。

> kubectl get nodes --watch
NAME              STATUS    AGE       VERSION
ip-10-0-0-10       Ready     37m       v1.10.3
ip-10-0-2-135      Ready     53s       v1.10.3  

如果您的节点启动但在几分钟后没有加入集群,请尝试登录节点并查看cloud-init日志文件。此日志的结尾将包括脚本的输出。

> cat /var/log/cloud-init-output.log  

演示时间

恭喜,如果您已经通过本章走到这一步!到目前为止,您应该已经拥有一个完全功能的 Kubernetes 集群,可以用来进行实验并更全面地探索 Kubernetes。

让我们通过部署一个应用程序到我们的集群来演示我们构建的集群正在工作,如下所示:

kubectl apply -f
 https://raw.githubusercontent.com/PacktPublishing/Kubernetes-on-AWS/master/chapter03/demo.yaml

此清单部署了一个简单的 Web 应用程序和一个服务,使用负载均衡器将应用程序暴露到互联网。我们可以使用kubectl get service命令查看负载均衡器的公共 DNS 名称,如下所示:

> kubectl get svc demo -o wide  

一旦您获得负载均衡器的公共地址,您可能需要等待一段时间,直到地址开始解析。在浏览器中访问该地址;您应该看到以下屏幕:

总结

到目前为止,您应该拥有一个完全功能的 Kubernetes 集群,可以用来进行实验并更全面地探索 Kubernetes。您的集群已正确配置,以充分利用 Kubernetes 与 AWS 的许多集成。

虽然有许多工具可以自动化和协助您在 AWS 上构建和管理 Kubernetes 集群的任务,但希望通过学习如何从头开始处理任务,您将更好地了解支持 Kubernetes 集群所需的网络和计算资源。

在第三部分,我们将在本章的知识基础上讨论您需要添加到集群中的其他组件,以使其适合托管生产服务。我们刚刚构建的集群是一个完全功能的 Kubernetes 安装。继续阅读,我们将研究在 Kubernetes 上成功运行生产服务所需的工具和技术:

  • 我们将研究您可以采用的工具和程序,以有效地管理部署和更新您的服务,使用 Kubernetes

  • 我们将研究您可以采用的策略和工具,以确保集群和其中运行的应用程序的安全

  • 我们将研究与 Kubernetes 一起使用的监控和日志管理工具

  • 我们将研究最佳的架构应用程序和集群的方式,以满足可用性目标

第四章:管理应用程序中的变更

在第二章 启动引擎中,我们首次尝试使用部署在 Kubernetes 上运行应用程序。在本章中,我们将深入了解 Kubernetes 提供的用于管理在集群上运行的 Pod 的工具。

  • 我们将学习如何通过使用Job资源来确保批处理任务成功完成

  • 我们将学习如何使用CronJob资源在预定时间间隔运行作业

  • 最后,我们将学习如何使用部署来使长时间运行的应用程序无限期运行,并在需要进行更改时更新它们或其配置

我们将看看如何可以使用 Kubernetes 以不同的方式启动 Pod,这取决于我们正在运行的工作负载。

您将学到更多关于如何使用部署资源来控制 Kubernetes 如何推出对长时间运行的应用程序的更改。您将了解可以使用 Kubernetes 执行常见部署模式的方法,例如蓝绿部署和金丝雀部署。

按设计,Pod 不打算以任何方式持久。正如我们之前讨论过的,有一系列条件可能导致 Pod 的生命周期终止。它们包括:

  • 底层节点的故障:可能是由于一些意外事件,例如硬件故障。或者可能是出于设计考虑;例如,在使用按需定价实例的集群中,如果实例需求增加,节点可以在没有警告的情况下被终止。

  • 调度程序启动的 Pod 驱逐:调度程序在需要时可以启动 Pod 驱逐,以优化集群上资源的使用。这可能是因为某些进程的优先级比其他进程更高,或者只是为了优化集群上的装箱。

  • 用户手动删除的 Pod。

  • 由于计划维护而删除的 Pod;例如,使用kubectl drain命令。

  • 由于网络分区,节点不再对集群可见。

  • 为了准备缩减操作而从节点中删除的 Pod。

因此,如果 Kubernetes 的设计期望 pod 是短暂的,我们如何部署可靠的应用程序呢?当然,我们需要一种无法失败地运行我们的程序的方式。幸运的是,情况并非完全如此。这种设计的重要部分是它准确地模拟了由于底层硬件和软件以及管理过程而可能发生的各种问题。Kubernetes 并不试图使基本构建块(pod)本身对故障具有弹性,而是提供了许多控制器,我们作为用户可以直接与之交互来构建具有弹性的服务。这些控制器负责为因任何原因丢失的 pod 创建替代品。

这些控制器分为四组,我们的选择取决于我们想要运行的工作负载的类型:

  • 对于我们期望结束的进程,比如批处理作业或其他有限的进程,Kubernetes 提供了作业抽象。作业确保一个 pod 至少运行一次完成。

  • 对于我们期望长时间运行的 pod,比如 web 服务器或后台处理工作者,Kubernetes 提供了部署和较低级别的 ReplicationController 或 ReplicaSet。

  • 对于我们希望在所有机器(或其中一部分)上运行的 pod,Kubernetes 提供了 DaemonSet。DaemonSet 通常用于提供作为平台一部分的特定于机器的服务,比如日志管理或监控代理,通常用于部署覆盖网络的每个节点组件。

  • 对于每个 pod 都需要稳定标识或访问持久存储的 pod 组,Kubernetes 提供了StatefulSets。(我们将在第九章中介绍StatefulSets存储状态。)

如果回想一下我们在第一章中学到的关于 Kubernetes 架构的知识,《谷歌的基础设施服务于我们其余的人》, 重要的是要记住控制器管理器(运行所有这些控制器的 Kubernetes 微服务)是一个独立的、不同的进程,与调度器分开。Kubernetes 的核心低级部分,比如调度器和 kubelet,只知道 pod,而高级控制器不需要了解实际调度和在节点上运行 pod 的任何细节。它们只是向 API 服务器发出请求创建一个 pod,而较低级的机制确保它们被正确地调度和运行。

在本章中,我们将逐步介绍作业、部署和 DaemonSet 提供给我们的重要功能和配置选项。通过一些示例,您将开始了解何时使用每个资源来部署您的应用程序。您应该花时间了解每个控制器正在做什么,以及为什么要使用它。

将软件部署到分布式环境可能会有点不同寻常,因为在部署到单台机器时可能会有很多假设不适用于分布式系统。

Kubernetes 非常擅长让我们能够部署大多数软件而无需进行任何修改。我认为 Kubernetes 让我们以一点简单性换取了很多可靠性。

直接运行 pod

Kubernetes 并不真的打算让用户直接在集群上提交和启动 pod。正如我们之前讨论的,pod 被设计为短暂存在,因此不适合运行需要确保执行已完成或需要确保进程保持运行的工作负载。

在这里,我们将从头开始,启动 pod,然后再使用控制器来帮助我们管理它们。请记住,这是一个学习练习;如果您需要它们可靠地运行,就不应该以这种方式提交 pod:

pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-loop
spec:
  containers:
  - name: loop
    image: alpine
    command: ["/bin/sh"]
    args:
    - -c
    - while true; do echo "hello world"; sleep 2s; done

这个 pod 启动了一个无限循环,每 2 秒打印一次hello world。首先使用kubectl将 pod 提交到集群中:

$ kubectl create -f pod.yaml
pod "hello-loop" created

在容器运行时下载镜像的过程中,可能需要一些时间来创建 pod。在此期间,您可以通过运行kubectl describe pod/hello-loop或使用仪表板来检查 pod 的状态。

Kubernetes 使得即使是最低级别的抽象,比如 pod,也可以通过 API 来控制,这使得使用或构建附加工具来扩展 Kubernetes 的功能变得很容易,这些工具可以和内置的控制器一样强大。

一旦 pod 启动并运行,您可以使用kubectl logs -f hello-loop来跟踪输出,您应该每 2 秒看到一个hello world的输出。

kubectl logs 允许我们显示在集群上运行的 pod 的日志。如果您知道要从中获取日志的 pod 的名称,您可以将名称作为参数传递。但是,如果您使用控制器来启动 pod,您可以使用作业或部署的名称来代替 pod 名称,只需在名称前加上资源类型。

如果您对感兴趣的 pod 或 pod 有标签选择器,可以使用 -l 标志传递它们。使用 -c 标志,您可以针对具有多个容器的 pod 中的特定命名容器进行定位;如果 pod 只有一个容器,则可以省略此选项。

尝试运行 kubectl。它可以帮助查看一些更多的选项,以便查看您感兴趣的日志,包括将其限制在特定时间段内。

作业

作业的最简单用例是启动单个 pod,并确保它成功运行完成。

在我们的下一个示例中,我们将使用 Ruby 编程语言来计算并打印出前 100 个斐波那契数:

fib.yaml apiVersion: batch/v1
kind: Job
metadata:
  name: fib
spec:
  template:
     metadata:
       name: fib
     spec:
       containers:
       - name: fib
         image: ruby:alpine
         command: ["ruby"]
         args:
         - -e
         - |
           a,b = 0,1
           100.times { puts b = (a = a+b) - b }
       restartPolicy: Never

请注意,spectemplate 的内容与我们直接启动 pod 时使用的规范非常相似。当我们为作业中的 pod 模板定义一个 pod 模板时,我们需要选择 restartPolicyNeverOnFailure

这是因为作业的最终目标是运行 pod 直到成功退出。如果基础 pod 在成功退出时重新启动,那么 pod 将继续重新启动,作业将永远无法完成。

将定义保存到文件,然后使用 kubectl create 将其提交到集群:

$ kubectl create -f fib.yaml
job "fib" created

一旦您向 Kubernetes 提交了作业,您可以使用 kubectl describe 命令来检查其状态。可能需要一点时间来下载 Docker 镜像并启动 pod。一旦 pod 运行,您应该在 Pods Statues 字段中看到首先是 1 Running,然后是 1 Succeeded

$ kubectl describe jobs/fib
Name: fib
Namespace: default
Selector: controller-uid=278fa785-9b86-11e7-b25b-080027e071f1
Labels: controller-uid=278fa785-9b86-11e7-b25b-080027e071f1
 job-name=fib
Annotations: <none>
Parallelism: 1
Completions: 1
Start Time: Sun, 17 Sep 2017 09:56:54 +0100
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed

在等待 Kubernetes 执行某些操作时,反复运行 kubectl 以了解发生了什么可能会变得乏味。我喜欢将 watch 命令与 kubectl 结合使用。要观察 Kubernetes 启动此作业,我可以运行:

**$ watch kubectl describe jobs/fib**

大多数 Linux 发行版将默认包含 watch 命令,或者可以通过软件包管理器轻松安装。如果您使用 macOS,可以通过 Homebrew 轻松安装:

**$ brew install watch**

我们可以使用kubectl logs来查看我们作业的输出。注意我们不需要知道底层 pod 的名称;我们只需要通过名称引用作业即可:

$ kubectl logs job/fib
...
83621143489848422977
135301852344706746049
218922995834555169026

我们还可以使用kubectl get查看由该作业创建的底层 pod,通过使用 Kubernetes 为我们添加到 pod 的job-name标签:

$ kubectl get pods -l job-name=fib --show-all
NAME READY STATUS RESTARTS AGE
fib-dg4zh 0/1 Completed 0 1m

--show-all标志意味着显示所有的 pod(即使那些不再具有运行状态的 pod)。

注意 Kubernetes 根据作业名称为我们的 pod 创建了一个唯一的名称。这很重要,因为如果第一个被创建的 pod 在某种方式上失败了,Kubernetes 需要根据相同的 pod 规范启动另一个 pod。

作业相对于直接启动 pod 的一个关键优势是,作业能够处理不仅是由底层基础设施引起的错误,可能导致 pod 在完成之前丢失,还有在运行时发生的错误。

为了说明这是如何工作的,这个作业模拟了一个(大部分)以非零退出状态失败的过程,但有时以(成功的)零退出状态退出。这个 Ruby 程序选择一个从 0 到 10 的随机整数并以它退出。因此,平均来说,Kubernetes 将不得不运行该 pod 10 次,直到它成功退出:

luck.yaml apiVersion: batch/v1
kind: Job
metadata:
  name: luck
spec:
  template:
    metadata:
      name: luck
    spec:
      containers:
      - name: luck
      image: ruby:alpine
      command: ["ruby"]
      args: ["-e", "exit rand(10)"]
restartPolicy: Never

像以前一样,使用kubectl将作业提交到你的集群中:

$ kubectl create -f luck.yaml
job "luck" created

除非你非常幸运,当你检查作业时,你应该看到 Kubernetes 需要启动多个 pod,直到有一个以 0 状态退出:

使用 Kubernetes 仪表板检查由 luck 作业启动的 pod

在这个例子中,pod 规范具有restartPolicyNever。这意味着当 pod 以非零退出状态退出时,该 pod 被标记为终止,作业控制器会启动另一个 pod。还可以使用restartPolicyOnFailure运行作业。

尝试编辑luck.yaml来进行这个更改。删除luck作业的第一个版本并提交你的新版本:

$ kubectl delete jobs/luck
job "luck" deleted
$ kubectl create -f luck.yaml
job "luck" created

这一次,你应该注意到,Kubernetes 不再快速启动新的 pod,直到一个成功退出,而是重启一个 pod 直到成功。你会注意到这需要更长的时间,因为当 Kubernetes 使用指数回退本地重启一个 pod 时,这种行为对于由于底层资源过载或不可用而导致的失败是有用的。你可能会注意到 pod 处于CrashLoopBackoff状态,而 Kubernetes 正在等待重新启动 pod:

$ kubectl get pods -l job-name=luck -a
NAME READY STATUS RESTARTS AGE
luck-0kptd 0/1 Completed 5 3m

允许作业控制器在每次终止时重新创建一个新的 pod,以确保新的 pod 在新的原始环境中运行,并导致作业资源保留每次执行尝试的记录。因此,通常最好不要在作业中使用 pod 重启策略,除非您必须处理定期失败的 pod,或者您希望在尝试之间保留执行环境。

CronJob

现在您已经学会了如何使用作业运行一次性或批量任务,可以简单地扩展该概念以运行定时作业。在 Kubernetes 中,CronJob是一个控制器,根据给定的计划从模板创建新的作业。

让我们从一个简单的例子开始。以下示例将每分钟启动一个作业。该作业将输出当前日期和时间,然后退出:

fun-with-cron.yaml apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: fun-with-cron
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            cronjob: fun-with-cron
        spec:
          restartPolicy: OnFailure
          containers:
          - name: how-soon-is-now
            image: alpine:3.6
            command: ["/bin/date"]

使用kubectl将 CronJob 推送到 Kubernetes:

$ kubectl apply -f fun-with-cron.yaml

过一段时间(不到一分钟),您应该会看到第一个作业被创建:

$ kubectl get jobs
NAME DESIRED SUCCESSFUL AGE
fun-with-cron-1533475680 1 1 9s

我们添加到 pod 模板规范的标签允许我们使用kubectl logs来查看 CronJob 创建的所有 pod 的输出:

$ kubectl logs -l cronjob=fun-with-cron
 Sun Aug 5 13:26:08 UTC 2018
 Sun Aug 5 13:27:08 UTC 2018
 Sun Aug 5 13:28:08 UTC 2018

Cron 语法

调度字段的语法遵循标准的 Cron 格式,如果您曾在类 Unix 系统上设置过 CronJobs,这应该是很熟悉的。Kubernetes 支持带有一些常见扩展的标准 cron 字符串。

标准的 cron 字符串由五个字段组成,每个字段代表不同的时间单位。每个字段可以设置为表示特定时间的表达式,或者通配符(*),表示匹配每个时间。例如,在月份列中的通配符将匹配每个月:

分钟 小时 月份中的日期 月份 星期中的日期

Cron 字段的顺序

如果从左到右阅读,Cron 格式最容易理解。以下是一些示例:

  • 0 * * * *:每小时整点

  • 15 * * * *:每小时 15 分钟

  • 0 0 * * *:每天午夜

  • 30 5 1 * *:每个月的第一天上午 5:30

  • 30 17 * * 1:每周一下午 3:30

除了通配符之外,还有一些其他具有特殊含义的字符。

斜杠用于指示步长:

  • 0/15 * * * *:每 15 分钟一次,从 0 开始;例如,12:00, 12:15, 12:30 等

  • 15/15 * * * *:每 15 分钟一次,从 15 开始;例如,12:15, 12:30, 12:45, 13:15, 13:30 等

  • 0 0 0/10 * *:每 10 天的午夜

连字符表示范围:

  • 0 9-17 * * *:在办公时间(上午 9 点至下午 5 点)每小时一次

  • 0 0 1-15/2 * *:每月前 15 天隔一天

逗号表示列表:

  • 0 0 * * 6,0:星期六和星期日午夜

  • 0 9,12,17 * * 1-5:上午 9:00,中午 12:00 和下午 5:00,周一至周五

为了方便阅读,月份和星期几字段可以使用名称:

  • 0 0 * * SUN:星期日午夜

  • 0 6 * MAR-MAY *:每天上午 6 点在春季

如果你不介意作业的具体运行时间,你可以指定一个固定的间隔,Kubernetes 会按固定的间隔创建作业:

  • @every 15m:每 15 分钟

  • @every 1h30m:每 1 个半小时

  • @every 12h:每 12 小时

请记住,间隔不考虑作业运行所需的时间;它只是确保每个作业计划的时间间隔由给定的间隔分隔。

最后,有几个预定义的计划可以用作 cron 字符串的快捷方式:

快捷方式 等效的 cron
@hourly 0 0 * * * * 每小时整点
@daily 0 0 0 * * * 每天午夜
@weekly 0 0 0 * * 0 每周星期日午夜
每月,每月 1 日午夜
@yearly 0 0 0 1 1 * 每年除夕午夜

并发策略

与传统的 CronJob 相比,Kubernetes CronJob 允许我们决定当作业超时并且在上一个作业仍在运行时到达计划时间时会发生什么。我们可以通过在 CronJob 上设置spec.concurrencyPolicy字段来控制这种行为。我们可以选择三种可能的策略:

  • 默认情况下,如果字段未设置,则我们将获得Allow策略。这就像传统的 CronJob 一样工作,并允许多个作业实例同时运行。如果你坚持这一点,你应该确保你的作业确实在某个时候完成,否则你的集群可能会因为同时运行许多作业而不堪重负。

  • Forbid策略防止在现有作业仍在运行时启动任何新作业。这意味着如果作业超时,Kubernetes 将跳过下一次运行。如果一个作业的两个或更多实例可能会导致冲突或使用共享资源,这是一个很好的选择。当然,你的作业需要能够处理在这种情况下缺少的运行。

  • 最后,Replace策略还可以防止多个作业同时运行,而不是跳过运行,它首先终止现有作业,然后启动新作业。

历史限制

默认情况下,当您使用 CronJob 时,它创建的作业将保留下来,因此您可以检查特定作业运行的情况以进行调试或报告。但是,当使用 CronJob 时,您可能会发现成功或失败状态的作业数量开始迅速增加。这可以通过spec.successfulJobsHistoryLimitspec.failedJobsHistoryLimit字段简单管理。一旦成功或失败的作业达到限制中指定的数量,每次创建新作业时,最旧的作业都会被删除。如果将限制设置为 0,则作业在完成后立即删除。

使用部署管理长时间运行的进程

更新批处理进程,例如作业和 CronJobs,相对较容易。由于它们的寿命有限,更新代码或配置的最简单策略就是在再次使用之前更新相关资源。

长时间运行的进程更难处理,如果您将服务暴露给网络,管理起来更加困难。Kubernetes 为我们提供了部署资源,使部署和更新长时间运行的进程变得更简单。

在第二章 启动引擎中,我们首次了解了部署资源,既可以使用kubectl run创建部署,也可以通过在 YAML 文件中定义部署对象。在本章中,我们将回顾部署控制器用于推出更改的过程,然后深入研究一些更高级的选项,以精确控制新版本的 Pod 的可用性。我们将介绍如何使用部署与服务结合,在不中断服务的情况下对网络上提供的服务进行更改。

就像 CronJob 是作业的控制器一样,部署是 ReplicaSets 的控制器。 ReplicaSet 确保特定配置所需的 Pod 的数量正常运行。为了管理对此配置的更改,部署控制器创建一个具有新配置的新 ReplicaSet,然后根据特定策略缩减旧的 ReplicaSet 并扩展新的 ReplicaSet。即使新配置的部署完成后,部署也会保留对旧 ReplicaSet 的引用。这允许部署在需要时还可以协调回滚到以前的版本。

让我们从一个示例应用程序开始,这将让您快速了解部署提供的不同选项如何允许您在更新代码或配置时操纵应用程序的行为。

我们将部署一个我创建的应用程序,以便简单地说明如何使用 Kubernetes 部署新版本的软件。这是一个简单的 Ruby Web 应用程序,位于 Docker 存储库中,有许多版本标签。每个版本在浏览器中打开主页时都会显示一个独特的名称和颜色方案。

当我们将长时间运行的进程部署到 Kubernetes 时,我们可以使用标签以受控的方式推出对应用程序的访问。

实施的最简单策略是使用单个部署来推出对应用程序新版本的更改。

为了实现这一点,我们需要首先创建一个带有标签选择器的服务,该选择器将匹配我们现在或将来可能部署的应用程序的每个版本:

service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: ver
spec:
  selector:
    app: ver
  ports:
  - protocol: TCP
    port: 80
    targetPort: http

在这种情况下,我们通过匹配具有与selector相匹配的标签app: ver的任何 pod 来实现这一点。

当运行一个更复杂的应用程序,该应用程序由多个部署管理的多个不同进程组成时,您的标签和选择器将需要更复杂。一个常见的模式是使用component标签区分应用程序的组件部分。

在开始任何 pod 之前提交服务定义是有意义的。这是因为调度程序将尽可能地尝试将特定服务使用的 pod 分布在多个节点上,以提高可靠性。

使用kubectl apply -f service.yaml将服务定义提交到您的集群。

一旦服务提交到集群,我们可以准备初始部署:

deployment.yaml apiVersion: apps/v1
kind: Deployment
metadata:
  name: versions
  labels:
    app: ver
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ver
  template:
    metadata:
      labels:
        app: ver
        version: 0.0.1
    spec:
      containers:
      - name: version-server
        image: errm/versions:0.0.1
        ports:
        - name: http
          containerPort: 3000

要访问正在运行的服务,最简单的方法是使用kubectl打开代理到运行在您的集群上的 Kubernetes API:

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

完成后,您应该能够使用浏览器在http://localhost:8001/api/v1/namespaces/default/services/ver/proxy查看应用程序。

在我们的集群中运行的版本 0.0.1

现在我们有许多方法可以对我们的部署进行更改。

kubectl patch

要升级到版本 0.0.2,我们将执行以下命令:

$ kubectl patch deployment/versions -p ' {"spec":{"template":{"spec":{"containers":[{"name":"version-server", "image":"errm/versions:0.0.2"}] }}}}'

因为容器是一个列表,我们需要为 Kubernetes 指定合并键name,以便理解我们要更新图像字段的容器。

使用patch命令,Kubernetes 执行合并,将提供的 JSON 与deployment/versions对象的当前定义进行合并。

继续在浏览器中重新加载应用程序,然后您应该会注意到(几秒钟后)应用程序的新版本变为可用。

kubectl edit

要升级到版本 0.0.3,我们将使用kubectl edit命令:

kubectl edit deployment/versions

kubectl edit使用您系统的标准编辑器来编辑 Kubernetes 资源。通常是 vi、vim,甚至是 ed,但如果您有其他更喜欢的文本编辑器,您应该设置EDITOR环境变量指向您的首选选择。

这应该会打开您的编辑器,这样您就可以对部署进行更改。一旦发生这种情况,请将图像字段编辑为使用版本 0.0.3 并保存文件。

您可能会注意到在您的编辑器中打开的对象中有比您提交给 Kubernetes 的原始文件中更多的字段。这是因为 Kubernetes 在此对象中存储有关部署当前状态的元数据。

kubectl apply

要升级到版本 0.0.4,我们将使用apply命令。这允许我们将完整的资源提交给 Kubernetes,就像我们进行初始部署时一样。

首先编辑您的部署 YAML 文件,然后将图像字段更新为使用版本 0.0.4。保存文件,然后使用kubectl将其提交到 Kubernetes:

$ kubectl apply -f deployment.yaml

如果您使用kubectl apply来创建尚不存在的资源,它将为您创建。如果您在脚本化部署中使用它,这可能会很有用。

使用kubectl apply而不是 edit 或 patch 的优势在于,您可以保持将文件提交到版本控制以表示集群状态。

Kubernetes 仪表板

Kubernetes 仪表板包括一个基于树的编辑器,允许您直接在浏览器中编辑资源。在 Minikube 上,您可以运行 Minikube 仪表板以在浏览器中打开仪表板。然后,您可以选择您的部署并单击页面顶部的编辑按钮:

您应该能够通过滚动或使用搜索功能找到容器图像字段。单击值进行编辑然后按UPDATE非常简单。

当您了解 Kubernetes 并尝试不同的配置时,您用于更新配置的方法应该是您自己的个人偏好。使用 Kubernetes 仪表板或诸如kubectl edit之类的工具非常适合学习和调试。但是,当您进入生产环境时,您将希望开始将您的配置检入版本控制,或者使用诸如 Helm(我们将在第五章中讨论的使用 Helm 管理复杂应用)之类的工具。

更好地控制您的部署

到目前为止,我们已经介绍了一些在 Kubernetes 中更新资源的方法。正如我们所观察到的,当我们在 Kubernetes 中更新部署时,集群中的 Pod 最终会更新以反映新的配置。

Kubernetes 通过在幕后管理 ReplicaSets 来实现这一点。

ReplicaSet 纯粹关注管理一组 Pod,以确保集群上运行所需数量的副本。在更新期间,现有 ReplicaSet 的 Pod 规范永远不会更改。部署控制器会使用新的 Pod 配置创建一个新的 ReplicaSet。通过改变每个 ReplicaSet 的所需副本数量来编排这种新配置的推出。

这种关注点的分离是 Kubernetes 中资源设计的典型方式。通过编排更简单的对象,其控制器实现更简单的行为来实现更复杂的行为。

这种设计还使我们(集群操作员)能够非常简单地决定在更新配置时我们想要的确切行为。spec.stratergy字段用于配置推出更改时使用的行为。

.spec.strategy.type字段定义了用于用新的 Pod 替换旧的 Pod 的策略。目前有两种策略:RecreateRollingUpdateRollingUpdate是默认策略,因此通常您不需要在配置中指定它。

滚动更新部署

.spec.strategy.type=RollingUpdate 是默认策略。这是我们迄今为止在示例中使用的策略。

当您想要在不中断服务的情况下进行更新时,您会明确选择滚动更新。相反,如果您使用此策略,您的应用程序必须在同时运行多个版本时正确工作。

在使用RollingUpdate策略时,有两个设置允许我们指定新的 ReplicaSet 如何快速扩展,旧的 ReplicaSet 如何快速缩减:

  • .spec.strategy.rollingUpdate.maxUnavailable:它指定在部署过程中可以不可用的 Pod 数量(超出所需总数)。

  • .spec.strategy.rollingUpdate.maxSurge:它指定在部署过程中可以创建的 Pod 数量,超出所需总数。

这些设置接受绝对值,例如 1 或 0,或部署中所需的总 Pod 数量的百分比。百分比值在以下情况下很有用:如果您打算使此配置可在不同级别进行扩展的不同部署中重复使用,或者如果您打算使用自动扩展机制来控制所需的 Pod 数量。

通过将maxUnavailable设置为0,Kubernetes 将等待替换的 Pod 被调度并运行,然后再杀死由旧的 ReplicationSet 管理的任何 Pod。如果以这种方式使用maxUnavailable,那么在部署过程中,Kubernetes 将运行超出所需数量的 Pod,因此maxSurge不能为0,并且您必须具有所需的资源(在集群中和用于后备服务)来支持在部署阶段临时运行额外的实例。

一旦 Kubernetes 启动了所有实例,它必须等待新的 Pod 处于服务状态并处于Ready状态。这意味着如果您为 Pod 设置了健康检查,如果这些检查失败,部署将暂停。

如果maxSurge和/或maxUnavailable设置为较低的值,部署将需要更长时间,因为部署将暂停并等待新的 Pod 可用后才能继续。这是有用的,因为它可以在部署损坏的代码或配置时提供一定程度的保护。

maxSurge设置为更大的值将减少部署更新应用程序所需的扩展步骤的数量。例如,如果将maxSurge设置为 100%,maxUnavailable设置为 0,那么 Kubernetes 将在部署开始时创建所有替换的 Pod,并在新的 Pod 进入 Ready 状态时杀死现有的 Pod。

确切地配置部署将取决于应用程序的要求和集群可用的资源。

您应该记住,将maxSurge设置为较低的值将使部署速度较慢,需要更长的时间来完成,但可能更具有错误的弹性,而较高的maxSurge值将使您的部署进展更快。但您的集群需要具有足够的容量来支持额外的运行实例。如果您的应用程序访问其他服务,您还应该注意可能对它们施加的额外负载。例如,数据库可以配置为接受的连接数量有限。

重新创建部署

.spec.strategy.type=Recreate采用了一种更简单的方法来推出对应用程序的更改。首先,通过缩减活动的 ReplicaSet 来终止具有先前配置的所有 pod,然后创建一个启动替换 pod 的新 ReplicaSet。

当您不介意短暂的停机时间时,这种策略特别合适。例如,在后台处理中,当工作程序或其他任务不需要提供通过网络访问的服务时。在这些用例中的优势是双重的。首先,您不必担心由同时运行两个版本的代码引起的任何不兼容性。其次,当然,使用这种策略更新您的 pod 的过程不会使用比您的应用程序通常需要的更多资源。

DaemonSet

如果您希望特定 pod 的单个实例在集群的每个节点(或节点的子集)上运行,则需要使用 DaemonSet。当您将 DaemonSet 调度到集群时,您的 pod 的一个实例将被调度到每个节点,并且当您添加新节点时,该 pod 也会被调度到那里。DaemonSet 非常适用于提供需要在集群的每个地方都可用的普遍服务。您可能会使用 DaemonSet 来提供以下服务:

  • 用于摄取和传送日志的代理,如 Fluentd 或 Logstash

  • 监控代理,如 collectd、Prometheus Node Exporter、datadog、NewRelic 或 SysDig 等

  • 用于分布式存储系统的守护程序,如 Gluster 或 Ceph

  • 用于覆盖网络的组件,如 Calico 或 Flannel

  • 每个节点组件,如 OpenStack 虚拟化工具

在 Kubernetes 之前,这些类型的服务将要求您在基础设施中的每台服务器上配置一个 init 系统,例如systemd或 SysVnit。当您要更新服务或其配置时,您将不得不更新该配置并重新启动所有服务器上的服务,当您管理少量服务器时,这并不是问题,但是当您有数十、数百甚至数千台服务器时,事情很快变得更加难以管理。

DaemonSet 允许您使用与您正在管理基础设施的应用程序相同的配置和容器化。

让我们看一个简单的例子,以了解如何为有用的目的创建一个 DaemonSet。我们将部署 Prometheus Node Exporter。这个应用程序的目的是公开一个包含有关其正在运行的 Linux 系统的指标的 HTTP 端点。

如果您决定监视您的集群,Prometheus Node Exporter 是一个非常有用的工具。如果您决定在自己的集群中运行它,我建议您查看 GitHub 页面上提供的广泛文档github.com/prometheus/node_exporter

这个清单会导致在模板部分指定的 pod 被调度到您集群中的每个节点上:

node-exporter.yaml 
apiVersion: apps/v1 
kind: DaemonSet 
metadata: 
  labels: 
    app: node-exporter 
  name: node-exporter 
spec: 
  selector: 
    matchLabels: 
      app: node-exporter 
  template: 
    metadata: 
      labels: 
        app: node-exporter 
    spec: 
      containers: 
      - name: node-exporter 
        image: quay.io/prometheus/node-exporter:v0.15.2 
        args: 
        - --path.procfs=/host/proc 
        - --path.sysfs=/host/sys 
        volumeMounts: 
        - mountPath: /host/proc 
          name: proc 
          readOnly: false 
        - mountPath: /host/sys 
          name: sys 
          readOnly: false 
        ports: 
        - containerPort: 9100 
          hostPort: 9100 
      hostNetwork: true 
      hostPID: true 
      volumes: 
      - hostPath: 
          path: /proc 
        name: proc 
      - hostPath: 
          path: /sys 
        name: sys 

一旦您准备好 Node Exporter 的清单文件,通过运行kubectl apply -f node-exporter.yaml命令将其提交到 Kubernetes。

您可以通过运行kubectl describe ds/node-exporter命令来检查 DaemonSet 控制器是否已正确将我们的 pod 调度到集群中的节点。假设 pod 成功运行,您应该能够在其中一个节点的端口9100上发出 HTTP 请求,以查看其公开的指标。

如果您在 Minikube 上尝试此示例,可以通过运行minikube ip来发现集群中(唯一)节点的 IP 地址。

然后您可以使用curl等工具发出请求:

**curl 192.168.99.100:9100/metrics**

使用 DaemonSet 来管理基础设施工具和组件的一个关键优势是,它们可以像您在集群上运行的任何其他应用程序一样轻松更新,而不是依赖于节点上的静态配置来管理它们。

默认情况下,DaemonSet 具有updateStrategyRollingUpdate。这意味着如果您编辑了 DaemonSet 中的 pod 模板,当前在集群上运行的现有 pod 将被逐个杀死并替换。

让我们尝试使用此功能来升级到 Prometheus Node Exporter 的新版本:

kubectl set image ds/node-exporter node-exporter=quay.io/prometheus/node-exporter:v0.16.0

您可以通过运行kubectl rollout status ds/node-exporter命令来查看替换旧版本的 pod 的进度。一旦更新完成,您应该会看到以下消息:daemon set "node-exporter" successfully rolled out

您可能想知道 DaemonSet 还有哪些其他updateStrategys可用。唯一的其他选项是OnDelete。使用此选项时,当 DaemonSet 更新时,不会对集群上运行的现有 pod 进行任何更改,您需要手动删除运行的 pod,然后再启动新版本。这主要是为了与 Kubernetes 先前版本中的行为兼容,并且在实践中并不是非常有用。

值得注意的是,为了部署一个带有 DaemonSet 的新版本的 pod,旧的 pod 被杀死并启动新的 pod 之间会有一个短暂的时间,在此期间您运行的服务将不可用。

DaemonSet 也可以用于在集群中的节点子集上运行 pod。这可以通过为集群中的节点打标签并在 DaemonSet 的 pod 规范中添加nodeSelector来实现:

... 
    spec: 
      nodeSelector: 
        monitoring: prometheus 
      containers: 
      - name: node-exporter 
... 

一旦您编辑了清单以添加nodeSelector,请使用以下命令将新配置提交给 Kubernetes:kubectl apply -f node-exporter.yaml

您应该注意到正在运行的节点导出器 pod 被终止并从集群中删除。这是因为您的集群中没有节点与我们添加到 DaemonSet 的标签选择器匹配。可以使用kubectl动态地为节点打标签。

kubectl label node/<node name> monitoring=prometheus      

一旦节点被正确标记,您应该注意到 DaemonSet 控制器会将一个 pod 调度到该节点上。

在 AWS 上,节点会自动带有标签,包括区域、可用区、实例类型和主机名等信息。您可能希望使用这些标签将服务部署到集群中的特定节点,或者为集群中不同类型的节点提供不同配置版本的工具。

如果您想添加额外的标签,可以使用--node-labels标志将它们作为参数传递给 kubelet。

总结

在本章中,我们学习了如何使用 Kubernetes 来运行我们的应用程序,以及如何推出新版本的应用程序和它们的配置。

我们在前几章基础知识的基础上构建了对 pod 和部署的了解。

  • Pod 是 Kubernetes 提供给我们的最低级抽象。

  • 所有其他处理容器运行的资源,如作业、ScheduledJobs、部署,甚至 DaemonSet,都是通过特定方式创建 pod 来工作的。

  • 通常,我们不希望直接创建 pod,因为如果运行 pod 的节点停止工作,那么 pod 也将停止工作。使用其中一个高级控制器可以确保创建新的 pod 来替换失败的 pod。

  • 高级资源,如部署和 DaemonSet,提供了一种以受控方式用不同版本的 pod 替换另一个版本的机制。我们了解了可用于执行此操作的不同策略。

在进入下一章之前,花点时间通过观察部署过程中它们的行为来了解每种部署策略的工作方式。通过一些经验,您将了解在给定应用程序中选择哪些选项。

在下一章中,我们将学习如何使用一个工具,该工具基于这些概念提供了更强大的部署和更新应用程序的方式。

第五章:使用 Helm 管理复杂的应用程序

在之前的章节中,您开始学习如何构建和部署所需的配置,以在您的 Kubernetes 集群上运行不同的应用程序。

一旦您超越了部署最简单的应用程序,您会发现您的应用程序通常有一个或多个组件是协同工作的。例如,您可能有一个 Web 应用程序,它从数据库中显示信息,该数据库还使用定期作业定期更新该信息。为了使该应用程序能够正确运行,这两个组件都需要被部署并正确运行。此外,这两个组件可能共享一些配置,例如后端数据库的凭据。

在将应用程序部署到我们的 Kubernetes 集群时,我们可能会遇到另一个问题,即可重用性的问题。也许我们需要在多个上下文或环境中运行相同的工具或应用程序。例如,许多组织都有一个用于测试软件新版本的暂存环境。

在维护多个环境时,我们理想情况下希望每个环境中的配置尽可能匹配,但当然,配置中可能需要一些差异。维护每个环境的 Kubernetes 清单的多个副本可能会出错,并且不能保证在一个环境中运行的应用程序在另一个环境中也能正常工作。

Helm 是 Kubernetes 生态系统中的一款流行工具,它解决了这些问题。它为我们提供了一种构建相关 Kubernetes 对象的软件包(称为图表)的方式,可以以一种协调的方式部署到集群中。它还允许我们对这些软件包进行参数化,以便在不同的上下文中重复使用,并部署到可能需要这些服务的不同环境中。

与 Kubernetes 一样,Helm 的开发由 Cloud Native Computing Foundation 监督。除了 Helm(软件包管理器)之外,社区还维护了一个标准图表的存储库,用于安装和运行各种开源软件,例如 Jenkins CI 服务器、MySQL 或 Prometheus,使用 Helm 可以简单地安装和运行涉及许多基础 Kubernetes 资源的复杂部署。

在本章中,您将学习:

  • 如何安装helm命令行工具

  • 如何安装 Helm 的集群组件 Tiller

  • 如何使用社区维护的图表将服务部署到您的集群

  • 创建图表时需要了解的语法

  • 如何在自己的图表存储库中托管图表,以便在组织内或更广泛地共享您的图表

  • 将 Helm 图表集成到您自己的部署流程的策略

安装 Helm

如果您已经设置了自己的 Kubernetes 集群,并且在您的机器上正确配置了 kubectl,那么安装 Helm 就很简单。

macOS

在 macOS 上,安装 Helm 客户端的最简单方法是使用 Homebrew:

$ brew install kubernetes-helm  

Linux 和 Windows

Helm 的每个版本都包括 Linux、Windows 和 macOS 的预构建二进制文件。访问github.com/kubernetes/helm/releases下载您所需的平台版本。

要安装客户端,只需解压并将二进制文件复制到您的路径上。

例如,在 Linux 机器上,您可能会执行以下操作:

$ tar -zxvf helm-v2.7.2-linux-amd64.tar.gz
$ mv linux-amd64/helm /usr/local/bin/helm  

安装 Tiller

一旦您在您的机器上安装了 Helm CLI 工具,您可以开始安装 Helm 的服务器端组件 Tiller。

Helm 使用与 kubectl 相同的配置,因此首先检查您将要安装 Tiller 的上下文:

$ kubectl config current-context
minikube  

在这里,我们将把 Tiller 安装到 Minikube 上下文引用的集群中。在这种情况下,这正是我们想要的。如果您的 kubectl 当前没有指向另一个集群,您可以快速切换到您想要使用的上下文,就像这样:

$ kubectl config use-context minikube  

如果您仍然不确定是否使用了正确的上下文,请快速查看完整配置,并检查集群服务器字段是否正确:

$ kubectl config view --minify=true  

minify 标志会删除当前上下文中未引用的任何配置。一旦您确认 kubectl 连接的集群是正确的,我们可以设置 Helm 的本地环境并将 Tiller 安装到您的集群上:

$ helm init
$HELM_HOME has been configured at /Users/edwardrobinson/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!  

我们可以使用 kubectl 来检查 Tiller 是否确实在我们的集群上运行:

$ kubectl -n kube-system get deploy -l app=helm
NAME            DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
tiller-deploy   1         1         1            1           3m  

一旦我们验证了 Tiller 在集群上正确运行,让我们使用 version 命令。这将验证我们能够正确连接到 Tiller 服务器的 API,并返回 CLI 和 Tiller 服务器的版本号:

$ helm version
Client: &version.Version{SemVer:"v2.7.2", GitCommit:"8478fb4fc723885b155c924d1c8c410b7a9444e6", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.7.2", GitCommit:"8478fb4fc723885b155c924d1c8c410b7a9444e6", GitTreeState:"clean"}

安装图表

让我们首先通过使用社区提供的图表之一来安装一个应用程序。

您可以在hub.kubeapps.com/发现社区为 Helm 图表制作的应用程序。除了简化将各种应用程序部署到 Kubernetes 集群中,这也是一个学习社区在为 Helm 打包应用程序时使用的一些最佳实践的好资源。

Helm 图表可以存储在存储库中,因此可以通过名称简单安装它们。默认情况下,Helm 已配置为使用一个名为Stable的远程存储库。

这使我们可以在安装 Helm 后立即尝试一些常用的应用程序。

在安装图表之前,您需要了解三件事:

  • 要安装的图表的名称

  • 要为此发布指定的名称(如果省略此项,Helm 将为此发布创建一个随机名称)

  • 要安装图表的集群上的命名空间(如果省略此项,Helm 将使用默认命名空间)

Helm 将特定图表的每个不同安装称为一个发布。每个发布都有一个唯一的名称,如果以后要更新、升级或删除集群中的发布,将使用该名称。能够在单个集群上安装多个图表实例使 Helm 与我们对传统软件包管理器的想法有些不同,传统软件包管理器通常与单台机器绑定,并且通常一次只允许安装一个特定软件包。但一旦您习惯了这些术语,就会非常容易理解:

  • 图表是包含有关如何将特定应用程序或工具安装到集群的所有信息的软件包。您可以将其视为一个模板,可重复使用以创建打包的应用程序或工具的许多不同实例或发布。

  • 发布是将图表命名安装到特定集群的过程。通过按名称引用发布,Helm 可以对特定发布进行升级,更新已安装工具的版本或进行配置更改。

  • 存储库是存储图表以及索引文件的 HTTP 服务器。当配置了存储库的位置后,Helm 客户端可以通过从该存储库下载图表然后创建一个新的发布来安装该图表。

在将图表安装到集群之前,您需要确保 Helm 知道您要使用的存储库。您可以通过运行helm repo list命令列出当前使用的存储库:

$ helm repo list
NAME   URL
stable https://kubernetes-charts.storage.googleapis.com
local  http://127.0.0.1:8879/charts  

默认情况下,Helm 配置了一个名为 stable 的存储库,指向社区图表存储库,以及一个指向本地地址的本地存储库,用于测试您自己的本地存储库(您需要运行helm serve)。

使用helm repo add命令将 Helm 存储库添加到此列表非常简单。您可以通过运行以下命令添加包含与本书相关的一些示例应用程序的 Helm 存储库:

$ helm repo add errm https://charts.errm.co.uk
"errm" has been added to your repositories  

为了从配置的存储库中获取最新的图表信息,您可以运行以下命令:

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...Successfully got an update from the "errm" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete.  Happy Helming!

让我们从我的 Helm 存储库中提供的最简单的应用程序之一kubeslate开始。这提供了有关您的集群的一些非常基本的信息,例如您正在运行的 Kubernetes 版本以及您的集群中的 Pod、部署和服务的数量。我们将从这个应用程序开始,因为它非常简单,不需要任何特殊的配置来在 Minikube 上运行,或者在任何其他集群上运行。

从集群上的存储库安装图表非常简单:

$ helm install --name=my-slate errm/kubeslate  

您应该会看到来自helm命令的大量输出。

首先,您将看到有关发布的一些元数据,例如其名称、状态和命名空间:

NAME:   my-slate
LAST DEPLOYED: Mon Mar 26 21:55:39 2018
NAMESPACE: default
STATUS: DEPLOYED  

接下来,您应该会看到有关 Helm 已指示 Kubernetes 在集群上创建的资源的一些信息。正如您所看到的,已创建了一个服务和一个部署:

RESOURCES:
==> v1/Service
NAME                TYPE       CLUSTER-IP     PORT(S)  AGE
my-slate-kubeslate  ClusterIP  10.100.209.48  80/TCP   0s

==> v1/Deployment
NAME                DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
my-slate-kubeslate  2        0        0           0          0s

==> v1/Pod(related)
NAME                                 READY  STATUS             AGE
my-slate-kubeslate-77bd7479cf-gckf8  0/1    ContainerCreating  0s
my-slate-kubeslate-77bd7479cf-vvlnz 0/1 ContainerCreating 0s

最后,有一个部分包含图表作者提供的一些注释,以便为我们提供有关如何开始使用应用程序的一些信息:

注意

访问kubeslate

  1. 首先启动 kubectl 代理:

**kubectl proxy**

  1. 现在在浏览器中打开以下 URL:

**http://localhost:8001/api/v1/namespaces/default/services/my-slate-kubeslate:http/proxy**

如果您看到ServiceUnavailable / no endpoints available for service,请尝试重新加载页面,因为 Pod 的创建可能需要一些时间。

尝试按照这些说明自己打开 Kubeslate 在浏览器中。

使用 Helm 部署的 Kubeslate

配置图表

当您使用 Helm 发布图表时,可能需要更改某些属性或提供配置。幸运的是,Helm 为图表的用户提供了一种标准的方式来覆盖一些或所有的配置值。

在本节中,我们将看看作为图表用户,您可能如何向 Helm 提供配置。在本章的后面,我们将看看如何创建自己的图表,并使用传递的配置来允许您的图表进行自定义。

当我们调用helm install时,有两种方式可以提供配置值:将它们作为命令行参数传递,或者提供一个配置文件。

这些配置值与图表提供的默认值合并。这使得图表作者可以提供默认配置,让用户快速上手,但仍然允许用户调整重要设置或启用高级功能。

使用 set 标志在命令行上向 Helm 提供单个值。 kubeslate图表允许我们使用podLabels变量为其启动的 pod(s)指定附加标签。让我们发布 kubeslate 图表的新版本,然后使用podLabels变量添加一个额外的hello标签,其值为world

$ helm install --name labeled-slate --set podLabels.hello=world errm/kubeslate

运行此命令后,您应该能够证明您传递给 Helm 的额外变量确实导致 Helm 启动的 pod 具有正确的标签。使用带有我们使用 Helm 应用的标签的标签选择器的kubectl get pods命令应返回刚刚使用 Helm 启动的 pod:

$ kubectl get pods -l hello=world
NAME                                      READY     STATUS
labeled-slate-kubeslate-5b75b58cb-7jpfk   1/1       Running
labeled-slate-kubeslate-5b75b58cb-hcpgj   1/1       Running  

除了在创建新版本时能够向 Helm 传递配置外,还可以使用升级命令更新预先存在的版本的配置。当我们使用 Helm 更新配置时,这个过程与上一章中更新部署资源时的过程大致相同,如果我们想要避免服务中断,许多考虑因素仍然适用。例如,通过启动服务的多个副本,我们可以避免停机时间,因为部署配置的新版本会被推出。

让我们还将我们原始的 kubeslate 发布升级,以包括我们应用于第二个发布的相同的hello: world pod标签。正如您所看到的,upgrade命令的结构与install命令非常相似。但是,我们不是使用--name标志指定发布的名称,而是将其作为第一个参数传递。这是因为当我们将图表安装到集群时,发布的名称是可选的。如果我们省略它,Helm 将为发布创建一个随机名称。但是,当执行升级时,我们需要针对要升级的现有发布,因此此参数是必需的:

$ helm upgrade my-slate --set podLabels.hello=world errm/kubeslate  

如果您现在运行helm ls,您应该会看到名为my-slate的发布已经升级到 Revision 2。您可以通过重复我们的kubectl get命令来测试由此发布管理的部署是否已升级以包括此 pod 标签:

$ kubectl get pods -l hello=world
NAME                                      READY     STATUS
labeled-slate-kubeslate-5b75b58cb-7jpfk   1/1       Running
labeled-slate-kubeslate-5b75b58cb-hcpgj   1/1       Running
my-slate-kubeslate-5c8c4bc77-4g4l4        1/1       Running
my-slate-kubeslate-5c8c4bc77-7pdtf        1/1       Running  

我们现在可以看到,我们的四个发布中的每个都有两个 pod,现在都与我们传递给kubectl get的标签选择器匹配。

使用set标志在命令行上传递变量在我们只想为一些变量提供值时很方便。但是,当我们想要传递更复杂的配置时,将值提供为文件可能更简单。让我们准备一个配置文件,将几个标签应用到我们的 kubeslate pod 上:

values.yml 
podLabels: 
  hello: world 
  access: internal 
  users: admin 

然后,我们可以使用helm命令将此配置文件应用到我们的发布上:

$ helm upgrade labeled-slate -f values.yml errm/kubeslate  

创建您自己的图表

现在您已经有了一点 Helm 的经验,并且可以使用命令行工具从社区存储库安装图表,我们将看看如何利用 Helm 为自己的应用程序构建图表。

我们将使用 Helm 来部署我们在第四章中手动部署的 versions 应用程序。这里的目标是复制我们在第四章中进行的部署,但这次将配置封装在 Helm 图表中,以便简单地进行配置更改,部署我们代码的新版本,甚至多次部署相同的配置。

Helm 使构建图表并将其部署到集群变得非常容易。Helm 命令行工具有一些命令,可以让我们非常快速地开始。helm create命令将为我们的新图表创建一个骨架,我们可以快速填写应用程序的配置。

$ helm create version-app
Creating version-app

$ tree version-app
version-app
├── Chart.yaml
├── values.yaml
└── templates
    ├── NOTES.txt
    ├── _helpers.tpl
    ├── deployment.yaml
    └── service.yaml

2 directories, 7 files  

让我们看看 Helm 创建的每个文件,然后看看我们需要添加的配置,以部署我们的版本化 Web 服务来自第四章,管理应用程序中的变更

Chart.yaml

该文件包含有关此图表的一些基本元数据,例如名称、描述和版本号。此文件是必需的。

values.yaml

该文件包含此图表的默认配置值。这些值将在安装图表时用于渲染模板资源,除非提供了覆盖值。

模板

该目录包含将被渲染以生成此图表提供的资源定义的模板。当我们运行helm new命令时,会为我们创建一些骨架模板文件。

NOTES.txt是一个特殊文件,用于向您的图表用户提供安装后的消息。在本章早些时候,当我们安装 kube-ops-dashboard 时,您看到了一个示例。

与我们在之前章节手工创建的 YAML 资源一样,Helm 不会对我们为资源指定的文件名附加任何重要性。如何组织模板目录中的资源取决于您。我们刚刚创建的骨架图表带有一些文件,但如果您需要创建更多资源,可以向模板目录添加其他文件。

deployment.yaml包含一个部署的简单清单,service.yaml包含此部署的简单服务清单,_helpers.tpl包含一些预定义的辅助函数,您可以在整个图表中重复使用。

当您运行helm new时,可能会创建一些其他文件。这些是用于一些更高级功能的可选文件,我们现在可以忽略它们,但如果您愿意,可以从图表中完全删除它们。

有一些标准的模板目录工作方式,遵循社区图表存储库中的规则。您可能希望查看这些规则,因为它们有助于保持您的工作有条不紊。但除非您计划尝试将您的图表发布到社区存储库,否则没有必要严格遵循这些准则:docs.helm.sh/chart_best_practices

使其成为您自己的

让我们按照以下步骤来编辑这个图表,以便部署我们自己的应用程序。首先看一下生成的 deployment.yaml 文件。您会注意到它看起来非常类似于我们在第四章中生成的清单,管理应用程序中的变更,但有一个重要的区别:所有特定的配置值都已经替换为变量调用。例如,看一下指定容器镜像的那一行:

image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 

您会注意到,当将变量的引用插入模板时,它被两个花括号括起来,就像这样:{{ variable }}。其次,您还会注意到用于访问对象的嵌套属性的点表示法。.Values 对象指的是所有的值,可以是从图表中的 values.yaml 文件(默认)提供的,也可以是在部署图表时从命令行覆盖的。

因此,为了配置我们部署中要使用的图像的源,让我们从编辑 values.yaml 文件开始。找到配置图像的部分,并编辑以拉取我们在第四章中部署的应用程序的版本:

image: 
  repository: errm/versions 
  tag: 0.0.1 
  pullPolicy: IfNotPresent 

在编辑 values.yaml 文件的同时,让我们也编辑用于配置 Helm 为我们的部署创建的服务的值。我们需要将容器暴露的端口从 80 更改为 3000,并且我们应该将服务的名称从 nginx 更改为更具描述性的名称:

service: 
  name: versions 
  type: ClusterIp 
  externalPort: 80 
  internalPort: 3000 

如果我们回过头来看 deployment.yamlservice.yaml,我们可以看到能够在我们的 Kubernetes 资源中使用模板注入变量的一个优势。

通过在 values.yaml 文件中更改 service.internalPort 的值,我们有了一个单一的真相来源;在这种情况下,就是我们的容器暴露的端口。这个单一的真相来源在 deployment.yaml 中被使用了三次,然后又在 service.yaml 中被使用了一次。当然,对于这样一个简单的例子,我们可以手动编辑这些文件,但这会增加维护配置的成本,需要搜索多个资源,并理解不同配置值之间的交互方式。

当我构建 Helm 图表时,我试图想象我的未来自己使用该图表。我的目标是暴露足够的变量,使图表足够灵活,可以在多个环境中重复使用和重新部署,而无需更改或甚至查看模板。为了实现这一点,选择描述性变量名称并为这些变量的使用提供清晰的文档非常重要

README.md文件。

使用 Helm 命令行客户端部署我们的图表非常简单,而不是引用远程存储库中图表的名称(例如,stable/kube-ops-view)。我们可以通过指向磁盘上的图表目录来运行我们的 Helm 命令:

$ helm install --name happy-bear version-app/
NAME:   happy-bear
LAST DEPLOYED: Sun Dec  3 13:22:13 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME                    TYPE       CLUSTER-IP  EXTERNAL-IP  PORT(S)
happy-bear-version-app  ClusterIP  10.0.0.121  <none>       80/TCP

==> v1/Deployment
NAME                    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE
happy-bear-version-app  1        1        1           0

==> v1/Pod(related)
NAME                                     READY  STATUS
happy-bear-version-app-6597799867-ct5lk  0/1    ContainerCreating

现在图表已安装到我们的集群上,让我们测试它是否正常工作。最简单的方法是运行kubectl proxy来设置到 kubernetes API 的本地代理,并使用服务端点来查看我们的服务。Helm 为我们创建的图表创建了一个服务,其名称由发布的名称和图表的名称组合而成。因此,假设kubectl proxy在端口8001上启动,我们应该能够在以下 URL 查看我们的服务:http://localhost:8001/api/v1/namespaces/default/services/happy-bear-version-app:80/

开发和调试

随着我们的图表变得更加复杂,并且利用 Helm 提供的模板语言的更多功能来构建我们自己的抽象层,您可能会注意到,要推理 Kubernetes 返回的错误变得更加困难。因为我们无法直接看到我们提交给 Kubernetes 的资源,因此很难找出错误或错误配置的来源。

幸运的是,Helm 有一些选项可以帮助我们在开发图表时调试它们:

  • --dry-run:此选项允许我们将图表提交到 Tiller 服务器,在那里它将以与我们部署图表时完全相同的方式进行验证,而无需实际提交资源到 Kubernetes。这样我们可以快速查看和理解图表中的任何错误,而无需在我们的集群上使用资源。

  • --debug:此选项允许我们查看大量有用的调试信息;实际上,有时可能会有点压倒性。首先,我们看到一些标记为[debug]的日志信息。这包括有关 Helm 客户端如何连接到 Tiller 以及正在部署的图表的一些详细信息。

接下来是发布元数据。这由Chart.yaml中的图表元数据和有关发布的计算信息组成,例如其编号以及制作日期和时间。

下一节,“计算值”,显示了 Helm 在渲染模板以生成此版本的资源时将使用的确切值。如果在发布时没有传递任何额外的变量,这应该与values.yaml的内容相同,但如果您在调用 Helm 时提供了覆盖,这将非常有用,以便了解模板使用的确切变量。 HOOKS部分显示了将由 Helm 钩子机制创建的资源。您将在本章后面了解有关钩子的一些信息。

最后,MANIFEST部分列出了计算资源,因为它们将被提交到 Kubernetes。当您开发图表模板时,这是非常宝贵的,可以快速查看您的图表在不同值下的行为。您会发现,将这两个选项与helm installhelm upgrade一起使用非常有用,用于调试您的图表,以及验证您的工作并建立对图表或值的更改是否产生预期效果的信心。

模板语言

Helm 的模板语言基于 Go 模板语言。基本上,Helm 提供了来自 Go 编程语言的标准模板语言,以及一些额外的函数和使变量在模板内可用的机制。

您已经看到如何使用模板语言将信息放入 YAML 格式的 Kubernetes 资源中。Helm 提供的函数调用用双花括号括起来,如{{ this }}

如果我们只是想将变量包含到我们的模板中,我们可以直接按名称引用它。Helm 将其变量命名空间化在一些对象内,这些对象暴露给模板。您可能已经注意到,我们的values.yaml文件中的值(由命令行传入的任何覆盖变量修改)在.Values对象中可用。除了这个对象,Helm 还在模板内提供了其他对象:

  • .Release: 此对象描述了发布本身,并包括许多属性,可用于自定义资源以适应其父发布。通常,您将使用这些值来确保此发布的资源不会与同一图表的另一个发布的资源发生冲突。

  • .Release.Name: 这是发布的名称。它可以通过helm install传递给--name标志,或者可能会自动生成。

  • .Release.Time.Seconds: 这是发布创建时的时间,作为 UNIX 风格的时间戳。如果您需要向资源名称添加唯一值,这可能很有用。

  • .Release.Namespace: 这表示此发布的 Kubernetes 命名空间。

  • .Release.Service: 这表示进行发布的服务。目前,这始终是 Tiller,但如果 Helm 有另一种实现,可能会以不同的方式填充此属性。

  • .Release.Revision: 这是一个用于跟踪发布更新的数字。它从 1 开始,每次通过helm upgrade升级发布时都会增加。

  • .Release.IsUpgrade.Release.IsInstall: 这些是布尔值,指示生成此发布的操作是图表的新安装,还是对现有发布的升级。这些可能被用于仅在图表生命周期的特定阶段执行操作。

  • .Chart: 图表对象包含来自Chart.yaml的字段。

  • .Files: 此对象允许您访问图表中包含的非模板文件的内容。它公开了两个函数,.Get.GetBytes,允许您以文本或字节的形式读取文件的内容。这对于提供静态配置文件或其他未包含在容器映像中的数据作为图表的一部分可能很有用。

  • .Capabilities: 此对象提供有关 Tiller 正在运行的集群的信息。如果要创建一个可以与多个版本的 Kubernetes 一起使用的图表,查询此信息可能很有用。您将在本章后面看到一个示例。

  • .Template: 此对象提供了一个.Name和一个.BasePath属性,其中包括 Helm 当前正在呈现的模板的文件名和目录。

函数

Helm 的模板语言提供了超过 60 个函数,可以操作和格式化我们传递给模板的数据。

其中一些函数是 Go 模板语言的一部分,但大多数是 Sprig 模板语言的一部分。

当您开始使用 Helm 时,随手准备文档可能会很有用,这样您就可以找到所需的函数。

在 Helm 模板语言中调用模板函数有两种方式。其中之一涉及调用一个函数,并将一个值作为参数传递。

例如,{{ upper "hello" }}将产生输出HELLO

调用函数的第二种方式是作为管道。您可以将管道想象成类似于 UNIX 管道;它提供了一种简洁的方式将一个函数的结果传递给另一个函数。这使我们能够组合多个函数以获得我们想要的结果。

我们可以将我们的第一个示例重写为{{ "hello" | upper }},结果将完全相同。这种形式的优势在于当我们想要对一个值应用多个函数时。当我们使用管道运算符时,上一个函数的结果被传递到下一个函数作为最后一个参数。这使我们还可以调用需要多个参数的函数,并且这也是 Helm 中大多数函数被优化为将要操作的值作为最后一个参数的原因。

例如,我们可以使用trunc函数形成一个流水线,将我们的字符串截断为一定数量的字符,然后使用upper函数将结果转换为大写,就像这样:{{ "hello" | trunc 4 | upper }}。当然,结果将是HELL

流程控制

通过能够从图表中获取单个值并将其包含在图表的许多地方,我们已经从 Helm 中获得了很多价值,就像本章前面的示例中,我们在几个相关的地方引用了相同的端口号。例如,您还可以使用此技术来确保由不同容器提供的多个不同组件的系统始终部署到相同的版本号。

我们在 Helm 图表中使用变量的另一个重要方式是为我们的模板提供信号,以更改我们的配置,甚至将整个功能转换为可选的附加功能,可能并非始终启用。

有三种结构允许我们使用 Helm 模板构建非常强大的抽象:if...elserangewith

Helm 中if...else结构的结构对于使用过编程语言的人来说应该非常熟悉。我们使用if关键字来测试变量或表达式。如果测试通过,我们执行第一个分支中的操作;如果不通过,则退回到else分支指示的操作。

以下是一个示例,您可以根据变量的值在NOTES.txt模板中提供自定义消息:

{{ if .Values.production }} 
WARNING THIS IS A PRODUCTION ENVIRONMENT - do not use for testing. 
{{ else }} 
THIS IS A TEST ENVIRONMENT; any data will be purged at midnight. 
{{ end }} 

if函数可以嵌套在else分支中,以提供更复杂的行为。在这个例子中,查询Capabilities对象,以便模板化资源可以为CronJob资源使用正确的 API 版本。这种能力很有用,因为它允许您更改配置以支持 Kubernetes 的更新版本,但保持向后兼容性。如果我们对支持的版本进行的两个测试都失败了,那么我们明确地抛出一个错误,这将停止图表的安装:

{{- if and ge .Capabilities.KubeVersion.Minor "8" -}} 
apiVersion: batch/v1beta1
 {{- else if ge .Capabilities.KubeVersion.Minor "5" -}} 
apiVersion: batch/v1alpha1 
{{- else -}} 
{{required "Kubernetes version 1.5 or higher required" nil }} 
{{- end -}} 

像这样围绕基于特性标志或甚至版本号的配置提供切换是管理配置中变化的非常有用的工具。它允许您向图表添加一个选项,在安全性中测试它,然后只有在您满意时才启用它。

range关键字用于循环遍历集合。它可以循环遍历简单列表或具有键值结构的集合。

让我们首先在我们的values.yaml文件中添加一个值列表:

users: 
  - yogi 
  - paddington 
  - teddy 

然后我们可以使用range关键字来循环遍历我们列表中的数据,并在我们的模板中使用值:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: {{ .Release.Name }}-configmap 
data: 
  usernames: |- 
    {{- range .Values.users }} 
    {{ . }} 
    {{- end }} 

在这个例子中,我们使用了|-标记,这是 YAML 的一部分。它表示用户名字符串是多行的。这将导致每个用户名在ConfigMap中以新行分隔。

正如您在这里看到的,当我们在列表上使用 range 函数时,在每次迭代中,特殊的.变量都会被列表中的值替换。

渲染时,此模板产生以下结果:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: ordered-dolphin-configmap 
data: 
  usernames: |- 
    yogi 
    paddington 
    teddy 

在下一个示例中,我们将把 range 函数的结果分配给两个变量。当我们对列表这样做时,第一个变量包括一个索引,您会注意到当我们分配一个变量时,我们会用$作为前缀:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: {{ .Release.Name }}-configmap 
data: 
  user_id.properties: |- 
    {{- range $index, $user := .Values.users }} 
    user.{{ $user }}={{ $index }} 
    {{- end }} 

当渲染此模板时,输出如下:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: interested-ibex-configmap 
data: 
  user_id.properties: |- 
    user.yogi.id=0 
    user.paddington.id=1 
    user.teddy.id=2 

在使用 range 函数循环遍历键值结构时,我们还可以使用变量来捕获键和值。

让我们在我们的values.yaml文件中考虑以下数据:

users:
   yogi:
     food: picnic 
    height: 1500 
  paddington:
     food: marmalade 
    height: 1066 
  teddy: 
    food: honey 
    height: 500  

现在我们在用户变量中有一些键值数据,让我们用它来配置一些 pod 的环境变量:

apiVersion: v1 
kind: Pod 
metadata: 
  name: {{ .Release.Name }}-env-pod 
spec: 
  containers: 
  - image: alpine 
    name: bear-env 
    env: 
    {{- range $name, $user := .Values.users }} 
      {{- range $var, $value := $user }} 
      - name: {{ $name | upper }}_BEAR_{{ $var | upper }} 
        value: {{ $value | quote }} 
      {{- end }} 
    {{- end }} 
    command: ["env"] 

当我们使用 range 关键字循环遍历键值结构时,键成为第一个返回的变量,值成为第二个。通过嵌套循环,就像在这种情况下一样,可以在值文件中使用相当复杂的数据结构。

Kubernetes 资源中某些变量的类型很重要。在前面的例子中,环境变量中的值必须始终是一个字符串,因此我们使用了quote管道函数来确保其他类型的值(如数字)是正确的字符串类型。

渲染时,此模板将生成一个 pod 清单,如下所示:

apiVersion: v1 
kind: Pod 
metadata: 
  name: solemn-whale-env-pod 
spec: 
  containers: 
  - image: alpine 
    name: bear-env 
    env: 
      - name: PADDINGTON_BEAR_FOOD 
        value: "marmalade" 
      - name: PADDINGTON_BEAR_HEIGHT 
        value: "1066" 
      - name: TEDDY_BEAR_FOOD 
        value: "honey" 
      - name: TEDDY_BEAR_HEIGHT 
        value: "500" 
      - name: YOGI_BEAR_FOOD 
        value: "picnic" 
      - name: YOGI_BEAR_HEIGHT 
        value: "1500" 
    command: ["env"] 

钩子

到目前为止,我们一直在使用 Helm 来帮助我们生成我们的应用程序需要提交到 Kubernetes 的资源。在理想的世界中,这将是 Helm 这样的工具所需要做的一切。Kubernetes 旨在是声明性的;换句话说,我们提交描述集群状态的资源,Kubernetes 会处理其余的工作。

不幸的是,在现实世界中,有时我们仍然需要明确地采取一些行动来使我们的应用程序正确运行。也许当您安装应用程序时,您需要运行脚本来初始化数据库架构或设置一些默认用户。也许当您安装应用程序的新版本时,您需要运行脚本来迁移数据库架构,以使其与应用程序的新版本兼容。

Helm 提供了一个钩子机制,允许我们在发布的生命周期的八个特定点上采取行动。为了在 Helm 图表中定义一个钩子,您需要向资源添加helm.sh/hook注释。您可以在任何资源上使用钩子注释,以确保它在适当的时间创建。但通常,创建作业类型的资源非常有用。如果您的资源是作业类型,Tiller 将阻塞,直到作业成功运行完成。这意味着如果您使用pre-钩子之一,那么您的应用程序可以依赖于该作业已经运行。

  • pre-install:此操作在 Tiller 渲染图表模板后但在任何资源提交到 Kubernetes API 之前运行。此操作在通过安装图表创建新发布时运行。如果您还需要在发布升级时运行钩子,您应该将此钩子与pre-upgrade钩子结合使用。您可以利用此钩子来初始化将被应用程序使用的资源。

  • post-install:此操作在所有资源已提交到 Kubernetes API 后运行。例如,您可以使用此操作运行一个脚本,向聊天室发送通知,或者向监控工具注册图表的新实例。

  • pre-delete:此钩子在删除请求发出时,在从 Kubernetes 删除任何资源之前运行。例如,如果您需要备份应用程序存储的数据,这可能会很有用。

  • post-delete:此钩子在 Helm 删除作为发布的一部分创建的资源后运行。您可以利用此钩子清理应用程序使用的任何未由 Helm 或 Kubernetes 管理的外部资源。

  • pre-upgrade:此钩子提供与pre-install钩子相同的功能,但每次升级发布时运行。您可以使用此钩子运行数据库迁移脚本。

  • post-upgrade:此钩子提供与post-install钩子相同的功能,但每次升级发布时运行。同样,这可能用于通知目的。

  • pre-rollback:此钩子在提交回滚发布升级的更改到 Kubernetes API 之前运行。

  • post-rollback:此钩子在提交回滚发布升级的请求到 Kubernetes 后运行。根据您的应用程序的期望,您可能在此处或在pre-rollback钩子中运行脚本来回滚数据库更改。

  • 让我们看一个例子,我们将使用一个钩子来运行设置脚本:

apiVersion: batch/v1 
kind: Job 
metadata: 
  name: "{{.Release.Name}}-setup" 
  labels: 
    heritage: {{.Release.Service | quote }} 
    release: {{.Release.Name | quote }} 
    chart: "{{.Chart.Name}}-{{.Chart.Version}}" 
  annotations: 
    "helm.sh/hook": pre-install 
spec: 
  template: 
    metadata: 
      name: "{{.Release.Name}}-setup" 
    labels: 
      heritage: {{.Release.Service | quote }} 
      release: {{.Release.Name | quote }} 
      chart: "{{.Chart.Name}}-{{.Chart.Version}}" 
    spec: 
      restartPolicy: Never 
      containers: 
      - name: setup 
        image: errm/awesome-application 
        command: ["bin/setup"] 

关于此作业定义的一切都与我们在第四章中看到的标准 Kubernetes 资源定义相同,管理应用程序中的变更。是作业元数据中添加的注释使 Helm 能够将此定义视为钩子,而不是我们应用程序的受管部分。

单个资源可以用于实现多个钩子。例如,如果您希望设置脚本在每次更新发布时以及首次安装时运行,我们可以更改钩子注释为:

annotations: 
  "helm.sh/hook": pre-install,pre-upgrade 

Helm 允许您使用钩子机制创建任何 Kubernetes 资源。例如,如果使用钩子创建的作业依赖于ConfigMapSecret,这可能很有用。

如果您有多个需要按特定顺序创建的钩子资源,可以使用helm.sh/hook-weight注释。此权重可以是任何正整数或负整数。当 Helm 评估特定的钩子时,资源将按照这些权重按升序排序。由于注释只能保存字符串,因此重要的是引用钩子权重中使用的数字。

例如,具有注释"helm.sh/hook-weight": "-5"的资源将在"helm.sh/hook-weight": "5"之前运行,但会在具有注释"helm.sh/hook-weight": "-10"的资源之后运行。

Helm 的钩子系统中有一个小问题,一开始可能会令人困惑,但幸运的是,一旦您理解了它,就有一些简单的方法来解决它。

Helm 跟踪您使用模板创建的几乎所有资源。这意味着当您升级发布时,Helm 可以更新发布管理的所有资源,当删除发布时,Helm 可以删除它创建的所有资源。唯一的例外是钩子创建的资源。一旦它们被创建,Helm 就不再管理它们,而是由 Kubernetes 接管。

在使用图表时,这可能会导致两个不同的问题:

首先,删除图表时,钩子创建的资源不会被删除。除非手动删除资源,否则这可能意外地使用集群中的资源。其次,如果您使用的钩子在图表发布的生命周期中可以被调用多次,那么您的资源名称可能会发生冲突。

使用我们的示例作业,如果我们将钩子注释更新为"helm.sh/hook": pre-install,pre-upgrade,我们会发现当安装图表时,作业将正确运行,但是当我们升级发布时,Helm 会尝试创建一个与pre-install钩子中已创建的作业同名的新作业。这将导致错误,从而阻止升级继续进行。

解决此问题的一种方法是在作业名称中包含发布修订号,如下所示:

metadata: 
  name: "{{.Release.Name}}-setup-{{ Release.Revision }}" 

虽然这可以防止作业名称冲突,但这意味着每次升级发布都会创建一个新资源,所有这些资源在不再需要时可能需要手动清理。

Helm 提供了另一个注释来帮助我们解决这个问题。helm.sh/hook-delete-policy允许我们指示 Helm 在成功执行后删除资源,或在失败后删除资源,或两者都删除。

注释"helm.sh/hook-delete-policy": hook-succeeded对于大多数用例非常有用,例如设置脚本作业示例。如果作业成功运行,则会被删除,清理资源,以便在下次升级图表时创建具有相同名称的新实例。如果作业失败,则会保留在 Kubernetes 服务器上,以便进行调试目的的检查。

如果您正在使用 Helm 作为自动化工作流程的一部分,确保通过安装图表创建的所有资源都被删除,无论结果如何,您可能希望使用以下注释:

"helm.sh/hook-delete-policy": hook-succeeded,hook-failed

打包 Helm 图表

在开发图表时,可以简单地使用 Helm CLI 直接从本地文件系统部署图表。但是,Helm 还允许您创建自己的存储库,以便共享您的图表。

Helm 存储库是存储在标准 HTTP Web 服务器上特定目录结构中的打包 Helm 图表的集合,以及索引。

一旦您满意您的图表,您将希望打包它,以便在 Helm 存储库中分发。使用helm package命令很容易做到这一点。当您开始使用存储库分发图表时,版本控制变得重要。Helm 存储库中图表的版本号需要遵循 SemVer 2 指南。

要构建打包的图表,首先要检查您是否在Chart.yaml中设置了适当的版本号。如果这是您第一次打包图表,那么默认值将是 OK:

$ helm package version-app
Successfully packaged chart and saved it to: ~/helm-charts/version-app-0.1.0.tgz  

您可以使用helm serve命令测试打包的图表,而无需将其上传到存储库。此命令将为当前目录中找到的所有打包图表提供服务,并动态生成索引:

$ helm serve
Regenerating index. This may take a moment.
Now serving you on 127.0.0.1:8879  

现在您可以尝试使用本地存储库安装您的图表:

$ helm install local/version-app  

您可以测试构建索引

Helm 存储库只是存储在目录中的一组打包图表。为了发现和搜索特定存储库中可用的图表和版本,Helm 客户端会下载一个包含有关每个打包图表及其可下载位置的元数据的特殊index.yaml

为了生成这个索引文件,我们需要将我们想要在索引中的所有打包的图表复制到同一个目录中:

cp ~/helm-charts/version-app-0.1.0.tgz ~/helm-repo/ 

然后,为了生成index.yaml文件,我们使用helm repo index命令。您需要传递打包图表将被提供的根 URL。这可以是 Web 服务器的地址,或者在 AWS 上,您可以使用 S3 存储桶:

helm repo index ~/helm-repo --url https://helm-repo.example.org  

图表索引是一个非常简单的格式,列出了每个可用图表的名称,然后提供了每个命名图表的每个版本的列表。索引还包括一个校验和,以验证从存储库下载图表:

apiVersion: v1 
entries: 
  version-app: 
  - apiVersion: v1 
    created: 2018-01-10T19:28:27.802896842Z 
    description: A Helm chart for Kubernetes 
    digest: 79aee8b48cab65f0d3693b98ae8234fe889b22815db87861e590276a657912c1 
    name: version-app 
    urls: 
    - https://helm-repo.example.org/version-app-0.1.0.tgz 
    version: 0.1.0 
generated: 2018-01-10T19:28:27.802428278Z 

我们新的图表存储库生成的index.yaml文件。

一旦我们创建了index.yaml文件,只需将打包的图表和索引文件复制到您选择使用的主机上。如果您使用 S3,可能会像这样:

aws s3 sync ~/helm-repo s3://my-helm-repo-bucket  

为了使 Helm 能够使用您的存储库,您的 Web 服务器(或 S3)需要正确配置。

Web 服务器需要提供带有正确内容类型标头(text/yamltext/x-yaml)的index.yaml文件。

图表需要在索引中列出的 URL 上可用。

使用您的存储库

一旦您设置了存储库,就可以配置 Helm 来使用它:

helm repo add my-repo https://helm-repo.example.org 
my-repo has been added to your repositories 

当您添加一个存储库时,Helm 会验证它确实可以连接到给定的 URL 并下载索引文件。

您可以通过使用helm search来搜索您的图表来检查这一点:

$ helm search version-app 
NAME                  VERSION     DESCRIPTION 
my-repo/version-app   0.1.1       A Helm chart for Kubernetes 

Helm 的组织模式

在使用 Kubernetes 部署自己的应用程序的组织中,有一些策略您可能想要考虑,以便生成和维护用于管理您使用的应用程序的部署的图表。

每个应用程序一个图表

在您的组织中使用 Helm 的最简单方法是为要部署到 Kubernetes 的每个应用程序创建一个新的图表。

当您有一个可能部署到多个不同上下文的应用程序时,比如测试、暂存和生产环境,这可以确保在每个环境之间有一致性,同时也可以简单地提供可能是特定于环境的配置的覆盖。

为您的应用程序创建 Helm 图表可以帮助在较大的组织中,应用程序可能需要在没有构建和管理应用程序的团队的帮助下部署到多个不同的环境。

例如,移动应用程序或前端 Web 开发人员可能会使用 Helm 将另一个团队开发的后端 API 应用部署到测试或开发环境。如果开发后端的团队提供了一个 Helm 图表,那么其他团队可以简单地部署而无需深入了解如何安装和配置该应用程序。

如果相同的 Helm 图表用于部署到生产环境以及测试和开发环境,那么可以更简单地减少生产和开发环境之间不可避免的漂移。

使用 Helm 模板语言的控制流功能可以在适当的情况下提供不同的配置是很简单的。例如,在暂存或生产环境中,您的应用程序可能依赖将数据保存到 EBS 卷,而在开发机器上,应用程序可能只是保存到本地卷。

当您的图表部署时,可能需要覆盖一些值。例如,在生产环境中,您可能希望运行更多 pod 的副本,而在开发机器上,单个副本可能就足够了。

如果您的应用程序可以通过添加更多 pod 的副本来水平扩展,那么在所有环境中提供相同的内存和 CPU 限制,然后通过添加额外的 pod 来扩展生产流量,而不是给每个 pod 提供更大的资源限制是有意义的。这样做可以更容易地调试由于内存不足错误或 CPU 资源不足而导致应用程序被终止的问题,因为单个 pod 在开发和生产集群上将具有相同的资源。

共享图表

如果您的组织维护基于服务或微服务的系统,通常会在部署的不同服务之间保持一定程度的标准化。

在每个应用程序之间保持一致的部署模式的一种方法是提供一个 Helm 图表,可以用来部署所有服务。

如果这样做,您会发现需要为图表提供的配置和模板本身变得更加复杂。但这种工作方式的优势在于,它可以让您快速将新的配置最佳实践应用到所有应用程序中。

在更简单的 Helm 图表中,我们为应用程序的每个 pod 提供了一个新模板。当一个图表要被多个应用程序重用时,每个应用程序可能需要不同的 pod。

例如,一个应用程序可能需要一个 Web 服务器和一个每小时运行的批处理作业,而另一个服务提供一个管理界面和一个用于处理消息队列中的后台作业的工作程序。

为了能够使用一个图表部署两个不同类型的应用程序,您需要生成一个模板——不是针对应用程序中的每个 pod,而是针对您的服务合同支持的每种类型的 pod。

例如,您可能有一个用于管理长时间运行的 pod 的模板,该模板使用 Kubernetes 部署资源,以及另一个用于使用CronJob资源管理批处理作业的模板。然后,为了启用和配置这些模板,您可以在部署应用程序时提供应用程序需要的每个 pod 的列表。

我制作了一个采用这种方法的示例图表。它可以在github.com/errm/charts/tree/master/app找到。

库图表

如果您的组织有配置和部署模式,您希望在不同的应用程序之间共享,但共享图表的方法提供的灵活性不够,或者在模板中导致过于复杂的逻辑,那么另一种选择是提供包含模板或函数的库图表,这些模板或函数可以作为应用程序的依赖项,为需要它们的每个图表提供共同的组件或配置。

这样可以让您在能够根据特定应用程序定制您的图表的同时,仍然能够使用共享功能,以减少配置的重复或强制执行最佳实践或其他组织范围的部署模式。

下一步

Helm 之所以强大,是因为它让你可以在一组 Kubernetes 资源上构建自己的抽象,而几乎不需要额外的努力。你可能需要花一点时间学习如何使用模板语言,以及如何集成构建和更新图表、制作和更新发布与你的开发和发布流程。

Helm 可用于各种场景,您可以在其中部署资源到 Kubernetes 集群,从为他人提供一种简单的方式在他们自己的集群上安装您编写的应用程序,到在更大的组织内形成内部平台即服务的基石。除了本章包含的内容之外,还有很多东西等待您去学习。

Helm 有出色的文档,可以在docs.helm.sh/上访问。

学习如何有效使用 Helm 的另一个很好的来源是社区维护的图表存储库github.com/helm/charts。您会发现在那里可以通过查看可用的图表学习到很多技巧和最佳实践。

第六章:生产规划

Kubernetes 为开发人员提供了一个极好的平台,可以快速构建高度灵活的分布式应用程序。通过在 Kubernetes 上运行我们的应用程序,我们可以利用各种工具来简化它们的操作,并使它们更加可靠、抗错误,并且最终高度可用。

为了依赖于我们的应用程序可以从 Kubernetes 继承的一些保证和行为,重要的是我们了解 Kubernetes 的行为方式,以及对生产系统产生影响的一些因素。

作为集群管理员,重要的是你要了解你正在运行的应用程序的要求,以及这些应用程序的用户。

在生产中了解 Kubernetes 的行为方式至关重要,因此在开始提供关键任务流量之前,获得在 Kubernetes 上运行应用程序的实际经验是非常宝贵的。例如,当 GitHub 将他们的主要应用程序迁移到 Kubernetes 时,他们首先将内部用户的流量转移到他们基于 Kubernetes 的新基础设施,然后再切换到他们的主要生产流量。

“来自内部用户的负载帮助我们发现问题,修复错误,并开始逐渐熟悉 Kubernetes 在生产中的运行。在此期间,我们努力增加自己的信心,通过模拟未来预期执行的程序,编写运行手册,并进行故障测试。”—Jesse Newland (githubengineering.com/kubernetes-at-github/)

虽然我可以涵盖一些在 AWS 上使用 Kubernetes 进行生产时可能会遇到的事情,但重要的是要理解每个应用程序和组织在很多方面都是独特的。你应该把 Kubernetes 看作一个工具包,它将帮助你为你的组织构建一个强大而灵活的环境。Kubernetes 并不是一个能够消除对运维专业知识需求的魔法子弹;它是一个帮助你管理应用程序的工具。

设计过程

设计过程如下:

当你考虑准备使用 Kubernetes 来管理你的生产基础设施时,你不应该把 Kubernetes 看作你的最终目标。它是一个构建平台的基础,用于运行系统。

当你考虑构建一个满足组织中不同人员需求的平台时,定义你将对 Kubernetes 提出的要求变得更加简单。在尝试规划生产环境时,你需要了解你的组织的需求。显然,你想要管理的软件的技术要求很重要。但了解你的组织需要支持的运营流程也很关键。

采用 Kubernetes 为具有复杂软件要求的组织带来了许多好处。不幸的是,这种复杂性也可能导致在安全地成功采用 Kubernetes 方面出现挑战。

初始规划

你应该考虑你的初始推出的重点在哪里。你应该寻找一个既能够快速提供有价值的结果,又具有较低风险的应用程序。如果我们想想 GitHub 的例子,他们最初把重点放在为内部用户构建基础设施,以便快速测试他们软件的更改。通过专注于审查或分期基础设施,他们找到了一个适用于 Kubernetes 的应用程序,既能够为他们组织的开发人员快速提供价值,又是一个对他们的业务风险较低的领域,因为它只被内部用户访问。

像这样既具有即时有用性又对停机影响较小的应用程序非常有用。它们使你的组织能够在使用 Kubernetes 时获得宝贵的运营经验,并在尝试处理生产工作负载之前消除错误和其他问题。

在开始使用 Kubernetes 时,选择你的组织运营的最简单的应用程序并开始围绕它构建流程和工具可能是诱人的。然而,这可能是一个错误,因为这可能会导致你对应用程序应该如何操作做出假设,这可能会使将相同的流程和配置应用到更复杂的应用程序变得更加困难。

如果您选择开始构建支持不需要任何后端服务(如数据库)的简单应用程序的平台,您可能会错过一些需要考虑的事情作为部署过程的一部分。例如,由数据库支持的应用程序通常需要运行迁移脚本来在部署新版本的应用程序时更新架构。如果您首先设计部署流程以满足非常简单应用程序的需求,您可能要到后来才能发现这些要求。请记住,部署一个只需要平台提供的部分功能子集的简单应用程序将始终比部署一个需要您在设计时没有考虑到的更复杂的应用程序要简单得多。

如果您选择将精力集中在初始采用 Kubernetes 的单个应用程序上,请确保选择一个代表您组织需求的应用程序。很容易会开始为一个全新的项目使用 Kubernetes,因为您可以考虑平台的应用开发决策。但请记住,一个新应用程序可能会比使用时间更长的应用程序简单得多。在 GitHub 的例子中,他们选择首先部署的应用程序是他们组织运营的最大的应用程序,提供许多核心服务。

如果您的组织有一个每次部署都需要大量运营时间和精力的应用程序,那么这可能是初始采用 Kubernetes 的一个很好的选择。这些应用程序将因其需求而为您的开发和运营团队所熟知,并且他们将立即能够开始利用 Kubernetes 来解决以前花费时间和精力的问题。

成功的规划

为了成功地实施采用 Kubernetes 的项目,有一些事情您应该尽量避免。

一个很容易陷入的陷阱是改变得太快太多。如果您决定采用容器化和 Kubernetes,很容易会在此过程中采用许多新的流程和工具。这可能会显著减慢您的进展,因为最初是为了在容器中运行应用程序的项目很快就会扩展到包括您的组织想要采用的许多其他工具和流程。

您应该努力避免范围蔓延,并尽量改变尽可能少的内容,以便尽快交付您对 Kubernetes 的初始采用。重要的是不要试图一次实现太多容器化的承诺,因为这将阻碍您的采用,并可能导致整个项目的失败。

尝试考虑您当前部署应用程序的环境,并首先复制其功能,然后添加额外的功能。我们在本书的其余部分讨论的许多工具和流程可能确实是您的 Kubernetes 集群的可选项,可以在以后的日期添加,以提供额外的有价值的服务,但不应视为采用的障碍。

如果您有机会在额外的部署中减少 Kubernetes 部署提供的基础设施范围,您应该考虑这样做。这样可以减少组织需要理解的新工具和流程的范围。这将使您有机会在以后更详细地关注这个主题,并参考您在 Kubernetes 上运行应用程序时获得的运营经验。

以日志管理为例,如果您当前的流程是使用 SSH 登录服务器并查看日志文件,您可以使用kubectl logs命令为您的 Kubernetes 集群的操作员提供相同的功能。实施一个解决方案来聚合和搜索集群生成的日志可能是可取的,但不一定是使用 Kubernetes 的阻碍因素。

如果您当前将应用程序部署到运行 Linux 发行版的服务器上,该发行版作为容器映像 readily 可用,您应该坚持使用该发行版,而不是在这个阶段寻找替代方案,因为您的开发人员和运营人员已经了解它的工作原理,您不必投入时间来修复不兼容性。学习在 Kubernetes 上操作您的应用程序应该是您的重点,而不是学习如何配置新的操作系统发行版。

规划成功的部署。

改变组织中的流程和责任可能是诱人的。但在采用像 Kubernetes 这样的新工具时,尝试这样做可能是有风险的。例如,如果在您的组织中有一个负责部署和监控应用程序的运维团队,那么在采用 Kubernetes 时并不是将这一责任交给其他人(比如开发团队)或尝试自动化手动流程的正确时机。

这可能令人沮丧,因为通常采用 Kubernetes 是作为改进组织使用的流程和工具的更广泛计划的一部分。您应该等到成功建立 Kubernetes 的使用和操作后再进行。这将使您在有一个稳定的基础之后更好地引入新工具和流程。您应该将采用 Kubernetes 视为建立一个灵活的基础,以便在将来实施对工具和流程的任何更改。

一旦您的应用基础设施在 Kubernetes 上运行,您会发现实施新工具、服务和流程变得更加简单。一旦您拥有了一个 Kubernetes 集群,您会发现尝试新工具的障碍大大降低。您可以通过向集群提交新配置来快速评估和尝试新工具,而不是花费大量时间进行规划和配置。

发现需求

设计需求如下图所示:

在准备生产时,可用性容量性能是我们应该考虑的关键属性。在收集集群的功能需求时,可以帮助将需求归类为涉及这些属性的需求。

重要的是要明白,可能无法在不做出一些权衡的情况下优化所有三个属性。例如,对于依赖非常高网络性能的应用程序,AWS 提供了一个称为集群放置组的工具。这确保了通过在 AWS 数据中心内以某种方式配置 EC2 VM 来提供最佳网络性能,从而在它们之间提供快速的网络互连(可能是通过将它们放置在 AWS 数据中心内的相近位置)。通过这种方式配置实例,可以在集群放置组内的机器之间实现最高的网络吞吐量(超过 5 GB)和最低的延迟。对于一些需要这些性能水平的应用程序来说,这可能是一种值得的优化。

然而,由于集群放置组内的 EC2 实例不能跨多个可用区,因此这种设置的可靠性可能较低,因为潜在的电源或连接问题可能会影响特定区域内的所有实例,特别是如果它们被部署以最大化互连速度。如果您的应用程序对这种高性能网络没有要求,将可靠性换取更高性能的做法确实是不明智的。

这些属性的最重要的属性是生产系统的一个非常重要的属性——可观察性。可观察性实际上描述了集群操作员了解应用程序发生了什么的能力。如果无法了解应用程序是否按照预期执行和行为,就无法评估、改进和演变系统的设计。在设计集群时,这是一个重要的反馈循环,它使您能够根据运营经验维护和改进集群。如果在规划集群时不考虑可观察性,要调试集群本身和应用程序的问题就会更加困难。

在规划阶段讨论应用程序需求时,很难理解应用程序的需求会是什么。对集群性能、底层硬件以及运行在其上的应用程序有良好的可观察性,可以让您做出务实的决策,并且足够灵活,以便在发现更多关于它们在生产工作负载下的行为以及随着功能随时间的开发而发生变化时,支持应用程序所做出的更改。

最后,也许最重要的属性要考虑的是安全性。将集群的安全性留到规划过程的最后是一个错误。请记住,尽管单靠安全性本身不会导致项目的成功,但未能确保集群的安全性可能会导致灾难性后果。

最近的研究和披露显示,未加密的 Kubernetes 集群已经成为那些想要利用您的计算能力进行加密货币挖矿和其他不良目的的人的有吸引力的目标,更不用说访问您的集群可能被用来访问您组织内保存的敏感数据的潜力。

安全性应该在集群的整个生命周期中得到考虑和监控;事实上,您应该尝试并了解每一个其他要求的安全性影响。您需要考虑您的组织成员如何与 Kubernetes 进行交互,同时制定计划确保您的集群和在其上运行的应用程序的配置和软件的安全。

在本章的后续部分,我们将介绍一些想法,帮助您了解在这些属性方面可能需要考虑的事项。希望本章能够让您充分了解自己的需求,并开始规划生产集群。关于实施计划所需的具体知识,请继续阅读;本书的后半部分几乎完全专注于您实施计划所需的实用知识。

可用性

可用性如下图所示:

在规划生产系统时,考虑到可用性是最重要的事情之一。几乎总是我们运行软件来为用户提供服务。如果由于任何原因我们的软件无法满足用户的请求,那么通常我们就无法满足他们的期望。根据您的组织提供的服务,不可用可能会导致用户不满意、不便或甚至遭受损失或伤害。制定任何生产系统的充分计划的一部分是了解停机时间或错误可能如何影响您的用户。

可用性的定义取决于您的集群正在运行的工作负载类型和您的业务需求。规划 Kubernetes 集群的关键部分是了解用户对您正在运行的服务的需求。

例如,考虑一个每天给用户发送业务报告的批处理作业。只要您能确保它每天至少运行一次,大致在正确的时间,您就可以认为它的可用性达到 100%,而可以在白天或晚上任何时间访问的 Web 服务器需要在用户需要访问时可用且无错误。

当 CEO 在早上 9 点到达工作时,收件箱中已经准备好阅读的报告时,他们会很高兴。他们不会在意任务在午夜时分未能成功运行,并在几分钟后成功重试。然而,如果托管他们用来阅读电子邮件的 Web 邮件应用程序的应用服务器在一天中的任何时候甚至短暂不可用,他们可能会受到打扰和不便:

计算服务可用性的简单公式

一般来说,系统工程师认为给定服务的可用性是成功请求占总请求次数的百分比。

我们可以认为即使批处理作业失败了几次,也是可用的。我们对系统的要求(每天向正确的人发送报告)只需要作业至少成功完成一次。如果我们通过重试优雅地处理故障,对我们的用户没有影响。

您应该为系统计划的确切数字,当然,主要取决于用户的需求和组织的优先级。然而,值得记住的是,为了更高的可用性而设计的系统几乎总是比可以接受停机时间的类似系统更复杂,需要更多资源。随着服务接近 100%的可用性,实现额外可靠性的成本和复杂性呈指数增长。

如果您还不了解它们,可以合理地开始讨论组织内的可用性要求。您应该这样做,以便设定目标并了解在 Kubernetes 上运行软件的最佳方法。以下是一些您应该尝试回答的问题:

  • 你知道你的用户是如何访问你的服务的吗? 例如,如果你的用户使用移动设备,那么连接到互联网可能本来就更不可靠,掩盖了你的服务的正常运行时间(或其他情况)。

  • 如果你正在将你的服务迁移到 Kubernetes,你知道它目前的可靠性吗?

  • 你能够对不可用性进行货币价值评估吗? 例如,电子商务或广告技术组织将知道在停机期间将会损失多少金额。

  • 你的用户准备接受多少不可用性水平? 你有竞争对手吗?

你可能已经注意到,所有这些问题都是关于你的用户和你的组织;对于任何一个问题,都没有确定的技术答案,但你需要能够回答它们以了解你正在构建的系统的要求。

为了提供一个在网络上可以访问的高可用服务,比如一个网页服务器,我们需要确保服务能够在需要时响应请求。由于我们无法确保我们的服务运行在的底层机器是 100%可靠的,我们需要运行多个实例的软件,并且只将流量路由到那些能够响应请求的实例上。

这个批处理作业的语义意味着(在合理范围内),我们并不太关心作业执行所需的时间,而网页服务器响应所需的时间则非常重要。有许多研究表明,即使是对网页加载时间增加了不到一秒的延迟,也会对用户产生显著和可测量的影响。因此,即使我们能够隐藏故障(例如通过重试失败的请求),我们的余地要小得多,甚至我们甚至可能认为高优先级的请求如果超过特定阈值的时间就已经失败了。

你可能选择在 Kubernetes 上运行你的应用程序的一个原因是因为你听说过它的自愈特性。Kubernetes 将管理我们的应用程序,并在需要时采取行动,以确保我们的应用程序继续以我们要求的方式运行。这是 Kubernetes 对配置的声明性方法的一个有益的效果。

使用 Kubernetes,我们将要求集群上运行某项服务的特定数量的副本。即使发生影响运行应用程序的情况,例如节点故障或应用程序实例由于内存泄漏而定期被终止,控制平面也能够采取行动来确保这种条件继续成立。

与依赖操作员选择特定基础机器(或一组机器)运行应用程序的命令式部署程序形成对比。如果机器故障,甚至如果应用程序实例表现不佳,则需要手动干预。我们希望为用户提供所需的服务而不中断。

对于始终开启或延迟敏感的应用程序,例如 Web 服务器,Kubernetes 为我们提供机制来运行我们应用程序的多个副本,并测试我们服务的健康状况,以便从服务中删除失败的实例,甚至重新启动。

对于批处理作业,Kubernetes 将重试失败的作业,并将它们重新调度到其他节点,如果底层节点失败。这种重启和重新调度失败应用程序的语义依赖于 Kubernetes 控制平面的功能。一旦 pod 在特定节点上运行,它将继续运行,直到发生以下情况:

  • 它退出

  • 它被 kubelet 终止,因为使用了太多内存

  • API 服务器请求将其终止(可能是为了重新平衡集群或为了为具有更高优先级的 pod 腾出空间)

这意味着控制平面本身可以暂时不可用,而不会影响集群上运行的应用程序。但是,直到控制平面再次可用之前,没有失败的 pod,或者在已经失败的节点上运行的 pod 将被重新调度。显然,您还需要 API 服务器可用以与其交互,因此还应考虑组织推送新配置到集群的需求(例如,部署应用程序的新版本)。

我们将讨论一些策略和工具,您可以使用它们来提供一个高可用的控制平面,详情请参阅第七章 生产就绪的集群

容量

容量如下图所示:

运行诸如 Kubernetes 之类的系统意味着您可以在应用程序启动的时间内对服务的额外需求做出实际回应。这个过程甚至可以通过诸如水平 Pod 自动缩放器(我们将在第八章中讨论的抱歉,我的应用程序吃掉了集群)这样的工具自动化。

当我们将这种灵活性与我们随意启动新的 EC2 实例的能力相结合时,容量规划比过去要简单得多。Kubernetes 和 AWS 允许我们构建只在任何给定时间使用所需资源量的应用程序。我们可以对应用程序的使用要求做出反应,而不是预期对我们的应用程序的需求并预先承诺使用资源。Kubernetes 最终使我们能够实现云计算的一个承诺:我们只支付我们使用的资源。

在使用 AWS 支付的资源上最有效地使用资源时,您应该考虑一些因素。

EC2 实例类型

在准备启动 Kubernetes 集群时,您可能会考虑集群中将使用的实例的类型和大小。您选择的实例可能会对 Kubernetes 集群的利用率、性能和成本产生重大影响。

当 Kubernetes 将您的 pod 调度到集群中的工作节点时,它会考虑作为 pod 定义的一部分的资源请求和限制。

通常,您的 pod 规范将请求一定数量的 CPU(或其分数)和一定数量的内存。在 AWS 上,Kubernetes 使用 AWS 的 vCPU 作为其度量单位。vCPU(在大多数实例类型上)是一个单 CPU(超)线程,而不是 CPU 核心。如果您请求了 CPU 的分数,则 Kubernetes 会为您的 pod 分配一个 vCPU 的份额。内存以字节为单位请求。

EC2 实例有几种不同类型,提供不同的 CPU 到内存比例。

EC2 实例类型

EC2 实例类型显示在以下表中:

类别 类型 CPU 到内存比例:vCPU:GiB 备注
突发型 T3 1 CPU : 2 GiB 提供 5-40%的 CPU 基线+可突发额外使用。
CPU 优化 C5 1 CPU : 2 GiB
通用型 M5 1 CPU : 4 GiB
内存优化 R5 1 CPU : 8 GiB
X1 1 CPU : 15GiB
X1e 1 CPU : 30GiB
只有在需要它们提供的额外资源(GPU 和/或本地存储)时,您才应该考虑以下实例类型:
GPU P3 1 CPU : 7.6GiB 1 GPU : 8 CPU (NVIDIA Tesla V100)
P2 1 CPU : 4GiB i. 1 GPU : 4 CPU (NVIDIA K80)
存储 H1 1 CPU : 4GiB 2TB HDD : 8 CPU
D2 1 CPU : 7.6GiB 3TB HDD : 2 CPU
I3 1 CPU : 7.6GiB 475GiB SSD : 2 CPU

在准备集群时,我们应该考虑组成集群的实例类型和实例大小。

当 Kubernetes 将我们的 pod 调度到集群中的节点时,它当然是希望尽可能多地将容器放入集群中。然而,如果大多数 pod 的 CPU 到内存请求比在底层节点中显着不同,这可能会受到阻碍。

例如,考虑这样一个场景:我们在集群中部署了请求 1 个 CPU 和 2GiB 内存的 pod。如果我们的集群由m5.xlarge实例(4 vCPU 和 16 GiB 内存)组成,每个节点将能够运行四个 pod。一旦这四个 pod 在该节点上运行,就无法再将更多的 pod 调度到该节点,但是一半的内存将被闲置,实际上处于被困的状态。

如果您的工作负载非常同质化,那么确定哪种实例类型将为您的应用程序提供最佳的 CPU 到内存比率当然是非常简单的。然而,大多数集群运行多个应用程序,每个应用程序需要不同数量的内存和 CPU(甚至可能还需要其他资源)。

在第八章中,抱歉,我的应用程序吃掉了集群,我们讨论了如何使用集群自动缩放器自动向 AWS 自动缩放组添加和删除实例,以便在任何给定时间将您的集群大小调整到与集群要求相匹配。我们还讨论了如何使用集群自动缩放器来扩展具有多种不同实例类型的集群,以应对在这些集群中运行的工作负载的大小和形状相当动态,可能会不时发生变化的 CPU 到内存比率的问题。

广度与深度

亚马逊为每个系列提供了许多不同的实例大小;例如,m5 和 c5 系列都有六种不同的实例大小,每一级别提供的资源是前一级别的两倍。因此,最大的实例比最小的实例多 48 倍资源。我们应该如何选择用于构建集群的实例大小?

  • 您的实例大小限制了集群上可运行的最大 Pod 大小。实例需要比您最大的 Pod 大 10-20%,以考虑系统服务(如日志记录或监控工具、Docker 和 Kubernetes 本身)的开销。

  • 较小的实例将允许您以较小的增量扩展您的集群,增加利用率。

  • 较少(较大)的实例可能更容易管理。

  • 较大的实例可能会使用较低比例的资源来执行集群级任务,例如日志传送和指标。

  • 如果您想使用监控或日志记录工具,例如 Datadog、Sysdig、NewRelic 等,其定价是基于每个实例模型的,较少的较大实例可能更具成本效益。

  • 较大的实例可以提供更多的磁盘和网络带宽,但如果您在每个实例上运行更多的进程,这可能不会带来任何优势。

  • 较大的实例大小在超级管理程序级别更不太可能受到嘈杂邻居问题的影响。

  • 较大的实例通常意味着更多的 Pod 共存。当旨在增加利用率时,这通常是有利的,但有时可能会导致意外的资源限制模式。

性能

影响性能的集群的关键组件如下图所示:

磁盘性能

如果您的一些应用程序依赖于磁盘性能,了解连接到您实例的 EBS 卷的性能特征可能会非常有用。

所有当前一代的 EC2 实例都依赖于 EBS 存储。EBS 存储实际上是共享的网络附加存储,因此性能可能会受到多种因素的影响。

如果您的集群正在运行在最新一代的 EC2 实例上,您将使用 EBS 优化。这意味着专用带宽可用于对 EBS 卷的 I/O 操作,有效消除了 EBS 和其他网络活动之间的竞争。

EBS 卷可用的总最大带宽取决于 EC2 实例的大小。在一个运行多个容器的系统中,可能每个容器都连接了一个或多个 EBS 卷,您应该意识到这个上限适用于实例上所有正在使用的卷的总和。

如果您计划运行期望进行大量磁盘 I/O 的工作负载,您可能需要考虑实例可用的总 I/O。

EBS 基于两种基本技术提供了四种卷类型。gp2io2卷基于固态硬盘(SSD)技术,而 st1 和 sc1 卷基于硬盘驱动器(HDD)技术。

这种多样性的磁盘对我们很有用,因为广义上,我们可以将您的应用程序可能提供的工作负载分为两组。首先,那些需要对文件系统进行快速随机读取和/或写入的工作负载。属于这一类别的工作负载包括数据库、Web 服务器和引导卷。对于这些工作负载,性能的限制通常是每秒 I/O 操作(IOPS)。其次,有一些工作负载需要尽可能快地从磁盘进行顺序读取。这包括 Map Reduce、日志管理和数据存储,如 Kafka 或 Casandra,这些应用程序已经专门优化,尽可能地进行顺序读取和写入。

在实例级别存在硬性上限,限制了您可以通过 EBS 卷实现的最大性能。附加到单个实例的所有 EBS 卷的最大 IOPS 在 c5 和 m5 实例上可达到 64,000。最小的 c5 和 m5 实例只提供 1,600 IOPS。需要牢记这些限制,无论是如果您想在较小的 EC2 实例类型上运行需要更高磁盘性能的工作负载,还是在较大的实例类型上使用多个 EBS 卷。

gp2

gp2 EBS 卷应该是大多数通用应用的首选。它们以适中的价格提供固态硬盘(SSD)性能。gp2卷的性能基于一个信用系统。这些卷提供基准性能,并随着时间累积信用,允许在需要时性能突发到 3,000 IOPS,直到累积的信用用尽。

当创建一个gp2卷时,它会自动获得一个信用余额,允许它在 30 分钟内突发到 3,000 IOPS。当卷被用作引导卷或需要快速复制数据到卷作为引导过程的一部分时,这非常有用。

突发积分的积累速度和gp2卷的基准性能与卷的大小成正比。小于 33 GiB 的卷始终具有 100 IOPS 的最低基准性能。大于 1 TB 的卷的基准性能大于 3,000 IOPS,因此您不需要考虑突发积分。单个gp2卷可用的最大性能为 3.3 TB(及更大)的卷的 10,000 IOPS。

如果您的工作负载需要从gp2卷中获得更高的性能,一个快速的解决方法是使用更大的卷(即使您的应用程序不需要它提供的存储空间)。

您可以通过将 IOPS 乘以块大小(256 KiB)来计算卷支持的最大吞吐量。但是,gp2卷将总吞吐量限制为 160 MiB/s,因此大于 214 GiB 的卷将仅提供 160 MiB/s。

监视与磁盘使用相关的指标的能力对于了解磁盘性能如何影响您的应用程序,并确定您何时以及在哪里达到性能限制非常宝贵。

io2

对于可靠性能至关重要且gp2卷无法提供足够 IOPS 的应用程序,可以使用io2卷(也称为预留 IOPS 卷)。如果它们所附加的实例支持它们,io2卷可以被配置为提供最多 32,000 IOPS。创建io2实例时,需要预先指定所需的 IOPS(我们将在第九章中讨论如何在 Kubernetes 中执行此操作,存储状态)。可以为单个卷配置的最大 IOPS 取决于卷的大小,IOPS 和存储的 GiB 之间的比率为50:1。因此,为了配置最大 IOPS,您需要请求至少 640 GiB 的卷。

对于所需 IOPS 数量小于gp2卷支持的 IOPS(10,000)且所需吞吐量小于 160 MiB/s 的情况,支持类似性能特征的gp2卷通常会比io2卷的价格低一半。除非您知道自己需要io2卷的增强性能特征,否则大多数通用用途都应坚持使用gp2卷。

st1

对于已经针对顺序读取进行了优化的应用程序,其中主要的性能指标是吞吐量,也许令人惊讶的是,尽管 SSD 目前占据主导地位,但最佳性能仍然由旋转磁盘提供。

st1(和sc1)卷是 AWS 上可用的最新类型的 EBS 卷。它们旨在为诸如 Map Reduce、日志处理、数据仓库和流式工作负载(如 Kafka)等工作负载提供高吞吐量。st1 卷以不到 gp2 实例成本的一半提供高达 500 MiB/s 的吞吐量。缺点是它们支持的 IOPS 要低得多,因此对于随机或小写入来说性能要差得多。您可能会对 SSD 进行的 IOPS 计算略有不同,因为块大小要大得多(1 MB 对比 256 KB)。因此,进行小写入将花费与顺序写入完整 1 MB 块一样长的时间。

如果您的工作负载已经正确优化以利用 st1 卷的性能特性,那么考虑使用它们是非常值得的,因为成本大约是 gp2 卷的一半。

就像 gp2 卷一样,st1 使用了性能突发模型。然而,积累的信用额允许吞吐量突破基准性能。基准性能和信用额积累速度与卷大小成正比。对于大于 2 TiB 的卷,最大突发性能为 500 MiB/s,对于大于 12.5 TiB 的卷,最大基准性能为 500 MiB/s,对于这样大小(或更大)的卷,无需考虑突发特性,因为性能是恒定的。

sc1

sc1卷提供了 AWS 上最低成本的块存储。它们提供了与st1卷类似的性能配置文件,但大约只有一半的速度,成本也只有一半。您可以考虑将它们用于需要从文件系统存储和检索数据,但访问不太频繁或性能对您来说不那么重要的应用程序。

sc1卷可以被视为归档或 blob 存储系统(如s3)的替代方案,因为其成本大致相似,但具有无需使用特殊库或工具即可读写数据的优势,并且在数据可以被读取和使用之前具有更低的延迟。

在 Kafka 或日志管理等用例中,你可能会考虑使用 sc1 卷来存储旧数据,这样可以保持在线存储,以便立即使用,但访问频率较低,因此你希望优化存储成本。

网络

在运行分布式系统时,网络性能可能是应用程序整体可观察性能的关键因素。

鼓励构建应用程序的架构模式,其中不同组件之间的通信主要通过网络进行(例如,SOA 和微服务),会导致应用程序中的集群内部网络成为性能瓶颈。集群数据存储在进行写操作以及在扩展或维护操作期间重新平衡集群时,也可能对集群内部网络提出高要求。

当运行暴露给互联网或其他广域网的服务时,网络性能当然也是需要考虑的因素。

最新一代的 EC2 实例类型受益于 AWS 描述为增强网络的网络接口。要从中受益,你需要运行相对较新的实例类型(M5、C5 或 R4),并安装亚马逊的弹性网络适配器的特殊网络驱动程序。幸运的是,如果你使用主要 Linux 发行版的官方 AMI,这些都应该已经为你完成。

你可以使用 modinfo 命令检查是否安装了正确的驱动程序:

    $ modinfo ena
    filename:          /lib/modules/4.4.11- 
    23.53.amzn1.x86_64/kernel/drivers/amazon/net/ena/ena.ko
    version:            0.6.6
    license:            GPL
    description:      Elastic Network Adapter (ENA)
    author:             Amazon.com, Inc. or its affiliates
    ... 

如果未安装 弹性网络接口 的驱动程序,你将看到类似以下的内容:

    $ modinfo ena
    ERROR: modinfo: could not find module ena

增强网络带来的性能提升并不需要额外费用,因此在准备生产时,你应该检查是否正确配置了增强网络。常见使用中唯一不支持增强网络的实例类型是 t2 可突发性能实例。

EC2 实例的网络性能与实例大小成正比,每种实例类型中最大的实例大小才能达到 10 或 20 GBps 的网络吞吐量。即使使用最大的 EC2 实例大小,只有在与集群放置组中的其他实例进行通信时,才能实现网络吞吐量的最大值。

集群放置组可用于请求亚马逊在其数据中心的特定区域同时启动您需要的每个实例,以便获得最快的速度(和最低的延迟)。为了提高网络性能,我们可以调整两个变量:

  • 增加实例大小:这样可以使实例获得更快的网络,并增加共存,从而更有可能在服务之间进行本地主机网络调用。

  • 将您的实例添加到集群放置组:这可以确保您的实例在物理上靠近,从而提高网络性能。

在做出这样的决定之前,你需要知道网络是否真的是你的性能瓶颈,因为所有这些选择会使你的集群更容易受到 AWS 基础设施中潜在故障的影响。因此,除非你已经知道你的特定应用程序会对集群网络提出特定要求,否则不应该试图优化以获得更高的性能。

安全

以下图表显示了一些影响安全性的关键领域:

保护集群基础设施的配置和软件的安全性至关重要,特别是如果您计划将其上运行的服务暴露到互联网上。

你应该考虑,如果你将服务暴露到公共互联网上,而这些服务有众所周知的软件漏洞或配置错误,可能只是几个小时之内,你的服务就会被用于扫描易受攻击系统的自动化工具所发现。

重要的是,你要将集群的安全性视为一个不断变化的目标。这意味着你或者你使用的工具需要意识到新的软件漏洞和配置漏洞。

Kubernetes 软件和主机的基础操作系统软件的漏洞将由 Kubernetes 社区和您的操作系统供应商进行更新和修补,只需要操作员有一个应用更新的程序即可。

环境配置更为关键,因为验证其安全性和正确性的责任完全落在你的肩上。除了花时间验证和测试配置外,你还应该将配置的安全性视为一个不断变化的目标。在更新时,你应该确保花时间审查 Kubernetes 变更日志中的更改和建议。

始终进行更新

Kubernetes 的新次要版本大约每三个月发布一次。该项目可以对每个发布的次要版本发布补丁级更新,频率最多每周一次。补丁级更新通常包括修复更重大的错误和安全问题的修复。Kubernetes 社区目前同时支持三个次要版本,随着每个新的次要版本发布,最旧的受支持版本的常规补丁级更新将结束。这意味着在计划和构建集群时,您需要计划对 Kubernetes 软件进行两种维护:

  • 补丁级更新:每个月多次:

  • 这些应该保持非常紧密的兼容性,大多数情况下应该是微不足道的。

  • 它们应该简单易行,几乎没有(或没有)停机时间。

  • 次要版本升级:每 3 到 9 个月:

  • 在次要版本之间升级时,您可能需要对集群的配置进行微小更改。

  • Kubernetes 确实保持良好的向后兼容性,并且有一种在删除或更改配置选项之前废弃配置选项的策略。只需记住在更改日志和日志输出中注意废弃警告。

  • 如果您正在使用第三方应用程序(或编写了自己的工具),这些应用程序依赖于测试版或 alpha API,则可能需要在升级集群之前更新这些工具。只使用稳定 API 的工具应该在次要版本更新之间继续工作。

    • 您可能需要考虑以下事项:
  • 一个测试环境,您可以在其中应用 Kubernetes 软件的更新,以验证任何更改,然后再将其发布到生产环境。

  • 如果您检测到任何错误,可以通过程序或工具回滚任何版本升级。

  • 监控可以让您确定您的集群是否按预期运行。

  • 您用于更新组成集群的机器上的软件的程序确实取决于您使用的工具。

您可能采取两种主要策略——就地升级和基于不可变镜像的更新策略。

就地更新

有几种工具可以让您升级集群节点的底层操作系统。例如,对于基于 Debian 的系统,可以使用unattended-upgrades工具,对于基于 Red Hat 的系统,可以使用yum-cron工具,这些工具可以在没有任何操作员输入的情况下在节点上安装更新的软件包。

当然,在生产环境中,如果特定更新导致系统失败,这可能有一定风险。

通常,如果您正在管理具有自动更新的系统,您将使用软件包管理器将基本组件(如 Kubernetes 和 etcd)固定到特定版本,然后以更受控制的方式升级这些组件,可能使用配置管理工具,如 Puppet、Chef 或 Ansible。

以这种自动化方式升级软件包时,当更新某些组件时,系统需要重新启动。诸如 KUbernetes REboot Daemon(Kured)(github.com/weaveworks/kured)之类的工具可以监视特定节点需要重新启动的信号,并编排重新启动集群中的节点,以维护集群上运行的服务的正常运行时间。首先通过发出信号通知 Kubernetes Scheduler 重新调度工作负载到其他节点,然后触发重新启动。

还有一种新型操作系统,例如 CoreOS 的 Container Linux 或 Google 的 Container-Optimized OS,对更新采取了略有不同的方法。这些新的面向容器的 Linux 发行版根本不提供传统的软件包管理器,而是要求您将不在基本系统中运行的所有内容(如 Kubernetes)作为容器运行。

这些系统处理基本操作系统的更新方式更像是在消费类电子产品中找到的固件更新系统。这些操作系统中的基本根文件系统是只读的,并且从两个特殊分区中挂载。这允许系统在后台下载新的操作系统镜像到未使用的分区。当系统准备好升级时,它将被重新启动,并且来自第二分区的新镜像将被挂载为根文件系统。

这样做的好处是,如果升级失败或导致系统变得不稳定,可以简单地回滚到上一个版本;事实上,这个过程甚至可以自动化。

如果您正在使用 Container Linux,您可以使用 Container Linux Update Operator 来编排由于操作系统更新而需要重新启动的操作(github.com/coreos/container-linux-update-operator)。使用这个工具,您可以确保在重新启动之前,主机上的工作负载被重新调度。

不可变镜像

虽然有工具可以帮助管理原地升级您的主机,但是采用不可变镜像的策略也有一些优势。

一旦您使用 Kubernetes 管理运行在基础架构上的应用程序,需要安装在节点上的软件就变得标准化了。这意味着管理主机配置的更新变得更加简单,因为它们是不可变的镜像。

这可能很有吸引力,因为它允许您以与使用 Docker 构建应用程序容器类似的方式来管理构建和部署节点软件。

通常,如果采用这种方法,您将希望使用一种工具来简化以 AMI 格式构建镜像并使其可用于其他工具启动新的 EC2 实例以替换使用先前镜像启动的实例。packer 就是这样一种工具。

网络安全

在 AWS 上运行 Kubernetes 时,您需要配置四个不同的层次,以正确地保护集群上的流量。

基础节点网络

为了使 Pod 和服务之间的流量在集群上传递,您需要配置应用于节点的 AWS 组以允许此流量。如果您使用覆盖网络,这通常意味着允许特定端口上的流量,因为所有通信都是封装在单个端口上传递的(通常作为 UDP 数据包)。例如,flannel 覆盖网络通常配置为通过端口 7890 上的 UDP 进行通信。

当使用原生 VPC 网络解决方案,比如amazon-vpc-cni-k8s时,通常需要允许所有流量在节点之间传递。amazon-vpc-cni-k8s插件将多个 Pod IP 地址与单个弹性网络接口关联起来,因此通常无法使用安全组以更精细的方式管理基础架构节点网络。

节点-主节点网络

在正常操作中,运行在您的节点上的 kubelet 需要连接到 Kubernetes API 以发现它预期运行的 Pod 的定义。

通常,这意味着允许工作节点向控制平面安全组的 443 端口进行 TCP 连接。

控制平面连接到暴露在端口 10250 上的 API 上的 kubelet。这对于logsexec功能是必需的。

外部网络

正确理解外部集群允许访问节点的流量是保持集群安全的关键部分。

最近,一些研究人员发现了大量本来受到保护的集群,允许任何人在互联网上访问 Kubernetes 仪表板,从而访问集群本身。

通常,在这些情况下,集群管理员未能正确配置仪表板以对用户进行身份验证。但是,如果他们仔细考虑了向更广泛的互联网公开的服务,可能会避免这些违规行为。仅将这样的敏感服务暴露给特定 IP 地址或通过 VPN 访问您的 VPC 的用户,将提供额外的安全层。

当您想要将服务(或入口控制器)暴露给更广泛的互联网时,Kubernetes 负载均衡器服务类型将为您配置适当的安全组(以及提供弹性负载均衡器ELB))。

Kubernetes 基础设施- pod 网络

Kubernetes 默认情况下不提供控制集群上运行的 pod 之间的网络访问的任何设施。集群上运行的任何 pod 都可以连接到任何其他 pod 或服务。

对于完全受信任的应用程序的较小部署来说,这可能是合理的。如果您想要提供策略来限制集群上运行的不同应用程序之间的连接,则需要部署一个网络插件,该插件将执行 Kubernetes 网络策略,例如 Calico、Romana 或 WeaveNet。

虽然有很多网络插件可用于支持 Kubernetes 网络策略的执行,但如果您选择使用 AWS 支持的原生 VPC 网络,建议使用 Calico,因为 AWS 支持此配置。AWS 提供了示例配置,以在其 GitHub 存储库中部署 Calico 与amazon-vpc-cni-k8s插件:github.com/aws/amazon-vpc-cni-k8s

Kubernetes API 提供了NetworkPolicy资源,以提供控制流量从 pod 进入和流出的策略。每个NetworkPolicy都以标签选择器和命名空间为目标,影响它将影响的 pod。由于默认情况下 pod 没有网络隔离,如果您希望严格提供默认的NetworkPolicy以阻止尚未提供特定网络策略的 pod 的流量,这可能是有用的。

请查看 Kubernetes 文档,了解一些默认网络策略的示例,以便默认情况下允许或拒绝所有流量:kubernetes.io/docs/concepts/services-networking/network-policies/#default-policies

IAM 角色

Kubernetes 与 AWS 有一些深度集成。这意味着 Kubernetes 可以执行诸如提供 EBS 卷并将其附加到集群中的 EC2 实例、设置 ELB 以及为您配置安全组等任务。

为了使 Kubernetes 具有执行这些操作所需的访问权限,您需要提供 IAM 凭据,以允许控制平面和节点获得所需的访问权限。

通常,最方便的方法是将与相关 IAM 角色关联的实例配置文件附加到实例上,以授予实例上运行的 Kubernetes 进程所需的权限。在第三章中,云端之手中,我们使用kubeadm启动了一个小集群的示例。在规划生产集群时,您还应该考虑一些其他因素:

  • 您是否运行多个集群? 您是否需要隔离集群资源?

  • 您的集群上运行的应用程序是否还需要访问需要身份验证的 AWS 内部资源?

  • 您的集群中的节点是否需要使用 AWS IAM Authenticator 对 Kubernetes API 进行身份验证?如果您正在使用 Amazon EKS,这也适用。

如果您在 AWS 账户中运行多个集群(例如,用于生产和暂存或开发环境),值得考虑如何定制 IAM 角色,以防止集群干扰彼此的资源。

理论上,一个集群不应该干扰另一个集群创建的资源,但您可能会重视每个环境单独提供的 IAM 角色所提供的额外安全性。在生产和开发或分段环境之间不共享 IAM 角色是一个良好的做法,可以防止一个环境中的配置错误(甚至是 Kubernetes 中的错误)对与另一个集群关联的资源造成伤害。Kubernetes 交互的大多数资源都带有kubernetes.io/cluster/<cluster name>标签。对于其中一些资源,IAM 提供了将某些操作限制为与该标签匹配的资源的能力。以这种方式限制删除操作是减少潜在危害的一种方式。

当集群上运行的应用程序需要访问 AWS 资源时,有多种方法可以向 AWS 客户端库提供凭据,以便正确进行身份验证。您可以将凭据作为配置文件或环境变量挂载为秘密,然后提供给您的应用程序。但提供 IAM 凭据的最便捷的方法之一是使用与实例配置文件相同的机制将 IAM 角色与您的 pod 关联起来。

诸如kube2iamkiam之类的工具拦截 AWS 客户端库对元数据服务的调用,并根据 pod 上设置的注释提供令牌。这允许 IAM 角色作为您正常部署过程的一部分进行分配。

kiam (github.com/uswitch/kiam) 和 kube2iam (github.com/jtblin/kube2iam) 是两个类似的项目,旨在为 Kubernetes pod 提供 IAM 凭据。这两个项目都作为每个节点上的代理运行,添加网络路由以路由到 AWS 元数据服务的流量。kiam 另外还运行一个负责从 AWS API 请求令牌并维护所有运行中 pod 所需凭据的缓存的中央服务器组件。这种方法在生产集群中被认为更可靠,并减少了节点代理所需的 IAM 权限。

使用这些工具之一的另一个优势是,它可以防止集群上运行的应用程序使用分配给底层实例的权限,从而减少了应用程序可能错误或恶意访问资源以提供控制平面服务的风险。

验证

在设置集群时,您可能会做出许多不同选择来配置您的集群。重要的是,您需要一种快速验证集群是否能够正确运行的方法。

这是 Kubernetes 社区为了证明不同的 Kubernetes 发行版是“一致的”而解决的问题。为了获得特定 Kubernetes 发行版的一致性认证,需要对集群运行一组集成测试。这些测试对于供应预打包的 Kubernetes 安装的供应商来证明其发行版是否正确运行非常有用。对于集群操作员来说,它也非常有用,可以快速验证软件更新或配置更改是否使集群处于可操作状态。

Kubernetes 一致性测试基于 Kubernetes 代码库中的一些特殊自动化测试。这些测试作为 Kubernetes 代码库端到端验证的一部分运行在测试集群上,并且在每次对代码库的更改合并之前必须通过。

当然,您可以下载 Kubernetes 代码库(并设置 Golang 开发环境)并配置它直接运行一致性测试。但是,有一个名为Sonobuoy的工具可以为您自动化这个过程。

Sonobuoy 可以简化在集群上以简单和标准化的方式运行一组 Kubernetes 一致性测试。使用 Sonobuoy 的最简单方法是使用托管的基于浏览器的服务scanner.heptio.com/。该服务会提供一个清单供您提交到您的集群,然后在测试完成后显示测试结果。如果您想在自己的集群上运行所有内容,可以安装一个命令行工具,按照github.com/heptio/sonobuoy上的说明运行测试并收集结果。

Kubernetes 一致性测试很重要,因为它涵盖了各种 Kubernetes 功能,可以在部署应用程序之前提前警告您是否存在任何配置错误。当您更改集群配置时,如果更改可能影响集群功能,这些测试会非常有帮助。

尽管 Kubernetes 一致性测试侧重于测试集群的功能,安全基准测试会检查集群的配置是否符合已知的不安全配置设置,确保集群配置符合当前的安全最佳实践。

互联网安全中心发布了逐步检查清单,您可以手动按照这些清单来测试集群是否符合安全最佳实践。

您可以免费下载这些基准测试的副本:www.cisecurity.org/benchmark/kubernetes/

在构建集群时阅读并遵循这些清单中的建议可能会很有用,因为它将帮助您理解特定配置值的原因。

一旦您设置好了集群,自动验证配置可能会很有用,以便在更新和更改时避免配置意外偏离安全配置。

kube-bench是一个工具,它提供了一种自动运行 CIS 基准测试的方式:github.com/aquasecurity/kube-bench

您可能会发现编写自己的集成测试也很有用,这些测试可以检查您是否能成功部署和操作自己的一些应用程序。在快速开发集群配置时,这些测试可以作为一个重要的健全性检查。

有许多工具可以用来执行这样的测试。我建议使用您组织中的工程师已经熟悉的任何测试自动化工具。您可以使用专门设计用于运行自动化测试的工具,比如 cucumber,但是一个简单的 shell 脚本,部署一个应用程序到您的集群,然后检查它是否可访问,也是一个很好的开始。

可观测性

可观测性显示在以下图表中:

能够监视和调试集群是设计生产集群时最重要的要点之一。幸运的是,有许多解决方案可以很好地支持 Kubernetes 的日志和指标管理。

日志记录

每当您想要了解您的应用程序在做什么时,大多数运维人员首先想到的是查看应用程序生成的日志。

日志很容易理解,而且不需要任何特殊工具来生成,因为您的应用程序可能已经支持某种形式的日志记录。

在 Kubernetes 中,您可以直接查看和追踪应用程序写入标准输出和标准错误的日志。如果您在自己的计算机或服务器上使用过docker logs命令,那么使用kubectl logs命令应该对您来说很熟悉。

这比登录每个节点查看特定容器生成的日志更方便。除了查看特定 pod 的日志外,kubectl logs还可以显示与特定标签表达式匹配的所有 pod 的日志。

如果您需要搜索应用程序生成的日志以查找特定事件,或者如果您需要查看过去特定时间生成的日志,那么您需要考虑部署一个解决方案来聚合和管理您的日志。

实现此功能的最常用工具是Fluentd。Fluentd 是一个非常灵活的工具,可以用于从各种来源收集日志,然后将其推送到一个或多个目的地。如果您的组织已经维护或使用第三方工具来聚合应用程序日志,您几乎肯定会找到一种方法来配置 Fluentd 以将运行在 Kubernetes 上的应用程序的应用程序日志存储在您选择的工具中。Fluentd 团队和更广泛的社区维护着超过 800 个不同的插件,支持许多不同的输入、输出和过滤选项。

由于 Fluentd 是基于 Ruby 编程语言构建的,它的插件使用 Rubygems 软件包系统进行分发。按照惯例,所有 Fluentd 插件的名称都以fluent-plugin开头,并且当前所有可用的插件都在此处列出:www.fluentd.org/plugins/all。由于其中一些插件是由更广泛的社区维护的,因此值得对您计划使用的插件进行一些初始测试。插件的质量可能有所不同,这取决于特定插件所处的开发阶段以及维护频率。您可以使用gem install命令安装和管理 Fluentd 插件,或者使用bundler工具控制 Fluentd 插件的确切版本。您可以在此处阅读有关在 Fluentd 安装中安装插件的更多信息:docs.fluentd.org/v1.0/articles/plugin-management

监控

查看应用程序的日志输出可能是有用的,如果您知道应用程序存在问题并希望调试原因。但是,如果您不知道系统中出现问题的位置,或者只是想评估系统的健康状况,那么这将变得更加困难。

您的日志非常灵活,因为您的应用程序可以以非结构化的方式向日志端点写入任何信息。在大型系统中,这可能会变得非常压倒,以及需要过滤和分析此输出的工作量可能会变得复杂。

监控或指标收集采取了不同的方法。通过定义反映系统、Kubernetes 和基础设施的性能和运行情况的测量,您可以更快地回答有关系统健康和性能的问题。

收集的指标也是自动警报系统中最有用的信息源之一。它们可以警告您的组织成员有关应用程序或基础设施异常行为。

有许多商业和开源工具可用于收集指标并创建警报。您所做的决定很可能会受到您的组织和您的要求的影响。

正如我已经说过的,试图一次向您的组织引入太多新工具或流程可能会冒险。在许多情况下,许多监控工具已经支持与 Kubernetes 集成。如果是这种情况,考虑继续使用您的组织习惯使用的现有工具可能是明智的。

无论您选择哪些工具来记录应用程序、集群和基础设施的指标,您都应该仔细考虑如何使负责开发和部署应用程序的组织成员能够轻松地展示他们的指标。作为规划集群的一部分,尝试编写公开指标的流程文档,该文档应该由部署新应用程序到您的集群的开发人员遵循。您应该尽量使这个过程尽可能简单。如果需要自动化流程的步骤并提供默认配置值,您应该这样做以使流程简单化。如果从应用程序中导出新指标的过程复杂或需要大量手动步骤,那么您的组织的应用程序暴露它们的可能性就会降低。

如果流程简单且无摩擦,那么通过默认监控文化变得更加简单。例如,如果您选择使用 Prometheus,您可以像这样记录流程:

    • 在端口9102上暴露一个端点/metrics
  • 向您的 pod 添加注释"prometheus.io/scrape": true

在这个例子中,通过配置具有合理默认值的 Prometheus,从 pod 中暴露指标对于开发人员来说变得快速简单。可以暴露更复杂的配置方式,以便 Prometheus 抓取指标,但是通过使用众所周知的默认值,可以使设置过程更简单,并且可以更容易地在应用程序中包含标准的 Prometheus 库。无论您选择使用哪种系统来收集指标,尽量在可能的情况下遵循这些原则。

直接从应用程序 pod 和基础设施收集指标可以提供有关应用程序行为的深入丰富的信息。当您需要了解应用程序的具体信息时,这些信息非常有用,并且在预防问题方面非常有用。例如,关于磁盘使用情况的指标可以用于提供警报,警告操作员有可能导致应用程序失败的状态。

黑匣子监控

虽然特定于应用程序的指标提供了有用的根本原因分析和预警洞察,但黑匣子监控采取了相反的方法。通过将应用程序视为封闭实体,并执行面向用户的端点,您可以展现性能不佳的应用程序的症状。黑匣子监控可以通过使用诸如 Prometheus Blackbox 导出器之类的工具来实现。但另一个常见的模式是使用商业服务。其主要优势在于它们通常允许您从多个位置(也许是全球范围内)探测应用程序,真正地在用户和应用程序之间的完整基础设施堆栈上进行探测。

警报

记录关于在 Kubernetes 上运行的系统状态的指标是使您的系统易于观察的第一阶段。收集了指标之后,有几种方法可以使您收集的数据易于采取行动。

大多数指标收集工具都提供了一些方法来为组织中不同成员重要的指标构建图形和仪表板。例如,许多 Prometheus 用户使用 Grafana 构建仪表板来公开重要的指标。

虽然仪表板是了解特定系统或业务流程的表现的好方法,但你的系统有一些方面需要更主动的方法。

任何值得一试的度量收集系统都会提供一种向组织成员发出警报的方式。然而,当你收集度量和使用任何系统向团队发送警报时,有一些原则你应该考虑:

  • 警报应该是可执行的:当将仪表板上的图表或仪表上的度量提升为警报时,确保只对需要立即人工干预的状态发送警报,而不仅仅是警告或信息。警告或信息性警报应该出现在你的仪表板上,而不是在你的寻呼机上。

  • 警报应该节制使用:警报会打断人们当前正在做的事情:工作、休息,甚至最糟糕的是睡觉。如果一个人收到太多的警报,它们可能会成为压力的原因,并且在警报疲劳设置并且失去吸引注意力的力量。在设计警报机制时,你应该考虑记录你的组织成员被你的警报打断的频率。

警报应该是有针对性的——你应该考虑谁应该对特定的警报负责并适当地指导它。警报可以指向多个系统,如 bug 跟踪器、电子邮件、聊天系统,甚至寻呼应用程序。重要的是,接收组织中最关键的警报的人能够承担责任并管理响应。不太重要的警报可能会分配给 bug 跟踪工具中的一个团队或组。如果你的组织使用聊天系统,如 Slack、HipChat 或 IRC,你可能希望将特定应用程序的警报指向团队使用的频道或房间,该团队开发或负责该应用程序的运行。只需记住确保保持在可接受的水平,否则你的警报很快就会被需要知道它们的人忽视。

追踪

追踪是可观察性家族中最年轻的成员,因此通常是组织选择实施的最后一个。追踪系统的理念是测量单个请求通过你的应用程序所需的时间。

这可能不会暴露比为单体应用程序配置良好的指标更有趣的信息。但对于具有分布式或微服务架构的大规模系统,其中单个请求可能通过数十甚至数百个独立进程,跟踪可以帮助准确定位性能问题发生的时间和地点。

在实施从应用程序收集跟踪信息的系统时,您有多种选择。

AWS 的内置跟踪解决方案包括 X-Ray,支持 Java、Go、Node.js、Python、Ruby 和.NET 应用程序。对于这些技术,向您的应用程序添加分布式跟踪只是向应用程序添加库并正确配置的问题。aws.amazon.com/xray/

与 AWS 的解决方案竞争的是一些旨在在 OpenTracing 旗帜下共同工作的工具。

OpenTracing 为九种语言提供了客户端库,这些库与九种不同的开源和商业工具兼容,旨在收集跟踪数据。由于 OpenTracing 的开放性质,一些应用程序框架和基础设施组件选择添加对其跟踪格式的支持。您可以在opentracing.io了解更多关于 OpenTracing 的信息。

总结

本章希望能让您了解在决定在生产环境中运行 Kubernetes 时,您可以做出的多种不同选项和决策。不要因为选项和选择的深度和广度而却步,因为 Kubernetes 非常容易上手,特别是在 AWS 上。

在下一章中,我们将开始实际工作,设置集群并准备开始工作。我们不可能涵盖所有选项,更不用说 Kubernetes 周围社区制作的所有附加组件和附加工具,但我们将提供一个稳定的起点,让您可以开始实施自己的计划。

希望本章能为您和您的团队提供一个指南,讨论和规划满足您组织需求的集群。然后,您可以开始实施在阅读本章时可能确定的功能和功能。

如果在启动自己的集群时有一件事要记住:保持简单,傻瓜。 Kubernetes 使您能够在需要时轻松向您的工具库中添加新工具,因此不要过于复杂或过快地过度设计。从可能起作用的最简单设置开始,即使您认为需要稍后添加复杂性;通常,您会发现简单的解决方案完全有效。

利用 Kubernetes 本身的优势,它将允许您快速发展基础设施,从小处开始,需要时再添加功能和工具到系统中,而不是提前添加!

第七章:一个适合生产的集群

在上一章中,我们花了一些时间思考规划 Kubernetes 集群的框架。希望对你来说应该很清楚,构建集群时需要根据正在运行的系统的要求做出许多决策。

在本章中,我们将采取更加实际的方法来解决这个问题。我们将不再试图涵盖我们可以使用的众多选项,而是首先做出一些选择,然后构建一个完全功能的集群,作为许多不同用例的基础配置。

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

  • Terraform

  • 准备节点镜像和节点组

  • 配置附加组件

构建一个集群

本章中包含的信息只是构建和管理集群的一种可能方式。构建 Kubernetes 集群时,有许多选择要做,几乎可以选择同样多的工具。出于本章的目的,我选择使用可以简单说明构建集群过程的工具。如果您或您的团队更喜欢使用不同的工具,那么本章中概述的概念和架构将很容易转移到其他工具上。

在这一章中,我们将以一种更适合生产工作负载的方式启动我们的集群。我们在这里所做的大部分工作都将与第三章“云端之手”中的内容相似,但我们将在两个关键方面进一步完善我们在那里概述的流程。首先,在构建依赖的基础设施时,能够快速部署新的基础设施实例是非常重要的,而且要能够重复进行。我们希望能够做到这一点,因为这样可以简单地测试我们想要对基础设施进行的变更,而且是无风险的。通过自动化 Kubernetes 集群的配置,我们实现了之前讨论过的不可变基础设施模式。我们可以快速部署一个替代集群,然后在将工作负载迁移到新集群之前进行测试,而不是冒险升级或更改我们的生产基础设施。

为了实现这一点,我们将使用 Terraform 基础设施配置工具与 AWS 进行交互。Terraform 允许我们使用一种类似编程语言的方式将我们的基础设施定义为代码。通过将基础设施定义为代码,我们能够使用诸如版本控制之类的工具,并遵循其他软件开发实践来管理我们基础设施的演变。

在本章中,我们将做出许多关于在 AWS 上运行的 Kubernetes 集群应该是什么样子以及如何管理的决定。对于本章和我们将要处理的示例,我心中有以下要求。

  • 说明性:我们将看到符合平均生产用例要求的 Kubernetes 集群是什么样子。这个集群反映了我在设计用于真实生产的 Kubernetes 集群时所做的决定。为了使本章尽可能清晰易懂,我尽量保持集群及其配置尽可能简单。

  • 灵活性:我们将创建一些您可以视为模板并添加或更改以满足您需求的东西。

  • 可扩展性:无论您设计 Kubernetes 集群(或者任何基础设施),都应该考虑您现在所做的决定可能会阻止您以后扩展或扩展该基础设施。

显然,当您构建自己的集群时,您应该对自己的需求有一个更加具体的想法,这样您就能够根据自己的需求定制您的集群。我们将在这里构建的集群将是任何生产就绪系统的绝佳起点,然后您可以根据需要自定义和添加。

本章中的许多配置都已经被缩短了。您可以在github.com/PacktPublishing/Kubernetes-on-AWS/tree/master/chapter07查看本章中使用的完整配置。

开始使用 Terraform

Terraform 是一个命令行工具,您可以在工作站上运行它来对基础设施进行更改。Terraform 是一个单一的二进制文件,只需安装到您的路径上即可。

您可以从www.terraform.io/downloads.html下载 Terraform,支持六种不同的操作系统,包括 macOS、Windows 和 Linux。下载适用于您操作系统的 ZIP 文件,解压缩,然后将 Terraform 二进制文件复制到您的路径上。

Terraform 使用扩展名为.tf的文件来描述您的基础架构。因为 Terraform 支持在许多不同的云平台上管理资源,它可以包含相关提供者的概念,这些提供者根据需要加载,以支持不同云提供商提供的不同 API。

首先,让我们配置 AWS Terraform 提供者,以便准备构建一个 Kubernetes 集群。创建一个新目录来保存 Kubernetes 集群的 Terraform 配置,然后创建一个文件,在其中我们将配置 AWS 提供者,如下所示的代码:

aws.tf
provider "aws" { 
  version = "~> 1.0" 

  region = "us-west-2" 
} 

保存文件,然后运行以下命令:

terraform.init
Initializing provider plugins... 
- Checking for available provider plugins on https://releases.hashicorp.com... 
- Downloading plugin for provider "aws" (1.33.0)... 
Terraform has been successfully initialized! 

当您使用支持的提供者时,Terraform 可以发现并下载所需的插件。请注意,我们已经配置了提供者的 AWS 区域为us-west-2,因为这是我们在本例中将要启动集群的区域。

为了让 Terraform 与 AWS API 通信,您需要为 AWS 提供者提供一些凭据。我们在第三章中学习了如何获取凭据,云的探索。如果您遵循了第三章中的建议,并使用aws configure命令设置了您的凭据,那么 Terraform 将从您的本地配置文件中读取默认凭据。

另外,Terraform 可以从AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY环境变量中读取 AWS 凭据,或者如果您在 EC2 实例上运行 Terraform,它可以使用 EC2 实例角色提供的凭据。

也可以通过在 AWS 提供者块中内联添加access_keysecret_key参数来静态配置凭据,但我并不真的推荐这种做法,因为这样会使得将配置检入版本控制系统变得更加困难。

默认情况下,Terraform 使用名为terraform.tfstate的本地文件来跟踪您基础架构的状态。这样可以跟踪自上次运行 Terraform 以来对配置所做的更改。

如果你将是唯一管理基础架构的人,那么这可能是可以接受的,但你需要安全地备份状态文件。它应被视为敏感信息,如果丢失,Terraform 将无法正常运行。

如果您正在使用 AWS,我建议使用 S3 作为后端。您可以在 Terraform 文档中阅读如何设置这一点,网址为www.terraform.io/docs/backends/types/s3.html。如果配置正确,S3 存储是非常安全的,如果您正在团队中工作,那么您可以利用 DynamoDB 表作为锁,以确保多个 Terraform 实例不会同时运行。如果您想使用这个功能,请在backend.tf文件中设置配置,否则删除该文件。

变量

Terraform 允许我们定义变量,以使我们的配置更具重用性。如果以后您想要将您的配置用作模块来定义多个集群,这将特别有用。我们不会在本章中涵盖这一点,但我们可以遵循最佳实践并定义一些关键变量,以便您可以简单地塑造集群以满足您的需求。

创建一个variables.tf文件来包含项目中的所有变量是标准的。这很有帮助,因为它作为关于如何控制您的配置的高级文档。

正如你所看到的,选择变量的描述性名称并添加可选的描述字段之间,整个文件都相当自解释。因为我为每个变量提供了默认值,所以我们可以在不传递这些变量的任何值的情况下运行 Terraform,如下面的代码所示:

variables.tf
variable "cluster_name" { 
  default = "lovelace" 
} 

variable "vpc_cidr" { 
  default     = "10.1.0.0/16" 
  description = "The CIDR of the VPC created for this cluster" 
} 

variable "availability_zones" { 
  default     = ["us-west-2a","us-west-2b"] 
  description = "The availability zones to run the cluster in" 
} 

variable "k8s_version" { 
  default     = "1.10" 
  description = "The version of Kubernetes to use" 
} 

网络

我们将首先创建一个配置文件来描述我们的 Kubernetes 集群的网络设置。您可能会注意到这个网络的设计,因为它与我们在第三章中手动创建的网络非常相似,但是增加了一些内容,使其更适合生产环境。

Terraform 配置文件可以用注释进行文档化,为了更好地说明这个配置,我提供了一些注释形式的评论。你会注意到它们被/**/包围着。

为了支持高可用性,我们将为多个可用区域创建子网,如下面的代码所示。在这里,我们使用了两个,但如果您想要更高的弹性,您可以轻松地将另一个可用区域添加到availability_zones变量中:

networking.tf
/*  Set up a VPC for our cluster. 
*/resource "aws_vpc" "k8s" { 
  cidr_block           = "${var.vpc_cidr}" 
  enable_dns_hostnames = true 

  tags = "${ 
    map( 
     "Name", "${var.cluster_name}", 
     "kubernetes.io/cluster/${var.cluster_name}", "shared", 
    ) 
  }" 
} 

/*  In order for our instances to connect to the internet 
  we provision an internet gateway.*/ 
resource "aws_internet_gateway" "gateway" { 
  vpc_id = "${aws_vpc.k8s.id}" 

  tags { 
    Name = "${var.cluster_name}" 
  } 
} 

/*  For instances without a Public IP address we will route traffic  
  through a NAT Gateway. Setup an Elastic IP and attach it. 

  We are only setting up a single NAT gateway, for simplicity. 
  If the availability is important you might add another in a  
  second availability zone. 
*/ 
resource "aws_eip" "nat" { 
  vpc        = true 
  depends_on = ["aws_internet_gateway.gateway"] 
} 

resource "aws_nat_gateway" "nat_gateway" { 
  allocation_id = "${aws_eip.nat.id}" 
  subnet_id     = "${aws_subnet.public.*.id[0]}" 
} 

我们将为我们集群使用的每个可用区域提供两个子网。一个公共子网,它可以直接连接到互联网,Kubernetes 将在其中提供可供互联网访问的负载均衡器。还有一个私有子网,Kubernetes 将用它来分配给 pod 的 IP 地址。

因为私有子网中可用的地址空间将是 Kubernetes 能够启动的 pod 数量的限制因素,所以我们为其提供了一个大的地址范围,其中有 16382 个可用的 IP 地址。这应该为我们的集群提供一些扩展空间。

如果您只打算运行对外部不可访问的内部服务,那么您可能可以跳过公共子网。您可以在本章的示例文件中找到完整的networking.tf文件。

计划和应用

Terraform 允许我们通过添加和更改定义基础设施的代码来逐步构建我们的基础设施。然后,如果您希望,在阅读本章时,您可以逐步构建您的配置,或者您可以使用 Terraform 一次性构建整个集群。

每当您使用 Terraform 对基础设施进行更改时,它首先会生成一个将要进行的更改的计划,然后应用这个计划。这种两阶段的操作在修改生产基础设施时是理想的,因为它可以让您在实际应用到集群之前审查将要应用的更改。

一旦您将网络配置保存到文件中,我们可以按照一些步骤安全地为我们的基础设施提供。

我们可以通过运行以下命令来检查配置中的语法错误:

terraform validate  

如果您的配置正确,那么不会有输出,但如果您的文件存在语法错误,您应该会看到一个解释问题的错误消息。例如,缺少闭合括号可能会导致错误,如Error parsing networking.tf: object expected closing RBRACE got: EOF

一旦您确保您的文件已正确格式化为 Terraform,您可以使用以下命令为您的基础设施创建变更计划:

terraform plan -out k8s.plan  

该命令将输出一个摘要,显示如果运行此计划将对基础架构进行的更改。-out标志是可选的,但这是一个好主意,因为它允许我们稍后应用这些更改。如果您在运行 Terraform 计划时注意输出,那么您应该已经看到了这样的消息:

To perform exactly these actions, run the following command to apply:
terraform apply "k8s.plan"

当您使用预先计算的计划运行terraform apply时,它将进行在生成计划时概述的更改。您也可以运行terraform plan命令而不预先生成计划,但在这种情况下,它仍将计划更改,然后在应用更改之前提示您。

Terraform 计算基础架构中不同资源之间的依赖关系,例如,它确保在创建路由表和其他资源之前先创建 VPC。一些资源可能需要几秒钟才能创建,但 Terraform 会等待它们可用后再继续创建依赖资源。

如果您想删除 Terraform 在您的 AWS 帐户中创建的资源,只需从相关的.tf文件中删除定义,然后计划并应用您的更改。当您测试 Terraform 配置时,删除特定配置创建的所有资源可能很有用,以便测试从头开始配置基础架构。如果需要这样做,terraform destroy命令非常有用;它将从基础架构中删除在 Terraform 文件中定义的所有资源。但是,请注意,这可能导致关键资源被终止和删除,因此您不应该在运行中的生产系统上使用此方法。在删除任何资源之前,Terraform 将列出它们,然后询问您是否要删除它们。

控制平面

为了为我们的集群提供一个弹性和可靠的 Kubernetes 控制平面,我们将首次大幅偏离我们在第三章中构建的简单集群,云端的追求

正如我们在第一章中所学到的,“谷歌的基础设施服务于我们其他人”,Kubernetes 控制平面的关键组件是支持 etcd 存储、API 服务器、调度程序和控制器管理器。如果我们想要构建和管理一个弹性的控制平面,我们需要跨多个实例管理这些组件,最好分布在多个可用区。

由于 API 服务器是无状态的,并且调度程序和控制器管理器具有内置的领导者选举功能,因此在 AWS 上运行多个实例相对简单,例如,通过使用自动扩展组。

运行生产级别的 etcd 略微棘手,因为在添加或删除节点时,应小心管理 etcd 以避免数据丢失和停机。在 AWS 上成功运行 etcd 集群是一项相当困难的任务,需要手动操作或复杂的自动化。

幸运的是,AWS 开发了一项服务,几乎消除了在配置 Kubernetes 控制平面时涉及的所有操作复杂性——Amazon EKS,或者使用全名,亚马逊弹性容器服务用于 Kubernetes。

通过 EKS,AWS 将代表您在多个可用区管理和运行组成 Kubernetes 控制平面的组件,从而避免任何单点故障。有了 EKS,您不再需要担心执行或自动化运行稳定 etcd 集群所需的操作任务。

我们应该牢记,使用 EKS 时,我们集群基础设施的关键部分现在由第三方管理。您应该对 AWS 能够比您自己的团队更好地提供弹性控制平面感到满意。这并不排除您设计集群以在控制平面故障时具有一定的抗性的可能性——例如,如果 kubelet 无法连接到控制平面,那么正在运行的容器将保持运行,直到控制平面再次可用。您应该确保您添加到集群中的任何其他组件都能以类似的方式应对临时停机。

EKS 减少了管理 Kubernetes 最复杂部分(控制平面)所需的工作量,从而减少了设计集群和维护集群所需的时间(和金钱)。此外,即使是规模适中的集群,EKS 服务的成本也明显低于在多个 EC2 实例上运行自己的控制平面的成本。

为了让 Kubernetes 控制平面管理 AWS 账户中的资源,你需要为 EKS 提供一个 IAM 角色,EKS 本身将扮演这个角色。

EKS 在你的 VPC 中创建网络接口,以允许 Kubernetes 控制平面与 kubelet 通信,从而提供日志流执行等服务。为了控制这种通信,我们需要在启动时为 EKS 提供一个安全组。你可以在本章的示例文件中的control_plane.tf中找到用于配置控制平面的完整 Terraform 配置。

我们可以使用 Terraform 资源来查询 EKS 集群,以获取用于访问 Kubernetes API 的端点和证书颁发机构。

这些信息,结合 Terraform 的模板功能,允许我们生成一个kubeconfig文件,其中包含连接到 EKS 提供的 Kubernetes API 所需的信息。我们以后可以使用这个文件来配置附加组件。

如果你愿意,你也可以使用这个文件手动连接到集群,使用 kubectl 命令,可以通过将文件复制到默认位置~/.kube/config,或者通过--kubeconfig标志或KUBECONFIG环境变量传递其位置给 kubectl,如下面的代码所示:

KUBECONFIG环境变量在管理多个集群时非常有用,因为你可以通过分隔它们的路径轻松加载多个配置;例如:

将 KUBECONFIG 环境变量设置为$HOME/.kube/config:/path/to/other/conf

kubeconfig.tpl 
apiVersion: v1 
kind: Config 
clusters: 
- name: ${cluster_name} 
  cluster: 
    certificate-authority-data: ${ca_data} 
    server: ${endpoint} 
users: 
- name: ${cluster_name} 
  user: 
    exec: 
      apiVersion: client.authentication.k8s.io/v1alpha1 
      command: aws-iam-authenticator 
      args: 
      - "token" 
      - "-i" 
      - "${cluster_name}" 
contexts: 
- name: ${cluster_name} 
  context: 
    cluster: ${cluster_name} 
    user: ${cluster_name} 
current-context: ${cluster_name} 

准备节点镜像

就像我们在第三章中所做的那样,云端之手,我们现在将为集群中的工作节点准备一个 AMI。但是,我们将通过Packer自动化这个过程。Packer 是一个在 AWS(和其他平台)上构建机器镜像的简单工具。

安装 Packer

就像 Terraform 一样,Packer 被分发为一个单一的二进制文件,只需将其复制到您的路径上。您可以在 Packer 网站上找到详细的安装说明www.packer.io/intro/getting-started/install.html

安装了 Packer 之后,您可以运行packer version来检查您是否已经正确地将其复制到您的路径上。

Packer 配置

Packer 配置为一个 JSON 格式的配置文件,您可以在ami/node.json中看到。

这里的示例配置有三个部分。第一个是变量列表。在这里,我们使用变量来存储我们将在镜像中安装的重要软件的版本号。这将使得在将来可用时,构建和测试具有更新版本的 Kubernetes 软件的镜像变得简单。

配置的第二部分配置了构建器。Packer 允许我们选择使用一个或多个构建器来构建支持不同云提供商的镜像。由于我们想要构建一个用于 AWS 的镜像,我们使用了amazon-ebs构建器,它通过启动临时 EC2 实例然后从其根 EBS 卷的内容创建 AMI 来创建镜像(就像我们在第三章中遵循的手动过程,云的到来)。这个构建器配置允许我们选择我们的机器将基于的基础镜像;在这里,我们使用了官方的 Ubuntu 服务器镜像,一个可信的来源。构建器配置中的ami-name字段定义了输出镜像将被赋予的名称。我们已经包含了所使用的 Kubernetes 软件的版本和时间戳,以确保这个镜像名称是唯一的。拥有唯一的镜像名称让我们能够精确地定义在部署服务器时使用哪个镜像。

最后,我们配置了一个 provisioner 来安装我们的镜像所需的软件。Packer 支持许多不同的 provisioners,可以安装软件,包括诸如 Chef 或 Ansible 之类的完整配置管理系统。为了保持这个示例简单,我们将使用一个 shell 脚本来自动安装我们需要的软件。Packer 将上传配置的脚本到构建实例,然后通过 SSH 执行它。

我们只是使用一个简单的 shell 脚本,但如果您的组织已经在使用配置管理工具,那么您可能更喜欢使用它来安装您的镜像所需的软件,特别是因为它可以简单地包含您组织的基本配置。

在这个脚本中,我们正在安装我们的工作节点将需要加入 EKS 集群并正确运行的软件和配置,如下面的列表所示。在实际部署中,可能还有其他工具和配置,您希望除了这些之外添加。

  • Docker:Docker 目前是与 Kubernetes 一起使用的经过最全面测试和最常见的容器运行时

  • kubelet:Kubernetes 节点代理

  • ekstrap:配置 kubelet 连接到 EKS 集群端点

  • aws-iam-authenticator:允许节点使用节点的 IAM 凭据与 EKS 集群进行身份验证

我们使用以下代码安装这些元素:

install.sh
          #!/bin/bash
          set -euxo pipefail  
...
          # Install aws-iam-authenticator
curl -Lo /usr/local/bin/heptio-authenticator-aws https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.3.0/heptio-authenticator-aws_0.3.0_linux_amd64
chmod +x /usr/local/bin/heptio-authenticator-aws

apt-get install -y \
  docker-ce=$DOCKER_VERSION* \
  kubelet=$K8S_VERSION* \
  ekstrap=$EKSTRAP_VERSION*
# Cleanup
apt-get clean
rm -rf /tmp/*
   # Cleanup
   apt-get clean
   rm -rf /tmp/*

一旦您为 Packer 准备好配置,您可以使用packer build命令在您的 AWS 账户中构建 AMI,如下面的代码所示。这将启动一个临时的 EC2 实例。将新的 AMI 保存到您的账户中,并清理临时实例:

packer build node.json  

如果您的组织使用持续集成服务,您可能希望配置它以便定期构建您的节点镜像,以便获取基本操作系统的安全更新。

节点组

现在我们已经为集群中的工作节点准备好了一个镜像,我们可以设置一个自动扩展组来管理启动 EC2 实例,这些实例将组成我们的集群。

EKS 不会限制我们以任何特定的方式管理我们的节点,因此自动扩展组并不是管理集群中节点的唯一选项,但使用它们是管理集群中多个工作实例的最简单方式之一。

如果您想在集群中使用多种实例类型,您可以为您想要使用的每种实例类型重复启动配置和自动扩展组配置。在这个配置中,我们正在按需启动c5.large实例,但您应该参考第六章 生产规划,了解有关为您的集群选择适当实例大小的更多信息。

配置的第一部分设置了我们的实例要使用的 IAM 角色。这很简单,因为 AWS 提供了托管策略,这些策略具有 Kubernetes 所需的权限。AmazonEKSWorkerNodePolicy代码短语允许 kubelet 查询有关 EC2 实例、附加卷和网络设置的信息,并查询有关 EKS 集群的信息。AmazonEKS_CNI_Policy提供了vpc-cni-k8s网络插件所需的权限,以将网络接口附加到实例并为这些接口分配新的 IP 地址。AmazonEC2ContainerRegistryReadOnly策略允许实例从 AWS Elastic Container Registry 中拉取 Docker 镜像(您可以在第十章中了解更多关于使用此功能的信息,管理容器镜像)。我们还将手动指定一个策略,允许kube2iam工具假定角色,以便为在集群上运行的应用程序提供凭据,如下面的代码所示:

nodes.tf 
/* 
  IAM policy for nodes 
*/ 
data "aws_iam_policy_document" "node" { 
  statement { 
    actions = ["sts:AssumeRole"] 

    principals { 
      type        = "Service" 
      identifiers = ["ec2.amazonaws.com"] 
    } 
  } 
} 
... 

resource "aws_iam_instance_profile" "node" { 
  name = "${aws_iam_role.node.name}" 
  role = "${aws_iam_role.node.name}" 
} 

我们的工作节点在能够向 Kubernetes API 服务器注册之前,需要具有正确的权限。在 EKS 中,IAM 角色和用户之间的映射是通过向集群提交配置映射来配置的。

您可以在 EKS 文档中阅读有关如何将 IAM 用户和角色映射到 Kubernetes 权限的更多信息,网址为docs.aws.amazon.com/eks/latest/userguide/add-user-role.html

Terraform 将使用我们在设置控制平面时生成的kubeconfig文件,通过 local-exec provisioner 使用kubectl将此配置提交到集群,如下面的nodes.tf继续的代码所示:

/* 
  This config map configures which IAM roles should be trusted by Kubernetes 
*/ 

resource "local_file" "aws_auth" { 
  content = <<YAML 
apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: aws-auth 
  namespace: kube-system 
data: 
  mapRoles: | 
    - rolearn: ${aws_iam_role.node.arn} 
      username: system:node:{{EC2PrivateDNSName}} 
      groups: 
        - system:bootstrappers 
        - system:nodes 
YAML 
  filename = "${path.module}/aws-auth-cm.yaml" 
  depends_on = ["local_file.kubeconfig"] 

  provisioner "local-exec" { 
    command = "kubectl --kubeconfig=${local_file.kubeconfig.filename} apply -f ${path.module}/aws-auth-cm.yaml" 
  } 
} 

接下来,我们需要准备安全组来控制与我们的节点之间的网络流量。

我们将设置一些规则,以允许以下通信流,这些流对于我们的集群能够正常运行是必需的:

  • 节点需要相互通信,用于集群内的 pod 和服务通信。

  • 运行在节点上的 Kubelet 需要连接到 Kubernetes API 服务器,以便读取和更新有关集群状态的信息。

  • 控制平面需要连接到端口10250上的 Kubelet API;这用于功能,如kubectl execkubectl logs

  • 为了使用 API 的代理功能将流量代理到 pod 和服务,控制平面需要连接到在集群中运行的 pod。在这个例子中,我们打开了所有端口,但是,例如,如果您只在您的 pod 上打开了非特权端口,那么您只需要允许流量到 1024 以上的端口。

我们使用以下代码设置这些规则。nodes.tf的代码如下:

 resource "aws_security_group" "nodes" { 
  name        = "${var.cluster_name}-nodes" 
  description = "Security group for all nodes in the cluster" 
  vpc_id      = "${aws_vpc.k8s.id}" 

  egress { 
    from_port   = 0 
    to_port     = 0 
    protocol    = "-1" 
    cidr_blocks = ["0.0.0.0/0"] 
  } 

... 
resource "aws_security_group_rule" "nodes-control_plane-proxy" { 
  description              = "API (proxy) communication to pods" 
  from_port                = 0 
  to_port                  = 65535 
  protocol                 = "tcp" 
  security_group_id        = "${aws_security_group.nodes.id}" 
  source_security_group_id = \ 
                          "${aws_security_group.control_plane.id}" 
  type                     = "ingress" 
} 

现在我们已经准备好运行我们的节点的基础设施,我们可以准备一个启动配置并将其分配给一个自动扩展组,以实际启动我们的节点,如下面的代码所示。

显然,我在这里选择的实例类型和磁盘大小可能不适合您的集群,因此在选择集群实例大小时,您需要参考第六章中的信息,即生产规划。所需的磁盘大小将在很大程度上取决于您的应用程序的平均镜像大小。nodes.tf的代码如下:

data "aws_ami" "eks-worker" { 
  filter { 
    name   = "name" 
    values = ["eks-worker-${var.k8s_version}*"] 
  } 

  most_recent = true 
  owners      = ["self"] 
} 

...                                                                    
  resource "aws_autoscaling_group" "node" { 
  launch_configuration = "${aws_launch_configuration.node.id}" 
  max_size             = 2 
  min_size             = 10 
  name                 = "eks-node-${var.cluster_name}" 
  vpc_zone_identifier  = ["${aws_subnet.private.*.id}"] 

  tag { 
    key                 = "Name" 
    value               = "eks-node-${var.cluster_name}" 
    propagate_at_launch = true 
  } 

  tag { 
    key              = "kubernetes.io/cluster/${var.cluster_name}" 
    value               = "owned" 
    propagate_at_launch = true 
  } 
} 

kubernetes.io/cluster/<node name>标签被ekstrap工具用来发现 EKS 端点以注册节点到集群,并被kubelet用来验证它是否已连接到正确的集群。

配置附加组件

Kubernetes 的许多功能来自于它易于通过添加额外的服务来扩展以提供额外的功能。

我们将通过部署kube2iam来看一个例子。这是一个守护程序,在我们的集群中的每个节点上运行,并拦截由我们的 pod 中运行的进程发出的对 AWS 元数据服务的调用。

通过使用 DaemonSet 在集群中的每个节点上运行一个 pod 来为这样的服务提供服务是一个简单的方法,如下面的代码所示。这种方法已经在我们的集群中用于将aws-vpc-cni网络插件部署到每个节点,并运行kube-proxy,这是运行在每个节点上的 Kubernetes 组件,负责将流向服务 IP 的流量路由到底层 pod:

kube2iam.yaml
--- 
apiVersion: v1 
kind: ServiceAccount 
metadata: 
  name: kube2iam 
  namespace: kube-system 
--- 
apiVersion: v1 
kind: List 
items:         
...                                                           kube2iam.tf 
resource "null_resource" "kube2iam" { 
  triggers = { 
    manifest_sha1 = "${sha1(file("${path.module}/kube2iam.yaml"))}" 
  } 

  provisioner "local-exec" { 
    command = " kubectl --kubeconfig=${local_file.kubeconfig.filename} apply -f 
${path.module}/kube2iam.yaml" 
  } 
} 

变革管理

使用像 Terraform 这样的工具来管理您的 Kubernetes 集群比我们在第三章中探索的手动方法具有许多优势,云的选择。当您想要测试对配置的更改,甚至当您要升级集群正在运行的 Kubernetes 版本时,能够快速轻松地重复配置集群的过程非常有用。

将基础设施定义为代码的另一个关键优势是,您可以使用版本控制工具随着时间的推移跟踪对基础设施所做的更改。其中一个关键优势是,每次进行更改时,您都可以留下提交消息。您现在做出的决定可能看起来很明显,但记录为什么以某种方式选择做某事将肯定有助于您和其他人在将来与您的配置一起工作,特别是因为那些其他人可能没有在您进行更改时拥有相同的上下文。

许多软件工程师已经写了很多关于写好提交消息的东西。最好的建议是确保您包含尽可能多的信息来解释为什么需要进行更改。如果您需要返回到配置几个月后,您未来的自己会感谢您。

考虑这个提交消息:

Update K8s Node Security Groups 

Open port 80 on the Node Security Group  

还要考虑这个提交消息:

Allow deveopers to access the guestbook app 

The guestbook is served from port 80\. We are allowing the control plane access to this port on the Node security groups, so developers can test the application using kubectl proxy. 

Once the application is in production and we provision a LoadBalancer, we can remove these rules. 

第一个提交消息很糟糕,因为它只是解释了你做了什么,而这应该很明显,只需看看配置如何改变就可以了。第二个消息提供了更多的信息。重要的是,第二个消息解释了为什么需要进行更改,并提供了一些对将来对集群进行更改的人有用的信息。没有这个重要的上下文,您可能会想知道为什么打开了端口80,并担心如果更改了该信息会发生什么。

在生产环境中操作 Kubernetes 集群不仅仅是关于如何在第一天启动集群;而是确保您可以随着时间的推移更新和扩展集群,以继续满足组织的要求。

总结

我们在本章中构建的集群仍然非常简单,实际上反映了我们可以在接下来的章节中建立的起点。然而,它确实满足了生产就绪的以下基本要求:

  • 可靠性:通过使用 EKS,我们已经配置了一个可靠的控制平面,我们可以依赖它来管理我们的集群。

  • 可扩展性:通过通过自动扩展组操作我们的节点,我们可以简单地在几秒钟内增加集群的额外容量。

  • 可维护性:通过使用 Terraform 将我们的基础设施定义为代码,我们已经简化了将来管理我们的集群。通过为我们的节点机器使用的 AMI 设置构建过程,我们能够快速重建镜像以引入安全更新和更新版本的节点软件。

第八章:抱歉,我的应用程序吃掉了集群

使用 Kubernetes 运行我们的应用程序可以使我们在集群中的机器上实现更高的资源利用率。Kubernetes 调度程序非常有效地将不同的应用程序打包到您的集群中,以最大程度地利用每台机器上的资源。您可以安排一些低优先级的作业,如果需要可以重新启动,例如批处理作业,以及高优先级的作业,例如 Web 服务器或数据库。Kubernetes 将帮助您利用空闲的 CPU 周期,这些周期发生在您的 Web 服务器等待请求时。

如果您想减少在 AWS 上支付的 EC2 实例的费用来运行您的应用程序,这是个好消息。学会如何配置您的 Pod 是很重要的,这样 Kubernetes 可以核算您的应用程序的资源使用情况。如果您没有正确配置您的 Pod,那么您的应用程序的可靠性和性能可能会受到影响,因为 Kubernetes 可能需要从节点中驱逐您的 Pod,因为资源不足。

在本章中,您将首先学习如何核算 Pod 将使用的内存和 CPU。我们将学习如何配置具有不同服务质量的 Pod,以便重要的工作负载能够保证它们所需的资源,但不太重要的工作负载可以在有空闲资源时使用,而无需专用资源。您还将学习如何利用 Kubernetes 自动缩放功能,在负载增加时向您的应用程序添加额外的 Pod,并在资源不足时向您的集群添加额外的节点。

在本章中,您将学习如何执行以下操作:

  • 配置容器资源请求和限制

  • 为所需的服务质量QoS)类别配置您的 Pod

  • 设置每个命名空间资源使用的配额

  • 使用水平 Pod 自动缩放器自动调整您的应用程序,以满足对它们的需求

  • 使用集群自动缩放器根据集群随时间变化的使用情况自动提供和终止 EC2 实例

资源请求和限制

Kubernetes 允许我们通过将多个不同的工作负载调度到一组机器中来实现集群的高利用率。每当我们要求 Kubernetes 调度一个 Pod 时,它需要考虑将其放置在哪个节点上。如果我们可以给调度器一些关于 Pod 所需资源的信息,它就可以更好地决定在哪个节点上放置 Pod。然后它可以计算每个节点上的当前工作负载,并选择符合我们 Pod 预期资源使用的节点。我们可以选择使用资源请求向 Kubernetes 提供这些信息。请求在将 Pod 调度到节点时考虑。请求不会对 Pod 在特定节点上运行时可能消耗的资源量提供任何限制,它们只是代表我们,集群操作员,在要求将特定 Pod 调度到集群时所做的请求的记录。

为了防止 Pod 使用超过其应该的资源,我们可以设置资源限制。这些限制可以由容器运行时强制执行,以确保 Pod 不会使用超过所需资源的数量。

我们可以说,容器的 CPU 使用是可压缩的,因为如果我们限制它,可能会导致我们的进程运行更慢,但通常不会造成其他不良影响,而容器的内存使用是不可压缩的,因为如果容器使用超过其内存限制,唯一的补救措施就是杀死相关的容器。

向 Pod 规范添加资源限制和请求的配置非常简单。在我们的清单中,每个容器规范都可以有一个包含请求和限制的resources字段。在这个例子中,我们请求分配 250 MiB 的 RAM 和四分之一的 CPU 核心给一个 Nginx web 服务器容器。因为限制设置得比请求高,这允许 Pod 使用高达半个 CPU 核心,并且只有在内存使用超过 128 Mi 时才会杀死容器:

apiVersion: v1 
kind: Pod 
metadata: 
  name: webserver 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    resources: 
      limits: 
        memory: 128Mi 
        cpu: 500m 
      requests:
        memory: 64Mi 
        cpu: 250m 

资源单位

每当我们指定 CPU 请求或限制时,我们都是以 CPU 核心为单位进行指定。因为我们经常希望请求或限制 Pod 使用整个 CPU 核心的一部分,我们可以将这部分 CPU 指定为小数或毫核值。例如,值为 0.5 表示半个核心。还可以使用毫核值配置请求或限制。由于 1,000 毫核等于一个核心,我们可以将一半 CPU 指定为 500 m。可以指定的最小 CPU 量为 1 m 或 0.001。

我发现在清单中使用毫核单位更易读。当使用kubectl或 Kubernetes 仪表板时,您还会注意到 CPU 限制和请求以毫核值格式化。但是,如果您正在使用自动化流程创建清单,您可能会使用浮点版本。

内存的限制和请求以字节为单位。但在清单中以这种方式指定它们会非常笨拙且难以阅读。因此,Kubernetes 支持用于引用字节的倍数的标准前缀;您可以选择使用十进制乘数,如 M 或 G,或者其中一个二进制等效项,如 Mi 或 Gi,后者更常用,因为它们反映了物理 RAM 的实际大小。

这些单位的二进制版本实际上是大多数人在谈论兆字节或千兆字节时真正意味着的,尽管更正确的说法是他们在谈论兆比字节和吉比字节!

在实践中,您应该始终记住使用末尾带有i的单位,否则您将得到比预期少一些的内存。这种表示法是在 1998 年引入 ISO/IEC 80000 标准中的,以避免十进制和二进制单位之间的混淆。

十进制 二进制
名称 字节
千字节 1000
兆字节 1000²
吉字节 1000³
太字节 1000⁴
皮字节 1000⁵
艾字节 1000⁶

Kubernetes 支持的内存单位

如何管理具有资源限制的 Pods

当 Kubelet 启动容器时,CPU 和内存限制将传递给容器运行时,然后容器运行时负责管理该容器的资源使用。

如果您正在使用 Docker,CPU 限制(以毫核为单位)将乘以 100,以确定容器每 100 毫秒可以使用的 CPU 时间。如果 CPU 负载过重,一旦容器使用完其配额,它将不得不等到下一个 100 毫秒周期才能继续使用 CPU。

在 cgroups 中运行的不同进程之间共享 CPU 资源的方法称为完全公平调度器CFS;这通过在不同的 cgroups 之间分配 CPU 时间来实现。这通常意味着为一个 cgroup 分配一定数量的时间片。如果一个 cgroup 中的进程处于空闲状态,并且没有使用其分配的 CPU 时间,这些份额将可供其他 cgroup 中的进程使用。

这意味着一个 pod 即使限制设置得太低,可能仍然表现良好,但只有在另一个 pod 开始占用其公平份额的 CPU 后,它才可能突然停止。您可能会发现,如果您在空集群上开始为您的 pod 设置 CPU 限制,并添加额外的工作负载,您的 pod 的性能会开始受到影响。

在本章的后面,我们将讨论一些基本的工具,可以让我们了解每个 pod 使用了多少 CPU。

如果内存限制达到,容器运行时将终止容器(并可能重新启动)。如果容器使用的内存超过请求的数量,那么当节点开始内存不足时,它就成为被驱逐的候选者。

服务质量(QoS)

当 Kubernetes 创建一个 pod 时,它被分配为三个 QoS 类别之一。这些类别用于决定 Kubernetes 如何在节点上调度和驱逐 pod。广义上讲,具有保证 QoS 类别的 pod 将受到最少的驱逐干扰,而具有 BestEffort QoS 类别的 pod 最有可能受到干扰:

  • 保证:这适用于高优先级的工作负载,这些工作负载受益于尽可能避免从节点中被驱逐,并且对于 CPU 资源具有比较低 QoS 类别的 pod 的优先级,容器运行时保证在需要时将提供指定限制中的全部 CPU 数量。

  • 可突发: 这适用于较不重要的工作负载,例如可以在可用时利用更多 CPU 的后台作业,但只保证 CPU 请求中指定的级别。当节点资源不足时,可突发的 pod 更有可能被从节点中驱逐,特别是如果它们使用的内存超过了请求的数量。

  • BestEffort: 具有此类别的 pod 在节点资源不足时最有可能被驱逐。此 QoS 类别中的 pod 也只能使用节点上空闲的 CPU 和内存,因此如果节点上运行的其他 pod 正在大量使用 CPU,这些 pod 可能会完全被资源耗尽。如果您在此类别中调度 Pods,您应确保您的应用在资源匮乏和频繁重启时表现如预期。

实际上,最好避免使用具有 BestEffort QoS 类别的 pod,因为当集群负载过重时,这些 pod 将受到非常不寻常的行为影响。

当我们在 pod 的容器中设置资源和请求限制时,这些值的组合决定了 pod 所在的 QoS 类别。

要被赋予 BestEffort 的 QoS 类别,pod 中的任何容器都不应该设置任何 CPU 或内存请求或限制:

apiVersion: v1 
kind: Pod 
metadata: 
  name: best-effort 
spec: 
  containers: 
  - name: nginx 
    image: nginx 

一个没有资源限制或请求的 pod 将被分配 BestEffort 的 QoS 类别。

要被赋予保证的 QoS 类别,pod 需要在 pod 中的每个容器上都设置 CPU 和内存请求和限制。限制和请求必须相匹配。作为快捷方式,如果一个容器只设置了其限制,Kubernetes 会自动为资源请求分配相等的值:

apiVersion: v1 
kind: Pod 
metadata: 
  name: guaranteed 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    resources: 
      limits: 
        memory: 256Mi 
        cpu: 500m 

一个将被分配保证的 QoS 类别的 pod。

任何介于这两种情况之间的情况都将被赋予可突发的 QoS 类别。这适用于任何设置了任何 pod 的 CPU 或内存限制或请求的 pod。但是如果它们不符合保证类别的标准,例如没有在每个容器上设置限制,或者请求和限制不匹配:

apiVersion: v1 
kind: Pod 
metadata: 
  name: burstable 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    resources: 
      limits: 
        memory: 256Mi 
        cpu: 500m 
      requests: 
        memory: 128Mi 
        cpu: 250m 

一个将被分配可突发的 QoS 类别的 pod。

资源配额

资源配额允许您限制特定命名空间可以使用多少资源。根据您在组织中选择使用命名空间的方式,它们可以为您提供一种强大的方式,限制特定团队、应用程序或一组应用程序使用的资源,同时仍然让开发人员有自由调整每个单独容器的资源限制。

资源配额是一种有用的工具,当您想要控制不同团队或应用程序的资源成本,但仍希望实现将多个工作负载调度到同一集群的利用率时。

在 Kubernetes 中,资源配额由准入控制器管理。该控制器跟踪诸如 Pod 和服务之类的资源的使用,如果超出限制,它将阻止创建新资源。

资源配额准入控制器由在命名空间中创建的一个或多个 ResourceQuota 对象配置。这些对象通常由集群管理员创建,但您可以将创建它们整合到您组织中用于分配资源的更广泛流程中。

让我们看一个示例,说明配额如何限制集群中 CPU 资源的使用。由于配额将影响命名空间中的所有 Pod,因此我们将从使用 kubectl 创建一个新的命名空间开始:

$ kubectl create namespace quota-example
namespace/quota-example created  

我们将从创建一个简单的示例开始,确保每个新创建的 Pod 都设置了 CPU 限制,并且总限制不超过两个核心:

apiVersion: v1 
kind: ResourceQuota 
metadata: 
  name: resource-quota 
  namespace: quota-example 
spec: 
  hard: 
    limits.cpu: 2 

通过使用 kubectl 将清单提交到集群来创建 ResourceQuota

一旦在命名空间中创建了指定资源请求或限制的 ResourceQuota,则在创建之前,所有 Pod 必须指定相应的请求或限制。

为了看到这种行为,让我们在我们的命名空间中创建一个示例部署:

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: example 
  namespace: quota-example 
spec: 
  selector: 
    matchLabels: 
      app: example 
  template: 
    metadata: 
      labels: 
        app: example 
    spec: 
      containers: 
      - name: nginx 
        image: nginx 
        resources: 
          limits: 
            cpu: 500m 

一旦您使用 kubectl 将部署清单提交给 Kubernetes,请检查 Pod 是否正在运行:

$ kubectl -n quota-example get pods
NAME                      READY     STATUS    RESTARTS   AGE
example-fb556779d-4bzgd   1/1       Running   0          1m  

现在,扩展部署并观察是否创建了额外的 Pod:

$ kubectl -n quota-example scale deployment/example --replicas=4$ kubectl -n quota-example get pods
NAME                      READY     STATUS    RESTARTS   AGE
example-fb556779d-4bzgd   1/1       Running   0          2m
example-fb556779d-bpxm8   1/1       Running   0          1m
example-fb556779d-gkbvc   1/1       Running   0          1m
example-fb556779d-lcrg9   1/1       Running   0          1m  

因为我们指定了 500m 的 CPU 限制,所以将部署扩展到四个副本没有问题,这使用了我们在配额中指定的两个核心。

但是,如果您现在尝试扩展部署,使其使用的资源超出配额中指定的资源,您将发现 Kubernetes 不会安排额外的 Pod:

$ kubectl -n quota-example scale deployment/example --replicas=5  

运行kubectl get events将显示一个消息,其中调度程序未能创建满足副本计数所需的额外 pod:

$ kubectl -n quota-example get events
...
Error creating: pods "example-fb556779d-xmsgv" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=500m, used: limits.cpu=2, limited: limits.cpu=2  

默认限制

当您在命名空间上使用配额时,一个要求是命名空间中的每个容器必须定义资源限制和请求。有时,这个要求可能会导致复杂性,并使在 Kubernetes 上快速工作变得更加困难。正确指定资源限制是准备应用程序投入生产的重要部分,但是,例如,在使用 Kubernetes 作为开发或测试工作负载的平台时,这确实增加了额外的开销。

Kubernetes 提供了在命名空间级别提供默认请求和限制的功能。您可以使用这个功能为特定应用程序或团队使用的命名空间提供一些合理的默认值。

我们可以使用LimitRange对象为命名空间中的容器配置默认限制和请求。这个对象允许我们为 CPU 或内存,或两者都提供默认值。如果一个命名空间中存在一个LimitRange对象,那么任何没有在LimitRange中配置资源请求或限制的容器将从限制范围中继承这些值。

有两种情况下,当创建一个 pod 时,LimitRange会影响资源限制或请求:

  • 没有资源限制或请求的容器将从LimitRange对象继承资源限制和请求

  • 没有资源限制但有指定请求的容器将从LimitRange对象中继承资源限制

如果容器已经定义了限制和请求,那么LimitRange将不起作用。因为只指定了限制的容器会将请求字段默认为相同的值,它们不会从LimitRange继承请求值。让我们看一个快速示例。我们首先创建一个新的命名空间:

$ kubectl create namespace limit-example
namespace/limit-example created  

创建限制范围对象的清单,并使用kubectl将其提交到集群中:

apiVersion: v1 
kind: LimitRange 
metadata: 
  name: example 
  namespace: limit-example 
spec: 
  limits: 
  - default: 
      memory: 512Mi 
      cpu: 1 
    defaultRequest: 
      memory: 256Mi 
      cpu: 500m 
    type: Container 

如果您在此命名空间中创建一个没有资源限制的 pod,它将在创建时从限制范围对象中继承:

$ kubectl -n limit-example run --image=nginx example  

deployment.apps/示例已创建。

您可以通过运行kubectl describe来检查限制:

$ kubectl -n limit-example describe pods 
... 
    Limits: 
      cpu:     1 
      memory:  512Mi 
    Requests: 
      cpu:        500m 
      memory:     256Mi 
... 

水平 Pod 自动缩放

一些应用程序可以通过添加额外的副本来扩展以处理增加的负载。无状态的 Web 应用程序就是一个很好的例子,因为添加额外的副本提供了处理对应用程序的增加请求所需的额外容量。一些其他应用程序也设计成可以通过添加额外的 pod 来处理增加的负载;许多围绕从中央队列处理消息的系统也可以以这种方式处理增加的负载。

当我们使用 Kubernetes 部署来部署我们的 pod 工作负载时,使用 kubectl scale 命令简单地扩展应用程序使用的副本数量。然而,如果我们希望我们的应用程序自动响应其工作负载的变化并根据需求进行扩展,那么 Kubernetes 为我们提供了水平 Pod 自动缩放。

水平 Pod 自动缩放允许我们定义规则,根据 CPU 利用率和其他自定义指标,在我们的部署中扩展或缩减副本的数量。在我们的集群中使用水平 Pod 自动缩放之前,我们需要部署 Kubernetes 度量服务器;该服务器提供了用于发现应用程序生成的 CPU 利用率和其他指标的端点。

部署度量服务器

在我们可以使用水平 Pod 自动缩放之前,我们需要将 Kubernetes 度量服务器部署到我们的集群中。这是因为水平 Pod 自动缩放控制器使用 metrics.k8s.io API 提供的指标,而这些指标是由度量服务器提供的。

虽然一些 Kubernetes 的安装可能默认安装此附加组件,在我们的 EKS 集群中,我们需要自己部署它。

有许多方法可以部署附加组件到您的集群中:

  • 如果您遵循了第七章中的建议,一个生产就绪的集群,并且正在使用 Terraform 为您的集群进行配置,您可以像我们在第七章中配置 kube2iam 时一样,使用 kubectl 部署所需的清单。

  • 如果您正在使用 helm 管理集群上的应用程序,您可以使用 stable/metrics server 图表。

  • 在这一章中,为了简单起见,我们将使用 kubectl 部署度量服务器清单。

  • 我喜欢将部署诸如指标服务器和 kube2iam 等附加组件集成到配置集群的过程中,因为我认为它们是集群基础设施的组成部分。但是,如果您要使用类似 helm 的工具来管理在集群上运行的应用程序的部署,那么您可能更喜欢使用相同的工具来管理在集群上运行的所有内容。您所做的决定取决于您和您的团队为管理集群及其上运行的应用程序采用的流程。

  • 指标服务器是在 GitHub 存储库中开发的,网址为github.com/kubernetes-incubator/metrics-server。您将在该存储库的 deploy 目录中找到部署所需的清单。

首先从 GitHub 克隆配置。指标服务器从版本 0.0.3 开始支持 EKS 提供的身份验证方法,因此请确保您使用的清单至少使用该版本。

您将在deploy/1.8+目录中找到许多清单。auth-reader.yamlauth-delegator.yaml文件配置了指标服务器与 Kubernetes 授权基础设施的集成。resource-reader.yaml文件配置了一个角色,以赋予指标服务器从 API 服务器读取资源的权限,以便发现 Pod 所在的节点。基本上,metrics-server-deployment.yamlmetrics-server-service.yaml定义了用于运行服务本身的部署,以及用于访问该服务的服务。最后,metrics-apiservice.yaml文件定义了一个APIService资源,将 metrics.k8s.io API 组注册到 Kubernetes API 服务器聚合层;这意味着对于 metrics.k8s.io 组的 API 服务器请求将被代理到指标服务器服务。

使用kubectl部署这些清单很简单,只需使用kubectl apply将所有清单提交到集群:

$ kubectl apply -f deploy/1.8+  

您应该看到关于在集群上创建的每个资源的消息。

如果您使用类似 Terraform 的工具来配置集群,您可以在创建集群时使用它来提交指标服务器的清单。

验证指标服务器和故障排除

在我们继续之前,我们应该花一点时间检查我们的集群和指标服务器是否正确配置以共同工作。

在度量服务器在您的集群上运行并有机会从集群中收集度量数据(给它一分钟左右的时间)之后,您应该能够使用kubectl top命令来查看集群中 pod 和节点的资源使用情况。

首先运行kubectl top nodes。如果您看到像这样的输出,那么度量服务器已经正确配置,并且正在从您的节点收集度量数据:

$ kubectl top nodes
NAME             CPU(cores)   CPU%      MEMORY(bytes)   MEMORY%
ip-10-3-29-209   20m          1%        717Mi           19%
ip-10-3-61-119   24m          1%        1011Mi          28%  

如果您看到错误消息,那么有一些故障排除步骤可以遵循。

您应该从描述度量服务器部署并检查一个副本是否可用开始:

kubectl -n kube-system describe deployment metrics-server  

如果没有配置正确,您应该通过运行kubectl -n kube-system describe pod来调试创建的 pod。查看事件,看看服务器为什么不可用。确保您至少运行版本 0.0.3 的度量服务器,因为之前的版本不支持与 EKS API 服务器进行身份验证。

如果度量服务器正在正确运行,但在运行kubectl top时仍然看到错误,问题可能是聚合层注册的 APIservice 没有正确配置。在运行kubectl describe apiservice v1beta1.metrics.k8s.io时,检查底部返回的信息中的事件输出。

一个常见的问题是,EKS 控制平面无法连接到端口443上的度量服务器服务。如果您遵循了第七章中的说明,一个生产就绪的集群,您应该已经有一个安全组规则允许控制平面到工作节点的流量,但一些其他文档可能建议更严格的规则,这可能不允许端口443上的流量。

根据 CPU 使用率自动扩展 pod

一旦度量服务器安装到我们的集群中,我们将能够使用度量 API 来检索有关集群中 pod 和节点的 CPU 和内存使用情况的信息。使用kubectl top命令就是一个简单的例子。

水平 Pod 自动缩放器也可以使用相同的度量 API 来收集组成部署的 pod 的当前资源使用情况的信息。

让我们来看一个例子;我们将部署一个使用大量 CPU 的示例应用程序,然后配置一个水平 Pod 自动缩放器,在 CPU 利用率超过目标水平时扩展该 pod 的额外副本,以提供额外的容量。

我们将部署的示例应用程序是一个简单的 Ruby Web 应用程序,可以计算斐波那契数列中的第 n 个数字,该应用程序使用简单的递归算法,效率不是很高(非常适合我们进行自动缩放实验)。该应用程序的部署非常简单。设置 CPU 资源限制非常重要,因为目标 CPU 利用率是基于此限制的百分比:

deployment.yaml 
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: fib 
  labels: 
    app: fib 
spec: 
  selector: 
    matchLabels: 
      app: fib 
  template: 
    metadata: 
      labels: 
        app: fib 
    spec: 
      containers: 
      - name: fib 
        image: errm/fib 
        ports: 
        - containerPort: 9292 
        resources: 
          limits: 
            cpu: 250m 
            memory: 32Mi 

我们没有在部署规范中指定副本的数量;因此,当我们首次将此部署提交到集群时,副本的数量将默认为 1。这是创建部署的良好实践,我们打算通过 Horizontal Pod Autoscaler 调整副本的数量,因为这意味着如果我们稍后使用kubectl apply来更新部署,我们不会覆盖水平 Pod Autoscaler 设置的副本值(无意中缩减或增加部署)。

让我们将这个应用程序部署到集群中:

kubectl apply -f deployment.yaml  

您可以运行kubectl get pods -l app=fib来检查应用程序是否正确启动。

我们将创建一个服务,以便能够访问部署中的 Pod,请求将被代理到每个副本,分散负载:

service.yaml 
kind: Service 
apiVersion: v1 
metadata: 
  name: fib 
spec: 
  selector: 
    app: fib 
  ports: 
  - protocol: TCP 
    port: 80 
    targetPort: 9292 

使用kubectl将服务清单提交到集群:

kubectl apply -f service.yaml  

我们将配置一个 Horizontal Pod Autoscaler 来控制部署中副本的数量。spec定义了我们希望自动缩放器的行为;我们在这里定义了我们希望自动缩放器维护应用程序的 1 到 10 个副本,并实现 60%的目标平均 CPU 利用率。

当 CPU 利用率低于 60%时,自动缩放器将调整目标部署的副本计数;当超过 60%时,将添加副本:

hpa.yaml 
kind: HorizontalPodAutoscaler 
apiVersion: autoscaling/v2beta1 
metadata: 
  name: fib 
spec: 
  maxReplicas: 10 
  minReplicas: 1 
  scaleTargetRef: 
    apiVersion: app/v1 
    kind: Deployment 
    name: fib 
  metrics: 
  - type: Resource 
    resource: 
      name: cpu 
      targetAverageUtilization: 60 

使用kubectl创建自动缩放器:

kubectl apply -f hpa.yaml  

kubectl autoscale命令是创建HorizontalPodAutoscaler的快捷方式。运行kubectl autoscale deployment fib --min=1 --max=10 --cpu-percent=60将创建一个等效的自动缩放器。

创建了 Horizontal Pod Autoscaler 后,您可以使用kubectl describe查看有关其当前状态的许多有趣信息:

$ kubectl describe hpa fib    
Name:              fib
Namespace:         default
CreationTimestamp: Sat, 15 Sep 2018 14:32:46 +0100
Reference:         Deployment/fib
Metrics:           ( current / target )
  resource cpu:    0% (1m) / 60%
Min replicas:      1
Max replicas:      10
Deployment pods:   1 current / 1 desired  

现在我们已经设置好了 Horizontal Pod Autoscaler,我们应该在部署中的 Pod 上生成一些负载,以说明它的工作原理。在这种情况下,我们将使用ab(Apache benchmark)工具重复要求我们的应用程序计算第 30 个斐波那契数:

load.yaml
apiVersion: batch/v1 
kind: Job 
metadata: 
  name: fib-load 
  labels: 
    app: fib 
    component: load 
spec: 
  template: 
    spec: 
      containers: 
      - name: fib-load 
        image: errm/ab 
        args: ["-n1000", "-c4", "fib/30"] 
      restartPolicy: OnFailure 

此作业使用ab向端点发出 1,000 个请求(并发数为 4)。将作业提交到集群,然后观察水平 Pod 自动缩放器的状态:

kubectl apply -f load.yaml    
watch kubectl describe hpa fib

一旦负载作业开始发出请求,自动缩放器将扩展部署以处理负载:

Name:                   fib
Namespace:              default
CreationTimestamp: Sat, 15 Sep 2018 14:32:46 +0100
Reference:         Deployment/fib
Metrics:           ( current / target )
  resource cpu:    100% (251m) / 60%
Min replicas:      1
Max replicas:      10
Deployment pods:   2 current / 2 desired  

基于其他指标自动缩放 pod

度量服务器提供了水平 Pod 自动缩放器可以使用的 API,以获取有关集群中 pod 的 CPU 和内存利用率的信息。

可以针对利用率百分比进行目标设置,就像我们对 CPU 指标所做的那样,或者可以针对绝对值进行目标设置,就像我们对内存指标所做的那样:

hpa.yaml 
kind: HorizontalPodAutoscaler 
apiVersion: autoscaling/v2beta1 
metadata: 
  name: fib 
spec: 
  maxReplicas: 10 
  minReplicas: 1 
  scaleTargetRef: 
    apiVersion: app/v1 
    kind: Deployment 
    name: fib 
  metrics: 
  - type: Resource 
    resource: 
      name: memory 
      targetAverageValue: 20M 

水平 Pod 自动缩放器还允许我们根据更全面的指标系统提供的其他指标进行缩放。Kubernetes 允许聚合自定义和外部指标的指标 API。

自定义指标是与 pod 相关的除 CPU 和内存之外的指标。例如,您可以使用适配器,使您能够使用像 Prometheus 这样的系统从您的 pod 中收集的指标。

如果您有关于应用程序利用率的更详细的指标可用,这可能非常有益,例如,一个公开忙碌工作进程计数的分叉 Web 服务器,或者一个公开有关当前排队项目数量的指标的队列处理应用程序。

外部指标适配器提供有关与 Kubernetes 中的任何对象不相关的资源的信息,例如,如果您正在使用外部排队系统,比如 AWS SQS 服务。

总的来说,如果您的应用程序可以公开有关其所依赖的资源的指标,并使用外部指标适配器,那将更简单,因为很难限制对特定指标的访问,而自定义指标与特定的 Pod 相关联,因此 Kubernetes 可以限制只有那些需要使用它们的用户和进程才能访问它们。

集群自动缩放

Kubernetes Horizontal Pod Autoscaler 的功能使我们能够根据应用程序随时间变化的资源使用情况添加和删除 pod 副本。然而,这对我们集群的容量没有影响。如果我们的 pod 自动缩放器正在添加 pod 来处理负载增加,那么最终我们的集群可能会用尽空间,额外的 pod 将无法被调度。如果我们的应用程序负载减少,pod 自动缩放器删除 pod,那么我们就需要为 EC2 实例支付费用,而这些实例将处于空闲状态。

当我们在第七章中创建了我们的集群,一个生产就绪的集群,我们使用自动缩放组部署了集群节点,因此我们应该能够利用它根据部署到集群上的应用程序的需求随时间变化而扩展和收缩集群。

自动缩放组内置支持根据实例的平均 CPU 利用率来调整集群的大小。然而,当处理 Kubernetes 集群时,这并不是很合适,因为运行在集群每个节点上的工作负载可能会有很大不同,因此平均 CPU 利用率并不是集群空闲容量的很好代理。

值得庆幸的是,为了有效地将 pod 调度到节点上,Kubernetes 会跟踪每个节点的容量和每个 pod 请求的资源。通过利用这些信息,我们可以自动调整集群的大小以匹配工作负载的大小。

Kubernetes 自动缩放器项目为一些主要的云提供商提供了集群自动缩放器组件,包括 AWS。集群自动缩放器可以很简单地部署到我们的集群。除了能够向我们的集群添加实例外,集群自动缩放器还能够从集群中清除 pod,然后在集群容量可以减少时终止实例。

部署集群自动缩放器

将集群自动缩放器部署到我们的集群非常简单,因为它只需要一个简单的 pod 在运行。我们只需要一个简单的 Kubernetes 部署,就像我们在之前的章节中使用过的那样。

为了让集群自动缩放器更新自动缩放组的期望容量,我们需要通过 IAM 角色授予权限。如果您正在使用 kube2iam,正如我们在第七章中讨论的那样,我们将能够通过适当的注释为集群自动缩放器 pod 指定这个角色:

cluster_autoscaler.tf
data "aws_iam_policy_document" "eks_node_assume_role_policy" { 
  statement { 
    actions = ["sts:AssumeRole"] 
    principals { 
      type = "AWS" 
      identifiers = ["${aws_iam_role.node.arn}"] 
    } 
  } 
} 

resource "aws_iam_role" "cluster-autoscaler" { 
  name = "EKSClusterAutoscaler" 
  assume_role_policy = "${data.aws_iam_policy_document.eks_node_assume_role_policy.json}" 
} 

data "aws_iam_policy_document" "autoscaler" { 
  statement { 
    actions = [ 
      "autoscaling:DescribeAutoScalingGroups", 
      "autoscaling:DescribeAutoScalingInstances", 
      "autoscaling:DescribeTags", 
      "autoscaling:SetDesiredCapacity", 
      "autoscaling:TerminateInstanceInAutoScalingGroup" 
    ] 
    resources = ["*"] 
  } 
} 

resource "aws_iam_role_policy" "cluster_autoscaler" { 
  name = "cluster-autoscaler" 
  role = "${aws_iam_role.cluster_autoscaler.id}" 
  policy = "${data.aws_iam_policy_document.autoscaler.json}" 
} 

为了将集群自动缩放器部署到我们的集群,我们将使用kubectl提交一个部署清单,类似于我们在第七章中部署 kube2iam 的方式,一个生产就绪的集群。我们将使用 Terraform 的模板系统来生成清单。

我们创建一个服务账户,用于自动扩展器连接到 Kubernetes API:

cluster_autoscaler.tpl
--- 
apiVersion: v1 
kind: ServiceAccount 
metadata: 
  labels: 
    k8s-addon: cluster-autoscaler.addons.k8s.io 
    k8s-app: cluster-autoscaler 
  name: cluster-autoscaler 
  namespace: kube-system 

集群自动缩放器需要读取有关集群当前资源使用情况的信息,并且需要能够从需要从集群中移除并终止的节点中驱逐 Pod。基本上,cluster-autoscalerClusterRole为这些操作提供了所需的权限。以下是cluster_autoscaler.tpl的代码续写:

--- 
apiVersion: rbac.authorization.k8s.io/v1beta1 
kind: ClusterRole 
metadata: 
  name: cluster-autoscaler 
  labels: 
    k8s-addon: cluster-autoscaler.addons.k8s.io 
    k8s-app: cluster-autoscaler 
rules: 
- apiGroups: [""] 
  resources: ["events","endpoints"] 
  verbs: ["create", "patch"] 
- apiGroups: [""] 
  resources: ["pods/eviction"] 
  verbs: ["create"] 
- apiGroups: [""] 
  resources: ["pods/status"] 
  verbs: ["update"] 
- apiGroups: [""] 
  resources: ["endpoints"] 
  resourceNames: ["cluster-autoscaler"] 
  verbs: ["get","update"] 
- apiGroups: [""] 
  resources: ["nodes"] 
  verbs: ["watch","list","get","update"] 
- apiGroups: [""] 
  resources: ["pods","services","replicationcontrollers","persistentvolumeclaims","persistentvolumes"] 
  verbs: ["watch","list","get"] 
- apiGroups: ["extensions"] 
  resources: ["replicasets","daemonsets"] 
  verbs: ["watch","list","get"] 
- apiGroups: ["policy"] 
  resources: ["poddisruptionbudgets"] 
  verbs: ["watch","list"] 
- apiGroups: ["apps"] 
  resources: ["statefulsets"] 
  verbs: ["watch","list","get"] 
- apiGroups: ["storage.k8s.io"] 
  resources: ["storageclasses"] 
  verbs: ["watch","list","get"] 
--- 
apiVersion: rbac.authorization.k8s.io/v1beta1 
kind: ClusterRoleBinding 
metadata: 
  name: cluster-autoscaler 
  labels: 
    k8s-addon: cluster-autoscaler.addons.k8s.io 
    k8s-app: cluster-autoscaler 
roleRef: 
  apiGroup: rbac.authorization.k8s.io 
  kind: ClusterRole 
  name: cluster-autoscaler 
subjects: 
  - kind: ServiceAccount 
    name: cluster-autoscaler 
    namespace: kube-system 

请注意,cluster-autoscaler在配置映射中存储状态信息,因此需要有权限能够从中读取和写入。这个角色允许了这一点。以下是cluster_autoscaler.tpl的代码续写:

--- 
apiVersion: rbac.authorization.k8s.io/v1beta1 
kind: Role 
metadata: 
  name: cluster-autoscaler 
  namespace: kube-system 
  labels: 
    k8s-addon: cluster-autoscaler.addons.k8s.io 
    k8s-app: cluster-autoscaler 
rules: 
- apiGroups: [""] 
  resources: ["configmaps"] 
  verbs: ["create"] 
- apiGroups: [""] 
  resources: ["configmaps"] 
  resourceNames: ["cluster-autoscaler-status"] 
  verbs: ["delete","get","update"] 
--- 
apiVersion: rbac.authorization.k8s.io/v1beta1 
kind: RoleBinding 
metadata: 
  name: cluster-autoscaler 
  namespace: kube-system 
  labels: 
    k8s-addon: cluster-autoscaler.addons.k8s.io 
    k8s-app: cluster-autoscaler 
roleRef: 
  apiGroup: rbac.authorization.k8s.io 
  kind: Role 
  name: cluster-autoscaler 
subjects: 
  - kind: ServiceAccount 
    name: cluster-autoscaler 
    namespace: kube-system 

最后,让我们考虑一下集群自动缩放器部署本身的清单。集群自动缩放器 Pod 包含一个运行集群自动缩放器控制循环的单个容器。您会注意到我们正在向集群自动缩放器传递一些配置作为命令行参数。最重要的是,--node-group-auto-discovery标志允许自动缩放器在具有我们在第七章中创建集群时在我们的自动缩放组上设置的kubernetes.io/cluster/<cluster_name>标记的自动缩放组上操作。这很方便,因为我们不必显式地配置自动缩放器与我们的集群自动缩放组。

如果您的 Kubernetes 集群在多个可用区中有节点,并且正在运行依赖于被调度到特定区域的 Pod(例如,正在使用 EBS 卷的 Pod),建议为您计划使用的每个可用区创建一个自动缩放组。如果您使用跨多个区域的一个自动缩放组,那么集群自动缩放器将无法指定它启动的实例的可用区。

这是cluster_autoscaler.tpl的代码续写:

--- 
apiVersion: extensions/v1beta1 
kind: Deployment 
metadata: 
  name: cluster-autoscaler 
  namespace: kube-system 
  labels: 
    app: cluster-autoscaler 
spec: 
  replicas: 1 
  selector: 
    matchLabels: 
      app: cluster-autoscaler 
  template: 
    metadata: 
      annotations: 
        iam.amazonaws.com/role: ${iam_role} 
      labels: 
        app: cluster-autoscaler 
    spec: 
      serviceAccountName: cluster-autoscaler 
      containers: 
        - image: k8s.gcr.io/cluster-autoscaler:v1.3.3 
          name: cluster-autoscaler 
          resources: 
            limits: 
              cpu: 100m 
              memory: 300Mi 
            requests: 
              cpu: 100m 
              memory: 300Mi 
          command: 
            - ./cluster-autoscaler 
            - --v=4 
            - --stderrthreshold=info 
            - --cloud-provider=aws 
            - --skip-nodes-with-local-storage=false 
            - --expander=least-waste 
            - --node-group-auto-discovery=asg:tag=kubernetes.io/cluster/${cluster_name} 
          env: 
            - name: AWS_REGION 
              value: ${aws_region} 
          volumeMounts: 
            - name: ssl-certs 
              mountPath: /etc/ssl/certs/ca-certificates.crt 
              readOnly: true 
          imagePullPolicy: "Always" 
      volumes: 
        - name: ssl-certs 
          hostPath: 
            path: "/etc/ssl/certs/ca-certificates.crt" 

最后,我们通过传递 AWS 区域、集群名称和 IAM 角色的变量来渲染模板化的清单,并使用kubectl将文件提交给 Kubernetes:

这是cluster_autoscaler.tpl的代码续写:

data "aws_region" "current" {} 

data "template_file" " cluster_autoscaler " { 
  template = "${file("${path.module}/cluster_autoscaler.tpl")}" 

  vars { 
    aws_region = "${data.aws_region.current.name}" 
    cluster_name = "${aws_eks_cluster.control_plane.name}" 
    iam_role = "${aws_iam_role.cluster_autoscaler.name}" 
  } 
} 

resource "null_resource" "cluster_autoscaler" { 
  trigers = { 
    manifest_sha1 = "${sha1("${data.template_file.cluster_autoscaler.rendered}")}" 
  } 

  provisioner "local-exec" { 
    command = "kubectl  
--kubeconfig=${local_file.kubeconfig.filename} apply -f -<<EOF\n${data.template_file.cluster_autoscaler.rendered}\nEOF" 
  } 
} 

总结

Kubernetes 是一个强大的工具;它非常有效地实现了比手动将应用程序调度到机器上更高的计算资源利用率。重要的是,您要学会通过设置正确的资源限制和请求来为您的 Pod 分配资源;如果不这样做,您的应用程序可能会变得不可靠或者资源匮乏。

通过了解 Kubernetes 如何根据您分配给它们的资源请求和限制为您的 pod 分配服务质量类别,您可以精确控制您的 pod 的管理方式。通过确保您的关键应用程序(如 Web 服务器和数据库)以保证类别运行,您可以确保它们将始终表现一致,并在需要重新安排 pod 时遭受最小的中断。您可以通过为较低优先级的 pod 设置限制来提高集群的效率,这将导致它们以可突发的 QoS 类别进行安排。可突发的 pod 可以在有空闲资源时使用额外的资源,但在负载增加时不需要向集群添加额外的容量。

资源配额在管理大型集群时非常有价值,该集群用于运行多个应用程序,甚至由组织中的不同团队使用,特别是如果您试图控制非生产工作负载(如测试和分段环境)的成本。

AWS 之所以称其机器为弹性,是有原因的:它们可以在几分钟内按需扩展或缩减,以满足应用程序的需求。如果您在负载变化的集群上运行工作负载,那么您应该利用这些特性和 Kubernetes 提供的工具来扩展部署,以匹配应用程序接收的负载以及需要安排的 pod 的大小。

第九章:存储状态

本章主要介绍 Kubernetes 与 AWS 原生存储解决方案 Elastic Block Store(EBS)的深度集成。 Amazon EBS 提供网络附加存储作为服务,并且是提供块存储给 EC2 实例的主要解决方案。

几乎每个启动的 EC2 实例都由 EBS 根卷支持(从 AMI 机器映像创建)。由于 EBS 存储是网络附加的,如果支持 EC2 实例的底层机器以某种方式失败,存储在卷上的数据是安全的,因为它会自动在多个物理存储设备上复制。

除了用于存储 EC2 实例的根文件系统外,还可以通过 AWS API 将附加的 EBS 卷附加到 EC2 实例并按需挂载。 Kubernetes 与 AWS EBS 的集成利用了这一点,提供了可以被您的 pod 使用的持久卷。如果一个 pod 被杀死并被另一个 EC2 实例上的 pod 替换,Kubernetes 将处理将 EBS 卷从旧的 EC2 实例分离并附加到新实例,准备好根据需要挂载到新的 pod 中。

在本章中,我们将首先看看如何配置我们的 pod 以利用附加卷。然后,我们将研究 Kubernetes 提供的用于处理提供持久性的存储(如 EBS)的抽象。我们将看看 Kubernetes 如何根据我们在 pod 配置中请求的规格自动为我们提供 EBS 卷。

一旦您掌握了使用 Kubernetes 为您的 pod 提供持久存储,本章的下半部分将介绍有状态集,这是 Kubernetes 提供的一个抽象,用于运行一组 pod,每个 pod 都可以有自己的附加存储和即使重新调度到另一个节点也保持不变的身份。如果您想在 Kubernetes 集群上运行复杂的有状态应用程序,比如数据库,这是所需的最后一块拼图。

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

  • 存储类

  • 有状态集

让我们首先看一下如何将卷附加到我们的 Pod。可用的最简单的卷类型emptyDir只是一个与 Pod 的生命周期相关联的临时目录。当卷被创建时,它是空的,正如其名称所示,并且会一直保留在节点上,直到 Pod 从节点中移除。您在卷内存储的数据在同一节点上的 Pod 重启之间是持久的,因此可以用于需要在文件系统上缓存昂贵计算的进程,或者用于检查它们的进度。在第一章中,Google 的基础设施供给我们,我们讨论了emptyDir卷在 Pod 内不同容器之间共享文件的一些其他可能用途。

在这个例子中,我们将利用emptyDir卷来部署一个应用程序,该应用程序希望写入容器中的/data目录,而根文件系统已被设置为只读。

这个应用程序是为了说明 Kubernetes 中卷的一些属性而设计的。当它启动时,它会在/data目录中写入一个随机的文件名。然后它启动一个显示该目录内容的 Web 服务器:

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: randserver 
spec: 
  selector: 
    matchLabels: 
      app: randserver 
  template: 
    metadata: 
      labels: 
        app: example 
    spec: 
      containers: 
      - image: errm/randserver 
        name: randserver 
        volumeMounts: 
        - mountPath: /data 
          name: data 
        securityContext: 
          readOnlyRootFilesystem: true 
      volumes: 
      - name: data 
        emptyDir: {} 

查看这个配置,有一些关于我们如何在 Pod 中使用卷的事项需要注意。这些规则不仅适用于emptyDir卷,也适用于您可能遇到的每一种卷类型:

  • 每个卷都在 Pod 规范的顶层定义。即使一个卷被 Pod 中的多个容器使用,我们只需要定义一次。

  • 当您想要从容器内访问卷时,您必须指定一个卷挂载点,将该卷挂载到容器的文件系统的特定位置。当我们挂载一个卷时,我们会用在volumes部分中定义它时使用的名称来引用它。

一旦您部署了这个示例清单,您应该能够使用kubectl port-forward命令来访问 Pod 内运行的 Web 服务器:

$ kubectl port-forward deployment/randserver 3000:3000   
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

现在您应该能够在浏览器中访问http://localhost:3000,以查看在容器启动时创建的一个随机文件:

如果您删除此 Pod,那么部署将重新创建一个新的 Pod。因为emptyDir卷的内容在 Pod 被销毁时会丢失,所以当第一个 Pod 启动时创建的文件将会消失,并且将创建一个具有不同名称的新文件:

$ kubectl delete pod -l app=randserver
pod "randserver-79559c5fb6-htnxm" deleted  

您需要重新运行kubectl port-forward以选择新的 pod:

$ kubectl port-forward deployment/randserver 3000:3000  

正在提供的新创建的文件

EBS 卷

让 Kubernetes 附加 EBS 卷,然后将其挂载到我们的 pod 中的容器中,几乎与使用emptyDir卷一样简单。挂载 EBS 卷的最低级别和最简单的方法是使用awsElasticBlockStore卷类型。此卷类型处理将 EBS 卷附加到我们的 pod 将运行的节点,然后将卷挂载到容器中的路径。

在使用此卷类型时,Kubernetes 不处理实际为我们创建卷,因此我们需要手动执行此操作。我们可以使用 AWS CLI 来执行此操作:

$ aws ec2 create-volume --availability-zone=us-east-1a --size=5 --volume-type=gp2
{
 "AvailabilityZone": "us-east-1a",
 "CreateTime": "2018-11-17T15:17:54.000Z",
 "Encrypted": false,
 "Size": 5,
 "SnapshotId": "",
 "State": "creating",
 "VolumeId": "vol-04e744aad50d4911",
 "Iops": 100,
 "Tags": [],
 "VolumeType": "gp2"
} 

请记住,EBS 卷与特定的可用性区域(就像ec2实例一样)相关联,并且只能附加到该相同可用性区域中的实例,因此您需要在与集群中的实例相同的区域中创建卷。

在这里,我们已更新了上一个示例中创建的部署,以使用awsElasticBlockStore卷类型,并将我们刚刚创建的卷附加到我们的 pod。EBS 卷的 ID 作为参数传递给卷配置:

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: randserver 
spec: 
  selector: 
    matchLabels: 
      app: randserver 
  template: 
    metadata: 
      labels: 
        app: randserver 
    spec: 
      containers: 
      - image: errm/randserver 
        name: randserver 
        volumeMounts: 
        - mountPath: /data 
          name: data 
        securityContext: 
          readOnlyRootFilesystem: true 
      volumes: 
      - name: data 
        awsElasticBlockStore: 
          volumeID: vol-04e744aad50d4911 
          fsType: ext4 
      nodeSelector: 
        "failure-domain.beta.kubernetes.io/zone": us-east-1a 

您将看到以这种方式手动附加 EBS 卷与使用更简单的emptyDir卷非常相似。

特殊的failure-domain.beta.kubernetes.io/zone标签由 AWS 云提供商自动添加到每个节点。在这里,我们在 pod 定义的nodeSelector中使用它,以将 pod 调度到与我们在其中创建卷的可用性区域相同的节点。Kubernetes 将自动向您的集群中的节点添加几个其他标签。您可以在 Kubernetes 文档中阅读有关它们的信息kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/

当您首次提交此部署时,其行为将与先前版本完全相同。但是,当我们删除该 pod 并进行替换时,您会注意到在此容器的先前运行中创建的文件将保留,并且每次启动时都会向列表中添加一个新文件:

当我们的应用程序由 EBS 卷支持时,文件将在 pod 重新调度时保留

持久卷

虽然我们当然可以以这种方式手动创建 EBS 卷并在我们的清单中使用它们的 ID,但这种方法存在一些问题。

对于想要在集群上运行他们的应用程序的用户来说,首先考虑为应用程序提供 EBS 卷,然后再修改清单以引用硬编码的 ID,这是笨拙且耗时的。这意味着 pod 清单将需要包含特定于在 AWS 上运行所涉及的应用程序的配置。理想情况下,我们希望尽可能多地重用我们的配置,以避免由于不得不修改配置而引入错误的风险,这些配置可能在我们可能部署它的不同环境之间重复使用。

Kubernetes 提供了两个抽象,将帮助我们管理 EBS 卷:PersistentVolumePersistentVolumeClaim

PersistentVolume对象代表集群中的物理存储空间;在 AWS 上,这是一个 EBS 卷,就像Node对象代表集群中的 EC2 实例一样。该对象捕获了存储实现的细节,因此对于 EBS 卷,它记录了其 ID,以便 Kubernetes 在调度使用该卷的 pod 时将其附加到正确的节点上。

PersistentVolumeClaim是 Kubernetes 对象,允许我们在 pod 中表达对PersistentVolume的请求。当我们请求持久卷时,我们只需要请求所需的存储量,以及可选的存储类(请参见下一节)。PersistentVolumeClaim通常嵌入在 pod 规范中。当 pod 被调度时,它的PersistentVolumeClaim将与足够大以满足所请求存储量的特定PersistentVolume匹配。PersistentVolume绑定到请求的PersistentVolumeClaim,因此即使 pod 被重新调度,相同的基础卷也将附加到 pod 上。

这比手动提供 EBS 卷并在我们的配置中包含卷 ID 要大大改进,因为我们不需要在每次将我们的 pod 部署到新环境时修改我们的清单。

如果你手动操作 Kubernetes(例如,在裸金属部署中),集群管理员可能会预先提供一组PersistentVolume,然后在创建时与每个PersistentVolumeClaim匹配并绑定。在使用 AWS 时,无需预先提供存储,因为 Kubernetes 会根据需要使用 AWS API 动态创建PersistentVolume

持久卷示例

让我们看看如何使用持久卷来简化我们示例应用程序的部署。

为了避免在 AWS 账户上产生额外的费用,你可能想要删除在上一个示例中手动创建的 EBS 卷。

首先,删除我们创建的部署,这样 Kubernetes 就可以卸载卷:

**$ kubectl delete deployment/randserver**然后,你可以使用 AWS CLI 来删除 EBS 卷:

**$ aws ec2 delete-volume --volume-id vol-04e744aad50d4911**

在开始之前,请确保您至少已将通用存储类添加到您的集群中。

使用 Kubernetes 动态卷提供创建 EBS 卷就像使用kubectl创建任何其他资源一样简单:

apiVersion: v1 
kind: PersistentVolumeClaim 
metadata: 
  name: randserver-data 
spec: 
  accessModes: 
    - ReadWriteOnce 
  storageClassName: general-purpose 
  resources: 
    requests: 
      storage: 1Gi 

如果你在集群中的存储类中添加了storageclass.kubernetes.io/is-default-class注释,如果你愿意,你可以省略storageClassName字段。

一旦你使用kubernetes.io/aws-ebs供应商为存储类创建了PersistantVolumeClaim,Kubernetes 将会根据你指定的大小和存储类参数来提供一个匹配的 EBS 卷。完成后,你可以使用kubectl describe来查看声明;你会看到状态已更新为BoundVolume字段显示了声明绑定的底层PersistentVolume

$ kubectl describe pvc/randserver-data
Name:          randserver-data
Namespace:     default
StorageClass:  general-purpose
Status:        Bound
Volume:        pvc-5c2dab0d-f017-11e8-92ac-0a56f9f52542
Capacity:      1Gi
Access Modes:  RWO

如果我们使用kubectl describe来检查这个PersistentVolume,我们可以看到自动提供的底层 EBS 卷的细节:

$ kubectl describe pv/pvc-5c2dab0d-f017-11e8-92ac-0a56f9f52542
Name: pvc-5c2dab0d-f017-11e8-92ac-0a56f9f52542
StorageClass: general-purpose
Status: Bound
Claim: default/randserver-data
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1Gi
Source:
 Type: AWSElasticBlockStore (a Persistent Disk resource in AWS)
 VolumeID: aws://us-east-1a/vol-04ad625aa4d5da62b
 FSType: ext4
 Partition: 0
 ReadOnly: false

在我们的部署中,我们可以更新 pod 规范的volumes部分,通过名称引用PersistentVolumeClaim

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: randserver 
spec: 
  selector: 
    matchLabels: 
      app: randserver 
  template: 
    metadata: 
      labels: 
        app: randserver 
    spec: 
      containers: 
      - image: errm/randserver 
        name: randserver 
        volumeMounts: 
        - mountPath: /data 
          name: data 
        securityContext: 
          readOnlyRootFilesystem: true 
      volumes: 
      - name: data 
        persistentVolumeClaim: 
          claimName: randserver-data 

存储类

在 AWS 上,有几种不同类型的卷可用,提供不同的价格和性能特性。

为了在我们提供卷时提供一种简单的选择卷类型(和其他一些设置),我们创建了一个StorageClass对象,然后在创建PersistentVolumeClaim时可以通过名称引用它。

存储类的创建方式与任何其他 Kubernetes 对象相同,通过使用kubectl向 API 提交清单来创建:

kind: StorageClass 
apiVersion: storage.k8s.io/v1 
metadata: 
  name: general-purpose 
  annotations: 
    "storageclass.kubernetes.io/is-default-class": "true" 
provisioner: kubernetes.io/aws-ebs 
parameters: 
  type: gp2 

此清单创建了一个名为general-purpose的存储类,该存储类创建具有gp2卷类型的卷。如果您还记得我们在第六章中关于 EBS 卷类型的讨论,生产规划,这种基于 SSD 的卷类型适用于大多数通用应用程序,提供了良好的性能和价格平衡。

您还会注意到storageclass.kubernetes.io/is-default-class注释,该注释使StorageClass成为任何未指定存储类的PersistentVolumeClaim要使用的默认存储类。您应该只将此注释应用于单个StorageClass

parameter字段接受几种不同的选项。

最重要的参数字段是type,它允许我们选择gp2(默认值)、io1(预留 IOPS)、sc1(冷存储)或st1(吞吐量优化)中的一个。

如果您选择使用io1类型,还应使用iopsPerGB参数来指定每 GB 磁盘存储请求的 IOPS 数量。io1 EBS 卷支持的最大 IOPS/GB 比率为 50:1。

请记住,预留 IOPS 的成本使io1卷的成本比等效的通用卷高得多。为了提供与相同大小的gp2卷相似的吞吐量,预留 IOPS 的io1卷的成本可能是后者的三倍。因此,您应该仅在需要超过gp2卷提供的性能时才使用io1卷。一个可以优化成本的技巧是使用比应用程序要求的更大的gp2卷,以提供额外的 IO 积分。

例如,您可以创建几个不同的使用io1类型的类,供具有不同性能要求的应用程序使用:

kind: StorageClass 
apiVersion: storage.k8s.io/v1 
metadata: 
  name: high-iops-ssd 
provisioner: kubernetes.io/aws-ebs 
parameters: 
  type: io1 
  iopsPerGB: "50" 
---
 kind: StorageClass 
apiVersion: storage.k8s.io/v1 
metadata: 
  name: medium-iops-ssd 
provisioner: kubernetes.io/aws-ebs 
parameters: 
  type: io1 
  iopsPerGB: "25" 

请注意,Kubernetes 期望为iopsPerGb字段提供字符串值,因此您需要引用此值。

如果您使用的应用程序经过优化,可以对文件系统进行顺序读写操作,那么您可能会从使用st1卷类型中受益,该类型使用优化的磁盘存储提供高吞吐量的读写。不建议将此存储用于通用用途,因为进行随机访问读取或写入时的性能将很差:

kind: StorageClass 
apiVersion: storage.k8s.io/v1 
metadata: 
  name: throughput 
provisioner: kubernetes.io/aws-ebs 
parameters: 
  type: st1 

sc1 卷类型提供了作为 EBS 卷可用的最低成本存储,并且适用于不经常访问的数据。与 st1 卷一样,sc1 优化了顺序读写,因此在具有随机读写的工作负载上性能较差。

kind: StorageClass 
apiVersion: storage.k8s.io/v1 
metadata: 
  name: cold-storage 
provisioner: kubernetes.io/aws-ebs 
parameters: 
  type: sc1 

提前决定您想在集群中提供的不同存储类,并向集群用户提供关于何时使用每个类的文档是一个好主意。

在您的配置过程中,考虑提交一个存储类列表到您的集群中,因为在配置 EKS 集群时,默认情况下不会创建任何存储类。

有状态集

到目前为止,我们已经看到了如何使用 Kubernetes 自动为 PersistentVolumeClaim 配置 EBS 卷。这对于许多需要单个卷为单个 pod 提供持久性的应用程序非常有用。

然而,当我们尝试扩展我们的部署时,我们会遇到问题。运行在同一节点上的 pod 可能最终共享卷。但是,由于 EBS 卷一次只能附加到单个实例,任何调度到另一个节点的 pod 都将被卡在 ContainerCreating 状态,无休止地等待 EBS 卷被附加。

如果您正在运行一个应用程序,希望每个副本都有自己独特的卷,我们可以使用有状态集。当我们想要部署每个副本都需要有自己的持久存储的应用程序时,有状态集比部署具有两个关键优势。

首先,我们可以提供一个模板来为每个 pod 创建一个新的持久卷,而不是通过名称引用单个持久卷。这使我们能够通过扩展有状态集为每个 pod 副本提供独立的 EBS 卷。如果我们想要通过部署实现这一点,我们需要为每个副本创建一个单独的部署,每个部署通过名称引用不同的持久卷。

其次,当通过 StatefulSet 调度 pod 时,每个副本都有一个一致和持久的主机名,即使 pod 被重新调度到另一个节点,主机名也保持不变。这在运行软件时非常有用,每个副本都希望能够连接到特定地址的同行。在 Kubernetes 添加有状态集之前,将这样的软件部署到 Kubernetes 通常依赖于使用 Kubernetes API 执行服务发现的特殊插件。

为了说明有状态集的工作原理,我们将重写我们的示例应用部署清单,以使用StatefulSet。因为StatefulSet中的每个副本 Pod 都有可预测的主机名,所以我们首先需要创建一个服务,以允许将流量路由到这些主机名并传递到底层的 Pod:

apiVersion: v1 
kind: Service 
metadata: 
  name: randserver 
  labels: 
    app: randserver 
spec: 
  ports: 
  - port: 80 
    name: web 
    targetPort: 3000 
  clusterIP: None 
  selector: 
    app: randserver 

每个 Pod 将被赋予一个由有状态集的名称和集合中的 Pod 编号构成的主机名。主机名的域是服务的名称。

因此,当我们创建一个名为randserver的有三个副本的有状态集。集合中的 Pod 将被赋予主机名randserver-0randserver-1randserver-2。集群中运行的其他服务将能够通过使用名称randserver-0.randserverrandserver-1.randserverrandserver-2.randserver连接到这些 Pod。

StatefulSet的配置与部署的配置非常相似。应该注意的主要区别如下:

  • serviceName字段是我们需要引用用于为 Pod 提供网络访问的服务。

  • volumeClaimTemplates字段,我们在其中包含一个PersistentVolumeClaim的模板,该模板将为StatefulSet中的每个 Pod 副本创建。您可以将其视为为每个创建的 Pod 提供模板的模板字段的类比:

apiVersion: apps/v1 
kind: StatefulSet 
metadata: 
  name: randserver 
spec: 
  selector: 
    matchLabels: 
      app: randserver 
  serviceName: randserver 
  replicas: 3 
  template: 
    metadata: 
      labels: 
        app: randserver 
    spec: 
      containers: 
      - image: errm/randserver 
        name: randserver 
        volumeMounts: 
        - mountPath: /data 
          name: data 
        securityContext: 
          readOnlyRootFilesystem: true 
  volumeClaimTemplates: 
    - metadata: 
        name: data 
      spec: 
        accessModes: 
          - ReadWriteOnce 
        storageClassName: general-purpose 
        resources: 
          requests: 
            storage: 1Gi 

一旦您将StatefulSet提交给 Kubernetes,您应该能够看到已成功调度到集群的 Pod:

$ kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
randserver-0   1/1       Running   0          39s
randserver-1   1/1       Running   0          21s
randserver-2   1/1       Running   0          10s  

请注意,每个 Pod 的名称都遵循可预测的模式,不像使用部署或副本集创建的 Pod,它们每个都有一个随机名称。

尝试删除有状态集中的一个 Pod,并注意它被一个与被删除的 Pod 完全相同的名称的 Pod 所替换:

$ kubectl delete pod/randserver-1
$ kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
randserver-0   1/1       Running   0          17m
randserver-1   1/1       Running   0          18s
randserver-2   1/1       Running   0          17m  

如果您查看持久卷索赔,您将看到它们的名称也遵循可预测的模式,索赔的名称是由卷索赔模板元数据中给定的名称,有状态集的名称和 Pod 编号组成的。

kubectl get pvc
NAME                STATUS    VOLUME
data-randserver-0   Bound     pvc-803210cf-f027-11e8-b16d
data-randserver-1   Bound     pvc-99192c41-f027-11e8-b16d
data-randserver-2   Bound     pvc-ab2b25b1-f027-11e8-b16d  

如果删除(或缩减)一个有状态集,那么相关的持久卷索赔将保留。这非常有利,因为它使得更难丢失应用程序创建的宝贵数据。如果稍后重新创建(或扩展)有状态集,那么由于使用可预测的名称,相同的卷将被重用。

如果您确实打算从集群中完全删除有状态集,您可能还需要另外删除相应的持久卷声明:

$ kubectl delete statefulset randserver
statefulset.apps "randserver" deleted
$ kubectl delete pvc -l app=randserver
persistentvolumeclaim "data-randserver-0" deleted
persistentvolumeclaim "data-randserver-1" deleted
persistentvolumeclaim "data-randserver-2" deleted  

摘要

在本章中,我们已经了解了 Kubernetes 为您的应用程序提供存储的丰富工具集。

您应该已经学会了以下内容:

  • 如何为您的 pod 配置卷

  • 如何将卷挂载到容器中

  • 如何使用持久卷声明自动提供 EBS 卷

  • 通过配置存储类来提供不同的 EBS 卷类型

  • 如何为有状态集中的每个 pod 动态提供卷

现在您应该已经掌握足够的知识,可以将许多类型的应用程序部署到您的 Kubernetes 集群中。

进一步阅读

如果您想了解如何在 Kubernetes 中利用存储,这里有一些资源可能对您有用:

第十章:管理容器映像

容器编排平台需要一个坚实的基础来运行我们的容器。基础设施的一个重要组成部分是存储容器映像的位置,这将允许我们在创建 pod 时可靠地获取它们。

从开发人员的角度来看,应该非常容易和快速地推送新的映像,同时开发我们希望部署到 Kubernetes 的软件。我们还希望有机制帮助我们进行版本控制、编目和描述如何使用我们的图像,以便促进部署并减少交付错误版本或配置的风险。

容器映像通常包含知识产权、专有源代码、基础设施配置秘密,甚至商业机密。因此,我们需要适当的身份验证和授权机制来保护它们免受未经授权的访问。

在本章中,我们将学习如何利用 AWS 弹性容器注册表(ECR)服务来存储我们的容器映像,以满足所有这些需求。

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

  • 将 Docker 映像推送到 ECR

  • 给图像打标签

  • 给图像打标签

将 Docker 映像推送到 ECR

目前,存储和传递 Docker 映像的最常见方式是通过 Docker 注册表,这是 Docker 的一个开源应用程序,用于托管 Docker 仓库。这个应用程序可以部署在本地,也可以作为多个提供商的服务使用,比如 Docker Hub、Quay.io 和 AWS ECR。

该应用程序是一个简单的无状态服务,其中大部分维护工作涉及确保存储可用、安全和安全。正如任何经验丰富的系统管理员所知道的那样,这绝非易事,特别是如果有一个大型数据存储。因此,特别是如果您刚开始,强烈建议使用托管解决方案,让其他人负责保持图像的安全和可靠可用。

ECR 是 AWS 对托管 Docker 注册表的方法,每个帐户有一个注册表,使用 AWS IAM 对用户进行身份验证和授权以推送和拉取图像。默认情况下,仓库和图像的限制都设置为 1,000。正如我们将看到的,设置流程感觉非常类似于其他 AWS 服务,同时也对 Docker 注册表用户来说是熟悉的。

创建一个仓库

要创建一个仓库,只需执行以下aws ecr命令即可:

$ aws ecr create-repository --repository-name randserver 

这将创建一个存储我们randserver应用程序的存储库。其输出应该如下所示:

 {
 "repository": {
 "repositoryArn": "arn:aws:ecr:eu-central-1:123456789012:repository/randserver",
 "registryId": "123456789012",
 "repositoryName": "randserver",
 "repositoryUri": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/randserver",
 "createdAt": 1543162198.0
 }
 } 

对您的存储库的一个不错的补充是一个生命周期策略,清理旧版本的图像,这样您最终不会被阻止推送更新版本。可以通过使用相同的aws ecr命令来实现:

$ aws ecr put-lifecycle-policy --registry-id 123456789012 --repository-name randserver --lifecycle-policy-text '{"rules":[{"rulePriority":10,"description":"Expire old images","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":800},"action":{"type":"expire"}}]}'  

一旦在同一个存储库中有超过 800 个图像,这个特定的策略将开始清理。您还可以根据图像、年龄或两者进行清理,以及仅考虑清理中的一些标签。

有关更多信息和示例,请参阅docs.aws.amazon.com/AmazonECR/latest/userguide/lifecycle_policy_examples.html

从您的工作站推送和拉取图像

为了使用您新创建的 ECR 存储库,首先我们需要对本地 Docker 守护程序进行身份验证,以针对 ECR 注册表。再次,aws ecr将帮助您实现这一点:

aws ecr get-login --registry-ids 123456789012 --no-include-email  

这将输出一个docker login命令,该命令将为您的 Docker 配置添加一个新的用户密码对。您可以复制粘贴该命令,或者您可以按照以下方式运行它;结果将是一样的:

$(aws ecr get-login --registry-ids 123456789012 --no-include-email)  

现在,推送和拉取图像就像使用任何其他 Docker 注册表一样,使用创建存储库时得到的存储库 URI 输出:

$ docker push 123456789012.dkr.ecr.eu-central-1.amazonaws.com/randserver:0.0.1    
$ docker pull 123456789012.dkr.ecr.eu-central-1.amazonaws.com/randserver:0.0.1  

设置推送图像的权限

IAM 用户的权限应该允许您的用户执行严格需要的操作,以避免可能会产生更大影响的任何可能的错误。对于 ECR 管理也是如此,为此,有三个 AWS IAM 托管策略大大简化了实现它的过程:

  • AmazonEC2ContainerRegistryFullAccess:这允许用户对您的 ECR 存储库执行任何操作,包括删除它们,因此应该留给系统管理员和所有者。

  • AmazonEC2ContainerRegistryPowerUser:这允许用户在任何存储库上推送和拉取图像,对于正在积极构建和部署您的软件的开发人员非常方便。

  • AmazonEC2ContainerRegistryReadOnly:这允许用户在任何存储库上拉取图像,对于开发人员不是从他们的工作站推送软件,而是只是拉取内部依赖项来处理他们的项目的情况非常有用。

所有这些策略都可以附加到 IAM 用户,方法是通过将 ARN 末尾的策略名称替换为适当的策略(如前所述),并将--user-name指向您正在管理的用户:

$ aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly  --user-name johndoe

所有这些由 AWS 管理的策略都具有一个重要特征-它们都为您的注册表上的所有存储库添加权限。您可能会发现有几种情况远非理想-也许您的组织有几个团队不需要访问彼此的存储库;也许您希望有一个有权删除一些存储库但不是全部的用户;或者您只需要对持续集成CI)设置中的单个存储库进行访问。

如果您的需求符合上述描述的任何情况,您应该创建自己的策略,并具有所需的细粒度权限。

首先,我们将为我们的randserver应用程序的开发人员创建一个 IAM 组:

$ aws iam create-group --group-name randserver-developers
    {
          "Group": {
          "Path": "/",
          "GroupName": "randserver-developers",
          "GroupId": "AGPAJRDMVLGOJF3ARET5K",
          "Arn": "arn:aws:iam::123456789012:group/randserver-developers",
          "CreateDate": "2018-10-25T11:45:42Z"
          }
    } 

然后我们将johndoe用户添加到组中:

$ aws iam add-user-to-group --group-name randserver-developers --user-name johndoe  

现在我们需要创建我们的策略,以便我们可以将其附加到组上。将此 JSON 文档复制到文件中:

{ 
   "Version": "2012-10-17", 
   "Statement": [{ 
         "Effect": "Allow", 
         "Action": [ 
               "ecr:GetAuthorizationToken", 
               "ecr:BatchCheckLayerAvailability", 
               "ecr:GetDownloadUrlForLayer", 
               "ecr:GetRepositoryPolicy", 
               "ecr:DescribeRepositories", 
               "ecr:ListImages", 
               "ecr:DescribeImages", 
               "ecr:BatchGetImage", 
               "ecr:InitiateLayerUpload", 
               "ecr:UploadLayerPart", 
               "ecr:CompleteLayerUpload", 
               "ecr:PutImage"
          ], 
         "Resource": "arn:aws:ecr:eu-central-1:123456789012:repository/randserver" 
   }] 
} 

要创建策略,请执行以下操作,传递适当的 JSON 文档文件路径:

$ aws iam create-policy --policy-name EcrPushPullRandserverDevelopers --policy-document file://./policy.json

    {
          "Policy": {
          "PolicyName": "EcrPushPullRandserverDevelopers",
          "PolicyId": "ANPAITNBFTFWZMI4WFOY6",
          "Arn": "arn:aws:iam::123456789012:policy/EcrPushPullRandserverDevelopers",
          "Path": "/",
          "DefaultVersionId": "v1",
          "AttachmentCount": 0,
          "PermissionsBoundaryUsageCount": 0,
          "IsAttachable": true,
          "CreateDate": "2018-10-25T12:00:15Z",
          "UpdateDate": "2018-10-25T12:00:15Z"
          }
    }  

最后一步是将策略附加到组,以便johndoe和这个应用程序的所有未来开发人员可以像我们之前一样从他们的工作站使用存储库:

$ aws iam attach-group-policy --group-name randserver-developers --policy-arn arn:aws:iam::123456789012:policy/EcrPushPullRandserverDevelopers 

在 Kubernetes 中使用存储在 ECR 上的图像

您可能还记得,在第七章中,生产就绪的集群,我们将 IAM 策略AmazonEC2ContainerRegistryReadOnly附加到了我们集群节点使用的实例配置文件。这允许我们的节点在托管集群的 AWS 帐户中的任何存储库中获取任何图像。

为了以这种方式使用 ECR 存储库,您应该将清单上的 pod 模板的image字段设置为指向它,就像以下示例中一样:

image: 123456789012.dkr.ecr.eu-central-1.amazonaws.com/randserver:0.0.1.

给图像打标签

每当将 Docker 图像推送到注册表时,我们需要使用标签标识图像。标签可以是任何字母数字字符串:latest stable v1.7.3甚至c31b1656da70a0b0b683b060187b889c4fd1d958都是您可能用来标识推送到 ECR 的图像的完全有效的示例标签。

根据软件的开发和版本控制方式,您在此标签中放入的内容可能会有所不同。根据不同类型的应用程序和开发流程,可能会采用三种主要策略来生成图像。

版本控制系统(VCS)引用

当您从源代码受版本控制系统管理的软件构建图像时,例如 Git,此时标记图像的最简单方式是利用来自您 VCS 的提交 ID(在使用 Git 时通常称为 SHA)。这为您提供了一个非常简单的方式来确切地检查您的代码当前正在运行的版本。

这种策略通常适用于以增量方式交付小改动的应用程序。您的图像的新版本可能会每天推送多次,并自动部署到测试和类生产环境中。这些应用程序的良好示例是 Web 应用程序和其他以服务方式交付的软件。

通过将提交 ID 通过自动化测试和发布流水线,您可以轻松生成软件确切修订版本的部署清单。

语义版本

然而,如果您正在构建用于许多用户使用的容器图像,无论是您组织内的多个用户,还是当您公开发布图像供第三方使用时,这种策略会变得更加繁琐和难以处理。对于这类应用程序,使用具有一定含义的语义版本号可能会有所帮助,帮助依赖于您图像的人决定是否安全地迁移到新版本。

这些图像的常见方案称为语义化版本SemVer)。这是由三个由点分隔的单独数字组成的版本号。这些数字被称为MAJORMINORPATCH版本。语义版本号以MAJOR.MINOR.PATCH的形式列出这些数字。当一个数字递增时,右边的不那么重要的数字将被重置为0

这些版本号为下游用户提供了有关新版本可能如何影响兼容性的有用信息:

  • 每当实施了一个保持向后兼容的错误修复或安全修复时,PATCH版本会递增

  • 每当添加一个保持向后兼容的新功能时,MINOR版本号会递增

  • 任何破坏向后兼容性的更改都应该增加 MAJOR 版本号

这很有用,因为镜像的用户知道 MINORPATCH 级别的更改不太可能导致任何问题,因此升级到新版本时只需要进行基本测试。但是,如果升级到新的 MAJOR 版本,他们应该检查和测试更改的影响,这可能需要更改配置或集成代码。

您可以在 semver.org/ 了解更多关于 SemVer 的信息。

上游版本号

通常,当我们构建重新打包现有软件的容器镜像时,希望使用打包软件本身的原始版本号。有时,为了对打包软件使用的配置进行版本控制,添加后缀可能会有所帮助。

在较大的组织中,常常会将软件工具与组织特定的默认配置文件打包在一起。您可能会发现将配置文件与软件工具一起进行版本控制很有用。

如果我要为我的组织打包 MySQL 数据库供使用,镜像标签可能看起来像 8.0.12-c15,其中 8.0.12 是上游 MySQL 版本,c15 是我为包含在我的容器镜像中的 MySQL 配置文件创建的版本号。

给镜像贴标签

如果您的软件开发和发布流程稍微复杂,您可能会迅速发现自己希望在镜像标签中添加更多关于镜像的语义信息,而不仅仅是简单的版本号。这可能很快变得难以管理,因为每当您想添加一些额外信息时,都需要修改构建和部署工具。

感谢地,Docker 镜像携带标签,可用于存储与镜像相关的任何元数据。

在构建时,可以使用 Dockerfile 中的 LABEL 指令为镜像添加标签。LABEL 指令接受多个键值对,格式如下:

LABEL <key>=<value> <key>=<value> ... 

使用此指令,我们可以在镜像上存储任何我们认为有用的任意元数据。由于元数据存储在镜像内部,不像标签那样可以更改。通过使用适当的镜像标签,我们可以发现来自我们版本控制系统的确切修订版,即使镜像已被赋予不透明的标签,如 lateststable

如果要在构建时动态设置这些标签,还可以在 Dockerfile 中使用 ARG 指令。

让我们看一个使用构建参数设置标签的例子。这是一个示例 Dockerfile:

FROM scratch 
ARG SHA  
ARG BEAR=Paddington 
LABEL git-commit=$GIT_COMMIT \ 
      favorite-bear=$BEAR \ 
      marmalade="5 jars" 

当我们构建容器时,我们可以使用--build-arg标志传递标签的值。当我们想要传递动态值,比如 Git 提交引用时,这是很有用的:

docker build --build-arg SHA=`git rev-parse --short HEAD` -t bear . 

与 Kubernetes 允许您附加到集群中对象的标签一样,您可以自由地使用任何方案为您的图像添加标签,并保存对您的组织有意义的任何元数据。

开放容器倡议(OCI),一个促进容器运行时和其图像格式标准的组织,已经提出了一套标准标签,可以用来提供有用的元数据,然后可以被其他理解它们的工具使用。如果您决定向您的容器图像添加标签,选择使用这套标签的部分或全部可能是一个很好的起点。

这些标签都以org.opencontainers.image为前缀,以便它们不会与您可能已经使用的任何临时标签发生冲突:

  • * org.opencontainers.image.title:这应该是图像的可读标题。例如,Redis

  • org.opencontainers.image.description:这应该是图像的可读描述。例如,Redis 是一个开源的键值存储

  • org.opencontainers.image.created:这应该包含图像创建的日期和时间。它应该按照 RFC 3339 格式。例如,2018-11-25T22:14:00Z

  • org.opencontainers.image.authors:这应该包含有关负责此图像的人或组织的联系信息。通常,这可能是电子邮件地址或其他相关联系信息。例如,Edward Robinson <ed@errm.co.uk>

  • org.opencontainers.image.url:这应该是一个可以找到有关图像更多信息的 URL。例如,github.com/errm/kubegratulations

  • org.opencontainers.image.documentation:这应该是一个可以找到有关图像文档的 URL。例如,github.com/errm/kubegratulations/wiki

  • org.opencontainers.image.source:这应该是一个 URL,可以在其中找到用于构建镜像的源代码。您可以使用它来链接到版本控制存储库上的项目页面,例如 GitHub、GitLab 或 Bitbucket。例如,github.com/errm/kubegratulations

  • org.opencontainers.image.version:这可以是打包在此镜像中的软件的语义版本,也可以是您的 VCS 中使用的标签或标记。例如,1.4.7

  • org.opencontainers.image.revision:这应该是对您的 VCS 中的修订的引用,例如 Git 提交 SHA。例如,e2f3bbdf80acd3c96a68ace41a4ac699c203a6a4

  • org.opencontainers.image.vendor:这应该是分发镜像的组织或个人的名称。例如,Apache Software FoundationASF)。

  • org.opencontainers.image.licenses:如果您的镜像包含受特定许可证覆盖的软件,您可以在这里列出它们。您应该使用 SPDX 标识符来引用许可证。您可以在spdx.org/licenses/找到完整的列表。例如,Apache-2.0

总结

在这一章中,我们学习了如何轻松地在 AWS 上配置 Docker 注册表,以可复制和防错的方式存储我们应用程序的镜像,使用 ECR。

我们发现了如何从我们自己的工作站推送镜像,如何使用 IAM 权限限制对我们镜像的访问,以及如何允许 Kubernetes 直接从 ECR 拉取容器镜像。

您现在应该了解了几种标记镜像的策略,并知道如何向镜像添加附加标签,以存储有关其内容的元数据,并且您已经了解了 Open Container Initiative 镜像规范推荐的标准标签。

posted @ 2024-05-20 11:58  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报