Kubernetes-云原生指南-全-
Kubernetes 云原生指南(全)
原文:
annas-archive.org/md5/58dd843cc49b42503e619a37722eeb6c译者:飞龙
前言
本书的目标是为你提供必要的知识和工具,帮助你使用 Kubernetes 构建云原生应用。Kubernetes 是一项强大的技术,为工程师提供了使用容器构建云原生平台的有力工具。该项目本身在不断发展,包含了许多不同的工具来应对常见的场景。
本书的布局并没有专注于 Kubernetes 工具集的某一特定领域,我们将首先为你提供 Kubernetes 默认功能中最重要部分的全面总结——让你掌握运行 Kubernetes 应用所需的所有技能。接着,我们将为你提供应对 Kubernetes 安全性和故障排除的工具,适用于 Day 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 还提供了一个Alerts选项卡,用于配置 Prometheus 警报。”
提示或重要备注
如此显示。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并通过 customercare@packtpub.com 与我们联系。
勘误:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您发现本书中有错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法复制品,我们将非常感激您能提供该位置地址或网站名称。请通过版权@packt.com 与我们联系,并提供该材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣撰写或参与编写一本书,请访问authors.packtpub.com。
评价
请留下评论。阅读并使用完本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并利用您公正的意见来做出购买决策,我们 Packt 公司也能了解您对我们产品的看法,而我们的作者则能看到您对他们书籍的反馈。谢谢!
如需了解更多关于 Packt 的信息,请访问packt.com。
第一部分:设置 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 是一个开源容器编排工具,最初由 Google 创建,借鉴了其内部的编排工具 Borg 和 Omega,这些工具在 Google 内部已使用多年。自从 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(参见 第十三章,通过 CRD 扩展 Kubernetes),API 还具有一组基于 API 组的动态端点,这些端点可以将相同的 RESTful API 功能暴露给自定义资源。
Kubernetes 调度器
Kubernetes 调度器决定工作负载的实例应该运行在哪里。默认情况下,这个决定受到工作负载资源需求和节点状态的影响。你还可以通过在 Kubernetes 中可配置的放置控制来影响调度器(参见 第八章,Pod 放置控制)。这些控制可以作用于节点标签、节点上已经运行的其他 Pod,以及许多其他可能性。
Kubernetes 控制器管理器
Kubernetes 控制器管理器是一个运行多个控制器的组件。控制器运行控制循环,确保集群的实际状态与存储在配置中的状态相匹配。默认情况下,这些包括以下内容:
-
节点控制器,确保节点正常运行
-
副本控制器,确保每个工作负载得到适当的扩展
-
端点控制器,负责处理每个工作负载的通信和路由配置(参见 第五章**,服务与入口 – 与外界通信)
-
服务账户和令牌控制器,负责创建 API 访问令牌和默认账户
etcd
etcd 是一个分布式键值存储,用于以高度可用的方式存储集群的配置。每个主节点上运行一个 etcd 副本,并使用 Raft 一致性算法,确保在允许对键值进行任何更改之前,必须保持法定人数。
Kubernetes 工作节点
每个 Kubernetes 工作节点都包含一些组件,使其能够与控制平面通信并处理网络。
首先是 kubelet,它确保集群配置要求的容器在节点上运行。其次,kube-proxy 提供了一个网络代理层,支持每个节点上运行的工作负载。最后,容器运行时 用于在每个节点上运行工作负载。
kubelet
kubelet 是一个运行在每个节点上的代理(包括主节点,尽管在此上下文中具有不同的配置)。它的主要功能是接收一份 PodSpecs 列表(稍后将详细介绍),并确保这些 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 安装还包括插件,它们是提供集群功能的附加组件。
例如,Calico、Flannel 或 Weave 提供符合 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 提供了几种不同的选项:HTTP 基本认证、客户端证书、承载令牌和基于代理的认证。
要使用 HTTP 认证,请求者发送包含 Authorization 头的请求,该头部的值为 bearer "token value"。
为了指定哪些令牌有效,启动 API 服务器应用程序时可以提供一个 CSV 文件,使用 --token-auth-file=filename 参数。一个新的 beta 特性(截至本书撰写时)叫做 引导令牌,允许在 API 服务器运行时动态交换和更改令牌,而无需重启。
也可以通过 Authorization 令牌进行基本的用户名/密码认证,使用请求头值 Basic base64encoded(username:password)。
Kubernetes 的 TLS 和安全证书基础设施
为了使用客户端证书(X.509 证书),必须使用 --client-ca-file=filename 参数启动 API 服务器。此文件需要包含一个或多个 证书颁发机构(CAs),它们将在验证 API 请求中传递的证书时使用。
除了 groups 可以包含的内容,我们将在 Authorization 选项部分讨论。
例如,你可以使用以下内容:
openssl req -new -key myuser.pem -out myusercsr.pem -subj "/CN=myuser/0=dev/0=staging"
这将为用户 myuser 创建一个 CSR,该用户属于名为 dev 和 staging 的组。
一旦 CA 和 CSR 被创建,就可以使用 openssl、easyrsa、cfssl 或任何证书生成工具创建实际的客户端和服务器证书。此时还可以为 Kubernetes API 创建 TLS 证书。
因为我们的目标是尽快让你开始在 Kubernetes 上运行工作负载,所以我们将本书中省略所有可能的证书配置——但 Kubernetes 文档和文章 Kubernetes The Hard Way 提供了一些关于从零开始设置集群的很好的教程。在大多数生产环境中,你不会手动执行这些步骤。
授权选项
Kubernetes 提供了几种授权方法:节点、webhook、RBAC 和 ABAC。在本书中,我们将重点讨论 RBAC 和 ABAC,因为它们是最常用于用户授权的方式。如果你通过其他服务和/或自定义功能扩展集群,其他授权模式可能会变得更加重要。
RBAC
Role、ClusterRole、RoleBinding 和 ClusterRoleBinding。要启用 RBAC 模式,API 服务器可以通过 --authorization-mode=RBAC 参数启动。
Role 和 ClusterRole 资源指定了一组权限,但并没有将这些权限分配给任何特定的用户。权限是通过 resources 和 verbs 来指定的。以下是一个指定 Role 的示例 YAML 文件。不要过于担心 YAML 文件的前几行——我们稍后会解释。重点关注 resources 和 verbs 行,看看如何将操作应用于资源:
Read-only-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: read-only-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
Role 和 ClusterRole 之间唯一的区别在于,Role 限制在特定的命名空间中(在这种情况下是默认命名空间),而 ClusterRole 可以影响集群中该类型的所有资源访问,以及集群范围的资源,如节点。
RoleBinding 和 ClusterRoleBinding 是将 Role 或 ClusterRole 与用户或用户列表关联的资源。以下文件表示一个将 read-only-role 与用户 readonlyuser 连接的 RoleBinding 资源:
Read-only-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 键包含所有要关联角色的实体列表;在此例中是用户 alex。roleRef 包含要关联的角色的名称,以及类型(Role 或 ClusterRole)。
ABAC
--authorization-mode=ABAC 和 --authorization-policy-file=filename 参数。
在策略文件中,每个策略对象包含有关单个策略的信息:首先,它对应的主体是哪个,可以是用户或组;其次,哪些资源可以通过该策略进行访问。此外,可以包含一个布尔值 readonly 来限制策略仅对 list、get 和 watch 操作有效。
一种次要类型的策略与资源无关,而是与非资源请求类型相关,例如对 /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 文件有三个部分:clusters、users和contexts:
-
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 命令)可以在 Kubernetes 文档中找到: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 标志来查看 create 或 apply 命令的输出(即,哪些资源将被创建,或者如果存在错误,会显示错误信息)。
要命令式地更新现有资源,请使用 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 文件至少有四个顶级键。它们是 apiVersion、kind、metadata 和 spec。
apiVersion 决定将使用哪个版本的 Kubernetes API 来创建资源。kind 指定 YAML 文件所引用的资源类型。metadata 提供了命名资源的位置,并可以添加注解和命名空间信息(稍后会详细介绍)。最后,spec 键将包含 Kubernetes 创建资源所需的所有资源特定信息。
暂时不用担心 kind 和 spec —— 我们将在 第三章,在 Kubernetes 上运行应用容器 中详细介绍什么是 Pod。
总结
在这一章,我们学习了容器编排的背景,Kubernetes 集群的架构概述,集群如何验证和授权 API 调用,以及如何通过命令式和声明式模式使用 kubectl 与 API 进行通信,kubectl 是 Kubernetes 官方支持的命令行工具。
在下一章,我们将学习几种启动测试集群的方法,并掌握如何利用你迄今为止学到的 kubectl 命令。
问题
-
什么是容器编排?
-
Kubernetes 控制平面的组成部分有哪些?它们分别有什么作用?
-
如何以 ABAC 授权模式启动 Kubernetes API 服务器?
-
为什么在生产 Kubernetes 集群中有多个主节点很重要?
-
kubectl apply和kubectl create有什么区别? -
如何使用
kubectl在不同上下文之间切换? -
在声明式地创建 Kubernetes 资源后,再进行命令式编辑有什么缺点?
深入阅读
-
Kubernetes 官方文档:
kubernetes.io/docs/home/ -
Kubernetes The Hard Way:
github.com/kelseyhightower/kubernetes-the-hard-way
第二章:设置您的 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)
-
Google Cloud – Google Kubernetes 引擎(GKE)
-
Microsoft Azure – Azure Kubernetes 服务(AKS)
重要提示
管理型 Kubernetes 服务的数量和实施方式始终在变化。选择 AWS、Google Cloud 和 Azure 作为本书这一章节的内容,因为它们很可能会继续以相同的方式运行。无论使用哪个管理型服务,都要确保检查服务附带的官方文档,以确保集群创建过程仍与本书中所述相同。
管理型 Kubernetes 服务的好处
一般来说,主要的管理型 Kubernetes 服务提供一些好处。首先,我们审查的三个管理型服务都提供完全托管的 Kubernetes 控制平面。
这意味着,当您使用这些管理型 Kubernetes 服务时,您无需担心主节点。它们已被抽象化,几乎可以认为它们不存在。这三个管理型集群都允许您在创建集群时选择工作节点的数量。
管理型集群的另一个好处是可以无缝地将 Kubernetes 从一个版本升级到另一个版本。一般来说,一旦为管理型服务验证了一个新的 Kubernetes 版本(并不总是最新版本),您应该能够通过一键操作或相对简单的程序来进行升级。
管理型 Kubernetes 服务的缺点
尽管管理型 Kubernetes 集群在许多方面可以简化操作,但也有一些缺点。
对于许多可用的管理型 Kubernetes 服务,管理集群的最低成本远超手动创建或使用像 Kops 这样的工具创建的最小集群成本。对于生产使用场景,这通常不是问题,因为生产集群应包含最低数量的节点,但对于开发环境或测试集群,额外的成本可能无法根据预算与操作简便性相匹配。
此外,虽然抽象化主节点使得操作更简单,但它也限制了主节点功能的微调或高级功能,这些功能在具有定义主节点的集群中可能是可用的。
AWS – 弹性 Kubernetes 服务
AWS 的管理型 Kubernetes 服务称为 EKS(Elastic Kubernetes Service)。有几种不同的方式可以开始使用 EKS,但我们将介绍最简单的方式。
开始使用
为了创建一个 EKS 集群,您必须配置适当的虚拟私有云(VPC)和身份与访问管理(IAM)角色设置——此时您就可以通过控制台创建集群。这些设置可以通过控制台手动创建,或者使用像 CloudFormation 和 Terraform 这样的基础设施配置工具来创建。有关通过控制台创建集群的完整说明,请参见 docs.aws.amazon.com/en_pv/eks/latest/userguide/getting-started-console.html。
假设您是从头开始创建集群和 VPC,则可以使用名为eksctl的工具来提供您的集群。
若要安装eksctl,您可以在 macOS、Linux 和 Windows 上找到安装说明,请访问docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html。
一旦安装了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_1和compute_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 Service
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的集群。对于appId和password,请使用服务主体创建步骤中的值。
最后,为了在您的机器上生成正确的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 虚拟机的地方工作。
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 的领域知识,我们将在本书中不讨论这一部分内容——但关于 Kubespray 的所有内容可以参考github.com/kubernetes-sigs/kubespray/blob/master/docs/getting-started.md。
使用 Kubeadm 创建集群
要使用 Kubeadm 创建一个集群,您需要提前为节点进行配置。与任何其他 Kubernetes 集群一样,我们需要运行 Linux 的虚拟机或裸金属服务器。
本书将展示如何仅使用单个主节点来引导一个 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 文档中找到其他支持的平台的安装说明: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 正常工作,你需要在机器上配置 AWS 凭证,并授予一些必要的 IAM 权限。为了安全起见,你应为 Kops 创建一个专用的 IAM 用户。
首先,为kops用户创建一个 IAM 组:
aws iam create-group --group-name kops_users
然后,为 kops_users 组附加所需的角色。为了正常运行,Kops 需要 AmazonEC2FullAccess、AmazonRoute53FullAccess、AmazonS3FullAccess、IAMFullAccess 和 AmazonVPCFullAccess。我们可以通过运行以下命令来实现:
提供 AWS 策略给 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),该 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将被启动在每个主节点上。要在节点上实际启动etcd,我们运行以下命令:
{
sudo systemctl daemon-reload
sudo systemctl enable etcd
sudo systemctl start etcd
}
这使得etcd服务能够启用,并在节点重启时自动重启。
引导控制平面组件
在主节点上引导控制平面组件的过程类似于创建etcd集群的过程。为每个组件——API 服务器、控制器管理器和调度器——创建systemd文件,然后使用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-scheduler和kube-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,我们希望在节点关闭时确保服务能够重启。
引导工作节点
在工作节点上情况类似。需要为kubelet、容器运行时、cni和kube-proxy创建并使用systemctl运行服务规格。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 在架构层面是如何工作的知识,这将使你更容易理解接下来的几章中的概念。
问题
-
minikube 的作用是什么?
-
使用托管的 Kubernetes 服务有什么缺点?
-
Kops 和 Kubeadm 有什么区别?主要区别是什么?
-
Kops 支持哪些平台?
-
手动创建集群时,如何指定主要的集群组件?它们如何在每个节点上运行?
进一步阅读
-
官方 Kubernetes 文档:
kubernetes.io/docs/home/ -
Kubernetes The Hard Way:
github.com/kelseyhightower/kubernetes-the-hard-way
第三章: 在 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 上运行应用程序的最佳方式。为了充分利用像 Kubernetes 这样的容器编排工具的真正能力,Pod 应被视为一次性资源。这意味着应将容器(因此也包括 Pod)视为牲畜,而不是宠物。要真正利用容器和 Kubernetes,应用程序应在自愈、可扩展的组中运行。Pod 是这些组的构建块,我们将在后续章节中讲解如何以这种方式配置应用程序。
实现 Pods
Pod 是通过 Linux 隔离原则(如组和命名空间)实现的,通常可以看作是一个逻辑主机。Pod 可以运行一个或多个容器(这些容器可以基于 Docker、CRI-O 或其他运行时),并且这些容器之间可以像虚拟机中的不同进程那样进行通信。
为了使两个不同 Pod 中的容器能够通信,它们需要通过 Pod 的 IP 访问另一个 Pod(及其容器)。默认情况下,只有运行在同一 Pod 中的容器可以使用较低层次的通信方式,尽管可以配置不同的 Pod,使其通过主机 IPC 相互通信。
Pod 范式
从最基本的层面来看,Pod 有两种类型:
-
单容器 Pod
-
多容器 Pod
一般来说,最佳实践是每个 Pod 只包含一个容器。这样做可以让你分别扩展应用的不同部分,并且在创建能够正常启动和运行的 Pod 时通常会更简单。
另一方面,多容器 Pod 更为复杂,但在某些情况下非常有用:
-
如果你的应用有多个部分运行在独立的容器中,但它们是紧密耦合的,你可以将它们放在同一个 Pod 中运行,以便实现无缝的通信和文件系统访问。
-
在实现侧车模式时,工具容器会与主应用一起注入,用于处理日志、指标、网络或更高级的功能,如服务网格(关于这方面的内容可以参考第十四章,服务网格与无服务器架构)。
下图展示了一个常见的侧车实现:

图 3.1 – 常见侧边栏实现
在这个示例中,我们有一个 Pod,里面有两个容器:一个运行 Web 服务器的应用容器和一个日志应用,它从我们的服务器 Pod 拉取日志并将其转发到日志基础设施。这是侧车模式的一个非常典型的应用,尽管许多日志收集器是在节点级别工作,而不是 Pod 级别,因此这并不是在 Kubernetes 中从我们的应用容器收集日志的普遍方式。
Pod 网络
正如我们刚刚提到的,Pods 有自己的 IP 地址,可以用于 Pod 间的通信。每个 Pod 都有一个 IP 地址以及端口,如果 Pod 中有多个容器,这些端口是容器之间共享的。
在一个 Pod 内,如前所述,容器之间可以不通过 Pod 的 IP 来通信——它们可以直接使用 localhost。这是因为 Pod 内的容器共享一个网络命名空间——本质上,它们通过相同的桥接进行通信,而这个桥接是通过虚拟网络接口实现的。
Pod 存储
Kubernetes 中的存储是一个庞大的话题,我们将在第七章,Kubernetes 上的存储中深入探讨——但现在,你可以将 Pod 存储理解为附加到 Pod 上的持久性或非持久性卷。非持久性卷可以被 Pod 用于存储数据或文件,具体取决于卷的类型,但它们会在 Pod 关闭时被删除。持久性卷将在 Pod 关闭后仍然存在,甚至可以在多个 Pod 或应用之间共享数据。
在我们继续讨论 Pods 之前,我们先简要讨论一下命名空间。由于我们在与 Pods 的操作中将使用kubectl命令,因此了解命名空间如何与 Kubernetes 和kubectl关联是非常重要的,因为这可能是一个大“陷阱”。
命名空间
我们在第一章《与 Kubernetes 通信》的授权部分中简要讨论了命名空间,但在这里我们将重申并扩展它们的目的。命名空间是一种在集群中逻辑上分离不同区域的方法。一个常见的用例是为每个环境设置一个命名空间——一个用于开发环境,一个用于暂存环境,一个用于生产环境——它们都存在于同一个集群中。
正如我们在授权部分中提到的,可以在每个命名空间的基础上指定用户权限——例如,允许用户将新应用和资源部署到dev命名空间,而不能部署到生产环境。
在你的运行集群中,你可以通过运行kubectl get namespaces或kubectl get ns来查看现有的命名空间,这将输出如下内容:
NAME STATUS AGE
default Active 1d
kube-system Active 1d
kube-public Active 1d
要强制创建命名空间,你可以简单地运行kubectl create namespace staging,或者运行kubectl apply -f /path/to/file.yaml,并使用以下 YAML 资源规格:
Staging-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: staging
如你所见,Namespace规格非常简单。让我们继续讨论一些更复杂的内容——PodSpec 本身。
Pod 生命周期
若要快速查看集群中正在运行的 Pods,可以运行kubectl get pods或kubectl get pods --all-namespaces,分别查看当前命名空间(由kubectl上下文定义,或者如果未指定则是默认命名空间)或所有命名空间中的 Pods。
kubectl get pods的输出如下所示:
NAME READY STATUS RESTARTS AGE
my-pod 1/1 Running 0 9s
如你所见,Pods 有一个STATUS值,告诉我们 Pod 当前处于哪个状态。
Pod 状态的值如下:
-
Running状态表示 Pod 已经成功启动其容器,且没有任何问题。如果 Pod 只有一个容器,且其状态为Running,则表示容器尚未完成或退出其进程。它也可能正在重启,你可以通过检查READY列来判断。例如,如果READY值为0/1,则意味着 Pod 中的容器当前未通过健康检查。这可能有多种原因:容器可能仍在启动中,数据库连接可能不可用,或某些重要的配置可能阻止了应用进程的启动。 -
如果容器已完成其进程命令,则为
Succeeded状态。 -
Pending状态表示 Pod 中至少有一个容器正在等待其镜像。这通常是因为容器镜像仍在从外部仓库拉取,或因为 Pod 本身正在等待kube-scheduler调度。 -
Unknown状态意味着 Kubernetes 无法确定 Pod 当前的状态。这通常表示 Pod 所在的节点正在经历某种错误。可能是磁盘空间不足、与集群其他部分断开连接,或遇到其他问题。 -
Failed状态,表示 Pod 中一个或多个容器已经以失败状态终止。此外,Pod 中的其他容器必须已经以成功或失败状态终止。这可能是由于集群删除 Pod 或容器应用程序内部的某些问题导致进程中断。
理解 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 值对应于 Kubernetes 中资源类型的实际名称。在这种情况下,我们要指定一个 Pod,这就是我们填写的内容。kind 值总是采用驼峰命名法,例如 Pod、ConfigMap、CronJob 等等。
重要说明
要查看完整的 kind 值列表,请参考官方 Kubernetes 文档:kubernetes.io/docs/home/。新的 Kubernetes kind 值会在新版本中添加,因此本书中讨论的值可能不是完整的列表。
元数据
元数据是一个顶级键,可以在其下有多个不同的值。首先,name 是资源的名称,它是通过 kubectl 显示的内容,并且在 etcd 中存储的名称。namespace 对应于资源应创建的命名空间。如果 YAML 规范中未指定命名空间,则资源将创建在 default 命名空间中——除非在 apply 或 create 命令中指定了命名空间。
接下来,labels 是用于向资源添加元数据的键值对。与其他元数据不同,labels 是 Kubernetes 原生 selectors 默认使用的,用于过滤和选择资源——但它们也可以用于自定义功能。
最后,metadata块可以包含多个annotations,这些annotations像labels一样,可以被控制器和自定义的 Kubernetes 功能使用,用于提供额外的配置和特定功能的数据。在这个 PodSpec 中,我们在metadata中指定了几个注解:
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
spec是包含资源特定配置的顶级键。在这种情况下,由于我们的kind值是Pod,我们将添加一些特定于 Pod 的配置。所有后续的键都会在spec键下缩进,并表示我们的 Pod 配置。
容器
containers键期望一个包含一个或多个容器的列表,这些容器将在 Pod 内运行。每个容器的规格将暴露它自己的配置值,这些值会在资源 YAML 中容器列表项下进行缩进。我们将在这里回顾其中一些配置,但完整的配置清单请参考 Kubernetes 文档(kubernetes.io/docs/home/)。
名称
在容器规格中,name与容器在 Pod 中的名称相关。容器名称可以用来通过kubectl logs命令特定地访问某个容器的日志,但我们稍后会讲到。现在,确保为 Pod 中的每个容器选择一个清晰的名称,以便在调试时更容易。
镜像
对于每个容器,image用于指定应该在 Pod 中启动的 Docker(或其他运行时)镜像的名称。镜像将从配置的仓库中拉取,默认是公共的 Docker Hub,也可以是私有仓库。
就这样——这就是你在 Kubernetes 中指定并运行一个 Pod 所需的一切。从此之后,Pod部分中的所有内容都属于附加配置范围。
Pod 资源规格
Pods 可以配置特定的内存和计算资源分配。这样可以防止资源消耗较大的应用程序影响集群性能,同时也有助于防止内存泄漏。可以指定两种资源——cpu和memory。对于每种资源,都有两种不同类型的规格——Requests和Limits,总共有四个可能的资源规格键。
内存请求和限制可以使用任何典型的内存数量后缀进行配置,或者使用其二的幂等效值——例如,50 Mi(兆二进制字节),50 MB(兆字节)或 1 Gi(吉比字节)。
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 镜像的容器,该镜像具有对 cpu 和 memory 的请求和限制。在这种情况下,我们的容器镜像名称 mydockername 是一个占位符 - 但如果你想在这个示例中测试 Pod 资源限制,你可以使用 busybox 镜像。
容器启动命令
当一个容器在 Kubernetes Pod 中启动时,它会运行容器规范中指定的默认启动脚本 - 例如 Docker 容器规范中指定的脚本。为了使用不同的命令或附加参数覆盖此功能,你可以提供 command 和 args 键。让我们来看一个配置了 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 – 初始化容器流程图
如果一个 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 会在指定的间隔时间内运行这些探针,以判断容器是否工作正常。
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
如您所见,我们的存活探针(liveness probe)的定义与准备就绪探针(readiness probe)相同,唯一的不同是增加了failureThreshold。
failureThreshold的值将决定 Kubernetes 在采取行动之前尝试探测的次数。对于存活探针,当failureThreshold被超越时,Kubernetes 会重启 Pod;而对于准备就绪探针,Kubernetes 则会简单地将 Pod 标记为Not Ready。该阈值的默认值是3,但可以更改为大于或等于1的任何值。
在这种情况下,我们使用exec机制来进行探测。稍后我们会回顾各种可用的探测机制。
启动探针
最后,启动探针(startup probe)是一种特殊类型的探针,它只会在容器启动时运行一次。有些(通常是较旧的)应用程序在容器中启动时可能需要很长时间,因此通过在容器首次启动时提供一些额外的宽限期,您可以避免存活探针或准备就绪探针失败,从而导致重启。以下是配置了启动探针的 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,但是我们可以更改就绪探针和启动探针的值。
探针机制配置
有多种机制可以指定三种探针之一:exec、httpGet 和 tcpSocket。
exec 方法允许你指定一个将在容器内运行的命令。成功执行的命令将导致探针通过,而失败的命令将导致探针失败。到目前为止,我们配置的所有探针都使用了 exec 方法,因此配置应该是显而易见的。如果所选命令(带有以逗号分隔的参数列表)失败,则探针将失败。
httpGet 方法允许你指定一个容器上的 URL,该 URL 将通过 HTTP GET 请求访问。如果 HTTP 请求返回的状态码在 200 到 400 之间,则探针成功。任何其他 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 状态与探针功能之间的互动分解开来是很有价值的。再次重申,以下是我们的状态:
-
运行中 -
成功 -
待处理 -
未知 -
失败
常见的流程是运行 kubectl get pods -w(-w 标志为命令添加监视功能),并查看有问题的 Pod 在 待处理 和 失败 之间转换。通常,发生的情况是 Pod(及其容器)正在启动并拉取镜像——这就是 待处理 状态,因为健康检查尚未开始。
一旦初始探测超时(正如我们在前一节中看到的可配置的),第一个探测失败。这可能持续几秒钟甚至几分钟,具体取决于失败阈值设置的高低,状态仍然固定在Pending。
最终,我们达到了失败阈值,我们的 Pod 状态转为Failed。此时,会有两种可能发生,决策完全基于 PodSpec 中的 RestartPolicy,可以是Always、Never或者OnFailure。如果 Pod 失败且restartPolicy为Never,则 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 将被启动。
摘要
在本章中,我们了解到 Pods 是我们在 Kubernetes 中处理的最基本的构建块。对 Pods 及其所有微妙之处有深入的理解非常重要,因为在 Kubernetes 上所有的计算都使用 Pods 作为构建块。现在可能已经很明显了,但 Pods 是非常小、个体化的东西,不是很结实。在 Kubernetes 上以单个 Pod 运行应用程序而没有控制器是一个糟糕的决定,你的 Pod 出现任何问题都会导致停机。
在下一章中,我们将看到如何通过使用 Pod 控制器一次运行多个应用程序副本来防止这种情况发生。
问题
-
你如何使用命名空间来分隔应用程序环境?
-
Pod 状态显示为
Unknown的可能原因是什么? -
限制 Pod 内存资源的原因可能是什么?
-
如果运行在 Kubernetes 上的应用程序经常在失败的探测重启 Pod 之前无法及时启动,你应该调整哪种探测类型?就绪性、存活性还是启动?
进一步阅读
-
Kubernetes 官方文档:
kubernetes.io/docs/home/ -
Kubernetes 逆天之路:
github.com/kelseyhightower/kubernetes-the-hard-way
第二部分:在 Kubernetes 上配置和部署应用程序
在本节中,你将学习如何在 Kubernetes 上配置和部署应用程序,以及如何提供存储并将应用程序暴露到集群外部。
本书的这一部分包含以下章节:
-
第四章,扩展和部署你的应用程序
-
第五章,服务和入口——与外部世界的通信
-
第六章,Kubernetes 应用程序配置
-
第七章,Kubernetes 上的存储
-
第八章,Pod 放置控制
第四章:扩展和部署您的应用程序
本章中,我们将学习用于运行应用程序和控制 Pods 的高级 Kubernetes 资源。首先,我们将讨论 Pod 的缺点,然后介绍最简单的 Pod 控制器 —— ReplicaSets。接下来,我们将介绍 Deployments,这是最流行的 Kubernetes 应用程序部署方法。然后,我们将介绍一些特殊资源,帮助您部署特定类型的应用程序 —— 水平 Pod 自动扩展器、DaemonSets、StatefulSets 和 Jobs。最后,我们将通过一个完整的示例展示如何在 Kubernetes 上运行一个复杂的应用程序。
本章将涵盖以下主题:
-
理解 Pod 的缺点及其解决方案
-
使用 ReplicaSets
-
控制 Deployments
-
利用水平 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 保持一定数量的 Pod 实例。如果某个实例失败,ReplicaSet 会启动一个新的实例来替代它。
其次,Deployments 本身控制一个 ReplicaSet。Deployments 是 Kubernetes 中运行应用程序时最常见的控制器,它们使得通过对 ReplicaSet 进行滚动更新轻松升级应用程序。
Horizontal Pod Autoscalers 通过允许应用程序根据性能指标自动扩展到不同数量的实例,将 Deployments 提升到一个新水平。
最后,存在一些特殊的控制器,在某些情况下可能非常有价值:
-
DaemonSets,运行应用程序的一个实例在每个节点上,并维护它们
-
StatefulSets,用于保持 Pod 的身份静态,以帮助运行有状态的工作负载
-
Jobs,启动后运行直到完成,然后在指定数量的 Pods 上关闭
无论是像 ReplicaSet 这样的默认 Kubernetes 控制器,还是自定义控制器(例如,PostgreSQL Operator),控制器的实际行为应该是容易预测的。标准控制循环的简化视图大致如下图所示:

图 4.1 – Kubernetes 控制器的基本控制循环
正如你所看到的,控制器不断地检查预期集群状态(我们希望有七个此应用的 Pod)与当前集群状态(我们现在有五个此应用的 Pod 正在运行)之间的差异。当预期状态与当前状态不符时,控制器会通过 API 采取行动,将当前状态修正为与预期状态一致。
到目前为止,你应该明白为什么在 Kubernetes 中控制器是必要的:单独的 Pod 并不是一个足够强大的基本元素,无法提供高度可用的应用程序。接下来我们将讨论最简单的这种控制器:ReplicaSet。
使用 ReplicaSets
ReplicaSet 是最简单的 Kubernetes Pod 控制器资源。它们替代了较旧的 ReplicationController 资源。
ReplicaSet 和 ReplicationController 之间的主要区别在于,ReplicationController 使用一种更基础的选择器—决定哪些 Pods 应该被控制的过滤器。
虽然 ReplicationController 使用基于简单等式的(key=value)选择器,ReplicaSet 使用具有多种可能格式的选择器,如 matchLabels 和 matchExpressions,本章将讨论这些格式。
重要提示
除非你有充分的理由,否则没有必要使用 ReplicationController,直接使用 ReplicaSet 即可。
ReplicaSet 允许我们告知 Kubernetes 维护一个特定 Pod 规格的特定数量的 Pods。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 spec 中还有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 的工作方式依赖于它——它是一个核心实现为 selector 的控制器。ReplicaSet 的任务是确保与其 selector 匹配的正在运行的 Pod 数量正确。
假设,举个例子,你有一个正在运行你的应用程序MyApp的 Pod。这个 Pod 使用selector键标记为App=MyApp。
现在,假设你想创建一个 ReplicaSet 来添加额外的三个实例,以扩展你现有的应用程序。你创建一个具有相同 selector 的 ReplicaSet,并指定三个副本,计划总共运行四个实例,因为你已经有一个在运行。
一旦你启动 ReplicaSet,会发生什么?你会发现运行该应用程序的 Pod 总数是三个,而不是四个。这是因为 ReplicaSet 具有接管孤立 Pod 并将其纳入控制的能力。
当 ReplicaSet 启动时,它会发现已经存在一个与其selector键匹配的 Pod。根据需要的副本数量,ReplicaSet 将关闭现有的 Pod 或启动新的与selector匹配的 Pod,以确保正确的副本数。
模板
template部分包含 Pod,并支持与 Pod YAML 一样的所有字段,包括 metadata 部分和 spec 本身。大多数其他控制器遵循这种模式——它们允许你在更大的控制器 YAML 中定义 Pod spec。
你现在应该理解了 ReplicaSet spec 的各个部分及其作用。接下来,我们将实际使用我们的 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
在清理过集群后,我们来继续了解更复杂的控制器——Deployments。
控制 Deployments
尽管 ReplicaSet 包含了运行高可用性应用所需的大部分功能,但大多数情况下,你会想要使用 Deployments 来在 Kubernetes 上运行应用。
与 ReplicaSets 相比,Deployments 有一些优势,并且它们实际上是通过拥有和控制 ReplicaSet 来工作的。
Deployment 的主要优势在于它允许你指定一个 rollout 程序——即应用升级是如何部署到 Deployment 中各个 pod 的。这使你可以轻松配置控制,以阻止错误的升级。
在我们复习如何执行此操作之前,先看一下 Deployment 的完整规格:
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 设置,我们可以告诉 Deployment 以何种方式升级应用,可以选择 RollingUpdate 或 Recreate。
Recreate 是一种非常基础的部署方法:Deployment 中的所有 Pods 会同时被删除,然后使用新版本创建新的 Pods。Recreate 并未提供多少控制,以应对不良的部署——如果新 Pods 出现无法启动的情况,我们将面临一个完全无法工作的应用。
而使用 RollingUpdate,Deployment 更新的速度较慢,但控制得更好。首先,新应用将逐步滚动发布,一次一个 Pod。我们可以为 maxSurge 和 maxUnavailable 设置值来调优策略。
滚动更新是这样工作的——当 Deployment 规格更新为新的 Pod 容器版本时,Deployment 会一次删除一个 Pod,创建一个带有新应用版本的新 Pod,等待新 Pod 注册为 Ready(根据就绪检查确定),然后继续下一个 Pod。
maxSurge 和 maxUnavailable 参数允许你加快或减慢这一过程。maxUnavailable 让你可以调整在发布过程中不可用的 Pod 的最大数量。它可以是百分比或固定数值。maxSurge 让你可以调整每次最多可以创建超过 Deployment 副本数的 Pod 数量。像 maxUnavailable 一样,它可以是百分比或固定数值。
下图展示了 RollingUpdate 过程:

图 4.2 – Deployment 的 RollingUpdate 过程
如你所见,RollingUpdate 过程遵循几个关键步骤。Deployment 尝试逐个更新 Pods,只有一个 Pod 成功更新后,更新才会进行到下一个 Pod。
使用命令式命令控制 Deployments
正如我们所讨论的,我们可以通过简单地更新 YAML 文件来改变 Deployment,使用声明式方法。然而,Kubernetes 还为我们提供了一些 kubectl 中的特殊命令,用于控制 Deployment 的各个方面。
首先,Kubernetes 允许我们手动缩放一个 Deployment——也就是说,我们可以编辑应该运行的副本数量。
要将 myapp-deployment 缩放到五个副本,我们可以运行以下命令:
kubectl scale deployment myapp-deployment --replicas=5
同样地,如果需要,我们可以将myapp-deployment回滚到旧版本。为了演示这一点,首先让我们手动编辑我们的 Deployment,使用新版本的容器:
Kubectl set image deployment myapp-deployment myapp-container=busybox:1.2 –record=true
该命令告诉 Kubernetes 将我们 Deployment 中的容器版本更改为 1.2。然后,我们的 Deployment 将按照前述图示中的步骤滚动更新。
现在,假设我们想回到更新容器镜像版本之前的版本。我们可以使用 rollout undo 命令轻松实现:
Kubectl rollout undo deployment myapp-deployment
在我们之前的案例中,我们只有两个版本,初始版本和更新容器的版本,但如果有其他版本,我们可以在 undo 命令中这样指定:
Kubectl rollout undo deployment myapp-deployment –to-revision=10
这应该能让你看出为什么 Deployments 如此有价值——它们让我们能够精确控制应用程序新版本的发布过程。接下来,我们将讨论一个与 Deployments 和 ReplicaSets 协同工作的智能缩放器。
利用水平 Pod 自动缩放器
正如我们所看到的,Deployments 和 ReplicaSets 允许你指定在某一时间点应该有多少个副本可用。然而,这两个结构都不支持自动缩放——它们必须手动进行缩放。
水平 Pod 自动缩放器 (HPA) 通过作为一个更高级的控制器,提供这种功能,能够根据 CPU 和内存使用等指标来改变 Deployment 或 ReplicaSet 的副本数。
默认情况下,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 定义为扩展我们之前创建的 Deployment,myapp-deployment。
对于调优参数,我们使用基于默认 CPU 利用率的扩展,因此可以使用 targetCPUUtilizationPercentage 来定义每个运行我们应用程序的 Pod 的目标 CPU 利用率。如果我们 Pod 的平均 CPU 使用率超过 70%,HPA 会扩展 Deployment 规格,如果使用率长时间低于该值,它会将 Deployment 缩减。
一个典型的扩容事件如下所示:
-
一个 Deployment 的平均 CPU 使用率超过了三个副本的 70%。
-
HPA 控制循环会注意到 CPU 利用率的增加。
-
HPA 会通过新的副本数量来编辑 Deployment 规格。这个数量是根据 CPU 利用率计算的,目的是使每个节点的 CPU 使用率保持在 70% 以下的稳定状态。
-
Deployment 控制器启动一个新的副本。
-
该过程会重复,以便扩展或缩减 Deployment。
总结来说,HPA 会跟踪 CPU 和内存利用率,并在超出边界时启动扩容事件。接下来,我们将回顾 DaemonSets,它提供了一种非常特定类型的 Pod 控制器。
实现 DaemonSets
从现在开始直到本章结束,我们将回顾更多适用于具有特定要求的应用程序运行的细分选项。
我们将从 DaemonSets 开始,它类似于 ReplicaSets,区别在于每个节点的副本数固定为一个副本。这意味着集群中的每个节点在任何时候都会保持一个应用程序副本处于活动状态。
重要提示
需要记住的是,在没有额外的 Pod 调度控制(如污点或节点选择器)的情况下,此功能仅会在每个节点上创建一个副本,后者我们将在第八章中详细介绍,Pod 调度控制。
对于典型的 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 的顺序和身份,即使这些 Pods 被重新调度到新的节点上。
例如,在一个包含 3 个副本的 StatefulSet 中,Pod 1、Pod 2 和 Pod 3 会始终存在,并且这些 Pods 会在 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,每个 Pod 都有一个数字标识符来表示其身份。这个特性对于有状态应用程序(如数据库集群)非常有用。在 Kubernetes 上运行数据库集群时,主节点与副本节点的身份非常重要,我们可以使用 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 使用中可以得知,多个命令参数必须作为字符串数组传递。
为了创建一个可以并行运行的作业(也就是说,多个副本同时运行作业),你需要以一种能够在结束进程之前确定作业是否完成的方式开发应用代码。为了做到这一点,每个作业实例需要包含代码,确保它执行正确的批量任务部分,并防止重复工作。
有几种应用模式可以启用此功能,包括互斥锁和工作队列。此外,代码需要检查整个批量任务的状态,这可以通过更新数据库中的一个值来处理。一旦作业代码发现大任务已完成,它应该退出。
完成上述操作后,你可以使用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 会跟踪作业的完成次数。你仍然可以为这种情况设置并行性,但如果没有设置,它默认会是 1。
这个规格将运行一个作业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 的 CronJob 触发 Kubernetes 作业,它提供了一个额外的抽象层,可以用于触发例如每晚的批处理作业。
Kubernetes 中的 CronJob 使用非常典型的 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 点创建一个与我们之前的作业规格完全相同的作业。为了快速回顾 cron 时间表示法,了解我们凌晨 1 点作业的语法,继续阅读。如果需要更全面的 cron 表示法回顾,请查阅man7.org/linux/man-pages/man5/crontab.5.html。
cron 表示法由五个值组成,用空格分隔。每个值可以是数字、字符或组合。每个值代表一个时间值,格式如下,从左到右:
-
分钟
-
小时
-
每月的某一天(例如
25) -
月份
-
星期几(例如,
3= 星期三)
上述 YAML 假设是一个非并行的 CronJob。如果我们想增加 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 – 多层应用程序架构
如您所见,我们的应用程序架构包含一个运行 .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,然后讨论了具有更具体用途的控制器,如 HPA、Jobs、CronJobs、StatefulSets 和 DaemonSets。最后,我们将所有学习内容结合起来,实现在 Kubernetes 上运行复杂应用的过程。
在下一章中,我们将学习如何使用 Services 和 Ingress 将我们的应用(现在已经高可用地运行)暴露给外界。
问题
-
ReplicaSet 和 ReplicationController 有什么区别?
-
Deployment 相较于 ReplicaSet 有什么优势?
-
Job 的一个好的使用场景是什么?
-
为什么 StatefulSets 更适合有状态工作负载?
-
我们如何通过 Deployments 支持金丝雀发布流程?
进一步阅读
-
官方 Kubernetes 文档:
kubernetes.io/docs/home/ -
Kubernetes Job 资源文档:
kubernetes.io/docs/concepts/workloads/controllers/job/ -
FluentD DaemonSet 安装文档:
github.com/fluent/fluentd-kubernetes-daemonset -
Kubernetes The Hard Way:
github.com/kelseyhightower/kubernetes-the-hard-way
第五章:服务和 Ingress——与外界通信
本章包含了 Kubernetes 提供的允许应用程序彼此通信,以及与集群外部资源通信的方法的全面讨论。你将学习到 Kubernetes 服务资源及其所有可能的类型——ClusterIP、NodePort、LoadBalancer 和 ExternalName——以及如何实现它们。最后,你将学习如何使用 Kubernetes Ingress。
在本章中,我们将讨论以下主题:
-
理解 Services 和集群 DNS
-
实现 ClusterIP
-
使用 NodePort
-
设置 LoadBalancer 服务
-
创建 ExternalName 服务
-
配置 Ingress
技术要求
为了运行本章中详细说明的命令,你需要一台支持 kubectl 命令行工具并且有一个正常工作的 Kubernetes 集群的计算机。查看 第一章,与 Kubernetes 通信,以了解几种快速启动和运行 Kubernetes 的方法,并查看如何安装 kubectl 工具的说明。
本章中使用的代码可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter5。
理解 Services 和集群 DNS
在前几章中,我们讨论了如何使用包括 Pods、Deployments 和 StatefulSets 在内的资源,在 Kubernetes 上有效运行应用程序。然而,许多应用程序(如 web 服务器)需要能够接受来自其容器外部的网络请求。这些请求可能来自其他应用程序或从设备访问公共互联网。
Kubernetes 提供了多种资源类型,用于处理允许集群外部和内部资源访问运行在 Pods、Deployments 等上的应用程序的各种场景。
这些分为两大资源类型:Services 和 Ingress:
-
服务有多个子类型——ClusterIP、NodePort 和 LoadBalancer——通常用于从集群内部或外部提供对单一应用程序的简单访问。
-
Ingress 是一种更高级的资源,它创建一个控制器,用于处理基于路径名和主机名的路由,指向集群内部运行的各种资源。Ingress 通过使用规则将流量转发到 Services。你需要使用 Services 来使用 Ingress。
在开始介绍我们第一种服务资源类型之前,让我们先回顾一下 Kubernetes 如何在集群内部处理 DNS。
集群 DNS
让我们首先讨论 Kubernetes 中哪些资源默认会获得自己的 DNS 名称。Kubernetes 中的 DNS 名称仅限于 Pods 和 Services。Pod 的 DNS 名称包含多个部分,结构类似子域名。
在 Kubernetes 中运行的 Pod 的典型 完全限定域名 (FQDN) 如下所示:
my-hostname.my-subdomain.my-namespace.svc.my-cluster-domain.example
让我们从最右边开始逐步分解:
-
my-cluster-domain.example对应于集群 API 自身配置的 DNS 名称。根据用于设置集群的工具和其运行的环境,这可以是外部域名或内部 DNS 名称。 -
svc是一个部分,即使在 Pod DNS 名称中也会出现 - 所以我们可以假设它会出现。然而,正如您很快将看到的那样,您通常不会通过它们的完全限定域名访问 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,我们可以讨论如何将其转换为服务代理。
服务代理类型
Services,尽可能简单地解释,提供了一个抽象层,用于将请求转发到运行应用程序的一个或多个 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
正如你所看到的,服务会交替发送请求到不同的 Pod。第一个请求发送到 Pod A,第二个请求发送到 Pod B,第三个请求发送到 Pod C,然后它会循环往复。现在我们知道服务是如何处理请求的,让我们从 ClusterIP 开始,回顾主要的服务类型。
实现 ClusterIP
ClusterIP 是一种简单的服务类型,暴露在集群内的内部 IP 上。这种类型的服务无法从集群外部访问。让我们来看一下服务的 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 资源一样,我们有 metadata 块,其中包含我们的 name 值。正如我们在讨论 DNS 时提到的,name 值是你可以从集群中的其他地方访问服务的方式。正因如此,ClusterIP 是一个很好的选择,适用于仅需通过集群内其他 Pods 访问的服务。
接下来,我们有 Spec,它由三大部分组成:
-
首先,我们有
type,它对应于我们的服务类型。由于默认类型是ClusterIP,如果您想使用 ClusterIP 服务,实际上不需要指定类型。 -
接下来,我们有
selector。我们的selector由键值对组成,这些键值对必须与相关 Pods 元数据中的标签匹配。在这种情况下,我们的服务将寻找标签为app=web-application和environment=staging的 Pods 来转发流量。 -
最后,我们有
ports块,在这里我们可以将服务上的端口映射到 Pods 上的targetPort号。在这种情况下,服务上的端口80(HTTP 端口)将映射到应用程序 Pod 上的端口8080。可以在服务上打开多个端口,但当打开多个端口时,name字段是必需的。
接下来,让我们深入了解 protocol 选项,因为这些对我们讨论服务端口非常重要。
协议
在我们之前的 ClusterIP 服务中,我们选择了 TCP 作为协议。Kubernetes 当前(截至版本 1.19)支持几种协议:
-
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 成为在负载均衡器服务不可行或无法使用时,对外部应用程序进行访问的一个不错的选择。
NodePort 听起来就像它的名字——这种类型的服务在集群中的每个节点上打开一个端口,可以通过该端口访问服务。该端口默认位于30000-32767范围内,并且在创建服务时会自动链接。
这是我们 NodePort 服务的 YAML 文件:
nodeport-service.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 服务的唯一区别是服务类型——然而,值得注意的是,我们在 ports 部分指定的端口 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 文件如下:
manual-nodeport-service.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 服务
如你所见,虽然服务可以在集群中的每个节点(节点 A、节点 B 和节点 C)上访问,但网络请求仍会在所有节点的 Pods(Pod A、Pod B 和 Pod C)之间进行负载均衡,而不仅仅是访问的节点。这是一种有效的方式,确保应用程序可以从任何节点进行访问。然而,在使用云服务时,你已经有了一系列工具来在服务器之间分发请求。下一个服务类型 LoadBalancer 让我们可以在 Kubernetes 上使用这些工具。
设置 LoadBalancer 服务
LoadBalancer 是 Kubernetes 中一种特殊的服务类型,它根据集群运行的位置配置一个负载均衡器。例如,在 AWS 中,Kubernetes 会配置一个弹性负载均衡器。
重要提示
要查看所有负载均衡服务及其配置,请查看 Kubernetes 服务文档中的kubernetes.io/docs/concepts/services-networking/service/#loadbalancer。
与ClusterIP或 NodePort 不同,我们可以以云特定的方式修改负载均衡服务的功能。通常,这是通过在服务的 YAML 文件中使用注释块来完成的——正如我们之前讨论过的,这只是一些键值对。要查看 AWS 的配置方式,我们来看一下负载均衡服务的规格:
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
虽然我们可以在没有任何注释的情况下创建负载均衡器,但支持的 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/。
最后,通过负载均衡服务,我们已经覆盖了你可能最常用的服务类型。然而,对于那些服务本身运行在 Kubernetes 外部的特殊情况,我们可以使用另一种服务类型:ExternalName。
创建 ExternalName 服务
类型为 ExternalName 的服务可以用来代理那些实际上并没有在你的集群中运行的应用程序,同时仍然保持服务作为一个抽象层,可以随时更新。
让我们设定一下场景:你有一个在 Azure 上运行的遗留生产应用程序,你希望从集群内访问它。你可以通过myoldapp.mydomain.com来访问这个遗留应用程序。然而,你的团队目前正在将这个应用程序容器化,并在 Kubernetes 上运行,而这个新版本现在正在你的dev命名空间环境中工作。
不必让你的其他应用程序根据环境与不同的地方进行通信,你可以始终指向一个名为my-svc的服务,无论是在生产(prod)命名空间还是开发(dev)命名空间中。
在dev中,这个服务可以是一个ClusterIP服务,指向你新容器化的应用程序所在的 Pod。以下 YAML 展示了正在开发中的、容器化服务应该如何工作:
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命名空间中,这个服务将成为一个ExternalName服务:
externalname-service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-svc
namespace: prod
spec:
type: ExternalName
externalName: myoldapp.mydomain.com
由于我们的ExternalName服务实际上并没有将请求转发到 Pod,因此我们不需要选择器。相反,我们指定一个ExternalName,即我们希望服务指向的 DNS 名称。
以下图示展示了ExternalName服务如何在此模式中使用:

图 5.3 – ExternalName 服务配置
在前面的图示中,我们的EC2 运行传统应用程序是一个外部 AWS 虚拟机,位于集群外部。我们的Service B类型是ExternalName,将请求路由到该虚拟机。这样,我们的Pod C(或者集群中的任何其他 Pod)可以通过 ExternalName 服务的 Kubernetes DNS 名称,轻松访问外部的传统应用程序。
使用ExternalName,我们已经完成了对所有 Kubernetes 服务类型的回顾。接下来,我们将介绍一种更复杂的应用暴露方式——Kubernetes Ingress 资源。
配置 Ingress
如本章开头所提到的,Ingress 提供了一种细粒度的机制,用于将请求路由到集群内。Ingress 并不取代服务,而是增强它们的功能,如基于路径的路由。为什么需要这个?有很多原因,包括成本。一个包含 10 条路径指向ClusterIP服务的 Ingress,比为每条路径创建一个新的负载均衡器服务便宜得多——而且它保持了简单易懂。
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 控制器是一组 Pods,每当创建一个新的 Ingress 资源(一个自定义的 Kubernetes 资源)时,它会自动更新 Nginx 配置。除了 Ingress 控制器,我们还需要一种方式将请求路由到 Ingress 控制器——这被称为入口点。
Ingress 入口点
默认的 nginx-ingress 安装还会创建一个单一的 Service,用于将请求发送到 Nginx 层,在此时 Ingress 规则接管。根据你配置 Ingress 的方式,这可以是一个 LoadBalancer 或 NodePort Service。在云环境中,你可能会使用云 LoadBalancer Service 作为集群 Ingress 的入口点。
Ingress 规则和 YAML
现在我们已经启动了 Ingress 控制器,可以开始配置我们的 Ingress 规则了。
让我们从一个简单的示例开始。我们有两个服务,service-a 和 service-b,我们希望通过 Ingress 在不同的路径上公开它们。一旦你的 Ingress 控制器和任何相关的弹性负载均衡器(假设我们在 AWS 上运行)创建完成,首先让我们通过以下步骤创建我们的服务:
-
首先,让我们看一下如何在 YAML 中创建 Service A。我们将文件命名为
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 -
你可以通过运行以下命令创建我们的 Service A:
kubectl apply -f service-a.yaml -
接下来,让我们创建我们的 Service 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 -
通过运行以下命令创建我们的 Service B:
kubectl apply -f service-b.yaml -
最后,我们可以创建我们的 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 传入流量的 host 请求头。然后,我们有两个路径,/a 和 /b,它们分别指向我们之前创建的两个 ClusterIP 服务。为了以图形化的方式展示这个配置,我们来看一下下图:

图 5.4 – Kubernetes Ingress 示例
如你所见,我们简单的基于路径的规则使得网络请求能够直接路由到正确的 Pods。这是因为nginx-ingress使用 Service 选择器获取 Pod IP 列表,但并不直接通过 Service 与 Pods 通信。而是,Nginx(在这种情况下)的配置会随着新的 Pod IP 上线而自动更新。
host 值其实并不是必需的。如果你省略它,任何通过 Ingress 传入的流量,无论 host 请求头是什么(除非它匹配其他指定 host 的规则),都会按照规则路由。以下 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 定义仍会将流量引导到基于路径的路由规则。
同样,你也可以根据主机头(host header)将流量拆分成多个独立的分支路径,如下所示:
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 提供的各种方法,用于将运行在集群上的应用程序暴露给外部世界。主要的方法是 Services 和 Ingress。在 Services 中,你可以使用 ClusterIP Services 进行集群内路由,使用 NodePort 直接通过节点的端口访问服务。LoadBalancer Services 允许你使用现有的云负载均衡系统,而 ExternalName Services 则允许你将请求路由到集群外部的资源。
最后,Ingress 提供了一个强大的工具,可以通过路径在集群中路由请求。要实现 Ingress,你需要在集群上安装一个第三方或开源的 Ingress 控制器。
在下一章中,我们将讨论如何使用两种资源类型(ConfigMap 和 Secret)将配置信息注入到运行在 Kubernetes 上的应用程序中。
问题
-
对于仅在集群内访问的应用程序,你会使用哪种类型的 Service?
-
如何判断一个 NodePort 服务在哪个端口上激活?
-
为什么 Ingress 比单纯使用 Services 更具成本效益?
-
除了支持遗留应用程序外,ExternalName Services 在云平台上还有什么用处?
进一步阅读
- 来自 Kubernetes 文档的云服务提供商信息:
kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/
第六章: 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。
使用最佳实践配置容器化应用程序
到目前为止,我们已经了解了如何有效地部署(见 第四章,扩展与部署您的应用程序)和暴露(见 第五章,服务与 Ingress – 与外界通信)容器化应用程序在 Kubernetes 上。这足以在 Kubernetes 上运行非平凡的无状态容器化应用程序。然而,Kubernetes 还提供了更多的工具来进行应用程序配置和 Secrets 管理。
由于 Kubernetes 运行容器,您可以始终配置您的应用程序使用嵌入在 Dockerfile 中的环境变量。但这绕过了像 Kubernetes 这样的编排工具的某些实际价值。我们希望能够在不重建 Docker 镜像的情况下更改我们的应用程序容器。为此,Kubernetes 为我们提供了两个关注配置的资源:ConfigMaps 和 Secrets。我们首先来看一下 ConfigMaps。
理解 ConfigMaps
在生产环境中运行应用程序时,开发人员希望能够快速、轻松地注入应用程序配置。实现这一目标的方式有很多——从使用一个单独的配置服务器进行查询,到使用环境变量或环境文件。这些策略在安全性和可用性方面各有不同。
对于容器化应用程序,环境变量通常是最简便的方式——但以安全的方式注入这些变量可能需要额外的工具或脚本。在 Kubernetes 中,ConfigMap 资源让我们可以以灵活、简便的方式做到这一点。ConfigMap 允许 Kubernetes 管理员指定并注入配置数据,可以是文件,也可以是环境变量。
对于像密钥这样的高度敏感信息,Kubernetes 提供了另一种类似的资源——Secrets。
理解 Secrets
Secrets 是指需要以稍微更安全的方式存储的额外应用配置项——例如,限制 API 的主密钥、数据库密码等。Kubernetes 提供了一种叫做 Secret 的资源,它以编码方式存储应用配置数据。这本身并不使 Secret 更加安全,但 Kubernetes 通过不自动在 kubectl get 或 kubectl describe 命令中打印出秘密信息来尊重保密的概念。这样可以防止 Secret 被意外记录到日志中。
为了确保 Secrets 确实保密,必须在集群上启用静态加密来保护 secret 数据——我们将在本章稍后讨论如何做到这一点。自 Kubernetes 1.13 起,这个功能允许 Kubernetes 管理员防止 Secrets 在 etcd 中以未加密形式存储,并限制 etcd 管理员的访问权限。
在深入讨论 Secrets 之前,我们先来讨论一下 ConfigMaps,它更适合存储非敏感信息。
实现 ConfigMap
ConfigMap 提供了一种简单的方式来存储和注入运行在 Kubernetes 上的容器的应用配置数据。
创建 ConfigMap 很简单——它们提供了两种方式来注入应用配置数据:
-
作为环境变量注入
-
作为文件注入
虽然第一个选项只是通过容器环境变量在内存中操作,但后一个选项涉及到某些卷的概念——卷是 Kubernetes 的一种存储介质,将在下一章中进行详细介绍。我们现在简要回顾一下,并将其作为对卷的介绍,卷的内容将在下一章 第七章 Kubernetes 存储 中展开。
在使用 ConfigMap 时,通过命令式的 Kubectl 命令来创建它可能会更简单。创建 ConfigMap 有几种可能的方法,这也会导致数据存储和访问方式的不同。第一种方式是直接从文本值创建,如我们接下来将看到的。
来自文本值
通过命令从文本值创建 ConfigMap 的方法如下:
kubectl create configmap myapp-config --from-literal=mycategory.mykey=myvalue
上述命令创建了一个名为 myapp-config 的 configmap,它有一个键,叫做 mycategory.mykey,值为 myvalue。你也可以创建一个包含多个键值对的 ConfigMap,如下所示:
kubectl create configmap myapp-config2 --from-literal=mycategory.mykey=myvalue
--from-literal=mycategory.mykey2=myvalue2
前面的命令将导致 ConfigMap 在 data 部分中包含两个值。
要查看你的 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:
-
让我们从创建文件开始,我们将其命名为
env.properties:myconfigid=1125 publicapikey=i38ahsjh2 -
然后,我们可以通过运行以下命令来创建我们的 ConfigMap:
kubectl create configmap my-config-map --from-file=env.properties -
为了检查我们的
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 方法,env 文件中的数据在运行 kubectl describe 时很容易查看。这也意味着我们可以直接将 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 的名称以及要查看的键。
如我们在本章开始时的 使用最佳实践配置容器化应用程序 部分所提到的,ConfigMaps 默认不安全,它们的数据以明文形式存储。为了增加安全性,我们可以使用 Secrets 替代 ConfigMaps。
使用 Secrets
Secrets 的工作方式与 ConfigMaps 非常相似,不同之处在于它们以编码文本(具体来说是 Base64)而非明文形式存储。
因此,创建 Secret 与创建 ConfigMap 非常相似,但有一些关键的不同之处。首先,命令式创建 Secret 会自动对 Secret 中的数据进行 Base64 编码。首先,让我们看看如何通过一对文件命令式地创建 Secret。
从文件
首先,让我们尝试从文件创建一个 Secret(这对于多个文件也有效)。我们可以使用 kubectl create 命令来实现:
> echo -n 'mysecretpassword' > ./pass.txt
> kubectl create secret generic my-secret --from-file=./pass.txt
这应该会产生以下输出:
secret "my-secret" created
现在,让我们通过 kubectl describe 查看我们的 Secret 长什么样:
> kubectl describe secrets/db-user-pass
这个命令应该会产生以下输出:
Name: my-secret
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
pass.txt: 16 bytes
如你所见,describe 命令显示了 Secret 中包含的字节数,以及它的类型 Opaque。
创建 Secret 的另一种方式是使用声明式方法手动创建它。接下来让我们看看如何操作。
手动声明式方法
当从 YAML 文件声明式地创建 Secret 时,您需要使用编码工具(如 Linux 上的 base64 管道)预先对要存储的数据进行编码。
让我们在这里使用 Linux 的 base64 命令对我们的密码进行编码:
> echo -n 'myverybadpassword' | base64
bXl2ZXJ5YmFkcGFzc3dvcmQ=
现在,我们可以使用 Kubernetes YAML 规格声明性地创建我们的 Secret,并将其命名为 secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
data:
dbpass: bXl2ZXJ5YmFkcGFzc3dvcmQ=
我们的 secret.yaml 规格包含我们创建的 Base64 编码字符串。
要创建 Secret,请运行以下命令:
kubectl create -f secret.yaml
现在你已经知道如何创建 Secrets。接下来,让我们学习如何将 Secret 挂载供 Pod 使用。
将 Secret 挂载为卷
挂载 Secrets 与挂载 ConfigMaps 非常相似。首先,让我们看看如何将 Secret 作为卷(文件)挂载到 Pod。
让我们看一下我们的 Pod 规格。在这个例子中,我们运行一个示例应用程序来测试我们的 Secret。这里是 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 运行时对 Secret 进行任何更改。其他方面与挂载 Secret 的方式相同。
同样,在下一章节第七章,在 Kubernetes 上存储中,我们将深入研究卷。
将 Secret 作为环境变量挂载
类似于文件挂载,我们可以像 ConfigMap 挂载一样将我们的 Secret 作为环境变量挂载。
让我们再看一个 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 服务(包括亚马逊的 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,允许使用以下提供者:
-
Identity:无加密。
-
Aescbc:推荐的加密提供者。
-
Secretbox:比 Aescbc 更快,也更新。
-
Aesgcm:请注意,您需要自行实现 Aesgcm 的密钥轮换。
-
Kms:与 Vault 或 AWS KMS 等第三方 Secrets 存储一起使用。
若要查看完整列表,请参阅 https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#providers。当列表中添加多个提供者时,Kubernetes 将使用第一个配置的提供者来加密对象。在解密时,Kubernetes 将逐个尝试使用每个提供者解密 - 如果都不起作用,将返回错误。
一旦我们创建了一个 secret(可以参考我们之前的任何示例),并且我们的 EncryptionConfig 是激活的,我们就可以检查我们的 Secrets 是否真正加密了。
检查你的 Secrets 是否已加密
检查你的 secret 是否真正加密在 etcd 中的最简单方法是直接从 etcd 获取值并检查加密前缀:
-
首先,让我们使用
base64创建一个 secret 密钥:> echo -n 'secrettotest' | base64 c2VjcmV0dG90ZXN0 -
创建一个名为
secret_to_test.yaml的文件,内容如下:apiVersion: v1 kind: Secret metadata: name: secret-to-test type: Opaque data: myencsecret: c2VjcmV0dG90ZXN0 -
创建 Secret:
kubectl apply -f secret_to_test.yaml -
创建好 Secret 后,让我们通过直接查询它来检查它是否已在
etcd中加密。你不应该经常直接查询etcd,但如果你有访问用于引导集群的证书,这是一个简单的过程:k8s:enc:kms:v1:azurekmsprovider. -
现在,检查一下 Secret 是否已经正确解密(它仍然是编码过的),通过
kubectl来查看:> kubectl get secrets secret-to-test -o yaml
输出应该是 myencsecret: c2VjcmV0dG90ZXN0,这是我们未加密的、编码后的 Secret 值:
> echo 'c2VjcmV0dG90ZXN0' | base64 --decode
> secrettotest
成功!
我们现在已经在集群中运行加密了。让我们找出如何移除它。
禁用集群加密
我们也可以相对容易地从我们的 Kubernetes 资源中移除加密。
首先,我们需要使用一个空白的加密配置 YAML 文件重新启动 Kubernetes API 服务器。如果你是自行搭建的集群,这应该比较容易,但在 EKS 或 AKS 上,手动操作是不可能的。你需要按照云服务提供商的特定文档来禁用加密。
如果你是自行搭建集群或使用了 Kops 或 Kubeadm 等工具,那么你可以在所有主节点上重新启动 kube-apiserver 进程,并使用以下 EncryptionConfiguration:
encryption-reset.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- identity: {}
重要提示
注意,身份提供者不需要是唯一列出的提供者,但它需要是第一个,因为正如我们之前提到的,Kubernetes 使用第一个提供者来加密 etcd 中的新对象或更新对象。
现在,我们将手动重新创建所有的 Secrets,到时它们将自动使用身份提供者(未加密):
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
此时,我们所有的 Secrets 都是未加密的!
总结
在本章中,我们回顾了 Kubernetes 提供的注入应用配置的方法。首先,我们回顾了一些配置容器化应用的最佳实践。接着,我们复习了 Kubernetes 提供的第一个方法——ConfigMaps,并介绍了如何创建和挂载它们到 Pods。最后,我们介绍了 Secrets,在加密后,它们是一种更安全的方式来处理敏感配置。现在,你应该已经掌握了为应用提供安全和不安全配置值所需的所有工具。
在下一章中,我们将深入探讨一个我们已经触及过的主题——挂载 Secrets 和 ConfigMaps——Kubernetes 卷资源,以及更广泛的 Kubernetes 存储。
问题
-
Secrets 和 ConfigMaps 之间有哪些区别?
-
Secrets 是如何编码的?
-
从普通文件创建 ConfigMap 和从环境文件创建 ConfigMap 之间的主要区别是什么?
-
如何在 Kubernetes 上保证 Secrets 的安全?为什么它们默认情况下不安全?
进一步阅读
- 有关 Kubernetes 数据加密配置的信息可以在官方文档中找到,网址是
kubernetes.io/docs/tasks/administer-cluster/encrypt-data/。
第七章:Kubernetes 上的存储
在本章中,我们将学习如何在 Kubernetes 上提供应用存储。我们将回顾 Kubernetes 上的两种存储资源:卷和持久卷。卷适用于短暂的数据需求,而持久卷对于在 Kubernetes 上运行任何有状态的工作负载是必需的。通过本章中所学的技能,你将能够以多种方式和环境配置在 Kubernetes 上运行的应用存储。
在本章中,我们将讨论以下主题:
-
理解卷和持久卷之间的区别
-
使用卷
-
创建持久卷
-
持久卷声明
技术要求
为了运行本章详细介绍的命令,你需要一台支持kubectl命令行工具并且有一个正常运行的 Kubernetes 集群的计算机。有关如何快速启动 Kubernetes 的几种方法以及如何安装kubectl工具的说明,请参见第一章,与 Kubernetes 通信。
本章中使用的代码可以在本书的 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 将创建一个 Pod 和一个类型为 emptyDir 的卷。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 下的列表项)和卷的 mount(volumeMounts 下的列表项)。
每个挂载项包含一个名称,该名称对应于 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 的生命周期绑定。这意味着持久卷可以在 Pods 之间共享并且只要集群在运行,就可以被重复使用。因此,这种模式更适合用于外部存储,如 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 命令处理该文件时,应会创建一个新的自动生成的 Persistent Volume(PV)。如果你的 AWS 云服务商配置正确,这将导致创建一个新的类型为 GP2 并启用了加密的 EBS 卷。
在将我们基于 EBS 的持久卷附加到 Pod 之前,让我们确认 EBS 卷是否在 AWS 中正确创建。
为此,我们可以导航到 AWS 控制台,确保我们在与 EKS 集群相同的区域。然后,进入 服务 > EC2,并在左侧菜单下的 弹性块存储 中点击 卷。在此部分中,我们应该看到一行自动生成的与我们 PVC 声明相同大小(1 GiB)的卷。它应该属于 GP2 类,并且应启用加密。让我们看看在 AWS 控制台中这将是什么样子:

图 7.1 – 自动创建的 AWS 控制台 EBS 卷
如你所见,我们在 AWS 中成功创建了我们的动态生成的 EBS 卷,启用了加密,并分配了 gp2 卷类型。现在我们已经创建了卷,并确认它已在 AWS 中创建,我们可以将它附加到 Pod 上。
将持久化卷声明(PVC)附加到 Pod
现在我们有了 PersistentVolume 和 PersistentVolumeClaim,我们可以将它们附加到 Pod 中进行使用。这个过程与附加 ConfigMap 或 Secret 非常相似——这也有道理,因为 ConfigMap 和 Secret 本质上是卷的一种类型!
查看允许我们将加密的 EBS 卷附加到 Pod 的 YAML 文件,并将其命名为 pod-with-attachment.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,并通过我们的声明将 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 创建一个由云存储提供的持久化卷。然而,你可能在本地运行 Kubernetes,或者使用 minikube 在笔记本电脑上运行。让我们看看一些可以替代使用的持久化卷子类型。
没有云存储的持久化卷
我们之前的示例假设你在云环境中运行 Kubernetes,并且可以使用云平台提供的存储服务(如 AWS EBS 等)。然而,这并不总是可能的。你可能在数据中心环境中运行 Kubernetes,或者在专用硬件上运行。
在这种情况下,有许多为 Kubernetes 提供存储的潜在解决方案。一种简单的方案是将卷类型更改为 hostPath,它在节点现有存储设备上工作,创建持久卷。这在例如 minikube 上运行时非常有用,但它并没有像 AWS EBS 那样提供强大的抽象。对于一个具有类似云存储工具(如 EBS)功能的本地工具,我们可以使用 Ceph 和 Rook。欲了解完整文档,请参阅 Rook 文档(它也会教你使用 Ceph),访问 rook.io/docs/rook/v1.3/ceph-quickstart.html。
Rook 是一个流行的开源 Kubernetes 存储抽象层。它可以通过多种提供者提供持久卷,如 EdgeFS 和 NFS。在这种情况下,我们将使用 Ceph,这是一个开源存储项目,提供对象存储、块存储和文件存储。为了简化操作,我们将使用块模式。
在 Kubernetes 上安装 Rook 实际上非常简单。我们将带你从安装 Rook,到设置 Ceph 集群,最后在我们的集群上配置持久卷。
安装 Rook
我们将使用 Rook GitHub 仓库提供的典型 Rook 安装默认设置。这可以根据使用案例进行高度定制,但它将使我们能够快速为工作负载设置块存储。请按照以下步骤操作:
-
首先,让我们克隆 Rook 仓库:
> git clone --single-branch --branch master https://github.com/rook/rook.git > cd cluster/examples/kubernetes/ceph -
我们的下一步是创建所有相关的 Kubernetes 资源,包括几个 自定义资源定义(CRD)。我们将在后续章节中讲解这些,但现在可以认为它们是 Rook 特有的新的 Kubernetes 资源,超出了典型的 Pods、Services 等。要创建常见资源,请运行以下命令:
> kubectl apply -f ./common.yaml -
接下来,让我们启动 Rook 操作符,它将处理为特定 Rook 提供者(在这种情况下为 Ceph)配置所有必要资源的工作:
> kubectl apply -f ./operator.yaml -
在下一步之前,请通过以下命令确保 Rook 操作符 Pod 实际正在运行:
> kubectl -n rook-ceph get pod -
一旦 Rook Pod 处于
Running状态,我们就可以设置我们的 Ceph 集群了!这个 YAML 配置也在我们从 Git 克隆的文件夹中。使用以下命令创建它:> kubectl create -f cluster.yaml
这个过程可能需要几分钟。Ceph 集群由几种不同类型的 Pod 组成,包括操作符、对象存储设备(OSD)和管理器。
为了确保我们的 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 启动,你可以使用rookctl和ceph命令来检查集群状态(有关具体操作,请参阅 Rook 文档)。
rook-ceph-block 存储类
现在集群正常运行,我们可以创建将由 PV 使用的存储类。我们将这个存储类命名为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 规范定义了我们的StorageClass和CephBlockPool资源。正如我们在本章之前提到的,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
成功!
虽然我们刚刚使用了 Rook 和 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
在这种情况下,我们将元数据和数据复制到至少两个池中以提高可靠性,这在metadataPool和dataPool块中进行配置。我们还使用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
这个持久卷引用了我们在ReadWriteMany模式下的新rook-cephfs存储类——我们请求分配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通过volumes下的persistentVolumeClaim块引用了我们的ReadWriteMany持久卷声明。部署后,所有的 Pod 现在都可以读写同一个持久卷。
完成此步骤后,你应该能很好地理解如何创建持久卷并将其附加到 Pod。
总结
本章中,我们回顾了在 Kubernetes 上提供存储的两种方法——卷和持久卷。首先,我们讨论了这两种方法的区别:卷与 Pod 的生命周期绑定,而持久卷的生命周期直到它们或集群被删除。然后,我们了解了如何实现卷并将其附加到 Pod。最后,我们将学习从卷到持久卷的扩展,探讨了如何使用多种不同类型的持久卷。这些技能将帮助你在各种环境中——从本地到云——为应用程序分配持久存储和非持久存储。
在下一章中,我们将暂时脱离应用程序的关注点,讨论如何控制 Kubernetes 中 Pod 的调度位置。
问题
-
卷和持久卷有什么区别?
-
什么是
StorageClass,它与卷有什么关系? -
如何在创建 Kubernetes 资源(例如持久卷)时自动配置云资源?
-
在哪些使用场景下,你认为使用卷而非持久卷会受到限制?
深入阅读
请参考以下链接了解更多信息:
-
Ceph 存储快速入门(适用于 Rook):
github.com/rook/rook/blob/master/Documentation/ceph-quickstart.md -
Rook 工具箱:
rook.io/docs/rook/v0.7/toolbox.html -
云服务提供商:
kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/
第八章:Pod 放置控制
本章描述了在 Kubernetes 中控制 Pod 放置的各种方式,并解释了为何实现这些控制可能是个好主意。Pod 放置指的是控制 Pod 在 Kubernetes 中被调度到哪个节点。我们从简单的控制方式如节点选择器开始,然后介绍更复杂的工具,如污点和容忍,最后讨论两个 beta 功能——节点亲和性和 Pod 间亲和性/反亲和性。
在前几章中,我们学习了如何在 Kubernetes 上最佳运行应用 Pod——通过使用部署协调和扩展它们,通过 ConfigMaps 和 Secrets 注入配置,通过持久卷添加存储等方式。
然而,在所有这些过程中,我们始终依赖 Kubernetes 调度器将 Pod 放置在最优的节点上,但并未向调度器提供关于 Pod 的太多信息。到目前为止,我们已经为 Pod 添加了资源限制和请求(Pod 规范中的resource.requests和resource.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)——这意味着没有其他应用程序会在底层硬件和虚拟机监控器上运行。这与其他典型的云提供商虚拟机不同,后者可能会让多个客户共享一台物理机器。
对于某些数据法规要求,为了保持合规,必须采用这种专用租赁级别。为了满足这一需求,你可以使用 Pod 放置控制来确保相关应用程序只在具有专用租赁的节点上运行,同时通过在更典型的虚拟机上运行控制平面来降低成本。
多租户集群
如果你正在运行一个有多个租户的集群(例如通过命名空间分隔),你可以使用 Pod 放置控制来为某个租户保留特定的节点或节点组,以物理或其他方式将其与集群中的其他租户隔离。这类似于 AWS 或 Azure 中的专用硬件概念。
多个故障域
尽管 Kubernetes 已经通过允许在多个节点上调度工作负载来提供高可用性,但也可以扩展这一模式。我们可以创建自己的 Pod 调度策略,考虑到跨多个节点的故障域。处理这种情况的一种很好的方法是通过 Pod 或节点亲和性或反亲和性特性,我们将在本章后面讨论这些内容。
目前,假设我们的集群部署在裸金属环境中,每个物理机架有 20 个节点。如果每个机架都有自己的专用电源连接和备份,那么可以将其视为一个故障域。当电源连接发生故障时,机架上的所有机器都会失败。因此,我们可能希望鼓励 Kubernetes 将两个实例或 Pod 分别运行在不同的机架/故障域上。下图显示了应用程序如何跨故障域运行:

图 8.1 - 故障域
如图所示,当应用程序 Pod 分布在多个故障域而不仅仅是同一故障域的多个节点上时,即使故障域 1发生故障,我们仍然可以保持正常运行。App A - Pod 1和App B - Pod 1位于同一(红色)故障域。然而,如果该故障域(Rack 1)出现故障,我们仍然能在Rack 2上保持每个应用程序的副本。
我们在这里使用“鼓励”一词,因为可以将 Kubernetes 调度程序中的某些功能配置为硬性要求或尽力而为。
这些示例应该能帮助你深入理解一些潜在的高级调度控制用例。
现在让我们讨论实际的实现,逐一讨论每个调度工具集。我们将从最简单的节点选择器开始。
使用节点选择器和节点名称
节点选择器是 Kubernetes 中一种非常简单的调度控制方式。每个 Kubernetes 节点可以在元数据块中用一个或多个标签进行标记,Pod 可以指定节点选择器。
要标记现有节点,可以使用kubectl label命令:
> kubectl label nodes node1 cpu_speed=fast
在这个例子中,我们将node1节点标记为cpu_speed,值为fast。
现在,假设我们有一个真正需要快速 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,所以如果它因为某种原因当前不接受 Pod,Pod 就不会被调度。
对于稍微更精细的调度控制,让我们继续讨论污点和容忍度。
实现污点和容忍度
Kubernetes 中的污点和容忍度类似于反向节点选择器。与节点通过拥有正确的标签吸引 Pod(这些标签被选择器消耗)不同,我们对节点施加污点,这样就会排斥所有 Pod 不被调度到该节点上,然后我们为 Pod 添加容忍度,这样它们就能被调度到这些有污点的节点上。
正如本章开头所提到的,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
前两个效果,NoSchedule和NoExecute提供的是强制性效果——也就是说,像节点选择器一样,只有两种可能性,要么 Pod 上有容忍度(我们马上会看到),要么 Pod 不会被调度。NoExecute通过驱逐所有具有容忍度的 Pod 来扩展了这个基本功能,而NoSchedule则允许现有的 Pod 留在原地,但防止任何没有容忍度的新 Pod 加入。
另一方面,PreferNoSchedule给 Kubernetes 调度器提供了一些灵活性。它告诉调度器尽量为 Pod 找到没有未容忍污点的节点,但如果没有找到这样的节点,就继续调度 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 提供一个容忍——我们可以这样做:
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 可以是 Equal 或 Exists。对于 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 起作用的方式相同。它可以包含与污点效果相同的值——NoSchedule、NoExecute 和 PreferNoSchedule。
拥有 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
在这种情况下,已经在一个带有 slow 污点的节点上运行的 Pod,当污点和容忍被执行时,将在该节点上停留 60 秒,然后才会被重新调度到另一个节点。
多个污点和容忍
当 Pod 和节点上有多个污点或容忍时,调度器会检查它们所有的污点。这里没有OR逻辑运算符——如果节点上的任何一个污点没有在 Pod 上找到匹配的容忍,该 Pod 就不会被调度到该节点(PreferNoSchedule除外,在这种情况下,调度器会尽量避免将 Pod 调度到该节点)。即使节点上有六个污点,Pod 容忍其中五个,它仍然不会因为 NoSchedule 污点而被调度,并且它仍会因为 NoExecute 污点而被驱逐。
对于一个提供更细粒度控制的工具,让我们来看看节点亲和性。
使用节点亲和性控制 Pod
正如你可能已经猜到的,污点和容忍——虽然比节点选择器更灵活——仍然无法解决某些用例,并且通常只允许一种过滤模式,你可以使用 Exists 或 Equals 匹配特定的污点。在更高级的用例中,你可能需要更灵活的节点选择方法——而 亲和性 是 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
这两种类型的功能分别与NoSchedule和PreferNoSchedule的工作原理直接对应。
使用requiredDuringSchedulingIgnoredDuringExecution节点亲和性
对于requiredDuringSchedulingIgnoredDuringExecution,Kubernetes 将永远不会调度没有与节点匹配的条款的 Pod。
对于preferredDuringSchedulingIgnoredDuringExecution,它会尽量满足软要求,但如果无法满足,它仍然会调度 Pod。
节点亲和性相较于节点选择器和污点容忍的真正优势在于选择器中你可以实现的实际表达式和逻辑。
requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution的功能差异较大,因此我们将分别进行回顾。
对于我们的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)的节点上调度。我们不再将节点分为像slow和fast这样的广泛组别,而是可以在规格中更加细致。
接下来,我们来看另一个节点亲和性类型——preferredDuringSchedulingIgnoredDuringExecution。
使用preferredDuringSchedulingIgnoredDuringExecution节点亲和性
这种语法稍有不同,并为我们提供了更多的细粒度来影响这个软要求。让我们来看一个实现这一点的 Pod 规格 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,我们可以为每个条目分配一个weight值,并指定一个相关的偏好,这可以再次是一个matchExpressions块,里面包含多个使用相同key-operator-values语法的表达式。
weight值是这里的关键区别。由于preferredDuringSchedulingIgnoredDuringExecution是一个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运行在最佳节点上的过程中,我们决定仅实现软要求。如果没有快速节点存在,我们仍然希望我们的应用能够被调度并运行。为此,我们指定了两个偏好——一个cpu_speed超过 3(3 GHz)的节点和一个内存速度超过 4(4 GHz)的节点。
由于我们的应用更多是受 CPU 限制而非内存限制,我们决定适当加权我们的偏好。在这种情况下,cpu_speed的weight为90,而memory_speed的weight为10。
因此,任何满足我们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 功能。
一个额外的节点亲和性类型 – requiredDuringSchedulingRequiredDuringExecution – 将在未来版本中添加此功能。到 Kubernetes 1.19 为止,这个功能尚不存在。
接下来,我们将查看 Pod 之间的亲和性和反亲和性,它们定义了 Pods 之间的亲和性规则,而不是节点的规则。
使用 Pod 之间的亲和性和反亲和性
Pod 之间的亲和性和反亲和性允许你根据节点上已经存在的其他 Pods 来决定 Pods 应该如何运行。由于集群中的 Pods 数量通常远大于节点数量,而且一些 Pod 亲和性和反亲和性规则可能相对复杂,因此如果你在许多节点上运行大量 Pods,这项功能可能会对你的集群控制平面造成很大负担。因此,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
同样,类似于节点亲和性,我们可以有一个或多个选择器——它们被称为 labelSelector,因为我们选择的是 Pods 而不是节点。matchExpressions 功能与节点亲和性相同,但 Pod 亲和性新增了一个名为 topologyKey 的全新键。
topologyKey 本质上是一个选择器,它限制了调度器查看是否有相同选择器的其他 Pods 正在运行的范围。这意味着 Pod 亲和性不仅仅意味着同一节点上具有相同类型(选择器)的其他 Pods;它还可以意味着多个节点的组合。
让我们回到本章开始时的故障域示例。在那个示例中,每个机架都是一个故障域,每个机架上有多个节点。为了将这个概念扩展到 topologyKey,我们可以给每个机架上的节点贴上 rack=1 或 rack=2 标签。然后我们可以像在 YAML 中那样使用 topologyKey 机架,指定调度器应该检查所有运行在具有相同 topologyKey 的节点上的 Pods(在这个例子中,意味着在同一机架上的 Node 1 和 Node 2 上的所有 Pods),以便应用 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 共享同一节点。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 亲和性一样,我们可以使用preferredDuringSchedulingIgnoredDuringExecution或requireDuringSchedulingIgnoredDuringExecution。我们甚至使用与 Pod 亲和性相同的语法来选择选择器。
唯一的实际语法差异是在affinity键下使用podAntiAffinity。
那么,这个 YAML 文件是做什么的呢?在这种情况下,我们正在向调度器推荐(一个软需求),它应该尝试将 Pod 调度到一个节点,在该节点或任何其他具有相同rack标签值的节点上,不运行带有hunger标签值为 4 或 5 的 Pod。我们在告诉调度器,尽量不要将这个 Pod 和任何更多饥饿的 Pod 放在同一个区域。
该功能为我们提供了一种通过故障域来分离 Pod 的绝佳方式——我们可以将每个机架指定为一个域,并为其指定具有自身选择器的反亲和性。这样,调度器将在一个优选的亲和性下,尽力将 Pod 的克隆调度到不同的故障域节点上,从而在发生故障域故障时提高应用的可用性。
我们甚至可以将 Pod 的亲和性与反亲和性结合使用。让我们看看这将如何运作。
结合亲和性和反亲和性
这正是一个可能给你的集群控制平面带来不必要负担的情况。将 Pod 的亲和性与反亲和性结合使用,可以允许非常复杂的规则传递给 Kubernetes 调度器,而调度器则肩负着实现这些规则的艰巨任务。
让我们看看结合这两个概念的 Deployment 规格的 YAML。请记住,亲和性和反亲和性是应用于 Pod 的概念——但通常我们不会指定没有控制器的 Pod,如 Deployment 或 ReplicaSet。因此,这些规则是在 Deployment YAML 的 Pod 规格级别应用的。为了简洁起见,我们仅展示 Deployment 的 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
在这段代码中,我们告诉调度器将 Deployment 中的 Pod 视作:Pod 必须被调度到一个带有rack标签的节点上,该节点或任何其他带有相同rack标签值的节点上,必须有一个带有app=hungry-label-cache标签的 Pod。
其次,调度器必须尝试将 Pod 调度到一个带有rack标签的节点上,前提是该节点或任何其他带有相同rack标签值的节点上,没有运行带有app=other-hungry-app标签的 Pod。
简而言之,我们希望hungry-app的 Pod 与hungry-app-cache的 Pod 在同一拓扑中运行,并且如果可能的话,我们不希望它们与other-hungry-app的 Pod 处于同一拓扑。
由于巨大的能力带来了巨大的责任,而我们在 Pod 亲和性和反亲和性工具方面的能力强大且可能会降低性能,Kubernetes 确保对它们的使用设置了一些限制,以防止奇怪的行为或显著的性能问题。
Pod 的亲和性和反亲和性限制
亲和性和反亲和性的最大限制是你不能使用空的topologyKey。如果不限制调度器将什么视为单一拓扑类型,可能会发生一些意想不到的行为。
第二个限制是,默认情况下,如果你使用的是反亲和性的硬性版本——requiredOnSchedulingIgnoredDuringExecution,你不能仅使用任何标签作为topologyKey。
Kubernetes 只允许使用 kubernetes.io/hostname 标签,这意味着如果你使用 required 反亲和性时,每个节点上只能有一个拓扑结构。对于 prefer 反亲和性或任何亲和性(即使是 required 的)而言,并没有这样的限制。虽然可以更改这一功能,但这需要编写自定义准入控制器——我们将在第十二章《Kubernetes 安全性与合规性》和第十三章《使用 CRD 扩展 Kubernetes》中讨论这一内容。
到目前为止,我们对放置控制的讨论并没有涉及命名空间。然而,对于 Pod 的亲和性和反亲和性,命名空间确实是相关的。
Pod 亲和性和反亲和性命名空间
由于 Pod 的亲和性和反亲和性会根据其他 Pod 的位置导致行为变化,因此命名空间在决定哪些 Pods 适用于亲和性或反亲和性时是一个相关的因素。
默认情况下,调度器只会查看创建带有亲和性或反亲和性 Pod 的命名空间。在我们之前的所有示例中,我们并没有指定命名空间,因此将使用默认命名空间。
如果你想添加一个或多个命名空间,以便 Pods 会影响亲和性或反亲和性,你可以使用以下 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 放置规则。我们了解到,有“硬性”要求和“软性”规则,后者会尽力调度 Pods,但不一定会阻止违反规则的 Pods 被放置。我们还了解了你可能希望实现调度控制的一些原因,比如现实中的故障域和多租户。
我们了解到,有一些简单的方法可以影响 Pod 的放置,比如节点选择器和节点名称——此外,还有更高级的方法,如污点和容忍度,Kubernetes 本身也默认使用这些方法。最后,我们发现 Kubernetes 提供了一些高级工具,用于节点和 Pod 的亲和性和反亲和性,这使我们能够为调度器创建复杂的规则集。
在下一章中,我们将讨论 Kubernetes 的可观察性。我们将学习如何查看应用程序日志,并使用一些优秀的工具实时查看集群中发生的事情。
问题
-
节点选择器和节点名称字段的区别是什么?
-
Kubernetes 如何使用系统提供的污点(taints)和容忍(tolerations)?出于什么原因?
-
在使用多种类型的 Pod 亲和性或反亲和性时,为什么需要小心?
-
如何在为三层 Web 应用程序跨多个失败区域平衡可用性与性能优化的情况下使用节点或 Pod 亲和性和反亲和性?可以举例说明。
进一步阅读
- 欲了解更详细的默认系统污点和容忍解释,请访问
kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/#taint-based-evictions。
第三部分:在生产环境中运行 Kubernetes
在本节中,您将了解 Kubernetes 的第二天运维工作、CI/CD 的最佳实践、如何定制和扩展 Kubernetes,以及更广泛的云原生生态系统的基础知识。
本书的这一部分包括以下章节:
-
第九章,Kubernetes 上的可观察性
-
第十章,Kubernetes 故障排除
-
第十一章,Kubernetes 上的模板代码生成和 CI/CD
-
第十二章,Kubernetes 的安全性和合规性
第九章:Kubernetes 上的可观察性
本章深入探讨了在生产环境中运行 Kubernetes 时,强烈推荐实施的功能。首先,我们讨论在 Kubernetes 等分布式系统中的可观察性。然后,我们查看 Kubernetes 内置的可观察性堆栈及其实现的功能。最后,我们学习如何通过额外的可观察性、监控、日志记录和度量基础设施来补充内置的可观察性工具。您将在本章中学到的技能将帮助您将可观察性工具部署到 Kubernetes 集群中,并使您能够了解您的集群(以及在其上运行的应用程序)如何运作。
本章将涵盖以下主题:
-
理解 Kubernetes 上的可观察性
-
使用默认的可观察性工具 – 度量、日志记录和仪表盘
-
实现生态系统中的最佳实践
首先,我们将了解 Kubernetes 提供的开箱即用的可观察性工具和流程。
技术要求
为了运行本章中详细介绍的命令,您需要一台支持 kubectl 命令行工具的计算机,并且需要一个正常工作的 Kubernetes 集群。请参阅 第一章,与 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 或第三方可观察性解决方案为 Kubernetes 提供的众多指标和日志中,我们可以缩小范围,找出最可能对集群造成重大问题的一些指标。你应该将这些要点放在最终使用的任何可观察性解决方案的核心位置。首先,让我们来看一下 CPU 使用率与集群健康之间的关系。
节点 CPU 使用率
Kubernetes 集群中各节点的 CPU 使用情况是一个非常重要的指标,应该在你的可观察性解决方案中时刻关注。我们在前面的章节中讨论了 Pods 如何定义 CPU 使用的资源请求和限制。然而,当限制设置高于集群的最大 CPU 容量时,节点仍然有可能会过度分配 CPU 使用情况。此外,运行我们控制平面的主节点也可能遇到 CPU 容量问题。
CPU 满载的工作节点可能会表现不佳,或限制在 Pods 上运行的工作负载。如果没有为 Pods 设置限制,或者一个节点的总 Pod 资源限制超过了其最大容量,即使其总资源请求较低,也很容易发生这种情况。CPU 满载的主节点可能会影响调度器、kube-apiserver 或任何其他控制平面组件的性能。
一般来说,工作节点和主节点的 CPU 使用情况应该在你的可观察性解决方案中可见。最好通过结合使用指标(例如在图表解决方案如 Grafana 上,你将在本章稍后学习到)以及集群中各节点的高 CPU 使用率警报来实现。
内存使用情况也是一个极其重要的指标,类似于 CPU。
节点内存使用情况
与 CPU 使用情况类似,内存使用情况是观察集群内情况时非常重要的一个指标。内存使用情况可以通过 Pod 资源限制被过度分配——并且许多与 CPU 使用情况相同的问题可能适用于集群中的主节点和工作节点。
同样,警报和指标的结合对于集群内存使用情况的可见性至关重要。我们将在本章稍后学习一些工具来实现这一点。
对于下一个主要的可观察性内容,我们将关注日志,而不是指标。
控制平面日志
Kubernetes 控制平面组件在运行时会输出日志,这些日志可以用于深入了解集群操作。这些日志也能显著帮助故障排除,正如我们在第十章《故障排除 Kubernetes》中所看到的那样。Kubernetes API 服务器、控制器管理器、调度器、kube proxy 和 kubelet 的日志,对于某些故障排除或可观察性原因来说,都非常有用。
应用程序日志
应用程序日志也可以被集成到 Kubernetes 的可观察性栈中——能够查看应用程序日志和其他指标一起,能够帮助运维人员非常有效。
应用程序性能指标
与应用程序日志一样,应用程序性能指标和监控与 Kubernetes 上应用程序的性能高度相关。在应用程序层面上的内存使用情况和 CPU 分析可以成为可观察性栈中的一个有价值的组成部分。
一般而言,Kubernetes 提供了应用监控和日志记录的数据基础设施,但避免提供如图表和搜索等更高级的功能。考虑到这一点,让我们回顾一下 Kubernetes 默认提供给我们的工具,以解决这些问题。
使用默认的可观察性工具
Kubernetes 提供了可观察性工具,即使没有添加任何第三方解决方案。这些原生的 Kubernetes 工具是许多更强大解决方案的基础,因此它们非常值得讨论。由于可观察性包括度量、日志、跟踪和警报,我们将依次讨论每个方面,首先聚焦于 Kubernetes 原生解决方案。首先,让我们讨论度量。
Kubernetes 的度量
通过简单运行kubectl describe pod,你可以获取大量关于应用的信息。我们可以看到关于 Pod 的规格、其所处的状态,以及影响其功能的关键问题。
假设我们的应用程序遇到了一些问题。具体来说,Pod 没有启动。为了进行调查,我们运行了 kubectl describe pod。提醒一下,第一章中提到的 kubectl 别名,kubectl describe pod 和 kubectl describe pods 是等效的。以下是 describe pod 命令的示例输出——我们已去掉除 Events(事件)信息之外的所有内容:

图 9.1 – 描述 Pod 事件输出
如你所见,Pod 无法调度是因为我们的节点内存已满!这应该是进一步调查的一个重要问题。
我们继续前进。通过运行 kubectl describe nodes,我们可以了解很多关于 Kubernetes 节点的信息。部分信息对于我们的系统性能非常相关。这里是另一个示例输出,这次来自 kubectl describe nodes 命令。为了避免输出过长,我们将聚焦于两个重要部分——Conditions(条件)和 Allocated resources(分配的资源)。首先,让我们回顾一下 Conditions(条件)部分:

图 9.2 – 描述节点条件输出
如你所见,我们已经包含了 kubectl describe nodes 命令输出中的 Conditions(条件)块。这是查看任何问题的好地方。正如我们在这里看到的,我们的节点确实存在问题。我们的 MemoryPressure(内存压力)条件为真,且 Kubelet 内存不足。难怪我们的 Pods 无法调度!
接下来,查看 Allocated resources(分配的资源)部分:
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%)
现在我们看到了些指标!看起来我们的 Pods 请求了过多的内存,导致了节点和 Pod 的问题。从这个输出中可以看出,Kubernetes 已经在默认情况下收集了有关节点的指标数据。如果没有这些数据,调度器就无法正常工作,因为维护 Pod 资源请求与节点容量之间的平衡是它最重要的功能之一。
然而,默认情况下,这些指标并不会显示给用户。它们实际上是由每个节点的 Kubelet 收集并传递给调度器,以便调度器可以执行其工作。幸运的是,我们可以通过将 Metrics Server 部署到集群中,轻松获取这些指标。
Metrics Server 是一个官方支持的 Kubernetes 应用程序,用于收集指标信息并将其通过 API 端点提供给使用。实际上,Metrics Server 是 Horizontal Pod Autoscaler 正常工作的必要条件,但根据 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 命令可以与 Pods 或 Nodes 一起使用,以查看内存和 CPU 使用量的详细信息。
让我们看一些示例用法。运行 kubectl top nodes 查看节点级别的指标。以下是该命令的输出:

图 9.3 – 节点指标输出
如你所见,我们可以看到 CPU 和内存使用情况的绝对值和相对值。
重要提示
CPU 核心的度量单位是 millcpu 或 millicores。1,000 millicores 等于一个虚拟 CPU。内存以字节为单位进行度量。
接下来,让我们来看一下 kubectl top pods 命令。使用 –namespace kube-system 标志运行它,可以查看 kube-system 命名空间中的 Pods。
为此,我们运行以下命令:
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 的集群,可以使用 journalctl CLI 工具来查看日志(有关更多信息,请参阅以下链接: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 proxy 的日志。
尽管一般来说,集群的健康状况受到 Kubernetes 主节点和工作节点组件健康状况的影响,但当然也需要关注你的应用日志。
应用日志
在 Kubernetes 上查找应用日志非常简单。在我们解释如何运作之前,先来看一个例子。
要查看特定 Pod 的日志,你可以使用 kubectl logs <pod_name> 命令。该命令的输出会显示写入容器 stdout 或 stderr 的任何文本。如果一个 Pod 有多个容器,你必须在命令中包含容器的名称:
kubectl logs <pod_name> <container_name>
从底层来看,Kubernetes 通过使用容器引擎的日志驱动程序来处理 Pod 日志。通常,任何写入 stdout 或 stderr 的日志都会被保存到每个节点的磁盘中的 /var/logs 文件夹。根据 Kubernetes 发行版的不同,日志轮转可能已设置,以防止日志占用节点磁盘空间过多。此外,Kubernetes 组件(如调度器、kubelet 和 kube-apiserver)也会将日志保存到节点磁盘空间,通常是在 /var/logs 文件夹中。需要注意的是,这种默认的日志记录能力非常有限——一个强大的可观察性栈肯定会包括第三方的日志转发解决方案,正如我们接下来将看到的那样。
接下来,为了进行 Kubernetes 的整体可观察性,我们可以使用 Kubernetes Dashboard。
安装 Kubernetes Dashboard
Kubernetes Dashboard 提供了 kubectl 的所有功能——包括查看日志和编辑资源——通过图形用户界面(GUI)实现。设置仪表板非常简单——我们来看看如何操作。
仪表板可以通过一个 kubectl apply 命令来安装。若要进行自定义,请访问 Kubernetes Dashboard 的 GitHub 页面:github.com/kubernetes/dashboard。
要安装 Kubernetes 仪表盘的版本,请运行以下kubectl命令,将<VERSION>标签替换为你想要的版本,基于你使用的 Kubernetes 版本(再次检查仪表盘的 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-forward或kubectl proxy来从本地计算机查看我们的仪表盘。
在这个例子中,我们将使用kubectl proxy命令,因为我们还没有在之前的例子中使用过它。
与kubectl port-forward命令不同,kubectl proxy命令只需要一个命令来代理到集群中运行的所有服务。它通过将 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 仪表盘登录
当我们部署并访问 Kubernetes 仪表盘时,首先会看到一个登录界面。我们可以创建一个服务账户(或使用我们自己的账户)来登录仪表盘,或者直接链接本地的Kubeconfig文件。通过使用特定服务账户的令牌登录 Kubernetes 仪表盘,仪表盘用户将继承该服务账户的权限。这使得我们能够指定用户在使用 Kubernetes 仪表盘时可以执行的操作类型——例如,只读权限。
现在我们创建一个全新的服务账户用于 Kubernetes 仪表盘。你可以自定义这个服务账户并限制它的权限,但现在我们将给予它管理员权限。为此,请按照以下步骤操作:
-
我们可以使用以下 Kubectl 命令以命令式方式创建服务账户:
kubectl create serviceaccount dashboard-user这将产生以下输出,确认我们已创建服务账户:
serviceaccount/dashboard-user created -
现在,我们需要将服务帐户与 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 -
运行此命令后,我们应该能够登录仪表盘!首先,我们只需要找到用于登录的 token。服务帐户的 token 作为 Kubernetes 密钥存储,因此我们来看看它是什么样的。运行以下命令查看我们的 token 存储在哪个密钥中:
kubectl get secrets在输出中,你应该能看到一个类似以下内容的密钥:
NAME TYPE DATA AGE dashboard-user-token-dcn2g kubernetes.io/service-account-token 3 112s -
现在,为了获取我们的 token 以登录仪表盘,我们只需要使用以下命令描述密钥内容:
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 > -
要登录仪表盘,复制
token旁边的字符串,将其粘贴到 Kubernetes 仪表盘登录页面的 token 输入框中,然后点击登录。你应该会看到 Kubernetes 仪表盘的概览页面! -
继续点击仪表盘,你应该能够看到所有与使用 kubectl 相同的资源,并且可以在左侧边栏按命名空间进行筛选。例如,这里是命名空间页面的视图:
![图 9.5 – Kubernetes 仪表盘详情]()
图 9.5 – Kubernetes 仪表盘详情
-
你还可以点击单独的资源,甚至使用仪表盘编辑这些资源,只要你用于登录的服务帐户具有适当的权限。
这是从部署详情页面编辑部署资源的视图:

图 9.6 – Kubernetes 仪表盘编辑视图
Kubernetes 仪表盘还允许你查看 Pod 日志,并深入了解集群中的许多其他资源类型。要了解仪表盘的全部功能,请查看前面提到的 GitHub 页面上的文档。
最后,为了完整讨论 Kubernetes 的默认可观察性,让我们来看一下警报功能。
Kubernetes 上的警报和跟踪
不幸的是,可观察性谜题的最后两部分——警报和跟踪——目前在 Kubernetes 中还不是原生功能。为了创建这类功能,让我们进入下一个部分——整合 Kubernetes 生态系统中的开源工具。
使用生态系统中的最佳工具增强 Kubernetes 的可观察性
正如我们所讨论的,虽然 Kubernetes 提供了强大的可视化功能基础,但通常由社区和供应商生态系统创建更高级的度量、日志、跟踪和警报工具。对于本书的目的,我们将重点关注完全开源、自托管的解决方案。由于许多这样的解决方案跨越多个可视化支柱(包括度量、日志、跟踪和警报),因此在回顾过程中,我们不会根据每个可视化支柱来分类这些解决方案,而是将每个解决方案分别回顾。
让我们从一个常用的度量和警报技术组合开始:Prometheus 和 Grafana。
介绍 Prometheus 和 Grafana
Prometheus 和 Grafana 是 Kubernetes 上典型的可视化技术组合。Prometheus 是一个时间序列数据库、查询层和警报系统,拥有许多集成,而 Grafana 是一个复杂的图表和可视化层,能与 Prometheus 集成。我们将带你了解如何安装和使用这些工具,首先从 Prometheus 开始。
安装 Prometheus 和 Grafana
在 Kubernetes 上安装 Prometheus 有多种方式,但大多数都使用 Deployments 来扩展服务。为了我们的目的,我们将使用 kube-prometheus 项目(github.com/coreos/kube-prometheus)。该项目包括一个 operator 和几个 自定义资源定义(CRDs)。它还会自动为我们安装 Grafana!
Operator 本质上是 Kubernetes 上的一个应用控制器(像其他应用一样部署在 Pod 中),它通过 Kubernetes API 发出命令来正确运行或操作其应用程序。
CRD 允许我们在 Kubernetes API 内建模自定义功能。我们将在第十三章《使用 CRD 扩展 Kubernetes》中学到更多关于 operators 和 CRDs 的内容,但现在只需将 operators 理解为创建 智能部署 的一种方式,其中应用程序能够自我管理并根据需要启动其他 Pods 和 Deployments,而 CRD 则是使用 Kubernetes 存储特定应用程序相关问题的一种方式。
要安装 Prometheus,首先我们需要下载一个发行版,具体版本可能会根据 Prometheus 的最新版本或你计划使用的 Kubernetes 版本有所不同:
curl -LO https://github.com/coreos/kube-prometheus/archive/v0.5.0.zip
接下来,使用任何工具解压文件。首先,我们需要安装 CRDs。通常,大多数 Kubernetes 工具的安装说明会要求你先在 Kubernetes 上创建 CRDs,因为任何使用 CRD 的额外配置都会在底层 CRD 尚未创建时失败。
让我们使用以下命令进行安装:
kubectl apply -f manifests/setup
我们需要等待几秒钟,直到 CRD(自定义资源定义)被创建。此命令还将为我们的资源创建一个monitoring命名空间。一旦一切准备就绪,让我们使用以下命令启动其余的 Prometheus 和 Grafana 资源:
kubectl apply -f manifests/
让我们来看看这个命令实际会创建什么。整个堆栈由以下组件组成:
-
Prometheus Deployment:Prometheus 应用程序的 Pods
-
Prometheus Operator:控制和操作 Prometheus 应用程序 Pods
-
Alertmanager Deployment:Prometheus 组件,用于指定和触发警报
-
Grafana:一个强大的可视化仪表盘
-
Kube-state-metrics agent:从 Kubernetes API 状态生成指标
-
Prometheus Node Exporter:将节点硬件和操作系统级别的指标导出到 Prometheus
-
Prometheus Adapter for Kubernetes Metrics:用于 Prometheus 吸收 Kubernetes 资源指标 API 和自定义指标 API 的适配器
这些组件一起将提供对我们集群的深度可视化,从命令平面到应用程序容器本身。
一旦堆栈创建完成(可以通过使用 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
如你所见,Prometheus UI 有一个 Graph 页面,这就是你在 图 9.4 中看到的内容。它还有自己的 UI 用于查看已配置的警报——但它不允许你通过 UI 创建警报。Grafana 和 Alertmanager 将帮助我们完成这项任务。
要执行查询,请导航到 PromQL——我们在本书中不会完全介绍它,但 Prometheus 文档是学习的好途径。你可以通过以下链接参考:prometheus.io/docs/prometheus/latest/querying/basics/。
为了展示如何运作,让我们输入一个基本的查询,如下所示:
kubelet_http_requests_total
此查询将列出每个 Node 上对 kubelet 发出的 HTTP 请求总数,按每个请求类别分类,如下图所示:

图 9.8 – HTTP 请求查询
你还可以通过点击图形标签查看请求的图形形式,位置在表格旁边,如下图所示:

图 9.9 – HTTP 请求查询 – 图形视图
这提供了前面截图中数据的时间序列图表视图。如你所见,图表功能相当简单。
Prometheus 还提供了一个警报标签,用于配置 Prometheus 警报。通常,这些警报是通过代码配置的,而不是使用警报标签的 UI,因此我们将跳过这一页面。如果你想了解更多信息,可以查阅官方 Prometheus 文档:prometheus.io/docs/alerting/latest/overview/。
让我们继续前进,进入 Grafana,在那里我们可以通过可视化扩展 Prometheus 强大的数据工具。
使用 Grafana
Grafana 提供了强大的可视化工具,可以实时更新多种支持的图表类型。我们可以将 Grafana 连接到 Prometheus,以便在 Grafana UI 上查看我们的集群指标图表。
要开始使用 Grafana,请执行以下操作:
-
我们将结束当前的端口转发(CTRL + C即可完成),并设置一个新的端口转发监听器,指向 Grafana UI:
Kubectl -n monitoring port-forward svc/grafana 3000:3000 -
再次访问
localhost:3000以查看 Grafana UI。你应该能够使用admin和admin登录,此时你可以更改初始密码,如下图所示:![图 9.10 – Grafana 修改密码屏幕]()
图 9.10 – Grafana 修改密码屏幕
-
登录后,你将看到以下屏幕。Grafana 默认没有配置任何仪表板,但我们可以通过点击屏幕截图中显示的+号轻松添加它们:
![图 9.11 – Grafana 主页面]()
图 9.11 – Grafana 主页面
-
每个 Grafana 仪表板包含一个或多个用于不同指标集的图表。要添加一个预配置的仪表板(而不是自己创建一个),请点击左侧菜单栏中的加号(+),然后点击导入。你应该会看到如下截图的页面:
![图 9.12 – Grafana 仪表板导入]()
图 9.12 – Grafana 仪表板导入
我们可以通过此页面添加仪表板,既可以使用 JSON 配置,也可以粘贴公共仪表板 ID。
-
你可以在
grafana.com/grafana/dashboards/315找到公共仪表板及其相关 ID。仪表板#315 是一个很好的 Kubernetes 入门仪表板—将其添加到标记为Grafana.com 仪表板的文本框中,然后点击加载。 -
然后,在下一页中,从Prometheus下拉菜单中选择Prometheus数据源,如果有多个数据源可用,可以通过该菜单进行选择。点击导入,仪表板应加载完成,显示如下截图:

图 9.13 – Grafana 仪表板
这个 Grafana 仪表板提供了关于网络、内存、CPU 和文件系统利用率的整体概览,并按 Pod 和容器进行拆分。它配置了实时图表,显示网络 I/O 压力、集群内存使用、集群 CPU 使用和集群文件系统使用——不过,最后一个选项可能不会启用,这取决于你安装 Prometheus 的方式。
最后,让我们看看 Alertmanager UI。
使用 Alertmanager
Alertmanager 是一个开源解决方案,用于管理由 Prometheus 警报生成的警报。我们之前已经作为栈的一部分安装了 Alertmanager——让我们看看它能做些什么:
-
首先,让我们使用以下命令对 Alertmanager 服务进行
port-forward:Kubectl -n monitoring port-forward svc/alertmanager-main 3000:9093 -
如常,访问
localhost:3000查看 UI,界面如下图所示。它与 Prometheus 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 栈的方法有很多,但 Kubernetes 的 GitHub 存储库本身提供了一些支持的 YAML 配置,因此我们就使用这些:
-
首先,使用以下命令克隆或下载 Kubernetes 存储库:
git clone https://github.com/kubernetes/kubernetes -
清单文件位于
kubernetes/cluster/addons文件夹中,具体在fluentd-elasticsearch下:cd kubernetes/cluster/addons对于生产工作负载,我们可能需要对这些清单文件进行一些更改,以便根据我们的集群需求进行定制配置,但在本教程中,我们将保留所有默认设置。让我们开始启动 EFK 栈的过程。
-
首先,让我们创建 Elasticsearch 集群。该集群作为 Kubernetes 上的 StatefulSet 运行,并提供一个服务。要创建集群,我们需要运行两个
kubectl命令:kubectl apply -f ./fluentd-elasticsearch/es-statefulset.yaml kubectl apply -f ./fluentd-elasticsearch/es-service.yaml重要说明
关于 Elasticsearch StatefulSet 的一个警告——默认情况下,每个 Pod 的资源请求是 3 GB 内存,因此如果你的 Node 没有足够的内存,你将无法按默认配置部署它。
-
接下来,让我们部署 FluentD 日志代理。这些代理将作为 DaemonSet 运行——每个 Node 一个——并将日志从 Node 转发到 Elasticsearch。我们还需要创建 ConfigMap YAML 文件,其中包含 FluentD 代理的基础配置。此配置可以进一步自定义,添加例如日志过滤器和新数据源等内容。
-
要安装代理和其配置的 DaemonSet,运行以下两个
kubectl命令:kubectl apply -f ./fluentd-elasticsearch/fluentd-es-configmap.yaml kubectl apply -f ./fluentd-elasticsearch/fluentd-es-ds.yaml -
现在我们已经创建了 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 -
一旦所有内容初始化完成,这可能需要几分钟,我们可以像访问 Prometheus 和 Grafana 一样访问 Kibana 用户界面。要检查我们刚刚创建的资源状态,可以运行以下命令:
kubectl get po -A -
一旦所有 FluentD、Elasticsearch 和 Kibana 的 Pods 都放在
addons文件夹中,更多信息请参考。 -
一旦确认我们的组件正常工作,我们可以使用
port-forward命令来访问 Kibana 用户界面。顺便提一下,我们的 EFK 堆栈组件将位于kube-system命名空间中——所以我们的命令需要反映这一点。那么,让我们使用以下命令:port-forward to your local machine's port 8080 from the Kibana UI. -
让我们在
localhost:8080上查看 Kibana 用户界面。它应该看起来像下面的样子,具体取决于你的版本和配置:![图 9.15 – 基本的 Kibana 用户界面]()
图 9.15 – 基本的 Kibana 用户界面
Kibana 提供了几种不同的功能,用于搜索和可视化日志、度量数据等。对于我们的目的来说,仪表板中最重要的部分是日志,因为我们在示例中仅将 Kibana 用作日志搜索 UI。
然而,Kibana 还有许多其他功能,其中一些与 Grafana 类似。例如,它包含一个完整的可视化引擎、应用性能监控(APM)功能和 Timelion,这是一个类似于 Prometheus 的 PromQL 的时间序列数据表达引擎。Kibana 的度量功能与 Prometheus 和 Grafana 相似。
-
为了让 Kibana 正常工作,我们首先需要指定一个索引模式。为此,点击可视化按钮,然后点击添加索引模式。从模式列表中选择一个选项,并选择当前日期的索引,然后创建索引模式。
现在我们已经设置好了,h:

图 9.16 – Discover 用户界面
当 Kibana 无法找到任何结果时,它会提供一组便捷的解决方案,包括查询示例,正如你在图 9.13中看到的那样。
现在你知道如何创建搜索查询后,你可以在Visualize页面根据查询创建可视化。这些可视化可以从多种可视化类型中选择,包括图形、图表等,并可以根据特定查询进行定制,如下图所示:

图 9.17 – 新的可视化
接下来,这些可视化可以组合成仪表盘。这类似于 Grafana,多个可视化可以添加到一个仪表盘中,然后保存并重复使用。
你还可以使用搜索栏进一步过滤仪表盘的可视化内容——非常方便!下图展示了如何将仪表盘与特定查询关联:

图 9.18 – 仪表盘 UI
如你所见,可以使用添加按钮为特定查询创建仪表盘。
接下来,Kibana 提供了一个名为Timelion的工具,这是一个时间序列可视化合成工具。它本质上允许你将不同的数据源合并成一个单一的可视化图表。Timelion 功能强大,但对其特性的全面讨论超出了本书的范围。下图展示了 Timelion UI——你可能会注意到它与 Grafana 有一些相似之处,因为这两套工具提供了非常相似的功能:

图 9.19 – Timelion UI
如你所见,在 Timelion 中,查询可以用来驱动实时更新的图表,和 Grafana 一样。
此外,尽管与本书关系较少,Kibana 提供了 APM 功能,这需要进一步的配置,特别是在 Kubernetes 环境下。在本书中,我们依赖 Prometheus 来获取此类信息,同时使用 EFK 堆栈来搜索我们应用程序的日志。
现在,我们已经介绍了用于度量和告警的 Prometheus 与 Grafana,以及用于日志记录的 EFK 堆栈,只有可观察性拼图的最后一块还未讲解。为了解决这个问题,我们将使用另一个出色的开源软件——Jaeger。
使用 Jaeger 实现分布式追踪
Jaeger 是一个开源的分布式追踪解决方案,兼容 Kubernetes。Jaeger 实现了 OpenTracing 规范,这是定义分布式追踪的一组标准。
Jaeger 提供了一个用于查看追踪信息的 UI,并与 Prometheus 集成。Jaeger 的官方文档可以在www.jaegertracing.io/docs/找到。由于自本书出版以来可能已有变化,建议始终查看文档以获取最新信息。
使用 Jaeger Operator 安装 Jaeger
为了安装 Jaeger,我们将使用 Jaeger 操作员,这是我们在本书中遇到的第一个操作员。在 Kubernetes 中,操作员只是创建自定义应用程序控制器的模式,这些控制器能与 Kubernetes 语言进行交互。这意味着,您不必部署应用程序所需的各种 Kubernetes 资源,而是可以部署一个单一的 Pod(或通常是单一的 Deployment),然后该应用程序将与 Kubernetes 进行通信,并为您启动所有其他所需的资源。操作员甚至可以进一步自我操作应用程序,在必要时进行资源更改。操作员可以非常复杂,但它们让我们作为终端用户更容易在 Kubernetes 集群上部署商业或开源软件。
要开始使用 Jaeger 操作员,我们需要为 Jaeger 创建一些初始资源,之后操作员将自动完成其余部分。安装 Jaeger 的先决条件是集群中必须安装nginx-ingress控制器,因为我们将通过它来访问 Jaeger UI。
首先,我们需要为 Jaeger 创建一个命名空间。我们可以通过kubectl create namespace命令来完成:
kubectl create namespace observability
现在我们的命名空间已经创建,我们需要创建一些CRDs供 Jaeger 和操作员使用。我们将在后续关于扩展 Kubernetes 的章节中详细讨论 CRDs,但现在可以将它们看作是利用 Kubernetes API 为应用程序构建自定义功能的一种方式。通过以下步骤,让我们安装 Jaeger:
-
要创建 Jaeger CRDs,请运行以下命令:
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml在我们创建了 CRDs 之后,操作员需要创建一些角色和绑定才能开始工作。
-
我们希望 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 -
现在,我们终于拥有了操作员所需的所有组件。让我们通过最后一个
kubectl命令安装操作员:kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml -
最后,使用以下命令检查操作员是否正在运行:
kubectl get deploy -n observability
如果操作员运行正常,你将看到类似以下的输出,并且会有一个可用的 Pod 用于部署:

图 9.20 – Jaeger 操作员 Pod 输出
现在我们的 Jaeger 操作员已经启动并运行,但 Jaeger 本身还没有运行。这是为什么呢?Jaeger 是一个非常复杂的系统,可以在不同的配置下运行,而操作员使得部署这些配置变得更加容易。
Jaeger 操作员使用一个名为Jaeger的 CRD 来读取 Jaeger 实例的配置,在此过程中,操作员将部署所有必要的 Pod 和其他 Kubernetes 资源。
Jaeger 可以以三种主要配置运行:AllInOne、Production 和 Streaming。关于这些配置的详细讨论超出了本书的范围(请查看之前分享的 Jaeger 文档链接),但我们将使用 AllInOne 配置。该配置将 Jaeger UI、Collector、Agent 和 Ingestor 合并到一个 Pod 中,并不包括任何持久化存储。这非常适合演示用途 – 若要查看适用于生产环境的配置,请查看 Jaeger 文档。
为了创建我们的 Jaeger 部署,我们需要将我们选择的配置告诉 Jaeger Operator。我们使用之前创建的 CRD – Jaeger CRD 来实现这一点。为此 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 应该会启动。我们可以使用以下命令,通过列出可观察性命名空间中的所有 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 服务输出
现在,您可以导航到集群的 Ingress 记录中列出的地址以查看 Jaeger UI。它应显示如下:

图 9.22 – Jaeger UI
如您所见,Jaeger UI 非常简单。顶部有三个标签页 – 搜索、比较和系统架构。我们将重点讨论搜索标签页,但有关其他两个标签页的更多信息,请查看 Jaeger 文档:www.jaegertracing.io。
Jaeger 的搜索页面允许我们根据多种输入条件搜索 trace。我们可以基于 trace 中包含的 Service、标签、持续时间等进行搜索。然而,目前我们的 Jaeger 系统中还没有任何数据。
这样做的原因是,即使我们已经启动并运行了 Jaeger,我们的应用仍然需要配置为将追踪发送到 Jaeger。这通常需要在代码或框架层面完成,超出了本书的范围。如果你想体验 Jaeger 的追踪功能,可以安装一个示例应用——请参见 Jaeger 文档页面 www.jaegertracing.io/docs/1.18/getting-started/#sample-app-hotrod。
通过服务将追踪信息发送到 Jaeger,可以看到追踪。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 Dashboard。最后,我们学习了如何实施和使用一些关键的开源工具来提供这四个支柱的可视化、搜索和警报功能。这些知识将帮助你为未来的 Kubernetes 集群构建强大的可观测性基础设施,并帮助你决定在集群中最需要观察的内容。
在下一章中,我们将利用我们学到的可观测性知识来帮助我们排查 Kubernetes 上的应用程序问题。
问题
-
解释指标和日志之间的区别。
-
为什么你会选择使用 Grafana,而不是仅仅使用 Prometheus 的 UI?
-
在生产环境中运行 EFK 堆栈时(为了尽可能将计算负载从生产应用集群中分离出来),堆栈的哪些组件会运行在生产应用集群上?哪些组件会在集群外运行?
进一步阅读
- Kibana Timelion 的深入回顾:
www.elastic.co/guide/en/kibana/7.10/timelion-tutorial-create-time-series-visualizations.html
第十章:排查 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 Microsystems)和詹姆斯·戈斯林(他增加了第八点)共同撰写了《分布式计算的谬误》一文,成为业界普遍认可的分布式应用程序故障的因素。在《分布式计算的谬误解析》一文中,阿尔农·罗特姆-加尔-奥兹讨论了这些谬误的来源(www.rgoarchitects.com/Files/fallacies.pdf)。
这些谬误按数字顺序列出如下:
-
网络是可靠的。
-
延迟为零。
-
带宽是无限的。
-
网络是安全的。
-
拓扑不会改变。
-
只有一个管理员。
-
传输成本为零。
-
网络是同质化的。
Kubernetes 在设计和开发时就考虑到了这些错误观念,因此它更具容错性。它还帮助解决在 Kubernetes 上运行的应用程序面临的这些问题——但并不是完美的。因此,当你的应用程序容器化并在 Kubernetes 上运行时,很可能会遇到这些问题。每个错误观念,当被假定为不真实并推到其逻辑结论时,都可能在分布式应用程序中引入故障模式。我们来逐一分析这些错误观念,看看它们如何应用到 Kubernetes 以及在 Kubernetes 上运行的应用程序。
网络是可靠的。
运行在多个逻辑机器上的应用程序必须通过互联网进行通信——因此,网络中的任何可靠性问题都可能引发问题。特别是在 Kubernetes 上,控制平面本身可以通过高可用性设置进行分布式(这意味着使用多个主节点的设置——参见 第一章,与 Kubernetes 通信),这意味着控制器级别可能引入故障模式。如果网络不可靠,kubelet 可能无法与控制平面通信,从而导致 Pod 调度问题。
同样,控制平面的节点可能无法彼此通信——尽管etcd当然是采用共识协议构建的,可以容忍通信失败。
最后,工作节点可能无法相互通信——这在微服务场景中可能会根据 Pod 的放置位置引发问题。在某些情况下,工作节点可能都能与控制平面通信,但仍然无法相互通信,这可能会导致 Kubernetes 覆盖网络出现问题。
与一般的不可靠性一样,延迟也会引发许多相同的问题。
延迟为零。
如果网络延迟较大,许多与网络不可靠性相关的失败也可能会发生。例如,kubelet 与控制平面之间的调用可能会失败,导致etcd中的数据不准确,因为控制平面可能无法联系到 kubelet,或者无法正确更新etcd。类似地,在工作节点上运行的应用程序之间的请求可能会丢失,而如果这些应用程序运行在同一个节点上,它们本来是可以正常工作的。
带宽是无限的。
带宽限制可能会暴露与前两个谬误类似的问题。Kubernetes 目前没有一个完全支持的方法来基于带宽订阅来放置 Pods。这意味着那些达到网络带宽限制的节点仍然可以被调度新的 Pod,导致请求的失败率和延迟问题增加。曾有请求将其作为 Kubernetes 调度的核心特性(基本上,是一种像 CPU 和内存一样,基于节点带宽消耗进行调度的方式),但目前的解决方案主要还是局限于容器网络接口(CNI)插件。
重要提示
例如,CNI 带宽插件支持在 Pod 级别进行流量整形 – 请参见kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping。
第三方 Kubernetes 网络实现也可能提供与带宽相关的附加功能——并且许多与 CNI 带宽插件兼容。
网络是安全的
网络安全的影响远超 Kubernetes 本身——任何不安全的网络都容易受到一类攻击的威胁。攻击者可能通过 SSH 访问 Kubernetes 集群中的主节点或工作节点,这可能导致重大的安全漏洞。由于 Kubernetes 的许多“魔力”发生在网络上,而非单一机器中,因此在攻击情况下,访问网络会带来双重问题。
拓扑不会改变
这个谬误在 Kubernetes 环境中尤其相关,因为不仅元网络拓扑会因新增或移除节点而变化——覆盖网络拓扑也会直接受到 Kubernetes 控制平面和 CNI 的影响。
因此,在某一时刻运行在一个逻辑位置的应用程序,可能会在网络中的完全不同位置运行。因此,使用 Pod IP 来标识逻辑应用程序是一个不好的主意 – 这是 Service 抽象的目的之一(请参见第五章,Service 和 Ingress – 与外部世界通信)。任何没有假设集群内拓扑是无限的应用程序(至少是关于 IP 的)都可能会遇到问题。例如,将流量路由到特定 Pod IP 仅在该 Pod 存在时有效。如果该 Pod 关闭,控制它的 Deployment(例如)将启动一个新的 Pod 来替代它,但 IP 会完全不同。集群 DNS(以及扩展的 Services)提供了一种更好的方式来在集群内的应用程序之间发起请求,除非你的应用程序有能力即时适应集群变化,比如 Pod 的位置调整。
只有一个管理员
多个管理员和冲突的规则可能会导致基础网络的问题,而多个 Kubernetes 管理员通过更改资源配置(例如 Pod 资源限制)可能会导致进一步的问题,从而导致意外行为。使用 Kubernetes 基于角色的访问控制(RBAC)功能可以通过仅授予 Kubernetes 用户所需的权限(例如只读权限)来帮助解决这个问题。
传输成本为零
这种谬误有两种常见的解释方式。首先,认为传输的延迟成本为零——显然这是不正确的,因为通过线路传输数据的速度不是无限的,且低层次的网络问题会增加延迟。这本质上与延迟为零的谬误所带来的影响是相同的。
其次,这个说法可以解释为:创建和运营网络用于传输的成本为零——即零美元零分。虽然这显然也是不正确的(只需要看看你的云服务提供商的数据传输费用就能证明这一点),但这与 Kubernetes 上应用程序故障排除并没有直接关联,因此我们将重点讨论第一种解释。
网络是同质的
这个最终的谬误与 Kubernetes 的组件关系较小,而与运行在 Kubernetes 上的应用程序更相关。然而,事实上,今天在这种环境中操作的开发人员都很清楚,应用程序的网络实现可能在不同应用之间有所不同——从 HTTP 1 和 2 到像gRPC 这样的协议。
现在我们已经回顾了一些导致 Kubernetes 上应用程序失败的主要原因,我们可以深入实际的故障排除过程,排除 Kubernetes 本身以及运行在 Kubernetes 上的应用程序的问题。
Kubernetes 集群故障排除
由于 Kubernetes 是一个分布式系统,设计时就考虑了容忍应用运行故障的情况,大多数(但不是全部)问题往往集中在控制平面和 API 上。工作节点失败,在大多数场景下,只会导致 Pods 被重新调度到另一个节点——尽管复合因素可能会引发问题。
为了走过常见的 Kubernetes 集群问题场景,我们将使用案例研究方法。这将为你提供解决现实世界集群问题所需的所有工具。我们的第一个案例研究聚焦于 API 服务器本身的故障。
重要提示
对于本教程的目的,我们将假设一个自我管理的集群。像 EKS、AKS 和 GKE 这样的托管 Kubernetes 服务通常会去除一些故障域(例如通过自动扩展和管理主节点)。一个好的规则是首先检查托管服务的文档,因为任何问题可能都是实现特定的。
案例研究——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
如我们所见,没有任何 Pods 在运行。此外,我们运行了三个副本的应用程序,但没有一个被调度。接下来的好步骤是检查节点状态,看看是否存在任何问题。运行以下命令以获取输出:
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 – 描述节点条件输出
我们这里遇到一个有趣的分歧:MemoryPressure 和 DiskPressure 一切正常,而 OutOfDisk 和 Ready 状态未知,消息显示为 kubelet 停止发布节点状态。乍一看,这似乎不合常理——怎么会在 MemoryPressure 和 DiskPressure 一切正常的情况下,kubelet 停止工作呢?
重要部分在 LastTransitionTime 列中。kubelet 最近的内存和磁盘相关通信发送了正面的状态。然后,稍后 kubelet 停止发布其节点状态,导致 OutOfDisk 和 Ready 状态为 Unknown。
到此为止,我们确信问题出在我们的节点上——kubelet 不再将节点状态发送到控制平面。然而,我们不知道发生了什么。可能是网络错误,机器本身的问题,或者更具体的原因。我们需要进一步挖掘来找出问题所在。
这里的一个好步骤是更接近我们故障的节点,因为我们可以合理地假设它遇到了一些问题。如果你可以访问 node-01 的虚拟机或机器,现在是通过 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 不支持在默认情况下 swap 设置为 on 的 Linux 机器上运行。我们唯一的选择是禁用 swap 或者使用 --fail-swap-on 标志将 kubelet 重启并设置为 false。
在我们的情况下,我们将通过以下命令更改 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 集群仍然可能存在应用程序问题。这些问题可能是由于应用程序本身的 bug,或者由于构成应用程序的 Kubernetes 资源配置错误。与集群故障排查一样,我们将通过一个案例研究来深入探讨这些概念。
案例研究 1 – Service 无响应
我们将把这一部分分解为多个层次的 Kubernetes 堆栈故障排查,首先从更高层次的组件开始,然后深入到 Pod 和容器的调试。
假设我们已经配置了应用程序 app-1,通过 NodePort Service 来响应请求,端口为 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 Service 并未将请求路由到任何 Pod。按照我们的典型调试路径,首先让我们查看集群中运行了哪些资源,使用以下命令:
kubectl get services
添加 -o wide 标志以查看额外信息。接着,运行以下命令:
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
很明显,我们的 Service 存在并且配置了正确的 Node port,但正如从失败的 curl 命令中看出,我们的请求并没有被路由到 Pods。
要查看我们的 Service 设置了哪些路由,我们可以使用 get endpoints 命令。这个命令将列出该 Service 的 Pod IP(如果有的话):
kubectl get endpoints app-1-svc
让我们检查命令的结果输出:
NAME ENDPOINTS
app-1-svc <none>
好吧,显然这里出了点问题。
我们的 Service 没有指向任何 Pod。这很可能意味着没有任何 Pod 匹配我们的 Service 选择器。这可能是因为没有可用的 Pods,或者是因为这些 Pods 没有正确匹配 Service 选择器。
为了检查我们的 Service 选择器,让我们按照调试路径的下一步,使用 describe 命令,如下所示:
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>
如你所见,我们的 Service 已经配置为与应用程序的正确端口进行通信。然而,选择器正在寻找标签为 app = app-11 的 Pods。由于我们知道我们的应用程序名为 app-1,这可能是问题的根源。
让我们编辑我们的 Service,查找正确的 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>
现在,你可以在输出中看到我们的 Service 正在寻找正确的 Pod 选择器,但我们仍然没有任何端点。让我们通过以下命令检查一下我们的 Pods 出现了什么问题:
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 -
我们的 Pods 仍在等待调度。这解释了为什么即使有正确的选择器,我们的 Service 仍然无法正常工作。为了更细致地了解为什么我们的 Pods 没有被调度,我们可以使用 describe 命令:
kubectl describe pod app-1-pod-2821252345-tj8ks
以下是输出。我们关注 Events 部分:

图 10.2 – 描述 Pod 事件输出
从Events部分来看,我们的 Pod 未能成功调度,原因是容器镜像拉取失败。这可能有很多原因——例如,我们的集群可能没有从私有仓库拉取镜像所需的认证机制——但这种情况通常会显示为不同的错误信息。
从上下文和Events输出来看,我们大概可以推测出问题所在:我们的 Pod 定义正在寻找名为myappimage:lates的容器,而不是myappimage:latest。
让我们用正确的镜像名称更新我们的 Deployment 规范,并推出更新。
使用以下命令来确认:
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
我们的 Pods 现在正在运行——让我们检查一下 Service 是否已注册正确的端点。使用以下命令来完成此操作:
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>
成功!我们的 Service 已正确指向应用程序 Pods。
在接下来的案例研究中,我们将通过故障排除启动参数不正确的 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
该输出看起来一切正常。似乎我们的应用程序作为 Deployment 的一部分(因此也是 ReplicaSet 的一部分)在运行——我们可以通过运行以下命令来确认:
kubectl get deployments
输出应该如下所示:
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app-1-pod 3 3 3 3 5m
让我们通过以下命令更仔细地查看 Deployment,看看 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
让我们看看是否能修复这个问题,其实这非常简单。我们使用了错误版本的应用程序,而且启动命令也错了。在这种情况下,假设我们没有包含 Deployment 规范的文件——那么我们就直接在原地编辑它。
让我们使用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
一旦 Deployment 被保存,你应该开始看到新的 Pod 启动。让我们通过以下命令再确认一下:
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 应用程序故障与日志
在前一章节第九章,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——这里的一个关键特征是 Pod ID 从 0 开始递增。
我们可以通过以下命令确认这一点,检查 StatefulSet:
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 spec 部分,以便保持简洁:
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 作为查询字符串,这将使第一个数字和第二个数字(由操作符值分割)都等于 1。
这需要一些额外的挖掘。我们可以通过发送额外的请求并检查输出以猜测发生了什么来进行问题诊断,但在这种情况下,最好是直接获得 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 和我们的应用程序本身。我们需要这些信息来通过ssh进入其中一个 Pod(对于本练习来说,哪个 Pod 无关紧要),使用 kubectl exec 命令。
我们可以使用以下命令来做到这一点:
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 上的模板代码生成与持续集成/持续部署,我们将探讨一些用于模板化 Kubernetes 资源清单和使用 Kubernetes 进行持续集成/持续部署的生态系统扩展。
问题
-
分布式系统谬论 "拓扑结构不会改变" 如何适用于运行在 Kubernetes 上的应用程序?
-
Kubernetes 控制平面组件(和 kubelet)如何在操作系统级别实现?
-
在处理 Pods 处于
Pending状态的问题时,您会如何进行调试?您的第一步是什么?第二步呢?
进一步阅读
- 用于流量整形的 CNI 插件:
kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping
第十一章:Kubernetes 上的模板代码生成与 CI/CD
本章讨论了几种更简单的方式来模板化和配置具有大量资源的 Kubernetes 部署。同时,详细介绍了在 Kubernetes 上实现 持续集成/持续部署 (CI/CD) 的多种方法,并探讨了每种方法的优缺点。具体来说,我们讨论了集群内 CI/CD,其中一些或所有的 CI/CD 步骤在 Kubernetes 集群中执行;以及集群外 CI/CD,其中所有步骤都在集群外部完成。
本章的案例研究将包括从头开始创建 Helm chart,并解释 Helm chart 中的每个部分及其工作原理。
首先,我们将介绍 Kubernetes 资源模板生成的概况,以及为什么应该使用模板生成工具。接下来,我们将介绍如何将 CI/CD 实现到 Kubernetes 中,首先使用 AWS CodeBuild,然后使用 FluxCD。
在本章中,我们将覆盖以下主题:
-
理解 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 文件而不支持变量,而是推荐一种 分支和修补 工作流,根据选择的修补程序用新的 YAML 替换 YAML 的某些部分。
现在我们对这些工具的区别有了基本的了解,我们可以在实践中使用它们。
在 Kubernetes 上使用 Helm 和 Kustomize 实现模板化
既然我们了解了选项,现在可以通过一个示例应用程序来实现每个选项。这将帮助我们理解每个工具如何处理变量以及模板化的过程。我们从 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 文件、一个或多个模板文件,并可选地包含一个值文件的文件夹组成。
为了安装带有本地值文件的本地 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 V3 中,发布是通过 Kubernetes 秘密来实现的。当我们在 Helm 中创建发布时,它会作为秘密存储在相同的命名空间中。
为了说明这一点,我们可以使用前面的 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 的秘密列表输出
如你所见,其中一个秘密的类型为 helm.sh/release.v1。这是 Helm 用来跟踪 Cert Manager 发布的秘密。
最后,要查看 Helm CLI 中列出的发布,我们可以运行以下命令:
helm ls -A
该命令将列出所有命名空间中的 Helm 发布(就像 kubectl get pods -A 会列出所有命名空间中的 Pod 一样)。输出将如下所示:

图 11.2 – Helm 发布列表输出
现在,Helm 具有更多的组成部分,包括 升级、回滚 等,我们将在下一部分进行回顾。为了展示 Helm 的功能,我们将从零开始创建并安装一个图表。
创建 Helm 图表
所以,我们想为我们的应用程序创建一个 Helm 图表。让我们先设定一下场景。我们的目标是将一个简单的 Node.js 应用程序轻松部署到多个环境中。为此,我们将创建一个包含应用程序组件的图表,并将其与三个独立的值文件(dev、staging 和 production)结合,目的是将应用程序部署到这三个环境中。
让我们从 Helm 图表的文件夹结构开始。正如我们之前提到的,Helm 图表由模板、元数据文件和可选的值组成。我们将在实际安装图表时注入这些值,但我们可以按照以下方式组织我们的文件夹:
Chart.yaml
charts/
templates/
dev-values.yaml
staging-values.yaml
production-values.yaml
我们尚未提到的一点是,你实际上可以在现有 chart 中拥有一个 Helm charts 文件夹!这些子 chart 可以方便地将复杂的应用拆分成多个组件。出于本书的目的,我们将不会使用子 chart,但如果你的应用变得过于复杂或模块化,不适合使用单一 chart,那么这是一个非常有用的功能。
此外,你可以看到我们为每个环境都提供了不同的环境文件,这些文件将在我们安装命令时使用。
那么,Chart.yaml 文件是什么样子的呢?这个文件将包含一些关于你的 chart 的基本元数据,通常至少包含如下内容:
apiVersion: v2
name: mynodeapp
version: 1.0.0
Chart.yaml 文件支持许多可选字段,你可以在 helm.sh/docs/topics/charts/ 查看,但为了本教程的目的,我们将保持简单。必须字段包括 apiVersion、name 和 version。
在我们的 Chart.yaml 文件中,apiVersion 对应于该 chart 所对应的 Helm 版本。有点让人困惑的是,当前版本的 Helm,即 Helm V3,使用 apiVersion v2,而旧版本的 Helm,包括 Helm V2,也使用 apiVersion v2。
接下来,name 字段对应于我们 chart 的名称。这个非常直观,尽管请记住,我们可以为一个 chart 的特定版本命名——这一点对于多个环境非常有用。
最后,我们有 version 字段,它对应于 chart 的版本。此字段支持 SemVer(语义化版本控制)。
那么,我们的模板到底是什么样子的呢?Helm charts 在后台使用 Go 模板库(更多信息请参见 golang.org/pkg/text/template/),并支持各种强大的操作、辅助函数等等。目前,我们将保持极其简单,以便让你了解基本概念。全面讨论 Helm chart 的创建可能本身就是一本书!
首先,我们可以使用 Helm CLI 命令来自动生成我们的 Chart 文件夹,其中包含所有前述文件和文件夹,缺少子 chart 和值文件。让我们来试一下——首先用以下命令创建一个新的 Helm chart:
helm create myfakenodeapp
这个命令将在名为 myfakenodeapp 的文件夹中创建一个自动生成的 chart。让我们使用以下命令检查 templates 文件夹的内容:
Ls myfakenodeapp/templates
这个命令将会产生如下输出:
helpers.tpl
deployment.yaml
NOTES.txt
service.yaml
这个自动生成的 chart 作为起点会非常有帮助,但为了本教程的目的,我们将从头开始创建这些文件。
创建一个名为 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。我们在模板中并没有使用任何变量。
现在,我们已经具备了安装 chart 的足够信息。接下来我们来做这个操作。
安装和卸载 Helm chart。
要使用 Helm V3 安装一个 chart,你需要在 chart 的 root 目录下运行 helm install 命令:
helm install myapp .
这个安装命令创建了一个名为 frontend-app 的 Helm 发布,并安装了我们的 chart。此时,我们的 chart 仅由一个包含两个 pod 的单一部署组成,应该能够通过以下命令在集群中看到它运行:
kubectl get deployment
这应该会产生以下输出:
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
default frontend-myapp 2/2 2 2 2m
从输出中可以看到,我们的 Helm install 命令已经成功地在 Kubernetes 中创建了一个部署对象。
卸载我们的 chart 同样简单。我们可以通过运行以下命令来卸载所有通过我们的 chart 安装的 Kubernetes 资源:
helm uninstall myapp
这个 uninstall 命令(在 Helm V2 中是 delete)只需要我们的 Helm 发布名称。
到目前为止,我们还没有使用 Helm 的真正强大功能——我们一直把它当作 kubectl 的替代工具,且没有添加任何新特性。让我们通过在 chart 中实现一些变量来改变这一点。
使用模板变量。
向 Helm chart 模板中添加变量就像使用双大括号 – {{ }} – 语法一样简单。我们放入双大括号中的内容将直接来自我们在安装 chart 时使用的值,并采用点表示法。
让我们来看一个简短的示例。到目前为止,我们已经将应用名称(和容器镜像名称/版本)硬编码进了 YAML 文件。如果我们想使用 Helm chart 部署不同的应用或不同的应用版本,这将大大限制我们。
为了解决这个问题,我们将向 chart 添加模板变量。看一下这个生成的模板:
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:用于引用 chart 中非 YAML 模板的文件(例如,config文件)。 -
.Capabilities:用于引用目标 Kubernetes 集群的信息(换句话说,版本)。
在我们的 YAML 文件中,我们使用了多个这样的引用。首先,我们在多个位置引用了我们发布的name(包含在.Release对象中)。接下来,我们利用Chart对象将元数据注入到chartVersion键中。最后,我们使用Values对象来引用容器镜像的name和tag。
现在,最后一个我们缺少的就是我们将通过values.yaml或者 CLI 命令注入的实际值。其他所有内容都将通过 Chart.yaml 创建,或者我们将在运行时通过 helm 命令注入的值来完成。
考虑到这一点,让我们从模板中创建我们的值文件,我们将传递我们的镜像name和tag。因此,让我们以正确的格式将它们包括在内:
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 文件。
让我们从使用一个 Kustomization 文件来专门修补 YAML 开始。
在 Kustomization 文件中直接指定更改
如果我们想直接在 Kustomization 文件中指定更改,我们可以这样做,但我们的选择会有些限制。我们可以在 Kustomization 文件中使用的键类型如下:
-
resources– 指定在应用补丁时要自定义的文件 -
transformers– 直接从 Kustomization 文件中应用补丁的方法 -
generators– 从 Kustomization 文件中创建新资源的方法 -
meta– 设置可以影响生成器、变换器和资源的元数据字段
如果我们想在 Kustomization 文件中指定直接补丁,我们需要使用变换器。前面提到的 PatchStrategicMerge 和 JSONPatch 合并策略是两种变换器。然而,为了直接应用对 Kustomization 文件的更改,我们可以使用几种变换器,包括 commonLabels、images、namePrefix 和 nameSuffix。
在以下 Kustomization 文件中,我们使用 commonLabels 和 images 变换器对初始部署 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 文档,网址为 kubernetes-sigs.github.io/kustomize/。Kustomize 文件假设 deployment.yaml 与其本身位于同一文件夹中。
要查看 Kustomize 文件应用于我们的部署时的结果,我们可以使用 Kustomize CLI 工具。我们将使用以下命令来生成经过 Kustomize 处理的输出:
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
这个补丁文件是原始部署中字段的一个子集。在这个例子中,它仅仅将replicas从2更新到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 的方法。
理解 Kubernetes 上的 CI/CD 范式——集群内外
持续集成和部署到 Kubernetes 可以有多种形式。
大多数 DevOps 工程师都会熟悉像 Jenkins、TravisCI 等工具。这些工具的共同之处在于它们提供了一个执行环境,用于构建应用程序、执行测试并在受控环境中调用任意的 Bash 脚本。其中一些工具在容器内运行命令,而另一些则不在容器内运行。
在 Kubernetes 的使用中,存在多种观点,关于如何以及在哪里使用这些工具。还有一种新型的 CI/CD 平台,它们与 Kubernetes 原语紧密耦合,许多这样的平台是为在集群内部运行而设计的。
为了全面讨论工具如何与 Kubernetes 相关,我们将把管道分为两个逻辑步骤:
-
构建:编译、测试应用程序,构建容器镜像,并发送到镜像仓库
-
部署:通过 kubectl、Helm 或其他工具更新 Kubernetes 资源
本书的重点主要放在第二个以部署为重点的步骤上。尽管许多可用的选项都同时处理构建和部署步骤,但构建步骤几乎可以在任何地方发生,因此在涉及 Kubernetes 细节的书中,构建步骤并不值得我们过多关注。
鉴于这一点,为了讨论我们的工具选项,我们将在管道的部署部分将我们的工具集分为两类:
-
集群外 CI/CD
-
集群内 CI/CD
集群外 CI/CD
在第一种模式中,我们的 CI/CD 工具运行在目标 Kubernetes 集群之外。我们称之为集群外 CI/CD。有一种灰色地带,工具可能在一个专门用于 CI/CD 的 Kubernetes 集群中运行,但我们暂时忽略这种情况,因为这两类之间的区别仍然有效。
你经常会看到 Jenkins 等行业标准工具与这种模式一起使用,但任何能够运行脚本并安全存储秘钥的 CI 工具都能在这里使用。一些例子包括 , CircleCI、TravisCI、GitHub Actions 和 AWS CodeBuild。Helm 也是这种模式的一个重要组成部分,因为集群外的 CI 脚本可以调用 Helm 命令来代替 kubectl。
这种模式的一些优点体现在其简洁性和可扩展性上。这是一种基于push的模式,其中代码的更改会同步触发 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 通常会循环执行,而不是在更改推送时立即发生)。
在集群内和集群外实现 CI/CD 与 Kubernetes
由于 Kubernetes 的 CI/CD 有许多选项,我们将选择两种选项并逐一实现,以便你可以比较它们的功能集。首先,我们将实现 AWS CodeBuild 到 Kubernetes 的 CI/CD,这是一个很好的示例实现,可以与任何可以运行 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 步骤:
-
首先,测试并构建容器镜像,并将其推送到 ECR。
-
其次,将更新后的容器部署到 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 设置分为三个阶段:install、pre_deploy 和 deploy。在 install 阶段,我们安装 kubectl CLI 工具。
然后,在 pre_deploy 阶段,我们使用 AWS CLI 命令和几个环境变量来更新我们的 kubeconfig 文件,以便与我们的 EKS 集群通信。在任何其他 CI 工具中(或在不使用 EKS 时),您可以使用不同的方法为 CI 工具提供集群凭证。这里使用安全的选项很重要,因为将 kubeconfig 文件直接包含在 Git 仓库中并不安全。通常,使用环境变量的组合会是一个不错的选择。Jenkins、CodeBuild、CircleCI 等都有自己的系统来处理这个问题。
最后,在 deploy 阶段,我们使用 kubectl 更新我们的部署(也包含在环境变量中),并指定在第一个 CodeBuild 步骤中定义的新镜像标签。这个 kubectl rollout restart 命令将确保为我们的部署启动新 Pods。结合使用 imagePullPolicy 的 Always,这将导致我们的新应用版本被部署。
在这种情况下,我们通过 ECR 中的特定镜像标签名称来修补我们的部署。$IMAGE_TAG环境变量将自动填充为来自 GitHub 的最新标签,我们可以使用它来自动将新的容器镜像推出到我们的部署中。
接下来,我们来看看我们的第二个 CodePipeline。这个管道只有一个步骤——它监听来自一个独立 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 有几种选择,包括ArgoCD和JenkinsX,但我们喜欢FluxCD,因为它相对简单,并且能够在没有额外配置的情况下自动使用新容器版本更新 pods。作为一个附加的特点,我们将使用 FluxCD 的 Helm 集成来管理部署。我们先从 FluxCD 的安装开始(我们假设你已经在本章前面的部分中安装了 Helm)。这些安装遵循了截至本书写作时,FluxCD 的官方安装说明,确保兼容 Helm。
官方 FluxCD 文档可以在 docs.fluxcd.io/ 找到,我们强烈建议你查看一下!FluxCD 是一个非常复杂的工具,而在本书中我们仅仅是触及了表面。全面的评审不在本书的范围内——我们只是尝试向你介绍集群内的 CI/CD 模式和相关工具。
让我们通过在集群中安装 FluxCD 来开始我们的回顾。
安装 FluxCD (H3)
FluxCD 可以通过 Helm 很容易地安装,只需几个步骤:
-
首先,我们需要添加 Flux Helm chart 仓库:
helm repo add fluxcd https://charts.fluxcd.io -
接下来,我们需要添加 FluxCD 所需的自定义资源定义,以便它能够与 Helm 发布一起工作:
kubectl apply -f https://raw.githubusercontent.com/fluxcd/helm-operator/master/deploy/crds.yaml -
在我们可以安装 FluxCD Operator(FluxCD 在 Kubernetes 上的核心功能)和 FluxCD Helm Operator 之前,我们需要为 FluxCD 创建一个命名空间:
kubectl create namespace flux现在我们可以安装 FluxCD 的主要组件,但我们需要提供一些关于 Git 仓库的额外信息给 FluxCD。
为什么?因为 FluxCD 使用 GitOps 模式进行更新和部署。这意味着 FluxCD 将每隔几分钟主动连接到我们的 Git 仓库,而不是响应像 CodeBuild 这样的 Git 钩子。
FluxCD 还将通过拉取策略响应新的 ECR 镜像,但我们稍后会讨论这个问题。
-
要安装 FluxCD 的主要组件,运行以下两个命令,并将
GITHUB_USERNAME和REPOSITORY_NAME替换为你将存储工作负载规格(Kubernetes YAML 或 Helm charts)的 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 charts,FluxCD 将根据这些内容更新运行在集群中的工作负载。
-
为了真正让 Flux 有事可做,我们需要为 Flux 创建实际的清单。我们使用一个
HelmReleaseYAML 文件来完成这个操作,文件内容如下所示:
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 仓库。同时,我们还为 HelmRelease 添加了 automated 注解,告诉 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 图表。
FluxCD 在获得新版本和 Helm 图表后,将把我们的 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 上的安全性和合规性,这是当今软件环境中一个重要的话题。
问题
-
Helm 和 Kustomize 模板之间的两种区别是什么?
-
在使用外部 CI/CD 设置时,如何处理 Kubernetes API 凭证?
-
为什么在集群内的 CI 设置比集群外设置更可取?反之又如何?
进一步阅读
-
Kustomize 文档:
kubernetes-sigs.github.io/kustomize/ -
Helm 文档
docs.fluxcd.io/en/latest/tutorials/get-started-helm/
第十二章: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 组件的安全性得以维护。此外,广泛的个人贡献者和公司使用该技术,确保了 Kubernetes 在 CVE 被报告和处理时不断改进。不幸的是,正如我们在下一节将讨论的那样,Kubernetes 的复杂性意味着存在许多潜在的攻击途径。
然后,应用共享责任模型,作为开发者,你需要负责如何配置 Kubernetes 组件的安全性、在 Kubernetes 上运行的应用程序的安全性,以及集群配置中的访问级别安全性。虽然应用程序和容器本身的安全性不在本书的讨论范围内,但它们对 Kubernetes 安全性至关重要。我们将花费大部分时间讨论配置级别的安全性、访问安全性和运行时安全性。
无论是 Kubernetes 本身还是 Kubernetes 生态系统,都提供了工具、库和完整的产品来处理各个层次的安全性——我们将在本章中回顾其中的一些选项。
在我们讨论这些解决方案之前,最好先从一个基本的理解出发,弄清楚它们为什么在一开始就可能是需要的。接下来,让我们进入下一部分,详细说明 Kubernetes 在安全领域遇到的一些问题。
审查 Kubernetes 的 CVE 和安全审计
Kubernetes 遇到了多个 kubernetes。每个问题都直接或间接地与 Kubernetes 本身,或与在 Kubernetes 上运行的常见开源解决方案(例如 NGINX Ingress 控制器)相关。
其中一些问题严重到需要对 Kubernetes 源代码进行热修复,因此它们在 CVE 描述中列出了受影响的版本。所有与 Kubernetes 相关的 CVE 完整列表可以在 cve.mitre.org/cgi-bin/cvekey.cgi?keyword=kubernetes 找到。为了让你了解一些已发现的问题,我们将按时间顺序回顾其中的一些 CVE。
理解 CVE-2016-1905 —— 不当的准入控制
这个 CVE 是 Kubernetes 生产环境中遇到的第一个重大安全问题之一。国家漏洞数据库(NVD,国家标准与技术研究院网站)给出了 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 控制器(它们本身也是准入控制器,只不过是特殊的):MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。所有其他准入控制器在内部使用这两个 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 控制器之一来完成。其工作方式如下:
-
你必须编写自己的服务器或脚本,它需要独立于 Kubernetes API 服务器运行。
-
然后,你需要配置前面提到的两种 webhook 触发器之一,向你的自定义服务器控制器发送包含资源数据的请求。
-
根据结果,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,我们设置name、rules 和 configuration。name只是 webhook 的标识符。rules 让我们配置 Kubernetes 在哪些情况下应该向我们的准入控制器发起请求。在此案例中,我们已经将 webhook 配置为在 pods、deployments 和 configmaps 类型的资源发生 CREATE 事件时触发。
最后,我们有clientConfig,在这里我们指定 Kubernetes 应该如何以及在何处发起 webhook 请求。由于我们在 Kubernetes 上运行自定义服务器,因此除了指定服务器路径("/mutate" 是此处的最佳实践)外,我们还需要指定服务名称,正如之前的 YAML 文件中所示,并提供集群的 CA 以便与 HTTPS 终止证书进行比较。如果您的自定义准入服务器运行在其他地方,还有其他可能的配置字段——如果需要,请查阅文档(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 安全策略的基本原理是,它们允许集群管理员创建规则,要求 Pods 必须遵循这些规则才能被调度到节点上。从技术上讲,Pod 安全策略仅是另一种类型的准入控制器。然而,这一功能已被 Kubernetes 官方支持,值得深入讨论,因为有许多可用的选项。
Pod 安全策略可以用来防止 Pods 以 root 身份运行,限制使用的端口和卷,限制特权提升等。我们现在将回顾 Pod 安全策略的一部分功能,但要查看完整的 Pod 安全策略配置类型列表,请查阅官方的 PSP 文档 kubernetes.io/docs/concepts/policy/pod-security-policy/。
最后需要注意的是,Kubernetes 还支持用于控制容器权限的低级原语——即 AppArmor、SELinux 和 Seccomp。这些配置超出了本书的范围,但它们在高安全性环境中可能会非常有用。
创建 Pod 安全策略的步骤
实现 Pod 安全策略的步骤如下:
-
首先,必须启用 Pod 安全策略准入控制器。
-
这将阻止在集群中创建所有 Pod,因为它要求匹配的 Pod 安全策略和角色才能创建 Pod。由于这个原因,您可能希望在启用准入控制器之前先创建 Pod 安全策略和角色。
-
启用准入控制器后,必须创建相应的策略。
-
然后,必须创建一个
Role或ClusterRole对象,并授予其访问 Pod 安全策略的权限。 -
最后,可以将该角色与
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: The Hard Way的方式进行部署),则应在此文件中更新标志。通常,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 安全策略允许用户或服务账户(通过PodSecurityPolicy)绑定主机网络上的2000至65535端口,可以以任何用户身份运行,并且可以绑定任何类型的卷。此外,我们还为allowedProfileNames添加了一个seccomp限制注释—以帮助您理解Seccomp和AppArmor注释如何与PodSecurityPolicies配合使用。
如前所述,仅创建 PSP 并不会做任何事情。对于任何将创建特权 Pod 的服务账户或用户,我们需要通过ClusterRole和ClusterRoleBinding为他们提供对 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 能做的事情进行限制。
让我们来看一下一个示例 YAML,它展示了具有以下限制的 PSP:
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 中,runAsUser 和 supplementalGroups 部分控制可以由容器运行或添加的 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 安全策略应用后,将防止任何 Pod 以用户 ID 0 或 3001 或更高的 ID 运行。为了创建符合这一条件的 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 指定了一个特定的用户 ID 1000。我们还禁止 Pod 提升为 root 权限。即使在 specific-user-psp 策略生效的情况下,这个 Pod 规范也能成功调度。
现在我们已经讨论了如何通过对 Pod 运行的限制来保障 Kubernetes 的安全,接下来我们可以讨论网络策略,利用它我们可以限制 Pod 之间的网络通信。
使用网络策略
Kubernetes 中的网络策略与防火墙规则或路由表类似。它们允许用户通过选择器指定一组 Pod,并确定这些 Pod 如何以及在哪里进行通信。
为了使网络策略生效,您选择的 Kubernetes 网络插件(如Weave、Flannel或Calico)必须支持网络策略规格。网络策略可以像所有其他 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。
接下来,我们为网络策略指定一个策略类型。有两种策略类型:ingress和egress。一个网络策略可以指定一个或两个类型。ingress指的是为连接到匹配的 Pod 的流量设置网络规则,而egress指的是为离开匹配 Pod 的连接设置网络规则。
在这个特定的网络策略中,我们仅定义了一个ingress规则:只有来自标签为app:frontend的 Pod 的流量才会被标签为app=server的 Pod 接受。此外,标签为app=server的 Pod 上唯一会接受流量的端口是80。
在ingress策略集中可以有多个from块,它们对应多个流量规则。类似地,在egress中,也可以有多个to块。
需要注意的是,网络策略是按命名空间工作的。默认情况下,如果一个命名空间中没有任何网络策略,则该命名空间内的 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中,我们指定将同时包含Ingress和Egress策略,但我们没有为它们编写任何块。这会导致自动拒绝任何Egress和Ingress流量,因为没有规则可以匹配流量。
此外,我们的{} Pod 选择器值对应于选择命名空间中的每个 Pod。此规则的最终结果是,development命名空间中的每个 Pod 都无法接受ingress流量或发送egress流量。
重要说明
还需要注意的是,网络策略是通过将所有影响 Pod 的单独网络策略结合起来进行解释,然后将所有这些规则的组合应用于 Pod 流量。
这意味着,即使我们在前面的例子中已经限制了development命名空间中所有的进出流量,我们仍然可以通过添加另一个网络策略来为特定 Pod 启用流量。
假设现在我们的development命名空间对 Pod 之间的流量进行了完全限制,我们希望允许一部分 Pod 在443端口接收网络流量,并在6379端口向数据库 Pod 发送流量。为了实现这一点,我们只需要创建一个新的网络策略,通过策略的累加特性,允许这些流量。
这是网络策略的样子:
Override-restriction-network-policy.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 接收来自前端 Pod 的443端口流量,并将流量发送到数据库 Pod 的6379端口。
如果我们想要开放所有 Pod 之间的通信,而没有任何限制,同时仍然实际实施一个网络策略,我们可以使用以下的 YAML 配置:
All-open-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-egress
spec:
podSelector: {}
egress:
- {}
ingress:
- {}
policyTypes:
- Egress
- Ingress
现在我们已经讨论了如何使用网络策略来设置 Pod 之间流量的规则。然而,网络策略也可以作为外部防火墙使用。为此,我们需要创建基于外部 IP 地址的网络策略规则,而不是基于 Pod 的来源或目的地。
让我们看一个例子,网络策略限制了一个 Pod 的进出通信,并且指定了一个特定的 IP 范围作为目标:
External-ip-network-policy.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。
在我们的例子中,我们为Ingress和Egress规则选择了一个/16子网掩码范围(并指定了一个/24CIDR 例外)。这会导致任何来自集群内的流量无法到达这些 Pod,因为在默认的集群网络设置中,我们的 Pod IP 地址都不会与这些规则匹配。
然而,来自指定子网掩码内(而不在例外范围内)集群外部的流量将能够向worker Pod 发送流量,并且能够接收来自worker Pod 的流量。
在我们讨论完网络策略之后,我们可以转到安全堆栈的完全不同层次——运行时安全性和入侵检测。
处理 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 直接安装到 Linux 层的 Kubernetes 节点上。使用 DaemonSet 的 Helm 图表非常便捷,但从安全角度来看,直接安装 Falco 比通过 Helm 图表安装要更为安全。要将 Falco 直接安装到你的节点,请查看安装说明:falco.org/docs/installation/。
在上述说明之后,我们可以使用 Helm 安装 Falco:
-
首先,我们需要将
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。 -
要安装 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 Chart 中随附的 Default Falco 规则集中的几行规则。
在指定 Falco 配置时,我们可以使用三种不同类型的键来帮助编写规则。这些键分别是宏、列表和规则本身。
我们在这个示例中查看的特定规则叫做 Launch Privileged Container。这个规则将检测何时启动一个特权容器,并将容器的一些信息记录到 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 键让我们为规则指定优先级,优先级可以是 emergency、alert、critical、error、warning、notice、informational 或 debug 之一。
最后,tags 键为相关规则添加标签,从而使规则分类更容易。这在使用并非单纯文本的 STDOUT 警报时尤其重要。
condition 的语法在这里尤为重要,我们将重点讲解这一过滤系统的工作原理。
首先,由于过滤器本质上是逻辑语句,如果你曾经编写过程序或伪代码,你会看到一些熟悉的语法——例如 if、and、not 等。这些语法非常简单易学,关于 Sysdig 过滤器语法的详细讨论可以在 github.com/draios/sysdig/wiki/sysdig-user-guide#filtering 中找到。
需要注意的是,Falco 开源项目最初是由Sysdig创建的,这也是为什么它使用了常见的Sysdig过滤器语法。
接下来,你会看到对container_started和container的引用,以及falco_privileged_containers和user_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过滤器语法块,通常使用其他宏来实现规则功能。列表(此处未显示)类似于宏,但它们不描述过滤逻辑。相反,它们包含一组字符串值,可以作为比较的一部分,使用过滤语法进行处理。
例如,(``trusted_images) 在 falco_privileged_containers 宏中引用了一个名为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 安全策略 —— 你可能会发现 PSP 和 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 适应自己的需求。这将使你能够为集群添加重要的新功能。
问题
-
自定义准入控制器可以使用的两个 webhook 控制器的名称是什么?
-
空白的
NetworkPolicy对入站流量有什么影响? -
为了防止攻击者更改 Pod 功能,跟踪哪些类型的 Kubernetes 控制平面事件是有价值的?
进一步阅读
- Kubernetes CVE 数据库:
cve.mitre.org/cgi-bin/cvekey.cgi?keyword=kubernetes
第四部分:扩展 Kubernetes
在本节中,你将把前面章节中获得的知识应用到 Kubernetes 上的高级模式。我们将通过自定义资源定义扩展默认的 Kubernetes 功能,在集群中实现服务网格和无服务器架构模式,并运行一些有状态工作负载。
本书的这一部分包括以下章节:
-
第十三章,使用 CRD 扩展 Kubernetes
-
第十四章,服务网格与无服务器架构
-
第十五章,Kubernetes 上的有状态工作负载
第十三章:通过 CRD 扩展 Kubernetes
本章解释了扩展 Kubernetes 功能的多种可能性。首先讨论了kubectl命令,如get、create、describe和apply。接着讨论了 Operator 模式,它是 CRD 的一种扩展。然后详细介绍了一些云服务提供商在其 Kubernetes 实现中附加的钩子,最后简要介绍了更广泛的云原生生态系统。通过本章学到的概念,你将能够设计和开发扩展你的 Kubernetes 集群的功能,解锁高级使用模式。
本章的案例研究将包括创建两个简单的 CRD 来支持示例应用程序。我们将从 CRD 开始,这将为你提供一个良好的基础理解,帮助你理解扩展如何构建在 Kubernetes API 之上。
本章将涵盖以下主题:
-
如何通过自定义资源定义(CRDs)扩展 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 集成,也可能没有。
另一个选项是 CRD,我们可以使用 Kubernetes API 和底层数据存储(etcd),而不是构建自己的数据存储。我们可以使用我们熟悉的kubectl和kube api方法来与我们自己的自定义功能进行交互。
在本书中,我们将不会讨论 API 聚合。尽管它比 CRD 更加灵活,但这是一个高级话题,需深入理解 Kubernetes API 并仔细阅读 Kubernetes 文档才能正确实现。你可以在Kubernetes 文档中了解更多关于 API 聚合的信息。
现在,我们知道我们正在使用 Kubernetes 控制平面作为我们新自定义功能的有状态存储,我们需要一个架构。类似于 Kubernetes 中的 Pod 资源规范期望某些字段和配置一样,我们可以告诉 Kubernetes 我们对新自定义资源的期望。现在让我们通过 CRD 的规范来了解一下。
编写自定义资源定义
对于 CRD,Kubernetes 使用 OpenAPI V3 规范。有关 OpenAPI V3 的更多信息,你可以查看github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md上的官方文档,但我们很快就会看到它是如何转化为 Kubernetes CRD 定义的。
让我们来看一个 CRD 规范示例。现在需要明确的是,这并不是该 CRD 的任何特定记录的 YAML 格式。相反,这是我们在 Kubernetes 内部定义 CRD 要求的地方。创建之后,Kubernetes 将接受符合规范的资源,我们可以开始创建自己的此类记录。
这是一个用于 CRD 规范的 YAML 示例,我们称之为delayedjob。这个极为简化的 CRD 旨在延迟启动一个容器镜像任务,这样用户就无需为容器编写延迟启动脚本。这个 CRD 非常脆弱,我们不建议任何人实际使用它,但它很适合展示构建 CRD 的过程。我们先看一下完整的 CRD 规范 YAML,然后再逐步解析:
Custom-resource-definition-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 访问的资源,URL 格式为/apis/<GROUP NAME>/<VERSION>。这些命名组构成了 Kubernetes 资源的大部分。然而,最古老和最基本的资源,如 Pod、Service、Secret 和 Volume,属于核心组。一个属于命名组的资源示例是StorageClass资源,它位于storage.k8s.io组中。
重要说明
要查看哪些资源属于哪个组,你可以查阅官方的 Kubernetes API 文档,了解你使用的 Kubernetes 版本。例如,版本1.18的文档可以在kubernetes.io/docs/reference/generated/kubernetes-api/v1.18找到。
CRD 可以指定它们自己的命名组,这意味着特定的 CRD 将在 Kubernetes API 服务器可以监听的REST端点上可用。记住这一点后,让我们回到 YAML 文件,继续讨论 CRD 的主要部分——版本规范。
理解自定义资源定义版本
如你所见,我们选择了delayedresources.mydomain.com组。这个组理论上会包含所有其他延迟类型的 CRD——例如,DelayedDaemonSet或DelayedDeployment。
接下来,我们有 CRD 的主要部分。在versions字段下,我们可以定义一个或多个 CRD 版本(在name字段中),以及该版本的 API 规范。然后,当你创建 CRD 实例时,可以在 YAML 的apiVersion键的版本参数中定义将使用的版本——例如,apps/v1,或者在这种情况下是delayedresources.mydomain.com/v1。
每个版本项也有一个served属性,基本上是用来定义该版本是否启用。如果served为false,则 Kubernetes API 不会创建该版本,并且对于该版本的 API 请求(或kubectl命令)将失败。
此外,可以在特定版本上定义一个deprecated键,这会导致当使用已弃用版本对 API 发出请求时,Kubernetes 返回警告信息。这就是带有弃用版本的 CRD yaml 文件的样子——我们去掉了一些 spec 以保持 YAML 简洁:
Custom-resource-definition-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,向其发送请求,传递关于传入资源对象的信息,包括其当前版本和需要转换到的版本。
其原理是我们的Webhook服务器将处理转换并返回经过修正的 Kubernetes 资源对象。Webhook策略很复杂,可能有许多配置选项,本书中不会详细探讨。
重要提示
要了解如何配置转换 Webhook,请查看官方 Kubernetes 文档:kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/。
现在,回到我们 YAML 文件中的 version 条目!在 served 和 storage 键下,我们看到 schema 对象,它包含我们资源的实际规格。如前所述,这遵循 OpenAPI Spec v3 的规范。
由于空间原因,前面的代码块中移除了 schema 对象,下面是其内容:
Custom-resource-definition-3.yaml(续)
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
delaySeconds:
type: integer
image:
type: string
如你所见,我们支持一个名为 delaySeconds 的字段,它是一个整数,以及一个名为 image 的字段,它是一个字符串,表示我们的容器镜像。如果我们真的想让 DelayedJob 适用于生产环境,我们还需要包括各种其他选项,使其更接近原始的 Kubernetes Job 资源——但这并非我们的目的。
回到原始代码块中的版本列表之外,我们看到了一些其他属性。首先是 scope 属性,它可以是 Cluster 或 Namespaced。这个属性告诉 Kubernetes 是否将 CRD 对象的实例视为命名空间特定的资源(例如 Pods、Deployments 等),或者视为集群范围的资源——比如命名空间本身,因为在一个命名空间内获取命名空间对象是没有意义的!
最后,我们有 names 块,它允许你定义资源名称的复数和单数形式,以供在不同情况下使用(例如,kubectl get pods 和 kubectl 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 概念:
-
首先,让我们在 Kubernetes 中创建 CRD 规范——就像我们创建任何其他对象一样:
kubectl apply -f delayedjob-crd-spec.yaml这将产生以下输出:
customresourcedefinition "delayedjob.delayedresources.mydomain.com" has been created -
现在,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 数据库中添加了一张新的 table。
仅仅因为我们给我们的 DelayedJob 资源分配了一个应用镜像和一个 delaySeconds 字段,并不意味着我们期望的功能就会发生。通过创建我们的 DelayedJob 实例,我们只是向该 table 添加了一个条目。我们可以使用 Kubernetes API 或 kubectl 命令获取、编辑或删除它,但没有实现任何应用功能。
为了让我们的 DelayedJob 资源执行某些操作,我们需要一个自定义控制器,它将处理我们的 DelayedJob 实例并执行相应操作。最终,我们仍然需要使用官方的 Kubernetes 资源 —— 如 Pods 等,来实现实际的容器功能。
这就是我们现在要讨论的内容。有许多方法可以为 Kubernetes 构建自定义控制器,但一种流行的方法是将 DelayedJob 资源赋予其自身的生命周期。
使用 Kubernetes 操作符实现自管理功能
在讨论 Kubernetes 操作符之前,必须先讨论 Operator Framework。一个常见的误解是认为操作符是通过 Operator Framework 特别构建的。Operator Framework 是一个开源框架,最初由 Red Hat 创建,旨在简化 Kubernetes 操作符的编写。
实际上,操作符只是一个自定义控制器,它与 Kubernetes 接口并对资源进行操作。Operator Framework 是一种创建 Kubernetes 操作符的标准方法,但你也可以使用许多其他开源框架,或者从零开始创建一个!
在使用框架构建操作符时,最受欢迎的两个选项是前面提到的 Operator Framework 和 Kubebuilder。
这两个项目有很多相似之处。它们都使用了 controller-tools 和 controller-runtime,这两个库是用于构建 Kubernetes 控制器的官方支持库。如果你是从零开始构建操作符,使用这些官方支持的控制器库会让事情变得更容易。
与 Operator Framework 不同,Kubebuilder 是 Kubernetes 项目的一部分,类似于 controller-tools 和 controller-runtime 库 —— 但这两个项目都有其优缺点。重要的是,这两种选项,以及操作符模式,通常都要求控制器运行在集群内。乍一看,这似乎是最佳选择,但你也可以在集群外部运行控制器,并使其正常工作。要开始使用 Operator Framework,可以访问其官方 GitHub:github.com/operator-framework。要了解 Kubebuilder,可以访问:github.com/kubernetes-sigs/kubebuilder。
大多数操作符,无论框架如何,都遵循控制循环范式 – 让我们来看一下这个概念是如何工作的。
映射操作符控制循环
控制循环是一种系统设计和编程中的控制方案,它由一个永不停息的逻辑处理循环组成。通常,控制循环采用测量-分析-调整的方法,其中它测量系统的当前状态,分析需要哪些变化以使其符合预期状态,然后调整系统组件,使其与预期状态一致(或至少更接近)。
在 Kubernetes 操作符或控制器中,这个操作通常是这样的:
-
首先是
监视步骤 – 即监视 Kubernetes API 中的预期状态变化,该状态存储在etcd中。 -
然后,
分析步骤 – 即控制器决定如何操作以使集群状态与预期状态一致。 -
最后是
更新步骤 – 即更新集群状态以实现集群变化的预期目标。
为了帮助理解控制循环,这里有一个图示,展示了各个部分如何组合在一起:

图 13.1 – 测量分析更新循环
让我们使用 Kubernetes 调度器 – 它本身就是一个控制循环过程 – 来说明这个问题:
-
让我们从一个假设的集群开始,该集群处于稳定状态:所有 Pod 都已调度,节点健康,所有操作正常。
-
然后,用户创建一个新的 Pod。
我们之前讨论过,kubelet 是基于拉取机制工作的。这意味着,当 kubelet 在其 Node 上创建 Pod 时,Pod 已经通过调度器分配到了该 Node。然而,当 Pod 通过kubectl create或kubectl apply命令首次创建时,Pod 尚未被调度或分配到任何地方。这就是我们的调度器控制循环开始的地方:
-
第一步是测量,即调度器读取 Kubernetes API 的状态。当从 API 列出 Pods 时,它发现其中一个 Pod 没有分配到 Node。然后,它进入下一步。
-
接下来,调度器对集群状态和 Pod 需求进行分析,以决定将 Pod 分配到哪个 Node。正如我们在前几章中讨论的,这个过程考虑了 Pod 资源限制和请求、Node 状态、放置控制等,因此是一个相当复杂的过程。一旦处理完成,更新步骤就可以开始。
-
最后,更新 – 调度器通过将 Pod 分配到从步骤 2分析中获得的 Node,来更新集群状态。此时,kubelet 接管自己的控制循环,并在其 Node 上为 Pod 创建相关容器。
接下来,让我们把从调度器控制循环中学到的东西应用到我们自己的DelayedJob资源中。
为自定义资源定义设计操作符
实际上,为我们的 DelayedJob CRD 编写操作符超出了本书的范围,因为它需要编程语言的知识。如果你选择编程语言来构建操作符,Go 提供了与 Kubernetes SDK、controller-tools 和 controller-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 步:分析
接下来,让我们回顾一下我们控制器伪代码中的 analyzeDelayedJobs 函数:
Analysis-function.pseudo
// 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;
}
如你所见,前面的函数通过检查 DelayedJob 对象的某个注释值来遍历来自集群的 DelayedJob 对象列表,以确定该对象是否已被调度。如果尚未调度,它将一个对象添加到名为 listOfJobsToSchedule 的数组中,该数组包含在 DelayedJob 对象中指定的图像、一个命令用于在 DelayedJob 对象中指定的秒数后休眠,以及 DelayedJob 的原始名称,我们将在更新步骤中使用该名称来标记为已调度。
最后,在 analyzeDelayedJobs 函数返回我们新创建的 listOfJobsToSchedule 数组给主函数后,我们将通过最终的更新步骤来完成我们的 Operator 设计,即主循环中的 scheduleDelayedJobs 函数。
第 3 步:更新
最后,我们控制循环的更新部分将根据分析的输出更新集群,以创建预期的状态。以下是伪代码:
Update-function.pseudo
// 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"
});
}
}
在这种情况下,我们将常规的 Kubernetes 对象(它是从我们的DelayedJob对象派生的)创建到 Kubernetes 中,以便Kube调度器可以捕捉它,创建相关的 Pod 并进行管理。一旦我们创建了带有延迟的常规 Job 对象,我们还会更新DelayedJob CRD 实例,添加一个注释,将is-scheduled注释设置为true,从而防止它被重新调度。
这完成了我们的控制循环——从此时起,Kube调度器接管,我们的 CRD 作为 Kubernetes Job 对象诞生,它控制一个 Pod,最终被分配到一个节点上,并且容器被安排运行我们的代码!
这个例子当然是高度简化的,但你会惊讶于有多少 Kubernetes 操作员执行简单的控制循环来协调 CRD,并将其简化为基本的 Kubernetes 资源。操作员可以变得非常复杂,并执行特定应用的功能,比如备份数据库、清空持久化卷等——但这些功能通常与被控制的对象紧密耦合。
现在我们已经讨论了 Kubernetes 控制器中的 Operator 模式,我们可以谈论一些针对特定云的 Kubernetes 控制器的开源选项。
使用特定于云的 Kubernetes 扩展
云特定的 Kubernetes 扩展和控制器通常在托管的 Kubernetes 服务中默认可用,例如 Amazon EKS、Azure AKS 和 Google Cloud 的 GKE,它们可以与相应的云平台紧密集成,并使从 Kubernetes 控制其他云资源变得容易。
即使不添加任何额外的第三方组件,这些特定于云的功能也可以通过云控制器管理器(CCM)组件在上游 Kubernetes 中使用,该组件包含许多与主要云服务提供商集成的选项。这是公共云上每个托管 Kubernetes 服务通常默认启用的功能——但它们也可以与在该特定云平台上运行的任何集群集成,无论该集群是否是托管的。
在本节中,我们将回顾一些常见的 Kubernetes 云扩展,包括云控制器管理器(CCM)和需要安装其他控制器的功能,如external-dns和cluster-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,我们将 CCM 部署在kube-system命名空间中。我们还使用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,接下来让我们深入了解它提供的一些功能。
了解 cloud-controller-manager 的功能
默认的 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。
使用 external-dns 与 Kubernetes
external-dns 库是一个官方支持的 Kubernetes 插件,它允许集群配置外部 DNS 提供商,以自动化方式为服务和入口提供 DNS 解析。external-dns 插件支持广泛的云提供商,如 AWS 和 Azure,以及其他 DNS 服务,如 Cloudflare。
重要提示
要安装external-dns,可以访问官方的 GitHub 仓库 github.com/kubernetes-sigs/external-dns。
一旦在集群中实现了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 被调度。
同样,当任何节点上的 Pod 可以被清空而不对其他节点造成内存或 CPU 压力时,集群自动扩展器将执行向下扩展操作。
要安装 cluster-autoscaler,只需按照云服务提供商提供的正确安装指南进行操作,选择适合的集群类型和目标版本的 cluster-autoscaler。例如,AWS 上 EKS 的 cluster-autoscaler 安装说明可以在aws.amazon.com/premiumsupport/knowledge-center/eks-cluster-autoscaler-setup/找到。
接下来,让我们通过检查 Kubernetes 生态系统,来看一下如何找到开源和闭源的 Kubernetes 扩展。
与生态系统集成
Kubernetes(以及更广泛的云原生)生态系统庞大,包含数百个流行的开源软件库,还有成千上万个新兴的库。由于每个月都会有新技术需要评估,而且收购、整合或公司倒闭可能会将你最喜欢的开源库变成无人维护的烂摊子,因此在这个生态系统中导航是非常困难的。
幸运的是,这个生态系统中有一些结构,了解这些结构对于帮助我们在云原生开源项目的选择中导航非常重要。这个结构的第一个重要组成部分就是 Cloud Native Computing Foundation 或 CNCF。
介绍 Cloud Native Computing Foundation
CNCF 是 Linux 基金会的一个子基金会,Linux 基金会是一个非盈利实体,负责托管开源项目,并协调不断变化的公司名单,这些公司为开源软件做出贡献并使用开源软件。
CNCF 的成立几乎完全是为了引领 Kubernetes 项目的未来。它在 Kubernetes 1.0 发布时宣布成立,随后扩展到涵盖了云原生领域的数百个项目 —— 从 Prometheus 到 Envoy 再到 Helm,还有更多项目。
查看 CNCF 组成项目的概览的最佳方式是查看 CNCF 云原生景观,网址为 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。首先,我们讨论了 CRD —— 它是什么,相关的使用案例,以及如何在集群中实现它们。接下来,我们回顾了 Kubernetes 中操作员的概念,并讨论了如何使用操作员或自定义控制器为 CRD 赋予生命。
然后,我们讨论了 Kubernetes 针对云服务提供商的特定扩展,包括 cloud-controller-manager、external-dns 和 cluster-autoscaler。最后,我们介绍了云原生开源生态系统的一些基本概念,并提供了一些发现适合自己用例的项目的优秀方法。
本章中使用的技能将帮助你扩展 Kubernetes 集群,以便与云服务提供商及你自定义的功能进行交互。
在下一章中,我们将讨论应用于 Kubernetes 的两种新兴架构模式——无服务器架构和服务网格。
问题
-
CRD 的服务版本与存储版本有什么区别?
-
自定义控制器或操作符控制循环的三个典型部分是什么?
-
cluster-autoscaler如何与现有云提供商的自动扩展解决方案(如 AWS 自动扩展组)交互?
深入阅读
-
CNCF Landscape:
landscape.cncf.io/ -
官方 Kubernetes 论坛:
discuss.kubernetes.io/
第十四章:服务网格与无服务器架构
本章讨论了高级 Kubernetes 模式。首先,它详细介绍了当前流行的服务网格模式,其中可观察性和服务到服务的发现由边车代理处理,并提供了设置流行服务网格 Istio 的指南。最后,它描述了无服务器模式及其如何在 Kubernetes 中应用。本章的主要案例研究将包括为示例应用程序设置 Istio 和服务发现,以及 Istio 入口网关的配置。
让我们从边车代理的讨论开始,它为服务网格提供了服务到服务连接的基础。
在本章中,我们将涵盖以下主题:
-
使用边车代理
-
向 Kubernetes 添加服务网格
-
在 Kubernetes 上实现无服务器架构
技术要求
为了运行本章中详细介绍的命令,您需要一台支持 kubectl 命令行工具的计算机,并且需要一个正在运行的 Kubernetes 集群。请参阅 第一章,与 Kubernetes 通信,了解如何快速启动并运行 Kubernetes,并查看如何安装 kubectl 工具的说明。
本章中使用的代码可以在本书的 GitHub 仓库中找到,网址是 github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter14。
使用边车代理
如本书前面提到的,边车是一种模式,在这种模式中,一个 Pod 除了运行实际应用容器外,还包含另一个容器。这个额外的容器就是边车。边车有多种用途,其中最常见的用途包括监控、日志记录和代理。
对于日志记录,边车容器可以从应用容器中获取日志(因为它们可以共享卷并在本地主机上进行通信),然后将日志发送到集中式日志堆栈,或解析日志以便进行警报。监控也是类似的情况,边车 Pod 可以跟踪并发送关于应用 Pod 的度量数据。
使用边车代理时,当请求进入 Pod 时,它们首先会进入代理容器,代理容器会在记录日志或进行其他过滤后,将请求路由到应用容器。同样,当请求离开应用容器时,它们首先会进入代理,代理可以提供从 Pod 外部的路由。
通常,像 NGINX 这样的代理边车仅为进入 Pod 的请求提供代理服务。然而,在服务网格模式中,进入和离开 Pod 的请求都必须经过代理,这为服务网格模式本身提供了基础。
请参考以下图表,了解边车代理如何与应用容器进行交互:

图 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 等)级别指定。
让我们看一下如何将 NGINX 配置为边车,下面的 Deployment YAML 我们暂时还不创建。这个过程比使用 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
如你所见,我们指定了两个容器,一个是我们的主应用容器myapp,另一个是nginx边车容器,在这里我们通过卷挂载注入了一些配置,以及一些 TLS 证书。
接下来,让我们看一下同一文件中的volumes规格,在这里我们注入了一些证书(来自 secret)和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 中的容器可以共享本地主机端口,这就充当了我们的边车代理。为了本书的目的,我们不再逐一审查其他行,但你可以查看 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 上的生产环境部署中,可能会使用某种形式的动态、API 驱动的 Envoy 配置。这个功能叫做 xDS,并且被 Istio 所使用。此外,还有其他开源产品和解决方案也使用 Envoy 配合 xDS,包括 Ambassador API 网关。
本书的目的下,我们将查看一些静态(非动态)Envoy 配置;这样我们可以逐个分析配置的每一部分,并且当我们回顾 Istio 时,你将对一切工作原理有清晰的了解。
现在让我们深入了解一个 Envoy 配置,其中单个 Pod 需要能够将请求路由到两个服务,Service 1 和 Service 2。该配置如下所示:

图 14.2 – 出站 Envoy 代理
如你所见,应用 Pod 中的 Envoy sidecar 将有配置来路由到两个上游服务,Service 1 和 Service 2。这两个服务各自有两个可能的端点。
在一个动态设置中,Envoy xDS 会从 API 加载端点的 Pod IP,但为了方便我们的审查,我们将在端点中展示静态的 Pod IP。我们将完全忽略 Kubernetes 服务,而是直接在轮询配置中访问 Pod IP。在服务网格场景中,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_key 和一个 certificate_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 代理容器(或 sidecar)。它监听两个端口。接下来,在 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
最后,我们需要我们的下游服务,service1 和 service2。为此,我们将继续使用开源的 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 添加服务网格
服务网格模式是 sidecar 代理的逻辑扩展。通过将 sidecar 代理附加到每个 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”配置:
-
在集群上安装 Istio 的第一步是安装 Istio CLI 工具。我们可以使用以下命令来安装 CLI 工具的最新版本:
curl -L https://istio.io/downloadIstio | sh - -
接下来,我们需要将 CLI 工具添加到路径中,以方便使用:
cd istio-<VERSION> export PATH=$PWD/bin:$PATH -
现在,让我们安装 Istio!Istio 的配置被称为配置文件,正如前面提到的,它们可以通过 YAML 文件完全自定义。
在本示例中,我们将使用 Istio 的内置
demo配置文件,它提供了一些基本的设置。使用以下命令安装配置文件:istioctl install --set profile=demo这将产生以下输出:
![图 14.3 – Istioctl 配置文件安装输出]()
图 14.3 – Istioctl 配置文件安装输出
-
由于 sidecar 资源在 Kubernetes 1.19 版本中尚未发布,Istio 将自动将 Envoy 代理注入到任何带有
istio-injection=enabled标签的命名空间中。要为任何命名空间添加标签,请运行以下命令:
kubectl label namespace my-namespace istio-injection=enabled -
为了方便测试,使用之前的
label命令为default命名空间添加标签。一旦 Istio 组件启动,该命名空间中的任何 Pod 将自动注入 Envoy sidecar,就像我们在上一节中手动创建的那样。为了从集群中移除 Istio,请运行以下命令:
istioctl x uninstall --purge这应该会显示一条确认消息,告诉你 Istio 已经被移除。
-
现在,让我们部署一些内容来测试我们新的网格!我们将部署三个不同的应用服务,每个服务都有一个部署和一个服务资源:
a. 服务前端
b. 服务后端 A
c. 服务后端 B
这是服务前端的部署配置:
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这是服务前端的服务配置:
apiVersion: v1 kind: Service metadata: name: service-frontend spec: selector: name: service-frontend ports: - protocol: TCP port: 80 targetPort: 5000服务后端 A 和 B 的 YAML 与服务前端相同,除了交换名字、镜像名称和选择器标签。
-
现在我们有了几个服务来路由(和在其间路由),让我们开始设置一些 Istio 资源!
第一件事,我们需要一个
Gateway资源。在这个例子中,我们并没有使用 NGINX Ingress Controller,但这没关系,因为 Istio 提供了一个Gateway资源,可以用于入口和出口。以下是一个 IstioGateway定义的样子: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定义看起来与入口记录非常相似。我们有name和selector,Istio 使用这些来决定使用哪个 Istio Ingress Controller。接下来,我们有一个或多个服务器,它们本质上是网关上的入口点。在这种情况下,我们不限制主机,并且接受在80端口上的请求。 -
现在我们有了一个网关来接收请求进入集群,我们可以开始设置一些路由了。我们在 Istio 中通过
VirtualService来实现。Istio 中的VirtualService是一组路由规则,当向特定主机名发出请求时,这些路由应该被遵循。此外,我们可以使用通配符主机来为来自网格中任何地方的请求制定全局规则。让我们看一个VirtualService配置的例子: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前缀之一匹配,则指向我们的入口点服务前端。在这种情况下,我们按前缀进行匹配,但你也可以通过将prefix替换为exact在 URI 匹配器中使用精确匹配。 -
所以,现在我们有了一个设置,和 NGINX Ingress 相似,集群的入口由路由匹配来决定。
但是,路由中的
v1代表什么?这实际上表示我们的前端服务的一个版本。让我们通过一个新的资源类型来指定这个版本——IstioDestinationRule。以下是一个DestinationRule配置的样子: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 将所有请求路由到该版本的服务。此外,我们的v1版本也已配置,并在之前的VirtualService中进行了引用。这条硬规则是路由请求到 Istio 中不同子集的一个可能方式。现在,我们已经通过网关成功地将流量路由到我们的集群,并根据目标规则路由到虚拟服务子集。此时,我们实际上已经“进入”了我们的服务网格!
-
现在,从我们的服务前端,我们希望能够路由到服务后端 A和服务后端 B。我们该如何做到这一点?更多的虚拟服务是答案!让我们来看一下后端服务 A的虚拟服务:
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将路由到我们服务service-backend-a的v1子集。我们还需要为service-backend-b创建另一个VirtualService,尽管我们不会将其全部包括在内(但它几乎完全相同)。要查看完整的 YAML 文件,请查看代码库中的istio-virtual-service-3.yaml。 -
一旦我们的虚拟服务准备好,我们就需要一些目标规则!后端服务 A的
DestinationRule如下所示:
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
对于后端服务 B的DestinationRule也很相似,只是子集不同。我们不会包括代码,但可以在代码库中的istio-destination-rule-3.yaml查看具体规范。
这些目标规则和虚拟服务加起来形成了以下路由图:

图 14.4 – Istio 路由图
如你所见,来自前端服务的请求可以路由到后端服务 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。
使用 Knative 在 Kubernetes 上实现 FaaS
如前所述,Knative 是一套用于 Kubernetes 上无服务器模式的模块化构建块。因此,在实际使用功能之前,需要进行一些配置。Knative 也可以与 Istio 一起安装,Istio 被用作无服务器应用程序路由和扩展的基础层。也提供了其他非 Istio 的路由选项。
要在 FaaS 中使用 Knative,我们需要安装 Knative Serving 和 Knative Eventing。虽然 Knative Serving 使我们能够运行无服务器工作负载,但 Knative Eventing 将提供一个途径,以便向这些自动扩展到零的工作负载发出 FaaS 请求。让我们按照以下步骤来实现这一目标:
-
首先,让我们安装 Knative Serving 组件。我们将从安装 CRDs 开始:
kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-crds.yaml -
接下来,我们可以安装服务组件本身:
kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-core.yaml -
此时,我们需要为 Knative 安装一个网络/路由层。让我们使用 Istio:
kubectl apply --filename https://github.com/knative/net-istio/releases/download/v0.18.0/release.yaml -
我们需要从 Istio 获取网关 IP 地址。根据你的运行环境(即 AWS 或本地),这个值可能有所不同。使用以下命令获取它:
Kubectl get service -n istio-system istio-ingressgateway -
Knative 需要特定的 DNS 设置来启用服务组件。在云环境中,最简单的方法是使用
xip.io的“魔术 DNS”,不过这在基于 Minikube 的集群中无法使用。如果您正在运行这些集群(或者只是想查看所有可用选项),请查看 Knative 文档:knative.dev/docs/install/any-kubernetes-cluster/。要设置魔术 DNS,请使用以下命令:
kubectl apply --filename https://github.com/knative/serving/releases/download/v0.18.0/serving-default-domain.yaml -
现在我们已经安装了 Knative Serving,让我们安装 Knative Eventing 来传递我们的 FaaS 请求。首先,我们需要更多的 CRD。请使用以下命令安装它们:
kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/eventing-crds.yaml -
现在,像我们安装服务组件一样安装事件组件:
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/。 -
要安装
in-memory消息层,请使用以下命令:kubectl apply --filename https://github.com/knative/eventing/releases/download/v0.18.0/in-memory-channel.yaml -
以为我们已经完成了?不!还有最后一件事。我们需要安装一个代理,它将从消息层获取事件,并将其处理到正确的位置。让我们使用默认的代理层,即 MT-Channel 代理层。您可以使用以下命令安装它:
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 YAML 文件如下所示:
Knative-broker.yaml:
apiVersion: eventing.knative.dev/v1
kind: broker
metadata:
name: my-broker
namespace: default
接下来,我们可以创建一个消费者服务。这个组件实际上就是我们的应用程序,用来处理事件——即我们的函数本身!为了避免展示更多 YAML 文件内容,假设我们的消费者服务只是一个名为 service-consumer 的普通 Kubernetes 服务,它将请求路由到一个包含四个副本的 Pod 部署,这些 Pod 运行我们的应用程序。
最后,我们需要一个触发器。它决定了如何以及哪些事件会从代理中路由过来。触发器的 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 测试我们的触发器。我们需要发出的请求需要有一个 Ce-Type 头,其值为 myeventtype,因为这是我们的触发器所要求的。Knative 使用 Ce-Id、Ce-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 charts,地址是 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 图表会作为钩子创建这些凭证,并将它们存储在一个 secret 中,因此我们可以通过运行以下命令来获取它们:
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 时使用了 ingress 设置,以便请求能够通过该 ingress。不过,我们的新函数生成的 YAML 文件是设置为在 localhost:8080 上进行请求的,目的是用于开发。我们可以编辑该文件,修改为 ingress 网关的正确 URL(可以参考文档 docs.openfaas.com/deployment/kubernetes/ 来了解如何操作),但我们可以使用一个捷径,让 OpenFaaS 在本地打开。
我们可以使用 kubectl port-forward 命令将 OpenFaaS 服务映射到本地的 8080 端口。操作方法如下:
export OPENFAAS_URL=http://127.0.0.1:8080
kubectl port-forward -n openfaas svc/gateway 8080:8080
现在,让我们将之前生成的身份验证凭证添加到 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 上运行 sidecar 代理,特别是使用 Envoy 代理。
接着,我们转向了服务网格,学习了如何安装和配置 Istio 服务网格,以便进行服务到服务的路由,并实现双向 TLS。
最后,我们转向了 Kubernetes 上的无服务器模式,在这里你学习了如何配置和安装 Knative,以及另一个无服务器事件和 FaaS 解决方案 OpenFaaS。
本章中你所使用的技能将帮助你在 Kubernetes 上构建服务网格和无服务器模式,帮助你实现完全自动化的服务到服务发现和 FaaS 事件处理。
在下一章(也是最后一章),我们将讨论如何在 Kubernetes 上运行有状态的应用程序。
问题
-
静态和动态 Envoy 配置有什么区别?
-
Envoy 配置的四个主要部分是什么?
-
Knative 的一些缺点是什么?OpenFaaS 又是如何对比的?
进一步阅读
-
CNCF landscape:
landscape.cncf.io/ -
官方 Kubernetes 论坛:
discuss.kubernetes.io/
第十五章:Kubernetes 上的有状态工作负载
本章详细介绍了当前在数据库中运行有状态工作负载的行业现状。我们将讨论使用 Kubernetes(以及流行的开源项目)在 Kubernetes 上运行数据库、存储和队列。案例研究教程将包括在 Kubernetes 上运行对象存储、数据库和队列系统。
在本章中,我们将首先了解有状态应用如何在 Kubernetes 上运行,然后学习如何为有状态应用使用 Kubernetes 存储。接下来,我们将学习如何在 Kubernetes 上运行数据库,并讨论消息和队列。我们先从讨论为什么有状态应用比无状态应用在 Kubernetes 上更复杂开始。
本章将涵盖以下主题:
-
理解 Kubernetes 上的有状态应用
-
使用 Kubernetes 存储进行有状态应用
-
在 Kubernetes 上运行数据库
-
在 Kubernetes 上实现消息和队列
技术要求
为了运行本章中详细介绍的命令,你需要一台支持 kubectl 命令行工具并且有一个工作的 Kubernetes 集群的计算机。请参见第一章,与 Kubernetes 通信,了解几种快速启动 Kubernetes 的方法,并获得如何安装 kubectl 工具的说明。
本章中使用的代码可以在书籍的 GitHub 仓库中找到:
github.com/PacktPublishing/Cloud-Native-with-Kubernetes/tree/master/Chapter15
理解 Kubernetes 上的有状态应用
Kubernetes 提供了出色的原语来运行无状态和有状态应用,但有状态工作负载在 Kubernetes 上的成熟度要花费更长时间。然而,近年来,一些高调的 Kubernetes 基础的有状态应用框架和项目证明了有状态应用在 Kubernetes 上的逐步成熟。让我们先回顾一下其中的一些,为本章的其他部分奠定基础。
流行的 Kubernetes 原生有状态应用
有许多类型的有状态应用。尽管大多数应用是有状态的,但只有其中某些组件存储了状态数据。我们可以从应用中剔除这些特定的有状态组件,专注于这些组件进行回顾。在本书中,我们将讨论数据库、队列和对象存储,排除像在第七章中回顾的持久存储组件,Kubernetes 上的存储。我们还会提到一些较少通用的组件,作为荣誉提名。我们从数据库开始吧!
Kubernetes 兼容的数据库
除了典型的 数据库(DBs)和键值存储,如 Postgres、MySQL 和 Redis,这些都可以使用 StatefulSets 或社区操作器在 Kubernetes 上进行部署外,还有一些专为 Kubernetes 设计的重要选项:
-
CockroachDB:一种分布式 SQL 数据库,可以无缝地在 Kubernetes 上部署
-
Vitess:MySQL 分片协调器,允许 MySQL 的全球可扩展性,也可以通过操作器在 Kubernetes 上安装
-
YugabyteDB:一种分布式 SQL 数据库,类似于 CockroachDB,也支持类似 Cassandra 的查询
接下来,让我们来看一下 Kubernetes 上的队列和消息传递。
Kubernetes 上的队列、流处理和消息传递
另外,还有一些行业标准的选项,如 Kafka 和 RabbitMQ,可以使用社区 Helm charts 和操作器在 Kubernetes 上进行部署,除此之外,还有一些专门制作的开源和闭源选项:
-
NATS:开源消息和流处理系统
-
KubeMQ:原生 Kubernetes 消息代理
接下来,让我们看看 Kubernetes 上的对象存储。
Kubernetes 上的对象存储
对象存储从 Kubernetes 中获取基于卷的持久存储,并添加一个对象存储层,类似于(并且在许多情况下兼容)Amazon S3 的 API:
-
Minio:与 S3 兼容的对象存储,专为高性能设计。
-
Open IO:类似于 Minio,具有高性能,支持 S3 和 Swift 存储。
接下来,让我们看一下几项荣誉提名。
荣誉提名
除了前面提到的通用组件外,还有一些更专门(但仍然是分类性的)有状态应用程序可以在 Kubernetes 上运行:
-
密钥和身份管理:Vault、Keycloak
-
容器注册表:Harbor、Dragonfly、Quay
-
工作流管理:Apache Airflow 与 Kubernetes 操作器
现在我们已经回顾了一些有状态应用程序的类别,让我们讨论一下这些状态密集型应用程序通常是如何在 Kubernetes 上实现的。
理解在 Kubernetes 上运行有状态应用程序的策略
尽管使用 ReplicaSet 或 Deployment 在 Kubernetes 上部署有状态应用程序本身没有问题,但你会发现大多数在 Kubernetes 上运行的有状态应用程序使用的是 StatefulSets。我们在第四章《扩展和部署你的应用程序》中讨论过 StatefulSets,但为什么它们对应用程序如此有用呢?我们将在本章中回顾并解答这个问题。
主要原因是 Pod 的身份。许多分布式有状态应用程序有自己的集群机制或共识算法。为了平滑处理这些类型应用程序的过程,StatefulSets 提供了基于序号系统的静态 Pod 命名,从 0 到 n。结合滚动更新和创建方法,使得应用程序更容易自我集群化,这对于像 CockroachDB 这样的云原生数据库来说非常重要。
为了说明 StatefulSets 如何以及为什么能帮助在 Kubernetes 上运行有状态应用程序,让我们看看如何在 Kubernetes 上使用 StatefulSets 运行 MySQL。
现在,为了明确,在 Kubernetes 上运行单个 MySQL Pod 是非常简单的。我们需要做的就是找到一个 MySQL 容器镜像,并确保它具有正确的配置和 startup 命令。
然而,当我们尝试扩展我们的数据库时,就会遇到一些问题。与简单的无状态应用程序不同,在无状态应用程序中,我们可以在不创建新状态的情况下扩展我们的部署,而 MySQL(像许多其他数据库一样)有自己的集群和共识方法。MySQL 集群中的每个成员都知道其他成员,最重要的是,它知道集群中的哪个成员是领导者。这就是像 MySQL 这样的数据库能够提供一致性保证和 原子性、一致性、隔离性、持久性(ACID)合规性的方式。
因此,由于 MySQL 集群中的每个成员都需要了解其他成员(最重要的是主节点),我们需要以一种方式运行我们的数据库 Pod,使它们能够以一种共同的方式找到并与数据库集群中的其他成员进行通信。
StatefulSets 提供这种功能的方式是,正如我们在本节开始时提到的,通过序号 Pod 编号。通过这种方式,应用程序在 Kubernetes 上运行时,如果需要自我集群化,它们知道会使用从 0 到 n 的共同命名方案。此外,当特定序号的 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 的开头。在这个 Pod 中,initContainers 和常规容器之间会有相当多的容器在运行,因此我们将分别解释每个容器。接下来是第一个 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 实例和常规运行容器。这是因为该容器包含了正确的嵌入式脚本和工具,能够以编程方式完成常见的设置任务。
在这个例子中,MySQL 的 initContainer 创建了一个文件 /mnt/conf.d/server-id.cnf,并向文件中添加了一个 server ID,值对应于 StatefulSet 中 Pod 的 ordinal ID。当写入 ordinal ID 时,它会加上 100 作为偏移量,以绕过 MySQL 中保留的 server-id 值为 0 的限制。
然后,根据 Pod 的 ordinal D 是否为 0,它会将 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,搭配一个全新且空的 PersistentVolume 加入集群。在这种情况下,数据复制进程需要通过复制其他 MySQL 集群成员的数据来填充数据。对于大型数据库,这个过程可能非常缓慢。
因此,我们有一个 initContainer 实例,它从 StatefulSet 中的另一个 MySQL Pod 加载数据,这样 MySQL 的数据复制功能就有了起始数据。如果 MySQL Pod 中已经有数据,则不会进行数据加载。[[ -d /var/lib/mysql/mysql ]] && exit 0 这一行代码会检查是否已有数据。
一旦这两个 initContainer 实例成功完成任务,我们就拥有了第一个 initContainer 提供的所有 MySQL 配置,同时也获得了来自 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 还有一些存活性(liveness)和就绪性(readiness)探针配置——有关这些配置,请查看本书的 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 加载数据,以便让该 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 的这些特性,结合 Pod 和工具的正确配置,使得在 Kubernetes 上轻松扩展有状态数据库成为可能。
既然我们已经讨论了为什么有状态的 Kubernetes 应用可能会使用 StatefulSets,那么让我们开始实现一些示例来验证这一点!我们将从一个对象存储应用开始。
在 Kubernetes 上部署对象存储
对象存储与文件系统或块存储不同。它提供了更高层次的抽象,封装了一个文件,赋予它一个标识符,并通常包括版本控制。然后可以通过其特定的标识符访问该文件。
最流行的对象存储服务可能是 AWS S3,但 Azure Blob Storage 和 Google Cloud Storage 是类似的替代方案。此外,还有几种可以在 Kubernetes 上运行的自托管对象存储技术,我们在前一部分中已有讨论。
在本书中,我们将审查在 Kubernetes 上配置和使用Minio。Minio 是一个高性能的对象存储引擎,可以部署在 Kubernetes 上,此外也支持其他编排技术,如Docker Swarm 和 Docker Compose。
Minio 支持使用 Operator 和 Helm chart 在 Kubernetes 上部署。在本书中,我们将重点讲解 Operator,但有关 Helm chart 的更多信息,请查看 Minio 文档 docs.min.io/docs。让我们从 Minio Operator 开始,这样我们就可以查看一些很酷的社区扩展功能,增强 kubectl 的功能。
安装 Minio Operator
安装 Minio Operator 与我们之前做的任何操作都大不相同。Minio 实际上提供了一个 kubectl 插件,用于管理 Operator 和 Minio 的安装与配置。
在本书中,我们没有详细讨论 kubectl 插件,但它们是 Kubernetes 生态系统中一个不断增长的部分。kubectl 插件可以通过新命令提供额外的功能。
为了安装 minio kubectl 插件,我们使用 Krew,Krew 是一个 kubectl 插件管理工具,它可以轻松地搜索和添加 kubectl 插件,只需一条命令。
安装 Krew 和 Minio kubectl 插件
首先,让我们安装 Krew。安装过程根据操作系统和环境的不同而有所不同,但对于 macOS,安装过程如下所示(有关更多信息,请查看 Krew 文档 krew.sigs.k8s.io/docs):
-
首先,让我们通过以下终端命令安装 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 ) -
现在,我们可以使用以下命令将 Krew 添加到我们的
PATH环境变量中:kubectl krew commands. -
要安装 Minio kubectl 插件,可以运行以下
krew命令:kubectl krew install minio
现在,安装了 Minio kubectl 插件后,让我们看看如何在集群上设置 Minio。
启动 Minio Operator
首先,我们需要在集群上安装 Minio Operator。这个部署将控制我们稍后需要执行的所有 Minio 任务:
-
我们可以使用以下命令安装 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 -
为了检查 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 是一个多租户系统,每个租户都有自己独立的命名空间,用于存储桶和对象的隔离,以及独立的 PersistentVolumes。此外,Minio Operator 会以分布式模式启动 Minio,确保高可用性和数据复制。
在创建 Minio 租户之前,我们需要为 Minio 安装一个 容器存储接口(CSI)驱动程序。CSI 是一种标准化的方式,用于在存储提供商和容器之间进行接口——而 Kubernetes 实现了 CSI,允许第三方存储提供商为 Kubernetes 编写自己的驱动程序,以实现无缝集成。Minio 推荐使用 Direct CSI 驱动程序来管理 Minio 的 PersistentVolumes。
要安装 Direct CSI 驱动程序,我们需要使用 Kustomize 执行 kubectl apply 命令。然而,安装 Direct CSI 驱动程序时需要设置一些环境变量,以便使用正确的配置创建 Direct CSI 配置,如下所示:
-
首先,让我们根据 Minio 的建议创建这个环境文件:
DIRECT_CSI_DRIVES=data{1...4} DIRECT_CSI_DRIVES_DIR=/mnt KUBELET_DIR_PATH=/var/lib/kubelet如您所见,环境文件决定了 Direct CSI 驱动程序将在哪里挂载卷。
-
一旦我们创建了
default.env,让我们使用以下命令将这些变量加载到内存中:export $(cat default.env) -
最后,让我们使用以下命令安装 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 -
在创建 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 -
现在,我们的 CSI 驱动程序已经安装完成,接下来让我们创建 Minio 租户——但首先,让我们看看
kubectl minio tenant create命令生成的 YAML 文件:Tenant Tenant CRD. This first part of our spec has two containers specified, a container for the Minio console and one for the Minio server itself. In addition, the replicas value mirrors what we specified in our kubectl minio tenant create command. Finally, it specifies the name of a secret for the Minio console.Next, let's look at the bottom portion of the 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: ""
As you can see, the `Tenant` resource specifies a number of servers (also specified by the `creation` command) that matches the number of replicas. It also specifies the name of the internal Minio Service, as well as a `volumeClaimTemplate` instance to be used.This spec, however, does not work for our purposes, since we are using the Direct CSI. Let's update the `zones` key with a new `volumeClaimTemplate` that uses the Direct CSI, as follows (save this file as `my-updated-minio-tenant.yaml`). Here's just the `zones` portion of that file, which we updated:zones:
- resources: {}
servers: 2
volumeClaimTemplate:
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 256Mi
storageClassName: direct.csi.min.io
-
现在,让我们继续创建我们的 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 输出
现在我们的 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 可用。由于在此示例中我们没有为 localhost 设置控制台的 TLS 证书,你将需要接受浏览器的安全警告。输入从前面的步骤中获取的 access 密钥和 secret 密钥进行登录!
现在我们已登录到控制台,可以开始向我们的 Minio 租户添加内容。首先,创建一个桶。为此,点击左侧边栏中的 Buckets,然后点击 Create Bucket 按钮。
在弹出的窗口中,输入桶的名称(在我们的例子中,我们将使用 my-bucket)并提交表单。你应该会看到列表中的一个新桶 – 请参考以下截图作为示例:

图 15.2 – 桶
我们现在已经准备好分布式 Minio 设置,并且已经有了一个桶可以上传。让我们通过上传一个文件到全新的对象存储系统来完成这个示例!
我们将使用 Minio CLI 进行上传,这使得与 S3 兼容的存储(如 Minio)交互的过程变得更加简单。我们将不使用本地机器上的 Minio CLI,而是在 Kubernetes 内运行一个预加载了 Minio CLI 的容器镜像,因为 TLS 设置仅在集群内访问 Minio 时有效。
首先,我们需要获取 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)
现在,让我们启动这个 Pod 并使用 Minio CLI。为此,使用以下 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 Pod 的 IP – 在我们的例子中,它们是 my-tenant-zone-0-0 和 my-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 控制台的Dashboard页面,查看该对象是否显示出来,如下图所示:

图 15.3 – Dashboard
如你所见,我们的文件已成功上传!
这就是关于 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 图表:
-
我们需要做的第一件事是添加 CockroachDB Helm 图表仓库,使用以下命令:
helm repo add cockroachdb https://charts.cockroachdb.com/这应该会产生以下输出:
"cockroachdb" has been added to your repositories -
在安装图表之前,让我们创建一个自定义的
values.yaml文件,以调整 CockroachDB 的一些默认设置。我们的文件在本演示中如下所示:storage: persistentVolume: size: 2Gi statefulset: resources: limits: memory: "1Gi" requests: memory: "1Gi" conf: cache: "256Mi" max-sql-memory: "256Mi"如你所见,我们指定了
2GB 的持久卷大小,1GB 的 Pod 内存限制和请求,以及 CockroachDB 的配置文件内容。该配置文件包括cache和最大memory的设置,分别设置为内存限制的 25%(256MB)。这个比例是 CockroachDB 的最佳实践。请记住,这些设置并不完全适用于生产环境,但它们适用于我们的演示。 -
此时,让我们使用以下 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如你所见,除了用于一些初始化任务的设置 Pod 外,我们还有三个 Pod 在 StatefulSet 中。
-
为了检查我们的集群是否正常工作,我们可以使用 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 命令了!
-
首先,使用以下命令创建一个数据库:
CREATE DATABASE mydb; -
接下来,让我们创建一个简单的表:
CREATE TABLE mydb.users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), first_name STRING, last_name STRING, email STRING ); -
然后,使用以下命令添加一些数据:
INSERT INTO mydb.users (first_name, last_name, email) VALUES ('John', 'Smith', 'jsmith@fake.com'); -
最后,让我们使用以下内容来确认数据:
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 图表:
-
首先,让我们添加正确的
helm仓库(由Bitnami提供):helm repo add bitnami https://charts.bitnami.com/bitnami -
接下来,让我们创建一个自定义的值文件,以调整一些参数:
auth: user: user password: test123 persistence: enabled: false如你所见,在这种情况下,我们禁用了持久化,这对于快速演示非常合适。
-
然后,可以使用以下命令轻松地在集群中安装 RabbitMQ:
helm install rabbitmq bitnami/rabbitmq --values values-rabbitmq.yaml一旦成功,你将看到来自 Helm 的确认消息。RabbitMQ Helm 图表还包含一个管理 UI,所以我们使用这个 UI 来验证我们的安装是否成功。
-
首先,让我们为
rabbitmq服务启动端口转发:http://localhost:15672. It will look like the following:Figure 15.4 – RabbitMQ management console login -
现在,我们应该能够使用在值文件中指定的用户名和密码登录仪表盘。登录后,你将看到 RabbitMQ 仪表盘的主视图。
重要的是,你将看到你的 RabbitMQ 集群中的节点列表。在我们的案例中,我们只有一个单节点,显示如下:
![图 15.5 – RabbitMQ 管理控制台节点项]()
图 15.5 – RabbitMQ 管理控制台节点项
对于每个节点,你可以看到其名称以及一些元数据,包括内存、运行时间等。
-
为了添加一个新队列,导航到顶部栏的队列,点击屏幕底部的添加新队列。按照以下方式填写表单,然后点击添加队列:
![图 15.6 – RabbitMQ 管理控制台队列创建]()
图 15.6 – RabbitMQ 管理控制台队列创建
如果成功,屏幕应该会刷新,并且你会看到新队列已添加到列表中。这意味着我们的 RabbitMQ 设置正常工作!
-
最后,既然我们已经有了一个队列,我们可以向其中发布消息。为此,点击你刚创建的队列,进入队列页面,然后点击发布消息。
-
在有效负载文本框中输入任何文本,然后点击发布消息。你应该会看到一个确认弹窗,告诉你消息已经成功发布,屏幕应该会刷新,显示队列中的消息,如下图所示:
![图 15.7 – RabbitMQ 管理控制台队列状态]()
图 15.7 – RabbitMQ 管理控制台队列状态
-
最后,为了模拟从队列中获取消息,点击页面底部附近的获取消息,此时会展开显示一个新区域,然后点击获取消息按钮。你应该能看到你发送的消息输出,证明队列系统正常工作!
总结
在本章中,我们了解了如何在 Kubernetes 上运行有状态工作负载。首先,我们回顾了几种有状态工作负载的高层次概述,并给出了每种类型的一些示例。接着,我们实际部署了其中一种工作负载——对象存储系统——到 Kubernetes 上。然后,我们做了同样的操作,部署了一个 NewSQL 数据库 CockroachDB,向你展示了如何轻松地在 Kubernetes 上部署一个 CockroachDB 集群。
最后,我们向你展示了如何使用 Helm 图表在 Kubernetes 上部署 RabbitMQ 消息队列。你在本章中使用的技巧将帮助你在 Kubernetes 上部署和使用流行的有状态应用模式。
如果你已经读到这里,感谢你陪伴我们走过本书的 15 章!我希望你已经学会如何使用 Kubernetes 的广泛功能,并且现在你拥有了构建和部署复杂应用所需的所有工具。
问题
-
Minio 的 API 与哪个云存储服务兼容?
-
StatefulSet 对分布式数据库有哪些好处?
-
用你自己的话来说,是什么让有状态应用在 Kubernetes 上运行变得困难?
进一步阅读
-
Minio 快速入门文档:
docs.min.io/docs/minio-quickstart-guide.html -
CockroachDB Kubernetes 指南:
www.cockroachlabs.com/docs/v20.2/orchestrate-a-local-cluster-with-kubernetes
第十六章:评估
第一章:– 与 Kubernetes 进行通信
-
容器编排是一种软件模式,在这种模式下,多个容器被控制和调度以服务于应用程序。
-
Kubernetes API 服务器(
kube-apiserver)处理更新 Kubernetes 资源的请求。调度器(kube-scheduler)决定将容器放置(调度)到哪里。控制器管理器(kube-controller-manager)确保 Kubernetes 资源的所需配置在集群中得到体现。etcd提供集群配置的数据存储。 -
kube-apiserver必须使用--authorization-mode=ABAC和--authorization-policy-file=filename参数启动。 -
为了控制平面的高可用性,以防其中一个主节点发生故障。
-
如果资源已经创建,
kubectl create将失败,因为资源已经存在,而kubectl apply将尝试将任何 YAML 更改应用到该资源。 -
kubectl use-context命令可用于在kubeconfig文件中切换多个上下文。要在kubeconfig文件之间切换,可以将KUBECONFIG环境变量设置为新文件的路径。 -
强制命令不提供资源更改的历史记录。
第二章:– 设置你的 Kubernetes 集群
-
Minikube 使得设置本地 Kubernetes 集群进行开发变得更加容易。
-
在某些情况下,集群可能存在一个固定的最低成本,这个成本比自我配置的集群要高。一些托管选项除了计算成本外,还有许可证费用。
-
Kubeadm 对基础设施提供商是中立的,而 Kops 仅支持几个主要提供商,并且具有更深的集成和计算资源配置能力。
-
截至本书编写时,AWS、Google Cloud Platform、Digital Ocean、VMware 和 OpenStack,处于不同的生产就绪阶段。
-
通常,集群组件在
systemd服务定义中被定义,这样可以在节点关闭并在操作系统级别重新启动时自动重启服务。
第三章:– 在 Kubernetes 上运行应用容器
-
如果你有开发、预发布和生产环境,你可以为每个环境创建一个命名空间。
-
Pod 所在的节点可能处于 故障 状态,控制平面无法访问该节点。通常,当节点优雅地退出集群时,Pod 会被重新调度,而不会显示 未知 状态。
-
防止占用大量内存的 Pod 占用整个节点,并导致该节点上其他 Pod 的不可预测行为。
-
如果你有 启动 探针,应该增加更多的延迟。如果没有,你需要添加一个,或者增加 就绪 探针的延迟。
第四章:– 扩展和部署你的应用程序
-
ReplicationControllers 在选择器的配置方式上灵活性较差 – 只允许使用键值选择器。
-
部署允许你指定如何推出更新。
-
Jobs 非常适合批处理任务,或者那些可以通过清晰的完成目标水平扩展的任务。
-
StatefulSets 提供了一个顺序的 Pod 标识符,当这些 Pod 重启时,该标识符保持不变。
-
除了现有版本,还可以创建一个带有金丝雀版本的新部署。然后,两个版本可以并行访问。
第五章:– 服务与 Ingress – 与外部世界的通信
-
你可以使用 ClusterIP 服务。
-
你可以使用
kubectl describe命令查看 NodePort 服务在节点上的哪个端口处于活动状态。 -
在云环境中,通常需要为每个负载均衡器付费,而 Ingress 允许你指定多个路由规则,同时只需为一个负载均衡器付费。
-
ExternalName 服务可以用于轻松地将流量路由到云环境中的其他基础设施组件,如托管数据库和对象存储。
第六章:– Kubernetes 应用配置
-
Secrets 以编码形式存储,并可以选择加密存储在
etcd中。ConfigMaps 以纯文本形式存储。 -
它们是 Base64 编码的。
-
在描述 ConfigMap 时,数据将更加可见。键值对模式在将 ConfigMap 挂载为环境变量时也更易于使用。
-
根据你的集群设置,你的 secrets 可能根本没有加密。如果集群的 EncryptionConfiguration 没有设置,secrets 将仅进行 Base64 编码,并且可以很容易地解码。通过创建带有 EncryptionConfiguration 的集群,你的 secrets 将加密存储在
etcd中。这并不是一种安全万能方案,但静态加密无疑是提高 secrets 安全性所必需的。
第七章:– Kubernetes 上的存储
-
Volumes 与 Pod 的生命周期相关联,Pod 被删除时,Volumes 也会被删除。而持久卷将在集群被删除或它们被特别删除之前一直存在。
-
StorageClasses 定义了持久卷(Persistent Volume)的类型。它们可用于区分不同类型的存储,例如区分更快的 SSD 存储和较慢的硬盘,或不同类型的云存储。StorageClasses 决定了持久卷声明(PersistentVolumeClaim)和持久卷(Persistent Volume)将如何获取配置的存储。
-
使用带有集成存储配置的托管 Kubernetes 服务,或将 cloud-controller-manager 配置添加到你的集群中。
-
任何需要存储状态,且存储周期超过单个 Pod 生命周期的应用程序,都无法使用 Volumes。任何需要具有对 Pod 故障容忍性的状态的应用程序需要持久卷。
第八章:– Pod 放置控制
-
节点选择器可以用来与节点标签匹配,多个节点可以满足要求。使用节点名称意味着你指定了 Pod 必须放置的单个节点。
-
Kubernetes 实现了一些默认的污点,以确保 Pods 不会调度到出现故障或资源不足的节点上。此外,Kubernetes 还会对主节点进行污点处理,以防止在主节点上调度用户应用程序。
-
过多的亲和性和反亲和性可能会拖慢调度器的速度,甚至导致其无响应。在有很多亲和性或反亲和性的情况下,确定 Pod 的调度位置是一个计算量非常大的过程。
-
通过使用反亲和性,你可以阻止 Pods 在相同的故障域中与相似的 Pods 共存。位于同一故障域中的节点会被标记上故障域或区域标识符。反亲和性会查找与特定应用层级匹配的 Pods,并阻止其在与该故障域匹配的节点上调度。最终结果是,三层应用程序的每个层级都会分布在多个故障域之间。
第九章:– Kubernetes 上的可观察性
-
指标是对应于数值的内容,展示了多个类别下应用程序/计算性能和/或使用情况,包括磁盘、CPU、内存、延迟等。日志则对应于应用程序、节点或控制平面的文本日志。
-
Grafana UI 高度可定制,可以用来以优雅且灵活的方式展示复杂的 Prometheus(或其他数据源)的查询结果。
-
FluentD 需要在生产集群上运行以收集日志。Elasticsearch 和 Kibana 可以运行在一个单独的集群或其他基础设施上。
第十章:– Kubernetes 故障排除
-
Kubernetes 的一大优势是可以通过添加节点或使用诸如污点和容忍度等控制手段轻松地扩展集群,或者改变 Pod 的调度位置。此外,Pod 的重启可能会导致相同应用程序使用完全不同的 IP,这意味着计算和网络拓扑可以是不断变化的。
-
kubelet通常作为 Linux 服务运行,并使用systemd管理,可以通过systemctl控制,日志则保存在journalctl中。 -
有几种不同的方法可以使用,但通常,你需要检查所有节点是否准备好并可调度;是否有任何 Pod 调度控制阻止了 Pod 的调度;以及是否存在缺失的存储、ConfigMaps 或密钥等依赖项。
第十一章:– Kubernetes 上的模板代码生成与 CI/CD
-
Helm Charts 使用模板和变量,而 Kustomize 则采用基于补丁的策略。Kustomize 被集成在 kubectl 的最新版本中,而 Helm 使用的是一个单独的 CLI 工具。
-
配置应侧重于安全性,因为部署凭证可能会被用来将攻击者的工作负载部署到集群中。使用安全的环境变量或云服务提供商的访问管理控制是两种有效的策略。凭证绝对不应放在任何 Git 仓库中。
-
集群内设置通常更为优选,因为不需要外部系统提供 Kubernetes 凭证。集群外设置通常较为简单,并且比集群内设置更为同步,后者由控制循环决定何时对资源配置进行更改。
第十二章:– Kubernetes 安全性和合规性
-
MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。
-
一个没有 Pod 选择器的 NetworkPolicy 会选中所有 Pod。如果选择所有 Pod,并且未添加任何规则的情况下添加了 Ingress 和 Egress 类型,则该 NetworkPolicy 会自动拒绝该命名空间中所有 Pod 的所有入站和出站流量。
-
我们希望追踪所有 API 请求,这些请求涉及资源的修补或更新,因为攻击者可能会使用恶意容器更新 Deployment、Pod 或其他资源。
第十三章:– 使用 CRD 扩展 Kubernetes
-
存储版本是实际存储在数据存储中的版本。服务版本是 API 接受的任何版本,用于读写操作。服务版本在存储到
etcd时会被转换为存储版本。 -
测量、分析和更新(通常)。
-
根据云服务提供商的不同,cluster-autoscaler 插件会直接更新自动伸缩组,以便添加或移除节点。
第十四章:– 服务网格和无服务器架构
-
静态 Envoy 配置是指由用户手动创建或编写的 Envoy 配置。动态 Envoy 配置(如 Istio 提供的配置)会不断适应来自外部控制器或数据平面的新容器,以及新的路由和过滤规则。
-
监听器、路由、集群和端点。
-
Knative 运行需要多个组件。这提供了丰富的定制化选项,但也使得其配置和操作比 OpenFaaS 更加复杂。
第十五章:– 在 Kubernetes 上运行有状态工作负载
-
Minio 是一个兼容 AWS S3 的存储工具。
-
StatefulSets 通过为分布式数据库等自聚类应用提供稳定的、顺序的 Pod 标识符,以及持久化存储稳定性,来协助这些应用。
-
在 Kubernetes 中,Pod 的生命周期可能较短,而有状态应用可以是分布式的。这意味着,在 Pod 的身份发生变化,且存储需要从零开始复制的情况下,维护 Pod 之间状态的过程(例如数据库一致性)可能变得十分复杂。











浙公网安备 33010602011771号