既然我们已经了解了构成 Kubernetes 集群的基础知识,我们仍然需要了解如何将所有的 Kubernetes 组件放在一起,以及如何满足它们的需求来提供一个生产就绪的 Kubernetes 集群。
在本章中,我们将研究如何确定这些需求,以及它们将如何帮助我们保持稳定的工作负载并实现成功的部署。
我们将在本章探讨以下主题:
- Kube 尺寸
- 确定存储注意事项
- 确定网络需求
- 自定义 kube 对象
在设计 Kubernetes 集群时,我们不仅需要担心如何配置部署对象来托管我们的应用,或者如何配置服务对象来提供跨吊舱的通信,所有这些都托管在哪里也很重要。因此,我们还需要考虑平衡应用工作负载和控制平面所需的资源。
我们将需要至少一个三节点etcd
集群,以便它能够在一个节点出现故障时支持自己。因为etcd
使用了一种叫做 Raft 的分布式普查算法,所以推荐奇数集群。这是因为,为了允许一个动作,集群中超过 50%的成员必须同意它。例如,在双节点群集的情况下,如果其中一个节点出现故障,另一个节点的投票率仅为群集的 50%,因此群集会失去仲裁。现在,当我们有一个三节点集群时,单个节点故障仅代表 33.33%的投票损失,其余两个节点的投票仍然是 66.66%的行动被允许。
The following link is for a great website where you can learn exactly how the Raft algorithm works: http://thesecretlivesofdata.com/raft/.
对于etcd
,我们可以为集群选择两种部署模式。我们可以在与 kube-apiserver 相同的节点上运行它,也可以让一组单独的集群运行我们的键值存储。无论哪种方式,这都不会改变etcd
达到法定人数的方式,所以您仍然需要在您的控制平面管理器节点上以奇数安装etcd
。
对于 Kubernetes 用例来说,etcd
不会消耗大量的计算资源,比如 CPU 或者内存。虽然etcd
确实积极缓存键值数据,并使用其大部分内存跟踪观察器,但两个内核和 8 GB 内存将绰绰有余。
说到磁盘,这是您需要更加关键的地方。etcd
集群严重依赖磁盘延迟,因为共识协议在日志中持久存储元数据的方式。etcd
集群的每个成员都必须存储每个请求,延迟的任何大峰值都会触发集群领导者选举,这将导致集群不稳定。除非您在 Raid 0 磁盘中运行 15k RPM 的磁盘,以尽可能从磁盘驱动器中获得最高性能,否则用于 T2 的硬盘驱动器 ( 硬盘驱动器)是不可能的。一个固态硬盘 ( 固态硬盘)是不错的选择,而且凭借极低的延迟和每秒更高的输入/输出操作 ( IOPS ),它们是托管您的键值存储的最佳选择。谢天谢地,所有主要的云提供商都提供固态硬盘解决方案来满足这一需求。
控制平面组件所需的剩余资源将取决于它们将管理的节点数量以及您将在其上运行的加载项。需要考虑的另一件事是,您可以将这些主节点放在负载平衡器后面,以减轻负载并提供高可用性。除此之外,您还可以在争用期间水平扩展主节点。
考虑到所有这些,并考虑到etcd
将与我们的主节点一起托管,我们可以说一个三主节点集群,其中有 2 到 4 个虚拟机,8 到 16 GB 的内存足以处理大于或等于 100 个工作节点的虚拟机 ( 虚拟机)。
另一方面,工作节点将承担重任——这些人将运行我们的应用工作负载。标准化这些节点的大小是不可能的,因为它们属于*万一呢?*场景。我们需要确切地知道我们将在我们的节点上运行什么类型的应用,以及它们的资源需求,以便我们能够正确地调整它们的大小。节点不仅要根据应用资源需求来调整大小,而且我们还必须考虑在其上运行超过计划的单元的时间段。例如,您可以对部署执行滚动更新,以使用更新的映像,这取决于您如何配置您的maxSurge
;这个节点必须处理 10%到 25%的负载。
容器确实是轻量级的,但是当管弦乐队开始演奏时,您可以在单个节点上运行 30 个、40 个甚至 100 个容器!这将成倍增加每台主机的资源消耗。虽然 pods 附带了资源限制功能和规范来限制容器的资源消耗,但您仍然需要考虑这些容器所需的资源。
在竞争和高资源需求期间,节点总是可以水平扩展的。然而,拥有这些额外的资源总是好的,以避免任何不受欢迎的内存不足的杀手。所以,为未来和*做计划,如果呢?*拥有额外资源池的场景。
我们的节点仍然需要与我们的应用编程接口服务器通信,正如我们之前提到的,拥有几个主节点需要一个负载平衡器。当涉及到从我们的节点到主节点的负载平衡请求时,我们有几个选项可供选择,具体取决于您运行集群的位置。如果您在公共云中运行 Kubernetes,您可以继续使用云提供商的负载平衡器选项,因为它们通常是弹性的。这意味着它们会根据需要自动缩放,并提供比您实际需要的更多功能。本质上,对 API 服务器的负载平衡请求将是负载平衡器将执行的唯一任务。这将我们引向内部场景——由于我们坚持使用开源解决方案,因此您可以配置一个运行 HAProxy 或 NGINX 的 Linux 盒子来满足您的负载平衡需求。在 HAProxy 和 NGINX 之间进行选择没有错误的答案,因为它们为您提供了您所需要的东西。
到目前为止,基本架构如下图所示:
存储需求不像常规主机或虚拟机管理程序那样简单。我们的节点和 pod 将消耗几种类型的存储,我们需要对它们进行适当的分层。因为您运行的是 Linux,所以将存储分层到不同的文件系统和存储后端将变得非常容易——没有什么是逻辑卷管理器 ( LVM )或不同的挂载点无法解决的。
基本的 Kubernetes 二进制文件,比如kubelet
、kube-proxy
,可以和 OS 文件一起运行在基本存储上;不需要非常高端的东西,因为任何固态硬盘都足以满足他们的需求。
另一方面,现在我们有了存储和运行容器图像的存储空间。回到第 6 章、创建高可用性自愈架构,我们了解到容器是由只读层组成的。这意味着,当磁盘在单个节点上运行数十个甚至数百个容器时,它们在读取请求时会受到很大冲击。为此,存储后端必须以非常低的延迟为读取请求提供服务。IOPS 和延迟方面的具体数字因环境而异,但基础是相同的。这是因为容器的性质——提供比写入更高的读取性能的磁盘将是优选的。
存储性能不是唯一需要考虑的因素。储物空间也很重要。计算所需空间取决于以下两点:
- 你要运行的图像有多大?
- 你将运行多少个不同的图像,它们的大小是多少?
这将直接消耗/var/lib/docker
或/var/lib/containerd
中的空间。考虑到这一点,/var/lib/docker
或containerd/
的单独挂载点将是一个不错的选择,该挂载点有足够的空间来存储你将要在吊舱上运行的所有图像。请注意,这些图像是短暂的,不会永远存在于您的节点上。Kubernetes 确实在 kubelet 中嵌入了垃圾收集策略,如果达到指定的磁盘使用阈值,它将删除不再使用的旧映像。这些选项是HighThresholdPercent
和LowThresholdPercent.
你可以用一个 kubelet 标志来设置它们:--eviction-hard=imagefs.available
或--eviction-soft=imagefs.available
。默认情况下,这些标志已经配置为当可用存储空间小于 15%时进行垃圾收集,但是,您可以根据需要进行调整。eviction-hard
是开始删除图像需要达到的阈值,eviction-soft
是停止删除图像需要达到的百分比或量。
一些容器仍然需要某种持久数据的读/写卷。正如在第 7 章、中所讨论的,了解 Kubernetes 集群的核心组件,有几个存储资源调配器,它们都适合不同的场景。您需要知道的是,由于 Kubernetes 存储类,您可以选择一系列选项。值得一提的一些开源软件定义的存储解决方案如下:
- Ceph
- 格鲁斯特
- OpenStack 煤渣
- 网络文件系统 ( NFS )
每个存储资源调配者都有其优点和缺点,但详细介绍每一个都超出了本书的范围。在前面的章节中,我们已经对 Gluster 进行了很好的概述,因为我们将在后面的章节中使用它来进行示例部署。
为了了解我们集群的网络需求,我们首先需要了解 Kubernetes 网络模型及其旨在解决的问题。容器联网可能很难掌握;然而,它有三个基本问题:
- 容器如何相互对话(在同一台主机上和不同的主机上)?
- 容器如何与外界对话,外界如何与容器对话?
- 谁分配和配置每个容器的唯一 IP 地址?
同一个主机上的容器可以通过虚拟桥相互通信,您可以从bridge-utils
包中使用brctl
实用程序看到该虚拟桥。这是由 Docker 引擎处理的,它被称为 Docker 网络模型。容器通过一个“T3”虚拟接口连接到名为“T2”的虚拟桥,该接口从一个专用子网地址分配一个 IP。这样,所有容器都可以通过它们的veth
虚拟接口相互对话。当容器被分配在不同的主机上,或者当外部服务想要与它们通信时,Docker 模型的问题就出现了。为了解决这个问题,Docker 提供了一种方法,其中容器通过主机的端口暴露给外部世界。请求进入主机 IP 地址中的某个端口,然后被代理到该端口后面的容器。
这种方法有用,但不理想。您不能将服务配置到特定的端口,也不能在动态端口分配场景中配置服务——我们的服务在每次部署时都需要标志来连接到正确的端口。这会很快变得非常混乱。
为了避免这种情况,Kubernetes 实现了自己的网络模型,该模型必须遵守以下规则:
- 所有吊舱无需网络地址转换 ( NAT )即可与所有其他吊舱通信
- 所有节点都可以在没有 NAT 的情况下与所有吊舱通信
- 吊舱认为自己的知识产权和别人认为的知识产权是一样的
有几个开源项目可以帮助我们实现这个目标,最适合你的项目将取决于你的情况。以下是其中的一些:
- 卡利科项目
- 编织网
- 法兰绒
- 多维数据集路由器
将入侵防御系统分配给吊舱,并让它们相互对话,这并不是唯一需要注意的问题。Kubernetes 还提供了基于 DNS 的服务发现,因为通过 DNS 记录而不是 IPs 进行对话的应用要高效得多,并且可扩展。
Kubernetes 在其 kube-system 名称空间中有一个部署,我们将在本章的后面部分重新讨论名称空间。该部署由一个带有一组容器的 pod 组成,这些容器组成了一个 DNS 服务器,负责创建集群中的所有 DNS 记录,并为服务发现的 DNS 请求提供服务。
Kubernetes 还将创建一个指向上述部署的服务,并将告诉 kubelet 配置每个 pod 的容器,默认情况下使用服务的 IP 作为 DNS 解析器。这是默认行为,但是您可以通过在 pod 的规范上设置 DNS 策略来覆盖它。您可以从以下规格中选择:
- 默认:这个是反直觉的,因为它不是现实中的默认。使用此策略,pod 将从运行该 pod 的节点继承名称解析。例如,如果一个节点被配置为使用
8.8.8.8
作为其 DNS 服务器,resolv.conf
单元也将被配置为使用相同的 DNS 服务器。 - 集群优先:这实际上是默认策略,正如我们之前提到的,任何运行集群优先的 pod 都将使用
kube-dns
服务的 IP 配置resolv.conf
。任何不在群集本地的请求都将被转发到节点配置的 DNS 服务器。
并非所有 Kubernetes 对象都有 DNS 记录。只有服务,在某些特定情况下,pods 会为它们创建记录。DNS 服务器中有两种类型的记录: A 记录和服务记录(SRV)。a 根据创建的服务类型创建记录;这里我们指的不是spec.type
。有两种类型的服务:正常服务,我们在第 7 章、了解一个 Kubernetes 集群的核心组件中进行了修改,与type
规范下的对应;和无头服务。在解释无头服务之前,让我们探索一下正常服务的行为。
对于每个正常服务,创建一个指向该服务的群集 IP 地址的 A 记录;这些记录的结构如下:
<service-name>.<namespace>.svc.cluster.local
任何运行在与服务相同命名空间上的 pod 都只能通过其shortname: <service-name>
字段解析服务。这是因为命名空间之外的任何其他 pod 都必须在 shortname 实例之后指定命名空间:
<service-name>.<namespace>
对于无头服务,记录的工作方式有点不同。首先,无头服务是没有分配集群 IP 的服务。因此,指向服务 IP 的 A 记录是不可能创建的。要创建一个无头服务,您可以用none
定义.spec.clusterIP
命名空间,这样就不会给它分配任何 IP。然后,Kubernetes 将基于该服务的端点创建一个记录。本质上,豆荚是通过selector
领域选择的,尽管这不是唯一的要求。由于创建 A 记录的格式,pods 需要几个新字段,以便 DNS 服务器为它们创建记录。
Pods 将需要两个新的规格字段:hostname
和subdomain
。hostname
字段将是吊舱的hostname
字段,而subdomain
将是您为这些吊舱创建的无头服务的名称。此的 A 记录将以以下方式指向每个 pod 的 IP:
<pod hostname>.<subdomian/headless service name>.<namespace>.svc.cluster.local
此外,将使用无头服务创建另一条记录,如下所示:
<headless service>.<namespace>.svc.cluster.local
该记录将返回服务背后的所有吊舱的 IP 地址。
我们现在已经具备了开始构建集群的必要条件。但是,仍然有一些设计特性不仅包括 Kubernetes 二进制文件及其配置,还可以调整 Kubernetes API 对象。我们将在下一节中介绍您可以执行的一些调整。
说到 Kubernetes 对象,一切都取决于您试图为其构建基础架构的工作负载或应用的类型。因此,我们将讨论如何在每个对象上配置最常用和最有用的规范,而不是设计或构建任何特定的定制。
Kubernetes 提供了名称空间,作为将集群分割成多个虚拟集群的一种方式。可以把它看作是一种分割集群资源和对象的方式,并把它们彼此逻辑隔离。
名称空间将仅在非常特定的场景中使用,但是 Kubernetes 附带了一些预定义的名称空间:
- 默认:这是默认的命名空间,所有没有命名空间定义的对象都会被放入其中。
- kube-system :任何由 Kubernetes 集群创建并为其创建的对象都将被放在这个命名空间中。集群基本功能所需的对象将放在这里。例如,您会发现
kube-dns
、kubernetes-dashboard
、kube-proxy
或任何外部应用的附加组件或代理,如fluentd
、logstash
、traefik
和入口控制器。 - kube-public :为任何人都可以看到的对象保留的命名空间,包括未经身份验证的用户。
创建命名空间非常简单明了;您可以通过运行以下命令来实现:
kubectl create namespace <name>
就是这样,您现在有了自己的名称空间。要在这个名称空间中放置对象,您将使用metadata
字段并添加namespace
键值对;例如,考虑来自 YAML 吊舱的这个摘录:
apiVersion: v1
kind: Pod
metadata:
namespace: mynamespace
name: pod1
您会发现自己正在为集群创建自定义名称空间,这些集群通常非常大,并且有相当多的用户或不同的团队在消耗他们的资源。对于这些类型的场景,名称空间是完美的。名称空间将允许您将团队的所有对象与其他对象隔离开来。名称甚至可以在相同的类对象上重复,只要它们在不同的命名空间上。
命名空间不仅为对象提供隔离,还可以为每个命名空间设置资源配额。假设您有几个开发团队在您的集群上工作——一个团队正在开发一个非常轻量级的应用,另一个团队正在开发一个非常资源密集型的应用。在这种情况下,您不希望第一个开发团队消耗资源密集型应用团队的任何额外计算资源,这就是资源配额发挥作用的地方。
资源配额也是 Kubernetes API 对象;但是,它们被设计为通过对计算资源进行限制,甚至限制每个分配空间上的对象数量,来专门处理名称空间。
通过传递给kubectl
命令的YAML
文件,该ResourceQuota
应用编程接口对象像 Kubernetes 斯中的任何其他对象一样被声明。
基本资源配额定义如下:
apiVersion: v1
kind: ResourceQuota
Metadata:
Namespace: devteam1
name: compute-resources
spec:
hard:
pods: "4"
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
我们可以设置两种类型的基本配额:计算资源配额和对象资源配额。如前例所示,pods
是对象配额,其余是计算配额。
在这些字段中,您将指定所提供资源的总和,命名空间不能超过该总和。例如,在这个命名空间中,运行的pods
总数不能超过4
,它们的资源总和不能超过1
CPU 和2Gi
的 RAM 内存。
每个命名空间可以分配给任何可以放入命名空间的 kube API 对象的最大对象数;以下是可以用名称空间限制的对象列表:
- 持续卷索赔 ( PVCs )
- 服务
- 秘密
- 配置地图
- 复制控制器
- 部署
- 复制集
- 状态集
- 乔布斯
- 计划任务
说到计算资源,不仅内存和 CPU 会受到限制,而且您还可以为存储空间分配配额—但是,这些配额仅适用于物理卷。
为了更好地理解计算配额,我们需要更深入地探索如何在 pod 基础上管理和分配这些资源。这也将是一个很好的时间来理解如何更好地设计吊舱。
在非受限名称空间上没有资源限制的 Pods 可以在没有警告的情况下消耗节点的所有资源;但是,在 pod 的规范中有一组工具可以更好地处理它们的计算分配。
当您将资源分配给 pod 时,实际上并没有将它们分配给 pod。相反,你是在一个容器的基础上做的。因此,一个具有多个容器的 pod 将对其每个容器有多个资源约束;让我们考虑以下示例:
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: db
image: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "password"
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
- name: wp
image: wordpress
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
在这个 pod 声明中,在containers
定义下,我们有两个新的领域没有涉及到:env
和resources
。resources
字段包含我们的containers
的计算资源限制和要求。通过设置limits
,您告诉容器它可以向该资源类型请求的最大资源数量。如果容器超过限制,它将被重新启动或终止。
request
字段指的是 Kubernetes 将向该容器保证多少资源。为了使容器能够运行,主机节点必须有足够的空闲资源来满足请求。
CPU 和内存的测量方式不同。例如,当我们分配或限制中央处理器时,我们用中央处理器单位说话。有几种设置中央处理器单元的方法;首先,您可以指定整数或小数,如 1、2、3、0.1 和 1.5,这将对应于您要分配给该容器的虚拟内核数量。另一种赋值方式是使用 milicore 表达式。一个百万核心(1m),这是您可以分配的最小 CPU 数量,相当于 0.001 个 CPU 核心;例如,您可以完成以下任务:
cpu: "250m"
这与编写以下内容相同:
cpu: 0.25
分配中央处理器的首选方式是通过毫核心,因为应用编程接口会将整数转换为毫核心。
对于内存分配,您可以使用正常的内存单位,如千字节或千字节;其他任何存储单元也是如此,例如 E、P、T、G 和 m
回到资源配额,我们可以看到单个容器资源管理将如何与名称空间上的资源配额一起发挥作用。这是因为资源配额会告诉我们在容器中每个名称空间可以设置多少限制和请求。
第二个我们没有修改的字段是env
字段。借助env
,我们为容器配置环境变量。通过变量声明,我们可以将设置、参数、密码和更多配置传递给容器。在 pod 中声明变量的最简单方法如下:
...
env:
- name: VAR
value: “Hello World”
现在容器可以访问其 Shell 中的VAR
变量内容,称为$VAR
。正如我们前面提到的,这是声明变量并为其提供值的最简单方法。然而,这并不是最有效的方法——当你以这种方式声明一个值时,这个值将只存在于 pod 声明中。
如果我们需要编辑该值或将该值传递给多个窗格,这将变得很麻烦,因为您需要在每个需要它的窗格上键入相同的值。这里我们将介绍另外两个 Kubernetes API 对象:Secrets
和ConfigMaps
。
有了ConfigMaps
和Secrets
,我们可以以一种持久的和更模块化的形式存储变量的值。本质上,ConfigMaps
和Secrets
是一样的,但是秘密包含了它们编码在base64
中的价值。机密用于存储敏感信息,如密码或私钥,本质上是任何类型的机密数据。其余所有不需要隐藏的数据都可以通过ConfigMap
传递。
创建这两种类型的对象的方式与在 Kubernetes 中创建任何其他对象的方式相同—通过YAML
。您可以如下创建一个ConfigMap
对象:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
super.data: much-data
very.data: wow
与本章中的所有其他定义相比,这个定义的唯一区别是我们缺少了规范字段。相反,我们有数据,我们将把包含我们想要存储的数据的键值对放在那里。
与Secrets
相比,这种工作方式略有不同。这是因为我们需要存储的密钥的值必须被编码。为了在秘密密钥中存储一个值,我们将该值传递给base64
,如下所示:
[dsala@RedFedora]$ echo -n “our secret” | base64
WW91IEhhdmUgRGVjb2RlZCBNeSBTZWNyZXQhIENvbmdyYXR6IQ==
当我们有了字符串的散列后,我们就准备好创造我们的秘密了。
下面的代码块显示了一个在base64
中配置了秘密值的YAML
文件:
apiVersion: v1
kind: Secret
metadata:
name: kube-secret
type: Opaque
data:
password: WW91IEhhdmUgRGVjb2RlZCBNeSBTZWNyZXQhIENvbmdyYXR6IQ==
要在豆荚中使用我们的ConfigMaps
和Secrets
对象,我们使用env
数组中的valueFrom
字段:
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: secret-container
image: busybox
env:
- name: SECRET_VAR
valueFrom:
secretKeyRef:
name: kube-secret
key: password
这里secretKeyRef
下的名称对应Secret
API 对象名称,key
是Secret
中data
字段的key
。
有了ConfigMaps
,看起来会差不多;但是,在valueFrom
领域,我们将使用configMapKeyRef
代替secretKeyRef
。
ConfigMap
申报如下:
…
env:
- name: CONFMAP_VAR
valueFrom:
configMapKeyRef:
name: my-config
key: very.data
Now that you understand the basics of customizing pods, you can take a look at a real-life example at https://kubernetes.io/docs/tutorials/configuration/configure-redis-using-configmap/.
在本章中,我们学习了如何确定 Kubernetes 集群的计算和网络需求。我们还谈到了随之而来的软件需求,例如etcd
,以及奇数编号的集群是如何被优先选择的(由于人口普查算法),因为集群需要获得 50%以上的一致投票。
etcd
集群既可以在 kube-apiserver 上运行,也可以有一组单独的集群专门用于etcd
。说到资源,2 个 CPU,8gb RAM 应该够了。在决定etcd
的存储系统时,选择延迟更低、IOPS 更高的存储,如固态硬盘。然后我们开始估算 kube-apiserver,它可以和etcd
一起运行。考虑到两个组件可以共存,资源应该提升到每个节点 8 到 16 GB 的内存和 2 到 4 个处理器。
为了适当地调整工作节点的大小,我们必须记住这是实际应用工作负载运行的地方。这些节点应该根据应用需求进行调整,并且在运行的 pods 数量可能超过计划数量的时段,例如滚动更新期间,应该考虑额外的资源。继续讨论集群的需求,我们讨论了负载平衡器如何通过平衡集群之间的请求来帮助主节点的通信。
【Kubernetes 的存储需求可能会非常大,因为许多因素会影响整体设置,倾向于读取优于写入的存储系统更可取。此外,Kubernetes 最常见的存储提供商如下:
然后,我们转向网络方面,了解了 Kubernetes 如何提供服务,例如基于 DNS 的服务发现,它负责创建集群中的所有 DNS 记录,并为服务发现的 DNS 请求提供服务。Kubernetes 中的对象可以定制,以适应每个工作负载的不同需求,名称空间之类的东西被用作将集群分割成多个虚拟集群的一种方式。资源限制可以通过资源配额来实现。
最后,可以定制 pod,以允许分配绝对最大数量的资源,并避免单个 pod 消耗所有工作节点的资源。我们详细讨论了各种存储注意事项和要求,包括如何定制 kube 对象和 pods。
在下一章中,我们将跳转到部署 Kubernetes 集群,并学习如何配置它。
- 为什么优先选择奇数
etcd
簇? etcd
能和 kube-apiserver 一起跑吗?- 为什么建议
etcd
延迟更低? - 什么是工作者节点?
- 调整工作节点时应该考虑什么?
- Kubernetes 有哪些存储提供商?
- 为什么需要负载平衡器?
- 如何使用命名空间?
- 掌握 Kubernetes 作者:Gigi Sayfan:https://www . packtpub . com/虚拟化与云/掌握-kubernetes
- 开发人员的 Kubernetes作者:Joseph Heck:https://www . packtpub . com/虚拟化与云/kubernetes-developers
- 与 Kubernetes 的实践微服务作者:Gigi Sayfan:https://www . packtpub . com/虚拟化与云/实践微服务-kubernetes
- Kubernetes 入门-第三版作者:Jonathan Baier,杰西·怀特:https://www . packtpub . com/虚拟化与云/入门-Kubernetes-第三版
- Mastering Docker–第二版Russ McKendrick,Scott 加拉格尔:https://www . packtpub . com/虚拟化与云/Mastering-Docker-第二版
- Docker Bootcamp 作者:Russ McKendrick 等人:https://www . packtpub . com/虚拟化与云/docker-bootcamp