Kubernetes-云原生指南(全)

Kubernetes 云原生指南(全)

原文:zh.annas-archive.org/md5/58DD843CC49B42503E619A37722EEB6C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书的目的是为您提供构建使用 Kubernetes 的云原生应用程序所需的知识和广泛的工具集。Kubernetes 是一种强大的技术,为工程师提供了强大的工具,以使用容器构建云原生平台。该项目本身不断发展,并包含许多不同的工具来解决常见的场景。

对于本书的布局,我们不会局限于 Kubernetes 工具集的任何一个特定领域,而是首先为您提供默认 Kubernetes 功能的最重要部分的全面摘要,从而为您提供在 Kubernetes 上运行应用程序所需的所有技能。然后,我们将为您提供在第 2 天场景中处理 Kubernetes 安全性和故障排除所需的工具。最后,我们将超越 Kubernetes 本身的界限,探讨一些强大的模式和技术,以构建在 Kubernetes 之上的内容,例如服务网格和无服务器。

这本书是为谁准备的

这本书是为初学者准备的,但您应该对容器和 DevOps 原则非常熟悉,以便充分利用本书。对 Linux 有扎实的基础将有所帮助,但并非完全必要。

本书涵盖内容

第一章《与 Kubernetes 通信》向您介绍了容器编排的概念以及 Kubernetes 工作原理的基础知识。它还为您提供了与 Kubernetes 集群通信和认证所需的基本工具。

第二章《设置您的 Kubernetes 集群》将指导您通过几种不同的流行方式在本地机器和云上创建 Kubernetes 集群。

第三章《在 Kubernetes 上运行应用程序容器》向您介绍了在 Kubernetes 上运行应用程序的最基本构建块 - Pod。我们将介绍如何创建 Pod,以及 Pod 生命周期的具体内容。

第四章《扩展和部署您的应用程序》回顾了更高级的控制器,这些控制器允许扩展和升级应用程序的多个 Pod,包括自动扩展。

第五章,服务和入口 - 与外部世界通信,介绍了将在 Kubernetes 集群中运行的应用程序暴露给外部用户的几种方法。

第六章,Kubernetes 应用程序配置,为您提供了在 Kubernetes 上运行的应用程序提供配置(包括安全数据)所需的技能。

第七章,Kubernetes 上的存储,回顾了为在 Kubernetes 上运行的应用程序提供持久性和非持久性存储的方法和工具。

第八章,Pod 放置控制,介绍了控制和影响 Kubernetes 节点上 Pod 放置的几种不同工具和策略。

第九章,Kubernetes 上的可观察性,涵盖了在 Kubernetes 上下文中可观察性的多个原则,包括指标、跟踪和日志记录。

第十章,Kubernetes 故障排除,回顾了 Kubernetes 集群可能出现故障的一些关键方式,以及如何有效地对 Kubernetes 上的问题进行分类。

第十一章,Kubernetes 上的模板代码生成和 CI/CD,介绍了 Kubernetes YAML 模板工具和一些常见的 Kubernetes 上的 CI/CD 模式。

第十二章,Kubernetes 安全和合规性,涵盖了 Kubernetes 安全的基础知识,包括 Kubernetes 项目的一些最近的安全问题,以及集群和容器安全的工具。

第十三章,使用 CRD 扩展 Kubernetes,介绍了自定义资源定义(CRD)以及其他向 Kubernetes 添加自定义功能的方法,如操作员。

第十四章,服务网格和无服务器,回顾了 Kubernetes 上的一些高级模式,教您如何向集群添加服务网格并启用无服务器工作负载。

第十五章,Kubernetes 上的有状态工作负载,详细介绍了在 Kubernetes 上运行有状态工作负载的具体内容,包括运行生态系统中一些强大的有状态应用程序的教程。

充分利用本书

由于 Kubernetes 基于容器,本书中的一些示例可能使用自出版以来发生了变化的容器。其他说明性示例可能使用在 Docker Hub 中不存在的容器。这些示例应作为运行您自己的应用程序容器的基础。

在某些情况下,像 Kubernetes 这样的开源软件可能会有重大变化。本书与 Kubernetes 1.19 保持最新,但始终检查文档(对于 Kubernetes 和本书涵盖的任何其他开源项目)以获取最新信息和规格说明。

如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 上的github.com/PacktPublishing/Cloud-Native-with-Kubernetes下载本书的示例代码文件。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781838823078_ColorImages.pdf

使用的约定

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

文本中的代码:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“在我们的情况下,我们希望让集群上的每个经过身份验证的用户创建特权 Pod,因此我们绑定到system:authenticated组。”

代码块设置如下:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: full-restriction-policy
  namespace: development
spec:
  policyTypes:
  - Ingress
  - Egress
  podSelector: {}

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

spec:
  privileged: false
  allowPrivilegeEscalation: false
  volumes:
 - 'configMap'
 - 'emptyDir'
 - 'projected'
 - 'secret'
 - 'downwardAPI'
 - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false

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

helm install falco falcosecurity/falco

粗体:表示一个新术语,一个重要的词,或者屏幕上看到的词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“Prometheus 还提供了一个用于配置 Prometheus 警报的警报选项卡。”

提示或重要说明

像这样出现。

第一部分:设置 Kubernetes

在本节中,您将了解 Kubernetes 的用途,其架构以及与之通信和创建简单集群的基础知识,以及如何运行基本工作负载。

本书的这一部分包括以下章节:

  • 第一章, 与 Kubernetes 通信

  • 第二章, 设置您的 Kubernetes 集群

  • 第三章, 在 Kubernetes 上运行应用容器

第一章:与 Kubernetes 通信

本章包含容器编排的解释,包括其优势、用例和流行的实现。我们还将简要回顾 Kubernetes,包括架构组件的布局,以及对授权、身份验证和与 Kubernetes 的一般通信的入门。到本章结束时,您将知道如何对 Kubernetes API 进行身份验证和通信。

在本章中,我们将涵盖以下主题:

  • 容器编排入门

  • Kubernetes 的架构

  • 在 Kubernetes 上的身份验证和授权

  • 使用 kubectl 和 YAML 文件

技术要求

为了运行本章详细介绍的命令,您需要一台运行 Linux、macOS 或 Windows 的计算机。本章将教您如何安装kubectl命令行工具,您将在以后的所有章节中使用它。

本章中使用的代码可以在书的 GitHub 存储库中找到,链接如下:

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter1

介绍容器编排

谈论 Kubernetes 时,不能不介绍其目的。Kubernetes 是一个容器编排框架,让我们在本书的背景下回顾一下这意味着什么。

什么是容器编排?

容器编排是在云端和数据中心运行现代应用程序的流行模式。通过使用容器-预配置的应用程序单元和捆绑的依赖项-作为基础,开发人员可以并行运行许多应用程序实例。

容器编排的好处

容器编排提供了许多好处,但我们将重点介绍主要的好处。首先,它允许开发人员轻松构建高可用性应用程序。通过运行多个应用程序实例,容器编排系统可以配置成自动替换任何失败的应用程序实例为新的实例。

这可以通过在物理数据中心中分散应用程序的多个实例来扩展到云端,因此如果一个数据中心崩溃,应用程序的其他实例将保持运行,并防止停机。

其次,容器编排允许高度可扩展的应用程序。由于可以轻松创建和销毁应用程序的新实例,编排工具可以自动扩展以满足需求。在云环境或数据中心环境中,可以向编排工具添加新的虚拟机VMs)或物理机,以提供更大的计算资源池。在云环境中,这个过程可以完全自动化,实现完全无需人工干预的扩展,无论是在微观还是宏观层面。

流行的编排工具

生态系统中有几种非常流行的容器编排工具:

  • Docker Swarm:Docker Swarm 是由 Docker 容器引擎团队创建的。与 Kubernetes 相比,它更容易设置和运行,但相对灵活性较差。

  • Apache Mesos:Apache Mesos 是一个较低级别的编排工具,可以管理数据中心和云环境中的计算、内存和存储。默认情况下,Mesos 不管理容器,但是 Marathon - 一个在 Mesos 之上运行的框架 - 是一个完全成熟的容器编排工具。甚至可以在 Mesos 之上运行 Kubernetes。

  • Kubernetes:截至 2020 年,容器编排工作大部分集中在 Kubernetes(koo-bur-net-ees)周围,通常缩写为 k8s。Kubernetes 是一个开源容器编排工具,最初由谷歌创建,借鉴了谷歌多年来在内部编排工具 Borg 和 Omega 的经验。自 Kubernetes 成为开源项目以来,它已经成为企业环境中运行和编排容器的事实标准。其中一些原因包括 Kubernetes 是一个成熟的产品,拥有一个非常庞大的开源社区。它比 Mesos 更容易操作,比 Docker Swarm 更灵活。

从这个比较中最重要的一点是,尽管容器编排有多个相关选项,而且在某些方面确实更好,但 Kubernetes 已经成为事实标准。有了这个认识,让我们来看看 Kubernetes 是如何工作的。

Kubernetes 的架构

Kubernetes 是一个可以在云虚拟机上运行的编排工具,也可以在数据中心的虚拟机或裸机服务器上运行。一般来说,Kubernetes 在一组节点上运行,每个节点可以是虚拟机或物理机。

Kubernetes 节点类型

Kubernetes 节点可以是许多不同的东西-从虚拟机到裸金属主机再到树莓派。Kubernetes 节点分为两个不同的类别:首先是主节点,运行 Kubernetes 控制平面应用程序;其次是工作节点,运行您部署到 Kubernetes 上的应用程序。

一般来说,为了实现高可用性,Kubernetes 的生产部署应该至少有三个主节点和三个工作节点,尽管大多数大型部署的工作节点比主节点多得多。

Kubernetes 控制平面

Kubernetes 控制平面是一套运行在主节点上的应用程序和服务。有几个高度专业化的服务在发挥作用,构成了 Kubernetes 功能的核心。它们如下:

  • kube-apiserver:这是 Kubernetes API 服务器。该应用程序处理发送到 Kubernetes 的指令。

  • kube-scheduler:这是 Kubernetes 调度程序。该组件处理决定将工作负载放置在哪些节点上的工作,这可能变得非常复杂。

  • kube-controller-manager:这是 Kubernetes 控制器管理器。该组件提供了一个高级控制循环,确保集群的期望配置和运行在其上的应用程序得到实施。

  • etcd:这是一个包含集群配置的分布式键值存储。

一般来说,所有这些组件都采用系统服务的形式,在每个主节点上运行。如果您想完全手动引导集群,可以手动启动它们,但是通过使用集群创建库或云提供商管理的服务,例如弹性 Kubernetes 服务(EKS),在生产环境中通常会自动完成这些操作。

Kubernetes API 服务器

Kubernetes API 服务器是一个接受 HTTPS 请求的组件,通常在端口443上。它提供证书,可以是自签名的,以及身份验证和授权机制,我们将在本章后面介绍。

当对 Kubernetes API 服务器进行配置请求时,它将检查etcd中的当前集群配置,并在必要时进行更改。

Kubernetes API 通常是一个 RESTful API,每个 Kubernetes 资源类型都有端点,以及在查询路径中传递的 API 版本;例如,/api/v1

为了扩展 Kubernetes(参见[第十三章](B14790_13_Final_PG_ePub.xhtml#_idTextAnchor289),使用 CRD 扩展 Kubernetes),API 还具有一组基于 API 组的动态端点,可以向自定义资源公开相同的 RESTful API 功能。

Kubernetes 调度程序

Kubernetes 调度程序决定工作负载的实例应该在哪里运行。默认情况下,此决定受工作负载资源要求和节点状态的影响。您还可以通过 Kubernetes 中可配置的放置控件来影响调度程序(参见[第八章](B14790_08_Final_PG_ePub.xhtml#_idTextAnchor186),Pod 放置控件)。这些控件可以作用于节点标签,其他 Pod 已经在节点上运行的情况,以及许多其他可能性。

Kubernetes 控制器管理器

Kubernetes 控制器管理器是运行多个控制器的组件。控制器运行控制循环,确保集群的实际状态与配置中存储的状态匹配。默认情况下,这些包括以下内容:

  • 节点控制器,确保节点正常运行

  • 复制控制器,确保每个工作负载被适当地扩展

  • 端点控制器,处理每个工作负载的通信和路由配置(参见[第五章](B14790_05_Final_PG_ePub.xhtml#_idTextAnchor127),服务和入口 - 与外部世界通信

  • 服务帐户和令牌控制器,处理 API 访问令牌和默认帐户的创建

etcd

etcd 是一个分布式键值存储,以高可用的方式存储集群的配置。每个主节点上都运行一个etcd副本,并使用 Raft 一致性算法,确保在允许对键或值进行任何更改之前保持法定人数。

Kubernetes 工作节点

每个 Kubernetes 工作节点都包含允许其与控制平面通信和处理网络的组件。

首先是kubelet,它确保容器根据集群配置在节点上运行。其次,kube-proxy为在每个节点上运行的工作负载提供网络代理层。最后,容器运行时用于在每个节点上运行工作负载。

kubelet

kubelet 是在每个节点上运行的代理程序(包括主节点,尽管在该上下文中它具有不同的配置)。它的主要目的是接收 PodSpecs 的列表(稍后会详细介绍),并确保它们所规定的容器在节点上运行。kubelet 通过几种不同的可能机制获取这些 PodSpecs,但主要方式是通过查询 Kubernetes API 服务器。另外,kubelet 可以通过文件路径启动,它将监视 PodSpecs 的列表,监视 HTTP 端点,或者在其自己的 HTTP 端点上接收请求。

kube-proxy

kube-proxy 是在每个节点上运行的网络代理。它的主要目的是对其节点上运行的工作负载进行 TCP、UDP 和 SCTP 转发(通过流或轮询)。kube-proxy 支持 Kubernetes 的Service构造,我们将在第五章**,服务和入口 - 与外部世界通信中讨论。

容器运行时

容器运行时在每个节点上运行,它实际上运行您的工作负载。Kubernetes 支持 CRI-O、Docker、containerd、rktlet 和任何有效的容器运行时接口CRI)运行时。从 Kubernetes v1.14 开始,RuntimeClass 功能已从 alpha 版移至 beta 版,并允许特定于工作负载的运行时选择。

插件

除了核心集群组件外,典型的 Kubernetes 安装包括插件,这些是提供集群功能的附加组件。

例如,容器网络接口CNI)插件,如CalicoFlannelWeave,提供符合 Kubernetes 网络要求的覆盖网络功能。

另一方面,CoreDNS 是一个流行的插件,用于集群内的 DNS 和服务发现。还有一些工具,比如 Kubernetes Dashboard,它提供了一个 GUI,用于查看和与您的集群进行交互。

到目前为止,您应该对 Kubernetes 的主要组件有一个高层次的了解。接下来,我们将回顾用户如何与 Kubernetes 交互以控制这些组件。

Kubernetes 上的身份验证和授权

命名空间是 Kubernetes 中一个非常重要的概念,因为它们可以影响 API 访问以及授权,我们现在将介绍它们。

命名空间

Kubernetes 中的命名空间是一种构造,允许您在集群中对 Kubernetes 资源进行分组。它们是一种分离的方法,有许多可能的用途。例如,您可以在集群中为每个环境(开发、暂存和生产)创建一个命名空间。

默认情况下,Kubernetes 将创建默认命名空间、kube-system命名空间和kube-public命名空间。在未指定命名空间的情况下创建的资源将在默认命名空间中创建。kube-system包含集群服务,如etcd、调度程序以及 Kubernetes 本身创建的任何资源,而不是用户创建的资源。kube-public默认情况下可被所有用户读取,并且可用于公共资源。

用户

Kubernetes 中有两种类型的用户 - 常规用户和服务帐户。

通常由集群外的服务管理常规用户,无论是私钥、用户名和密码,还是某种用户存储形式。但是,服务帐户由 Kubernetes 管理,并且受限于特定的命名空间。要创建服务帐户,Kubernetes API 可能会自动创建一个,或者可以通过调用 Kubernetes API 手动创建。

Kubernetes API 有三种可能的请求类型 - 与常规用户关联的请求,与服务帐户关联的请求和匿名请求。

认证方法

为了对请求进行身份验证,Kubernetes 提供了几种不同的选项:HTTP 基本身份验证、客户端证书、bearer 令牌和基于代理的身份验证。

要使用 HTTP 身份验证,请求者发送带有Authorization头的请求,其值为 bearer "token value"

为了指定哪些令牌是有效的,可以在 API 服务器应用程序启动时使用--token-auth-file=filename参数提供一个 CSV 文件。一个新的测试功能(截至本书撰写时),称为引导令牌,允许在 API 服务器运行时动态交换和更改令牌,而无需重新启动它。

还可以通过Authorization令牌进行基本的用户名/密码身份验证,方法是使用头部值Basic base64encoded(username:password)

Kubernetes 的 TLS 和安全证书基础设施

为了使用客户端证书(X.509 证书),API 服务器必须使用--client-ca-file=filename参数启动。该文件需要包含一个或多个用于验证通过 API 请求传递的证书的证书颁发机构CAs)。

除了CA之外,必须为每个用户创建一个证书签名请求CSR)。在这一点上,可以包括用户groups,我们将在授权选项部分讨论。

例如,您可以使用以下内容:

openssl req -new -key myuser.pem -out myusercsr.pem -subj "/CN=myuser/0=dev/0=staging"

这将为名为myuser的用户创建一个 CSR,该用户属于名为devstaging的组。

创建 CA 和 CSR 后,可以使用openssleasyrsacfssl或任何证书生成工具创建实际的客户端和服务器证书。此时还可以创建用于 Kubernetes API 的 TLS 证书。

由于我们的目标是尽快让您开始在 Kubernetes 上运行工作负载,我们将不在本书中涉及各种可能的证书配置 - 但 Kubernetes 文档和文章* Kubernetes The Hard Way*都有一些关于从头开始设置集群的很棒的教程。在大多数生产环境中,您不会手动执行这些步骤。

授权选项

Kubernetes 提供了几种授权方法:节点、webhooks、RBAC 和 ABAC。在本书中,我们将重点关注 RBAC 和 ABAC,因为它们是用户授权中最常用的方法。如果您通过其他服务和/或自定义功能扩展了集群,则其他授权模式可能变得更加重要。

RBAC

RBAC代表基于角色的访问控制,是一种常见的授权模式。在 Kubernetes 中,RBAC 的角色和用户使用四个 Kubernetes 资源来实现:RoleClusterRoleRoleBindingClusterRoleBinding。要启用 RBAC 模式,API 服务器可以使用--authorization-mode=RBAC参数启动。

RoleClusterRole资源指定了一组权限,但不会将这些权限分配给任何特定的用户。权限使用resourcesverbs来指定。以下是一个指定Role的示例 YAML 文件。不要太担心 YAML 文件的前几行 - 我们很快就会涉及到这些内容。专注于resourcesverbs行,以了解如何将操作应用于资源:

只读角色.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: read-only-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]

RoleClusterRole 之间唯一的区别是,Role 限定于特定的命名空间(在本例中是默认命名空间),而 ClusterRole 可以影响集群中该类型的所有资源的访问,以及集群范围的资源,如节点。

RoleBindingClusterRoleBinding 是将 RoleClusterRole 与用户或用户列表关联的资源。以下文件表示一个 RoleBinding 资源,将我们的 read-only-role 与用户 readonlyuser 连接起来:

只读-rb.yaml

apiVersion: rbac.authorization.k8s.io/v1namespace.
kind: RoleBinding
metadata:
  name: read-only
  namespace: default
subjects:
- kind: User
  name: readonlyuser
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: read-only-role
  apiGroup: rbac.authorization.k8s.io

subjects 键包含要将角色与的所有实体的列表;在本例中是用户 alexroleRef 包含要关联的角色的名称和类型(RoleClusterRole)。

ABAC

ABAC 代表 基于属性的访问控制。ABAC 使用 策略 而不是角色。API 服务器在 ABAC 模式下启动,使用一个称为授权策略文件的文件,其中包含一个名为策略对象的 JSON 对象列表。要启用 ABAC 模式,API 服务器可以使用 --authorization-mode=ABAC--authorization-policy-file=filename 参数启动。

在策略文件中,每个策略对象包含有关单个策略的信息:首先,它对应的主体,可以是用户或组,其次,可以通过策略访问哪些资源。此外,可以包括一个布尔值 readonly,以限制策略仅限于 listgetwatch 操作。

与资源关联的第二种类型的策略与非资源请求类型相关联,例如对 /version 端点的调用。

当在 ABAC 模式下对 API 发出请求时,API 服务器将检查用户及其所属的任何组是否与策略文件中的列表匹配,并查看是否有任何策略与用户正在尝试访问的资源或端点匹配。匹配时,API 服务器将授权请求。

现在您应该对 Kubernetes API 如何处理身份验证和授权有了很好的理解。好消息是,虽然您可以直接访问 API,但 Kubernetes 提供了一个出色的命令行工具,可以简单地进行身份验证并发出 Kubernetes API 请求。

使用 kubectl 和 YAML

kubectl 是官方支持的命令行工具,用于访问 Kubernetes API。它可以安装在 Linux、macOS 或 Windows 上。

设置 kubectl 和 kubeconfig

要安装最新版本的 kubectl,可以使用kubernetes.io/docs/tasks/tools/install-kubectl/上的安装说明。

安装了 kubectl 之后,需要设置身份验证以与一个或多个集群进行身份验证。这是使用kubeconfig文件完成的,其外观如下:

示例-kubeconfig

apiVersion: v1
kind: Config
preferences: {}
clusters:
- cluster:
    certificate-authority: fake-ca-file
    server: https://1.2.3.4
  name: development
users:
- name: alex
  user:
    password: mypass
    username: alex
contexts:
- context:
    cluster: development
    namespace: frontend
    user: developer
  name: development

该文件以 YAML 编写,与我们即将介绍的其他 Kubernetes 资源规范非常相似 - 只是该文件仅驻留在您的本地计算机上。

Kubeconfig YAML 文件有三个部分:clustersuserscontexts

  • clusters部分是您可以通过 kubectl 访问的集群列表,包括 CA 文件名和服务器 API 端点。

  • users部分列出了您可以授权的用户,包括用于身份验证的任何用户证书或用户名/密码组合。

  • 最后,contexts部分列出了集群、命名空间和用户的组合,这些组合形成一个上下文。使用kubectl config use-context命令,您可以轻松地在上下文之间切换,从而实现集群、用户和命名空间组合的轻松切换。

命令式与声明式命令

与 Kubernetes API 交互有两种范式:命令式和声明式。命令式命令允许您向 Kubernetes“指示要做什么” - 也就是说,“启动两个 Ubuntu 副本”,“将此应用程序扩展到五个副本”等。

另一方面,声明式命令允许您编写一个文件,其中包含应在集群上运行的规范,并且 Kubernetes API 确保配置与集群配置匹配,并在必要时进行更新。

尽管命令式命令允许您快速开始使用 Kubernetes,但最好在运行生产工作负载或任何复杂工作负载时编写一些 YAML 并使用声明性配置。原因是这样做可以更容易地跟踪更改,例如通过 GitHub 存储库,或者向您的集群引入基于 Git 的持续集成/持续交付(CI/CD)。

一些基本的 kubectl 命令

kubectl 提供了许多方便的命令来检查集群的当前状态,查询资源并创建新资源。 kubectl 的结构使大多数命令可以以相同的方式访问资源。

首先,让我们学习如何查看集群中的 Kubernetes 资源。您可以使用kubectl get resource_type来执行此操作,其中resource_type是 Kubernetes 资源的完整名称,或者是一个更短的别名。别名(和kubectl命令)的完整列表可以在 kubectl 文档中找到:kubernetes.io/docs/reference/kubectl/overview

我们已经了解了节点,所以让我们从那里开始。要查找集群中存在哪些节点,我们可以使用kubectl get nodes或别名kubectl get no

kubectl 的get命令返回当前集群中的 Kubernetes 资源列表。我们可以使用任何 Kubernetes 资源类型运行此命令。要向列表添加附加信息,可以添加wide输出标志:kubectl get nodes -o wide

列出资源是不够的,当然 - 我们需要能够查看特定资源的详细信息。为此,我们使用describe命令,它的工作方式类似于get,只是我们可以选择传递特定资源的名称。如果省略了最后一个参数,Kubernetes 将返回该类型所有资源的详细信息,这可能会导致终端中大量的滚动。

例如,kubectl describe nodes将返回集群中所有节点的详细信息,而kubectl describe nodes node1将返回名为node1的节点的描述。

您可能已经注意到,这些命令都是命令式风格的,这是有道理的,因为我们只是获取有关现有资源的信息,而不是创建新资源。要创建 Kubernetes 资源,我们可以使用以下命令:

  • kubectl create -f /path/to/file.yaml,这是一个命令式命令

  • kubectl apply -f /path/to/file.yaml,这是声明式的

这两个命令都需要一个文件路径,可以是 YAML 或 JSON 格式,或者您也可以使用 stdin。您还可以传递文件夹的路径,而不是文件的路径,这将创建或应用该文件夹中的所有 YAML 或 JSON 文件。create 是命令式的,因此它将创建一个新的资源,但如果您再次运行它并使用相同的文件,命令将失败,因为资源已经存在。apply 是声明性的,因此如果您第一次运行它,它将创建资源,而后续运行将使用任何更改更新 Kubernetes 中正在运行的资源。您可以使用 --dry-run 标志来查看 createapply 命令的输出(即将创建的资源,或者如果存在错误的话)。

要以命令式方式更新现有资源,可以使用 edit 命令,如:kubectl edit resource_type resource_name – 就像我们的 describe 命令一样。这将打开默认的终端编辑器,并显示现有资源的 YAML,无论您是以命令式还是声明式方式创建的。您可以编辑并保存,这将触发 Kubernetes 中资源的自动更新。

要以声明性方式更新现有资源,可以编辑您用于首次创建资源的本地 YAML 资源文件,然后运行 kubectl apply -f /path/to/file.yaml。最好通过命令式命令 kubectl delete resource_type resource_name 来删除资源。

我们将在本节讨论的最后一个命令是 kubectl cluster-info,它将显示主要 Kubernetes 集群服务运行的 IP 地址。

编写 Kubernetes 资源 YAML 文件

用于与 Kubernetes API 声明性通信的格式包括 YAML 和 JSON。为了本书的目的,我们将坚持使用 YAML,因为它更清晰,占用页面空间更少。典型的 Kubernetes 资源 YAML 文件如下:

resource.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: ubuntu
    image: ubuntu:trusty
    command: ["echo"]
    args: ["Hello Readers"]

有效的 Kubernetes YAML 文件至少有四个顶级键。它们是 apiVersionkindmetadataspec

apiVersion决定将使用哪个版本的 Kubernetes API 来创建资源。kind指定 YAML 文件引用的资源类型。metadata提供了一个位置来命名资源,以及添加注释和命名空间信息(稍后会详细介绍)。最后,spec键将包含 Kubernetes 创建资源所需的所有特定于资源的信息。

不要担心kindspec,我们将在第三章中介绍Pod是什么,在 Kubernetes 上运行应用容器

总结

在本章中,我们学习了容器编排背后的背景,Kubernetes 集群的架构概述,集群如何对 API 调用进行身份验证和授权,以及如何使用 kubectl 以命令和声明模式与 API 进行通信,kubectl 是 Kubernetes 的官方支持的命令行工具。

在下一章中,我们将学习几种启动测试集群的方法,并掌握到目前为止学到的 kubectl 命令。

问题

  1. 什么是容器编排?

  2. Kubernetes 控制平面的组成部分是什么,它们的作用是什么?

  3. 如何启动处于 ABAC 授权模式的 Kubernetes API 服务器?

  4. 为什么对于生产 Kubernetes 集群来说拥有多个主节点很重要?

  5. kubectl applykubectl create之间有什么区别?

  6. 如何使用kubectl在上下文之间切换?

  7. 以声明方式创建 Kubernetes 资源然后以命令方式进行编辑的缺点是什么?

进一步阅读

第二章:设置您的 Kubernetes 集群

本章包含了创建 Kubernetes 集群的一些可能性的审查,这将使我们能够学习本书中其余概念所需的知识。我们将从 minikube 开始,这是一个创建简单本地集群的工具,然后涉及一些其他更高级(且适用于生产)的工具,并审查来自公共云提供商的主要托管 Kubernetes 服务,最后介绍从头开始创建集群的策略。

在本章中,我们将涵盖以下主题:

  • 创建您的第一个集群的选项

  • minikube – 一个简单的开始方式

  • 托管服务 – EKS、GKE、AKS 等

  • Kubeadm – 简单的一致性

  • Kops – 基础设施引导

  • Kubespray – 基于 Ansible 的集群创建

  • 完全从头开始创建集群

技术要求

为了在本章中运行命令,您需要安装 kubectl 工具。安装说明可在第一章与 Kubernetes 通信中找到。

如果您确实要使用本章中的任何方法创建集群,您需要查看相关项目文档中每种方法的具体技术要求。对于 minikube,大多数运行 Linux、macOS 或 Windows 的计算机都可以工作。对于大型集群,请查阅您计划使用的工具的具体文档。

本章中使用的代码可以在书籍的 GitHub 存储库中找到,链接如下:

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter2

创建集群的选项

有许多方法可以创建 Kubernetes 集群,从简单的本地工具到完全从头开始创建集群。

如果您刚开始学习 Kubernetes,可能希望使用 minikube 等工具快速启动一个简单的本地集群。

如果您希望为应用程序构建生产集群,您有几个选项:

  • 您可以使用 Kops、Kubespray 或 Kubeadm 等工具以编程方式创建集群。

  • 您可以使用托管的 Kubernetes 服务。

  • 您可以在虚拟机或物理硬件上完全从头开始创建集群。

除非您在集群配置方面有极其特定的需求(即使是这样),通常不建议完全不使用引导工具从头开始创建您的集群。

对于大多数用例,决策将在使用云提供商上的托管 Kubernetes 服务和使用引导工具之间进行。

在空气隔离系统中,使用引导工具是唯一的选择,但对于特定的用例,有些引导工具比其他引导工具更好。特别是,Kops 旨在使在云提供商(如 AWS)上创建和管理集群变得更容易。

重要提示

本节未包括讨论替代的第三方托管服务或集群创建和管理工具,如 Rancher 或 OpenShift。在选择在生产环境中运行集群时,重要的是要考虑包括当前基础设施、业务需求等在内的各种因素。为了简化问题,在本书中,我们将专注于生产集群,假设没有其他基础设施或超特定的业务需求——可以说是一个“白板”。

minikube-开始的简单方法

minikube 是开始使用简单本地集群的最简单方法。这个集群不会设置为高可用性,并且不针对生产使用,但这是一个在几分钟内开始在 Kubernetes 上运行工作负载的好方法。

安装 minikube

minikube 可以安装在 Windows、macOS 和 Linux 上。接下来是三个平台的安装说明,您也可以通过导航到minikube.sigs.k8s.io/docs/start找到。

在 Windows 上安装

在 Windows 上最简单的安装方法是从storage.googleapis.com/minikube/releases/latest/minikube-installer.exe下载并运行 minikube 安装程序。

在 macOS 上安装

使用以下命令下载和安装二进制文件。您也可以在代码存储库中找到它:

Minikube-install-mac.sh

     curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 \
&& sudo install minikube-darwin-amd64 /usr/local/bin/minikube

在 Linux 上安装

使用以下命令下载和安装二进制文件:

Minikube-install-linux.sh

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
&& sudo install minikube-linux-amd64 /usr/local/bin/minikube

在 minikube 上创建一个集群

使用 minikube 创建一个集群,只需运行minikube start,这将使用默认的 VirtualBox VM 驱动程序创建一个简单的本地集群。minikube 还有一些额外的配置选项,可以在文档站点上查看。

运行minikube start命令将自动配置您的kubeconfig文件,这样您就可以在新创建的集群上运行kubectl命令,而无需进行进一步的配置。

托管 Kubernetes 服务

提供托管 Kubernetes 服务的云提供商数量不断增加。然而,对于本书的目的,我们将专注于主要的公共云及其特定的 Kubernetes 服务。这包括以下内容:

  • 亚马逊网络服务(AWS) - 弹性 Kubernetes 服务(EKS)

  • 谷歌云 - 谷歌 Kubernetes 引擎(GKE)

  • 微软 Azure - Azure Kubernetes 服务(AKS)

重要提示

托管 Kubernetes 服务的数量和实施方式总是在变化。AWS、谷歌云和 Azure 被选为本书的这一部分,因为它们很可能会继续以相同的方式运行。无论您使用哪种托管服务,请确保查看服务提供的官方文档,以确保集群创建过程与本书中所呈现的相同。

托管 Kubernetes 服务的好处

一般来说,主要的托管 Kubernetes 服务提供了一些好处。首先,我们正在审查的这三个托管服务提供了完全托管的 Kubernetes 控制平面。

这意味着当您使用这些托管 Kubernetes 服务之一时,您不需要担心主节点。它们被抽象化了,可能根本不存在。这三个托管集群都允许您在创建集群时选择工作节点的数量。

托管集群的另一个好处是从一个 Kubernetes 版本无缝升级到另一个版本。一般来说,一旦验证了托管服务的新版本 Kubernetes(不一定是最新版本),您应该能够使用一个按钮或一个相当简单的过程进行升级。

托管 Kubernetes 服务的缺点

尽管托管 Kubernetes 集群在许多方面可以简化操作,但也存在一些缺点。

对于许多可用的托管 Kubernetes 服务,托管集群的最低成本远远超过手动创建或使用诸如 Kops 之类的工具创建的最小集群的成本。对于生产用例,这通常不是一个问题,因为生产集群应该包含最少数量的节点,但对于开发环境或测试集群,根据预算,额外的成本可能不值得操作的便利。

此外,虽然抽象化主节点使操作更容易,但它也阻止了对已定义主节点的集群可能可用的精细调整或高级主节点功能。

AWS - 弹性 Kubernetes 服务

AWS 的托管 Kubernetes 服务称为 EKS,或弹性 Kubernetes 服务。有几种不同的方式可以开始使用 EKS,但我们将介绍最简单的方式。

入门

要创建一个 EKS 集群,您必须配置适当的虚拟私有云(VPC)身份和访问管理(IAM)角色设置 - 在这一点上,您可以通过控制台创建一个集群。这些设置可以通过控制台手动创建,也可以通过基础设施配置工具如 CloudFormation 和 Terraform 创建。有关通过控制台创建集群的完整说明,请参阅docs.aws.amazon.com/en_pv/eks/latest/userguide/getting-started-console.html

假设您是从头开始创建集群和 VPC,您可以使用一个名为eksctl的工具来配置您的集群。

要安装eksctl,您可以在docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html找到 macOS、Linux 和 Windows 的安装说明。

一旦安装了eksctl,创建一个集群就像使用eksctl create cluster命令一样简单:

Eks-create-cluster.sh

eksctl create cluster \
--name prod \
--version 1.17 \
--nodegroup-name standard-workers \
--node-type t2.small \
--nodes 3 \
--nodes-min 1 \
--nodes-max 4 \
--node-ami auto

这将创建一个由三个t2.small实例组成的集群,这些实例被设置为一个具有一个节点最小和四个节点最大的自动缩放组。使用的 Kubernetes 版本将是1.17。重要的是,eksctl从一个默认区域开始,并根据选择的节点数量,在该区域的多个可用区中分布它们。

eksctl还将自动更新您的kubeconfig文件,因此在集群创建过程完成后,您应该能够立即运行kubectl命令。

使用以下代码测试配置:

kubectl get nodes

您应该看到您的节点及其关联的 IP 列表。您的集群已准备就绪!接下来,让我们看看 Google 的 GKE 设置过程。

Google Cloud – Google Kubernetes Engine

GKE 是 Google Cloud 的托管 Kubernetes 服务。使用 gcloud 命令行工具,可以很容易地快速启动 GKE 集群。

入门

要使用 gcloud 在 GKE 上创建集群,可以使用 Google Cloud 的 Cloud Shell 服务,也可以在本地运行命令。如果要在本地运行命令,必须通过 Google Cloud SDK 安装 gcloud CLI。有关安装说明,请参阅cloud.google.com/sdk/docs/quickstarts

安装了 gcloud 后,您需要确保已在 Google Cloud 帐户中激活了 GKE API。

要轻松实现这一点,请转到console.cloud.google.com/apis/library,然后在搜索栏中搜索kubernetes。单击Kubernetes Engine API,然后单击启用

现在 API 已激活,请使用以下命令在 Google Cloud 中设置您的项目和计算区域:

gcloud config set project proj_id
gcloud config set compute/zone compute_zone

在命令中,proj_id对应于您想要在 Google Cloud 中创建集群的项目 ID,compute_zone对应于您在 Google Cloud 中期望的计算区域。

实际上,GKE 上有三种类型的集群,每种类型具有不同(增加)的可靠性和容错能力:

  • 单区集群

  • 多区集群

  • 区域集群

GKE 中的单区集群意味着具有单个控制平面副本和一个或多个在同一 Google Cloud 区域运行的工作节点的集群。如果区域发生故障,控制平面和工作节点(因此工作负载)都将宕机。

GKE 中的多区集群意味着具有单个控制平面副本和两个或多个在不同的 Google Cloud 区域运行的工作节点的集群。这意味着如果单个区域(甚至包含控制平面的区域)发生故障,集群中运行的工作负载仍将持续存在,但是直到控制平面区域恢复之前,Kubernetes API 将不可用。

最后,在 GKE 中,区域集群 意味着具有多区域控制平面和多区域工作节点的集群。如果任何区域出现故障,控制平面和工作节点上的工作负载将持续存在。这是最昂贵和可靠的选项。

现在,要实际创建您的集群,您可以运行以下命令以使用默认设置创建名为 dev 的集群:

gcloud container clusters create dev \
    --zone [compute_zone]

此命令将在您选择的计算区域创建一个单区域集群。

为了创建一个多区域集群,您可以运行以下命令:

gcloud container clusters create dev \
    --zone [compute_zone_1]
    --node-locations [compute_zone_1],[compute_zone_2],[etc]

在这里,compute_zone_1compute_zone_2 是不同的 Google Cloud 区域。此外,可以通过 node-locations 标志添加更多区域。

最后,要创建一个区域集群,您可以运行以下命令:

gcloud container clusters create dev \
    --region [region] \
    --node-locations [compute_zone_1],[compute_zone_2],[etc]

在这种情况下,node-locations 标志实际上是可选的。如果省略,集群将在该区域内的所有区域中创建工作节点。如果您想更改此默认行为,可以使用 node-locations 标志进行覆盖。

现在您已经运行了一个集群,需要配置您的 kubeconfig 文件以与集群通信。为此,只需将集群名称传递给以下命令:

gcloud container clusters get-credentials [cluster_name]

最后,使用以下命令测试配置:

kubectl get nodes

与 EKS 一样,您应该看到所有已配置节点的列表。成功!最后,让我们来看看 Azure 的托管服务。

Microsoft Azure – Azure Kubernetes 服务

Microsoft Azure 的托管 Kubernetes 服务称为 AKS。可以通过 Azure CLI 在 AKS 上创建集群。

入门

要在 AKS 上创建集群,可以使用 Azure CLI 工具,并运行以下命令以创建服务主体(集群将使用该服务主体访问 Azure 资源的角色):

az ad sp create-for-rbac --skip-assignment --name myClusterPrincipal

此命令的结果将是一个包含有关服务主体信息的 JSON 对象,我们将在下一步中使用。此 JSON 对象如下所示:

{
  "appId": "559513bd-0d99-4c1a-87cd-851a26afgf88",
  "displayName": "myClusterPrincipal",
  "name": "http://myClusterPrincipal",
  "password": "e763725a-5eee-892o-a466-dc88d980f415",
  "tenant": "72f988bf-90jj-41af-91ab-2d7cd011db48"
}

现在,您可以使用上一个 JSON 命令中的值来实际创建您的 AKS 集群:

Aks-create-cluster.sh

az aks create \
    --resource-group devResourceGroup \
    --name myCluster \
    --node-count 2 \
    --service-principal <appId> \
    --client-secret <password> \
    --generate-ssh-keys

此命令假定存在名为 devResourceGroup 的资源组和名为 devCluster 的集群。对于 appIdpassword,请使用服务主体创建步骤中的值。

最后,要在您的计算机上生成正确的 kubectl 配置,您可以运行以下命令:

az aks get-credentials --resource-group devResourceGroup --name myCluster

到这一步,您应该能够正确运行 kubectl 命令。使用 kubectl get nodes 命令测试配置。

程序化集群创建工具

有几种可用的工具可以在各种非托管环境中引导 Kubernetes 集群。我们将重点关注三种最流行的工具:Kubeadm、Kops 和 Kubespray。每种工具都针对不同的用例,并且通常通过不同的方法工作。

Kubeadm

Kubeadm 是由 Kubernetes 社区创建的工具,旨在简化已经配置好的基础架构上的集群创建。与 Kops 不同,Kubeadm 无法在云服务上提供基础架构。它只是创建一个符合 Kubernetes 一致性测试的最佳实践集群。Kubeadm 对基础架构是不可知的-它应该可以在任何可以运行 Linux VM 的地方工作。

Kops

Kops 是一种流行的集群配置工具。它为您的集群提供基础架构,安装所有集群组件,并验证您的集群功能。它还可以用于执行各种集群操作,如升级、节点旋转等。Kops 目前支持 AWS,在撰写本书时,还支持 Google Compute Engine 和 OpenStack 的 beta 版本,以及 VMware vSphere 和 DigitalOcean 的 alpha 版本。

Kubespray

Kubespray 与 Kops 和 Kubeadm 都不同。与 Kops 不同,Kubespray 并不固有地提供集群资源。相反,Kubespray 允许您在 Ansible 和 Vagrant 之间进行选择,以执行配置、编排和节点设置。

与 Kubeadm 相比,Kubespray 集成了更少的集群创建和生命周期流程。Kubespray 的新版本允许您在节点设置后专门使用 Kubeadm 进行集群创建。

重要说明

由于使用 Kubespray 创建集群需要一些特定于 Ansible 的领域知识,我们将不在本书中讨论这个问题-但可以在github.com/kubernetes-sigs/kubespray/blob/master/docs/getting-started.md找到有关 Kubespray 的所有信息的指南。

使用 Kubeadm 创建集群

要使用 Kubeadm 创建集群,您需要提前配置好节点。与任何其他 Kubernetes 集群一样,我们需要运行 Linux 的 VM 或裸金属服务器。

为了本书的目的,我们将展示如何使用单个主节点引导 Kubeadm 集群。对于高可用设置,您需要在其他主节点上运行额外的加入命令,您可以在kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/找到。

安装 Kubeadm

首先,您需要在所有节点上安装 Kubeadm。每个支持的操作系统的安装说明可以在kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm找到。

对于每个节点,还要确保所有必需的端口都是开放的,并且已安装您打算使用的容器运行时。

启动主节点

要快速启动使用 Kubeadm 的主节点,您只需要运行一个命令:

kubeadm init

此初始化命令可以接受几个可选参数 - 根据您的首选集群设置、网络等,您可能需要使用它们。

init命令的输出中,您将看到一个kubeadm join命令。确保保存此命令。

启动工作节点

为了引导工作节点,您需要运行保存的join命令。命令的形式如下:

kubeadm join --token [TOKEN] [IP ON MASTER]:[PORT ON MASTER] --discovery-token-ca-cert-hash sha256:[HASH VALUE]

此命令中的令牌是引导令牌。它用于验证节点之间的身份,并将新节点加入集群。拥有此令牌的访问权限即可加入新节点到集群中,因此请谨慎对待。

设置 kubectl

使用 Kubeadm,kubectl 已经在主节点上正确设置。但是,要从任何其他机器或集群外部使用 kubectl,您可以将主节点上的配置复制到本地机器:

scp root@[IP OF MASTER]:/etc/kubernetes/admin.conf .
kubectl --kubeconfig ./admin.conf get nodes 

这个kubeconfig将是集群管理员配置 - 为了指定其他用户(和权限),您需要添加新的服务账户并为他们生成kubeconfig文件。

使用 Kops 创建集群

由于 Kops 将为您提供基础设施,因此无需预先创建任何节点。您只需要安装 Kops,确保您的云平台凭据有效,并立即创建您的集群。Kops 可以安装在 Linux、macOS 和 Windows 上。

在本教程中,我们将介绍如何在 AWS 上创建一个集群,但您可以在 Kops 文档中找到其他支持的 Kops 平台的说明,网址为github.com/kubernetes/kops/tree/master/docs

在 macOS 上安装

在 OS X 上,安装 Kops 的最简单方法是使用 Homebrew:

brew update && brew install kops

或者,您可以从 Kops GitHub 页面上获取最新的稳定 Kops 二进制文件,网址为github.com/kubernetes/kops/releases/tag/1.12.3

在 Linux 上安装

在 Linux 上,您可以通过以下命令安装 Kops:

Kops-linux-install.sh

curl -LO https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-linux-amd64
chmod +x kops-linux-amd64
sudo mv kops-linux-amd64 /usr/local/bin/kops

在 Windows 上安装

要在 Windows 上安装 Kops,您需要从github.com/kubernetes/kops/releases/latest下载最新的 Windows 版本,将其重命名为kops.exe,并将其添加到您的path变量中。

设置 Kops 的凭据

为了让 Kops 工作,您需要在您的机器上具有一些必需的 IAM 权限的 AWS 凭据。为了安全地执行此操作,您需要为 Kops 专门创建一个 IAM 用户。

首先,为kops用户创建一个 IAM 组:

aws iam create-group --group-name kops_users

然后,为kops_users组附加所需的角色。为了正常运行,Kops 将需要AmazonEC2FullAccessAmazonRoute53FullAccessAmazonS3FullAccessIAMFullAccessAmazonVPCFullAccess。我们可以通过运行以下命令来实现这一点:

提供-aws-policies-to-kops.sh

aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops

最后,创建kops用户,将其添加到kops_users组,并创建程序访问密钥,然后保存:

aws iam create-user --user-name kops
aws iam add-user-to-group --user-name kops --group-name kops_users
aws iam create-access-key --user-name kops

为了让 Kops 访问您的新 IAM 凭据,您可以使用以下命令配置 AWS CLI,使用前一个命令(create-access-key)中的访问密钥和秘钥:

aws configure
export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id)
export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key)

设置状态存储

凭据设置好后,我们可以开始创建我们的集群。在这种情况下,我们将构建一个简单的基于 gossip 的集群,因此我们不需要处理 DNS。要查看可能的 DNS 设置,您可以查看 Kops 文档(github.com/kubernetes/kops/tree/master/docs)。

首先,我们需要一个位置来存储我们的集群规范。由于我们在 AWS 上,S3 非常适合这个任务。

像往常一样,使用 S3 时,存储桶名称需要是唯一的。您可以使用 AWS SDK 轻松创建一个存储桶(确保将my-domain-dev-state-store替换为您想要的 S3 存储桶名称):

aws s3api create-bucket \
    --bucket my-domain-dev-state-store \
    --region us-east-1

启用存储桶加密和版本控制是最佳实践:

aws s3api put-bucket-versioning --bucket prefix-example-com-state-store  --versioning-configuration Status=Enabled
aws s3api put-bucket-encryption --bucket prefix-example-com-state-store --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'

最后,要设置 Kops 的变量,请使用以下命令:

export NAME=devcluster.k8s.local
export KOPS_STATE_STORE=s3://my-domain-dev-cluster-state-store

重要提示

Kops 支持多种状态存储位置,如 AWS S3,Google Cloud Storage,Kubernetes,DigitalOcean,OpenStack Swift,阿里云和 memfs。但是,您可以将 Kops 状态仅保存到本地文件并使用该文件。云端状态存储的好处是多个基础架构开发人员可以访问并使用版本控制进行更新。

创建集群

使用 Kops,我们可以部署任何规模的集群。在本指南的目的是,我们将通过在三个可用区域跨越工作节点和主节点来部署一个生产就绪的集群。我们将使用 US-East-1 地区,主节点和工作节点都将是t2.medium实例。

要为此集群创建配置,可以运行以下kops create命令:

Kops-create-cluster.sh

kops create cluster \
    --node-count 3 \
    --zones us-east-1a,us-east-1b,us-east-1c \
    --master-zones us-east-1a,us-east-1b,us-east-1c \
    --node-size t2.medium \
    --master-size t2.medium \
    ${NAME}

要查看已创建的配置,请使用以下命令:

kops edit cluster ${NAME}

最后,要创建我们的集群,请运行以下命令:

kops update cluster ${NAME} --yes

集群创建过程可能需要一些时间,但一旦完成,您的kubeconfig应该已经正确配置,可以使用 kubectl 与您的新集群进行交互。

完全从头开始创建集群

完全从头开始创建一个 Kubernetes 集群是一个多步骤的工作,可能需要跨越本书的多个章节。然而,由于我们的目的是尽快让您开始使用 Kubernetes,我们将避免描述整个过程。

如果您有兴趣从头开始创建集群,无论是出于教育目的还是需要精细定制您的集群,都可以参考Kubernetes The Hard Way,这是由Kelsey Hightower编写的完整集群创建教程。它可以在github.com/kelseyhightower/kubernetes-the-hard-way找到。

既然我们已经解决了这个问题,我们可以继续概述手动创建集群的过程。

配置您的节点

首先,您需要一些基础设施来运行 Kubernetes。通常,虚拟机是一个很好的选择,尽管 Kubernetes 也可以在裸机上运行。如果您在一个不能轻松添加节点的环境中工作(这会消除云的许多扩展优势,但在企业环境中绝对可行),您需要足够的节点来满足应用程序的需求。这在空隔离环境中更有可能成为一个问题。

一些节点将用于主控制平面,而其他节点将仅用作工作节点。没有必要从内存或 CPU 的角度使主节点和工作节点相同 - 甚至可以有一些较弱的和一些更强大的工作节点。这种模式会导致一个非同质的集群,其中某些节点更适合特定的工作负载。

为 TLS 创建 Kubernetes 证书颁发机构

为了正常运行,所有主要控制平面组件都需要 TLS 证书。为了创建这些证书,需要创建一个证书颁发机构(CA),它将进一步创建 TLS 证书。

要创建 CA,需要引导公钥基础设施(PKI)。对于这个任务,可以使用任何 PKI 工具,但 Kubernetes 文档中使用的是 cfssl。

一旦为所有组件创建了 PKI、CA 和 TLS 证书,下一步是为控制平面和工作节点组件创建配置文件。

创建配置文件

需要为 kubelet、kube-proxy、kube-controller-manager 和 kube-scheduler 组件创建配置文件。它们将使用这些配置文件中的证书与 kube-apiserver 进行身份验证。

创建 etcd 集群并配置加密

通过一个带有数据加密密钥的 YAML 文件来处理数据加密配置。此时,需要启动 etcd 集群。

为此,在每个节点上创建带有 etcd 进程配置的 systemd 文件。然后在每个节点上使用 systemctl 启动 etcd 服务器。

这是一个 etcd 的 systemd 文件示例。其他控制平面组件的 systemd 文件将类似于这个:

示例-systemd-control-plane

[Unit]
Description=etcd
Documentation=https://github.com/coreos
[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\
  --cert-file=/etc/etcd/kubernetes.pem \\
  --key-file=/etc/etcd/kubernetes-key.pem \\
  --peer-cert-file=/etc/etcd/kubernetes.pem \\
  --peer-key-file=/etc/etcd/kubernetes-key.pem \\
  --trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-client-cert-auth \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

该服务文件为我们的 etcd 组件提供了运行时定义,它将在每个主节点上启动。要在我们的节点上实际启动 etcd,我们运行以下命令:

{
  sudo systemctl daemon-reload
  sudo systemctl enable etcd
  sudo systemctl start etcd
}

这使得etcd服务能够在节点重新启动时自动重新启动。

引导控制平面组件

在主节点上引导控制平面组件的过程类似于创建etcd集群所使用的过程。为每个组件创建systemd文件 - API 服务器、控制器管理器和调度器 - 然后使用systemctl命令启动每个组件。

先前创建的配置文件和证书也需要包含在每个主节点上。

让我们来看看我们的kube-apiserver组件的服务文件定义,按照以下各节进行拆分。Unit部分只是我们systemd文件的一个快速描述:

[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

Api-server-systemd-example

第二部分是服务的实际启动命令,以及要传递给服务的任何变量:

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\
  --allow-privileged=true \\
  --apiserver-count=3 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-cluster-ip-range=10.10.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2

最后,Install部分允许我们指定一个WantedBy目标:

Restart=on-failure
RestartSec=5
 [Install]
WantedBy=multi-user.target

kube-schedulerkube-controller-manager的服务文件将与kube-apiserver的定义非常相似,一旦我们准备在节点上启动组件,这个过程就很容易:

{
  sudo systemctl daemon-reload
  sudo systemctl enable kube-apiserver kube-controller-manager kube-scheduler
  sudo systemctl start kube-apiserver kube-controller-manager kube-scheduler
}

etcd类似,我们希望确保服务在节点关闭时重新启动。

引导工作节点

工作节点上也是类似的情况。需要创建并使用systemctl运行kubelet、容器运行时、cnikube-proxy的服务规范。kubelet配置将指定上述 TLS 证书,以便它可以通过 API 服务器与控制平面通信。

让我们看看我们的kubelet服务定义是什么样子的:

Kubelet-systemd-example

[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=containerd.service
Requires=containerd.service
[Service]
ExecStart=/usr/local/bin/kubelet \\
  --config=/var/lib/kubelet/kubelet-config.yaml \\
  --container-runtime=remote \\
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\
  --image-pull-progress-deadline=2m \\
  --kubeconfig=/var/lib/kubelet/kubeconfig \\
  --network-plugin=cni \\
  --register-node=true \\
  --v=2
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

正如你所看到的,这个服务定义引用了cni、容器运行时和kubelet-config文件。kubelet-config文件包含我们工作节点所需的 TLS 信息。

在引导工作节点和主节点之后,集群应该可以通过作为 TLS 设置的一部分创建的管理员kubeconfig文件来使用。

总结

在本章中,我们回顾了创建 Kubernetes 集群的几种方法。我们研究了使用 minikube 在本地创建最小的集群,设置在 Azure、AWS 和 Google Cloud 上管理的 Kubernetes 服务的集群,使用 Kops 配置工具创建集群,最后,从头开始手动创建集群。

现在我们有了在几种不同环境中创建 Kubernetes 集群的技能,我们可以继续使用 Kubernetes 来运行应用程序。

在下一章中,我们将学习如何在 Kubernetes 上开始运行应用程序。您对 Kubernetes 在架构层面的工作原理的了解应该会让您更容易理解接下来几章中的概念。

问题

  1. minikube 有什么作用?

  2. 使用托管 Kubernetes 服务有哪些缺点?

  3. Kops 与 Kubeadm 有何不同?主要区别是什么?

  4. Kops 支持哪些平台?

  5. 在手动创建集群时,如何指定主要集群组件?它们如何在每个节点上运行?

进一步阅读

第三章:在 Kubernetes 上运行应用程序容器

本章包含了 Kubernetes 提供的最小的乐高积木块——Pod 的全面概述。其中包括 PodSpec YAML 格式和可能的配置的解释,以及 Kubernetes 如何处理和调度 Pod 的简要讨论。Pod 是在 Kubernetes 上运行应用程序的最基本方式,并且在所有高阶应用程序控制器中使用。

在本章中,我们将涵盖以下主题:

  • 什么是 Pod?

  • 命名空间

  • Pod 的生命周期

  • Pod 资源规范

  • Pod 调度

技术要求

为了运行本章详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参见第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书的 GitHub 存储库中找到以下链接:

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter3

什么是 Pod?

Pod 是 Kubernetes 中最简单的计算资源。它指定一个或多个容器由 Kubernetes 调度程序在节点上启动和运行。Pod 有许多潜在的配置和扩展,但仍然是在 Kubernetes 上运行应用程序的最基本方式。

重要说明

单独一个 Pod 并不是在 Kubernetes 上运行应用程序的很好的方式。Pod 应该被视为一次性的东西,以便充分利用像 Kubernetes 这样的容器编排器的真正能力。这意味着将容器(因此也是 Pod)视为牲畜,而不是宠物。为了真正利用容器和 Kubernetes,应用程序应该在自愈、可扩展的组中运行。Pod 是这些组的构建块,我们将在后面的章节中讨论如何以这种方式配置应用程序。

实现 Pod

Pod 是使用 Linux 隔离原则(如组和命名空间)实现的,并且通常可以被视为逻辑主机。Pod 运行一个或多个容器(可以基于 Docker、CRI-O 或其他运行时),这些容器可以以与 VM 上的不同进程通信的方式相互通信。

为了使两个不同 Pod 中的容器进行通信,它们需要通过 IP 访问另一个 Pod(和容器)。默认情况下,只有运行在同一个 Pod 上的容器才能使用更低级别的通信方法,尽管可以配置不同的 Pod 以使它们能够通过主机 IPC 相互通信。

Pod 范例

在最基本的层面上,有两种类型的 Pods:

  • 单容器 Pods

  • 多容器 Pods

通常最好的做法是每个 Pod 包含一个单独的容器。这种方法允许您分别扩展应用程序的不同部分,并且在创建一个可以启动和运行而不出现问题的 Pod 时通常会保持简单。

另一方面,多容器 Pods 更复杂,但在各种情况下都可能很有用:

  • 如果您的应用程序有多个部分运行在不同的容器中,但彼此之间紧密耦合,您可以将它们都运行在同一个 Pod 中,以使通信和文件系统访问无缝。

  • 在实施侧车模式时,实用程序容器被注入到主应用程序旁边,用于处理日志记录、度量、网络或高级功能,比如服务网格(更多信息请参阅第十四章服务网格和无服务器)。

下图显示了一个常见的侧车实现:

图 3.1 - 常见的侧边栏实现

图 3.1 - 常见的侧边栏实现

在这个例子中,我们有一个只有两个容器的 Pod:我们的应用容器运行一个 Web 服务器,一个日志应用程序从我们的服务器 Pod 中拉取日志并将其转发到我们的日志基础设施。这是侧车模式非常适用的一个例子,尽管许多日志收集器在节点级别工作,而不是在 Pod 级别,所以这并不是在 Kubernetes 中从我们的应用容器收集日志的通用方式。

Pod 网络

正如我们刚才提到的,Pods 有自己的 IP 地址,可以用于 Pod 间通信。每个 Pod 都有一个 IP 地址和端口,如果有多个容器运行在一个 Pod 中,这些端口是共享的。

在 Pod 内部,正如我们之前提到的,容器可以在不调用封装 Pod 的 IP 的情况下进行通信 - 相反,它们可以简单地使用 localhost。这是因为 Pod 内的容器共享网络命名空间 - 本质上,它们通过相同的bridge进行通信,这是使用虚拟网络接口实现的。

Pod 存储

Kubernetes 中的存储是一个独立的大主题,我们将在第七章中深入讨论它 - 但现在,您可以将 Pod 存储视为附加到 Pod 的持久或非持久卷。非持久卷可以被 Pod 用于存储数据或文件,具体取决于类型,但它们在 Pod 关闭时会被删除。持久类型的卷将在 Pod 关闭后保留,并且甚至可以用于在多个 Pod 或应用程序之间共享数据。

在我们继续讨论 Pod 之前,我们将花一点时间讨论命名空间。由于我们在处理 Pod 时将使用kubectl命令,了解命名空间如何与 Kubernetes 和kubectl相关联非常重要,因为这可能是一个重要的“坑”。

命名空间

第一章与 Kubernetes 通信部分,我们简要讨论了命名空间,但在这里我们将重申并扩展它们的目的。命名空间是一种在集群中逻辑上分隔不同区域的方式。一个常见的用例是每个环境一个命名空间 - 一个用于开发,一个用于暂存,一个用于生产 - 所有这些都存在于同一个集群中。

正如我们在授权部分中提到的,可以按命名空间指定用户权限 - 例如,允许用户向dev命名空间部署新应用程序和资源,但不允许向生产环境部署。

在运行的集群中,您可以通过运行kubectl get namespaceskubectl get ns来查看存在哪些命名空间,这应该会产生以下输出:

NAME          STATUS    AGE
default       Active    1d
kube-system   Active    1d
kube-public   Active    1d

通过以下命令可以创建一个命名空间:kubectl create namespace staging,或者使用以下 YAML 资源规范运行kubectl apply -f /path/to/file.yaml

Staging-ns.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: staging

如您所见,Namespace规范非常简单。让我们继续讨论更复杂的内容 - PodSpec 本身。

Pod 生命周期

要快速查看集群中正在运行的 Pods,您可以运行kubectl get podskubectl get pods --all-namespaces来分别获取当前命名空间中的 Pods(由您的kubectl上下文定义,如果未指定,则为默认命名空间)或所有命名空间中的 Pods。

kubectl get pods的输出如下:

NAME     READY   STATUS    RESTARTS   AGE
my-pod   1/1     Running   0          9s

正如您所看到的,Pod 具有一个STATUS值,告诉我们 Pod 当前处于哪种状态。

Pod 状态的值如下:

  • 运行:在运行状态下,Pod 已成功启动其容器,没有任何问题。如果 Pod 只有一个容器,并且处于运行状态,那么容器尚未完成或退出其进程。它也可能正在重新启动,您可以通过检查READY列来判断。例如,如果READY值为0/1,这意味着 Pod 中的容器当前未通过健康检查。这可能是由于各种原因:容器可能仍在启动,数据库连接可能无法正常工作,或者一些重要配置可能会阻止应用程序进程启动。

  • 成功:如果您的 Pod 容器设置为运行可以完成或退出的命令(不是长时间运行的命令,例如启动 Web 服务器),则如果这些容器已完成其进程命令,Pod 将显示成功状态。

  • 挂起挂起状态表示 Pod 中至少有一个容器正在等待其镜像。这可能是因为容器镜像仍在从外部存储库获取,或者因为 Pod 本身正在等待被kube-scheduler调度。

  • 未知:未知状态表示 Kubernetes 无法确定 Pod 实际处于什么状态。这通常意味着 Pod 所在的节点遇到某种错误。可能是磁盘空间不足,与集群的其余部分断开连接,或者遇到其他问题。

  • 失败:在失败状态下,Pod 中的一个或多个容器以失败状态终止。此外,Pod 中的其他容器必须以成功或失败的方式终止。这可能是由于集群删除 Pods 或容器应用程序内部的某些东西破坏了进程而发生的各种原因。

理解 Pod 资源规范

由于 Pod 资源规范是我们真正深入研究的第一个资源规范,我们将花时间详细介绍 YAML 文件的各个部分以及它们如何配合。

让我们从一个完全规范的 Pod 文件开始,然后我们可以分解和审查它:

Simple-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
  namespace: dev
  labels:
    environment: dev
  annotations:
    customid1: 998123hjhsad 
spec:
  containers:
  - name: my-app-container
    image: busybox

这个 Pod YAML 文件比我们在第一章中看到的要复杂一些。它公开了一些新的 Pod 功能,我们将很快进行审查。

API 版本

让我们从第 1 行开始:apiVersion。正如我们在第一章中提到的,与 Kubernetes 通信apiVersion 告诉 Kubernetes 在创建和配置资源时应查看哪个 API 版本。Pod 在 Kubernetes 中已经存在很长时间,因此 PodSpec 已经固定为 API 版本v1。其他资源类型可能除了版本名称外还包含组名 - 例如,在 Kubernetes 中,CronJob 资源使用batch/v1beta1 apiVersion,而 Job 资源使用batch/v1 apiVersion。在这两种情况下,batch 对应于 API 组名。

Kind

kind 值对应于 Kubernetes 中资源类型的实际名称。在这种情况下,我们正在尝试规范一个 Pod,所以这就是我们放置的内容。kind 值始终采用驼峰命名法,例如 PodConfigMapCronJob 等。

重要说明

要获取完整的kind值列表,请查看官方 Kubernetes 文档kubernetes.io/docs/home/。新的 Kubernetes kind 值会在新版本中添加,因此本书中审查的内容可能不是详尽的列表。

元数据

元数据是一个顶级键,可以在其下具有几个不同的值。首先,name 是资源名称,这是资源通过kubectl显示的名称,也是在etcd中存储的名称。namespace 对应于资源应该被创建在的命名空间。如果在 YAML 规范中未指定命名空间,则资源将被创建在default命名空间中 - 除非在applycreate命令中指定了命名空间。

接下来,labels 是用于向资源添加元数据的键值对。labels 与其他元数据相比是特殊的,因为它们默认用于 Kubernetes 本机selectors中,以过滤和选择资源 - 但它们也可以用于自定义功能。

最后,metadata块可以承载多个annotations,就像labels一样,可以被控制器和自定义 Kubernetes 功能用来提供额外的配置和特定功能的数据。在这个 PodSpec 中,我们在元数据中指定了几个注释:

pod-with-annotations.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
  namespace: dev
  labels:
    environment: dev
  annotations:
    customid1: 998123hjhsad
    customid2: 1239808908sd 
spec:
  containers:
  - name: my-app-container
    image: busybox

通常,最好使用labels来进行 Kubernetes 特定功能和选择器的配置,同时使用annotations来添加数据或扩展功能 - 这只是一种惯例。

规范

spec是包含特定于资源的配置的顶级键。在这种情况下,由于我们的kind值是Pod,我们将添加一些特定于我们的 Pod 的配置。所有进一步的键将缩进在这个spec键下,并将代表我们的 Pod 配置。

容器

containers键期望一个或多个容器的列表,这些容器将在一个 Pod 中运行。每个容器规范将公开其自己的配置值,这些配置值缩进在资源 YAML 中的容器列表项下。我们将在这里审查一些这些配置,但是要获取完整列表,请查看 Kubernetes 文档(kubernetes.io/docs/home/)。

名称

在容器规范中,name指的是容器在 Pod 中的名称。容器名称可以用于使用kubectl logs命令特别访问特定容器的日志,但这部分我们以后再说。现在,请确保为 Pod 中的每个容器选择一个清晰的名称,以便在调试时更容易处理事情。

图像

对于每个容器,image用于指定应在 Pod 中启动的 Docker(或其他运行时)镜像的名称。默认情况下,图像将从配置的存储库中拉取,这是公共 Docker Hub,但也可以是私有存储库。

就是这样 - 这就是你需要指定一个 Pod 并在 Kubernetes 中运行它的全部内容。从Pod部分开始的一切都属于额外配置的范畴。

Pod 资源规范

Pod 可以配置为具有分配给它们的特定内存和计算量。这可以防止特别耗费资源的应用程序影响集群性能,也可以帮助防止内存泄漏。可以指定两种可能的资源 - cpumemory。对于每个资源,有两种不同类型的规范,RequestsLimits,总共有四个可能的资源规范键。

内存请求和限制可以使用任何典型的内存数字后缀进行配置,或者其二的幂等价 - 例如,50 Mi(mebibytes),50 MB(megabytes)或 1 Gi(gibibytes)。

CPU 请求和限制可以通过使用m来配置,它对应于 1 毫 CPU,或者只是使用一个小数。因此,200m等同于0.2,相当于 20%或五分之一的逻辑 CPU。无论核心数量如何,这个数量都将是相同的计算能力。1 CPU 等于 AWS 中的虚拟核心或 GCP 中的核心。让我们看看这些资源请求和限制在我们的 YAML 文件中是什么样子的:

pod-with-resource-limits.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app-container
    image: mydockername
    resources:
      requests:
        memory: "50Mi"
        cpu: "100m"
      limits:
        memory: "200Mi"
        cpu: "500m"

在这个Pod中,我们有一个运行 Docker 镜像的容器,该容器在cpumemory上都指定了请求和限制。在这种情况下,我们的容器镜像名称mydockername是一个占位符 - 但是如果您想在此示例中测试 Pod 资源限制,可以使用 busybox 镜像。

容器启动命令

当容器在 Kubernetes Pod 中启动时,它将运行容器的默认启动脚本 - 例如,在 Docker 容器规范中指定的脚本。为了使用不同的命令或附加参数覆盖此功能,您可以提供commandargs键。让我们看一个配置了start命令和一些参数的容器:

pod-with-start-command.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app-container
    image: mydockername
    command: ["run"]
    args: ["--flag", "T", "--run-type", "static"]

正如您所看到的,我们指定了一个命令以及作为字符串数组的参数列表,用逗号分隔空格。

初始化容器

init容器是 Pod 中特殊的容器,在正常 Pod 容器启动之前启动、运行和关闭。

init容器可用于许多不同的用例,例如在应用程序启动之前初始化文件,或者确保其他应用程序或服务在启动 Pod 之前正在运行。

如果指定了多个init容器,它们将按顺序运行,直到所有init容器都关闭。因此,init容器必须运行一个完成并具有端点的脚本。如果您的init容器脚本或应用程序继续运行,Pod 中的正常容器将不会启动。

在下面的 Pod 中,init容器正在运行一个循环,通过nslookup检查我们的config-service是否存在。一旦它看到config-service已经启动,脚本就会结束,从而触发我们的my-app应用容器启动:

pod-with-init-container.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
  initContainers:
  - name: init-before
    image: busybox
    command: ['sh', '-c', 'until nslookup config-service; do echo config-service not up; sleep 2; done;']

重要提示

init容器失败时,Kubernetes 将自动重新启动 Pod,类似于通常的 Pod 启动功能。可以通过在 Pod 级别更改restartPolicy来更改此功能。

这是一个显示 Kubernetes 中典型 Pod 启动流程的图表:

图 3.2-初始化容器流程图

图 3.2-初始化容器流程图

如果一个 Pod 有多个initContainer,它们将按顺序被调用。这对于那些设置了必须按顺序执行的模块化步骤的initContainers非常有价值。以下 YAML 显示了这一点:

pod-with-multiple-init-containers.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
  initContainers:
  - name: init-step-1
    image: step1-image
    command: ['start-command']
  - name: init-step-2
    image: step2-image
    command: ['start-command']

例如,在这个Pod YAML 文件中,step-1 init容器需要在调用init-step-2之前成功,两者都需要在启动my-app容器之前显示成功。

在 Kubernetes 中引入不同类型的探针

为了知道容器(因此也是 Pod)何时失败,Kubernetes 需要知道如何测试容器是否正常工作。我们通过定义probes来实现这一点,Kubernetes 可以在指定的间隔运行这些probes,以确定容器是否正常工作。

Kubernetes 允许我们配置三种类型的探针-就绪、存活和启动。

就绪探针

首先,就绪探针可用于确定容器是否准备好执行诸如通过 HTTP 接受流量之类的功能。这些探针在应用程序运行的初始阶段非常有帮助,例如,当应用程序可能仍在获取配置,尚未准备好接受连接时。

让我们看一下配置了就绪探针的 Pod 是什么样子。接下来是一个附有就绪探针的 PodSpec:

pod-with-readiness-probe.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
    ports:
    - containerPort: 8080
    readinessProbe:
      exec:
        command:
        - cat
        - /tmp/thisfileshouldexist.txt
      initialDelaySeconds: 5
      periodSeconds: 5

首先,正如您所看到的,探针是针对每个容器而不是每个 Pod 定义的。Kubernetes 将对每个容器运行所有探针,并使用它来确定 Pod 的总体健康状况。

存活探针

存活探针可用于确定应用程序是否因某种原因(例如,由于内存错误)而失败。对于长时间运行的应用程序容器,存活探针可以作为一种方法,帮助 Kubernetes 回收旧的和损坏的 Pod,以便创建新的 Pod。虽然探针本身不会导致容器重新启动,但其他 Kubernetes 资源和控制器将检查探针状态,并在必要时使用它来重新启动 Pod。以下是附有存活探针定义的 PodSpec:

pod-with-liveness-probe.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
    ports:
    - containerPort: 8080
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/thisfileshouldexist.txt
      initialDelaySeconds: 5
      failureThreshold: 3
      periodSeconds: 5

正如您所看到的,我们的活跃性探针与就绪性探针以相同的方式指定,只是增加了failureThreshold

failureThreshold值将决定 Kubernetes 在采取行动之前尝试探测的次数。对于活跃性探针,一旦超过failureThreshold,Kubernetes 将重新启动 Pod。对于就绪性探针,Kubernetes 将简单地标记 Pod 为Not Ready。此阈值的默认值为3,但可以更改为大于或等于1的任何值。

在这种情况下,我们使用了exec机制进行探测。我们将很快审查可用的各种探测机制。

启动探针

最后,启动探针是一种特殊类型的探针,它只会在容器启动时运行一次。一些(通常是较旧的)应用程序在容器中启动需要很长时间,因此在容器第一次启动时提供一些额外的余地,可以防止活跃性或就绪性探针失败并导致重新启动。以下是配置了启动探针的 Pod 示例:

pod-with-startup-probe.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
    ports:
    - containerPort: 8080
    startupProbe:
      exec:
        command:
        - cat
        - /tmp/thisfileshouldexist.txt
      initialDelaySeconds: 5
      successThreshold: 2
      periodSeconds: 5

启动探针提供的好处不仅仅是延长活跃性或就绪性探针之间的时间 - 它们允许 Kubernetes 在启动后处理问题时保持快速反应,并且(更重要的是)防止启动缓慢的应用程序不断重新启动。如果您的应用程序需要多秒甚至一两分钟才能启动,您将更容易实现启动探针。

successThreshold就像它的名字一样,是failureThreshold的对立面。它指定在容器标记为Ready之前需要连续多少次成功。对于在启动时可能会上下波动然后稳定下来的应用程序(如一些自我集群应用程序),更改此值可能很有用。默认值为1,对于活跃性探针,唯一可能的值是1,但我们可以更改就绪性和启动探针的值。

探测机制配置

有多种机制可以指定这三种探针中的任何一种:exechttpGettcpSocket

exec方法允许您指定在容器内运行的命令。成功执行的命令将导致探测通过,而失败的命令将导致探测失败。到目前为止,我们配置的所有探针都使用了exec方法,因此配置应该是不言自明的。如果所选命令(以逗号分隔的列表形式指定的任何参数)失败,探测将失败。

httpGet方法允许您为探针指定容器上的 URL,该 URL 将受到 HTTP GET请求的访问。如果 HTTP 请求返回的代码在200400之间,它将导致探测成功。任何其他 HTTP 代码将导致失败。

httpGet的配置如下:

pod-with-get-probe.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
    ports:
    - containerPort: 8080
    livenessProbe:
      httpGet:
        path: /healthcheck
        port: 8001
        httpHeaders:
        - name: My-Header
          value: My-Header-Value
        initialDelaySeconds: 3
        periodSeconds: 3

最后,tcpSocket方法将尝试在容器上打开指定的套接字,并使用结果来决定成功或失败。tcpSocket配置如下:

pod-with-tcp-probe.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myApp
spec:
  containers:
  - name: my-app
    image: mydockername
    command: ["run"]
    ports:
    - containerPort: 8080
    readinessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10

正如您所看到的,这种类型的探针接收一个端口,每次检查发生时都会对其进行 ping 测试。

常见的 Pod 转换

Kubernetes 中的失败 Pod 往往在不同状态之间转换。对于初次使用者来说,这可能会令人生畏,因此将我们之前列出的 Pod 状态与探针功能相互作用进行分解是很有价值的。再次强调一下,这是我们的状态:

  • Running

  • Succeeded

  • Pending

  • Unknown

  • Failed

一个常见的流程是运行kubectl get pods -w-w标志会在命令中添加一个监视器),然后查看有问题的 Pod 在PendingFailed之间的转换。通常情况下,发生的是 Pod(及其容器)正在启动和拉取镜像 - 这是Pending状态,因为健康检查尚未开始。

一旦初始探测超时(正如我们在前一节中看到的那样,这是可配置的),第一个探测失败。这可能会持续几秒甚至几分钟,具体取决于失败阈值的高低,状态仍然固定在Pending

最后,我们的失败阈值达到,我们的 Pod 状态转换为Failed。在这一点上,有两种情况可能发生,决定纯粹基于 PodSpec 上的RestartPolicy,它可以是AlwaysNeverOnFailure。如果一个 Pod 失败并且restartPolicyNever,那么 Pod 将保持在失败状态。如果是其他两个选项之一,Pod 将自动重新启动,并返回到Pending,这是我们永无止境的转换循环的根本原因。

举个不同的例子,您可能会看到 Pod 永远停留在Pending状态。这可能是由于 Pod 无法被调度到任何节点。这可能是由于资源请求约束(我们将在本书的后面深入讨论,第八章Pod 放置控件),或其他问题,比如节点无法访问。

最后,对于Unknown,通常 Pod 被调度的节点由于某种原因无法访问 - 例如,节点可能已关闭,或者通过网络无法访问。

Pod 调度

Pod 调度的复杂性以及 Kubernetes 让您影响和控制它的方式将保存在我们的第八章中,Pod 放置控件 - 但现在我们将回顾基础知识。

在决定在哪里调度一个 Pod 时,Kubernetes 考虑了许多因素,但最重要的是考虑(当不深入研究 Kubernetes 让我们使用的更复杂的控件时)Pod 优先级、节点可用性和资源可用性。

Kubernetes 调度程序操作一个不断的控制循环,监视集群中未绑定(未调度)的 Pod。如果找到一个或多个未绑定的 Pod,调度程序将使用 Pod 优先级来决定首先调度哪一个。

一旦调度程序决定要调度一个 Pod,它将执行几轮和类型的检查,以找到调度 Pod 的节点的局部最优解。后面的检查由细粒度的调度控件决定,我们将在第八章中详细介绍Pod 放置控件。现在我们只关心前几轮的检查。

首先,Kubernetes 检查当前时刻哪些节点可以被调度。节点可能无法正常工作,或者遇到其他问题,这将阻止新的 Pod 被调度。

其次,Kubernetes 通过检查哪些节点与 PodSpec 中规定的最小资源需求匹配来过滤可调度的节点。

在没有其他放置控制的情况下,调度器将做出决定并将新的 Pod 分配给一个节点。当该节点上的 kubelet 看到有一个新的 Pod 分配给它时,该 Pod 将被启动。

摘要

在本章中,我们了解到 Pod 是我们在 Kubernetes 中使用的最基本的构建块。对 Pod 及其所有微妙之处有深入的理解非常重要,因为在 Kubernetes 上的所有计算都使用 Pod 作为构建块。现在可能很明显了,但 Pod 是非常小的、独立的东西,不太牢固。在 Kubernetes 上以单个 Pod 运行应用程序而没有控制器是一个糟糕的决定,你的 Pod 出现任何问题都会导致停机时间。

在下一章中,我们将看到如何通过使用 Pod 控制器同时运行应用程序的多个副本来防止这种情况发生。

问题

  1. 你如何使用命名空间来分隔应用程序环境?

  2. Pod 状态被列为 Unknown 的可能原因是什么?

  3. 限制 Pod 内存资源的原因是什么?

  4. 如果在 Kubernetes 上运行的应用程序经常在失败的探测重新启动 Pod 之前无法及时启动,你应该调整哪种探测类型?就绪性、存活性还是启动?

进一步阅读

第二部分:在 Kubernetes 上配置和部署应用程序

在本节中,您将学习如何在 Kubernetes 上配置和部署应用程序,以及配置存储并将应用程序暴露到集群外部。

本书的这一部分包括以下章节:

  • 第四章,扩展和部署您的应用程序

  • 第五章,服务和入口 - 与外部世界通信

  • 第六章,Kubernetes 应用程序配置

  • 第七章,Kubernetes 上的存储

  • 第八章,Pod 放置控制

第四章:扩展和部署您的应用程序

在本章中,我们将学习用于运行应用程序和控制 Pod 的高级 Kubernetes 资源。首先,我们将介绍 Pod 的缺点,然后转向最简单的 Pod 控制器 ReplicaSets。然后我们将转向部署,这是将应用程序部署到 Kubernetes 的最流行方法。然后,我们将介绍特殊资源,以帮助您部署特定类型的应用程序–水平 Pod 自动缩放器、DaemonSets、StatefulSets 和 Jobs。最后,我们将通过一个完整的示例将所有内容整合起来,演示如何在 Kubernetes 上运行复杂的应用程序。

在本章中,我们将涵盖以下主题:

  • 了解 Pod 的缺点及其解决方案

  • 使用 ReplicaSets

  • 控制部署

  • 利用水平 Pod 自动缩放

  • 实施 DaemonSets

  • 审查 StatefulSets 和 Jobs

  • 把所有东西放在一起

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书籍的 GitHub 存储库中找到github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter4

了解 Pod 的缺点及其解决方案

正如我们在上一章第三章中所回顾的,在 Kubernetes 上运行应用程序容器,在 Kubernetes 中,Pod 是在节点上运行一个或多个应用程序容器的实例。创建一个 Pod 就足以像在任何其他容器中一样运行应用程序。

也就是说,使用单个 Pod 来运行应用程序忽略了在容器中运行应用程序的许多好处。容器允许我们将应用程序的每个实例视为一个可以根据需求进行扩展或缩减的无状态项目,通过启动应用程序的新实例来满足需求。

这既可以让我们轻松扩展应用程序,又可以通过在给定时间提供多个应用程序实例来提高应用程序的可用性。如果我们的一个实例崩溃,应用程序仍将继续运行,并将自动扩展到崩溃前的水平。在 Kubernetes 上,我们通过使用 Pod 控制器资源来实现这一点。

Pod 控制器

Kubernetes 提供了几种 Pod 控制器的选择。最简单的选择是使用 ReplicaSet,它维护特定 Pod 的给定数量的实例。如果一个实例失败,ReplicaSet 将启动一个新实例来替换它。

其次,有部署,它们自己控制一个 ReplicaSet。在 Kubernetes 上运行应用程序时,部署是最受欢迎的控制器,它们使得通过 ReplicaSet 进行滚动更新来升级应用程序变得容易。

水平 Pod 自动缩放器将部署带到下一个级别,允许应用根据性能指标自动缩放到不同数量的实例。

最后,在某些特定情况下可能有一些特殊的控制器可能是有价值的:

  • DaemonSets,每个节点上运行一个应用程序实例并维护它们

  • StatefulSets,其中 Pod 身份保持静态以帮助运行有状态的工作负载

  • 作业,它在指定数量的 Pod 上启动,运行完成,然后关闭。

控制器的实际行为,无论是默认的 Kubernetes 控制器,如 ReplicaSet,还是自定义控制器(例如 PostgreSQL Operator),都应该很容易预测。标准控制循环的简化视图看起来像下面的图表:

图 4.1- Kubernetes 控制器的基本控制循环

图 4.1- Kubernetes 控制器的基本控制循环

正如您所看到的,控制器不断地检查预期的集群状态(我们希望有七个此应用程序的 Pod)与当前的集群状态(我们有五个此应用程序的 Pod 正在运行)是否匹配。当预期状态与当前状态不匹配时,控制器将通过 API 采取行动来纠正当前状态以匹配预期状态。

到目前为止,您应该明白为什么在 Kubernetes 上需要控制器:Pod 本身在提供高可用性应用程序方面不够强大。让我们继续讨论最简单的控制器:ReplicaSet。

使用 ReplicaSets

ReplicaSet 是最简单的 Kubernetes Pod 控制器资源。它取代了较旧的 ReplicationController 资源。

ReplicaSet 和 ReplicationController 之间的主要区别在于 ReplicationController 使用更基本类型的选择器 - 确定应该受控制的 Pod 的过滤器。

虽然 ReplicationControllers 使用简单的基于等式(key=value)的选择器,但 ReplicaSets 使用具有多种可能格式的选择器,例如matchLabelsmatchExpressions,这将在本章中进行审查。

重要说明

除非您有一个非常好的理由,否则不应该使用 ReplicationController 而应该使用 ReplicaSet-坚持使用 ReplicaSets。

ReplicaSets 允许我们通知 Kubernetes 维护特定 Pod 规范的一定数量的 Pod。ReplicaSet 的 YAML 与 Pod 的 YAML 非常相似。实际上,整个 Pod 规范都嵌套在 ReplicaSet 的 YAML 中,位于template键下。

还有一些其他关键区别,可以在以下代码块中观察到:

replica-set.yaml

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-group
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp-container
        image: busybox

正如您所看到的,除了template部分(本质上是一个 Pod 定义),在我们的 ReplicaSet 规范中还有一个selector键和一个replicas键。让我们从replicas开始。

副本

replicas键指定了副本数量,我们的 ReplicaSet 将确保在任何给定时间始终运行指定数量的副本。如果一个 Pod 死掉或停止工作,我们的 ReplicaSet 将创建一个新的 Pod 来替代它。这使得 ReplicaSet 成为一个自愈资源。

ReplicaSet 控制器如何决定一个 Pod 何时停止工作?它查看 Pod 的状态。如果 Pod 的当前状态不是“Running”或“ContainerCreating”,ReplicaSet 将尝试启动一个新的 Pod。

正如我们在第三章中讨论的那样,在 Kubernetes 上运行应用容器,容器创建后 Pod 的状态由存活探针、就绪探针和启动探针驱动,这些探针可以针对 Pod 进行特定配置。这意味着您可以设置特定于应用程序的方式来判断 Pod 是否以某种方式损坏,并且您的 ReplicaSet 可以介入并启动一个新的 Pod 来替代它。

选择器

selector键很重要,因为 ReplicaSet 的工作方式是以选择器为核心实现的控制器。ReplicaSet 的工作是确保与其选择器匹配的运行中的 Pod 数量是正确的。

比如说,你有一个现有的 Pod 运行你的应用程序 MyApp。这个 Pod 被标记为 selector 键为 App=MyApp

现在假设你想创建一个具有相同应用程序的 ReplicaSet,这将增加你的应用程序的三个额外实例。你使用相同的选择器创建一个 ReplicaSet,并指定三个副本,目的是总共运行四个实例,因为你已经有一个在运行。

一旦你启动 ReplicaSet,会发生什么?你会发现运行该应用程序的总 pod 数将是三个,而不是四个。这是因为 ReplicaSet 有能力接管孤立的 pods 并将它们纳入其管理范围。

当 ReplicaSet 启动时,它会看到已经存在一个与其 selector 键匹配的现有 Pod。根据所需的副本数,ReplicaSet 将关闭现有的 Pods 或启动新的 Pods,以匹配 selector 以创建正确的数量。

模板

template 部分包含 Pod,并支持与 Pod YAML 相同的所有字段,包括元数据部分和规范本身。大多数其他控制器都遵循这种模式 - 它们允许你在更大的控制器 YAML 中定义 Pod 规范。

现在你应该了解 ReplicaSet 规范的各个部分以及它们的作用。让我们继续使用我们的 ReplicaSet 来运行应用程序。

测试 ReplicaSet

现在,让我们部署我们的 ReplicaSet。

复制先前列出的 replica-set.yaml 文件,并在与你的 YAML 文件相同的文件夹中使用以下命令在你的集群上运行它:

kubectl apply -f replica-set.yaml

为了检查 ReplicaSet 是否已正确创建,请运行 kubectl get pods 来获取默认命名空间中的 Pods。

由于我们没有为 ReplicaSet 指定命名空间,它将默认创建。kubectl get pods 命令应该给你以下结果:

NAME                            READY     STATUS    RESTARTS   AGE
myapp-group-192941298-k705b     1/1       Running   0          1m
myapp-group-192941298-o9sh8     1/1       Running   0        1m
myapp-group-192941298-n8gh2     1/1       Running   0        1m

现在,尝试使用以下命令删除一个 ReplicaSet Pod:

kubectl delete pod myapp-group-192941298-k705b

ReplicaSet 将始终尝试保持指定数量的副本在线。

让我们使用 kubectl get 命令再次查看我们正在运行的 pods:

NAME                         READY  STATUS             RESTARTS AGE
myapp-group-192941298-u42s0  1/1    ContainerCreating  0     1m
myapp-group-192941298-o9sh8  1/1    Running            0     2m
myapp-group-192941298-n8gh2  1/1    Running            0     2m

如你所见,我们的 ReplicaSet 控制器正在启动一个新的 pod,以保持我们的副本数为三。

最后,让我们使用以下命令删除我们的 ReplicaSet:

kubectl delete replicaset myapp-group

清理了一下我们的集群,让我们继续学习一个更复杂的控制器 - 部署。

控制部署

虽然 ReplicaSets 包含了您想要运行高可用性应用程序的大部分功能,但大多数时候您会想要使用部署来在 Kubernetes 上运行应用程序。

部署比 ReplicaSets 有一些优势,实际上它们通过拥有和控制一个 ReplicaSet 来工作。

部署的主要优势在于它允许您指定rollout过程 - 也就是说,应用程序升级如何部署到部署中的各个 Pod。这让您可以轻松配置控件以阻止糟糕的升级。

在我们回顾如何做到这一点之前,让我们看一下部署的整个规范:

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: myapp
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25% 
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp-container
        image: busybox

正如您所看到的,这与 ReplicaSet 的规范非常相似。我们在这里看到的区别是规范中的一个新键:strategy

使用strategy设置,我们可以告诉部署方式升级我们的应用程序,可以通过RollingUpdateRecreate

Recreate是一种非常基本的部署方法:部署中的所有 Pod 将同时被删除,并将使用新版本创建新的 Pod。Recreate不能给我们太多控制权来防止糟糕的部署 - 如果由于某种原因新的 Pod 无法启动,我们将被困在一个完全无法运行的应用程序中。

另一方面,使用RollingUpdate,部署速度较慢,但控制更加严格。首先,新应用程序将逐步推出,逐个 Pod。我们可以指定maxSurgemaxUnavailable的值来调整策略。

滚动更新的工作方式是这样的 - 当部署规范使用 Pod 容器的新版本进行更新时,部署将逐个关闭一个 Pod,创建一个新的带有新应用程序版本的 Pod,等待新的 Pod 根据就绪检查注册为Ready,然后继续下一个 Pod。

maxSurgemaxUnavailable参数允许您加快或减慢此过程。maxUnavailable允许您调整在部署过程中不可用的最大 Pod 数量。这可以是百分比或固定数量。maxSurge允许您调整在任何给定时间内可以创建的超出部署副本数量的最大 Pod 数量。与maxUnavailable一样,这可以是百分比或固定数量。

以下图表显示了RollingUpdate过程:

图 4.2 - 部署的 RollingUpdate 过程

图 4.2 - 部署的 RollingUpdate 过程

正如您所看到的,“滚动更新”过程遵循了几个关键步骤。部署尝试逐个更新 Pod。只有在成功更新一个 Pod 之后,更新才会继续到下一个 Pod。

使用命令控制部署。

正如我们所讨论的,我们可以通过简单地更新其 YAML 文件来更改我们的部署,使用声明性方法。然而,Kubernetes 还为我们提供了一些在kubectl中控制部署的特殊命令。

首先,Kubernetes 允许我们手动扩展部署-也就是说,我们可以编辑应该运行的副本数量。

要将我们的myapp-deployment扩展到五个副本,我们可以运行以下命令:

kubectl scale deployment myapp-deployment --replicas=5

同样,如果需要,我们可以将我们的myapp-deployment回滚到旧版本。为了演示这一点,首先让我们手动编辑我们的部署,以使用容器的新版本:

Kubectl set image deployment myapp-deployment myapp-container=busybox:1.2 –record=true

这个命令告诉 Kubernetes 将我们部署中容器的版本更改为 1.2。然后,我们的部署将按照前面的图表中的步骤来推出我们的更改。

现在,假设我们想回到之前更新容器图像版本之前的版本。我们可以使用rollout undo命令轻松实现这一点:

Kubectl rollout undo deployment myapp-deployment

在我们之前的情况下,我们只有两个版本,初始版本和我们更新容器的版本,但如果有其他版本,我们可以在undo命令中指定它们,就像这样:

Kubectl rollout undo deployment myapp-deployment –to-revision=10

这应该让您对为什么部署如此有价值有所了解-它们为我们提供了对应用程序新版本的推出的精细控制。接下来,我们将讨论一个与部署和副本集协同工作的 Kubernetes 智能缩放器。

利用水平 Pod 自动缩放器

正如我们所看到的,部署和副本集允许您指定应在某个时间可用的副本的总数。然而,这些结构都不允许自动缩放-它们必须手动缩放。

水平 Pod 自动缩放器(HPA)通过作为更高级别的控制器存在,可以根据 CPU 和内存使用等指标改变部署或副本集的副本数量来提供这种功能。

默认情况下,HPA 可以根据 CPU 利用率进行自动缩放,但通过使用自定义指标,可以扩展此功能。

HPA 的 YAML 文件如下所示:

hpa.yaml

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  maxReplicas: 5
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp-deployment
  targetCPUUtilizationPercentage: 70

在上述规范中,我们有scaleTargetRef,它指定了 HPA 应该自动缩放的内容,以及调整参数。

scaleTargetRef的定义可以是部署(Deployment)、副本集(ReplicaSet)或复制控制器(ReplicationController)。在这种情况下,我们已经定义了 HPA 来扩展我们之前创建的部署myapp-deployment

对于调整参数,我们使用默认的基于 CPU 利用率的扩展,因此我们可以使用targetCPUUtilizationPercentage来定义运行我们应用程序的每个 Pod 的预期 CPU 利用率。如果我们的 Pod 的平均 CPU 使用率超过 70%,我们的 HPA 将扩展部署规范,如果它长时间下降到以下水平,它将缩小部署。

典型的扩展事件看起来像这样:

  1. 部署的平均 CPU 使用率超过了三个副本的 70%。

  2. HPA 控制循环注意到 CPU 利用率的增加。

  3. HPA 使用新的副本计数编辑部署规范。这个计数是基于 CPU 利用率计算的,目的是使每个节点的 CPU 使用率保持在 70%以下的稳定状态。

  4. 部署控制器启动一个新的副本。

  5. 这个过程会重复自身来扩展或缩小部署。

总之,HPA 跟踪 CPU 和内存利用率,并在超出边界时启动扩展事件。接下来,我们将审查 DaemonSets,它们提供了一种非常特定类型的 Pod 控制器。

实施 DaemonSets

从现在到本章结束,我们将审查更多关于具有特定要求的应用程序运行的小众选项。

我们将从 DaemonSets 开始,它们类似于 ReplicaSets,只是副本的数量固定为每个节点一个副本。这意味着集群中的每个节点将始终保持应用程序的一个副本处于活动状态。

重要说明

重要的是要记住,在没有额外的 Pod 放置控制(如污点或节点选择器)的情况下,这个功能只会在每个节点上创建一个副本,我们将在第八章中更详细地介绍Pod 放置控制

这最终看起来像典型 DaemonSet 的下图所示:

图 4.3 - DaemonSet 分布在三个节点上

图 4.3 - DaemonSet 分布在三个节点上

正如您在上图中所看到的,每个节点(由方框表示)包含一个由 DaemonSet 控制的应用程序的 Pod。

这使得 DaemonSets 非常适合在节点级别收集指标或在每个节点上提供网络处理。DaemonSet 规范看起来像这样:

daemonset-1.yaml

apiVersion: apps/v1 
kind: DaemonSet
metadata:
  name: log-collector
spec:
  selector:
      matchLabels:
        name: log-collector   
  template:
    metadata:
      labels:
        name: log-collector
    spec:
      containers:
      - name: fluentd
        image: fluentd

如您所见,这与您典型的 ReplicaSet 规范非常相似,只是我们没有指定副本的数量。这是因为 DaemonSet 会尝试在集群中的每个节点上运行一个 Pod。

如果您想指定要运行应用程序的节点子集,可以使用节点选择器,如下面的文件所示:

daemonset-2.yaml

apiVersion: apps/v1 
kind: DaemonSet
metadata:
  name: log-collector
spec:
  selector:
      matchLabels:
        name: log-collector   
  template:
    metadata:
      labels:
        name: log-collector
    spec:
      nodeSelector:
        type: bigger-node 
      containers:
      - name: fluentd
        image: fluentd

这个 YAML 将限制我们的 DaemonSet 只能在其标签中匹配type=bigger-node的节点上运行。我们将在第八章中更多地了解有关节点选择器的信息,Pod 放置控制。现在,让我们讨论一种非常适合运行有状态应用程序(如数据库)的控制器类型 - StatefulSet。

理解 StatefulSets

StatefulSets 与 ReplicaSets 和 Deployments 非常相似,但有一个关键的区别,使它们更适合有状态的工作负载。StatefulSets 保持每个 Pod 的顺序和标识,即使 Pod 被重新调度到新节点上。

例如,在一个有 3 个副本的 StatefulSet 中,将始终存在 Pod 1、Pod 2 和 Pod 3,并且这些 Pod 将在 Kubernetes 和存储中保持它们的标识(我们将在第七章中介绍,Kubernetes 上的存储),无论发生任何重新调度。

让我们来看一个简单的 StatefulSet 配置:

statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: stateful
spec:
  selector:
    matchLabels:
      app: stateful-app
  replicas: 5
  template:
    metadata:
      labels:
        app: stateful-app
    spec:
      containers:
      - name: app
        image: busybox

这个 YAML 将创建一个具有五个应用程序副本的 StatefulSet。

让我们看看 StatefulSet 如何与典型的 Deployment 或 ReplicaSet 不同地维护 Pod 标识。让我们使用以下命令获取所有 Pods:

kubectl get pods

输出应如下所示:

NAME      		   READY     STATUS    RESTARTS   AGE
stateful-app-0     1/1       Running   0         55s
stateful-app-1     1/1       Running   0         48s
stateful-app-2     1/1       Running   0         26s
stateful-app-3     1/1       Running   0         18s
stateful-app-4     0/1       Pending   0         3s

如您所见,在这个例子中,我们有五个 StatefulSet Pods,每个都有一个数字指示其标识。这个属性对于有状态的应用程序非常有用,比如数据库集群。在 Kubernetes 上运行数据库集群时,主 Pod 与副本 Pod 的标识很重要,我们可以使用 StatefulSet 标识来轻松管理它。

另一个有趣的地方是,您可以看到最终的 Pod 仍在启动,并且随着数字标识的增加,Pod 的年龄也在增加。这是因为 StatefulSet Pods 是按顺序逐个创建的。

StatefulSets 在持久的 Kubernetes 存储中非常有价值,以便运行有状态的应用程序。我们将在第七章《Kubernetes 上的存储》中了解更多相关内容,但现在让我们讨论另一个具有非常特定用途的控制器:Jobs。

使用 Jobs

Kubernetes 中 Job 资源的目的是运行可以完成的任务,这使它们不太适合长时间运行的应用程序,但非常适合批处理作业或类似任务,可以从并行性中受益。

以下是 Job 规范 YAML 的样子:

job-1.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: runner
spec:
  template:
    spec:
      containers:
      - name: run-job
        image: node:lts-jessie
        command: ["node", "job.js"]
      restartPolicy: Never
  backoffLimit: 4

这个 Job 将启动一个单独的 Pod,并运行一个命令 node job.js,直到完成,然后 Pod 将关闭。在这个和未来的示例中,我们假设使用的容器镜像有一个名为 job.js 的文件,其中包含了作业逻辑。node:lts-jessie 容器镜像默认情况下不会有这个文件。这是一个不使用并行性运行的 Job 的示例。正如您可能从 Docker 的使用中知道的那样,多个命令参数必须作为字符串数组传递。

为了创建一个可以并行运行的 Job(也就是说,多个副本同时运行 Job),您需要以一种可以在结束进程之前告诉它 Job 已完成的方式来开发应用程序代码。为了做到这一点,每个 Job 实例都需要包含代码,以确保它执行更大批处理任务的正确部分,并防止发生重复工作。

有几种应用程序模式可以实现这一点,包括互斥锁和工作队列。此外,代码需要检查整个批处理任务的状态,这可能需要通过更新数据库中的值来处理。一旦 Job 代码看到更大的任务已经完成,它就应该退出。

完成后,您可以使用 parallelism 键向作业代码添加并行性。以下代码块显示了这一点:

job-2.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: runner
spec:
  parallelism: 3
  template:
    spec:
      containers:
      - name: run-job
        image: node:lts-jessie
        command: ["node", "job.js"]
      restartPolicy: Never
  backoffLimit: 4

如您所见,我们使用 parallelism 键添加了三个副本。此外,您可以将纯作业并行性替换为指定数量的完成次数,在这种情况下,Kubernetes 可以跟踪 Job 已完成的次数。您仍然可以为此设置并行性,但如果不设置,它将默认为 1。

下一个规范将运行一个 Job 完成 4 次,每次运行 2 次迭代:

job-3.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: runner
spec:
  parallelism: 2
  completions: 4
  template:
    spec:
      containers:
      - name: run-job
        image: node:lts-jessie
        command: ["node", "job.js"]
      restartPolicy: Never
  backoffLimit: 4

Kubernetes 上的作业提供了一种很好的方式来抽象一次性进程,并且许多第三方应用程序将它们链接到工作流中。正如你所看到的,它们非常容易使用。

接下来,让我们看一个非常相似的资源,CronJob。

CronJobs

CronJobs 是用于定时作业执行的 Kubernetes 资源。这与你可能在你喜欢的编程语言或应用程序框架中找到的 CronJob 实现非常相似,但有一个关键的区别。Kubernetes CronJobs 触发 Kubernetes Jobs,这提供了一个额外的抽象层,可以用来触发每天晚上的批处理作业。

Kubernetes 中的 CronJobs 使用非常典型的 cron 表示法进行配置。让我们来看一下完整的规范:

cronjob-1.yaml

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
           - name: run-job
             image: node:lts-jessie
             command: ["node", "job.js"]
          restartPolicy: OnFailure

这个 CronJob 将在每天凌晨 1 点创建一个与我们之前的 Job 规范相同的 Job。要快速查看 cron 时间表示法,以解释我们凌晨 1 点工作的语法,请继续阅读。要全面了解 cron 表示法,请查看man7.org/linux/man-pages/man5/crontab.5.html

Cron 表示法由五个值组成,用空格分隔。每个值可以是数字整数、字符或组合。这五个值中的每一个代表一个时间值,格式如下,从左到右:

  • 分钟

  • 小时

  • 一个月中的某一天(比如25

  • 星期几(例如,3 = 星期三)

之前的 YAML 假设了一个非并行的 CronJob。如果我们想增加 CronJob 的批处理能力,我们可以像之前的作业规范一样添加并行性。以下代码块显示了这一点:

cronjob-2.yaml

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      parallelism: 3
      template:
        spec:
          containers:
           - name: run-job
             image: node:lts-jessie
             command: ["node", "job.js"]
          restartPolicy: OnFailure

请注意,为了使其工作,你的 CronJob 容器中的代码需要优雅地处理并行性,这可以使用工作队列或其他类似的模式来实现。

我们现在已经审查了 Kubernetes 默认提供的所有基本控制器。让我们利用我们的知识,在下一节中运行一个更复杂的应用程序示例在 Kubernetes 上。

把所有这些放在一起

我们现在有了在 Kubernetes 上运行应用程序的工具集。让我们看一个真实的例子,看看如何将所有这些组合起来运行一个具有多个层和功能分布在 Kubernetes 资源上的应用程序:

图 4.4 - 多层应用程序图表

图 4.4 - 多层应用程序图表

正如您在前面的代码中所看到的,我们的示例应用程序包含一个运行.NET Framework 应用程序的 Web 层,一个运行 Java 的中间层或服务层,一个运行 Postgres 的数据库层,最后是一个日志/监控层。

我们对每个层级的控制器选择取决于我们计划在每个层级上运行的应用程序。对于 Web 层和中间层,我们运行无状态应用程序和服务,因此我们可以有效地使用 Deployments 来处理更新、蓝/绿部署等。

对于数据库层,我们需要我们的数据库集群知道哪个 Pod 是副本,哪个是主节点 - 因此我们使用 StatefulSet。最后,我们的日志收集器需要在每个节点上运行,因此我们使用 DaemonSet 来运行它。

现在,让我们逐个查看每个层级的示例 YAML 规范。

让我们从基于 JavaScript 的 Web 应用程序开始。通过在 Kubernetes 上托管此应用程序,我们可以进行金丝雀测试和蓝/绿部署。需要注意的是,本节中的一些示例使用在 DockerHub 上不公开可用的容器映像名称。要使用此模式,请将示例调整为您自己的应用程序容器,或者如果您想在没有实际应用程序逻辑的情况下运行它,只需使用 busybox。

Web 层的 YAML 文件可能如下所示:

example-deployment-web.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webtier-deployment
  labels:
    tier: web
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 25% 
  selector:
    matchLabels:
      tier: web
  template:
    metadata:
      labels:
        tier: web
    spec:
      containers:
      - name: reactapp-container
        image: myreactapp

在前面的 YAML 中,我们使用tier标签对我们的应用程序进行标记,并将其用作我们的matchLabels选择器。

接下来是中间层服务层。让我们看看相关的 YAML:

example-deployment-mid.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: midtier-deployment
  labels:
    tier: mid
spec:
  replicas: 8
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25% 
  selector:
    matchLabels:
      tier: mid
  template:
    metadata:
      labels:
        tier: mid
    spec:
      containers:
      - name: myjavaapp-container
        image: myjavaapp

正如您在前面的代码中所看到的,我们的中间层应用程序与 Web 层设置非常相似,并且我们使用了另一个 Deployment。

现在是有趣的部分 - 让我们来看看我们的 Postgres StatefulSet 的规范。我们已经在这个代码块中进行了一些截断,以便适应页面,但您应该能够看到最重要的部分:

example-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres-db
  labels:
    tier: db
spec:
  serviceName: "postgres"
  replicas: 2
  selector:
    matchLabels:
      tier: db
  template:
    metadata:
      labels:
        tier: db
    spec:
      containers:
      - name: postgres
        image: postgres:latest
        envFrom:
          - configMapRef:
              name: postgres-conf
        volumeMounts:
        - name: pgdata
          mountPath: /var/lib/postgresql/data
          subPath: postgres

在前面的 YAML 文件中,我们可以看到一些我们尚未审查的新概念 - ConfigMaps 和卷。我们将在第六章Kubernetes 应用程序配置第七章Kubernetes 上的存储中更仔细地了解它们的工作原理,但现在让我们专注于规范的其余部分。我们有我们的postgres容器以及在默认的 Postgres 端口5432上设置的端口。

最后,让我们来看看我们的日志应用程序的 DaemonSet。这是 YAML 文件的一部分,我们为了长度又进行了截断:

example-daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    tier: logging
spec:
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        tier: logging
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1-debian-papertrail
        env:
          - name: FLUENT_PAPERTRAIL_HOST
            value: "mycompany.papertrailapp.com"
          - name: FLUENT_PAPERTRAIL_PORT
            value: "61231"
          - name: FLUENT_HOSTNAME
            value: "DEV_CLUSTER"

在这个 DaemonSet 中,我们正在设置 FluentD(一个流行的开源日志收集器)将日志转发到 Papertrail,一个基于云的日志收集器和搜索工具。同样,在这个 YAML 文件中,有一些我们以前没有审查过的内容。例如,tolerations部分用于node-role.kubernetes.io/master,实际上允许我们的 DaemonSet 将 Pod 放置在主节点上,而不仅仅是工作节点上。我们将在第八章 Pod 放置控制中审查这是如何工作的。

我们还在 Pod 规范中直接指定环境变量,这对于相对基本的配置来说是可以的,但是可以通过使用 Secrets 或 ConfigMaps(我们将在第六章 Kubernetes 应用配置中进行审查)来改进,以避免将其放入我们的 YAML 代码中。

摘要

在本章中,我们回顾了在 Kubernetes 上运行应用程序的一些方法。首先,我们回顾了为什么 Pod 本身不足以保证应用程序的可用性,并介绍了控制器。然后,我们回顾了一些简单的控制器,包括 ReplicaSets 和 Deployments,然后转向具有更具体用途的控制器,如 HPAs、Jobs、CronJobs、StatefulSets 和 DaemonSets。最后,我们将所有学到的知识应用到了在 Kubernetes 上运行复杂应用程序的实现中。

在下一章中,我们将学习如何使用 Services 和 Ingress 将我们的应用程序(现在具有高可用性)暴露给世界。

问题

  1. ReplicaSet 和 ReplicationController 之间有什么区别?

  2. Deployment 相对于 ReplicaSet 的优势是什么?

  3. 什么是 Job 的一个很好的用例?

  4. 为什么 StatefulSets 对有状态的工作负载更好?

  5. 我们如何使用 Deployments 支持金丝雀发布流程?

进一步阅读

第五章:服务和 Ingress-与外部世界通信

本章包含了 Kubernetes 提供的方法的全面讨论,允许应用程序相互通信,以及与集群外部的资源通信。您将了解 Kubernetes 服务资源及其所有可能的类型-ClusterIP、NodePort、LoadBalancer 和 ExternalName-以及如何实现它们。最后,您将学习如何使用 Kubernetes Ingress。

在本章中,我们将涵盖以下主题:

  • 理解服务和集群 DNS

  • 实现 ClusterIP

  • 使用 NodePort

  • 设置 LoadBalancer 服务

  • 创建 ExternalName 服务

  • 配置 Ingress

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请查看第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书籍的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter5

理解服务和集群 DNS

在过去的几章中,我们已经讨论了如何有效地在 Kubernetes 上运行应用程序,使用包括 Pods、Deployments 和 StatefulSets 在内的资源。然而,许多应用程序,如 Web 服务器,需要能够接受来自其容器外部的网络请求。这些请求可能来自其他应用程序,也可能来自访问公共互联网的设备。

Kubernetes 提供了几种资源类型,用于处理允许集群外部和内部资源访问运行在 Pods、Deployments 等应用程序的各种情况。

这些属于两种主要资源类型,服务和 Ingress:

  • 服务有几种子类型-ClusterIP、NodePort 和 LoadBalancer-通常用于提供从集群内部或外部简单访问单个应用程序。

  • Ingress 是一个更高级的资源,它创建一个控制器,负责基于路径名和主机名的路由到集群内运行的各种资源。Ingress 通过使用规则将流量转发到服务来工作。您需要使用服务来使用 Ingress。

在我们开始第一种类型的服务资源之前,让我们回顾一下 Kubernetes 如何处理集群内部的 DNS。

集群 DNS

让我们首先讨论在 Kubernetes 中哪些资源默认拥有自己的 DNS 名称。Kubernetes 中的 DNS 名称仅限于 Pod 和服务。Pod DNS 名称包含几个部分,结构化为子域。

在 Kubernetes 中运行的 Pod 的典型完全限定域名(FQDN)如下所示:

my-hostname.my-subdomain.my-namespace.svc.my-cluster-domain.example

让我们从最右边开始分解:

  • my-cluster-domain.example对应于 Cluster API 本身的配置 DNS 名称。根据用于设置集群的工具以及其运行的环境,这可以是外部域名或内部 DNS 名称。

  • svc是一个部分,即使在 Pod DNS 名称中也会出现 - 因此我们可以假设它会在那里。但是,正如您很快会看到的,您通常不会通过它们的 FQDN 访问 Pod 或服务。

  • my-namespace相当容易理解。DNS 名称的这一部分将是您的 Pod 所在的命名空间。

  • my-subdomain对应于 Pod 规范中的subdomain字段。这个字段是完全可选的。

  • 最后,my-hostname将设置为 Pod 在 Pod 元数据中的名称。

总的来说,这个 DNS 名称允许集群中的其他资源访问特定的 Pod。这通常本身并不是很有用,特别是如果您正在使用通常有多个 Pod 的部署和有状态集。这就是服务的用武之地。

让我们来看看服务的 A 记录 DNS 名称:

my-svc.my-namespace.svc.cluster-domain.example

正如您所看到的,这与 Pod DNS 名称非常相似,不同之处在于我们在命名空间左侧只有一个值 - 就是服务名称(与 Pod 一样,这是基于元数据名称生成的)。

这些 DNS 名称的处理方式的一个结果是,在命名空间内,您可以仅通过其服务(或 Pod)名称和子域访问服务或 Pod。

例如,以前的服务 DNS 名称。在my-namespace命名空间内,可以通过 DNS 名称my-svc简单地访问服务。在my-namespace之外,可以通过my-svc.my-namespace访问服务。

现在我们已经了解了集群内 DNS 的工作原理,我们可以讨论这如何转化为服务代理。

服务代理类型

服务,尽可能简单地解释,提供了一个将请求转发到一个或多个运行应用程序的 Pod 的抽象。

创建服务时,我们定义了一个选择器,告诉服务将请求转发到哪些 Pod。通过kube-proxy组件的功能,当请求到达服务时,它们将被转发到与服务选择器匹配的各个 Pod。

在 Kubernetes 中,有三种可能的代理模式:

  • 用户空间代理模式:最古老的代理模式,自 Kubernetes 版本 1.0 以来可用。这种代理模式将以轮询方式将请求转发到匹配的 Pod。

  • Iptables 代理模式:自 1.1 版本以来可用,并且自 1.2 版本以来是默认选项。这比用户空间模式的开销要低,并且可以使用轮询或随机选择。

  • IPVS 代理模式:自 1.8 版本以来提供的最新选项。此代理模式允许其他负载平衡选项(不仅仅是轮询):

a. 轮询

b. 最少连接(最少数量的打开连接)

c. 源哈希

d. 目标哈希

e. 最短预期延迟

f. 从不排队

与此列表相关的是对轮询负载均衡的讨论,对于那些不熟悉的人。

轮询负载均衡涉及循环遍历潜在的服务端点列表,每个网络请求一次。以下图表显示了这个过程的简化视图,它与 Kubernetes 服务后面的 Pod 相关:

图 5.1 - 服务负载均衡到 Pods

图 5.1 - 服务负载均衡到 Pods

正如您所看到的,服务会交替将请求发送到不同的 Pod。第一个请求发送到 Pod A,第二个发送到 Pod B,第三个发送到 Pod C,然后循环。现在我们知道服务实际上如何处理请求了,让我们来回顾一下主要类型的服务,从 ClusterIP 开始。

实现 ClusterIP

ClusterIP 是在集群内部公开的一种简单类型的服务。这种类型的服务无法从集群外部访问。让我们来看看我们服务的 YAML 文件:

clusterip-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
Spec:
  type: ClusterIP
  selector:
    app: web-application
    environment: staging
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080

与其他 Kubernetes 资源一样,我们有我们的元数据块和我们的name值。正如您可以从我们关于 DNS 的讨论中回忆起来,这个name值是您如何可以从集群中的其他地方访问您的服务的。因此,ClusterIP 是一个很好的选择,适用于只需要被集群内其他 Pod 访问的服务。

接下来,我们有我们的Spec,它由三个主要部分组成:

  • 首先,我们有我们的type,它对应于我们服务的类型。由于默认类型是ClusterIP,如果您想要一个 ClusterIP 服务,实际上不需要指定类型。

  • 接下来,我们有我们的selector。我们的selector由键值对组成,必须与相关 Pod 的元数据中的标签匹配。在这种情况下,我们的服务将寻找具有app=web-applicationenvironment=staging标签的 Pod 来转发流量。

  • 最后,我们有我们的ports块,我们可以将服务上的端口映射到我们 Pod 上的targetPort号码。在这种情况下,我们服务上的端口80(HTTP 端口)将映射到我们应用程序 Pod 上的端口8080。我们的服务可以打开多个端口,但在打开多个端口时,name字段是必需的。

接下来,让我们深入审查protocol选项,因为这些对我们讨论服务端口很重要。

协议

在我们之前的 ClusterIP 服务的情况下,我们选择了TCP作为我们的协议。截至目前(截至版本 1.19),Kubernetes 支持多种协议:

  • TCP

  • UDP

  • HTTP

  • PROXY

  • SCTP

这是一个新功能可能会出现的领域,特别是涉及 HTTP(L7)服务的地方。目前,在不同环境或云提供商中,并不完全支持所有这些协议。

重要提示

有关更多信息,您可以查看主要的 Kubernetes 文档(kubernetes.io/docs/concepts/services-networking/service/)了解当前服务协议的状态。

现在我们已经讨论了 Cluster IP 的服务 YAML 的具体内容,我们可以继续下一个类型的服务 - NodePort。

使用 NodePort

NodePort 是一种面向外部的服务类型,这意味着它实际上可以从集群外部访问。创建 NodePort 服务时,将自动创建同名的 ClusterIP 服务,并由 NodePort 路由到,因此您仍然可以从集群内部访问服务。这使 NodePort 成为在无法或不可能使用 LoadBalancer 服务时外部访问应用程序的良好选择。

NodePort 听起来像它的名字 - 这种类型的服务在集群中的每个节点上打开一个可以访问服务的端口。这个端口默认在30000-32767之间,并且在服务创建时会自动链接。

以下是我们的 NodePort 服务 YAML 的样子:

NodePort 服务.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
Spec:
  type: NodePort
  selector:
    app: web-application
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080

正如您所看到的,与 ClusterIP 服务唯一的区别是服务类型 - 然而,重要的是要注意,我们在“端口”部分中的预期端口80只有在访问自动创建的 ClusterIP 版本的服务时才会被使用。从集群外部,我们需要查看生成的端口链接以访问我们的节点 IP 上的服务。

为了做到这一点,我们可以使用以下命令创建我们的服务:

kubectl apply -f svc.yaml 

然后运行这个命令:

kubectl describe service my-svc

上述命令的结果将是以下输出:

Name:                   my-svc
Namespace:              default
Labels:                 app=web-application
Annotations:            <none>
Selector:               app=web-application
Type:                   NodePort
IP:                     10.32.0.8
Port:                   <unset> 8080/TCP
TargetPort:             8080/TCP
NodePort:               <unset> 31598/TCP
Endpoints:              10.200.1.3:8080,10.200.1.5:8080
Session Affinity:       None
Events:                 <none>

从这个输出中,我们看NodePort行,看到我们为这个服务分配的端口是31598。因此,这个服务可以在任何节点上通过[NODE_IP]:[ASSIGNED_PORT]访问。

或者,我们可以手动为服务分配一个 NodePort IP。手动分配 NodePort 的 YAML 如下:

手动 NodePort 服务.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
Spec:
  type: NodePort
  selector:
    app: web-application
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 31233

正如您所看到的,我们选择了一个在30000-32767范围内的nodePort,在这种情况下是31233。要确切地了解这个 NodePort 服务在节点之间是如何工作的,请看下面的图表:

图 5.2 - NodePort 服务

图 5.2 - NodePort 服务

正如您所看到的,虽然服务可以在集群中的每个节点(节点 A、节点 B 和节点 C)访问,但网络请求仍然在所有节点的 Pod(Pod A、Pod B 和 Pod C)之间进行负载均衡,而不仅仅是访问的节点。这是确保应用程序可以从任何节点访问的有效方式。然而,在使用云服务时,您已经有了一系列工具来在服务器之间分发请求。下一个类型的服务,LoadBalancer,让我们在 Kubernetes 的上下文中使用这些工具。

设置 LoadBalancer 服务

LoadBalancer 是 Kubernetes 中的特殊服务类型,根据集群运行的位置提供负载均衡器。例如,在 AWS 中,Kubernetes 将提供弹性负载均衡器。

重要提示

有关 LoadBalancer 服务和配置的完整列表,请查阅 Kubernetes 服务文档,网址为kubernetes.io/docs/concepts/services-networking/service/#loadbalancer

ClusterIP或 NodePort 不同,我们可以以特定于云的方式修改 LoadBalancer 服务的功能。通常,这是通过服务 YAML 文件中的注释块完成的-正如我们之前讨论的那样,它只是一组键和值。要了解如何在 AWS 中完成此操作,让我们回顾一下 LoadBalancer 服务的规范:

loadbalancer-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws.. 
spec:
  type: LoadBalancer
  selector:
    app: web-application
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080

虽然我们可以创建没有任何注释的 LoadBalancer,但是支持的 AWS 特定注释使我们能够(如前面的 YAML 代码所示)指定要附加到我们的负载均衡器的 TLS 证书(通过其在 Amazon 证书管理器中的 ARN)。AWS 注释还允许配置负载均衡器的日志等。

以下是 AWS 云提供商支持的一些关键注释,截至本书编写时:

  • service.beta.kubernetes.io/aws-load-balancer-ssl-cert

  • service.beta.kubernetes.io/aws-load-balancer-proxy-protocol

  • service.beta.kubernetes.io/aws-load-balancer-ssl-ports

重要提示

有关所有提供商的注释和解释的完整列表可以在官方 Kubernetes 文档的云提供商页面上找到,网址为kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/

最后,通过 LoadBalancer 服务,我们已经涵盖了您可能最常使用的服务类型。但是,对于服务本身在 Kubernetes 之外运行的特殊情况,我们可以使用另一种服务类型:ExternalName。

创建 ExternalName 服务

类型为 ExternalName 的服务可用于代理实际未在集群上运行的应用程序,同时仍保持服务作为可以随时更新的抽象层。

让我们来设定场景:你有一个在 Azure 上运行的传统生产应用程序,你希望从集群内部访问它。你可以在myoldapp.mydomain.com上访问这个传统应用程序。然而,你的团队目前正在将这个应用程序容器化,并在 Kubernetes 上运行它,而这个新版本目前正在你的dev命名空间环境中在你的集群上运行。

与其要求你的其他应用程序根据环境对不同的地方进行通信,你可以始终在你的生产(prod)和开发(dev)命名空间中都指向一个名为my-svc的 Service。

dev中,这个 Service 可以是一个指向你的新容器化应用程序的 Pods 的ClusterIP Service。以下 YAML 显示了开发中的容器化 Service 应该如何工作:

clusterip-for-external-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
  namespace: dev
Spec:
  type: ClusterIP
  selector:
    app: newly-containerized-app
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080

prod命名空间中,这个 Service 将会是一个ExternalName Service:

externalname-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-svc
  namespace: prod
spec:
  type: ExternalName
  externalName: myoldapp.mydomain.com

由于我们的ExternalName Service 实际上并不转发请求到 Pods,所以我们不需要一个选择器。相反,我们指定一个ExternalName,这是我们希望 Service 指向的 DNS 名称。

以下图表显示了如何在这种模式中使用ExternalName Service:

图 5.3 - ExternalName Service 配置

图 5.3 - ExternalName Service 配置

在上图中,我们的EC2 Running Legacy Application是一个 AWS VM,不属于集群。我们的类型为ExternalNameService B将请求路由到 VM。这样,我们的Pod C(或集群中的任何其他 Pod)可以通过 ExternalName 服务的 Kubernetes DNS 名称简单地访问我们的外部传统应用程序。

通过ExternalName,我们已经完成了对所有 Kubernetes Service 类型的审查。让我们继续讨论一种更复杂的暴露应用程序的方法 - Kubernetes Ingress 资源。

配置 Ingress

正如本章开头提到的,Ingress 提供了一个将请求路由到集群中的细粒度机制。Ingress 并不取代 Services,而是通过诸如基于路径的路由等功能来增强它们。为什么这是必要的?有很多原因,包括成本。一个具有 10 个路径到ClusterIP Services 的 Ingress 比为每个路径创建一个新的 LoadBalancer Service 要便宜得多 - 而且它保持了事情简单和易于理解。

Ingress 与 Kubernetes 中的其他服务不同。仅仅创建 Ingress 本身是不会有任何作用的。您需要两个额外的组件:

  • Ingress 控制器:您可以选择许多实现,构建在诸如 Nginx 或 HAProxy 等工具上。

  • 用于预期路由的 ClusterIP 或 NodePort 服务。

首先,让我们讨论如何配置 Ingress 控制器。

Ingress 控制器

一般来说,集群不会预先配置任何现有的 Ingress 控制器。您需要选择并部署一个到您的集群中。ingress-nginx 可能是最受欢迎的选择,但还有其他几种选择 - 请参阅kubernetes.io/docs/concepts/services-networking/ingress-controllers/获取完整列表。

让我们学习如何部署 Ingress 控制器 - 为了本书的目的,我们将坚持使用由 Kubernetes 社区创建的 Nginx Ingress 控制器 ingress-nginx

安装可能因控制器而异,但对于 ingress-nginx,有两个主要部分。首先,要部署主控制器本身,请运行以下命令,具体取决于目标环境和最新的 Nginx Ingress 版本:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.41.2/deploy/static/provider/cloud/deploy.yaml

其次,我们可能需要根据我们运行的环境来配置我们的 Ingress。对于在 AWS 上运行的集群,我们可以配置 Ingress 入口点以使用我们在 AWS 中创建的弹性负载均衡器。

重要提示

要查看所有特定于环境的设置说明,请参阅 ingress-nginx 文档kubernetes.github.io/ingress-nginx/deploy/

Nginx Ingress 控制器是一组 Pod,它将在创建新的 Ingress 资源(自定义的 Kubernetes 资源)时自动更新 Nginx 配置。除了 Ingress 控制器,我们还需要一种方式将请求路由到 Ingress 控制器 - 称为入口点。

Ingress 入口点

默认的 nginx-ingress 安装还将创建一个服务,用于为 Nginx 层提供请求,此时 Ingress 规则接管。根据您配置 Ingress 的方式,这可以是一个负载均衡器或节点端口服务。在云环境中,您可能会使用云负载均衡器服务作为集群 Ingress 的入口点。

Ingress 规则和 YAML

既然我们的 Ingress 控制器已经启动并运行,我们可以开始配置我们的 Ingress 规则了。

让我们从一个简单的例子开始。我们有两个服务,service-aservice-b,我们希望通过我们的 Ingress 在不同的路径上公开它们。一旦您的 Ingress 控制器和任何相关的弹性负载均衡器被创建(假设我们在 AWS 上运行),让我们首先通过以下步骤来创建我们的服务:

  1. 首先,让我们看看如何在 YAML 中创建服务 A。让我们将文件命名为service-a.yaml

service-a.yaml

apiVersion: v1
kind: Service
metadata:
  name: service-a
Spec:
  type: ClusterIP
  selector:
    app: application-a
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
  1. 您可以通过运行以下命令来创建我们的服务 A:
kubectl apply -f service-a.yaml
  1. 接下来,让我们创建我们的服务 B,其 YAML 代码看起来非常相似:
apiVersion: v1
kind: Service
metadata:
  name: service-b
Spec:
  type: ClusterIP
  selector:
    app: application-b
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8000
  1. 通过运行以下命令来创建我们的服务 B:
kubectl apply -f service-b.yaml
  1. 最后,我们可以为每个路径创建 Ingress 规则。以下是我们的 Ingress 的 YAML 代码,根据基于路径的路由规则,将根据需要拆分请求:

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-first-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: my.application.com
    http:
      paths:
      - path: /a
        backend:
          serviceName: service-a
          servicePort: 80
      - path: /b
        backend:
          serviceName: service-b
          servicePort: 80

在我们之前的 YAML 中,ingress 有一个单一的host值,这对应于通过 Ingress 传入的流量的主机请求头。然后,我们有两个路径,/a/b,它们分别指向我们之前创建的两个ClusterIP服务。为了将这个配置以图形的形式呈现出来,让我们看一下下面的图表:

图 5.4 - Kubernetes Ingress 示例

图 5.4 - Kubernetes Ingress 示例

正如您所看到的,我们简单的基于路径的规则导致网络请求直接路由到正确的 Pod。这是因为nginx-ingress使用服务选择器来获取 Pod IP 列表,但不直接使用服务与 Pod 通信。相反,Nginx(在这种情况下)配置会在新的 Pod IP 上线时自动更新。

host值实际上并不是必需的。如果您将其省略,那么通过 Ingress 传入的任何流量,无论主机头如何(除非它匹配指定主机的不同规则),都将根据规则进行路由。以下的 YAML 显示了这一点:

ingress-no-host.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-first-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
   - http:
      paths:
      - path: /a
        backend:
          serviceName: service-a
          servicePort: 80
      - path: /b
        backend:
          serviceName: service-b
          servicePort: 80

这个先前的 Ingress 定义将流量流向基于路径的路由规则,即使没有主机头值。

同样,也可以根据主机头将流量分成多个独立的分支路径,就像这样:

ingress-branching.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multiple-branches-ingress
spec:
  rules:
  - host: my.application.com
    http:
      paths:
      - backend:
          serviceName: service-a
          servicePort: 80
  - host: my.otherapplication.com
    http:
      paths:
      - backend:
          serviceName: service-b
          servicePort: 80

最后,在许多情况下,您还可以使用 TLS 来保护您的 Ingress,尽管这个功能在每个 Ingress 控制器的基础上有所不同。对于 Nginx,可以使用 Kubernetes Secret 来实现这一点。我们将在下一章介绍这个功能,但现在,请查看 Ingress 端的配置:

ingress-secure.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secured-ingress
spec:
  tls:
  - hosts:
    - my.application.com
    secretName: my-tls-secret
  rules:
    - host: my.application.com
      http:
        paths:
        - path: /
          backend:
            serviceName: service-a
            servicePort: 8080

此配置将查找名为my-tls-secret的 Kubernetes Secret,以附加到 Ingress 以进行 TLS。

这结束了我们对 Ingress 的讨论。Ingress 的许多功能可能取决于您决定使用的 Ingress 控制器,因此请查看您选择的实现的文档。

摘要

在本章中,我们回顾了 Kubernetes 提供的各种方法,以便将在集群上运行的应用程序暴露给外部世界。主要方法是服务和 Ingress。在服务中,您可以使用 ClusterIP 服务进行集群内路由,使用 NodePort 直接通过节点上的端口访问服务。LoadBalancer 服务允许您使用现有的云负载均衡系统,而 ExternalName 服务允许您将请求路由到集群外部的资源。

最后,Ingress 提供了一个强大的工具,可以通过路径在集群中路由请求。要实现 Ingress,您需要在集群上安装第三方或开源 Ingress 控制器。

在下一章中,我们将讨论如何使用 ConfigMap 和 Secret 两种资源类型将配置信息注入到在 Kubernetes 上运行的应用程序中。

问题

  1. 对于仅在集群内部访问的应用程序,您会使用哪种类型的服务?

  2. 您如何确定 NodePort 服务正在使用哪个端口?

  3. 为什么 Ingress 比纯粹的服务更具成本效益?

  4. 除了支持传统应用程序外,在云平台上 ExternalName 服务可能有什么用处?

进一步阅读

第六章:Kubernetes 应用程序配置

本章描述了 Kubernetes 提供的主要配置工具。我们将首先讨论一些将配置注入到容器化应用程序中的最佳实践。接下来,我们将讨论 ConfigMaps,这是 Kubernetes 旨在为应用程序提供配置数据的资源。最后,我们将介绍 Secrets,这是一种安全的方式,用于存储和提供敏感数据给在 Kubernetes 上运行的应用程序。总的来说,本章应该为您提供一个很好的工具集,用于在 Kubernetes 上配置生产应用程序。

在本章中,我们将涵盖以下主题:

  • 使用最佳实践配置容器化应用程序

  • 实施 ConfigMaps

  • 使用 Secrets

技术要求

为了运行本章详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个正常运行的 Kubernetes 集群。请查看第一章与 Kubernetes 通信,以找到快速启动和运行 Kubernetes 的几种方法,并获取有关如何安装kubectl工具的说明。

本章中使用的代码可以在书籍的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter6

使用最佳实践配置容器化应用程序

到目前为止,我们知道如何有效地部署(如第四章中所述,扩展和部署您的应用程序)和暴露(如第五章中所述,服务和入口 - 与外部世界通信)Kubernetes 上的容器化应用程序。这已足以在 Kubernetes 上运行非平凡的无状态容器化应用程序。然而,Kubernetes 还提供了用于应用程序配置和 Secrets 管理的额外工具。

由于 Kubernetes 运行容器,您始终可以配置应用程序以使用嵌入到 Dockerfile 中的环境变量。但这有些绕过了像 Kubernetes 这样的编排器的一些真正价值。我们希望能够在不重建 Docker 镜像的情况下更改我们的应用程序容器。为此,Kubernetes 为我们提供了两个以配置为重点的资源:ConfigMaps 和 Secrets。让我们首先看一下 ConfigMaps。

理解 ConfigMaps

在生产环境中运行应用程序时,开发人员希望能够快速、轻松地注入应用程序配置信息。有许多模式可以做到这一点 - 从使用查询的单独配置服务器,到使用环境变量或环境文件。这些策略在提供的安全性和可用性上有所不同。

对于容器化应用程序来说,环境变量通常是最简单的方法 - 但以安全的方式注入这些变量可能需要额外的工具或脚本。在 Kubernetes 中,ConfigMap 资源让我们以灵活、简单的方式做到这一点。ConfigMaps 允许 Kubernetes 管理员指定和注入配置信息,可以是文件或环境变量。

对于诸如秘密密钥之类的高度敏感信息,Kubernetes 为我们提供了另一个类似的资源 - Secrets。

理解 Secrets

Secrets 指的是需要以稍微更安全的方式存储的额外应用程序配置项 - 例如,受限 API 的主密钥、数据库密码等。Kubernetes 提供了一个称为 Secret 的资源,以编码方式存储应用程序配置信息。这并不会本质上使 Secret 更安全,但 Kubernetes 通过不自动在kubectl getkubectl describe命令中打印秘密信息来尊重秘密的概念。这可以防止秘密意外打印到日志中。

为了确保 Secrets 实际上是秘密的,必须在集群上启用对秘密数据的静态加密 - 我们将在本章后面讨论如何做到这一点。从 Kubernetes 1.13 开始,这个功能让 Kubernetes 管理员可以防止 Secrets 未加密地存储在etcd中,并限制对etcd管理员的访问。

在我们深入讨论 Secrets 之前,让我们先讨论一下 ConfigMaps,它们更适合非敏感信息。

实施 ConfigMaps

ConfigMaps 为在 Kubernetes 上运行的容器存储和注入应用程序配置数据提供了一种简单的方式。

创建 ConfigMap 很简单 - 它们可以实现两种注入应用程序配置数据的可能性:

  • 作为环境变量注入

  • 作为文件注入

虽然第一种选项仅仅是在内存中使用容器环境变量,但后一种选项涉及到一些卷的方面 - 一种 Kubernetes 存储介质,将在下一章中介绍。我们现在将简要回顾一下,并将其用作卷的介绍,这将在下一章第七章中进行扩展,Kubernetes 上的存储

在处理 ConfigMaps 时,使用命令式的Kubectl命令创建它们可能更容易。创建 ConfigMaps 的方法有几种,这也导致了从 ConfigMap 本身存储和访问数据的方式上的差异。第一种方法是简单地从文本值创建它,接下来我们将看到。

从文本值

通过命令从文本值创建 ConfigMap 的方法如下:

kubectl create configmap myapp-config --from-literal=mycategory.mykey=myvalue 

上一个命令创建了一个名为myapp-configconfigmap,其中包含一个名为mycategory.mykey的键,其值为myvalue。您也可以创建一个具有多个键和值的 ConfigMap,如下所示:

kubectl create configmap myapp-config2 --from-literal=mycategory.mykey=myvalue
--from-literal=mycategory.mykey2=myvalue2 

上述命令会在data部分中生成一个具有两个值的 ConfigMap。

要查看您的 ConfigMap 的样子,请运行以下命令:

kubectl get configmap myapp-config2

您将看到以下输出:

configmap-output.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config2
  namespace: default
data:
  mycategory.mykey: myvalue
  mycategory.mykey2: myvalue2

当您的 ConfigMap 数据很长时,直接从文本值创建它就没有太多意义。对于更长的配置,我们可以从文件创建我们的 ConfigMap。

从文件

为了更容易创建一个具有许多不同值的 ConfigMap,或者重用您已经拥有的环境文件,您可以按照以下步骤从文件创建一个 ConfigMap:

  1. 让我们从创建我们的文件开始,我们将把它命名为env.properties
myconfigid=1125
publicapikey=i38ahsjh2
  1. 然后,我们可以通过运行以下命令来创建我们的 ConfigMap:
kubectl create configmap my-config-map --from-file=env.properties
  1. 要检查我们的kubectl create命令是否正确创建了 ConfigMap,让我们使用kubectl describe来描述它:
kubectl describe configmaps my-config-map

这应该会产生以下输出:

Name:           my-config-map
Namespace:      default
Labels:         <none>
Annotations:    <none>
Data
====
env.properties:        39 bytes

正如你所看到的,这个 ConfigMap 包含了我们的文本文件(以及字节数)。在这种情况下,我们的文件可以是任何文本文件 - 但是如果你知道你的文件被格式化为环境文件,你可以让 Kubernetes 知道这一点,以便让你的 ConfigMap 更容易阅读。让我们学习如何做到这一点。

从环境文件

如果我们知道我们的文件格式化为普通的环境文件与键值对,我们可以使用稍微不同的方法来创建我们的 ConfigMap-环境文件方法。这种方法将使我们的数据在 ConfigMap 对象中更加明显,而不是隐藏在文件中。

让我们使用与之前相同的文件进行环境特定的创建:

kubectl create configmap my-env-config-map --from-env-file=env.properties

现在,让我们使用以下命令描述我们的 ConfigMap:

> kubectl describe configmaps my-env-config-map

我们得到以下输出:

Name:         my-env-config-map
Namespace:    default
Labels:       <none>
Annotations:  <none>
Data
====
myconfigid:
----
1125
publicapikey:
----
i38ahsjh2
Events:  <none>

如您所见,通过使用-from-env-file方法,当您运行kubectl describe时,env文件中的数据很容易查看。这也意味着我们可以直接将我们的 ConfigMap 挂载为环境变量-稍后会详细介绍。

将 ConfigMap 挂载为卷

要在 Pod 中使用 ConfigMap 中的数据,您需要在规范中将其挂载到 Pod 中。这与在 Kubernetes 中挂载卷的方式非常相似(出于很好的原因,我们将会发现),卷是提供存储的资源。但是现在,不要担心卷。

让我们来看看我们的 Pod 规范,它将我们的my-config-map ConfigMap 作为卷挂载到我们的 Pod 上:

pod-mounting-cm.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod-mount-cm
spec:
  containers:
    - name: busybox
      image: busybox
      command:
      - sleep
      - "3600"
      volumeMounts:
      - name: my-config-volume
        mountPath: /app/config
  volumes:
    - name: my-config-volume
      configMap:
        name: my-config-map
  restartPolicy: Never

如您所见,我们的my-config-map ConfigMap 被挂载为卷(my-config-volume)在/app/config路径上,以便我们的容器访问。我们将在下一章关于存储中更多了解这是如何工作的。

在某些情况下,您可能希望将 ConfigMap 挂载为容器中的环境变量-我们将在下面学习如何做到这一点。

将 ConfigMap 挂载为环境变量

您还可以将 ConfigMap 挂载为环境变量。这个过程与将 ConfigMap 挂载为卷非常相似。

让我们来看看我们的 Pod 规范:

pod-mounting-cm-as-env.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod-mount-env
spec:
  containers:
    - name: busybox
      image: busybox
      command:
      - sleep
      - "3600"
      env:
        - name: MY_ENV_VAR
          valueFrom:
            configMapKeyRef:
              name: my-env-config-map
              key: myconfigid
  restartPolicy: Never

正如您所看到的,我们不是将 ConfigMap 作为卷挂载,而是在容器环境变量MY_ENV_VAR中引用它。为了做到这一点,我们需要在valueFrom键中使用configMapRef,并引用我们的 ConfigMap 的名称以及 ConfigMap 本身内部要查看的键。

正如我们在使用最佳实践配置容器化应用程序部分的章节开头提到的,ConfigMaps 默认情况下不安全,它们的数据以明文存储。为了增加一层安全性,我们可以使用 Secrets 而不是 ConfigMaps。

使用 Secrets

Secrets 与 ConfigMaps 非常相似,不同之处在于它们以编码文本(具体来说是 Base64)而不是明文存储。

因此,创建秘密与创建 ConfigMap 非常相似,但有一些关键区别。首先,通过命令方式创建秘密将自动对秘密中的数据进行 Base64 编码。首先,让我们看看如何从一对文件中命令方式创建秘密。

从文件

首先,让我们尝试从文件创建一个秘密(这也适用于多个文件)。我们可以使用kubectl create命令来做到这一点:

> echo -n 'mysecretpassword' > ./pass.txt
> kubectl create secret generic my-secret --from-file=./pass.txt

这应该会产生以下输出:

secret "my-secret" created

现在,让我们使用kubectl describe来查看我们的秘密是什么样子的:

> kubectl describe secrets/db-user-pass

这个命令应该会产生以下输出:

Name:            my-secret
Namespace:       default
Labels:          <none>
Annotations:     <none>
Type:            Opaque
Data
====
pass.txt:    16 bytes

正如您所看到的,describe命令显示了秘密中包含的字节数,以及它的类型Opaque

创建秘密的另一种方法是使用声明性方法手动创建它。让我们看看如何做到这一点。

手动声明性方法

当从 YAML 文件声明性地创建秘密时,您需要使用编码实用程序预先对要存储的数据进行编码,例如 Linux 上的base64管道。

让我们在这里使用 Linux 的base64命令对我们的密码进行编码:

> echo -n 'myverybadpassword' | base64
bXl2ZXJ5YmFkcGFzc3dvcmQ=

现在,我们可以使用 Kubernetes YAML 规范声明性地创建我们的秘密,我们可以将其命名为secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque
data:
  dbpass: bXl2ZXJ5YmFkcGFzc3dvcmQ=

我们的secret.yaml规范包含我们创建的 Base64 编码字符串。

要创建秘密,请运行以下命令:

kubectl create -f secret.yaml

现在您知道如何创建秘密了。接下来,让我们学习如何挂载一个秘密供 Pod 使用。

将秘密挂载为卷

挂载秘密与挂载 ConfigMaps 非常相似。首先,让我们看看如何将秘密挂载到 Pod 作为卷(文件)。

让我们来看看我们的 Pod 规范。在这种情况下,我们正在运行一个示例应用程序,以便测试我们的秘密。以下是 YAML:

pod-mounting-secret.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod-mount-cm
spec:
  containers:
    - name: busybox
      image: busybox
      command:
      - sleep
      - "3600"
      volumeMounts:
      - name: my-config-volume
        mountPath: /app/config
        readOnly: true
  volumes:
    - name: foo
      secret:
      secretName: my-secret
  restartPolicy: Never

与 ConfigMap 的一个区别是,我们在卷上指定了readOnly,以防止在 Pod 运行时对秘密进行任何更改。在其他方面,我们挂载秘密的方式与 ConfigMap 相同。

接下来,我们将在下一章[第七章](B14790_07_Final_PG_ePub.xhtml#_idTextAnchor166)Kubernetes 上的存储中深入讨论卷。但简单解释一下,卷是一种向 Pod 添加存储的方式。在这个例子中,我们挂载了我们的卷,你可以把它看作是一个文件系统,到我们的 Pod 上。然后我们的秘密被创建为文件系统中的一个文件。

将秘密挂载为环境变量

类似于文件挂载,我们可以以与 ConfigMap 挂载方式相同的方式将我们的秘密作为环境变量挂载。

让我们看一下另一个 Pod YAML。在这种情况下,我们将我们的 Secret 作为环境变量挂载:

pod-mounting-secret-env.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod-mount-env
spec:
  containers:
    - name: busybox
      image: busybox
      command:
      - sleep
      - "3600"
      env:
        - name: MY_PASSWORD_VARIABLE
          valueFrom:
            secretKeyRef:
              name: my-secret
              key: dbpass
  restartPolicy: Never

在使用kubectl apply创建前面的 Pod 后,让我们运行一个命令来查看我们的 Pod,看看变量是否被正确初始化。这与docker exec的方式完全相同:

> kubectl exec -it my-pod-mount-env -- /bin/bash
> printenv MY_PASSWORD_VARIABLE
myverybadpassword

它奏效了!现在您应该对如何创建,挂载和使用 ConfigMaps 和 Secrets 有了很好的理解。

作为关于 Secrets 的最后一个主题,我们将学习如何使用 Kubernetes EncryptionConfig创建安全的加密 Secrets。

实施加密的 Secrets

一些托管的 Kubernetes 服务(包括亚马逊的弹性 Kubernetes 服务EKS))会自动加密etcd数据在静止状态下-因此您无需执行任何操作即可实现加密的 Secrets。像 Kops 这样的集群提供者有一个简单的标志(例如encryptionConfig: true)。但是,如果您是以困难的方式创建集群,您需要使用一个标志--encryption-provider-config和一个EncryptionConfig文件启动 Kubernetes API 服务器。

重要提示

从头开始创建一个完整的集群超出了本书的范围(请参阅Kubernetes The Hard Way,了解更多信息,网址为github.com/kelseyhightower/kubernetes-the-hard-way)。

要快速了解加密是如何处理的,请查看以下EncryptionConfiguration YAML,它在启动时传递给kube-apiserver

encryption-config.yaml

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aesgcm:
        keys:
        - name: key1
          secret: c2VjcmV0IGlzIHNlY3VyZQ==
        - name: key2
          secret: dGhpcyBpcyBwYXNzd29yZA==

前面的EncryptionConfiguration YAML 列出了应在etcd中加密的资源列表,以及可用于加密数据的一个或多个提供程序。截至 Kubernetes 1.17,允许以下提供程序:

  • 身份:无加密。

  • Aescbc:推荐的加密提供程序。

  • 秘密盒:比 Aescbc 更快,更新。

  • Aesgcm:请注意,您需要自己实现 Aesgcm 的密钥轮换。

  • Kms:与第三方 Secrets 存储一起使用,例如 Vault 或 AWS KMS。

要查看完整列表,请参阅 https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#providers。当列表中添加多个提供程序时,Kubernetes 将使用第一个配置的提供程序来加密对象。在解密时,Kubernetes 将按列表顺序进行解密尝试-如果没有一个有效,它将返回错误。

一旦我们创建了一个秘密(查看我们以前的任何示例如何做到这一点),并且我们的EncryptionConfig是活动的,我们可以检查我们的秘密是否实际上是加密的。

检查您的秘密是否已加密

检查您的秘密是否实际上在etcd中被加密的最简单方法是直接从etcd中获取值并检查加密前缀:

  1. 首先,让我们使用base64创建一个秘密密钥:
> echo -n 'secrettotest' | base64
c2VjcmV0dG90ZXN0
  1. 创建一个名为secret_to_test.yaml的文件,其中包含以下内容:
apiVersion: v1
kind: Secret
metadata:
 name: secret-to-test
type: Opaque
data:
  myencsecret: c2VjcmV0dG90ZXN0
  1. 创建秘密:
kubectl apply -f secret_to_test.yaml
  1. 创建了我们的秘密后,让我们检查它是否在etcd中被加密,通过直接查询它。您通常不需要经常直接查询etcd,但如果您可以访问用于引导集群的证书,这是一个简单的过程:
> export ETCDCTL_API=3 
> etcdctl --cacert=/etc/kubernetes/certs/ca.crt 
--cert=/etc/kubernetes/certs/etcdclient.crt 
--key=/etc/kubernetes/certs/etcdclient.key 
get /registry/secrets/default/secret-to-test

根据您配置的加密提供程序,您的秘密数据将以提供程序标记开头。例如,使用 Azure KMS 提供程序加密的秘密将以k8s:enc:kms:v1:azurekmsprovider开头。

  1. 现在,通过kubectl检查秘密是否被正确解密(它仍然会被编码):
> kubectl get secrets secret-to-test -o yaml

输出应该是myencsecret: c2VjcmV0dG90ZXN0,这是我们未加密的编码的秘密值:

> echo 'c2VjcmV0dG90ZXN0' | base64 --decode
> secrettotest

成功!

我们现在在我们的集群上运行加密。让我们找出如何删除它。

禁用集群加密

我们也可以相当容易地从我们的 Kubernetes 资源中删除加密。

首先,我们需要使用空白的加密配置 YAML 重新启动 Kubernetes API 服务器。如果您自行配置了集群,这应该很容易,但在 EKS 或 AKS 上,这是不可能手动完成的。您需要按照云提供商的具体文档来了解如何禁用加密。

如果您自行配置了集群或使用了诸如 Kops 或 Kubeadm 之类的工具,那么您可以使用以下EncryptionConfiguration在所有主节点上重新启动您的kube-apiserver进程:

encryption-reset.yaml

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - identity: {}

重要提示

请注意,身份提供者不需要是唯一列出的提供者,但它需要是第一个,因为正如我们之前提到的,Kubernetes 使用第一个提供者来加密etcd中的新/更新对象。

现在,我们将手动重新创建所有我们的秘密,此时它们将自动使用身份提供者(未加密):

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

此时,我们所有的秘密都是未加密的!

摘要

在本章中,我们看了 Kubernetes 提供的注入应用程序配置的方法。首先,我们看了一些配置容器化应用程序的最佳实践。然后,我们回顾了 Kubernetes 提供的第一种方法,ConfigMaps,以及创建和挂载它们到 Pod 的几个选项。最后,我们看了一下 Secrets,当它们被加密时,是处理敏感配置的更安全的方式。到目前为止,您应该已经掌握了为应用程序提供安全和不安全配置值所需的所有工具。

在下一章中,我们将深入探讨一个我们已经涉及到的主题,即挂载我们的 Secrets 和 ConfigMaps - Kubernetes 卷资源,以及更一般地说,Kubernetes 上的存储。

问题

  1. Secrets 和 ConfigMaps 之间有什么区别?

  2. Secrets 是如何编码的?

  3. 从常规文件创建 ConfigMap 和从环境文件创建 ConfigMap 之间的主要区别是什么?

  4. 如何在 Kubernetes 上确保 Secrets 的安全?为什么它们不是默认安全的?

进一步阅读

第七章:Kubernetes 上的存储

在本章中,我们将学习如何在 Kubernetes 上提供应用程序存储。我们将回顾 Kubernetes 上的两种存储资源,即卷和持久卷。卷非常适合临时数据需求,但持久卷对于在 Kubernetes 上运行任何严肃的有状态工作负载是必不可少的。通过本章学到的技能,您将能够在多种不同的方式和环境中为在 Kubernetes 上运行的应用程序配置存储。

在本章中,我们将涵盖以下主题:

  • 理解卷和持久卷之间的区别

  • 使用卷

  • 创建持久卷

  • 持久卷索赔

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书籍的 GitHub 存储库中找到:github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter7

理解卷和持久卷之间的区别

一个完全无状态的容器化应用可能只需要磁盘空间来存储容器文件本身。在运行这种类型的应用程序时,Kubernetes 不需要额外的配置。

然而,在现实世界中,这并不总是正确的。正在转移到容器中的传统应用程序可能出于许多可能的原因需要磁盘空间卷。为了保存容器使用的文件,您需要 Kubernetes 卷资源。

Kubernetes 中可以创建两种主要存储资源:

  • 持久卷

两者之间的区别在于名称:虽然卷与特定 Pod 的生命周期相关联,但持久卷会一直保持活动状态,直到被删除,并且可以在不同的 Pod 之间共享。卷可以在 Pod 内部的容器之间共享数据,而持久卷可以用于许多可能的高级目的。

让我们先看看如何实现卷。

Kubernetes 支持许多不同类型的卷。大多数可以用于卷或持久卷,但有些是特定于资源的。我们将从最简单的开始,然后回顾一些类型。

重要提示

您可以在 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes 上查看完整的当前卷类型列表。

以下是卷子类型的简短列表:

  • awsElasticBlockStore

  • cephfs

  • ConfigMap

  • emptyDir

  • hostPath

  • local

  • nfs

  • persistentVolumeClaim

  • rbd

  • Secret

正如您所看到的,ConfigMaps 和 Secrets 实际上是卷的类型。此外,列表包括云提供商卷类型,如awsElasticBlockStore

与持久卷不同,持久卷是单独从任何一个 Pod 创建的,创建卷通常是在 Pod 的上下文中完成的。

要创建一个简单的卷,可以使用以下 Pod YAML:

pod-with-vol.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-vol
spec:
  containers:
  - name: busybox
    image: busybox
    volumeMounts:
    - name: my-storage-volume
      mountPath: /data
  volumes:
  - name: my-storage-volume
    emptyDir: {}

这个 YAML 将创建一个带有emptyDir类型卷的 Pod。emptyDir类型的卷是使用分配给 Pod 的节点上已经存在的存储来配置的。如前所述,卷与 Pod 的生命周期相关,而不是与其容器相关。

这意味着在具有多个容器的 Pod 中,所有容器都将能够访问卷数据。让我们看一个 Pod 的以下示例 YAML 文件:

pod-with-multiple-containers.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: busybox
    image: busybox
    volumeMounts:
    - name: config-volume
      mountPath: /shared-config
  - name: busybox2
    image: busybox
    volumeMounts:
    - name: config-volume
      mountPath: /myconfig
  volumes:
  - name: config-volume
    emptyDir: {}

在这个例子中,Pod 中的两个容器都可以访问卷数据,尽管路径不同。容器甚至可以通过共享卷中的文件进行通信。

规范的重要部分是volume spec本身(volumes下的列表项)和卷的mountvolumeMounts下的列表项)。

每个挂载项都包含一个名称,对应于volumes部分中卷的名称,以及一个mountPath,它将决定卷被挂载到容器上的哪个文件路径。例如,在前面的 YAML 中,卷config-volume将在busybox Pod 中的/shared-config处访问,在busybox2 Pod 中的/myconfig处访问。

卷规范本身需要一个名称 - 在本例中是my-storage,以及特定于卷类型的其他键/值,本例中是emptyDir,只需要空括号。

现在,让我们来看一个云配置卷挂载到 Pod 的例子。例如,要挂载 AWS 弹性块存储EBS)卷,可以使用以下 YAML:

pod-with-ebs.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - image: busybox
    name: busybox
    volumeMounts:
    - mountPath: /data
      name: my-ebs-volume
  volumes:
  - name: my-ebs-volume
    awsElasticBlockStore:
      volumeID: [INSERT VOLUME ID HERE]

只要您的集群正确设置了与 AWS 的身份验证,此 YAML 将把现有的 EBS 卷附加到 Pod 上。正如您所看到的,我们使用awsElasticBlockStore键来专门配置要使用的确切卷 ID。在这种情况下,EBS 卷必须已经存在于您的 AWS 帐户和区域中。使用 AWS 弹性 Kubernetes 服务EKS)会更容易,因为它允许我们从 Kubernetes 内部自动提供 EBS 卷。

Kubernetes 还包括 Kubernetes AWS 云提供程序中的功能,用于自动提供卷-但这些是用于持久卷。我们将在持久卷部分看看如何获得这些自动提供的卷。

持久卷

持久卷相对于常规的 Kubernetes 卷具有一些关键优势。如前所述,它们(持久卷)的生命周期与集群的生命周期相关,而不是与单个 Pod 的生命周期相关。这意味着持久卷可以在集群运行时在 Pod 之间共享和重复使用。因此,这种模式更适合外部存储,比如 EBS(AWS 上的块存储服务),因为存储本身可以超过单个 Pod 的寿命。

实际上,使用持久卷需要两个资源:PersistentVolume本身和PersistentVolumeClaim,用于将PersistentVolume挂载到 Pod 上。

让我们从PersistentVolume本身开始-看一下创建PersistentVolume的基本 YAML:

pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  storageClassName: manual
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/mydata"

现在让我们来分析一下。从规范中的第一行开始-storageClassName

这个第一个配置,storageClassName,代表了我们想要使用的存储类型。对于hostPath卷类型,我们只需指定manual,但是对于 AWS EBS,例如,您可以创建并使用一个名为gp2Encrypted的存储类,以匹配 AWS 中的gp2存储类型,并启用 EBS 加密。因此,存储类是特定卷类型的可用配置的组合-可以在卷规范中引用。

继续使用我们的 AWS StorageClass示例,让我们为gp2Encrypted提供一个新的StorageClass

gp2-storageclass.yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp2Encrypted
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
  encrypted: "true"
  fsType: ext4

现在,我们可以使用gp2Encrypted存储类创建我们的PersistentVolume。但是,使用动态配置的 EBS(或其他云)卷创建PersistentVolumes有一个快捷方式。当使用动态配置的卷时,我们首先创建PersistentVolumeClaim,然后自动生成PersistentVolume

持久卷声明

现在我们知道您可以在 Kubernetes 中轻松创建持久卷,但是这并不允许您将存储绑定到 Pod。您需要创建一个PersistentVolumeClaim,它声明一个PersistentVolume并允许您将该声明绑定到一个或多个 Pod。

在上一节的新StorageClass的基础上,让我们创建一个声明,这将自动导致创建一个新的PersistentVolume,因为没有其他具有我们期望的StorageClass的持久卷:

pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: my-pv-claim
spec:
  storageClassName: gp2Encrypted
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

在这个文件上运行kubectl apply -f应该会导致创建一个新的自动生成的持久卷PV)。如果您的 AWS 云服务提供商设置正确,这将导致创建一个新的类型为 GP2 且启用加密的 EBS 卷。

在将我们的基于 EBS 的持久卷附加到我们的 Pod 之前,让我们确认 EBS 卷在 AWS 中是否正确创建。

为此,我们可以转到 AWS 控制台,并确保我们在运行 EKS 集群的相同区域。然后转到服务 > EC2,在弹性块存储下的左侧菜单中单击。在这一部分,我们应该看到一个与我们的 PVC 状态相同大小(1 GiB)的自动生成卷的项目。它应该具有 GP2 的类,并且应该启用加密。让我们看看这在 AWS 控制台中会是什么样子:

图 7.1 - AWS 控制台自动生成的 EBS 卷

图 7.1 - AWS 控制台自动生成的 EBS 卷

正如您所看到的,我们在 AWS 中正确地创建了我们动态生成的启用加密和分配gp2卷类型的 EBS 卷。现在我们已经创建了我们的卷,并且确认它已经在 AWS 中创建,我们可以将它附加到我们的 Pod 上。

将持久卷声明(PVC)附加到 Pods

现在我们既有了PersistentVolume又有了PersistentVolumeClaim,我们可以将它们附加到一个 Pod 以供使用。这个过程与附加 ConfigMap 或 Secret 非常相似 - 这是有道理的,因为 ConfigMaps 和 Secrets 本质上是卷的类型!

查看允许我们将加密的 EBS 卷附加到 Pod 并命名为pod-with-attachment.yaml的 YAML:

Pod-with-attachment.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  volumes:
    - name: my-pv
      persistentVolumeClaim:
        claimName: my-pv-claim
  containers:
    - name: my-container
      image: busybox
      volumeMounts:
        - mountPath: "/usr/data"
          name: my-pv

运行kubectl apply -f pod-with-attachment.yaml将创建一个 Pod,该 Pod 通过我们的声明将我们的PersistentVolume挂载到/usr/data

为了确认卷已成功创建,让我们exec到我们的 Pod 中,并在我们的卷被挂载的位置创建一个文件:

> kubectl exec -it shell-demo -- /bin/bash
> cd /usr/data
> touch myfile.txt

现在,让我们使用以下命令删除 Pod:

> kubectl delete pod my-pod

然后使用以下命令再次重新创建它:

> kubectl apply -f my-pod.yaml

如果我们做得对,当再次运行kubectl exec进入 Pod 时,我们应该能够看到我们的文件:

> kubectl exec -it my-pod -- /bin/bash
> ls /usr/data
> myfile.txt

成功!

我们现在知道如何为 Kubernetes 创建由云存储提供的持久卷。但是,您可能正在本地环境或使用 minikube 在笔记本电脑上运行 Kubernetes。让我们看看您可以使用的一些替代持久卷子类型。

没有云存储的持久卷

我们之前的示例假设您正在云环境中运行 Kubernetes,并且可以使用云平台提供的存储服务(如 AWS EBS 和其他服务)。然而,这并非总是可能的。您可能正在数据中心环境中运行 Kubernetes,或者在专用硬件上运行。

在这种情况下,有许多潜在的解决方案可以为 Kubernetes 提供存储。一个简单的解决方案是将卷类型更改为hostPath,它可以在节点现有的存储设备中创建持久卷。例如,在 minikube 上运行时非常适用,但是不像 AWS EBS 那样提供强大的抽象。对于具有类似云存储工具 EBS 的本地功能的工具,让我们看看如何使用 Rook 的 Ceph。有关完整的文档,请查看 Rook 文档(它也会教你 Ceph)rook.io/docs/rook/v1.3/ceph-quickstart.html

Rook 是一个流行的开源 Kubernetes 存储抽象层。它可以通过各种提供者(如 EdgeFS 和 NFS)提供持久卷。在这种情况下,我们将使用 Ceph,这是一个提供对象、块和文件存储的开源存储项目。为简单起见,我们将使用块模式。

在 Kubernetes 上安装 Rook 实际上非常简单。我们将带您从安装 Rook 到设置 Ceph 集群,最终在我们的集群上提供持久卷。

安装 Rook

我们将使用 Rook GitHub 存储库提供的典型 Rook 安装默认设置。这可能会根据用例进行高度定制,但将允许我们快速为我们的工作负载设置块存储。请参考以下步骤来完成这个过程:

  1. 首先,让我们克隆 Rook 存储库:
> git clone --single-branch --branch master https://github.com/rook/rook.git
> cd cluster/examples/kubernetes/ceph
  1. 我们的下一步是创建所有相关的 Kubernetes 资源,包括几个自定义资源定义CRDs)。我们将在后面的章节中讨论这些,但现在,请将它们视为特定于 Rook 的新 Kubernetes 资源,而不是典型的 Pods、Services 等。要创建常见资源,请运行以下命令:
> kubectl apply -f ./common.yaml
  1. 接下来,让我们启动我们的 Rook 操作员,它将处理为特定的 Rook 提供程序(在本例中将是 Ceph)提供所有必要资源的规划:
> kubectl apply -f ./operator.yaml
  1. 在下一步之前,请确保 Rook 操作员 Pod 实际上正在运行,使用以下命令:
> kubectl -n rook-ceph get pod
  1. 一旦 Rook Pod 处于“运行”状态,我们就可以设置我们的 Ceph 集群!此 YAML 也在我们从 Git 克隆的文件夹中。使用以下命令创建它:
> kubectl create -f cluster.yaml

这个过程可能需要几分钟。Ceph 集群由几种不同的 Pod 类型组成,包括操作员、对象存储设备OSDs)和管理器。

为了确保我们的 Ceph 集群正常工作,Rook 提供了一个工具箱容器映像,允许您使用 Rook 和 Ceph 命令行工具。要启动工具箱,您可以使用 Rook 项目提供的工具箱 Pod 规范,网址为rook.io/docs/rook/v0.7/toolbox.html

这是工具箱 Pod 的规范示例:

rook-toolbox-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: rook-tools
  namespace: rook
spec:
  dnsPolicy: ClusterFirstWithHostNet
  containers:
  - name: rook-tools
    image: rook/toolbox:v0.7.1
    imagePullPolicy: IfNotPresent

正如您所看到的,这个 Pod 使用了 Rook 提供的特殊容器映像。该映像预装了您需要调查 Rook 和 Ceph 的所有工具。

一旦您的工具箱 Pod 运行起来,您可以使用rookctlceph命令来检查集群状态(查看 Rook 文档以获取具体信息)。

rook-ceph-block 存储类

现在我们的集群正在运行,我们可以创建将被 PVs 使用的存储类。我们将称这个存储类为rook-ceph-block。这是我们的 YAML 文件(ceph-rook-combined.yaml),其中将包括我们的CephBlockPool(它将处理 Ceph 中的块存储 - 有关更多信息,请参阅rook.io/docs/rook/v0.9/ceph-pool-crd.html)以及存储类本身:

ceph-rook-combined.yaml

apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
    clusterID: rook-ceph
    pool: replicapool
    imageFormat: "2"
currently supports only `layering` feature.
    imageFeatures: layering
    csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
    csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
    csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
csi-provisioner
    csi.storage.k8s.io/fstype: xfs
reclaimPolicy: Delete

正如你所看到的,YAML 规范定义了我们的StorageClassCephBlockPool资源。正如我们在本章前面提到的,StorageClass是我们告诉 Kubernetes 如何满足PersistentVolumeClaim的方式。另一方面,CephBlockPool资源告诉 Ceph 如何以及在哪里创建分布式存储资源-在这种情况下,要复制多少存储。

现在我们可以给我们的 Pod 一些存储了!让我们使用我们的新存储类创建一个新的 PVC:

rook-ceph-pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: rook-pvc
spec:
  storageClassName: rook-ceph-block
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

我们的 PVC 是存储类rook-ceph-block,因此它将使用我们刚刚创建的新存储类。现在,让我们在 YAML 文件中将 PVC 分配给我们的 Pod:

rook-ceph-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-rook-test-pod
spec:
  volumes:
    - name: my-rook-pv
      persistentVolumeClaim:
        claimName: rook-pvc
  containers:
    - name: my-container
      image: busybox
      volumeMounts:
        - mountPath: "/usr/rooktest"
          name: my-rook-pv

当 Pod 被创建时,Rook 应该会启动一个新的持久卷并将其附加到 Pod 上。让我们查看一下 Pod,看看它是否正常工作:

> kubectl exec -it my-rook-test-pod -- /bin/bash
> cd /usr/rooktest
> touch myfile.txt
> ls

我们得到了以下输出:

> myfile.txt

成功!

尽管我们刚刚使用了 Ceph 的块存储功能,但它也有文件系统模式,这有一些好处-让我们讨论一下为什么你可能想要使用它。

Rook Ceph 文件系统

Rook 的 Ceph 块提供程序的缺点是一次只能由一个 Pod 进行写入。为了使用 Rook/Ceph 创建一个ReadWriteMany持久卷,我们需要使用支持 RWX 模式的文件系统提供程序。有关更多信息,请查看 Rook/Ceph 文档rook.io/docs/rook/v1.3/ceph-quickstart.html

在创建 Ceph 集群之前,所有先前的步骤都适用。在这一点上,我们需要创建我们的文件系统。让我们使用以下的 YAML 文件来创建它:

rook-ceph-fs.yaml

apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
  name: ceph-fs
  namespace: rook-ceph
spec:
  metadataPool:
    replicated:
      size: 2
  dataPools:
    - replicated:
        size: 2
  preservePoolsOnDelete: true
  metadataServer:
    activeCount: 1
    activeStandby: true

在这种情况下,我们正在复制元数据和数据到至少两个池,以确保可靠性,如在metadataPooldataPool块中配置的那样。我们还使用preservePoolsOnDelete键在删除时保留池。

接下来,让我们为 Rook/Ceph 文件系统存储专门创建一个新的存储类。以下的 YAML 文件就是这样做的:

rook-ceph-fs-storageclass.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  fsName: ceph-fs
  pool: ceph-fs-data0
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete

这个rook-cephfs存储类指定了我们之前创建的池,并描述了我们存储类的回收策略。最后,它使用了一些在 Rook/Ceph 文档中解释的注释。现在,我们可以通过 PVC 将其附加到一个部署中,而不仅仅是一个 Pod!看一下我们的 PV:

rook-cephfs-pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: rook-ceph-pvc
spec:
  storageClassName: rook-cephfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

这个持久卷引用了我们的新的rook-cephfs存储类,使用ReadWriteMany模式 - 我们要求1 Gi的数据。接下来,我们可以创建我们的Deployment

rook-cephfs-deployment.yaml

apiVersion: v1
kind: Deployment
metadata:
  name: my-rook-fs-test
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25% 
  selector:
    matchLabels:
      app: myapp
  template:
      spec:
  	  volumes:
    	  - name: my-rook-ceph-pv
        persistentVolumeClaim:
          claimName: rook-ceph-pvc
  	  containers:
    	  - name: my-container
         image: busybox
         volumeMounts:
         - mountPath: "/usr/rooktest"
           name: my-rook-ceph-pv

这个Deployment引用了我们的ReadWriteMany持久卷声明,使用volumes下的persistentVolumeClaim块。部署后,我们所有的 Pod 现在都可以读写同一个持久卷。

之后,您应该对如何创建持久卷并将它们附加到 Pod 有很好的理解。

总结

在本章中,我们回顾了在 Kubernetes 上提供存储的两种方法 - 卷和持久卷。首先,我们讨论了这两种方法之间的区别:虽然卷与 Pod 的生命周期相关,但持久卷会持续到它们或集群被删除。然后,我们看了如何实现卷并将它们附加到我们的 Pod。最后,我们将我们对卷的学习扩展到持久卷,并发现了如何使用几种不同类型的持久卷。这些技能将帮助您在许多可能的环境中为您的应用分配持久和非持久的存储 - 从本地到云端。

在下一章中,我们将从应用程序关注点中脱离出来,讨论如何在 Kubernetes 上控制 Pod 的放置。

问题

  1. 卷和持久卷之间有什么区别?

  2. 什么是StorageClass,它与卷有什么关系?

  3. 在创建 Kubernetes 资源(如持久卷)时,如何自动配置云资源?

  4. 在哪些情况下,您认为使用卷而不是持久卷会是禁止的?

进一步阅读

请参考以下链接获取更多信息:

第八章:Pod 放置控制

本章描述了在 Kubernetes 中控制 Pod 放置的各种方式,以及解释为什么首先实施这些控制可能是一个好主意。Pod 放置意味着控制 Pod 在 Kubernetes 中被调度到哪个节点。我们从简单的控制开始,比如节点选择器,然后转向更复杂的工具,比如污点和容忍度,最后介绍两个 beta 功能,节点亲和性和 Pod 间亲和性/反亲和性。

在过去的章节中,我们已经学习了如何在 Kubernetes 上最好地运行应用程序 Pod - 从使用部署协调和扩展它们,使用 ConfigMaps 和 Secrets 注入配置,到使用持久卷添加存储。

然而,尽管如此,我们始终依赖 Kubernetes 调度程序将 Pod 放置在最佳节点上,而没有给调度程序提供有关所讨论的 Pod 的太多信息。到目前为止,我们已经在 Pod 中添加了资源限制和请求(Pod 规范中的resource.requestsresource.limits)。资源请求指定 Pod 在调度时需要的节点上的最低空闲资源水平,而资源限制指定 Pod 允许使用的最大资源量。然而,我们并没有对 Pod 必须运行在哪些节点或节点集上提出任何具体要求。

对于许多应用程序和集群来说,这是可以的。然而,正如我们将在第一节中看到的,有许多情况下使用更精细的 Pod 放置控制是一种有用的策略。

在本章中,我们将涵盖以下主题:

  • 识别 Pod 放置的用例

  • 使用节点选择器

  • 实施污点和容忍度

  • 使用节点亲和性控制 Pod

  • 使用 Pod 亲和性和反亲和性

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书的 GitHub 存储库中找到github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter8

识别 Pod 放置的用例

Pod 放置控制是 Kubernetes 提供给我们的工具,用于决定将 Pod 调度到哪个节点,或者由于缺少我们想要的节点而完全阻止 Pod 的调度。这可以用于几种不同的模式,但我们将回顾一些主要的模式。首先,Kubernetes 本身默认完全实现了 Pod 放置控制-让我们看看如何实现。

Kubernetes 节点健康放置控制

Kubernetes 使用一些默认的放置控制来指定某种方式不健康的节点。这些通常是使用污点和容忍来定义的,我们将在本章后面详细讨论。

Kubernetes 使用的一些默认污点(我们将在下一节中讨论)如下:

  • memory-pressure

  • disk-pressure

  • unreachable

  • not-ready

  • out-of-disk

  • network-unavailable

  • unschedulable

  • uninitialized(仅适用于由云提供商创建的节点)

这些条件可以将节点标记为无法接收新的 Pod,尽管调度器在处理这些污点的方式上有一定的灵活性,我们稍后会看到。这些系统创建的放置控制的目的是防止不健康的节点接收可能无法正常运行的工作负载。

除了用于节点健康的系统创建的放置控制之外,还有一些用例,您作为用户可能希望实现精细调度,我们将在下一节中看到。

需要不同节点类型的应用程序

在异构的 Kubernetes 集群中,每个节点并不相同。您可能有一些更强大的虚拟机(或裸金属)和一些较弱的,或者有不同的专门的节点集。

例如,在运行数据科学流水线的集群中,您可能有具有 GPU 加速能力的节点来运行深度学习算法,常规计算节点来提供应用程序,具有大量内存的节点来基于已完成的模型进行推理,等等。

使用 Pod 放置控制,您可以确保平台的各个部分在最适合当前任务的硬件上运行。

需要特定数据合规性的应用程序

与前面的例子类似,应用程序要求可能决定了对不同类型的计算需求,某些数据合规性需求可能需要特定类型的节点。

例如,像 AWS 和 Azure 这样的云提供商通常允许您购买具有专用租户的 VM - 这意味着没有其他应用程序在底层硬件和虚拟化程序上运行。这与其他典型的云提供商 VM 不同,其他客户可能共享单个物理机。

对于某些数据法规,需要这种专用租户级别来保持合规性。为了满足这种需求,您可以使用 Pod 放置控件来确保相关应用仅在具有专用租户的节点上运行,同时通过在更典型的 VM 上运行控制平面来降低成本。

多租户集群

如果您正在运行一个具有多个租户的集群(例如通过命名空间分隔),您可以使用 Pod 放置控件来为租户保留某些节点或节点组,以便将它们与集群中的其他租户物理或以其他方式分开。这类似于 AWS 或 Azure 中的专用硬件的概念。

多个故障域

尽管 Kubernetes 已经通过允许您在多个节点上运行工作负载来提供高可用性,但也可以扩展这种模式。我们可以创建自己的 Pod 调度策略,考虑跨多个节点的故障域。处理这个问题的一个很好的方法是通过 Pod 或节点的亲和性或反亲和性特性,我们将在本章后面讨论。

现在,让我们构想一个情况,我们的集群在裸机上,每个物理机架有 20 个节点。如果每个机架都有自己的专用电源连接和备份,它可以被视为一个故障域。当电源连接失败时,机架上的所有机器都会失败。因此,我们可能希望鼓励 Kubernetes 在不同的机架/故障域上运行两个实例或 Pod。以下图显示了应用程序如何跨故障域运行:

图 8.1 - 故障域

图 8.1 - 故障域

正如您在图中所看到的,由于应用程序 Pod 分布在多个故障域中,而不仅仅是在同一故障域中的多个节点,即使故障域 1发生故障,我们也可以保持正常运行。App A - Pod 1App B - Pod 1位于同一个(红色)故障域。但是,如果该故障域(Rack 1)发生故障,我们仍将在Rack 2上有每个应用的副本。

我们在这里使用“鼓励”这个词,因为在 Kubernetes 调度程序中,可以将一些功能配置为硬性要求或尽力而为。

这些示例应该让您对高级放置控件的一些潜在用例有一个扎实的理解。

现在让我们讨论实际的实现,逐个使用每个放置工具集。我们将从最简单的节点选择器开始。

使用节点选择器和节点名称

节点选择器是 Kubernetes 中一种非常简单的放置控制类型。每个 Kubernetes 节点都可以在元数据块中带有一个或多个标签,并且 Pod 可以指定一个节点选择器。

要为现有节点打标签,您可以使用kubectl label命令:

> kubectl label nodes node1 cpu_speed=fast

在这个例子中,我们使用标签cpu_speed和值fast来标记我们的node1节点。

现在,让我们假设我们有一个应用程序,它确实需要快速的 CPU 周期才能有效地执行。我们可以为我们的工作负载添加nodeSelector,以确保它只被调度到具有我们快速 CPU 速度标签的节点上,如下面的代码片段所示:

pod-with-node-selector.yaml

apiVersion: v1
kind: Pod
metadata:
  name: speedy-app
spec:
  containers:
  - name: speedy-app
    image: speedy-app:latest
    imagePullPolicy: IfNotPresent
  nodeSelector:
    cpu_speed: fast

当部署时,作为部署的一部分或单独部署,我们的speedy-app Pod 将只被调度到具有cpu_speed标签的节点上。

请记住,与我们即将审查的一些其他更高级的 Pod 放置选项不同,节点选择器中没有任何余地。如果没有具有所需标签的节点,应用程序将根本不会被调度。

对于更简单(但更脆弱)的选择器,您可以使用nodeName,它指定 Pod 应该被调度到的确切节点。您可以像这样使用它:

pod-with-node-name.yaml

apiVersion: v1
kind: Pod
metadata:
  name: speedy-app
spec:
  containers:
  - name: speedy-app
    image: speedy-app:latest
    imagePullPolicy: IfNotPresent
  nodeName: node1

正如您所看到的,这个选择器只允许 Pod 被调度到node1,所以如果它当前由于任何原因不接受 Pods,Pod 将不会被调度。

对于稍微更加微妙的放置控制,让我们转向污点和容忍。

实施污点和容忍

在 Kubernetes 中,污点和容忍的工作方式类似于反向节点选择器。与节点吸引 Pods 因具有适当的标签而被选择器消耗不同,我们对节点进行污点处理,这会排斥所有 Pod 被调度到该节点,然后标记我们的 Pods 具有容忍,这允许它们被调度到被污点处理的节点上。

正如本章开头提到的,Kubernetes 使用系统创建的污点来标记节点为不健康,并阻止新的工作负载被调度到它们上面。例如,out-of-disk污点将阻止任何新的 Pod 被调度到具有该污点的节点上。

让我们使用污点和容忍度来应用与节点选择器相同的示例用例。由于这基本上是我们先前设置的反向,让我们首先使用kubectl taint命令给我们的节点添加一个污点:

> kubectl taint nodes node2 cpu_speed=slow:NoSchedule

让我们分解这个命令。我们给node2添加了一个名为cpu_speed的污点和一个值slow。我们还用一个效果标记了这个污点 - 在这种情况下是NoSchedule

一旦我们完成了我们的示例(如果您正在跟随命令进行操作,请不要立即执行此操作),我们可以使用减号运算符删除taint

> kubectl taint nodes node2 cpu_speed=slow:NoSchedule-

taint效果让我们在调度器处理污点时增加了一些细粒度。有三种可能的效果值:

  • NoSchedule

  • NoExecute

  • PreferNoSchedule

前两个效果,NoScheduleNoExecute,提供了硬效果 - 也就是说,像节点选择器一样,只有两种可能性,要么 Pod 上存在容忍度(我们马上就会看到),要么 Pod 没有被调度。NoExecute通过驱逐所有具有容忍度的节点上的 Pod 来增加这个基本功能,而NoSchedule让现有的 Pod 保持原状,同时阻止任何没有容忍度的新 Pod 加入。

PreferNoSchedule,另一方面,为 Kubernetes 调度器提供了一些余地。它告诉调度器尝试为没有不可容忍污点的 Pod 找到一个节点,但如果不存在,则继续安排它。它实现了软效果。

在我们的情况下,我们选择了NoSchedule,因此不会将新的 Pod 分配给该节点 - 除非当然我们提供了一个容忍度。现在让我们这样做。假设我们有第二个应用程序,它不关心 CPU 时钟速度。它很乐意生活在我们较慢的节点上。这是 Pod 清单:

pod-without-speed-requirement.yaml

apiVersion: v1
kind: Pod
metadata:
  name: slow-app
spec:
  containers:
  - name: slow-app
    image: slow-app:latest

现在,我们的slow-app Pod 将不会在任何具有污点的节点上运行。我们需要为这个 Pod 提供一个容忍度,以便它可以被调度到具有污点的节点上 - 我们可以这样做:

pod-with-toleration.yaml

apiVersion: v1
kind: Pod
metadata:
  name: slow-app
spec:
  containers:
  - name: slow-app
    image: slow-app:latest
tolerations:
- key: "cpu_speed"
  operator: "Equal"
  value: "slow"
  effect: "NoSchedule"

让我们分解我们的tolerations条目,这是一个值数组。每个值都有一个key-与我们的污点名称相同。然后是一个operator值。这个operator可以是EqualExists。对于Equal,您可以使用value键,就像前面的代码中那样,配置污点必须等于的值,以便 Pod 容忍。对于Exists,污点名称必须在节点上,但不管值是什么都没有关系,就像这个 Pod 规范中一样:

pod-with-toleration2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: slow-app
spec:
  containers:
  - name: slow-app
    image: slow-app:latest
tolerations:
- key: "cpu_speed"
  operator: "Exists"
  effect: "NoSchedule"

如您所见,我们已经使用了Exists operator值来允许我们的 Pod 容忍任何cpu_speed污点。

最后,我们有我们的effect,它的工作方式与污点本身的effect相同。它可以包含与污点效果完全相同的值- NoScheduleNoExecutePreferNoSchedule

具有NoExecute容忍的 Pod 将无限期容忍与其关联的污点。但是,您可以添加一个名为tolerationSeconds的字段,以便在经过规定的时间后,Pod 离开受污染的节点。这允许您指定在一段时间后生效的容忍。让我们看一个例子:

pod-with-toleration3.yaml

apiVersion: v1
kind: Pod
metadata:
  name: slow-app
spec:
  containers:
  - name: slow-app
    image: slow-app:latest
tolerations:
- key: "cpu_speed"
  operator: "Equal"
  Value: "slow"
  effect: "NoExecute"
  tolerationSeconds: 60

在这种情况下,当污点和容忍执行时,已经在具有taint的节点上运行的 Pod 将在重新调度到不同节点之前在节点上保留60秒。

多个污点和容忍

当 Pod 和节点上有多个污点或容忍时,调度程序将检查它们所有。这里没有OR逻辑运算符-如果节点上的任何污点在 Pod 上没有匹配的容忍,它将不会被调度到节点上(除了PreferNoSchedule之外,在这种情况下,与以前一样,调度程序将尽量不在节点上调度)。即使在节点上有六个污点中,Pod 容忍了其中五个,它仍然不会被调度到NoSchedule污点,并且仍然会因为NoExecute污点而被驱逐。

对于一个可以更微妙地控制放置方式的工具,让我们看一下节点亲和力。

使用节点亲和力控制 Pod

正如你可能已经注意到的,污点和容忍性 - 虽然比节点选择器灵活得多 - 仍然留下了一些用例未解决,并且通常只允许过滤模式,你可以使用ExistsEquals来匹配特定的污点。可能有更高级的用例,你想要更灵活的方法来选择节点 - Kubernetes 的亲和性就是解决这个问题的功能。

有两种亲和性:

  • 节点亲和性

  • 跨 Pod 的亲和性

节点亲和性是节点选择器的类似概念,只是它允许更强大的选择特征集。让我们看一些示例 YAML,然后分解各个部分:

pod-with-node-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: affinity-test
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: cpu_speed
            operator: In
            values:
            - fast
            - medium_fast
  containers:
  - name: speedy-app
    image: speedy-app:latest

正如你所看到的,我们的Pod spec有一个affinity键,并且我们指定了一个nodeAffinity设置。有两种可能的节点亲和性类型:

  • requiredDuringSchedulingIgnoredDuringExecution

  • preferredDuringSchedulingIgnoredDuringExecution

这两种类型的功能直接映射到NoSchedulePreferNoSchedule的工作方式。

使用 requiredDuringSchedulingIgnoredDuringExecution 节点亲和性

对于requiredDuringSchedulingIgnoredDuringExecution,Kubernetes 永远不会调度一个没有与节点匹配的术语的 Pod。

对于preferredDuringSchedulingIgnoredDuringExecution,它将尝试满足软性要求,但如果不能,它仍然会调度 Pod。

节点亲和性相对于节点选择器和污点和容忍性的真正能力在于你可以在选择器方面实现的实际表达式和逻辑。

requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution亲和性的功能是非常不同的,因此我们将分别进行审查。

对于我们的required亲和性,我们有能力指定nodeSelectorTerms - 可以是一个或多个包含matchExpressions的块。对于每个matchExpressions块,可以有多个表达式。

在我们在上一节中看到的代码块中,我们有一个单一的节点选择器术语,一个matchExpressions块 - 它本身只有一个表达式。这个表达式寻找key,就像节点选择器一样,代表一个节点标签。接下来,它有一个operator,它给了我们一些灵活性,让我们决定如何识别匹配。以下是操作符的可能值:

  • In

  • NotIn

  • Exists

  • DoesNotExist

  • Gt(注意:大于)

  • Lt(注意:小于)

在我们的情况下,我们使用了In运算符,它将检查值是否是我们指定的几个值之一。最后,在我们的values部分,我们可以列出一个或多个值,根据运算符,必须匹配才能使表达式为真。

正如你所看到的,这为我们在指定选择器时提供了更大的粒度。让我们看一个使用不同运算符的cpu_speed的例子:

pod-with-node-affinity2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: affinity-test
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: cpu_speed
            operator: Gt
            values:
            - "5"
  containers:
  - name: speedy-app
    image: speedy-app:latest

正如你所看到的,我们正在使用非常精细的matchExpressions选择器。现在,使用更高级的运算符匹配的能力使我们能够确保我们的speedy-app只安排在具有足够高时钟速度(在本例中为 5 GHz)的节点上。我们可以更加精细地规定,而不是将我们的节点分类为“慢”和“快”这样的广泛组别。

接下来,让我们看看另一种节点亲和性类型 - preferredDuringSchedulingIgnoredDuringExecution

使用 preferredDuringSchedulingIgnoredDuringExecution 节点亲和性

这种情况的语法略有不同,并且使我们能够更精细地影响这个“软”要求。让我们看一个实现这一点的 Pod spec YAML:

pod-with-node-affinity3.yaml

apiVersion: v1
kind: Pod
metadata:
  name: slow-app-affinity
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: cpu_speed
            operator: Lt
            values:
            - "3"
  containers:
  - name: slow-app
    image: slow-app:latest

这看起来与我们的required语法有些不同。

对于preferredDuringSchedulingIgnoredDuringExecution,我们有能力为每个条目分配一个“权重”,并附带一个偏好,这可以再次是一个包含多个内部表达式的matchExpressions块,这些表达式使用相同的key-operator-values语法。

这里的关键区别是“权重”值。由于preferredDuringSchedulingIgnoredDuringExecution是一个要求,我们可以列出几个不同的偏好,并附带权重,让调度器尽力满足它们。其工作原理是,调度器将遍历所有偏好,并根据每个偏好的权重和是否满足来计算节点的得分。假设所有硬性要求都得到满足,调度器将选择得分最高的节点。在前面的情况下,我们有一个权重为 1 的单个偏好,但权重可以从 1 到 100 不等 - 所以让我们看一个更复杂的设置,用于我们的speedy-app用例:

pod-with-node-affinity4.yaml

apiVersion: v1
kind: Pod
metadata:
  name: speedy-app-prefers-affinity
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 90
        preference:
          matchExpressions:
          - key: cpu_speed
            operator: Gt
            values:
            - "3"
      - weight: 10
        preference:
          matchExpressions:
          - key: memory_speed
            operator: Gt
            values:
            - "4"
  containers:
  - name: speedy-app
    image: speedy-app:latest

在确保我们的speedy-app在最佳节点上运行的过程中,我们决定只实现soft要求。如果没有快速节点存在,我们仍希望我们的应用程序被调度和运行。为此,我们指定了两个偏好 - 一个cpu_speed超过 3(3 GHz)和一个内存速度超过 4(4 GHz)的节点。

由于我们的应用程序更多地受限于 CPU 而不是内存,我们决定适当地权衡我们的偏好。在这种情况下,cpu_speed具有weight90,而memory_speed具有weight10

因此,满足我们的cpu_speed要求的任何节点的计算得分都比仅满足memory_speed要求的节点高得多 - 但仍然比同时满足两者的节点低。当我们尝试为这个应用程序调度 10 或 100 个新的 Pod 时,您可以看到这种计算是如何有价值的。

多个节点亲和性

当我们处理多个节点亲和性时,有一些关键的逻辑要记住。首先,即使只有一个节点亲和性,如果它与同一 Pod 规范下的节点选择器结合使用(这确实是可能的),则节点选择器必须在任何节点亲和性逻辑生效之前满足。这是因为节点选择器只实现硬性要求,并且两者之间没有OR逻辑运算符。OR逻辑运算符将检查两个要求,并确保它们中至少有一个为真 - 但节点选择器不允许我们这样做。

其次,对于requiredDuringSchedulingIgnoredDuringExecution节点亲和性,nodeSelectorTerms下的多个条目将在OR逻辑运算符中处理。如果满足一个但不是全部,则 Pod 仍将被调度。

最后,对于matchExpressions下有多个条目的nodeSelectorTerm,所有条目都必须满足 - 这是一个AND逻辑运算符。让我们看一个这样的示例 YAML:

pod-with-node-affinity5.yaml

apiVersion: v1
kind: Pod
metadata:
  name: affinity-test
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: cpu_speed
            operator: Gt
            values:
            - "5"
          - key: memory_speed
            operator: Gt
            values:
            - "4"
  containers:
  - name: speedy-app
    image: speedy-app:latest

在这种情况下,如果一个节点的 CPU 速度为5,但不满足内存速度要求(或反之亦然),则 Pod 将不会被调度。

关于节点亲和性的最后一件事要注意的是,正如您可能已经注意到的,这两种亲和性类型都不允许我们在我们的污点和容忍设置中可以使用的NoExecute功能。

另一种节点亲和性类型 - requiredDuringSchedulingRequiredDuring execution - 将在将来的版本中添加此功能。截至 Kubernetes 1.19,这种类型尚不存在。

接下来,我们将看一下 Pod 间亲和性和反亲和性,它提供了 Pod 之间的亲和性定义,而不是为节点定义规则。

使用 Pod 间亲和性和反亲和性

Pod 间亲和性和反亲和性让您根据节点上已经存在的其他 Pod 来指定 Pod 应该如何运行。由于集群中的 Pod 数量通常比节点数量要大得多,并且一些 Pod 亲和性和反亲和性规则可能相当复杂,如果您在许多节点上运行许多 Pod,这个功能可能会给您的集群控制平面带来相当大的负载。因此,Kubernetes 文档不建议在集群中有大量节点时使用这些功能。

Pod 亲和性和反亲和性的工作方式有很大不同-让我们先单独看看每个,然后再讨论它们如何结合起来。

Pod 亲和性

与节点亲和性一样,让我们深入讨论 YAML,以讨论 Pod 亲和性规范的组成部分:

pod-with-pod-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: not-hungry-app-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: hunger
            operator: In
            values:
            - "1"
            - "2"
        topologyKey: rack
  containers:
  - name: not-hungry-app
    image: not-hungry-app:latest

就像节点亲和性一样,Pod 亲和性让我们在两种类型之间进行选择:

  • preferredDuringSchedulingIgnoredDuringExecution

  • requiredDuringSchedulingIgnoredDuringExecution

与节点亲和性类似,我们可以有一个或多个选择器-因为我们选择的是 Pod 而不是节点,所以它们被称为labelSelectormatchExpressions功能与节点亲和性相同,但是 Pod 亲和性添加了一个全新的关键字叫做topologyKey

topologyKey本质上是一个选择器,限制了调度器应该查看的范围,以查看是否正在运行相同选择器的其他 Pod。这意味着 Pod 亲和性不仅需要意味着同一节点上相同类型(选择器)的其他 Pod;它可以意味着多个节点的组。

让我们回到本章开头的故障域示例。在那个例子中,每个机架都是自己的故障域,每个机架有多个节点。为了将这个概念扩展到topologyKey,我们可以使用rack=1rack=2为每个机架上的节点打上标签。然后我们可以使用topologyKey机架,就像我们在 YAML 中所做的那样,指定调度器应该检查所有运行在具有相同topologyKey的节点上的 Pod(在这种情况下,这意味着同一机架上的Node 1Node 2上的所有 Pod)以应用 Pod 亲和性或反亲和性规则。

因此,将我们的示例 YAML 全部加起来,告诉调度器的是:

  • 这个 Pod 必须被调度到具有标签rack的节点上,其中标签rack的值将节点分成组。

  • 然后 Pod 将被调度到一个组中,该组中已经存在一个带有标签hunger和值为 1 或 2 的 Pod。

基本上,我们将我们的集群分成拓扑域 - 在这种情况下是机架 - 并指示调度器仅在共享相同拓扑域的节点上将相似的 Pod 一起调度。这与我们第一个故障域示例相反,如果可能的话,我们不希望 Pod 共享相同的域 - 但也有理由希望相似的 Pod 在同一域上。例如,在多租户设置中,租户希望在域上拥有专用硬件租用权,您可以确保属于某个租户的每个 Pod 都被调度到完全相同的拓扑域。

您可以以相同的方式使用preferredDuringSchedulingIgnoredDuringExecution。在我们讨论反亲和性之前,这里有一个带有 Pod 亲和性和preferred类型的示例:

pod-with-pod-affinity2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: not-hungry-app-affinity
spec:
  affinity:
    podAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: hunger
              operator: Lt
              values:
              - "3"
          topologyKey: rack
  containers:
  - name: not-hungry-app
    image: not-hungry-app:latest

与之前一样,在这个代码块中,我们有我们的weight - 在这种情况下是50 - 和我们的表达式匹配 - 在这种情况下,使用小于(Lt)运算符。这种亲和性将促使调度器尽力将 Pod 调度到一个节点上,该节点上已经运行着一个hunger小于 3 的 Pod,或者与另一个在同一机架上运行着hunger小于 3 的 Pod。调度器使用weight来比较节点 - 正如在节点亲和性部分讨论的那样 - 使用节点亲和性控制 Pod(参见pod-with-node-affinity4.yaml)。在这种特定情况下,50的权重并没有任何区别,因为亲和性列表中只有一个条目。

Pod 反亲和性使用相同的选择器和拓扑结构来扩展这种范例-让我们详细看一下它们。

Pod 反亲和性

Pod 反亲和性允许您阻止 Pod 在与匹配选择器的 Pod 相同的拓扑域上运行。它们实现了与 Pod 亲和性相反的逻辑。让我们深入了解一些 YAML,并解释一下它是如何工作的:

pod-with-pod-anti-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: hungry-app
spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: hunger
              operator: In
              values:
              - "4"
              - "5"
          topologyKey: rack
  containers:
  - name: hungry-app
    image: hungry-app

与 Pod 亲和性类似,我们使用affinity键来指定podAntiAffinity下的反亲和性的位置。与 Pod 亲和性一样,我们可以使用preferredDuringSchedulingIgnoredDuringExecutionrequireDuringSchedulingIgnoredDuringExecution。我们甚至可以使用与 Pod 亲和性相同的选择器语法。

语法上唯一的实际区别是在affinity键下使用podAntiAffinity

那么,这个 YAML 文件是做什么的呢?在这种情况下,我们建议调度器(一个soft要求)应该尝试将这个 Pod 调度到一个节点上,在这个节点或具有相同值的rack标签的任何其他节点上都没有运行带有hunger标签值为 4 或 5 的 Pod。我们告诉调度器尽量不要将这个 Pod 与任何额外饥饿的 Pod 放在一起

这个功能为我们提供了一个很好的方法来按故障域分隔 Pod - 我们可以将每个机架指定为一个域,并给它一个与自己相同类型的反亲和性。这将使调度器将 Pod 的克隆(或尝试在首选亲和性中)调度到不在相同故障域的节点上,从而在发生域故障时提供更大的可用性。

我们甚至可以选择结合 Pod 的亲和性和反亲和性。让我们看看这样可以如何工作。

结合亲和性和反亲和性

这是一个情况,你可以真正给你的集群控制平面增加不必要的负载。结合 Pod 的亲和性和反亲和性可以允许传递给 Kubernetes 调度器的非常微妙的规则。

让我们看一些结合这两个概念的部署规范的 YAML。请记住,亲和性和反亲和性是应用于 Pod 的概念 - 但我们通常不会指定没有像部署或副本集这样的控制器的 Pod。因此,这些规则是在部署 YAML 中的 Pod 规范级别应用的。出于简洁起见,我们只显示了这个部署的 Pod 规范部分,但你可以在 GitHub 存储库中找到完整的文件。

pod-with-both-antiaffinity-and-affinity.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hungry-app-deployment
# SECTION REMOVED FOR CONCISENESS  
     spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - other-hungry-app
            topologyKey: "rack"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - hungry-app-cache
            topologyKey: "rack"
      containers:
      - name: hungry-app
        image: hungry-app:latest

在这个代码块中,我们告诉调度器将我们的部署中的 Pod 视为这样:Pod 必须被调度到具有rack标签的节点上,以便它或具有相同值的rack标签的任何其他节点都有一个带有app=hungry-label-cache的 Pod。

其次,调度器必须尝试将 Pod 调度到具有rack标签的节点上,以便它或具有相同值的rack标签的任何其他节点都没有运行带有app=other-hungry-app标签的 Pod。

简而言之,我们希望我们的hungry-app的 Pod 在与hungry-app-cache相同的拓扑结构中运行,并且如果可能的话,我们不希望它们与other-hungry-app在相同的拓扑结构中。

由于强大的力量伴随着巨大的责任,而我们的 Pod 亲和性和反亲和性工具既强大又降低性能,Kubernetes 确保对您可以使用它们的可能方式设置了一些限制,以防止奇怪的行为或重大性能问题。

Pod 亲和性和反亲和性限制

亲和性和反亲和性的最大限制是,您不允许使用空的topologyKey。如果不限制调度器将作为单个拓扑类型处理的内容,可能会发生一些非常意外的行为。

第二个限制是,默认情况下,如果您使用反亲和性的硬版本-requiredOnSchedulingIgnoredDuringExecution,您不能只使用任何标签作为topologyKey

Kubernetes 只允许您使用kubernetes.io/hostname标签,这基本上意味着如果您使用required反亲和性,您只能在每个节点上有一个拓扑。这个限制对于prefer反亲和性或任何亲和性都不存在,甚至是required。可以更改此功能,但需要编写自定义准入控制器-我们将在第十二章中讨论,Kubernetes 安全性和合规性,以及第十三章使用 CRD 扩展 Kubernetes

到目前为止,我们对放置控件的工作尚未讨论命名空间。但是,对于 Pod 亲和性和反亲和性,它们确实具有相关性。

Pod 亲和性和反亲和性命名空间

由于 Pod 亲和性和反亲和性会根据其他 Pod 的位置而改变行为,命名空间是决定哪些 Pod 计入或反对亲和性或反亲和性的相关因素。

默认情况下,调度器只会查看创建具有亲和性或反亲和性的 Pod 的命名空间。对于我们之前的所有示例,我们没有指定命名空间,因此将使用默认命名空间。

如果要添加一个或多个命名空间,其中 Pod 将影响亲和性或反亲和性,可以使用以下 YAML:

pod-with-anti-affinity-namespace.yaml

apiVersion: v1
kind: Pod
metadata:
  name: hungry-app
spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: hunger
              operator: In
              values:
              - "4"
              - "5"
          topologyKey: rack
          namespaces: ["frontend", "backend", "logging"]
  containers:
  - name: hungry-app
    image: hungry-app

在这个代码块中,调度器将在尝试匹配反亲和性时查看前端、后端和日志命名空间(如您在podAffinityTerm块中的namespaces键中所见)。这允许我们限制调度器在验证其规则时操作的命名空间。

总结

在本章中,我们了解了 Kubernetes 提供的一些不同控件,以强制执行调度器通过规则来放置 Pod。我们了解到有“硬”要求和“软”规则,后者是调度器尽最大努力但不一定阻止违反规则的 Pod 被放置。我们还了解了一些实施调度控件的原因,比如现实生活中的故障域和多租户。

我们了解到有一些简单的方法可以影响 Pod 的放置,比如节点选择器和节点名称,还有更高级的方法,比如污点和容忍,Kubernetes 本身也默认使用这些方法。最后,我们发现 Kubernetes 提供了一些高级工具,用于节点和 Pod 的亲和性和反亲和性,这些工具允许我们创建复杂的调度规则。

在下一章中,我们将讨论 Kubernetes 上的可观察性。我们将学习如何查看应用程序日志,还将使用一些很棒的工具实时查看我们集群中正在发生的事情。

问题

  1. 节点选择器和节点名称字段之间有什么区别?

  2. Kubernetes 如何使用系统提供的污点和容忍?出于什么原因?

  3. 在使用多种类型的 Pod 亲和性或反亲和性时,为什么要小心?

  4. 如何在多个故障区域之间平衡可用性,并出于性能原因进行合作,为三层 Web 应用程序提供一个例子?使用节点或 Pod 的亲和性和反亲和性。

进一步阅读

第三部分:在生产环境中运行 Kubernetes

在本节中,您将了解 Kubernetes 的第 2 天运维、CI/CD 的最佳实践、如何定制和扩展 Kubernetes,以及更广泛的云原生生态系统的基础知识。

本书的这一部分包括以下章节:

  • 第九章,Kubernetes 的可观察性

  • 第十章,Kubernetes 故障排除

  • 第十一章,模板代码生成和 Kubernetes 上的 CI/CD

  • 第十二章,Kubernetes 安全性和合规性

第九章:Kubernetes 上的可观测性

本章深入讨论了在生产环境中运行 Kubernetes 时强烈建议实施的能力。首先,我们讨论了在分布式系统(如 Kubernetes)的上下文中的可观测性。然后,我们看一下内置的 Kubernetes 可观测性堆栈以及它实现的功能。最后,我们学习如何通过生态系统中的额外可观测性、监控、日志记录和指标基础设施来补充内置的可观测性工具。本章中学到的技能将帮助您将可观测性工具部署到您的 Kubernetes 集群,并使您能够了解您的集群(以及在其上运行的应用程序)的运行方式。

在本章中,我们将涵盖以下主题:

  • 在 Kubernetes 上理解可观测性

  • 使用默认的可观测性工具 - 指标、日志和仪表板

  • 实施生态系统的最佳实践

首先,我们将学习 Kubernetes 为可观测性提供的开箱即用工具和流程。

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个正常运行的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和安装 kubectl 工具的几种方法。

本章中使用的代码可以在该书的 GitHub 存储库中找到:

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter9

在 Kubernetes 上理解可观测性

没有监控的生产系统是不完整的。在软件中,我们将可观测性定义为在任何时间点都能够了解系统的性能(在最好的情况下,还能了解原因)。可观测性在安全性、性能和运行能力方面带来了显著的好处。通过了解您的系统在虚拟机、容器和应用程序级别的响应方式,您可以调整它以高效地运行,快速响应事件,并更容易地排除错误。

例如,让我们来看一个场景,您的应用程序运行非常缓慢。为了找到瓶颈,您可能会查看应用程序代码本身,Pod 的资源规格,部署中的 Pod 数量,Pod 级别或节点级别的内存和 CPU 使用情况,以及外部因素,比如在集群外运行的 MySQL 数据库。

通过添加可观察性工具,您将能够诊断许多这些变量,并找出可能导致应用程序减速的问题。

Kubernetes 作为一个成熟的容器编排系统,为我们提供了一些默认工具来监控我们的应用程序。在本章中,我们将把可观察性分为四个概念:指标、日志、跟踪和警报。让我们来看看每一个:

  • 指标在这里代表着查看系统当前状态的数值表示能力,特别关注 CPU、内存、网络、磁盘空间等。这些数字让我们能够判断当前状态与系统最大容量之间的差距,并确保系统对用户保持可用。

  • 日志指的是从应用程序和系统中收集文本日志的做法。日志可能是 Kubernetes 控制平面日志和应用程序 Pod 自身的日志的组合。日志可以帮助我们诊断 Kubernetes 系统的可用性,但它们也可以帮助我们排除应用程序错误。

  • 跟踪指的是收集分布式跟踪。跟踪是一种可观察性模式,提供了对请求链的端到端可见性 - 这些请求可以是 HTTP 请求或其他类型的请求。在使用微服务的分布式云原生环境中,这个主题尤为重要。如果您有许多微服务并且它们相互调用,那么在涉及多个服务的单个端到端请求时,很难找到瓶颈或问题。跟踪允许您查看每个服务对服务调用的每个环节分解的请求。

  • 警报对应于在发生某些事件时设置自动触点的做法。警报可以设置在指标日志上,并通过各种媒介传递,从短信到电子邮件再到第三方应用程序等等。

在这四个可观测性方面之间,我们应该能够了解我们集群的健康状况。然而,可以配置许多不同的可能的指标、日志甚至警报。因此,了解要寻找的内容非常重要。下一节将讨论 Kubernetes 集群和应用程序健康最重要的可观测性领域。

了解对 Kubernetes 集群和应用程序健康至关重要的内容

在 Kubernetes 或第三方可观测性解决方案提供的大量可能的指标和日志中,我们可以缩小一些最有可能导致集群出现重大问题的指标。无论您最终选择使用哪种可观测性解决方案,您都应该将这些要点放在最显眼的位置。首先,让我们看一下 CPU 使用率与集群健康之间的关系。

节点 CPU 使用率

在您的可观测性解决方案中,跨 Kubernetes 集群节点的 CPU 使用率状态是一个非常重要的指标。我们在之前的章节中已经讨论过,Pod 可以为 CPU 使用率定义资源请求和限制。然而,当限制设置得比集群的最大 CPU 容量更高时,节点仍然可能过度使用 CPU。此外,运行我们控制平面的主节点也可能遇到 CPU 容量问题。

CPU 使用率达到最大的工作节点可能会表现不佳,或者限制运行在 Pod 上的工作负载。如果 Pod 上没有设置限制,或者节点的总 Pod 资源限制大于其最大容量,即使其总资源请求较低,也很容易发生这种情况。CPU 使用率达到最大的主节点可能会影响调度器、kube-apiserver 或其他控制平面组件的性能。

总的来说,工作节点和主节点的 CPU 使用率应该在您的可观测性解决方案中可见。最好的方法是通过一些指标(例如在本章后面将要介绍的 Grafana 等图表解决方案)以及对集群中节点的高 CPU 使用率的警报来实现。

内存使用率也是一个非常重要的指标,与 CPU 类似,需要密切关注。

节点内存使用率

与 CPU 使用率一样,内存使用率也是集群中需要密切观察的一个极其重要的指标。内存使用率可以通过 Pod 资源限制进行过度使用,对于集群中的主节点和工作节点都可能出现与 CPU 使用率相同的问题。

同样,警报和指标的组合对于查看集群内存使用情况非常重要。我们将在本章后面学习一些工具。

下一个重要的可观察性部分,我们将不再关注指标,而是关注日志。

控制平面日志记录

当运行时,Kubernetes 控制平面的组件会输出日志,这些日志可以用于深入了解集群操作。正如我们将在《第十章》Chapter 10中看到的那样,这些日志也可以在故障排除中起到重要作用,故障排除 Kubernetes。Kubernetes API 服务器、控制器管理器、调度程序、kube 代理和 kubelet 的日志对于某些故障排除或可观察性原因都非常有用。

应用程序日志记录

应用程序日志记录也可以并入 Kubernetes 的可观察性堆栈中——能够查看应用程序日志以及其他指标可能对操作员非常有帮助。

应用程序性能指标

与应用程序日志记录一样,应用程序性能指标和监控对于在 Kubernetes 上运行的应用程序的性能非常重要。在应用程序级别进行内存使用和 CPU 分析可以成为可观察性堆栈中有价值的一部分。

一般来说,Kubernetes 提供了应用程序监控和日志记录的数据基础设施,但不提供诸如图表和搜索等更高级的功能。考虑到这一点,让我们回顾一下 Kubernetes 默认提供的工具,以解决这些问题。

使用默认的可观察性工具

Kubernetes 甚至在不添加任何第三方解决方案的情况下就提供了可观察性工具。这些本机 Kubernetes 工具构成了许多更强大解决方案的基础,因此讨论它们非常重要。由于可观察性包括指标、日志、跟踪和警报,我们将依次讨论每个内容,首先关注 Kubernetes 本机解决方案。首先,让我们讨论指标。

Kubernetes 上的指标

通过简单运行kubectl describe pod,可以获得关于应用程序的大量信息。我们可以看到有关 Pod 规范的信息,它所处的状态以及阻止其功能的关键问题。

假设我们的应用程序出现了一些问题。具体来说,Pod 没有启动。为了调查,我们运行kubectl describe pod。作为第一章中提到的 kubectl 别名的提醒,kubectl describe podkubectl describe pods是相同的。这是describe pod命令的一个示例输出 - 我们除了Events信息之外剥离了所有内容:

图 9.1 - 描述 Pod 事件输出

图 9.1 - 描述 Pod 事件输出

正如您所看到的,这个 Pod 没有被调度,因为我们的 Nodes 都没有内存了!这将是一个值得进一步调查的好事。

让我们继续。通过运行kubectl describe nodes,我们可以了解很多关于我们的 Kubernetes Nodes 的信息。其中一些信息对我们系统的性能非常重要。这是另一个示例输出,这次是来自kubectl describe nodes命令。而不是将整个输出放在这里,因为可能会相当冗长,让我们聚焦在两个重要部分 - ConditionsAllocated resources。首先,让我们回顾一下Conditions部分:

图 9.2 - 描述 Node 条件输出

图 9.2 - 描述 Node 条件输出

正如您所看到的,我们已经包含了kubectl describe nodes命令输出的Conditions块。这是查找任何问题的好地方。正如我们在这里看到的,我们的 Node 实际上正在遇到问题。我们的MemoryPressure条件为 true,而Kubelet内存不足。难怪我们的 Pod 无法调度!

接下来,检查分配的资源块:

Allocated resources:
 (Total limits may be over 100 percent, i.e., overcommitted.)
 CPU Requests	CPU Limits    Memory Requests  Memory Limits
 ------------	----------    ---------------  -------------
 8520m (40%)	4500m (24%)   16328Mi (104%)   16328Mi (104%)

现在我们看到了一些指标!看起来我们的 Pod 正在请求过多的内存,导致我们的 Node 和 Pod 出现问题。从这个输出中可以看出,Kubernetes 默认已经在收集有关我们的 Nodes 的指标数据。没有这些数据,调度器将无法正常工作,因为维护 Pod 资源请求与 Node 容量是其最重要的功能之一。

然而,默认情况下,这些指标不会向用户显示。实际上,它们是由每个 Node 的Kubelet收集并传递给调度器来完成其工作。幸运的是,我们可以通过部署 Metrics Server 轻松地获取这些指标到我们的集群中。

Metrics Server 是一个官方支持的 Kubernetes 应用程序,它收集指标信息并在 API 端点上公开它以供使用。实际上,Metrics Server 是使水平 Pod 自动缩放器工作所必需的,但它并不总是默认包含在内,这取决于 Kubernetes 发行版。

部署 Metrics Server 非常快速。在撰写本书时,可以使用以下命令安装最新版本:

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.3.7/components.yaml

重要说明

有关如何使用 Metrics Server 的完整文档可以在github.com/kubernetes-sigs/metrics-server找到。

一旦 Metrics Server 运行起来,我们就可以使用一个全新的 Kubernetes 命令。kubectl top命令可用于 Pod 或节点,以查看有关内存和 CPU 使用量的详细信息。

让我们看一些示例用法。运行kubectl top nodes以查看节点级别的指标。以下是命令的输出:

图 9.3-节点指标输出

图 9.3-节点指标输出

正如您所看到的,我们能够看到绝对和相对的 CPU 和内存使用情况。

重要说明

CPU 核心以millcpumillicores来衡量。1000millicores相当于一个虚拟 CPU。内存以字节为单位。

接下来,让我们来看一下kubectl top pods命令。使用-namespace kube-system标志运行它,以查看kube-system命名空间中的 Pod。

为此,我们运行以下命令:

Kubectl top pods -n kube-system 

然后我们得到以下输出:

NAMESPACE     NAME                CPU(cores)   MEMORY(bytes)   
default       my-hungry-pod       8m           50Mi            
default       my-lightweight-pod  2m           10Mi       

正如您所看到的,该命令使用与kubectl top nodes相同的绝对单位-毫核和字节。在查看 Pod 级别的指标时,没有相对百分比。

接下来,我们将看一下 Kubernetes 如何处理日志记录。

Kubernetes 上的日志记录

我们可以将 Kubernetes 上的日志记录分为两个领域- 应用程序日志控制平面日志。让我们从控制平面日志开始。

控制平面日志

控制平面日志是指由 Kubernetes 控制平面组件(如调度程序、API 服务器等)创建的日志。对于纯净的 Kubernetes 安装,控制平面日志可以在节点本身找到,并且需要直接访问节点才能查看。对于设置为使用systemd的组件的集群,日志可以使用journalctlCLI 工具找到(有关更多信息,请参阅以下链接:manpages.debian.org/stretch/systemd/journalctl.1.en.html)。

在主节点上,您可以在文件系统的以下位置找到日志:

  • /var/log/kube-scheduler.log中,您可以找到 Kubernetes 调度器的日志。

  • /var/log/kube-controller-manager.log中,您可以找到控制器管理器的日志(例如,查看扩展事件)。

  • /var/log/kube-apiserver.log中,您可以找到 Kubernetes API 服务器的日志。

在工作节点上,日志可以在文件系统的两个位置找到:

  • /var/log/kubelet.log中,您可以找到 kubelet 的日志。

  • /var/log/kube-proxy.log中,您可以找到 kube 代理的日志。

尽管通常情况下,集群健康受 Kubernetes 主节点和工作节点组件的影响,但跟踪应用程序日志也同样重要。

应用程序日志

在 Kubernetes 上查找应用程序日志非常容易。在我们解释它是如何工作之前,让我们看一个例子。

要检查特定 Pod 的日志,可以使用kubectl logs <pod_name>命令。该命令的输出将显示写入容器的stdoutstderr的任何文本。如果一个 Pod 有多个容器,您必须在命令中包含容器名称:

kubectl logs <pod_name> <container_name> 

在幕后,Kubernetes 通过使用容器引擎的日志驱动程序来处理 Pod 日志。通常,任何写入stdoutstderr的日志都会被持久化到每个节点的磁盘中的/var/logs文件夹中。根据 Kubernetes 的分发情况,可能会设置日志轮换,以防止日志占用节点磁盘空间过多。此外,Kubernetes 组件,如调度器、kubelet 和 kube-apiserver 也会将日志持久化到节点磁盘空间中,通常在/var/logs文件夹中。重要的是要注意默认日志记录功能的有限性 - Kubernetes 的强大可观察性堆栈肯定会包括第三方解决方案用于日志转发,我们很快就会看到。

接下来,对于一般的 Kubernetes 可观察性,我们可以使用 Kubernetes 仪表板。

安装 Kubernetes 仪表板

Kubernetes 仪表板提供了 kubectl 的所有功能,包括查看日志和编辑资源,都可以在图形界面中完成。设置仪表板非常容易,让我们看看如何操作。

仪表板可以通过单个kubectl apply命令安装。要进行自定义,请查看 Kubernetes 仪表板 GitHub 页面github.com/kubernetes/dashboard

要安装 Kubernetes 仪表板的版本,请运行以下kubectl命令,将<VERSION>标签替换为您所需的版本,根据您正在使用的 Kubernetes 版本(再次检查 Dashboard GitHub 页面以获取版本兼容性):

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/<VERSION> /aio/deploy/recommended.yaml

在我们的案例中,截至本书撰写时,我们将使用 v2.0.4 - 最终的命令看起来像这样:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.4/aio/deploy/recommended.yaml

安装了 Kubernetes 仪表板后,有几种方法可以访问它。

重要提示

通常不建议使用 Ingress 或公共负载均衡器服务,因为 Kubernetes 仪表板允许用户更新集群对象。如果由于某种原因您的仪表板登录方法受到损害或容易被发现,您可能面临着很大的安全风险。

考虑到这一点,我们可以使用kubectl port-forwardkubectl proxy来从本地机器查看我们的仪表板。

在本例中,我们将使用kubectl proxy命令,因为我们还没有在示例中使用过它。

kubectl proxy命令与kubectl port-forward命令不同,它只需要一个命令即可代理到集群上运行的每个服务。它通过直接将 Kubernetes API 代理到本地机器上的一个端口来实现这一点,默认端口为8081。有关kubectl proxy命令的详细讨论,请查看kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#proxy上的文档。

为了使用kubectl proxy访问特定的 Kubernetes 服务,您只需要正确的路径。运行kubectl proxy后访问 Kubernetes 仪表板的路径将如下所示:

http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

正如您所看到的,我们在浏览器中放置的kubectl proxy路径是在本地主机端口8001上,并提到了命名空间(kubernetes-dashboard)、服务名称和选择器(https:kubernetes-dashboard)以及代理路径。

让我们将 Kubernetes 仪表板的 URL 放入浏览器中并查看结果:

图 9.4 - Kubernetes 仪表板登录

图 9.4 - Kubernetes 仪表板登录

当我们部署和访问 Kubernetes 仪表板时,我们会看到一个登录界面。我们可以创建一个服务账户(或使用我们自己的)来登录仪表板,或者简单地链接我们的本地 Kubeconfig 文件。通过使用特定服务账户的令牌登录到 Kubernetes 仪表板,仪表板用户将继承该服务账户的权限。这允许您指定用户将能够使用 Kubernetes 仪表板执行哪种类型的操作 - 例如,只读权限。

让我们继续为我们的 Kubernetes 仪表板创建一个全新的服务账户。您可以自定义此服务账户并限制其权限,但现在我们将赋予它管理员权限。要做到这一点,请按照以下步骤操作:

  1. 我们可以使用以下 Kubectl 命令来命令式地创建一个服务账户:
kubectl create serviceaccount dashboard-user

这将产生以下输出,确认了我们服务账户的创建:

serviceaccount/dashboard-user created
  1. 现在,我们需要将我们的服务账户链接到一个 ClusterRole。您也可以使用 Role,但我们希望我们的仪表板用户能够访问所有命名空间。为了使用单个命令将服务账户链接到 cluster-admin 默认 ClusterRole,我们可以运行以下命令:
kubectl create clusterrolebinding dashboard-user \--clusterrole=cluster-admin --serviceaccount=default:dashboard-user

这个命令将产生以下输出:

clusterrolebinding.rbac.authorization.k8s.io/dashboard-user created
  1. 运行此命令后,我们应该能够登录到我们的仪表板!首先,我们只需要找到我们将用于登录的令牌。服务账户的令牌存储为 Kubernetes 秘密,所以让我们看看它是什么样子。运行以下命令以查看我们的令牌存储在哪个秘密中:
kubectl get secrets

在输出中,您应该会看到一个类似以下的秘密:

NAME                         TYPE                                  DATA   AGE
dashboard-user-token-dcn2g   kubernetes.io/service-account-token   3      112s
  1. 现在,为了获取我们用于登录到仪表板的令牌,我们只需要使用以下命令描述秘密内容:
kubectl describe secret dashboard-user-token-dcn2g   

生成的输出将如下所示:

Name:         dashboard-user-token-dcn2g
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: dashboard-user
              kubernetes.io/service-account.uid: 9dd255sd-426c-43f4-88c7-66ss91h44215
Type:  kubernetes.io/service-account-token
Data
====
ca.crt:     1025 bytes
namespace:  7 bytes
token: < LONG TOKEN HERE >
  1. 要登录到仪表板,复制token旁边的字符串,将其复制到 Kubernetes 仪表板登录界面上的令牌输入中,然后点击登录。您应该会看到 Kubernetes 仪表板概览页面!

  2. 继续在仪表板上点击 - 您应该能够看到您可以使用 kubectl 查看的所有相同资源,并且您可以在左侧边栏中按命名空间进行过滤。例如,这是一个命名空间页面的视图:图 9.5 - Kubernetes 仪表板详细信息

图 9.5 - Kubernetes 仪表板详细信息

  1. 您还可以单击单个资源,甚至使用仪表板编辑这些资源,只要您用于登录的服务帐户具有适当的权限。

这是从部署详细页面编辑部署资源的视图:

图 9.6 – Kubernetes 仪表板编辑视图

图 9.6 – Kubernetes 仪表板编辑视图

Kubernetes 仪表板还允许您查看 Pod 日志,并深入了解集群中的许多其他资源类型。要了解仪表板的全部功能,请查看先前提到的 GitHub 页面上的文档。

最后,为了完成我们对 Kubernetes 上默认可观察性的讨论,让我们来看一下警报。

Kubernetes 上的警报和跟踪

不幸的是,可观察性谜题的最后两个部分——警报跟踪——目前还不是 Kubernetes 上的本机功能。为了创建这种类型的功能,让我们继续我们的下一节——从 Kubernetes 生态系统中整合开源工具。

利用生态系统最佳增强 Kubernetes 的可观察性

正如我们所讨论的,尽管 Kubernetes 提供了强大的可见性功能的基础,但通常是由社区和供应商生态系统来创建用于度量、日志、跟踪和警报的高级工具。对于本书的目的,我们将专注于完全开源的自托管解决方案。由于许多这些解决方案在度量、日志、跟踪和警报之间满足多个可见性支柱的需求,因此在我们的审查过程中,我们将分别审查每个解决方案,而不是将解决方案分类到每个可见性支柱中。

让我们从用于度量和警报的技术常用组合PrometheusGrafana开始。

介绍 Prometheus 和 Grafana

Prometheus 和 Grafana 是 Kubernetes 上典型的可见性技术组合。Prometheus 是一个时间序列数据库、查询层和具有许多集成的警报系统,而 Grafana 是一个复杂的图形和可视化层,与 Prometheus 集成。我们将带您了解这些工具的安装和使用,从 Prometheus 开始。

安装 Prometheus 和 Grafana

有许多在 Kubernetes 上安装 Prometheus 的方法,但大多数都使用部署来扩展服务。对于我们的目的,我们将使用 kube-prometheus 项目(github.com/coreos/kube-prometheus)。该项目包括一个 operator 以及几个自定义资源定义CRDs)。它还将自动为我们安装 Grafana!

操作员本质上是 Kubernetes 上的一个应用控制器(像其他 Pod 中部署的应用程序一样部署),它恰好会向 Kubernetes API 发出命令,以便正确运行或操作其应用程序。

另一方面,CRD 允许我们在 Kubernetes API 内部建模自定义功能。我们将在第十三章中学到更多关于操作员和 CRDs 的知识,但现在只需将操作员视为创建智能部署的一种方式,其中应用程序可以正确地控制自身并根据需要启动其他 Pod 和部署 – 将 CRD 视为一种使用 Kubernetes 存储特定应用程序关注点的方式。

要安装 Prometheus,首先我们需要下载一个发布版,这可能会因 Prometheus 的最新版本或您打算使用的 Kubernetes 版本而有所不同:

curl -LO https://github.com/coreos/kube-prometheus/archive/v0.5.0.zip

接下来,使用任何工具解压文件。首先,我们需要安装 CRDs。一般来说,大多数 Kubernetes 工具安装说明都会让您首先在 Kubernetes 上创建 CRDs,因为如果底层 CRD 尚未在 Kubernetes 上创建,那么任何使用 CRD 的其他设置都将失败。

让我们使用以下命令安装它们:

kubectl apply -f manifests/setup

在创建 CRDs 时,我们需要等待几秒钟。此命令还将为我们的资源创建一个 monitoring 命名空间。一旦一切准备就绪,让我们使用以下命令启动其余的 Prometheus 和 Grafana 资源:

kubectl apply -f manifests/

让我们来谈谈这个命令实际上会创建什么。整个堆栈包括以下内容:

  • Prometheus 部署:Prometheus 应用程序的 Pod

  • Prometheus 操作员:控制和操作 Prometheus 应用程序 Pod

  • Alertmanager 部署:用于指定和触发警报的 Prometheus 组件

  • Grafana:一个强大的可视化仪表板

  • Kube-state-metrics 代理:从 Kubernetes API 状态生成指标

  • Prometheus 节点导出器:将节点硬件和操作系统级别的指标导出到 Prometheus

  • 用于 Kubernetes 指标的 Prometheus 适配器:用于将 Kubernetes 资源指标 API 和自定义指标 API 摄入到 Prometheus 中

所有这些组件将为我们的集群提供复杂的可见性,从命令平面到应用程序容器本身。

一旦堆栈已经创建(通过使用kubectl get po -n monitoring命令进行检查),我们就可以开始使用我们的组件。让我们从简单的 Prometheus 开始使用。

使用 Prometheus

尽管 Prometheus 的真正力量在于其数据存储、查询和警报层,但它确实为开发人员提供了一个简单的 UI。正如您将在后面看到的,Grafana 提供了更多功能和自定义选项,但值得熟悉 Prometheus UI。

默认情况下,kube-prometheus只会为 Prometheus、Grafana 和 Alertmanager 创建 ClusterIP 服务。我们需要将它们暴露到集群外部。在本教程中,我们只是将服务端口转发到我们的本地机器。对于生产环境,您可能希望使用 Ingress 将请求路由到这三个服务。

为了port-forward到 Prometheus UI 服务,使用port-forward kubectl 命令:

Kubectl -n monitoring port-forward svc/prometheus-k8s 3000:9090

我们需要使用端口9090来访问 Prometheus UI。在您的机器上访问服务http://localhost:3000

您应该看到类似以下截图的内容:

图 9.7 – Prometheus UI

图 9.7 – Prometheus UI

您可以看到,Prometheus UI 有一个Graph页面,这就是您在图 9.4中看到的内容。它还有自己的 UI 用于查看配置的警报 – 但它不允许您通过 UI 创建警报。Grafana 和 Alertmanager 将帮助我们完成这项任务。

要执行查询,导航到Graph页面,并将查询命令输入到Expression栏中,然后单击Execute。Prometheus 使用一种称为PromQL的查询语言 – 我们不会在本书中完全向您介绍它,但 Prometheus 文档是学习的好方法。您可以使用以下链接进行参考:prometheus.io/docs/prometheus/latest/querying/basics/

为了演示这是如何工作的,让我们输入一个基本的查询,如下所示:

kubelet_http_requests_total

此查询将列出每个节点上发送到 kubelet 的 HTTP 请求的总数,对于每个请求类别,如下截图所示:

图 9.8 – HTTP 请求查询

图 9.8 – HTTP 请求查询

您还可以通过单击旁边的图表选项卡以图形形式查看请求,如下截图所示:

图 9.9 – HTTP 请求查询 – 图表视图

图 9.9 – HTTP 请求查询 – 图表视图

这提供了来自前面截图数据的时间序列图表视图。正如您所见,图表功能相当简单。

Prometheus 还提供了一个警报选项卡,用于配置 Prometheus 警报。通常,这些警报是通过代码配置而不是使用警报选项卡 UI 配置的,所以我们将在审查中跳过该页面。有关更多信息,您可以查看官方的 Prometheus 文档prometheus.io/docs/alerting/latest/overview/

让我们继续前往 Grafana,在那里我们可以通过可视化扩展 Prometheus 强大的数据工具。

使用 Grafana

Grafana 提供了强大的工具来可视化指标,支持许多可以实时更新的图表类型。我们可以将 Grafana 连接到 Prometheus,以便在 Grafana UI 上查看我们的集群指标图表。

要开始使用 Grafana,请执行以下操作:

  1. 我们将结束当前的端口转发(CTRL + C即可),并设置一个新的端口转发监听器到 Grafana UI:
Kubectl -n monitoring port-forward svc/grafana 3000:3000
  1. 再次导航到localhost:3000以查看 Grafana UI。您应该能够使用用户名admin密码admin登录,然后您应该能够按照以下截图更改初始密码:图 9.10 – Grafana 更改密码屏幕

图 9.10 – Grafana 更改密码屏幕

  1. 登录后,您将看到以下屏幕。Grafana 不会预先配置任何仪表板,但我们可以通过单击如下截图所示的+号轻松添加它们:图 9.11 – Grafana 主页

图 9.11 – Grafana 主页

  1. 每个 Grafana 仪表板都包括一个或多个不同集合的指标图。要添加一个预配置的仪表板(而不是自己创建一个),请单击左侧菜单栏上的加号(+)并单击导入。您应该会看到以下截图所示的页面:图 9.12 – Grafana 仪表板导入

图 9.12 – Grafana 仪表板导入

我们可以通过此页面使用 JSON 配置或粘贴公共仪表板 ID 来添加仪表板。

  1. 您可以在 grafana.com/grafana/dashboards/315 找到公共仪表板及其关联的 ID。仪表板#315 是 Kubernetes 的一个很好的起始仪表板 - 让我们将其添加到标有Grafana.com 仪表板的文本框中,然后单击Load

  2. 然后,在下一页中,从Prometheus选项下拉菜单中选择Prometheus数据源,用于在多个数据源之间进行选择(如果可用)。单击Import,应该加载仪表板,看起来像以下截图:

图 9.13 – Grafana 仪表盘

图 9.13 – Grafana 仪表盘

这个特定的 Grafana 仪表板提供了对集群中网络、内存、CPU 和文件系统利用率的良好高级概述,并且按照 Pod 和容器进行了细分。它配置了网络 I/O 压力集群内存使用集群 CPU 使用集群文件系统使用的实时图表 - 尽管最后一个选项可能根据您安装 Prometheus 的方式而不启用。

最后,让我们看一下 Alertmanager UI。

使用 Alertmanager

Alertmanager 是一个用于管理从 Prometheus 警报生成的警报的开源解决方案。我们之前作为堆栈的一部分安装了 Alertmanager - 让我们看看它能做什么:

  1. 首先,让我们使用以下命令port-forward Alertmanager 服务:
Kubectl -n monitoring port-forward svc/alertmanager-main 3000:9093
  1. 像往常一样,导航到 localhost:3000,查看如下截图所示的 UI。它看起来与 Prometheus UI 类似:

图 9.14 – Alertmanager UI

图 9.14 – Alertmanager UI

Alertmanager 与 Prometheus 警报一起工作。您可以使用 Prometheus 服务器指定警报规则,然后使用 Alertmanager 将类似的警报分组为单个通知,执行去重,并创建静音,这实质上是一种静音警报的方式,如果它们符合特定规则。

接下来,我们将回顾 Kubernetes 的一个流行日志堆栈 - Elasticsearch、FluentD 和 Kibana。

在 Kubernetes 上实现 EFK 堆栈

类似于流行的 ELK 堆栈(Elasticsearch、Logstash 和 Kibana),EFK 堆栈将 Logstash 替换为 FluentD 日志转发器,在 Kubernetes 上得到了很好的支持。实现这个堆栈很容易,让我们可以使用纯开源工具在 Kubernetes 上开始日志聚合和搜索功能。

安装 EFK 堆栈

在 Kubernetes 上安装 EFK Stack 有很多种方法,但 Kubernetes GitHub 存储库本身有一些支持的 YAML,所以让我们就使用那个吧:

  1. 首先,使用以下命令克隆或下载 Kubernetes 存储库:
git clone https://github.com/kubernetes/kubernetes
  1. 清单位于kubernetes/cluster/addons文件夹中,具体位于fluentd-elasticsearch下:
cd kubernetes/cluster/addons

对于生产工作负载,我们可能会对这些清单进行一些更改,以便为我们的集群正确定制配置,但出于本教程的目的,我们将保留所有内容为默认值。让我们开始引导我们的 EFK 堆栈的过程。

  1. 首先,让我们创建 Elasticsearch 集群本身。这在 Kubernetes 上作为一个 StatefulSet 运行,并提供一个 Service。要创建集群,我们需要运行两个kubectl命令:
kubectl apply -f ./fluentd-elasticsearch/es-statefulset.yaml
kubectl apply -f ./fluentd-elasticsearch/es-service.yaml

重要提示

关于 Elasticsearch StatefulSet 的一个警告 - 默认情况下,每个 Pod 的资源请求为 3GB 内存,因此如果您的节点没有足够的可用内存,您将无法按默认配置部署它。

  1. 接下来,让我们部署 FluentD 日志代理。这些将作为一个 DaemonSet 运行 - 每个节点一个 - 并将日志从节点转发到 Elasticsearch。我们还需要创建包含基本 FluentD 代理配置的 ConfigMap YAML。这可以进一步定制以添加诸如日志过滤器和新来源之类的内容。

  2. 要安装代理和它们的配置的 DaemonSet,请运行以下两个kubectl命令:

kubectl apply -f ./fluentd-elasticsearch/fluentd-es-configmap.yaml
kubectl apply -f ./fluentd-elasticsearch/fluentd-es-ds.yaml
  1. 现在我们已经创建了 ConfigMap 和 FluentD DaemonSet,我们可以创建我们的 Kibana 应用程序,这是一个用于与 Elasticsearch 交互的 GUI。这一部分作为一个 Deployment 运行,带有一个 Service。要将 Kibana 部署到我们的集群,运行最后两个kubectl命令:
kubectl apply -f ./fluentd-elasticsearch/kibana-deployment.yaml
kubectl apply -f ./fluentd-elasticsearch/kibana-service.yaml
  1. 一旦所有东西都已启动,这可能需要几分钟,我们就可以像我们之前对 Prometheus 和 Grafana 做的那样访问 Kibana UI。要检查我们刚刚创建的资源的状态,我们可以运行以下命令:
kubectl get po -A
  1. 一旦 FluentD、Elasticsearch 和 Kibana 的所有 Pod 都处于Ready状态,我们就可以继续进行。如果您的任何 Pod 处于ErrorCrashLoopBackoff阶段,请参阅addons文件夹中的 Kubernetes GitHub 文档以获取更多信息。

  2. 一旦我们确认我们的组件正常工作,让我们使用port-forward命令来访问 Kibana UI。顺便说一句,我们的 EFK 堆栈组件将位于kube-system命名空间中 - 因此我们的命令需要反映这一点。因此,让我们使用以下命令:

kubectl port-forward -n kube-system svc/kibana-logging 8080:5601

这个命令将从 Kibana UI 开始一个port-forward到您本地机器的端口8080

  1. 让我们在localhost:8080上查看 Kibana UI。根据您的确切版本和配置,它应该看起来像下面这样:图 9.15 – 基本 Kibana UI

图 9.15 – 基本 Kibana UI

Kibana 为搜索和可视化日志、指标等提供了几种不同的功能。对于我们的目的来说,仪表板中最重要的部分是日志,因为在我们的示例中,我们仅将 Kibana 用作日志搜索 UI。

然而,Kibana 还有许多其他功能,其中一些与 Grafana 相当。例如,它包括一个完整的可视化引擎,应用程序性能监控APM)功能,以及 Timelion,一个用于时间序列数据的表达式引擎,非常类似于 Prometheus 的 PromQL。Kibana 的指标功能类似于 Prometheus 和 Grafana。

  1. 为了让 Kibana 工作,我们首先需要指定一个索引模式。要做到这一点,点击可视化按钮,然后点击添加索引模式。从模式列表中选择一个选项,并选择带有当前日期的索引,然后创建索引模式。

现在我们已经设置好了,发现页面将为您提供搜索功能。这使用 Apache Lucene 查询语法(www.elastic.co/guide/en/elasticsearch/reference/6.7/query-dsl-query-string-query.html#query-string-syntax),可以处理从简单的字符串匹配表达式到非常复杂的查询。在下面的屏幕截图中,我们正在对字母h进行简单的字符串匹配。

图 9.16 – 发现 UI

图 9.16 – 发现 UI

当 Kibana 找不到任何结果时,它会为您提供一组方便的可能解决方案,包括查询示例,正如您在图 9.13中所看到的。

现在您已经知道如何创建搜索查询,可以在可视化页面上从查询中创建可视化。这些可视化可以从包括图形、图表等在内的可视化类型中选择,然后使用特定查询进行自定义,如下面的屏幕截图所示:

图 9.17 – 新可视化

图 9.17 – 新可视化

接下来,这些可视化可以组合成仪表板。这与 Grafana 类似,多个可视化可以添加到仪表板中,然后可以保存和重复使用。

您还可以使用搜索栏进一步过滤您的仪表板可视化 - 非常巧妙!下面的屏幕截图显示了如何将仪表板与特定查询关联起来:

图 9.18 – 仪表板 UI

图 9.18 – 仪表板 UI

如您所见,可以使用添加按钮为特定查询创建仪表板。

接下来,Kibana 提供了一个名为Timelion的工具,这是一个时间序列可视化综合工具。基本上,它允许您将单独的数据源合并到单个可视化中。Timelion 非常强大,但其功能集的全面讨论超出了本书的范围。下面的屏幕截图显示了 Timelion UI - 您可能会注意到与 Grafana 的一些相似之处,因为这两组工具提供了非常相似的功能:

图 9.19 – Timelion UI

图 9.19 – Timelion UI

如您所见,在 Timelion 中,查询可以用于驱动实时更新的图形,就像在 Grafana 中一样。

此外,虽然与本书关联较小,但 Kibana 提供了 APM 功能,这需要一些进一步的设置,特别是在 Kubernetes 中。在本书中,我们依赖 Prometheus 获取这种类型的信息,同时使用 EFK 堆栈搜索我们应用程序的日志。

现在我们已经介绍了用于度量和警报的 Prometheus 和 Grafana,以及用于日志记录的 EFK 堆栈,观察性谜题中只剩下一个部分。为了解决这个问题,我们将使用另一个优秀的开源软件 - Jaeger。

使用 Jaeger 实现分布式跟踪

Jaeger 是一个与 Kubernetes 兼容的开源分布式跟踪解决方案。Jaeger 实现了 OpenTracing 规范,这是一组用于定义分布式跟踪的标准。

Jaeger 提供了一个用于查看跟踪并与 Prometheus 集成的 UI。官方 Jaeger 文档可以在www.jaegertracing.io/docs/找到。始终检查文档以获取新信息,因为自本书出版以来可能已经发生了变化。

使用 Jaeger Operator 安装 Jaeger

要安装 Jaeger,我们将使用 Jaeger Operator,这是本书中首次遇到的操作员。在 Kubernetes 中,操作员只是一种创建自定义应用程序控制器的模式,它们使用 Kubernetes 的语言进行通信。这意味着,您不必部署应用程序的各种 Kubernetes 资源,您可以部署一个单独的 Pod(通常是单个部署),该应用程序将与 Kubernetes 通信并为您启动所有其他所需的资源。它甚至可以进一步自我操作应用程序,在必要时进行资源更改。操作员可能非常复杂,但它们使我们作为最终用户更容易在我们的 Kubernetes 集群上部署商业或开源软件。

要开始使用 Jaeger Operator,我们需要为 Jaeger 创建一些初始资源,然后操作员将完成其余工作。安装 Jaeger 的先决条件是在我们的集群上安装了nginx-ingress控制器,因为这是我们将访问 Jaeger UI 的方式。

首先,我们需要为 Jaeger 创建一个命名空间。我们可以通过kubectl create namespace命令获取它:

kubectl create namespace observability

现在我们的命名空间已创建,我们需要创建一些 Jaeger 和操作员将使用的CRDs。我们将在我们的 Kubernetes 扩展章节中深入讨论 CRDs,但现在,把它们看作是一种利用 Kubernetes API 来构建应用程序自定义功能的方式。使用以下步骤,让我们安装 Jaeger:

  1. 要创建 Jaeger CRDs,请运行以下命令:
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml

创建了我们的 CRDs 后,操作员需要创建一些角色和绑定以便进行工作。

  1. 我们希望 Jaeger 在我们的集群中拥有全局权限,因此我们将创建一些可选的 ClusterRoles 和 ClusterRoleBindings。为了实现这一点,我们运行以下命令:
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role_binding.yaml
  1. 现在,我们终于拥有了操作员工作所需的所有要素。让我们用最后一个kubectl命令安装操作员:
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml
  1. 最后,使用以下命令检查操作员是否正在运行:
kubectl get deploy -n observability

如果操作员正常运行,您将看到类似以下输出,部署中将有一个可用的 Pod:

图 9.20 - Jaeger Operator Pod 输出

图 9.20 - Jaeger Operator Pod 输出

我们现在已经启动并运行了我们的 Jaeger Operator - 但是 Jaeger 本身并没有在运行。为什么会这样?Jaeger 是一个非常复杂的系统,可以以不同的配置运行,操作员使得部署这些配置变得更加容易。

Jaeger Operator 使用一个名为Jaeger的 CRD 来读取您的 Jaeger 实例的配置,此时操作员将在 Kubernetes 上部署所有必要的 Pod 和其他资源。

Jaeger 可以以三种主要配置运行:AllInOneProductionStreaming。对这些配置的全面讨论超出了本书的范围(请查看之前分享的 Jaeger 文档链接),但我们将使用 AllInOne 配置。这个配置将 Jaeger UI,Collector,Agent 和 Ingestor 组合成一个单独的 Pod,不包括任何持久存储。这非常适合演示目的 - 要查看生产就绪的配置,请查看 Jaeger 文档。

为了创建我们的 Jaeger 部署,我们需要告诉 Jaeger Operator 我们选择的配置。我们使用之前创建的 CRD - Jaeger CRD 来做到这一点。为此创建一个新文件:

Jaeger-allinone.yaml

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: all-in-one
  namespace: observability
spec:
  strategy: allInOne

我们只是使用了可能的 Jaeger 类型配置的一个小子集 - 再次查看完整的文档以了解全部情况。

现在,我们可以通过运行以下命令来创建我们的 Jaeger 实例:

Kubectl apply -f jaeger-allinone.yaml

这个命令创建了我们之前安装的 Jaeger CRD 的一个实例。此时,Jaeger Operator 应该意识到已经创建了 CRD。不到一分钟,我们的实际 Jaeger Pod 应该正在运行。我们可以通过以下命令列出 observability 命名空间中的所有 Pod 来检查:

Kubectl get po -n observability

作为输出,您应该看到为我们的全功能实例新创建的 Jaeger Pod:

NAME                         READY   STATUS    RESTARTS   AGE
all-in-one-12t6bc95sr-aog4s  1/1     Running   0          5m

当我们的集群上也运行有 Ingress 控制器时,Jaeger Operator 会创建一个 Ingress 记录。这意味着我们可以简单地使用 kubectl 列出我们的 Ingress 条目,以查看如何访问 Jaeger UI。

您可以使用这个命令列出 Ingress:

Kubectl get ingress -n observability

输出应该显示您的 Jaeger UI 的新 Ingress,如下所示:

图 9.21 - Jaeger UI 服务输出

图 9.21 - Jaeger UI 服务输出

现在,您可以导航到集群 Ingress 记录中列出的地址,查看 Jaeger UI。它应该看起来像下面这样:

图 9.22 – Jaeger UI

图 9.22 – Jaeger UI

如您所见,Jaeger UI 非常简单。顶部有三个标签-搜索比较系统架构。我们将专注于搜索标签,但是要了解其他两个标签的更多信息,请查看 Jaeger 文档www.jaegertracing.io

Jaeger 搜索 页面允许我们根据许多输入搜索跟踪。我们可以根据跟踪中包含的服务来搜索,也可以根据标签、持续时间等进行搜索。然而,现在我们的 Jaeger 系统中什么都没有。

原因是,即使我们已经启动并运行了 Jaeger,我们的应用程序仍然需要配置为将跟踪发送到 Jaeger。这通常需要在代码或框架级别完成,超出了本书的范围。如果您想尝试 Jaeger 的跟踪功能,可以安装一个示例应用程序-请参阅 Jaeger 文档页面www.jaegertracing.io/docs/1.18/getting-started/#sample-app-hotrod

通过服务将跟踪发送到 Jaeger,可以查看跟踪。Jaeger 中的跟踪如下所示。为了便于阅读,我们裁剪了跟踪的后面部分,但这应该可以让您对跟踪的外观有一个很好的想法:

图 9.23 – Jaeger 中的跟踪视图

图 9.23 – Jaeger 中的跟踪视图

如您所见,Jaeger UI 视图将服务跟踪分成组成部分。每个服务之间的调用,以及服务内部的任何特定调用,在跟踪中都有自己的行。您看到的水平条形图从左到右移动,每个跟踪中的单独调用都有自己的行。在这个跟踪中,您可以看到我们有 HTTP 调用、SQL 调用,以及一些 Redis 语句。

您应该能够看到 Jaeger 和一般跟踪如何帮助开发人员理清服务之间的网络调用,并帮助找到瓶颈所在。

通过对 Jaeger 的回顾,我们对可观察性桶中的每个问题都有了一个完全开源的解决方案。然而,这并不意味着没有商业解决方案有用的情况-在许多情况下是有用的。

第三方工具

除了许多开源库之外,还有许多商业产品可用于 Kubernetes 上的指标、日志和警报。其中一些可能比开源选项更强大。

通常,大多数指标和日志工具都需要您在集群上配置资源,以将指标和日志转发到您选择的服务。在本章中我们使用的示例中,这些服务在集群中运行,尽管在商业产品中,这些通常可以是单独的 SaaS 应用程序,您可以登录分析日志并查看指标。例如,在本章中我们配置的 EFK 堆栈中,您可以支付 Elastic 提供的托管解决方案,其中解决方案的 Elasticsearch 和 Kibana 部分将托管在 Elastic 的基础设施上,从而降低了解决方案的复杂性。此外,还有许多其他解决方案,包括 Sumo Logic、Logz.io、New Relic、DataDog 和 AppDynamics 等供应商提供的解决方案。

对于生产环境,通常会使用单独的计算资源(可以是单独的集群、服务或 SaaS 工具)来执行日志和指标分析。这确保了运行实际软件的集群可以专门用于应用程序,并且任何昂贵的日志搜索或查询功能可以单独处理。这也意味着,如果我们的应用程序集群崩溃,我们仍然可以查看日志和指标,直到故障发生的时刻。

总结

在本章中,我们学习了关于 Kubernetes 上的可观察性。我们首先了解了可观察性的四个主要原则:指标、日志、跟踪和警报。然后我们发现了 Kubernetes 本身提供的可观察性工具,包括它如何管理日志和资源指标,以及如何部署 Kubernetes 仪表板。最后,我们学习了如何实施和使用一些关键的开源工具,为这四个支柱提供可视化、搜索和警报。这些知识将帮助您为未来的 Kubernetes 集群构建健壮的可观察性基础设施,并帮助您决定在集群中观察什么最重要。

在下一章中,我们将运用我们在 Kubernetes 上学到的可观察性知识来帮助我们排除应用程序故障。

问题

  1. 解释指标和日志之间的区别。

  2. 为什么要使用 Grafana 而不是简单地使用 Prometheus UI?

  3. 在生产环境中运行 EFK 堆栈(以尽量减少生产应用集群的计算负载),堆栈的哪些部分会在生产应用集群上运行?哪些部分会在集群外运行?

进一步阅读

第十章:排除故障的 Kubernetes

本章将审查有效排除 Kubernetes 集群和运行在其中的应用程序的最佳实践方法。这包括讨论常见的 Kubernetes 问题,以及如何分别调试主节点和工作节点。常见的 Kubernetes 问题将以案例研究的形式进行讨论和教学,分为集群问题和应用程序问题。

我们将首先讨论一些常见的 Kubernetes 故障模式,然后再讨论如何最好地排除集群和应用程序的故障。

在本章中,我们将涵盖以下主题:

  • 理解分布式应用的故障模式

  • 排除故障的 Kubernetes 集群

  • 在 Kubernetes 上排除故障

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个正常运行的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书籍的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter10

理解分布式应用的故障模式

默认情况下,Kubernetes 组件(以及在 Kubernetes 上运行的应用程序)是分布式的,如果它们运行多个副本。这可能导致一些有趣的故障模式,这些故障模式可能很难调试。

因此,如果应用程序是无状态的,它们在 Kubernetes 上就不太容易失败-在这种情况下,状态被卸载到在 Kubernetes 之外运行的缓存或数据库中。Kubernetes 的原语,如 StatefulSets 和 PersistentVolumes,可以使在 Kubernetes 上运行有状态的应用程序变得更加容易-并且随着每个版本的发布,在 Kubernetes 上运行有状态的应用程序的体验也在不断改善。然而,决定在 Kubernetes 上运行完全有状态的应用程序会引入复杂性,因此也会增加失败的可能性。

分布式应用程序的故障可能由许多不同的因素引起。诸如网络可靠性和带宽限制等简单事物可能会导致重大问题。这些问题如此多样化,以至于Sun MicrosystemsPeter Deutsch帮助撰写了分布式计算的谬论(连同James Gosling一起添加了第八点),这些谬论是关于分布式应用程序失败的共识因素。在论文解释分布式计算的谬论中,Arnon Rotem-Gal-Oz讨论了这些谬论的来源。

这些谬论按照数字顺序如下:

  1. 网络是可靠的。

  2. 延迟为零。

  3. 带宽是无限的。

  4. 网络是安全的。

  5. 拓扑结构不会改变。

  6. 只有一个管理员。

  7. 传输成本为零。

  8. 网络是同质的。

Kubernetes 在设计和开发时考虑了这些谬论,因此更具有容忍性。它还有助于解决在 Kubernetes 上运行的应用程序的这些问题-但并非完美。因此,当您的应用程序在 Kubernetes 上进行容器化并运行时,很可能会在面对这些问题时出现问题。每个谬论,当假设为不真实并推导到其逻辑结论时,都可能在分布式应用程序中引入故障模式。让我们逐个讨论 Kubernetes 和在 Kubernetes 上运行的应用程序的每个谬论。

网络是可靠的。

在多个逻辑机器上运行的应用程序必须通过互联网进行通信-因此,网络中的任何可靠性问题都可能引入问题。特别是在 Kubernetes 上,控制平面本身可以在高度可用的设置中进行分布(这意味着具有多个主节点的设置-请参见第一章与 Kubernetes 通信),这意味着故障模式可能会在控制器级别引入。如果网络不可靠,那么 kubelet 可能无法与控制平面进行通信,从而导致 Pod 放置问题。

同样,控制平面的节点可能无法彼此通信-尽管etcd当然是使用一致性协议构建的,可以容忍通信故障。

最后,工作节点可能无法彼此通信-在微服务场景中,这可能会根据 Pod 的放置而引起问题。在某些情况下,工作节点可能都能够与控制平面通信,但仍然无法彼此通信,这可能会导致 Kubernetes 叠加网络出现问题。

与一般的不可靠性一样,延迟也可能引起许多相同的问题。

延迟是零

如果网络延迟显着,许多与网络不可靠性相同的故障也会适用。例如,kubelet 和控制平面之间的调用可能会失败,导致etcd中出现不准确的时期,因为控制平面可能无法联系 kubelet-或者正确更新etcd。同样,如果运行在工作节点上的应用程序之间的请求丢失,否则如果这些应用程序在同一节点上共存,则可以完美运行。

带宽是无限的

带宽限制可能会暴露与前两个谬论类似的问题。Kubernetes 目前没有完全支持的方法来基于带宽订阅来放置 Pod。这意味着达到网络带宽限制的节点仍然可以安排新的 Pod,导致请求的失败率和延迟问题增加。已经有要求将此作为核心 Kubernetes 调度功能添加的请求(基本上,一种根据节点带宽消耗进行调度的方法,就像 CPU 和内存一样),但目前,解决方案大多受限于容器网络接口(CNI)插件。

重要提示

举例来说,CNI 带宽插件支持在 Pod 级别进行流量整形-请参阅kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping

第三方 Kubernetes 网络实现也可能提供围绕带宽的附加功能-并且许多与 CNI 带宽插件兼容。

网络是安全的

网络安全的影响远不止于 Kubernetes——因为任何不安全的网络都可能遭受各种攻击。攻击者可能能够获得对 Kubernetes 集群中的主节点或工作节点的 SSH 访问权限,这可能会造成重大破坏。由于 Kubernetes 的许多功能都是通过网络而不是在单台机器上完成的,因此在攻击情况下对网络的访问会变得更加棘手。

拓扑结构不会改变

这种谬误在 Kubernetes 的背景下尤为重要,因为不仅可以通过添加和移除新节点来改变元网络拓扑结构,覆盖网络拓扑结构也会直接受到 Kubernetes 控制平面和 CNI 的影响。

因此,一个应用程序在某一时刻在一个逻辑位置运行,可能在网络中的完全不同位置运行。因此,使用 Pod IP 来识别逻辑应用程序是一个不好的主意——这是服务抽象的一个目的(参见第五章服务和入口——与外部世界通信)。任何不考虑集群内部拓扑结构(至少涉及 IP)的应用程序可能会出现问题。例如,将应用程序路由到特定的 Pod IP 只能在该 Pod 发生变化之前起作用。如果该 Pod 关闭,控制它的部署(例如)将启动一个新的 Pod 来替代它,但 IP 将完全不同。集群 DNS(以及由此衍生的服务)为在集群中的应用程序之间进行请求提供了更好的方式,除非您的应用程序具有动态调整到集群变化(如 Pod 位置)的能力。

只有一个管理员

在基础网络中,多个管理员和冲突的规则可能会导致问题,多个 Kubernetes 管理员还可能通过更改资源配置(例如 Pod 资源限制)而引发进一步的问题,导致意外行为。使用 Kubernetes 的基于角色的访问控制RBAC)功能可以通过为 Kubernetes 用户提供他们所需的权限(例如只读权限)来解决这个问题。

运输成本为零

这种谬误有两种常见的解释方式。首先,传输的延迟成本为零 - 这显然是不真实的,因为数据在电线上传输的速度并不是无限的,而且更低级的网络问题会增加延迟。这与“延迟为零”谬误产生的影响本质上是相同的。

其次,这个声明可以被解释为创建和操作网络传输的成本为零 - 就像零美元和零美分一样。虽然这也是显然不真实的(只需看看您的云服务提供商的数据传输费用就可以证明),但这并不特别对应于 Kubernetes 上的应用程序故障排查,所以我们将专注于第一种解释。

网络是同质的

这个最后的谬误与 Kubernetes 的组件关系不大,而与在 Kubernetes 上运行的应用程序有更多关系。然而,事实是,今天的环境中操作的开发人员都清楚地知道,应用程序网络可能在不同的应用程序中有不同的实现 - 从 HTTP 1 和 2 到诸如 gRPC 的协议。

现在我们已经回顾了一些 Kubernetes 应用失败的主要原因,我们可以深入研究排查 Kubernetes 和在 Kubernetes 上运行的应用程序的实际过程。

排查 Kubernetes 集群

由于 Kubernetes 是一个分布式系统,旨在容忍应用程序运行的故障,大多数(但不是全部)问题往往集中在控制平面和 API 上。在大多数情况下,工作节点的故障只会导致 Pod 被重新调度到另一个节点 - 尽管复合因素可能会引入问题。

为了演示常见的 Kubernetes 集群问题场景,我们将使用案例研究方法。这应该为您提供调查真实世界集群问题所需的所有工具。我们的第一个案例研究集中在 API 服务器本身的故障上。

重要提示

在本教程中,我们将假设一个自管理的集群。托管的 Kubernetes 服务,如 EKS、AKS 和 GKE 通常会消除一些故障域(例如通过自动缩放和管理主节点)。一个好的规则是首先检查您的托管服务文档,因为任何问题可能是特定于实现的。

案例研究 - Kubernetes Pod 放置失败

让我们来设定场景。您的集群正在运行,但是您遇到了 Pod 调度的问题。Pods 一直停留在 Pending 状态,无限期地。让我们用以下命令确认一下:

kubectl get pods

命令的输出如下:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-tj8ks        0/1       Pending   0          2d
app-1-pod-2821252345-9fj2k        0/1       Pending   0          2d
app-1-pod-2821252345-06hdj        0/1       Pending   0          2d

正如我们所看到的,我们的 Pod 都没有在运行。此外,我们正在运行应用程序的三个副本,但没有一个被调度。下一个很好的步骤是检查节点状态,看看是否有任何问题。运行以下命令以获取输出:

kubectl get nodes

我们得到以下输出:

  NAME           STATUS     ROLES    AGE    VERSION
  node-01        NotReady   <none>   5m     v1.15.6

这个输出给了我们一些很好的信息 - 我们只有一个工作节点,并且它无法用于调度。当 get 命令没有给我们足够的信息时,describe 通常是一个很好的下一步。

让我们运行 kubectl describe node node-01 并检查 conditions 键。我们已经删除了一列,以便将所有内容整齐地显示在页面上,但最重要的列都在那里:

图 10.1 - 描述节点条件输出

图 10.1 - 描述节点条件输出

我们在这里有一个有趣的分裂:MemoryPressureDiskPressure 都很好,而 OutOfDiskReady 条件的状态是未知的,消息是 kubelet stopped posting node status。乍一看,这似乎是荒谬的 - MemoryPressureDiskPressure 怎么可能正常,而 kubelet 却停止工作了呢?

重要的部分在 LastTransitionTime 列中。kubelet 最近的内存和磁盘特定通信发送了积极的状态。然后,在稍后的时间,kubelet 停止发布其节点状态,导致 OutOfDiskReady 条件的状态为 Unknown

在这一点上,我们可以肯定我们的节点是问题所在 - kubelet 不再将节点状态发送到控制平面。然而,我们不知道为什么会发生这种情况。可能是网络错误,机器本身的问题,或者更具体的问题。我们需要进一步挖掘才能弄清楚。

在这里一个很好的下一步是接近我们的故障节点,因为我们可以合理地假设它遇到了某种问题。如果您可以访问 node-01 VM 或机器,现在是 SSH 进入的好时机。一旦我们进入机器,让我们进一步进行故障排除。

首先,让我们检查节点是否可以通过网络访问控制平面。如果不能,这显然是 kubelet 无法发布状态的明显原因。假设我们的集群控制平面(例如,本地负载均衡器)位于10.231.0.1,为了检查我们的节点是否可以访问 Kubernetes API 服务器,我们可以像下面这样 ping 控制平面:

ping 10.231.0.1   

重要提示

为了找到控制平面的 IP 或 DNS,请检查您的集群配置。在 AWS Elastic Kubernetes Service 或 Azure AKS 等托管的 Kubernetes 服务中,这可能可以在控制台中查看。例如,如果您使用 kubeadm 自己引导了集群,那么这是您在安装过程中提供的值之一。

让我们来检查结果:

Reply from 10.231.0.1: bytes=1500 time=28ms TTL=54
Reply from 10.231.0.1: bytes=1500 time=26ms TTL=54
Reply from 10.231.0.1: bytes=1500 time=27ms TTL=54

这证实了 - 我们的节点确实可以与 Kubernetes 控制平面通信。因此,网络不是问题。接下来,让我们检查实际的 kubelet 服务。节点本身似乎是正常运行的,网络也正常,所以逻辑上,kubelet 是下一个要检查的东西。

Kubernetes 组件在 Linux 节点上作为系统服务运行。

重要提示

在 Windows 节点上,故障排除说明会略有不同 - 请参阅 Kubernetes 文档以获取更多信息(kubernetes.io/docs/setup/production-environment/windows/intro-windows-in-kubernetes/)。

为了找出我们的kubelet服务的状态,我们可以运行以下命令:

systemctl status kubelet -l 

这给我们以下输出:

 • kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/lib/systemd/system/kubelet.service; enabled)
  Drop-In: /etc/systemd/system/kubelet.service.d
           └─10-kubeadm.conf
   Active: activating (auto-restart) (Result: exit-code) since Fri 2020-05-22 05:44:25 UTC; 3s ago
     Docs: http://kubernetes.io/docs/
  Process: 32315 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_CERTIFICATE_ARGS $KUBELET_EXTRA_ARGS (code=exited, status=1/FAILURE)
 Main PID: 32315 (code=exited, status=1/FAILURE)

看起来我们的 kubelet 目前没有运行 - 它以失败退出。这解释了我们所看到的集群状态和 Pod 问题。

实际上修复问题,我们可以首先尝试使用以下命令重新启动kubelet

systemctl start kubelet

现在,让我们使用我们的状态命令重新检查kubelet的状态:

 • kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/lib/systemd/system/kubelet.service; enabled)
  Drop-In: /etc/systemd/system/kubelet.service.d
           └─10-kubeadm.conf
   Active: activating (auto-restart) (Result: exit-code) since Fri 2020-05-22 06:13:48 UTC; 10s ago
     Docs: http://kubernetes.io/docs/
  Process: 32007 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_CERTIFICATE_ARGS $KUBELET_EXTRA_ARGS (code=exited, status=1/FAILURE)
 Main PID: 32007 (code=exited, status=1/FAILURE)

看起来kubelet又失败了。我们需要获取一些关于失败模式的额外信息,以便找出发生了什么。

让我们使用journalctl命令查看是否有相关的日志:

sudo journalctl -u kubelet.service | grep "failed"

输出应该显示kubelet服务的日志,其中发生了故障:

May 22 04:19:16 nixos kubelet[1391]: F0522 04:19:16.83719    1287 server.go:262] failed to run Kubelet: Running with swap on is not supported, please disable swap! or set --fail-swap-on flag to false. /proc/swaps contained: [Filename                                Type                Size        Used        Priority /dev/sda1                               partition        6198732        0        -1]

看起来我们已经找到了原因-Kubernetes 默认不支持在 Linux 机器上运行时将swap设置为on。我们在这里的唯一选择要么是禁用swap,要么是使用设置为false--fail-swap-on标志重新启动kubelet

在我们的情况下,我们将使用以下命令更改swap设置:

sudo swapoff -a

现在,重新启动kubelet服务:

sudo systemctl restart kubelet

最后,让我们检查一下我们的修复是否奏效。使用以下命令检查节点:

kubectl get nodes 

这应该显示类似于以下内容的输出:

  NAME           STATUS     ROLES    AGE    VERSION
  node-01        Ready      <none>   54m    v1.15.6

我们的节点最终发布了Ready状态!

让我们使用以下命令检查我们的 Pod:

kubectl get pods

这应该显示如下输出:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-tj8ks        1/1       Running   0          1m
app-1-pod-2821252345-9fj2k        1/1       Running   0          1m
app-1-pod-2821252345-06hdj        1/1       Running   0          1m

成功!我们的集群健康,我们的 Pod 正在运行。

接下来,让我们看看在解决了任何集群问题后如何排除 Kubernetes 上的应用程序故障。

在 Kubernetes 上排除应用程序故障

一个完全运行良好的 Kubernetes 集群可能仍然存在需要调试的应用程序问题。这可能是由于应用程序本身的错误,也可能是由于组成应用程序的 Kubernetes 资源的错误配置。与排除集群故障一样,我们将通过使用案例研究来深入了解这些概念。

案例研究 1-服务无响应

我们将把这一部分分解为 Kubernetes 堆栈各个级别的故障排除,从更高级别的组件开始,然后深入到 Pod 和容器调试。

假设我们已经配置我们的应用程序app-1通过NodePort服务响应端口32688的请求。该应用程序监听端口80

我们可以尝试通过在我们的节点之一上使用curl请求来访问我们的应用程序。命令将如下所示:

curl http://10.213.2.1:32688

如果 curl 命令失败,输出将如下所示:

curl: (7) Failed to connect to 10.231.2.1 port 32688: Connection refused

此时,我们的NodePort服务没有将请求路由到任何 Pod。按照我们典型的调试路径,让我们首先查看使用以下命令在集群中运行的哪些资源:

kubectl get services

添加-o宽标志以查看更多信息。接下来,运行以下命令:

kubectl get services -o wide 

这给了我们以下输出:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR 
app-1-svc NodePort 10.101.212.57 <none> 80:32688/TCP 3m01s app=app-1

很明显,我们的服务存在一个正确的节点端口-但是我们的请求没有被路由到 Pod,这是从失败的 curl 命令中显而易见的。

要查看我们的服务设置了哪些路由,让我们使用get endpoints命令。这将列出服务配置的 Pod IP(如果有的话)。

kubectl get endpoints app-1-svc

让我们检查命令的结果输出:

NAME        ENDPOINTS
app-1-svc   <none>

嗯,这里肯定有问题。

我们的服务没有指向任何 Pod。这很可能意味着没有任何与我们的服务选择器匹配的 Pod 可用。这可能是因为根本没有可用的 Pod - 或者因为这些 Pod 不正确地匹配了服务选择器。

要检查我们的服务选择器,让我们沿着调试路径迈出下一步,并使用以下命令:

kubectl describe service app-1-svc  

这给我们一个类似以下的输出:

Name:                   app-1-svc
Namespace:              default
Labels:                 app=app-11
Annotations:            <none>
Selector:               app=app-11
Type:                   NodePort
IP:                     10.57.0.15
Port:                   <unset> 80/TCP
TargetPort:             80/TCP
NodePort:               <unset> 32688/TCP
Endpoints:              <none>
Session Affinity:       None
Events:                 <none>

正如您所看到的,我们的服务配置为与我们的应用程序上的正确端口进行通信。但是,选择器正在寻找与标签app = app-11匹配的 Pod。由于我们知道我们的应用程序名称为app-1,这可能是我们问题的原因。

让我们编辑我们的服务,以寻找正确的 Pod 标签app-1,再次运行另一个describe命令以确保:

kubectl describe service app-1-svc

这会产生以下输出:

Name:                   app-1-svc
Namespace:              default
Labels:                 app=app-1
Annotations:            <none>
Selector:               app=app-1
Type:                   NodePort
IP:                     10.57.0.15
Port:                   <unset> 80/TCP
TargetPort:             80/TCP
NodePort:               <unset> 32688/TCP
Endpoints:              <none>
Session Affinity:       None
Events:                 <none>

现在,您可以在输出中看到我们的服务正在寻找正确的 Pod 选择器,但我们仍然没有任何端点。让我们使用以下命令来查看我们的 Pod 的情况:

kubectl get pods

这显示了以下输出:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-tj8ks        0/1       Pending   0          -
app-1-pod-2821252345-9fj2k        0/1       Pending   0          -
app-1-pod-2821252345-06hdj        0/1       Pending   0          -

我们的 Pod 仍在等待调度。这解释了为什么即使有正确的选择器,我们的服务也无法正常运行。为了更细致地了解为什么我们的 Pod 没有被调度,让我们使用describe命令:

kubectl describe pod app-1-pod-2821252345-tj8ks

以下是输出。让我们专注于“事件”部分:

图 10.2 - 描述 Pod 事件输出

图 10.2 - 描述 Pod 事件输出

从“事件”部分来看,似乎我们的 Pod 由于容器镜像拉取失败而无法被调度。这可能有很多原因 - 例如,我们的集群可能没有必要的身份验证机制来从私有仓库拉取,但这会出现不同的错误消息。

从上下文和“事件”输出来看,我们可能可以假设问题在于我们的 Pod 定义正在寻找一个名为myappimage:lates的容器,而不是myappimage:latest

让我们使用正确的镜像名称更新我们的部署规范并进行更新。

使用以下命令来确认:

kubectl get pods

输出看起来像这样:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-152sf        1/1       Running   0          1m
app-1-pod-2821252345-9gg9s        1/1       Running   0          1m
app-1-pod-2821252345-pfo92        1/1       Running   0          1m

我们的 Pod 现在正在运行 - 让我们检查一下我们的服务是否已注册了正确的端点。使用以下命令来执行此操作:

kubectl describe services app-1-svc

输出应该是这样的:

Name:                   app-1-svc
Namespace:              default
Labels:                 app=app-1
Annotations:            <none>
Selector:               app=app-1
Type:                   NodePort
IP:                     10.57.0.15
Port:                   <unset> 80/TCP
TargetPort:             80/TCP
NodePort:               <unset> 32688/TCP
Endpoints:              10.214.1.3:80,10.214.2.3:80,10.214.4.2:80
Session Affinity:       None
Events:                 <none>

成功!我们的服务正确地指向了我们的应用程序 Pod。

在下一个案例研究中,我们将通过排除具有不正确启动参数的 Pod 来深入挖掘一些问题。

案例研究 2 - 错误的 Pod 启动命令

让我们假设我们的 Service 已经正确配置,我们的 Pods 正在运行并通过健康检查。然而,我们的 Pod 没有按照我们的预期响应请求。我们确信这不是 Kubernetes 的问题,而更多是应用程序或配置的问题。

我们的应用程序容器工作方式如下:它接受一个带有color标志的启动命令,并根据容器的image标签的version number变量组合起来,并将其回显给请求者。我们期望我们的应用程序返回green 3

幸运的是,Kubernetes 为我们提供了一些很好的工具来调试应用程序,我们可以用这些工具来深入研究我们特定的容器。

首先,让我们使用以下命令curl应用程序,看看我们得到什么响应:

curl http://10.231.2.1:32688  
red 2

我们期望得到green 3,但得到了red 2,所以看起来输入和版本号变量出了问题。让我们先从前者开始。

像往常一样,我们首先用以下命令检查我们的 Pods:

kubectl get pods

输出应该如下所示:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-152sf        1/1       Running   0          5m
app-1-pod-2821252345-9gg9s        1/1       Running   0          5m
app-1-pod-2821252345-pfo92        1/1       Running   0          5m

这个输出看起来很好。我们的应用程序似乎作为部署的一部分运行(因此也是 ReplicaSet) - 我们可以通过运行以下命令来确保:

kubectl get deployments

输出应该如下所示:

NAME          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
app-1-pod     3         3         3            3           5m

让我们更仔细地查看我们的部署,看看我们的 Pods 是如何配置的,使用以下命令:

kubectl describe deployment app-1-pod -o yaml

输出应该如下所示:

Broken-deployment-output.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-1-pod
spec:
  selector:
    matchLabels:
      app: app-1
  replicas: 3
  template:
    metadata:
      labels:
        app: app-1
    spec:
      containers:
      - name: app-1
        image: mycustomrepository/app-1:2
        command: [ "start", "-color", "red" ]
        ports:
        - containerPort: 80

让我们看看是否可以解决我们的问题,这实际上非常简单。我们使用了错误版本的应用程序,而且我们的启动命令也是错误的。在这种情况下,让我们假设我们没有一个包含我们部署规范的文件 - 所以让我们直接在原地编辑它。

让我们使用kubectl edit deployment app-1-pod,并编辑 Pod 规范如下:

fixed-deployment-output.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-1-pod
spec:
  selector:
    matchLabels:
      app: app-1
  replicas: 3
  template:
    metadata:
      labels:
        app: app-1
    spec:
      containers:
      - name: app-1
        image: mycustomrepository/app-1:3
        command: [ "start", "-color", "green" ]
        ports:
        - containerPort: 80

一旦部署保存,你应该开始看到你的新 Pods 启动。让我们通过以下命令再次检查:

 kubectl get pods

输出应该如下所示:

NAME                              READY     STATUS    RESTARTS   AGE
app-1-pod-2821252345-f928a        1/1       Running   0          1m
app-1-pod-2821252345-jjsa8        1/1       Running   0          1m
app-1-pod-2821252345-92jhd        1/1       Running   0          1m

最后 - 让我们发出一个curl请求来检查一切是否正常运行:

curl http://10.231.2.1:32688  

命令的输出如下:

green 3

成功!

案例研究 3 - Pod 应用程序日志故障

在上一章[第九章](B14790_9_Final_PG_ePub.xhtml#_idTextAnchor212),Kubernetes 上的可观测性中,我们为我们的应用程序实现了可观测性,让我们看一个案例,这些工具确实非常有用。我们将使用手动的kubectl命令来进行这个案例研究 - 但要知道,通过聚合日志(例如,在我们的 EFK 堆栈实现中),我们可以使调试这个应用程序的过程变得更容易。

在这个案例研究中,我们再次部署了 Pod - 为了检查它,让我们运行以下命令:

kubectl get pods

命令的输出如下:

NAME              READY     STATUS    RESTARTS   AGE
app-2-ss-0        1/1       Running   0          10m
app-2-ss-1       1/1       Running   0          10m
app-2-ss-2       1/1       Running   0          10m

看起来,在这种情况下,我们使用的是 StatefulSet 而不是 Deployment - 这里的一个关键特征是从 0 开始递增的 Pod ID。

我们可以通过使用以下命令来确认这一点:

kubectl get statefulset

命令的输出如下:

NAME          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
app-2-ss      3         3         3            3           10m

让我们使用kubectl get statefulset -o yaml app-2-ss来更仔细地查看我们的 StatefulSet。通过使用get命令以及-o yaml,我们可以以与典型的 Kubernetes 资源 YAML 相同的格式获得我们的describe输出。

上述命令的输出如下。我们已经删除了 Pod 规范部分以使其更短:

statefulset-output.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: app-2-ss
spec:
  selector:
    matchLabels:
      app: app-2
  replicas: 3
  template:
    metadata:
      labels:
        app: app-2

我们知道我们的应用程序正在使用一个服务。让我们看看是哪一个!

运行 kubectl get services -o wide。输出应该类似于以下内容:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR 
app-2-svc NodePort 10.100.213.13 <none> 80:32714/TCP 3m01s app=app-2

很明显我们的服务叫做app-2-svc。让我们使用以下命令查看我们的确切服务定义:

kubectl describe services app-2-svc 

命令的输出如下:

Name:                   app-2-svc
Namespace:              default
Labels:                 app=app-2
Annotations:            <none>
Selector:               app=app-2
Type:                   NodePort
IP:                     10.57.0.12
Port:                   <unset> 80/TCP
TargetPort:             80/TCP
NodePort:               <unset> 32714/TCP
Endpoints:              10.214.1.1:80,10.214.2.3:80,10.214.4.4:80
Session Affinity:       None
Events:                 <none>

要确切地查看我们的应用程序对于给定输入返回的内容,我们可以在我们的NodePort服务上使用curl

> curl http://10.231.2.1:32714?equation=1plus1
3

根据我们对应用程序的现有知识,我们会假设这个调用应该返回2而不是3。我们团队的应用程序开发人员已经要求我们调查任何日志输出,以帮助他们找出问题所在。

我们知道从之前的章节中,你可以使用kubectl logs <pod name>来调查日志输出。在我们的情况下,我们有三个应用程序的副本,所以我们可能无法在一次迭代中找到我们的日志。让我们随机选择一个 Pod,看看它是否是为我们提供服务的那个:

> kubectl logs app-2-ss-1
>

看起来这不是为我们提供服务的 Pod,因为我们的应用程序开发人员告诉我们,当向服务器发出GET请求时,应用程序肯定会记录到stdout

我们可以使用联合命令从所有三个 Pod 中获取日志,而不是逐个检查另外两个 Pod。命令将如下:

> kubectl logs statefulset/app-2-ss

输出如下:

> Input = 1plus1
> Operator = plus
> First Number = 1
> Second Number = 2

这样就解决了问题 - 而且更重要的是,我们可以看到一些关于我们问题的很好的见解。

除了日志行读取Second Number之外,一切都如我们所期望的那样。我们的请求明显使用1plus1作为查询字符串,这将使第一个数字和第二个数字(由运算符值分隔)都等于一。

这将需要一些额外的挖掘。我们可以通过发送额外的请求并检查输出来对这个问题进行分类,以猜测发生了什么,但在这种情况下,最好只是获取对 Pod 的 bash 访问并弄清楚发生了什么。

首先,让我们检查一下我们的 Pod 规范,这是从前面的 StatefulSet YAML 中删除的。要查看完整的 StatefulSet 规范,请检查 GitHub 存储库:

Statefulset-output.yaml

spec:
  containers:
  - name: app-2
    image: mycustomrepository/app-2:latest
    volumeMounts:
    - name: scratch
      mountPath: /scratch
  - name: sidecar
    image: mycustomrepository/tracing-sidecar
  volumes:
  - name: scratch-volume
    emptyDir: {}

看起来我们的 Pod 正在挂载一个空卷作为临时磁盘。每个 Pod 中还有两个容器 - 一个用于应用程序跟踪的 sidecar,以及我们的应用程序本身。我们需要这些信息来使用kubectl exec命令ssh到其中一个 Pod(对于这个练习来说,无论选择哪一个都可以)。

我们可以使用以下命令来完成:

kubectl exec -it app-2-ss-1 app2 -- sh.  

这个命令应该给你一个 bash 终端作为输出:

> kubectl exec -it app-2-ss-1 app2 -- sh
# 

现在,使用我们刚创建的终端,我们应该能够调查我们的应用程序代码。在本教程中,我们使用了一个非常简化的 Node.js 应用程序。

让我们检查一下我们的 Pod 文件系统,看看我们使用以下命令在处理什么:

# ls
# app.js calculate.js scratch

看起来我们有两个 JavaScript 文件,以及我们之前提到的scratch文件夹。可以假设app.js包含引导和提供应用程序的逻辑,而calculate.js包含我们的控制器代码来进行计算。

我们可以通过打印calculate.js文件的内容来确认:

Broken-calculate.js

# cat calculate.js
export const calculate(first, second, operator)
{
  second++;
  if(operator === "plus")
  {
   return first + second;
  }
}

即使对 JavaScript 几乎一无所知,这里的问题也是非常明显的。代码在执行计算之前递增了second变量。

由于我们在 Pod 内部,并且正在使用非编译语言,我们实际上可以内联编辑这个文件!让我们使用vi(或任何文本编辑器)来纠正这个文件:

# vi calculate.js

并编辑文件如下所示:

fixed-calculate.js

export const calculate(first, second, operator)
{
  if(operator === "plus")
  {
   return first + second;
  }
}

现在,我们的代码应该正常运行。重要的是要说明,这个修复只是临时的。一旦我们的 Pod 关闭或被另一个 Pod 替换,它将恢复到最初包含在容器镜像中的代码。然而,这种模式确实允许我们尝试快速修复。

在使用exit bash 命令退出exec会话后,让我们再次尝试我们的 URL:

> curl http://10.231.2.1:32714?equation=1plus1
2

正如你所看到的,我们的热修复容器显示了正确的结果!现在,我们可以使用我们的修复以更加永久的方式更新我们的代码和 Docker 镜像。使用exec是一个很好的方法来排除故障和调试运行中的容器。

总结

在本章中,我们学习了如何在 Kubernetes 上调试应用程序。首先,我们介绍了分布式应用程序的一些常见故障模式。然后,我们学习了如何对 Kubernetes 组件的问题进行分类。最后,我们回顾了几种 Kubernetes 配置和应用程序调试的场景。在本章中学到的 Kubernetes 调试和故障排除技术将帮助你在处理任何 Kubernetes 集群和应用程序的问题时。

在下一章,第十一章Kubernetes 上的模板代码生成和 CI/CD,我们将探讨一些用于模板化 Kubernetes 资源清单和与 Kubernetes 一起进行持续集成/持续部署的生态系统扩展。

问题

  1. 分布式系统谬误“拓扑结构不会改变”如何适用于 Kubernetes 上的应用程序?

  2. Kubernetes 控制平面组件(和 kubelet)在操作系统级别是如何实现的?

  3. 当 Pod 被卡在Pending状态时,你会如何调试问题?你的第一步会是什么?第二步呢?

进一步阅读

第十一章:Kubernetes 上的模板代码生成和 CI/CD

本章讨论了一些更容易的方法,用于模板化和配置具有许多资源的大型 Kubernetes 部署。它还详细介绍了在 Kubernetes 上实施持续集成/持续部署CI/CD)的多种方法,以及与每种可能方法相关的利弊。具体来说,我们谈论了集群内 CI/CD,其中一些或所有的 CI/CD 步骤在我们的 Kubernetes 集群中执行,以及集群外 CI/CD,其中所有步骤都在我们的集群之外进行。

本章的案例研究将包括从头开始创建 Helm 图表,以及对 Helm 图表的每个部分及其工作原理的解释。

首先,我们将介绍 Kubernetes 资源模板生成的概况,以及为什么应该使用模板生成工具。然后,我们将首先使用 AWS CodeBuild,然后使用 FluxCD 来实施 CI/CD 到 Kubernetes。

在本章中,我们将涵盖以下主题:

  • 了解在 Kubernetes 上进行模板代码生成的选项

  • 使用 Helm 和 Kustomize 在 Kubernetes 上实施模板

  • 了解 Kubernetes 上的 CI/CD 范式-集群内和集群外

  • 在 Kubernetes 上实施集群内和集群外的 CI/CD

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参考第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装 kubectl 工具的说明。此外,您还需要一台支持 Helm CLI 工具的机器,通常具有与 kubectl 相同的先决条件-有关详细信息,请查看 Helm 文档helm.sh/docs/intro/install/

本章中使用的代码可以在书籍的 GitHub 存储库中找到

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter11

了解在 Kubernetes 上进行模板代码生成的选项

正如在第一章中讨论的那样,与 Kubernetes 通信,Kubernetes 最大的优势之一是其 API 可以通过声明性资源文件进行通信。这使我们能够运行诸如kubectl apply之类的命令,并确保控制平面确保集群中运行的任何资源与我们的 YAML 或 JSON 文件匹配。

然而,这种能力引入了一些难以控制的因素。由于我们希望将所有工作负载声明在配置文件中,任何大型或复杂的应用程序,特别是如果它们包含许多微服务,可能会导致大量的配置文件编写和维护。

这个问题在多个环境下会更加复杂。假设我们需要开发、暂存、UAT 和生产环境,这将需要每个 Kubernetes 资源四个单独的 YAML 文件,假设我们想要保持每个文件一个资源的清晰度。

解决这些问题的一种方法是使用支持变量的模板系统,允许单个模板文件适用于多个应用程序或多个环境,通过注入不同的变量集。

有几种受社区支持的流行开源选项可用于此目的。在本书中,我们将重点关注其中两种最受欢迎的选项。

  • Helm

  • Kustomize

有许多其他选项可供选择,包括 Kapitan、Ksonnet、Jsonnet 等,但本书不在讨论范围之内。让我们先来回顾一下 Helm,它在很多方面都是最受欢迎的模板工具。

Helm

Helm 实际上扮演了模板/代码生成工具和 CI/CD 工具的双重角色。它允许您创建基于 YAML 的模板,可以使用变量进行填充,从而实现跨应用程序和环境的代码和模板重用。它还配备了一个 Helm CLI 工具,可以根据模板本身来推出应用程序的更改。

因此,你可能会在 Kubernetes 生态系统中到处看到 Helm 作为安装工具或应用程序的默认方式。在本章中,我们将使用 Helm 来完成它的两个目的。

现在,让我们转向 Kustomize,它与 Helm 有很大不同。

Kustomize

与 Helm 不同,Kustomize 得到了 Kubernetes 项目的官方支持,并且支持直接集成到kubectl中。与 Helm 不同,Kustomize 使用原始的 YAML 而不是变量,并建议使用fork and patch工作流,在这个工作流中,YAML 的部分根据所选择的补丁被替换为新的 YAML。

既然我们对这些工具的区别有了基本的了解,我们可以在实践中使用它们。

使用 Helm 和 Kustomize 在 Kubernetes 上实现模板

既然我们知道了我们的选择,我们可以用一个示例应用程序来实现它们中的每一个。这将使我们能够了解每个工具处理变量和模板化过程的具体细节。让我们从 Helm 开始。

使用 Helm 与 Kubernetes

如前所述,Helm 是一个开源项目,它使得在 Kubernetes 上模板化和部署应用程序变得容易。在本书的目的上,我们将专注于最新版本(写作时),即 Helm V3。之前的版本 Helm V2 有更多的移动部分,包括一个称为Tiller的控制器,它会在集群上运行。Helm V3 被简化了,只包含 Helm CLI 工具。然而,它在集群上使用自定义资源定义来跟踪发布,我们很快就会看到。

让我们从安装 Helm 开始。

安装 Helm

如果你想使用特定版本的 Helm,你可以按照helm.sh/docs/intro/install/中的特定版本文档来安装它。对于我们的用例,我们将简单地使用get helm脚本,它将安装最新版本。

您可以按照以下步骤获取并运行脚本:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

现在,我们应该能够运行helm命令了。默认情况下,Helm 将自动使用您现有的kubeconfig集群和上下文,因此为了在 Helm 中切换集群,您只需要使用kubectl来更改您的kubeconfig文件,就像您通常做的那样。

要使用 Helm 安装应用程序,请运行helm install命令。但是 Helm 是如何决定安装什么和如何安装的呢?我们需要讨论 Helm 图表、Helm 仓库和 Helm 发布的概念。

Helm 图表、仓库和发布

Helm 提供了一种使用变量在 Kubernetes 上模板化和部署应用程序的方法。为了做到这一点,我们通过一组模板来指定工作负载,这被称为Helm 图表

Helm 图表由一个或多个模板、一些图表元数据和一个values文件组成,该文件用最终值填充模板变量。在实践中,您将为每个环境(或应用程序,如果您正在为多个应用程序重用模板)拥有一个values文件,该文件将使用新配置填充共享模板。然后,模板和值的组合将用于在集群中安装或部署应用程序。

那么,您可以将 Helm 图表存储在哪里?您可以像对待任何其他 Kubernetes YAML 一样将它们放在 Git 存储库中(这对大多数用例都适用),但 Helm 还支持存储库的概念。Helm 存储库由 URL 表示,可以包含多个 Helm 图表。例如,Helm 在hub.helm.sh/charts上有自己的官方存储库。同样,每个 Helm 图表由一个包含元数据文件的文件夹、一个Chart.yaml文件、一个或多个模板文件以及一个可选的 values 文件组成。

为了安装具有本地 values 文件的本地 Helm 图表,您可以为每个传递路径到helm install,如以下命令所示:

helm install -f values.yaml /path/to/chart/root

然而,对于常用的安装图表,您也可以直接从图表存储库安装图表,并且您还可以选择将自定义存储库添加到本地 Helm 中,以便能够轻松地从非官方来源安装图表。

例如,为了通过官方 Helm 图表安装 Drupal,您可以运行以下命令:

helm install -f values.yaml stable/drupal

此代码从官方 Helm 图表存储库安装图表。要使用自定义存储库,您只需要首先将其添加到 Helm 中。例如,要安装托管在jetstack Helm 存储库上的cert-manager,我们可以执行以下操作:

helm repo add jetstack https://charts.jetstack.io
helm install certmanager --namespace cert-manager jetstack/cert-manager

此代码将jetstack Helm 存储库添加到本地 Helm CLI 工具中,然后通过其中托管的图表安装cert-manager。我们还将发布命名为cert-manager。Helm 发布是 Helm V3 中使用 Kubernetes secrets 实现的概念。当我们在 Helm 中创建一个发布时,它将作为同一命名空间中的一个 secret 存储。

为了说明这一点,我们可以使用前面的install命令创建一个 Helm 发布。现在让我们来做吧:

helm install certmanager --namespace cert-manager jetstack/cert-manager

该命令应该产生以下输出,具体内容可能会有所不同,取决于当前的 Cert Manager 版本。为了便于阅读,我们将输出分为两个部分。

首先,命令的输出给出了 Helm 发布的状态:

NAME: certmanager
LAST DEPLOYED: Sun May 23 19:07:04 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None

正如您所看到的,此部分包含部署的时间戳、命名空间信息、修订版本和状态。接下来,我们将看到输出的注释部分:

NOTES:
cert-manager has been deployed successfully!
In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them
can be found in our documentation:
https://cert-manager.io/docs/configuration/
For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:
https://cert-manager.io/docs/usage/ingress/

正如您所看到的,我们的 Helm install命令已经成功,这也给了我们一些来自cert-manager的信息,告诉我们如何使用它。这个输出在安装 Helm 软件包时可能会很有帮助,因为它们有时包括先前片段中的文档。现在,为了查看我们的 Kubernetes 中的发布对象是什么样子,我们可以运行以下命令:

Kubectl get secret -n cert-manager

这将产生以下输出:

图 11.1 – 来自 kubectl 的 Secrets 列表输出

图 11.1 – 来自 kubectl 的 Secrets 列表输出

正如您所看到的,其中一个密钥的类型为helm.sh/release.v1。这是 Helm 用来跟踪 Cert Manager 发布的密钥。

最后,要在 Helm CLI 中查看发布列表,我们可以运行以下命令:

helm ls -A

此命令将列出所有命名空间中的 Helm 发布(就像kubectl get pods -A会列出所有命名空间中的 pod 一样)。输出将如下所示:

图 11.2 – Helm 发布列表输出

图 11.2 – Helm 发布列表输出

现在,Helm 有更多的组件,包括升级回滚等,我们将在下一节中进行审查。为了展示 Helm 的功能,我们将从头开始创建和安装一个图表。

创建 Helm 图表

因此,我们希望为我们的应用程序创建一个 Helm 图表。让我们开始吧。我们的目标是轻松地将一个简单的 Node.js 应用程序部署到多个环境中。为此,我们将创建一个包含应用程序组件的图表,然后将其与三个单独的值文件(devstagingproduction)结合起来,以便将我们的应用程序部署到三个环境中。

让我们从 Helm 图表的文件夹结构开始。正如我们之前提到的,Helm 图表由模板、元数据文件和可选值组成。我们将在实际安装图表时注入这些值,但我们可以将我们的文件夹结构设计成这样:

Chart.yaml
charts/
templates/
dev-values.yaml
staging-values.yaml
production-values.yaml

我们还没有提到的一件事是,您实际上可以在现有图表中拥有一个 Helm 图表的文件夹!这些子图表可以将复杂的应用程序分解为组件,使其易于管理。对于本书的目的,我们将不使用子图表,但是如果您的应用程序变得过于复杂或模块化,这是一个有价值的功能。

此外,您可以看到我们为每个环境都有一个不同的环境文件,在安装命令期间我们将使用它们。

那么,Chart.yaml文件是什么样子的呢?该文件将包含有关图表的一些基本元数据,并且通常看起来至少是这样的:

apiVersion: v2
name: mynodeapp
version: 1.0.0

Chart.yaml文件支持许多可选字段,您可以在helm.sh/docs/topics/charts/中查看,但是对于本教程的目的,我们将保持简单。强制字段是apiVersionnameversion

在我们的Chart.yaml文件中,apiVersion对应于图表对应的 Helm 版本。有点令人困惑的是,当前版本的 Helm,Helm V3,使用apiVersion v2,而包括 Helm V2 在内的旧版本的 Helm 也使用apiVersion v2

接下来,name字段对应于我们图表的名称。这相当容易理解,尽管请记住,我们有能力为图表的特定版本命名 - 这对于多个环境非常方便。

最后,我们有version字段,它对应于图表的版本。该字段支持SemVer(语义化版本)。

那么,我们的模板实际上是什么样子的呢?Helm 图表在底层使用 Go 模板库(有关更多信息,请参见golang.org/pkg/text/template/),并支持各种强大的操作、辅助函数等等。现在,我们将保持极其简单,以便让您了解基础知识。有关 Helm 图表创建的全面讨论可能需要一本专门的书!

首先,我们可以使用 Helm CLI 命令自动生成我们的Chart文件夹,其中包括所有先前的文件和文件夹,减去为您生成的子图和值文件。让我们试试吧 - 首先使用以下命令创建一个新的 Helm 图表:

helm create myfakenodeapp

这个命令将在名为myfakenodeapp的文件夹中创建一个自动生成的图表。让我们使用以下命令检查我们templates文件夹的内容:

Ls myfakenodeapp/templates

这个命令将产生以下输出:

helpers.tpl
deployment.yaml
NOTES.txt
service.yaml

这个自动生成的图表可以作为起点帮助很多,但是对于本教程的目的,我们将从头开始制作这些。

创建一个名为mynodeapp的新文件夹,并将我们之前向您展示的Chart.yaml文件放入其中。然后,在里面创建一个名为templates的文件夹。

要记住的一件事是:一个 Kubernetes 资源 YAML 本身就是一个有效的 Helm 模板。在模板中使用任何变量并不是必需的。你可以只编写普通的 YAML,Helm 安装仍然可以工作。

为了展示这一点,让我们从我们的模板文件夹中添加一个单个模板文件开始。将其命名为deployment.yaml,并包含以下非变量 YAML:

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-myapp
  labels:
    app: frontend-myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend-myapp
  template:
    metadata:
      labels:
        app: frontend-myapp
    spec:
      containers:
      - name: frontend-myapp
        image: myrepo/myapp:1.0.0
        ports:
        - containerPort: 80

正如你所看到的,这个 YAML 只是一个普通的 Kubernetes 资源 YAML。我们在我们的模板中没有使用任何变量。

现在,我们有足够的内容来实际安装我们的图表。让我们接下来做这件事。

安装和卸载 Helm 图表

要使用 Helm V3 安装图表,你需要从图表的root目录运行helm install命令:

helm install myapp .

这个安装命令创建了一个名为frontend-app的 Helm 发布,并安装了我们的图表。现在,我们的图表只包括一个具有两个 pod 的单个部署,我们应该能够通过以下命令在我们的集群中看到它正在运行:

kubectl get deployment

这应该会产生以下输出:

NAMESPACE  NAME            READY   UP-TO-DATE   AVAILABLE   AGE
default    frontend-myapp  2/2     2            2           2m

从输出中可以看出,我们的 Helm install命令已经成功在 Kubernetes 中创建了一个部署对象。

卸载我们的图表同样简单。我们可以通过运行以下命令来安装通过我们的图表安装的所有 Kubernetes 资源:

helm uninstall myapp

这个uninstall命令(在 Helm V2 中是delete)只需要我们 Helm 发布的名称。

到目前为止,我们还没有使用 Helm 的真正功能 - 我们一直把它当作kubectl的替代品,没有添加任何功能。让我们通过在我们的图表中实现一些变量来改变这一点。

使用模板变量

向我们的 Helm 图表模板添加变量就像使用双括号 - {{ }} - 语法一样简单。我们在双括号中放入的内容将直接从我们在安装图表时使用的值中取出,使用点符号表示法。

让我们看一个快速的例子。到目前为止,我们的应用名称(和容器镜像名称/版本)都是硬编码到我们的 YAML 文件中的。如果我们想要使用我们的 Helm 图表部署不同的应用程序或不同的应用程序版本,这将极大地限制我们。

为了解决这个问题,我们将在我们的图表中添加模板变量。看一下这个结果模板:

Templated-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-{{ .Release.Name }}
  labels:
    app: frontend-{{ .Release.Name }}
    chartVersion: {{ .Chart.version }}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend-{{ .Release.Name }}
  template:
    metadata:
      labels:
        app: frontend-{{ .Release.Name }}
    spec:
      containers:
      - name: frontend-{{ .Release.Name }}
        image: myrepo/{{ .Values.image.name }}
:{{ .Values.image.tag }}
        ports:
        - containerPort: 80

让我们浏览一下这个 YAML 文件,审查一下我们的变量。在这个文件中,我们使用了几种不同类型的变量,但它们都使用相同的点符号表示法。

Helm 实际上支持几种不同的顶级对象。这些是您可以在模板中引用的主要对象:

  • .Chart:用于引用Chart.yaml文件中的元数据值

  • .Values:用于引用在安装时从values文件传递到图表中的值

  • .Template:用于引用当前模板文件的一些信息

  • .Release:用于引用有关 Helm 发布的信息

  • .Files:用于引用图表中不是 YAML 模板的文件(例如config文件)

  • .Capabilities:用于引用目标 Kubernetes 集群的信息(换句话说,版本)

在我们的 YAML 文件中,我们正在使用其中的几个。首先,我们在几个地方引用我们发布的name(包含在.Release对象中)。接下来,我们正在利用Chart对象将元数据注入chartVersion键中。最后,我们使用Values对象引用容器镜像的nametag

现在,我们缺少的最后一件事是我们将通过values.yaml注入的实际值,或者通过 CLI 命令。其他所有内容将使用Chart.yaml创建,或者我们将通过helm命令本身在运行时注入的值。

考虑到这一点,让我们从我们的模板创建我们的值文件,我们将在其中传递我们的图像nametag。因此,让我们以正确的格式包含它们:

image:
  name: myapp
  tag: 2.0.1

现在我们可以通过我们的 Helm 图表安装我们的应用程序!使用以下命令:

helm install myrelease -f values.yaml .

正如您所看到的,我们正在使用-f键传递我们的值(您也可以使用--values)。此命令将安装我们应用程序的发布。

一旦我们有了一个发布,我们就可以使用 Helm CLI 升级到新版本或回滚到旧版本-我们将在下一节中介绍这一点。

升级和回滚

现在我们有了一个活动的 Helm 发布,我们可以升级它。让我们对我们的values.yaml进行一些小改动:

image:
  name: myapp
  tag: 2.0.2

要使这成为我们发布的新版本,我们还需要更改我们的图表 YAML:

apiVersion: v2
name: mynodeapp
version: 1.0.1

现在,我们可以使用以下命令升级我们的发布:

helm upgrade myrelease -f values.yaml .

如果出于任何原因,我们想回滚到早期版本,我们可以使用以下命令:

helm rollback myrelease 1.0.0

正如您所看到的,Helm 允许无缝地对应用程序进行模板化、发布、升级和回滚。正如我们之前提到的,Kustomize 达到了许多相同的点,但它的方式大不相同-让我们看看。

使用 Kustomize 与 Kubernetes

虽然 Helm 图表可能会变得非常复杂,但 Kustomize 使用 YAML 而不使用任何变量,而是使用基于补丁和覆盖的方法将不同的配置应用于一组基本的 Kubernetes 资源。

使用 Kustomize 非常简单,正如我们在本章前面提到的,不需要先决条件 CLI 工具。一切都可以通过使用kubectl apply -k /path/kustomize.yaml命令来完成,而无需安装任何新内容。但是,我们还将演示使用 Kustomize CLI 工具的流程。

重要说明

要安装 Kustomize CLI 工具,您可以在kubernetes-sigs.github.io/kustomize/installation上查看安装说明。

目前,安装使用以下命令:

curl -s "https://raw.githubusercontent.com/\
kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"  | bash

现在我们已经安装了 Kustomize,让我们将 Kustomize 应用于我们现有的用例。我们将从我们的普通 Kubernetes YAML 开始(在我们开始添加 Helm 变量之前):

plain-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-myapp
  labels:
    app: frontend-myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend-myapp
  template:
    metadata:
      labels:
        app: frontend-myapp
    spec:
      containers:
      - name: frontend-myapp
        image: myrepo/myapp:1.0.0
        ports:
        - containerPort: 80

创建了初始的deployment.yaml后,我们现在可以创建一个 Kustomization 文件,我们称之为kustomize.yaml

当我们稍后使用-k参数调用kubectl命令时,kubectl将查找此kustomize YAML 文件,并使用它来确定要应用到传递给kubectl命令的所有其他 YAML 文件的补丁。

Kustomize 让我们可以修补单个值或设置自动设置的常见值。一般来说,Kustomize 会创建新行,或者如果 YAML 中的键已经存在,则更新旧行。有三种方法可以应用这些更改:

  • 在 Kustomization 文件中直接指定更改。

  • 使用PatchStrategicMerge策略和patch.yaml文件以及 Kustomization 文件。

  • 使用JSONPatch策略和patch.yaml文件以及 Kustomization 文件。

让我们从专门用于修补 YAML 的 Kustomization 文件开始。

直接在 Kustomization 文件中指定更改

如果我们想在 Kustomization 文件中直接指定更改,我们可以这样做,但我们的选择有些有限。我们可以在 Kustomization 文件中使用的键的类型如下:

  • resources-指定应在应用补丁时自定义的文件

  • transformers-直接从 Kustomization 文件中应用补丁的方法

  • generators-从 Kustomization 文件创建新资源的方法

  • meta-设置可以影响生成器、转换器和资源的元数据字段

如果我们想在 Kustomization 文件中指定直接补丁,我们需要使用转换器。前面提到的PatchStrategicMergeJSONPatch合并策略是两种转换器。然而,为了直接应用更改到 Kustomization 文件,我们可以使用几种转换器之一,其中包括commonLabelsimagesnamePrefixnameSuffix

在下面的 Kustomization 文件中,我们正在使用commonLabelsimages转换器对我们的初始部署YAML进行更改。

Deployment-kustomization-1.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
namespace: default
commonLabels:
  app: frontend-app
images:
  - name: frontend-myapp
    newTag: 2.0.0
    newName: frontend-app-1

这个特定的Kustomization.yaml文件将图像标签从1.0.0更新为2.0.0,将应用程序的名称从frontend-myapp更新为frontend-app,并将容器的名称从frontend-myapp更新为frontend-app-1

要全面了解每个转换器的具体细节,您可以查看Kustomize 文档。Kustomize 文件假定deployment.yaml与其自身在同一个文件夹中。

要查看当我们的 Kustomize 文件应用到我们的部署时的结果,我们可以使用 Kustomize CLI 工具。我们将使用以下命令生成经过自定义处理的输出:

kustomize build deployment-kustomization1.yaml

该命令将给出以下输出:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-myapp
  labels:
    app: frontend-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend-app
  template:
    metadata:
      labels:
        app: frontend-app
    spec:
      containers:
      - name: frontend-app-1
        image: myrepo/myapp:2.0.0
        ports:
        - containerPort: 80

如您所见,我们的 Kustomization 文件中的自定义已经应用。因为kustomize build命令输出 Kubernetes YAML,我们可以轻松地将输出部署到 Kubernetes,如下所示:

kustomize build deployment-kustomization.yaml | kubectl apply -f -

接下来,让我们看看如何使用带有PatchStrategicMerge的 YAML 文件来修补我们的部署。

使用 PatchStrategicMerge 指定更改

为了说明PatchStrategicMerge策略,我们再次从相同的deployment.yaml文件开始。这次,我们将通过kustomization.yaml文件和patch.yaml文件的组合来发布我们的更改。

首先,让我们创建我们的kustomization.yaml文件,它看起来像这样:

Deployment-kustomization-2.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
namespace: default
patchesStrategicMerge:
  - deployment-patch-1.yaml

正如您所见,我们的 Kustomization 文件在patchesStrategicMerge部分引用了一个新文件deployment-patch-1.yaml。这里可以添加任意数量的补丁 YAML 文件。

然后,我们的deployment-patch-1.yaml文件是一个简单的文件,镜像了我们的部署并包含我们打算进行的更改。它看起来像这样:

Deployment-patch-1.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-myapp
  labels:
    app: frontend-myapp
spec:
  replicas: 4

这个补丁文件是原始部署中字段的一个子集。在这种情况下,它只是将 replicas2 更新为 4。再次应用更改,我们可以使用以下命令:

 kustomize build deployment-kustomization2.yaml

但是,我们也可以在 kubectl 命令中使用 -k 标志!它看起来是这样的:

Kubectl apply -k deployment-kustomization2.yaml

这个命令相当于以下内容:

kustomize build deployment-kustomization2.yaml | kubectl apply -f -

PatchStrategicMerge 类似,我们还可以在我们的 Kustomization 中指定基于 JSON 的补丁 - 现在让我们来看看。

使用 JSONPatch 指定更改

要使用 JSON 补丁文件指定更改,该过程与涉及 YAML 补丁的过程非常相似。

首先,我们需要我们的 Kustomization 文件。它看起来像这样:

Deployment-kustomization-3.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
namespace: default
patches:
- path: deployment-patch-2.json
  target:
    group: apps
    version: v1
    kind: Deployment
    name: frontend-myapp

正如您所看到的,我们的 Kustomize 文件有一个 patches 部分,其中引用了一个 JSON 补丁文件以及一个目标。您可以在此部分引用尽可能多的 JSON 补丁。target 用于确定在资源部分中指定的哪个 Kubernetes 资源将接收补丁。

最后,我们需要我们的补丁 JSON 本身,它看起来像这样:

Deployment-patch-2.json:

[
  {
   "op": "replace",
   "path": "/spec/template/spec/containers/0/name",
   "value": "frontend-myreplacedapp"
  }
]

应用此补丁时,将对我们第一个容器的名称执行 replace 操作。您可以沿着我们原始的 deployment.yaml 文件路径查看,以查看它引用了第一个容器的名称。它将用新值 frontend-myreplacedapp 替换此名称。

现在我们已经在 Kubernetes 资源模板化和使用 Kustomize 和 Helm 进行发布方面有了坚实的基础,我们可以继续自动化部署到 Kubernetes。在下一节中,我们将看到两种实现 CI/CD 的模式。

了解 Kubernetes 上的 CI/CD 范式 - 集群内和集群外

对 Kubernetes 进行持续集成和部署可以采用多种形式。

大多数 DevOps 工程师将熟悉 Jenkins、TravisCI 等工具。这些工具非常相似,它们提供了一个执行环境来构建应用程序,执行测试,并在受控环境中调用任意的 Bash 脚本。其中一些工具在容器内运行命令,而其他工具则不会。

在涉及 Kubernetes 时,有多种思路和使用这些工具的方式。还有一种较新的 CI/CD 平台,它们与 Kubernetes 原语更紧密地耦合,并且许多平台都是设计在集群本身上运行的。

为了彻底讨论工具如何与 Kubernetes 相关,我们将把我们的流水线分为两个逻辑步骤:

  1. 构建:编译、测试应用程序、构建容器映像,并发送到映像仓库

  2. 部署:通过 kubectl、Helm 或其他工具更新 Kubernetes 资源

为了本书的目的,我们将主要关注第二个部署为重点的步骤。虽然许多可用的选项都处理构建和部署步骤,但构建步骤几乎可以发生在任何地方,并且不值得我们在涉及 Kubernetes 具体细节的书中关注。

考虑到这一点,为了讨论我们的工具选项,我们将把我们的工具集分为两个类别,就我们流水线的部署部分而言:

  • 集群外 CI/CD

  • 集群内 CI/CD

集群外 CI/CD

在第一种模式中,我们的 CI/CD 工具运行在目标 Kubernetes 集群之外。我们称之为集群外 CI/CD。存在一个灰色地带,即工具可能在专注于 CI/CD 的单独 Kubernetes 集群中运行,但我们暂时忽略该选项,因为这两个类别之间的差异仍然基本有效。

您经常会发现行业标准的工具,如 Jenkins 与这种模式一起使用,但任何具有运行脚本和以安全方式保留秘钥的能力的 CI 工具都可以在这里工作。一些例子是GitLab CICircleCITravisCIGitHub ActionsAWS CodeBuild。Helm 也是这种模式的重要组成部分,因为集群外 CI 脚本可以调用 Helm 命令来代替 kubectl。

这种模式的一些优点在于其简单性和可扩展性。这是一种“推送”模式,代码的更改会同步触发 Kubernetes 工作负载的更改。

在推送到多个集群时,集群外 CI/CD 的一些弱点是可伸缩性,以及需要在 CI/CD 管道中保留集群凭据,以便它能够调用 kubectl 或 Helm 命令。

集群内 CI/CD

在第二种模式中,我们的工具在与我们的应用程序相同的集群上运行,这意味着 CI/CD 发生在与我们的应用程序相同的 Kubernetes 上下文中,作为 pod。我们称之为集群内 CI/CD。这种集群内模式仍然可以使“构建”步骤发生在集群外,但部署步骤发生在集群内。

自从 Kubernetes 发布以来,这些类型的工具已经变得越来越受欢迎,许多使用自定义资源定义和自定义控制器来完成它们的工作。一些例子是 FluxCD、Argo CD、JenkinsX 和 Tekton Pipelines。在这些工具中,GitOps 模式很受欢迎,其中 Git 存储库被用作集群上应该运行什么应用程序的真相来源。

内部 CI/CD 模式的一些优点是可伸缩性和安全性。通过使用 GitOps 操作模型,使集群从 GitHub“拉取”更改,解决方案可以扩展到许多集群。此外,它消除了在 CI/CD 系统中保留强大的集群凭据的需要,而是在集群本身上具有 GitHub 凭据,从安全性的角度来看可能更好。

内部 CI/CD 模式的弱点包括复杂性,因为这种拉取操作略微异步(因为git pull通常在循环中发生,不总是在推送更改时发生)。

使用 Kubernetes 实现内部和外部 CI/CD

由于在 Kubernetes 中有很多 CI/CD 的选择,我们将选择两个选项并逐一实施它们,这样您可以比较它们的功能集。首先,我们将在 AWS CodeBuild 上实施 CI/CD 到 Kubernetes,这是一个很好的示例实现,可以在任何可以运行 Bash 脚本的外部 CI 系统中重复使用,包括 Bitbucket Pipelines、Jenkins 等。然后,我们将转向 FluxCD,这是一种基于 GitOps 的内部 CI 选项,它是 Kubernetes 原生的。让我们从外部选项开始。

使用 AWS CodeBuild 实现 Kubernetes CI

正如前面提到的,我们的 AWS CodeBuild CI 实现将很容易在任何基于脚本的 CI 系统中复制。在许多情况下,我们将使用的流水线 YAML 定义几乎相同。此外,正如我们之前讨论的,我们将跳过容器镜像的实际构建。我们将专注于实际的部署部分。

快速介绍一下 AWS CodeBuild,它是一个基于脚本的 CI 工具,可以运行 Bash 脚本,就像许多其他类似的工具一样。在 AWS CodePipeline 的上下文中,可以将多个独立的 AWS CodeBuild 步骤组合成更大的流水线。

在我们的示例中,我们将同时使用 AWS CodeBuild 和 AWS CodePipeline。我们不会深入讨论如何使用这两个工具,而是将我们的讨论专门与如何将它们用于部署到 Kubernetes 联系起来。

重要提示

我们强烈建议您阅读和审阅 CodePipeline 和 CodeBuild 的文档,因为我们在本章中不会涵盖所有基础知识。您可以在docs.aws.amazon.com/codebuild/latest/userguide/welcome.html找到 CodeBuild 的文档,以及docs.aws.amazon.com/codepipeline/latest/userguide/welcome.html找到 CodePipeline 的文档。

在实践中,您将拥有两个 CodePipeline,每个都有一个或多个 CodeBuild 步骤。第一个 CodePipeline 在 AWS CodeCommit 或其他 Git 仓库(如 GitHub)中的代码更改时触发。

这个流水线的第一个 CodeBuild 步骤运行测试并构建容器镜像,将镜像推送到 AWS 弹性容器仓库ECR)。第一个流水线的第二个 CodeBuild 步骤部署新的镜像到 Kubernetes。

第二个 CodePipeline 在我们提交对 Kubernetes 资源文件(基础设施仓库)的次要 Git 仓库的更改时触发。它将使用相同的流程更新 Kubernetes 资源。

让我们从第一个 CodePipeline 开始。如前所述,它包含两个 CodeBuild 步骤:

  1. 首先,测试和构建容器镜像,并将其推送到 ECR

  2. 其次,部署更新后的容器到 Kubernetes。

正如我们在本节前面提到的,我们不会在代码到容器镜像的流水线上花费太多时间,但这里有一个示例(不适用于生产)的codebuild YAML,用于实现这一步骤:

Pipeline-1-codebuild-1.yaml:

version: 0.2
phases:
  build:
    commands:
      - npm run build
  test:
    commands:
      - npm test
  containerbuild:
    commands:
      - docker build -t $ECR_REPOSITORY/$IMAGE_NAME:$IMAGE_TAG .
  push:
    commands:
      - docker push_$ECR_REPOSITORY/$IMAGE_NAME:$IMAGE_TAG

这个 CodeBuild 流水线包括四个阶段。CodeBuild 流水线规范是用 YAML 编写的,并包含一个与 CodeBuild 规范版本对应的version标签。然后,我们有一个phases部分,按顺序执行。这个 CodeBuild 首先运行build命令,然后在测试阶段运行test命令。最后,containerbuild阶段创建容器镜像,push阶段将镜像推送到我们的容器仓库。

需要记住的一件事是,CodeBuild 中每个以$开头的值都是环境变量。这些可以通过 AWS 控制台或 AWS CLI 进行自定义,并且有些可以直接来自 Git 仓库。

现在让我们看一下我们第一个 CodePipeline 的第二个 CodeBuild 步骤的 YAML:

Pipeline-1-codebuild-2.yaml:

version: 0.2
phases:
  install:
    commands:
      - curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.16.8/2020-04-16/bin/darwin/amd64/kubectl  
      - chmod +x ./kubectl
      - mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin
      - echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc
      - source ~/.bashrc
  pre_deploy:
    commands:
      - aws eks --region $AWS_DEFAULT_REGION update-kubeconfig --name $K8S_CLUSTER
  deploy:
    commands:
      - cd $CODEBUILD_SRC_DIR
      - kubectl set image deployment/$KUBERNETES-DEPLOY-NAME myrepo:"$IMAGE_TAG"

让我们来分解这个文件。我们的 CodeBuild 设置分为三个阶段:installpre_deploydeploy。在install阶段,我们安装 kubectl CLI 工具。

然后,在pre_deploy阶段,我们使用 AWS CLI 命令和一些环境变量来更新我们的kubeconfig文件,以便与我们的 EKS 集群通信。在任何其他 CI 工具(或者不使用 EKS 时),您可以使用不同的方法为您的 CI 工具提供集群凭据。在这里使用安全选项很重要,因为直接在 Git 仓库中包含kubeconfig文件是不安全的。通常,一些环境变量的组合在这里会很好。Jenkins、CodeBuild、CircleCI 等都有它们自己的系统来处理这个问题。

最后,在deploy阶段,我们使用kubectl来使用第一个 CodeBuild 步骤中指定的新镜像标签更新我们的部署(也包含在一个环境变量中)。这个kubectl rollout restart命令将确保为我们的部署启动新的 pod。结合使用imagePullPolicyAlways,这将导致我们的新应用程序版本被部署。

在这种情况下,我们正在使用 ECR 中特定的镜像标签名称来修补我们的部署。$IMAGE_TAG环境变量将自动填充为 GitHub 中最新的标签,因此我们可以使用它来自动将新的容器镜像滚动到我们的部署中。

接下来,让我们来看看我们的第二个 CodePipeline。这个 Pipeline 只包含一个步骤 - 它监听来自一个单独的 GitHub 仓库的更改,我们的“基础设施仓库”。这个仓库不包含应用程序本身的代码,而是 Kubernetes 资源的 YAML 文件。因此,我们可以更改一个 Kubernetes 资源的 YAML 值 - 例如,在部署中的副本数量,并在 CodePipeline 运行后在 Kubernetes 中看到它更新。这种模式可以很容易地扩展到使用 Helm 或 Kustomize。

让我们来看看我们第二个 CodePipeline 的第一个,也是唯一的步骤。

Pipeline-2-codebuild-1.yaml:

version: 0.2
phases:
  install:
    commands:
      - curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.16.8/2020-04-16/bin/darwin/amd64/kubectl  
      - chmod +x ./kubectl
      - mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin
      - echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc
      - source ~/.bashrc
  pre_deploy:
    commands:
      - aws eks --region $AWS_DEFAULT_REGION update-kubeconfig --name $K8S_CLUSTER
  deploy:
    commands:
      - cd $CODEBUILD_SRC_DIR
      - kubectl apply -f .

正如您所看到的,这个 CodeBuild 规范与我们之前的规范非常相似。与以前一样,我们安装 kubectl 并准备好与我们的 Kubernetes 集群一起使用。由于我们在 AWS 上运行,我们使用 AWS CLI 来完成,但这可以通过许多方式来完成,包括只需将Kubeconfig文件添加到我们的 CodeBuild 环境中。

不同之处在于,我们不是用新版本的应用程序来修补特定部署,而是在管道中运行全面的kubectl apply命令,同时将整个基础设施文件夹传输进来。这样一来,Git 中进行的任何更改都会应用到我们集群中的资源上。例如,如果我们通过更改deployment.yaml文件中的值,将我们的部署从 2 个副本扩展到 20 个副本,它将在这个 CodePipeline 步骤中部署到 Kubernetes,并且部署将会扩展。

现在我们已经介绍了使用集群外 CI/CD 环境对 Kubernetes 资源进行更改的基础知识,让我们来看看一个完全不同的 CI 范式,其中流水线在我们的集群上运行。

使用 FluxCD 实施 Kubernetes CI

对于我们的集群内 CI 工具,我们将使用FluxCD。集群内 CI 有几个选项,包括ArgoCDJenkinsX,但我们喜欢FluxCD相对简单的特点,以及它可以自动更新 Pod 的新容器版本而无需任何额外配置。作为一个额外的变化,我们将使用 FluxCD 的 Helm 集成来管理部署。让我们从安装 FluxCD 开始(我们假设您已经从本章的前几部分安装了 Helm)。这些安装遵循了书写本书时的官方 FluxCD Helm 兼容性安装说明。

官方的 FluxCD 文档可以在docs.fluxcd.io/找到,我们强烈建议您去看一看!FluxCD 是一个非常复杂的工具,我们在本书中只是浅尝辄止。全面的审查不在范围内 - 我们只是试图向您介绍集群内 CI/CD 模式和相关工具。

让我们从在我们的集群上安装 FluxCD 开始我们的审查。

安装 FluxCD(H3)

FluxCD 可以在几个步骤中使用 Helm 轻松安装:

  1. 首先,我们需要添加 Flux Helm 图表存储库:
helm repo add fluxcd https://charts.fluxcd.io
  1. 接下来,我们需要添加一个自定义资源定义,FluxCD 需要这样做才能与 Helm 发布一起工作:
kubectl apply -f https://raw.githubusercontent.com/fluxcd/helm-operator/master/deploy/crds.yaml
  1. 在我们安装 FluxCD Operator(这是 FluxCD 在 Kubernetes 上的核心功能)和 FluxCD Helm Operator 之前,我们需要为 FluxCD 创建一个命名空间。
kubectl create namespace flux

现在我们可以安装 FluxCD 的主要组件,但我们需要为 FluxCD 提供有关我们的 Git 存储库的一些额外信息。

为什么?因为 FluxCD 使用 GitOps 模式进行更新和部署。这意味着 FluxCD 将每隔几分钟主动访问我们的 Git 仓库,而不是响应 Git 钩子,比如 CodeBuild。

FluxCD 还将通过拉取策略响应新的 ECR 镜像,但我们稍后再讨论这一点。

  1. 要安装 FluxCD 的主要组件,请运行以下两个命令,并将GITHUB_USERNAMEREPOSITORY_NAME替换为您将在其中存储工作负载规范(Kubernetes YAML 或 Helm 图表)的 GitHub 用户和仓库。

这组指令假设 Git 仓库是公开的,但实际上它可能不是。由于大多数组织使用私有仓库,FluxCD 有特定的配置来处理这种情况-只需查看文档docs.fluxcd.io/en/latest/tutorials/get-started-helm/。事实上,为了看到 FluxCD 的真正力量,无论如何你都需要给它对 Git 仓库的高级访问权限,因为 FluxCD 可以写入你的 Git 仓库,并在创建新的容器镜像时自动更新清单。但是,在本书中我们不会涉及这个功能。FluxCD 的文档绝对值得仔细阅读,因为这是一个具有许多功能的复杂技术。要告诉 FluxCD 要查看哪个 GitHub 仓库,你可以在安装时使用 Helm 设置变量,就像下面的命令一样:

helm upgrade -i flux fluxcd/flux \
--set git.url=git@github.com:GITHUB_USERNAME/REPOSITORY_NAME \
--namespace flux
helm upgrade -i helm-operator fluxcd/helm-operator \
--set git.ssh.secretName=flux-git-deploy \
--namespace flux

正如你所看到的,我们需要传递我们的 GitHub 用户名,仓库的名称,以及在 Kubernetes 中用于 GitHub 秘钥的名称。

此时,FluxCD 已完全安装在我们的集群中,并指向我们在 Git 上的基础设施仓库!如前所述,这个 GitHub 仓库将包含 Kubernetes YAML 或 Helm 图表,基于这些内容,FluxCD 将更新在集群中运行的工作负载。

  1. 为了让 Flux 有实际操作的内容,我们需要创建 Flux 的实际清单。我们使用HelmRelease YAML 文件来实现,其格式如下:

helmrelease-1.yaml:

apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
  name: myapp
  annotations:
    fluxcd.io/automated: "true"
    fluxcd.io/tag.chart-image: glob:myapp-v*
spec:
  releaseName: myapp
  chart:
    git: ssh://git@github.com/<myuser>/<myinfrastructurerepository>/myhelmchart
    ref: master
    path: charts/myapp
  values:
    image:
      repository: myrepo/myapp
      tag: myapp-v2

让我们分析一下这个文件。我们正在指定 Flux 将在哪里找到我们应用程序的 Helm 图表的 Git 仓库。我们还使用automated注释标记HelmRelease,这告诉 Flux 每隔几分钟去轮询容器镜像仓库,看看是否有新版本需要部署。为了帮助这一点,我们包括了一个chart-image过滤模式,标记的容器镜像必须匹配才能触发重新部署。最后,在值部分,我们有 Helm 值,将用于 Helm 图表的初始安装。

为了向 FluxCD 提供这些信息,我们只需要将此文件添加到我们的 GitHub 仓库的根目录并推送更改。

一旦我们将这个发布文件helmrelease-1.yaml添加到我们的 Git 仓库中,Flux 将在几分钟内捕捉到它,然后查找chart值中指定的 Helm 图表。只有一个问题 - 我们还没有制作它!

目前,我们在 GitHub 上的基础设施仓库只包含我们的单个 Helm 发布文件。文件夹内容如下:

helmrelease1.yaml

为了闭环并允许 Flux 实际部署我们的 Helm 图表,我们需要将其添加到这个基础设施仓库中。让我们这样做,使我们 GitHub 仓库中的最终文件夹内容如下:

helmrelease1.yaml
myhelmchart/
  Chart.yaml
  Values.yaml
  Templates/
    … chart templates

现在,当 FluxCD 下次检查 GitHub 上的基础设施仓库时,它将首先找到 Helm 发布 YAML 文件,然后将其指向我们的新 Helm 图表。

有了新版本和 Helm 图表的 FluxCD,然后将我们的 Helm 图表部署到 Kubernetes!

然后,每当对 Helm 发布 YAML 或 Helm 图表中的任何文件进行更改时,FluxCD 将捕捉到,并在几分钟内(在其下一个循环中)部署更改。

此外,每当推送一个具有与过滤模式匹配的标签的新容器镜像到镜像仓库时,应用程序的新版本将自动部署 - 就是这么简单。这意味着 FluxCD 正在监听两个位置 - 基础设施 GitHub 仓库和容器仓库,并将部署对任一位置的任何更改。

您可以看到这如何映射到我们的集群外 CI/CD 实现,我们有一个 CodePipeline 来部署我们应用程序容器的新版本,另一个 CodePipeline 来部署对基础设施仓库的任何更改。FluxCD 以一种拉取方式做同样的事情。

总结

在本章中,我们学习了关于 Kubernetes 上的模板代码生成。我们回顾了如何使用 Helm 和 Kustomize 创建灵活的资源模板。有了这些知识,您将能够使用任一解决方案模板化您的复杂应用程序,创建或部署发布。然后,我们回顾了 Kubernetes 上的两种 CI/CD 类型;首先是通过 kubectl 将外部 CI/CD 部署到 Kubernetes,然后是使用 FluxCD 的集群内 CI 范例。有了这些工具和技术,您将能够为生产应用程序在 Kubernetes 上设置 CI/CD。

在下一章中,我们将回顾 Kubernetes 上的安全性和合规性,这是当今软件环境中的一个重要主题。

问题

  1. Helm 和 Kustomize 模板之间有哪两个区别?

  2. 在使用外部 CI/CD 设置时,应如何处理 Kubernetes API 凭据?

  3. 为什么在集群内设置 CI 可能比集群外设置更可取?反之呢?

进一步阅读

第十二章:Kubernetes 安全性和合规性

在本章中,您将了解一些关键的 Kubernetes 安全性要点。我们将讨论一些最近的 Kubernetes 安全问题,以及对 Kubernetes 进行的最近审计的发现。然后,我们将从我们集群的每个级别开始实施安全性,从 Kubernetes 资源及其配置的安全性开始,然后是容器安全,最后是入侵检测的运行时安全。首先,我们将讨论一些与 Kubernetes 相关的关键安全概念。

在本章中,我们将涵盖以下主题:

  • 了解 Kubernetes 上的安全性

  • 审查 Kubernetes 的 CVE 和安全审计

  • 实施集群配置和容器安全的工具

  • 处理 Kubernetes 上的入侵检测、运行时安全性和合规性

技术要求

为了运行本章详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个正常运行的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

此外,您还需要一台支持 Helm CLI 工具的机器,通常具有与kubectl相同的先决条件-有关详细信息,请查看 Helm 文档helm.sh/docs/intro/install/

本章中使用的代码可以在书籍的 GitHub 存储库中找到github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter12

了解 Kubernetes 上的安全性

在讨论 Kubernetes 上的安全性时,非常重要的是要注意安全边界和共享责任。共享责任模型是一个常用术语,用于描述公共云服务中的安全处理方式。它指出客户对其应用程序的安全性以及公共云组件和服务的配置的安全性负责。另一方面,公共云提供商负责服务本身的安全性以及其运行的基础设施,一直到数据中心和物理层。

同样,Kubernetes 的安全性是共享的。尽管上游 Kubernetes 不是商业产品,但成千上万的 Kubernetes 贡献者和来自大型科技公司的重要组织力量确保了 Kubernetes 组件的安全性得到维护。此外,大量的个人贡献者和使用该技术的公司构成了庞大的生态系统,确保了在 CVE 报告和处理时的改进。不幸的是,正如我们将在下一节讨论的那样,Kubernetes 的复杂性意味着存在许多可能的攻击向量。

因此,作为开发人员,根据共享责任模型,你需要负责配置 Kubernetes 组件的安全性,你在 Kubernetes 上运行的应用程序的安全性,以及集群配置中的访问级别安全性。虽然你的应用程序和容器本身的安全性不在本书的范围内,但它们对 Kubernetes 的安全性绝对重要。我们将花大部分时间讨论配置级别的安全性,访问安全性和运行时安全性。

Kubernetes 本身或 Kubernetes 生态系统提供了工具、库和完整的产品来处理这些级别的安全性 - 我们将在本章中审查其中一些选项。

现在,在我们讨论这些解决方案之前,最好先从为什么可能需要它们的基本理解开始。让我们继续下一节,我们将详细介绍 Kubernetes 在安全领域遇到的一些问题。

审查 Kubernetes 的 CVE 和安全审计

Kubernetes 在其悠久历史中遇到了几个通用漏洞和暴露CVEs)。在撰写本文时,MITRE CVE 数据库在搜索kubernetes时列出了 2015 年至 2020 年间的 73 个 CVE 公告。其中每一个要么直接与 Kubernetes 相关,要么与在 Kubernetes 上运行的常见开源解决方案相关(例如 NGINX 入口控制器)。

其中一些攻击向量足够严重,需要对 Kubernetes 源代码进行热修复,因此它们在 CVE 描述中列出了受影响的版本。关于 Kubernetes 相关的所有 CVE 的完整列表可以在cve.mitre.org/cgi-bin/cvekey.cgi?keyword=kubernetes找到。为了让你了解一些已经发现的问题,让我们按时间顺序回顾一些这些 CVE。

了解 CVE-2016-1905 – 不正确的准入控制

这个 CVE 是生产 Kubernetes 中的第一个重大安全问题。国家漏洞数据库(NIST 网站)给出了这个问题的基础评分为 7.7,将其归类为高影响类别。

通过这个问题,Kubernetes 准入控制器不会确保kubectl patch命令遵循准入规则,允许用户完全绕过准入控制器 - 在多租户场景中是一场噩梦。

了解 CVE-2018-1002105 – 连接升级到后端

这个 CVE 很可能是迄今为止 Kubernetes 项目中最关键的。事实上,NVD 给出了它 9.8 的严重性评分!在这个 CVE 中,发现在某些版本的 Kubernetes 中,可以利用 Kubernetes API 服务器的错误响应进行连接升级。一旦连接升级,就可以向集群中的任何后端服务器发送经过身份验证的请求。这允许恶意用户在没有适当凭据的情况下模拟完全经过身份验证的 TLS 请求。

除了这些 CVE(很可能部分受它们驱动),CNCF 在 2019 年赞助了 Kubernetes 的第三方安全审计。审计的结果是开源的,公开可用,值得一看。

了解 2019 年安全审计结果

正如我们在前一节中提到的,2019 年 Kubernetes 安全审计是由第三方进行的,审计结果完全是开源的。所有部分的完整审计报告可以在www.cncf.io/blog/2019/08/06/open-sourcing-the-kubernetes-security-audit/找到。

总的来说,这次审计关注了以下 Kubernetes 功能的部分:

  • kube-apiserver

  • etcd

  • kube-scheduler

  • kube-controller-manager

  • cloud-controller-manager

  • kubelet

  • kube-proxy

  • 容器运行时

意图是在涉及安全性时专注于 Kubernetes 最重要和相关的部分。审计的结果不仅包括完整的安全报告,还包括威胁模型和渗透测试,以及白皮书。

深入了解审计结果不在本书的范围内,但有一些重要的收获是对许多最大的 Kubernetes 安全问题的核心有很好的了解。

简而言之,审计发现,由于 Kubernetes 是一个复杂的、高度网络化的系统,具有许多不同的设置,因此有许多可能的配置,经验不足的工程师可能会执行,并在这样做的过程中,打开他们的集群给外部攻击者。

Kubernetes 的这个想法足够复杂,以至于不安全的配置很容易发生,这一点很重要,需要注意和牢记。

整个审计值得一读-对于那些具有重要的网络安全和容器知识的人来说,这是对 Kubernetes 作为平台开发过程中所做的一些安全决策的极好的视角。

现在我们已经讨论了 Kubernetes 安全问题的发现位置,我们可以开始研究如何增加集群的安全姿态。让我们从一些默认的 Kubernetes 安全功能开始。

实施集群配置和容器安全的工具

Kubernetes 为我们提供了许多内置选项,用于集群配置和容器权限的安全性。由于我们已经讨论了 RBAC、TLS Ingress 和加密的 Kubernetes Secrets,让我们讨论一些我们还没有时间审查的概念:准入控制器、Pod 安全策略和网络策略。

使用准入控制器

准入控制器经常被忽视,但它是一个极其重要的 Kubernetes 功能。许多 Kubernetes 的高级功能都在幕后使用准入控制器。此外,您可以创建新的准入控制器规则,以添加自定义功能到您的集群中。

有两种一般类型的准入控制器:

  • 变异准入控制器

  • 验证准入控制器

变异准入控制器接受 Kubernetes 资源规范并返回更新后的资源规范。它们还执行副作用计算或进行外部调用(在自定义准入控制器的情况下)。

另一方面,验证准入控制器只是接受或拒绝 Kubernetes 资源 API 请求。重要的是要知道,这两种类型的控制器只对创建、更新、删除或代理请求进行操作。这些控制器不能改变或更改列出资源的请求。

当这些类型的请求进入 Kubernetes API 服务器时,它将首先通过所有相关的变异准入控制器运行请求。然后,输出(可能已经变异)将通过验证准入控制器,最后在 API 服务器中被执行(或者如果被准入控制器拒绝,则不会被执行)。

在结构上,Kubernetes 提供的准入控制器是作为 Kubernetes API 服务器的一部分运行的函数或“插件”。它们依赖于两个 webhook 控制器(它们本身就是准入控制器,只是特殊的准入控制器):MutatingAdmissionWebhookValidatingAdmissionWebhook。所有其他准入控制器在底层都使用这两个 webhook 中的一个,具体取决于它们的类型。此外,您编写的任何自定义准入控制器都可以附加到这两个 webhook 中的任一个。

在我们看创建自定义准入控制器的过程之前,让我们回顾一下 Kubernetes 提供的一些默认准入控制器。有关完整列表,请查看 Kubernetes 官方文档 kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do

理解默认准入控制器

在典型的 Kubernetes 设置中有许多默认的准入控制器,其中许多对一些非常重要的基本功能是必需的。以下是一些默认准入控制器的示例。

NamespaceExists 准入控制器

NamespaceExists 准入控制器检查任何传入的 Kubernetes 资源(除了命名空间本身)。这是为了检查资源所附加的命名空间是否存在。如果不存在,它将在准入控制器级别拒绝资源请求。

PodSecurityPolicy 准入控制器

PodSecurityPolicy 准入控制器支持 Kubernetes Pod 安全策略,我们马上就会了解到。该控制器阻止不符合 Pod 安全策略的资源被创建。

除了默认准入控制器之外,我们还可以创建自定义准入控制器。

创建自定义准入控制器

可以使用两个 webhook 控制器之一动态地创建自定义准入控制器。其工作方式如下:

  1. 您必须编写自己的服务器或脚本,以独立于 Kubernetes API 服务器运行。

  2. 然后,您可以配置前面提到的两个 webhook 触发器之一,向您的自定义服务器控制器发送带有资源数据的请求。

  3. 基于结果,webhook 控制器将告诉 API 服务器是否继续。

让我们从第一步开始:编写一个快速的准入服务器。

编写自定义准入控制器的服务器

为了创建我们的自定义准入控制器服务器(它将接受来自 Kubernetes 控制平面的 webhook),我们可以使用任何编程语言。与大多数对 Kubernetes 的扩展一样,Go 语言具有最好的支持和库,使编写自定义准入控制器更容易。现在,我们将使用一些伪代码。

我们的服务器的控制流将看起来像这样:

Admission-controller-server.pseudo

// This function is called when a request hits the
// "/mutate" endpoint
function acceptAdmissionWebhookRequest(req)
{
  // First, we need to validate the incoming req
  // This function will check if the request is formatted properly
  // and will add a "valid" attribute If so
  // The webhook will be a POST request from Kubernetes in the
  // "AdmissionReviewRequest" schema
  req = validateRequest(req);
  // If the request isn't valid, return an Error
  if(!req.valid) return Error; 
  // Next, we need to decide whether to accept or deny the Admission
  // Request. This function will add the "accepted" attribute
  req = decideAcceptOrDeny(req);
  if(!req.accepted) return Error;
  // Now that we know we want to allow this resource, we need to
  // decide if any "patches" or changes are necessary
  patch = patchResourceFromWebhook(req);
  // Finally, we create an AdmissionReviewResponse and pass it back
  // to Kubernetes in the response
  // This AdmissionReviewResponse includes the patches and
  // whether the resource is accepted.
  admitReviewResp = createAdmitReviewResp(req, patch);
  return admitReviewResp;
}

现在我们有了一个简单的服务器用于我们的自定义准入控制器,我们可以配置一个 Kubernetes 准入 webhook 来调用它。

配置 Kubernetes 调用自定义准入控制器服务器

为了告诉 Kubernetes 调用我们的自定义准入服务器,它需要一个地方来调用。我们可以在任何地方运行我们的自定义准入控制器 - 它不需要在 Kubernetes 上。

也就是说,出于本章的目的,在 Kubernetes 上运行它很容易。我们不会详细介绍清单,但让我们假设我们有一个 Service 和一个 Deployment 指向它,运行着我们的服务器的容器。Service 看起来会像这样:

Service-webhook.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-custom-webhook-server
spec:
  selector:
    app: my-custom-webhook-server
  ports:
    - port: 443
      targetPort: 8443

重要的是要注意,我们的服务器需要使用 HTTPS,以便 Kubernetes 接受 webhook 响应。有许多配置的方法,我们不会在本书中详细介绍。证书可以是自签名的,但证书的通用名称和 CA 需要与设置 Kubernetes 集群时使用的名称匹配。

现在我们的服务器正在运行并接受 HTTPS 请求,让我们告诉 Kubernetes 在哪里找到它。为此,我们使用MutatingWebhookConfiguration

下面的代码块显示了MutatingWebhookConfiguration的一个示例:

Mutating-webhook-config-service.yaml

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: my-service-webhook
webhooks:
  - name: my-custom-webhook-server.default.svc
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods", "deployments", "configmaps"]
    clientConfig:
      service:
        name: my-custom-webhook-server
        namespace: default
        path: "/mutate"
      caBundle: ${CA_PEM_B64}

让我们分解一下我们的MutatingWebhookConfiguration的 YAML。正如你所看到的,我们可以在这个配置中配置多个 webhook - 尽管在这个示例中我们只做了一个。

对于每个 webhook,我们设置namerulesconfigurationname只是 webhook 的标识符。rules允许我们精确配置 Kubernetes 应该在哪些情况下向我们的准入控制器发出请求。在这种情况下,我们已经配置了我们的 webhook,每当发生podsdeploymentsconfigmaps类型资源的CREATE事件时触发。

最后,我们有clientConfig,在其中我们指定 Kubernetes 应该如何在哪里进行 webhook 请求。由于我们在 Kubernetes 上运行我们的自定义服务器,我们指定了服务名称,以及在我们的服务器上要命中的路径("/mutate"在这里是最佳实践),以及要与 HTTPS 终止证书进行比较的集群 CA。如果您的自定义准入服务器在其他地方运行,还有其他可能的配置字段-如果需要,可以查看文档(kubernetes.io/docs/reference/access-authn-authz/admission-controllers/)。

一旦我们在 Kubernetes 中创建了MutatingWebhookConfiguration,就很容易测试验证。我们所需要做的就是像平常一样创建一个 Pod、Deployment 或 ConfigMap,并检查我们的请求是否根据服务器中的逻辑被拒绝或修补。

假设我们的服务器目前设置为拒绝任何包含字符串deny-me的 Pod。它还设置了在AdmissionReviewResponse中添加错误响应。

让我们使用以下的 Pod 规范:

To-deny-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod-to-deny
spec:
  containers:
  - name: nginx
    image: nginx

现在,我们可以创建我们的 Pod 来检查准入控制器。我们可以使用以下命令:

kubectl create -f to-deny-pod.yaml

这导致以下输出:

Error from server (InternalError): error when creating "to-deny-pod.yaml": Internal error occurred: admission webhook "my-custom-webhook-server.default.svc" denied the request: Pod name contains "to-deny"!

就是这样!我们的自定义准入控制器成功拒绝了一个不符合我们在服务器中指定条件的 Pod。对于被修补(而不是被拒绝但被更改)的资源,kubectl不会显示任何特殊响应。您需要获取相关资源以查看修补的效果。

现在我们已经探讨了自定义准入控制器,让我们看看另一种实施集群安全实践的方法- Pod 安全策略。

启用 Pod 安全策略

Pod 安全策略的基本原则是允许集群管理员创建规则,Pod 必须遵循这些规则才能被调度到节点上。从技术上讲,Pod 安全策略只是另一种准入控制器。然而,这个功能得到了 Kubernetes 的官方支持,并值得深入讨论,因为有许多选项可用。

Pod 安全策略可用于防止 Pod 以 root 身份运行,限制端口和卷的使用,限制特权升级等等。我们现在将回顾一部分 Pod 安全策略的功能,但要查看完整的 Pod 安全策略配置类型列表,请查阅官方 PSP 文档[https://kubernetes.io/docs/concepts/policy/pod-security-policy/]。

最后,Kubernetes 还支持用于控制容器权限的低级原语 - 即AppArmorSELinuxSeccomp。这些配置超出了本书的范围,但对于高度安全的环境可能会有用。

创建 Pod 安全策略的步骤

实施 Pod 安全策略有几个步骤:

  1. 首先,必须启用 Pod 安全策略准入控制器。

  2. 这将阻止在您的集群中创建所有 Pod,因为它需要匹配的 Pod 安全策略和角色才能创建 Pod。出于这个原因,您可能希望在启用准入控制器之前创建您的 Pod 安全策略和角色。

  3. 启用准入控制器后,必须创建策略本身。

  4. 然后,必须创建具有对 Pod 安全策略访问权限的RoleClusterRole对象。

  5. 最后,该角色可以与ClusterRoleBindingRoleBinding绑定到用户或服务accountService帐户,允许使用该服务帐户创建的 Pod 使用 Pod 安全策略可用的权限。

在某些情况下,您的集群可能默认未启用 Pod 安全策略准入控制器。让我们看看如何启用它。

启用 Pod 安全策略准入控制器

为了启用 PSP 准入控制器,kube-apiserver必须使用指定准入控制器的标志启动。在托管的 Kubernetes(EKS、AKS 等)上,PSP 准入控制器可能会默认启用,并且为初始管理员用户创建一个特权 Pod 安全策略。这可以防止 PSP 在新集群中创建 Pod 时出现任何问题。

如果您正在自行管理 Kubernetes,并且尚未启用 PSP 准入控制器,您可以通过使用以下标志重新启动kube-apiserver组件来启用它:

kube-apiserver --enable-admission-plugins=PodSecurityPolicy,ServiceAccount…<all other desired admission controllers>

如果您的 Kubernetes API 服务器是使用systemd文件运行的(如果遵循Kubernetes:困难的方式,它将是这样),则应该在那里更新标志。通常,systemd文件放置在/etc/systemd/system/文件夹中。

为了找出已经启用了哪些准入插件,您可以运行以下命令:

kube-apiserver -h | grep enable-admission-plugins

此命令将显示已启用的准入插件的长列表。例如,您将在输出中看到以下准入插件:

NamespaceLifecycle, LimitRanger, ServiceAccount…

现在我们确定了 PSP 准入控制器已启用,我们实际上可以创建 PSP 了。

创建 PSP 资源

Pod 安全策略本身可以使用典型的 Kubernetes 资源 YAML 创建。以下是一个特权 Pod 安全策略的 YAML 文件:

Privileged-psp.yaml

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: privileged-psp
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
  privileged: true
  allowedCapabilities:
  - '*'
  volumes:
  - '*'
  hostNetwork: true
  hostPorts:
  - min: 2000
    max: 65535
  hostIPC: true
  hostPID: true
  allowPrivilegeEscalation: true
  runAsUser:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'

此 Pod 安全策略允许用户或服务账户(通过RoleBindingClusterRoleBinding)创建具有特权功能的 Pod。例如,使用此PodSecurityPolicy的 Pod 将能够绑定到主机网络的端口2000-65535,以任何用户身份运行,并绑定到任何卷类型。此外,我们还有一个关于allowedProfileNamesseccomp限制的注释-这可以让您了解SeccompAppArmor注释与PodSecurityPolicies的工作原理。

正如我们之前提到的,仅仅创建 PSP 是没有任何作用的。对于将创建特权 Pod 的任何服务账户或用户,我们需要通过RoleRoleBinding(或ClusterRoleClusterRoleBinding)为他们提供对 Pod 安全策略的访问权限。

为了创建具有对此 PSP 访问权限的ClusterRole,我们可以使用以下 YAML:

Privileged-clusterrole.yaml

apiVersion: rbac.authorization.k8s.io
kind: ClusterRole
metadata:
  name: privileged-role
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - privileged-psp

现在,我们可以将新创建的ClusterRole绑定到我们打算创建特权 Pod 的用户或服务账户上。让我们使用ClusterRoleBinding来做到这一点:

Privileged-clusterrolebinding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: privileged-crb
roleRef:
  kind: ClusterRole
  name: privileged-role
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated

在我们的情况下,我们希望让集群上的每个经过身份验证的用户都能创建特权 Pod,因此我们绑定到system:authenticated组。

现在,我们可能不希望所有用户或 Pod 都具有特权。一个更现实的 Pod 安全策略会限制 Pod 的能力。

让我们看一下具有这些限制的 PSP 的一些示例 YAML:

unprivileged-psp.yaml

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: unprivileged-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    rule: 'MustRunAsNonRoot'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  readOnlyRootFilesystem: false

正如您所看到的,这个 Pod 安全策略在其对创建的 Pod 施加的限制方面大不相同。在此策略下,不允许任何 Pod 以 root 身份运行或升级为 root。它们还对它们可以绑定的卷的类型有限制(在前面的代码片段中已经突出显示了这一部分)-它们不能使用主机网络或直接绑定到主机端口。

在这个 YAML 中,runAsUsersupplementalGroups部分都控制可以运行或由容器添加的 Linux 用户 ID 和组 ID,而fsGroup键控制容器可以使用的文件系统组。

除了使用诸如MustRunAsNonRoot之类的规则,还可以直接指定容器可以使用的用户 ID - 任何未在其规范中明确使用该 ID 运行的 Pod 将无法调度到节点上。

要查看限制用户特定 ID 的示例 PSP,请查看以下 YAML:

Specific-user-id-psp.yaml

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: specific-user-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 3000
  readOnlyRootFilesystem: false

应用此 Pod 安全策略后,将阻止任何以用户 ID03001或更高的身份运行的 Pod。为了创建一个满足这个条件的 Pod,我们在 Pod 规范的securityContext中使用runAs选项。

这是一个满足这一约束的示例 Pod,即使有了这个 Pod 安全策略,它也可以成功调度:

Specific-user-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: specific-user-pod
spec:
  securityContext:
    runAsUser: 1000
  containers:
  - name: test
    image: busybox
    securityContext:
      allowPrivilegeEscalation: false

正如您在这个 YAML 中看到的,我们为我们的 Pod 指定了一个特定的用户 ID1000来运行。我们还禁止我们的 Pod 升级为 root。即使specific-user-psp已经生效,这个 Pod 规范也可以成功调度。

现在我们已经讨论了 Pod 安全策略如何通过对 Pod 运行方式施加限制来保护 Kubernetes,我们可以转向网络策略,我们可以限制 Pod 的网络。

使用网络策略

Kubernetes 中的网络策略类似于防火墙规则或路由表。它们允许用户通过选择器指定一组 Pod,然后确定这些 Pod 可以如何以及在哪里进行通信。

为了使网络策略工作,您选择的 Kubernetes 网络插件(如WeaveFlannelCalico)必须支持网络策略规范。网络策略可以像其他 Kubernetes 资源一样通过一个 YAML 文件创建。让我们从一个非常简单的网络策略开始。

这是一个限制访问具有标签app=server的 Pod 的网络策略规范。

Label-restriction-policy.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-network-policy
spec:
  podSelector:
    matchLabels:
      app: server
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 80

现在,让我们逐步解析这个网络策略的 YAML,因为这将帮助我们解释随着我们的进展一些更复杂的网络策略。

首先,在我们的规范中,我们有一个podSelector,它在功能上类似于节点选择器。在这里,我们使用matchLabels来指定这个网络策略只会影响具有标签app=server的 Pod。

接下来,我们为我们的网络策略指定一个策略类型。有两种策略类型:ingressegress。一个网络策略可以指定一个或两种类型。ingress指的是制定适用于连接到匹配的 Pod 的网络规则,而egress指的是制定适用于离开匹配的 Pod 的连接的网络规则。

在这个特定的网络策略中,我们只是规定了一个单一的ingress规则:只有来自具有标签app=server的 Pod 的流量才会被接受,这些流量是源自具有标签app:frontend的 Pod。此外,唯一接受具有标签app=server的 Pod 上的流量的端口是80

ingress策略集中可以有多个from块对应多个流量规则。同样,在egress中也可以有多个to块。

重要的是要注意,网络策略是按命名空间工作的。默认情况下,如果在命名空间中没有单个网络策略,那么在该命名空间中的 Pod 之间的通信就没有任何限制。然而,一旦一个特定的 Pod 被单个网络策略选中,所有到该 Pod 的流量和从该 Pod 出去的流量都必须明确匹配一个网络策略规则。如果不匹配规则,它将被阻止。

有了这个想法,我们可以轻松地创建强制执行广泛限制的 Pod 网络策略。让我们来看看以下网络策略:

Full-restriction-policy.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: full-restriction-policy
  namespace: development
spec:
  policyTypes:
  - Ingress
  - Egress
  podSelector: {}

在这个NetworkPolicy中,我们指定我们将包括IngressEgress策略,但我们没有为它们写一个块。这样做的效果是自动拒绝任何EgressIngress的流量,因为没有规则可以匹配流量。

另外,我们的{} Pod 选择器值对应于选择命名空间中的每个 Pod。这条规则的最终结果是,development命名空间中的每个 Pod 将无法接受入口流量或发送出口流量。

重要提示

还需要注意的是,网络策略是通过结合影响 Pod 的所有单独的网络策略,然后将所有这些规则的组合应用于 Pod 流量来解释的。

这意味着,即使在我们先前的示例中限制了development命名空间中的所有入口和出口流量,我们仍然可以通过添加另一个网络策略来为特定的 Pod 启用它。

假设现在我们的development命名空间对 Pod 有完全的流量限制,我们希望允许一部分 Pod 在端口443上接收网络流量,并在端口6379上向数据库 Pod 发送流量。为了做到这一点,我们只需要创建一个新的网络策略,通过策略的叠加性质,允许这种流量。

这就是网络策略的样子:

覆盖限制网络策略.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: override-restriction-policy
  namespace: development
spec:
  podSelector:
    matchLabels:
      app: server
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 443
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 6379

在这个网络策略中,我们允许development命名空间中的服务器 Pod 在端口443上接收来自前端 Pod 的流量,并在端口6379上向数据库 Pod 发送流量。

如果我们想要打开所有 Pod 之间的通信而没有任何限制,同时实际上还要制定网络策略,我们可以使用以下 YAML 来实现:

全开放网络策略.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-egress
spec:
  podSelector: {}
  egress:
  - {}
  ingress:
  - {}
  policyTypes:
  - Egress
  - Ingress

现在我们已经讨论了如何使用网络策略来设置 Pod 之间的流量规则。然而,也可以将网络策略用作外部防火墙。为了做到这一点,我们创建基于外部 IP 而不是 Pod 作为源或目的地的网络策略规则。

让我们看一个限制与特定 IP 范围作为目标的 Pod 之间通信的网络策略的示例:

外部 IP 网络策略.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: specific-ip-policy
spec:
  podSelector:
    matchLabels:
      app: worker
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 157.10.0.0/16
        except:
        - 157.10.1.0/24
  egress:
  - to:
    - ipBlock:
        cidr: 157.10.0.0/16
        except:
        - 157.10.1.0/24

在这个网络策略中,我们指定了一个Ingress规则和一个Egress规则。每个规则根据网络请求的源 IP 而不是来自哪个 Pod 来接受或拒绝流量。

在我们的情况下,我们已经为我们的IngressEgress规则选择了一个/16子网掩码范围(带有指定的/24 CIDR 异常)。这会产生一个副作用,即阻止集群内部的任何流量到达这些 Pod,因为我们的 Pod IP 都不会匹配默认集群网络设置中的规则。

然而,在指定的子网掩码中来自集群外部的流量(并且不在异常范围内)将能够向workerPod 发送流量,并且还能够接受来自workerPod 的流量。

随着我们讨论网络策略的结束,我们可以转向安全堆栈的一个完全不同的层面 - 运行时安全和入侵检测。

处理 Kubernetes 上的入侵检测、运行时安全和合规性

一旦您设置了 Pod 安全策略和网络策略,并且通常确保您的配置尽可能牢固 - Kubernetes 仍然存在许多可能的攻击向量。在本节中,我们将重点关注来自 Kubernetes 集群内部的攻击。即使在具有高度特定的 Pod 安全策略的情况下(这确实有所帮助,需要明确),您的集群中运行的容器和应用程序仍可能执行意外或恶意操作。

为了解决这个问题,许多专业人士寻求运行时安全工具,这些工具允许对应用程序进程进行持续监控和警报。对于 Kubernetes 来说,一个流行的开源工具就是Falco

安装 Falco

Falco 自称为 Kubernetes 上进程的行为活动监视器。它可以监视在 Kubernetes 上运行的容器化应用程序以及 Kubernetes 组件本身。

Falco 是如何工作的?在实时中,Falco 解析来自 Linux 内核的系统调用。然后,它通过规则过滤这些系统调用 - 这些规则是可以应用于 Falco 引擎的一组配置。每当系统调用违反规则时,Falco 就会触发警报。就是这么简单!

Falco 附带了一套广泛的默认规则,可以在内核级别增加显著的可观察性。当然,Falco 支持自定义规则 - 我们将向您展示如何编写这些规则。

但首先,我们需要在我们的集群上安装 Falco!幸运的是,Falco 可以使用 Helm 进行安装。但是,非常重要的是要注意,有几种不同的安装 Falco 的方式,在事件发生时它们在有效性上有很大的不同。

我们将使用 Helm 图表安装 Falco,这对于托管的 Kubernetes 集群或者您可能无法直接访问工作节点的任何情况都非常简单且有效。

然而,为了获得最佳的安全姿态,Falco 应该直接安装到 Kubernetes 节点的 Linux 级别。使用 DaemonSet 的 Helm 图表非常易于使用,但本质上不如直接安装 Falco 安全。要直接将 Falco 安装到您的节点上,请查看falco.org/docs/installation/上的安装说明。

有了这个警告,我们可以使用 Helm 安装 Falco:

  1. 首先,我们需要将falcosecurity存储库添加到我们本地的 Helm 中:
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update

接下来,我们可以继续使用 Helm 实际安装 Falco。

重要提示

Falco Helm 图表有许多可能可以在 values 文件中更改的变量-要全面审查这些变量,您可以在官方 Helm 图表存储库github.com/falcosecurity/charts/tree/master/falco上查看。

  1. 要安装 Falco,请运行以下命令:
helm install falco falcosecurity/falco

此命令将使用默认值安装 Falco,您可以在github.com/falcosecurity/charts/blob/master/falco/values.yaml上查看默认值。

接下来,让我们深入了解 Falco 为安全意识的 Kubernetes 管理员提供了什么。

了解 Falco 的功能

如前所述,Falco 附带一组默认规则,但我们可以使用新的 YAML 文件轻松添加更多规则。由于我们使用的是 Helm 版本的 Falco,因此将自定义规则传递给 Falco 就像创建一个新的 values 文件或编辑具有自定义规则的默认文件一样简单。

添加自定义规则看起来像这样:

Custom-falco.yaml

customRules:
  my-rules.yaml: |-
    Rule1
    Rule2
    etc...

现在是讨论 Falco 规则结构的好时机。为了说明,让我们借用一些来自随 Falco Helm 图表一起提供的Default Falco 规则集的规则。

在 YAML 中指定 Falco 配置时,我们可以使用三种不同类型的键来帮助组成我们的规则。这些是宏、列表和规则本身。

在这个例子中,我们正在查看的具体规则称为启动特权容器。这个规则将检测特权容器何时被启动,并记录一些关于容器的信息到STDOUT。规则在处理警报时可以做各种事情,但记录到STDOUT是在发生高风险事件时增加可观察性的好方法。

首先,让我们看一下规则条目本身。这个规则使用了一些辅助条目,几个宏和列表 - 但我们将在稍后讨论这些:

- rule: Launch Privileged Container
  desc: Detect the initial process started in a privileged container. Exceptions are made for known trusted images.
  condition: >
    container_started and container
    and container.privileged=true
    and not falco_privileged_containers
    and not user_privileged_containers
  output: Privileged container started (user=%user.name command=%proc.cmdline %container.info image=%container.image.repository:%container.image.tag)
  priority: INFO
  tags: [container, cis, mitre_privilege_escalation, mitre_lateral_movement]

正如您所看到的,Falco 规则有几个部分。首先,我们有规则名称和描述。然后,我们指定规则的触发条件 - 这充当 Linux 系统调用的过滤器。如果系统调用匹配condition块中的所有逻辑过滤器,规则就会被触发。

当触发规则时,output键允许我们设置输出文本的格式。priority键让我们分配一个优先级,可以是emergencyalertcriticalerrorwarningnoticeinformationaldebug中的一个。

最后,tags键将标签应用于相关的规则,使得更容易对规则进行分类。当使用不仅仅是简单文本STDOUT条目的警报时,这一点尤为重要。

这里condition的语法特别重要,我们将重点关注过滤系统的工作原理。

首先,由于过滤器本质上是逻辑语句,您将看到一些熟悉的语法(如果您曾经编程或编写伪代码) - andand notand so on。这种语法很容易学习,可以在github.com/draios/sysdig/wiki/sysdig-user-guide#filtering找到关于它的全面讨论 - Sysdig过滤器语法。

需要注意的是,Falco 开源项目最初是由Sysdig创建的,这就是为什么它使用常见的Sysdig过滤器语法。

接下来,您将看到对container_startedcontainer的引用,以及falco_privileged_containersuser_privileged_containers的引用。这些不是简单的字符串,而是宏的使用 - 引用 YAML 中其他块的引用,指定了额外的功能,并且通常使得编写规则变得更加容易,而不需要重复大量的配置。

为了了解这个规则是如何真正工作的,让我们看一下在前面规则中引用的所有宏的完整参考:

- macro: container
  condition: (container.id != host)
- macro: container_started
  condition: >
    ((evt.type = container or
     (evt.type=execve and evt.dir=< and proc.vpid=1)) and
     container.image.repository != incomplete)
- macro: user_sensitive_mount_containers
  condition: (container.image.repository = docker.io/sysdig/agent)
- macro: falco_privileged_containers
  condition: (openshift_image or
              user_trusted_containers or
              container.image.repository in (trusted_images) or
              container.image.repository in (falco_privileged_images) or
              container.image.repository startswith istio/proxy_ or
              container.image.repository startswith quay.io/sysdig)
- macro: user_privileged_containers
  condition: (container.image.repository endswith sysdig/agent)

您将在前面的 YAML 中看到,每个宏实际上只是一块可重用的Sysdig过滤器语法块,通常使用其他宏来完成规则功能。列表在这里没有显示,它们类似于宏,但不描述过滤逻辑。相反,它们包括一个字符串值列表,可以作为使用过滤器语法的比较的一部分。

例如,在falco_privileged_containers宏中的(``trusted_images)引用了一个名为trusted_images的列表。以下是该列表的来源:

- list: trusted_images
  items: []

正如您所看到的,在默认规则中,这个特定列表是空的,但自定义规则集可以在这个列表中使用一组受信任的镜像,然后这些受信任的镜像将自动被所有使用trusted_image列表作为其过滤规则一部分的其他宏和规则所使用。

正如之前提到的,除了跟踪 Linux 系统调用之外,Falco 在版本 v0.13.0 中还可以跟踪 Kubernetes 控制平面事件。

了解 Falco 中的 Kubernetes 审计事件规则

在结构上,这些 Kubernetes 审计事件规则的工作方式与 Falco 的 Linux 系统调用规则相同。以下是 Falco 中默认 Kubernetes 规则的示例:

- rule: Create Disallowed Pod
  desc: >
    Detect an attempt to start a pod with a container image outside of a list of allowed images.
  condition: kevt and pod and kcreate and not allowed_k8s_containers
  output: Pod started with container not in allowed list (user=%ka.user.name pod=%ka.resp.name ns=%ka.target.namespace images=%ka.req.pod.containers.image)
  priority: WARNING
  source: k8s_audit
  tags: [k8s]

这个规则在 Falco 中针对 Kubernetes 审计事件(基本上是控制平面事件),在创建不在allowed_k8s_containers列表中的 Pod 时发出警报。默认的k8s审计规则包含许多类似的规则,大多数在触发时输出格式化日志。

现在,我们在本章的前面谈到了一些 Pod 安全策略,你可能会发现 PSPs 和 Falco Kubernetes 审计事件规则之间有一些相似之处。例如,看看默认的 Kubernetes Falco 规则中的这个条目:

- rule: Create HostNetwork Pod
  desc: Detect an attempt to start a pod using the host network.
  condition: kevt and pod and kcreate and ka.req.pod.host_network intersects (true) and not ka.req.pod.containers.image.repository in (falco_hostnetwork_images)
  output: Pod started using host network (user=%ka.user.name pod=%ka.resp.name ns=%ka.target.namespace images=%ka.req.pod.containers.image)
  priority: WARNING
  source: k8s_audit
  tags: [k8s]

这个规则在尝试使用主机网络启动 Pod 时触发,直接映射到主机网络 PSP 设置。

Falco 利用这种相似性,让我们可以使用 Falco 作为一种试验新的 Pod 安全策略的方式,而不会在整个集群中应用它们并导致运行中的 Pod 出现问题。

为此,falcoctl(Falco 命令行工具)带有convert psp命令。该命令接受一个 Pod 安全策略定义,并将其转换为一组 Falco 规则。这些 Falco 规则在触发时只会将日志输出到STDOUT(而不会像 PSP 不匹配那样导致 Pod 调度失败),这样就可以更轻松地在现有集群中测试新的 Pod 安全策略。

要了解如何使用falcoctl转换工具,请查看官方 Falco 文档falco.org/docs/psp-support/

现在我们对 Falco 工具有了很好的基础,让我们讨论一下它如何用于实施合规性控制和运行时安全。

将 Falco 映射到合规性和运行时安全用例

由于其可扩展性和审计低级别的 Linux 系统调用的能力,Falco 是持续合规性和运行时安全的绝佳工具。

在合规性方面,可以利用 Falco 规则集,这些规则集专门映射到合规性标准的要求-例如 PCI 或 HIPAA。这使用户能够快速检测并采取行动,处理不符合相关标准的任何进程。有几个标准的开源和闭源 Falco 规则集。

同样地,对于运行时安全,Falco 公开了一个警报/事件系统,这意味着任何触发警报的运行时事件也可以触发自动干预和补救过程。这对安全性和合规性都适用。例如,如果一个 Pod 触发了 Falco 的不合规警报,一个进程可以立即处理该警报并删除有问题的 Pod。

总结

在本章中,我们了解了 Kubernetes 上下文中的安全性。首先,我们回顾了 Kubernetes 上的安全性基础知识-安全堆栈的哪些层对我们的集群相关,以及如何管理这种复杂性的一些基本概念。接下来,我们了解了 Kubernetes 遇到的一些主要安全问题,以及讨论了 2019 年安全审计的结果。

然后,我们在 Kubernetes 的两个不同级别实施了安全性-首先是使用 Pod 安全策略和网络策略进行配置,最后是使用 Falco 进行运行时安全。

在下一章中,我们将学习如何通过构建自定义资源使 Kubernetes 成为您自己的。这将允许您为集群添加重要的新功能。

问题

  1. 自定义准入控制器可以使用哪两个 Webhook 控制器的名称?

  2. 空的NetworkPolicy对入口有什么影响?

  3. 为了防止攻击者更改 Pod 功能,哪种类型的 Kubernetes 控制平面事件对于跟踪是有价值的?

进一步阅读

第四部分:扩展 Kubernetes

在这一部分,您将把在前几节中学到的知识应用到 Kubernetes 上的高级模式中。我们将使用自定义资源定义来扩展默认的 Kubernetes 功能,实现服务网格和无服务器模式在您的集群上,并运行一些有状态的工作负载。

本书的这一部分包括以下章节:

  • 第十三章, 使用 CRD 扩展 Kubernetes

  • 第十四章, 服务网格和无服务器

  • 第十五章, Kubernetes 上的有状态工作负载

第十三章:使用 CRD 扩展 Kubernetes

本章解释了扩展 Kubernetes 功能的许多可能性。它从讨论自定义资源定义CRD)开始,这是一种 Kubernetes 本地的方式,用于指定可以通过熟悉的kubectl命令(如getcreatedescribeapply)对其进行操作的自定义资源。接下来是对运算符模式的讨论,这是 CRD 的扩展。然后详细介绍了云提供商附加到其 Kubernetes 实现的一些钩子,并以对更大的云原生生态系统的简要介绍结束。使用本章学到的概念,您将能够设计和开发对 Kubernetes 集群的扩展,解锁高级使用模式。

本章的案例研究将包括创建两个简单的 CRD 来支持一个示例应用程序。我们将从 CRD 开始,这将让您对扩展如何构建在 Kubernetes API 上有一个良好的基础理解。

在本章中,我们将涵盖以下主题:

  • 如何使用自定义资源定义CRD)扩展 Kubernetes

  • 使用 Kubernetes 运算符进行自管理功能

  • 使用特定于云的 Kubernetes 扩展

  • 与生态系统集成

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个正常运行的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具。

本章中使用的代码可以在书籍的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter13

如何使用自定义资源定义扩展 Kubernetes

让我们从基础知识开始。什么是 CRD?我们知道 Kubernetes 有一个 API 模型,我们可以对资源执行操作。一些 Kubernetes 资源的例子(现在你应该对它们非常熟悉)是 Pods、PersistentVolumes、Secrets 等。

现在,如果我们想在集群中实现一些自定义功能,编写我们自己的控制器,并将控制器的状态存储在某个地方,我们可以,当然,将我们自定义功能的状态存储在 Kubernetes 或其他地方运行的 SQL 或 NoSQL 数据库中(这实际上是扩展 Kubernetes 的策略之一)-但是如果我们的自定义功能更像是 Kubernetes 功能的扩展,而不是完全独立的应用程序呢?

在这种情况下,我们有两个选择:

  • 自定义资源定义

  • API 聚合

API 聚合允许高级用户在 Kubernetes API 服务器之外构建自己的资源 API,并使用自己的存储,然后在 API 层聚合这些资源,以便可以使用 Kubernetes API 进行查询。这显然是非常可扩展的,实质上只是使用 Kubernetes API 作为代理来使用您自己的自定义功能,这可能实际上与 Kubernetes 集成,也可能不会。

另一个选择是 CRDs,我们可以使用 Kubernetes API 和底层数据存储(etcd)而不是构建我们自己的。我们可以使用我们知道的kubectlkube api方法与我们自己的自定义功能进行交互。

在这本书中,我们不会讨论 API 聚合。虽然比 CRDs 更灵活,但这是一个高级主题,需要对 Kubernetes API 有深入的了解,并仔细阅读 Kubernetes 文档以正确实施。您可以在 Kubernetes 文档中了解更多关于 API 聚合的信息kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/

所以,现在我们知道我们正在使用 Kubernetes 控制平面作为我们自己的有状态存储来存储我们的新自定义功能,我们需要一个模式。类似于 Kubernetes 中 Pod 资源规范期望特定字段和配置,我们可以告诉 Kubernetes 我们对新的自定义资源期望什么。现在让我们来看一下 CRD 的规范。

编写自定义资源定义

对于 CRDs,Kubernetes 使用 OpenAPI V3 规范。有关 OpenAPI V3 的更多信息,您可以查看官方文档github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md,但我们很快将看到这如何转化为 Kubernetes CRD 定义。

让我们来看一个 CRD 规范的示例。现在让我们明确一点,这不是任何特定记录的 YAML 的样子。相反,这只是我们在 Kubernetes 内部定义 CRD 的要求的地方。一旦创建,Kubernetes 将接受与规范匹配的资源,我们就可以开始制作我们自己的这种类型的记录。

这里有一个 CRD 规范的示例 YAML,我们称之为delayedjob。这个非常简单的 CRD 旨在延迟启动容器镜像作业,这样用户就不必为他们的容器编写延迟启动的脚本。这个 CRD 非常脆弱,我们不建议任何人真正使用它,但它确实很好地突出了构建 CRD 的过程。让我们从一个完整的 CRD 规范 YAML 开始,然后分解它:

自定义资源定义-1.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: delayedjobs.delayedresources.mydomain.com
spec:
  group: delayedresources.mydomain.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                delaySeconds:
                  type: integer
                image:
                  type: string
  scope: Namespaced
  conversion:
    strategy: None
  names:
    plural: delayedjobs
    singular: delayedjob
    kind: DelayedJob
    shortNames:
    - dj

让我们来审视一下这个文件的部分。乍一看,它看起来像是您典型的 Kubernetes YAML 规范 - 因为它就是!在apiVersion字段中,我们有apiextensions.k8s.io/v1,这是自 Kubernetes 1.16以来的标准(在那之前是apiextensions.k8s.io/v1beta1)。我们的kind将始终是CustomResourceDefinition

metadata字段是当事情开始变得特定于我们的资源时。我们需要将name元数据字段结构化为我们资源的复数形式,然后是一个句号,然后是它的组。让我们从我们的 YAML 文件中快速偏离一下,讨论一下 Kubernetes API 中组的工作原理。

理解 Kubernetes API 组

组是 Kubernetes 在其 API 中分割资源的一种方式。每个组对应于 Kubernetes API 服务器的不同子路径。

默认情况下,有一个名为核心组的遗留组 - 它对应于在 Kubernetes REST API 的/api/v1端点上访问的资源。因此,这些遗留组资源在其 YAML 规范中具有apiVersion: v1。核心组中资源的一个例子是 Pod。

接下来,有一组命名的组 - 这些组对应于可以在REST URL 上访问的资源,形式为/apis/<GROUP NAME>/<VERSION>。这些命名的组构成了 Kubernetes 资源的大部分。然而,最古老和最基本的资源,如 Pod、Service、Secret 和 Volume,都在核心组中。一个在命名组中的资源的例子是StorageClass资源,它在storage.k8s.io组中。

重要说明

要查看哪个资源属于哪个组,您可以查看您正在使用的 Kubernetes 版本的官方 Kubernetes API 文档。例如,版本1.18的文档将位于kubernetes.io/docs/reference/generated/kubernetes-api/v1.18

CRD 可以指定自己的命名组,这意味着特定的 CRD 将在 Kubernetes API 服务器可以监听的REST端点上可用。考虑到这一点,让我们回到我们的 YAML 文件,这样我们就可以讨论 CRD 的主要部分-版本规范。

理解自定义资源定义版本

正如您所看到的,我们选择了组delayedresources.mydomain.com。该组理论上将包含任何其他延迟类型的 CRD-例如,DelayedDaemonSetDelayedDeployment

接下来,我们有我们的 CRD 的主要部分。在versions下,我们可以定义一个或多个 CRD 版本(在name字段中),以及该 CRD 版本的 API 规范。然后,当您创建 CRD 的实例时,您可以在 YAML 文件的apiVersion键的版本参数中定义您将使用的版本-例如,apps/v1,或在这种情况下,delayedresources.mydomain.com/v1

每个版本项还有一个served属性,这实质上是一种定义给定版本是否启用或禁用的方式。如果servedfalse,则该版本将不会被 Kubernetes API 创建,并且该版本的 API 请求(或kubectl命令)将失败。

此外,还可以在特定版本上定义一个deprecated键,这将导致 Kubernetes 在使用弃用版本进行 API 请求时返回警告消息。这就是带有弃用版本的 CRD 的yaml文件的样子-我们已删除了一些规范,以使 YAML 文件简短:

自定义资源定义-2.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: delayedjob.delayedresources.mydomain.com
spec:
  group: delayedresources.mydomain.com
  versions:
    - name: v1
      served: true
      storage: false
      deprecated: true
      deprecationWarning: "DelayedJob v1 is deprecated!"
      schema:
        openAPIV3Schema:
		…
    - name: v2
      served: true
      storage: true
      schema:
        openAPIV3Schema:
		...
  scope: Namespaced
  conversion:
    strategy: None
  names:
    plural: delayedjobs
    singular: delayedjob
    kind: DelayedJob
    shortNames:
    - dj

正如您所看到的,我们已将v1标记为已弃用,并且还包括一个弃用警告,以便 Kubernetes 作为响应发送。如果我们不包括弃用警告,将使用默认消息。

进一步向下移动,我们有storage键,它与served键交互。这是必要的原因是,虽然 Kubernetes 支持同时拥有多个活动(也就是served)版本的资源,但是只能有一个版本存储在控制平面中。然而,served属性意味着 API 可以提供多个版本的资源。那么这是如何工作的呢?

答案是,Kubernetes 将把 CRD 对象从存储的版本转换为您要求的版本(或者反过来,在创建资源时)。

这种转换是如何处理的?让我们跳过其余的版本属性,看看conversion键是如何工作的。

conversion键允许您指定 Kubernetes 将如何在您的服务版本和存储版本之间转换 CRD 对象的策略。如果两个版本相同-例如,如果您请求一个v1资源,而存储的版本是v1,那么不会发生转换。

截至 Kubernetes 1.13 的默认值是none。使用none设置,Kubernetes 不会在字段之间进行任何转换。它只会包括应该出现在served(或存储,如果创建资源)版本上的字段。

另一个可能的转换策略是Webhook,它允许您定义一个自定义 Webhook,该 Webhook 将接收一个版本并对其进行适当的转换为您想要的版本。这里有一个使用Webhook转换策略的 CRD 示例-为了简洁起见,我们省略了一些版本模式:

Custom-resource-definition-3.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: delayedjob.delayedresources.mydomain.com
spec:
  group: delayedresources.mydomain.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
		...
  scope: Namespaced
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        url: "https://webhook-conversion.com/delayedjob"
  names:
    plural: delayedjobs
    singular: delayedjob
    kind: DelayedJob
    shortNames:
    - dj

正如您所看到的,Webhook策略让我们定义一个 URL,请求将发送到该 URL,其中包含有关传入资源对象、其当前版本和需要转换为的版本的信息。

想法是我们的Webhook服务器将处理转换并传回修正后的 Kubernetes 资源对象。Webhook策略是复杂的,可以有许多可能的配置,我们在本书中不会深入讨论。

重要提示

要了解如何配置转换 Webhooks,请查看官方 Kubernetes 文档kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/

现在,回到我们在 YAML 中的version条目!在servedstorage键下,我们看到schema对象,其中包含我们资源的实际规范。如前所述,这遵循 OpenAPI Spec v3 模式。

schema对象,由于空间原因已从前面的代码块中删除,如下所示:

自定义资源定义-3.yaml(续)

     schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                delaySeconds:
                  type: integer
                image:
                  type: string

正如您所看到的,我们支持delaySeconds字段,它将是一个整数,以及image,它是一个与我们的容器映像相对应的字符串。如果我们真的想要使DelayedJob达到生产就绪状态,我们会希望包括各种其他选项,使其更接近原始的 Kubernetes Job 资源 - 但这不是我们的意图。

在原始代码块中进一步向后移动,超出版本列表,我们看到一些其他属性。首先是scope属性,可以是ClusterNamespaced。这告诉 Kubernetes 是否将 CRD 对象的实例视为特定于命名空间的资源(例如 Pods,Deployments 等),还是作为集群范围的资源 - 就像命名空间本身一样,因为在命名空间中获取命名空间对象是没有意义的!

最后,我们有names块,它允许您定义资源名称的复数和单数形式,以在各种情况下使用(例如,kubectl get podskubectl get pod都可以工作)。

names块还允许您定义驼峰命名的kind值,在资源 YAML 中将使用该值,以及一个或多个shortNames,可以用来在 API 或kubectl中引用该资源 - 例如,kubectl get po

解释了我们的 CRD 规范 YAML 后,让我们来看一下我们 CRD 的一个实例 - 正如我们刚刚审查的规范所定义的,YAML 将如下所示:

Delayed-job.yaml

apiVersion: delayedresources.mydomain.com/v1
kind: DelayedJob
metadata:
  name: my-instance-of-delayed-job
spec:
  delaySeconds: 6000
  image: "busybox"

正如您所看到的,这就像我们的 CRD 定义了这个对象。现在,所有的部分都就位了,让我们测试一下我们的 CRD!

测试自定义资源定义

让我们继续在 Kubernetes 上测试我们的 CRD 概念:

  1. 首先,让我们在 Kubernetes 中创建 CRD 规范 - 就像我们创建任何其他对象一样:
kubectl apply -f delayedjob-crd-spec.yaml

这将导致以下输出:

customresourcedefinition "delayedjob.delayedresources.mydomain.com" has been created
  1. 现在,Kubernetes 将接受对我们的DelayedJob资源的请求。我们可以通过最终使用前面的资源 YAML 创建一个来测试这一点:
kubectl apply -f my-delayed-job.yaml

如果我们正确定义了我们的 CRD,我们将看到以下输出:

delayedjob "my-instance-of-delayed-job" has been created

正如您所看到的,Kubernetes API 服务器已成功创建了我们的DelayedJob实例!

现在,您可能会问一个非常相关的问题 - 现在怎么办?这是一个很好的问题,因为事实上,到目前为止,我们实际上什么也没有做,只是向 Kubernetes API 数据库添加了一个新的

仅仅因为我们给我们的DelayedJob资源一个应用程序镜像和一个delaySeconds字段,并不意味着我们打算的任何功能实际上会发生。通过创建我们的DelayedJob实例,我们只是向那个添加了一个条目。我们可以使用 Kubernetes API 或kubectl命令获取它,编辑它或删除它,但没有实现任何应用功能。

为了让我们的DelayedJob资源真正做些什么,我们需要一个自定义控制器,它将获取我们的DelayedJob实例并对其进行操作。最终,我们仍然需要使用官方 Kubernetes 资源(如 Pods 等)来实现实际的容器功能。

这就是我们现在要讨论的。有许多构建 Kubernetes 自定义控制器的方法,但流行的方法是运算符模式。让我们继续下一节,看看我们如何让我们的DelayedJob资源拥有自己的生命。

使用 Kubernetes 运算符自管理功能

在没有首先讨论Operator Framework之前,不可能讨论 Kubernetes 运算符。一个常见的误解是,运算符是通过 Operator Framework 专门构建的。Operator Framework 是一个开源框架,最初由 Red Hat 创建,旨在简化编写 Kubernetes 运算符。

实际上,运算符只是一个与 Kubernetes 接口并对资源进行操作的自定义控制器。Operator Framework 是一种使 Kubernetes 运算符的一种偏见方式,但还有许多其他开源框架可以使用 - 或者,您可以从头开始制作一个!

使用框架构建运算符时,最流行的两个选项是前面提到的Operator FrameworkKubebuilder

这两个项目有很多共同之处。它们都使用controller-toolscontroller-runtime,这是两个由 Kubernetes 项目官方支持的构建 Kubernetes 控制器的库。如果您从头开始构建运算符,使用这些官方支持的控制器库将使事情变得更容易。

与 Operator Framework 不同,Kubebuilder 是 Kubernetes 项目的官方部分,就像controller-toolscontroller-runtime库一样 - 但这两个项目都有其优缺点。重要的是,这两个选项以及一般的 Operator 模式都是在集群上运行控制器。这似乎是最好的选择,但你也可以在集群外运行控制器,并且它可以正常工作。要开始使用 Operator Framework,请查看官方 GitHub 网站github.com/operator-framework。对于 Kubebuilder,你可以查看github.com/kubernetes-sigs/kubebuilder

大多数操作员,无论使用哪种框架,都遵循控制循环范式 - 让我们看看这个想法是如何工作的。

映射操作员控制循环

控制循环是系统设计和编程中的控制方案,由一系列逻辑过程组成的永无止境的循环。通常,控制循环实现了一种测量-分析-调整的方法,它测量系统的当前状态,分析需要做出哪些改变使其与预期状态一致,然后调整系统组件使其与预期状态一致(或至少更接近预期状态)。

在 Kubernetes 的操作员或控制器中,这个操作通常是这样工作的:

  1. 首先是一个“监视”步骤 - 也就是监视 Kubernetes API 中预期状态的变化,这些状态存储在etcd中。

  2. 然后是一个“分析”步骤 - 控制器决定如何使集群状态与预期状态一致。

  3. 最后是一个“更新”步骤 - 更新集群状态以实现集群变化的意图。

为了帮助理解控制循环,这里有一个图表显示了这些部分是如何组合在一起的:

图 13.1 - 测量分析更新循环

图 13.1 - 测量分析更新循环

让我们使用 Kubernetes 调度器来说明这一点 - 它本身就是一个控制循环过程:

  1. 让我们从一个假设的集群开始,处于稳定状态:所有的 Pod 都已经调度,节点也健康,一切都在正常运行。

  2. 然后,用户创建了一个新的 Pod。

我们之前讨论过 kubelet 是基于pull的工作方式。这意味着当 kubelet 在其节点上创建一个 Pod 时,该 Pod 已经通过调度器分配给了该节点。然而,当通过kubectl createkubectl apply命令首次创建 Pod 时,该 Pod 尚未被调度或分配到任何地方。这就是我们的调度器控制循环开始的地方:

  1. 第一步是测量,调度器从 Kubernetes API 读取状态。当从 API 列出 Pod 时,它发现其中一个 Pod 未分配给任何节点。现在它转移到下一步。

  2. 接下来,调度器对集群状态和 Pod 需求进行分析,以决定将 Pod 分配给哪个节点。正如我们在前几章中讨论的那样,这涉及到 Pod 资源限制和请求、节点状态、放置控制等等,这使得它成为一个相当复杂的过程。一旦处理完成,更新步骤就可以开始了。

  3. 最后,更新 - 调度器通过将 Pod 分配给从步骤 2分析中获得的节点来更新集群状态。此时,kubelet 接管自己的控制循环,并为其节点上的 Pod 创建相关的容器。

接下来,让我们将从调度器控制循环中学到的内容应用到我们自己的DelayedJob资源上。

为自定义资源定义设计运算符

实际上,为我们的DelayedJob CRD 编写运算符超出了我们书的范围,因为这需要对编程语言有所了解。如果您选择使用 Go 构建运算符,它提供了与 Kubernetes SDK、controller-toolscontroller-runtime最多的互操作性,但任何可以编写 HTTP 请求的编程语言都可以使用,因为这是所有 SDK 的基础。

然而,我们仍将逐步实现DelayedJob CRD 的运算符步骤,使用一些伪代码。让我们一步一步来。

步骤 1:测量

首先是测量步骤,我们将在我们的伪代码中实现为一个永远运行的while循环。在生产实现中,会有去抖动、错误处理和一堆其他问题,但是对于这个说明性的例子,我们会保持简单。

看一下这个循环的伪代码,这实际上是我们应用程序的主要功能:

Main-function.pseudo

// The main function of our controller
function main() {
  // While loop which runs forever
  while() {
     // fetch the full list of delayed job objects from the cluster
	var currentDelayedJobs = kubeAPIConnector.list("delayedjobs");
     // Call the Analysis step function on the list
     var jobsToSchedule = analyzeDelayedJobs(currentDelayedJobs);
     // Schedule our Jobs with added delay
     scheduleDelayedJobs(jobsToSchedule);
     wait(5000);
  }
}

正如您所看到的,在我们的main函数中的循环调用 Kubernetes API 来查找存储在etcd中的delayedjobs CRD 列表。这是measure步骤。然后调用分析步骤,并根据其结果调用更新步骤来安排需要安排的任何DelayedJobs

重要说明

请记住,在这个例子中,Kubernetes 调度程序仍然会执行实际的容器调度 - 但是我们首先需要将我们的DelayedJob简化为官方的 Kubernetes 资源。

在更新步骤之后,我们的循环在执行循环之前等待完整的 5 秒。这确定了控制循环的节奏。接下来,让我们继续进行分析步骤。

步骤 2:分析

接下来,让我们来审查我们操作员的Analysis步骤,这是我们控制器伪代码中的analyzeDelayedJobs函数:

分析函数伪代码

// The analysis function
function analyzeDelayedJobs(listOfDelayedJobs) {
  var listOfJobsToSchedule = [];
  foreach(dj in listOfDelayedJobs) {
    // Check if dj has been scheduled, if not, add a Job object with
    // added delay command to the to schedule array
    if(dj.annotations["is-scheduled"] != "true") {
      listOfJobsToSchedule.push({
        Image: dj.image,
        Command: "sleep " + dj.delaySeconds + "s",
        originalDjName: dj.name
      });
    }
  }
  return listOfJobsToSchedule;  
}

正如您所看到的,前面的函数循环遍历了从Measure循环传递的集群中的DelayedJob对象列表。然后,它检查DelayedJob是否已经通过检查对象的注释之一的值来进行了调度。如果尚未安排,它将向名为listOfJobsToSchedule的数组添加一个对象,该数组包含DelayedJob对象中指定的图像,一个命令以睡眠指定的秒数,以及DelayedJob的原始名称,我们将在Update步骤中用来标记为已调度。

最后,在Analyze步骤中,analyzeDelayedJobs函数将我们新创建的listOfJobsToSchedule数组返回给主函数。让我们用最终的更新步骤来结束我们的操作员设计,这是我们主循环中的scheduleDelayedJobs函数。

步骤 3:更新

最后,我们的控制循环的Update部分将从我们的分析中获取输出,并根据需要更新集群以创建预期的状态。以下是伪代码:

更新函数伪代码

// The update function
function scheduleDelayedJobs(listOfJobs) {
  foreach(job in listOfDelayedJobs) {
    // First, go ahead and schedule a regular Kubernetes Job
    // which the Kube scheduler can pick up on.
    // The delay seconds have already been added to the job spec
    // in the analysis step
    kubeAPIConnector.create("job", job.image, job.command);
    // Finally, mark our original DelayedJob with a "scheduled"
    // attribute so our controller doesn't try to schedule it again
    kubeAPIConnector.update("delayedjob", job.originalDjName,
    annotations: {
      "is-scheduled": "true"
    });
  } 
}

在这种情况下,我们正在使用从我们的DelayedJob对象派生的常规 Kubernetes 对象,并在 Kubernetes 中创建它,以便Kube调度程序可以找到它,创建相关的 Pod 并管理它。一旦我们使用延迟创建了常规作业对象,我们还会使用注释更新我们的DelayedJob CRD 实例,将is-scheduled注释设置为true,以防止它被重新调度。

这完成了我们的控制循环 - 从这一点开始,Kube调度器接管并且我们的 CRD 被赋予生命作为一个 Kubernetes Job 对象,它控制一个 Pod,最终分配给一个 Node,并且一个容器被调度来运行我们的代码!

当然,这个例子是高度简化的,但你会惊讶地发现有多少 Kubernetes 操作员执行一个简单的控制循环来协调 CRD 并将其简化为基本的 Kubernetes 资源。操作员可以变得非常复杂,并执行特定于应用程序的功能,例如备份数据库、清空持久卷等,但这种功能通常与被控制的内容紧密耦合。

现在我们已经讨论了 Kubernetes 控制器中的操作员模式,我们可以谈谈一些特定于云的 Kubernetes 控制器的开源选项。

使用特定于云的 Kubernetes 扩展

通常情况下,在托管的 Kubernetes 服务(如 Amazon EKS、Azure AKS 和 Google Cloud 的 GKE)中默认可用,特定于云的 Kubernetes 扩展和控制器可以与相关的云平台紧密集成,并且可以轻松地从 Kubernetes 控制其他云资源。

即使不添加任何额外的第三方组件,许多这些特定于云的功能都可以通过云控制器管理器CCM)组件在上游 Kubernetes 中使用,该组件包含许多与主要云提供商集成的选项。这通常是在每个公共云上的托管 Kubernetes 服务中默认启用的功能,但它们可以与在特定云平台上运行的任何集群集成,无论是托管还是非托管。

在本节中,我们将回顾一些常见的云扩展到 Kubernetes 中,包括云控制器管理器(CCM)和需要安装其他控制器的功能,例如external-dnscluster-autoscaler。让我们从一些常用的 CCM 功能开始。

了解云控制器管理器组件

正如在第一章中所述,与 Kubernetes 通信,CCM 是一个官方支持的 Kubernetes 控制器,提供了对几个公共云服务功能的钩子。为了正常运行,CCM 组件需要以访问特定云服务的权限启动,例如在 AWS 中的 IAM 角色。

对于官方支持的云,如 AWS、Azure 和 Google Cloud,CCM 可以简单地作为集群中的 DaemonSet 运行。我们使用 DaemonSet,因为 CCM 可以执行诸如在云提供商中创建持久存储等任务,并且需要能够将存储附加到特定的节点。如果您使用的是官方不支持的云,您可以为该特定云运行 CCM,并且应该遵循该项目中的具体说明。这些替代类型的 CCM 通常是开源的,可以在 GitHub 上找到。关于安装 CCM 的具体信息,让我们继续下一节。

安装 cloud-controller-manager

通常,在创建集群时配置 CCM。如前一节所述,托管服务,如 EKS、AKS 和 GKE,将已经启用此组件,但即使 Kops 和 Kubeadm 也将 CCM 组件作为安装过程中的一个标志暴露出来。

假设您尚未以其他方式安装 CCM 并计划使用上游版本的官方支持的公共云之一,您可以将 CCM 安装为 DaemonSet。

首先,您需要一个ServiceAccount

Service-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: cloud-controller-manager
  namespace: kube-system

这个ServiceAccount将被用来给予 CCM 必要的访问权限。

接下来,我们需要一个ClusterRoleBinding

Clusterrolebinding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system:cloud-controller-manager
subjects:
- kind: ServiceAccount
  name: cloud-controller-manager
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin

如您所见,我们需要给cluster-admin角色访问我们的 CCM 服务账户。CCM 将需要能够编辑节点,以及其他一些操作。

最后,我们可以部署 CCM 的DaemonSet本身。您需要使用适合您特定云提供商的正确设置填写此 YAML 文件-查看您云提供商关于 Kubernetes 的文档以获取这些信息。

DaemonSet规范非常长,因此我们将分两部分进行审查。首先,我们有DaemonSet的模板,其中包含所需的标签和名称:

Daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    k8s-app: cloud-controller-manager
  name: cloud-controller-manager
  namespace: kube-system
spec:
  selector:
    matchLabels:
      k8s-app: cloud-controller-manager
  template:
    metadata:
      labels:
        k8s-app: cloud-controller-manager

正如您所看到的,为了匹配我们的ServiceAccount,我们在kube-system命名空间中运行 CCM。我们还使用k8s-app标签对DaemonSet进行标记,以将其区分为 Kubernetes 控制平面组件。

接下来,我们有DaemonSet的规范:

Daemonset.yaml(续)

    spec:
      serviceAccountName: cloud-controller-manager
      containers:
      - name: cloud-controller-manager
        image: k8s.gcr.io/cloud-controller-manager:<current ccm version for your version of k8s>
        command:
        - /usr/local/bin/cloud-controller-manager
        - --cloud-provider=<cloud provider name>
        - --leader-elect=true
        - --use-service-account-credentials
        - --allocate-node-cidrs=true
        - --configure-cloud-routes=true
        - --cluster-cidr=<CIDR of the cluster based on Cloud Provider>
      tolerations:
      - key: node.cloudprovider.kubernetes.io/uninitialized
        value: "true"
        effect: NoSchedule
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      nodeSelector:
        node-role.kubernetes.io/master: ""

正如您所看到的,此规范中有一些地方需要查看您选择的云提供商的文档或集群网络设置,以找到正确的值。特别是在网络标志中,例如--cluster-cidr--configure-cloud-routes,这些值可能会根据您如何设置集群而改变,即使在单个云提供商上也是如此。

既然我们在集群中以某种方式运行 CCM,让我们深入了解它提供的一些功能。

了解云控制器管理器的功能

默认的 CCM 在一些关键领域提供了功能。首先,CCM 包含了节点、路由和服务的子控制器。让我们依次审查每个,看看它为我们提供了什么,从节点/节点生命周期控制器开始。

CCM 节点/节点生命周期控制器

CCM 节点控制器确保集群状态,就是集群中的节点与云提供商系统中的节点是等价的。一个简单的例子是 AWS 中的自动扩展组。在使用 AWS EKS(或者只是在 AWS EC2 上使用 Kubernetes,尽管这需要额外的配置)时,可以配置 AWS 自动扩展组中的工作节点组,根据节点的 CPU 或内存使用情况进行扩展或缩减。当这些节点由云提供商添加和初始化时,CCM 节点控制器将确保集群对于云提供商呈现的每个节点都有一个节点资源。

接下来,让我们转向路由控制器。

CCM 路由控制器

CCM 路由控制器负责以支持 Kubernetes 集群的方式配置云提供商的网络设置。这可能包括分配 IP 和在节点之间设置路由。服务控制器也处理网络 - 但是外部方面。

CCM 服务控制器

CCM 服务控制器提供了在公共云提供商上运行 Kubernetes 的“魔力”。我们在第五章中审查的一个方面是LoadBalancer服务,服务和入口 - 与外部世界通信,例如,在配置了 AWS CCM 的集群上,类型为LoadBalancer的服务将自动配置匹配的 AWS 负载均衡器资源,为您提供了一种在集群中公开服务的简单方法,而无需处理NodePort设置甚至 Ingress。

现在我们了解了 CCM 提供的内容,我们可以进一步探讨一下在公共云上运行 Kubernetes 时经常使用的一些其他云提供商扩展。首先,让我们看看external-dns

使用 Kubernetes 的 external-dns

external-dns库是一个官方支持的 Kubernetes 插件,允许集群配置外部 DNS 提供程序以自动化方式为服务和 Ingress 提供 DNS 解析。external-dns插件支持广泛的云提供商,如 AWS 和 Azure,以及其他 DNS 服务,如 Cloudflare。

重要说明

要安装external-dns,您可以在github.com/kubernetes-sigs/external-dns上查看官方 GitHub 存储库。

一旦在您的集群上实施了external-dns,就可以简单地以自动化的方式创建新的 DNS 记录。要测试external-dns与服务的配合,我们只需要在 Kubernetes 中创建一个带有适当注释的服务。

让我们看看这是什么样子:

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-service-with-dns
  annotations:
    external-dns.alpha.kubernetes.io/hostname: myapp.mydomain.com
spec:
  type: LoadBalancer
  ports:
  - port: 80
    name: http
    targetPort: 80
  selector:
    app: my-app

正如您所看到的,我们只需要为external-dns控制器添加一个注释,以便检查要在 DNS 中创建的域记录。当然,域和托管区必须可以被您的external-dns控制器访问 - 例如,在 AWS Route 53 或 Azure DNS 上。请查看external-dns GitHub 存储库上的具体文档。

一旦服务启动运行,external-dns将获取注释并创建一个新的 DNS 记录。这种模式非常适合多租户或每个版本部署,因为像 Helm 图表这样的东西可以使用变量来根据应用程序的部署版本或分支来更改域 - 例如,v1.myapp.mydomain.com

对于 Ingress,这甚至更容易 - 您只需要在 Ingress 记录中指定一个主机,就像这样:

ingress.yaml

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-domain-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx".
spec:
  rules:
  - host: myapp.mydomain.com
    http:
      paths:
      - backend:
          serviceName: my-app-service
          servicePort: 80

此主机值将自动创建一个 DNS 记录,指向 Ingress 正在使用的任何方法 - 例如,在 AWS 上的负载均衡器。

接下来,让我们谈谈cluster-autoscaler库的工作原理。

使用 cluster-autoscaler 插件

external-dns类似,cluster-autoscaler是 Kubernetes 的一个官方支持的附加组件,支持一些主要的云提供商具有特定功能。 cluster-autoscaler的目的是触发集群中节点数量的扩展。它通过控制云提供商自己的扩展资源(例如 AWS 自动缩放组)来执行此过程。

集群自动缩放器将在任何单个 Pod 由于节点上的资源限制而无法调度时执行向上缩放操作,但仅当现有节点大小(例如,在 AWS 中为t3.medium大小的节点)可以允许 Pod 被调度时才会执行。

类似地,集群自动缩放器将在任何节点可以在不会对其他节点造成内存或 CPU 压力的情况下清空 Pod 时执行向下缩放操作。

要安装cluster-autoscaler,只需按照您的云提供商的正确说明,为集群类型和预期的cluster-autoscaler版本进行操作。例如,EKS 上的 AWScluster-autoscaler的安装说明可在aws.amazon.com/premiumsupport/knowledge-center/eks-cluster-autoscaler-setup/找到。

接下来,让我们看看如何通过检查 Kubernetes 生态系统来找到开源和闭源的扩展。

与生态系统集成

Kubernetes(以及更一般地说,云原生)生态系统是庞大的,包括数百个流行的开源软件库,以及成千上万个新兴的软件库。这可能很难导航,因为每个月都会有新的技术需要审查,而收购、合并和公司倒闭可能会将您最喜欢的开源库变成一个未维护的混乱。

幸运的是,这个生态系统中有一些结构,了解它是值得的,以帮助导航云原生开源选项的匮乏。这其中的第一个重要结构组件是云原生计算基金会CNCF

介绍云原生计算基金会

CNCF 是 Linux 基金会的一个子基金会,它是一个主持开源项目并协调不断变化的公司列表的非营利实体,这些公司为和使用开源软件做出贡献。

CNCF 几乎完全是为了引导 Kubernetes 项目的未来而成立的。它是在 Kubernetes 1.0 发布时宣布的,并且此后已经发展到涵盖了云原生空间中的数百个项目 - 从 Prometheus 到 Envoy 到 Helm,以及更多。

了解 CNCF 组成项目的最佳方法是查看 CNCF Cloud Native Landscape,网址为landscape.cncf.io/

如果你对你在 Kubernetes 或云原生中遇到的问题感兴趣,CNCF Landscape 是一个很好的起点。对于每个类别(监控、日志记录、无服务器、服务网格等),都有几个开源选项供您选择。

当前云原生技术生态系统的优势和劣势。有大量的选择可用,这使得正确的路径通常不明确,但也意味着你可能会找到一个接近你确切需求的解决方案。

CNCF 还经营着一个官方的 Kubernetes 论坛,可以从 Kubernetes 官方网站kubernetes.io加入。Kubernetes 论坛的网址是discuss.kubernetes.io/

最后,值得一提的是KubeCon/CloudNativeCon,这是由 CNCF 主办的一个大型会议,涵盖了 Kubernetes 本身和许多生态项目等主题。KubeCon每年都在扩大规模,2019 年KubeCon North America有近 12,000 名与会者。

总结

在本章中,我们学习了如何扩展 Kubernetes。首先,我们讨论了 CRDs - 它们是什么,一些相关的用例,以及如何在集群中实现它们。接下来,我们回顾了 Kubernetes 中操作员的概念,并讨论了如何使用操作员或自定义控制器来赋予 CRD 生命。

然后,我们讨论了针对 Kubernetes 的特定于云供应商的扩展,包括cloud-controller-managerexternal-dnscluster-autoscaler。最后,我们介绍了大型云原生开源生态系统以及发现适合你使用情况的项目的一些好方法。

本章中使用的技能将帮助您扩展 Kubernetes 集群,以便与您的云提供商以及您自己的自定义功能进行接口。

在下一章中,我们将讨论作为应用于 Kubernetes 的两种新兴架构模式 - 无服务器和服务网格。

问题

  1. 什么是 CRD 的服务版本和存储版本之间的区别?

  2. 自定义控制器或操作员控制循环的三个典型部分是什么?

  3. cluster-autoscaler如何与现有的云提供商扩展解决方案(如 AWS 自动扩展组)交互?

进一步阅读

第十四章:服务网格和无服务器

本章讨论了高级 Kubernetes 模式。首先,它详细介绍了时髦的服务网格模式,其中通过 sidecar 代理处理可观察性和服务到服务的发现,以及设置流行的服务网格 Istio 的指南。最后,它描述了无服务器模式以及如何在 Kubernetes 中应用它。本章的主要案例研究将包括为示例应用程序和服务发现设置 Istio,以及 Istio 入口网关。

让我们从讨论 sidecar 代理开始,它为服务网格的服务到服务连接性奠定了基础。

在本章中,我们将涵盖以下主题:

  • 使用 sidecar 代理

  • 向 Kubernetes 添加服务网格

  • 在 Kubernetes 上实现无服务器

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具的计算机,以及一个可用的 Kubernetes 集群。请参阅第一章与 Kubernetes 通信,了解快速启动和运行 Kubernetes 的几种方法,以及如何安装kubectl工具的说明。

本章中使用的代码可以在书的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter14

使用 sidecar 代理

正如我们在本书中早些时候提到的,sidecar 是一种模式,其中一个 Pod 包含另一个容器,除了要运行的实际应用程序容器。这个额外的“额外”容器就是 sidecar。Sidecar 可以用于许多不同的原因。一些最常用的 sidecar 用途是监控、日志记录和代理。

对于日志记录,一个 sidecar 容器可以从应用容器中获取应用程序日志(因为它们可以共享卷并在本地通信),然后将日志发送到集中式日志堆栈,或者解析它们以进行警报。监控也是类似的情况,sidecar Pod 可以跟踪并发送有关应用程序 Pod 的指标。

使用侧车代理时,当请求进入 Pod 时,它们首先进入代理容器,然后路由请求(在记录或执行其他过滤之后)到应用程序容器。同样,当请求离开应用程序容器时,它们首先进入代理,代理可以提供 Pod 的路由。

通常,诸如 NGINX 之类的代理侧车只为进入 Pod 的请求提供代理。然而,在服务网格模式中,进入和离开 Pod 的请求都通过代理,这为服务网格模式本身提供了基础。

请参考以下图表,了解侧车代理如何与应用程序容器交互:

图 14.1 - 代理侧车

图 14.1 - 代理侧车

正如您所看到的,侧车代理负责将请求路由到 Pod 中的应用程序容器,并允许功能,如服务路由、记录和过滤。

侧车代理模式是一种替代基于 DaemonSet 的代理,其中每个节点上的代理 Pod 处理对该节点上其他 Pod 的代理。Kubernetes 代理本身类似于 DaemonSet 模式。使用侧车代理可以提供比使用 DaemonSet 代理更灵活的灵活性,但性能效率会有所降低,因为需要运行许多额外的容器。

一些用于 Kubernetes 的流行代理选项包括以下内容:

  • NGINX

  • HAProxy

  • Envoy

虽然 NGINX 和 HAProxy 是更传统的代理,但 Envoy 是专门为分布式、云原生环境构建的。因此,Envoy 构成了流行的服务网格和为 Kubernetes 构建的 API 网关的核心。

在我们讨论 Envoy 之前,让我们讨论安装其他代理作为侧车的方法。

使用 NGINX 作为侧车反向代理

在我们指定 NGINX 如何作为侧车代理之前,值得注意的是,在即将发布的 Kubernetes 版本中,侧车将成为一个 Kubernetes 资源类型,它将允许轻松地向大量 Pod 注入侧车容器。然而,目前侧车容器必须在 Pod 或控制器(ReplicaSet、Deployment 等)级别指定。

让我们看看如何使用以下部署 YAML 配置 NGINX 作为侧车,我们暂时不会创建。这个过程比使用 NGINX Ingress Controller 要手动一些。

出于空间原因,我们将 YAML 分成两部分,并删除了一些冗余内容,但您可以在代码存储库中完整地看到它。让我们从部署的容器规范开始:

Nginx-sidecar.yaml:

   spec:
     containers:
     - name: myapp
       image: ravirdv/http-responder:latest
       imagePullPolicy: IfNotPresent
     - name: nginx-sidecar
       image: nginx
       imagePullPolicy: IfNotPresent
       volumeMounts:
         - name: secrets
           mountPath: /app/cert
         - name: config
           mountPath: /etc/nginx/nginx.conf
           subPath: nginx.conf

正如您所看到的,我们指定了两个容器,即我们的主应用程序容器myappnginx sidecar,我们通过卷挂载注入了一些配置,以及一些 TLS 证书。

接下来,让我们看看同一文件中的volumes规范,我们在其中注入了一些证书(来自一个密钥)和config(来自ConfigMap):

    volumes:
     - name: secrets
       secret:
         secretName: nginx-certificates
         items:
           - key: server-cert
             path: server.pem
           - key: server-key
             path: server-key.pem
     - name: config
       configMap:
         name: nginx-configuration

正如您所看到的,我们需要一个证书和一个密钥。

接下来,我们需要使用ConfigMap创建 NGINX 配置。NGINX 配置如下:

nginx.conf:

http {
    sendfile        on;
    include       mime.types;
    default_type  application/octet-stream;
    keepalive_timeout  80;
    server {
       ssl_certificate      /app/cert/server.pem;
      ssl_certificate_key  /app/cert/server-key.pem;
      ssl_protocols TLSv1.2;
      ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!EECDH+3DES:!RSA+3DES:!MD5;
      ssl_prefer_server_ciphers on;
      listen       443 ssl;
      server_name  localhost;
      location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:5000/;
      }
    }
}
worker_processes  1;
events {
    worker_connections  1024;
}

正如您所看到的,我们有一些基本的 NGINX 配置。重要的是,我们有proxy_pass字段,它将请求代理到127.0.0.1上的端口,或者本地主机。由于 Pod 中的容器可以共享本地主机端口,这充当了我们的 sidecar 代理。出于本书的目的,我们不会审查所有其他行,但是请查看 NGINX 文档,了解每行的更多信息(nginx.org/en/docs/)。

现在,让我们从这个文件创建ConfigMap。使用以下命令来命令式地创建ConfigMap

kubectl create cm nginx-configuration --from-file=nginx.conf=./nginx.conf

这将导致以下输出:

Configmap "nginx-configuration" created

接下来,让我们为 NGINX 创建 TLS 证书,并将它们嵌入到 Kubernetes 密钥中。您需要安装 CFSSL(CloudFlare 的 PKI/TLS 开源工具包)库才能按照这些说明进行操作,但您也可以使用任何其他方法来创建您的证书。

首先,我们需要创建证书颁发机构CA)。从 CA 的 JSON 配置开始:

nginxca.json:

{
   "CN": "mydomain.com",
   "hosts": [
       "mydomain.com",
       "www.mydomain.com"
   ],
   "key": {
       "algo": "rsa",
       "size": 2048
   },
   "names": [
       {
           "C": "US",
           "ST": "MD",
           "L": "United States"
       }
   ]
}

现在,使用 CFSSL 创建 CA 证书:

cfssl gencert -initca nginxca.json | cfssljson -bare nginxca

接下来,我们需要 CA 配置:

Nginxca-config.json:

{
  "signing": {
      "default": {
          "expiry": "20000h"
      },
      "profiles": {
          "client": {
              "expiry": "43800h",
              "usages": [
                  "signing",
                  "key encipherment",
                  "client auth"
              ]
          },
          "server": {
              "expiry": "20000h",
              "usages": [
                  "signing",
                  "key encipherment",
                  "server auth",
                  "client auth"
              ]
          }
      }
  }
}

我们还需要一个证书请求配置:

Nginxcarequest.json:

{
  "CN": "server",
  "hosts": [
    ""
  ],
  "key": {
    "algo": "rsa",
    "size": 2048
  }
}

现在,我们实际上可以创建我们的证书了!使用以下命令:

cfssl gencert -ca=nginxca.pem -ca-key=nginxca-key.pem -config=nginxca-config.json -profile=server -hostname="127.0.0.1" nginxcarequest.json | cfssljson -bare server

作为证书密钥的最后一步,通过最后一个cfssl命令从证书文件的输出创建 Kubernetes 密钥:

kubectl create secret generic nginx-certs --from-file=server-cert=./server.pem --from-file=server-key=./server-key.pem

现在,我们终于可以创建我们的部署了:

kubectl apply -f nginx-sidecar.yaml 

这将产生以下输出:

deployment "myapp" created

为了检查 NGINX 代理功能,让我们创建一个服务来指向我们的部署:

Nginx-sidecar-service.yaml:

apiVersion: v1
kind: Service
metadata:
 name:myapp
 labels:
   app: myapp
spec:
 selector:
   app: myapp
 type: NodePort
 ports:
 - port: 443
   targetPort: 443
   protocol: TCP
   name: https

现在,使用https访问集群中的任何节点应该会导致一个正常工作的 HTTPS 连接!但是,由于我们的证书是自签名的,浏览器将显示一个不安全的消息。

现在您已经看到了 NGINX 如何与 Kubernetes 一起作为边车代理使用,让我们转向更现代的、云原生的代理边车 - Envoy。

使用 Envoy 作为边车代理

Envoy 是为云原生环境构建的现代代理。在我们稍后将审查的 Istio 服务网格中,Envoy 充当反向和正向代理。然而,在我们进入 Istio 之前,让我们尝试部署 Envoy 作为代理。

我们将告诉 Envoy 在哪里路由各种请求,使用路由、监听器、集群和端点。这个功能是 Istio 的核心,我们将在本章后面进行审查。

让我们逐个查看 Envoy 配置的每个部分,看看它是如何工作的。

Envoy 监听器

Envoy 允许配置一个或多个监听器。对于每个监听器,我们指定 Envoy 要监听的端口,以及我们想要应用到监听器的任何过滤器。

过滤器可以提供复杂的功能,包括缓存、授权、跨源资源共享CORS)配置等。Envoy 支持将多个过滤器链接在一起。

Envoy 路由

某些过滤器具有路由配置,指定应接受请求的域、路由匹配和转发规则。

Envoy 集群

Envoy 中的集群表示可以根据监听器中的路由将请求路由到的逻辑服务。在云原生环境中,集群可能包含多个可能的 IP 地址,因此它支持负载均衡配置,如轮询

Envoy 端点

最后,在集群中指定端点作为服务的一个逻辑实例。Envoy 支持从 API 获取端点列表(这基本上是 Istio 服务网格中发生的事情),并在它们之间进行负载均衡。

在 Kubernetes 上的生产 Envoy 部署中,很可能会使用某种形式的动态、API 驱动的 Envoy 配置。Envoy 的这个特性称为 xDS,并被 Istio 使用。此外,还有其他开源产品和解决方案使用 Envoy 与 xDS,包括 Ambassador API 网关。

在本书中,我们将查看一些静态(非动态)的 Envoy 配置;这样,我们可以分解配置的每个部分,当我们审查 Istio 时,您将对一切是如何工作有一个很好的理解。

现在让我们深入研究一个 Envoy 配置,用于设置一个单个 Pod 需要能够将请求路由到两个服务,Service 1Service 2。设置如下:

图 14.2-出站 envoy 代理

图 14.2-出站 envoy 代理

如您所见,我们应用 Pod 中的 Envoy sidecar 将配置为路由到两个上游服务,Service 1Service 2。两个服务都有两个可能的端点。

在 Envoy xDS 的动态设置中,端点的 Pod IPs 将从 API 中加载,但是为了我们的审查目的,我们将在端点中显示静态的 Pod IPs。我们将完全忽略 Kubernetes 服务,而是直接访问 Pod IPs 以进行轮询配置。在服务网格场景中,Envoy 也将部署在所有目标 Pod 上,但现在我们将保持简单。

现在,让我们看看如何在 Envoy 配置 YAML 中配置这个网络映射(您可以在代码存储库中找到完整的配置)。这当然与 Kubernetes 资源 YAML 非常不同-我们将在稍后讨论这一部分。整个配置涉及大量的 YAML,所以让我们一点一点地来。

理解 Envoy 配置文件

首先,让我们看看我们配置的前几行-关于我们的 Envoy 设置的一些基本信息。

Envoy-configuration.yaml:

admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

如您所见,我们为 Envoy 的admin指定了一个端口和地址。与以下配置一样,我们将 Envoy 作为一个 sidecar 运行,因此地址将始终是本地的- 0.0.0.0。接下来,我们用一个 HTTPS 监听器开始我们的监听器列表:

static_resources:
  listeners:
   - address:
      socket_address:
        address: 0.0.0.0
        port_value: 8443
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          stat_prefix: ingress_https
          codec_type: auto
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/service/1"
                route:
                  cluster: service1
              - match:
                  prefix: "/service/2"
                route:
                  cluster: service2
          http_filters:
          - name: envoy.filters.http.router
            typed_config: {}

如您所见,对于每个 Envoy 监听器,我们有一个本地地址和端口(此监听器是一个 HTTPS 监听器)。然后,我们有一个过滤器列表-尽管在这种情况下,我们只有一个。每个 envoy 过滤器类型的配置略有不同,我们不会逐行审查它(请查看 Envoy 文档以获取更多信息www.envoyproxy.io/docs),但这个特定的过滤器匹配两个路由,/service/1/service/2,并将它们路由到两个 envoy 集群。在我们的 YAML 的第一个 HTTPS 监听器部分下,我们有 TLS 配置,包括证书:

      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
              certificate_chain:
                inline_string: |
                   <INLINE CERT FILE>
              private_key:
                inline_string: |
                  <INLINE PRIVATE KEY FILE>

如您所见,此配置传递了private_keycertificate_chain。接下来,我们有第二个也是最后一个监听器,一个 HTTP 监听器:

  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/service1"
                route:
                  cluster: service1
              - match:
                  prefix: "/service2"
                route:
                  cluster: service2
          http_filters:
          - name: envoy.filters.http.router
            typed_config: {}

如您所见,这个配置与我们的 HTTPS 监听器的配置非常相似,只是它监听不同的端口,并且不包括证书信息。接下来,我们进入我们的集群配置。在我们的情况下,我们有两个集群,一个用于service1,一个用于service2。首先是service1

  clusters:
  - name: service1
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    load_assignment:
      cluster_name: service1
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service1
                port_value: 5000

接下来,Service 2

  - name: service2
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    load_assignment:
      cluster_name: service2
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service2
                port_value: 5000

对于这些集群中的每一个,我们指定请求应该路由到哪里,以及到哪个端口。例如,对于我们的第一个集群,请求被路由到http://service1:5000。我们还指定了负载均衡策略(在这种情况下是轮询)和连接的超时时间。现在我们有了我们的 Envoy 配置,我们可以继续创建我们的 Kubernetes Pod,并注入我们的 sidecar 以及 envoy 配置。我们还将把这个文件分成两部分,因为它有点太大了,以至于难以理解:

Envoy-sidecar-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: my-service
    spec:
      containers:
      - name: envoy
        image: envoyproxy/envoy:latest
        ports:
          - containerPort: 9901
            protocol: TCP
            name: envoy-admin
          - containerPort: 8786
            protocol: TCP
            name: envoy-web

如您所见,这是一个典型的部署 YAML。在这种情况下,我们实际上有两个容器。首先是 Envoy 代理容器(或边车)。它监听两个端口。接下来,继续向下移动 YAML,我们为第一个容器进行了卷挂载(用于保存 Envoy 配置),以及一个启动命令和参数:

        volumeMounts:
          - name: envoy-config-volume
            mountPath: /etc/envoy-config/
        command: ["/usr/local/bin/envoy"]
        args: ["-c", "/etc/envoy-config/config.yaml", "--v2-config-only", "-l", "info","--service-cluster","myservice","--service-node","myservice", "--log-format", "[METADATA][%Y-%m-%d %T.%e][%t][%l][%n] %v"]

最后,我们有我们 Pod 中的第二个容器,这是一个应用容器:

- name: my-service
        image: ravirdv/http-responder:latest
        ports:
        - containerPort: 5000
          name: svc-port
          protocol: TCP
      volumes:
        - name: envoy-config-volume
          configMap:
            name: envoy-config
            items:
              - key: envoy-config
                path: config.yaml

如您所见,这个应用在端口5000上响应。最后,我们还有我们的 Pod 级别卷定义,以匹配 Envoy 容器中挂载的 Envoy 配置卷。在创建部署之前,我们需要创建一个带有我们的 Envoy 配置的ConfigMap。我们可以使用以下命令来做到这一点:

kubectl create cm envoy-config 
--from-file=config.yaml=./envoy-config.yaml

这将导致以下输出:

Configmap "envoy-config" created

现在我们可以使用以下命令创建我们的部署:

kubectl apply -f deployment.yaml

这将导致以下输出:

Deployment "my-service" created

最后,我们需要我们的下游服务,service1service2。为此,我们将继续使用http-responder开源容器映像,在端口5000上进行响应。部署和服务规范可以在代码存储库中找到,并且我们可以使用以下命令创建它们:

kubectl create -f service1-deployment.yaml
kubectl create -f service1-service.yaml
kubectl create -f service2-deployment.yaml
kubectl create -f service2-service.yaml

现在,我们可以测试我们的 Envoy 配置!从我们的my-service容器中,我们可以向端口8080的本地主机发出请求,路径为/service1。这应该会指向我们的service1 Pod IP 之一。为了发出这个请求,我们使用以下命令:

Kubectl exec <my-service-pod-name> -it -- curl localhost:8080/service1

我们已经设置了我们的服务来在curl请求上回显它们的名称。看一下我们curl命令的以下输出:

Service 1 Reached!

现在我们已经看过了 Envoy 如何与静态配置一起工作,让我们转向基于 Envoy 的动态服务网格 - Istio。

在 Kubernetes 中添加服务网格

服务网格模式是侧车代理的逻辑扩展。通过将侧车代理附加到每个 Pod,服务网格可以控制服务之间的功能,如高级路由规则、重试和超时。此外,通过让每个请求通过代理,服务网格可以实现服务之间的相互 TLS 加密,以增加安全性,并且可以让管理员对集群中的请求有非常好的可观察性。

有几个支持 Kubernetes 的服务网格项目。最流行的如下:

  • Istio

  • Linkerd

  • Kuma

  • Consul

这些服务网格中的每一个对服务网格模式有不同的看法。Istio可能是最流行和最全面的解决方案,但也非常复杂。Linkerd也是一个成熟的项目,但更容易配置(尽管它使用自己的代理而不是 Envoy)。Consul是一个支持 Envoy 以及其他提供者的选项,不仅仅在 Kubernetes 上。最后,Kuma是一个基于 Envoy 的选项,也在不断增长。

探索所有选项超出了本书的范围,因此我们将坚持使用 Istio,因为它通常被认为是默认解决方案。也就是说,所有这些网格都有优势和劣势,在计划采用服务网格时值得看看每一个。

在 Kubernetes 上设置 Istio

虽然 Istio 可以使用 Helm 安装,但 Helm 安装选项不再是官方支持的安装方法。

相反,我们使用Istioctl CLI 工具将 Istio 与配置安装到我们的集群上。这个配置可以完全定制,但是为了本书的目的,我们将只使用"demo"配置:

  1. 在集群上安装 Istio 的第一步是安装 Istio CLI 工具。我们可以使用以下命令来完成这个操作,这将安装最新版本的 CLI 工具:
curl -L https://istio.io/downloadIstio | sh -
  1. 接下来,我们将希望将 CLI 工具添加到我们的路径中,以便使用:
cd istio-<VERSION>
export PATH=$PWD/bin:$PATH
  1. 现在,让我们安装 Istio!Istio 的配置被称为配置文件,如前所述,它们可以使用 YAML 文件进行完全定制。

对于这个演示,我们将使用内置的demo配置文件与 Istio 一起使用,这提供了一些基本设置。使用以下命令安装配置文件:

istioctl install --set profile=demo

这将导致以下输出:

图 14.3 - Istioctl 配置文件安装输出

图 14.3 - Istioctl 配置文件安装输出

  1. 由于截至 Kubernetes 1.19,sidecar 资源尚未发布,因此 Istio 本身将在任何打上istio-injection=enabled标签的命名空间中注入 Envoy 代理。

要为任何命名空间打上标签,请运行以下命令:

kubectl label namespace my-namespace istio-injection=enabled
  1. 为了方便测试,使用前面的label命令为default命名空间打上标签。一旦 Istio 组件启动,该命名空间中的任何 Pod 将自动注入 Envoy sidecar,就像我们在上一节中手动创建的那样。

要从集群中删除 Istio,请运行以下命令:

istioctl x uninstall --purge

这应该会出现一个确认消息,告诉您 Istio 已被移除。

  1. 现在,让我们部署一些东西来测试我们的新网格!我们将部署三种不同的应用服务,每个都有一个部署和一个服务资源:

a. 服务前端

b. 服务后端 A

c. 服务后端 B

这是服务前端的部署:

Istio-service-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-frontend
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: service-frontend
        version: v2
    spec:
      containers:
      - name: service-frontend
        image: ravirdv/http-responder:latest
        ports:
        - containerPort: 5000
          name: svc-port
          protocol: TCP

这是服务前端的服务:

Istio-service-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: service-frontend
spec:
  selector:
    name: service-frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000

服务后端 A 和 B 的 YAML 与服务前端相同,除了交换名称、镜像名称和选择器标签。

  1. 现在我们有了一些要路由到(和之间)的服务,让我们开始设置一些 Istio 资源!

首先,我们需要一个Gateway资源。在这种情况下,我们不使用 NGINX Ingress Controller,但这没关系,因为 Istio 提供了一个可以用于入口和出口的Gateway资源。以下是 IstioGateway定义的样子:

Istio-gateway.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: myapplication-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

这些Gateway定义看起来与入口记录非常相似。我们有nameselector,Istio 用它们来决定使用哪个 Istio Ingress Controller。接下来,我们有一个或多个服务器,它们实质上是我们网关上的入口点。在这种情况下,我们不限制主机,并且接受端口80上的请求。

  1. 现在我们有了一个用于将请求发送到我们的集群的网关,我们可以开始设置一些路由。我们在 Istio 中使用VirtualService来做到这一点。Istio 中的VirtualService是一组应该遵循的路由,当对特定主机名的请求时。此外,我们可以使用通配符主机来为网格中的任何地方的请求制定全局规则。让我们看一个示例VirtualService配置:

Istio-virtual-service-1.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapplication
spec:
  hosts:
  - "*"
  gateways:
  - myapplication-gateway
  http:
  - match:
    - uri:
        prefix: /app
    - uri:
        prefix: /frontend
    route:
    - destination:
        host: service-frontend
        subset: v1

在这个VirtualService中,如果匹配我们的uri前缀,我们将请求路由到任何主机到我们的入口点Service Frontend。在这种情况下,我们匹配前缀,但你也可以在 URI 匹配器中使用精确匹配,将prefix替换为exact

  1. 所以,现在我们有一个设置,与我们预期的 NGINX Ingress 非常相似,入口进入集群由路由匹配决定。

然而,在我们的路由中,v1是什么?这实际上代表了我们Frontend Service的一个版本。让我们继续使用一个新的资源类型 - Istio DestinationRule来指定这个版本。这是一个DestinationRule配置的样子:

Istio-destination-rule-1.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: service-frontend
spec:
  host: service-frontend
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

正如你所看到的,我们在 Istio 中指定了我们前端服务的两个不同版本,每个版本都查看一个标签选择器。从我们之前的部署和服务中,你可以看到我们当前的前端服务版本是v2,但我们也可以并行运行两者!通过在入口虚拟服务中指定我们的v2版本,我们告诉 Istio 将所有请求路由到服务的v2。此外,我们还配置了我们的v1版本,它在之前的VirtualService中被引用。这个硬规则只是在 Istio 中将请求路由到不同子集的一种可能的方式。

现在,我们已经成功通过网关将流量路由到我们的集群,并基于目标规则路由到虚拟服务子集。在这一点上,我们实际上已经“在”我们的服务网格中!

  1. 现在,从我们的Service Frontend,我们希望能够路由到Service Backend AService Backend B。我们该怎么做?更多的虚拟服务就是答案!让我们来看看Backend Service A的虚拟服务:

Istio-virtual-service-2.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapplication-a
spec:
  hosts:
  - service-a
  http:
    route:
    - destination:
        host: service-backend-a
        subset: v1

正如你所看到的,这个VirtualService路由到我们服务的v1子集,service-backend-a。我们还需要另一个VirtualService用于service-backend-b,我们不会完全包含(但看起来几乎相同)。要查看完整的 YAML,请检查istio-virtual-service-3.yaml的代码存储库。

  1. 一旦我们的虚拟服务准备好了,我们需要一些目标规则!Backend Service ADestinationRule如下:

Istio-destination-rule-2.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: service-backend-a
spec:
  host: service-backend-a
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
  subsets:
  - name: v1
    labels:
      version: v1

Backend Service BDestinationRule类似,只是有不同的子集。我们不会包含代码,但是在代码存储库中检查istio-destination-rule-3.yaml以获取确切的规格。

这些目标规则和虚拟服务相加,形成了以下路由图:

图 14.4 - Istio 路由图

图 14.4 - Istio 路由图

正如您所看到的,来自“前端服务”Pod 的请求可以路由到“后端服务 A 版本 1”或“后端服务 B 版本 3”,每个后端服务也可以相互路由。对后端服务 A 或 B 的这些请求还额外利用了 Istio 的最有价值的功能之一 - 双向 TLS。在这种设置中,网格中的任何两点之间都会自动保持 TLS 安全。

接下来,让我们看看如何在 Kubernetes 上使用无服务器模式。

在 Kubernetes 上实现无服务器

云提供商上的无服务器模式迅速变得越来越受欢迎。无服务器架构由可以自动扩展的计算组成,甚至可以扩展到零(即没有使用计算容量来提供函数或其他应用)。函数即服务(FaaS)是无服务器模式的扩展,其中函数代码是唯一的输入,无服务器系统负责根据需要路由请求到计算资源并进行扩展。AWS Lambda、Azure Functions 和 Google Cloud Run 是一些更受欢迎的 FaaS/无服务器选项,它们得到了云提供商的官方支持。Kubernetes 还有许多不同的无服务器框架和库,可以用于在 Kubernetes 上运行无服务器、扩展到零的工作负载以及 FaaS。其中一些最受欢迎的如下:

  • Knative

  • Kubeless

  • OpenFaaS

  • Fission

关于 Kubernetes 上所有无服务器选项的全面讨论超出了本书的范围,因此我们将专注于两种不同的选项,它们旨在满足两种完全不同的用例:OpenFaaS 和 Knative。

虽然 Knative 非常可扩展和可定制,但它使用了多个耦合的组件,增加了复杂性。这意味着需要一些额外的配置才能开始使用 FaaS 解决方案,因为函数只是 Knative 支持的许多其他模式之一。另一方面,OpenFaaS 使得在 Kubernetes 上轻松启动和运行无服务器和 FaaS 变得非常容易。这两种技术出于不同的原因都是有价值的。

在本章的教程中,我们将看看 Knative,这是最流行的无服务器框架之一,也支持通过其事件功能的 FaaS。

在 Kubernetes 上使用 Knative 进行 FaaS

如前所述,Knative 是用于 Kubernetes 上无服务器模式的模块化构建块。因此,在我们实际使用函数之前,它需要一些配置。Knative 也可以与 Istio 一起安装,它用作路由和扩展无服务器应用程序的基础。还有其他非 Istio 路由选项可用。

使用 Knative 进行 FaaS,我们需要安装Knative ServingKnative Eventing。Knative Serving 将允许我们运行无服务器工作负载,而 Knative Eventing 将提供通道来向这些规模为零的工作负载发出 FaaS 请求。让我们按照以下步骤来完成这个过程:

  1. 首先,让我们安装 Knative Serving 组件。我们将从安装 CRDs 开始:
kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-crds.yaml
  1. 接下来,我们可以安装服务组件本身:
kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-core.yaml
  1. 此时,我们需要安装一个网络/路由层供 Knative 使用。让我们使用 Istio:
kubectl apply --filename https://github.com/knative/net-istio/releases/download/v0.18.0/release.yaml
  1. 我们需要从 Istio 获取网关 IP 地址。根据您运行的位置(换句话说,是在 AWS 还是本地),此值可能会有所不同。使用以下命令获取它:
Kubectl get service -n istio-system istio-ingressgateway
  1. Knative 需要特定的 DNS 设置来启用服务组件。在云设置中最简单的方法是使用xip.io的“Magic DNS”,尽管这对基于 Minikube 的集群不起作用。如果您正在运行其中之一(或者只是想查看所有可用选项),请查看Knative 文档

要设置 Magic DNS,请使用以下命令:

kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-default-domain.yaml
  1. 现在我们已经安装了 Knative Serving,让我们安装 Knative Eventing 来处理我们的 FaaS 请求。首先,我们需要更多的 CRDs。使用以下命令安装它们:
kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/eventing-crds.yaml
  1. 现在,安装事件组件,就像我们安装服务一样:
kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/eventing-core.yaml

在这一点上,我们需要为我们的事件系统添加一个队列/消息层来使用。我们是否提到 Knative 支持许多模块化组件?

重要提示

为了简化事情,让我们只使用基本的内存消息层,但了解所有可用选项对您也是有好处的。关于消息通道的模块化选项,请查看knative.dev/docs/eventing/channels/channels-crds/上的文档。对于事件源选项,您可以查看knative.dev/docs/eventing/sources/

  1. 安装in-memory消息层,请使用以下命令:
kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/in-memory-channel.yaml
  1. 以为我们已经完成了?不!还有最后一件事。我们需要安装一个 broker,它将从消息层获取事件并将它们处理到正确的位置。让我们使用默认的 broker 层,MT-Channel broker 层。您可以使用以下命令安装它:
kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/mt-channel-broker.yaml

到此为止,我们终于完成了。我们通过 Knative 安装了一个端到端的 FaaS 实现。正如你所看到的,这并不是一项容易的任务。Knative 令人惊奇的地方与令人头疼的地方是一样的——它提供了许多不同的模块选项和配置,即使在每个步骤选择了最基本的选项,我们仍然花了很多时间来解释安装过程。还有其他可用的选项,比如 OpenFaaS,它们更容易上手,我们将在下一节中进行探讨!然而,在 Knative 方面,现在我们的设置终于准备好了,我们可以添加我们的 FaaS。

在 Knative 中实现 FaaS 模式

现在我们已经设置好了 Knative,我们可以使用它来实现一个 FaaS 模式,其中事件将通过触发器触发在 Knative 中运行的一些代码。要设置一个简单的 FaaS,我们将需要三样东西:

  • 一个从入口点路由我们的事件的 broker

  • 一个消费者服务来实际处理我们的事件

  • 一个指定何时将事件路由到消费者进行处理的触发器定义

首先,我们需要创建我们的 broker。这很简单,类似于创建入口记录或网关。我们的broker YAML 如下所示:

Knative-broker.yaml:

apiVersion: eventing.knative.dev/v1
kind: broker
metadata:
 name: my-broker
 namespace: default

接下来,我们可以创建一个消费者服务。这个组件实际上就是我们的应用程序,它将处理事件——我们的函数本身!我们不打算向你展示比你已经看到的更多的 YAML,让我们假设我们的消费者服务只是一个名为service-consumer的普通的 Kubernetes 服务,它路由到一个运行我们应用程序的四个副本的 Pod 部署。

最后,我们需要一个触发器。这决定了如何以及哪些事件将从 broker 路由。触发器的 YAML 如下所示:

Knative-trigger.yaml:

apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: my-trigger
spec:
  broker: my-broker
  filter:
    attributes:
      type: myeventtype
  subscriber:
    ref:
     apiVersion: v1
     kind: Service
     name: service-consumer

在这个 YAML 中,我们创建了一个 Trigger 规则,任何通过我们的经纪人 my-broker 并且类型为 myeventtype 的事件将自动路由到我们的消费者 service-consumer。有关 Knative 中触发器过滤器的完整文档,请查看 knative.dev/development/eventing/triggers/ 上的文档。

那么,我们如何创建一些事件呢?首先,使用以下命令检查经纪人 URL:

kubectl get broker

这应该会产生以下输出:

NAME      READY   REASON   URL                                                                                 AGE
my-broker   True             http://broker-ingress.knative-eventing.svc.cluster.local/default/my-broker     1m

现在我们终于可以测试我们的 FaaS 解决方案了。让我们快速启动一个 Pod,从中我们可以向我们的触发器发出请求:

kubectl run -i --tty --rm debug --image=radial/busyboxplus:curl --restart=Never -- sh

现在,从这个 Pod 内部,我们可以继续测试我们的触发器,使用 curl。我们需要发出的请求需要有一个等于 myeventtypeCe-Type 标头,因为这是我们触发器所需的。Knative 使用形式为 Ce-IdCe-Type 的标头,如下面的代码块所示,来进行路由。

curl 请求将如下所示:

curl -v "http://broker-ingress.knative-eventing.svc.cluster.local/default/my-broker" \
  -X POST \
  -H "Ce-Id: anyid" \
  -H "Ce-Specversion: 1.0" \
  -H "Ce-Type: myeventtype" \
  -H "Ce-Source: any" \
  -H "Content-Type: application/json" \
  -d '{"payload":"Does this work?"}'

正如您所看到的,我们正在向经纪人 URL 发送 curl http 请求。此外,我们还在 HTTP 请求中传递了一些特殊的标头。重要的是,我们传递了 type=myeventtype,这是我们触发器上的过滤器所需的,以便发送请求进行处理。

在这个例子中,我们的消费者服务会回显请求的 JSON 主体的 payload 键,以及一个 200 的 HTTP 响应,因此运行这个 curl 请求会给我们以下结果:

> HTTP/1.1 200 OK
> Content-Type: application/json
{
  "Output": "Does this work?"
}

成功!我们已经测试了我们的 FaaS,并且它返回了我们期望的结果。从这里开始,我们的解决方案将根据事件的数量进行零扩展和缩减,与 Knative 的所有内容一样,还有许多自定义和配置选项,可以精确地调整我们的解决方案以满足我们的需求。

接下来,我们将使用 OpenFaaS 而不是 Knative 来查看相同的模式,以突出两种方法之间的区别。

在 Kubernetes 上使用 OpenFaaS 进行 FaaS

现在我们已经讨论了如何开始使用 Knative,让我们用 OpenFaaS 做同样的事情。首先,要安装 OpenFaaS 本身,我们将使用来自 faas-netes 仓库的 Helm 图表,该仓库位于 github.com/openfaas/faas-netes

使用 Helm 安装 OpenFaaS 组件

首先,我们将创建两个命名空间来保存我们的 OpenFaaS 组件:

  • openfaas 用于保存 OpenFaas 的实际服务组件

  • openfaas-fn 用于保存我们部署的函数

我们可以使用以下命令使用faas-netes存储库中的一个巧妙的 YAML 文件来添加这两个命名空间:

kubectl apply -f https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.yml

接下来,我们需要使用以下 Helm 命令添加faas-netes Helm 存储库

helm repo add openfaas https://openfaas.github.io/faas-netes/
helm repo update

最后,我们实际部署 OpenFaaS!

在前面的faas-netes存储库中的 OpenFaaS 的 Helm 图表有几个可能的变量,但我们将使用以下配置来确保创建一组初始的身份验证凭据,并部署入口记录:

helm install openfaas openfaas/openfaas \
    --namespace openfaas  \
    --set functionNamespace=openfaas-fn \
    --set ingress.enabled=true \
    --set generateBasicAuth=true 

现在,我们的 OpenFaaS 基础设施已经部署到我们的集群中,我们将希望获取作为 Helm 安装的一部分生成的凭据。Helm 图表将作为钩子的一部分创建这些凭据,并将它们存储在一个秘密中,因此我们可以通过运行以下命令来获取它们:

OPENFAASPWD=$(kubectl get secret basic-auth -n openfaas -o jsonpath="{.data.basic-auth-password}" | base64 --decode)

这就是我们需要的所有 Kubernetes 设置!

接下来,让我们安装 OpenFaas CLI,这将使管理 OpenFaas 函数变得非常容易。

安装 OpenFaaS CLI 和部署函数

要安装 OpenFaaS CLI,我们可以使用以下命令(对于 Windows,请查看前面的 OpenFaaS 文档):

curl -sL https://cli.openfaas.com | sudo sh

现在,我们可以开始构建和部署一些函数。这最容易通过 CLI 来完成。

在构建和部署 OpenFaaS 的函数时,OpenFaaS CLI 提供了一种简单的方法来生成样板,并为特定语言构建和部署函数。它通过“模板”来实现这一点,并支持各种类型的 Node、Python 等。有关模板类型的完整列表,请查看github.com/openfaas/templates上的模板存储库。

使用 OpenFaaS CLI 创建的模板类似于您从 AWS Lambda 等托管无服务器平台期望的内容。让我们使用以下命令创建一个全新的 Node.js 函数:

faas-cli new my-function –lang node

这将产生以下输出:

Folder: my-function created.
Function created in folder: my-function
Stack file written: my-function.yml

正如你所看到的,new命令生成一个文件夹,在其中有一些函数代码本身的样板,以及一个 OpenFaaS YAML 文件。

OpenFaaS YAML 文件将如下所示:

My-function.yml:

provider:
  name: openfaas
  gateway: http://localhost:8080
functions:
  my-function:
    lang: node
    handler: ./my-function
    image: my-function

实际的函数代码(在my-function文件夹中)包括一个函数文件handler.js和一个依赖清单package.json。对于其他语言,这些文件将是不同的,我们不会深入讨论 Node 中的具体依赖。但是,我们将编辑handler.js文件以返回一些文本。编辑后的文件如下所示:

Handler.js:

"use strict"
module.exports = (context, callback) => {
    callback(undefined, {output: "my function succeeded!"});
}

这段 JavaScript 代码将返回一个包含我们文本的 JSON 响应。

现在我们有了我们的函数和处理程序,我们可以继续构建和部署我们的函数。OpenFaaS CLI 使构建函数变得简单,我们可以使用以下命令来完成:

faas-cli build -f /path/to/my-function.yml 

该命令的输出很长,但当完成时,我们将在本地构建一个新的容器映像,其中包含我们的函数处理程序和依赖项!

接下来,我们将像对待任何其他容器一样,将我们的容器映像推送到容器存储库。OpenFaaS CLI 具有一个很好的包装命令,可以将映像推送到 Docker Hub 或其他容器映像存储库:

faas-cli push -f my-function.yml 

现在,我们可以将我们的函数部署到 OpenFaaS。再次,这由 CLI 轻松完成。使用以下命令进行部署:

faas-cli deploy -f my-function.yml

现在,一切都已准备好让我们测试在 OpenFaaS 上部署的函数了!我们在部署 OpenFaaS 时使用了一个入口设置,以便请求可以通过该入口。但是,我们新函数生成的 YAML 文件设置为在开发目的地对localhost:8080进行请求。我们可以编辑该文件以将请求发送到我们入口网关的正确URL(请参阅docs.openfaas.com/deployment/kubernetes/中的文档),但相反,让我们通过快捷方式在本地主机上打开 OpenFaaS。

让我们使用kubectl port-forward命令在本地主机端口8080上打开我们的 OpenFaaS 服务。我们可以按照以下方式进行:

export OPENFAAS_URL=http://127.0.0.1:8080
kubectl port-forward -n openfaas svc/gateway 8080:8080

现在,让我们按照以下方式将先前生成的 auth 凭据添加到 OpenFaaS CLI 中:

echo -n $OPENFAASPWD | faas-cli login -g $OPENFAAS_URL -u admin --password-stdin

最后,为了测试我们的函数,我们只需运行以下命令:

faas-cli invoke -f my-function.yml my-function

这将产生以下输出:

Reading from STDIN - hit (Control + D) to stop.
This is my message
{ output: "my function succeeded!"});}

如您所见,我们成功收到了我们预期的响应!

最后,如果我们想要删除这个特定的函数,我们可以使用以下命令,类似于我们使用kubectl delete -f的方式:

faas-cli rm -f my-function.yml 

就是这样!我们的函数已被删除。

总结

在本章中,我们学习了关于 Kubernetes 上的服务网格和无服务器模式。为了为这些做好准备,我们首先讨论了在 Kubernetes 上运行边车代理,特别是使用 Envoy 代理。

然后,我们转向服务网格,并学习了如何安装和配置 Istio 服务网格,以实现服务到服务的互相 TLS 路由。

最后,我们转向了在 Kubernetes 上的无服务器模式,您将学习如何配置和安装 Knative,以及另一种选择 OpenFaaS,用于 Kubernetes 上的无服务器事件和 FaaS。

本章中使用的技能将帮助您在 Kubernetes 上构建服务网格和无服务器模式,为您提供完全自动化的服务发现和 FaaS 事件。

在下一章(也是最后一章)中,我们将讨论在 Kubernetes 上运行有状态应用程序。

问题

  1. 静态和动态 Envoy 配置有什么区别?

  2. Envoy 配置的四个主要部分是什么?

  3. Knative 的一些缺点是什么,OpenFaaS 又如何比较?

进一步阅读

第十五章:Kubernetes 上的有状态工作负载

本章详细介绍了在数据库中运行有状态工作负载时行业的当前状态。我们将讨论在 Kubernetes 上运行数据库、存储和队列时使用 Kubernetes(和流行的开源项目)。案例研究教程将包括在 Kubernetes 上运行对象存储、数据库和队列系统。

在本章中,我们将首先了解有状态应用在 Kubernetes 上的运行方式,然后学习如何使用 Kubernetes 存储来支持有状态应用。然后,我们将学习如何在 Kubernetes 上运行数据库,并涵盖消息传递和队列。让我们从讨论为什么有状态应用在 Kubernetes 上比无状态应用复杂得多开始。

在本章中,我们将涵盖以下主题:

  • 在 Kubernetes 上理解有状态应用

  • 使用 Kubernetes 存储支持有状态应用

  • 在 Kubernetes 上运行数据库

  • 在 Kubernetes 上实现消息传递和队列

技术要求

为了运行本章中详细介绍的命令,您需要一台支持kubectl命令行工具以及一个正常运行的 Kubernetes 集群的计算机。请参阅第一章与 Kubernetes 通信,了解快速启动和安装 kubectl 工具的几种方法。

本章中使用的代码可以在该书的 GitHub 存储库中找到:

github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter15

在 Kubernetes 上理解有状态应用

Kubernetes 为运行无状态和有状态应用提供了出色的基元,但有状态工作负载在 Kubernetes 上的成熟度要花更长时间。然而,近年来,一些基于 Kubernetes 的高调有状态应用框架和项目已经证明了 Kubernetes 上有状态应用日益成熟。让我们首先回顾其中一些,以便为本章的其余部分做铺垫。

流行的 Kubernetes 原生有状态应用

有许多类型的有状态应用程序。虽然大多数应用程序都是有状态的,但只有其中某些组件存储状态数据。我们可以从应用程序中移除这些特定的有状态组件,并专注于我们审查的这些组件。在本书中,我们将讨论数据库、队列和对象存储,略过像我们在第七章中审查的持久存储组件,Kubernetes 上的存储。我们还将介绍一些不太通用的组件作为荣誉提及。让我们从数据库开始!

与 Kubernetes 兼容的数据库

除了典型的数据库DBs)和可以使用 StatefulSets 或社区操作员部署在 Kubernetes 上的键值存储,如PostgresMySQLRedis,还有一些专为 Kubernetes 设计的重要选项:

  • CockroachDB:可以无缝部署在 Kubernetes 上的分布式 SQL 数据库

  • Vitess:一个 MySQL 分片编排器,允许 MySQL 全球可扩展性,也可以通过操作员在 Kubernetes 上安装

  • YugabyteDB:类似于CockroachDB的分布式 SQL 数据库,还支持类似 Cassandra 的查询

接下来,让我们看看 Kubernetes 上的排队和消息传递。

Kubernetes 上的队列、流和消息传递

同样,有一些行业标准选项,如KafkaRabbitMQ,可以使用社区 Helm 图表和操作员部署在 Kubernetes 上,另外还有一些专门开源和闭源选项:

  • NATS:开源消息传递和流系统

  • KubeMQ:Kubernetes 本地消息代理

接下来,让我们看看 Kubernetes 上的对象存储。

Kubernetes 上的对象存储

对象存储从 Kubernetes 获取基于卷的持久存储,并添加一个对象存储层,类似于(在许多情况下与 Amazon S3 的 API 兼容):

  • Minio:为高性能而构建的与 S3 兼容的对象存储。

  • Open IO:类似于Minio,具有高性能并支持 S3 和 Swift 存储。

接下来,让我们看看一些荣誉提及。

荣誉提及

除了前述的通用组件,还有一些更专业(但仍然分类的)有状态应用程序可以在 Kubernetes 上运行:

  • 密钥和认证管理VaultKeycloak

  • 容器注册表HarborDragonflyQuay

  • 工作流管理:带有 Kubernetes 操作员的Apache Airflow

既然我们已经回顾了一些有状态应用程序的类别,让我们谈谈这些状态密集型应用程序在 Kubernetes 上通常是如何实现的。

了解在 Kubernetes 上运行有状态应用程序的策略

虽然使用 ReplicaSet 或 Deployment 在 Kubernetes 上部署有状态应用程序并没有本质上的问题,但你会发现大多数在 Kubernetes 上的有状态应用程序使用 StatefulSets。我们在第四章中讨论了 StatefulSets,扩展和部署您的应用程序,但为什么它们对应用程序如此有用?我们将在本章中回顾并回答这个问题。

主要原因是 Pod 身份。许多分布式有状态应用程序都有自己的集群机制或共识算法。为了简化这些类型应用程序的流程,StatefulSets 提供了基于顺序系统的静态 Pod 命名,从0n。这个特性,再加上滚动更新和创建方法,使得应用程序更容易集群化,这对于像 CockroachDB 这样的云原生数据库非常重要。

为了说明 StatefulSets 如何帮助在 Kubernetes 上运行有状态应用程序,让我们看看如何使用 StatefulSets 在 Kubernetes 上运行 MySQL。

现在,要明确一点,在 Kubernetes 上运行单个 MySQL Pod 非常简单。我们只需要找到一个 MySQL 容器镜像,并确保它具有适当的配置和startup命令。

然而,当我们试图扩展我们的数据库时,我们开始遇到问题。与简单的无状态应用程序不同,我们可以在不创建新状态的情况下扩展我们的部署,MySQL(像许多其他数据库一样)有自己的集群和共识方法。MySQL 集群的每个成员都知道其他成员,最重要的是,它知道集群的领导者是哪个成员。这就是像 MySQL 这样的数据库可以提供一致性保证和原子性、一致性、隔离性、持久性ACID)合规性的方式。

因此,由于 MySQL 集群中的每个成员都需要知道其他成员(最重要的是主节点),我们需要以一种方式运行我们的 DB Pods,以便它们有一个共同的方式来找到并与 DB 集群的其他成员进行通信。

StatefulSets 提供这种功能的方式,正如我们在本节开头提到的,是通过 Pod 的序数编号。这样,运行在 Kubernetes 上需要自我集群的应用程序就知道,将使用从0n开始的常见命名方案。此外,当特定序数的 Pod 重新启动时,例如mysql-pod-2,相同的 PersistentVolume 将被挂载到在该序数位置启动的新 Pod 上。这允许在 StatefulSet 中单个 Pod 重新启动时保持状态的一致性,这使得应用程序更容易形成稳定的集群。

为了看到这在实践中是如何工作的,让我们看一下 MySQL 的 StatefulSet 规范。

在 StatefulSets 上运行 MySQL

以下的 YAML 规范是从 Kubernetes 文档版本中调整的。它展示了我们如何在 StatefulSets 上运行 MySQL 集群。我们将分别审查 YAML 规范的每个部分,以便我们可以准确理解这些机制如何与 StatefulSet 的保证相互作用。

让我们从规范的第一部分开始:

statefulset-mysql.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql

正如你所看到的,我们将创建一个具有三个replicas的 MySQL 集群。

这段内容没有太多其他令人兴奋的地方,所以让我们继续讨论initContainers的开始。在initContainers和常规容器之间,将有相当多的容器在此 Pod 中运行,因此我们将分别解释每个容器。接下来是第一个initContainer实例:

    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - |
          set -ex
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/master.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/slave.cnf /mnt/conf.d/
          fi
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map

这个第一个initContainer,正如你所看到的,是 MySQL 容器镜像。现在,这并不意味着我们不会在 Pod 中持续运行 MySQL 容器。这是一个你会经常在复杂应用中看到的模式。有时相同的容器镜像既用作initContainer实例,又用作 Pod 中正常运行的容器。这是因为该容器具有正确的嵌入式脚本和工具,可以以编程方式执行常见的设置任务。

在这个例子中,MySQL 的initContainer创建一个文件/mnt/conf.d/server-id.cnf,并向文件中添加一个server ID,该 ID 对应于 StatefulSet 中 Pod 的ordinal ID。在写入ordinal ID 时,它添加了100作为偏移量,以避免 MySQL 中server-id ID 的保留值为0

然后,根据 Pod 的ordinal D 是否为0,它将配置复制到卷中,用于主 MySQL 服务器或从 MySQL 服务器。

接下来,让我们看一下下一节中的第二个initContainer(为了简洁起见,我们省略了一些卷挂载信息的代码,但完整的代码可以在本书的 GitHub 存储库中找到):

      - name: clone-mysql
        image: gcr.io/google-samples/xtrabackup:1.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          [[ $ordinal -eq 0 ]] && exit 0          ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
          xtrabackup --prepare --target-dir=/var/lib/mysql

正如你所看到的,这个initContainer根本不是 MySQL!相反,容器镜像是一个叫做 Xtra Backup 的工具。为什么我们需要这个容器呢?

考虑这样一种情况:一个全新的 Pod,带有全新的空的持久卷加入了集群。在这种情况下,数据复制过程将需要通过从 MySQL 集群中的其他成员进行复制来复制所有数据。对于大型数据库来说,这个过程可能会非常缓慢。

因此,我们有一个initContainer实例,它从 StatefulSet 中的另一个 MySQL Pod 中加载数据,以便 MySQL 的数据复制功能有一些数据可供使用。如果 MySQL Pod 中已经有数据,则不会加载数据。[[ -d /var/lib/mysql/mysql ]] && exit 0这一行是用来检查是否存在现有数据的。

一旦这两个initContainer实例成功完成了它们的任务,我们就有了所有 MySQL 配置,这得益于第一个initContainer,并且我们从 MySQL StatefulSet 的另一个成员那里得到了一组相对较新的数据。

现在,让我们继续讨论 StatefulSet 定义中的实际容器,从 MySQL 本身开始:

      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ALLOW_EMPTY_PASSWORD
          value: "1"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d

正如你所看到的,这个 MySQL 容器设置相当基本。除了环境变量外,我们还挂载了之前创建的配置。这个 Pod 还有一些存活和就绪探针配置 - 请查看本书的 GitHub 存储库了解详情。

现在,让我们继续查看我们的最终容器,这看起来很熟悉 - 实际上是另一个 Xtra Backup 的实例!让我们看看它是如何配置的:

- name: xtrabackup
containerPort: 3307
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; thencat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.inrm -f xtrabackup_slave_info xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm -f xtrabackup_binlog_info xtrabackup_slave_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi if [[ -f change_master_to.sql.in ]]; then
echo "Waiting for mysqld to be ready (accepting connections)"
until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
mysql -h 127.0.0.1 \
-e "$(<change_master_to.sql.in), \
MASTER_HOST='mysql-0.mysql', \
MASTER_USER='root', \
MASTER_PASSWORD='', \
MASTER_CONNECT_RETRY=10; \
START SLAVE;" || exit 1
mv change_master_to.sql.in change_master_to.sql.orig
fi exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
"xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"

这个容器设置有点复杂,所以让我们逐节审查一下。

我们从initContainers知道,Xtra Backup 会从 StatefulSet 中的另一个 Pod 中加载数据,以便为复制到 StatefulSet 中的其他成员做好准备。

在这种情况下,Xtra Backup 容器实际上启动了复制!这个容器首先会检查它所在的 Pod 是否应该是 MySQL 集群中的从属 Pod。如果是,它将从主节点开始数据复制过程。

最后,Xtra Backup 容器还将在端口3307上打开一个监听器,如果请求,将向 Pod 中的数据发送一个克隆。这是在其他 StatefulSet 中的 Pod 请求克隆时发送克隆数据的设置。请记住,第一个initContainer查看 StatefulSet 中的其他 Pod,以便获取克隆。最后,StatefulSet 中的每个 Pod 都能够请求克隆,以及运行一个可以向其他 Pod 发送数据克隆的进程。

最后,让我们来看一下volumeClaimTemplate。规范的这一部分还列出了先前容器的卷挂载和 Pod 的卷设置(但出于简洁起见,我们将其省略。请查看本书的 GitHub 存储库以获取其余部分):

  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

正如您所看到的,最后一个容器或卷列表的卷设置并没有什么特别有趣的地方。然而,值得注意的是volumeClaimTemplates部分,因为只要 Pod 在相同的序数位置重新启动,数据就会保持不变。集群中添加的新 Pod 将以空白的 PersistentVolume 开始,这将触发初始数据克隆。

所有这些 StatefulSets 的特性,再加上 Pods 和工具的正确配置,都可以让在 Kubernetes 上轻松扩展有状态的数据库。

现在我们已经讨论了为什么有状态的 Kubernetes 应用程序可能会使用 StatefulSets,让我们继续实施一些来证明它!我们将从一个对象存储应用程序开始。

在 Kubernetes 上部署对象存储

对象存储与文件系统或块存储不同。它提供了一个封装文件的更高级抽象,给它一个标识符,并经常包括版本控制。然后可以通过其特定标识符访问文件。

最流行的对象存储服务可能是 AWS S3,但 Azure Blob Storage 和 Google Cloud Storage 是类似的替代方案。此外,还有几种可以在 Kubernetes 上运行的自托管对象存储技术,我们在上一节中进行了审查。

在本书中,我们将回顾在 Kubernetes 上配置和使用 Minio。Minio 是一个强调高性能的对象存储引擎,可以部署在 Kubernetes 上,除了其他编排技术,如 Docker Swarm 和 Docker Compose。

Minio 支持使用运算符和 Helm 图表进行 Kubernetes 部署。在本书中,我们将专注于运算符,但有关 Helm 图表的更多信息,请查看 Minio 文档docs.min.io/docs。让我们开始使用 Minio Operator,这将让我们审查一些很酷的社区扩展到 kubectl。

安装 Minio Operator

安装 Minio Operator 将与我们迄今为止所做的任何事情都大不相同。实际上,Minio 提供了一个kubectl插件,以便管理运算符和整个 Minio 的安装和配置。

在本书中,我们并没有多谈kubectl插件,但它们是 Kubernetes 生态系统中不断增长的一部分。kubectl插件可以提供额外的功能,以新的kubectl命令的形式。

为了安装minio kubectl 插件,我们使用 Krew,这是一个kubectl的插件管理器,可以通过一个命令轻松搜索和添加kubectl插件。

安装 Krew 和 Minio kubectl 插件

所以首先,让我们安装 Krew。安装过程取决于您的操作系统和环境,但对于 macOS,它看起来像以下内容(查看Krew 文档以获取更多信息):

  1. 首先,让我们使用以下终端命令安装 Krew CLI 工具:
(
  set -x; cd "$(mktemp -d)" &&
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz" &&
  tar zxvf krew.tar.gz &&
  KREW=./krew-"$(uname | tr '[:upper:]' '[:lower:]')_$(uname -m | sed -e 's/x86_64/amd64/' -e 's/arm.*$/arm/')" &&
  "$KREW" install krew
)
  1. 现在,我们可以使用以下命令将 Krew 添加到我们的PATH变量中:
export PATH="${KREW_ROOT:-$HOME/.krechw}/bin:$PATH"

在新的 shell 中,我们现在可以开始使用 Krew!Krew 可以使用kubectl krew命令访问。

  1. 要安装 Minio kubectl 插件,您可以运行以下krew命令:
kubectl krew install minio

现在,安装了 Minio kubectl 插件,让我们看看如何在我们的集群上设置 Minio。

启动 Minio Operator

首先,我们需要在我们的集群上安装 Minio Operator。这个部署将控制我们以后需要做的所有 Minio 任务:

  1. 我们可以使用以下命令安装 Minio Operator:
kubectl minio init

这将导致以下输出:

CustomResourceDefinition tenants.minio.min.io: created
ClusterRole minio-operator-role: created
ServiceAccount minio-operator: created
ClusterRoleBinding minio-operator-binding: created
MinIO Operator Deployment minio-operator: created
  1. 要检查 Minio Operator 是否准备就绪,让我们用以下命令检查我们的 Pods:
kubectl get pods

您应该在输出中看到 Minio Operator Pod 正在运行:

NAMESPACE     NAME                               READY   STATUS    RESTARTS   AGE
default       minio-operator-85ccdcfb6-r8g8b     1/1     Running   0          5m37s

现在,我们在 Kubernetes 上正确运行 Minio Operator。接下来,我们可以创建一个 Minio 租户。

创建一个 Minio 租户

下一步是创建一个租户。由于 Minio 是一个多租户系统,每个租户都有自己的命名空间,用于存储桶和对象,另外还有单独的持久卷。此外,Minio Operator 以高可用设置和数据复制的方式启动 Minio 分布式模式。

在创建 Minio 租户之前,我们需要为 Minio 安装一个容器存储接口CSI)驱动程序。CSI 是存储提供商和容器之间的标准化接口方式,Kubernetes 实现了 CSI,以允许第三方存储提供商编写自己的驱动程序,以便无缝集成到 Kubernetes 中。Minio 建议使用 Direct CSI 驱动程序来管理 Minio 的持久卷。

要安装 Direct CSI 驱动程序,我们需要使用 Kustomize 运行kubectl apply命令。然而,Direct CSI 驱动程序的安装需要设置一些环境变量,以便根据正确的配置创建 Direct CSI 配置,如下所示:

  1. 首先,让我们根据 Minio 的建议来创建这个环境文件:

默认.env

DIRECT_CSI_DRIVES=data{1...4}
DIRECT_CSI_DRIVES_DIR=/mnt
KUBELET_DIR_PATH=/var/lib/kubelet

正如你所看到的,这个环境文件确定了 Direct CSI 驱动程序将挂载卷的位置。

  1. 一旦我们创建了default.env,让我们使用以下命令将这些变量加载到内存中:
export $(cat default.env)
  1. 最后,让我们使用以下命令安装 Direct CSI 驱动程序:
kubectl apply -k github.com/minio/direct-csi

这应该会产生以下输出:

kubenamespace/direct-csi created
storageclass.storage.k8s.io/direct.csi.min.io created
serviceaccount/direct-csi-min-io created
clusterrole.rbac.authorization.k8s.io/direct-csi-min-io created
clusterrolebinding.rbac.authorization.k8s.io/direct-csi-min-io created
configmap/direct-csi-config created
secret/direct-csi-min-io created
service/direct-csi-min-io created
deployment.apps/direct-csi-controller-min-io created
daemonset.apps/direct-csi-min-io created
csidriver.storage.k8s.io/direct.csi.min.io created
  1. 在继续创建 Minio 租户之前,让我们检查一下我们的 CSI Pods 是否已经正确启动。运行以下命令进行检查:
kubectl get pods –n direct-csi

如果 CSI Pods 已经启动,你应该会看到类似以下的输出:

NAME                                          READY   STATUS    RESTARTS   AGE
direct-csi-controller-min-io-cd598c4b-hn9ww   2/2     Running   0          9m
direct-csi-controller-min-io-cd598c4b-knvbn   2/2     Running   0          9m
direct-csi-controller-min-io-cd598c4b-tth6q   2/2     Running   0          9m
direct-csi-min-io-4qlt7                       3/3     Running   0          9m
direct-csi-min-io-kt7bw                       3/3     Running   0          9m
direct-csi-min-io-vzdkv                       3/3     Running   0          9m
  1. 现在我们的 CSI 驱动程序已安装,让我们创建 Minio 租户 - 但首先,让我们看一下kubectl minio tenant create命令生成的 YAML:
kubectl minio tenant create --name my-tenant --servers 2 --volumes 4 --capacity 1Gi -o > my-minio-tenant.yaml

如果你想直接创建 Minio 租户而不检查 YAML,可以使用以下命令:

kubectl minio tenant create --name my-tenant --servers 2 --volumes 4 --capacity 1Gi

这个命令只会创建租户,而不会先显示 YAML。然而,由于我们使用的是 Direct CSI 实现,我们需要更新 YAML。因此,仅使用命令是行不通的。现在让我们来看一下生成的 YAML 文件。

出于空间原因,我们不会完整查看文件,但让我们看一下Tenant自定义资源定义CRD)的一些部分,Minio Operator 将使用它来创建托管我们的 Minio 租户所需的资源。首先,让我们看一下规范的上部分,应该是这样的:

my-minio-tenant.yaml

apiVersion: minio.min.io/v1
kind: Tenant
metadata:
  creationTimestamp: null
  name: my-tenant
  namespace: default
scheduler:
  name: ""
spec:
  certConfig:
    commonName: ""
    organizationName: []
    dnsNames: []
  console:
    consoleSecret:
      name: my-tenant-console-secret
    image: minio/console:v0.3.14
    metadata:
      creationTimestamp: null
      name: my-tenant
    replicas: 2
    resources: {}
  credsSecret:
    name: my-tenant-creds-secret
  image: minio/minio:RELEASE.2020-09-26T03-44-56Z
  imagePullSecret: {}

正如您所看到的,此文件指定了Tenant CRD 的一个实例。我们的规范的第一部分指定了两个容器,一个用于 Minio 控制台,另一个用于 Minio server本身。此外,replicas值反映了我们在kubectl minio tenant create命令中指定的内容。最后,它指定了 Minioconsole的秘钥的名称。

接下来,让我们看一下 Tenant CRD 的底部部分:

 liveness:
    initialDelaySeconds: 10
    periodSeconds: 1
    timeoutSeconds: 1
  mountPath: /export
  requestAutoCert: true
  zones:
  - resources: {}
    servers: 2
    volumeClaimTemplate:
      apiVersion: v1
      kind: persistentvolumeclaims
      metadata:
        creationTimestamp: null
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 256Mi
      status: {}
    volumesPerServer: 2
status:
  availableReplicas: 0
  currentState: ""

正如您所看到的,Tenant资源指定了一些服务器(也由creation命令指定),与副本的数量相匹配。它还指定了内部 Minio 服务的名称,以及要使用的volumeClaimTemplate实例。

然而,这个规范对我们的目的不起作用,因为我们正在使用 Direct CSI。让我们使用一个使用 Direct CSI 的新volumeClaimTemplate来更新zones密钥,如下所示(将此文件保存为my-updated-minio-tenant.yaml)。这里只是该文件的zones部分,我们已经更新了:

my-updated-minio-tenant.yaml

zones:
  - resources: {}
    servers: 2
    volumeClaimTemplate:
      metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 256Mi
        storageClassName: direct.csi.min.io
  1. 现在让我们继续创建我们的 Minio 租户!我们可以使用以下命令来完成:
kubectl apply -f my-updated-minio-tenant.yaml

这应该导致以下输出:

tenant.minio.min.io/my-tenant created
secret/my-tenant-creds-secret created
secret/my-tenant-console-secret created

此时,Minio Operator 将开始为我们的新 Minio 租户创建必要的资源,几分钟后,除了运算符之外,您应该看到一些 Pods 启动,类似于以下内容:

图 15.1 – Minio Pods 输出

图 15.1 – Minio Pods 输出

现在我们的 Minio 租户已经完全运行起来了!接下来,让我们看一下 Minio 控制台,看看我们的租户是什么样子的。

访问 Minio 控制台

首先,为了获取控制台的登录信息,我们需要获取两个密钥的内容,这些密钥保存在自动生成的<TENANT NAME>-console-secret秘钥中。

为了获取控制台的access密钥和secret密钥(在我们的情况下将是自动生成的),让我们使用以下两个命令。在我们的情况下,我们使用我们的my-tenant租户来获取access密钥:

echo $(kubectl get secret my-tenant-console-secret -o=jsonpath='{.data.CONSOLE_ACCESS_KEY}' | base64 --decode)

为了获取secret密钥,我们使用以下命令:

echo $(kubectl get secret my-tenant-console-secret -o=jsonpath='{.data.CONSOLE_SECRET_KEY}' | base64 --decode)

现在,我们的 Minio 控制台将在一个名为<TENANT NAME>-console的服务上可用。

让我们使用port-forward命令访问这个控制台。在我们的情况下,这将是如下所示:

kubectl port-forward service/my-tenant-console 8081:9443

然后,我们的 Minio 控制台将在浏览器上的https://localhost:8081上可用。您需要接受浏览器的安全警告,因为在这个示例中,我们还没有为本地主机的控制台设置 TLS 证书。输入从前面步骤中获得的access密钥和secret密钥来登录!

现在我们已经登录到控制台,我们可以开始向我们的 Minio 租户添加内容。首先,让我们创建一个存储桶。要做到这一点,点击左侧边栏上的存储桶,然后点击创建存储桶按钮。

在弹出窗口中,输入存储桶的名称(在我们的情况下,我们将使用my-bucket)并提交表单。您应该在列表中看到一个新的存储桶 - 请参阅以下截图以获取示例:

图 15.2 - 存储桶

图 15.2 - 存储桶

现在,我们的分布式 Minio 设置已经准备就绪,还有一个要上传的存储桶。让我们通过向我们全新的对象存储系统上传文件来结束这个示例!

我们将使用 Minio CLI 进行上传,这使得与 Minio 等兼容 S3 存储进行交互的过程变得更加容易。我们将在 Kubernetes 内部运行一个预加载了 Minio CLI 的容器镜像,而不是从我们的本地机器使用 Minio CLI,因为只有在集群内访问 Minio 时 TLS 设置才能生效。

首先,我们需要获取 Minio 的access密钥和secret,这与我们之前获取的控制台access密钥和secret不同。要获取这些密钥,运行以下控制台命令(在我们的情况下,我们的租户是my-tenant)。首先,获取access密钥:

echo $(kubectl get secret my-tenant-creds-secret -o=jsonpath='{.data.accesskey}' | base64 --decode)

然后,获取secret密钥:

echo $(kubectl get secret my-tenant-creds-secret -o=jsonpath='{.data.secretkey}' | base64 --decode)

现在,让我们启动带有 Minio CLI 的 Pod。为此,让我们使用以下 Pod 规范:

minio-mc-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: minio-mc
spec:
  containers:
  - name: mc
    image: minio/mc
    command: ["/bin/sh", "-c", "sleep 10000000s"]
  restartPolicy: OnFailure

使用以下命令创建这个 Pod:

kubectl apply -f minio-mc-pod.yaml

然后,要exec进入这个minio-mc Pod,我们运行通常的exec命令:

Kubectl exec -it minio-mc -- sh

现在,让我们在 Minio CLI 中为我们新创建的 Minio 分布式集群配置访问。我们可以使用以下命令来完成这个操作(在这个配置中,--insecure标志是必需的):

mc config host add my-minio https://<MINIO TENANT POD IP>:9000 --insecure

此命令的 Pod IP 可以是我们的任一租户 Minio Pods 的 IP - 在我们的情况下,这些是my-tenant-zone-0-0my-tenant-zone-0-1。运行此命令后,系统将提示您输入访问密钥和秘密密钥。输入它们,如果成功,您将看到一个确认消息,看起来像这样:

Added `my-minio` successfully.

现在,为了测试 CLI 配置是否正常工作,我们可以使用以下命令创建另一个测试存储桶:

mc mb my-minio/my-bucket-2 --insecure

这应该会产生以下输出:

Bucket created successfully `my-minio/my-bucket-2`.

作为我们设置的最后一个测试,让我们将一个文件上传到我们的 Minio 存储桶!

首先,仍然在minio-mc Pod 上,创建一个名为test.txt的文本文件。用任何您喜欢的文本填充文件。

现在,让我们使用以下命令将其上传到我们最近创建的存储桶中:

mc mv test.txt my-minio/my-bucket-2 --insecure

您应该会看到一个带有上传进度的加载栏,最终显示整个文件大小已上传。

作为最后的检查,转到 Minio 控制台上的仪表板页面,查看对象是否显示出来,如下图所示:

图 15.3 – 仪表板

图 15.3 – 仪表板

正如您所看到的,我们的文件已成功上传!

就这些关于 Minio 的内容 - 在配置方面还有很多事情可以做,但这超出了本书的范围。请查看docs.min.io/上的文档以获取更多信息。

接下来,让我们看看在 Kubernetes 上运行数据库。

在 Kubernetes 上运行数据库

现在我们已经看过了 Kubernetes 上的对象存储工作负载,我们可以继续进行数据库的讨论。正如我们在本章和本书其他地方讨论过的那样,许多数据库支持在 Kubernetes 上运行,具有不同程度的成熟度。

首先,有几个传统和现有的数据库引擎支持部署到 Kubernetes。通常,这些引擎将有受支持的 Helm 图表或操作员。例如,诸如 PostgreSQL 和 MySQL 之类的 SQL 数据库有受各种不同组织支持的 Helm 图表和操作员。诸如 MongoDB 之类的 NoSQL 数据库也有支持的部署到 Kubernetes 的方式。

除了这些先前存在的数据库引擎之外,诸如 Kubernetes 之类的容器编排器已经导致了一个新类别的创建 - NewSQL数据库。

这些数据库提供了 NoSQL 数据库的令人难以置信的可扩展性,还具有符合 SQL 标准的 API。它们可以被视为一种在 Kubernetes(和其他编排器)上轻松扩展 SQL 的方式。CockroachDB 在这里是一个受欢迎的选择,Vitess也是如此,它不仅仅是一个替代 NewSQL 数据库,而且还可以轻松扩展 MySQL 引擎。

在本章中,我们将专注于部署 CockroachDB,这是一个为分布式环境构建的现代 NewSQL 数据库,非常适合 Kubernetes。

在 Kubernetes 上运行 CockroachDB

要在我们的集群上运行 CockroachDB,我们将使用官方的 CockroachDB Helm 图表:

  1. 我们需要做的第一件事是添加 CockroachDB Helm 图表存储库,使用以下命令:
helm repo add cockroachdb https://charts.cockroachdb.com/

这应该会产生以下输出:

"cockroachdb" has been added to your repositories
  1. 在安装图表之前,让我们创建一个自定义的values.yaml文件,以便调整一些 CockroachDB 的默认设置。我们的演示文件如下所示:

Cockroach-db-values.yaml

storage:
  persistentVolume:
    size: 2Gi
statefulset:
  resources:
    limits:
      memory: "1Gi"
    requests:
      memory: "1Gi"
conf:
  cache: "256Mi"
  max-sql-memory: "256Mi"

正如您所看到的,我们指定了2GB 的 PersistentVolume 大小,1GB 的 Pod 内存限制和请求,以及 CockroachDB 的配置文件内容。此配置文件包括cache和最大memory的设置,它们设置为内存限制大小的 25%,为256MB。这个比例是 CockroachDB 的最佳实践。请记住,这些并不是所有生产就绪的设置,但它们对我们的演示来说是有效的。

  1. 在这一点上,让我们继续使用以下 Helm 命令创建我们的 CockroachDB 集群:
helm install cdb --values cockroach-db-values.yaml cockroachdb/cockroachdb

如果成功,您将看到来自 Helm 的冗长部署消息,我们将不在此重现。让我们使用以下命令检查在我们的集群上到底部署了什么:

kubectl get po 

您将看到类似以下的输出:

NAMESPACE     NAME                                          READY   STATUS      RESTARTS   AGE
default       cdb-cockroachdb-0                             0/1     Running     0          57s
default       cdb-cockroachdb-1                             0/1     Running     0          56s
default       cdb-cockroachdb-2                             1/1     Running     0          56s
default       cdb-cockroachdb-init-8p2s2                    0/1     Completed   0          57s

正如您所看到的,我们在一个 StatefulSet 中有三个 Pods,另外还有一个用于一些初始化任务的设置 Pod。

  1. 为了检查我们的集群是否正常运行,我们可以使用 CockroachDB Helm 图表输出中方便给出的命令(它将根据您的 Helm 发布名称而变化):
kubectl run -it --rm cockroach-client \
        --image=cockroachdb/cockroach \
        --restart=Never \
        --command -- \
        ./cockroach sql --insecure --host=cdb-cockroachdb-public.default

如果成功,将打开一个类似以下的提示符的控制台:

root@cdb-cockroachdb-public.default:26257/defaultdb>

接下来,我们将使用 SQL 测试 CockroachDB。

使用 SQL 测试 CockroachDB

现在,我们可以对我们的新 CockroachDB 数据库运行 SQL 命令了!

  1. 首先,让我们使用以下命令创建一个数据库:
CREATE DATABASE mydb;
  1. 接下来,让我们创建一个简单的表:
CREATE TABLE mydb.users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    first_name STRING,
    last_name STRING,
    email STRING
 );
  1. 然后,让我们使用这个命令添加一些数据:
INSERT INTO mydb.users (first_name, last_name, email)
  VALUES
      ('John', 'Smith', 'jsmith@fake.com');
  1. 最后,让我们使用以下命令确认数据:
SELECT * FROM mydb.users;

这将给您以下输出:

                  id                  | first_name | last_name |      email
---------------------------------------+------------+-----------+------------------
  e6fa342f-8fe5-47ad-adde-e543833ffd28 | John       | Smith     | jsmith@fake.com
(1 row)

成功!

正如您所看到的,我们有一个完全功能的分布式 SQL 数据库。让我们继续进行最后一个我们将审查的有状态工作负载类型:消息传递。

在 Kubernetes 上实现消息传递和队列

对于消息传递,我们将实现 RabbitMQ,这是一个支持 Kubernetes 的开源消息队列系统。消息系统通常用于应用程序中,以解耦应用程序的各个组件,以支持规模和吞吐量,以及异步模式,如重试和服务工作器群。例如,一个服务可以将消息放入持久消息队列,然后由监听队列的工作容器接收。这允许轻松的水平扩展,并且相对于负载均衡方法,更容忍整个组件的停机。

RabbitMQ 是消息队列的众多选项之一。正如我们在本章的第一节中提到的,RabbitMQ 是消息队列的行业标准选项,不一定是专为 Kubernetes 构建的队列系统。然而,它仍然是一个很好的选择,并且非常容易部署,我们很快就会看到。

让我们从在 Kubernetes 上实现 RabbitMQ 开始!

在 Kubernetes 上部署 RabbitMQ

在 Kubernetes 上安装 RabbitMQ 可以通过运算符或 Helm 图轻松完成。出于本教程的目的,我们将使用 Helm 图:

  1. 首先,让我们添加适当的helm存储库(由Bitnami提供):
helm repo add bitnami https://charts.bitnami.com/bitnami
  1. 接下来,让我们创建一个自定义值文件来调整一些参数:

Values-rabbitmq.yaml

auth:
  user: user
  password: test123
persistence:
  enabled: false

正如您所看到的,在这种情况下,我们正在禁用持久性,这对于快速演示非常有用。

  1. 然后,RabbitMQ 可以通过以下命令轻松安装到集群中:
helm install rabbitmq bitnami/rabbitmq --values values-rabbitmq.yaml

成功后,您将看到来自 Helm 的确认消息。RabbitMQ Helm 图还包括管理 UI,让我们使用它来验证我们的安装是否成功。

  1. 首先,让我们开始将端口转发到rabbitmq服务:
kubectl port-forward --namespace default svc/rabbitmq 15672:15672

然后,我们应该能够在http://localhost:15672上访问 RabbitMQ 管理 UI。它将如下所示:

图 15.4 - RabbitMQ 管理控制台登录

图 15.4 - RabbitMQ 管理控制台登录

  1. 现在,我们应该能够使用值文件中指定的用户名和密码登录到仪表板。登录后,您将看到 RabbitMQ 仪表板的主视图。

重要的是,您将看到 RabbitMQ 集群中节点的列表。在我们的案例中,我们只有一个单一节点,显示如下:

图 15.5 - RabbitMQ 管理控制台节点项目

图 15.5 - RabbitMQ 管理控制台节点项目

对于每个节点,您可以看到名称和一些元数据,包括内存、正常运行时间等。

  1. 为了添加新队列,导航到屏幕顶部的队列,然后点击屏幕底部的添加新队列。填写表单如下,然后点击添加队列图 15.6 - RabbitMQ 管理控制台队列创建

图 15.6 - RabbitMQ 管理控制台队列创建

如果成功,屏幕应该会刷新,您的新队列将添加到列表中。这意味着我们的 RabbitMQ 设置正常工作!

  1. 最后,现在我们有了一个队列,我们可以向其发布消息。要做到这一点,点击队列页面上新创建的队列,然后点击发布消息

  2. 有效载荷文本框中写入任何文本,然后点击发布消息。您应该会看到一个确认弹出窗口,告诉您您的消息已成功发布,并且屏幕应该会刷新,显示您的消息在队列中,如下图所示:图 15.7 - RabbitMQ 管理控制台队列状态

图 15.7 - RabbitMQ 管理控制台队列状态

  1. 最后,为了模拟从队列中获取消息,点击页面底部附近的获取消息,这将展开显示一个新部分,然后点击获取消息按钮。您应该看到您发送的消息的输出,证明队列系统正常工作!

摘要

在本章中,我们学习了如何在 Kubernetes 上运行有状态的工作负载。首先,我们回顾了一些有状态工作负载的高级概述以及每种工作负载的一些示例。然后,我们继续实际在 Kubernetes 上部署这些工作负载之一 - 对象存储系统。接下来,我们使用 NewSQL 数据库 CockroachDB 做了同样的事情,向您展示了如何在 Kubernetes 上轻松部署 CockroachDB 集群。

最后,我们向您展示了如何使用 Helm 图表在 Kubernetes 上部署 RabbitMQ 消息队列。本章中使用的技能将帮助您在 Kubernetes 上部署和使用流行的有状态应用程序模式。

如果你已经读到这里,感谢你一直陪伴我们阅读完这本书的所有 15 章!我希望你已经学会了如何使用广泛的 Kubernetes 功能,现在你已经拥有了构建和部署复杂应用程序所需的所有工具。

问题

  1. Minio 的 API 兼容哪种云存储提供?

  2. 对于分布式数据库,StatefulSet 有哪些好处?

  3. 用你自己的话来说,什么让有状态的应用在 Kubernetes 上运行变得困难?

进一步阅读

第十六章:评估

第一章 - 与 Kubernetes 通信

  1. 容器编排是一种软件模式,其中多个容器被控制和调度以便为应用程序提供服务。

  2. Kubernetes API 服务器(kube-apiserver)处理更新 Kubernetes 资源的请求。调度程序(kube-scheduler)决定在哪里放置(调度)容器。控制器管理器(kube-controller-manager)确保 Kubernetes 资源的期望配置反映在集群中。etcd为集群配置提供数据存储。

  3. kube-apiserver必须使用--authorization-mode=ABAC--authorization-policy-file=filename参数启动。

  4. 为了保证控制平面的高可用性,以防其中一个主节点发生故障。

  5. 如果资源已经创建,kubectl create将失败,因为资源已经存在,而kubectl apply将尝试将任何 YAML 更改应用于资源。

  6. kubectl use-context命令可用于在kubeconfig文件中切换多个上下文。要在kubeconfig文件之间切换,可以将KUBECONFIG环境变量设置为新文件的路径。

  7. 命令式命令不提供对资源更改的历史记录。

第二章 - 设置您的 Kubernetes 集群

  1. Minikube 使得在本地轻松设置 Kubernetes 集群进行开发。

  2. 在某些情况下,集群的固定最低成本可能大于自行配置的集群。一些托管选项还有许可成本,除了计算成本之外。

  3. Kubeadm 对基础设施提供商是不可知的,而 Kops 仅支持几个主要提供商,并具有更深入的集成和计算资源提供。

  4. 截至本书撰写时,AWS,Google Cloud Platform,Digital Ocean,VMware 和 OpenStack 在各种生产准备级别上。

  5. 通常,集群组件在systemd服务定义中定义,这允许在节点在操作系统级别关闭和重新启动时自动重新启动服务。

第三章 - 在 Kubernetes 上运行应用程序容器

  1. 如果您有开发、分级和生产环境,您可以为每个环境创建一个命名空间。

  2. Pod 正在运行的节点可能处于损坏状态,控制平面无法到达它。通常,当节点正常退出集群时,Pod 将被简单地重新调度,而不是显示未知状态。

  3. 防止占用内存的 Pod 占据整个节点并导致节点上其他 Pod 的不确定行为。

  4. 如果有Startup探测器,您应该增加更多的延迟。如果没有,您将需要添加一个,或者在Readiness探测器中添加延迟。

第四章 - 扩展和部署您的应用程序

  1. ReplicationControllers 在选择器配置方面的灵活性较小 - 只允许键值选择器。

  2. 部署允许您指定如何滚动更新。

  3. 作业非常适合批处理任务,或者可以水平扩展并具有明确完成目标的任务。

  4. StatefulSets 提供了一个有序的 Pod 标识,当这些 Pod 重新启动时保持不变。

  5. 除了现有版本外,还可以创建一个带有金丝雀版本的新部署。然后,两个版本可以并行访问。

第五章 - 服务和入口 - 与外部世界通信

  1. 您应该使用 ClusterIP 服务。

  2. 您可以使用kubectl describe命令查看 NodePort 服务在节点上的哪个端口处于活动状态。

  3. 在云环境中,您经常需要按负载均衡器付费,入口允许您指定多个路由规则,同时只需支付一个负载均衡器的费用。

  4. ExternalName 服务可用于轻松路由到云环境中的其他基础设施 - 例如托管数据库和对象存储。

第六章 - Kubernetes 应用程序配置

  1. 秘密以编码形式存储,并在etcd中可选加密。ConfigMaps 以明文形式存储。

  2. 它们是 Base64 编码的。

  3. 在描述 ConfigMap 时,数据将更加可见。当将 ConfigMap 挂载为环境变量时,键值模式也更容易使用。

  4. 根据您设置集群的方式,您的秘密可能根本没有加密。如果集群的 EncryptionConfiguration 未设置,秘密将只被 Base64 编码 - 并且可以很容易地被解码。通过使用 EncryptionConfiguration 创建您的集群,您的秘密将以加密形式存储在etcd中。这并不是一个安全的灵丹妙药,但是静态加密对于提高秘密的安全性当然是必要的。

第七章 - Kubernetes 上的存储

  1. 卷与 Pod 的生命周期相关联,并在删除 Pod 时被删除。持久卷将保留,直到集群被删除,或者它们被明确地删除。

  2. StorageClasses 定义持久卷的类型。它们可用于区分不同类型的存储,例如更快的 SSD 存储和较慢的硬盘存储 - 或不同类型的云存储。StorageClasses 确定持久卷索赔和持久卷将去获取配置存储的位置。

  3. 使用带有集成存储配置的托管 Kubernetes 服务,或者向您的集群添加cloud-controller-manager配置。

  4. 任何需要存储状态超过单个 Pod 寿命的应用程序都无法使用卷。任何需要具有对 Pod 故障具有容忍性状态的应用程序都需要持久卷。

第八章 - Pod 放置控制

  1. 节点选择器可用于匹配节点标签,并且多个节点可以满足要求。使用节点名称意味着您指定必须放置 Pod 的单个节点。

  2. Kubernetes 实施了一些默认的污点,以确保 Pod 不会被调度到发生故障或缺乏资源的节点上。此外,Kubernetes 会在主节点上设置污点,以防止用户应用程序在主节点上进行调度。

  3. 太多的亲和性和反亲和性可能会减慢调度器的速度,或导致其无响应。在具有许多亲和性或反亲和性的情况下确定 Pod 的放置是非常计算密集的。

  4. 使用反亲和性,您可以防止 Pod 与同一故障域中的相似 Pod 共存。同一故障域中的节点将带有故障域或区域标识符。反亲和性将寻找与同一故障域中应用程序级别的特定层相匹配的 Pod,并防止在匹配该域的节点上进行调度。最终结果将是三层应用程序的每个层在多个故障域中分布。

第九章 - Kubernetes 上的可观察性

  1. 指标对应于数字值,表示应用程序/计算性能和/或跨许多类别的使用情况,包括磁盘、CPU、内存、延迟等。日志对应于应用程序、节点或控制平面文本日志。

  2. Grafana UI 高度可定制,并可用于以优雅灵活的方式呈现复杂的 Prometheus(或其他数据源的)查询。

  3. FluentD 需要在生产集群上运行以收集日志。Elasticsearch 和 Kibana 可以在单独的集群或其他基础设施上运行。

第十章 - Kubernetes 故障排除

  1. Kubernetes 的一个优势是通过添加节点或使用控件(如污点和容忍度)轻松扩展集群。 此外,Pod 重新启动可能导致相同应用程序具有完全不同的 IP。 这意味着计算和网络拓扑结构可能会不断变化。

  2. kubelet通常作为 Linux 服务在systemd中运行,可以使用systemctl进行控制,并在journalctl中查看日志。

  3. 有一些不同的方法可以使用,但通常,您会想要检查所有节点是否准备就绪和可调度; 是否有任何 Pod 放置控件阻止了 Pod 的调度; 以及是否存在任何依赖存储、ConfigMaps 或不存在的 secrets。

第十一章 - 模板代码生成和 Kubernetes 上的 CI/CD

  1. Helm Charts 使用模板和变量,而 Kustomize 使用基于补丁的策略。 Kustomize 内置于最新版本的 kubectl 中,而 Helm 使用单独的 CLI 工具。

  2. 配置应强调安全性,因为部署凭据可能被用于部署攻击者的工作负载到您的集群。 在云提供商上使用安全环境变量或访问管理控制是两种好策略。 凭据绝对不应放置在任何 Git 存储库中。

  3. 在集群内设置可能更可取,因为 Kubernetes 凭据不需要由外部系统提供。 集群外设置通常比集群内设置更简单,更同步,其中控制循环确定何时对资源配置进行更改。

第十二章 - Kubernetes 安全性和合规性

  1. MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。

  2. 具有空 Pod 选择器的 NetworkPolicy 会选择所有 Pod。 具有选择了所有 Pod 的 NetworkPolicy,并且添加了 Ingress 和 Egress 类型而没有任何规则,将自动拒绝对命名空间中所有 Pod 的所有入口和出口。

  3. 我们希望跟踪任何 API 请求,在这些请求中资源被打补丁或更新,因为攻击者可能会更新部署、Pod 或其他资源,并植入恶意容器。

第十三章 - 使用 CRD 扩展 Kubernetes

  1. 存储版本是实际存储在数据存储中的版本。 服务版本是 API 接受的任何读取或写入操作的版本。 存储在etcd中时,服务版本将转换为存储版本。

  2. 测量、分析和更新(通常)。

  3. 根据云提供商,cluster-autoscaler插件将直接更新自动缩放组,以添加或删除节点。

第十四章 - 服务网格和无服务器

  1. 静态的 Envoy 配置是指由用户手动创建或编写的 Envoy 配置。动态的 Envoy 配置(例如 Istio 提供的那些)将不断适应新的容器,以及来自外部控制器或数据平面的新路由和过滤规则。

  2. 监听器、路由、集群和端点。

  3. Knative 需要许多组件才能运行。这样可以进行大量定制,但使得设置和操作比 OpenFaaS 更困难。

第十五章 - Kubernetes 上的有状态工作负载

  1. Minio 是一种与 AWS S3 兼容的存储工具。

  2. StatefulSets 通过提供稳定的序数 Pod 标识以及持久卷稳定性,帮助自我集群化的应用程序,如分布式数据库。

  3. 在 Kubernetes 中,Pod 可以是短暂的,有状态的应用程序可以是分布式的。这意味着在 Pod 之间保持状态的过程(例如,数据库一致性)可能会变得困难,如果 Pod 改变身份并且需要从头开始复制存储。

posted @ 2024-05-06 18:36  绝不原创的飞龙  阅读(38)  评论(0编辑  收藏  举报