Kubernetes-圣经第二版-全-

Kubernetes 圣经第二版(全)

原文:annas-archive.org/md5/251eabe0fadf586c38e10251d0f9ba96

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

容器的广泛采用使得虚拟化领域实现了真正的飞跃,因为它们提供了更大的灵活性,特别是在如今,像敏捷DevOps这样的流行词汇几乎是每个人嘴边的常谈。

如今,几乎没有人质疑容器的使用——它们几乎无处不在,尤其是在 Docker 成功和 Kubernetes 作为领先容器编排平台崛起之后。

容器为组织带来了巨大的灵活性,但当组织面临将容器部署到生产环境中的挑战时,容器的使用一直备受质疑。多年来,许多公司将容器用于概念验证项目、本地开发和类似的目的,但将容器用于实际生产工作负载的想法,对于许多组织来说是不可想象的。

容器编排器是改变游戏规则的工具,Kubernetes 处于领先地位。最初由 Google 构建,今天,Kubernetes 是领先的容器编排器,提供了部署生产环境中容器所需的所有功能。Kubernetes 非常流行,但也很复杂。这个工具非常多功能,以至于入门并逐步掌握高级用法并非易事:它并不是一个容易学习和操作的工具。

作为一个编排器,Kubernetes 有其独立于容器引擎的概念。但当容器引擎与编排器结合使用时,你将得到一个非常强大的平台,能够将你的云原生应用程序部署到生产环境中。作为每天与 Kubernetes 打交道的工程师,我们和许多人一样确信,它是一项必须掌握的技术,因此我们决定分享我们的知识,涵盖大部分这个编排器的内容,让 Kubernetes 变得更易于理解。

本书完全致力于 Kubernetes,并且是我们工作成果的结晶。它为 Kubernetes 提供了一个广阔的视角,涵盖了编排器的许多方面,从纯粹的容器 Pod 创建到在公有云上部署编排器。我们不希望本书仅仅是一本入门指南

我们希望本书能教会你所有关于 Kubernetes 的知识!

本书适合的人群

本书适合那些打算将 Kubernetes 与容器运行时结合使用的人。尽管 Kubernetes 通过容器运行时接口CRI)支持各种容器引擎,并且并不依赖于任何特定的引擎,但 Kubernetes 与containerd的组合依然是最常见的使用案例之一。

本书内容高度技术化,主要关注 Kubernetes 和容器运行时,从工程师的角度进行探讨。它面向工程师,无论他们是来自开发还是系统管理员背景,并不针对项目经理。本书是那些每天使用 Kubernetes 的人的“圣经”,或者是那些希望了解这一工具的人。你不应该害怕在终端上输入一些命令。

无论你是 Kubernetes 的完全初学者,还是拥有中级水平,都不会成为学习本书的障碍。虽然我们在章节中涵盖了一些容器基础知识,但对容器有基本的技术了解会更有帮助。如果你正在将现有应用迁移到 Kubernetes,本书也可以作为一份指南。

本书包含了可以帮助读者在公共云平台上部署 Kubernetes 的内容,比如 Amazon EKS 或 Google GKE。希望在云上将 Kubernetes 添加到技术栈中的云用户将会很受益。

本书涵盖的内容

第一章Kubernetes 基础知识,是对 Kubernetes 的介绍。我们将解释什么是 Kubernetes,为什么它被创造出来,谁创造了它,谁在维护这个项目,以及你在何时、为何应将它作为技术栈的一部分使用。

第二章Kubernetes 架构——从容器镜像到运行的 Pod,讲述了 Kubernetes 是如何构建成一个分布式软件的,技术上它不是一个单一的巨型二进制文件,而是由一组微服务相互交互构建的。在这一章中,我们将解释这种架构,以及 Kubernetes 如何将你的指令转换为运行的容器。

第三章安装你的第一个 Kubernetes 集群,解释了由于 Kubernetes 的分布式特性,它的安装非常困难,因此为了让学习过程更加容易,可以通过使用其中一个发行版来安装 Kubernetes 集群。在这一章中,我们将探索 Kind 和 minikube 这两个选项,以便让 Kubernetes 集群在你的机器上运行。

第四章在 Kubernetes 中运行你的容器,是对 Pod 概念的介绍。

第五章使用多容器 Pod 和设计模式,介绍了多容器 Pod 和设计模式,比如代理、适配器或 sidecar,你可以在运行多个容器作为同一 Pod 的一部分时构建这些模式。

第六章Kubernetes 中的命名空间、配额和多租户限制,解释了使用命名空间是集群管理的关键方面,在你与 Kubernetes 一起工作的过程中,命名空间是不可避免的。尽管它是一个简单的概念,但它是一个关键概念,你必须完全掌握命名空间,才能在 Kubernetes 中取得成功。我们还将学习如何使用命名空间、配额和限制在 Kubernetes 中实现多租户。

第七章使用 ConfigMaps 和 Secrets 配置你的 Pod,解释了在 Kubernetes 中如何将 Kubernetes 应用与其配置分开。由于 ConfigMap 和 Secret 资源,应用和配置都有各自的生命周期。本章将专门讲解这两个对象,以及如何将 ConfigMap 或 Secret 中的数据作为环境变量或挂载卷在你的 Pod 上进行挂载。

第八章通过服务暴露你的 Pods,教你 Kubernetes 中的服务概念。每个 Pod 在 Kubernetes 中会动态分配一个 IP 地址。如果你希望为 Pods 提供一个一致的地址,暴露集群中的 Pods 给其他 Pods 或外部世界,可以使用服务,并为其分配一个静态的 DNS 名称。你将在这里学到三种主要的服务类型,分别是 ClusterIp、NodePort 和 LoadBalancer,它们都专注于在 Pod 暴露方面的单一用例。

第九章Kubernetes 中的持久存储,讲解了默认情况下,Pods 并不是持久化的。由于它们只是管理原始容器,销毁它们将导致数据丢失。解决方案是使用持久存储,得益于 PersistentVolume 和 PersistentVolumeClaim 资源。本章将专门讲解这两个对象以及 StorageClass 对象:你将了解到 Kubernetes 在存储方面的极高灵活性,并且你的 Pods 可以与多种不同的存储技术进行对接。

第十章运行生产级 Kubernetes 工作负载,深入探讨了 Kubernetes 中的高可用性和故障容错机制,使用 ReplicationController 和 ReplicaSet 实现。

第十一章使用 Kubernetes 部署无状态工作负载,是上一章的延续,解释了如何使用 Deployment 对象管理多个版本的 ReplicaSets。这是 Kubernetes 上运行无状态应用的基本构建块。

第十二章StatefulSet – 部署有状态应用,介绍了下一个重要的 Kubernetes 对象:StatefulSet。这个对象是运行有状态应用的支柱。本章将解释在 Kubernetes 中运行无状态应用和有状态应用之间的最重要区别。

第十三章DaemonSet – 在节点上维持 Pod 单例,讲解了 DaemonSet,这是一个特殊的 Kubernetes 对象,用于在 Kubernetes 集群上运行操作性或支持性工作负载。当你需要在单个 Kubernetes 节点上运行精确一个容器 Pod 时,DaemonSet 就是你需要的对象。

第十四章使用 Helm Charts 和 Operators,介绍了 Helm Charts,这是一种专门用于 Kubernetes 应用打包和重新分发的工具。在本章获得的知识将帮助你快速设置 Kubernetes 开发环境,甚至规划将你的 Kubernetes 应用以 Helm Chart 形式重新分发。在这一章中,我们还将介绍 Kubernetes 操作符,并讲解它们如何帮助你部署应用栈。

第十五章在 Google Kubernetes Engine 上使用 Kubernetes 集群,讲述了如何使用本地命令行客户端和 Google Cloud 控制台将我们的 Kubernetes 工作负载迁移到 Google Cloud。

第十六章在 Amazon Web Services 上使用 Amazon Elastic Kubernetes Service 启动 Kubernetes 集群,讨论了如何将我们在上一章中启动的工作负载迁移到 Amazon 的 Kubernetes 服务。

第十七章在 Microsoft Azure 上使用 Azure Kubernetes Service 启动 Kubernetes 集群,讨论了如何在 Microsoft Azure 上启动一个集群。

第十八章Kubernetes 安全性,讲解了如何使用内置的基于角色的访问控制和授权方案,以及用户管理。本章还教你如何使用准入控制器、基于 TLS 证书的通信和安全上下文的实现。

第十九章Pod 调度的高级技术,深入探讨了节点亲和性、节点污点和容忍度,以及一般的高级调度策略。

第二十章Kubernetes Pod 和节点的自动扩展,介绍了 Kubernetes 中自动扩展的原理,并解释了如何使用 Vertical Pod Autoscaler(垂直 Pod 自动扩展器)、Horizontal Pod Autoscaler(水平 Pod 自动扩展器)和 Cluster Autoscaler(集群自动扩展器)。

第二十一章高级 Kubernetes:流量管理多集群策略及更多,介绍了 Kubernetes 中的 Ingress 对象和 IngressController。我们解释了如何使用 nginx 作为 IngressController 的实现,以及如何在 Azure 环境中使用 Azure 应用程序网关作为本地的 IngressController。我们还将介绍 Kubernetes 的高级主题,包括集群二期任务、最佳实践以及故障排除。

为了最大程度地从本书中获益

虽然我们在各章中涉及了容器基础知识,但本书的重点是 Kubernetes。虽然 Kubernetes 支持多种容器引擎,本书内容主要讨论如何在 Kubernetes 中使用 containerd 作为运行时。你不需要成为专家,但在深入本书之前,了解如何启动和管理容器应用将会有所帮助。

虽然 Kubernetes 支持运行 Windows 容器,但本书讨论的大多数主题将基于 Linux。了解 Linux 知识会很有帮助,但并非必须。再次强调,你不需要成为专家:了解如何使用终端会话和基本的 Bash 脚本就足够了。

最后,拥有一些关于软件架构的基本知识,比如 REST API,将会非常有益。

本书涵盖的软件/硬件 操作系统要求
Kubernetes >= 1.31 Windows、macOS、Linux
kubectl >= 1.31 Windows、macOS、Linux

我们强烈建议你现在不要尝试在你的机器上安装 Kubernetes 或 kubectl。Kubernetes 不是一个单一的二进制文件,而是一个由多个组件组成的分布式软件,因此,从零开始安装一个完整的 Kubernetes 集群非常复杂。相反,我们建议你按照本书的第三章进行操作,该章节专门讲解了 Kubernetes 的安装设置。

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

请注意,kubectlhelm 等是我们在本书中最常使用的工具,但 Kubernetes 周围有一个庞大的生态系统,我们可能会安装本节未提到的额外软件。本书还涉及在云中使用 Kubernetes,我们将学习如何在像亚马逊 Web 服务(AWS)和谷歌云平台(GCP)等公共云平台上部署 Kubernetes 集群。在此设置过程中,我们可能会安装一些专门针对这些平台的软件,这些软件不仅与 Kubernetes 相关,还与这些平台提供的其他服务相关。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,地址为 github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。

我们还提供了其他来自我们丰富的书籍和视频目录的代码包,地址为 github.com/PacktPublishing/。快去看看吧!

下载彩色图片

我们还提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在此处下载:static.packt-cdn.com/downloads/9781835464717_ColorImages.pdf

使用的约定

本书中使用了若干文本约定。

文中的代码:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“现在,我们需要为本地 kubectl CLI 创建一个 kubeconfig 文件。”

一段代码按如下方式编写:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-Pod 

当我们希望将注意力集中到代码块的特定部分时,相关的行或项会以粗体显示:

apiVersion: v1
kind: **ReplicationController**
metadata:
  name: nginx-replicationcontroller-example 

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

$ kubectl get nodes 

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词汇将在文本中以这种方式显示。以下是一个例子:“在此屏幕上,您应该看到一个启用计费按钮。”

重要说明

显示如下

提示

显示如下。

联系我们

我们始终欢迎读者的反馈。

常规反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过电子邮件与我们联系 customercare@packtpub.com。

勘误:尽管我们已尽力确保内容的准确性,但错误难免。如果您在本书中发现任何错误,我们将非常感谢您向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关信息。

盗版:如果您在互联网上发现任何非法复制的我们的作品,感谢您向我们提供该位置或网站名称。请通过电子邮件与我们联系 copyright@packt.com,并附上该内容的链接。

如果您有意成为作者:如果您对某个领域有专业知识,并且有兴趣编写或为书籍做贡献,请访问 authors.packtpub.com

留下您的评论!

感谢您从 Packt 出版社购买本书——我们希望您喜欢它!您的反馈对我们非常重要,有助于我们改进和发展。读完后,请花点时间在亚马逊上留下评论;这将只需一分钟,但对于像您这样的读者来说,意义重大。

扫描以下二维码,获得您选择的免费电子书。

一个带黑色方块的二维码描述自动生成

packt.link/NzOWQ

下载本书的免费 PDF 副本

感谢您购买本书!

你喜欢在旅途中阅读,但又无法随身携带纸质书籍吗?

您的电子书购买是否与您选择的设备不兼容?

不用担心,现在购买任何 Packt 图书,您都会免费获得该书的无 DRM 版本 PDF。

在任何地方、任何设备上随时阅读。直接将您最喜欢的技术书籍中的代码搜索、复制并粘贴到您的应用程序中。

优惠不止于此,您还可以独享折扣、新闻通讯以及每日送到邮箱的免费内容。

按照以下简单步骤,您即可享受所有优惠:

  1. 扫描二维码或访问以下链接:

packt.link/free-ebook/9781835464717

  1. 提交您的购买凭证。

  2. 就是这样!我们将把免费的 PDF 和其他福利直接发送到您的邮箱。

第一章:Kubernetes 基础

欢迎来到 Kubernetes 圣经,我们很高兴陪伴你一起走上 Kubernetes 的学习之旅。如果你从事软件开发行业,你可能听说过 Kubernetes。这是很正常的,因为 Kubernetes 的受欢迎程度在近几年大幅上升。

由 Google 构建的 Kubernetes 是目前最流行且被广泛采用的容器编排解决方案:如果你在寻找一个解决方案来管理生产环境中大规模的容器化应用,无论是在本地还是公共云中,它都是你所需要的工具。请专注于这个词。大规模部署和管理容器是极其困难的,因为默认情况下,像 Docker 这样的容器引擎本身并不提供任何方法来保持容器的大规模可用性和可扩展性。

Kubernetes 最初作为 Google 的一个项目出现,Google 在构建一个能够在其大规模分布式基础设施上部署大量容器的解决方案上投入了大量的精力。通过将 Kubernetes 作为你技术栈的一部分,你将获得一个由互联网最大公司之一构建的开源平台,该公司在稳定性方面有着最为严苛的需求。

尽管 Kubernetes 可以与许多不同的容器运行时一起使用,本书将专注于 Kubernetes 与容器(Docker 和 Podman)的组合。

也许你已经在日常工作中使用 Docker,但容器编排的世界对你来说可能完全陌生。甚至可能你看不到使用这种技术的好处,因为对于你来说,仅仅使用原生 Docker 就已经足够了。这就是为什么在本章中,我们不会详细讲解 Kubernetes。相反,我们将重点解释 Kubernetes 是什么,以及它如何帮助你管理生产环境中的应用容器。如果你了解它为什么被构建出来,那么学习这项新技术会更容易。

在本章中,我们将涵盖以下主要内容:

  • 理解单体应用与微服务

  • 理解容器

  • Kubernetes 如何帮助你管理容器?

  • 理解 Kubernetes 的历史

  • 探索 Kubernetes 解决的问题

你可以从官方 GitHub 仓库下载本章的最新代码示例,地址是 github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter01

理解单体应用与微服务

现在让我们暂时把 Kubernetes 和 Docker 放到一边,换个角度来谈一谈过去 20 年里互联网和软件开发是如何共同发展的。这将帮助你更好地理解 Kubernetes 的定位及其所解决的问题。

理解自 1990 年代末以来互联网的增长

自 1990 年代末以来,互联网的普及迅速增长。在 1990 年代,甚至是 2000 年代初期,全球只有几十万人在使用互联网。如今,几乎有 20 亿人使用互联网进行电子邮件、网页浏览、视频游戏等活动。

现在互联网用户数量庞大,我们用它来满足各种不同的需求,而这些需求由部署在不同设备上的数十个应用程序来解决。

此外,联网设备的数量也在增加,因为现在每个人可以有多种不同类型的设备连接到互联网:笔记本电脑、台式电脑、智能手机、电视、平板电脑等。

如今,我们可以使用互联网购物、工作、娱乐、阅读或做任何事情。互联网几乎渗透到我们社会的每个角落,并且在过去 20 年里引发了深刻的范式转变。所有这些都使得软件开发变得至关重要。

理解更频繁发布软件版本的需求

为了应对这不断增长的用户需求,这些用户总是希望软件能提供更多的功能,软件开发行业不得不进化,以便更快、更频繁地发布新版本。

事实上,回到 1990 年代,你可以构建一个应用程序,将其部署到生产环境中,并且每年更新一次或两次。今天,企业必须能够在生产环境中更新软件,有时甚至是一天好几次,无论是部署新功能、与社交媒体平台集成、支持最新流行智能手机的分辨率,还是发布一个前一天发现的安全漏洞补丁。今天的一切都变得更加复杂,你必须比以前更快。

我们不断需要更新软件,最终,许多公司的生存直接取决于它们能多频繁地向用户发布新版本。但我们如何加速软件开发生命周期,以便能更频繁地向用户交付新版本呢?

企业的 IT 部门必须进行转型,无论是从组织层面还是技术层面。在组织上,他们改变了管理项目和团队的方式,以便转向敏捷方法;在技术上,云计算平台、容器技术和虚拟化等技术被广泛采用,并在很大程度上帮助将技术敏捷性与组织敏捷性对齐。所有这些都是为了确保更频繁的软件发布!接下来,我们将重点讨论这一演变。

理解组织向敏捷方法论转型

从纯粹的组织角度来看,敏捷方法论如 Scrum、Kanban 和 DevOps 成为组织 IT 团队的标准方式。

典型的 IT 部门如果不采用敏捷方法,通常由三个不同的团队组成,每个团队在开发和发布生命周期中负责单一的任务。

请放心,尽管我们目前讨论的是敏捷方法论和互联网的历史,但这本书的主题实际上是 Kubernetes!我们只是需要解释一些在正式引入 Kubernetes 之前遇到的问题!

在采用敏捷方法论之前,开发和运维通常在不同的孤岛中工作。这可能导致低效和沟通障碍。敏捷方法论帮助弥补了这些沟通空白,促进了合作。以下是三个孤立的团队。

  • 业务团队:他们就像是客户的声音。他们的工作是解释应用程序中需要哪些功能,以满足用户需求。他们将业务目标转化为开发人员的明确指令。

  • 开发团队:这些是将应用程序付诸实践的工程师。他们将业务团队的功能需求转化为代码,构建用户将交互的功能和特性。来自业务团队的清晰沟通至关重要。如果指令定义不清,就像打电话游戏一样——误解会导致延迟和返工。

  • 运维团队:他们是服务器的守护者。他们的主要工作是保持应用程序的平稳运行。新功能可能会带来干扰,因为它们需要更新,这可能具有风险。过去,他们并不总是知道即将推出的新功能,因为他们未参与计划。

这些就是我们所说的孤岛,如图 1.1所示:

图 1.1:典型 IT 部门中孤立的团队

各角色分工明确,来自不同团队的人很少一起合作,当出现问题时,每个人都浪费时间寻找正确的信息来源。

这种孤立的组织方式已经导致了许多重大问题:

  • 显著更长的开发时间

  • 部署的发布版本可能完全无法在生产环境中运行的更大风险

这正是敏捷方法论和 DevOps 所解决的问题。敏捷方法论带来的改变是通过创建跨职能团队,让人们一起工作。

DevOps 是一种协作文化和一套实践,旨在弥合开发(Dev)和运维(Ops)团队之间的鸿沟。DevOps 推动了整个软件生命周期中的协作和自动化,从开发、测试到部署和维护。

一个敏捷团队由产品负责人组成,他们通过将功能写成用户故事的方式,向开发人员描述具体特性,开发人员在同一个团队中工作。开发人员应当能看到生产环境,并能够在其上进行部署,最好是采用持续集成和持续部署CI/CD)方法。测试人员也应该是敏捷团队的一部分,以便编写测试。

通过协作方式,团队将更好地理解整体情况,如下图所示。

图 1.2: 团队协作打破了隔阂

只需要理解的是,通过采用敏捷方法和 DevOps,这些隔阂被打破了,能够将需求正式化、实施、测试、发布,并在生产环境中维护的跨职能团队得以建立。表 1.1 展示了从传统开发到敏捷和 DevOps 方法的转变。

特性 传统开发 敏捷 & DevOps
团队结构 隔离的部门(开发、运维) 跨职能、多学科的团队
工作方式 隔离的工作流程,有限的沟通 协作的,迭代的开发周期
拥有权 开发交给运维进行部署和维护 “你构建它,你运行它” - 团队负责整个生命周期
关注点 特性和功能 商业价值,持续改进
发布周期 长周期发布,部署不频繁 短冲刺,频繁发布并带有反馈循环
测试 开发后单独的测试阶段 在整个开发周期中进行集成测试
基础设施 静态、手动管理的基础设施 自动化的基础设施配置和管理(DevOps)

表 1.1: DevOps 与传统开发 – 协作方式的转变

所以,我们已经讨论了采用敏捷方法所带来的组织转型。现在,让我们来讨论过去几年我们经历的技术演变。

理解从本地托管到云托管的转变

拥有敏捷团队非常好,但敏捷性也必须应用到软件的构建和托管方式上。

为了始终实现更快速和更频繁的发布,敏捷软件开发团队必须重新审视软件开发和发布的两个重要方面:

  • 托管

  • 软件架构

现在,应用程序不仅仅是为几百个用户提供服务,而是可能为数百万个用户同时使用。更多的互联网用户意味着需要更多的计算能力来处理这些用户。的确,托管应用程序已成为一个非常大的挑战。

在早期的网页托管时代,企业主要依赖两种主要方法来托管其应用程序:其中一种方法是本地托管。这种方式涉及到物理拥有和管理运行其应用程序的服务器。实现本地托管有两种主要方式:

  1. 专用服务器:从已有的数据中心供应商租用物理服务器:这涉及到从托管公司租用专用服务器硬件。托管服务提供商管理物理基础设施(电力、冷却、安全),但服务器配置、软件安装和持续维护的责任则由业务方承担。这种方式相比共享托管提供了更大的控制和定制化,但仍然需要大量的内部技术专长。

  2. 建设您自己的数据中心:建设和维护私有数据中心:此选项涉及公司大量投资以建设和维护其自己的物理数据中心设施。这包括购买服务器硬件、网络设备和存储解决方案,并实施强大的电源、冷却和安全措施。尽管提供了最高级别的控制和安全性,但这种方法非常昂贵且资源密集,通常只由具有重要 IT 资源的大公司才能承担。

此外,本地托管还涵盖了操作系统的管理、安全补丁、备份和服务器灾难恢复计划。公司通常需要专门的 IT 人员来管理和维护其本地基础设施,从而增加了总体成本。

当您的用户基础增长时,您需要获取更强大的机器来处理负载。解决方案是购买一台更强大的服务器,并从一开始就在其上安装您的应用程序,或者如果您管理您的数据中心,则是订购和安装新的硬件。这不是很灵活。今天,许多公司仍在使用本地解决方案,通常情况下,这种方案不是很灵活。

改变游戏规则的是采用另一种方法,即公共云,这与本地不同。云计算的理念是,像亚马逊、谷歌和微软这样的大公司,拥有大量数据中心,决定在其庞大的基础设施上构建虚拟化,以确保通过 API 访问虚拟机的创建和管理。换句话说,您只需点击几下或输入几个命令就可以获得虚拟机。

下表提供了云计算对组织有益的高级信息。

特性 本地
可扩展性 有限 – 扩展时需要购买新硬件 高度可扩展 – 可按需增加或减少资源
灵活性 不灵活 – 变更需要物理硬件调整 高度灵活 – 资源可以快速配置和去配置
成本 硬件、软件许可和 IT 人员的高前期成本 低前期成本 – 按使用资源付费模式
维护 需要专门的 IT 人员进行维护和更新 最小维护要求 – 云服务提供商管理基础设施
安全性 对安全性有很高的控制,但需要显著的专业知识 云服务提供商实施了强大的安全措施
宕机时间 从硬件故障中恢复可能耗时 云提供商提供高可用性和灾难恢复功能
位置 仅限于数据中心的物理位置 可通过任何具有互联网连接的地方访问

表 1.2:云计算对组织的重要性

我们将在下一节中学习云计算技术如何帮助组织扩展其 IT 基础设施。

理解为什么云计算非常适合可扩展性

今天,几乎任何人都可以通过几次点击,获得数百或数千台服务器,这些服务器以虚拟机或实例的形式存在,由亚马逊 AWS谷歌云平台微软 Azure等云服务提供商在物理基础设施上维护。许多公司决定将其工作负载从本地迁移到云服务提供商,并且在过去几年中,这种采用率巨大。

借助这一点,现在计算能力是你最容易获取的资源之一。

云计算提供商如今是敏捷团队工具库中的典型托管解决方案。其主要原因是云计算非常适合现代开发。

虚拟机配置、CPU、操作系统、网络规则等都是公开显示并完全可配置的,因此你的团队在了解生产环境构成时没有任何秘密。由于云服务提供商的可编程特性,复制一个生产环境到开发或测试环境变得非常容易,这为团队提供了更多的灵活性,帮助他们在开发软件时应对挑战。这对于基于 DevOps 理念的敏捷开发团队来说,是一个非常有用的优势,他们需要管理生产环境中的应用开发、发布和维护。

云服务提供商提供了许多好处,具体如下:

  • 弹性和可扩展性

  • 有助于打破信息孤岛并推动敏捷方法论的实施

  • 与敏捷方法论和 DevOps 高度契合

  • 低成本和灵活的计费模型

  • 确保无需管理物理服务器

  • 允许虚拟机随时销毁并重新创建

  • 相较于每月租用裸金属机器,更加灵活

由于这些好处,云计算成为了敏捷开发团队工具库中的宝贵资产。本质上,你可以不断构建和复制生产环境,而无需自己管理物理机器。云计算使你能够根据使用者数量或他们消耗的计算资源来扩展你的应用。你将使你的应用高可用并具备容错能力。结果是为终端用户提供更好的体验。

重要提示

请注意,Kubernetes 可以在云端和本地运行。Kubernetes 非常灵活,你甚至可以在 Raspberry Pi 上运行它。Kubernetes 与公共云非常匹配,但你并不需要或被强制在云端运行它。

既然我们已经解释了云计算带来的变化,让我们接着谈谈软件架构,因为多年来,这里也发生了一些变化。

探索单体架构

过去,应用程序大多由单体架构构成。一个典型的单体应用程序由一个简单的进程、一个二进制文件或一个包组成,如图 1.3所示。

这个独立组件负责整个业务逻辑的实现,软件必须对其作出响应。如果你想开发一些不一定会频繁更新的简单应用程序,单体架构是一个不错的选择。为什么?因为单体架构有一个主要的缺点。如果你的单体架构因某些原因变得不稳定或崩溃,整个应用程序将无法使用:

图 1.3:单体应用程序由一个大组件组成,包含了所有的软件

单体架构可以在开发过程中为你节省大量时间,也许这是你选择这种架构时唯一能找到的好处。然而,它也有许多缺点。以下是其中的一些:

  • 一个部署到生产环境的失败可能会破坏整个应用程序。

  • 扩展活动变得难以实现;如果你无法扩展,所有的应用程序可能都会变得无法使用。

  • 单体应用程序的任何类型的失败都可能导致整个应用程序的宕机。

在 2010 年代,这些缺点开始带来实际问题。随着部署频率的增加,必须考虑一种新架构,能够支持频繁的部署和更短的更新周期,同时降低应用程序的风险或一般不可用性。这就是为什么微服务架构被设计出来的原因。

探索微服务架构

微服务架构是将软件应用程序作为一组独立的微应用程序进行开发。每个应用程序被称为微服务,它有自己的版本控制、生命周期、环境和依赖关系。此外,它还可以有自己的部署生命周期。每个微服务只能负责有限数量的业务规则,所有的微服务一起构成整个应用程序。可以将微服务视为独立的完整软件,具有自己的生命周期和版本控制过程。

由于微服务仅应承载整个应用程序的一部分功能,它们必须是可访问的,以便暴露其功能。你必须从一个微服务获取数据,但也可能希望向其推送数据。你可以通过广泛支持的协议(如 HTTP 或 AMQP)使你的微服务可访问,并且它们需要能够相互通信。

这就是为什么微服务通常作为通过明确定义的 API 暴露其功能的 Web 服务构建的原因。虽然 HTTP(或 HTTPS)REST API 由于其简单性和广泛采用而成为流行的选择,但其他协议,如 GraphQL、AMQP 和 gRPC,正在获得关注并被广泛使用。

关键要求是微服务提供一个良好文档化且可发现的 API 端点,无论选择何种协议。这允许其他微服务无缝地进行交互并交换数据。

这是与单体架构大不相同的地方:

图 1.4:一个微服务架构,其中不同的微服务通过 HTTP 协议进行通信

微服务架构的另一个关键方面是微服务需要解耦:如果一个微服务变得不可用或不稳定,它必须不会影响其他微服务或整个应用程序的稳定性。你必须能够独立地部署、扩展、启动、更新或停止每个微服务,而不影响其他任何部分。如果你的微服务需要与数据库引擎一起工作,记住即使是数据库也必须解耦。每个微服务应该有自己的数据库,等等。所以,如果微服务 A的数据库崩溃,它不会影响到微服务 B

图 1.5:一个微服务架构,其中不同的微服务相互通信并与专用的数据库服务器进行交互;通过这种方式,微服务是隔离的,没有共同的依赖关系

关键规则是尽可能地解耦,使你的微服务完全独立。因为微服务旨在独立运作,它们还可以拥有完全不同的技术环境,并且可以用不同的语言实现。你可以有一个用 Go 实现的微服务,一个用 Java 实现的微服务,另一个用 PHP 实现的微服务,它们共同构成一个应用程序。在微服务架构的背景下,这不是问题。因为 HTTP 是标准协议,它们即使底层技术不同,也能够互相通信。

微服务必须与其他微服务解耦,但它们也必须与运行它们的操作系统解耦。微服务不应该在主机系统级别操作,而应该在更高层次上操作。你应该能够根据需要将它们部署到不同的机器上,而无需依赖于主机系统的强依赖性;这就是为什么微服务架构和容器是一个很好的组合。

如果你需要在生产环境中发布新特性,你只需要部署受到新特性版本影响的微服务,其他的可以保持不变。

正如你可以想象的那样,微服务架构在现代应用开发中具有巨大的优势:

  • 它更容易执行周期性的生产交付,同时对整个应用的稳定性影响最小。

  • 你每次只能升级特定的微服务,而不是整个应用程序。

  • 扩展活动更加顺畅,因为你可能只需要扩展特定的服务。

然而,从另一方面来看,微服务架构也有一些缺点:

  • 该架构需要更多的规划,并且开发难度较大。

  • 管理每个微服务的依赖关系存在问题。

微服务应用被认为难以开发。这种方法可能很难理解,尤其是对于初级开发人员而言。依赖关系管理也可能变得复杂,因为所有微服务可能具有不同的依赖关系。

在单体架构和微服务架构之间做出选择

构建一个成功的软件应用需要谨慎的规划,而你面临的关键决策之一就是选择使用哪种架构。两种主要的架构方法主导了这一领域:单体架构和微服务架构:

  • 单体架构:想象一个紧凑的、集成的系统。这就是单体架构的精髓。一切都存在于同一个代码库中,使得对于小型项目或资源有限的团队来说,开发和初始部署都变得简单。此外,单体架构的更新往往很快,因为只需要管理一个系统。

  • 微服务:可以把它看作是一个复杂的应用被分解成独立的、模块化的组件。每个服务都可以单独构建、扩展和部署。这种方法在大型功能丰富的项目和拥有多样技能团队中表现尤为出色。微服务提供了灵活性和可能较快的开发周期。然而,它们也带来了在故障排除和安全管理上的额外复杂性。

最终,单体架构和微服务架构的选择取决于你的具体需求。考虑项目的规模、团队结构以及所需的灵活性水平。不要被趋势左右——选择能帮助你的团队高效开发和管理应用的架构。

Kubernetes 提供了灵活性,适用于快速变化的单体架构和微服务架构,允许你选择最适合你项目需求的架构。

在下一节中,我们将了解容器以及它们如何帮助微服务软件架构。

了解容器

通过单体架构和微服务架构的比较,你应该已经明白,最能结合敏捷性和 DevOps 的架构是微服务架构。正是这种架构,我们将在本书中讨论,因为它是 Kubernetes 能够很好管理的架构。

现在,我们将讨论如何使用 Docker,作为 Linux 的容器引擎,来管理微服务。如果你已经对 Docker 有很多了解,可以跳过这一节。否则,我建议你仔细阅读这一节。

了解为什么容器适用于微服务

回顾微服务架构的两个重要方面:

  1. 每个微服务可以有自己的技术环境和依赖关系。

  2. 同时,它必须与运行它的操作系统解耦。

让我们暂时搁置后面提到的问题,先讨论第一个问题:同一个应用的两个微服务可能使用两种不同的编程语言开发,或者使用相同的编程语言,但作为两个不同的版本。现在,假设你想在同一台 Linux 机器上部署这两个微服务,那将是一个噩梦。

原因在于你需要安装所有不同运行时的版本,以及相应的依赖项,而且两个微服务之间可能也存在版本冲突或重叠。此外,所有这些都将运行在同一宿主操作系统上。现在,假设你想从机器上移除其中一个微服务,将其部署到另一台服务器上,并清理掉该微服务使用的所有依赖项。当然,如果你是一个才华横溢的 Linux 工程师,你会成功做到这一点。但是对于大多数人来说,依赖项之间冲突的风险非常大,最终你可能只是让你的应用在这种噩梦般的基础设施下变得不可用。

这有一个解决方案:你可以为每个微服务构建一个机器镜像,然后将每个微服务部署到一个专用的虚拟机上。换句话说,你避免在同一台机器上部署多个微服务。然而,在这个例子中,你将需要与微服务数量相等的机器。当然,在 AWS 或 GCP 的帮助下,启动大量服务器,每台服务器只负责运行一个微服务将变得非常容易,但如果不共享宿主机提供的计算能力,这将是一个巨大的浪费。

在容器世界中有类似的解决方案,但默认的容器运行时并不提供微服务之间的完全隔离。这正是Kata 运行时保密容器项目发挥作用的地方。这些技术为容器化应用程序提供了增强的安全性和隔离性。我们将在本书的后续章节中深入探讨这些容器隔离的概念。

我们将在下一节中学习容器如何帮助实现隔离。

理解容器隔离的好处

像 Docker 和 Podman 这样的容器引擎在管理微服务中起着至关重要的作用。与需要完整操作系统的虚拟机VMs)不同,容器是轻量级单元,它们共享宿主机的 Linux 内核。这使得容器比虚拟机更快地启动和停止。

容器引擎提供了一个用户友好的 API,用于构建、部署和管理容器。容器引擎并不会引入额外的虚拟化层,而是利用 Linux 内核的内建能力来实现进程隔离、安全性和资源分配。这种高效的方式使得容器化成为部署微服务的一个有吸引力的解决方案。

下图展示了容器与虚拟机的区别:

图 1.6:虚拟机与容器之间的区别

您的微服务将会在这一层之上启动,而不是直接在宿主系统上运行,宿主系统的唯一角色是运行您的容器。

由于容器是隔离的,您可以根据需要运行任意数量的容器,并且让它们运行用不同语言编写的应用程序,而不会发生任何冲突。微服务的迁移变得像停止运行一个容器并从相同镜像在另一台机器上启动一个新容器一样简单。

使用容器与微服务结合提供了三个主要的好处:

  • 它减少了对宿主系统的占用。

  • 它使得宿主系统可以互相共享,而不会有不同微服务之间的冲突。

  • 它消除了微服务与宿主系统之间的耦合。

一旦微服务被容器化,您可以消除它与宿主操作系统的耦合。微服务将仅依赖于它所运行的容器。由于容器比真正的全功能 Linux 操作系统轻得多,因此它可以轻松地共享并部署到多台不同的机器上。因此,容器和您的微服务可以在任何运行容器引擎的机器上工作。

下图展示了一个微服务架构,其中每个微服务都被一个容器封装:

图 1.7:一个微服务应用,其中所有微服务都被容器封装;应用的生命周期与容器绑定,并且可以轻松地在任何运行容器引擎的机器上进行部署。

容器也与 DevOps 方法论非常契合。通过在容器中本地开发,稍后在生产环境中构建并部署,您可以确保在与最终运行应用程序的环境相同的环境中进行开发。

容器引擎不仅能管理容器的生命周期,还能管理围绕容器的整个生态系统。它们可以管理网络、不同容器之间的通信,这些特性特别适应我们之前提到的微服务架构的特点。

通过将云和容器结合使用,您可以构建一个非常强大的基础设施来托管您的微服务。云将为您提供任意数量的机器。您只需在每台机器上安装容器引擎,就可以在这些机器上部署多个容器化的微服务。

Docker 或 Podman 等容器引擎本身就是非常优秀的工具。然而,您会发现,单独在生产环境中运行它们是很困难的,就像它们本身一样。

容器引擎在开发环境中表现出色,原因在于:

  • 简便性:容器引擎易于安装和使用,允许开发人员快速构建、测试和运行容器化的应用程序。

  • 灵活性:开发人员可以使用容器引擎尝试不同的容器配置,探索容器化的世界。

  • 隔离:容器引擎确保应用程序之间的隔离,防止冲突并简化调试。

然而,生产环境有严格的要求,仅依靠容器引擎无法满足所有需求:

  • 扩展性:容器引擎(如 Docker 或 Podman)并不提供内建的自动扩展功能,无法根据资源利用率动态调整容器部署。

  • 灾难恢复:容器引擎并不提供全面的灾难恢复能力,无法确保在故障发生时服务的可用性。

  • 安全性:尽管容器引擎提供基本的隔离功能,但在多个机器上管理大规模容器化部署的安全策略可能是一个挑战。

  • 标准化:容器引擎需要自定义脚本或集成来与外部系统交互,例如 CI/CD 流水线或监控工具。

尽管容器引擎在开发环境中表现出色,但生产部署需要更强大的方法。Kubernetes 是一个强大的容器编排平台,通过提供全面的功能套件来解决这个挑战。它管理整个容器生命周期,从调度容器运行在可用资源上,到根据需求扩展或缩减部署规模,再到分配流量以优化性能(负载均衡)。与容器引擎的自定义脚本不同,Kubernetes 提供了一个明确定义的 API,用于与容器化应用程序交互,简化了与生产环境中其他工具的集成。除了基本的隔离,Kubernetes 还提供了高级安全功能,如基于角色的访问控制和网络策略。这使得能够高效管理多个团队或项目在同一基础设施上运行的容器化工作负载,优化资源利用,并简化复杂的部署。

在我们深入探讨 Kubernetes 相关话题之前,让我们先在下一节讨论容器和容器引擎的基础知识。

容器引擎

一个 容器引擎 作为终端用户和 REST 客户端的接口,管理用户输入,从容器注册中心下载容器镜像,将下载的镜像提取到磁盘,转换用户或 REST 客户端数据以与容器引擎交互,准备容器挂载点,并促进与容器引擎的通信。从本质上讲,容器引擎作为面向用户的层,简化了镜像和容器管理,而底层的容器运行时处理容器和镜像管理的复杂低级细节。

Docker 脱颖而出,成为最广泛采用的容器引擎之一,但需要注意的是,容器化领域中存在各种替代品。值得注意的有 LXDRktCRI-OPodman

Docker 的核心依赖于 containerd 容器运行时,后者负责容器管理的关键方面,包括容器生命周期、镜像传输和存储、执行与监控,以及存储和网络附件。containerd 进一步依赖于诸如 runchcsshim 等组件。runc 是一个命令行工具,用于在 Linux 中创建和运行容器,而 hcsshim 在创建和管理 Windows 容器方面发挥着关键作用。

值得注意的是,containerd 通常不是为了直接与终端用户交互而设计的。相反,像 Docker 这样的容器引擎通过与容器运行时交互来促进容器的创建和管理。runc 的核心作用非常明显,它不仅服务于 containerd,还被 Podman、CRI-O 以及间接地被 Docker 本身使用。

容器基础知识

正如我们在上一节中所学,Docker 是一个广为人知且被广泛使用的容器引擎。现在,让我们了解一些与容器相关的基本术语。

容器镜像

容器镜像是一种由容器引擎用来启动容器的模板。容器镜像是一个自包含的可执行包,封装了应用程序及其依赖项。它包含运行软件所需的一切,如代码、运行时、库和系统工具。容器镜像是从 DockerfileContainerfile 创建的,这些文件指定了构建步骤。容器镜像存储在镜像仓库中,并通过 Docker Hub 等容器注册中心共享,是容器化的基本组成部分。

容器

容器可以视为容器镜像的一个运行实例。容器就像是应用程序的模块化运输箱。它们将应用程序的代码、依赖关系和运行时环境打包成一个轻量级的单一包裹。容器在不同的环境中运行时保持一致,因为它们包含运行软件所需的所有内容。每个容器独立运行,避免了与系统中其他应用程序的冲突。容器共享主机操作系统的内核,使得容器比虚拟机启动和停止更快。

容器注册中心

容器注册表是一个集中式仓库,用于存储和共享容器镜像。它充当分发机制,允许用户将镜像推送到注册表或从注册表拉取镜像。流行的公共注册表包括 Docker Hub、Red Hat Quayi、Amazon 的 Elastic Container Registry (ECR)、Azure Container Registry、Google Container Registry 和 GitHub Container Registry。组织通常使用私有注册表来安全地存储和共享自定义镜像。注册表在 Docker 生态系统中起着至关重要的作用,促进了容器化应用程序的协作和高效管理。

Dockerfile 或 Containerfile

Dockerfile 或 Containerfile 是一个包含构建容器镜像一系列指令的文本文件。它定义了基础镜像、设置环境、复制应用程序代码、安装依赖项,并配置运行时设置。Dockerfile 或 Containerfile 提供了一种可重复和自动化的方式来创建一致的镜像,使开发人员能够对应用程序配置进行版本管理并共享。

一个示例 Dockerfile 可以在以下代码片段中看到:

# syntax=docker/dockerfile:1

FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000 

下面是对所提供的 Dockerfile 的逐行解释:

  1. # syntax=docker/dockerfile:1:这一行定义了构建镜像时使用的 Dockerfile 语法版本。在本例中,它指定了标准 Dockerfile 语法的版本 1。

  2. FROM node:18-alpine:这一行定义了容器的基础镜像。它指示容器引擎使用官方的 Node.js 18 镜像,并基于 Alpine Linux。这样为您的应用程序提供了一个轻量高效的基础。

  3. WORKDIR /app:这一行设置了容器内的工作目录。这里,它将 /app 设置为工作目录。接下来的 Dockerfile 命令将相对于这个目录执行。

  4. COPY . .:这一行将当前上下文中的所有文件和目录(即包含 Dockerfile 的目录)复制到前一步中定义的工作目录(/app)中。实际上,它是将整个应用程序代码库复制到容器中。

  5. RUN yarn install --production:这一行指示容器引擎在容器内执行一个命令。这里,它运行 yarn install --production。这个命令使用 yarn 包管理器安装 package.json 文件中列出的所有生产环境依赖项。--production 标志确保只安装生产依赖项,排除开发依赖项。

  6. CMD ["node", "src/index.js"]:这一行定义了容器启动时执行的默认命令。这里,它指定了一个包含两个元素的数组:"node""src/index.js"。这告诉 Docker 启动 Node.js 解释器(node)并执行应用程序的入口脚本(src/index.js)。

  7. EXPOSE 3000:这一行暴露了容器中的一个端口。在这里,它暴露了容器中的端口 3000。这默认并不会将端口映射到主机,但允许你在稍后运行容器时通过 -p 标志来映射(例如 docker run -p 3000:3000 my-image)。暴露端口 3000 表明你的应用可能正在此端口监听传入连接。

    重要提示

    要构建容器镜像,你可以使用支持的容器引擎(如 Docker 或 Podman)或容器构建工具,例如 Buildah 或 kaniko。

Docker Compose 或 Podman Compose

Docker Compose 是一个定义和运行多容器应用程序的工具。它使用 YAML 文件来配置应用所需的服务、网络和数据卷,允许开发人员在一个文件中定义整个应用堆栈。Docker Compose 或 Podman Compose 简化了复杂应用程序的编排,使得管理多个容器作为一个单一应用堆栈变得更加容易。

以下 compose.yaml 文件将通过单个 docker composepodman compose 命令启动两个容器,构建一个 WordPress 应用堆栈:

# compose.yaml
services:
  db:
    image: docker.io/library/mariadb
    command: '--default-authentication-plugin=mysql_native_password'
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=somewordpress
      - MYSQL_DATABASE=wordpress
      - MYSQL_USER=wordpress
      - MYSQL_PASSWORD=wordpress
    expose:
      - 3306
      - 33060
    networks:
      - wordpress
  wordpress:
    image: wordpress:latest
    ports:
      - 8081:80
    restart: always
    environment:
      - WORDPRESS_DB_HOST=db
      - WORDPRESS_DB_USER=wordpress
      - WORDPRESS_DB_PASSWORD=wordpress
      - WORDPRESS_DB_NAME=wordpress
    networks:
      - wordpress
volumes:
  db_data:
networks:
  wordpress: {} 

在下一节中,我们将学习 Kubernetes 如何高效地编排所有这些容器操作。

Kubernetes 如何帮助你管理容器?

在本节中,我们将重点讨论 Kubernetes,这也是本书的目的所在。

Kubernetes – 旨在运行生产环境中的工作负载

如果你打开官方 Kubernetes 网站(kubernetes.io),你会看到的标题是 生产级容器编排

图 1.8:Kubernetes 首页展示标题并将 Kubernetes 介绍为生产级容器编排平台

这四个词完美地总结了 Kubernetes 的功能:它是一个面向生产的容器编排平台。Kubernetes 并不旨在取代 Docker 或 Docker 或其他容器引擎的任何功能;而是旨在管理运行容器运行时的机器集群。在使用 Kubernetes 时,你同时使用 Kubernetes 和完整功能的标准容器运行时安装。

标题提到的生产一词确实非常关键。事实上,生产概念是 Kubernetes 的核心:它是为了满足现代生产需求而构思和设计的。如今,管理生产工作负载的方式与 2000 年代的做法不同。2000 年代时,你的生产工作负载通常由少数几台裸金属服务器组成,甚至可能只有一台本地服务器。这些服务器大多直接在宿主 Linux 系统上运行单体应用。然而今天,得益于公共云平台,如Amazon Web ServicesAWS)或Google Cloud PlatformGCP),任何人都可以通过几个点击获取数百甚至数千台机器,作为实例或虚拟机。更棒的是,我们不再在宿主系统上部署应用程序,而是将其作为容器化的微服务部署在 Docker Engine 之上,从而减少了宿主系统的负担。

当你必须在云端的每台虚拟机上管理 Docker 安装时,问题就会出现。假设你在首选云平台上启动了 10 台(或者 100 台或 1,000 台)机器,并且你希望完成一个非常简单的任务:在这些机器上部署容器化的 Docker 应用程序。

你可以通过在每台机器上运行docker run命令来实现这个操作。虽然这样是可行的,但当然有更好的方法,那就是使用容器编排工具,例如Kubernetes。为了让你对 Kubernetes 有一个极其简化的理解,它是一个REST API,用于保持你运行 Docker 守护进程的机器的注册表。

再次强调,这只是对 Kubernetes 的一个极其简化的定义。实际上,它并不是由单一的集中式 REST API 组成,因为正如你可能已经了解的那样,Kubernetes 是作为一组微服务构建的。

另外需要注意的是,虽然 Kubernetes 在管理容器化工作负载方面表现出色,但它并不能完全取代虚拟机(VMs)。虚拟机在某些特定的应用场景中仍然非常有价值,例如运行遗留应用程序或具有复杂依赖关系且难以容器化的软件。然而,Kubernetes 正在不断发展,弥补容器和虚拟机之间的差距。

KubeVirt – 容器与虚拟机之间的桥梁

KubeVirt是一个扩展 Kubernetes 管理虚拟机能力的项目,使用的是熟悉的 Kubernetes API。这使得用户能够利用 Kubernetes 的强大功能和灵活性来部署虚拟机,并与容器化应用一起管理。KubeVirt 采用了基础设施即代码IaC)原则,使用户能够在 Kubernetes 清单中声明性地定义和管理虚拟机。这简化了虚拟机管理,并无缝地将其集成到现有的 Kubernetes 工作流程中。

通过将虚拟机纳入 Kubernetes 的框架,KubeVirt 为需要容器和虚拟机混合环境的组织提供了一种有吸引力的解决方案。这展示了 Kubernetes 作为一个平台在管理多样化工作负载方面的持续演变,可能会推动更加统一的应用程序部署和管理方式。

我们已经了解了容器及其在大规模管理和编排中的复杂性。在接下来的部分,我们将了解 Kubernetes 的历史和演变。

了解 Kubernetes 的历史

现在,让我们来讨论一下 Kubernetes 项目的历史。了解 Kubernetes 项目的背景以及推动这个项目发展的人员,对你来说将非常有帮助。

了解 Kubernetes 是如何以及在何处开始的

自 1998 年成立以来,谷歌在大规模管理高需求工作负载方面积累了大量经验,特别是基于容器的工作负载。从 2000 年代中期开始,谷歌一直处于将应用程序开发为 Linux 容器的前沿。在 Docker 简化容器使用之前,谷歌就已经认识到容器化的优势,并发起了一个名为 Borg 的内部项目。为了增强 Borg 的架构,使其更加可扩展和健壮,谷歌启动了另一个容器编排项目,名为 Omega。随后,Omega 引入的几个改进也被纳入了 Borg 项目中。

Kubernetes 最初是作为谷歌的一个内部项目诞生的,Kubernetes 的第一次提交发生在 2014 年,由 Brendan Burns、Joe Beda 和 Craig McLendon 等人完成。然而,谷歌并没有独立开源 Kubernetes。正是像 Clayton Coleman 这样的个人努力推动了 Kubernetes 的开源,并确保其作为社区驱动项目的成功。当时在 Red Hat 工作的 Clayton Coleman 在这一过程中发挥了至关重要的作用。Kelsey Hightower 是 CoreOS 的早期 Kubernetes 支持者,他成为了倡导这项技术的重要声音。通过他的演讲、写作以及作为 KubeCon 共同创始人的工作,他大大推动了 Kubernetes 的普及和社区的发展。

今天,除了谷歌,Red Hat、亚马逊、微软等公司也在积极参与 Kubernetes 项目。

重要提示

Borg 并不是 Kubernetes 的前身,因为该项目并没有死亡,至今仍在谷歌使用。更准确的说法是,Borg 中的许多理念被重新利用于 Kubernetes 的开发。请记住,Kubernetes 不是 Borg 或 Omega。Borg 是用 C++编写的,而 Kubernetes 是用 Go 编写的。事实上,它们是两个完全不同的项目,但一个深受另一个的启发。这一点非常重要:Borg 和 Omega 是谷歌的两个内部项目,它们并不是为公众开发的。

Kubernetes 是通过 Google 在生产环境中管理容器的经验开发的。最重要的是,它继承了 Borg 和 Omega 的思想、概念和架构。以下是从 Borg 和 Omega 中提取的思想和概念简要列表,这些现在已经在 Kubernetes 中实现:

  • Pods 概念用于管理容器:Kubernetes 使用一个逻辑对象,称为 Pod,用来创建、更新和删除容器。

  • 每个 Pod 在集群中都有自己的 IP 地址。

  • 存在分布式组件,它们都监视着中央 Kubernetes API,以获取集群状态。

  • Pods 和 Services 之间存在内部负载均衡。

  • 标签和选择器是元数据,用于在 Kubernetes 中一起管理和编排资源。

这就是为什么 Kubernetes 在大规模生产环境中管理容器时如此强大的原因。事实上,你从 Kubernetes 中学到的概念比 Kubernetes 本身还要古老。尽管 Kubernetes 是一个年轻的项目,但它是建立在坚实的基础上的。

今天谁在管理 Kubernetes?

Kubernetes 不再由 Google 维护,因为 Google 于 2018 年 8 月 29 日将 Kubernetes 项目的运营控制权移交给了云原生计算基金会CNCF)。CNCF 是一个非营利组织,旨在促进和维持云原生技术的开放生态系统。

Google 是 CNCF 的创始成员之一,与思科、红帽、英特尔等公司一起。Kubernetes 的源代码托管在 GitHub 上,并且是该平台上一个非常活跃的项目。Kubernetes 的代码使用 Apache 2.0 许可证,这是一种宽松的开源许可证。你无需为使用 Kubernetes 付费,如果你精通 Go 语言,你甚至可以为代码做贡献。

Kubernetes 现在的现状如何?

在容器编排领域,Kubernetes 面临着来自各种替代方案的竞争,包括开源解决方案和平台特定的服务。一些 notable 竞争者包括:

  • Apache Mesos

  • HashiCorp Nomad

  • Docker Swarm

  • 亚马逊 ECS

虽然这些编排工具各有优劣,但 Kubernetes 在该领域仍然以其广泛的采用度和受欢迎程度脱颖而出。

Kubernetes 已经赢得了流行度和采用度的竞争,成为了在生产环境中部署基于容器的工作负载的标准方式。随着其巨大的增长,Kubernetes 已经成为 IT 行业最热门的话题之一,对于云服务提供商来说,推出 Kubernetes 服务成为了一个至关重要的任务。因此,Kubernetes 几乎在任何地方都得到了支持。

以下基于 Kubernetes 的服务可以帮助你只需几次点击就能启动并运行一个 Kubernetes 集群:

  • Google Cloud Platform 上的 Google Kubernetes Engine(GKE)

  • 弹性 Kubernetes 服务(Amazon EKS)

  • Microsoft Azure 上的 Azure Kubernetes Service

  • 阿里云 Kubernetes 容器服务(ACK)

它不仅仅涉及云服务的提供。它还涉及平台即服务(PaaS)市场。Red Hat 从 2015 年发布 OpenShift 版本 3 开始,将 Kubernetes 融入其 OpenShift 容器平台。这标志着 OpenShift 架构的重大转变,从原有设计转向基于 Kubernetes 的容器编排系统,为用户提供了增强的容器管理功能,并提供了一整套企业级工具,用于在 Kubernetes 上构建、部署和管理容器。除此之外,其他项目如 Rancher 也作为 Kubernetes 发行版 提供围绕 Kubernetes 编排器的完整工具集,而像 Knative 这样的项目则使用 Kubernetes 编排器管理无服务器工作负载。

重要提示

AWS 是一个例外,因为它有两个容器编排服务。第一个是 Amazon ECS,完全由 AWS 制作。第二个是 Amazon EKS,发布的时间晚于 ECS,是 AWS 上完整的 Kubernetes 服务。这些服务不相同,所以不要被它们相似的名称误导。

Kubernetes 将去向何方?

Kubernetes 不仅仅局限于容器!它正在发展,以管理更广泛的工作负载。KubeVirt 将其扩展到虚拟机,而与 TensorFlow 等 AI/ML 框架的集成可能使 Kubernetes 甚至能够编排机器学习任务。Kubernetes 的未来充满灵活性,可能成为管理容器、虚拟机甚至 AI/ML 工作流的“一站式”平台。

如果你从事云原生应用的生产管理,今天学习 Kubernetes 是你能做出的最明智的决定之一。Kubernetes 正在快速发展,完全不必怀疑它的增长会停止。

通过掌握这个强大的工具,你将获得目前 IT 行业中最受欢迎的技能之一。希望你现在已经信服了!

在下一部分中,我们将学习 Kubernetes 如何简化运维工作。

探索 Kubernetes 解决的问题

那么,为什么 Kubernetes 适合 DevOps 团队呢?这里有个联系:Kubernetes 作为容器编排平台,在容器化应用的部署、扩展和网络管理方面表现突出。容器是轻量级的包,将应用及其依赖项捆绑在一起,从而使跨不同环境的部署更快、更可靠。用户选择使用 Kubernetes 的原因有很多:

  • 自动化:Kubernetes 自动化了与部署和管理容器化应用程序相关的许多手动任务,让开发人员可以腾出时间专注于创新。

  • 可扩展性:Kubernetes 方便地根据需求扩展或缩减应用程序,确保资源的最佳利用。

  • 一致性:Kubernetes 确保在不同环境中一致的部署,从开发到生产,最小化配置错误并简化交付过程。

  • 灵活性:Kubernetes 兼容 DevOps 团队常用的各种工具和技术,简化了与现有工作流的集成。

你可以想象,在本地机器或开发环境上启动容器并不会像在可能面临数百万用户的远程机器上启动这些容器那样需要相同级别的规划。生产环境中特有的问题将会出现,而 Kubernetes 在生产中使用容器时,正是解决这些问题的绝佳方式:

  • 确保高可用性

  • 处理发布管理和容器部署

  • 容器自动扩展

  • 网络隔离

  • 基于角色的访问控制(RBAC)

  • 有状态工作负载

  • 资源管理

确保高可用性

高可用性是生产环境中的核心原则。这意味着你的应用程序应始终保持可访问,并且永远不应出现宕机。 当然,这个目标有些乌托邦化,甚至最大的大公司也会经历服务中断。然而,你应该始终记住,这就是你的目标。Kubernetes 包含一整套功能,能够通过将容器复制到多个主机并定期频繁地监控它们的健康状况来确保容器的高可用性。

当你部署容器时,应用程序的可访问性将直接取决于容器的健康状况。假设由于某种原因,包含你某个微服务的容器变得不可访问;仅凭 Docker,你无法自动确保容器被终止并重新创建以保证服务恢复。而在 Kubernetes 中,这是可能的,因为 Kubernetes 会帮助你设计能够自动修复自己的应用程序,执行如健康检查和容器替换等自动化任务。

如果集群中的一台机器发生故障,所有在其上运行的容器都会消失。Kubernetes 会立即察觉到这一点,并将所有容器重新调度到另一台机器上。通过这种方式,你的应用程序将变得高度可用并具备容错能力。

发布管理和容器部署

部署管理是 Kubernetes 解决的另一个生产特定的问题。部署过程包括在生产环境中更新应用程序,用新版本替换某个微服务的旧版本。

生产环境中的部署总是复杂的,因为你必须更新那些正在响应终端用户请求的容器。如果你错过了它们,可能会对你的应用程序造成严重后果,因为它可能会变得不稳定或无法访问,这就是为什么你应该始终能够通过执行回滚操作快速恢复到应用程序的先前版本。部署的挑战在于,它需要以最不显眼的方式进行,尽量减少对终端用户的影响。

每当你发布新版本的应用时,涉及的多个过程如下:

  1. 更新 DockerfileContainerfile,加入最新的应用信息(如果有的话)。

  2. 使用应用的最新版本构建一个新的 Docker 容器镜像。

  3. 将新的容器镜像推送到容器镜像仓库。

  4. 从容器镜像仓库将新的容器镜像拉取到预发布/UAT/生产系统(Docker 主机)中。

  5. 停止并删除系统上运行的现有(旧版本)应用容器。

  6. 在预发布/UAT/生产系统中启动包含新版本应用容器镜像的新容器镜像。

请参考下图以了解典型场景中的高层次流程(请注意,这是一个理想场景,因为在实际环境中,你可能会为开发、预发布和生产环境使用不同且隔离的容器镜像仓库)。

图 1.9:容器管理的高层次工作流

重要提示

容器构建过程与 Kubernetes 完全无关:它纯粹是容器镜像管理的部分。Kubernetes 直到你需要基于新构建的镜像部署新容器时才会发挥作用。

如果没有 Kubernetes,你将不得不在你想要部署新版本容器的机器上运行所有这些操作,包括 docker pulldocker stopdocker deletedocker run。然后,你还需要在每台运行容器副本的服务器上重复这些操作。这样虽然能工作,但由于没有自动化,过程非常繁琐。而且,猜猜看?Kubernetes 可以为你自动化这一切。

Kubernetes 具有可以管理 Docker 容器部署和回滚的功能,这将在你应对这个问题时让你的生活变得更加轻松。只需一条命令,你就可以要求 Kubernetes 更新你所有机器上的容器,具体操作如下:

$ kubectl set image deploy/myapp myapp_container=myapp:1.0.0 

在一个真实的 Kubernetes 集群中,这条命令将更新每台机器上运行的名为 myapp_container 的容器,并将其部署为应用 myapp 的一部分,更新到 1.0.0 标签版本。

无论是必须更新在单台机器上运行的容器,还是更新跨多个数据中心的数百万个容器,这个命令的作用是相同的。更棒的是,它能确保高可用性。

记住,目标始终是满足高可用性的要求;部署不应导致你的应用崩溃或服务中断。Kubernetes 本身能够管理像滚动更新这样的部署策略,旨在防止服务中断。

此外,Kubernetes 会将特定部署的所有版本保存在内存中,并允许你通过一个命令回滚到先前的版本。这是一个非常强大的工具,可以让你通过一个命令更新整个 Docker 容器集群。

自动扩展容器

扩展是另一个与生产相关的问题,已经通过公共云服务如亚马逊 Web 服务AWS)和谷歌云平台GCP)被广泛普及。扩展是指根据所面临的负载调整计算能力,再次是为了满足高可用性和负载均衡的要求。永远记住,目标是防止系统宕机和停机。

当你的生产机器面临流量激增,而其中一个容器不再能够应对负载时,你需要找到一种有效的方式来扩展容器工作负载。有两种扩展方法:

  • 垂直扩展:这使得你的容器可以使用主机提供的更多计算能力。

  • 水平扩展:你可以在同一台机器或另一台机器上复制你的容器,并且可以在多个容器之间进行负载均衡。

Docker 无法单独应对这个问题;然而,当你使用 Kubernetes 来管理 Docker 时,这变得可能。

图 1.10:垂直扩展与水平扩展的对比

Kubernetes 可以自动管理垂直和水平扩展。它通过让容器从主机获取更多计算能力,或通过创建可以部署在集群中同一节点或其他节点上的额外容器来实现。如果你的 Kubernetes 集群无法处理更多容器,因为所有节点已满,Kubernetes 甚至可以通过一个名为集群自动扩展器的组件,与云提供商接口,自动且透明地启动新的虚拟机。

重要提示

集群自动扩展器仅在 Kubernetes 集群部署在支持的云提供商(私有云或公共云)上时才有效。

如果没有容器编排工具,无法实现这些目标。原因很简单,你无法承担这些任务;你需要考虑 DevOps 的文化和敏捷性,寻求自动化这些任务,使你的应用能够自我修复,具备容错性,并且具有高可用性。

与扩展容器或集群不同,如果负载开始下降,你还必须能够减少容器的数量,以便根据负载调整资源,无论负载是上升还是下降。Kubernetes 也能做到这一点。

网络隔离

在数百万用户的环境中,确保容器之间的安全通信至关重要。传统方法可能涉及复杂的手动配置。此时,Kubernetes 发挥了它的优势:

  • Pod 网络:Kubernetes 为你的 Pods 创建了一个虚拟网络覆盖层。默认情况下,同一 Pod 内的容器可以直接通信,而不同 Pod 之间的容器则默认是隔离的。这防止了容器之间的非预期通信,增强了安全性。

  • 网络策略:Kubernetes 允许你定义精细的网络策略,进一步限制 Pod 之间的通信方式。你可以指定允许的 ingress(入站流量)和 egress(出站流量),确保 Pod 只访问它们所需的资源。这种方法简化了网络配置,增强了生产环境中的安全性。

基于角色的访问控制(RBAC)

在生产环境中,管理对容器资源的访问权限至关重要。以下是 Kubernetes 如何实现安全访问控制:

  • 用户角色:Kubernetes 定义了用户角色,指定了访问和管理容器资源的权限。这些角色可以分配给单个用户或用户组,从而实现对谁可以执行特定操作(如查看 Pod 日志和部署新容器)的精细控制。

  • 服务账户:Kubernetes 使用服务账户为在集群中运行的 Pod 提供身份。这些服务账户可以被分配角色,确保 Pod 只获得其正常运行所需的访问权限。

通过使用用户角色和服务账户的多层次方法,加强了生产环境部署中的安全性和治理。

有状态工作负载

尽管容器通常是无状态的(即数据在停止后不会持久化),但一些应用程序需要持久存储。Kubernetes 提供了管理有状态工作负载的解决方案:持久卷(PVs)持久卷声明(PVCs)。Kubernetes 引入了 PV 的概念,它是由管理员配置的持久存储资源(如主机目录、云存储)。应用程序可以通过 PVC 请求存储。这种抽象将存储管理与应用程序解耦,使容器能够利用持久存储,而无需担心底层细节。

资源管理

在生产环境中,合理分配资源以优化容器性能并避免资源瓶颈变得至关重要。Kubernetes 提供了用于资源管理的功能:

  • 资源配额:Kubernetes 允许你为命名空间或 Pod 设置资源配额(限制和请求),包括 CPU、内存和其他资源。这确保了资源的公平分配,防止单个 Pod 消耗过多资源,导致其他应用程序资源短缺。

  • 资源限制和请求:在定义部署时,你可以为容器指定资源请求(最小保障资源)和资源限制(最大允许资源)。这些确保了你的应用能够获得正常运行所需的资源,同时避免了资源的滥用。

我们将在接下来的章节中学习所有这些功能。

我们应该在每个地方使用 Kubernetes 吗?让我们在下一部分讨论这个问题。

Kubernetes 何时及何地不是解决方案?

Kubernetes 有着不可否认的优势;然而,它并不总是最佳解决方案。在这里,我们列出了几个其他解决方案可能更为适用的情况:

  • 无容器架构:如果你完全不使用容器,那么 Kubernetes 对你将没有任何帮助。

  • 非常少量的微服务或应用:当必须管理大量容器时,Kubernetes 尤为突出。如果你的应用由两到三个微服务组成,那么一个更简单的调度器可能更适合。

总结

第一章为我们提供了一个大致的介绍空间。我们讨论了很多主题,例如单体架构、微服务、Docker 容器、云计算和 Kubernetes。我们还讨论了这个项目是如何诞生的。现在,你应该对如何使用 Kubernetes 在生产环境中管理容器有了一个全面的了解。你也学到了 Kubernetes 为何被引入,以及它是如何成为一个广为人知的容器编排工具的。

在下一章中,我们将讨论 Kubernetes 启动 Docker 容器的过程。你将发现,你可以向 Kubernetes 发出命令,这些命令将被 Kubernetes 解释为运行容器的指令。我们将列出并解释 Kubernetes 的每个组件及其在整个集群中的角色。Kubernetes 集群由许多组件组成,我们将一一介绍它们。我们还将解释 Kubernetes 是如何构建的,重点讲解主节点、工作节点和控制平面组件之间的区别。

进一步阅读

加入我们社区的 Discord

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/cloudanddevops

第二章:Kubernetes 架构 – 从容器镜像到运行的 Pods

在上一章节中,我们从功能角度铺垫了关于 Kubernetes 的基础知识。现在你应该更清楚 Kubernetes 如何帮助你管理运行容器化微服务的机器集群。接下来,让我们深入探讨一些技术细节。在本章节中,我们将研究 Kubernetes 如何使你能够管理分布在不同机器上的容器。在本章节结束后,你应该能够更好地理解 Kubernetes 集群的构成,特别是你将更清楚每个 Kubernetes 组件的职责,以及它们在执行容器时的作用。

Kubernetes 由多个分布式组件组成,每个组件在容器执行过程中都扮演着特定角色。为了理解每个 Kubernetes 组件的角色,我们将跟随容器的生命周期,看看它是如何由 Kubernetes 创建和管理的:即,从你执行创建容器命令的那一刻,到容器最终在你的 Kubernetes 集群中的某台机器上执行的时刻。

本章节将涵盖以下主要内容:

  • 名称 – Kubernetes

  • 理解控制平面节点和计算节点之间的区别

  • Kubernetes 组件

  • 控制平面组件

  • 计算节点组件

  • 探索 kubectl 命令行工具和 YAML 语法

  • 如何使 Kubernetes 高可用

技术要求

以下是继续本章节的技术要求:

  • 基本了解 Linux 操作系统以及如何在 Linux 中处理基本操作

  • 一台或多台 Linux 机器

本章节中使用的代码和片段已经在 Fedora 工作站上测试过。所有本章节的代码、命令和其他片段都可以在 GitHub 仓库中找到,链接:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter02

名称 – Kubernetes

Kubernetes 源自希腊语,特别是来自单词“kubernētēs”,意思是舵手或驾驶员。这个海事术语指的是擅长驾驶和导航船只的人。选择这个名字与平台在引导和编排容器化应用程序的部署和管理方面的基本角色相契合,就像舵手在复杂的数字世界中引导船只一样。

除了正式名称,Kubernetes 在社区中通常被称为“K8s”。这个昵称巧妙地源于通过计算“K”和“s”之间的八个字母来缩写这个词。这种缩写不仅简化了沟通,还为 Kubernetes 生态系统中的讨论增添了一丝非正式感。

理解控制平面节点和计算节点之间的区别

要运行 Kubernetes,您将需要 Linux 机器,这些机器在 Kubernetes 中被称为节点。节点可以是物理机器,也可以是云提供商上的虚拟机器,例如 EC2 实例。Kubernetes 中有两种类型的节点:

  • 控制平面节点(也称为主节点)

  • 计算节点(也称为工作节点)

主节点和工作节点

在不同的上下文中,您可能会遇到“主节点”和“工作节点”这些术语,这些术语以前用来描述分布式系统中传统的角色层次分配。在这种设置中,“主”节点负责监督并分配任务给“工作”节点。然而,这些术语可能带有历史和文化上的含义,可能被认为是不敏感或不合适的。针对这一问题,Kubernetes 社区决定用“控制平面节点”(或控制器节点)来替代这些术语,指代负责管理集群整体状态的组件集合。同样,术语“节点”或“计算节点”现在取代了“工作”节点,用于标识集群中执行请求任务或运行应用工作负载的单独机器。控制平面负责维护 Kubernetes 集群的状态,而计算节点负责运行容器和应用程序。

Linux 和 Windows 容器

您可以灵活地利用基于 Windows 的节点在 Kubernetes 集群中启动适用于 Windows 的容器。值得注意的是,您的集群可以和谐地容纳 Linux 和 Windows 机器;然而,尝试在 Linux 工作节点上启动 Windows 容器,反之亦然,是不可行的。在集群中找到 Linux 和 Windows 机器之间的平衡,以确保最佳性能。

在本章接下来的章节中,我们将学习不同的 Kubernetes 组件及其职责。

Kubernetes 组件

根据 Kubernetes 的固有设计,它作为一个分布式应用程序运行。当我们提到 Kubernetes 时,它并不是一个独立的、在单个构建中发布并安装到专用机器上的大型应用程序。相反,Kubernetes 是由多个小型项目组成,每个项目都用 Go 语言编写,共同构成了 Kubernetes 这个整体项目。

要建立一个完全可操作的 Kubernetes 集群,您需要单独安装和配置这些组件,确保它们之间的无缝通信。完成这些先决条件后,您可以开始使用 Kubernetes 调度器运行您的容器。

对于开发或本地测试,将所有 Kubernetes 组件安装在同一台机器上是可以的。然而,在生产环境中,为了满足高可用性、负载均衡、分布式计算、扩展性等需求,这些组件应该分布在不同的主机上。通过将不同组件分布在多台机器上,你将获得两个好处:

  • 你可以使你的集群高度可用和容错。

  • 你可以让集群更具可扩展性。每个组件都有自己的生命周期,它们可以独立扩展而不会影响其他组件。

这样,即使你的某台服务器宕机,也不会影响整个集群,只会影响其中的一小部分,增加更多机器到你的服务器也变得很容易。

每个 Kubernetes 组件都有自己明确的责任。了解每个组件的职责以及它如何与其他组件协调运作,对于理解 Kubernetes 的整体工作原理非常重要。

根据其角色,组件需要部署在控制平面节点或计算节点上。虽然一些组件负责维护整个集群的状态并操作集群本身,但其他组件则负责通过与容器运行时(例如,containerd 或 Docker 守护进程)直接交互来运行我们的应用容器。因此,Kubernetes 的组件可以分为两类:控制平面组件和计算节点组件。

你不应该自己启动容器,因此,你不会直接与计算节点交互。相反,你将指令发送给控制平面。然后,控制平面会代表你委派实际的容器创建和维护工作给计算节点。

图 2.1:一个典型的 Kubernetes 工作流

由于 Kubernetes 的分布式特性,控制平面组件可以分布在多台机器上。设置控制平面组件有两种方式:

  • 你可以将所有控制平面组件运行在同一台机器上,也可以运行在不同的机器上。为了实现最大的容错能力,最好将控制平面组件分布在不同的机器上。其核心思想是,Kubernetes 组件必须能够相互通信,即使它们安装在不同的主机上,这一点也能得到保证。

  • 在计算节点(或工作节点)方面,事情变得更简单。在这些节点上,你从一台运行支持的容器运行时的标准机器开始,然后将计算节点组件安装到该容器运行时旁边。这些组件将与已安装在该机器上的本地容器引擎进行交互,并根据你发送到控制平面组件的指令执行容器。向集群添加更多计算能力很简单;你只需要添加更多的工作节点,并让它们加入集群,以便为更多容器腾出空间。

通过将控制平面和计算节点的组件分布到不同的机器上,你可以使你的集群具备高度可用性和可扩展性。Kubernetes 的设计考虑了所有云原生相关问题;它的组件是无状态的,易于扩展,并且可以分布在不同的主机上。整体思路是通过将所有组件分散在不同的主机上,避免出现单点故障。

这里是一个简化的图示,展示了一个具有所有组件的完整功能 Kubernetes 集群。在本章中,我们将解释这个图中列出的所有组件,它们的角色和职责。在此,所有控制平面组件都安装在单一的主节点机器上:

图 2.2:一个完整功能的 Kubernetes 集群,包含一个控制平面节点和三个计算节点。

上图展示了一个四节点的 Kubernetes 集群,包含所有必要的组件。

请记住,Kubernetes 是可修改的,因此可以根据特定的环境进行修改。当 Kubernetes 部署并作为某些发行版的一部分使用时,例如 Amazon EKS 或 Red Hat OpenShift,可能会有额外的组件,或者默认组件的行为可能会有所不同。在本书中,大部分情况下,我们将讨论裸 Kubernetes。 本章讨论的组件是默认组件,你会在任何地方看到它们,因为它们是 Kubernetes 的核心组成部分。

以下图示展示了 Kubernetes 集群的基本核心组件。

图 2.3:Kubernetes 集群的组件(图片来源:kubernetes.io/docs/concepts/overview/components

你可能注意到,这些组件中的大多数名称都以 kube 开头:这些是 Kubernetes 项目的一部分。此外,你可能注意到,有两个组件的名称并没有以 kube 开头。其他两个组件(etcd容器引擎)是两个外部依赖,它们并不是严格意义上的 Kubernetes 项目一部分,但 Kubernetes 的正常运行依赖于它们:

  • etcd 是 Kubernetes 项目使用的第三方数据存储。别担心,你不需要精通它就能使用 Kubernetes。

  • 容器引擎也是一个第三方引擎。

请放心,你不需要自己安装和配置这些组件。几乎没有人会自己管理这些组件,事实上,获得一个正常工作的 Kubernetes 集群是非常容易的,不需要单独安装组件。

对于开发目的,你可以使用 minikube,它是一个允许开发者在本地机器上运行单节点 Kubernetes 集群的工具。它是一个轻量级且易于使用的解决方案,可以在不需要完整集群的情况下测试和开发 Kubernetes 应用。绝对不推荐将 minikube 用于生产环境。

对于生产部署,像 Amazon EKS 或 Google GKE 这样的云服务提供了集成的、可扩展的 Kubernetes 集群。或者,kubeadm 作为一个 Kubernetes 安装工具,适用于没有云访问的平台。

出于教育目的,著名的教程 Kubernetes the Hard WayKelsey Hightower 提供,指导用户通过手动安装,涵盖了 PKI 管理、网络配置和 Google Cloud 中裸机 Linux 机器上的计算资源配置。虽然这个教程对于初学者来说可能会感觉困难,但仍然推荐实践,它提供了一个理解 Kubernetes 内部机制的宝贵机会。需要注意的是,建立和管理一个生产级的 Kubernetes 集群,如 Kubernetes the Hard Way 中所示,是复杂且耗时的。建议不要在生产环境中使用该教程的结果。你会在互联网上看到许多对该教程的引用,因为它非常有名。

我们将在下一节学习 Kubernetes 控制平面和计算节点组件。

控制平面组件

这些组件负责维护集群的状态。它们应该安装在控制平面节点上。这些组件会记录由 Kubernetes 集群执行的容器列表或集群中的机器数量。作为管理员,当你与 Kubernetes 交互时,你实际上是与控制平面组件交互,以下是控制平面的主要组件:

  • kube-apiserver

  • etcd

  • kube-scheduler

  • kube-controller-manager

  • cloud-controller-manager

计算节点组件

这些组件负责与容器运行时进行交互,以根据它们从控制平面组件接收到的指令启动容器。计算节点组件必须安装在运行受支持容器运行时的 Linux 机器上,且不应直接与这些组件进行交互。在 Kubernetes 集群中可能有数百或数千个计算节点。以下是计算节点的主要组件部分:

  • kubelet

  • kube-proxy

  • 容器运行时

附加组件

附加组件利用 Kubernetes 资源,如 DaemonSet、Deployment 等,来实现集群特性。由于这些特性在集群级别运行,因此具有命名空间的附加组件资源位于 kube-system 命名空间内。以下是你在 Kubernetes 集群中常见的一些附加组件:

  • DNS

  • Web UI(仪表盘)

  • 容器资源监控

  • 集群级别日志记录

  • 网络插件

托管 Kubernetes 集群中的控制平面

与自管理的 Kubernetes 集群相比,像 Amazon EKS、Google GKE 等云服务会处理大部分 Kubernetes 控制平面组件的安装和配置。它们提供对 Kubernetes 端点的访问,或者可选地,kube-apiserver 端点,而无需暴露底层机器或已配置负载均衡器的复杂细节。这适用于 kube-schedulerkube-controller-manageretcd 等组件。

下面是一个在 Amazon EKS 服务上创建的 Kubernetes 集群截图:

图 2.4:UI 控制台显示在 Amazon EKS 上配置的 Kubernetes 集群详细信息

本书后续章节将详细介绍 EKS、GKE 和 AKS。

在接下来的章节中,我们将学习控制平面组件,它们负责维护集群的状态。

控制平面组件

在接下来的章节中,让我们探索不同的控制平面组件及其职责。

kube-apiserver

Kubernetes 最重要的组件是一个 表述性状态转移 (REST) API,称为 kube-apiserver,它暴露了 Kubernetes 的所有功能。你将通过 kubectl 命令行工具、直接 API 调用或 Kubernetes 仪表盘(Web UI)工具与 Kubernetes 进行交互。

kube-apiserver 的角色

kube-apiserver 是 Kubernetes 控制平面的一部分。它是用 Go 编写的,源代码开源并可以在 GitHub 上通过 Apache 2.0 许可证获取。与 Kubernetes 交互的过程非常简单。每当你想指示 Kubernetes 时,你只需要发送一个 HTTP 请求到 kube-apiserver。无论是创建、删除还是更新容器,你总是通过正确的 HTTP 动词向相应的 kube-apiserver 端点发出这些请求。这就是 Kubernetes 的常规操作—kube-apiserver 是所有指向调度器的操作的唯一入口点。避免直接与容器运行时交互是一个良好的实践(除非是进行故障排除活动)。

kube-apiserver 是按照 REST 标准构建的。REST 通过 HTTP 端点展示功能非常高效,使用 HTTP 协议的不同方法(如 GETPOSTPUTPATCHDELETE)可以访问这些端点。当你将 HTTP 方法和路径结合时,可以对通过路径标识的资源执行方法指定的各种操作。

REST 标准提供了相当大的灵活性,允许通过添加新的路径轻松扩展任何 REST API,添加新的资源。通常,REST API 使用数据存储来管理对象或资源的状态。

在此类 API 中,数据保留可以通过多种方式处理,包括以下几种:

REST API 内存存储

  • 将数据保存在自己的内存中。

  • 然而,这会导致一个有状态的 API,使得扩展变得不可能。

Kubernetes 使用etcd来存储状态,etcd的发音是/ˈɛtsiːdiː/,意思是分布式的etc目录。etcd是一个开源的分布式键值存储,用于保存和管理分布式系统所需的关键信息,以保持系统运行。

数据库引擎使用

  • 使用像 MariaDB 或 PostgreSQL 这样的全功能数据库引擎。

  • 将存储委托给外部引擎使得 API 无状态并且具有水平扩展性。

任何 REST API 都可以轻松升级或扩展,以实现比最初设计更多的功能。总结来说,REST API 的基本属性如下:

  • 依赖于 HTTP 协议

  • 定义由 URL 路径标识的一组资源

  • 指定一组由 HTTP 方法标识的操作

  • 基于正确构造的 HTTP 请求对资源执行操作

  • 在数据存储上维护其资源的状态

总结来说,kube-apiserver只不过是一个 REST API,它是你将要设置的任何 Kubernetes 集群的核心,无论是本地的、云上的,还是本地部署的。它也是无状态的;也就是说,它通过依赖名为etcd的数据库引擎来保持资源的状态。这意味着你可以通过将kube-apiserver部署到多台机器上,使用七层负载均衡器来负载均衡请求,并且不会丢失数据,从而实现kube-apiserver组件的水平扩展。

由于 HTTP 几乎被所有地方支持,因此与 Kubernetes 集群进行通信并向其发出指令非常容易。然而,我们大多数时候是通过名为kubectl的命令行工具与 Kubernetes 互动,它是 Kubernetes 项目官方支持的 HTTP 客户端。当你下载kube-apiserver时,你将得到一个用 Go 语言编译的二进制文件,能够在任何 Linux 机器上执行。Kubernetes 开发者为我们定义了一组资源,这些资源直接打包在该二进制文件中。因此,可以预期在kube-apiserver中涉及容器管理、网络和计算的一般资源。

以下是其中一些资源:

  • Pod

  • ReplicaSet

  • PersistentVolume

  • NetworkPolicy

  • Deployment

当然,这个资源列表并不详尽。如果你想查看完整的 Kubernetes 组件列表,可以从 Kubernetes 官方文档的 API 参考页面访问:kubernetes.io/docs/reference/kubernetes-api/

你可能会想,为什么这里没有容器资源?正如在第一章Kubernetes 基础中提到的,Kubernetes 使用一种名为 Pod 的资源来管理容器。目前,你可以将 Pod 视为容器。

虽然 Pod 可以容纳多个容器,但通常一个 Pod 中只有一个容器。如果你有兴趣在一个 Pod 内使用多个容器,我们将在第五章使用多容器 Pod 和设计模式中探讨如sidecarinit containers等模式。

我们将在接下来的章节中深入学习它们。每个资源都与一个专用的 URL 路径关联,调用该 URL 路径时,改变 HTTP 方法会产生不同的效果。所有这些行为都在kube-apiserver中定义。请注意,这些行为不是你需要开发的;它们是kube-apiserver的一部分,已经直接实现。

在 Kubernetes 对象存储到etcd数据库后,其他 Kubernetes 组件会转换这些对象为原始的容器指令。

记住,kube-apiserver是整个 Kubernetes 集群的中央枢纽和最终源。Kubernetes 中的所有操作都围绕它展开。其他组件,包括管理员,通过 HTTP 与kube-apiserver交互,在大多数情况下避免直接与集群组件交互。

这是因为kube-apiserver不仅管理集群的状态,还包括许多身份验证、授权和 HTTP 响应格式化机制。因此,强烈建议避免手动干预,因为这些过程非常复杂。

如何运行 kube-apiserver?

第三章安装你的第一个 Kubernetes 集群中,我们将重点介绍如何在本地安装和配置 Kubernetes 集群。

本质上,有两种方式可以运行kube-apiserver(以及其他组件),如下所示:

  • 通过将kube-apiserver作为容器镜像运行

  • 通过下载并安装kube-apiserver并使用systemd单元文件运行它

由于推荐的方法是运行容器化的kube-apiserver,我们暂时放下systemd方法。根据 Kubernetes 集群的部署机制,kube-apiserver和其他组件将通过从容器注册表(例如registry.k8s.io)下载相应的镜像,作为容器进行配置。

你在哪里运行 kube-apiserver?

kube-apiserver应在控制平面节点上运行,因为它是控制平面的一部分。确保kube-apiserver组件安装在专门用于控制平面操作的强大机器上。这个组件至关重要,如果它变得无法访问,尽管你的容器仍然存在,但它们将失去与 Kubernetes 的连接。本质上,它们会变成“孤立”容器,处于独立的机器上,不再受 Kubernetes 管理。

此外,来自所有集群节点的其他 Kubernetes 组件会不断向kube-apiserver发送 HTTP 请求,以了解集群的状态或更新状态。而且,计算节点越多,向kube-apiserver发出的 HTTP 请求就越多。因此,kube-apiserver应该随着集群的扩展而进行独立扩展。

如前所述,kube-apiserver是一个无状态组件,它本身并不直接维护 Kubernetes 集群的状态,而是依赖于第三方数据库来实现这一点。你可以通过将其托管在一组机器上并放置在负载均衡器后面(如 HTTP API)来横向扩展它。在这种设置下,你通过调用 API 负载均衡器的端点与kube-apiserver进行交互。

在接下来的章节中,我们将学习 Kubernetes 如何使用etcd存储集群和资源信息。

etcd 数据存储

我们之前解释过,kube-apiserver可以横向扩展。我们还提到,kube-apiserver使用etcd来存储集群状态和详细信息,etcd是一个开源的分布式键值存储。严格来说,etcd不是 Kubernetes 项目的一部分,而是由etcd-io社区维护的独立项目。

虽然etcd是 Kubernetes 集群中常用的数据存储,但一些发行版,如k3s,默认使用其他替代方案,例如 SQLite,甚至是像 MySQL 或 PostgreSQL 这样的外部数据库(docs.k3s.io/datastore)。

etcd也是一个开源项目(像 Kubernetes 一样用 Go 编写),可以在 GitHub 上找到(github.com/etcd-io/etcd),并采用 Apache 2.0 许可证。它还是一个由云原生计算基金会CNCF)孵化的项目(2018 年孵化,2020 年毕业),该基金会是 Kubernetes 的维护组织。

当你调用kube-apiserver时,每次通过调用 Kubernetes API 进行读写操作时,都会从etcd中读取或写入数据。

现在,让我们深入了解主节点内部的内容:

图 2.5:kube-apiserver 组件位于 etcd 数据存储前面,充当其代理;kube-apiserver 是唯一可以从 etcd 读取或写入数据的组件。

etcd就像你集群的“心脏”。如果丢失了etcd中的数据,你的 Kubernetes 集群将无法再正常工作。它比kube-apiserver还要关键。如果kube-apiserver崩溃,你可以重启它。但如果etcd数据丢失或损坏且没有备份,你的 Kubernetes 集群就完了。

幸运的是,你不需要深入掌握etcd才能使用 Kubernetes。如果你不知道自己在做什么,强烈建议你完全不要接触它。因为操作不当可能会破坏etcd中存储的数据,从而影响集群的状态。

记住,Kubernetes 架构中的一般规则是,每个组件都必须通过kube-apiserver来读取或写入etcd。这是因为,从技术角度看,kubectl通过 TLS 客户端证书对kube-apiserver进行身份验证,而只有kube-apiserver拥有该证书。因此,它是唯一有权读取或写入etcd的 Kubernetes 组件。这在 Kubernetes 的架构中是一个非常重要的概念。所有其他组件都无法在没有通过 HTTP 调用kube-apiserver端点的情况下,读取或写入etcd中的任何内容。

请注意,etcd也被设计为一个 REST API。默认情况下,它监听2379端口。

让我们探讨一个简单的kubectl命令,如下所示:

$ kubectl run nginx --restart Never --image nginx 

当你执行上述命令时,kubectl工具将创建一个 HTTP POST请求,该请求将执行针对kube-apiserver组件的操作,该组件在kubeconfig文件中指定。kube-apiserver会在etcd中写入一个新条目,该条目将持久化存储在磁盘上。

此时,Kubernetes 的状态发生了变化:接下来,将由其他 Kubernetes 组件来协调集群的实际状态与目标状态(即etcd中的状态)。

与 Redis 或 Memcached 不同,etcd不是内存存储。如果你重启机器,你不会丢失数据,因为它保存在磁盘上。

你在哪里运行etcd

在自我管理的 Kubernetes 环境中,你可以在容器内或作为systemd单元文件的一部分操作etcdetcd可以通过将数据集分布到多个服务器上,天然地进行水平扩展,成为一个独立的集群解决方案。

此外,你有两个地方可以运行 Kubernetes 的etcd,如下所示:

  • etcd可以与kube-apiserver(以及其他控制平面组件)一起部署在控制平面节点上——这是默认且简单的设置(在大多数 Kubernetes 集群中,像etcdkube-apiserver这样的组件通常使用静态清单进行初始化部署。我们将在本书后面更详细地探讨这种方法及其替代方案)。

  • 你可以配置使用专用的etcd集群——这是一种更复杂的方法,但如果你的环境对可靠性有较高要求,它会更可靠。

操作 Kubernetes 的etcd集群

有关单节点或多节点专用etcd集群的详细信息,请参见 Kubernetes 官方文档:kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/

了解更多关于etcd的信息

如果你有兴趣了解etcd的工作原理,并想尝试etcd数据集,可以访问一个免费的在线游乐场。访问play.etcd.io/play,学习如何管理etcd集群和其中的数据。

让我们在下一节中探讨并了解kube-scheduler

kube-scheduler

kube-scheduler负责从可用的工作节点中选举出一个节点来运行新创建的 Pod。

在创建时,Pods 是未调度的,表示尚未指定任何工作节点来执行它们。未调度的 Pod 会被记录在etcd中,但没有分配工作节点。因此,任何活动的kubelet都不会收到启动此 Pod 的通知,导致 Pod 规范中列出的容器无法执行。

在内部,Pod 对象在etcd中存储时有一个属性叫做nodeName。顾名思义,这个属性应该包含将托管 Pod 的工作节点的名称。当这个属性被设置时,我们称该 Pod 已被调度;否则,Pod 仍处于挂起状态,等待调度。

kube-scheduler会定期查询kube-apiserver,列出那些尚未被调度nodeName属性为空的 Pods。一旦找到这些 Pods,它将执行一个算法来选举一个工作节点。然后,它会通过向kube-apiserver组件发出 HTTP 请求来更新 Pod 中的nodeName属性。在选举工作节点时,kube-scheduler组件会考虑一些配置值,这些配置值是你可以传递的:

图 2.6:kube-scheduler组件轮询kube-apiserver组件以查找未调度的 Pod

kube-scheduler组件将考虑你可以选择传递的一些配置值。通过使用这些配置,你可以精确控制kube-scheduler组件如何选举工作节点。以下是调度 Pods 到你首选节点时需要注意的一些特性:

  • 节点选择器

  • 节点亲和性和反亲和性

  • 污点和容忍度

也有一些高级调度技术可以完全绕过kube-scheduler组件。我们稍后将讨论这些特性。

kube-scheduler组件可以被自定义组件替代。你可以实现自己的kube-scheduler组件,使用自定义逻辑选择节点并在集群中使用它。这是 Kubernetes 组件分布式特性的一大优势。

你应该将kube-scheduler安装在哪里?

你可以选择将kube-scheduler安装在一台专用机器上,或与kube-apiserver安装在同一台机器上。这是一个简短的过程,不会消耗太多资源,但有一些事项需要注意。

kube-scheduler组件应该是高可用的。这就是为什么你应该在多台机器上安装它。如果你的集群没有一个正常工作的kube-scheduler组件,新的 Pods 将无法被调度,结果就是会有大量挂起的 Pods。同时请注意,如果没有kube-scheduler组件,它不会对已经调度的 Pods 产生影响。

在下一节中,我们将学习另一个重要的控制平面组件,叫做kube-controller-manager

kube-controller-manager

kube-controller-manager 是一个重要的单一二进制文件,包含了各种功能,实质上嵌入了所谓的控制器。它是执行我们所称的协调循环的组件。kube-controller-manager 尝试保持集群的实际状态与 etcd 中描述的状态一致,以确保两者之间没有差异。

在某些情况下,集群的实际状态可能会偏离存储在 etcd 中的期望状态。这种差异可能由 Pod 故障或其他因素引起。因此,kube-controller-manager 组件在协调实际状态和期望状态方面发挥着至关重要的作用。例如,考虑到复制控制器,这是 kube-controller-manager 组件内运行的控制器之一。实际上,Kubernetes 允许你在不同的计算节点上指定并维护一定数量的 Pod。如果由于任何原因,实际的 Pod 数量与指定的数量不符,复制控制器会向 kube-apiserver 组件发起请求,试图在 etcd 中重建一个新的 Pod,从而替换在计算节点上失败的 Pod。

以下是 kube-controller-manager 中的一些控制器:

  • 节点控制器:处理节点的生命周期,负责节点的添加、删除和在集群中的更新

  • 复制控制器:确保始终保持指定数量的副本,以符合 Pod 规格

  • 端点控制器:为服务填充端点对象,反映每个服务可用的当前 Pod

  • 服务账户控制器:管理命名空间中的 ServiceAccount,确保每个当前活跃的命名空间中都存在名为 default 的 ServiceAccount

  • 命名空间控制器:管理命名空间的生命周期,包括创建、删除和隔离

  • 部署控制器:管理部署的生命周期,确保每个部署的 Pod 数量保持在所需状态

  • StatefulSet 控制器:管理有状态集的生命周期,保持所需的副本数、Pod 顺序和身份

  • DaemonSet 控制器:管理守护进程集的生命周期,确保每个集群节点上都有一个守护进程 Pod 副本处于活动状态

  • 任务控制器:管理任务的生命周期,确保每个任务的 Pod 数量保持在指定数量,直到任务完成

  • 水平 Pod 自动扩展器 (HPA) 控制器:根据资源利用率或其他指标动态调整部署或有状态集的副本数量

  • Pod 垃圾回收器:移除不再由所有者(如复制控制器或部署)控制的 Pod

正如你所理解的,kube-controller-manager组件相当庞大。但本质上,它是一个单一的二进制文件,负责将集群的实际状态与存储在etcd中的期望状态进行调节。

你在哪里运行 kube-controller-manager?

kube-controller-manager组件可以像kube-apiserver一样,作为容器或systemd服务运行在控制平面节点上。此外,你还可以选择将kube-controller-manager组件安装在专用的机器上。现在,让我们来谈谈cloud-controller-manager

cloud-controller-manager

cloud-controller-manager是 Kubernetes 控制平面中的一个组件,负责管理 Kubernetes 与底层云基础设施之间的交互。cloud-controller-manager处理云资源的供应和管理,包括节点和卷,以促进 Kubernetes 工作负载。它专门操作针对云提供商定制的控制器。在 Kubernetes 是自托管的、在个人计算机上的学习环境中,或在本地部署时,集群不会拥有云控制器管理器。

类似于kube-controller-managercloud-controller-manager将多个逻辑上独立的控制循环整合为一个统一的二进制文件,并作为一个单独的进程执行。通过运行多个副本来实现水平扩展,这是提高性能或增强容错性的一种选择。

具有潜在云提供商依赖的控制器包括:

  • 节点控制器:验证节点在停止响应后是否已被云中删除

  • 路由控制器:在底层云基础设施中建立路由

  • 服务控制器:管理云提供商负载均衡器的创建、更新和删除

你在哪里运行 cloud-controller-manager?

cloud-controller-manager组件可以像kube-apiserver一样,作为容器或systemd服务运行在控制平面节点上。

在接下来的章节中,我们将讨论 Kubernetes 集群中计算节点(也称为工作节点)的组成部分。

计算节点组件

我们将在本章的这一部分中,详细解释计算节点的组成,介绍运行在其上的三个组件:

  • 容器引擎和容器运行时

  • kubelet

  • kube-proxy组件

    kubeletkube-proxy和容器运行时是控制平面(主节点)和工作节点的必备组件。我们将在本节中介绍它们,以突出它们在这两种环境中的功能。

容器引擎和容器运行时

容器引擎是一个旨在监督容器创建、执行和生命周期的软件平台。与容器运行时相比,它提供了一个更抽象的层次,简化了容器管理并提高了开发者的可访问性。知名的容器引擎包括 Podman、Docker Engine 和 CRI-O。相对而言,容器运行时是一个基础的软件组件,负责在容器引擎或容器编排器的指示下创建、执行和管理容器。它为容器操作提供必要的功能,包括镜像加载、容器创建、资源分配和容器生命周期管理等任务。Containerdruncdockerd和 Mirantis Container Runtime 是一些知名的容器运行时。

“容器引擎”和“容器运行时”这两个术语有时可以互换使用,这可能会导致混淆。容器运行时(低层次)是负责执行容器镜像、管理其生命周期(启动、停止、暂停)并与底层操作系统交互的核心引擎。例子包括runc和 CRI-O(当用作运行时时)。容器引擎(高层次)建立在容器运行时之上,提供额外的功能,如镜像构建、注册表和管理工具。可以参考 Docker、Podman 或 CRI-O(当与 Kubernetes 一起使用时)。记住,关键在于理解核心功能:低层次的运行时处理容器执行,而高层次的引擎则添加了一层管理和用户友好性。

在早期,Docker 是 Kubernetes 后台运行容器的默认选项。但现在 Kubernetes 不再仅限于 Docker;它可以使用多个其他容器运行时,如containerd、CRI-O(与runc一起使用)、Mirantis Container Runtime 等。然而,在本书中,我们将使用containerd或 CRI-O 与 Kubernetes 结合,原因包括以下几点:

  • 专注与灵活性containerd和 CRI-O 专注于容器运行时功能,使它们相比于 Docker 更轻量,且在安全性上可能更具优势。这种专注还使它们能更无缝地与 Kubernetes 等容器编排平台集成。与 Docker 不同,您不需要像cri-dockerd这样的额外组件来确保与 Kubernetes 的兼容性。

  • 与 Kubernetes 的对齐:Kubernetes 正在积极地将默认运行时从 Docker 迁移出去。之前(v1.24 之前),Docker 依赖一个名为dockershim的组件与 Kubernetes 进行集成。

然而,这种方法已经被弃用,Kubernetes 现在鼓励使用符合容器运行时接口CRI)标准的运行时,专门为该平台设计。通过选择containerd或 CRI-O,可以确保与 Kubernetes 环境的更加本地化和高效的集成。

  • 以 Kubernetes 为中心的设计:特别是 CRI-O,作为一个轻量级容器运行时,专为 Kubernetes 设计。它严格遵循 Kubernetes 版本发布周期(如 1.x.y),简化了版本管理。当 Kubernetes 版本达到生命周期结束时,相应的 CRI-O 版本也可能被视为废弃,从而简化了维护安全且最新的 Kubernetes 环境的决策过程。

容器运行时接口

Kubernetes 使用容器运行时在 Pod 内执行容器。默认情况下,Kubernetes 利用 CRI 与选定的容器运行时建立通信。CRI 最早在 Kubernetes 1.5 版本中引入,该版本于 2016 年 12 月发布。

CRI 作为一个插件接口,使得 kubelet 能够与各种容器运行时无缝集成。这种灵活性使得可以根据特定的环境需求选择最优的容器运行时,如 containerd、Docker Engine 或 CRI-O。

在 CRI 内,一组已定义的 API 使得 kubelet 可以高效地与容器运行时进行交互。这些 API 涵盖了基本操作,如创建、启动、停止和删除容器,以及管理 Pod 沙箱和网络。

以下表格展示了适用于 Linux 机器的已知端点。

运行时 Unix 域套接字路径
containerd unix:///var/run/containerd/containerd.sock
CRI-O unix:///var/run/crio/crio.sock
Docker Engine(使用 cri-dockerd unix:///var/run/cri-dockerd.sock

表 2.1:适用于 Linux 机器的已知容器运行时端点

请参考文档(kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm)了解更多信息。

Kubernetes 与 Docker

在 v1.24 版本之前的 Kubernetes 版本中,通过一个名为 dockershim 的组件实现了与 Docker Engine 的直接集成。然而,这种集成已经停止,并且在 v1.20 版本中宣布移除。Docker 作为基础运行时的弃用正在进行中,Kubernetes 现在鼓励使用与 Kubernetes 设计的 CRI 对应的运行时。

尽管发生了这些变化,Docker 生成的镜像仍然会在任何运行时中持续正常工作,确保与之前的兼容性。如需了解更多信息,请参考 kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/

因此,任何运行 containerd 的 Linux 机器都可以用作构建 Kubernetes 工作节点的基础。(我们将在本书后续章节中讨论 Windows 计算节点。)

开放容器倡议

开放容器倡议OCI)是一个开源倡议,定义了容器镜像、容器、容器运行时和容器注册表的标准。这个努力旨在建立跨容器系统的互操作性和兼容性,确保在不同环境中容器的一致执行。此外,CRI 与 OCI 合作,提供了一个标准化的接口,使得kubelet能够与容器运行时进行通信。OCI 定义了由 CRI 支持的容器镜像和运行时的标准,从而促进了 Kubernetes 中容器管理和部署的高效性。

容器运行时 RuntimeClass

Kubernetes RuntimeClass允许你为 Pods 定义并分配不同的容器运行时配置。这使得你可以在应用程序的性能和安全性之间取得平衡。想象一下,使用硬件虚拟化运行时调度高安全性的工作负载,以便提供更强的隔离性,即使这意味着稍微降低性能。RuntimeClass 还允许你为特定的 Pods 使用相同的运行时,但具有不同的设置。要利用这一点,你需要在节点上配置 CRI(安装过程有所不同),并在 Kubernetes 中创建相应的 RuntimeClass 资源。

在下一节中,我们将学习kubelet代理,这是 Kubernetes 集群节点的另一个重要组件。

kubelet

kubelet是计算节点中最重要的组件,因为它是与计算节点上安装的本地容器运行时进行交互的组件。

kubelet仅作为系统守护进程运行,不能在容器内操作。它的执行必须直接在主机系统上进行,通常通过systemd来实现。这使得kubelet与其他 Kubernetes 组件有所区别,突出了它必须在主机机器上运行的独特要求。

kubelet启动时,默认情况下,它会读取位于/etc/kubernetes/kubelet.conf的配置文件。

该配置指定了两个对kubelet工作至关重要的值:

  • kube-apiserver组件的端点

  • 本地容器运行时 UNIX 套接字

一旦计算节点加入集群,kubelet将充当kube-apiserver与本地容器运行时之间的桥梁。kubelet会不断向kube-apiserver发送 HTTP 请求,以获取有关它必须启动的 pods 的信息。

默认情况下,每 20 秒kubelet会对kube-apiserver组件发出一个 GET 请求,以列出在etcd上创建并指向它的 pods。

一旦它从kube-apiserver接收到 HTTP 响应体中的 pod 规格,它就可以将其转换为容器规格,并在指定的 UNIX 套接字上执行。结果是在计算节点上使用本地容器运行时(例如containerd)创建容器。

记住,像其他 Kubernetes 组件一样,kubelet 并不会直接从 etcd 中读取数据;它是通过与 kube-apiserver 交互来暴露 etcd 数据层中的内容。kubelet 甚至不知道它所轮询的 kube-apiserver 后面运行着一个 etcd 服务器。

Kubernetes 中的轮询机制,术语中称为 watch 机制,正是为了定义 Kubernetes 如何在规模化地对工作节点执行容器的运行和删除。这里有两点需要注意:

  • kubeletkube-apiserver 必须能够通过 HTTP 相互通信。这就是为什么计算节点和控制平面节点之间必须开放 HTTPS 端口 6443

  • 由于它们运行在同一台机器上,kubelet、CRI 和容器运行时是通过使用 UNIX 套接字进行接口通信的。

Kubernetes 集群中的每个工作节点都需要自己的 kubelet,导致对 kube-apiserver 的 HTTP 轮询在增加节点后更加频繁。在较大的集群中,尤其是那些有数百台机器的集群,这种增加的活动可能会影响 kube-apiserver 的性能,甚至可能导致影响 API 可用性的情况。高效的扩展非常重要,以确保 kube-apiserver 和其他控制平面组件的高可用性。

另外,请注意,您可以完全绕过 Kubernetes,在工作节点上创建容器而不使用 kubelet,而 kubelet 的唯一工作就是确保其本地容器运行时反映存储在 etcd 中的配置。因此,如果您手动在工作节点上创建容器,kubelet 将无法管理它。然而,将容器运行时套接字暴露给容器化工作负载是一个安全风险。它绕过了 Kubernetes 的安全机制,是攻击者常常瞄准的目标。一项重要的安全实践是防止容器挂载此套接字,从而保护您的 Kubernetes 集群。

请注意,运行在工作节点上的容器引擎并不知道它是通过本地 kubelet 代理由 Kubernetes 管理的。计算节点不过是运行容器运行时的 Linux 机器,旁边安装了 kubelet 代理,执行容器管理指令。

我们将在下一节中学习 kube-proxy 组件。

kube-proxy 组件

Kubernetes 的一个重要部分是网络。我们将在后续深入学习网络;然而,您需要理解,Kubernetes 在暴露 Pod 给外界或暴露 Pod 之间相互通信时有着许多机制。

这些机制在 kube-proxy 级别实现;也就是说,每个工作节点都需要运行一个 kube-proxy 实例,以便可以访问运行在其上的 Pod。我们将探讨一个称为Service的 Kubernetes 特性,该特性在 kube-proxy 组件的级别实现。就像 kubelet 一样,kube-proxy 组件还与kube-apiserver组件通信。

其他几个子组件或扩展在计算节点级别操作,如cAdvisor容器网络接口CNI)。但它们是我们稍后会讨论的高级主题。

现在我们已经了解了不同的 Kubernetes 组件和概念,让我们在下一节学习kubectl客户端实用程序以及它如何与 Kubernetes API 交互。

探索 kubectl 命令行工具和 YAML 语法

kubectl是官方的命令行工具,用于管理 Kubernetes 平台。这是一个完全优化的 HTTP 客户端,可与 Kubernetes 进行交互,并允许您向 Kubernetes 集群发出命令。

基于 Kubernetes 和 Linux 的学习环境

为了在 Linux 容器和相关主题中进行有效学习,最好使用安装有 Linux 操作系统的工作站或实验机器。对 Linux 基础知识的良好理解对于处理容器和 Kubernetes 至关重要。在工作站上使用 Linux 操作系统会自动将您置于 Linux 环境中,从而提升学习体验。您可以选择自己喜欢的 Linux 发行版,如 Fedora、Ubuntu 或其他发行版。我们致力于包容性,并将在必要时为 Windows 和 macOS 用户提供替代步骤,确保每个人都能获得多样化且无障碍的学习体验。但是,并不需要在安装有 Linux 操作系统的工作站上学习 Kubernetes。如果您使用的是 Windows 机器,则可以使用Windows 子系统用于 LinuxWSL)(learn.microsoft.com/en-us/windows/wsl/)等替代方案。

安装 kubectl 命令行工具

kubectl命令行工具可以安装在您的 Linux、Windows 或 macOS 工作站上。您需要确保您的kubectl客户端版本与您的 Kubernetes 集群的次要版本兼容,以获得最佳兼容性。这意味着 v1.30 的kubectl可以管理 v1.29、v1.30 和 v1.31 的集群。坚持使用最新的兼容版本有助于避免潜在问题。

由于您将在接下来的章节中需要 kubectl 实用程序,请立即按照以下部分的说明安装它。

Kubernetes 遗留包存储库

截至 2024 年 1 月,遗留的 Linux 包仓库——即 apt.kubernetes.ioyum.kubernetes.io(也称为 packages.cloud.google.com)——自 2023 年 9 月 13 日起被冻结,现已不可用。建议用户迁移到新的社区维护的 Debian 和 RPM 包仓库 pkgs.k8s.io,该仓库于 2023 年 8 月 15 日推出。这些仓库取代了现在已废弃的 Google 托管的仓库(apt.kubernetes.ioyum.kubernetes.io)。此变更直接影响那些安装上游版本 Kubernetes 的用户,以及通过遗留包仓库安装 kubectl 的用户。详细信息请参见官方公告:遗留包仓库弃用(kubernetes.io/blog/2023/08/31/legacy-package-repository-deprecation/)。

在 Linux 上安装 kubectl

在 Linux 上安装 kubectl 时,你需要下载 kubectl 工具并将其复制到可执行路径,具体操作如下:

$ curl -LO “https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl”
$ chmod +x ./kubectl
$ sudo mv ./kubectl /usr/local/bin/kubectl
$ kubectl version
Client Version: v1.30.0
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
The connection to the server localhost:8080 was refused - did you specify the right host or port? 

忽略此处的连接错误,因为你没有配置 Kubernetes 集群供 kubectl 访问。

路径 /usr/local/bin/kubectl 在你的环境中可能不同。你需要确保配置了合适的 PATH 变量,确保 kubectl 工具处于可检测路径下。你也可以使用 /etc/profile 来配置 kubectl 工具路径。

要下载特定版本,可以将命令中的$(curl -L -s https://dl.k8s.io/release/stable.txt)部分替换为所需版本。

例如,如果你希望在 Linux x86-64 上下载版本 1.28.4,请输入:

$ curl -LO https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl 

此命令将下载特定版本的 kubectl 工具,你可以将其复制到你希望的路径。

现在让我们学习如何在 macOS 上安装 kubectl 工具。

在 macOS 上安装 kubectl

安装过程与 macOS 上相似,唯一不同的是针对 Intel 和 Apple 版本的 kubectl 包:

# Intel
$ curl -LO “https://dl.k8s.io/release/$(curl -L -s
 https://dl.k8s.io/release/stable.txt)/bin/darwin/amd64/kubectl”
# Apple Silicon
$ curl -LO “https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/darwin/arm64/kubectl”
$ chmod +x ./kubectl
$ sudo mv ./kubectl /usr/local/bin/kubectl
$ sudo chown root: /usr/local/bin/kubectl 

在 Windows 上安装 kubectl

使用浏览器或 curl 下载 kubectl.exedl.k8s.io/release/v1.28.4/bin/windows/amd64/kubectl.exe)(如果你在 Windows 上安装了 curl 或等效的命令工具):

$ curl.exe -LO https://dl.k8s.io/release/v1.28.4/bin/windows/amd64/kubectl.exe 

最后,将 kubectl 二进制文件夹添加到或前置到你的 PATH 环境变量中,并进行测试,确保 kubectl 的版本与下载的版本匹配。

你也可以使用本地包管理器安装 kubectl,如 apt-get、yum、Zypper、brew(macOS)或 Chocolatey(Windows)。参考文档(kubernetes.io/docs/tasks/tools/install-kubectl-linux)了解更多信息。

我们将在下一节中学习如何使用 kubectl 命令。

kubectl 的作用

由于 kube-apiserver 本质上只是一个 HTTP API,任何 HTTP 客户端都可以用来与 Kubernetes 集群进行交互。你甚至可以使用 curl 来管理 Kubernetes 集群,但当然,有更好的方式来做到这一点。

那么,为什么你要使用这样的客户端而不是直接使用 curl 命令呢?原因就是简便性。实际上,kube-apiserver 管理着许多不同的资源,每个资源都有其独立的 URL 路径。

通过 curl 不断调用 kube-apiserver 是可行的,但极其耗时。这是因为记住每个资源的路径以及如何调用它并不友好。实际上,curl 并不是最佳选择,因为 kubectl 还管理与 Kubernetes 认证层的身份验证、集群上下文等多个方面。

你需要不断查阅文档来记住 URL 路径、HTTP 头或查询字符串。kubectl 会通过让你调用 kube-apiserver 来为你处理这些,它的命令易于记忆、安全,并且专门用于 Kubernetes 管理。

当你调用 kubectl 时,它会读取你传递给它的参数,并基于这些参数创建并发送 HTTP 请求给 Kubernetes 集群中的 kube-apiserver 组件。

图 2.7:kubectl 命令行将通过 HTTP 协议调用 kube-apiserver;你将通过 kubectl 与 Kubernetes 集群进行交互。

一旦 kube-apiserver 组件接收到来自你的有效 HTTP 请求,它将根据你提交的请求读取或更新 etcd 中集群的状态。如果是写操作——例如,更新正在运行的容器的镜像——kube-apiserver 将在 etcd 中更新集群的状态。然后,托管该容器的工作节点上的组件将发出适当的容器管理命令,以根据新的镜像启动一个新的容器。这是为了确保容器的实际状态与 etcd 中的状态一致。

由于你不需要直接与容器引擎或 etcd 进行交互,我们可以说,掌握 Kubernetes 很大程度上依赖于你对 kubectl 命令的了解。要有效使用 Kubernetes,你必须尽可能掌握 Kubernetes API 和相关细节。你只需要与 kube-apiserver 和允许你调用它的 kubectl 命令行工具进行交互,其他组件则无需直接操作。

由于 kube-apiserver 组件可以通过 HTTP(S) 协议访问,你可以使用任何基于 HTTP 的库或通过你喜欢的编程语言与 Kubernetes 集群进行交互。虽然有许多替代 kubectl 的工具,但作为 Kubernetes 项目的官方工具,kubectl 一直是文档中的主要示例。你遇到的大多数示例都会使用 kubectl

kubectl 是如何工作的?

当你调用kubectl命令时,它将尝试从默认位置$HOME/.kube/config读取名为kubeconfig的配置文件。kubeconfig文件应包含以下信息,以便kubectl可以使用它并对kube-apiserver进行身份验证:

  • kube-apiserver端点的 URL 和端口

  • 用户账户

  • 用于验证kube-apiserver的客户端证书(如果有的话)

  • 用户与集群的映射,也就是上下文

也可以将详细信息(例如集群信息、用户认证信息等)作为参数传递给kubectl命令,但当你有多个集群需要管理时,这种方法并不方便。

下图展示了一个典型的kubeconfig文件及其详细信息。

图 2.8:kubeconfig 上下文和结构

在上面的图示中,配置了多个集群、用户和上下文:

  • 集群:该部分定义了你可以与之交互的 Kubernetes 集群。它包含了每个集群的服务器地址、API 版本、证书授权信息等,允许kubectl连接并向它们发送命令。

  • 用户:该部分存储你访问 Kubernetes 集群的凭证。通常包括用户名和用于与 API 服务器进行身份验证的密钥(如令牌或客户端证书)。kubeconfig文件还可以在用户部分引用证书,以便安全地验证用户与 Kubernetes API 服务器的身份。这种双向验证确保只有具有有效证书的授权用户才能访问集群,从而防止未经授权的访问和潜在的安全漏洞。

  • 上下文:该部分充当集群与用户之间的桥梁。每个上下文引用一个特定的集群和该集群中的特定用户。通过选择一个上下文,你可以定义kubectl在后续命令中使用哪个集群和用户凭证。

kubeconfig文件中配置了多个集群、用户和上下文后,切换到不同的 Kubernetes 集群并使用不同的用户凭证变得很容易。

可以通过设置名为KUBECONFIG的环境变量,或在调用kubectl时使用--kubeconfig参数,来覆盖系统中的kubeconfig路径:

$ export KUBECONFIG=”/custom/path/.kube/config”
$ kubectl --kubeconfig=”/custom/path/.kube/config” 

每次运行kubectl命令时,kubectl命令行工具会按照以下顺序查找kubeconfig文件以加载其配置:

  1. 首先,它检查是否传递了--kubeconfig参数,并加载配置文件。

  2. 如果在此时没有找到kubeconfig文件,kubectl会查找KUBECONFIG环境变量。

  3. 最终,它会回退到$HOME/.kube/config中的默认位置。

要查看本地kubectl安装当前使用的配置文件,你可以运行以下命令:

$ kubectl config view 

然后,HTTP 请求被发送到kube-apiserver,该组件生成 HTTP 响应,kubectl将其重新格式化为人类可读的格式并输出到终端。

以下命令可能是你在使用 Kubernetes 时几乎每天都会输入的命令:

$ kubectl get pods 

该命令列出了Pods。本质上,它会向kube-apiserver发出GET请求,以检索集群中容器(Pod)的列表。在内部,kubectl将命令中传递的Pods参数与/api/v1/podsURL 路径关联,这是kube-apiserver用来公开 Pod 资源的路径。

这是另一个命令:

$ kubectl run nginx --restart Never --image nginx 

这个命令稍微复杂一点,因为run不是一个 HTTP 方法。此命令会向kube-apiserver组件发出一个POST请求,最终会创建一个名为nginx的容器,该容器基于容器注册表中托管的nginx镜像(例如,Docker Hub 或 quay.io)。

事实上,这个命令不会创建一个容器,而是创建一个 Pod。我们将在第四章在 Kubernetes 中运行你的容器中详细讨论 Pod 资源。让我们尽量不再谈论容器,而是转向 Pod,并熟悉 Kubernetes 的概念和术语。从现在开始,如果你看到容器一词,它指的是从容器角度来看一个真正的容器。此外,Pod 指的是 Kubernetes 资源。

我们将在下一节学习如何启用kubectl的补全功能。

kubectl 自动补全

kubectl为各种 Shell 提供了内置的自动补全功能,节省了你宝贵的时间和避免了沮丧。kubectl支持常用 Shell 的自动补全,如:

  • Bash

  • Zsh

  • Fish

  • PowerShell

以 Linux Bash 为例,启用自动补全的方法如下:

# Install Bash Completion (if needed)
$ sudo yum install bash-completion  # For RPM-based systems
$ sudo apt install bash-completion  # For Debian/Ubuntu-based systems
# Add the source line to your shell configuration
$ echo ‘source <(kubectl completion bash)’ >>~/.bashrc 

现在,当你开始输入kubectl命令时,魔法就会发生!kubectl将根据可用的资源和选项建议补全。只需按Tab键接受建议,或者继续输入以缩小选项范围。

启用自动补全的过程可能会因其他 Shell(如 Zsh 或 Fish)而略有不同。请参阅官方kubectl文档以获取具体说明:kubernetes.io/docs/reference/kubectl/generated/kubectl_completion/

这个设置确保每次你打开新的终端会话时,自动补全都能正常工作。

在下一节中,我们将从kubectl命令开始,介绍如何使用命令式和声明式语法。

命令式语法

你发送给kube-apiserver的几乎每一条指令都可以使用两种语法编写:命令式声明式。命令式语法侧重于发出直接修改集群状态的命令,这些命令基于你传递给kubectl命令的参数和选项。

让我们来看一些命令式风格的操作,具体如下:

# Creates a pod, called my-pod, based on the busybox:latest container image:
$ kubectl run my-pod --restart Never --image busybox:latest
#  list all the ReplicaSet resources in the my-namespace namespace created on the Kubernetes cluster:
$ kubectl get rs -n my-namespace
# Delete a pod, called my-pod, in the default namespace:
$ kubectl delete pods my-pod 

命令式语法有很多优点。如果你已经理解了要向 Kubernetes 发送什么样的指令以及如何正确地执行这些指令,那么你将非常迅速。命令式语法容易输入,而且你可以用少数几个命令做很多事。有些操作只有在命令式语法下才能进行。例如,列出集群中的现有资源,只有通过命令式语法才能实现。

然而,命令式语法有一个大问题。如果你需要记住之前在集群中做的操作记录,这将变得非常复杂。如果由于某些原因,你丢失了集群的状态并且需要从头开始重建,那么你会发现很难记得之前输入的所有命令,来使集群恢复到你想要的状态。你可以查看 .bash_history 文件,但显然还有更好的方法,我们将在下一节学习声明式方法。

声明式语法

“声明式”正如其名所示。我们“声明”我们希望集群的状态,然后 Kubernetes 创建所需的资源来达到该状态。JSON 和 YAML 格式都被支持;然而,根据约定,Kubernetes 用户更喜欢 YAML 语法,因为它简单易懂。

YAML(“YAML Ain’t Markup Language” 或 “Yet Another Markup Language”)是一种广泛用于 Kubernetes 的人类可读的数据序列化格式。它允许你以清晰简洁的方式定义 Kubernetes 资源的配置,如 Deployments、Services 和 Pods。该格式使得管理和版本控制 Kubernetes 配置变得简单,促进了协作和可重复性。同时需要注意,YAML 不是一种编程语言,它背后没有真正的逻辑。它仅仅是一种 key:value 配置语法,许多项目现在都在使用这种语法,Kubernetes 就是其中之一。

每个 key:value 对表示你想要设置到 Kubernetes 资源的配置数据。

以下是使用之前我们使用过的 busybox:latest 容器镜像创建名为 my-pod 的 pod 的命令:

$ kubectl run my-pod --restart Never --image busybox:latest 

现在我们将使用声明式语法做相同的操作:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  -  name: busybox-container
     image: busybox:latest 

假设这个文件被保存为 pod.yaml。要创建实际的 pod,你需要运行以下命令:

$ kubectl create -f pod.yaml 

这个结果将等同于之前的命令。

每个为 Kubernetes 创建的 YAML 文件必须包含四个必需的键:

  • apiVersion:此字段告诉你资源声明的 API 版本。每种资源类型都有一个 apiVersion 键,必须在此字段中设置。pod 资源类型使用 API 版本 v1

  • kind:此字段表示 YAML 文件将创建的资源类型。在这里,它将创建一个pod

  • metadata:此字段告诉 Kubernetes 关于实际资源的名称。在这里,pod 的名称是 my-pod。此字段描述的是 Kubernetes 资源,而不是容器资源。该元数据是为 Kubernetes 提供的,而不是为像 Docker Engine 或 Podman 这样的容器引擎提供的。

  • spec:此字段告诉 Kubernetes 对象的组成。在前面的示例中,pod 由一个容器组成,该容器将基于 busybox:latest 容器镜像命名为 busybox-container。这些是将在后台容器运行时创建的容器。

声明式语法的另一个重要方面是,它允许你在同一个文件中声明多个资源,并使用三个破折号作为资源之间的分隔符。下面是修订版的 YAML 文件,它将创建两个 pods:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  -  name: busybox-container
     image: busybox:latest
---
apiVersion: v1
kind: Pod
metadata:
  name: my-second-pod
spec:
  containers:
  - name: nginx-container
    image: nginx:latest 

你应该能够自己阅读这个文件并理解它;它仅创建了两个 pods。第一个使用 busybox 镜像,第二个使用 nginx 镜像。

当然,你不必记住所有语法以及为每个键设置什么值。你可以随时参考官方的 Kubernetes 文档,获取示例声明的 YAML 文件。如果文档不足或没有解释某些细节,你可以使用 kubectl explain 命令来理解资源的详细信息,如下所示:

$ kubectl explain pod.spec.containers.image
KIND:       Pod
VERSION:    v1
FIELD: image <string>
DESCRIPTION:
    Container image name. More info:
    https://kubernetes.io/docs/concepts/containers/images 

你将从 kubectlexplain 输出中获得非常清晰的解释和字段信息。

声明式语法也带来了许多好处。使用它,你的速度可能会变慢,因为编写这些 YAML 文件比直接以命令式方式执行命令要耗时得多。然而,它有两个主要好处:

  • 基础设施即代码(IaC)管理:你可以将配置存储在某个地方,并使用 Git(源代码管理)对 Kubernetes 资源进行版本控制,就像处理 IaC 一样。如果你丢失了集群的状态,保持 YAML 文件在 Git 中的版本化将使你能够清洁有效地重新创建它。

  • 同时创建多个资源:由于你可以在同一个 YAML 文件中声明多个资源,因此可以将整个应用及其所有依赖项放在同一个地方。此外,你可以通过一个命令创建并重新创建复杂的应用程序。稍后,你将发现一个叫做 Helm 的工具,它可以在 Kubernetes YAML 文件的基础上实现模板化。

没有更好的方式来使用 kubectl;这些只是与其交互的两种方式,你需要掌握这两种方式。因为某些功能在命令式语法中不可用,而另一些功能在声明式语法中不可用。请记住,最终,两者都通过 HTTP 协议调用 kube-apiserver 组件。

kubectl 应该安装在任何需要与集群交互的机器上。

从技术角度来看,每当你想与 Kubernetes 集群进行交互时,必须安装并配置 kubectl 命令行工具。

当然,这台机器可以是你的本地机器或你访问 Kubernetes 集群的服务器。不过,在较大的项目中,将 kubectl 安装到持续集成平台的代理/运行器中也是一个好主意。

实际上,你可能会希望自动化维护或部署任务,以便对 Kubernetes 集群进行操作,并且你可能会使用 持续集成 (CI) 平台,如 GitLab CI、Tekton 或 Jenkins 来完成这些任务。

如果你希望能够在 CI 管道中运行 Kubernetes 命令,你需要在 CI 代理上安装 kubectl,并且在 CI 代理的文件系统中写入正确配置的 kubeconfig 文件。这样,你的 CI/CD 管道就能够向 Kubernetes 集群发出命令,并更新集群的状态。

需要补充的是,kubectl 不应仅仅被视为人类用户使用的 Kubernetes 客户端。它应被视为与 Kubernetes 通信的通用工具:将其安装在你希望与集群通信的任何地方。

如何使 Kubernetes 高可用

正如你之前观察到的,Kubernetes 是一种集群解决方案。它的分布式特性使得它能够在多台机器上运行。通过将不同的组件分配到不同的机器上,你可以使 Kubernetes 集群具备高可用性。接下来,我们将简要讨论不同的 Kubernetes 配置。

单节点集群

如果你想在生产环境中部署 Kubernetes,将所有 Kubernetes 组件安装在同一台机器上是最糟糕的选择。然而,对于测试开发来说,这是完全可以接受的。单节点方式是将所有不同的 Kubernetes 组件聚集在同一主机或虚拟机上的方式:

图 2.9:所有组件都运行在同一台机器上

通常,这种配置被认为是通过本地测试入门 Kubernetes 的坚实起点。有一个名为 minikube 的工具,使你可以轻松地在计算机上设置单节点 Kubernetes。它运行一个虚拟机,并且所有必要的组件已经配置好。虽然 minikube 对于本地测试非常方便,并且运行 minikube 作为多节点集群也是可能的,但请记住,minikube 绝对不推荐用于生产环境。下表提供了一些使用单节点 Kubernetes 集群的优缺点。

优点 缺点
适合测试 无法扩展
本地设置简单 不具备高可用性
minikube 原生支持 不推荐用于生产环境

表 2.2:单节点 Kubernetes 集群的优缺点

单节点 Kubernetes 是一个非常适合资源受限边缘环境的选项。它提供了轻量级的占用空间,同时仍然支持具有灾难恢复策略的强大部署。

单主节点集群

这种配置包含了一个执行所有控制平面组件的节点,以及你想要的多个计算节点:

图 2.10:单一控制平面节点管理所有计算节点(这里是三个)

这种配置相比于单节点集群已经很好,且由于有多个计算节点,将能为容器化应用提供高可用性。然而,仍然有改进的空间:

  • 存在单点故障,因为只有一个控制平面节点。如果这个节点发生故障,你将无法以 Kubernetes 的方式管理你的运行容器。你的容器将成为孤儿,唯一的停止/更新方法是通过 SSH 进入工作节点,执行传统的容器管理命令(例如 ctrcrictl 或 Docker 命令,取决于你使用的容器运行时)。

  • 此外,这里有一个重大问题:通过使用单个 etcd 实例,如果控制平面节点损坏,将会有巨大的风险丢失你的数据集。如果发生这种情况,你的集群将无法恢复。

  • 最后,如果你开始扩展工作节点,你的集群将会遇到一个问题。每个计算节点都会带有自己的 kubelet 代理,且 kubelet 每 20 秒会周期性地向 kube-apiserver 发送请求。如果你开始添加几十台服务器,可能会影响 kube-apiserver 的可用性,导致控制平面宕机。记住,控制平面必须能够扩展并处理这样的流量。

优点 缺点
拥有高可用计算节点 控制平面是单点故障
支持多节点功能 运行单个 etcd 实例
可以与 kind 或 minikube 等项目一起在本地运行,但并不完美 无法有效扩展

表 2.3:单控制器多计算节点 Kubernetes 集群的优缺点

总体来说,这种配置总是优于单节点 Kubernetes;然而,它仍然不是高可用的。

多主节点多计算节点集群

这是实现高可用 Kubernetes 集群的最佳方式。你的运行容器和控制平面都会被复制,以避免单点故障。

图 2.11:多控制平面节点 Kubernetes 集群

通过使用这样的集群,你消除了我们在早期集群架构中学到的许多风险,因为你正在运行多个计算节点和控制平面节点的实例。你需要在 kube-apiserver 实例上使用负载均衡器,以便在它们之间均匀分配负载,这需要稍微更多的规划。像 Amazon EKS 或 Google GKE 这样的云服务提供商正在提供多控制器和多计算节点的 Kubernetes 集群。如果你希望进一步提升,可以将所有不同的控制平面组件拆分到专用主机上。这样更好,但不是强制的。前面图示的集群完全可以使用。

在 Kubernetes 中管理具有多个控制平面节点的 etcd

在一个多控制平面集群中,每个控制平面节点都运行一个 etcd 实例。这确保了即使某些控制平面节点不可用,集群仍然具有高可用性的 etcd 存储。在一个多控制平面 Kubernetes 集群中,etcd 实例将在内部形成一个 etcd 集群。这意味着 etcd 实例会相互通信,以复制集群状态,并确保所有实例具有相同的数据。etcd 集群将使用一种共识算法,称为 Raft,以确保始终只有一个领导者。领导者负责接受对集群状态的写操作,并将更改复制到其他实例。如果领导者变得不可用,其他实例将选举一个新的领导者。

在本书的后续章节中,我们将学习 etcd 成员管理和 etcd 备份/恢复机制。

在我们结束本章之前,我们想总结一下所有 Kubernetes 组件。以下的表格将帮助你记住它们的所有职责:

组件名称 通信对象 角色
kube-apiserver kubectl clients, etcd, kube-scheduler, kube-controller-manager, kubelet, kube-proxy HTTP REST API。它读取和写入存储在 etcd 中的状态。是唯一能够直接与 etcd 通信的组件。
etcd kube-apiserver 这个存储了 Kubernetes 集群的状态。
kube-scheduler kube-apiserver 它每 20 秒读取一次 API,列出未调度的 pod(一个空的 nodeName 属性),选择一个工作节点,并通过调用 kube-apiserver 更新 pod 条目的 nodeName 属性。
kube-controller-manager kube-apiserver 它轮询 API 并运行协调循环。
kubelet kube-apiserver 和容器运行时 它每 20 秒读取一次 API,获取调度到其运行节点的 pods,并通过调用本地容器运行时操作将 pod 规范转换为正在运行的容器。
kube-proxy kube-apiserver 它实现了 Kubernetes 的网络层。
容器引擎 kubelet 通过接收来自本地 kubelet 的指令来运行容器。

表 2.4:Kubernetes 组件及其连接性

这些组件是默认组件,且作为 Kubernetes 项目的一部分被官方支持。请记住,其他 Kubernetes 发行版可能会带来额外的组件,或改变这些组件的行为。

这些组件是你需要的最基本组件,用于构建一个可工作的 Kubernetes 集群。

总结

这一章内容相当庞大,但至少你现在已经列出了所有 Kubernetes 组件。我们接下来的所有操作都会与这些组件相关,它们是 Kubernetes 的核心。虽然这一章充满了技术细节,但依然偏向理论。如果你还感到不太清楚,不用担心,通过实践你将更好地理解。

好消息是,你现在已经完全准备好在本地安装你的第一个 Kubernetes 集群,从现在开始,一切将变得更加实用。这是下一步,我们将在下一章进行。在下一章之后,你将在本地的工作站上运行一个 Kubernetes 集群,并且你将准备好使用 Kubernetes 运行你的第一个 Pods!

进一步阅读

若要了解更多本章中涉及的主题,请查看以下资源:

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第三章:安装你的第一个 Kubernetes 集群

在上一章中,我们有机会解释了 Kubernetes 是什么,它的分布式架构,一个正常工作的集群的构成,以及它如何在多台 Linux 机器上管理 Docker 容器。现在,我们要亲自动手了,因为是时候安装 Kubernetes 了。本章的主要目标是为接下来的章节安装一个可用的 Kubernetes 集群。这样,你就可以拥有自己的集群进行实践、操作和学习,边读这本书边实践。

安装 Kubernetes 意味着你需要让不同的组件协同工作。当然,我们不会采用手动设置单个集群组件的繁琐方法;相反,我们将使用自动化工具。这些工具的好处在于它们可以本地启动并配置所有组件。这个自动化的 Kubernetes 集群设置对于 DevOps 团队快速测试 YAML 更改、开发人员希望拥有一个本地环境来测试应用程序,以及安全团队快速测试 Kubernetes 对象 YAML 定义的更改尤其有益。

如果你不想在本地计算机上搭建 Kubernetes 集群,我们还将在本书的后续章节中设置简化但功能完整的生产级 Kubernetes 集群,分别使用Google Kubernetes EngineGKE)、Amazon Elastic Kubernetes ServiceEKS)和Azure Kubernetes ServiceAKS)。这些都是基于云的、生产就绪的解决方案。这样,你可以在一个实际的 Kubernetes 集群上进行实践和学习,该集群托管在云端。

无论你是选择本地搭建还是使用云端服务,这由你来决定。你需要通过考虑每种解决方案的优缺点来选择最适合你的方式。然而,在这两种情况下,你都需要在本地工作站上安装一个可用的kubectl,以便与最终的 Kubernetes 集群进行通信。关于kubectl的安装说明可以在上一章中找到,第二章Kubernetes 架构 – 从容器镜像到运行的 Pod

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

  • 使用minikube安装 Kubernetes 集群

  • 使用kind的多节点 Kubernetes 集群

  • 替代的 Kubernetes 学习环境

  • 生产级 Kubernetes 集群

技术要求

为了跟随本章的示例,你将需要以下内容:

  • 在本地计算机上安装kubectl

  • 一台配备至少 2 个 CPU、2GB 可用内存和 20GB 可用磁盘空间的工作站。(如果你想探索多节点集群环境,你将需要更多的资源。)

  • 在工作站上安装的容器或虚拟机管理器,如 Docker、QEMU、Hyperkit、Hyper-V、KVM、Parallels、Podman、VirtualBox 或 VMware Fusion/Workstation

  • 可靠的互联网连接

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter03

使用 minikube 安装 Kubernetes 集群

在本节中,我们将学习如何使用 minikube 安装本地 Kubernetes 集群。这可能是获得本地工作 Kubernetes 安装的最简单方法。在本节结束时,你将能够在本地机器上安装一个工作中的单节点 Kubernetes 集群。

minikube 使用起来很简单,且完全免费。它会在本地机器上安装所有 Kubernetes 组件并进行配置。通过 minikube 卸载所有组件也很容易,因此如果有一天你想销毁本地集群,也不会被困住。

与完整的生产集群部署方法相比,minikube 有一个很大的优势:它是一个非常有用的工具,可以快速测试 Kubernetes 场景。如果你不希望使用 minikube,可以完全跳过这一部分,选择本章中描述的其他方法。

虽然 minikube 是本地 Kubernetes 开发的热门选择,但与完整的生产集群相比,它在资源使用和功能一致性上有一些权衡:

  • 资源压力:在本地机器上运行 minikube 与其他进程一起运行时可能会占用大量资源。当你想创建更大的 Kubernetes 集群时,它需要较多的 CPU 和内存,这可能会影响其他应用程序的性能。

  • 网络差异:与生产环境中的 Kubernetes 集群不同,minikube 的默认网络设置可能无法完全模拟现实世界中的网络环境。这可能会在复制或排除生产中可能发生的网络相关问题时带来挑战。

  • 兼容性考虑:某些 Kubernetes 特性或第三方工具可能需要比 minikube 提供的更完整的 Kubernetes 设置,这可能导致开发过程中的兼容性问题。

  • 持久存储挑战:由于 minikube 在持久卷支持方面的限制,管理应用程序的持久存储可能会很麻烦,相较于完整的 Kubernetes 集群。

我们将在下一节学习如何安装 minikube 并部署和开发 Kubernetes 集群。

安装 minikube

在这里,我们将看到如何在 Linux、macOS 和 Windows 上安装 minikube 工具。使用二进制文件或包管理器方法安装 minikube 是一项简单的任务,如下文所述。

你可以使用本地包管理器,如 apt-getyum、Zypper、Homebrew(macOS)或 Chocolatey(Windows)来安装 minikube。请参考文档(minikube.sigs.k8s.io/docs/start)了解更多信息。

在 Linux 上安装 minikube

在 Linux 上,minikube可以通过 Debian 包、RPM 包或二进制文件安装,如下所述:

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Verify minikube command and path
$ which minikube
/usr/local/bin/minikube 

请注意,路径在你的工作站上可能会有所不同,这取决于操作系统。你需要确保路径已包含在PATH环境变量中,以便minikube命令能正常工作。

在 macOS 上安装 minikube

在 macOS 上,minikube可以通过二进制文件安装,如下所述:

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64
$ sudo install minikube-darwin-amd64 /usr/local/bin/minikube
# Verify minikube command and path
$ which minikube 

也可以通过包管理器 Homebrew 在 macOS 上安装minikube

在 Windows 上安装 minikube

与 macOS 和 Linux 一样,也可以通过多种方式在 Windows 上安装minikube,如下所示:

# Using Windows Package Manager (if installed)
$ winget install minikube
# Using Chocolatey
$ choco install minikube
# Via .exe download and setting the PATH
# 1\. Download minikube: https://storage.googleapis.com/minikube/releases/latest/minikube-installer.exe
# 2\. Set PATH 

一旦你配置了minikube,你就可以使用minikube创建不同类型的 Kubernetes 集群,具体说明见接下来的章节。

minikube 配置

minikube工具提供了适用于开发环境的最基本但有效的定制功能。

例如,通过minikube创建的 Kubernetes 集群的默认配置为 2 个 CPU 和 2GB 内存。如果你需要更大的 Kubernetes 集群节点,可以使用以下命令调整这个值:

$ minikube config set cpus 4
❗  These changes will take effect upon a minikube delete and then a minikube start
$ minikube config set memory 16000
❗  These changes will take effect upon a minikube delete and then a minikube start
$ minikube config set container-runtime containerd
❗  These changes will take effect upon a minikube delete and then a minikube start 

如你在屏幕上看到的,你需要删除并重新创建minikube集群以应用设置。

minikube 的驱动程序

minikube作为一种简单而轻量的方式,用于在你的开发机器上运行本地 Kubernetes 集群。为了实现这一点,它利用驱动程序——这些是管理集群生命周期的核心组件。这些驱动程序与不同的虚拟化和容器化技术进行交互,使minikube能够创建、配置并控制本地 Kubernetes 环境的底层基础设施。minikube的驱动程序灵活性使你能够根据特定的需求和偏好,将集群部署为虚拟机、容器,甚至直接部署到开发机器的裸机上,从而量身定制设置:

  • 容器驱动程序:对于容器化的方式,minikube可以利用本地的 Podman 或 Docker 安装。这允许你在开发机器的容器中直接运行minikube,从而可能提供更轻量和更高效的资源配置。

  • 虚拟机(VM)驱动程序:如果你更倾向于虚拟机的方式,minikube可以在你的机器上启动虚拟机。这些虚拟机将容纳并封装所需的 Kubernetes 组件,为你的本地集群提供一个更为隔离的环境。

选择容器驱动程序还是虚拟机驱动程序取决于你的具体需求和偏好,以及你的开发环境的能力。

参考minikube驱动程序文档(minikube.sigs.k8s.io/docs/drivers/)了解可用和支持的minikube驱动程序及支持的操作系统。

你也可以使用以下命令设置minikube的默认驱动程序:

$  minikube config set driver docker
❗  These changes will take effect upon a minikube delete and then a minikube start
# or set the VirtualBox as driver
$  minikube config set driver virtualbox
❗  These changes will take effect upon a minikube delete and then a minikube start$  minikube config view driver
- driver: docker 

此外,可以在创建minikube集群时设置驱动程序,方法如下:

$ minikube start --driver=docker 

先决条件取决于各个minikube驱动程序,必须先安装并准备好。这些可能包括安装 Docker、Podman 或 VirtualBox,并在特定操作系统上授予权限。安装和配置说明可以在minikube驱动程序特定的文档中找到(minikube.sigs.k8s.io/docs/drivers)。

让我们在下一部分学习如何使用minikube启动我们的第一个 Kubernetes 集群。

使用 minikube 启动单节点 Kubernetes 集群

minikube的主要目的是在本地系统上启动 Kubernetes 组件,并使它们彼此通信。在接下来的部分中,我们将学习如何使用 VirtualBox 驱动程序和 Docker 部署minikube集群。

使用虚拟机设置 minikube

VM 方法要求您在工作站上安装虚拟机管理程序,如下所示:

  • Linux: KVM2(推荐),VirtualBox,QEMU

  • Windows: Hyper-V(推荐),VirtualBox,VMware Workstation,QEMU

  • macOS: Hyperkit, VirtualBox, Parallels, VMware Fusion, QEMU

然后,minikube将把所有 Kubernetes 组件封装成一个虚拟机,并启动该虚拟机。

在以下示例中,我们使用 Fedora 39 作为工作站,并将 VirtualBox 作为我们的虚拟机管理程序软件,因为它适用于 Linux、macOS 和 Windows。

请参考www.virtualbox.org/wiki/Downloads下载并安装适用于您工作站的 VirtualBox。您可以自由选择您喜欢的虚拟化软件,并始终按照文档(minikube.sigs.k8s.io/docs/drivers/)查看支持的虚拟化软件。

不要混淆minikube版本和已部署的 Kubernetes 版本。例如,minikube 1.32使用 Kubernetes 1.28,出于稳定性和兼容性的考虑。这可以进行彻底的测试,提供更广泛的工具支持,控制版本发布,并对旧版本提供长期支持。用户仍然可以独立运行不同版本的 Kubernetes。这种稳定性与灵活性之间的平衡使得minikube成为开发人员可靠且多功能的平台。

在您已经安装了minikube和 VirtualBox 的工作站上,执行以下命令:

$  minikube start --driver=virtualbox --memory=8000m --cpus=2 

如果您使用的是特定版本的minikube,但想要安装不同版本的 Kubernetes,则可以指定特定版本,如下所示:

$ minikube start --driver=virtualbox --memory=8000m --cpus=2 --kubernetes-version=1.29.0 

您将看到minikube正在启动 Kubernetes 部署过程,包括 VM 镜像的下载,如下所示:

😄  minikube v1.32.0 on Fedora 39
❗  Specified Kubernetes version 1.29.0 is newer than the newest supported version: v1.28.3\. Use `minikube config defaults kubernetes-version` for details.
❗  Specified Kubernetes version 1.29.0 not found in Kubernetes version list
🤔  Searching the internet for Kubernetes version...
✅  Kubernetes version 1.29.0 found in GitHub version list
✨  Using the virtualbox driver based on user configuration
👍  Starting control plane node minikube in cluster minikube
🔥  Creating virtualbox VM (CPUs=2, Memory=8000MB, Disk=20000MB) ...
🐳  Preparing Kubernetes v1.29.0 on Docker 24.0.7 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5 

根据您工作站的操作系统和虚拟化软件,您还可能会看到以下信息,作为建议:

│    You have selected "virtualbox" driver, but there are better options!                          │    For better performance and support consider using a different driver:
│            - kvm2                                                                                 │            - qemu2                                                                                │
│    To turn off this warning run:
│            $ minikube config set WantVirtualBoxDriverWarning false                                │    To learn more about on minikube drivers checkout https://minikube.sigs.k8s.io/docs/drivers/    │
│    To see benchmarks checkout https://minikube.sigs.k8s.io/docs/benchmarks/cpuusage/              │ 

最后,您将看到minikube显示以下成功消息:

🔎  Verifying Kubernetes components...
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default 

是的,你已经在一分钟内部署了一个完全工作的 Kubernetes 集群,并准备好部署你的应用。

现在使用 minikube 命令验证 Kubernetes 集群的状态,如下所示:

$  minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured 

你还可以在 VirtualBox UI 中看到新的 minikube 虚拟机,如下所示:

图 3.1:VirtualBox UI 中的 minikube 虚拟机

在接下来的部分中,我们将学习如何使用 minikube 和容器部署 Kubernetes 集群。

使用容器设置 minikube

容器方法更简单。minikube 不使用虚拟机,而是使用本地的 Docker 引擎实例或 Podman 启动一个大容器内的 Kubernetes 组件。要使用基于容器的 minikube,确保按照你所安装 minikube 的工作站操作系统的说明安装 Docker 或 Podman;minikube 不会为你安装 Podman 或 Docker。如果缺少提供的驱动程序,或者 minikube 无法在系统上找到该驱动程序,你可能会遇到如下错误:

$ minikube start --driver=podman
😄 minikube v1.32.0 on Fedora 39 (hyperv/amd64)
✨ Using the podman driver based on user configuration
🤷 Exiting due to PROVIDER_PODMAN_NOT_FOUND: The 'podman' provider was not found: exec: "podman": executable file not found in $PATH 

Docker 安装过程很简单,但步骤可能因操作系统不同而有所变化,你可以查看文档 (docs.docker.com/engine/install/) 获取更多信息。同样,Podman 的安装步骤也可以在 podman.io/docs/installation 上找到,适用于不同的操作系统版本。

如果你正在使用 Windows 工作站和基于 Hyper-V 的虚拟机进行实践实验,记得在安装 minikube 和容器引擎的虚拟机中禁用动态内存。

当使用 Podman 驱动程序运行时,minikube 在启动时会检查可用内存,并报告“正在使用的”内存(动态设置)。因此,你需要确保有足够的内存可用,或者为 Kubernetes 节点配置内存需求。

在以下示例中,我们使用 Fedora 39 作为工作站,Docker 作为容器引擎:

$ minikube start --driver=docker --kubernetes-version=1.29.0
😄  minikube v1.32.0 on Fedora 39
❗  Specified Kubernetes version 1.29.0 is newer than the newest supported version: v1.28.3\. Use `minikube config defaults kubernetes-version` for details.
❗  Specified Kubernetes version 1.29.0 not found in Kubernetes version list
🤔  Searching the internet for Kubernetes version...
✅  Kubernetes version 1.29.0 found in GitHub version list
✨  Using the docker driver based on user configuration
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🔥  Creating docker container (CPUs=2, Memory=8000MB) ...
🐳  Preparing Kubernetes v1.29.0 on Docker 24.0.7 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying Kubernetes components...
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default 

我们也可以使用 Podman 作为容器引擎,并通过以下命令使用 minikube 创建 Kubernetes 集群:

$ minikube start --driver=podman 

现在我们已经创建了一个通过 minikube 部署的 Kubernetes 集群,在接下来的部分中,让我们学习如何使用 kubectl 访问和管理该集群。

访问由 minikube 创建的 Kubernetes 集群

现在,我们需要为本地的 kubectl 命令行工具创建一个 kubeconfig 文件,以便它能与这个新的 Kubernetes 安装进行通信。好消息是,当我们执行 minikube start 命令时,minikube 会即时为我们生成一个 kubeconfig 文件。minikube 生成的 kubeconfig 文件指向本地的 kube-apiserver 端点,且你的本地 kubectl 已配置为默认调用该集群。因此,实际上不需要做任何额外操作:kubeconfig 文件已经被正确格式化并放置在合适的位置。

默认情况下,这个配置在~/.kube/config中,你应该能够看到现在有一个minikube上下文:

$ cat ~/.kube/config
...<removed for brevity>..
- context:
    cluster: minikube
    extensions:
    - extension:
        last-update: Mon, 03 Jun 2024 13:06:44 +08
        provider: minikube.sigs.k8s.io
        version: v1.33.1
      name: context_info
    namespace: default
    user: minikube
  name: minikube
...<removed for brevity>.. 

使用以下命令显示当前的kubeconfig文件。你应该能看到一个名为minikube的集群,它指向一个本地 IP 地址:

$ kubectl config view 

接下来,运行以下命令,这将显示当前你的kubectl指向的 Kubernetes 集群:

$ kubectl config current-context
minikube 

现在,让我们尝试发出一个真实的kubectl命令,列出属于我们minikube集群的节点。如果一切正常,此命令应该会到达由minikube启动的kube-apiserver组件,该组件将返回一个节点,因为minikube是单节点解决方案。让我们使用以下命令列出节点:

$ kubectl get nodes
NAME       STATUS   ROLES           AGE     VERSION
minikube   Ready    control-plane   3m52s   v1.29.0 

如果运行此命令时没有看到任何错误,说明你的minikube集群已经准备好并且完全正常工作!

这是你作为本书的一部分运行的第一个真实的kubectl命令。在这里,真实的kube-apiserver组件接收了你的 API 调用,并返回了来自真实etcd数据存储的 HTTP 响应数据。在我们的场景中,这是集群中节点的列表。

由于minikube默认创建一个单节点的 Kubernetes 集群,这个命令只会输出一个节点。这个节点既是控制平面节点,又是计算节点。适合本地测试,但不要在生产环境中部署这种配置。

现在我们可以做的,是列出控制平面组件的状态,以便你开始熟悉kubectl

$ kubectl get componentstatuses
Warning: v1 ComponentStatus is deprecated in v1.19+
NAME                 STATUS    MESSAGE   ERROR
controller-manager   Healthy   ok       
scheduler            Healthy   ok       
etcd-0               Healthy   ok 

此命令应输出控制平面组件的状态。你应该看到如下内容:

  • 一个正在运行的etcd数据存储

  • 一个正在运行的kube-scheduler组件

  • 一个正在运行的kube-controller-manager组件

在接下来的章节中,我们将学习如何通过停止并删除minikube Kubernetes 集群来清理你的 Kubernetes 学习环境。

停止并删除本地的 minikube 集群

你可能想停止或删除本地的minikube安装。继续操作时,不要直接杀死虚拟机或容器,而是使用minikube命令行工具。以下是执行此操作的两个命令:

$ minikube stop
✋  Stopping node "minikube"  ...
🛑  1 node stopped. 

上面的命令将停止集群。然而,集群将继续存在;其状态将被保留,你可以稍后使用以下minikube start命令重新启动它。你可以通过再次运行minikube status命令来检查它:

$ minikube status
minikube
type: Control Plane
host: Stopped
kubelet: Stopped
apiserver: Stopped
kubeconfig: Stopped 

也可以暂停集群,而不是停止它,这样你可以快速重新启动 Kubernetes 集群:

$  minikube pause
⏸️  Pausing node minikube ...
⏯️  Paused 14 containers in: kube-system, kubernetes-dashboard, storage-gluster, istio-operator
$  minikube status
minikube
type: Control Plane
host: Running
kubelet: Stopped
apiserver: Paused
kubeconfig: Configured 

之后,你可以按如下方式恢复集群:

$ minikube unpause 

如果你想销毁集群,请使用以下命令:

$ minikube delete
🔥  Deleting "minikube" in docker ...
🔥  Deleting container "minikube" ...
🔥  Removing /home/gmadappa/.minikube/machines/minikube ...
💀  Removed all traces of the "minikube" cluster. 

如果你使用此命令,集群将被完全销毁。它的状态将丢失,无法恢复。

现在你的minikube集群已经运行,决定是否使用它继续后续章节,或选择其他解决方案,就由你来定。

使用 minikube 的多节点 Kubernetes 集群

也可以使用minikube创建多节点 Kubernetes 集群。在接下来的演示中,我们将使用minikube创建一个三节点 Kubernetes 集群:

在创建多节点集群时,您需要确保工作站有足够的资源来创建多个 Kubernetes 节点(无论是虚拟机还是容器)。同时,注意minikube会根据您在设置或参数中指定的 vCPU 和内存配置启动节点。

$ minikube start --driver=podman --nodes=3 

集群创建完成后,检查节点详情并查看所有节点,如下所示:

$ kubectl get nodes
NAME           STATUS   ROLES           AGE   VERSION
minikube       Ready    control-plane   93s   v1.29.0
minikube-m02   Ready    <none>          74s   v1.29.0
minikube-m03   Ready    <none>          54s   v1.29.0 

minikube创建了一个三节点集群(--nodes=3),其中第一个节点为控制平面节点(或主节点),其余两个节点为计算节点(稍后您需要分配适当的标签;我们将在后续章节中学习此内容)。

使用 minikube 的多主节点 Kubernetes 集群

可能会有这种情况:您想要部署并测试具有高可用控制平面的 Kubernetes 集群,带有多个控制平面节点。您可以使用minikube通过以下命令实现这一点:

$ minikube start \
  --driver=virtualbox \
  --nodes 5 \
  --ha true \
  --cni calico \
  --cpus=2 \
  --memory=2g \
  --kubernetes-version=v1.30.0 \
  --container-runtime=containerd
$ kubectl get nodes
NAME           STATUS   ROLES           AGE     VERSION
minikube       Ready    control-plane   6m28s   v1.30.0
minikube-m02   Ready    control-plane   4m36s   v1.30.0
minikube-m03   Ready    control-plane   2m45s   v1.30.0
minikube-m04   Ready    <none>          112s    v1.30.0
minikube-m05   Ready    <none>          62s     v1.30.0 

minikube将创建一个五节点集群(--nodes 5),并将前 3 个节点配置为控制平面节点(--ha true)。

再次提醒,确保您的工作站有足够的资源来创建这样一个多节点集群。

使用 minikube 的多个 Kubernetes 集群

正如我们所学,minikube是为 Kubernetes 环境的开发和测试设计的。可能会有这样的情况:您想要模拟一个包含多个 Kubernetes 集群的环境。在这种情况下,您可以再次使用minikube,因为使用minikube可以创建多个 Kubernetes 集群。但请记住,为您的不同 Kubernetes 集群指定不同的名称(--profile),如下面所示:

# Start a minikube cluster using VirtualBox as driver.
$  minikube start --driver=virtualbox --kubernetes-version=1.30.0 --profile cluster-vbox
# Start another minikube cluster using Docker as driver
$ minikube start --driver=docker --kubernetes-version=1.30.0 --profile cluster-docker 

您可以列出minikube集群并查看详细信息,如下图所示:

图 3.2:minikube 配置文件列表显示多个 Kubernetes 集群

# Stop cluster with profile name
$ minikube stop --profile cluster-docker
# Remove the cluster with profile name
$ minikube delete --profile cluster-docker 

我们已经学习了如何创建不同类型和大小的 Kubernetes 集群;现在让我们来看看另一个用于设置本地 Kubernetes 集群的工具,名为kind

使用 kind 的多节点 Kubernetes 集群

在本节中,我们将讨论一个名为kind的工具,它也被设计用于在本地运行 Kubernetes 集群,就像minikube一样。

kind的核心思想是使用 Docker 或 Podman 容器作为 Kubernetes 节点,通过Docker-in-DockerDinD)或容器内容器模型。通过启动容器(容器内包含容器引擎和 kubelet),可以使它们表现为 Kubernetes 工作节点。

下图展示了kind集群组件的高层架构:

图 3.3:kind 集群组件(图片来源:https://kind.sigs.k8s.io/docs/design/initial)

这与使用 Docker 驱动的minikube完全相同,唯一不同的是,在这里,它不会在单个容器中完成,而是在多个容器中完成。结果是一个本地多节点集群。类似于minikubekind是一个免费的开源工具。

类似于minikubekind是一个用于本地开发和测试的工具。请勿在生产环境中使用它,因为它并未为此设计。

在本地系统上安装 kind

由于kind是完全围绕 Docker 和 Podman 构建的工具,因此你需要在本地系统上安装并运行这两个容器引擎之一。

由于 Docker 和 Podman 的安装说明已经作为文档提供,我们将在这里跳过这些步骤(请参阅之前部分 使用容器设置 minikube 以获取详细信息)。

请参考kind发布页面以获取kind版本信息和可用性(github.com/kubernetes-sigs/kind/releases)。

同样,安装kind的过程将取决于你的操作系统:

  • 对于 Linux,请使用以下命令:

    $  curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64
    $ chmod +x ./kind
    $ mv ./kind /usr/local/bin/kind
    # Check version
    $ kind version
    kind v0.23.0 go1.21.10 linux/amd64 
    
  • 对于 macOS,请使用以下命令:

    $  curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-darwin-amd64
    $ chmod +x ./kind
    $ mv ./kind /usr/local/bin/kind 
    
  • 你也可以使用 Homebrew 进行安装:

    $ brew install kind 
    
  • 对于 Windows PowerShell,请使用以下命令:

    $ curl.exe -Lo kind-windows-amd64.exe https://kind.sigs.k8s.io/dl/v0.23.0/kind-windows-amd64
    $ Move-Item .\kind-windows-amd64.exe c:\some-dir-in-your-PATH\kind.exe 
    
  • 你也可以使用 Chocolatey 进行安装:

    $ choco install kind 
    
  • 如果你已安装 Go 语言环境,那么可以使用以下命令:

    $ go install sigs.k8s.io/kind@v0.22.0 && kind create cluster 
    

请参考文档(https://kind.sigs.k8s.io/docs/user/quick-start#installation)了解适用于你系统的其他安装方法。

接下来,让我们学习如何使用kind创建 Kubernetes 集群。

使用 kind 创建 Kubernetes 集群

一旦kind安装到你的系统上,你可以立即使用以下命令启动一个新的 Kubernetes 集群:

$ kind create cluster --name test-kind
Creating cluster "kind" ... 

当你运行此命令时,kind将通过拉取一个包含所有控制平面组件的容器镜像,在本地开始构建 Kubernetes 集群。最终结果将是一个单节点的 Kubernetes 集群,其中一个 Docker 容器充当控制平面节点

如果你喜欢,也可以使用 Podman 作为kind集群的提供者,方法如下:

$ KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster 

我们不想要这种设置,因为我们已经可以通过minikube实现它。我们想要的是一个可以定制集群和节点的kind多节点集群。为此,我们需要编写一个非常小的配置文件,并告诉kind使用它作为模板来构建本地 Kubernetes 集群。因此,先让我们删除刚才构建的单节点kind集群,并重新构建它为多节点集群:

  1. 运行此命令删除集群:

    $ kind delete cluster
    Deleting cluster "kind" ... 
    
  2. 然后,我们需要创建一个config文件,作为kind构建我们集群的模板。只需将以下内容复制到该目录下的本地文件中,例如~/.kube/kind_cluster

     kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
    - role: worker
    - role: worker
    - role: worker 
    

请注意,此文件采用 YAML 格式。请注意nodes数组,这是文件中最重要的部分。在这里,你可以告诉kind你希望集群中有多少个节点。角色键可以有两个值:控制平面和工作节点。

根据你选择的角色,会创建不同的节点。

  1. 让我们使用这个config文件重新启动kind create命令来构建我们的多节点集群。对于给定的文件,结果将是一个包含一个主节点和三个工作节点的 Kubernetes 集群:

    $ kind create cluster --config ~/.kube/kind_cluster 
    

通过在创建kind集群时使用适当的镜像信息,也可以构建特定版本的 Kubernetes,如下所示:

$ kind create cluster \
  --name my-kind-cluster \
  --config ~/.kube/kind_cluster \
  --image kindest/node:v1.29.0@sha256:eaa1450915475849a73a9227b8f201df25e55e268e5d619312131292e324d570 

一个新的 Kubernetes 集群将由kind部署和配置,且你将在最后收到与集群访问相关的消息,内容如下:

Creating cluster "my-kind-cluster" ...
✓ Ensuring node image (kindest/node:v1.29.0) 🖼
✓ Preparing nodes 📦 📦 📦 
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-my-kind-cluster"
You can now use your cluster with:
kubectl cluster-info --context kind-my-kind-cluster
Thanks for using kind! 😊 

接下来,你应该有四个新的 Docker 容器:一个作为主节点运行,另外三个作为同一 Kubernetes 集群的工作节点。

现在,像往常一样,我们需要为我们的Kubectl工具编写一个kubeconfig文件,以便能够与新的集群进行交互。猜猜看,kind已经生成了正确的配置,并将其附加到我们的~/.kube/config文件中。此外,kind还将当前上下文设置为我们的新集群,因此实际上没有什么需要做的了。我们可以立即开始查询我们的新集群。让我们使用kubectl get nodes命令列出节点。如果一切正常,我们应该看到四个节点:

$ kubectl get nodes
NAME                            STATUS   ROLES           AGE     VERSION
my-kind-cluster-control-plane   Ready    control-plane   4m      v1.29.0
my-kind-cluster-worker          Ready    <none>          3m43s   v1.29.0
my-kind-cluster-worker2         Ready    <none>          3m42s   v1.29.0 

一切看起来都很完美。你的kind集群正在运行!

就像我们对minikube所做的那样,你也可以使用以下命令检查组件的状态:

$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:42547
CoreDNS is running at https://127.0.0.1:42547/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy 

为了进一步调试和诊断集群问题,可以使用'kubectl cluster-info dump'命令:

$  kubectl get --raw='/readyz?verbose'
[+]ping ok
[+]log ok
[+]etcd ok
...<removed for brevity>...
[+]poststarthook/apiservice-openapiv3-controller ok
[+]shutdown ok
readyz check passed 

作为开发和学习环境管理的一部分,我们需要学习如何停止和删除使用kind创建的 Kubernetes 集群。接下来,我们将学习如何操作。

停止并删除本地的 kind 集群

你可能想要停止或移除kind在本地系统上创建的所有内容,以便在实践后清理环境。为此,你可以使用以下命令:

$ kind stop 

该命令将停止kind正在管理的 Docker 容器。如果你手动在容器上运行 Docker stop命令,你将得到相同的结果。这会停止容器,但会保持集群的状态。也就是说,集群不会被销毁,只需要通过以下命令重新启动它,就可以恢复到停止前的状态。

如果你想完全从系统中移除集群,可以使用以下命令。运行此命令将会从你的系统中删除集群及其状态,你将无法恢复该集群:

$ kind delete cluster
Deleting cluster "kind" ... 

现在你的 kind 集群已投入使用,接下来你可以决定是否在接下来的章节中一边阅读一边进行实践。你还可以选择是否使用本章接下来的部分中描述的其他解决方案。kind 特别适合,因为它是免费的,并且允许你安装一个多节点集群。然而,它并非为生产环境设计,依然是一个面向开发和测试的非生产环境解决方案。kind 利用 Docker 容器创建 Kubernetes 节点,这些节点在实际环境中应该是 Linux 机器。

接下来我们将了解一些替代的 Kubernetes 学习和测试环境。

替代的 Kubernetes 学习环境

你还可以利用一些可用的零配置学习环境,这些环境旨在让你的 Kubernetes 之旅更加顺畅和愉快。

玩转 Kubernetes

DockerTutorius 提供的这个互动沙盒(labs.play-with-k8s.com),为你提供了一个简单有趣的方式来实验 Kubernetes。在几秒钟内,你就可以直接在浏览器中运行你自己的 Kubernetes 集群。

该环境具备以下特点:

  • 免费的 Alpine Linux 虚拟机:体验一个真实的虚拟机环境,无需离开浏览器。

  • DinD:该技术创造了多个虚拟机的假象,让你能够探索分布式系统的概念。

Killercoda Kubernetes 沙盒

Killercoda (killercoda.com/playgrounds/scenario/kubernetes) 是一个零配置沙盒环境,提供一个可通过浏览器访问的临时 Kubernetes 环境。通过他们承诺在发布几周后提供最新的 kubeadm Kubernetes 版本,让你时刻掌握最新趋势。

该环境具备以下特点:

  • 瞬态环境:使用预配置集群快速开始,集群在你完成后会消失。这使其非常适合快速实验,无需任何承诺。

  • 带有两个节点的空 kubeadm 集群:通过一个现成的两节点集群,深入了解 Kubernetes 的核心功能。

  • 带有调度能力的控制平面节点:与一些沙盒环境不同,这个环境允许你在控制平面节点上调度工作负载,为测试提供了更多的灵活性。

接下来,我们将探讨一些生产级 Kubernetes 选项。

生产级 Kubernetes 集群

到目前为止,我们一直在讨论用于开发和学习的 Kubernetes 环境。如何构建一个满足你特定需求的生产级 Kubernetes 环境?接下来,我们将看到一些 Kubernetes 用户采纳的知名选项。

在接下来的部分,我们将了解主要 云服务提供商CSPs)提供的托管 Kubernetes 服务。

使用云服务的托管 Kubernetes 集群

如果你更倾向于使用托管服务来搭建 Kubernetes 环境,那么有多个选择可以使用,如 GKE、AKS、EKS 等:

  • Google Kubernetes Engine (GKE):由Google Cloud PlatformGCP)提供,GKE 是一个完全托管的 Kubernetes 服务。它负责集群生命周期的各个环节,从配置、扩展到维护。GKE 与其他 GCP 服务无缝集成,是现有 GCP 用户的理想选择。

  • Azure Kubernetes Service (AKS):是 Microsoft Azure 的一部分,AKS 是另一个托管的 Kubernetes 解决方案。与 GKE 类似,AKS 处理所有集群管理工作,让你可以专注于容器化应用的部署和管理。AKS 与其他 Azure 服务很好地集成,使其成为 Azure 用户的自然选择。

  • Amazon Elastic Kubernetes Service (EKS):由Amazon Web ServicesAWS)提供,EKS 提供了 AWS 生态系统中的托管 Kubernetes 服务。像 GKE 和 AKS 一样,EKS 负责集群管理,让你可以专注于应用程序的开发。EKS 与其他 AWS 服务集成,是 AWS 用户的强大选择。

这些托管 Kubernetes 服务提供了一种方便且可扩展的方式来部署和管理容器化应用程序,避免了自管理 Kubernetes 集群的复杂性。

我们有详细的章节来学习如何部署和管理这些集群,如下所示:

  • 第十五章在 Google Kubernetes Engine 上运行 Kubernetes 集群

  • 第十六章在 Amazon Web Services 上使用 Amazon Elastic Kubernetes Service 启动 Kubernetes 集群

  • 第十七章在 Microsoft Azure 上使用 Azure Kubernetes Service 运行 Kubernetes 集群

如果你没有本地 Kubernetes 环境,如我们在本章前面的部分所述,你可以通过参考相关章节,在你选择的云平台上使用托管 Kubernetes 服务创建一个。但在开始阅读本书的下一部分 第二部分深入了解 Kubernetes 核心概念 之前,必须拥有一个正常工作的 Kubernetes 集群。

我们将在下一节学习 Kubernetes 发行版和平台。

Kubernetes 发行版

Kubernetes 发行版本质上是 Kubernetes 的预打包版本,除了核心 Kubernetes 提供的功能外,还包括其他附加功能和特性。它们像增值包一样,满足特定需求并简化用户的部署过程。为了获得更丰富的体验,可以考虑以下 Kubernetes 发行版:

  • Red Hat OpenShift:这个企业级发行版通过开发者工具(镜像构建和 CI/CD 流水线)、多集群管理、安全特性(RBAC 和 SCC)以及内建的复杂部署扩展功能来扩展 Kubernetes(www.redhat.com/en/technologies/cloud-computing/openshift)。

  • Rancher:一个完整的容器管理平台,Rancher 超越了 Kubernetes。它提供跨多环境的多集群管理、各种编排平台的工作负载管理以及预配置应用程序的市场(www.rancher.com/)。

  • VMware Tanzu:为 VMware 生态系统设计,Tanzu 无缝集成基础设施供应、安全性和混合云部署。它提供了针对 VMware 环境中容器化应用程序的生命周期管理工具(tanzu.vmware.com/platform)。

请注意,以上列出的某些 Kubernetes 发行版是基于订阅或许可证的产品,并非免费提供。

在下一节中,让我们了解一些 Kubernetes 部署工具。

Kubernetes 安装工具

以下工具提供了对 Kubernetes 集群设置的灵活性和控制。当然,你还需要使用其他第三方工具和平台添加更多的自动化,以管理你的 Kubernetes 环境:

  • kubeadm:这是 Kubernetes 官方工具,提供了一种用户友好的方式来设置 Kubernetes 集群,适用于测试和生产环境。它的简便性使得集群可以快速部署,但可能需要额外的配置来实现生产级别的功能,如高可用性(kubernetes.io/docs/reference/setup-tools/kubeadm/)。

  • kops:为了管理生产中的强大 Kubernetes 集群,kops 是 Kubernetes 的一个官方项目,提供命令行控制。它简化了高可用集群的创建、升级和维护,确保容器化应用程序的可靠运行(kops.sigs.k8s.io/)。

  • Kubespray:想要在裸金属或虚拟机上部署 Kubernetes?Kubespray 利用 Ansible 自动化的力量。它将 Ansible 剧本与 Kubernetes 资源相结合,允许在你首选的基础设施上自动化集群部署(github.com/kubespray)。

  • Terraform:该工具允许你在不同的云提供商上定义和管理 Kubernetes 集群基础设施。基于代码的方法确保了在不同环境中部署集群时的一致性和可重复性(www.terraform.io/)。

  • Pulumi:类似于 Terraform,Pulumi 提供基础设施即代码功能。它允许你使用 Python 或 Go 等编程语言定义和管理 Kubernetes 集群基础设施。与纯声明性配置语言相比,这种方法提供了更大的灵活性和定制化(www.pulumi.com/)。

如果 Kubernetes 的景观非常广阔,有多个 Kubernetes 集群,那么您需要考虑混合多集群管理解决方案;让我们在下一节中了解这些内容。

混合和多云解决方案

跨不同环境管理 Kubernetes 集群需要强大的工具,有一些工具提供了这样的多集群管理功能:

  • Anthos(Google):这个混合和多云平台便于在不同环境中管理 Kubernetes 集群。Anthos 允许组织在本地、云端或边缘上采用一致的方法来部署和管理容器化应用程序(cloud.google.com/anthos)。

  • Red Hat 高级集群管理(RHACM)for Kubernetes:Red Hat 也提供了一种解决方案,用于管理混合和多云环境中的 Kubernetes 集群。他们的高级集群管理平台为您的容器化工作负载提供了一致的部署、管理和治理的集中控制平面(www.redhat.com/en/technologies/management/advanced-cluster-management)。

  • VMware Tanzu Mission Control:这个集中管理工具简化了跨多个环境管理 Kubernetes 集群的过程。从单个控制台,您可以无论在本地、云端还是混合环境,进行集群的配置、监控和管理(docs.vmware.com/en/VMware-Tanzu-Mission-Control/index.html)。

如何选择您的 Kubernetes 平台和管理解决方案?让我们在下一节中探讨一些关键点。

选择正确的环境

最佳的生产级 Kubernetes 环境取决于几个因素:

  • 控制级别:您是否需要完全控制集群配置,还是对预配置的托管服务感到满意?

  • 现有基础设施:在选择部署方法时,请考虑您的现有基础设施(云提供商、裸金属)。

  • 可扩展性需求:您需要多快地扩展或缩减集群以满足变化的需求?

  • 团队专业知识:评估您的团队在 Kubernetes 和云基础设施方面的经验,以确定哪种解决方案最适合他们的技能。

通过仔细考虑这些因素并探索各种可用的选项,您可以构建一个适合您容器化应用程序的生产级 Kubernetes 环境,以实现最佳性能和可扩展性。

在接下来的部分中,我们将讨论一些集群维护任务。

在本地运行 Kubernetes:挑战与考虑因素

在本地环境中运行 Kubernetes 提供了对基础设施的更多控制,但也需要仔细的管理。与云托管解决方案相比,维护本地 Kubernetes 集群需要手动处理从供应到升级的所有方面。下面,我们将探讨在管理本地 Kubernetes 时遇到的关键考虑事项和挑战。

  • 基础设施供应:在本地设置 Kubernetes 的基础设施意味着自动化节点的供应。像 Rancher 的云控制器或 Terraform 这样的工具可以帮助简化这一过程,确保一致性。Packer 也可以用于创建虚拟机镜像,通过在节点之间部署更新的镜像,从而实现更顺畅的升级。

  • 集群设置与维护:在本地部署 Kubernetes 集群需要使用kubeadm等工具。与云托管环境相比,这一过程通常更加复杂。集群维护任务包括更新证书、管理节点和处理高可用性设置,这些都增加了进一步的复杂性。

  • 负载均衡与访问:在本地环境中为应用程序提供外部访问可能具有挑战性。像NodePortLoadBalancer服务这样的标准 Kubernetes 选项可能不足以满足需求。MetalLB可以为裸金属环境提供负载均衡解决方案,但也有一些限制,例如无法在高可用性环境中负载均衡 API 服务器。

  • 持久存储:持久存储对于运行生产工作负载至关重要。Kubernetes 依赖于PersistentVolumeClaimsPVCs)和PersistentVolumesPVs),它们需要与物理存储系统进行集成。像 Longhorn 这样的工具可以实现卷的动态供应和跨节点的复制,从而提供本地环境中的灵活性。

  • 升级与扩展性:Kubernetes 发布频繁的更新,这意味着在本地环境中管理升级可能会变得棘手。必须在将新版本推向生产环境之前进行测试。像 Packer 和 Terraform 这样的工具可以通过简化节点的添加和升级来帮助扩展。

  • 网络:本地 Kubernetes 网络依赖于数据中心的配置。需要手动管理 DNS、负载均衡器和网络设置。像 Prometheus 这样的监控工具,以及像 MetalLB 这样的负载均衡解决方案可以提供帮助,尽管它们需要集成和持续的监控。

  • 监控与管理:监控本地集群对于确保系统健康至关重要。可以使用像 Prometheus 和 Grafana 这样的工具来监控资源使用情况。此外,应该设置日志记录和告警系统,以便快速发现和解决问题,帮助减少停机时间。

  • 工具与自动化:自动化任务,如节点管理和升级,对于本地集群至关重要。像 Rancher 或 OpenShift 这样的企业 Kubernetes 平台有助于减少人工干预,提供更简化且易于管理的 Kubernetes 环境。

  • 安全与合规性:安全性在企业 Kubernetes 部署中至关重要。从一开始就包括FIPS联邦信息处理标准)支持,有助于满足合规性需求,并在系统发展过程中保持安全的环境。

  • 总结来说,管理本地 Kubernetes 提供了更多灵活性,但需要仔细关注基础设施、网络和存储配置。借助合适的工具和策略,组织可以在自己的基础设施上有效地扩展和维护强大的 Kubernetes 环境。

总结

这一章内容非常密集!要跟随本书,您需要一个 Kubernetes 集群,因此我们探讨了五种在不同平台上设置 Kubernetes 集群的方法。您了解了minikube,它是设置本地集群最常用的方法。您还发现了kind,这是一种可以设置多节点本地集群的工具,这是minikube的一个限制。

我们了解了一些 Kubernetes 的学习环境,并探索了包括三个主要 Kubernetes 云服务(GKE、Amazon EKS 和 AKS)在内的生产级 Kubernetes 环境。这三个服务允许您在云端创建 Kubernetes 集群,供您进行实践和训练。这只是对这些服务的简要介绍,我们以后会有机会更深入地了解它们。目前,您只需要选择最适合您的解决方案。

在下一章,我们将深入探讨 Kubernetes,首先了解 Pods 的概念。Pod 资源是 Kubernetes 管理的最重要资源。我们将学习如何创建、更新和删除 Pods。此外,我们还将了解如何配置它们,如何从中获取信息,以及如何更新它们运行的容器。

我们将在 Kubernetes 集群上部署一个 NGINX Pod,并查看如何从外部访问它。在下一章结束时,您将能够通过 Pods 在 Kubernetes 集群上启动您的第一个容器。您在此安装的集群将在您跟随下一章的实际示例时非常有用。

进一步阅读

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第四章:在 Kubernetes 中运行你的容器

本章可能是本书中最重要的一章。在这里,我们将讨论 Pods 的概念,Pods 是 Kubernetes 用来启动应用程序容器的对象。Pods 是 Kubernetes 的核心,掌握它们至关重要。

第三章安装你的第一个 Kubernetes 集群中,我们提到过 Kubernetes API 定义了一组表示计算单元的资源。Pods 是在 Kubernetes API 中定义的资源,表示一个或多个容器。我们从不直接使用 Kubernetes 创建容器,而是总是创建 Pods,这些 Pods 会在 Kubernetes 集群的计算节点上转化为容器。

起初,理解 Kubernetes Pods 和容器之间的关系可能会有些困难,这就是为什么我们要解释什么是 Pods 以及为什么我们使用 Pods 而不是直接使用容器的原因。Kubernetes Pod 可以包含一个或多个应用程序容器。但在本章中,我们将重点讨论只包含一个容器的 Kubernetes Pod。接下来,在下一章中,我们将有机会了解包含多个容器的 Pods。

我们将使用BusyBox镜像来创建、删除和更新 Pods,BusyBox 是一个基于 Linux 的镜像,包含许多在运行测试时有用的工具。我们还将基于 NGINX 容器镜像启动一个 Pod 来启动 HTTP 服务器。我们将探索如何通过 kubectl 提供的端口转发功能访问默认的 NGINX 主页。这将有助于我们通过 Web 浏览器访问和测试在 Kubernetes 集群中运行的 Pods。

然后,我们将探索如何为 Pods 添加标签和注释,使它们更易于访问。这将帮助我们组织 Kubernetes 集群,以确保集群尽可能清晰。最后,我们将介绍两个额外的资源,分别是 JobsCronJobs。到本章结束时,你将能够启动第一个由 Kubernetes 管理的容器,这是成为 Kubernetes 大师的第一步!

本章中我们将讨论以下主要内容:

  • 让我们来解释一下 Pods 的概念

  • 启动你的第一个 Pod

  • 给 Pods 添加标签和注释

  • 启动你的第一个 Job

  • 启动你的第一个 CronJob

技术要求

为了跟随本章中的示例,你需要以下资源:

  • 一个正确配置的 Kubernetes 集群,以便你可以在阅读时实践文中的命令。无论是 minikube、Kind、GKE、EKS 还是 AKS 集群,都无关紧要。

  • 本地机器上安装了可用的 kubectl。你可以选择多个节点,但至少需要一个 Ready 节点才能保证 Kubernetes 设置正常运行。

你可以从官方 GitHub 仓库下载本章的最新代码示例,链接:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter04

让我们来解释一下 Pods 的概念

在这一部分,我们将从理论角度解释 Pods 的概念。Pods 有一些特性,必须理解这些特性,如果你想掌握它们的话。

什么是 Pods?

当你想通过 Kubernetes 创建、更新或删除容器时,你是通过 Pod 来实现的。Pod 是一个或多个你希望在同一台机器上、同一个 Linux 命名空间中启动的容器组。这是理解 Pods 的第一个规则:它们可以由一个或多个容器组成,但所有属于同一个 Pod 的容器都会在同一个工作节点上启动。Pod 不能,也永远不会跨多个工作节点:这是一个绝对的规则。

但是,为什么我们要将容器的管理委托给这个中介资源呢?毕竟,Kubernetes 本可以有一个容器资源,直接启动一个单独的容器。原因是,容器化让你在思考时应该更多关注 Linux 进程,而不是虚拟机。你可能已经知道,容器最大的反模式之一就是将容器当作虚拟机的替代品:过去,你通常会将所有进程安装并部署在虚拟机上。但容器并不是虚拟机的替代品,它们并不设计用来运行多个进程。

容器技术要求你遵循一条黄金法则:容器和 Linux 进程之间应该有一一对应的关系。也就是说,现代应用通常由多个进程组成,而不仅仅是一个进程,因此在大多数情况下,单独使用一个容器不足以运行一个功能齐全的微服务。这意味着这些进程,也就是容器,应该能够通过共享文件系统、网络等进行相互通信。这正是 Kubernetes Pods 所提供的功能:让你能够逻辑上地组织容器。构成应用的所有容器/进程应当被分组到同一个 Pod 中。这样,它们就会一起启动,并在促进进程间和容器间通信时,受益于所有相关功能。

图 4.1:容器和 Pods

为了帮助你理解这一点,假设你有一个在虚拟机上运行的 WordPress 博客,并且你想将这台虚拟机转换成一个 WordPress Pod,来在 Kubernetes 集群中部署你的博客。WordPress 是最常见的软件之一,是一个完美的示例,用来说明 Pods 的必要性。这是因为 WordPress 需要多个进程才能正常工作。

WordPress 是一个 PHP 应用程序,需要 Web 服务器和 PHP 解释器才能工作。我们来列举一下 WordPress 在 Linux 上运行时所需的进程:

  • NGINX HTTP 服务器:它是一个 Web 应用程序,因此需要一个 HTTP 服务器进程来接收并提供服务器的博客页面。NGINX 是一个非常好的 HTTP 服务器,能够完美地完成这项工作。

  • PHP-FastCGI-Process-Manager (FPM) 解释器:它是用 PHP 编写的博客引擎,因此需要 PHP 解释器才能工作。

NGINX 和 PHP-FPM 是两个进程:它们是两个二进制文件,需要分别启动,但它们需要能够协同工作。在虚拟机上,这项工作很简单:只需在虚拟机上安装 NGINX 和 PHP-FPM,并通过 Unix 套接字使它们进行通信。你可以通过配置 /etc/nginx.config 文件告诉 NGINX,PHP-FPM 的 Linux 套接字是可访问的。

在容器世界中,情况变得更加复杂,因为将这两个进程运行在同一个容器中是一种反模式:你必须运行两个容器,每个容器运行一个进程,而且它们需要能够互相通信并共享一个公共目录,以便它们都能够访问应用程序代码。为了解决这个问题,你必须使用 Docker 网络层,使 NGINX 容器能够与 PHP-FPM 容器进行通信。然后,你还需要使用卷挂载来在这两个容器之间共享 WordPress 的代码。你可以通过一些 Docker 命令做到这一点,但现在想象一下它在生产环境中的规模,涉及多个机器、多个环境等等。在裸 Docker 中实现进程间通信是可能的,但在大规模环境中实现这一点,同时保持所有生产相关的要求,是非常困难的。随着大量微服务的管理,分布在不同的机器上,管理所有这些 Docker 网络、卷挂载等将变得非常复杂。正如你可以想象的那样,这正是 Kubernetes Pod 资源解决的问题。Pods 非常有用,因为它们封装了多个容器,并实现了简单的进程间通信。以下是 Pods 为你带来的核心优势:

  • 同一 Pod 中的所有容器可以通过 localhost 相互访问,因为它们共享相同的网络命名空间。

  • 同一 Pod 中的所有容器共享相同的端口空间。

  • 你可以将卷附加到 Pod,然后将卷挂载到底层容器中,使它们能够共享目录和文件位置。

利用 Kubernetes 带来的优势,你可以轻松配置你的 WordPress 博客,只需创建一个运行两个容器的 Pod:NGINX 和 PHP-FPM。由于它们可以在 localhost 上相互访问,因此它们之间的通信非常简单。然后,你可以使用卷将 WordPress 的代码暴露给这两个容器。

最复杂的应用程序将强制要求多个容器,因此最好将它们分组在同一个 Pod 内,这样 Kubernetes 就可以将它们一起启动。请记住,Pod 只有一个目的:简化大规模的容器间(或进程间)通信。

话虽如此,只有一个容器的 Pod 并不罕见。但无论如何,Pod 是 Kubernetes API 提供的最低抽象级别,也是你将与之交互的对象。

最后,请注意,手动在 Kubernetes 集群管理的机器上启动的容器,Kubernetes 不会将其视为它所管理的容器。它会变成一种孤立容器,超出了调度器的管理范围。Kubernetes 仅管理通过其 Pod API 启动的容器。

每个 Pod 都会获得一个 IP 地址

单个 Pod 内的容器能够通过 localhost 相互通信,但 Pods 之间也能够相互通信。每个 Pod 在启动时会获得一个私有 IP 地址。每个 Pod 可以通过其 IP 地址与集群中的任何其他 Pod 进行通信。

Kubernetes 使用平面网络模型,该模型由一个名为 容器网络接口CNI)的组件实现。CNI 充当容器化应用程序和 Kubernetes 集群内底层网络基础设施之间的标准化桥梁。这消除了为每个容器配置自定义网络的需要,从而简化了通信和数据流。

CNI 利用灵活的插件架构。这些插件使用各种语言编写,通过标准输入/输出与容器运行时通信。插件规范定义了一个清晰的接口,用于网络配置、IP 地址分配和跨多个主机的连接维护。容器运行时调用这些插件,从而在 Kubernetes 环境内实现容器网络的动态管理和更新。这种方法确保了容器化应用程序的无缝和适应性强的网络连接。

以下图表展示了 Pods 与容器之间的高层次通信流程。

图 4.2:容器与 Pod 的通信

你应该如何设计你的 Pods?

尽管理解 Pods 至关重要,但在 Kubernetes 的实际应用中,大多数团队会利用一个更强大的构件:部署(Deployment)。部署提供了一个更高级别的抽象,用于管理 Pods。它们自动化了任务,如扩展和在故障时重新启动 Pods,确保应用程序体验更加稳健和可管理。稍后我们将深入讨论部署,但现在先让我们探索 Pods API,巩固对这些基础构建块的理解。

所以,关于 Pods 的第二条黄金法则是:它们应该容易被销毁并重新创建。Pods 可以自愿或非自愿地被销毁。例如,如果某个工作节点运行了四个 Pods,并且该节点发生故障,则每个底层容器都会变得不可访问。因此,您应该能够随时销毁和重新创建 Pods,而不会影响应用的稳定性。实现这一点的最佳方法是在构建 Pods 时遵循两条简单的设计规则:

  • 一个 Pod 应该包含启动应用所需的所有内容。

  • 一个 Pod 应该将任何类型的状态存储在 Pod 外部,使用外部存储(PersistentVolume)。

当您开始设计 Kubernetes 上的 Pods 时,很难确切知道一个 Pod 应该包含什么,不该包含什么。解释起来相当简单:一个 Pod 必须包含一个应用或一个微服务。以我们之前提到的 WordPress Pod 为例:该 Pod 应该包含启动 WordPress 所需的 NGINX 和 PHP-FPM 容器。如果这样的 Pod 失败,我们的 WordPress 将无法访问,但重新创建该 Pod 会使 WordPress 再次可用,因为 Pod 包含了运行 WordPress 所需的一切。

也就是说,每个现代应用都会利用外部存储来存储其状态,数据库存储,例如 Redis 或 MySQL,或者通过调用另一个微服务应用来存储状态。WordPress 本身也这么做——它使用 MySQL(或 MariaDB)来存储和检索您的帖子。所以,您还需要在某个地方运行一个 MySQL 容器。这里有两种解决方案:

  • 您将 MySQL 容器作为 WordPress Pod 的一部分运行。

  • 您将 MySQL 容器作为专用 MySQL Pod 的一部分运行。

两种解决方案都可以使用,但第二种更为推荐。一个好主意是将您的应用(在这里是 WordPress,但明天可能是一个微服务)与其数据库或逻辑层解耦,分别运行在两个独立的 Pods 中。记住,Pods 之间是可以通信的。您可以通过将一个 Pod 专门用于运行 MySQL,并将其 Pod IP 地址提供给您的 WordPress 博客,从中获益。

通过将数据库层与应用分离,您可以提高设置的稳定性:应用 Pod 崩溃不会影响数据库。

总结来说,将应用层合并在同一个 Pod 中会导致三个问题:

  • 数据持久性

  • 可用性

  • 稳定性

这就是为什么建议尽可能保持应用 Pods 无状态,通过将其状态存储在独立的 Pod 中来实现。通过将数据层视为一个独立的应用,并具有自己的开发和管理生命周期,我们可以实现解耦架构。这种分离使得数据层可以独立扩展、更新和测试,而不会影响应用代码本身。

有状态的单体应用

尽管有一些特殊的使用场景,但在 2024 年,通常不建议在 Kubernetes 上运行快速变化的单体有状态工作负载,因为在容器中管理单体应用程序的复杂性、在快节奏环境中频繁更新的潜在低效性,以及与传统部署相比,持续存储需求带来的更高管理开销。

通过 IP 地址访问 Pods

您可以使用 Pod 的 IP 地址来访问它们;然而,这不是与正在运行的应用程序交互的推荐方法。在接下来的章节中,我们将深入探讨 Service 资源,它在将 IP 地址映射到 Pods 中发挥着至关重要的作用。敬请关注如何通过 Services 提高 Pod 可访问性和应用程序之间的通信的详细解释。

现在,让我们启动我们的第一个 Pod。创建一个 WordPress Pod 目前过于复杂,所以我们先从启动一些 NGINX Pods 开始,看看 Kubernetes 如何管理容器。

启动您的第一个 Pods

在本节中,我们将解释如何在 Kubernetes 集群中创建我们的第一个 Pods。Pods 有一些特殊性,必须理解这些特性才能很好地掌握它们。

目前我们不会在您的 Kubernetes 集群上创建资源;相反,我们将简单地解释什么是 Pods。在下一节中,我们将开始构建我们的第一个 Pods。

使用命令式语法创建 Pod

在本节中,我们将基于 NGINX 镜像创建一个 Pod。我们需要两个参数来创建一个 Pod:

  • Pod 的名称,由您随意定义

  • 用于构建其基础容器的容器镜像

与 Kubernetes 中的几乎所有内容一样,您可以使用两种可用的语法来创建 Pods:命令式语法和声明式语法,您在第二章Kubernetes 架构 – 从容器镜像到运行的 Pods中已经了解过。提醒一下,命令式语法是直接从终端运行 kubectl 命令,而声明式语法则需要编写一个包含 Pod 配置信息的 YAML 文件,然后使用 kubectl apply -f 命令应用该文件。

要在 Kubernetes 集群上创建 Pod,您必须使用 kubectl run 命令。这是启动 Pod 在 Kubernetes 集群上运行的最简单和最快的方法。下面是如何调用此命令:

$ kubectl run nginx-pod --image nginx:latest 

在此命令中,Pod 的名称设置为 nginx-pod。这个名称非常重要,因为它是指向该 Pod 的标识:当您需要对这个 Pod 执行 updatedelete 命令时,必须指定这个名称,以告知 Kubernetes 应在哪个 Pod 上执行该操作。--image 标志将用于指定该 Pod 将要运行的容器。一旦 Pod 被集群创建,您可以通过以下方式检查其状态:

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

立即创建一个 Pod 并非即时完成。如果容器镜像在本地不可用,Kubernetes 可能需要从注册表拉取该镜像并配置 Pod 的环境。要实时跟踪此过程,请使用kubectl get po -w命令,该命令显示 Pod 的信息并自动刷新。

在这里,您告诉 Kubernetes 基于 Docker Hub 上托管的nginx:latest容器镜像构建一个 Pod。这个nginx-pod Pod 只包含一个基于这个nginx:latest镜像的容器:在此处不能指定多个镜像;这是命令式语法的一个限制。

如果您想要构建一个包含多个容器的 Pod,这些容器由几个不同的容器镜像构建而成,那么您将不得不通过声明性语法编写一个 YAML 文件。

标签与摘要 – 确保镜像一致性

在创建 Pod 时,您可能会遇到标签和摘要的引用。两者都用于标识容器镜像,但有一个关键区别:

  • 标签:将标签视为镜像版本的可读名称。它们可以更改为指向相同镜像的不同版本,可能会导致意外行为。

  • 摘要:这些是镜像的唯一指纹,确保您始终引用确切的所需版本。这对于安全性和可重现性至关重要,特别是考虑到潜在的软件供应链攻击。

例如,而不是使用nginx:latest(标签),您可能使用nginx@sha256:1445eb9c6dc5e9619346c836ef6fbd6a95092e4663f27dcfce116f051cdbd232(摘要)。您可以从注册表本身获取容器镜像的摘要信息,或者使用podman manifest inspect nginx:latest命令。

图 4.3:从容器注册表获取镜像摘要

这确保您部署具有唯一abcd1234哈希的特定镜像版本。这种做法在安全和可靠部署方面变得越来越重要。

让我们学习如何在下一节中使用 YAML 声明创建 Pod。

使用声明性语法创建 Pod

使用声明性语法创建 Pod 也很简单。您只需创建一个包含 Pod 定义的 YAML 文件,并使用kubectl apply -f命令将其应用于您的 Kubernetes 集群。

请记住,Kubernetes 不能在同一命名空间(例如,在我们的情况下是default命名空间)中运行两个具有相同名称的 Pod:Pod 的名称是唯一标识符,用于识别命名空间内的 Pod。在创建新 Pod 与前一步骤中相同名称的 Pod 之前,您需要删除现有的 Pod:

$ kubectl delete pod nginx-pod
pod "nginx-pod" deleted 

这里是nginx-pod.yaml文件的内容,您可以在本地工作站上创建:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
    - name: nginx-container
      image: nginx:latest 

尝试阅读此文件并理解其内容。YAML 文件仅包含键值对。Pod 的名称是nginx-Pod,然后我们在文件的spec:部分有一个容器数组,该数组仅包含从nginx:latest镜像创建的一个容器。容器本身的名称是nginx-container

一旦保存了nginx-Pod.yaml文件,请运行以下命令以创建 Pod:

$ kubectl apply -f nginx-pod.yaml
pod/nginx-pod created 

如果您的集群中已经存在名为nginx-pod的 Pod,则此命令将失败。尝试编辑 YAML 文件以更新 Pod 的名称,然后再次应用它。

Kubernetes 中的命名空间

在资源创建过程中,如果您忘记指定命名空间,则默认为默认命名空间。敬请关注第六章,《Kubernetes 中的命名空间、配额和多租户限制》,我们将深入探讨 Kubernetes 命名空间的重要性。

读取 Pod 的信息和元数据

此时,您的 Kubernetes 集群应该已经运行了一个 Pod。在这里,我们将尝试读取其信息。随时,我们需要能够检索和阅读有关在您的 Kubernetes 集群上创建的资源的信息;这对于 Pods 尤其重要。可以通过两个kubectl命令实现读取 Kubernetes 集群的操作:kubectl getkubectl describe。让我们来看看它们:

  • kubectl getkubectl get命令是一个列表操作;您可以使用此命令列出一组对象。还记得我们在上一章节中描述的所有安装过程后列出集群节点的情况吗?我们使用了kubectl get nodes命令。该命令通过要求您传递想要列出的对象类型来工作。在我们的情况下,将是kubectl get pods操作。在接下来的章节中,我们将发现其他对象,如configmapssecrets。要列出它们,您需要键入kubectl get configmaps;对其他对象类型也是如此。例如,nginx-pod可以按以下方式列出:

    $ kubectl get pods 
    
  • kubectl describekubectl describe命令有所不同。它旨在检索已从其种类和对象名称标识的一个特定对象的完整信息集。您可以使用kubectl describe pods nginx-pod检索我们之前创建的 Pod 的信息。调用此命令将返回关于该特定 Pod 的全部可用信息,例如其 IP 地址。要查看nginx-pod的详细信息,可以使用以下命令:

    $ kubectl describe pod nginx-pod
    Name:             nginx-pod
    Namespace:        default
    ...<removed for brevity>...
    Containers:
      nginx-container:
        Container ID:   containerd://3afbbe30b51b77994df69f4c4dbefb02fc304efb2bf0f5bdb65a65
    1154a8e311
        Image:          nginx:latest
    ...<removed for brevity>...
    Conditions:
      Type                        Status
      PodReadyToStartContainers   True
      Initialized                 True
    ...<removed for brevity>...
    Events:
      Type    Reason     Age   From               Message
      ----    ------     ----  ----               -------
    ...<removed for brevity>...
      Normal  Created    112s  kubelet            Created container nginx-container
      Normal  Started    112s  kubelet            Started container nginx-container 
    

从前面的命令输出中,您可以读取大量信息,包括以下内容:

  • Pod 名称和命名空间:这标识了您请求信息的特定 Pod(例如,nginx-pod)。

  • 容器详细信息:列出 Pod 内的容器信息,包括镜像名称、资源请求/限制和当前状态。

  • Pod 状态:显示 Pod 的当前操作状态(例如,Running、Pending、CrashLoopBackOff)。

  • 事件:提供与 Pod 生命周期相关的事件历史记录,包括创建、重启或错误。

现在,让我们来看一下在 Kubernetes 中列出和描述对象的一些更高级的选项。

以 JSON 或 YAML 格式列出对象

-o--output 选项是 kubectl 命令行中最有用的选项之一。这个选项有一些你必须了解的好处。它允许你自定义 kubectl 命令行的输出。默认情况下,kubectl get pods 命令将以一种格式化的方式返回你 Kubernetes 集群中的 Pod 列表,方便最终用户查看。你还可以使用 -o 选项以 JSON 格式或 YAML 格式检索这些信息:

$ kubectl get pods --output yaml # In YAML format
$ kubectl get pods --output json # In JSON format 

如果你知道 Pod 的名称,你还可以获取特定的 Pod:

$ kubectl get pods <POD_NAME> -o yaml
# OR
$ kubectl get pods <POD_NAME> -o json 

通过这种方式,你可以以适合脚本的格式从 Kubernetes 集群中检索和导出数据。

使用列出操作备份资源

你还可以使用这些标志来备份你的 Kubernetes 资源。假设你使用命令式方式创建了一个 Pod,因此你没有将 YAML 声明文件存储在计算机上。如果 Pod 失败,重新创建它将变得很困难。-o选项帮助我们检索 Kubernetes 中已创建资源的 YAML 声明文件,即使我们是通过命令式方式创建的。要做到这一点,请运行以下命令:

$ kubectl get pods/nginx-pod -o yaml > nginx-pod-output.yaml 

通过这种方式,你拥有了一个与运行中的集群 nginx-pod 资源相对应的 YAML 备份。你可以随时将输出文件与原始 YAML 声明进行比较,并使用 diff 命令或其他工具分析差异:

$ diff nginx-pod.yaml nginx-pod-output.yaml 

有一些工具可以清理 YAML 并获得干净的可用声明输出。例如,kube-neat 就是这样一个工具,它可以帮助清理详细输出中的不需要的信息。请参考 github.com/itaysk/kubectl-neat 了解更多。

如果发生问题,你将能够轻松地重新创建 Pod。请注意此命令的nginx-pod部分。要检索 YAML 声明,你需要指定你要操作的资源。通过将此命令的输出重定向到文件中,你可以轻松地获取并备份 Kubernetes 集群中对象的配置。

从列出操作中获取更多信息

还值得提到的是-o宽格式,这对你来说非常有用:使用此选项可以将默认输出扩展以添加更多数据。例如,使用它查看Pods对象时,你将获得 Pod 所在工作节点的名称:

$ kubectl get pods -o wide
NAME        READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
nginx-pod   1/1     Running   0          15m   10.244.0.4   minikube   <none>           <none> 

请记住,-o 选项可以接受许多不同的参数,其中一些要高级得多,比如 jsonpath,它允许您直接在 JSON 主体文档上执行排序操作,仅检索特定信息,就像您之前使用过的 jq 库,如果您已经编写了一些处理 JSON 解析的 bash 脚本。

从外部世界访问 Pod

此时,您的 Kubernetes 集群应包含一个 Pod,其中运行着一个 NGINX HTTP 服务器。现在您应该能够从 web 浏览器访问它。但是,这有点复杂。

默认情况下,您的 Kubernetes 集群不会将其运行的 Pod 暴露给外部世界。为此,您需要使用另一种称为服务的资源,我们将在 第八章 中详细介绍如何使用服务公开您的 Pod。不过,kubectl 确实提供了一条快速访问集群上正在运行的容器的命令,称为 kubectl port-forward。以下是如何使用它:

$ kubectl port-forward pod/nginx-pod 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

这条命令非常容易理解:我们告诉 kubectl 将本地机器(运行 kubectl 的机器)上的端口 8080 转发到由 pod/nginx-Pod 标识的 Pod 上的端口 80

然后,kubectl 输出一条消息,告诉您它已开始将本地 8080 端口转发到 Pod 的 80 端口。如果收到错误消息,可能是因为您的本地端口 8080 当前正在使用中。尝试设置不同的端口或简单地从命令中删除本地端口,让 kubectl 随机选择一个本地端口:

$ kubectl port-forward pod/nginx-pod 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

现在,您可以启动浏览器,并尝试访问 http://localhost:<localport> 地址,您的情况下是 http://localhost:8080

图 4.1 – 运行在 Pod 中并可以在本地主机上访问的 NGINX 默认页面,表明端口转发命令已生效!

图 4.4:运行在 Pod 中并可以在本地主机上访问的 NGINX 默认页面,表明端口转发命令已生效

完成测试后,请使用 Ctrl + C 命令结束端口转发任务。

进入 Pod 内部的容器

当 Pod 被启动时,您可以访问它包含的 Pod。在 Docker 下,执行在运行的容器中执行命令的命令称为 docker exec。Kubernetes 通过称为 kubectl exec 的命令复制此行为。使用以下命令访问我们早些时候启动的 nginx-pod 内部的 NGINX 容器:

$ kubectl exec -it nginx-pod -- bash
root@nginx-pod:/# hostname
nginx-pod 

运行此命令后,您将进入 NGINX 容器。您可以在这里像处理任何其他容器一样做任何操作。上述命令假设您正在尝试访问的容器中安装了 bash 二进制文件。否则,许多容器通常安装了 sh 二进制文件,可能会用于访问容器。不要害怕采取完整的二进制路径,就像这样:

$ kubectl exec -it nginx-pod -- /bin/bash 

测试完成后,请使用 exit 命令退出容器的 bash shell:

root@nginx-pod:/# exit
exit 

重要提示:容器中的安全性与非 root 用户

一般建议使用非 root 用户运行容器。你需要限制漏洞可能带来的潜在损害。如果漏洞被利用,非 root 用户对系统的访问权限较少,从而减少影响。此外,遵循最小权限原则,仅授予容器运行所需的权限,从而减少其攻击面。我们将在第十八章Kubernetes 中的安全性中探讨安全上下文。

现在,让我们了解如何从 Kubernetes 集群中删除 Pod。

删除 Pod

删除 Pod 非常简单。你可以使用kubectl delete命令来删除。你需要知道你想删除的 Pod 的名称。在我们的例子中,Pod 的名称是nginx-pod。运行以下命令:

$ kubectl delete pods nginx-pod
# or...
$ kubectl delete pods/nginx-pod 

如果你不知道 Pod 的名称,记得运行kubectl get pods命令以获取 Pod 列表,并找到你想删除的 Pod。

你还需要知道一件事:如果你使用声明式语法构建了 Pod,并且仍然拥有其 YAML 配置文件,你可以删除 Pod,而无需知道容器的名称,因为它包含在 YAML 文件中。

运行以下命令,使用声明式语法删除 Pod:

$ kubectl delete -f nginx-pod.yaml 

在你运行此命令后,Pod 将以相同的方式被删除。

请记住,Pod 所属的所有容器都将被删除。容器的生命周期与启动它的 Pod 的生命周期绑定。如果 Pod 被删除,它管理的容器也会被删除。记得始终与 Pod 交互,而不是直接与容器交互。

至此,我们已经回顾了 Pod 管理中的最重要方面,例如使用命令式或声明式语法启动 Pod,删除 Pod,以及列出和描述它们。现在,我们将介绍 Kubernetes 中 Pod 管理的一个重要方面:标签和注释。

对 Pod 进行标签和注释

我们现在将讨论 Kubernetes 的另一个关键概念:标签和注释。标签是你可以附加到 Kubernetes 对象的键值对。标签用于标记你的 Kubernetes 对象,标签的键值对由你定义。一旦 Kubernetes 对象被标记,你就可以构建自定义查询,基于它们所持有的标签检索特定的 Kubernetes 对象。在这一节中,我们将通过kubectl与标签交互,给我们的 Pod 分配一些标签。

什么是标签,为什么我们需要它们?

你为对象定义的标签由你决定——对此没有具体的规则。这些标签是属性,允许你在 Kubernetes 集群中组织对象。举个非常具体的例子,你可以为一些 Pod 附加一个名为environment = prod的标签,然后使用kubectl get pods命令列出该环境中的所有 Pod。因此,你可以通过一个命令列出属于生产环境的所有 Pod:

$ kubectl get pods --label "environment=production" 

如你所见,这可以通过--label参数来实现,并且可以使用其-l等效参数进行简化:

$ kubectl get pods --label "environment=production" 

这个命令将列出所有持有名为environment且值为production的标签的 Pod。当然,在我们的例子中,由于之前创建的 Pod 都没有持有此标签,所以不会找到任何 Pod。你必须在创建 Pod 或其他对象时非常自律,确保每次都设置标签,这也是我们在本书中较早介绍标签的原因:不仅是 Pod 几乎所有 Kubernetes 对象都可以被标记,你应该利用这一特性来保持集群资源的组织和整洁。

你使用标签不仅是为了组织集群,还可以在不同的 Kubernetes 对象之间建立关系:你会注意到,一些 Kubernetes 对象会读取某些 Pod 携带的标签,并根据它们携带的标签执行某些操作。如果你的 Pod 没有标签,或者标签命名不当,或者标签包含错误的值,那么这些机制可能无法按预期工作。

另一方面,使用标签是完全任意的:没有特定的命名规则,也没有 Kubernetes 期望你遵循的约定。因此,你可以根据自己的需要使用标签并构建自己的约定。如果你负责 Kubernetes 集群的管理,你应该强制使用强制标签,并建立一些监控规则,以便快速识别没有标签的资源。

请记住,标签的字符数限制为 63 个;它们旨在简短。以下是一些你可以使用的标签示例:

  • environmentproddevuat等)

  • stackbluegreen等)

  • tierfrontendbackend

  • app_namewordpressmagentomysql等)

  • teambusinessdevelopers

标签不要求在对象之间唯一。例如,也许你想列出所有属于生产环境的 Pod。在这里,集群中可以同时存在具有相同标签键值对的多个 Pod 而不会引发问题——如果你希望查询列表正常工作,甚至建议这么做。例如,如果你想列出所有属于 prod 环境的资源,应该在多个资源上创建一个名为environment = prod的标签。接下来,让我们看一下注解,它是另一种为 Pod 分配元数据的方式。

注解是什么,它们与标签有何不同?

Kubernetes 还使用另一种元数据类型,称为注解。注解与标签非常相似,因为它们也是键值对。然而,注解与标签的用途不同。标签旨在识别资源并建立它们之间的关系,而注解用于提供有关定义在其上的资源的上下文信息。

例如,当你创建一个 Pod 时,可以添加一个注解,包含支持团队的电子邮件地址,以便在该应用无法正常工作时进行联系。这个信息应该放在注解中,而与标签无关。

尽管强烈建议在可能的地方定义标签,但你可以省略注解:它们对集群的操作比标签重要性要低。不过,要注意,某些 Kubernetes 对象或第三方应用程序通常会读取注解并将其作为配置使用。在这种情况下,它们对注解的使用将在其文档中明确说明。

添加标签

在本节中,我们将学习如何向 Pod 添加和移除标签和注解。我们还将学习如何修改已经存在于集群中的 Pod 的标签。

让我们以之前使用的基于 NGINX 镜像的 Pod 为例。我们将在这里重新创建它,并添加一个名为 tier 的标签,值为 frontend。这是用于此操作的 kubectl 命令:

$ kubectl run nginx-pod --image nginx --labels "tier=frontend" 

如你所见,可以使用 --labels 参数分配标签。你还可以通过使用 --labels 参数和逗号分隔的值,像这样添加多个标签:

$ kubectl run nginx-pod --image nginx  --labels="app=myapp,env=dev,tier=frontend" 

在这里,nginx Pod 将创建两个标签。

--labels 标志有一个简写版本 -l。你可以使用这个简写使命令更简洁、更易读。标签可以附加到 YAML Pod 定义中。这里是相同的 Pod,包含我们之前创建的两个标签,但这次,它是通过声明式语法创建的:

# labelled_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    environment: prod
    tier: frontend
spec:
  containers:
    - name: nginx-container
      image: nginx:latest 

考虑一下在 ~/labelled_pod.yaml 中创建的文件。以下 kubectl 命令将以与之前相同的方式创建该 Pod:

$ kubectl apply -f ~/labelled_pod.yaml 

这次,运行我们之前使用的命令应该至少返回一个 Pod——我们刚刚创建的那个:

$ kubectl get pod -l environment=prod
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   1/1     Running   0          31m 

现在,让我们学习如何列出附加到 Pod 的标签。

列出附加到 Pod 的标签

没有专门的命令来列出附加到 Pod 的标签,但你可以通过使 kubectl get pods 的输出更加详细来实现。通过使用 --show-labels 参数,命令的输出将包含附加到 Pod 的标签:

$  kubectl get pods --show-labels
NAME        READY   STATUS    RESTARTS   AGE   LABELS
nginx-pod   1/1     Running   0          56s   environment=prod,tier=frontend 

该命令并不根据标签执行任何查询;相反,它将标签本身作为输出的一部分显示。

向运行中的 Pod 添加或更新标签

现在我们已经学会了如何创建带标签的 Pod,我们将学习如何向正在运行的 Pod 添加标签。你可以随时使用 kubectl label 命令来添加、创建或修改资源的标签。在这里,我们将向 nginx Pod 添加另一个标签。这个标签名为 stack,值为 blue

$ kubectl label pod nginx-pod stack=blue
pod/nginx-pod labeled 

该命令仅在 Pod 没有名为 stack 的标签时有效。执行该命令时,它只能添加一个新标签,而不能更新它。此命令将通过添加一个名为 stack 且值为 blue 的标签来更新 Pod。运行以下命令以查看更改是否已应用:

$ kubectl get pods nginx-pod --show-labels
NAME        READY   STATUS    RESTARTS   AGE   LABELS
nginx-pod   1/1     Running   0          38m   environment=prod,stack=blue,tier=frontend 

要更新现有的标签,必须在前面的命令中附加 --overwrite 参数。让我们将 stack=blue 标签更新为 stack=green;请注意 overwrite 参数:

$ kubectl label pod nginx-pod stack=green --overwrite
pod/nginx-pod labeled 

在这里,标签应该已经更新。stack 标签的值现在应为 green。运行以下命令再次显示 Pod 及其标签:

$ kubectl get pods nginx-pod --show-labels
NAME        READY   STATUS    RESTARTS   AGE   LABELS
nginx-pod   1/1     Running   0          41m   environment=prod,stack=green,tier=frontend 

使用 kubectl label 命令添加或更新标签可能是危险的。正如我们之前提到的,你会根据标签在不同的 Kubernetes 对象之间建立关系。通过更新它们,你可能会破坏其中的一些关系,导致资源的行为不如预期。因此,最好在 Pod 创建时添加标签,并保持 Kubernetes 配置不可变。比起更新已经在运行的配置,销毁并重新创建更为安全。

我们必须做的最后一件事是学习如何删除附加到正在运行的 Pod 上的标签。

删除附加到正在运行的 Pod 上的标签

就像我们向正在运行的 Pod 添加和更新标签一样,我们也可以删除它们。这个命令稍微有点复杂。在这里,我们将删除名为 stack 的标签,方法是在标签名称后加一个减号符号(-):

$ kubectl label pod nginx-pod stack-
pod/nginx-pod unlabeled 

在命令末尾添加减号符号可能会让人感觉有些奇怪,但再次运行 kubectl get pods --show-labels 命令后,应该能看到 stack 标签已消失:

$ kubectl get pods nginx-pod --show-labels 

现在,让我们在下一部分学习 Kubernetes 中的注解。

添加注解

Kubernetes 注解是键值对,你可以将其附加到各种 Kubernetes 对象上,如 Pod、Deployment 和 Service。它们允许你向这些对象添加额外信息,而不改变它们的核心功能。与用于标识和选择的标签不同,注解旨在存储可以供人类阅读或由外部工具使用的附加数据。注解可以包括配置相关信息或创建者的名称等细节。

让我们学习如何向 Pod 添加注解:

# annotated_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    tier: webserver
  name: nginx-pod
  labels:
    environment: prod
    tier: frontend
spec:
  containers:
    - name: nginx-container
      image: nginx:latest 

在这里,我们仅仅添加了 tier: webserver 注解,它帮助我们识别该 Pod 正在运行 HTTP 服务器。请记住,这只是添加额外元数据的一种方式。

当你应用这个新配置时,可以使用 kubectl replace -f 命令来替换现有的 Pod 配置。

kubectl replace 是一个命令,用于使用清单文件更新或替换现有的 Kubernetes 资源。与 kubectl apply -f 相比,它提供了一种更强制的方法。kubectl replace 命令将现有资源定义替换为清单文件中指定的定义。本质上,它会覆盖现有资源配置。与可能尝试合并更改的 kubectl apply 不同,kubectl replace 的目的是完全替换。这条命令对于你想要确保资源的配置特定于某种状态时很有用,无论当前状态如何。它也适用于资源定义可能已损坏,需要完全替换的情况。

注解的名称可以以 DNS 名称为前缀。这适用于 Kubernetes 组件,如 kube-scheduler,它必须向集群用户表明该组件是 Kubernetes 核心的一部分。前缀可以完全省略,如前面的例子所示。

你可以通过使用 kubectl describe Pod、kubectl get po -o yaml 或者 jq 工具来查看注解,具体方法如下:

$ kubectl get pod nginx-pod -o json | jq '.metadata.annotations'
{
  "cni.projectcalico.org/containerID": "666d12cd2fb7d6ffe09add73d8466db218f01e7c7ef5315ef0187a675725b5ef",
  "cni.projectcalico.org/podIP": "10.244.151.1/32",
  "cni.projectcalico.org/podIPs": "10.244.151.1/32",
  "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"nginx-pod\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx:latest\",\"name\":\"nginx-container\"}]}}\n"
} 

接下来让我们在本章的下一节学习 Kubernetes 中的 Jobs。

启动你的第一个 Job

现在,让我们来探索另一个 Kubernetes 资源——Job 资源。 在 Kubernetes 中,计算资源是 Pod,其他一切只是操作 Pods 的中介资源。

这就是 Job 对象的情况,它会创建一个或多个 Pods 来完成特定的计算任务,比如运行一个 Linux 命令。

什么是 Jobs?

Job 是 Kubernetes API 提供的另一种资源。最终,Job 会创建一个或多个 Pods 来执行你定义的命令。这就是 Jobs 的工作原理:它们启动 Pods。你必须理解两者之间的关系:Jobs 不是独立于 Pods 的,若没有 Pods,它们将毫无意义。最终,它们能做的两件事就是启动 Pods 并管理它们。Jobs 旨在处理特定任务并在完成后退出。以下是 Kubernetes Job 的一些典型用例:

  • 数据库备份

  • 发送电子邮件

  • 消费队列中的一些消息

这些是你不希望永远运行的任务。你希望 Pods 在完成任务后被终止。这时,Jobs 资源将为你提供帮助。

但为什么要使用另一个资源来执行命令呢?毕竟,我们可以直接创建一个或多个 Pods 来运行命令然后退出。

这是事实。你可以使用基于容器镜像的 Pod 来运行你想要的命令,这样是可以正常工作的。然而,Jobs 在它们的层面实现了允许它们以更高级方式管理 Pods 的机制。以下是 Jobs 能够做的一些事情:

  • 运行多个 Pods

  • 并行运行多个 Pods

  • 如果遇到错误,重新尝试启动 Pods

  • 在指定的秒数后终止一个 Pod

另一个优点是,job 会管理它所创建的 Pods 的标签,因此你无需直接管理这些 Pods 上的标签。

所有这些都可以在不使用 job 的情况下完成,但这将非常难以管理,这也是 Kubernetes 中有 Jobs 资源的原因。

创建具有 restartPolicy 的 job

由于创建 job 可能需要一些高级配置,我们将在此集中讲解声明式语法。这是通过 YAML 创建 Kubernetes job 的方式。我们将简化操作,job 仅仅回显 Hello world

# hello-world-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job
spec:
  template:
    metadata:
      name: hello-world-job
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'"] 

请注意 kind 资源,它告诉 Kubernetes 我们需要创建一个 job,而不是像之前那样创建 Pod。同时,注意 apiVersion:,它与创建 Pod 时使用的版本不同。

图 4.5:Job 定义细节与 Pod 模板

你可以使用以下命令创建 job:

$ kubectl apply -f hello-world-job.yaml 

如你所见,这个 job 将基于 busybox 容器镜像创建一个 Pod:

$ kubectl get jobs
NAME              COMPLETIONS   DURATION   AGE
hello-world-job   1/1           9s         8m46s 

这将运行 echo 'Hello World' 命令。最后,restartPolicy 选项设置为 OnFailure,这告诉 Kubernetes 在 Pod 或容器失败时重新启动它。如果整个 Pod 失败,将重新启动一个新的 Pod。如果容器失败(内存限制已达或出现非零退出代码),该容器会在同一节点上重新启动,因为 Pod 会保持不变,这意味着它仍然会调度到同一台机器上。

restartPolicy 参数可以选择两个选项:

  • Never

  • OnFailure

将其设置为 Never 会阻止 job 在失败时重新启动 Pods。调试失败的 job 时,设置 restartPolicyNever 是个好主意,这有助于调试。否则,新的 Pods 可能会被反复创建,这会让你在调试时更加困难。

在我们的例子中,我们的 job 成功的可能性很大,因为我们只想运行一个简单的 Hello world。为了确保我们的 job 成功执行,我们可以按以下方式查看 job 日志:

$kubectl logs jobs/hello-world-job
Hello world 

我们还可以使用 kubectl get pods 命令来检索 job 创建的 Pod 的名称。然后,我们可以使用 kubectl logs 命令,如下所示:

$  kubectl logs pods/hello-world-job-2qh4d
Hello world 

在这里,我们可以看到我们的 job 执行得很好,因为我们能在 Pod 的日志中看到 Hello world 消息。但是,如果它失败了呢?嗯,这取决于 restartPolicy —— 如果设置为 Never,那么什么都不会发生,Kubernetes 不会尝试重新启动 Pods。

然而,如果 restartPolicy 设置为 OnFailure,Kubernetes 会在 10 秒后尝试重新启动 job,然后在每次失败时将时间加倍。10 秒,20 秒,40 秒,80 秒,依此类推。6 分钟后,Kubernetes 会放弃。

理解 job 的 backoffLimit

默认情况下,Kubernetes 任务会在 Pod 失败后的六分钟内尝试重新启动该 Pod 六次。你可以通过修改backoffLimit选项来改变这个限制。以下是更新后的 YAML 文件:

# hello-world-job-2.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job-2
spec:
  backoffLimit: 3
  template:
    metadata:
      name: hello-world-job-2
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'"] 

这样,任务在失败后只会尝试重新启动 Pod 两次。

使用completions选项多次运行任务

你也可以指示 Kubernetes 使用Job对象多次启动一个任务。你可以通过使用completions选项来指定你希望命令执行的次数。在下面的示例中,完成次数将创建 10 个不同的 Pod,它们将依次启动。一旦一个 Pod 完成,下一个 Pod 将启动。以下是更新后的 YAML 文件:

# hello-world-job-3.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job-3
spec:
  backoffLimit: 3
  completions: 10
  template:
    metadata:
      name: hello-world-job-3
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'"] 

这里添加了completions选项。此外,请注意,我们更新了args部分,添加了sleep 3选项。使用此选项会让任务在完成前休眠三秒钟,给我们足够的时间注意到下一个 Pod 的创建。一旦你将此配置文件应用到你的 Kubernetes 集群中,你可以运行以下命令:

$ kubectl get pods --watch
$ kubectl get jobs -w
NAME                STATUS     COMPLETIONS   DURATION   AGE
hello-world-job-2   Complete   1/1           10s        52s
hello-world-job-3   Running    5/10          49s        49s
hello-world-job-3   Running    5/10          51s        51s
hello-world-job-3   Running    5/10          52s        52s
hello-world-job-3   Running    6/10          52s        52s
hello-world-job-3   Running    6/10          56s        56s
<removed for brevity> 

watch-w–watch)机制将在有新内容到达时更新你的kubectl输出,例如新创建的 Pod 被 Kubernetes 管理时。如果你希望等待任务完成,你将看到 10 个 Pod 被创建,每个之间有 3 秒的延迟。

并行运行任务多次

completions选项确保 Pod 按顺序一个接一个地创建。你也可以使用parallelism选项强制并行执行。如果这样做,你可以去掉completions选项。Kubernetes 任务可以利用并行性显著加快执行速度。通过并行运行多个 Pod,你将工作负载分配到集群中,从而加快完成时间并提高资源利用率,尤其是在处理大型或复杂任务时。以下是更新后的 YAML 文件:

# hello-world-job-4.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job-4
spec:
  backoffLimit: 3
  parallelism: 5
  template:
    metadata:
      name: hello-world-job-4
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'; sleep 3"] 

请注意,completions选项已被移除,我们用parallelism代替了它。现在,任务会同时启动五个 Pod,并且会并行运行:

$ kubectl get pods -w
NAME                      READY   STATUS              RESTARTS   AGE
hello-world-job-4-9dspk   0/1     ContainerCreating   0          7s
hello-world-job-4-n6qv9   0/1     Completed           0          7s
hello-world-job-4-pv754   0/1     ContainerCreating   0          7s
hello-world-job-4-ss4g8   1/1     Running             0          7s
hello-world-job-4-v78cj   1/1     Running             0          7s
...<removed for brevity>... 

在下一部分,我们将学习如何在特定时间后自动终止一个任务。

在特定时间后终止任务

你也可以决定在特定时间后终止一个 Pod。当你运行一个任务时,可能会用到这个功能,例如,当任务要处理一个队列时。你可以在轮询消息一分钟后自动终止进程。你可以通过使用activeDeadlineSeconds参数来实现这一点。以下是更新后的 YAML 文件:

# hello-world-job-5.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job-5
spec:
  backoffLimit: 3
  activeDeadlineSeconds: 60
  template:
    metadata:
      name: hello-world-job-5
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'"] 

在这里,不管发生什么,任务将在 60 秒后终止。如果你希望一个进程在特定时间内运行并在之后终止,使用这个功能是个不错的选择。

如果任务成功,会发生什么?

如果你的任务完成了,它将保留在 Kubernetes 集群中,并不会自动删除:这是默认行为。原因是,你可以在任务完成很久之后查看它的日志。然而,长时间保留这些任务在 Kubernetes 集群中可能不适合你。你可以通过使用 ttlSecondsAfterFinished 选项来自动删除任务及其创建的 Pods。以下是实现此解决方案的更新 YAML 文件:

# hello-world-job-6.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-world-job-6
spec:
  backoffLimit: 3
  ttlSecondsAfterFinished: 30
  template:
    metadata:
      name: hello-world-job-6
    spec:
      restartPolicy: OnFailure
      containers:
      - name: hello-world-container
        image: busybox
        command: ["/bin/sh", "-c"]
        args: ["echo 'Hello world'"] 

在这里,任务将在完成后 30 秒被删除。

删除一个任务

请记住,创建的 Pods 绑定到其父资源的生命周期。删除任务会导致删除它们管理的 Pods。

首先获取你想要删除的任务的名称。在我们的例子中,它是 hello-world-job。否则,可以使用 kubectl get jobs 命令来获取正确的名称。然后,运行以下命令:

$ kubectl delete jobs hello-world-job 

如果你想删除这些任务,但不想删除它们所创建的 Pods,你需要在 delete 命令中添加 --cascade=false 参数:

$ kubectl delete jobs hello-world-job --cascade=false 

通过这个命令,你可以清除所有在 Kubernetes 集群中完成后的任务。现在,我们继续讨论如何启动第一个 CronJob。

启动你的第一个 CronJob

为了结束关于 Pods 的第一章,我们将介绍另一个 Kubernetes 资源 —— CronJob

什么是 CronJobs?

CronJob 这个名字可以有两种不同的含义,我们需要明确区分这两者:

  • Unix 的 cron 功能

  • Kubernetes 的 CronJob 资源

历史上,CronJobs 是使用 Unix 的 cron 功能调度的命令,这是在 Unix 系统中调度命令执行的最强大方法。这个概念后来被引入到 Kubernetes 中。

要小心,因为尽管这两个概念相似,但它们的工作方式完全不同。在 Unix 和其他类似 Unix 的系统中,你是通过编辑一个名为 Crontab 的文件来调度命令,通常该文件位于 /etc/crontab 或相关路径。而在 Kubernetes 的世界里,事情则有所不同:你不是调度命令的执行,而是调度 Job 资源的执行,而这些 Job 资源会创建 Pod 资源。记住,你创建的 CronJob 对象将会创建 Job 对象。

可以将其看作是 Job 资源的一个封装:在 Kubernetes 中,我们称之为控制器。CronJob 能做所有 Job 资源能够做的事情,因为它不过是 Job 资源的一个封装,具体取决于所指定的 cron 表达式。

好消息是,Kubernetes 的 CronJob 资源使用的是从 Unix 继承的 cron 格式。所以,如果你已经在 Linux 系统上写过一些 CronJobs,掌握 Kubernetes 的 CronJobs 会变得非常简单。

但首先,为什么你需要执行一个 Pod 呢?答案很简单,这里有一些具体的使用场景:

  • 每周日凌晨 1 点定期进行数据库备份

  • 每周一下午 4 点清除缓存数据

  • 每隔 5 分钟发送一次排队的邮件。

  • 各种需要定期执行的维护操作。

Kubernetes CronJobs 的使用场景与 Unix 中的类似 – 它们用于解决相同的需求,但它们提供了一个巨大的优势,即允许你使用已经配置好的 Kubernetes 集群,通过你的容器镜像在现有的 Kubernetes 集群上调度定期任务。

准备你的第一个 CronJob

现在是时候创建你的第一个 CronJob 了。我们将使用声明式语法来实现。首先,创建一个 cronjob.yaml 文件,并将以下 YAML 内容放入其中:

# hello-world-cronjob.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello-world-cronjob
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          name: hello-world-cronjob
        spec:
          restartPolicy: OnFailure
          containers:
          - name: hello-world-container
            image: busybox
            imagePullPolicy: IfNotPresent
            command: ["/bin/sh", "-c"]
            args: ["echo 'Hello world'"] 

在将该文件应用到 Kubernetes 集群之前,我们先开始解释它。这里有两点非常重要需要注意:

  • schedule 键,让你输入 cron 表达式

  • jobTemplate 部分,实际上就是你在作业 YAML 清单中输入的内容。

在应用这个文件之前,我们先快速解释一下这两个键。

理解 schedule 键

schedule 键允许你插入一个类似 Linux 中 cron 格式的表达式。我们来解释一下这些表达式是如何工作的;如果你已经知道这些表达式的用法,可以跳过这些解释:

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │                                   7 is also Sunday on some systems)
# │ │ │ │ │                                   OR sun, mon, tue, wed, thu, fri, sat
# │ │ │ │ │
# * * * * * 

一个 cron 表达式由五个条目组成,这些条目通过空格分隔。从左到右,这些条目分别对应以下内容:

  • 分钟

  • 小时

  • 每月的日期

  • 月份

  • 星期几

每个条目都可以用星号填充,这表示 每个。你还可以通过用 , 来分隔多个值为一个条目设置多个值。你也可以使用 输入一个值的范围。让我给你一些示例:

  • 10 11 * * *” 表示“每天的 11:10 执行。”

  • 10 11 * 12 *” 表示“每年 12 月的每天 11:10 执行。”

  • 10 11 * 12 1” 表示“每年 12 月的每个星期一的 11:10 执行。”

  • 10 11 * * 1,2” 表示“每个月的每周一和周二的 11:10 执行。”

  • 10 11 * 2-5 *” 表示“每年 2 月到 5 月的每天 11:10 执行。”

这里有一些示例可以帮助你理解 cron 的工作原理。当然,你不必记住所有语法:大多数人通过查阅文档或使用在线的 cron 表达式生成器来帮助自己,比如 crontab.cronhub.io 和 crontab.guru。如果觉得这太复杂,可以随时使用这类工具;它可以帮助你在将对象部署到 Kubernetes 前确认你的语法是否有效。

理解 jobTemplate 部分的作用

如果你注意到 YAML 文件的结构,可能会发现 jobTemplate 键包含了 Job 对象的定义。当我们使用 CronJob 对象时,我们实际上是将 Job 对象的创建委托给了 CronJob 对象。

图 4.6:CronJob YAML 架构

因此,CronJob 对象是一个仅操作另一个资源的资源。

稍后我们将发现许多对象,它们可以帮助我们创建 Pods,这样我们就不需要自己手动创建了。这些特殊的对象被称为控制器:它们按照自己的逻辑操作其他 Kubernetes 资源。此外,当你仔细思考时,Job 对象本身也是一个控制器,因为最终它只操作 Pods,通过为 Pods 提供自己的特性,比如并行运行 Pods 的能力。

在实际应用中,你应该始终尝试使用这些中间对象来创建 Pods,因为它们提供了额外的、更高级的管理功能。

尝试记住这个规则:Kubernetes 中的基本单元是 Pod,但你可以将 Pod 的创建委托给许多其他对象。在本节的剩余部分,我们将继续探索裸 Pod。稍后我们将学习如何通过控制器来管理 Pod 的创建和管理。

控制 CronJob 执行截止时间

由于某些原因,CronJob可能无法执行。在这种情况下,Kubernetes 无法在预定的启动时间执行 Job。如果 Job 超过其配置的截止时间,Kubernetes 会将其视为失败。

可选的.spec.startingDeadlineSeconds字段设定了一个截止时间(以完整的秒数表示),用于在因任何原因错过预定时间时启动 Job。一旦错过截止时间,Cronjob 会跳过该特定实例的 Job,但未来的执行仍然会被调度。

管理 Job 的历史记录限制

Cronjob 完成后,无论其成功与否,你的 Kubernetes 集群都会保留历史记录。历史记录的设置可以在CronJob级别进行配置,允许你决定是否保留每个CronJob的历史记录。如果你选择保留历史记录,你可以使用可选的.spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit字段来指定保留成功和失败的 Job 条目的数量。

创建一个 CronJob

如果你已经有了 YAML 清单文件,创建一个CronJob对象很简单:

# hello-world-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello-world-cronjob
spec:
  schedule: "*/1 * * * *"
  # Run every minute
  successfulJobsHistoryLimit: 5
  startingDeadlineSeconds: 30
  jobTemplate:
    spec:
      template:
        metadata:
          name: hello-world-cronjob
        spec:
          restartPolicy: OnFailure
          containers:
            - name: hello-world-container
              image: busybox
              imagePullPolicy: IfNotPresent
              command: ["/bin/sh", "-c"]
              args: ["echo 'Hello world'"] 

请参阅前面 YAML 示例中的详细信息。

successfulJobsHistoryLimit: 5指示CronJob控制器保留最近的 5 次成功执行的 Job 记录。较早的成功 Job 将被自动删除。

你可以使用kubectl apply -f命令来创建CronJob,命令如下:

$ kubectl apply -f hello-world-cronjob.yaml
cronjob.batch/hello-world-cronjob created 

这样,CronJob已经在你的 Kubernetes 集群上创建成功。它将启动一个按 YAML 文件配置的调度 Pod;在这个例子中,每分钟执行一次:

$  kubectl get cronjobs
NAME                  SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello-world-cronjob   */1 * * * *   <none>     False     0        37s             11m
$ kubectl get jobs
NAME                           COMPLETIONS   DURATION   AGE
hello-world-cronjob-28390196   1/1           3s         4m47s
hello-world-cronjob-28390197   1/1           3s         3m47s
hello-world-cronjob-28390198   1/1           3s         2m47s
hello-world-cronjob-28390199   1/1           3s         107s
hello-world-cronjob-28390200   1/1           4s         47s
$ kubectl get pods
NAME                                 READY   STATUS      RESTARTS   AGE
hello-world-cronjob-28390196-fpmc6   0/1     Completed   0          4m52s
hello-world-cronjob-28390197-vkzw2   0/1     Completed   0          3m52s
hello-world-cronjob-28390198-tj6qv   0/1     Completed   0          2m52s
hello-world-cronjob-28390199-dd666   0/1     Completed   0          112s
hello-world-cronjob-28390200-kn89r   0/1     Completed   0          52s 

由于你配置了successfulJobsHistoryLimit: 5,只有最后的 5 个 Job 或 Pod 会显示出来。

删除一个 CronJob

与其他 Kubernetes 资源一样,删除一个CronJob可以通过kubectl delete命令实现。和之前一样,如果你有 YAML 清单文件,这会非常简单:

$ kubectl delete -f ~/cronjob.yaml
cronjob/hello-world-cronjob deleted 

这样,CronJob就被你的 Kubernetes 集群销毁了。以后将不再启动任何调度的 Job。

总结

我们已经结束了本章关于 Pods 以及如何创建 Pods 的内容;希望你觉得有趣。你已经学会了如何使用 Kubernetes 中最重要的对象:Pods。

你在本章中所学到的知识是掌握 Kubernetes 的基础之一:你在 Kubernetes 中所做的一切都是操作 Pods、为它们打标签以及访问它们。但也请记住,在实际的 Kubernetes 环境中,你不会直接创建或修改资源,而是通过其他方法来部署你的应用程序 Pods 和其他资源。此外,你已经看到 Kubernetes 像传统的 API 一样,通过执行 CRUD 操作与集群中的资源进行交互。在本章中,你学习了如何在 Kubernetes 上启动容器,如何通过 kubectl 端口转发访问这些容器,如何为 Pods 添加标签和注释,如何删除 Pods,以及如何使用 CronJob 资源启动和调度任务。

只要记住关于容器管理的这个规则:任何将在 Kubernetes 中启动的容器,都会通过对象启动。掌握这个对象就像掌握大多数 Kubernetes:其他一切都将围绕 Pods 的管理进行自动化,就像我们用 CronJob 对象所做的那样;你已经看到,CronJob 对象仅启动 Job 对象,而 Job 对象又启动 Pods。如果你理解了某些对象可以管理其他对象,但最终所有容器都是由 Pods 管理的,那么你就理解了 Kubernetes 背后的哲学,这样你就能轻松地继续使用这个编排工具。

此外,我们鼓励你为 Pods 添加标签和注释,即使你目前看不到它们的必要性。知道良好的标签管理对保持一个干净、结构化且有条理的集群至关重要。

然而,在管理 Pods 方面,你仍然有很多东西需要发现,因为到目前为止,我们仅看到了由一个 Docker 容器组成的 Pods。Pods 的最大优势在于它们允许你同时管理多个容器,当然,为了做好这件事,当 Pods 由多个容器组成时,我们可以遵循几种设计模式来管理我们的 Pods。

在下一章中,我们将学习如何管理由多个容器组成的 Pods。虽然这与我们迄今为止看到的 Pods 非常相似,但你会发现一些小地方不同,且有些不同之处是值得了解的。首先,你将学习如何使用 kubectl 启动多容器 Pods(提示:kubectl 不会直接生效),然后学习如何让容器之间进行通信。接下来,你将学习如何访问多容器 Pod 中的特定容器,以及如何访问特定容器的日志。最后,你将学习如何在同一个 Pod 中共享卷。

在你阅读下一章时,你将学习 Kubernetes 中 Pods 的其他基础知识。这样,你将对 Pods 有一个概览,同时我们将继续向前迈进,探索 Kubernetes 中其他对部署应用于集群有帮助的对象。

进一步阅读

加入我们在 Discord 上的社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第五章:使用多容器 Pod 和设计模式

在 Kubernetes 上运行复杂应用程序需要在同一个 Pod 中运行多个容器,而不仅仅是一个。Kubernetes 的优势也在于它能够创建由多个容器组成的 Pod。本章将重点研究这些 Pod,探讨在同一个 Pod 中托管多个容器的不同方面,以及如何让这些不同的容器彼此通信。

到目前为止,我们只创建了运行单一容器的 Pod:这些是最简单的 Pod 形式,你将使用这些 Pod 来管理最简单的应用程序。我们还发现了如何通过使用 kubectl 命令行工具对这些 Pod 执行简单的 创建读取更新和删除CRUD)操作来更新和删除它们。

除了掌握 CRUD 操作的基础知识,你还需要学会如何访问 Kubernetes 集群中正在运行的 Pod。

虽然单容器 Pod 更为常见,但也有一些情况,使用单个 Pod 中的多个容器会更有益。例如,在 Pod 内使用专门的容器来处理日志收集,或者使用另一个专用容器来实现服务之间的代理通信。本章将进一步探讨如何管理多个容器的 Pod,并学习如何处理这些 Pod。当 Pod 用于启动不止一个容器时,你所学到的一切都将适用于多容器 Pod。在原始的 Pod 管理方面没有太大不同,因为更新和删除 Pod 的操作没有区别,无论 Pod 包含多少个容器。

除了这些基本操作外,我们还将讲解如何访问多容器 Pod 中的特定容器以及如何访问其日志。当一个 Pod 包含多个容器时,你需要运行一些特定的命令和参数来访问它,这正是我们将在本章中讨论的内容。

我们还将探索一些重要的设计模式,例如大使(Ambassador)、边车(Sidecar)和适配器(Adapter)容器。你需要学习这些架构,以有效管理多容器 Pod。你还将学习如何处理 Kubernetes 中的卷。Docker 也提供卷,但在 Kubernetes 中,卷用于在由同一个 Pod 启动的容器之间共享数据,这将是本章的一个重要部分。学习完本章后,你将能够在 Kubernetes Pod 内启动复杂应用程序。

本章将涵盖以下主要内容:

  • 理解多容器 Pod

  • 在同一个 Pod 中共享容器之间的卷

  • 大使设计模式

  • 边车设计模式

  • 适配器设计模式

  • 边车与 Kubernetes 本地边车

技术要求

本章需要以下前置条件:

  • 一个有效的 kubectl 命令行工具。

  • 一个本地或基于云的 Kubernetes 集群用于实践。

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter05

理解什么是多容器 Pod

多容器 Pod 是将紧密耦合的应用程序打包在一起的方式,适用于 Kubernetes。这使得多个容器可以共享资源并轻松地相互通信,非常适合像边车(sidecar)和服务网格(service mesh)这样的场景。在本节中,我们将通过讨论一些具体的多容器 Pod 示例,来了解管理多个容器时的 Pod 核心概念。

需要使用多容器 Pod 的具体场景

当容器需要紧密关联时,你应该将它们分组为一个 Pod。从更广义的角度来看,一个 Pod 必须对应于在 Kubernetes 集群中运行的一个应用程序或进程。如果你的应用程序需要多个容器才能正常工作,那么这些容器应该通过一个 Pod 来启动和管理。

当这些容器需要协同工作时,应该将它们分组为一个 Pod。需要记住的是,Pod 不能跨多个计算节点。因此,如果你创建了一个包含多个容器的 Pod,那么所有这些容器都会被创建在同一个计算节点上。为了理解何时以及如何使用多容器 Pod,可以通过以下两个简单应用程序的示例来进行说明:

  • 日志转发器:在这个示例中,假设你已经部署了一个如 NGINX 这样的 Web 服务器,并将其日志存储在一个专用目录中。你可能希望收集并转发这些日志。为此,你可以将像 Splunk 转发器这样的工具作为容器,部署在与 NGINX 服务器相同的 Pod 中。

这些日志转发工具用于将日志从源位置转发到目标位置,通常会部署像 Splunk、Fluentd 或 Filebeat 这样的代理,来从容器中获取日志并将其转发到一个中心位置,比如 Elasticsearch 集群。在 Kubernetes 环境中,这通常是通过运行一个多容器 Pod 来实现,其中一个容器专门用于运行应用程序,另一个容器专门用于获取日志并将其发送到其他地方。将这两个容器由同一个 Pod 管理,可以确保它们在同一个节点上启动,并且同时运行。

  • 代理服务器:想象一个与主应用程序位于同一 Pod 中的 NGINX 反向代理容器,它高效地处理流量路由和安全性,使用自定义规则。这一概念扩展到服务网格,在这里像 Envoy 这样的专用代理可以与应用程序容器一起部署,从而在微服务架构中启用负载均衡和服务发现等功能。(我们将在第八章《通过服务暴露 Pod》详细学习服务网格。)通过将这两个容器捆绑在同一个 Pod 中,你将得到两个 Pod 运行在同一个节点上。你也可以在同一个 Pod 中运行第三个容器,将其他两个容器发出的日志转发到中央日志位置!这是因为 Kubernetes 对同一个 Pod 中的容器数量没有限制,只要你有足够的计算资源来运行它们。

图 5.1:示例多容器 Pod 场景

通常,每当多个容器一起工作并紧密耦合时,你应该将它们放在一个多容器 Pod 中。

现在,让我们来了解如何创建多容器 Pod。

创建一个由两个容器组成的 Pod

在上一章中,我们发现了两种用于操作 Kubernetes 的语法:

  • 命令式语法

  • 声明式语法

本书中我们将要探索的大多数 Kubernetes 对象都可以通过这两种方法创建或更新,但不幸的是,多容器 Pod 的情况并非如此。

当你需要创建一个包含多个容器的 Pod 时,你需要使用声明式语法。这意味着你需要创建一个 YAML 文件,包含 Pod 及其将管理的所有容器的声明,然后通过kubectl apply -f file.yaml来应用它。

请考虑以下存储在~/multi-container-pod.yaml中的 YAML 清单文件:

# multi-container-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  restartPolicy: Never
  containers:
    - name: nginx-container
      image: nginx:latest
    - name: debian-container
      image: debian
      command: ["/bin/sh"]
      args: ["-c", "while true; do date;echo debian-container; sleep 5 ; done"] 

这个 YAML 清单将创建一个由两个容器组成的 Kubernetes Pod:一个基于nginx:latest镜像,另一个基于debian镜像。

要创建它,请使用以下命令:

$ kubectl apply -f multi-container-pod.yaml
pod/multi-container-pod created 

这将导致 Pod 被创建。选定节点上的 kubelet 将拥有容器运行时(例如 containerd、CRI-O 或 Docker 守护进程)来拉取两个镜像并实例化两个容器。

为了检查 Pod 是否正确创建,我们可以运行kubectl get pods

$ kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
multi-container-pod   2/2     Running   0          2m7s 

你还记得来自第二章《Kubernetes 架构——从容器镜像到运行 Pod》中kubelet的角色吗?这个组件运行在 Kubernetes 集群中的每个节点上,负责将从kube-apiserver接收到的 Pod 清单转换为实际的容器。

所有在同一个 Pod 中声明的容器都会被调度或启动在同一个节点上,Pod 不能跨多个机器。

相同 Pod 中的容器是共存的。如果终止 Pod,则所有其容器将一同终止;创建 Pod 时,kubelet 至少会尝试一起创建其所有容器。

通常通过在多个节点上复制多个 Pods 来实现高可用性,这是您稍后在本书中将学习的内容。

从 Kubernetes 的角度来看,应用此文件会导致由两个容器组成的完全工作的多容器 Pod,并且可以通过运行标准的 kubectl get pods 命令从 kube-apiserver 获取 Pod 列表来确保 Pod 正在运行。

您看到前一个 kubectl 命令输出中标有 2/2 的列了吗?这是 Pod 中的容器数量。在这里,它表示成功启动了两个容器!我们可以查看来自不同容器的日志如下。

$ kubectl logs multi-container-pod -c debian-container
Mon Jan  8 01:33:23 UTC 2024
debian-container
Mon Jan  8 01:33:28 UTC 2024
debian-container
...<removed for brevity>...
$ kubectl logs multi-container-pod -c nginx-container
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
...<removed for brevity>...
2024/01/08 01:33:20 [notice] 1#1: start worker process 39
2024/01/08 01:33:20 [notice] 1#1: start worker process 40 

我们学习了如何创建和管理多容器 Pods,接下来,我们将学习当多容器 Pod 失败时如何进行故障排除。

当 Kubernetes 无法启动 Pod 中的一个容器时会发生什么?

Kubernetes 记录了在同一 Pod 中启动的所有容器。但经常发生特定容器无法启动的情况。让我们在 YAML 清单中引入一个打字错误,以演示当某些特定 Pod 的容器无法启动时 Kubernetes 的反应。

在以下示例中,我们定义了一个容器镜像,对于 NGINX 容器根本不存在;请注意 nginx:i-do-not-exist 标签:

# failed-multi-container-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  restartPolicy: Never
  containers:
    - name: nginx-container
      image: nginx:i-do-not-exist
    - name: debian-container
      image: debian
      command: ["/bin/sh"]
      args: ["-c", "while true; do date;echo debian-container; sleep 5 ; done"] 

现在,我们可以使用 kubectl apply -f failed-multi-container-pod.yaml 命令应用以下容器:

$ kubectl apply -f failed-multi-container-pod.yaml
pod/failed-multi-container-pod created 

在这里,您可以看到 Pod 已有效创建。这是因为即使存在不存在的镜像,从 Kubernetes 的角度来看,YAML 仍然有效。因此,Kubernetes 简单地创建 Pod 并将条目持久化到 etcd 中,但我们可以轻松想象,当 kubelet 尝试从容器注册表(例如 Docker Hub)中检索镜像以启动容器时会遇到错误。

让我们使用 kubectl get pod 命令检查 Pod 的状态:

$ kubectl get pod
NAME                         READY   STATUS             RESTARTS   AGE
failed-multi-container-pod   1/2     ImagePullBackOff   0          93s 

正如您所见,Pod 的状态为 ImagePullBackOff。这意味着 Kubernetes 正试图启动 Pod,但由于镜像访问问题而失败。要查明失败原因,您必须使用以下命令描述 Pod:kubectl describe pod failed-multi-container-pod

$  kubectl describe pod failed-multi-container-pod
Name:             failed-multi-container-pod
Namespace:        default
...<removed for brevity>...
Events:
  Type     Reason     Age                    From               Message
  ----     ------     ----                   ----               -------
...<removed for brevity>...
  Warning  Failed     5m23s (x3 over 6m13s)  kubelet            Error: ErrImagePull
  Warning  Failed     4m55s (x5 over 6m10s)  kubelet            Error: ImagePullBackOff
  Normal   Pulling    4m42s (x4 over 6m17s)  kubelet            Pulling image "nginx:i-do-not-exist"
  **Warning  Failed     4m37s (x4 over 6m13s)  kubelet            Failed to pull image "nginx:i-do-not-exist": Error response from daemon: manifest for nginx:i-do-not-exist not found: manifest unknown: manifest unknown**
  Normal   BackOff    75s (x19 over 6m10s)   kubelet            Back-off pulling image "nginx:i-do-not-exist" 

稍微有点难以阅读,但通过跟随此日志,您可以看到 debian-container 是正常的,因为 kubelet 已成功创建它,如前一输出的最后一行所示。但另一个容器存在问题,即 nginx-container

在这里,您可以看到输出错误为 ErrImagePull,正如您猜测的那样,它表示无法启动容器,因为镜像拉取无法检索到 nginx:i-do-not-exist 镜像标签。

因此,Kubernetes 执行以下操作:

  1. 首先,如果 YAML 文件中的 Pod 有效,它会在 etcd 中创建条目。

  2. 然后,它只是尝试启动容器。

  3. 如果遇到错误,它将反复尝试重新启动失败的容器。

如果其他容器正常工作也没问题。然而,由于失败的容器,你的 Pod 永远无法进入 Running 状态。毕竟,你的应用程序肯定需要失败的容器才能正常工作;否则,这个容器根本不该存在!

现在,让我们学习如何删除一个多容器 Pod。

删除多容器 Pod

当你想删除包含多个容器的 Pod 时,你必须通过kubectl delete命令,就像删除单容器 Pod 一样。

然后,你有两个选择:

  • 你可以通过使用 -f 选项来指定 YAML 清单文件的路径。

  • 如果你知道 Pod 的名称,可以不使用 YAML 路径直接删除它。

第一种方式是指定 YAML 清单文件的路径。你可以使用以下命令来执行:

$ kubectl delete -f multi-container-pod.yaml 

否则,如果你已经知道 Pod 的名称,可以按以下方式进行操作:

$ kubectl delete pods/multi-pod
$ # or equivalent
$ kubectl delete pods multi-pod 

要弄清楚 Pod 的名称,可以使用 kubectl get 命令:

$ kubectl get pod
NAME                         READY   STATUS             RESTARTS   AGE
failed-multi-container-pod   1/2     ImagePullBackOff   0          13m 

当我们运行它们时,集群中只创建了 failed-multi-container-pod,所以你在输出中只看到一行。

这里是如何在不指定创建它的 YAML 文件的情况下命令式地删除 failed-multi-container-pod

$ kubectl delete -f failed-multi-container-pod.yaml
pod "failed-multi-container-pod" deleted 

几秒钟后,Pod 会从 Kubernetes 集群中移除,所有容器也会从容器守护进程和 Kubernetes 集群节点中移除。

在命令发出之前,Pod 的名称被删除并释放的时间称为 宽限期。让我们来了解如何处理它!

理解 Pod 删除的宽限期

删除 Pods 相关的一个重要概念是所谓的宽限期。单容器 Pod 和多容器 Pod 都有这个宽限期,在删除它们时可以观察到。通过传递 --grace-period=0 --force 选项到 kubectl delete 命令,可以忽略这个宽限期。

在删除 Pod 的过程中,某些 kubectl 命令会显示其状态为 Terminating。值得注意的是,这个 Terminating 状态并不属于标准的 Pod 阶段。Pod 会被分配一个专门的宽限期以实现优雅终止,通常设置为 30 秒。要强制终止 Pod,可以使用 --force 标志。当通过设置 --grace-period=0--force 标志来强制删除时,Pod 的名称会立即释放,并可供另一个 Pod 使用。在非强制删除的情况下,宽限期会被尊重,Pod 的名称会在其被有效删除后释放。

$ kubectl delete pod failed-multi-container-pod --grace-period=0 --force
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "failed-multi-container-pod" force deleted 

如果你不知道自己在做什么,使用这个命令时需要小心。强制删除 Pod 不应被视为常规操作,因为正如输出所示,你无法确保 Pod 已被有效删除。如果由于某种原因 Pod 无法被删除,它可能会无限期运行,因此如果你不确定该怎么做,请不要运行这个命令。

现在,让我们来了解如何访问运行在多容器 Pod 中的特定容器。

访问多容器 Pod 中的特定容器

当你在同一个 Pod 中有多个容器时,可以单独访问每个容器。在这里,我们将访问我们多容器 Pod 中的 NGINX 容器。让我们从重新创建它开始,因为我们在之前的例子中已经删除了它:

$ kubectl apply -f multi-container-pod.yaml
pod/multi-container-pod created 

要访问一个正在运行的容器,你需要使用 kubectl exec 命令,就像在没有 Kubernetes 的 Docker 环境中使用 docker exec 在已创建的容器中启动命令一样。

该命令会要求提供两个重要参数:

  • 包裹你想要访问的容器的 Pod

  • 容器本身的名称,如 YAML 清单文件中所写的那样

我们已经知道 Pod 的名称,因为我们可以轻松地通过 kubectl get 命令检索它。在我们的例子中,Pod 被命名为 multi-container-pod

然而,我们无法获取容器的名称,因为没有 kubectl get 容器命令来列出正在运行的容器。这就是为什么我们必须使用 kubectl describe pods/multi-container-pod 命令来查看该 Pod 中包含了什么:

$ kubectl describe pods/multi-container-pod 

该命令将显示目标 Pod 中所有容器的名称。在这里,我们可以看到我们的 Pod 正在运行两个容器,一个名为 debian-container,另一个名为 nginx-container

此外,以下是列出某个专用 Pod 中所有容器名称的命令:

$ kubectl get pod/multi-container-pod -o jsonpath="{.spec.containers[*].name}"
nginx-container debian-container 

这个命令将避免你使用 describe 命令。然而,它使用了 jsonpath,这是 kubectl 的高级功能:这个命令看起来可能有些奇怪,但它主要由一个排序过滤器组成,应用于该命令。jsonpath 表达式 {.spec.containers[*].name} 可以与 kubectl get pod 命令一起使用,获取特定 Pod 中所有容器的名称。. 表示整个响应对象,而 spec.containers 指向 Pod 规范中的容器部分。[*] 操作符指示 jsonpath 遍历容器列表中的所有元素,.name 提取每个容器对象的 name 属性。本质上,这个表达式返回指定 Pod 中容器名称的逗号分隔列表。

jsonpath 过滤器不容易正确编写,因此,可以将此命令添加为 bash 别名或记下来,因为它非常有用。

无论如何,我们现在可以看到在 multi-container-pod Pod 中有这两个容器:

  • nginx-container

  • busybox-container

现在,让我们访问nginx-container。你可以在目标 Pod 中找到目标容器的名称,然后使用以下命令访问该 Pod:

$ kubectl exec -it multi-container-pod --container nginx-container -- /bin/bash
root@multi-container-pod:/# hostname
multi-container-pod
root@multi-container-pod:/# 

运行此命令后,你会发现自己已经进入了nginx-container。让我们稍微解释一下这个命令。kubectl exec的作用和docker exec一样:

  • kubectl exec:这是在 Kubernetes 容器中执行命令的命令。

  • -it:这些是执行命令的选项。-t分配一个伪终端,-i允许与容器进行交互。

  • multi-container-pod:这是你希望执行命令的 Pod 的名称。

  • --container nginx-container:这指定了在 Pod 中执行命令的容器。在多容器的 Pod 中,你需要指定你想要交互的容器。

  • -- /bin/bash:这是将在指定容器中执行的实际命令。它启动一个交互模式的 Bash shell(/bin/bash),允许你与容器的命令行进行交互。

当你运行这个命令时,你将进入多容器 Pod 内容器的 Shell,此时你准备好在 Kubernetes 集群中的这个特定容器内运行命令了。

与单容器 Pod 的情况相比,主要的不同是--container选项(-c短选项也有效)。你需要传递这个选项,告诉kubectl你要访问哪个容器。

现在,让我们来学习如何在你的 Pod 中的容器中运行命令!

在容器中运行命令

Kubernetes 的一个强大之处是,你可以随时访问运行在 Pod 中的容器,以执行一些命令。我们之前做过这个,但你知道吗,你也可以直接从kubectl命令行工具中执行任何你想要的命令?

首先,我们将重新创建多容器 Pod:

$ kubectl apply -f multi-container-pod.yaml
pod/multi-container-pod created 

要在容器中运行命令,你需要使用kubectl exec,就像我们之前做的那样。但这次,你必须去掉-ti参数,以防止kubectl附加到你正在运行的终端会话。

在这里,我们正在运行ls命令,以列出nginx-container容器中的文件,来自multi-container-pod Pod:

$ kubectl exec pods/multi-container-pod -c nginx-container -- ls
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
...<removed for brevity> 

你可以省略容器名称,但如果这么做,kubectl将使用默认的第一个容器。

接下来,我们将学习如何覆盖容器运行的命令。

覆盖容器运行的默认命令

在多容器 Pod 中,覆盖默认命令非常重要,因为它可以让您单独控制每个容器的行为。这意味着您可以定制 Pod 中每个容器的工作方式。例如,一个 Web 服务器容器通常会运行 start server 命令,但您可以覆盖旁车容器的命令,使其处理日志记录。此方法还可以帮助资源管理。如果一个容器通常运行一个资源密集型的进程,您可以将其更换为一个更轻的进程,从而确保其他容器有足够的资源。最后,这有助于依赖关系管理。例如,一个数据库容器可能通常会立即启动,但您可以覆盖它的命令,直到相关的应用容器准备好为止。

使用 Docker 时,您可以编写名为 Dockerfile 的文件来构建容器镜像。Dockerfile 使用两个关键字来告诉我们,当通过 docker run 命令创建容器时,使用此镜像构建的容器将启动哪些命令和参数。这两个关键字是 ENTRYPOINTCMD

  • ENTRYPOINT 是容器将要启动的主命令。

  • CMD 用于替换传递给 ENTRYPOINT 命令的参数。

例如,一个经典的 Dockerfile,应当启动并运行 sleep 命令 30 秒,代码如下:

# ~/Dockerfile
FROM busybox:latest
ENTRYPOINT ["sleep"]
CMD ["30"] 

Containerfile 和 Podman

Containerfile 就像 Dockerfile 一样,是构建容器镜像的配方。它包含一系列指令,指定操作系统、安装依赖项、复制应用程序代码和配置设置。Podman 是一个类似于 Docker 的工具,它可以解析此 Containerfile 并根据指令构建镜像。

CMD argument is what you can pass to the docker run command. If you build this image with this Dockerfile using the docker build command, you’ll end up with a BusyBox image that just runs the sleep command (ENTRYPOINT) when the docker run command is run for 30 seconds (the CMD argument).

通过 CMD 指令,您可以覆盖默认的 30 秒,如下所示:

$ docker run my-custom-ubuntu:latest 60
$ docker run my-custom-ubuntu:latest # Just sleep for 30 seconds 

另一方面,Kubernetes 允许我们通过 YAML Pod 定义文件来覆盖 ENTRYPOINTCMD。为此,您需要在 YAML 配置文件中添加两个可选键:commandargs

这是 Kubernetes 带来的一个非常大的好处,因为您可以决定向容器的 Dockerfile 运行命令附加参数,就像裸 Docker 中的 CMD 参数一样,或者完全覆盖 ENTRYPOINT

在这里,我们将编写一个新的清单文件,覆盖 busybox 镜像的默认 ENTRYPOINTCMD 参数,使得 busybox 容器休眠 60 秒。操作步骤如下:

# nginx-debian-with-custom-command-and-args
apiVersion: v1
kind: Pod
metadata:
  name: nginx-debian-with-custom-command-and-args
spec:
  restartPolicy: Never
  containers:
    - name: nginx-container
      image: nginx:latest
    - name: debian-container
      image: debian
      command: ["sleep"] # Corresponds to the ENTRYPOINT
      args: ["60"] # Corresponds to CMD 

这有点难理解,因为 Dockerfile 中的 ENTRYPOINT 对应 YAML 清单文件中的 command 参数,而 Dockerfile 中的 CMD 对应 YAML 清单文件中的 args 配置项。

如果你遗漏其中之一会怎样?Kubernetes 会默认使用容器镜像中的内容。如果你在 YAML 中省略了 args 键,Kubernetes 会使用 Dockerfile 中提供的 CMD,而如果你省略了 command 键,Kubernetes 会使用 Dockerfile 中声明的 ENTRYPOINT。大多数情况下,或者至少当你对容器的 ENTRYPOINT 感到满意时,你只需要覆盖 args 文件(CMD Dockerfile 指令)。

当我们创建 Pod 时,我们可以查看输出,如下所示:

$ kubectl apply -f nginx-debian-with-custom-command-and-args.yaml
pod/nginx-debian-with-custom-command-and-args created
$ kubectl get po -w
NAME                                        READY   STATUS     RESTARTS   AGE
nginx-debian-with-custom-command-and-args   0/2     ContainerCreating   0          2s
nginx-debian-with-custom-command-and-args   2/2     Running             0          6s
nginx-debian-with-custom-command-and-args   1/2     NotReady            0          66s 

因此,覆盖默认命令为多容器 Pod 中的容器行为提供了更精细的控制。这使得在 Pod 操作中能够实现量身定制的功能、资源优化和依赖管理。在本节中,我们了解到 Kubernetes 允许通过 Pod YAML 定义中的commandargs字段来覆盖默认值。

现在,让我们来看一下另一个功能:initContainers!在下一节中,你将看到另一种方式,来在 Pod 中执行一些额外的侧容器以配置主容器。

引入 initContainers

initContainers是 Kubernetes Pods 提供的一项功能,用于在实际容器启动之前运行设置脚本。你可以将它们看作是你可以在 Pod YAML 清单文件中定义的额外侧容器:它们会在 Pod 创建时首先运行。然后,一旦它们完成,Pod 开始创建其主容器。

你可以在同一个 Pod 中执行不止一个 initContainers,但当你定义多个时,请记住它们会一个接一个地运行,而不是并行运行。一旦一个 initContainer 完成,下一个就会开始,依此类推。一般来说,initContainers 用于准备工作;其中一些任务在以下列表中列出:

  • 数据库初始化:在主应用程序启动之前,设置并配置数据库。

  • 配置文件下载:从远程位置下载必要的配置文件。

  • 包安装:安装主应用程序所需的依赖项。

  • 等待外部服务:在启动主容器之前,确保外部服务可用。

  • 运行前检查:在启动主应用程序之前,执行必要的检查或验证。

  • 机密管理:安全地下载并注入机密到主容器的环境中。

  • 数据迁移:在主应用程序启动之前,将数据迁移到数据库或存储系统。

  • 自定义文件权限:为主应用程序设置适当的文件权限。

由于 initContainers 可以有自己的容器镜像,你可以通过保持主容器镜像尽可能小,来卸载一些配置,从而通过从主容器镜像中移除不必要的工具,增强整个设置的安全性。以下是一个引入 initContainer 的 YAML 清单:

# nginx-with-init-container.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-init-container
  labels:
    environment: prod
    tier: frontend
spec:
  restartPolicy: Never
  volumes:
    - name: website-volume
      emptyDir: {}
  initContainers:
    - name: download-website
      image: busybox
      command:
        - sh
        - -c
        - |
          wget https://github.com/iamgini/website-demo-one-page/archive/refs/heads/main.zip -O /tmp/website.zip && \
          mkdir /tmp/website && \
          unzip /tmp/website.zip -d /tmp/website && \
          cp -r /tmp/website/website-demo-one-page-main/* /usr/share/nginx/html
      volumeMounts:
        - name: website-volume
          mountPath: /usr/share/nginx/html
  containers:
    - name: nginx-container
      image: nginx:latest
      volumeMounts:
        - name: website-volume
          mountPath: /usr/share/nginx/html 

正如你从这个 YAML 文件中看到的,initContainer运行 BusyBox 镜像,它会下载应用程序(在此案例中为github.com/iamgini/website-demo-one-page中的简单网站内容),并将相同的应用程序复制到一个名为website-volume的共享卷中。(你将在第九章Kubernetes 中的持久存储中学习卷和持久存储。)同一个卷还被配置为挂载在 NGINX 容器下,以便 NGINX 使用它作为默认的网站内容。initContainer执行完毕后,Kubernetes 将创建nginx-container容器。

请记住,如果initContainer失败,Kubernetes 将不会启动主容器。重要的是不要把initContainer当作可选组件,或者认为它可以失败。如果在 YAML 清单文件中包含了它们,它们是必需的,而且它们的失败会阻止主容器的启动!

让我们创建 Pod。创建后,我们将运行kubectl get Pods -w命令,以便kubectl可以监控 Pod 列表中的变化。该命令的输出将定期更新,显示 Pod 状态的变化。请注意status列,显示initContainer正在运行:

$ kubectl apply -f nginx-with-init-container.yaml
pod/nginx-with-init-container created
$ kubectl get po -w
NAME                        READY   STATUS     RESTARTS   AGE
nginx-with-init-container   0/1     Init:0/1   0          3s
nginx-with-init-container   0/1     Init:0/1   0          4s
nginx-with-init-container   0/1     PodInitializing   0          19s
nginx-with-init-container   1/1     Running           0          22s 

如你所见,Init:0/1表示initContainer正在启动。完成后,Init:前缀消失,显示下一个状态,表示我们已完成initContainer,Kubernetes 现在正在创建主容器——在我们的例子中是 NGINX 容器!

如果你想进一步探索,你可以通过以下方式使用 NodePort 服务暴露 Pod:

$ kubectl expose pod nginx-with-init-container --port=80 --type=NodePort
service/nginx-with-init-container exposed 

现在,使用kubectl port-forward命令启动端口转发服务,以便我们可以在集群外部访问该服务:

$ kubectl port-forward pod/nginx-with-init-container 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

现在,访问http://localhost:8080,你将看到一个一页式网站,内容来自于github.com/iamgini/website-demo-one-page。我们将在第八章通过服务暴露你的 Pods中学习如何暴露服务。并且在继续到下一部分之前,记得通过按下ctrl + c停止端口转发。

在构建 Pods 时,明智地使用initContainer!你不一定必须使用init容器,但它们在运行配置脚本或在启动实际容器之前从外部服务器拉取内容时非常有用!

现在,让我们学习如何访问正在运行的 Pod 中某个特定容器的日志!

访问特定容器的日志

当在单个 Pod 中使用多个容器时,你可以检索 Pod 中某个专用容器的日志。正确的操作方式是使用kubectl logs命令。

容器化应用程序暴露日志的最常见方式是将日志发送到stdoutkubectl logs命令能够流式传输专门容器中stdout属性的内容,并从容器中检索应用程序日志。为了使其正常工作,你需要知道容器及其父 Pod 的精确名称,就像我们使用kubectl exec访问特定容器时一样。

请阅读上一节,访问多容器 Pod 中的特定容器,了解如何执行此操作:

$ kubectl logs -f pods/multi-container-pod --container nginx-container 

请注意--container选项(-c短选项也可以使用),它指定了你想要获取日志的容器。注意,这对于initContainers同样适用:你必须将其名称传递给此选项以检索其日志。

请记住,如果你没有传递--container选项,你将检索 Pod 中所有容器的所有日志。如果 Pod 中只有一个容器,这时不传递该选项是有用的,但在使用多个容器的 Pod 时,你每次都应该考虑传递这个选项。

在访问 Pod 中容器日志时,还有其他一些有用的选项你需要了解。你可以通过以下命令来检索过去两小时内写入的日志:

$ kubectl logs --since=2h pods/multi-container-pod --container nginx-container 

此外,你还可以使用--tail选项来检索日志输出中最近的几行。以下是操作方法:

$ kubectl logs --tail=30 pods/multi-container-pod --container nginx-container 

在这里,我们正在检索nginx-container日志输出中的 30 行最新内容。

现在,你已经准备好读取并检索 Kubernetes Pod 中的日志,无论它们由一个容器还是多个容器组成!

在本节中,我们学习了如何创建、更新和删除多容器 Pod。我们还学习了如何强制删除 Pod。然后我们发现了如何访问 Pod 中的特定容器,以及如何检索 Pod 中特定容器的日志。尽管我们在 Pod 中创建了 NGINX 和 Debian 容器,但它们之间的连接较弱,因为它们没有协同工作。为了改善这一点,我们将学习如何处理卷,以便在两个容器之间共享文件。

在同一 Pod 中的容器之间共享卷

在本节中,我们将从 Kubernetes 的角度了解卷是什么以及如何使用它们。Docker 也有卷,但它们与 Kubernetes 卷不同:它们满足相同的需求,但并不相同。

我们将了解 Kubernetes 卷是什么,它们为什么有用,以及它们在处理 Kubernetes 卷时如何帮助我们。

什么是 Kubernetes 卷?

我们将解答一个简单的问题。我们的多容器 Pod 目前由两个容器组成:一个 NGINX 容器和一个 Debian 容器。我们将尝试通过将 NGINX 容器中的日志目录挂载到 Debian 容器中的目录来共享日志目录。这样,我们将创建两个容器之间的关系,使它们共享一个目录。

Kubernetes 有两种类型的卷:

  • 卷,这是我们将在这里讨论的内容。

  • PersistentVolume,这是一个更高级的功能,我们将在第九章Kubernetes 中的持久存储中讨论。

请记住,这两者并不相同。PersistentVolume 是一个独立的资源,而“卷”是 Pod 配置。顾名思义,PersistentVolume 是持久的,而卷不应是持久的。但请记住,这并不总是如此!

简单来说,Kubernetes 中的卷与 Pod 的生命周期密切相关。当你实例化一个 Pod 时,可以定义并将卷连接到其中的容器。实质上,卷表示与 Pod 存在性相关联的存储。一旦 Pod 被删除,任何关联的卷也会被删除。

尽管卷的用途远超这一场景,但需要注意的是,这一描述并不适用于所有情况。然而,你可以将卷视为一种特别有效的方式,用于促进同一 Pod 中多个容器之间共享目录和文件。

记住,卷与 Pod 的生命周期绑定,而不是容器的生命周期。如果一个容器崩溃,卷会继续存在,因为容器崩溃不会导致其父 Pod 崩溃,因此不会删除任何卷。只要 Pod 存活,其卷也会存在。

卷是管理容器化应用程序中数据的核心概念。它们提供了一种使数据独立于容器生命周期而持久化的方式。Kubernetes 支持各种类型的卷,包括从主机文件系统、云提供商和网络存储系统挂载的卷。

然而,Kubernetes 通过引入对各种驱动程序的支持,扩展了这一点,使得 Pod 卷能够与外部解决方案进行集成。例如,AWS EBS(弹性块存储)卷可以无缝作为 Kubernetes 卷。一些广泛使用的解决方案包括:

  • hostPath

  • emptyDir

  • nfs

  • persistentVolumeClaim(当你需要使用一个PersistentVolume,但这超出了本章的范围)

请注意,在最新的 Kubernetes 版本中,某些旧的卷类型已被移除或弃用;更多信息请参阅文档(kubernetes.io/docs/concepts/storage/volumes/)。以下是一些示例:

  • azureDisk(已移除)

  • gcePersistentDisk(已移除)

  • glusterfs(已移除)

  • azureFile(已弃用)

请注意,使用外部解决方案来管理 Kubernetes 卷将要求您遵循这些外部解决方案的要求。例如,使用 AWS EBS 卷作为 Kubernetes 卷将要求您的 Pod 运行在 Kubernetes 工作节点上,这通常是一个 EC2 实例。原因在于 AWS EBS 卷只能附加到 EC2 实例。因此,利用此类卷的 Pod 需要在 EC2 实例上启动。有关更多信息,请参阅kubernetes.io/docs/concepts/storage/volumes/

以下图表展示了关于hostPathemptyDir卷的高层次概念。

图 5.2:hostPath 和 emptyDir 卷

我们将在这里介绍两种基本的卷驱动器:emptyDirhostPath。我们还将讨论persistentVolumeClaim,因为与其他卷相比,它有些特殊,并将在第九章《Kubernetes 中的持久存储》中进行全面介绍。

现在,让我们开始发现如何在同一个 Pod 中的容器之间使用emptyDir卷类型共享文件!

创建并挂载一个 emptyDir 卷

正如名称所示,它只是一个在 Pod 创建时初始化的空目录,你可以将其挂载到 Pod 中每个容器的位置。

这无疑是最简单和最直接的方式,让你的容器之间共享数据。让我们创建一个 Pod 来管理两个容器。

在以下示例中,我们将创建一个 Pod,该 Pod 将启动两个容器,和之前一样,它将是一个 NGINX 容器和一个 Debian 容器。我们将覆盖 Debian 容器启动时执行的命令,以防止它完成。通过这种方式,我们将让它作为一个长时间运行的进程持续运行,并能够启动额外的命令来检查我们的emptyDir是否已正确初始化。

两个容器将有一个共同的卷挂载在/var/i-am-empty-dir-volume/,这个就是我们的emptyDir卷,它在同一个 Pod 中初始化。以下是创建 Pod 的 YAML 文件:

# multi-container-with-emptydir-pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-with-emptydir-pod
spec:
  containers:
    - name: nginx-container
      image: nginx:latest
      volumeMounts:
      - mountPath: /var/i-am-empty-dir-volume
        name: empty-dir-volume
    - name: debian-container
      image: debian
      command: ["/bin/sh"]
      args: ["-c", "while true; do sleep 30; done;"] # Prevents container from exiting after completion
      volumeMounts:
      - mountPath: /var/i-am-empty-dir-volume
        name: empty-dir-volume
  volumes:
  - name: empty-dir-volume # name of the volume
    emptyDir: {} # Initialize an empty directory # The path on the worker node. 

请注意,我们将在 Kubernetes 集群中创建的对象会随着本示例的进行而变得更加复杂,正如你想象的那样,大多数复杂的事情无法仅通过命令式命令来实现。这就是为什么你会看到越来越多的示例依赖于 YAML 清单文件的原因:你应该养成尝试阅读它们以弄清楚它们所做的事情的习惯。

现在,我们可以使用以下kubectl apply -f命令来应用清单文件:

$ kubectl apply -f multi-container-with-emptydir-pod.yaml
pod/multi-container-with-emptydir-pod created 

现在,我们可以通过执行kubectl get Pods命令来检查 Pod 是否成功运行:

$ kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
multi-container-with-emptydir-pod   2/2     Running   0          25s 

现在我们确认 Pod 正在运行,并且 NGINX 和 Debian 容器都已启动,我们可以通过执行ls命令来检查在两个容器中是否可以访问该目录。

如果命令没有失败,正如我们之前看到的,我们可以通过运行 kubectl exec 命令在容器中执行 ls 命令。正如你记得的,命令需要 Pod 的名称和容器的名称作为参数。我们将执行两次,以确保卷在两个容器中都已挂载:

$ kubectl exec multi-container-with-emptydir-pod -c debian-container -- ls /var
backups
cache
i-am-empty-dir-volume
lib
local
lock
log
mail
opt
run
spool
tmp
$ kubectl exec multi-container-with-emptydir-pod -c nginx-container  -- ls /var
backups
cache
i-am-empty-dir-volume
lib
local
lock
log
mail
opt
run
spool
tmp 

如你所见,ls /var 命令显示了两个容器中的文件名!这意味着 emptyDir 已经初始化并正确地挂载到两个容器中。

现在,让我们在两个容器中的其中一个创建一个文件。该文件应该立即在另一个容器中可见,证明卷挂载正常工作!

在以下命令中,我们只是简单地在挂载目录中创建一个名为 hello-world.txt.txt 文件:

$ kubectl exec multi-container-with-emptydir-pod -c debian-container -- bin/sh -c "echo 'hello world' >> /var/i-am-empty-dir-volume/hello-world.txt"
$ kubectl exec multi-container-with-emptydir-pod -c nginx-container -- cat /var/i-am-empty-dir-volume/hello-world.txt
hello world
$ kubectl exec multi-container-with-emptydir-pod -c debian-container -- cat /var/i-am-empty-dir-volume/hello-world.txt
hello world 

如你所见,我们使用 debian-container 创建了 /var/i-am-empty-dir-volume/hello-world.txt 文件,该文件包含了 hello-world 字符串。然后,我们只是使用 cat 命令从两个容器中访问该文件;你可以看到,在两种情况下,文件都可以访问。再次提醒,记住 emptyDir 卷完全与 Pod 的生命周期绑定。如果 Pod 声明已销毁,那么卷也会被销毁,所有内容也会丢失,并且将无法恢复!

现在,我们将介绍另一种卷类型:hostPath 卷。正如你所想,它将是一个目录,你可以将其挂载到容器上,该目录由宿主机上的路径支持——也就是运行 Pod 的 Kubernetes 节点!

创建并挂载一个 hostPath

正如其名称所示,hostPath 允许你将宿主机上的目录挂载到 Pod 中的容器上!宿主机是执行 Pod 的 Kubernetes 计算节点(或控制节点)。以下是一些示例:

  • 如果你的集群是基于 minikube(一个单节点集群),那么宿主机就是你的本地机器。

  • 在 Amazon EKS 上,宿主机将是一个 EC2 实例。

  • kubeadm 集群中,宿主机通常是一个标准的 Linux 机器。

宿主机是运行 Pod 的机器,你可以将宿主机文件系统中的目录挂载到 Kubernetes Pod 中!

在以下示例中,我们将在基于 minikube 的 Kubernetes 集群上进行操作,因此 hostPath 将是你在电脑上创建的目录,然后将其挂载到 Kubernetes Pod 中。

使用 hostPath 卷类型可能会有用,但在 Kubernetes 的世界中,它可以被认为是一种反模式。虽然 hostPath 卷类型很方便,但由于其可移植性差和潜在的安全风险,它在 Kubernetes 中是不推荐使用的。它也可能与像 SELinux 多类别安全 (MCS) 等高级安全功能不兼容,而这类功能现在已被许多 Kubernetes 发行版支持。为了实现更具可移植性、安全性和未来兼容性的解决方案,建议使用 持久化卷 (PVs) 和 持久化卷声明 (PVCs) 来管理容器化应用中的持久数据。

Pods 的整体理念是,它们应该能够轻松删除并重新调度到另一个工作节点上而不产生问题。使用hostPath会在 Pod 和工作节点之间创建紧密的关系,如果你的 Pod 失败并被重新调度到一个主机上没有所需路径的节点,可能会导致重大问题。

现在,让我们探索如何创建 hostPath

假设我们在工作节点上的 worker-node/nginx.conf 有一个文件,我们希望将它挂载到 nginx 容器的 /var/config/nginx.conf 中。

这是用于创建该设置的 YAML 文件。如你所见,我们在文件底部声明了一个 hostPath 卷,定义了应存在于主机上的路径。现在,我们可以将它挂载到任何需要处理该卷的容器上,位于 containers 块中:

# multi-container-with-hostpath.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-with-hostpath
spec:
  containers:
    - name: nginx-container
      image: nginx:latest
      volumeMounts:
      - mountPath: /foo
        name: my-host-path-volume
    - name: debian-container
      image: debian
      command: ["/bin/sh"]
      args: ["-c", "while true; do sleep 30; done;"] # Prevents container from exiting after completion
  volumes:
  - name: my-host-path-volume
    hostPath:
      path: /tmp # The path on the worker node.
      type: Directory 

如你所见,挂载该值就像我们在上一节中使用 emptyDir 卷类型时所做的那样。通过在 Pod 级别使用卷组合,并在容器级别使用 volumeMounts,你可以在容器上挂载卷。

type: Directory, which means the directory already exists on the host machine. If you want to create the directory or file on the host machine, then use DirectoryOrCreate and FileOrCreate respectively.

你也可以将目录挂载到 Debian 容器中,以便它能够访问主机上的目录。

在运行 YAML 清单文件之前,你需要在主机上创建路径并创建必要的文件:

$ echo "Hello World" >> /tmp/hello-world.txt 

如果你使用的是 minikube 集群,请记住在 minikube 虚拟机中执行此步骤,具体如下:

$ minikube ssh
docker@minikube:~$ echo "Hello World" > /tmp/hello-world.txt
docker@minikube:~$ exit
Logout 

如果你的 minikube 集群是使用 Podman 容器创建的(例如,minikube start --profile cluster2-podman --driver=podman),那么请登录到 minikube Pod 中并创建该文件:

$ sudo podman exec -it minikube /bin/bash
root@minikube:/# cat /tmp/hello-world.txt 

既然路径已经在主机上存在,我们可以将 YAML 文件应用到我们的 Kubernetes 集群中,并在之后立即运行kubectl get Pod命令来检查 Pod 是否已正确创建:

$ kubectl apply -f multi-container-with-hostpath.yaml
pod/multi-container-with-hostpath created
$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
multi-container-with-hostpath   2/2     Running   0          11 

一切看起来都很好!现在,让我们输出应挂载到/foo/hello-world.txt的文件:

$ kubectl exec multi-container-with-hostpath -c nginx-container -- cat /foo/hello-world.txt
Hello World 

我们可以看到本地文件(在 Kubernetes 节点上)通过 hostPath 卷挂载可在容器内使用。

在本章的开始部分,我们发现了多容器 Pods 的不同方面!我们了解到如何创建、更新和删除多容器 Pods,以及如何使用 initContainers、访问日志、重写通过 Pod 资源直接传递给容器的命令行参数,并使用两个基本卷在容器之间共享目录。

现在,我们将结合一些架构原则,并探索与多容器 Pods 相关的一些概念,称为“模式”。

使者设计模式

设计多容器 Pod 时,你可以决定遵循一些架构原则来构建你的 Pod。这些设计原则回答了某些典型需求,其中使者模式就是其中之一。

在这里,我们将探索什么是使者设计模式,学习如何在 Kubernetes Pods 中构建使者容器,并查看它们的具体示例。

什么是 Ambassador 设计模式?

实质上,Ambassador 设计模式适用于多容器 Pod。我们可以在同一个 Pod 中定义两个容器:

  • 第一个容器将被称为主容器。

  • 另一个容器将被称为 Ambassador 容器。

在这个设计模式中,我们假设主容器可能需要访问外部服务进行通信。例如,你可能有一个应用程序,必须与位于 Pod 外部的 SQL 数据库进行交互,并且你需要访问这个数据库以从中检索数据。

图 5.3:Kubernetes 中的 Ambassador 设计模式

这是一个典型的用例,你可以在主容器旁边部署一个适配器容器,两个容器位于同一个 Pod 中。整体思路是让 Ambassador 容器充当代理,将主容器发出的请求转发到数据库服务器。

在这种情况下,Ambassador 容器实际上将充当 SQL 代理。每当主容器想要访问数据库时,它不会直接访问,而是会创建与 Ambassador 容器的连接,后者将充当代理角色。

运行 Ambassador 容器是可以的,但前提是外部 API 不在同一 Kubernetes 集群中。要在另一个 Pod 上运行请求,Kubernetes 提供了强大的机制,称为服务(Services)。我们将在第八章通过服务暴露 Pod 中有机会了解它们。

但为什么你需要通过代理访问外部数据库呢?以下是这种设计模式能为你带来的具体好处:

  • 卸载 SQL 配置

  • 安全套接层/传输层安全SSL/TLS)证书的管理

请注意,拥有 Ambassador 代理并不限于 SQL 代理,这个示例仅展示了这种设计模式能为你带来的好处。请注意,Ambassador 代理仅用于从主容器到其他地方(如数据存储或外部 API)的出站连接,不能视为进入集群的入口点!现在,让我们快速了解如何通过 YAML 文件创建一个 Ambassador SQL 代理。

Ambassador 多容器 Pod —— 一个示例

现在我们了解了 Ambassador 容器,让我们学习如何通过 Kubernetes 创建一个。以下 YAML 清单文件创建一个 Pod,其中创建了两个容器:

  • nginx-app,基于 nginx:latest 镜像

  • sql-ambassador-proxy,基于 mysql-proxy:latest 容器镜像创建

以下示例仅用于展示 Ambassador SQL 代理的概念。如果你想测试完整功能,你应该有一个可以从 Kubernetes 集群访问的 AWS RDS关系数据库服务)实例,并有一个合适的应用程序来测试数据库操作。

# ~/ nginx-with-ambassador.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-ambassador
spec:
  containers:
    - name: mysql-proxy-ambassador-container
      image: mysql-proxy:latest
      ports:
         - containerPort: 3306
      env:
      - name: DB_HOST
        value: mysql.xxx.us-east-1.rds.amazonaws.com
    - name: nginx-container
      image: nginx:latest 

如你所想,开发者的工作是让 NGINX 容器中的应用代码查询使者,而不是查询 Amazon RDS 端点。由于使者容器可以通过环境变量进行配置,所以你可以轻松地将配置变量输入到ambassador-container中。

不要被 YAML 文件中容器的顺序所迷惑。使者容器出现得较早,并不意味着它是 Pod 中的容器。从 Kubernetes 的角度来看,容器这个概念根本不存在——这两个容器是平行运行的容器,它们之间没有层级关系。在这里,我们只是通过 NGINX 容器来访问 Pod,这使得 NGINX 容器成为最重要的容器。

记住,和 NGINX 容器在同一个 Pod 中运行的使者容器使得它可以通过localhost:3306从 NGINX 访问。

在下一节中,我们将学习侧车模式,这是多容器 Pod 中的另一个重要概念。

侧车设计模式

侧车设计模式适用于当你想扩展主容器的功能,而这些功能是主容器本身通常无法实现的。

就像我们为使者容器做的那样,我们将通过一些示例来详细解释侧车设计模式到底是什么。

什么是侧车设计模式?

可以将侧车容器视为主容器的扩展或助手。它的主要作用是扩展主容器,为其带来新的功能,但不改变主容器本身。与使者设计模式不同,主容器甚至可能不知道有一个侧车的存在。

就像使者设计模式一样,侧车设计模式至少需要两个容器:

  • 主容器——运行应用程序的容器。

  • 侧车容器——就是那个给第一个容器带来额外功能的容器。

你可能已经猜到了,但这种模式在你想运行监控或日志转发代理时特别有用。下图展示了一个简单的侧车设计,包含一个主应用容器和一个收集应用日志的侧车容器。

图 5.4:Kubernetes 中的侧车设计模式

当你想构建一个侧车来将日志转发到另一个位置时,有三件事需要理解:

  • 你必须找到主容器写入数据的目录(例如,日志)。

  • 你必须创建一个卷,使得这个目录对侧车容器可访问(例如,日志转发侧车)。

  • 你必须以正确的配置启动侧车容器。

基于这些概念,主容器保持不变,即使侧车失败,也不会影响主容器,主容器仍然可以继续工作。

什么时候使用侧车设计模式?

在考虑使用 sidecar 容器时,它们在以下场景中尤其有用:

  • 网络代理:可以配置网络代理在 Pod 中其他容器之前初始化,确保它们的服务能立即可用。Istio 的“Envoy”代理就是作为代理使用的 sidecar 容器的一个很好的例子。

  • 增强的日志记录:日志收集 sidecar 可以尽早启动并持续运行,直到 Pod 终止,即使在 Pod 崩溃的情况下,也能可靠地捕获日志。

  • 任务:Sidecar 可以与 Kubernetes Jobs 一同部署,而不会影响任务的完成。sidecar 在 Jobs 内运行不需要额外的配置。

  • 凭证管理:许多第三方凭证管理平台利用 sidecar Pods 在工作负载中注入并管理凭证。它们还可以促进安全的凭证轮换和撤销。

Sidecar 多容器 Pod – 示例

就像大使设计模式一样,sidecar 利用多容器 Pod。我们将在同一个 Pod 中定义两个容器,分别是 NGINX 容器(作为应用容器)和Fluentd 容器(作为 sidecar 容器,用于收集 NGINX web 服务器的日志):

# nginx-with-fluentd-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-sidecar
spec:
  containers:
    - name: nginx-container
      image: nginx:latest
      ports:
        - containerPort: 80
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/nginx
    - name: fluentd-sidecar
      image: fluent/fluentd:v1.17
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/nginx
  volumes:
    - name: log-volume
      emptyDir: {} 

请注意,为了使 Fluentd 正常工作,我们需要通过 ConfigMap 传递配置;一个典型的配置可以在以下代码中找到(你将在第七章中学到更多关于 ConfigMap 的内容,使用 ConfigMap 和 Secrets 配置你的 Pods):

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config-map
  namespace: default
data:
  fluentd.conf: |
    <source>
      @type tail
      path /var/log/nginx/*.log
      pos_file /var/log/nginx/nginx.log.pos
      tag nginx
      <parse>
        @type nginx
      </parse>
    </source>
    <match nginx.**>
      @type elasticsearch
      host elastic.lab.example.com
      port 9200
      logstash_format true
      logstash_prefix fluentd
      logstash_dateformat %Y.%m.%d
    </match> 

Fluentd是一个流行的开源日志收集和转发代理,常作为 Kubernetes 部署中的 sidecar 容器使用。它高效地收集来自不同来源的日志,解析其结构,并将其转发到集中式日志平台,如 Elasticsearch、Google Cloud Logging 或 Amazon CloudWatch Logs。这使得日志管理更加简洁,提升了可观察性,并且简化了应用程序健康状况和性能的分析。尽管这个示例展示了将日志发送到一个虚拟的 Elasticsearch 服务器(例如elastic.lab.example.com),Fluentd 仍然提供灵活性,可以根据具体需求集成各种外部日志解决方案。

在本章的下一个部分,我们将讨论适配器设计模式。

适配器设计模式

正如其名字所示,适配器设计模式将把源格式中的输入适配为目标格式。

与大使和 sidecar 设计模式一样,这种模式要求你至少运行两个容器:

  • 第一个是主容器。

  • 第二个是适配器容器。

这种设计模式非常有用,当主容器以格式 A 发出数据,而另一个应用程序期望数据以格式 B 接收时,就应该使用这种模式。正如其名字所示,适配器容器的作用就是进行适配

同样,这种设计模式特别适合日志或监控管理。想象一个 Kubernetes 集群,里面运行着几十个应用程序;它们以 Apache 格式写入日志,你需要将这些日志转换为 JSON 格式,以便可以被搜索引擎索引。这正是适配器设计模式发挥作用的地方。在应用程序容器旁边运行适配器容器可以帮助你在日志被发送到其他地方之前将其适配为源格式。

就像在 sidecar 设计模式中一样,这种方法只有在 Pod 中的两个容器通过卷访问同一个目录时才有效。

适配器多容器 Pod – 示例

在这个示例中,我们将使用一个 Pod,它使用适配器容器,并将共享目录作为 Kubernetes 卷进行挂载。

图 5.5:Kubernetes 中的适配器设计模式

这个 Pod 将运行两个容器:

  • alpine-writer:主应用容器,负责将日志写入/var/log/app

  • log-adapter:适配器容器,它将读取日志并将其转换为另一种格式(例如,在每条日志末尾添加PROCESSED字符串)。

以下的 YAML 文件包含了适配器多容器 Pod 的定义,里面有多个容器:

# alpine-with-adapter.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-adapter
spec:
  containers:
    - name: alpine-writer
      image: alpine:latest
      command: [ "sh", "-c", "i=1; while true; do echo \"$(date) - log $i\" >> /var/log/app/app.log; i=$((i+1)); sleep 5; done" ]
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/app
      # adapter container
    - name: log-adapter
      image: alpine:latest
      command: [ "sh", "-c", "while true; do cat /logs/app.log | sed 's/$/ PROCESSED/' > /logs/processed_app.log; cat /logs/processed_app.log; sleep 10; done" ]
      volumeMounts:
        - name: log-volume
          mountPath: /logs
  volumes:
    - name: log-volume
      emptyDir: {} 

应用 YAML 并检查 Pod 状态,如下所示:

$ kubectl apply -f nginx-with-adapter.yaml
pod/pod-with-adapter created 

一旦 Pod 创建完成,日志将被生成,我们可以从两个容器中验证日志。以下命令将显示由alpine-writer容器生成的日志:

$ kubectl exec -it pod-with-adapter -c alpine-writer -- head -5 /var/log/app/app.log
Sun Jun 30 15:05:26 UTC 2024 - log 1
Sun Jun 30 15:05:31 UTC 2024 - log 2
Sun Jun 30 15:05:36 UTC 2024 - log 3
Sun Jun 30 15:05:41 UTC 2024 - log 4
Sun Jun 30 15:05:46 UTC 2024 - log 5 

我们也可以通过使用log-adapter来检查转换后的日志:

$ kubectl exec -it pod-with-adapter -c log-adapter -- head -5 /logs/processed_app.log
Sun Jun 30 15:05:26 UTC 2024 - log 1 PROCESSED
Sun Jun 30 15:05:31 UTC 2024 - log 2 PROCESSED
Sun Jun 30 15:05:36 UTC 2024 - log 3 PROCESSED
Sun Jun 30 15:05:41 UTC 2024 - log 4 PROCESSED
Sun Jun 30 15:05:46 UTC 2024 - log 5 PROCESSED 

通过使用适配器容器,能够在不修改原始应用容器的情况下处理复杂的操作。

在我们结束这一章之前,在下一部分,我们将了解 Kubernetes 中与多容器 Pod 相关的另一个特性。

Sidecars 与 Kubernetes 原生 Sidecars

传统上,Kubernetes 中的 sidecar 是与主应用程序一起部署在 Pod 中的常规容器,正如我们在前面部分所学的那样。这种方法提供了额外的功能,但也有局限性。例如,即使主应用程序退出,sidecar 仍可能继续运行,浪费资源。此外,Kubernetes 本身并不固有地了解 sidecar 及其与主应用程序的关系。

为了解决这些限制,Kubernetes v1.28 引入了一个新概念:原生 sidecar。它们利用现有的init容器并进行特殊配置。这允许你为 Pod 的initContainers部分中的容器定义restartPolicy。这些特殊的 sidecar 容器可以独立启动、停止或重启,而不会影响主应用程序或其他初始化容器,从而提供对其生命周期的更细粒度控制。

以下的 Deployment 定义文件解释了如何在 Kubernetes 中配置原生 sidecar 容器:

...<removed for brevity>...
    spec:
      containers:
        - name: myapp
          image: alpine:latest
          command: ['sh', '-c', 'while true; do echo "logging" >> /opt/logs.txt; sleep 1; done']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: logshipper
          image: alpine:latest
          **restartPolicy:****Always**
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      volumes:
        - name: data
          emptyDir: {} 

这种方法确保了侧车容器与主容器的同步启动和关闭,优化了资源的使用。更重要的是,Kubernetes 会意识到侧车在 Pod 中的角色,未来可能会启用更紧密集成的功能。

通过利用带有初始化容器、侧车容器以及适配器或代理模式的多容器 Pod,Kubernetes 使你能够将复杂的应用程序构建为模块化单元。这简化了部署,并促进了在容器化环境中高效的资源利用。

总结

本章内容较长,但你现在应该对 Pod 有了较为深入的理解,特别是在同一 Pod 中管理多个容器时。

我们建议你专注于掌握声明式的方式来创建 Kubernetes 资源。正如你在本章中所注意到的,使用 Kubernetes 实现最复杂的任务的关键就在于编写 YAML 文件。举个例子,你根本无法轻松创建一个多容器的 Pod,而不编写 YAML 文件。

本章是对上一章的补充:第四章在 Kubernetes 中运行容器。你需要明白的是,我们在 Kubernetes 中做的所有操作都与 Pod 管理相关,因为 Kubernetes 中的一切都围绕着 Pod 进行。请记住,容器从不直接创建,而是总是通过 Pod 对象来创建,并且同一 Pod 中的所有容器都在同一 Kubernetes 节点上创建。如果你理解这一点,那么你可以继续阅读下一章!

在下一章中,我们将讨论 Kubernetes 的另一个重要方面——命名空间。

进一步阅读

加入我们社区的 Discord

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第六章:Kubernetes 中的多租户命名空间、配额和限制

到目前为止,我们已经通过在集群中启动对象并观察它们的行为来学习 Kubernetes 的关键概念。你可能已经注意到,从长远来看,保持集群的清晰组织将变得非常困难。随着集群的不断增长,管理集群中日益增多的资源将变得愈加困难。这时,Kubernetes 命名空间就发挥了作用。

在本章中,我们将学习命名空间。它们帮助我们通过按应用程序或环境对资源进行分组来保持集群的良好组织。Kubernetes 命名空间是 Kubernetes 管理的另一个关键方面,掌握它们非常重要!

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

  • Kubernetes 命名空间简介

  • 命名空间如何影响你的资源和服务

  • 在命名空间级别配置 ResourceQuota 和限制

技术要求

本章你需要具备以下条件:

  • 一个正常工作的 Kubernetes 集群(本地或基于云的,但这不重要)

  • 配置好的kubectl CLI,以便与集群进行通信

如果你没有这些技术要求,请阅读第二章Kubernetes 架构——从容器镜像到运行的 Pods第三章安装你的第一个 Kubernetes 集群来了解这些要求。

你可以从官方 GitHub 仓库下载本章的最新代码示例,网址为github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter06

Kubernetes 命名空间简介

在 Kubernetes 集群上部署的应用越多,保持集群资源有序的需求就越大。你可以使用标签和注解来管理集群中的对象,但你可以通过使用命名空间进一步提高组织性。Kubernetes 中的命名空间可以让你逻辑上隔离集群的各个部分,帮助你更有效地管理资源。然而,为了强制执行资源分配和限制,还需要像ResourceQuotas这样的额外对象。一旦创建了命名空间,你就可以启动 Kubernetes 对象,如 Pod,这些对象只会存在于该命名空间中。因此,所有针对集群执行的kubectl操作将仅限于该特定命名空间,你可以在其中执行尽可能多的操作,同时消除影响其他命名空间中资源的风险。

我们首先来了解命名空间到底是什么,以及它们为何会被创建。

在 Kubernetes 的高级多集群和多租户场景中,像Capsule (capsule.clastix.io/)和HyperShift (github.com/openshift/hypershift)等项目提供了强大的解决方案。Capsule 通过允许不同团队或租户管理他们自己的隔离命名空间,从而实现安全的多租户 Kubernetes 集群。HyperShift 则通过提供一种轻量且可扩展的方式来安全地隔离和管理不同环境中的 Kubernetes 资源,简化了多集群的管理。

现在,让我们继续下一节,讨论 Kubernetes 中命名空间的重要性。

Kubernetes 中命名空间的重要性

正如我们之前提到的,Kubernetes 中的命名空间是一种帮助集群管理员保持一切整洁有序,同时提供资源隔离的方式。

最大的 Kubernetes 集群可以运行数百个甚至数千个应用。当所有内容都部署在同一个命名空间时,了解哪个特定资源属于哪个应用程序可能变得非常复杂。

如果不幸的是,你更新或修改了错误的资源,可能会导致集群中运行的应用出现故障。为了解决这个问题,你可以使用标签和选择器,但即便如此,随着资源数量的增加,如果不开始使用命名空间,集群的管理很快会变得混乱。

我们在第八章《通过服务暴露你的 Pods》中学习了创建命名空间的基础知识,但没有深入了解。现在,让我们详细了解命名空间如何帮助保持一切整洁有序,同时提供资源隔离。

命名空间如何用来将资源划分为不同的块

在你安装 Kubernetes 之后,当你的集群是全新的时,它会为集群组件创建一些命名空间。所以即使你之前没有注意到,你实际上已经在使用命名空间,如下所示:

$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   8d
kube-node-lease   Active   8d
kube-public       Active   8d
kube-system       Active   8d 

主要概念是将你的 Pods 和其他对象部署到 Kubernetes 中,同时指定你偏好的命名空间。这种做法有助于保持集群的整洁和良好的结构。值得注意的是,Kubernetes 默认带有一个命名空间,如果你没有显式指定,系统将使用默认命名空间。

下图展示了命名空间和隔离的高层次概览。

图 6.1:Kubernetes 命名空间与资源隔离

从更广泛的意义上看,Kubernetes 命名空间为管理员提供了几个用途,包括:

  • 资源隔离

  • 作用域资源名称

  • 硬件分配和消费限制

  • 基于角色的访问控制中的权限和访问控制

我们建议你为每个微服务或应用程序创建一个命名空间,然后将所有属于该微服务的资源部署在该命名空间中。然而,请注意,Kubernetes 并没有对你施加任何特定的规则。例如,你可以选择以下方式使用命名空间:

  • 区分环境:例如,一个命名空间用于生产环境,另一个用于开发环境。

  • 区分层次:一个命名空间用于数据库,另一个用于应用程序 Pod,另一个则用于中间件部署。

  • 使用默认命名空间:对于仅部署少量资源的小型集群,你可以选择最简单的设置,使用一个大的默认命名空间,将所有内容部署到其中。

无论哪种方式,请记住,即使两个 Pod 部署在不同的命名空间并通过服务暴露,它们仍然可以互相互动和通信。即使 Kubernetes 服务是在某个命名空间中创建的,它们会收到完全限定的域名FQDN),并且该域名在整个集群中都可以访问。所以,即使运行在命名空间 A 中的应用程序需要与命名空间 B 中的应用程序互动,它也必须通过 FQDN 调用暴露应用 B 的服务。你不需要担心跨命名空间的通信,因为默认情况下是允许的,并且可以通过网络策略进行控制。

现在,让我们了解一下默认命名空间。

理解默认命名空间

大多数 Kubernetes 集群默认创建了几个命名空间。你可以使用kubectl get namespaces(或kubectl get ns)列出你的命名空间,如下所示:

$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   8d
kube-node-lease   Active   8d
kube-public       Active   8d
kube-system       Active   8d 

例如,我们使用的是一个 minikube 集群。通过查看该命令的输出,我们可以看到当前使用的集群在开始时就已经设置了以下命名空间:

  • default:Kubernetes 会自动提供此命名空间,允许你在不手动创建命名空间的情况下开始使用新集群。迄今为止,这个命名空间一直是创建所有资源的默认位置,当没有指定其他命名空间时,也会作为默认命名空间使用。

  • kube-public:此命名空间对所有客户端可访问,包括没有认证的客户端。主要用于集群范围内的目的,确保某些资源在整个集群中是公开可见且可读取的。然而,需要注意的是,命名空间的公开性更多的是一种约定,而非严格要求。当前该命名空间未被使用,你可以放心将其保留原样。

  • kube-system:这个命名空间是为 Kubernetes 系统本身创建的对象保留的。Kubernetes 在此命名空间中部署其操作所需的对象。在典型的 Kubernetes 设置中,kube-schedulerkube-apiserver等关键组件作为 Pod 在此命名空间中部署。

这些组件对 Kubernetes 集群的正常运行至关重要。因此,建议避免对该命名空间进行更改,因为任何修改都可能会扰乱集群的功能。

  • kube-node-lease:这个命名空间的目的是存储与各个节点相关的 Lease 对象。这些节点租约使得 kubelet 能够发送心跳,从而帮助控制平面检测节点故障。

根据你使用的 Kubernetes 发行版,预先存在的命名空间集合可能会有所不同。但大多数时候,这些命名空间会默认创建。

现在我们先把这个命名空间放一边,因为我们要进入主题,开始创建命名空间。我们将查看这些命名空间对你的 Pod 可能产生的影响,特别是在服务的 DNS 解析层面。

命名空间如何影响你的资源和服务

在这一节中,我们将学习如何创建、更新和删除命名空间,以及命名空间对服务和 Pod 的影响。

我们还将学习如何通过指定自定义命名空间来创建资源,以便我们不依赖默认的命名空间。

列出集群中的命名空间

我们在前一节中看到了这一点,了解默认命名空间,但在这一节中,我们将学习如何列出并探索已经在 Kubernetes 集群中创建的命名空间:

$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   8d
kube-node-lease   Active   8d
kube-public       Active   8d
kube-system       Active   8d 

请记住,所有使用namespaces资源kind的命令也可以使用ns别名,以便使用更简短的格式。

检索特定命名空间的数据

使用kubectl describe命令可以检索特定命名空间的数据,方法如下:

$ kubectl describe namespaces default
Name:         default
Labels:       kubernetes.io/metadata.name=default
Annotations:  <none>
Status:       Active
No resource quota.
No LimitRange resource. 

你还可以使用get命令并将 YAML 格式的输出重定向到文件,以获取特定命名空间的数据:

$ kubectl get namespaces default -o yaml > default-ns.yaml 

请注意,命名空间可以处于两种状态之一:

  • 活动:命名空间处于活动状态,可以用来放置新对象。

  • 终止:命名空间正在被删除,所有对象也将一并删除。在此状态下,不能再向命名空间中添加新对象。

现在,让我们学习如何命令式地创建一个新命名空间。

使用命令式语法创建命名空间

要使用命令式方法创建命名空间,你可以通过指定要创建的命名空间名称,使用kubectl create namespaces命令。在这里,我们将创建一个名为custom-ns的新命名空间。请注意,所有与命名空间相关的kubectl操作都可以使用更简短的ns别名:

$ kubectl create ns custom-ns
namespace/custom-ns created 

新的命名空间,名为custom-ns,现在应该已经在你的集群中创建。你可以再次运行kubectl get命令来检查它:

$ kubectl get ns custom-ns
NAME        STATUS   AGE
custom-ns   Active   35s 

如你所见,命名空间已经创建,并处于活动状态。我们现在可以将资源放入其中。

请避免将集群命名为以kube-为前缀的名称,因为这是 Kubernetes 对象和系统命名空间的术语。

现在,让我们学习如何使用声明式语法创建另一个命名空间。

使用声明式语法创建命名空间

让我们看看如何使用声明式语法创建命名空间。像往常一样,你必须使用 YAML(或 JSON)文件。以下是一个基本的 YAML 文件,用于在集群中创建一个新的命名空间。请注意文件中的kind: Namespace部分:

# custom-ns-2.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: custom-ns-2 

使用kubectl create命令应用定义,通过指定 YAML 文件路径:

$ kubectl apply -f custom-ns-2.yaml
namespace/custom-ns-2 created 

到此为止,我们已经创建了两个自定义命名空间。第一个命名空间是通过命令式方式创建的,名为custom-ns,而第二个命名空间是通过声明式方式创建的,名为custom-ns-2

现在,让我们学习如何使用kubectl删除这两个命名空间。

删除命名空间

你可以使用kubectl delete命令删除命名空间,如下所示:

$ kubectl delete namespaces custom-ns
namespace "custom-ns" deleted 

请注意,这也可以通过声明式语法实现。让我们删除之前使用 YAML 文件创建的custom-ns-2命名空间:

$ kubectl delete -f custom-ns-2.yaml
namespace "custom-ns-2" deleted 

运行此命令将使命名空间从Active状态变为Terminating状态。在命令执行后,命名空间将无法再承载新对象,几秒钟后,它应该完全从集群中消失。

我们必须警告你使用kubectl delete namespace命令,因为它极其危险。删除命名空间是永久性的,无法恢复。所有在该命名空间中创建的资源都会被销毁。如果你需要使用此命令,确保你有 YAML 文件来重新创建被销毁的资源,甚至是被销毁的命名空间。

现在,让我们来看看如何在特定命名空间内创建资源。

在命名空间中创建资源

以下代码演示了如何通过指定自定义命名空间来创建 NGINX Pod。这里,我们将重新创建一个新的custom-ns命名空间,并在其中启动一个 NGINX Pod:

$ kubectl create ns custom-ns
$ kubectl run nginx --image nginx:latest -n custom-ns
Pod/nginx created 

注意-n选项,它的长格式是--namespace选项。这个选项用于输入你想要创建资源(或获取资源详情)的命名空间的名称。所有可以在命名空间中范围限定的kind资源都支持这个选项。

这是另一个命令来演示这一点。以下命令将在custom-ns命名空间中创建一个新的configmap

$ kubectl create configmap configmap-custom-ns --from-literal=Lorem=Ipsum -n custom-ns
configmap/configmap-custom-ns created 

使用声明式语法时,你也可以指定命名空间。以下是如何在特定命名空间中使用声明式语法创建 Pod:

# pod-in-namespace.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx2
  namespace: custom-ns
spec:
  containers:
  - name: nginx
    image: nginx:latest 

请注意metadata部分下 Pod 名称下的namespace键,它指示将 Pod 创建在custom-ns命名空间中。现在,我们可以使用kubectl应用此文件:

$ kubectl apply -f pod-in-namespace.yaml
pod/nginx2 created 

到目前为止,我们应该已经有一个名为custom-ns的命名空间,里面包含两个nginx Pods,以及一个名为configmap-custom-nsconfigmap

使用命名空间时,你应该始终指定-n标志来指定你选择的特定命名空间。否则,你可能会在错误的命名空间中执行操作。

现在,让我们继续列出特定命名空间中的资源。

列出特定命名空间中的资源

要列出某个命名空间中的资源,必须添加-n选项,就像创建资源时一样。使用以下命令列出custom-ns命名空间中的 Pod:

$ kubectl get pods -n custom-ns 
NAME     READY   STATUS    RESTARTS   AGE 
nginx    1/1     Running   0          9m23s 
nginx2   1/1     Running   0          94s 

在这里,您可以看到我们之前创建的nginx Pod 存在于命名空间中。从现在开始,所有针对该 Pod 的命令都应该包含-n custom-ns选项。

这样做的原因是 Pod 在默认命名空间中不存在,如果您省略了-n选项,则会请求默认命名空间。让我们尝试从get命令中删除-n custom-ns。我们会看到nginx Pod 不再存在:

$ kubectl get pods
No resources found in default namespace. 

现在,我们还可以运行get configmap命令来检查configmap是否列出在输出中。如您所见,行为与尝试列出 Pod 时相同。如果省略-n选项,则列出操作将在default命名空间中进行:

$ kubectl get cm
NAME               DATA   AGE
kube-root-ca.crt   1      9d
$ kubectl get cm -n custom-ns
NAME                  DATA   AGE
configmap-custom-ns   1      70m
kube-root-ca.crt      1      76m 

从我们到目前为止讨论的内容中,最重要的一点是:在操作有多个命名空间的集群时,千万不要忘记添加-n选项。这个小小的疏忽可能会浪费您的时间,因为如果忘记了,您做的所有操作都会在default命名空间中进行。

不必每次在命令行中传递命名空间信息,也可以在 kubeconfig 上下文中进行设置。在下一部分中,我们将学习如何在当前kubeconfig上下文中设置工作命名空间。

使用 kubectl config set-context 设置当前命名空间

在某些情况下,也可以设置当前的命名空间。例如,如果您正在处理一个特定项目,并为您的应用程序和其他资源使用特定的命名空间,那么可以按如下方式设置命名空间上下文:

$ kubectl config set-context --current --namespace=custom-ns
Context "minikube" modified. 

我们还可以检查当前上下文中是否配置了任何命名空间,如下所示:

$ kubectl config view --minify --output 'jsonpath={..namespace}'
custom-ns 

现在,我们可以在不提及-n <namespace>选项的情况下获取应用程序的详细信息或应用配置:

$ kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
nginx    1/1     Running   0          79m
nginx2   1/1     Running   0          71m 

运行kubectl config命令及其子命令仅会触发对~/.kube/config文件的修改或读取操作,这是kubectl使用的配置文件。

当您使用kubectl config set-context命令时,您只是更新该文件,使其指向另一个命名空间。

知道如何使用kubectl在命名空间之间切换非常重要,但在执行任何写操作(如kubectl deletekubectl create)之前,请确保您处于正确的命名空间中。否则,您应该继续使用-n标志。由于这种切换操作可能会执行很多次,Kubernetes 用户通常会创建 Linux 别名以便更方便使用。如果您认为对您有帮助,不妨定义一个 Linux 别名。

例如,您可以在~/.bashrc文件中设置一个别名(假设您使用的是 Linux 或 macOS),如下所示:

alias kubens='kubectl config set-context --current --namespace' 

下次使用此别名,如下所示:

$ kubens custom-ns
Context "minikube" modified.
$ kubectl config view --minify --output 'jsonpath={..namespace}'
custom-n
# change to another namespace
$ kubens default
Context "minikube" modified.
$ kubectl config view --minify --output 'jsonpath={..namespace}'
default 

但再次强调,强烈建议使用-n namespace选项,以避免在 Kubernetes 操作中发生任何意外。

在继续本章内容和实践教程之前,让我们通过运行以下命令将命名空间设置回正常状态:

$  kubectl config set-context --current --namespace=default
Context "minikube" modified. 

现在,让我们来了解如何列出特定命名空间中的所有资源。

列出特定命名空间中的所有资源

如果你想列出特定命名空间中的所有资源,有一个非常有用的命令可以使用,叫做kubectl get all -n custom-ns

$ kubectl get all -n custom-ns 

正如你所看到的,这个命令可以帮助你检索在-n标志指定的命名空间中创建的所有资源。

了解在命名空间中名称是如何被限定的

理解命名空间提供了额外的优势:它们为所包含的资源的名称定义了作用域。

以 Pod 名称为例。当你不使用命名空间时,你与默认命名空间交互,并且当你创建两个具有相同名称的 Pod 时,会出现错误,因为 Kubernetes 使用 Pod 的名称作为它们的唯一标识符来区分它们。

让我们尝试在默认命名空间中创建两个名为nginx的 Pod。在这里,我们可以简单地连续运行相同的命令两次:

$ kubectl run nginx --image nginx:latest
Pod/nginx created
$ kubectl run nginx --image nginx:latest
Error from server (AlreadyExists): Pods "nginx" already exists 

第二个命令会产生一个错误,提示 Pod 已经存在,而它确实存在。如果我们运行kubectl get pods,我们可以看到只存在一个 Pod:

$ kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          64s 

现在,让我们再尝试列出 Pod,这次是在custom-ns命名空间中:

$ kubectl get Pods --namespace custom-ns
NAME     READY   STATUS    RESTARTS   AGE
nginx    1/1     Running   0          89m
nginx2   1/1     Running   0          23m 

正如你所看到的,这个命名空间中也有一个名为nginx的 Pod,它与default命名空间中的 Pod 不同。这是命名空间的一个主要优势。通过使用命名空间,你的 Kubernetes 集群现在可以定义多个具有相同名称的资源,只要它们位于不同的命名空间中。你可以通过使用命名空间元素轻松复制微服务或应用程序。

另外,请注意,你可以通过声明性地创建资源时覆盖资源的命名空间键。通过在kubectl create命令中添加-n选项,你强制将命名空间作为命令的上下文;kubectl将考虑命令中传递的命名空间,而不是 YAML 文件中存在的命名空间。这样做可以非常方便地在不同命名空间之间复制资源——例如,将生产环境放在production命名空间中,将测试环境放在test命名空间中。

理解并非所有资源都在命名空间中

在 Kubernetes 中,并非所有对象都属于某个命名空间。例如,节点就是这种情况,它在集群级别以Node类型的条目表示,但并不属于任何特定命名空间。你可以使用以下命令列出不属于命名空间的资源:

$ kubectl api-resources --namespaced=false
NAME                              SHORTNAMES   APIVERSION                        NAMESPACED   KIND
componentstatuses                 cs           v1                                false        ComponentStatus
namespaces                        ns           v1                                false        Namespace
nodes                             no           v1                                false        Node
persistentvolumes                 pv           v1                                false        PersistentVolume
…<removed for brevity>... 

你还可以通过将--namespaced参数设置为true来列出属于某个命名空间的所有资源:

$ kubectl api-resources --namespaced=true 

现在,让我们学习命名空间如何影响服务的 DNS。

使用命名空间解析服务

正如我们在第八章中发现的那样,通过服务暴露你的 Pods,Pods 可以通过一种称为服务的对象进行暴露。创建时,服务会分配一个 DNS 记录,允许集群中的 Pods 访问它们。

然而,当一个 Pod 尝试通过 DNS 调用服务时,只有在服务与 Pod 处于同一个命名空间时,它才能访问到该服务,这就带来了限制。命名空间为此问题提供了解决方案。当一个服务在特定命名空间中创建时,它的服务名称会被添加到 DNS 中:

<service_name>.<namespace_name>.svc.cluster.local 

通过查询该域名,你可以轻松查询到 Kubernetes 集群中任何命名空间中的服务。因此,你不受限于某一层级。即使 Pods 不在同一命名空间中,它们仍然能够进行相互通信。

在接下来的部分中,我们将探讨一些 Kubernetes 命名空间的最佳实践。

Kubernetes 命名空间的最佳实践

尽管创建和管理命名空间没有严格的规则,但让我们来学习一些与命名空间相关的行业最佳实践。

  • 组织和分离

    • 逻辑分组:根据应用程序、服务和资源的功能、它们所处的开发阶段(如开发、测试和生产)或谁拥有它们(例如,不同的团队),将它们组织在一起。这有助于保持结构清晰并简化资源管理。

    • 隔离:使用命名空间将部署隔离开。这意味着一个命名空间中的应用不会影响另一个命名空间中的内容,从而减少冲突。你还可以通过应用适当的基于角色的访问控制RBAC)网络策略,进一步提高安全性和隔离性。

  • 命名规则

    • 清晰且具描述性:为你的命名空间取一个能清楚表明其用途的名字。这样可以更容易地跟踪它们,尤其是在大型环境中。可以使用常见的命名方式,如 dev-test-prod- 来区分不同的环境。

    • 保持一致性:在集群中使用相同的命名风格,这样你的团队可以更容易地讨论和理解正在发生的事情。

  • 管理资源

    • 资源限制:设置命名空间的资源使用限制。这可以防止某个部署占用所有资源,确保每个人都能获得公平的资源分配。请记住,也可以在 Pod 级别设置资源限制,以获得更精细的控制。

    • 资源限制总览:为命名空间中的每个 Pod 和容器设置资源使用限制。这使你能更好地控制资源的使用方式。

  • 控制访问

    • 基于角色的访问控制:使用 RBAC 来控制每个命名空间中谁可以做什么。为人员和服务分配合适的权限,以管理他们命名空间中的内容。
  • 关注资源使用情况

    • 监控资源:关注每个命名空间中资源的使用情况以及你的应用是否健康。这有助于你发现问题并更好地利用资源。

    • 命名空间生命周期:定期检查你的命名空间。删除不再使用的命名空间,以保持整洁并避免安全风险。考虑自动化命名空间的创建和删除。

  • 其他需要考虑的事项

    • 非完美隔离:尽管命名空间有助于保持事物的分离,但它们并非万无一失。你可能还需要网络规则来提供额外的安全性。

    • 集群与命名空间:如果你的设置较为复杂并且需要大量的隔离,考虑使用不同的 Kubernetes 集群,而不仅仅是命名空间。

通过遵循这些最佳实践,你可以使用命名空间保持 Kubernetes 设置的组织性、安全性和易管理性。只要记得根据你的具体设置调整内容,以获得最佳结果。至此,我们已经掌握了 Kubernetes 中命名空间的基础知识。我们了解了命名空间是什么,如何创建和删除它们,如何使用它们保持集群的整洁和有序,以及如何更新 kubeconfig 上下文以使 kubectl 指向特定命名空间。

现在,我们将介绍一些与命名空间相关的更高级选项。此时,介绍 ResourceQuotaLimit 是个不错的时机,你可以使用它们来限制部署在 Kubernetes 上的应用程序可以访问的计算资源!

在命名空间级别配置资源配额(ResourceQuota)和限制(Limit)

在本节中,我们将发现命名空间不仅可以用于对集群中的资源进行排序,还可以限制 Pods 能够访问的计算资源。

使用 ResourceQuotaLimits 配合命名空间,你可以为 Pods 可以访问的计算资源设置限制。我们将学习如何操作以及如何使用这些新概念。通常,定义 ResourceQuotaLimits 被认为是生产集群的好实践——这就是为什么你应该明智地使用它们。

理解设置 ResourceQuota 的必要性

就像应用程序或系统一样,Kubernetes Pods 需要一定量的计算资源才能正常工作。在 Kubernetes 中,你可以配置两种类型的计算资源:

  • CPU

  • 内存

所有的节点(计算节点和控制节点)共同工作,提供 CPU 和内存,在 Kubernetes 中,添加更多的 CPU 和内存只需添加更多的计算(或工作)节点,以为更多的 Pods 腾出空间。根据你的 Kubernetes 集群是基于本地部署还是云端,添加更多计算节点可以通过购买硬件并进行设置来实现,或者简单地调用云 API 来创建额外的虚拟机。

了解 Pods 如何消耗这些资源

当你在 Kubernetes 上启动一个 Pod 时,一个控制平面组件,称为 kube-scheduler,将选举出一个计算节点并将 Pods 分配给它。然后,选举出的计算节点上的 kubelet 将尝试启动在 Pod 中定义的容器。

计算节点选举过程在 Kubernetes 中被称为 Pod 调度

当一个 Pod 被调度并在计算节点上启动时,默认情况下,它可以访问计算节点拥有的所有资源。没有任何东西能阻止它在应用程序使用过程中访问更多的 CPU 和内存,最终,如果 Pods 耗尽了内存或 CPU 资源,导致无法正常工作,那么应用程序就会崩溃。

这可能会成为一个真正的问题,因为计算节点可以同时运行多个应用程序——因此,也可以运行多个 Pod。所以,如果 10 个 Pod 在同一个计算节点上启动,但其中一个消耗了所有的计算资源,那么这将对该计算节点上运行的所有 10 个 Pod 产生影响。

这个问题意味着你有两个必须考虑的方面:

  • 每个 Pod 都应该能够要求一些计算资源来运行。

  • 集群应该能够限制 Pod 的资源消耗,使其不会占用所有可用资源,而是与其他 Pod 共享资源。

在 Kubernetes 中有可能解决这两个问题,我们将探索如何使用暴露给 Pod 对象的两个选项。第一个叫做资源请求,它是用来让 Pod 指示所需的计算资源量,另一个叫做资源限制,用于指示 Pod 能够访问的最大计算资源。

让我们现在来探索这些选项。

理解 Pods 如何要求计算资源

requestlimit选项将被声明在 Pod 资源的 YAML 定义文件中,或者你可以使用kubectl set resource命令将其应用到正在运行的部署中。在这里,我们将重点讨论request选项。

资源请求只是 Pod 为正常工作所需的最小计算资源量,建议始终为 Pods 定义request选项,至少对于那些计划在生产环境中运行的 Pod 来说。

假设你想在 Kubernetes 集群上启动一个 NGINX Pod。通过填写request选项,你可以告诉 Kubernetes,你的 NGINX Pod 至少需要 512 MiB 的内存和 25%的 CPU 核心才能正常工作。

这是将创建该 Pod 的 YAML 定义文件:

# pod-in-namespace-with-request.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-request
  namespace: custom-ns
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          memory: "512Mi"
          cpu: "250m" 
request at the container level and set different ones for each container.

有三点需要注意关于这个 Pod:

  • 它是在custom-ns命名空间内创建的。

  • 它需要512Mi的内存。

  • 它需要250m的 CPU。

那么这些指标是什么意思呢?

内存以字节为单位表示(1 MiB 等于 1,048,576 字节),而 CPU 以 千核心 表示,并允许小数值。如果你希望 Pod 消耗一个完整的 CPU 核心,可以将 cpu 键设置为 1000m。如果你想要两个核心,必须设置为 2000m;如果是半个核心,则为 500m0.5;以此类推。然而,为了请求一个完整的 CPU 核心,使用整数(例如 2)而不是 2000m 更为简单且常见。所以前面的 YAML 定义表示我们将要创建的 NGINX Pod 强制要求 512 MiB 的内存(因为内存以字节为单位表示),以及一个底层计算节点的四分之一 CPU 核心。这里没有涉及 CPU 或内存频率。

当你将这个 YAML 定义文件应用到集群时,调度器将寻找一个能够启动你的 Pods 的计算节点。这意味着你需要一个计算节点,其中有足够的可用 CPU 和内存资源来满足 Pod 的请求。

那么,如果没有计算节点能够满足这些要求呢?在这种情况下,Pod 将永远不会被调度,也永远不会启动。除非你删除一些正在运行的 Pod,为这个 Pod 腾出空间,或者添加一个能够启动该 Pod 的计算节点,否则它将永远不会被启动。

请记住,Pods 不能跨多个节点运行。所以如果你设置了 8000m(表示八个 CPU 核心),但你的集群由两个各有四个核心的计算节点组成,那么没有任何计算节点能够满足这一请求,你的 Pod 将无法调度。

因此,明智地使用 request 选项——可以将其视为 Pod 运行所需的最小计算资源。如果你设置的请求过高,可能会导致 Pod 永远不会被调度,但另一方面,如果你的 Pod 成功调度并启动,这部分资源是有保障的。

现在,让我们来看一下如何限制资源消耗。

了解如何限制资源消耗

当你编写 YAML 定义文件时,可以定义有关 Pod 能够消耗的资源限制。

设置资源请求不足以正确完成任务。每次设置资源时,都应设置一个限制。设置限制会告诉 Kubernetes 让 Pod 消耗最多到这个限制的资源,而不会超过这个限制。这样,你就能确保 Pod 不会把所有计算资源占为己有。

但是,要小心——根据达到的限制类型,Kubernetes 的行为会有所不同。如果 Pod 达到其 CPU 限制,它将被限速,你会注意到性能下降。但如果 Pod 达到其内存限制,则可能会被终止。原因是内存不是可以限速的资源,Kubernetes 仍然需要确保其他应用不会受到影响并保持稳定。因此,要对此有所了解。

如果没有设置限制,Pod 将能够消耗计算节点的所有资源。以下是更新后的 YAML 文件,对应我们之前看到的 NGINX Pod,现在它已更新,定义了内存和 CPU 的限制。

在这里,Pod 将能够消耗最多 1 GiB 的内存和 1 个完整 CPU 核心的底层计算节点:

# pod-in-namespace-with-request-and-limit.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-request-and-limit
  namespace: quota-ns
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          memory: "512Mi"
          cpu: "250m"
        limits:
          memory: "1Gi"
          cpu: "1000m" 

所以在设置请求时,也要设置限制。让我们在接下来的实践实验中尝试这种请求和限制。

在这个练习中,让我们检查当前系统资源的可用性。由于我们使用的是 minikube 集群进行演示,接下来让我们启用指标以获取详细的资源使用信息。你将在 第十章运行生产级 Kubernetes 工作负载 中使用这些指标:

$ minikube addons enable metrics-server 

等待指标服务器 Pod 进入 Running 状态后再继续:

$ kubectl get po -n kube-system | grep metrics
metrics-server-7c66d45ddc-82ngt            1/1     Running   0               113m 

让我们使用指标信息检查集群的使用情况:

$ kubectl top node
NAME       CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%  
minikube   181m         1%     806Mi           2% 

在这种情况下,我们的 Kubernetes 集群有大约 800Mi 内存和 180m CPU 可供消耗。

如果你使用的是带有 Podman 或 Docker 驱动的minikube,则minikube将显示实际主机的 CPU 和内存,而不是minikube Kubernetes 集群节点的内存。在这种情况下,你可以尝试使用 VirtualBox 创建另一个minikube集群(minikube start --profile cluster2-vb --driver=virtualbox),这样它将使用minikube VM 的 CPU 和内存资源。

让我们为资源请求和限制演示创建一个新的命名空间:

$ kubectl create ns quota-ns
namespace/quota-ns created 

现在,让我们创建一个新的 YAML 文件,其中包含不真实的内存请求,例如 100Gi resources.requests.memory,如下所示:

# pod-with-request-and-limit-1.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-request-and-limit-1
  namespace: quota-ns
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          memory: "100Gi"
          cpu: "100m"
        limits:
          memory: "100Gi"
          cpu: "500m" 

使用 kubectl apply 创建 Pod,如下所示:

$ kubectl apply -f pod-with-request-and-limit-1.yaml
pod/nginx-with-request-and-limit-1 created 

检查 Pod 状态以查看 Pod 创建情况:

$ kubectl get pod -n quota-ns
NAME                             READY   STATUS    RESTARTS   AGE
nginx-with-request-and-limit-1   0/1     Pending   0          45s 

这表示状态为 Pending,Pod 尚未运行。让我们使用 kubectl describe 命令查看 Pod 的详细信息,如下所示:

$ kubectl describe po nginx-with-request-and-limit-1 -n quota-ns
Name:             nginx-with-request-and-limit-1
Namespace:        quota-ns
...<removed for brevity>...
Status:           Pending
IP:              
IPs:              <none>
Containers:
  nginx:
...<removed for brevity>...
    Limits:
      cpu:     500m
      memory:  100Gi
    Requests:
      cpu:        100m
      memory:     100Gi
    Environment:  <none>
...<removed for brevity>...
  Warning  FailedScheduling  105s  default-scheduler  0/1 nodes are available: 1 Insufficient memory. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod. 

你会看到一个错误,因为调度器找不到集群中有足够内存请求的节点来容纳你的 Pod,这意味着 Pod 将一直等待,直到 Kubernetes 集群有合适的节点来部署该 Pod。

现在,我们将使用合理的内存更新 YAML,1Gi resources.requests.memory,如下所示:

# pod-with-request-and-limit-2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-request-and-limit-2
  namespace: quota-ns
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          memory: "1Gi"
          cpu: "100m"
        limits:
          memory: "2Gi"
          cpu: "500m" 

现在,让我们创建 Pod:

$ kubectl apply -f pod-with-request-and-limit-2.yaml
pod/nginx-with-request-and-limit-2 created
$ kubectl get po -n quota-ns
NAME                             READY   STATUS    RESTARTS   AGE
nginx-with-request-and-limit-1   0/1     Pending   0          8m24s
nginx-with-request-and-limit-2   1/1     Running   0          20s 

现在,Kubernetes 调度器可以根据资源请求找到合适的节点,Pod 已经开始按常规运行。

现在你已经了解了这种请求和限制的考虑因素,别忘了将其添加到你的 Pods 中!

理解为什么你需要使用 ResourceQuota:

你可以完全依赖其请求和限制选项来管理 Pod 的资源消耗。Kubernetes 中的所有应用程序都是 Pod,因此设置这两个选项可以为你提供一种强大且可靠的方式来管理集群中的资源消耗,前提是你永远不要忘记设置它们。

很容易忘记这两个选项,并在集群上部署一个没有定义任何请求或限制的 Pod。也许是你,或者是你团队中的某个成员,但部署这样的 Pod 风险很大,因为每个人都可能忘记这两个选项。如果这样做,应用的不稳定性风险会很高,因为没有限制的 Pod 可以消耗其启动的计算节点上的所有资源。

Kubernetes 提供了一种解决这个问题的方法,通过两个名为 ResourceQuotaLimitRange 的对象。这两个对象非常有用,因为它们可以在命名空间级别强制执行这些约束。

ResourceQuota 是另一种资源类型,就像 Pod 或 ConfigMap 一样。工作流程相当简单,包含两个步骤:

  1. 你必须创建一个新的命名空间。

  2. 你必须在该命名空间中创建一个 ResourceQuota 和一个 LimitRange 对象。

然后,所有在该命名空间内启动的 Pod 都将受到这两个对象的约束。

这些配额用于确保例如命名空间中的所有容器不会消耗超过 4 GiB 的 RAM。

因此,设置对 Pod 内部可以运行和不能运行的内容进行限制是可能的,甚至是推荐的。强烈建议你在集群中为每个命名空间始终定义一个 ResourceQuota 和一个 LimitRange 对象!

如果没有这些配额,部署的资源可能会消耗任意多的 CPU 或 RAM,这将使得你的集群以及所有运行在其中的应用不稳定,因为 Pod 并没有在其各自配置中设置请求和限制。

一般来说,ResourceQuota 用于执行以下操作:

  • 限制命名空间内的 CPU 消耗

  • 限制命名空间内的内存消耗

  • 限制命名空间内运行的对象数量,例如 Pod、Service、ReplicationController、Replica、Deployment 等。

  • 根据关联的存储类限制存储资源的消耗

有很多使用场景,你可以直接在 Kubernetes 文档中发现它们。现在,让我们学习如何在命名空间中定义 ResourceQuota

创建一个 ResourceQuota

为了演示 ResourceQuota 的实用性,我们将为命名空间 quota-ns 创建一个 ResourceQuota 对象。这个 ResourceQuota 将用于创建所有 Pod 合并后的请求和限制。以下是将创建 ResourceQuota 的 YAML 文件;请注意资源类型:

# resourcequota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: resourcequota
  namespace: quota-ns
spec:
  hard:
    requests.cpu: "1000m"
    requests.memory: "1Gi"
    limits.cpu: "2000m"
    limits.memory: "2Gi" 

请记住,ResourceQuota 对象的作用范围仅限于一个命名空间。

这个声明表示,在这个命名空间中,将会发生以下情况:

  • 所有 Pod 合并起来的 CPU 核心请求不能超过 1 个。

  • 所有 Pod 合并起来的内存请求不能超过 1 GiB。

  • 所有 Pod 合并起来的 CPU 核心消耗不能超过 2 个。

  • 所有 Pod 合并起来的内存消耗不能超过 2 GiB。

通过应用 YAML 配置来创建 ResourceQuota

$ kubectl apply -f resourcequota.yaml
resourcequota/resourcequota created
$ kubectl get quota -n quota-ns
NAME            AGE   REQUEST                                          LIMIT
resourcequota   4s    requests.cpu: 100m/1, requests.memory: 1Gi/1Gi   limits.cpu: 500m/2, limits.memory: 2Gi/2Gi 

让我们检查 quota-ns 命名空间中的当前资源,如下所示:

$ kubectl get po -n quota-ns
NAME                             READY   STATUS    RESTARTS        AGE
nginx-with-request-and-limit-2   1/1     Running   1 (5h41m ago)   7h18m
$ kubectl top pod -n quota-ns
NAME                             CPU(cores)   MEMORY 

有一个 nginx Pod(如果你没有删除之前的演示 Pod),并且使用量非常低。

现在,我们有了一个新的 Pod YAML 文件,但我们为 Pod 请求 3Gi 内存,如下所示:

# pod-with-request-and-limit-3.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-request-and-limit-3
  namespace: quota-ns
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          memory: "3Gi"
          cpu: "100m"
        limits:
          memory: "4Gi"
          cpu: "500m" 

现在,让我们尝试创建这个 Pod,看看结果会是什么:

$ kubectl apply -f pod-with-request-and-limit-3.yaml
Error from server (Forbidden): error when creating "pod-with-request-and-limit-3.yaml": pods "nginx-with-request-and-limit-3" is forbidden: exceeded quota: resourcequota, requested: limits.memory=4Gi,requests.memory=3Gi, used: limits.memory=2Gi,requests.memory=1Gi, limited: limits.memory=2Gi,requests.memory=1Gi 

是的,错误信息非常清楚;我们请求的资源超出了配额,Kubernetes 不允许创建新的 Pod。

只要它们遵循这些约束,你可以在命名空间中拥有任意数量的 Pods 和容器。大多数情况下,ResourceQuota 用于强制执行请求和限制的约束,但它们也可以用于在命名空间级别强制这些限制。

在命名空间级别设置 ResourceQuota 时,至关重要的是防止任何单一命名空间消耗所有集群资源;同时,在 Pod 或 Deployment 级别应用资源请求和限制也很重要。这种双重方法确保了资源消耗的控制,既限于各个命名空间,也在应用级别进行管理。通过强制执行这些限制,你能创建一个更可预测、稳定的环境,防止任何一个组件扰乱整个系统。

在以下示例中,前面的 ResourceQuota 已被更新,指定了创建该资源的命名空间不能包含超过 10 个 ConfigMap 和 5 个服务,这个示例看似无意义,但它很好地展示了 ResourceQuota 的不同应用可能性:

# resourcequota-with-object-count.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: my-resourcequota
  namespace: quota-ns
spec:
  hard:
    requests.cpu: "1000m"
    requests.memory: "1Gi"
    limits.cpu: "2000m"
    limits.memory: "2Gi"
    configmaps: "10"
    services: "5" 

在应用 ResourceQuota 的 YAML 定义时,确保 ResourceQuota 被分配到正确的命名空间。如果 YAML 文件中未指定命名空间,请记得使用 --namespace 标志来指定 ResourceQuota 应应用的位置。

如下所示创建 ResourceQuota

$ kubectl apply -f resourcequota-with-object-count.yaml 

在以下部分,我们将学习 Kubernetes 中的存储 ResourceQuota

存储资源配额

在 Kubernetes 中,资源配额允许你控制一个命名空间内请求的总存储资源。你可以限制所有持久卷声明的存储请求总和,以及允许的持久卷声明数量。此外,还可以根据特定的存储类定义配额,从而为不同的存储类类型设置单独的限制。例如,你可以分别为黄金和铜存储类设置配额。从 1.8 版本开始,配额还支持本地临时存储,允许你限制命名空间内所有 Pod 的本地临时存储请求和限制总和。

现在,让我们来学习如何列出 ResourceQuota

列出 ResourceQuota

ResourceQuota 对象可以通过 kubectl 使用配额的资源名称选项来访问。kubectl get 命令将为我们完成这项工作:

$ kubectl get resourcequotas -n quota-ns
NAME            AGE   REQUEST                                          LIMIT
resourcequota   15m   requests.cpu: 100m/1, requests.memory: 1Gi/1Gi   limits.cpu: 500m/2, limits.memory: 2Gi/2Gi 

现在,让我们学习如何从 Kubernetes 集群中删除 ResourceQuota

删除 ResourceQuota

要从集群中删除 ResourceQuota 对象,可以使用 kubectl delete 命令:

$ kubectl delete -f resourcequota-with-object-count.yaml
resourcequota "my-resourcequota" deleted 

现在,让我们介绍 LimitRange 的概念。

介绍 LimitRange

LimitRange 是另一个类似于 ResourceQuota 的对象,它是在命名空间级别创建的。LimitRange 对象用于强制为单个容器设置默认的请求和限制值。即使你使用 ResourceQuota 对象,也可以创建一个占用命名空间内所有可用资源的对象,因此 LimitRange 对象的作用是防止你在命名空间中创建过小或过大的容器。

这里是一个 YAML 文件,它将创建 LimitRange

# limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: my-limitrange
  namespace: quota-ns
spec:
  limits:
    - default: # this section defines default limits
        cpu: 500m
        memory: 256Mi
      defaultRequest: # this section defines default requests
        cpu: 500m
        memory: 128Mi
      max: # max and min define the limit range
        cpu: "1"
        memory: 1000Mi
      min:
        cpu: 100m
        memory: 128Mi
      type: Container 

如你所见,LimitRange 对象由四个重要的键组成,它们都包含 memorycpu 配置。这些键如下:

  • default:如果你忘记在 Pod 层面应用 memorycpu 限制,这将帮助你强制执行容器的默认值。每个没有设置限制的容器将从 LimitRange 对象中继承这些默认值。

  • defaultRequest:这与 default 相同,但适用于 request 选项。如果你没有为 Pod 中的某个容器设置 request 选项,LimitRange 对象中此键的值将自动作为默认值使用。

  • max:该值表示 Pod 可以设置的容器的最大限制(不是请求)。你无法配置一个超出该限制的 Pod 容器。它与 default 值相同,即不能大于这里定义的值。

  • min:该值类似于 max,但适用于请求。它是 Pod 可以请求的计算资源的最小值,defaultRequest 选项不能低于该值。

最后,注意,如果你省略 defaultdefaultRequest 键,那么 max 键将作为 default 键使用,min 键将作为 default 键使用。

定义 LimitRange 是一个好主意,如果你想避免忘记为 Pod 设置请求和限制。至少通过 LimitRange,这些对象将有默认的请求和限制!

$ kubectl apply -f limitrange.yaml 

现在,让我们学习如何列出 LimitRanges

列出 LimitRanges

kubectl 命令行工具将帮助你列出你的 LimitRanges。不要忘记加上 -n 标志,将请求限定到特定的命名空间:

$ kubectl get limitranges -n quota-ns
NAME            CREATED AT
my-limitrange   2024-03-10T16:13:00Z
$ kubectl describe limitranges my-limitrange -n quota-ns
Name:       my-limitrange
Namespace:  quota-ns
Type        Resource  Min    Max     Default Request  Default Limit  Max Limit/Request Ratio
----        --------  ---    ---     ---------------  -------------  -----------------------
Container   memory    128Mi  1000Mi  128Mi            256Mi          -
Container   cpu       100m   1       500m             500m           - 

现在,让我们学习如何从命名空间中删除 LimitRange

删除 LimitRange

删除 LimitRange 可以使用 kubectl 命令行工具。以下是具体步骤:

$ kubectl delete limitranges my-limitrange -n quota-ns
limitrange "my-limitrange" deleted 

一如既往,不要忘记加上 -n 标志,将请求限定到特定的命名空间;否则,你可能会错误地操作到其他命名空间!

总结

本章介绍了命名空间,它在 Kubernetes 中非常重要。如果不使用命名空间,你无法有效地管理你的集群,因为它们提供了集群内的逻辑资源隔离。例如,大多数人使用生产和开发命名空间,或者为每个应用程序创建一个命名空间。创建数百个命名空间的集群并不罕见。

我们发现大多数 Kubernetes 资源都与命名空间相关联,虽然也有一些例外。请记住,Kubernetes 默认配置了一些预设的命名空间,例如 kube-system,而且通常不建议更改这些命名空间中的内容,特别是如果你不知道自己在做什么的话。

我们还发现,命名空间可以用来设置配额并限制 Pod 可消耗的资源,且在命名空间级别设置这些配额和限制是一个非常好的实践,可以通过 ResourceQuotaLimitRange 对象来防止 Pod 消耗过多计算资源。通过实施这些措施,你为有效的容量管理奠定了基础,这对那些旨在维持集群中所有应用程序的稳定性和效率的组织至关重要。

在下一章中,我们将学习如何使用 ConfigMaps 和 Secrets 来处理 Kubernetes 中的配置信息和敏感数据。

进一步阅读

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/cloudanddevops

第七章:使用 ConfigMaps 和 Secrets 配置你的 Pods

前几章介绍了如何使用 Kubernetes 启动应用容器。现在你知道,每当你需要在 Kubernetes 上启动容器时,必须通过 Pods 来实现。这是你需要理解和掌握的关键概念。Kubernetes 是一个通过 RESTful API 管理的复杂系统。处理这一过程的核心组件是 Kubernetes API 服务器,它提供了与集群交互的主要接口。当用户通过这个 API 创建 Kubernetes 对象,如 Pods 时,系统会在集群节点上分配必要的资源。在这些资源中,Pod 非常重要,因为它在 Kubernetes 节点上创建时会启动应用容器。

在本章中,我们将学习两个新的 Kubernetes 对象:ConfigMapsSecrets。Kubernetes 使用 ConfigMaps 和 Secrets 来将应用配置与代码本身解耦。这些对象提供了一种独立管理配置值的机制,从而增强了应用的可移植性和安全性。ConfigMaps 将非敏感数据以键值对的形式存储,而 Secrets 则处理诸如密码或 API 密钥等敏感信息。这两者都可以作为环境变量注入到 Pods 中,或者作为卷挂载,允许应用动态访问配置,而无需硬编码值。通过将配置与应用分离,你可以在 Kubernetes 生态系统中创建更灵活、更具韧性和更安全的部署。

以下是本章我们将要覆盖的主要内容:

  • 理解 ConfigMaps 和 Secrets 的概念

  • 使用 ConfigMaps 配置你的 Pods

  • 使用 Secret 对象管理敏感配置

技术要求

本章你将需要以下内容:

  • 一个正在运行的 Kubernetes 集群(本地或云端,虽然这并不重要)

  • 一个已配置的 kubectl CLI,用于与 Kubernetes 集群通信

你可以通过阅读 第二章Kubernetes 架构 - 从容器镜像到运行的 Pod第三章安装你的第一个 Kubernetes 集群,分别获得一个工作中的 Kubernetes 集群和正确配置的 kubectl 客户端。

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter07

理解 ConfigMaps 和 Secrets 的概念

Kubernetes 环境是动态的,并且不断变化。这使得应用配置的管理变得复杂。传统方法通常无法跟上云原生开发的快速步伐。

为了解决这个问题,Kubernetes 提供了 ConfigMaps 和 Secrets,这些是处理不同类型配置数据的专用方式。通过将配置与应用程序代码分离,这些资源显著提高了应用程序的可移植性、安全性和可管理性。

在接下来的部分中,我们将深入探讨 ConfigMaps 和 Secrets 的细节,了解它们是如何工作的,以及如何在 Kubernetes 部署中最好地利用它们。

解耦应用程序和配置

容器天生是不可变的,这意味着一旦创建,就无法更改。这在配置管理时带来了挑战。将配置嵌入容器镜像中需要在每次配置更改时重建整个镜像,这个过程既费时又低效。仅依赖环境变量或命令行参数进行配置也会显得繁琐,尤其是对于复杂的设置,并且不能保证在容器重启时配置的持久性。这些限制突显了在 Kubernetes 中需要更有效的配置管理策略。

当我们使用 Kubernetes 时,我们希望我们的应用程序尽可能具有可移植性。实现这一目标的一个好方法是将应用程序与其配置解耦。早些时候,配置和应用程序是同一个概念;由于应用程序代码只设计用于在单一环境中运行,配置值通常会被捆绑在应用程序代码中,因此配置和应用程序代码是紧密耦合的。

将应用程序代码和配置值视为相同的内容会降低应用程序的可移植性。如今,情况发生了很大变化,我们必须能够更新应用程序配置,因为我们希望尽可能地使应用程序具有可移植性,使我们能够在多个环境中无缝部署应用程序。

考虑以下问题:

  1. 你将一个 Java 应用程序部署到开发环境进行测试。

  2. 测试完成后,应用程序准备好进入生产环境,并且需要进行部署。然而,生产环境的 MySQL 端点与开发环境中的不同。

这里有两种可能性:

  • 配置和应用程序代码没有解耦,MySQL 被硬编码并捆绑在应用程序代码中;你被困住了,在编辑应用程序代码后需要重新构建整个应用程序。

  • 配置和应用程序代码已解耦。这对你来说是好消息,因为你可以在部署到生产环境时,简单地覆盖 MySQL 端点。

这就是可移植性概念的关键——应用程序代码应独立于其运行的基础设施。实现这一目标的最佳方法是将应用程序代码与其配置解耦。

在下图中,我们有一个常见的应用程序容器镜像,并且为不同环境配置了不同的配置。

图 7.1:应用程序配置解耦

让我们来看一些典型的例子,看看您应该从应用程序中解耦的配置值类型:

  • 访问 Amazon S3 存储桶的 API 密钥

  • 您的应用程序使用的 MySQL 服务器的密码

  • 您的应用程序使用的 Redis 集群的端点

  • 预计算的值,如 JWT 令牌的私钥

这些值可能会在不同的环境之间发生变化,而在您的 Pod 中启动的应用程序应该能够加载不同的配置,具体取决于它们所启动的环境。这就是为什么我们要系统地维护应用程序与它们所消耗配置之间的隔离的原因。这样做可以让我们在 Kubernetes 集群中将它们视为两个完全不同的实体。实现这一目标的最佳方法是将应用程序及其配置视为两个不同的实体。

这就是 Kubernetes 建议使用 ConfigMap 和 Secret 对象的原因,它们旨在承载您的配置数据。然后,您需要在创建 Pod 时附加这些 ConfigMap 和 Secret。

请避免将您的配置值作为容器镜像的一部分,比如 Docker 镜像。您的 Dockerfile(或 Podman 的 Containerfile)应该构建您的应用程序,而不是配置它。在构建时包含容器配置,会在应用程序与其配置之间建立强关系,从而减少容器在不同环境中的可移植性。

ConfigMap 用于保存非敏感的配置值,而 Secret 通常是相同的,但用于保存敏感的配置值,如数据库密码、API 密钥等。

因此,您可以为每个环境和每个应用程序想象一个 ConfigMap 和 Secret,其中将包含应用程序在特定上下文和环境中运行所需的参数。关键点是,ConfigMap 和 Secret 作为 Kubernetes 集群内配置数据的键值对存储机制。这些键值对可以包含称为 literals 的纯值或完整的配置文件,如 YAMLTOML 等。然后,在创建 Pod 时,您可以选择 ConfigMap 或 Secret 的名称,并将其与 Pod 链接,以便将配置值暴露给运行在其中的容器。

您始终按此顺序进行,以确保在 Pod 启动时配置数据可用:

  1. 使用配置值创建一个 ConfigMap 或 Secret。

  2. 创建一个引用 ConfigMap 或 Secret 的 Pod。

通过采用这种方法,您可以增强应用程序的可移植性和可维护性,符合常见的 DevOps 最佳实践。

现在我们已经解释了为何将应用程序代码与配置值解耦很重要,接下来是时候解释为什么以及如何以 Kubernetes 友好的方式实现这一目标。

了解 Pods 如何使用 ConfigMap 和 Secret

在深入了解 ConfigMap 和 Secrets 的具体细节之前,让我们先来看看在容器化环境中管理配置的传统方法。在 Kubernetes 之外,现代容器化应用程序通过多种方式使用它们的配置:

  • 作为操作系统环境变量

  • 作为配置文件

  • 命令行参数

  • API 访问

这是因为在 Docker 或 Podman 中覆盖环境变量非常容易,而且所有编程语言都提供了轻松读取环境变量的功能。配置文件可以很容易地在容器之间共享并作为卷挂载。

重要

虽然 ConfigMap 和 Secrets 是 Kubernetes 中管理配置的主要方法,但也有其他替代方法。命令行参数可以直接将配置传递给容器,但这种方法比 ConfigMap 和 Secrets 更不灵活和安全。直接访问 Kubernetes API 获取配置通常不被推荐,因为它存在安全风险和增加的复杂性。

在 Kubernetes 环境中,ConfigMap 和 Secrets 遵循这两种方法。一旦在 Kubernetes 集群中创建,ConfigMap 可以通过以下两种方式使用:

  • 作为容器中运行的 Pod 的环境变量

  • 像其他任何卷一样,作为 Kubernetes 卷进行挂载

你可以选择将 ConfigMap 或 Secret 中的某个值注入为环境变量,或者将 ConfigMap 或 Secret 中的所有值注入为环境变量。

重要

强烈不推荐使用环境变量来暴露 Secrets,因为这可能带来安全风险并导致值被截断。建议使用基于文件的 Secrets 卷挂载方法,利用 kubelet 缓存实现动态更新和增强安全性。

ConfigMap 和 Secrets 也可以作为卷挂载。当你将 ConfigMap 挂载为卷时,可以将它包含的所有值作为一个目录注入到容器中。如果你将完整的配置文件存储在 ConfigMap 中,使用此功能覆盖配置目录变得异常简单。

通过这部分内容的介绍,你应该理解为什么在配置应用程序时,ConfigMap 和 Secrets 如此重要。如果你打算在 Kubernetes 中进行干净且扎实的工作,掌握它们至关重要。正如我们在本章前面提到的,ConfigMap 用于存储不安全的配置值,而 Secrets 用于存储更为敏感的配置信息,如哈希值或数据库密码。

下表展示了 Kubernetes 中 ConfigMap 和 Secrets 之间的高级差异。

功能 ConfigMaps Secrets
目的 存储非敏感配置数据 存储敏感信息
数据格式 明文文本 Base64 编码
安全性 不太安全 更加安全
常见用途 应用程序配置、环境变量和命令行参数 密码、API 密钥、SSH 密钥和 TLS 证书
处理敏感数据 不推荐 强烈推荐

表 7.1:ConfigMap 和 Secrets 之间的区别

由于 ConfigMap 和 Secrets 的行为不同,我们将分别探讨它们。首先,我们将了解 ConfigMap 的工作原理,之后再讨论 Secrets。

使用 ConfigMap 配置你的 Pods

在本节中,我们将学习如何列出、创建、删除和读取 ConfigMap。同时,我们还将学习如何将它们附加到 Pods 上,以便将其值作为环境变量或卷注入到 Pods 中。

在接下来的几节中,我们将学习如何列出、创建和管理 Kubernetes 中的 ConfigMap。

列出 ConfigMap

列出在集群中创建的 ConfigMap 非常简单,可以像使用 Kubernetes 中的其他对象一样使用 kubectl 完成。你可以通过使用完整的资源名称 configmaps 来实现:

$ kubectl get configmaps 

或者,你可以使用较短的别名 cm

$ kubectl get cm 

这两个命令的效果相同。执行时,kubectl 可能会显示一些默认的 ConfigMap,或者出现错误,提示没有找到 ConfigMap。这种差异是因为某些云服务会为内部流程生成默认的 ConfigMap,而有些则不会。这些默认 ConfigMap 的存在与否取决于 Kubernetes 集群的部署环境。

$  kubectl get configmaps -A
NAMESPACE         NAME                                                   DATA   AGE
default           kube-root-ca.crt                                       1      23d
kube-node-lease   kube-root-ca.crt                                       1      23d
kube-public       cluster-info                                           1      23d
kube-public       kube-root-ca.crt                                       1      23d
...<removed for brevity>... 

如前面的输出所示,默认命名空间和 Kubernetes 管理的命名空间中有多个 ConfigMap,这是在集群部署过程中创建的。现在,让我们学习如何在下一节中创建一个新的 ConfigMap。

创建一个 ConfigMap

像其他 Kubernetes 对象一样,ConfigMap 可以通过命令式或声明式方法创建。你可以选择创建一个空的 ConfigMap 然后添加值,或者直接创建一个包含初始值的 ConfigMap。以下命令将通过命令式方法创建一个名为 my-first-configmap 的空 ConfigMap:

$ kubectl create configmap my-first-configmap
configmap/my-first-configmap created 

执行完此命令后,你可以再次输入 kubectl get cm 命令,查看你新创建的 configmap

$ kubectl get cm
NAME                 DATA   AGE
my-first-configmap   0      42s 

现在,我们将创建一个新的空 ConfigMap,但这次我们将采用声明式方法创建。这样,我们需要创建一个 YAML 文件,并通过 kubectl 来应用它。

以下内容应该放置在名为 ~/my-second-configmap.yaml 的文件中:

# ~/my-second-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-second-configmap 

一旦这个文件创建完成,你可以通过 kubectl apply -f 命令将其应用到 Kubernetes 集群中:

$ kubectl apply -f ~/my-second-configmap.yaml
configmap/my-second-configmap created 

你可以再次输入 kubectl get cm 命令,以查看你新创建的 configmap 是否出现在你之前创建的 configmap 旁边:

$  kubectl get cm
NAME                  DATA   AGE
kube-root-ca.crt      1      23d
my-first-configmap    0      5s
my-second-configmap   0      2s 

请注意,kubectl get cm 命令的输出还会在 DATA 列中返回每个 ConfigMap 所包含的键的数量。目前是零,但在接下来的示例中,你将看到我们可以在创建 ConfigMap 时填写 configmap,因此 DATA 将反映我们在 configmap 中放入的键的数量。

从字面值创建一个 ConfigMap

拥有一个空的 ConfigMap 是相当无用的,所以让我们学习如何在 ConfigMap 中创建带有值的对象。我们通过命令式方式来实现:向 kubectl create cm 命令添加 –from-literal 标志。

在这里,我们将创建一个名为 my-third-configmap 的 ConfigMap,键名为 color,其值设置为 blue

$ kubectl create cm my-third-configmap --from-literal=color=blue
configmap/my-third-configmap created 

同时,请注意,您可以创建一个包含多个参数的 ConfigMap;您只需要通过在命令中链式添加所需数量的 from-literals,来将任意数量的配置数据添加到 configmap 中:

$ kubectl create cm my-fourth-configmap --from-literal=color=blue --from-literal=version=1 --from-literal=environment=prod
configmap/my-fourth-configmap created 

在这里,我们创建了一个包含三个配置值的 ConfigMap。现在,您可以再次使用此命令列出您的 ConfigMap。您应该能看到刚刚创建的几个额外的 ConfigMap。

请注意,kubectl get cm 的返回结果中的 DATA 列现在反映了每个 configmap 内部的配置值数量:

$ kubectl get cm
NAME                  DATA   AGE
my-first-configmap    0      9m30s
**my-fourth-configmap   3      6m23s**
my-second-configmap   0      8m2s
my-third-configmap    1      7m9s 

我们还可以通过以 YAML 或 JSON 格式显示良好格式化的输出,查看 ConfigMap(或其他任何对象)的详细信息,如下所示。

$ kubectl get cm my-fourth-configmap -o yaml
apiVersion: v1
data:
  color: blue
  environment: prod
  version: "1"
kind: ConfigMap
metadata:
  creationTimestamp: "2024-08-10T06:20:49Z"
  name: my-fourth-configmap
  namespace: default
  resourceVersion: "25647"
  uid: 3c8477dc-f3fe-4d69-b66a-403679a88450 

这种方法也有助于以 YAML 或 JSON 格式备份对象,符合 配置即代码CaC)和 基础设施即代码IaC)的最佳实践。

现在,也可以通过声明方式创建相同的 ConfigMap。下面是一个准备应用于集群的声明式 YAML 配置文件。请注意新的数据 YAML 键,它包含了所有的配置值:

# ~/my-fifth-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-fifth-configmap
data:
  color: "blue"
  version: "1"
  environment: "prod" 

一旦创建了文件,您可以使用 kubectl apply 命令创建 ConfigMap,如下所示:

$ kubectl apply -f my-fifth-configmap.yaml
configmap/my-fifth-configmap created 

基于 my-fifth-configmap.yaml 文件,创建一个新的 ConfigMap 对象,其中包括按 YAML 定义的数据。

现在,让我们在本章的下一个部分学习如何将整个配置文件存储在 ConfigMap 中。

将整个配置文件存储在 ConfigMap 中

如前所述,也可以将完整的文件存储在 ConfigMap 中——您不局限于字面值。技巧是将文件在文件系统中的路径提供给 kubectl 命令行。然后,kubectl 会获取文件的内容,并将其用于填充 configmap 中的参数。

将配置文件的内容存储在 ConfigMap 中非常有用,因为您可以像挂载卷一样,将 ConfigMap 挂载到您的 Pods 中。

好消息是,您可以在 ConfigMap 中混合使用字面值和文件。字面值通常是短字符串,而文件则被视为较长的字符串;它们并不是两种不同的数据类型。

我们在第五章Sidecar 多容器 Pod 示例部分中已经看到过一个这样的示例 ConfigMap,我们将 Fluentd 配置内容作为文件内容存储在 ConfigMap 中。

在这里,创建了第六个 ConfigMap,它与之前一样包含字面值,但现在我们也将存储一个文件的内容。

让我们在 $HOME/configfile.txt 位置创建一个名为 configfile.txt 的文件,并填入任意内容:

$ echo "I'm just a dummy config file" >> $HOME/configfile.txt 

这里的配置文件扩展名为 .txt,但它也可以是 .yaml.toml.rb 或任何其他您的应用程序可以使用的配置格式。

现在,我们需要将该文件导入到一个 ConfigMap 中,因此让我们创建一个全新的 ConfigMap 来演示这一点。您可以使用 --from-file 标志来实现此操作,该标志可以与 --from-literal 标志一起使用:

$ kubectl create cm my-sixth-configmap --from-literal=color=yellow --from-file=$HOME/configfile.txt
configmap/my-sixth-configmap created 

让我们再次运行 kubectl get cm 命令,以确保我们的第六个 configmap 已创建。该命令将显示它包含两个配置值——在我们的案例中,一个是从字面值创建的,另一个是从文件内容创建的:

$ kubectl get cm my-sixth-configmap
NAME                  DATA   AGE
my-sixth-configmap    2      38s 

如您所见,my-sixth-configmap 包含两条数据:字面值和文件。

现在,让我们创建第七个 ConfigMap。就像第六个一样,它将包含一个字面值和一个文件,但这次我们将以声明方式创建它。

YAML 格式允许您使用 | 符号来表示多行。我们在声明文件中使用了这种语法:

# ~/my-seventh-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-seventh-configmap
data:
  color: "green"
  configfile.txt: |
    I'm another configuration file. 

让我们使用 kubectl apply 命令应用这个 YAML 文件来创建我们的 configmap

$ kubectl apply -f my-seventh-configmap.yaml
configmap/my-seventh-configmap created 

让我们使用 kubectl get cm 列出集群中的 ConfigMap,确保我们的第七个 configmap 已创建并包含两个值。所以我们再运行一次 kubectl get cm 命令:

$ kubectl get configmap/my-seventh-configmap
NAME                   DATA   AGE
my-seventh-configmap   2      36s 

现在,让我们发现创建 ConfigMap 的最后一种可能方式——那就是从 env 文件创建。

从 env 文件创建 ConfigMap

如您所料,您可以通过使用 --from-env-file 标志,从 env 文件中命令式地创建 ConfigMap。

env 文件是一种 key=value 格式的文件,每个键由换行符分隔。这是某些应用程序使用的配置格式,因此 Kubernetes 引入了一种从现有 env 文件生成 ConfigMap 的方式。如果您有已经存在的应用程序,并希望将其迁移到 Kubernetes 中,这非常有用。

下面是一个典型的 env 文件:

# ~/my-env-file.env
hello=world
color=blue
release=1.0
production=true 

按惯例,env 文件命名为 .env,但这不是强制要求。只要文件格式正确,Kubernetes 就能基于参数生成 ConfigMap。

您可以使用以下命令将 env 文件中的配置作为 ConfigMap 导入到 Kubernetes 集群中:

$ kubectl create cm my-eight-configmap --from-env-file my-env-file.env
configmap/my-eight-configmap created 

最后,让我们列出集群中的 ConfigMap,以检查我们的新 ConfigMap 是否已创建,并包含三个配置值:

$ kubectl get cm my-eight-configmap
NAME                 DATA   AGE
my-eight-configmap   4      7s 

如您所见,新创建的 configmap 现在已在集群中可用,并且它是根据 env 文件中的三个参数创建的。这是将您的 env 文件导入 Kubernetes ConfigMap 的一种可靠方法。

请记住,ConfigMap 并不适合存储敏感值。ConfigMap 中的数据没有加密,这也是为什么你只需通过 kubectl describe cm 命令就能查看它们。对于任何需要隐私保护的内容,你必须使用 Secret 对象,而不是 ConfigMap 对象。需要注意的是,尽管 Secrets 本身默认并不加密,但它们以 base64 格式存储,提供了基本的混淆保护。此外,在 Kubernetes 1.27 及之后的版本中,你可以利用 Kubernetes 密钥管理系统KMS)插件提供者来加密 Secrets 和 ConfigMap 中的数据,这为敏感信息提供了更强大的安全层。

现在,让我们来探索如何读取 ConfigMap 中的值。

读取 ConfigMap 中的值

到目前为止,我们只列出了 ConfigMap 以查看其中的键的数量。让我们再进一步:你可以读取 ConfigMap 中的实际数据,而不仅仅是计数 ConfigMap 的数量。如果你想调试 ConfigMap 或者对其存储的数据类型不确定,这非常有用。

ConfigMap 中的数据并非敏感数据,因此你可以轻松地通过 kubectl 阅读并获取它;它将在终端的输出中显示。

你可以使用 kubectl describe 命令来读取 ConfigMap 中的值。我们将针对 my-fourth-configmap 这个 ConfigMap 运行此命令,因为它包含的数据最多。输出内容相当多,但正如你所看到的,两项配置信息被清晰地显示了出来:

$ kubectl describe cm my-fourth-configmap
Name:         my-fourth-configmap
Namespace:    default
Labels:       <none>
Annotations:  <none>
Data
====
color:
----
blue
environment:
----
prod
version:
----
1
BinaryData
====
Events:  <none> 

kubectl describe cm 命令返回这些结果。你可以预期会收到类似于此的结果,而不是计算机友好的格式(如 JSON 或 YAML)的结果。

由于数据在终端输出中清晰显示,请记住,任何 Kubernetes 集群的用户(只要有访问该 ConfigMap 的权限)都能够通过输入 kubectl describe cm 命令直接获取这些数据,因此请小心不要在 ConfigMap 中存储任何敏感数据。

现在,让我们来探索如何将 ConfigMap 数据注入到正在运行的 Pods 作为环境变量。

将 ConfigMap 作为环境变量链接

在本节中,我们将通过将 ConfigMap 链接到 Pods 来“赋予”它们生命。首先,我们将重点讲解如何将 ConfigMap 注入为环境变量。在这里,我们希望 Pod 中容器的环境变量来自一个 ConfigMap 的值。

你可以通过两种不同的方式来实现:

  • 在给定的 ConfigMap 中注入一个或多个指定值:你可以根据一个或多个 ConfigMap 中的参数设置环境变量的值。

  • 注入一个 ConfigMap 中的所有值:你可以将一个 ConfigMap 中包含的所有值一次性注入到环境中。如果你为每个 Pod 规范或应用程序创建一个 ConfigMap,那么这种方式非常适合,这样每个应用程序就有一个准备好部署的 ConfigMap。

请注意,无法通过kubectl命令式方法将 ConfigMap 链接到 Pod。原因是无法通过kubectl run命令直接创建引用 ConfigMap 的 Pod。你必须编写声明式的 YAML 文件来在 Pod 中使用你的 ConfigMap。

在本章前面,我们创建了一个名为my-third-configmap的 ConfigMap,它包含一个名为color的参数,值为blue。在这个示例中,我们将创建一个使用quay.io/iamgini/my-flask-app:1.0镜像的 Pod,并将my-third-configmap链接到 Pod,使得 Flask 应用程序容器创建时,环境变量COLOR的值设置为blue,与我们在 ConfigMap 中的设置一致。以下是实现这一目标的 YAML 清单。请注意env:键在container规范中的位置:

# flask-pod-with-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-configmap
  labels:
    app: my-flask-app
spec:
  containers:
    - name: flask-with-configmap
      image: quay.io/iamgini/my-flask-app:1.0
      env:
        - name: COLOR # Any other name works here.
          valueFrom:
            configMapKeyRef:
              name: my-third-configmap
              key: color 

现在,我们可以使用kubectl apply命令创建这个 Pod:

$ kubectl apply -f flask-pod-with-configmap.yaml
pod/flask-pod-with-configmap created
$ kubectl get pod
NAME                       READY   STATUS    RESTARTS   AGE
flask-pod-with-configmap   1/1     Running   0          5s 

现在我们的应用程序 Pod 已经创建完成,让我们在容器内部运行env命令,列出容器中所有可用的环境变量。正如你可能猜到的,我们将在这个特定容器内通过调用kubectl exec命令来执行env Linux 命令。以下是命令及预期输出:

$ kubectl exec pods/flask-pod-with-configmap -- env
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=flask-pod-with-configmap
**COLOR=blue**
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
...<removed for brevity>... 

如果你的 ConfigMap 已正确链接到 Pod,你应该在输出中看到COLOR环境变量。

你还可以通过暴露 Pod 使用服务来检查部署的应用程序,如下所示:

$ kubectl expose pod flask-pod-with-configmap --port=8081 --target-port=5000 --type=NodePort 

让我们通过浏览器使用kubectl port-forward方法来测试应用程序。

你可以启动一个port-forward来测试应用程序,如下所示:

$ kubectl port-forward flask-pod-with-configmap 20000:5000
Forwarding from 127.0.0.1:20000 -> 5000
Forwarding from [::1]:20000 -> 5000 

使用 kubectl port-forward

kubectl port-forward在本地机器和 Kubernetes 集群中的特定 Pod 之间创建一个安全的隧道。这使得你可以像访问本地应用程序一样访问和与运行在 Pod 内的应用程序进行交互。它是一个有价值的调试、测试和开发工具,提供对服务的直接访问,而无需外部暴露。

20000 to the target port 5000 temporarily. You can now access the application in your local browser using the address 127.0.0.1:20000.

虽然 NodePort 服务是为外部访问设计的,但它们也可以在 minikube 环境中使用。在这种情况下,要外部访问你的应用程序,使用minikube service命令获取 NodePort 和相应的 IP 地址。例如,minikube 服务--url flask-pod-with-configmap可能会输出http://192.168.49.2:31997,从而允许你通过这个 URL 访问你的 Flask 应用程序。

现在,你可以通过网页浏览器访问 flask 应用程序,并看到背景为蓝色,因为它在 ConfigMap 中被配置为COLOR=blue。你可以更改 ConfigMap 中COLOR的值并重新创建flask-pod-with-configmap Pod 来查看更改。

我们将在第八章通过服务暴露你的 Pods中学习更多关于 Kubernetes 服务和 DNS 的内容。

请注意,kubectl port-forward会继续转发,直到你结束命令(例如,按下Ctrl + C键)。

现在,我们将探索将 ConfigMap 作为环境变量注入的第二种方式。

在这个示例中,我们将链接另一个 ConfigMap,名为my-fourth-configmap。这次,我们不打算获取该 ConfigMap 中的单个值,而是获取它里面的所有值。以下是更新后的 YAML Pod 清单。这次,我们不使用单独的env键,而是使用envFrom键来代替:

# flask-pod-with-configmap-all.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-configmap-all
spec:
  containers:
    - name: flask-with-configmap
      image: quay.io/iamgini/my-flask-app:1.0
      envFrom:
        - configMapRef:
            name: my-fourth-configmap 

一旦清单文件准备好,你可以重新创建 NGINX Pod:

$ kubectl apply -f flask-pod-with-configmap-all.yaml
pod/ flask-pod-with-configmap-all created 

现在,让我们在nginx容器中再次运行env命令,使用kubectl exec命令列出环境变量:

$ kubectl exec pods/flask-pod-with-configmap-all -- env
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=flask-pod-with-configmap-all
environment=prod
version=1
color=blue
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
...<removed for brevity>... 

请注意,my-fourth-configmap中声明的三个参数– colorversionenvironment已经作为环境变量设置在容器中,但这次,你无法控制环境变量在容器中的命名方式。它们的名称直接继承自 ConfigMap 中的参数键名称。

现在,是时候学习如何将 ConfigMap 作为卷挂载到容器中了。

将 ConfigMap 挂载为卷挂载

在本章前面,我们创建了两个存储虚拟配置文件的 ConfigMap。kubectl 允许你将 ConfigMap 作为卷挂载到 Pod 内部。当 ConfigMap 包含你希望注入到容器文件系统中的文件内容时,这尤其有用。

就像我们注入环境变量时一样,我们需要通过 YAML 清单文件以命令式方式进行操作。在这里,我们将把名为my-sixth-configmap的 ConfigMap 挂载为卷挂载到新的 Pod flask-pod-with-configmap-volume,如下所示:

# flask-pod-with-configmap-volume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-configmap-volume
spec:
  containers:
    - name: flask-with-configmap-volume
      image: quay.io/iamgini/my-flask-app:1.0
      volumeMounts:
        - name: configuration-volume # match the volume name
          mountPath: /etc/conf
  volumes:
    - name: configuration-volume
      configMap:
        name: my-sixth-configmap # Configmap name goes here 

在这里,我们声明了一个名为configuration volume的卷,它与容器处于同一层级,并且我们告诉 Kubernetes 这个卷是从 ConfigMap 构建的。所引用的 ConfigMap(此处为my-sixth-configmap)在我们应用此文件时必须存在于集群中。然后,在容器级别,我们将之前声明的卷挂载到/etc/conf:路径上。ConfigMap 中的参数应当出现在指定的位置。

让我们应用这个文件来创建一个新的 ConfigMap,并将卷附加到我们的集群:

$ kubectl apply -f flask-pod-with-configmap-volume.yaml
pod/flask-pod-with-configmap-volume created 

在容器中运行ls命令,确保目录已经成功挂载:

$  kubectl exec pods/flask-pod-with-configmap-volume -- ls /etc/conf
color
configfile.txt 

在这里,目录已成功挂载,且在目录中创建的两个参数作为普通文件可用。

让我们运行cat命令,确保这两个文件包含正确的值:

$ kubectl exec pods/flask-pod-with-configmap-volume -- cat /etc/conf/color
yellow
$ kubectl exec pods/flask-pod-with-configmap-volume -- cat /etc/conf/configfile.txt
I'm just a dummy config file 

很好!这两个文件包含了我们创建 ConfigMap 时之前声明的值!例如,你可以存储一个虚拟主机 NGINX 配置文件,并将其挂载到正确的目录,从而使 NGINX 根据存储在 ConfigMap 中的配置值提供你的网站。这就是如何覆盖默认配置并在 Kubernetes 中干净地管理你的应用程序。现在,你拥有了一个非常强大且一致的界面来管理和配置运行在 Kubernetes 中的容器。

接下来,我们将学习如何删除和更新 ConfigMap。

删除 ConfigMap

删除 ConfigMap 非常简单。但是,请注意,即使容器正在使用 ConfigMap 的值,你也可以删除 ConfigMap。一旦 Pod 启动,它就与 configmap 对象独立:

$ kubectl delete cm my-first-configmap
configmap "my-first-configmap" deleted 

无论 ConfigMap 的值是否被容器使用,一旦输入此命令,它将立即被删除。请注意,ConfigMap 无法恢复,因此在删除你直接创建的 ConfigMap 时请三思,因为你将无法重新创建它。与声明式创建的 ConfigMap 不同,它的内容不会存储在任何 YAML 文件中。但正如我们之前学到的,通过格式化 kubectl get 命令,仍然可以以 YAML 格式收集这些资源的内容——例如,kubectl get cm <configmap-name> -o YAML > my-first-configmap.yaml

此外,我们建议在删除 ConfigMap 时要小心,特别是当你删除正在运行的 Pod 使用的 ConfigMap。如果 Pod 崩溃了,在没有更新清单文件的情况下,你将无法重新启动它;Pods 会寻找你删除的缺失的 ConfigMap。

让我们用 my-sixth-configmap ConfigMap 测试这个场景;按照以下步骤删除 ConfigMap 资源:

$ kubectl delete cm my-sixth-configmap
configmap "my-sixth-configmap" deleted 

现在,尝试重新启动 flask-pod-with-configmap-volume Pod(或重新创建 Pod)以查看问题:

$ kubectl apply -f flask-pod-with-configmap-volume.yaml
pod/flask-pod-with-configmap-volume created
$ kubectl get pod
NAME                              READY   STATUS              RESTARTS   AGE
flask-pod-with-configmap-volume   0/1     ContainerCreating   0          61s 

你会看到 Pod 处于 ContainerCreating 状态,而不是 Running 状态。让我们查看 Pod 的详细信息,如下所示:

$ kubectl describe pod flask-pod-with-configmap-volume
Name:             flask-pod-with-configmap-volume
Namespace:        default
...<removed for brevity>...
Events:
  Type     Reason       Age               From               Message
  ----     ------       ----              ----               -------
  Normal   Scheduled    71s               default-scheduler  Successfully assigned default/flask-pod-with-configmap-volume to minikube-m03
  **Warning  FailedMount  7s (x8 over 71s)  kubelet            MountVolume.SetUp failed for volume "configuration-volume" : configmap "my-sixth-configmap" not found** 
ContainerCreating state.

更新 ConfigMap

在 Kubernetes 中,有两种主要方法可以更新 ConfigMap。第一种方法是使用 kubectl apply 命令与修改后的 ConfigMap 定义文件。这种方法适用于版本控制和协作环境。只需对 ConfigMap YAML 文件进行必要的更改,并使用 kubectl apply 应用更新。

或者,你可以直接使用 kubectl edit 命令编辑现有的 ConfigMap。这为你提供了一种交互式的方式来修改 ConfigMap 的内容。但是,在使用此方法时要小心,因为它不涉及版本控制。

不可变 ConfigMap

Kubernetes 提供了一个名为 Immutable ConfigMaps 的功能,以防止 ConfigMap 数据被意外或故意修改。通过将 ConfigMap 标记为不可变,你可以确保它的内容保持不变。

此功能对于高度依赖 ConfigMap 的集群特别有益,因为它可以防止配置错误导致应用程序中断。此外,通过减少 kube-apiserver 的负载,它还能提高集群性能。

要创建一个不可变的 ConfigMap,只需在 ConfigMap 定义中将 immutable 字段设置为 true,如下所示:

# immutable-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: immutable-configmap
data:
  color: "blue"
  version: "1.0"
  environment: "prod"
immutable: true 

一旦标记为不可变,您就无法修改 ConfigMap 的数据。如果需要进行更改,您必须删除现有的 ConfigMap 并创建一个新的。为了保持正确的配置,必须重新创建所有引用已删除 ConfigMap 的 Pods。

在本章的下一节中,我们将学习 Kubernetes Secrets。

使用 Secret 对象管理敏感配置

Secret 对象是一个允许您配置运行在 Kubernetes 上的应用程序的资源。Secrets 与 ConfigMap 极为相似,并且可以一起使用。不同之处在于,Secrets 被编码,用于存储敏感数据,如密码、令牌或私有 API 密钥,而 ConfigMap 用于存储非敏感配置数据。除此之外,Secrets 和 ConfigMap 在大多数情况下的行为相同。

为了确保 Kubernetes Secrets 中存储的敏感信息得到保护,请遵循以下最佳实践:

  • 限制访问:利用 基于角色的访问控制RBAC)来根据用户角色和权限限制对 Secrets 的访问。仅授予个人或服务必要的权限。

  • 避免硬编码密钥:绝不将 Secrets 直接嵌入到应用程序代码或配置文件中。

  • 定期轮换密钥:为 Secrets 实施定期轮换计划,以降低未授权访问的风险。

  • 考虑外部秘密管理:对于高级安全需求,探索像 HashiCorp Vault 或 AWS Secrets Manager 这样的专用秘密管理解决方案。

  • 利用加密:使用 KMS 插件对 Secrets 数据进行静态加密,提供额外的保护层。

  • 监控和审计:定期审查访问日志和审计轨迹,以检测可疑活动和潜在的安全漏洞。

  • 教育您的团队:通过提供有关处理和管理 Secrets 的最佳实践的培训,培养安全意识文化。

虽然 Secrets 用于存储敏感信息并且其数据是以 base64 编码的,但仅凭编码并不能保证强大的安全性。Base64 是一种可逆编码格式,这意味着原始数据可以轻易恢复。为了更好地保护敏感信息,请考虑采用额外的安全措施,如使用 KMS 插件进行静态加密或外部的秘密管理解决方案。

虽然 Secrets 主要用于存储敏感数据,但 Kubernetes 提供了额外的安全措施。通过引入密钥管理服务KMS)插件提供者,现在你可以在 Secrets 和 ConfigMaps 中加密数据。这为存储在 ConfigMaps 中的敏感信息提供了额外的保护层,使它们更加安全。

通过对 ConfigMaps 使用 KMS 加密,你可以在不依赖 Secrets 的情况下保护敏感的配置数据。这种方法简化了配置管理,同时保持了高水平的安全性。

即使使用了 KMS 加密,仍然需要仔细考虑存储在 ConfigMaps 中数据的敏感性。对于高度机密的信息,Secrets 依然是推荐的选项。

请参阅文档(kubernetes.io/docs/tasks/administer-cluster/kms-provider/)了解有关数据加密的 KMS 提供者的更多信息。

让我们从了解如何列出 Kubernetes 集群中可用的 Secrets 开始。

列出 Secrets

像其他 Kubernetes 资源一样,你可以使用 kubectl get 命令来列出 Secrets。这里的资源标识符是 Secret:

$ kubectl get secret -A 

与 ConfigMaps 一样,DATA 列告诉你已被哈希并保存到 secret 中的敏感参数数量。当执行时,kubectl 可能会显示一些默认的 Secrets 或报错,表示没有找到资源。这种差异出现的原因是某些云服务为内部流程生成默认的 Secrets,而其他云服务则没有。这些默认 ConfigMaps 的存在与否取决于 Kubernetes 集群的部署环境。

使用 --from-literal 创建一个 Secret(命令式方式)

你可以命令式或声明式地创建一个 Secret —— 两种方式都被 Kubernetes 支持。让我们从了解如何命令式地创建一个 Secret 开始。在这里,我们想将数据库密码 my-db-password 存储在 Kubernetes 集群中的一个 Secret 对象中。你可以通过将 --from-literal 标志添加到 kubectl create secret 命令中来命令式地实现这一点:

$ kubectl create secret generic my-first-secret --from-literal='db_password=my-db-password' 

现在,运行 kubectl get secrets 命令,获取 Kubernetes 集群中 Secrets 的列表。新创建的 Secret 应该会显示出来:

$ kubectl get secrets
NAME              TYPE     DATA   AGE
my-first-secret   Opaque   1      37s 

让我们使用 YAML 输出格式查看 Secret 的详细信息,如下所示:

$ kubectl get secrets my-first-secret -o yaml
apiVersion: v1
data:
  db_password: bXktZGItcGFzc3dvcmQ=
kind: Secret
metadata:
  creationTimestamp: "2024-08-10T13:13:32Z"
  name: my-first-secret
  namespace: default
  resourceVersion: "36719"
  uid: 7ccf5120-d1c5-4874-ba4b-894274fd27e6
type: Opaque 

你将看到包含db_password: bXktZGItcGFzc3dvcmQ=行的data,其中密码以编码格式存储。

现在,让我们来了解如何声明式地创建一个 Secret。

使用 YAML 文件声明式地创建一个 Secret

可以通过使用 YAML 文件声明性地创建 Secrets。虽然可以手动将秘密值编码为 base64 并放入 data 字段,但 Kubernetes 提供了更方便的方法。stringData 字段允许你将秘密值指定为明文字符串。Kubernetes 在创建 Secret 时会自动将这些值编码为 base64 格式。这种方法简化了过程,并帮助防止敏感数据在明文配置文件中被意外暴露。

注意:虽然 base64 编码提供了基本的混淆功能,但必须记住,它并不是一种强加密方法。为了提高安全性,考虑使用 KMS 插件或外部的秘密管理解决方案。

当你使用 --from-literal 时,Kubernetes 会自动将字符串编码为 base64,但如果你从 YAML 清单文件创建 Secret,你需要自己处理这一步。

所以,让我们从将 my-db-password 字符串转换为 base64 开始:

$ echo -n 'my-db-password' | base64
bXktZGItcGFzc3dvcmQ= 

bXktZGItcGFzc3dvcmQ=my-db-password 字符串的 base64 表示形式,这也是我们需要写入 YAML 文件中的内容。以下是正确创建 Secret 对象的 YAML 文件内容:

# ~/secret-from-file.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-second-secret
type: Opaque
data:
    db_password: bXktZGItcGFzc3dvcmQ= 

一旦这个文件存储在你的系统上,你可以使用 kubectl apply 命令创建 secret

$ kubectl apply -f ~/secret-from-file.yaml 

我们可以通过在 Kubernetes 集群中列出所有 secrets 及其详细信息,来确保 secret 已正确创建:

$ kubectl get secret my-second-secret -o yaml
apiVersion: v1
data:
  db_password: bXktZGItcGFzc3dvcmQ=
kind: Secret
...<removed for brevity>...
type: Opaque 

现在,让我们通过使用 stringData 来创建相同的 Secret,这样我们就不需要手动编码它。创建一个新的 YAML 文件,如下所示:

# ~/secret-from-file-stringData.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-secret-stringdata
type: Opaque
stringData:
  db_password: my-db-password 

注意密码是以明文形式而不是编码后的文本形式出现。

如下所示,从文件中创建 Secret:

kubectl apply -f secret-from-file-stringData.yaml
secret/my-secret-stringdata created 

验证 secret 内容并将其与 Secret my-second-secret 进行比较,以查看编码后的内容:

$ kubectl get secrets my-secret-stringdata -o yaml
apiVersion: v1
data:
  db_password: bXktZGItcGFzc3dvcmQ=
kind: Secret
...<removed for brevity>...
  name: my-secret-stringdata
  namespace: default
  resourceVersion: "37229"
  uid: 5078124b-7318-44df-a2da-a30ea5088e3f
type: Opaque 

你会注意到,stringData 已经被编码并存储在 data 部分。

现在,让我们发现 Kubernetes 的另一个功能:从文件中创建 Secret 的能力。

使用文件内容创建 Secret

我们可以像使用 ConfigMaps 一样,通过文件中的值来创建一个 Secret。我们首先创建一个包含秘密值的文件。假设我们需要将密码存储在文件中,并将其作为 Secret 对象导入 Kubernetes 中:

$ echo -n 'mypassword' > ./password.txt 

执行这个命令后,我们会得到一个名为 password.txt 的文件,里面包含一个名为 mypassword 的字符串,它应该是我们的 Secret 值。-n 标志用于确保 password.txt 文件末尾没有额外的空白行。

现在,让我们通过传递 password.txt 的位置给 --from-file 标志来运行 kubectl create secret 命令。这样将创建一个新的 secret,其中包含 mypassword 字符串的 base64 表示形式:

$ kubectl create secret generic mypassword --from-file=./password.txt
secret/mypassword created 

这个新的 secret 现在在你的 Kubernetes 集群中可用!接下来,我们来学习如何读取 Kubernetes Secret。

读取一个 Secret

如观察到的,Secrets 应该托管敏感数据,因此,kubectl 输出不会显示密钥的解码数据,以确保机密性。你必须自己解码才能理解。为什么要保持这种机密性呢?我们来看一下:

  • 为了防止密钥被不应有权限的人意外打开。

  • 为了防止密钥作为终端输出的一部分显示,这可能导致它被记录到某个地方。

虽然 Base64 编码使密钥数据变得模糊,但它并不提供强加密。任何拥有 API 访问权限的用户都可以检索并解码 Secret。为了保护敏感信息,实施 RBAC 来根据用户角色和权限限制对 Secrets 的访问。通过精确定义 RBAC 规则,你可以限制谁可以查看、修改或删除 Secrets,从而增强集群的整体安全性。

由于这些安全性措施,你将无法检索到密钥的实际内容,但仍然可以获取有关其大小等信息。

你可以像我们之前对 ConfigMaps 做的那样,使用 kubectl describe 命令来查看。正如我们之前提到的,ConfigMaps 和 Secrets 非常相似;它们几乎表现得一样:

$ kubectl describe secret/mypassword
Name:         mypassword
Namespace:    default
Labels:       <none>
Annotations:  <none>
Type:  Opaque
Data
====
password.txt:  10 bytes 

如果你的输出与此稍有不同,请不要感到困惑。如果你收到类似的内容,说明新的密钥已经在你的 Kubernetes 集群中可用,并且你成功地检索到了它的数据!

然而,也要记住,编码后的数据可以通过 kubectl 和 YAML 输出格式显示,如下所示:

$ kubectl get secret my-second-secret -o yaml
apiVersion: v1
data:
  db_password: bXktZGItcGFzc3dvcmQ=
kind: Secret
metadata:
  creationTimestamp: "2024-02-13T04:15:56Z"
  name: my-second-secret
  namespace: default
  resourceVersion: "90372"
  uid: 94b7b529-baed-4844-8097-f6c2a001fa7b
type: Opaque 

任何有权限访问此 Secret 的人都可以解码数据,如下所示:

$  echo 'bXktZGItcGFzc3dvcmQ=' | base64 --decode
my-db-password 

重要提示

Base64 编码并不是加密;它仅仅是一种将二进制数据表示为 ASCII 字符的方式。虽然它使数据看起来不那么容易读取,但这并不是加密。任何具备相应知识和工具的人都可以轻松地将其解码回纯文本。为了有效保护 Secrets,可以将 Base64 编码与额外的安全控制措施(如 RBAC 和加密)结合使用,或者考虑使用外部密钥管理解决方案。

现在我们已经探讨了如何创建和管理 Secrets,让我们深入了解如何使这些敏感信息在 Pods 中的应用程序可以访问。

将密钥作为环境变量进行使用

我们已经看到如何将 ConfigMap 的值作为环境变量注入到 Pod 中,我们也可以对 Secrets 做同样的操作。回到我们 NGINX 容器的例子中,我们将检索 my-first-secret Secret 的 db_password 值,并将其作为环境变量注入到 Pod 中。以下是 YAML 清单。同样,所有操作都发生在 env: 键下:

# flask-pod-with-secret.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-secret
  labels:
    app: flask-with-secret
spec:
  containers:
    - name: flask-with-secret
      image: quay.io/iamgini/my-flask-app:1.0
      env:
        - name: PASSWORD_ENV_VAR # Name of env variable
          valueFrom:
            secretKeyRef:
              name: my-first-secret # Name of secret object
              key: db_password # Name of key in secret object 

现在,你可以使用 kubectl apply 命令来应用这个文件:

$ kubectl apply -f flask-pod-with-secret.yaml 

现在,运行 env 命令来列出容器中的环境变量:

$  kubectl exec pods/flask-pod-with-secret -- env
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=flask-pod-with-secret
**PASSWORD_ENV_VAR=my-db-password**
KUBERNETES_SERVICE_PORT=443
...<removed for brevity>... 

如你所见,my-db-password 字符串作为环境变量 PASSWORD_ENV_VAR 可用。

查找 Secret 详细信息的另一种方法是使用 envFrom YAML 键。使用此键时,您将从 Secret 中读取所有值,并将它们作为环境变量一次性加载到 Pod 中。其工作原理与 ConfigMap 对象相同。

要使 Pod 成功启动,引用的 Secret 必须存在,除非在 Pod 定义中明确标记为可选。请确保在部署依赖于该 Secret 的 Pod 之前先创建该 Secret。

先通过使用以下 YAML 和 envFrom 示例创建一个 Secret:

# secret-from-file-database.yaml
apiVersion: v1
kind: Secret
metadata:
  name: appdb-secret
type: Opaque
stringData:
  db_user: appadmin
  db_password: appdbpassword 
$ kubectl apply -f secret-from-file-database.yaml
secret/appdb-secret created 

这里是一个之前的 Pod 示例,但已使用 envFrom 键进行了更新:

# flask-pod-with-secret-all.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-secret-all
  labels:
    app: flask-with-secret
spec:
  containers:
    - name: flask-with-secret
      image: quay.io/iamgini/my-flask-app:1.0
      envFrom:
        - secretRef:
            name: appdb-secret # Name of the secret object 

使用 kubectl apply 命令创建 Pod:

$ kubectl apply -f flask-pod-with-secret-all.yaml
pod/flask-pod-with-secret-all created 

使用此方法,Secret 对象中的所有键都将作为 Pod 中的环境变量使用。让我们验证 Pod 中的环境变量,以确保 Secret 对象中的变量已正确加载:

$ kubectl exec pods/flask-pod-with-secret-all -- env |grep -i app
db_user=appadmin
db_password=appdbpassword 

请注意,如果某个键名不能作为环境变量名使用,它将被忽略!

现在,让我们在下一节学习如何将 Secret 作为卷挂载使用。

将 Secret 作为卷挂载使用

您可以将 Secrets 作为卷挂载到 Pod 中,但只能以声明方式进行。因此,您必须编写 YAML 文件才能成功实现这一点。

您必须从一个 YAML 清单文件开始,该文件将创建一个 Pod。以下是一个挂载名为 mypassword 的 Secret 到 /etc/passwords-mounted-path 路径的 YAML 文件:

# flask-pod-with-secret-volume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: flask-pod-with-secret-volume
  labels:
    app: flask-with-secret-volume
spec:
  containers:
    - name: flask-with-secret
      image: quay.io/iamgini/my-flask-app:1.0
      volumeMounts:
        - name: mysecret-volume
          mountPath: '/etc/password-mounted-path'
          readOnly: true  # Setting readOnly to true to prevent writes to the secret
  volumes:
    - name: mysecret-volume
      secret:
        secretName: my-second-secret # Secret name goes here 

一旦您在文件系统中创建了此文件,可以使用 kubectl 应用 YAML 文件:

$ kubectl apply -f flask-pod-with-secret-volume.yaml 

在尝试创建 Secret 之前,请确保my-second-secret已存在。

最后,您可以在 flask-with-secret 中运行一个命令,使用 kubectl exec 命令,检查包含 Secret 的卷是否已正确设置:

$ kubectl exec pods/flask-pod-with-secret-volume --  cat /etc/password-mounted-path/db_password
my-db-password 

如您所见,my-db-password 字符串已正确显示;Secret 已成功挂载为卷!

现在我们已经学会了如何在 Kubernetes 中创建 Secret,并通过多种方法将其与 Pod 配合使用,让我们在下一节学习如何删除和更新 Secrets。

删除一个 Secret

删除一个 Secret 非常简单,可以通过 kubectl delete 命令完成:

$ kubectl delete secret my-first-secret
secret "my-first-secret" deleted 

现在,让我们学习如何在 Kubernetes 集群中更新一个现有的 Secret。

更新一个 Secret

要更新 Kubernetes 中的 Secret,您可以使用 kubectl apply 命令并修改 Secret 定义,或使用 kubectl edit 命令。

Kubernetes Secrets 提供了一种安全存储和管理敏感信息(如密码、API 密钥和证书)的方法。与 ConfigMaps 不同,Secrets 是经过编码的,以保护数据的机密性。本节介绍了如何使用 YAML 命令和声明性方法创建 Secrets。您学习了如何通过环境变量和卷挂载将 Secrets 注入 Pod,从而使应用程序能够访问敏感数据,而无需以明文形式暴露。

概述

本章深入探讨了 Kubernetes 配置管理的基本概念。我们研究了 ConfigMap 和 Secrets 之间的关键区别,了解了它们在处理非敏感数据和敏感数据方面的各自作用。通过有效利用这些 Kubernetes 资源,您可以显著增强应用程序的可移植性和安全性。

我们学习了如何创建和管理 ConfigMap 和 Secrets,采用了命令式和声明式方法。您还了解了如何通过环境变量和卷挂载将配置信息注入到 Pods 中,确保应用程序设置的无缝访问。

为了保护敏感信息,我们强调了实施强大安全措施的重要性,超越了仅使用 base64 编码。通过结合 RBAC、加密和外部机密管理解决方案,您可以显著增强 Kubernetes 环境的安全性。

通过掌握本章介绍的概念,您将能够构建具有弹性和安全性的 Kubernetes 应用程序,这些应用程序与配置解耦,从而促进灵活性和可维护性。在下一章中,我们将继续探索 Kubernetes,介绍 Kubernetes 的另一个核心概念——服务。服务是 Kubernetes 对象,允许您将 Pods 暴露给彼此以及互联网。这是 Kubernetes 中一个非常重要的网络概念,掌握它对于正确使用 Kubernetes 至关重要。幸运的是,掌握服务并不复杂,下一章将讲解如何实现这一点。您将学习如何将容器的端口与其运行的工作节点的端口关联,以及如何将静态 IP 地址与 Pods 关联,以便其他集群中的 Pods 始终可以通过相同的地址访问它们。

进一步阅读

加入我们在 Discord 上的社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第八章:使用 Services 暴露你的 Pods

阅读了前几章之后,你现在已经知道如何通过构建 Pods 来部署应用程序,这些 Pods 可以包含一个容器,或者在更复杂的应用程序中包含多个容器。你还知道,通过将 Pods 和 ConfigMaps 一起使用,可以将应用程序与其配置解耦,并且 Kubernetes 也能够存储你的敏感配置,感谢 Secret 对象。

好消息是,凭借这三种资源,你可以开始在 Kubernetes 上正确地部署应用程序并让你的第一个应用程序运行。不过,你仍然缺少一些重要的东西:你需要能够将 Pods 暴露给终端用户,甚至暴露给 Kubernetes 集群中的其他 Pods。这就是 Kubernetes Services 的作用,我们现在就来探索这个概念!

在本章中,我们将学习一种新的 Kubernetes 资源类型,称为 Service。由于 Kubernetes Services 是一个涉及众多内容的大主题,本章将包含大量信息,内容也会比较多。但一旦你掌握了这些 Services,你就能暴露你的 Pods,并将终端用户连接到你的应用程序!

服务也是掌握 高可用性 (HA) 和冗余的关键概念。在 Kubernetes 配置中,简单来说,掌握它们对于有效使用 Kubernetes 至关重要!

在本章中,我们将覆盖以下主要内容:

  • 为什么你需要暴露你的 Pods?

  • NodePort Service

  • ClusterIP Service

  • LoadBalancer Service

  • ExternalName Service 类型

  • 使用探针实现服务就绪状态

技术要求

要跟随本章中的示例,请确保你具备以下条件:

  • 一个正常工作的 Kubernetes 集群(无论是本地的还是基于云的,都无关紧要)

  • 一个正常工作的 kubectl 命令行界面 (CLI),已配置与 Kubernetes 集群进行通信

你可以从官方 GitHub 仓库下载本章的最新代码示例,地址为 github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter08

为什么你需要暴露你的 Pods?

在前几章中,我们讨论了微服务架构,它通过 表现层状态转移 (REST) 应用程序编程接口 (APIs) 暴露你的功能。这些 APIs 完全依赖于 超文本传输协议 (HTTP),这意味着你的微服务必须可以通过网络访问,因此必须通过 互联网协议 (IP) 地址进行访问。

尽管 REST API 常用于微服务架构中的通信,但在服务之间的性能和效率至关重要的场景下,评估其他通信协议,如gRPC,是很重要的。gRPC 基于HTTP/2并使用二进制序列化(协议缓冲区),可以在分布式系统中提供显著的优势,例如更快的通信、更低的延迟和支持流媒体。在默认使用 REST 之前,考虑一下 gRPC 是否更适合你的系统需求。

在接下来的部分,我们将学习 Kubernetes 中的集群网络。

Kubernetes 中的集群网络

网络是 Kubernetes 的一个基本方面,使容器、Pod、服务和外部客户端之间能够进行通信。了解 Kubernetes 如何管理网络有助于确保分布式环境中的顺利运行。Kubernetes 解决了四个关键的网络挑战:

  • 容器到容器的通信:这通过 Pod 来解决,允许同一个 Pod 中的容器通过 localhost(即内部通信)进行通信。

  • Pod 之间的通信:Kubernetes 通过其网络模型实现了跨节点的 Pod 间通信。

  • Pod 到服务的通信:服务抽象了一组 Pod,并提供稳定的端点进行通信。

  • 外部到服务的通信:这允许集群外部的流量访问服务,如 Web 应用程序或 API。

在 Kubernetes 集群中,多个应用程序运行在同一组机器上。这会带来一些挑战,例如,当不同的应用程序使用相同的网络端口时,如何防止冲突。

Kubernetes 中的 IP 地址管理

Kubernetes 集群使用不重叠的 IP 地址范围来为 Pod、服务和节点分配地址。网络模型通过以下配置实现:

  • Pods:一个网络插件,通常是容器网络接口CNI)插件,为 Pod 分配 IP 地址。

  • 服务:kube-apiserver 负责为服务分配 IP 地址。

  • 节点:节点的 IP 地址由 kubelet 或 cloud-controller-manager 管理。

你可以参考以下网站,了解更多 Kubernetes 中的网络知识:kubernetes.io/docs/concepts/cluster-administration/networking/

现在,在探讨 Pod 网络和服务之前,让我们了解一下 Kubernetes 网络空间中的一些基础技术。

学习网络插件

CNI是由云原生计算基金会CNCF)开发的一个规范和一组库。其主要目的是标准化 Linux 容器上网络接口的配置,实现容器与外部环境之间的无缝通信。

这些插件对于实现 Kubernetes 网络模型至关重要,确保集群内的连接性和通信。选择一个与您的 Kubernetes 集群需求和兼容性要求相匹配的 CNI 插件非常重要。在 Kubernetes 生态系统中有各种插件,包括开源和闭源插件,选择合适的插件对于确保集群顺利运行至关重要。

这里是 CNI 插件的简洁列表,每个插件的简要描述:

开源 CNI 插件:

  • Calico:提供网络和网络安全,重点是第 3 层连接性和细粒度的策略控制。

  • Flannel:一个简单的 CNI,为 Kubernetes 集群提供基本的网络功能,适合覆盖网络。

  • Weave Net:专注于简单的设置和加密,适用于云环境和本地环境。

  • Cilium:利用 eBPF 提供先进的安全功能和网络可观察性,完美适用于微服务架构。

闭源 CNI 插件:

  • AWS VPC CNI:将 Kubernetes Pods 与 AWS VPC 直接集成,实现无缝的 IP 管理和连接。

  • Azure CNI:允许 Pods 使用 Azure VNet 中的 IP 地址,确保与 Azure 网络基础设施的集成。

  • VMware NSX-T:为 VMware 环境中的 Kubernetes 提供先进的网络功能,如微分段和安全性。

在本书的一些练习中,我们将使用 Calico。现在,让我们了解什么是服务网格。

什么是服务网格?

服务网格 是微服务架构中不可或缺的基础设施层,促进服务间的通信。它包括服务发现、负载均衡、加密、认证和监控等功能,所有这些都在网络基础设施内实现。通常,服务网格是通过部署在各个服务旁边的轻量级代理来实现的,从而精确控制流量路由,并强制执行如速率限制和断路等策略。通过将复杂的网络任务抽象化,服务网格简化了微服务应用程序的开发、部署和管理,同时提高了可靠性、安全性和可观察性。著名的服务网格实现包括IstioLinkerdConsul Connect。然而,值得注意的是,这个话题超出了本书的范围。

接下来,我们将解释它们的用途以及它们如何帮助您暴露 Pod 启动的微服务。

理解 Pod 的 IP 分配

要理解什么是服务,我们需要再次讨论一下 Pods。在 Kubernetes 中,一切都是 Pod 管理:Pods 托管您的应用程序,并且它们具有一个特殊的属性。Kubernetes 在 Pods 被创建在您的集群中时会为其分配一个私有 IP 地址。记住这一点,因为它非常重要:集群中创建的每个 Pod 都会被 Kubernetes 分配一个唯一的 IP 地址。

为了说明这一点,我们将从创建一个 nginx Pod 开始。我们在这里使用 nginx 容器镜像来创建一个 Pod,但实际上,使用任何容器镜像创建 Pod 都会得到相同的结果。

让我们使用声明式的方法,结合以下 YAML 定义来完成这项操作:

# new-nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: new-nginx-pod
spec:
  containers:
  - image: nginx:1.17
    name: new-nginx-container
    ports:
    - containerPort: 80 

从之前的 YAML 文件中可以看到,这个名为 new-nginx-pod 的 Pod 没有什么特别之处,它只会基于 nginx:1.17 容器镜像启动一个名为 new-nginx-container 的容器。

一旦我们有了这个 YAML 文件,我们可以使用以下命令应用它,让 Pod 在我们的集群上运行:

$ kubectl apply -f new-nginx-pod.yaml
pod/new-nginx-pod.yaml created 

一旦调用这个命令,Pod 就会在集群中创建,并且在 Pod 被创建的瞬间,Kubernetes 会为其分配一个唯一的 IP 地址。

-o wide option, which will display the IP address as part of the output:
$  kubectl get po -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP             NODE       NOMINATED NODE   READINESS GATES
new-nginx-pod   1/1     Running   0          99s   10.244.0.109   minikube   <none>           <none> 

在我们的例子中,IP 地址是 10.244.0.109。这个 IP 地址在我的 Kubernetes 集群中是唯一的,并且被分配给这个特定的 Pod。

当然,如果你在自己的集群中进行操作,你会看到不同的 IP 地址。这个 IP 地址是私有的 IPv4 地址,并且只存在于 Kubernetes 集群中。如果你尝试在浏览器中输入这个 IP 地址,你将不会得到任何响应,因为这个地址并不存在于外部网络或公共互联网中,它只存在于你的 Kubernetes 集群内。

无论你使用的是哪种云平台——无论是Amazon Web ServicesAWS)、Google Cloud PlatformGCP)还是 Azure——Kubernetes 集群都利用云服务商提供的网络段。这个网络段通常被称为虚拟私有云VPC),定义了一个私有且隔离的网络,类似于你本地局域网中的私有 IP 范围。在所有情况下,Kubernetes 使用的 CNI 插件确保每个 Pod 都被分配一个唯一的 IP 地址,从而在 Pod 层面提供细粒度的隔离。这一规则适用于所有云环境和本地环境。

我们现在将发现,这个 IP 地址分配是动态的,并且还会了解到它在规模化过程中可能会带来的问题。

理解 Kubernetes 中 Pod IP 分配的动态性

分配给 Pods 的 IP 地址不是静态的,如果你删除并重新创建一个 Pod,你会发现该 Pod 会获得一个新的 IP 地址,这个地址与之前使用的地址完全不同,即使是用相同的 YAML 配置重新创建的。为了演示这一点,我们来删除 Pod 并使用相同的 YAML 文件重新创建它,如下所示:

$ kubectl delete -f new-nginx-pod.yaml
pod/new-nginx-pod.yaml deleted
$ kubectl apply -f new-nginx-pod.yaml
pod/new-nginx-pod.yaml created 

现在我们可以再次运行 kubectl get pods -o wide 命令,来确认新的 IP 地址与之前的不同,如下所示:

$ kubectl get pods -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP             NODE       NOMINATED NODE   READINESS GATES
new-nginx-pod   1/1     Running   0          97s   10.244.0.110   minikube   <none>           <none> 

现在,IP 地址是 10.244.0.110。这个 IP 地址与之前的 10.244.0.109 不同。

正如你所看到的,当一个 Pod 被销毁然后重新创建时,即使你用相同的名称和配置重新创建它,它也会有一个不同的 IP 地址。

原因是从技术上讲,这不是同一个 Pod 而是两个不同的 Pod;这就是为什么 Kubernetes 分配两个完全不同 IP 地址的原因。

现在,想象一下,您有一个应用程序访问那个使用其 IP 地址与之通信的 nginx Pod。如果由于某种原因删除并重新创建 nginx Pod,则您的应用程序将中断,因为该 IP 地址将不再有效。

在下一节中,我们将讨论为什么不建议在应用程序代码中硬编码 Pod IP 地址,并探讨在生产环境中所面临的挑战。我们还将探讨在 Kubernetes 中确保稳定微服务之间通信的更可靠方法。

在应用程序开发中不要硬编码 Pod 的 IP 地址

在生产环境中,依赖 Pod IP 地址进行应用程序通信带来了重大挑战。设计用于通过 HTTP 相互交互且依赖 TCP/IP 的微服务,需要一种可靠的方法来识别并连接彼此。

因此,建立一种强大的机制来检索 Pod 信息,而不仅仅是 IP 地址,是至关重要的。这样可以确保即使 Pod 在工作节点间被重新创建或重新调度,通信也能保持一致。

至关重要的是,应避免直接在应用程序中硬编码 Pod IP 地址,因为 Pod IP 地址是动态的。Pod 的短暂性质意味着它们可以被删除、重新创建或移动,这使得在应用程序的 YAML 中硬编码 Pod IP 地址的做法变得不可靠。如果具有硬编码 IP 的 Pod 被重新创建,依赖它的应用程序将由于 IP 解析为空而失去连接。

我们可以提供一些具体案例,说明这个问题可能会出现,如下所述:

  • 运行微服务 A 的 Pod 有一个依赖项,并调用同一 Kubernetes 集群中另一个 Pod 上运行的微服务 B

  • 作为 Pod 运行的应用程序需要从同一 Kubernetes 集群上也作为 Pod 运行的 MySQL 服务器检索一些数据。

  • 一个应用程序在同一集群中的多个 Pod 上部署了 Redis 集群 作为缓存引擎。

  • 您的最终用户通过调用 IP 地址访问应用程序,由于 Pod 失败,该 IP 地址会发生变化。

任何时候当服务之间存在互联或任何网络通信时,都会出现这个问题。

解决此问题的方法是使用 Kubernetes Service 资源。

Service 对象将作为一个中间对象留在您的集群上。Service 不应被销毁,但即使被销毁,也可以重新创建而不会有任何影响,因为使用的是 Service 名称,而不是 IP 地址。实际上,它们可以长期保留在您的集群中而不会引起任何问题。Service 对象提供了一个抽象层,通过整个生命周期在网络层级上暴露在 Pod(s) 中运行的应用程序,而无需进行任何代码或配置更改。

理解 Services 如何将流量路由到 Pods

Kubernetes Services 作为集群中的资源存在,并充当网络流量管理的抽象层。Services 利用 CNI 插件促进客户端与后端 Pods 之间的通信。Services 通过创建服务端点实现这一点,服务端点代表一组 Pods,支持负载均衡并确保流量到达健康的实例。

Kubernetes Services 提供了一种静态且可靠的方式来访问集群中的 Pods。即使底层 Pods 因部署、扩展或重启而发生变化,它们提供的 DNS 名称也会保持不变。Services 利用 服务发现 机制和内部 负载均衡 有效地将流量路由到健康的 Pods。

图 8.1:service-webapp 正在暴露 webapp Pods 1、2 和 3,而 service-backendapp 正在暴露 backendapp Pods 1 和 2。

实际上,Services 作为资源类型部署到 Kubernetes,就像大多数 Kubernetes 对象一样,你可以使用交互式命令或声明式 YAML 文件将它们部署到集群中。

和 Kubernetes 中的其他资源一样,当你创建一个 Service 时,必须为它指定一个名称。Kubernetes 会使用这个名称来构建一个 DNS 名称,集群中的所有 Pods 都能访问这个名称。这个 DNS 条目会解析到你的 Service,它应该始终驻留在集群中。唯一有点棘手的部分是你需要为 Service 提供一个 Pods 列表:我们将在本章中学习如何做到这一点。别担心,这只是一个基于 标签选择器 的配置。

一旦所有配置完成,你只需通过调用 Service 来访问 Pods。这个 Service 会接收请求并将其转发到 Pods。就这么简单!

理解 Kubernetes 中的轮询负载均衡

Kubernetes Services 一旦配置正确,就可以暴露一个或多个 Pods。当同一个 Service 暴露多个 Pods 时,使用轮询算法将请求均匀地负载均衡到背后的 Pods,如下图所示:

图 8.2:Service A 将三个请求代理到它背后的三个 Pods。在大规模情况下,每个 Service 将接收到由 Service 收到的 33% 请求。

扩展应用变得容易。只需通过 Pod 副本向 Service 添加更多的 Pods 即可。你将在第十一章使用 Kubernetes 部署无状态工作负载中了解有关 Deployments 和副本的内容。由于 Kubernetes Service 实现了轮询逻辑,它可以将请求均匀地代理到其背后的 Pods。

Kubernetes Services 提供的不仅仅是轮询负载均衡。虽然轮询通常在使用 kube-proxy 的 IPVS 模式的设置中使用,但需要注意的是,iptables(许多发行版中的默认模式)通常使用随机或基于哈希的方法来分配流量。

Kubernetes 还支持其他负载均衡算法以满足各种需求:最少连接数用于平衡负载,源 IP 用于一致路由,甚至为更复杂的场景提供自定义逻辑。用户还应注意,IPVS 提供了更高级的流量管理功能,如会话亲和性和流量整形,而这些功能可能在 iptables 模式下不可用。

了解你的集群正在使用的模式(无论是 iptables 还是 IPVS)可以帮助你根据你的扩展和流量分配需求微调服务的行为。有关更多信息,请参考文档 (kubernetes.io/docs/reference/networking/virtual-ips/)。

如果前面的 Pod 有四个副本,那么每个副本大约会接收到该服务收到的所有请求的 25%。如果该服务背后有 50 个 Pod,那么每个 Pod 大约会接收到该服务收到的所有请求的 2%。你只需要理解的是,服务通过遵循特定的负载均衡算法,表现得像负载均衡器一样。

现在让我们探索如何从另一个 Pod 中调用 Kubernetes 中的服务。

了解如何在 Kubernetes 中调用服务

当你在 Kubernetes 中创建一个服务时,它会附加到两个非常重要的事物,如下所示:

  • 一个 IP 地址,它是唯一且特定的(就像 Pod 获得它们自己的 IP 一样)

  • 一个自动生成的内部 DNS 名称,它不会改变并且是静态的

你可以使用其中任何一个来访问服务,然后它将把你的请求转发到其后端配置的 Pod。大多数情况下,你会通过其生成的 DNS 名称调用该服务,它是容易确定且可预测的。让我们探索一下 Kubernetes 如何为服务分配 DNS 名称。

了解如何为服务生成 DNS 名称

为服务生成的 DNS 名称是根据其名称派生的。例如,如果你创建一个名为 my-app-service 的服务,它的 DNS 名称将是 my-app-service.default.svc.cluster.local

这个比较复杂,所以我们将它分解成更小的部分,如下所示:

图 8.3:服务 FQDN

这两个活动部分是前两个部分,基本上是服务名称和它所在的命名空间。DNS 名称将始终以 .svc.cluster.local 字符串结尾。

因此,在任何时刻,只要你在集群中的任何地方尝试使用 curlwget 调用 my-app-service.default.svc.cluster.local 地址,你就知道你会到达你的服务。

该名称将在从集群中的 Pod 执行时解析为该服务。但默认情况下,如果服务没有配置来获取要代理的 Pod 列表,它们不会代理任何内容。我们现在将探索如何做到这一点!

服务如何在 Kubernetes 中发现并将流量路由到 Pod

在 Kubernetes 中使用服务时,你经常会遇到“暴露”你的 Pods 这一概念。实际上,这就是 Kubernetes 用来表示服务正在将网络流量代理到 Pods 上的术语。这个术语无处不在:有一天你的同事可能会问你,“哪个服务暴露了那个 Pod?”以下截图显示了被暴露的 Pods:

图 8.4:Webapp Pods 1、2 和 3 通过 service-webapp 暴露,而 backendapp Pods 1 和 2 通过 service-backendapp 暴露。

你可以通过kubectl在一条命令中成功创建一个 Pod 和一个服务来暴露它,使用--expose参数。为了演示这个例子,我们创建一个 nginx Pod 和服务,如下所示。

我们还需要为命令提供一个端口,以指定该服务将在哪个端口上可访问:

$ kubectl run nginx --image nginx --expose=true --port=80
service/nginx created
pod/nginx created 

现在,让我们使用kubectl列出 Pods 和服务,以演示以下命令创建了这两个对象:

$ kubectl get po,svc nginx
NAME        READY   STATUS    RESTARTS   AGE
pod/nginx   1/1     Running   0          24s
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/nginx   ClusterIP   10.111.12.100   <none>        80/TCP    24s 

如你从命令的输出结果中看到的那样,两个对象都已经创建。我们之前提到过,服务可以根据 Pod 的标签来查找需要暴露的 Pod。我们刚刚创建的nginx Pod 当然有一些标签。为了展示这些标签,我们可以运行kubectl get pods nginx --show-labels命令。

--show-labels parameter, which will display the labels as part of the output:
$  kubectl get po nginx --show-labels
NAME    READY   STATUS    RESTARTS   AGE   LABELS
nginx   1/1     Running   0          51s   run=nginx 

如你所见,一个名为run、值为nginx的标签被添加到了创建的nginx Pod 上。现在让我们描述一下nginx服务。它应该有一个与这个标签匹配的选择器。代码如下:

$ kubectl describe svc nginx
Name:              nginx
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          run=nginx
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.111.12.100
IPs:               10.111.12.100
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.244.0.9:80
Session Affinity:  None
Events:            <none> 

你可以清楚地看到,服务有一行叫做 Selector,它匹配了分配给nginx Pod 的标签。通过这种方式,两个对象之间建立了连接。我们现在可以 100% 确定,服务可以访问nginx Pod,并且一切应该正常工作。

请注意,如果你在实验室中使用 minikube,你将无法从集群外部访问该服务,因为 ClusterIP 服务只能从集群内部访问。你需要使用调试方法,如kubectl port-forwardkubectl proxy,来处理这种情况。你将在下一节中学习如何测试 ClusterIP 类型的服务。

此外,为了测试服务的访问,让我们创建一个临时的端口转发,如下所示:

$ kubectl port-forward pod/nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

现在,打开另一个控制台并访问以下 URL:

$ curl 127.0.0.1:8080
<!DOCTYPE html>
<html>
...<removed for brevity>... 

在前面的代码片段中,端口8080是我们用于端口转发的本地主机端口,而 80 是 nginx 端口,用于暴露 Web 服务。

请注意,kubectl port-forward命令将持续运行,直到你通过 Ctrl+C 终止它。

虽然这样可以工作,但我们强烈建议你在生产环境中永远不要这么做。服务是高度可定制的对象,而--expose参数隐藏了它们的许多功能。相反,你应该使用声明式语法,并调整 YAML 文件以满足你的具体需求。

让我们通过使用dnsutils容器镜像来演示这一点。

使用调试工具 Pod 来调试你的服务。

由于您的服务是在集群内创建的,因此通常很难像我们之前提到的那样访问它们,特别是如果我们的 Pod 旨在仅在您的集群内保持可访问性,或者如果您的集群没有互联网连接等。

在这种情况下,最好在您的集群中部署一个调试 Pod,只需安装一些二进制文件即可运行基本的网络命令,例如wgetnslookup等。让我们使用我们的自定义实用容器镜像quay.io/iamgini/k8sutils:debian12来实现这个目的。

如果需要,您可以在实用容器镜像中添加更多工具或实用程序;参考Chapter-070/Containerfile获取源代码。

在这里,我们将通过调用暴露 Pod 的服务来对nginx Pod 的首页运行 curl。该服务的名称只是nginx。因此,我们可以忽略 Kubernetes 为其分配的 DNS 名称:nginx.default.svc.cluster.local

如果您尝试从集群内部的 Pod 访问这个统一资源定位符URL),您应该可以成功访问nginx首页。

下面的 Pod 定义将帮助我们使用k8sutils镜像创建一个调试 Pod。

# k8sutils.yaml
apiVersion: v1
kind: Pod
metadata:
  name: k8sutils
  namespace: default
spec:
  containers:
    - name: k8sutils
      image: **quay.io/iamgini/k8sutils:debian12**
      command:
        - sleep
        - "infinity"
      # imagePullPolicy: IfNotPresent
  restartPolicy: Always 

让我们运行以下命令,在我们的集群上启动k8sutils Pod:

$ kubectl apply -f k8sutils.yaml
pod/k8sutils created 

现在运行kubectl get pods命令,以验证 Pod 是否成功启动,如下所示:

$ kubectl get po k8sutils
NAME       READY   STATUS    RESTARTS   AGE
k8sutils   1/1     Running   0          13m 

太棒了!现在让我们从k8sutils Pod 运行nslookup命令来查询服务的 DNS 名称,如下所示:

$ kubectl exec -it k8sutils -- nslookup nginx.default.svc.cluster.local
Server:         10.96.0.10
Address:        10.96.0.10#53
Name:   nginx.default.svc.cluster.local
Address: 10.106.124.200 

在前面的代码片段中,

  • Server: 10.96.0.10 - 是kube-dns服务的 IP 地址(kubectl get svc kube-dns -n kube-system -o wide)。如果您使用不同的 DNS 服务,请相应地检查服务详细信息。

  • nginx.default.svc.cluster.local解析为nginx服务的 IP 地址(kubectl get svc nginx -o wide),即10.106.124

一切看起来都很好。现在让我们运行一个curl命令来检查我们是否可以获取nginx首页,如下所示:

$ kubectl exec -it k8sutils -- curl nginx.default.svc.cluster.local
...<removed for brevity>....
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...<removed for brevity>... 

这里一切都很完美!我们成功通过使用k8sutils调试 Pod 调用了nginx服务,如下截图所示:

图 8.5:k8sutils Pod 用于对 nginx 服务运行 curl,与服务背后的 nginx Pod 通信

请记住,您需要在集群内部署一个k8sutils Pod 以便调试服务。确实,nginx.default.svc.cluster.local DNS 名称不是公共名称,只能从集群内部访问。

让我们解释为什么不应该使用expose命令来暴露您的 Pod,在下一节中。

了解在 Kubernetes 中直接使用 kubectl expose 的缺点

不建议使用kubectl expose来创建服务,因为您无法控制服务的创建方式。默认情况下,kubectl expose将创建一个ClusterIP服务,但您可能希望创建一个NodePort服务。

也可以使用命令式语法定义 Service 类型,但最终你需要执行的命令将会非常长且难以理解。因此,我们鼓励你不要使用 expose 选项,而是对像 Service 这样复杂的对象使用声明式语法。

现在让我们讨论在 Kubernetes 中使用 Services 时,DNS 名称是如何生成的。

理解 DNS 名称是如何为 Services 生成的

你现在已经知道,Kubernetes 中的 Service 到 Pod 的通信完全依赖于 Pod 端的标签和 Service 端的选择器。

如果你没有正确使用这两者之间的配合,通信将无法建立。

流程如下:

  1. 你创建了一些 Pods,并且随意设置了标签。

  2. 你创建一个 Service,并配置其选择器以匹配 Pods 的标签。

  3. Service 启动后会查找与其选择器匹配的 Pods。

  4. 你可以通过 DNS 或 IP 调用 Service(DNS 更为便捷)。

  5. Service 会将流量转发到与其标签匹配的某个 Pod。

如果你查看之前通过命令式风格和 kubectl expose 参数实现的例子,你会发现 Pod 和 Service 分别使用了合适的标签(在 Pod 端)和选择器(在 Service 端),这就是为什么 Pod 能够成功暴露的原因。请注意,在实际应用中,你需要为 Pods 使用合适的标签,而不是默认标签。

除此之外,你现在必须理解,Kubernetes 中不仅有一种类型的 Service,而是有几种类型的 Service——让我们更深入地了解一下。

理解不同类型的 Services

Kubernetes 中有几种类型的 Service。虽然 Kubernetes 中只有一种叫做 Service 的类型,但这种类型可以通过不同的配置来实现不同的效果。

幸运的是,不论你选择哪种类型的 Service,目标始终不变:使用单一的静态接口来暴露你的 Pods。

每种类型的 Service 都有其特定的功能和用途,因此基本上,每种用途都有对应的 Service。一个 Service 不能同时属于多种类型,但你仍然可以通过使用具有不同类型的两个 Service 对象来暴露相同的 Pods,只要这些 Service 对象的名称不同,Kubernetes 就能为其分配不同的 DNS 名称。

在本章中,我们将介绍三种主要类型的 Services,如下所示:

  • NodePort:这种类型将主机机器(工作节点)上的临时端口范围内的一个端口绑定到 Pod 上的一个端口,从而使其可以公开访问。通过调用主机机器的端口,你将能够访问关联的 Kubernetes Pod。这是从集群外部访问你的 Pods 的方法。

  • ClusterIPClusterIP 服务是用于 Kubernetes 集群内 Pod 之间私密通信的服务。这就是我们在本章中实验过的服务,也是 kubectl expose 默认创建的服务。它无疑是最常用的,因为它允许 Pod 之间的相互通信:正如它的名字所示,它有一个集群范围内设置的静态 IP 地址。通过访问该 IP 地址,你将被重定向到其背后的 Pod。如果有多个 Pod 在背后,ClusterIP 服务将提供负载均衡机制,采用轮询或其他算法。

  • LoadBalancer:Kubernetes 中的 LoadBalancer 服务简化了将 Pod 暴露给外部流量的过程。它通过在支持的云平台上自动配置一个云特定的负载均衡器(如 AWS ELB)来实现这一点。这消除了在云环境中手动设置外部负载均衡器的需求。然而,需要认识到,除非手动配置,否则该服务不支持裸金属或非云管理的集群。虽然 Terraform 等替代方案可以用于管理云基础设施,但 LoadBalancer 服务为无缝集成云原生负载均衡器到 Kubernetes 部署中提供了一个便捷的选项,特别是在以云为中心的场景中。请记住,它的适用性取决于你的具体需求和基础设施配置。

现在,让我们立即深入了解第一种类型的服务——NodePort 服务。

如前所述,这将非常有用,能够让我们在开发环境中通过将 Pod 绑定到 Kubernetes 节点的端口,从外部访问我们的 Pod。

NodePort 服务

NodePort 是一种 Kubernetes 服务类型,旨在使 Pod 通过主机机器(工作节点)上可用的端口进行访问。在本节中,我们将深入了解这种类型的端口,并专注于 NodePort 服务!

为什么你需要 NodePort 服务?

首先要理解的是,NodePort 服务允许我们通过 Kubernetes 节点上的端口访问正在运行的 Pod。在你通过 NodePort 类型的服务暴露 Pod 后,你将能够通过获取节点的 IP 地址和 NodePort 服务的端口(如 <node_ip_address>:<node port>)来访问这些 Pod。

端口可以在你的 YAML 声明中声明,或者由 Kubernetes 随机分配。让我们通过声明一些 Kubernetes 对象来说明这一点。

大多数时候,NodePort 服务作为 Kubernetes 集群的入口点。在接下来的示例中,我们将基于 Docker Hub 上的 containous/whoami 容器镜像创建两个 Pod,这个容器镜像非常简洁,会简单地打印出容器的主机名。

我们将创建两个 Pods,以便得到两个具有不同主机名的容器,并通过NodePort服务暴露它们。

创建两个 containous/whoami Pods

让我们首先创建两个 Pods,别忘了添加一个或多个标签,因为我们将需要标签来告诉服务哪些 Pods 将被暴露。

我们还需要在 Pod 端打开端口。虽然这并不会使它自动暴露,但它会打开一个 Service 能够访问的端口。代码如下:

$ kubectl run whoami1 --image=containous/whoami --port 80 --labels="app=whoami"
pod/whoami1 created
$ kubectl run whoami2 --image=containous/whoami --port 80 --labels="app=whoami"
pod/whoami2 created 

现在,我们可以运行kubectl get pods命令来验证我们的两个 Pods 是否正确运行。我们还可以添加--show-labels参数,以便在命令输出中显示标签,如下所示:

$ kubectl get pods --show-labels
NAME      READY   STATUS    RESTARTS   AGE    LABELS
whoami1   1/1     Running   0          3m5s   app=whoami
whoami2   1/1     Running   0          3m     app=whoami 

一切看起来都正常!现在我们已经创建了两个带有标签的 Pods,我们将能够通过一个 Service 将它们暴露出去。接下来,我们将了解用于创建NodePort服务并暴露这两个 Pods 的 YAML 清单文件。

理解 NodePort YAML 定义

由于服务是相当复杂的资源,最好通过 YAML 文件而不是直接命令输入来创建服务。

这是将通过NodePort服务暴露whoami1whoami2 Pods 的 YAML 文件:

# ~/nodeport-whoami.yaml
apiVersion: v1
kind: Service
metadata:
  name: nodeport-whoami
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
  - nodePort: 30001
    port: 80
    targetPort: 80 

这个 YAML 可能比较难理解,因为它涉及到三个不同的端口以及一个selector块。

在解释 YAML 文件之前,让我们先应用它并检查 Service 是否被正确创建,操作如下:

$ kubectl apply -f nodeport-whoami.yaml
service/nodeport-whoami created
$ kubectl get service nodeport-whoami
NAME              TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
nodeport-whoami   NodePort   10.98.160.98   <none>        80:30001/TCP   14s 

之前的kubectl get services命令表明 Service 已正确创建!

selector 块对于 NodePort 服务至关重要,它充当标签过滤器来确定服务暴露哪些 pods。它基本上告诉服务要将流量路由到哪些 pods。如果没有 selector,服务将保持不活动。在此示例中,selector 定位带有标签键“app”和标签值“whoami”的 pods。这将通过 Service 有效地暴露“whoami1”和“whoami2”两个 pods。

接下来,我们在spec下有一个type子键,用来指定我们的 Service 类型。当我们创建ClusterIPLoadBalancer服务时,需要更新此行。在这里,我们创建的是NodePort服务,因此对我们来说这行是正确的。

最后一个比较难理解的部分是ports块。在这里,我们定义了多个端口组合的映射。我们指明了三个端口,如下所示:

  • nodePort: 您希望此NodePort服务可以从中访问的主机机器/工作节点上的端口。在这里,我们指定了端口30001,这使得此NodePort服务可以通过工作节点的 IP 地址上的端口30001进行访问。您将通过调用以下地址来访问此NodePort服务及其所暴露的 Pods:<WORKER_NODE_IP_ADDRESS>:30001。此NodePort设置不能随意设定。事实上,在默认的 Kubernetes 安装中,它只能是3000032767范围内的端口。

  • port:此设置表示 NodePort 服务本身的端口。理解起来可能有点难,但 NodePort 服务确实有自己的端口,这就是你在此处指定它的地方。只要它是有效端口,你可以随便填入任何你想要的值。

  • targetPort:如你所料,targetPort 是目标 Pods 的端口。它是应用程序运行的地方:NodePort 会将流量转发到通过前述选择器找到的 Pod 上的端口。

这里有一张简明的图表来总结这一切:

图 8.6:NodePort 服务。NodePort 设置中涉及三个端口——nodePort 位于工作节点 n006Fde 上,端口位于 Service 本身,targetPort 位于顶部。

在这种情况下,TCP 端口 31001 被用作每个节点的外部端口。如果你没有指定 nodePort,它将使用动态分配的范围分配端口。对于内部通信,该服务仍然像一个简单的 ClusterIP 服务一样工作,你可以使用它的 ClusterIP 地址。

为了方便起见并减少复杂性,NodePort 服务端口和目标端口(Pod 的端口)通常定义为相同的值。

确保 NodePort 按预期工作

要尝试你的 NodePort 设置,首先要做的是获取运行该服务的机器的公共 IP 地址。在我们的例子中,我们使用的是本地的单机 Kubernetes 设置,并通过 minikube 运行。若在 AWS、GCP 或 Azure 上,你的节点可能拥有公共 IP 地址,或者如果通过 虚拟专用网络VPN)访问节点,则可能是私有 IP 地址。

minikube 上,获取 IP 地址的最简单方法是执行以下命令:

$ minikube ip
192.168.64.2
Or you can access the full URL as follows.
$ minikube service --url nodeport-whoami
http://192.168.49.2:30001 

现在我们有了所有信息,可以打开浏览器并输入 URL 来访问 NodePort 服务和正在运行的 Pods。你应该能看到轮询算法的执行,访问 whoami1 然后是 whoami2,依此类推。NodePort 服务正在按预期工作!

这个设置是生产就绪的吗?

这个问题可能没有明确的答案,因为它取决于你的配置。

NodePort 提供了一种通过在节点端口上暴露 Pods 来将其公开到外部世界的方式。在当前的设置中,你没有高可用性(HA):如果你的两个 Pods 出现故障,你将无法自动重新启动它们,因此你的服务将无法将流量转发到任何地方,导致最终用户体验较差。

请注意,当我们使用 Deployments 和 replicasets 创建 Pods 时,Kubernetes 会在其他可用节点中创建新的 Pods。我们将在第十一章《使用 Kubernetes 部署无状态工作负载》中学习 Deployments 和 replicasets。

另一个问题是端口选择的限制。事实上,默认情况下,你只能使用 30000-32767 范围内的端口,而这种限制对很多人来说会很不方便。实际上,如果你想暴露一个 HTTP 应用程序,你可能会希望使用前端机器的 80443 端口,而不是 3000032767 范围内的端口,因为所有的 web 浏览器都将 80443 端口配置为标准 HTTP 和 HTTPSHTTP Secure)端口。

解决方案是使用分层架构。事实上,许多 Kubernetes 架构师倾向于不将 NodePort 服务暴露为架构中的第一层,而是将 Kubernetes 集群放置在反向代理后面,例如 AWS 应用负载均衡器(AWS Application Load Balancer)等。Kubernetes 的两个其他概念是 IngressIngressController 对象:这两个对象允许你直接从 Kubernetes 对象配置反向代理,例如 nginx 或 HAProxy,并帮助你将应用程序作为 Kubernetes 入口的第一层使其公开可访问。但这已经超出了 Kubernetes 服务的范围。

让我们在接下来的章节中探索更多有关 NodePort 的信息,包括如何列出服务以及如何将 Pods 添加到 NodePort 服务。

列出 NodePort 服务

列出 NodePort 服务可以通过使用 kubectl 命令行工具来实现。你只需要执行 kubectl get services 命令来获取集群中创建的服务。

$ kubectl get service
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
example-service   ClusterIP   10.106.224.122   <none>        80/TCP         26d
kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP        26d
nodeport-whoami   NodePort    10.100.85.171    <none>        80:30001/TCP   21s 

话虽如此,现在让我们来了解如何更新 NodePort 服务,以便使其执行我们想要的操作。

向 NodePort 服务添加更多的 Pods

如果你想将 Pod 添加到服务所提供的池中,这是非常容易的。事实上,你只需要添加一个与服务定义的标签选择器匹配的新 Pod——Kubernetes 会处理剩下的工作。这个 Pod 将成为服务所提供池的一部分。如果你删除一个 Pod,它将在进入 Terminating 状态后被从服务池中删除。

Kubernetes 基于 Pod 可用性来处理服务流量——例如,如果你有三个副本的 web 服务器,其中一个出现故障,创建一个匹配服务标签选择器的新副本就足够了。你将在后面的 第十一章使用 Kubernetes 部署无状态工作负载 中发现,这种行为可以完全自动化。

描述 NodePort 服务

描述 NodePort 服务非常简单,可以通过 kubectl describe 命令来实现,就像任何其他 Kubernetes 对象一样。让我们在以下命令的输出中探索 nodeport-whoami 服务的详细信息:

$ kubectl describe Service nodeport-whoami:
Name:                     nodeport-whoami
...<removed for brevity>...
**Selector:                 app=whoami**
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
**IP:                       10.98.160.98**
IPs:                      10.98.160.98
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30001/TCP
**Endpoints:                10.244.0.16:80,10.244.0.17:80**
...<removed for brevity>... 

在上述输出中,我们可以看到几个详细信息,包括以下内容:

  • IP:10.98.160.98:分配给服务的 ClusterIP。这是集群中其他服务可以用来访问此服务的内部 IP 地址。

  • Port: <unset> 80/TCP:该服务监听端口 80 并使用 TCP 协议。<unset> 表示该端口没有指定名称。

  • NodePort: <unset> 30001/TCPNodePort 是集群中每个节点上的端口,通过这个端口外部流量可以访问该服务。在这里,它设置为端口 30001,允许通过任何节点的 IP 地址和端口 30001 来访问该服务。

  • Endpoints: 10.244.0.16:80, 10.244.0.17:80:服务背后实际的 Pods 的 IP 地址和端口。在这种情况下,两个 Pods 支持该服务,可以通过 10.244.0.16:8010.244.0.17:80 访问。

在接下来的部分,我们将学习如何使用 kubectl delete svc 命令删除一个服务。

删除服务

删除服务,无论它是否为 NodePort 服务,都不应频繁进行。事实上,虽然 Pods 应该是易于删除和重建的,但服务应该是长期存在的。它们为你的 Pod 提供了一种一致的暴露方式,删除它们将影响到你的应用程序如何被访问。

因此,你在删除服务时需要小心:它不会删除服务背后的 Pods,但这些 Pods 将无法再从集群外部访问!

以下是删除用来暴露 whoami1whoami2 Pods 的服务的命令:

$ kubectl delete svc/nodeport-whoami
service "nodeport-whoami" deleted 

现在你可以运行 kubectl get svc 命令来检查服务是否已正确删除,然后通过刷新网页浏览器再次访问它。你会发现应用程序不再可访问,但 Pods 将继续存在于集群中。Pods 和服务有完全独立的生命周期。如果你想删除 Pods,那么你需要单独删除它们。

你可能还记得我们在创建 nginx Pod 并测试显示主页时使用的 kubectl port-forward 命令。你可能认为 NodePortkubectl port-forward 是相同的,但它们并不是。接下来的部分,我们将简要解释这两者之间的区别。

NodePort 还是 kubectl port-forward?

NodePort 服务与 kubectl port-forward 命令进行比较可能会很有诱惑力,因为到目前为止,我们已经使用这两种方法通过网页浏览器访问集群中正在运行的 Pod。

kubectl port-forward 命令是一个测试工具,而 NodePort 服务则用于实际应用场景,并且是一个生产就绪的功能。

请记住,kubectl port-forward 必须保持在终端会话中开启才能工作。一旦该命令被终止,端口转发也会停止,且你的应用程序将再次无法从集群外部访问。它只是一个测试工具,供 kubectl 用户使用,并且是 kubectl CLI 中捆绑的有用工具之一。

另一方面,NodePort 确实是为了生产环境使用的,并且是一个长期的生产就绪解决方案。它不需要 kubectl 就能工作,并且使您的应用程序可以被任何调用该服务的人访问,只要该服务配置正确,Pods 标签正确。

简单来说,如果您只需要测试应用程序,使用 kubectl port-forward。如果您需要将 Pod 真正暴露到外部世界,选择 NodePort。不要为了测试而创建 NodePort,也不要在生产环境中使用 kubectl port-forward!每种用例都选择合适的工具!

现在,我们将探索另一种 Kubernetes 服务类型,叫做 ClusterIP。这可能是最广泛使用的一种,甚至比 NodePort 类型还要常用!

ClusterIP 服务

现在,我们将了解另一种服务类型,称为 ClusterIP。实际上,ClusterIP 是 Kubernetes 提供的最简单的服务类型。有了 ClusterIP 服务,您可以暴露您的 Pod,使得 Kubernetes 中的其他 Pods 可以通过其 IP 地址或 DNS 名称与其通信。

为什么需要 ClusterIP 服务?

ClusterIP 服务类型与 NodePort 服务类型非常相似,但它们有一个很大的区别:NodePort 服务旨在将 Pod 暴露给外部世界,而 ClusterIP 服务旨在将 Pod 暴露给 Kubernetes 集群内部的其他 Pod。

事实上,ClusterIP 服务允许同一集群中的不同 Pods 通过静态接口互相通信:即 ClusterIP 服务对象本身。

ClusterIP 完全解决了我们在 NodePort 服务中遇到的对静态 DNS 名称或 IP 地址的需求:如果 Pod 失败、被重新创建、删除、重新启动等等,那么 Kubernetes 会为其分配另一个 IP 地址。ClusterIP 服务通过提供一个仅能从集群内部访问的内部 DNS 名称,解决了这个问题,该名称会解析到由标签选择器定义的 Pods。

正如 ClusterIP 这个名字所暗示的,这种服务会在集群内分配一个静态 IP 地址!现在让我们来看看如何使用 ClusterIP 暴露我们的 Pods!请记住,ClusterIP 服务不能从集群外部访问——它们仅用于集群内部 Pod 之间的通信。

我怎么知道我需要 NodePort 还是 ClusterIP 服务来暴露我的 Pods?

选择这两种服务类型非常简单,基本上是因为它们不是为同样的用途设计的。

如果你需要让你的应用程序可以从集群外部访问,那么你需要一个NodePort服务(或者本章后面将探讨的其他服务),但如果你的需求是让应用程序从集群内部访问,那么你需要一个ClusterIP服务。ClusterIP服务也适用于可以扩展、销毁、重新创建等的无状态应用程序。原因在于,ClusterIP服务将维持对整个 Pod 池的静态入口点,而不受工作节点端口的限制,这与NodePort服务不同。

ClusterIP服务通过 kube-proxy 在每个节点上管理的内部可见虚拟 IP 地址来暴露 Pods。这意味着该服务仅能在集群内部访问。我们已经在下面的图表中展示了ClusterIP服务的原理:

图 8.7:ClusterIP服务

在前面的图片中,ClusterIP服务配置成将来自其 IP 和 TCP 端口8080的请求映射到容器的 TCP 端口80。实际的ClusterIP地址是动态分配的,除非你在规格中明确指定了一个。Kubernetes 集群中的内部 DNS 服务负责将nginx-deployment-example名称解析为实际的ClusterIP地址,作为服务发现的一部分。

kube-proxy 负责节点上虚拟 IP 地址的管理,并相应地调整转发规则。Kubernetes 中的服务本质上是集群内的逻辑构造。集群内并没有为每个服务单独运行物理进程来处理代理。而是,kube-proxy 基于这些逻辑服务执行必要的代理和路由操作。

NodePort服务不同,ClusterIP服务不会占用工作节点的一个端口,因此无法从 Kubernetes 集群外部访问。

请记住,没有任何东西可以阻止你为同一组 Pods 使用两种类型的服务。实际上,如果你有一个应用程序需要对外部可访问,但又需要私密地暴露给其他 Pods,那么你可以简单地创建两个服务,一个是NodePort服务,另一个是ClusterIP服务。

在这个特定的用例中,你只需要为这两个服务命名不同,以便在创建时避免与kube-apiserver发生冲突。没有其他任何东西能阻止你这样做!

列出ClusterIP服务

列出ClusterIP服务非常简单。它基本上与用于NodePort服务的命令相同。这里是要运行的命令:

$ kubectl get svc 

与往常一样,这个命令会列出服务,并将其类型添加到输出中。

使用命令式方法创建ClusterIP服务

创建ClusterIP服务可以通过多种不同的方法来实现。由于它是一个广泛使用的功能,因此有很多方法可以创建这些服务,如下所示:

  • 使用--expose参数或kubectl expose方法(命令式方式)

  • 使用 YAML 清单文件(声明式方式)

命令式方式是使用--expose方法。这将通过kubectl run命令直接创建ClusterIP服务。在以下示例中,我们将创建一个nginx-clusterip Pod,并创建一个ClusterIP服务来同时暴露它们。使用--expose参数还需要定义ClusterIP端口。ClusterIP将监听端口,以使 Pod 可访问。代码如下所示:

$ kubectl run nginx-clusterip --image nginx --expose=true --port=80
service/nginx-clusterip created
pod/nginx-clusterip created 

正如你所看到的,我们得到了一个 Pod 和一个暴露它的服务。让我们来描述这个服务。

描述 ClusterIP 服务

描述ClusterIP服务的过程与描述 Kubernetes 中任何类型的对象相同,使用kubectl describe命令来完成。你只需要知道服务的名称,即可进行描述。

这里,我将访问之前创建的ClusterIP服务:

$ kubectl describe svc/nginx-clusterip
Name:              nginx-clusterip
Namespace:         default
Labels:            <none>
Annotations:       <none>
**Selector:          run=nginx-clusterip**
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.101.229.225
IPs:               10.101.229.225
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.244.0.10:80
Session Affinity:  None
Events:            <none> 

该命令的输出显示了Selector块,表明ClusterIP服务是通过--expose参数创建的,并且已正确配置标签。这个标签与我们同时创建的nginx-clusterip Pod 相匹配。为了确认这一点,让我们显示该 Pod 的标签,如下所示:

$ kubectl get pods/nginx-clusterip --show-labels
NAME              READY   STATUS    RESTARTS   AGE   LABELS
nginx-clusterip   1/1     Running   0          76s   **run=nginx-clusterip** 

正如你所看到的,服务上的选择器与 Pod 上定义的标签相匹配。因此,二者之间建立了通信。接下来,我们将从集群中的另一个 Pod 直接调用ClusterIP服务。

由于ClusterIP服务名为nginx-clusterip,我们知道它可以通过这个地址访问:nginx-clusterip.default.svc.cluster.local

让我们重用k8sutils容器,如下所示:

$ kubectl exec k8sutils -- curl nginx-clusterip.default.svc.cluster.local
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0<!DOCTYPE ...<removed for brevity>...
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
 ...<removed for brevity>... 

ClusterIP服务正确地将请求转发到 nginx Pod,我们确实看到了 nginx 的默认首页。服务正常工作!

这次我们没有使用containous/whoami作为 Web 服务,但请记住,ClusterIP服务内部也在执行负载均衡,采用轮询算法。如果你在ClusterIP服务后有 10 个 Pod,并且该服务接收到 1,000 个请求,那么每个 Pod 将收到 100 个请求。

现在让我们了解如何使用 YAML 创建ClusterIP服务。

使用声明式方式创建ClusterIP服务

ClusterIP服务也可以通过应用 YAML 配置文件对kube-apiserver来声明式创建。

这是一个 YAML 清单文件,我们可以用它来创建与之前命令式创建的完全相同的ClusterIP服务:

# clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-clusterip
spec:
  type: ClusterIP # Indicates that the service is a ClusterIP
  ports:
    - port: 80 # The port exposed by the service
      protocol: TCP
      targetPort: 80 # The destination port on the pods
  selector:
    run: nginx-clusterip 

花点时间阅读 YAML 中的注释,特别是关于porttargetPort的部分。

确实,ClusterIP服务具有独立于 Pod 端暴露端口的端口。你通过调用其 DNS 名称及端口来访问ClusterIP服务,流量将被转发到匹配标签和选择器的 Pods 的目标端口。

请记住,这里没有涉及工作节点端口。我们在讨论 ClusterIP 场景时提到的端口与主机机器完全无关!

在继续下一节之前,你可以通过以下方式删除 nginx-clusterip 服务来清理环境:

$ kubectl delete  svc nginx-clusterip
service "nginx-clusterip" deleted 

请记住,删除集群不会删除它暴露的 Pods。这是一个不同的过程;你需要单独删除 Pods。接下来我们将介绍一个与 ClusterIP 服务相关的额外资源——无头服务。

理解无头服务

无头服务源自 ClusterIP 服务。它们技术上不是一种专用类型的服务(如 NodePortClusterIP),而是 ClusterIP 的一种选项。

无头服务可以通过在 ClusterIP 服务的 YAML 配置文件中将 .spec.clusterIP 选项设置为 None 来配置。以下是从我们之前的 YAML 文件中派生的示例:

# clusterip-headless.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-clusterip-headless
spec:
  clusterIP: None
  type: ClusterIP # Indicates that the service is a ClusterIP
  ports:
  - port: 80 # The port exposed by the service
    protocol: TCP
    targetPort: 80 # The destination port on the pods
  selector:
    run: nginx-clusterip 

一个无头服务大致由一个没有负载均衡和没有预分配 ClusterIP 地址的 ClusterIP 服务组成。因此,负载均衡逻辑和与 Pod 的接口不由 Kubernetes 定义。

由于无头服务没有 IP 地址,你将直接访问其背后的 Pod,而没有代理和负载均衡逻辑。无头服务的作用是返回背后 Pods 的 DNS 名称,以便你可以直接访问它们。这里仍然存在一些负载均衡逻辑,但它是在 DNS 层面实现的,而不是作为 Kubernetes 的逻辑。

当你使用普通的 ClusterIP 服务时,你将始终访问分配给该服务的一个静态 IP 地址,这将作为与其背后 Pod 通信的代理。使用无头服务时,ClusterIP 服务只会返回背后 Pods 的 DNS 名称,客户端将负责与其选择的 DNS 名称建立连接。

Kubernetes 中的无头服务主要用于需要与单个 Pod 直接通信的场景,而不是与单一端点或负载均衡的 Pods 集合进行通信。

当你想与集群状态服务(如轻量目录访问协议LDAP))建立连接时,无头服务非常有用。在这种情况下,你可能想使用一个 LDAP 客户端,它将能够访问托管 LDAP 服务器的 Pods 的不同 DNS 名称,而这不能通过普通的 ClusterIP 服务实现,因为普通的 ClusterIP 服务会带来一个静态 IP 地址以及 Kubernetes 的负载均衡实现。现在,让我们简要介绍另一种类型的服务,叫做 LoadBalancer

负载均衡服务

LoadBalancer 服务非常有趣,因为它依赖于 Kubernetes 集群所在的云平台。为了使其工作,必须使用支持 LoadBalancer 服务类型的云平台上的 Kubernetes。

对于提供外部负载均衡器的云服务提供商,指定type字段为LoadBalancer会为你的服务配置负载均衡器。负载均衡器的创建是异步进行的,关于所提供负载均衡器的详细信息将在服务的.status.loadBalancer字段中提供。

某些云服务提供商提供定义loadBalancerIP的选项。在这种情况下,负载均衡器会使用指定的loadBalancerIP进行生成。如果没有提供loadBalancerIP,负载均衡器将配置为一个临时 IP 地址。然而,如果你在不支持此功能的云服务提供商上指定了loadBalancerIP,则提供的loadBalancerIP将被忽略。

对于类型设置为 LoadBalancer 的服务,.spec.loadBalancerClass字段允许你使用云服务提供商默认负载均衡器以外的负载均衡器实现。当.spec.loadBalancerClass未指定时,LoadBalancer类型的服务将自动使用云服务提供商提供的默认负载均衡器实现,前提是集群已使用--cloud-provider组件标志配置了云服务提供商。然而,如果你指定了.spec.loadBalancerClass,则表示一个与指定类匹配的负载均衡器实现正在积极监控服务。在这种情况下,任何默认的负载均衡器实现,例如云服务提供商提供的负载均衡器,将忽略设置了该字段的服务。需要注意的是,spec.loadBalancerClass只能在LoadBalancer类型的服务上设置,并且一旦设置,就不能更改。此外,spec.loadBalancerClass的值必须符合标签样式的标识符格式,前缀可选,如internal-vipexample.com/internal-vip,而不带前缀的名称保留给终端用户使用。

以下图表展示了 Kubernetes Loadbalancer类型服务的原理:

图 8.8:LoadBalancer 服务

在下一节中,我们将学习支持LoadBalancer服务类型的云服务提供商。

支持LoadBalancer服务类型的云服务提供商

并非所有云服务提供商都支持LoadBalancer服务类型,但我们可以列出几个支持的服务提供商。它们如下:

  • AWS

  • GCP

  • Azure

  • OpenStack

这个列表并不详尽,但值得注意的是,所有三大主要公共云服务提供商都受支持。

如果你的云服务提供商受支持,请记住,负载均衡的逻辑将由云服务提供商实现:你对流量如何从 Kubernetes 路由到你的 Pods 的控制较少,你需要了解你的云服务提供商的负载均衡器组件如何工作。将其视为作为 Kubernetes 资源实现的第三方组件。

是否应该使用 LoadBalancer 服务类型?

这个问题很难回答,但许多人因以下几个原因倾向于不使用LoadBalancer服务类型。

主要原因是,LoadBalancer服务几乎无法从 Kubernetes 进行配置。事实上,如果必须使用云服务提供商,最好通过提供商提供的工具进行配置,而不是通过 Kubernetes 来配置。LoadBalancer服务类型是一个通用的方式来提供LoadBalancer服务,但无法暴露云服务提供商可能提供的所有高级功能。

此外,云服务提供商提供的负载均衡器通常会产生额外费用,具体费用取决于提供商和处理的流量量。

在接下来的章节中,我们将学习另一种服务类型,称为ExternalName服务。

ExternalName 服务类型

ExternalName服务是一种强大的方式,可以将你的 Kubernetes 集群连接到外部资源,如数据库、API 或托管在集群外部的服务。它们通过将集群中的服务映射到 DNS 名称而不是集群内的 Pod 来工作。这使得集群内的应用程序能够无缝访问外部资源,而无需知道其 IP 地址或内部细节。

ExternalName type Service YAML definition:
# externalname-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-db
  namespace: prod
spec:
  type: ExternalName
  externalName: app-db.database.example.com 

它是如何工作的:你无需将外部名称或 IP 地址链接到内部 Pod,而是简单地在服务配置中定义一个 DNS 名称,如app-db.database.example.com。现在,当集群中的应用程序尝试访问mysql-db时,魔法发生了——集群的 DNS 服务将它们指向你的外部数据库!它们与数据库的交互是无缝的,就像任何其他服务一样,但重定向发生在 DNS 级别,保持了简洁和透明。

图 8.9:ExternalName 服务类型

ExternalName服务可以是托管在另一个 Kubernetes 命名空间中的服务,Kubernetes 集群外部的服务,托管在另一个 Kubernetes 集群中的服务,等等。

这种方法有几个好处:

  • 简化配置:应用程序只需要知道服务名称,而不需要了解外部资源的详细信息,这使得配置变得更加简单。

  • 灵活的资源管理:如果你稍后将数据库迁移到集群内,你只需更新服务并在集群内管理它,而不会影响到应用程序。

  • 增强的安全性:像 IP 地址这样的敏感信息会隐藏在集群内,从而提高整体安全性。

记住,ExternalName服务的重点是连接到外部资源。如果是集群内的资源访问,请使用常规服务或无头服务。

现在我们已经学习了 Kubernetes 中的不同服务类型,接下来让我们探索如何使用探针来确保服务可用性。

使用探针实现服务就绪性

当你创建一个 Service 来暴露运行在 Pods 中的应用程序时,Kubernetes 不会自动验证该应用程序的健康状况。Pods 可能已经启动并运行,但应用程序本身可能仍然存在问题,而 Service 将继续向其路由流量。这可能导致用户或其他应用程序接收到错误或完全没有响应。为了防止这种情况发生,Kubernetes 提供了名为探针的健康检查机制。在接下来的部分,我们将探讨不同类型的探针——存活探针、就绪探针和启动探针——以及它们如何帮助确保你的应用程序正常运行。

什么是 ReadinessProbe,为什么需要它?

ReadinessProbeLivenessProbe一起,是掌握 Kubernetes 中提供最佳用户体验的重要方面。我们将首先了解如何实现ReadinessProbe以及它如何帮助确保容器已完全准备好接受流量。

就技术而言,Readiness probes 并不是 Services 的一部分,但在了解 Kubernetes Services 的同时,了解这个特性非常重要。

正如 Kubernetes 中的所有功能一样,ReadinessProbe是为了解决一个问题而实现的。这个问题是:如何确保 Pod 在可以接收流量之前完全准备好,可能是来自 Service 的流量。

Services 遵循一个简单规则:它们会向每个与其标签选择器匹配的 Pod 提供流量。一旦 Pod 被分配,如果该 Pod 的标签与集群中 Service 的选择器匹配,那么该 Service 将立即开始向它转发流量。这可能会导致一个简单的问题:如果应用程序尚未完全启动,因为它有一个缓慢的启动过程,或者需要从远程 API 获取一些配置,等等,那么它可能会在准备好之前就收到来自 Services 的流量。结果将是一个糟糕的用户体验UX)。

为确保此场景永远不会发生,我们可以使用名为ReadinessProbe的功能,这是一种需要添加到 Pod 配置中的附加配置。

当 Pod 配置了就绪探针时,它可以向控制平面发送信号,表示它还没有准备好接收流量,而当 Pod 尚未准备好时,Services 将不会向其转发流量。让我们来看看如何实现就绪探针。

实现 ReadinessProbe

ReadinessProbe的实现通过向 Pod 的 YAML 清单添加一些配置数据来完成。请注意,它与Service对象本身无关。通过向 Pod 对象中的容器spec添加一些配置,你基本上可以告诉 Kubernetes,在 Pod 完全准备好之前,它不能接收来自 Services 的流量。

ReadinessProbe可以有三种不同的类型,如下所述:

  • Command:在 Pod 内部发出一个命令,该命令应以退出代码0完成,表示 Pod 已准备好。

  • HTTP:一个 HTTP 请求,该请求应以响应码>=200且<400完成,表示 Pod 已准备好。

  • TCP:发起 TCP 连接尝试。如果连接建立,Pod 就准备好了。

这是一个 YAML 文件,配置了带有 HTTP 类型准备性探测的 nginx Pod:

# nginx-pod-with-readiness-http.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-readiness-http
spec:
  containers:
    - name: nginx-pod-with-readiness-http
      image: nginx
      readinessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /ready
          port: 80 

如您所见,我们在readinessProbe键下有两个重要的输入,如下所示:

  • initialDelaySeconds,表示探测在进行第一次健康检查之前将等待的秒数

  • periodSeconds,表示探测在两个连续健康检查之间将等待的秒数

准备性探测将定期重放,两个检查之间的间隔将由periodSeconds参数定义。

在我们的示例中,ReadinessProbe将针对/ready路径进行 HTTP 调用。如果此请求收到的 HTTP 响应代码>= 200 且 < 400,则探测将成功,Pod 将被视为健康。

ReadinessProbe是非常重要的。在我们的示例中,被调用的端点应该测试应用程序是否真的处于能够接收流量的状态。因此,尝试调用与实际应用程序状态相关的端点。例如,您可以尝试调用一个页面,该页面将内部打开一个 MySQL 连接,以确保应用程序能够与数据库通信(如果它使用的是数据库),等等。如果您是开发人员,不要犹豫,创建一个专用的端点,该端点仅打开与不同后端的连接,以确保应用程序确实已经准备好。

然后,Pod 将加入由 Service 提供服务的池,并开始接收流量。ReadinessProbe也可以配置为 TCP 和命令,但我们将这些示例保留给LivenessProbe。现在,让我们来探索它们吧!

什么是 LivenessProbe,它为什么重要?

LivenessProbeReadinessProbe非常相似。事实上,如果您以前使用过任何云服务提供商,您可能已经听说过“健康检查”这一概念。LivenessProbe基本上就是健康检查。

Liveness 探测用于确定 Pod 是否处于故障状态,LivenessProbe特别适用于像 Web 服务这样的长期运行的进程。假设有一个情况,您的 Service 正在将流量转发到三个 Pod,其中一个 Pod 出现故障。服务无法自行检测到这一点,它们会继续将流量转发到这三个 Pod,包括故障的那个 Pod。在这种情况下,33%的请求必然会导致错误响应,导致用户体验差,如以下截图所示:

图 8.10:其中一个 Pod 出现故障,但 Service 仍然会将流量转发给它

您希望避免这种情况, 为此,您需要一种方法来检测 Pod 故障的情况,并且需要一种方法来终止这种容器,使其不再处于由 Service 指向的 Pod 池中。

LivenessProbe是解决这个问题的方案,它是在 Pod 级别实现的。需要小心的是,LivenessProbe无法修复 Pod:它只能检测 Pod 不健康并命令其终止。让我们看看如何实现一个带有LivenessProbe的 Pod。

实现 LivenessProbe

LivenessProbe是一个定期执行的健康检查,用于长期跟踪应用程序的状态。这些健康检查由kubelet组件执行,可以是不同类型的,正如这里所列:

  • Command,你在容器中发出一个命令,命令的结果将告诉你 Pod 是否健康(退出码=0表示健康)

  • HTTP,你对 Pod 发出一个 HTTP 请求,它的结果告诉你 Pod 是否健康(HTTP 响应码>=200且<400意味着 Pod 健康)

  • TCP,在这里你定义一个 TCP 调用(成功连接意味着 Pod 健康)

  • GRPC,如果应用程序支持并实现了 gRPC 健康检查协议

每个 livenessProbe 都需要你输入一个名为periodSeconds的参数,必须是一个整数。该参数告诉kubelet组件在执行新的健康检查之前等待的秒数。你还可以使用另一个名为initialDelaySeconds的参数,它表示执行第一次健康检查之前等待的秒数。实际上,在一些常见的情况下,健康检查可能会导致应用程序被标记为不健康,仅仅是因为检查太早执行了。这就是为什么在执行第一次健康检查之前等待一段时间可能是个好主意,这个参数就是为此提供帮助的。

LivenessProbe配置是在 Pod 的 YAML 配置清单中实现的,而不是在 Service 中实现的。Pod 中的每个容器都可以有自己的livenessProbe

HTTP livenessProbe

Kubernetes 中的 HTTP 探针提供了额外的可定制字段,如主机、方案、路径、头部和端口,可以精细调整健康检查请求如何发送到应用程序。这里是一个配置文件,它通过对 nginx 容器中的/healthcheck端点进行 HTTP 调用来检查 Pod 是否健康:

# nginx-pod-with-liveness-http.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-liveness-http
spec:
  containers:
    - name: nginx-pod-with-liveness-http
      image: nginx
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /healthcheck
          port: 80
          httpHeaders:
            - name: My-Custom-Header
              value: My-Custom-Header-Value 

请注意livenessProbe块之后的所有部分。如果你理解这一点,你会看到我们将在执行第一次健康检查前等待 5 秒钟,然后每 5 秒钟对端口80上的/healthcheck路径进行一次 HTTP 调用。一个自定义 HTTP 头部已经被添加。添加这样的头部将有助于在访问日志中识别我们的健康检查。需要小心的是,/healthcheck路径可能在我们的 nginx 容器中不存在,因此该容器将永远不会被视为健康,因为活跃探针将返回404 HTTP 响应。请记住,要使 HTTP 健康检查成功,必须返回一个 HTTP 响应代码>=200且<400404不在此范围内,因此答复的 Pod 不会被视为健康。

命令型 livenessProbe

你也可以使用命令来检查 Pod 是否健康。让我们获取相同的 YAML 配置,但这次我们将在存活探针中使用命令而不是 HTTP 调用,如下所示:

# nginx-pod-with-liveness-command.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-liveness-command
spec:
  containers:
    - name: nginx-pod-with-liveness-command
      image: nginx
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        exec:
          command:
            - cat
            - /hello/world 

如果你查看这个示例,你会发现它比 HTTP 健康检查要简单得多。这里,我们基本上每 5 秒运行一次 cat /hello/world 命令。如果文件存在且 cat 命令以退出代码 0 完成,则健康检查成功。否则,如果文件不存在,健康检查将失败,Pod 将永远不会被视为健康,并且会被终止。

TCP 型 livenessProbe

在这种情况下,我们将尝试连接到 80 端口的 TCP 套接字。如果连接成功建立,健康检查将通过,容器将被认为已准备好。否则,健康检查将失败,Pod 最终会被终止。代码如下所示:

# nginx-pod-with-liveness-tcp.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-liveness-tcp
spec:
  containers:
    - name: nginx-pod-with-liveness-tcp
      image: nginx
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        tcpSocket:
          port: 80 

使用 TCP 健康检查与使用 HTTP 健康检查非常相似,因为 HTTP 是基于 TCP 的。但如果你要监控一个不基于 HTTP 协议的应用,且使用该命令对你来说无关紧要(例如在健康检查 LDAP 连接时),那么使用 TCP 作为存活探针尤其有用。

使用命名端口配置 TCP 和 HTTP 型 livenessProbe

你可以使用命名端口来配置 livenessProbe,适用于 HTTP 和 TCP 类型(但不适用于 gRPC),如下所示:

# nginx-pod-with-liveness-http-named-port.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-liveness-http-named-port
spec:
  containers:
    - name: nginx-pod-with-liveness-http-named-port
      image: nginx
      ports:
        - name: liveness-port
          containerPort: 8080
          hostPort: 8080
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /healthcheck
          port: liveness-port 

在前面的示例中,liveness-port 已在 ports 部分中定义,并在 livenessProbehttpGet 中使用。

既然我们已经探讨了多种存活探针,让我们在下一节学习 startupProbe

使用 startupProbe

传统应用程序有时在首次启动时需要更多时间。这在设置存活探针时可能会造成困境,因为快速的响应时间对于检测死锁至关重要。

解决方案在于使用 initialDelaySeconds 或专门的 startupProbeinitialDelaySeconds 参数允许你推迟首次就绪探针的执行,为应用程序提供初始化的时间。

然而,若需要更精细的控制,可以考虑使用 startupProbe。这个探针与存活探针(命令、HTTP 或 TCP 检查)类似,但具有更长的 failureThreshold * periodSeconds 时长。这个扩展的等待时间确保应用程序有足够的时间进行初始化,才会被认为已准备好接收流量,同时仍然能让存活探针在之后迅速检测到问题,正如以下的 YAML 片段所示:

# nginx-pod-with-startupprobe.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-with-startupprobe
spec:
  containers:
    - name: nginx-pod-with-startupprobe
      image: nginx
      ports:
        - name: liveness-port
          containerPort: 8080
          hostPort: 8080
      livenessProbe:
        initialDelaySeconds: 5
        periodSeconds: 5
        httpGet:
          path: /healthcheck
          port: liveness-port
      startupProbe:
        httpGet:
          path: /healthz
          port: liveness-port
        failureThreshold: 30
        periodSeconds: 10 

正如你在上面的示例代码中看到的,确实可以结合使用多个探针来确保应用程序准备好提供服务。在接下来的部分中,我们还将学习如何同时使用 ReadinessProbeLivenessProbe

同时使用 ReadinessProbe 和 LivenessProbe

你可以在同一个 Pod 中同时使用 ReadinessProbeLivenessProbe

它们的配置方式几乎相同——它们的目的并不完全相同,且可以一起使用。请注意,这两个探针共享以下参数:

  • initialDelaySeconds:在执行第一次探针前等待的秒数。

  • periodSeocnds:两个探针之间的秒数。

  • timeoutSeconds:在超时之前等待的秒数。

  • successThreshold:将 Pod 视为已准备好(对于 ReadinessProbe)或健康(对于 LivenessProbe)的成功尝试次数。

  • failureThreshold:将 Pod 视为未准备好(对于 ReadinessProbe)或准备好被终止(对于 LivenessProbe)的失败尝试次数。

  • TerminationGracePeriodSeconds:在强制停止容器之前,给予容器优雅关闭的宽限期(默认继承 Pod 层级的值)。

我们现在已经了解了ReadinessProbeLivenessProbe,并且本章关于 Kubernetes 服务和实现方法的内容已经结束。

总结

本章内容较为密集,涵盖了在 Kubernetes 中应用的网络知识。服务就像 Pods 一样:它们是 Kubernetes 的基础,掌握它们对使用这个编排工具至关重要。

总体来说,本章我们了解到,Pods 拥有动态 IP 分配,在创建时会获得一个唯一的 IP 地址。为了建立一个可靠的连接方式来连接到你的 Pods,你需要一个被称为 Service 的代理。我们还了解到 Kubernetes 服务可以有多种类型,每种类型的服务都旨在解决特定的需求。我们还发现了 ReadinessProbeLivenessProbe 是什么,以及它们如何帮助你设计健康检查,确保 Pods 在准备好和存活时能接收到流量。

在下一章,我们将继续探索 Kubernetes 的基础知识,了解 PersistentVolumePersistentVolumeClaims 的概念,它们是 Kubernetes 处理持久化数据的方法。如果你希望在 Kubernetes 集群上构建和提供有状态应用程序,如数据库或文件存储解决方案,这将是一个非常有趣的章节。

进一步阅读

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者一起讨论:

packt.link/cloudanddevops

第九章:Kubernetes 中的持久化存储

在前面的章节中,我们学习了 Kubernetes 的关键概念,而本章将是关于这些概念的最后一章。到目前为止,我们已经发现 Kubernetes 的核心目标是通过在其etcd数据存储中创建一个对象,表示所需的状态,从而将所有传统的 IT 层转化为集群中的实际计算资源。

本章将重点讨论有状态应用的持久化存储。与其他资源抽象一样,这将是我们需要掌握的一组对象,用以在集群中获得持久化存储。Kubernetes 通过使用PersistentVolume资源类型来实现持久化存储,它有其自己的机制。说实话,这些内容最初可能相对难以理解,但我们将逐一探索并深入讲解它们!

本章将涵盖以下主要主题:

  • 为什么要使用持久化存储?

  • 理解如何将PersistentVolume挂载到你的 Pod 中

  • 理解 Kubernetes 中PersistentVolume对象的生命周期

  • 理解静态和动态PersistentVolume配置

  • 高级存储主题

技术要求

  • 一个可用的 Kubernetes 集群(无论是本地集群还是云端集群)

  • 配置好的工作kubectl命令行工具,用于与集群通信

如果你没有满足这些技术要求,可以参考第二章Kubernetes 架构——从容器镜像到运行的 Pod,以及第三章安装 Kubernetes 集群,以获取这两个先决条件。

你可以从官方 GitHub 仓库下载本章的最新代码示例,地址为github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter09

为什么要使用持久化存储?

存储是 IT 世界中的一个重要资源,因为它提供了一种逻辑方式来创建读取更新删除CRUD)信息,从员工工资单的 PDF 文件格式到 PB 级的医疗记录。虽然存储是向用户提供相关信息的关键元素,但容器和微服务应该是无状态的。换句话说,在运行的容器中保存的信息在重新调度或迁移到其他集群时将不可用。微服务也同理;数据组件应该解耦,允许微服务保持微型,不关心数据的状态和可用性,也不关心在重新调度时的情况。

那么,我们将应用数据保存在哪里呢?可以保存到任何类型的数据存储中,从业务连续性的角度来看,如果相关的数据存储运行在与微服务相同的 Kubernetes 集群中,它应该具有与应用相关的复制机制。但请记住,Kubernetes 是一个资源协调器,它会根据你为应用定义的期望状态进行操作。当你配置 Pods 时,你可以定义将要使用的存储组件,给容器提供创建、读取、更新和删除数据的方式。让我们探索 Kubernetes 提供的持久化数据的不同选项。

引入卷

存储抽象的第一层是访问 Kubernetes 对象并将它们挂载到容器中,像数据卷一样。这可以为以下内容完成:

  • 一个 ConfigMap

  • 一个 Secret

  • 一个 ServiceAccount 令牌(与 Secret 相同)

这使得应用团队能够将微服务的配置与容器或部署定义解耦。如果我们考虑到应用的生命周期,外部服务的凭证、证书或令牌可能需要刷新,或者配置参数可能需要更新。出于明显的安全原因,我们不希望这些信息被硬编码在部署清单或容器镜像中。

让我们看一下一个带有清单 nginx-configmap.yaml 的 configMap 示例:

# nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-hello
  labels:
    app: test
immutable: false
data:
  hello1.html: |
    <html>
      hello world 1
    </html>
  hello2.html: |
    <html>
      hello world 2
    </html> 

这个 ConfigMap 有两个定义,分别对应两个不同的文件,我们将使用清单 nginx-pod.yaml 将它们挂载到 NGINX Pod 中:

# nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hello
  labels:
    app: test
spec:
  containers:
    - name: nginx
      image: nginx:1.14.2
      ports:
        - containerPort: 80
      volumeMounts:
        - name: nginx-hello
          mountPath: "/usr/share/nginx/html/hello"
  volumes:
    - name: nginx-hello
      configMap:
        name: nginx-hello 

让我们应用这两个清单:

$ kubectl apply -f nginx-configmap.yaml
configmap/nginx-hello created
$ kubectl apply -f nginx-pod.yaml
pod/nginx-hello created 

让我们验证这两个对象的状态:

$ kubectl get pod,cm
NAME              READY   STATUS    RESTARTS   AGE
pod/nginx-hello   1/1     Running   0          7m26s
NAME                         DATA   AGE
configmap/kube-root-ca.crt   1      7d17h
configmap/nginx-hello        2      7m31s 

验证我们提供的挂载路径 /usr/share/nginx/hello 中是否有文件:

$ kubectl exec -t pod/nginx-hello -- ls -al /usr/share/nginx/html/hello/
total 12
...<removed for brevity>...
lrwxrwxrwx 1 root root   18 Sep  7 21:19 hello1.html -> ..data/hello1.html
lrwxrwxrwx 1 root root   18 Sep  7 21:19 hello2.html -> ..data/hello2.html 

让我们通过 port-forward 验证数据是否通过 NGINX 提供,以避免设置服务:

$ kubectl port-forward nginx-hello 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

在第二个终端中,你可以使用 curl 请求这两个 URL:

$ curl 127.0.0.1:8080/hello/hello1.html
<html>
  hello world 1
</html>
$ curl 127.0.0.1:8080/hello/hello2.html
<html>
  hello world 2
</html> 

尽管这是一个很好的开始,但这些对象的一个限制是你可以存储的数据量。由于它依赖于 etcd 数据存储系统,为避免性能问题,限制为 1.5 MB(参见 etcd.io/docs/v3.5/dev-guide/limit)。因此,下一组对象将允许你的应用存储更多的数据,实际上,可以存储与托管这些卷对象的系统能够存储的所有数据。

让我们考虑一个具有两个工作节点的 Kubernetes 集群,在这些节点上可以调度 Pods,并探索以下五种卷类型:

  • 一个 emptyDir

  • 一个 hostPath

  • 本地卷

  • 一个光纤通道 (FC) 块磁盘

  • 一个网络文件系统 (NFS) 卷导出

前三种类型,emptyDirhostPath 和本地卷,有两个主要的限制:

  • 它们仅限于它们所在工作节点上可用的磁盘空间。

  • 它们绑定到 Pod 将要部署的节点。如果你的 Pod 部署在工作节点 1 上,数据将只存储在工作节点 1 上。

这些卷类型可能会导致服务降级,甚至更糟,例如出现脑裂(split-brain)情形。如果工作节点 1 变得不健康,导致 Pod 被重新调度到工作节点 2,则应用程序将在没有数据的情况下启动,可能会导致严重的服务中断。

请注意,有些应用程序具有原生的复制引擎。此类应用程序的典型部署会运行两个副本,并在每个节点上创建一个 hostPath 卷。在这种情况下,如果一个工作节点变得不健康,应用程序会出现降级,但只是在高可用性和性能方面。

作为 Kubernetes 集群的计算资源之外的外部资源,最后两种类型的卷,FC 块存储和 NFS 卷,解决了上述的弱点,但引入了更多的复杂性。虽然前三种类型的卷不需要与存储管理员交互,但最后两种类型需要。简单来说,您的存储管理员将需要:

  • 存储区域网络SAN)中提供一个逻辑单元号LUN – FC 块存储磁盘),并通过 FC 光纤网络连接到 Kubernetes 工作节点,并允许通过 zoning 配置进行访问。

  • 在连接到企业网络并且可以被您的 Kubernetes 工作节点访问的网络附加存储 (NAS) 上提供数据空间,并允许通过导出策略进行访问。

请注意,测试这两种类型的卷需要专用设备,并且设置过程并不简单,尽管 NAS 在家庭实验室中越来越受欢迎。然而,从 Kubernetes 的角度来看,这些卷的配置与 configMap 示例一样简单。以下是修改后的 NGINX Pod 定义版本:

  • 对于 FC 卷(nginx-pod-fiberchannel.yaml):

    ...
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80
          volumeMounts:
            - name: fc-vol
              mountPath: "/usr/share/nginx/html/hello"
      volumes:
        - name: fc-vol
          fc:
            targetWWNs:
              - 500a0982991b8dc5
              - 500a0982891b8dc5
            lun: 2
            fsType: ext4
            readOnly: true 
    

fc 部分是您的 SAN 和 LUN 必须配置的地方。

  • 对于 NFS 卷(nginx-pod-nfs-volume.yaml):

    ...
      containers:
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nfs-volume
              mountPath: "/usr/share/nginx/html/hello"
      volumes:
        - name: nfs-volume
          nfs:
            server: nfs.corp.mycompany.org
            path: /k8s-nginx-hello
            readOnly: true 
    

nfs 部分是您的 NAS 和导出卷必须配置的地方。

请注意以下几点:

  • 这两种类型的卷,FC 块存储和 NFS,将根据 Pod 的存在需求附加到节点。

  • 虽然这两种类型的卷可以解决一系列挑战,但它们代表了配置与资源解耦的反模式。

  • 虽然 configMap 作为卷挂载在容器上,并包含两个 HTML 文件,但其他类型的卷需要不同的方法来注入数据。

  • 还有其他类型的存储卷可供选择:kubernetes.io/docs/concepts/storage/volumes/

在 Kubernetes 中,卷的概念是部署有状态应用程序的一个很好的起点。然而,由于某些卷的限制、其他卷的复杂性以及所需的存储知识,似乎很难通过这种对象定义来扩展数百个或数千个微服务。幸运的是,通过增加一个抽象层,Kubernetes 提供了一种无关存储的方式,通过使用 PersistentVolume 对象来按规模消耗存储,我们将在下一节中介绍这一点。

引入 PersistentVolumes

就像 PodConfigMapPersistentVolume 是通过 kube-apiserver 暴露的资源类型;你可以像操作其他 Kubernetes 对象一样,使用 YAML 和 kubectl 来创建、更新和删除持久卷PVs)。

以下命令将演示如何列出当前在 Kubernetes 集群中已配置的 PersistentVolume 资源类型:

$ kubectl get persistentvolume
No resource found 

persistentvolume 对象也可以通过复数形式 persistentvolumes 和别名 pv 访问。以下三个命令本质上是相同的:

$ kubectl get persistentvolume
No resource found
$ kubectl get persistentvolumes
No resource found
$ kubectl get pv
No resource found 

你会发现,在 Kubernetes 世界中,pv 别名被广泛使用,很多人直接称持久卷为 pv,所以请注意这一点。到目前为止,我们的 Kubernetes 集群中尚未创建任何 PersistentVolume 对象,这也是我们在前一个命令的输出中没有看到任何资源的原因。

PersistentVolume 是一个对象,本质上代表一个你可以附加到 Pod 的存储。这个存储被称为持久性存储,因为它不应该与 Pod 的生命周期绑定。

事实上,正如第五章《使用多容器 Pod 和设计模式》中提到的,Kubernetes Pods 使用了卷的概念。此外,我们还发现了 emptyDir 卷,它初始化一个空目录,Pod 可以共享这个目录。它还定义了一个在工作节点文件系统中的路径,该路径将暴露给你的 Pods。两个卷都应该与 Pod 的生命周期绑定。这意味着,一旦 Pod 被销毁,存储在卷中的数据也会被销毁。

然而,有时候,你并不希望卷被销毁。你只是希望它有自己的生命周期,即使 Pod 失败,卷和其数据依然能够保持存活。这时,PersistentVolumes就派上用场了:本质上,它们是与 Pod 生命周期无关的卷。由于它们是像 Pod 一样的资源类型,它们可以独立存在!从本质上讲,PV 确保了你的存储在 Pod 存在之外仍然可用,这对于在有状态应用中保持数据完整性至关重要。现在,让我们来分解一下PersistentVolume对象:它们由两个关键元素组成——后端技术(PersistentVolume类型)和访问模式(如ReadWriteOnce (RWO))。理解这些概念对于在 Kubernetes 环境中有效利用 PV 至关重要。

请记住,PersistentVolumes对象仅仅是etcd 数据存储中的条目,它们本身并不是实际的磁盘。

PersistentVolume仅仅是 Kubernetes 中指向某个存储资源的指针,比如 NFS、磁盘、Amazon 弹性块存储 (EBS) 卷等。这样,你就可以通过 Kubernetes 以 Kubernetes 的方式访问这些技术。

在接下来的章节中,我们将首先解释什么是PersistentVolume类型。

引入 PersistentVolume 类型

正如你已经知道的,最简单的 Kubernetes 配置只包含一个简单的minikube安装,而最复杂的 Kubernetes 配置则可能由数十台服务器组成,运行在一个大规模可扩展的基础设施上。所有这些不同的配置必然会有不同的方式来管理持久化存储。例如,三大著名的公共云提供商提供了许多不同的解决方案。我们来列举几个,如下所示:

  • Amazon EBS 卷

  • Amazon 弹性文件系统 (EFS) 文件系统

  • Google GCE 持久磁盘 (PD)

  • Microsoft Azure 磁盘

这些解决方案都有自己独特的设计和原则,并且有各自的逻辑和机制。Kubernetes 的构建原则是,所有这些配置应当通过一个对象进行抽象,以便处理所有不同的技术;这个单一的对象就是PersistentVolume资源类型。PersistentVolume资源类型是将附加到运行中的 Pod 上的对象。事实上,Pod 是 Kubernetes 的一种资源,并不知道 EBS 或 PD 是什么;Kubernetes Pod 只与PersistentVolumes协同工作,而PersistentVolumes也是一种 Kubernetes 资源。

无论你的 Kubernetes 集群是在 Google GKE、Amazon EKS 上运行,还是在本地机器上的单个 minikube 集群上运行,都没有关系。当你希望管理持久化存储时,你需要创建、使用并部署PersistentVolumes对象,并将它们绑定到你的 Pods!

以下是 Kubernetes 开箱即用的一些后端技术:

  • csi容器存储接口 (CSI)

  • fc:FC 存储

  • iscsi:通过 IP 的 SCSI

  • local:使用本地存储

  • hostPath:HostPath 卷

  • nfs:常规网络文件存储

上述列表并不详尽:Kubernetes 非常灵活,可以与许多存储解决方案一起使用,这些解决方案可以在您的集群中抽象为 PersistentVolume 对象。

请注意,在 Kubernetes 的最新版本中,几个 PersistentVolume 类型已经被弃用或删除,这表明存储管理方式发生了变化。这一变化是 Kubernetes 持续演进的一部分,旨在简化其 API 并提高与现代存储解决方案的兼容性。

例如,从 Kubernetes 1.29 起,以下 PersistentVolume 类型已被删除或弃用:

  • awsElasticBlockStore – 亚马逊 EBS

  • azureDisk – Azure 磁盘

  • azureFile – Azure 文件

  • portworxVolume – Portworx 卷

  • flexVolume – FlexVolume

  • vsphereVolume – vSphere VMDK 卷

  • cephfs – CephFS 卷

  • cinder

这些变化反映了朝着标准化存储接口的更广泛趋势,并强调了更具可移植性和云中立的解决方案。如需详细指南和有关 PV 和支持类型的更新信息,您可以参考官方的 Kubernetes 文档:kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes

PersistentVolume 带来的好处

PV 是 Kubernetes 中管理有状态应用程序的关键组件。与短暂存储不同,PVs 确保数据超越单个 Pod 的生命周期,因此它们非常适合需要数据保留和一致性的应用程序。这些存储资源为 Kubernetes 生态系统带来了灵活性和可靠性,提升了性能和弹性。

PersistentVolume 有三个主要的好处:

  • Kubernetes 中的 PV 独立于使用它的 Pod 存在。这意味着,如果您删除或重新创建一个附加到 PersistentVolume 的 Pod,该卷上存储的数据将保持不变。数据的持久性取决于 PersistentVolume 的回收策略:在保留策略下,数据将继续保留供未来使用,而删除策略则在 Pod 被删除时同时删除该卷及其数据。因此,您可以管理您的 Pod,而无需担心丢失存储在 PersistentVolumes 上的数据。

  • 当 Pod 崩溃时,PersistentVolume 对象将继续存在,不会从集群中删除。

  • PersistentVolume 是集群级别的;这意味着它可以附加到任何节点上运行的任何 Pod。(您将在本章后面了解有关限制和方法的内容。)

请记住,这三个声明并不总是 100% 有效。事实上,有时,PersistentVolume 对象可能会受到其底层技术的影响。

为了演示这一点,假设有一个 PersistentVolume 对象,它是指向计算节点上 hostPath 存储的指针。在这样的设置中,PersistentVolume 不会对其他节点可用。

然而,如果你考虑另一个例子,比如 NFS 设置,情况就不一样了。实际上,你可以从多个机器同时访问 NFS。因此,一个由 NFS 支持的PersistentVolume对象可以从运行在不同节点的多个 Pod 访问,而不会出现太大问题。为了理解如何在多个不同节点上同时创建PersistentVolume对象,我们需要考虑访问模式的概念,接下来我们将深入探讨这个话题。

引入 PersistentVolume 访问模式

顾名思义,访问模式是你在创建PersistentVolume类型时可以设置的选项,它将告诉 Kubernetes 该如何挂载卷。

PersistentVolumes支持四种访问模式,分别如下:

  • ReadWriteOnceRWO):此卷仅允许一个节点同时进行读写操作。

  • ReadOnlyManyROX):此卷允许多个节点同时以只读模式访问。

  • ReadWriteManyRWX):此卷允许多个节点同时进行读写操作。

  • ReadWriteOncePod:这是最近引入的一种新模式,并且在 Kubernetes 1.29 版本中已稳定。在此访问模式下,卷可以被单个 Pod 以读写方式挂载。当你希望整个集群中只有一个 Pod 可以读取或写入持久卷声明PVC)时,请使用ReadWriteOncePod访问模式。

即使某个PersistentVolume类型支持多种访问模式,仍然必须至少设置一种访问模式。事实上,并非所有PersistentVolume类型都支持所有访问模式,具体如下面的表格所示。

表 9.1:不同 PersistentVolume 类型支持的访问模式(图片来源:kubernetes.io/docs/concepts/storage/persistent-volumes)

在 Kubernetes 中,PersistentVolume类型的访问模式与底层存储技术及其数据处理方式密切相关。这就是为什么不同的 PV 类型只支持特定模式的原因:

文件存储与块存储:

  • 文件存储(如网络文件系统NFS)或常见互联网文件系统CIFS))允许多个客户端同时访问相同的文件。这就是为什么文件存储系统能够支持多种访问模式,如 RWO、ROX 和 RWX。它们设计用于处理网络上的多客户端访问,使得多个节点能够从同一卷读取和写入,而不会导致数据损坏。

  • 块存储(如本地存储或 hostPath)本质上有所不同。块存储设计用于一次只能由一个客户端访问,因为它处理的是原始磁盘扇区,而不是文件。多个客户端的并发访问会导致数据不一致或损坏。因此,块存储仅支持 RWO 模式,在该模式下,单个节点可以对卷进行读写操作。

内部存储与外部存储:

  • hostPath卷,指的是与工作负载位于同一节点上的存储,天生受到该节点的限制。由于此存储与物理节点绑定,它不能被集群中的其他节点访问。这使得它仅与 RWO 模式兼容。

  • 另一方面,NFS 或其他外部存储解决方案被设计为允许通过网络访问,使多个节点能够共享相同的存储。这种灵活性使它们能够支持额外的模式,如 RWX。

理解这一区分有助于澄清为什么某些PersistentVolume类型支持更灵活的访问模式,而其他类型则受限。

现在,让我们创建我们的第一个PersistentVolume对象。

创建我们的第一个 PersistentVolume 对象

让我们使用声明式方法在 Kubernetes 集群上创建一个PersistentVolume。由于PersistentVolume是更复杂的资源,强烈建议避免使用命令式方法。声明式方法允许你在YAML文件中一致地定义和管理资源,使得跟踪更改、版本控制配置,并确保在不同环境中重复执行变得更加容易。这个方法也使得管理像PersistentVolume这样的大型或复杂资源变得更简单,因为精确的配置和细致的规划至关重要。

请参阅下面的示例 YAML 定义,用于创建PersistentVolume对象:

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data" 

这是PersistentVolume的最简单形式。本质上,这个YAML文件在 Kubernetes 集群中创建了一个PersistentVolume条目。因此,这个PersistentVolume将是hostPath类型。

hostPath类型的PersistentVolume不建议用于生产或关键工作负载。我们在这里仅用于演示目的。

让我们按如下方式将 PV 配置应用到集群中:

$ kubectl apply -f pv-hostpath.yaml
persistentvolume/pv-hostpath created 

它可以是更复杂的卷,比如基于云的磁盘或 NFS,但在最简单的形式下,PersistentVolume可以仅仅是运行 Pod 的节点上的hostPath类型。

Kubernetes 的 PersistentVolumes 如何处理存储?

正如我们之前学到的,PersistentVolume资源类型是指向一个存储位置的指针,这个位置可以是磁盘、NFS 驱动器,或者是由存储操作员控制的磁盘卷。所有这些不同的技术都以不同的方式进行管理。然而,幸运的是,在 Kubernetes 中,它们都由PersistentVolume对象表示。

简单来说,创建PersistentVolumeYAML文件会根据PersistentVolume背后的后端技术有所不同。例如,如果你希望你的PersistentVolume指向 NFS 共享,你需要满足以下两个条件:

  • NFS 共享已经配置好,并且可以从 Kubernetes 节点访问。

  • 用于创建PersistentVolumeYAML文件必须包含 NFS 服务器的详细信息和 NFS 共享信息。

以下 YAML 定义是使用 NFS 作为后端创建PersistentVolume的示例:

# pv-nfs.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /appshare
    server: nfs.example.com 

为了让PersistentVolume正常工作,它需要能够将 Kubernetes 与实际的存储进行连接。所以,你需要在 Kubernetes 外部创建一个存储资源或进行存储资源的供应,然后通过包含由外部存储技术支持的磁盘或卷的唯一 ID 来创建PersistentVolume条目。接下来,让我们在下一节详细了解一些PersistentVolume的 YAML 文件示例。

使用原始块存储卷创建 PersistentVolume

这个示例展示了一个指向原始块存储卷的PersistentVolume对象:

# pv-block.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-block
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  persistentVolumeReclaimPolicy: Retain
  fc:
    targetWWNs: ["50060e801049cfd1"]
    lun: 0
    readOnly: false 

如你所见,在这个YAML文件中,fc 部分包含了这个PersistentVolume对象所指向的 FC 存储卷的详细信息。具体的原始卷通过targetWWNs键来识别。就是这么简单。通过这个 YAML 文件,Kubernetes 能够找到合适的全球唯一名称WWN)并保持指向它的指针。

现在,让我们稍微讨论一下存储资源的供应。

Kubernetes 能否处理资源本身的供应或创建?

需要单独创建实际的存储资源,然后在 Kubernetes 中创建PersistentVolume这一事实可能有些繁琐。

幸运的是,Kubernetes 也能够与云提供商或其他存储后端的API进行通信,以便动态创建卷或磁盘。这里有一个叫做动态供应的功能,专门用于管理PersistentVolume。当涉及到PersistentVolume供应时,它让事情变得更加简单,但仅在受支持的存储后端或云提供商上有效。

然而,这是一个高级话题,我们将在本章稍后详细讨论。

现在我们已经知道如何在集群内部署PersistentVolume对象,接下来可以尝试将它们挂载到 Pod 上。实际上,在 Kubernetes 中,一旦你创建了一个PersistentVolume,你需要将它挂载到 Pod 上才能使用。在这里,事情会稍微变得更为复杂和概念化;Kubernetes 使用一个中间对象来将PersistentVolume挂载到 Pod 上。这个中间对象叫做PersistentVolumeClaim。接下来我们将专注于它。

理解如何将 PersistentVolume 挂载到你的 Pod

现在我们可以尝试将一个PersistentVolume对象挂载到 Pod。为了实现这一点,我们需要使用另一个对象,这也是本章需要探索的第二个对象,叫做PersistentVolumeClaim

介绍 PersistentVolumeClaim

就像PersistentVolumeConfigMap一样,PersistentVolumeClaim也是在你的 Kubernetes 集群中存在的另一种独立资源类型。

首先,请记住,即使这两个名字几乎相同,PersistentVolumePersistentVolumeClaim是两个不同的资源,代表着两种不同的事物。

你可以使用kubectl列出在集群内创建的PersistentVolumeClaim资源类型,如下所示:

$ kubectl get persistentvolumeclaims
No resources found in default namespace. 

以下输出告诉我们,在我的集群中没有创建任何PersistentVolumeClaim资源。请注意,pvc别名同样有效:

$ kubectl get pvc
No resources found in default namespace. 

你会很快发现,很多使用 Kubernetes 的人简单地将PersistentVolumeClaim资源称为pvc。所以,在使用 Kubernetes 时,如果看到pvc这个术语,不要感到惊讶。话虽如此,接下来让我们来解释一下 Kubernetes 中的PersistentVolumeClaim资源。

存储创建与存储消费的分离

理解PersistentVolumePersistentVolumeClaim之间区别的关键在于理解:一个是用来表示存储本身,而另一个则表示 Pod 请求实际存储的需求。

原因在于 Kubernetes 通常是由两类人使用:

  • Kubernetes 管理员:这个人负责维护集群、操作集群,并添加计算资源和持久存储。

  • Kubernetes 应用开发者:这个人负责开发和部署应用程序,简单来说,就是消费管理员提供的计算资源和存储。

事实上,如果你在你的组织中同时承担这两个角色也是没有问题的;然而,这个信息对于理解如何将PersistentVolume挂载到 Pods 的工作流程至关重要。

Kubernetes 的设计理念是,PersistentVolume对象应该属于集群管理员的范围,而PersistentVolumeClaim对象属于应用开发者的范围。集群管理员负责添加PersistentVolumes(或动态卷操作器),因为它们可能是硬件资源,而开发者更清楚需要多少存储以及需要什么样的存储,这就是为什么构建了PersistentVolumeClaim对象。

本质上,Pod 无法直接挂载PersistentVolume对象。它需要明确地请求它。这种请求操作是通过创建PersistentVolumeClaim对象并将其附加到需要PersistentVolume对象的 Pod 上来实现的。

这就是为什么存在额外抽象层的唯一原因。现在,让我们理解下一部分总结的PersistentVolume工作流程。

理解 PersistentVolume 的工作流程

一旦开发者构建好应用程序,如果需要,他们有责任请求一个PersistentVolume对象。为此,开发者将编写两个YAML清单:

  • 一个清单将会写为 Pod 或部署。

  • 另一个清单将会写为PersistentVolumeClaim

Pod 必须按如下方式编写,以便 PersistentVolumeClaim 对象作为 volumeMount 配置键挂载在 YAML 文件中。请注意,为了使其工作,PersistentVolumeClaim 对象需要与挂载它的应用程序 Pod 在同一命名空间中。当两个 YAML 文件都应用并且资源都在集群中创建时,PersistentVolumeClaim 对象将查找一个符合声明要求的 PersistentVolume 对象。假设在 Kubernetes 集群中创建并准备了一个能够满足声明的 PersistentVolume 对象,那么该对象将被附加到 PersistentVolumeClaim 对象上。

如果一切正常,声明被认为已完成,卷被正确挂载到 Pod 上:如果你理解这个工作流程,基本上你就理解了与 PersistentVolume 使用相关的一切。

以下图示展示了 Kubernetes 中静态存储供应的工作流程。

图 9.1:Kubernetes 中的静态存储供应

你将在本章的后续部分学习动态存储供应,介绍动态供应

想象一下,一个开发者需要为其在 Kubernetes 中运行的应用程序提供持久存储。以下是接下来发生的流程:

  1. 管理员准备 PersistentVolume:Kubernetes 管理员准备后端存储并创建一个 PersistentVolume 对象。该 PV 类似于存储声明,指定容量、访问模式(读写、只读)以及底层存储系统(例如,hostPath、NFS)等细节。

  2. 开发者使用 PersistentVolumeClaim 发出请求:开发者创建一个 PersistentVolumeClaim 对象。这个 PVC 就像一个存储请求,列出了开发者的需求。它指定了大小、访问模式和任何存储类的偏好(可以将其视为存储的愿望清单)。开发者还在 Pod 的 YAML 文件中定义了一个卷挂载,指定了 Pod 如何访问持久存储卷。

  3. Kubernetes 执行请求:在 Pod 和 PVC 创建后,Kubernetes 会查找一个合适的 PV,匹配 PVC 中列出的要求。这就像一个配对服务,确保请求的存储与可用的存储相符。

  4. Pod 使用 volumeMount 利用存储:一旦 Kubernetes 找到一个匹配的 PV,它会将其绑定到 PVC 上。这使得存储对 Pod 可访问。

  5. 数据流开始(读/写操作):现在,Pod 可以根据 PV 中定义的访问模式与持久存储进行交互。它可以对存储在卷中的数据执行读写操作,即使 Pod 重启也能确保数据持久性。

请注意,PersistentVolume 是集群范围的,而 PersistentVolumeClaim、Pod 和 volumeMount 是命名空间范围的对象。

PV、PVC 和 Kubernetes 之间的协作确保了开发人员可以访问持久存储,以支持他们的应用程序,从而使他们能够在 Pod 生命周期之间存储和检索数据。

这种设置可能一开始看起来有些复杂,但你很快就会习惯它。

在接下来的部分,我们将学习如何使用PersistentVolumePersistentVolumeClaim在 Pod 中使用存储。

创建一个带有 PersistentVolumeClaim 对象的 Pod

在这一部分,我们将创建一个 Pod,该 Pod 会在minikube集群中挂载PersistentVolume。这将是一个PersistentVolume对象,但这次它不会绑定到 Pod 的生命周期。实际上,由于它将作为一个真正的PersistentVolume对象进行管理,hostPath类型将使其生命周期独立于 Pod。

第一件事是创建一个PersistentVolume对象,它将是一个hostPath类型。以下是实现该操作的 YAML 文件。请注意,我们在metadata部分为这个PersistentVolume对象创建了一些任意标签。这样,稍后从PersistentVolumeClaim对象中获取它时会更容易。

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-hostpath-pv
  labels:
    type: hostpath
    env: prod
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/tmp/test"
  storageClassName: slow 

请注意 YAML 中的以下项目,我们稍后将用于匹配 PVC:

  • labels

  • capacity

  • accessModes

  • StorageClassName

现在我们可以创建并列出我们集群中可用的PersistentVolume条目,并且应该看到这个条目已存在。请注意,pv别名同样有效:

$ kubectl apply -f pv.yaml
persistentvolume/my-hostpath-pv created
$ kubectl get pv
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
my-hostpath-pv   1Gi        RWO            Retain           Available           slow           <unset>                          3s 

我们可以看到,PersistentVolume已经成功创建,状态为Available

现在,我们需要创建两个东西来挂载PersistentVolume对象:

  • 一个针对特定PersistentVolume对象的PersistentVolumeClaim对象

  • 一个使用PersistentVolumeClaim对象的 Pod

为了演示命名空间范围的项目和集群范围的项目,让我们为 PVC 和 Pod 创建一个命名空间(请参阅pv-ns.yaml文件):

$ kubectl apply -f pv-ns.yaml
namespace/pv-ns created 

接下来,让我们按顺序创建PersistentVolumeClaim对象:

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-hostpath-pvc
  namespace: pv-ns
spec:
  resources:
    requests:
      storage: 1Gi
  accessModes:
    - ReadWriteOnce
  selector:
    matchLabels:
      type: hostpath
      env: prod
  storageClassName: slow 

让我们创建 PVC,并检查它是否在集群中成功创建。请注意,pvc别名在这里也能正常工作:

$ kubectl apply -f pvc.yaml
persistentvolumeclaim/my-hostpath-pvc created
$ kubectl get pvc -n pv-ns
NAME              STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
my-hostpath-pvc   Bound    my-hostpath-pv   1Gi        RWO            slow           <unset>                 2m29s 

请注意,现在 PVC 的状态是Bound,这意味着 PVC 已经与 PV 匹配,并准备好使用存储。

现在,PersistentVolume对象和PersistentVolumeClaim对象已经存在,我们可以创建一个 Pod,该 Pod 将使用 PVC 挂载 PV。

让我们创建一个 NGINX Pod 来完成这个任务:

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: pv-ns
spec:
  containers:
    - image: nginx
      name: nginx
      volumeMounts:
        - mountPath: "/var/www/html"
          name: mypersistentvolume
  volumes:
    - name: mypersistentvolume
      persistentVolumeClaim:
        claimName: my-hostpath-pvc 

如您所见,在volumeMounts部分,PersistentVolumeClaim对象被作为一个卷引用,我们通过其名称来引用 PVC。请注意,PVC 必须与挂载它的 Pod 位于同一命名空间。这是因为 PVC 是命名空间范围的资源,而 PV 不是。这个资源没有标签和选择器;要将 PVC 绑定到 Pod,只需使用 PVC 的名称即可。

这样,Pod 将会附加到 PersistentVolumeClaim 对象上,该对象会找到对应的 PersistentVolume 对象。最终,这将使主机路径在我的 NGINX Pod 上可用并挂载。

创建 Pod 并测试状态:

$ kubectl apply -f pod.yaml
pod/nginx created
$ kubectl get pvc,pod -n pv-ns
NAME                                    STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/my-hostpath-pvc   Bound    my-hostpath-pv   1Gi        RWO            slow           <unset>                 4m32s
NAME        READY   STATUS    RESTARTS   AGE
pod/nginx   1/1     Running   0          13s 

Pod 启动并运行,主机路径 /tmp/test 通过 PV 和 PVC 挂载在里面。到目前为止,我们已经了解了 PersistentVolumePersistentVolumeClaim 对象是什么,以及如何使用它们在 Pod 上挂载持久存储。

接下来,我们必须继续探索 PersistentVolumePersistentVolumeClaim 的机制,解释这两个对象的生命周期。由于它们独立于 Pod,它们的生命周期具有一些特定的行为,需要你特别关注。

理解 Kubernetes 中 PersistentVolume 对象的生命周期

PersistentVolume 对象非常适合用来维护应用的状态,而不受运行它们的 Pods 或容器生命周期的限制。

然而,由于 PersistentVolume 对象有自己的生命周期,它们具有一些你在使用时需要注意的特定机制。我们接下来会更详细地探讨它们。

理解为什么 PersistentVolume 对象不绑定到命名空间

在使用 PersistentVolume 对象时需要注意的第一件事是,它们不是 namespaced(命名空间限定的)资源,而 PersistentVolumeClaim 对象是。

$ kubectl api-resources --namespaced=false |grep -i volume
persistentvolumes                 pv           v1                                false        PersistentVolume
volumeattachments                              storage.k8s.io/v1                 false        VolumeAttachment 

因此,如果 Pod 想要使用 PersistentVolume,那么 PersistentVolumeClaim 必须与 Pod 在同一个命名空间内创建。

PersistentVolume 通常会有以下生命周期阶段:

  • 供应:管理员创建 PV,定义容量、访问模式以及可选的详细信息,如存储类和回收策略。

  • 未绑定状态:最初,PV 可用但未附加到任何 Pod(未绑定)。

  • 声明:开发者创建 PVC,指定大小、访问模式和存储类偏好(请求存储)。

  • 匹配和绑定:Kubernetes 查找一个未绑定的 PV,该 PV 满足 PVC 的要求,并将它们绑定在一起。

  • 使用:Pod 通过其 YAML 文件中定义的卷挂载访问绑定的 PV。

  • 释放:当使用 PVC 的 Pod 被删除时,PVC 变为未绑定(PV 状态取决于回收策略)。

  • 删除:管理员可以根据存储资源的回收策略删除 PV 对象本身。

现在,让我们检查一下 PersistentVolume 的另一个重要方面,称为回收策略。这在你想要卸载运行中的 Pod 上的 PVC 时非常重要。

回收 PersistentVolume 对象

当涉及到PersistentVolume时,有一个非常重要的选项是你需要理解的,那就是回收策略。那么这个选项具体有什么作用呢?

这个选项会告诉 Kubernetes 在删除与之关联的 PersistentVolumeClaim 对象时,应该对你的 PersistentVolume 对象执行何种操作。

实际上,删除 PersistentVolumeClaim 对象就是删除 Pods 与 PersistentVolume 对象之间的连接,所以这就像是卸载卷,然后该卷可以再次被其他应用程序使用。

然而,在某些情况下,你可能不希望这种行为;相反,你希望在删除相应的 PersistentVolumeClaim 对象后,PersistentVolume 对象会被自动移除。这就是回收策略选项存在的原因,也是你应该配置的内容。

让我们来解释这三种回收策略:

  • 删除:这是三者中最简单的一种。当你将回收策略设置为删除时,在删除对应的 PersistentVolumeClaim 对象时,PersistentVolume 对象会被清除,并且 PersistentVolume 记录将从 Kubernetes 集群中移除。你可以在希望删除数据并不让其他应用程序使用时使用这种策略。请记住,这是一个永久性选项,所以如果你需要恢复任何内容,可能需要与底层存储提供商建立备份策略。

在我们的示例中,PV 是通过 hostPath 手动创建的,路径为/tmp/。删除操作在这里将没有任何问题。然而,当你手动创建 PV 时,删除操作可能并不适用于所有 PV 类型。强烈建议使用动态 PV 配置,稍后在本章中你将学习到它。

  • 保留:这是第二种策略,与删除策略相反。如果你设置了这个回收策略,那么在删除对应的 PersistentVolumeClaim 对象时,PersistentVolume 对象不会被删除。相反,PersistentVolume 对象会进入释放状态,这意味着它仍然可在集群中使用,且集群管理员可以手动取回所有数据。

  • 回收:这是一种结合前两种策略的策略。首先,卷会被清空所有数据,例如基本的 rm -rf volume/* 操作。然而,卷本身仍然会在集群中可访问,因此你可以将它重新挂载到你的应用程序上。

回收策略中的回收模式已经被弃用。现在建议采用动态配置作为首选方法。

回收策略可以在集群中直接通过 PersistentVolume 的 YAML 定义文件进行设置。

更新回收策略

回收策略的好消息是,你可以在创建 PersistentVolume 对象后更改它;这是一个可变的设置。

为了演示回收策略的差异,让我们使用之前创建的 Pod、PV 和 PVC,如下所示:

$ kubectl get pod,pvc -n pv-ns
NAME        READY   STATUS    RESTARTS   AGE
pod/nginx   1/1     Running   0          30m
NAME                                    STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/my-hostpath-pvc   Bound    my-hostpath-pv   1Gi        RWO            slow           <unset>                 34m 

先删除 Pod,因为它正在使用 PVC:

$ kubectl delete pod nginx -n pv-ns
pod "nginx" deleted
$ kubectl delete pvc my-hostpath-pvc -n pv-ns
persistentvolumeclaim "my-hostpath-pvc" deleted 

现在,检查 PV 的状态:

$ kubectl get pv
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
my-hostpath-pv   1Gi        RWO            Retain           Released   pv-ns/my-hostpath-pvc   slow           <unset>                          129m 

从输出中可以看出,PV 处于Released状态,但尚未进入Available状态,无法供下一个 PVC 使用。

让我们使用kubectl patch命令更新回收策略为Delete,如下所示:

$ kubectl patch pv/my-hostpath-pv -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
persistentvolume/my-hostpath-pv patched
Since the PV is not bound to any PVC, the PV will be instantly deleted due to the Delete reclaim policy:
$ kubectl get pv
No resources found 

如前面的输出所示,我们更新了 PV 的回收策略,然后 PV 被从 Kubernetes 集群中删除。

现在,让我们讨论一下 PV 和 PVC 可能具有的不同状态。

理解 PersistentVolume 和 PersistentVolumeClaim 的状态

就像 Pods 可以处于不同的状态,如PendingContainerCreatingRunning等,PersistentVolumePersistentVolumeClaim也可以有不同的状态。你可以通过执行kubectl get pvkubectl get pvc命令来查看它们的状态。

PersistentVolume有以下不同的状态,你需要了解:

  • Available:这是新创建的 PV 的初始状态,表示该 PV 已准备好绑定到 PVC。

  • Bound:该状态表示 PV 当前已被特定的 PVC 声明,并且正在 Pod 中使用。基本上,它表示该卷目前正在使用中。当此状态应用于PersistentVolumeClaim对象时,表示该 PVC 当前正在使用:即,Pod 正在使用它并通过它访问 PV。

  • TerminatingTerminating状态适用于PersistentVolumeClaim对象。这是当你执行kubectl delete pvc命令后,PVC 进入的状态。

  • Released:如果使用该 PV 的 PVC 被删除(并且 PV 的回收策略设置为“Retain”),则 PV 将转为此状态。它本质上是未绑定的,但仍然可以供未来的 PVC 使用。

  • Failed:该状态表示 PV 出现问题,无法使用。可能的原因包括存储提供商错误、访问问题或供给器的问题(如果适用)。

  • Unknown:在少数情况下,由于与底层存储系统的通信失败,PV 状态可能为未知。

现在我们已经掌握了与PersistentVolumePersistentVolumeClaim相关的基础知识,这些足以开始在 Kubernetes 中使用持久存储。然而,还有一个重要的概念需要了解,那就是动态供给。这是 Kubernetes 的一个非常令人印象深刻的功能,它能够与云服务提供商的 API 进行交互,在云端创建持久存储。此外,它还可以通过动态创建 PV 对象,将这些存储提供给集群。在下一节中,我们将对比静态和动态供给。

理解静态与动态 PersistentVolume 供给

到目前为止,我们仅通过静态供给来配置了PersistentVolume。现在,我们将探索动态PersistentVolume供给,它允许直接从 Kubernetes 集群进行PersistentVolume供给。

静态与动态供给

到目前为止,使用静态配置时,您已经学会了必须遵循以下工作流程:

  1. 您针对云提供商或后端技术创建存储部分。

  2. 接着,您创建PersistentVolume对象,作为指向实际存储的 Kubernetes 指针。

  3. 随后,您创建一个 Pod 和一个 PVC,将 PV 绑定到 Pod。

这就是静态配置。它是静态的,因为您必须在创建 Kubernetes 中的 PV 和 PVC 之前创建存储部分。它工作得很好;但是,在规模上,尤其是在管理数百个 PV 和 PVC 时,管理起来可能变得越来越困难。假设您要创建一个 Amazon EBS 卷,并将其挂载为PersistentVolume对象,您可以像这样使用静态配置:

  1. 对 AWS 控制台进行身份验证。

  2. 创建一个 EBS 卷。

  3. 复制/粘贴其唯一 ID 到PersistentVolume YAML 定义文件中。

  4. 使用您的 YAML 文件创建 PV。

  5. 创建一个 PVC 来获取此 PV。

  6. 将 PVC 挂载到 Pod 对象。

同样,它应该以手动或自动化的方式工作,但如果在规模上进行操作,特别是处理可能有数十个 PV 和 PVC 时,操作可能变得复杂且极其耗时。

这就是为什么 Kubernetes 开发人员决定,如果 Kubernetes 能够代表您配置实际存储部分,并创建PersistentVolume对象来作为指向它的指针,那将会更好。这就是所谓的动态配置。

引入动态配置

当使用动态配置时,您需要配置您的 Kubernetes 集群,以便对接后端存储提供商(如 AWS、Azure 或其他存储设备)。然后,您发出一个命令来提供存储磁盘或卷,并自动创建PersistentVolume,以便 PVC 可以使用它。这样,通过自动化可以节省大量时间。动态配置非常有用,因为 Kubernetes 支持各种存储技术。在本章的前面部分,我们已经介绍了其中一些,例如 NFS 和其他类型的存储。

那么,Kubernetes 如何实现这种多功能性呢?答案是它利用第三种资源类型,即StorageClass对象,我们将在本章中学习它。

CSI 简介

在我们讨论StorageClass之前,让我们了解一下 CSI,它充当 Kubernetes 与各种存储解决方案之间的桥梁。它定义了一种标准接口,用于将存储公开给容器工作负载。CSI 为与 Kubernetes 原语(如PersistentVolume)交互提供了抽象层,使得可以将各种存储解决方案集成到 Kubernetes 中,并保持供应商中立的方法。

Kubernetes 动态存储配置通常涉及以下步骤:

  1. 安装并配置StorageClass和提供者:管理员安装 CSI 驱动程序(或内置提供者)并配置StorageClass,该类定义了存储类型、参数和回收策略。

  2. 开发人员创建包含StorageClass信息的 PVC:开发人员创建PersistentVolumeClaim,指定所需的大小和访问模式,并引用StorageClass以请求动态配置。

  3. StorageClass/CSI 驱动程序触发向后端提供者的请求:当 Kubernetes 检测到 PVC 时,自动触发 CSI 驱动程序(或提供者),将请求发送给后端存储系统以进行存储配置。

  4. 提供者与后端存储通信并创建卷:提供者与后端存储系统通信,创建卷,并在 Kubernetes 中生成与 PVC 绑定的PersistentVolume

  5. PVC 挂载到 Pod,允许访问存储:PVC 挂载到请求的 Pod,使 Pod 能够访问根据 Pod 配置中的volumeMount指定的存储。

下图展示了动态 PV 配置工作流。

图 9.2:Kubernetes 中的动态 PV 配置

CSI 驱动程序是存储供应商提供的容器化实现,符合 CSI 规范,提供存储卷的配置、附加、分离和管理等功能。

CSI 节点和控制器服务是 Kubernetes 服务,分别在工作节点和控制平面上运行 CSI 驱动程序逻辑,促进 Pod 与存储系统之间的通信。

一旦在 Kubernetes 集群上部署了支持 CSI 的卷驱动程序,用户可以利用csi卷类型。(参考文档:kubernetes-csi.github.io/docs/drivers.html,了解可以与 Kubernetes 一起使用的 CSI 驱动程序集合)。这使得用户可以附加或挂载由 CSI 驱动程序暴露的卷。在 Pod 中使用csi卷有三种方法:

  • 引用PersistentVolumeClaim:这种方法将 Pod 与由 Kubernetes 管理的持久存储连接起来。

  • 使用通用临时卷:此方法提供不在 Pod 重启之间保持的临时存储。

  • 利用 CSI 临时卷(如果驱动程序支持):这为驱动程序提供了特定的临时存储选项,超出了通用版本的范畴。

请记住,您不会直接与 CSI 交互。StorageClass可以通过provisioner字段按名称引用 CSI 驱动程序,利用 CSI 进行卷配置。

引入 StorageClasses

StorageClass是由kube-apiserver暴露的另一种资源类型。您可能已经在kubectl get pv命令输出中注意到了此字段。此资源类型使 Kubernetes 能够透明地处理多种底层技术。

StorageClasses充当用户接口,用于定义存储需求。CSI 驱动程序,由StorageClasses引用,提供了基于特定存储系统来供应和管理存储的实际实现细节。StorageClasses本质上弥合了存储需求与 CSI 驱动程序所提供的能力之间的差距。

你可以通过使用kubectl来访问并列出在 Kubernetes 集群中创建的storageclasses资源。以下是列出存储类的命令:

$ kubectl get sc
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
standard (default)   k8s.io/minikube-hostpath 

我们还可以使用-o yaml选项查看关于StorageClass的详细信息:

$ kubectl get storageclasses standard  -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
...<removed for brevity>...
  name: standard
  resourceVersion: "290"
  uid: f41b765f-301f-4781-b9d0-46aec694336b
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Delete
volumeBindingMode: Immediate 

此外,你还可以使用storageclasses的复数形式以及sc别名。以下三个命令本质上是相同的:

$ kubectl get storageclass
$ kubectl get storageclasses
$ kubectl get sc 

请注意,为了简化起见,我们没有包含命令的输出,但对于这三个命令来说,它们的输出本质上是相同的。命令输出中有两个对我们来说很重要的字段:

  • NAME:这是storageclass对象的名称和唯一标识符。

  • PROVISIONER:这是底层存储技术的名称:这基本上是 Kubernetes 集群用来与底层技术交互的代码。

请注意,你可以创建多个使用相同provisionerStorageClass对象。

由于我们当前在实验室环境中使用的是minikube集群,我们有一个名为standardstorageclass资源,它使用了k8s.io/minikube-hostpath供应器。

这个提供程序处理我的主机文件系统,为我的 Pod 自动创建预配置的主机路径卷,但对于 Amazon EBS 卷或 Google PD 也可以是相同的。

在 GKE 中,Google 构建了一个存储类,具有一个能够与 Google PD API 交互的供应器,这是一个纯 Google Cloud 功能,你可以通过StorageClass来实现,方法如下:

# gce-pd-sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard 

相比之下,在 AWS 中,我们有一个storageclass对象,其供应器能够处理 EBS 卷 API。这些供应器只是与不同云提供商的 API 交互的库。

storageclass对象使得 Kubernetes 能够处理如此多不同的存储技术。从 Pod 的角度来看,无论它是 EBS 卷、NFS 驱动器,还是 GKE 卷,Pod 只会看到一个PersistentVolume对象。所有与实际存储技术相关的底层逻辑都由storageclass对象使用的供应器实现。

好消息是,你可以像插件一样向 Kubernetes 集群中添加任意多个storageclass对象及其供应器。

顺便说一下,什么也不妨碍你通过向集群添加storageclasses来扩展你的集群。你只需要为你的集群添加处理不同存储技术的能力。例如,我们可以将一个 Amazon EBS storageclass对象添加到我们的minikube集群中。然而,尽管这是可能的,但它将完全没用。事实上,如果你的minikube设置不是运行在 EC2 实例上,而是在本地机器上,它将无法附加 EBS。

话虽如此,若采用更实用的方法,你可以考虑使用支持本地部署的 CSI 驱动程序,例如 OpenEBS、TopoLVM 或 Portworx。这些驱动程序允许你即使在 minikube 上,也能使用本地的持久存储。此外,大多数云服务提供商提供免费层,适用于小规模的 Kubernetes 部署,这对于在云环境中测试存储解决方案非常有用,并且不会产生过多成本。

在下一部分中,我们将了解在使用 PVC 时动态存储供应的区别。

理解 PersistentVolumeClaim 在动态存储供应中的角色

使用动态存储供应时,PersistentVolumeClaim对象将会有一个全新的角色。由于在这种使用情况下PersistentVolume已被移除,你将只需要管理PersistentVolumeClaim对象,因为PersistentVolume对象将由StorageClass管理。

让我们通过创建一个 NGINX Pod 来动态挂载一个hostPath类型来展示这一点。在这个例子中,管理员根本不需要预配置PersistentVolume对象。这是因为PersistentVolumeClaim对象和StorageClass对象将能够一起创建并供应PersistentVolume

让我们从创建一个名为dynamicstorage的新命名空间开始,在这里我们将运行我们的示例:

$ kubectl create ns dynamicstorage
namespace/dynamicstorage created 

现在,让我们运行kubectl get sc命令,检查我们是否有一个能够处理在集群中配置的hostPath的存储类。

对于这个特定的storageclass对象,在这个特定的 Kubernetes 设置(minikube)中,我们无需做任何事情来获取storageclass对象,因为它在集群安装时默认创建。然而,根据你的 Kubernetes 发行版,这种情况可能有所不同。

记住这一点,因为它非常重要:在 GKE 上设置的集群可能有默认的存储类,能够处理 Google 的存储产品,而基于 AWS 的集群可能有storageclass来与 Amazon 的存储产品进行通信等等。对于minikube,我们至少有一个默认的storageclass对象,能够处理基于hostPathPersistentVolume对象。如果你理解这一点,你应该明白kubectl get sc命令的输出会根据你的集群所在的位置而有所不同:

$ kubectl get sc
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           false                  21d 

如你所见,我们的集群中确实有一个名为standard的存储类,它能够处理hostPath

一些跨多个云环境和/或本地部署的复杂集群可能会配置大量不同的storageclass对象,以便与多种存储技术进行通信。请记住,Kubernetes 并不依赖于任何特定的云提供商,因此不会在使用支持存储解决方案方面强制或限制您。

现在,我们将创建一个PersistentVolumeClaim对象,它将动态创建一个hostPath类型。以下是创建 PVC 的 YAML 文件。请注意,storageClassName被设置为standard

# pvc-dynamic.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-dynamic-hostpath-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard # VERY IMPORTANT !
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      type: hostpath
      env: prod 

接下来,我们可以在正确的命名空间中创建它:

$ kubectl apply -f pvc-dynamic.yaml -n dynamicstorage
persistentvolumeclaim/my-dynamic-hostpath-pvc created 

现在让我们检查 PV 和 PVC 的状态:

$ kubectl get pod,pvc,pv -n dynamicstorage
NAME                                            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/my-dynamic-hostpath-pvc   Bound    pvc-4597ab27-c894-40de-a7ac-1b6ca961bcdc   1Gi        RWO            standard       <unset>                 7s
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
persistentvolume/pvc-4597ab27-c894-40de-a7ac-1b6ca961bcdc   1Gi        RWO            Delete           Bound    dynamicstorage/my-dynamic-hostpath-pvc   standard       <unset>                          7s 

我们可以看到,PV 已经通过StorageClass创建,并根据请求与 PVC 绑定。

现在,既然这个 PVC 已经创建,我们可以添加一个新的 Pod,它将挂载这个PersistentVolumeClaim对象。以下是一个 Pod 的 YAML 定义文件,它将挂载之前创建的PersistentVolumeClaim对象:

# pod-with-dynamic-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-dynamic-storage
spec:
  containers:
    - image: nginx
      name: nginx
      volumeMounts:
        - mountPath: "/var/www/html"
          name: mypersistentvolume
  volumes:
    - name: mypersistentvolume
      persistentVolumeClaim:
        claimName: my-dynamic-hostpath-pvc 

现在,让我们在正确的命名空间中创建它:

$ kubectl apply -f pod-with-dynamic-pvc.yaml -n dynamicstorage
pod/nginx-dynamic-storage created
$ kubectl get pod,pvc,pv -n dynamicstorage
NAME                        READY   STATUS    RESTARTS   AGE
pod/nginx-dynamic-storage   1/1     Running   0          45s
NAME                                            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/my-dynamic-hostpath-pvc   Bound    pvc-4597ab27-c894-40de-a7ac-1b6ca961bcdc   1Gi        RWO            standard       <unset>                 7m39s
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
persistentvolume/pvc-4597ab27-c894-40de-a7ac-1b6ca961bcdc   1Gi        RWO            Delete           Bound    dynamicstorage/my-dynamic-hostpath-pvc   standard       <unset>                          7m39s 

一切正常!我们终于完成了动态供给!请注意,默认情况下,回收策略将设置为delete,这样当创建它的 PVC 被删除时,PV 也会被删除。如果您需要保留敏感数据,请随时更改回收策略。

您可以通过删除 Pod 和 PVC 来进行测试;PV 将由StorageClass自动删除:

$ kubectl delete po nginx-dynamic-storage -n dynamicstorage
pod "nginx-dynamic-storage" deleted
$ kubectl delete pvc my-dynamic-hostpath-pvc -n dynamicstorage
persistentvolumeclaim "my-dynamic-hostpath-pvc" deleted
$ kubectl get pod,pvc,pv -n dynamicstorage
No resources found 

从上面的代码片段中我们可以看到,当 PVC 被删除时,PV 也会被自动删除。

我们已经覆盖了 PVs、PVCs、StorageClasses的基础知识,以及静态和动态供给之间的区别。在下一部分中,我们将深入探讨 Kubernetes 中的一些高级存储话题,研究如何优化和扩展您的存储策略。

高级存储话题

除了了解 PVs、PVCs 和StorageClasses的基础知识之外,深入探讨一些 Kubernetes 中的高级存储话题也是非常有益的。虽然这不是强制性的,但掌握这些概念能够显著提升你作为 Kubernetes 从业者的专业能力。在接下来的章节中,我们将介绍一些高级话题,如临时存储的短暂卷、灵活的卷管理的 CSI 卷克隆,以及扩展PersistentVolumeClaims以满足增加的存储需求。这些话题将为你提供关于 Kubernetes 存储能力和实际应用的更广泛视角。

Kubernetes 中的临时存储短暂卷

短暂卷提供了一种便捷的方式为 Kubernetes 中的 Pod 提供临时存储。它们非常适合需要临时空间进行缓存或需要只读数据的应用程序,如配置文件或 Secrets。与 PVs 不同,短暂卷在 Pod 终止时会自动删除,从而简化了部署和管理。

以下是临时存储卷的一些关键好处:

  • Pod 的临时存储

  • Pod 终止时自动删除

  • 简化部署和管理

Kubernetes 中有多种类型的临时存储,如下所示:

  • emptyDir:这会在节点的本地存储上创建一个空目录。

  • ConfigMap、downwardAPI、Secret:将 Kubernetes 对象中的数据注入到 Pod 中

  • CSI 临时卷:这些由外部 CSI 驱动程序提供(需要特定驱动程序支持)。

  • 通用临时卷:这些由支持 PV 的存储驱动程序提供。

现在我们已经了解了有关临时卷的一些细节,让我们继续学习 CSI 卷克隆和卷快照的相关知识。

CSI 卷克隆和卷快照

CSI 引入了一个强大的功能:卷克隆。此功能允许您将现有的 PersistentVolumeClaim 完全复制为新的 PVC。

以下 YAML 片段演示了一个典型的 PVC 克隆声明:

# pv-cloning.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cloned-pvc
  namespace: mynamespace
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: custom-storage-class
  resources:
    requests:
      storage: 5Gi
  dataSource:
    kind: PersistentVolumeClaim
    name: original-pvc 

以下是 CSI 卷克隆的一些关键好处:

  • 简化工作流程:CSI 卷克隆自动化数据复制,消除了手动复制的需求,简化了存储管理。

  • 提高效率:轻松创建现有卷的副本,优化部署和资源利用。

  • 故障排除实时数据:您可以复制生产数据并将其用于 QA、故障排除等,而不是直接操作生产数据。

请参阅文档以了解更多关于 CSI 卷克隆的信息:kubernetes.io/docs/concepts/storage/volume-pvc-datasource

与卷克隆类似,Kubernetes 还通过 CSI 驱动程序提供了一种称为卷快照的数据备份机制。VolumeSnapshot 提供了一种标准化的方式来创建卷数据的时间点副本。类似于 PersistentVolumePersistentVolumeClaim 资源,Kubernetes 使用 VolumeSnapshot、VolumeSnapshotContentVolumeSnapshotClass 资源来管理卷快照。VolumeSnapshots 是用户请求的快照,而 VolumeSnapshotContent 表示存储系统中的实际快照。这些资源使用户能够在不创建全新卷的情况下捕获卷的状态,适用于在执行重要更新或删除之前进行数据库备份等场景。与常规 PV 不同,这些快照资源是 自定义资源定义 (CRD) 并且需要支持快照功能的 CSI 驱动程序。CSI 驱动程序使用名为 csi-snapshotter 的 sidecar 容器来处理 CreateSnapshotDeleteSnapshot 操作。

当用户创建快照时,它可以是由管理员预先配置的,或者是从现有 PVC 动态配置的。在这两种情况下,快照控制器都将绑定 VolumeSnapshot 和 VolumeSnapshotContent,确保快照内容与用户请求匹配。根据设置的 DeletionPolicy,快照可以轻松删除或保留,从而提供灵活的数据管理方式。此外,Kubernetes 还提供了将快照的卷模式(例如,从文件系统到块)转换并将数据从快照恢复到新 PVC 的选项。这一功能使得 VolumeSnapshot 成为数据保护中的强大工具,可以通过 CSI 卷克隆与之配合使用,从而创建高效的备份或测试环境,为 Kubernetes 中的存储管理增加更多灵活性。

在 Kubernetes 中,卷克隆非常适合创建PersistentVolumes的相同副本,通常用于开发和测试环境。另一方面,快照捕捉了卷的某一时刻状态,使其在备份和恢复过程中非常有用。

请参考文档(https://kubernetes.io/docs/concepts/storage/volume-snapshots/)了解有关卷快照的更多信息。

在接下来的章节中,我们将学习如何扩展 PVC。

学习如何扩展 PersistentVolumeClaim

Kubernetes 提供了内置的 PVC 扩展支持,可以无缝地增加应用程序的存储容量。此功能目前仅限于由 CSI 驱动程序提供的卷(从 1.29 版本开始,其他类型的卷已被弃用)。

要启用特定 StorageClass 的 PVC 扩展,您需要在 StorageClass 定义中将 allowVolumeExpansion 字段设置为 true。此标志控制引用该 StorageClass 的 PVC 是否可以请求更多的存储空间:

示例存储类配置:

# storageclass-expandable.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: expadable-sc
provisioner: vendor-name.example/magicstorage
...<removed for brevity>...
allowVolumeExpansion: true 

当应用程序需要更多存储时,只需编辑 PVC 对象,并在 resources.requests.storage 字段中指定更大的容量。Kubernetes 将启动扩展过程,调整由 CSI 驱动程序管理的底层卷的大小。这消除了创建新卷和迁移数据的需要,简化了存储管理。

请参考文档(https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims)了解更多信息。

总结

本章已结束,您已经学习了如何在 Kubernetes 上管理持久存储。您发现 PersistentVolume 是一种资源类型,指向底层的资源技术,例如 hostPath 和 NFS,以及基于云的解决方案,如 Amazon EBS 和 Google PD。

此外,您还了解了 PersistentVolumePersistentVolumeClaimstorageClass 之间的关系。您了解到,PersistentVolume 可以拥有不同的回收策略,这使得在其对应的 PersistentVolumeClaim 对象被删除时,可以选择移除、回收或保留它们。

最后,我们了解了什么是动态供应以及它如何帮助我们。请记住,您需要了解此功能,因为如果创建并保留了太多卷,即使可以通过命名空间的资源配额来限制存储使用,它也可能会对月末的云账单产生负面影响。

我们现在已经掌握了 Kubernetes 的基础知识,本章也结束了这一部分。在下一部分,您将会学习 Kubernetes 控制器,它们是设计用来自动化 Kubernetes 中某些任务的对象,例如使用 Deployment 或 StatefulSet 资源类型来维持一定数量的 Pod 副本。还有很多内容需要学习!

进一步阅读

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十章:运行生产级 Kubernetes 工作负载

在前几章中,我们专注于容器化概念和基本的 Kubernetes 构建模块,如 Pods、Jobs 和 ConfigMaps。到目前为止,我们的旅程主要涵盖了单机场景,其中应用程序仅需要一个容器主机或 Kubernetes 节点。对于生产级 Kubernetes,您必须考虑不同的方面,如 可扩展性高可用性(HA)负载均衡,这总是需要在多个主机上进行容器的编排

简而言之,容器编排 是在大型动态环境中管理多个容器生命周期的一种方式——这可以包括部署和维护容器网络的所需状态,提供容器的冗余和高可用性(使用外部组件),扩展集群和容器副本,自动健康检查以及收集遥测数据(日志和指标)。解决云规模下高效容器编排的问题并不直接——这就是 Kubernetes 存在的原因!

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

  • 在 Kubernetes 上确保高可用性和容错性

  • 什么是复制控制器(ReplicationController)?

  • 什么是副本集(ReplicaSet)?

技术要求

对于本章,您将需要以下内容:

  • 已部署 Kubernetes 集群。您可以使用本地或基于云的集群,但为了全面理解概念,我们建议使用多节点、基于云的 Kubernetes 集群。

  • 在本地机器上安装并配置用于管理 Kubernetes 集群的 Kubernetes 命令行界面(CLI)kubectl)。

Kubernetes 集群部署(本地和基于云)以及在 第三章 安装您的第一个 Kubernetes 集群 中介绍的 kubectl 安装。

您可以从官方 GitHub 仓库 github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter10 下载本章的最新代码示例。

在 Kubernetes 上确保高可用性和容错性

首先,让我们快速回顾一下我们如何定义 高可用性容错性(FT) 以及它们之间的区别。这些是云应用中的关键概念,描述了系统或解决方案持续运行的能力,具有期望的长时间。从系统最终用户的角度来看,可用性方面,以及数据一致性,通常是最重要的要求。

高可用性

简而言之,系统工程中的可用性描述的是系统在用户端完全功能性和可操作性的时间百分比。换句话说,它是系统正常运行时间与正常运行时间和停机时间总和(即总时间)的比值。例如,如果在过去的 30 天(720 小时)中,你的云应用程序有 1 小时的计划外维护时间,并且对终端用户不可用,这意味着你的应用程序的可用性指标为。通常,在设计系统时,为了简化这个表示,可用性将以所谓的“九”来表示:例如,如果我们说一个系统有五个九的可用性,意味着它至少在 99.999%的总时间内可用。换句话说,这样的系统每月最多只能有 26 秒的停机时间!这些指标通常是定义服务水平协议SLA)的基础,这些协议适用于计费的云服务。

基于此,高可用性的定义相对直接,尽管不够精确——如果一个系统在长时间内能够不中断地运行(可用),则认为它具有高可用性。通常,我们可以说五个九的可用性是高可用性的黄金标准。

在你的系统中实现高可用性(HA)通常涉及以下一种或多种技术:

  • 消除系统中的单点故障(SPOF)。这通常通过组件冗余来实现。

  • 故障切换设置,这是一种可以自动将当前活跃(可能不健康)的组件切换到冗余组件的机制。

  • 负载均衡,指的是管理流入系统的流量,并将其路由到可以处理流量的冗余组件。这通常会涉及适当的故障切换设置、组件监控和遥测。

让我们介绍与 FT 相关的概念,它在分布式系统中也非常重要,例如运行在 Kubernetes 上的应用程序。

容错

现在,FT 可以作为 HA 概念的补充来展示:如果一个系统在其一个或多个组件发生故障时仍能继续保持功能和运行,那么这个系统就是容错的。例如,像 RAID 这样的 FT 机制用于数据存储,将数据分布在多个磁盘上,或者负载均衡器将流量重定向到健康的节点,通常用于确保系统的韧性并尽量减少中断。实现完整的 FT 意味着实现 100%的 HA,这在许多情况下需要复杂的解决方案,能够主动检测故障并在不间断的情况下修复组件中的问题。根据实现的不同,故障可能会导致性能的平滑降级,降级程度与故障的严重程度成比例。这意味着系统中的小故障对整体性能的影响较小,同时仍然可以响应终端用户的请求。

Kubernetes 应用的 HA 和 FT

在前面的章节中,你了解了 Pods 以及如何通过 Services 将其暴露给外部流量(第八章通过 Services 暴露你的 Pods)。Services 是 Kubernetes 中的对象,它为一组健康的 Pods 提供一个稳定的网络地址。在 Kubernetes 集群内部,Service 通过每个节点上的kube-proxy组件管理的虚拟 IP 地址使 Pods 可以被寻址。在外部,云环境通常使用云负载均衡器来暴露 Service。这个负载均衡器通过cloud-controller-manager组件中的云特定插件与 Kubernetes 集群集成。通过外部负载均衡器,运行在 Kubernetes 上的微服务或工作负载可以在同一节点或不同节点上的健康 Pods 之间实现负载均衡,这是高可用性的一个关键构建模块。

Services 是请求负载均衡到 Pods 所必需的,但我们还没有讨论如何维护可能冗余并分配到不同节点上的同一 Pod 对象定义的多个副本。Kubernetes 提供了多个构建模块来实现这一目标,具体如下:

  • ReplicationController 对象—是 Kubernetes 中定义 Pod 复制的原始形式。

  • ReplicaSet 对象—是 ReplicationController 的继任者。主要的区别是 ReplicaSet 支持基于集合的 Pod 选择器。

管理 ReplicaSets 的首选方式是通过 Deployment 对象,它简化了更新和回滚操作。

  • Deployment 对象—是 ReplicaSet 之上的另一层抽象。它提供对 Pods 和 ReplicaSets 的声明式更新,包括发布和回滚。它用于管理无状态的微服务和工作负载。

  • StatefulSet 对象—类似于 Deployment,但用于管理集群中的有状态微服务和工作负载。在分布式系统设计中,管理集群内的状态通常是最难解决的挑战。

  • DaemonSet 对象—用于在集群的所有(或部分)节点上运行 Pod 的单例副本。这些对象通常用于管理内部 Services,如日志聚合或节点监控。

在接下来的章节中,我们将介绍 ReplicationController 和 ReplicaSets 的基础知识。更高级的对象,如 Deployment、StatefulSet 和 DaemonSet,将在后续章节中讨论。

本章介绍了 Kubernetes 工作负载和应用程序的 HA 和 FT。如果你对如何确保 Kubernetes 本身的 HA 和 FT 感兴趣,请参考官方文档:kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/。请注意,在云端的托管 Kubernetes 服务中,例如 Azure Kubernetes Service (AKS),Amazon Elastic Kubernetes Service (EKS),或 Google Kubernetes Engine (GKE),你将获得高度可用的集群,无需自行管理主节点。

什么是 ReplicationController?

实现高可用性(HA)和故障转移(FT)需要为组件提供冗余,并在组件的副本之间对传入流量进行适当的负载均衡。我们来看一下 Kubernetes 中第一个允许你创建和维护 Pod 副本的对象:ReplicationController。请注意,我们主要讨论 ReplicationController 是出于历史原因,因为它是 Kubernetes 中最初用于创建多个 Pod 副本的方式。我们建议你在可能的情况下使用 ReplicaSet,它基本上是 ReplicationController 的下一代,具有扩展的规格 API。

Kubernetes 中的控制器对象有一个主要目标:观察当前和期望的集群状态,这些状态通过 Kubernetes API 服务器暴露,并指挥变更,试图将当前状态更改为期望的状态。它们作为持续反馈循环,尽最大努力将集群带入由你对象模板描述的期望状态。

ReplicationController 的任务很简单——它需要确保集群中始终有指定数量的 Pod 副本(由模板定义)在运行并处于健康状态。这意味着,如果 ReplicationController 配置为维护给定 Pod 的三个副本,它将尝试通过创建和终止 Pod 来确保始终保持恰好三个 Pod。例如,在你创建 ReplicationController 对象后,它将根据模板定义创建三个新的 Pod。如果由于某种原因集群中有四个这样的 Pod,ReplicationController 会终止一个 Pod;如果某个 Pod 被删除或变得不健康,它将被一个新的、 hopefully 健康的 Pod 替换。

由于现在推荐使用配置了 ReplicaSet 的 Deployment 来管理副本,我们将在此不讨论 ReplicationController。接下来的章节将专注于理解和实践 ReplicaSet 概念。关于 Deployments 的详细探讨将在 第十一章,《使用 Kubernetes 部署无状态工作负载》中进行。

什么是 ReplicaSet?

让我们介绍另一个 Kubernetes 对象:ReplicaSet。它与我们刚刚讨论的 ReplicationController 密切相关。实际上,ReplicaSet 是 ReplicationController 的继任者,它具有非常相似的规格 API 和功能。ReplicaSet 的目的也是相同的——旨在维护一定数量的健康且相同的 Pods(副本),以满足特定条件。因此,你只需为 Pod 指定一个模板,并提供合适的标签选择器和所需的副本数量,Kubernetes ReplicaSetController(这是负责维护 ReplicaSet 对象的控制器的实际名称)将执行必要的操作,确保 Pods 运行。

在我们深入了解 ReplicaSet 之前,让我们在下一节中学习 ReplicationController 和 ReplicaSet 之间的主要区别。

ReplicaSet 与 ReplicationController 有何不同?

ReplicaSet 和 ReplicationController 之间的差异总结如下表所示:

特性 ReplicaSet ReplicationController
标签选择器 支持基于集合的选择器(例如,包含/排除标签)。允许更复杂的逻辑,如包含 environment=testenvironment=dev,同时排除 environment=prod 仅支持基于等式的选择器(例如,key=value)。不支持高级标签匹配。
与其他 Kubernetes 对象的集成 作为更高级对象(如DeploymentHorizontalPodAutoscalerHPA))的基础。 主要直接管理 Pod 的复制,未实现此类集成。
Pod 更新发布 通过 Deployment 对象声明式管理,支持分阶段发布回滚 使用现在已弃用的 kubectl rolling-update 命令进行手动管理。
未来支持 一个更现代且灵活的资源,具备面向未来的功能。 预计未来将被弃用。

表 10.1:ReplicaSet 和 ReplicationController 的区别

底线—始终选择 ReplicaSet 而非 ReplicationController。然而,你也应该记住,单独使用 ReplicaSet 在生产集群中通常没有实际意义,你应该使用更高级的抽象,如 Deployment 对象来管理 ReplicaSet。我们将在下一章介绍这个概念。

在下一节中,我们将学习如何创建和管理 ReplicaSet 对象。

创建 ReplicaSet 对象

在接下来的演示中,我们使用的是一个多节点集群,基于 kind,你已经在第三章《安装你的第一个 Kubernetes 集群》中学过:

$ kind create cluster --config Chapter03/kind_cluster --image kindest/node:v1.31.0
$ kubectl get nodes
NAME                 STATUS   ROLES           AGE   VERSION
kind-control-plane   Ready    control-plane   60s   v1.31.0
kind-worker          Ready    <none>          47s   v1.31.0
kind-worker2         Ready    <none>          47s   v1.31.0
kind-worker3         Ready    <none>          47s   v1.31.0 

首先,让我们创建一个命名空间来存放我们的 ReplicaSet 资源。

$ kubectl create -f ns-rs.yaml
namespace/rs-ns created 

现在,让我们来看一下 nginx-replicaset.yaml 示例 YAML 清单文件的结构,该文件维护三个 nginx Pod 的副本,如下所示:

# nginx-replicaset-example.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset-example
  namespace: rs-ns
spec:
  replicas: 4
  selector:
    matchLabels:
      app: nginx
      environment: test
  template:
    metadata:
      labels:
        app: nginx
        environment: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80 

ReplicaSet 规范有三个主要组件,如下所示:

  • replicas:定义应该使用给定 template 和匹配标签 selector 运行的 Pod 副本数量。可以创建或删除 Pods,以保持所需的副本数量。

  • selector:一个标签选择器,定义如何识别 ReplicaSet 对象拥有或获取的 Pods。再次提醒,与 ReplicationController 的情况类似,请注意,如果现有裸 Pods 与选择器匹配,它们可能会被 ReplicaSet 获取!

  • template:定义 Pod 创建的模板。metadata 中使用的标签必须与 selector 标签查询匹配。

这些概念已经在下图中进行了可视化:

图 10.2 – Kubernetes ReplicaSet

图 10.1:Kubernetes ReplicaSet

如你所见,ReplicaSet 对象使用 .spec.template 来创建 Pods。这些 Pods 必须与 .spec.selector 中配置的标签选择器匹配。请注意,也可能会获取与 ReplicaSet 对象标签匹配的现有裸 Pods。在 图 10.1 所示的情况下,ReplicaSet 对象只创建了两个新的 Pods,而第三个 Pod 是一个被获取的裸 Pod。

在前面的示例中,我们使用了由 spec.selector.matchLabels 指定的简单 基于相等的 选择器。也可以使用 spec.selector.matchExpressions 定义更高级的 基于集合的 选择器——例如,像这样:

# nginx-replicaset-expressions.yaml
...<removed for brevity>...
spec:
  replicas: 4
  selector:
    matchLabels:
      app: nginx
    matchExpressions:
      - key: environment
        operator: In
        values:
          - test
          - dev
  template:
...<removed for brevity>... 

该规范将使 ReplicaSet 仍然只匹配 app=nginxenvironment=testenvironment=dev 的 Pods。

在定义 ReplicaSet 时,.spec.template.metadata.labels 必须与 spec.selector 匹配,否则将被 API 拒绝。

现在,让我们使用 kubectl apply 命令将 ReplicaSet 清单应用到集群中,如下所示:

$ kubectl apply -f nginx-replicaset-example.yaml
replicaset.apps/nginx-replicaset-example created 

你可以立即使用以下命令观察名为 nginx-replicaset-example 的新 ReplicaSet 对象的状态:

$ kubectl describe replicaset/nginx-replicaset-example -n rs-ns
...
Replicas:     4 current / 4 desired
Pods Status:  4 Running / 0 Waiting / 0 Succeeded / 0 Failed
... 

你可以使用 kubectl get pods -n rs-ns 命令观察由 ReplicaSet 对象管理的 Pods。如果你感兴趣,你可以使用 kubectl describe pod <podId> 命令检查 Pods 的标签,并且看到它包含一个 Controlled By: ReplicaSet/nginx-replicaset-example 属性,以标识我们的示例 ReplicaSet 对象。

在使用 kubectl 命令时,你可以使用 rs 缩写来代替输入 replicaset

现在,让我们进入下一部分,测试 ReplicaSet 的行为。

测试 ReplicaSet 的行为

为了展示我们 ReplicaSet 对象的灵活性,现在让我们使用以下 kubectl delete 命令删除一个由 nginx-replicaset-example ReplicaSet 对象拥有的 Pod:

$ kubectl delete po nginx-replicaset-example-6qc9p -n rs-ns 

现在,如果你足够快速,你将能够通过使用 kubectl get pods 命令看到其中一个 Pod 正在终止,并且 ReplicaSet 会立即创建一个新的 Pod,以便匹配目标副本数量!

如果你想查看与我们示例的 ReplicationController 对象相关的事件的更多细节,可以使用 kubectl describe 命令,如下所示:

$ kubectl describe rs/nginx-replicaset-example -n rs-ns
...
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
...<removed for brevity>...
  Normal  SuccessfulCreate  9m9s  replicaset-controller  Created pod: nginx-replicaset-example-r2qfn
  Normal  SuccessfulCreate  3m1s  replicaset-controller  Created pod: nginx-replicaset-example-krdrs 

在这个例子中,nginx-replicaset-example-krdrs Pod 是一个由 ReplicaSet 创建的新 Pod。

现在,让我们尝试不同的操作,创建一个与我们 ReplicaSet 对象的标签选择器匹配的裸 Pod。你可以预期,匹配 ReplicaSet 的 Pod 数量将是四个,因此 ReplicaSet 将终止其中一个 Pod 以将副本数恢复到三个。

小心裸 Pod(没有 ReplicaSet 管理的 Pod)上的标签。ReplicaSets 可以控制任何具有匹配标签的 Pod,可能会无意中管理你的裸 Pod。请为裸 Pod 使用唯一的标签,以避免冲突。

首先,让我们创建一个简单的裸 Pod 清单文件,命名为 nginx-pod-bare.yaml,如下所示:

# nginx-pod-bare.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-bare-example
  namespace: rs-ns
  labels:
    app: nginx
    environment: test
spec:
  containers:
    - name: nginx
      image: nginx:1.17
      ports:
        - containerPort: 80 

Pod 的 metadata 必须有与 ReplicaSet 选择器匹配的标签。现在,使用以下命令将清单应用到你的集群中:

$ kubectl apply -f nginx-pod-bare.yaml
pod/nginx-pod-bare-example created
$ kubectl  get pods
NAME                             READY   STATUS        RESTARTS   AGE
nginx-pod-bare-example           0/1     Terminating   0          1s
nginx-replicaset-example-74kq9   1/1     Running       0          23h
nginx-replicaset-example-qfvx6   1/1     Running       0          23h
nginx-replicaset-example-s5cwc   1/1     Running       0          23h 

紧接着,使用 kubectl describe 命令检查我们示例 ReplicaSet 对象的事件,如下所示:

$ kubectl describe rs/nginx-replicaset-example
...
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
...
  Normal  SuccessfulDelete  29s   replicaset-controller  Deleted pod: nginx-pod-bare-example 

如你所见,ReplicaSet 对象已经立即检测到有一个新 Pod 被创建,并且与其标签选择器匹配,并已终止该 Pod。

同样,通过修改 Pod 的标签,使其不再匹配选择器,可以从 ReplicaSet 对象中移除 Pods。这在各种调试或故障排查场景中非常有用。

在接下来的章节中,我们将学习 ReplicaSet 如何帮助应用程序实现高可用性(HA)和容错(FT)。

使用 ReplicaSet 测试 HA 和 FT

为了演示这个场景,我们将使用之前部署的 ReplicaSet nginx-replicaset-example。不过,我们将创建一个 Service 来暴露这个应用程序,具体如下。

$ kubectl apply -f nginx-service.yaml
service/nginx-service created 

为了测试目的,让我们像下面这样转发 Service:

$ kubectl port-forward svc/nginx-service 8080:80 -n rs-ns
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80 

打开另一个控制台并验证应用程序的访问。

$ curl localhost:8080 

你应该能得到一个默认的 NGINX 页面输出。

之前我们测试了删除 Pod,并验证了 ReplicaSet 会根据副本数重新创建 Pod。现在,让我们移除一个 Kubernetes 节点并观察其行为。在删除节点之前,先检查当前的 Pod 分配情况,如下所示:

$ kubectl get po -n rs-ns -o wide
NAME                             READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
nginx-replicaset-example-cfcfs   1/1     Running   0          24m   10.244.1.3   kind-worker2   <none>           <none>
nginx-replicaset-example-krdrs   1/1     Running   0          17m   10.244.3.5   kind-worker    <none>           <none>
nginx-replicaset-example-kw7cl   1/1     Running   0          24m   10.244.3.4   kind-worker    <none>           <none>
nginx-replicaset-example-r2qfn   1/1     Running   0          24m   10.244.2.4   kind-worker3   <none>           <none> 
kind-worker node; let us remove this node from the Kubernetes cluster as follows:
$ kubectl cordon kind-worker
node/kind-worker cordoned
$ kubectl drain kind-worker --ignore-daemonsets
...<removed for brevity>...
pod/nginx-replicaset-example-kw7cl evicted
node/kind-worker drained
$ kubectl delete node kind-worker
node "kind-worker" deleted 

现在,让我们检查 Pod 的分配情况和副本数量。

$ kubectl get po -n rs-ns -o wide
NAME                             READY   STATUS    RESTARTS   AGE     IP           NODE           NOMINATED NODE   READINESS GATES
nginx-replicaset-example-8lz9x   1/1     Running   0          2m33s   10.244.1.4   kind-worker2   <none>           <none>
nginx-replicaset-example-cfcfs   1/1     Running   0          30m     10.244.1.3   kind-worker2   <none>           <none>
nginx-replicaset-example-d8rz5   1/1     Running   0          2m33s   10.244.2.5   kind-worker3   <none>           <none>
nginx-replicaset-example-r2qfn   1/1     Running   0          30m     10.244.2.4   kind-worker3   <none>           <none> 

根据输出,ReplicaSet 已经在可用节点上创建了所需数量的 Pods。

如果你的 kubectl port-forward 仍然在运行,你可以再次验证应用程序的访问(curl localhost:8080)并确认其可用性。请注意,在生产环境中,最佳做法是集成监控工具,如 PrometheusGrafana,用于实时健康检查和资源可视化,并使用 Fluentd 进行日志记录,捕获 Pod 日志以诊断故障。

现在我们通过多个示例已经了解了 ReplicaSet 的行为,接下来让我们学习如何扩展 ReplicaSet。

扩展 ReplicaSet

对于 ReplicaSet,我们可以执行类似于前一节中 ReplicationController 的扩展操作。通常,在常规场景下,你不会手动扩展 ReplicaSet。而是 ReplicaSet 对象的大小将由另一个更高层的对象(如 Deployment)来管理。

让我们首先扩展我们的示例 ReplicaSet 对象。打开nginx-replicaset.yaml文件,并将replicas属性修改为5,如下所示:

...
spec:
  replicas: 5
... 

现在,我们需要声明性地将更改应用于集群状态。使用以下kubectl apply命令来实现:

$ kubectl apply -f ./nginx-replicaset.yaml 

若要查看 ReplicaSet 对象控制的 Pod 数量是否发生变化,可以使用kubectl get podskubectl describe rs/nginx-replicaset-example命令。

你可以使用kubectl scale rs/nginx-replicaset-example --replicas=5这种命令来达到与 ReplicationController 类似的扩展效果。通常,这种命令只推荐在开发或学习场景中使用。

同样,如果你想进行缩容,你需要打开nginx-replicaset.yaml文件,并将replicas属性修改为2,如下所示:

...
spec:
  replicas: 2
... 

再次声明性地将更改应用于集群状态。使用以下kubectl apply命令来实现:

$ kubectl apply -f ./nginx-replicaset.yaml 

此时,你可以使用kubectl get podskubectl describe rs/nginx-replicaset-example命令来验证 Pods 的数量已减少至2

将 Pod 活性探针与 ReplicaSet 一起使用

有时,即使容器中的主进程未崩溃,你可能仍希望将 Pod 视为不健康并需要重启容器。你已经在第八章《使用服务暴露 Pod》中了解了探针。我们将快速演示如何将活性探针与 ReplicaSet 结合使用,以增强容器化组件故障的容错能力。

在我们的示例中,我们将创建一个 ReplicaSet 对象,运行带有额外活性探针的nginx Pods,该探针会检查主容器的路径/是否能返回成功的 HTTP 状态码。你可以想象,一般情况下,容器中的nginx进程始终会保持健康(直到崩溃),但这并不意味着 Pod 本身可以被认为是健康的。如果 Web 服务器无法成功提供内容,说明 Web 服务器进程运行正常,但可能发生了其他问题,这时 Pod 就不再适用。我们通过简单地删除容器中的/index.html文件来模拟这种情况,这会导致活性探针失败。

首先,让我们为新的nginx-replicaset-livenessprobe-example ReplicaSet 对象创建一个名为nginx-replicaset-livenessprobe.yaml的 YAML 清单文件,内容如下:

# nginx-replicaset-livenessprobe.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset-livenessprobe-example
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      environment: test
  template:
    metadata:
      labels:
        app: nginx
        environment: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80
          **livenessProbe:**
            **httpGet:**
              **path:****/**
              **port:****80**
            **initialDelaySeconds:****2**
            **periodSeconds:****2** 

上述代码块中突出显示的部分包含了存活探针定义,这是与我们之前的 ReplicaSet 示例的唯一区别。存活探针被配置为每 2 秒对容器的 / 路径和 80 端口执行 HTTP GET 请求(periodSeconds)。第一次探测将在容器启动后的 2 秒后(initialDelaySeconds)开始。

如果你正在修改现有的 ReplicaSet 对象,你需要先删除它并重新创建,以便应用对 Pod 模板的更改。

现在,使用以下命令将清单文件应用到集群中:

$ kubectl apply -f ./nginx-replicaset-livenessprobe.yaml 

使用以下命令验证 Pod 是否已成功启动:

$ kubectl get pods 

现在,你需要选择一个 ReplicaSet Pod,以模拟容器内部导致存活探针失败的故障。在我们的示例中,我们将使用列表中的第一个 Pod,并删除 Pod 内的 index.html 文件。为了模拟故障,运行以下命令。该命令将删除由 nginx Web 服务器提供的 index.html 文件,并导致 HTTP GET 请求以非成功的 HTTP 状态码失败:

$ kubectl exec -it nginx-replicaset-livenessprobe-example-lgvqv -- rm /usr/share/nginx/html/index.html 

使用 kubectl describe 命令检查该 Pod 的事件,如下所示:

$ kubectl describe pod/nginx-replicaset-livenessprobe-example-lgvqv
Name:             nginx-replicaset-livenessprobe-example-lgvqv
...<removed for brevitt>...
Events:
  Type     Reason     Age                 From               Message
  ----     ------     ----                ----               -------
  Normal   Scheduled  2m9s                default-scheduler  Successfully assigned default/nginx-replicaset-livenessprobe-example-lgvqv to kind-worker2
  Normal   Pulled     60s (x2 over 2m8s)  kubelet            Container image "nginx:1.17" already present on machine
  Normal   Created    60s (x2 over 2m8s)  kubelet            Created container nginx
  Normal   Started    60s (x2 over 2m8s)  kubelet            Started container nginx
  Warning  Unhealthy  60s (x3 over 64s)   kubelet            Liveness probe failed: HTTP probe failed with statuscode: 403
  Normal   Killing    60s                 kubelet            Container nginx failed liveness probe, will be restarted 

如你所见,存活探针已正确检测到 Web 服务器变得不健康,并重新启动了 Pod 内的容器。

然而,请注意,ReplicaSet 对象本身并未以任何方式参与重启——该操作是在 Pod 层面执行的。这展示了各个 Kubernetes 对象如何提供不同的功能,并能够协同工作以实现更好的故障转移。没有存活探针的话,最终用户可能会被一个无法提供内容的副本服务,而这将无法被发现!

删除 ReplicaSet 对象

最后,让我们来看一下如何删除 ReplicaSet 对象。有两种可能性,概述如下:

  1. 删除 ReplicaSet 对象及其拥有的 Pod——这一过程首先会自动缩容。

  2. 删除 ReplicaSet 对象并保持 Pod 不受影响。

要删除 ReplicaSet 对象及其 Pod,可以使用常规的 kubectl delete 命令,如下所示:

$ kubectl delete rs/nginx-replicaset-livenessprobe-example 

你会看到,Pod 会首先被终止,然后 ReplicaSet 对象会被删除。

现在,如果你只想删除 ReplicaSet 对象,则需要为 kubectl delete 命令使用 --cascade=orphan 选项,如下所示:

$ kubectl delete rs/nginx-replicaset-livenessprobe-example --cascade=orphan 

执行此命令后,如果检查集群中的 Pod,你仍然会看到所有由 nginx-replicaset-livenessprobe-example ReplicaSet 对象拥有的 Pod。这些 Pod 现在可以被其他具有匹配标签选择器的 ReplicaSet 对象获取。

概述

在这一章中,你了解了为在 Kubernetes 集群中运行的应用程序提供高可用性(HA)和容错性(FT)的关键构建块。首先,我们解释了 HA 和 FT 为什么重要。接着,你了解了如何使用 ReplicaSet 提供组件复制和故障转移的更多细节,ReplicaSet 用于 Kubernetes 中提供相同 Pods 的多个副本(replicas)。我们演示了 ReplicationController 和 ReplicaSet 之间的区别,并解释了为什么当前推荐使用 ReplicaSet 来提供多个 Pods 副本。

本书接下来的章节将概述如何使用 Kubernetes 来编排容器应用程序和工作负载。你将熟悉与最重要的 Kubernetes 对象相关的概念,如 Deployment、StatefulSet 和 DaemonSet。此外,在下一章中,我们将重点介绍 ReplicaSet 上的下一个抽象层:Deployment 对象。你将学习如何部署并轻松管理应用程序的新版本的发布和回滚。

进一步阅读

加入我们社区的 Discord

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/cloudanddevops

第十一章:使用 Kubernetes 部署无状态工作负载

前一章介绍了两个重要的 Kubernetes 对象:ReplicationControllerReplicaSet。到此为止,你已经知道它们在保持 Pod 的相同健康副本(拷贝)方面起着类似的作用。实际上,ReplicaSet 是 ReplicationController 的继任者,在 Kubernetes 的最新版本中,应该使用 ReplicaSet 来代替 ReplicationController。

现在,是时候介绍 Deployment 对象了,它为你的无状态 Kubernetes 应用程序和服务提供了轻松的可扩展性、滚动更新和版本回滚。部署对象建立在 ReplicaSets 之上,并且提供了一种声明式的管理方式——只需在部署清单中描述所需的状态,Kubernetes 将负责以受控、可预测的方式协调底层的 ReplicaSets。与 StatefulSet 一起,它是 Kubernetes 中最重要的工作负载管理对象,本章将涵盖 StatefulSet。Deployment 将是你在 Kubernetes 上开发和运维的核心!本章的目标是确保你掌握所有工具和知识,使用 Deployment 对象部署无状态应用组件,并通过 Deployment 的滚动更新安全地发布新版本的组件。

本章将涵盖以下内容:

  • 介绍部署对象

  • Kubernetes 部署如何无缝处理修订和版本发布

  • 部署对象的最佳实践

技术要求

本章需要以下内容:

  • 一个已部署的 Kubernetes 集群。你可以使用本地集群或云集群,但为了更好地理解本章展示的概念,如果有条件,建议使用多节点的云基础 Kubernetes 集群。

  • 必须在本地计算机上安装 Kubernetes 命令行工具(kubectl),并将其配置为管理你的 Kubernetes 集群。

Kubernetes 集群的部署(本地和云端)以及 kubectl 安装已在 第三章安装你的第一个 Kubernetes 集群 中涵盖。

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter11

介绍部署对象

Kubernetes 在运行不同类型的工作负载方面为您提供了开箱即用的灵活性,具体取决于您的使用案例。通过了解哪种工作负载类型适合您的应用需求,您可以做出更明智的决策,优化资源使用,并确保在云端应用程序中获得更好的性能和可靠性。这一基础知识帮助您充分发挥 Kubernetes 的灵活性,使您能够自信地部署和扩展应用程序。让我们简要看看支持的工作负载类型,以了解 Deployment 对象的适用场景及其目的。

以下图示展示了 Kubernetes 中不同类型的应用程序工作负载,我们将在接下来的章节中进行解释。

图 11.1:Kubernetes 中的应用程序工作负载类型

在实施基于云的应用程序时,您通常需要以下类型的工作负载:

  • 无状态:在容器的世界里,无状态应用程序是指那些不在容器内部保存数据(状态)的应用程序。设想两个 Nginx 容器执行相同的任务:一个将用户数据存储在容器内的文件中,而另一个使用像 MongoDB 这样的独立容器进行数据持久化。尽管它们实现了相同的目标,第一个 Nginx 容器因为依赖内部存储而变成了有状态。第二个 Nginx 容器则使用外部数据库,保持无状态。这种无状态的方式使得在 Kubernetes 中管理和扩展应用程序更加简单,因为它们可以轻松重启或替换,而无需担心数据丢失。在 Kubernetes 中,通常使用 Deployment 对象来管理这些无状态工作负载。

  • 有状态:在容器和 Pod 的情况下,如果它们内部存储任何可修改的数据,我们称之为有状态。一个典型的有状态 Pod 示例是 MySQL 或 MongoDB Pod,它将数据读写到持久化存储卷(PersistentVolume)。有状态工作负载的管理难度较大——在发布、回滚和扩展时,您需要仔细管理粘性会话或数据分区。一般来说,如果可能的话,尽量将有状态工作负载保持在 Kubernetes 集群之外,例如使用基于云的软件即服务SaaS)数据库服务。在 Kubernetes 中,StatefulSet 对象用于管理有状态工作负载。第十二章StatefulSet – 部署有状态应用程序,提供了有关这些对象的更多详细信息。

  • Job 或 CronJob:这种类型的工作负载执行作业或任务处理,可以是定时任务或按需执行。根据应用程序的类型,批处理工作负载可能需要成千上万个容器和大量节点——这可以是任何发生在后台的事情。用于批处理的容器也应该是无状态的,以便更容易恢复中断的作业。在 Kubernetes 中,Job 和 CronJob 对象用于管理批处理工作负载。第四章在 Kubernetes 中运行容器,提供了有关这些类型对象的更多详细信息。

  • DaemonSet:在某些情况下,我们希望在每个 Kubernetes 节点上运行工作负载,以支持 Kubernetes 的功能。这可以是监控应用程序、日志应用程序、存储管理代理(用于 PersistentVolumes)等。对于这些工作负载,我们可以使用一种特殊的部署类型,称为 DaemonSet,它保证在集群中的每个节点上都运行该工作负载的一个副本。第十三章DaemonSet - 在节点上维护 Pod 单例,提供了有关这些类型对象的更多详细信息。

通过了解 Kubernetes 中不同类型工作负载的概念,我们可以更深入地探讨如何使用 Deployment 对象管理无状态工作负载。简而言之,它们为 Pods 和 ReplicaSets 提供声明式和可控的更新。

你可以通过使用 Deployment 对象声明性地执行以下操作:

  • 新 ReplicaSet 的发布:部署擅长管理受控发布。你可以在 Deployment 对象中定义一个具有所需 Pod 模板的新 ReplicaSet。Kubernetes 随后通过逐步扩展新的 ReplicaSet 并缩减旧的 ReplicaSet 来进行发布,最小化停机时间,确保平稳过渡。

  • 通过 Pod 模板变更进行受控发布:部署允许你在 Deployment 定义中更新 Pod 模板。当你部署更新后的 Deployment 时,Kubernetes 会执行滚动更新,使用修改后的模板扩展新的 ReplicaSet,同时缩减旧的 ReplicaSet。这使你能够以受控的方式对应用程序的 Pods 进行变更。

  • 回滚到之前的版本:部署会跟踪其修订历史。如果你在新版本中遇到任何问题,可以轻松回滚到先前的稳定部署版本。这使你能够快速恢复更改,最小化中断。

  • 扩展 ReplicaSets:直接扩展 Deployment 将会扩展相关的 ReplicaSet。你可以指定 Deployment 所需的副本数量,Kubernetes 会自动扩展底层的 ReplicaSet 以满足该需求。

  • 暂停和恢复发布:部署提供了暂停新 ReplicaSet 发布的功能,如果你需要解决任何问题或进行额外配置,可以暂停发布。同样,问题解决后,可以恢复发布。这为部署过程提供了灵活性。

通过这种方式,Deployment 对象提供了一个端到端的管道,用于管理运行在 Kubernetes 集群中的无状态组件。通常,你会将它们与 Service 对象结合使用,正如 第八章 中所展示的 通过服务暴露你的 Pods,以实现高容错性、健康监控和智能负载均衡,从而处理进入应用的流量。

现在,让我们更仔细地看看 Deployment 对象规范的结构,以及如何在我们的 Kubernetes 集群中创建一个简单的示例 Deployment。

创建一个 Deployment 对象

不用担心 Deployment 骨架,因为你可以使用任何支持的工具来创建 Deployment YAML。你可以通过以下 kubectl create deployment 命令来创建 Deployment 骨架,该命令基本上会显示包含这些值的 YAML:

$ kubectl create deployment my-deployment --replicas=1 --image=my-image:latest --dry-run=client --port=80 -o yaml 

你可以将输出重定向到一个文件,并将其用作你的部署配置的基础:

$ kubectl create deployment my-deployment --replicas=1 --image=my-image:latest --dry-run=client --port=80 -o yaml >my-deployment.yaml 

首先,让我们看一下一个示例 Deployment YAML 清单文件 nginx-deployment.yaml 的结构,它保持三个 nginx Pod 副本:

# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment-example
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      environment: test
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: nginx
        environment: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80 

如你所见,Deployment 规范的结构与 ReplicaSet 几乎相同,尽管它有一些额外的参数来配置发布新版本的策略。上述的 YAML 规范有四个主要组件:

  • replicas:定义应该使用给定的 template 和匹配的标签 selector 运行的 Pod 副本数量。Pods 可以被创建或删除,以保持所需的数量。此属性由底层的 ReplicaSet 使用。

  • selector:标签选择器,用于定义如何识别底层 ReplicaSet 所拥有的 Pods。可以包括基于集合的选择器和基于相等性的选择器。在 Deployments 的情况下,底层的 ReplicaSet 还会使用生成的 pod-template-hash 标签来确保在滚动发布新版本时,不同子 ReplicaSets 之间没有冲突。此外,这通常可以防止无意间获取裸 Pods,这在简单的 ReplicaSets 中很容易发生。然而,Kubernetes 不会阻止你在不同的 Deployments 或甚至其他类型的控制器之间定义重叠的 Pod 选择器。但如果发生这种情况,它们可能会发生冲突并表现出异常行为。

  • template:定义 Pod 创建的模板。metadata 中使用的标签必须与我们的 selector 匹配。

  • strategy:定义用于用新 Pods 替换现有 Pods 的策略细节。你将在接下来的章节中了解更多关于此类策略的内容。在这个例子中,我们展示了默认的 RollingUpdate 策略。简而言之,这个策略通过慢慢地用新版本的 Pods 替换旧版本的 Pods,从而确保零停机时间,并与 Service 对象和就绪探针一起,为能够处理传入流量的 Pods 提供流量负载均衡。

Deployment 规格提供了高度的可重配置性,以满足您的需求。我们建议参考官方文档以获取所有详细信息:Kubernetes 官方文档(请根据您的 Kubernetes 集群版本选择合适的版本,例如 v1.31)。

为了更好地理解 Deployment、其底层的子 ReplicaSet 以及 Pods 之间的关系,请查看以下图表:

图 11.1 – Kubernetes Deployment

图 11.2:Kubernetes Deployment

一旦定义并创建了 Deployment,就无法更改其 selector。这是因为如果可以更改 selector,您很容易会遇到孤立的 ReplicaSets。您可以对现有 Deployment 对象执行两种重要操作:

  • 修改模板:通常,您可能希望将 Pod 定义更改为应用程序的新镜像版本。这将根据发布 策略 开始进行发布。

  • 修改副本数:仅更改副本数将导致 ReplicaSet 平稳地进行扩展或缩减。

现在,让我们声明性地应用我们的示例 Deployment YAML 清单文件 nginx-deployment.yaml 到集群中,使用 kubectl apply 命令:

$ kubectl apply -f ./nginx-deployment.yaml 

使用 --record 标志对于跟踪对对象所做的更改非常有用,还可以检查哪些命令导致了这些更改。然后,您将看到一个额外的自动注释 kubernetes.io/change-cause,该注释包含有关命令的信息。但是,--record 标志已被弃用,并将在未来移除。因此,如果您依赖该注释,最佳实践是手动将该注释作为 Deployment 更新的一部分进行添加。

如果需要,可以使用 kubectl annotate 命令手动向 Deployment 添加注释,如下所示:

$ kubectl annotate deployment/ nginx-deployment-example kubernetes.io/change-cause='Updated image to 1.2.3' 

在 Deployment 对象创建之后,立即使用 kubectl rollout 命令来实时跟踪 Deployment 的状态:

$ kubectl rollout status deployment nginx-deployment-example
Waiting for deployment "nginx-deployment-example" rollout to finish: 0 of 3 updated replicas are available...
deployment "nginx-deployment-example" successfully rolled out 

这是一个有用的命令,可以帮助我们深入了解正在进行的 Deployment 发布的情况。您还可以使用常见的 kubectl getkubectl describe 命令:

$ kubectl get deploy nginx-deployment-example
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment-example   3/3     3            3           6m21s 

如您所见,Deployment 已成功创建,所有三个 Pods 当前处于就绪状态。

在输入 deployment 时,您可以使用 deploy 缩写来代替,在使用 kubectl 命令时。

您可能还会对查看底层的 ReplicaSets 感兴趣:

$ kubectl get rs
NAME                                  DESIRED   CURRENT   READY   AGE
nginx-deployment-example-5b8dc6b8cd   3         3         3       2m17s 

请注意,我们的 ReplicaSet 名称中生成的哈希值 5b8dc6b8cd,这也是 pod-template-hash 标签的值,如前所述。

最后,您可以使用以下命令查看由 Deployment 对象创建的集群中的 Pods:

$ kubectl get pods
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-deployment-example-5b8dc6b8cd-lj2bz   1/1     Running   0          3m30s
nginx-deployment-example-5b8dc6b8cd-nxkbj   1/1     Running   0          3m30s
nginx-deployment-example-5b8dc6b8cd-shzmd   1/1     Running   0          3m30s 

恭喜—您已经创建并检查了第一个 Kubernetes Deployment!接下来,我们将看看如何使用 Service 对象将 Deployment 暴露给集群外部的流量。

使用 Service 对象暴露 Deployment Pods

Service 对象已在第八章通过服务暴露你的 Pods 中详细介绍,因此在本节中,我们将简要回顾 Service 的作用以及它们通常如何与 Deployments 一起使用。

以下图示可作为 Kubernetes 集群中不同网络的基础参考。

图 11.3:Kubernetes 中的不同网络

服务是 Kubernetes 对象,允许你将 Pods 暴露给集群中的其他 Pods 或终端用户。它们是高可用和容错的 Kubernetes 应用程序的关键构建块,因为它们提供了一个负载均衡层,主动将传入流量路由到就绪和健康的 Pods。

另一方面,Deployment 对象提供 Pod 复制、故障发生时的自动重启、轻松扩展、受控版本发布和回滚。但有一个问题:由 ReplicaSets 或 Deployments 创建的 Pods 有一个有限的生命周期。到某个时刻,你可以预计它们会被终止;然后,将在它们的位置创建具有新 IP 地址的新 Pod 副本。那么,如果你有一个运行 Web 服务器 Pods 的 Deployment,并且这些 Pods 需要与作为另一个 Deployment 一部分创建的 Pods(例如后端 Pods)进行通信怎么办?Web 服务器 Pods 不能假设后端 Pods 的 IP 地址或 DNS 名称,因为它们可能会随着时间的推移而变化。这个问题可以通过 Service 对象来解决,后者为一组 Pods 提供可靠的网络连接。

简而言之,服务针对一组 Pods,这由标签选择器决定。这些标签选择器的工作原理与您在 ReplicaSets 和 Deployments 中学到的相同。最常见的场景是通过使用相同的标签选择器为现有的 Deployment 暴露一个 Service。

Service 负责提供可靠的 DNS 名称和 IP 地址,并监控选择器的结果,更新与之关联的端点对象,包含匹配 Pods 的当前 IP 地址。对于集群内部的通信,通常使用简单的 ClusterIP 服务,而要将其暴露给外部流量,你可以使用 NodePort 服务,或者在云部署中更常见的是使用 LoadBalancer 服务。

要可视化 Service 对象与 Kubernetes 中的 Deployment 对象如何交互,请查看以下图示:

图 11.2 – 客户端 Pod 执行请求到通过 ClusterIP 服务暴露的 Kubernetes 部署

图 11.4:客户端 Pod 执行请求到通过 ClusterIP 服务暴露的 Kubernetes 部署

此图示展示了集群中任何客户端 Pod 如何透明地与通过我们的 Deployment 对象创建并通过 ClusterIP 服务暴露的 nginx Pods 进行通信。ClusterIP 本质上是由每个节点上运行的 kube-proxy 服务管理的虚拟 IP 地址。kube-proxy 负责集群中的所有巧妙路由逻辑,确保路由对客户端 Pods 完全透明——它们无需知道自己是在与同一节点、不同节点,甚至是外部组件进行通信。在后台,kube-proxy 监视服务对象的更新,并维护每个节点上所需的所有路由规则,以确保正确的流量。kube-proxy 通常使用 iptablesIP 虚拟服务器 (IPVS) 来管理流量路由。

服务对象的作用是定义一组应当被 隐藏 在稳定 ClusterIP 后的准备就绪的 Pods。通常,内部客户端不会使用 ClusterIP 调用服务 Pods,而是使用与服务名称相同的 DNS 短名称——例如,nginx-service-example。这将通过集群的内部 DNS 服务解析为 ClusterIP。或者,它们也可以使用 DNS 完全限定域名 (FQDN) 形式的 <serviceName>.<namespaceName>.svc.<clusterDomain>;例如,nginx-service-example.default.svc.cluster.local

对于暴露 Pods 给外部流量的 LoadBalancerNodePort 服务,其原理与内部服务类似;它们也提供用于内部通信的 ClusterIP。不同之处在于,它们还配置了更多组件,以便外部流量能够被路由到集群。

现在您已掌握了有关服务对象及其与 Deployment 对象交互的必要知识,让我们把所学的知识付诸实践!

请参阅 第八章通过服务暴露您的 Pods,了解更多关于服务以及 Kubernetes 中不同类型的服务的信息。

声明式创建服务

在本节中,我们将通过执行以下步骤,使用 LoadBalancer 类型的 nginx-service-example 服务对象暴露我们的 nginx-deployment-example Deployment。

  1. 创建一个 nginx-service.yaml 清单文件,内容如下:

    # nginx-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: nginx-service-example
    spec:
      selector:
        app: nginx
        environment: test
      type: LoadBalancer
      ports:
        - port: 80
          protocol: TCP
          targetPort: 80 
    

服务的标签选择器与我们为 Deployment 对象使用的标签选择器相同。服务的规格指示我们将 Deployment 暴露在云负载均衡器的 80 端口上,然后将来自目标端口 80 的流量路由到底层的 Pods。

根据你的 Kubernetes 集群部署方式,你可能无法使用LoadBalancer类型。在这种情况下,你可能需要为本次练习使用NodePort类型,或者坚持使用简单的ClusterIP类型,并跳过有关外部访问的部分。对于本地开发部署,如minikube,你将需要使用minikube service命令来访问你的服务。你可以在文档中找到更多细节:minikube.sigs.k8s.io/docs/commands/service/

  1. 创建nginx-service-example服务,并使用kubectl getkubectl describe命令获取有关新服务及相关负载均衡器状态的信息:

    $  kubectl apply -f nginx-service.yaml
    service/nginx-service-example created
    $ kubectl describe service nginx-service-example
    Name:                     nginx-service-example
    Namespace:                default
    Labels:                   <none>
    Annotations:              <none>
    Selector:                 app=nginx,environment=test
    ...<removed for brevity>...
    Endpoints:                10.244.1.2:80,10.244.2.2:80,10.244.2.3:80
    Session Affinity:         None
    External Traffic Policy:  Cluster
    Events:                   <none> 
    
  2. 现在,让我们像在第八章中那样,从另一个 Pod 访问该服务,通过服务暴露你的 Pods。我们将按如下方式创建一个k8sutils Pod,并测试服务的访问:

    $ kubectl apply -f ../Chapter07/k8sutils.yaml
    pod/k8sutils created
    $ kubectl exec -it k8sutils -- curl nginx-service-example.default.svc.cluster.local |grep Welcome -A2
    <title>Welcome to nginx!</title>
    <style>
        body {
    --
      <h1>Welcome to nginx!</h1>
    <p>If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.</p> 
    

这展示了如何使用服务将部署的 Pods 暴露给外部流量。现在,我们将快速展示如何使用命令式命令为我们的部署对象创建一个服务,从而实现类似的效果。

命令式创建服务

使用命令式的kubectl expose命令也可以实现类似的效果——一个名为nginx-deployment-example的服务将为我们的部署对象创建。使用以下命令:

$ kubectl expose deployment --type=LoadBalancer nginx-deployment-example
service/nginx-deployment-example exposed 

让我们解释一下前面的代码片段:

  • 这将创建一个与部署对象同名的服务——即nginx-deployment-example。如果你希望使用不同的名称,如声明式示例所示,可以使用--name=nginx-service-example参数。

  • 此外,将由服务使用的端口80将与 Pods 定义的端口相同。如果你希望更改此端口,可以使用--port=<number>--target-port=<number>参数。

查看kubectl expose deployment --help以查看暴露部署时可用的选项。

请注意,这个命令式命令仅推荐在开发或调试场景中使用。对于生产环境,你应尽可能采用声明式的基础设施即代码配置即代码方法。

在接下来的章节中,让我们学习如何在部署中使用就绪探针、存活探针和启动探针。

就绪探针、存活探针和启动探针的作用

第八章通过服务暴露你的 Pods中,我们学到了有三种类型的探针可以为每个运行在 Pod 中的容器配置:

  • 就绪探针

  • 存活探针

  • 启动探针

所有这些探针在你配置部署时都非常有用——总是尝试预测运行在容器中的进程可能的生命周期场景,并相应地为你的部署配置探针。

请注意,默认情况下,Pod 中的容器没有配置任何探针。Kubernetes 会在 Pod 容器启动成功后通过服务提供流量,但只有在容器成功启动时才会提供流量,并且如果容器崩溃,它会使用默认的always-restart策略重启容器。这意味着你需要负责确定在特定情况下需要什么类型的探针和设置。你还需要理解配置错误的探针可能带来的后果和注意事项——例如,如果你的存活探针过于严格,且超时设置过短,可能会错误地重启容器,进而降低应用程序的可用性。

现在,让我们演示如何在你的部署中配置就绪探针,以及它如何在实时中工作。

如果你对其他类型的探针的配置细节感兴趣,可以参考第八章通过服务暴露你的 Pod,以及官方文档:kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/

我们使用的nginx部署非常简单,不需要任何专门的就绪探针。相反,我们将安排容器的设置,以便根据需要让容器的就绪探针失败或成功。具体来说,在容器设置过程中,我们将创建一个名为/usr/share/nginx/html/ready的空文件,nginx将通过/ready端点提供该文件(就像其他任何文件一样),并配置一个httpGet类型的就绪探针,用于查询/ready端点并获取成功的 HTTP 状态码。现在,通过使用kubectl exec命令删除或重新创建ready文件,我们可以轻松模拟导致就绪探针失败或成功的 Pod 故障。

按照以下步骤配置和测试就绪探针:

  1. 使用以下命令删除现有的部署:

    $ kubectl delete deployment nginx-deployment-example 
    
  2. 按如下方式创建一个新的部署 YAML:

    # nginx-deployment-readinessprobe.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment-readiness
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: nginx
          environment: test
      minReadySeconds: 10
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
    ... 
    

现在,我们有如下的spec.template部分:

# nginx-deployment-readinessprobe.yaml
...<continues>...
  template:
    metadata:
      labels:
        app: nginx
        environment: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.4
          ports:
            - containerPort: 80
          command:
            - /bin/sh
            - -c
            - |
              touch /usr/share/nginx/html/ready
              echo "You have been served by Pod with IP address: $(hostname -i)" > /usr/share/nginx/html/index.html
              nginx -g "daemon off;"
          readinessProbe:
            httpGet:
              path: /ready
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 2
            timeoutSeconds: 10
            successThreshold: 1
            failureThreshold: 2 

部署清单中有多个部分发生了变化,所有这些变化都已被突出显示。首先,我们通过command覆盖了默认的容器入口点命令,并传递了额外的参数。command被设置为/bin/sh,用于执行自定义 Shell 命令。额外的参数构造方式如下:

  • -c/bin/sh的一个参数,指示它后续内容是要在 Shell 中执行的命令。

  • touch /usr/share/nginx/html/ready是容器 Shell 中使用的第一个命令。这将创建一个空的ready文件,nginx将通过/ready端点提供该文件。

  • echo "You have been served by Pod with IP address: $(hostname -i)" > /usr/share/nginx/html/index.html是第二个命令,它将index.html的内容设置为关于内部集群 Pod 的 IP 地址信息。hostname -i是用于获取容器 IP 地址的命令。此值在每个运行在我们部署中的 Pod 中会有所不同。

  • nginx -g "daemon off;":最后,我们执行nginx:1.25.4镜像的默认entrypoint命令。该命令将启动nginx Web 服务器,并作为容器中的主进程运行。

通常,你会使用新的容器镜像进行这种自定义,该镜像继承自通用的容器镜像(例如nginx镜像)作为基础,并添加专用的应用程序脚本。这里展示的方法是用于演示目的,并展示了 Kubernetes 运行时的灵活性。有关创建自定义容器镜像的更多信息,请参考 GitHub 仓库中的示例Chapter11/Containerfile

我们在 YAML 清单中所做的第二组更改是针对readinessProbe的定义,其配置如下:

  • 探针的类型是httpGet,并执行一个 HTTP GET 请求,访问容器的/ready HTTP 端点,端口为80

  • initialDelaySeconds:此值设置为5秒,配置探针在容器启动后 5 秒开始查询。

  • periodSeconds:此值设置为2秒,配置探针以 2 秒为间隔进行查询。

  • timeoutSeconds:此值设置为10秒,配置 HTTP GET 请求超时的秒数。

  • successThreshold:此值设置为1,配置探针在失败后需要连续成功查询的最小次数,才会被认为是成功的。

  • failureThreshold:此值设置为2,配置探针在连续失败查询达到最小次数后,才会被认为失败。将其设置为大于1的值可以确保探针不会出现假阳性。

按照以下步骤创建部署:

  1. 使用以下命令将新的 YAML 清单文件应用到集群中:

    $ kubectl apply -f ./nginx-deployment-readinessprobe.yaml 
    
  2. 验证nginx-service-example服务是否显示后端 Pod 的 IP 地址。你可以看到该服务有三个端点,分别映射到我们的部署 Pod,所有端点都准备好为流量提供服务:

    $  kubectl describe svc nginx-service-example
    Name:                     nginx-service-example
    Namespace:                default
    Labels:                   <none>
    Annotations:              <none>
    Selector:                 app=nginx,environment=test
    Type:                     LoadBalancer
    IP Family Policy:         SingleStack
    IP Families:              IPv4
    IP:                       10.96.231.126
    IPs:                      10.96.231.126
    Port:                     <unset>  80/TCP
    TargetPort:               80/TCP
    NodePort:                 <unset>  32563/TCP
    Endpoints:                10.244.1.6:80,10.244.1.7:80,10.244.2.6:80
    Session Affinity:         None
    External Traffic Policy:  Cluster
    Events:                   <none> 
    
  3. 使用本章早些时候创建的k8sutils Pod 测试 nginx Web 服务器访问。你会注意到,响应会在不同的 Pod IP 地址之间循环。这是因为我们的部署配置了三个 Pod 副本。每次你发起请求时,可能会命中不同的 Pod:

    $ kubectl exec -it k8sutils -- curl nginx-service-example.default.svc.cluster.local
    You have been served by Pod with IP address: 10.244.1.7
    Chapter07  $  kubectl exec -it k8sutils -- curl nginx-service-example.default.svc.cluster.local
    You have been served by Pod with IP address: 10.244.2.6 
    
  4. 现在,让我们模拟第一个 Pod 的就绪失败。在我们的案例中,Pod 为nginx-deployment-readiness-69dd4cfdd9-4pkwr,其 IP 地址为10.244.1.7。为了做到这一点,我们只需要使用kubectl exec命令删除容器中的ready文件:

    $ kubectl exec -it nginx-deployment-readiness-69dd4cfdd9-4pkwr -- rm /usr/share/nginx/html/ready 
    
  5. 就绪探针现在开始失败,但不会立即失败!我们已经设置了它,要求至少失败两次,并且每次检查的间隔是 2 秒。稍后你会注意到,只有两个其他仍然就绪的 Pod 为你提供服务。

  6. 现在,如果你描述 nginx-service-example 服务,你会看到它只有两个可用的端点,正如预期的那样:

    $  kubectl describe svc nginx-service-example |grep Endpoint
    Endpoints:                10.244.1.6:80,10.244.2.6:80 
    
  7. 在 Pod 的事件中,你还可以看到它被认为是未就绪的:

    $  kubectl describe pod nginx-deployment-readiness-69dd4cfdd9-4pkwr
    Name:             nginx-deployment-readiness-69dd4cfdd9-4pkwr
    ...<removed for brevity>...
      Normal   Started    21m                  kubelet            Started container nginx
      Warning  Unhealthy  72s (x25 over 118s)  kubelet            Readiness probe failed: HTTP probe failed with statuscode: 404 
    

我们可以进一步推动这一过程。删除其他两个 Pod 中的就绪文件,使整个服务失败:

$ kubectl exec -it nginx-deployment-readiness-69dd4cfdd9-7n2kz -- rm /usr/share/nginx/html/ready
$ kubectl exec -it nginx-deployment-readiness-69dd4cfdd9-t7rp2 -- rm /usr/share/nginx/html/ready
You can see, the Pods are Running but none of them are Ready to serve the webservice due to readinessProbe failure.
$ kubectl get po -w
NAME                                          READY   STATUS    RESTARTS   AGE
k8sutils                                      1/1     Running   0          166m
nginx-deployment-readiness-69dd4cfdd9-4pkwr   0/1     Running   0          25m
nginx-deployment-readiness-69dd4cfdd9-7n2kz   0/1     Running   0          25m
nginx-deployment-readiness-69dd4cfdd9-t7rp2   0/1     Running   0          25m 

现在,当你从 k8sutils Pod 检查服务时,你会看到请求处于挂起状态,最终会因为超时而失败。我们现在处于一种非常糟糕的状态——我们的部署中所有 Pod 副本都未就绪!

$ kubectl exec -it k8sutils -- curl nginx-service-example.default.svc.cluster.local
curl: (7) Failed to connect to nginx-service-example.default.svc.cluster.local port 80 after 5 ms: Couldn't connect to server
command terminated with exit code 7 
  1. 最后,让我们通过重新创建文件使其中一个 Pod 再次就绪。你可以刷新网页,让请求处于挂起状态,并同时执行必要的命令以创建 ready 文件:

    $ kubectl exec -it nginx-deployment-readiness-69dd4cfdd9-4pkwr -- touch /usr/share/nginx/html/ready
    After about 2 seconds (this is the probe interval), the pending request in the web browser should succeed and you will be presented with a nice response from nginx:
    $  kubectl exec -it k8sutils -- curl nginx-service-example.default.svc.cluster.local
    You have been served by Pod with IP address: 10.244.1.7 
    

恭喜你——你已成功配置并测试了你的部署 Pod 的就绪探针!这应该能让你深入了解探针是如何工作的,以及如何将它们与暴露你部署的服务一起使用。

接下来,我们将简要了解如何扩展你的部署。

扩展部署对象

部署的优点在于,你可以根据需求几乎即时地扩展或缩减它们。当部署通过服务暴露时,新 Pods 会在扩展时自动作为新端点被发现,或者在缩减时自动从端点列表中移除。操作步骤如下:

  1. 首先,让我们声明式地扩展我们的部署。打开 nginx-deployment-readinessprobe.yaml 清单文件并修改副本数量:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment-readiness
    spec:
      replicas: 10
    ... 
    
  2. 使用 kubectl apply 命令将这些更改应用到集群中:

    $ kubectl apply -f ./nginx-deployment-readinessprobe.yaml
    deployment.apps/nginx-deployment-readiness configured 
    
  3. 现在,如果你使用 kubectl get pods 命令检查 Pod,你会立即看到正在创建新的 Pod。类似地,如果你检查 kubectl describe 命令的输出,你会看到以下事件:

    $ kubectl describe deployments.apps nginx-deployment-readiness
    Name:                   nginx-deployment-readiness
    ...<removed fro brevity>...
    Events:
      Type    Reason             Age   From                   Message
      ----    ------             ----  ----                   -------
      Normal  ScalingReplicaSet  32m   deployment-controller  Scaled up replica set nginx-deployment-readiness-69dd4cfdd9 to 3
      Normal  ScalingReplicaSet  9s    deployment-controller  Scaled up replica set nginx-deployment-readiness-69dd4cfdd9 to 10 from 3 
    

你也可以使用 imperative 命令达到相同的效果,这仅推荐用于开发场景:

$ kubectl scale deploy nginx-deployment-readiness --replicas=10
deployment.apps/nginx-deployment-readiness scaled 
  1. 要声明式地缩小我们的部署,只需修改 nginx-deployment-readinessprobe.yaml 清单文件并更改副本数量:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment-readiness
    spec:
      replicas: 2
    ... 
    
  2. 使用 kubectl apply 命令将更改应用到集群中:

    $ kubectl apply -f ./nginx-deployment-readinessprobe.yaml 
    
  3. 你也可以通过命令式方式达到相同的效果。例如,你可以执行以下命令:

    $ kubectl scale deploy nginx-deployment-readiness --replicas=2 
    

如果你描述该部署,你会看到这个缩小操作在事件中有所反映:

$ kubectl describe deploy nginx-deployment-readiness
...
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
Normal  ScalingReplicaSet  27s    deployment-controller  Scaled down replica set nginx-deployment-readiness-69dd4cfdd9 to 2 from 10 

部署事件非常有用,如果你想了解扩展的确切时间线以及可以在部署对象上执行的其他操作。

你可以使用 HorizontalPodAutoscaler 自动扩展你的 Deployments。这将在第二十章自动扩展 Kubernetes Pods 和节点中讲解。

接下来,你将学习如何从集群中删除一个 Deployment。

删除 Deployment 对象

要删除 Deployment 对象,你可以做两件事:

  • 删除 Deployment 对象以及它所拥有的 Pods。这可以通过首先自动缩减来完成。

  • 删除 Deployment 对象,同时保持其他 Pods 不受影响。

要删除 Deployment 对象及其 Pods,你可以使用常规的 kubectl delete 命令:

$ kubectl delete deploy nginx-deployment-readiness 

你会看到 Pods 被终止,Deployment 对象也随之被删除。

现在,如果你只想删除 Deployment 对象,你需要在 kubectl delete 命令中使用 --cascade=orphan 选项:

$ kubectl delete deploy nginx-deployment-readiness --cascade=orphan 

执行此命令后,如果你检查集群中的 Pods,你仍然会看到所有由 nginx-deployment-example Deployment 所拥有的 Pods。

在接下来的部分,我们将探索如何使用 Deployment 管理不同的修订和发布。

Kubernetes Deployments 如何无缝处理修订和版本发布

到目前为止,我们只讨论了对一个活跃的 Deployment 进行的单一修改——我们通过修改规范中的 replicas 参数来进行扩缩容。但这并不是我们能做的所有操作!你还可以在规范中修改 Deployment 的 Pod 模板(.spec.template),通过这种方式触发发布。这个发布可能是由于简单的更改,比如更改 Pods 的标签,但当 Pod 定义中的容器镜像更换为不同版本时,这也可能是一个更复杂的操作。这是最常见的场景,因为它使你作为 Kubernetes 集群操作员,能够进行可控、可预测的新版镜像发布,并有效地创建 Deployment 的新修订。

你的 Deployment 使用了发布策略,这可以通过 YAML 清单中的 .spec.strategy.type 来指定。Kubernetes 默认支持两种策略:

  • RollingUpdate:这是默认的发布策略,允许你以可控的方式发布应用程序的新版本。此策略内部使用两个 ReplicaSets。当你在 Deployment 规范中进行导致发布的更改时,Kubernetes 会创建一个新的 ReplicaSet,并将新的 Pod 模板初始扩容为零个 Pods。此时,旧的 ReplicaSet 将保持不变。接下来,旧的 ReplicaSet 将逐步缩减,而新的 ReplicaSet 将同时逐步扩容。通过 .spec.strategy.rollingUpdate.maxUnavailable 参数,可以控制在发布过程中可能出现的不可用 Pods(就绪探针失败的 Pods)数量。

可以调度的额外 Pod 数量(超出目标数量的 Pod)由.spec.strategy.rollingUpdate.maxSurge参数控制。此外,这种策略提供了自动修订历史记录,可用于在发生故障时进行快速回滚。

  • Recreate:这是一种简单的策略,适用于开发场景,其中所有旧的 Pod 都已终止,并被新的 Pod 替换。它会立即删除任何现有的底层 ReplicaSet,并用新的 ReplicaSet 替换。除非有特定的用例,否则不应将此策略用于生产工作负载。

将部署策略视为更高级部署场景的基本构建块。例如,如果你有兴趣进行蓝绿部署,可以通过使用部署和服务的组合,并操控标签选择器,轻松在 Kubernetes 中实现。你可以在 Kubernetes 官方博客文章中了解更多内容:kubernetes.io/blog/2018/04/30/zero-downtime-deployment-kubernetes-jenkins/

现在,我们将使用RollingUpdate策略执行一次滚动更新。Recreate策略更简单,类似地也可以使用。

更新一个部署对象

本节将通过实际例子来探讨RollingUpdate策略。首先,让我们重新创建之前用于演示就绪探针的部署:

  1. 复制前一个 YAML 清单文件:

    $ cp nginx-deployment-readinessprobe.yaml nginx-deployment-rollingupdate.yaml 
    
  2. 确保你已经设置了RollingUpdate类型的策略,名为readinessProbe,并使用了nginx:1.17的镜像版本。如果你完成了之前的章节,这些内容应该已经在nginx-deployment-readinessprobe.yaml清单文件中设置好了:

    # nginx-deployment-rollingupdate.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment-rollingupdate
    spec:
      replicas: 3
    ...
      minReadySeconds: 10
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
      template:
    ...
        spec:
          containers:
            - name: nginx
              image: nginx:1.17
    ...
              readinessProbe:
                httpGet:
    ... 
    
  3. 在这个例子中,我们使用maxUnavailable设置为1,这意味着我们允许三个 Pod 中只有一个(即目标数量)不可用(未准备好)。这意味着,在任何时候,必须有至少两个 Pod 准备好提供流量。

  4. 类似地,将maxSurge设置为1意味着我们允许在滚动更新期间创建一个额外的 Pod,使其数量超过目标的三个 Pod。实际效果是,在滚动更新期间,集群中最多可以有四个 Pod(无论是否准备好)。请注意,这些参数也可以设置为百分比值(例如25%),这在自动伸缩场景中非常有用。

  5. 此外,minReadySeconds(设置为10)为 Pod 提供了额外的时间段,只有 Pod 准备好后才能在滚动更新期间被宣布为成功。

  6. 将清单文件应用到集群中:

    $ kubectl apply -f nginx-deployment-rollingupdate.yaml
    deployment.apps/nginx-deployment-rollingupdate created
    $ kubectl get pod
    NAME                                              READY   STATUS    RESTARTS   AGE
    nginx-deployment-rollingupdate-69d855cf4b-nshn2   1/1     Running   0          24s
    nginx-deployment-rollingupdate-69d855cf4b-pqvjh   1/1     Running   0          24s
    nginx-deployment-rollingupdate-69d855cf4b-tdxzl   1/1     Running   0          24s 
    

部署在集群中准备好后,我们可以开始发布应用程序的新版本。我们将更改部署 Pod 模板中的镜像为更新版本,并观察在发布过程中发生了什么。按照以下步骤操作:

  1. 将部署中使用的容器镜像修改为nginx:1.18

    ...
      template:
        metadata:
          labels:
            app: nginx
            environment: test
        spec:
          containers:
            - name: nginx
              **image:****nginx:1.18**
    ... 
    
  2. 使用以下命令将更改应用到集群中:

    $  kubectl apply -f nginx-deployment-rollingupdate.yaml
    deployment.apps/nginx-deployment-rollingupdate configured 
    
  3. 紧接着,使用kubectl rollout status命令查看实时进度:

    $ kubectl rollout status deployment.apps/nginx-deployment-rollingupdate
    deployment "nginx-deployment-rollingupdate" successfully rolled out 
    
  4. 部署过程需要一些时间,因为我们在部署规格中配置了minReadySeconds,并在 Pod 容器就绪探针中配置了initialDelaySeconds

  5. 同样,使用kubectl describe命令,你可以查看有关部署的事件,这些事件会告诉我们 ReplicaSets 是如何被扩展和缩减的:

    $ kubectl describe deploy nginx-deployment-rollingupdate
    Name:                   nginx-deployment-rollingupdate
    ...<removed for brevity>...
    Events:
      Type    Reason             Age    From                   Message
      ----    ------             ----   ----                   -------
      Normal  ScalingReplicaSet  8m21s  deployment-controller  Scaled up replica set nginx-deployment-rollingupdate-69d855cf4b to 3
      Normal  ScalingReplicaSet  2m51s  deployment-controller  Scaled up replica set nginx-deployment-rollingupdate-5479f5d87f to 1
      Normal  ScalingReplicaSet  2m51s  deployment-controller  Scaled down replica set nginx-deployment-rollingupdate-69d855cf4b to 2 from 3
      Normal  ScalingReplicaSet  2m51s  deployment-controller  Scaled up replica set nginx-deployment-rollingupdate-5479f5d87f to 2 from 1
      Normal  ScalingReplicaSet  2m24s  deployment-controller  Scaled down replica set nginx-deployment-rollingupdate-69d855cf4b to 1 from 2
      Normal  ScalingReplicaSet  2m24s  deployment-controller  Scaled up replica set nginx-deployment-rollingupdate-5479f5d87f to 3 from 2
      Normal  ScalingReplicaSet  2m14s  deployment-controller  Scaled down replica set nginx-deployment-rollingupdate-69d855cf4b to 0 from 1 
    
  6. 现在,让我们查看集群中的 ReplicaSets:

    $ kubectl get rs
    NAME                                        DESIRED   CURRENT   READY   AGE
    nginx-deployment-rollingupdate-5479f5d87f   3         3         3       4m22s
    nginx-deployment-rollingupdate-69d855cf4b   0         0         0       9m52s 
    

你将在这里看到一些有趣的事情:旧的 ReplicaSet 仍然存在于集群中,但已被缩减为零个 Pod!其原因是我们保留了部署的修订历史——每个修订都有一个匹配的 ReplicaSet,可以在需要回滚时使用。每个部署保留的修订历史的数量由.spec.revisionHistoryLimit参数控制——默认情况下,设置为10。修订历史非常重要,尤其是当你对部署进行命令式更改时。如果你使用声明式模型,并且总是将更改提交到源代码仓库,那么修订历史可能不那么重要。

  1. 最后,我们可以检查 Pods 是否确实已更新为新的镜像版本。使用以下命令并在输出中验证其中一个 Pod:

    $ kubectl get po
    NAME                                              READY   STATUS    RESTARTS   AGE
    nginx-deployment-rollingupdate-5479f5d87f-2k7d6   1/1     Running   0          5m41s
    nginx-deployment-rollingupdate-5479f5d87f-6gn9m   1/1     Running   0          5m14s
    nginx-deployment-rollingupdate-5479f5d87f-mft6b   1/1     Running   0          5m41s
    $ kubectl describe pod nginx-deployment-rollingupdate-5479f5d87f-2k7d6|grep 'Image:'
        Image:         nginx:1.1 
    

这表明我们确实已经进行了新的nginx容器镜像版本的部署!

你可以通过kubectl set image deployment nginx-deployment-example nginx=nginx:1.18命令以命令式方式更改部署容器镜像。这种方法仅建议在非生产环境中使用,并且与命令式回滚配合使用效果良好。

接下来,你将学习如何回滚一个部署对象。

回滚部署对象

如果你使用声明式模型向 Kubernetes 集群引入更改,并将每个更改提交到源代码仓库,执行回滚非常简单,只需恢复提交并重新应用配置即可。通常,应用更改的过程作为源代码仓库的 CI/CD 管道的一部分执行,而不是由操作员(如应用团队或管理员)手动应用更改。这是管理部署的最简单方式,通常建议在基础设施即代码(Infrastructure-as-Code)和配置即代码(Configuration-as-Code)范式中使用。

一个使用声明式模型的强大示例是 Flux (fluxcd.io/)。Flux 最初由 Cloud Native Computing Foundation (CNCF) 孵化,后来已毕业并成为 CNCF 生态系统中的一个成熟项目。Flux 是 GitOps 方法的核心,GitOps 是实现云原生应用持续部署的一种方法论。它通过利用 Git 和持续部署管道等熟悉的工具,优先考虑开发者中心的体验,从而简化了基础设施管理。

然而,Kubernetes 仍然提供了一种通过修订历史回滚 Deployment 的命令式方法。即使是通过声明式更新的 Deployments,也可以执行命令式回滚。现在,我们将演示如何使用 kubectl 进行回滚。请按照以下步骤操作:

  1. 首先,我们命令式地发布另一个版本的 Deployment。这次,我们将 nginx 镜像更新为版本 1.19

    $ kubectl set image deployment nginx-deployment-rollingupdate nginx=nginx:1.19
    deployment.apps/nginx-deployment-rollingupdate image updated 
    
  2. 请注意,nginx=nginx:1.19 表示我们正在为名为 nginx 的容器设置 nginx:1.19 镜像,该容器位于 nginx-deployment-rollingupdate Deployment 中。

  3. 使用 kubectl rollout status,等待 Deployment 完成:

    $ kubectl rollout status deployment.apps/nginx-deployment-rollingupdate
    deployment "nginx-deployment-rollingupdate" successfully rolled out 
    
  4. 现在,假设新版本的应用镜像 1.19 造成了一些问题,你的团队决定回滚到之前的版本,这个版本运行良好。

  5. 使用以下 kubectl rollout history 命令查看 Deployment 可用的所有修订:

    $ kubectl rollout history deployment.apps/nginx-deployment-rollingupdate
    deployment.apps/nginx-deployment-rollingupdate
    REVISION  CHANGE-CAUSE
    1         <none>
    2         <none>
    3         <none> 
    
  6. 如你所见,我们有三个修订版本。第一个修订是我们最初创建的 Deployment。第二个修订是将 Deployment 声明式更新为 nginx:1.18 镜像。最后,第三个修订是我们最后一次的命令式更新,将 nginx:1.19 镜像发布。这里 CHANGE-CAUSE 为空,因为我们没有使用 --record 标志,--record 标志已被弃用,并将在未来的版本中删除。如果你有更新 CHANGE-CAUSE 的需求,你需要手动更新 Deployment 注解,正如我们在本章前面学到的那样。

  7. 作为声明式更改创建的修订中,CHANGE-CAUSE 并未包含太多信息。若要了解第二个修订版本的更多信息,你可以使用以下命令:

    $ kubectl rollout history deploy nginx-deployment-rollingupdate --revision=2
    deployment.apps/nginx-deployment-rollingupdate with revision #2
    Pod Template:
    ...<removed for brevity>...
      Containers:
       nginx:
        Image:      nginx:1.18
    ...<removed for brevity>... 
    
  8. 现在,让我们回滚到这个修订版本。由于它是上一个修订,你可以简单地执行以下命令:

    $ kubectl rollout undo deploy nginx-deployment-rollingupdate
    deployment.apps/nginx-deployment-rollingupdate rolled back 
    
  9. 这相当于执行回滚到特定的修订版本:

    $ kubectl rollout undo deploy nginx-deployment-rollingupdate --to-revision=2 
    
  10. 再次,和正常的发布一样,你可以使用以下命令跟踪回滚过程:

    $ kubectl rollout status deploy nginx-deployment-rollingupdate
    $ kubectl rollout history deployment.apps/nginx-deployment-rollingupdate
    deployment.apps/nginx-deployment-rollingupdate
    REVISION  CHANGE-CAUSE
    1         <none>
    3         <none>
    4         <none> 
    
  11. 你会注意到版本 2 缺失,并且创建了一个新的部署版本。

请注意,你还可以在当前正在进行的发布中执行回滚。回滚可以以声明式和命令式两种方式进行。

如果你需要暂停并恢复正在进行的 Deployment 部署,可以使用 kubectl rollout pause deployment nginx-deployment-examplekubectl rollout resume deployment nginx-deployment-example 命令。

恭喜 – 你已经成功回滚了你的 Deployment。在下一部分,我们将为你提供一套最佳实践,帮助你管理 Kubernetes 中的 Deployment 对象。

Canary 部署策略

Canary 部署为应用程序的发布提供了一种宝贵的风险最小化策略。它们的灵感来自于将金丝雀(鸟类)带入煤矿中以检测危险气体的做法。类似地,canary 部署将应用程序的新版本首先引入一小部分用户中,然后再向整个生产环境开放。

这种受控发布允许在现实环境中测试新版本,同时最小化潜在的中断。以下是它的工作原理:假设你正在为电子商务网站部署一个更新。传统方法下,整个网站会同时切换到新版本。而使用 canary 部署时,可以创建两个部署:一个运行当前版本、为大多数用户提供服务的稳定部署,以及一个运行新版本、为少部分用户提供服务的 canary 部署。流量路由机制,如 ingress 控制器或服务注释,然后将特定比例的流量(例如 10%)路由到 canary 部署。

通过密切监控 canary 部署的性能,利用错误率、响应时间和用户反馈等指标,你可以评估新版本的稳定性。如果一切顺利,你可以逐步增加指向 canary 部署的流量比例,直到它为所有用户提供服务。相反,如果出现问题,你可以轻松回滚 canary 部署,并保持稳定版本,从而防止对用户群体产生更广泛的影响。

参考这个 YAML 示例,展示了前端应用程序的 canary 部署:我们有一个稳定版本的 app 部署配置,其中 image: frontend-app:1.0 如下所示。

# Stable app
  ...
  name: frontend-stable
  replicas: 3
  ...
  labels:
    app: myapp
    tier: frontend
    version: stable
  ...
  image: frontend-app:1.0 

现在我们有一个相同应用程序的 canary 部署配置,其中 image: frontend-app:2.0 如下所示。

# Canary version
  ...
  name: frontend-canary
  replicas: 1
  ...
  labels:
    app: myapp
    tier: frontend
    version: canary
  ...
  image: frontend-app:2.0 

我们需要一个 Service 来将流量路由到 Deployment 的两个版本。请注意,我们只使用了 app: myapptier: frontend 标签作为选择器,这样 Service 就会同时使用 stableCanary Pods 来处理流量:

# Service (routes traffic to both deployments)
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    app: myapp
    tier: frontend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080 

通过调整每个 Deployment 中的副本数量,你可以控制指向每个版本的用户比例。

另一种方式是为稳定版本和 canary 版本创建不同的 Service 对象,并使用如 ingress 控制器或服务注释等机制,将特定比例的流量(例如 10%)路由到 canary 部署。

关键部分是监控金丝雀部署,并确保新版本按预期工作。根据结果,你可以将金丝雀部署推广为稳定部署,或删除金丝雀并在稳定部署中更新新的应用镜像(例如,image: frontend-app:2.0)。

请参考文档(kubernetes.io/docs/concepts/workloads/management/#canary-deployments)了解更多关于金丝雀部署的内容。

现在,让我们进入下一部分,讨论如何处理部署对象的最佳实践。

部署对象最佳实践

本节将总结在 Kubernetes 中使用部署对象的最佳实践。这个列表并不完整,但它是你开始使用 Kubernetes 的一个良好起点。

使用声明式对象管理进行部署

在 DevOps 和容器化应用的世界中,更新基础设施和应用时,坚持使用声明式模型是一个好习惯。这是 基础设施即代码配置即代码 范式的核心。在 Kubernetes 中,你可以轻松地使用 kubectl apply 命令进行声明式更新,该命令可以用于单个文件,甚至整个目录的 YAML 清单文件。

删除对象时,最好还是使用命令式的方式。这更可预测且不容易出错。在 CI/CD 和 GitOps 场景中,声明式删除集群资源通常更有用,因为整个过程是完全自动化的。

同样的原则也适用于部署对象。当你的 YAML 清单文件已经版本化并保存在源代码控制仓库中时,执行发布或回滚是简单且可预测的。在生产环境中,通常不建议使用 kubectl rollout undokubectl set image deployment 命令。由于多人共同在集群中进行操作时,这些命令会变得更加复杂。

不要在生产工作负载中使用 Recreate 策略

使用 Recreate 策略可能会让人觉得很有诱惑力,因为它能为你的部署提供即时更新。然而,这同时也意味着你的最终用户会经历停机时间。这是因为所有旧版本部署的现有 Pod 会被一次性终止,并用新的 Pod 替换。新的 Pod 变为就绪状态可能会有显著的延迟,这意味着停机时间。通过在生产场景中使用 RollingUpdate 策略,可以轻松避免这种停机时间。

Kubernetes 中的 Recreate 部署策略最适用于可以接受停机时间的特定场景,并且相较于其他策略有一些优势。例如,在部署重大应用变更或引入全新版本时,Recreate 策略可以提供一个干净的环境,确保新版本与之前的版本独立运行。

不要创建与现有部署标签选择器匹配的 Pods

可以创建与某个现有部署的标签选择器匹配的 Pods。这可以通过裸 Pods 或其他部署或副本集来完成。这可能会导致冲突,而 Kubernetes 不会阻止这种情况,且可能使现有的部署误认为它已经创建了其他 Pods。结果可能不可预测,通常情况下,你需要注意如何标记集群中的资源。我们建议你在这里使用语义化标签,更多信息可以参考官方文档:kubernetes.io/docs/concepts/configuration/overview/#using-labels

仔细配置你的容器探针

Pod 容器的存活、就绪和启动探针可以提供很多好处,但如果配置错误,也可能导致宕机,包括级联故障。你应始终确保理解每个探针进入失败状态的后果,以及它如何影响其他 Kubernetes 资源,如 Service 对象。

有一些已建立的就绪探针最佳实践,你应该考虑以下几点:

  • 当容器启动后可能无法立即准备好处理流量时,请使用此探针。

  • 在就绪探针评估期间,确保检查诸如缓存预热或数据库迁移状态等事项。你也可以考虑启动实际的预热过程(如果尚未开始),但要谨慎使用这种方法——就绪探针会在 Pod 生命周期内持续执行,这意味着你不应该在每次请求时执行任何昂贵的操作。另一个选择是,你可以为此目的使用启动探针。

  • 对于暴露 HTTP 端点的微服务应用,考虑配置 httpGet 就绪探针。这将确保在容器成功运行但 HTTP 服务器尚未完全初始化时,所有基础工作都已覆盖。

  • 为你的应用使用一个独立的 HTTP 端点进行就绪检查是个好主意。例如,一个常见的约定是使用 /health

  • 如果你在此类探针中检查依赖项的状态(外部数据库、日志服务等),请注意共享依赖项,例如数据库。在这种情况下,你应考虑使用探针超时,该超时应大于外部依赖项的最大允许超时——否则,你可能会遇到级联故障和更低的可用性,而不是偶尔增加的延迟。

与就绪探针类似,关于何时以及如何使用存活探针有一些指导原则:

  • 存活探针应该谨慎使用。错误地配置这些探针可能会导致服务的级联故障和容器重启循环。

  • 除非有充分的理由,否则不要使用存活探针。一个充分的理由可能是,如果你的应用程序中存在已知的死锁问题,但根本原因尚不明确。

  • 执行简单且快速的检查来确定进程的状态,而不是其依赖项。换句话说,你不希望在存活探针中检查外部依赖项的状态,因为这可能会导致级联故障,进而引发容器重启的雪崩效应,并使一小部分服务 Pod 过载。

  • 如果容器中运行的进程在遇到无法恢复的错误时可能崩溃或退出,你可能根本不需要存活探针。

  • initialDelaySeconds使用保守的设置,以避免任何过早的容器重启并防止进入重启循环。

这些是有关 Pod 探针的最重要事项。接下来,让我们讨论如何标记你的容器镜像。

使用有意义且符合语义的镜像标签

管理部署回滚和检查发布历史记录需要我们对容器镜像使用良好的标签。如果你依赖于latest标签,执行回滚将变得不可能,因为这个标签会随着时间的推移指向不同版本的镜像。使用语义版本控制来标记你的容器镜像是一种良好的做法。此外,你可以考虑使用源代码哈希(例如 Git 提交哈希)来标记镜像,以确保你可以轻松追踪 Kubernetes 集群中运行的内容。

从 Kubernetes 的旧版本迁移

如果你正在处理在旧版本的 Kubernetes 上开发的工作负载,你可能会注意到,从 Kubernetes 1.16 开始,你无法将部署应用到集群中,因为出现以下错误:

error: unable to recognize "deployment": no matches for kind "Deployment" in version "extensions/v1beta1" 

之所以这样,是因为在 1.16 版本中,Deployment 对象已从 extensions/v1beta1 API 组中移除,符合 API 版本控制政策。你应该改为使用 apps/v1 API 组,自 1.9 版本起,Deployment 就属于该组。这也展示了在使用 Kubernetes 时需要遵循的一条重要规则:始终遵循 API 版本控制政策,并在迁移到 Kubernetes 新版本时尽量将资源升级到最新的 API 组。这可以避免在资源最终被弃用时遇到令人不悦的意外。

在 Deployment 中包含资源管理

为你的 Deployment 中的容器定义资源请求(最小保证资源)和资源限制(最大允许资源)。这有助于 Kubernetes 调度器高效地分配资源并防止资源不足。此外,切勿高估或低估资源需求。分析应用程序行为以确定适当的资源请求和限制,避免资源浪费或性能瓶颈。

扩展和副本管理

使用 HorizontalPodAutoscaler (HPA) 根据预定义的度量标准(如 CPU 或内存使用情况)自动扩展或缩减你的 Deployment 副本。

以下图片展示了 HPA 如何处理所需的副本数:

图 11.5:HPA 基于度量信息处理 Deployment

同时,最佳实践是根据预期工作负载和资源可用性等因素设置 Deployment 的初始副本数(Pod 实例数量)。

安全性考虑

实现 Deployment 策略,禁止以不必要的权限(例如 root 用户)运行容器。这可以减少攻击面和潜在的安全漏洞。此外,记得将敏感信息(如密码或配置详情)存储在 Secrets 和 ConfigMaps 中,而不是直接嵌入到 Deployments 中。参考第十八章Kubernetes 安全性,以探索 Kubernetes 的不同安全方面。

概述

在本章中,你学会了如何在 Kubernetes 上使用 Deployment 对象处理无状态工作负载和应用程序。首先,你创建了一个示例 Deployment,并使用 LoadBalancer 类型的 Service 对象暴露其 Pods 以供外部流量访问。接下来,你了解了如何在集群中扩展和管理 Deployment 对象。我们涵盖的管理操作包括推出新的 Deployment 版本,以及在失败时回滚到早期版本。最后,我们为你提供了一套在使用 Deployment 对象时的已知最佳实践。

下一章将通过介绍管理有状态工作负载和应用程序的细节来扩展这些知识。与此同时,我们将介绍一个新的 Kubernetes 对象:StatefulSet。

进一步阅读

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者一起讨论:

packt.link/cloudanddevops

第十二章:StatefulSet – 部署有状态应用程序

在上一章中,我们解释了如何使用 Kubernetes 集群运行 无状态 工作负载和应用程序,以及如何为此目的使用 Deployment 对象。 在云中运行无状态工作负载通常更容易处理,因为任何容器副本都可以处理请求,而无需依赖于用户先前操作的结果。换句话说,每个容器副本都会以相同的方式处理请求;你需要关心的只是适当的负载均衡。

然而,主要的复杂性在于管理应用程序的 状态。这里的 状态 指的是应用程序或组件需要存储的 数据,这些数据用于服务请求,并且可以被这些请求修改。应用程序中最常见的有状态组件是数据库——例如,它可以是 关系型 MySQL 数据库NoSQL MongoDB 数据库。在 Kubernetes 中,你可以使用专门的对象来运行 有状态 工作负载和应用程序:StatefulSet。在管理 StatefulSet 对象时,你通常需要与 Persistent Volumes (PVs) 一起工作,这部分内容已经在 第九章Kubernetes 中的持久化存储 中介绍过。本章将向你介绍 StatefulSet 在 Kubernetes 中的作用,以及如何创建和管理 StatefulSet 对象来发布你有状态应用程序的新版本。

本章将涵盖以下主题:

  • 介绍 StatefulSet 对象

  • 管理 StatefulSet

  • 发布作为 StatefulSet 部署的应用的新版本

  • StatefulSet 最佳实践

技术要求

本章你需要以下内容:

  • 需要一个已部署的 Kubernetes 集群。你可以使用本地集群或云端集群,但为了更好地理解这些概念,建议使用 多节点 的云端 Kubernetes 集群。该集群必须支持创建 PersistentVolumeClaims。任何云端集群或本地集群,例如使用 k8s.io/minikube-hostpath 提供程序的 minikube,都足够了。

  • 必须在本地计算机上安装 Kubernetes CLI (kubectl),并配置它来管理你的 Kubernetes 集群。

Kubernetes 集群的部署(本地和云端)和 kubectl 安装已在 第三章安装你的第一个 Kubernetes 集群 中介绍过。

你可以从官方 GitHub 仓库下载本章的最新代码示例,地址是 github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter12

介绍 StatefulSet 对象

你可能会想,为什么在分布式云中运行有状态工作负载通常被认为比运行无状态工作负载更困难。在经典的三层应用中,所有的状态都会存储在数据库中(数据层持久化层),这没有什么特别的地方。对于 SQL 服务器,你通常会添加一个故障转移设置和数据复制,如果需要更高的性能,你可以通过购买更好的硬件来进行纵向扩展。然后,某个时刻,你可能会考虑集群化的 SQL 解决方案,引入数据分片(水平数据分区)。但即便如此,从运行你应用的 web 服务器的角度来看,数据库也只是一个用于读写数据的单一连接字符串。数据库将负责持久化一个可变状态

记住,除非应用程序仅提供静态内容或只是转换用户输入,否则每个应用程序整体上都是有状态的。然而,这并不意味着应用程序中的每个组件都是有状态的。运行应用程序逻辑的 web 服务器可以是无状态组件,但这个应用程序存储用户输入和会话的数据库将是有状态组件。

我们将首先解释如何在容器中管理状态,并阐明我们认为的应用程序或系统状态。

在容器中管理状态

现在,假设你将 SQL 服务器(单实例)部署在容器中。你会注意到的第一件事是,每次重新启动容器后,数据库中存储的数据都会丢失——每次重新启动时,你得到的是 SQL 服务器的一个全新实例。容器是临时性的。这对于我们的使用案例来说似乎不太有用。幸运的是,容器提供了挂载数据卷的选项。一个卷可以是,例如,一个主机目录或外部磁盘卷,它会被挂载到容器文件系统中的特定路径。你在这个路径中存储的任何东西,即使容器终止或重新启动,也会保存在卷中。类似地,你可以使用 NFS 共享或外部磁盘实例作为卷。现在,如果你配置 SQL 服务器将其数据文件放在挂载卷的路径下,即使容器重新启动,你也能实现数据持久化。容器本身仍然是临时性的,但数据(状态)却不是

这是关于如何在普通容器中持久化状态的高级概述,而不涉及 Kubernetes。但在我们进入 Kubernetes 之前,我们需要澄清我们实际认为的状态是什么。

假设你有一个只提供简单 静态 内容的 Web 服务器(这意味着它始终是相同的,比如一个简单的 HTML 网页)。虽然仍然有一些数据是持久化的,例如 HTML 文件,但这 不是 状态:用户请求无法修改这些数据,因此 之前 的请求不会影响 当前 请求的结果。同样,Web 服务器的配置文件也不是它的状态,磁盘上写入的日志文件也不是(嗯,这个可以争论,但从最终用户的角度来看,它不是)。

现在,如果你有一个 Web 服务器,用来保持用户会话并存储有关用户是否已登录的信息,那么这确实是状态。根据这些信息,Web 服务器将返回不同的网页(响应),以区分用户是否登录。假设这个 Web 服务器运行在一个容器中 —— 当涉及到它是否是你应用程序中的 有状态 组件时,会有一些细节需要注意。如果 Web 服务器进程将用户会话存储为容器内的文件(警告:这可能是一个相当糟糕的设计),那么 Web 服务器容器就是一个 有状态 组件。但如果它将用户会话存储在一个运行在独立容器中的数据库或 Redis 缓存中,那么 Web 服务器就是 无状态 的,而数据库或 Redis 容器则成为有状态组件。

这从单个容器的角度来看大致是这样的。现在我们需要稍微放大一些,来看看在 Kubernetes Pods 中的状态管理。

在 Kubernetes Pods 中管理状态

在 Kubernetes 中,容器卷的概念通过 持久卷PVs)、持久卷声明PVCs)和 存储类SCs)得到了扩展,这些都是专门用于存储的对象。PVC 的目标是 解耦 Pods 与实际存储之间的关系。PVC 是一个 Kubernetes 对象,用于表示对特定类型、类或大小存储的请求 —— 比如说 我需要 10 GB 的一次性读写 SSD 存储。为了满足这样的请求,必须有一个 PV 对象,它是由集群的自动化过程提供的实际存储 —— 可以将其理解为主机系统上的一个目录或存储驱动程序管理的磁盘。PV 类型通过插件实现,类似于 Docker 或 Podman 中的卷。现在,整个 PV 提供过程可以是 动态的 —— 它需要创建一个 SC 对象,并在定义 PVC 时使用该 SC。创建新的 SC 时,你提供一个 提供者(或插件细节)与特定参数,每个使用该 SC 的 PVC 会自动创建一个 PV,使用所选的提供者。例如,提供者可以创建云管理磁盘来提供后端存储。除此之外,给定 Pod 的容器可以通过使用相同的 PV 来共享数据,并将其挂载到文件系统中。

这只是 Kubernetes 提供的状态存储的简要概述。我们在第九章《Kubernetes 中的持久存储》中对此进行了更详细的讲解。

除了单个 Pod 及其容器的状态管理外,还有多个副本的 Pod 的状态管理。让我们考虑一下,如果我们使用 Deployment 对象来运行多个 MySQL Server Pod 会发生什么。首先,您需要确保在容器中的卷上持久化状态——为此,您可以在 Kubernetes 中使用 PVs。但这样,您实际上会得到多个独立的 MySQL 服务器,如果您想要高可用性和容错性,这并不十分有用。如果您通过服务暴露这样的部署,它也将没有用处,因为每次您可能会连接到不同的 MySQL Pod 并获得不同的数据。

所以,您要么设计一个带有主从复制的多节点故障转移设置,要么设计一个复杂的数据分片集群。无论哪种情况,您的单个 MySQL Server Pod 副本都需要具有唯一身份,并且最好具有可预测的网络名称,以便节点和客户端可以进行通信。

在为 Kubernetes 集群设计云原生应用程序时,请始终分析将应用程序状态存储为有状态组件在 Kubernetes 中运行的所有优缺点。

这就是 StatefulSet 的作用。让我们更详细地看看这个 Kubernetes 对象。

StatefulSet 及其与 Deployment 对象的区别

Kubernetes StatefulSet 是一个类似于 Deployment 对象的概念。它也提供了一种管理和扩展 Pod 集合的方式,但它提供了关于 Pod 的顺序和唯一性(唯一身份)的保证。与 Deployment 一样,它使用 Pod 模板来定义每个副本的样子。您可以进行扩展和缩减,并进行新版本的发布。但在 StatefulSet 中,单个 Pod 副本是不可互换的。每个 Pod 的唯一持久身份在任何重新调度或发布期间都得以保持——这包括Pod 名称和其集群 DNS 名称。这个唯一的持久身份可以用于标识分配给每个 Pod 的 PVs,即使 Pods 在故障后被替换。为此,StatefulSet 在其规格中提供了另一种类型的模板,名为volumeClaimTemplates。该模板可以用于动态创建给定 SC 的 PVC。通过这种方式,整个存储配置过程完全动态——您只需创建一个 StatefulSet。底层存储对象由 StatefulSet 控制器管理。

StatefulSet 中单个 Pod 的集群 DNS 名称保持不变,但其集群 IP 地址不保证保持不变。这意味着,如果您需要连接到 StatefulSet 中的单个 Pod,应该使用集群 DNS 名称。

基本上,您可以将 StatefulSet 用于以下应用程序:

  • 由 Kubernetes 集群管理的持久化存储(这是主要使用场景,但不是唯一的)

  • 为每个 Pod 副本提供稳定且唯一的网络标识符(通常是 DNS 名称)

  • 有序部署和扩展

  • 有序的滚动更新

在以下图示中,您可以看到 StatefulSets 可以视为 Deployment 对象的一个更具可预测性的版本,并且可以使用 PVC 提供的持久化存储。

图 12.1:StatefulSet 高层视图

总结一下,StatefulSet 和 Deployment 之间的主要区别如下:

  • StatefulSet 确保为 Pod 提供 确定性(粘性)名称,名称由 <statefulSetName>-<ordinal> 组成。对于 Deployments,您将获得一个由 <deploymentName>-<podTemplateHash>-<randomHash> 组成的 随机 名称。

  • 对于 StatefulSet 对象,Pod 是以 特定可预测 的顺序启动和终止的,这确保了在扩展 ReplicaSet 时的一致性、稳定性和协调性。以前面提到的 MySQL StatefulSet 示例为例,Pod 将按顺序创建(mysql-0、mysql-1 和 mysql-2)。当您缩小 StatefulSet 时,Pod 将按相反的顺序终止 —— mysql-2、mysql-1,最后是 mysql-0。

  • 在存储方面,Kubernetes 会根据 StatefulSet 规范中的 volumeClaimTemplates 为 StatefulSet 中的每个 Pod 创建 PVC,并始终将其附加到具有 相同 名称的 Pod。对于 Deployment,如果您选择在 Pod 模板中使用 persistentVolumeClaim,Kubernetes 将创建一个单一的 PVC,并将其附加到 Deployment 中的所有 Pods。这在某些场景下可能有用,但并不是常见的使用场景。

  • 您需要创建一个 headless 服务对象,负责管理 Pod 的 确定性网络标识(集群 DNS 名称)。Headless 服务使我们能够将所有 Pod 的 IP 地址作为 DNS A 记录返回,而不是将其作为带有 ClusterIP 服务的单一 DNS A 记录。只有在不使用常规服务时,才需要 Headless 服务。StatefulSet 的规范要求在 .spec.serviceName 中提供服务名称。请参阅《第八章,通过服务暴露您的 Pod》中的 理解 Headless 服务

在我们探索 StatefulSet 之前,需要了解一些 StatefulSet 对象的局限性,具体内容将在以下部分中解释。

探索 StatefulSet 的局限性

以下是使用 StatefulSets 时需要注意的具体事项:

  • 存储设置:StatefulSets 不会自动为您的 Pod 创建存储。您需要使用内建工具(如 PersistentVolume Provisioner)或事先手动设置存储。

  • 残留存储:当你缩减或删除一个 StatefulSet 时,其 Pod 使用的存储会保留下来。这是因为你的数据很重要,不应被意外删除。如果需要,你需要手动清理存储。否则,残留的存储可能会成为一个问题,因为随着时间的推移,所有这些未使用的存储会积累,导致资源浪费和存储成本增加。

  • Pod 地址:你需要设置一个单独的服务(称为无头服务),以为 Pod 提供唯一且稳定的网络名称。

  • 停止 StatefulSets:当你删除 StatefulSet 时,无法保证 Pod 会按特定顺序关闭。为了确保干净地关闭,最好在完全删除 StatefulSet 之前,将其缩放为零个 Pod。

  • 更新 StatefulSets:使用 StatefulSets 的默认更新方法有时会导致需要手动修复的问题。请注意这一点,并在需要时考虑替代的更新策略。

在开始使用 StatefulSet 进行练习之前,请阅读下一节关于 StatefulSets 的重要信息。

Statefulset 中的数据管理

Kubernetes 提供了 StatefulSets 作为管理有状态应用程序的强大工具。然而,成功地部署和维护有状态应用程序需要用户的参与,远远不止是定义 StatefulSet 本身。以下是需要你关注的关键领域:

  • 数据克隆和同步:与无状态应用程序不同,有状态应用程序依赖于持久化数据。虽然 StatefulSets 管理 Pod 的顺序和身份,但它们不处理 Pods 之间的数据复制。你需要自己实现这一功能。常见的做法包括使用初始化容器在 Pod 创建时从预定义的源复制数据,利用应用程序内部的内建复制功能(例如 MySQL 复制),或使用外部脚本来管理数据同步。

  • 远程存储可访问性:StatefulSets 确保 Pod 可以在集群中可用的节点之间重新调度。为了在重新调度期间保持数据持久性,PV 提供的存储需要能够从所有工作节点访问。这意味着选择一个跨节点复制数据的存储类,或使用所有机器都能访问的网络附加存储解决方案。

  • 外部备份:StatefulSets 旨在管理 Pod 生命周期和集群内的数据持久化。然而,它们不处理外部备份。为了确保在灾难性事件发生时的灾难恢复,实施一个单独的外部备份解决方案是至关重要的。这可能涉及将你的 PV 备份到云存储服务或 Kubernetes 集群外的专用备份服务器。

有关于在 StatefulSets 中处理数据的最佳实践和推荐方法。下一节将解释一些 StatefulSets 的复制管理技术。

复制管理

正如名称所示,处理数据的有状态应用通常需要数据初始化或同步。你可能需要使用以下方法来实现这些功能:

  • 初始化容器:在启动应用程序之前,将数据从源(如配置映射)复制到持久化存储。

  • 应用级别的复制:利用应用内置的复制功能,在 Pods 之间处理数据更新。

  • 外部脚本:使用外部脚本或工具,在更新过程中管理数据迁移。

现在,让我们来看一个具体的 StatefulSet 示例,它部署了 MySQL Pods,并且支持持久化存储。

管理 StatefulSet

为了演示 StatefulSet 对象的工作原理,我们将修改 MySQL 部署并将其适配为 StatefulSet。StatefulSet 规格中的重要部分与部署(Deployments)相同。由于我们希望演示在 StatefulSet 对象中 PVC 的自动管理如何工作,我们将在规格中使用 volumeClaimTemplates 来创建 PVC 和 PV,这些 PVC 和 PV 会被 Pods 使用。每个 Pod 都会在容器文件系统中将其分配的 PV 挂载到 /var/lib/mysql 路径下,这里是 MySQL 数据文件的默认位置。通过这种方式,即使我们强制重启 Pods,也能演示如何保持 状态 持久化。

本章将使用的示例仅用于演示,目的是尽可能简单。如果你对 复杂 的示例感兴趣,例如在 StatefulSets 中部署和管理分布式数据库,请查看官方 Kubernetes 博文,关于部署 Cassandra 数据库的教程,网址是 kubernetes.io/docs/tutorials/stateful-application/cassandra/。通常,这类情况下的复杂性主要来自于在扩展 StatefulSet 时处理 Pod 副本的加入与移除。

现在,我们将逐一查看创建 StatefulSet 所需的所有 YAML 清单,并将它们应用到集群中。

创建 StatefulSet

我们已经讨论了 StatefulSet 的概念,现在是时候学习如何管理它们了。首先,让我们来看一下名为 mysql-statefulset.yaml 的 StatefulSet YAML 清单文件:

# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-stateful
  labels:
    app: mysql
  namespace: mysql
spec:
  serviceName: mysql-headless
  replicas: 3
  selector:
    matchLabels:
      app: mysql
      environment: test
# (to be continued in the next paragraph) 

上述文件的第一部分与 Deployment 对象规格非常相似,你需要提供 replicas 数量以及 Pod 的 selector。有一个新参数 serviceName,稍后我们会解释它。

文件的下一部分涉及 StatefulSet 使用的 Pod 模板的规格:

# (continued)
  template:
    metadata:
      labels:
        app: mysql
        environment: test
    spec:
      containers:
        - name: mysql
          image: mysql:8.2.0
          ports:
            - containerPort: 3306
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_ROOT_PASSWORD
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
# (to be continued in the next paragraph) 

如果你仔细观察,会发现其结构与部署(Deployments)是相同的。注意,我们通过 Secret 对象提供的环境变量,这些环境变量需要在创建 StatefulSet 之前创建。此外,文件的最后部分包含了 volumeClaimTemplates,它用于定义 Pod 使用的 PVC 模板:

# (continued)
  volumeClaimTemplates:
    - metadata:
        name: mysql-data
      spec:
        accessModes:
          - "ReadWriteOnce"
        resources:
          requests:
            storage: 1Gi 

如你所见,通常 StatefulSet 规范的结构与 Deployment 相似,尽管它有一些额外的参数用于配置 PVC 和相关的服务对象。该规范有五个主要组件:

  • replicas:定义应该使用给定的 template 和匹配的标签 selector 运行的 Pod 副本数。Pods 可能会被创建或删除,以维持所需的数量。

  • serviceName:管理 StatefulSet 并为 Pod 提供网络身份的服务名称。此服务必须在创建 StatefulSet 之前创建。我们将在下一步中创建 mysql-headless 服务。

  • selector:一个 标签选择器,定义如何识别 StatefulSet 所拥有的 Pods。这可以包括 基于集合基于相等 的选择器。

  • template:定义 Pod 创建的模板。metadata 中使用的标签必须与 selector 匹配。Pod 名称不是随机的,遵循 <statefulSetName>-<ordinal> 的约定。你可以选择使用 .spec.ordinals 来控制分配给 StatefulSet 中每个 Pod 的唯一标识符的起始数字。

  • volumeClaimTemplates:定义为每个 Pod 创建的 PVC 模板。StatefulSet 对象中的每个 Pod 都将获得自己的 PVC,并且该 PVC 始终与给定的 Pod 名称关联。在我们的示例中,这是一个 1 GB 的卷,访问模式为 ReadWriteOnce。此访问模式允许仅由 单个 节点挂载该卷进行读写。我们没有指定 storageClassName,因此 PVC 将使用集群中的默认 SC 进行配置。PVC 名称不是随机的,遵循 <volumeClaimTemplateName>-<statefulSetName>-<ordinal> 的约定。

集群中的默认 SC 通过 storageclass.kubernetes.io/is-default-class 注解标记。是否有默认 SC 以及它如何定义,取决于你的集群部署。例如,在 Azure Kubernetes Service 集群中,默认 SC 为名为 default 的 SC,使用 kubernetes.io/azure-disk 提供者。在 minikube 中,默认 SC 为名为 standard 的 SC,使用 k8s.io/minikube-hostpath 提供者。

规范还包含与滚动发布 StatefulSet 新修订版相关的其他字段——我们将在下一部分中详细解释这些内容。

接下来,让我们看一下我们名为 mysql-headless无头 服务。创建一个 mysql-headless-service.yaml 文件,内容如下:

# mysql-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  namespace: mysql
spec:
  selector:
    app: mysql
    environment: test
  clusterIP: None
  ports:
    - port: 3306 

该规范与我们之前为 Deployment 创建的普通服务非常相似;唯一的区别是它的 clusterIP 字段的值为 None。这将导致创建一个无头服务 mysql-headless。无头服务允许我们将 所有 Pods 的 IP 地址作为 DNS A 记录 返回,而不是使用带有 clusterIP 服务的单个 DNS A 记录。我们将在接下来的步骤中演示这在实际中的意义。

使用所有的 YAML 清单文件,我们可以开始部署我们的示例 StatefulSet!请执行以下步骤:

  1. 创建一个名为 mysql 的命名空间(使用 mysql-ns.yaml):

    $ kubectl apply –f mysql-ns.yaml
    namespace/mysql created 
    
  2. 创建一个 Secret 来存储 MySQL 环境变量:

    $ kubectl create secret generic mysql-secret \
      --from-literal=MYSQL_ROOT_PASSWORD='mysqlroot' \
      --from-literal=MYSQL_USER='mysqluser' \
      --from-literal=MYSQL_PASSWORD='mysqlpassword' \
      -n mysql
    secret/mysql-secret created 
    

请注意,也可以使用 YAML 和 base64 编码的值(例如,echo -n 'mysqlroot' | base64)在其中创建相同的 Secret(请参考仓库中的 mysql-secret.yaml 查看示例 YAML 文件);我们使用这种命令式方法来演示带有实际值的 Secret。

  1. 使用以下命令创建一个无头服务 mysql-headless

    $ kubectl apply -f mysql-headless-service.yaml
    service/mysql-headless created 
    
  2. 使用以下命令创建一个名为 mysql-stateful 的 StatefulSet 对象:

    $ kubectl apply -f mysql-statefulset.yaml
    statefulset.apps/mysql-stateful created 
    
  3. 现在,您可以使用 kubectl describe 命令来观察 StatefulSet 对象的创建(或者,您也可以在使用 kubectl 命令时使用 sts 作为 StatefulSet 的缩写):

    $ kubectl describe statefulset mysql-stateful -n mysql
    $  kubectl get sts -n mysql
    NAME             READY   AGE
    mysql-stateful   3/3     2m3s 
    
  4. 使用 kubectl get pods 命令查看是否已创建三个期望的 Pod 副本。请注意,这可能需要一些时间,因为 Pods 必须根据其 PVC 获取已提供的 PV。

    $ kubectl get pod -n mysql
    NAME               READY   STATUS    RESTARTS   AGE
    mysql-stateful-0   1/1     Running   0          2m32s
    mysql-stateful-1   1/1     Running   0          2m29s
    mysql-stateful-2   1/1     Running   0          2m25s 
    

请注意有序、确定性的 Pod 命名——这是为 StatefulSet 对象中的 Pods 提供唯一标识的关键。

  1. 如果您描述其中一个 Pod,您将看到与之关联的 PV 和 PVC 的更多详细信息:

    $ kubectl -n mysql describe pod mysql-stateful-0
    Name:             mysql-stateful-0
    Namespace:        mysql
    Priority:         0
    Service Account:  default
    ...<removed for brevity>...
    Volumes:
      mysql-data:
        Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
        ClaimName:  mysql-data-mysql-stateful-0
        ReadOnly:   false
    ...<removed for brevity>... 
    

对于第二个 Pod,您将看到类似以下的输出,但 PVC 会有所不同:

$ kubectl -n mysql describe pod mysql-stateful-1
Name:             mysql-stateful-1
Namespace:        mysql
Priority:         0
...<removed for brevity>...
Volumes:
  mysql-data:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  mysql-data-mysql-stateful-1
    ReadOnly:   false
...<removed for brevity>... 

如您所见,这个 mysql-stateful-0 Pod 使用的 PVC 名为 mysql-data-mysql-stateful-0,而这个 mysql-stateful-1 Pod 使用的 PVC 名为 mysql-data-mysql-stateful-1。在 Pod 被调度到其目标节点后,PV 会通过各自的 StorageClass 被提供并绑定到各个 PVC。之后,实际的容器会被创建,并内部挂载这个 PV。

  1. 使用 kubectl get 命令,我们可以揭示更多关于 PVC 的详细信息:

    $  kubectl get pvc -n mysql
    NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
    mysql-data-mysql-stateful-0   Bound    pvc-453dbfee-6076-48b9-8878-e7ac6f79d271   1Gi        RWO            standard       <unset>                 8m38s
    mysql-data-mysql-stateful-1   Bound    pvc-36494153-3829-42aa-be6d-4dc63163ea38   1Gi        RWO            standard       <unset>                 8m35s
    mysql-data-mysql-stateful-2   Bound    pvc-6730af33-f0b6-445d-841b-4fbad5732cde   1Gi        RWO            standard       <unset>                 8m31s 
    
  2. 最后,让我们来看看被提供的 PV:

    $ kubectl get pv
    NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                               STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
    pvc-36494153-3829-42aa-be6d-4dc63163ea38   1Gi        RWO            Delete           Bound    mysql/mysql-data-mysql-stateful-1   standard       <unset>                          11m
    pvc-453dbfee-6076-48b9-8878-e7ac6f79d271   1Gi        RWO            Delete           Bound    mysql/mysql-data-mysql-stateful-0   standard       <unset>                          11m
    pvc-6730af33-f0b6-445d-841b-4fbad5732cde   1Gi        RWO            Delete           Bound    mysql/mysql-data-mysql-stateful-2   standard       <unset>                          11m 
    

请注意,在我们的示例中,我们使用的是 minikube 的 hostPath 类型。如果您的 Kubernetes 集群使用了不同的存储后端,您将看到不同的输出。

我们已经成功创建了 StatefulSet 对象,现在是时候验证它在基本场景中的工作情况了。为此,我们将使用一个更新的 k8sutils 容器镜像,里面安装了默认的 MySQL 客户端包。(请查看 Chapter12/Containerfile 查看 k8sutils 镜像的详细信息。)按照以下方式创建 k8sutils.yaml

# k8sutils.yaml
apiVersion: v1
kind: Pod
metadata:
  name: k8sutils
  # namespace: default
spec:
  containers:
    - name: k8sutils
      image: quay.io/iamgini/k8sutils:debian12-1.1
      command:
        - sleep
        - "infinity"
      # imagePullPolicy: IfNotPresent
  restartPolicy: Always 

按照以下方式创建 k8sutils Pod:

$ kubectl apply -f k8sutils.yaml -n mysql
pod/k8sutils created 

请注意,在应用 YAML 时我们使用了 -n mysql,这样资源将被创建在 mysql 命名空间中。

按照以下步骤验证 StatefulSet 中不同 Pods 的内容:

  1. 进入 k8sutil Pod 执行我们的测试命令:

    $ kubectl exec -it -n mysql k8sutils -- /bin/bash
    root@k8sutils:/# 
    
  2. 使用我们之前创建的默认无头服务访问 MySQL Stateful 应用(记得之前通过 Secret 对象创建的密码):

    root@k8sutils:/# mysql -u root -p -h mysql-headless.mysql.svc.cluster.local
    Enter password: <mysqlroot>
    Welcome to the MariaDB monitor.  Commands end with ; or \g.
    Your MySQL connection id is 8
    Server version: 8.2.0 MySQL Community Server - GPL
    Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    MySQL [(none)]> show databases;
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    | sys                |
    +--------------------+
    4 rows in set (0.003 sec)
    MySQL [(none)]> 
    

基本的 MySQL 连接已正常工作,我们能够访问作为 StatefulSet 应用程序运行的 MySQL 服务器。现在,我们将快速查看无头服务的行为。

使用无头服务和稳定的网络身份

之前,我们了解了 Kubernetes 中的无头服务以及如何使用它来访问有状态集应用程序。(参考 第八章 中的 理解无头服务 部分,通过服务暴露你的 Pods)。在本节中,让我们深入探讨 StatefulSet 后端中的无头服务机制。

让我们做一个实验,演示如何使用 headless 服务为我们的 Pod 提供稳定且可预测的网络身份:

  1. 登录到我们在上一个测试中使用的相同 k8sutils Pod。

  2. 对无头服务 mysql-headless 执行 DNS 检查:

    root@k8sutils:/# nslookup mysql-headless
    Server:         10.96.0.10
    Address:        10.96.0.10#53
    Name:   mysql-headless.mysql.svc.cluster.local
    Address: 10.244.0.14
    Name:   mysql-headless.mysql.svc.cluster.local
    Address: 10.244.0.15
    Name:   mysql-headless.mysql.svc.cluster.local
    Address: 10.244.0.16 
    

我们收到了三个 A 记录,它们直接指向 Pod 的 IP 地址。此外,它们有 CNAME 记录,形式为 <podName>-<ordinal-number>.<headless-serviceName>.<namespace>.svc.cluster.local。因此,与默认服务的不同之处在于,具有 ClusterIP 的服务会进行负载均衡,达到 虚拟 IP 级别(在 Linux 中,通常通过 iptables 规则,由 kube-proxy 配置在内核级别处理),而对于无头服务,负载均衡或选择目标 Pod 的责任由发起请求的 客户端 承担。

  1. 对于 StatefulSet 中的 Pods,拥有 可预测的 FQDN 给了我们直接向单个 Pods 发送请求的选项,而无需猜测它们的 IP 地址或名称。让我们尝试通过无头服务提供的短 DNS 名称来访问由 mysql-stateful-0 提供的 MySQL 服务器:

    root@k8sutils:/# mysql -u root -p -h mysql-stateful-0.mysql-headless
    Enter password: <mysqlroot>
    Welcome to the MariaDB monitor.  Commands end with ; or \g.
    Your MySQL connection id is 8
    Server version: 8.2.0 MySQL Community Server - GPL
    Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    MySQL [(none)]> 
    

正如预期的那样,你已经直接连接到 Pod,并由正确的 Pod 提供服务。

  1. 让我们在 MySQL 数据库服务器中创建一个数据库,如下所示:

    MySQL [(none)]> create database ststest;
    Query OK, 1 row affected (0.002 sec)
    MySQL [(none)]> exit;
    Bye 
    
  2. 现在,我们将展示即使 Pod 重启,DNS 名称也保持不变。Pod 的 IP 地址会变化,但 DNS 名称不会变化。更重要的是,挂载的 PV 也会保持不变,但我们将在接下来的段落中调查这一点。在另一个 shell 窗口中,容器外执行以下命令,强制重启 mysql-stateful-0 Pod:

    $ kubectl delete po -n mysql mysql-stateful-0 
    

检查 Pods,你会看到 mysql-stateful-0 已被重新创建,并挂载了相同的 mysql-data-mysql-stateful-0 PVC:

$ kubectl get po -n mysql
NAME               READY   STATUS    RESTARTS   AGE
k8sutils           1/1     Running   0          35m
mysql-stateful-0   1/1     Running   0          6s
mysql-stateful-1   1/1     Running   0          52m
mysql-stateful-2   1/1     Running   0          51m 
  1. k8sutils shell 中,执行 MySQL 客户端命令以检查数据库服务器内容:

    root@k8sutils:/# mysql -u root -p -h mysql-stateful-0.mysql-headless
    Enter password: <mysqlroot>
    Welcome to the MariaDB monitor.  Commands end with ; or \g.
    Your MySQL connection id is 8
    Server version: 8.2.0 MySQL Community Server - GPL
    Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    MySQL [(none)]> show databases;
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    | ststest            |
    | sys                |
    +--------------------+
    5 rows in set (0.003 sec) 
    

你可以看到我们在 Pod 删除之前创建的数据库 ststest 仍然存在,这意味着数据是持久的或有状态的。同时,注意到 Pod 的 IP 地址发生了变化,但 DNS 名称保持不变。

这解释了如何利用无头服务来获取一个稳定且可预测的网络标识符,该标识符在 Pod 被重启或重建时不会改变。你可能会想,这到底有什么实际用途,为什么它对 StatefulSet 对象如此重要。这里有几个可能的使用案例:

  • 部署集群数据库,如 etcd 或 MongoDB,需要指定数据库集群中其他节点的网络地址。如果数据库没有提供自动发现功能,这一点尤其重要。在这种情况下,由无头服务提供的稳定 DNS 名称可以帮助在 Kubernetes 上以 StatefulSets 的形式运行此类集群。仍然存在在扩展期间添加或删除 Pod 副本时更改配置的问题。在某些情况下,sidecar 容器模式可以解决这个问题,它监控 Kubernetes API 以动态更改数据库配置。

  • 如果你决定实现自己的存储解决方案,并将其作为 StatefulSet 运行,具有高级数据分片功能,你很可能需要将逻辑分片映射到集群中的物理 Pod 副本。然后,稳定的 DNS 名称可以作为映射的一部分使用。它们将确保每个逻辑分片的查询始终发送到正确的 Pod,无论该 Pod 是否被重新调度到另一个节点或重启。

最后,让我们看看运行在 StatefulSet 中的 Pods 的状态持久性。

状态持久性

正如我们之前演示的,数据会在 PV 内持久化,并将绑定到具有相同顺序号的新创建的 Pod。

在以下示例中,我们正在删除 StatefulSet 中的所有 Pods:

$ kubectl delete po -n mysql mysql-stateful-0 mysql-stateful-1 mysql-stateful-2
pod "mysql-stateful-0" deleted
pod "mysql-stateful-1" deleted
pod "mysql-stateful-2" deleted 

Kubernetes 会按以下顺序重新创建所有 Pods:

$ kubectl get pod -n mysql
NAME               READY   STATUS    RESTARTS   AGE
k8sutils           1/1     Running   0          47m
mysql-stateful-0   1/1     Running   0          44s
mysql-stateful-1   1/1     Running   0          43s
mysql-stateful-2   1/1     Running   0          41s 

我们还可以验证已挂载的 PV,以确保 Pod 到 PVC 的绑定成功:

$ kubectl describe pod -n mysql -l app=mysql |egrep 'ClaimName|Name:'
Name:             mysql-stateful-0
    ClaimName:  mysql-data-mysql-stateful-0
    ConfigMapName:           kube-root-ca.crt
Name:             mysql-stateful-1
    ClaimName:  mysql-data-mysql-stateful-1
    ConfigMapName:           kube-root-ca.crt
Name:             mysql-stateful-2
    ClaimName:  mysql-data-mysql-stateful-2
    ConfigMapName:           kube-root-ca.crt 

正如你所学到的,StatefulSet 控制器不会在 Pod 或 StatefulSet 删除时删除 PV。若你完全删除 StatefulSet 对象,清理数据并手动删除 PV 是你的责任。

接下来,我们将看看如何扩展 StatefulSet 对象。

扩展 StatefulSet

在 StatefulSet 的情况下,你可以像操作 Deployment 对象一样执行类似的扩展操作,通过更改规格中的 replicas 数量或使用 kubectl scale 命令。新 Pods 会在扩展时自动作为服务的新 Endpoints 被发现,或者在缩减时自动从 Endpoints 列表中移除。

然而,与 Deployment 对象相比,仍然存在一些差异:

  • 当你部署一个有 N 个副本的 StatefulSet 对象时,部署过程中,Pods 是按顺序创建的,从 0N-1。在我们的示例中,创建一个有三个副本的 StatefulSet 对象时,第一个 mysql-stateful-0 Pod 被创建,然后是 mysql-stateful-1,最后是 mysql-stateful-2

  • 当你扩容StatefulSet 时,新的 Pod 也会按顺序创建,并且有序进行。

  • 当你缩容StatefulSet 时,Pod 会按反向顺序依次终止,从N-10。在我们的例子中,当 StatefulSet 对象缩容至零副本时,mysql-stateful-2 Pod 首先被终止,然后是mysql-stateful-1,最后是mysql-stateful-0

  • 在 StatefulSet 对象进行扩容时,在按顺序创建下一个 Pod 之前,所有前一个 Pod 必须是运行中并且就绪的

  • 在 StatefulSet 对象进行缩容时,在下一个 Pod 按反向顺序终止之前,所有前一个 Pod 必须完全终止并被删除

  • 此外,通常在对 StatefulSet 对象中的 Pod 执行任何扩展操作之前,所有前一个 Pod 必须处于运行并就绪状态。这意味着,如果在从四个副本缩容到一个副本时,mysql-stateful-0 Pod 突然失败,那么就不会对mysql-stateful-1mysql-stateful-2mysql-stateful-3 Pod 进行进一步的扩展操作。扩展操作将在mysql-stateful-0 Pod 重新就绪后恢复。

通过更改规范中的.spec.podManagementPolicy字段,可以放宽扩展操作的顺序行为。默认值为OrderedReady。如果将其更改为Parallel,则扩展操作将在 Pod 上并行执行,类似于 Deployment 对象中的操作。请注意,这仅影响扩展操作。使用RollingUpdate类型的updateStrategy更新 StatefulSet 对象的方式不变。

具备了这些知识后,让我们扩容我们的 StatefulSet 对象,快速演示一下:

  1. 使用以下命令扩展 StatefulSet:

    $ kubectl scale statefulset -n mysql mysql-stateful --replicas 4
    statefulset.apps/mysql-stateful scaled 
    
  2. 如果你现在使用kubectl get pods命令检查 Pod,你将看到新的 Pod 按顺序创建:

    $ kubectl get pod -n mysql
    NAME               READY   STATUS    RESTARTS   AGE
    k8sutils           1/1     Running   0          56m
    mysql-stateful-0   1/1     Running   0          9m13s
    mysql-stateful-1   1/1     Running   0          9m12s
    mysql-stateful-2   1/1     Running   0          9m10s
    mysql-stateful-3   1/1     Running   0          4s 
    

同样,如果你检查 StatefulSet 对象的kubectl describe命令输出,你会在事件中看到以下内容:

$ kubectl describe sts -n mysql mysql-stateful
Name:               mysql-stateful
Namespace:          mysql
...<removed for brevity>...
Events:
  Type    Reason                   Age                 From                    Message
  ----    ------                   ----                ----                    -------
  Normal  SuccessfulCreate         23m (x2 over 75m)   statefulset-controller  create Pod mysql-stateful-0 in StatefulSet mysql-stateful successful
  Normal  RecreatingTerminatedPod  11m (x13 over 23m)  statefulset-controller  StatefulSet mysql/mysql-stateful is recreating terminated Pod mysql-stateful-0
  Normal  SuccessfulDelete         11m (x13 over 23m)  statefulset-controller  delete Pod mysql-stateful-0 in StatefulSet mysql-stateful successful
  Normal  SuccessfulCreate         2m28s               statefulset-controller  create Claim mysql-data-mysql-stateful-3 Pod mysql-stateful-3 in StatefulSet mysql-stateful success
  Normal  SuccessfulCreate         2m28s               statefulset-controller  create Pod mysql-stateful-3 in StatefulSet mysql-stateful successful
Let us scale down our StatefulSet object imperatively and check the Pods as follows:
$ kubectl scale statefulset -n mysql mysql-stateful --replicas 2
statefulset.apps/mysql-stateful scaled 

你可以看到最后两个 Pod——mysql-stateful-3mysql-stateful-2——已按顺序删除。现在,让我们检查statefulset中的 Pod,如下所示:

$  kubectl get pod -n mysql
NAME               READY   STATUS    RESTARTS   AGE
k8sutils           1/1     Running   0          61m
mysql-stateful-0   1/1     Running   0          15m
mysql-stateful-1   1/1     Running   0          15m 
  1. 现在检查 PVC,你会发现 PVC 仍然存在。这是 StatefulSet 的预期情况,正如我们之前所学到的:

    $ kubectl get pvc -n mysql
    NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
    mysql-data-mysql-stateful-0   Bound    pvc-453dbfee-6076-48b9-8878-e7ac6f79d271   1Gi        RWO            standard       <unset>                 79m
    mysql-data-mysql-stateful-1   Bound    pvc-36494153-3829-42aa-be6d-4dc63163ea38   1Gi        RWO            standard       <unset>                 79m
    mysql-data-mysql-stateful-2   Bound    pvc-6730af33-f0b6-445d-841b-4fbad5732cde   1Gi        RWO            standard       <unset>                 79m
    mysql-data-mysql-stateful-3   Bound    pvc-6ec1ee2a-5be3-4bf9-84e5-4f5aee566c11   1Gi        RWO            standard       <unset>                 7m4s 
    

当使用水平 Pod 自动扩展器HPA)或类似的水平扩展工具管理 StatefulSet 时,避免在清单中为.spec.replicas指定值。相反,保持该字段为空。Kubernetes 控制平面会根据资源需求动态调整副本数,从而有效地扩展你的应用程序,无需手动干预。

恭喜!我们已经学会了如何部署和扩展 StatefulSet 对象。接下来,我们将演示如何删除 StatefulSet 对象。

删除 StatefulSet

删除 StatefulSet 对象有两种可能性:

  • 删除 StatefulSet 及其拥有的 Pods

  • 删除 StatefulSet 并保持 Pods 不受影响

在这两种情况下,使用 volumeClaimTemplates 为 Pods 创建的 PVC 和 PV 默认不会被删除。这确保了状态数据不会意外丢失,除非你明确清理 PVC 和 PV。

但是在最新的 Kubernetes 版本(从 v1.27 开始)中,你可以使用 .spec.persistentVolumeClaimRetentionPolicy 字段来控制 PVC 在 StatefulSet 生命周期中的删除:

apiVersion: apps/v1
kind: StatefulSet
...
spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain
    whenScaled: Delete
... 

请参阅文档以了解更多关于 persistentVolumeClaimRetentionPolicy 的信息(https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#persistentvolumeclaim-retention)。

要删除 StatefulSet 对象及其 Pods,你可以使用常规的 kubectl delete 命令:

$ kubectl delete sts -n mysql mysql-stateful
statefulset.apps "mysql-stateful" deleted 

你将看到 Pods 会首先被终止,然后是 StatefulSet 对象。请注意,这一操作与将 StatefulSet 对象缩容至零副本并删除它的操作不同。如果你删除包含现有 Pods 的 StatefulSet 对象,无法保证单个 Pods 的终止顺序。在大多数情况下,它们会被同时终止。

可选地,如果你只想删除 StatefulSet 对象,你需要为 kubectl delete 命令使用 --cascade=orphan 选项:

$ kubectl delete sts -n mysql mysql-stateful --cascade=orphan 

执行此命令后,如果你检查集群中的 Pods,你仍然会看到所有由 mysql-stateful StatefulSet 所拥有的 Pods。

最后,如果你希望在删除 StatefulSet 对象后清理 PVC 和 PV,你需要手动执行此步骤。使用以下命令删除作为 StatefulSet 一部分创建的 PVC:

$ kubectl delete -n mysql pvc mysql-data-mysql-stateful-0 mysql-data-mysql-stateful-1 mysql-data-mysql-stateful-2 mysql-data-mysql-stateful-3
persistentvolumeclaim "mysql-data-mysql-stateful-0" deleted
persistentvolumeclaim "mysql-data-mysql-stateful-1" deleted
persistentvolumeclaim "mysql-data-mysql-stateful-2" deleted
persistentvolumeclaim "mysql-data-mysql-stateful-3" deleted 

此命令将删除 PVC 及相关的 PV。

重要提示

请注意,如果你想在下一部分的新版本发布过程中验证状态持久性,你不应该删除 PVC。否则,你将丢失存储在 PV 中的 MySQL 文件。

通过这一部分,我们完成了对 Kubernetes 中 StatefulSet 对象基本操作的学习。接下来,让我们看看如何发布作为 StatefulSet 部署的应用的新版本,以及 StatefulSet 修订版是如何管理的。

发布作为 StatefulSet 部署的应用的新版本

我们在上一节中已经介绍了 StatefulSet 的扩缩容,通过kubectl scale命令(或通过修改规范中的.spec.replicas数量)。你所学到的关于 Pod 的顺序和有序变化的知识在使用RollingUpdate策略推出 StatefulSet 对象的新版本时发挥着重要作用。StatefulSet 和 Deployment 对象之间有许多相似之处。我们在第十一章《使用 Kubernetes 部署无状态工作负载》中详细讨论了 Deployment 更新的内容。对 StatefulSet Pod 模板spec.template)的更改也会导致 StatefulSet 的新版本发布。

StatefulSet 支持两种更新策略,你可以通过规范中的.spec.updateStrategy.type字段来定义它们:

  • RollingUpdate:默认策略,允许你以可控的方式推出应用程序的新版本。这与 Deployment 对象中已知的RollingUpdate策略略有不同。对于 StatefulSet,该策略会以顺序和有序的方式终止并重新创建 Pod,并确保在继续处理下一个 Pod 之前,Pod 已经重新创建并处于就绪状态。

  • OnDelete:此策略实现了 Kubernetes 1.7 之前 StatefulSet 更新的传统行为。然而,它仍然非常有用!在这种策略下,StatefulSet 不会通过重新创建 Pod 副本来自动更新 Pod 副本。你需要手动删除 Pod 副本,以便应用新的 Pod 模板。在需要在继续处理下一个 Pod 副本之前执行额外的手动操作或验证的场景中,这非常有用。例如,如果你在 StatefulSet 中运行一个Cassandra 集群etcd 集群,你可能想要验证新的 Pod 在删除旧版本 Pod 之后,是否正确地加入了现有集群。当然,使用RollingUpdate策略时,也可以通过 Pod 模板生命周期中的postStartpreStop钩子来执行类似的检查,但这需要在钩子中处理更复杂的错误。

现在让我们仔细看看RollingUpdate策略,这是 StatefulSet 最重要和最常用的更新策略。关键在于,这个策略尊重我们在上一节中解释的关于扩缩容的 StatefulSet 保证。发布是按相反的顺序进行的;例如,第一个 Pod mysql-stateful-2 会使用新的 Pod 模板重新创建,接着是mysql-stateful-1,最后是mysql-stateful-0

如果发布过程中失败(不一定是当前重新创建的 Pod),StatefulSet 控制器会将任何失败的 Pod 恢复到其当前版本。这意味着已经成功更新到当前版本的 Pod 将保持在当前版本,而尚未更新的 Pod 将保持在之前的版本。这样,StatefulSet 会尽力保持应用的健康和一致性。然而,这也可能导致 StatefulSet 的失败发布。如果某个 Pod 副本永远没有变为运行并准备好,StatefulSet 将停止发布并等待手动干预。仅仅再次应用模板到 StatefulSet 的先前版本是不够的——这个操作不会继续,因为 StatefulSet 会等待失败的 Pod 变为就绪。唯一的解决方法是手动删除失败的 Pod,然后让 StatefulSet 应用 Pod 模板的先前版本。

最后,RollingUpdate策略还提供了使用.spec.updateStrategy.rollingUpdate.partition字段执行分阶段发布的选项。此字段定义了一个数字,所有较小的 Pod 副本序号将不会被更新,即使它们被删除,也会按之前的版本重新创建。因此,在我们的示例中,如果将partition设置为1,这意味着在发布过程中,只有mysql-stateful-1mysql-stateful-2会被更新,而mysql-stateful-0将保持不变,并运行在之前的版本。通过控制partition字段,你可以轻松地发布单个金丝雀副本并执行分阶段发布。请注意,默认值为0,这意味着所有 Pod 副本都会被更新。

现在,我们将使用RollingUpdate策略发布新的 mysqlserver 版本。

更新 StatefulSet

我们现在将演示如何使用之前创建的 StatefulSet YAML 清单文件,发布 Pod 容器的新镜像版本:

  1. 复制之前的 YAML 清单文件:

    $ cp mysql-statefulset.yaml mysql-statefulset-rolling-update.yaml 
    
  2. 确保你使用的是RollingUpdate策略类型,并将partition设置为0。同时请注意,如果你曾尝试先使用不同的策略创建 StatefulSet 对象,在删除 StatefulSet 之前无法修改它:

    # mysql-statefulset-rolling-update.yaml
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: mysql-stateful
      labels:
        app: mysql
      namespace: mysql
    spec:
      serviceName: mysql-headless
      **podManagementPolicy:****OrderedReady**
      updateStrategy:
        type: RollingUpdate
        rollingUpdate:
          **partition:****0**
      replicas: 3
    ...<removed for brevity>...
    (Refer to the GitHub repo for full YAML) 
    

这些值是默认值,但明确指定它们有助于理解实际发生了什么。

  1. 将清单文件应用到集群中,以使用新配置创建mysql-stateful StatefulSet:

    $ kubectl apply -f mysql-statefulset-rolling-update.yaml
    statefulset.apps/mysql-stateful created 
    

在继续滚动更新任务之前,等待 Pod 运行。我们可以通过以下kubectl get pods命令验证由 StatefulSet 创建的 Pod:

$ kubectl get pods -n mysql
NAME               READY   STATUS    RESTARTS      AGE
k8sutils           1/1     Running   1 (21h ago)   23h
mysql-stateful-0   1/1     Running   0             65s
mysql-stateful-1   1/1     Running   0             62s
mysql-stateful-2   1/1     Running   0             58s 
  1. 当 StatefulSet 在集群中准备好后,我们将通过 k8sutils Pod 在 StatefulSet 内部创建一个新数据库,如下所示:

    $ kubectl exec -it -n mysql k8sutils -- /bin/bash
    root@k8sutils:/# mysql -u root -p -h mysql-stateful-0.mysql-headless
    Enter password: <mysqlroot>
    Welcome to the MariaDB monitor.  Commands end with ; or \g.
    ...<removed for brevity>...
    MySQL [(none)]> create database stsrolling;
    Query OK, 1 row affected (0.027 sec)
    MySQL [(none)]> exit; 
    

现在,我们有了一个新的 StatefulSet,带有updateStrategy,并在其中创建了一个新的数据库。

接下来,我们可以为我们的 StatefulSet 对象发布一个新的 MySQL 容器镜像版本。为此,执行以下步骤:

  1. 将 StatefulSet Pod 模板中使用的容器镜像修改为mysql:8.3.0

    # mysql-statefulset-rolling-update.yaml
    ...<removed for brevity>...
        spec:
          containers:
            - name: mysql
              image: mysql:8.3.0
    ...<removed for brevity>... 
    
  2. 使用以下命令将更改应用到集群中:

    $ kubectl apply -f mysql-statefulset-rolling-update.yaml
    statefulset.apps/mysql-stateful configured 
    
  3. 紧接着,使用kubectl rollout status命令查看实时进度。由于发布是按顺序和有序的方式进行的,这个过程比 Deployment 对象要稍长一些:

    $ kubectl rollout status statefulset -n mysql
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for partitioned roll out to finish: 1 out of 3 new pods have been updated...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for partitioned roll out to finish: 2 out of 3 new pods have been updated...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    partitioned roll out complete: 3 new pods have been updated... 
    
  4. 类似地,使用kubectl describe命令,你可以查看 StatefulSet 的事件,这些事件精确展示了 Pod 副本重建的顺序:

    $ kubectl describe sts -n mysql mysql-stateful
    Name:               mysql-stateful
    Namespace:          mysql
    ...<removed for brevity>...
    Events:
      Type     Reason                   Age                From                    Message
      ----     ------                   ----               ----                    -------
    ...<removed for brevity>...
      Normal   SuccessfulDelete         72s (x7 over 73s)  statefulset-controller  delete Pod mysql-stateful-2 in StatefulSet mysql-stateful successful
      Normal   RecreatingTerminatedPod  72s (x7 over 72s)  statefulset-controller  StatefulSet mysql/mysql-stateful is recreating terminated Pod mysql-stateful-2
      Warning  FailedDelete             72s                statefulset-controller  delete Pod mysql-stateful-2 in StatefulSet mysql-stateful failed error: pods "mysql-stateful-2" not found
      Normal   SuccessfulDelete         70s (x2 over 71s)  statefulset-controller  delete Pod mysql-stateful-1 in StatefulSet mysql-stateful successful
      Normal   RecreatingTerminatedPod  70s                statefulset-controller  StatefulSet mysql/mysql-stateful is recreating terminated Pod mysql-stateful-1 
    

如预期的那样,发布是按相反顺序完成的。第一个重建的 Pod 是mysql-stateful-2,然后是mysql-stateful-1,最后是mysql-stateful-0。此外,由于我们使用了默认的partition0,所有的 Pods 都被更新了。这是因为所有 Pod 副本的顺序编号都大于或等于0

  1. 现在,我们可以验证 Pods 是否使用新镜像进行了重建。执行以下命令来验证 StatefulSet 对象中的第一个 Pod 副本:

    $ kubectl describe pod -n mysql mysql-stateful-0|grep Image
        Image:          mysql:8.3.0
        Image ID:       docker.io/library/mysql@sha256:9de9d54fecee6253130e65154b930978b1fcc336bcc86dfd06e89b72a2588ebe 
    
  2. 最后,你可以验证状态是否得以保持,因为新 Pods 使用了现有的 PVC。请注意,只有在你没有在前一部分手动删除 StatefulSet 的 PVC 时,这一过程才能正常工作:

    $ kubectl exec -it -n mysql k8sutils -- /bin/bash
    root@k8sutils:/# mysql -u root -p -h mysql-stateful-0.mysql-headless
    Enter password: **<mysqlroot>**
    ...<removed for brevity>...
    Server version: **8.3.0 MySQL Community Server - GPL**
    Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    MySQL [(none)]> show databases;
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    **| stsrolling         |**
    | sys                |
    +--------------------+
    5 rows in set (0.004 sec)
    MySQL [(none)]> 
    

如你在上面的输出中所看到的,MySQL 新版本的发布已成功完成,状态得以保持,尽管 Pods 被重建了;你可以看到stsrolling数据库,这是你在滚动更新之前创建的。

你可以通过使用kubectl -n mysql set image sts mysql-stateful mysql=mysql:8.3.0命令命令式地修改 StatefulSet 容器镜像。这种方法仅推荐用于非生产环境和测试场景。一般来说,StatefulSets 比起命令式管理,更容易通过声明式方式进行管理。

现在,让我们学习如何使用partition字段进行分阶段发布,并使用金丝雀发布。假设我们想要将 mysql 镜像版本更新为8.4.0。你希望通过金丝雀发布确保更改在你的环境中正常工作,这是一种将单个(或部分)Pod 副本更新为新镜像(或其他镜像)版本的方法。请参考以下步骤:

  1. 修改mysql-statefulset-rolling-update.yaml清单文件,使partition数字等于当前replicas,在我们的案例中是3

    ...<removed for brevity>...
    spec:
      serviceName: mysql-headless
      podManagementPolicy: OrderedReady
      updateStrategy:
        type: RollingUpdate
        rollingUpdate:
          **partition: 3**
    ...<removed for brevity>...
    Also, update the image to 8.4.0 as follows:
    ...
        spec:
          containers:
            - name: mysql
              **image: mysql:8.4.0**
    ...<removed for brevity>... 
    

partition的数字与replicas的数量相同,我们可以将 YAML 清单文件应用到集群中,而 Pods 将不会立即发生变化。这称为发布预演

  1. 将清单文件应用到集群中:

    $ kubectl apply -f mysql-statefulset-rolling-update.yaml
    statefulset.apps/mysql-stateful configured 
    
  2. 现在,让我们为新版本创建一个金丝雀。在清单文件中将partition数量减少 1 至2。这意味着所有序号小于2的 Pod 副本将不会更新——在我们的例子中,这仅意味着更新mysql-stateful-2 Pod。其他 Pod 将保持不变:

    ...
    spec:
      serviceName: mysql-headless
      podManagementPolicy: OrderedReady
      updateStrategy:
        type: RollingUpdate
        rollingUpdate:
          partition: 2
      replicas: 3
    ... 
    
  3. 再次将清单文件应用到集群中:

    $ kubectl apply -f mysql-statefulset-rolling-update.yaml
    statefulset.apps/mysql-stateful configured 
    
  4. 使用kubectl rollout status命令来跟踪进程。如预期,只有一个 Pod 会被重新创建:

    $ kubectl rollout status statefulset -n mysql
    Waiting for partitioned roll out to finish: 0 out of 1 new pods have been updated...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    partitioned roll out complete: 1 new pods have been updated... 
    
  5. 如果你描述 MySQL mysql-stateful-0 和 MySQL mysql-stateful-2 Pods,你会发现第一个 Pod 使用的是旧版本的镜像,而第二个 Pod 则使用的是新版本:

    $ kubectl describe pod -n mysql mysql-stateful-0|grep Image
        Image:          mysql:8.3.0
        Image ID:       docker-pullable://mysql@sha256:9de9d54fecee6253130e65154b930978b1fcc336bcc86dfd06e89b72a2588ebe
    $ kubectl describe pod -n mysql mysql-stateful-2|grep Image
        Image:          mysql:8.4.0
        Image ID:       docker-pullable://mysql@sha256:4a4e5e2a19aab7a67870588952e8f401e17a330466ecfc55c9acf51196da5bd0 
    
  6. 此时,你可以对金丝雀进行验证和冒烟测试。登录到 k8sutils Pod,并确保新的 Pod 使用新镜像(例如,8.4.0)运行正常。金丝雀看起来没有问题,所以我们可以继续进行新版本的阶段性发布:

    $ kubectl exec -it -n mysql k8sutils -- /bin/bash
    root@k8sutils:/# mysql -u root -p -h mysql-stateful-2.mysql-headless
    Enter password: <mysqlroot>
    Welcome to the MariaDB monitor.  Commands end with ; or \g.
    Your MySQL connection id is 11
    Server version: 8.4.0 MySQL Community Server - GPL
    ... 
    
  7. 对于阶段性发布,你可以在清单中使用任何较低partition值。你可以进行几个小的阶段性发布,或者直接进行完全发布。让我们通过将partition减少到0来进行完全发布:

    ...
      updateStrategy:
        type: RollingUpdate
        rollingUpdate:
          partition: 0
    ... 
    
  8. 再次将清单文件应用到集群中:

    $ kubectl apply -f mysql-statefulset-rolling-update.yaml
    statefulset.apps/mysql-stateful configured 
    
  9. 使用kubectl rollout status命令观察发布的下一个阶段:

    $ kubectl rollout status statefulset -n mysql
    Waiting for partitioned roll out to finish: 1 out of 3 new pods have been updated...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for partitioned roll out to finish: 2 out of 3 new pods have been updated...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    Waiting for 1 pods to be ready...
    partitioned roll out complete: 3 new pods have been updated... 
    

如你所见,mysql:8.4.0 镜像版本的阶段性发布已成功完成。

可以通过命令式的方式进行阶段性发布。为此,你需要使用kubectl patch命令来控制partition数量,例如,kubectl patch sts mysql-stateful -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}' -n mysql。然而,这种方式的可读性较差,且比声明式变更更容易出错。

接下来,我们将看一下如何在下一部分执行 StatefulSets 的回滚操作。

回滚 StatefulSet

在之前的第十一章使用 Kubernetes 部署无状态工作负载》中,我们已经描述了如何对部署进行命令式回滚。对于 StatefulSets,你可以做完全相同的操作。为此,你需要使用kubectl rollout undo命令。然而,特别是对于 StatefulSets,我们建议使用声明式模型来引入更改到 Kubernetes 集群中。在这种模型下,通常会将每个更改提交到源代码仓库中。执行回滚非常简单,只需要恢复提交并再次应用配置即可。通常,应用更改(包括部署和更新)的过程可以作为源代码仓库的 CI/CD 管道的一部分来执行,而不是由操作员手动应用更改。这是管理 StatefulSets 最简单的方法,在基础设施即代码(Infrastructure-as-Code)和配置即代码(Configuration-as-Code)范式中通常是推荐的方式。

在执行对 StatefulSets 的回滚操作时,你必须充分意识到一些操作的后果,例如在保持状态的同时将容器镜像降级到早期版本。例如,如果你在升级到新版本时引入了数据模式变化,那么除非确保实现了数据状态的向下迁移,否则你将无法安全地回滚到早期版本!

在我们的示例中,如果你想将 StatefulSet 回滚到 mysql:8.3.0 镜像版本,你可以手动修改 YAML 清单文件,或者如果你使用源代码仓库,可以恢复该提交。然后,你需要做的就是对集群执行 kubectl apply 命令。

现在,在本章的最后一节,我们将为你提供一套在 Kubernetes 中管理 StatefulSets 的最佳实践。

StatefulSet 最佳实践

本节总结了在使用 Kubernetes 中的 StatefulSet 对象时已知的最佳实践。这个列表并非完整,但为你与 Kubernetes StatefulSet 的使用旅程提供了一个很好的起点。

对 StatefulSets 使用声明式对象管理

在 DevOps 领域,遵循声明式模型来引入基础设施和应用程序更新是一种良好的实践。使用声明式更新方式是像基础设施即代码(Infrastructure-as-Code)和配置即代码(Configuration-as-Code)这样的范式的核心概念。在 Kubernetes 中,你可以通过 kubectl apply 命令轻松地执行声明式更新,这个命令可以用于单个文件甚至整个 YAML 清单文件目录。

对于删除对象,仍然最好使用命令式操作。这样更具可预测性,且更不容易出错。声明式删除集群中的资源主要适用于 CI/CD 场景,在这些场景中,整个过程是完全自动化的。

同样的原则也适用于 StatefulSets。当你的 YAML 清单文件被版本化并保存在源代码控制库中时,执行升级或回滚是简单且可预测的。通常在生产环境中不会使用 kubectl rollout undo 方法和 kubectl set image deployment 命令。当有多人在集群中进行操作时,使用这些命令会变得更加复杂。

不要在 StatefulSets 中使用 TerminationGracePeriodSeconds 值为 0 的 Pod

Pod 的规格允许你设置 TerminationGracePeriodSeconds,该参数告诉 kubelet 在尝试终止 Pod 时,它应该允许多少时间来优雅地终止该 Pod。如果你将 TerminationGracePeriodSeconds 设置为 0,这将使 Pods 立即终止,这对于 StatefulSets 强烈不推荐。StatefulSets 通常需要优雅的清理,或者需要在容器被移除之前运行 preStop 生命周期钩子。否则,StatefulSet 的状态可能会变得不一致。请参考容器钩子文档 (https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks) 以了解更多信息。

在删除 StatefulSets 之前缩减规模

当你删除一个 StatefulSet 并打算稍后重新使用 PVC 时,你需要确保 StatefulSet 以有序的方式优雅终止,这样后续的重新部署就不会因为 PVC 状态不一致而失败。如果你对 StatefulSet 执行 kubectl delete 操作,所有的 Pods 会同时终止。这通常是不可取的,你应该首先将 StatefulSet 优雅地缩减到零副本,然后再删除 StatefulSet 本身。

确保 StatefulSet 回滚期间的状态兼容性

如果你打算使用 StatefulSet 回滚,你需要了解在持久化状态的同时,执行操作(例如降级到较早版本的容器镜像)所带来的后果。例如,如果你升级到新版本时引入了数据架构变化,那么除非你确保实现了状态数据的向下迁移,否则你将无法安全地回滚到早期版本。否则,你的回滚将仅重新创建具有旧版本容器镜像的 Pods,而这些 Pods 将无法正常启动,因为状态数据不兼容。

不要创建与现有 StatefulSet 标签选择器匹配的 Pods

可以创建与某些现有 StatefulSet 标签选择器匹配的 Pods。这可以通过裸 Pods 或其他 Deployment 或 ReplicaSet 来完成。这会导致冲突,Kubernetes 并不会阻止这种情况,并使现有的 StatefulSet 认为它已经创建了其他 Pods。结果可能是不可预测的,通常来说,你需要注意如何在集群中组织资源的标签。建议使用语义化标签。你可以在官方文档中了解更多关于这种方法的信息:kubernetes.io/docs/concepts/configuration/overview/#using-labels

使用远程存储作为 PV

使用 StatefulSets 时,确保使用远程存储非常重要。这意味着将应用程序的数据存储在一个单独的存储系统中,通常是网络附加存储NAS)、存储区域网络SAN)或云存储服务。通过远程存储数据,你可以确保从应用程序的任何实例(或集群中的任何节点)访问这些数据,即使该实例被替换或移动。这提供了数据持久性和弹性,帮助防止在 StatefulSet 失败或更新时的数据丢失。

定义活跃性和就绪性探针

对于有状态的应用程序,一个健康的 Pod 需要不仅仅是运行,还要能够访问和处理其持久化的状态。活跃性探针有助于确保这一功能。如果活跃性探针持续失败,这意味着 Pod 处理其状态的能力存在更深层次的问题。在这种情况下,重启 Pod 可能会触发恢复机制,或允许 StatefulSet 控制器将故障切换到另一个健康的、具有相同状态的 Pod。

StatefulSets 通常管理依赖于特定数据或配置在处理流量之前必须可用的服务。就绪探针可以根据 Pod 的状态来判断其是否准备就绪并且正常运行。通过防止流量进入未准备好的 Pod,你可以确保平稳的用户体验,并避免潜在的数据不一致问题。

监控你的 StatefulSets

监控你的 StatefulSets 的健康状态和性能至关重要。利用监控工具跟踪关键指标,如 Pod 重启、资源利用率和应用程序错误。这可以帮助你主动识别并解决潜在问题,以免影响应用程序的功能。

总结

本章演示了如何使用 StatefulSets 在 Kubernetes 上处理有状态的工作负载和应用程序。我们首先了解了在容器和 Kubernetes Pods 中持久化状态的方法,并基于此描述了如何使用 StatefulSet 对象来持久化状态。接下来,我们创建了一个示例 StatefulSet,并与一个无头服务一起使用。在此基础上,你学习了如何在 StatefulSets 中使用 PVCs 和 PVs 来确保 Pod 重启时状态得以持久化。然后,我们学习了如何扩展 StatefulSet,以及如何使用金丝雀发布阶段发布来引入更新。最后,我们提供了一组处理 StatefulSets 时的已知最佳实践。

在下一章中,你将学习如何管理需要在 Kubernetes 中每个节点上保持恰好一个 Pod 的特殊工作负载。我们将介绍一个新的 Kubernetes 对象:DaemonSet。

进一步阅读

加入我们在 Discord 的社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十三章:DaemonSet – 在节点上维护 Pod 单例

前几章已经解释并演示了如何使用最常见的 Kubernetes 控制器来管理 Pods,如 ReplicaSet、Deployment 和 StatefulSet。通常,当运行包含实际业务逻辑的云应用组件时,你将需要使用 Deployments 或 StatefulSets 来控制你的 Pods。在某些情况下,当你需要将批处理工作负载作为应用程序的一部分运行时,你将使用 Jobs 和 CronJobs。

然而,在某些情况下,你需要运行具有支持功能的组件,例如执行维护任务或聚合日志和度量数据。更具体地说,如果你有需要在集群中的每个节点上执行的任务,可以使用DaemonSet来执行。DaemonSet 的目的是确保每个节点(除非另有指定)运行单个副本的 Pod。如果你向集群添加新节点,它将自动调度一个 Pod 副本。同样,如果你从集群中移除节点,Pod 副本将被终止——DaemonSet 会执行所有必需的操作。

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

  • 介绍 DaemonSet 对象

  • 创建和管理 DaemonSets

  • DaemonSet 的常见使用案例

  • DaemonSet 的替代方案

技术要求

本章中,你将需要以下内容:

  • 已部署的 Kubernetes 集群。你可以使用本地或基于云的集群,但为了更好地理解这些概念,我们建议使用多节点Kubernetes 集群。

  • 安装在本地机器上的 Kubernetes CLI(kubectl)并已配置用于管理你的 Kubernetes 集群。

Kubernetes 集群部署(本地和基于云的)以及kubectl安装已经在第三章安装你的第一个 Kubernetes 集群中介绍过。

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter13

介绍 DaemonSet 对象

操作系统中的守护进程一词有着悠久的历史,简而言之,用于描述作为后台进程运行的程序,用户无法进行交互控制。在许多情况下,守护进程负责处理维护任务、提供网络请求或监控硬件活动。这些通常是你希望一直可靠地在后台运行的进程,从启动操作系统到关闭它的整个过程。

守护进程通常与类 Unix 操作系统相关联。在 Windows 中,你更常见到的术语是Windows 服务

想象一下,你需要一个程序在办公室的每台计算机上运行,以确保一切井然有序。在 Kubernetes 中,DaemonSet 就是为此而设。它们就像是 Pod 的特殊管理者,确保每个节点(Node)上都有一个 Pod 的副本运行。此外,在像 gRPC 这样的应用场景中,这一点尤为重要,因为 gRPC 可能需要在节点的文件系统上创建一个专用的套接字,单节点一个 Pod 的管理方式会更加简便。

这些 Pod 处理集群的关键任务,如:

  • 监控:监控每个节点的健康状况

  • 日志记录:收集有关每个节点及其上运行的 Pod 的状态信息

  • 存储管理:处理应用程序的存储空间请求

  • 网络管理:集群组件,如 kube-proxy 和容器网络接口CNI)(例如,Calico)用于连接性

随着集群的扩展(添加更多节点),DaemonSet 会自动为新的机器添加更多 Pod。相反,当节点被移除时,节点上的 Pod 会被自动清理。可以把它想象成一个自我调节的团队,始终确保每个节点都能得到所需的帮助。

以下图表显示了 DaemonSet 对象的高级细节。

图 13.1:Kubernetes 中的 DaemonSet

在简单的设置中,一个 DaemonSet 可以处理所有节点上某一特定任务(如监控)。更复杂的情况可能会使用多个 DaemonSet 来执行相同的任务,但根据节点类型的不同,配置或资源需求也会有所不同(例如,高性能机器与基础机器之间的差异)。

通过使用 DaemonSet,你可以确保你的 Kubernetes 集群在每个节点上都运行着必要的工具,保持系统顺畅、高效地运行。

你在前面章节中学到的关于 ReplicaSets、Deployments 和 StatefulSets 的知识在 DaemonSet 中基本适用。它的规范要求你提供 Pod 模板、Pod 标签选择器,此外,如果你希望仅在部分节点上调度 Pod,还可以选择提供节点选择器。

根据不同的情况,你可能不需要从其他 Pod 或外部网络与 DaemonSet 进行通信。例如,如果 DaemonSet 的任务只是定期清理节点上的文件系统,那么你可能不希望与这些 Pod 进行通信。如果你的应用场景需要与 DaemonSet Pod 进行任何入口或出口通信,那么你可以使用以下常见模式:

  • 将容器端口映射到主机端口:由于 DaemonSet 的 Pod 在集群节点上保证是单一副本,因此可以使用映射的主机端口。客户端必须知道节点的 IP 地址。

  • 将数据推送到不同的服务:在某些情况下,DaemonSet 仅负责向其他服务发送更新,而无需允许入口流量。

  • 无头服务匹配 DaemonSet Pod 标签选择器:这与 StatefulSets 的情况类似,你可以使用集群 DNS,通过无头服务的 DNS 名称来检索多个 A 记录,以获取 Pod。

  • 正常服务匹配 DaemonSet Pod 标签选择器:较少情况下,你可能需要访问 DaemonSet 中的 任意 Pod。使用普通的 Service 对象,比如 ClusterIP 类型,将允许你与 DaemonSet 中的随机 Pod 进行通信。

正如我们所讨论的,DaemonSets 确保了关键服务的 Pods 会在所有或选定的节点上运行。接下来我们将探讨 DaemonSets 如何有效地进行调度。

如何调度 DaemonSet Pods

DaemonSets 保证每个符合条件的节点上都会运行一个 Pod。DaemonSet 控制器根据节点亲和性规则创建 Pods,目标是特定节点。这确保了 DaemonSet 的 Pods 只会调度到满足条件的节点上,这对于在复杂应用程序设置中定位特定类型的节点非常有用。默认的调度器随后将 Pod 绑定到目标节点,如果资源不足,可能会抢占现有 Pods。虽然可以指定自定义调度器,但最终是 DaemonSet 控制器确保 Pod 的位置与所需的节点亲和性一致。

在接下来的部分,我们将学习如何检查 Kubernetes 集群中的 DaemonSet 资源。

检查 DaemonSets

一旦部署了 Kubernetes 集群,你可能已经在集群支持组件(如 DNS 服务或 CNI)中使用了一些作为 DaemonSets 部署的组件。

minikube Kubernetes cluster:
$ minikube start \
  --driver=virtualbox \
  --nodes 3 \
  --cni calico \
  --cpus=2 \
  --memory=2g \
  --kubernetes-version=v1.31.0 \
  --container-runtime=containerd 

集群创建完成后,验证集群中的节点如下:

$ kubectl get nodes
NAME           STATUS   ROLES           AGE     VERSION
minikube       Ready    control-plane   3m28s   v1.31.0
minikube-m02   Ready    <none>          2m29s   v1.31.0
minikube-m03   Ready    <none>          91s     v1.31.0 

现在,让我们检查新安装的系统中是否有可用的 DaemonSet:

$ kubectl get daemonsets -A
NAMESPACE     NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-system   calico-node   3         3         3       3            3           kubernetes.io/os=linux   63m
kube-system   kube-proxy    3         3         3       3            3           kubernetes.io/os=linux   63m 
-A or --all namespaces lists the requested objects across all namespaces.

如果你按照相同的方法创建 minikube 集群,你会看到类似的输出,包含 calico-nodekube-proxy,它们作为 DaemonSets 部署。(你也可以在其他 Kubernetes 集群中安装 Calico,并按照这里的剩余步骤进行操作。)你可能已经注意到,我们之前已经在 minikube 集群中启用了 Calico 作为 CNI 插件。当 Calico 用于 Kubernetes 网络时,通常会作为 DaemonSet 部署。

暂时忽略 kube-proxy DaemonSet,因为 minikube 将 kube-proxy 作为 DaemonSet 运行。这保证了负责管理集群内网络流量的 kube-proxy 在你的 minikube 环境中的每台机器上始终运行。

现在,让我们检查由 calico-node DaemonSet 部署的 Pods。

$ kubectl get po -n kube-system -o wide|grep calico
calico-kube-controllers-ddf655445-jx26x   1/1     Running   0          82m   10.244.120.65    minikube       <none>           <none>
calico-node-fkjxb                         1/1     Running   0          82m   192.168.59.126   minikube       <none>           <none>
calico-node-nrzpb                         1/1     Running   0          81m   192.168.59.128   minikube-m03   <none>           <none>
calico-node-sg66x                         1/1     Running   0          82m   192.168.59.127   minikube-m02   <none>           <none> 

从这个输出中,我们可以看到:

  • Calico Pods 被部署在所有 minikube 节点上。这些 Pods 分布在不同的节点上,对应你的 minikube 虚拟机(minikubeminikube-m02minikube-m03)。这表明 Calico 正在使用 DaemonSet 确保每个节点上都有一个 Pod 运行。

  • Pod calico-kube-controllers-ddf655445-jx26x 是 Calico CNI 的控制器。

由于 Calico DaemonSet 是由 minikube 安装的,因此我们不会深入探讨这一方面。但在下一部分,我们将学习如何从头开始部署 DaemonSet 并详细探索它。

创建和管理 DaemonSets

为了演示 DaemonSet 如何工作,我们将使用 Fluentd Pods。Fluentd 是一个流行的开源日志聚合器,用于将来自各种来源的日志数据集中化。它高效地收集、过滤和转换日志消息,然后将它们转发到不同的目的地进行分析和存储。

为了访问 DaemonSet 端点,我们将使用一个 无头 服务,类似于我们在 第十二章 中为 StatefulSet 所做的。DaemonSets 的大多数实际使用案例相对复杂,涉及将各种系统资源挂载到 Pods。为了展示基本原理,我们将使 DaemonSet 示例尽可能简单。

如果你想尝试另一个 DaemonSet 示例,我们提供了一个可以工作的 Prometheus node-exporter 示例,该示例作为 DaemonSet 部署,并通过一个无头服务进行访问:node-exporter.yaml。在本节中,唯一的区别是,你需要使用 node-exporter 作为服务名称,使用端口 9100,并在请求中添加 /metrics 路径,使用 wget 发送请求。这个 DaemonSet 会在端口 9100/metrics 路径下以 Prometheus 数据模型 格式公开节点指标。

接下来,我们将通过所有创建 DaemonSet 所需的 YAML 清单并将其应用于集群。

创建 DaemonSet

作为最佳实践,让我们采用声明式方式来创建 DaemonSet 以进行动手操作。首先,让我们看一下名为 Fluentd-daemonset.yaml 的 DaemonSet YAML 清单文件。

YAML 的第一部分用于为日志创建一个独立的命名空间。

---
apiVersion: v1
kind: Namespace
metadata:
  name: logging 

之后,我们将展示 DaemonSet 声明的详细信息如下。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
# (to be continued in the next paragraph) 

前面文件的第一部分包含 DaemonSet 的 metadata 和 Pod 标签 selector,与您在 Deployments 和 StatefulSets 中看到的非常相似。文件的第二部分展示了 DaemonSet 将使用的 Pod 模板:

# (continued)
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      containers:
        - name: fluentd-elasticsearch
          image: quay.io/fluentd_elasticsearch/fluentd:v4.7
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:
            - name: varlog
              mountPath: /var/log
      terminationGracePeriodSeconds: 30
      volumes:
        - name: varlog
          hostPath:
            path: /var/log 

正如你所看到的,DaemonSet 规格的结构与 Deployments 和 StatefulSets 的结构类似。大体思路是相同的;你需要配置 Pod 模板,并使用合适的标签选择器来匹配 Pod 标签。请注意,您在此处不会看到 replicas 字段,因为集群中运行的 Pods 数量将取决于集群中的节点数。DaemonSet 规格有两个主要组件:

  • spec.selector:一个标签选择器,用于定义如何识别 DaemonSet 所拥有的 Pods。这可以包括 基于集合基于相等性 的选择器。

  • spec.template:这定义了 Pod 创建的模板。metadata 中使用的标签必须与 selector 匹配。

也很常见通过指定.spec.template.spec.nodeSelector.spec.template.spec.tolerations来控制 DaemonSet Pods 部署到哪些节点。我们将在第十九章调度 Pod 的高级技术中详细讲解 Pod 调度。此外,你还可以指定.spec.updateStrategy.spec.revisionHistoryLimit.spec.minReadySeconds,这些与 Deployment 对象的配置类似。

如果你运行混合的 Linux-Windows Kubernetes 集群,Node 选择器或 Node 亲和性在 DaemonSets 中的一个常见用例是确保 Pod 仅调度到 Linux 节点或仅调度到 Windows 节点。这样做是有意义的,因为容器运行时和操作系统在这些节点之间非常不同。

另外,请注意卷挂载行,Fluentd Pods 将访问宿主机(Pod 所在的主机)上的/var/log目录,以便 Fluentd 可以处理数据并将其发送到日志聚合器。

请注意,在实际部署中,我们需要向 Fluentd Pods 提供目标 Elasticsearch 服务器,以便 Fluentd 可以发送日志。在我们的演示中,我们没有覆盖 Elasticsearch 的设置,你现在可以忽略这一部分。

你可以通过环境变量将这些参数传递给容器,如下所示。(请参考fluentd-daemonset.yaml文件了解更多信息。)

 env:
            - name:  FLUENT_ELASTICSEARCH_HOST
              value: "elasticsearch-logging"
            - name:  FLUENT_ELASTICSEARCH_PORT
              value: "9200" 

我们有所有必要的 YAML 清单文件进行演示,现在可以继续将这些清单应用到集群中。请按照以下步骤操作:

  1. 使用以下命令创建fluentd-elasticsearch DaemonSet:

    $ kubectl apply -f fluentd-daemonset.yaml
    namespace/logging created 
    daemonset.apps/fluentd-elasticsearch created 
    
  2. 现在,你可以使用kubectl describe命令来观察 DaemonSet 的创建过程:

    $ kubectl describe daemonset fluentd-elasticsearch -n logging 
    

或者,你可以使用ds作为daemonset的缩写,在使用kubectl命令时。

  1. 使用kubectl get pods命令并加上-w选项,你可以看到每个集群中的节点都会调度一个 Pod,如下所示:

    $ kubectl get po -n logging -o wide
    NAME                          READY   STATUS    RESTARTS   AGE     IP               NODE           NOMINATED NODE   READINESS GATES
    fluentd-elasticsearch-cs4hm   1/1     Running   0          3m48s   10.244.120.68    minikube       <none>           <none>
    fluentd-elasticsearch-stfqs   1/1     Running   0          3m48s   10.244.205.194   minikube-m02   <none>           <none>
    fluentd-elasticsearch-zk6pt   1/1     Running   0          3m48s   10.244.151.2     minikube-m03   <none>           <none> 
    

在我们的例子中,集群中有三个节点,因此正好创建了三个 Pod。

我们已经成功部署了 DaemonSet,现在可以验证它是否按预期工作。确保 Fluentd Pods 能够访问 Kubernetes 节点的日志文件。为此,登录到一个 Fluentd Pod 并检查/var/log目录。

$ kubectl exec -n logging -it fluentd-elasticsearch-cs4hm -- /bin/bash
root@fluentd-elasticsearch-cs4hm:/# ls -l /var/log/
total 20
drwxr-xr-x  3 root root 4096 May 29 10:56 calico
drwxr-xr-x  2 root root 4096 May 29 12:40 containers
drwx------  3 root root 4096 May 29 10:55 crio
drwxr-xr-x  2 root root 4096 May 29 11:53 journal
drwxr-x--- 12 root root 4096 May 29 12:40 pods
root@fluentd-elasticsearch-cs4hm:/# 

这展示了 DaemonSet Pods 在 Kubernetes 中调度的最重要原理。

最佳实践是为节点使用适当的污点容忍,以实现 DaemonSets。我们将在第十九章调度 Pod 的高级技术中学习污点和容忍。

让我们在下一节中了解一些 DaemonSet 的高级配置。

在 Kubernetes 中优先考虑关键的 DaemonSets

在 Kubernetes 中使用 DaemonSets 管理关键系统组件时,确保其连续运行至关重要。以下是如何利用 Pod 优先级和 PriorityClasses 来保证这些关键 Pods 不会被低优先级任务中断的方法。

Kubernetes 为每个 Pod 分配一个优先级,决定其在集群中的相对重要性。较高优先级的 Pods 被认为比低优先级的 Pods 更重要。

通过为 DaemonSet 分配更高的 PriorityClass,你可以提升其 Pods 的重要性。这样可以确保这些关键 Pods 在资源紧张时不会被调度器抢占,以腾出空间给低优先级的 Pods。

PriorityClass 定义了 Pods 的特定优先级。此类中的值可以从负整数到最大 10 亿。较高的值表示较高的优先级。

PriorityClass 的 YAML 示例定义如下。

# priorityclass.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: fluentd-priority
value: 100000  # A high value for criticality
globalDefault: false  # Not a default class for all Pods
description: "Fluentd Daemonset priority class" 

一旦创建了 PriorityClass,你可以在 DaemonSet 配置中按如下方式使用它。

spec:
  template:
    spec:
      priorityClassName: fluentd-priority 

作为参考,像 kube-proxy 和集群 CNI(如 Calico)这样的系统组件通常使用内置的 system-node-critical PriorityClass。此类拥有最高优先级,确保这些重要 Pods 在任何情况下都不会被驱逐。

现在,我们将展示如何修改 DaemonSet 以便为 Pods 推出新版本的容器镜像。

修改 DaemonSet

更新 DaemonSet 可以通过类似于 Deployments 的方式进行。如果修改了 DaemonSet 的 Pod 模板,这将触发 DaemonSet 新版本的 滚动更新,并根据其 updateStrategy 进行。提供了两种策略:

  • RollingUpdate:默认策略,允许以受控的方式推出你的守护进程的新版本。它类似于 Deployments 中的滚动更新,你可以定义 .spec.updateStrategy.rollingUpdate.maxUnavailable 来控制在滚动过程中集群中最多有多少 Pods 是不可用的(默认为 1),以及 .spec.minReadySeconds(默认为 0)。可以保证,在滚动过程中的每个节点上,DaemonSet 至多只有一个 Pod 处于运行状态。

  • OnDelete:此策略实现了 Kubernetes 1.6 之前 StatefulSet 更新的旧行为。在这种策略下,DaemonSet 不会通过重新创建 Pods 来自动更新。你需要手动删除节点上的 Pod,才能应用新的 Pod 模板。此策略适用于在继续下一个节点之前,需要进行额外的手动操作或验证的场景。

新的 DaemonSet 版本的滚动更新可以通过与 Deployment 对象类似的方式进行控制。你可以使用 kubectl rollout status 命令并通过 kubectl rollout undo 命令执行 强制 回滚。以下是如何 声明性 地将 DaemonSet Pod 中的容器镜像更新到新版本的演示:

  1. 修改 fluentd-daemonset.yaml YAML 清单文件,使其在模板中使用 quay.io/fluentd_elasticsearch/fluentd:v4.7.5 容器镜像:

    ...
          containers:
            - name: fluentd-elasticsearch
              **image: quay.io/fluentd_elasticsearch/fluentd:v4.7.5**
    ... 
    
  2. 将清单文件应用到集群中:

    $ kubectl apply -f fluentd-daemonset.yaml
    namespace/logging unchanged
    daemonset.apps/fluentd-elasticsearch configured 
    
  3. 紧接着,使用 kubectl rollout status 命令实时查看进度:

    $ kubectl rollout status ds -n logging
    Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 2 out of 3 new pods have been updated...
    Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 2 out of 3 new pods have been updated...
    Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 2 of 3 updated pods are available...
    daemon set "fluentd-elasticsearch" successfully rolled out 
    
  4. 同样,使用 kubectl describe 命令,你可以查看 DaemonSet 的事件,准确显示 Pod 重建的顺序:

    $ kubectl describe ds -n logging
    Name:           fluentd-elasticsearch
    Selector:       name=fluentd-elasticsearch
    ...<removed for brevity>...
    Events:
      Type    Reason            Age    From                  Message
      ----    ------            ----   ----                  -------
    ...<removed for brevity>...
    -elasticsearch-24v2z
      Normal  SuccessfulDelete  5m52s  daemonset-controller  Deleted pod: fluentd-elasticsearch-zk6pt
      Normal  SuccessfulCreate  5m51s  daemonset-controller  Created pod: fluentd-elasticsearch-fxffp 
    

你可以看到 Pod 被一个个替换。

你可以通过 kubectl set image ds fluentd-elasticsearch fluentd-elasticsearch=quay.io/fluentd_elasticsearch/fluentd:v4.7.5 -n logging 命令强制更改 DaemonSet 容器镜像。此方法仅推荐用于非生产环境。

此外,如果新节点加入集群,DaemonSet 会自动创建 Pod(前提是节点符合选择器和亲和性参数)。如果一个节点从集群中移除,相应的 Pod 也会被终止。如果你修改节点上的标签或污点使其符合 DaemonSet 的条件,那么会为该节点创建一个新的 Pod。如果你修改节点的标签或污点,使其不再符合 DaemonSet 的条件,现有的 Pod 将被终止。

接下来,我们将学习如何回滚 DaemonSet 更新。

回滚 DaemonSet

正如我们在前面的章节中所学,你也可以通过以下 kubectl rollback 命令回滚 DaemonSet。

$ kubectl rollout undo daemonset fluentd-elasticsearch -n logging 

然而,强烈建议在生产环境中通过更新 YAML 并以声明式方式应用配置。

接下来,我们将展示如何删除 DaemonSet。

删除 DaemonSet

要删除 DaemonSet 对象,有两种可能性:

  • 删除 DaemonSet 及其所拥有的 Pod。

  • 删除 DaemonSet 并保持 Pod 不受影响。

要删除 DaemonSet 及其 Pod,可以使用常规的 kubectl delete 命令,如下所示:

$ kubectl delete ds fluentd-elasticsearch -n logging 

你会看到 Pod 首先会被终止,然后 DaemonSet 会被删除。

现在,如果你只想删除 DaemonSet,可以在 kubectl delete 命令中使用 --cascade=orphan 选项:

$ kubectl delete ds fluentd-elasticsearch -n logging --cascade=orphan 

执行此命令后,如果你检查集群中的 Pod,你仍然会看到所有由 fluentd-elasticsearch DaemonSet 所管理的 Pod。

如果你正在使用 kubectl drain 命令进行节点排空,并且该节点正在运行由 DaemonSet 管理的 Pod,你需要传递 --ignore-daemonsets 标志,以完全排空该节点。

现在让我们来看看 Kubernetes 中 DaemonSet 的最常见使用场景。

DaemonSet 的常见使用场景

到此时,你可能会想,DaemonSet 实际上是做什么的,现实中的使用场景有哪些。一般来说,DaemonSets 要么用于集群的基础功能,没有这些功能集群无法使用,要么用于执行维护或数据收集的辅助工作负载。我们在以下几点中总结了 DaemonSets 的常见和有趣的使用场景:

  • 根据你的集群部署方式,kube-proxy 核心服务可能会作为 DaemonSet 而不是常规操作系统服务进行部署。例如,在 Azure Kubernetes ServiceAKS)的情况下,你可以使用 kubectl describe ds -n kube-system kube-proxy 命令查看该 DaemonSet 的定义。这是一个典型的示例,展示了需要在集群中每个节点上以单例方式运行的骨干服务。你还可以在这里看到 kube-proxy 的示例 YAML 清单:github.com/kubernetes/kubernetes/blob/master/cluster/addons/kube-proxy/kube-proxy-ds.yaml

  • 另一个作为 DaemonSets 运行的基础服务示例是运行 CNI 插件和代理程序,用于维护 Kubernetes 集群中的网络。我们已经在本章开始时通过 minikube 集群测试过 Calico CNI DaemonSet。另一个很好的 DaemonSet 示例是 Flannel 代理(github.com/flannel-io/flannel/blob/master/Documentation/kube-flannel.yml),它在每个节点上运行,负责从更大的、预配置的地址空间中为每个主机分配一个子网租约。当然,这取决于集群中安装的网络类型。

  • 集群存储守护进程通常会作为 DaemonSets 部署。一个常见的守护进程示例是 对象存储守护进程OSD),它是 Ceph 的一部分,Ceph 是一个分布式的对象、块和文件存储平台。OSD 负责在每个节点的本地文件系统上存储对象,并通过网络提供访问。你可以在这里找到一个示例清单文件(作为 Helm 图表模板的一部分):github.com/ceph/ceph-container/blob/master/examples/helm/ceph/templates/osd/daemonset.yaml

  • Kubernetes 中的 Ingress 控制器有时作为 DaemonSet 部署。我们将在 第二十一章高级 Kubernetes:流量管理、多集群策略及更多 中更详细地探讨 Ingress。例如,当你在集群中部署 nginx 作为 Ingress 控制器时,你可以选择将其作为 DaemonSet 部署:github.com/nginxinc/kubernetes-ingress/blob/master/deployments/daemon-set/nginx-ingress.yaml。如果你在裸机服务器上进行 Kubernetes 集群部署,将 Ingress 控制器作为 DaemonSet 部署尤其常见。

  • 日志收集和聚合代理通常作为 DaemonSet 部署。例如,fluentd 可以作为 DaemonSet 在集群中部署。你可以在官方仓库中找到多个包含示例的 YAML 清单文件:github.com/fluent/fluentd-kubernetes-daemonset

  • 用于收集节点指标的代理非常适合作为 DaemonSet 部署。一个著名的例子是 Prometheus 的 node-exportergithub.com/prometheus-operator/kube-prometheus/blob/main/manifests/nodeExporter-daemonset.yaml

这只是其中一部分——正如你所看到的,DaemonSet 是为设计在 Kubernetes 集群上运行的工作负载的工程师提供的另一个构建模块。在许多情况下,DaemonSet 是集群的隐性支柱,使其能够完全运行。

现在,让我们学习一下 DaemonSet 实现的推荐最佳实践。

DaemonSet 最佳实践

DaemonSet 是 Kubernetes 中强大的工具,用于管理需要在每个节点上运行的 Pods。但为了确保它们按预期工作,有一些关键点需要记住:

  • 资源请求与限制:简要提到为 DaemonSet Pods 设置适当的资源请求与限制的重要性,有助于用户有效地管理资源分配。这可以帮助避免集群中其他 Pods 资源的饥饿问题。

  • 清晰且分离:通过将每个 DaemonSet 放在其自己的命名空间中来组织它们。这可以保持整洁,并简化资源管理。

  • 调度智能:创建 DaemonSet 时,建议使用 preferredDuringSchedulingIgnoredDuringExecution 而不是 requiredDuringSchedulingIgnoredDuringExecution。第一个选项在初始时如果没有足够的节点可用时,提供更多的灵活性。

  • 等待就绪(可选):你可以在 Pod 模式中使用 minReadySeconds 设置。这告诉 Kubernetes 在更新期间创建新 Pod 之前,等待一段时间。这有助于确保所有现有 Pods 都处于健康状态,然后再添加新 Pods。

  • 监控与日志:简要提一下监控与日志对于 DaemonSet Pods 的重要性是有帮助的。这允许用户跟踪 DaemonSets 的健康状态和性能,并识别潜在问题。

  • 始终运行:确保你的 DaemonSet Pods 的 重启策略 设置为 Always(或保持未指定)。这保证了 Pods 在崩溃时会自动重启。

  • 高优先级:为你的 DaemonSet Pods 设置高优先级(例如 10000),以确保它们获得所需的资源,并且不会被其他 Pods 驱逐。

  • 匹配标签:定义一个 Pod 选择器,匹配你的 DaemonSet 模板的标签。这样可以确保 DaemonSet 部署的 Pods 是你所期望的。

通过遵循这些最佳实践,你可以配置你的 DaemonSets 使其平稳运行,并保持 Kubernetes 集群的最佳性能。

接下来,让我们讨论一下使用 DaemonSets 的可能替代方案。

DaemonSets 的替代方案

使用 DaemonSets 的原因非常简单——你希望在集群中的每个节点上都有一个具有特定功能的 Pod。然而,有时你应该考虑其他可能更适合你需求的方法:

  • 在日志收集场景中,你需要评估是否希望基于 DaemonSets 或 sidecar 容器模式来设计你的日志管道架构。两者各有优缺点,但通常来说,运行 sidecar 容器可能更容易实现,且更具稳健性,尽管可能需要更多的系统资源。

  • 如果你只是想运行定期任务,并且不需要在集群中的每个节点上都执行,使用 Kubernetes CronJobs 可能是更好的解决方案。再次强调,了解实际的用例是什么,以及是否必须在每个节点上运行独立的 Pod 是非常重要的。

  • 操作系统守护进程(例如,Ubuntu 中由 systemd 提供的守护进程)可以用来执行与 DaemonSets 类似的任务。该方法的缺点是,你不能像管理 Kubernetes 集群一样使用相同的工具(例如 kubectl)来管理这些本地守护进程。但同时,你不依赖于任何 Kubernetes 服务,在某些情况下这可能是个好处。

  • 静态 Pods (kubernetes.io/docs/tasks/configure-pod-container/static-pod/) 可以用来实现类似的结果。这种类型的 Pod 是基于 kubelet 监视的特定目录中的静态清单文件创建的。静态 Pods 无法通过 kubectl 管理,最常用于集群引导功能。

最后,我们现在可以总结我们关于 DaemonSets 的知识。

摘要

在本章中,你已经学习了如何在 Kubernetes 中使用 DaemonSets,以及它们是如何用于管理必须作为单例在每个节点上运行的特殊类型的工作负载或进程。你首先创建了一个示例 DaemonSet,并了解了其规范中最重要的部分。接下来,你练习了如何向集群推出 DaemonSet 的新版本,并观察了如何监控该部署。此外,我们还讨论了这种特殊类型的 Kubernetes 对象的最常见使用案例以及你可以考虑的替代方案。

这是我们在本书这一部分讨论的最后一种 Pod 管理控制器。在本书的下一部分,我们将探讨一些更高级的 Kubernetes 使用方法,从 Helm Charts 开始,然后是 Kubernetes Operators。

深入阅读

)

)

)

加入我们的社区 Discord

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十四章:使用 Helm Charts 和 Operators

在 Kubernetes 生态系统中,当你希望让应用程序便于客户下载和安装,或者希望在团队之间共享时,管理应用程序的重新分配和依赖关系管理非常重要。与像 APT 或 YUM 这样的 Linux 包管理器不同,Kubernetes 工具(如 Helm)有一个显著的区别,那就是它们是通用的,并且不依赖于特定的 Kubernetes 发行版。Helm 通过将多个资源打包成 charts,使应用程序的分发创建变得简单,支持在不同环境中轻松地重用和定制应用程序。这减轻了在应用程序部署中管理大量 YAML 清单的难题,从而减轻了开发者或运维人员的负担。

Kubernetes Operators 通过增加自动化管理应用生命周期的能力(如升级、故障转移和备份),并维护一致的配置,从而进一步补充 Helm。Helm 和 Operators 共同帮助解决在 Kubernetes 中扩展和管理应用程序的一些挑战。本章将深入探讨这些工具:Helm chart 开发,如何安装流行组件,以及 Kubernetes 应用程序的成功重分发。

本章将涵盖以下主题:

  • 了解 Helm

  • 使用 Helm 向 Kubernetes 发布软件

  • 使用 Helm Charts 安装 Kubernetes Dashboard

  • 介绍 Kubernetes Operators

  • 使用 Prometheus 和 Grafana 启用 Kubernetes 监控

技术要求

本章所需内容:

  • 部署的 Kubernetes 集群。如果可能的话,建议使用多节点的或基于云的 Kubernetes 集群。你需要确保为 Kubernetes 集群分配了 CPU、内存和存储资源,以确保可以调度多个 Pods(例如,你可以使用命令 minikube start --kubernetes-version=1.31.0 --cpus=4 --memory=8g 创建一个较大的 minikube 集群)。

  • 本地机器上安装并配置的 Kubernetes 命令行接口CLI)(kubectl),用于管理你的 Kubernetes 集群。

基本的 Kubernetes 集群部署(本地和基于云的)和 kubectl 安装已经在第三章《安装你的第一个 Kubernetes 集群》中讲解。接下来的第151617 章将为你提供如何在不同云平台上部署一个功能完善的 Kubernetes 集群的概览。你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter14

)

了解 Helm

这种方法通常作为展示如何将给定应用程序作为容器运行在 Kubernetes 上的基本示例。然而,直接共享原始 YAML 清单有很多缺点:

  • YAML 模板中的所有值都是硬编码的。这意味着,如果你想更改 Service 对象目标的副本数或存储在 ConfigMap 对象中的值,你需要通过清单文件,找到你想要配置的值,然后进行编辑。同样,如果你想将清单部署到与创建者原意不同的集群命名空间,你也需要编辑所有的 YAML 文件。此外,除非创建者有文档说明,否则你并不知道哪些 YAML 模板中的值是打算由创建者配置的。

  • 部署过程因应用程序而异。没有标准化的方法来定义创建者提供哪些 YAML 清单,以及你需要手动部署哪些组件。

  • 没有依赖管理。例如,如果你的应用程序需要在集群中以 StatefulSet 形式运行的 MySQL 服务器,你要么需要自己部署它,要么依赖应用程序的创建者提供 MySQL 服务器的 YAML 清单。

如果你不使用 Application Store 或类似 ChocolateyAPTYUMDNF 等包管理器,其他应用程序也有类似的情况。有些下载的应用程序会提供作为 setup.sh 脚本文件的安装程序,有些提供 .exe 文件,有些是 .msi 文件,还有的只是 .zip 文件,需要你自行解压并配置。

在 Kubernetes 中,你可以使用Helmhelm.sh),它是最流行的 Kubernetes 应用程序和服务包管理器之一。如果你熟悉流行的包管理器,如 APT、yum、npm 或 Chocolatey,你会发现 Helm 中的许多概念非常相似且易于理解。以下是 Helm 中三个最重要的概念:

  • 图表是 Helm 。当你使用 Helm CLI 时,安装的就是这个图表。一个 Helm 图表包含了所有需要部署特定应用程序到集群上的 Kubernetes YAML 清单。请注意,这些 YAML 清单可能是参数化的,因此你可以轻松地注入由安装图表的用户提供的配置值。

  • 仓库是 Helm 图表的存储位置,用于收集和共享图表。它们可以是公开的或私有的 – 有多个公共仓库可以浏览,你可以在 Artifact Hub 上找到它们(artifacthub.io)。私有仓库通常用于在同一个产品的不同团队之间分发 Kubernetes 上运行的组件。

  • 发布 是已安装并在 Kubernetes 集群中运行的 Helm chart 的实例。这是您通过 Helm CLI 管理的内容,例如通过升级或卸载它。您可以在同一集群上多次安装同一个 chart,并拥有多个通过发布名称唯一标识的发布。

简而言之,Helm charts 包含可以参数化的 YAML 清单,您将其存储在 Helm 仓库中以供分发。当您安装 Helm chart 时,在您的集群中会创建一个 Helm 发布,您可以进一步管理它。

让我们快速总结一些 Helm 的常见使用场景:

  • 将流行的软件部署到您的 Kubernetes 集群。这使得在 Kubernetes 上进行开发变得更加容易——您可以在几秒钟内将第三方组件部署到集群中。相同的方法也可以用于生产集群。您无需依赖为这些第三方组件编写自己的 YAML 清单。

  • Helm charts 提供依赖管理功能。如果 chart A 需要先安装 chart B,并带有特定参数,Helm 支持这种语法。

  • 共享您自己的应用程序作为 Helm charts。这可以包括将产品打包供最终用户使用,或将 Helm 作为您产品中微服务的内部包和依赖管理器。

  • 确保应用程序获得适当的升级。Helm 有自己的一套升级 Helm 发布的流程。

  • 为您的需求配置软件部署。Helm charts 基本上是 Kubernetes 对象清单的通用 YAML 模板,可以进行参数化。Helm 使用 Go 模板(godoc.org/text/template)进行参数化。

目前,Helm 以二进制客户端(库)的形式分发,具有类似于 kubectl 的 CLI。您使用 Helm 执行的所有操作都不需要在 Kubernetes 集群上安装任何额外组件。

请注意,Helm 的架构在 Helm 3.0.0 版本发布时发生了变化。以前,Helm 的架构是不同的,需要在 Kubernetes 上运行一个名为 Tiller 的特殊专用服务。这造成了各种问题,例如与 基于角色的访问控制 (RBAC) 和在集群内运行的具有提升权限的 Pods 相关的安全问题。您可以在官方 FAQ 中阅读更多关于最新主要版本 Helm 与之前版本之间的区别:

https://helm.sh/docs/faq/#changes-since-helm-2

这对于您找到的任何仍然提到 Tiller 的在线指南非常有用——它们很可能是为 Helm 的旧版本编写的。

现在我们已经了解了 Helm 及其重要概念,我们将安装 Helm 并从 Artifact Hub 部署一个简单的 Helm chart,以验证它在您的集群中是否正常工作。

使用 Helm 向 Kubernetes 发布软件

在本节中,你将学习如何安装 Helm,并通过部署示例 Helm 图表来测试安装。Helm 以二进制发布的形式提供(github.com/helm/helm/releases),支持多个平台。你可以使用这些版本,或者参考以下指南通过包管理器在你所选的操作系统上安装。

在 Linux 上安装 Helm

在 Fedora 上安装 Helm,你需要确保默认的 Fedora 仓库已配置并正常工作:

$ sudo dnf repolist | grep fedora
fedora                                         Fedora 39 – x86_64 

然后按如下方式安装 Helm:

$ sudo dnf install helm 

安装完成后,你可以验证已安装 Helm 包的版本:

$ helm version
version.BuildInfo{Version:"v3.11", GitCommit:"", GitTreeState:"", GoVersion:"go1.21rc3"} 

也可以使用脚本安装 Helm(https://helm.sh/docs/intro/install/#from-script),该脚本会自动检测平台、下载最新的 Helm,并将其安装到你的机器上。

安装完成后,你可以继续进行本节中的 部署示例图表 – WordPress

在 Windows 上安装 Helm

在 Windows 上安装 Helm,最简单的方法是使用 Chocolatey 包管理器。如果你之前没有使用过 Chocolatey,可以在官方文档中找到更多详细信息和安装指南,网址是 chocolatey.org/install

在 PowerShell 或命令行中执行以下命令来安装 Helm:

PS C:\Windows\system32> choco install kubernetes-helm
PS C:\Windows\system32> helm version
version.BuildInfo{Version:"v3.15.0-rc.2", GitCommit:"c4e37b39dbb341cb3f716220df9f9d306d123a58", GitTreeState:"clean", GoVersion:"go1.22.3"} 

安装完成后,你可以继续进行本节稍后的 部署示例图表 – WordPress

在 macOS 上安装 Helm

在 macOS 上安装 Helm,你可以使用标准的 Homebrew 包管理器。使用以下命令来安装 Helm 配方:

$ brew install helm 

通过尝试从命令行获取 Helm 版本来验证安装是否成功:

 $ helm version
version.BuildInfo{Version:"v3.16.2", GitCommit:"13654a52f7c70a143b1dd51416d633e1071faffb", GitTreeState:"dirty", GoVersion:"go1.23.2"} 

安装完成后,我们可以部署一个示例图表来验证 Helm 是否在你的 Kubernetes 集群上正常工作。

从二进制发布版安装

也可以直接从二进制文件安装最新的 Helm 包。你需要从发布页面(github.com/helm/helm/releases)找到最新或所需版本的二进制文件,并根据你的操作系统下载它。在以下示例中,我们将展示如何在 Fedora 工作站上从二进制文件安装最新版本的 Helm:

下载并安装 Helm:

$ wget https://get.helm.sh/helm-v3.15.1-linux-amd64.tar.gz
$ tar -zxvf helm-v3.15.1-linux-amd64.tar.gz
linux-amd64/
linux-amd64/README.md
linux-amd64/LICENSE
linux-amd64/helm
$ sudo mv linux-amd64/helm /usr/local/bin/helm
$ helm version
version.BuildInfo{Version:"v3.15.1", GitCommit:"e211f2aa62992bd72586b395de50979e31231829", GitTreeState:"clean", GoVersion:"go1.22.3"} 

现在,我们将在下一节中通过 部署示例图表 – WordPress 来测试 Helm 包。

部署示例图表 – WordPress

默认情况下,Helm 没有配置任何仓库。一个不再推荐的做法是添加 stable 仓库,以便浏览最受欢迎的 Helm 图表:

$ helm repo add stable https://charts.helm.sh/stable
"stable" has been added to your repositories 

为部署添加随机的 Helm 图表仓库可能会带来严重的安全风险。必须进行安全审计,确保在 Kubernetes 环境中仅部署受信任且安全的负载。

请注意,大多数图表现在正处于弃用过程中,因为它们被迁移到不同的 Helm 仓库,并由原始创建者维护。如果你尝试使用helm search repo命令搜索可用的 Helm 图表,你会看到这一点:

$ helm search repo stable|grep -i deprecated|head
stable/acs-engine-autoscaler            2.2.2           2.1.1                   DEPRECATED Scales worker nodes within agent pools
stable/aerospike                        0.3.5           v4.5.0.5                DEPRECATED A Helm chart for Aerospike in Kubern...
stable/airflow                          7.13.3          1.10.12                 DEPRECATED ...<removed for brevity>... 

相反,新的推荐方法是使用helm search hub命令,它允许你直接从 CLI 浏览 Artifact Hub:

$ helm search hub|head
URL                                                     CHART VERSION                                           APP VERSION                                             DESCRIPTION                                      
https://artifacthub.io/packages/helm/mya/12factor       24.1.2                                                                                                          Easily deploy any application that conforms to ...
https://artifacthub.io/packages/helm/gabibbo97/...      0.1.0                                                   fedora-32                                               389 Directory Server                             
...<removed for brevity>... 

现在,让我们尝试搜索一些最受欢迎的 Helm 图表,以测试我们的安装。我们希望在我们的 Kubernetes 集群上部署WordPress。我们选择 WordPress 作为示范,因为它是一个典型的三层应用程序,包含公共访问层(服务)、网页层(WordPress)和数据库层(MariaDB)。首先,让我们查看 Artifact Hub 上关于 WordPress 的可用图表:

$ helm search hub wordpress
URL                                                     CHART VERSION   APP VERSION            DESCRIPTION                                      
https://artifacthub.io/packages/helm/kube-wordp...      0.1.0           1.1                    this is my wordpress package                     
https://artifacthub.io/packages/helm/wordpress-...      1.0.2           1.0.0                  A Helm chart for deploying Wordpress+Mariadb st...
https://artifacthub.io/packages/helm/bitnami-ak...      15.2.13         6.1.0                  WordPress is the world's most popular blogging ...
...<removed for brevity>... 

同样,你也可以直接使用 Artifact Hub 的网页 UI 搜索 WordPress Helm 图表,如下所示:

图 14.1:Artifact Hub 上 WordPress Helm 图表的搜索结果

我们将使用Bitnami提供并维护的 Helm 图表,Bitnami 是一家专门在各种平台(如 Kubernetes)上分发开源软件的公司。如果你导航到 Bitnami 提供的 WordPress 图表的搜索结果,你将看到以下内容:

图 14.2:Artifact Hub 上 Bitnami WordPress Helm 图表的安装说明

该页面提供了详细的信息,介绍了如何添加bitnami仓库并安装 WordPress 的 Helm 图表。此外,你还会找到有关可用配置、已知限制和故障排除的详细信息。你还可以导航到每个图表的主页,以查看组成该图表的 YAML 模板(github.com/bitnami/charts/tree/master/bitnami/wordpress)。

我们现在可以通过遵循网页上的说明进行安装。首先,将bitnami仓库添加到你的 Helm 安装中:

$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories 

作为最佳实践,我们将在名为wordpress的专用命名空间内安装 WordPress:

$ kubectl create ns wordpress 
namespace/wordpress created 

在添加仓库后,我们可以安装bitnami/wordpress Helm 图表,但在此之前,我们需要为部署准备一些细节。请查看 Artifact Hub 上的图表页面(artifacthub.io/packages/helm/bitnami/wordpress)。你将看到很多参数供你配置和定制你的 WordPress 部署。如果你不提供任何参数,将使用默认值进行 Helm 发布部署。为了演示,让我们配置一些参数,而不是使用默认值。

你可以通过--set参数传递单独的参数,如下所示:

$ helm install wp-demo bitnami/wordpress -n wordpress --set wordpressUsername=wpadmin 

当您有多个参数需要配置时,您可以传递多个--set参数,但建议使用文件中的变量;您可以使用一个或多个文件传递变量。

让我们创建一个wp-values.yaml文件来存储变量和值如下:

# wp-values.yaml
wordpressUsername: wpadmin
wordpressPassword: wppassword
wordpressEmail: admin@example.com
wordpressFirstName: WP
wordpressLastName: Admin
service:
  type: NodePort
volumePermissions:
  enabled: true 

正如您所见,我们将一些 WordPress 配置传递给 Helm 图表。请注意,由于我们在使用 minikube 集群,我们正在将默认的 WordPress 类型更改为NodePort。如果您使用不同的 Kubernetes 集群(例如基于云的 Kubernetes),那么您可以将其保留为默认值,即LoadBalancer

现在我们已经配置了 Helm 仓库,创建了一个专用命名空间,并在变量文件中配置了参数,让我们使用 Helm 部署 WordPress;我们将为此发布使用名称wp-demo

$ helm install wp-demo bitnami/wordpress -n wordpress --values wp-values.yaml
NAME: wp-demo
LAST DEPLOYED: Tue Jun  4 21:27:49 2024
NAMESPACE: wordpress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: wordpress
CHART VERSION: 22.4.2
APP VERSION: 6.5.3
...<to be continued>... 

Helm 将显示基本的发布信息,如此处所示。过一会儿,您还将看到部署详细信息,包括服务名称,如何访问 WordPress 网站等:

...
** Please be patient while the chart is being deployed **
Your WordPress site can be accessed through the following DNS name from within your cluster:
    wp-demo-wordpress.wordpress.svc.cluster.local (port 80)
To access your WordPress site from outside the cluster follow the steps below:
Get the WordPress URL by running these commands:
   export NODE_PORT=$(kubectl get --namespace wordpress -o jsonpath="{.spec.ports[0].nodePort}" services wp-demo-wordpress)
   export NODE_IP=$(kubectl get nodes --namespace wordpress -o jsonpath="{.items[0].status.addresses[0].address}")
   echo "WordPress URL: http://$NODE_IP:$NODE_PORT/"
...<removed for brevity>... 

这就是 Helm 的美妙之处——您只需执行单个helm install命令,即可获得如何在您的集群上使用部署组件的详细指南。与此同时,WordPress 实例部署无需任何干预!

首先检查由 Helm 生成的 Kubernetes 对象的 YAML 清单是一种良好的做法。您可以通过带有额外标志的helm install命令来执行此操作:helm install wp-demo bitnami/wordpress --dry-run --debug。输出将包含 YAML 清单的联合输出,它们将不会应用于集群。

您还可以使用--version参数来指定 Helm 图表的特定版本,如下所示:

$ helm install my-wordpress bitnami/wordpress --version 22.4.2 

现在让我们按照 Helm 图表安装输出的说明进行操作:

  1. 等待一会儿,因为数据库需要初始化并部署 Pods。检查 Pod 状态:

    $ kubectl get po -n wordpress
    NAME                                 READY   STATUS    RESTARTS        AGE
    wp-demo-mariadb-0                    1/1     Running   9 (5m57s ago)   31m
    wp-demo-wordpress-5d98c44785-9xd6h   1/1     Running   0               31m 
    
  2. 注意数据库部署为 StatefulSet 如下:

    $ kubectl get statefulsets.apps -n wordpress
    NAME              READY   AGE
    wp-demo-mariadb   1/1     99s 
    
  3. 等待wp-demo Service 对象(NodePort 类型)获取端口详细信息:

    $  kubectl get svc -n wordpress
    NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
    wp-demo-mariadb     ClusterIP   10.100.118.79   <none>        3306/TCP                     2m39s
    wp-demo-wordpress   NodePort    10.100.149.20   <none>        80:30509/TCP,443:32447/TCP   2m39s 
    

在我们的情况下,端口将是80:31979/TCP

  1. 在这种情况下,由于我们正在使用 minikube,请找到 IP 地址和端口详细信息(如果您使用LoadBalancer类型,则可以直接访问 IP 地址查看 WordPress 站点):

    $ minikube service --url wp-demo-wordpress -n wordpress
    http://192.168.59.150:30509
    http://192.168.59.150:32447 
    
  2. 现在打开您的 Web 浏览器并导航到 WordPress URL,http://192.168.59.150:30509(另一个端口用于 HTTPS URL):

图 14.3:部署在 Kubernetes 上的 WordPress 图表——主页

  1. 现在,您可以登录 WordPress 仪表板,网址为http://192.168.59.150:30509/wp-admin。请注意,如果您错过了设置 WordPress 参数(包括密码),您需要查找 Helm 使用的默认值。例如,要检索 WordPress 登录密码,请按以下方式检查秘密。使用以下命令获取存储在专用wp-demo-wordpress Secret 对象中的凭据:

    $ kubectl get secret --namespace wordpress wp-demo-wordpress -o jsonpath="{.data.wordp
    ress-password}" | base64 --decode
    wppassword 
    
  2. 使用凭据登录为 WordPress 管理员:

图 14.4:部署在 Kubernetes 上的 WordPress chart —— 管理员仪表盘

现在你可以尽情享受你的 WordPress 了,恭喜!如果你感兴趣,可以检查作为该 Helm chart 一部分部署的 Pods、Services、Deployments 和 StatefulSets。这将帮助你深入了解 chart 的组件以及它们是如何交互的。

Helm CLI 提供了对各种 shell 的自动补全功能。你可以运行 helm completion 命令以了解更多信息。

如果你想获取关于所有已部署的 Helm 发布的信息,可以使用以下命令:

$ helm list -n wordpress
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART              APP VERSION
wp-demo wordpress       1               2024-06-04 22:20:32.556683096 +0800 +08 deployed        wordpress-22.4.2   6.5.3 

在接下来的章节中,我们将学习如何使用 Helm 删除一个已部署的发布。

删除 Helm 发布

正如我们在前面的章节中所学到的,Helm chart 已经部署了多个资源,包括 Deployment、PVC、Services、Secrets 等。逐一查找并删除这些项目并不实际。但 Helm 提供了一种简单的方法来使用 helm uninstall 命令删除部署。当你准备好时,可以通过以下命令卸载 Helm 发布,从而清理发布:

$ helm uninstall wp-demo -n wordpress
release "wp-demo" uninstalled 

这将删除该发布创建的所有 Kubernetes 对象。但请注意,Helm chart 创建的 PersistentVolumes 和 PersistentVolumeClaims 将不会被清理——你需要手动清理它们。接下来,我们将更详细地了解 Helm charts 的内部结构。

Helm chart 结构

作为一个示例,我们将使用 Bitnami 提供的 WordPress Helm chart(github.com/bitnami/charts/tree/master/bitnami/wordpress),我们刚刚用它在集群中执行了测试部署。Helm charts 只是具有特定结构(约定)的目录,这些目录可以存在于本地文件系统或 Git 仓库中。目录名称同时也是 chart 的名称——在本例中为 wordpress。chart 目录中文件的结构如下:

  • Chart.yaml:包含关于 chart 的元数据的 YAML 文件,如版本、关键词以及必须安装的依赖 charts 的引用。

  • LICENSE:可选的纯文本文件,包含许可证信息。

  • README.md:最终用户的 README 文件,将在 Artifact Hub 上可见。

  • values.yaml:chart 的默认配置值,这些值将作为 YAML 模板参数使用。用户可以通过 Helm 覆盖这些值,方法是在 CLI 中逐个传递,或使用单独的 YAML 文件传递值。你已经通过传递 --values wp-values.yaml 参数使用了这种方法。

  • values.schema.json:可选地,你可以提供一个 values.yaml 必须遵循的 JSON 模式。

  • charts/:可选目录,包含附加的依赖 charts。

  • crds/:可选的 Kubernetes 自定义资源定义。

  • templates/:包含所有 YAML模板的最重要目录,用于生成 Kubernetes YAML 清单文件。这些 YAML 模板将与提供的结合,最终生成的 YAML 清单文件将被应用到集群中。

  • templates/NOTES.txt:包含简短使用说明的可选文件。

例如,如果你检查 WordPress Helm 图表中的Chart.yaml,你会发现它依赖于 Bitnami 的MariaDB图表,前提是提供的值中mariadb.enabled被设置为true

...
appVersion: 6.5.3
dependencies:
- condition: memcached.enabled
  name: memcached
  repository: oci://registry-1.docker.io/bitnamicharts
  version: 7.x.x
- condition: mariadb.enabled
  name: mariadb
  repository: oci://registry-1.docker.io/bitnamicharts
  version: 18.x.x
... 

现在,如果你查看values.yaml文件(它包含默认值并且内容较为冗长),你可以看到默认情况下启用了 MariaDB:

...
## MariaDB chart configuration
## ref: https://github.com/bitnami/charts/blob/main/bitnami/mariadb/values.yaml
##
mariadb:
  ## @param mariadb.enabled Deploy a MariaDB server to satisfy the applications database requirements
  ## To use an external database set this to false and configure the `externalDatabase.*` parameters
  ##
  enabled: true
... 

最后,让我们来看一下其中一个 YAML 模板的样子——打开deployment.yaml文件(github.com/bitnami/charts/blob/master/bitnami/wordpress/templates/deployment.yaml),这是一个 Kubernetes 部署对象的模板,用于包含 WordPress 容器的 Pod。例如,你可以看到replicas的数量是如何从提供的值中引用的:

kind: Deployment
...
spec:
...
  replicas: {{ .Values.replicaCount }}
... 

这将被replicaCount的值替换(values.yaml文件中的默认值为1)。关于如何使用 Go 模板的详细信息可以在pkg.go.dev/text/template找到。你还可以通过分析现有的 Helm 图表来通过示例学习——大多数图表使用类似的模式来处理提供的值。

关于 Helm 图表结构的详细文档可以在 https://helm.sh/docs/topics/charts/找到。

在大多数情况下,你在安装图表时需要覆盖values.yaml文件中的某些默认值,正如我们之前所学到的。

现在,你已经了解了关于 Helm 图表结构的最重要细节,在接下来的章节中,我们将使用 Helm 图表部署 Kubernetes 仪表盘。

使用 Helm Charts 安装 Kubernetes 仪表盘

Kubernetes 仪表盘是官方的 Web UI,用于提供集群概览。这个组件的 Helm 图表由 Kubernetes 社区官方维护(artifacthub.io/packages/helm/k8s-dashboard/kubernetes-dashboard)。我们将以默认参数安装它,因为此时没有必要进行任何自定义。

对于 minikube 集群,你可以通过一个命令启用仪表盘并访问它:minikube dashboard。但我们此时的目的是学习如何为任何类型的 Kubernetes 集群部署仪表盘。

首先,将kubernetes-dashboard仓库添加到 Helm:

$ helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/
"kubernetes-dashboard" has been added to your repositories 

现在,我们可以将 Helm 图表作为kubernetes-dashboard发布安装到集群中,方法如下:

$ helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard 

等待安装完成并查看输出信息。注意以下信息,因为我们稍后将用它来访问仪表盘 WEBUI:

...
Congratulations! You have just installed Kubernetes Dashboard in your cluster.
To access the Dashboard, run the following:
  kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443
... 

同时,确保 Pods 的状态为 Running

$  kubectl get pod -n kubernetes-dashboard
NAME                                                    READY   STATUS    RESTARTS   AGE
kubernetes-dashboard-api-86c68c7896-7jxwz               1/1     Running   0          2m53s
kubernetes-dashboard-auth-59784dd8b-vsr99               1/1     Running   0          2m53s
kubernetes-dashboard-kong-7696bb8c88-6q7zs              1/1     Running   0          2m53s
kubernetes-dashboard-metrics-scraper-5485b64c47-9d69q   1/1     Running   0          2m53s
kubernetes-dashboard-web-84f8d6fff4-nxt5f               1/1     Running   0          2m53s 

目前您可以忽略 Helm 图表部署的其他 Pods。我们将在下一部分学习如何访问 Dashboard UI。

确保 Kubernetes Dashboard 的访问安全

默认情况下,Kubernetes Dashboard 优先考虑安全性,使用最小化的 RBAC 配置。这有助于保护您的集群数据。目前,登录 Dashboard 需要使用 Bearer Token。

本示例用户创建指南可能会授予管理员权限。请务必仅将其用于教育目的,并为生产环境实施适当的 RBAC 控制。

按照以下步骤创建一个令牌以访问 Kubernetes Dashboard WEBUI:

  1. 创建一个 ServiceAccount;准备以下 YAML 文件:

    # dashboard-sa.yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: admin-user
      namespace: kubernetes-dashboard 
    
  2. 按如下方式创建 ServiceAccount:

    $ kubectl apply -f dashboard-sa.yaml
    serviceaccount/admin-user created 
    
  3. 创建 ClusterRoleBinding 以允许访问。准备以下 YAML 文件:

    # dashboard-rbac.yml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: admin-user
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-admin
    subjects:
      - kind: ServiceAccount
        name: admin-user
        namespace: kubernetes-dashboard 
    
  4. 通过应用 YAML 定义创建 ClusterRoleBinding

    $ kubectl apply -f dashboard-rbac.yml
    clusterrolebinding.rbac.authorization.k8s.io/admin-user created 
    
  5. 创建令牌:

    $ kubectl -n kubernetes-dashboard create token admin-user 
    

复制生成的长令牌字符串,我们将在下一部分使用它登录集群 Dashboard。

访问 Dashboard WEBUI

Kubernetes Dashboard 提供了多种访问方式。在这里,我们将重点介绍默认方法。此方法假设在安装过程中您没有更改标准配置。如果您进行过修改,步骤可能会有所不同。

执行以下命令(该命令是您从 helm install 输出中复制的),以获取 Dashboard 访问权限。该命令会保持在 port-forward 状态;请不要退出该命令:

$ kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
... 

现在,在浏览器中访问 https://localhost:8443/。您可以忽略 SSL 证书警告,因为 Dashboard 正在使用自签名的 SSL 证书。输入在 步骤 3 中生成的令牌,确保访问 Kubernetes Dashboard 的安全性,并按图示登录 Dashboard。

图 14.5:Kubernetes Dashboard 图表 – 登录页面

此时,您已能够访问 Dashboard,并且可以浏览其功能:

图 14.6:Kubernetes Dashboard 图表 – 部署页面

承载令牌是为具有 cluster-admin 角色的用户准备的,因此请小心使用,因为您可以执行任何操作,包括删除资源。

恭喜,您已经成功通过 Helm 图表部署了 Kubernetes Dashboard,并使用令牌验证了访问权限。您可以根据我们在下一部分的讲解,探索更多通过 Helm 图表的部署。

使用 Helm 图表安装其他流行的解决方案

若要更多地练习 Helm 图表,您可以快速安装 Kubernetes 集群的其他软件。这在您的开发场景中可能非常有用,或者作为云原生应用程序的构建模块。

Elasticsearch 与 Kibana

Elasticsearch 是一个流行的全文搜索引擎,常用于日志索引和日志分析。Kibana 是 Elasticsearch 生态系统的一部分,是一个用于 Elasticsearch 数据库的可视化 UI。为了安装这个堆栈,我们需要使用两个图表,这两个图表都由 Elasticsearch 的创建者维护:

Prometheus 与 Grafana

Prometheus 是一个流行的监控系统,具有时间序列数据库,Grafana 用作可视化 UI。类似于 Elastic Stack,为了安装 Prometheus 和 Grafana 堆栈,我们需要使用两个图表:

由于我们已经解释了如何在 Kubernetes 集群中部署 Helm 图表,因此我们将跳过这些步骤的详细说明。你可以继续部署这些图表并探索其功能。

在本节中,我们将探讨一些 Helm 图表的关键安全考虑因素。

Helm 图表的安全考虑

Helm 图表简化了应用程序在 Kubernetes 中的部署,但它们也可能引入一些需要管理的安全漏洞。需要牢记的一些重要事项包括以下几点:

  • 源验证:在部署 Helm 图表之前,始终检查图表的来源,绝不要安装来自不信任或名声较差的仓库的图表。可能会包含恶意或不安全的应用程序。验证图表的来源,并尽量使用官方图表或维护良好的社区仓库。

  • 定期审计:定期对 Helm 图表及其依赖项进行安全审计。此过程有助于识别已知的漏洞,从而确保部署的应用程序足够安全,符合组织设定的标准。使用 Trivy 或 Anchore 等工具进行 Helm 图表的漏洞扫描。

  • 图表配置:注意 Helm 图表中的默认配置。大多数图表附带的配置设置并不适合你的生产环境。根据组织的安全政策和最佳实践,考虑必要时审查和调整默认设置。

  • 基于角色的访问控制 (RBAC):实现这一功能,以限制在 Kubernetes 集群中部署和管理 Helm 图表的权限。这将减少未经授权的更改,并确保只有受信任的人才能部署敏感应用程序。

  • 依赖管理: 监控和管理在 Helm charts 中显示的依赖项列表。定期更新这些依赖项,以避免应用程序中的安全漏洞,并确保它们接收到最新的安全补丁和改进。

  • 命名空间隔离: 考虑使用 Helm charts,并将每个 chart 放在各自独立的命名空间中,这样可以提高安全性。如果发生问题,爆炸范围将被限制,从而为应用程序及其资源提供更好的隔离。

通过在这些领域采取主动措施,你可以大大提高 Kubernetes 部署的安全性,确保潜在的漏洞不会摧毁你的应用程序和数据。

现在,我们已经在本章的前半部分探讨并实践了 Helm charts。我们还了解到,Helm chart 是在 Kubernetes 中部署和管理复杂应用程序的一个极好的方式。

随着 Kubernetes 的普及,管理应用程序的生产环境的复杂性也在增加。虽然 Helm 和类似的工具改进了应用程序部署的过程,但它们不能独立解决有状态应用程序在运行时的操作需求,如扩展、配置管理和故障恢复。我们需要能够将应用程序知识和最佳实践打包的解决方案,以便在应用程序生命周期的每个阶段自动化操作任务,同时确保健康和高性能。通过这些解决方案,团队可以最小化人工干预,减少人为错误,并专注于通过应用程序交付价值,而不是管理基础设施。Kubernetes 操作员的引入正是为了满足这些需求。

在接下来的部分中,我们将了解 Kubernetes 操作员是什么,以及如何使用 Kubernetes 操作员安装复杂的部署。

引入 Kubernetes 操作员

我们已经探讨了 Deployment 和 StatefulSet 之间的差异,其中 StatefulSet 管理需要持久数据存储的有状态应用程序。我们还了解了 StatefulSet 正常运行所需的手动(和自动)操作,例如 Pod 副本之间的数据同步和初始化任务。

然而,人工干预违背了 Kubernetes 的核心原则,即自动化和自愈至关重要。这时,Kubernetes 操作员发挥作用。

从人到软件

想象一下,将人类操作员替换为软件操作员。Kubernetes 操作员本质上是软件扩展,它们自动化了复杂的应用程序管理任务,尤其是有状态应用程序。操作员不再依赖人工干预来维护应用程序栈,而是利用其内建的软件组件和智能来有效地执行和管理这些任务。

以下图像展示了 Kubernetes 集群中组件与操作员之间的高级关系。

图 14.7:操作员如何在 Kubernetes 中管理资源

在接下来的章节中,我们将讨论 Kubernetes 操作员的优势和好处。

Helm Charts 与 Kubernetes 操作员

虽然 Helm charts 是与 Kubernetes 上的应用程序管理相关的组件,它们与操作员不同。使用 Helm,用户可以比使用操作员更快地安装应用程序,因为 Helm 是一个有效的包管理器,使用预设计的 chart,使得这一过程更加简单。

它在需要速度的情况下最为适用,因此非常适合一次性安装或较小的应用程序。然而,Helm 仍然更多地关注部署阶段,而不是在应用程序运行时的持续操作需求。

操作员是旨在承担复杂和有状态应用程序整个生命周期管理责任的软件程序。通过自定义资源定义CRDs),它们编码了应用程序如何运行的知识,并允许扩展、配置管理、自动化升级和自我修复机制。操作员不断观察应用程序的健康状况,并采取纠正措施使其恢复到期望的状态。这就是为什么它们在那些需要大量管理和监控的应用程序中变得非常宝贵。简而言之,Helm charts 简化了部署过程,但操作员扩展了 Kubernetes 到生产环境操作性,让团队能够自动化并简化如何管理生产环境中的应用程序。

在下一节中,我们将探讨与 Helm charts 相比,Kubernetes 操作员的一些主要特点。

操作员如何帮助应用程序部署?

操作员具备管理和维护应用程序的所有可能操作的能力,包括以下内容:

  • 生命周期管理:操作员管理应用程序或应用程序堆栈的生命周期,不仅仅限于初始部署。它们自动化关键的操作任务,如升级、扩展和故障恢复,以保持应用程序的健康和长期性能。其他方法,如 Helm charts,通常仅停留在部署阶段,而操作员则不断监控当前应用程序状态,并在发生变化或问题时自动将其与期望配置进行对比。这允许自动化升级、配置更改和状态监控——所有这些都无需干预,甚至不希望干预。对于需要复杂生命周期管理的有状态应用程序,操作员相较于 Helm 提供了显著的优势,对于那些需要持续维护和自动化管理的应用程序,应优先选择操作员。

  • 资源编排:操作员创建应用程序正常运行所需的基本资源,如 ConfigMapsSecretsPVCs

  • 自动化部署:操作员可以基于用户提供的配置或默认值来部署应用程序堆栈,从而简化部署过程。

  • 数据库管理:以 PostgreSQL 集群为例,Operator 可以利用 StatefulSets 部署它们,并确保副本之间的数据同步。

  • 自愈能力:Operator 能够检测并响应应用程序故障,触发恢复或故障转移机制,以保持服务的连续性。

自动化的可重用性

Operator 促进了可重用性。相同的 Operator 可以在不同项目或多个 Kubernetes 集群中使用,确保在整个基础设施中一致且高效的应用程序管理。

Operator 如何确保应用程序状态

Kubernetes Operator 的功能与 Kubernetes 本身类似,利用控制循环来管理应用程序。该循环不断监控由 CRD 定义的应用程序期望状态,并与集群中应用程序的实际状态进行比较。任何差异都会触发 Operator 采取纠正措施。这些措施可能包括扩展应用程序、更新配置或重启 Pods。控制循环的持续运行确保你的应用程序始终与期望状态保持一致,从而促进自愈和自动化管理。

自定义资源定义——Operator 的构建模块

Kubernetes Operator 依赖于 CRD。这些实际上是 Kubernetes API 的扩展,允许你为应用程序或其需求定义特定的自定义资源。可以把它们看作是 Kubernetes 生态系统中应用程序期望配置的蓝图。

CRD 本质上扩展了 Kubernetes API,允许你定义特定于应用程序的自定义资源。这些资源代表了你在 Kubernetes 集群中构建应用程序的基本模块。它们可以指定以下细节:

  • 应用程序副本(Pods)的期望数量。

  • 内存和 CPU 的资源请求与限制。

  • 持久数据的存储配置。

  • 环境变量和配置设置。

CRD 的优势

使用 CRD 部署应用程序有多个好处,包括以下几点:

  • 声明式管理:与手动配置单独的资源(如部署或服务)不同,CRD 让你可以以声明式的方式定义应用程序的期望状态。然后,Operator 会负责将该期望状态转换为集群中实际运行的资源。

  • 应用程序特定配置:CRD 满足应用程序的独特需求。你可以定义特定于应用程序逻辑或配置要求的自定义字段。

  • 简化管理:CRD 提供了一个集中管理应用程序配置的点。与不同资源类型分散配置的方式相比,这简化了管理过程。

Operator 分发机制

Operators 主要由拥有特定应用堆栈部署经验的应用供应商创建和分发。然而,一个充满活力的社区积极开发、分发和维护着各种 Operators。

OperatorHub (operatorhub.io/) 作为一个中央仓库,供你发现和安装适用于各种应用和功能的 Operators。

图 14.8:OperatorHub 主页面

构建你自己的 Operator

Operator 框架提供了一个强大的工具包,称为 Operator SDK。这个开源宝石使你能够开发和构建自定义 Operators,从而掌控 Kubernetes 中的应用管理。Operator SDK 简化了通常复杂的 Operator 创建过程。它提供了预构建的组件和功能,处理与 Kubernetes API 交互、管理自定义资源以及实现控制循环等常见任务。这使你能够专注于特定应用程序的独特逻辑和配置需求。

构建你自己的 Operators 解锁了几个优势。首先,你获得了对应用管理的精细控制。Operator 可以根据应用程序的具体需求进行定制,完美地处理配置、部署策略和扩展要求。其次,Operators 自动化了与应用生命周期相关的重复性任务,从而显著提高效率。最后,自定义的 Operators 是可重用的。一旦为特定应用程序构建,它们可以在同一应用的不同部署中应用,从而节省你在管理 Kubernetes 基础设施上的时间和精力。

你可以在 sdk.operatorframework.io/ 了解更多关于 Operator SDK 的信息。

在我们动手操作 Operator 之前,让我们在下一节中了解一些关于Operator 生命周期管理器OLM)的细节。

Operator 生命周期管理器(OLM)

Kubernetes 中的 Operator 生命周期管理器简化了将应用程序作为 Operator 打包并进行部署和管理的过程。OLM 使用声明式方法通过 YAML 文件指定资源。在此过程中,无需依赖特定顺序的复杂多文件部署。安装和自动升级也确保你的 Operators 始终保持最新。它还提供了一个名为目录源(Catalog Sources)的包发现功能,使你能够使用来自例如 OperatorHub 或其他选择来源的 Operators。通过 OLM,你将获得几个优势。首先,它通过管理依赖关系和顺序,减少了部署的复杂性。OLM 可以在大规模集群中管理成千上万的 Operators。更重要的是,OLM 强制执行所需的配置,这将简化发布和更新过程。更不用说,OLM 通过提供一种一致的方式来打包和部署应用程序作为 Operators,推动了标准化。

这是一个关于环境和个人偏好的问题。OLM 与 Kubernetes 更加集成和原生,而 Helm 由于在其他部署中更为常见,具有更丰富的软件包生态系统。

在学习 Operators 和 OLM 的过程中,了解 ClusterServiceVersionCSV)同样很重要。ClusterServiceVersion 是 Kubernetes Operator 生命周期管理器中的一个重要部分,它是 Operator 的核心元数据处理和部署信息。它定义了 Operator 的名称及其所包含的版本,并给出了简短的描述。它还概述了 Operator 正常运行所需的权限或角色。它定义了 Operator 所管理的 CRD、安装策略以及升级流程。有关 CSV 的更多信息,请参见文档(olm.operatorframework.io/docs/concepts/crds/clusterserviceversion/)。

在接下来的部分,我们将探讨如何使用 OLM 和 Prometheus Operator 在 Kubernetes 上部署 Prometheus 监控,展示这两种方法在应用程序管理中的强大功能。

使用 Prometheus 和 Grafana 启用 Kubernetes 监控

保持 Kubernetes 应用程序健康和高效运行的成功取决于多个因素,其中之一是拥有一个强大且可靠的环境。在这里,像 PrometheusGrafana 这样的监控工具可以提供帮助。Prometheus 在幕后工作,它收集并存储有关 Kubernetes 集群的宝贵指标。Grafana 则将这些宝贵的数据以易于理解的格式进行可视化,帮助你深入了解应用程序和基础设施的健康状况和行为。

下图展示了 Prometheus 组件的高层架构。(这是官方参考资料。)

图 14.9:Prometheus 及其一些生态系统组件的架构(来源:prometheus.io/docs/introduction/overview/

传统的部署方式,例如部署包含 Prometheus 和 Grafana 的监控栈,往往非常繁琐。手动编写多个 YAML 清单,处理所有的依赖关系和正确的部署顺序,不仅繁琐,而且容易出错。Prometheus 和 Grafana Operators 提供了一个更高效、更易维护的解决方案。

可以利用 Helm charts 部署整个监控栈——包括 Operators 和实例——使用如 kube-prometheus-stackartifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack)这样的项目。但在下一部分,我们的目的是使用 OLM 设置相同的监控栈。

在接下来的部分,我们将探索在 Kubernetes 集群中部署 Prometheus 和 Grafana 的过程,帮助你有效地监控应用程序,确保它们的顺利运行。

安装 Operator 生命周期管理器

要利用基于 OLM 的操作员安装,我们需要在集群中安装 OLM。 您可以使用operator-sdk实用程序,Helm 图表甚至通过应用于 OLM 的 Kubernetes YAML 清单来在集群中安装 OLM。 对于这个练习,让我们使用install.sh脚本为基础的安装。

$ curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.28.0/install.sh | bash -s v0.28.0 

等待脚本完成并使用 OLM 配置 Kubernetes 集群。 完成后,请验证 Pods 是否在olm命名空间中运行:

$ kubectl get pods -n olm
NAME                               READY   STATUS    RESTARTS   AGE
catalog-operator-9f6dc8c87-rt569   1/1     Running   0          36s
olm-operator-6bccddc987-nrlkm      1/1     Running   0          36s
operatorhubio-catalog-6l8pw        0/1     Running   0          21s
packageserver-6df47456b9-8fdt7     1/1     Running   0          24s
packageserver-6df47456b9-lrvzp     1/1     Running   0          24s 

还要使用kubectl get csv命令检查 ClusterServiceVersion 详细信息如下:

$ kubectl get csv -n olm
NAME            DISPLAY          VERSION   REPLACES   PHASE
packageserver   Package Server   0.28.0               Succeeded 

具有成功状态 - 我们可以看到 OLM 已部署并准备好管理操作员。 在下一部分中,我们将使用 OLM 部署 Prometheus 和 Grafana 操作员。

使用 OLM 安装 Prometheus 和 Grafana 操作员

一旦配置 OLM,部署操作员就非常简单。 大多数情况下,您会在operatorhub.io的操作员页面上找到操作员安装命令和说明。

要使用 OLM 安装 Prometheus 操作员(operatorhub.io/operator/prometheus),请按照以下步骤操作:

  1. 在 OperatorHub 中搜索 Prometheus Operator:

    $ kubectl get packagemanifests | grep prometheus
    ack-prometheusservice-controller           Community Operators   12m
    prometheus                                 Community Operators   12m
    prometheus-exporter-operator               Community Operators   12m 
    
  2. 使用 OLM 安装 Prometheus 操作员:

    $ kubectl create -f https://operatorhub.io/install/prometheus.yaml
    subscription.operators.coreos.com/my-prometheus created 
    

    operatorhub.io/install/prometheus.yaml提供了创建订阅的基本 YAML 定义。 您可以创建带有所有所需自定义的本地 YAML 文件。

  3. 等待几分钟并确保 Prometheus 操作员已正确部署:

    $ kubectl get csv -n operators
    NAME                         DISPLAY               VERSION   REPLACES                     PHASE
    prometheusoperator.v0.70.0   Prometheus Operator   0.70.0    prometheusoperator.v0.65.1   Succeeded 
    
  4. 验证 Prometheus 操作员 Pods 是否在运行:

    $ kubectl get pods -n operators
    NAME                                   READY   STATUS    RESTARTS   AGE
    prometheus-operator-84f9b76686-2j27n   1/1     Running   0          87s 
    
  5. 以相同方式,使用 OLM 找到并安装 Grafana 操作员(按照先前的步骤进行参考):

    $ kubectl create -f https://operatorhub.io/install/grafana-operator.yaml 
    
  6. 现在,还要验证在后端创建的 CRDs,因为这些条目是作为操作员安装的一部分创建的:

    $ kubectl get crd
    AME                                                  CREATED AT
    alertmanagerconfigs.monitoring.coreos.com             2024-10-18T09:14:03Z
    alertmanagers.monitoring.coreos.com                   2024-10-18T09:14:04Z
    catalogsources.operators.coreos.com                   2024-10-18T09:10:00Z
    clusterserviceversions.operators.coreos.com           2024-10-18T09:10:00Z
    grafanaalertrulegroups.grafana.integreatly.org        2024-10-18T09:25:19Z
    ...<removed for brevity>... 
    
  7. 您将在 Kubernetes 集群中找到多个创建的 CRD。

现在我们已经部署了操作员,是时候创建 Prometheus 和 Grafana 实例并配置堆栈以监视 Kubernetes 集群了。 我们将在下一部分了解这些操作。

使用操作员配置 Prometheus 和 Grafana 实例

要配置新实例,让我们使用带有 CRD 配置的标准 YAML 定义。 此练习的说明和 YAML 定义文件存储在 GitHub 存储库的第十四章目录中。

按照步骤配置使用 Prometheus 和 Grafana 在 Kubernetes 中的监控堆栈:

  1. 作为最佳实践,让我们创建一个命名空间来部署监控解决方案(参考monitoring-ns.yaml):

    $ kubectl apply -f monitoring-ns.yaml
    namespace/monitoring created 
    
  2. 使用适当的角色和 RBAC 配置 ServiceAccount(请参阅存储库中的monitoring-sa.yaml):

    $ kubectl apply -f monitoring-sa.yaml
    serviceaccount/prometheus created
    role.rbac.authorization.k8s.io/prometheus-role created
    rolebinding.rbac.authorization.k8s.io/prometheus-rolebinding created 
    
  3. 为新的 Prometheus 实例准备 YAML(请参阅promethues-instance.yaml):

    apiVersion: monitoring.coreos.com/v1
    kind: Prometheus
    metadata:
      name: example-prometheus
      namespace: monitoring
    spec:
      replicas: 2
      serviceAccountName: prometheus
      serviceMonitorSelector:
        matchLabels:
          app.kubernetes.io/name: node-exporter 
    

注意在前述 YAML 定义中的kind: Prometheus,因为我们在这里使用 CRD; Prometheus 操作员将理解此 CRD 并采取必要的操作以创建部署。

  1. 通过将配置应用到集群,创建一个新的 Prometheus 实例:

    $ kubectl apply -f promethues-instance.yaml
    prometheus.monitoring.coreos.com/example-prometheus 
    
  2. 以类似的方式,使用 Operator 部署 Grafana 实例:

    # grafana-instnace.yaml
    apiVersion: grafana.integreatly.org/v1beta1
    kind: Grafana
    metadata:
      labels:
        dashboards: grafana-a
        folders: grafana-a
      name: grafana-a
      namespace: monitoring
    spec:
      config:
        auth:
          disable_login_form: 'false'
        log:
          mode: console
        security:
          admin_password: start
          admin_user: root 
    

在这里我们直接在 YAML 定义中以明文方式使用了配置和密码。在生产环境中,你应该使用 Kubernetes Secrets 来存储这些敏感数据。

  1. 应用 YAML 定义以创建 Grafana 实例:

    $ kubectl apply -f grafana-instance.yaml
    grafana.grafana.integreatly.org/grafana-a created 
    
  2. 验证 Prometheus 和 Grafana Operator 在 monitoring 命名空间中创建的对象:

    $ kubectl get pod,svc,sts -n monitoring
    NAME                                     READY   STATUS    RESTARTS   AGE
    pod/grafana-a-deployment-69f8999f8-82zbv   1/1   Running   0          17m
    pod/node-exporter-n7tlb                    1/1   Running   0          93s
    pod/prometheus-example-prometheus-0        2/2   Running   0          20m
    pod/prometheus-example-prometheus-1        2/2   Running   0          20m
    NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
    service/grafana-a-service     ClusterIP   10.107.212.241   <none>        3000/TCP   17m
    service/prometheus-operated   ClusterIP   None             <none>        9090/TCP   20m
    NAME                                             READY   AGE
    statefulset.apps/prometheus-example-prometheus   2/2     20m 
    
  3. 你可以看到,在前面的输出中,Operators 已根据 CRD 创建了适当的 Kubernetes 资源。你甚至可以看到 Prometheus 和 Grafana 服务已创建,并且能够访问它。我们将在本练习的后续阶段演示这一过程。

为了演示,我们在集群中启用 Node Exporter,并通过 Grafana 进行可视化。Node Exporter 是 Prometheus 的一种导出器,它暴露了主机机器的详细指标,包括硬件和操作系统信息,如 CPU 使用率、内存使用率以及其他系统级别的指标。它作为一个独立服务在 Kubernetes 集群的每个节点上运行,或在物理/虚拟服务器上运行,并通过 HTTP 端点暴露这些指标。通过抓取这些数据,Prometheus 可以了解节点的健康状况和性能,从而使管理员能够理解资源利用情况并指出基础设施中的问题。

以 DaemonSet 方式运行 Node Exporter 意味着 Kubernetes 集群中的每个节点都会运行一个导出器实例。通过这种方式,Prometheus 将能够持续抓取所有节点的系统指标,从而有效观察整个集群的健康状况和性能。使用 node-exporter-daemonset.yaml 创建 Node Exporter,配置如下:

$ kubectl apply -f node-exporter-daemonset.yaml
daemonset.apps/node-exporter created 

应该创建一个 Node Exporter 服务(svc)来暴露 Node Exporter 收集的指标,以便 Prometheus 抓取。这项服务为 Prometheus 提供了一种方式,帮助它发现并抓取运行在每个节点上的 Node Exporter pods 的指标,从而实现对 Kubernetes 集群中节点性能的集中监控。创建 Node Exporter 服务,配置如下:

$ kubectl apply -f node-exporter-svc.yaml
service/node-exporter created 

一个 Node Exporter serviceMonitor 通常会启用 Prometheus 来发现并抓取 Node Exporter 服务的指标。这个配置简化了整个监控过程,通过定义 Prometheus 应该如何以及在哪里抓取指标(例如,指定目标服务、间隔、标签等),确保在无需管理员干预的情况下收集的一致性。如下所示,创建一个 serviceMonitor CRD:

$ kubectl apply -f servicemonitor-instance.yaml
servicemonitor.monitoring.coreos.com/node-exporter created
Now it is our time to verify the monitoring stack deployment in Kubernetes cluster. 

让我们验证 Prometheus 门户,确保那里收集了相关详细信息。在你的某个控制台中,启动 kubectl port-forward 命令以暴露 Prometheus 服务,命令如下(测试完成后,你可以通过按 Ctrl+C 结束 port-forward):

$ kubectl port-forward -n monitoring svc/prometheus-operated 9091:9090
Forwarding from 127.0.0.1:9091 -> 9090
Forwarding from [::1]:9091 -> 9090 

打开浏览器并访问http://localhost:9091/targets,以确保node-exporter对 Prometheus 可见。

图 14.10:Prometheus 门户,Kubernetes 节点上可见 node-exporter

你可以从上面的截图确认 Prometheus 正在成功获取节点指标。

现在,让我们使用可视化工具 Grafana 来可视化指标和监控数据。打开控制台,使用kubectl port-forward命令将 Grafana 服务暴露出来,如下所示:

$ kubectl port-forward -n monitoring service/grafana-a-service 3000:3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000 

打开浏览器并访问http://localhost:3000查看 Grafana 仪表板。

图 14.11:Grafana 门户

使用你在grafana-instance.yaml中配置的登录凭据(或者如果使用了密钥,则使用密钥),登录 Grafana 仪表板。

在那里你只会看到一个默认的仪表板,因为我们需要为自己的目的配置一个新的仪表板。

从左侧菜单中,转到Connections | Data sources,如下图所示。

图 14.12:在 Grafana 中添加数据源

在下一个窗口中,选择Prometheus作为数据源,并输入 Prometheus URL,如下图所示。记得输入 FQDN(例如http://prometheus-operated.monitoring.svc.cluster.local:9090 – 参见第八章使用服务暴露你的 Pods,了解更多关于服务和 FQDN 的内容)。

图 14.13:配置 Grafana 数据源

点击页面底部的保存并测试按钮,你将收到成功消息,如下图所示。(如果出现任何错误消息,请检查你使用的 Prometheus URL,包括 FQDN 和端口号。)

图 14.14:Grafana 数据源配置成功

现在我们已有数据源,需要创建一个仪表板来可视化数据。你可以从头开始创建一个仪表板,也可以导入一个带有预定义配置的仪表板。为此,访问https://grafana.com/grafana/dashboards/并找到Node Exporter Full仪表板。点击复制 ID 到剪贴板按钮,如下所示。

图 14.15:复制 Grafana 仪表板 ID 以导入

返回到 Grafana 的WEBUI | Dashboards | New | Import,如下图所示。

图 14.16:将新仪表板导入 Grafana

输入你在前一步已经复制的Node Exporter Full仪表板 ID,如下所示,然后点击加载按钮。

图 14.17:提供仪表板 ID 以在 Grafana 中导入

在下一个页面中,选择Prometheus作为数据源(之前已配置过的),然后点击导入按钮,如下所示。

图 14.18:在 Grafana 中完成仪表板导入

就这样,你将看到一个漂亮的仪表板,内置了预配置的部件和图表,如下图所示。你可以探索仪表板,查看关于集群的详细信息,如 CPU、内存、网络流量等。

图 14.19:Node Exporter:在 Grafana 中导入的完整仪表板

恭喜你,成功在 Kubernetes 集群上部署了 Prometheus 和 Grafana 堆栈,并启用了 Node Exporter!

本章到此为止。正如你所看到的,使用 Helm 图表和 Operators,即使是复杂的多组件解决方案,也非常简单,并且可以为你的开发和生产环境带来诸多好处。

总结

本章介绍了如何使用 Helm、Helm 图表和 Kubernetes Operators。首先,你了解了软件包管理的目的以及 Helm 如何作为 Kubernetes 的软件包管理器工作。我们演示了如何在本地计算机上安装 Helm,并如何部署一个 WordPress 图表来测试安装。接着,我们介绍了 Helm 图表的结构,并展示了如何使用用户提供的值来配置图表中的 YAML 模板。随后,我们展示了如何使用 Helm 在 Kubernetes 集群上安装流行的解决方案。我们安装了 Kubernetes Dashboard 并探索了其组件。之后,我们学习了 Kubernetes Operators 和其他组件,包括自定义资源定义(CRD)。我们还使用 Helm 和 Operators 部署了 Prometheus 堆栈,包括 Grafana。

在下一部分,你将获得在不同云环境中有效部署 Kubernetes 集群所需的所有细节。我们将首先介绍如何在 Google Kubernetes Engine 上操作集群。

进一步阅读

)

)

)

)

)

)

)

如需了解有关 Helm 和 Helm Charts 的更多信息,请参考以下 Packt Publishing 的书籍:

你可以在以下 Packt Publishing 的书籍中了解更多关于 Elasticsearch 和 Prometheus 的内容:

加入我们的社区,进入 Discord

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十五章:Google Kubernetes Engine 上的 Kubernetes 集群

在本章中,我们将在Google Cloud PlatformGCP)上启动一个 Kubernetes 集群,这是我们在接下来几章中要介绍的三大公有云提供商中的第一个。

到本章结束时,你将注册 Google Cloud Platform,使用Google Kubernetes EngineGKE)启动 Kubernetes 工作负载,并了解 GKE 的功能。

本章将涵盖以下主题:

  • 什么是 Google Cloud Platform 和 Google Kubernetes Engine?

  • 准备你的本地环境

  • 启动你的第一个 Google Kubernetes Engine 集群

  • 部署工作负载并与集群交互

  • 更多关于集群节点的信息

技术要求

要跟随本章内容,你需要一个带有有效支付方式的 Google Cloud Platform 帐户。

按照本章中的说明操作将会产生财务费用。因此,在完成使用后,务必终止你启动的任何资源。本章中列出的所有价格在写作时是准确的,我们建议你在启动任何资源之前查看当前的费用。

什么是 Google Cloud Platform 和 Google Kubernetes Engine?

在我们动手注册 Google Cloud Platform 帐户并安装工具之前,我们需要启动一个由 GKE 支持的集群。我们还应该讨论一下 Google Kubernetes Engine 以及它是如何诞生的。

从此,我将在本章正文中将 Google Cloud Platform 称为 GCP,将 Google Kubernetes Engine 称为 GKE。

Google Cloud Platform

在“三大”公有云提供商中,GCP 是最新的。在接下来的两章中,我们将探讨Amazon Web ServicesAWS)和Microsoft Azure

Google 进入公有云技术的方式与另外两家提供商不同。2008 年 4 月,Google 推出了其 Google App Engine 的公有预览版,这是其云服务的第一个组成部分。作为一项服务,App Engine 至今仍然可用,截止写作时(2024 年中期)。该服务允许开发者将应用程序部署在 Google 管理的运行时环境中,包括 PHP、Java、Ruby、Python、Node.js 和 C#,以及 Google 自家编程语言 Go,后者于 2009 年开源。

2010 年 5 月,Google Cloud Storage在 GCP 品牌下推出,紧接着是Google BigQuery和其预测 API 的预览版。2011 年 10 月,Google Cloud SQL上线。然后,在 2012 年 6 月,Google Compute Engine的预览版也上线了。

正如你所看到的,四年过去了,到了那时我们已经有了大多数人认为构成公有云服务的核心服务。

然而,大多数服务仍处于预览阶段;直到 2013 年,许多核心服务才会脱离预览并变为 正式发布GA)。这意味着可以安全地在大规模生产工作负载中运行,且更为重要的是,提供了 服务级别协议SLA),这对于 中小型企业SMEs)和大型企业采用该服务至关重要。所有这些都发生在 Google 启动 Kubernetes 的前一年。

2014 年底,Google 发布了 GKE 的第一个 Alpha 版本。

Google Kubernetes Engine

鉴于 Kubernetes 是在 Google 开发的,且 Google 在通过 Borg 项目大规模运行容器工作负载方面拥有丰富经验,因此 Google 成为第一个在 GKE 中提供自家 Kubernetes 服务的公共云提供商也就不足为奇了。

在 Kubernetes v1 发布并于 2015 年 7 月移交给 云原生计算基金会CNCF)维护后,仅仅一个月,GKE 服务就已成为 GA。

GKE 服务允许你启动并管理一个 CNCF 认证的 Kubernetes 集群,利用 GCP 的本地计算、存储和网络服务。此外,它还允许与平台的监控、身份和访问管理功能进行深度集成。

我们将在 第十七章 的结尾讨论不同云端 Kubernetes 服务的差异,以及你为什么应该使用它们。

既然我们已经了解了该服务的一些历史背景,我们可以注册并安装一些用于启动集群的管理工具。

准备本地环境

我们需要做的第一件事是让你能够访问 GCP。为此,你必须注册一个新账户或登录现有账户。让我们学习一下如何操作。

要注册 GCP 账户,你需要访问 cloud.google.com/。此页面会显示一个类似下面的界面:

网页截图自动生成的描述

图 15.1:Google Cloud 首页

如果你已经在使用 Google 服务,如 Gmail、YouTube,或拥有一部安卓手机,那么你已经拥有了一个 Google 账户。你可以使用这个账户进行注册,具体操作见 图 15.1。我已经登录到自己的 Google 账户,截图右上角的头像显示了这一点。

截至本文撰写时,Google 提供 $300 的信用额度,可以在 90 天内使用。你仍然需要输入有效的付款方式来利用这些免费信用。Google 这样做是为了确保不是一个自动化机器人在注册账户并滥用他们提供的信用额度。一旦信用额度使用完毕或过期,你将获得将账户升级为付费账户的选项。有关此优惠的更多细节,请参见以下链接:cloud.google.com/free/docs/free-cloud-features

如果你想利用免费的信用额度,可以点击这个链接:cloud.google.com/free/docs/free-cloud-features#try-it-for-yourself。点击 立即免费开始;如果你符合条件,按照屏幕上的提示操作,确保你已阅读相关条款和条件。注册后,你将进入 GCP 控制台。或者,如果你已经有了 GCP 账户,你可以直接登录 GCP 控制台:console.cloud.google.com/

现在我们已经有了账户,让我们学习如何创建一个项目,在其中启动我们的资源。

创建项目

GCP 有一个概念,即资源被启动到项目中。如果你刚刚注册了一个账户,那么在注册过程中会为你创建一个“My First Project”项目。

如果你使用的是现有账户,我建议创建一个新的项目来启动你的 GKE 集群。为此,点击 选择 菜单,该菜单位于页面左上角 GCP 标志右侧的顶部栏中。

这将显示你可以访问的所有项目,并允许你创建一个新项目。为此,请按以下步骤操作:

  1. 点击 新建项目

  2. 系统会要求你完成以下操作:

    • 给你的新项目命名。

    • 选择一个组织来附加到项目上。

    • 选择位置;这是一个存储项目的组织或文件夹。

  3. 输入上一步骤的详细信息后,点击 创建 按钮。

A screenshot of a computer 描述自动生成

图 15.2:创建新项目

既然我们已经有了一个启动资源的地方,我们可以开始安装 GCP 命令行工具了。

安装 GCP 命令行界面

在这里,我们将介绍如何在本地计算机上使用 GCP 命令行界面 (CLI) 。如果你不想安装或无法安装,也不用担心,因为有一种方法可以在云端运行 CLI。但让我们先从我选择的操作系统——macOS 开始。

在 macOS 上安装

如果你像我一样,在 macOS 上使用终端进行大量工作,那么你很可能在某个时候安装并使用过 Homebrew。

有关 Homebrew 的最新安装说明,请参见项目网站 brew.sh/

安装了 Homebrew 后,您可以使用以下命令安装 GCP CLI:

$ brew install --cask google-cloud-sdk 

您可以通过运行以下命令来测试安装:

$ gcloud version 

如果一切顺利,您应该会看到类似以下的输出:

计算机截图自动生成的描述

图 15.3:在 macOS 上检查版本号

如果您遇到问题,可以查看上面链接的文档;有关 GCP CLI 安装的信息,请参见:

$ brew info --cask google-cloud-sdk 

一旦安装并正常运行 GCP CLI,您可以继续本章的 初始化 部分。

在 Windows 上安装

在 Windows 上有几种安装 GCP CLI 的方式。第一种是打开一个 PowerShell 会话,并运行以下命令通过 Chocolatey 安装它。

像 macOS 上的 Homebrew 一样,Chocolatey 是一个包管理器,允许您通过 PowerShell 在 Windows 上轻松且一致地安装各种软件包,使用相同的命令语法,而不必担心 Windows 上存在的多种安装方法——更多信息请参见 chocolatey.org/

如果您没有安装 Chocolatey,可以在以管理员权限启动的 PowerShell 会话中运行以下命令:

$ Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 

一旦安装了 Chocolatey,或者如果您已经安装了它,运行以下命令:

$ choco install --ignore-checksum gcloudsdk 

另一种安装方式是从以下网址下载安装程序:

[dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe](https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe

)

下载后,双击可执行文件并按照屏幕上的提示进行操作。

安装完成后,打开一个新的 PowerShell 窗口并运行以下命令:

$ gcloud version 

如果一切顺利,您应该会看到类似于前一节 macOS 版本中截图所示的输出。再次提醒,安装并成功运行后,您可以继续本章的 初始化 部分。

在 Linux 上安装

虽然 Google Cloud CLI 包对于大多数发行版都可以使用,但为了涵盖各种包管理器,我们需要更多的空间,因此,我们将使用 Google 提供的全局安装脚本。

要运行此命令,您需要使用以下命令:

$ curl https://sdk.cloud.google.com | bash 

在安装过程中,脚本会询问您几个问题。对于大多数人来说,回答 Yes 就可以了。安装完成后,运行以下命令以重新加载会话,从而使其更新所有安装程序所做的更改:

$ exec -l $SHELL 

根据我们在前面章节中讨论的 macOS 和 Windows 安装方法,您可以运行以下命令来查看已安装的 GCP CLI 版本的详细信息:

$ gcloud version 

你可能已经注意到,尽管安装方法有所不同,但一旦安装完成并使用gcloud命令,我们会得到相同的结果。从这里开始,您运行的操作命令将适用于所有三种操作系统,因此您所运行的操作系统不再重要。

云 Shell

在我们开始安装 Google Cloud CLI 之前,我提到过还有第四个选项。该选项是 Google Cloud Shell,内置于 Google Cloud 控制台中。要访问此选项,请单击位于顶部菜单右侧的Shell图标。

配置完成后,您应该会看到一个基于 Web 的终端,您可以从中运行以下命令:

$ gcloud version 

输出略有不同,因为 Google 提供了完整的支持工具套件。但是,您将从以下屏幕看到,版本与我们在本地安装的版本匹配:

计算机截图描述自动生成

图 15.4:使用 GCP Cloud Shell

如果您使用 Google Cloud Shell,则可以跳过下面的初始化步骤,因为这已为您完成。

初始化

如果您选择在本地安装客户端,您需要执行最后一步操作,将其链接到您的 GCP 账户。为此,请运行以下命令:

$ gcloud init 

这将立即运行一个快速的网络诊断,以确保客户端具备运行所需的连接性。一旦诊断通过,您将被提示回答以下问题:

You must log in to continue. Would you like to log in (Y/n)? 

回答Y将打开一个浏览器窗口。如果没有打开,请复制并粘贴提供的 URL 到您的浏览器中,在那里您将选择要使用的账户后,将看到客户端请求的权限概述,如下屏幕所示:

计算机截图描述自动生成

图 15.5:审核权限

如果您同意授予权限,请点击允许按钮。返回到您的终端,您将收到登录用户的确认信息。然后,您将被要求选择要使用的云项目。列表中仅包含项目的唯一 ID,而不是您之前在 Google Cloud 控制台中看到或设置的友好名称。如果您有多个项目,请选择正确的项目。

如果您需要随时更新客户端使用的项目,可以运行以下命令:

$ gcloud config set project PROJECT_ID 

确保您用项目的唯一 ID 替换PROJECT_ID

现在您已经安装了 Google Cloud CLI 并配置了您的账户,您可以准备启动您的 GKE 集群。

启动您的第一个 Google Kubernetes Engine 集群

由于集群需要几分钟才能完全部署,让我们运行命令来启动该过程,并详细讨论在启动过程中发生的情况。

在启动集群之前,我们必须确保启用了container.googleapis.com服务。为此,请运行以下命令:

$ gcloud services enable container.googleapis.com 

然后我们需要安装一个插件,允许我们使用kubectl对集群进行身份验证;为此,请运行以下命令:

$ gcloud components install gke-gcloud-auth-plugin 

一旦启用了服务和插件,启动名为myfirstgkecluster的两节点集群的命令如下,该集群将托管在美国中部地区的单一区域:

$ gcloud container clusters create myfirstgkecluster --num-nodes=2 --zone=us-central1-a 

大约五分钟后,您应该看到如下所示的输出:

计算机程序截图自动生成的描述

图 15.6:启动集群

一旦集群启动,您应该能够按照输出中的 URL 并在 Google Cloud Console 中查看,如下所示:

计算机截图自动生成的描述

图 15.7:在 Google Cloud Console 中查看集群

您可以访问 Google Cloud Console,网址为console.cloud.google.com/

现在我们的集群已启动并运行,我们可以部署工作负载并更详细地查看 Google Cloud Console。

部署工作负载并与您的集群互动

从启动集群时得到的反馈中需要注意的一点是:

$ gcloud container clusters create myfirstgkecluster --num-nodes=2 --zone=us-central1-a 

输出中的以下行:

kubeconfig entry generated for myfirstgkecluster. 

正如您可能猜到的,这已经下载并配置了所有必要的信息,将您的本地kubectl副本连接到新部署的 GKE 集群。

您可以通过运行以下命令来确认这一点:

$ kubectl get nodes 

您从命令中获得的输出应该显示两个以gke为前缀的节点,因此它应类似于以下终端输出:

计算机截图自动生成的描述

图 15.8:使用 kubectl 列出节点

如果在运行上述输出时您的 GKE 集群节点已列出,并且您满意当前使用的kubectl配置,可以跳过本章的下一节,直接进入启动示例工作负载部分。

本章末尾的进一步阅读部分还提供了指向官方 GKE 文档的链接。

配置您的本地客户端

如果您需要配置另一个或您的kubectl安装以连接到您的集群,GCP CLI 提供了一个命令。

运行以下命令的前提是您已安装并配置了 GCP CLI。如果没有,请按照本章安装 GCP 命令行界面部分中的说明进行操作。

您需要运行的命令以下载凭据并配置kubectl如下:

$ gcloud container clusters get-credentials myfirstgkecluster --zone=us-central1-a 

如果需要切换到另一个配置(或称为上下文),可以运行以下命令。第一个命令列出当前的上下文:

$ kubectl config current-context 

以下命令列出了您已配置的所有上下文的名称:

$ kubectl config get-contexts -o name 

一旦你知道需要使用的上下文名称,可以运行以下命令,确保将CONTEXT_NAME替换为你修改后的上下文名称,如下所示:

$ kubectl config use-context CONTEXT_NAME 

现在你的 kubectl 客户端已配置为与 GKE 集群进行交互,我们可以启动示例工作负载。

启动示例工作负载

我们将启动的示例工作负载是官方 Kubernetes 文档中使用的 PHP/Redis Guestbook 示例。启动工作负载的第一步是创建 Redis Leader 部署和服务。为此,我们使用以下命令:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-service.yaml 

接下来,我们需要重复这个过程,但这次是启动 Redis Follower 部署和服务,如下所示:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-service.yaml 

现在我们已经启动了 Redis,接下来是启动前端部署和服务;这就是应用程序本身:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-service.yaml 

几分钟后,你应该能够运行以下命令,获取你刚刚启动的服务的信息:

$ kubectl get service frontend 

命令的输出应该会给出一个外部 IP 地址,类似以下终端输出:

计算机截图 Description automatically generated

图 15.9:获取前端服务的信息

现在我们已经启动了应用程序,复制 EXTERNAL-IP 值并将 IP 地址输入浏览器;确保使用 http://EXTERNAL-IP 而不是 https://EXTERNAL-IP,因为 Guestbook 监听的是端口 80 而不是 443

当你在浏览器中打开地址时,你应该看到简单的 Guestbook 应用程序。尝试提交一些消息,如下所示:

计算机截图 Description automatically generated

图 15.10:带有一些测试消息的 Guestbook 应用程序

现在我们已经启动了基本的工作负载,让我们回到并探索 Google Cloud Console。

探索 Google Cloud Console

我们已经在 Google Cloud Console 中看到了集群,接下来点击左侧菜单中的工作负载链接,该链接位于Kubernetes Engine部分。

计算机截图 Description automatically generated

图 15.11:在控制台中查看工作负载

如前所示,三个部署列出,并确认它们所属的命名空间和工作负载所属的集群。

如果我们有多个集群,且每个集群中有多个命名空间和部署,我们可以使用过滤器来深入查看我们的 GKE 工作负载。

工作负载

点击前端部署将让你查看更多信息:

计算机截图 Description automatically generated

图 15.12:获取前端部署的概览

在此页面上,你将能够通过部署名称下方的标签进一步深入查看你的部署,如下所示:

  • 概述:此视图为你提供所选工作负载的概览。如之前的截图所示,你可以看到 CPU、内存和磁盘的使用情况,以及其他信息。

  • 详情:此处列出了有关环境和工作负载本身的更多信息。在这里,你可以了解它是何时创建的、注解、标签(以及副本的详细信息)、更新策略以及 Pod 信息。

  • 可观测性:在这里,你可以查看工作负载基础设施的所有指标。

  • 修订历史:在这里,你会找到所有工作负载修订的列表。如果你的部署频繁更新并且需要跟踪部署何时更新,这将非常有用。

  • 事件:如果你的工作负载有任何问题,这是查看的地方。所有事件,如扩展、Pod 可用性和其他错误,将列在这里。

  • 日志:这是一个可搜索的日志列表,包含了运行该 Pod 的所有容器的日志。

  • 应用错误:在这里,你会找到任何应用程序错误。

  • YAML:这是一个可导出的 YAML 文件,包含完整的部署配置。

此信息适用于你在项目中启动的所有 GKE 集群中的所有部署。

网关、服务和入口

在这个部分,我们将查看 网关、服务和入口 部分。正如你可能已经猜到的,这里列出了在你所有 GKE 集群中启动的所有服务。你可以通过点击 网关、服务和入口 来查看。

当你首次进入此部分时,它默认会显示 网关 标签页,但我们还没有启动网关,因此你需要点击 服务 标签页。当加载完成后,你将看到如下所示的界面:

计算机截图自动生成的描述

图 15.13:在控制台中查看服务

如你所见,我们启动的三个服务都列在其中。然而,前端 服务有一个 外部负载均衡器 类型和一个公开的 IP 地址,而 redis-leaderredis-follower 服务则仅有一个 集群 IP

这是因为,在这三个服务中,我们只希望前端服务能够公开访问,因为只有我们的前端服务使用了两个 Redis 服务。

当外部负载均衡器启动时,由于我们在公共云提供商上运行集群,Kubernetes 调度器知道要联系 Google Cloud API 并启动负载均衡器供使用。然后,它将其配置为与我们的集群节点进行通信,暴露工作负载;该部署在外部运行在端口 80 上,流量将传递回节点上的端口 31740

和以前一样,点击三个运行中的服务之一会给你提供几个信息:

  • 概述:此视图提供了服务配置和利用情况的总结。

  • 详情:在这里,你可以找到有关服务的更多细节,并可以查看已在我们 Google Cloud 项目中启动的负载均衡器资源的链接。

  • 事件:如前所述,你可以在这里找到任何影响服务的事件。这对于故障排除非常有用。

  • 日志:这是 工作负载 部分中显示的日志的重复。

  • 应用程序错误:你将在这里找到任何应用程序的错误。

  • YAML:同样,这是将完整的服务配置导出为 YAML 文件的一种方式。

虽然这涵盖了我们启动的基本工作负载,但 GKE 提供了许多其他选项和功能。让我们快速浏览左侧菜单中其他项目背后的功能。

其他 GKE 特性

GKE 支持两种类型的特性;以下列出的为标准特性。还有可以在你运行 GKE 企业版时启用的企业特性,我们将在本节末尾讨论这些特性:

  • 集群:在这里,你可以管理你的集群;你将看到我们通过 GCP CLI 启动的集群,并可以使用引导式 Web 界面创建更多集群。

  • 工作负载:如前所述,你将在这里找到你部署到集群中的工作负载。

  • 应用程序:这里会显示你从市场中部署的所有应用程序,我们稍后会介绍这个内容。

  • 秘密和配置映射:在这里,你可以安全地管理你的秘密和配置映射。

  • 存储:你可以查看你在这里配置的任何持久存储声明,并管理存储类。

  • 对象浏览器:可以将它视为 K8s 和 GCP API 的资源管理器;点击它,你可以看到你可以与之交互的所有 API 和端点的概览,以及当前已部署的对象/资源。

以下服务将 Google 原生服务引入 GKE:

  • GKE 备份:在这里,你可以使用 GCP 的备份服务(它是 GKE 提供服务之外的)来备份你在 GKE 中托管的工作负载和应用程序。就我个人而言,当我在云端运行 Kubernetes 工作负载时,里面的数据是临时的。对于像数据库这样的服务,我希望有备份,它们运行在一个受 SLA 支持的、云原生的服务中,位于我的集群之外——但如果由于某些原因你不能这样做,那么这个服务可以满足你的需求。

  • 网关、服务和入口:我们已经介绍过这个选项。

  • 网络功能优化器:你可以在这里将 GKE 工作负载连接到其他 GCP 资源的私有网络。这使你能够扩展你的集群网络,以便像 Google Cloud 数据库这样的服务可以通过集群资源私密访问。

  • 市场:在这里,你可以找到可以在 GKE 集群中运行的预包装应用程序。这些应用程序由各种软件供应商发布,包括免费和商业产品。

如上所述,上述功能在 GKE 的标准版中是可用的;此版本类似于自己部署 Kubernetes,只是谷歌为您处理了节点部署和管理计划。

接下来是企业版;正如您可能已经猜到的那样,这引入了额外的功能,企业部署 Kubernetes 可能需要这些功能,例如托管服务网格、安全性、合规性和基于角色的访问控制选项。这些功能需要额外的费用,但对于可能在更受监管的行业中工作的大型组织来说是必不可少的。

删除您的集群

当您完成您的集群时,可以通过运行以下命令来删除它。第一个命令将删除我们创建的服务,第二个命令将终止集群本身:

$ kubectl delete service frontend
$ gcloud container clusters delete myfirstgkecluster --zone=us-central1-a 

删除集群可能需要几分钟,请确保在此过程完成之前等待。

这还应该删除您的工作负载启动的任何服务,例如负载均衡器。但是,我建议您在 Google Cloud 控制台上仔细检查是否有任何被遗弃的服务或资源,以确保您不会产生意外的费用。

我们一直在使用 --zone=us-central1-a 标志,在美国中部地区的一个可用区启动我们的集群。让我们讨论一下其他可用的集群选项。

关于集群节点的更多信息

在上一节的结尾,我提到了可用区和区域。在讨论一些集群部署选项之前,我们应该更好地理解一下我们所说的可用区和区域的含义:

  • 区域:一个区域由多个区域组成。不同区域内的区域具有出色的低延迟网络连接,可实现高可用性、始终在线和容错工作负载的部署。

  • 可用区:将可用区视为区域内的单独数据中心。这些区域具有不同的网络和电力,这意味着如果一个单一区域出现问题并且您在多个区域中运行您的工作负载,则您的工作负载不应受到影响。

关于区域的一点需要注意的是,并非所有机器类型在一个区域内的所有区域中都可用。因此,请在尝试部署工作负载之前进行检查。

谷歌的最佳实践建议您将您的工作负载跨单个区域内的最大区域数量部署,以获得最佳性能和可用性。

然而,可以通过使用多集群入口将您的工作负载分布在多个区域中 – 您只需考虑和允许共享服务之间的增加延迟,例如数据库。有关多集群入口的更多信息,请参见本章末尾的进一步阅读部分。

让我们重新看看如何在单个区域跨多个区域启动集群,通过检查我们在本章开头用于启动测试集群的命令:

$ gcloud container clusters create myfirstgkecluster --num-nodes=2 --zone=us-central1-a 

如我们所知,它将启动两个节点,但仅在一个区域内。我们只使用 -zone 标志传递了一个区域,在我们的例子中是 us-central1 区域和 a 区域。

让我们运行以下命令,但使用 --region 标志而不是 --zone 标志:

$ gcloud container clusters create myfirstgkecluster --num-nodes=2 --region=us-central1 

启动后,运行以下命令:

$ kubectl get nodes 

这应该会输出如下内容:

计算机程序的屏幕截图自动生成的描述

图 15.14:查看运行在区域中的节点

如你所见,我们在每个区域都有两个节点,总共六个节点。这是因为默认情况下,当你定义一个区域时,集群会分布在三个区域内。你可以通过使用--node-locations标志来覆盖此设置。

如果我们想从头开始部署一个集群,命令会像下面这样;正如你所见,我们现在有一个逗号分隔的可用区列表:

$ gcloud container clusters create myfirstgkecluster --num-nodes=2 --region=us-central1 --node-locations us-central1-a,us-central1-b,us-central1-c,us-central1-f 

我们仍然使用 us-central1 区域,但部署到 abcf 区域。

由于我们已经有一个正在运行的集群,我们可以运行以下命令来更新集群,添加新的可用区和两个新节点:

$ gcloud container clusters update myfirstgkecluster --min-nodes=2 --region=us-central1 --node-locations us-central1-a,us-central1-b,us-central1-c,us-central1-f 

运行 kubectl get nodes 命令现在会显示如下内容:

计算机程序的屏幕截图自动生成的描述

图 15.15:我们的集群现在跨四个可用区运行

如你所见,Google 已经使得在多个区域间部署集群变得简单。这意味着你可以轻松地将工作负载部署到一个完全冗余的集群中;考虑到手动使用虚拟机和网络来复制这种类型的部署所涉及的复杂性,这种简化是非常受欢迎的。

要删除使用 --region 标志部署的集群,你应该使用以下命令:

$ gcloud container clusters delete myfirstgkecluster --region=us-central1 

截至目前,我们在本章开始时启动的简单两节点集群每月约为 55 美元,而我们刚刚启动的八节点集群的费用约为每月 347 美元。有关更多费用信息,请参阅进一步阅读部分中的 Google Cloud 定价计算器链接。

这标志着我们对 GKE 的探讨结束。在我们继续讨论下一个公共云服务提供商之前,让我们总结一下我们已经覆盖的内容。

总结

在本章中,我们讨论了 GCP 和 GKE 服务的起源,然后介绍了如何注册帐户以及如何安装和配置 Google Cloud 命令行工具。

然后我们使用一个命令启动了一个简单的两节点集群,接着我们使用 kubectl 命令和 Google Cloud Console 部署并与工作负载进行交互。

最后,再次仅使用一个命令,我们重新部署了集群,以利用多个可用区,快速扩展到一个完全冗余且高度可用的八节点集群,跨四个可用区运行。

我相信你会同意,Google 在将复杂的基础架构配置的部署和维护工作变成相对简单和快速的任务方面做得非常出色。

此外,一旦你的工作负载部署完成,你可以像管理其他集群一样管理它们——我们实际上并没有因为集群在 GCP 上运行而做出任何特殊调整。

下一章将探讨如何使用 AWS 的 Amazon Elastic Kubernetes Service 部署 Kubernetes 集群,这是亚马逊完全托管的 Kubernetes 服务。

进一步阅读

下面是一些关于本章所涵盖的主题和工具的更多信息链接:

)

)

)

)

)

)

加入我们的社区,在 Discord 上进行交流

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十六章:在 Amazon Web Services 上使用 Amazon Elastic Kubernetes Service 启动 Kubernetes 集群

让我们在前一章所学的基础上继续前进。我们在公共云中启动了一个 Kubernetes 集群,迈出了在“前三大”公共云服务提供商中运行 Kubernetes 的第一步。

现在我们已经了解了Google Cloud PlatformGCP)的 Kubernetes 服务,我们将继续介绍Amazon Elastic Kubernetes ServiceEKS)由Amazon Web ServicesAWS)提供的服务。

在本章中,你将学习如何设置 AWS 账户,并在 macOS、Windows 和 Linux 系统上安装相关工具集,最后启动并与 Amazon EKS 集群进行交互。

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

  • 什么是 Amazon Web Services 和 Amazon Elastic Kubernetes Service?

  • 准备你的本地环境

  • 启动你的 Amazon Elastic Kubernetes Service 集群

  • 部署工作负载并与集群进行交互

  • 删除你的 Amazon Elastic Kubernetes Service 集群

技术要求

如果你计划跟随本章的内容进行操作,你需要一个附有有效支付方式的 AWS 账户。

按照本章中的说明操作将会产生费用,在使用完毕后,你必须终止你启动的所有资源。本章中列出的所有价格均为本书撰写时的有效价格,我们建议你在启动任何资源之前,查看当前的费用。

什么是 Amazon Web Services 和 Amazon Elastic Kubernetes Service?

你可能已经听说过 Amazon Web Services,简称 AWS。它是最早的公共云服务提供商之一,截至撰写本书时(2024 年 6 月),AWS 的市场份额最大,达到了 31%,微软 Azure 以 25% 排名第二,GCP 排名第三,占有 11% 的市场份额。

Amazon Web Services

正如你可能已经猜到的,Amazon 拥有并运营 AWS。从 2000 年 Amazon 开始通过为零售合作伙伴开发和部署应用程序编程接口APIs)来实验云服务,到现在 AWS 已发展成全球领先的云计算平台,服务于各行各业的各种规模的企业。

基于这项工作,Amazon 意识到他们需要构建一个更好、更标准化的基础设施平台,不仅要托管他们正在开发的服务,还要确保在更多零售网点使用这些软件服务并迅速增长时能够快速扩展。

Chris Pinkham 和 Benjamin Black 在 2003 年写了一篇白皮书,2004 年获得 Jeff Bezos 的批准,文中描述了一个可以通过编程方式部署计算和存储资源的基础设施平台。

AWS 首次公开承认其存在是在 2004 年末;然而,当时该术语用于描述一组工具和 API,允许第三方与 Amazon 的零售产品目录进行交互,而非我们今天所知的 AWS。

直到 2006 年,重新品牌化后的 AWS 才正式推出,从 3 月开始推出 简单存储服务 (S3)。此服务允许开发者使用 Web API 写入和提供单个文件,而无需从传统的本地或远程文件系统中写入和读取数据。

下一个推出的服务是 亚马逊简单队列服务 (SQS),它是原始 AWS 工具集的一部分。这是一个分布式消息系统,开发者可以通过 API 控制和使用它。

最后推出的服务是 2006 年的 亚马逊弹性计算云 (Amazon EC2) 的测试版,仅限现有的 AWS 客户。你仍然可以使用亚马逊开发的 API 启动 Amazon EC2 资源。

这对亚马逊来说是最后一块拼图。现在他们有了一个公共云平台的基础,不仅可以用于他们自己的零售平台,还可以将空间出售给其他公司和公众,比如你我。

让我们从 2006 年的 3 项服务开始,快速前进到 2024 年中期,当时已经有超过 200 项服务可用,所有这些服务都运行在 39 个区域的 125 个物理数据中心中,总面积超过 3800 万平方英尺。

所有 200 多个服务都遵循 2003 年白皮书中提出的核心原则。每个服务都是软件定义的,这意味着开发者只需发出一个简单的 API 请求来启动、配置,并在某些情况下消费该服务,然后再发出另一个 API 请求来终止它。

你可能已经注意到,从目前为止提到的服务来看,在 AWS 中运行的服务通常以亚马逊或 AWS 为前缀——这是为什么呢?因为以 Amazon 开头的服务是独立服务,而以 AWS 为前缀的服务则是设计为与以 Amazon 为前缀的服务一起使用的。

再也没有必要像过去那样去订购一项服务,等待某人构建并部署它,再交给你;这将部署时间从有时需要几周或几个月缩短到几秒钟。

我们不打算讨论所有 200 多项服务,这将需要一整套书籍;我们应该讨论本章将要研究的服务。

亚马逊弹性 Kubernetes 服务

虽然 AWS 是主要公有云提供商中第一个推出的,但它是最后一个推出独立 Kubernetes 服务的。亚马逊 EKS 于 2017 年底首次宣布,并于 2018 年 6 月在美国推出,首先在东部(北弗吉尼亚)和西部(俄勒冈)地区提供。

该服务旨在与其他 AWS 服务和功能一起使用并充分利用,例如以下内容:

  • AWS 身份与访问管理 (IAM) 使您能够控制和管理终端用户以及程序化访问其他 AWS 服务的权限。

  • Amazon Route 53 是 Amazon 的 域名系统DNS)服务。Amazon EKS 可以将其用作集群的 DNS 来源,意味着服务发现和路由可以在您的集群内轻松管理。

  • Amazon Elastic Block StoreEBS):如果您需要为在 Amazon EKS 集群中运行的容器提供持久性块存储,Amazon Elastic Block StoreEBS)提供此存储,就像为您的 EC2 计算资源提供存储一样。

  • EC2 自动扩展:如果您的集群需要扩展,将采用相同的技术来扩展您的 EC2 实例。

  • 多可用区AZs)可以是一个有用的功能。Amazon EKS 管理层和集群节点可以配置为分布在给定区域内的多个 AZ 上,以为您的部署提供 高可用性HA)和弹性。

在启动 Amazon EKS 集群之前,我们需要下载、安装并配置一些工具。

准备本地环境

我们需要安装两个命令行工具,但在安装之前,我们先快速讨论一下注册 AWS 新账户的步骤。如果您已经有 AWS 账户,请跳过此任务,直接进入 安装 AWS 命令行界面 部分。

注册 AWS 账户

注册 AWS 账户是一个简单的过程,如下所述:

  1. 访问 aws.amazon.com/ 并点击页面右上角的 创建 AWS 账户 按钮。

亚马逊为新用户提供了免费套餐。它仅限于某些服务和实例规格,且持续 12 个月。有关 AWS 免费套餐的详细信息,请参见 aws.amazon.com/free/

  1. 填写要求提供电子邮件地址的初始表单。该电子邮件地址将用于账户恢复和一些基本的管理功能。同时,提供您选择的 AWS 账户名称。如果您改变主意,不必担心;注册后可以在账户设置中更改该名称。然后,您需要验证您的电子邮件地址。验证电子邮件地址后,您将被要求为您的“根”账户设置密码。

  2. 输入密码后,点击 继续 并按照屏幕上的指示操作。共有五个步骤;这些步骤将涉及您通过自动电话确认您的支付信息和身份。

一旦您创建并启用了账户,您将在可以开始使用 AWS 服务时收到通知——大多数情况下,这将是立即开始,但也可能需要长达 48 小时的时间。

现在,我们需要安装将用于启动 Amazon EKS 集群的命令行工具。

安装 AWS 命令行界面

接下来的任务是安装 AWS 命令行界面CLI)。正如我们在上一章 第十五章Google Kubernetes Engine 上的 Kubernetes 集群 中所做的那样,我们将针对 Windows、Linux 和 macOS 进行操作,我们将首先查看 macOS 部分。

在 macOS 上安装

在 macOS 上使用 Homebrew 安装 AWS CLI 就像运行以下命令一样简单:

$ brew install awscli 

安装完成后,运行以下命令,它应该会给出版本号:

$ aws –version 

这将输出 AWS CLI 的版本,以及它所需的一些支持服务,如下图所示:

一台计算机的截图自动生成的描述

图 16.1:检查 AWS CLI 版本

安装完成后,你可以继续进行 AWS CLI 配置部分。

在 Linux 上安装

虽然每个发行版都有可用的软件包,但在 Linux 上安装 AWS CLI 最简单的方式是下载并运行安装程序。

这些指令假设你已经安装了curlunzip软件包。如果没有,请使用你的发行版的包管理器安装它们。例如,在 Ubuntu 上,你需要运行sudo apt-get install unzip curl来安装这两个软件包。

要下载并安装 AWS CLI,请运行以下命令:

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install 

安装完成后,你应该能够执行aws --version命令,你会看到类似于在 macOS 上安装部分中的输出,以及我们接下来会看的 Windows 版本。

在 Windows 上安装

与 macOS 类似,你也可以使用包管理器安装 AWS CLI。正如第十五章Google Kubernetes Engine 上的 Kubernetes 集群》中所提到的,我们将使用 Chocolatey。你需要运行的命令如下所示:

$ choco install awscli 

使用 Chocolatey 安装后,执行下面的命令将给出类似于我们在 macOS 上看到的输出,操作系统和 Python 版本的变化如下所示:

$ aws --version 

同样,安装完成后,你可以继续下面的AWS CLI 配置部分。

AWS CLI 配置

一旦你安装了 AWS CLI,并通过运行aws --version命令检查它是否正常运行,你必须将本地 CLI 安装与 AWS 账户链接。为此,你需要登录到 AWS 控制台,可以通过console.aws.amazon.com/访问。

登录后,在页面左上方的搜索框中输入IAM,它位于服务按钮旁边。然后,点击IAM 身份中心服务链接,进入IAM 身份中心页面。

我们需要创建一个具有编程访问权限的用户;为此,请按照以下步骤操作:

  1. 根据你的 AWS 账户的年龄或访问权限,你可能需要启用IAM 身份中心。我的 AWS 账户是用于我个人项目的,所以点击启用按钮后,我选择了仅在此 AWS 账户中启用选项,而不是推荐的与 AWS Organizations 一起启用。我之所以这样做,是因为我没有,也不需要在单一组织中管理多个 AWS 账户。请按照屏幕上的指示启用该服务。

  2. 一旦启用 IAM 身份中心 服务,我们必须创建一个仅限编程访问的用户。为此,请返回屏幕顶部菜单中的搜索框,再次搜索 IAM,但这次选择 IAM,该选项列为 管理 AWS 资源访问。页面加载后,点击左侧菜单中的 用户,可以在 访问管理 部分找到,然后点击 创建用户 按钮。

  3. 输入 ekscluster 的用户名,并确保不要选择 为用户提供 AWS 管理控制台访问权限 - 可选 选项,然后点击 下一步。我们将在本章稍后讨论此选项,一旦我们启动了集群。

计算机截图描述自动生成

图 16.2:添加用户

  1. 我们不会创建一个新的组,而是将现有策略附加到我们的用户。为此,选择 直接附加现有策略,选择 AdministratorAccess 策略,然后点击 下一步

计算机截图描述自动生成

图 16.3:分配权限

由于这是在非生产环境的 AWS 账户中进行的测试,而且我们将在本章结束时删除该用户,因此我使用了一个相对宽松的策略。如果你打算将其部署到更接近生产环境的环境中,建议参考 AWS 文档,了解如何设置正确的权限和策略的详细指南。

  1. 审核信息后,点击 创建用户 按钮。一旦用户创建完成,从列表中选择 ekscluster 用户,选择 安全凭证 标签,然后在 访问密钥 部分点击 创建访问密钥 按钮。选择 命令行界面(CLI) 并继续创建访问密钥,点击 下一步 然后点击 创建访问密钥 按钮;密钥创建后,点击 下载 .csv 文件,最后点击 完成 按钮。

保管好你下载的文件,因为它包含了访问你的 AWS 账户的有效凭证。

返回到终端,然后运行以下命令以创建一个默认的配置文件:

$ aws configure 

这将要求输入以下几项信息:

  • AWS 访问密钥标识符ID):这是我们从 逗号分隔值CSV)文件中获取的访问密钥 ID。

  • AWS 秘密访问密钥:这是从 CSV 文件中获取的密钥。

  • 默认区域名称:我输入了 us-west-2

  • 默认输出格式:我将其留空。

为了测试一切是否正常工作,你可以运行以下命令:

$ aws ec2 describe-regions 

这将列出可用的 AWS 区域,输出应该类似于以下内容:

带有白色文字的计算机屏幕描述自动生成

图 16.4:测试 AWS CLI

现在我们已经安装并配置好了 AWS CLI 用于我们的账户,我们需要安装第二个命令行工具,使用它来启动 Amazon EKS 集群。

安装 eksctl,这是 Amazon EKS 的官方 CLI

虽然可以使用 AWS CLI 启动 Amazon EKS 集群,但它比较复杂并且有很多步骤。为了解决这个问题,Weaveworks 创建了一个简单的命令行工具,它生成 AWS CloudFormation 模板并启动你的集群。

不幸的是,Weaveworks 在 2024 年初停止了商业运营,但在停止运营之前,他们将该项目的控制权交给了 AWS 团队。

AWS CloudFormation 是 Amazon 的基础设施即代码(IaC)定义语言。它让你能够定义你的 AWS 资源,以便它们可以在多个账户之间或在同一个账户中反复部署。这在你需要不断创建环境时非常有用,例如,作为持续集成(CI)构建的一部分。

如你所料,在 macOS 和 Windows 上的安装遵循我们一直在使用的相同模式;macOS 用户可以运行以下命令:

$ brew install eksctl 

同样地,在 Windows 上,你可以运行:

$ choco install eksctl 

在 Linux 上安装eksctl与其他工具略有不同,命令如下:

$ PLATFORM=$(uname -s)_$(uname -m)
$ curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
$ tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
$ sudo mv /tmp/eksctl /usr/local/bin 

安装完成后,你应该能够运行下面的命令来获取版本号:

$ eksctl version 

所以,我们现在已经准备好启动我们的 Amazon EKS 集群。

启动你的 Amazon Elastic Kubernetes Service 集群

在安装好所有前提条件后,我们终于可以开始部署我们的 Amazon EKS 集群。一旦部署完成,我们将能够开始与它互动,启动一个工作负载,就像我们在第十五章中所做的那样,在 Google Kubernetes Engine 上部署 Kubernetes 集群

为此,我们将使用eksctl命令中内置的默认设置,因为这只是一个沙箱 Amazon EKS,我们可以在上面运行一些命令。这将启动一个具有以下属性的 Amazon EKS 集群:

  • us-west-1区域

  • 使用两个工作节点,采用m5.large实例类型

  • 使用官方 AWS EKS Amazon 机器镜像(AMI)

  • 使用Amazon 的虚拟私有云(VPC)作为其网络服务

  • 使用自动生成的随机名称

所以,事不宜迟,让我们通过运行以下命令来启动我们的集群:

$ eksctl create cluster 

你可以去做点饮料或者处理一下电子邮件,因为这个过程可能需要最多 30 分钟才能完成。如果你不打算部署 Amazon EKS 集群,下面是我运行命令时的输出。

首先,显示一些关于eksctl版本的基本信息以及将使用的区域:

[i]  eksctl version 0.180.0-dev+763027060.2024-05-29T21:36:10Z
[i]  using region us-west-2 

接下来,它将提供一些关于网络和可用区(AZ)的信息,说明它将把资源部署到哪些地方,如下面的代码片段所示:

[i]  setting availability zones to [us-west-2d us-west-2b us-west-2c]
[i]  subnets for us-west-2d - public:192.168.0.0/19 private:192.168.96.0/19
[i]  subnets for us-west-2b - public:192.168.32.0/19 private:192.168.128.0/19
[i]  subnets for us-west-2c - public:192.168.64.0/19 private:192.168.160.0/19 

它现在将显示将要使用的 AMI 版本的详细信息,以及该镜像所支持的 Kubernetes 版本,如下所示:

[i]  nodegroup "ng-11c87ff4" will use "[AmazonLinux2/1.29]"
[i]  using Kubernetes version 1.29 

现在它已经知道了所有元素,将要创建一个集群。在这里,你可以看到它开始部署:

[i]  creating EKS cluster "hilarious-wardrobe-1717847351" in "us-west-2" region with managed nodes
[i]  will create 2 separate CloudFormation stacks for cluster itself and the initial managed nodegroup
[i]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=us-west-2 --cluster=hilarious-wardrobe-1717847351'
[i]  Kubernetes API endpoint access will use default of {publicAccess=true, privateAccess=false} for cluster "hilarious-wardrobe-1717847351" in "us-west-2" 

如你所见,它已经将我的集群命名为hilarious-wardrobe-1717847351;这个名称将在整个构建过程中被引用。默认情况下,日志记录是禁用的,正如我们在这里所见:

[i]  CloudWatch logging will not be enabled for cluster "hilarious-wardrobe-1717847351" in "us-west-2"
[i]  you can enable it with 'eksctl utils update-cluster-logging --enable-types={SPECIFY-YOUR-LOG-TYPES-HERE (e.g. all)} --region=us-west-2 --cluster=hilarious-wardrobe-1717847351' 

现在是我们等待的时候,控制平面和集群正在部署:

[i]2 sequential tasks: { create cluster control plane "hilarious-wardrobe-1717847351",2 sequential sub-tasks: {wait for control plane to become ready, create managed nodegroup "ng-11c87ff4",}}
[i]  building cluster stack "eksctl-hilarious-wardrobe-1717847351-cluster"
[i]  waiting for CloudFormation stack "eksctl-hilarious-wardrobe-1717847351-cluster"
[i]  building managed nodegroup stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4"
[i]  deploying stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4"
[i]  waiting for CloudFormation stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4"
[i]  waiting for the control plane to become ready 

部署后,它将下载集群凭证并配置kubectl,如下所示:

[✔]  saved kubeconfig as "/Users/russ.mckendrick/.kube/config"
[i]  no tasks
[✔]  all EKS cluster resources for "hilarious-wardrobe-1717847351" have been created 

最后的步骤是等待节点变为可用,如此处所示:

2024-06-08 13:03:34 [✔]  created 0 nodegroup(s) in cluster "hilarious-wardrobe-1717847351"
2024-06-08 13:03:35 [i]  node "ip-192-168-34-120.us-west-2.compute.internal" is ready
2024-06-08 13:03:35 [i]  node "ip-192-168-67-233.us-west-2.compute.internal" is ready
2024-06-08 13:03:35 [i]  waiting for at least 2 node(s) to become ready in "ng-11c87ff4"
2024-06-08 13:03:35 [i]  nodegroup "ng-11c87ff4" has 2 node(s)
2024-06-08 13:03:35 [i]  node "ip-192-168-34-120.us-west-2.compute.internal" is ready
2024-06-08 13:03:35 [i]  node "ip-192-168-67-233.us-west-2.compute.internal" is ready
2024-06-08 13:03:35 [✔]  created 1 managed nodegroup(s) in cluster "hilarious-wardrobe-1717847351" 

现在我们已经让两个节点在线并准备就绪,是时候显示一条消息来确认一切已准备好,如下所示:

2024-06-08 13:03:36 [i]  kubectl command should work with "/Users/russ.mckendrick/.kube/config", try 'kubectl get nodes'
2024-06-08 13:03:36 [✔]  EKS cluster "hilarious-wardrobe-1717847351" in "us-west-2" region is ready 

现在集群已经准备好,让我们按照输出提示,运行kubectl get nodes。如预期所示,这将为我们提供组成集群的两个节点的详细信息,如下图所示:

一张计算机屏幕的截图描述自动生成

图 16.5:查看两个 Amazon EKS 集群节点

现在我们的集群已经启动并运行,让我们部署与之前在Google Kubernetes EngineGKE)集群中启动时相同的工作负载。

部署工作负载并与集群进行交互

第十五章Google Kubernetes Engine 上的 Kubernetes 集群中,我们使用了来自 GCP GKE 示例 GitHub 仓库的 Guestbook 示例。在这一部分,我们首先将部署工作负载,然后再探索基于 Web 的 AWS 控制台。所以现在让我们开始部署 Guestbook。

部署工作负载

即使我们的集群运行在 AWS 中,使用 Amazon EKS,我们仍然会使用之前在 GKE 中启动工作负载时所使用的相同 YAML 文件,并通过本地的kubectl进行操作;为此,请按照以下步骤操作:

  1. 如之前所述,我们的第一步是通过运行以下两条命令来启动 Redis 主节点的部署和服务:

    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-deployment.yaml
    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-service.yaml 
    
  2. 一旦 Redis 主节点部署完成,我们需要按以下步骤启动 Redis 从节点的部署和服务:

    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-deployment.yaml
    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-service.yaml 
    
  3. 一旦 Redis 主节点和从节点启动并运行,就该使用以下命令启动前端部署和服务:

    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-deployment.yaml
    $ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-service.yaml 
    
  4. 几分钟后,我们将能够运行以下命令以获取有关我们刚刚启动的服务的信息,其中应该包括访问工作负载的相关细节:

    $ kubectl get service frontend 
    

你会注意到,这次输出与我们在 GKE 上运行工作负载时得到的输出略有不同,如下图所示:

一张计算机的截图描述自动生成

图 16.6:获取前端服务信息

如你所见,我们得到的不是互联网协议IP)地址,而是统一资源定位符URL)。将其复制到浏览器中。

一旦你打开了网址,鉴于我们使用了相同的命令和工作负载配置,你不会对看到Guestbook应用感到惊讶,如下图所示:

![一张计算机的截图

描述自动生成

图 16.7:Guestbook 应用示例

现在我们的工作负载已经启动并运行,让我们来探索一下在 AWS 控制台中可以看到关于我们集群的信息。

探索 AWS 控制台

本节将通过 AWS 控制台查看我们新部署的工作负载。首先,登录到 AWS 控制台,网址是console.aws.amazon.com/。登录后,选择屏幕右上角用户名旁边的美国西部(俄勒冈)us-west-2区域。

![计算机截图描述自动生成图 16.8:选择正确的区域选择正确的区域后,在右上角的搜索栏中搜索弹性 Kubernetes 服务,并选择该服务,它应该是第一个结果,进入俄勒冈区域的 EKS 页面。![计算机截图描述自动生成图 16.9:我们首次查看 AWS 控制台中的 EKS 集群到目前为止,一切顺利;好了,我们稍后会谈到它——点击你的集群名称,你将看到类似以下页面的内容:![计算机截图描述自动生成图 16.10:访问被拒绝!那么,让我们来分析一下这里发生了什么。你可能会想,“这是我的主用户,肯定拥有完全访问权限吧?”这么做是有原因的;当eksctl启动我们的集群时,它授予了我们之前创建的ekscluster用户权限,以便使用 AWS 服务与集群进行交互,因为我们已配置 AWS CLI 使用该用户连接,而不是我们当前登录的主用户。这意味着要在 AWS 控制台中查看工作负载等内容,我们需要以之前创建的用户身份登录。为此,返回到 AWS 控制台中的 IAM,进入用户页面,选择ekscluster用户;然后转到安全凭证标签页,点击启用控制台访问按钮:![计算机截图描述自动生成图 16.11:为 ekscluster 用户启用控制台访问选择自动生成的密码选项并启用访问;最后,像之前一样下载包含凭证的 CSV 文件。下载完成后,退出 AWS 控制台并打开你下载的 CSV 文件。进入控制台登录网址。这是一个允许 IAM 用户(如我们创建的用户)登录到你账户的 URL;使用 CSV 文件中的用户名和密码。登录后,返回 EKS 页面并选择你的集群;这次你将不再看到权限相关的警告。当你首次打开集群时,你将看到几个标签页。它们是:+ 概述:显示各种集群详细信息,例如运行的 Kubernetes 版本、端点信息、集群状态、创建时间和日期等+ 资源:提供有关节点、Pods、命名空间和工作负载的信息+ 计算:显示节点信息、节点组以及与集群关联的任何 Fargate 配置文件的详细信息+ 网络:详细说明 VPC 配置+ 附加组件:列出已安装和可用的集群附加组件+ 访问:显示 IAM 角色、AWS 认证 ConfigMap 和 Kubernetes RBAC 角色绑定+ 可观察性:配置并显示日志、监控和最近的事件+ 升级洞察:列出可用的 Kubernetes 版本升级和兼容性检查+ 更新历史:提供集群和节点组更新的历史记录+ 标签:列出并管理与 EKS 集群相关的标签下面,你可以看到有关节点的详细信息:计算机截图描述自动生成

图 16.12:在计算选项卡中查看集群中的两个节点

点击资源,选择部署,并过滤到default工作区,将显示我们启动的工作负载:

计算机截图描述自动生成

图 16.13:查看我们的工作负载

点击其中一个部署将为你提供有关该部署的更多信息——包括 Pods、配置等详细信息。然而,当你四处点击时,你会发现实际上你只能查看有关服务的信息;没有图表、日志输出或任何提供比基本概述更多信息的内容。这是因为 AWS 控制台主要只是暴露 Kubernetes 本身的信息。

从 EKS 服务页面移开,进入 AWS 控制台的 EC2 服务部分,将显示两个节点,如下图所示:

计算机截图描述自动生成

图 16.14:查看原始 EC2 计算资源

在这里,你可以深入了解实例的更多信息,包括 CPU、RAM 和网络利用率;不过,这仅适用于实例本身,而不是我们的 Kubernetes 工作负载。

从左侧菜单的负载均衡部分选择负载均衡器,将显示我们在应用前端服务时启动和配置的弹性负载均衡器,如下图所示:

计算机截图描述自动生成

图 16.15:查看原始负载均衡器资源

我们使用的最后一个 AWS 服务是 AWS CloudFormation,因此在服务菜单中输入CloudFormation并点击链接将带你进入 CloudFormation 服务页面。

在这里,你将看到两个堆栈:一个是 EKS 节点,即我们的两个 EC2 实例,另一个是 EKS 集群,即我们的 Kubernetes 管理平面。这些堆栈在以下截图中有所展示:

一张计算机截图 Description automatically generated

图 16.16:构成我们集群的两个堆栈

选择其中一个堆栈将给出堆栈启动时发生的详细信息。它将列出在使用 eksctl 启动 Amazon EKS 集群时创建的所有资源。

你选择一个模板,然后在设计器中查看它;你甚至可以看到 eksctl 生成的 CloudFormation 模板,这是一个相当复杂的 JSON 文件——如果你点击 在应用程序作曲器中查看 按钮,你将能够获得堆栈的更易于理解的可视化表示。以下是此视图的截图:

一张计算机截图 Description automatically generated

图 16.17:在应用程序作曲器中查看 CloudFormation 模板

这就是我们在 AWS 控制台中能看到的所有内容。正如我们所看到的,虽然使用 eksctl 启动 Amazon EKS 相对简单,但与我们在上一章中启动的 GKE 集群相比,它与 AWS 控制台的集成程度可以做得更好。

虽然我们能够浏览并查看我们的工作负载,但我们无法进行过多交互;此外,集群的反馈也仅限于 Amazon EC2 服务提供的基本监控。

一旦你完成了对 Amazon EKS 集群的使用,你可以删除它。

删除你的 Amazon Elastic Kubernetes Service 集群

你可以通过运行以下命令删除你的集群,确保将集群名称替换为你自己的名称:

$ eksctl delete cluster --name hilarious-wardrobe-1717847351 

删除集群所需的时间比启动时更少;实际上,删除过程大约需要 5 分钟。

如前所述,eksctl 会在删除资源时提供其操作的详细信息:

[i]  deleting EKS cluster "hilarious-wardrobe-1717847351"
[i]  will drain 0 unmanaged nodegroup(s) in cluster "hilarious-wardrobe-1717847351"
[i]  starting parallel draining, max in-flight of 1
[i]  deleted 0 Fargate profile(s) 

第一个更新的是本地 kubectl 配置,如下所示:

[✔]  kubeconfig has been updated 

然后,任何作为将工作负载部署到我们集群的一部分而启动的资源都会被终止:

[i]  cleaning up AWS load balancers created by Kubernetes objects of Kind Service or Ingress 

然后,两个 AWS CloudFormation 堆栈被移除,从而移除了它们创建和配置的所有资源,如以下代码片段所示:

[i]  2 sequential tasks: { delete nodegroup "ng-11c87ff4", delete cluster control plane "hilarious-wardrobe-1717847351" [async] }
[i]  will delete stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4"
[i]  waiting for stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4" to get deleted
[i]  waiting for CloudFormation stack "eksctl-hilarious-wardrobe-1717847351-nodegroup-ng-11c87ff4"
[i]  will delete stack "eksctl-hilarious-wardrobe-1717847351-cluster"
[✔]  all cluster resources were deleted 

此时,我们的集群已完全删除。

请再次检查 AWS 控制台中的 EC2、EKS 和 CloudFormation 部分,确保所有服务已被正确删除,因为如果有任何孤立或闲置的资源留下,你将会被收费。虽然这种情况不太可能发生,但最好现在核对一下,而不是在月底收到意外的账单。

那么,我们的 Amazon EKS 集群运行一个月的费用大概是多少?

我们需要考虑两类费用:

  • 第一个费用是针对 Amazon EKS 集群本身。每创建一个 Amazon EKS 集群,费用为每小时 0.10 美元(USD);然而,每个 Amazon EKS 集群可以运行多个节点组,因此你不需要在每个区域启动多个集群。这意味着 Amazon EKS 集群的费用大约为每月 73 美元。

  • 下一项考虑因素是集群使用的 AWS 资源,例如,在我们的案例中,EC2 集群节点的费用大约为每个 70 美元,运行我们集群一个月的总费用大约为 213 美元。我说的是大约,因为还会有带宽费用和 AWS Elastic Load BalancingELB)服务的费用,这将进一步增加我们的工作负载成本。

价格概览页面的链接可以在本章末尾的进一步阅读部分找到。

总结

在本章中,我们讨论了 AWS 和 Amazon EKS 的起源,然后介绍了如何注册账户,以及如何安装和配置两个命令行工具,便于轻松启动 Amazon EKS 集群。

一旦我们的集群启动并运行,我们就部署了与启动 GKE 集群时相同的工作负载。我们无需针对不同云提供商上的工作负载做出任何调整——它直接就能运行,甚至在我们没有指示的情况下,使用 AWS 原生的负载均衡服务部署了负载均衡器。

然而,我们确实发现,与我们之前查看的 Google 服务相比,Amazon EKS 与 AWS 控制台的集成程度较低。我们还了解到,由于使用 AWS CLI 启动集群的复杂性,我们不得不安装第二个命令行工具以便轻松启动集群。假设 Amazon VPC 配置和 IAM 角色已创建并部署,这大约需要八个步骤。

与其他提供商相比,缺乏集成以及启动和维护集群的复杂性让我不太愿意在 Amazon EKS 上运行 Kubernetes 工作负载——这一切感觉有些支离破碎,不如 Google 的服务流畅。

在下一章中,我们将讨论在 Microsoft Azure 上启动Azure Kubernetes ServiceAKS)集群,这是我们将要介绍的三大公共提供商中的最后一个。

进一步阅读

以下是我们在本章中涉及的一些话题和工具的更多信息链接:

)

)

)

)

)

)

)

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十七章:在 Microsoft Azure 上使用 Azure Kubernetes 服务的 Kubernetes 集群

我们将要研究的最后一个公有云 Kubernetes 服务是 Azure Kubernetes 服务 (AKS),它托管在 Microsoft Azure 中,是“三大”公有云提供商之一 —— 另外两个我们已经在第十五章《Google Kubernetes Engine 上的 Kubernetes 集群》和第十六章《在 Amazon Web Services 上使用 Amazon Elastic Kubernetes 服务启动 Kubernetes 集群》中介绍过了。

在本章结束时,您将配置好本地环境,具备与 Microsoft Azure 账户交互并启动 AKS 集群所需的工具。

部署集群后,我们将启动与前几章相同的工作负载,并探索您的 AKS 集群与 Microsoft Azure 门户之间的集成水平。

最后,在本章结束时,我们将讨论我们在本章和前两章中介绍的三个服务,以及我推荐的服务。

为此,我们将涵盖以下主题:

  • 什么是 Microsoft Azure 和 Azure Kubernetes 服务?

  • 准备您的本地环境

  • 启动您的 Azure Kubernetes 服务集群

  • 部署工作负载并与集群交互

  • 删除您的 Azure Kubernetes 服务集群

技术要求

如果您打算跟随本章中涵盖的示例操作,您需要一个有效支付方式关联的 Microsoft Azure 账户。

跟随本章中的示例操作将产生费用,因此完成操作后,终止您启动的所有资源以避免不必要的支出是非常重要的。本章中所列价格在印刷时是准确的,我们建议您在启动任何资源之前先查看当前费用。

什么是 Microsoft Azure 和 Azure Kubernetes 服务?

在我们开始安装支持工具之前,让我们快速讨论一下我们将要了解的服务的起源,从 Microsoft Azure 开始。

Microsoft Azure

2008 年,微软正式宣布推出名为 Windows Azure 的新服务。

这个服务是一个名为 Project Red Dog 的项目的一部分,该项目自 2004 年以来一直在开发,旨在利用核心的 Windows 组件提供数据中心服务。

微软在 2008 年开发者大会上宣布的五个核心组件如下:

  • Microsoft SQL Data Services:这是微软 SQL 数据库服务的云版本,作为 平台即服务 (PaaS) 运行,旨在消除托管 SQL 服务的复杂性。

  • Microsoft .NET Services:另一个 PaaS 服务,允许开发者将他们基于 .NET 的应用程序部署到 Microsoft 管理的 .NET 运行时中。

  • Microsoft SharePoint:流行的内部网产品的 软件即服务 (SaaS) 版本。

  • Microsoft Dynamics:微软 CRM 产品的 SaaS 版本。

  • Windows Azure:一种基础设施即服务IaaS)产品,像其他云服务提供商一样,允许用户启动虚拟机、存储以及支持计算工作负载所需的网络服务。

所有这些服务都是建立在 Red Dog 操作系统之上的,该操作系统为项目命名。这是一个专门版的 Windows 操作系统,内置了云层。

2014 年,Windows Azure 更名为 Microsoft Azure,体现了提供云服务的底层操作系统名称,同时也表明 Azure 运行着许多基于 Linux 的工作负载。作为这一宣布的一部分,新任微软 CEO 萨蒂亚·纳德拉展示了那张现在著名(或臭名昭著,取决于你的视角)“微软爱 Linux”幻灯片,幻灯片上用心形表情符号代表“爱”字。

我之所以说“著名”和“臭名昭著”,是因为前微软首席执行官史蒂夫·巴尔默曾引用过以下的话:

“Linux 是一种癌症,它在知识产权意义上附着于它所接触到的所有事物。”

因此,这被视为一次重大转变,令许多人感到意外。

2020 年,公开数据显示超过 50%的虚拟机核心正在运行 Linux 工作负载,60%的 Azure Marketplace 镜像现在基于 Linux。这主要归因于微软对 Linux 和开源项目(如 Kubernetes)的拥抱,这也使我们走向了他们的本地 Kubernetes 服务。

Azure Kubernetes 服务

最初,微软推出了一种名为Azure Container ServiceACS)的基于容器的服务。它允许用户部署容器工作负载,并选择由三种不同的编排工具之一来支持这些工作负载:Docker Swarm、DC/OS 或 Kubernetes。所有这些都提供了基于容器的集群解决方案。

很快,Kubernetes 显现出比另外两个编排工具更受欢迎,因此 ACS 逐渐被 AKS 取代。AKS 是一个符合 CNCF 标准、完全基于 Kubernetes 的服务。这一过渡大约花费了两年时间,AKS 在 2018 年开始全面发布,而 ACS 则在 2020 年初退休。

AKS 服务与 Azure Active Directory、策略以及其他关键的 Microsoft Azure 服务紧密集成。除了 AKS,微软还提供了其他容器服务;最新的是 Azure 容器应用。

Azure Container Apps 是一个无服务器平台,运行容器化应用程序,用户无需管理基础设施,提供如基于流量、事件、CPU 或内存负载的动态扩展、HTTPS 或 TCP 入口、Dapr 集成和自动扩展等功能。它允许使用各种 Azure 原生工具进行管理、安全秘密处理、内部服务发现以及部署策略的流量拆分。应用程序可以从任何注册表中运行容器,并与其他 Azure 服务集成。令人困惑的是,你也可以选择在 Azure 应用服务中启动基于容器的工作负载。

然而,与其讨论 Microsoft Azure 中可以用来运行基于容器的工作负载的所有服务,我发现亲自动手操作某个服务更为简便,因此不再拖延,接下来我们来看如何安装启动和管理 AKS 集群所需的工具。

准备您的本地环境

在启动集群之前,您需要完成一些任务。首先,您需要一个地方来启动集群,因此如果您还没有账户,您需要注册一个 Azure 账户。

创建一个免费的 Microsoft Azure 账户

如果您还没有账户,请访问 azure.microsoft.com/free/,在此您可以注册一个免费账户:

白色背景下的电脑描述自动生成

图 17.1:查看您可以免费获得的 Microsoft Azure 服务

在撰写本文时,您的免费账户包括 12 个月的热门服务、$200 的信用额度,可用于探索和测试不同的 Azure 服务,并且可以访问超过 55 种始终免费的服务。

点击开始免费按钮,并按照屏幕上的指示操作。注册过程大约需要 15 分钟,您需要提供有效的信用卡或借记卡信息以完成注册并获得免费账户。

一旦您能够访问您的账户,下一步就是安装 Azure CLI。

Azure CLI

Microsoft 提供了一个功能强大的跨平台命令行工具,用于管理您的 Microsoft Azure 资源。无论是在 macOS、Linux 还是 Windows 上安装它,都非常简单。

在 macOS 上安装

如果您已经跟随前两章的内容,您可能已经猜到我们将在 macOS 上使用 Homebrew 来安装 Azure CLI。

为此,运行以下命令:

$ brew install azure-cli 

安装完 Azure CLI 后,运行以下命令:

$ az --version 

这应该会返回类似以下截图的内容:

计算机程序的截图描述自动生成

图 17.2:在 macOS 上检查 Azure CLI 版本

一旦安装了 Azure CLI,您可以继续查看配置 Azure CLI部分。

在 Linux 上安装

Microsoft 提供了一个覆盖大多数常见 Linux 发行版的安装脚本。要运行此脚本,请使用以下命令:

$ curl -L https://aka.ms/InstallAzureCli | bash 

这将下载、安装并配置运行 Azure CLI 所需的一切,适用于您选择的 Linux 发行版。安装完成后,您需要重启会话。

您可以通过注销然后重新登录,或者在某些发行版上运行以下命令来做到这一点:

$ source ~/.profile 

一旦您重启了会话,运行以下命令:

$ az --version 

这将返回几乎与我们在安装 CLI 时在 macOS 上所看到的输出完全相同,唯一的区别是操作系统的信息。安装完成 Azure CLI 后,继续进行配置 Azure CLI部分。

在 Windows 上安装

您可以通过几种方式在 Windows 机器上安装 Azure CLI。第一种选择是从aka.ms/installazurecliwindows下载安装程序副本,然后通过双击安装程序来运行它。

下一个选项是使用以下 PowerShell 命令,它将从前面的 URL 下载安装程序并安装:

$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'; Remove-Item .\AzureCLI.msi 

第三个选项是使用Chocolatey包管理器,运行以下命令:

$ choco install azure-cli 

无论您选择哪种方式安装包,安装完成后,运行以下命令以查看版本号:

$ az --version 

如您所料,这也将显示与我们在 macOS 上运行命令时所看到的类似的输出。现在我们已经安装了 Azure CLI,可以开始配置它。

配置 Azure CLI

配置 Azure CLI 是一个直接的过程;您需要运行以下命令:

$ az login 

这将打开您的默认浏览器,要求您登录。一旦登录,Azure CLI 将配置为使用您登录的账户。

如果您遇到问题或正在进行仅命令行的 Azure CLI 安装(例如在远程 Linux 服务器上),则运行以下命令将提供一个 URL 和唯一的登录代码供您使用:

$ az login --use-device-code 

登录后,您的命令行会话应返回一些有关您 Azure 账户的信息。您可以使用以下命令再次查看这些信息:

$ az account show 

如果您由于任何原因无法在本地安装 Azure CLI,您也不会失去任何东西,因为 Azure 门户中有一个基于 Web 的终端,您可以使用它。接下来我们将介绍这个内容。

访问 Azure Cloud Shell

要访问 Azure Cloud Shell,请打开portal.azure.com/并使用您的凭据登录。一旦登录,点击页面顶部菜单栏中的 Cloud Shell 图标,它是位于中央搜索框旁边的第一个图标。

启动 Cloud Shell 时,您可以选择挂载存储。考虑到我们将使用 kubectl,希望我们的配置能够持久化,因此请选择挂载存储账户选项并选择您的订阅,如以下截图所示:

![计算机截图自动生成的描述图 17.3:为您的云终端会话选择存储选项点击应用按钮后,您将看到三个选项,如下截图所示。请选择我们将为您创建一个存储账户选项:计算机截图自动生成的描述

图 17.4:你希望如何创建存储帐户?

大约一分钟后,Cloud Shell 应该会打开,你将看到一个命令提示符:

计算机截图自动生成的描述

图 17.5:已登录并准备使用

现在你有了命令提示符,运行下面的命令,就像我们在本地 Azure CLI 安装中所做的一样,可以获取已安装的 Azure CLI 版本信息:

$ az --version 

由于 Azure 门户在后台为你处理了此操作,因此你无需运行az login命令,当你的Cloud Shell实例启动时,它已经为你完成了登录。

现在你已经可以访问配置好的 Azure CLI,我们可以以某种方式启动我们的 AKS 集群。

启动你的 Azure Kubernetes 服务集群

在所有先决条件都准备就绪之后,我们现在可以启动我们的 AKS 集群了。为此,我们只需运行两个命令。

第一个命令创建了一个 Azure 资源组:

$ az group create --name rg-myfirstakscluster-eus --location eastus -o table 

在上述命令中,我们在eastus区域创建了一个名为rg-myfirstakscluster-eus的资源组,并将输出格式设置为表格,而不是默认的 JSON 格式,这是 Azure CLI 的默认输出类型。

资源组是一个逻辑容器,用于将相关的 Azure 资源进行分组。在资源组内启动的服务可以继承角色基础访问控制、锁定以及资源启动的区域等设置。

一旦资源组创建完成,你应该会看到类似下方格式化为表格的确认信息:

计算机截图自动生成的描述

图 17.6:创建资源组

既然我们有了资源组作为容器来存储我们的资源,我们可以通过运行下面的命令来启动 AKS 集群。正如你所看到的,它引用了我们刚刚创建的资源组:

$ az aks create --resource-group rg-myfirstakscluster-eus --name aks-myfirstakscluster-eus --node-count 2 --enable-addons monitoring --generate-ssh-keys -o yaml 

启动和配置集群大约需要五分钟,因此在部署过程中,我将介绍我们传递给前面az aks create命令的选项:

  • --resource-group:正如你所猜测的,这是你希望启动 AKS 集群的资源组。集群将继承资源组的位置。在我们的示例中,我们使用的是前面创建的rg-myfirstakscluster-eus资源组,集群将在eastus区域创建。

  • --name:此参数传递你要启动的集群的名称。我们将它命名为aks-myfirstakscluster-eus

  • --node-count:在这里,你设置想要启动的节点数量。我们将启动两个节点。在写作时,节点的默认实例类型为 Standard_DS2_v2,这意味着每个节点将拥有 2 个 vCPU 和 7 GB 的 RAM。

  • --enable-addons:此标志用于提供要在集群启动时启用的附加组件列表—我们仅启用监控附加组件。

  • --generate-ssh-keys:这将为集群生成 SSH 公钥和私钥文件。

  • -o:正如我们在讨论之前的命令时提到的,这个选项决定了命令的输出格式。这次我们将输出 YAML 格式的结果,因为这种格式比 JSON 和表格选项更易读。

一旦你的集群启动成功,你应该看到类似以下的输出:

计算机截图描述自动生成

图 17.7:查看集群启动输出

如你所见,有很多信息。不过我们不必担心这些,因为我们将使用 Azure CLI 和门户与集群交互,而不是手动构造 API 请求来访问 Azure 资源管理器 API。

现在我们的集群已经启动并运行,部署示例工作负载之前,最后需要做的是配置本地 kubectl 客户端与集群进行交互。

要执行此操作,请运行以下命令:

$ az aks get-credentials --resource-group rg-myfirstakscluster-eus --name aks-myfirstakscluster-eus 

执行此命令后,你应该能看到类似以下的内容:

图 17.8:下载集群凭证并配置 kubectl

随着集群的启动和本地 kubectl 的配置,我们现在可以开始向集群发出命令。如果你已经阅读过前两章,你会知道该命令是:

$ kubectl get nodes 

这将返回集群中的节点,如下图所示:

计算机截图描述自动生成

图 17.9:检查节点是否已启动并运行

我们现在可以启动之前两个章节中使用的示例 Guestbook 工作负载来测试我们的集群。

部署工作负载并与集群交互

我们将使用在第十五章Google Kubernetes Engine 上的 Kubernetes 集群,以及第十六章使用 Amazon Elastic Kubernetes Service 在 Amazon Web Services 上启动 Kubernetes 集群中启动的相同工作负载,因此我在这里不会详细介绍,除了覆盖命令部分。

启动工作负载

我们从 Redis 主节点部署和服务开始:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-leader-service.yaml 

接下来是 Redis 从节点:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/redis-follower-service.yaml 

最后,我们可以使用以下命令启动前端部署和服务:

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/quickstarts/guestbook/frontend-service.yaml 

然后,过几分钟后,我们将能够运行以下命令来获取前端服务的信息:

$ kubectl get service frontend 

和之前一样,我们已经部署了示例工作负载。这将为我们提供一个公共 IP 地址,供我们访问 Guestbook 应用程序:

图 17.10:获取前端服务的信息

将 IP 地址输入浏览器,并确保使用http://<ipaddress>,因为我们没有配置 SSL 证书,浏览器会显示 Guestbook 应用程序:

计算机截图描述自动生成

图 17.11:查看 Guestbook 应用程序

工作负载运行后,我们可以进入 Azure 门户。

探索 Azure 门户

如果你还没有登录,访问portal.azure.com/并登录 Azure 门户。一旦登录,开始在页面顶部的“搜索资源、服务和文档”框中输入Kubernetes

在服务列表中,你会看到Kubernetes 服务。点击这个服务,你将看到你用户有权限访问的订阅下运行的 Kubernetes 服务列表。

计算机截图自动生成的描述

图 17.12:列出 Kubernetes 服务

点击aks-myfirstakscluster-eus将带你进入概览页面。这将是我们查看工作负载和集群信息的起点。

计算机截图自动生成的描述

图 17.13:集群概览页面

在 Kubernetes 资源菜单左侧,你将看到几个选项。让我们逐一处理这些选项。

命名空间(Kubernetes 资源)

在这里,你可以找到集群中所有活动的命名空间。由于我们在启动工作负载时没有定义自定义命名空间,我们的部署和服务将列在default命名空间下。

除了default命名空间外,还有作为集群一部分部署的命名空间:kube-node-leasekube-publickube-system。我建议不要修改这些。

点击默认命名空间后,你将看到概览页面。在这里,你可以编辑定义命名空间的 YAML,查看事件日志,并配置可能已部署的服务网格;在我们的测试中,没有服务网格。

工作负载(Kubernetes 资源)

正如你可能已经猜到的,你可以在这里查看你的工作负载信息。在下面的截图中,我只筛选出显示default命名空间中的工作负载:

计算机截图自动生成的描述

图 17.14:查看工作负载

点击其中一个部署将带你进入更详细的部署视图。例如,选择frontend部署显示如下内容:

计算机截图自动生成的描述

图 17.15:深入查看部署

从左侧菜单可以看出,选项中有一些新增项:除了YAML事件,现在我们还可以查看Insights。我们将在本节末尾更详细地讲解 Insights。

下一个选项是实时日志。在这里,你可以选择一个 pod 并实时流式传输日志:

计算机截图自动生成的描述

图 17.16:实时查看 pod 日志

返回到 Workloads 屏幕并选择 Pods 标签,将显示构成您工作负载的 Pod 列表。列出了 IP 地址以及 Pod 所在的节点。这对于快速了解您正在运行的 Pod 很有帮助。

计算机截图描述自动生成

图 17.17:列出所有正在运行的 Pod

点击其中一个 Pod 将为您提供概览,并显示 Pod 的 YAML 和任何事件。

工作负载屏幕上的下一个标签是 Replica sets,它提供了一种方便的方式来一目了然地查看作为工作负载一部分部署的副本集。同样,点击列出的副本集会提供我们之前在其他标签中看到的 概览YAML事件 选项。

计算机截图描述自动生成

图 17.18:列出所有副本集

Workloads 中的下一个标签是 Stateful sets;我们在命名空间中没有任何有状态集,Microsoft 在其他命名空间中也没有,因此这里没有什么可以查看的。不过,如果有,选择它后,您将看到与在 Workloads 部分其他标签中看到的信息相同。

接下来,我们有 Daemon sets 标签。再次强调,我们的工作区没有守护进程集,但是有一些是由 Microsoft 作为集群的一部分启动的,您可以进行查看。

最后,我们有最后两个标签,JobsCron jobs;在这里,您将找到您在集群中部署的任何作业和定时任务的详细信息。

服务和入口(Kubernetes 资源)

在这里,您可以找到在集群中部署的所有服务的列表。从以下截图中可以看到,您可以查看为服务使用的集群 IP 以及您配置的任何外部 IP:

计算机截图描述自动生成

图 17.19:查看服务

点击列出的其中一个服务将提供熟悉的视图,并允许您深入配置服务。

存储(Kubernetes 资源)

如果我们在集群中配置了任何持久存储,您可以在这里查看详细信息并进行管理;在我们的示例工作负载中,我们没有,因此几乎没有内容可查看。

配置(Kubernetes 资源)

在这里,您可以查看和编辑您在集群中配置的任何 ConfigMap 或 Secret。由于我们在工作负载中没有配置这些,所列出的项目是集群本身的,因此不建议对现有项目进行更改。

自定义资源(Kubernetes 资源)

在这里,您可以管理附加到集群的任何自定义资源。

事件(Kubernetes 资源)

这里是集群的所有实时事件;这些事件有助于您监控和排查集群和应用工作负载中的任何健康问题。

运行命令(Kubernetes 资源)

这是一个有用的补充;在这里,你可以直接从 Azure 门户运行任何 kubectl 命令,无需启动云终端或配置本地的 kubectl

计算机截图

描述自动生成](https://github.com/OpenDocCN/freelearn-devops-pt6-zh/raw/master/docs/k8s-bbl-2e/img/B22019_17_20.png)

图 17.20:从门户运行命令以对集群进行操作

继续进入菜单的 设置 部分,我们有以下内容。

节点池(设置)

在这里,你可以找到有关节点池的详细信息,并可以升级节点池中运行的 Kubernetes 版本。此选项只有在升级控制平面上运行的 Kubernetes 版本时才可用。

你还可以扩展节点池,并有选项添加一个新的节点池。在以下截图中,我们可以看到扩展选项的样子:

计算机截图

描述自动生成](https://github.com/OpenDocCN/freelearn-devops-pt6-zh/raw/master/docs/k8s-bbl-2e/img/B22019_17_21.png)

图 17.21:查看扩缩选项

你还可以查看每个节点的信息,如下图所示:

计算机截图

描述自动生成](https://github.com/OpenDocCN/freelearn-devops-pt6-zh/raw/master/docs/k8s-bbl-2e/img/B22019_17_22.png)

图 17.22:查看节点

集群配置(设置)

在上一点中,我提到过,只有在升级控制平面时,你才能升级运行在节点池中的 Kubernetes 版本,而这个选项就是用来执行此操作的。Microsoft 管理控制平面,并将其与节点池分离。

Kubernetes 控制平面提供向后兼容性,最多支持三个版本的回退,因此你通常只能在当前运行的版本的三个版本内进行升级。

应用扩缩(设置)

在这里,你可以启用 Kubernetes 事件驱动的自动扩缩器 (KEDA),它会根据来自外部源的事件动态调整工作负载。在撰写本文时,使用 Azure 门户配置该服务时,支持的扩缩事件仍然存在一些限制。它支持以下来源的扩缩:Azure Service Bus、Cron、内存和 CPU。

网络(设置)

在这一部分,你可以查看并管理集群的网络设置。

扩展 + 应用(设置)

作为 Azure 市场的一部分,Microsoft 提供了可以部署到你的 AKS 集群中的第一方和第三方应用程序。在这一部分,你可以管理这些部署。

备份(设置)

Azure 本地备份服务现在支持备份你的 Kubernetes 工作负载和应用数据;所有这些都可以从这里进行管理。

其他选项(设置)

设置 菜单中的其他选项允许你配置集群的各个部分,并将集群连接到其他 Azure 本地服务。如果你需要更多信息,可以在本章末尾的 进一步阅读 部分找到 AKS 文档的链接。

见解(监控)

我们将要查看的 Azure 门户的最后一部分是位于集群视图中的 监控 菜单下的 洞察 选项。正如您记得的那样,当我们部署集群时,我们使用 --enable-addons 监控标志启用了监控附加组件。

这使得 Microsoft 的本地监控能够将数据从资源传输到 Azure 日志分析服务。一旦数据传输到该服务,Microsoft 会将这些信息以洞察的形式呈现给您。大多数 Azure 服务都有 洞察 选项,Azure Monitor 可以利用这里的数据创建并生成警报。

计算机截图描述自动生成

图 17.23:洞察页面

洞察页面上有几个标签,我们来看看:

  • 集群:这是前述截图中所展示的,能快速查看整个集群的 CPU 和内存利用率,还显示节点和 Pod 的数量。

  • 报告:在这里,您可以找到关于节点监控(性能)、资源监控(可用性)、计费和网络的预写报告。随着服务的成熟,将会有更多报告添加。

  • 节点:在这里,您可以获得节点的概览。

  • 控制器:这里是您可以找到已在集群中启动的控制器的详细信息,例如副本集和守护进程集。

  • 容器:在这里,您可以找到有关您部署的 Pod 中所有运行的容器的详细信息。

现在,您可能会觉得前面的部分有很多重复——确实有一点;然而,如果您需要快速查看集群中的状况,您现在有一种方法可以在无需翻阅大量页面的情况下获取这些信息。

我建议您四处浏览,尽可能点击多个选项,探索集群与 Azure 门户的集成程度。完成后,我们就可以开始删除集群了。

删除您的 Azure Kubernetes 服务集群

我们要查看的最后一部分是如何删除集群。返回到 Azure CLI,我们只需运行以下命令来删除集群:

$ az aks delete --resource-group rg-myfirstakscluster-eus --name aks-myfirstakscluster-eus 

系统会询问您是否确认,回答“是”将继续删除集群。

这个过程大约需要五分钟。前述命令仅删除集群,而不删除资源组。要删除资源组,请运行以下命令:

$ az group delete --name rg-myfirstakscluster-eus 

系统会再次询问您是否要删除该组,您只需回答“是”即可。

那么,我们的集群运行的费用是多少?

与前两章我们讨论的其他两个云服务不同,集群管理对于非生产工作负载是免费的,您只需为计算资源付费。

在我们的例子中,两个位于美国东部地区的 Standard_DS2_v2 实例每月大约需要 $213,或者如果选择标准集群管理,则每月需 $286。

其他选项,例如新一代实例,可能会以更低的价格为我们提供类似大小的集群。例如,我们可以使用以下命令启动一个不同的集群:

$ az group create --name rg-myfirstakscluster-eus --location eastus -o table
$ az aks create --resource-group rg-myfirstakscluster-eus --name aks-myfirstakscluster-eus --node-count 2 --enable-addons monitoring --generate-ssh-keys --node-vm-size standard_ds3_v2 -o yaml 

如果选择非生产选项,这将为我们提供一个具有四个 vCPU 和 16GB 内存的双节点集群,费用大约为每月$140。

比较三大公共云服务

在本章结束之前,我们快速比较一下这三大公共云服务:

特性 谷歌 Kubernetes 引擎(GKE) 亚马逊弹性 Kubernetes 服务(EKS) 微软 AKS
Kubernetes 版本支持 最新版本,频繁更新 在版本支持上略有滞后 最新版本,频繁更新
自动更新 控制平面和节点自动更新 控制平面按需更新,节点手动更新 控制平面和节点按需更新
易用性 高 – 直观的界面 中等 – 复杂的设置 高 – 直观的界面
与云服务的集成 强大的与 GCP 服务的集成 强大的与 AWS 服务的集成 强大的与 Azure 服务的集成
可扩展性 良好,支持自动扩展 良好,支持自动扩展 良好,支持自动扩展
安全特性 强大,集成 GCP 安全工具 强大,集成 AWS 安全工具 强大,集成 Azure 安全工具
定价 免费控制平面,按节点收费 控制平面和节点费用为每小时$0.10 免费控制平面,按节点收费
多区域集群支持
私有集群支持
无服务器计算选项 是,使用 Cloud Run for Anthos 是,使用 Fargate for EKS 是,使用 AKS 虚拟节点
定价 免费控制平面,按节点收费 控制平面和节点费用按小时计费 免费控制平面,按节点收费

表 17.1:三大公共云服务的比较

有几个关键点需要注意:

  • GKE 通常在 Kubernetes 版本支持和自动更新方面处于领先地位。

  • AKS 被大多数人认为是最用户友好的,尤其适合已经在使用 Azure 服务的用户。

  • EKS 对控制平面收费,而 GKE 和 AKS 仅对工作节点收费。

  • 三个服务都与各自的云生态系统有着强大的集成。

  • GKE 因其先进的功能和性能而常受到赞誉,利用了谷歌作为 Kubernetes 的原创者的专业知识。

  • 每个服务都有独特的优势:GKE 在性能和功能上,EKS 在 AWS 生态系统集成上,AKS 在易用性和 Azure 集成上。

  • 最佳选择通常取决于您现有的云基础设施、需求以及对云服务商生态系统的熟悉程度。

如你所见,选择使用三种服务中的哪一种,与您已经在其中运行工作负载的云服务紧密相关;正如我们在过去三章中学到的那样,一旦集群启动,体验几乎是相同的。

总结

在本章中,我们探讨了 Microsoft Azure 的发展历程、微软容器服务的一些历史背景,以及他们最终如何选择 AKS。

然后,我们注册了一个 Azure 账户,并在启动我们自己的 AKS 集群之前安装并配置了 Azure CLI。集群启动后,我们部署了与我们在 GKE 和 Amazon EKS 集群中部署的相同工作负载。

一旦工作负载被部署,我们转到 Azure 门户,查看有关如何获取工作负载和集群的洞察以及一些集群管理选项。

最后,我们删除了已启动的资源,并讨论了集群运行的成本。

在过去三章中我们讨论的三大公共云服务中,我个人会将 Microsoft Azure AKS 排在第一位;它拥有最全面且功能丰富的产品,同时易于使用。我会将 Google 提供的服务,第十五章,《在 Google Kubernetes Engine 上部署 Kubernetes 集群*》排在第二;它很好,但它的定价需要调整,以与 Microsoft 的产品相竞争。

这也留下了亚马逊的服务,见于第十六章,《在 Amazon Web Services 上使用 Amazon Elastic Kubernetes Service 启动 Kubernetes 集群*》。AWS 是我个人推荐最少的服务。它的感觉远没有 Microsoft 和 Google 提供的服务那样成熟,它本应给人一种启动服务来补充云提供商其他服务的感觉,但实际上,它就像是在 AWS 中运行一个 Kubernetes 集群。

撇开个人意见不谈,从在三种完全不同的公共云服务中启动 Kubernetes 集群的关键收获是,一旦你启动了集群并且配置了 kubectl 客户端来与其交互,体验几乎是相同的,且你的工作负载并不关心它被部署在哪个平台。你也不必考虑三家服务提供商之间的差异——仅仅几年之前,这似乎是不可想象的情况,真正的云无关工作负载只是一种空想。

在下一章中,我们将探索 Kubernetes 的安全性方面,包括认证与授权、准入控制器、网络策略以及其他重要主题。

进一步阅读

以下是本章涵盖的一些主题和工具的更多信息链接:

)

)

)

)

在 Discord 上加入我们的社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

第十八章:Kubernetes 中的安全性

认证和授权是现代软件系统的基石,分别提供必要的身份管理和访问管理。尽管这两个过程有所不同,但许多人仍然混淆这两个术语。认证是指验证用户身份的过程,通常通过用户名和密码等机制来实现;而授权则是指一个已认证的用户可以访问或执行系统中的哪些操作。认证总是优先于授权,只有通过认证的用户,系统才会根据授权进行交互。Kubernetes 在此基础上进一步扩展了另一个模型,称为基于角色的访问控制RBAC),该模型允许管理员定义具有特定权限的角色,并将这些角色分配给用户,从而有效地实施最小权限原则,并实现细粒度的访问控制。

除了身份和访问管理,Kubernetes 还拥有许多其他的安全机制,进一步加强其他组件的安全性。作为最成熟和最广泛采用的容器编排平台,Kubernetes 的设计非常重视集群、节点、容器、网络和应用程序中各种组件的安全性,通过在多个层次上缓解风险来确保安全。

接下来,本章将介绍一些基础的 Kubernetes 安全概念,包括系统灵活认证的不同方式——如 X509 客户端证书或来自 OpenID Connect 的令牌。在一些特殊情况下,例如与 LDAP 的集成,Kubernetes 提供了额外的选项。例如,使用认证代理或 Webhooks 的可能性也是推荐的。接着,我们将回顾平台中的RBAC模型,它赋予管理员对集群中资源访问的控制权,并允许他们管理用户、组和 ServiceAccounts。

我们还将介绍 Kubernetes 中的一个高级功能:Admission Controllers。Admission Controller 在资源进入集群时执行安全策略,以验证和控制资源。Admission Controllers 通过在资源创建和修改时强制执行策略,为资源请求提供了额外的防护层。

Pods 和容器本身需要得到保护,因为它们是工作负载或应用程序的运行时,可能会与敏感信息交互。Kubernetes 提供了一套 securityContext 选项,使管理员能够为容器声明特定的安全设置;这包括强制容器以非 root 用户身份运行。同样重要的是网络安全,我们将讨论如何通过 NetworkPolicies 提供一种机制,通过控制流量流动来在集群内隔离并保护 Pod 通信,从而实现细粒度的安全管理。

接下来我们将讨论容器运行时的安全性。我们将研究 gVisor 和 Kata Containers 作为运行时的选项,它们分别通过用户空间内核拦截系统调用或为每个容器提供轻量级虚拟机环境,在提供容器速度的同时增强虚拟机的安全性。

最后,也是最重要的,私有注册表凭证是确保集群内部容器镜像安全的关键。我们将逐步讲解 Kubernetes 如何安全地处理这些凭证——确保只有经过授权的组件可以访问它们。到本章结束时,您将更深入地理解 Kubernetes 中的这些高级安全概念和工具。您将确切了解如何增强集群的安全性,降低风险,并为可能的漏洞提供最好的防御。通过这些措施,您将能够在每一层保护您的 Kubernetes 部署,从身份管理到运行时隔离,并增强容器化应用程序的稳健性。

本章将涵盖以下主题:

  • 认证与授权 —— 用户访问控制

  • 入场控制 —— 安全策略和检查

  • 保护 Pods 和容器

  • 管理机密和注册表凭证

技术要求

本章所需的内容:

  • 一个待部署的 Kubernetes 集群。我们推荐使用多节点的云端 Kubernetes 集群。

  • 已在您的本地机器上安装并配置 Kubernetes CLI(kubectl),用于管理您的 Kubernetes 集群。

本书的第三章《安装您的第一个 Kubernetes 集群》已涵盖了基础的 Kubernetes 集群部署(本地和云端)以及 kubectl 的安装。

本书的前几章(第十五章第十六章第十七章)已经为您提供了如何在不同的云平台上部署功能完备的 Kubernetes 集群的概述,并安装了管理它们所需的 CLI 工具。

您可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter18

认证与授权 —— 用户访问控制

认证为访问控制提供了依据,确保只有经过认证和授权的用户才能使用 Kubernetes 资源。认证验证用户的身份,而授权则决定认证用户在集群内可以执行的操作。Kubernetes 在认证方面提供了灵活性,支持 X509 证书、OpenID Connect、基于令牌等多种方式。结合验证过程,RBAC 确实提供了细粒度的控制,帮助管理员高效地管理各种权限——这是接下来章节将详细讨论的内容。

接下来,我们将在下一部分开始讲解认证。

认证和用户管理

Kubernetes API 服务器提供用于管理 Kubernetes 集群的 RESTful 端点,并充当集群共享状态的前端。所有与集群的交互,从用户到内部组件,都会通过 Kubernetes API 服务器进行,它充当集群共享状态的前端。

让我们在接下来的部分中看看 Kubernetes 中的认证机制是如何工作的。

Kubernetes 中的认证工作流

就像一个高安全性设施一样,你的 Kubernetes 集群需要强大的安全措施来保护其资源。这涉及一种分层的方法,其中多个关键组件共同工作,如下图所示:

图 18.1:请求 Kubernetes API 会经过多个阶段(来源:https://kubernetes.io/docs/concepts/security/controlling-access/)

  • 认证:它作为第一道防线,验证任何试图访问 Kubernetes API 服务器的人的身份。可以把它想象成门口的保安检查身份。用户可能使用密码、令牌或特殊证书来证明他们已获得授权。

  • 授权:一旦某人的身份被确认,授权决定了他们在集群内实际上能做什么。可以把它理解为授予特定的访问权限。用户可能有权限查看资源,但不能修改它们,或者他们可能被授权在特定区域内创建新资源。

  • 准入控制:这一阶段增加了额外的审查层。可以将其理解为入口处的安全扫描仪。准入控制模块可以检查传入的请求,确保它们符合预定义的安全政策。它们甚至可以修改请求,强制执行特定的规则,或者如果请求构成威胁,则完全拒绝。

  • 审计:就像记录谁进出一个安全设施一样,Kubernetes 的审计功能会记录集群内的所有活动。这包括用户、应用程序,甚至控制平面本身的操作。这些日志对监控可疑活动和维持安全环境至关重要。

这些安全措施相互配合,形成了一个分层防御系统,确保只有授权用户能够访问你的 Kubernetes 集群,并且他们的行为符合已建立的安全政策。

我们将在下一部分了解更多关于认证机制的细节。

Kubernetes API 的认证

Kubernetes API 认证确保只有授权的用户或服务可以与集群中的资源进行通信。每个传入请求都必须经过一系列身份验证模块的设置,这些模块已被配置。

对 API 的请求总是以下列之一进行:

  • 与外部、普通用户或在 Kubernetes 集群中定义的 ServiceAccount 相关联。

  • 如果集群已配置为允许匿名请求,则视为匿名请求。

这在身份验证过程中决定——整个 HTTP 请求作为输入传入该过程,但通常只分析请求头或客户端证书。身份验证由依赖于集群配置的身份验证模块执行。你的集群可能启用了多个身份验证模块,每个模块会按顺序执行,直到有一个成功。如果请求身份验证失败,API 服务器将响应401 (unauthorized) HTTP 状态码,或者如果启用了匿名请求,则将其视为匿名请求。

匿名请求本质上映射到一个特殊的用户名system:anonymous和一个叫做system:unauthenticated的组。这意味着你可以像管理其他用户或服务账户一样,管理这些请求的资源授权。

由于集群内外的所有操作必须通过 Kubernetes API 服务器,这意味着所有操作都必须经过身份验证过程。这包括内部集群组件和 Pod 的操作,它们会查询 API 服务器。作为集群的外部用户,你通过kubectl命令或直接向 Kubernetes API 服务器发出的任何请求也会经过身份验证过程:

  • 普通用户:这些用户是外部管理的,独立于 Kubernetes 集群。目前,Kubernetes 没有提供任何对象来表示这些用户。外部管理用户的方式可以像通过token-auth-file参数在静态 Pod 定义文件/etc/kubernetes/manifests/kube-apiserver.yaml中将静态的用户密码文件传递给 API 服务器(这通常发生在控制平面节点,即主节点启动时),尽管这种方法简单(但推荐)。在生产环境中,应利用现有的身份提供者IdPs),如GoogleGitHubAzure Active DirectoryAAD)或AWS IAM来管理用户。使用OpenID ConnectOIDC)令牌将 Kubernetes 集群与这些身份提供者集成,提供无缝的身份验证体验。请记住,Kubernetes 中的常规用户账户是全局性的,并且没有命名空间限制。

  • ServiceAccount: 这些由 Kubernetes 集群管理,并建模为 ServiceAccount 对象。您可以像在 Kubernetes 中管理任何其他资源一样,例如使用kubectl和 YAML 清单文件来创建和管理服务帐户。此类帐户适用于集群组件中或运行在 Pod 中的进程。ServiceAccounts 的凭据将作为 Secrets(手动创建或通过 TokenRequest API)创建,并挂载到 Pod 中,以便容器进程可以使用它们与 Kubernetes API 服务器进行通信。当进程使用ServiceAccount令牌进行身份验证时,它被视为名为system:serviceaccount:<namespace>:<serviceAccountName>的用户。请注意,ServiceAccounts 是命名空间的。

正如您所看到的,Kubernetes 中的用户管理是一种不同方法的混合,应该适合不同组织的所有需求。这里的关键是,在认证过程之后,请求将被拒绝(可选地视为匿名)或被视为来自特定用户。外部用户管理系统可以提供username属性,例如普通用户的情况,或者对于 ServiceAccounts,它将是system:serviceaccount:<namespace>:<serviceAccountName>。此外,请求将具有更多与之相关的属性,如用户 IDUID)、额外字段。这些信息用于基于 RBAC 的授权过程,我们将在下一节中进行解释。

现在,让我们看看您可以在 Kubernetes 中使用的认证方法。

Kubernetes 中的认证方法

通常情况下,各种认证方法帮助安全地控制对 Kubernetes API 服务器的访问。为了验证用户和服务,可以启用多种认证策略。每种策略都适用于不同的用例和安全级别。这些策略包括验证人类用户和与集群交互的应用程序身份的令牌和证书。Kubernetes API 服务器的一个优点是它支持多种认证机制,因此可以使用前述方法的组合来配置集群。在接下来的部分中,我们将介绍一些常见的认证方法,如静态令牌文件、ServiceAccount 令牌、X.509 客户端证书和 OpenID Connect 令牌。

静态令牌文件

这种方法是 Kubernetes 提供给普通用户管理的最基本方法之一。这种方法在某种程度上类似于 Unix/Linux 系统中的/etc/shadow/etc/passwd文件。但请注意,不建议在生产集群中使用,因为它被认为是不安全的。

在此方法中,您需要定义一个.csv文件,其中每行具有以下格式:

token,user,uid,"group1,group2,group3" 

然后,在启动 Kubernetes API 服务器进程时,你需要传递该文件,使用静态 Pod 定义文件 /etc/kubernetes/manifests/kube-apiserver.yaml 中的 token-auth-file 参数,文件位于你的控制平面节点(即主节点)中:

# /etc/kubernetes/manifests/kube-apiserver.yaml
...<removed for brevity>...
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=192.168.59.154
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    **-****--token-auth-file=/etc/kubernetes/user-tokens.csv**
    - --client-ca-file=/var/lib/minikube/certs/ca.crt
...<removed for brevity>... 

要对 API 服务器进行身份验证,你需要为请求使用标准的 HTTP 承载身份验证方案。这意味着你的请求将需要使用以下格式的附加头:

Authorization: Bearer <token> 

基于此请求信息,Kubernetes API 服务器将根据静态令牌文件匹配令牌,并根据匹配的记录分配用户属性。

使用 kubectl 时,你必须修改你的 kubeconfig。你可以使用 kubectl 命令来做到这一点:

$ kubectl config set-credentials <contextUser> --token=<token> 

之后,你需要创建并使用此用户的上下文来进行请求,使用 kubectl config use-context 命令。

在 Kubernetes 1.19 版本之前,曾有一种类似的身份验证方法,允许我们使用 HTTP 基本身份验证方案 和通过 basic-auth-file 参数传递给 API 服务器的文件。由于安全原因,该方法现在已不再支持。

以下图示化了这种身份验证方法背后的原理:

图 18.1 – Kubernetes 中的静态令牌文件身份验证

图 18.2: Kubernetes 中的静态令牌文件身份验证

现在我们可以总结使用静态令牌文件方法进行身份验证的优缺点。

静态令牌文件方法的优点如下:

  • 配置起来很简单。

  • 它容易理解。

静态令牌文件方法的缺点如下:

  • 这是不安全的;暴露一个令牌文件会危及所有集群用户。

  • 它要求我们手动管理用户。

  • 添加新用户或删除现有用户需要我们重新启动 Kubernetes API 服务器。

  • 轮换任何令牌需要我们重新启动 Kubernetes API 服务器。

  • 当你拥有多个控制平面节点的高可用性控制平面时,复制令牌文件内容到每个控制平面节点需要额外的工作。

总之,这种方法适合开发环境和学习 Kubernetes 身份验证背后的原理,但不推荐在生产环境中使用。接下来,我们将介绍如何使用 ServiceAccount 令牌进行用户身份验证。

ServiceAccount 令牌

正如我们在本节开头提到的,ServiceAccounts 旨在为在 Pod 容器中运行的过程或集群组件提供集群内身份。然而,它们也可以用于验证外部请求。

ServiceAccounts 是 Kubernetes 对象,可以像集群中的其他资源一样进行管理;也就是说,可以使用 kubectl 或通过原始 HTTP 请求与 API 服务器进行交互。ServiceAccount 的令牌是 JSON Web 令牌 (JWTs),会根据需求或使用 kubectl create token 命令生成。

每个 Kubernetes 命名空间都有一个预创建的名为 default 的 ServiceAccount。没有指定 ServiceAccount 的 Pod 会自动继承该默认账户以进行集群内的授权。你可以使用 kubectl get pods/<podname> -o yaml 来验证 Pod 的 ServiceAccount,并检查 spec.serviceAccountName 字段。

通常,在定义 Pod 时,你可以指定用于容器中运行的进程的 ServiceAccount。你可以在 Pod 规范中使用 .spec.serviceAccountName 来实现这一点。JWT 令牌将被注入到容器中;然后,容器内的进程可以在 HTTP 承载身份验证方案中使用它来认证 Kubernetes API 服务器。只有当它与 API 服务器有任何交互时,这才是必要的,例如,如果它需要发现集群中的其他 Pod。我们已在下图中总结了这种身份验证方法:

图 18.2 – Kubernetes 中的 ServiceAccount 身份验证

图 18.3:Kubernetes 中的 ServiceAccount 身份验证

这也说明了为什么 ServiceAccount 令牌可以用于外部请求——API 服务器并不关心请求的来源;它关心的是随请求头一起传递的承载令牌。再次强调,你可以在 kubectl 中或者通过直接的 HTTP 请求发送到 API 服务器时使用此令牌。请注意,这通常不是推荐的使用 ServiceAccount 的方式,但在某些场景下它是可行的,尤其是当你无法为普通用户使用外部身份验证提供者时。

在 1.22 版本之前,Kubernetes 使用 Secrets 自动为 ServiceAccount 生成 API 凭证。这些 Secrets 包含 Pod 可以挂载以访问的令牌。这种方法有一些局限性:

  • 静态令牌:Secrets 存储的令牌是明文的,如果泄露会带来安全风险。

  • 有限控制:令牌的生命周期和权限不易管理。

从 1.22 版本开始,Kubernetes 转向了一种更安全的方法。Pod 现在通过 TokenRequest API 直接获取令牌。这些令牌如下所示:

  • 短生命周期:令牌的生命周期有限,减少了潜在泄露的影响。

  • 自动挂载到 Pods 中:令牌会自动挂载为卷,消除了预存 Secret 的需求。

虽然自动挂载是首选方法,但你仍然可以手动为服务账户令牌创建 Secrets。对于需要更长生命周期的令牌,这可能是有用的,但在大多数场景中,优先使用自动令牌挂载来增强安全性。

正如我们所学到的,Kubernetes 自动在 Pod 中挂载 Service Account API 凭据以简化访问。要禁用此行为并以不同方式管理令牌,请在 ServiceAccount 清单或 Pod 规范中设置 automountServiceAccountToken: false。此设置适用于所有引用该 ServiceAccount 的 Pod,除非由特定 Pod 配置覆盖。如果两者都定义了,则 Pod 的设置优先。有关详细信息,请参阅文档(kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting)。

现在我们将演示如何创建和管理 ServiceAccounts,以及在使用 kubectl 时如何使用 JWT 令牌进行身份验证。这还将让我们一窥即将在下一节详细介绍的 RBAC。请按照以下步骤操作:

  1. 创建一个新的 Namespace 和一个 ServiceAccount 的 YAML 清单如下。我们将为此帐户配置 RBAC,使其仅能读取该命名空间中的 Pods:

    # 01_serviceaccount/example-sa-ns.yaml
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: example-ns
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: example-sa
      namespace: example-ns 
    

请注意,您还可以使用 命令式 命令 kubectl create serviceaccount example-sa 创建资源。

  1. example-ns 命名空间中为名为 pod-readerRole 对象创建一个 YAML 清单。此角色将允许您获取、监视和列出此命名空间中的 Pods。01_serviceaccount/pod-reader-role.yaml YAML 清单文件的内容如下:

    # 01_serviceaccount/pod-reader-role.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      namespace: example-ns
      name: pod-reader
    rules:
      - apiGroups: [""]
        resources: ["pods"]
        verbs: ["get", "watch", "list"] 
    
  2. 创建一个名为 reads-podsRoleBinding 的 YAML 清单。这是 关联 我们创建的角色与我们的 example-sa ServiceAccount 的操作 - 此帐户现在将具有对 Pods 的只读访问权限,没有其他权限。01_serviceaccount/read-pods-rolebinding.yaml YAML 清单文件的内容如下:

    # 01_serviceaccount/read-pods-rolebinding.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: read-pods
      namespace: example-ns
    subjects:
      - kind: ServiceAccount
        name: example-sa
        namespace: example-ns
    roleRef:
      kind: Role
      name: pod-reader
      apiGroup: rbac.authorization.k8s.io 
    
  3. 现在,我们可以使用 kubectl apply 命令一次性将所有清单文件应用到集群中:

    $ kubectl apply -f 01_serviceaccount/
    namespace/example-ns created
    serviceaccount/example-sa created
    role.rbac.authorization.k8s.io/pod-reader created
    rolebinding.rbac.authorization.k8s.io/read-pods created 
    
  4. 现在,我们将如下创建一个 ServiceAccount 的 Token:

    $ kubectl create token example-sa -n example-ns 
    

从命令输出中收集 JWT 令牌,您可以使用该令牌作为该 ServiceAccount 的身份验证。如果您感兴趣,可以使用 jwt.io/ 检查 JWT 的内容,如下图所示:

图 18.4:检查 ServiceAccount 的 JWT

如您所见,JWT 映射到 example-ns 命名空间中的 example-sa ServiceAccount。此外,您可以确定在 Kubernetes 中将映射到的实际用户名(在负载中标记为 subject)是 system:serviceaccount:example-ns:example-sa,正如我们之前解释的那样。

  1. 使用此令牌,我们可以设置 kubeconfig 进行测试。首先,您需要使用以下命令在您的 kubeconfig 中创建一个用户:

    $ kubectl config set-credentials example-sa --token=<your-token>
    User "example-sa" set. 
    

其中 example-sa 是您创建的新 ServiceAccount,并将 <your-token> 替换为您之前收集的令牌字符串。

  1. 创建一个使用此用户的新上下文,kubeconfig中也需要知道您当前连接的集群名称——您可以使用kubectl config view命令查看。使用kubectl config set-context命令创建新上下文:

    $ kubectl config set-context <new-context-name> --user=<new-user-created --cluster=<clusterName> 
    

例如,使用以下命令创建一个名为example-sa-context的新上下文,目标集群为 minikube,用户为example-sa

$ kubectl config set-context example-sa-context --user=example-sa --cluster=minikube
Context "example-sa-context" created. 
  1. 在切换到新创建的上下文之前,让我们在example-ns命名空间中创建一个简单的 nginx Pod。将示例 YAML 文件Chapter18/references/sa-demo-nginx-pod.yaml复制到Chapter18/01_serviceaccount/nginx-pod.yaml并应用配置:

    $ cp references/sa-demo-nginx-pod.yaml 01_serviceaccount/nginx-pod.yaml
    $ kubectl apply -f 01_serviceaccount/nginx-pod.yaml
    pod/nginx-pod created
    $  kubectl get po -n example-ns
    NAME        READY   STATUS    RESTARTS   AGE
    nginx-pod   1/1     Running   0          12m 
    
  2. 此外,在切换到新上下文之前,您可能想要使用kubectl config current-context命令检查当前使用的上下文名称。这将使您更容易回到旧的集群管理员上下文:

    $ kubectl config current-context
    minikube 
    
  3. 现在,使用以下命令切换到新上下文:

    $ kubectl config use-context example-sa-context
    Switched to context "example-sa-context". 
    
  4. 您还可以通过以下方式验证您当前使用的凭证身份:

    $ kubectl auth whoami
    ATTRIBUTE                                           VALUE
    Username                                            system:serviceaccount:example-ns:example-sa
    UID                                                 ebc5554b-306f-48fe-b9d7-3e5777fabf06
    Groups                                              [system:serviceaccounts system:serviceaccounts:example-ns system:authenticated]
    Extra: authentication.kubernetes.io/credential-id   [JTI=45dc861c-1024-4857-a694-00a5d2eeba5f] 
    
  5. 我们现在准备验证我们的身份验证是否有效,并且 RBAC 角色是否允许对example-ns命名空间中的 Pods 进行只读访问。首先,尝试获取 Pods:

    $ kubectl get po -n example-ns
    NAME        READY   STATUS    RESTARTS   AGE
    nginx-pod   1/1     Running   0          18m 
    
  6. 这按预期工作!现在,尝试从kube-system命名空间获取 Pods:

    $ kubectl get pods -n kube-system
    Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:example-ns:example-sa" cannot list resource "pods" in API group "" in the namespace "kube-system" 
    
  7. 我们已经正确认证,但由于 RBAC 授权限制,操作被禁止,这正是我们预期的。最后,让我们尝试获取服务对象:

    $ kubectl get svc -n example-ns
    Error from server (Forbidden): services is forbidden: User "system:serviceaccount:example-ns:example-sa" cannot list resource "services" in API group "" in the namespace "example-ns" 
    

这也是预期的,因为 RBAC 未配置 ServiceAccount 以查看或列出example-ns命名空间中的服务资源。

如您所见,我们已经成功地使用 ServiceAccount 令牌进行身份验证,并验证了我们的权限正常工作。您现在可以使用kubectl config use-context <context-name>命令切换回您的旧kubectl上下文。

配置kubectl上下文与 bearer 令牌的上述过程也可以用于静态令牌文件身份验证方法。

让我们总结一下使用 ServiceAccount 令牌进行身份验证的优缺点。

使用 ServiceAccount 令牌的优点如下:

  • 配置和使用简单,类似于静态令牌文件。

  • 完全由 Kubernetes 集群管理,因此无需外部身份验证提供者。

  • ServiceAccounts 是命名空间级的。

使用 ServiceAccount 令牌的缺点如下:

  • ServiceAccounts 旨在为在 Pod 容器中运行的进程提供身份,并允许它们使用 Kubernetes RBAC。用户使用 ServiceAccount 令牌并不是最佳实践

一般来说,使用 ServiceAccount 令牌进行外部认证仅适用于开发和测试场景,当你无法与外部认证提供商集成时。然而,对于生产集群来说,这不是最佳选择,主要是由于安全问题。现在,让我们来看看如何使用 X.509 客户端证书进行 Kubernetes API 认证。

X.509 客户端证书

使用 X.509 客户端证书是认证过程中一种行业标准。然而,有一个重要的注意事项——你需要有良好的证书签名、吊销和轮换管理手段。否则,你可能会遇到与使用 ServiceAccount 令牌类似的安全问题。你可以在 www.ssl.com/faqs/what-is-an-x-509-certificate/ 上了解更多关于 X.509 证书及其相关流程的信息。

该方法在 Kubernetes 中的工作原理如下:

  1. Kubernetes API 服务器通过 client-ca-file 参数启动。这个参数提供 证书颁发机构CA)信息,用于验证提供给 API 服务器的客户端证书。你可以在这里配置自定义的 CA 证书,或者使用集群部署过程中创建的默认 CA。例如,如果你使用的是 minikube,你可以看到在 kube-apiserver 中已经配置了一个默认的 CA 文件,如下所示:

    # /etc/kubernetes/manifests/kube-apiserver.yaml
    ...<removed for brevity>...
    spec:
      containers:
      - command:
        - kube-apiserver
        - --advertise-address=192.168.59.154
        - --allow-privileged=true
        - --authorization-mode=Node,RBAC
        **-****--client-ca-file=/var/lib/minikube/certs/ca.crt**
    ...<removed for brevity>... 
    
  2. 需要对 API 服务器进行认证的用户需要从 CA 请求一个 X.509 客户端证书。这应该是一个安全且经过审计的过程。证书的主题公共名称(证书主题中的 CN 属性)在认证成功时会作为 username 属性使用。请注意,从 Kubernetes 1.19 开始,你可以使用证书 API 来管理签名请求。更多信息请参阅官方文档:kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/

  3. 用户必须在认证过程中向 API 服务器提供客户端证书,服务器将证书与 CA 进行验证。基于此,请求会成功通过认证过程或被拒绝。再次提醒,如果你使用的是 minikube 集群,那么你已经在使用基于证书的认证,如下例所示:

    $ kubectl config view -o json | jq '.users[]'
    {
      "name": "example-sa",
      "user": {
        "token": "REDACTED"
      }
    }
    {
      "name": "minikube",
      "user": {
        "client-certificate": "/home/iamgini/.minikube/profiles/minikube/client.crt",
        "client-key": "/home/iamgini/.minikube/profiles/minikube/client.key"
      }
    } 
    

在使用 kubectl 命令时,用户可以通过 kubectl config set-credentials 命令在 kubeconfig 中配置这种认证方法,正如我们之前所学到的那样。我们在下图中总结了这个过程:

图 18.4 – Kubernetes 中的 X.509 客户端证书认证

图 18.5:Kubernetes 中的 X.509 客户端证书认证

请注意,这展示的是用户初始 CSR 由 Kubernetes 集群中的证书 API 处理的情况。实际上,不必如此,因为 CA 可能位于集群外部,Kubernetes API 服务器可以依赖于 CA .pem 文件的副本。

在接下来的实操练习中,我们将生成并配置 Kubernetes 中的证书认证:

  1. 使用 openssl 命令开始创建私钥:

    $ openssl genrsa -out iamgini.key 2048 
    
  2. 生成证书签名请求CSR):

    $ openssl req -new -key iamgini.key -out iamgini.csr -subj "/CN=iamgini/O=web1/O=frontend" 
    
  3. 收集 CSR 数据并使用 base64 编码:

    $ cat iamgini.csr | base64 -w 0 
    
  4. 现在,我们需要使用证书 API创建一个 证书签名请求 资源;让我们按如下方式使用 csr.yaml 文件:

    # csr.yaml
    apiVersion: certificates.k8s.io/v1
    kind: CertificateSigningRequest
    metadata:
      name: iamgini
    spec:
      request: <**your****encoded****CSR****content****here****from****Step.3>**
      signerName: kubernetes.io/kube-apiserver-client
      usages:
        - client auth 
    
  5. 创建 证书签名请求

    $ kubectl apply -f csr.yaml
    certificatesigningrequest.certificates.k8s.io/iamgini created 
    
  6. 现在,管理员(或具有 certificatesigningrequests 权限的用户)可以查看 CSR 资源:

    $ kubectl get csr
    NAME      AGE   SIGNERNAME                            REQUESTOR       REQUESTEDDURATION   CONDITION
    iamgini   25s   kubernetes.io/kube-apiserver-client   minikube-user   <none>              Pending 
    
  7. 按如下方式检查并批准 CSR:

    $ kubectl certificate approve iamgini
    certificatesigningrequest.certificates.k8s.io/iamgini approved 
    
  8. 一旦 CSR 被批准,从批准的 CSR 资源中获取证书数据,具体操作如下;以下命令会将数据提取到 iamgini.crt 文件:

    $ kubectl get csr iamgini -o json | jq -r '.status.certificate' | base64 --decode > iamgini.crt 
    
  9. 现在,我们有了私钥和证书,具体如下(可以删除 .csr 文件,因为不再需要):

    $ ls iamgini.*
    iamgini.crt  iamgini.csr  iamgini.key 
    
  10. 现在,我们将使用新用户和上下文来配置 kubeconfig;按如下方式在 kubeconfig 中创建一个新用户条目(记得使用密钥和证书文件的完整路径):

    $ kubectl config set-credentials iamgini --client-key=/full-path/iamgini.key --client-certificate=/full-path/iamgini.crt
    User "iamgini" set. 
    
  11. 使用新用户创建一个新上下文:

    $ kubectl config set-context iamgini --cluster=minikube --user=iamgini
    Context "iamgini" created. 
    
  12. 现在,kubeconfig 已更新为新用户和上下文。让我们测试访问权限。按如下方式更改 kubeconfig 上下文:

    $ kubectl config use-context iamgini
    Switched to context "iamgini". 
    
  13. 验证上下文和连接:

    $ kubectl auth whoami
    ATTRIBUTE   VALUE
    Username    iamgini
    Groups      [web1 frontend system:authenticated] 
    

恭喜;你已经配置了基于 X509 证书的认证新用户。但请记住,在配置适当的 RBAC 资源之前,该用户无法执行任何操作。

基于我们所学的内容,我们可以总结此方法的优点如下:

  • 这是一个比使用 ServiceAccount 令牌或静态令牌文件更安全的过程。

  • 无法在集群中存储证书意味着无法危及所有证书。X.509 客户端证书可以用于高权限用户账户。

  • X.509 客户端证书可以按需撤销。这在发生安全事件时非常重要。

X.509 客户端证书认证的缺点如下:

  • 证书有有效期,这意味着它们不能无限期有效。对于开发中的简单用例,这是一个缺点。从安全角度来看,在生产集群中,这是一个巨大的优点。但请记住,确保证书安全存储,因为基于文件的认证机制存在安全风险;文件可能被窃取并用于未经授权的访问

  • 必须处理证书到期、撤销和轮换监控。这应该是一个自动化过程,以便在发生安全事件时我们能够迅速响应。

  • 在浏览器中使用客户端证书进行认证是麻烦的,例如,当您想要认证到 Kubernetes Dashboard 时。

关键要点是,使用 X.509 客户端证书是安全的,但需要复杂的证书管理,以便我们能获得所有的好处。现在,我们将看看 OpenID Connect 令牌,它是云环境中推荐的方法。

OpenID Connect 令牌

使用OpenID ConnectOIDC),您可以为您的 Kubernetes 集群(以及可能是组织中的其他资源)实现单点登录SSO)体验。OIDC 是一个建立在 OAuth 2.0 之上的认证层,它允许第三方应用程序验证终端用户的身份并获取基本的用户个人信息。OIDC 使用 JWT,这些 JWT 可以通过符合 OAuth 2.0 规范的流程获取。使用 OIDC 进行 Kubernetes 认证的最大问题是 OpenID 提供者的可用性有限。但如果您在云环境中部署,所有一级云服务提供商,如 Microsoft Azure、Amazon Web Services 和 Google Cloud Platform,都有自己的 OpenID 提供者版本。云中托管的 Kubernetes 集群部署(如 AKS、Amazon EKS 和 Google Kubernetes Engine)的优势在于,它们提供与其本地 OpenID 提供者的即插即用集成,或者只需简单的配置开关。换句话说,您无需担心重新配置 Kubernetes API 服务器并使其与所选的 OpenID 提供者一起工作——您将与托管解决方案一起获得它。如果您有兴趣了解更多有关 OIDC 协议的信息,可以参考openid.net的官方网站。

有关更多细节和更具体的流程,如 AAD 的上下文,请查看docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc

在下图中,您可以看到 Kubernetes 中 OIDC 认证流程的基础:

图 18.5 – Kubernetes 中的 OpenID Connect 认证

图 18.6:Kubernetes 中的 OIDC 认证

最重要的是,OpenID 提供者负责单点登录(SSO)体验以及管理承载令牌。此外,Kubernetes API 服务器必须验证收到的承载令牌,并与 OpenID 提供者进行对比。

使用 OIDC 具有以下优点:

  • 您可以获得单点登录(SSO)体验,您可以与组织中的其他服务一起使用。

  • 大多数云服务提供商都有自己的 OpenID 提供者,这些提供者能够轻松与其托管的 Kubernetes 服务集成。

  • 它也可以与其他 OpenID 提供者和非云部署一起使用——不过这需要更多的配置。

  • 这是一个安全且可扩展的解决方案。

OIDC 方法的缺点可以总结如下:

  • Kubernetes 没有 Web 界面可以触发身份验证过程。这意味着你需要通过手动向 IdP 请求凭证来获取它们。在托管的云 Kubernetes 服务中,通常通过附加的简单工具来解决这一问题,这些工具可以生成带有凭证的 kubeconfig

  • 如果 IdP 支持令牌端点撤销功能,OIDC 令牌可以被撤销。这允许在令牌过期之前使其失效,例如当用户帐户被泄露时。然而,并非所有 IdP 都支持此功能,Kubernetes 也不处理令牌撤销。

在 Kubernetes 中使用 OIDC

Kubernetes 不提供集成的 OpenID Connect 身份提供者。因此,它依赖于由云服务提供商或独立工具提供的外部身份提供者。如我们在本节前面提到的,最流行的云环境——如 AWS、GCP 和 Azure——在其托管的 Kubernetes 产品中原生提供 OIDC 集成,这使得启用单点登录(SSO)非常简单。或者,身份提供者还可以通过使用如 Dex、Keycloak、UAA 或 OpenUnison 等工具,为非云或自管理的 Kubernetes 集群独立设置。

Kubernetes 中的身份提供者要求

要使 OIDC 身份提供者与 Kubernetes 配合使用,它必须满足一些重要的前提条件:

  • 支持 OIDC 发现:OIDC 发现简化了配置工作,因为通过它可以获取关于 IdP 端点和公钥的所有信息。Kubernetes 从发现端点读取 IdP 的公钥来验证 OIDC 令牌。

  • 传输层安全性TLS合规性:身份提供者应处理 TLS 以处理非过时的加密算法,因为敏感的身份验证数据处理至关重要。

  • CA 签名证书:无论是使用商业 CA 还是自签名证书,身份提供者的证书必须将 CA 标志设置为 TRUE。这是因为 Kubernetes 使用 Go 的 TLS 客户端,严格执行这一要求,以便 Kubernetes 在用户令牌验证过程中能够安全地信任身份提供者的证书。

对于没有商业 CA 的身份提供者的自部署者,可以使用如 Dex gencert 脚本等工具来创建符合要求的 CA 证书及签名密钥。

以下列表包含一些适用于 Kubernetes 的流行 OIDC 身份提供者:

  • Dex:一种轻量级的开源流行身份提供者(IdP),适用于 Kubernetes 环境。它支持 OIDC,并且与 Kubernetes 预期的身份验证工作流兼容。Dex 通过连接到其他外部 IdP(如 LDAP、GitHub 和 Google)来工作,这使其成为具有更复杂身份场景的组织的理想选择。

  • Keycloak:这是一个开源身份提供者(IdP),提供强大的功能集,广泛支持 OIDC 和 SAML。除了核心功能外,Keycloak 还支持企业级功能,如用户联合和基于角色的访问控制(RBAC)。如果你希望在身份验证设置中拥有更多控制或定制,Keycloak 会是一个不错的选择。

  • OpenUnison:另一个为 Kubernetes 优化的 IdP 是 OpenUnison,具备像本地集成 Kubernetes RBAC 和身份联合等功能。它应该会受到准备好采用预构建解决方案、并根据自身需求对 Kubernetes 进行安全配置的企业的青睐。

  • Cloud Foundry 用户账户与认证UAA):这是一个开源的多用途身份提供者,源自 Cloud Foundry。它支持 OIDC,并在云平台和企业认证系统集成方面表现非常强大,非常适合在混合云环境中部署更复杂的 Kubernetes 集群。

配置 Kubernetes API 服务器的 OIDC

在 Kubernetes 中启用 OIDC 需要对 Kubernetes API 服务器进行一些配置,使用特定的 OIDC 相关标志。主要配置包括以下内容:

  • oidc-issuer-url:OIDC 提供者的 URL。Kubernetes 使用它来验证令牌的真实性。

  • oidc-client-id string:在 Kubernetes 作为客户端时,用于与 IdP 进行身份验证时的客户端 ID。

  • oidc-username-claim:指定令牌中的哪个声明应该映射到 Kubernetes 用户名。

  • oidc-groups-claim:将 IdP 中的组映射到 Kubernetes 组,以便管理 RBAC 角色。

关于配置特定 OIDC 身份提供者的更多细节,你可以参考官方资源,例如 Kubernetes 指南中的 Dex (dexidp.io/docs/guides/kubernetes/) 或 Kubernetes 中的 OpenID Connect 认证 (kubernetes.io/docs/reference/access-authn-authz/authentication/)。

关于 OIDC 的一个关键要点是,这是配置 Kubernetes 身份验证时最好的选择,尤其是在你部署生产集群到云环境中时。

其他方法

Kubernetes 提供了几种其他认证方法供你使用。这些方法主要用于高级用例,例如与 LDAP 或 Kerberos 集成。第一个是 认证代理

当你在 Kubernetes API 服务器前使用认证代理时,可以配置 API 服务器使用某些 HTTP 头部来提取认证用户信息。换句话说,你的认证代理在执行用户认证的工作,并将这部分信息以附加头部的形式随请求一起传递。

你可以在官方文档中找到更多信息(kubernetes.io/docs/reference/access-authn-authz/authentication/#authenticating-proxy)。

另一种方法是Webhook 令牌认证,在这种方法中,Kubernetes API 服务器使用外部服务来验证持有令牌。外部服务通过 HTTP POST 请求从 API 服务器接收 TokenReview 对象,并执行验证,随后返回带有结果附加信息的 TokenReview 对象。

从官方文档中可以找到更多信息(kuberntes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)。

Kubernetes 还使用另一种常见的认证方法,称为引导令牌。但引导令牌并不用于一般的认证,而是用于集群节点。引导令牌是 Kubernetes 中的一种特殊类型的秘密,用于简化将新节点添加到集群的过程。存储在 kube-system 命名空间中,这些短生命周期的令牌允许 API 服务器在初始连接时对 kubelet(运行在节点上的程序)进行认证。这简化了引导过程,使得加入新节点或从头创建新集群变得更加容易。它们可以与 kubeadm 工具一起使用,也可以独立使用,并与 Kubelet TLS 引导结合使用以实现安全通信。有关使用引导令牌和 TLS 引导认证的详细信息,请参阅文档(kubernetes.io/docs/reference/access-authn-authz/bootstrap-tokens)。

一般来说,当你希望与组织中现有的身份提供者集成,而这些身份提供者不被 Kubernetes 默认支持时,你需要使用认证代理和 Webhook 令牌认证方法。

在下一节中,我们将介绍 Kubernetes 中的授权和 RBAC。

授权与 RBAC 简介

Kubernetes 的安全性依赖于两个关键过程:认证授权。认证验证试图访问系统的用户的身份,确保他们是自己所声称的人。这个初步步骤通常涉及检查用户名、密码或令牌等凭据。

成功认证后,授权过程开始发挥作用。此过程决定用户在系统内可以执行的操作。在 Kubernetes 中,API 服务器会评估用户的身份(来源于认证)以及其他请求属性,如请求的特定 API 端点或操作。基于预定义的策略或外部服务,授权模块决定是否允许或拒绝该请求。

身份验证是确定用户身份的第一步,而授权则是在验证用户是否可以执行其想要的操作时进行的下一步。

基于特定对象字段的访问控制由准入控制器处理,它们发生在授权之后,并且只有在授权允许请求的情况下才会执行。我们将在本章的后面部分学习准入控制器。

在 Kubernetes API 服务器中,认证请求会生成一组额外的请求属性,例如 usergroupAPI 请求动词HTTP 请求动词 等。这些属性将被传递给进一步的授权模块,基于这些属性,模块会判断用户是否被允许执行该操作。如果任何模块拒绝该请求,用户将看到 HTTP 状态码 403 (Forbidden)

这是 HTTP 状态码之间的重要区别。如果你收到 401 (Unauthorized),这意味着你未被系统识别;例如,你提供了错误的凭据或用户不存在。如果你收到 403 (Forbidden),这意味着身份验证已成功并且你已被识别,但你不被允许执行你请求的操作。这对于调试访问 Kubernetes 集群的问题非常有用。

Kubernetes 提供了一些授权模式,可以通过启动 Kubernetes API 服务器时使用 authorization-mode 参数来启用,具体如下:

# /etc/kubernetes/manifests/kube-apiserver.yaml
...<removed for brevity>...
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=192.168.59.154
    - --allow-privileged=true
    **-****--authorization-mode=Node,RBAC**
...<removed for brevity>... 

以下是 Kubernetes 中可用的授权模式:

  • RBAC:这允许你通过角色和权限来组织访问控制和管理。RBAC 是访问管理的行业标准之一,也被广泛应用于 Kubernetes 之外。角色可以分配给系统中的用户,从而赋予他们一定的权限和访问权限。通过这种方式,你可以实现非常细粒度的访问管理,并且可以用来执行最小权限原则。例如,你可以在系统中定义一个角色,允许你访问网络共享上的某些文件。然后,你可以将这些角色分配给系统中的用户组中的个别用户,允许他们访问这些文件。这可以通过将用户与角色关联来实现——在 Kubernetes 中,你使用 RoleBindingClusterRoleBinding 对象来建模这种关系。通过这种方式,可以将多个角色分配给多个用户,单个用户也可以拥有多个角色。请注意,在 Kubernetes 中,RBAC 是宽松的,这意味着没有拒绝规则。默认情况下,一切都被拒绝,你需要定义允许规则。

  • 基于属性的访问控制(ABAC):这是一种访问控制范式的一部分,不仅在 Kubernetes 中使用,也可用于其他系统。它基于用户、资源和环境的属性来制定策略。这是一种非常细粒度的访问控制方法——例如,你可以定义用户可以访问某个文件,但前提是用户有权限访问机密数据(用户属性),该文件的所有者是 Mike(资源属性),并且用户是在内部网络中尝试访问该文件(环境属性)。因此,策略是一组必须共同满足的属性,才能执行某个操作。在 Kubernetes 中,这通过 Policy 对象进行建模。例如,你可以定义已认证的用户 mike 可以读取 default 命名空间中的任何 Pods。如果你希望将相同的访问权限授予用户 bob,则需要为用户 bob 创建一个新的 Policy。

  • Node:这是一个特殊用途的授权模式,用于授权集群中由 kubelet 发起的 API 请求。

  • Webhook:这种模式类似于身份验证的 Webhook。你可以定义一个外部服务,该服务需要处理 Kubernetes API 服务器发送的包含 SubjectAccessReview 对象的 HTTP POST 请求。此服务必须处理请求,并确定该请求是否应被允许或拒绝。该服务的响应应包含 SubjectAccessReview,以及有关是否允许该主体访问的详细信息。根据该信息,Kubernetes API 服务器将继续处理请求或使用 HTTP 状态码 403 拒绝请求。当你与组织中现有的访问控制解决方案集成时,这种方法非常有用。

  • AlwaysAllow:这授予所有请求不受限制的访问权限,仅适用于由于安全问题而限制在测试环境中使用。

  • AlwaysDeny:这会阻止所有请求,仅用于测试目的,用于建立授权的基准。

当前,RBAC 被认为是 Kubernetes 中的行业标准,因为它具有灵活性和易于管理的特点。因此,RBAC 是我们将详细描述的唯一身份验证模式。

Kubernetes 中的 RBAC 模式

在 Kubernetes 中使用 RBAC 涉及以下几种 API 资源,它们属于 rbac.authorization.k8s.io API 组:

  • RoleClusterRole:它们定义了一组权限。Role 中的每个 rule 都指定了哪些动词(verbs)可以对哪些 API 资源(resources)执行。Role 和 ClusterRole 唯一的区别是,Role 是基于命名空间的,而 ClusterRole 是全局范围的。

  • RoleBindingClusterRoleBinding:它们将用户或一组用户(或群组或 ServiceAccounts)与给定的 Role 关联。类似地,RoleBinding 是基于命名空间的,而 ClusterRoleBinding 是集群范围的。请注意,ClusterRoleBinding 与 ClusterRole 一起使用,而 RoleBinding 可与 ClusterRole 和 Role 一起使用。

所有这些 Kubernetes 对象都可以使用 kubectl 和 YAML 清单进行管理,就像你管理 Pods、Services 等一样。

我们现在将实际演示这一过程。在前一部分中,我们展示了一个基本的 RBAC 配置,用于通过 kubectl 进行身份验证的服务账户。我们将使用的这个示例会有所不同,它将涉及创建一个在 专用 服务账户下运行的 Pod,并定期查询 Kubernetes API 服务器以获取 Pod 列表。通常,为 Pod 创建专用的服务账户是一种好做法,这样可以确保最小权限原则。例如,如果你的 Pod 需要获取集群中 Pods 的列表,但不需要创建新的 Pod,则该 Pod 的 ServiceAccount 应该分配一个允许列出 Pods 的角色,而不做其他操作。按照以下步骤来配置这个示例:

  1. 首先,使用以下 YAML 文件为对象创建一个专用的命名空间:

    # 02_rbac/rbac-demo-ns.yaml
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: rbac-demo-ns 
    

通过应用 YAML 来创建命名空间

$ kubectl apply -f 02_rbac/rbac-demo-ns.yaml
namespace/rbac-demo-ns created 
  1. 为了演示,我们使用 02_rbac/nginx-pod.yaml 定义,在相同的命名空间中创建一个示例 nginx Pod:

    $  kubectl apply -f 02_rbac/nginx-pod.yaml
    pod/nginx-pod created 
    

请注意,nginx Pod 在这里并没有做任何事情;我们需要 pod-logger-app Pod 来获取稍后在 rbac-demo-ns 命名空间中的 nginx Pod 详情。

  1. 现在,创建一个名为 pod-logger 的 ServiceAccount。创建一个名为 pod-logger-serviceaccount.yaml 的 YAML 清单:

    # 02_rbac/pod-logger-serviceaccount.yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: pod-logger
      namespace: rbac-demo-ns 
    

使用以下命令将清单应用到集群中:

$ kubectl apply -f 02_rbac/pod-logger-serviceaccount.yaml
serviceaccount/pod-logger created 
  1. 创建一个名为 pod-reader 的角色。该角色仅允许对 Kubernetes RESTful API 中的 pods 资源执行 getwatchlist 操作。换句话说,这相当于 API 中的 /api/v1/namespaces/rbac-demo-ns/pods 端点。请注意,apiGroups 被指定为 "" 意味着 core API 组。pod-reader-role.yaml 清单文件的结构如下:

    # 02_rbac/pod-reader-role.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      namespace: rbac-demo-ns
      name: pod-reader
    rules:
      - apiGroups: [""]
        resources: ["pods"]
        verbs: ["get", "watch", "list"] 
    
  2. 使用以下命令将清单应用到集群中:

    $ kubectl apply -f 02_rbac/pod-reader-role.yaml
    role.rbac.authorization.k8s.io/pod-reader created 
    
  3. 现在,我们通常会创建一个 RoleBinding 对象,将服务账户与角色关联。但为了使演示更加有趣,我们将创建一个在 pod-logger 服务账户下运行的 Pod。这将使该 Pod 无法查询 API 中的 Pods,因为它将是 未授权 的(记住,RBAC 中的默认设置是所有操作都被拒绝)。创建一个名为 pod-logger-app.yaml 的 YAML 清单,定义一个名为 pod-logger-app 的 Pod,且没有任何额外的控制器:

    # 02_rbac/pod-logger-app.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod-logger-app
      namespace: rbac-demo-ns
    spec:
      serviceAccountName: pod-logger
      containers:
        - name: logger
          image: quay.io/iamgini/k8sutils:debian12
          command:
            - /bin/sh
            - -c
            - |
              SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
              TOKEN=$(cat ${SERVICEACCOUNT}/token)
              while true
              do
                echo "Querying Kubernetes API Server for Pods in default namespace..."
                curl --cacert $SERVICEACCOUNT/ca.crt --header "Authorization: Bearer $TOKEN" -X GET https://kubernetes.default.svc.cluster.local/api/v1/namespaces/**rbac-demo-ns**/pods
                sleep 10
              done 
    

在这里,最重要的字段是 .spec.serviceAccountName,它指定 Pod 应该运行的服务账户;以及容器定义中的 command,我们已重写该命令,使其定期查询 Kubernetes API。

  1. 让我们应用 02_rbac/pod-logger-app.yaml 来创建 Pod,如下所示:

    $ kubectl apply -f 02_rbac/pod-logger-app.yaml
    pod/pod-logger-app created
    $ kubectl get po -n rbac-demo-ns
    NAME             READY   STATUS    RESTARTS   AGE
    nginx-pod        1/1     Running   0          15m
    pod-logger-app   1/1     Running   0          9s 
    
  2. 如前所述,为pod-logger服务帐户分配身份将导致将一个带有此帐户的 Bearer JWT 的 Secret 挂载到容器文件系统中的/var/run/secrets/kubernetes.io/serviceaccount/token。让我们使用kubectl exec验证这一点,如下所示:

    $ kubectl exec -it -n rbac-demo-ns pod-logger-app -- bash
    root@pod-logger-app:/# ls -l /var/run/secrets/kubernetes.io/serviceaccount/
    total 0
    lrwxrwxrwx 1 root root 13 Jul 14 03:33 ca.crt -> ..data/ca.crt
    lrwxrwxrwx 1 root root 16 Jul 14 03:33 namespace -> ..data/namespace
    lrwxrwxrwx 1 root root 12 Jul 14 03:33 token -> ..data/token 
    
  3. 重写的命令在 Linux shell(例如 bash)中以 10 秒间隔运行无限循环。在每次迭代中,我们使用curl命令使用 HTTP GET 方法查询 Kubernetes API 端点(https://kubernetes/api/v1/namespaces/rbac-demo-ns/pods)中rbac-demo-ns命名空间中的 Pod。为了正确进行身份验证,我们将/var/run/secrets/kubernetes.io/serviceaccount/token的内容作为bearer令牌传递到请求的Authorization头中。此外,我们通过传递 CA 证书路径来使用cacert参数验证远程服务器。证书由 Kubernetes 运行时注入到/var/run/secrets/kubernetes.io/serviceaccount/ca.crt中。当您检查其日志时,您应该期望看到大量 HTTP 状态代码为403 (Forbidden)的消息。这是因为服务帐户尚未具有将其与pod-reader角色关联的 RoleBinding 类型。

  4. 使用以下命令开始跟踪pod-logger-app Pod 的日志:

    $ kubectl logs -n rbac-demo-ns pod-logger-app -f
    Querying Kubernetes API Server for Pods in rbac-demo-ns namespace...
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100   336  100   336    0     0  25611      0 --:--:-- --:--:-- --:--:-- 25846
    {
      "kind": "Status",
      "apiVersion": "v1",
      "metadata": {},
      "status": "Failure",
      "message": "pods is forbidden: User \"system:serviceaccount:rbac-demo-ns:pod-logger\" cannot list resource \"pods\" in API group \"\" in the namespace \"rbac-demo-ns\"",
      "reason": "Forbidden",
      "details": {
        "kind": "pods"
      },
      "code": 403
    } 
    
  5. 在新的控制台窗口(或通过使用 Ctrl + F 命令结束日志),我们将创建并应用一个 RoleBinding,将pod-logger服务帐户与pod-reader角色关联起来。创建一个名为read-pods-rolebinding.yaml的 YAML 清单文件,其内容如下:

    # 02_rbac/read-pods-rolebinding.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: read-pods
      namespace: rbac-demo-ns
    subjects:
    - kind: ServiceAccount
      name: pod-logger
      namespace: rbac-demo-ns
    roleRef:
      kind: Role
      name: pod-reader
      apiGroup: rbac.authorization.k8s.io 
    

RoleBinding 清单中有三个关键组件:name,用于标识用户;subjects,引用用户、组或服务帐户;roleRef,引用角色。

  1. 使用以下命令应用 RoleBinding 清单文件:

    $ kubectl apply -f 02_rbac/read-pods-rolebinding.yaml
    rolebinding.rbac.authorization.k8s.io/read-pods created 
    
  2. 现在再次检查pod-logger-app的日志,您将看到 Pod 能够成功检索rbac-demo-ns命名空间中 Pod 的列表。换句话说,请求已成功授权:

    $ kubectl logs -n rbac-demo-ns pod-logger-app -f
    ...<removed for brevity>...
    Querying Kubernetes API Server for Pods in default namespace...
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    {
      "kind": "PodList",
      "apiVersion": "v1",
      "metadata": {
        "resourceVersion": "4889"
      },
      "items": 
        {
          "metadata": {
            "name": "nginx-pod",
            "namespace": "rbac-demo-ns",
            "uid": "b62b2bdb-2677-4809-a134-9d6cfa07ecad",
    ...<removed for brevity>... 
    
  3. 最后,您可以使用以下命令删除 RoleBinding 类型:

    $ kubectl delete rolebinding read-pods -n rbac-demo-ns 
    
  4. 现在,如果再次检查pod-logger-app Pod 的日志,您将再次看到请求被拒绝,HTTP 状态码为403

恭喜!您已成功在 Kubernetes 中使用 RBAC 来读取集群中运行的 Pod 的权限。要清理 Kubernetes 环境,您可以删除rbac-demo-ns命名空间,这样您创建的资源将随命名空间一起删除。

正如我们已经探讨了身份验证和授权,在下一节中,让我们了解 Kubernetes 中的另一个安全功能,称为准入控制器。

准入控制 - 安全策略和检查

想象一个关键设施的安全检查点。Kubernetes 中的入场控制器对集群的作用类似。它们充当守门员,拦截请求到 Kubernetes API 服务器,在资源创建、删除或修改之前进行处理。这些控制器可以根据预定义规则验证或修改请求,确保只有经过授权且配置正确的资源进入系统。同时请注意,入场控制器不能(也无法)阻止读取(获取、监视或列出)对象的请求。

Kubernetes 的多个关键特性依赖于特定的入场控制器来正确运行。因此,没有适当入场控制器的 Kubernetes API 服务器是不完整的,无法支持所有预期的功能。

入场控制器有两种类型:

  • 验证控制器:这些控制器会仔细检查传入的请求。如果发现任何可疑的或不符合设定策略的内容,它们将完全拒绝该请求。

  • 变更控制器:这些控制器有权在请求被永久存储之前修改它们。例如,它们可以添加缺失的安全注解或调整资源限制。

现在,让我们来了解一下两阶段的入场处理流程。

两阶段入场处理流程

Kubernetes 的入场控制通过两步过程进行,确保只有符合要求的资源进入集群。

Kubernetes 入场控制的 Mutation 和 Validation 阶段的高层次流程如下面的图所示。这个流程将传入的请求送入 Kubernetes 进行处理,首先进行适当的变更,以修改或丰富请求,然后进行实际的验证,以检查请求是否符合所有必需的安全性和策略验证要求。

这个流程展示了 Kubernetes 如何强制执行一致性、安全性和策略合规性,在允许对集群状态进行任何更改之前:

![

图 18.7:API 请求处理流程中的入场控制器(图片来源:kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

以下是每个阶段的详细解析。

Mutation 阶段

Mutation 阶段是 Kubernetes 中的一步入场控制,运行在变更控制器角色中的控制器将修改传入的 API 请求,使其符合集群策略,然后再进行后续处理。这些控制器基本上就像是“模具”,它们不仅确保请求与已设定的配置一致,还可以自动添加或调整设置,例如默认值或安全标签。这使得系统能够在没有人工干预的情况下,保持策略一致性和配置的对齐。

这里列出了该阶段的几个示例:

  • 向 pods 添加缺失的安全注解。

  • 根据预定义规则调整 pods 的资源请求和限制。

  • 为特定功能注入侧车容器。

验证阶段

正是在验证阶段,Kubernetes Admission 控制完成了在前述变更阶段由控制器执行的操作。控制器随后会密切检查可能已被某些控制器修改的传入请求。通常被称为“守护者”,这些控制器检查请求是否符合集群策略和安全标准。这是防止错误配置和未经授权的更改的重要阶段,通过拒绝不符合设定标准的请求,保持集群的完整性和安全性。

一些示例操作如下所示:

  • 如果请求符合设定的标准(例如资源配额、安全标准),则批准请求。

  • 如果请求违反任何策略,则拒绝请求,并提供有用的错误信息。

在接下来的章节中,我们将学习如何在 Kubernetes 中启用和禁用 Admission 控制器。

启用和禁用 Admission 控制器

要检查启用了哪些 Admission 控制器,通常需要查看 Kubernetes API 服务器 的配置。这通常通过访问定义 API 服务器的配置文件来完成,该文件通常位于系统的配置目录中,或通过配置管理工具进行管理。查找 --enable-admission-plugins 标志,它指定当前启用的 Admission 控制器列表。

例如,在 minikube 环境中,可以使用 minikube ssh 命令 SSH 登录到 minikube 虚拟机。登录后,可以定位并检查 kube-apiserver.yaml 文件,通常位于 /etc/kubernetes/manifests/。使用 sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml 查看其内容,并查找 --enable-admission-plugins 标志:

$ minikube ssh 'sudo grep -- '--enable-admission-plugins' /etc/kubernetes/manifests/kube-apiserver.yaml'
    - --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota 

要修改启用的插件列表,可以使用 nano 或 vi 等文本编辑器编辑此文件,根据需要调整插件,然后保存更改。kubelet 会监视清单文件,并在检测到文件更改时自动重启 API 服务器(重新创建 Pod)。

还可以按如下方式关闭默认的 Admission 控制器:

kube-apiserver --disable-admission-plugins=PodNodeSelector,AlwaysDeny ... 

在接下来的章节中,我们将了解 Kubernetes 中可用的 Admission 控制器列表。

常见的 Admission 控制器

在 Kubernetes 中,Admission 控制器内置于 kube-apiserver,仅应由集群管理员配置。在这些控制器中,有两个特别值得注意:MutatingAdmissionWebhookValidatingAdmissionWebhook。这些控制器执行通过 API 配置的相应变更和验证 Admission 控制 Webhook:

  • 基础控制AlwaysAdmit(已弃用)、AlwaysDeny(已弃用)、AlwaysPullImages

  • 默认DefaultStorageClassDefaultTolerationSecond

  • 安全性DenyEscalatingExecDenyServiceExternalIPsPodSecurityPolicySecurityContextDeny

  • 资源管理: LimitRangerResourceQuotaRuntimeClass

  • 对象生命周期: NamespaceAutoProvisionNamespaceExistsNamespaceLifecyclePersistentVolumeClaimResizeStorageObjectInUseProtection

  • 节点管理: NodeRestrictionTaintNodesByCondition

  • Webhooks: MutatingAdmissionWebhookValidatingAdmissionWebhook

  • 其他: EventRateLimitLimitPodHardAntiAffinityTopologyOwnerReferencesPermissionEnforcementPodNodeSelector(已弃用)、PriorityServiceAccount

使用入驻控制器在 Kubernetes 集群中有许多优点,接下来我们将介绍其中的一些。

入驻控制器的好处

在你的 Kubernetes 集群中使用入驻控制器有多个优点,包括以下几点:

  • 增强的安全性: 通过执行像 Pod 安全标准这样的安全策略,入驻控制器帮助保护你的集群免受未经授权或脆弱的部署。

  • 策略执行: 你可以定义资源使用、镜像拉取等规则,入驻控制器将自动执行这些规则。

  • 一致性与标准化: 入驻控制器确保集群中的资源遵循既定的最佳实践和配置。

总结来说,入驻控制器部分强调了入驻控制器在确保 Kubernetes 安全性方面的至关重要作用,具体体现在 Mutation 和 Validation 阶段。我们学习了 mutation 控制器如何对请求进行修改,以确保其符合集群策略,而 validation 控制器则确保只有符合安全标准的请求被处理。总体而言,这些过程通过保证合规性和禁止未经授权的更改,提升了 Kubernetes 集群的整体安全性。

在接下来的部分,我们将学习如何通过 Security Context 和 NetworkPolicies 在 Kubernetes 中保护工作负载。

保护 Pod 和容器

保护 Pod 和容器对于保持 Kubernetes 环境的健康状态至关重要,因为它们直接与工作负载和敏感数据交互。在接下来的部分,我们将讨论如何通过 securityContext 设置和 NetworkPolicies 强化访问控制和隔离,进一步增强集群中 Pod 和容器的安全性。

使用 Security Context 安全地保护 Kubernetes 中的 Pod 和容器

在 Kubernetes 中,securityContext 定义了一组安全设置,决定了 Pod 或容器在集群中如何操作。这使得你能够执行最佳的安全实践,通过限制权限和控制访问来最小化攻击面。

securityContext 的主要目的是通过定义 Pod 或容器在集群内如何运行,增强 Kubernetes 集群的安全性。通过指定安全设置,您可以确保应用程序遵循最小权限原则,减少恶意活动和意外配置错误的可能性。

securityContext 的一个典型用例是将容器以非 root 用户身份运行。这可以防止容器拥有不必要的权限,从而在容器被攻破时限制潜在的损害。此外,您还可以配置其他安全设置,例如只读文件系统和细粒度的能力,以进一步增强集群的安全性。

安全上下文的关键组件

下面是安全上下文的关键组件以及说明性示例的详细说明。

用户和组

这个安全上下文指定了容器内部进程运行时的用户和组 ID。通过执行最小权限原则,它仅授予容器执行所需的最小权限。以下代码片段展示了一个典型的 Pod 定义,其中配置了 securityContext:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app-container
    securityContext:
      runAsUser: 1000  # Run container processes as user ID 1000
      runAsGroup: 1000 # Run container processes as group ID 1000 

Linux 能力

能力是可以授予容器的特殊权限,超出了用户的限制。securityContext 允许您定义容器应该拥有的能力,启用特定功能而不提供完全的 root 权限,如以下示例所示:

apiVersion: v1
kind: Pod
metadata:
  name: privileged-container
spec:
  containers:
  - name: app-container
    securityContext:
      capabilities:
        add:
          - CAP_NET_ADMIN  # Grant network management capabilities 

请参考 Linux 能力文档以了解更多信息(linux-audit.com/kernel/capabilities/linux-capabilities-hardening-linux-binaries-by-removing-setuid/)。

特权模式

privileged mode securityContext:
apiVersion: v1
kind: Pod
metadata:
  name: privileged-container
spec:
  containers:
  - name: app-container
    securityContext:
      privileged: true  # Run container in privileged mode (use cautiously) 

只读根文件系统

这个 securityContext 允许您配置容器拥有只读的根文件系统。通过防止对基础系统的意外或恶意修改,从而提高安全性:

apiVersion: v1
kind: Pod
metadata:
  name: read-only-container
spec:
  containers:
  - name: app-container
    securityContext:
      **readOnlyRootFilesystem:****true**  # Mount root filesystem as read-only 

还有一些其他的 SecurityContext 设置,例如 增强型安全 LinuxSELinux)、AppArmorkubernetes.io/docs/tutorials/security/apparmor/)、Seccomp 等等。请参考 kubernetes.io/docs/tasks/configure-pod-container/security-context/ 了解更多。

您还需要了解在配置中应用 SecurityContext 的最佳位置;我们将在下一节学习这个内容。

在 Pod 和容器层级应用 SecurityContext

在 Kubernetes 中,securityContext 可以应用于 Pod 层级和容器层级,为您的应用程序定义安全设置提供灵活性。

Pod 层级的 SecurityContext

当在 Pod 级别应用时,securityContext 设置将由 Pod 内的所有容器继承。这对于设置应统一应用于 Pod 中所有容器的默认安全配置非常有用:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  securityContext:
    runAsUser: 1000  # All containers in the pod run as user ID 1000
    runAsGroup: 1000 # All containers in the pod run as group ID 1000
  containers:
  - name: app-container
    image: my-app-image
  - name: sidecar-container
    image: my-sidecar-image 

容器级别的 SecurityContext

当在容器级别应用时,securityContext 设置仅影响特定容器。这允许更细粒度的控制,使得同一 Pod 中的不同容器可以拥有不同的安全配置。

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: app-container
    image: my-app-image
    securityContext:
      runAsUser: 1000  # This container runs as user ID 1000
      capabilities:
        add: ["CAP_NET_ADMIN"]  # Grant specific capabilities
  - name: sidecar-container
    image: my-sidecar-image
    securityContext:
      runAsUser: 2000  # This container runs as user ID 2000
      readOnlyRootFilesystem: true  # This container has a read-only root filesystem 

在接下来的部分中,让我们通过一个示例 Pod 来展示 Security Context。

将 Security Context 应用到 Pod

以下示例创建一个 Pod,容器运行时具有 read-only 根文件系统,并指定非 root 用户和组 ID:

# pod-with-security-context.yaml
apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo
spec:
  containers:
    - name: app-container
      image: nginx:latest
      securityContext:
        runAsUser: 1000                # Run container processes as user ID 1000
        runAsGroup: 1000               # Run container processes as group ID 1000
        readOnlyRootFilesystem: true   # Mount the root filesystem as read-only
      volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
  volumes:
    - name: html-volume
      emptyDir: {}                     # Volume to provide writable space 

在前面的 YAML 中,请注意以下几点:

  • runAsUserrunAsGroup:这些设置确保容器以特定的非 root 用户 ID 和组 ID 运行,遵循最小权限原则。

  • readOnlyRootFilesystem:此设置将容器的根文件系统挂载为只读,防止任何意外或恶意修改基础系统。

使用以下 YAML 创建 Pod:

$ kubectl apply -f pod-with-security-context.yaml
pod/security-context-demo created 

一旦 Pod 被创建,让我们在容器内测试一些命令,以验证我们应用的 securityContext:

$ kubectl exec -it security-context-demo -- /bin/sh
~ $ id
uid=1000 gid=1000 groups=1000
~ $ touch /testfile
touch: /testfile: Read-only file system 

你可以看到只读文件系统错误;这是预期的。

参考 kubernetes.io/docs/tasks/configure-pod-container/security-context/ 了解更多关于 Kubernetes 中 securityContext 的信息。

下一部分介绍使用 NetworkPolicy 对象控制 Kubernetes 中的网络流量。你将看到如何直接在 Kubernetes 中构建一种网络防火墙,以防止 Pods 之间的相互访问。

使用 NetworkPolicy 对象保护 Pods

NetworkPolicy 对象是我们在本章中需要探索的最后一种资源类型,它将帮助我们全面了解本章的服务。NetworkPolicy 使你能够直接在集群中定义网络防火墙。

为什么需要 NetworkPolicy?

当你需要在生产环境中管理一个真实的 Kubernetes 工作负载时,你将不得不在其上部署越来越多的应用程序,且这些应用程序之间可能需要进行通信。

实现应用程序之间的通信实际上是微服务架构的一个基本目标。大多数通信将通过网络进行,而网络是你强制想要通过防火墙来保护的内容。

Kubernetes 有自己实现的网络防火墙,称为 NetworkPolicy。假设你希望某个 nginx 资源可以通过特定 IP 地址在端口 80 上访问,并且阻止任何不符合这些要求的其他流量。为此,你需要使用 NetworkPolicy 并将其附加到该 Pod。

NetworkPolicy 带来了以下三个好处:

  • 你可以基于 无类域间路由 (CIDR) 块来构建出口/入口规则。

  • 你可以基于 Pods 的标签和选择器来构建出口/入口规则(就像我们之前在服务和 Pod 关联中看到的那样)。

  • 你可以基于命名空间来构建出口/入口规则(这个概念将在下一章中介绍)。

最后,请记住,要使 NetworkPolicy 生效,你需要一个安装了 CNI 插件的 Kubernetes 集群。CNI 插件通常默认不在 Kubernetes 上安装。如果你是为了学习目的使用 minikube,好的消息是它与 Calico 集成,Calico 是一个支持 NetworkPolicy 的 CNI 插件,开箱即用。你只需要按以下方式重新创建 minikube 集群:

$ minikube start --network-plugin=cni --cni=calico --container-runtime=containerd 

如果你在云平台上使用 Kubernetes,我们建议你阅读云服务提供商的文档,以验证你的云平台提供哪些 CNI 选项以及是否实现了 NetworkPolicy 支持。

理解 Pods 默认不被隔离

默认情况下,在 Kubernetes 中,Pods 不被隔离,任何 Pod 都可以被任何其他 Pod 访问而没有任何约束。

如果你不使用 NetworkPolicy,Pods 将保持原样:任何 Pod 都可以被其他 Pod 访问,且没有任何约束。一旦你将 NetworkPolicy 附加到 Pod 上,NetworkPolicy 中描述的规则将应用于该 Pod。

要在两个与网络策略相关联的 Pods 之间建立通信,双方必须都是开放的。这意味着 Pod A 必须有到 Pod B 的出口规则,而 Pod B 必须有来自 Pod A 的入口规则;否则,流量将被拒绝。下图说明了这一点:

图 7.7 – 其中一个 Pod 被破坏,但服务仍会将流量转发到它

图 18.8:其中一个 Pod 被破坏,但服务仍会将流量转发到它

请记住,你需要进行 NetworkPolicy 的故障排除,因为它可能是许多问题的根源。现在让我们使用标签和选择器配置两个 Pods 之间的 NetworkPolicy。

配置带有标签和选择器的 NetworkPolicy

首先,让我们创建两个 nginx Pods 来演示我们的例子。为了展示隔离,我们将在此例中使用两个不同的命名空间。你将在 第六章,《Kubernetes 中的命名空间、配额和多租户限制》中了解到更多有关 Kubernetes 命名空间的信息。

在命名空间内实现完全的通信隔离可能会很复杂,并产生意想不到的后果。在应用任何限制之前,请仔细评估你的需求和潜在影响。

让我们创建命名空间并创建两个具有不同标签的 Pods,这样它们就可以更容易地与 NetworkPolicy 进行配对。

我们的 web1 命名空间包含 nginx1 pod,将如下创建:

# web1-app.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    project: web1
  name: web1
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx1
  namespace: web1
  labels:
    app: nginx1
spec:
  containers:
    - name: nginx1
      image: nginx 

同时,将创建一个包含 nginx2 Pod 的 web2 命名空间,具体如下:

# web2-app.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    project: web2
  name: web2
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx2
  namespace: web2
  labels:
    app: nginx2
spec:
  containers:
    - name: nginx2
      image: nginx 

在之前的代码示例中,我们使用了命名空间(web1web2),而不是将部署放在 default 命名空间中。

让我们创建资源并验证 Pod,如下所示:

$ kubectl apply -f web-app1.yaml
namespace/web1 created
pod/nginx1 created
$ kubectl apply -f web-app2.yaml
namespace/web2 created
pod/nginx2 created
$ kubectl get po -o wide -n web1
NAME     READY   STATUS    RESTARTS   AGE   IP              NODE       NOMINATED NODE   READINESS GATES
nginx1   1/1     Running   0          3m    10.244.120.71   minikube   <none>           <none>
$ kubectl get po -o wide -n web2
NAME     READY   STATUS    RESTARTS   AGE     IP              NODE       NOMINATED NODE   READINESS GATES
nginx2   1/1     Running   0          2m53s   10.244.120.72   minikube   <none>           <none> 

现在,两个 Pod 已在不同的命名空间内使用不同的标签创建,我们使用 -o wide 标志来获取两个 Pod 的 IP 地址。从 nginx1 Pod 执行 curl 命令以访问 nginx2 Pod,以确认默认情况下允许网络流量,因为此时没有创建任何 NetworkPolicy。代码如下所示;10.244.120.72web2 命名空间中 nginx2 Pod 的 IP 地址:

$ kubectl -n web1 exec nginx1 -- curl 10.244.120.72
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   615  100   615    0     0   698k      0 --:--:-- --:--:-- --:--:--  600k
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...<removed for brevity>... 

正如你所看到的,我们从 nginx2 Pod 正确地收到了 nginx 首页。

现在,让我们显式地阻止所有进入 web2 命名空间的流量。为此,我们可以创建一个默认策略,如下所示:

# default-deny-ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress: [] 

在前面的 YAML 片段中,注意以下几点:

  • podSelector: {}:选择适用 NetworkPolicy 的 Pod。在这种情况下,{} 选择命名空间中的所有 Pod。这意味着 NetworkPolicy 中定义的规则 将应用于命名空间中的所有 Pod,无论它们的标签如何。

  • policyTypes: - Ingress:指定应用的策略类型,这里是 “Ingress”。这意味着 NetworkPolicy 将控制进入(ingress)流量到所选 Pod。

  • ingress: []:定义了 NetworkPolicy 的进入规则列表。在这种情况下,列表为空([]),表示没有定义特定的进入规则。因此,所有进入所选 Pod 的流量将默认被拒绝。

让我们将此拒绝策略应用到 web2 命名空间,以阻止所有进入(ingress)流量,如下所示:

$ kubectl apply -f default-deny-ingress.yaml -n web2
networkpolicy.networking.k8s.io/default-deny-ingress created 

现在,我们将尝试从 nginx1 pod 访问 nginx2 pod,并查看输出:

$ kubectl -n web1 exec nginx1 -- curl 10.244.120.72
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:02:15 --:--:--     0
curl: (28) Failed to connect to 10.244.120.72 port 80 after 135435 ms: Couldn't connect to server
command terminated with exit code 28 

从前面的输出可以清楚地看出,使用 default-deny-ingress NetworkPolicy 资源,web2 命名空间和 Pod 的流量被拒绝。

现在,我们将在 web2 命名空间中为 nginx2 添加 NetworkPolicy,以显式允许来自 web1 命名空间中 nginx1 Pod 的流量。以下是如何使用 YAML 代码:

# allow-from-web1-netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-web1-netpol
  namespace: web2
spec:
  podSelector:
    matchLabels:
      app: nginx2
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              project: web1
        - podSelector:
            matchLabels:
              app: nginx1
      ports:
        - protocol: TCP
          port: 80 

请注意这里的 namespaceSelector.matchLabels,它带有 project: web1 标签,这是我们为此目的显式使用的 web1 命名空间。让我们应用此 NetworkPolicy,如下所示:

$ kubectl apply -f nginx2-networkpolicy.yaml
networkpolicy.networking.k8s.io/nginx2-networkpolicy created 

现在,让我们像之前一样运行相同的 curl 命令,如下所示:

$ kubectl -n web1 exec nginx1 -- curl 10.244.120.72
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   615  100   615    0     0  1280k      0 --:--:-- --:--:-- --:--:--  600k
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...<removed for brevity>... 

正如你所看到的,它像之前一样工作。为什么?原因有两个:

  • nginx2 现在显式地允许来自 web1 命名空间中 nginx1 的端口 80 的进入流量;其他所有流量都被拒绝。

  • nginx1 没有 NetworkPolicy,因此它的所有出口流量都被允许。

请记住,如果 Pod 上没有设置 NetworkPolicy,则会应用默认行为——所有流量对该 Pod 都是允许的。

我们强烈建议您养成与 Pod 一起使用 NetworkPolicy 的习惯。最后,请注意,NetworkPolicy 还可以用来基于 CIDR 块构建防火墙。特别是当您的 Pods 来自集群外部时,这可能会很有用。否则,当您需要在 Pods 之间配置防火墙时,建议继续使用标签和选择器,就像您在服务配置中已经做的那样。

接下来,我们将重点讨论另一个确保 Kubernetes 安全的重要方面,即通过 TLS 证书保障 Kubernetes 组件之间的通信安全。在本节中,我们将讨论 TLS 证书如何帮助保护传输中的数据,并确保 Kubernetes 生态系统中各个组件之间的安全交互。

保障通信安全 – Kubernetes 组件之间的 TLS 证书

在 Kubernetes 中,各个组件之间的安全通信至关重要。传输层安全性TLS)和 安全套接字层SSL)在加密数据传输和建立服务之间的信任方面起着至关重要的作用。

通过实施 带有双向认证的 TLSmTLS),通信中的客户端和服务器都可以使用受信任的 CA 发放的数字证书验证彼此的身份。这增加了一层安全性,防止未经授权的访问,并确保数据完整性。

以下是 TLS 证书在 Kubernetes 中使用的一些示例:

  • API 服务器与 etcd:API 服务器(即中央控制平面组件)与分布式键值存储 etcd 通信,以管理集群状态。使用 mTLS 来保护这些组件之间的通信,防止敏感集群数据被拦截或篡改。

  • Ingress 控制器与服务:Ingress 控制器作为外部流量的单一入口点,将请求路由到后端服务。在 Ingress 控制器与服务之间实施 mTLS 确保只有授权服务能接收到流量,从而降低潜在的安全风险。

  • 内部服务通信:集群内的服务也可以利用 mTLS 进行安全通信。这对于处理敏感数据或需要强认证的服务尤为重要。

  • 服务网格 – 例如 Istio: 这类服务网格具有多种高级流量管理和安全功能,如微服务之间的自动 mTLS。这使得在不必将 TLS 配置嵌入开发人员管理的代码中的情况下,简化了服务间通信的安全过程。

  • 负载均衡器: 部署在负载均衡器后的应用程序也可以使用 TLS 来保护负载均衡器与后端服务之间的通信。在这种配置中,数据将在整个路径中保持加密。

  • 另一种安全机制是启用 Kubernetes 集群中的 IPSec,以加密节点之间的网络流量。这对于保护云环境中的流量或不同数据中心之间的流量可能非常有用。

通过部署带有 mTLS 的 TLS 证书,Kubernetes 管理员显著增强了集群的安全性。这种方法加密了通信路径,验证了通信组件的身份,并减轻了与未经授权的数据访问或篡改相关的风险。

在接下来的章节中,我们将学习如何通过使用特殊的容器(如 gVisor 和 Kata Containers)来启用容器安全性。

容器安全 – gVisor 和 Kata Containers

传统容器与主机操作系统内核共享,且与机器上运行的其他应用程序共享内核。如果容器存在漏洞,可能会允许访问底层系统,进而带来安全风险。gVisorKata Containers 作为替代的容器运行时技术,优先考虑安全性。我们将在接下来的章节中了解它们。

gVisor(客户虚拟机监控器)

gVisor 是一个在用户空间实现的轻量级虚拟机。它作为每个容器的沙箱,将容器与主机内核和其他容器隔离开来。

下图展示了 gVisor 的高层架构。

图 18.9:gVisor 架构

通过为每个容器虚拟化内核功能,gVisor 确保容器漏洞不会直接危及主机系统。它建立了一个强大的隔离边界,即使在容器受到攻击的情况下也能保持隔离。尽管可能会带来更高的资源开销,gVisor 最适用于需要最高级别安全隔离的环境。

Kata Containers

Kata Containers 利用轻量级虚拟机,这些虚拟机类似于传统的虚拟机,但经过优化以适应容器工作负载。Kata Containers 通过将容器隔离在轻量级虚拟机中,提供了一个安全的执行环境。与标准容器相比,这种增强的隔离性加强了安全性,同时保持了性能效率。

下图展示了 Kata Containers 如何与传统容器技术不同。

图 18.10:Kata Containers 与传统容器(来源:katacontainers.io/learn/

当需要在强大安全性与最佳性能之间取得平衡时,特别是对于资源密集型工作负载,推荐使用 Kata Containers。

使用 RuntimeClass 配置安全配置文件

在 Kubernetes 中,利用 pod 规范中的 runtimeClassName 字段来指定容器运行时环境。以下是一个配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  runtimeClassName: kata-containers  # Specifies Kata Containers runtime
  containers:
  - name: my-app
    image: my-secure-image 

此设置指示 Kubernetes 使用 Kata Containers 运行时以增强安全隔离。

我们在本章中学到了 Kubernetes 安全的几个重要内容。在我们结束本章之前,让我们在下一节学习另一个安全主题,即访问私有注册表和容器镜像。

管理 Secrets 和 Registry 凭据

在 Kubernetes 中,注册表凭据对于安全地从需要认证的私有注册表中拉取容器镜像是必需的。没有这些凭据,Kubernetes Pod 无法访问存储在私有仓库中的镜像。安全地管理这些凭据对于确保只有授权的 Pod 可以检索和使用特定容器镜像至关重要。

使用 kubectl create secret docker-registry 简化了 Kubernetes 中容器注册表凭据的管理。它通过在静态情况下加密密钥来确保安全,仅授权的节点可以访问它们。与手动方法相比,这种方法减少了复杂性,减少了错误并提高了操作效率。此外,它与 Kubernetes Pod 规范无缝集成,允许直接配置 imagePullSecrets 以认证 Pod 访问私有容器注册表。

使用 kubectl 创建 Docker 注册表 secret

为了说明,这里是如何创建一个 Docker 注册表 secret 并集成到 Kubernetes Pod 配置中:

$ kubectl create secret docker-registry my-registry-secret \
  --docker-server=your-registry.com \
  --docker-username=your_username \
  --docker-password=your_password \
  --docker-email=your-email@example.com 

用您的实际注册详细信息替换 your-registry.comyour_usernameyour_passwordyour-email@example.com

更新您的 Pod YAML,以使用新创建的 secret 从私有注册表中拉取镜像:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: your-registry.com/your-image:tag
  imagePullSecrets:
  - name: my-registry-secret 

确保 my-registry-secret 与创建 Docker 注册表 secret 时使用的名称匹配。

当 Kubernetes 创建 Pod 时,将使用 imagePullSecrets 作为认证凭据从私有注册表中拉取镜像。

恭喜,您已经完成了这一关于 Kubernetes 安全的长篇章节。

摘要

本章涵盖了 Kubernetes 中的认证授权。首先,我们概述了 Kubernetes 中可用的认证方法,并解释了如何使用 ServiceAccount 令牌进行外部用户认证。接下来,我们专注于 Kubernetes 中的 RBAC。您学习了如何使用 Roles、ClusterRoles、RoleBindings 和 ClusterRoleBindings 来管理集群中的授权。我们通过创建一个 Pod 来演示了 RBAC 在 ServiceAccounts 中的实际用例,该 Pod 可以使用 Kubernetes API 列出集群中的 Pods(遵循最小权限原则)。

之后,我们学习了 Kubernetes 中的 Admission Controllers 以及有哪些控制器可用来保护你的 Kubernetes 集群。我们还学习了 SecurityContext 和不同的 securityContext 配置示例。我们还发现,如何通过使用一个叫做 NetworkPolicy 的对象来控制 Pod 之间的流量,它在集群中就像一个网络防火墙一样工作。作为容器安全的一部分,我们探讨了替代的容器运行时选项,如 Kata Containers 和 gVisor。最后,我们学习了如何为私有容器注册表配置凭证。在下一章中,我们将深入研究 Pod 调度的高级技巧。

进一步阅读

加入我们的社区,讨论区在 Discord 上

加入我们社区的 Discord 频道,与作者和其他读者一起讨论:

packt.link/cloudanddevops

第十九章:Pods 调度的高级技术

在本书的 第二章Kubernetes 架构 – 从容器镜像到运行 Pods 中,我们解释了 Kubernetes 调度器(kube-scheduler)控制平面组件背后的原理及其在集群中的关键作用。简而言之,它的责任是调度容器工作负载(Kubernetes Pods),并将它们分配到满足特定工作负载运行要求的健康节点。

本章将介绍如何控制集群中调度 Pods 的标准。我们将特别关注 Node 亲和性污点容忍度。我们还将深入探讨 调度策略,这为 kube-scheduler 提供了如何优先考虑 Pod 工作负载的灵活性。你会发现这些概念在云规模的生产集群运行中非常重要。

本章将覆盖以下内容:

  • 温故 – 什么是 kube-scheduler?

  • 管理 Node 亲和性

  • 使用 Node 污点和容忍度

  • 了解 Kubernetes 中的静态 Pods

  • Kubernetes 中的扩展调度器配置

技术要求

本章需要以下内容:

  • 需要一个 多节点 Kubernetes 集群。拥有一个多节点集群将使理解 Node 亲和性、污点和容忍度变得更加容易。

  • 本地机器上已安装并配置用于管理 Kubernetes 集群的 Kubernetes CLI (kubectl)。

基本的 Kubernetes 集群部署(本地和云端)以及 kubectl 安装已在 第三章安装你的第一个 Kubernetes 集群 中进行讲解。之前的 第 15、16、17 章为你提供了如何在不同的云平台上部署一个功能完整的 Kubernetes 集群的概述。

你可以从官方 GitHub 仓库下载本章的最新代码示例:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter19

温故 – 什么是 kube-scheduler?

在 Kubernetes 集群中,kube-scheduler 是控制平面中的一个关键组件。该组件的主要职责是调度容器工作负载(Pods),并将它们分配到满足特定工作负载运行要求的健康计算节点(也称为工作节点)上。简而言之,Pod 是一个或多个共享网络和存储的容器组,是 Kubernetes 系统中最小的部署单元。你通常会使用不同的 Kubernetes 控制器,例如 Deployment 对象和 StatefulSet 对象,来管理 Pods,但最终是 kube-scheduler 将创建的 Pods 分配到集群中的特定节点。

对于云中的托管 Kubernetes 集群,例如 Azure Kubernetes ServiceAKS)或 Amazon Elastic Kubernetes ServiceEKS),通常无法访问控制平面或控制节点,因为它们由云服务提供商管理。这意味着你无法直接访问 kube-scheduler 等组件,也无法控制其配置(如调度策略)。然而,你仍然可以控制所有影响 Pod 调度的参数。

kube-scheduler 会定期查询 Kubernetes API Serverkube-apiserver),以列出尚未 调度 的 Pods。在创建时,Pods 会被标记为 调度—这意味着没有节点被选举来运行它们。一个未调度的 Pod 将在 etcd 集群状态中注册,但没有分配任何节点,因此没有运行的 kubelet 会知道这个 Pod。最终,Pod 规范中描述的容器此时不会运行。

在内部,Pod 对象在 etcd 中存储时具有一个名为 nodeName 的属性。顾名思义,这个属性应该包含将要托管该 Pod 的节点的名称。当这个属性被设置时,我们说 Pod 处于 scheduled 状态;否则,Pod 处于 pending 状态。

我们需要找到一种方法来填充这个nodeName值,而这正是 kube-scheduler 的作用。为此,kube-scheduler 会定期轮询 kube-apiserver。它会查找具有空 nodeName 属性的 Pod 资源。一旦找到这样的 Pod,它将执行算法来选举一个节点,并通过向 kube-apiserver 发出请求来更新 Pod 对象中的 nodeName 属性。在为 Pod 选择节点时,kube-scheduler 会考虑其内部的调度策略和你为 Pod 定义的标准。最后,负责在选定节点上运行 Pod 的 kubelet 会注意到该节点的 scheduled 状态下有一个新 Pod,并会尝试启动该 Pod。这些原则在下图中已可视化:

图 19.1:kube-scheduler 和 kube-apiserver 的交互

Pod 的调度过程分为两个阶段:

  • 过滤:kube-scheduler 确定能够运行给定 Pod 的节点集合。这包括检查节点的实际状态,并验证 Pod 定义中指定的资源需求和标准。在此阶段,如果没有节点能够运行给定的 Pod,Pod 将无法调度,保持在待处理状态。

  • 评分:kube-scheduler 根据一组 调度策略 为每个节点分配分数。然后,调度器将 Pod 分配给得分最高的节点。我们将在本章后面的部分讨论调度策略。

kube-scheduler 将考虑你可以选择传递到 Pod 规格中的标准和配置值。通过使用这些配置项,你可以精确控制 kube-scheduler 如何为 Pod 选择节点。为了控制 Pod 运行的位置,你可以设置约束,将其限制在特定节点上,或指明优先选择的节点。我们已经了解到,通常情况下,Kubernetes 会有效地处理 Pod 的分配,不需要任何手动约束,确保 Pods 在节点间分布,以避免资源短缺。然而,有时你可能需要影响 Pod 的位置,比如确保 Pod 运行在带有 SSD 的节点上,或将频繁通信的 Pods 安排在同一可用区内。

kube-scheduler 的决策只在 Pod 被调度的时刻有效。一旦 Pod 被调度并运行,kube-scheduler 就不会在 Pod 运行期间进行任何重新调度操作(这个过程可能持续数天甚至数月)。因此,即使 Pod 不再符合你的规则与节点匹配,它仍然会继续运行。只有当 Pod 被终止,并且需要调度新 Pod 时,才会进行重新调度。

你可以使用以下方法来影响 Kubernetes 中的 Pod 调度:

  • 使用 nodeSelector 字段来匹配节点标签。

  • 设置亲和性和反亲和性规则。

  • 指定 nodeName 字段。

  • 定义 Pod 拓扑分布约束。

  • 污点和容忍。

在接下来的章节中,我们将讨论这些配置项以控制 Pods 的调度。在开始实际操作之前,请确保你有一个多节点的 Kubernetes 集群,以体验节点调度场景。

在我们的案例中,我们正在使用一个多节点的 Kubernetes 集群,并通过 minikube 进行部署,如下所示(你可以将驱动程序更改为 kvm2、Docker 或 Podman):

$ minikube start \
  --driver=virtualbox \
  --nodes 3 \
  --cni calico \
  --cpus=2 \
  --memory=2g \
  --kubernetes-version=v1.30.0 \
  --container-runtime=containerd 

--nodes=3 参数将触发 minikube 部署一个 Kubernetes 集群,第一个节点作为控制节点(或主节点),第二个和第三个节点作为计算节点(或工作节点),如下所示:

$ kubectl get nodes
NAME           STATUS   ROLES           AGE     VERSION
minikube       Ready    control-plane   3m34s   v1.30.0
minikube-m02   Ready    <none>          2m34s   v1.30.0
minikube-m03   Ready    <none>          87s     v1.30.0 

如果你正在使用其他 Kubernetes 集群进行学习,可以跳过此 minikube 集群的设置。

现在,让我们来看看节点亲和性,以及节点名称和节点选择器。

管理节点亲和性

要了解 节点亲和性 在 Kubernetes 中是如何工作的,我们首先需要看一下最基本的调度选项,这些选项使用 节点名称节点选择器 来调度 Pods。

使用 nodeName 来为 Pods 分配节点

如前所述,每个 Pod 对象都有一个 nodeName 字段,通常由 kube-scheduler 控制。然而,在你创建 Pod 时,或者创建使用 Pod 模板的控制器时,可以直接在 YAML 清单中设置该属性。这是将 Pod 静态调度到给定节点的最简单形式,通常不推荐使用——它不够灵活,也无法扩展。节点的名称可能随时间变化,而且你有可能在该节点上耗尽资源。

在调试场景中,当你想在特定节点上运行 Pod 时,明确设置 nodeName 可能会很有用。

我们将基于 第十一章 中介绍的示例 Deployment 对象演示所有调度原理,使用 Kubernetes Deployment 管理无状态工作负载。这是一个简单的 Deployment,管理着一个 nginx Web 服务器的 五个 Pod 副本。

在我们在 Deployment 清单中使用 nodeName 之前,我们需要知道集群中有哪些节点,以便理解它们是如何调度的,以及我们如何能影响 Pod 的调度。你可以使用 kubectl get nodes 命令获取节点列表,如下所示:

$ kubectl get nodes
NAME           STATUS   ROLES           AGE     VERSION
minikube       Ready    control-plane   3m34s   v1.30.0
minikube-m02   Ready    <none>          2m34s   v1.30.0
minikube-m03   Ready    <none>          87s     v1.30.0 

在我们的示例中,我们运行的是一个三节点集群(稍后记得在清单中引用你正确的集群节点名称)。为了简化,我们将 minikube 称为 Node1minikube-m02 称为 Node2minikube-m02 称为 Node3

在这个演示中,我们希望将所有五个 nginx Pods 调度到 minikube-m02。创建以下 YAML 清单,并命名为 n01_nodename/nginx-deployment.yaml

# 01_nodename/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
      environment: test
  template:
    metadata:
      labels:
        app: nginx
        environment: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80 

像往常一样应用 Deployment YAML:

$ kubectl apply -f 01_nodename/nginx-deployment.yaml
deployment.apps/nginx-app created 

Deployment 对象将创建五个 Pod 副本。使用 kubectl get pods -o wide 来查看 Pod 和节点名称。我们将使用如下的自定义输出:

$ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
NAME                         STATUS    NODE
nginx-app-7b547cfd87-4g9qx   Running   minikube
nginx-app-7b547cfd87-m76l2   Running   minikube-m02
nginx-app-7b547cfd87-mjf78   Running   minikube-m03
nginx-app-7b547cfd87-vvrgk   Running   minikube-m02
nginx-app-7b547cfd87-w7jcw   Running   minikube-m03 

如你所见,默认情况下,Pods 已经均匀分配——Node1 收到了一个 Pod,Node2 收到了两个 Pods,Node3 收到了两个 Pods。这是 kube-scheduler 中启用的默认调度策略进行过滤和评分的结果。

如果你正在运行一个 非托管 的 Kubernetes 集群,你可以使用 kubectl logs 命令检查 kube-scheduler Pod 的日志,或者直接在控制平面节点中的 /var/log/kube-scheduler.log 查看。这可能还需要增加 kube-scheduler 进程的日志详细级别。你可以在 kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/ 阅读更多信息。

此时,.spec.template.spec 中的 Pod 模板并不包含任何影响 Pod 副本调度的配置。

我们现在将通过在 Pod 模板中使用 nodeName 字段,强制将 Deployment 中的所有 Pod 分配到 Node2(在我们的例子中是 minikube-m02)。请更改 nginx-deployment.yaml YAML 清单,将该属性设置为 集群的正确节点名称:

# 01_nodename/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  ...
  template:
  ...
    spec:
      **nodeName:****minikube-m02**
...<removed for brevity>... 

注意这一行 nodeName: minikube-m02;我们明确指出 minikube-m02 应该作为部署 nginx Pods 的节点。

使用 kubectl apply -f ./nginx-deployment.yaml 命令将清单应用到集群中:

$ kubectl apply -f 01_nodename/nginx-deployment.yaml
deployment.apps/nginx-app created 

现在,再次检查 Pod 状态和节点分配情况:

$ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
NAME                         STATUS    NODE
nginx-app-85b577894f-8tqj7   Running   minikube-m02
nginx-app-85b577894f-9c6hd   Running   minikube-m02
nginx-app-85b577894f-fldxx   Running   minikube-m02
nginx-app-85b577894f-jrnjc   Running   minikube-m02
nginx-app-85b577894f-vs7c5   Running   minikube-m02 

正如预期的那样,所有五个 Pod 现在都运行在 Node2(minikube-m02)上。这些都是新的 Pod——当你在 Deployment 规范中更改 Pod 模板时,它会导致一个新的 ReplicaSet 对象的内部回滚,而旧的 ReplicaSet 对象则会缩小,如 第十一章 中所述,使用 Kubernetes Deployments 管理无状态工作负载

通过这种方式,我们实际上已经 绕过kube-scheduler。如果你使用 kubectl describe pod 命令检查其中一个 Pod 的事件,你会看到它缺少任何带有 Scheduled 作为原因的事件。

接下来,我们将看看另一种基本的 Pod 调度方法,即 nodeSelector

使用 nodeSelector 为 Pods 调度

Pod 规范有一个特殊字段 .spec.nodeSelector,该字段使你能够仅在具有特定标签值的节点上调度 Pod。这个概念类似于 标签选择器 用于 Deployments 和 StatefulSets,但不同之处在于,它仅允许对标签进行简单的 等式比较。你不能进行复杂的 基于集合 的逻辑操作。

这在以下情况下特别有用:

  • 混合集群:通过将操作系统指定为调度标准,确保 Windows 容器在 Windows 节点上运行,而 Linux 容器在 Linux 节点上运行。

  • 资源分配:将 Pods 定向到具有特定资源(CPU、内存、存储)的节点,以优化资源利用率。

  • 硬件要求:仅在具有相关能力的节点上调度需要特殊硬件(例如 GPU)的 Pod。

  • 安全区域:通过标签定义安全区域,并使用nodeSelector限制 Pods 只能在特定区域内运行,以增强安全性。

每个 Kubernetes 节点默认都带有一组标签,包括以下内容:

  • kubernetes.io/arch:描述节点的处理器架构,例如 amd64arm。这也被定义为 beta.kubernetes.io/arch

  • kubernetes.io/os:该标签的值为 linuxWindows。这也被定义为 beta.kubernetes.io/os

  • node-role.kubernetes.io/control-plane:节点在 Kubernetes 集群中的角色。

如果你检查其中一个节点的标签,你会看到有很多标签。在我们的例子中,其中一些是特定于minikube集群的:

$ kubectl describe nodes minikube
Name:               minikube
Roles:              control-plane
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=minikube
                    kubernetes.io/os=linux
                    minikube.k8s.io/commit=5883c09216182566a63dff4c326a6fc9ed2982ff
                    minikube.k8s.io/name=minikube
                    minikube.k8s.io/primary=true
                    minikube.k8s.io/updated_at=2024_07_21T16_40_25_0700
                    minikube.k8s.io/version=v1.33.1
                    node-role.kubernetes.io/control-plane=
                    node.kubernetes.io/exclude-from-external-load-balancers=
...<removed for brevity>... 

当然,你可以为节点定义 自己的 标签,并使用这些标签来控制调度。请注意,通常你应该在 Kubernetes 中使用语义标签来标识资源,而不是仅仅为了调度的目的给资源添加特殊标签。让我们通过以下步骤演示如何操作:

  1. 使用 kubectl label nodes 命令,将一个 node-type 标签(值为 superfast)添加到集群中的 Node 1Node 2

    $ kubectl label nodes minikube-m02 node-type=superfast
    node/minikube-m02 labeled
    $ kubectl label nodes minikube-m03 node-type=superfast
    node/minikube-m03 labeled 
    
  2. 按照以下方式验证节点标签:

    $ kubectl get nodes --show-labels |grep superfast
    minikube-m02   Ready    <none>          2m59s   v1.31.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=minikube-m02,kubernetes.io/os=linux,minikube.k8s.io/commit=5883c09216182566a63dff4c326a6fc9ed2982ff,minikube.k8s.io/name=minikube,minikube.k8s.io/primary=false,minikube.k8s.io/updated_at=2024_10_13T15_26_06_0700,minikube.k8s.io/version=v1.33.1,**node-type=superfast**
    minikube-m03   Ready    <none>          2m30s   v1.31.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux,minikube.k8s.io/commit=5883c09216182566a63dff4c326a6fc9ed2982ff,minikube.k8s.io/
    name=minikube,minikube.k8s.io/primary=false,minikube.k8s.io/updated_at=2024_10_13T15_26_34_0700,minikube.k8s.io/version=v1.33.1,**node-type=superfast** 
    
  3. 编辑 ./nginx-deployment.yaml 部署清单(或创建另一个名为 02_nodeselector/nginx-deployment.yaml 的清单),使 Pod 模板中的 nodeSelector 设置为 node-type: superfast,如下所示:

    # 02_nodeselector/nginx-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-app
    spec:
      ...
      template:
        ...
        spec:
          nodeSelector:
            **node-type:****superfast**
    ...<removed for brevity> 
    
  4. 使用 kubectl apply -f 02_nodeselector/nginx-deployment.yaml 命令将清单应用到集群中,然后再次检查 Pod 状态和节点分配。您可能需要等一段时间,直到 Deployment 完成滚动更新:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                        STATUS    NODE
    nginx-app-6c5b8b758-2dcsc   Running   minikube-m02
    nginx-app-6c5b8b758-48c5t   Running   minikube-m03
    nginx-app-6c5b8b758-pfmvg   Running   minikube-m03
    nginx-app-6c5b8b758-v6rhj   Running   minikube-m02
    nginx-app-6c5b8b758-zqvqm   Running   minikube-m02 
    

如前面的输出所示,Pod 现在已分配给 minikube-m02minikube-m03minikube-m02 已分配了三个 Pod,minikube-m03 分配了两个 Pod)。这些 Pod 已经分布在具有 node-type=superfast 标签的节点上。

  1. 相反,如果您修改 ./nginx-deployment.yaml 清单,使 Pod 模板中的 nodeSelector 设置为 node-type: slow,而集群中没有节点分配此标签,我们将看到 Pod 无法调度,Deployment 会被卡住。编辑清单(或将其复制到一个名为 02_nodeselector/nginx-deployment-slow.yaml 的新文件中):

    # 02_nodeselector/nginx-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-app
    spec:
      ...
      template:
        ...
        spec:
          nodeSelector:
            node-type: slow
    ...<removed for brevity> 
    
  2. 以如下方式将清单应用到集群中:

    $ kubectl apply -f  02_nodeselector/nginx-deployment-slow.yaml 
    

再次检查 Pod 状态和节点分配:

$ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
NAME                        STATUS    NODE
nginx-app-6c5b8b758-48c5t   Running   minikube-m03
nginx-app-6c5b8b758-pfmvg   Running   minikube-m03
nginx-app-6c5b8b758-v6rhj   Running   minikube-m02
nginx-app-6c5b8b758-zqvqm   Running   minikube-m02
nginx-app-9cc8544f4-7dwcd   Pending   <none>
nginx-app-9cc8544f4-cz947   Pending   <none>
nginx-app-9cc8544f4-lfqqj   Pending   <none> 

三个新 Pod 正处于挂起状态,四个旧 Pod 仍在运行的原因是 Deployment 对象中滚动更新的默认配置。默认情况下,maxSurge 设置为 Pod 副本的 25%(绝对数量会向上取整),因此在我们的情况下,允许创建超过五个 Pod 所需数量的两个 Pod。总的来说,现在我们有七个 Pod。同时,maxUnavailable 也是 Pod 副本的 25%(但绝对数量会向下取整),所以在我们的情况下,五个 Pod 中有一个 Pod 是不可用的。换句话说,四个 Pod 必须是 Running 状态。而且,由于新创建的 Pending 状态 Pod 在调度过程中无法获取节点,Deployment 被卡住,无法继续进行。通常,在这种情况下,您需要要么将 Deployment 回滚到之前的版本,要么将 nodeSelector 更改为与现有节点匹配的值。当然,您也可以选择添加一个带有匹配标签的新节点,或者向现有节点添加缺失的标签,而不进行回滚。

我们现在将继续讨论 Pod 调度的话题,首先介绍一些更高级的技术:Node 亲和性

使用 Pod 的 nodeAffinity 配置

Node 亲和性的概念扩展了 nodeSelector 方法,并提供了一个更丰富的语言来定义哪些节点是 Pod 更倾向或避免的。在日常生活中,“affinity” 一词的定义是“对某人或某事的自然喜好和理解”,这正好描述了 Node 亲和性对 Pod 的目的。也就是说,您可以控制 Pod 将被吸引到哪些节点,或者哪些节点会被排斥

通过使用 Node 亲和性,表示在 Pod 的 .spec.affinity.nodeAffinity 中,您可以获得比简单的 nodeSelector 更强大的功能:

  • 您将获得一个更丰富的语言来表达匹配 Pod 到节点的规则。例如,您可以使用 InNotInExistsDoesNotExistGtLt 操作符来处理标签。

  • nodeAffinity类似,也可以使用Pod 间亲和性(podAffinity)以及额外的反亲和性podAntiAffinity)进行调度。反亲和性与亲和性具有相反的效果——你可以定义排斥 Pod 的规则。通过这种方式,你可以让 Pod 被吸引到已经运行某些 Pod 的节点上。如果你想将 Pod 协同调度以降低延迟,这尤其有用。

  • 可以定义软性亲和性和反亲和性规则,表示偏好,而不是硬性规则。换句话说,即使调度器无法匹配软性规则,它仍然可以调度 Pod。软性规则在规范中由preferredDuringSchedulingIgnoredDuringExecution字段表示,而硬性规则由requiredDuringSchedulingIgnoredDuringExecution字段表示。

  • 软性规则可以加权,并且可以添加多个不同权重值的规则。调度器将考虑这些权重值以及其他参数来做出亲和性决策。

尽管规范中没有提供一个专门的字段用于节点反亲和性,与 Pod 间反亲和性类似,你仍然可以通过使用NotInDoesNotExist操作符来实现类似效果。通过这种方式,你可以让 Pod 从带有特定标签的节点上被排斥,同样以软性方式进行。有关更多信息,请参见文档:kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity

定义节点亲和性和 Pod 间亲和性/反亲和性规则的使用场景和案例是无限的。只要节点上有足够的标签,你就可以通过这种方式表达各种要求。例如,你可以建模类似的需求:仅将 Pod 调度到带有 Intel CPU 和高端存储的 Windows 节点上,并且位于西欧区域,但当前没有运行 MySQL 的 Pod,或者尽量不将 Pod 调度到可用区 1,但如果无法避免,那么可用区 1 仍然可以接受。

为了演示节点亲和性,我们将尝试为我们的部署建模以下需求:“尽量将 Pod 调度到带有node-type标签且值为fastsuperfast的节点上,但如果无法满足这个条件,可以使用任何节点,但严格避免使用node-type标签值为extremelyslow的节点。”为此,我们需要使用:

  • 软性节点亲和性规则,类型为preferredDuringSchedulingIgnoredDuringExecution,用于匹配fastsuperfast节点。

  • 硬性节点亲和性规则,类型为requiredDuringSchedulingIgnoredDuringExecution,用于严格排除带有node-type标签且值为extremelyslow的节点。我们需要使用NotIn操作符来实现反亲和性效果。

在我们的集群中,我们将首先为节点定义以下标签:

  • Node1: slow

  • Node2: fast

  • Node3superfast

如你所见,根据我们的要求,Deployment Pods 应该被调度到 Node2 和 Node3,除非有某些因素阻止它们被分配到那里,比如缺乏 CPU 或内存资源。在这种情况下,Node1 也会被允许,因为我们使用了软性亲和性规则。

接下来,我们将按照以下方式重新标记节点:

  • Node1slow

  • Node2extremelyslow

  • Node3extremelyslow

接下来,我们需要重新部署我们的 Deployment(例如,将其缩放为零,然后恢复到原始副本数,或者使用kubectl rollout restart命令),以便重新调度 Pods。之后,根据我们的要求,kube-scheduler 应该将所有 Pods 调度到 Node1(因为它仍然符合软性规则),但无论如何都要避免Node2 和 Node3。如果 Node1 没有资源来运行 Pod,那么 Pods 将处于Pending状态。

为了解决已经运行的 Pods 重新调度的问题(换句话说,让 kube-scheduler 重新考虑它们),有一个正在孵化的 Kubernetes 项目叫做Descheduler。你可以在这里了解更多信息:github.com/kubernetes-sigs/descheduler

为了进行演示,请按照以下步骤操作:

  1. 使用kubectl label nodes命令为 Node1 添加一个node-type标签,值为slow,为 Node2 添加fast值标签,为 Node3 添加superfast值标签:

    $ kubectl label nodes --overwrite minikube node-type=slow
    node/minikube labeled
    $ kubectl label nodes --overwrite minikube-m02 node-type=fast
    node/minikube-m02 labeled
    $ kubectl label nodes --overwrite minikube-m03 node-type=superfast
    node/minikube-m03 not labeled
    # Note that this label was already present with this value 
    
  2. 编辑03_affinity/nginx-deployment.yaml Deployment 清单,并按如下方式定义软性节点亲和性规则:

    # 03_affinity/nginx-deployment.yaml
    ...
    spec:
      ...
      template:
        ...
        spec:
          affinity:
            nodeAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                nodeSelectorTerms:
                - matchExpressions:
                  - key: node-type
                    operator: NotIn
                    values:
                    - extremelyslow
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 1
                preference:
                  matchExpressions:
                  - key: node-type
                    operator: In
                    values:
                    - fast
                    - superfast
    ...<removed for brevity>... 
    

如你所见,我们使用了nodeAffinity(而不是podAffinitypodAntiAffinity),并设置了preferredDuringSchedulingIgnoredDuringExecution,这样它只有一个软性规则:node-type应该具有fast值或superfast值。这意味着,如果这些节点上没有资源,它们仍然可以调度到其他节点。此外,我们在requiredDuringSchedulingIgnoredDuringExecution中指定了一个硬性反亲和性规则,要求node-type不能是extremelyslow。你可以在官方文档中找到 Pod 的.spec.affinity的完整规范:kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity

  1. 使用kubectl apply -f 03_affinity/nginx-deployment.yaml命令将清单应用到集群中,然后再次检查 Pod 的状态和节点分配情况。你可能需要等待一段时间,直到部署完成:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-7766c596cc-4d4sl   Running   minikube-m02
    nginx-app-7766c596cc-4h6k6   Running   minikube-m03
    nginx-app-7766c596cc-ksld5   Running   minikube-m03
    nginx-app-7766c596cc-nw9hx   Running   minikube-m02
    nginx-app-7766c596cc-tmwhm   Running   minikube-m03 
    

我们的节点亲和性规则被定义为偏好具有node-typefastsuperfast的节点,实际上 Pods 只会调度到 Node2 和 Node3。

现在,我们将进行一次实验,演示节点亲和性的软性部分是如何与节点反亲和性的硬性部分协同工作的。我们将按照引言中描述的方式重新标记节点,重新部署 Deployment,并观察发生的变化。请按照以下步骤操作:

  1. 使用 kubectl label nodes 命令为 Node 0 添加 node-type 标签,并将其值设置为 slow,为 Node1 设置值为 extremelyslow,为 Node2 设置值为 extremelyslow

    $ kubectl label nodes --overwrite minikube node-type=slow
    node/minikube not labeled
    # Note that this label was already present with this value
    $ kubectl label nodes --overwrite minikube-m02 node-type=extremelyslow
    node/minikube-m02 labeled
    $ kubectl label nodes --overwrite minikube-m03 node-type=extremelyslow
    node/minikube-m03 labeled 
    
  2. 此时,如果你使用 kubectl get pods 检查 Pod 分配情况,结果将没有变化。因为正如我们之前解释的,Pod 的节点分配只在调度时有效,之后除非 Pod 被重启,否则不会更改。为了强制重启 Pods,我们可以将 Deployment 缩放到零副本,然后再恢复到五个副本。但有一种更简单的方法,即使用 kubectl rollout restart 命令。这种方法的好处是不会使 Deployment 不可用,并且会进行滚动重启,而不会减少可用 Pod 的数量。执行以下命令:

    $ kubectl rollout restart deployment nginx-app
    deployment.apps/nginx-app restarted 
    
  3. 再次检查 Pod 状态和节点分配。你可能需要等待一段时间,直到部署完成:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-7d8c65464c-5d9cc   Running   minikube
    nginx-app-7d8c65464c-b97g8   Running   minikube
    nginx-app-7d8c65464c-cqwh5   Running   minikube
    nginx-app-7d8c65464c-kh8bm   Running   minikube
    nginx-app-7d8c65464c-xhpss   Running   minikube 
    

输出结果显示,如预期所示,所有 Pod 都已调度到标记为 node-type=slow 的 Node1 上。如果没有更好的节点可用,我们允许 Pod 调度到这样的节点,而此时,Node2 和 Node3 被标记为 node-type=extremelyslow,这被硬性节点反亲和性规则禁止。

为了实现更高的粒度和对 Pod 调度的控制,你可以使用 Pod 拓扑分布约束。更多细节请参考官方文档:kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/

恭喜,你已经成功为我们的部署 Pod 配置了节点亲和性!接下来,我们将探索另一种调度 Pod 的方法——污点和容忍

使用节点污点和容忍

使用节点和 Pod 之间的亲和性机制调度 Pods 非常强大,但有时你需要一种更简单的方式来指定哪些节点应该 排斥 Pods。Kubernetes 提供了两个稍微旧一些、但更简单的功能来实现这一点——污点容忍。你可以对某个节点应用污点(描述某种限制),而 Pod 必须定义一个特定的容忍才可以调度到这个被污点标记的节点上。如果 Pod 有容忍,并不意味着该节点上必须存在污点。污点的定义是“有不良或不希望出现的物质或特质的痕迹”,这个定义很符合我们的想法——如果节点上有污点,所有 Pod 都会 避开 这个节点,但我们可以指示 Pod 容忍 特定的污点。

如果仔细观察污点和容忍度的描述,你会发现,你可以通过节点标签和节点的硬性和软性亲和性规则(使用NotIn操作符)实现类似的效果。有一个例外——你可以定义一个NoExecute效果的污点,如果 Pod 无法容忍这个污点,Pod 将会被终止。除非手动重启 Pod,否则你不能通过亲和性规则实现类似的效果。

节点的污点结构如下:<key>=<value>:<effect>keyvalue标识 了污点,可以用于更精细的容忍度定义,例如,容忍所有具有给定 key 和任意 value 的污点。这类似于标签,但请记住,污点是独立的属性,定义污点不会影响节点标签。在我们的示例中,我们将使用machine-check-exception作为 key,memory作为 value。这当然是一个理论示例,表示主机的内存存在硬件问题,但你也可以使用相同的 key,而 value 可以是cpudisk

一般来说,你的污点应当 语义上 标明节点遇到的具体问题。虽然没有任何限制禁止你使用任意的 key 和 value 来创建污点,但如果它们具有语义意义,那么管理这些污点和定义容忍度会更容易。

污点可能会产生不同的效果:

  • NoSchedule – kube-scheduler 将不会调度 Pods 到此节点。通过使用硬性节点亲和性规则也可以实现类似的行为。

  • PreferNoSchedule – kube-scheduler 将尽量不调度 Pods 到这个节点。通过使用软性节点亲和性规则也可以实现类似的行为。

  • NoExecute – kube-scheduler 将不会调度 Pods 到此节点,并且会 驱逐(终止并重新调度)已在此节点上运行的 Pods。你不能通过节点亲和性规则实现类似的行为。注意,当你为 Pod 定义这种类型的污点的容忍度时,你可以控制 Pod 容忍该污点的时间长度,直到被驱逐,这可以通过tolerationSeconds来设置。

Kubernetes 通过监控节点主机自动管理许多NoExecute类型的污点。以下污点是由NodeControllerkubelet内置并管理的:

  • node.kubernetes.io/not-ready:当节点条件Ready的状态为false时会添加此污点。

  • node.kubernetes.io/unreachable:当节点条件Ready的状态为Unknown时会添加此污点。这种情况发生在NodeController无法访问节点时。

  • node.kubernetes.io/memory-pressure:节点正在经历内存压力。

  • node.kubernetes.io/disk-pressure:节点正在经历磁盘压力。

  • node.kubernetes.io/network-unavailable:节点的网络当前不可用。

  • node.kubernetes.io/unschedulable:节点当前处于unschedulable状态。

  • node.cloudprovider.kubernetes.io/uninitialized:用于由外部云提供商准备的节点。当节点被cloud-controller-manager初始化时,这个污点会被移除。

要在节点上添加污点,您可以使用以下方式的kubectl taint node命令:

$ kubectl taint node <nodeName> <key>=<value>:<effect> 

所以,例如,如果我们想对 Node1 使用machine-check-exception作为 key,memory作为 value,并设置NoExecute效果,可以使用以下命令:

$ kubectl taint node minikube machine-check-exception=memory:NoExecute
node/minikube tainted 

要移除相同的污点,您需要使用以下命令(请记住污点定义末尾的-字符):

$ kubectl taint node minikube machine-check-exception=memory:NoExecute-
node/minikube untainted 

您还可以删除具有指定key的所有污点:

$ kubectl taint node minikube machine-check-exception:NoExecute- 

为了抵消污点对特定 Pods 的影响,您可以在它们的规格中定义宽容度。换句话说,您可以使用宽容度忽略污点,并将 Pod 调度到这些节点上。如果一个节点上有多个污点,Pod 必须容忍所有这些污点。宽容度在 Pod 规格的.spec.tolerations中定义,并具有以下结构:

tolerations:
- key: <key>
  operator: <operatorType>
  value: <value>
  effect: <effect> 

操作符可以是EqualExistsEqual意味着污点的keyvalue必须完全匹配,而Exists则意味着只需匹配keyvalue不做考虑。在我们的示例中,如果我们想忽略污点,则宽容度将需要如下所示:

tolerations:
- key: machine-check-exception
  operator: Equal
  value: memory
  effect: NoExecute 

请注意,您可以为 Pod 定义多个宽容度,以确保 Pod 正确地放置。

对于NoExecute宽容度,您可以定义一个额外的字段tolerationSeconds,它指定 Pod 将容忍污点多长时间,直到被驱逐。因此,这是一种在超时后对污点进行部分宽容的方式。请注意,如果使用NoExecute污点,通常还需要添加NoSchedule污点。通过这种方式,您可以防止当 Pod 具有NoExecute宽容度并设置tolerationSeconds时发生驱逐循环。这是因为污点在指定的秒数内不起作用,这也包括阻止 Pod 被调度到带污点的节点。

当 Pods 在集群中创建时,Kubernetes 会自动为node.kubernetes.io/not-readynode.kubernetes.io/unreachable添加两个Exists宽容度,tolerationSeconds设置为300

现在我们已经了解了污点和宽容度,我们将通过一些演示将这些知识付诸实践。请按照以下步骤进行污点和宽容度练习:

  1. 如果您在前一个部分中定义了带有节点亲和性的nginx-app部署仍在运行,它当前会将所有 Pods 运行在 Node1(minikube)上。节点亲和性规则的构造方式使得 Pods 无法调度到 Node2 和 Node3 上。让我们看看如果您将 Node1 带上machine-check-exception=memory:NoExecute污点时会发生什么:

    $ kubectl taint node minikube machine-check-exception=memory:NoExecute
    node/minikube tainted 
    
  2. 检查 Pod 状态和节点分配:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-7d8c65464c-5j69n   Pending   <none>
    nginx-app-7d8c65464c-c8j58   Pending   <none>
    nginx-app-7d8c65464c-cnczc   Pending   <none>
    nginx-app-7d8c65464c-drpdh   Pending   <none>
    nginx-app-7d8c65464c-xss9b   Pending   <none> 
    

所有部署的 Pod 现在都处于 Pending 状态,因为 kube-scheduler 无法找到一个可以运行它们的 Node。

  1. 编辑 ./nginx-deployment.yaml 部署清单(或者检查 04_taints/nginx-deployment.yaml)并移除 affinity。取而代之的是,定义对 machine-check-exception=memory:NoExecute 的污点容忍,超时时间为 60 秒,如下所示:

    # 04_taints/nginx-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-app
    spec:
      ...
      template:
        ...
        spec:
          tolerations:
            - key: machine-check-exception
              operator: Equal
              value: memory
              effect: NoExecute
              tolerationSeconds: 60
    ...<removed for brevity>... 
    

当这个清单应用到集群时,阻止调度到 Node2 和 Node3 的旧 Node 亲和规则将被移除。Pod 将能够在 Node2 和 Node3 上调度,但 Node1 有一个污点 machine-check-exception=memory:NoExecute。因此,Pod 应该被调度到 Node0,因为 NoExecute 意味着 NoSchedule对吗?让我们来检查一下。

  1. 使用 kubectl apply -f 04_taints/nginx-deployment.yaml 命令将清单应用到集群,并再次检查 Pod 状态和 Node 分配。你可能需要等一段时间,直到部署完成:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-84d755f746-4zkjd   Running   minikube
    nginx-app-84d755f746-58qmh   Running   minikube-m02
    nginx-app-84d755f746-5h5vk   Running   minikube-m03
    nginx-app-84d755f746-psmgf   Running   minikube-m02
    nginx-app-84d755f746-zkbc6   Running   minikube-m03 
    

这个结果可能有点令人惊讶。正如你所看到的,我们将 Pod 调度到了 Node2 和 Node3,但同时 Node1 也接收到了 Pod,并且它们每 60 秒就会进入驱逐循环!解释是,NoExecute 污点的 tolerationSeconds 表示该污点会被忽略 60 秒。因此,kube-scheduler 可以将 Pod 调度到 Node1,即使它会被稍后驱逐。

  1. 让我们通过应用一个建议来修复这种行为:每当使用 NoExecute 污点时,使用一个 NoSchedule 污点。这样,驱逐的 Pod 就没有机会再次调度到被污点标记的 Node,除非它们也开始容忍这种类型的污点。执行以下命令来给 Node0 添加污点:

    $ kubectl taint node minikube machine-check-exception=memory:NoSchedule
    node/minikube tainted 
    
  2. 再次检查 Pod 状态和 Node 分配:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-84d755f746-58qmh   Running   minikube-m02
    nginx-app-84d755f746-5h5vk   Running   minikube-m03
    nginx-app-84d755f746-psmgf   Running   minikube-m02
    nginx-app-84d755f746-sm2cm   Running   minikube-m03
    nginx-app-84d755f746-zkbc6   Running   minikube-m03 
    

在输出中,你可以看到 Pod 现在在 Node2 和 Node3 之间分布——完全符合我们的预期。

  1. 现在,移除 Node1 上的两个污点:

    $ kubectl taint node minikube machine-check-exception-
    node/minikube untainted 
    
  2. 使用以下命令重启部署以重新调度 Pod:

    $ kubectl rollout restart deployment nginx-app
    deployment.apps/nginx-app restarted 
    
  3. 再次检查 Pod 状态和 Node 分配:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-5bdd957558-fj7bk   Running   minikube-m02
    nginx-app-5bdd957558-mrddn   Running   minikube-m03
    nginx-app-5bdd957558-mz2pz   Running   minikube-m02
    nginx-app-5bdd957558-pftz5   Running   minikube-m03
    nginx-app-5bdd957558-vm6k9   Running   minikube 
    

Pod 已经在所有三个 Node 上均匀分布。

  1. 最后,让我们看看 NoExecuteNoSchedule 污点的组合是如何工作的,NoExecutetolerationSeconds 设置为 60。再次对 Node1 应用两个污点:

    $ kubectl taint node minikube machine-check-exception=memory:NoSchedule
    node/minikube tainted
    $ kubectl taint node minikube machine-check-exception=memory:NoExecute
    node/minikube tainted 
    
  2. 紧接着,开始监视 Pod 及其 Node 分配。一开始,你会看到 Pod 在 Node1 上运行了一段时间。但在 60 秒后,你会看到:

    $ kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"
    NAME                         STATUS    NODE
    nginx-app-5bdd957558-7n42p   Running   minikube-m03
    nginx-app-5bdd957558-fj7bk   Running   minikube-m02
    nginx-app-5bdd957558-mrddn   Running   minikube-m03
    nginx-app-5bdd957558-mz2pz   Running   minikube-m02
    nginx-app-5bdd957558-pftz5   Running   minikube-m03 
    

正如我们预期的那样,Pod 在 60 秒后被驱逐,并且没有出现驱逐调度循环。

这演示了污点的一个更高级的使用场景,是你无法轻易通过 Node 亲和规则替代的。

让我们在下一节了解静态 Pod。

理解 Kubernetes 中的静态 Pod

静态 Pod 提供了在 Kubernetes 集群中管理 Pod 的另一种方式。与由集群的 Pod 调度器和 API 服务器控制的常规 Pod 不同,静态 Pod 由特定节点上的 kubelet 守护进程直接管理。除非通过 kubelet 为其创建的镜像 Pod,否则 API 服务器无法感知这些 Pod。

关键特性

  • 节点特定:静态 Pod 绑定到单个节点,无法在集群中其他地方移动。

  • Kubelet 管理:指定节点上的 kubelet 负责启动、停止和重启静态 Pod。

  • 镜像 Pod:kubelet 在 API 服务器上创建镜像 Pod 以反映静态 Pod 的状态,但这些镜像 Pod 不能通过 API 控制。

静态 Pod 可以通过两种主要方法创建。第一种方法是文件系统托管配置,您将 Pod 定义以 YAML 或 JSON 格式放在节点上的特定目录中。kubelet 定期扫描此目录,并根据现有文件管理 Pod。第二种方法是 Web 托管配置,其中 Pod 定义托管在 Web 服务器上的 YAML 文件中。kubelet 配置该文件的 URL,并定期下载它以管理静态 Pod。

静态 Pod 通常用于在每个节点上引导集群的核心组件,例如 API 服务器或控制器管理器。然而,在集群中的每个节点上运行 Pod 时,通常推荐使用 DaemonSets。静态 Pod 有一些限制,比如不能引用其他 Kubernetes 对象(如 Secrets 或 ConfigMaps),并且不支持临时容器。在需要对 Pod 在各个节点上的位置进行严格控制的场景下,理解静态 Pod 会很有用。

到目前为止,我们已覆盖了控制 Pod 调度和位置的不同机制。在下一节中,我们将简要概述其他调度器配置和功能。

Kubernetes 中的扩展调度器配置

除了调度器的自定义配置,Kubernetes 还支持一些高级调度配置,我们将在本节中讨论这些配置。

调度器配置

您可以使用配置文件自定义调度行为。该文件定义了调度器如何根据各种标准为 Pod 优先选择节点。

关键概念

  • 调度配置文件:配置文件可以指定多个调度配置文件。每个配置文件都有一个独特的名称,并且可以配置自己的插件集合。

  • 调度插件:插件像是构建块,在调度过程中执行特定任务。它们可以基于资源可用性、硬件兼容性或其他因素过滤节点。

  • 扩展点:这些是调度过程中可以插入插件的阶段。不同的插件适用于不同的阶段,比如过滤不适合的节点或为合适的节点评分。

重要:调度策略(v1.23 版本之前的 Kubernetes)

在 v1.23 版本之前,Kubernetes 允许通过 kube-scheduler 标志或 ConfigMap 指定调度策略。这些策略定义了调度器如何使用谓词(过滤标准)和优先级(评分函数)来选择节点作为 Pod 的调度目标。从 v1.23 开始,这一功能被调度器配置所取代。这种新方法为调度行为提供了更多的灵活性和控制力。

Kubernetes 调度器配置文件提供了多项优点,用于管理集群中 Pod 的调度位置。它为调度行为提供了灵活性,能够根据您的需求进行定制。例如,您可以将需要 GPU 的 Pods 优先调度到具有这些资源的节点上。此外,您还可以开发自定义插件来处理默认插件未涉及的独特调度需求。最后,定义多个配置文件的能力允许您通过为不同类型的 Pods 分配不同的调度配置文件来实现更精细的控制。

在结束本章之前,让我们来看看 Kubernetes 调度中的节点限制功能。

节点隔离与限制

Kubernetes 允许您使用节点标签将 Pods 隔离在特定节点上。这些标签可以定义诸如安全要求或合规性等属性,从而确保 Pods 仅在符合这些条件的节点上调度。为了防止被攻击的节点篡改标签以谋取私利,NodeRestriction 准入插件限制了 kubelet 修改具有特定前缀(例如,node-restriction.kubernetes.io/)的标签。要使用此功能,您需要启用 NodeRestriction 插件和 Node authorizer。然后,您可以将带有受限前缀的标签添加到您的节点,并在 Pod 的 nodeSelector 配置中引用它们。这确保了您的 Pods 只会在预定义的、隔离的环境中运行。

调优 Kubernetes 调度器性能

在大型 Kubernetes 集群中,调度器的高效性能至关重要。让我们来看看一个关键的调优参数:percentageOfNodesToScore

percentageOfNodesToScore 设置决定了调度器在搜索合适的 Pod 放置位置时考虑多少个节点。较高的值意味着调度器会检查更多的节点,可能找到更合适的节点,但会耗费更多时间。相反,较低的值会加快调度速度,但可能导致不理想的节点选择。您可以在 kube-scheduler 配置文件中配置此值。有效范围是 1% 到 100%,默认值根据集群大小计算(100 个节点时为 50%,5000 个节点时为 10%)。

要将 percentageOfNodesToScore 设置为 50% 并应用于一个有数百个节点的集群,您需要在调度器文件中包含以下配置:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
  provider: DefaultProvider
...
percentageOfNodesToScore: 50 

最优值取决于你的优先级。如果快速调度至关重要,较低的值可能是可以接受的。然而,如果确保最佳的节点分配比速度更重要,建议选择较高的值。避免将其设置得过低,以免调度器忽视潜在的更优节点。

到此为止,我们已完成本章学习,并了解了 Kubernetes 中控制 Pod 在节点上分配的不同机制和策略,包括 nodeNamenodeSelectornodeAffinity、污点和容忍度,以及其他有用的高级调度器配置。

总结

本章概述了 Kubernetes 中 Pod 调度的高级技术。首先,我们回顾了 kube-scheduler 实现背后的理论,并解释了调度 Pod 的过程。接下来,我们介绍了 Pod 调度中的节点亲和性概念。我们讨论了使用节点名称和节点选择器的基本调度方法,并基于此解释了更高级的节点亲和性是如何工作的。我们还解释了如何使用亲和性概念实现反亲和性,以及什么是 Pod 之间的亲和性/反亲和性。之后,我们讨论了节点的污点以及 Pod 指定的容忍度。你了解了污点的不同效果,并在一个涉及节点上 NoExecuteNoSchedule 污点的高级用例中将这些知识付诸实践。最后,我们讨论了 Kubernetes 中的一些高级调度特性,如调度器配置、节点隔离和静态 Pod。

在下一章,我们将讨论 Kubernetes 中 Pod 和节点的自动扩缩。这是一个展示 Kubernetes 如何在云环境中灵活运行工作负载的话题。

进一步阅读

有关 Kubernetes 中 Pod 调度的更多信息,请参阅以下 PacktPub 书籍:

你也可以参考官方文档:

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/cloudanddevops

第二十章:Kubernetes Pods 和节点的自动扩展

不用多说,拥有自动扩展功能的云原生应用被认为是云端应用运行的圣杯。简而言之,自动扩展是指一种自动且动态调整应用可用计算资源(如 CPU 和内存)的方式。自动扩展的目标是根据最终用户的活动和需求来增加或减少资源。例如,某个应用在白天用户最活跃时可能需要更多的 CPU 和内存,但在夜间需求会大幅减少。类似地,例如,如果你支持一个电商业务基础设施,在所谓的黑色星期五期间,你可以预期会有巨大的需求激增。通过这种方式,你不仅可以为用户提供更好的、高可用的服务,还可以降低企业的销售成本COGS)。你在云端消耗的资源越少,支付的费用也越少,企业可以将这些节省下来的资金投入到其他地方——这是一种双赢局面。当然,没有单一的规则适用于所有的用例,因此,良好的自动扩展需要基于关键的使用指标,并且应该具有预测功能,能够基于历史数据预测工作负载。

Kubernetes 作为最成熟的容器编排系统,拥有多种内建的自动扩展功能。这些功能中的一些在每个 Kubernetes 集群中都是原生支持的,而另一些则需要安装或特定类型的集群部署。你还可以使用多种扩展维度

  • Pod 的垂直扩展:这涉及调整分配给 Pod 的 CPU 和内存资源。Pod 可以在指定的 CPU 和内存限制下运行,以防止过度消耗,但这些限制可能需要自动调整,而不是依赖人工操作员来猜测。这是通过VerticalPodAutoscalerVPA)来实现的。

  • Pod 的水平扩展:这涉及动态调整你的 Deployment 或 StatefulSet 中 Pod 副本的数量。这些对象自带出色的扩展功能,但副本数量的调整可以通过HorizontalPodAutoscalerHPA)自动化。

  • 节点的水平扩展:这是水平扩展的另一种维度(向外扩展),但这次是在 Kubernetes 节点的层级上。你可以通过添加或移除节点来扩展整个集群。当然,这需要一个运行在支持动态机器配置的环境中的 Kubernetes Deployment,比如云环境。这是通过Cluster AutoscalerCA)来实现的,某些云服务商提供了该功能。

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

  • Pod 资源请求和限制

  • 使用 VerticalPodAutoscaler 对 Pod 进行垂直扩展

  • 使用 HorizontalPodAutoscaler 对 Pod 进行水平扩展

  • 使用集群自动扩展器(Cluster Autoscaler)进行 Kubernetes 节点的自动扩展

  • Kubernetes 的替代自动扩展器

技术要求

本章需要以下内容:

  • 部署了一个 Kubernetes 集群。我们建议使用一个多节点的 Kubernetes 集群。

  • 一个多节点Google Kubernetes EngineGKE)集群。这是 VPA 和集群自动扩展的前提条件。

  • 在您的本地机器上安装并配置了 Kubernetes CLI(kubectl),以管理您的 Kubernetes 集群。

基础 Kubernetes 集群部署(本地和基于云的)以及kubectl安装已在第三章《安装您的第一个 Kubernetes 集群》中涵盖。

本书的第十五章第十六章第十七章已为您概述了如何在不同的云平台上部署一个功能齐全的 Kubernetes 集群,并安装必要的 CLI 工具来管理它们。

本章的最新代码示例可以从官方 GitHub 仓库下载:github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter20

Pod 资源请求和限制

在深入探讨 Kubernetes 中的自动扩展主题之前,我们需要更好地理解如何控制 Kubernetes 中 Pod 容器的 CPU 和内存资源(即计算资源)的使用。控制计算资源的使用非常重要,因为通过这种方式,可以强制执行资源治理——这有助于更好地规划集群容量,最重要的是,防止单个容器消耗所有计算资源,从而阻止其他 Pods 处理请求。

当您创建 Pod 时,可以指定其容器需要多少计算资源,并且设定限制,即允许的资源消耗量。Kubernetes 资源模型进一步区分了两类资源:可压缩不可压缩。简而言之,可压缩资源可以轻松地进行限流,而不会产生严重后果。

这种资源的一个典型例子是 CPU——如果您需要限制某个容器的 CPU 使用量,该容器将正常运行,只是变慢。另一方面,我们还有不可压缩资源,无法在不产生严重后果的情况下进行限制——内存分配就是这种资源的一个例子。如果您不允许容器内运行的进程分配更多的内存,该进程将崩溃并导致容器重启。

要控制 Pod 容器的资源,您可以在其规格中指定两个值:

  • requests:此项指定由系统提供的某个资源的保证量。你也可以从另一个角度来看,这就是 Pod 容器从系统获取的,为了正常运行所需的资源量。这很重要,因为 Pod 调度依赖于requests值(而非limits),即PodFitsResources谓词和BalancedResourceAllocation优先级。

  • limits:此项指定由系统提供的某个资源的最大数量。如果与requests一起指定,则此值必须大于或等于requests。根据资源是否可压缩,超过限制会有不同的后果——可压缩资源(如 CPU)会被限制,而不可压缩资源(如 RAM)可能导致容器被终止并重新启动。

通过为requestslimits设置不同的值,你可以允许资源的过度分配。这样,系统就能够在优化整体资源利用率的同时,更加平稳地处理短时间内的高资源使用情况。这是因为所有容器在节点上同时达到资源限制的情况相对不太可能。因此,Kubernetes 可以在大多数时候更有效地使用可用资源。这有点像虚拟机的过度配置或航空公司超额预订,因为并不是每个人都会在同一时间使用所有分配的资源。这意味着你实际上可以在每个节点上运行更多的 Pods,从而提高整体资源利用率。

如果你完全不指定limits,容器就可以无限制地使用节点上的资源。这可以通过命名空间资源配额限制范围来控制,我们在第六章《Kubernetes 中的命名空间、配额和多租户限制》中探讨过这些内容。

在更高级的场景中,还可以控制巨页和临时存储的requestslimits

在深入配置细节之前,我们需要了解 Kubernetes 中 CPU 和内存的计量单位:

  • 对于 CPU,基本单位是Kubernetes CPUKCU),其中1等于例如 Azure 上的 1 个 vCPU、GCP 上的 1 个核心,或者裸机上的 1 个超线程核心。

  • 允许使用分数值:0.1也可以表示为100m毫 KCPU)。

  • 对于内存,基本单位是字节;当然,你也可以指定标准的单位前缀,例如MMiGGi

要为前几章中使用的nginx Deployment 中的 Pod 容器启用计算资源的requestslimits,请对 YAML 清单resource-limit/nginx-deployment.yaml进行以下更改:

# resource-limit/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment-example
spec:
  replicas: 5
.
...<removed for brevity>...
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80
          **resources:**
            **limits:**
              **cpu:****200m**
              **memory:****60Mi**
            **requests:**
              **cpu:****100m**
              **memory:****50Mi** 

对于 Pod 中的每个容器,指定.spec.template.spec.containers[*].resources字段。在这种情况下,我们已将limits设置为200m KCU 和60Mi的 RAM,requests设置为100m KCPU 和50Mi的 RAM。

当你使用 kubectl apply -f resource-limit/nginx-deployment.yaml 将清单应用到集群时,描述集群中运行此部署的一个节点,你将看到关于计算资源配额和分配的详细信息:

$ kubectl describe node minikube-m03
...<removed for brevity>...
Non-terminated Pods:          (5 in total)
  Namespace                   Name                                         CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                                         ------------  ----------  ---------------  -------------  ---
  default                     nginx-deployment-example-6d444cfd96-f5tnq    100m (5%)     200m (10%)  50Mi (2%)        60Mi (3%)      23s
  default                     nginx-deployment-example-6d444cfd96-k6j9d    100m (5%)     200m (10%)  50Mi (2%)        60Mi (3%)      23s
  default                     nginx-deployment-example-6d444cfd96-mqxxp    100m (5%)     200m (10%)  50Mi (2%)        60Mi (3%)      23s
  kube-system                 calico-node-92bdc                            250m (12%)    0 (0%)      0 (0%)           0 (0%)         6d23h
  kube-system                 kube-proxy-5cd4x                             0 (0%)        0 (0%)      0 (0%)           0 (0%)         6d23h
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                550m (27%)  600m (30%)
  memory             150Mi (7%)  180Mi (9%)
  ephemeral-storage  0 (0%)      0 (0%)
  hugepages-2Mi      0 (0%)      0 (0%)
Events:              <none> 

现在,根据这些信息,你可以进行实验,将容器的 CPU requests 设置为高于集群中单个节点的容量;在我们的例子中,我们在 resource-limit/nginx-deployment.yaml 中修改该值如下:

...
          resources:
            limits:
              **cpu:****2000m**
              memory: 60Mi
            requests:
              **cpu:****2000m**
              memory: 50Mi
... 

按如下配置应用:

$ kubectl apply -f resource-limit/nginx-deployment.yaml
deployment.apps/nginx-deployment-example configured 

如下检查 Pod 状态,你将注意到新的 Pod 会挂在 Pending 状态,因为它们无法在匹配的节点上调度:

$ kubectl get pod
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-deployment-example-59b669d85f-cdptx   1/1     Running   0          52s
nginx-deployment-example-59b669d85f-hdzdf   1/1     Running   0          54s
nginx-deployment-example-59b669d85f-ktn59   1/1     Running   0          54s
nginx-deployment-example-59b669d85f-vdn87   1/1     Running   0          52s
nginx-deployment-example-69bd6d55b4-n2mzq   0/1     Pending   0          3s
nginx-deployment-example-69bd6d55b4-qb62p   0/1     Pending   0          3s
nginx-deployment-example-69bd6d55b4-w7xng   0/1     Pending   0          3s 

通过如下方式描述 Pod,以调查 Pending 状态:

$ kubectl describe pod nginx-deployment-example-69bd6d55b4-n2mzq
...
Events:
  Type     Reason            Age                  From               Message
  ----     ------            ----                 ----               -------
  Warning  FailedScheduling  23m (x21 over 121m)  default-scheduler  **0/3 nodes are available:** 1 node(s) had untolerated taint {machine-check-exception: memory}, 2 Insufficient cpu. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod. 

本章讨论的一些自动扩缩容机制目前处于 alpha 或 beta 版本,可能不完全稳定,因此不适合生产环境。对于更成熟的自动扩缩容解决方案,请参考本章的 Kubernetes 备用自动扩缩容器部分。

在前面的输出中,没有任何节点能够容纳一个需要 2000m KCU 的容器,因此该 Pod 此时无法调度。

现在了解了如何管理计算资源,我们将继续探讨自动扩缩容的主题:首先,我们将解释 Pod 的垂直自动扩缩容。

使用 VerticalPodAutoscaler 进行 Pod 的垂直自动扩缩容

在前一节中,我们手动管理了计算资源的 requestslimits。正确设置这些值需要一些准确的人类猜测、观察指标并进行基准测试以调整。使用过高的 requests 值会导致计算资源浪费,而将 requests 设置得过低可能导致 Pod 被过度密集地安排,从而出现性能问题。此外,在某些情况下,扩展 Pod 工作负载的唯一方式是通过增加其可以消耗的计算资源来垂直扩展。对于裸金属机器来说,这意味着升级 CPU 硬件并增加更多物理内存。对于容器来说,方法就简单多了,只需允许它们获得更多的计算资源配额。当然,这只在单个节点的容量范围内有效。如果要超出这一范围进行垂直扩展,你只能通过向集群添加更强大的节点来实现。

为了帮助解决这些问题,你可以使用 VPA,它可以动态增加或减少 Pod 容器的 CPU 和内存资源 requests

下图显示了 Pod 的垂直扩展。

图 20.1:Pod 的垂直扩展

目标是更好地匹配容器的实际使用情况,而不是依赖于硬编码的、预定义的资源请求和限制值。控制limits在指定的比率范围内也是支持的。

VPA 由名为VerticalPodAutoscaler自定义资源定义CRD)对象创建。这意味着该对象不是标准 Kubernetes API 组的一部分,必须安装在集群中。VPA 是 Kubernetes 生态系统中autoscaler项目的一部分(github.com/kubernetes/autoscaler)。

VPA 的三个主要组件如下:

  • Recommender: 监视当前和过去的资源消耗,并为 Pod 容器提供建议的 CPU 和内存请求值。

  • Updater: 检查具有不正确资源的 Pod 并将其删除,以便可以使用更新后的requestslimits值重新创建 Pod。

  • Admission plugin: 设置由其控制器(例如,Deployment 对象)创建或重新创建的新 Pod 上的正确资源requestslimits,由于更新程序所做的更改。

更新程序需要终止 Pod 的原因,以及 VPA 必须依赖于 Admission 插件的原因,是因为 Kubernetes 不支持对资源requestslimits的动态更改。唯一的方法是终止 Pod 并使用新值创建新 Pod。

VPA 可以以推荐模式运行,其中您可以在 VPA 对象中看到建议的值,但不会将更改应用于 Pods。当前 VPA 被视为实验性,在重新创建 Pods 的模式下使用可能会导致应用程序停机。当实现 Pod requestslimits的原地更新时,情况可能会有所改变。

一些 Kubernetes 提供的服务具有一键或操作员支持来安装 VPA。两个很好的例子是 OpenShift 和 GKE。参考使用垂直 Pod 自动缩放器自动调整 Pod 资源级别文章(docs.openshift.com/container-platform/4.16/nodes/pods/nodes-pods-vertical-autoscaler.html)了解在 OpenShift 中实现 VPA。

启用 InPlacePodVerticalScaling

In-place pod resizing 是 Kubernetes 1.27 引入的α功能,允许动态调整 Pod 资源而无需重新启动,可能提高应用程序性能和资源效率。

Alpha Feature Warning

In-place pod resizing 是 Kubernetes 1.27 的α功能,未来版本可能会更改而没有提前通知。不应在生产集群上部署此功能,因为可能存在潜在的不稳定性;一般而言,α功能可能不适用于稳定版本,并可能随时更改。

要启用此功能,必须在所有集群节点上启用InPlacePodVerticalScaling特性门。

对于 Kubernetes 集群,请使用以下方法启用特性门:

  1. 更新 /etc/kubernetes/manifests/kube-apiserver.yaml(或适用于您的 Kubernetes 集群的配置)。

  2. 添加以下feature-gates

    # /etc/kubernetes/manifests/kube-apiserver.yaml
    ...
      - command:
        - kube-apiserver
       ...<removed for brevity>...
        - --feature-gates=InPlacePodVerticalScaling=true 
    

对于 minikube 环境,请在集群启动时加入功能门控,方法如下:

$ minikube start --feature-gates=InPlacePodVerticalScaling=true 

现在,我们将快速解释如何在运行 GKE 集群的情况下启用 VPA。

在 GKE 中启用 VPA

Google Kubernetes EngineGKE)中启用 VPA 就像运行以下命令一样简单:

$ gcloud container clusters update <cluster-name> --enable-vertical-pod-autoscaling 

请注意,这个操作会导致 Kubernetes 控制平面重启。

如果你想为一个新的集群启用 VPA,可以使用额外的参数 --enable-vertical-pod-autoscaling

$ gcloud container clusters create k8sforbeginners --num-nodes=2 --zone=us-central1-a --enable-vertical-pod-autoscaling 

GKE 集群将会有一个 VPA CRD,你可以利用它来控制 Pods 的垂直自动扩展。

让我们在下一节中了解如何为标准 Kubernetes 集群启用 VPA。如果你使用的是其他类型的 Kubernetes 集群,请按照特定设置的说明进行操作。

为其他 Kubernetes 集群启用 VPA

对于像 AKS 或 EKS 等不同平台(甚至是用于测试的本地部署),你需要手动安装 VPA,通过向集群添加 VPA CRD。最新的安装步骤已在相应的 GitHub 仓库中记录: https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#installation。

要在你的集群中安装 VPA,请执行以下步骤:

  1. 克隆 Kubernetes autoscaler 仓库(github.com/kubernetes/autoscaler):

    $ git clone https://github.com/kubernetes/autoscaler 
    
  2. 进入 VPA 组件目录:

    $ cd autoscaler/vertical-pod-autoscaler 
    
  3. 使用以下命令开始安装。这假设你当前的 kubectl 上下文指向了目标集群:

    $ ./hack/vpa-up.sh 
    
  4. 这将创建一堆 Kubernetes 对象。使用以下命令验证主要组件 Pods 是否正确启动:

    $ kubectl get pods -n kube-system | grep vpa
    vpa-admission-controller-5b64b4f4c4-vsn9j   1/1     Running   0             5m34s
    vpa-recommender-54c76554b5-m7wnk            1/1     Running   0             5m34s
    vpa-updater-7d5f6fbf9b-rkwlb                1/1     Running   0             5m34s 
    

VPA 组件已经在运行,我们现在可以在实际 Pods 上测试 VPA。

使用 VPA

出于演示目的,我们需要一个带有 Pods 的 Deployment,这些 Pods 会实际消耗 CPU。Kubernetes autoscaler 仓库中有一个很好的简单示例,具有 可预测 的 CPU 使用: github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/examples/hamster.yaml。我们将稍微修改这个示例,并进行逐步演示。

警告

VPA 的使用很大程度上取决于底层 Kubernetes 的发行版和成熟度。有时,Pods 可能不会按预期重新调度,这可能导致应用程序的停机。因此,如果启用了 VPA 的完全自动化,在没有进行监控的情况下,可能会导致资源超额分配和集群不稳定等级联问题。

首先让我们准备好部署:

  1. 首先,为你的 Kubernetes 集群启用度量服务器。你可以使用默认的度量服务器(github.com/kubernetes-sigs/metrics-server)并将其部署在你的 Kubernetes 集群中。如果你使用的是 minikube 集群,可以按照以下方式启用度量服务器:

    $ minikube addons enable metrics-server 
    
  2. 为此创建一个新的 Namespace:

    # vpa/vpa-demo-ns.yaml
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      labels:
        project: vpa-demo
      name: vpa-demo 
    

使用以下 kubectl apply 命令创建 Namespace:

$ kubectl apply -f vpa/vpa-demo-ns.yaml
namespace/vpa-demo created 
  1. 创建 hamster-deployment.yamlYAML 清单 文件(查看 vpa/hamster-deployment.yaml 了解示例):

    # vpa/hamster-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hamster
      namespace: vpa-demo
    spec:
      selector:
        matchLabels:
          app: hamster
      replicas: 5
      template:
        metadata:
          labels:
            app: hamster
        spec:
          containers:
            - name: hamster
              image: ubuntu:20.04
              resources:
                requests:
                  cpu: 100m
                  memory: 50Mi
              command:
                - /bin/sh
                - -c
                - while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done 
    

这是一只真正的仓鼠!Pod 的 ubuntu 容器中使用的 command 会重复最大限度地消耗 CPU 0.5 秒,然后空闲 0.5 秒。这意味着实际的 CPU 使用率平均保持在大约 500m KCPU。然而,资源的 requests 值指定了它需要 100m KCPU。这意味着 Pod 会消耗超过其声明的 CPU,但由于没有设置 limits,Kubernetes 不会限制容器的 CPU。这可能导致 Kubernetes 调度器做出错误的调度决策。

  1. 使用以下命令将清单应用到集群中:

    $ kubectl apply -f vpa/hamster-deployment.yaml
    deployment.apps/hamster created 
    
  2. 查看 vpa-demo Namespace 中的 Pods:

    $  kubectl get po -n vpa-demo
    NAME                      READY   STATUS    RESTARTS   AGE
    hamster-7fb7dbff7-hmzt5   1/1     Running   0          8s
    hamster-7fb7dbff7-lbk9f   1/1     Running   0          8s
    hamster-7fb7dbff7-ql6gd   1/1     Running   0          8s
    hamster-7fb7dbff7-qmxd8   1/1     Running   0          8s
    hamster-7fb7dbff7-qtrpp   1/1     Running   0          8s 
    
  3. 让我们验证一下 Pod 的 CPU 使用情况。最简单的方法是使用 kubectl top 命令:

    $ kubectl top pod -n vpa-demo
    NAME                      CPU(cores)   MEMORY(bytes)
    hamster-7fb7dbff7-hmzt5   457m         0Mi            
    hamster-7fb7dbff7-lbk9f   489m         0Mi            
    hamster-7fb7dbff7-ql6gd   459m         0Mi            
    hamster-7fb7dbff7-qmxd8   453m         0Mi            
    hamster-7fb7dbff7-qtrpp   451m         0Mi 
    

正如我们预期的那样,部署中每个 Pod 的 CPU 消耗在大约 500m KCU 附近波动。

这样,我们就可以继续为我们的 Pods 创建 VPA 了。VPA 可以以四种 模式 运行,这些模式通过 .spec.updatePolicy.updateMode 字段进行指定:

  • Recreate:Pod 容器的 limitsrequests 值在 Pod 创建时分配,并根据计算出的推荐动态更新。要更新这些值,必须重启 Pod。请注意,这可能会对您的应用程序产生干扰。

  • Auto:目前等同于 Recreate,但当 Pod 容器的 requestslimits 进行就地更新时,这可以自动切换到新的更新机制。

  • Initial:Pod 容器的 limitsrequests 值仅在 Pod 创建时分配。

  • Off:VPA 以仅推荐模式运行。推荐的值可以在 VPA 对象中查看,例如,通过使用 kubectl

首先,我们将为 hamster 部署创建一个 VPA,该部署以 Off 模式运行,稍后我们将启用 Auto 模式。为此,请按照以下步骤操作:

  1. 创建一个名为 vpa/hamster-vpa.yaml 的 VPA YAML 清单:

    # vpa/hamster-vpa.yaml
    apiVersion: autoscaling.k8s.io/v1
    kind: VerticalPodAutoscaler
    metadata:
      name: hamster-vpa
      namespace: vpa-demo
    spec:
      targetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: hamster
      updatePolicy:
        updateMode: 'Off'
      resourcePolicy:
        containerPolicies:
          - containerName: '*'
            minAllowed:
              cpu: 100m
              memory: 50Mi
            maxAllowed:
              cpu: 1
              memory: 500Mi
            controlledResources:
              - cpu
              - memory 
    

该 VPA 是为名为 hamster 的部署对象创建的,如 .spec.targetRef 中所指定。模式在 .spec.updatePolicy.updateMode 中设置为 "Off""Off" 需要加上引号,以避免被解释为布尔值),容器资源策略在 .spec.resourcePolicy.containerPolicies 中配置。我们使用的策略允许 Pod 容器的 CPU requests100m KCU 和 1000m KCU 之间自动调整,内存在 50Mi500Mi 之间调整。

  1. 将清单文件应用到集群:

    $ kubectl apply -f vpa/hamster-vpa.yaml
    verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created 
    
  2. 您需要稍等片刻以便首次计算推荐值。然后,描述 VPA 来检查推荐值:

    $ kubectl describe vpa hamster-vpa -n vpa-demo
    ...<removed for brevity>...
    Status:
      Conditions:
        Last Transition Time:  2024-08-11T09:20:44Z
        Status:                True
        Type:                  RecommendationProvided
      Recommendation:
        Container Recommendations:
          Container Name:  hamster
          Lower Bound:
            Cpu:     461m
            Memory:  262144k
          Target:
            Cpu:     587m
            Memory:  262144k
          Uncapped Target:
            Cpu:     587m
            Memory:  262144k
          Upper Bound:
            Cpu:     1
            Memory:  500Mi
    Events:          <none> 
    

VPA 已建议分配比预期的 500m KCU 和 262144k 内存略多的资源。这是有道理的,因为 Pod 应该有足够的缓冲区来应对 CPU 消耗。

  1. 现在我们可以实践 VPA,并将其模式更改为 Auto。修改 vpa/hamster-vpa.yaml

    # vpa/hamster-vpa.yaml
    apiVersion: autoscaling.k8s.io/v1
    kind: VerticalPodAutoscaler
    metadata:
      name: hamster-vpa
      namespace: vpa-demo
    spec:
    ...
      updatePolicy:
    **updateMode:****Auto**
    ... 
    
  2. 将清单应用到集群中:

    $ kubectl apply -f vpa/hamster-vpa.yaml
    verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa configured 
    
  3. 一段时间后,您会注意到 Deployment 中的 Pods 正在被 VPA 重启:

    $ kubectl get po -n vpa-demo -w
    NAME                      READY   STATUS              RESTARTS   AGE
    hamster-7fb7dbff7-24p89   0/1     ContainerCreating   0          2s
    hamster-7fb7dbff7-6nz8f   0/1     ContainerCreating   0          2s
    hamster-7fb7dbff7-hmzt5   1/1     Running             0          20m
    hamster-7fb7dbff7-lbk9f   1/1     Running             0          20m
    hamster-7fb7dbff7-ql6gd   1/1     Terminating         0          20m
    hamster-7fb7dbff7-qmxd8   1/1     Terminating         0          20m
    hamster-7fb7dbff7-qtrpp   1/1     Running             0          20m
    hamster-7fb7dbff7-24p89   1/1     Running             0          2s
    hamster-7fb7dbff7-6nz8f   1/1     Running             0          2s 
    
  4. 我们可以检查其中一个已重启的 Pod,以查看当前的资源 requests

    $ kubectl describe pod hamster-7fb7dbff7-24p89 -n vpa-demo
    ...
    Annotations:      ...<removed for brevity>...
                      vpaObservedContainers: hamster
                      vpaUpdates: Pod resources updated by hamster-vpa: container 0: memory request, cpu request
    ...
    Containers:
      hamster:
        ...
        Requests:
          cpu:        587m
          memory:     262144k
    ...<removed for brevity>... 
    

如您所见,新启动的 Pod 已将 CPU 和内存的 requests 设置为 VPA 推荐的值!

目前不应将 VPA 与基于 CPU/内存指标的 HPA 一起使用。然而,您可以将 VPA 与基于自定义指标的 HPA 配合使用。

接下来,我们将讨论如何使用 HPA 实现 Pod 的水平自动扩展。

使用 HorizontalPodAutoscaler 实现 Pod 的水平自动扩展

虽然 VPA 起到了资源使用优化器的作用,但真正实现运行多个 Pod 副本的 Deployment 和 StatefulSets 扩展的是 HPA。从高层次来看,HPA 的目标是根据当前的 CPU 使用率或其他自定义指标(包括同时使用多个指标)自动扩展 Deployment 或 StatefulSets 中的副本数量。基于指标值确定目标副本数的算法细节可以在此找到:https://kubernetes.io/docs/tasks/run-application/horizontal-Pod-autoscale/#algorithm-details。

并非所有应用程序在使用 HPA 和 VPA 时都会同样高效。有些应用可能通过某一种方法运行得更好,但其他的应用可能不支持自动扩展,甚至可能受到该方法的负面影响。在使用任何自动扩展方法之前,请始终分析您的应用程序行为。

下图展示了垂直扩展和水平扩展的区别:

图 20.2:Pod 的垂直扩展与水平扩展

HPA 是高度可配置的,在本章中,我们将介绍一个标准场景,其中我们希望基于目标 CPU 使用率进行自动扩展。

HPA 是 Kubernetes 自动扩展 API 组中的一种 API 资源。目前的稳定版本是 autoscaling/v2,它支持基于内存和自定义指标进行扩展。当使用 autoscaling/v1 时,autoscaling/v2 中引入的新字段会作为注解保留。

HPA 的作用是监控 Pod 的配置指标,例如 CPU 使用率,并确定是否需要更改副本数。通常,HPA 会计算所有 Pod 当前指标值的平均值,并判断添加或删除副本是否能使指标值更接近指定的目标值。例如,假设您将目标 CPU 使用率设置为 50%。在某些情况下,应用程序的需求增加导致 Deployment Pods 的 CPU 使用率达到 80%。HPA 会决定添加更多 Pod 副本,以使所有副本的平均使用率降低,并接近 50%。然后这个过程会重复进行。换句话说,HPA 会尽力保持平均 CPU 使用率尽可能接近 50%。这就像一个持续的闭环控制器——温控器对建筑物内温度变化的反应是一个很好的例子。

下图展示了 Kubernetes HPA 组件的高级图示:

图 20.3:Kubernetes 中的 HPA 概览

HPA 还使用机制如 稳定窗口 来防止副本过快缩减,从而避免出现不必要的副本 波动

GKE 提供了用于多维 Pod 自动扩展的 beta 功能,该功能同时结合了基于 CPU 指标的水平扩展和基于内存使用情况的垂直扩展。有关此功能的更多信息,请参阅官方文档:cloud.google.com/kubernetes-engine/docs/how-to/multidimensional-pod-autoscaling。请注意,此功能适用于一般服务条款中的 Pre-GA 提供条款,并以“按原样”方式提供,支持有限;有关详细信息,请参阅发布阶段描述。

由于 HPA 是 Kubernetes 的内置功能,因此无需执行任何安装。我们只需要为测试准备一个 Deployment 并创建一个 HorizontalPodAutoscaler Kubernetes 资源。

部署应用以演示 HPA

要测试 HPA,我们将依赖于标准的 CPU 使用率指标。这意味着我们需要为 Deployment Pods 配置 requests 以保证 CPU,否则无法进行自动扩展,因为没有绝对的数值来计算百分比指标。此外,我们还需要一个可以消耗可预测 CPU 资源的 Deployment。当然,在实际使用场景中,变化的 CPU 使用率将来自终端用户对应用的实际需求。

首先,为您的 Kubernetes 集群启用度量服务器。您可以使用默认的度量服务器(github.com/kubernetes-sigs/metrics-server)并将其部署到您的 Kubernetes 集群中。如果您使用的是 minikube 集群,可以按照以下步骤启用度量服务器:

$ minikube addons enable metrics-server 

按照以下说明学习如何实现 HPA:

  1. 为了隔离我们在本次演示中的资源,请创建一个新的命名空间,如下所示:

    # hpa/hpa-demo-ns.yaml
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      labels:
        project: hpa-demo
      name: hpa-demo 
    
  2. 应用 YAML 并创建命名空间:

    $ kubectl apply -f hpa/hpa-demo-ns.yaml
    namespace/hpa-demo created 
    
  3. 为了演示,我们将使用一个基于自定义镜像quay.io/iamgini/one-page-web:1.0的简单 Web 服务器容器。以下 YAML 包含一个简单的部署定义,将创建一个 Pod 副本:

    ---
    # hpa/todo-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: todo-app
      namespace: hpa-demo
    spec:
      replicas: 1  # Adjust as needed
      selector:
        matchLabels:
          app: todo
      template:
        metadata:
          labels:
            app: todo
        spec:
          containers:
            - name: todoapp
              image: quay.io/ginigangadharan/todo-app:2.0
              ports:
                - containerPort: 3000
              resources:
                requests:
                  memory: "50Mi"   # Request 50 MiB of memory
                  cpu: "50m"      # Request 0.05 CPU core
                limits:
                  memory: "100Mi"  # Request 100 MiB of memory
                  cpu: "100m"      # Request 0.1 CPU core 
    
  4. 应用配置并确保 Pod 按预期运行:

    $ kubectl apply -f hpa/todo-deployment.yaml
    deployment.apps/todo-app created
    $ kubectl get po -n hpa-demo
    NAME                        READY   STATUS    RESTARTS   AGE
    todo-app-5cfb496d77-l6r69   1/1     Running   0          8s 
    
  5. 为了暴露应用程序,我们创建一个 Service,使用以下 YAML:

    # hpa/todo-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: todo-app
      namespace: hpa-demo
    spec:
      type: ClusterIP
      selector:
        app: todo
      ports:
        - port: 8081          # Port exposed within the cluster
          targetPort: 3000    # containerPort on the pods 
    
  6. 应用配置并验证 Service 资源:

    $ kubectl apply -f hpa/todo-service.yaml
    service/todo-app created
    $ kubectl get svc -n hpa-demo
    NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
    todo-app   ClusterIP   10.96.171.71   <none>        8081/TCP   15s 
    
  7. 现在应用程序正在运行,并通过ClusterIP Service 暴露,我们可以使用kubectl port-forward命令在集群外访问该应用程序:

    $ kubectl port-forward svc/todo-app -n hpa-demo 8081:8081
    Forwarding from 127.0.0.1:8081 -> 3000
    Forwarding from [::1]:8081 -> 3000 
    
  8. 打开浏览器并访问http://localhost:8081,您将看到 Todo 应用程序如下所示:

图 20.4:Todo 应用程序在 Kubernetes 上运行

  1. 在控制台上,按Ctrl+C结束kubectl port-forward任务。

现在我们已经在集群中运行了 Todo 应用程序的部署,是时候了解 HPA 的工作原理了。在下一节中,我们将学习如何创建 HPA,并向部署施加负载以观察自动扩展的效果。

实现 HPA

你已经学会了如何通过使用kubectl scale命令来扩展 Pod 的数量(例如,kubectl scale deployment one-page-web -n hpa-demo --replicas 3),但是在本示例中,我们想了解 HPA 如何基于工作负载进行自动扩展。

正如我们在本节前面所学,HPA 是根据指标触发扩展的,因此我们需要给 Pod 提供工作负载。为了进行压力测试和负载测试,有几个工具可以模拟 Web 应用程序的工作负载。在本次演示中,我们将使用一个叫做hey的小程序进行负载测试。hey 是一个用 Go 编写的轻量级 HTTP 负载测试工具。它的设计目的是简化基准测试,用户可以快速发送大量请求并查看响应时间、请求吞吐量等信息,以便评估 Web 应用程序在负载下的性能。

也可以使用其他方法增加负载。例如,您可以运行另一个容器,通过以下命令访问应用程序 Pod:

$ kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://todo-app; done" 

然而,这种方法可能并不适合精确控制工作负载。

hey 应用程序适用于 Linux、macOS 和 Windows 系统(github.com/rakyll/hey),安装过程非常简单:

  1. 下载适合您操作系统的 hey 包。

  2. 设置可执行权限并将文件复制到可执行路径(例如,ln -s ~/Downloads/hey_linux_amd64 ~/.local/bin/)。

现在,创建 HPA 资源来根据工作负载扩展单页 Web 部署:

  1. 准备 HPA 的 YAML 文件,如下所示:

    # hpa/todo-hpa.yaml
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: todo-hpa
      namespace: hpa-demo
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: todo-app
      minReplicas: 1
      maxReplicas: 5
      metrics:
        - resource:
            name: cpu
            target:
              averageUtilization: 80
              type: Utilization
          type: Resource 
    
  2. 应用配置并创建 HPA:

    $ kubectl apply -f hpa/todo-hpa.yaml
    horizontalpodautoscaler.autoscaling/todo-hpa created
    $ kubectl get hpa -n hpa-demo
    NAME       REFERENCE             TARGETS              MINPODS   MAXPODS   REPLICAS   AGE
    todo-hpa   Deployment/todo-app   cpu: <unknown>/80%   1         5         0          6s 
    
  3. 让我们再次使用kubectl port-forward命令从集群外部访问 Todo 应用程序:

    $ kubectl port-forward svc/todo-app -n hpa-demo 8081:8081
    Forwarding from 127.0.0.1:8081 -> 3000
    Forwarding from [::1]:8081 -> 3000 
    
  4. 目前没有人在使用todo应用程序,因此 Pod 副本仍为 1。现在让我们使用hey工具来模拟负载。在另一个控制台中,执行如下的 hey 工作负载命令:

    $ hey -z 4m -c 25 http://localhost:8081 
    

请参见以下命令的参数和详细信息:

  • -z 4m:运行 4 分钟,维持更长时间的负载

  • -c 25: 使用 15 个并发连接以生成更高的负载,旨在将 CPU 使用率推向 80%

  • http://localhost:8081:访问 todo 应用程序的 URL(通过kubectl port-forward命令启用)

你会在kubectl port-forward 控制台中看到很多连接条目,因为 hey 正在模拟单页面 Web 应用程序的负载。

  1. 打开第三个控制台(无需等待 hey 完成执行)并检查 Pod 资源的利用情况:

    $ watch 'kubectl get po -n hpa-demo;kubectl top pods -n hpa-demo'
    Every 2.0s: kubectl get po -n hpa-demo;kubectl top pods -n hpa-demo
    NAME                        READY   STATUS    RESTARTS   AGE
    todo-app-5cfb496d77-5kc27   1/1     Running   0          76s
    todo-app-5cfb496d77-l6r69   1/1     Running   0          10m
    todo-app-5cfb496d77-pb7tx   1/1     Running   0          76s
    NAME                        CPU(cores)   MEMORY(bytes)
    todo-app-5cfb496d77-5kc27   10m          14Mi
    todo-app-5cfb496d77-l6r69   100m         48Mi
    todo-app-5cfb496d77-pb7tx   7m           14Mi 
    

你可以看到现在已经创建了三个(或更多)Pod,因为hey正在为todo应用程序施加更多的工作负载,这触发了 HPA 来创建更多的副本。

  1. 同时,检查部署的详细信息,确认副本数和事件,以查看扩展事件:

    $ kubectl describe deployments.apps todo-app -n hpa-demo
    Name:                   todo-app
    ...<removed for brevity>...
    Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
    StrategyType:           RollingUpdate
    ...<removed for brevity>...
    Events:
      Type    Reason             Age   From                   Message
      ----    ------             ----  ----                   -------
      Normal  ScalingReplicaSet  16m   deployment-controller  Scaled up replica set todo-app-749854577d to 1
      Normal  ScalingReplicaSet  13m   deployment-controller  Scaled up replica set todo-app-5cfb496d77 to 1
      Normal  ScalingReplicaSet  13m   deployment-controller  Scaled down replica set todo-app-749854577d to 0 from 1
      Normal  ScalingReplicaSet  4m9s  deployment-controller  Scaled up replica set todo-app-5cfb496d77 to 3 from 1 
    

恭喜!你已成功为你的部署配置了水平自动扩展(HPA)。作为后续操作的一部分,删除资源,通过删除hpa-demo命名空间(例如,kubectl delete namespaces hpa-demo)。在下一节中,我们将了解如何使用 CA 自动扩展 Kubernetes 节点,这与 HPA 结合时能够提供更大的灵活性。

使用集群自动缩放器自动扩展 Kubernetes 节点

到目前为止,我们讨论的是在单个 Pod 级别进行扩展,但这并不是你在 Kubernetes 上扩展工作负载的唯一方式。你也可以扩展集群本身,以适应计算资源需求的变化——某个时刻,我们需要更多的节点来运行更多的 Pod。你可以配置固定数量的节点来手动管理节点级别的容量。即使这些节点的设置、管理和退役过程是自动化的,这种方法仍然适用。

这是由 CA(集群自动缩放器)解决的,它是 Kubernetes 自动缩放器仓库的一部分(github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler)。CA 必须能够为 Kubernetes 集群提供和移除节点,这意味着必须实现特定供应商的插件。你可以在这里找到支持的云服务提供商列表:https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#deployment。

CA 会定期检查 Pod 和节点的状态,并决定是否需要采取行动:

  • 如果由于集群资源不足,某些 Pod 无法调度且处于Pending状态,CA 将添加更多节点,直到达到预定义的最大规模。

  • 如果节点未充分利用,而且即使集群中的节点数量更少,所有 Pod 仍能调度,CA 将从集群中移除这些节点,除非节点已达到预定义的最小数量。在节点被移除之前,它们会被优雅地排空。

  • 对于一些云服务提供商,CA 还可以在不同的 VM SKU 之间进行选择,以更好地优化集群操作成本。

Pod 容器必须为计算资源指定 requests,以使 CA 正常工作。此外,这些值应反映实际使用情况;否则,CA 将无法为您的工作负载类型做出正确决策。

CA 可以补充 HPA 的功能。如果 HPA 决定某个部署或 StatefulSet 应该有更多 Pod,但无法调度更多 Pod,那么 CA 可以介入并增加集群的规模。

在我们深入探讨 CA 之前,让我们先了解一些基于 CA 的 Kubernetes 自动扩展的限制。

CA 限制

CA 有一些约束条件,可能会影响其有效性:

  • 从云服务提供商请求新节点到节点可用之间存在延迟。这个延迟通常为几分钟,可能会在需求高峰期间影响应用性能。

  • CA 的扩展决策仅基于 Pod 的资源请求和限制,而不是实际的 CPU 或内存利用率。如果 Pod 过度请求资源,可能会导致节点未充分利用,进而浪费资源。

  • CA 主要是为云环境设计的。尽管可以将其适配到本地或其他基础设施中,但这需要额外的工作。这包括使用自定义脚本或工具来管理节点的预配和去配,以及配置自动扩展器与这些机制的交互。没有基于云的自动扩展功能时,管理集群规模变得更加复杂,并需要更紧密的监控。

启用 CA 需要根据您的云服务提供商执行不同的步骤。此外,一些配置值是针对每个云服务商特定的。接下来我们将在下一节中首先看一下 GKE。

警告 – 资源消耗通知

对于 CA 配置要非常小心,因为许多此类配置可能会导致非常高的资源消耗,进而影响系统稳定性或引发意外的扩展行为。始终监控并微调配置,以避免资源耗尽或性能下降。

在 GKE 中启用 CA

对于 GKE,最简单的方式是从零开始创建启用了 CA 的集群。为此,您需要运行以下命令来创建一个名为k8sbible的集群:

$ gcloud container clusters create k8sbible \
  --enable-autoscaling \
  --num-nodes 3 \
  --min-nodes 2 \
  --max-nodes 10 \
  --region=us-central1-a
...<removed for brevity>...
Creating cluster k8sbible in us-central1-a... Cluster is being health-checked (master is healthy)...done.
Created [https://container.googleapis.com/v1/projects/k8sbible-project/zones/us-central1-a/clusters/k8sbible].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-a/k8sbible?project=k8sbible-project
kubeconfig entry generated for k8sbible.
NAME      LOCATION       MASTER_VERSION      MASTER_IP      MACHINE_TYPE  NODE_VERSION        NUM_NODES  STATUS
k8sbible  us-central1-a  1.29.7-gke.1008000  <removed>      e2-medium     1.29.7-gke.1008000  3          RUNNING 

在前面的命令中:

  • cloud container clusters create k8sbible:创建一个名为k8sbible的新 Kubernetes 集群。

  • --enable-autoscaling:为集群的节点池启用自动扩展功能。

  • --num-nodes 3:将初始节点数设置为3

  • --min-nodes 2:将最小节点数设置为2

  • --max-nodes 10:将节点的最大数量设置为10

  • --region=us-central1-a:指定区域为 us-central1-a

你应该已经为你的 GCP 账户配置了适当的配置和权限,包括虚拟私有云VPC)、网络、安全等。

对于已有集群,你需要在现有的 Node pool 上启用 CA。例如,如果你有一个名为 k8sforbeginners 的集群,并且有一个名为 nodepool1 的 Node pool,那么你需要运行以下命令:

$ gcloud container clusters update k8sforbeginners --enable-autoscaling --min-nodes=2 --max-nodes=10 --zone=us-central1-a --node-pool=nodepool1 

更新将需要几分钟时间。

使用 gcloud CLI 验证自动扩展功能,命令如下:

$ gcloud container node-pools describe default-pool --cluster=k8sdemo |grep autoscaling -A 1
autoscaling:
  enabled: true 

在 GKE 中了解更多关于自动扩展的内容,官方文档:cloud.google.com/kubernetes-engine/docs/concepts/cluster-autoscaler

配置完成后,你可以继续进行 使用 CA 部分。

在 Amazon Elastic Kubernetes Service 中启用 CA

在 Amazon EKS 中设置 CA 目前无法通过一键操作或单一命令完成。你需要创建适当的 IAM 策略和角色,部署 CA 资源到 Kubernetes 集群,并进行手动配置。因此,我们在本书中不会涉及这一部分,建议你参考官方文档:docs.aws.amazon.com/eks/latest/userguide/cluster-autoscaler.html

配置完成后,继续进行 使用 CA 部分。

在 Azure Kubernetes Service 中启用 CA

AKS 提供了与 GKE 类似的 CA 设置体验——你可以使用单一命令程序来部署一个启用了 CA 的新集群,或者更新现有集群以使用 CA。要从头开始在 k8sforbeginners-rg 资源组中创建一个名为 k8sforbeginners-aks 的新集群,执行以下命令:

$ az aks create --resource-group k8sforbeginners-rg \
  --name k8sforbeginners-aks \
  --node-count 1 \
  --enable-cluster-autoscaler \
  --min-count 1 \
  --max-count 10 \
  --vm-set-type VirtualMachineScaleSets \
  --load-balancer-sku standard \
  --generate-ssh-keys 

你可以通过使用 --min-count 参数控制自动扩展中的最小节点数,通过使用 --max-count 参数控制最大节点数。

要在现有的 AKS 集群 k8sforbeginners-aks 上启用 CA,执行以下命令:

$ az aks update --resource-group k8sforbeginners-rg --name k8sforbeginners-aks --enable-cluster-autoscaler --min-count 2 --max-count 10 

更新将需要几分钟时间。

在官方文档中了解更多:docs.microsoft.com/en-us/azure/aks/cluster-autoscaler。此外,AKS 中的 CA 有更多的参数可以通过 autoscaler profile 配置。更多详细信息请参考官方文档:docs.microsoft.com/en-us/azure/aks/cluster-autoscaler#using-the-autoscaler-profile

现在,让我们来看一下如何在 Kubernetes 集群中使用 CA。

使用 CA

我们刚刚为集群配置了 CA,它可能需要一些时间来执行第一次操作。这取决于 CA 配置,可能是厂商特定的。例如,在 AKS 的情况下,集群每 10 秒评估一次(scan-interval),以检查是否需要进行扩展或缩减。如果在扩展后需要缩减,则会有 10 分钟的延迟(scale-down-delay-after-add)。如果请求的资源总和除以容量低于 0.5(scale-down-utilization-threshold),则会触发缩减。

结果是,在启用 CA 后,集群可能会自动进行扩展、缩减或保持不变。

本示范中,我们使用的是一个包含两个节点的 GKE 集群:

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU_ALLOCATABLE:.status.allocatable.cpu,MEMORY_ALLOCATABLE:.status.allocatable.memory
NAME                                     CPU_ALLOCATABLE   MEMORY_ALLOCATABLE
gke-k8sdemo-default-pool-1bf4f185-6422   940m              2873304Ki
gke-k8sdemo-default-pool-1bf4f185-csv0   940m              2873312Ki 
  • 基于此,我们在 GKE 集群中总共有 1.88 核 CPU 和 5611.34 Mi 内存的计算能力。

  • 请记住,kube-system 命名空间的 Pods 会消耗一些 KCU。

  • 使用 kubectl top nodes 命令查看集群中 CPU 和内存的精确使用情况。

不幸的是,容器中的 CPU 使用率并没有现成的简单方式来预测和变化。因此,我们需要设置一个带有 Pod 模板的部署来实现这一目标。在我们的示范中,我们将使用另一个仓鼠部署来创建一个 elastic-hamster 部署(请参阅 GitHub 仓库中的 Chapter20/ca 目录)。容器中持续运行的 hamster.sh 脚本将根据 TOTAL_HAMSTER_USAGE 值增加工作负载。我们将设置所有仓鼠在所有 Pods 上的总工作量。每个 Pod 将查询 Kubernetes API,以确定当前运行的副本数量。然后,我们将总的工作量除以副本数来确定每个仓鼠的工作量。

例如,如果我们将所有仓鼠的总工作量设置为 1.0,这代表集群中的总 KCU 消耗,并且部署五个副本,则每个仓鼠将执行 1.0/5 = 0.2 的工作量。这意味着它们将工作 0.2 秒并休息 0.8 秒。如果我们将部署规模扩展到 10 个副本,则每个仓鼠将工作 0.1 秒,休息 0.9 秒。因此,不论副本数量如何,仓鼠们的总工作时间始终为 1.0 秒。这模拟了一个现实场景,最终用户生成的流量需要被管理,这些负载被分配到 Pod 副本中。副本越多,每个副本处理的流量越少,导致平均 CPU 使用率降低。

你可以使用你熟悉的工具以其他方式增加工作负载。然而,为了避免在此上下文中引入额外的工具,我们采用了一种解决方法来演示工作负载的增加和扩展。

按照以下步骤在集群中实现并测试集群自动扩展:

  1. 为了隔离测试,我们将使用一个 ca-demo 命名空间:

    # ca/ca-demo-ns.yaml
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      labels:
        project: ca-demo
      name: ca-demo 
    
  2. 要通过 Kubernetes API 查询 Deployments,您需要设置额外的 RBAC 权限。更多细节可以参考第十八章Kubernetes 安全性。请准备如下的Role定义:

    # ca/deployment-reader-role.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      namespace: ca-demo
      name: deployment-reader
    rules:
    - apiGroups: ["apps"]
      resources: ["deployments"]
      verbs: ["get", "watch", "list"] 
    
  3. hamster Pods 准备一个ServiceAccount

    # ca/elastic-hamster-serviceaccount.yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: elastic-hamster
      namespace: ca-demo 
    
  4. 同时,准备一个RoleBinding YAML:

    # ca/read-deployments-rolebinding.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: read-deployments
      namespace: ca-demo
    subjects:
    - kind: ServiceAccount
      name: elastic-hamster
      namespace: default
    roleRef:
      kind: Role
      name: deployment-reader
      apiGroup: rbac.authorization.k8s.io 
    
  5. hamster 的部署非常简单,如下所示,但使用了一个特殊的容器镜像(请参考ca/elastic-hamster-deployment.yaml):

    ...
        spec:
          serviceAccountName: elastic-hamster
          containers:
            - name: hamster
              image: quay.io/iamgini/elastic-hamster:1.0
              resources:
                requests:
                  cpu: 500m
                  memory: 50Mi
              env:
                - name: TOTAL_HAMSTER_USAGE
                  value: "1.0" 
    

我们已经创建了一个自定义容器镜像elastic-hammer,其中包含hamster.sh脚本(请参考Chaper20文件夹中的ca/Dockerfileca/hamster.sh)。

  1. 最后,创建一个 HPA 来自动扩展 Pods:

    # elastic-hamster-hpa.yaml
    apiVersion: autoscaling/v1
    kind: HorizontalPodAutoscaler
    metadata:
      name: elastic-hamster-hpa
      namespace: ca-demo
    spec:
      minReplicas: 1
      maxReplicas: 25
      metrics:
        - resource:
            name: cpu
            target:
              averageUtilization: 75
              type: Utilization
          type: Resource
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: elastic-hamster 
    
  2. 不要逐个应用 YAML 文件,让我们将它们一起应用;按如下方式应用ca目录下的所有 YAML 文件:

    $ kubectl apply -f ca/
    namespace/ca-demo created
    role.rbac.authorization.k8s.io/deployment-reader created
    deployment.apps/elastic-hamster created
    horizontalpodautoscaler.autoscaling/elastic-hamster-hpa created
    serviceaccount/elastic-hamster created
    rolebinding.rbac.authorization.k8s.io/read-deployments created 
    

现在,根据计算,我们在 HPA 中配置了maxReplicas: 25。根据脚本的计算,HPA 将尝试调度 25 个cpu: 500m请求的 Pods。实际上,集群没有足够的容量调度这些 Pods,CA 将开始扩展 Kubernetes 节点。

  1. 检查 Pods,我们会发现由于容量问题,几个 Pods 处于 Pending 状态:

    $ kubectl get po -n ca-demo
    NAME                              READY   STATUS    RESTARTS   AGE
    elastic-hamster-87d4db7fd-4tmxn   0/1     Pending   0          7m20s
    elastic-hamster-87d4db7fd-59lcd   1/1     Running   0          8m4s
    elastic-hamster-87d4db7fd-5d2gf   0/1     Pending   0          7m20s
    elastic-hamster-87d4db7fd-5m27q   0/1     Pending   0          8m4s
    elastic-hamster-87d4db7fd-7nc48   0/1     Pending   0          7m19s
    ...<removed for brevity>...
    elastic-hamster-87d4db7fd-st7r5   0/1     Pending   0          7m34s
    elastic-hamster-87d4db7fd-twb86   1/1     Running   0          8m48s
    elastic-hamster-87d4db7fd-xrppp   0/1     Pending   0          7m34s 
    
  2. 现在检查节点,您会发现集群中总共有 10 个节点(这是我们使用--max-nodes 10参数配置的最大数量):

    $ kubectl top pod -n ca-demo
    NAME                                     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%  
    gke-k8sdemo-default-pool-1bf4f185-6422   196m         20%    1220Mi          43%      
    gke-k8sdemo-default-pool-1bf4f185-csv0   199m         21%    1139Mi          40%      
    gke-k8sdemo-default-pool-1bf4f185-fcsd   751m         79%    935Mi           33%      
    gke-k8sdemo-default-pool-1bf4f185-frq6   731m         77%    879Mi           31%      
    gke-k8sdemo-default-pool-1bf4f185-h8hw   742m         78%    846Mi           30%      
    gke-k8sdemo-default-pool-1bf4f185-j99r   733m         77%    923Mi           32%      
    gke-k8sdemo-default-pool-1bf4f185-k6xq   741m         78%    986Mi           35%      
    ...<removed for brevity>... 
    

这展示了 CA 如何与 HPA 协作,在同一时间无缝地扩展 Deployment 和集群,以适应工作负载(在我们的案例中,由于最大节点限制,工作负载并未满载)。我们现在将展示自动缩减的过程。请执行以下步骤:

  1. 为了减少集群的负载,让我们减少 HPA 中的最大副本数量。虽然可以编辑 YAML 文件并将其应用到系统中,但我们这里将使用kubectl patch命令:

    $ kubectl patch hpa elastic-hamster-hpa -n ca-demo -p '{"spec": {"maxReplicas": 2}}'
    horizontalpodautoscaler.autoscaling/elastic-hamster-hpa patch 
    
  2. Pod 数量现在将根据更新后的 HPA 进行调整:

    $ kubectl get pod -n ca-demo
    NAME                              READY   STATUS    RESTARTS   AGE
    elastic-hamster-87d4db7fd-2qghf   1/1     Running   0          20m
    elastic-hamster-87d4db7fd-mdvpx   1/1     Running   0          19m 
    
  3. 由于容量需求减少,CA 也将开始缩减节点。但在缩减时,CA 会允许 10 分钟的宽限期,以便将 Pods 从一个节点重新调度到其他节点,然后再强制终止该节点。所以,检查 10 分钟后的节点,您会看到不需要的节点已经从集群中移除。

    $ kubectl get nodes
    NAME                                     STATUS   ROLES    AGE    VERSION
    gke-k8sdemo-default-pool-1bf4f185-6422   Ready    <none>   145m   v1.29.7-gke.1008000
    gke-k8sdemo-default-pool-1bf4f185-csv0   Ready    <none>   145m   v1.29.7-gke.1008000 
    

这展示了 CA 在 HPA 缩减 Deployment 时如何高效地响应集群负载的减少。早些时候,在没有任何干预的情况下,集群短时间内扩展到 10 个节点,然后又缩减到仅 2 个节点。想象一下,始终运行一个八节点集群和使用 CA 智能地按需自动扩展之间的成本差异!

为了确保不会为任何不必要的云资源付费,您需要清理集群或禁用集群自动扩缩,以确保不会运行过多的节点。

本演示结束了我们关于 Kubernetes 自动扩展的章节。但是在进入总结之前,让我们在下一节中了解一些其他的 Kubernetes 自动扩展工具。

Kubernetes 的替代自动扩展器

与基本的 Kubernetes 自动扩展器相比,其他自动扩展器如 Kubernetes 事件驱动自动扩展KEDA)和 Karpenter 提供了更多的灵活性和效率,通过基于特定应用程序指标和工作负载来管理资源扩展。KEDA 允许基于集群外部的事件和自定义指标进行自动扩展。这非常适合事件驱动的应用程序。另一方面,Karpenter 通过自动调整节点数来简化节点的预配和扩展,基于工作负载需求高效地利用集群资源,降低成本。通过这些工具,能够实现精细化的扩展控制,从而使得应用程序能够在不同的负载条件下稳定运行。

让我们在接下来的章节中学习这两个常见的 Kubernetes 自动扩展工具。

KEDA

KEDA (keda.sh) 的设计目的是通过根据自定义指标或外部事件来实现 Kubernetes 中的事件驱动扩展,从而允许你根据需要扩展 Pod 副本的数量。与传统的自动扩展器不同,传统自动扩展器依赖于 CPU 或内存使用率,KEDA 可以根据来自各种事件源的指标触发扩展,例如消息队列、HTTP 请求速率和自定义应用程序指标。这使得它特别适用于那些由特定事件或指标驱动的工作负载,而不是依赖于一般资源使用的工作负载。

KEDA 与现有的 Kubernetes HPA 无缝集成,能够根据动态工作负载扩展应用程序。通过支持多种事件源,KEDA 在扩展决策中提供了灵活性和精确度。KEDA 有助于确保根据实时需求高效地分配资源,从而优化成本并提升应用程序性能。

以下图表展示了 KEDA 的架构和组件:

图 20.5:KEDA 架构(图片来源:https://keda.sh/docs/2.15/concepts/)

KEDA 是一个由 CNCF 托管的开源项目,通过 GitHub 提供最佳努力支持,用于报告 bug 和功能请求。多个供应商将 KEDA 作为其产品的一部分并提供支持,包括 Azure 容器应用程序、Red Hat OpenShift 自动扩展器与自定义指标以及 KEDA Azure Kubernetes 服务插件。

Karpenter

Karpenter (karpenter.sh) 是一个先进的 Kubernetes 集群自动扩展器,专注于优化集群内节点的预配和扩展。它通过根据工作负载的需求动态调整节点数量,自动化计算资源的扩展过程。Karpenter 旨在快速适应需求变化并优化集群容量,从而提高性能和成本效率。

下图展示了 Karpenter 如何在 Kubernetes 集群中工作。

图 20.6: Karpenter 的工作原理(图片来源: https://karpenter.sh

Karpenter 提供了快速高效的节点扩展,具备容量优化和智能配置等功能。它确保能够提供满足工作负载需求的正确类型和数量的节点,从而最大限度地减少浪费和成本。通过提供复杂的扩展和配置功能,Karpenter 有助于保持集群性能,同时控制运营成本。

使用 KEDA 或 Karpenter 实现自动扩展超出了本书的范围;请参考文档 (keda.sh/docs/latest) 了解更多信息。

现在,让我们总结一下本章所学的内容。

总结

在这一章中,你了解了 Kubernetes 集群中的自动扩展技术。我们首先解释了 Pod 资源请求和限制的基础知识,以及它们在 Pod 的自动扩展和调度中的重要性。

接下来,我们介绍了 VPA,它可以根据当前和过去的指标自动调整 Pod 的请求和限制。之后,你了解了 HPA,它可以用于自动调整 Deployment 或 StatefulSet 副本的数量。这些调整是基于 CPU、内存或自定义指标进行的。最后,我们解释了 CA 在云环境中的作用。我们还展示了如何高效地将 HPA 与 CA 结合,以实现工作负载和集群的扩展。

在 VPA、HPA 和 CA 中可以配置的内容还有很多,所以我们只是触及了 Kubernetes 强大自动扩展功能的表面!我们还提到了其他的 Kubernetes 自动扩展器,如 KEDA 和 Karpenter。

在下一章中,我们将解释 Kubernetes 的高级主题,如使用 Ingress 进行流量管理、多集群策略和新兴技术。

深入阅读

关于 Kubernetes 中自动扩展的更多信息,请参阅以下 Packt 出版的书籍:

您还可以参考官方的 Kubernetes 文档:

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/cloudanddevops

第二十一章:高级 Kubernetes:流量管理、多集群策略及更多

本章将讨论 Kubernetes 中的高级主题,这些主题超出了本书前几部分的内容。我们将从深入探讨如何利用 Ingress 实现一些复杂的路由到你的 Pods 开始,接着介绍有效的 Kubernetes 故障排除方法和加固 Kubernetes 安全性的技巧,并提供优化 Kubernetes 设置的最佳实践。

本章将介绍如何使用 Ingress 资源在 Kubernetes 中实现高级流量路由。简而言之,Ingress 允许你通过 HTTP 和 HTTPS 路由将运行在 Service 对象后面的 Pods 暴露给外部世界。到目前为止,我们已介绍了通过 Service 对象直接暴露应用程序的方法,特别是 LoadBalancer Service。但这种方法仅在有 cloud-controller-manager 运行的云环境中有效,它通过配置外部负载均衡器与该类型的 Service 一起使用。此外,每个 LoadBalancer Service 都需要一个单独的云负载均衡器实例,这会带来额外的成本和维护开销。接下来,我们将介绍 Ingress 和 Ingress Controller,它们可以在任何类型的环境中为你的应用程序提供路由和负载均衡能力。你还将了解如何使用 nginx Web 服务器作为 Ingress Controller,以及如何为你的 AKS 集群配置专用的 Azure 应用程序网关 Ingress ControllerAGIC)。

此外,我们还将回顾一些近期的 Kubernetes 项目,包括用于虚拟化的 KubeVirt 和无服务器解决方案,如 Knative 和 OpenFaaS。你还将了解短暂容器及其在实时故障排除中的应用、不同 Kubernetes 插件的角色,以及多集群管理。虽然我们会概述大多数内容,但请注意,部分话题仅会进行高层次讲解,因为它们超出了本书的详细范围。

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

  • 使用 Ingress 进行高级流量路由

  • 网关 API

  • Kubernetes 的现代进展

  • 维护 Kubernetes 集群——第二天任务

  • 加固 Kubernetes 集群——最佳实践

  • 故障排除 Kubernetes

技术要求

本章需要以下内容:

  • 部署一个 Kubernetes 集群。我们建议使用一个多节点的基于云的 Kubernetes 集群。也可以在启用所需插件后,在 minikube 中使用 Ingress。

  • 需要一个 AKS 集群来学习有关 Azure AGIC 的部分内容。

  • 需要在本地机器上安装 Kubernetes CLI(kubectl)并进行配置,以便管理 Kubernetes 集群。

基本的 Kubernetes 集群部署(本地和基于云的)及 kubectl 安装内容已在第三章《安装你的第一个 Kubernetes 集群》中讨论。

本书的前几章,第十五章第十六章,和第十七章,为您提供了如何在不同云平台上部署一个完全功能的 Kubernetes 集群,并安装所需的 CLI 工具来管理它们的概述。

您可以从官方 GitHub 仓库下载本章的最新代码示例,地址为github.com/PacktPublishing/The-Kubernetes-Bible-Second-Edition/tree/main/Chapter21

使用 Ingress 进行高级流量路由

本节将解释如何使用 Ingress 提供 Kubernetes 中的高级网络功能和流量路由机制。从根本上讲,Ingress 是一个反向代理 Kubernetes 资源。它将根据 Ingress 配置中指定的规则,将来自集群外部的传入请求路由到集群内部的服务。单个入口可以用于允许外部用户访问集群内部部署的应用程序。

在我们深入了解 Ingress 及其资源之前,让我们快速回顾一下我们已经使用过的几种 Kubernetes 服务类型来访问应用程序。

回顾 – Kubernetes 服务

第八章通过服务暴露您的 Pod中,您了解了可以用来暴露 Pod 以供负载均衡流量访问的 Service 对象,既包括内部流量也包括外部流量。在内部,它们作为由 kube-proxy 在每个节点上管理的虚拟 IP 地址实现。接下来,我们将快速回顾不同类型的服务:

  • ClusterIP: 通过由kube-proxy在每个节点上管理的内部可见虚拟 IP 地址暴露 Pod。这意味着该服务只能从集群内部访问。

  • NodePort: 像ClusterIP服务一样,可以通过任何节点的 IP 地址和指定端口进行访问。Kube-proxy 在3000032767范围内暴露端口——默认情况下,这是可以配置的——并在每个节点上设置转发规则,将该端口的连接引导到相应的ClusterIP服务。

  • LoadBalancer: 通常用于有软件定义网络SDN)的云环境,在这种环境中,您可以根据需要配置负载均衡器,将流量重定向到集群。在云控制器管理器中,云中负载均衡器的自动配置是通过特定于供应商的插件驱动的。这种类型的服务结合了NodePort服务的方式,并在其前面增加了外部负载均衡器,将流量路由到 NodePorts。

当然,您仍然可以通过其ClusterIP在内部使用该服务。

虽然总是使用 Kubernetes 服务来启用外部流量访问集群可能听起来很诱人,但始终使用它们也有一些缺点。接下来,我们将介绍 Ingress 对象,并讨论它为何是必需的,以及在什么情况下应该用它来替代服务处理外部流量。

Ingress 对象概述

在上一节中,我们简要回顾了 Kubernetes 中的 Service 对象及其在路由流量中的作用。从传入流量的角度来看,最重要的是 NodePort 服务和 LoadBalancer 服务。通常,NodePort 服务仅与另一个路由和负载均衡组件一起使用,因为在所有 Kubernetes 节点上暴露多个外部端点并不安全。现在,这就留下了 LoadBalancer 服务,它在背后依赖 NodePort。然而,在某些使用场景中,使用这种服务有一些限制:

  • 层 4(L4)的LoadBalancer服务基于 OSI 层 4,按 TCP/UDP 协议路由流量。大多数基于 HTTP/HTTPS 的应用程序要求 L7 负载均衡,这与 OSI 层 7 的应用程序相关。

  • HTTPS 流量无法在 L4 负载均衡器中终止和卸载。

  • 无法使用相同的 L4 负载均衡器在多个域名之间进行基于名称的虚拟主机托管。

  • 如果你拥有一个 L7 负载均衡器,可以实现基于路径的路由。实际上,你根本无法配置 L4 负载均衡器来代理像https://<loadBalancerIp>/service1这样的请求到名为service1的 Kubernetes 服务,以及像https://<loadBalancerIp>/service2的请求到名为service2的 Kubernetes 服务,因为 L4 负载均衡器完全无法识别 HTTP(S)协议。

  • 一些特性,比如会话保持或 cookie 亲和性,要求使用 L7 负载均衡器。

在 Kubernetes 中,这些问题可以通过使用 Ingress 对象来解决,它可以用于实现和建模 L7 负载均衡。Ingress 对象仅用于定义路由和负载均衡规则;例如,哪个路径应路由到哪个 Kubernetes 服务。

让我们来看一个 Ingress 的 YAML 清单文件示例,ingress/portal-ingress.yaml

# ingress/portal-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: portal-ingress
  namespace: ingress-demo
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: **k8sbible.local**
      http:
        paths:
          - **path:****/video**
            pathType: Prefix
            backend:
              service:
                **name:****video-service**
                port:
                  number: 8080
          - **path:****/shopping**
            pathType: Prefix
            backend:
              service:
                **name:****shopping-service**
                port:
                  number: 8080 

我们可以通过以下图示来可视化 Ingress Controller 后面的发生情况:

图 21.1:在云环境中使用 nginx 作为 Ingress Controller

简单来说,Ingress 是服务的路由规则的抽象定义。单独使用 Ingress 并不执行任何操作;它需要 Ingress Controller 来实际处理和实现这些规则——你可以应用manifest文件,但此时它不会产生任何效果。但首先,我们将解释 Ingress HTTP 路由规则是如何构建的。规范中的每个规则都包含以下内容:

  • 可选主机:我们在示例中没有使用这个字段,因此我们在这里定义的规则适用于所有传入流量。如果提供了该字段值,那么规则仅适用于目标为此主机的请求——你可以让多个主机名解析到同一个 IP 地址。host字段支持通配符。

  • 路径路由的列举:每个路径都有一个关联的 Ingress 后端,您通过提供 serviceNameservicePort 来定义。在前面的示例中,所有到达以 /video 为前缀的路径的请求都将被路由到 video-service 服务的 Pods,而所有到达以 /shopping 为前缀的路径的请求都将被路由到 shopping-service 服务的 Pods。path 字段支持前缀匹配和精确匹配,您还可以使用特定实现的匹配方式,这些匹配由底层的 Ingress Controller 执行。

这样,您将能够配置涉及集群中多个服务的复杂路由规则,但在外部它们将作为一个单一的端点呈现,并且有多个可用的路径。

为了实现这些 Ingress 对象,我们需要在集群中安装一个 Ingress Controller,我们将在下一节中学习如何操作。

使用 nginx 作为 Ingress Controller

Ingress Controller 是一个 Kubernetes 控制器,通常以 DaemonSet 或 Deployment 对象的形式手动部署到集群中,专门负责处理传入流量的负载均衡和智能路由。它负责处理 Ingress 对象,也就是说,它负责那些指定要使用 Ingress Controller 的对象,并进行实时路由规则的动态配置。

与其他作为 kube-controller-manager 二进制文件的一部分运行的控制器不同,Ingress 控制器不会随着集群的启动而自动启动。Kubernetes 项目维护了多个 Ingress 控制器,包括 AWS、GCE 和 nginx Ingress 控制器。对于第三方 Ingress 控制器项目,请参阅文档以获取详细列表:

kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers

一个常用的 Kubernetes Ingress 控制器是 nginx。正确的术语是 Nginx Ingress Controller。Ingress Controller(www.f5.com/products/nginx/nginx-ingress-controller)作为 Deployment 安装在集群中,并设置处理 Ingress API 对象的规则。Ingress Controller 作为一个 Service 暴露,类型取决于安装方式——在云环境中,类型将是 LoadBalancer

在云环境中,您经常会遇到专用的 Ingress 控制器,这些控制器利用云提供商提供的特殊功能,允许外部负载均衡器直接与 Pods 通信。在这种情况下,没有额外的 Pod 开销,甚至可能不需要 NodePort 服务。这种路由是在 SDN 和 CNI 层进行的,而负载均衡器可能会使用 Pods 的私有 IP 地址。我们将在下一节讨论时,回顾这种方法的一个示例,具体是关于 AKS 的应用网关 Ingress 控制器

ingress-nginx 的安装方法已在官方文档中针对不同环境进行了描述:kubernetes.github.io/ingress-nginx/deploy/

请注意,虽然使用 Helm 是首选的部署方法,但某些环境可能需要特定的说明。对于云环境,安装 ingress-nginx 通常非常简单,只需应用一个 YAML 清单文件(或在创建云托管的 Kubernetes 集群时启用 ingress-nginx),即可创建多个 Kubernetes 对象。例如,可以通过以下单个命令,在 AKS 或 GKE 中部署所需的 ingress 控制器组件:

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

在编写本文及测试部署时,情况是这样的。要获取最新的稳定版本,请参考 Ingress 控制器的文档。还要注意,不同的 Kubernetes 发行版可能有不同的前提条件来实现这些功能;例如,您需要在 GKE 集群中拥有集群管理员权限,才能启用 ingress-nginx。请参考文档 (kubernetes.github.io/ingress-nginx/) 以了解更多信息。

在 AWS 中,通过配置类型为 LoadBalancer 的服务来使用 网络负载均衡器 (NLB) 来公开 Nginx Ingress 控制器:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/aws/deploy.yaml 

该 YAML 文件包含多个资源,用于在集群中设置 Ingress,包括 RolesRoleBindingNamespaceConfigMap 等。

如果您没有云环境或基于云的 Kubernetes 部署,请参考以下部分,在 minikube 集群中部署 Ingress 控制器。

在 minikube 中部署 NGINX Ingress 控制器

可以使用以下命令部署一个多节点的 minikube Kubernetes 集群:

$ minikube start --cni calico --nodes 3 --kubernetes-version=v1.31.0 

一旦 Kubernetes 集群启动并运行,使用以下命令在 minikube 集群中启用 Ingress:

$ minikube addons enable ingress
💡  ingress is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
...<removed for brevity>...
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled 

按照如下方式验证ingress-nginx命名空间中的 Pods:

$ kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-rsznt        0/1     Completed   0          78s
ingress-nginx-admission-patch-4c7xh         0/1     Completed   0          78s
ingress-nginx-controller-6fc95558f4-zdhp7   1/1     Running     0          78s 

现在,Ingress 控制器已准备好监控 Ingress 资源。在接下来的部分,我们将学习如何在 Kubernetes 集群中部署 Ingress 资源。

在 Kubernetes 中部署 Ingress 资源

现在,我们已准备好部署我们的应用程序;请参考存储库中的Chapter21/ingress目录,我们已在该目录中准备了以下 YAML 文件:

  • 00-ingress-demo-ns.yaml:创建 ingress-demo 命名空间。

  • video-portal.yaml:创建一个包含 ConfigMap、Deployment 和 Service 的 video 门户。

  • blog-portal.yaml:创建一个包含 ConfigMap、Deployment 和 Service 的 blog 门户。

  • shopping-portal.yaml:创建一个包含 ConfigMap、Deployment 和 Service 的 shopping 门户。

  • portal-ingress.yaml:创建 ingress 资源,为我们的网站(k8sbible.local)创建基于路径的 ingress。

portal-ingress.yaml 文件中,以下规则告诉 ingress,当用户访问 k8sbible.local/video 时,服务 video-service

# ingress/portal-ingress.yaml
...
spec:
  rules:
    - host: k8sbible.local
      http:
        paths:
          - path: /video
            pathType: Prefix
            backend:
              service:
                name: video-service
                port:
                  number: 8080
... 

以下规则告诉 ingress,当用户访问 k8sbible.local/shopping 时,服务 shopping-service

...
          - path: /shopping
            pathType: Prefix
            backend:
              service:
                name: shopping-service
                port:
                  number: 8080
...
Finally, the following rule tells the ingress to serve the **blog-service** when users access **k8sbible.local/ or k8sbible.local:**
...
          - path: /
            pathType: Prefix
            backend:
              service:
                name: blog-service
                port:
                  number: 8080 

由于我们已经了解了 Deployment、ConfigMaps 和 Services,因此我们将跳过这些内容的解释;你可以参考仓库中的 YAML 文件获取更多信息。

按如下方式应用 ingress 目录中的 YAML 文件:

$ kubectl apply -f ingress/
namespace/ingress-demo created
configmap/blog-configmap created
deployment.apps/blog created
service/blog-service created
ingress.networking.k8s.io/portal-ingress created
configmap/shopping-configmap created
deployment.apps/shopping created
service/shopping-service created
configmap/video-configmap created
deployment.apps/video created
service/video-service created 

检查 Pods、Services 和 Ingress 资源:

$ kubectl get po,svc,ingress -n ingress-demo
NAME                            READY   STATUS    RESTARTS   AGE
pod/blog-675df44d5-5s8sg        1/1     Running   0          88s
pod/shopping-6f88c5f485-lw6ts   1/1     Running   0          88s
pod/video-7d945d8c9f-wkxc5      1/1     Running   0          88s
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/blog-service       ClusterIP   10.111.70.32    <none>        8080/TCP   88s
service/shopping-service   ClusterIP   10.99.103.137   <none>        8080/TCP   88s
service/video-service      ClusterIP   10.109.3.177    <none>        8080/TCP   88s
NAME                                       CLASS   HOSTS            ADDRESS         PORTS   AGE
ingress.networking.k8s.io/portal-ingress   nginx   k8sbible.local   192.168.39.18   80      88s 

如果你使用的是基于云的 Kubernetes 集群,那么 k8sbible.local 或你在 ingress 配置中使用的任何 host 应指向云负载均衡器的 IP 地址。如果你没有注册任何实际的域名,你可以通过本地 /etc/hosts 文件(Windows 机器中的路径为 C:\windows\system32\drivers\etc\hosts)模拟相同的效果。

例如,假设我们已经部署了一个 minikube 集群,并使用以下通过 minikube ip 命令获取的 VM IP 地址,将它们添加到 /etc/hosts 文件中:

# k8sbible minikube
192.168.39.18 k8sbible.local 

现在,你可以通过 http://k8sbible.local 访问你的门户。打开浏览器(或使用 curl 命令)并测试不同的服务,如下图所示。

图 21.2:Ingress 提供不同服务。

当你向 http://k8sbible.local/video 发起 HTTP 请求时,流量会通过 nginx 路由到 video-service。同样,当你使用 /shopping 路径时,流量将被路由到 shopping-service。注意,在此操作中你只使用了一个云负载均衡器(或一个公共 IP/主机名),实际的路由到 Kubernetes 服务是通过 Ingress Controller Pods 使用基于路径的路由来完成的。

实际操作中,如果你想要确保 HTTP 端点的安全性,应设置 SSL 证书。可以为入口设置 SSL,但你需要一个域名或本地环境替代方案——本地域名。为了简洁和清晰,我们在示例中不会设置本地域名。欲了解更多信息,请参阅 cert-manager 的文档:

cert-manager.io/docs/tutorials/acme/nginx-ingress/

恭喜!你已经成功配置了集群中的 Ingress 和 Ingress Controller。

正如我们在本节开头提到的,存在多种可供使用的 Ingress 控制器和方法。在我们学习另一种 Ingress 方法之前,让我们在下一节中学习 ingressClass。

ingressClass 和多个 Ingress 控制器

在某些情况下,我们可能需要为 Ingress 控制器配置不同的设置。使用单个 Ingress 控制器时,可能无法实现这一点,因为自定义配置可能会影响 Kubernetes 集群中的其他 Ingress 对象。在这种情况下,您可以通过使用 ingressClass 机制,在单一的 Kubernetes 集群内部署多个 Ingress 控制器。以下是一些典型场景:

  • 不同需求的不同 Ingress 类:Kubernetes Ingress 控制器可以使用特定的 Ingress 类进行标注,例如 nginx-publicnginx-private。这有助于引导不同类型的流量;例如,公共流量可以由性能优化的控制器处理,而内部服务则保持在更严格的访问控制后面。

  • 多协议支持: 不同的应用程序将需要支持多种协议,包括 HTTP/HTTPS 和 TCP/UDP。这可以通过为每种协议配置不同的 Ingress 控制器来处理。这样,具有不同协议要求的应用程序可以在同一 Kubernetes 集群上得到支持,而无需依赖于单一的 Ingress 控制器来处理所有类型的协议。这样不仅能提升性能,还能减少配置的复杂性。

需要注意的是 ingressClass 资源的 .metadata.name,因为在指定 Ingress 对象的 ingressClassName 字段时,这个名称是必需的。该 ingressClassName 字段替代了早期通过注解将 Ingress 与特定控制器关联的方法,正如 IngressSpec v1 文档中所述。

如果在创建 Ingress 时未指定 IngressClass,且您的集群中只有一个被标记为默认的 IngressClass,Kubernetes 会自动将该默认 IngressClass 应用于 Ingress。要将 IngressClass 标记为默认,您应在该 IngressClass 上设置 ingressclass.kubernetes.io/is-default-class 注解,并将其值设为 true。尽管这是预期的规范,但需要注意的是,不同的 Ingress 控制器在实现这些功能时可能会有些许差异。

现在,让我们查看在之前的实践实验中使用的 nginx Ingress 控制器,来识别 ingressClass:

$ kubectl get IngressClass -o yaml
apiVersion: v1
items:
- apiVersion: networking.k8s.io/v1
  kind: IngressClass
  metadata:
    name: nginx
    annotations:
      ingressclass.kubernetes.io/is-default-class: "true"
...<removed for brevity>...
  spec:
    controller: k8s.io/ingress-nginx 

在前面的代码片段中,以下内容是正确的:

  • ingressClass 的名称是 nginx.metadata.name

  • 您可以看到 ingressclass.kubernetes.io/is-default-class: "true"

在接下来的部分中,我们将探索一种针对 AKS 的特殊类型的 Ingress 控制器,名为 Azure 应用程序网关 Ingress 控制器。

针对 AKS 的 Azure 应用程序网关 Ingress 控制器

正如前面章节中详细讨论的那样,使用 nginx Ingress 控制器是一种相当灵活的 Kubernetes 集群内流量路由方式。尽管这种方法通常能很好地工作,但当选择像Azure Kubernetes ServiceAKS)这样的云服务提供商时,由于多层负载均衡的存在,这种方式可能会变得有些复杂。这些负载均衡层可能会引入不必要的复杂性,并增加故障点的数量。

为了解决这些问题,AKS 提供了一种原生的 L7 负载均衡服务,称为Azure 应用程序网关 Ingress 控制器AGIC)。AGIC 与 Azure 的网络服务协同工作,支持更高效、更可靠的流量路由,使得能够通过 Pods 的私有 IP 地址直接与 Pods 通信。这样的功能通过一些 Azure SDN 特性得以实现,例如 VNet 对等连接。

为什么选择 AGIC 用于 AKS?

选择 AGIC 用于 AKS 的原因如下:

  • 简化的负载均衡:AGIC 消除了使用单独的 Azure 负载均衡器的需求,这个负载均衡器会将请求通过 NodePorts 代理到 nginx Ingress 控制器 Pods。相反,它直接将流量转发到 Pods。这减少了负载均衡过程中涉及的层数,并最小化了故障点的可能性。

  • 直接 Pod 通信:AGIC 利用 Azure SDN 能力,允许与 Pods 进行直接通信,而不需要 kube-proxy 来管理服务的路由。

AGIC 的高层设计如下图所示:

图 21.7 – AKS 中的应用程序网关 Ingress 控制器

图 21.3:AKS 中的应用程序网关 Ingress 控制器

可以在现有的 AKS 集群上配置 AGIC,具体方法可以参见官方文档:docs.microsoft.com/en-us/azure/application-gateway/tutorial-ingress-controller-add-on-existing

为了简化操作,我们将通过单个命令创建一个启用了 AGIC 的新 AKS 集群。要在 k8sforbeginners-rg 资源组中部署一个名为 k8sforbeginners-aks-agic 的双节点集群,请执行以下命令:

$ az aks create --resource-group myResourceGroup --name myAKSCluster --node-count 2 --network-plugin azure --enable-managed-identity -a ingress-appgw --appgw-name MyAppGateway --appgw-subnet-cidr "10.2.0.0/16" --generate-ssh-keys 

这将创建一个名为 AksApplicationGateway 的 Azure 应用程序网关,子网 CIDR 为 10.2.0.0/16

集群部署完成后,我们需要生成 kubeconfig 以便与 kubectl 一起使用。运行以下命令(它会切换到一个新的上下文,这样你以后仍然可以使用旧的上下文):

$ az aks get-credentials --resource-group k8sforbeginners-rg --name k8sforbeginners-aks-agic
Merged "k8sforbeginners-aks-agic" as current context in .kube/config 

现在我们可以使用与前一节相同的 YAML 清单来定义 ingress 目录中的部署和服务——与 Book 仓库中的定义相同。但是,我们需要在 AGIC 的 YAML 中做一些修改;为了更清晰起见,我们将 ingress 目录的内容复制到 aks_agic 目录中,并在其中进行修改。修改 Ingress 资源定义如下:

# aks-agic/portal-ingress.yaml
...
spec:
  **ingressClassName:****azure-application-gateway**
... 

我们还将命名空间重命名为 agic-demo,以隔离测试。按照以下方式从 aks_agic 目录应用 YAML 定义:

$ kubectl apply -f aks_agic/ 

等待片刻,直到应用网关更新其配置。要获取 Ingress 的外部 IP 地址,运行以下命令:

$ kubectl get ingress
NAME              CLASS    HOSTS   ADDRESS         PORTS   AGE
example-ingress   <none>   *       52.191.222.39   80      36m 

在我们的例子中,IP 地址是 52.191.222.39。

通过使用获取到的 IP 地址,访问 /video/shopping 路径来测试配置:

  • 服务 1http://<external-IP>/video 将由 video-service Pods 提供服务。

  • 服务 2http://<external-IP>/shopping 将由 shopping-service Pods 提供服务。

  • 默认服务http://<external-IP>/ 将由 blog-service Pods 提供服务。

使用此设置,您已经成功配置并测试了 AKS 中的 AGIC。

在下一部分,我们将了解 Kubernetes 中的 Gateway API,这是一种相对较新且强大的方法,用于管理集群内的流量路由。

Gateway API

Kubernetes Gateway API 是一组不断发展的资源,它提供了一种更具表现力和可扩展的方式来定义集群中的网络流量路由。

它的设计目的是最终取代 Ingress API,提供一个更强大、灵活的机制来配置负载均衡、HTTP 路由和其他网络相关功能。

组成 Gateway API 的三个主要 API 资源如下:

  • GatewayClass 代表一类共享共同配置集并由实现该资源的同一控制器管理的 Gateways。

  • Gateway 是一个通过控制器管理流量的环境实例,例如云负载均衡器。

  • HTTPRoute 定义了从 Gateway 监听器到后端网络端点的 HTTP 特定路由规则,通常表现为服务。

下图展示了使用 Gateway API 资源的高级流程:

图 21.4:Gateway API 资源模型

在这些功能中,使用 Gateway API 相比 Ingress API 的主要优势包括对复杂路由场景(如多级、跨命名空间路由)的灵活支持。此外,设计还强调可扩展性,第三方开发者可以编写自己的 Gateway 控制器,与 Kubernetes 无缝交互。此外,Gateway API 允许对路由规则、流量策略和负载均衡管理任务进行更精细的控制。

这里提供了一个典型的 GatewayClass 作为参考:

# gateway_api/gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: dev-cluster-gateway
spec:
  controllerName: "example.net/gateway-controller" 

gateway-api/gateway_api/gateway.yaml 包含一个典型的 Gateway 资源,指向 dev-cluster-gateway 作为 gatewayClassName

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: dev-gateway
  namespace: gateway-api-demo
spec:
  gatewayClassName: dev-cluster-gateway
  listeners:
    - name: http
      protocol: HTTP
      port: 80 

最后,我们有 HTTPRoute(类似于 Ingress),其中的规则指向不同的服务:

# gateway-api/httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: dev-httproute
  namespace: gateway-api-demo
spec:
  parentRefs:
    - name: dev-cluster-gateway
      kind: Gateway
      namespace: gateway-api-demo
  hostnames:
    - "k8sbible.local"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /video
      backendRefs:
        - name: video-service
          port: 80 

下图解释了 Gateway API 工作流中涉及的组件:

图 21.5:Gateway API 组件。

如果你想进一步探索,可以参考文档并在集群中实现 Gateway API。Gateway API 旨在替代 Ingress API,但它不直接支持 Ingress 资源类型。因此,你需要将现有的 Ingress 资源转换为 Gateway API 资源,这是一项一次性的迁移工作。有关如何执行此迁移的指南,请查阅 Ingress 迁移指南(gateway-api.sigs.k8s.io/guides/migrating-from-ingress)。

在我们结束高级路由、Ingress 和 Gateway API 话题之前,让我们在下一节快速介绍一下 EndPointSlices。

理解 Endpoints 和 EndpointSlices

传统上,Kubernetes 通过 Pod 管理应用程序的部署,其中 Service 对象充当可靠的网络中介。Services 会作为某种入口进入 Pod,并维护一个对应的 Endpoints 对象,该对象列出了与 Service 的选择器标准匹配的活跃、健康的 Pods。当规模增长时,这种方式无法扩展。

假设一个 Service 代表多个 Pods。对应的 Endpoints 对象携带每个 Pod 的 IP 和端口,这些信息会传播到集群中,并用于网络配置。任何对此对象的更新,都会影响整个集群中的节点,即使是微小的变化,也会导致大量网络流量和节点处理负担。

为了解决这一挑战,EndpointSlices 将庞大的 Endpoints 对象拆分成更小的部分。每个 EndpointSlice 默认容纳 100 个端点,这些端点表示 Pod 的网络详细信息。

使用 EndpointSlices 更新将更加精确。与其重新下载整个 Endpoints 对象,只更新包含该 Pod 的切片。这样可以减少网络流量和节点工作负载,最重要的是,提高可扩展性和性能,这已被证明是 Kubernetes 进化中的一个令人兴奋的前景。

请参考 EndPointSlices 文档了解更多信息(kubernetes.io/docs/concepts/services-networking/endpoint-slices/)。

在下一节中,我们将探索诸如无服务器计算、机器学习、虚拟化等先进技术,以及它们如何与 Kubernetes 集成。

Kubernetes 的现代进展

Kubernetes 处于整合和支持一系列先进技术的前沿,这些技术正在重塑 IT 领域的格局。因此,Kubernetes 提供了一个灵活且可扩展的平台,可以轻松集成现代和前沿的无服务器计算解决方案,如Knative;函数即服务,如OpenFaas;虚拟机管理,如KubeVirt;或者机器学习工作流,如Kubeflow。这些解决方案扩展了 Kubernetes 的功能,从而帮助组织创新,并以更高效和快速的方式迈向采用新范式的目标。

在本章中,我们将深入探讨两大最强大的框架 Knative 和 OpenFaaS 及其主要用例。

使用 Knative 和 OpenFaaS 实现无服务器架构

无服务器计算正在改变构建和部署应用程序的范式,这使得开发人员可以只编写代码,而基础设施管理则交给自动化平台。在 Kubernetes 环境中,结合KnativeOpenFaaS,强大的无服务器功能可以部署、扩展和管理函数即服务。

Knative 是一个基于 Kubernetes 的平台,它抽象化了管理容器化应用程序的底层复杂性。它为诸如自动扩展、流量管理和事件驱动函数执行等任务提供了自动化。此外,这也使得 Knative 在处理可变工作负载或事件驱动任务时非常高效。你可以使用 Knative 在高峰期扩展微服务,处理后台任务,如用户上传处理,或构建超响应的事件驱动系统。

另一个灵活的框架是OpenFaaS,它在 Kubernetes 上部署函数时提供了极大的便利。OpenFaaS 允许在容器中部署轻量级的无服务器函数,从而确保易于扩展和管理。这在微服务架构中非常有用,可以根据需求单独扩展每个函数。OpenFaaS 非常适用于实时数据处理、事件触发的函数,或者构建 API 以调整图像大小或转换数据,而无需整个应用堆栈的开销。结合 Knative 和 OpenFaaS,组织可以更好地利用 Kubernetes 来减少复杂性,并以更高效的应用程序实现扩展。

Kubeflow – Kubernetes 上的机器学习

Kubeflow 是一个开源平台,使得在 Kubernetes 上部署、扩展和管理机器学习工作流变得简单顺畅。它将各种工具和框架整合到一个系统中,使数据科学家和开发人员能够专注于创建和实验机器学习模型,而无需担心管理基础设施。

Kubeflow (www.kubeflow.org) 可以自动化整个机器学习周期,从数据准备到模型训练,再到部署和监控。它支持大多数流行的机器学习框架,如 TensorFlowPyTorchXGBoost,因此这些工具能够无缝集成到现有的工作流中。Kubeflow 运行在 Kubernetes 之上,借助 Kubernetes 层的可扩展性和弹性,意味着当需要时,您的机器学习工作负载可以扩展,并能够自动从故障中恢复。

特别是,Kubeflow 是管理大型机器学习项目的有效解决方案,适用于在分布式数据集上进行模型训练、将模型部署到生产环境或在新数据上重复训练模型的场景。这意味着,Kubeflow 可以实现一个真正强大且灵活的平台,加速机器学习应用程序在 Kubernetes 上的开发和部署。

在下一节中,我们将学习什么是 KubeVirt。

KubeVirt – Kubernetes 上的虚拟机

KubeVirt (https://kubevirt.io) 是一个开源项目,它通过管理虚拟机(VMs)扩展了 Kubernetes,除了容器化的工作负载外,还支持虚拟机的管理。这一集成让组织能够在 Kubernetes 集群内运行虚拟机,使得依赖虚拟机的传统应用和现代容器化应用能够在同一管理平台上并行部署。

KubeVirt 允许虚拟机与容器平稳共存。它使得用户能够利用 Kubernetes 强大的编排和扩展功能来管理所有工作负载。这对那些正在迁移到云原生环境,但仍需要支持在虚拟机上运行的遗留应用的组织尤其有帮助。在这种情况下,KubeVirt 可以像容器化应用一样,在同一个 Kubernetes 环境中管理、扩展和编排这些虚拟机。

对于使用 Red Hat OpenShift 的用户,这就是 KubeVirt 的产品化版本,称为 OpenShift Virtualization。它将带来所有这些功能,使您能够直接在 OpenShift 内部运行和管理虚拟机,与容器化的工作负载并排运行。这将减少操作和复杂性,解锁资源的灵活高效使用,并在继续支持基于虚拟机的现有应用程序的同时,更轻松地实现 IT 基础设施的现代化。

我们讨论了新的集群构建,并且大多数时候,我们讨论的是用于开发环境的 Kubernetes,例如 minikube 集群。实际上,也有一些正在运行的 Kubernetes 集群,这些集群托管着生产关键应用程序,因此确保所有种类的集群维护任务作为第二天操作的一部分得到妥善处理至关重要。

在接下来的章节中,我们将探讨一些 Kubernetes 的维护任务。

Kubernetes 集群的维护 – 第二天任务

在接下来的章节中,我们将重点介绍 Kubernetes 的标准维护任务,例如备份、升级、多集群管理等。

Kubernetes 集群的备份与恢复

Kubernetes 的备份与恢复是确保数据完整性和业务连续性的关键问题,特别是在生产环境中。在 Kubernetes 集群的备份范围中,最重要的元素是etcd,即存储集群所有关键配置和状态的键值存储。对于本地或自管集群的etcd备份,涉及到创建快照并安全地存储它们。

备份 etcd

备份etcd集群对于所有 Kubernetes 对象的完整性至关重要,因为etcd存储了整个 Kubernetes 集群的状态。定期备份可以在丢失所有控制平面节点时帮助恢复集群。备份过程会创建一个包含所有 Kubernetes 状态和其他关键数据的快照文件。由于这些数据可能包含敏感信息,建议加密快照文件。

通过使用etcdctl备份的机制完全是在etcd项目层面。对于其他 Kubernetes 发行版,通常会提供适当的工具或机制来执行etcd备份。例如,这个cluster-backup.sh脚本是OpenShiftetcd Cluster Operator的一部分,它封装了执行etcdctl snapshot save的过程,简化了对etcd集群执行快照的操作。

etcdctl工具允许你直接从活跃的etcd成员创建etcd集群的快照。这个过程不会影响etcd实例的性能。

etcdctletcdutl工具可以从etcd发布页面安装(github.com/etcd-io/etcd/releases/):

$ ETCDCTL_API=3 etcdctl \
  --endpoints=[https://127.0.0.1:2379] \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /tmp/snapshot-pre-patch.db 

这些文件(trusted-ca-file、cert-file 和 key-file)通常可以在etcd Pod 的描述中找到(例如,/etc/kubernetes/manifests/etcd.yaml)。

创建快照后,使用etcdutl工具验证其完整性:

$ etcdutl --write-out=table snapshot status snapshot.db 

此命令显示如哈希值、修订版本、总键数和快照大小等详细信息。

如果你的etcd数据存储在支持快照的卷上(例如,Amazon Elastic Block Store),你可以通过对存储卷进行快照来备份etcd数据。这种方法通常用于云环境,其中存储快照可以自动化。

在基于云的集群中,像Google Kubernetes EngineGKE)、Amazon EKS 或 Azure AKS 等托管服务简化了备份过程。这些平台通常提供集成的自动化备份工具和简便的恢复方式。例如,你可以使用 AWS Backup for EKS 或 Azure Backup for AKS 定期备份集群的状态和配置,而无需人工干预。

使用 etcdutl 恢复 etcd 快照

从快照恢复etcd集群是一个关键且复杂的任务,特别是在多节点设置中,必须确保所有节点的一致性。该过程需要小心处理,以避免问题,尤其是当 API 服务器正在运行时。在启动恢复之前,重要的是停止所有 API 服务器实例,以防止不一致。一旦恢复完成,你应该重新启动 API 服务器,并且要重新启动 Kubernetes 的关键组件,如 kube-scheduler、kube-controller-manager 和 kubelet,以确保它们不会依赖过时的数据。

要执行恢复,使用etcdutl工具并指定恢复数据的目录:

$ etcdutl --data-dir <data-dir-location> snapshot restore snapshot.db 

在恢复过程中,指定的<data-dir-location>将被创建。

重新配置 Kubernetes API 服务器

如果恢复后etcd集群的访问 URL 发生变化,你需要重新配置并使用更新后的etcd服务器 URL 重新启动 Kubernetes API 服务器(将$NEW_ETCD_CLUSTER替换为 IP 地址):

...
--etcd-servers=$NEW_ETCD_CLUSTER
... 

如果在etcd集群前使用了负载均衡器,需相应更新其配置。

利用基础设施即代码(IaC)和配置即代码(CaC)进行弹性集群管理

备份和恢复etcd是复杂的,考虑到其可以通过多种方式执行以保持数据一致性和系统稳定性。最重要的是,你需要在 Kubernetes 集群和应用中实施 IaC 和 CaC 实践,以避免这些挑战。这样,你就可以轻松从零开始重新构建一切,因为所有内容都是版本控制、可重复且一致的。

在采用 IaC 和 CaC 实践时,需要注意的是,在 Git 工作流中应该遵循四眼原则。通常,这意味着所有变更必须经过团队中至少两名成员的审查,才能合并。这一做法将提升代码质量,确保合规性,并在备份和恢复过程中减少错误的发生。

为了稳健地设置这一过程,把你的集群视为无状态且不可变的。为所有配置(如命名空间、操作符、基于角色的访问控制RBAC)设置、NetworkPolicies 等)保留 YAML 文件。将这些文件版本化,提交到仓库,并自动应用到新的集群中。这能确保新集群与旧集群一致,从而尽可能减少停机时间,并降低人为错误的发生。

同样,将这一做法扩展到你的应用程序;从 ConfigMaps 和 Services 到 PVCs,所有与应用程序部署相关的内容都应该被编码化。在有状态应用程序中,数据存储在集群外的 PVs 中。由于你将数据与配置分离,恢复应用程序到先前状态时,只需重新应用其 YAML 文件并重新连接到数据即可。

此外,使用 Helm 模板和通过 GitOps 实现持续部署也是可选的,可以让这个过程更加顺畅。这种自动化确保了所有配置的一致性,因为更改会自动应用到环境中,从而减少了手动干预。全面的集群和应用程序管理方法确实有助于简化灾难恢复,同时提升可扩展性、安全性和操作效率。

在接下来的部分中,我们将探讨一些集群升级任务和注意事项。

Kubernetes 集群升级

升级 Kubernetes 集群是确保你的环境安全、稳定,并且能随时使用新功能的重要任务之一。大多数托管 Kubernetes 发行版在基于云的集群中容易升级,因为底层的复杂性由托管服务处理。这些包括 Amazon EKS、GKE 和 Azure AKS。它们提供一键升级功能,可以轻松升级到 Kubernetes 的新版本,且几乎没有或完全没有停机时间。

这对于本地或定制集群会有所不同;例如,使用 kubeadm 构建的集群提供了由 Kubernetes 提供的文档化升级路径(kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade),该路径会引导你完成升级控制平面和节点的步骤。

无论你是使用基于云的集群还是管理本地部署的环境,遵循结构化的升级流程是至关重要的。以下是如何升级 Kubernetes 集群的详细概述。

升级前检查清单

在启动升级之前,准备集群至关重要。以下是一些关键步骤:

  • 验证兼容性:确保新的 Kubernetes 版本与你现有的所有组件和附加功能兼容。请参考 Kubernetes 官方文档中的兼容性矩阵。

  • 备份 etcdetcd 是 Kubernetes 集群的核心。在进行升级之前,请始终创建备份,以保护你的集群配置。

  • 禁用交换空间:Kubernetes 要求所有节点禁用交换空间。确保正确配置此设置,以防止潜在问题。

升级过程

升级过程通常涉及几个步骤:

  • 排空节点:使用 kubectl drain <node-to-drain> --ignore-daemonsets 安全地将所有 pod 从计划升级的节点中驱逐出去。这样可以确保在升级过程中不会向节点分配新的工作。

  • 升级控制平面:首先升级控制平面组件,例如 API 服务器、etcd 和 controller-manager。使用你的包管理器的更新和升级命令(例如 apt-get 或 yum)来安装最新版本。

  • 升级 kubeadm:将 kubeadm 更新到所需版本。这可以确保与新的 Kubernetes 版本兼容。

  • 升级 kubelet 和 kubectl:更新控制平面后,升级每个节点上的 kubelet 和 kubectl。这些组件与控制平面交互,并管理 pods。

  • 解除节点禁用:升级节点后,使用 kubectl uncordon <node-name> 重新启用节点以调度 pods。

  • 升级计算节点:对工作节点执行滚动升级,按照与控制平面相同的步骤进行。

  • 升级 CNI 插件:确保您的 容器网络接口CNI)插件与新的 Kubernetes 版本兼容。如果需要,进行更新。

升级后任务

升级后任务通常包括以下内容:

  • 验证集群状态:使用 kubectl get nodes 确认所有节点处于 Ready 状态。

  • 监控 etcd:在升级过程中和升级后,保持对 etcd 健康状况和性能的关注。

  • 切换包仓库:如果尚未更新,请更新您的包仓库,指向新的 Kubernetes 版本源。

回滚计划

一个重要的事情是,应该为升级过程中的意外错误制定回滚计划。回滚计划应包括执行回退到先前配置和恢复备份所需的步骤。尽管 etcd 的 API 和数据结构的内部变化使得回滚变得困难,但做好准备可以减少时间和运营中断。确定需要由谁做什么,并在团队中协调响应,即使发生需要实施该计划的情况较少,也能保证及时和协调的反应。

额外提示

一些额外的提示如下:

  • 在预演环境中测试升级:在升级生产集群之前,最好先在预演或开发环境中测试升级过程。

  • 考虑使用集群升级工具:一些工具会自动执行升级过程中的某些操作,因此您需要手动做的工作会更少,发生错误的机会也更少。

  • 监控问题:在升级过程中以及之后,监控集群,检查是否有任何异常迹象。

还可以通过使用 Ansible、Terraform、AWS CloudFormation 和 ARM 模板来进一步支持升级自动化,这些工具将代替节点配置、部署包和滚动更新来驱动升级过程。

在这种实际使用场景中,可以通过自动化在多云环境中升级集群。您可以使用 ArgoCD 或 Fleet 等工具管理多集群部署,确保跨不同环境的所有集群都能一致地升级。以上方法对管理多个集群的组织非常有用,从而减少了人工工作并保持环境的一致性。

我们将在下一部分探讨一些知名的多集群管理工具。

多集群管理

组织规模的指数增长也增加了在不同环境中管理大量 Kubernetes 集群的复杂性。正是在这里,多集群管理解决方案提供了一个单一的控制点,能够部署、监控和升级集群。许多这类解决方案具备自动化集群配置和滚动更新等功能,这些都能确保所有管理的集群保持一致性和安全性。

例如,在一个多云环境中,可以使用 Terraform 和 ArgoCD 在 AWS、Azure 和 Google Cloud 上配置和管理 Kubernetes 集群。在这种环境下,部署和升级可以自动化进行,极大地减少了人为错误的可能性,同时所有集群可以保持相同版本的 Kubernetes。这在一个大型组织中尤其有用,特别是当组织拥有多个团队或地区时,这样就能确保 Kubernetes 环境的一致性和实时性,以提高运营效率。

以下列表包含了一些知名的 Kubernetes 多集群管理工具和服务:

  • Rancher:Rancher 是一个开源平台,旨在简化 Kubernetes 管理。它使得跨不同环境的集群能够集中管理,无论是在本地还是云端。Rancher 提供了多集群应用部署、集成监控以及用于跨集群管理用户权限的 RBAC 等功能。

  • Lens:Lens 是一个 Kubernetes 集成开发环境 (IDE),它使得从单一界面管理多个集群变得更加容易。它提供了实时洞察、内置终端以及资源管理视图,使得开发人员和运维人员能够更加轻松地可视化和控制他们的 Kubernetes 环境。

  • Kops: Kubernetes Operations (Kops) 是一个用于管理 Kubernetes 集群生命周期的工具,特别是在 AWS 上。它自动化了创建、升级和删除集群的过程,并因其能够简化不同云平台上的操作而受到好评。

  • Red Hat Advanced Cluster Management for Kubernetes:这个工具为管理跨混合云和多云环境的 Kubernetes 集群提供了全面的解决方案。它包括基于策略的治理、应用生命周期管理和集群可观察性等功能,确保集群符合合规性并且表现优化。

  • Anthos (Google Cloud):这是谷歌云推出的一个多云和混合云管理平台,旨在简化跨不同环境管理 Kubernetes 集群的操作,无论集群是部署在本地还是不同的云服务提供商上。Anthos 提供了集中的治理、安全性,并确保在多种基础设施设置中,应用部署保持一致性,从而保证所有管理集群的统一运营体验。

  • Azure Arc:该服务将 Azure 的管理和治理功能扩展到任何运行的 Kubernetes 集群——无论是在本地、其他云环境还是边缘。使用 Azure Arc,您可以通过一个单一界面管理和保护跨多个环境的 Kubernetes 集群,从而实现一致的政策执行、安全管理和基础设施监控。

在接下来的部分,我们将学习 Kubernetes 集群强化的最佳实践。

保护 Kubernetes 集群 – 最佳实践

保护 Kubernetes 集群至关重要,以防止未经授权的访问、数据泄露和服务中断。通过实施强有力的安全措施,您可以保护敏感数据并确保系统的平稳运行。本节概述了帮助您保护集群免受意外和恶意威胁的安全指南和最佳实践。

本章将讨论的某些安全概念已经在第十八章《Kubernetes 安全》中有所涉及。在这里,我们重新审视这些要点,并强调它们作为 Kubernetes 最佳实践的一部分。

控制对 Kubernetes API 的访问

由于 Kubernetes 在很大程度上依赖于其 API,控制和限制访问是确保集群安全的第一步:

  • 使用 TLS 加密 API 流量:Kubernetes 默认使用 TLS 加密 API 通信。大多数安装方法会自动处理所需的证书。然而,管理员应注意任何未加密的本地端口,并相应地进行加固。

  • API 认证:选择适合您需求的认证方法。对于较小的单用户集群,简单的证书或静态 Bearer Token 可能足够。较大的集群可能需要与现有的认证系统(如 OIDC 或 LDAP)集成。

  • API 授权:经过认证后,每个 API 请求必须通过授权检查。Kubernetes 使用 RBAC 将用户或组与角色中定义的一组权限匹配。这些权限与对资源的特定操作相关,并且可以作用于命名空间或整个集群。为了更好的安全性,建议将节点和 RBAC 授权一起使用。

控制对 Kubelet 的访问

Kubelet 负责管理节点和容器,暴露 HTTPS 端点,这些端点可能会赋予对节点的重大控制权。在生产环境中,请确保启用 Kubelet 认证和授权。

在生产环境中控制对 Kubelet 的访问时,应确保 Kubelet API 的认证和授权有效地限制并分配权限。默认情况下,仅允许通过 Kubernetes API 服务器发出的请求,这样可以阻止未经授权的直接访问 Kubelet。您还可以通过为用户和服务实施 RBAC 策略设置,进一步增强此限制,定义与 Kubelet 相关的 RBAC 权限,同时利用网络策略或防火墙规则限制 Kubelet 端点的网络暴露。

控制运行时工作负载或用户权限

Kubernetes 中的授权是高层次的,但你可以应用更细粒度的策略来限制资源使用并控制容器特权:

  • 限制资源使用:使用资源配额和限制范围来控制命名空间可以使用的 CPU、内存或磁盘空间等资源的数量。这可以防止用户请求不合理的资源值。

  • 控制容器特权:Pod 可以请求以特定用户或具有某些特权的身份运行。大多数应用程序不需要 root 访问权限,因此建议将容器配置为以非 root 用户身份运行。

  • 防止不必要的内核模块:为了防止攻击者利用漏洞,阻止或卸载节点上不必要的内核模块。你还可以使用像 SELinux 这样的 Linux 安全模块来防止模块加载到容器中。

限制网络访问

Kubernetes 允许你在不同层次上控制网络访问:

  • 网络策略:使用网络策略来限制其他命名空间中的哪些 Pod 可以访问你命名空间中的资源。你还可以使用配额和限制范围来控制节点端口请求或负载均衡服务。

  • 限制云元数据 API 访问:云平台通常暴露包含敏感信息的元数据服务。使用网络策略限制对这些 API 的访问,并避免将云元数据用于存储秘密信息。

保护集群组件

为了确保集群安全,保护像 etcd 这样的关键组件并确保正确的访问控制非常重要:

  • 限制对 etcd 的访问:获得对 etcd 的访问权限可能会导致对集群的完全控制。使用强认证凭据,并考虑将 etcd 服务器隔离在防火墙后面。例如,对于 AWS 上的 Kubernetes 集群,可以创建一个具有限制入站规则的安全组,仅允许 Kubernetes 控制平面的 IP 访问私有部署中 etcd2379 端口。你还可以使用 --client-cert-auth--trusted-ca-file 标志来配置 etcd,这样只有控制平面才能通过安全连接进行访问。

  • 启用审计日志:审计日志记录 API 操作以供后续分析。启用并保护这些日志有助于检测和响应潜在的安全问题。Kubernetes 集群管理团队需要为创建、删除和更新事件定义一个自定义审计策略,并可以指示将日志安全存储在像 Elasticsearch 这样的安全日志工具中。以下代码片段展示了 kube-apiserver Pod 清单中的日志配置示例:

    ...
    --audit-log-path=/var/log/audit.log
    --audit-policy-file=/etc/kubernetes/audit-policy.yaml
    ... 
    
  • 频繁轮换基础设施凭证:短生命周期的凭证减少了未经授权访问的风险。定期轮换证书、令牌和其他敏感凭证以保持安全性。例如,可以配置 cert-managercert-manager.io/)自动更新 TLS 证书,并配置 kubelet 定期刷新其自身证书,使用 RotateKubeletClientCertificateRotateKubeletServerCertificate 标志。

  • 审查第三方集成:在添加第三方工具或集成时,仔细审查其权限。尽可能将它们的访问权限限制在特定的命名空间中,以减少风险。例如,在安装 Prometheus 或 Grafana 等工具时,只需通过创建只读角色并将该角色绑定到所需的命名空间中,从而限制数据暴露的量。

  • 加密静态秘密:Kubernetes 支持对存储在 etcd 中的秘密进行静态加密。这确保了即使有人获得了 etcd 数据的访问权限,他们也无法轻易查看敏感信息。在 Kubernetes API 服务器配置中配置 EncryptionConfig,以对存储在 etcd 中的秘密使用 AES 加密,从而在 etcd 被泄露时,数据仍然会被加密,增加一层安全保护。

以下表格总结了 Kubernetes 安全强化的一些最佳实践:

部分 最佳实践
安全集群设置 启用 RBAC 并使用专用的服务账户。保持 Kubernetes 组件更新。使用 TLS 保护 API 服务器访问。
控制集群访问 使用强认证方法。强制执行严格的访问控制和最小权限原则。定期审计和审查访问权限。
保护网络通信 加密内部通信。实施网络分段。使用安全的网络插件并强制执行网络策略。
安全容器镜像 使用可信的容器镜像注册表。扫描镜像漏洞。强制实施 Pod 安全策略以限制容器权限。
监控和记录集群活动 实施日志记录和监控解决方案。启用审计。定期查看日志以查找可疑活动。
定期更新和修补 及时应用更新和修补程序以解决漏洞。遵循严格的更新管理流程。
持续教育和培训 教育团队了解安全最佳实践。保持对最新安全发展的了解。在组织内部推广安全文化。

表 21.1:Kubernetes 集群安全最佳实践

要获得更详细的 Kubernetes 安全加固指导,请参考官方文档和社区资源。此外,考虑查阅如 Kubernetes 加固指南等全面的安全加固准则,该指南由国防信息系统局DISA)提供 (media.defense.gov/2022/Aug/29/2003066362/-1/-1/0/CTR_KUBERNETES_HARDENING_GUIDANCE_1.2_20220829.PDF)。

在接下来的章节中,我们将学习一些常见的 Kubernetes 故障排除方法。

故障排除 Kubernetes

故障排除 Kubernetes 涉及诊断和解决影响集群和应用程序功能及稳定性的问题。常见错误可能包括 Pod 调度问题、容器崩溃、镜像拉取问题、网络问题或资源约束。高效地识别和解决这些错误对于维护健康的 Kubernetes 环境至关重要。

在接下来的章节中,我们将介绍你开始进行 Kubernetes 故障排除所需的基本技能。

获取资源的详细信息

在 Kubernetes 故障排除过程中,kubectl getkubectl describe 命令是诊断和了解集群内资源状态的重要工具。你在前几章中已经多次使用过这些命令,我们现在再回顾一下这些命令。

kubectl get 命令提供了集群中各种资源的高级概览,例如 Pod、服务、部署和节点。例如,如果你怀疑某个 Pod 没有按预期运行,可以使用 kubectl get pods 列出所有 Pod 及其当前状态。该命令会显示 Pod 是否正在运行、等待中,或是否遇到错误,帮助你快速识别潜在问题。

另一方面,kubectl describe 可以深入查看特定资源的详细信息。该命令提供资源的全面描述,包括其配置、事件和最近的变更。例如,如果前面命令列出的 Pod 失败,你可以使用 kubectl describe pod todo-app 获取有关其失败原因的详细信息。

该输出包括 Pod 的事件,例如容器启动失败尝试或拉取镜像的问题。它还会显示详细的配置数据,如资源限制和环境变量,这些信息有助于 pinpoint misconfigurations 或其他问题。

举个例子,假设你正在排查部署问题。使用 kubectl get deployments 可以查看部署的状态和副本数。如果部署卡住或未正确更新,kubectl describe deployment webapp 将提供有关部署滚动历史、条件和更新过程中遇到的错误的详细信息。

在接下来的章节中,我们将学习在 Kubernetes 中查找日志和事件的重要方法,帮助我们简化故障排除过程。

Kubernetes 日志和事件用于故障排除

Kubernetes 提供了强大的工具,如 EventsAudit Logs,可以有效地监控和保护你的集群。Events 是 Event 类型的集群范围资源,提供了关键操作的实时概览,比如 Pod 调度、容器重启和错误。这些事件有助于快速诊断问题,并了解集群的状态。你可以使用 kubectl get events 命令查看事件:

$ kubectl get events 

这个命令会输出一个事件时间线,帮助你识别和排查问题。你可以通过资源类型、命名空间或时间段来过滤特定的事件。例如,要查看与特定 Pod 相关的事件,可以使用以下命令:

$ kubectl get events --field-selector involvedObject.name=todo-pod 

审计日志(Audit Logs),表示为 Policy 类型,对于确保 Kubernetes 环境中的合规性和安全性至关重要。这些日志记录了对 Kubernetes API 服务器发出的 API 请求的详细记录,包括用户、执行的操作和结果。这些信息对于审计活动非常重要,如登录尝试或权限升级。要启用审计日志,您需要通过审计策略配置 API 服务器。有关详细信息,请参阅审计文档(kubernetes.io/docs/tasks/debug/debug-cluster/audit/)。

在调试 Kubernetes 应用时,kubectl logs 命令是一个关键工具,可以用来获取和分析特定容器的日志,帮助有效地诊断和排查问题。

要从 Pod 获取日志,基本命令如下:

$ kubectl logs todo-app 

该命令会从 Pod 中的第一个容器获取日志。如果 Pod 包含多个容器,请指定容器名称:

$ kubectl logs todo-app -c app-container 

对于实时日志流,类似于 Linux 中的 tail -f,可以使用 -f 标志:

$ kubectl logs -f todo-app 

这对于监控实时进程非常有用。如果一个 Pod 已经重启,你可以通过以下命令访问它之前实例的日志:

$ kubectl logs todo-app --previous 

要根据标签过滤日志,可以将 kubectljq 等工具结合使用:

$ kubectl get pods -l todo -o json | jq -r '.items[] | .metadata.name' | xargs -I {} kubectl logs {} 

要有效地管理 Kubernetes 中的日志,实施日志轮转是至关重要的,以防止磁盘空间的过度占用,确保旧日志在生成新日志时被归档或删除。利用结构化日志(例如 JSON 格式)可以更轻松地使用 jq 等工具解析和分析日志。此外,设置像 ElasticsearchFluentdKibanaEFK)这样的集中式日志系统,可以帮助你在整个 Kubernetes 集群中汇总和高效搜索日志,从而全面了解应用程序的行为。

Kubernetes 事件和审计日志一起提供了全面的监控和安全能力。事件提供了对应用程序状态和行为的洞察,而审计日志确保集群内的所有操作都被追踪,帮助你维持安全和合规的环境。

kubectl explain – 内联帮助工具

kubectl explain命令是 Kubernetes 中的一个强大工具,帮助你理解 Kubernetes 资源的结构和字段。它提供了关于特定资源类型的详细信息,允许你直接从命令行浏览 API 架构。这在编写或调试 YAML 清单时尤其有用,因为它确保你使用正确的字段和结构。

例如,要了解 Pod 资源,你可以使用以下命令:

$ kubectl explain pod 

该命令将显示 Pod 资源的概览,包括简要描述。要深入了解特定字段,例如spec字段,你可以像这样扩展命令:

$ kubectl explain pod.spec 

这将提供关于spec字段的详细解释,包括其嵌套字段和预期的数据类型,帮助你更好地理解如何正确配置 Kubernetes 资源。

使用kubectl exec进行交互式故障排除

使用kubectl exec是故障排除和与 Kubernetes 中运行的容器交互的强大方式。这个命令允许你直接在容器内执行命令,这对于调试、检查容器环境和进行快速修复非常有价值。无论你是需要检查日志、检查配置文件,甚至诊断网络问题,kubectl exec都提供了一个直接的方式来实时与应用程序交互。

要使用kubectl exec,你可以先执行一个简单的命令在容器内部执行(你可以使用kubectl apply –f trouble/blog-portal.yaml进行测试):

$ kubectl get po -n trouble-ns
NAME                   READY   STATUS    RESTARTS   AGE
blog-675df44d5-gkrt2   1/1     Running   0          29m 

例如,要列出容器的环境变量,你可以使用以下命令:

$ kubectl exec blog-675df44d5-gkrt2 -- env 

如果 pod 有多个容器,你可以使用-c标志指定要交互的容器:

$ kubectl exec blog-675df44d5-gkrt2 -c blog -- env 

kubectl exec最常见的用法之一是打开容器内的交互式 shell 会话。这样,你可以实时运行诊断命令,例如检查日志文件或修改配置文件。你可以启动一个交互式 shell(如/bin/sh/bin/bash等),如下所示:

$ kubectl exec -it blog-675df44d5-gkrt2 -n trouble-ns -- /bin/bash
root@blog-675df44d5-gkrt2:/app# whoami;hostname;uptime
root
blog-675df44d5-gkrt2
14:36:03 up 10:19,  0 user,  load average: 0.17, 0.07, 0.69
root@blog-675df44d5-gkrt2:/app# 

在这里,适用以下内容:

  • -i:这是一个交互式会话。

  • -t:分配伪终端。

这个交互式会话在需要探索容器环境或故障排除需要按顺序运行多个命令时尤其有用。

除了命令执行,kubectl exec还支持使用kubectl cp将文件复制到容器内外。这在你需要将脚本导入容器或获取日志文件进行进一步分析时特别方便。例如,下面是如何将文件从本地机器复制到容器中:

$ kubectl cp troubles/test.txt blog-675df44d5-gkrt2:/app/test.txt -n trouble-ns
$ kubectl exec -it blog-675df44d5-gkrt2 -n trouble-ns -- ls -l /app
total 8
-rw-r--r-- 1 root root 902 Aug 20 16:52 app.py
-rw-r--r-- 1 1000 1000  20 Aug 31 14:42 test.txt 

要将文件从容器复制到本地机器,你需要以下内容:

$ kubectl cp blog-675df44d5-gkrt2:/app/app.py /tmp/app.py  -n trouble-ns 

这一功能简化了在本地环境和 Kubernetes 集群中运行的容器之间传输文件的过程,使故障排除和调试更加高效。

在下一节中,我们将学习临时容器,它们在 Kubernetes 故障排除任务中非常有用。

Kubernetes 中的临时容器

临时容器是 Kubernetes 中的一种特殊容器类型,设计用于临时的、即时的任务,如调试。与长期用于 Pod 内的常规容器不同,临时容器用于检查和故障排除,并且不会自动重启,也不能保证具有特定的资源。

这些容器可以添加到现有的 Pod 中,帮助诊断问题,当传统方法如 kubectl exec 失败时,它们特别有用。例如,如果 Pod 正在运行一个没有调试工具的 distroless 镜像,可以引入一个临时容器来提供 shell 和其他工具(例如 nslookupcurlmysql 客户端等)进行检查。临时容器通过特定的 API 处理程序进行管理,不能通过 kubectl edit 添加或修改。

例如,在 第八章通过服务暴露你的 Pods 中,我们使用了 k8sutilsquay.io/iamgini/k8sutils:debian12)作为一个独立的 Pod 来测试服务和其他任务。使用临时容器时,我们可以使用相同的容器镜像,但将容器插入到应用 Pod 内进行故障排除。

假设我们有一个名为 video-service 的 Pod 和 Service,运行在 ingress-demo 命名空间中(参见 ingress/video-portal.yaml 文件中的部署详情)。我们可以通过以下方式开始利用 k8sutils 容器镜像进行调试:

$ kubectl debug -it pod/video-7d945d8c9f-wkxc5 --image=quay.io/iamgini/k8sutils:debian12 -c k8sutils -n ingress-demo
root@video-7d945d8c9f-wkxc5:/# nslookup video-service
Server:         10.96.0.10
Address:        10.96.0.10#53
Name:   video-service.ingress-demo.svc.cluster.local
Address: 10.109.3.177
root@video-7d945d8c9f-wkxc5:/# curl http://video-service:8080
    <!DOCTYPE html>
    <html>
    <head>
      <title>Welcome</title>
      <style>
        body {
          background-color: yellow;
          text-align: center;
...<removed for brevity>... 

总结来说,临时容器提供了一种灵活的方式来检查正在运行的 Pod,而不会改变现有的设置或依赖于基础容器的限制。

在接下来的部分中,我们将演示一些常见的 Kubernetes 故障排除任务和方法。

Kubernetes 中的常见故障排除任务

故障排除 Kubernetes 可能非常复杂,且高度依赖于你的集群设置和操作,因为潜在问题的清单可能非常广泛。相反,我们将重点关注一些最常见的 Kubernetes 问题及其故障排除方法,为提供一个实用的起点:

  • Pod 处于 Pending 状态:错误信息Pending表示 Pod 正在等待调度到节点。这可能是由于资源不足或配置错误引起的。要进行故障排除,使用kubectl describe pod <pod_name>查看描述 Pod 为何处于 Pending 状态的事件,如资源限制或节点条件。如果集群资源不足,Pod 将保持 Pending 状态。你可以调整资源请求或添加更多节点。(尝试使用troubles/app-with-high-resource.yaml来测试此情况。)

  • CrashLoopBackOff 或容器错误:当容器重复启动失败时,可能是由于配置错误、缺少文件或应用程序错误,CrashLoopBackOff错误就会发生。要进行故障排除,使用kubectl logs <pod_name>kubectl describe pod <pod_name>查看日志以确定原因。查找错误消息或堆栈跟踪,帮助诊断问题。如果容器启动命令错误,它将无法启动,从而导致此错误。检查容器的退出代码和日志有助于修复问题。(应用troubles/failing-pod.yaml并测试此场景。)

  • 网络问题:这类错误表明网络策略阻止了 Pod 的流量进出。要进行故障排除,可以使用kubectl describe pod <pod_name>检查影响 Pod 的网络策略,并使用kubectl get svc验证服务端点。如果网络策略过于严格,必要的流量可能会被阻止。例如,空的入口策略可能会阻止所有流量进入 Pod,调整策略将允许所需的服务进行通信。(使用troubles/networkpolicy.yaml来测试此场景。)

  • 节点未准备好或无法访问NotReady错误表示节点由于网络问题等原因未处于准备状态。要进行故障排除,使用kubectl get nodeskubectl describe node <node_name>检查节点状态。此错误也可能由节点污点引起,污点会阻止调度。如果节点有NoSchedule污点,它将不会接受 Pod,直到问题解决或污点被移除。

  • 存储问题:当持久卷声明PVC)正在等待与之匹配的持久卷PV)绑定时,会出现Pending错误。要进行故障排除,请使用kubectl get pvkubectl get pvc检查 PV 和 PVC 的状态。对于 CSI,确保storageClass已正确配置并在 PVC 定义中进行了请求。(检查troubles/pvc.yaml以探索此场景。)

  • 服务不可用Service Unavailable 错误表示服务不可访问,可能是由于配置错误或网络问题。要进行故障排除,请使用 kubectl describe svc <service_name> 查看服务的详细信息。验证服务是否已正确配置,并通过适当的标签指向正确的 Pod。如果服务配置错误,可能无法将流量路由到预期的端点,导致服务不可用。你可以通过 kubectl describe svc <service_name> 命令验证服务的端点(Pods)。

  • API 服务器或控制平面问题:这些错误通常表明与 API 服务器的连接问题,通常是由于控制平面或网络中的问题导致的。由于 API 服务器宕机时 kubectl 命令无法工作,你需要直接登录到运行 API 服务器 Pod 的控制平面服务器。在登录后,可以使用 crictl ps(如果你使用的是 containerd)或 docker ps(如果你使用的是 Docker)等命令检查控制平面组件的状态,以确保 API 服务器 Pod 正常运行。此外,查看日志并检查网络连接,确保所有控制平面组件正常工作。

  • 认证和授权问题Unauthorized 错误表示用户权限或认证问题。要进行故障排除,请使用 kubectl auth can-i <verb> <resource> 验证用户权限。例如,如果用户缺少所需的角色或角色绑定,将遇到授权错误。根据需要调整角色和角色绑定,以授予所需的权限。

  • 资源耗尽ResourceQuota Exceeded 错误表示超出了资源配额,导致无法分配更多资源。要进行故障排除并监控资源使用情况,可以使用 kubectl get quotakubectl top nodeskubectl top pods 命令。如果配额设置过低,可能会阻止新的资源分配。调整资源配额或减少资源使用可以缓解此问题。

  • Ingress 或负载均衡器问题IngressController 失败错误表明 ingress 控制器未正常工作,从而影响流量路由。要进行故障排除,请使用 kubectl describe ingress <ingress_name> 查看 Ingress 详细信息。确保 ingress 控制器已正确安装和配置,并且 ingress 规则正确映射到服务。如果 ingress 规则配置错误,可能会导致流量路由异常。此外,如果在 Ingress 配置中使用了可选的 host 字段,请确保主机名的 DNS 解析已就绪。

这是本书中的最后一个实践示范,接下来我们来总结一下你所学到的内容。

总结

在最后一章中,我们解释了如何使用 Ingress 对象和 Ingress 控制器在 Kubernetes 中实现高级流量路由方法。一开始,我们简单回顾了 Kubernetes 服务类型,刷新了关于 ClusterIPNodePortLoadBalancer 服务对象的知识。基于此,我们介绍了 Ingress 对象和 Ingress 控制器,并解释了它们如何融入 Kubernetes 中的流量路由体系。现在,你知道当需要 L4 负载均衡时,简单的服务通常会被使用,但如果你的应用有 HTTP 或 HTTPS 端点,最好使用 Ingress 和 Ingress 控制器提供的 L7 负载均衡。你学会了如何将 nginx Web 服务器部署为 Ingress 控制器,并在示例部署中进行了测试。

最后,我们解释了如何在具有 L7 负载均衡原生支持的云环境中使用 Ingress 和 Ingress 控制器,尤其是在 Kubernetes 集群外的情况。作为演示,我们部署了一个 AKS 集群,并使用应用网关 Ingress 控制器AGIC)来处理 Ingress 对象。

我们还看到 Kubernetes 如何推进自己成为一个平台,集成诸如 Knative 和 KubeVirt 等前沿技术,扩展 Kubernetes 的能力,涵盖无服务器计算、虚拟机管理和机器学习等领域。我们了解了任何集群管理员必须执行的“第 2 天”操作,包括备份和升级、强化集群的基础安全最佳实践,以及一些关键的故障排除技术,可以帮助解决集群中可能出现的常见问题。这些原则是基本的,基于这些原则,工程师能够安全有效地操作和保护 Kubernetes 环境,确保创新解决方案的持续运行。

恭喜!这段旅程带领我们进入了激动人心的 Kubernetes 和容器编排领域。祝你在 Kubernetes 之路上好运,并感谢你的阅读。

进一步阅读

)

)

)

)

)

)

)

)

)

有关 Kubernetes 自动扩展的更多信息,请参考以下 Packt 出版的书籍:

你还可以参考以下官方文档:

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/cloudanddevops

posted @ 2025-06-30 19:29  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报