Kubernetes-研讨会-全-

Kubernetes 研讨会(全)

原文:zh.annas-archive.org/md5/DFC15E6DFB274E63E53841C0858DE863

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

由于其广泛的支持,可以管理数百个运行云原生应用程序的容器,Kubernetes 是最受欢迎的开源容器编排平台,使集群管理变得简单。本研讨会采用实用方法,让您熟悉 Kubernetes 环境及其应用。

从介绍 Kubernetes 的基础知识开始,您将安装和设置 Kubernetes 环境。您将了解如何编写 YAML 文件并部署您的第一个简单的 Web 应用程序容器使用 Pod。然后,您将为 Pod 分配人性化的名称,探索各种 Kubernetes 实体和功能,并发现何时使用它们。随着您逐章阅读,这本 Kubernetes 书将向您展示如何通过应用各种技术来设计组件和部署集群,充分利用 Kubernetes。您还将掌握限制对集群内部某些功能访问的安全策略。在本书的最后,您将了解构建自己的控制器和升级到 Kubernetes 集群的高级功能,而无需停机。

通过本研讨会,您将能够使用 Kubernetes 高效地管理容器并运行基于云的应用程序。

受众

无论您是新手网页编程世界,还是经验丰富的开发人员或软件工程师,希望使用 Kubernetes 来管理和扩展容器化应用程序,您都会发现这个研讨会很有用。要充分利用本书,需要对 Docker 和容器化有基本的了解。

关于章节

第一章Kubernetes 和容器简介,从容器化技术以及支持容器化的各种基础 Linux 技术开始。该章节介绍了 Kubernetes,并阐明了它带来的优势。

第二章Kubernetes 概述,为您提供了对 Kubernetes 的第一次实际介绍,并概述了 Kubernetes 的架构。

第三章kubectl - Kubernetes 命令中心,介绍了使用 kubectl 的各种方式,并强调了声明式管理的原则。

第四章如何与 Kubernetes(API 服务器)通信,深入介绍了 Kubernetes API 服务器的细节以及与其通信的各种方式。

第五章,“Pods”,介绍了部署任何应用程序所使用的基本 Kubernetes 对象。

第六章,“标签和注释”,涵盖了 Kubernetes 中用于对不同对象进行分组、分类和链接的基本机制。

第七章,“Kubernetes 控制器”,介绍了各种 Kubernetes 控制器,如部署和有状态集等,它们是声明式管理方法的关键推动者之一。

第八章,“服务发现”,描述了如何使不同的 Kubernetes 对象在集群内以及集群外可被发现。

第九章,“存储和读取磁盘上的数据”,解释了 Kubernetes 提供的各种数据存储抽象,以使应用程序能够在磁盘上读取和存储数据。

第十章,“ConfigMaps 和 Secrets”,教会你如何将应用程序配置数据与应用程序本身分离开来,同时看到采取这种方法的优势。

第十一章,“构建您自己的 HA 集群”,指导您在亚马逊网络服务AWS)平台上设置自己的高可用性、多节点 Kubernetes 集群。

第十二章,“您的应用程序和 HA”,阐述了使用 Kubernetes 进行持续集成的一些概念,并演示了在亚马逊弹性 Kubernetes 服务上运行的高可用性、多节点、托管 Kubernetes 集群的一些方法。

第十三章,“Kubernetes 中的运行时和网络安全性”,概述了应用程序和集群可能受到攻击的方式,然后介绍了 Kubernetes 提供的访问控制和安全功能。

第十四章,“在 Kubernetes 中运行有状态的组件”,教会你如何正确使用不同的 Kubernetes 抽象来可靠地部署有状态的应用程序。

第十五章,“Kubernetes 中的监控和自动扩展”,涵盖了您可以监视不同 Kubernetes 对象的方式,然后利用这些信息来扩展集群的容量。

第十六章,“Kubernetes Admission Controllers”,描述了 Kubernetes 如何允许我们扩展 API 服务器提供的功能,以在 API 服务器接受请求之前实施自定义策略。

第十七章,“Kubernetes 中的高级调度”,描述了调度器如何在 Kubernetes 集群上放置 pod。您将使用高级功能来影响 pod 的调度器放置决策。

第十八章,“无停机升级您的集群”,教会您如何将您的 Kubernetes 平台升级到新版本,而不会对您的平台或应用程序造成任何停机时间。

第十九章,“Kubernetes 中的自定义资源定义”,向您展示了扩展 Kubernetes 提供的功能的主要方式之一。您将看到自定义资源如何允许您在集群上实现特定于您自己领域的概念。

注意

章节中提出的活动的解决方案可以在此地址找到:packt.live/304PEoD

约定

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入显示如下:“在当前工作目录中创建名为sample-pod.yaml的文件。”

代码块、终端命令或创建 YAML 文件的文本设置如下:

kubectl -n webhooks create secret tls webhook-server-tls \
--cert "tls.crt" \
--key "tls.key"

新的重要单词会显示为:“Kubernetes 通过Admission Controllers提供了这种能力。”

代码片段的关键部分如下所示:

kind: Pod
metadata:
  name: infra-libraries-application-staging
  namespace: metadata-activity
  labels:
    environment: staging
    team: infra-libraries
  annotations:
      team-link: "https://jira-link/team-link-2"
spec:
  containers:

您在屏幕上看到的文字,例如菜单或对话框中的文字,会以如下方式出现在文本中:“在左侧边栏上,点击配置,然后点击数据源。”

长代码片段已被截断,并且在截断的代码顶部放置了 GitHub 上代码文件的相应名称。完整代码的永久链接放置在代码片段下方。它应该如下所示:

mutatingcontroller.go

46 //create the response with patch bytes 
47 var admissionResponse *v1beta1.AdmissionResponse 
48 admissionResponse = &v1beta1.AdmissionResponse { 
49     allowed: true, 
50     Patch:   patchBytes, 
51     PatchType: func() *v1beta1.PatchType { 
52         pt := v1beta1.PatchTypeJSONPatch 
53         return &pt 
54     }(), 
55 } 

此示例的完整代码可以在packt.live/35ieNiX找到。

设置您的环境

在我们详细探讨本书之前,我们需要设置特定的软件和工具。在接下来的部分中,我们将看到如何做到这一点。

硬件要求

您需要至少具有虚拟化支持的双核 CPU、4GB 内存和 20GB 可用磁盘空间。

操作系统要求

我们推荐的操作系统是 Ubuntu 20.04 LTS 或 macOS 10.15。如果您使用 Windows,可以双启动 Ubuntu。我们已在本节末尾提供了相关说明。

虚拟化

您需要在硬件和操作系统上启用虚拟化功能。

在 Linux 中,您可以运行以下命令来检查虚拟化是否已启用:

grep -E --color 'vmx|svm' /proc/cpuinfo

您应该收到此命令的非空响应。如果您收到空响应,则表示您未启用虚拟化。

在 macOS 中,运行以下命令:

sysctl -a | grep -E --color 'machdep.cpu.features|VMX'

如果虚拟化已启用,你应该能够在输出中看到VMX

注意

如果你的主机环境是虚拟化的,你将无法按照本书中的说明进行操作,因为 Minikube(默认情况下)在虚拟机中运行所有 Kubernetes 组件,如果主机环境本身是虚拟化的,则无法工作。虽然可以在没有虚拟化程序的情况下使用 Minikube,但你的结果有时可能与本书中的演示不同。因此,我们建议直接在你的机器上安装其中一个推荐的操作系统。

安装和设置

本节列出了本书所需软件的安装说明。由于我们推荐使用 Ubuntu,我们将使用 APT 软件包管理器在 Ubuntu 中安装大部分所需软件。

对于 macOS,我们建议你使用 Homebrew 来方便。你可以通过在终端中运行此脚本来安装它:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

此脚本的终端输出将显示将应用的更改,然后要求你确认。确认后,安装就可以完成了。

更新你的软件包列表

在 Ubuntu 中使用 APT 安装任何软件包之前,请确保你的软件包列表是最新的。使用以下命令:

sudo apt update

此外,你可以使用以下命令升级你机器上的任何可升级软件包:

sudo apt upgrade

同样,在 macOS 的情况下,使用以下命令更新 Homebrew 的软件包列表:

brew update

安装 Git

这个研讨会的代码包在我们的 GitHub 存储库中可用。你可以使用 Git 克隆存储库以获取所有代码文件。

在 Ubuntu 上安装 Git,请使用以下命令:

sudo apt install git-all

如果你在 macOS 上使用 Xcode,很可能已经安装了 Git。你可以通过运行此命令来检查:

git --version

如果出现“命令未找到”错误,则表示你没有安装它。你可以使用 Homebrew 来安装,使用以下命令:

brew install git

jq

jq 是一个 JSON 解析器,对于从 JSON 格式的 API 响应中提取任何信息非常有用。你可以使用以下命令在 Ubuntu 上安装它:

sudo apt install jq

你可以使用以下命令在 macOS 上进行安装:

brew install jq

Tree

Tree 是一个包,可以让你在终端中看到目录结构。你可以使用以下命令在 Ubuntu 上安装它:

sudo apt install tree

你可以使用以下命令在 macOS 上进行安装:

brew install tree

AWS CLI

AWS 命令行工具是一个 CLI 工具,您可以从终端使用它来管理您的 AWS 资源。您可以使用此 URL 中的安装说明进行安装:docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html

Minikube 和 kubectl

Minikube 允许我们为学习和测试目的创建一个单节点 Kubernetes 集群。kubectl 是一个命令行接口工具,允许我们与我们的集群进行通信。您将在第二章 Kubernetes 概述中找到这些工具的详细安装说明。

即使您已经安装了 Minikube,我们建议您使用第二章 Kubernetes 概述中指定的版本,以确保本书中所有说明的可重复性。

Minikube 需要您安装一个 hypervisor。我们将使用 VirtualBox。

VirtualBox

VirtualBox 是一个开源的 hypervisor,可以被 Minikube 用来为我们的集群虚拟化一个节点。使用以下命令在 Ubuntu 上安装 VirtualBox:

sudo apt install virtualbox

对于 macOS 的安装,请首先从此链接获取适当的文件:

www.virtualbox.org/wiki/Downloads.

然后,按照此处提到的安装说明进行操作:

www.virtualbox.org/manual/ch02.html#installation-mac.

Docker

Docker 是 Kubernetes 使用的默认容器化引擎。您将在第一章 Kubernetes 和容器简介中了解更多关于 Docker 的信息。

要安装 Docker,请按照此链接中的安装说明进行操作:

docs.docker.com/engine/install/.

要在 Mac 中安装 Docker,请按照以下链接中的安装说明进行操作:

docs.docker.com/docker-for-mac/install/.

要在 Ubuntu 中安装 Docker,请按照以下链接中的安装说明进行操作:

docs.docker.com/engine/install/ubuntu/.

Go

Go 是一种用于构建本书中演示的应用程序的编程语言。此外,Kubernetes 也是用 Go 编写的。要在您的机器上安装 Go,请使用以下命令进行 Ubuntu 的安装:

sudo apt install golang-go

对于 macOS 的安装,请使用以下说明:

  1. 使用以下命令安装 Go:
brew install golang

注意

该代码已经在 Go 版本 1.13 和 1.14 上进行了测试。请确保您拥有这些版本,尽管代码预计将适用于所有 1.x 版本。

  1. 现在,我们需要设置一些环境变量。使用以下命令:
mkdir - p $HOME/go
export GOPATH=$HOME/go
export GOROOT="$(brew --prefix golang)/libexec"
export PATH="$PATH:${GOPATH}/bin:${GOROOT}/bin"

kops

kops 是一个命令行接口工具,允许您在 AWS 上设置 Kubernetes 集群。使用 kops 安装 Kubernetes 的实际过程在第十一章“构建您自己的 HA 集群”中有所涵盖。为了确保本书中给出的说明的可重复性,我们建议您安装 kops 版本 1.15.1。

要在 Ubuntu 上安装,请按照以下步骤进行:

  1. 使用以下命令下载 kops 版本 1.15.1 的二进制文件:
curl -LO https://github.com/kubernetes/kops/releases/download/1.15.0/kops-linux-amd64
  1. 现在,使用以下命令使二进制文件可执行:
chmod +x kops-linux-amd64
  1. 将可执行文件添加到您的路径:
sudo mv kops-linux-amd64 /usr/local/bin/kops
  1. 通过运行以下命令检查 kops 是否已成功安装:
kops version

如果 kops 已成功安装,您应该会得到一个声明版本为 1.15.0 的响应。

要在 macOS 上安装,请按照以下步骤进行:

  1. 使用以下命令下载 kops 版本 1.15.1 的二进制文件:
curl -LO https://github.com/kubernetes/kops/releases/download/1.15.0/kops-darwin-amd64
  1. 现在,使用以下命令使二进制文件可执行:
chmod +x kops-darwin-amd64
  1. 将可执行文件添加到您的路径:
sudo mv kops-darwin-amd64 /usr/local/bin/kops
  1. 通过运行以下命令检查 kops 是否已成功安装:
kops version

如果 kops 已成功安装,您应该会得到一个声明版本为 1.15.0 的响应。

为 Windows 用户双引导 Ubuntu

在本节中,您将找到有关如何在运行 Windows 的计算机上双引导 Ubuntu 的说明。

注意

在安装任何操作系统之前,强烈建议您备份系统状态以及所有数据。

调整分区大小

如果您的计算机上安装了 Windows,那么您的硬盘很可能已完全被使用-也就是说,所有可用空间都已被分区和格式化。我们需要在硬盘上有一些未分配的空间。因此,我们将调整一个有大量空闲空间的分区的大小,以便为我们的 Ubuntu 分区腾出空间:

  1. 打开计算机管理实用程序。按下Win + R并输入compmgmt.msc图 0.1:Windows 上的计算机管理实用程序

图 0.1:Windows 上的计算机管理实用程序

  1. 在左侧窗格中,转到存储 > 磁盘管理选项,如下所示:图 0.2:磁盘管理

图 0.2:磁盘管理

您将在屏幕下半部分看到所有分区的摘要。您还可以看到与所有分区相关的驱动器号以及有关 Windows 引导驱动器的信息。如果您有一个有大量可用空间(20 GB +)的分区,既不是引导驱动器(C:),也不是恢复分区,也不是 EFI 系统分区,那么这将是选择的理想选项。如果没有这样的分区,那么您可以调整C:驱动器。

  1. 在这个例子中,我们将选择D:驱动器。您可以右键单击任何分区并打开属性来检查可用的空间:图 0.3:检查 D:驱动器的属性

图 0.3:检查 D:驱动器的属性

现在,在我们调整分区大小之前,我们需要确保文件系统没有错误或任何硬件故障。我们将使用 Windows 上的chkdsk实用程序来做到这一点。

  1. 通过按Win + R并输入cmd.exe来打开命令提示符。现在,运行以下命令:
chkdsk D: /f

用您想要使用的驱动器号替换驱动器号。您应该看到类似以下的响应:

图 0.4:扫描驱动器以查找任何文件系统错误

图 0.4:扫描驱动器以查找任何文件系统错误

请注意,在此截图中,Windows 报告已扫描文件系统并未发现问题。如果您的情况遇到任何问题,您应该先解决这些问题,以防止数据丢失。

  1. 现在,回到计算机管理窗口,右键单击所需的驱动器,然后单击收缩卷,如下所示:图 0.5:打开收缩卷对话框

图 0.5:打开收缩卷对话框

  1. 在提示窗口中,输入您想要清除的空间量在唯一可以编辑的字段中。在这个例子中,我们通过收缩我们的D:驱动器来清除大约 25 GB 的磁盘空间:图 0.6:通过收缩现有卷清除 25 GB

图 0.6:通过收缩现有卷清除 25 GB

  1. 收缩驱动器后,您应该能够在驱动器上看到未分配的空间,如下所示:图 0.7:收缩卷后的未分配空间

图 0.7:收缩卷后的未分配空间

现在我们准备安装 Ubuntu。但首先,我们需要下载它并创建一个可启动的 USB,这是最方便的安装介质之一。

创建一个可启动的 USB 驱动器来安装 Ubuntu

您需要一个至少容量为 4GB 的闪存驱动器。请注意,其中的所有数据将被删除:

  1. 从这个链接下载 Ubuntu 桌面的 ISO 映像:releases.ubuntu.com/20.04/

  2. 接下来,我们需要将 ISO 映像烧录到 USB 闪存盘并创建一个可启动的 USB 驱动器。有许多可用的工具,您可以使用其中任何一个。在本例中,我们使用的是免费开源的 Rufus。您可以从这个链接获取它:www.fosshub.com/Rufus.html

  3. 安装了 Rufus 后,插入您的 USB 闪存盘并打开 Rufus。确保选择了正确的Device选项,如下面的屏幕截图所示。

  4. Boot selection下按SELECT按钮,然后打开您下载的 Ubuntu 18.04 映像。

  5. 分区方案的选择将取决于您的 BIOS 和磁盘驱动器的配置方式。对于大多数现代系统来说,GPT将是最佳选择,而MBR将兼容较旧的系统:图 0.8:Rufus 的配置

图 0.8:Rufus 的配置

  1. 您可以将所有其他选项保持默认,然后按START。完成后,关闭 Rufus。您现在有一个可启动的 USB 驱动器,准备安装 Ubuntu。

安装 Ubuntu

现在,我们将使用可启动的 USB 驱动器来安装 Ubuntu:

  1. 安装 Ubuntu,使用我们刚刚创建的可启动安装介质进行引导。在大多数情况下,您应该可以通过在启动机器时插入 USB 驱动器来实现这一点。如果您没有自动引导到 Ubuntu 设置,请进入 BIOS 设置,并确保您的 USB 设备处于最高的引导优先级,并且安全启动已关闭。输入 BIOS 设置的说明通常显示在启动计算机时显示的闪屏(即您的个人电脑制造商标志的屏幕)上。您也可以在启动时选择进入启动菜单的选项。通常情况下,您必须在 PC 启动时按住DeleteF1F2F12或其他一些键。这取决于您主板的 BIOS。

您应该看到一个带有“尝试 Ubuntu”或“安装 Ubuntu”选项的屏幕。如果您没有看到这个屏幕,而是看到一个以“最小的 BASH 类似行编辑支持…”开头的消息的 shell,那么很可能在下载 ISO 文件或创建可启动的 USB 驱动器时可能出现了一些数据损坏。通过计算您下载文件的MD5SHA1SHA256哈希值来检查下载的 ISO 文件的完整性,并将其与 Ubuntu 下载页面上的文件MD5SUMSSHA1SUMSSHA256SUMS中的哈希值进行比较。然后,重复上一节中的步骤重新格式化和重新创建可启动的 USB 驱动器。

如果您已经在 BIOS 中将最高启动优先级设置为正确的 USB 设备,但仍然无法使用 USB 设备启动(您的系统可能会忽略它而启动到 Windows),那么可能有两个最常见的问题:

  • USB 驱动器未正确配置为可识别的可启动设备,或者 GRUB 引导加载程序未正确设置。验证您下载的镜像的完整性并重新创建可启动的 USB 驱动器应该在大多数情况下解决这个问题。

  • 您选择了错误的“分区方案”选项适用于您的系统配置。尝试另一个选项并重新创建 USB 驱动器。

  1. 使用 USB 驱动器启动计算机后,选择“安装 Ubuntu”。

  2. 选择您想要的语言,然后按“继续”。

  3. 在下一个屏幕上,选择适当的键盘布局,然后继续到下一个屏幕。

  4. 在下一个屏幕上,选择“普通安装”。

勾选“在安装 Ubuntu 时下载更新”和“为图形和 Wi-Fi 硬件以及其他媒体格式安装第三方软件”选项。

然后,继续到下一个屏幕。

  1. 在下一个屏幕上,选择“在 Windows 引导管理器旁边安装 Ubuntu”,然后点击“立即安装”。您将看到一个提示描述 Ubuntu 将对您的系统进行的更改,例如将创建的新分区。确认更改并继续到下一个屏幕。

  2. 在下一个屏幕上,选择您的地区,然后按“继续”。

  3. 在下一个屏幕上,设置您的姓名(可选)、用户名、计算机名和密码,然后按“继续”。

安装现在应该开始了。根据您的系统配置,这将需要一些时间。安装完成后,您将收到提示重新启动计算机。拔掉您的 USB 驱动器,然后点击“立即重启”。

如果您忘记拔掉 USB 驱动器,可能会重新启动到 Ubuntu 安装界面。在这种情况下,只需退出安装程序。如果已启动 Ubuntu 的实例,请重新启动您的机器。这次记得拔掉 USB 驱动器。

如果重新启动后,您直接进入 Windows 而没有选择操作系统的选项,可能的问题是 Ubuntu 安装的 GRUB 引导加载程序没有优先于 Windows 引导加载程序。在某些系统中,硬盘上引导加载程序的优先级是在 BIOS 中设置的。您需要在 BIOS 设置菜单中找到适当的设置。它可能被命名为类似于UEFI 硬盘驱动器优先级的内容。确保将GRUB/Ubuntu设置为最高优先级。

其他要求

Docker Hub 账户:您可以在此链接创建免费的 Docker 账户:hub.docker.com/

AWS 账户:您将需要自己的 AWS 账户以及一些关于使用 AWS 的基本知识。您可以在此处创建一个账户:aws.amazon.com/

注意

本书中的练习和活动要求超出了 AWS 的免费套餐范围,因此您应该知道您将因使用云服务而产生费用。您可以在此处查看定价信息:aws.amazon.com/pricing/

访问代码文件

您可以在packt.live/3bE3zWY找到本书的完整代码文件。

安装 Git 后,您可以使用以下命令克隆存储库:

git clone https://github.com/PacktWorkshops/Kubernetes-Workshop
cd Kubernetes-Workshop

如果您在安装过程中遇到任何问题或有任何疑问,请发送电子邮件至workshops@packt.com

第一章: Kubernetes 和容器简介

概述

本章首先描述了软件开发和交付的演变,从在裸机上运行软件,到现代容器化方法。我们还将看一下支持容器化的底层 Linux 技术。在本章结束时,您将能够从镜像中运行基本的 Docker 容器。您还将能够打包自定义应用程序以制作自己的 Docker 镜像。接下来,我们将看一下如何控制容器的资源限制和分组。最后,本章结束时描述了为什么我们需要像 Kubernetes 这样的工具,以及对其优势的简短介绍。

介绍

大约十年前,关于服务导向架构、敏捷开发和软件设计模式等软件开发范式进行了大量讨论。回顾来看,这些都是很好的想法,但只有少数被实际采纳了十年前。

这些范式缺乏采纳的一个主要原因是底层基础设施无法提供资源或能力来抽象细粒度的软件组件,并管理最佳的软件开发生命周期。因此,仍然需要大量重复的工作来解决软件开发的一些常见问题,如管理软件依赖关系和一致的环境、软件测试、打包、升级和扩展。

近年来,以 Docker 为首的容器技术提供了一种新的封装机制,允许您捆绑应用程序、其运行时和其依赖项,并为软件开发带来了新的视角。通过使用容器技术,底层基础设施被抽象化,以便应用程序可以在异构环境中无缝移动。然而,随着容器数量的增加,您可能需要编排工具来帮助您管理它们之间的交互,以及优化底层硬件的利用率。

这就是 Kubernetes 发挥作用的地方。Kubernetes 提供了各种选项来自动化部署、扩展和管理容器化应用程序。它近年来得到了爆炸式的采用,并已成为容器编排领域的事实标准。

作为本书的第一章,我们将从过去几十年软件开发的简要历史开始,然后阐述容器和 Kubernetes 的起源。我们将重点解释它们可以解决什么问题,以及它们为什么在最近几年的采用率大幅上升的三个关键原因

软件开发的演变

随着虚拟化技术的发展,公司通常使用虚拟机VMs)来管理其软件产品,无论是在公共云还是本地环境中。这带来了诸如自动机器配置、更好的硬件资源利用、资源抽象等巨大好处。更为重要的是,它首次采用了计算、网络和存储资源的分离,使软件开发摆脱了硬件管理的繁琐。虚拟化还带来了以编程方式操纵底层基础设施的能力。因此,从系统管理员和开发人员的角度来看,他们可以更好地优化软件维护和开发的工作流程。这是软件开发历史上的一大进步。

然而,在过去的十年中,软件开发的范围和生命周期发生了巨大变化。以前,将软件开发成大型的单块是很常见的,发布周期很慢。如今,为了跟上业务需求的快速变化,一款软件可能需要被拆分成个别的细粒度子组件,并且每个组件可能需要有自己的发布周期,以便尽可能频繁地发布,以便更早地从市场获得反馈。此外,我们可能希望每个组件都具有可伸缩性和成本效益。

那么,这对应用程序开发和部署有什么影响呢?与裸机时代相比,采用虚拟机并没有太大帮助,因为虚拟机并没有改变不同组件管理的粒度;整个软件仍然部署在一台机器上,只不过是虚拟机而不是物理机。使一些相互依赖的组件共同工作仍然不是一件容易的事情。

这里的一个直接的想法是添加一个抽象层,将机器与运行在其上的应用程序连接起来。这样应用程序开发人员只需要专注于业务逻辑来构建应用程序。一些例子包括 Google App Engine(GAE)和 Cloud Foundry。

这些解决方案的第一个问题是不同环境之间缺乏一致的开发体验。开发人员在他们的机器上开发和测试应用程序,使用他们本地的依赖关系(无论是在编程语言还是操作系统级别);而在生产环境中,应用程序必须依赖另一组底层依赖关系。而且我们还没有谈到需要不同团队中不同开发人员合作的软件组件。

第二个问题是应用程序和底层基础设施之间的硬性边界会限制应用程序的高性能,特别是如果应用程序对存储、计算或网络资源敏感。例如,您可能希望应用程序部署在多个可用区(数据中心内的隔离地理位置,云资源在其中管理),或者您可能希望一些应用程序共存,或者不与其他特定应用程序共存。或者,您可能希望一些应用程序遵循特定的硬件(例如固态驱动器)。在这种情况下,很难专注于应用程序的功能,而不向上层应用程序暴露基础设施的拓扑特征。

事实上,在软件开发的生命周期中,基础设施和应用程序之间没有明确的界限。我们想要实现的是自动管理应用程序,同时最大限度地利用基础设施。

那么,我们如何实现这一点呢?Docker(我们将在本章后面介绍)通过利用 Linux 容器化技术来解决第一个问题,封装应用程序及其依赖关系。它还引入了 Docker 镜像的概念,使应用程序运行时环境的软件方面变得轻量、可重现和可移植。

第二个问题更加复杂。这就是 Kubernetes 发挥作用的地方。Kubernetes 利用一种经过考验的设计理念,称为声明式 API,来抽象基础设施以及应用交付的每个阶段,如部署、升级、冗余、扩展等。它还为用户提供了一系列构建模块,供用户选择、编排并组合成最终的应用程序。我们将逐渐开始学习 Kubernetes,这是本书的核心内容,在本章末尾。

注意

如果没有特别指定,本书中可能会将术语“容器”与“Linux 容器”互换使用。

虚拟机与容器

虚拟机VM),顾名思义,旨在模拟物理计算机系统。从技术上讲,虚拟机是由虚拟化监控程序提供的,并且虚拟化监控程序运行在主机操作系统上。下图说明了这个概念:

图 1.1:在虚拟机上运行应用程序

图 1.1:在虚拟机上运行应用程序

在这里,虚拟机具有完整的操作系统堆栈,虚拟机上运行的操作系统(称为“客户操作系统”)必须依赖底层的虚拟化监控程序才能运行。应用程序和操作系统驻留并在虚拟机内运行。它们的操作经过客户操作系统的内核,然后由虚拟化监控程序翻译成系统调用,最终在主机操作系统上执行。

另一方面,容器不需要底层的虚拟化监控程序。通过利用一些 Linux 容器化技术,如命名空间和 cgroups(我们稍后会重新讨论),每个容器都可以独立地在主机操作系统上运行。下图说明了容器化,以 Docker 容器为例:

图 1.2:在容器中运行应用程序

图 1.2:在容器中运行应用程序

值得一提的是,我们将 Docker 放在容器旁边,而不是在容器和主机操作系统之间。这是因为从技术上讲,没有必要让 Docker 引擎托管这些容器。Docker 引擎更多地扮演着一个管理者的角色,来管理容器的生命周期。将 Docker 引擎比作虚拟化监控程序也是不恰当的,因为一旦容器启动运行,我们就不需要额外的层来“翻译”应用程序操作,使其能够被主机操作系统理解。从图 1.2中,你也可以看出容器内的应用程序实质上是直接在主机操作系统上运行的。

当我们启动一个容器时,我们不需要启动整个操作系统;相反,它利用了主机操作系统上 Linux 内核的特性。因此,与虚拟机相比,容器启动更快,功能开销更小,占用的空间也要少得多。以下是一个比较虚拟机和容器的表格:

图 1.3:虚拟机和容器的比较

图 1.3:虚拟机和容器的比较

从这个比较来看,容器在所有方面都胜过虚拟机,除了隔离性。容器所利用的 Linux 容器技术并不新鲜。关键的 Linux 内核特性,命名空间和 cgroup(我们将在本章后面学习)已经存在了十多年。在 Docker 出现之前,还有一些旧的容器实现,如 LXC 和 Cloud Foundry Warden。现在,一个有趣的问题是:鉴于容器技术有这么多好处,为什么它在最近几年才被采用,而不是十年前?我们将在接下来的章节中找到这个问题的一些答案。

Docker 基础知识

到目前为止,我们已经看到了容器化相对于在虚拟机上运行应用程序提供的不同优势。Docker 是目前最常用的容器化技术。在本节中,我们将从一些 Docker 基础知识开始,并进行一些练习,让您亲身体验使用 Docker 的工作。

注意

除了 Docker 之外,还有其他容器管理器,如 containerd 和 podman。它们在功能和用户体验方面表现不同,例如,containerd 和 podman 被称为比 Docker 更轻量级,比 Kubernetes 更合适。然而,它们都符合Open Container Initiatives (OCI)标准,以确保容器镜像兼容。

尽管 Docker 可以安装在任何操作系统上,但你应该知道,在 Windows 和 macOS 上,它实际上创建了一个 Linux 虚拟机(或者在 macOS 中使用类似的虚拟化技术,如 HyperKit),并将 Docker 嵌入到虚拟机中。在本章中,我们将使用 Ubuntu 18.04 LTS 作为操作系统,以及 Docker Community Edition 18.09.7。

在继续之前,请确保按照前言中的说明安装了 Docker。您可以通过使用以下命令查询 Docker 的版本来确认 Docker 是否已安装:

docker --version

您应该看到以下输出:

Docker version 18.09.7, build 2d0083d

注意

以下部分中的所有命令都是以root身份执行的。在终端中输入sudo -s,然后在提示时输入管理员密码,以获取 root 访问权限。

docker run 背后是什么?

安装 Docker 后,运行容器化应用程序非常简单。为了演示目的,我们将使用 Nginx web 服务器作为示例应用程序。我们可以简单地运行以下命令来启动 Nginx 服务器:

docker run -d nginx

你应该看到类似的结果:

图 1.4:启动 Nginx

图 1.4:启动 Nginx

这个命令涉及几个动作,描述如下:

  1. docker run告诉 Docker 引擎运行一个应用程序。

  2. -d参数(--detach的缩写)强制应用程序在后台运行,这样你就看不到应用程序在终端的输出。相反,你必须运行docker logs <container ID>来隐式获取输出。

注意

“分离”模式通常意味着应用程序是一个长时间运行的服务。

  1. 最后一个参数nginx表示应用程序所基于的镜像名称。该镜像封装了 Nginx 程序及其依赖项。

输出日志解释了一个简要的工作流程:首先,它尝试在本地获取nginx镜像,但失败了,所以它从公共镜像仓库(稍后我们将重新讨论的 Docker Hub)中检索了镜像。一旦镜像在本地下载完成,它就使用该镜像启动一个实例,然后输出一个 ID(在前面的示例中,这是96c374…),用于标识运行中的实例。正如你所看到的,这是一个十六进制字符串,你可以在实践中使用前四个或更多的唯一字符来引用任何实例。你应该看到,即使docker命令的终端输出也会截断 ID。

可以使用以下命令验证运行实例:

docker ps

你应该看到以下结果:

图 1.5:获取所有正在运行的 Docker 容器的列表

图 1.5:获取所有正在运行的 Docker 容器的列表

docker ps命令列出所有正在运行的容器。在前面的示例中,只有一个名为nginx的容器正在运行。与在物理机器或虚拟机上本地运行的典型 Nginx 发行版不同,nginx容器以隔离的方式运行。nginx容器默认不会在主机端口上公开其服务。相反,它在其容器的端口上提供服务,这是一个隔离的实体。我们可以通过调用容器 IP 的端口80来访问nginx服务。

首先,让我们通过运行以下命令获取容器 IP:

docker inspect --format '{{.NetworkSettings.IPAddress}}' <Container ID or NAME>

您应该看到以下输出(具体内容可能因您的本地环境而异):

172.17.0.2

正如您所看到的,在这种情况下,nginx容器的 IP 地址为172.17.0.2。让我们通过在端口80上访问此 IP 来检查 Nginx 是否有响应:

curl <container IP>:80

您应该看到以下输出:

图 1.6:Nginx 容器的响应

图 1.6:Nginx 容器的响应

正如您在图 1.6中所看到的,我们得到了一个响应,它显示在终端上作为默认主页的源 HTML。

通常,我们不依赖内部 IP 来访问服务。更实际的方法是在主机的某个端口上暴露服务。要将主机端口8080映射到容器端口80,请使用以下命令:

docker run -p 8080:80 -d nginx

您应该看到类似的响应:

39bf70d02dcc5f038f62c276ada1675c25a06dd5fb772c5caa19f02edbb0622a

-p 8080:80参数告诉 Docker Engine 启动容器并将主机端口 8080 上的流量映射到容器内部的端口80。现在,如果我们尝试在端口8080上访问localhost,我们将能够访问容器化的nginx服务。让我们试一试:

curl localhost:8080

您应该看到与图 1.6中相同的输出。

Nginx 是一种没有固定终止时间的工作负载的示例,也就是说,它不仅仅显示输出然后终止。这也被称为长时间运行的服务。另一种工作负载,只是运行到完成并退出的类型,称为短时间服务,或简称为作业。对于运行作业的容器,我们可以省略-d参数。以下是作业的一个示例:

docker run hello-world

您应该看到以下响应:

图 1.7:运行 hello-world 镜像

图 1.7:运行 hello-world 镜像

现在,如果您运行docker ps,这是用于列出运行中容器的命令,它不会显示hello-world容器。这是预期的,因为容器已经完成了它的工作(即,打印出我们在上一个截图中看到的响应文本)并退出了。为了能够找到已退出的容器,您可以使用相同的命令加上-a标志运行,这将显示所有容器:

docker ps -a

您应该看到以下输出:

图 1.8:检查我们的已退出容器

图 1.8:检查我们的已退出容器

对于已停止的容器,您可以使用docker rm <container ID>删除它,或者使用docker run <container ID>重新运行它。或者,如果您重新运行docker run hello-world,它将再次启动一个新的容器,并在完成工作后退出。您可以按照以下步骤自行尝试:

docker run hello-world
docker ps -a

您应该看到以下输出:

图 1.9:检查多个已退出的容器

图 1.9:检查多个已退出的容器

因此,您可以看到基于相同基础镜像运行多个容器是非常简单的。

到目前为止,您应该对容器是如何启动以及如何检查其状态有了非常基本的了解。

Dockerfile 和 Docker 镜像

在虚拟机时代,没有标准或统一的方式来抽象和打包各种类型的应用程序。传统的方法是使用工具,比如 Ansible,来管理每个应用程序的安装和更新过程。这种方法现在仍在使用,但它涉及大量的手动操作,并且由于不同环境之间的不一致性而容易出错。从开发人员的角度来看,应用程序是在本地机器上开发的,这与分级和最终生产环境大不相同。

那么,Docker 是如何解决这些问题的呢?它带来的创新被称为Dockerfile和 Docker 镜像。Dockerfile是一个文本文件,它抽象了一系列指令来构建一个可重现的环境,包括应用程序本身以及所有的依赖项。

通过使用docker build命令,Docker 使用Dockerfile生成一个名为 Docker 镜像的标准化实体,您可以在几乎任何操作系统上运行它。通过利用 Docker 镜像,开发人员可以在与生产环境相同的环境中开发和测试应用程序,因为依赖项被抽象化并捆绑在同一个镜像中。让我们退一步,看看我们之前启动的nginx应用程序。使用以下命令列出所有本地下载的镜像:

docker images

您应该看到以下列表:

图 1.10:获取镜像列表

图 1.10:获取镜像列表

与虚拟机镜像不同,Docker 镜像只捆绑必要的文件,如应用程序二进制文件、依赖项和 Linux 根文件系统。在内部,Docker 镜像被分成不同的层,每个层都堆叠在另一个层上。这样,升级应用程序只需要更新相关的层。这既减少了镜像的占用空间,也减少了升级时间。

以下图显示了一个假想的 Docker 镜像的分层结构,该镜像是从基本操作系统层(Ubuntu)、Java Web 应用程序运行时层(Tomcat)和最顶层的用户应用程序层构建而成:

图 1.11:容器中堆叠层的示例

图 1.11:容器中堆叠层的示例

请注意,通常会使用流行操作系统的镜像作为构建 Docker 镜像的起点(正如您将在以下练习中看到的),因为它方便地包含了开发应用程序所需的各种组件。在上述假设的容器中,应用程序将使用 Tomcat 以及 Ubuntu 中包含的一些依赖项才能正常运行。这是将 Ubuntu 包含为基础层的唯一原因。如果我们愿意,我们可以在不包含整个 Ubuntu 基础镜像的情况下捆绑所需的依赖项。因此,不要将其与虚拟机的情况混淆,虚拟机需要包含一个客户操作系统的情况。

让我们看看如何在以下练习中为我们自己构建一个 Docker 镜像。

练习 1.01:创建 Docker 镜像并将其上传到 Docker Hub

在这个练习中,我们将为一个用 Go 语言编写的简单应用程序构建一个 Docker 镜像。

在这个练习中,我们将使用 Go,这样源代码和它的语言依赖可以编译成一个可执行的二进制文件。然而,你可以自由选择任何你喜欢的编程语言;只要记得如果你要使用 Java、Python、Node.js 或任何其他语言,就要捆绑语言运行时依赖。

  1. 在这个练习中,我们将创建一个名为Dockerfile的文件。请注意,这个文件名没有扩展名。你可以使用你喜欢的文本编辑器创建这个文件,内容如下:
FROM alpine:3.10
COPY k8s-for-beginners /
CMD ["/k8s-for-beginners"]

注意

从终端,无论你是使用 vim 或 nano 这样的简单文本编辑器,还是使用cat命令创建文件,它都会被创建在当前工作目录中,无论是在任何 Linux 发行版还是 macOS 中。当你打开终端时,默认的工作目录是/home/。如果你想使用不同的目录,请在遵循本书中的任何练习步骤时考虑这一点。

第一行指定了要使用的基础镜像。这个示例使用了 Alpine,一个流行的基础镜像,只占用大约 5MB,基于 Alpine Linux。第二行将一个名为k8s-for-beginners的文件从Dockerfile所在的目录复制到镜像的根目录。在这个示例中,我们将构建一个微型网络服务器,并将其编译成一个名为k8s-for-beginners的二进制文件,该文件将放在与Dockerfile相同的目录中。第三行指定了默认的启动命令。在这种情况下,我们只是启动我们的示例网络服务器。

  1. 接下来,让我们构建我们的示例网络服务器。创建一个名为main.go的文件,内容如下:
package main
import (
        "fmt"
        "log"
        "net/http"
)
func main() {
        http.HandleFunc("/", handler)
        log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
        log.Printf("Ping from %s", r.RemoteAddr)
        fmt.Fprintln(w, "Hello Kubernetes Beginners!")
}

正如你可以从func main()中观察到的那样,这个应用程序充当一个网络服务器,在 8080 端口的根路径接受传入的 HTTP 请求,并用消息Hello Kubernetes Beginners做出响应。

  1. 要验证这个程序是否有效,你可以运行go run main.go,然后在浏览器上打开http://localhost:8080。你应该会得到"Hello Kubernetes Beginners!"的输出。

  2. 使用go build将运行时依赖和源代码编译成一个可执行的二进制文件。在终端中运行以下命令:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o k8s-for-beginners

注意

步骤 3不同,参数GOOS=linux GOARCH=amd64告诉 Go 编译器在特定平台上编译程序,这与我们将要构建的 Linux 发行版兼容。CGO_ENABLED=0旨在生成一个静态链接的二进制文件,以便它可以与一些最小定制的镜像一起工作(例如 alpine)。

  1. 现在,检查k8s-for-beginners文件是否已创建:
ls

您应该会看到以下响应:

Dockerfile k8s-for-beginners  main.go
  1. 现在我们有了Dockerfile和可运行的二进制文件。使用以下命令构建 Docker 镜像:
docker build -t k8s-for-beginners:v0.0.1 .

不要错过这个命令末尾的点(.)。您应该会看到以下响应:

图 1.12:docker build 命令的输出

图 1.12:docker build 命令的输出

我们使用的命令中有两个参数:-t k8s-for-beginners:v0.0.1为镜像提供了一个格式为<imagename:version>的标签,而.(命令末尾的点)表示查找Dockerfile的路径。在这种情况下,.指的是当前工作目录。

注意

如果您克隆了本章的 GitHub 存储库,您会发现我们在每个目录中都提供了Dockerfile的副本,以便您可以方便地通过转到该目录运行docker build命令。

  1. 现在,我们本地有了k8s-for-beginners:v0.0.1镜像。您可以通过运行以下命令来确认:
docker images

您应该会看到以下响应:

图 1.13:验证我们的 Docker 镜像是否已创建

图 1.13:验证我们的 Docker 镜像是否已创建

一个有趣的观察是,该镜像仅占用 11.4 MB,其中包括 Linux 系统文件和我们的应用程序。这里的建议是只在 Docker 镜像中包含必要的文件,使其紧凑,以便易于分发和管理。

现在我们已经构建了我们的镜像,接下来我们将在容器中运行它。另一个需要注意的是,目前这个镜像驻留在我们的本地机器上,我们只能在本地机器上使用它构建一个容器。然而,将应用程序与其依赖项打包的优势在于它可以轻松地在不同的机器上运行。为了方便起见,我们可以将我们的镜像上传到在线 Docker 镜像仓库,如 Docker Hub(hub.docker.com/)。

注意:

除了 Docker Hub,还有其他公共镜像仓库,如quay.iogcr.io等。您可以参考各自仓库的文档,以正确配置在您的 Docker 客户端中。

练习 1.02:在 Docker 中运行您的第一个应用程序

练习 1.01中,创建 Docker 镜像并将其上传到 Docker Hub,我们将 Web 应用程序打包成 Docker 镜像。在这个练习中,我们将运行它并将其推送到 Docker Hub:

  1. 首先,我们应该通过在终端中运行以下命令清理掉上一个练习中的任何残留容器:
docker rm -f $(docker ps -aq)

您应该看到以下响应:

43c01e2055cf
286bc0c92b3a
39bf70d02dcc
96c374000f6f

我们已经看到docker ps -a返回所有容器的信息。-aq标志中的额外q表示“安静”,该标志只会显示数字 ID。这些 ID 将被传递给docker rm -f,因此所有容器将被强制删除。

  1. 运行以下命令启动 web 服务器:
docker run -p 8080:8080 -d k8s-for-beginners:v0.0.1

您应该看到以下响应:

9869e9b4ab1f3d5f7b2451a7086644c1cd7393ac9d78b6b4c1bef6d423fd25ac

如前述命令中所示,我们将容器的内部端口8080映射到主机的端口8080。由-p前置的8080:8080参数将容器的端口8080映射到主机上的端口8080-d参数表示分离模式。默认情况下,Docker 首先检查本地注册表。因此,在这种情况下,将使用本地 Docker 镜像来启动容器。

  1. 现在,让我们通过向localhost的端口8080发送 HTTP 请求来检查它是否按预期工作:
curl localhost:8080

curl命令检查来自指定地址的响应。您应该看到以下响应:

Hello Kubernetes Beginners!
  1. 我们还可以使用以下命令观察运行容器的日志:
docker logs <container ID>

您应该看到以下日志:

2019/11/18  05:19:41 Ping from 172.17.0.1:41416

注意

在运行以下命令之前,您应该注册一个 Docker Hub 帐户,并准备好您的用户名和密码。

  1. 最后,我们需要登录到 Docker Hub,然后将本地镜像推送到远程 Docker Hub 注册表。使用以下命令:
docker login

现在在提示时输入您的 Docker Hub 帐户的用户名和密码。您应该看到以下响应:

图 1.14:登录到 Docker Hub

图 1.14:登录到 Docker Hub

  1. 接下来,我们将把本地镜像k8s-for-beginners:v0.0.1推送到远程 Docker Hub 注册表。运行以下命令:
docker push k8s-for-beginners:v0.0.1

您应该看到以下响应:

图 1.15:无法将镜像推送到 Docker Hub

图 1.15:无法将图像推送到 Docker Hub

但是,等等,为什么它说“请求访问被拒绝”?那是因为docker push后面的参数必须符合<username/imagename:version>的命名约定。在上一个练习中,我们指定了一个本地图像标签,k8s-for-beginners:v0.0.1,没有用户名。在docker push命令中,如果没有指定用户名,它将尝试将其推送到默认用户名library的存储库,该存储库还托管一些知名库,如 Ubuntu、nginx 等。

  1. 要将我们的本地图像推送到我们自己的用户,我们需要通过运行docker tag <imagename:version> <username/imagename:version>来为本地图像提供符合规范的名称,如下命令所示:
docker tag k8s-for-beginners:v0.0.1 <your_DockerHub_username>/k8s-for-beginners:v0.0.1
  1. 您可以使用以下命令验证图像是否已正确标记:
docker images

您应该看到以下输出:

图 1.16:检查标记的 Docker 图像

图 1.16:检查标记的 Docker 图像

标记正确后,您可以看到新图像实际上与旧图像具有相同的IMAGE ID,这意味着它们是相同的图像。

  1. 现在我们已经适当地标记了图像,我们准备通过运行以下命令将此图像推送到 Docker Hub:
docker push <your_username>/k8s-for-beginners:v0.0.1

您应该看到类似于此的响应:

图 1.17:图像成功推送到 Docker Hub

图 1.17:图像成功推送到 Docker Hub

  1. 图像将在 Docker Hub 上短时间后上线。您可以通过在以下链接中用您的用户名替换<username>来验证它:https://hub.docker.com/repository/docker/<username>/k8s-for-beginners/tags

您应该能够看到有关您的图像的一些信息,类似于以下图像:

图 1.18:我们图像的 Docker Hub 页面

图 1.18:我们图像的 Docker Hub 页面

现在我们的 Docker 图像对任何人都是公开可访问的,就像我们在本章开头使用的nginx图像一样。

在这一部分,我们学习了如何构建 Docker 镜像并将其推送到 Docker Hub。尽管看起来不起眼,但这是我们第一次拥有一个统一的机制来一致地管理应用程序及其依赖项,跨所有环境。Docker 镜像及其底层分层文件系统也是容器技术近年来被广泛采用的主要原因,与十年前相比。

在下一节中,我们将深入了解 Docker,看看它如何利用 Linux 容器技术。

Linux 容器技术的本质

所有事物从外表看起来都优雅而简单。但是在底层是如何运作的,让一个容器如此强大?在这一部分,我们将尝试打开引擎盖,看看里面。让我们来看看一些为容器奠定基础的 Linux 技术。

命名空间

容器依赖的第一个关键技术称为 Linux 命名空间。当 Linux 系统启动时,它会创建一个默认命名空间(root命名空间)。然后,默认情况下,稍后创建的进程将加入相同的命名空间,因此它们可以无限制地相互交互。例如,两个进程能够查看同一文件夹中的文件,并通过localhost网络进行交互。这听起来很简单,但从技术上讲,这都归功于连接所有进程的root命名空间。

为了支持高级用例,Linux 提供了命名空间 API,以便将不同的进程分组到不同的命名空间中,这样只有属于同一命名空间的进程才能相互感知。换句话说,不同组的进程被隔离。这也解释了为什么我们之前提到 Docker 的隔离是进程级别的。以下是 Linux 内核支持的命名空间类型列表:

  • 挂载命名空间

  • PID(进程 ID)命名空间

  • 网络命名空间

  • IPC(进程间通信)命名空间

  • UTS(Unix 时间共享系统)命名空间

  • 用户命名空间(自 Linux 内核 3.8 以来)

  • Cgroup 命名空间(自 Linux 内核 4.6 以来)

  • 时间命名空间(将在未来版本的 Linux 内核中实现)

为了简洁起见,我们将选择两个简单的(UTS 和 PID)并使用具体示例来解释它们如何在 Docker 中体现。

注意

如果你正在运行 macOS,一些以下命令将需要以不同的方式使用,因为我们正在探索 Linux 的特性。Docker 在 macOS 上使用 HyperKit 在 Linux VM 中运行。因此,你需要打开另一个终端会话并登录到 VM 中:

screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

运行此命令后,你可能会看到一个空屏幕。按 Enter,你应该获得运行 Docker 的 VM 的 root 访问权限。要退出会话,你可以按 Ctrl + A + K,然后在要求确认关闭窗口时按 Y

我们建议您使用另一个终端窗口访问 Linux VM。如果你使用 macOS,我们将提到需要在此终端会话中运行哪些命令。如果你使用任何 Linux 操作系统,你可以忽略这一点,并在同一个终端会话中运行所有命令,除非在说明中另有说明。

创建 Docker 容器后,Docker 会创建并关联一些命名空间到容器。例如,让我们看看在上一节中创建的示例容器。让我们使用以下命令:

docker inspect --format '{{.State.Pid}}' <container ID>

上述命令检查在主机操作系统上运行的容器的 PID。你应该看到类似以下的响应:

5897

在这个例子中,PID 是 5897,正如你在前面的响应中所看到的。现在,在 Linux VM 中运行以下命令:

ps -ef | grep k8s-for-beginners

这应该产生类似于以下内容的输出:

图 1.19:检查我们进程的 PID

图 1.19:检查我们进程的 PID

ps -ef 命令列出主机操作系统上所有正在运行的进程,然后 | grep k8s-for-beginners 过滤此列表,以显示名称中包含 k8s-for-beginners 的进程。我们可以看到该进程还具有 PID 5897,这与第一个命令一致。这揭示了一个重要的事实,即容器只是直接在主机操作系统上运行的特定进程。

接下来,运行此命令:

ls -l /proc/<PID>/ns

对于 macOS,在 VM 终端中运行此命令。你应该看到以下输出:

图 1.20:列出为我们的容器创建的不同命名空间

图 1.20:列出为我们的容器创建的不同命名空间

此命令检查/proc文件夹(这是一个 Linux 伪文件系统),列出了随着容器启动创建的所有命名空间。结果显示了一些众所周知的命名空间(看一下突出显示的矩形),如utspidnet等。让我们仔细看看它们。

uts命名空间被创建,以使容器具有其主机名,而不是主机的主机名。默认情况下,容器被分配其容器 ID 作为主机名,并且可以在运行容器时使用-h参数进行更改,如下所示:

docker run -h k8s-for-beginners -d packtworkshops/the-kubernetes-workshop:k8s-for-beginners

这应该给出以下响应:

df6a15a8e2481ec3e46dedf7850cb1fbef6efafcacc3c8a048752da24ad793dc

使用返回的容器 ID,我们可以进入容器并使用以下两个命令依次检查其主机名:

docker exec -it <container ID> sh
hostname

您应该看到以下响应:

k8s-for-beginners

docker exec命令尝试进入容器并执行sh命令,在容器内启动 shell。一旦我们进入容器,我们运行hostname命令来检查容器内的主机名。从输出中,我们可以看出-h参数正在生效,因为我们可以看到k8s-for-beginners作为主机名。

除了uts命名空间,容器还在其自己的PID命名空间中进行隔离,因此它只能查看由自己启动的进程,而启动进程(由我们在练习 1.01中创建的Dockerfile中的CMDENTRYPOINT指定)被分配为PID 1。让我们通过依次输入以下两个命令来看一下这个:

docker exec -it <container ID> sh
ps

您应该看到以下响应:

图 1.21:容器内的进程列表

图 1.21:容器内的进程列表

Docker 为容器提供了--pid选项,以加入另一个容器的 PID 命名空间。

除了utspid命名空间,Docker 还利用了一些其他命名空间。我们将在下一个练习中检查网络命名空间(图 1.20中的"net")。

练习 1.03:将一个容器加入另一个容器的网络命名空间

在这个练习中,我们将重新创建k8s-for-beginners容器,而不进行主机映射,然后创建另一个容器加入其网络命名空间:

  1. 与之前的练习一样,通过运行以下命令删除所有现有容器:
docker rm -f $(docker ps -aq)

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

43c01e2055cf
286bc0c92b3a
39bf70d02dcc
96c374000f6f
  1. 现在,开始使用以下命令运行我们的容器:
docker run -d packtworkshops/the-kubernetes-workshop:k8s-for-beginners

您应该会看到以下响应:

33003ddffdf4d85c5f77f2cae2528cb2035d37f0a7b7b46947206ca104bbbaa5
  1. 接下来,我们将获取正在运行的容器列表,以便查看容器的 ID:
docker ps

您应该会看到以下响应:

图 1.22:获取所有正在运行的容器列表

图 1.22:获取所有正在运行的容器列表

  1. 现在,我们将在与我们在步骤 1中创建的容器相同的网络命名空间中运行一个名为netshoot的镜像,使用--net参数:
docker run -it --net container:<container ID> nicolaka/netshoot

使用我们在上一步中获得的先前容器的容器 ID。您应该会看到类似于以下响应:

图 1.23:启动 netshoot 容器

图 1.23:启动 netshoot 容器

nicolaka/netshoot是一个打包了一些常用网络库(如iproute2curl等)的微型镜像。

  1. 现在,让我们在netshoot内部运行curl命令,以检查我们是否能够访问k8s-for-beginners容器:
curl localhost:8080

您应该会看到以下响应:

Hello Kubernetes Beginners!

前面的示例证明了netshoot容器是通过加入k8s-for-beginners的网络命名空间而创建的;否则,在localhost上访问端口8080就不会得到响应。

  1. 这也可以通过在接下来的步骤中验证两个容器的网络命名空间 ID 来进行验证。

为了确认我们的结果,让我们首先在不退出netshoot容器的情况下打开另一个终端。获取容器列表以确保两个容器都在运行:

docker ps

您应该会看到以下响应:

图 1.24:检查 k8s-for-beginners 和 netshoot 是否都在线容器都在线

图 1.24:检查 k8s-for-beginners 和 netshoot 容器是否都在线

  1. 接下来,获取k8s-for-beginners容器的 PID:
docker inspect --format '{{.State.Pid}}' <container ID>

您应该会看到以下响应:

7311

如您所见,此示例的 PID 为7311

  1. 现在使用前面的 PID 获取进程的伪文件系统:
ls -l /proc/<PID>/ns/net

如果您使用的是 macOS,请在另一个终端会话中在 Linux VM 上运行此命令。在此命令中使用您在上一步中获得的 PID。您应该会看到以下响应:

lrwxrwxrwx 1 root root 0 Nov 19 08:11 /proc/7311/ns/net -> 'net:[4026532247]'
  1. 同样地,使用以下命令获取netshoot容器的 PID:
docker inspect --format '{{.State.Pid}}' <container ID>

在此命令中使用步骤 6中的适当容器 ID。您应该会看到以下响应:

8143

如您所见,netshoot 容器的 PID 是 8143

  1. 接下来,我们可以通过其 PID 或使用此命令获取其伪文件系统:
ls -l /proc/<PID>/ns/net

如果您使用 macOS,在另一个会话中在 Linux VM 上运行此命令。在此命令中使用上一步中的 PID。您应该会看到以下响应:

lrwxrwxrwx 1 root root 0 Nov 19 09:15 /proc/8143/ns/net -> 'net:[4026532247]'

正如您从 步骤 8步骤 10 的输出中所观察到的,这两个容器共享相同的网络命名空间(4026532247)。

  1. 作为最后的清理步骤,让我们删除所有的容器:
docker rm -f $(docker ps -aq)

您应该会看到类似以下的响应:

61d0fa62bc49
33003ddffdf4
  1. 如果您想要将容器加入到主机的根命名空间中怎么办?嗯,--net host 是实现这一目标的好方法。为了演示这一点,我们将使用相同的镜像启动一个容器,但使用 --net host 参数:
docker run --net host -d packtworkshops/the-kubernetes-workshop:k8s-for-beginners

您应该会看到以下响应:

8bf56ca0c3dc69f09487be759f051574f291c77717b0f8bb5e1760c8e20aebd0
  1. 现在,列出所有正在运行的容器:
docker ps

您应该会看到以下响应:

图 1.25:列出所有容器

图 1.25:列出所有容器

  1. 使用以下命令获取正在运行的容器的 PID:
docker inspect --format '{{.State.Pid}}' <container ID>

在此命令中使用适当的容器 ID。您应该会看到以下响应:

8380
  1. 通过查找 PID 查找网络命名空间 ID:
ls -l /proc/<PID>/ns/net

如果您使用 macOS,在 Linux VM 上运行此命令。在此命令中使用适当的 PID。您应该会看到以下响应:

lrwxrwxrwx 1 root root 0 Nov 19 09:20 /proc/8380/ns/net -> 'net:[4026531993]'

您可能会对 4026531993 命名空间感到困惑。通过给出 --net host 参数,Docker 不应该绕过创建新的命名空间吗?答案是这不是一个新的命名空间;事实上,它就是前面提到的 Linux 根命名空间。我们将在下一步中确认这一点。

  1. 获取主机操作系统的 PID 1 的命名空间:
ls -l /proc/1/ns/net

如果您使用 macOS,在 Linux VM 上运行此命令。您应该会看到以下响应:

lrwxrwxrwx 1 root root 0 Nov 19 09:20 /proc/1/ns/net -> 'net:[4026531993]'

正如您在此输出中所看到的,主机的这个命名空间与我们在 步骤 15 中看到的容器的命名空间是相同的。

通过这个练习,我们可以对容器如何被隔离到不同的命名空间以及哪些 Docker 参数可以用来与其他命名空间相关联有所了解。

Cgroups

默认情况下,无论容器加入哪个命名空间,它都可以使用主机的所有可用资源。这当然不是我们在系统上运行多个容器时想要的情况;否则,一些容器可能会独占所有容器共享的资源。

为了解决这个问题,Linux 内核版本 2.6.24 以后引入了 cgroupsControl Groups 的缩写)功能,用于限制进程的资源使用。使用这个功能,系统管理员可以控制最重要的资源,如内存、CPU、磁盘空间和网络带宽。

在 Ubuntu 18.04 LTS 中,默认情况下会在路径 /sys/fs/cgroup/<cgroup type> 下创建一系列 cgroups。

注意

您可以运行 mount -t cgroup 来查看 Ubuntu 中的所有 cgroups;尽管如此,我们不会在本书的范围内涉及它们,因为它们对我们来说并不是很相关。

现在,我们并不太关心系统进程及其 cgroups;我们只想关注 Docker 在整个 cgroups 图中的关系。Docker 在路径 /sys/fs/cgroup/<resource kind>/docker 下有其 cgroups 文件夹。使用 find 命令来检索列表:

find /sys/fs/cgroup/* -name docker -type d

如果您使用的是 macOS,在 Linux VM 的另一个会话中运行此命令。您应该会看到以下结果:

图 1.26:获取与 Docker 相关的所有 cgroups

图 1.26:获取与 Docker 相关的所有 cgroups

每个文件夹都被视为一个控制组,这些文件夹是分层的,这意味着每个 cgroup 都有一个从其继承属性的父级,一直到在系统启动时创建的根 cgroup。

为了说明 cgroup 在 Docker 中的工作原理,我们将使用 图 1.26 中突出显示的 memory cgroup 作为示例。

但首先,让我们使用以下命令删除所有现有的容器:

docker rm -f $(docker ps -aq)

您应该会看到类似以下的响应:

61d0fa62bc49

让我们通过以下命令来确认:

docker ps

您应该会看到一个空列表,如下所示:

CONTAINER ID     IMAGE       COMMAND          CREATED          STATUS
        PORTS          NAMES

让我们看看是否有 cgroup 内存文件夹:

find /sys/fs/cgroup/memory/docker/* -type d

如果您使用的是 macOS,在 Linux VM 上运行此命令。然后您应该会看到以下响应:

root@ubuntu: ~# find /sys/fs/cgroup/memory/docker/* -type d

没有文件夹显示出来。现在,让我们运行一个容器:

docker run -d packtworkshops/the-kubernetes-workshop:k8s-for-beginners 

您应该会看到类似以下的输出:

8fe77332244b2ebecbda27a4496268264218c4e59614d59b5849a22b12941e1

再次检查 cgroup 文件夹:

find /sys/fs/cgroup/memory/docker/* -type d

如果您使用的是 macOS,在 Linux VM 上运行此命令。您应该会看到以下响应:

/sys/fs/cgroup/memory/docker/8fe77332244b2ebecbda27a4496268264218c4e59614d59b5849a22b12941e1

到目前为止,您可以看到一旦我们创建一个容器,Docker 就会在特定资源类型(在我们的示例中是内存)下创建其 cgroup 文件夹。现在,让我们看看在这个文件夹中创建了哪些文件:

ls /sys/fs/cgroup/memory/docker/8fe77332244b2ebecbd8a2704496268264218c4e59614d59b5849022b12941e1

如果您使用的是 macOS,在 Linux VM 上运行此命令。请使用您从上一张截图中获得的适当路径。您应该会看到以下文件列表:

图 1.27:探索 Docker 创建的内存 cgroups

图 1.27:探索 Docker 创建的内存 cgroups

我们不会在这里介绍每个设置。 我们感兴趣的设置是memory.limit_in_bytes,如前所述,它表示容器可以使用多少内存。 让我们看看这个文件中写了什么值:

cat /sys/fs/cgroup/memory/docker/8fe77332244b2ebecbd8a2704496268264218c4e59614d59b5849022b12941e1/memory.limit_in_bytes

如果您使用的是 macOS,请在 Linux VM 上运行此命令。 您应该看到以下响应:

9223372036854771712

9223372036854771712是 64 位系统中最大的正有符号整数(263-1),这意味着此容器可以使用无限的内存。

为了了解 Docker 如何处理过度使用声明内存的容器,我们将向您展示另一个程序,该程序消耗一定量的 RAM。 以下是一个用于逐步消耗 50 MB RAM 然后保持整个程序(休眠 1 小时)以防止退出的 Golang 程序:

package main
import (
        "fmt"
        "strings"
        "time"
)
func main() {
        var longStrs []string
        times := 50
        for i := 1; i <= times; i++ {
                fmt.Printf("===============%d===============\n", i)
                // each time we build a long string to consume 1MB                     (1000000 * 1byte) RAM
                longStrs = append(longStrs, buildString(1000000,                     byte(i)))
        }
        // hold the application to exit in 1 hour
        time.Sleep(3600 * time.Second)
}
// buildString build a long string with a length of `n`.
func buildString(n int, b byte) string {
        var builder strings.Builder
        builder.Grow(n)
        for i := 0; i < n; i++ {
                builder.WriteByte(b)
        }
        return builder.String()
}

您可以尝试使用此代码构建一个镜像,如练习 1.01中所示,创建 Docker 镜像并将其上传到 Docker Hub。 此代码将用于替换该练习中步骤 2中提供的代码,然后您可以使用<username>/memconsumer为镜像打标签。 现在,我们可以测试资源限制。 让我们使用 Docker 镜像并使用--memory(或-m)标志运行它,以指示 Docker 我们只想使用一定量的 RAM。

如果您使用的是 Ubuntu 或任何其他基于 Debian 的 Linux,在继续本章之前,如果在运行此命令时看到以下警告消息,则可能需要手动启用 cgroup 内存和交换功能:

docker info > /dev/null

这是您可能会看到的警告消息:

WARNING: No swap limit support

启用 cgroup 内存和交换功能的步骤如下:

注意

如果您使用的是 macOS,则以下三个步骤不适用。

  1. 编辑/etc/default/grub文件(可能需要 root 权限)。 添加或编辑GRUB_CMDLINE_LINUX行以添加以下两个键值对:
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
  1. 使用 root 权限运行update-grub

  2. 重新启动机器。

接下来,我们应该能够通过运行以下命令来限制容器的内存使用量为 100 MB:

docker run --name memconsumer -d --memory=100m --memory-swap=100m packtworkshops/the-kubernetes-workshop:memconsumer

注意

此命令拉取了我们为此演示提供的镜像。 如果您已构建了自己的镜像,可以在前面的命令中使用<your_username>/<tag_name>

您应该看到以下响应:

WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
366bd13714cadb099c7ef6056e3b72853735473938b2e633a5cdbf9e94273143

这个命令禁用了交换内存的使用(因为我们在--memory--memory-swap上指定了相同的值),以便轻松地衡量内存的消耗。

让我们检查一下我们的容器的状态:

docker ps

你应该看到以下响应:

图 1.28:获取容器列表

图 1.28:获取容器列表

现在,让我们通过读取容器的cgroup文件来确认对容器施加的限制:

cat /sys/fs/cgroup/memory/docker/366bd13714cadb099c7ef6056e3b7285373547e9e8b2e633a5cdbf9e94273143/memory.limit_in_bytes

如果您使用的是 macOS,请在 Linux VM 上运行此命令。请在此命令中使用适当的路径。你应该看到以下响应:

104857600

容器启动时请求了 100 MB 的 RAM,并且它在内部只消耗了 50 MB 的 RAM,因此可以正常运行。从 cgroup 设置中,您可以观察到该值已更新为104857600,这正好是 100 MB。

但是,如果容器请求少于 50 MB,而其中运行的程序需要超过 50 MB 呢?Docker 和 Linux 会如何响应?让我们来看看。

首先,让我们删除任何正在运行的容器:

docker rm -f $(docker ps -aq)

你应该看到以下响应:

366bd13714ca

接下来,我们将再次运行容器,但是我们只会请求 20 MB 的内存:

docker run --name memconsumer -d --memory=20m --memory-swap=20m packtworkshops/the-kubernetes-workshop:memconsumer

你应该看到这个响应:

298541bc46855a749f9f8944860a73f3f4f2799ebda7969a5eada60e3809539bab

现在,让我们检查一下我们的容器的状态:

docker ps

你应该看到一个空列表,就像这样:

CONTAINER ID     IMAGE       COMMAND      CREATED        STATUS
       PORTS          NAMES

如您所见,我们无法看到我们的容器。让我们列出所有类型的容器:

docker ps -a

你应该看到以下输出:

图 1.29:获取所有容器的列表

图 1.29:获取所有容器的列表

我们找到了我们的容器。它已被强制终止。可以通过检查容器日志来验证:

docker logs memconsumer

你应该看到以下输出:

图 1.30:我们终止的容器的日志

图 1.30:我们终止的容器的日志

容器试图每次增加 1 MB 的内存消耗,当它达到内存限制(20 MB)时,它被杀死。

从前面的例子中,我们已经看到 Docker 如何向最终用户公开标志,以及这些标志如何与底层的 Linux cgroups 交互以限制资源使用。

容器化:思维方式的改变

在前面的章节中,我们看了 Linux 命名空间和 cgroups 的解剖。我们解释了容器本质上是在主机操作系统上本地运行的进程。它是一个特殊的进程,具有额外的限制,如与其他进程的操作系统级隔离和资源配额的控制。

自 Docker 1.11 以来,containerd 已被采用为默认的容器运行时,而不是直接使用 Docker Daemon(dockerd)来管理容器。让我们来看看这个运行时。首先,正常重启我们的容器:

docker run -d packtworkshops/the-kubernetes-workshop:k8s-for-beginners

您应该看到以下响应:

c7ee681ff8f73fa58cf0b37bc5ce08306913f27c5733c725f7fe97717025625d

我们可以使用ps -aef --forest来列出层次结构中所有运行的进程,然后使用| grep containerd来通过containerd关键字过滤输出。最后,我们可以使用-A 1来输出一行额外的内容(使用-A 1),以便至少有一个运行的容器显示出来:

ps -aef --forest | grep containerd -A 1

如果您正在使用 macOS,请在没有--forest标志的 Linux VM 上运行此命令。您应该看到以下响应:

图 1.31:获取与 containerd 相关的进程

图 1.31:获取与 containerd 相关的进程

在输出中,我们可以看到containerd(PID 1037)充当顶级父进程,并管理containerd-shim(PID 19374),而containerd-shim管理k8s-for-beginners(PID 19394)的大多数子进程,这是我们启动的容器。

牢记容器的核心思想可以帮助您将任何基于 VM 的应用程序迁移到基于容器的应用程序。基本上,有两种模式可以部署容器中的应用程序:

一个容器中的多个应用程序

这种实现需要一个监督者应用程序来启动和持有容器。然后,我们可以将应用程序放入容器作为监督者的子进程。监督者有几个变体:

  • 自定义包装脚本:这需要复杂的脚本来控制受管应用程序的故障。

  • 第三方工具,如 supervisord 或 systemd:在应用程序失败时,监督者负责重新启动它。

一个容器中的一个应用程序

这种实现不需要像之前那样的监督者。事实上,应用程序的生命周期与容器的生命周期相关联。

这些方法的比较

通过在单个容器中部署多个应用程序,我们实质上是将容器视为 VM。这种容器作为轻量级 VM的方法曾经被用作容器技术的宣传口号。然而,正如所解释的,它们在许多方面都有所不同。当然,这种方式可以节省从基于 VM 的开发/部署模型迁移到容器的工作,但它也在以下方面引入了一些缺点:

  • 应用程序生命周期控制:从外部看,容器暴露为一个状态,因为它本质上是一个单一的主机进程。内部应用程序的生命周期由“监督者”管理,因此无法从外部观察。因此,从外部看,您可能会观察到容器保持健康,但其中一些应用程序可能会持续重启。它可能会因为内部应用程序的致命错误而持续重启,而您可能无法指出这一点。

  • 版本升级:如果您想升级容器中的任何一个不同的应用程序,您可能需要拉下整个容器。这会导致容器中其他不需要版本升级的应用程序不必要的停机时间。因此,如果应用程序需要由不同团队开发的组件,它们的发布周期必须紧密耦合。

  • 水平扩展:如果只有一个应用程序需要扩展,您别无选择,只能扩展整个容器,这也会复制所有其他应用程序。这会导致不需要扩展的应用程序浪费资源。

  • 运行时的考虑:检查应用程序的日志变得更具挑战性,因为容器的标准输出(stdout)和错误(stderr)不代表容器内应用程序的日志。您必须额外努力来管理这些日志,比如安装额外的监控工具来诊断每个应用程序的健康状况。

从技术上讲,在单个容器中运行多个应用程序是可行的,并且不需要从虚拟机的角度进行太多思维转变。然而,当我们采用容器技术来享受其好处时,我们需要在迁移便利性和长期可维护性之间进行权衡。

第二种方式(即一个容器中只有一个应用程序)使容器能够自动管理其内部唯一应用程序的生命周期。通过利用原生的 Linux 功能,例如通过检查容器状态获取应用程序状态,并从容器的stdout/stderr获取应用程序日志,我们可以统一容器管理。这使您能够管理每个应用程序的发布周期。

然而,这并不是一件容易的事情。这需要你重新思考不同组件之间的关系和依赖,以将单片应用程序拆分为微服务。这可能需要对架构设计进行一定程度的重构,包括源代码和交付流程的改变。

总之,采用容器技术是一次分离和重组的旅程。这不仅需要技术成熟的时间,更重要的是,它需要改变人们的思维方式。只有通过这种思维方式的改变,你才能重构应用程序以及底层基础设施,释放容器的价值并享受它们的真正好处。正是这个第二个原因,容器技术才在最近几年开始崛起,而不是十年前。

容器编排的需求

我们在练习 1.01中构建的k8s-for-beginners容器只是一个简单的演示。在生产环境中部署严重工作负载,并在集群中运行数十万个容器时,我们需要考虑更多的事情。我们需要一个系统来解决以下问题:

容器交互

举个例子,假设我们要构建一个 Web 应用程序,其中前端容器显示信息并接受用户请求,后端容器作为与前端容器交互的数据存储。第一个挑战是如何指定后端容器的地址给前端容器。硬编码 IP 并不是一个好主意,因为容器 IP 不是静态的。在分布式系统中,由于意外问题,容器或机器可能会失败。因此,任何两个容器之间的链接必须是可发现的,并且在所有机器上都是有效的。另一方面,第二个挑战是我们可能希望限制哪些容器(例如后端容器)可以被哪种类型的容器(例如其对应的前端容器)访问。

网络和存储

在前面的部分中,我们给出的所有示例都是在同一台机器上运行的容器。这相当简单,因为底层的 Linux 命名空间和 cgroup 技术是设计为在同一操作系统实体内工作的。如果我们想在生产环境中运行数千个容器,这是非常常见的,我们必须解决网络连接问题,以确保不同机器上的不同容器能够相互连接。另一方面,本地或临时的磁盘存储并不总是适用于所有工作负载。应用程序可能需要将数据存储在远程位置,并且可以随时挂载到集群中任何一台机器上,无论容器是第一次启动还是在故障后重新启动。

资源管理和调度

我们已经看到,容器利用 Linux cgroups 来管理其资源使用情况。要成为现代资源管理器,它需要构建一个易于使用的资源模型,以抽象资源,如 CPU、RAM、磁盘和 GPU。我们需要有效地管理多个容器,并及时分配和释放资源,以实现高集群利用率。

调度涉及为集群中的每个工作负载分配适当的机器来运行。随着我们在本书中继续深入研究,我们将更仔细地研究调度。为了确保每个容器都有最佳的机器来运行,调度器(负责调度的 Kubernetes 组件)需要全局查看集群中不同机器上所有容器的分布情况。此外,在大型数据中心中,容器需要根据机器的物理位置或云服务的可用区进行分布。例如,如果支持某项服务的所有容器都分配给同一台物理机,而该机器发生故障,无论您部署了多少个容器的副本,该服务都将经历一段宕机期。

故障转移和恢复

在分布式系统中,应用程序或机器错误是相当常见的。因此,我们必须考虑容器和机器故障。当容器遇到致命错误并退出时,它们应该能够在同一台或另一台可用的机器上重新启动。我们应该能够检测机器故障或网络分区,以便将容器从有问题的机器重新调度到健康的机器上。此外,协调过程应该是自主的,以确保应用程序始终以其期望的状态运行。

可扩展性

随着需求的增加,您可能希望扩展应用程序。以 Web 前端应用程序为例。我们可能需要运行多个副本,并使用负载均衡器将传入的流量均匀分配到支持服务的容器的多个副本中。更进一步,根据传入请求的数量,您可能希望应用程序动态扩展,无论是水平扩展(增加或减少副本)还是垂直扩展(分配更多或更少的资源)。这使得系统设计的难度提升到了另一个层次。

服务暴露

假设我们已经解决了之前提到的所有挑战;也就是说,在集群内一切都运行良好。好吧,又来了另一个挑战:应用程序如何可以被外部访问?一方面,外部端点需要与基础的本地或云环境相关联,以便利用基础设施的 API 使其始终可访问。另一方面,为了保持内部网络流量始终通过,外部端点需要动态关联内部备份副本 - 任何不健康的副本都需要被自动取出并自动填充,以确保应用程序保持在线。此外,L4(TCP/UDP)和 L7(HTTP,HTTPS)流量在数据包方面具有不同的特征,因此需要以稍微不同的方式处理以确保效率。例如,HTTP 头信息可以用于重用相同的公共 IP 来为多个后端应用程序提供服务。

交付管道

从系统管理员的角度来看,一个健康的集群必须是可监控的、可操作的,并且能够自主应对故障。这要求部署在集群上的应用程序遵循标准化和可配置的交付流程,以便在不同阶段和不同环境中进行良好的管理。

一个单独的容器通常只用于完成单一功能,这是不够的。我们需要提供几个构建块来将所有容器连接在一起,以完成复杂的任务。

编排器:将所有事物整合在一起

我们并不是要压倒你,但上述问题非常严重,这是由于需要自动管理大量容器而产生的。与虚拟机时代相比,容器在大型分布式集群中为应用程序管理打开了另一扇门。然而,这也将容器和集群管理的挑战提升到了另一个层面。为了将容器连接在一起,以以可扩展、高性能和自我恢复的方式实现所需的功能,我们需要一个设计良好的容器编排器。否则,我们将无法将我们的应用程序从虚拟机迁移到容器中。这是第三个原因,为什么近年来容器化技术开始大规模采用,特别是在 Kubernetes 出现后 - 它现在是事实上的容器编排器。

欢迎来到 Kubernetes 世界

与通常逐步发展的典型软件不同,Kubernetes 是一个快速启动的项目,因为它是基于谷歌内部大规模集群管理软件(如 Borg 和 Omega)多年经验的设计而来。也就是说,Kubernetes 诞生时就装备了容器编排和管理领域的许多最佳实践。从一开始,团队就理解了真正的痛点,并提出了适当的设计来解决这些问题。像 Pod、每个 Pod 一个 IP、声明式 API 和控制器模式等概念,都是 Kubernetes 首次引入的,似乎有点“不切实际”,当时可能有人质疑它们的真正价值。然而,5 年后,这些设计原理仍然保持不变,并已被证明是与其他软件的关键区别。

Kubernetes 解决了前一节提到的所有挑战。Kubernetes 提供的一些众所周知的功能包括:

  • 本地支持应用程序生命周期管理

这包括对应用程序复制、自动缩放、部署和回滚的内置支持。您可以描述应用程序的期望状态(例如,多少个副本,哪个镜像版本等),Kubernetes 将自动协调实际状态以满足其期望状态。此外,在部署和回滚方面,Kubernetes 确保旧副本逐渐被新副本替换,以避免应用程序的停机时间。

  • 内置健康检查支持

通过实现一些“健康检查”钩子,您可以定义容器何时被视为就绪、存活或失败。只有当容器健康且就绪时,Kubernetes 才会开始将流量引导到容器,并且会自动重新启动不健康的容器。

  • 服务发现和负载均衡

Kubernetes 在工作负载的不同副本之间提供内部负载均衡。由于容器偶尔会失败,Kubernetes 不使用 IP 进行直接访问。相反,它使用内部 DNS,并为集群内的通信为每个服务公开一个 DNS 记录。

  • 配置管理

Kubernetes 使用标签来描述机器和工作负载。它们受 Kubernetes 组件的尊重,以松散耦合和灵活的方式管理容器和依赖关系。此外,简单但强大的标签可以用于实现高级调度功能(例如,污点/容忍和亲和性/反亲和性)。

在安全方面,Kubernetes 提供了 Secret API,允许您存储和管理敏感信息。这可以帮助应用程序开发人员安全地将凭据与应用程序关联起来。从系统管理员的角度来看,Kubernetes 还提供了各种选项来管理身份验证和授权。

此外,一些选项,如 ConfigMaps,旨在提供精细的机制来构建灵活的应用交付流水线。

  • 网络和存储抽象

Kubernetes 启动了抽象网络和存储规范的标准,即 CNI(容器网络接口)和 CSI(容器存储接口)。每个网络和存储提供商都遵循接口并提供其实现。这种机制解耦了 Kubernetes 和异构提供商之间的接口。有了这个,最终用户可以使用标准的 Kubernetes API 以可移植的方式编排其工作负载。

在引擎盖下,有一些支持前面提到的功能的关键概念,更为关键的是,Kubernetes 为最终用户提供了不同的扩展机制,以构建定制的集群甚至他们自己的平台:

  • 声明式 API

声明式 API 是描述您想要完成的方式。在这个约定下,我们只需指定期望的最终状态,而不是描述到达那里的步骤。

声明式模型在 Kubernetes 中被广泛使用。它不仅使 Kubernetes 的核心功能能够以容错的方式运行,而且还作为构建 Kubernetes 扩展解决方案的黄金法则。

  • 简洁的 Kubernetes 核心

软件项目随着时间的推移往往会变得越来越庞大,尤其是像 Kubernetes 这样著名的开源软件。越来越多的公司参与了 Kubernetes 的开发。但幸运的是,自从第一天起,Kubernetes 的先驱者们就设定了一些基线,以保持 Kubernetes 的核心简洁整洁。例如,Kubernetes 并没有绑定到特定的容器运行时(例如 Docker 或 Containerd),而是定义了一个接口(CRI容器运行时接口)以保持技术的中立性,使用户可以选择使用哪种运行时。此外,通过定义CNI容器网络接口),它将 pod 和主机的网络路由实现委托给不同的项目,如 Calico 和 Weave Net。这样,Kubernetes 能够保持其核心的可管理性,并鼓励更多的供应商加入,以便最终用户可以有更多的选择,避免供应商锁定。

  • 可配置、可插拔和可扩展的设计

所有 Kubernetes 组件都提供配置文件和标志,供用户自定义功能。每个核心组件都严格实现以符合公共 Kubernetes API;对于高级用户,您可以选择自己实现部分或整个组件,以满足特殊需求,只要它符合 API。此外,Kubernetes 提供了一系列扩展点来扩展 Kubernetes 的功能,以及构建您的平台。

在本书的过程中,我们将带您了解高级别的 Kubernetes 架构、其核心概念、最佳实践和示例,以帮助您掌握 Kubernetes 的基本知识,这样您就可以在 Kubernetes 上构建您的应用程序,并扩展 Kubernetes 以满足复杂的需求。

活动 1.01:创建一个简单的页面计数应用程序

在这个活动中,我们将创建一个简单的网络应用程序,用于统计访问者的数量。我们将把这个应用程序放入容器中,将其推送到 Docker 镜像注册表,然后运行容器化的应用程序。

页面浏览网络应用

我们将首先构建一个简单的网络应用程序,用于显示特定网页的页面浏览量:

  1. 使用您喜欢的编程语言编写一个 HTTP 服务器,监听端口8080,在根路径(/)。一旦收到请求,它会将1添加到其内部变量,并以消息Hello, you're visitor #i做出响应,其中i是累积数字。您应该能够在本地开发环境中运行此应用程序。

注意

如果您需要代码帮助,我们提供了一个用 Go 编写的示例代码片段,也用于解决这个活动的问题。您可以从以下链接获取:packt.live/2DcCQUH

  1. 编写一个Dockerfile来构建 HTTP 服务器,并将其与其依赖项打包到 Docker 镜像中。在最后一行设置启动命令以运行 HTTP 服务器。

  2. 构建Dockerfile并将镜像推送到公共 Docker 镜像注册表(例如,hub.docker.com/)。

  3. 通过启动 Docker 容器来测试您的 Docker 镜像。您应该使用 Docker 端口映射或内部容器 IP 来访问 HTTP 服务器。

您可以通过重复使用curl命令来访问它,以测试您的应用程序是否正常工作。

root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #1.
root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #2.
root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #3.

奖励目标

到目前为止,我们已经实现了本章学到的 Docker 的基础知识。然而,我们可以通过扩展这个活动来演示连接不同容器的需求。

对于一个应用程序,通常我们需要多个容器来专注于不同的功能,然后将它们连接在一起作为一个完全功能的应用程序。在本书的后面,您将学习如何使用 Kubernetes 来做到这一点;然而,现在让我们直接连接容器。

我们可以通过附加后端数据存储来增强此应用程序。这将使其能够在容器终止后保持其状态,即保留访问者数量。如果容器重新启动,它将继续计数,而不是重置计数。以下是构建到目前为止构建的应用程序的一些建议。

一个后端数据存储

当容器终止时,我们可能会丢失页面浏览次数,因此我们需要将其持久化到后端数据存储中:

  1. 在容器中运行三种知名的数据存储之一:Redis、MySQL 或 MongoDB。

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD。我们已经为我们的数据存储实现了 Redis。

您可以在此链接找到有关 Redis 容器用法的更多详细信息:hub.docker.com/_/redis

如果您希望使用 MySQL,您可以在此链接找到有关其用法的详细信息:hub.docker.com/_/mysql

如果您希望使用 MongoDB,您可以在此链接找到有关其用法的详细信息:hub.docker.com/_/mongo

  1. 您可能需要使用--name db标志运行容器以使其可发现。如果您使用 Redis,则命令应如下所示:
docker run --name db -d redis

修改 Web 应用程序以连接到后端数据存储

  1. 每当有请求时,您应该修改逻辑以从后端读取页面浏览次数,然后将1添加到其内部变量,并响应消息Hello, you're visitor #i,其中i是累积数字。同时,将添加的页面浏览次数存储在数据存储中。您可能需要使用数据存储的特定 SDK(软件开发工具包)来连接到数据存储。您现在可以将连接 URL 设置为db:<db 端口>

注意

您可以使用以下链接的源代码:packt.live/3lBwOhJ

如果您正在使用此链接中的代码,请确保将其修改为映射到数据存储的公开端口。

  1. 使用新的镜像版本重建网络应用程序。

  2. 使用--link db:db标志运行网络应用程序容器。

  3. 验证页面浏览次数是否正确返回。

  4. 终止网络应用程序容器并重新启动,以查看页面浏览次数是否恢复正常。

创建应用程序成功后,通过重复访问来测试它。您应该看到它的工作如下:

root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #1.
root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #2.
root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #3.

然后,终止容器并重新启动。现在,尝试访问它。应用程序的状态应该被保留,也就是说,计数必须从您重新启动容器之前的位置继续。您应该看到以下结果:

root@ubuntu:~# curl localhost: 8080
Hello, you're visitor #4.

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

摘要

在本章中,我们向您介绍了软件开发的简要历史,并解释了 VM 时代的一些挑战。随着 Docker 的出现,容器化技术在解决早期软件开发方法存在的问题方面开辟了新的大门。

我们向您介绍了 Docker 的基础知识,并详细介绍了 Linux 的基本特性,如命名空间和 cgroups,这些特性实现了容器化。然后,我们提出了容器编排的概念,并阐明了它旨在解决的问题。最后,我们对 Kubernetes 的一些关键特性和方法进行了非常简要的概述。

在下一章中,我们将深入了解 Kubernetes 的架构,以了解其工作原理。

第二章: Kubernetes 概述

概述

在本章中,我们将首次介绍 Kubernetes。本章将为您简要介绍 Kubernetes 的不同组件以及它们如何协同工作。我们还将尝试使用一些基本的 Kubernetes 组件。

在本章结束时,您将拥有一个设置好的单节点 Minikube 环境,可以在其中运行本书中的许多练习和活动。您将能够理解 Kubernetes 的高层架构,并确定不同组件的角色。您还将学会将容器化应用程序迁移到 Kubernetes 环境所需的基础知识。

介绍

我们在上一章中通过提供简要和抽象的介绍以及一些优势来结束了对 Kubernetes 的介绍。在本章中,我们将为您提供对 Kubernetes 工作方式的更具体的高层次理解。首先,我们将带您了解如何安装 Minikube,这是一个方便的工具,可以创建单节点集群,并为 Kubernetes 提供便捷的学习环境。然后,我们将对所有组件进行一次总览,包括它们的职责以及它们如何相互交互。之后,我们将把我们在上一章中构建的 Docker 应用迁移到 Kubernetes,并说明它如何享受 Kubernetes 所提供的好处,比如创建多个副本和版本更新。最后,我们将解释应用程序如何响应外部和内部流量。

在我们深入了解 Kubernetes 的不同方面之前,了解 Kubernetes 的概述是很重要的,这样当我们学习更多关于不同方面的具体内容时,您将知道它们在整体中的位置。此外,当我们进一步探索如何使用 Kubernetes 在生产环境中部署应用程序时,您将了解到后台是如何处理一切的。这也将帮助您进行优化和故障排除。

设置 Kubernetes

如果三年前你问这样一个问题:“如何轻松安装 Kubernetes?”,那么很难给出一个令人信服的答案。尴尬但却是事实。Kubernetes 是一个复杂的系统,安装和有效管理它并不是一件容易的事。

然而,随着 Kubernetes 社区的扩大和成熟,出现了越来越多用户友好的工具。截至今天,根据您的需求,有很多选择:

  • 如果您正在使用物理(裸机)服务器或虚拟机(VMs),Kubeadm 是一个很好的选择。

  • 如果您在云环境中运行,Kops 和 Kubespray 可以简化 Kubernetes 的安装,以及与云提供商的集成。事实上,我们将教您如何在 AWS 上使用 Kops 部署 Kubernetes,第十一章构建您自己的 HA 集群,我们将再次看看我们可以使用的各种选项来设置 Kubernetes。

  • 如果您想摆脱管理 Kubernetes 控制平面的负担(我们将在本章后面学习),几乎所有的云提供商都有他们的 Kubernetes 托管服务,如 Google Kubernetes Engine(GKE)、Amazon Elastic Kubernetes Service(EKS)、Azure Kubernetes Service(AKS)和 IBM Kubernetes Service(IKS)。

  • 如果您只是想要一个用来学习 Kubernetes 的游乐场,Minikube 和 Kind 可以帮助您在几分钟内建立一个 Kubernetes 集群。

在本书中,我们将广泛使用 Minikube 作为一个方便的学习环境。但在我们继续安装过程之前,让我们更仔细地看一下 Minikube 本身。

Minikube 概述

Minikube 是一个用于设置单节点集群的工具,它提供了方便的命令和参数来配置集群。它的主要目标是提供一个本地测试环境。它打包了一个包含所有 Kubernetes 核心组件的虚拟机,一次性安装到您的主机上。这使得它能够支持任何操作系统,只要预先安装了虚拟化工具(也称为 Hypervisor)。以下是 Minikube 支持的最常见的 Hypervisors:

  • VirtualBox(适用于所有操作系统)

  • KVM(特定于 Linux)

  • Hyperkit(特定于 macOS)

  • Hyper-V(特定于 Windows)

关于所需的硬件资源,最低要求是 2GB RAM 和任何支持虚拟化的双核 CPU(Intel VT 或 AMD-V),但如果您要尝试更重的工作负载,当然需要一台更强大的机器。

就像任何其他现代软件一样,Kubernetes 提供了一个方便的命令行客户端,称为 kubectl,允许用户方便地与集群交互。在下一个练习中,我们将设置 Minikube 并使用一些基本的 kubectl 命令。我们将在下一章更详细地介绍 kubectl。

练习 2.01:开始使用 Minikube 和 Kubernetes 集群

在这个练习中,我们将使用 Ubuntu 20.04 作为基本操作系统来安装 Minikube,使用它可以轻松启动单节点 Kubernetes 集群。一旦 Kubernetes 集群设置好了,你应该能够检查它的状态并使用kubectl与之交互:

注意

由于这个练习涉及软件安装,你需要以 root/superuser 身份登录。切换到 root 用户的简单方法是运行以下命令:sudo su -

在这个练习的第 9 步中,我们将创建一个普通用户,然后切换回该用户。

  1. 首先确保 VirtualBox 已安装。你可以使用以下命令确认:
which VirtualBox

你应该看到以下输出:

/usr/bin/VirtualBox

如果 VirtualBox 已成功安装,which命令应该显示可执行文件的路径,就像前面的截图中显示的那样。如果没有,那么请确保你已按照前言中提供的说明安装了 VirtualBox。

  1. 使用以下命令下载 Minikube 独立二进制文件:
curl -Lo minikube https://github.com/kubernetes/minikube/releases/download/<version>/minikube-<ostype-arch> && chmod +x minikube

在这个命令中,<version>应该被替换为一个特定的版本,比如v1.5.2(这是本章中我们将使用的版本)或者latest。根据你的主机操作系统,<ostype-arch>应该被替换为linux-amd64(对于 Ubuntu)或者darwin-amd64(对于 macOS)。

注意

为了确保与本书提供的命令兼容,我们建议安装 Minikube 版本v1.5.2

你应该看到以下输出:

图 2.1:下载 Minikube 二进制文件

图 2.1:下载 Minikube 二进制文件

上述命令包含两部分:第一个命令curl下载 Minikube 二进制文件,而第二个命令chmod更改权限以使其可执行。

  1. 将二进制文件移动到系统路径(在本例中是/usr/local/bin),这样我们可以直接运行 Minikube,而不管命令在哪个目录中运行:
mv minikube /usr/local/bin

当成功执行时,移动(mv)命令不会在终端中给出响应。

  1. 运行移动命令后,我们需要确认 Minikube 可执行文件现在位于正确的位置:
which minikube

您应该看到以下输出:

/usr/local/bin/minikube

注意

如果which minikube命令没有给出预期的结果,您可能需要通过运行export PATH=$PATH:/usr/local/bin来显式将/usr/local/bin添加到系统路径。

  1. 您可以使用以下命令检查 Minikube 的版本:
minikube version

您应该看到以下输出:

minikube version: v1.5.2
commit: 792dbf92a1de583fcee76f8791cff12e0c9440ad-dirty
  1. 现在,让我们下载 kubectl 版本v1.16.2(以便与稍后我们的 Minikube 设置创建的 Kubernetes 版本兼容),并使用以下命令使其可执行:
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.16.2/bin/<ostype>/amd64/kubectl && chmod +x kubectl

如前所述,<ostype>应替换为linux(对于 Ubuntu)或darwin(对于 macOS)。

您应该看到以下输出:

图 2.2:下载 kubectl 二进制文件

图 2.2:下载 kubectl 二进制文件

  1. 然后,将其移动到系统路径,就像我们之前为 Minikube 的可执行文件所做的那样:
mv kubectl /usr/local/bin
  1. 现在,让我们检查 kubectl 的可执行文件是否在正确的路径上:
which kubectl

您应该看到以下响应:

/usr/local/bin/kubectl
  1. 由于我们当前以root用户登录,让我们通过运行以下命令创建一个名为k8suser的常规用户:
useradd k8suser

在提示时输入您想要的密码。您还将被提示输入其他详细信息,例如您的全名。您可以选择通过简单地按Enter来跳过这些细节。您应该看到类似于以下的输出:

图 2.3:创建新的 Linux 用户

图 2.3:创建一个新的 Linux 用户

输入Y并按Enter确认创建用户的最终提示,如前一个屏幕截图的末尾所示。

  1. 现在,从root切换用户到k8suser
su - k8suser

您应该看到以下输出:

root@ubuntu:~# su – k8suser
k8suser@ubuntu:~$
  1. 现在,我们可以使用minikube start创建一个 Kubernetes 集群:
minikube start --kubernetes-version=v1.16.2

注意

如果您想管理多个集群,Minikube 为每个集群提供了一个--profile <profile name>参数。

下载 VM 镜像并进行所有设置需要几分钟时间。Minikube 成功启动后,您应该看到类似于以下的响应:

图 2.4:Minikube 首次启动

图 2.4:Minikube 首次启动

正如我们之前提到的,Minikube 在一个 VM 实例中启动了所有 Kubernetes 的组件。默认情况下,它使用 VirtualBox,并且您可以使用--vm-driver标志来指定特定的 hypervisor 驱动程序(例如hyperkit用于 macOS)。Minikube 还提供了--kubernetes-version标志,因此您可以指定要使用的 Kubernetes 版本。如果未指定,它将使用 Minikube 发布时可用的最新版本。在本章中,为了确保 Kubernetes 版本与 kubectl 版本的兼容性,我们明确指定了 Kubernetes 版本v1.16.2

以下命令应该有助于建立 Minikube 启动的 Kubernetes 集群是否正常运行。

  1. 使用以下命令获取集群各个组件的基本状态:
minikube status

您应该看到以下响应:

host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
  1. 现在,让我们看一下 kubectl 客户端和 Kubernetes 服务器的版本:
kubectl version --short

您应该看到以下响应:

Client Version: v1.16.2
Server Version: v1.16.2
  1. 让我们了解一下集群由多少台机器组成,并获取一些关于它们的基本信息:
kubectl get node

您应该看到类似以下的响应:

NAME          STATUS          ROLES          AGE          VERSION
minikube      Ready           master         2m41s        v1.16.2

完成这个练习后,您应该已经设置好了一个单节点的 Kubernetes 集群。在下一节中,我们将进入 Minikube 虚拟机,看看集群是如何组成的,以及使其工作的 Kubernetes 的各个组件。

Kubernetes 组件概述

通过完成上一个练习,您已经拥有一个单节点的 Kubernetes 集群正在运行。在开始您的第一场音乐会之前,让我们等一下,拉开帷幕,看看 Kubernetes 在幕后是如何架构的,然后检查 Minikube 是如何在其虚拟机内将其各个组件粘合在一起的。

Kubernetes 有几个核心组件,使机器的轮子转动。它们如下:

  • API 服务器

  • etcd

  • 控制器管理器

  • 调度器

  • Kubelet

这些组件对于 Kubernetes 集群的运行至关重要。

除了这些核心组件,您将在容器中部署您的应用程序,这些应用程序被捆绑在一起作为 pod。我们将在第五章 Pods中更多地了解 pod。这些 pod 和其他几个资源是由称为 API 对象的东西定义的。

API 对象描述了在 Kubernetes 中应该如何尊重某个资源。我们通常使用人类可读的清单文件来定义 API 对象,然后使用工具(如 kubectl)来解析它并将其交给 Kubernetes API 服务器。然后,Kubernetes 尝试创建对象中指定的资源,并将其状态与清单文件中指定的期望状态匹配。接下来,我们将带您了解 Minikube 创建的单节点集群中这些组件是如何组织和行为的。

Minikube 提供了一个名为minikube ssh的命令,用于从主机(在我们的机器上,它是运行 Ubuntu 20.04 的物理机)到minikube虚拟机的 SSH 访问,后者作为我们 Kubernetes 集群中唯一的节点。让我们看看它是如何工作的:

minikube ssh

您将看到以下输出:

图 2.5:通过 SSH 访问 Minikube 虚拟机

图 2.5:通过 SSH 访问 Minikube 虚拟机

注意

本节中将显示的所有命令都假定已在 Minikube 虚拟机内运行minikube ssh之后运行。

容器技术带来了封装应用程序的便利。Minikube 也不例外 - 它利用容器将 Kubernetes 组件粘合在一起。在 Minikube 虚拟机中,Docker 预先安装,以便它可以管理核心 Kubernetes 组件。您可以通过运行docker ps来查看这一点;但是,结果可能会让人不知所措,因为它包括所有正在运行的容器 - 包括核心 Kubernetes 组件和附加组件,以及所有列 - 这将输出一个非常大的表格。

为了简化输出并使其更易于阅读,我们将把docker ps的输出传输到另外两个 Bash 命令中:

  1. grep -v pause:这将通过不显示“沙盒”容器来过滤结果。

如果没有grep -v pause,您会发现每个容器都与一个“沙盒”容器(在 Kubernetes 中,它被实现为pause镜像)“配对”。这是因为,如前一章所述,Linux 容器可以通过加入相同(或不同)的 Linux 命名空间来关联(或隔离)。在 Kubernetes 中,“沙盒”容器用于引导 Linux 命名空间,然后运行真实应用程序的容器可以加入该命名空间。为了简洁起见,关于所有这些是如何在幕后工作的细节已被忽略。

注意

如果没有明确指定,本书中术语“命名空间”与“Kubernetes 命名空间”可以互换使用。在“Linux 命名空间”方面,“Linux”不会被省略以避免混淆。

  1. awk '{print $NF}':这将只打印最后一列的容器名称。

因此,最终命令如下:

docker ps | grep -v pause | awk '{print $NF}'

您应该看到以下输出:

图 2.6:通过运行 Minikube VM 获取容器列表

图 2.6:通过运行 Minikube VM 获取容器列表

在前面的截图中显示的突出显示的容器基本上是 Kubernetes 的核心组件。我们将在接下来的章节中详细讨论每一个。

etcd

分布式系统可能在任何时刻面临各种故障(网络、存储等)。为了确保在出现故障时仍能正常工作,关键的集群元数据和状态必须以可靠的方式存储。

Kubernetes 将集群元数据和状态抽象为一系列 API 对象。例如,节点 API 对象代表了 Kubernetes 工作节点的规范,以及其最新状态。

Kubernetes 使用etcd作为后端键值数据库,在 Kubernetes 集群的生命周期中持久化 API 对象。重要的是要注意,没有任何东西(内部集群资源或外部客户端)被允许直接与 etcd 通信,而必须通过 API 服务器。对 etcd 的任何更新或请求都只能通过对 API 服务器的调用来进行。

实际上,etcd 通常部署多个实例,以确保数据以安全和容错的方式持久化。

API 服务器

API 服务器允许标准 API 访问 Kubernetes API 对象。它是唯一与后端存储(etcd)通信的组件。

此外,通过利用它作为与 etcd 通信的唯一接触点,它为客户端提供了一个方便的接口,以“监视”它们可能感兴趣的任何 API 对象。一旦 API 对象被创建、更新或删除,正在“监视”的客户端将立即收到通知,以便他们可以对这些更改采取行动。正在“监视”的客户端也被称为“控制器”,它已经成为内置 Kubernetes 对象和 Kubernetes 扩展中广受欢迎的实体。

注意

您将在第四章“如何与 Kubernetes 通信”(API 服务器)中了解更多关于 API 服务器的信息,并在第七章“Kubernetes 控制器”中了解有关控制器的信息。

调度器

调度程序负责将传入的工作负载分配给最合适的节点。关于分配的决定是由调度程序对整个集群的理解以及一系列调度算法来做出的。

注意

您将在《第十七章》《Kubernetes 高级调度》中了解更多关于调度程序的信息。

控制器管理器

正如我们在《API 服务器》小节中提到的,API 服务器公开了几乎任何 API 对象的“监视”方式,并通知观察者有关正在观察的 API 对象的更改。

它的工作方式几乎与发布者-订阅者模式相似。控制器管理器充当典型的订阅者,监视它感兴趣的唯一 API 对象,然后尝试进行适当的更改,以将当前状态移向对象中描述的期望状态。

例如,如果它从 API 服务器那里得到一个更新,说一个应用程序要求两个副本,但是现在集群中只有一个副本,它将创建第二个副本,以使应用程序符合其期望的副本数量。协调过程在控制器管理器的生命周期中持续运行,以确保所有应用程序保持在预期状态。

控制器管理器聚合各种类型的控制器,以遵守 API 对象的语义,例如部署和服务,我们将在本章后面介绍。

kubelet 在哪里?

请注意,etcd、API 服务器、调度程序和控制器管理器组成了 Kubernetes 的控制平面。运行这些组件的机器称为主节点。另一方面,kubelet 部署在每台工作节点上。

在我们的单节点 Minikube 集群中,kubelet 部署在携带控制平面组件的同一节点上。然而,在大多数生产环境中,它不会部署在任何主节点上。当我们在《第十一章》《构建您自己的 HA 集群》中部署多节点集群时,我们将了解更多关于生产环境的信息。

kubelet 主要是与底层容器运行时(例如 Docker、containerd 或 cri-o)进行通信,以启动容器并确保容器按预期运行。此外,它负责将状态更新发送回 API 服务器。

然而,如前面的屏幕截图所示,docker ps命令并没有显示任何名为kubelet的内容。通常,为了启动、停止或重新启动任何软件并使其在失败时自动重新启动,我们需要一个工具来管理其生命周期。在 Linux 中,systemd 负责这个责任。在 Minikube 中,kubelet 由 systemd 管理,并作为本地二进制文件而不是 Docker 容器运行。我们可以运行以下命令来检查其状态:

systemctl status kubelet

您应该看到类似以下的输出:

图 2.7:kubelet 的状态

图 2.7:kubelet 的状态

默认情况下,kubelet 在其配置文件中(存储在/var/lib/kubelet/config.yaml)具有staticPodPath的配置。kubelet 被指示持续监视该路径下文件的更改,该路径下的每个文件代表一个 Kubernetes 组件。让我们首先找到 kubelet 的config文件中的staticPodPath,以了解这意味着什么:

grep "staticPodPath" /var/lib/kubelet/config.yaml

您应该看到以下输出:

staticPodPath: /etc/kubernetes/manifests

现在,让我们看看这个路径的内容:

ls /etc/kubernetes/manifests

您应该看到以下输出:

addon-manager.yaml.tmpl kube-apiserver.yaml      kube-scheduler.yaml
etcd.yaml               kube-controller-manager.yaml

如文件列表所示,Kubernetes 的核心组件由在 YAML 文件中指定定义的对象定义。在 Minikube 环境中,除了管理用户创建的 pod 之外,kubelet 还充当 systemd 的等效物,以管理 Kubernetes 系统级组件的生命周期,如 API 服务器、调度程序、控制器管理器和其他附加组件。一旦这些 YAML 文件中的任何一个发生变化,kubelet 会自动检测到并更新集群的状态,使其与更新后的 YAML 配置中定义的期望状态相匹配。

我们将在这里停下,不深入探讨 Minikube 的设计。除了“静态组件”之外,kubelet 还是“常规应用程序”的管理者,以确保它们在节点上按预期运行,并根据 API 规范或资源短缺驱逐 pod。

kube-proxy

kube-proxy 出现在docker ps命令的输出中,但在我们在上一小节中探索该目录时,它并不存在于/etc/kubernetes/manifests中。这意味着它的角色——它更多地被定位为一个附加组件,而不是核心组件。

kube-proxy 被设计为在每个节点上运行的分布式网络路由器。它的最终目标是确保流入到 Service(这是我们稍后将介绍的一个 API 对象)端点的流量能够正确路由。此外,如果多个容器提供一个应用程序,它可以通过利用底层的 Linux iptables/IPVS 技术以循环方式平衡流量。

还有一些其他附加组件,比如 CoreDNS,但我们将跳过它们,以便我们可以专注于核心组件并获得高层次的图像。

注意

有时,kube-proxy 和 CoreDNS 也被认为是 Kubernetes 安装的核心组件。在某种程度上,从技术上讲,这是正确的,因为它们在大多数情况下是必需的;否则,Service API 对象将无法工作。然而,在本书中,我们更倾向于将它们归类为“附加组件”,因为它们侧重于实现特定的 Kubernetes API 资源,而不是一般的工作流程。此外,kube-proxy 和 CoreDNS 是在addon-manager.yaml.tmpl中定义的,而不是被描绘在与其他核心 Kubernetes 组件同一级别。

Kubernetes 架构

在前一节中,我们对核心 Kubernetes 组件有了初步印象:etcd、API 服务器、调度器、控制器管理器和 kubelet。这些组件,加上其他附加组件,构成了 Kubernetes 架构,可以在以下图表中看到:

图 2.8:Kubernetes 架构

图 2.8:Kubernetes 架构

在这一点上,我们不会过多地查看每个组件。然而,在高层次上,理解组件如何相互通信以及它们为什么以这种方式设计是至关重要的。

首先要理解的是 API 服务器可以与哪些组件进行交互。从前面的图表中,我们可以很容易地看出 API 服务器几乎可以与每个其他组件进行通信(除了容器运行时,由 kubelet 处理),它还可以直接与最终用户进行交互。这种设计使 API 服务器充当 Kubernetes 的“心脏”。此外,API 服务器还会审查传入的请求,并将 API 对象写入后端存储(etcd)。换句话说,这使得 API 服务器成为安全控制措施(如身份验证、授权和审计)的节流阀。

理解的第二件事是不同的 Kubernetes 组件(除了 API 服务器)如何相互交互。事实证明它们之间没有明确的连接 - 控制器管理器不与调度程序交谈,kubelet 也不与 kube-proxy 交谈。

没错 - 他们确实需要协调工作来完成许多功能,但它们从不直接交谈。相反,它们通过 API 服务器隐式通信。更准确地说,它们通过观察、创建、更新或删除相应的 API 对象进行通信。这也被称为控制器/操作员模式。

容器网络接口

有几个网络方面需要考虑,比如一个 pod 如何与其主机的网络接口通信,一个节点如何与其他节点通信,最终一个 pod 如何与不同节点上的任何 pod 通信。由于云端或本地环境中的网络基础设施差异巨大,Kubernetes 选择通过定义一个称为容器网络接口CNI)的规范来解决这些问题。不同的 CNI 提供者可以遵循相同的接口并实现符合 Kubernetes 标准的逻辑,以确保整个 Kubernetes 网络运行。我们将在第十一章构建您自己的 HA 集群中重新讨论 CNI 的概念。现在,让我们回到讨论不同的 Kubernetes 组件如何工作。

在本章的后面,练习 2.05Kubernetes 如何管理 Pod 的生命周期,将帮助您巩固对此的理解,并澄清一些问题,比如不同的 Kubernetes 组件如何同步或异步地操作,以确保典型的 Kubernetes 工作流程,以及如果其中一个或多个组件发生故障会发生什么。这个练习将帮助您更好地理解整体的 Kubernetes 架构。但在那之前,让我们把我们在上一章中介绍的容器化应用引入到 Kubernetes 世界中,并探索 Kubernetes 的一些好处。

将容器化应用迁移到 Kubernetes

在上一章中,我们构建了一个名为k8s-for-beginners的简单 HTTP 服务器,并且它作为一个 Docker 容器运行。对于一个示例应用程序来说,它运行得很完美。但是,如果你需要管理成千上万个容器,并且正确协调和调度它们,该怎么办?你如何在没有停机的情况下升级一个服务?在意外故障时如何保持服务的健康?这些问题超出了仅仅使用容器的系统的能力。我们需要的是一个可以编排和管理我们的容器的平台。

我们已经告诉过你,Kubernetes 是我们需要的解决方案。接下来,我们将带你进行一系列关于如何使用 Kubernetes 本地方法编排和运行容器的练习。

Pod 规范

一个直观的想法是,我们希望看到在 Kubernetes 中运行容器的等效 API 调用或命令是什么。正如第一章 Kubernetes 和容器简介中所解释的,一个容器可以加入另一个容器的命名空间,以便它们可以访问彼此的资源(例如网络、存储等),而无需额外的开销。在现实世界中,一些应用程序可能需要多个容器密切合作,无论是并行工作还是按特定顺序工作(一个的输出将由另一个处理)。此外,一些通用容器(例如日志代理、网络限速代理等)可能需要与它们的目标容器密切合作。

由于一个应用程序通常可能需要多个容器,容器不是 Kubernetes 中的最小操作单元;相反,它引入了一个称为pods的概念来捆绑一个或多个容器。Kubernetes 提供了一系列规范来描述这个 pod 应该是什么样的,包括诸如镜像、资源请求、启动命令等几个具体的内容。为了将这个 pod 规范发送给 Kubernetes,特别是 Kubernetes API 服务器,我们将使用 kubectl。

注意

我们将在第五章 Pods中了解更多关于 Pods 的内容,但在本章中,我们将使用它们进行简单演示。您可以在此链接查看可用 Pod 规范的完整列表:godoc.org/k8s.io/api/core/v1#PodSpec

接下来,让我们学习如何通过编写 pod 规范文件(也称为规范、清单、配置或配置文件)在 Kubernetes 中运行单个容器。在 Kubernetes 中,您可以使用 YAML 或 JSON 来编写此规范文件,尽管 YAML 通常更常用,因为它更易读和可编辑。

考虑以下用于一个非常简单的 pod 的 YAML 规范:

kind: Pod
apiVersion: v1
metadata:
  name: k8s-for-beginners
spec:
  containers:
  - name: k8s-for-beginners
    image: packtworkshops/the-kubernetes-workshop:k8s-for-beginners

让我们简要地浏览一下不同的字段:

  • kind 告诉 Kubernetes 您想要创建哪种类型的对象。在这里,我们正在创建一个 Pod。在后面的章节中,您将看到许多其他类型,比如 Deployment、StatefulSet、ConfigMap 等等。

  • apiVersion 指定 API 对象的特定版本。不同版本可能会有一些不同的行为。

  • metadata 包括一些属性,可以用来唯一标识 pod,比如名称和命名空间。如果我们不指定命名空间,它就会放在 default 命名空间中。

  • spec 包含一系列描述 pod 的字段。在这个例子中,有一个容器,它有指定的镜像 URL 和名称。

Pod 是部署的最简单的 Kubernetes 对象之一,因此我们将使用它们来学习如何使用 YAML 清单部署对象。

应用 YAML 清单

一旦我们准备好一个 YAML 清单,我们可以使用 kubectl apply -f <yaml file>kubectl create -f <yaml file> 来指示 API 服务器持久化在此清单中定义的 API 资源。当您首次从头开始创建一个 pod 时,您使用这两个命令之一并没有太大的区别。然而,我们经常需要修改 YAML(比如说,如果我们想要升级镜像版本),然后重新应用它。如果我们使用 kubectl create 命令,我们必须删除并重新创建它。但是,使用 kubectl apply 命令,我们可以重新运行相同的命令,Kubernetes 会自动计算并应用增量变化。

从运维的角度来看,这非常方便。例如,如果我们使用某种形式的自动化,重复相同的命令会更简单。因此,我们将在接下来的练习中使用 kubectl apply,无论是第一次应用还是不是。

注意

可以在 第四章 如何与 Kubernetes(API 服务器)通信 中获取有关 kubectl 的详细信息。

练习 2.02:在 Kubernetes 中运行一个 Pod

在上一个练习中,我们启动了 Minikube,并查看了各种作为 pod 运行的 Kubernetes 组件。现在,在这个练习中,我们将部署我们的 pod。按照以下步骤完成这个练习:

注意

如果您一直在尝试Kubernetes 组件概述部分的命令,请不要忘记在开始这个练习之前使用exit命令离开 SSH 会话。除非另有说明,所有使用kubectl的命令应该在主机上运行,而不是在 Minikube VM 内部。

  1. 在 Kubernetes 中,我们使用一个 spec 文件来描述一个 API 对象,比如一个 pod。如前所述,我们将坚持使用 YAML,因为它更易读和易编辑。创建一个名为k8s-for-beginners-pod.yaml的文件(使用你选择的任何文本编辑器),内容如下:
kind: Pod
apiVersion: v1
metadata:
  name: k8s-for-beginners
spec:
  containers:
  - name: k8s-for-beginners
    image: packtworkshops/the-kubernetes-workshop:k8s-for-      beginners

注意

请用前面 YAML 文件中最后一行的路径替换成您在上一章中创建的图像的路径。

  1. 在主机上运行以下命令来创建这个 pod:
kubectl apply -f k8s-for-beginners-pod.yaml

您应该看到以下输出:

pod/k8s-for-beginners created
  1. 现在,我们可以使用以下命令来检查 pod 的状态:
kubectl get pod

您应该看到以下响应:

NAME                   READY     STATUS      RESTARTS       AGE
k8s-for-beginners      1/1       Running     0              7s

默认情况下,kubectl get pod将以表格格式列出所有的 pod。在前面的输出中,我们可以看到k8s-for-beginners pod 正常运行,并且它有一个容器是就绪的(1/1)。此外,kubectl 提供了一个额外的标志叫做-o,这样我们可以调整输出格式。例如,-o yaml-o json将分别以 YAML 或 JSON 格式返回 pod API 对象的完整输出,因为它存储在 Kubernetes 的后端存储(etcd)中。

  1. 您可以使用以下命令获取有关 pod 的更多信息:
kubectl get pod -o wide

您应该看到以下输出:

图 2.9:获取有关 pod 的更多信息

图 2.9:获取有关 pod 的更多信息

正如你所看到的,输出仍然是表格格式,我们得到了额外的信息,比如IP(内部 pod IP)和NODE(pod 所在的节点)。

  1. 您可以通过运行以下命令来获取我们集群中节点的列表:
kubectl get node

您应该看到以下响应:

NAME          STATUS          ROLES          AGE          VERSION
minikube      Ready           master         30h          v1.16.2
  1. 图 2.9中列出的 IP 是 Kubernetes 为此 pod 分配的内部 IP,用于 pod 之间的通信,而不是用于将外部流量路由到 pod。因此,如果您尝试从集群外部访问此 IP,您将得到空白。您可以尝试使用以下命令从主机上执行,但会失败:
curl 172.17.0.4:8080

注意

请记得将172.17.0.4更改为您在步骤 4中获得的值,如图 2.9所示。

curl命令将会挂起并返回空白,如下所示:

k8suser@ubuntu:~$ curl 172.17.0.4:8080
^C

您需要按下Ctrl + C来中止它。

  1. 在大多数情况下,最终用户不需要与内部 pod IP 进行交互。但是,仅出于观察目的,让我们 SSH 进入 Minikube VM:
minikube ssh

您将在终端中看到以下响应:

图 2.10:通过 SSH 访问 Minikube VM

图 2.10:通过 SSH 访问 Minikube VM

  1. 现在,尝试从 Minikube VM 内部调用 IP 以验证其是否有效:
curl 172.17.0.4:8080

您应该会收到一个成功的响应:

Hello Kubernetes Beginners!

有了这个,我们已经成功在 Kubernetes 集群上部署了我们的应用程序。我们可以确认它正在工作,因为当我们从集群内部调用应用程序时,我们会得到一个响应。现在,您可以使用exit命令结束 Minikube SSH 会话。

Service 规范

前一节的最后部分证明了集群内不同组件之间的网络通信非常顺畅。但在现实世界中,您不希望应用程序的用户获得 SSH 访问权限来使用您的应用程序。因此,您希望您的应用程序可以从外部访问。

为了方便起见,Kubernetes 提供了一个称为Service的概念,用于抽象应用程序 pod 的网络访问。Service 充当网络代理,接受来自外部用户的网络流量,然后将其分发到内部 pod。但是,应该有一种方法来描述 Service 和相应 pod 之间的关联规则。Kubernetes 使用标签(在 pod 定义中定义)和标签选择器(在 Service 定义中定义)来描述这种关系。

注意

您将在第六章标签和注释中了解更多关于标签和标签选择器的内容。

让我们考虑以下 Service 的样本规范:

kind: Service
apiVersion: v1
metadata:
  name: k8s-for-beginners
spec:
  selector:
    tier: frontend
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080

与 Pod 规范类似,在这里,我们定义了 kindapiVersion,而 name 是在 metadata 字段下定义的。在 spec 字段下,有几个关键字段需要注意:

  • selector 定义要选择的标签,以便与相应的 pod 匹配关系,正如您将在接下来的练习中看到的,这些标签应该被正确地标记。

  • type 定义了服务的类型。如果未指定,默认类型为 ClusterIP,这意味着它仅在集群内部使用。在这里,我们将其指定为 NodePort。这意味着服务将在集群的每个节点上公开一个端口,并将该端口与相应的 pod 关联起来。另一个众所周知的类型称为 LoadBalancer,通常不在原始的 Kubernetes 提供中实现。相反,Kubernetes 将实现委托给每个云提供商,例如 GKE、EKS 等。

  • ports 包括一系列 port 字段,每个字段都有一个 targetPort 字段。targetPort 字段是目标 pod 公开的实际端口。

因此,可以通过 <service ip>:<port> 内部访问服务。现在,例如,如果您有一个在内部运行并在端口 8080 上侦听的 NGINX pod,则应将 targetPort 定义为 8080。您可以在此案例中为 port 字段指定任意数字,例如 80。Kubernetes 将建立并维护 <service IP>:<port><pod IP>:<targetPort> 之间的映射。在接下来的练习中,我们将学习如何从集群外访问服务,并通过服务将外部流量带入集群。

在接下来的练习中,我们将定义服务清单并使用 kubectl apply 命令创建它们。您将了解到在 Kubernetes 中解决问题的常见模式是找到适当的 API 对象,然后使用 YAML 清单组合详细规范,最后创建对象以使其生效。

练习 2.03:通过服务访问 Pod

在之前的练习中,我们观察到内部 pod IP 对于集群外部的任何人都不起作用。在这个练习中,我们将创建服务,这些服务将充当连接器,将外部请求映射到目标 pod,以便我们可以在不进入集群的情况下外部访问 pod。按照以下步骤完成这个练习:

  1. 首先,让我们调整来自 练习 2.02在 Kubernetes 中运行一个 Pod 的 pod 规范,以应用一些标签。修改 k8s-for-beginners-pod1.yaml 文件的内容如下:
kind: Pod
apiVersion: v1
metadata:
  name: k8s-for-beginners
  labels:
    tier: frontend
spec:
  containers:
  - name: k8s-for-beginners
    image: packtworkshops/the-kubernetes-workshop:k8s-for-      beginners

在这里,我们在 labels 字段下添加了一个标签对,tier: frontend

  1. 因为 pod 名称保持不变,让我们重新运行 apply 命令,这样 Kubernetes 就知道我们正在尝试更新 pod 的规范,而不是创建一个新的 pod:
kubectl apply -f k8s-for-beginners-pod1.yaml

你应该看到以下响应:

pod/k8s-for-beginners configured

kubectl apply 命令背后,kubectl 生成指定 YAML 和 Kubernetes 服务器端存储(即 etcd)中存储版本的差异。如果请求有效(即,我们在规范格式或命令中没有出现任何错误),kubectl 将向 Kubernetes API 服务器发送 HTTP 补丁。因此,只会应用增量更改。如果查看返回的消息,你会看到它说 pod/k8s-for-beginners configured 而不是 created,所以我们可以确定它正在应用增量更改,而不是创建一个新的 pod。

  1. 你可以使用以下命令显式显示已应用到现有 pod 的标签:
kubectl get pod --show-labels

你应该看到以下响应:

NAME              READY  STATUS   RESTARTS   AGE  LABELS
k8s-for-beginners 1/1    Running  0          16m  tier=frontend

现在,pod 具有 tier: frontend 属性,我们准备创建一个服务并将其链接到这些 pod。

  1. 创建一个名为 k8s-for-beginners-svc.yaml 的文件,内容如下:
kind: Service
apiVersion: v1
metadata:
  name: k8s-for-beginners
spec:
  selector:
    tier: frontend
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
  1. 现在,让我们使用以下命令创建服务:
kubectl apply -f k8s-for-beginners-svc.yaml

你应该看到以下响应:

service/k8s-for-beginners created
  1. 使用 get 命令返回已创建服务的列表,并确认我们的服务是否在线:
kubectl get service

你应该看到以下响应:

图 2.11:获取服务列表

图 2.11:获取服务列表

所以,你可能已经注意到 PORT(S) 列输出 80:32571/TCP。端口 32571 是在每个节点上暴露的自动生成的端口,这是有意为之,以便外部用户可以访问它。现在,在进行下一步之前,退出 SSH 会话。

  1. 现在,我们有了“外部端口”为 32571,但我们仍然需要找到外部 IP。Minikube 提供了一个实用程序,我们可以使用它轻松访问 k8s-for-beginners 服务:
minikube service k8s-for-beginners

应该看到类似以下的响应:

图 2.12:获取访问 NodePort 服务的 URL 和端口

图 2.12:获取访问 NodePort 服务的 URL 和端口

根据您的环境,这可能还会自动打开一个浏览器页面,以便您可以访问服务。从 URL 中,您将能够看到服务端口是32571。外部 IP 实际上是 Minikube VM 的 IP。

  1. 您还可以通过命令行从集群外部访问我们的应用:
curl http://192.168.99.100:32571

您应该看到以下响应:

Hello Kubernetes Beginners!

总之,在这个练习中,我们创建了一个NodePort服务,以便外部用户可以访问内部的 Pod,而不需要进入集群。在幕后,有几个层次的流量转换使这成为可能:

  • 第一层是从外部用户到机器 IP 的自动生成的随机端口(3XXXX)。

  • 第二层是从随机端口(3XXXX)到服务 IP(10.X.X.X)的端口80

  • 第三层是从服务 IP(10.X.X.X)最终到端口8080的 Pod IP。

以下是一个说明这些交互的图表:

图 2.13:将来自集群外部用户的流量路由到运行我们应用的 Pod 到运行我们应用的 Pod

图 2.13:将来自集群外部用户的流量路由到运行我们应用的 Pod

服务和 Pod

在上一个练习的步骤 3中,您可能已经注意到服务尝试通过标签(spec部分下的selector字段)来匹配 Pod,而不是使用固定的 Pod 名称或类似的东西。从 Pod 的角度来看,它不需要知道哪个服务正在为其带来流量。(在一些罕见的情况下,它甚至可以映射到多个服务;也就是说,多个服务可能会向一个 Pod 发送流量。)

这种基于标签的匹配机制在 Kubernetes 中被广泛使用。它使 API 对象在运行时松散耦合。例如,您可以指定tier: frontend作为标签选择器,这将与被标记为tier: frontend的 Pod 相关联。

因此,一旦创建了 Service,备份 pod 是否存在都无关紧要。备份 pod 后来创建也是完全可以接受的,创建后,Service 对象将与正确的 pod 关联起来。在内部,整个映射逻辑是由服务控制器实现的,它是控制器管理器组件的一部分。Service 可能一次有两个匹配的 pod,并且后来创建了一个具有匹配标签的第三个 pod,或者其中一个现有的 pod 被删除。在任何一种情况下,服务控制器都可以检测到这些更改,并确保用户始终可以通过 Service 端点访问其应用程序。

在 Kubernetes 中使用不同类型的 API 对象来编排应用程序,然后通过使用标签或其他松散耦合的约定将它们粘合在一起是一个非常常见的模式。这也是容器编排的关键部分。

交付 Kubernetes 原生应用程序

在前面的部分中,我们将基于 Docker 的应用程序迁移到了 Kubernetes,并成功地从 Minikube VM 内部和外部访问了它。现在,让我们看看如果我们从头开始设计我们的应用程序,使其可以使用 Kubernetes 进行部署,Kubernetes 还可以提供哪些其他好处。

随着您的应用程序使用量增加,运行多个特定 pod 的副本以提供业务功能可能很常见。在这种情况下,仅仅将不同容器分组在一个 pod 中是不够的。我们需要继续创建一组共同工作的 pod。Kubernetes 为 pod 组提供了几种抽象,例如 Deployments、DaemonSets、Jobs、CronJobs 等。就像 Service 对象一样,这些对象也可以通过在 YAML 文件中定义的 spec 来创建。

要开始了解 Kubernetes 的好处,让我们使用 Deployment 来演示如何在多个 pod 中复制(扩展/缩减)应用程序。

使用 Kubernetes 对 pod 组进行抽象化给我们带来了以下优势:

  • 创建 pod 的副本以实现冗余:这是使用 Deployments 等 pod 组抽象的主要优势。Deployment 可以根据给定的 spec 创建多个 pod。Deployment 将自动确保它创建的 pod 处于在线状态,并将自动替换任何失败的 pod。

  • 简单的升级和回滚:Kubernetes 提供了不同的策略,你可以使用这些策略来升级你的应用程序,以及回滚版本。这很重要,因为在现代软件开发中,软件经常是迭代开发的,更新频繁。升级可以改变部署规范中的任何内容。它可以是标签或任何其他字段的更新,镜像版本的升级,对其嵌入式容器的更新等等。

让我们来看一下样本部署规范的一些值得注意的方面:

k8s-for-beginners-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-for-beginners
spec:
  replicas: 3
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: k8s-for-beginners
        image: packtworkshops/the-kubernetes-workshop:k8s-for-          beginners

除了将 pod 规范包装为 "template",部署还必须指定其种类(Deployment),以及 API 版本(apps/v1)。

注意

出于某些历史原因,规范名称 apiVersion 仍在使用。但从技术上讲,它实际上意味着 apiGroupVersion。在前面的部署示例中,它属于 apps 组,版本为 v1

在部署规范中,replicas 字段指示 Kubernetes 使用在 template 字段中定义的 pod 规范启动三个 pod。selector 字段扮演了与服务案例中相同的角色 - 它旨在以一种松散耦合的方式将部署对象与特定的 pod 关联起来。如果你想要将任何现有的 pod 纳入新部署的管理,这将特别有用。

在部署或其他类似的 API 对象中定义的副本数量代表了持续运行的 pod 数量的期望状态。如果其中一些 pod 因某些意外原因而失败,Kubernetes 将自动检测到并创建相应数量的 pod 来替代它们。我们将在接下来的练习中探讨这一点。

我们将在接下来的练习中看到部署的实际操作。

练习 2.04:扩展 Kubernetes 应用程序

在 Kubernetes 中,通过更新部署规范的 replicas 字段,很容易增加运行应用程序的副本数量。在这个练习中,我们将尝试如何扩展 Kubernetes 应用程序的规模。按照以下步骤完成这个练习:

  1. 使用这里显示的内容创建一个名为 k8s-for-beginners-deploy.yaml 的文件:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-for-beginners
spec:
  replicas: 3
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: k8s-for-beginners
        image: packtworkshops/the-kubernetes-workshop:k8s-for-          beginners

如果你仔细看,你会发现这个部署规范在很大程度上基于之前练习中的 pod 规范(k8s-for-beginners-pod1.yaml),你可以在 template 字段下看到。

  1. 接下来,我们可以使用 kubectl 来创建部署:
kubectl apply -f k8s-for-beginners-deploy.yaml

您应该会看到以下输出:

deployment.apps/k8s-for-beginners created
  1. 鉴于部署已经成功创建,我们可以使用以下命令来显示所有部署的状态,比如它们的名称、运行的 pod 等等:
kubectl get deploy

您应该会得到以下响应:

NAME                   READY   UP-TO-DATE   AVAILABLE    AGE
k8s-for-beginners      3/3     3            3            41s

注意

如前面的命令所示,我们使用的是deploy而不是deployment。这两者都可以使用,deploydeployment的允许的简称。您可以在此链接找到一些常用的简称列表:kubernetes.io/docs/reference/kubectl/overview/#resource-types

您也可以通过运行kubectl api-resources来查看短名称,而不指定资源类型。

  1. 我们在上一个练习中创建的名为k8s-for-beginners的 pod 存在。为了确保我们只看到由部署管理的 pod,让我们删除旧的 pod:
kubectl delete pod k8s-for-beginners

您应该会看到以下响应:

pod "k8s-for-beginners" deleted
  1. 现在,获取所有的 pod 列表:
kubectl get pod

您应该会看到以下响应:

图 2.14:获取 pod 列表

图 2.14:获取 pod 列表

部署已经创建了三个 pod,并且它们的标签(在步骤 1中指定的labels字段)恰好与我们在上一节中创建的 Service 匹配。那么,如果我们尝试访问 Service 会发生什么呢?网络流量会聪明地路由到这三个新的 pod 吗?让我们来测试一下。

  1. 为了查看流量是如何分配到这三个 pod 的,我们可以通过在 Bash 的for循环中运行curl命令来模拟一系列连续的请求到 Service 端点,如下所示:
for i in $(seq 1 30); do curl <minikube vm ip>:<service node port>; done

注意

在这个命令中,如果您正在运行相同的 Minikube 实例,请使用与上一个练习中相同的 IP 和端口。如果您重新启动了 Minikube 或进行了其他更改,请按照上一个练习的步骤 9获取您的 Minikube 集群的正确 IP。

一旦您使用正确的 IP 和端口运行了命令,您应该会看到以下输出:

图 2.15:重复访问我们的应用

图 2.15:重复访问我们的应用

从输出中,我们可以看出所有 30 个请求都得到了预期的响应。

  1. 您可以运行kubectl logs <pod name>来检查每个 pod 的日志。让我们再进一步,找出每个 pod 实际响应的确切请求数,这可能有助于我们找出流量是否均匀分布。为此,我们可以将每个 pod 的日志传输到wc命令中以获取行数:
kubectl logs <pod name> | wc -l

运行上述命令三次,复制您获得的 pod 名称,如图 2.16所示:

图 2.16:获取运行我们应用程序的三个 pod 副本的日志

图 2.16:获取运行我们应用程序的三个 pod 副本的日志

结果显示,三个 pod 分别处理了91011个请求。由于样本量较小,分布并不绝对均匀(即每个10),但足以表明服务使用的默认轮询分发策略。

注意

您可以通过查看官方文档了解 kube-proxy 如何利用 iptables 执行内部负载平衡:kubernetes.io/docs/concepts/services-networking/service/#proxy-mode-iptables

  1. 接下来,让我们学习如何扩展部署。有两种方法可以实现这一点:一种方法是修改部署的 YAML 配置,我们可以将replicas的值设置为另一个数字(例如5),另一种方法是使用kubectl scale命令,如下所示:
kubectl scale deploy k8s-for-beginners --replicas=5

您应该会看到以下响应:

deployment.apps/k8s-for-beginners scaled
  1. 让我们验证一下是否有五个 pod 在运行:
kubectl get pod

您应该会看到类似以下的响应:

图 2.17:获取 pod 列表

图 2.17:获取 pod 列表

输出显示现有的三个 pod 被保留,另外创建了两个新的 pod。

  1. 同样,您也可以指定小于当前数量的副本。在我们的示例中,假设我们想将副本数量缩减到2。此命令如下所示:
kubectl scale deploy k8s-for-beginners --replicas=2

您应该会看到以下响应:

deployment.apps/k8s-for-beginners scaled
  1. 现在,让我们验证一下 pod 的数量:
kubectl get pod

您应该会看到类似以下的响应:

图 2.18:获取 pod 列表

图 2.18:获取 pod 列表

如前面的截图所示,有两个 pod,它们都按预期运行。因此,在 Kubernetes 术语中,我们可以说,“部署处于期望的状态”。

  1. 我们可以运行以下命令来验证这一点:
kubectl get deploy

你应该看到以下响应:

NAME                   READY    UP-TO-DATE   AVAILABLE    AGE
k8s-for-beginners      2/2      2            2           19m
  1. 现在,让我们看看如果我们删除两个 pod 中的一个会发生什么:
kubectl delete pod <pod name>

你应该得到以下响应:

pod "k8s-for-beginners-66644bb776-7j9mw" deleted
  1. 检查 pod 的状态以查看发生了什么:
kubectl get pod

你应该看到以下响应:

图 2.19:获取 pod 列表

图 2.19:获取 pod 列表

我们可以看到仍然有两个 pod。从输出中值得注意的是,第一个 pod 的名称与图 2.18中的第二个 pod 的名称相同(这是未被删除的那个),但是突出显示的 pod 名称与图 2.18中的任何一个 pod 的名称都不同。这表明突出显示的那个是新创建的用来替换已删除的 pod。部署创建了一个新的 pod,以使运行中的 pod 数量满足部署的期望状态。

在这个练习中,我们学习了如何扩展部署的规模。您可以以相同的方式扩展其他类似的 Kubernetes 对象,例如 DaemonSets 和 StatefulSets。此外,对于这样的对象,Kubernetes 将尝试自动恢复失败的 pod。

Pod 生命周期和 Kubernetes 组件

本章的前几节简要描述了 Kubernetes 组件以及它们如何在内部相互工作。另一方面,我们还演示了如何使用一些 Kubernetes API 对象(Pods、Services 和 Deployments)来组合您的应用程序。

但是 Kubernetes API 对象如何由不同的 Kubernetes 组件管理呢?让我们以 pod 为例。其生命周期可以如下所示:

图 2.20:创建 pod 的过程

图 2.20:创建 pod 的过程

整个过程可以分解如下:

  1. 用户通过向 Kubernetes API 服务器发送部署 Deployment YAML 清单来部署应用程序。API 服务器验证请求并检查其是否有效。如果有效,它将持久化部署 API 对象到其后端数据存储(etcd)。

注意

对于通过修改 API 对象演变的任何步骤,etcd 和 API 服务器之间必须发生交互,因此我们不会将交互列为额外的步骤。

  1. 到目前为止,pod 还没有被创建。控制器管理器从 API 服务器那里收到通知,部署已经被创建。

  2. 然后,控制器管理器会检查所需数量的副本 pod 是否已经在运行。

  3. 如果正在运行的 Pod 数量不足,它会创建适当数量的 Pod。创建 Pod 是通过向 API 服务器发送具有 Pod 规范的请求来完成的。这与用户应用部署 YAML 的方式非常相似,但主要区别在于这是以编程方式在控制器管理器内部发生的。

  4. 尽管 Pod 已经被创建,但它们只是存储在 etcd 中的一些 API 对象。现在,调度器从 API 服务器那里收到通知,称新的 Pod 已被创建,但尚未分配节点来运行它们。

  5. 调度器检查资源使用情况,以及现有的 Pod 分配情况,然后计算最适合每个新 Pod 的节点。在这一步结束时,调度器通过将 Pod 的nodeName规范设置为所选节点,向 API 服务器发送更新请求。

  6. 到目前为止,Pod 已被分配到适当的节点上运行。然而,没有运行实际的容器。换句话说,应用程序还没有运行。每个 kubelet(运行在不同的工作节点上)都会收到通知,指示某些 Pod 应该被运行。然后,每个 kubelet 将检查将要运行的 Pod 是否已被分配到 kubelet 正在运行的节点。

  7. 一旦 kubelet 确定一个 Pod 应该在其节点上,它会调用底层的容器运行时(例如 Docker、containerd 或 cri-o)在主机上启动容器。一旦容器启动,kubelet 负责向 API 服务器报告其状态。

有了这个基本流程,现在你应该对以下问题的答案有一个模糊的理解:

  • 谁负责创建 Pod?创建后 Pod 的状态是什么?

  • 谁负责放置 Pod?放置后 Pod 的状态是什么?

  • 谁启动具体的容器?

  • 谁负责整体消息传递过程,以确保所有组件协同工作?

在接下来的练习中,我们将使用一系列具体的实验来帮助您巩固这一理解。这将让您看到事情在实践中是如何运作的。

练习 2.05:Kubernetes 如何管理 Pod 的生命周期

由于 Kubernetes 集群包括多个组件,并且每个组件同时工作,通常很难知道每个 pod 生命周期的每个阶段发生了什么。为了解决这个问题,我们将使用电影剪辑技术来“以慢动作播放整个生命周期”,以便观察每个阶段。我们将关闭主平面组件,然后尝试创建一个 pod。然后,我们将响应我们看到的错误,并逐步将每个组件逐个上线。这将使我们能够放慢速度,逐步检查 pod 创建过程的每个阶段。按照以下步骤完成此练习:

  1. 首先,让我们使用以下命令删除之前创建的部署和服务:
kubectl delete deploy k8s-for-beginners && kubectl delete service k8s-for-beginners

您应该看到以下响应:

deployment.apps "k8s-for-beginners" deleted
service "k8s-for-beginners" deleted
  1. 准备两个终端会话:一个(主机终端)用于在主机上运行命令,另一个(Minikube 终端)用于通过 SSH 在 Minikube VM 内部传递命令。因此,您的 Minikube 会话将像这样启动:
minikube ssh

您将看到以下输出:

图 2.21:通过 SSH 访问 Minikube VM

图 2.21:通过 SSH 访问 Minikube VM

注意

所有kubectl命令都应在主机终端会话中运行,而所有docker命令都应在 Minikube 终端会话中运行。

  1. 在 Minikube 会话中,清理所有已停止的 Docker 容器:
docker rm $(docker ps -a -q)

您应该看到以下输出:

图 2.22:清理所有已停止的 Docker 容器

图 2.22:清理所有已停止的 Docker 容器

您可能会看到一些错误消息,比如“您无法删除正在运行的容器...”。这是因为前面的docker rm命令针对所有容器(docker ps -a -q)运行,但不会停止任何正在运行的容器。

  1. 在 Minikube 会话中,通过运行以下命令停止 kubelet:
sudo systemctl stop kubelet

此命令在成功执行后不会显示任何响应。

注意

在本练习中,我们将手动停止和启动其他由 kubelet 在 Minikube 环境中管理的 Kubernetes 组件,例如 API 服务器。因此,在本练习中,需要先停止 kubelet;否则,kubelet 将自动重新启动其管理的组件。

请注意,在典型的生产环境中,与 Minikube 不同,不需要在主节点上运行 kubelet 来管理主平面组件;kubelet 只是工作节点上的一个强制组件。

  1. 30 秒后,在主机终端会话中运行以下命令来检查集群的状态:
kubectl get node

您应该看到以下响应:

NAME         STATUS       ROLES      AGE       VERSION
minikube     NotReady     master     32h       v1.16.2

预计minikube节点的状态将更改为NotReady,因为 kubelet 已停止。

  1. 在您的 Minikube 会话中,停止kube-schedulerkube-controller-managerkube-apiserver。正如我们之前所看到的,所有这些都作为 Docker 容器运行。因此,您可以依次使用以下命令:
docker stop $(docker ps | grep kube-scheduler | grep -v pause | awk '{print $1}')
docker stop $(docker ps | grep kube-controller-manager | grep -v pause | awk '{print $1}')
docker stop $(docker ps | grep kube-apiserver | grep -v pause | awk '{print $1}')

您应该看到以下响应:

图 2.23:停止运行 Kubernetes 组件的容器

图 2.23:停止运行 Kubernetes 组件的容器

正如我们在Kubernetes 组件概述部分所解释的,grep -v pause | awk '{print $1}'命令可以获取所需 Docker 容器的确切容器 ID($1 = 第一列)。然后,docker pause命令可以暂停正在运行的 Docker 容器。

现在,三个主要的 Kubernetes 组件已经停止。

  1. 现在,您需要在主机机器上创建一个部署规范。创建一个名为k8s-for-beginners-deploy2.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-for-beginners
spec:
  replicas: 1
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: k8s-for-beginners
        image: packtworkshops/the-kubernetes-workshop:k8s-for-          beginners
  1. 尝试在主机会话中运行以下命令来创建部署:
kubectl apply -f k8s-for-beginners-deploy2.yaml

您应该看到类似于以下内容的响应:

图 2.24:尝试创建新的部署

图 2.24:尝试创建新的部署

毫不奇怪,我们收到了网络超时错误,因为我们有意停止了 Kubernetes API 服务器。如果 API 服务器宕机,您将无法运行任何kubectl命令或使用任何依赖 API 请求的等效工具(例如 Kubernetes 仪表板):

The connection to the server 192.168.99.100:8443 was refused – did you specify the right host or port?
  1. 让我们看看如果重新启动 API 服务器并尝试再次创建部署会发生什么。通过在 Minikube 会话中运行以下命令来重新启动 API 服务器容器:
docker start $(docker ps -a | grep kube-apiserver | grep -v pause | awk '{print $1}')

该命令尝试查找携带 API 服务器的停止容器的容器 ID,然后启动它。您应该得到类似于这样的响应:

9e1cf098b67c
  1. 等待 10 秒。然后,检查 API 服务器是否在线。您可以在主机会话中运行任何简单的 kubectl 命令来进行此操作。让我们尝试通过运行以下命令来获取节点列表:
kubectl get node

您应该看到以下响应:

NAME         STATUS       ROLES      AGE       VERSION
minikube     NotReady     master     32h       v1.16.2

正如您所看到的,我们能够得到一个没有错误的响应。

  1. 让我们再次尝试创建部署:
kubectl apply -f k8s-for-beginners-deploy2.yaml

您应该看到以下响应:

deployment.apps/k8s-for-beginners created
  1. 通过运行以下命令来检查部署是否已成功创建:
kubectl get deploy

您应该看到以下响应:

NAME               READY     UP-TO-DATE    AVAILABLE   AGE
k8s-for-beginners  0/1       0             0           113s

从前面的截图中,似乎有些问题,因为在READY列中,我们可以看到0/1,这表明与此部署关联的 pod 数量为 0,而期望的数量是 1(我们在部署规范中指定的replicas字段)。

  1. 让我们检查所有在线的 pod:
kubectl get pod

您应该看到以下响应:

No resources found in default namespace.

我们可以看到我们的 pod 尚未创建。这是因为 Kubernetes API 服务器只创建 API 对象;任何 API 对象的实现都是由其他组件执行的。例如,在部署的情况下,是kube-controller-manager创建相应的 pod。

  1. 现在,让我们重新启动kube-controller-manager。在 Minikube 会话中运行以下命令:
docker start $(docker ps -a | grep kube-controller-manager | grep -v pause | awk '{print $1}')

您应该看到类似以下的响应:

35facb013c8f
  1. 等待几秒钟后,在主机会话中运行以下命令来检查部署的状态:
kubectl get deploy

您应该看到以下响应:

NAME               READY     UP-TO-DATE    AVAILABLE   AGE
k8s-for-beginners  0/1       1             0           5m24s

正如我们所看到的,我们正在寻找的 pod 仍然没有上线。

  1. 现在,检查 pod 的状态:
kubectl get pod

您应该看到以下响应:

图 2.25:获取 pod 列表

图 2.25:获取 pod 列表

输出与步骤 15中的输出不同,因为在这种情况下,一个 pod 是由kube-controller-manager创建的。但是,在STATUS列下我们可以看到Pending。这是因为将 pod 分配给适当的节点不是kube-controller-manager的责任;这是kube-scheduler的责任。

  1. 在启动kube-scheduler之前,让我们看一下有关 pod 的一些额外信息:
kubectl get pod -o wide

您应该看到以下响应:

图 2.26:获取有关 pod 的更多信息

图 2.26:获取有关 pod 的更多信息

突出显示的NODE列表明尚未为此 pod 分配节点。这证明了调度程序没有正常工作,我们知道这是因为我们将其下线。如果调度程序在线,此响应将表明没有地方可以放置此 pod。

注意

您将在第十七章Kubernetes 中的高级调度中学到更多关于 pod 调度的知识。

  1. 让我们通过在 Minikube 会话中运行以下命令来重新启动kube-scheduler
docker start $(docker ps -a | grep kube-scheduler | grep -v pause | awk '{print $1}')

您应该看到类似以下的响应:

11d8a27e3ee0
  1. 我们可以通过在主机会话中运行以下命令来验证kube-scheduler是否工作:
kubectl describe pod k8s-for-beginners-66644bb776-kvwfr

请从步骤 17中获得响应中的 pod 名称,如图 2.26中所示。您应该看到以下输出:

Name:         k8s-for-beginners-66644bb776-kvwfr
Namespace:    default
Priority:     0
Node:         <none>

我们正在截断输出截图以便更好地展示。请看以下摘录,重点是Events部分:

图 2.27:检查 pod 报告的事件

图 2.27:检查 pod 报告的事件

Events部分,我们可以看到kube-scheduler尝试调度,但它报告没有可用的节点。为什么会这样?

这是因为我们之前停止了 kubelet,并且 Minikube 环境是一个单节点集群,因此没有可用的带有运行 kubelet 的节点,可以放置 pod。

  1. 让我们通过在 Minikube 会话中运行以下命令来重新启动 kubelet:
sudo systemctl start kubelet

成功执行后,终端不应该给出任何响应。

  1. 在主机终端中,通过在主机会话中运行以下命令来验证部署的状态:
kubectl get deploy

您应该看到以下响应:

NAME               READY     UP-TO-DATE    AVAILABLE   AGE
k8s-for-beginners  1/1       1             1           11m

现在,一切看起来都很健康,因为部署在READY列下显示1/1,这意味着 pod 在线。

  1. 同样地,验证 pod 的状态:
kubectl get pod -o wide

您应该得到类似以下的输出:

图 2.28:获取有关 pod 的更多信息

图 2.28:获取有关 pod 的更多信息

我们可以看到STATUS下是Running,并且它已被分配给minikube节点。

在这个练习中,我们通过逐个破坏 Kubernetes 组件然后逐个恢复它们来追踪 pod 生命周期的每个阶段。现在,基于我们对这个练习所做的观察,我们对在这个练习之前提出的问题有了更清晰的认识:

  • 步骤 12 – 16:我们看到在部署的情况下,控制器管理器负责请求创建 pod。

  • 步骤 17 – 19:调度程序负责选择要放置在 pod 中的节点。它通过将 pod 的nodeName规范设置为所需的节点来分配节点。此时,将 pod 关联到节点仅仅发生在 API 对象的级别。

  • 步骤 20 – 22:kubelet 实际上会启动容器来运行我们的 pod。

在整个 Pod 的生命周期中,Kubernetes 组件通过适当地更新 Pod 的规范来合作。API 服务器作为接受 Pod 更新请求的关键组件,同时向感兴趣的方报告 Pod 的变化。

在接下来的活动中,我们将汇集本章学到的技能,以找出如何从基于容器的环境迁移到 Kubernetes 环境,以便运行我们的应用程序。

活动 2.01:在 Kubernetes 中运行 Pageview 应用程序

Activity 1.01中,创建一个简单的页面计数应用程序,在上一章中,我们构建了一个名为 Pageview 的 Web 应用程序,并将其连接到了一个 Redis 后端数据存储。所以,这里有一个问题:在不对源代码进行任何更改的情况下,我们能否将基于 Docker 的应用程序迁移到 Kubernetes,并立即享受 Kubernetes 的好处?根据给定的指导方针,在这个活动中尝试一下。

这个活动分为两个部分:在第一部分中,我们将创建一个简单的 Pod,其中包含我们的应用程序,通过一个 Service 暴露给集群外的流量,并连接到另一个作为另一个 Pod 运行的 Redis 数据存储。在第二部分中,我们将将应用程序扩展到三个副本。

使用 Service 将 Pageview 应用程序连接到 Redis 数据存储

类似于 Docker 中的--link选项,Kubernetes 提供了一个 Service,作为一个抽象层来暴露一个应用程序(比如,一系列带有相同标签集的 Pod)可以在内部或外部访问。例如,正如我们在本章中讨论的那样,前端应用程序可以通过NodePort Service 暴露,以便外部用户访问。除此之外,在这个活动中,我们需要定义一个内部 Service,以便将后端应用程序暴露给前端应用程序。按照以下步骤进行:

  1. Activity 1.01中,创建一个简单的页面计数应用程序,我们构建了两个 Docker 镜像——一个用于前端 Pageview Web 应用程序,另一个用于后端 Redis 数据存储。您可以使用本章学到的技能将它们迁移到 Kubernetes YAML 中。

  2. 为该应用程序创建两个 Pod(每个由一个 Deployment 管理)是不够的。我们还必须创建 Service YAML 来将它们连接在一起。

确保清单中的targetPort字段与 Redis 镜像中定义的暴露端口一致,在这种情况下是6379。就port字段而言,理论上它可以是任何端口,只要它与 Pageview 应用程序中指定的端口一致即可。

这里值得一提的另一件事是 Redis 数据存储的 pod 的name字段。这是 Pageview 应用程序源代码中用来引用 Redis 数据存储的符号。

现在,您应该有三个 YAML 文件 - 两个 pod 和一个 Service。使用kubectl -f <yaml 文件名>应用它们,然后使用kubectl get deploy,service来确保它们被成功创建。

  1. 在这个阶段,Pageview 应用程序应该能够正常运行,因为它通过 Service 与 Redis 应用程序连接在一起。然而,Service 只能作为内部连接器工作,以确保它们可以在集群内部相互通信。

要从外部访问 Pageview 应用程序,我们需要定义一个NodePort Service。与内部 Service 不同,我们需要明确指定typeNodePort

  1. 使用kubectl -f <yaml 文件名>应用外部 Service YAML。

  2. 运行minikube service <外部 service 名称>来获取 Service URL。

  3. 多次访问 URL,确保 Pageview 数量每次增加一个。

有了这个,我们成功地在 Kubernetes 中运行了 Pageview 应用程序。但是如果 Pageview 应用程序宕机怎么办?尽管 Kubernetes 可以自动创建替代的 pod,但在故障被检测到和新的 pod 准备就绪之间仍然存在停机时间。

一个常见的解决方案是增加应用程序的副本数量,以便只要至少有一个副本在运行,整个应用程序就是可用的。

在多个副本中运行 Pageview 应用程序

Pageview 应用程序当然可以使用单个副本运行。然而,在生产环境中,高可用性是必不可少的,并且通过在节点之间维护多个副本来避免单点故障来实现。(这将在接下来的章节中详细介绍。)

在 Kubernetes 中,为了确保应用程序的高可用性,我们可以简单地增加副本数量。按照以下步骤来做:

  1. 修改 Pageview YAML 将replicas更改为3

  2. 通过运行kubectl apply -f <pageview 应用 yaml>来应用这些更改。

  3. 通过运行kubectl get pod,您应该能够看到三个 Pageview pod 正在运行。

  4. 使用minikube service命令输出中显示的 URL 多次访问。

检查每个 pod 的日志,看看请求是否均匀地分布在三个 pod 之间。

  1. 现在,让我们验证 Pageview 应用程序的高可用性。在保持一个健康的 pod 的同时连续终止任意的 pod。您可以通过手动或编写脚本来实现这一点。或者,您可以打开另一个终端,检查 Pageview 应用程序是否始终可访问。

如果您选择编写脚本来终止 pod,您将看到类似以下的结果:

图 2.29:通过脚本杀死 pod

图 2.29:通过脚本杀死 pod

假设您采用类似的方法并编写脚本来检查应用程序是否在线,您应该会看到类似以下的输出:

图 2.30:通过脚本重复访问应用程序

图 2.30:通过脚本重复访问应用程序

此活动的解决方案可在以下地址找到:packt.live/304PEoD

Kubernetes 多节点集群优势一览

只有在多节点集群的环境中才能真正体会到 Kubernetes 的优势。本章以单节点集群(Minikube 环境)来演示 Kubernetes 提供的功能,就像本书的许多其他章节一样。然而,在真实的生产环境中,Kubernetes 是部署在多个工作节点和主节点上的。只有这样,您才能确保单个节点的故障不会影响应用程序的一般可用性。可靠性只是多节点 Kubernetes 集群可以为我们带来的众多好处之一。

但等等 - 难道我们不是可以在不使用 Kubernetes的情况下实现应用程序并以高可用的方式部署它们吗?这是真的,但通常会伴随着大量的管理麻烦,无论是在管理应用程序还是基础设施方面。例如,在初始部署期间,您可能需要手动干预,以确保所有冗余容器不在同一台机器上运行。在节点故障的情况下,您不仅需要确保新的副本被正确地重新生成,还需要确保新的副本不会落在已经运行现有副本的节点上。这可以通过使用 DevOps 工具或在应用程序端注入逻辑来实现。然而,无论哪种方式都非常复杂。Kubernetes 提供了一个统一的平台,我们可以使用它来通过描述我们想要的高可用特性(Kubernetes 原语(API 对象))将应用程序连接到适当的节点。这种模式使应用程序开发人员的思维得到解放,因为他们只需要考虑如何构建他们的应用程序。Kubernetes 在幕后处理了高可用性所需的功能,如故障检测和恢复。

总结

在本章中,我们使用 Minikube 来提供单节点 Kubernetes 集群,并对 Kubernetes 的核心组件以及其关键设计原理进行了高层概述。之后,我们将现有的 Docker 容器迁移到 Kubernetes,并探索了一些基本的 Kubernetes API 对象,如 pod、服务和部署。最后,我们有意破坏了一个 Kubernetes 集群,并逐个恢复了它的组件,这使我们能够了解不同的 Kubernetes 组件是如何协同工作的,以便在节点上启动和运行一个 pod。

在整个本章中,我们使用 kubectl 来管理我们的集群。我们对这个工具进行了快速介绍,但在接下来的章节中,我们将更仔细地了解这个强大的工具,并探索我们可以使用它的各种方式。

第三章: kubectl - Kubernetes 命令中心

概述

在本章中,我们将揭开一些常见的 kubectl 命令,并看看如何使用 kubectl 来控制我们的 Kubernetes 集群。我们将从简要了解使用 kubectl 命令与 Kubernetes 集群通信的端到端过程开始。然后,我们将为 Bash 终端设置一些快捷方式和自动补全。我们将从学习如何创建、删除和管理 Kubernetes 对象的基础知识开始使用 kubectl。我们将通过练习了解在 Kubernetes 中管理资源的两种方法 - 声明式和命令式。到本章结束时,您还将学会如何使用 kubectl 实时更新运行在您的 Kubernetes 集群上的应用程序。

介绍

在《第一章 Kubernetes 和容器简介》中,我们看到 Kubernetes 是一个便携且高度可扩展的开源容器编排工具。它提供了非常强大的功能,可用于规模化管理容器化工作负载。在上一章中,您了解了 Kubernetes 的不同组件如何共同工作以实现期望的目标。我们还在《第二章 Kubernetes 概述》中演示了 kubectl 的一些基本用法。在本章中,我们将更仔细地研究这个实用程序,并看看如何利用其潜力。

重申一下,kubectl 是一个用于与 Kubernetes 集群交互和执行各种操作的命令行实用程序。在管理集群时,有两种使用 kubectl 的方式 - 命令式管理,重点是使用命令而不是 YAML 清单来实现所需的状态,以及声明式管理,重点是创建和更新 YAML 清单文件。kubectl 可以支持这两种管理技术来管理 Kubernetes API 对象(也称为 Kubernetes API 原语)。在上一章中,我们看到各种组件不断尝试将集群的状态从实际状态更改为所需状态。这可以通过使用 kubectl 命令或 YAML 清单来实现。

kubectl 允许您向 Kubernetes 集群发送命令。kubectl命令可用于部署应用程序、检查和管理 Kubernetes 对象,或者进行故障排除和查看日志。有趣的是,尽管 kubectl 是控制和与 Kubernetes 集群通信的标准工具,但它并不随 Kubernetes 一起提供。因此,即使您在集群的任何节点上运行 kubectl,您仍需要单独安装 kubectl 二进制文件,这是我们在上一章的练习 2.01中所做的,使用 Minikube 和 Kubernetes 集群入门

本章将带您深入了解 kubectl 的幕后功能,并提供更多关于如何使用 kubectl 命令与一些常用的 Kubernetes 对象进行交互的见解。我们将学习如何为 kubectl 设置一些快捷方式。我们将带您不仅使用 kubectl 创建新对象,还对 Kubernetes 中的实时部署进行更改。但在此之前,让我们偷偷看看幕后,了解 kubectl 如何与 Kubernetes 通信的确切方式。

kubectl 如何与 Kubernetes 通信

正如我们在上一章中看到的,API 服务器管理终端用户与 Kubernetes 之间的通信,并且还充当集群的 API 网关。为了实现这一点,它实现了基于 HTTP 和 HTTPS 协议的 RESTful API,以执行 CRUD 操作,以填充和修改 Kubernetes API 对象,例如 pod、service 等,根据用户通过 kubectl 发送的指令。这些指令可以采用各种形式。例如,要检索集群中运行的 pod 的信息,我们将使用kubectl get pods命令,而要创建一个新的 pod,我们将使用kubectl run命令。

首先,让我们看看运行kubectl命令时幕后发生了什么。看一下下面的插图,它提供了该过程的概述,然后我们将更仔细地查看该过程的不同细节:

图 3.1:kubectl 实用程序的代表性流程图

图 3.1:kubectl 实用程序的代表性流程图

kubectl 命令被转换为 API 调用,然后发送到 API 服务器。API 服务器然后对请求进行身份验证和验证。一旦身份验证和验证阶段成功,API 服务器将从etcd中检索和更新数据,并以请求的信息做出响应。

设置自动补全和快捷方式的环境

在大多数 Linux 环境中,您可以在开始使用本章提到的指令之前为 kubectl 命令设置自动补全。了解自动补全和快捷方式在 Linux 环境中的工作原理对于那些有兴趣获得由 Linux 基金会颁发的认证 Kubernetes 管理员CKA)和认证 Kubernetes 应用开发者CKAD)等证书的人来说将会非常有帮助。我们将在下面的练习中学习如何设置自动补全。

练习 3.01:设置自动补全

在本练习中,我们将向您展示如何在 Bash 中为 kubectl 命令设置自动补全和别名。这是一个有用的功能,将帮助您节省时间并避免打字错误。执行以下步骤完成本练习:

  1. 我们将需要bash-completion包,如果尚未安装,请安装它。您可以前往 GitHub 存储库获取各种平台的安装说明,网址为github.com/scop/bash-completion。如果您正在运行 Ubuntu 20.04,您可以使用以下命令通过 APT 软件包管理器安装它:
sudo apt-get install bash-completion
  1. 您可以使用以下命令在 Bash 中设置自动补全:
source <(kubectl completion bash)

注意

这个命令以及本练习中的后续命令在成功执行后不会在终端上显示任何响应。

  1. 如果您想在 Bash shell 中使自动补全持久化,可以使用以下命令,它将把kubectl自动补全写入到您当前用户目录下的.bashrc文件中:
echo "source <(kubectl completion bash)" >> ~/.bashrc 
  1. 您也可以使用alias关键字为您的kubectl命令设置别名,方法如下:
alias k=kubectl
  1. 同样,如果您想为某些特定命令设置别名,可以使用类似以下的命令:
alias kcdp='kubectl describe po'
alias kcds='kubectl describe svc'
alias kcdd='kubectl describe deploy'
  1. 最后,您可以使用以下命令在按下Tab时设置kubectl命令的自动补全:
complete -F __start_kubectl k

注意

您也可以使用以下命令在zsh(Bash shell 的替代品)中设置自动补全:

source <(kubectl completion zsh)

echo "if [ $commands[kubectl] ]; then source <(kubectl completion zsh); fi" >> ~/.zshrc

在完成这个练习之后,你将为你的 Bash shell 设置好自动补全。你也可以在命令中使用别名,比如使用k代替kubectl。然而,为了避免混淆并保持标准化的结构,我们在本书中将使用完整的命令。

设置 kubeconfig 配置文件

在大多数企业环境中,通常会有不止一个 Kubernetes 集群,这取决于组织的策略。管理员、开发人员或者其他与 Kubernetes 集群打交道的角色需要与多个集群进行交互,并在不同的集群上执行不同的操作。

配置文件会让事情变得更加简单。你可以使用这个文件来存储关于不同集群、用户、命名空间和认证机制的信息。这样的配置文件被称为kubeconfig文件。请注意,kubeconfig 是指 kubectl 配置文件的通用方式,而不是config文件的名称。kubectl 使用这样的文件来存储我们选择集群并与其 API 服务器通信所需的信息。

默认情况下,kubectl 会在$HOME/.kube目录中查找该文件。在大多数情况下,你可以指定一个KUBECONFIG环境变量或使用--kubeconfig标志来指定 kubeconfig 文件。这些文件通常保存在$HOME/.kube/config中。

注意

你可以通过设置KUBECONFIG环境变量和--kubeconfig标志来了解如何配置访问多个集群的更多信息,网址为kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/#set-the-kubeconfig-environment-variable

安全上下文用于定义 pod 的特权和访问控制设置。我们将在第十三章Kubernetes 中的运行时和网络安全中重新讨论访问控制和安全性。

让我们来看一下 kubeconfig 文件,以了解它是如何工作的。你可以使用以下命令查看 kubeconfig 文件:

kubectl config view

或者,你也可以使用以下命令:

cat $HOME/.kube/config

你应该会得到类似以下的输出:

图 3.2:kubectl config view 命令的输出

图 3.2:kubectl config view 命令的输出

context是访问集群所需的一组信息。 它包含集群的名称、用户和命名空间。 图 3.2中的current-context字段显示您正在使用的当前上下文。 如果要切换当前上下文,可以使用以下命令:

kubectl config use-context <the cluster you want to switch to>

例如,如果我们想要切换到名为minikube的上下文,我们将使用以下命令:

kubectl config use-context minikube

这将产生类似以下的输出:

Switched to context "minikube".

常用的 kubectl 命令

如前所述,kubectl 是一个用于与 Kubernetes API 服务器通信的 CLI 工具。 kubectl 具有许多有用的命令,用于处理 Kubernetes。 在本节中,我们将为您介绍一些常用的 kubectl 命令和快捷方式,用于管理 Kubernetes 对象。

常用的 kubectl 命令来创建、管理和删除 Kubernetes 对象

有几个简单的 kubectl 命令,您几乎每次都会使用。 在本节中,我们将看一些基本的 kubectl 命令:

  • get <object>:您可以使用此命令获取所需类型对象的列表。 使用all而不是指定对象类型将获取所有类型对象的列表。 默认情况下,这将获取默认命名空间中指定对象类型的列表。 您可以使用-n标志从特定命名空间获取对象;例如,kubectl get pod -n mynamespace

  • describe <object-type> <object-name>:您可以使用此命令检查特定对象的所有相关信息;例如,kubectl describe pod mypod

  • logs <object-name>:您可以使用此命令检查特定对象的所有相关日志,以找出创建该对象时发生了什么;例如,kubectl logs mypod

  • edit <object-type> <object-name>:您可以使用此命令编辑特定对象;例如,kubectl edit pod mypod

  • delete <object-type> <object-name>:您可以使用此命令删除特定对象;例如,kubectl delete pod mypod

  • create <filename.yaml>:您可以使用此命令创建在 YAML 清单文件中定义的一堆 Kubernetes 对象;例如,kubectl create -f your_spec.yaml

  • apply <filename.yaml>:您可以使用此命令创建或更新在 YAML 清单文件中定义的一堆 Kubernetes 对象;例如,kubectl apply -f your_spec.yaml

一些简单 kubectl 命令的演示

在本节中,我们将为您介绍一些常用的 kubectl 命令。本节主要用于演示目的,因此您可能看不到与这些图像中相同的确切输出。但是,本节将帮助您了解这些命令的使用方式。您将在后面的练习中广泛使用它们,以及在整本书中。让我们来看一下:

  • 如果您想显示节点,请使用以下命令:
kubectl get nodes

您将看到类似以下的输出:

图 3.3:kubectl get nodes 命令的输出

图 3.3:kubectl get nodes 命令的输出

由于我们在练习 3.01设置自动补全中设置了别名,您也可以使用以下命令获得相同的结果:

k get no
  • 如果您想显示所有当前命名空间,可以使用以下命令:
kubectl get namespaces

您应该看到类似以下的输出:

NAME                  STATUS         AGE
default               Active         7m5s
kube-node-lease       Active         7m14s
kube-public           Active         7m14s
kube-system           Active         7m15s

您也可以使用以下缩短命令获得相同的结果:

k get ns
  • 如果您想检查kubectl的版本,可以使用以下命令:
kubectl version

您将看到类似以下的输出:

Client version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2, GitCommit: 59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-21T22:17:28Z, GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64}
Server version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2, GitCommit: 59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-18T23:22:30Z, GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64}
  • 如果您想查看有关当前 Kubernetes 集群的一些信息,可以使用以下命令:
kubectl cluster-info

您应该看到类似以下的输出:

图 3.4:kubectl cluster-info 命令的输出

图 3.4:kubectl cluster-info 命令的输出

在我们继续进行演示之前,我们将提到一些命令,您可以使用这些命令创建一个示例应用程序,我们已经在本章的 GitHub 存储库中提供了。使用以下命令获取运行应用程序所需的所有对象的 YAML 规范:

curl https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter03/Activity03.01/sample-application.yaml --output sample-application.yaml

现在,您可以使用以下命令部署sample-application.yaml文件:

kubectl apply -f sample-application.yaml 

如果您能看到以下输出,这意味着示例应用程序已成功创建在您的 Kubernetes 集群中:

deployment.apps/redis-back created
service/redis-back created
deployment.apps/melonvote-front created
service/melonvote-front created

现在您已部署了提供的应用程序,如果您尝试本节后面显示的任何命令,您将看到与该应用程序相关的各种对象、事件等。请注意,您的输出可能与此处显示的图像不完全匹配:

  • 您可以使用以下命令获取default命名空间下的集群中的所有内容:
kubectl get all

这将给出类似以下的输出:

图 3.5:kubectl get all 命令的输出

图 3.5:kubectl get all 命令的输出

  • 事件描述了 Kubernetes 集群中到目前为止发生的事情,您可以使用事件更好地了解您的集群,并帮助解决任何故障排除工作。要列出默认命名空间中的所有事件,请使用以下命令:
kubectl get events

这将产生类似以下的输出:

图 3.6:kubectl get events 命令的输出

图 3.6:kubectl get events 命令的输出

  • 服务是用来向最终用户公开应用程序的抽象。您将在第八章 Service Discovery中了解更多关于服务的内容。您可以使用以下命令列出所有服务:
kubectl get services

这将产生类似以下的输出:

图 3.7:kubectl get services 命令的输出

图 3.7:kubectl get services 命令的输出

您也可以使用以下缩写命令获得相同的结果:

k get svc
  • 部署是一个允许我们轻松管理和更新 pod 的 API 对象。您将在第七章 Kubernetes Controllers中了解更多关于部署的内容。您可以使用以下命令获取部署列表:
kubectl get deployments 

这应该会产生类似以下的响应:

NAME               READY    UP-TO-DATE    AVAILABLE     AGE
aci-helloworld     1/1      1             1             34d
melonvote-front    1/1      1             1             7d6h
redis-back         1/1      1             1             7d6h

您也可以使用以下缩写版本的命令获得相同的结果:

k get deploy

get命令的一些有用标志

正如您所见,get命令是一个非常标准的命令,用于在我们需要获取集群中对象列表时使用。它还有一些有用的标志。让我们在这里看一些:

  • 如果您想要列出所有命名空间中特定类型的资源,您可以在命令中添加--all-namespaces标志。例如,如果我们想要列出所有命名空间中的所有部署,我们可以使用以下命令:
kubectl get deployments --all-namespaces

这将产生类似这样的输出:

图 3.8:在所有命名空间下 kubectl get deployments 的输出

图 3.8:在所有命名空间下 kubectl get deployments 的输出

您还可以看到左侧有一个额外的列,指定了相应部署的命名空间。

  • 如果您想要列出特定命名空间中特定类型的资源,您可以使用-n标志。在这里,-n标志代表命名空间。例如,如果您想要列出名为keda的命名空间中的所有部署,将使用以下命令:
kubectl get deployments -n keda

这个命令会显示类似以下的输出:

图 3.9:在 keda 命名空间下 kubectl get deployments 的输出

图 3.9:在 keda 命名空间下使用 kubectl get deployments 的输出

  • 您可以添加--show-labels标志来显示列表中对象的标签。例如,如果您想要获取default命名空间中所有 Pod 的列表,以及它们的标签,您可以使用以下命令:
kubectl get pods --show-labels

此命令应该会产生类似以下的输出:

图 3.10:使用所有标签获取 kubectl get pods 的输出

图 3.10:使用所有标签获取 kubectl get pods 的输出

右侧有一个额外的列,指定了 Pod 的标签。

  • 您可以使用-o wide标志来显示有关对象的更多信息。这里,-o标志代表输出。让我们看一个如何使用这个标志的简单例子:
kubectl get pods -o wide

这将产生类似以下的输出:

图 3.11:使用附加信息获取 kubectl get pods 的输出

图 3.11:使用附加信息获取 kubectl get pods 的输出

您还可以看到右侧有额外的列,指定了 Pod 所在的节点,以及节点的内部 IP 地址。您可以在kubernetes.io/docs/reference/kubectl/overview/#output-options找到更多使用-o标志的方法。

注意

我们将限制本节中常用的命令,以限制本章的范围。您可以在kubernetes.io/docs/reference/generated/kubectl/kubectl-commands找到更多 kubectl 命令。

在 Kubernetes 中填充部署

正如我们之前提到的,部署是管理和更新 Pod 的便捷方式。在 Kubernetes 中定义部署是为集群中运行的应用程序提供声明性更新的有效和高效方式。

您可以使用 kubectl 命令来创建部署,也可以使用声明性的 YAML 清单文件。在接下来的练习中,我们将在 Kubernetes 中部署一个应用程序(本练习将使用 Nginx),并学习如何使用 kubectl 命令与部署进行交互,以及如何修改 YAML 清单文件。

练习 3.02:创建部署

在 Kubernetes 中创建部署有两种方式 - 使用kubectl create/run命令和创建一个 YAML 格式的清单文件,然后使用kubectl apply命令。我们可以用这两种选项实现相同的目标。让我们尝试一下,然后进行比较:

  1. 直接使用以下命令创建一个部署:
kubectl create deployment kubeserve --image=nginx:1.7.8

您可以期待类似以下的输出:

deployment.apps/kubeserve created

注意

您也可以使用kubectl run命令创建一个部署。为了在这里实现相同的结果,您可以使用以下命令:

kubectl run nginx --image=nginx:1.7.8

kubectl run nginx --image=nginx:1.7.8 --replicas=3

  1. 您还可以通过定义部署的 YAML 清单文件来创建一个部署。使用您喜欢的文本编辑器创建一个名为sample-deployment.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubeserve 
  labels:
    app: kubeserve 
spec:
  replicas : 3
  selector:
    matchLabels:
      app: kubeserve 
  template:
    metadata:
      labels:
        app: kubeserve 
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

在这个 YAML 定义中,replicas字段定义了部署中副本 pod 的数量。

  1. 使用以下命令应用您在 YAML 清单文件中定义的配置:
kubectl apply -f sample-deployment.yaml

示例输出将如下所示:

kubectl apply -f sample-deployment.yaml
  1. 使用以下命令检查当前存在于default命名空间中的部署:
kubectl get deployments 

输出将如下所示:

NAME              READY    UP-TO-DATE    AVAILABLE     AGE
aci-helloworld    1/1      1             1             27d
kubeserve         3/3      3             3             26m

在这个练习中,我们已经看到了使用不同方法创建部署的差异。kubectl create命令被广泛用于测试。对于大多数实施现代 DevOps 方法的企业解决方案,使用 YAML 定义来方便地定义配置,并使用 Git 等源代码控制工具进行跟踪,更有意义。当您的组织将 YAML 定义与 DevOps 工具集成时,可以使解决方案更易管理和可追踪。

现在我们已经看到如何创建一个部署,在下一个练习中,我们将学习如何修改或更新已经运行的部署。这是您经常需要做的事情,因为软件被更新到新版本,bug 被识别和修复,您的应用程序的需求发生变化,或者您的组织转移到完全新的解决方案。我们还将学习如何将部署回滚到较早的版本,如果更新没有达到预期的结果,这是您想要做的事情。

练习 3.03:更新部署

在这个练习中,我们将更新在上一个练习中部署的应用程序到一个更近期的版本,并演示如果有必要,我们如何回滚部署到先前的版本。

与我们在创建部署时看到的两种方法类似,更新应用程序也有两种方式——使用kubectl set image命令和更新 YAML 清单文件,然后使用kubectl apply命令。以下步骤将指导您完成这两种方法:

  1. 首先,让我们使用以下命令获取当前部署的详细信息:
kubectl describe deploy kubeserve

您将看到类似以下的输出:

图 3.12:描述 kubeserve 部署

图 3.12:描述 kubeserve 部署

  1. 可以使用以下命令更新图像:
kubectl set image deployment/kubeserve nginx=nginx:1.9.1 –-record

image子命令表示我们要更新对象的image字段,如我们在上一个练习的步骤 2中所见的 YAML 清单中定义的那样。

然后,我们以<object-type>/<object name>格式指定对象。

接下来的部分,nginx=nginx:1.9.1,告诉 Kubernetes 在 NGINX 的 Docker Hub 存储库中查找特定标记为1.9.1的图像。您可以在hub.docker.com/_/nginx?tab=tags上查看可用的标记。

--record标志在您想要保存kubectl命令对当前资源所做的更新时非常有用。

通过应用此命令,您将看到类似以下的输出:

deployment.extensions/kubeserve image updated
  1. 现在,让我们使用以下命令获取部署的详细信息:
kubectl describe deploy kubeserve

您应该看到以下输出:

图 3.13:使用 kubectl describe 命令检查容器中的图像版本

图 3.13:使用 kubectl describe 命令检查容器中的图像版本

在前面的截图中,您可以看到图像已成功更新为版本1.9.1

实现相同结果的另一种方法是修改 YAML 文件,然后使用kubectl apply命令。我们将使用在上一个练习中创建的相同 YAML 文件。如果您没有对象的 YAML 文件,可以使用以下命令导出 YAML 清单:

kubectl get deploy kubeserve -o yaml > kubeserve-spec.yaml

该命令将输出一个名为kubeserve-spec.yaml的文件,其中包含在集群中生效的清单。然后,您可以使用 vim、nano 或任何其他文本编辑器对其进行编辑,然后使用kubectl apply命令应用编辑后的kubeserve-spec.yaml清单,如前一个练习中所示,还需添加--record标志。

  1. 如果要执行回滚操作,可以使用以下命令:
kubectl rollout undo deployments kubeserve

您将看到类似以下的输出:

deployment.extensions/kubeserve rolled back
  1. 您可以使用kubectl rollout history命令来检查特定部署的所有修订版本,如下所示:
kubectl rollout history deployment kubeserve

您将看到类似以下的输出:

图 3.14:kubectl rollout history 命令的输出

图 3.14:kubectl rollout history 命令的输出

  1. 您还可以使用以下命令来检查特定修订的详细信息:
kubectl rollout history deployment kubeserve --revision=3

该命令的输出将如下所示:

图 3.15:检查修订版本 3 的详细信息

图 3.15:检查修订版本 3 的详细信息

  1. 您可以通过指定--to-revision标志将部署回滚到特定修订版本:
kubectl rollout undo deployments kubeserve --to-revision=3

您将看到类似以下的输出:

deployment.extensions/kubeserve rolled back

在这个练习中,我们学习了如何更新已经存在的部署,以及如何将部署回滚到先前的规范。

部署允许我们以声明性的方式定义副本 Pod 的期望状态。我们将重新访问部署的工作原理,并在第七章Kubernetes Controllers中发现更多关于它的信息。如果您有意删除单个 Pod 副本,或者如果 Pod 由于任何原因失败,由于我们定义了具有一定数量副本的部署,部署将会不断重新创建 Pod,直到您删除它为止。这就是我们所说的自动修复。因此,您需要删除部署本身,这也将删除由其管理的所有 Pod。我们将在接下来的练习中学习如何做到这一点。

练习 3.04:删除部署

在这个练习中,我们将删除在上一个练习中创建的部署:

  1. 使用以下命令获取现有部署的列表:
kubectl get deployment

您可以期望看到类似以下的输出:

NAME              READY    UP-TO-DATE    AVAILABLE     AGE
aci-helloworld    1/1      1             1             27d
kubeserve         3/3      3             3             26m
melonkedaaf       0/0      0             0             26d
  1. 假设,为了这个练习的目的,我们想要删除在上一个练习中创建的kubeserve部署。使用以下命令来删除部署:
kubectl delete deployment kubeserve

示例输出将类似于以下内容:

deployment.extensions "kubeserve" deleted
  1. 获取部署列表以检查并确保目标部署已成功删除:
kubectl get deployment

您应该看到类似以下的输出:

NAME              READY    UP-TO-DATE    AVAILABLE     AGE
aci-helloworld    1/1      1             1             27d
kubeserve         0/0      0             0             26d

您可以使用kubectl delete命令来删除任何其他对象。但是,正如我们之前提到的,在诸如由部署管理的 Pod 的情况下,删除单个 Pod 是没有意义的,因为部署将会重新创建它们,所以您需要删除部署。

活动 3.01:编辑实时部署以进行真实应用程序

假设您是一名 SysOps 工程师,被要求管理一个集群并部署一个 Web 应用程序。您已经将其部署到您的 Kubernetes 集群,并使其对公众可用。自从成功部署以来,您一直在监视这个应用程序,并且您已经发现在高峰时段,Web 应用程序遇到了限流问题。根据您的监控,您想要实施的解决方案是为这个应用程序分配更多的内存和 CPU。因此,您需要编辑部署,以便为应用程序分配足够的 CPU 和内存资源来运行应用程序并在最后测试这个应用程序。您需要证明您的 Web 应用程序正在运行,并且可以通过您选择的浏览器通过公共 IP 地址访问。

为了模拟这种情况,我们将在 Kubernetes 集群中部署一个示例应用程序,并向您展示如何编辑实时部署。编辑实时部署是您在修复问题或进行测试时需要做的事情。

您可以使用以下命令获取您将在此活动中使用的 YAML 清单文件:

curl https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter03/Activity03.01/sample-application.yaml --output sample-application.yaml

此清单文件定义了运行应用程序所需的所有不同对象,以及应用程序本身。

注意

此清单已经改编自 Microsoft Azure 提供的开源示例,可在github.com/Azure-Samples/azure-voting-app-redis上找到。

执行以下步骤以完成此活动:

  1. 首先,使用kubectl apply命令和提供的 YAML 定义文件部署目标 Web 应用程序。

  2. 获取暴露您的应用程序的服务的 IP 地址。对于这种简单的情况,这将类似于练习 2.03,通过服务访问 Pod,来自上一章。后面的章节将解释如何使用入口控制器并创建入口资源来暴露前端应用程序。

  3. 使用kubectl edit命令编辑实时部署。您需要编辑名为melonvote-front的部署。以下是您需要修改以满足此场景要求的字段。您可以简单地将这些值加倍:

  1. resources.limits.cpu:这是 CPU 使用的资源限制。

  2. resources.limits.memory:这是内存使用的资源限制。

  3. resources.requests.cpu: 这是请求的最小 CPU 使用量,用于启动和运行您的应用程序。

  4. resources.requests.memory: 这是请求的最小内存使用量,用于启动和运行您的应用程序。

在本活动结束时,您将能够看到您使用 Kubernetes 部署的应用程序的用户界面:

图 3.16:活动的预期输出

图 3.16:活动的预期输出

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD

总结

本章揭开了 kubectl 如何允许我们使用 API 调用来控制 Kubernetes 集群的神秘面纱。首先,我们学习了如何为 kubectl 命令设置环境,并查看了一些快捷方式。此外,我们介绍了如何使用 kubectl 命令创建、编辑和删除 Kubernetes 对象,并以 Deployment 为例进行了介绍。最后,我们部署了一个真实的应用程序,并向您展示了如何编辑一个实时的 Deployment。本章中的每个示例都是在一般情境下应用的;然而,我们相信本章中培养的技能可以帮助您解决在专业环境中可能遇到的特定问题。

在下一章中,您将探索这座桥的另一侧,并深入了解 API 服务器的工作原理。您还将更仔细地研究 REST API 请求以及 API 服务器如何处理它们。

第四章: 如何与 Kubernetes(API 服务器)通信

概述

在本章中,我们将建立对 Kubernetes API 服务器及其各种交互方式的基础理解。我们将学习 kubectl 和其他 HTTP 客户端如何与 Kubernetes API 服务器进行通信。我们将使用一些实际演示来跟踪这些通信,并查看 HTTP 请求的详细信息。然后,我们还将看到如何查找 API 详细信息,以便您可以从头开始编写自己的 API 请求。通过本章的学习,您将能够使用任何 HTTP 客户端(如 curl)直接与 API 服务器通信,以创建 API 对象并进行 RESTful API 调用。

介绍

正如您在第二章《Kubernetes 概述》中所记得的,API 服务器充当与 Kubernetes 中所有不同组件通信的中央枢纽。在上一章中,我们看到了如何使用 kubectl 指示 API 服务器执行各种操作。

在本章中,我们将进一步了解构成 API 服务器的组件。由于 API 服务器位于整个 Kubernetes 系统的中心,因此学习如何有效地与 API 服务器本身进行通信以及如何处理 API 请求非常重要。我们还将了解各种 API 概念,如资源、API 组和 API 版本,这将帮助您了解发送到 API 服务器的 HTTP 请求和响应。最后,我们将使用多个 REST 客户端与 Kubernetes API 进行交互,以实现在上一章中使用 kubectl 命令行工具实现的许多相同结果。

Kubernetes API 服务器

在 Kubernetes 中,控制平面组件和外部客户端(如 kubectl)之间的所有通信和操作都被转换为由 API 服务器处理的RESTful API调用。实际上,API 服务器是一个 RESTful Web 应用程序,它通过 HTTP 处理 RESTful API 调用以存储和更新 etcd 数据存储中的 API 对象。

API 服务器也是一个前端组件,充当与外部世界的网关,所有客户端都可以访问,比如 kubectl 命令行工具。即使控制平面中的集群组件也只能通过 API 服务器相互交互。此外,它是唯一直接与 etcd 数据存储交互的组件。由于 API 服务器是客户端访问集群的唯一方式,必须正确配置以供客户端访问。通常会看到 API 服务器实现为kube-apiserver

注意

我们将在本章后面的* Kubernetes API*部分更详细地解释 RESTful API。

现在,让我们通过运行以下命令来回顾一下我们的 Minikube 集群中的 API 服务器的外观:

kubectl get pods -n kube-system

您应该看到以下响应:

图 4.1:观察 Minikube 中 API 服务器的实现方式

图 4.1:观察 Minikube 中 API 服务器的实现方式

正如我们在之前的章节中看到的,在 Minikube 环境中,API 服务器被称为kube-apiserver-minikube,位于kube-system命名空间中。如前面的屏幕截图所示,我们有一个 API 服务器的单个实例:kube-apiserver-minikube

API 服务器是无状态的(即,其行为将始终保持一致,无论集群的状态如何),并且设计为水平扩展。通常,为了集群的高可用性,建议至少有三个实例来更好地处理负载和容错能力。

Kubernetes HTTP 请求流程

正如我们在之前的章节中学到的,当我们运行任何kubectl命令时,该命令会被转换为 JSON 格式的 HTTP API 请求,并发送到 API 服务器。然后,API 服务器将响应返回给客户端,并提供任何请求的信息。以下图表显示了 API 请求的生命周期以及 API 服务器在接收请求时发生的情况:

图 4.2:API 服务器 HTTP 请求流

图 4.2:API 服务器 HTTP 请求流

如前图所示,HTTP 请求经过身份验证、授权和准入控制阶段。我们将在以下子主题中查看每个阶段。

身份验证

在 Kubernetes 中,每个 API 调用都需要通过 API 服务器进行身份验证,无论是来自集群外部的调用,比如 kubectl 发出的调用,还是来自集群内部的进程,比如 kubelet 发出的调用。

当向 API 服务器发送 HTTP 请求时,API 服务器需要对发送此请求的客户端进行身份验证。HTTP 请求将包含所需的身份验证信息,例如用户名、用户 ID 和组。身份验证方法将由请求的标头或证书确定。为了处理这些不同的方法,API 服务器有不同的身份验证插件,例如 ServiceAccount 令牌,用于对 ServiceAccounts 进行身份验证,以及至少一种其他方法来对用户进行身份验证,例如 X.509 客户端证书。

注意

集群管理员通常在集群创建过程中定义认证插件。您可以在kubernetes.io/docs/reference/access-authn-authz/authentication/了解更多关于各种认证策略和认证插件的信息。

我们将在第十一章 构建您自己的 HA 集群中查看基于证书的身份验证的实现。

API 服务器将依次调用这些插件,直到其中一个对请求进行身份验证。如果它们全部失败,则身份验证失败。如果身份验证成功,则身份验证阶段完成,请求继续到授权阶段。

授权

身份验证成功后,将 HTTP 请求中的属性发送到授权插件,以确定用户是否被允许执行请求的操作。不同用户可能具有不同级别的权限;例如,特定用户是否可以在请求的命名空间中创建一个 pod?用户是否可以删除一个部署?这些决定是在授权阶段做出的。

考虑一个例子,假设您有两个用户。一个名为ReadOnlyUser(只是一个假设的名称)应该被允许仅列出default命名空间中的 pod,而ClusterAdmin(另一个假设的名称)应该能够在所有命名空间中执行所有任务:

图 4.3:我们两个用户的权限

图 4.3:我们两个用户的权限

为了更好地理解这一点,请看以下演示:

注意

我们不会深入讨论如何创建用户,因为这将在《第十三章》《Kubernetes 中的运行时和网络安全》中讨论。对于这个演示,用户以及他们的权限已经设置好,并且通过切换上下文来演示他们的权限限制。

图 4.4:演示不同用户权限

图 4.4:演示不同用户权限

请注意,从前面的截图中可以看出,ReadOnlyUser只能在默认命名空间中列出pod,但是当尝试执行其他任务时,比如在default命名空间中删除 pod 或者列出其他命名空间中的 pod 时,用户将会收到Forbidden错误。这个Forbidden错误是由授权插件返回的。

kubectl 提供了一个工具,您可以使用kubectl auth can-i来检查当前用户是否允许执行某项操作。

让我们在前面演示的情境下考虑以下例子。假设ReadOnlyUser运行以下命令:

kubectl auth can-i get pods --all-namespaces
kubectl auth can-i get pods -n default

用户应该看到以下响应:

图 4.5:检查 ReadOnlyUser 的权限

图 4.5:检查 ReadOnlyUser 的权限

现在,在切换上下文之后,假设ClusterAdmin用户运行以下命令:

kubectl auth can-i delete pods
kubectl auth can-i get pods
kubectl auth can-i get pods --all-namespaces

用户应该看到以下响应:

图 4.6:检查 ClusterAdmin 的权限

图 4.6:检查 ClusterAdmin 的权限

与认证阶段模块不同,授权模块是按顺序检查的。如果配置了多个授权模块,并且如果任何授权者批准或拒绝请求,该决定将立即返回,不会联系其他授权者。

准入控制

在请求经过认证和授权之后,它会经过准入控制模块。这些模块可以修改或拒绝请求。如果请求只是尝试执行读取操作,它将绕过这个阶段;但是如果它尝试创建、修改或删除,它将被发送到准入控制器插件。Kubernetes 带有一组预定义的准入控制器,尽管您也可以定义自定义的准入控制器。

这些插件可能会修改传入的对象,在某些情况下会应用系统配置的默认值,甚至会拒绝请求。与授权模块一样,如果任何准入控制器模块拒绝请求,那么请求将被丢弃,不会进一步处理。

一些例子如下:

  • 如果我们配置一个自定义规则,要求每个对象都必须有一个标签(您将在第十六章“Kubernetes 准入控制器”中学习如何做到这一点),那么任何尝试创建没有标签的对象的请求都将被准入控制器拒绝。

  • 当您删除一个命名空间时,它会进入“Terminating”状态,在这种状态下,Kubernetes 会尝试在删除之前清除其中的所有资源。因此,我们无法在此命名空间中创建任何新对象。NamespaceLifecycle就是阻止这种情况发生的。

  • 当客户端尝试在不存在的命名空间中创建资源时,NamespaceExists准入控制器会拒绝该请求。

在 Kubernetes 中包含的不同模块中,并非所有准入控制模块都是默认启用的,默认模块通常会根据 Kubernetes 版本而变化。云基 Kubernetes 解决方案的提供商,如亚马逊网络服务(AWS)、谷歌和 Azure,控制着默认可以启用哪些插件。集群管理员还可以在初始化 API 服务器时决定启用或禁用哪些模块。通过使用--enable-admission-plugins标志,管理员可以控制除默认模块之外应启用哪些模块。另一方面,--disable-admission-plugins标志控制默认模块中应禁用哪些模块。

注意

您将在第十六章“Kubernetes 准入控制器”中学习更多关于准入控制器的知识,包括创建自定义准入控制器。

正如您在第二章“Kubernetes 概述”中所记得的,当我们使用minikube start命令创建集群时,Minikube 默认为我们启用了几个模块。让我们在下一个练习中更仔细地看一下这一点,我们不仅会查看默认情况下为我们启用的不同 API 模块,还会使用自定义的模块启动 Minikube。

练习 4.01:使用自定义模块启动 Minikube

在这个练习中,我们将看一下如何查看我们的 Minikube 实例启用的不同 API 模块,然后使用自定义的 API 模块重新启动 Minikube:

  1. 如果 Minikube 尚未在您的计算机上运行,请使用以下命令启动它:
minikube start

您应该看到以下响应:

图 4.7:启动 Minikube

图 4.7:启动 Minikube

  1. 现在,让我们看看默认情况下启用了哪些模块。使用以下命令:
kubectl describe pod kube-apiserver-minikube -n kube-system | grep enable-admission-plugins

您应该看到以下响应:

图 4.8:Minikube 中默认启用的模块

图 4.8:Minikube 中默认启用的模块

正如您可以从前面的输出中观察到的,Minikube 已为我们启用了以下模块:NamespaceLifecycleLimitRangerServiceAccountDefaultStorageClassDefaultTolerationSecondsNodeRestrictionMutatingAdmissionWebhookValidatingAdmissionWebhookResourceQuota

注意

要了解更多关于模块的信息,请参考以下链接:kubernetes.io/docs/reference/access-authn-authz/admission-controllers/

  1. 另一种检查模块的方法是查看 API 服务器清单,方法是运行以下命令:
kubectl exec -it kube-apiserver-minikube -n kube-system -- kube-apiserver -h | grep "enable-admission-plugins" | grep -vi deprecated

注意

我们使用了grep -vi deprecated,因为还有另一个标志--admission-control,我们正在从输出中丢弃,因为该标志将在将来的版本中被弃用。

kubectl 有exec命令,允许我们对正在运行的 pod 执行命令。该命令将在kube-apiserver-minikube pod 内执行kube-apiserver -h命令,并将输出返回到我们的 shell:

图 4.9:检查 Minikube 中默认启用的模块

图 4.9:检查 Minikube 中默认启用的模块

  1. 现在,我们将使用我们期望的配置启动 Minikube。使用以下命令:
minikube start --extra-config=apiserver.enable-admission-plugins="LimitRanger,NamespaceExists,NamespaceLifecycle,ResourceQuota,ServiceAccount,DefaultStorageClass,MutatingAdmissionWebhook"

如您在此处所见,minikube start命令具有--extra-config配置器标志,允许我们向集群安装传递附加配置。在我们的情况下,我们可以使用--extra-config标志,以及--enable-admission-plugins,并指定我们需要启用的插件。我们的命令应该产生这样的输出:

图 4.10:使用自定义模块重新启动 Minikube

图 4.10:使用自定义模块重新启动 Minikube

  1. 现在,让我们将此 Minikube 实例与我们之前的实例进行比较。使用以下命令:
kubectl describe pod kube-apiserver-minikube -n kube-system | grep enable-admission-plugins

您应该看到以下响应:

图 4.11:检查 Minikube 的自定义模块集

图 4.11:检查 Minikube 的自定义模块集

如果您将此处看到的模块集与图 4.7中的模块集进行比较,您会注意到只启用了指定的插件;而DefaultTolerationSecondsNodeRestrictionValidatingAdmissionWebhook模块不再启用。

注意

您可以通过再次运行minikube start来恢复 Minikube 中的默认配置。

验证

在让请求通过所有三个阶段之后,API 服务器然后验证对象——也就是说,它检查响应主体中以 JSON 格式携带的对象规范是否符合所需的格式和标准。

成功验证后,API 服务器将对象存储在 etcd 数据存储中,并向客户端返回响应。之后,正如您在第二章中学到的* Kubernetes 概述*中所了解的那样,其他组件,如调度程序和控制器管理器,接管了寻找合适的节点并实际在集群上实现对象。

Kubernetes API

Kubernetes API 使用 JSON over HTTP 进行请求和响应。它遵循 REST 架构风格。您可以使用 Kubernetes API 来读取和编写 Kubernetes 资源对象。

注意

有关 RESTful API 的更多详细信息,请参阅restfulapi.net/

Kubernetes API 允许客户端通过标准 HTTP 方法(或 HTTP 动词)创建、更新、删除或读取对象的描述,例如以下表中的示例:

图 4.12:HTTP 动词及其用法

图 4.12:HTTP 动词及其用法

在 Kubernetes API 调用的上下文中,了解这些 HTTP 方法如何映射到 API 请求动词是有帮助的。因此,让我们看看哪些动词通过哪些方法发送:

  • GETgetlistwatch

一些示例 kubectl 命令是kubectl get podkubectl describe pod <pod-name>kubectl get pod -w

  • POSTcreate

一个示例 kubectl 命令是kubectl create -f <filename.yaml>

  • PATCHpatch

一个示例 kubectl 命令是kubectl set image deployment/kubeserve nginx=nginx:1.9.1

  • DELETEdelete

一个示例 kubectl 命令是kubectl delete pod <pod-name>

  • PUTupdate

一个示例 kubectl 命令是kubectl apply -f <filename.yaml>

注意

如果您还没有遇到这些命令,您将在接下来的章节中遇到。随时参考本章节或以下 Kubernetes 文档,了解每个命令的 API 请求如何工作:kubernetes.io/docs/reference/kubernetes-api/

如前所述,这些 API 调用携带 JSON 数据,所有这些数据都有一个由 KindapiVersion 字段标识的 JSON 模式。Kind 是一个字符串,用于标识对象应该具有的 JSON 模式类型,而 apiVersion 是一个字符串,用于标识对象应该具有的 JSON 模式版本。下一个练习应该让您更好地了解这一点。

您可以参考 Kubernetes API 参考文档,查看不同的 HTTP 方法的实际操作,网址为 kubernetes.io/docs/reference/kubernetes-api/

例如,如果您需要在特定命名空间下创建一个 Deployment,在 WORKLOADS APIS 下,您可以转到 Deployment v1 apps > Write Operations > Create。您将看到使用 kubectlcurl 的 HTTP 请求和不同的示例。API 参考文档的以下页面应该让您了解如何使用此参考文档:

图 4.13:kubectl create 命令的 HTTP 请求

图 4.13:kubectl create 命令的 HTTP 请求

当您参考前面提到的文档时,需要牢记您的 API 服务器版本。您可以通过运行 kubectl version --short 命令并查找 Server Version 来找到您的 Kubernetes API 服务器版本。例如,如果您的 Kubernetes API 服务器版本运行的是 1.14 版本,您应该转到 Kubernetes 版本 1.14 参考文档 (v1-14.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/) 查找相关的 API 信息。

了解这一点的最佳方法是跟踪 kubectl 命令。让我们在接下来的部分中做到这一点。

跟踪 kubectl 的 HTTP 请求

让我们尝试跟踪 kubectl 发送到 API 服务器的 HTTP 请求,以更好地理解它们。在开始之前,让我们使用以下命令获取 kube-system 命名空间中的所有 pod:

kubectl get pods -n kube-system

该命令应该以表格视图显示输出,如下面的屏幕截图所示:

图 4.14:获取 kube-system 命名空间中 pod 的列表

图 4.14:获取 kube-system 命名空间中 pod 的列表

在幕后,由于 kubectl 是一个 REST 客户端,它会调用 HTTP GET请求到 API 服务器端点,并从/api/v1/namespaces/kube-system/pods请求信息。

我们可以通过在kubectl命令中添加--v=8来启用详细输出。v表示命令的详细程度。数字越高,响应中的细节就越多。这个数字可以从010。让我们看看详细程度为8的输出:

kubectl get pods -n kube-system --v=8

这应该给出以下输出:

图 4.15:详细程度为 8 的 get pods 命令的输出

图 4.15:详细程度为 8 的 get pods 命令的输出

让我们逐个检查前面的输出,以更好地理解它:

  • 输出的第一部分如下:图 4.16:输出的一部分指示配置文件的加载

图 4.16:输出的一部分指示配置文件的加载

从中我们可以看到,kubectl 从我们的 kubeconfig 文件中加载了配置,其中包括 API 服务器端点、端口和凭据,如证书或身份验证令牌。

  • 这是输出的下一部分:图 4.17:输出的一部分指示 HTTP GET 请求

图 4.17:输出的一部分指示 HTTP GET 请求

在这里,您可以看到将HTTP GET请求提及为GET https://192.168.99.100:8443/api/v1/namespaces/kube-system/pods?limit=500。这一行包含了我们需要针对 API 服务器执行的操作,/api/v1/namespaces/kube-system/pods是 API 路径。您还可以在 URL 路径的末尾看到limit=500,这是块大小;kubectl 以块的形式获取大量资源,以提高延迟。我们将在本章后面看到一些关于以块检索大型结果集的示例。

  • 输出的下一部分如下:图 4.18:输出的一部分指示请求头

图 4.18:输出的一部分指示请求头

从输出的这部分可以看出,请求头描述了要获取的资源或请求资源的客户端。在我们的示例中,输出有两部分内容协商:

  1. Accept:这是由 HTTP 客户端使用的,告诉服务器它们将接受什么内容类型。在我们的示例中,我们可以看到 kubectl 通知 API 服务器有关application/json内容类型。如果请求标头中不存在这个内容类型,服务器将返回默认的预配置表示类型,这与 Kubernetes API 的 JSON 模式相同,即application/json。我们还可以看到它正在请求以表格视图输出,这在这一行中由as=Table表示。

  2. User-Agent:此标头包含有关请求此信息的客户端的信息。在这种情况下,我们可以看到 kubectl 正在提供有关自身的信息。

  • 让我们来检查下一部分:图 4.19:显示响应状态的部分输出

图 4.19:显示响应状态的部分输出

在这里,我们可以看到 API 服务器返回了200 OKHTTP 状态代码,这表明请求已在 API 服务器上成功处理。我们还可以看到处理此请求所花费的时间,为 10 毫秒。

  • 让我们来看下一部分:图 4.20:显示响应头的部分输出

图 4.20:显示响应头的部分输出

正如您所看到的,这部分显示了“响应头”,其中包括请求的日期和时间等详细信息,在我们的示例中。

  • 现在,让我们来看一下 API 服务器发送的主要响应:图 4.21:显示响应主体的部分输出

图 4.21:显示响应主体的部分输出

“响应主体”包含客户端请求的资源数据。在我们的案例中,这是有关kube-system命名空间中的 pod 的信息。在这里,这些信息以原始 JSON 格式呈现,然后 kubectl 可以将其呈现为整齐的表格。然而,前一个屏幕截图的突出显示部分显示,响应主体并没有我们请求的所有 JSON 输出;部分“响应主体”被截断了。这是因为--v=8显示了带有截断响应内容的 HTTP 请求内容。

要查看完整的响应主体,您可以使用--v=10运行相同的命令,这样就不会截断输出。命令将如下所示:

kubectl get pods -n kube-system --v=10

出于简洁起见,我们将不会检查使用--v=10详细程度的命令。

  • 现在,我们来到了我们正在检查的输出的最后部分:图 4.22:输出的一部分,显示最终结果

图 4.22:输出的一部分,显示最终结果

这是一个表格形式的最终输出,这也是请求的内容。kubectl 已经将原始的 JSON 数据格式化为一个整洁的表格。

注意

您可以在kubernetes.io/docs/reference/kubectl/cheatsheet/#kubectl-output-verbosity-and-debugging了解更多关于 kubectl 冗长度和调试标志的信息。

API 资源类型

在前一节中,我们看到 HTTP URL 由 API 资源、API 组和 API 版本组成。现在,让我们了解 URL 中定义的资源类型,比如 pods、namespaces 和 services。在 JSON 格式中,这被称为Kind

  • 资源的集合:这代表了资源类型的所有实例的集合,比如所有命名空间中的所有 pods。在 URL 中,这将如下所示:
GET /api/v1/pods
  • 单个资源:这代表了资源类型的单个实例,比如在给定命名空间中检索特定 pod 的详细信息。这种情况下的 URL 将如下所示:
GET /api/v1/namespaces/{namespace}/pods/{name}

现在我们已经了解了向 API 服务器发出请求的各个方面,让我们在下一节了解 API 资源的范围。

API 资源的范围

所有资源类型可以是集群范围的资源或命名空间范围的资源。资源的范围会影响对该资源的访问以及该资源的管理方式。让我们来看看命名空间和集群范围之间的区别。

命名空间范围的资源

正如我们在第二章中看到的,Kubernetes 概述,Kubernetes 利用 Linux 命名空间来组织大多数 Kubernetes 资源。同一命名空间中的资源共享相同的控制访问策略和授权检查。当一个命名空间被删除时,该命名空间中的所有资源也会被删除。

让我们看看与命名空间范围的资源交互的请求路径是什么样的:

  • 返回命名空间中特定 pod 的信息:
GET /api/v1/namespaces/{my-namespace}/pods/{pod-name}
  • 返回命名空间中所有 Deployments 的集合的信息:
GET /apis/apps/v1/namespaces/{my-namespace}/deployments
  • 返回关于资源类型的所有实例的信息(在本例中为 services),跨所有命名空间:
GET /api/v1/services

请注意,当我们在所有命名空间中查找信息时,URL 中将不会有namespace

您可以使用以下命令获取命名空间范围 API 资源的完整列表:

kubectl api-resources --namespaced=true

您应该看到类似于这样的响应:

图 4.23:列出所有命名空间范围资源

图 4.23:列出所有命名空间范围资源

集群范围资源

大多数 Kubernetes 资源是命名空间范围的,但命名空间资源本身不是命名空间范围的。不在命名空间范围内的资源是集群范围的。集群范围资源的其他示例是节点。由于节点是集群范围的,您可以在所需的节点上部署一个 pod,而不管您希望 pod 在哪个命名空间,一个节点可以托管来自不同命名空间的不同 pod。

让我们看看与集群范围资源交互的请求路径是什么样的:

  • 返回集群中特定节点的信息:
GET /api/v1/nodes/{node-name}
  • 返回集群中资源类型的所有实例的信息(在本例中是节点):
GET /api/v1/nodes
  • 您可以使用以下命令获取集群范围 API 资源的完整列表:
kubectl api-resources --namespaced=false

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

图 4.24:列出所有集群范围资源

图 4.24:列出所有集群范围资源

API 组

API 组是逻辑相关的资源的集合。例如,部署、副本集和守护进程集都属于 apps API 组:apps/v1

注意

您将在第七章Kubernetes Controllers中详细了解部署、副本集和守护进程集。事实上,本章将讨论您将在后续章节中遇到的许多 API 资源。

--api-group标志可用于将输出范围限定到特定的 API 组,我们将在接下来的章节中看到。让我们在接下来的章节中更仔细地看看各种 API 组。

核心组

这也被称为传统组。它包含诸如 pod、服务、节点和命名空间等对象。这些的 URL 路径是/api/v1apiVersion字段中只指定版本。例如,考虑以下截图,我们正在获取有关一个 pod 的信息:

图 4.25:一个 pod 的 API 组

图 4.25:一个 pod 的 API 组

正如您在这里看到的,apiVersion: v1字段表示此资源属于核心组。

kubectl api-resources命令输出中显示空条目的资源属于核心组。您还可以指定一个空参数标志(--api-group='')来仅显示核心组资源,如下所示:

kubectl api-resources --api-group=''

您应该看到以下输出:

图 4.26:列出核心 API 组中的资源

图 4.26:列出核心 API 组中的资源

命名组

该组包括请求 URL 格式为/apis/$NAME/$VERSION的对象。与核心组不同,命名组在 URL 中包含组名。例如,让我们考虑以下屏幕截图,其中我们有有关部署的信息:

图 4.27:部署的 API 组

图 4.27:部署的 API 组

正如您所看到的,突出显示的字段显示apiVersion: apps/v1,表明此资源属于appsAPI 组。

您还可以指定--api-group='<NamedGroup Name>'标志来显示指定命名组中的资源。例如,让我们使用以下命令列出appsAPI 组中的资源:

kubectl api-resources --api-group='apps'

这应该会产生以下响应:

图 4.28:列出 apps API 组中的资源

图 4.28:列出 apps API 组中的资源

在上述屏幕截图中,所有这些资源都被合并在一起,因为它们都属于我们在查询命令中指定的apps命名组。

作为另一个例子,让我们看一下rbac.authorization.k8s.io API 组,该组具有确定授权策略的资源。我们可以使用以下命令查看该组中的资源:

kubectl api-resources --api-group='rbac.authorization.k8s.io'

您应该看到以下响应:

图 4.29:列出 rbac.authorization.k8s.io API 组中的资源

图 4.29:列出 rbac.authorization.k8s.io API 组中的资源

系统范围

该组包括系统范围的 API 端点,例如/version/healthz/logs/metrics。例如,让我们考虑以下命令的输出:

kubectl version --short --v=6

这应该会产生类似于以下内容的输出:

图 4.30:kubectl 版本命令的请求 URL

图 4.30:kubectl 版本命令的请求 URL

正如您在此屏幕截图中所看到的,当您运行kubectl --version时,这将转到/version 特殊实体,如GET请求 URL 中所示。

API 版本

在 Kubernetes API 中,存在 API 版本控制的概念;也就是说,Kubernetes API 支持一种资源类型的多个版本。这些不同版本可能会有不同的行为。每个版本都有不同的 API 路径,例如/api/v1/apis/extensions/v1beta1

不同的 API 版本在稳定性和支持方面有所不同:

  • Alpha:此版本在apiVersion字段中以alpha表示,例如/apis/batch/v1alpha1。默认情况下禁用资源的 alpha 版本,因为它不适用于生产集群,但可以供早期采用者和愿意提供反馈和建议并报告错误的开发人员使用。此外,alpha 资源的支持可能会在 Kubernetes 的最终稳定版本确定时被取消,而无需事先通知。

  • Beta:此版本在apiVersion字段中以beta表示,例如/apis/certificates.k8s.io/v1beta1。默认情况下启用资源的 beta 版本,并且其背后的代码经过了充分测试。然而,建议仅在非业务关键场景下使用,因为可能会在后续版本中进行更改,导致不兼容性;也就是说,某些功能可能不会长时间得到支持。

  • 稳定:对于这些版本,apiVersion字段只包含版本号,没有提及alphabeta,例如/apis/networking.k8s.io/v1。稳定版本的资源在 Kubernetes 的许多后续版本中得到支持。因此,建议在任何关键用例中使用此版本的 API 资源。

您可以使用以下命令获取集群中启用的 API 版本的完整列表:

kubectl api-versions

您应该看到类似于以下内容的响应:

图 4.31:已启用的 API 资源版本列表

图 4.31:已启用的 API 资源版本列表

在此截图中,您可能会注意到一件有趣的事情,即某些 API 资源(例如autoscaling)具有多个版本;例如,对于autoscaling,有v1beta1v1beta2v1。那么,它们之间有什么区别,应该使用哪个版本呢?

让我们再次考虑autoscaling的例子。此功能允许您根据特定指标扩展副本控制器(例如 Deployments、ReplicaSets 或 StatefulSets)中的 Pod 数量。例如,如果平均 CPU 负载超过 50%,则可以将 Pod 的数量从 3 个扩展到 10 个。

在这种情况下,版本之间的差异是功能支持。自动缩放的稳定版本是autoscaling/v1,它仅支持根据平均 CPU 指标缩放 pod 的数量。自动缩放的 beta 版本是autoscaling/v2beta1,支持根据 CPU 和内存利用率进行缩放。beta 版本中的新版本是autoscaling/v2beta2,支持根据自定义指标以及 CPU 和内存进行缩放 pod 的数量。然而,由于 beta 版本仍不适用于业务关键场景,当您创建自动缩放资源时,它将使用autoscaling/v1版本。但是,您仍然可以通过在 YAML 文件中指定 beta 版本来使用其他版本以使用附加功能,直到所需功能添加到稳定版本为止。

所有这些信息可能看起来令人不知所措。然而,Kubernetes 提供了访问所有您需要导航 API 资源的信息的方法。您可以使用 kubectl 访问 Kubernetes 文档,并获取有关各种 API 资源的必要信息。让我们看看在以下练习中如何运作。

练习 4.02:获取有关 API 资源的信息

假设我们想要创建一个 ingress 对象。在这个练习中,您不需要太多了解 ingress;我们将在接下来的章节中学习它。我们将使用 kubectl 获取有关 Ingress API 资源的更多信息,确定可用的 API 版本,并找出它属于哪些组。如果您还记得之前的部分,我们需要这些信息来填写 YAML 清单的apiVersion字段。然后,我们还需要获取清单文件的其他字段所需的信息:

  1. 首先,让我们询问我们的集群是否有与ingresses关键字匹配的所有可用 API 资源:
    kubectl api-resources | grep ingresses    

这个命令将通过ingresses关键字过滤所有 API 资源的列表。您应该得到以下输出:

    ingresses     ing     extensions            true         Ingress
    ingresses     ing     networking.k8s.io     true         Ingress    

我们可以看到我们在两个不同的 API 组上有 ingress 资源——extensionsnetworking.k8s.io

  1. 我们还看到了如何获取属于特定组的 API 资源。让我们检查一下我们在上一步中看到的 API 组:
    kubectl api-resources --api-group="extensions"    

您应该得到以下输出:

    NAME       SHORTNAMES     APIGROUP     NAMESPACED      KIND
    ingresses  ing            extensions   true            Ingress    

现在,让我们检查另一个组:

    kubectl api-resources --api-group="networking.k8s.io"    

您应该看到以下输出:

图 4.32:列出 networking.k8s.io API 组中的资源

图 4.32:列出 networking.k8s.io API 组中的资源

但是,如果我们要使用 Ingress 资源,我们仍然不知道我们应该使用来自extensions组还是networking.k8s.io组的资源。在下一步中,我们将获得一些更多信息,这将帮助我们决定。

  1. 使用以下命令获取更多信息:
kubectl explain ingress

您应该得到这个响应:

图 4.33:从扩展 API 组获取 Ingress 资源的详细信息

图 4.33:从扩展 API 组获取 Ingress 资源的详细信息

正如您所看到的,kubectl explain命令描述了 API 资源,以及与其相关的字段的详细信息。我们还可以看到 Ingress 使用extensions/v1beta1 API 版本,但如果我们阅读DESCRIPTION,它提到此 Ingress 组版本已被networking.k8s.io/v1beta1弃用。弃用意味着标准正在逐步淘汰,尽管目前仍受支持,但不建议使用。

注意

如果您将此与我们在此练习之前看到的autoscaling的不同版本进行比较,您可能会认为从v1beta的逻辑升级路径将是v2beta,这完全是有道理的。然而,Ingress 资源已从extensions组移动到networking.k8s.io组,因此这违反了命名趋势。

  1. 使用弃用版本不是一个好主意,所以假设您想要使用networking.k8s.io/v1beta1版本。但是,我们首先需要获取更多关于它的信息。我们可以向kubectl explain命令添加一个标志来获取关于 API 资源特定版本的信息,如下所示:
kubectl explain ingress --api-version=networking.k8s.io/v1beta1

您应该看到这个响应:

图 4.34:从 networking.k8s.io API 组获取 Ingress 资源的详细信息

图 4.34:从 networking.k8s.io API 组获取 Ingress 资源的详细信息

  1. 我们还可以通过使用JSONPath标识符来过滤kubectl explain命令的输出。这使我们能够获取关于定义 YAML 清单时需要指定的各个字段的信息。因此,例如,如果我们想要查看 Ingress 的spec字段,命令将如下所示:
kubectl explain ingress.spec --api-version=networking.k8s.io/v1beta1

这应该给出以下响应:

图 4.35:过滤 kubectl explain 命令的输出要获取 Ingress 的 spec 字段

图 4.35:过滤 kubectl explain 命令的输出以获取 ingress 的 spec 字段

  1. 我们可以深入了解嵌套字段的更多细节。例如,如果您想要获取有关 ingress 的backend字段的更多详细信息,我们可以指定ingress.spec.backend以获取所需的信息:
kubectl explain ingress.spec.backend --api-version=networking.k8s.io/v1beta1

这将产生以下输出:

图 4.36:过滤 kubectl explain 命令的输出以获取 ingress 的 spec.backend 字段

图 4.36:过滤 kubectl explain 命令的输出以获取 ingress 的 spec.backend 字段

同样,我们可以重复这个过程以获取您需要的任何字段的信息,这对于构建或修改 YAML 清单非常方便。因此,我们已经看到kubectl explain命令在寻找有关 API 资源的更多详细信息和文档时非常有用。在使用 YAML 清单文件创建或修改对象时,它也非常有用。

如何启用/禁用 API 资源、组或版本

在典型的集群中,并非所有 API 组都是默认启用的。这取决于管理员确定的集群用例。例如,一些 Kubernetes 云提供商出于稳定性和安全性原因禁用使用 alpha 级别的资源。但是,仍然可以通过使用--runtime-config标志在 API 服务器上启用这些资源,该标志接受逗号分隔的列表。

要能够创建任何资源,集群中应启用组和版本。例如,当您尝试在清单文件中使用apiVersion: batch/v2alpha1创建一个CronJob时,如果组/版本未启用,您将收到类似以下错误的消息:

No matches for kind "CronJob" in version "batch/v2alpha1".

要启用batch/v2alpha1,您需要在 API 服务器上设置--runtime-config=batch/v2alpha1。这可以在创建集群期间或通过更新/etc/kubernetes/manifests/kube-apiserver.yaml清单文件来完成。该标志还支持通过为特定版本设置false值来禁用 API 组或版本,例如,--runtime-config=batch/v1=false

--runtime-config还支持api/all特殊键,用于控制所有 API 版本。例如,要关闭除v1之外的所有 API 版本,您可以传递--runtime-config=api/all=false,api/v1=true标志。让我们尝试自己动手创建和禁用 API 组和版本的示例练习。

练习 4.03:在 Minikube 集群上启用和禁用 API 组和版本

在这个练习中,我们将在启动 Minikube 时创建特定的 API 版本,禁用运行中集群中的某些 API 版本,然后在整个 API 组中启用/禁用资源:

  1. 使用以下命令启动 Minikube:
minikube start --extra-config=apiserver.runtime-config=batch/v2alpha1

您应该看到以下响应:

图 4.37:使用额外的 API 资源组启动 Minikube

图 4.37:使用额外的 API 资源组启动 Minikube

注意

您可以参考minikube start文档,了解有关--extra-config标志的更多详细信息,网址为minikube.sigs.k8s.io/docs/handbook/config/

  1. 您可以通过使用describe pod命令并通过runtime关键字过滤结果来确认它是否已启用:
kubectl describe pod kube-apiserver-minikube -n kube-system | grep runtime

您应该看到以下响应:

--runtime-config=batch/v2alpha1
  1. 另一种确认的方法是使用以下命令查看已启用的 API 版本:
kubectl api-versions | grep batch/v2alpha1

您应该看到以下响应:

batch/v2alpha1
  1. 现在,让我们创建一个名为CronJob的资源,它使用batch/v2alpha1来确认我们的 API 服务器是否接受 API。创建一个名为sample-cronjob.yaml的文件,内容如下:
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure
  1. 现在,使用此 YAML 文件创建一个CronJob
kubectl create -f sample-cronjob.yaml

您应该看到以下输出:

cronjob.batch/hello created

正如您所看到的,API 服务器接受了我们的 YAML 文件,并成功创建了CronJob

  1. 现在,让我们在集群上禁用batch/v2alpha1。为此,我们需要访问 Minikube 虚拟机(VM),如前几章所示:
minikube ssh

您应该看到这个响应:

图 4.38:通过 SSH 访问 Minikube VM

图 4.38:通过 SSH 访问 Minikube VM

  1. 打开 API 服务器清单文件。这是 Kubernetes 用于 API 服务器 pod 的模板。我们将使用 vi 来修改此文件,尽管您可以使用任何您喜欢的文本编辑器:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

您应该看到类似以下的响应:

图 4.39:API 服务器规范文件

图 4.39:API 服务器规范文件

查找包含--runtime-config=batch/v2alpha1的行,并将其更改为--runtime-config=batch/v2alpha1=false。然后保存修改后的文件。

  1. 使用以下命令结束 SSH 会话:
exit
  1. 为了使 API 服务器清单中的更改生效,我们需要重新启动 API 服务器和控制器管理器。由于它们是部署为无状态的 pod,我们可以简单地删除它们,它们将自动重新部署。首先,让我们通过运行这个命令来删除 API 服务器:
kubectl delete pods -n kube-system -l component=kube-apiserver

您应该看到这个输出:

pod "kube-apiserver-minikube" deleted

现在,让我们删除控制器管理器:

kubectl delete pods -n kube-system -l component=kube-controller-manager

您应该看到这个输出:

pod "kube-controller-manager-minikube" deleted

请注意,对于这两个命令,我们没有按名称删除 pod。-l标志寻找标签。这些命令删除了kube-system命名空间中所有具有与-l标志后指定的标签匹配的 pod。

  1. 我们可以使用以下命令确认batch/v2alpha1不再显示在 API 版本中:
kubectl api-versions | grep batch/v2alpha1

这个命令不会给你任何响应,表明我们已经禁用了batch/v2alpha1

因此,我们已经看到了如何启用或禁用特定组或版本的 API 资源。但这仍然是一个广泛的方法。如果你想要禁用特定的 API 资源怎么办?

举个例子,假设你想要禁用 ingress。我们在前面的练习中看到,我们在extensionsnetworking.k8s.io API 组中都有 ingresses。如果你要针对特定的 API 资源,你需要指定它的组和版本。假设你想要禁用extensions组中的 ingress,因为它已经被弃用。在这个组中,我们只有一个版本的 ingresses,即v1beta,你可以从图 4.33中观察到。

要实现这一点,我们只需要修改--runtime-config标志来指定我们想要的资源。所以,如果我们想要禁用extensions组中的 ingress,标志将如下所示:

--runtime-config=extensions/v1beta1/ingresses=false

要禁用资源,我们可以在启动 Minikube 时使用这个标志,就像本练习的步骤 1中所示,或者我们可以将这行添加到 API 服务器的清单文件中,就像本练习的步骤 7中所示。回想一下,如果我们想要启用资源,我们只需要从这个标志的末尾删除=false部分。

使用 Kubernetes API 与集群交互

到目前为止,我们一直在使用 Kubernetes kubectl 命令行工具,这使得与我们的集群交互非常方便。它通过从客户端 kubeconfig 文件中提取 API 服务器地址和身份验证信息来实现这一点,默认情况下位于~/.kube/config中,正如我们在上一章中看到的那样。在本节中,我们将看看使用 curl 等 HTTP 客户端直接访问 API 服务器的不同方法。

有两种直接通过 REST API 访问 API 服务器的可能方式——通过使用 kubectl 代理模式或直接向 HTTP 客户端提供位置和身份验证凭据。我们将探讨这两种方法,以了解各自的优缺点。

使用 kubectl 作为代理访问 Kubernetes API 服务器

kubectl 有一个很棒的功能叫做kubectl proxy,这是与 API 服务器交互的推荐方法。这是推荐的,因为它更容易使用,并提供了更安全的方式,因为它通过使用自签名证书验证 API 服务器的身份,防止了中间人MITM)攻击。

kubectl 代理将 HTTP 客户端的请求路由到 API 服务器,同时自行处理身份验证。身份验证也是通过使用我们 kubeconfig 文件中的当前配置来处理的。

为了演示如何使用 kubectl 代理,让我们首先在默认命名空间中创建一个具有两个副本的 NGINX 部署,并使用kubectl get pods查看它:

kubectl create deployment mynginx --image=nginx:latest 

这应该会得到类似以下的输出:

deployment.apps/mynginx created

现在,我们可以使用以下命令将我们的部署扩展到两个副本:

kubectl scale deployment mynginx --replicas=2

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

deployment.apps/mynginx scaled

现在让我们检查一下 pod 是否正在运行:

kubectl get pods

这会产生类似以下的输出:

NAME                        READY    STATUS     RESTARTS   AGE
mynginx-565f67b548-gk5n2    1/1      Running    0          2m30s
mynginx-565f67b548-q6slz    1/1      Running    0          2m30s

要启动到 API 服务器的代理,请运行kubectl proxy命令:

kubectl proxy

这应该会产生以下输出:

Starting to serve on 127.0.0.1:8001

请注意前面截图中本地代理连接正在127.0.0.1:8001上运行,默认情况下。我们还可以通过添加--port=<YourCustomPort>标志来指定自定义端口,同时在命令的末尾添加&(和号)符号,以允许代理在终端后台运行,这样我们可以在同一个终端窗口中继续工作。因此,命令看起来像这样:

kubectl proxy --port=8080 &

这应该会得到以下响应:

[1] 48285
AbuTalebMBP:~ mohammed$ Starting to serve on 127.0.0.1:8080

代理作为后台作业运行,在前面的截图中,[1]表示作业编号,48285表示其进程 ID。要退出在后台运行的代理,可以运行fg将作业带回前台:

fg

这将显示以下响应:

kubectl proxy --port=8080
^C

将代理切换到前台后,我们可以简单地使用Ctrl + C退出它(如果没有其他作业在运行)。

注意

如果您对作业控制不熟悉,可以在www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html了解相关信息。

现在我们可以开始使用 curl 来探索 API:

curl http://127.0.0.1:8080/apis

请记住,尽管我们主要使用 YAML 来方便,但数据以 JSON 格式存储在 etcd 中。您将看到一个长的响应,类似于以下内容:

图 4.40:来自 API 服务器的响应

图 4.40:来自 API 服务器的响应

但是,我们如何找到查询我们之前创建的部署的确切路径?另外,我们如何查询该部署创建的 pod?

您可以先问自己几个问题:

  • 部署使用的 API 版本和 API 组是什么?

图 4.27中,我们看到部署在apps/v1中,因此我们可以从那里开始添加路径:

curl http://127.0.0.1:8080/apis/apps/v1
  • 它是命名空间范围的资源还是集群范围的资源?如果它是命名空间范围的资源,命名空间的名称是什么?

我们还在 API 资源范围部分看到,部署是命名空间范围的资源。当我们创建部署时,由于我们没有指定不同的命名空间,它进入了default命名空间。因此,除了apiVersion字段外,我们还需要将namespaces/default/deployments添加到我们的路径中:

curl http://127.0.0.1:8080/apis/apps/v1/namespaces/default/deployments

这将返回一个包含存储在此路径上的 JSON 数据的大型输出。这是响应的一部分,给了我们需要的信息:

图 4.41:使用 curl 获取有关所有部署的信息

图 4.41:使用 curl 获取有关所有部署的信息

正如您在此输出中所看到的,这列出了default命名空间中的所有部署。您可以从"kind": "DeploymentList"推断出这一点。另外,请注意,响应是以 JSON 格式呈现的,而不是整齐地呈现为表格。

现在,我们可以通过将特定部署添加到路径来指定特定的部署:

curl http://127.0.0.1:8080/apis/apps/v1/namespaces/default/deployments/mynginx

您应该看到这个响应:

图 4.42:使用 curl 获取有关我们的 NGINX 部署的信息

图 4.42:使用 curl 获取有关我们的 NGINX 部署的信息

您也可以将此方法用于任何其他资源。

使用 curl 创建对象

当您使用任何 HTTP 客户端(如 curl)向 API 服务器发送请求以创建对象时,您需要更改三件事:

  1. HTTP请求方法更改为POST。默认情况下,curl 将使用GET方法。要创建对象,我们需要使用POST方法,正如我们在* Kubernetes API *部分学到的那样。您可以使用-X标志更改这一点。

  2. 更改 HTTP 请求头。我们需要修改标头以通知 API 服务器请求的意图是什么。我们可以使用-H标志修改标头。在这种情况下,我们需要将标头设置为'Content-Type: application/yaml'

  3. 包括要创建的对象的规范。正如您在前两章中学到的,每个 API 资源都作为 API 对象持久存在于 etcd 中,由 YAML 规范/清单文件定义。要创建对象,您需要使用--data标志将 YAML 清单传递给 API 服务器,以便它可以将其持久保存在 etcd 中作为对象。

因此,curl 命令将如下所示:

curl -X POST <URL-path> -H 'Content-Type: application/yaml' --data <spec/manifest>

有时,您会随时掌握清单文件。但情况并非总是如此。此外,我们还没有看到命名空间的清单是什么样子。

让我们考虑一个情况,我们想要创建一个命名空间。通常,您会按以下方式创建命名空间:

kubectl create namespace my-namespace

这将产生以下响应:

namespace/my-namespace created

在这里,您可以看到我们创建了一个名为my-namespace的命名空间。但是,为了在不使用 kubectl 的情况下传递请求,我们需要用于定义命名空间的规范。我们可以通过使用--dry-run=client-o标志来获取:

kubectl create namespace my-second-namespace --dry-run=client -o yaml

这将产生以下响应:

图 4.43:使用 dry-run 获取命名空间的规范

图 4.43:使用 dry-run 获取命名空间的规范

当您使用kubectl命令带有--dry-run=client标志时,API 服务器会将其通过正常命令的所有阶段,只是不会将更改持久化到 etcd 中。因此,命令经过了身份验证、授权和验证,但更改不是永久性的。这是一个很好的测试某个命令是否有效的方法,也可以获取 API 服务器为该命令创建的清单,就像您在之前的截图中看到的那样。让我们看看如何将其付诸实践,并使用 curl 创建一个部署。

练习 4.04:使用 kubectl 代理和 curl 创建和验证部署

在这个练习中,我们将在名为example的命名空间中创建一个名为nginx-example的 NGINX 部署,其中包含三个副本。我们将通过使用 kubectl 代理通过 curl 将我们的请求发送到 API 服务器来完成这个操作:

  1. 首先,让我们启动我们的代理:
kubectl proxy &

这应该会得到以下响应:

[1] 50034
AbuTalebMBP:~ mohammed$ Starting to serve on 127.0.0.1:8080

代理作为后台作业启动,并在本地主机的端口8001上进行侦听。

  1. 由于example命名空间不存在,我们应该在创建部署之前创建该命名空间。正如我们在上一节中学到的,我们需要获取应该用于创建命名空间的规范。让我们使用以下命令:
kubectl create namespace example --dry-run -o yaml

注意

对于 Kubernetes 版本 1.18+,请使用--dry-run=client

这将产生以下输出:

图 4.44:获取我们命名空间所需的规范

图 4.44:获取我们命名空间所需的规范

现在,我们已经获得了创建命名空间所需的规范。

  1. 现在,我们需要使用 curl 向 API 服务器发送请求。命名空间属于核心组,因此路径将是/api/v1/namespaces。在添加所有必需的参数后,用于创建命名空间的最终curl命令应该如下所示:
curl -X POST http://127.0.0.1:8001/api/v1/namespaces -H 'Content-    Type: application/yaml' --data "
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: null
  name: example
spec: {}
status: {}
"

注意

您可以像前面的练习中所示的那样发现任何资源所需的路径。在这个命令中,--data后面的双引号(")允许您在 Bash 中输入多行输入,这些输入由末尾的另一个双引号限定。因此,您可以在此之前复制上一步的输出。

现在,如果我们的命令一切正确,你应该会得到以下类似的响应:

图 4.45:使用 curl 发送请求创建命名空间

图 4.45:使用 curl 发送请求创建命名空间

  1. 相同的过程适用于部署。因此,首先让我们使用kubectl create命令和--dry-run=client来了解我们的 YAML 数据的外观:
kubectl create deployment nginx-example -n example --image=nginx:latest --dry-run -o yaml

注意

对于 Kubernetes 版本 1.18+,请使用--dry-run=client

您应该会得到以下响应:

图 4.46:使用 curl 发送请求创建部署

图 4.46:使用 curl 发送请求创建部署

注意

请注意,如果您使用--dry-run=client标志,则命名空间将不会显示,因为我们需要在 API 路径中指定它。

  1. 现在,创建部署的命令将与创建命名空间的命令类似。请注意,命名空间在 API 路径中指定:
curl -X POST http://127.0.0.1:8001/apis/apps/v1/namespaces/example/    deployments -H 'Content-Type: application/yaml' --data "
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: nginx-example
  name: nginx-example
spec:
  replicas: 3
  selector:
    matchLabels:
      run: nginx-example
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: nginx-example
    spec:
      containers:
      - image: nginx:latest
        name: nginx-example
        resources: {}
status: {}
"

如果一切正确,您应该从 API 服务器获得以下响应:

图 4.47:创建部署后 API 服务器的响应

图 4.47:创建部署后 API 服务器的响应

请注意,kubectl 代理进程仍在后台运行。如果您已经完成了使用 kubectl 代理与 API 服务器的交互,则可能希望停止后台运行的代理。要做到这一点,请运行fg命令将 kubectl 代理进程带到前台,然后按下Ctrl + C

因此,我们已经看到了如何使用 kubectl 代理与 API 服务器进行交互,并且通过使用 curl,我们已经能够在新的命名空间中创建了一个 NGINX 部署。

使用认证凭据直接访问 Kubernetes API

我们可以直接向 HTTP 客户端提供位置和凭据,而不是使用 kubectl 代理模式。如果您使用的客户端可能会被代理搞混,可以使用这种方法,但由于中间人攻击的风险,这种方法比使用 kubectl 代理不够安全。为了减轻这种风险,在使用这种方法时建议导入根证书并验证 API 服务器的身份。

在考虑使用凭据访问集群时,我们需要了解认证是如何配置的,以及我们的集群中启用了哪些认证插件。可以使用多种认证插件,允许以不同的方式与服务器进行认证:

  • 客户端证书

  • ServiceAccount bearer tokens

  • 认证代理

  • HTTP 基本认证

注意

请注意,上述列表仅包括一些身份验证插件。您可以在kubernetes.io/docs/reference/access-authn-authz/authentication/了解更多关于身份验证的信息。

让我们通过以下命令查看我们集群中启用了哪些身份验证插件,查看运行中的 API 服务器进程并查看传递给 API 服务器的标志:

kubectl exec -it kube-apiserver-minikube -n kube-system -- /bin/sh -c "apt update ; apt -y install procps ; ps aux | grep kube-apiserver"

此命令将首先在我们的 Minikube 服务器上安装/更新procps(用于检查进程的工具)在 API 服务器中作为一个 pod 运行。然后,它将获取进程列表,并使用kube-apiserver关键字进行过滤。您将获得一个很长的输出,但这是我们感兴趣的部分:

图 4.48:获取传递给 API 服务器的详细标志

图 4.48:获取传递给 API 服务器的详细标志

此截图中的两个标志告诉我们一些重要信息:

  • --client-ca-file=/var/lib/minikube/certs/ca.crt

  • --service-account-key-file=/var/lib/minikube/certs/sa.pub

这些标志告诉我们,我们配置了两种不同的身份验证插件——基于 X.509 客户端证书(基于第一个标志)和 ServiceAccount 令牌(基于第二个标志)。现在我们将学习如何使用这两种身份验证方法与 API 服务器进行通信。

方法 1:使用客户端证书身份验证

X.509 证书用于验证外部请求,这是我们 kubeconfig 文件中的当前配置。--client-ca-file=/var/lib/minikube/certs/ca.crt标志表示用于验证客户端证书的证书颁发机构,这将用于与 API 服务器进行身份验证。X.509 证书定义了一个主题,这是在 Kubernetes 中标识用户的内容。例如,www.google.com/使用的 SSL 的 X.509 证书包含以下信息:

Common Name = www.google.com
Organization = Google LLC
Locality = Mountain View
State = California
Country = US

当使用 X.509 证书对 Kubernetes 用户进行身份验证时,主题的“通用名称”用作用户的用户名,“组织”字段用作用户的组成员身份。

Kubernetes 对其所有 API 调用使用 TLS 协议作为安全措施。到目前为止,我们一直在使用的 HTTP 客户端 curl 可以与 TLS 一起工作。之前,kubectl 代理为我们处理了 TLS 通信,但是如果我们想直接使用 curl 进行通信,我们需要向所有 API 调用添加三个更多的细节:

  • --cert:客户端证书路径

  • --key:私钥路径

  • --cacert:证书颁发机构路径

因此,如果我们将它们组合起来,命令语法应该如下所示:

curl --cert <ClientCertificate> --key <PrivateKey> --cacert <CertificateAuthority> https://<APIServerAddress:port>/api

在本节中,我们不会创建这些证书,而是将使用在使用 Minikube 引导集群时创建的证书。所有相关信息都可以从我们的 kubeconfig 文件中获取,该文件是在初始化集群时由 Minikube 准备的。让我们看看那个文件:

kubectl config view

您应该收到以下响应:

图 4.49:kubeconfig 中的 API 服务器 IP 和身份验证证书

图 4.49:kubeconfig 中的 API 服务器 IP 和身份验证证书

最终的命令应该如下所示:您可以看到我们可以探索 API:

curl --cert ~/.minikube/client.crt --key ~/.minikube/client.key --cacert ~/.minikube/ca.crt https://192.168.99.110:8443/api

您应该收到以下响应:

图 4.50:来自 API 服务器的响应

图 4.50:来自 API 服务器的响应

因此,我们可以看到 API 服务器正在响应我们的调用。您可以使用此方法来实现我们在上一节中使用 kubectl 代理所做的一切。

方法 2:使用 ServiceAccount Bearer Token

ServiceAccount 用于对集群内运行的进程进行身份验证,例如 pod,以允许与 API 服务器进行内部通信。它们使用签名的承载JSON Web TokensJWTs)与 API 服务器进行身份验证。这些令牌存储在称为Secrets的 Kubernetes 对象中,这是一种用于存储敏感信息的实体类型,例如前述的身份验证令牌。Secret 中存储的信息是 Base64 编码的。

因此,每个 ServiceAccount 都有一个与之相关的 secret。当 pod 使用 ServiceAccount 与 API 服务器进行身份验证时,该 secret 将被挂载到 pod 上,并且 bearer token 将被解码,然后挂载到 pod 内的以下位置:/run/secrets/kubernetes.io/serviceaccount。然后,pod 中的任何进程都可以使用它来与 API 服务器进行身份验证。通过 ServiceAccounts 进行身份验证是通过内置模块实现的,该模块称为准入控制器,默认情况下已启用。

然而,仅仅 ServiceAccounts 是不够的;一旦经过身份验证,Kubernetes 还需要允许该 ServiceAccount 的任何操作(这是授权阶段)。这由基于角色的访问控制RBAC)策略来管理。在 Kubernetes 中,您可以定义某些Roles,然后使用RoleBinding将这些 Roles绑定到特定的用户或 ServiceAccounts。

Role 定义了允许的操作(API 动词)以及可以访问的 API 组和资源。RoleBinding 定义了哪个用户或 ServiceAccount 可以承担该角色。ClusterRole 类似于 Role,不同之处在于 Role 是命名空间范围的,而 ClusterRole 是集群范围的策略。RoleBinding 和 ClusterRoleBinding 也是同样的区别。

注意

您将在第十章“ConfigMaps 和 Secrets”中了解更多关于 secrets 的内容;在第十三章“Kubernetes 中的运行时和网络安全”中了解更多关于 RBAC 的内容;以及在第十六章“Kubernetes Admission Controllers”中了解有关准入控制器的更多信息。

每个命名空间都包含一个名为default的 ServiceAccount。我们可以使用以下命令来查看:

kubectl get serviceaccounts --all-namespaces

您应该看到以下响应:

图 4.51:检查每个命名空间的默认 ServiceAccounts

图 4.51:检查每个命名空间的默认 ServiceAccounts

如前所述,ServiceAccount 与包含 API 服务器的 CA 证书和 bearer token 的 secret 相关联。我们可以通过以下方式查看default命名空间中与 ServiceAccount 相关联的 secret:

kubectl get secrets

您应该得到以下响应:

NAME                 TYPE                                 DATA   AGE
default-token-wtkk5  kubernetes.io/service-account-token  3      10h

我们可以看到我们的默认命名空间中有一个名为default-token-wtkk5的 secret(其中wtkk5是一个随机字符串)。我们可以使用以下命令查看 Secret 资源的内容:

kubectl get secrets default-token-wtkk5 -o yaml

该命令将获取对象定义,如存储在 etcd 中,并以 YAML 格式显示,以便易于阅读。这将产生以下输出:

图 4.52:显示存储在密钥中的信息

图 4.52:显示存储在密钥中的信息

请注意前面的密钥中的namespacetoken和 API 服务器的 CA 证书(ca.crt)都是 Base64 编码的。您可以在 Linux 终端中使用base64 --decode进行解码,如下所示:

echo "<copied_value>" | base64 --decode

从前面的命令中复制并粘贴ca.crttoken的值。这将输出解码后的值,然后您可以将其写入文件或变量以供以后使用。但是,在此演示中,我们将展示另一种获取值的方法。

让我们窥探一下我们的一个 pod:

kubectl exec -it <pod-name> -- /bin/bash

此命令进入 pod,然后在其上运行 Bash shell。然后,一旦我们在 pod 内部运行 shell,我们可以探索 pod 中可用的各种挂载点:

df -h

这将产生类似以下的输出:

图 4.53:承载令牌的挂载点

图 4.53:承载令牌的挂载点

可以进一步探索挂载点:

ls /var/run/secrets/kubernetes.io/serviceaccount

您应该看到类似以下的输出:

ca.crt  namespace  token

如您在此处所见,挂载点包含 API 服务器 CA 证书、此密钥所属的命名空间和 JWT 承载令牌。如果您在终端上尝试这些命令,可以通过输入exit退出 pod 的 shell。

如果我们尝试从 pod 内部使用 curl 访问 API 服务器,我们需要提供 CA 路径和令牌。让我们尝试通过从 pod 内部访问 API 服务器来列出 pod 的所有内容。

我们可以创建一个新的 Deployment,并使用以下过程启动 Bash 终端:

kubectl run my-bash --rm --restart=Never -it --image=ubuntu -- bash

这可能需要几秒钟来启动,然后您将得到类似这样的响应:

If you don't see a command prompt, try pressing enter.
root@my-bash: /#

这将启动一个运行 Ubuntu 的 Deployment,并立即将我们带入 pod 并打开 Bash shell。此命令中的--rm标志将在 pod 内部的所有进程终止后删除 pod,也就是在我们使用exit命令离开 pod 后。但现在,让我们安装 curl:

apt update && apt -y install curl

这应该产生类似这样的响应:

图 4.54:安装 curl

图 4.54:安装 curl

现在我们已经安装了 curl,让我们尝试使用 curl 列出 pod,通过访问 API 路径:

curl https://kubernetes/api/v1/namespaces/$NAMESPACE/pods

您应该看到以下响应:

图 4.55:尝试在没有 TLS 的情况下访问 API

图 4.55:尝试在没有 TLS 的情况下访问 API

请注意,命令失败了。这是因为 Kubernetes 强制所有通信使用 TLS,通常拒绝不带任何身份验证令牌的不安全连接。让我们添加--insecure标志,这将允许使用 curl 进行不安全连接,并观察结果:

curl --insecure https://kubernetes/api/v1/namespaces/$NAMESPACE/pods

您应该得到以下响应:

图 4.56:匿名请求 API 服务器

图 4.56:匿名请求 API 服务器

我们可以看到,我们能够使用不安全连接到达服务器。然而,API 服务器将我们的请求视为匿名,因为我们的命令没有提供身份。

现在,为了使命令更容易,我们可以将命名空间、CA 证书(ca.crt)和令牌添加到变量中,以便 API 服务器知道生成 API 请求的 ServiceAccount 的身份:

CACERT=/run/secrets/kubernetes.io/serviceaccount/ca.crt
TOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /run/secrets/kubernetes.io/serviceaccount/namespace)

请注意,当从 Pod 内部查看时,我们可以直接使用这些值,因为它们是明文(未编码的),而不是从 Secret 中解码。现在,我们已经准备好了所有参数。在使用持有者令牌身份验证时,客户端应该在请求的标头中发送这个令牌,即授权标头。这应该看起来像这样:Authorization: Bearer <token>。由于我们已经将令牌添加到一个变量中,我们可以简单地使用它。让我们运行curl命令,看看我们是否可以使用 ServiceAccount 的身份列出 Pods:

curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/$NAMESPACE/pods

您应该得到以下响应:

图 4.57:使用默认 ServiceAccount 向 API 服务器发出的请求

](image/B14870_04_57.jpg)

图 4.57:使用默认 ServiceAccount 向 API 服务器发出的请求

请注意,我们能够到达 API 服务器,并且 API 服务器验证了"system:serviceaccount:default:default"的身份,它的表示形式是:system:<resource_type>:<namespace>:<resource_name>。然而,我们仍然得到了Forbidden错误,因为默认情况下 ServiceAccounts 没有任何权限。我们需要手动为我们的默认 ServiceAccount 分配权限,以便能够列出 Pods。这可以通过创建一个 RoleBinding 并将其链接到view ClusterRole 来完成。

打开另一个终端窗口,确保不要关闭运行my-bash pod 的终端会话(因为如果关闭它,pod 将被删除,您将丢失进度)。现在,在第二个终端会话中,您可以运行以下命令来创建一个rolebinding defaultSA-view,将view ClusterRole 附加到 ServiceAccount:

kubectl create rolebinding defaultSA-view \
  --clusterrole=view \
  --serviceaccount=default:default \
  --namespace=default

注意

查看 ClusterRole 应该已经存在于您的 Kubernetes 集群中,因为它是可供使用的默认 ClusterRoles 之一。

正如您可能还记得上一章所述,这是一种创建资源的命令性方法;您将学习如何在《第十三章 Kubernetes 中的运行时和网络安全》中为 RBAC 策略创建清单。请注意,我们必须将 ServiceAccount 指定为<namespace>:<ServiceAccountName>,并且我们有一个--namespace标志,因为 RoleBinding 只能应用于该命名空间内的 ServiceAccounts。您应该会得到以下响应:

rolebinding.rbac.authorization.k8s.io/defaultSA-view created

现在,回到我们访问my-bash pod 的终端窗口。权限设置完成后,让我们再次尝试 curl 命令:

curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/$NAMESPACE/pods

您应该会得到以下响应:

图 4.58:API 服务器的成功响应

图 4.58:API 服务器的成功响应

我们的 ServiceAccount 现在可以与 API 服务器进行身份验证,并且被授权列出默认命名空间中的 pod。

在集群外部使用 ServiceAccount 令牌也是有效的。您可能希望在长期运行的作业中使用令牌而不是证书作为身份标识,因为只要 ServiceAccount 存在,令牌就不会过期,而证书的到期日期由颁发证书的机构设置。一个例子是 CI/CD 流水线,外部服务通常使用 ServiceAccount 令牌进行身份验证。

活动 4.01:使用 ServiceAccount 身份创建部署

在这个活动中,我们将汇集本章学到的所有知识。我们将在集群上使用各种操作,并使用不同的方法访问 API 服务器。

使用 kubectl 执行以下操作:

  1. 创建一个名为activity-example的新命名空间。

  2. 创建一个名为activity-sa的新 ServiceAccount。

  3. 创建一个名为activity-sa-clusteradmin的新 RoleBinding,将activity-sa ServiceAccount 附加到cluster-admin ClusterRole(默认情况下存在)。这一步是为了确保我们的 ServiceAccount 具有与集群管理员交互所需的权限。

使用 curl 和持有者令牌进行以下操作进行身份验证:

  1. 使用activity-sa ServiceAccount 的身份创建一个新的 NGINX 部署。

  2. 列出部署中的 pod。一旦您使用 curl 检查部署,如果您已经成功完成了前面的步骤,您应该会得到一个类似于以下内容的响应:图 4.59:检查部署时的预期响应

图 4.59:检查部署时的预期响应

  1. 最后,删除所有相关资源的命名空间。当使用 curl 删除命名空间时,您应该看到一个响应,其中status字段的phase设置为terminating,如下面的屏幕截图所示:
"status": {
  "phase": "Terminating"

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

摘要

在本章中,我们更仔细地研究了 Kubernetes API 服务器,Kubernetes 如何使用 RESTful API 以及 API 资源的定义方式。我们了解到 kubectl 命令行实用程序中的所有命令都被转换为 RESTful HTTP API 调用,并发送到 API 服务器。我们了解到 API 调用经过多个阶段,包括身份验证、授权和准入控制。我们还更仔细地研究了每个阶段和一些涉及的模块。

然后,我们了解了一些 API 资源,它们是如何分类为命名空间范围或集群范围的资源,以及它们的 API 组和 API 版本。然后,我们学习了如何使用这些信息来构建与 Kubernetes API 交互的 API 路径。

我们还通过直接向 API 服务器发出 API 调用,使用 curl HTTP 客户端以不同的身份验证方法与对象进行交互,例如 ServiceAccounts 和 X.509 证书。

在接下来的几章中,我们将更仔细地检查大多数常用的 API 对象,主要关注这些对象提供的不同功能,以使我们能够在 Kubernetes 集群中部署和维护我们的应用程序。我们将从下一章开始,通过查看 Kubernetes 中部署的基本单元(pod)来开始这一系列章节。

第五章: Pod

概述

本章介绍了 Pod 的概念,并教授如何正确配置和部署它们。我们将从创建一个简单的 Pod 开始,其中运行您的应用程序容器。我们将解释 Pod 配置的不同方面意味着什么,并根据您的应用程序或用例决定使用哪种配置。您将能够为 Pod 定义资源分配要求和限制。然后,我们将看看如何调试 Pod,检查日志,并在需要时对其进行更改。本章还涵盖了一些用于管理 Pod 中故障的更多有用工具,例如活跃性和就绪性探针以及重启策略。

介绍

在上一章中,我们学习了如何使用 kubectl 与 Kubernetes API 进行交互。在本章和即将到来的章节中,我们将利用这些知识与 API 进行交互,以创建各种类型的 Kubernetes 对象。

在 Kubernetes 系统中,许多实体代表了集群的状态以及集群的工作负载。这些实体被称为 Kubernetes 对象。Kubernetes 对象描述各种事物,例如,在集群中将运行哪些容器,它们将使用什么资源,这些容器将如何相互交互,以及它们将如何暴露给外部世界。

Pod 是 Kubernetes 的基本构建块,可以描述为部署的基本单元。就像我们将进程定义为执行中的程序一样,我们可以将 Pod 定义为 Kubernetes 世界中正在运行的进程。Pod 是 Kubernetes 中最小的复制单元。一个 Pod 可以有任意数量的容器在其中运行。Pod 基本上是围绕在节点上运行的容器的包装器。使用 Pod 而不是单独的容器有一些好处。例如,Pod 中的容器具有共享卷、Linux 命名空间和 cgroups。每个 Pod 都有唯一的 IP 地址,端口空间由该 Pod 中的所有容器共享。这意味着 Pod 内部的不同容器可以使用本地主机上的相应端口相互通信。

理想情况下,我们应该只在希望它们在 Kubernetes 集群中被管理和定位在一起时才在一个 pod 中使用多个容器。例如,我们可能有一个容器运行我们的应用程序,另一个容器从应用程序容器中获取日志并将其转发到一些中央存储。在这种情况下,我们希望我们的两个容器保持在一起,共享相同的 IP,以便它们可以通过 localhost 进行通信,并共享相同的存储,以便第二个容器可以读取我们的应用程序容器生成的日志。

在本章中,我们将介绍 pod 是什么,它是如何工作的,以及如何定义其 pod 规范,描述 pod 的状态。我们将经历 pod 生命周期的不同阶段,并学习如何使用健康检查或探针来控制 pod。让我们开始学习如何配置一个 pod。

Pod 配置

为了成功配置一个 pod,我们必须首先能够阅读和理解一个 pod 配置文件。以下是一个示例 pod 配置文件:

apiVersion: v1
kind: Pod
metadata:
  name: pod-name
spec:
  containers:
  - name: container1-name
    image: container1-image
  - name: container2-name
    image: container2-image

我们可以将 pod 的配置分解为四个主要组件:

  • apiVersion:我们将要使用的 Kubernetes API 的版本。

  • kind:我们要创建的 Kubernetes 对象的类型,在这种情况下是Pod

  • metadata:唯一标识我们正在创建的对象的元数据或信息。

  • spec:我们的 pod 的规范,如容器名称、镜像名称、卷和资源请求。

apiVersionkindmetadata适用于所有类型的 Kubernetes 对象,并且是必需的字段。spec也是一个必需的字段;但是,它的布局对于不同类型的对象是不同的。

以下练习演示了如何使用这样一个 pod 配置文件来创建一个简单的 pod。

练习 5.01:创建一个带有单个容器的 Pod

在这个练习中,我们的目标是创建我们的第一个简单的 pod,其中运行一个单一的容器。要完成这个练习,请执行以下步骤:

  1. 创建一个名为single-container-pod.yaml的文件,其中包含以下内容:
apiVersion: v1
kind: Pod
metadata:
  name: first-pod
spec:
  containers:
  - name: my-first-container
    image: nginx
  1. 在终端中运行以下命令以创建具有上述配置的 pod:
kubectl create -f single-container-pod.yaml

您应该看到以下响应:

pod/first-pod created

输出表明已创建了 pod。

  1. 通过使用以下命令获取所有 pod 的列表来验证是否已创建 pod:
kubectl get pods

您应该看到以下响应:

NAME         READY       STATUS       RESTARTS      AGE
first-pod    1/1         Running      0             5m44s
  1. 现在我们已经创建了我们的第一个 pod,让我们更详细地了解一下。为了做到这一点,我们可以使用以下命令在终端中描述我们刚刚创建的 pod:
kubectl describe pod first-pod

您应该看到以下输出:

图 5.1:描述第一个 pod

图 5.1:描述第一个 pod

输出显示了我们刚刚创建的 pod 的各种细节。在接下来的部分中,我们将通过前面输出的突出部分来了解更多关于正在运行的 pod 的信息。

名称

该字段说明了 pod 的名称,有时也被称为 pod ID。在特定的命名空间中,pod 名称是唯一的。Pod 名称最多可以有 253 个字符长。在 pod 名称中允许的字符是数字(0-9)、小写字母(a-z)、连字符(-)和点(.)。

考虑输出中显示的第二行图 5.1

Name: first-pod

这与我们在 YAML 配置中提到的是一样的。

命名空间

Kubernetes 支持命名空间,在同一物理集群中创建多个虚拟集群。如果我们想要为在同一集群上工作的不同团队提供单独的环境,我们可能需要使用命名空间。命名空间还有助于限定对象名称。例如,在同一命名空间中不能有两个具有相同名称的 pod。但是,在两个不同的命名空间中可以有两个具有相同名称的 pod。现在,考虑输出中显示的第二行图 5.1

Namespace: default

我们可以通过为特定的 kubectl 命令传递--namespace参数来临时更改请求的命名空间,或者我们可以更新 kubectl 配置以更改所有后续 kubectl 命令的命名空间。要创建一个新的命名空间,我们可以使用以下命令:

kubectl create namespaces <namespace-name>

有两种方法可以在不同的命名空间中创建 pod-通过使用 CLI 命令,或者通过在 pod 配置中指定命名空间。以下练习演示了如何在不同的命名空间中创建 pod,以获得前面提到的命名空间的好处。

练习 5.02:通过在 CLI 中指定命名空间来创建不同命名空间中的 Pod

在这个练习中,我们将在一个不同于default的命名空间中创建一个 pod。我们将使用相同的 pod 配置从练习 5.01使用单个容器创建一个 Pod中,通过在命令参数中指定命名空间来完成这个操作。按照以下步骤完成这个练习:

  1. 运行以下命令来查看我们 Kubernetes 集群中所有可用的命名空间:
kubectl get namespaces

你应该看到以下命名空间列表:

NAME               STATUS       AGE
default            Active       16d
kube-node-lease    Active       16d
kube-public        Active       16d
kube-system        Active       16d

输出显示了我们 Kubernetes 集群中的所有命名空间。default命名空间就像字面意思一样,是所有没有任何命名空间创建的 Kubernetes 对象的默认命名空间。

  1. 运行以下命令,使用single-container-pod.yaml的 Pod 配置创建一个不同命名空间中的 Pod:
kubectl --namespace kube-public create -f single-container-pod.yaml

你应该看到以下响应:

pod/first-pod created

注意

如果在特定命名空间中创建了一个 Pod,只能通过切换到该命名空间来查看它。

  1. 验证 Pod 是否在kube-public命名空间中创建:
kubectl --namespace kube-public get pods

你应该看到以下响应:

NAME            READY       STATUS      RESTARTS     AGE
first-pod       1/1         Running     0            8s

这里的输出显示我们已经成功在kube-public命名空间中创建了 Pod。

下一个练习演示了如何基于 YAML 文件在不同的命名空间中创建 Pod。

练习 5.03:通过在 Pod 配置 YAML 文件中指定命名空间来创建不同命名空间中的 Pod

在这个练习中,我们将在 YAML 配置文件中添加一行,以指定使用指定命名空间创建的所有 Pod。

  1. 运行以下命令来查看我们 Kubernetes 集群中所有可用的命名空间:
kubectl get namespaces

你应该看到以下命名空间列表:

NAME               STATUS       AGE
default            Active       16d
kube-node-lease    Active       16d
kube-public        Active       16d
kube-system        Active       16d
  1. 接下来,创建一个名为single-container-pod-with-namespace.yaml的文件,其中包含以下配置:
apiVersion: v1
kind: Pod
metadata:
  name: first-pod-with-namespace
  namespace: kube-public
spec:
  containers:
  - name: my-first-container
    image: nginx
  1. 运行以下命令,使用single-container-pod-with-namespace.yaml的 Pod 配置创建一个 Pod:
kubectl create -f single-container-pod-with-namespace.yaml

你应该看到以下响应:

pod/first-pod-with-namespace created
  1. 验证 Pod 是否在kube-public命名空间中创建:
kubectl --namespace kube-public get pods

你应该看到以下 Pod 列表:

NAME                     READY     STATUS      RESTARTS   AGE
first-pod                 1/1      Running     0          5m2s
first-pod-with-namespace  1/1      Running     0          46s

输出显示我们创建的新 Pod 占据了kube-public命名空间。使用single-container-pod-with-namespace.yaml的 Pod 配置创建的任何其他 Pod 都将占据相同的命名空间。

在接下来的练习中,我们将更改默认的 kubectl 命名空间,以便所有没有定义命名空间的 Pod 都使用我们新定义的命名空间,而不是default

练习 5.04:更改所有后续 kubectl 命令的命名空间

在这个练习中,我们将把所有后续的 kubectl 命令的命名空间从default改为kube-public

  1. 运行以下命令来查看我们 Kubernetes 集群中所有可用的命名空间:
kubectl get namespaces

你应该看到以下命名空间列表:

NAME               STATUS       AGE
default            Active       16d
kube-node-lease    Active       16d
kube-public        Active       16d
kube-system        Active       16d
  1. 运行以下命令,通过修改当前上下文来更改所有后续请求的命名空间:
kubectl config set-context $(kubectl config current-context) --namespace kube-public

您应该看到以下的响应:

Context "minikube" modified.
  1. 运行以下命令来列出kube-public命名空间中的所有 pod,而不使用namespace参数:
kubectl get pods

您应该看到以下的 pod 列表:

NAME                     READY     STATUS      RESTARTS   AGE
first-pod                 1/1      Running     0          48m
first-pod-with-namespace  1/1      Running     0          44m

输出显示,前面的命令列出了我们在kube-public命名空间中创建的所有 pod。我们在练习 5.01中看到,使用kubectl get pods命令可以显示默认命名空间中的 pod。但在这里,我们得到的是kube-public命名空间的结果。

  1. 在这一步中,我们将撤消更改,以便不影响本章中即将进行的练习。我们将再次将默认命名空间更改为default,以避免任何混淆:
kubectl config set-context $(kubectl config current-context) --namespace default

您应该看到以下的响应:

Context "minikube" modified.

在这个练习中,我们已经学会了如何更改和重置上下文的默认命名空间。

节点

正如您在之前的章节中学到的,节点是在我们的集群中运行的各种机器。这个字段反映了这个 pod 在 Kubernetes 集群中运行的节点。知道一个 pod 在哪个节点上运行可以帮助我们调试该 pod 的问题。观察图 5.1中显示的输出的第六行:

Node: minikube/10.0.2.15

我们可以通过运行以下命令列出 Kubernetes 集群中的所有节点:

kubectl get nodes

您应该看到以下的响应:

NAME         STATUS      ROLES       AGE         VERSION
minikube     Ready       <none>      16d         v1.14.3

在这种情况下,我们的集群中只有一个节点,因为我们在这些练习中使用的是 Minikube:

apiVersion: v1
kind: Pod
metadata:
  name: firstpod
spec:
  nodeName: my-favorite-node # run this pod on a specific node
  containers:
  - name: my-first-pod
    image: nginx

如果我们的集群中有多个节点,我们可以通过在配置中添加以下nodeName字段来配置我们的 pod 在特定节点上运行,就像在上一个规范的第六行中看到的那样。

注意

在生产环境中,通常不使用nodeName来指定某个 pod 在所需节点上运行。在下一章中,我们将学习nodeSelector,这是一种更好的控制 pod 分配到哪个节点的方法。

状态

这个字段告诉我们 pod 的状态,以便我们可以采取适当的行动,比如根据需要启动或停止一个 pod。虽然这个演示展示了获取 pod 状态的一种方式,但在实际操作中,您可能希望根据 pod 状态自动执行操作。考虑图 5.1中显示的输出的第十行:

Status: Running

这表明 pod 的当前状态是Running。这个字段反映了 pod 处于生命周期的哪个阶段。我们将在本章的下一节中讨论 pod 生命周期的各个阶段。

容器

在本章的前面,我们看到我们可以在一个 pod 中捆绑各种容器。这个字段列出了我们在这个 pod 中创建的所有容器。考虑图 5.1中从第 12 行开始的输出字段:

图 5.2:使用描述命令的容器字段

图 5.2:使用描述命令的容器字段

在这种情况下,我们只有一个。我们可以看到容器的名称和镜像与我们在 YAML 配置中指定的相同。以下是我们可以设置的其他字段的列表:

  • Image:Docker 镜像的名称

  • Args:容器入口点的参数

  • Command:容器启动后要运行的命令

  • Ports:要从容器中暴露的端口列表

  • Env:要在容器中设置的环境变量列表

  • resources:容器的资源需求

在下面的练习中,我们将使用一个简单的命令创建一个容器。

练习 5.05:使用 CLI 命令创建运行容器的 Pod

在这个练习中,我们将创建一个将通过运行命令来运行容器的 pod。

  1. 首先,让我们创建一个名为pod-with-container-command.yaml的文件,其中包含以下的 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: command-pod
spec:
  containers:
  - name: container-with-command
    image: ubuntu
    command:
    - /bin/bash
    - -ec
    - while :; do echo '.'; sleep 5; done
  1. 运行以下命令,使用pod-with-container-command.yaml文件中定义的配置来创建 pod:
kubectl create -f pod-with-container-command.yaml

您应该会看到以下的响应:

pod/command-pod created

我们在上一步创建的 YAML 文件指示 pod 启动一个带有 Ubuntu 镜像的容器,并运行以下命令:

/bin/bash -ec "while :; do echo '.'; sleep 5; done"

这个命令应该每 5 秒打印一个点(.)字符到新的一行。

  1. 让我们检查这个 pod 的日志,以验证它是否按预期运行。要检查 pod 的日志,我们可以使用kubectl logs命令:
kubectl logs command-pod -f

您应该会看到以下的响应:

图 5.3:命令-pod 的日志

图 5.3:命令-pod 的日志

在日志中,我们定期更新,每 5 秒打印一个点(.)字符到新的一行。因此,我们成功创建了期望的容器。

注意

-f标志是为了跟踪容器上的日志。也就是说,日志会实时更新。如果我们跳过该标志,我们将看到日志而不是跟踪它们。

在下一个练习中,我们将运行一个打开端口的容器,这是您经常需要做的事情,以使容器可以被集群或互联网访问。

练习 5.06:创建运行公开端口的容器的 Pod

在这个练习中,我们将创建一个运行容器的 pod,该容器将公开一个可以从 pod 外部访问的端口。

  1. 首先,让我们创建一个名为pod-with-exposed-port.yaml的文件,其中包含以下 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: port-exposed-pod
spec:
  containers:
    - name: container-with-exposed-port
      image: nginx
      ports:
        - containerPort: 80
  1. 运行以下命令,使用pod-with-exposed-port.yaml文件创建 pod:
kubectl create -f pod-with-exposed-port.yaml

您应该会看到以下响应:

pod/port-exposed-pod created

这个 pod 应该创建一个容器并公开其端口80。我们已经配置了 pod 以运行具有nginx镜像的容器,这是一个流行的 Web 服务器。

  1. 接下来,我们将把 pod 的端口80转发到 localhost:
sudo kubectl port-forward pod/port-exposed-pod 80

您应该会看到以下响应:

Forwarding from 127.0.0.1:80 -> 80
Forwarding from [::1] -> 80

这将把 pod 的端口80公开到 localhost 的端口80

注意:

我们需要在一个终端中保持此命令运行。

  1. 现在,我们可以在浏览器的地址栏中输入http://localhosthttp://127.0.0.1

  2. 或者,我们可以运行以下命令并查看响应中默认索引页面的 HTML 源代码:

curl 127.0.0.1

您应该会看到以下输出:

图 5.4:使用 curl 获取 HTML 源代码

图 5.4:使用 curl 获取 HTML 源代码

  1. 接下来,让我们通过使用kubectl logs命令来检查日志,验证 pod 是否实际接收到请求:
kubectl logs port-exposed-pod

您应该会看到以下响应:

图 5.5:检查 nginx pod 的日志

图 5.5:检查 nginx pod 的日志

日志显示,我们运行nginx镜像的容器正在接收我们发送到 localhost 的 HTTP 请求,并如预期地做出响应。

我们还可以为我们的容器定义最小和最大资源分配。这对于管理部署使用的资源非常有用。这可以通过 YAML 配置文件中的以下两个字段来实现:

  • limits:描述此容器允许的资源的最大量。

  • requests:描述此容器所需资源的最小量。

我们可以使用这些字段来定义容器的最小和最大内存和 CPU 资源。CPU 资源以 CPU 单位来衡量。1 个 CPU 单位表示容器可以访问 1 个逻辑 CPU 核心。

在下一个练习中,我们将创建一个具有定义资源需求的容器。

练习 5.07:创建运行具有资源需求的 Pod

在这个练习中,我们将创建一个带有资源需求的容器的 pod。首先,让我们看看如何配置容器的资源需求:

  1. 创建一个名为pod-with-resource-requirements.yaml的文件,其中包含指定内存和 CPU 资源的limitsrequests的 pod 配置,如下所示:
apiVersion: v1
kind: Pod
metadata:
  name: resource-requirements-pod
spec:
  containers:
    - name: container-with-resource-requirements
      image: nginx
      resources:
        limits:
          memory: "128M"
          cpu: "1"
        requests:
          memory: "64M"
          cpu: "0.5"

在这个 YAML 文件中,我们定义了容器的最小内存需求为 64MB,容器可以占用的最大内存为 128MB。如果容器尝试分配超过 128MB 的内存,它将被杀死,并显示OOMKilled状态。

CPU 的最小需求为 0.5(也可以理解为 500 毫 CPU,可以写为500m而不是0.5),容器只能使用最多 1 个 CPU 单位。

  1. 接下来,我们将使用kubectl create命令创建使用此 YAML 配置的 pod:
kubectl create -f pod-with-resource-requirements.yaml

您应该看到以下响应:

pod/resource-requirements-pod created
  1. 接下来,让我们确保 pod 以正确的资源需求创建。使用describe命令检查 pod 定义:
kubectl describe pod resource-requirements-pod

您应该看到以下输出:

图 5.6:描述资源需求-pod

图 5.6:描述资源需求-pod

输出中突出显示的字段显示,pod 已被分配了我们在 YAML 文件中声明的limitsrequests部分。

如果我们为我们的 pod 定义不切实际的资源需求会发生什么?让我们在以下练习中探讨这个问题。

练习 5.08:创建一个资源请求无法满足任何节点的 pod

在这个练习中,我们将创建一个具有对集群中的节点来说太大的资源请求的 pod,并查看会发生什么。

  1. 创建一个名为pod-with-huge-resource-requirements.yaml的文件,其中包含以下 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: huge-resource-requirements-pod
spec:
  containers:
    - name: container-with-huge-resource-requirements
      image: nginx
      resources:
        limits:
          memory: "128G"
          cpu: "1000"
        requests:
          memory: "64G"
          cpu: "500"

在这个 YAML 文件中,我们定义了内存的最小需求为 64GB,CPU 核心为 500 个。您运行此练习的机器可能不满足这些要求。

  1. 接下来,我们将使用kubectl create命令创建使用此 YAML 配置的 pod:
kubectl create -f pod-with-huge-resource-requirements.yaml

您应该看到以下响应:

pod/huge-resource-requirements-pod created
  1. 现在,让我们看看我们的 pod 发生了什么。使用kubectl get命令获取其状态:
kubectl get pod huge-resource-requirements-pod

您应该看到以下响应:

图 5.7:获取巨大资源需求-pod 的状态

图 5.7:获取巨大资源需求-pod 的状态

我们看到 pod 已经处于“挂起”状态将近一分钟。这很不寻常!

  1. 让我们深入挖掘,并使用以下命令描述 pod:
kubectl describe pod huge-resource-requirements-pod

您应该看到以下输出:

图 5.8:描述巨大资源需求的 pod

图 5.8:描述巨大资源需求的 pod

让我们专注于输出的最后一行。我们可以清楚地看到警告,指出 Kubernetes 控制器找不到满足 pod 的 CPU 和内存需求的任何节点。因此,pod 调度失败了。

总之,pod 调度是基于资源需求的。一个 pod 只会被调度到满足其所有资源需求的节点上。如果我们不指定资源(内存或 CPU)限制,那么 pod 可以使用的资源数量就没有上限。

这带来了一个风险,即一个糟糕的 pod 消耗了太多的 CPU 或分配了太多的内存,影响了在同一命名空间/集群中运行的其他 pod。因此,在生产环境中,向 pod 配置添加资源请求和限制是一个好主意。

正如本章前面提到的,一个 pod 可以运行多个容器。在接下来的练习中,我们将创建一个具有多个容器的 pod。

练习 5.09:创建一个包含多个容器的 pod

在这个练习中,我们将创建一个具有多个容器的 pod。为此,我们可以使用在上一节中使用的配置,唯一的区别是“容器”字段现在将包含多个容器规范。按照以下步骤完成练习:

  1. 创建一个名为multiple-container-pod.yaml的文件,其中包含以下 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  containers:
    - name: first-container
      image: nginx
    - name: second-container
      image: ubuntu
      command:
        - /bin/bash
        - -ec
        - while :; do echo '.'; sleep 5; done
  1. 接下来,我们将使用kubectl create命令创建一个使用前面的 YAML 配置的 pod:
kubectl create -f multiple-container-pod.yaml

您应该看到以下响应:

pod/multi-container-pod created
  1. 接下来,我们将描述 pod 并查看它正在运行的容器:
kubectl describe pod multi-container-pod

您应该看到以下输出:

图 5.9:描述多容器 pod

图 5.9:描述多容器 pod

从前面的输出可以看出,我们有两个容器在一个单独的 pod 中运行。现在,我们需要确保我们可以访问任一容器的日志。

我们可以指定容器名称来获取运行在 pod 中的特定容器的日志,如下所示:

kubectl logs <pod-name> <container-name>

例如,要查看第二个容器的日志,该容器每 5 秒在新行上打印出点,使用此命令:

kubectl logs multi-container-pod second-container -f

您应该看到以下响应:

图 5.10:多容器 pod 中 second-container 的日志

图 5.10:多容器 pod 中 second-container 的日志

我们在这里看到的输出与练习 5.05使用 CLI 命令创建运行容器的 pod类似,因为我们基本上使用了与那里定义的类似的容器。

因此,我们已经创建了一个具有多个容器的 pod,并访问了所需容器的日志。

Pod 的生命周期

现在我们知道如何运行一个 pod 以及如何为我们的用例配置它,在本节中,我们将讨论 pod 的生命周期,以了解它的工作原理。

Pod 的阶段

每个 pod 都有一个 pod 状态,告诉我们 pod 处于生命周期的哪个阶段。我们可以通过运行kubectl get命令来查看 pod 状态:

kubectl get pod

您将看到以下响应:

NAME         READY       STATUS        RESTARTS      AGE
first-pod    1/1         Running       0             5m44s

对于我们的第一个名为first-pod的 pod,我们看到 pod 处于运行状态。

让我们看看 pod 在其生命周期中可能具有的不同状态:

  • 挂起:这意味着 pod 已经提交到集群,但控制器尚未创建所有的容器。它可能正在下载镜像或等待 pod 被调度到集群节点之一。

  • 运行:这个状态意味着 pod 已经分配给集群节点,并且至少一个容器正在运行或正在启动过程中。

  • 成功:这个状态意味着 pod 已经运行,并且所有的容器都已成功终止。

  • 失败:这个状态意味着 pod 已经运行,至少一个容器以非零退出代码终止,也就是说,它未能执行其命令。

  • 未知:这意味着无法找到 pod 的状态。这可能是因为控制器无法连接到分配给 pod 的节点。

注意

get pod命令无法获取被驱逐或删除的 pod。为此,您可以使用--show-all标志,但自 Kubernetes v1.15 以来已被弃用。

探针/健康检查

探针是可以配置为检查运行在 pod 中的容器的健康状况的健康检查。探针可以用来确定容器是否正在运行或准备好接收请求。探针可能返回以下结果:

  • 成功:容器通过了健康检查。

  • 失败:容器未通过健康检查。

  • 未知:健康检查由于未知原因失败。

探针的类型

我们可以使用以下类型的探针。

活跃探针

这是一个用于确定特定容器是否正在运行的健康检查。如果容器未通过活跃探针,控制器将根据为 Pod 配置的重启策略尝试在同一节点上重新启动 Pod。

当特定检查失败时,指定活跃探针并希望容器在失败时被终止并重新启动是一个好主意。

就绪探针

这是一个用于确定特定容器是否准备好接收请求的健康检查。我们如何定义容器的就绪状态在很大程度上取决于容器内运行的应用程序。

例如,对于一个提供 Web 应用程序的容器,就绪可能意味着容器已经加载了所有静态资产,与数据库建立了连接,启动了 Web 服务器,并在主机上打开了一个特定的端口来开始提供请求。另一方面,对于提供一些数据的容器,就绪探针只有在它从磁盘加载了所有数据并准备好开始为该数据提供请求时才能成功。

如果一个容器未通过就绪探针,Kubernetes 控制器将确保该 Pod 不会收到任何请求。如果容器指定了就绪探针,其默认状态将是“失败”,直到就绪探针成功。只有在就绪探针返回“成功”状态后,容器才会开始接收请求。如果没有配置就绪探针,容器将在启动后立即开始接收请求。

探针的配置

我们可以使用一堆通用字段来配置探针:

图 5.11:显示探针配置字段的表格

图 5.11:显示探针配置字段的表格

探针的实现

探针(活跃或就绪)可以通过向容器传递命令、让其获取一些资源,或尝试连接到它来实现,正如我们将在本节中看到的那样。我们可以在同一个容器中使用不同的实现来进行活跃和就绪探针。

命令探针

在探针的命令实现中,控制器将使容器执行指定的命令以对容器执行探针。对于此实现,我们使用command字段。该字段指定要执行的命令以对容器执行探针。它可以是字符串或数组。

以下示例显示了如何在容器规范中使用活跃性和就绪性探针配置:

livenessProbe:
  exec:
    command:
    - cat
    - /tmp/health
  initialDelaySeconds:
  periodSeconds: 15
  failureThreshold: 3
readinessProbe:
  exec:
    command:
    - cat
    - /tmp/health
  initialDelaySeconds:
  periodSeconds: 15

HTTP 请求探针

在这种类型的探针中,控制器将向给定地址(主机和端口)发送 GET HTTP 请求以对容器执行探针。可以设置要在探针请求中发送的自定义 HTTP 标头。

我们可以设置以下字段来配置 HTTP 请求探针:

  • 主机:将发出请求的主机名。默认为 pod IP 地址。

  • 路径:发出请求的路径。

  • 端口:要发出请求的端口的名称或编号。

  • httpHeaders:要在请求中设置的自定义标头。

  • 方案:在发出请求时使用的方案。默认值为 HTTP。

以下是一个用于活跃性和就绪性的 HTTP 请求探针的示例:

livenessProbe:
  httpGet:
    path: /health-check
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 20
readinessProbe:
  httpGet:
    path: /health-check
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

TCP 套接字探针

在这种探针的实现中,控制器将尝试在给定的主机和指定的端口号上建立连接。我们可以使用以下两个字段进行此探针:

  • 主机:将建立连接的主机名。默认为 pod IP 地址。

  • 端口:要连接的端口的名称或编号。

以下是一个 TCP 套接字探针的示例:

livenessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 20
readinessProbe:
  tcpSocket:
    port:8080
  initialDelaySeconds: 5
  periodSeconds: 10

重启策略

我们可以在 pod 规范中指定restartPolicy以指示控制器重新启动 pod 所需的条件。 restartPolicy的默认值为Always。它可以采用以下值:

  • Always:当 pod 终止时始终重新启动 pod。

  • OnFailure:仅在 pod 以失败终止时重新启动 pod。

  • 永不:在终止后永不重新启动 pod。

如果我们希望 pod 在出现问题或变得不健康时崩溃并重新启动,我们应该将重启策略设置为AlwaysOnFailure

在下面的练习中,我们将创建一个带有命令实现的活跃探针。

练习 5.10:创建一个运行带有活跃探针和无重启策略的容器的 pod

在这个练习中,我们将创建一个带有活跃探针和无重启策略的 pod。未为 pod 指定重启策略意味着将使用Always的默认策略。

  1. 创建liveness-probe.yaml,使用以下 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: liveness-probe
spec:
  containers:
    - name: ubuntu-container
      image: ubuntu
      command:
        - /bin/bash
        - -ec
        - touch /tmp/live; sleep 30; rm /tmp/live; sleep 600
      livenessProbe:
        exec:
          command:
            - cat
            - /tmp/live
        initialDelaySeconds: 5
        periodSeconds: 5

这个 pod 配置意味着将会创建一个来自 Ubuntu 镜像的容器,并且一旦它启动,将运行以下命令:

/bin/bash -ec "touch /tmp/live; sleep 30; rm /tmp/live; sleep 600"

前面的命令在路径/tmp/live创建一个空文件,休眠 30 秒,删除/tmp/live文件,然后休眠 10 分钟后成功终止。

接下来,我们有一个活跃探针,它每 5 秒执行一次以下命令,初始延迟为 5 秒:

cat /tmp/live
  1. 运行以下命令使用liveness-probe.yaml创建 pod:
kubectl create -f liveness-probe.yaml
  1. 当容器启动时,活跃探针将成功,因为命令将成功执行。现在,让我们等待至少 30 秒,然后运行describe命令:
kubectl describe pod liveness-probe

你应该看到以下输出:

图 5.12:描述活跃探针:第一次失败

图 5.12:描述活跃探针:第一次失败

在最后一行,也就是突出显示的内容中,我们可以看到活跃探针已经第一次失败。

  1. 让我们再等待几秒,直到探针失败三次,然后再次运行相同的命令:
kubectl describe pod liveness-probe

你应该看到以下输出:

图 5.13:描述活跃探针:三次失败后

图 5.13:描述活跃探针:三次失败后

输出中最后两行突出显示的内容告诉我们,活跃探针已经失败了三次。现在,该 pod 将被杀死并重新启动。

  1. 接下来,我们将使用以下命令验证该 pod 至少已经重新启动了一次:
kubectl get pod liveness-probe

你应该看到以下响应:

NAME             READY     STATUS      RESTARTS    AGE
liveness-probe   1/1       Running     1           89s

这个输出显示,该 pod 在活跃探针失败后已经重新启动。

现在让我们看看如果将重启策略设置为Never会发生什么。

练习 5.11:创建一个运行有活跃探针和重启策略的容器的 Pod

在这个练习中,我们将使用上一个练习中相同的 pod 配置,唯一的区别是restartPolicy字段将被设置为Never。按照以下步骤完成活动:

  1. 创建liveness-probe-with-restart-policy.yaml,使用以下 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: liveness-probe-never-restart
spec:
  restartPolicy: Never
  containers:
    - name: ubuntu-container
      image: ubuntu
      command:
        - /bin/bash
        - -ec
        - touch /tmp/ready; sleep 30; rm /tmp/ready; sleep 600
      livenessProbe:
        exec:
          command:
            - cat
            - /tmp/ready
        initialDelaySeconds: 5
        periodSeconds: 5
  1. 运行以下命令使用liveness-probe.yaml创建 pod:
kubectl create -f liveness-probe-with-restart-policy.yaml

你应该看到以下响应:

pod/liveness-probe-never-restart created
  1. 让我们等待大约一分钟,然后运行describe命令:
kubectl describe pod liveness-probe-never-restart

你应该看到以下输出:

图 5.14:描述活跃探针-永不重启

图 5.14:描述活跃探针-永不重启

正如我们所看到的,在最后两行中,控制器只会杀死容器,而不会尝试重新启动它,遵守了 pod 规范中指定的重启策略。

在接下来的练习中,我们将看一下就绪探针的实现。

练习 5.12:创建一个运行带有就绪探针的容器的 Pod

在这个练习中,我们将创建一个带有就绪探针的 pod。

  1. 创建一个名为readiness-probe.yaml的文件,其中包含以下的 pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: readiness-probe
spec:
  containers:
    - name: ubuntu-container
      image: ubuntu
      command:
        - /bin/bash
        - -ec
        - sleep 30; touch /tmp/ready; sleep 600
      readinessProbe:
        exec:
          command:
            - cat
            - /tmp/ready
        initialDelaySeconds: 10
        periodSeconds: 5

前面的 pod 配置指定将从 Ubuntu 镜像创建一个容器,并且一旦启动,将运行以下命令:

/bin/bash -ec "sleep 30; touch /tmp/ready; sleep 600"

前面的命令休眠 30 秒,在/tmp/ready创建一个空文件,然后再休眠 10 分钟后以成功终止。

接下来,我们有一个就绪探针,它每 5 秒执行一次以下命令,初始延迟为 10 秒:

cat /tmp/ready
  1. 运行以下命令使用readiness-probe.yaml创建 pod:
kubectl create -f readiness-probe.yaml

你应该看到以下的响应:

pod/readiness-probe created

当容器启动时,就绪探针的默认值将是Failure。它将在第一次执行探测之前等待 10 秒。

  1. 让我们来检查一下 pod 的状态:
kubectl get pod readiness-probe

你应该看到以下的响应:

NAME              READY       STATUS       RESTARTS       AGE
readiness-probe   0/1         Running      0              8s

我们可以看到该 pod 还没有准备好。

  1. 现在,让我们尝试使用describe命令找到有关该 pod 的更多信息。如果在容器启动后等待超过 10 秒,我们将看到就绪探针开始失败:
kubectl describe pod readiness-probe

你应该看到以下的输出:

图 5.15:描述就绪探针

图 5.15:描述就绪探针

该输出告诉我们,就绪探针已经失败了一次。如果我们等一会儿,再次运行该命令,我们将看到就绪探针一直失败,直到容器的启动时间已经过去 30 秒。之后,就绪探针将开始成功,因为在/tmp/ready将创建一个文件。

  1. 让我们再次检查一下 pod 的状态:
kubectl get pod readiness-probe

你应该看到以下的响应:

NAME              READY    STATUS      RESTARTS     AGE
readiness-probe   1/1      Running     0            66s

我们可以看到探针已经成功,pod 现在处于Ready状态。

在使用探针时的最佳实践

错误使用探针将无法帮助您实现预期的目的,甚至可能破坏 pod。遵循这些实践以正确使用探针:

  • 对于活跃探针,initialDelaySeconds应该比应用程序启动所需的时间大得多。否则,容器很可能会陷入重启循环,因为它一直无法通过活跃探针,因此一直被控制器重新启动。

  • 对于就绪探针,initialDelaySeconds可以很小,因为我们希望在容器准备就绪后尽快启用对 pod 的流量,并且在启动过程中更频繁地轮询容器在大多数情况下不会造成任何伤害。

  • 对于就绪探针,我们应该小心设置failureThreshold,以确保我们的就绪探针在临时中断或系统问题的情况下不会过早放弃。

活动 5.01:在 pod 中部署应用程序

想象一下,你正在与一组开发人员合作,他们构建了一个很棒的应用程序,希望你将其部署到一个 pod 中。该应用程序有一个启动过程,大约需要 20 秒来加载所有所需的资源。一旦应用程序启动,它就准备好开始接收请求。如果出现应用程序崩溃的情况,你也希望 pod 能够重新启动。他们让你使用一个配置来创建 pod,以最好的方式满足应用程序开发人员的需求。

我们提供了一个预制的应用程序镜像,以模拟上述应用程序的行为。你可以在 pod 规范中使用这行来获取它:

image: packtworkshops/the-kubernetes-workshop:custom-application-for-    pods-chapter

注意

理想情况下,你希望在不同的命名空间中创建这个 pod,以使其与你在练习期间创建的其他内容分开。所以,可以随意创建一个命名空间,并在该命名空间中创建 pod。

以下是完成此活动的高级步骤:

  1. 为你的 pod 创建一个新的命名空间。

  2. 创建一个适合应用程序需求的 pod 配置。确保你使用适当的命名空间、重启策略、就绪和活跃探针,以及应用程序开发人员提供的容器镜像。

  3. 使用你刚刚创建的配置创建一个 pod。

  4. 确保 pod 按照要求运行。

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

总结

在本章中,我们已经探讨了 Pod 配置的各种组件,并学会了在何时使用何种组件。现在我们应该能够创建一个 Pod,并根据应用程序的需求选择 Pod 配置中各个字段的正确值。这种能力使我们能够利用我们对这个基本的重要构建块的深刻理解,并将其扩展到开发一个可靠部署的完整应用程序。

在下一章中,我们将讨论如何向 Pod 添加标签和任意元数据,并使用它们来识别或搜索 Pod。这将帮助我们组织我们的 Pod,并在需要时选择它们的子集。

第六章: 标签和注释

概述

元数据对于任何组织都非常有用,并且在管理集群中潜在的成千上万的资源时非常有用。本章将教你如何向你的 Pod 或任何其他 Kubernetes 对象添加元数据。您将了解标签和注释的概念。我们还将解释它们的用例,以便您可以决定是否对特定用例使用标签或注释。您将利用标签通过使用标签选择器来组织对象,以选择或过滤组织好的对象集。您还将使用注释向对象添加非结构化的元数据信息。

介绍

在上一章中,我们创建了各种类型的 Pod,并管理了它们的生命周期。一旦我们开始使用不同的 Pod,理想情况下,我们希望根据某些属性对它们进行组织、分组和过滤。为了做到这一点,我们需要向我们的 Pod 添加一些信息,以便以后可以使用这些信息来对它们进行组织。我们已经看到了namenamespace字段作为 Pod 的元数据的用法。除了这些字段,我们还可以向 Pod 添加键值对,以便作为标签和注释添加额外的信息。

在本章中,我们将为这些 Pod 分配元数据,以便通过基于某些元数据的查询来识别 Pod,然后添加额外的非结构化元数据。我们将详细介绍标签和注释,并检查它们之间的区别。我们将同时使用标签和注释,并看看何时使用其中之一。

标签

标签是包含与 Kubernetes 对象相关的可识别信息的元数据。这些基本上是可以附加到对象(如 Pod)的键值对。每个键对于一个对象必须是唯一的。标签包含对用户有意义的信息。标签可以在创建时附加到 Pod,并且在运行时也可以添加或修改。以下是一个 YAML 文件中标签的示例:

metadata:
  labels:
    key1: value1
    key2: value2

标签的约束

如前所述,标签是键值对。标签键和值应遵循某些规则。存在这些约束是因为这样可以通过内部优化的数据结构和算法更快地评估使用标签的查询。Kubernetes 在内部使用优化的数据结构来维护标签与相应对象的映射,以使这些查询更快。

标签键

以下是标签键的示例:

label_prefix.com/worker-node-1

正如我们所看到的,标签键由两部分组成:标签前缀和标签名称。让我们更仔细地看看这两部分:

  • 标签前缀:标签前缀是可选的,必须是 DNS 子域。它不能长于 253 个字符,也不能包含空格。标签前缀总是跟着一个斜杠(/)。如果不使用前缀,则假定标签键是私有的。一些前缀,如kubernetes.io/k8s.io/,专门用于 Kubernetes 核心系统。

在我们的示例中,label_prefix.com/是该标签键的前缀。

  • 标签名称:标签名称是必需的,最长可以达到 63 个字符。标签名称只能以字母数字字符(a-z,A-Z,0-9)开头和结尾;但是,它可以包含破折号(-),下划线(_),点(.)和中间的字母数字字符。标签名称不能包含空格或换行符。

label_prefix.com/worker-node-1的示例中,标签键的名称是worker-node-1

标签值

标签值最长可以达到 63 个字符。与标签名称类似,标签值也应该以字母数字字符开头和结尾。但是,它们可以包含破折号(-),下划线(_),点(.)和中间的字母数字字符。标签值不能包含空格或换行符。

为什么我们需要标签?

标签通常用于组织对象的子集。然后可以根据这些标签对这些对象进行过滤。使用标签,您还可以在选定的节点上运行特定的 Pods。这两种情况在以下部分中进行了详细解释。

按组织/团队/项目组织 Pods

标签的一个用例可能是在您公司中基于团队或组织使用标签。假设您的组织有几个团队在不同的项目上工作。您可以使不同的团队只列出他们的 Pods,甚至那些特定于某些项目的 Pods。此外,如果您是基础设施服务提供商,您可以使用组织标签仅对与特定客户组织相关的 Pods 应用更改。对于这种用例,您可以使用标签键,如teamorgproject。以下是这种用例的一个示例labels部分:

metadata:
  labels:
    environment: production
    team: devops-infra
    project: test-k8s-infra

在特定节点上运行选择的 Pods

另一个有用的场景可能是当您希望将您的 pod 分配给具有特定硬件或其他属性的特定节点时。这可以通过向具有特殊硬件或其他属性的节点添加标签来实现。我们可以使用nodeSelector将 pod 分配给具有特定标签的任何节点。考虑以下示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-node-selector
spec:
  containers:
  - name: first-container
    image: nginx
  nodeSelector:
    region: east-us
    disktype: ssd

上述 pod 模板可用于确保 pod 将被分配到位于east-us地区并具有ssd存储的节点。此检查基于添加到节点的标签。因此,我们需要确保适当的regiondisktype标签已分配给所有适用的节点。

注意

请注意,要在nodeSelector部分中使用的确切节点标签将由云基础设施提供商提供,并且标签键和值可能会更改。本示例中使用的值仅用于演示用例。

在接下来的练习中,我们将向您展示如何创建带有标签的 pod,向正在运行的 pod 添加标签,并修改和/或删除正在运行的 pod 的现有标签。

练习 6.01:创建带标签的 Pod

在这个练习中,我们的目标是创建一个带有一些标签的 pod。为了成功完成这个练习,执行以下步骤:

  1. 创建一个名为pod-with-labels.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-labels
  labels:
    app: nginx
    foo: bar
spec:
  containers:
  - name: first-container
    image: nginx

如前面的片段所示,我们已添加了appfoo标签,并分别将它们分配为nginxbar的值。现在,我们需要创建一个带有这些标签的 pod,并验证这些标签是否实际包含在 pod 中,这将是接下来几个步骤的重点。

  1. 在终端中运行以下命令以使用上述配置创建 pod:
kubectl create -f pod-with-labels.yaml

您应该看到以下响应:

pod/pod-with-labels created
  1. 使用kubectl get命令验证 pod 是否已创建:
kubectl get pod pod-with-labels

以下输出表明已创建了 pod:

NAME              READY      STATUS        RESTARTS     AGE
pod-with-labels   1/1        Running       0            4m4s
  1. 使用kubectl describe命令验证实际是否已将labels元数据添加到 pod 中:
kubectl describe pod pod-with-labels

这应该导致以下输出:

图 6.1:描述带标签的 pod

图 6.1:描述带标签的 pod

输出显示了与 pod 相关的各种细节(正如我们在上一章中看到的那样)。在这种情况下,我们将关注输出的突出部分,显示了所需的标签app=nginxfoo=bar实际上已添加到了 pod 中。请注意,在这个练习中,我们在创建 pod 时添加了标签。但是,当 pod 已经在运行时,如何给 pod 添加标签呢?下一个练习将回答这个问题。

练习 6.02:向运行中的 pod 添加标签

在这个练习中,我们的目标是创建一个没有标签的 pod,然后在 pod 运行时添加标签。为了成功完成这个练习,执行以下步骤:

  1. 创建一个名为pod-without-initial-labels.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-without-initial-labels
spec:
  containers:
  - name: first-container
    image: nginx

请注意,我们尚未向我们的 pod 添加任何标签。

  1. 在终端中运行以下命令,创建具有前面步骤中提到的配置的 pod:
kubectl create -f pod-without-initial-labels.yaml

你应该看到以下响应:

pod/pod-without-initial-labels created
  1. 使用kubectl get命令验证 pod 是否已创建:
kubectl get pod pod-without-initial-labels

以下输出表明已创建了 pod:

图 6.2:检查没有初始标签的 pod 的状态

图 6.2:检查没有初始标签的 pod 的状态

  1. 使用kubectl describe命令检查labels元数据是否实际添加到了 pod 中:
kubectl describe pod pod-without-initial-labels

你应该看到以下输出:

图 6.3:描述没有初始标签的 pod

图 6.3:描述没有初始标签的 pod

在输出的突出部分中,我们可以注意到Labels字段为空。因此,我们可以验证,默认情况下,没有标签添加到 pod 中。在接下来的几步中,我们将添加一个标签,然后再次运行 pod,以验证标签是否实际包含在 pod 中。

  1. 使用kubectl label命令添加标签如下:
kubectl label pod pod-without-initial-labels app=nginx

你应该看到以下响应:

pod/pod-without-initial-labels labeled

输出显示pod-without-initial-labels pod 已被标记。

  1. 使用kubectl describe命令验证上一步中是否实际添加了标签:
kubectl describe pod pod-without-initial-labels

你应该看到以下输出:

图 6.4:验证已添加 app=nginx 标签

图 6.4:验证已添加 app=nginx 标签

我们可以观察到输出的突出显示部分实际上添加了app=nginx标签到 pod 中。在前面的情况下,我们只添加了一个标签。但是,您可以向 pod 添加多个标签,就像接下来要做的那样。

  1. 接下来,让我们在同一命令中添加多个标签。我们可以通过以key=value格式传递多个标签,用空格分隔来实现这一点:
kubectl label pod pod-without-initial-labels foo=bar foo2=baz

您应该看到以下响应:

pod/pod-without-initial-labels labeled
  1. 使用kubectl describe命令验证两个标签是否已添加到 pod 中:
kubectl describe pod pod-without-initial-labels

您应该看到以下输出:

图 6.5:验证新的两个标签也已添加

图 6.5:验证新的两个标签也已添加

在输出的突出显示部分,我们可以看到两个新标签foo=barfoo2=baz也已添加到 pod 中。

在接下来的练习中,我们将看到如何删除和修改已经运行的 pod 的现有标签。

练习 6.03:修改和/或删除运行中 pod 的现有标签

在这个练习中,我们的目标是创建一个带有一些标签的 pod,并在 pod 运行时修改和删除这些标签。为了成功完成这个练习,请执行以下步骤:

  1. 创建一个名为pod-with-some-labels.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-some-labels
  labels:
    app: nginx
spec:
  containers:
  - name: first-container
    image: nginx

如您在 pod 定义中所见,我们只添加了一个标签app,其值为nginx

  1. 在终端中运行以下命令以创建具有上述配置的 pod:
kubectl create -f pod-with-some-labels.yaml

您应该看到以下响应:

pod/pod-with-some-labels created
  1. 使用kubectl get命令验证 pod 是否已创建:
kubectl get pod pod-with-some-labels

以下输出表明已创建了 pod:

图 6.6:检查带有一些标签的 pod 的状态

图 6.6:检查带有一些标签的 pod 的状态

  1. 使用kubectl describe命令验证标签是否按照 pod 配置添加:
kubectl describe pod pod-with-some-labels

您应该看到以下输出:

图 6.7:验证标签是否已添加到带有一些标签的 pod

图 6.7:验证标签是否已添加到带有一些标签的 pod

一旦我们确定app=nginx标签存在,我们将在下一步中修改此标签。

  1. 使用kubectl label命令将app=nginx标签修改为app=nginx-application
kubectl label --overwrite pod pod-with-some-labels app=nginx-application

您应该看到以下响应:

pod/pod-with-some-labels labeled
  1. 使用kubectl describe命令验证标签的值是否从nginx修改为nginx-application
kubectl describe pod pod-with-some-labels

以下截图显示了此命令的输出:

图 6.8:验证标签值是否已修改

图 6.8:验证标签值是否已修改

如输出所示,我们可以看到具有app键的标签具有新值nginx-application

  1. 使用kubectl label命令删除具有app键的标签:
kubectl label pod pod-with-some-labels app-

请注意前面命令的连字符。您应该看到以下响应:

pod/pod-with-some-labels labeled
  1. 使用kubectl describe命令验证实际上已删除具有app键的标签:
kubectl describe pod pod-with-some-labels

您应该看到以下输出:

图 6.9:验证实际上已从 pod 中删除所需的标签

图 6.9:验证实际上已从 pod 中删除所需的标签

如前面的输出所示,我们可以再次注意到具有app键的标签已被删除,因此,该 pod 现在没有标签。因此,我们已经学会了如何修改和删除运行中 pod 的现有标签。

使用标签选择器选择 Kubernetes 对象

为了根据它们的标签对各种对象进行分组,我们使用标签选择器。它允许用户识别符合某些条件的一组对象。

我们可以使用以下语法来使用kubectl get命令,并使用-l--label参数传递标签选择器:

kubectl get pods -l {label_selector}

在接下来的练习中,我们将看到如何在实际场景中使用此命令。在此之前,让我们了解在这些命令中可以使用哪些{label_selector}参数。

目前,有两种类型的标签选择器:基于相等性和基于集合。

基于相等性的选择器

基于相等性的选择器允许根据标签键和值选择 Kubernetes 对象。这些类型的选择器允许我们匹配具有给定标签键的特定标签值的所有对象。实际上,我们也有基于不等性的选择器。

总的来说,有三种运算符:===!=

前两个实际上在操作上是相同的,并表示基于相等性的操作,而第三个表示基于不等性的操作。在使用这些类型的选择器时,我们可以使用任何前述运算符指定多个条件。

例如,如果我们使用标签键如environmentteam,我们可能想使用以下选择器:

environment=production

前面的选择器匹配所有具有标签键环境和相应的production值的对象:

team!=devops-infra

前面的选择器匹配所有没有team标签键或者存在team标签键但相应的值不等于devops-infra的对象。

同样,我们也可以同时使用两个选择器,用逗号(,)分隔:

environment=production,team!=devops-infra

在上面的例子中,选择器将匹配符合两个选择器指定的条件的所有对象。逗号充当两个选择器之间的逻辑 AND(&&)运算符。现在,让我们尝试在以下练习中实现这些选择器。

练习 6.04:使用基于相等性的标签选择器选择 Pod

在这个练习中,我们的目标是创建一些具有不同标签的 Pod,然后使用基于相等性的选择器选择它们。为了成功完成这个练习,请执行以下步骤:

  1. 创建一个名为pod-frontend-production.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: frontend-production
  labels:
    environment: production
    role: frontend
spec:
  containers:
  - name: application-container
    image: nginx

如我们所见,这是具有以下两个标签的 Pod 模板:environment=productionrole=frontend

  1. 创建另一个名为pod-backend-production.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: backend-production
  labels:
    environment: production
    role: backend
spec:
  containers:
  - name: application-container
    image: nginx

这是具有以下两个标签的 Pod 模板:environment=productionrole=backend

  1. 创建另一个名为pod-frontend-staging.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: frontend-staging
  labels:
    environment: staging
    role: frontend
spec:
  containers:
  - name: application-container
    image: nginx

这是具有以下两个标签的 Pod 模板:environment=stagingrole=frontend

  1. 使用以下三个命令创建所有三个 Pod:
kubectl create -f pod-frontend-production.yaml

您应该会看到以下响应:

pod/frontend-production created

现在,运行以下命令:

kubectl create -f pod-backend-production.yaml

以下响应表示已创建 Pod:

pod/backend-production created

现在,运行以下命令:

kubectl create -f pod-frontend-staging.yaml

这应该会得到以下响应:

pod/frontend-staging created
  1. 使用--show-labels参数来验证所有三个 Pod 是否使用正确的标签创建,首先,让我们检查frontend-production Pod:
kubectl get pod frontend-production --show-labels

以下响应表示已创建frontend-production Pod:

图 6.10:验证 frontend-production Pod 的标签

图 6.10:验证 frontend-production Pod 的标签

  1. 现在,检查backend-production Pod:
kubectl get pod backend-production --show-labels

以下响应表示已创建backend-production Pod:

图 6.11:验证 backend-production Pod 的标签

图 6.11:验证 backend-production Pod 的标签

  1. 最后,检查frontend-staging Pod:
kubectl get pod frontend-staging --show-labels

以下响应表明frontend-staging pod 已被创建:

图 6.12:验证 frontend-staging pod 的标签

图 6.12:验证 frontend-staging pod 的标签

  1. 现在,我们将使用标签选择器来查看分配给生产环境的所有 pod。我们可以使用environment=production作为标签选择器与kubectl get命令一起实现:
kubectl get pods -l environment=production

在下面的输出中,我们可以看到它只显示那些具有environment键和production值的标签的 pod:

NAME                    READY      STATUS       RESTARTS    AGE
backend-production      1/1        Running      0           67m
frontend-production     1/1        Running      0           68m

您可以从图 6.10图 6.11确认,这些是具有environment=production标签的 pod。

  1. 接下来,我们将使用标签选择器来查看所有具有frontend角色和staging环境的 pod。我们可以通过使用标签选择器与kubectl get命令来实现,如下所示:
kubectl get pods -l role=frontend,environment=staging

在下面的输出中,我们可以看到它只显示那些具有staging作为环境和frontend作为角色的 pod:

NAME                    READY      STATUS       RESTARTS    AGE
frontend-staging        1/1        Running      0           72m

在这个练习中,我们已经使用标签选择器来选择特定的 pod。get命令的这种标签选择器为基于标签选择所需的一组 pod 提供了一种便捷的方式。这也代表了一个常见的场景,您可能只想对涉及生产或分段环境的 pod 或前端或后端基础设施应用一些更改。

基于集合的选择器

基于集合的选择器允许根据给定键的一组值选择 Kubernetes 对象。这种类型的选择器允许我们匹配所有具有给定标签键和给定一组值中的一个值的对象。

有三种类型的操作符:innotinexists。让我们通过一些例子来看看这些操作符的含义:

environment in (production, staging)

在前面的例子中,选择器匹配所有具有environment标签键且值为productionstaging的对象:

team notin (devops-infra)

在前面的例子中,选择器匹配所有具有team标签键且值不是devops-infra的对象。它还匹配那些没有team标签键的对象:

!critical

在前面的例子中,选择器等同于exists操作。它匹配所有没有critical标签键的对象。它根本不检查值。

注意

这两种类型的选择器也可以一起使用,正如我们将在练习 6.06使用混合标签选择器选择 Pods中观察到的那样。

让我们在接下来的练习中实现基于集合的选择器。

练习 6.05:使用基于集合的标签选择器选择 Pods

在这个练习中,我们的目标是创建一些具有不同标签的 pod,然后使用基于集合的选择器来选择它们。

注意

在这个练习中,我们假设您已经成功完成了练习 6.04使用基于相等性的标签选择器选择 Pods。我们将重用在该练习中创建的 pods。

为了成功完成这个练习,请执行以下步骤:

  1. 打开终端并验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的frontend-production pod 是否仍在运行并具有所需的标签。我们将使用kubectl get命令和--show-labels参数:
kubectl get pod frontend-production --show-labels

以下响应表明frontend-production pod 存在:

图 6.13:验证前端生产 pod 的标签

图 6.13:验证前端生产 pod 的标签

  1. 使用kubectl get命令和--show-labels参数验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的backend-production pod 是否仍在运行并具有所需的标签:
kubectl get pod backend-production --show-labels

以下响应表明backend-production pod 存在:

图 6.14:验证后端生产 pod 的标签

图 6.14:验证后端生产 pod 的标签

  1. 使用kubectl get命令和--show-labels参数验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的frontend-staging pod 是否仍在运行并具有所需的标签:
kubectl get pod frontend-staging --show-labels

以下响应表明frontend-staging pod 存在:

图 6.15:验证前端暂存 pod 的标签

](image/B14870_06_15.jpg)

图 6.15:验证前端暂存 pod 的标签

  1. 现在,我们将使用标签选择器来匹配所有环境为production,角色为frontendbackend的所有 pod。我们可以通过使用标签选择器和kubectl get命令来实现这一点,如下所示:
kubectl get pods -l 'role in (frontend, backend),environment in (production)'

您应该看到以下响应:

NAME                    READY      STATUS       RESTARTS    AGE
backend-production      1/1        Running      0           82m
frontend-production     1/1        Running      0           82m
  1. 接下来,我们将使用标签选择器来匹配所有具有environment标签且角色不是backend的 pod。我们还希望排除那些没有设置role标签的 pod:
kubectl get pods -l 'environment,role,role notin (backend)'

这应该产生以下输出:

NAME                    READY      STATUS       RESTARTS    AGE
frontend-production     1/1        Running      0           86m
frontend-staging        1/1/       Running      0           86m

在这个例子中,我们有基于集合的选择器,可以用来获取所需的 pods。我们还可以将这些与基于选择器的 pods 结合起来,正如我们将在接下来的练习中看到的。

练习 6.06:使用混合标签选择器选择 Pods

在这个练习中,我们的目标是创建一些具有不同标签的 pods,然后使用基于相等性和基于集合的选择器来选择它们。

注意

在这个练习中,我们假设您已成功完成练习 6.04使用基于相等性的标签选择器选择 Pods。我们将重用在该练习中创建的 pods。

为了成功完成这个练习,请执行以下步骤:

  1. 打开终端,验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的frontend-production pod 是否仍在运行并具有所需的标签。我们将使用kubectl get命令和--show-labels参数:
kubectl get pod frontend-production --show-labels

下面的响应表明frontend-production pod 存在:

图 6.16:验证 frontend-production pod 的标签

图 6.16:验证 frontend-production pod 的标签

  1. 使用kubectl get命令和--show-labels参数验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的backend-production pod 是否仍在运行并具有所需的标签:
kubectl get pod backend-production --show-labels

下面的响应表明backend-production pod 存在:

图 6.17:验证 backend-production pod 的标签

图 6.17:验证 backend-production pod 的标签

  1. 使用kubectl get命令和--show-labels参数验证我们在练习 6.04使用基于相等性的标签选择器选择 Pods中创建的frontend-staging pod 是否仍在运行并具有所需的标签:
kubectl get pod frontend-staging --show-labels

下面的响应表明frontend-staging pod 存在:

图 6.18:验证 frontend-staging pod 的标签

图 6.18:验证 frontend-staging pod 的标签

  1. 现在,我们将使用标签选择器来匹配所有具有frontend角色且环境为productionstagingdev之一的 pod:
kubectl get pods -l 'role=frontend,environment in (production,staging,dev)'

这个命令应该给出以下 pod 列表:

NAME                    READY      STATUS       RESTARTS    AGE
frontend-production     1/1        Running      0           95m
frontend-staging        1/1        Running      0           95m

在输出中,我们只能看到具有frontend角色的那些 pod,而environment可以是给定值中的任何一个。因此,我们已经看到可以根据需要使用不同类型的选择器的混合。

注释

正如我们之前所看到的,标签用于添加我们稍后可以使用来过滤或选择对象的标识元数据。然而,标签在值方面有一定的限制,比如我们可以存储的字符数限制为 63 个字符,并且在开头和结尾只能使用字母数字字符。另一方面,注释在可以存储的数据类型方面有更少的限制。然而,我们不能使用注释来过滤或选择对象。

注释也是可以用于存储与 Kubernetes 对象相关的非结构化信息的键值对。以下是 YAML 文件中注释的示例:

metadata:
  annotations:
    key1: value1
    key2: value2

注释的约束

如前所述,注释是键值对,就像标签一样。然而,注释的规则比标签键和值的规则更宽松。更宽松的约束是因为注释不能用于过滤或选择对象。这是因为注释的键值对没有存储在一个高效的查找数据结构中。因此,在这里有更少的限制。

注释键

与标签键类似,注释键也有两部分:前缀和名称。注释前缀和名称的约束与标签前缀和名称的约束相同。

以下是注释键可能出现的示例:

annotation_prefix.com/worker-node-identifier

注释值

在注释值中,没有任何数据类型的限制。

注释的用例

注释通常用于添加元数据,这些元数据不会用于过滤或选择对象。它用于添加将由用户或工具用于获取有关 Kubernetes 对象的更主观信息的元数据。让我们看一些使用注释可能有用的场景:

  • 注释可用于添加时间戳、提交 SHA、问题跟踪器链接,或者组织中负责特定对象的用户的名称/信息。在这种情况下,我们可以根据我们的用例使用以下类型的元数据:
metadata:
  annotations:
    timestamp: 123456789
    commit-SHA: d6s9shb82365yg4ygd782889us28377gf6
    JIRA-issue: "https://your-jira-link.com/issue/ABC-1234"
    owner: "https://internal-link.to.website/username"
  • 注释还可以用于添加有关客户端库或工具的信息。我们可以添加诸如库的名称、使用的版本和公共文档链接等信息。这些信息以后可以用于调试我们应用程序中的问题:
metadata:
  annotations:
    node-version: 13.1.0
    node-documentation: "https://nodejs.org/en/docs/"
  • 我们还可以使用注释来存储先前部署的 pod 配置。这在弄清当前修订之前部署的配置以及发生了什么变化方面非常有帮助:
metadata:
  annotations:
    previous-configuration: "{ some json containing the       previously deployed configuration of the object }"
  • 注释还可以用于存储在部署过程中对我们的应用程序有帮助的配置或检查点。

我们将在下一个练习中学习如何向 pod 添加注释。

练习 6.07:添加注释以帮助应用程序调试

在这个练习中,我们将向我们的 pod 添加一些任意的元数据。为了成功完成这个练习,请执行以下步骤:

  1. 创建一个名为pod-with-annotations.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-annotations
  annotations:
    commit-SHA: d6s9shb82365yg4ygd782889us28377gf6
    JIRA-issue: "https://your-jira-link.com/issue/ABC-1234"
    timestamp: "123456789"
    owner: "https://internal-link.to.website/username"
spec:
  containers:
  - name: application-container
    image: nginx

在 pod 定义的突出显示部分显示了我们添加的注释。

  1. 在终端中运行以下命令,使用kubectl create命令创建 pod:
kubectl create -f pod-with-annotations.yaml

您应该收到以下响应:

pod/pod-with-annotations created
  1. 在终端中运行以下命令以验证 pod 是否按预期创建:
kubectl get pod pod-with-annotations

您应该看到以下 pod 列表:

NAME                    READY      STATUS       RESTARTS    AGE
pod-with-annotations    1/1        Running      0           29s
  1. 在终端中运行以下命令以验证创建的 pod 是否具有所需的注释:
kubectl describe pod pod-with-annotations

您应该看到此命令的以下输出:

图 6.19:验证带注释的 pod 的注释

图 6.19:验证带注释的 pod 的注释

正如我们在上述输出的突出显示部分中所看到的,所需的元数据已作为注释添加到了 pod 中。现在,这些数据可以被任何了解使用的部署工具或客户端使用。

使用注释

在上一个练习中,我们创建了一个带有注释的 pod。与标签类似,我们可以向运行中的 pod 添加注释,并修改/删除运行中的 pod 的注释。这可以通过运行类似于标签的命令来实现。以下列表向您展示了可以对注释执行的各种操作以及相关命令:

  • 因此,我们可以通过使用以下命令向运行中的 pod 添加注释:
kubectl annotate pod <pod_name> <annotation_key>=<annotation_label>

在上述命令中,我们可以添加多个注释,类似于练习 6.02步骤 7中添加多个标签。

  • 我们还可以修改(覆盖)注释,如下所示:
kubectl annotate --overwrite pod <pod_name> <annotation_key>=<annotation_label>
  • 最后,我们可以使用以下命令删除注释:
kubectl annotate pod <pod_name> <annotation_key>-

注意前面命令末尾的连字符。现在我们已经了解了标签和注释以及我们可以使用它们的各种方式,让我们在以下活动中将所有这些内容汇总起来。

活动 6.01:使用标签/注释创建 Pod 并根据给定标准对它们进行分组

考虑到您正在支持名为product-developmentinfra-libraries的两个团队。这两个团队都有一些应用程序 pod 用于不同的环境(生产或分级)。如果情况确实如此,团队还希望将其 pod 标记为关键。

简而言之,您需要根据以下元数据要求创建三个 pod:

  • 一个在生产环境中运行并由product-development团队拥有的arbitrary-product-application pod。这需要标记为非关键 pod。

  • 一个在生产环境中运行并由infra-libraries团队拥有的infra-libraries-application pod。这需要标记为关键 pod。

  • 一个在分级环境中运行并由infra-libraries团队拥有的infra-libraries-application-staging pod。由于它在分级环境中运行,因此不需要指示该 pod 的重要性。

除此之外,两个团队还想要添加另一个元数据 - "team-link",他们希望在其中存储团队联系信息的内部链接。

一旦创建了所有三个 pod,您应该能够执行以下任务:

  1. 将所有在生产环境中运行且关键的 pod 分组。

  2. 将所有环境中不关键的 pod 分组。

注意

理想情况下,您希望创建此 pod 位于不同的命名空间中,以使其与您在练习期间创建的其他内容分开。因此,可以随意创建一个命名空间并在该命名空间中创建 pod。

执行此活动的高级步骤如下:

  1. 为此活动创建一个命名空间。

  2. 编写所有三个 pod 的 pod 配置。确保在标签和注释中正确添加所有请求的元数据。

  3. 使用上一步中编写的配置创建所有三个 pod。

  4. 确保所有三个 pod 都在运行并具有所有请求的元数据。

  5. 将所有在生产环境中运行且关键的 pod 分组。

  6. 将所有环境中不重要的 pod 分组。

对于第一个任务,您的目标是在完成活动后获得infra-libraries-application pod,如下所示:

NAME                         READY   STATUS       RESTARTS    AGE
infra-libraries-application  1/1     Running      0           12m

对于第二个任务,您的目标是在完成活动后获得arbitrary-product-applicationinfra-libraries-application-staging,如下所示:

NAME                                 READY  STATUS    RESTARTS   AGE
arbitrary-product-application        1/1    Running   0          14m
infra-libraries-application-staging  1/1    Running   0          14m

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD

总结

在本章中,我们描述了标签和注释,并使用它们添加元数据信息,这些信息可以是可识别的信息,可用于过滤或选择对象,也可以是用户或工具用于获取有关应用程序状态的更多上下文的不可识别信息。更具体地说,我们还使用标签和注释组织了诸如 pod 之类的对象。这些是重要的技能,将帮助您更有效地管理 Kubernetes 对象。

在接下来的章节中,当我们熟悉更多的 Kubernetes 对象,比如部署和服务时,我们将看到标签和标签选择器在组织用于部署或发现的 pod 时的进一步应用。

第七章: Kubernetes 控制器

概述

本章介绍了 Kubernetes 控制器的概念,并解释了如何使用它们来创建复制的部署。我们将描述不同类型的控制器的使用,如 ReplicaSets、部署、DaemonSets、StatefulSets 和 Jobs。您将学习如何为特定用例选择合适的控制器。通过实际操作练习,我们将指导您如何使用所需的配置来部署应用程序的多个 Pod 副本。您还将学习如何使用各种命令来管理它们。

介绍

在之前的章节中,我们创建了不同的 Pod,手动管理它们的生命周期,并为它们添加了元数据(标签或注释)来帮助组织和识别各种 Pod。在本章中,我们将看看一些 Kubernetes 对象,帮助您以声明方式管理多个副本 Pod。

在生产环境中部署应用程序时,有几个原因会让您希望拥有多个 Pod 的副本。拥有多个副本可以确保在一个或多个 Pod 失败的情况下,您的应用程序仍然可以正常工作。除了处理故障之外,复制还允许您在不同的副本之间平衡负载,以便一个 Pod 不会因为大量请求而过载,从而使您能够轻松地处理比单个 Pod 能够处理的更高流量。

Kubernetes 支持不同的控制器,您可以用于复制,如 ReplicaSets、部署、DaemonSets、StatefulSets 和 Jobs。控制器是一个对象,确保您的应用程序在其整个运行时处于所需状态。每个控制器都对特定的用例有用。在本章中,我们将逐个探索一些最常用的控制器,并了解如何以及何时在实际场景中使用它们。

ReplicaSets

如前所述,拥有应用程序的多个副本可以确保即使一些副本失败,应用程序仍然可用。这也使我们能够轻松地扩展我们的应用程序以平衡负载以提供更多的流量。例如,如果我们正在构建一个向用户公开的 Web 应用程序,我们希望至少有两个应用程序副本,以防其中一个失败或意外死机。我们还希望失败的副本能够自行恢复。除此之外,如果我们的流量开始增长,我们希望增加运行我们的应用程序的 Pod(副本)的数量。ReplicaSet 是一个 Kubernetes 控制器,它在任何给定时间保持一定数量的 Pod 运行。

ReplicaSet 充当 Kubernetes 集群中不同节点上多个 Pod 的监督者。ReplicaSet 将终止或启动新的 Pod 以匹配 ReplicaSet 模板中指定的配置。因此,即使您的应用程序只需要一个 Pod,使用 ReplicaSet 也是一个好主意。即使有人删除了唯一运行的 Pod,ReplicaSet 也会确保创建一个新的 Pod 来替代它,从而确保始终有一个 Pod 在运行。

ReplicaSet 可以用来可靠地运行单个 Pod,也可以用来运行多个相同 Pod 的实例。

ReplicaSet 配置

让我们首先看一个 ReplicaSet 配置的示例,然后我们将介绍不同字段的含义:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      environment: production
  template:
    metadata:
      labels:
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx

与 Pod 配置一样,ReplicaSet 还需要字段,如apiVersionkindmetadata。对于 ReplicaSet,API 版本apps/v1是当前版本,kind字段将始终为ReplicaSet。到目前为止,在 Pod 配置中看到的一个不同的字段是spec

现在,我们将看到在spec字段中需要指定哪些信息。

副本

spec下的replicas字段指定 ReplicaSet 应保持同时运行多少个 Pod。您可以在前面的示例中看到以下值:

replicas: 2

ReplicaSet 将创建或删除 Pod 以匹配这个数字。如果未指定,默认值为1

Pod 模板

template字段中,我们将指定我们想要使用这个 ReplicaSet 运行的 Pod 的模板。这个 Pod 模板将与我们在前两章中使用的 Pod 模板完全相同。通常情况下,我们可以向 Pod 添加标签和注释的元数据。当有需要时,ReplicaSet 将使用这个 Pod 模板来创建新的 Pod。前面示例中的以下部分包括模板:

template:
  metadata:
    labels:
      environment: production
  spec:
    containers:
    - name: nginx-container
      image: nginx

Pod 选择器

这是一个非常重要的部分。在spec下的selector字段中,我们可以指定标签选择器,ReplicaSet 将使用这些选择器来识别要管理的 Pod:

selector:
  matchLabels:
    environment: production

前面的示例确保我们的控制器只管理具有environment: production标签的 Pod。

现在让我们继续创建我们的第一个 ReplicaSet。

练习 7.01:使用 nginx 容器创建一个简单的 ReplicaSet

在这个练习中,我们将创建一个简单的 ReplicaSet,并检查由它创建的 Pod。要成功完成这个练习,请执行以下步骤:

  1. 创建一个名为replicaset-nginx.yaml的文件,内容如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      environment: production
  template:
    metadata:
      labels:
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx

如您在配置的突出显示部分所见,我们有三个字段:replicasselectortemplate。我们将副本的数量设置为2。Pod 选择器已经设置,这样 ReplicaSet 将管理具有environment: production标签的 Pod。Pod 模板具有我们在前几章中使用的简单 Pod 配置。我们已确保 Pod 标签选择器与模板中的 Pod 标签完全匹配。

  1. 运行以下命令,使用前面的配置创建 ReplicaSet:
kubectl create -f replicaset-nginx.yaml

您应该看到以下响应:

replicaset.apps/nginx-replicaset created
  1. 使用kubectl get命令验证 ReplicaSet 是否已创建:
kubectl get rs nginx-replicaset

请注意,在所有 kubectl 命令中,rsreplicaset的缩写形式。

您应该看到以下响应:

NAME               DESIRED    CURRENT    READY    AGE
nginx-replicaset   2          2          2        30s

如您所见,我们有一个具有两个期望副本的 ReplicaSet,就像我们在步骤 1中在replicaset-nginx.yaml中定义的那样。

  1. 使用以下命令验证 Pod 是否实际创建:
kubectl get pods

您应该得到以下响应:

NAME                     READY    STATUS   RESTARTS   AGE
nginx-replicaset-b8fwt   1/1      Running  0          51s
nginx-replicaset-k4h9r   1/1      Running  0          51s

我们可以看到,由 ReplicaSet 创建的 Pod 的名称以 ReplicaSet 的名称作为前缀。

  1. 现在我们已经创建了我们的第一个 ReplicaSet,让我们更详细地查看它,以了解在创建过程中实际发生了什么。为了做到这一点,我们可以在终端中使用以下命令描述我们刚刚创建的 ReplicaSet:
kubectl describe rs nginx-replicaset

您应该看到类似以下的输出:

图 7.1:描述 nginx-replicaset

图 7.1:描述 nginx-replicaset

  1. 接下来,我们将检查由此副本集创建的 Pod,并验证它们是否已按正确的配置创建。运行以下命令以获取正在运行的 Pod 的列表:
kubectl get pods

您应该看到以下响应:

NAME                     READY    STATUS   RESTARTS   AGE
nginx-replicaset-b8fwt   1/1      Running  0          38m
nginx-replicaset-k4h9r   1/1      Running  0          38m
  1. 运行以下命令来描述其中一个 Pod,复制其名称:
kubectl describe pod <pod_name>

您应该看到类似以下的输出:

图 7.2:列出 Pods

图 7.2:列出 Pods

在上述输出的突出部分中,我们可以清楚地看到该 Pod 具有environment=production标签,并由ReplicaSet/nginx-replicaset控制。

因此,在本练习中我们创建了一个简单的副本集。在接下来的子主题中,我们将逐步了解正在运行的副本集的突出部分。

副本集上的标签

考虑来自图 7.1中显示的输出的以下行:

Labels:       app=nginx

它显示了所期望的,副本集是通过一个名为app的标签键和值为nginx来创建的。

副本集的选择器

现在,考虑来自图 7.1中显示的输出的以下行:

Selector:     environment=production

这表明副本集配置了一个environment=production的 Pod 选择器。这意味着这个副本集将尝试获取具有这个标签的 Pod。

副本

考虑来自图 7.1中显示的输出的以下行:

Replicas:     2 current / 2 desired

我们可以看到副本集对于 Pod 的期望数量为2,并且还显示当前有两个副本存在。

Pod 的状态

虽然副本字段只显示当前存在的 Pod 数量,Pod 的状态显示了这些 Pod 的实际状态:

Pods Status:  2 Running / 0 Waiting / 0 Succeeded / 0 Failed

我们可以看到当前有两个 Pod 在此副本集下运行。

Pod 模板

现在,让我们考虑图 7.1中显示的输出的Pod 模板部分。我们可以看到 Pod 模板与配置中描述的相同。

事件

图 7.1中显示的输出的最后一部分中,我们可以看到有两个事件,这表示创建了两个 Pod 以达到副本集中两个 Pod 的期望数量。

在上一个练习中,我们创建了一个副本集来维护一定数量的运行副本。现在,让我们考虑一种情况,即某些节点或 Pod 由于某种原因失败。我们将看到副本集在这种情况下的行为。

练习 7.02:删除 ReplicaSet 管理的 Pod

在这个练习中,我们将删除 ReplicaSet 管理的一个 Pod,以查看它的响应。这样,我们将模拟在 ReplicaSet 运行时单个或多个 Pod 失败的情况:

注意

在这个练习中,我们将假设您已经成功完成了上一个练习,因为我们将重用在那个练习中创建的 ReplicaSet。

  1. 验证 ReplicaSet 创建的 Pod 是否仍在运行:
kubectl get pods

您应该看到类似于以下响应:

NAME                     READY    STATUS   RESTARTS   AGE
nginx-replicaset-9tgb9   1/1      Running  0          103s
nginx-replicaset-zdjb5   1/1      Running  0          103s
  1. 使用以下命令删除第一个 Pod,以模拟运行时的 Pod 故障:
kubectl delete pod <pod_name>

您应该看到类似于以下的响应:

pod "nginx-replicaset-9tgb9" deleted
  1. 描述 ReplicaSet 并检查事件:
kubectl describe rs nginx-replicaset

您应该看到类似于以下的输出:

图 7.3:描述 ReplicaSet

图 7.3:描述 ReplicaSet

在前面的输出中,我们可以看到在删除一个 Pod 之后,ReplicaSet 会使用 ReplicaSet 配置中“模板”部分的 Pod 配置创建一个新的 Pod。即使我们删除了 ReplicaSet 管理的所有 Pod,它们也会被重新创建。因此,要永久删除所有 Pod 并避免 Pod 的重新创建,我们需要删除 ReplicaSet 本身。

  1. 运行以下命令删除 ReplicaSet:
kubectl delete rs nginx-replicaset

您应该看到以下响应:

replicaset.apps "nginx-replicaset" deleted

如前面的输出所示,nginx-replicaset ReplicaSet 已被删除。

  1. 运行以下命令验证 ReplicaSet 管理的 Pod 也已被删除:
kubectl get pods

您应该得到以下响应:

No resources found in default namespace

从这个输出中可以看出,我们可以验证 Pod 已被删除。

假设您已经部署了一个用于测试的单个 Pod。现在,它已经准备好上线。您将从开发到生产应用所需的标签更改,并且现在您希望使用 ReplicaSet 来控制这一切。我们将在下面的练习中看到如何做到这一点。

练习 7.03:创建一个已存在匹配 Pod 的 ReplicaSet

在这个练习中,我们将创建一个与 ReplicaSet 中 Pod 模板匹配的 Pod,然后创建 ReplicaSet。我们的目标是证明新创建的 ReplicaSet 将获取现有的 Pod,并开始管理它,就好像它自己创建了该 Pod 一样。

为了成功完成这个练习,请执行以下步骤:

  1. 创建一个名为pod-matching-replicaset.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-matching-replicaset
  labels:
    environment: production
spec:
  containers:
  - name: first-container
    image: nginx
  1. 运行以下命令使用上述配置来创建 Pod:
kubectl create -f pod-matching-replicaset.yaml

你应该看到以下响应:

pod/pod-matching-replicaset created
  1. 创建一个名为 replicaset-nginx.yaml 的文件,内容如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      environment: production
  template:
    metadata:
      labels:
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx
  1. 运行以下命令使用上述配置来创建 ReplicaSet:
kubectl create -f replicaset-nginx.yaml

你应该看到类似以下的响应:

replicaset.apps/nginx-replicaset created

这个输出表明 Pod 已经被创建。

  1. 运行以下命令来检查 ReplicaSet 的状态:
kubectl get rs nginx-replicaset

你应该得到以下响应:

NAME               DESIRED   CURRENT   READY   AGE
nginx-replicaset   2         2         2       2

我们可以看到目前有两个由 ReplicaSet 管理的 Pod,符合预期。

  1. 接下来,让我们使用以下命令来检查正在运行的 Pod:
kubectl get pods

你应该看到类似以下的输出:

NAME                     READY      STATUS    RESTARTS    AGE
nginx-replicaset-4dr7s   1/1        Running   0           28s
pod-matching-replicaset  1/1        Running   0           81s

在这个输出中,我们可以看到手动创建的名为 pod-matching-replicaset 的 Pod 仍在运行,并且 nginx-replicaset ReplicaSet 只创建了一个新的 Pod。

  1. 接下来,我们将使用 kubectl describe 命令来检查名为 pod-matching-replicaset 的 Pod 是否被 ReplicaSet 管理:
kubectl describe pod pod-matching-replicaset

你应该看到类似以下的输出:

图 7.4:描述 Pod

图 7.4:描述 Pod

在截断输出的突出部分中,我们可以看到即使这个 Pod 在 ReplicaSet 事件存在之前是手动创建的,现在这个 Pod 也是由 ReplicaSet 自己管理的。

  1. 接下来,我们将描述 ReplicaSet,以查看它触发了多少个 Pod 的创建:
kubectl describe rs nginx-replicaset

你应该看到类似以下的输出:

图 7.5:描述 ReplicaSet

图 7.5:描述 ReplicaSet

  1. 运行以下命令来删除 ReplicaSet 进行清理:
kubectl delete rs nginx-replicaset

你应该看到以下响应:

replicaset.apps "nginx-replicaset" deleted

因此,我们可以看到 ReplicaSet 能够获取现有的 Pod,只要它们符合标签选择器的条件。在存在更多匹配的 Pod 的情况下,ReplicaSet 将终止一些 Pod 以维持正在运行的 Pod 的总数。

另一个常见的操作是在之前创建的 ReplicaSet 上进行水平扩展。假设你创建了一个具有一定数量副本的 ReplicaSet,后来你需要有更多或更少的副本来管理增加或减少的需求。让我们看看如何在下一个练习中扩展副本的数量。

练习 7.04:在创建后扩展 ReplicaSet

在这个练习中,我们将创建一个具有两个副本的 ReplicaSet,然后修改它以增加副本的数量。然后,我们将减少副本的数量。

为了成功完成这个练习,请执行以下步骤:

  1. 创建一个名为replicaset-nginx.yaml的文件,内容如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      environment: production
  template:
    metadata:
      labels:
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx
  1. 运行以下命令来使用kubectl apply命令创建 ReplicaSet,如前面的代码所述:
kubectl apply -f replicaset-nginx.yaml

你应该会得到以下响应:

replicaset.apps/nginx-replicaset created
  1. 运行以下命令来检查所有现有的 Pod:
kubectl get pods

你应该会得到类似以下的响应:

NAME                     READY    STATUS    RESTARTS    AGE
nginx-replicaset-99tj7   1/1      Running   0           23s
nginx-replicaset-s4stt   1/1      Running   0           23s

我们可以看到有两个由副本集创建的 Pod。

  1. 运行以下命令来扩展 ReplicaSet 的副本数量到4
kubectl scale --replicas=4 rs nginx-replicaset

你应该会看到以下响应:

replicaset.apps/nginx-replicaset scaled
  1. 运行以下命令来检查所有正在运行的 Pod:
kubectl get pods

你应该会看到类似以下的输出:

NAME                     READY    STATUS    RESTARTS    AGE
nginx-replicaset-99tj7   1/1      Running   0           75s
nginx-replicaset-klh6k   1/1      Running   0           21s
nginx-replicaset-lrqsk   1/1      Running   0           21s
nginx-replicaset-s4stt   1/1      Running   0           75s

我们可以看到现在总共有四个 Pod。在我们应用新配置后,ReplicaSet 创建了两个新的 Pod。

  1. 接下来,让我们运行以下命令来将副本的数量缩减到1
kubectl scale --replicas=1 rs nginx-replicaset

你应该会看到以下响应:

replicaset.apps/nginx-replicaset scaled
  1. 运行以下命令来检查所有正在运行的 Pod:
kubectl get pods

你应该会看到类似以下的响应:

nginx-replicaset-s4stt   1/1      Running   0           11m

我们可以看到这一次,ReplicaSet 删除了所有超出期望数量1的 Pod,并只保留了一个副本在运行。

  1. 运行以下命令来删除 ReplicaSet 以进行清理:
kubectl delete rs nginx-replicaset

你应该会看到以下响应:

replicaset.apps "nginx-replicaset" deleted

在这个练习中,我们已经成功地扩展和缩减了副本的数量。如果您的应用程序的流量增加或减少,这可能特别有用。

部署

部署是一个 Kubernetes 对象,它充当了 ReplicaSet 的包装器,并使其更容易使用。一般来说,为了管理复制的服务,建议您使用部署,而部署又管理 ReplicaSet 和 ReplicaSet 创建的 Pod。

使用 Deployment 的主要动机是它保留了修订版本的历史记录。每当对 ReplicaSet 或底层 Pod 进行更改时,Deployment 都会记录 ReplicaSet 的新修订版本。这样,使用 Deployment 可以轻松回滚到先前的状态或版本。请记住,每次回滚也会为 Deployment 创建一个新的修订版本。以下图表概述了管理容器化应用程序的不同对象的层次结构:

图 7.6:Deployment、ReplicaSet、Pod 和容器的层次结构

图 7.6:Deployment、ReplicaSet、Pod 和容器的层次结构

Deployment 配置

Deployment 的配置实际上与 ReplicaSet 的配置非常相似。以下是一个 Deployment 配置的示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx

kind字段的值为Deployment。其余配置与 ReplicaSets 的配置相同。Deployments 还具有与 ReplicaSets 相同方式使用的replicasselector和 Pod template字段。

策略

spec下的strategy字段中,我们可以指定 Deployment 在用新的 Pod 替换旧的 Pod 时应使用的策略。这可以是RollingUpdateRecreate。默认值为RollingUpdate

RollingUpdate

这是一种用于更新 Deployment 而不会有任何停机时间的策略。使用RollingUpdate策略,控制器逐个更新 Pods。因此,在任何给定时间,总会有一些 Pod 在运行。当您想要更新 Pod 模板而不为应用程序带来任何停机时间时,这种策略特别有帮助。但是,请注意,进行滚动更新意味着可能会同时运行两个不同版本的 Pod(旧版本和新版本)。

如果应用程序提供静态信息,通常情况下这是可以接受的,因为使用两个不同版本的应用程序提供流量通常不会造成任何伤害,只要提供的信息是相同的。因此,对于这些应用程序,RollingUpdate通常是一个很好的策略。一般来说,我们可以将RollingUpdate用于新版本存储的数据可以被旧版本的应用程序读取和处理的应用程序。

以下是将策略设置为RollingUpdate的示例配置:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 1
    maxSurge: 1

maxUnavailable是在更新期间可以不可用的 Pod 的最大数量。此字段可以指定为表示不可用 Pod 的最大数量的整数,也可以指定为表示可以不可用的总副本的百分比的字符串。对于前面的示例配置,Kubernetes 将确保在应用更新时不会有超过一个副本不可用。maxUnavailable的默认值为25%

maxSurge是可以在所需的 Pod 数量(在replicas字段中指定)之上调度/创建的最大 Pod 数量。这个字段也可以指定为整数或百分比字符串,就像maxUnavailable一样。maxSurge的默认值也是25%

因此,在前面的示例中,我们告诉 Kubernetes 控制器以一次一个 Pod 的方式更新 Pod,以便永远不会有超过一个 Pod 不可用,并且永远不会有超过四个 Pod 被调度。

maxUnavailablemaxSurge这两个参数可以用于调整部署的可用性和扩展部署的速度。例如,maxUnavailable: 0maxSurge: "30%"可以确保快速扩展,同时始终保持所需的容量。maxUnavailable: "15%"maxSurge: 0可以确保在不使用任何额外容量的情况下执行部署,但最多可能会有 15%的 Pod 不运行。

重新创建

在这种策略中,所有现有的 Pod 都被杀死,然后使用更新的配置创建新的 Pod。这意味着在更新期间会有一些停机时间。然而,这确保了部署中运行的所有 Pod 将是相同的版本(旧的或新的)。当与需要具有共享状态的应用程序 Pod 一起工作时,这种策略特别有用,因此我们不能同时运行两个不同版本的 Pod。可以指定此策略如下:

strategy:
  type: Recreate

使用“重新创建”更新策略的一个很好的用例是,如果我们需要在新代码可以使用之前运行一些数据迁移或数据处理。在这种情况下,我们需要使用“重新创建”策略,因为我们不能承受任何新代码与旧代码一起运行而没有先运行迁移或处理所有 Pod 的情况。

现在我们已经研究了部署配置中的不同字段,让我们在以下练习中实现它们。

练习 7.05:创建一个带有 Nginx 容器的简单部署

在这个练习中,我们将使用前一节中描述的配置创建我们的第一个部署 Pod。

要成功完成这个练习,请执行以下步骤:

  1. 创建一个名为nginx-deployment.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx

在这个配置中,我们可以看到部署将有三个带有app: nginxenvironment: production标签的 Pod 的副本运行。

  1. 运行以下命令以创建前面步骤中定义的部署:
kubectl apply -f nginx-deployment.yaml

你应该看到以下响应:

deployment.apps/nginx-deployment created
  1. 运行以下命令以检查部署的状态:
kubectl get deployment nginx-deployment

你应该看到类似以下的响应:

NAME              READY    UP-TO-DATE    AVAILABLE   AGE
nginx-deployment  3/3      3             3           26m
  1. 运行以下命令以检查所有正在运行的 Pod:
kubectl get pods

你应该看到类似以下的响应:

图 7.7:由部署创建的 Pod 列表

图 7.7:由部署创建的 Pod 列表

我们可以看到部署已经创建了三个 Pod,就像我们期望的那样。

让我们试着理解自动分配给 Pod 的名称。nginx-deployment创建了一个名为nginx-deployment-588765684f的 ReplicaSet。然后,ReplicaSet 创建了三个 Pod 的副本,每个 Pod 的名称都以 ReplicaSet 的名称为前缀,后跟一个唯一的标识符。

  1. 现在我们已经创建了我们的第一个部署,让我们更详细地了解一下,以了解在创建过程中实际发生了什么。为了做到这一点,我们可以使用以下命令在终端中描述我们刚刚创建的部署:
kubectl describe rs nginx-deployment

你应该看到类似这样的输出:

图 7.8:描述 nginx-deployment

图 7.8:描述 nginx-deployment

此输出显示了我们刚刚创建的部署的各种细节。在接下来的子主题中,我们将逐步介绍前面输出的突出部分,以了解正在运行的部署。

部署上的标签和注释

与 ReplicaSets 类似,我们可以在图 7.8中显示的输出中看到以下行:

Labels:    app=nginx

这表明部署是使用app=nginx标签创建的。现在,让我们考虑输出中的下一个字段:

Annotations:    deployment.kubernetes.io/revision: 1
                kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"nginx-deployment","namespace":"d...

部署自动添加了两个注释。

修订注释

Kubernetes 控制器添加了一个带有deployment.kubernetes.io/revision键的注释,其中包含了特定部署的修订次数的信息。

最后应用的配置注释

控制器添加的另一个注释具有kubectl.kubernetes.io/last-applied-configuration键,其中包含应用于部署的最后配置(以 JSON 格式)。如果新的修订版本不起作用,此注释特别有助于将部署回滚到先前的修订版本。

部署的选择器

现在,考虑输出中图 7.8中显示的以下行:

Selector:    app=nginx,environment=production

这显示了部署配置的 Pod 选择器。因此,此部署将尝试获取具有这两个标签的 Pod。

副本

考虑输出中图 7.8中显示的以下行:

Replicas:    3 desired | 3 updated | 3 total | 3 available | 0 unavailable

我们可以看到,部署对于 Pod 的期望计数为3,并且还显示当前存在3个副本。

回滚部署

在实际场景中,当更改部署配置时可能会出错。您可以轻松撤消更改并回滚到先前稳定的部署修订版本。

我们可以使用kubectl rollout命令来检查修订历史和回滚。但是,为了使其工作,当我们使用任何applyset命令修改部署时,我们还需要使用--record标志。此标志记录发布历史。然后,您可以使用以下命令查看发布历史:

kubectl rollout history deployment <deployment_name>

然后,我们可以使用以下命令撤消任何更新:

kubectl rollout undo deployment <deployment_name>

让我们在以下练习中更仔细地看看这是如何工作的:

练习 7.06:回滚部署

在这个练习中,我们将更新部署两次。我们将在第二次更新中故意出错,并尝试回滚到先前的修订版本:

  1. 创建一个名为app-deployment.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
  labels:
    environment: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx
  1. 运行以下命令来创建部署:
kubectl apply -f app-deployment.yaml

您应该会看到以下响应:

deployment.apps/app-deployment created
  1. 运行以下命令来检查新创建的部署的发布历史:
kubectl rollout history deployment app-deployment

您应该会看到以下响应:

deployment.apps/app-deployment
REVISION     CHANGE-CAUSE
1            <none>

此输出显示,截至目前,部署没有发布历史记录。

  1. 对于第一次更新,让我们将容器的名称更改为nginx而不是nginx-container。使用以下内容更新app-deployment.yaml文件:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
  labels:
    environment: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx
        image: nginx

正如您所看到的,此模板中唯一更改的是容器的名称。

  1. 使用kubectl apply命令应用更改的配置,并使用--record标志。--record标志确保将部署的更新记录在部署的发布历史中:
kubectl apply -f app-deployment.yaml --record

您应该看到以下响应:

deployment.apps/app-deployment configured

请注意,由 --record 标志维护的滚动历史与注释中存储的过去配置不同,我们在“部署的标签和注释”子部分中看到了这一点。

  1. 等待几秒钟,让部署重新创建具有更新的 Pod 配置的 Pods,然后运行以下命令来检查部署的滚动历史:
kubectl rollout history deployment app-deployment

您应该看到以下响应:

图 7.9:检查部署历史

图 7.9:检查部署历史

在输出中,我们可以看到部署的第二个修订版本已经创建。它还跟踪了用于更新部署的命令。

  1. 接下来,让我们更新部署,并假设我们在这样做时犯了一个错误。在这个例子中,我们将使用 set image 命令将容器镜像更新为 ngnx(注意有意的拼写错误),而不是 nginx
kubectl set image deployment app-deployment nginx=ngnx --record

您应该看到以下响应:

deployment.apps/app-deployment image updated
  1. 等待几秒钟,让 Kubernetes 重新创建新的容器,然后使用 kubectl rollout status 命令检查部署的滚动状态:
kubectl rollout status deployment app-deployment

您应该看到以下响应:

Waiting for deployment "app-deployment" rollout to finish: 1 out of 3 new replicas have been updated...

在这个输出中,我们可以看到新的副本都还没有准备好。按 Ctrl + C 退出并继续。

  1. 运行以下命令检查 Pods 的状态:
kubectl get pods

您应该看到以下输出:

图 7.10:检查 Pods 的状态

图 7.10:检查 Pods 的状态

我们可以在输出中看到,新创建的 Pod 出现了ImagePullBackOff错误,这意味着 Pods 无法拉取镜像。这是预期的,因为我们在镜像名称中有一个拼写错误。

  1. 接下来,再次使用以下命令检查部署的修订历史:
kubectl rollout history deployment app-deployment

您应该看到以下响应:

图 7.11:检查部署的滚动历史

图 7.11:检查部署的滚动历史

我们可以看到使用包含拼写错误的 set image 命令创建了部署的第三个修订版本。现在,我们假装在更新部署时犯了一个错误,我们将看到如何撤消这个错误并回滚到部署的最后一个稳定修订版本。

  1. 运行以下命令回滚到上一个修订版本:
kubectl rollout undo deployment app-deployment

您应该看到以下响应:

deployment.apps/app-deployment rolled back

正如我们在这个输出中所看到的,Deployment 没有回滚到以前的修订版本。为了练习,我们可能希望回滚到与以前的修订版本不同的修订版本。我们可以使用 --to-revision 标志来指定我们想要回滚到的修订版本号。例如,在前面的情况下,我们可以使用以下命令,结果将完全相同:

kubectl rollout undo deployment app-deployment --to-revision=2
  1. 再次运行以下命令以检查 Deployment 的升级历史:
kubectl rollout history deployment app-deployment

你应该看到以下输出:

图 7.12:回滚后 Deployment 的升级历史

图 7.12:回滚后 Deployment 的升级历史

我们可以在这个输出中看到一个新的修订版本被创建,应用了之前的修订版本 2。我们可以看到修订版本 2 不再出现在修订版本列表中。这是因为升级总是以滚动向前的方式进行的。这意味着每当我们更新一个修订版本时,都会创建一个更高编号的新修订版本。同样,在回滚到修订版本 2 的情况下,修订版本 2 变成了修订版本 4。

在这个练习中,我们探索了许多与更新 Deployment、进行一些更改的滚动向前、跟踪 Deployment 历史、撤消一些更改以及回滚到以前修订版本相关的可能操作。

StatefulSets

StatefulSets 用于管理有状态的副本。与 Deployment 类似,StatefulSet 从相同的 Pod 模板创建和管理指定数量的 Pod 副本。然而,StatefulSets 与 Deployments 的不同之处在于它们为每个 Pod 保持唯一的标识。因此,即使所有的 Pods 规格相同,它们也不是可互换的。每个 Pod 都有一个固定的标识,应用代码可以使用它来管理特定 Pod 上的应用状态。对于具有 n 个副本的 StatefulSet,每个 Pod 被分配一个介于 0n – 1 之间的唯一整数序数。Pod 的名称反映了分配给它们的整数标识。创建 StatefulSet 时,所有的 Pods 都按照它们的整数序数顺序创建。

StatefulSet 管理的每个 Pod 都将保持它们的固定标识(整数序数),即使 Pod 重新启动。例如,如果特定的 Pod 崩溃或被删除,将创建一个新的 Pod,并分配与旧 Pod 相同的固定标识。

StatefulSet 配置

StatefulSet 的配置也与 ReplicaSet 非常相似。以下是一个 StatefulSet 配置的示例:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: example-statefulset
spec:
  replicas: 3
  selector:
    matchLabels:
      environment: production
  template:
    metadata:
      labels:
        environment: production
    spec:
      containers:
      - name: name-container
        image: image_name

正如我们在前面的配置中所看到的,StatefulSet 的apiVersionapps/v1kindStatefulSet。其余字段的使用方式与 ReplicaSets 相同。

注意

第十四章《在 Kubernetes 中运行有状态的组件》中,您将学习如何在多节点集群上实现 StatefulSets。

StatefulSets 的用例

  • 如果您需要持久存储,StatefulSets 非常有用。使用 StatefulSet,您可以将数据分区并存储在不同的 Pods 中。在这种情况下,一个 Pod 可能会关闭,一个新的 Pod 以相同的标识启动,并且具有之前由旧 Pod 存储的相同数据分区。

  • 如果您需要有序的更新或扩展,也可以使用 StatefulSet。例如,如果您希望按照为它们分配的标识的顺序创建或更新 Pods,使用 StatefulSet 是一个好主意。

DaemonSets

DaemonSets 用于管理集群中所有或选定节点上特定 Pod 的创建。如果我们配置一个 DaemonSet 在所有节点上创建 Pods,那么如果向集群添加新节点,将会创建新的 Pods 在这些新节点上运行。同样,如果一些节点从集群中移除,运行在这些节点上的 Pods 将被销毁。

DaemonSets 的用例

  • 日志记录:DaemonSet 最常见的用例之一是在所有节点上管理运行日志收集 Pod。这些 Pods 可用于从所有节点收集日志,然后在日志处理管道中处理它们。

  • 本地数据缓存:DaemonSet 也可以用于在所有节点上管理缓存 Pod。其他应用 Pods 可以使用这些 Pods 来临时存储缓存数据。

  • 监控:DaemonSet 的另一个用例是在所有节点上管理运行监控 Pod。这可用于收集特定节点上运行的 Pod 的系统或应用级别指标。

DaemonSet 配置

DaemonSet 的配置也与 ReplicaSet 或 Deployment 非常相似。以下是一个 DaemonSet 配置的示例:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: daemonset-example
  labels:
    app: daemonset-example
spec:
  selector:
    matchLabels:
      app: daemonset-example
  template:
    metadata:
      labels:
        app: daemonset-example
    spec:
      containers:
      - name: busybox-container
        image: busybox
        args:
        - /bin/sh
        - -c
        - sleep 10000

正如我们在前面的配置中所看到的,DaemonSet 的apiVersion设置为apps/v1kind设置为DaemonSet。其余字段的使用方式与 ReplicaSets 相同。

为了限制本书的范围,我们不会涵盖实现 DaemonSets 的详细信息。

在本章中,您已经了解了 ReplicaSets,它帮助我们管理运行应用程序的多个 Pod 副本,以及 Deployment 如何作为 ReplicaSet 的包装器添加一些功能来控制更新的发布和维护更新历史记录,并在需要时回滚。然后,我们了解了 StatefulSets,如果我们需要将每个副本视为唯一实体,则非常有用。我们还了解了 DaemonSets 如何允许我们在每个节点上调度一个 Pod。

所有这些控制器都有一个共同的特点——它们对于需要持续运行的应用程序或工作负载非常有用。然而,一些工作负载具有优雅的结论,任务完成后无需保持 Pod 运行。为此,Kubernetes 有一个称为 Job 的控制器。让我们在下一节中看看这个。

Jobs

Job 是 Kubernetes 中的一个监督者,可以用来管理应该运行确定任务然后优雅终止的 Pod。Job 创建指定数量的 Pod,并确保它们成功完成其工作负载或任务。当创建 Job 时,它会创建并跟踪其配置中指定的 Pod。当指定数量的 Pod 成功完成时,Job 被视为完成。如果 Pod 因底层节点故障而失败,Job 将创建一个新的 Pod 来替换它。这也意味着在 Pod 上运行的应用程序或代码应该能够优雅地处理在进程运行时出现新的 Pod 的情况。

Job 创建的 Pod 在作业完成后不会被删除。Pod 会运行到完成并以“已完成”状态留在集群中。

Job 可以以几种不同的方式使用:

  • 最简单的用例是创建一个只运行一个 Pod 直到完成的 Job。如果正在运行的 Pod 失败,Job 将只创建额外的新 Pod。例如,Job 可用于一次性或定期数据分析工作或用于机器学习模型的训练。

  • Job 也可以用于并行处理。我们可以指定多个成功的 Pod 完成,以确保 Job 仅在一定数量的 Pod 成功终止时才完成。

Job 配置

Job 的配置遵循与 ReplicaSet 或 Deployment 类似的模式。以下是 Job 配置的示例:

apiVersion: batch/v1
kind: Job
metadata:
  name: one-time-job
spec:
  template:
    spec:
      containers:
      - name: busybox-container
        image: busybox
        args:
        - /bin/sh
        - -c
        - date
      restartPolicy: OnFailure

作业对象的apiVersion字段设置为batch/v1batch API 组包含与批处理作业相关的对象。kind字段设置为Job

机器学习中作业的用例

作业非常适合批处理过程 - 在退出之前运行一定时间的过程。这使得作业非常适合许多类型的生产机器学习任务,如特征工程、交叉验证、模型训练和批量推断。例如,您可以创建一个 Kubernetes 作业来训练一个机器学习模型,并将模型和训练元数据持久化到外部存储。然后,您可以创建另一个作业来执行批量推断。这个作业将创建一个 Pod,从存储中获取预训练模型,将模型和数据加载到内存中,执行推断,并存储预测结果。

练习 7.07:创建一个在有限时间内完成的简单作业

在这个练习中,我们将创建我们的第一个作业,该作业将运行一个简单等待 10 秒然后完成的容器。

要成功完成此练习,请执行以下步骤:

  1. 创建一个名为one-time-job.yaml的文件,内容如下:
apiVersion: batch/v1
kind: Job
metadata:
  name: one-time-job
spec:
  template:
    spec:
      containers:
      - name: busybox-container
        image: busybox
        args:
        - /bin/sh
        - -c
        - date; sleep 20; echo "Bye"
      restartPolicy: OnFailure
  1. 运行以下命令,使用kubectl apply命令创建部署:
kubectl apply -f one-time-job.yaml

您应该看到以下响应:

job.batch/one-time-job created
  1. 运行以下命令以检查作业的状态:
kubectl get jobs

您应该看到类似于这样的响应:

NAME           COMPLETIONS    DURATION    AGE
one-time-job   0/1            3s          3s

我们可以看到作业需要一个完成,并且尚未完成。

  1. 运行以下命令来检查运行作业的 Pod 的状态:
kubectl get pods

请注意,您应该在作业完成之前运行此命令以查看此处显示的响应:

NAME                READY    STATUS    RESTARTS    AGE
one-time-job-bzz8l  1/1      Running   0           7s

我们可以看到作业已经创建了一个名为one-time-job-bzz8l的 Pod 来运行作业模板中指定的任务。

  1. 接下来,运行以下命令来检查由作业创建的 Pod 的日志:
kubectl logs -f <pod_name>

您应该看到类似以下的日志:

Sun   Nov 10 15:20:19 UTC 2019
Bye

我们可以看到 Pod 打印了日期,等待了 20 秒,然后在终端打印了Bye

  1. 让我们使用以下命令再次检查作业的状态:
kubectl get job one-time-job

您应该看到类似于这样的响应:

NAME           COMPLETIONS     DURATION    AGE
one-time-job   1/1             24s         14m

我们可以看到作业现在已经完成。

  1. 运行以下命令以验证 Pod 是否已完成运行:
kubectl get pods

您应该看到类似于这样的响应:

NAME                 READY    STATUS     RESTARTS    AGE
one-time-job-whw79   0/1      Completed  0           32m

我们可以看到 Pod 的状态为Completed

  1. 运行以下命令以删除作业(以及它创建的 Pod)进行清理:
kubectl delete job one-time-job

您应该看到以下响应:

job.batch "one-time-job" deleted

在这个练习中,我们创建了一个一次性作业,并验证了作业创建的 Pod 是否运行完成。为了简洁起见,我们将不在本次研讨会中实施并行任务的作业。

接下来,让我们通过一个活动来总结本章,我们将在其中创建一个部署,并汇集本章学到的几个想法。

活动 7.01:创建运行应用程序的部署

考虑这样一个情景,你正在与产品/应用团队合作,他们现在准备将他们的应用投入生产,并需要你的帮助以可复制和可靠的方式部署它。在本练习范围内,考虑应用的以下要求:

  • 默认副本数量应为 6。

  • 为简单起见,您可以使用nginx镜像来运行 Pod 中的容器。

  • 确保所有 Pod 都具有以下两个标签及其对应的值:

chapter=controllers
activity=1
  • 部署的更新策略应为RollingUpdate。最坏的情况下,Pod 的数量不应该超过一半,同样,在任何时候都不应该超过期望 Pod 数量的 150%。

一旦部署创建完成,您应该能够执行以下任务:

  • 将副本数量扩展到 10。

  • 将副本数量缩减到 5。

注意

理想情况下,您希望将此部署创建在不同的命名空间中,以使其与您在先前练习中创建的其他内容分开。因此,可以随意创建一个命名空间,并在该命名空间中创建部署。

以下是执行此活动的高级步骤:

  1. 为此活动创建一个命名空间。

  2. 编写部署配置。确保它满足所有指定的要求。

  3. 使用上一步的配置创建部署。

  4. 验证部署创建了六个 Pod。

  5. 执行前面提到的两个任务,并在每个步骤执行后验证 Pod 的数量。

您应该能够获取 Pod 的列表,以检查是否可以扩展 Pod 的数量,如下图所示:

图 7.13:检查 Pod 的数量是否扩展

](image/B14870_07_13.jpg)

图 7.13:检查 Pod 的数量是否扩展

同样,您还应该能够缩减并检查 Pod 的数量,如下所示:

图 7.14:检查 Pod 的数量是否缩减

](image/B14870_07_14.jpg)

图 7.14:检查 Pod 数量是否缩减

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD

总结

Kubernetes 将 Pod 视为短暂的实体,理想情况下,您不应该在单个 Pod 中部署任何应用程序或微服务。Kubernetes 提供了各种控制器来利用各种好处,包括自动复制、健康监控和自动扩展。

在本章中,我们介绍了不同类型的控制器,并了解了何时使用每种控制器。我们创建了 ReplicaSets,并观察了它们如何管理 Pods。我们学会了何时使用 DaemonSets 和 StatefulSets。我们还创建了一个 Deployment,并学会了如何扩展和缩减副本的数量,以及如何回滚到 Deployment 的早期版本。最后,我们学会了如何为一次性任务创建 Jobs。当您在即将到来的章节中看到时,所有这些控制器都将在您想要部署生产就绪的应用程序或工作负载时发挥作用。

在下一章中,我们将看到如何发现和访问由 Deployment 或 ReplicaSet 管理的 Pods 或副本。

第八章: 服务发现

概述

在本章中,我们将看看如何在先前章节中创建的各种对象之间路由流量,并使它们能够在集群内外被发现。本章还介绍了 Kubernetes 服务的概念,并解释了如何使用它们来公开使用部署控制器部署的应用程序。通过本章的学习,您将能够使您的应用程序对外部世界可访问。您还将了解不同类型的服务,并能够使用它们使不同的 Pod 集合相互交互。

介绍

在过去的几章中,我们学习了有关 Pod 和部署的知识,这有助于我们运行容器化应用程序。现在我们已经具备了部署我们的应用程序的能力,在本章中,我们将研究一些 API 对象,这些对象可以帮助我们进行网络设置,以确保我们的用户可以访问我们的应用程序,并且我们应用程序的不同组件以及不同的应用程序可以一起工作。

正如我们在之前的章节中所看到的,每个 Kubernetes Pod 都有其 IP 地址。然而,设置网络并连接所有内容并不像编写 Pod IP 地址那样简单。我们不能依赖单个 Pod 可靠地运行我们的应用程序。因此,我们使用部署来确保在任何给定时刻,我们将在集群中运行特定类型的 Pod 的固定数量。然而,这意味着在应用程序运行时,我们可以容忍一定数量的 Pod 失败,因为新的 Pod 会自动创建以取代它们。因此,这些 Pod 的 IP 地址不会保持不变。例如,如果我们有一组运行前端应用程序的 Pod,需要与集群内运行后端应用程序的另一组 Pod 进行通信,我们需要找到一种方法使这些 Pod 可被发现。

为了解决这个问题,我们使用 Kubernetes 服务。服务允许我们使一组逻辑 Pod(例如,所有由部署管理的 Pod)可被发现,并且可以被集群内运行的其他 Pod 或外部世界访问。

服务

服务定义了一组逻辑 Pod 可以被访问的策略。Kubernetes 服务使我们的应用程序的各个组件之间以及不同应用程序之间进行通信。服务帮助我们将应用程序与其他应用程序或用户连接起来。例如,假设我们有一组运行应用程序前端的 Pod,一组运行后端的 Pod,以及另一组连接数据源的 Pod。前端是用户需要直接交互的部分。然后前端需要连接到后端,后端又需要与外部数据源进行通信。

假设您正在制作一个调查应用程序,该应用程序还允许用户根据其调查结果进行可视化。使用一点简化,我们可以想象三个部署 - 一个运行表单前端以收集数据,另一个验证和存储数据,第三个运行数据可视化应用程序。以下图表应该帮助您想象服务如何在路由流量和公开不同组件方面发挥作用:

图 8.1:使用服务将流量路由到集群内部和集群内部

图 8.1:使用服务将流量路由到集群内部和集群内部

因此,服务的抽象有助于保持应用程序的不同部分解耦,并使它们之间能够进行通信。在传统(非 Kubernetes)环境中,您可能期望不同的组件通过运行不同资源的不同 VM 或裸金属机器的 IP 地址相互链接。在使用 Kubernetes 时,将不同资源链接在一起的主要方式是使用标签和标签选择器,这允许部署轻松替换失败的 Pod 或根据需要扩展部署的数量。因此,您可以将服务视为 IP 地址和基于标签选择器的不同资源链接机制之间的翻译层。因此,您只需指向一个服务,它将负责将流量路由到适当的应用程序,而不管与应用程序关联的副本 Pod 的数量或这些 Pod 运行在哪些节点上。

服务配置

与 Pod、ReplicaSets 和部署的配置类似,服务的配置也包含四个高级字段;即apiVersionkindmetadataspec

以下是服务的示例清单:

apiVersion: v1
kind: Service
metadata:
  name: sample-service
spec:
  ports:
    - port: 80
      targetPort: 80
  selector:
      key: value

对于一个服务,apiVersionv1kind总是Service。在metadata字段中,我们将指定服务的名称。除了名称,我们还可以在metadata字段中添加labelsannotations

spec字段的内容取决于我们想要创建的服务类型。在下一节中,我们将了解不同类型的服务并理解spec字段的配置的各个部分。

服务类型

有四种不同类型的服务:

  • NodePort:这种类型的服务使内部 Pod 在其所在的节点上的端口上可访问。

  • ClusterIP:这种类型的服务在集群内的特定 IP 上暴露服务。这是默认的服务类型。

  • LoadBalancer:这种类型的服务使用云提供商提供的负载均衡器在外部暴露应用程序。

  • ExternalName:这种类型的服务指向 DNS 而不是一组 Pod。其他类型的服务使用标签选择器来选择要暴露的 Pod。这是一种特殊类型的服务,默认情况下不使用任何选择器。

我们将在接下来的章节中更仔细地看看所有这些服务。

NodePort 服务

NodePort 服务在集群中的所有节点上都使用相同的端口暴露应用程序。Pod 可能在集群中的所有节点或部分节点上运行。

在一个简化的情况下,集群中只有一个节点时,服务会在服务配置的端口上暴露所有选定的 Pod。然而,在更实际的情况下,Pod 可能在多个节点上运行,服务跨越所有节点并在所有节点上的特定端口上暴露 Pod。这样,应用程序可以使用以下 IP/端口组合从 Kubernetes 集群外部访问:<NodeIP>:<NodePort>

一个示例服务的config文件看起来像这样:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort
  ports:
    - targetPort: 80
      port: 80
nodePort: 32023
  selector:
      app: nginx
      environment: production

正如我们所看到的,NodePort服务的定义中涉及了三个端口。让我们来看看这些:

  • targetPort:这个字段代表了 Pod 上运行的应用程序暴露的端口。这是服务转发请求的端口。默认情况下,targetPort设置为与port字段相同的值。

  • port:这个字段代表了服务本身的端口。

  • nodePort:这个字段代表了我们可以用来访问服务本身的节点上的端口。

除了端口,服务spec部分还有另一个字段叫做selector。这个部分用于指定一个 Pod 需要具有哪些标签,才能被服务选中。一旦这个服务被创建,它将识别所有具有app: nginxenvironment: production标签的 Pod,并为所有这样的 Pod 添加端点。我们将在下一个练习中更详细地了解端点。

练习 8.01:使用 Nginx 容器创建一个简单的 NodePort 服务

在这个练习中,我们将创建一个简单的 NodePort 服务,使用 Nginx 容器。默认情况下,Nginx 容器在 Pod 上暴露端口80,并显示一个 HTML 页面,上面写着Welcome to nginx!。我们将确保我们可以从本地机器的浏览器访问该页面。

要成功完成这个练习,请执行以下步骤:

  1. 创建一个名为nginx-deployment.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx
  1. 运行以下命令使用kubectl apply命令创建部署:
kubectl apply -f nginx-deployment.yaml

您应该得到以下输出:

deployment.apps/nginx-deployment created

正如我们所看到的,nginx-deployment已经被创建。

  1. 运行以下命令验证部署是否创建了三个副本:
kubectl get pods

您应该看到类似以下的响应:

图 8.2:获取所有 Pod

图 8.2:获取所有 Pod

  1. 创建一个名为nginx-service-nodeport.yaml的文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-nodeport
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 32023
  selector:
      app: nginx
      environment: production
  1. 运行以下命令来创建服务:
kubectl create -f nginx-service-nodeport.yaml

您应该看到以下输出:

service/nginx-service-nodeport created

或者,我们可以使用kubectl expose命令来暴露一个部署或一个 Pod,使用 Kubernetes 服务。以下命令还将创建一个名为nginx-service-nodeport的 NodePort 服务,porttargetPort设置为80。唯一的区别是,这个命令不允许我们自定义nodePort字段。使用kubectl expose命令创建服务时,nodePort会自动分配:

kubectl expose deployment nginx-deployment --name=nginx-service-nodeport --port=80 --target-port=80 --type=NodePort

如果我们使用这个命令创建服务,我们将能够在下一步中找出nodePort自动分配给服务的是什么。

  1. 运行以下命令以验证服务是否已创建:
kubectl get service

这应该会得到类似以下的响应:

图 8.3:获取 NodePort 服务

图 8.3:获取 NodePort 服务

您可以忽略名为kubernetes的额外服务,这个服务在我们创建服务之前已经存在。这个服务用于在集群内部暴露 Kubernetes API。

  1. 运行以下命令以验证 Service 是否以正确的配置创建:
kubectl describe service nginx-service-nodeport

这应该给我们以下输出:

图 8.4:描述 NodePort 服务

图 8.4:描述 NodePort 服务

在输出的突出显示部分,我们可以确认 Service 是使用正确的PortTargetPortNodePort字段创建的。

还有另一个字段叫做Endpoints。我们可以看到这个字段的值是一个 IP 地址列表;即172.17.0.3:80172.17.0.4:80172.17.0.5:80。这些 IP 地址分别指向由nginx-deployment创建的三个 Pod 分配的 IP 地址,以及所有这些 Pod 公开的目标端口。我们可以使用kubectl get pods命令以及custom-columns输出格式来获取所有三个 Pod 的 IP 地址。我们可以使用status.podIP字段创建自定义列输出,该字段包含正在运行的 Pod 的 IP 地址。

  1. 运行以下命令以查看所有三个 Pod 的 IP 地址:
kubectl get pods -o custom-columns=IP:status.podIP

您应该看到以下输出:

IP
172.17.0.4
172.17.0.3
172.17.0.5

因此,我们可以看到 Service 的Endpoints字段实际上指向我们三个 Pod 的 IP 地址。

正如我们所知,在 NodePort 服务的情况下,我们可以使用节点的 IP 地址和服务在节点上公开的端口来访问 Pod 的应用程序。为此,我们需要找出 Kubernetes 集群中节点的 IP 地址。

  1. 运行以下命令以获取本地运行的 Kubernetes 集群的 IP 地址:
minikube ip

您应该看到以下响应:

192.168.99.100
  1. 运行以下命令,使用curl发送请求到我们从上一步获得的 IP 地址的端口32023
curl 192.168.99.100:32023

您应该会收到 Nginx 的响应:

图 8.5:发送 curl 请求以检查 NodePort 服务

图 8.5:发送 curl 请求以检查 NodePort 服务

  1. 最后,打开浏览器并输入192.168.99.100:32023,以确保我们可以进入以下页面:图 8.6:在浏览器中访问应用程序

图 8.6:在浏览器中访问应用程序

注意

理想情况下,您希望为每个练习和活动创建不同的命名空间中的对象,以使它们与您的其他对象分开。因此,可以随意创建一个命名空间并在该命名空间中创建部署。或者,您可以确保清理掉以下命令中显示的任何对象,以确保没有干扰。

  1. 删除部署和服务,以确保在本章节的其余练习中你在干净的环境中工作:
kubectl delete deployment nginx-deployment

你应该看到以下响应:

deployment.apps "nginx-deployment" deleted

现在,使用以下命令删除该服务:

kubectl delete service nginx-service-nodeport

你应该看到这个响应:

service "nginx-service-nodeport" deleted

在这个练习中,我们创建了一个具有三个 Nginx 容器副本的部署(这可以替换为在容器中运行的任何真实应用程序),并使用 NodePort 服务暴露了该应用程序。

ClusterIP 服务

正如我们之前提到的,ClusterIP 服务会在集群内部暴露运行在 Pod 上的应用程序的 IP 地址。这使得 ClusterIP 服务成为在同一集群内不同类型的 Pod 之间进行通信的良好选择。

例如,让我们考虑一个简单调查应用的例子。假设我们有一个调查应用,用于向用户展示表单,用户可以在其中填写调查。它运行在由survey-frontend部署管理的一组 Pod 上。我们还有另一个应用,负责验证和存储用户填写的数据。它运行在由survey-backend部署管理的一组 Pod 上。这个后端应用需要被调查前端应用内部访问。我们可以使用 ClusterIP 服务来暴露后端应用,以便前端 Pod 可以使用单个 IP 地址轻松访问后端应用。

服务配置

以下是 ClusterIP 服务配置的示例:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: ClusterIP
  ports:
    - targetPort: 80
      port: 80
  selector:
      app: nginx
      environment: production

服务的type设置为ClusterIP。这种类型的服务只需要两个端口:targetPortport。它们分别代表了应用程序在 Pod 上暴露的端口和在给定集群 IP 上创建的服务的端口。

与 NodePort 服务类似,ClusterIP 服务的配置也需要一个selector部分,用于决定服务选择哪些 Pod。在这个例子中,这个服务将选择所有具有app: nginxenvironment: production标签的 Pod。我们将根据类似的示例在下一个练习中创建一个简单的 ClusterIP 服务。

练习 8.02:使用 Nginx 容器创建一个简单的 ClusterIP 服务

在这个练习中,我们将使用 Nginx 容器创建一个简单的 ClusterIP 服务。默认情况下,Nginx 容器在 Pod 上暴露端口80,显示一个 HTML 页面,上面写着Welcome to nginx!。我们将确保我们可以使用curl命令从 Kubernetes 集群内部访问该页面。让我们开始吧:

  1. 创建一个名为nginx-deployment.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
    spec:
      containers:
      - name: nginx-container
        image: nginx
  1. 运行以下命令,使用kubectl apply命令创建部署:
kubectl create -f nginx-deployment.yaml

您应该看到以下响应:

deployment.apps/nginx-deployment created
  1. 运行以下命令以验证部署是否已创建三个副本:
kubectl get pods

您应该看到类似以下的输出:

图 8.7:获取所有 Pod

图 8.7:获取所有 Pod

  1. 创建一个名为nginx-service-clusterip.yaml的文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-clusterip
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  selector:
      app: nginx
      environment: production
  1. 运行以下命令以创建服务:
kubectl create -f nginx-service-clusterip.yaml

您应该看到以下响应:

service/nginx-service-clusterip created
  1. 运行以下命令以验证服务是否已创建:
kubectl get service

您应该看到以下响应:

图 8.8:获取 ClusterIP 服务

图 8.8:获取 ClusterIP 服务

  1. 运行以下命令以验证服务是否已使用正确的配置创建:
kubectl describe service nginx-service-clusterip

您应该看到以下响应:

图 8.9:描述 ClusterIP 服务

图 8.9:描述 ClusterIP 服务

我们可以看到服务已使用正确的PortTargetPort字段创建。在Endpoints字段中,我们可以看到 Pod 的 IP 地址,以及这些 Pod 上的目标端口。

  1. 运行以下命令以查看所有三个 Pod 的 IP 地址:
kubectl get pods -o custom-columns=IP:status.podIP

您应该看到以下响应:

IP
172.17.0.5
172.17.0.3
172.17.0.4

因此,我们可以看到服务的Endpoints字段实际上指向我们三个 Pod 的 IP 地址。

  1. 运行以下命令以获取服务的集群 IP:
kubectl get service nginx-service-clusterip

这将产生以下输出:

图 8.10:从服务获取集群 IP

图 8.10:从服务获取集群 IP

正如我们所看到的,服务的集群 IP 是10.99.11.74

我们知道,在 ClusterIP 服务的情况下,我们可以从集群内部访问其端点上运行的应用程序。因此,我们需要进入集群以检查这是否真的有效。

  1. 运行以下命令通过 SSH 访问minikube节点:
minikube ssh

你会看到以下响应:

图 8.11:SSH 进入 minikube 节点

图 8.11:SSH 进入 minikube 节点

  1. 现在我们在集群内部,我们可以尝试访问服务的集群 IP 地址,看看我们是否可以访问运行 Nginx 的 Pods:
curl 10.99.11.74

应该看到来自 Nginx 的以下响应:

图 8.12:从集群内部向服务发送 curl 请求

图 8.12:从集群内部向服务发送 curl 请求

在这里,我们可以看到curl返回默认 Nginx 欢迎页面的 HTML 代码。因此,我们可以成功访问我们的 Nginx Pods。接下来,我们将删除 Pods 和 Services。

  1. 运行以下命令退出 minikube 内部的 SSH 会话:
exit
  1. 删除部署和服务,以确保在本章的后续练习中您正在处理干净的环境:
kubectl delete deployment nginx-deployment

你应该看到以下响应:

deployment.apps "nginx-deployment" deleted

使用以下命令删除服务:

kubectl delete service nginx-service-clusterip

你应该看到以下响应:

service "nginx-service-clusterip" deleted

在这个练习中,我们能够在单个 IP 地址上公开运行在多个 Pods 上的应用程序。这可以被同一集群内运行的所有其他 Pods 访问。

为服务选择自定义 IP 地址

在上一个练习中,我们看到服务是使用 Kubernetes 集群内的随机可用 IP 地址创建的。如果需要,我们也可以指定 IP 地址。如果我们已经为特定地址有 DNS 条目并且想要重用它作为我们的服务,这可能特别有用。

我们可以通过将spec.clusterIP字段设置为我们希望服务使用的 IP 地址的值来实现这一点。在该字段中指定的 IP 地址应为有效的 IPv4 或 IPv6 地址。如果使用无效的 IP 地址创建服务,API 服务器将返回错误。

练习 8.03:使用自定义 IP 创建 ClusterIP 服务

在这个练习中,我们将使用自定义 IP 地址创建一个 ClusterIP 服务。我们将尝试一个随机的 IP 地址。与之前的练习一样,我们将确保我们可以使用curl命令访问 Kubernetes 集群内的默认 Nginx 页面。让我们开始吧:

  1. 创建一个名为nginx-deployment.yaml的文件,内容与本章前面练习中使用的内容相同。

  2. 运行以下命令以创建部署:

kubectl create -f nginx-deployment.yaml

您应该看到以下响应:

deployment.apps/nginx-deployment created
  1. 创建一个名为nginx-service-custom-clusterip.yaml的文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-custom-clusterip
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  clusterIP: 10.90.10.70
  selector:
      app: nginx
      environment: production

目前使用的是一个随机的 ClusterIP 值。

  1. 运行以下命令以创建具有上述配置的服务:
kubectl create -f nginx-service-custom-clusterip.yaml

您应该看到以下响应:

图 8.13:由于 IP 地址不正确而导致服务创建失败

图 8.13:由于 IP 地址不正确而导致服务创建失败

正如我们所看到的,该命令给出了一个错误,因为我们使用的 IP 地址(10.90.10.70)不在有效的 IP 范围内。正如在前面的输出中所强调的,有效的 IP 范围是10.96.0.0/12

我们实际上可以在创建服务之前使用kubectl cluster-info dump命令找到这些有效的 IP 地址范围。它提供了大量可用于集群调试和诊断的信息。我们可以在命令的输出中过滤service-cluster-ip-range字符串,以找出我们可以在集群中使用的有效 IP 地址范围。以下命令将输出有效的 IP 范围:

kubectl cluster-info dump | grep -m 1 service-cluster-ip-range

您应该看到以下输出:

"--service-cluster-ip-range=10.96.0.0/12",

然后,我们可以为我们的服务使用适当的clusterIP IP 地址。

  1. 通过将clusterIP的值更改为10.96.0.5来修改nginx-service-custom-clusterip.yaml文件,因为这是一个有效的值:
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-custom-clusterip
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  clusterIP: 10.96.0.5
  selector:
      app: nginx
      environment: production
  1. 再次运行以下命令以创建服务:
kubectl create -f nginx-service-custom-clusterip.yaml

您应该看到以下输出:

service/nginx-service-custom-clusterip created

我们可以看到服务已成功创建。

  1. 运行以下命令以确保服务是使用我们在配置中指定的自定义 ClusterIP 创建的:
kubectl get service nginx-service-custom-clusterip

您应该看到以下输出:

图 8.14:从服务获取 ClusterIP

图 8.14:从服务获取 ClusterIP

在这里,我们可以确认服务确实是使用配置中提到的 IP 地址10.96.0.5创建的。

  1. 接下来,让我们确认我们可以使用集群内的自定义 IP 地址访问服务:
minikube ssh

您应该看到以下响应:

图 8.15:SSH 进入 minikube 节点

图 8.15:SSH 进入 minikube 节点

  1. 现在,运行以下命令,使用curl10.96.0.5:80发送请求:
curl 10.96.0.5

我们故意在curl请求中跳过了端口号(80),因为默认情况下,curl 假定端口号为80。如果服务使用不同的端口号,我们将不得不在 curl 请求中明确指定。您应该看到以下输出:

图 8.16:从 minikube 节点向服务发送 curl 请求

图 8.16:从 minikube 节点向服务发送 curl 请求

因此,我们可以看到我们能够从集群内部访问我们的服务,并且该服务可以在我们为clusterIP定义的 IP 地址上访问。

LoadBalancer 服务

LoadBalancer 服务使用云提供商提供的负载均衡器来外部公开应用程序。这种类型的服务没有默认的本地实现,只能使用云提供商部署。当创建LoadBalancer类型的服务时,云提供商会提供一个负载均衡器。

因此,LoadBalancer 服务基本上是 NodePort 服务的超集。LoadBalancer 服务使用云提供商提供的实现,并为服务分配外部 IP 地址。

LoadBalancer服务的配置取决于云提供商。每个云提供商都需要您添加一组特定的元数据,以注释的形式。以下是LoadBalancer服务配置的简化示例:

apiVersion: v1
kind: Service
metadata:
  name: loadbalancer-service
spec:
  type: LoadBalancer
  clusterIP: 10.90.10.0
  ports:
    - targetPort: 8080
      port: 80
  selector:
    app: nginx
    environment: production

ExternalName 服务

ExternalName 服务将服务映射到 DNS 名称。在 ExternalName 服务的情况下,没有代理或转发。重定向请求发生在 DNS 级别。当请求服务时,将返回一个 CNAME 记录,其值为在服务配置中设置的 DNS 名称。

ExternalName 服务的配置不包含任何选择器。它看起来是这样的:

apiVersion: v1
kind: Service
metadata:
  name: externalname-service
spec:
  type: ExternalName
  externalName: my.example.domain.com

前面的服务模板将externalname-service映射到一个 DNS 名称;例如,my.example.domain.com

假设您正在将生产应用程序迁移到一个新的 Kubernetes 集群。一个很好的方法是首先从无状态的部分开始,并将它们首先移动到 Kubernetes 集群。在迁移过程中,您需要确保 Kubernetes 集群中的这些无状态部分仍然可以访问其他生产服务,例如数据库存储或其他后端服务/ API。在这种情况下,我们可以简单地创建一个 ExternalName 服务,以便我们的新集群中的 Pod 可以仍然访问旧集群中的资源,这些资源超出了新集群的范围。因此,ExternalName 提供了 Kubernetes 应用程序与运行在 Kubernetes 集群之外的外部服务之间的通信。

入口

Ingress 是一个定义规则的对象,用于管理对 Kubernetes 集群中服务的外部访问。通常,Ingress 充当互联网和集群内运行的服务之间的中间人:

图 8.17:入口

图 8.17:入口

您将在第十二章“您的应用程序和 HA”中学到更多关于 Ingress 以及使用它的主要动机。因此,在这里我们不会涵盖 Ingress 的实现。

现在我们已经了解了 Kubernetes 中不同类型的服务,我们将实现所有这些服务,以了解它们在实际情况下如何一起工作。

活动 8.01:创建一个服务来暴露运行在 Pod 上的应用程序

考虑这样一个情景,你正在与产品团队合作,他们创建了一个调查应用程序,该应用程序有两个独立和解耦的组件 - 前端和后端。调查应用程序的前端组件呈现调查表单,并需要向外部用户公开。它还需要与后端组件通信,后端负责验证和存储调查的响应。

在本活动范围内,考虑以下任务:

  1. 为了避免使这项活动过于复杂,您可以部署 Apache 服务器(hub.docker.com/_/httpd)作为前端,并且我们可以将其默认的占位符主页视为应该对调查申请人可见的组件。公开前端应用程序,以便在主机节点的端口31000上可以访问它。

  2. 对于后端应用程序,部署一个 Nginx 服务器。我们将把 Nginx 的默认主页视为您应该能够从后端看到的页面。公开后端应用程序,以便前端应用程序 Pod 在同一集群中可以访问它。

默认情况下,Apache 和 Nginx 在 Pod 上以端口80公开。

注意

我们在这里使用 Apache 和 Nginx 来保持活动简单。在实际情况下,这两者将被替换为前端调查站点和调查应用程序的后端数据分析组件,以及用于存储所有调查数据的数据库组件。

  1. 为了确保前端应用程序知道后端应用程序服务,向包含后端服务的 IP 和端口地址的前端应用程序 Pod 添加环境变量。这将确保前端应用程序知道将请求发送到后端应用程序的位置。

要向 Pod 添加环境变量,可以在 Pod 配置的spec部分中添加一个名为env的字段,其中包含我们想要添加的所有环境变量的名称和值对的列表。以下是如何添加名为APPLICATION_TYPE的环境变量,其值为Frontend的示例:

apiVersion: v1
kind: Pod
metadata:
  name: environment-variables-example
  labels:
    application: frontend
spec:
  containers:
  - name: apache-httpd
    image: httpd
    env:
    - name: APPLICATION_TYPE
      value: "Frontend"

注意

我们在这里使用了一个叫做ConfigMap的东西来添加环境变量。我们将在第十章 ConfigMaps 和 Secrets中学到更多关于它们的知识。

  1. 假设根据对应用程序的负载测试,您估计最初需要五个前端应用程序副本和四个后端应用程序副本。

以下是您需要执行的高级步骤,以完成此活动:

  1. 为此活动创建一个命名空间。

  2. 为后端应用程序编写适当的部署配置,并创建部署。

  3. 为后端应用程序编写适当的服务配置,包括适当的服务类型,并创建服务。

  4. 确保后端应用程序可以按预期访问。

  5. 为前端应用程序编写适当的部署配置。确保为后端应用程序服务的 IP 地址和端口地址设置了环境变量。

  6. 为前端应用程序创建一个部署。

  7. 为前端应用程序编写适当的服务配置,包括适当的服务类型,并创建服务。

  8. 确保前端应用程序在主机节点的端口31000上按预期可访问。

预期输出:

在练习结束时,您应该能够使用主机 IP 地址和端口31000在浏览器中访问前端应用程序。您应该在浏览器中看到以下输出:

图 8.18:活动 8.01 的预期输出

图 8.18:活动 8.01 的预期输出

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD

总结

在本章中,我们介绍了在 Pod 上运行的应用程序的不同暴露方式。我们已经看到了如何使用 ClusterIP 服务来在集群内部暴露应用程序。我们还看到了如何使用 NodePort 服务来在集群外部暴露应用程序。我们还简要介绍了 LoadBalancer 和 ExternalName 服务。

现在我们已经创建了一个部署,并学会了如何使它可以从外部世界访问,在下一章中,我们将专注于存储方面。在那里,我们将涵盖在磁盘上读取和存储数据,在 Pod 之间和跨 Pod。

第九章: 在磁盘上存储和读取数据

概述

本章介绍了使用卷来存储或读取容器内部运行的数据的概念。在本章结束时,您将能够创建卷来临时存储数据在一个 pod 中,独立于容器的生命周期,并在同一个 pod 内的不同容器之间共享数据。您还将学习如何使用持久卷PVs)来在集群中独立于 pod 生命周期存储数据。我们还将介绍如何创建持久卷声明PVCs)来动态配置卷并在 pod 内使用它们。

介绍

在之前的章节中,我们创建了部署来创建应用程序的多个副本,并使用服务公开了我们的应用程序。然而,我们还没有完全探讨 Kubernetes 如何促进应用程序存储和读取数据,这是本章的主题。

在实践中,大多数应用程序以某种方式与数据交互。可能我们有一个需要从文件中读取数据的应用程序。同样,我们的应用程序可能需要在本地写入一些数据,以便应用程序的其他部分或不同的应用程序来读取。例如,如果我们有一个运行主应用程序的容器,它在本地产生一些日志,我们希望有一个旁路容器(这是一个与主应用程序容器一起在 pod 中运行的第二个容器),可以在同一个 pod 中运行以读取和处理主应用程序产生的本地日志。然而,为了实现这一点,我们需要找到一种在同一个 pod 中的不同容器之间共享存储的方法。

假设我们正在一个 pod 中训练一个机器学习模型。在模型训练的中间阶段,我们需要在磁盘上本地存储一些数据。同样,最终结果 - 训练好的模型 - 也需要被存储在磁盘上,以便即使 pod 终止后也可以稍后检索。对于这种用例,我们需要一种方式来为 pod 分配一些存储空间,以便在 pod 的生命周期之外存在写入该存储的数据。

同样,我们可能有一些数据需要被同一应用程序的多个副本写入或读取。当这些 pod 副本中的一些崩溃和/或重新启动时,这些数据也应该持久存在。例如,如果我们有一个电子商务网站,我们可能希望将用户数据以及库存记录存储在数据库中。这些数据需要在 pod 重新启动以及部署更新或回滚时持久存在。

为了实现这些目的,Kubernetes 提供了一个称为卷的抽象。持久卷PV)是您将遇到的最常见类型的卷。在本章中,我们将涵盖这一点,以及许多其他类型的卷。我们将学习如何使用它们,并按需进行配置。

假设我们有一个 pod 在本地磁盘上存储一些数据。现在,如果存储数据的容器崩溃并重新启动,数据将丢失。新容器将以分配的空白磁盘空间开始。因此,我们甚至不能依赖容器本身来临时存储数据。

我们可能还有这样一种情况,即 pod 中的一个容器存储了一些需要被同一 pod 中的其他容器访问的数据。

Kubernetes 卷抽象解决了这两个问题。下面是一个显示卷及其与物理存储和应用程序交互的图表:

图 9.1:卷作为应用程序的存储抽象

图 9.1:卷作为应用程序的存储抽象

从这个图表中可以看出,卷被暴露给应用程序作为一个抽象,最终将数据存储在您可能正在使用的任何类型的物理存储上。

Kubernetes 卷的生命周期与使用它的 pod 的生命周期相同。换句话说,即使 pod 中的容器重新启动,新容器也将使用相同的卷。因此,数据在容器重新启动时不会丢失。然而,一旦一个 pod 终止或重新启动,卷就会停止存在,数据也会丢失。为了解决这个问题,我们可以使用 PVs,我们将在本章后面介绍。

如何使用卷

卷在 pod 规范中定义。以下是一个带有卷的 pod 配置的示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-emptydir-volume
spec:
  restartPolicy: Never
  containers:
  - image: ubuntu
    name: ubuntu-container
    volumeMounts:
    - mountPath: /data
      name: data-volume
  volumes:
  - name: data-volume
    emptyDir: {}

正如我们在前面的配置中所看到的,要定义一个卷,pod 配置需要设置两个字段:

  • .spec.volumes字段定义了这个 pod 计划使用的卷。

  • .spec.containers.volumeMounts定义了在各个容器中挂载这些卷的位置。这将为所有容器单独定义。

定义卷

在前面的示例中,.spec.volumes字段有两个字段,定义了卷的配置:

  • name:这是卷的名称,当卷被挂载时,它将在容器的volumeMounts字段中被引用。它必须是有效的 DNS 名称。卷的名称必须在单个 pod 内是唯一的。

  • emptyDir:这取决于所使用的卷的类型(在前面的示例中是emptyDir)。这定义了卷的实际配置。我们将在下一节中通过一些示例来介绍卷的类型。

挂载卷

每个容器都需要单独指定volumeMounts来挂载卷。在前面的示例中,您可以看到.spec.containers[*].volumeMounts配置具有以下字段:

  • name:这是需要为此容器挂载的卷的名称。

  • mountPath:这是容器内应该挂载卷的路径。每个容器可以在不同的路径上挂载相同的卷。

除了这些之外,我们还可以设置两个其他值得注意的字段:

  • subPath:这是一个可选字段,包含需要在容器上挂载的卷的路径。默认情况下,卷是从其根目录挂载的。此字段可用于仅挂载卷中的子目录,而不是整个卷。例如,如果您将相同的卷用于多个用户,将子路径挂载到容器上比在卷的根目录上挂载更有用。

  • readonly:这是一个可选标志,用于确定挂载的卷是否为只读。默认情况下,卷是以读写方式挂载的。

卷的类型

正如前面提到的,Kubernetes 支持多种类型的卷,大多数类型的卷的可用性取决于您使用的云提供商。AWS、Azure 和 Google Cloud 都支持不同类型的卷。

让我们详细看一些常见类型的卷。

emptyDir

emptyDir卷是指在分配给节点的 pod 上创建的空目录。它只存在于 pod 存在的时间。在 pod 内运行的所有容器都可以从该目录中写入和读取文件。相同的emptyDir卷可以挂载到不同的路径以供不同的容器使用。

这是一个使用emptyDir卷的 pod 配置的示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-emptydir-volume
spec:
  restartPolicy: Never
  containers:
  - image: ubuntu
    name: ubuntu-container
    volumeMounts:
    - mountPath: /data
      name: data-volume
volumes:
  - name: data-volume
    emptyDir: {}

在这个例子中,{}表示emptyDir卷将以默认方式定义。默认情况下,emptyDir卷存储在磁盘或 SSD 上,具体取决于环境。我们可以通过将.emptyDir.medium字段设置为Memory来将其更改为使用 RAM。

因此,我们可以修改前面的 pod 配置的volumes部分,使用内存支持的emptyDir卷,如下所示:

  volumes:
  - name: data-volume
    emptyDir:
      medium: Memory

这通知 Kubernetes 使用基于 RAM 的文件系统(tmpfs)来存储卷。尽管与磁盘上的数据相比,tmpfs 非常快,但使用内存卷有一些缺点。首先,tmpfs 存储在节点重启时会被清除。其次,存储在基于内存的卷中的数据会计入容器的内存限制。因此,在使用基于内存的卷时需要小心。

我们还可以通过设置.volumes.emptyDir.sizeLimit字段来指定要在emptyDir卷中使用的存储的大小限制。这个大小限制适用于基于磁盘和基于内存的emptyDir卷。对于基于内存的卷,允许的最大使用量将是sizeLimit字段值或 pod 中所有容器的内存限制之和中较低的那个。

用例

emptyDir卷的一些用例如下:

  • 需要大量空间进行计算的临时临时空间,例如磁盘上的归并排序

  • 存储长时间计算的检查点所需的存储空间,例如训练机器学习模型,其中需要保存进度以便从崩溃中恢复

hostPath

hostPath卷用于将主机节点文件系统中的文件或目录挂载到 pod 中。

这是一个使用hostPath卷的 pod 配置的示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-hostpath-volume
spec:
  restartPolicy: Never
  containers:
  - image: ubuntu
    name: ubuntu-container
    volumeMounts:
    - mountPath: /data
      name: data-volume
  volumes:
  - name: data-volume
    hostPath:
      path: /tmp
      type: Directory

在这个例子中,主机节点上的/home/user/data目录将挂载到容器上的/data路径上。让我们看一下hostPath下的两个字段:

  • path:这是将要挂载在挂载此卷的容器上的目录或文件的路径。它也可以是一个符号链接指向目录或文件,一个 UNIX 套接字的地址,或者一个字符或块设备,取决于type字段。

  • type:这是一个可选字段,允许我们指定卷的类型。如果指定了此字段,将在挂载hostPath卷之前执行某些检查。

type字段支持以下值:

  • ""(空字符串):这是默认值,意味着在挂载hostPath卷之前不会执行任何检查。如果指定的路径在节点上不存在,Pod 仍将被创建,而不会验证路径的存在。因此,Pod 将因此错误而不断崩溃。

  • DirectoryOrCreate:这意味着指定的目录路径在主机节点上可能已经存在,也可能不存在。如果不存在,将创建一个空目录。

  • Directory:这意味着主机节点上必须存在指定路径的目录。如果指定路径上的目录不存在,创建 Pod 时将出现FailedMount错误,表示hostPath类型检查失败。

  • FileOrCreate:这意味着指定的文件路径在主机节点上可能已经存在,也可能不存在。如果不存在,将创建一个空文件。

  • File:这意味着主机节点上必须存在指定路径的文件。

  • Socket:这意味着必须在指定的路径上存在一个 UNIX 套接字。

  • CharDevice:这意味着在指定路径上必须存在一个字符设备。

  • BlockDevice:这意味着在指定路径上必须存在一个块设备。

用例

在大多数情况下,您的应用程序不需要hostPath卷。但是,有一些利基用例可能特别有用。hostPath卷的一些用例如下:

  • 只有在运行 Pod 之前主机节点上存在特定主机路径时才允许创建 Pod。例如,一个 Pod 可能需要一些秘密或凭据存在于主机上的文件中才能运行。

  • 运行需要访问 Docker 内部的容器。我们可以通过将hostPath设置为/var/lib/docker来实现。

注意

除了这里介绍的两种类型的 Volume 外,Kubernetes 还支持许多其他类型,其中一些是特定于某些云平台的。您可以在kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes找到更多信息。

在前面的章节中,我们学习了有关 Volumes 以及如何使用它们的不同类型的知识。在接下来的练习中,我们将把这些概念付诸实践,并在 pod 中使用 Volumes。

练习 9.01:创建一个带有 emptyDir Volume 的 Pod

在这个练习中,我们将创建一个带有emptyDir Volume 的基本 pod。我们还将模拟手动写入数据,然后确保 Volume 中存储的数据在容器重新启动时保持不变:

  1. 创建一个名为pod-with-emptydir-volume.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-emptydir-volume
spec:
  containers:
  - image: nginx
    name: nginx-container
    volumeMounts: 
    - mountPath: /mounted-data
      name: data-volume
  volumes:
  - name: data-volume
emptyDir: {}

在这个 pod 配置中,我们使用了一个挂载在/mounted-data目录下的emptyDir Volume。

  1. 运行以下命令以使用前面的配置创建 pod:
kubectl create -f pod-with-emptydir-volume.yaml

您应该看到以下响应:

pod/pod-with-emptydir-volume created
  1. 运行以下命令以确认 pod 已创建并准备就绪:
kubectl get pod pod-with-emptydir-volume

您应该看到以下响应:

NAME                       READY   STATUS   RESTARTS   AGE
pod-with-emptydir-volume   1/1     Running  0          20s
  1. 运行以下命令以描述 pod,以便我们可以验证该 pod 上挂载了正确的 Volume:
kubectl describe pod pod-with-emptydir-volume

这将产生很长的输出。在终端输出中查找以下部分:

图 9.2:描述具有挂载的 emptyDir 卷的 pod

图 9.2:描述具有挂载的 emptyDir 卷的 pod

如前面的图像所示,名为data-volumeemptyDir Volume 已创建,并且它被挂载在nginx-container上的/mounted-data路径上。我们可以看到 Volume 已以rw模式挂载,表示读写。

现在我们已经验证了 pod 是使用正确的 Volume 配置创建的,我们将手动向该路径写入一些数据。在实践中,这种写入将由您的应用程序代码完成。

  1. 现在,我们将使用kubectl exec命令在 pod 内部运行 Bash shell:
kubectl exec pod-with-emptydir-volume -it /bin/bash

您应该在终端屏幕上看到以下内容:

root@pod-with-emptydir-volume:/#

现在,这将允许您通过 SSH 连接在运行nginx-container中的 Bash shell 上运行命令。请注意,我们是以 root 用户身份运行的。

注意

如果 pod 中运行了一个 sidecar 容器(或者一个 pod 中有多个容器),那么你可以通过添加-c参数来控制kubectl exec命令的执行位置,以指定容器,就像你将在下一个练习中看到的那样。

  1. 运行以下命令来检查 pod 的根目录的内容:
ls

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

bin   dev  home  lib64  mnt          opt   root  sbin   sys   usr
boot  etc  lib   media  mounted-data proc  run   srv    tmp   var

注意到有一个名为mounted-data的目录。

  1. 运行以下命令进入mounted-data目录并检查其内容:
cd mounted-data
ls

你应该看到一个空白的输出,如下所示:

root@pod-with-emptydir-volume:/mounted-data#

这个输出表明mounted-data目录是空的,这是预期的,因为我们没有任何在 pod 内运行的代码会写入这个路径。

  1. 运行以下命令在mounted-data目录内创建一个简单的文本文件:
echo "Manually stored data" > manual-data.txt
  1. 现在,再次运行ls命令来检查目录的内容:
ls

你应该看到以下输出:

manual-data.txt

因此,我们已经在挂载卷目录中创建了一个新文件并添加了一些内容。现在,我们的目标是验证如果容器重新启动,这些数据是否仍然存在。

  1. 为了重新启动容器,我们将杀死nginx进程,这将触发重新启动。运行以下命令安装 procps 软件包,以便我们可以使用ps命令找到我们想要杀死的进程的进程 ID(PID)。首先,更新软件包列表:
sudo apt-get update

你应该看到类似于以下的输出:

图 9.3:apt-get 更新

图 9.3:apt-get 更新

我们的软件包列表已经更新,现在我们准备安装 procps。

  1. 使用以下命令安装 procps:
sudo apt-get install procps

在提示确认安装时输入Y,然后安装将继续并输出类似于以下内容:

图 9.4:使用 apt-get 安装 procps

图 9.4:使用 apt-get 安装 procps

  1. 现在,运行以下命令来检查容器上运行的进程列表:
ps aux

你应该看到以下输出:

图 9.5:运行中的进程列表

图 9.5:运行中的进程列表

在输出中,我们可以看到除了其他几个进程外,nginx主进程以PID1的形式运行。

  1. 运行以下命令来杀死nginx主进程:
kill 1

你应该看到以下响应:

图 9.6:杀死容器

图 9.6:杀死容器

输出显示终端退出了 pod 上的 Bash 会话。这是因为容器被杀死。137退出代码表示该会话被手动干预杀死。

  1. 运行以下命令以获取 pod 的状态:
kubectl describe pod pod-with-emptydir-volume

观察您获得的输出中的以下部分:

图 9.7:描述 pod

图 9.7:描述 pod

您将看到nginx-container现在有一个Restart Count字段,其值为1。这意味着在我们杀死它后容器被重新启动。请注意,重新启动容器不会触发 pod 的重新启动。因此,我们应该期望 Volume 中存储的数据仍然存在。让我们在下一步中验证一下。

  1. 让我们再次在 pod 内运行 Bash,并转到/mounted-data目录:
kubectl exec pod-with-emptydir-volume -it /bin/bash
cd mounted-data

您将看到以下输出:

root@pod-with-emptydir-volume:/# cd mounted data/
  1. 运行以下命令以检查/mounted-data目录的内容:
ls

您将看到以下输出:

manual-data.txt

这个输出表明我们在杀死容器之前创建的文件仍然存在于 Volume 中。

  1. 运行以下命令以验证我们在 Volume 中创建的文件的内容:
cat manual-data.txt

您将看到以下输出:

Manually stored data

这个输出表明,我们存储在 Volume 中的数据即使在容器重新启动时也保持完整。

  1. 运行以下命令以删除 pod:
kubectl delete pod pod-with-emptydir-volume

您将看到以下输出,确认 pod 已被删除:

pod "pod-with-emptydir-volume" deleted

在这个练习中,我们创建了一个带有emptyDir卷的 pod,检查了 pod 是否创建了一个空目录挂载在容器内的正确路径,并验证了我们可以在该目录内写入数据,并且只要 pod 仍在运行,数据就会在容器重新启动时保持完整。

现在,让我们转移到一个场景,让我们观察一些更多卷的用途。让我们考虑这样一个场景,我们有一个应用 pod,运行了三个容器。我们可以假设其中三个容器中有两个正在提供流量,并且它们将日志转储到一个共享文件中。第三个容器充当一个 sidecar 监控容器,从文件中读取日志,并将其转储到外部日志存储系统,以便进一步分析和警报。让我们在下一个练习中考虑这种情况,并了解如何利用 pod 的三个容器之间共享的emptyDir卷。

练习 9.02:创建一个由三个容器共享 emptyDir 卷的 Pod

在这个练习中,我们将展示emptyDir卷的一些更多用途,并在同一个 pod 中的三个容器之间共享它。每个容器将在不同的本地路径挂载相同的卷:

  1. 创建一个名为shared-emptydir-volume.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: shared-emptydir-volume
spec:
  containers:
  - image: ubuntu
    name: container-1
    command: ['/bin/bash', '-ec', 'sleep 3600']
    volumeMounts:
    - mountPath: /mounted-data-1
      name: data-volume
  - image: ubuntu
    name: container-2
    command: ['/bin/bash', '-ec', 'sleep 3600']
    volumeMounts:
    - mountPath: /mounted-data-2
      name: data-volume
  - image: ubuntu
    name: container-3
    command: ['/bin/bash', '-ec', 'sleep 3600']
    volumeMounts:
    - mountPath: /mounted-data-3
      name: data-volume
  volumes:
  - name: data-volume
    emptyDir: {}

在这个配置中,我们定义了一个名为data-volumeemptyDir卷,它被挂载在三个容器的不同路径上。

请注意,每个容器都被配置为在启动时运行一个使它们休眠 1 小时的命令。这是为了保持ubuntu容器运行,以便我们可以在容器上执行以下操作。默认情况下,ubuntu容器被配置为运行指定的命令并在完成后退出。

  1. 运行以下命令以使用上述配置创建 pod:
kubectl create -f shared-emptydir-volume.yaml

您将看到以下输出:

pod/shared-emptydir-volume created
  1. 运行以下命令来检查 pod 的状态:
kubectl get pod shared-emptydir-volume

您将看到以下输出:

NAME                     READY   STATUS    RESTARTS   AGE
shared-emptydir-volume   3/3     Running   0          13s

此输出表明此 pod 内的所有三个容器都在运行。

  1. 接下来,我们将运行以下命令在第一个容器中运行 Bash:
kubectl exec shared-emptydir-volume -c container-1 -it -- /bin/bash

这里,-c标志用于指定我们要在其中运行 Bash 的容器。您将在终端中看到以下内容:

root@shared-emptydir-volume:/#
  1. 运行以下命令来检查容器中根目录的内容:
ls

您将看到以下输出:

图 9.8:列出容器内根目录的内容

图 9.8:列出容器内根目录的内容

我们可以看到mounted-data-1目录已在容器中创建。此外,您还可以看到在典型的 Ubuntu 根目录中会看到的目录列表,以及我们创建的mounted-data-1目录。

  1. 现在,我们将进入mounted-data-1目录并创建一个带有一些文本的简单文本文件:
cd mounted-data-1
echo 'Data written on container-1' > data-1.txt
  1. 运行以下命令来验证文件是否已存储:
ls

您将看到以下输出:

data-1.txt
  1. 运行以下命令退出container-1并返回到您的主机终端:
exit
  1. 现在,让我们在第二个容器中运行 Bash,它的名称是container-2
kubectl exec shared-emptydir-volume -c container-2 -it -- /bin/bash

您将在终端中看到以下内容:

root@shared-emptydir-volume:/#
  1. 运行以下命令来定位容器中根目录中的挂载目录:
ls

您将看到以下输出:

图 9.9:列出容器内根目录的内容

图 9.9:列出容器内根目录的内容

注意名为mounted-data-2的目录,这是我们在container-2中的卷的挂载点。

  1. 运行以下命令检查mounted-data-2目录的内容:
cd mounted-data-2
ls

你会看到以下输出:

data-1.txt

这个输出表明,已经有一个名为data-1.txt的文件,我们之前在container-1中创建过。

  1. 让我们验证这是否是我们在早期步骤中创建的同一个文件。运行以下命令检查该文件的内容:
cat data-1.txt

你会看到以下输出:

Data written on container-1

这个输出验证了这是我们在这个练习的早期步骤中创建的同一个文件。

  1. 运行以下命令在该目录中写入一个名为data-2.txt的新文件:
echo 'Data written on container-2' > data-2.txt
  1. 现在,让我们确认文件已经创建:
ls

你应该看到以下输出:

data-1.txt   data-2.txt

正如你在截图中看到的,新文件已经创建,现在挂载目录中有两个文件——data-1.txtdata-2.txt

  1. 运行以下命令退出该容器上的 Bash 会话:
exit
  1. 现在,让我们在container-3中运行 Bash:
kubectl exec shared-emptydir-volume -c container-3 -it -- /bin/bash

你会在终端上看到以下内容:

root@shared-empty-dir-volume:/#
  1. 进入/mounted-data-3目录并检查其内容:
cd mounted-data-3
ls

你会看到以下输出:

data-1.txt   data-2.txt

这个输出显示,这个容器可以看到我们之前在container-1container-2中创建的两个文件——data-1.txtdata-2.txt

  1. 运行以下命令验证第一个文件data-1.txt的内容:
cat data-1.txt

你应该看到以下输出:

Data written on container-1
  1. 运行以下命令验证第二个文件data-2.txt的内容:
cat data-2.txt

你应该看到以下输出:

Data written on container-2

最后两个命令的输出证明,任何一个容器在挂载卷上写入的数据都可以被其他容器读取。接下来,我们将验证其他容器是否有权限写入特定容器写入的数据。

  1. 运行以下命令覆盖data-2.txt文件的内容:
echo 'Data updated on container 3' > data-2.txt
  1. 接下来,让我们退出container-3
exit
  1. 运行以下命令再次在container-1中运行 Bash:
kubectl exec shared-emptydir-volume -c container-1 -it -- /bin/bash

你会在终端上看到以下内容:

root@shared-emptydir-volume:/#
  1. 运行以下命令检查data-2.txt文件的内容:
cat mounted-data-1/data-2.txt

你应该看到以下输出:

Data updated on container 3

这个输出表明,被container-3覆盖的数据也可以被其他容器读取。

  1. 运行以下命令退出container-3中的 SSH 会话:
exit
  1. 运行以下命令以删除 pod:
kubectl delete pod shared-emptydir-volume

您应该看到以下输出,表明 pod 已被删除:

pod "shared-emptydir-volume" deleted

在这个练习中,我们学习了如何使用卷,并验证了相同的卷可以挂载到不同容器中的不同路径。我们还看到使用相同卷的容器可以读取或写入(或覆盖)卷的内容。

持久卷

到目前为止,我们看到的卷有一个限制,即它们的生命周期取决于 pod 的生命周期。例如,当使用它们的 pod 被删除或重新启动时,emptyDir 或 hostPath 等卷将被删除。例如,如果我们使用卷来存储电子商务网站的用户数据和库存记录,当应用程序 pod 重新启动时,数据将被删除。因此,卷不适合存储您想要持久保存的数据。

为了解决这个问题,Kubernetes 支持以持久卷PV)的形式提供持久存储。PV 是 Kubernetes 集群中表示存储块的对象。它可以由集群管理员预先配置,也可以动态配置。PV 可以被视为集群资源,就像节点一样,因此它不限于单个命名空间。这些卷的工作方式类似于我们在之前章节中看到的卷。PV 的生命周期不依赖于使用 PV 的任何 pod 的生命周期。然而,从 pod 的角度来看,使用普通卷和 PV 没有区别。

为了使用 PV,需要创建一个持久卷索赔PVC)。PVC 是用户或 pod 对存储的请求。PVC 可以请求特定大小的存储和特定的访问模式。PVC 实际上是用户访问各种存储资源的抽象方式。PVC 由命名空间范围,因此 pod 只能访问在同一命名空间中创建的 PVC。

注意

任何时候,PV 只能绑定到一个 PVC。

以下是一个显示应用程序如何与 PV 和 PVC 交互的图表:

图 9.10:PV 和 PVC 如何共同为您的应用程序 pod 提供存储

图 9.10:PV 和 PVC 如何共同为您的应用程序 pod 提供存储

如图所示,Kubernetes 使用 PV 和 PVC 的组合来为您的应用程序提供存储。PVC 基本上是提供符合特定条件的 PV 的请求。

这是与我们在先前练习中看到的有显著变化,我们在那里直接在 pod 定义中创建了卷。请求(PVC)和实际存储抽象(PV)的分离使应用程序开发人员不必担心集群上存在的所有不同 PV 的具体情况和状态;他们只需根据应用程序的要求创建 PVC,然后在 pod 中使用它。这种松散的绑定还允许整个系统在 pod 重新启动的情况下保持稳定和可靠。

与卷类似,Kubernetes 支持几种类型的 PV。其中一些可能特定于您的云平台。您可以在此链接找到不同支持类型的列表:kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes

持久卷配置

以下是 PV 配置的示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-persistent-volume
spec: 
  storageClassName: standard
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 172.10.1.1
    path: /tmp/pv

与往常一样,PV 对象还具有我们已经看到的三个字段:apiVersionkindmetadata。由于这是一种nfs类型的 PV,我们在配置中有nfs部分。让我们逐个浏览 PV spec部分中的一些重要字段。

storageClassName

每个 PV 都属于某个存储类别。我们使用storageClassName字段定义 PV 关联的存储类别的名称。StorageClass 是 Kubernetes 对象,为管理员提供了描述他们支持的不同类型或存储配置文件的方式。在前面的示例中,standard只是存储类别的一个示例。

不同的存储类别允许您根据应用程序的特定需求,基于性能和容量为不同的应用程序分配不同类型的存储。每个集群管理员都可以配置自己的存储类别。每个存储类别可以有自己的提供程序、备份策略或管理员确定的回收策略。提供程序是确定如何为特定类型的 PV 提供存储的系统。Kubernetes 支持一组内部提供程序以及用户可以实现的外部提供程序。关于如何使用或创建提供程序的详细信息,但是超出了本书的范围。

属于特定存储类的 PV 只能绑定到请求该特定类的 PVC。请注意,这是一个可选字段。没有存储类字段的任何 PV 只对不请求特定存储类的 PVC 可用。

容量

该字段表示 PV 的存储容量。我们可以像定义 Pod 规范中的内存和 CPU 限制字段一样设置这个字段。在前面的示例规范中,我们将容量设置为 10 GiB。

volumeMode

volumeMode字段表示我们希望如何使用存储。它可以有两个可能的值:Filesystem(默认)和Block。我们可以将volumeMode字段设置为Block,以便使用原始块设备作为存储,或者设置为Filesystem以在持久卷上使用传统文件系统。

访问模式

PV 的访问模式表示挂载卷允许的功能。一个卷一次只能使用一个支持的访问模式进行挂载。有三种可能的访问模式:

  • ReadWriteOnceRWO):仅由单个节点挂载为读写

  • ReadOnlyManyROX):由多个节点挂载为只读

  • ReadWriteManyRWX):由多个节点挂载为读写

请注意,并非所有类型的卷都支持所有访问模式。请检查允许的访问模式的参考文献,以了解您正在使用的特定类型的卷的访问模式。

持久卷回收策略

用户完成卷的使用后,他们可以删除他们的 PVC,这样就可以回收 PV 资源。回收策略字段表示在释放后允许声明 PV 的策略。PV 被释放意味着 PV 不再与 PVC 相关联,因为 PVC 已被删除。然后,PV 可供任何其他 PVC 使用,或者换句话说,回收。PV 是否可以被重用取决于回收策略。该字段有三个可能的值:

  • Retain:此回收策略表示即使 PV 已被释放,存储在 PV 中的数据也会保留。管理员需要手动删除存储中的数据。在此策略中,PV 标记为Released而不是Available。因此,Released PV 可能不一定为空。

  • 回收: 使用此回收策略意味着一旦 PV 被释放,卷上的数据将使用基本的rm -rf命令删除。这将标记 PV 为可用,因此可以再次声明。使用动态配置比使用此回收策略更好。我们将在下一节讨论动态配置。

  • 删除: 使用此回收策略意味着一旦 PV 被释放,底层存储中存储的数据以及 PV 本身将被删除。

注意

不同的云环境对回收策略有不同的默认值。因此,请确保您检查您使用的云环境的回收策略的默认值,以避免意外删除 PV 中的数据。

PV 状态

在其生命周期的任何时刻,PV 可以具有以下状态之一:

  • 可用: 这表示 PV 可以被声明。

  • 绑定: 这表示 PV 已绑定到 PVC。

  • 发布: 这表示绑定到此资源的 PVC 已被删除;但是,它尚未被其他 PVC 回收。

  • 失败: 这表示回收过程中出现了失败。

既然我们已经看了 PV 的各个方面,让我们来看看 PVC。

持久卷声明配置

以下是 PVC 配置的示例:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-persistent-volume-claim
spec:
  storageClassName: standard
  resources:
    requests:
      storage: 500Mi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  selector:
    matchLabels:
      environment: "prod"

同样,通常情况下,PVC 对象也有三个我们已经看到的字段:apiVersionkindmetadata。让我们逐个查看 PVC spec部分中的一些重要字段。

storageClassName

PVC 可以通过指定storageClassName字段来请求特定类别的存储。只有指定存储类的 PV 才能绑定到这样的 PVC。

如果storageClassName字段设置为空字符串(""),这些 PVC 只会绑定到没有设置存储类的 PV。

另一方面,如果 PVC 中的storageClassName字段未设置,则取决于管理员是否启用了DefaultStorageClass。如果为集群设置了默认存储类,则没有设置storageClassName字段的 PVC 将绑定到具有该默认存储类的 PV。否则,没有设置storageClassName字段的 PVC 将只绑定到没有设置存储类的 PV。

资源

就像我们学到的 pod 可以发出特定的资源请求一样,PVC 也可以通过指定requestslimits字段来以类似的方式请求资源,这是可选的。只有满足资源请求的 PV 才能绑定到 PVC 上。

volumeMode

PVC 遵循与 PV 相同的约定,以指示将存储用作文件系统或原始块设备。只有与 PVC 配置中指定的卷模式相同的 PV 才能绑定到 PVC。

accessMode

PVC 应该指定它需要的访问模式,并且根据该访问模式的可用性分配 PV。

selectors

与服务中的 pod 选择器类似,PVC 可以使用matchLabels和/或matchExpressions字段来指定可以满足特定声明的卷的条件。只有满足selectors字段中指定条件的 PV 才会被考虑为声明。当这两个字段一起作为选择器使用时,两个字段指定的条件将使用 AND 操作进行组合。

如何使用持久卷

为了使用 PV,我们有以下三个步骤:配置卷,将其绑定到声明(PVC),并在 pod 上使用声明作为卷。让我们详细了解这些步骤。

第一步 - 配置卷

卷可以通过两种方式进行配置 - 静态和动态:

  • 静态:在静态配置中,集群管理员必须预先配置多个 PV,然后它们才能作为可用资源提供给 PVC。

  • 动态:如果您使用动态配置,管理员不需要预先配置所有 PV。在这种配置中,集群将根据请求的存储类动态为 PVC 配置 PV。因此,当应用程序或微服务需要更多存储时,Kubernetes 可以自动处理并根据需要扩展云基础设施。

我们将在后面的部分更详细地介绍动态配置。

第二步 - 将卷绑定到声明

在这一步中,需要使用请求的存储限制、特定的访问模式和特定的存储类来创建 PVC。每当创建新的 PVC 时,Kubernetes 控制器将搜索与其条件匹配的 PV。如果找到与所有 PVC 条件匹配的 PV,它将绑定声明到 PV 上。每个 PV 一次只能绑定到一个 PVC。

第三步 - 使用声明

一旦 PV 被配置并绑定到 PVC,pod 就可以将 PV 作为 Volume 使用。接下来,当 pod 使用 PVC 作为 Volume 时,Kubernetes 将会找到与该 PVC 绑定的 PV,并将其挂载到 pod 上。

以下是使用 PVC 作为 Volume 的 pod 配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-pvc-as-volume
spec:
  containers:
  - image: nginx
    name: nginx-application
    volumeMounts:
    - mountPath: /data/application
      name: example-storage
  volumes:
  - name: example-storage
    persistentVolumeClaim:
      claimName: example-claim

在这个例子中,我们假设已经有一个名为example-claim的 PVC 已经绑定到PersistentVolume。pod 配置指定persistentVolumeClaim作为 Volume 的类型,并指定要使用的 claim 的名称。Kubernetes 将会找到实际绑定到该 claim 的 PV,并将其挂载到容器内的/data/application目录下。

注意

为了使这个工作,pod 和 PVC 必须在同一个命名空间中。这是因为 Kubernetes 只会在 pod 的命名空间中寻找 claim,如果找不到 PVC,pod 将无法被调度。在这种情况下,pod 将会被卡在Pending状态,直到被删除。

现在,让我们通过创建一个使用 PV 的 pod 来将这些概念付诸实践。

练习 9.03:创建使用 PersistentVolume 进行存储的 Pod

在这个练习中,我们首先假装集群管理员提前配置了 PV。接下来,假设是开发人员的角色,我们将创建一个绑定到 PV 的 PVC。之后,我们将创建一个使用这个 claim 作为 Volume 挂载到一个容器上的 pod:

  1. 首先,我们将通过 SSH 访问主机节点。在 Minikube 的情况下,我们可以使用以下命令来做到:
minikube ssh

你应该会看到类似于以下的输出:

图 9.11:SSH 到 minikube 节点

图 9.11:SSH 到 minikube 节点

  1. 运行以下命令在/mnt目录下创建一个名为data的目录:
sudo mkdir /mnt/data
  1. 运行以下命令在/mnt/data目录下创建一个名为data.txt的文件:
sudo bash -ec 'echo "Data written on host node" > /mnt/data/data.txt'

这个命令应该会创建一个名为data.txt的文件,其中包含Data written on host node的内容。我们将使用这个文件的内容在稍后的阶段验证,我们可以成功地使用 PV 和 PVC 在容器上挂载这个目录。

  1. 运行以下命令退出主机节点:
exit

这将会把我们带回到本地机器终端,我们可以在那里运行kubectl命令。

  1. 创建一个名为pv-hostpath.yaml的文件,内容如下:
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
spec:
  storageClassName: local-pv
  capacity:
    storage: 500Mi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /mnt/data

在这个 PV 配置中,我们使用了local-pv存储类。Volume 将托管在主机节点的/mnt/data路径上。卷的大小将为500Mi,访问模式将为ReadWriteOnce

  1. 运行以下命令来使用前面的配置创建 PV:
kubectl create -f pv-hostpath.yaml

你应该看到以下输出:

persistentvolume/pv-hostpath created
  1. 运行以下命令来检查我们刚刚创建的 PV 的状态:
kubectl get pv pv-hostpath

正如你在这个命令中看到的那样,pvPersistentVolume的一个被接受的缩写。你应该看到以下输出:

图 9.12:检查 PV 的状态

图 9.12:检查 PV 的状态

在前面的输出中,我们可以看到 Volume 已经使用所需的配置创建,并且其状态为Available

  1. 创建一个名为pvc-local.yaml的文件,内容如下:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-local
spec:
  storageClassName: local-pv
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi

在这个配置中,我们有一个声明,请求一个带有local-pv存储类、ReadWriteOnce访问模式和100Mi存储大小的 Volume。

  1. 运行以下命令来创建这个 PVC:
kubectl create -f pvc-local.yaml

你应该看到以下输出:

persistentvolumeclaim/pvc-local created

一旦我们创建了这个 PVC,Kubernetes 将搜索匹配的 PV 来满足这个声明。

  1. 运行以下命令来检查这个 PVC 的状态:
kubectl get pvc pvc-local

你应该看到以下输出:

图 9.13:检查声明的状态

图 9.13:检查声明的状态

正如我们在这个输出中看到的那样,PVC 已经使用所需的配置创建,并立即绑定到我们在此练习的早期步骤中创建的名为pv-hostpath的现有 PV。

  1. 接下来,我们可以创建一个将使用这个 PVC 作为 Volume 的 pod。创建一个名为pod-local-pvc.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-local-pvc
spec:
  restartPolicy: Never
  containers:
  - image: ubuntu
    name: ubuntu-container
    command: ['/bin/bash', '-ec', 'cat /data/application/data.txt']
    volumeMounts:
    - mountPath: /data/application
      name: local-volume
  volumes:
  - name: local-volume
    persistentVolumeClaim:
claimName: pvc-local

pod 将使用名为pvc-local的 PVC 作为 Volume,并在容器中的/data/application路径上挂载它。此外,我们有一个容器将在启动时运行cat /data/application/data.txt命令。这只是一个简化的例子,我们将展示最初在主机节点的 PV 目录中写入的数据现在可用于这个 pod。

  1. 运行以下命令来创建这个 pod:
kubectl create -f pod-local-pvc.yaml

你应该看到以下输出:

pod/pod-local-pvc created

这个输出表明 pod 已经成功创建。

  1. 运行以下命令来检查我们刚刚创建的 pod 的状态:
kubectl get pod pod-local-pvc

你应该看到以下输出:

NAME             READY     STATUS      RESTARTS    AGE
pod-local-pvc    0/1       Completed   1           7s

在这个输出中,我们可以看到 pod 已经成功完成,因为这次我们没有添加任何休眠命令。

  1. 运行以下命令来检查日志。我们期望在日志中看到cat /data/application/data.txt命令的输出:
kubectl logs pod-local-pvc

您应该看到以下输出:

Data written on host node

这个输出清楚地表明这个 pod 可以访问我们在/mnt/data/data.txt创建的文件。这个文件是容器中挂载在/data/application目录中的一部分。

  1. 现在,让我们清理本练习中创建的资源。使用以下命令删除 pod:
kubectl delete pod pod-local-pvc

您应该看到以下输出,表明 pod 已被删除:

pod "pod-local-pvc" deleted
  1. 使用以下命令删除 PVC:
kubectl delete pvc pvc-local

您应该看到以下输出,表明 PVC 已被删除:

persistentvolumeclaim "pvc-local" deleted

请注意,如果我们在删除 PVC 之前尝试删除 PV,PV 将被卡在Terminating阶段,并且将等待 PVC 释放它。因此,我们需要在删除 PV 之前先删除绑定到 PV 的 PVC。

  1. 现在我们的 PVC 已被删除,我们可以安全地通过运行以下命令删除 PV:
kubectl delete pv pv-hostpath

您应该看到以下输出,表明 PV 已被删除:

persistentvolume "pv-hostpath" deleted

在这个练习中,我们学习了如何配置 PV,创建声明来使用这些卷,然后在 pod 内部使用这些 PVC 作为卷。

动态配置

在本章的前几节中,我们看到集群管理员需要为我们配置 PV,然后我们才能将其用作应用程序的存储。为了解决这个问题,Kubernetes 也支持动态卷配置。动态卷配置使得可以按需创建存储卷。这消除了管理员在创建任何 PVC 之前创建卷的需要。只有在有要求时才会配置卷。

为了启用动态配置,管理员需要创建一个或多个存储类,用户可以在其声明中使用这些存储类来使用动态配置。这些StorageClass对象需要指定将使用的配置程序以及其参数。配置程序取决于环境。每个云提供商支持不同的配置程序,因此请确保您在集群中创建此类存储类时与您的云提供商进行核实。

以下是在 AWS 平台上创建新StorageClass的配置示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: example-storage-class
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io1
  iopsPerGB: "10"
  fsType: ext4

在这种配置中,使用了kubernetes.io/aws-ebs provisioner - EBS 代表弹性块存储,仅在 AWS 上可用。这个 provisioner 需要各种参数,包括type,我们可以使用它来指定我们想要为这个存储类使用什么类型的磁盘。请查看 AWS 文档,了解我们可以使用的各种参数及其可能的值。provisioner 和所需的参数将根据你使用的云提供商而改变。

一旦集群管理员创建了一个存储类,用户就可以创建一个 PVC,请求使用该存储类名称在storageClassName字段中设置的存储。Kubernetes 将自动提供存储卷,创建一个满足要求的存储类的 PV 对象,并将其绑定到声明:

以下是使用我们之前定义的存储类的 PVC 的配置示例:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-pvc
spec:
  storageClassName: example-storage-class
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

正如我们所看到的,PVC 的配置保持不变,只是现在,我们必须使用集群管理员为我们创建的存储类。

一旦声明被绑定到一个自动创建的 Volume,我们可以创建使用该 PVC 作为 Volume 的 pod,就像我们在前面的部分中看到的那样。一旦声明被删除,Volume 就会被自动删除。

活动 9.01:创建一个使用动态配置的持久卷的 Pod

考虑一下,你是一个集群管理员,首先需要创建一个自定义的存储类,以便让使用你的集群的开发人员动态地提供 PV。要在 minikube 集群上创建一个存储类,你可以使用k8s.io/minikube-hostpath provisioner,不需要任何额外的参数,就像我们在动态配置部分的StorageClass示例中所示的那样。

接下来,作为开发人员或集群用户,使用以下规格声明一个带有 100Mi 存储请求的 PV,并将其挂载到使用以下规格创建的 pod 中的容器中:

  1. 该 pod 应该有两个容器。

  2. 两个容器应该在本地挂载相同的 PV。

  3. 第一个容器应该向 PV 中写入一些数据,第二个容器应该读取并打印出第一个容器写入的数据。

为简单起见,考虑从第一个容器向 PV 中的文件写入一个简单的字符串。对于第二个容器,添加一些等待时间,以便第二个容器在完全写入之前不开始读取数据。然后,后一个容器应该读取并打印出第一个容器写入的文件的内容。

注意

理想情况下,您希望将此部署创建在一个不同的命名空间中,以使其与您在这些练习期间创建的其他内容分开。因此,请随意创建一个命名空间,并在该命名空间中创建此活动的所有对象。

执行此活动的高级步骤如下:

  1. 为此活动创建一个命名空间。

  2. 使用给定的信息编写适当的存储类配置,并创建StorageClass对象。

  3. 使用在上一步创建的存储类编写 PVC 的适当配置。使用此配置创建 PVC。

  4. 验证声明是否绑定到了与我们在步骤 2中创建的相同存储类的自动创建的 PV。

  5. 使用给定的信息和上一步的 PVC 作为卷,编写适当的 pod 配置。使用此配置创建 pod。

  6. 验证其中一个容器是否可以读取另一个容器写入 PV 的文件的内容。

您应该能够检查第二个容器的日志,并验证第一个容器在 PV 中写入的数据是否可以被第二个容器读取,如下面的输出所示:

Data written by container-1

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

总结

正如我们在介绍中提到的,大多数应用程序需要出于许多不同的原因存储或检索数据。在本章中,我们看到 Kubernetes 提供了各种方式来为存储应用程序的状态以及长期存储数据提供存储。

我们已经介绍了如何在 Pod 内运行的应用程序中使用存储的方法。我们看到了如何使用不同类型的卷来在同一 Pod 中运行的容器之间共享临时数据。我们还学习了如何在 Pod 重新启动时持久化数据。我们学会了如何手动配置 PV 来创建 PVC,并将其绑定到这些卷,以及如何创建可以使用这些声明作为其容器上挂载的卷的 Pod。接下来,我们学会了如何仅使用预先创建的存储类和 PVC 动态请求存储。我们还了解了这些卷与 Pod 的生命周期之间的关系。

在下一章中,我们将进一步扩展这些概念,并学习如何存储应用程序配置和机密信息。

第十章: ConfigMaps 和 Secrets

概述

在本章中,我们将学习如何将应用程序配置数据与应用程序本身解耦,并采取这种方法的优势。在本章结束时,您将能够定义 Kubernetes ConfigMap 和 Secret 对象,运行一个使用来自 ConfigMaps 和 Secrets 的数据的简单 Pod,描述将配置数据与应用程序解耦的优势,并使用 ConfigMaps 和 Secrets 将应用程序配置数据与应用程序容器解耦。

介绍

第五章Pods中,我们了解到 Pods 是 Kubernetes 中部署的最小单位。Pods 可以有多个容器,每个容器可以有一个与之关联的容器镜像。这个容器镜像通常打包了您计划运行的目标应用程序。一旦开发人员确信代码按预期运行,下一步就是将代码推广到测试、集成和生产环境。

容易,对吧?然而,一个问题是,当我们将打包的容器从一个环境移动到另一个环境时,尽管应用程序保持不变,但它需要特定于环境的数据,例如连接到的数据库 URL。为了解决这个问题,我们可以以这样的方式编写我们的应用程序,使得环境特定的数据由部署到的环境提供给应用程序。

在本章中,我们将发现 Kubernetes 提供了什么来将特定于环境的数据与我们的应用程序容器关联起来,而不改变我们的容器镜像。有多种方法可以为我们的应用程序提供特定于环境的配置数据:

  1. 为 Pods 提供命令行参数。

  2. 为 Pods 提供环境变量。

  3. 在容器中挂载配置文件。

首先,我们需要使用一个叫做ConfigMap的对象来定义我们的配置数据。一旦数据被定义并加载到 Kubernetes 中,第二步就是将定义的数据提供给您的应用程序。

然而,如果您有敏感数据,比如数据库密码,您想提供给应用程序容器怎么办?好吧,Kubernetes Secret 提供了一种定义敏感数据给应用程序的方法。

ConfigMap 和 Secret 对象都具有类似的目的。两者都提供了一种定义数据的方式,可以将其注入到应用程序中,以便相同的容器可以在不同的环境中使用。它们之间的区别很小,我们将在本章后面详细学习。简单来说,Secrets 旨在保存机密数据(例如密码、私钥等),而 ConfigMaps 更适用于一般配置数据,例如数据库位置。ConfigMaps 和 Secrets 驻留在创建它们的特定命名空间中。它们只能被驻留在相同命名空间中的 Pods 引用。

Kubernetes 使用名为etcd的内部键值存储作为其数据库,用于存储在 Kubernetes 中定义的所有对象。由于 ConfigMaps 和 Secrets 是 Kubernetes 对象,它们被存储在内部键值存储中。

让我们先深入了解一下 ConfigMaps。

什么是 ConfigMap?

ConfigMap 允许我们定义与应用程序相关的数据。ConfigMap 将应用程序数据与应用程序解耦,以便相同的应用程序可以在不同的环境中移植。它还提供了一种从相同容器镜像中的运行服务中注入定制数据的方式。

ConfigMaps 可以通过文字值或来自文件或目录中的所有文件来创建。请注意,我们存储在 ConfigMaps 中的主要数据是用于非敏感配置,例如配置文件或环境变量。

一旦定义了 ConfigMap,它将通过环境变量或一组文件加载到应用程序中。然后,应用程序可以将文件视为本地文件并从中读取。需要注意的是(从 Kubernetes 的 1.9.6 版本开始),从 ConfigMaps 加载的文件是只读的。ConfigMaps 还可以保存系统应用程序的配置数据,例如操作员和控制器。

在接下来的练习中,您将看到定义 ConfigMaps 的不同方式以及使 ConfigMap 数据可用于运行的 Pod 的不同方式。

让我们看看 Kubernetes 在 ConfigMap 创建方面提供了什么。Kubernetes 帮助命令提供了一个很好的起点:

kubectl create configmap --help

您应该看到以下响应:

图 10.1:Kubernetes 内置帮助创建 ConfigMap

图 10.1:Kubernetes 内置帮助创建 ConfigMap

从前面的输出中可以看出,ConfigMaps 可以用于创建单个值、值列表,或者整个文件或目录。我们将在本章的练习中学习如何分别执行这些操作。请注意,创建 ConfigMap 的命令格式如下:

kubectl create configmap <map-name> <data-source>

这里,<map-name> 是您想要分配给 ConfigMap 的名称,<data-source> 是要从中提取数据的目录、文件或文字值。

数据源对应于 ConfigMap 中的键值对,其中:

  • 是您在命令行上提供的文件名或键

  • 是您在命令行上提供的文件内容或文字值

在开始练习之前,让我们确保您的 Kubernetes 正在运行,并且您可以向其发出命令。我们将使用 minikube 在您的本地计算机上轻松运行单节点集群。

使用以下命令启动 minikube:

minikube start

当 minikube 启动时,您应该看到以下响应:

图 10.2:启动 minikube

图 10.2:启动 minikube

对于本章中的所有练习,我们建议创建一个新的命名空间。回想一下第五章Pods,命名空间是 Kubernetes 将解决方案组件分组在一起的方式。命名空间可以用于应用策略、配额,并且还可以用于分隔资源,如果相同的 Kubernetes 资源被不同的团队使用。

在下一个练习中,我们将使用 kubectl CLI 命令从文字值创建一个 ConfigMap。其思想是我们有一些配置数据(例如,主数据库名称),我们可以将其注入到 MySQL Pod 中,并且它将根据给定的环境变量创建数据库。这组命令也可以用于负责在多个环境中进行应用部署的自动化代码流水线中。

练习 10.01:从文字值创建 ConfigMap 并将其挂载到 Pod 上使用环境变量

在这个练习中,我们将在 Kubernetes 集群中创建一个 ConfigMap。此练习展示了如何使用键值模式创建 ConfigMaps。请按照以下步骤完成练习:

  1. 首先,让我们开始创建一个命名空间,用于本章中的所有练习。
kubectl create namespace configmap-test

您应该看到以下响应:

namespace/configmap-test created

注意

除非另有说明,我们将在本章的所有练习中使用 configmap-test 命名空间。

  1. 首先,让我们创建一个包含单个名称-值对的 ConfigMap。使用此处显示的命令:
kubectl create configmap singlevalue-map --from-literal=partner-url=https://www.auppost.com.au --namespace configmap-test 

您应该在终端中看到以下输出:

configmap/singlevalue-map created
  1. 创建 ConfigMap 后,让我们通过发出命令来获取命名空间中的所有 ConfigMap 来确认它是否已创建:
kubectl get configmaps --namespace configmap-test

由于singlevalue-mapconfigmap-test命名空间中唯一的 ConfigMap,您应该看到类似以下内容的输出:

NAME                 DATA     AGE
singlevalue-map      1        111s
  1. 让我们看看 Kubernetes ConfigMap 对象是什么样子的。输入以下 Kubernetes get命令:
kubectl get configmap singlevalue-map -o yaml --namespace configmap-test

完整对象应该描述为以下内容:

图 10.3:描述 singlevalue-map

图 10.3:描述 singlevalue-map

正如您在前述输出的第三行中所看到的,ConfigMap 已创建,并且我们输入的文字值作为键值对出现在 ConfigMap 的data部分中。

  1. 现在,我们将创建一个名为configmap-as-env.yaml的 YAML 文件,以创建一个 Pod,我们将从我们的 ConfigMap 中注入字段作为环境变量。使用您喜欢的文本编辑器,创建一个包含以下内容的 YAML 文件:
apiVersion: v1
kind: Pod
metadata:
  name: configmap-env-pod
spec:
  containers:
    - name: configmap-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "env" ]
      envFrom:
      - configMapRef:
          name: singlevalue-map

您可以看到前述文件中的envFrom部分正在从 ConfigMap 加载数据。

  1. 让我们根据前述规范创建一个 Pod。此 Pod 使用busybox容器映像,该映像运行在前述步骤中提到的 YAML 文件的command部分中指定的命令:
kubectl create -f configmap-as-env.yaml --namespace configmap-test

您应该看到以下输出:

pod/configmap-env-pod created
  1. 让我们使用以下命令检查此 Pod 的日志:
kubectl logs -f configmap-env-pod --namespace configmap-test

您应该看到如下所示的日志:

图 10.4:获取 configmap-env-pod 的日志

图 10.4:获取 configmap-env-pod 的日志

[ "/bin/sh", "-c", "env" ]命令将显示加载到 Pod 中的所有环境变量。在 ConfigMap 中,我们已将属性名称定义为partner-url,这是输出的一部分。

在这个练习中,环境变量的名称partner-url与我们的键值对中的键相同。我们还可以使环境变量的名称与键不同。例如,如果我们想要将partner-server-location作为我们环境变量的名称,我们可以用以下内容替换练习中 YAML 文件的内容:

apiVersion: v1
kind: Pod
metadata:
  name: configmap-multi-env-pod
spec:
  containers:
    - name: configmap-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "echo $(partner-server-location)"         ]
      env:
        - name: partner-server-location
          valueFrom:
            configMapKeyRef:
              name: singlevalue-map
              key: partner-url

特别注意前面 YAML 文件中的env部分。env后的第一个name字段定义了环境变量的名称,configMapKeyRef下的key字段定义了 ConfigMap 中的键的名称。

从文件定义一个 ConfigMap 并将其加载到 Pod 上

在这一部分,我们将从文件创建一个 ConfigMap,然后将文件加载到应用程序 Pod 上。如前所述,这个新挂载的文件将作为本地文件对 Pod 内运行的应用程序可访问。

当应用程序将其配置数据存储在外部时,这是很常见的,这样可以更容易地在不同环境中进行升级和容器镜像的补丁。我们可以在源代码控制存储库中拥有这样一个文件,并且使用 ConfigMap 在正确的容器中加载正确的文件。

让我们通过一个例子来理解这一点。想象一下,你编写了一个连接到数据库存储信息的 Web 应用程序。当你在开发环境部署应用程序时,你会想要连接到开发数据库。一旦你确信应用程序正常工作,你会想要将应用程序部署到测试环境。由于应用程序打包在一个容器中,你不希望改变容器来部署应用程序到测试环境。但是在测试环境中运行应用程序时,你需要连接到一个不同的数据库。一个简单的解决方案是配置你的应用程序从文件中读取数据库服务器的 URL,并且该文件可以通过 ConfigMap 挂载。这样,文件不作为容器的一部分打包,而是通过 Kubernetes 从外部注入;因此,你不需要修改你的容器化应用程序。另一个用例是外部软件供应商可以提供一个容器镜像,并且可以根据特定客户的要求挂载任何特定的配置设置到镜像上。

练习 10.02:从文件创建一个 ConfigMap

在这个练习中,我们将从文件创建一个 ConfigMap,然后可以挂载到任何 Pod 上:

  1. 首先,创建一个名为application.properties的文件,其中包含以下配置细节。你可以使用你喜欢的文本编辑器:
partner-url=https://www.fedex.com
partner-key=1234
  1. 现在,使用以下命令从文件创建一个 ConfigMap:
kubectl create configmap full-file-map --from-file=./application.properties --namespace configmap-test

你应该看到以下输出,表明 ConfigMap 已经被创建:

configmap/full-file-map created
  1. 获取所有 ConfigMaps 的列表以确认我们的 ConfigMap 已经创建:
kubectl get configmaps --namespace configmap-test

您应该看到所有 ConfigMaps 的列表,如下所示:

NAME               DATA      AGE
full-file-map      1         109m
singlevalue-map    1         127m

您可以看到 ConfigMaps 的名称与它们拥有的键的数量一起显示。

您可能会想知道,为什么这个输出只显示一个键,即使我们添加了两个键?让我们在下一步中理解这一点。

  1. 让我们看看如何使用以下命令存储 ConfigMap:
kubectl get configmap full-file-map -o yaml --namespace configmap-test

您应该看到以下输出:

图 10.5:获取完整文件映射的详细信息

图 10.5:获取完整文件映射的详细信息

请注意,文件名application.properties成为data部分下的key,整个文件负载是 key 的value

  1. 现在我们已经定义了我们的 ConfigMap,下一步是将其挂载到容器上。创建一个名为mount-configmap-as-volume.yaml的 YAML 文件,用以下内容作为我们的 Pod 配置:
apiVersion: v1
kind: Pod
metadata:
  name: configmap-test-pod
spec:
  containers:
    - name: configmap-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "ls /etc/appconfig/" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/appconfig
  volumes:
    - name: config-volume
      configMap:
        # Provide the name of the ConfigMap containing the           files you want
        # to add to the container
        name: full-file-map
  restartPolicy: Never

首先,让我们专注于前面文件中的volumes部分。在这个部分中,我们指示 Kubernetes 从名为full-file-map的 ConfigMap 中定义一个卷。

其次,在volumeMounts部分,我们定义 Kubernetes 应该在/etc/appconfig目录中挂载卷。

请注意容器中的command字段允许我们配置容器在启动时要执行的命令。在这个例子中,我们运行ls命令,这是一个列出目录内容的 Linux 命令。这类似于 Windows 的dir命令。这将打印出我们挂载了 ConfigMap 的/etc/appconfig目录的内容。

注意

volumevolumeMounts部分下的name字段必须相同,以便 Kubernetes 可以识别哪个volume与哪个volumeMounts相关联。

  1. 现在,使用以下命令使用我们刚刚创建的 YAML 文件启动一个 Pod:
kubectl create -f mount-configmap-as-volume.yaml --namespace configmap-test

您应该收到一个回复,说 Pod 已经创建:

pod/configmap-test-pod created
  1. 我们使用的 YAML 文件指定了 Pod 的名称为configmap-test-pod,并配置它只显示文件夹的内容。要验证这一点,只需发出以下命令以获取 Pod 的输出日志:
kubectl logs -f configmap-test-pod --namespace configmap-test

这应该打印出application.properties,这是我们放在文件夹中的文件:

application.properties

如您所见,我们得到了/etc/appconfig的内容,这是 Pod 中ls命令的输出。

您刚刚成功定义了一个 ConfigMap,并将其作为文件挂载到一个打印文件名的 Pod 中。

练习 10.03:从文件夹创建 ConfigMap

在这个练习中,我们将所有文件加载到一个文件夹中作为 ConfigMap。每个文件名都成为 ConfigMap 的一个键,当您挂载它时,所有文件都将挂载到volumeMounts位置(如在 YAML 文件中为容器定义的):

  1. 在一个新文件夹中创建两个文件。将其中一个命名为fileone.txt,其内容为file one,将另一个命名为filetwo.txt,其内容为file two。此练习的文件夹名称可以是任意的。您可以使用ls命令确认已创建文件:
ls

您将看到以下文件列表:

fileone.txt     filetwo.txt
  1. 使用以下命令从文件夹创建 ConfigMap。请注意,我们只提到文件夹的名称,而不是指定文件名:
kubectl create configmap map-from-folder --from-file=./ -n configmap-test

您应该看到以下响应:

configmap/map-from-folder created
  1. 现在,让我们描述一下 ConfigMap,看看它包含什么:
kubectl describe configmap map-from-folder -n configmap-test

您应该看到以下输出:

图 10.6:描述了从文件夹创建的 ConfigMap

图 10.6:描述了从文件夹创建的 ConfigMap

请注意,ConfigMap 中有两个键-每个文件一个,即fileone.txtfiletwo.txt。键的值是文件的内容。因此,我们可以看到可以从文件夹中的所有文件创建 ConfigMap。

什么是秘密?

ConfigMap 提供了一种将应用程序配置数据与应用程序本身解耦的方法。但是,ConfigMap 的问题在于它将数据以明文形式存储为 Kubernetes 对象。如果我们想要存储一些敏感数据,例如数据库密码,该怎么办?Kubernetes Secret 提供了一种存储敏感数据的方式,然后可以将其提供给需要的应用程序。

秘密与 ConfigMap

您可以将秘密视为与 ConfigMap 相同,具有以下区别:

  1. 与 ConfigMap 不同,秘密旨在存储少量(秘密为 1 MB)敏感数据。秘密是base64编码的,因此我们不能将其视为安全。它还可以存储二进制数据,例如公钥或私钥。

  2. Kubernetes 确保只将秘密传递给运行需要相应秘密的 Pod 的节点。

注意

存储敏感数据的另一种方法是使用诸如 HashiCorp Vault 之类的保险库解决方案。我们已经将这样的实现超出了研讨会的范围。

但是等等;如果 Kubernetes Secrets 由于它们的 base64 编码而不够安全,那么存储极其敏感的数据的解决方案是什么?一种方法是对其进行加密,然后将其存储在 Secrets 中。数据可以在加载到 Pod 时进行解密,尽管我们将此实现范围之外的实现。

一旦我们定义了我们的 Secrets,我们需要将它们暴露给应用程序 Pods。我们将 Secrets 暴露给运行的应用程序的方式与 ConfigMaps 相同,即通过将它们挂载为环境变量或文件。

与 ConfigMaps 一样,让我们使用 Kubernetes 提供的secret的内置help命令来查看提供的 Secrets 的类型:

kubectl create secret --help

help命令应该显示如下内容:

图 10.7:Secret 的内置帮助命令的输出

图 10.7:Secret 的内置帮助命令的输出

如您在前面的输出中所见,Available Commands部分列出了三种类型的 Secrets:

  • generic:通用 Secret 包含任何自定义键值对。

  • tls:TLS Secret 是一种用于使用 TLS 协议进行通信的公钥-私钥对的特殊类型的 Secret。

  • docker-registry:这是一种特殊类型的 Secret,用于存储访问 Docker 注册表的用户名、密码和电子邮件地址。

我们将在接下来的练习中深入探讨这些 Secrets 的实现和用途。

练习 10.04:从文字值定义一个 Secret,并将值加载到 Pod 作为环境变量

在这个练习中,我们将从文字值定义一个 Secret,并将其作为环境变量加载到运行中的 Kubernetes Pod 中。这个文字值可能是您内部数据库的密码。由于我们从文字值创建这个 Secret,它将被归类为通用 Secret。按照以下步骤执行练习:

  1. 首先,使用以下命令创建一个将保存简单密码的 Secret:
kubectl create secret generic test-secret --from-literal=password=secretvalue --namespace configmap-test

您应该得到以下响应:

secret/test-secret created
  1. 一旦我们定义了我们的 Secret,我们可以使用 Kubernetes 的describe命令获取更多关于它的详细信息:
kubectl describe secret test-secret --namespace configmap-test

图 10.8:描述 test-secret

图 10.8:描述 test-secret

您可以看到它将我们的值存储在password键下:

  1. 现在我们的 Secret 已创建,我们将在 Pod 中将其挂载为环境变量。要创建一个 Pod,请创建一个名为mount-secret-as-env.yaml的 YAML 文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: secret-env-pod
spec:
  containers:
    - name: secret-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "env" ]
      envFrom:
      - secretRef:
          name: test-secret

注意envFrom部分,其中提到要加载的秘密。在容器的command部分中,我们指定了env命令,这将使容器显示加载到 Pod 中的所有环境变量。

  1. 现在,让我们使用 YAML 配置来创建一个 Pod,并看它的运行情况:
kubectl create -f mount-secret-as-env.yaml --namespace=configmap-test

您应该会看到以下类似的回应:

pod/secret-env-pod created
  1. 现在,让我们获取 Pod 的日志,以查看容器显示的所有环境变量:
kubectl logs -f secret-env-pod --namespace=configmap-test

您应该会看到类似以下截图的日志:

图 10.9:从 secret-env-pod 获取日志

图 10.9:从 secret-env-pod 获取日志

正如您在前面的输出的突出显示行中所看到的,password键显示为secretvalue,这就是我们指定的值。

以下练习演示了如何使用公私钥组合,并将私钥文件挂载到 Pod 中。然后,公钥可以提供给连接到该 Pod 的任何其他服务,但本练习中没有演示。使用一个单独的文件作为秘密使我们能够使用任何类型的文件,而不仅仅是简单的键值字符串。这就打开了使用私钥存储等二进制文件的可能性。

练习 10.05:从文件定义一个秘钥,并将值作为文件加载到 Pod 上

在这个练习中,我们将创建一个私钥,将其存储在一个新的秘密中,然后将其加载到一个 Pod 中作为一个文件:

  1. 首先,让我们创建一个私钥。我们将使用一个用于创建 SSH 密钥的工具。在终端中输入以下命令:
ssh-keygen -f ~/test_rsa -t rsa -b 4096 -C "test@example.com"

如果提示,请不要为密钥提供任何密码。

注意

如果您需要更多关于 SSH 协议及其用途的信息,请参考www.ssh.com/ssh/protocol/

执行成功后,您将看到两个名为test_rsatest_rsa.pub的文件。您应该会看到类似于这里显示的输出:

图 10.10:创建 SSH 密钥

图 10.10:创建 SSH 密钥

您的输出可能与此处显示的不完全相同,因为密钥是随机的。

注意

大多数 Linux 发行版都包括ssh-keygen工具。但是,如果您没有或无法使用ssh-keygen,您可以使用任何其他文件代替私钥来进行此练习。

  1. 现在,让我们将新创建的私钥加载为一个秘密。这次,我们将使用create secret命令的from-file参数:
kubectl create secret generic test-key-secret --from-file=private-key=/Users/faisalmassod/test_rsa --namespace=configmap-test

您应该会得到这样的回应:

secret/test-key-secret created
  1. 创建了 Secret 之后,我们可以使用describe命令获取其详细信息:
kubectl describe secret test-key-secret --namespace=configmap-test

秘密应该描述如下:

图 10.11:描述 test-key-secret

图 10.11:描述 test-key-secret

  1. 现在我们的 Secret 已经创建,让我们将其挂载到一个 Pod 上。这个过程类似于挂载 ConfigMap。首先,创建一个名为mount-secret-as-volume.yaml的 YAML 文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: secret-test-pod
spec:
  containers:
    - name: secret-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "ls /etc/appconfig/; cat         /etc/appconfig/private-key" ]
      volumeMounts:
      - name: secret-volume
        mountPath: /etc/appconfig
  volumes:
    - name: secret-volume
      secret:
        # Provide the name of the Secret containing the files           you want
        # to add to the container
        secretName: test-key-secret

在前面的 Pod 规范中,请注意volumes的挂载方式与我们之前挂载 ConfigMap 的方式相同。在volumes部分,我们指示 Kubernetes 从我们的 Secret 中定义一个卷。在volumeMounts部分,我们定义了 Kubernetes 应该将卷挂载到的特定路径。"/bin/sh", "-c", "ls /etc/appconfig/; cat /etc/appconfig/private-key"命令将打印出作为 Secret 加载到其中的文件的内容。

注意

volumevolumeMounts部分的name字段必须相同,这样 Kubernetes 才能识别哪个volume与哪个volumeMounts相关联。在本例中,我们在两个地方都使用了secret-volume作为名称。

  1. 现在,让我们使用以下命令使用 YAML 文件作为 Pod 定义来创建一个 Pod:
kubectl create -f mount-secret-as-volume.yaml --namespace=configmap-test

如果 Pod 成功创建,您应该会看到以下输出:

pod/secret-test-pod created
  1. 要检查我们的 Pod 是否加载了 Secret,我们可以获取其日志并检查它们。使用以下命令:
kubectl logs -f secret-test-pod --namespace=configmap-test

日志应该显示私钥的内容,如下所示:

图 10.12:获取 secret-test-pod 的日志

图 10.12:获取 secret-test-pod 的日志

从日志中可以看出,容器显示了挂载到 Pod 上的 Secret 的内容。

注意

由于 SSH 密钥是随机的,您的输出可能与此处显示的内容不完全相同。

  1. SSH 密钥是随机的,因此每次您都会得到不同的输出。您可以尝试多次进行此练习并自行查看。确保每次要么删除 Pod,要么更改名称。您可以使用以下命令删除 Pod:
kubectl delete pod secret-test-pod --namespace=configmap-test

如果 Pod 成功删除,您将看到以下输出:

pod "secret-test-pod" deleted

在这个练习中,我们使用另一个工具创建了一个密钥对,并将私钥加载到我们的 Pod 中,将其作为二进制文件进行挂载。然而,在 TLS 协议中,公私钥对用于加密,这是一种用于保护网络流量的加密标准。

注意

要了解有关 TLS 的更多信息,请参考www.cloudflare.com/learning/ssl/transport-layer-security-tls/

Kubernetes 提供了自己的方式来创建密钥对并存储 TLS 密钥。让我们看看如何在以下练习中创建 TLS Secret。

练习 10.06:创建 TLS 密钥

在这个练习中,我们将看到如何创建一个可以存储 TLS 加密密钥的 Secret:

  1. 使用以下命令创建一对私钥-公钥:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=kube.example.com"

此命令将在名为tls.key的文件中创建私钥,并在名为tls.crt的文件中创建公钥证书。

注意

有关此处如何使用openssl工具的更多详细信息,请参考www.openssl.org/docs/manmaster/man1/req.html

如果密钥成功生成,您应该看到以下输出:

图 10.13:创建 SSL 密钥

图 10.13:创建 SSL 密钥

  1. 一旦成功,我们可以使用以下命令创建一个 Secret 来保存文件:
kubectl create secret tls test-tls --key="tls.key" --cert="tls.crt" --namespace=configmap-test

一旦 Secret 成功创建,您将看到以下输出:

secret/test-tls created
  1. 使用以下命令列出configmap-test命名空间中的所有 Secrets,以验证我们的 Secret 是否已创建:
kubectl get secrets --namespace configmap-test

我们的 Secret 必须列在以下输出中:

图 10.14:列出 configmap-test 中的所有 Secrets

图 10.14:列出 configmap-test 中的所有 Secrets

  1. 如果我们对新创建的 Secret 发出describe命令,您会看到它将两部分,即公钥和私钥,存储为 Secret 的两个不同键:
kubectl describe secrets test-tls --namespace configmap-test

您应该看到以下响应:

图 10.15:描述 test-tls

图 10.15:描述 test-tls

因此,我们使用 Kubernetes 提供的一组特殊命令创建了一组 TLS 的公钥-私钥。这个 Secret 可以以与练习 10.05中演示的类似方式挂载,即从文件定义 Secret 并将值加载到 Pod 中作为文件

另一个常见的任务是从外部 Docker 注册表中获取 Docker 镜像。许多组织使用企业容器注册表(例如 Nexus)来存储其应用程序,然后根据需要获取和部署。Kubernetes 还提供了一种特殊类型的 Secret 来存储访问这些 Docker 注册表的身份验证信息。让我们看看如何在以下练习中实现它。

练习 10.07:创建一个 docker-registry Secret

在这个练习中,我们将创建一个docker-registry Secret,用于在从注册表获取 Docker 镜像时进行身份验证:

  1. 我们可以直接使用以下命令创建 Secret:
kubectl create secret docker-registry test-docker-registry-secret --docker-username=test --docker-password=testpassword --docker-email=example@a.com --namespace configmap-test

正如你在命令参数中所看到的,我们需要指定 Docker 帐户的用户名、密码和电子邮件地址。一旦 Secret 被创建,你应该会看到以下响应:

secret/test-docker-registry-secret created
  1. 通过使用以下命令来验证它是否已创建:
kubectl get secrets test-docker-registry-secret --namespace configmap-test

你应该看到test-docker-registry-secret如下输出所示:

图 10.16:检查 test-docker-registry-secret

图 10.16:检查 test-docker-registry-secret

  1. 让我们使用describe命令并获取有关我们的 Secret 的更多详细信息:
kubectl describe secrets test-docker-registry-secret --namespace configmap-test

该命令应返回以下详细信息:

图 10.17:描述 test-docker-registry-secret

图 10.17:描述 test-docker-registry-secret

正如你在前面的输出的Data部分中所看到的,已创建了一个名为.dockerconfigjson的单个键。

注意

这个练习只是一种加载.dockerconfigjson文件的简单方法。你可以使用其他方法手动创建和加载文件,并实现我们在这个练习中的相同目标。

活动 10.01:使用 ConfigMap 和 Secret 来推广应用程序通过不同阶段

假设我们有一个应用程序,我们想要将其推广到不同的环境。你的任务是将应用程序从测试环境推广到生产环境,每个环境都有不同的配置数据。

在这个活动中,我们将使用 ConfigMap 和 Secret 来轻松地为应用程序在其生命周期的不同阶段重新配置。这也应该让你了解到,将 ConfigMap 数据和 Secret 数据与应用程序分离可以帮助应用程序更容易地在开发和部署的各个阶段之间过渡。

这些指南应该帮助你完成这个活动:

  1. 定义一个名为my-app-test的命名空间。

  2. my-app-test命名空间中定义一个名为my-app-data的 ConfigMap,并具有以下键值:

external-system-location=https://testvendor.example.com
external-system-basic-auth-username=user123
  1. my-app-test命名空间中定义一个名为my-app-secret的 Secret,并具有以下键值:
external-system-basic-auth-password=password123
  1. 定义一个 Pod 规范,并在/etc/app-data文件夹中部署 ConfigMap,文件名为application-data.properties

  2. 定义一个 Pod 规范,并在/etc/secure-data文件夹中部署 Secret,文件名为application-secure.properties

  3. 运行 Pod,以便它显示 ConfigMap 和 Secret 中的所有内容。你应该看到类似这样的东西:图 10.18:测试环境的键值

图 10.18:测试环境的键值

  1. 定义另一个名为my-app-production的命名空间。

  2. my-app-production中定义一个名为my-app-data的 ConfigMap,具有以下键值:

external-system-location=https://vendor.example.com
external-system-basic-auth-username=activityapplicationuser
  1. my-app-production中定义一个名为my-app-secret的 Secret,具有以下键值:
external-system-basic-auth-password=A#4b*(1=B88%tFr3
  1. 使用与步骤 5中定义的相同的 Pod 规范,并在my-app-production命名空间中运行 Pod。

  2. 检查在my-app-production中运行的应用程序是否显示了正确的数据。你应该看到类似这样的输出:图 10.19:生产环境的键值

图 10.19:生产环境的键值

注意

这个活动的解决方案可以在以下地址找到:packt.live/304PEoD。GitHub 存储库还包括了一个用于此活动的 Bash 脚本,它将自动执行所有这些解决方案步骤。但是,请查看解决方案中提供的详细步骤,以完全了解如何执行该活动。

摘要

在本章中,我们已经看到 Kubernetes 提供的不同方式,用于将环境特定数据与作为容器运行的应用程序相关联。

Kubernetes 提供了将敏感数据存储为 Secrets 和将普通应用程序数据存储为 ConfigMaps 的方式。我们还看到了如何通过 CLI 创建 ConfigMaps 和 Secrets,并将它们与我们的容器关联起来。通过命令行运行所有内容将有助于自动化这些步骤,并提高应用程序的整体灵活性。

将数据与容器关联使我们能够在 IT 系统的不同环境中使用相同的容器(例如,在测试和生产环境中)。在不同环境中使用相同的容器为 IT 流程提供了安全和可信的代码推广技术。每个团队都可以将容器作为部署单元,并对容器进行签名,以便其他方可以信任该容器。这也为分发代码提供了一种可信赖的方式,不仅可以在同一 IT 组织内部,还可以跨多个组织之间进行。例如,软件供应商可以直接向您提供打包软件的容器。然后可以使用 ConfigMaps 和 Secrets 来提供特定的配置,以在您的组织中使用打包软件。

接下来的章节都是关于部署 Kubernetes 并在高可用模式下运行它。这些章节将为您提供关于如何运行稳定的 Kubernetes 集群的基本和实用知识。

第十一章: 构建您自己的 HA 集群

概述

在本章中,我们将学习 Kubernetes 如何使我们能够部署具有显著弹性的基础设施,以及如何在 AWS 云中设置一个高可用性的 Kubernetes 集群。本章将帮助您了解是什么使 Kubernetes 能够用于高可用性部署,并帮助您在为您的用例设计生产环境时做出正确的选择。在本章结束时,您将能够在 AWS 上设置一个适当的集群基础设施,以支持您的高可用性(HA)Kubernetes 集群。您还将能够在生产环境中部署应用程序。

介绍

在之前的章节中,您了解了应用程序容器化、Kubernetes 的工作原理,以及 Kubernetes 中的一些“专有名词”或“对象”,这些对象允许您创建一种声明式的应用程序架构,Kubernetes 将代表您执行。

软件和硬件的不稳定在所有环境中都是现实。随着应用程序对更高可用性的需求越来越高,基础设施的缺陷变得更加明显。Kubernetes 是专门为帮助解决容器化应用程序的这一挑战而构建的。但是 Kubernetes 本身呢?作为集群操作员,我们是不是要从像鹰一样监视我们的单个服务器,转而监视我们的单个 Kubernetes 控制基础设施呢?

事实证明,这一方面是 Kubernetes 设计考虑的一个方面。Kubernetes 的设计目标之一是能够经受住其自身基础设施的不稳定性。这意味着当正确设置时,Kubernetes 控制平面可以经受相当多的灾难,包括:

  • 网络分裂/分区

  • 控制平面(主节点)服务器故障

  • etcd 中的数据损坏

  • 许多其他影响可用性的不太严重的事件

不仅可以 Kubernetes 帮助您的应用程序容忍故障,而且您可以放心,因为 Kubernetes 也可以容忍其自身控制基础设施的故障。在本章中,我们将建立一个属于我们自己的集群,并确保它具有高可用性。高可用性意味着系统非常可靠,几乎总是可用的。这并不意味着其中的一切总是完美运行;它只意味着每当用户或客户端需要某些东西时,架构规定 API 服务器应该“可用”来完成工作。这意味着我们必须为我们的应用程序设计一个系统,以自动响应并对任何故障采取纠正措施。

在本章中,我们将看看 Kubernetes 如何整合这些措施来容忍其自身控制架构中的故障。然后,您将有机会进一步扩展这个概念,通过设计您的应用程序来利用这种横向可扩展、容错的架构。但首先,让我们看看机器中不同齿轮如何一起转动,使其具有高可用性。

Kubernetes 组件如何一起实现高可用性

您已经在《第二章》《Kubernetes 概述》中学到了 Kubernetes 的各个部分是如何一起工作,为您的应用程序容器提供运行时的。但我们需要更深入地研究这些组件如何一起实现高可用性。为了做到这一点,我们将从 Kubernetes 的内存库,也就是 etcd 开始。

etcd

正如您在之前的章节中学到的,etcd 是存储所有 Kubernetes 配置的地方。这使得它可以说是集群中最重要的组件,因为 etcd 中的更改会影响一切的状态。更具体地说,对 etcd 中的键值对的任何更改都会导致 Kubernetes 的其他组件对此更改做出反应,这可能会导致对您的应用程序的中断。为了实现 Kubernetes 的高可用性,最好有多个 etcd 节点。

但是,当您将多个节点添加到像 etcd 这样的最终一致性数据存储中时,会出现更多的挑战。您是否必须向每个节点写入以保持状态的更改?复制是如何工作的?我们是从一个节点读取还是尽可能多地读取?它如何处理网络故障和分区?谁是集群的主节点,领导者选举是如何工作的?简短的答案是,通过设计,etcd 使这些挑战要么不存在,要么易于处理。etcd 使用一种称为Raft的共识算法来实现复制和容错,以解决上述许多问题。因此,如果我们正在构建一个 Kubernetes 高可用性集群,我们需要确保正确设置多个节点(最好是奇数,以便更容易进行领导者选举)的 etcd 集群,并且我们可以依靠它。

注意

etcd 中的领导者选举是一个过程,数据库软件的多个实例共同投票,决定哪个主机将成为处理实现数据库一致性所需的任何问题的权威。有关更多详细信息,请参阅此链接:raft.github.io/

网络和 DNS

许多在 Kubernetes 上运行的应用程序都需要某种形式的网络才能发挥作用。因此,在为您的集群设计拓扑时,网络是一个重要考虑因素。例如,您的网络应该能够支持应用程序使用的所有协议,包括 Kubernetes 使用的协议。Kubernetes 本身在主节点、节点和 etcd 之间的所有通信都使用 TCP,它还使用 UDP 进行内部域名解析,也就是服务发现。您的网络还应该配置为至少具有与您计划在集群中拥有的节点数量一样多的 IP 地址。例如,如果您计划在集群中拥有超过 256 台机器(节点),那么您可能不应该使用/24 或更高的 IP CIDR 地址空间,因为这样只有 255 个或更少的可用 IP 地址。

在本次研讨会的后续部分,我们将讨论作为集群操作员需要做出的安全决策。然而,在本节中,我们不会讨论这些问题,因为它们与 Kubernetes 实现高可用性的能力没有直接关系。我们将在 第十三章 Kubernetes 中的运行时和网络安全 中处理 Kubernetes 的安全性。

最后要考虑的一件事是你的主节点和工作节点所在的网络,即每个主节点都应该能够与每个工作节点通信。这一点很重要,因为每个主节点都要与工作节点上运行的 Kubelet 进程通信,以确定整个集群的状态。

节点和主服务器的位置和资源

由于 etcd 的 Raft 算法的设计,它允许 Kubernetes 的键值存储中发生分布式一致性,我们能够运行多个主节点,每个主节点都能够控制整个集群,而不必担心它们会独立行动(换句话说,变得不受控制)。提醒一下,主节点不同步在 Kubernetes 中是一个问题,考虑到你的应用程序的运行时是由 Kubernetes 代表你发出的命令来控制的。如果由于主节点之间的状态同步问题而导致这些命令发生冲突,那么你的应用程序运行时将受到影响。通过引入多个主节点,我们再次提供了对可能危及集群可用性的故障和网络分区的抵抗力。

Kubernetes 实际上能够以“无头”模式运行。这意味着 Kubelets(工作节点)最后从主节点接收的任何指令都将继续执行,直到可以重新与主节点通信。理论上,这意味着部署在 Kubernetes 上的应用程序可以无限期地运行,即使整个控制平面(所有主节点)崩溃,应用程序所在的工作节点上的 Pods 没有发生任何变化。显然,这是集群可用性的最坏情况,但令人放心的是,即使在最坏的情况下,应用程序不一定会遭受停机时间。

当您计划设计和容量高可用性部署 Kubernetes 时,重要的是要了解一些关于您的网络设计的事情,我们之前讨论过。例如,如果您在流行的云提供商中运行集群,它们可能有“可用区”的概念。数据中心环境的类似概念可能是物理隔离的数据中心。如果可能的话,每个可用区应至少有一个主节点和多个工作节点。这很重要,因为在可用区(数据中心)停机的情况下,您的集群仍然能够在剩余的可用区内运行。这在以下图表中有所说明:

图 11.1:可用区停机前的集群

图 11.1:可用区停机前的集群

假设可用区 C 完全停机,或者至少我们不再能够与其中运行的任何服务器进行通信。现在集群的行为如下:

图 11.2:可用区停机后的集群

图 11.2:可用区停机后的集群

正如您在图表中所看到的,Kubernetes 仍然可以执行。此外,如果在可用区 C 中运行的节点的丢失导致应用程序不再处于其期望的状态,这是由应用程序的 Kubernetes 清单所决定的,剩余的主节点将工作以在剩余的工作节点上安排中断的工作负载。

注意

根据您的 Kubernetes 集群中工作节点的数量,您可能需要计划额外的资源约束,因为运行连接到多个工作节点的主节点所需的 CPU 功率。您可以使用此链接中的图表来确定应该部署用于控制您的集群的主节点的资源要求:kubernetes.io/docs/setup/best-practices/cluster-large/

容器网络接口和集群 DNS

关于您的集群,您需要做出的下一个决定是容器本身如何在每个节点之间进行通信。Kubernetes 本身有一个容器网络接口称为kubenet,这是我们在本章中将使用的。

对于较小的部署和简单的操作,从容器网络接口(CNI)的角度来看,kubenet 已经超出了这些集群的需求。然而,它并不适用于每种工作负载和网络拓扑。因此,Kubernetes 提供了对几种不同 CNI 的支持。在考虑容器网络接口的高可用性时,您会希望选择性能最佳且稳定的选项。本文介绍 Kubernetes 的范围超出了讨论每种 CNI 提供的内容。

注意

如果您计划使用托管的 Kubernetes 服务提供商或计划拥有更复杂的网络拓扑,比如单个 VPC 内的多个子网,kubenet 将无法满足您的需求。在这种情况下,您将不得不选择更高级的选项。有关选择适合您环境的正确 CNI 的更多信息,请参阅此处:chrislovecnm.com/kubernetes/cni/choosing-a-cni-provider/

容器运行时接口

您将不得不做出的最终决定之一是您的容器将如何在工作节点上运行。Kubernetes 的默认选择是 Docker 容器运行时接口,最初 Kubernetes 是为了与 Docker 配合而构建的。然而,自那时以来,已经开发了开放标准,其他容器运行时接口现在与 Kubernetes API 兼容。一般来说,集群操作员倾向于坚持使用 Docker,因为它非常成熟。即使您想探索其他选择,也请记住,在设计能够维持工作负载和 Kubernetes 高可用性的拓扑时,您可能会选择更成熟和稳定的选项,比如 Docker。

注意

您可以在此页面找到与 Kubernetes 兼容的其他一些容器运行时接口:kubernetes.io/docs/setup/production-environment/container-runtimes/

容器存储接口

最近的 Kubernetes 版本引入了与数据中心和云提供商中可用的持久性工具进行交互的改进方法,例如存储阵列和 blob 存储。最重要的改进是引入和标准化了用于管理 Kubernetes 中的StorageClassPersistentVolumePersistentVolumeClaim的容器存储接口。对于高可用集群的考虑,您需要针对每个应用程序做出更具体的存储决策。例如,如果您的应用程序使用亚马逊 EBS 卷,这些卷必须驻留在一个可用区内,那么您将需要确保工作节点具有适当的冗余,以便在发生故障时可以重新安排依赖于该卷的 Pod。有关 CSI 驱动程序和实现的更多信息,请访问:kubernetes-csi.github.io/docs/

构建一个以高可用性为重点的 Kubernetes 集群

希望通过阅读前面的部分,您开始意识到当您首次接触这个主题时,Kubernetes 并不像看起来那么神奇。它本身是一个非常强大的工具,但当我们充分利用其在高可用配置中运行的能力时,Kubernetes 真正发挥作用。现在我们将看到如何实施它,并实际使用集群生命周期管理工具构建一个集群。但在我们这样做之前,我们需要了解我们可以部署和管理 Kubernetes 集群的不同方式。

自管理与供应商管理的 Kubernetes 解决方案

亚马逊网络服务,谷歌云平台,微软 Azure,以及几乎所有其他主要的云服务提供商都提供了托管的 Kubernetes 解决方案。因此,当您决定如何构建和运行您的集群时,您应该考虑一些不同的托管提供商及其战略性的提供,以确定它们是否符合您的业务需求和目标。例如,如果您使用亚马逊网络服务,那么 Amazon EKS 可能是一个可行的解决方案。

选择托管服务提供商而不是开源和自我管理的解决方案存在一些权衡。例如,很多集群组装的繁重工作都已经为您完成,但在这个过程中您放弃了很多控制权。因此,您需要决定您对能够控制 Kubernetes 主平面有多少价值,以及您是否希望能够选择您的容器网络接口或容器运行时接口。出于本教程的目的,我们将使用开源解决方案,因为它可以部署在任何地方,并且还可以帮助我们理解 Kubernetes 的工作原理以及应该如何配置。

注意

请确保您拥有 AWS 账户并能够使用 AWS CLI 访问:aws.amazon.com/cli

如果您无法访问它,请按照上面的链接中的说明操作。

假设我们现在想要对我们的集群有更多的控制,并且愿意自己管理它,让我们看一些可以用于设置集群的开源工具。

kops

我们将使用一个更受欢迎的开源安装工具来完成这个过程,这个工具叫做kops,它代表Kubernetes Operations。它是一个完整的集群生命周期管理工具,并且具有非常易于理解的 API。作为集群创建/更新过程的一部分,kops 可以生成 Terraform 配置文件,因此您可以将基础设施升级过程作为自己流程的一部分运行。它还具有良好的工具支持 Kubernetes 版本之间的升级路径。

注意

Terraform 是一个基础设施生命周期管理工具,我们将在下一章中简要了解。

kops 的一些缺点是它往往落后于 Kubernetes 的两个版本,它并不总是能够像其他工具那样快速响应漏洞公告,并且目前仅限于在 AWS、GCP 和 OpenStack 中创建集群。

我们决定在本章中使用 kops 来管理我们的集群生命周期的原因有四个:

  • 我们希望选择一个工具,可以将一些更令人困惑的 Kubernetes 设置抽象化,以便让您更容易进行集群管理。

  • 它支持的云平台不仅仅是 AWS,因此如果您选择不使用亚马逊,您不必被锁定在亚马逊上。

  • 它支持对 Kubernetes 基础设施进行广泛的定制,例如选择 CNI 提供程序、决定 VPC 网络拓扑和节点实例组定制。

  • 它对零停机集群版本升级有一流的支持,并自动处理该过程。

其他常用工具

除了 kops 之外,还有其他几种工具可以用来设置 Kubernetes 集群。您可以在此链接找到完整的列表:kubernetes.io/docs/setup/#production-environment

我们在这里提到其中一些,以便您了解有哪些可用的工具:

  • kubeadm:这是从 Kubernetes 源代码生成的工具,它将允许对 Kubernetes 的每个组件进行最大程度的控制。它可以部署在任何环境中。

使用 kubeadm 需要对 Kubernetes 有专家级的了解才能发挥作用。它给集群管理员留下了很少的错误空间,并且使用 kubeadm 升级集群是复杂的。

  • Kubespray:这使用 Ansible/Vagrant 风格的配置管理,这对许多 IT 专业人士来说是熟悉的。它更适用于基础设施更为静态而非动态的环境(如云)。Kubespray 非常可组合和可配置,从工具的角度来看。它还允许在裸机服务器上部署集群。关键是要注意协调集群组件和硬件和操作系统的软件升级。由于您提供了云提供商所做的许多功能,您必须确保您的升级过程不会破坏运行在集群之上的应用程序。

因为 Kubespray 使用 Ansible 进行配置,您受到了用于配置大型集群并保持其规范性的 Ansible 底层限制的限制。目前,Kubespray 仅限于以下环境:AWS、GCP、Azure、OpenStack、vSphere、Packet、Oracle Cloud Infrastructure 或您自己的裸机安装。

Kubernetes 中的身份验证和身份

Kubernetes 使用两个概念进行身份验证:ServiceAccounts 用于标识在 Pods 内运行的进程,而 User Accounts 用于标识人类用户。我们将在本章的后续主题中查看 ServiceAccounts,但首先让我们了解 User Accounts。

从一开始,Kubernetes 一直试图对用户帐户的任何形式的身份验证和身份保持非常中立,因为大多数公司都有一种非常特定的用户身份验证方式。有些使用 Microsoft Active Directory 和 Kerberos,有些可能使用 Unix 密码和 UGW 权限集,有些可能使用云提供商或基于软件的 IAM 解决方案。此外,组织可能使用多种不同的身份验证策略。

因此,Kubernetes 没有内置的身份管理或必需的身份验证方式。相反,它有身份验证“策略”的概念。策略本质上是 Kubernetes 将身份验证的验证委托给另一个系统或方法的方式。

在本章中,我们将使用基于 x509 证书的身份验证。X509 证书身份验证基本上利用了 Kubernetes 证书颁发机构和通用名称/组织名称。由于 Kubernetes RBAC 规则使用用户名组名将经过身份验证的身份映射到权限集,x509通用名称成为 Kubernetes 的用户名,而组织名称成为 Kubernetes 中的组名。kops 会自动为您提供基于 x509 的身份验证证书,因此几乎不用担心;但是当涉及添加自己的用户时,您需要注意这一点。

注意

Kubernetes RBAC 代表基于角色的访问控制,它允许我们根据用户的角色允许或拒绝对某些访问的访问。这将在第十三章《Kubernetes 中的运行时和网络安全》中更深入地介绍。

kops 的一个有趣特性是,你可以像使用 kubectl 管理集群资源一样使用它来管理集群资源。kops 处理节点的方式类似于 Kubernetes 处理 Pod 的方式。就像 Kubernetes 有一个名为“Deployment”的资源来管理一组 Pods,kops 有一个名为InstanceGroup的资源(也可以用它的简写形式ig)来管理一组节点。在 AWS 的情况下,kops InstanceGroup 实际上创建了一个 AWS EC2 自动扩展组。

扩展这个比较,kops get instancegroupskops get ig类似于kubectl get deploymentskops edit的工作方式类似于kubectl edit。我们将在本章后面的活动中使用这个功能,但首先,让我们在下面的练习中启动和运行我们的基本 HA 集群基础设施。

注意

在本章中,命令是使用 Zsh shell 运行的。但是,它们与 Bash 完全兼容。

练习 11.01:设置我们的 Kubernetes 集群

注意

这个练习将超出 AWS 免费套餐的范围,该套餐通常赠送给新账户持有者的前 12 个月。EC2 的定价信息可以在这里找到:aws.amazon.com/ec2/pricing/

此外,您应该记得在本章结束时删除您的实例,以停止对您消耗的 AWS 资源进行计费。

在这个练习中,我们将准备在 AWS 上运行 Kubernetes 集群的基础设施。选择 AWS 并没有什么特别之处;Kubernetes 是平台无关的,尽管它已经有了允许它与本地 AWS 服务(EBS、EC2 和 IAM)集成的代码,代表集群运营商。这对于 Azure、GCP、IBM Cloud 和许多其他云平台也是如此。

我们将建立一个具有以下规格的集群:

  • 三个主节点

  • 三个 etcd 节点(为了简单起见,我们将在主节点上运行这些节点)

  • 两个工作节点

  • 至少两个可用区

一旦我们设置好了我们的集群,我们将在下一个练习中在其上部署一个应用程序。现在按照以下步骤完成这个练习:

  1. 确保您已按前言中的说明安装了 kops。使用以下命令验证 kops 是否已正确安装和配置:
kops version

您应该看到以下响应:

Version 1.15.0 (git-9992b4055)

现在在我们继续以下步骤之前,我们需要在 AWS 中进行一些设置。以下大部分设置都是可配置的,但为了方便起见,我们将为您做出一些决定。

  1. 首先,我们将设置一个 AWS IAM 用户,kops 将用它来提供您的基础设施。在您的终端中依次运行以下命令:
aws iam create-group --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops
aws iam create-user --user-name kops
aws iam add-user-to-group --user-name kops --group-name kops
aws iam create-access-key --user-name kops

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

图 11.3:为 kops 设置 IAM 用户

图 11.3:为 kops 设置 IAM 用户

注意突出显示的AccessKeyIDSecretAccessKey字段,这是您将收到的输出。这是敏感信息,前面截图中的密钥当然将被作者作废。我们将需要突出显示的信息进行下一步操作。

  1. 接下来,我们需要将为 kops 创建的凭据导出为环境变量,用于我们的终端会话。使用前一步截图中的突出信息:
export AWS_ACCESS_KEY_ID=<AccessKeyId>
export AWS_SECRET_ACCESS_KEY=<SecretAccessKey>
  1. 接下来,我们需要为 kops 创建一个 S3 存储桶来存储其状态。要创建一个随机的存储桶名称,请运行以下命令:
export BUCKET_NAME="kops-$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c 13 ; echo)" && echo $BUCKET_NAME

第二个命令输出创建的 S3 存储桶的名称,您应该看到类似以下的响应:

kops-aptjv0e9o2wet
  1. 运行以下命令,使用 AWS CLI 创建所需的存储桶:
aws s3 mb s3://$BUCKET_NAME --region us-west-2

在这里,我们使用us-west-2地区。如果您愿意,您可以使用离您更近的地区。对于成功创建存储桶,您应该看到以下响应:

make_bucket: kops-aptjv0e9o2wet

现在我们有了 S3 存储桶,我们可以开始设置我们的集群。我们可以选择许多选项,但现在我们将使用默认设置。

  1. 导出您的集群名称和 kops 将用于存储其状态的 S3 存储桶的名称:
export NAME=myfirstcluster.k8s.local
export KOPS_STATE_STORE=s3://$BUCKET_NAME
  1. 生成所有的配置并将其存储在之前的 S3 存储桶中,使用以下命令创建一个 Kubernetes 集群:
kops create cluster --zones us-west-2a,us-west-2b,us-west-2c --master-count=3 --kubernetes-version=1.15.0 --name $NAME

通过传递--zones参数,我们正在指定我们希望集群跨越的可用区域,并通过指定master-count=3参数,我们有效地表示我们要使用一个高可用的 Kubernetes 集群。默认情况下,kops 将创建两个工作节点。

请注意,这实际上并没有创建集群,而是创建了一系列的预检查,以便我们可以在短时间内创建一个集群。它通知我们,为了访问 AWS 实例,我们需要提供一个公钥 - 默认搜索位置是~/.ssh/id_rsa.pub

  1. 现在,我们需要创建一个 SSH 密钥,以添加到所有的主节点和工作节点,这样我们就可以用 SSH 登录到它们。使用以下命令:
kops create secret --name myfirstcluster.k8s.local sshpublickey admin -i ~/.ssh/id_rsa.pub

秘钥类型(sshpublickey)是 kops 为此操作保留的特殊关键字。更多信息可以在此链接找到:github.com/kubernetes/kops/blob/master/docs/cli/kops_create_secret_sshpublickey.md

注意

在这里指定的密钥~/.ssh/id_rsa.pub将是 kops 要分发到所有主节点和工作节点并可用于从本地计算机到运行服务器进行诊断或维护目的的密钥。

您可以使用以下命令使用密钥以管理员帐户登录:

ssh -i ~/.ssh/id_rsa admin@<public_ip_of_instance>

虽然这对于这个练习并不是必需的,但你会发现这对以后的章节很有用。

  1. 要查看我们的配置,请运行以下命令:
kops edit cluster $NAME

这将打开您的文本编辑器,并显示我们集群的定义,如下所示:

图 11.4:检查我们集群的定义

图 11.4:检查我们集群的定义

为了简洁起见,我们已经截取了这个屏幕截图。在这一点上,你可以进行任何编辑,但是对于这个练习,我们将继续进行而不进行任何更改。为了简洁起见,我们将不在本研讨会的范围内保留此规范的描述。如果您想了解 kops 的clusterSpec中各种元素的更多细节,可以在这里找到更多详细信息:github.com/kubernetes/kops/blob/master/docs/cluster_spec.md

  1. 现在,拿出我们在 S3 中生成并存储的配置,并实际运行命令,以使 AWS 基础设施与我们在配置文件中所说的想要的状态相一致:
kops update cluster $NAME --yes

注意

默认情况下,kops 中的所有命令都是 dry-run(除了一些验证步骤外,实际上什么都不会发生),除非您指定--yes标志。这是一种保护措施,以防止您在生产环境中意外地对集群造成危害。

这将需要很长时间,但完成后,我们将拥有一个可工作的 Kubernetes HA 集群。您应该看到以下响应:

图 11.5:更新集群以匹配生成的定义

图 11.5:更新集群以匹配生成的定义

  1. 为了验证我们的集群是否正在运行,让我们运行以下命令。这可能需要 5-10 分钟才能完全运行:
kops validate cluster

您应该看到以下响应:

图 11.6:验证我们的集群

图 11.6:验证我们的集群

从这个屏幕截图中,我们可以看到我们有三个 Kubernetes 主节点分布在不同的可用区,并且两个工作节点分布在三个可用区中的两个(使这个集群具有高可用性)。此外,所有节点以及集群似乎都是健康的。

注意

请记住,您的集群资源仍在运行。如果您计划在一段时间后继续进行下一个练习,您可能希望删除此集群以停止对 AWS 资源的计费。要删除此集群,您可以使用以下命令:

kops delete cluster --name ${NAME} --yes

Kubernetes Service Accounts

正如我们之前学到的,Kubernetes ServiceAccount 对象用作 Pod 内部进程的标识标记。虽然 Kubernetes 不管理和验证人类用户的身份,但它管理和验证 ServiceAccount 对象。然后,类似于用户,您可以允许 ServiceAccount 对 Kubernetes 资源进行基于角色的访问。

ServiceAccount 充当使用JSON Web TokenJWT)样式、基于标头的身份验证方式对集群进行身份验证的一种方式。每个 ServiceAccount 都与一个令牌配对,该令牌存储在由 Kubernetes API 创建的秘密中,然后挂载到与该 ServiceAccount 关联的 Pod 中。每当 Pod 中的任何进程需要发出 API 请求时,它会将令牌与请求一起传递给 API 服务器,Kubernetes 会将该请求映射到 ServiceAccount。基于该身份,Kubernetes 可以确定应该授予该进程对资源/对象(授权)的访问级别。通常,ServiceAccount 只分配给集群内部的 Pod 使用,因为它们只用于内部使用。ServiceAccount 是一个 Kubernetes 命名空间范围的对象。

ServiceAccount 的示例规范如下:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system

我们将在下一个练习中使用这个示例。您可以通过在对象的定义中包含这个字段来将这个 ServiceAccount 附加到一个对象,比如一个 Kubernetes 部署:

serviceAccountName: admin-user

如果您创建一个 Kubernetes 对象而没有指定服务账户,它将会被创建为default服务账户。default服务账户是 Kubernetes 为每个命名空间创建的。

在接下来的练习中,我们将在我们的集群上部署 Kubernetes 仪表板。Kubernetes 仪表板可以说是任何 Kubernetes 集群中运行的最有用的工具之一。它对于调试 Kubernetes 中的工作负载配置问题非常有用。

注意

您可以在这里找到更多信息:kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/

练习 11.02:在我们的 HA 集群上部署应用程序

在这个练习中,我们将使用在上一个练习中部署的相同集群,并部署 Kubernetes 仪表板。如果您已经删除了集群资源,请重新运行上一个练习。kops 将自动将所需的信息添加到本地 Kube 配置文件中以连接到集群,并将该集群设置为默认上下文。

由于 Kubernetes 仪表板是一个帮助我们进行管理任务的应用程序,default ServiceAccount 没有足够的权限。在这个练习中,我们将创建一个具有广泛权限的新 ServiceAccount:

  1. 首先,我们将应用直接从官方 Kubernetes 存储库获取的 Kubernetes 仪表板清单。这个清单定义了我们应用程序所需的所有对象。运行以下命令:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta1/aio/deploy/recommended.yaml

您应该看到以下响应:

图 11.7:应用 Kubernetes 仪表板的清单

图 11.7:应用 Kubernetes 仪表板的清单

  1. 接下来,我们需要配置一个 ServiceAccount 来访问仪表板。为此,请创建一个名为sa.yaml的文件,并包含以下内容:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system
---
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: kube-system

注意

我们给这个用户非常宽松的权限,所以请小心处理访问令牌。ClusterRole 和 ClusterRoleBinding 对象是 RBAC 策略的一部分,这在《第十三章》《Kubernetes 中的运行时和网络安全》中有所涵盖。

  1. 接下来,运行以下命令:
kubectl apply -f sa.yaml

您应该看到这个响应:

serviceaccount/admin-user created
clusterrolebinding.rbac.authorization.k8s.io/admin-user created
  1. 现在,让我们通过运行以下命令来确认 ServiceAccount 的详细信息:
kubectl describe serviceaccount -n kube-system admin-user

您应该看到以下响应:

图 11.8:检查我们的 ServiceAccount

图 11.8:检查我们的 ServiceAccount

当您在 Kubernetes 中创建一个 ServiceAccount 时,它还会在相同的命名空间中创建一个包含用于对 API 服务器进行 API 调用所需的 JWT 内容的 Secret。正如我们从前面的截图中所看到的,这种情况下的 Secret 的名称是admin-user-token-vx84g

  1. 让我们检查secret对象:
kubectl get secret -n kube-system -o yaml admin-user-token-vx84g

您应该看到以下输出:

图 11.9:检查我们的 ServiceAccount 中的令牌

图 11.9:检查我们的 ServiceAccount 中的令牌

这是输出的一个截断截图。正如我们所看到的,我们在这个秘密中有一个令牌。请注意,这是 Base64 编码的,我们将在下一步中解码。

  1. 现在我们需要账户 Kubernetes 为我们创建的令牌的内容,所以让我们使用这个命令:
kubectl -n kube-system get secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}') -o jsonpath='{.data.token}' | base64 --decode

让我们分解这个命令。该命令获取名为admin-user的密钥,因为我们创建了一个具有该名称的 ServiceAccount。当在 Kubernetes 中创建 ServiceAccount 时,它会放置一个与我们用于对集群进行身份验证的令牌同名的密钥。命令的其余部分是用于将结果解码为有用的形式以便复制和粘贴到仪表板中的语法糖。您应该得到如下截图所示的输出:

图 11.10:获取与令牌相关的内容与 admin-user ServiceAccount

图 11.10:获取与 admin-user ServiceAccount 关联的令牌的内容

复制您收到的输出,但要小心不要复制输出末尾看到的$%符号(在 Bash 或 Zsh 中看到)。

  1. 默认情况下,Kubernetes 仪表板不会暴露给集群外的公共互联网。因此,为了使用浏览器访问它,我们需要一种允许浏览器与 Kubernetes 容器网络内的 Pod 进行通信的方式。一个有用的方法是使用内置在kubectl中的代理:
kubectl proxy

您应该看到这个响应:

Starting to serve on 127.0.0.1:8001
  1. 打开浏览器并导航到以下 URL:
http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

您应该看到以下提示:

图 11.11:输入令牌以登录 Kubernetes 仪表板

图 11.11:输入令牌以登录 Kubernetes 仪表板

粘贴从步骤 4复制的令牌,然后单击SIGN IN按钮。

成功登录后,您应该看到仪表板如下截图所示:

图 11.12:Kubernetes 仪表板登陆页面

图 11.12:Kubernetes 仪表板登陆页面

在这个练习中,我们已经部署了 Kubernetes 仪表板到集群,以便您可以从方便的 GUI 管理您的应用程序。在部署此应用程序的过程中,我们已经看到了如何为我们的集群创建 ServiceAccounts。

在本章中,您已经学会了如何使用 kops 创建云基础架构,以创建一个高可用的 Kubernetes 集群。然后,我们部署了 Kubernetes 仪表板,并在此过程中了解了 ServiceAccounts。现在您已经看到了创建集群并在其上运行应用程序所需的步骤,我们将创建另一个集群,并在接下来的活动中看到其弹性。

活动 11.01:测试高可用集群的弹性

在这个活动中,我们将测试我们自己创建的 Kubernetes 集群的弹性。以下是进行此活动的一些指南:

  1. 部署 Kubernetes 仪表板。但是这次,将运行应用程序的部署的副本计数设置为高于1的值。

Kubernetes Dashboard 应用程序在由名为kubernetes-dashboard的部署管理的 Pod 上运行,该部署在名为kubernetes-dashboard的命名空间中运行。这是您需要操作的部署。

  1. 现在,开始从 AWS 控制台关闭各种节点,以删除节点,删除 Pod,并尽力使底层系统不稳定。

  2. 在您尝试关闭集群的每次尝试后,如果控制台仍然可访问,请刷新 Kubernetes 控制台。只要从应用程序获得任何响应,这意味着集群和我们的应用程序(在本例中为 Kubernetes 仪表板)仍然在线。只要应用程序在线,您应该能够访问 Kubernetes 仪表板,如下截图所示:图 11.13:Kubernetes 仪表板提示输入令牌

图 11.13:Kubernetes 仪表板提示输入令牌

此截图仅显示您需要输入令牌的提示,但足以表明我们的应用程序在线。如果您的请求超时,这意味着我们的集群不再可用。

  1. 加入另一个节点到这个集群。

为了实现这一点,您需要找到并编辑管理节点的 InstanceGroup 资源。规范包含maxSizeminSize字段,您可以操纵这些字段来控制节点的数量。当您更新您的集群以匹配修改后的规范时,您应该能够看到三个节点,如下截图所示:

图 11.14:集群中主节点和工作节点的数量

图 11.14:集群中主节点和工作节点的数量

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD。确保在完成活动后删除您的集群。有关如何删除集群的更多详细信息,请参见以下部分(删除我们的集群)。

删除我们的集群

一旦我们完成了本章中的所有练习和活动,您应该通过运行以下命令来删除集群:

kops delete cluster --name ${NAME} --yes

您应该看到这个响应:

图 11.15:删除我们的集群

图 11.15:删除我们的集群

在这一点上,您不应该再从 AWS 那里收到本章中您所创建的 Kubernetes 基础架构的费用。

总结

高可用基础架构是实现应用程序高可用性的关键组成部分之一。Kubernetes 是一个设计非常精良的工具,具有许多内置的弹性特性,使其能够经受住重大的网络和计算事件。它致力于防止这些事件影响您的应用程序。在我们探索高可用性系统时,我们调查了 Kubernetes 的一些组件以及它们如何共同实现高可用性。然后,我们使用 kops 集群生命周期管理工具在 AWS 上构建了一个旨在实现高可用性的集群。

在下一章中,我们将看看如何通过利用 Kubernetes 原语来确保高可用性,使我们的应用程序更具弹性。

第十二章: 您的应用程序和 HA

概述

在这一章中,我们将通过使用 Terraform 和 Amazon Elastic Kubernetes Service (EKS)来探索 Kubernetes 集群的生命周期管理。我们还将部署一个应用程序,并学习一些原则,使应用程序更适合 Kubernetes 环境。

本章将指导您如何使用 Terraform 创建一个功能齐全、高可用的 Kubernetes 环境。您将在集群中部署一个应用程序,并修改其功能,使其适用于高可用环境。我们还将学习如何通过使用 Kubernetes 入口资源将来自互联网的流量传输到集群中运行的应用程序。

介绍

在上一章中,我们在云环境中设置了我们的第一个多节点 Kubernetes 集群。在本节中,我们将讨论如何为我们的应用程序操作 Kubernetes 集群,即我们将使用集群来运行除了仪表板之外的容器化应用程序。

由于 Kubernetes 的用途与集群操作员所能想象的一样多,因此 Kubernetes 的用例各不相同。因此,我们将对我们为集群操作的应用程序类型做一些假设。我们将优化一个工作流程,用于在基于云的环境中部署具有高可用性要求的具有有状态后端的无状态 Web 应用程序。通过这样做,我们希望能够涵盖人们通常使用 Kubernetes 集群的大部分内容。

Kubernetes 可以用于几乎任何事情。即使我们所涵盖的内容与您对 Kubernetes 的用例不完全匹配,也值得研究,因为这一点很重要。在本章中,我们要做的只是在云中运行一个 Web 应用程序的示例工作流程。一旦您学习了本章中我们将用于运行示例工作流程的原则,您可以在互联网上查找许多其他资源,帮助您发现其他优化工作流程的方式,如果这不符合您的用例。

但在我们继续确保我们将在集群上运行的应用程序的高可用性之前,让我们退一步考虑一下你的云基础设施的高可用性要求。为了在应用程序级别保持高可用性,同样重要的是我们以同样的目标来管理我们的基础设施。这让我们开始讨论基础设施生命周期管理。

基础设施生命周期管理概述

简单来说,基础设施生命周期管理是指我们如何在服务器的有用生命周期的每个阶段管理我们的服务器。这涉及到提供、维护和 decommissioning 物理硬件或云资源。由于我们正在利用云基础设施,我们应该利用基础设施生命周期管理工具来以编程方式提供和取消资源。为了理解这一点为什么重要,让我们考虑以下例子。

想象一下,你是一名系统管理员、DevOps 工程师、站点可靠性工程师,或者其他需要处理公司服务器基础设施的角色,而这家公司是数字新闻行业的公司。这意味着,这家公司的员工主要输出的是他们在网站上发布的信息。现在,想象一下,整个网站都在你公司服务器房的一台服务器上运行。服务器上运行的应用程序是一个带有 MySQL 后端的 PHP 博客网站。有一天,一篇文章突然爆红,你突然要处理的流量比前一天多得多。你会怎么做?网站一直崩溃(如果加载的话),你的公司正在因为你试图找到解决方案而损失金钱。

你的解决方案是开始分离关注点并隔离单点故障。你首先要做的是购买更多的硬件并开始配置它,希望能够水平扩展网站。做完这些之后,你运行了五台服务器,其中一台运行着 HAProxy,它负载均衡连接到运行在三台服务器上的 PHP 应用程序和一个数据库服务器上。好吧,现在你觉得你已经控制住了。然而,并非所有的服务器硬件都是一样的——它们运行着不同的 Linux 发行版,每台机器的资源需求也不同,对每台服务器进行补丁、升级和维护变得困难。好巧不巧,又一篇文章突然爆红,你突然面临着比当前硬件能处理的请求量多五倍的情况。现在你该怎么办?继续水平扩展?然而,你只是一个人,所以在配置下一组服务器时很可能会出错。由于这个错误,你以新颖的方式使网站崩溃了,管理层对此并不高兴。你读到这里是不是感到和我写这篇文章时一样紧张?

正是因为配置错误,工程师们开始利用工具和配置编写源代码来定义他们的拓扑结构。这样,如果需要对基础设施状态进行变更,就可以跟踪、控制并以一种使代码负责解决你声明的基础设施状态与实际观察到的状态之间差异的方式进行部署。

基础设施的好坏取决于围绕它的生命周期管理工具和运行在其之上的应用程序。这意味着,如果你的集群构建得很好,但没有工具可以成功地更新集群上的应用程序,那么它就不会为你服务。在本章中,我们将从应用程序级别的视角来看如何利用持续集成构建流水线以零停机、云原生的方式推出新的应用程序更新。

在本章中,我们将为您提供一个测试应用程序进行管理。我们还将使用一个名为Terraform的基础设施生命周期管理工具,以更有效地管理 Kubernetes 云基础设施的部署。本章应该能帮助您开发出一套有效的技能,让您能够在 Kubernetes 环境中快速开始创建自己的应用程序交付流水线。

Terraform

在上一章中,我们使用kops从头开始创建了一个 Kubernetes 集群。然而,这个过程可能被视为繁琐且难以复制,这会导致配置错误的高概率,从而在应用程序运行时导致意外事件。幸运的是,有一个非常强大的社区支持的工具,可以很好地解决这个问题,适用于在亚马逊网络服务AWS)以及其他几个云平台上运行的 Kubernetes 集群,比如 Azure、谷歌云平台GCP)等。

Terraform 是一种通用的基础设施生命周期管理工具;也就是说,Terraform 可以通过代码管理您的基础设施的状态。Terraform 最初创建时的目标是创建一种语言(HashiCorp 配置语言HCL))和运行时,可以以可重复的方式创建基础设施,并以与我们控制应用程序源代码变更相同的方式控制对基础设施的变更——通过拉取请求、审查和版本控制。Terraform 自那时以来已经有了相当大的发展,现在是一种通用的配置管理工具。在本章中,我们将使用其最经典的意义上的基础设施生命周期管理的原始功能。

Terraform 文件是用一种叫做 HCL 的语言编写的。HCL 看起来很像 YAML 和 JSON,但有一些不同之处。例如,HCL 支持在其文件中对其他资源的引用进行插值,并能够确定需要创建资源的顺序,以确保依赖于其他资源创建的资源不会以错误的顺序创建。Terraform 文件的文件扩展名是.tf

您可以将 Terraform 文件视为以类似的方式指定整个基础设施的期望状态,例如,Kubernetes YAML 文件将指定部署的期望状态。这允许声明式地管理整个基础设施。因此,我们得到了基础设施即代码IaC)的管理思想。

Terraform 分为两个阶段——计划应用。这是为了确保您有机会在进行更改之前审查基础设施更改。Terraform 假设它独自负责对基础设施的所有状态更改。因此,如果您使用 Terraform 来管理基础设施,通过任何其他方式进行基础设施更改(例如,通过 AWS 控制台添加资源)是不明智的。这是因为如果您进行更改并且没有确保它在 Terraform 文件中得到更新,那么下次应用 Terraform 文件时,它将删除您一次性的更改。这不是一个错误,这是一个功能,这次是真的。这样做的原因是,当您跟踪基础设施作为代码时,每个更改都可以被跟踪、审查和使用自动化工具进行管理,例如 CI/CD 流水线。因此,如果您的系统状态偏离了书面状态,那么 Terraform 将负责将您观察到的基础设施与您书面记录的内容进行调和。

在本章中,我们将向您介绍 Terraform,因为它在行业中被广泛使用,作为管理基础设施的便捷方式。但是,我们不会深入到使用 Terraform 创建每一个 AWS 资源,以便让我们的讨论集中在 Kubernetes 上。我们只会进行一个快速演示,以确保您理解一些基本原则。

注意

您可以在本书中了解有关在 AWS 中使用 Terraform 的更多信息:www.packtpub.com/networking-and-servers/getting-started-terraform-second-edition

练习 12.01:使用 Terraform 创建 S3 存储桶

在这个练习中,我们将实现一些常用的命令,这些命令在使用 Terraform 时会用到,并向您介绍一个 Terraform 文件,该文件将是我们基础设施的定义。

注意

Terraform 将代表我们在 AWS 上创建资源,这将花费你的钱。

  1. 首先,让我们创建一个目录,我们将在其中进行 Terraform 更改,然后我们将导航到该目录:
mkdir -p ~/Desktop/eks_terraform_demo
cd Desktop/eks_terraform_demo/
  1. 现在,我们要创建我们的第一个 Terraform 文件。Terraform 文件的扩展名是.tf。创建一个名为main.tf的文件(与其他一些语言不同,单词main没有特殊意义),内容如下:
resource "aws_s3_bucket" "my_bucket" {
  bucket = "<<NAME>>-test-bucket"
  acl    = "private"
}

这个块有一个叫做aws_s3_bucket的定义,这意味着它将创建一个 Amazon S3 存储桶,其名称在bucket字段中指定。acl="private"行表示我们不允许公共访问这个存储桶。请确保用您自己的唯一名称替换<<NAME>>

  1. 要开始使用 Terraform,我们需要初始化它。因此,让我们用以下命令来做到这一点:
terraform init

您应该看到以下响应:

图 12.1:初始化 Terraform

图 12.1:初始化 Terraform

  1. 运行以下命令,让 Terraform 确定创建资源的计划,这些资源由我们之前创建的main.tf文件定义:
terraform plan

您将被提示输入一个 AWS 区域。使用离您最近的一个。在下面的屏幕截图中,我们使用的是us-west-2

图 12.2:计算集群资源所需的更改用于创建 S3 存储桶

图 12.2:计算创建 S3 存储桶所需的集群资源的必要更改

因此,我们可以看到 Terraform 已经使用我们在上一章练习 11.01,在我们的 Kubernetes 集群中设置 AWS 账户中设置的访问密钥访问了我们的 AWS 账户,并计算了为使我们的 AWS 环境看起来像我们在 Terraform 文件中定义的那样需要做什么。正如我们在屏幕截图中看到的,它计划为我们添加一个 S3 存储桶,这正是我们想要的。

注意

Terraform 将尝试应用当前工作目录中所有扩展名为.tf的文件。

在上一个屏幕截图中,我们可以看到terraform命令指示我们没有指定-out参数,因此它不会保证精确计划将被应用。这是因为您的 AWS 基础设施中的某些内容可能已经从计划时发生了变化。假设您今天计划了一个计划。然后,稍后,您添加或删除了一些资源。因此,为了实现给定状态所需的修改将是不同的。因此,除非您指定-out参数,否则 Terraform 将在应用之前重新计算其计划。

  1. 运行以下命令来应用配置并创建我们 Terraform 文件中指定的资源:
terraform apply

Terraform 将为我们提供一次机会来审查计划并在对 AWS 资源进行更改之前决定我们想要做什么:

图 12.3:计算更改并确认创建 S3 存储桶的提示

图 12.3:计算更改并确认创建 S3 存储桶的提示

如前所述,即使我们使用apply命令,Terraform 也计算了所需的更改。确认 Terraform 显示的操作,然后输入yes以执行显示的计划。现在,Terraform 已经为我们创建了一个 S3 存储桶:

图 12.4:确认后创建 S3 存储桶

图 12.4:确认后创建 S3 存储桶

  1. 现在,我们将销毁我们创建的所有资源,以便在进行下一个练习之前进行清理。要销毁它们,请运行以下命令:
terraform destroy

再次,要确认此操作,您必须在提示时明确允许 Terraform 销毁您的资源,输入yes,如以下屏幕截图所示:

图 12.5:使用 Terraform 销毁资源

图 12.5:使用 Terraform 销毁资源

在这个练习中,我们演示了如何使用 Terraform 创建单个资源(S3 存储桶),以及如何销毁存储桶。这应该让您熟悉了 Terraform 的简单工具,并且我们现在将进一步扩展这些概念。

现在,让我们使用 Terraform 创建一个 Kubernetes 集群。上次,我们构建并管理了自己的集群控制平面。由于几乎每个云提供商都为他们的客户提供此服务,我们将利用由 AWS 提供的 Kubernetes 的托管服务 Amazon 弹性 Kubernetes 服务(EKS)。

当我们使用托管的 Kubernetes 服务时,以下内容由云服务供应商处理:

  • 管理和保护 etcd

  • 管理和保护用户身份验证

  • 管理控制平面组件,如控制器管理器、调度器和 API 服务器

  • 在您的网络中运行的 Pod 之间进行 CNI 配置

控制平面通过绑定到您的 VPC 的弹性网络接口暴露给您的节点。您仍然需要管理工作节点,它们作为您帐户中的 EC2 实例运行。因此,使用托管服务允许您专注于使用 Kubernetes 完成的工作,但缺点是对控制平面没有非常精细的控制。

注意

由于 AWS 处理集群的用户身份验证,我们将不得不使用 AWS IAM 凭据来访问我们的 Kubernetes 集群。我们可以在我们的机器上利用 AWS IAM Authenticator 二进制文件来做到这一点。关于这一点,我们将在接下来的章节中详细介绍。

练习 12.02:使用 Terraform 创建 EKS 集群

对于这个练习,我们将使用我们已经提供的main.tf文件来创建一个生产就绪、高可用的 Kubernetes 集群。

注意

这个 Terraform 文件是从github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples提供的示例进行了调整。

这将使 Terraform 能够创建以下内容:

  • 一个具有 IP 地址空间10.0.0.0/16的 VPC。它将有三个公共子网,每个子网都有/24255)个 IP 地址。

  • 路由表和 VPC 的互联网网关需要正常工作。

  • 控制平面与节点通信的安全组,以及在允许和必需的端口上接收来自外部世界的流量。

  • EKS 控制平面的 IAM 角色(执行诸如代表您创建服务的ELB(弹性负载均衡器)等任务)和节点(处理与 EC2 API 相关的问题)。

  • EKS 控制平面以及与您的 VPC 和节点的所有必要连接的设置。

  • 一个用于节点加入集群的ASG(自动扩展组)(它将提供两个m4.large实例)。

  • 生成一个 kubeconfig 文件和一个 ConfigMap,这对于节点加入集群以及与集群通信是必要的。

这是一个相对安全和稳定的方式,可以创建一个能够可靠处理生产工作负载的 Kubernetes 集群。让我们开始练习:

  1. 使用以下命令获取我们提供的main.tf文件:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.02/main.tf

这将替换现有的main.tf文件,如果您仍然拥有来自上一个练习的文件。请注意,您的目录中不应该有任何其他 Terraform 文件。

  1. 现在,我们需要 Terraform 将在main.tf文件中定义的状态应用到您的云基础设施上。为此,请使用以下命令:
terraform apply

注意

不应该使用我们在上一章生成的用于 kops 的 AWS IAM 用户来执行这些命令,而是应该使用具有 AWS 账户管理员访问权限的用户,以确保没有意外的权限问题。

这可能需要大约 10 分钟才能完成。您应该会看到一个非常长的输出,类似于以下内容:

图 12.6:为我们的 EKS 集群创建资源

图 12.6:为我们的 EKS 集群创建资源

完成后,将会有两个终端输出——一个用于节点的 ConfigMap,一个用于访问集群的 kubeconfig 文件,如下截图所示:

图 12.7:获取访问我们的集群所需的信息

图 12.7:获取访问我们的集群所需的信息

将 ConfigMap 复制到一个文件中,并将其命名为configmap.yaml,然后将 kubeconfig 文件复制并写入计算机上的~/.kube/config文件。

  1. 现在,我们需要应用更改,以允许我们的工作节点与控制平面通信。这是一个用于将工作节点加入到您的 EKS 集群的 YAML 格式文件;我们已经将其保存为configmap.yaml。运行以下命令:
kubectl apply -f configmap.yaml

注意

要运行此命令,您需要在计算机上安装aws-iam-authenticator二进制文件。要执行此操作,请按照此处的说明操作:docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html

这将应用允许 Kubernetes 集群与节点通信的 ConfigMap。您应该会看到以下响应:

configmap/aws-auth created
  1. 现在,让我们验证一切是否正常运行。在终端中运行以下命令:
kubectl get node

您应该会看到以下输出:

图 12.8:检查我们的节点是否可访问

图 12.8:检查我们的节点是否可访问

在这个阶段,我们使用 EKS 作为控制平面,有两个工作节点的运行中的 Kubernetes 集群。

注意

请记住,您的集群资源将保持在线,直到您删除它们。如果您计划稍后回到以下练习,您可能希望删除您的集群以减少账单。要执行此操作,请运行terraform destroy。要重新上线您的集群,请再次运行此练习。

现在我们已经设置好了集群,在接下来的部分,让我们来看一下一个高效灵活的方法,将流量引导到集群上运行的任何应用程序。

Kubernetes Ingress

在 Kubernetes 项目的早期阶段,Service 对象用于将外部流量传输到运行的 Pod。您只有两种选择来从外部获取流量 - 使用 NodePort 服务或 LoadBalancer 服务。在公共云提供商环境中,后者是首选,因为集群会自动管理设置安全组/防火墙规则,并将 LoadBalancer 指向工作节点上的正确端口。但是,这种方法有一个小问题,特别是对于刚开始使用 Kubernetes 或预算紧张的人。问题是一个 LoadBalancer 只能指向单个 Kubernetes 服务对象。

现在,想象一下您在 Kubernetes 中运行了 100 个微服务,所有这些微服务都需要公开。在 AWS 中,ELB(由 AWS 提供的负载均衡器)的平均成本大约为每月 20 美元。因此,在这种情况下,您每月支付 2000 美元,只是为了有获取流量进入您的集群的选项,并且我们还没有考虑网络的额外成本。

让我们再了解一下 Kubernetes 服务对象和 AWS 负载均衡器之间的一对一关系的另一个限制。假设对于您的项目,您需要将内部 Kubernetes 服务的基于路径的映射到同一负载平衡端点。假设您在api.example.io上运行一个 Web 服务,并且希望api.example.io/users转到一个微服务,api.examples.io/weather转到另一个完全独立的微服务。在 Ingress 到来之前,您需要设置自己的 Kubernetes 服务并对应用进行内部路径解析。

这现在不再是一个问题,因为 Kubernetes Ingress 资源的出现。Kubernetes Ingress 资源旨在与 Ingress 控制器一起运行(这是一个在您的集群中运行的应用程序,监视 Kubernetes API 服务器对 Ingress 资源的更改)。这两个组件一起允许您定义多个 Kubernetes 服务,它们本身不必被外部公开,也可以通过单个负载均衡端点进行路由。让我们看一下以下图表,以更好地理解这一点:

图 12.9:使用 Ingress 将流量路由到我们的服务

图 12.9:使用 Ingress 将流量路由到我们的服务

在这个例子中,所有请求都是从互联网路由到api.example.io。一个请求将转到api.example.io/a,另一个将转到api.example.io/b,最后一个将转到api.example.io/c。这些请求都将发送到一个负载均衡器和一个 Kubernetes 服务,通过 Kubernetes Ingress 资源进行控制。这个 Ingress 资源将流量从单个 Ingress 端点转发到它配置为转发流量的服务。在接下来的章节中,我们将设置ingress-nginx Ingress 控制器,这是 Kubernetes 社区中常用的开源工具用于 Ingress。然后,我们将配置 Ingress 以允许流量进入我们的集群,以访问我们的高可用应用程序。

在 Kubernetes 上运行的高可用应用程序

现在您有机会启动一个 EKS 集群并了解 Ingress,让我们向您介绍我们的应用程序。我们提供了一个示例应用程序,它有一个缺陷,阻止它成为云原生,并真正能够在 Kubernetes 中进行水平扩展。我们将在接下来的练习中部署这个应用程序并观察其行为。然后,在下一节中,我们将部署这个应用程序的修改版本,并观察它如何更适合实现我们所述的高可用目标。

练习 12.03:在 Kubernetes 中部署多副本非高可用应用程序

在这个练习中,我们将部署一个不具备水平扩展能力的应用程序版本。我们将尝试对其进行扩展,并观察阻止其水平扩展的问题:

注意

我们已经在 GitHub 存储库中提供了此应用程序的源代码以供参考。但是,由于我们的重点是 Kubernetes,我们将在此练习中使用命令直接从存储库中获取它。

  1. 使用以下命令获取运行应用程序所需的所有对象的清单:
curl https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.03/without_redis.yaml > without_redis.yaml

这应该会将清单下载到您当前的目录中:

图 12.10:下载应用程序清单

图 12.10:下载应用程序清单

如果您查看清单,您会发现它包含一个运行单个 Pod 副本的部署和一个 ClusterIP 类型的服务,用于将流量路由到它。

  1. 然后,创建一个 Kubernetes 部署和服务对象,以便我们可以运行我们的应用程序:
kubectl apply -f without_redis.yaml

您应该会看到以下响应:

图 12.11:创建我们的应用程序资源

图 12.11:为我们的应用程序创建资源

  1. 现在,我们需要添加一个 Kubernetes Ingress 资源,以便能够访问这个网站。要开始使用 Kubernetes Ingress,我们需要运行以下命令:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml 
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/aws/service-l4.yaml 
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/aws/patch-configmap-l4.yaml 

这三个命令将为 EKS 部署 Nginx Ingress 控制器实现。您应该看到以下响应:

图 12.12:实现 Ingress 控制器

图 12.12:实现 Ingress 控制器

注意

此命令仅适用于 AWS 云提供商。如果您在另一个平台上运行集群,您需要从kubernetes.github.io/ingress-nginx/deploy/#aws找到适当的链接。

  1. 然后,我们需要为自己创建一个 Ingress。在我们所在的同一文件夹中,让我们创建一个名为ingress.yaml的文件,内容如下:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: counter.com
      http:
        paths:
          - path: /
            backend:
              serviceName: kubernetes-test-ha-application-                without-redis
              servicePort: 80
  1. 现在,使用以下命令运行 Ingress:
kubectl apply -f ingress.yaml

您应该看到以下响应:

ingress.networking.k8s.io/ingress created
  1. 现在,我们将配置 Ingress 控制器,使得当请求到达具有Host:头部为counter.com的负载均衡器时,它应该转发到端口80上的kubernetes-test-ha-application-without-redis服务。

首先,让我们找到我们需要访问的 URL:

kubectl describe svc -n ingress-nginx ingress-nginx

您应该看到类似以下的输出:

图 12.13:检查访问 Ingress 负载均衡器端点的 URL

图 12.13:检查访问 Ingress 负载均衡器端点的 URL

从前面的截图中,注意 Kubernetes 在 AWS 为我们创建的 Ingress 负载均衡器端点如下:

a0c805e36932449eab6c966b16b6cf1-13eb0d593e468ded.elb.us-east-1.amazonaws.com

您的值可能与前面的值不同,您应该使用您设置的值。

  1. 现在,让我们使用curl访问端点:
curl -H 'Host: counter.com' a0c805e36932449eab6c966b16b6cf1-13eb0d593e468ded.elb.us-east-1.amazonaws.com/get-number

您应该得到类似以下的响应:

{number: 1}%

如果您多次运行它,您会看到每次数字增加 1:

图 12.14:重复访问我们的应用程序

图 12.14:重复访问我们的应用程序

  1. 现在,让我们发现应用程序的问题。为了使应用程序具有高可用性,我们需要同时运行多个副本,以便至少允许一个副本不可用。这反过来使应用程序能够容忍故障。为了扩展应用程序,我们将运行以下命令:
kubectl scale deployment --replicas=3 kubernetes-test-ha-application-without-redis-deployment

您应该看到以下响应:

图 12.15:扩展应用部署

图 12.15:扩展应用部署

  1. 现在,尝试多次访问应用,就像我们在步骤 7中所做的那样:
curl -H 'Host: counter.com' a3960d10c980e40f99887ea068f41b7b-1447612395.us-east-1.elb.amazonaws.com/get-number

您应该看到类似以下的响应:

图 12.16:重复访问扩展应用以观察行为

图 12.16:重复访问扩展应用以观察行为

注意

这个输出可能对您来说并不完全相同,但如果您看到前几次尝试时数字在增加,请继续访问应用。您将能够在几次尝试后观察到问题行为。

这个输出突出了我们应用的问题——数量并不总是增加。为什么呢?因为负载均衡器可能会将请求传递给任何一个副本,接收请求的副本会根据其本地状态返回响应。

处理有状态的应用

前面的练习展示了在分布式环境中处理有状态应用的挑战。简而言之,无状态应用是一种不保存客户端在一个会话中生成的数据以便在下一个会话中使用的应用程序。这意味着一般来说,无状态应用完全依赖于输入来推导其输出。想象一个服务器显示一个静态网页,不需要因任何原因而改变。在现实世界中,无状态应用通常需要与有状态应用结合,以便为客户或应用的消费者创建有用的体验。当然,也有例外。

有状态的应用是一种其输出取决于多个因素的应用,比如用户输入、来自其他应用的输入以及过去保存的事件。这些因素被称为应用的“状态”,它决定了应用的行为。创建具有多个副本的分布式应用最重要的部分之一是,用于生成输出的任何状态都需要在所有副本之间共享。如果您的应用的不同副本使用不同的状态,那么您的应用将会展现基于请求路由到哪个副本的随机行为。这实际上违背了使用副本水平扩展应用的目的。

在前面的练习中,对于每个副本都能以正确的数字进行响应,我们需要将该数字的存储移到每个副本之外。为了做到这一点,我们需要修改应用程序。让我们想一想如何做到这一点。我们能否使用另一个请求在副本之间传递数字?我们能否指定每个副本只能以其分配的数字的倍数进行响应?(如果我们有三个副本,一个只会以147…进行响应,另一个会以258…进行响应,最后一个会以369…进行响应。)或者,我们可以将数字存储在外部状态存储中,比如数据库?无论我们选择什么,前进的道路都将涉及在 Kubernetes 中更新我们正在运行的应用程序。因此,我们需要简要讨论一下如何做到这一点。

CI/CD 流水线

借助容器化技术和容器镜像标签修订策略的帮助,我们可以相对轻松地对我们的应用程序进行增量更新。就像源代码和基础设施代码一样,我们可以将执行构建和部署流水线步骤的脚本和 Kubernetes 清单版本化,存储在诸如git之类的工具中。这使我们能够对我们的集群中的软件更新发生的方式有极大的可见性和灵活性,使用 CI 和 CD 等方法来控制。

对于不熟悉的人来说,CI/CD代表持续集成和持续部署/交付。CI 方面使用工具,如 Jenkins 或 Concourse CI,将新的更改集成到我们的源代码中,进行可重复的测试和组装我们的代码成最终的构件以进行部署。CI 的目标是多方面的,但以下是一些好处:

  • 如果测试充分,软件中的缺陷会在流程的早期被发现。

  • 可重复的步骤在部署到环境时会产生可重复的结果。

  • 可见性存在是为了与利益相关者沟通功能的状态。

  • 它鼓励频繁的软件更新,以使开发人员确信他们的新代码不会破坏现有的功能。

CD 的另一部分是将自动化机制整合到不断向最终用户交付小型更新的过程中,例如在 Kubernetes 中更新部署对象并跟踪部署状态。CI/CD 流水线是当前主流的 DevOps 模型。

理想情况下,CI/CD 流水线应该能够可靠地、可预测地将代码从开发人员的机器带到生产环境,尽量减少手动干预。CI 流水线理想上应该包括编译(必要时)、测试和最终应用程序组装的组件(在 Kubernetes 集群的情况下,这是一个容器)。

CD 流水线应该有一种自动化与基础设施交互的方式,以获取应用程序修订版并部署它,以及任何依赖配置和一次性部署任务,使得所需版本的软件成为软件的运行版本,通过某种策略(比如在 Kubernetes 中使用 Deployment 对象)。它还应该包括遥测工具,以观察部署对周围环境的即时影响。

我们在上一节观察到的问题是,我们的应用程序中的每个副本都是根据其本地状态返回一个数字通过 HTTP。为了解决这个问题,我们建议使用外部状态存储(数据库)来管理应用程序的每个副本之间共享的信息(数字)。我们有几种状态存储的选择。我们选择 Redis,只是因为它很容易上手,而且很容易理解。Redis 是一个高性能的键值数据库,很像 etcd。在我们的示例重构中,我们将通过设置一个名为num的键来在副本之间共享状态,值是我们想要返回的递增整数值。在每个请求期间,这个值将被递增并存储回数据库,以便每个副本都可以使用最新的信息。

每家公司和个人都有自己管理部署新代码版本的不同流程。因此,我们将使用简单的命令来执行我们的步骤,可以通过 Bash 和您选择的工具自动化。

练习 12.04:使用状态管理部署应用程序

在这个练习中,我们将部署一个修改过的应用程序版本,这是我们在上一个练习中部署的应用程序的修改版本。作为提醒,这个应用程序会计算它被访问的次数,并以 JSON 格式返回给请求者。然而,在上一个练习的结尾,我们观察到在图 12.16中,当我们使用多个副本水平扩展这个应用程序时,我们得到的数字并不总是增加的。

注意

我们已经在 GitHub 存储库中提供了这个应用程序的源代码供您参考。然而,由于我们的重点是 Kubernetes,我们将在这个练习中使用命令直接从存储库中获取它。

在这个修改后的应用程序版本中,我们重构了我们的代码,以添加将这个增长计数存储在 Redis 数据库中的功能。这允许我们拥有多个应用程序副本,但每次向端点发出请求时,计数都会增加:

注意

在我们的 Redis 实现中,我们没有使用事务来设置获取后的计数。因此,当我们更新数据库中的值时,有很小的机会获取并处理旧信息,这可能导致意外的结果。

  1. 使用以下命令获取此应用程序所需的所有对象的清单:
curl https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.04/with_redis.yaml > with_redis.yaml

您应该看到类似以下的响应:

图 12.17:下载修改后应用程序的清单

图 12.17:下载修改后应用程序的清单

如果您打开这个清单,您会看到我们为我们的应用程序运行了三个副本的部署:一个 ClusterIP 服务来暴露它,一个运行一个副本的 Redis 部署,以及另一个 ClusterIP 服务来暴露 Redis。我们还修改了之前创建的 Ingress 对象,指向新的服务。

  1. 现在,是时候在 Kubernetes 上部署它了。我们可以运行以下命令:
kubectl apply -f with_redis.yaml

您应该看到类似以下的响应:

图 12.18:创建集群所需的资源

图 12.18:创建集群所需的资源

  1. 现在,让我们看看这个应用程序通过以下命令给我们带来了什么:
curl -H 'Host: counter.com' a3960d10c980e40f99887ea068f41b7b-1447612395.us-east-1.elb.amazonaws.com/get-number

重复运行此命令。您应该能够看到一个递增的数字,如下所示:

图 12.19:具有一致增长数字的可预测输出

图 12.19:具有一致增长数字的可预测输出

如您在前面的输出中所看到的,程序现在按顺序输出数字,因为我们的 Deployment 的所有副本现在共享一个负责管理应用程序状态(Redis)的单个数据存储。

如果您想创建一个真正高可用、容错的软件系统,还有许多其他范式需要转变,这超出了本书详细探讨的范围。但是,您可以在此链接查看有关分布式系统的更多信息:www.packtpub.com/virtualization-and-cloud/hands-microservices-kubernetes

注意

再次记住,此时您的集群资源仍在运行。如果您希望稍后继续进行活动,请不要忘记使用terraform destroy拆除您的集群。

现在,我们已经构建了具有持久性和在不同副本之间共享其状态能力的应用程序,我们将在接下来的活动中进一步扩展它。

活动 12.01:扩展我们应用程序的状态管理

目前,我们的应用程序可以利用运行在 Kubernetes 集群内部的共享 Redis 数据库来管理我们在获取时返回给用户的变量计数器。

但是,假设我们暂时不信任 Kubernetes 能够可靠地管理 Redis 容器(因为它是一个易失性的内存数据存储),而是希望使用 AWS ElastiCache 来管理。您在此活动中的目标是使用本章学习的工具修改我们的应用程序,使其与 AWS ElastiCache 配合使用。

您可以使用以下指南完成此活动:

  1. 使用 Terraform 来配置 ElastiCache。

您可以在此链接找到为配置 ElastiCache 所需的参数值:www.terraform.io/docs/providers/aws/r/elasticache_cluster.html#redis-instance

  1. 将应用程序更改为连接到 Redis。您需要在 Kubernetes Deployment 中使用环境变量。当您运行terraform apply命令时,您可以在redis_address字段中找到所需的信息。

  2. 将 ElastiCache 端点添加到适当的 Kubernetes 清单环境变量中。

  3. 使用任何您想要的工具在 Kubernetes 集群上推出新版本的代码。

到最后,您应该能够观察到应用程序的响应类似于我们在上一个练习中看到的,但这一次,它将使用 ElastiCache 来进行状态管理:

图 12.20:活动 12.01 的预期输出

图 12.20:活动 12.01 的预期输出

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD。请记住,您的集群资源将保持在线,直到您删除它们。要删除集群,您需要运行terraform destroy

摘要

在本书的早期章节中,我们探讨了 Kubernetes 如何与声明性应用程序管理方法相配合;也就是说,您定义所需的状态,然后让 Kubernetes 来处理其余的事情。在本章中,我们看了一些工具,这些工具可以帮助我们以类似的方式管理我们的云基础设施。我们介绍了 Terraform 作为一种可以帮助我们管理基础设施状态的工具,并介绍了将基础设施视为代码的概念。

然后,我们使用 Terraform 在 Amazon EKS 中创建了一个基本安全、生产就绪的 Kubernetes 集群。我们研究了 Ingress 对象,并了解了使用它的主要动机,以及它提供的各种优势。然后,我们在一个高可用的 Kubernetes 集群上部署了两个应用程序版本,并探讨了一些允许我们改进水平扩展有状态应用程序的概念。这让我们一窥了运行有状态应用程序所面临的挑战,并且我们将在第十四章中探讨更多处理这些挑战的方法,在 Kubernetes 中运行有状态组件

在下一章中,我们将继续查看如何通过进一步保护我们的集群来继续准备生产。

第十三章: Kubernetes 中的运行时和网络安全

概述

在本章中,我们将看看各种资源,我们可以使用来保护在我们集群中运行的工作负载。我们还将了解一个粗略的威胁模型,并将其应用于设计一个安全的集群,以便我们可以防御我们的集群和应用程序免受各种威胁。到本章结束时,您将能够创建 Role 和 ClusterRole,以及 RoleBinding 和 ClusterRoleBinding 来控制任何进程或用户对 Kubernetes API 服务器和对象的访问。然后,您将学习如何创建 NetworkPolicy 来限制应用程序与数据库之间的通信。您还将学习如何创建 PodSecurityPolicy 来确保应用程序的运行组件符合定义的限制。

介绍

在过去的几章中,我们戴上了 DevOps 的帽子,学习了如何在 Kubernetes 中设置集群,以及如何安全地部署新的应用程序版本而不会中断。

现在,是时候稍微转换一下,摘下我们的 DevOps 帽子,戴上我们的安全分析师帽子。首先,我们将看看有人可能攻击我们的 Kubernetes 集群的地方,以及未经授权的用户如何可能在我们的集群中造成严重破坏。之后,我们将介绍 Kubernetes 的一些安全原语以及我们如何对抗最常见的攻击形式。最后,我们将进一步修改我们的应用程序,并演示一些这些安全原语是如何工作的。

但在我们开始任何工作之前,让我们首先简要地看一下现代 Web 应用程序安全的各个关注领域,以及为我们的集群实施有效安全的基本范式。我们将首先检查我们所谓的“云原生安全的 4C”。

威胁建模

本章的范围远远超出了充分教授许多必要的安全学科的范围,以便您对现代工作负载安全应该如何实施和编排有严格的理解。然而,我们将简要了解我们应该如何思考。威胁建模是一种学科,我们在其中检查我们的应用程序可能受到攻击或未经授权使用的各个领域。

例如,考虑一个 HTTP Web 服务器。它通常会暴露端口 80 和 443 以提供 Web 流量服务,但它也作为潜在攻击者的入口点。它可能在某个端口上暴露 Web 管理控制台。它可能打开某些其他管理端口和 API 访问,以允许其他软件进行自动化管理。应用程序运行时可能需要定期处理敏感数据。用于创建和交付应用程序的整个端到端流水线可能暴露出各种容易受到攻击的点。应用程序依赖的加密算法可能会因暴力攻击的增加而被破坏或过时。所有这些都代表了我们的应用程序可能受到攻击的各个领域。

组织应用程序的一些攻击向量的简单方法是记住缩写STRIDE。它代表以下类型的攻击:

  • S欺骗:用户或应用程序伪装成其他人。

  • T篡改:未经相关利益相关者同意更改任何数据或提供信息。

  • R否认:否认参与行为或无法追踪特定用户的任何行为。

  • I信息泄露:窃取你未被授权获取的特权或敏感信息。

  • D拒绝服务:向服务器发送虚假请求以使其资源饱和,并拒绝其提供预期目的的能力。

  • E特权提升:通过利用漏洞获得对受限资源或特权的访问。

许多黑客发动的攻击都旨在执行上述一项或多项行动,通常是为了危害我们数据的机密性、完整性和可用性。考虑到这一点,我们可以使用一个心智模型来思考我们的系统可能存在威胁的各个部分在现代云原生应用程序堆栈中的位置。这个心智模型被称为“云原生安全的 4C”,我们将使用它来组织我们对 Kubernetes 安全原语的探索。理想情况下,通过利用所有这些原语,这应该能够让您对应用程序在 Kubernetes 环境中对抗类 STRIDE 攻击具有较高的信心。

云原生安全的 4C

安全可以并且应该组织成层。这被认为是安全的“深度防御”方法,并且被技术界普遍认为是防止任何单个组件暴露整个系统的最佳方式。当涉及到云原生应用程序时,我们认为安全分为四个层次:保护您的代码、容器、集群和云。以下图表显示了它们是如何组织的。这帮助我们想象,如果在较低层次发生了妥协,它几乎肯定会妥协依赖它的更高层次:

图 13.1:云原生安全的 4C

图 13.1:云原生安全的 4C

由于本书侧重于 Kubernetes,我们将重点关注集群安全,然后开始在我们的示例应用程序中实施一些建议。

注意

有关其他 C 的建议,请查看此链接:kubernetes.io/docs/concepts/security/overview/

集群安全

一种思考 Kubernetes 的方式是将其视为一个巨大的自我编排的计算、网络和存储池。因此,在许多方面,Kubernetes 就像一个云平台。理解这种等价性很重要,因为这种心理抽象使我们能够以集群操作员与集群开发人员的不同方式进行推理。集群操作员希望确保集群的所有组件都安全,并且针对任何工作负载进行了加固。集群开发人员将关注确保他们为 Kubernetes 定义的工作负载在集群内安全运行。

在这里,您的工作变得有点容易 - 大多数 Kubernetes 的云提供商提供的服务将为您确保 Kubernetes 控制平面的安全。如果由于某种原因,您无法利用云提供商的服务,您将希望在此链接的文档中阅读有关在此链接上保护您的集群的更多信息:kubernetes.io/docs/tasks/administer-cluster/securing-a-cluster/

即使您使用的是云提供商的服务,仅仅因为他们在保护您的控制平面并不意味着您的 Kubernetes 集群是安全的。您不能依赖于云提供商的安全性的原因是,您的应用程序、其容器或糟糕的策略实施可能会使您的基础设施非常容易受到攻击。因此,现在,我们需要讨论如何在集群内保护工作负载。

注意

Kubernetes 社区正在积极开展工作,以改进安全概念和实施。相关的 Kubernetes 文档应经常重新审视,以确定是否已经进行了改进。

为了加强我们内部集群的安全性,我们需要关注以下三个概念:

  • Kubernetes RBAC:这是 Kubernetes 的主要策略引擎。它定义了一套角色和权限系统,以及如何将权限授予这些角色。

  • 网络策略:这些是(取决于您的容器网络接口插件)在 Pod 之间充当“防火墙”的策略。将它们视为 Kubernetes 感知的网络访问控制列表。

  • Pod 安全策略:这些是在特定范围(命名空间、整个集群)定义的,并且作为 Pod 在 Kubernetes 中允许运行的定义。

我们不会涵盖在 etcd 中对 Kubernetes Secrets 进行加密,因为大多数云提供商要么为您处理这个问题,要么实现是特定于该云提供商的(例如 AWS KMS)。

Kubernetes RBAC

在我们深入研究 RBAC 之前,请回顾一下第四章中关于 Kubernetes 如何授权对 API 的请求的内容,我们了解到有三个阶段-认证、授权和准入控制。我们将在第十六章中更多地了解准入控制器。

Kubernetes 支持多种不同的集群认证方法,您需要参考您的云提供商的文档,以获取有关其特定实现的更多详细信息。

授权逻辑是通过一种称为RBAC的东西处理的。它代表基于角色的访问控制,是我们约束某些用户和组只能执行其工作所需的最低权限的基础。这基于软件安全中的一个概念,称为“最小特权原则”。例如,如果你是一家信用卡处理公司的软件工程师,PCI DSS合规要求你不应该访问生产集群和客户数据。因此,如果你确实可以访问生产集群,你应该有一个没有特权的角色。

RBAC 是由集群管理员通过四种不同的 API 对象实现的:RolesRoleBindingsClusterRolesClusterRoleBindings。让我们通过检查一个图表来看它们是如何一起工作的:

图 13.2:不同对象相互作用以实现 RBAC

图 13.2:不同对象相互作用以实现 RBAC

在这个图表中,我们可以看到 Kubernetes 的User/GroupServiceAccount对象通过绑定到RoleClusterRole来获得他们的权限。让我们分别了解这些对象。

角色

这是一个 Role 的样本规范:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: test-role
rules:
  - verbs:
      - "list"
    apiGroups:
      - ""
    resources:
      - "pods"

各种字段定义了 Role 应该具有的权限:

  • namespace:Roles 适用于 Kubernetes 命名空间,这在这个字段中定义。这使得 Role 与 ClusterRole 不同,后者的权限适用于集群中的任何命名空间。

  • 动词:这些描述了我们允许的 Kubernetes 操作。一些常用动词的例子包括getlistwatchcreateupdatedelete。还有更多,但这些通常对大多数用例来说已经足够了。如果需要复习,请参考第四章Kubernetes API部分,如何与 Kubernetes(API 服务器)通信

  • apiGroups:这些描述了 Role 将访问的 Kubernetes API 组。这些被指定为<group>/<version>(比如apps/v1)。如果使用 CustomResourceDefinitions,这些 API 组也可以在这里引用。

注意

Kubernetes 随附的 API 组的完整列表可以在这里找到(截至版本 1.18):kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/

  • resources:这些描述了我们正在讨论的 API 对象,并由对象定义的Kind字段中的值定义;例如,deploymentsecretconfigmappodnode等。

RoleBinding

如前图所示,RoleBinding 将角色绑定或关联到 ServiceAccounts、用户或用户组。以下是 RoleBinding 的示例规范:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-role-binding
  namespace: default
roleRef:
  name: test-role
  kind: ClusterRole
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: test-sa
    namespace: default 

此规范定义了应该能够使用角色执行需要在 Kubernetes 中进行授权的操作的主体:

  • subjects:这指的是经过身份验证的 ServiceAccount、用户或应该能够使用此角色的组。

  • roleRef:这指的是他们可以承担的角色。

ClusterRole

ClusterRole 在每个方面都与 Role 相同,除了一个方面。它不仅在一个 Kubernetes 命名空间内授予权限,而且在整个集群范围内授予权限。

ClusterRoleBinding

这与 RoleBinding 相同,只是它必须绑定到 ClusterRole 而不是 Role。您不能将 ClusterRoleBinding 绑定到 Role,也不能将 RoleBinding 绑定到 ClusterRole。

有关 RBAC 策略的一些重要说明

  • RBAC 策略文档仅允许。这意味着,默认情况下,主体没有访问权限,只有通过 RoleBinding 或 ClusterRoleBinding 才能具有相应角色或集群角色中规定的特定访问权限。

  • 绑定是不可变的。这意味着一旦您将主体绑定到角色或集群角色,就无法更改。这是为了防止特权升级。因此,实体可以被授予修改对象的权限(对于许多用例来说已经足够好),同时防止它提升自己的特权。如果需要修改绑定,只需删除并重新创建。

  • 一个可以创建其他 ClusterRoles 和 Roles 的 ClusterRole 或 Role 只能授予最多与其相同的权限。否则,这将是一个明显的特权升级路径。

服务账户

在前几章中,当我们学习有关 Minikube 和 Kops 的身份验证时,我们看到 Kubernetes 生成了我们使用的证书。在 EKS 的情况下,使用了 AWS IAM 角色和 AWS IAM Authenticator。

事实证明,Kubernetes 有一个特殊的对象类型,允许集群内的资源与 API 服务器进行身份验证。

我们可以使用 ServiceAccount 资源来允许 Pods 接收 Kubernetes 生成的令牌,它将传递给 API 服务器进行身份验证。所有官方的 Kubernetes 客户端库都支持这种类型的身份验证,因此这是从集群内部进行程序化 Kubernetes 集群访问的首选方法。

当您以集群管理员身份运行时,可以使用kubectl使用--as参数对特定 ServiceAccount 进行身份验证。对于之前显示的示例 ServiceAccount,这将看起来像这样:

kubectl --as=system:serviceaccount:default:test-sa get pods

我们将学习这些对象如何一起工作,以便在以下练习中控制访问。

练习 13.01:创建 Kubernetes RBAC ClusterRole

在这个练习中,我们将创建一个 ClusterRole 和 ClusterRoleBinding。然后,我们将成为用户并继承他们的权限,如 ClusterRole 所定义的,并演示 Kubernetes 如何基于规则阻止对某些 API 的访问。让我们开始吧:

  1. 首先,我们将从我们在练习 12.02中使用的 Terraform 文件中重新创建 EKS 集群,使用 Terraform 创建 EKS 集群。如果您已经有main.tf文件,可以使用它。否则,您可以运行以下命令获取它:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.02/main.tf

现在,依次使用以下两个命令,将您的集群资源恢复运行:

terraform init
terraform apply

注意:

在执行任何这些练习之后,如果您计划在较长时间后继续进行以下练习,最好释放集群资源以停止 AWS 计费。您可以使用terraform destroy命令来做到这一点。然后,当您准备进行练习或活动时,可以运行此步骤将所有内容恢复在线。

如果任何练习或活动依赖于在先前练习中创建的对象,您还需要重新创建这些对象。

  1. 现在,我们将为我们的 RBAC 资源创建三个 YAML 文件。第一个是一个 ServiceAccount,它允许我们通过集群授予的身份和认证令牌。创建一个名为sa.yaml的文件,内容如下:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-sa
  namespace: default
  1. 接下来,我们将创建一个 ClusterRole 对象并分配一些权限。创建一个名为cr.yaml的文件,内容如下:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: default
  name: test-sa-cluster-role
rules:
  - verbs:
      - "list"
    apiGroups:
      - ""
    resources:
      - "pods"

我们正在定义一个ClusterRole,它具有列出任何命名空间中所有 Pod 的能力,但其他操作不能执行。

  1. 接下来,我们将创建一个ClusterRoleBinding对象,将创建的 ServiceAccount 和 ClusterRole 绑定在一起。创建一个名为crb.yaml的文件,内容如下:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-sa-cluster-role-binding
  namespace: default
roleRef:
  name: test-sa-cluster-role
  kind: ClusterRole
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: test-sa
    namespace: default

在这些文件中,我们定义了三个对象:ServiceAccountClusterRoleClusterRoleBinding

  1. 运行以下命令来创建此 RBAC 策略,以及我们的 ServiceAccount:
kubectl apply -f sa.yaml -f cr.yaml -f crb.yaml

您应该看到以下响应:

图 13.3:创建 ServiceAccount、ClusterRole 和 ClusterRoleBinding

图 13.3:创建 ServiceAccount、ClusterRole 和 ClusterRoleBinding

  1. 在接下来的步骤中,我们将演示使用我们的服务账户的 ClusterRole 将阻止我们描述 Pods。但在那之前,让我们先获取 Pod 的列表,并证明一切仍然正常工作。通过运行以下命令来实现:
kubectl get pods --all-namespaces

您应该看到以下响应:

图 13.4:获取 Pod 列表

图 13.4:获取 Pod 列表

  1. 现在,让我们描述第一个 Pod。这里第一个 Pod 的名称是aws-node-fzr6m。在这种情况下,describe命令将如下所示:
kubectl describe pod -n kube-system aws-node-fzr6m

请使用您集群中的 Pod 名称。您应该看到类似以下的响应:

图 13.5:描述 aws-node-fzr6m Pod

图 13.5:描述 aws-node-fzr6m Pod

上述截图显示了describe命令输出的截断版本。

  1. 现在,我们将运行与之前相同的命令,但这次假装是使用当前绑定到我们创建的 ClusterRole 和 ClusterRoleBinding 的 ServiceAccount 的用户。我们将使用kubectl--as参数来实现这一点。因此,命令将如下所示:
kubectl --as=system:serviceaccount:default:test-sa get pods --all-namespaces

请注意,我们可以假设 ClusterRole,因为我们是我们创建的集群中的管理员。您应该看到以下响应:

图 13.6:假设 test-sa ServiceAccount 获取 Pod 列表

图 13.6:假设 test-sa ServiceAccount 获取 Pod 列表

确实,这仍然有效。正如您可能还记得的那样,从步骤 3中可以看到,我们提到了list作为一个允许的动词,这是用于获取某种类型的所有资源列表的动词。

  1. 现在,让我们看看如果具有我们创建的 ClusterRole 的用户尝试描述一个 Pod 会发生什么:
kubectl --as=system:serviceaccount:default:test-sa describe pod -n kube-system aws-node-fzr6m

您应该看到以下响应:

图 13.7:禁止错误

图 13.7:禁止错误

kubectl describe命令使用get动词。回想一下步骤 3,它不在我们的 ClusterRole 允许的动词列表中。

如果这是一个用户(或黑客)试图使用任何不允许的命令,我们将成功阻止它。Kubernetes 文档网站上有许多实用的 RBAC 示例。在本章中讨论 Kubernetes 中所有 RBAC 的设计模式超出了范围。我们只能说:在可能的情况下,您应该实践“最小特权原则”,以限制对 Kubernetes API 服务器的不必要访问。也就是说,每个人都应该获得完成工作所需的最低访问级别;并非每个人都需要成为集群管理员。

虽然我们无法就公司的安全性做出具体建议,但我们可以说有一些不错的“经验法则”,可以表述如下:

  • 在可能的情况下,尝试将集群贡献者/用户放在角色中,而不是 ClusterRole 中。由于角色受到命名空间的限制,这将防止用户未经授权地访问另一个命名空间。

  • 只有集群管理员应该访问 ClusterRoles,这应该是有限且临时的。例如,如果您进行值班轮换,工程师负责您的服务的可用性,那么他们在值班期间应该只有管理员 ClusterRole。

网络策略

Kubernetes 中的 NetworkPolicy 对象本质上是 Pod 和命名空间级别的网络访问控制列表。它们通过使用标签选择(例如服务)或指示 CIDR IP 地址范围来允许特定端口/协议上的访问。

这对于确保安全非常有帮助,特别是当您在集群上运行多个微服务时。现在,想象一下您有一个为您的公司托管许多应用程序的集群。它托管了一个运行开源库的营销网站,一个包含敏感数据的数据库服务器,以及一个控制对该数据访问的应用服务器。如果营销网站不需要访问数据库,那么它就不应该被允许访问数据库。通过使用 NetworkPolicy,我们可以防止营销网站中的漏洞或错误允许攻击者扩大攻击,以便他们可以通过阻止营销网站 Pod 甚至无法与数据库通信来访问您的业务数据。让我们来看一个示例 NetworkPolicy 文档并解释它:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: sample-network-policy
  namespace: my-namespace
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 192.18.0.0/16
        except:
        - 192.18.1.0/24
    - namespaceSelector:
        matchLabels:
          project: sample-project
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 3257
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5832

让我们来看看这个 NetworkPolicy 的一些字段:

  • 它包含了我们在本章前面描述的标准apiVersionkindmetadata字段。

  • podSelector:它应该在命名空间中查找的标签,以应用策略。

  • policyTypes:可以是入口、出口或两者。这意味着网络策略适用于被选择的 Pod 中进入的流量、离开被选择的 Pod 的流量,或两者。

  • Ingress:这需要一个from块,定义了策略中流量可以从哪里发起。这可以是一个命名空间、一个 Pod 选择器或一个 IP 地址块和端口组合。

  • Egress:这需要一个to块,并定义了网络策略中允许流量去哪里。这可以是一个命名空间、一个 Pod 选择器或一个 IP 地址块和端口组合。

您的 CNI 可能没有成熟的 NetworkPolicies 实现,因此请务必查阅您的云提供商的文档以获取更多信息。在我们使用 EKS 设置的集群中,它使用的是 Amazon CNI。我们可以使用Calico,一个开源项目,来增强现有的 EKS CNI,并弥补在执行 NetworkPolicy 声明方面的不足。值得一提的是,Calico 也可以作为 CNI 使用,但我们将只在以下练习中使用其补充功能来执行 NetworkPolicy。

练习 13.02:创建 NetworkPolicy

在这个练习中,我们将实现 Calico 来增强 Amazon CNI 在 EKS 中可用的 NetworkPolicy 声明的即插即用执行。让我们开始吧:

  1. 运行以下命令安装带有 Calico 的 Amazon CNI:
kubectl apply -f https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/release-1.5/config/v1.5/calico.yaml

你应该看到类似于以下的响应:

图 13.8:安装带有 Calico 的 Amazon CNI

图 13.8:安装带有 Calico 的 Amazon CNI

  1. 要验证您是否成功部署了与 Calico 对应的 DaemonSet,请使用以下命令:
kubectl get daemonset calico-node --namespace kube-system

您应该看到calico-node DaemonSet,如下所示:

图 13.9:检查 calico-node DaemonSet

图 13.9:检查 calico-node DaemonSet

  1. 现在,让我们创建我们的 NetworkPolicy 对象。首先,创建一个名为net_pol_all_deny.yaml的文件,内容如下:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

这个策略是一个非常简单的 NetworkPolicy。它表示不允许流入或流出集群的 Pod 之间的流量。这是我们将继续扩展我们应用程序的安全基础。

  1. 让我们使用以下命令应用我们的策略:
kubectl apply -f net_pol_all_deny.yaml

你应该看到以下响应:

networkpolicy.networking.k8s.io/default-deny created

现在,我们的集群中没有流量流动。我们可以通过部署我们的应用程序来证明这一点,因为它需要网络来与自身通信。

  1. 作为一个测试应用程序,我们将使用与Exercise 12.04部署应用程序版本更新中使用的相同应用程序。如果您已经有该 YAML 文件,可以使用它。否则,运行以下命令以在您的工作目录中获取该文件:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.04/with_redis.yaml

然后,使用以下命令部署应用程序:

kubectl apply -f with_redis.yaml

你应该看到以下响应:

图 13.10:部署我们的应用程序

图 13.10:部署我们的应用程序

  1. 现在,让我们使用以下命令检查我们部署的状态:
kubectl describe deployment kubernetes-test-ha-application-with-redis-deployment

你应该看到以下响应:

图 13.11:检查我们应用程序的状态

图 13.11:检查我们应用程序的状态

这是一个截断的截图。正如你所看到的,我们有一个问题,即无法与 Redis 通信。修复这个问题将是Activity 13.01超越基本操作的一部分。

  1. 现在我们将测试网络访问,因此在一个单独的终端窗口中,让我们启动我们的代理:
kubectl proxy

你应该看到这个响应:

Starting to serve on 127.0.0.1:8001

验证 NetworkPolicy 是否阻止流量的另一种方法是使用我们的curl命令:

curl localhost:8001/api/v1/namespaces/default/services/kubernetes-test-ha-application-with-redis:/proxy/get-number

你应该看到类似于这样的响应:

Error: 'dial tcp 10.0.0.193:8080: i/o timeout'
Trying to reach: 'http:10.0.0.193:8080/get-number'%

正如我们所看到的,我们能够防止 Kubernetes 集群中 Pod 之间的未经授权通信。通过利用 NetworkPolicies,我们可以防止攻击者在能够 compromise 集群、容器或源代码的一些组件后造成进一步的破坏。

PodSecurityPolicy

到目前为止,我们已经学习并测试了 Kubernetes RBAC 以防止未经授权的 API 服务器访问,并且还应用了 NetworkPolicy 以防止不必要的网络通信。网络之外安全性的下一个最重要领域是应用程序运行时。攻击者需要访问网络来进出,但他们还需要一个容易受攻击的运行时来做更严重的事情。这就是 Kubernetes PodSecurityPolicy 对象帮助防止这种情况发生的地方。

PodSecurityPolicy 对象与特定类型的 AdmissionController 重叠,并允许集群操作员动态定义已被允许在集群上调度的 Pod 的最低运行时要求。

为了确切了解 PodSecurityPolicies 如何有用,让我们考虑以下情景。您是一家大型金融机构的 Kubernetes 集群管理员。您的公司以符合 ITIL 的方式(ITIL 是 IT 服务的标准变更管理框架)使用基于票据的变更管理软件,以确保对环境所做的更改是稳定的。这可以防止开发人员在生产环境中做出灾难性的事情。为了跟上客户要求的市场变化速度,您需要一种程序化的方式来使开发人员能够更自主地进行更改管理。但您还需要以安全和符合某些标准的方式来做到这一点。PodSecurityPolicies 帮助我们做到这一点,因为它们允许管理员在软件中创建策略定义,并在 Pod 被允许进入集群时执行。这意味着开发人员可以更快地移动,而集群管理员仍然可以证明他们的环境完全符合设定的标准。

进一步扩展这种情况,您可能希望阻止用户将其容器以 root 用户身份运行,以防攻击者利用 Docker 中的任何漏洞。通过应用 PodSecurityPolicy,您可以防止用户意外部署不安全的容器。

既然我们已经看到它们如何有用,让我们考虑一个示例 PodSecurityPolicy 并对其进行检查:

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp-example
  namespace: default
spec:
  privileged: true
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: MustRunAs
    ranges:
      - min: 1
        max: 2500
  runAsUser:
    rule: MustRunAsNonRoot
  fsGroup:
    rule: MustRunAs
    ranges:
      - min: 655
        max: 655
  volumes:
    - '*'

让我们在这里检查一些值得注意的字段:

  • metadata.namespace: 这将在default命名空间中创建 PodSecurityPolicy,并将应用于同一命名空间中的 Pod。

  • privileged: 这控制容器是否允许在节点上以特权执行上下文中运行,这实际上授予容器对主机的根级访问权限。您可以在这里找到有关特权容器的更多信息:docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

  • seLinux: 这定义了任何 SELinux 设置。一些 Kubernetes 集群在 SELinux 环境中运行,这些环境在集群外实现了称为“强制访问控制”的东西。这允许将这些控制投影到集群中。通过声明RunAsAny,我们允许任何 SELinux 用户。

  • supplementalGroups: 这是策略的一个强制字段。它基本上告诉我们,我们允许任何 Linux 用户组 ID(GID)。在此示例规范中,我们说允许来自 ID 为 1 到 2500 的任何 Linux 用户组的用户。

  • runAsUser: 这允许我们指定可以在 Pod 中运行任何进程的特定 Linux 用户。通过声明MustRunAsNonRoot,我们说 Pod 中的任何进程都不能以 root 权限运行。

  • fsGroup: 这是容器进程必须以其运行的 Linux 组 ID,以便与集群上的某些卷进行交互。因此,即使 Pod 上存在卷,我们也可以限制该 Pod 中的某些进程访问它。在此示例规范中,我们说只有具有 GID 为 655 的devops组中的 Linux 用户可以访问该卷。这将适用于 Pod 在集群中的位置或卷的位置。

  • : 这使我们能够允许可以挂载到该 Pod 的不同类型的卷,例如configmappersistentVolumeClaim。在此示例规范中,我们已经指定了*(星号),这意味着所有类型的卷都可以被该 Pod 中的进程使用。

现在我们已经了解了规范中不同字段的含义,我们将在以下练习中创建一个 PodSecurityPolicy。

练习 13.03:创建和测试 PodSecurityPolicy

在这个练习中,我们将创建一个 PodSecurityPolicy 并将其应用到我们的集群,以演示我们应用后集群中 Pod 必须遵守的功能类型。让我们开始吧:

  1. 创建一个名为pod_security_policy_example.yaml的文件,内容如下:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp-example
  namespace: default
spec:
  privileged: false
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: MustRunAs
    ranges:
      - min: 1
        max: 2500
  runAsUser:
    rule: MustRunAsNonRoot
  fsGroup:
    rule: MustRunAs
    ranges:
      - min: 655
        max: 655
  volumes:
    - '*'
  1. 要将此应用到集群中,请运行以下命令:
kubectl apply -f pod_security_policy_example.yaml

您应该会看到以下响应:

podsecuritypolicy.policy/psp-example created

为了检查我们的策略是否得到执行,让我们尝试创建一个不符合这个策略的 Pod。现在我们有一个名为MustRunAsNonRoot的策略,所以我们应该尝试以 root 身份运行一个容器,看看会发生什么。

  1. 要创建一个违反这个 PodSecurityPolicy 的 Docker 容器,首先创建一个名为Dockerfile的文件,内容如下:
FROM debian:latest
USER 0
CMD echo $(whoami)

这个Dockerfile的第二行切换到 root 用户(由 UID 0表示),然后echo命令应该告诉我们在容器启动时运行的用户是谁。

  1. 通过运行以下命令构建 Docker 镜像:
docker build -t root .

您应该会看到以下响应:

图 13.12:构建我们的 Docker 镜像

图 13.12:构建我们的 Docker 镜像

  1. 让我们运行我们的 Docker 容器:
docker run root:latest

您应该会看到以下响应:

root

正如我们所看到的,这个容器将以 root 身份运行。

  1. 现在,我们需要从这个容器创建一个 Pod。创建一个名为pod.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: rooter
spec:
  containers:
    - name: rooter
      image: packtworkshops/the-kubernetes-workshop:root-tester

您可以将自己的镜像推送到 Docker Hub 存储库并替换此链接,或者您可以使用我们已经提供的容器以方便使用。作为一个一般的经验法则,当下载某些应该以 root 访问权限运行的东西时,您应该始终小心。

  1. 默认情况下,PodSecurityPolicy 在用户、组或 ServiceAccount 上安装了use权限之前不会执行任何操作,这些用户、组或 ServiceAccount 将创建 Pod。为了模仿这一点,我们将快速创建一个 ServiceAccount:
kubectl create serviceaccount fake-user

您应该会看到以下响应:

serviceaccount/fake-user created
  1. 现在,让我们创建一个将受到这个 PodSecurityPolicy 约束的角色:
kubectl create role psp:unprivileged --verb=use --resource=podsecuritypolicy --resource-name=psp-example

请注意,这是创建角色的另一种快速方法。在这里,psp:unprivileged对应于角色的名称,而标志对应于我们之前学习的字段。我们使用--resource-name标志将角色应用到我们特定的 PodSecurityPolicy。您应该会得到以下响应:

role.rbac.authorization.k8s.io/psp:unprivileged created
  1. 让我们使用 RoleBinding 将这个角色绑定到我们的 ServiceAccount:
kubectl create rolebinding fake-user:psp:unprivileged --role=psp:unprivileged --serviceaccount=psp-example:fake-user

在这里,我们使用了类似于上一步中使用的命令。您应该会看到以下响应:

rolebinding.rbac.authorization.k8s.io/fake-user: psp:unprivileged created
  1. 现在,让我们假扮成这个用户,尝试创建这个 Pod:
kubectl --as=system:serviceaccount:psp-example:fake-user apply -f pod.yaml

您应该会看到以下响应:

图 13.13:尝试在假用户 ServiceAccount 的假设下创建一个 Pod

图 13.13:尝试在假用户 ServiceAccount 的假设下创建 Pod

在本章的开头,我们探讨了集群安全的 4C,然后在本章的整个过程中,我们看到了 Kubernetes 允许我们以不同的方式加固集群以抵御各种攻击的方法。我们了解到 RBAC 策略允许我们控制对 API 和对象的访问,NetworkPolicy 允许我们加固网络拓扑,而 PodSecurityPolicy 则帮助我们防止受损的运行时。

现在,让我们在以下活动中将这些概念结合起来。

活动 13.01:保护我们的应用程序

就目前而言,我们在上一章中的应用程序已经相当安全了。但是,我们需要做的是防止用户部署特权 Pod,并确保我们的应用程序可以与外部世界和其数据存储通信。对于这个应用程序的正确解决方案应该具有以下功能:

  • 应用程序应该无缝工作,就像我们在上一章中演示的那样,但现在,它应该阻止任何不必要的网络流量。这里的不必要是指只有与 Redis 服务器通信的 Pod 应用程序,而且该应用程序只能与其他 IP 范围通信。

  • Exercise 13.02Creating a NetworkPolicy中,我们看到由于高度限制性的 NetworkPolicy,我们的应用程序无法工作。然而,在这种情况下,您应该看到应用程序运行并输出类似于以下内容的内容:图 13.14:活动 13.01 的预期输出

图 13.14:活动 13.01 的预期输出

以下是一些可以帮助您完成此活动的步骤:

  1. 确保您拥有集群基础架构和Exercise 13.01, Creating a Kubernetes RBAC ClusterRole中的所有对象。

  2. 创建名为pod_security_policy.yaml的文件(然后应用它)。在创建此文件时,请记住上面第一个要点中描述的功能。您可能需要重新访问PodSecurityPolicy部分,在那里我们详细描述了此类文件中使用的每个字段。

  3. 创建一个名为network_policy.yaml的文件。在创建此文件时,请记住上面第二个要求中列出的内容。您可能需要重新访问NetworkPolicies部分,我们在其中详细描述了此类文件中使用的每个字段。确保在创建后应用此策略。

  4. 如果您的集群中仍在部署Exercise 14.02, Creating a NetworkPolicy中的应用程序,则可以继续下一步。否则,请重新运行该练习中的步骤 56

  5. 现在,测试该应用程序。

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD

另外,考虑在完成本章后删除 NetworkPolicy 和 PodSecurityPolicy,以避免对后续章节造成干扰。

摘要

在我们构建生产就绪的 Kubernetes 环境的过程中,安全性是一个关键方面。考虑到这一点,在本章中,我们研究了威胁建模如何让我们以对抗性的方式思考我们的应用基础架构,以及它如何告诉我们如何防御攻击。然后,我们看了一下云原生安全的 4C,以了解我们的攻击面在哪里,然后看了一下 Kubernetes 如何帮助我们在集群中安全地运行工作负载。

Kubernetes 具有几个安全功能,我们可以利用这些功能来保护我们的集群。我们了解了三个重要的安全措施:RBAC、NetworkPolicies 和 PodSecurityPolicies。我们还了解了它们在保护对集群的访问、保护容器网络和保护容器运行时方面的各种应用。

在下一章中,我们将探讨如何在 Kubernetes 中管理存储对象,并处理具有状态的应用程序。

第十四章: 在 Kubernetes 中运行有状态的组件

概述

在本章中,我们将扩展我们的技能,超越无状态应用程序,学习如何处理有状态应用程序。我们将了解 Kubernetes 集群操作员可用的各种状态保留机制,并推导出一个心智模型,以确定在何处可以调用某些选项来有效运行应用程序。我们还将介绍 Helm,这是一个用于部署具有各种 Kubernetes 对象的复杂应用程序的有用工具。

通过本章的学习,您将能够同时使用 StatefulSets 和 PersistentVolumes 来运行需要在 Pod 中断期间保留基于磁盘的状态的应用。您还将能够使用 Helm charts 部署应用程序。

介绍

根据您到目前为止学到的一切,您知道 Pod 和其中运行的容器被认为是短暂的。这意味着不能依赖它们的稳定性,因为 Kubernetes 将会干预并将它们移动到集群中的其他位置,以符合集群中各种清单指定的期望状态。但是这里存在一个问题 - 我们该如何处理我们的应用程序的部分,这些部分依赖于从一次交互到下一次交互的状态持久化?如果没有诸如可预测的 Pod 命名和可靠的存储操作等特定保证(我们将在本章后面学习),这样的有状态组件可能会在 Kubernetes 重新启动相关 Pod 或将其移动时失败。然而,在深入讨论上述主题的细节之前,让我们简要谈谈有状态应用程序以及在容器化环境中运行它们的挑战。

有状态应用

我们在《第十二章,您的应用程序和 HA》中简要介绍了有状态性的概念。应用程序的有状态组件几乎对世界上所有的信息技术系统都是必需的。它们对于保持账户详细信息、交易记录、HTTP 请求信息以及许多其他用途都是必需的。在生产环境中运行这些应用程序的挑战部分原因几乎总是与网络或持久性机制有关。无论是旋转金属盘、闪存存储、块存储还是其他尚未被发明的工具,持久性在各种形式中都是非常难以处理的。这种困难的部分原因是因为所有这些形式都存在失败的非零概率,一旦你需要在生产环境中拥有数百甚至数千个存储设备,这个概率就会变得非常显著。如今,许多云服务提供商将为客户提供帮助,并提供托管服务来解决这个困难。在 AWS 的情况下,我们有诸如 S3、EBS、RDS、DynamoDB、Elasticache 等工具,这些工具可以帮助开发人员和运营商在没有太多重复工作的情况下顺利运行有状态应用程序(前提是您可以接受供应商锁定)。

一些公司在运行有状态应用和它们所依赖的持久性机制时面临的另一个权衡是,要么培训和维护一大批能够保持这些记录系统在线、健康和最新的员工,要么尝试开发一套工具和程序化强制执行的常见运营场景。这两种方法在组织规模扩大时所需的人力维护工作量上有所不同。

例如,以人为中心的运营方法一开始可以让事情迅速进行,但所有运营成本都会随着应用规模线性增长,最终,官僚主义会导致每次新员工的生产力回报递减。以软件为中心的方法需要更高的前期投资,但成本随着应用规模的对数增长,并且在出现意外错误时有更高的级联故障概率。

这些操作场景的一些例子包括配置和配置、正常操作、扩展输入/输出、备份和异常操作。异常操作的例子包括网络故障、硬盘故障、磁盘数据损坏、安全漏洞和特定应用程序的不规则性。特定应用程序的不规则性的例子可能包括处理特定于 MySQL 的排序问题、处理 S3 最终一致性读取故障、etcd Raft 协议解决错误等。

许多公司发现,他们更容易支付供应商支持费用,使用云托管产品提供,或者重新培训员工,而不是开发编程状态管理流程和软件。

Kubernetes 启用的开发生命周期的一个好处在于工作负载定义方面。公司越是努力地严格定义计算的最小逻辑单元(一个 pod 模板或 PersistentVolume 定义),它们就越能为 Kubernetes 干预不规则操作并适当编排整个应用做好准备。这在很大程度上是因为 Kubernetes 编排是一个经典的动态约束满足问题(CSP)。CSP 求解器可以利用的约束形式的信息越多,工作负载编排就会变得更可预测,因为可行稳态解的数量会减少。因此,以可预测的工作负载编排为最终目标,我们是否可以在 Kubernetes 中运行应用的状态组件?答案是毫无疑问的肯定。在 Kubernetes 中运行有状态的工作负载常常让人犹豫不决。我们从本书的开头就说过,pod 是短暂的,不应该依赖它们的稳定性,因为在节点故障的情况下,它们将被移动和重新启动。因此,在你决定在 Kubernetes 中运行数据库太冒险之前,请考虑一下——世界上最大的搜索引擎公司在一个与 Kubernetes 非常相似的工具中运行数据库。这告诉我们,不仅可能,而且实际上更好的是努力定义工作负载,使它们可以由编排器运行,因为它可能比人类更快地处理应用程序故障。

那么,我们如何实现这一点呢?对这个问题的答案是使用你之前学过的两个 Kubernetes 对象的组合-PersistentVolumesStatefulSets。这些在第 7第 9章介绍过,所以我们不会在这里详细说明它们的用法,除了说我们将把所有介绍性的主题结合起来,形成一个与我们的应用相关的示例。

有效的有状态工作负载编排的关键是模块化和抽象。这些是基本的软件概念,工程师们学习它们以便设计良构架构的软件系统,同样适用于良构架构的基础设施系统。让我们考虑下面的图表,作为在 Kubernetes 中运行数据库时模块化的一个例子:

图 14.1:Kubernetes 中的模块化有状态组件

图 14.1:Kubernetes 中的模块化有状态组件

正如你在前面的图表中所看到的,并且在本书中学到的,Kubernetes 由模块化组件组成。因此,通过利用 StatefulSet 资源,我们可以组合使用 PersistentVolumes、PersistentVolumeClaims、StorageClasses、pods 以及围绕它们的生命周期的一些特殊规则,从而更强有力地保证我们应用程序的持久性层的状态。

理解 StatefulSets

图 14.1中,我们可以看到 StatefulSet 被调用来管理 pod 的生命周期。StatefulSet(在 Kubernetes 的旧版本中,这被称为 PetSet)的操作方式与部署非常相似,我们提供一个 pod 模板,指定我们要运行的内容以及我们要运行多少个实例。StatefulSet 和部署之间的区别在于以下几点:

  • 一个可以依赖于 DNS 查询的清晰命名方案

这意味着在前面的图中,当我们将一个 StatefulSet 命名为mysql时,该 StatefulSet 中的第一个 pod 将始终是mysql-0。这与传统部署不同,传统部署中 pod 的 ID 是随机分配的。这也意味着,如果你有一个名为mysql-2的 pod,它崩溃了,它将在集群中使用完全相同的名称复活。

  • 更新必须进行的明确有序方式

根据此 StatefulSet 中的更新策略,每个 pod 将按非常特定的顺序关闭。因此,如果您有一个众所周知的升级路径(例如在 MySQL 的次要软件修订版本的情况下),您应该能够利用 Kubernetes 提供的软件更新策略之一。

  • 可靠的存储操作

由于存储是有状态解决方案中最关键的部分,因此 StatefulSet 采取的确定性操作至关重要。默认情况下,为 StatefulSet 配置的任何 PersistentVolume 都将被保留,即使该 StatefulSet 已被删除。虽然此行为旨在防止数据意外删除,但在测试期间可能会导致云提供商产生重大费用,因此您应该密切监视此行为。

  • 必须在 StatefulSet 中定义的 serviceName 字段

这个serviceName字段必须指向一个称为“无头”的服务,该服务指向这组 pod。这是为了允许使用常见的 Kubernetes DNS 语法单独地寻址这些 pod。例如,如果我的 StatefulSet 正在 default 命名空间中运行,并且名称为zachstatefulset,那么第一个 pod 将具有 DNS 条目zachstatefulset-0.default.svc.cluster.local。如果此 pod 失败,任何替换 pod 都将使用相同的 DNS 条目。

有关无头服务的更多信息,请访问此链接:kubernetes.io/docs/concepts/services-networking/service/#headless-services

部署与 StatefulSets

现在您已经以稍微更细粒度的方式介绍了 StatefulSets,那么在选择使用 PersistentVolumeClaim 的 StatefulSet 和部署之间应该根据什么基础进行选择呢?答案取决于您希望编排的内容。

从理论上讲,您可以使用两种类型的 Kubernetes 对象实现类似的行为。两者都创建 pod,都有更新策略,都可以使用 PVC 来创建和管理 PersistentVolume 对象。StatefulSets 的设计目的是为了提供前面列出的保证。通常,在编排数据库、文件服务器和其他形式的敏感持久性依赖应用程序时,您会希望有这些保证。

当我们了解到 StatefulSets 对于可预测地运行应用程序的有状态组件是有用的时,让我们看一个与我们相关的具体例子。正如您从以前的章节中回忆起,我们有一个小型计数器应用程序,我们正在重构以利用尽可能多的云原生原则。在本章中,我们将替换状态持久性机制并尝试一个新的引擎。

进一步重构我们的应用程序

我们现在希望将我们的应用程序进一步发展到云原生原则。让我们考虑一下,我们计数器应用程序的产品经理说我们的负载量非常大(您可以通过您的可观察性工具集来确认这一点),有些人并不总是得到一个严格递增的数字;有时,他们会得到相同数字的重复。因此,您与同事商讨后得出结论,为了保证递增的数字,您需要保证数据在应用程序中的访问和持久性。

具体来说,您需要保证针对此数据存储的操作是原子唯一的,在操作之间是一致的,与其他操作是隔离的,并且在故障时是持久的。也就是说,您正在寻找一个符合 ACID 标准的数据库。

有关 ACID 合规性的更多信息,请访问此链接:database.guide/what-is-acid-in-databases/

团队希望能够使用数据库,但他们宁愿不支付 AWS 运行该数据库的费用。如果他们以后在 GCP 或 Azure 上找到更好的交易,他们也宁愿不被锁定在 AWS 上。

因此,在谷歌上简要查看了一些选项后,您的团队决定使用 MySQL。MySQL 是更受欢迎的开源 RDBMS 解决方案之一,因此有很多关于在 Kubernetes 中作为数据库解决方案实施的文档、支持和社区建议。

现在,开始更改您的代码以支持使用 MySQL 支持的事务来递增计数器。因此,为了做到这一点,我们需要改变一些事情:

  • 更改我们的应用程序代码,以使用 SQL 而不是 Redis 来访问数据并递增计数器。

  • 修改我们的 Kubernetes 集群,以运行 MySQL 而不是 Redis。

  • 确保在发生灾难性故障时数据库下面的存储的持久性。

您可能会问自己为什么集群操作员或管理员需要能够理解和重构代码。Kubernetes 的出现加速了软件行业利用 DevOps 工具、实践和文化开始更快、更可预测地为客户提供价值的趋势。这意味着开始使用软件而不是人来扩展我们的操作。我们需要强大的自动化来取代以人为中心的流程,以便能够保证功能和交付速度。因此,基础架构设计师或管理员具有系统级软件工程经验,使他们能够协助重构代码库以利用更多的云原生实践,对他们的职业来说是一个巨大的好处,很快可能会成为所有 DevOps 工程师的工作要求。因此,让我们看看如何重构我们的应用程序以使用 MySQL 进行 StatefulSets 的事务处理。

注意

如果您还不熟悉编程,或者对作者选择的语言的语法(例如本例中的 Golang)不熟悉,您不必担心-所有解决方案都已经被解决并准备好使用。

首先,让我们检查Exercise 12.04使用状态管理部署应用程序的代码:

main.go

28 if r.Method == "GET" { 
29     val, err := client.Get("num").Result() 
30     if err == redis.Nil { 
31         fmt.Println("num does not exist") 
32         err := client.Set("num", "0", 0).Err() 
33         if err != nil { 
34             panic(err) 
35         } 
36     } else if err != nil { 
37         w.WriteHeader(500) 
38         panic(err) 
39     } else { 
40         fmt.Println("num", val) 
41         num, err := strconv.Atoi(val) 
42         if err != nil { 
43             w.WriteHeader(500) 
44             fmt.Println(err) 
45         } else { 
46             num++ 
47             err := client.Set("num", strconv.Itoa(num), 0).Err() 
48             if err != nil { 
49                 panic(err) 
50             } 
51             fmt.Fprintf(w, "{number: %d}", num) 
52         } 
53 } 

此步骤的完整代码可以在packt.live/3jSWTHB找到。

在上述代码中突出显示了我们访问持久层的两个实例。正如您所看到的,我们不仅没有使用事务,而且在代码中操作了值,因此无法保证这是一个严格递增的计数器。为了做到这一点,我们必须改变我们的策略。

注意

您可以在此链接找到使用 MySQL 容器所需的信息:hub.docker.com/_/mysql?tab=description

我们提供了使用 SQL 的重构应用程序。让我们来看看重构应用程序的代码:

main.go

38 fmt.Println("Starting HTTP server") 
39 http.HandleFunc("/get-number", func(w http.ResponseWriter, r      *http.Request) { 
40     if r.Method == "GET" { 
41         tx, err := db.Begin() 
42             if err != nil { 
43         panic(err) 
44         } 
45         _, err = tx.Exec(t1) 
46         if err != nil { 
47             tx.Rollback() 
48             fmt.Println(err) 
49         } 
50         err = tx.Commit() 
51         if err != nil { 
52             fmt.Println(err) 
53         } 
54         row := db.QueryRow(t2, 1) 
55         switch err := row.Scan(&num); err { 
56         case sql.ErrNoRows: 
57             fmt.Println("No rows were returned!") 
58         case nil: 
59             fmt.Fprintf(w, "{number: %d}\n", num) 
60         default: 
61             panic(err) 
62         } 
63     } else { 
64         w.WriteHeader(400) 
65         fmt.Fprint(w, "{\"error\": \"Only GET HTTP method is                supported.\"}") 
66     } 
67 }

此步骤的完整代码可以在packt.live/35ck7nX找到。

正如您所看到的,它与 Redis 代码大致相同,只是现在我们的值是在事务中设置的。与 Redis 不同,MySQL 不是一种易失性的内存数据存储,因此对数据库的操作必须持久化到磁盘才能成功,并且理想情况下,它们应该持久化到在 pod 中断时不会消失的磁盘上。让我们在下一个练习中设置我们应用程序的其他必需组件。

练习 14.01:部署带有 MySQL 后端的计数器应用

在这个练习中,我们将重新配置我们的计数器应用程序,使其与 MySQL 后端一起工作:

  1. 首先,我们将从 Terraform 文件中重新创建您的 EKS 集群练习 12.02使用 Terraform 在 EKS 上创建集群。如果您已经有main.tf文件,可以使用它。否则,您可以运行以下命令获取它:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter12/Exercise12.02/main.tf

现在,依次使用以下两个命令来启动并运行您的集群资源:

terraform init
terraform apply

注意

在执行任何练习之后,如果您计划在相当长的时间后继续进行以下练习,最好将集群资源分配给您以阻止 AWS 向您收费。您可以使用terraform destroy命令来做到这一点。然后,当您准备进行练习或活动时,可以运行此步骤将所有内容恢复在线。

如果任何练习或活动依赖于在先前练习中创建的对象,则您还需要重新创建这些对象。

  1. 运行以下命令获取定义所有所需对象的清单文件with_mysql.yaml
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter14/Exercise14.01/with_mysql.yaml

打开文件进行检查,以便我们可以检查这个 StatefulSet:

使用 MySQL.yaml

44 apiVersion: apps/v1 
45 kind: StatefulSet 
46 metadata: 
47   name: mysql 
48 spec: 
49   selector: 
50    matchLabels: 
51       app: mysql 
52   serviceName: mysql 
53   replicas: 1 
54   template: 
55     metadata: 
56       labels: 
57         app: mysql 
58     spec: 

此步骤的完整代码可以在packt.live/2R2WN3x找到。

注意

在这里,PersistentVolumeClaim 在启动时会自动将 10 GiB 卷从 Amazon EBS 绑定到每个 pod。 Kubernetes 将使用我们在 Terraform 文件中定义的 IAM 角色自动配置 EBS 卷。

当 pod 因任何原因中断时,Kubernetes 将在重新启动时自动将适当的 PersistentVolume 重新绑定到 pod,即使它在不同的工作节点上,只要它在相同的可用区。

  1. 让我们通过运行以下命令将其应用到我们的集群:
kubectl apply -f with_mysql.yaml

您应该看到这个响应:

图 14.2:部署使用 MySQL 后端的重构应用程序

图 14.2:部署使用 MySQL 后端的重构应用程序

  1. 现在在这个窗口运行kubectl proxy,然后让我们打开另一个终端窗口:
kubectl proxy

你应该看到这个回应:

Starting to serve on 127.0.0.1:8001
  1. 在另一个窗口中,运行以下命令来访问我们的应用程序:
curl localhost:8001/api/v1/namespaces/default/services/kubernetes-test-ha-application-with-mysql:/proxy/get-number

你应该看到这个回应:

{number: 1}

您应该看到应用程序按预期运行,就像我们在前几章中看到的那样。就像那样,我们有一个使用 MySQL 持久化数据的工作 StatefulSet 与我们的应用程序。

正如我们所说的,导致集群操作员不追求 StatefulSets 作为管理数据基础设施的一种方式的原因之一是错误地认为 PersistentVolumes 中的信息和它们绑定的 pod 一样短暂。这是不正确的。由 StatefulSet 创建的 PersistentVolumeClaims 如果删除了 pod 甚至 StatefulSet 也不会被删除。这是为了不惜一切代价保护这些卷中包含的数据。因此,对于清理,我们需要单独删除 PersistentVolume。集群操作员还可以利用其他工具来防止发生这种情况,例如更改 PersistentVolumes(或者创建它的 StorageClass)的回收策略。

练习 14.02:测试 PersistentVolumes 中 StatefulSet 数据的弹性

在这个练习中,我们将从上一个练习中离开的地方继续,并通过删除一个资源来测试我们应用程序中的数据的弹性,看看 Kubernetes 如何响应:

  1. 现在到了有趣的部分,让我们尝试通过删除 MySQL pod 来测试我们持久性机制的弹性:
kubectl delete pod mysql-0

你应该看到这个回应:

pod "mysql-0" deleted
  1. 此时应用可能会崩溃,但如果在删除 pod 之前几秒钟后再次尝试前面的curl命令,它应该会自动从我们删除 pod 之前的数字继续计数。我们可以通过尝试再次访问应用程序来验证这一点:
curl localhost:8001/api/v1/namespaces/default/services/kubernetes-test-ha-application-with-mysql:/proxy/get-number

您应该看到类似以下的回应:

{number: 2}

正如您所看到的,我们不仅从应用程序获得了有效的响应,而且还获得了序列中的下一个数字(2),这意味着当我们丢失 MySQL pod 并且 Kubernetes 恢复它时,没有丢失数据。

创建了这个 StatefulSet 之后,清理它并不像运行kubectl delete -f with_mysql.yaml那样简单。这是因为 Kubernetes 不会自动销毁由 StatefulSet 创建的 PersistentVolume。

注意

这也意味着,即使我们尝试使用terraform destroy删除所有 AWS 资源,我们仍将无限期地支付 AWS 中的孤立 EBS 卷(在这个示例中,我们不希望这样)。

  1. 因此,为了清理,我们需要找出哪些 PersistentVolumes 绑定到这个 StatefulSet。让我们列出集群默认命名空间中的 PersistentVolumes:
kubectl get pv

您应该看到类似于以下的响应:

图 14.3:获取持久卷列表

图 14.3:获取持久卷列表

  1. 看起来我们有一个名为data-mysql-0的 PersistentVolume,这是我们想要删除的。首先,我们需要删除创建它的对象。因此,让我们首先删除我们的应用程序及其所有组件:
kubectl delete -f with_mysql.yaml

您应该看到这个响应:

图 14.4:删除与 MySQL 关联的持久卷

图 14.4:删除与 MySQL 关联的持久卷

  1. 让我们检查一下我们试图删除的持久卷:
kubectl get pv

您应该看到类似于这样的响应:

图 14.5:获取持久卷列表

图 14.5:获取持久卷列表

从这个图像中,看起来我们的卷还在那里。

  1. 我们需要删除创建它的 PersistentVolume 和 PersistentVolumeClaim。为此,让我们首先运行以下命令:
kubectl delete pvc data-mysql-0

您应该看到这个响应:

persistentvolumeclaim "data-mysql-0" deleted

一旦我们删除 PersistentVolumeClaim,PersistentVolume 就变为unbound,并且受到其回收策略的约束,我们可以在上一步的截图中看到。在这种情况下,策略是删除底层存储卷。

  1. 为了验证 PV 是否已删除,让我们运行以下命令:
kubectl get pv

您应该看到以下响应:

No resources found in default namespace.

正如在这个截图中所显示的,我们的 PersistentVolume 现在已被删除。

注意

如果您的情况的回收策略不是Delete,您还需要手动删除 PersistentVolume。

  1. 现在我们已经清理了我们的 PersistentVolumes 和 PersistentVolumeClaims,我们可以继续按照通常的方式进行清理,通过运行以下命令:
terraform destroy

您应该看到一个以此截图结束的响应:

图 14.6:清理 Terraform 创建的资源

图 14.6:清理 Terraform 创建的资源

在这个练习中,我们已经看到了 Kubernetes 在删除 StatefulSet 时尝试保留 PersistentVolumes。我们还看到了当我们实际想要删除 PersistentVolume 时应该如何进行。

现在我们已经看到了如何设置 StatefulSet 并运行附加到其上的 MySQL 数据库,我们将在接下来的活动中进一步扩展高可用性的原则。不过,在我们这样做之前,我们需要解决 Kubernetes 清单蔓延的问题,因为似乎需要更多的 YAML 清单来实现构建高可用性有状态应用的目标。在接下来的部分中,我们将了解一个工具,它将帮助我们更好地组织和管理应用的清单。

Helm

在本节中,我们将看一下一个在 Kubernetes 生态系统中非常有帮助的工具,称为 Helm。Helm 是由微软创建的,因为很快就显而易见,对于任何规模的 Kubernetes 部署(例如,涉及 20 个或更多独立组件、可观察性工具、服务和其他对象的部署),需要跟踪大量的 YAML 清单。再加上许多公司运行除了生产环境之外的多个环境,您需要能够使它们彼此保持同步,这样您就开始面临一个难以控制的问题。

Helm 允许您编写 Kubernetes 清单模板,您可以向其提供参数以覆盖任何默认值,然后 Helm 会为您创建适当的 Kubernetes 清单。因此,您可以将 Helm 用作一种软件包管理器,您可以使用 Helm 图表部署整个应用程序,并在安装之前调整一些小参数。使用 Helm 的另一种方式是作为模板引擎。它允许经验丰富的 Kubernetes 操作员仅编写一次良好的模板,然后可以被不熟悉 Kubernetes 清单语法的人成功地创建 Kubernetes 资源。Helm 图表可以通过参数设置任意数量的字段,并且可以根据不同的需求调整基本模板以部署软件或微服务的大不相同的实现。

Helm 软件包称为“图表”,它们具有特定的文件夹结构。您可以使用来自 Git 的共享 Helm 图表存储库,Artifactory 服务器或本地文件系统。在即将进行的练习中,我们将查看一个 Helm 图表并在我们的集群上安装它。

这是一个很好的机会来介绍 Helm,因为如果你一直在学习 Kubernetes,你已经写了相当多的 YAML 并将其应用到了你的集群中。此外,我们所写的很多内容都是我们以前见过的东西的重复。因此,利用 Helm 的模板功能将有助于打包类似的组件并使用 Kubernetes 进行交付。你不一定要利用 Helm 的模板组件来使用它,但这样做会有所帮助,因为你可以重复使用图表来生成不同排列的 Kubernetes 对象。

注意

我们将使用 Helm 3,它与其前身 Helm 2 有很大的不同,并且最近才发布。如果你熟悉 Helm 2 并想了解其中的区别,你可以参考这个链接上的文档:v3.helm.sh/docs/faq/#changes-since-helm-2

Helm 的详细覆盖范围超出了本书的范围,但这里介绍的基本知识是一个很好的起点,也让我们明白了不同的工具和技术如何一起工作,以消除 Kubernetes 中复杂应用编排的几个障碍。

让我们看看如何创建一个图表(这是 Helm 术语中的一个包)并将其应用到一个集群中。然后,我们将了解 Helm 如何从 Helm 图表生成 Kubernetes 清单文件。

让我们通过运行以下命令来创建一个新的 Helm 图表:

helm create chart-dev

你应该会看到以下的回应:

Creating chart-dev

当你创建一个新的图表时,Helm 会默认生成一个 NGINX 的图表作为占位符应用。这将为我们创建一个新的文件夹和骨架图表供我们检查。

注意

在接下来的部分中,请确保你已经按照前言中的说明安装了tree

让我们使用 Linux 的tree命令来看看 Helm 为我们做了什么:

tree .

你应该会看到类似以下的回应:

图 14.7:Helm 图表的目录结构

图 14.7:Helm 图表的目录结构

注意templates文件夹和values.yaml文件。Helm 通过使用values.yaml文件中的值,并将这些值填充到templates文件夹中的文件中相应的占位符中。让我们来看一下values.yaml文件的一部分:

values.yaml

1  # Default values for chart-dev.
2  # This is a YAML-formatted file.
3  # Declare variables to be passed into your templates.
4  
5  replicaCount: 1
6  
7  image:
8    repository: nginx
9    pullPolicy: IfNotPresent
10   # Overrides the image tag whose default is the chart appVersion.
11   tag: ""
12 
13 imagePullSecrets: []
14 nameOverride: ""
15 fullnameOverride: ""

这一步的完整代码可以在packt.live/33ej2cO找到。

正如我们在这里所看到的,这不是一个 Kubernetes 清单,但它看起来有许多相同的字段。在前面的片段中,我们已经突出显示了整个image块。这有三个字段(repositorypullPolicytag),每个字段都有其相应的值。

另一个值得注意的文件是Chart.yaml。此文件中的以下行与我们的讨论相关:

appVersion: 1.16.0

注意

您可以在此链接找到完整的文件:packt.live/2FboR2a

文件中的注释对这意味着的描述相当详细:“这是部署的应用程序的版本号。每次对应用程序进行更改时,应递增此版本号。版本不应遵循语义化版本。它们应反映应用程序正在使用的版本。”

那么,Helm 是如何将这些组装成我们期望的传统 Kubernetes 清单格式的呢?要了解这一点,让我们检查templates文件夹中deployment.yaml文件的相应部分:

部署.yaml

30  containers:
31    - name: {{ .Chart.Name }}
32      securityContext:
33        {{- toYaml .Values.securityContext | nindent 12 }}
34      image: "{{ .Values.image.repository }}:{{ .Values.image.tag |           default .Chart.AppVersion }}"
35      imagePullPolicy: {{ .Values.image.pullPolicy }}

此步骤的完整代码可以在此链接找到:packt.live/3k0OGRL

这个文件看起来更像是一个 Kubernetes 清单,其中添加了许多变量。将deployment.yaml中的模板占位符与values.yamlChart.yaml中的观察结果进行比较,我们可以推断出以下内容:

  • {{ .Values.image.repository }}将被解释为nginx

  • {{ .Values.image.tag | default .Chart.AppVersion }}将被解释为1.16.0

因此,我们得到了我们部署规范的结果字段image: nginx:1.16.0

这是我们第一次看到 Helm 模板语言。对于那些熟悉模板引擎(如 Jinja,Go 模板或 Twig)的人来说,这种语法应该看起来很熟悉。如前所述,我们不会深入了解 Helm 的太多细节,但您可以在此链接找到有关 Helm 文档的更多信息:helm.sh/docs/chart_template_guide/

现在,让我们安装我们生成的示例图表chart-dev。这个图表将在我们的 Kubernetes 集群中部署一个示例 NGINX 应用程序。要安装 Helm 图表,命令如下所示:

helm install [NAME] [CHART] [flags]

我们可以使用--generate-name来获取一个随机名称。此外,由于我们已经在chart-dev目录中,我们可以直接使用当前工作目录根目录中的values.yaml

helm install --generate-name -f values.yaml .

您应该看到以下响应:

图 14.8:安装 Helm 图表

图 14.8:安装 Helm 图表

请注意,在输出中,您将收到关于接下来要做什么的说明。这些是来自templates/NOTES.txt文件的可定制说明。当您制作自己的 Helm 图表时,您可以使用这些来指导使用图表的人。现在,让我们运行这些命令。

注意

此输出中的确切值根据您的特定环境进行了定制,因此您应该从终端输出中复制命令。这适用于以下命令。

第一个命令将 pod 名称设置为名为POD_NAME的环境变量:

export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=chart-dev,app.kubernetes.io/instance=chart-1589678730" -o jsonpath="{.items[0].metadata.name}")

我们将跳过echo命令;它只是告诉您如何访问您的应用程序。存在这个echo命令的原因是为了显示终端输出中接下来的命令是什么。

现在在访问我们的应用程序之前,我们需要进行一些端口转发。下一个命令将在您的主机上将端口8080映射到 pod 上的端口80

kubectl --namespace default port-forward $POD_NAME 8080:80

您应该看到这个响应:

Forwarding from 127.0.0.1:8080 ->80
Forwarding from [::1]:8080 -> 80

现在让我们尝试访问 NGINX。在浏览器中,转到localhost:8080。您应该能够看到默认的 NGINX 欢迎页面:

图 14.9:访问我们的默认 NGINX 测试应用程序

图 14.9:访问我们的默认 NGINX 测试应用程序

您可以通过删除我们的资源来清理这个。首先,让我们通过获取 Helm 在您的集群中安装的所有发布的列表来获得此发布的生成名称:

helm ls

您应该看到类似于这样的响应:

图 14.10:获取 Helm 安装的所有应用程序列表

图 14.10:获取 Helm 安装的所有应用程序列表

现在,我们可以按以下方式删除发布:

helm uninstall chart-1589678730

使用前面输出中的名称。您应该看到这个响应:

release "chart-1589678730" uninstalled

就像那样,我们已经编写了我们的第一个图表。所以,让我们继续进行下一个练习,我们将学习 Helm 如何确切地使我们的工作变得更容易。

练习 14.03:为我们的基于 Redis 的计数器应用创建图表

在上一节中,我们创建了一个通用的 Helm 图表,但是如果我们想为我们的软件制作自己的图表呢?在这个练习中,我们将创建一个 Helm 图表,该图表将使用 Helm 从第十二章“您的应用程序和 HA”中部署我们的 HA 基于 Redis 的解决方案。

  1. 如果您在chart-dev目录中,导航到父目录:
cd ..
  1. 让我们首先制作一个全新的 Helm 图表:
helm create redis-based-counter && cd redis-based-counter

您应该看到这个响应:

Creating redis-based-counter
  1. 现在让我们从图表中删除不必要的文件:
rm templates/NOTES.txt; \
rm templates/*.yaml; \
rm -r templates/tests/; \
cd templates
  1. 现在,我们需要进入图表的templates文件夹,并从我们的存储库中复制 Redis 计数应用程序的文件:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter14/Exercise14.03/templates/redis-deployment.yaml; \
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter14/Exercise14.03/templates/deployment.yaml;\
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter14/Exercise14.03/templates/redis-service.yaml; \
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter14/Exercise14.03/templates/service.yaml

您可能还记得之前的章节中,我们有多个 Kubernetes 清单共享一个文件,由--- YAML 文件分隔符字符串分隔。现在我们有了一个管理 Kubernetes 清单的工具,最好将它们保存在单独的文件中,以便我们可以独立管理它们。捆绑的工作现在将由 Helm 来处理。

  1. templates文件夹中应该有四个文件。让我们确认一下:
tree .

您应该会看到以下响应:

图 14.11:我们应用程序的预期文件结构

图 14.11:我们应用程序的预期文件结构

  1. 现在我们需要修改values.yaml文件。从该文件中删除所有内容,然后只复制以下内容:
deployment:
  replicas: 3
redis:
  version: 3
  1. 现在,为了将它们连接在一起,我们需要编辑deployment.yamlredis-deployment.yaml。我们首先要编辑的是deployment.yaml。我们应该用模板替换replicas: 3,如下清单中的突出显示行所示:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubernetes-test-ha-application-with-redis-deployment
  labels:
    app: kubernetes-test-ha-application-with-redis
spec:
  replicas: {{ .Values.deployment.replicas }}
  selector:
    matchLabels:
      app: kubernetes-test-ha-application-with-redis
  template:
    metadata:
      labels:
        app: kubernetes-test-ha-application-with-redis
    spec:
      containers:
        - name: kubernetes-test-ha-application-with-redis
          image: packtworkshops/the-kubernetes-workshop:demo-app-            with-redis
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          env:
            - name: REDIS_SVC_ADDR
              value: "redis.default:6379"
  1. 接下来,编辑redis-deployment.yaml文件,并添加一个类似的模板语言块,如下清单中的突出显示行所示:
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: master
          image: redis:{{ .Values.redis.version }}
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
          ports:
            - containerPort: 6379
  1. 现在让我们使用 Helm 安装我们的应用程序:
helm install --generate-name -f values.yaml .

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

图 14.12:使用自动生成的名称安装我们的 Helm 图表

图 14.12:使用自动生成的名称安装我们的 Helm 图表

  1. 要检查我们的应用程序是否在线,我们可以获取部署列表:
kubectl get deployment

您应该会看到以下输出:

图 14.13:获取部署列表

图 14.13:获取部署列表

如您所见,Helm 已部署了我们的应用程序部署,以及为其部署的 Redis 后端。有了这些技能,您很快就会成为 Helm 的船长。

在接下来的活动中,我们将结合本章学到的两件事情——重构我们的应用程序以用于有状态的组件,然后将其部署为 Helm 图表。

活动 14.01:将我们的 StatefulSet 部署为图表

现在您已经有了 MySQL、StatefulSets 和 Helm 资源管理的经验,您的任务是将练习 14.0114.0214.03中学到的知识结合起来。

对于这个活动,我们将重构我们基于 Redis 的应用程序,使用 StatefulSets 来使用 MySQL 作为后端数据存储,并使用 Helm 进行部署。

遵循这些高级指南完成活动:

  1. 按照Exercise 14.01step 1中所示设置所需的集群基础设施,部署一个带有 MySQL 后端的计数器应用。

  2. 引入一个名为counter-mysql的新 Helm 图表。

  3. 创建一个使用 MySQL 作为后端的计数器应用的模板。

  4. 为我们的 MySQL StatefulSet 创建一个模板。

  5. 在适当的地方使用 Kubernetes Service 对象将所有内容连接起来。

  6. 配置模板,使values.yaml文件能够更改 MySQL 的版本。

  7. 测试应用程序。您应该看到与我们在以前的练习中看到的计数器应用程序类似的输出:图 14.14:活动 14.01 的预期输出

图 14.14:活动 14.01 的预期输出

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

此外,不要忘记使用terraform destroy命令清理云资源,以防止 AWS 在活动结束后向您收费。

总结

在本章的过程中,我们已经应用了我们的技能,以便能够在我们的示例应用程序中利用 StatefulSets。我们已经看到了如何以编程方式考虑运行软件的有状态部分,以及如何重构应用程序以利用状态持久性的变化。最后,我们学会了如何创建和运行 Kubernetes StatefulSets,这将使我们能够在集群中运行有状态的组件,并对工作负载的运行方式做出保证。

具备管理 Kubernetes 集群上有状态组件所需的技能是能够有效地在许多现实世界的应用中操作的重要一步。

在下一章中,我们将更多地讨论使用 Metrics Server、HorizontalPodAutoscalers 和 ClusterAutoscaler 进行数据驱动的应用编排。我们将学习这些对象如何帮助我们应对运行在 Kubernetes 集群上的应用的需求变化。

第十五章: Kubernetes 中的监控和自动扩展

概述

本章将介绍 Kubernetes 如何使您能够监视集群和工作负载,然后使用收集的数据自动驱动某些决策。您将了解 Kubernetes Metric Server,它汇总了所有集群运行时信息,使您能够使用这些信息来驱动应用程序运行时的扩展决策。我们将指导您如何使用 Kubernetes Metrics 服务器和 Prometheus 设置监控,然后使用 Grafana 来可视化这些指标。到本章结束时,您还将学会如何自动扩展您的应用程序以充分利用所提供的基础设施的资源,以及根据需要自动扩展您的集群基础设施。

介绍

让我们花一点时间回顾一下我们在这一系列章节中的进展,从第十一章“构建您自己的 HA 集群”开始。我们首先使用 kops 设置了一个 Kubernetes 集群,以高可用的方式配置 AWS 基础设施。然后,我们使用 Terraform 和一些脚本来提高集群的稳定性,并部署我们的简单计数器应用程序。之后,我们开始加固安全性,并使用 Kubernetes/云原生原则增加我们应用程序的可用性。最后,我们学会了运行一个负责使用事务来确保我们始终从我们的应用程序中获得一系列递增数字的有状态数据库。

在本章中,我们将探讨如何利用 Kubernetes 已有的关于我们应用程序的数据,驱动和自动化关于调整其规模的决策过程,以便始终使其适合我们的负载。由于观察应用程序指标、调度和启动容器以及从头开始引导节点需要时间,因此这种扩展并非瞬间发生,但最终(通常在几分钟内)会平衡集群上执行负载工作所需的 Pod 和节点数量。为了实现这一点,我们需要一种获取这些数据、理解/解释这些数据并利用这些数据向 Kubernetes 反馈指令的方法。幸运的是,Kubernetes 中已经有一些工具可以帮助我们做到这一点。这些工具包括 Kubernetes Metric Server、HorizontalPodAutoscalers(HPAs)和 ClusterAutoscaler。

Kubernetes 监控

Kubernetes 内置支持提供有关基础设施组件以及各种 Kubernetes 对象的有用监控信息。 Kubernetes Metrics 服务器是一个组件(不是内置的),它会在 API 服务器的 API 端点上收集和公开指标数据。 Kubernetes 使用这些数据来管理 Pod 的扩展,但这些数据也可以被第三方工具(如 Prometheus)抓取,供集群操作员使用。 Prometheus 具有一些非常基本的数据可视化功能,并且主要用作度量收集和存储工具,因此您可以使用更强大和有用的数据可视化工具,如 Grafana。 Grafana 允许集群管理员创建有用的仪表板来监视其集群。 您可以在此链接了解有关 Kubernetes 监控架构的更多信息:github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/monitoring_architecture.md

以下是我们在图表中的展示:

图 15.1:监控管道概述我们将在本章实施的

图 15.1:我们将在本章实施的监控管道概述

该图表代表了通过各种 Kubernetes 对象实施监控管道的方式。 总之,监控管道将按以下方式工作:

  1. Kubernetes 的各个组件已经被调整,以提供各种指标。 Kubernetes Metrics 服务器将从这些组件中获取这些指标。

  2. Kubernetes Metrics 服务器将在 API 端点上公开这些指标。

  3. Prometheus 将访问此 API 端点,抓取这些指标,并将其添加到其特殊数据库中。

  4. Grafana 将查询 Prometheus 数据库,收集这些指标,并在整洁的仪表板上以图表和其他可视化形式呈现。

现在,让我们逐个查看之前提到的每个组件,以更好地理解它们。

Kubernetes Metrics API/Metrics 服务器

Kubernetes Metrics server(以前称为 Heapster)收集并公开所有 Kubernetes 组件和对象的运行状态的度量数据。节点、控制平面组件、运行中的 pod 以及任何 Kubernetes 对象都可以通过 Metrics server 进行观察。它收集的一些度量的例子包括 Deployment/ReplicaSet 中所需 pod 的数量,该部署中处于Ready状态的 pod 的数量,以及每个容器的 CPU 和内存利用率。

在收集与我们编排应用程序相关的信息时,我们将主要使用默认公开的度量。

Prometheus

Prometheus 是一个度量收集器、时间序列数据库和警报管理器,几乎可以用于任何事情。它利用抓取功能从运行中的进程中提取度量,这些度量以 Prometheus 格式在定义的间隔内暴露。然后这些度量将存储在它们自己的时间序列数据库中,您可以对这些数据运行查询,以获取运行应用程序状态的快照。

它还带有警报管理器功能,允许您设置触发器以警报您的值班管理员。例如,您可以配置警报管理器,如果您的一个节点的 CPU 利用率在 15 分钟内超过 90%,则自动触发警报。警报管理器可以与多个第三方服务进行接口,通过各种方式发送警报,如电子邮件、聊天消息或短信电话警报。

注意:

如果您想了解更多关于 Prometheus 的信息,可以参考这本书:www.packtpub.com/virtualization-and-cloud/hands-infrastructure-monitoring-prometheus

Grafana

Grafana 是一个开源工具,可用于可视化数据并创建有用的仪表板。Grafana 将查询 Prometheus 数据库以获取度量,并在仪表板图表上绘制它们,这样人类更容易理解并发现趋势或差异。在运行生产集群时,这些工具是必不可少的,因为它们帮助我们快速发现基础设施中的问题并解决问题。

监控您的应用程序

虽然应用程序监控超出了本书的范围,但我们将提供一些粗略的指南,以便您可以在这个主题上进行更多的探索。我们建议您以 Prometheus 格式公开应用程序的指标,并使用 Prometheus 对其进行抓取;大多数语言都有许多库可以帮助实现这一点。

另一种方法是使用适用于各种应用程序的 Prometheus 导出器。导出器从应用程序中收集指标并将它们暴露给 API 端点,以便 Prometheus 可以对其进行抓取。您可以在此链接找到一些常见应用程序的开源导出器:prometheus.io/docs/instrumenting/exporters/

对于您的自定义应用程序和框架,您可以使用 Prometheus 提供的库创建自己的导出器。您可以在此链接找到相关的指南:prometheus.io/docs/instrumenting/writing_exporters/

一旦您从应用程序中公开并抓取了指标,您可以在 Grafana 仪表板中呈现它们,类似于我们将为监控 Kubernetes 组件创建的仪表板。

练习 15.01:设置 Metrics Server 并观察 Kubernetes 对象

在这个练习中,我们将为我们的集群设置 Kubernetes 对象的监控,并运行一些查询和创建可视化来查看发生了什么。我们将安装 Prometheus、Grafana 和 Kubernetes Metrics server:

  1. 首先,我们将从练习 12.02中的 Terraform 文件重新创建您的 EKS 集群,使用 Terraform 创建 EKS 集群。如果您已经有了main.tf文件,您可以使用它。否则,您可以运行以下命令来获取它:
curl -O https://github.com/PacktWorkshops/Kubernetes-Workshop/blob/master/Chapter12/Exercise12.02/main.tf

现在,依次使用以下两个命令来启动和运行您的集群资源:

terraform init
terraform apply

注意

您将需要jq来运行以下命令。jq是一个简单的操作 JSON 数据的工具。如果您还没有安装它,您可以使用以下命令来安装:sudo apt install jq

  1. 为了在我们的集群中设置 Kubernetes Metrics server,我们需要按顺序运行以下命令:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter15/Exercise15.01/metrics_server.yaml
kubectl apply -f metrics_server.yaml

您应该会看到类似以下的响应:

图 15.2:部署 Metrics server 所需的所有对象

图 15.2:部署 Metrics server 所需的所有对象

  1. 为了测试这一点,让我们运行以下命令:
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"

注意

如果您收到ServiceUnavailable错误,请检查防火墙规则是否允许 API 服务器与运行 Metrics 服务器的节点进行通信。

我们经常使用kubectl get命令来命名对象。我们在第四章中看到,Kubectl 解释请求,将请求指向适当的端点,并以可读格式格式化结果。但是在这里,由于我们在 API 服务器上创建了自定义端点,我们必须使用--raw标志指向它。您应该看到类似以下的响应:

图 15.3:来自 Kubernetes Metrics 服务器的响应

图 15.3:来自 Kubernetes Metrics 服务器的响应

正如我们在这里看到的,响应包含定义度量命名空间、度量值和度量元数据的 JSON 块,例如节点名称和可用区。但是,这些指标并不是非常可读的。我们将利用 Prometheus 对它们进行聚合,然后使用 Grafana 将聚合指标呈现在简洁的仪表板中。

  1. 现在,我们正在聚合度量数据。让我们开始使用 Prometheus 和 Grafana 进行抓取和可视化。为此,我们将使用 Helm 安装 Prometheus 和 Grafana。运行以下命令:
helm install --generate-name stable/prometheus

注意

如果您是第一次安装和运行 helm,您需要运行以下命令来获取稳定的存储库:

help repo add stable https://kubernetes-charts.storage.googleapis.com/

您应该看到类似以下的输出:

图 15.4:安装 Prometheus 的 Helm 图表

图 15.4:安装 Prometheus 的 Helm 图表

  1. 现在,让我们以类似的方式安装 Grafana:
helm install --generate-name stable/grafana

您应该看到以下响应:

图 15.5:安装 Grafana 的 Helm 图表

图 15.5:安装 Grafana 的 Helm 图表

在此截图中,请注意NOTES:部分,其中列出了两个步骤。按照这些步骤获取 Grafana 管理员密码和访问 Grafana 的端点。

  1. 在这里,我们正在运行 Grafana 在上一步输出中显示的第一个命令:
kubectl get secret --namespace default grafana-1576397218 -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

请使用您获得的命令版本;命令将被定制为您的实例。此命令获取您的密码,该密码存储在一个秘密中,解码它,并在您的终端输出中回显它,以便您可以将其复制以供后续步骤使用。您应该看到类似以下的响应:

brM8aEVPCJtRtu0XgHVLWcBwJ76wBixUqkCmwUK)
  1. 现在,让我们运行 Grafana 要求我们运行的下两个命令,如图 15.5中所示:
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana-1576397218" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace default port-forward $POD_NAME 3000

再次使用您的实例获取的命令,因为这将是定制的。这些命令会找到 Grafana 正在运行的 Pod,然后将本地机器的端口映射到它,以便我们可以轻松访问它。您应该会看到以下响应:

Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

注意

在这一步,如果您在获取正确的 Pod 名称时遇到任何问题,您可以简单地运行kubectl get pods来找到运行 Grafana 的 Pod 的名称,并使用该名称代替 shell($POD_NAME)变量。因此,您的命令将类似于这样:

kubectl --namespace default port-forward grafana-1591658222-7cd4d8b7df-b2hlm 3000

  1. 现在,打开浏览器并访问http://localhost:3000以访问 Grafana。您应该会看到以下着陆页面:图 15.6:Grafana 仪表板的登录页面

图 15.6:Grafana 仪表板的登录页面

默认用户名是admin,密码是步骤 6输出中的值。使用该值登录。

  1. 成功登录后,您应该会看到此页面:图 15.7:Grafana 主页仪表板

图 15.7:Grafana 主页仪表板

  1. 现在,让我们为 Kubernetes 指标创建一个仪表板。为此,我们需要将 Prometheus 设置为 Grafana 的数据源。在左侧边栏中,点击配置,然后点击数据源图 15.8:从配置菜单中选择数据源

图 15.8:从配置菜单中选择数据源

  1. 您将看到此页面:图 15.9:添加数据源选项

图 15.9:添加数据源选项

现在,点击添加数据源按钮。

  1. 您应该会看到带有几个数据库选项的页面。Prometheus 应该在顶部。点击它:图 15.10:选择 Prometheus 作为 Grafana 仪表板的数据源

图 15.10:选择 Prometheus 作为 Grafana 仪表板的数据源

现在,在我们继续到下一个屏幕之前,在这里,我们需要获取 Grafana 将用于从集群内部访问 Prometheus 数据库的 URL。我们将在下一步中执行此操作。

  1. 打开一个新的终端窗口并运行以下命令:
kubectl get svc --all-namespaces

您应该会看到类似以下的响应:

图 15.11:获取所有服务的列表

图 15.11:获取所有服务的列表

复制以prometheus开头并以server结尾的服务的名称。

  1. 步骤 12之后,您将看到以下屏幕截图所示的屏幕:图 15.12:在 Grafana 中输入我们 Prometheus 服务的地址

图 15.12:在 Grafana 中输入我们 Prometheus 服务的地址

HTTP部分的URL字段中,输入以下值:

http://<YOUR_PROMETHEUS_SERVICE_NAME>.default

请注意,您应该看到数据源正在工作,如前面的屏幕截图所示。然后,点击底部的保存并测试按钮。我们在 URL 中添加.default的原因是我们将此 Helm 图表部署到了default Kubernetes 命名空间。如果您将其部署到另一个命名空间,您应该用您的命名空间的名称替换default

  1. 现在,让我们设置仪表板。回到 Grafana 主页(http://localhost:3000),点击左侧边栏上的+符号,然后点击Import,如下所示:图 15.13:导航到导入仪表板选项

图 15.13:导航到导入仪表板选项

  1. 在下一页,您应该看到Grafana.com 仪表板字段,如下所示:图 15.14:输入要从中导入仪表板的源

图 15.14:输入要从中导入仪表板的源

将以下链接粘贴到“Grafana.com 仪表板”字段中:

https://grafana.com/api/dashboards/6417/revisions/1/download

这是一个官方支持的 Kubernetes 仪表板。一旦你在文件外的任何地方点击,你应该自动进入下一个屏幕。

  1. 上一步应该引导您到这个屏幕:图 15.15:将 Prometheus 设置为导入仪表板的数据源

图 15.15:将 Prometheus 设置为导入仪表板的数据源

在你看到prometheus的地方,点击旁边的下拉列表,选择Prometheus,然后点击Import

  1. 结果应该如下所示:图 15.16:用于监视我们集群的 Grafana 仪表板

图 15.16:用于监视我们集群的 Grafana 仪表板

正如您所看到的,我们在 Kubernetes 中有一个简洁的仪表板来监视工作负载。在这个练习中,我们部署了我们的 Metric Server 来收集和公开 Kubernetes 对象指标,然后我们部署了 Prometheus 来存储这些指标,并使用 Grafana 来帮助我们可视化 Prometheus 中收集的指标,这将告诉我们在任何时候我们集群中发生了什么。现在,是时候利用这些信息来扩展事物了。

Kubernetes 中的自动扩展

Kubernetes 允许您自动扩展工作负载以适应应用程序的不断变化的需求。从 Kubernetes 度量服务器收集的信息是用于驱动扩展决策的数据。在本书中,我们将涵盖两种类型的扩展操作——一种影响部署中运行的 pod 数量,另一种影响集群中运行的节点数量。这两种都是水平扩展的例子。让我们简要地直观了解一下 pod 的水平扩展和节点的水平扩展将涉及什么:

  • Pods:假设您在创建 Kubernetes 中的部署时填写了podTemplateresources:部分,那么该 pod 中的每个容器都将具有由相应的cpumemory字段指定的requestslimits字段。当处理工作负载所需的资源超出您分配的资源时,通过向部署添加 pod 的额外副本,您可以水平扩展以增加部署的容量。通过让软件进程根据负载为您决定部署中 Pod 的副本数量,您正在自动扩展部署,以使副本数量与您定义的用于表示应用程序负载的指标保持一致。应用程序负载的一个指标可能是当前正在使用的分配 CPU 的百分比。

  • 节点:每个节点都有一定数量的 CPU(通常以核心数表示)和内存(通常以升表示),可供 Pod 消耗。当所有工作节点的总容量被所有运行的 pod 耗尽时(这意味着所有 Pod 的 CPU 和内存请求/限制都等于或大于整个集群的请求/限制),那么我们已经饱和了集群的资源。为了允许在集群上运行更多的 Pod,或者允许集群中发生更多的自动扩展,我们需要以额外的工作节点的形式增加容量。当我们允许软件进程为我们做出这个决定时,我们被认为是自动扩展我们集群的总容量。在 Kubernetes 中,这由 ClusterAutoscaler 处理。

注意

当你增加应用程序的 Pod 副本数量时,这被称为水平扩展,由HorizontalPodAutoscaler处理。相反,如果你增加副本的资源限制,那就被称为垂直扩展。Kubernetes 还提供VerticalPodAutoscaler,但由于尚未普遍可用且在生产中使用时不安全,我们在此略过。

同时使用 HPA 和 ClusterAutoscalers 可以是公司确保始终有正确数量的应用程序资源部署以满足其负载,并且同时不会支付过多费用的有效方式。让我们在以下小节中分别研究它们。

HorizontalPodAutoscaler

HPA 负责确保部署中应用程序的副本数量与度量标准测得的当前需求相匹配。这很有用,因为我们可以使用实时度量数据,这些数据已经被 Kubernetes 收集,以始终确保我们的应用程序满足我们在阈值中设定的需求。这对一些不习惯使用数据运行应用程序的应用程序所有者可能是一个新概念,但一旦开始利用可以调整部署大小的工具,你就永远不想回头了。

Kubernetes 在autoscaling/v1autoscaling/v2beta2组中有一个 API 资源,用于提供可以针对另一个 Kubernetes 资源运行的自动缩放触发器的定义,这往往是一个 Kubernetes 部署对象。在autoscaling/v1的情况下,唯一支持的度量标准是当前的 CPU 消耗,在autoscaling/v2beta2的情况下,支持任何自定义度量标准。

HPA 查询 Kubernetes Metric Server 来查看特定部署的度量标准。然后,自动缩放资源将确定当前观察到的度量标准是否超出了缩放目标的阈值。如果是,它将根据负载将部署所需的 Pod 数量增加或减少。

举个例子,考虑一个由电子商务公司托管的购物车微服务。在输入优惠券代码的过程中,购物车服务经历了重负载,因为它必须遍历购物车中的所有商品,并在验证优惠券代码之前搜索其中的活动优惠券。在一个随机的周二早晨,有许多在线购物者使用该服务,并且他们都想使用优惠券。通常情况下,服务会不堪重负,请求会开始失败。然而,如果您能够使用 HPA,Kubernetes 将利用集群的空闲计算能力,以确保有足够的该购物车服务的 Pod 来处理负载。

请注意,简单地对部署进行自动缩放并不是解决应用程序性能问题的“一刀切”解决方案。现代应用程序中有许多可能出现减速的地方,因此应该仔细考虑您的应用程序架构,以查看您可以识别其他瓶颈的地方,而这些瓶颈并不是简单的自动缩放可以解决的。一个这样的例子是数据库上的慢查询性能。然而,在本章中,我们将专注于可以通过 Kubernetes 中的自动缩放来解决的应用程序问题。

让我们来看一下 HPA 的结构,以便更好地理解一些内容:

with_autoscaler.yaml

115 apiVersion: autoscaling/v1
116 kind: HorizontalPodAutoscaler
117 metadata:
118   name: shopping-cart-hpa
119 spec:
120   scaleTargetRef:
121     apiVersion: apps/v1
122     kind: Deployment
123     name: shopping-cart-deployment
124   minReplicas: 20
125   maxReplicas: 50
126   targetCPUUtilizationPercentage: 50

您可以在此链接找到完整的代码:packt.live/3bE9v28

在这个规范中,观察以下字段:

  • scaleTargetRef:这是被缩放的对象的引用。在这种情况下,它是指向购物车微服务的部署的指针。

  • minReplicas:部署中的最小副本数,不考虑缩放触发器。

  • maxReplicas:部署中的最大副本数,不考虑缩放触发器。

  • targetCPUUtilizationPercentage:部署中所有 Pod 的平均 CPU 利用率的目标百分比。Kubernetes 将不断重新评估此指标,并增加和减少 Pod 的数量,以使实际的平均 CPU 利用率与此目标相匹配。

为了模拟对我们的应用程序的压力,我们将使用wrk,因为它很容易配置,并且已经为我们制作了一个 Docker 容器。wrk 是一个 HTTP 负载测试工具。它使用简单,并且只有少量选项;但是,它将能够通过使用多个同时的 HTTP 连接反复发出请求来生成大量负载,针对指定的端点。

注意

您可以在此链接找到有关 wrk 的更多信息:github.com/wg/wrk

在接下来的练习中,我们将使用我们一直在运行的应用程序的修改版本来帮助驱动扩展行为。在我们的应用程序的这个修订版中,我们已经修改了它,使得应用程序以天真的方式执行斐波那契数列计算,直到第 10,000,000 个条目,以便它会稍微更加计算密集,并超过我们的 CPU 自动缩放触发器。如果您检查源代码,您会发现我们已经添加了这个函数:

main.go

74 func FibonacciLoop(n int) int { 
75   f := make([]int, n+1, n+2) 
76   if n < 2 { 
77         f = f[0:2] 
78   } 
79   f[0] = 0 
80   f[1] = 1 
81   for i := 2; i <= n; i++ { 
82         f[i] = f[i-1] + f[i-2] 
83   } 
84   return f[n] 
85 } 

您可以在此链接找到完整的代码:packt.live/3h5wCEd

除此之外,我们将使用 Ingress,我们在第十二章中学到的,并且在上一章中构建的相同的 SQL 数据库。

现在,说了这么多,让我们深入研究以下练习中这些自动缩放器的实现。

练习 15.02:在 Kubernetes 中扩展工作负载

在这个练习中,我们将整合之前的一些不同部分。由于我们的应用程序目前有几个移动部分,我们需要列出一些步骤,以便您了解我们的方向:

  1. 我们需要像练习 12.02中一样设置我们的 EKS 集群,使用 Terraform 创建一个集群。

  2. 我们需要为 Kubernetes Metrics 服务器设置所需的组件。

注意

考虑到这两点,您需要成功完成上一个练习才能执行这个练习。

  1. 我们需要使用修改后的计数器应用程序进行安装,以便它成为一个计算密集型的练习,以获取序列中的下一个数字。

  2. 我们需要安装 HPA 并设置 CPU 百分比的度量目标。

  3. 我们需要安装 ClusterAutoscaler 并赋予它在 AWS 中更改Autoscaling GroupASG)大小的权限。

  4. 我们需要通过生成足够的负载来对我们的应用进行压力测试,以便能够扩展应用并导致 HPA 触发集群扩展操作。

我们将使用 Kubernetes Ingress 资源来使用集群外部的流量进行负载测试,以便我们可以创建一个更真实的模拟。

完成后,您将成为 Kubernetes 的船长,所以让我们开始吧:

  1. 现在,让我们通过依次运行以下命令来部署ingress-nginx设置:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml 
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/aws/service-l4.yaml 
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/aws/patch-configmap-l4.yaml 

您应该看到以下响应:

图 15.17:部署 nginx Ingress 控制器

图 15.17:部署 nginx Ingress 控制器

  1. 现在,让我们获取具有 HA MySQL、Ingress 和 HPA 的应用程序清单:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter15/Exercise15.02/with_autoscaler.yaml

在应用之前,让我们看一下我们的自动缩放触发器:

with_autoscaler.yaml

115 apiVersion: autoscaling/v1 
116 kind: HorizontalPodAutoscaler 
117 metadata: 
118   name: counter-hpa 
119 spec: 
120   scaleTargetRef: 
121     apiVersion: apps/v1 
122     kind: Deployment 
123     name: kubernetes-test-ha-application-with-autoscaler-          deployment 
124   minReplicas: 2 
125   maxReplicas: 1000 
126   targetCPUUtilizationPercentage: 10 

完整的代码可以在此链接找到:packt.live/3bE9v28

在这里,我们从此部署的两个副本开始,并允许自己增长到 1000 个副本,同时尝试保持 CPU 利用率恒定在 10%。回想一下,根据我们的 Terraform 模板,我们使用 m4.large EC2 实例来运行这些 Pod。

  1. 通过运行以下命令来部署此应用程序:
kubectl apply -f with_autoscaler.yaml

您应该看到以下响应:

图 15.18:部署我们的应用程序

图 15.18:部署我们的应用程序

  1. 有了这个,我们准备进行负载测试。在开始之前,让我们检查一下部署中的 Pod 数量:
kubectl describe hpa counter-hpa

这可能需要长达 5 分钟才能显示百分比,之后您应该看到类似于这样的东西:

图 15.19:获取有关我们的 HPA 的详细信息

图 15.19:获取有关我们的 HPA 的详细信息

Deployment pods:字段显示2 current / 2 desired,这意味着我们的 HPA 已将期望的副本计数从 3 更改为 2,因为我们的 CPU 利用率为 0%,低于 10%的目标。

现在,我们需要进行一些负载。我们将从我们的计算机到集群运行一个负载测试,使用 wrk 作为 Docker 容器。但首先,我们需要获取 Ingress 端点以访问我们的集群。

  1. 首先运行以下命令以获取您的 Ingress 端点:
kubectl get svc ingress-nginx -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'

您应该看到以下响应:

图 15.20:检查我们的 Ingress 端点

图 15.20:检查我们的 Ingress 端点

  1. 在另一个终端会话中,使用以下命令运行wrk负载测试:
docker run --rm skandyla/wrk -t10 -c1000 -d600 -H ‚Host: counter.com'  http://YOUR_HOSTNAME/get-number

让我们快速了解这些参数:

-t10:此测试要使用的线程数,在本例中为 10。

-c1000:要保持打开的连接总数。在本例中,每个线程处理 1000 个连接。

-d600:运行此测试的秒数(在本例中为 600 秒或 10 分钟)。

您应该获得以下输出:

图 15.21:对我们的 Ingress 端点运行负载测试

图 15.21:对我们的 Ingress 端点运行负载测试

  1. 在另一个会话中,让我们留意我们应用程序的 Pod:
kubectl get pods --watch

您应该能够看到类似于这样的响应:

图 15.22:观察支持我们应用程序的 Pod

图 15.22:观察支持我们应用程序的 Pod

在这个终端窗口中,您应该看到 Pod 数量的增加。请注意,我们也可以在 Grafana 仪表板中检查相同的情况。

在这里,它增加了 1;但很快,这些 Pod 将超出所有可用空间。

  1. 在另一个终端会话中,您可以再次设置端口转发到 Grafana 以观察仪表板:
kubectl --namespace default port-forward $POD_NAME 3000

您应该看到以下响应:

Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000
  1. 现在,在浏览器上访问localhost:3000上的仪表板:图 15.23:在 Grafana 仪表板中观察我们的集群

图 15.23:在 Grafana 仪表板中观察我们的集群

您还应该能够在这里看到 Pod 数量的增加。因此,我们已成功部署了一个自动扩展 Pod 数量的 HPA,随着应用程序负载的增加而增加 Pod 数量。

ClusterAutoscaler

如果 HPA 确保部署中始终有正确数量的 Pod 在运行,那么当我们的集群对所有这些 Pod 的容量用完时会发生什么?我们需要更多的 Pod,但我们也不希望在不需要时为这些额外的集群容量付费。这就是 ClusterAutoscaler 的作用。

ClusterAutoscaler 将在您的集群内工作,以确保在 ASG(在 AWS 的情况下)中运行的节点数量始终具有足够的容量来运行集群当前部署的应用程序组件。因此,如果部署中的 10 个 Pod 可以放在 2 个节点上,那么当您需要第 11 个 Pod 时,ClusterAutoscaler 将要求 AWS 向您的 Kubernetes 集群添加第 3 个节点以安排该 Pod。当不再需要该 Pod 时,该节点也会消失。让我们看一个简短的架构图,以了解 ClusterAutoscaler 的工作原理:

图 15.24:节点满负荷的集群

图 15.24:节点满负荷的集群

请注意,在此示例中,我们运行了两个工作节点的 EKS 集群,并且所有可用的集群资源都已被占用。因此,ClusterAutoscaler 的作用如下。

当控制平面收到一个无法容纳的 Pod 的请求时,它将保持在Pending状态。当 ClusterAutoscaler 观察到这一点时,它将与 AWS EC2 API 通信,并请求 ASG 进行扩展,其中我们的工作节点部署在其中。这需要 ClusterAutoscaler 能够与云提供商的 API 通信,以便更改工作节点计数。在 AWS 的情况下,这还意味着我们要么必须为 ClusterAutoscaler 生成 IAM 凭据,要么允许它使用机器的 IAM 角色来访问 AWS API。

成功的扩展操作应该如下所示:

图 15.25:额外的节点用于运行额外的 Pod

图 15.25:额外的节点用于运行额外的 Pod

我们将在下一个练习中实现 ClusterAutoscaler,然后在随后的活动中进行负载测试。

练习 15.03:配置 ClusterAutoscaler

所以,现在我们已经看到我们的 Kubernetes 部署扩展,现在是时候看它扩展到需要向集群添加更多节点容量的地步了。我们将继续上一课的内容,并运行完全相同的应用程序和负载测试,但让它运行更长一点:

  1. 要创建 ClusterAutoscaler,首先,我们需要创建一个 AWS IAM 账户,并赋予它管理我们 ASG 的权限。创建一个名为permissions.json的文件,内容如下:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeAutoScalingInstances",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:SetDesiredCapacity",
        "autoscaling:TerminateInstanceInAutoScalingGroup",
        "autoscaling:DescribeLaunchConfigurations",
        "ec2:DescribeLaunchTemplateVersions",
        "autoscaling:DescribeTags"
      ],
      "Resource": "*"
    }
  ]
}
  1. 现在,让我们运行以下命令来创建一个 AWS IAM 策略:
aws iam create-policy --policy-name k8s-autoscaling-policy --policy-document file://permissions.json

您应该看到以下响应:

图 15.26:创建一个 AWS IAM 策略

图 15.26:创建一个 AWS IAM 策略

记下您获得的输出中Arn:字段的值。

  1. 现在,我们需要创建一个 IAM 用户,然后将策略附加到它。首先,让我们创建用户:
aws iam create-user --user-name k8s-autoscaler

您应该看到以下响应:

图 15.27:创建一个 IAM 用户来使用我们的策略

图 15.27:创建一个 IAM 用户来使用我们的策略

  1. 现在,让我们将 IAM 策略附加到用户:
aws iam attach-user-policy --policy-arn <ARN_VALUE> --user-name k8s-autoscaler

使用您在步骤 2中获得的 ARN 值。

  1. 现在,我们需要这个 IAM 用户的秘密访问密钥。运行以下命令:
aws iam create-access-key --user-name k8s-autoscaler

您应该得到以下响应:

图 15.28:获取创建的 IAM 用户的秘密访问密钥

图 15.28:获取创建的 IAM 用户的秘密访问密钥

在此命令的输出中,请注意AccessKeyIdSecretAccessKey

  1. 现在,获取我们提供的 ClusterAutoscaler 的清单文件:
curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter15/Exercise15.03/cluster_autoscaler.yaml
  1. 我们需要创建一个 Kubernetes Secret 来将这些凭据暴露给 ClusterAutoscaler。打开cluster_autoscaler.yaml文件。在第一个条目中,您应该看到以下内容:

cluster_autoscaler.yaml

1  apiVersion: v1
2  kind: Secret
3  metadata:
4    name: aws-secret
5    namespace: kube-system
6  type: Opaque
7  data:
8    aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
9    aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY

您可以在此链接找到完整的代码:packt.live/2DCDfzZ

您需要使用 AWS 在步骤 5中返回的值的 Base64 编码版本替换YOUR_AWS_ACCESS_KEY_IDYOUR_AWS_SECRET_ACCESS_KEY

  1. 要以 Base64 格式编码,运行以下命令:
echo -n <YOUR_VALUE> | base64

运行两次,使用AccessKeyIDSecretAccessKey替换<YOUR_VALUE>,以获取相应的 Base64 编码版本,然后将其输入到 secret 字段中。完成后应如下所示:

aws_access_key_id: QUtJQUlPU0ZPRE5ON0VYQU1QTEUK
aws_secret_access_key: d0phbHJYVXRuRkVNSS9LN01ERU5HL2JQeFJmaUNZRVhBTVBMRUtFWQo=
  1. 现在,在相同的cluster_autoscaler.yaml文件中,转到第 188 行。您需要将YOUR_AWS_REGION的值替换为您部署 EKS 集群的区域的值,例如us-east-1

集群自动缩放器.yaml

176   env: 
177   - name: AWS_ACCESS_KEY_ID 
178     valueFrom: 
179       secretKeyRef: 
180         name: aws-secret 
181         key: aws_access_key_id 
182   - name: AWS_SECRET_ACCESS_KEY 
183     valueFrom: 
184       secretKeyRef: 
185         name: aws-secret 
186         key: aws_secret_access_key 
187   - name: AWS_REGION 
188     value: <YOUR_AWS_REGION>

您可以在此链接找到整个代码:packt.live/2F8erkb

  1. 现在,通过运行以下命令应用此文件:
kubectl apply -f cluster_autoscaler.yaml

您应该看到类似以下的响应:

图 15.29:部署我们的 ClusterAutoscaler

图 15.29:部署我们的 ClusterAutoscaler

  1. 请注意,我们现在需要修改 AWS 中的 ASG 以允许扩展;否则,ClusterAutoscaler 将不会尝试添加任何节点。为此,我们提供了一个修改过的main.tf文件,只更改了一行:max_size = 5第 299 行)。这将允许集群最多添加五个 EC2 节点到自身。

导航到您下载了先前 Terraform 文件的相同位置,然后运行以下命令:

curl -O https://raw.githubusercontent.com/PacktWorkshops/Kubernetes-Workshop/master/Chapter15/Exercise15.03/main.tf

您应该看到以下响应:

图 15.30:下载修改后的 Terraform 文件

图 15.30:下载修改后的 Terraform 文件

  1. 现在,将修改应用到 Terraform 文件:
terraform apply

验证更改仅应用于 ASG 的最大容量,然后在提示时输入yes

图 15.31:应用我们的 Terraform 修改

图 15.31:应用我们的 Terraform 修改

注意

我们将在以下活动中测试此 ClusterAutoscaler。因此,现在不要删除您的集群和 API 资源。

在这一点上,我们已经部署了我们的 ClusterAutoscaler,并配置它以访问 AWS API。因此,我们应该能够根据需要扩展节点的数量。

让我们继续进行下一个活动,在那里我们将对我们的集群进行负载测试。您应该尽快进行此活动,以便降低成本。

活动 15.01:使用 ClusterAutoscaler 对我们的集群进行自动扩展

在这个活动中,我们将运行另一个负载测试,这一次,我们将运行更长时间,并观察基础架构的变化,因为集群扩展以满足需求。这个活动应该重复之前的步骤(如练习 15.02,在 Kubernetes 中扩展工作负载中所示)来运行负载测试,但这一次,应该安装 ClusterAutoscaler,这样当您的集群对 Pod 的容量不足时,它将扩展节点的数量以适应新的 Pod。这样做的目的是看到负载测试增加节点数量。

按照以下指南完成您的活动:

  1. 我们将使用 Grafana 仪表板来观察集群指标,特别关注运行中的 Pod 数量和节点数量。

  2. 我们的 HPA 应该设置好,这样当我们的应用程序接收到更多负载时,我们可以扩展 Pod 的数量以满足需求。

  3. 确保您的 ClusterAutoscaler 已成功设置。

注意

为了满足前面提到的三个要求,您需要成功完成本章中的所有练习。我们将使用在这些练习中创建的资源。

  1. 运行负载测试,如练习 15.02步骤 2所示。如果愿意,您可以选择更长或更强烈的测试。

在完成此活动时,您应该能够通过描述 AWS ASG 来观察节点数量的增加,如下所示:

图 15.32:观察到节点数量的增加通过描述 AWS 扩展组

图 15.32:通过描述 AWS 扩展组观察节点数量的增加

您还应该能够在 Grafana 仪表板中观察到相同的情况:

图 15.33:在 Grafana 仪表板中观察到节点数量的增加

图 15.33:在 Grafana 仪表板中观察到节点数量的增加

注意

此活动的解决方案可在以下地址找到:packt.live/304PEoD。确保通过运行命令terraform destroy删除 EKS 集群。

删除您的集群资源

这是我们将使用 EKS 集群的最后一章。因此,我们建议您使用以下命令删除您的集群资源:

terraform destroy

这应该停止使用 Terraform 创建的 EKS 集群的计费。

总结

让我们稍微回顾一下我们从第十一章《构建自己的 HA 集群》开始讨论如何以高可用的方式运行 Kubernetes 所走过的路程。我们讨论了如何设置一个安全的生产集群,并使用 Terraform 等基础设施即代码工具创建,以及保护其运行的工作负载。我们还研究了必要的修改,以便良好地扩展我们的应用程序——无论是有状态的还是无状态的版本。

然后,在本章中,我们看了如何使用数据来扩展我们的应用程序运行时,特别是在引入 Prometheus、Grafana 和 Kubernetes Metrics 服务器时。然后,我们利用这些信息来利用 HPA 和 ClusterAutoscaler,以便我们可以放心地确保我们的集群始终具有适当的大小,并且可以自动响应需求的激增,而无需支付过度配置的硬件。

在接下来的一系列章节中,我们将探索 Kubernetes 中的一些高级概念,从下一章开始介绍准入控制器。

第十六章: Kubernetes 准入控制器

概述

在本章中,我们将学习 Kubernetes 准入控制器,并使用它们来修改或验证传入的 API 请求。本章描述了 Kubernetes 准入控制器的实用性,以及它们如何扩展您的 Kubernetes 集群的功能。您将了解几个内置的准入控制器,以及变异和验证控制器之间的区别。在本章结束时,您将能够创建自己的自定义准入控制器,并将这些知识应用于构建适合您所需场景的控制器。

介绍

第四章中,如何与 Kubernetes(API 服务器)通信,我们学习了 Kubernetes 如何将其应用程序编程接口API)暴露出来,以便与 Kubernetes 平台进行交互。您还学习了如何使用 kubectl 来创建和管理各种 Kubernetes 对象。 kubectl 工具只是 Kubernetes API 服务器的客户端。 Kubernetes 主节点托管 API 服务器,通过它任何人都可以与集群通信。 API 服务器不仅为外部参与者提供了与 Kubernetes 通信的方式,还为所有内部组件(例如运行在工作节点上的 kubelet)提供了通信的方式。

API 服务器是我们集群的中央访问点。如果我们想确保我们组织的默认最佳实践和策略得到执行,那么检查和应用它们的最佳地方就是在 API 服务器上。 Kubernetes 通过准入控制器提供了这种能力。

让我们花点时间来了解为什么准入控制器很有用。例如,假设我们有一个标准的标签集策略,用于在所有对象中协助报告每个业务单元的对象组。这对于获取特定数据可能很重要,例如集成团队正在执行多少个 Pod。如果我们根据它们的标签来管理和监控对象,那么没有所需标签的对象可能会妨碍我们的管理和监控实践。因此,我们希望实施一个策略,如果对象规范中未定义这些标签,则将阻止创建对象。这个要求可以很容易地通过准入控制器来实现。

注意

Open Policy Agent 是一个很好的例子,展示了如何使用 webhooks 来构建一个可扩展的平台,以在 Kubernetes 对象上应用标准。您可以在此链接找到更多详细信息:www.openpolicyagent.org/docs/latest/kubernetes-admission-control

准入控制器是一组组件,拦截所有对 Kubernetes API 服务器的调用,并提供一种确保任何请求都符合所需标准的方法。重要的是要注意,这些控制器在 API 调用经过身份验证和授权后被调用,而在对象被操作和存储在 etcd 之前被调用。这提供了一个完美的机会来实施控制和治理,应用标准,并接受或拒绝 API 请求,以保持集群的期望状态。让我们来看看准入控制器在 Kubernetes 集群中是如何工作的。

准入控制器的工作原理

Kubernetes 提供了一组超过 25 个准入控制器。一组准入控制器默认启用,集群管理员可以向 API 服务器传递标志来控制启用/禁用其他控制器(配置生产级集群中的 API 服务器超出了本书的范围)。这些可以大致分为两种类型:

  • 变异准入控制器允许您在应用到 Kubernetes 平台之前修改请求。LimitRanger就是一个例子,如果 Pod 本身未定义,则将defaultRequests应用于 Pod。

  • 验证准入控制器验证请求,不能更改请求对象。如果此控制器拒绝请求,Kubernetes 平台将不会执行该请求。一个例子是NamespaceExists控制器,如果请求中引用的命名空间不可用,则会拒绝该请求。

基本上,准入控制器分为两个阶段执行。在第一阶段,执行变异准入控制器,在第二阶段,执行验证准入控制器。

注意

根据情况,最好避免使用变异控制器,因为它们会改变请求的状态,调用者可能不知道这些变化。相反,您可以使用验证控制器来拒绝无效的请求,并让调用者修复请求。

准入控制器的高级概述如下图所示:

图 16.1:创建对象的 API 请求的阶段

图 16.1:创建对象的 API 请求的阶段

当 Kubernetes API 服务器接收到 API 调用(可以通过 kubectl 或在其他节点上运行的 kubelet 进行调用),它会将调用通过以下阶段:

  1. 执行调用的身份验证和授权,以确保调用者已经过身份验证并应用了 RBAC 策略。

  2. 将有效负载通过所有现有的变异控制器。变异控制器是可以更改客户端发送的对象的控制器。

  3. 检查对象是否符合定义的模式,以及所有字段是否有效。

  4. 将有效负载通过所有现有的验证控制器。这些控制器验证最终对象。

  5. 将对象存储在 etcd 数据存储中。

图 16.1可以看出,一些准入控制器附有称为webhooks的东西。这可能并非所有准入控制器都是如此。我们将在本章的后面部分了解更多关于 webhooks 的内容。

请注意,一些控制器既提供变异功能,也提供验证功能。实际上,一些 Kubernetes 功能是作为准入控制器实现的。例如,当 Kubernetes 命名空间进入终止状态时,NamespaceLifecycle准入控制器会阻止在终止命名空间中创建新对象。

注意

出于简洁起见,本章只涵盖了一些准入控制器。请参考此链接,了解可用的控制器的完整列表:kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do

让我们确认我们的 Minikube 设置已配置为运行准入控制器。运行以下命令以启动 Minikube,并启用所有必需的插件:

minikube stop
minikube start --extra-config=apiserver.enable-admission-plugins="LimitRanger,NamespaceExists,NamespaceLifecycle,ResourceQuota,ServiceAccount,DefaultStorageClass,MutatingAdmissionWebhook,ValidatingAdmissionWebhook"

您应该看到类似以下截图的响应:

图 16.2:启动 Minikube 并启用所有必需的插件运行准入控制器

图 16.2:启动 Minikube 并启用所有必需的插件以运行准入控制器

现在我们已经概述了内置准入控制器,让我们看看如何使用我们自己的自定义逻辑创建一个准入控制器。

创建具有自定义逻辑的控制器

如前所述,Kubernetes 提供了一系列具有预定义功能的控制器。这些控制器已经内置到 Kubernetes 服务器二进制文件中。但是,如果您需要拥有自己的策略或标准来进行检查,并且没有一个准入控制器符合您的要求,会发生什么呢?

为了满足这样的需求,Kubernetes 提供了称为准入 Webhook的东西。准入 Webhook 有两种类型,我们将在以下部分进行学习。

变更准入 Webhook

变更准入 Webhook是一种变更准入控制器,它本身没有任何逻辑。相反,它允许您定义一个 URL,Kubernetes API 服务器将调用该 URL。这个 URL 就是我们 Webhook 的地址。从功能上讲,Webhook 是一个接受请求、处理请求,然后做出响应的 HTTPS 服务器。

如果定义了多个 URL,则它们将按链式处理,即第一个 Webhook 的输出将成为第二个 Webhook 的输入。

Kubernetes API 服务器将一个负载(AdmissionReview 对象)发送到 Webhook URL,请求正在处理中。您可以根据需要修改请求(例如,添加自定义注释)并发送回修改后的请求。Kubernetes API 服务器将在创建资源的下一个阶段使用修改后的对象。

执行流程将如下:

  1. Kubernetes API 接收到创建对象的请求。例如,假设您想创建一个如下所定义的 Pod:
apiVersion: v1
kind: Pod
metadata:
  name: configmap-env-pod
spec:
  containers:
    - name: configmap-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "sleep 5" ]
  1. Kubernetes 调用一个名为MutatingAdmissionWebHook的 Webhook,并将对象定义传递给它。在这种情况下,它是 Pod 规范。

  2. Webhook(由您编写的自定义代码)接收对象并根据自定义规则进行修改。例如,它添加自定义注释podModified="true"。修改后,对象将如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: configmap-env-pod
  annotations:
    podModified: "true"
spec:
  containers:
    - name: configmap-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "sleep 5" ]
  1. Webhook 返回修改后的对象。

  2. Kubernetes 将修改后的对象视为原始请求并继续进行。

前面提到的流程可以可视化如下。请注意,该流程经过简化,以便您理解主要阶段:

图 16.3:变更准入 Webhook 的流程

图 16.3:变异入场 Webhook 的流程

验证入场 Webhook

第二种类型的 Webhook 是验证入场 Webhook。这个钩子与变异入场 Webhook 类似,没有自己的逻辑。按照相同的模式,它允许我们定义一个 URL,最终提供决定接受或拒绝此调用的逻辑。

主要区别在于验证 Webhook 不能修改请求,只能允许或拒绝请求。如果此 Webhook 拒绝请求,Kubernetes 将向调用者发送错误;否则,它将继续执行请求。

Webhook 的工作原理

Webhook 部署为 Kubernetes 集群中的 Pod,并且 Kubernetes API 服务器使用 AdmissionReview 对象通过 SSL 调用它们。该对象定义 AdmissionRequest 和 AdmissionResponse 对象。Webhook 从 AdmissionRequest 对象中读取请求有效负载,并在 AdmissionResponse 对象中提供成功标志和可选更改。

以下是 AdmissionReview 对象的顶级定义。请注意,AdmissionRequest 和 AdmissionResponse 都是 AdmissionReview 对象的一部分。以下是 Kubernetes 源代码中 AdmissionReview 对象定义的摘录:

// AdmissionReview describes an admission review request/response.
type AdmissionReview struct {
    metav1.TypeMeta `json:",inline"`
    // Request describes the attributes for the admission request.
    // +optional
    Request *AdmissionRequest `json:"request,omitempty"       protobuf:"bytes,1,opt,name=request"`
    // Response describes the attributes for the admission response.
    // +optional
    Response *AdmissionResponse `json:"response,omitempty" protobuf:"bytes,2,opt,name=response"`
}

注意

此片段是从 Kubernetes 源代码中提取的。您可以在此链接查看 AdmissionReview 对象的更多详细信息:github.com/kubernetes/api/blob/release-1.16/admission/v1beta1/types.go

相同的 AdmissionReview 对象用于变异和验证入场 Webhook。变异 Webhook 计算满足 Webhook 中编码的自定义要求所需的更改。这些更改(定义为补丁)与 AdmissionResponse 对象中的patchType字段一起传递到patch字段中。然后 API 服务器将该补丁应用于原始对象,并将结果对象持久化在 API 服务器中。要验证 Webhook,这两个字段保持为空。

验证入场 Webhook 只需设置一个标志以接受或拒绝请求,而变异入场 Webhook 将设置一个标志,指示请求是否已根据请求成功修改。

首先,让我们更仔细地看一下如何手动修补一个对象,这将帮助您构建一个可以修补对象的 Webhook。

您可以使用kubectl patch命令手动打补丁一个对象。例如,假设您想在对象的.metadata.annotation部分添加一个字段。命令将如下所示:

kubectl patch configmap simple-configmap -n webhooks -p '{"metadata": {"annotations":  {"new":"annotation"}  } }'

请注意我们要添加的字段之前和之后的双空格(在前面的命令中显示为{"new":"annotation"})。让我们在一个练习中实现这个,并学习如何使用 JSON 负载来使用这个命令。

练习 16.01:通过补丁修改 ConfigMap 对象

在这个练习中,我们将使用 kubectl 打补丁一个 ConfigMap。我们将向 ConfigMap 对象添加一个注释。这个注释以后可以用来对对象进行分组,类似于我们在介绍部分提到的用例。因此,如果多个团队在使用一个集群,我们希望跟踪哪些团队在使用哪些资源。让我们开始练习:

  1. 创建一个名为webhooks的命名空间:
kubectl create ns webhooks

您应该看到以下响应:

namespace/webhooks created
  1. 接下来,使用以下命令创建一个 ConfigMap:
kubectl create configmap simple-configmap --from-literal=url=google.com -n webhooks

您将看到以下响应:

configmap/simple-configmap created
  1. 使用以下命令检查 ConfigMap 的内容:
kubectl get configmap simple-configmap -o yaml -n webhooks

您应该看到以下响应:

图 16.4:以 YAML 格式获取 ConfigMap 的内容

图 16.4:以 YAML 格式获取 ConfigMap 的内容

  1. 现在,让我们用一个注释来打补丁 ConfigMap。我们要添加的注释是teamname,值为kubeteam
kubectl patch configmap simple-configmap -n webhooks -p '{"metadata": {"annotations":  {"teamname":"kubeteam"}  } }'

您将得到以下响应:

configmap/simple-configmap patched

第六章标签和注释中,我们学到注释被存储为键值对。因此,一个键只能有一个值,如果一个键已经存在值(在这种情况下是teamname),那么新值将覆盖旧值。因此,请确保您的 webhook 逻辑排除已经具有所需配置的对象。

  1. 现在,让我们使用详细的补丁说明来应用另一个补丁,使用 JSON 格式提供所需的字段:
kubectl patch configmap simple-configmap -n webhooks --type='json' -p='[{"op": "add", "path": "/metadata/annotations/custompatched", "value": "true"}]'

请注意补丁的三个组成部分:op(用于操作,如add),path(用于要打补丁的字段的位置),和value(这是新值)。您应该看到以下响应:

configmap/simple-configmap patched

这是另一种应用补丁的方式。您可以看到前面的命令,它指示 Kubernetes 添加一个新的注释,键为custompatched,值为true

  1. 现在,让我们看看补丁是否已经应用。使用以下命令:
kubectl get configmap simple-configmap -n webhooks -o yaml

您应该看到以下输出:

图 16.5:检查我们的 ConfigMap 上修改的注释

图 16.5:检查我们的 ConfigMap 上修改的注释

正如您从 metadata 下的 annotations 字段中所看到的,这两个注释都已应用到我们的 ConfigMap 上。平台团队现在知道谁拥有这个 ConfigMap 对象。

构建变更 Admission WebHook 的指南

我们现在知道了工作中变更 Admission WebHook 的所有部分。请记住,Webhook 只是一个简单的 HTTPS 服务器,您可以使用自己选择的语言编写它。Webhook 被部署为 Pod 在集群中。Kubernetes API 服务器将通过 SSL 在端口 443 上调用这些 Pod 来变更或验证对象。

构建 Webhook Pod 的伪代码将如下所示:

  1. 在 Pod 中设置一个简单的 HTTPS 服务器(Webhook)来接受 POST 调用。请注意,调用必须通过 SSL 进行。

  2. Kubernetes 将通过 HTTPS POST 调用将 AdmissionReview 对象发送到 Webhook。

  3. Webhook 代码将处理 AdmissionRequest 对象,以获取请求中对象的详细信息。

  4. Webhook 代码将可选择地修补对象并将响应标志设置为指示成功或失败。

  5. Webhook 代码将使用更新后的请求填充 AdmissionReview 对象中的 AdmissionResponse 部分。

  6. Webhook 将使用 AdmissionReview 对象回应 POST 调用(在 步骤 2 中收到)。

  7. Kubernetes API 服务器将评估响应,并根据标志接受或拒绝客户端请求。

在 Webhook 的代码中,我们将使用 JSON 指定路径和所需的修改。请记住,从之前的练习中,我们的修补对象定义将包含以下内容:

  • op 指定操作,比如 addreplace

  • path 指定我们要修改的字段的位置。参考 图 16.5 中命令的输出,并注意不同的字段位于不同的位置。例如,名称位于 metadata 字段内,因此其路径将是 /metadata/name

  • value 指定字段的值。

用 Go 编写的简单变更 Webhook 应该如下所示:

mutatingcontroller.go

20 func MutateCustomAnnotation(admissionRequest      *v1beta1.AdmissionRequest ) (*v1beta1.AdmissionResponse,      error){ 
21 
22   // Parse the Pod object. 
23   raw := admissionRequest.Object.Raw 
24   pod := corev1.Pod{} 
25   if _, _, err := deserializer.Decode(raw, nil, &pod); err !=        nil{ 
26         return nil, errors.New("unable to parse pod") 
27   } 
28 
29   //create annotation to add 
30   annotations := map[string]string{"podModified" : "true"} 
31 
32   //prepare the patch to be applied to the object 
33   var patch []patchOperation 
34   patch = append(patch, patchOperation{ 
35         Op:   "add", 
36         Path: "/metadata/annotations", 
37         Value: annotations, 
38   }) 
39 
40   //convert patch into bytes 
41   patchBytes, err := json.Marshal(patch) 
42   if err != nil { 
43         return nil, errors.New("unable to parse the patch") 
44   } 
45 
46   //create the response with patch bytes 
47   var admissionResponse *v1beta1.AdmissionResponse 
48   admissionResponse = &v1beta1.AdmissionResponse { 
49         Allowed: true, 
50         Patch:   patchBytes, 
51         PatchType: func() *v1beta1.PatchType { 
52              pt := v1beta1.PatchTypeJSONPatch 
53              return &pt 
54         }(), 
55   } 
56 
57   //return the response 
58   return admissionResponse, nil 
59 
60 } 

此示例的完整代码可以在 packt.live/2GFRCot 找到。

正如您在前面的代码中所看到的,三个主要部分是 AdmissionRequest 对象,patch,以及带有修补信息的 AdmissionResponse 对象。

到目前为止,在本章中,我们已经学习了什么是准入 webhook,以及它如何与 Kubernetes API 服务器交互。我们还演示了通过使用补丁来更改请求的对象的一种方法。现在,让我们应用我们到目前为止学到的知识,在我们的 Kubernetes 集群中部署一个 webhook。

请记住,API 服务器和 webhook 之间的所有通信都是通过 SSL 进行的。SSL 是一种用于网络安全通信的协议。为了做到这一点,我们需要创建公钥和私钥,正如你将在接下来的练习中看到的。

请注意,我们还没有构建进入 webhook 的代码。首先,让我们演示如何部署用于 webhook 的 Pods(使用 Deployment)使用预构建的容器,然后我们将继续构建进入 Pod 的代码,使 webhook 运行起来。

练习 16.02:部署 Webhook

在这个练习中,我们将在 Kubernetes 中部署一个简单的预构建 webhook 服务器。请记住,webhook 只是一个 HTTPS 服务器,这正是我们要创建的。当 Kubernetes 需要通过 SSL 调用 webhook 端点时,我们需要为我们的调用创建一个证书。一旦我们为 SSL 通信创建了证书,我们将使用 Kubernetes Deployment 对象来部署我们的 webhook:

  1. 创建一个自签名证书的证书颁发机构CA)。这个 CA 将稍后用于在 Kubernetes 和我们的 webhook 服务器之间创建 HTTPS 调用的信任:
openssl req -nodes -new -x509 -keyout controller_ca.key -out controller_ca.crt -subj "/CN=Mutating Admission Controller Webhook CA"

这应该给你以下的回应:

图 16.6:生成自签名证书

图 16.6:生成自签名证书

注意

您可以在此链接了解更多关于自签名证书的信息:aboutssl.org/what-is-self-sign-certificate/

  1. 为 SSL 调用创建私钥:
openssl genrsa -out tls.key 2048

你应该会看到以下的回应:

图 16.7:为 SSL 调用创建私钥

图 16.7:为 SSL 调用创建私钥

  1. 现在用 CA 签署服务器证书:
openssl req -new -key tls.key -subj "/CN=webhook-server.webhooks.svc" \
    | openssl x509 -req -CA controller_ca.crt -CAkey controller_ca.key -CAcreateserial -out tls.crt

请注意,此命令中服务的名称是将在集群中公开我们的 webhook 的服务,以便 API 服务器可以访问它。我们将在步骤 7中重新访问这个名称。你应该会看到以下的回应:

Signature ok
subject=/CN=webhook-server.webhooks.svc
Getting CA Private Key
  1. 现在我们已经创建了一个我们的服务器可以使用的证书。接下来,我们将创建一个 Kubernetes Secret,将私钥和证书加载到我们的 webhook 服务器中:
kubectl -n webhooks create secret tls webhook-server-tls \
    --cert "tls.crt" \
    --key "tls.key"

您应该看到以下响应:

secret/webhook-server-tls created
  1. 我们的 webhook 将作为一个 Pod 运行,我们将使用部署来创建它。为此,首先创建一个名为mutating-server.yaml的文件,其中包含以下内容:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-server
  labels:
    app: webhook-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webhook-server
  template:
    metadata:
      labels:
        app: webhook-server
    spec:
      containers:
      - name: server
        image: packtworkshops/the-kubernetes-          workshop:mutating-webhook
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          name: webhook-api
        volumeMounts:
        - name: webhook-tls-certs
          mountPath: /etc/secrets/tls
          readOnly: true
      volumes:
      - name: webhook-tls-certs
        secret:
          secretName: webhook-server-tls

请注意,我们正在链接到我们提供的服务器的预制图像。

  1. 使用我们在上一步中创建的 YAML 文件创建部署:
kubectl create -f mutating-server.yaml -n webhooks

您应该看到以下响应:

deployment.apps/webhook-server created
  1. 创建服务器后,我们需要创建一个 Kubernetes 服务。请注意,服务可以通过webhook-server.webhooks.svc访问。这个字符串是我们在步骤 3中创建证书时使用的,它基于以下规范中定义的字段,格式为<SERVICENAME>.<NAMESPACENAME>.svc

创建一个名为mutating-serversvc.yaml的文件,以定义具有以下规范的服务:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: webhook-server
  name: webhook-server
  namespace: webhooks
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    app: webhook-server
  sessionAffinity: None
  type: ClusterIP
  1. 使用上一步的定义,使用以下命令创建服务:
kubectl create -f mutating-serversvc.yaml -n webhooks

您应该看到以下响应:

service/webhook-server created

在这个练习中,我们部署了一个预构建的 webhook,并配置了证书,使得我们的 webhook 准备好接受来自 Kubernetes API 服务器的调用。

配置 Webhook 以与 Kubernetes 一起工作

在这个阶段,我们已经使用部署创建并部署了 webhook。现在,我们需要向 Kubernetes 注册 webhook,以便 Kubernetes 知道它。这样做的方法是创建一个MutatingWebHookConfiguration对象。

注意

您可以在kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/找到有关 MutatingConfigurationWebhook 的更多详细信息。

以下片段显示了MutatingWebhookConfiguration的配置对象的示例:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-annotation-webhook
webhooks:
- name: webhook-server.webhooks.svc
   clientConfig:
     service:
       name: webhook-server
       namespace: webhooks
       path: "/mutate"
     caBundle: "LS0…"    #The caBundle is truncated for brevity
   rules:
     - operations: [ "CREATE" ]
       apiGroups: [""]
       apiVersions: ["v1"]
       resources: ["pods"]

以下是前述对象中的一些值得注意的定义:

  1. clientConfig.service部分定义了变异 webhook 的位置(我们在练习 16.02中部署的部署 webhook)。

  2. caBundle部分包含 SSL 信任将建立的证书。这是以 Base64 格式编码的证书。我们将在下一节中解释如何对其进行编码。

  3. rules部分定义了需要拦截的操作。在这里,我们指示 Kubernetes 拦截任何创建新 Pod 的调用。

如何以 Base64 格式编码证书

如前所述,当 Kubernetes API 服务器调用 webhook 时,调用是通过 SSL 加密的,我们需要在 webhook 定义中提供 SSL 信任证书。这可以在前一节中显示的MutatingWebhookConfiguration定义中的caBundle字段中看到。该字段中的数据是 Base64 编码的,正如您在第十章ConfigMaps 和 Secrets中学到的。以下命令可用于将证书编码为 Base64 格式。

首先,使用以下命令将生成的文件转换为 Base64 格式:

openssl base64 -in controller_ca.crt -out controller_ca-base64.crt

由于我们需要将生成的 CA 捆绑包转换为 Base64 格式并放入 YAML 文件中(如前所述),我们需要删除换行符(\n)字符。可以使用以下命令来执行此操作:

cat controller_ca-base64.crt | tr -d '\n' > onelinecert.pem

这两个命令在成功执行后不会在终端上显示任何响应。在这个阶段,您将在onelinecert.pem文件中拥有 CA 捆绑包,您可以复制它来创建您的 YAML 定义。

Activity 16.01:创建一个可变的 Webhook,向 Pod 添加注释

在这个活动中,我们将利用我们在本章和之前章节中所学到的知识来创建一个可变的 webhook,它会向 Pod 添加一个自定义注释。这样的 webhook 可能有许多用例。例如,您可能希望记录容器镜像是否来自先前批准的存储库,以供将来报告。进一步扩展,您还可以在不同的节点上从不同的存储库调度 Pods。

完成此活动的高级步骤如下:

  1. 创建一个名为webhooks的新命名空间。如果已经存在,则删除现有的命名空间,然后再次创建它。

  2. 生成自签名的 CA 证书。

  3. 为 SSL 生成私钥/公钥对并使用 CA 证书进行签名。

  4. 创建一个保存在先前步骤中生成的私钥/公钥对的密钥。

  5. 编写 webhook 代码以在 Pod 中添加自定义注释。

  6. 将 webhook 服务器代码打包为 Docker 容器。

  7. 将 Docker 容器推送到您选择的公共存储库。

注意

如果您在构建自己的 webhook 时遇到任何困难,可以使用此链接中提供的代码作为参考:packt.live/2R1vJlk

如果你想避免构建和打包 webhook,我们提供了一个预构建的容器,这样你就可以直接在你的部署中使用它。你可以从 Docker Hub 使用这个图像:packtworkshops/the-kubernetes-workshop:webhook

使用此图像可以跳过步骤 57

  1. 创建一个部署,部署 webhook 服务器。

  2. 将 webhooks 部署公开为 Kubernetes 服务。

  3. 创建 CA 证书的 Base64 编码版本。

  4. 创建一个MutatingWebHookConfiguration对象,以便 Kubernetes 可以拦截 API 调用并调用我们的 webhook。

在这个阶段,我们的 webhook 已经创建。现在,为了测试我们的 webhook 是否工作,创建一个没有注释的简单 Pod。

一旦 Pod 被创建,确保通过描述它来添加注释到 Pod。这里是预期输出的截断版本。请注意,这里的注释应该是由我们的 webhook 添加的:

图 16.8:活动 16.01 的预期输出

图 16.8:活动 16.01 的预期输出

注意

此活动的解决方案可在第 799 页找到。

验证 Webhook

我们已经了解到,变异 webhook 基本上允许修改 Kubernetes 对象。另一种 webhook 称为验证 webhook。顾名思义,这个 webhook 不允许对 Kubernetes 对象进行任何更改;相反,它作为我们集群的守门人。它允许我们编写代码,可以验证任何被请求的 Kubernetes 对象,并根据我们指定的条件允许或拒绝请求。

让我们通过一个例子来了解这如何有帮助。假设我们的 Kubernetes 集群被许多团队使用,我们想知道哪些 Pod 与哪些团队相关联。一个解决方案是要求所有团队在其 Pod 上添加一个标签(例如,键为teamName,值为团队名称)。正如你所猜测的那样,强制执行一组标签不是标准的 Kubernetes 功能。在这种情况下,我们需要创建自己的逻辑来禁止没有这些标签的 Pod。

实现这一点的一种方法是编写一个验证 webhook,查找任何 Pod 请求中的此标签,并拒绝创建请求的 Pod,如果此标签不存在。您将在本章后面的活动 16.02中做到这一点,创建一个验证 webhook,检查 Pod 中是否存在标签。现在,让我们看一下验证 webhook 的代码将是什么样子。

编写一个简单的验证 WebHook

让我们来看一段简单验证 webhook 代码的摘录:

func ValidateTeamAnnotation(admissionRequest   *v1beta1.AdmissionRequest ) (*v1beta1.AdmissionResponse, error){
      // Get the AdmissionReview Object
      raw := admissionRequest.Object.Raw
      pod := corev1.Pod{}

     // Parse the Pod object.
      if _, _, err := deserializer.Decode(raw, nil, &pod);         err != nil {
            return nil, errors.New("unable to parse pod")
      }
      //Get all the Labels of the Pod
      podLabels := pod.ObjectMeta.GetLabels()

      //Logic to check if label exists
      //check if the teamName label is available, if not         generate an error.
      if podLabels == nil || podLabels[teamNameLabel] == "" {
           return nil, errors.New("teamName label not found")
      }

      //Populate the Allowed flag
      //if the teamName label exists, return the response with 
      //Allowed flag set to true.
      var admissionResponse *v1beta1.AdmissionResponse
      admissionResponse = &v1beta1.AdmissionResponse {
           Allowed: true,
      }
      //Return the response with Allowed set to true
      return admissionResponse, nil
}
const (
      //This is the name of the label that is expected to be         part of the pods to allow them to be created.
      teamNameLabel = `teamName`
)

您可以在此片段中观察到的三个主要部分是 AdmissionRequest 对象,检查标签是否存在的逻辑,以及使用 Allowed 标志创建 AdmissionResponse 对象。

现在我们了解了验证 webhook 所需的所有不同组件,让我们在下一个活动中构建一个。

活动 16.02:创建一个验证 webhook,检查 Pod 中是否存在标签

在这个活动中,我们将利用我们在本章和之前章节中所学到的知识,编写一个验证 webhook,验证请求的 Pod 中是否存在标签。

所需的步骤如下:

  1. 创建一个名为webhooks的新命名空间。如果已经存在,请删除现有的命名空间,然后再次创建它。

  2. 生成自签名的 CA 证书。

  3. 生成 SSL 的私钥/公钥对,并使用 CA 证书进行签名。

  4. 创建一个保存在上一步生成的私钥/公钥对的秘密。

注意

即使您拥有上一个活动的证书和密钥,我们建议您丢弃它们,重新开始,以避免任何冲突。

  1. 编写 webhook 代码以检查是否存在具有键teamName的标签。如果不存在,则拒绝请求。

  2. 将 webhook 代码打包为 Docker 容器。

  3. 将 Docker 容器推送到您选择的公共存储库(quay.io 允许您创建一个免费的公共存储库)。

注意

如果您在构建自己的 webhook 时遇到任何困难,您可以使用此链接提供的代码作为参考:packt.live/2FbL7Jv

如果您想避免构建和打包 webhook,我们提供了一个预构建的容器,以便您可以直接在部署中使用它。您可以从 Docker Hub 使用此镜像:packtworkshops/the-kubernetes-workshop:webhook

使用此镜像可以跳过步骤 57

  1. 创建部署以部署 webhook 服务器。

  2. 将 webhooks Deployment 公开为 Kubernetes 服务。

  3. 创建 CA 证书的 Base64 编码版本。

  4. 创建ValidtingWebhookConfiguration,以便 Kubernetes 可以拦截 API 调用并调用我们的 webhook。

  5. 创建一个没有标签的简单 Pod,并验证它是否被拒绝。

  6. 创建一个带有所需标签的简单 Pod,并验证它是否已创建。

  7. 一旦创建了 Pod,请确保标签是 Pod 规范的一部分。

您可以通过尝试创建一个没有teamName标签的 Pod 来测试您的验证 webhook。它应该被拒绝,并显示以下消息:

图 16.9:活动 16.02 的预期输出

图 16.9:活动 16.02 的预期输出

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

控制 Webhook 对选定命名空间的影响

当您定义任何 webhook(变异或验证)时,您可以通过定义namespaceSelector参数来控制 webhook 将影响哪些命名空间。请注意,这仅适用于命名空间范围的对象。对于集群范围的对象,例如持久卷,此参数不会产生任何影响,并且将应用 webhook。

注意

并非所有准入控制器(变异或验证)都可以限制到一个命名空间。

就像许多 Kubernetes 对象一样,命名空间也可以有标签。我们将利用命名空间的这个属性,将 webhook 应用于特定的命名空间,正如您将在以下练习中看到的那样。

练习 16.03:使用命名空间选择器定义一个验证 Webhook

在这个练习中,我们将定义一个验证 webhook,强制执行一个自定义规则,应用于在webhooks命名空间中创建的 Pod。规则是 Pod 必须定义一个名为teamName的标签。由于该规则适用于在webhooks-demo命名空间中创建的 Pod,因此所有其他命名空间都可以创建没有定义标签的 Pod。

注意

在运行此练习之前,请确保您已完成活动 16.02创建一个检查 Pod 标签的验证 Webhook,因为我们正在重用那里创建的对象。如果您在活动中遇到任何问题,可以在附录中查看解决方案。

  1. 验证我们在活动 16.02中创建的验证 webhook 是否仍然存在:
kubectl get ValidatingWebHookConfiguration -n webhooks

您将看到以下响应:

NAME                        CREATED AT
pod-label-verify-webhook    201908-23T13:59:30Z
  1. 现在,删除在活动 16.02中定义的现有验证 webhook,创建一个检查 Pod 标签的验证 webhook
kubectl delete ValidatingWebHookConfiguration pod-label-verify-webhook -n webhooks

注意

ValidatingWebHookConfiguration是一个集群范围的对象,对于这个命令,指定-n标志是可选的。

您将会得到以下的响应:

图 16.10:删除现有的验证 webhook

图 16.10:删除现有的验证 webhook

  1. 删除webhooks命名空间:
kubectl delete ns webhooks

您将会得到以下的响应:

namespace "webhooks" deleted
  1. 创建webhooks命名空间:
kubectl create ns webhooks

您将会得到以下的响应:

namespace/webhooks created

现在我们应该有一个干净的板块来继续进行这个练习。

  1. 创建一个新的 CA 捆绑和一个私钥/公钥对,用于这个 webhook。使用以下命令生成一个自签名证书:
openssl req -nodes -new -x509 -keyout controller_ca.key -out controller_ca.crt -subj "/CN=Mutating Admission Controller Webhook CA"

您将会得到类似以下的输出:

图 16.11:生成一个自签名证书

图 16.11:生成一个自签名证书

注意

即使您在之前的活动中已经创建了 CA 和密钥,您仍需要重新创建它们以使本练习正常工作。

  1. 生成一个私钥/公钥对,并使用以下两个命令依次对其进行 CA 证书签名:
openssl genrsa -out tls.key 2048
openssl req -new -key tls.key -subj "/CN=webhook-server.webhooks.svc" \
    | openssl x509 -req -CA controller_ca.crt -Cakey controller_ca.key -Cacreateserial -out tls.crt

您将会得到类似以下响应的输出:

图 16.12:用我们的证书签署私钥/公钥对

图 16.12:用我们的证书签署私钥/公钥对

  1. 创建一个保存私钥/公钥对的 secret:
kubectl -n webhooks create secret tls webhook-server-tls \
--cert "tls.crt" \
--key "tls.key"

您应该会得到以下的响应:

secret/webhook-server-tls created
  1. 接下来,我们需要在webhooks命名空间部署 webhook。创建一个名为validating-server.yaml的文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-server
  labels:
    app: webhook-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webhook-server
  template:
    metadata:
      labels:
        app: webhook-server
    spec:
      containers:
      - name: server
        image: packtworkshops/the-kubernetes-workshop:webhook
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          name: webhook-api
        volumeMounts:
        - name: webhook-tls-certs
          mountPath: /etc/secrets/tls
          readOnly: true
      volumes:
      - name: webhook-tls-certs
        secret:
          secretName: webhook-server-tls

注意

您可以使用在活动 16.02中创建的相同的 webhook 镜像,创建一个检查 Pod 标签的验证 webhook。在这个参考 YAML 中,我们使用了我们在仓库中提供的镜像。

  1. 通过使用上一步的定义部署 webhook 服务器:
kubectl create -f validating-server.yaml -n webhooks

您应该会看到以下的响应:

deployment.apps/webhook-server created
  1. 您可能需要等一会儿,检查 webhook Pods 是否已经创建。不断检查 Pods 的状态:
kubectl get pods -n webhooks -w

您应该会看到以下的响应:

图 16.13:检查我们的 webhook 是否在线

图 16.13:检查我们的 webhook 是否在线

注意,-w标志会持续监视 Pods。当所有的 Pods 都准备就绪时,您可以结束监视。

  1. 现在,我们需要通过 Kubernetes 服务公开部署的 webhook 服务器。创建一个名为validating-serversvc.yaml的文件,内容如下:
apiVersion: v1
kind: Service
metadata:
  labels:
    app: webhook-server
  name: webhook-server
  namespace: webhooks
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    app: webhook-server
  sessionAffinity: None
  type: ClusterIP

请注意,webhook 服务必须在端口443上运行,因为这是 TLS 通信的标准端口。

  1. 使用上一步的定义来使用以下命令创建服务:
kubectl create -f validating-serversvc.yaml -n webhooks

您将看到以下输出:

service/webhook-server created
  1. 创建 CA 证书的 Base64 编码版本。依次使用以下命令:
openssl x509 -inform PEM -in controller_ca.crt > controller_ca.crt.pem
openssl base64 -in controller_ca.crt.pem -out controller_ca-base64.crt.pem

第一个命令是将证书转换为 PEM 格式。第二个命令是将 PEM 证书转换为 Base64。这些命令不会有任何响应。您可以使用以下命令检查文件:

cat controller_ca-base64.crt.pem

文件内容应该是这样的:

图 16.14:Base64 编码的 CA 证书内容

图 16.14:Base64 编码的 CA 证书内容

请注意,您生成的 TLS 证书不会完全与此处显示的内容相同。

  1. 使用以下两个命令清除我们的 CA 证书中的空行,并将内容添加到一个新文件中:
cat controller_ca-base64.crt.pem | tr -d '\n' > onelinecert.pem
cat onelinecert.pem

第一个命令不会有任何响应,第二个命令会打印出onlinecert.pem的内容。您应该会看到以下响应:

图 16.15:去除换行符的 Base64 编码 CA 证书

图 16.15:去除换行符的 Base64 编码 CA 证书

现在我们有了去除空行的 Base64 编码证书。在下一步中,我们将复制此输出中的值,注意不要复制结尾的$(在 Zsh 的情况下将是%)。将此值粘贴到validation-config-namespace-scoped.yaml中的CA_BASE64_PEMcaBundle的占位符)的位置,该文件将在下一步中创建。

  1. 创建一个名为validation-config-namespace-scoped.yaml的文件,使用以下ValidatingWebHookConfiguration规范来配置 Kubernetes API 服务器调用我们的 webhook:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-label-verify-webhook
webhooks:
  - name: webhook-server.webhooks.svc
    namespaceSelector:
      matchExpressions:
      - key: applyValidation
        operator: In
        values: ["true","yes", "1"]

    clientConfig:
      service:
        name: webhook-server
        namespace: webhooks
        path: "/validate"
      caBundle: "CA_BASE64_PEM"    #Retain the quotes when you         copy the caBundle here. Please read the note below on         how to add specific values here.
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        scope: "Namespaced"

注意

CA_BASE64_PEM占位符将被替换为上一步中onelinecert.pem的内容。请注意不要复制任何换行符。

  1. 根据上一步中定义的 webhook 创建 webhook。确保用之前步骤中创建的证书替换caBundle字段:
kubectl create -f validation-config-namespace-scoped.yaml

您将看到以下响应:

图 16.16:创建 ValidatingWebhookConfiguration

图 16.16:创建 ValidatingWebhookConfiguration

  1. 按照以下方式创建一个名为webhooks-demo的新命名空间:
kubectl create namespace webhooks-demo

您应该看到以下响应:

namespace/webhooks-demo created
  1. applyValidation=true标签应用到webhooks命名空间,如下所示:
kubectl label namespace webhooks applyValidation=true

您应该看到以下响应:

namespace/webhooks labeled

此标签将与步骤 14中定义的选择器匹配,并确保我们的验证标准(由 webhook 强制执行)适用于此命名空间。请注意,我们没有给webhooks-demo命名空间贴上标签,因此验证将适用于此命名空间。

  1. 现在定义一个没有teamName标签的 Pod。创建一个名为target-validating-pod.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: validating-pod-example
spec:
  containers:
    - name: validating-pod-example-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep         5 ; done" ]
  1. 根据上一步的定义,在webhooks命名空间中创建 Pod:
kubectl create -f target-validating-pod.yaml -n webhooks

Pod 的创建应该被拒绝,如下所示:

图 16.17:由于缺少必需标签而被拒绝的 Pod

图 16.17:由于缺少必需标签而被拒绝的 Pod

请记住,我们的 webhook 只检查 Pod 中的teamName标签。根据步骤 14中定义的命名空间选择器,Pod 创建将被拒绝。

  1. 现在,尝试在webhooks-demo命名空间中创建相同的 Pod,看看情况是否不同:
kubectl create -f target-validating-pod.yaml -n webhooks-demo

您应该得到这样的响应:

pod/validating-pod-example created

我们成功在webhooks-demo命名空间中创建了 Pod,但在webhooks命名空间中无法创建。

  1. 让我们描述一下 Pod 以获取更多细节:
kubectl describe pod validating-pod-example -n webhooks-demo

您应该看到类似于这样的响应:

图 16.18:检查我们的 Pod 的规范

图 16.18:检查我们的 Pod 的规范

正如您所看到的,这个 Pod 没有任何标签,但我们仍然能够创建它。这是因为我们的验证 webhook 没有监视webhooks-demo命名空间。

在这个练习中,您已经学会了如何配置 webhook 以在命名空间级别进行更改。这对于测试功能并为可能拥有不同命名空间的不同团队提供不同功能可能很有用。

摘要

在本章中,我们了解到准入控制器提供了一种在创建、更新和删除操作期间强制执行对象的变异和验证的方式。这是扩展 Kubernetes 平台以符合您组织标准的简单方法。它们可以用于将最佳实践和策略应用到 Kubernetes 集群上。

接下来,我们学习了什么是变异和验证 webhook,如何配置它们,以及如何在 Kubernetes 平台上部署它们。Webhook 提供了一种简单的方式来扩展 Kubernetes,并帮助您适应特定企业的需求。

在之前的一系列章节中,从第十一章构建您自己的 HA 集群,到第十五章Kubernetes 中的监控和自动扩展,您学会了如何在 AWS 上设置高可用性集群,并运行无状态和有状态的应用程序。在接下来的几章中,您将学习许多高级技能,这些技能将帮助您不仅仅是运行应用程序,还能够利用 Kubernetes 提供的许多强大的管理功能,并保持集群的健康。

具体来说,在下一章中,您将了解 Kubernetes 调度器。这是一个决定 Pod 将被调度到哪些节点的组件。您还将学习如何配置调度器以符合您的需求,以及如何控制 Pod 在节点上的放置。

第十七章: Kubernetes 中的高级调度

概述

本章重点介绍调度,即 Kubernetes 选择运行 Pod 的节点的过程。在本章中,我们将更仔细地研究这个过程和 Kubernetes 调度器,这是负责这个过程的默认 Kubernetes 组件。

通过本章的学习,您将能够使用不同的方式来控制 Kubernetes 调度器的行为,以满足应用程序的要求。本章将使您能够选择适当的 Pod 调度方法,根据业务需求控制您想要在哪些节点上运行 Pod。您将了解在 Kubernetes 集群上控制 Pod 调度的不同方式。

介绍

我们已经看到,我们将我们的应用程序打包为容器,并将它们部署为 Kubernetes 中的 Pod,这是部署的最小单位。借助 Kubernetes 提供的先进调度功能,我们可以优化这些 Pod 的部署,以满足我们的硬件基础设施的需求,并充分利用可用资源。

Kubernetes 集群通常有多个节点(或机器或主机),可以在其中执行 Pod。假设您正在管理一些机器,并且已被指定在这些机器上执行应用程序。为了决定哪台机器最适合给定的应用程序,您会怎么做?在本次研讨会中,每当您想在 Kubernetes 集群上运行 Pod 时,您是否提到过 Pod 应该在哪个节点上运行?

没错 - 我们不需要; Kubernetes 配备了一个智能组件,可以找到最适合运行您的 Pod 的节点。这个组件就是Kubernetes 调度器。在本章中,我们将更深入地了解 Kubernetes 调度器的工作原理,以及如何调整它以更好地控制我们的集群,以满足不同的需求。

Kubernetes 调度器

如介绍中所述,典型的集群有多个节点。当您创建一个 Pod 时,Kubernetes 必须选择一个节点并将 Pod 分配给它。这个过程被称为Pod 调度

负责决定将 Pod 分配给哪个节点以执行的 Kubernetes 组件称为调度器。Kubernetes 配备了一个默认调度器,适用于大多数用例。例如,默认的 Kubernetes 调度器在集群中均匀分配负载。

现在,考虑这样一个场景:两个不同的 Pod 预计经常需要相互通信。作为系统架构师,您可能希望它们位于同一节点上,以减少延迟并释放一些内部网络带宽。调度器不知道不同类型的 Pod 之间的关系,但 Kubernetes 提供了方法来告知调度器这种关系,并影响调度行为,以便这两个不同的 Pod 可以托管在同一节点上。但首先,让我们更仔细地看一下 Pod 的调度过程

Pod 调度过程

调度器的工作分为三个步骤:过滤评分分配。让我们来看看在执行每个步骤时会发生什么。下图描述了该过程的概述:

图 17.1:Kubernetes Scheduler 选择合适节点的概述

图 17.1:Kubernetes Scheduler 选择合适节点的概述

过滤

过滤是指Kubernetes Scheduler运行一系列检查或过滤器,以查看哪些节点不适合运行目标 Pod 的过程。过滤器的一个例子是查看节点是否有足够的 CPU 和内存来托管 Pod,或者 Pod 请求的存储卷是否可以挂载在主机上。如果集群中没有适合满足 Pod 要求的节点,那么 Pod 被视为不可调度,并且不会在集群上执行。

评分

一旦Kubernetes Scheduler有了可行节点的列表,第二步是对节点进行评分,并找到最适合托管目标 Pod 的节点。节点会经过几个优先函数,并分配一个优先级分数。每个函数都会分配一个介于 0 和 10 之间的分数,其中 0 是最低的,10 是最高的。

为了理解优先级函数,让我们以SelectorSpreadPriority为例。这个优先级函数使用标签选择器来找到相关的 Pod。比如说,一堆 Pod 是由同一个部署创建的。正如 SpreadPriority 这个名字所暗示的,这个函数试图将 Pod 分布在不同的节点上,这样在节点故障的情况下,我们仍然会在其他节点上运行副本。在这个优先级函数下,Kubernetes Scheduler 选择使用与请求的 Pod 相同的标签选择器运行最少的节点。这些节点将被分配最高的分数,反之亦然。

另一个优先级函数的示例是LeastRequestedPriority。这试图在具有最多资源可用的节点上分配工作负载。调度程序获取已分配给现有 Pod 的内存和 CPU 最少的节点。这些节点被分配最高的分数。换句话说,这个优先级函数将为更多的空闲资源分配更高的分数。

注意

在本章的有限范围内,有太多的优先级函数需要涵盖。完整的优先级函数列表可以在以下链接找到:kubernetes.io/docs/concepts/scheduling/kube-scheduler/#scoring

分配

最后,调度程序通知 API 服务器已基于最高分数选择的节点。如果有多个具有相同分数的节点,调度程序会选择一个随机节点,并有效地应用决胜局。

默认的 Kubernetes Scheduler 作为一个 Pod 在kube-system命名空间中运行。您可以通过列出kube-system命名空间中的所有 Pod 来查看它的运行情况:

kubectl get pods -n kube-system

您应该看到以下 Pod 列表:

图 17.2:在 kube-system 命名空间中列出 Pod

图 17.2:在 kube-system 命名空间中列出 Pod

在我们的 Minikube 环境中,Kubernetes Scheduler Pod 的名称为kube-scheduler-minikube,正如您在此截图中所看到的。

Pod 调度时间表

让我们深入了解Pod 调度过程的时间线。当您请求创建一个 Pod 时,不同的 Kubernetes 组件会被调用来将 Pod 分配给正确的节点。从请求 Pod 到分配节点,涉及三个步骤。以下图表概述了这个过程,我们将在图表之后详细阐述和分解这个过程:

步骤 2Kubernetes 调度器通过 API 服务器不断监视 Kubernetes 数据存储。一旦有 Pod 创建请求可用(或 Pod 处于挂起状态),调度器会尝试调度它。重要的是要注意,调度器不负责运行 Pod。它只是计算最适合托管 Pod 的节点,并通知 Kubernetes API 服务器,然后将这些信息存储在 etcd 中。在这一步中,Pod 被分配到最佳节点,并且关联关系被存储在 etcd 中。

管理 Kubernetes 调度器

图 17.3:Pod 调度过程的时间线

步骤 1:当提出创建和运行 Pod 的请求时,例如通过 kubectl 命令或 Kubernetes 部署,API 服务器会响应此请求。它会更新 Kubernetes 内部数据库(etcd),并将一个待执行的 Pod 条目添加到其中。请注意,在这个阶段,不能保证 Pod 将被调度。

污点和容忍

步骤 3:Kubernetes 代理(kubelet)通过 API 服务器不断监视 Kubernetes 数据存储。一旦一个新的 Pod 被分配到一个节点,它会尝试在节点上执行 Pod。当 Pod 成功启动并运行时,通过 API 服务器将其标记为在 etcd 中运行,此时过程完成。

现在我们对调度过程有了一定的了解,让我们看看如何调整它以满足我们的需求。

](image/B14870_17_03.jpg)

Kubernetes 提供了许多参数和对象,通过它们我们可以管理Kubernetes 调度器的行为。我们将研究以下管理调度过程的方式:

  • ![图 17.3:Pod 调度过程的时间线

  • Pod 亲和性和反亲和性

  • 节点亲和性和反亲和性

  • Pod 优先级和抢占

节点亲和性和反亲和性

使用节点亲和规则,Kubernetes 集群管理员可以控制 Pod 在特定节点集上的放置。节点亲和性或反亲和性允许您根据节点的标签来限制 Pod 可以运行的节点。

想象一下,您是银行共享 Kubernetes 集群的管理员。多个团队在同一集群上运行其应用程序。您的组织安全组已经确定了可以运行数据敏感应用程序的节点,并希望您确保没有其他应用程序在这些节点上运行。节点亲和性或反亲和性规则为满足此要求提供了解决方案,只将特定的 Pod 关联到一组节点。

节点亲和规则由两个组件定义。首先,您为一组节点分配一个标签。第二部分是配置 Pod,使它们只与具有特定标签的节点相关联。另一种思考方式是,Pod 定义了它应该放置在哪里,调度程序将此定义中的标签与节点标签进行匹配。

有两种类型的节点亲和性/反亲和性规则:

  • 必需规则是硬性规则。如果不满足这些规则,Pod 将无法在节点上调度。它在 Pod 规范的requiredDuringSchedulingIgnoredDuringExecution部分中定义。请参阅练习 17.01使用节点亲和性运行 Pod,作为此规则的示例。

  • 首选规则是软规则。调度程序尽量在可能的情况下执行首选规则,但如果规则无法执行,它会忽略这些规则,也就是说,如果严格遵循这些规则,Pod 将无法被调度。首选规则在 Pod 规范的preferredDuringSchedulingIgnoredDuringExecution部分中定义。

首选规则与每个标准相关联的权重。调度程序将根据这些权重创建一个分数,以在正确的节点上调度 Pod。权重字段的值范围从 1 到 100。调度程序计算所有合适节点的优先级分数,以找到最佳节点。请注意,分数可能会受到其他优先级函数的影响,例如LeastRequestedPriority

如果您定义的权重太低(与其他权重相比),则整体分数将受到其他优先级函数的最大影响,我们的首选规则可能对调度过程产生很少影响。如果定义了多个规则,则可以更改对您最重要的规则的权重。

亲和规则是在 Pod 规范中定义的。基于我们期望/不期望的节点的标签,我们将在 Pod 规范中提供选择标准的第一部分。它包括一组标签,以及可选的标签值。

标准的另一部分是提供我们想要匹配标签的方式。我们将这些匹配标准定义为亲和性定义中的运算符。此运算符可以具有以下值:

  • In运算符指示调度程序在匹配标签和指定值之一的节点上调度 Pod。

  • NotIn运算符指示调度程序不要在不匹配标签和任何指定值的节点上调度 Pod。这是一个否定运算符,表示反亲和性配置。

  • Exists运算符指示调度程序在匹配标签的节点上调度 Pod。在这种情况下,标签的值并不重要。因此,即使指定的标签存在且标签的值不匹配,此运算符也是满足的。

  • DoesNotExist运算符指示调度程序不要在不匹配标签的节点上调度 Pod。在这种情况下,标签的值并不重要。这是一个否定运算符,表示反亲和性配置。

请注意,亲和性和反亲和性规则是基于节点上的标签定义的。如果节点上的标签发生更改,可能会导致节点亲和性规则不再适用。在这种情况下,正在运行的 Pod 将继续在节点上运行。如果重新启动 Pod,或者 Pod 死亡并创建了一个新的 Pod,Kubernetes 将视其为新的 Pod。在这种情况下,如果节点标签已被修改,调度程序可能不会将 Pod 放在同一节点上。当您修改节点标签时,这是您需要注意的事项。让我们在以下练习中为一个 Pod 实现这些规则。

练习 17.01:运行具有节点亲和性的 Pod

在这个练习中,我们将配置一个 Pod,以便在我们的 Minikube 环境中可用的节点上进行调度。我们还将看到,如果标签不匹配,Pod 将处于Pending状态。想象一下这种状态,在这种状态下,调度程序无法找到合适的节点分配给 Pod:

  1. 使用以下命令创建一个名为schedulerdemo的新命名空间:
kubectl create ns schedulerdemo

您应该看到以下响应:

namespace/schedulerdemo created
  1. 现在我们需要创建一个具有节点亲和性定义的 Pod。创建一个名为pod-with-node-affinity.yaml的文件,其中包含以下规范:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-node-affinity
spec:
  affinity:
nodeAffinity: 
requiredDuringSchedulingIgnoredDuringExecution: 
       nodeSelectorTerms:
       - matchExpressions:
- key: data-center 
operator: In 
           values:
- sydney 
  containers:
    - name: pod-with-node-affinity-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep         5 ; done" ]

请注意,在 Pod 规范中,我们已经添加了新的affinity部分。这个规则被配置为requiredDuringSchedulingIgnoredDuringExecution。这意味着如果没有具有匹配标签的节点,这个 Pod 将不会被调度。还要注意,根据In运算符,这里提到的表达式将与节点标签匹配。在这个例子中,匹配的节点将具有标签data-center=sydney

  1. 尝试创建这个 Pod,看看它是否被调度和执行:
kubectl create -f pod-with-node-affinity.yaml -n schedulerdemo

你应该看到以下响应:

pod/pod-with-node-affinity created

请注意,这里看到的响应并不一定意味着 Pod 已成功在节点上执行。让我们在下一步中检查一下。

  1. 使用这个命令检查 Pod 的状态:
kubectl get pods -n schedulerdemo

你会看到以下响应:

NAME                     READY    STATUS    RESTARTS   AGE
pod-with-node-affinity   0/1      Pending   0          10s   

从这个输出中,你可以看到 Pod 处于Pending状态,没有被执行。

  1. 检查events以查看为什么 Pod 没有被执行:
kubectl get events -n schedulerdemo

你会看到以下响应:

图 17.4:获取事件列表

图 17.4:获取事件列表

你可以看到 Kubernetes 说没有节点与此 Pod 的选择器匹配。

  1. 在继续之前,让我们删除 Pod:
kubectl delete pod pod-with-node-affinity -n schedulerdemo

你应该看到以下响应:

pod "pod-with-node-affinity" deleted
  1. 现在,让我们看看我们集群中有哪些节点可用:
kubectl get nodes

你会看到以下响应:

NAME        STATUS    ROLES    AGE    VERSION
minikube    Ready     master   105d   v1.14.3

由于我们使用的是 Minikube,只有一个名为minikube的节点可用。

  1. 检查minikube节点的标签。使用如下所示的describe命令:
kubectl describe node minikube

你应该看到以下响应:

图 17.5:描述 minikube 节点

图 17.5:描述 minikube 节点

正如你所看到的,我们想要的标签data-center=sydney并不存在。

  1. 现在,让我们使用这个命令将期望的标签应用到我们的节点上:
kubectl label node minikube data-center=sydney

你会看到以下响应,表明节点已被标记:

node/minikube labeled
  1. 使用describe命令验证标签是否应用到节点上:
kubectl describe node minikube

你应该看到以下响应:

图 17.6:检查 minikube 节点上的标签

图 17.6:检查 minikube 节点上的标签

正如你在这张图片中看到的,我们的标签现在已经被应用。

  1. 现在再次尝试运行 Pod,看看它是否可以被执行:
kubectl create -f pod-with-node-affinity.yaml -n schedulerdemo

你应该看到以下响应:

pod/pod-with-node-affinity created
  1. 现在,让我们检查一下 Pod 是否成功运行:
kubectl get pods -n schedulerdemo

你应该看到以下响应:

NAME                     READY    STATUS     RESTARTS   AGE
pod-with-node-affinity   1/1      Running    0          5m22s

因此,我们的 Pod 成功运行。

  1. 让我们来看看 events 中如何显示 Pod 调度:
kubectl get events -n schedulerdemo

你将得到以下响应:

图 17.7:查看调度事件

图 17.7:查看调度事件

正如你在前面的输出中看到的,Pod 已成功调度。

  1. 现在,让我们进行一些清理工作,以避免与进一步的练习和活动发生冲突。使用以下命令删除 Pod:
kubectl delete pod pod-with-node-affinity -n schedulerdemo

你应该看到以下响应:

pod "pod-with-node-affinity" deleted
  1. 使用以下命令从节点中删除标签:
kubectl label node minikube data-center-

请注意,从 Pod 中删除标签的语法在标签名称后有一个额外的连字符()。你应该看到以下响应:

node/minikube labeled

在这个练习中,我们已经看到了节点亲和力是如何工作的,通过给节点贴标签,然后在贴有标签的节点上调度 Pod。我们还看到了 Kubernetes 事件如何用于查看 Pod 调度的状态。

在这个练习中我们使用的 data-center=sydney 标签也暗示了一个有趣的用例。我们可以使用节点亲和性和反亲和性规则来定位不仅特定的 Pod,还有特定的服务器机架或数据中心。我们只需为特定服务器机架、数据中心、可用区等的所有节点分配特定的标签。然后,我们可以简单地挑选所需的目标来为我们的 Pod。

Pod 亲和性和反亲和性

Pod 亲和力和 Pod 反亲和力允许你的 Pod 在被调度到节点之前检查在该节点上运行的其他 Pod。请注意,在这种情况下,其他 Pod 并不意味着相同 Pod 的新副本,而是与不同工作负载相关的 Pod。

Pod 亲和力允许你控制 Pod 有资格被调度到哪个节点,这取决于已经在该节点上运行的其他 Pod 的标签。其想法是满足在同一位置放置两种不同类型的容器的需求,或者将它们分开。

假设您的应用程序有两个组件:前端部分(例如 GUI)和后端(例如 API)。假设您希望将它们运行在同一主机上,因为如果前端和后端 Pod 在同一节点上托管,它们之间的通信将更快。在多节点集群(而不是 Minikube)上,默认情况下,调度程序将在不同的节点上调度这样的 Pod。Pod 亲和提供了一种控制 Pod 相对于彼此的调度的方式,以便我们可以确保应用程序的最佳性能。

定义 Pod 亲和需要两个组件。第一个组件定义了调度程序如何将目标 Pod(在我们之前的示例中,前端 Pod)与已经运行的 Pod(后端 Pod)相关联。这是通过 Pod 上的标签完成的。在 Pod 亲和规则中,我们提到了应该用于与新 Pod 相关联的其他 Pod 的哪些标签。标签选择器具有与节点亲和和反亲和部分中描述的类似操作符,用于匹配 Pod 的标签。

第二个组件描述了您希望在哪里运行目标 Pod。就像我们在前面的练习中看到的那样,我们可以使用 Pod 亲和规则将 Pod 调度到与其他 Pod 相同的节点(在我们的示例中,我们假设后端 Pod 是已经在运行的 otherPod),与其他 Pod 相同机架上的任何节点,与其他 Pod 相同数据中心的任何节点等等。该组件定义了 Pod 可以分配的节点集。为了实现这一点,我们对节点组进行标记,并在 Pod 规范中将此标签定义为topologyKey。例如,如果我们将主机名作为topologyKey的值,Pod 将被放置在同一节点上。

如果我们使用机架名称对节点进行标记,并将机架名称定义为topologyKey,那么候选 Pod 将被调度到具有相同机架名称标签的节点之一。

与前一节中定义的节点亲和规则类似,硬亲和规则和软亲和规则也存在。硬规则使用requiredDuringSchedulingIgnoredDuringExecution进行定义,而软规则使用preferredDuringSchedulingIgnoredDuringExecution进行定义。Pod 亲和配置中可能存在多种硬和软规则的组合。

练习 17.02:使用 Pod 亲和运行 Pod

在这个练习中,我们将看到 Pod 亲和性如何帮助调度器查看不同 Pod 之间的关系,并将它们分配到合适的节点上。我们将使用preferred选项放置 Pod。在这个练习的后面部分,我们将使用required选项配置 Pod 反亲和性,并看到直到满足所有标准为止,该 Pod 都不会被调度。我们将使用前面提到的前端和后端 Pod 的相同示例:

  1. 我们需要首先创建并运行后端 Pod。创建一个名为pod-with-pod-affinity-first.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-pod-affinity
  labels:
     application-name: banking-app
spec:
  containers:
    - name: pod-with-node-pod-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo 'this is         backend pod'; sleep 5 ; done" ]

这个 Pod 是一个简单的 Pod,只是循环打印一条消息。请注意,我们为 Pod 分配了一个标签,以便它与前端 Pod 相关联。

  1. 让我们创建上一步中定义的 Pod:
kubectl create -f pod-with-pod-affinity-first.yaml -n schedulerdemo

您应该看到以下响应:

pod/pod-with-pod-affinity created
  1. 现在,让我们看看 Pod 是否已成功创建:
kubectl get pods -n schedulerdemo

您应该看到这样的响应:

NAME                     READY    STATUS    RESTARTS   AGE
pod-with-pod-affinity    1/1      Running   0          22s
  1. 现在,让我们检查minikube节点上的标签:
kubectl describe node minikube

您应该看到以下响应:

图 17.8:描述 minikube 节点

图 17.8:描述 minikube 节点

由于我们希望在同一台主机上运行这两个 Pod,我们可以使用节点的kubernetes.io/hostname标签。

  1. 现在,让我们定义第二个 Pod。创建一个名为pod-with-pod-affinity-second.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-pod-affinity-fe
  labels:
     application-name: banking-app
spec:
  affinity:
   podAffinity: 
     preferredDuringSchedulingIgnoredDuringExecution: 
     - weight: 100
       podAffinityTerm:
         labelSelector:
           matchExpressions:
           - key: application-name
             operator: In 
             values:
             - banking-app
         topologyKey: kubernetes.io/hostname
  containers:
    - name: pod-with-node-pod-container-fe
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo 'this is         frontend pod'; sleep 5 ; done" ]

将此 Pod 视为前端应用程序。请注意,我们在podAffinity部分定义了preferredDuringSchedulingIgnoredDuringExecution规则。我们还为 Pod 和节点定义了labelstopologyKey

  1. 让我们创建上一步中定义的 Pod:
kubectl create -f pod-with-pod-affinity-second.yaml -n schedulerdemo

您应该看到以下响应:

pod/pod-with-pod-affinity-fe created
  1. 使用get命令验证 Pod 的状态:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

NAME                      READY    STATUS    RESTARTS   AGE
pod-with-pod-affinity     1/1      Running   0          7m33s
pod-with-pod-affinity-fe  1/1      Running   0          21s

如您所见,pod-with-pod-affinity-fe Pod 正在运行。这与普通的 Pod 放置没有太大不同。这是因为在 Minikube 环境中只有一个节点,并且我们使用了preferredDuringSchedulingIgnoredDuringExecution来定义 Pod 亲和性,这是匹配标准的软变体。

这个练习的下一步将讨论使用requiredDuringSchedulingIgnoredDuringExecution或匹配标准的硬变体的反亲和性,并且您将看到该 Pod 不会达到Running状态。

  1. 首先,让我们删除pod-with-pod-affinity-fe Pod:
kubectl delete pod pod-with-pod-affinity-fe -n schedulerdemo

您应该看到以下响应:

pod "pod-with-pod-affinity-fe" deleted
  1. 通过列出所有的 Pod 来确认 Pod 已被删除:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

NAME                     READY    STATUS    RESTARTS   AGE
pod-with-pod-affinity    1/1      Running   0          10m
  1. 现在创建另一个 Pod 定义,内容如下,并将其保存为pod-with-pod-anti-affinity-second.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-pod-anti-affinity-fe
  labels:
     application-name: backing-app
spec:
  affinity:
   podAntiAffinity: 
     requiredDuringSchedulingIgnoredDuringExecution: 
     - labelSelector:
         matchExpressions:
         - key: application-name
           operator: In 
           values:
           - banking-app
       topologyKey: kubernetes.io/hostname   
  containers:
    - name: pod-with-node-pod-anti-container-fe
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo 'this is         frontend pod'; sleep 5 ; done" ]

正如您所看到的,配置是针对podAntiAffinity,它使用requiredDuringSchedulingIgnoredDuringExecution选项,这是 Pod 亲和性规则的硬变体。在这里,调度程序将不会调度任何 Pod,如果条件不满足。我们使用In运算符,以便我们的 Pod 不会在与配置的labelSelector组件中定义的任何 Pod 相同的主机上运行。

  1. 尝试使用上述规范创建 Pod:
kubectl create -f pod-with-pod-anti-affinity-second.yaml -n schedulerdemo

您应该看到以下响应:

pod/pod-with-pod-anti-affinity-fe created
  1. 现在,检查此 Pod 的状态:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

NAME                           READY  STATUS    RESTARTS   AGE
pod-with-pod-affinity          1/1    Running   0          14m
pod-with-pod-anti-affinity-fe  1/1    Pending   0          3s

从这个输出中,您可以看到 Pod 处于Pending状态。

  1. 您可以通过检查事件来验证 Pod 反亲和性导致 Pod 无法调度:
kubectl get events -n schedulerdemo

您应该看到以下响应:

图 17.9:检查调度失败的事件

图 17.9:检查调度失败的事件

在这个练习中,我们已经看到 Pod 亲和性如何帮助将两个不同的 Pod 放置在同一个节点上。我们还看到了 Pod 反亲和性选项如何帮助我们在不同的主机集上调度 Pod。

Pod 优先级

Kubernetes 允许您为 Pod 关联一个优先级。如果存在资源约束,如果请求调度一个具有较高优先级的新 Pod,则 Kubernetes 调度程序可能会驱逐优先级较低的 Pod,以便为新的高优先级 Pod 腾出空间。

考虑一个例子,您是一个集群管理员,您在集群中运行关键和非关键的工作负载。一个例子是银行的 Kubernetes 集群。在这种情况下,您可能会有一个支付服务以及银行的网站。您可能会决定处理付款比运行网站更重要。通过配置 Pod 优先级,您可以防止低优先级的工作负载影响集群中的关键工作负载,特别是在集群开始达到其资源容量的情况下。将低优先级的 Pod 驱逐以安排更关键的 Pod 的技术可能比添加额外的节点更快,并且可以帮助您更好地管理集群上的流量波动。

将 Pod 与优先级关联的方式是定义一个名为PriorityClass的对象。该对象包含优先级,定义为 1 到 10 亿之间的数字。数字越高,优先级越高。一旦我们定义了我们的优先级类,我们通过将PriorityClass与 Pod 关联来为 Pod 分配优先级。默认情况下,如果 Pod 没有与其关联的优先级类,则 Pod 将被分配默认的优先级类(如果可用),或者将被分配优先级值为 0。

您可以像获取其他对象一样获取优先级类的列表:

kubectl get priorityclasses

您应该看到以下响应:

NAME                     VALUE         GLOBAL-DEFAULT   AGE
system-cluster-critical  2000000000    false            9d
system-node-critical     2000001000    false            9d

请注意,在 Minikube 中,环境中预定义了两个优先级类。让我们更多地了解system-cluster-critical类。发出以下命令以获取有关它的详细信息:

kubectl get pc system-cluster-critical -o yaml

您应该看到以下响应:

图 17.10:描述 system-cluster-critical PriorityClass

图 17.10:描述 system-cluster-critical PriorityClass

这里的输出提到这个类是为绝对关键的集群 Pod 保留的。etcd 就是这样的一个 Pod。让我们看看这个优先级类是否与它关联。

发出以下命令以获取有关在 Minikube 中运行的 etcd Pod 的详细信息:

kubectl get pod etcd-minikube -n kube-system -o yaml

您应该看到以下响应:

图 17.11:获取有关 etcd-minikube Pod 的信息

图 17.11:获取有关 etcd-minikube Pod 的信息

您可以从此输出中看到 Pod 已与system-cluster-critical优先级关联。

在接下来的练习中,我们将添加一个默认的优先级类和一个更高的优先级类,以更好地理解 Kubernetes 调度程序的行为。

重要的是要理解 Pod 优先级与其他规则(如 Pod 亲和性)协同工作。如果调度程序确定无法安排高优先级的 Pod,即使低优先级的 Pod 被驱逐,它也不会驱逐低优先级的 Pod。

同样,如果高优先级和低优先级的 Pod 正在等待调度,并且调度程序确定由于亲和性或反亲和性规则而无法安排高优先级的 Pod,则调度程序将安排适当的低优先级的 Pod。

练习 17.03:Pod 优先级和抢占

在这个练习中,我们将定义两个优先级类:默认(低优先级)和高优先级。然后,我们将创建 10 个具有默认优先级的 Pod,并为每个 Pod 分配一些 CPU 和内存。之后,我们将检查从我们的本地集群中使用了多少容量。然后,我们将创建 10 个具有高优先级的 Pod,并为它们分配资源。我们将看到具有默认优先级的 Pod 将被终止,并且更高优先级的 Pod 将被调度到集群上。然后,我们将把高优先级 Pod 的数量从 10 减少到 5,然后看到一些低优先级的 Pod 再次被调度。这是因为减少高优先级 Pod 的数量应该释放一些资源:

  1. 首先,让我们为默认优先级类创建定义。使用以下内容创建一个名为priority-class-default.yaml的文件:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: default-priority
value: 1
globalDefault: true
description: "Default Priority class."

请注意,我们通过将globalDefault的值设置为true来将此优先级类标记为默认。此外,优先级数字1非常低。

  1. 使用以下命令创建此优先级类:
kubectl create -f priority-class-default.yaml

您应该看到以下响应:

priorityclass.scheduling.k8s.io/default-priority

请注意,由于此对象不是命名空间级对象,因此我们没有提及命名空间。优先级类是 Kubernetes 中的集群范围对象。

  1. 让我们检查一下我们的优先级类是否已经创建:
kubectl get priorityclasses

您应该看到以下列表:

NAME                     VALUE         GLOBAL-DEFAULT   AGE
default-priority         1             true             5m46s
system-cluster-critical  2000000000    false            105d
system-node-critical     2000001000    false            105d

在此输出中,您可以看到我们刚刚创建的优先级类的名称为default-priority,并且正如您在GLOBAL-DEFAULT列中所看到的那样,它是全局默认的。现在创建另一个优先级更高的优先级类。

  1. 使用以下内容创建一个名为priority-class-highest.yaml的文件:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: highest-priority
value: 100000
globalDefault: false
description: "This priority class should be used for pods with   the highest of priority."

请注意此对象中value字段的非常高的值。

  1. 使用上一步的定义使用以下命令创建一个 Pod 优先级类:
kubectl create -f priority-class-highest.yaml

您应该看到以下响应:

priorityclass.scheduling.k8s.io/highest-priority created
  1. 现在让我们创建一个具有10个 Pod 和默认优先级的部署的定义。使用以下内容创建一个名为pod-with-default-priority.yaml的文件来定义我们的部署:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-default-priority-deployment
spec:
  replicas: 10
  selector:
    matchLabels:
      app: priority-test

  template:
    metadata:
      labels:
        app: priority-test
    spec:
      containers:
      - name: pod-default-priority-deployment-container
        image: k8s.gcr.io/busybox
        command: [ "/bin/sh", "-c", "while :; do echo 'this is           backend pod'; sleep 5 ; done" ]
      priorityClassName: default-priority
  1. 让我们创建我们在上一步中定义的部署:
kubectl create -f pod-with-default-priority.yaml -n schedulerdemo

您应该看到这个响应:

deployment.apps/pod-default-priority-deployment created
  1. 现在,通过使用以下命令将每个 Pod 分配的内存和 CPU 增加到 128 MiB 和 CPU 的 1/10:
kubectl set resources deployment/pod-default-priority-deployment --limits=cpu=100m,memory=128Mi -n schedulerdemo

您应该看到以下响应:

deployment.extensions/pod-default-priority-deployment resource requirements updated

注意

您可能需要根据计算机上可用的资源调整此资源分配。您可以从 1/10 的 CPU 开始,并按照步骤 10中提到的方式验证资源。

  1. 使用以下命令验证 Pod 是否正在运行:
kubectl get pods -n schedulerdemo

您应该会看到以下 Pod 列表:

图 17.12:获取 Pod 列表

图 17.12:获取 Pod 列表

  1. 检查我们集群中的资源使用情况。请注意,我们只有一个节点,因此我们可以通过发出describe命令轻松地看到这些值:
kubectl describe node minikube

以下截图已经被截断以便更好地呈现。在您的输出中找到分配的资源部分:

图 17.13:检查 minikube 节点上的资源利用率

图 17.13:检查 minikube 节点上的资源利用率

请注意,minikube主机的 CPU 使用率为 77%,内存使用率为 64%。请注意,资源利用率取决于您计算机的硬件和分配给 Minikube 的资源。如果您的 CPU 太强大,或者您有大量的内存(甚至如果您的 CPU 较慢,内存较少),您可能会看到与我们在这里看到的资源利用率值大不相同。请根据步骤 8中提到的方式调整 CPU 和内存资源,以便我们获得与我们在这里看到的类似的资源利用率。这将使您能够看到与我们在本练习的后续步骤中演示的类似结果。

  1. 现在让我们安排具有高优先级的 Pod。使用 Kubernetes 部署对象创建 10 个 Pod。为此,请创建一个名为pod-with-high-priority.yaml的文件,其中包含以下内容:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-highest-priority-deployment
spec:
  replicas: 10
  selector:
    matchLabels:
      app: priority-test

  template:
    metadata:
      labels:
        app: priority-test
    spec:
      containers:
      - name: pod-highest-priority-deployment-container
        image: k8s.gcr.io/busybox
        command: [ "/bin/sh", "-c", "while :; do echo 'this is           backend pod'; sleep 5 ; done" ]
      priorityClassName: highest-priority

请注意,在前面的规范中,priorityClassName已设置为highest-priority类。

  1. 现在创建我们在上一步中创建的部署:
kubectl create -f pod-with-high-priority.yaml -n schedulerdemo

您应该会得到以下输出:

deployment.apps/pod-with-highest-priority-deployment created
  1. 为这些 Pod 分配与具有默认优先级的 Pod 相似的 CPU 和内存量:
kubectl set resources deployment/pod-highest-priority-deployment --limits=cpu=100m,memory=128Mi -n schedulerdemo

您应该看到以下响应:

deployment.apps/pod-highest-priority-deployment resource requirements updated
  1. 大约一分钟后,运行以下命令以查看正在运行的 Pod:
kubectl get pods -n schedulerdemo

您应该看到类似于这样的响应:

图 17.14:获取 Pod 列表

图 17.14:获取 Pod 列表

您可以看到我们大多数高优先级的 Pod 都处于Running状态,而低优先级的 Pod 已经移动到Pending状态。这告诉我们 Kubernetes 调度程序实际上已经终止了低优先级的 Pod,并且现在正在等待资源再次安排它们。

  1. 尝试将高优先级的 Pod 数量从 10 个更改为 5 个,看看是否可以安排额外的低优先级 Pod。使用此命令更改副本的数量:
kubectl scale deployment/pod-highest-priority-deployment --replicas=5 -n schedulerdemo

您应该看到以下响应:

deployment.extensions/pod-highest-priority-deployment scaled
  1. 使用以下命令验证高优先级的 Pod 是否从 10 个减少到 5 个:
kubectl get pods -n schedulerdemo

图 17.15:获取 Pod 列表

图 17.15:获取 Pod 列表

正如您在此截图中所看到的,一些更低优先级的 Pod 已经从Pending状态变为Running状态。因此,我们可以看到调度程序正在根据工作负载的优先级来充分利用可用资源。

在这个练习中,我们已经使用了 Pod 优先级规则,并看到了 Kubernetes 调度程序可能会选择终止具有较低优先级的 Pod,如果有对具有较高优先级的 Pod 的请求需要满足。

污点和忍受度

之前,我们已经看到 Pod 可以配置以控制它们在哪个节点上运行。现在我们将看到节点如何控制可以在其上运行的 Pod,使用污点和忍受度。

污点阻止了 Pod 的调度,除非该 Pod 具有与之匹配的忍受度。将污点视为节点的属性,而忍受度是 Pod 的属性。只有当 Pod 的忍受度与节点的污点匹配时,Pod 才会被安排在该节点上。节点上的污点告诉调度程序检查哪些 Pod 能容忍污点,并且只运行与节点的污点匹配的 Pod。

污点定义包含键、值和效果。键和值将与 Pod 规范中的 Pod 忍受度定义匹配,而效果指示调度程序一旦节点的污点与 Pod 的忍受度匹配应该执行什么操作。

以下图表提供了一个概述,说明了基于污点和忍受度控制调度的过程是如何工作的。请注意,具有忍受度的 Pod 也可以安排在没有污点的节点上。

图 17.16:污点和忍受度如何影响调度的概述

图 17.16:污点和忍受度如何影响调度的概述

当我们定义一个污点时,我们还需要指定污点的行为。这可以通过以下值来指定:

  • NoSchedule提供了拒绝在节点上调度新 Pod 的能力。在定义污点之前已经调度的现有 Pod 将继续在节点上运行。

  • NoExecute污点提供了抵抗没有与污点匹配的容忍的新 Pod 的能力。它进一步检查所有正在节点上运行的现有 Pod 是否匹配此污点,并删除不匹配的 Pod。

  • PreferNoSchedule指示调度器避免在不容忍节点上调度 Pod。这是一个软规则,调度器会尝试找到正确的节点,但如果找不到其他适合定义的污点和容忍规则的节点,它仍会在节点上调度 Pod。

为了对节点应用污点,我们可以使用kubectl taint命令,如下所示:

kubectl taint nodes <NODE_NAME> <TAINT>:<TAINT_TYPE>

可能有很多原因你希望某些 Pod(应用程序)不在特定节点上运行。一个例子可能是需要专门的硬件,比如用于机器学习应用的 GPU。另一个情况可能是 Pod 上的软件的许可限制要求它在特定节点上运行。例如,在你的集群中有 10 个工作节点,只有 2 个节点被允许运行特定软件。使用污点和容忍的组合,你可以帮助调度器在正确的节点上调度 Pod。

练习 17.04:污点和容忍

在这个练习中,我们将看到污点和容忍如何允许我们在所需的节点上调度 Pod。我们将定义一个污点,并尝试在节点上调度一个 Pod。然后展示NoExecute功能,如果节点上的污点发生变化,Pod 可以从节点中移除:

  1. 使用以下命令获取节点列表:
kubectl get nodes

你应该看到以下节点列表:

NAME       STATUS    ROLES    AGE    VERSION
minikube   Ready     master   44h    v1.14.3

请记住,在我们的 Minikube 环境中,我们只有一个节点。

  1. 使用以下命令为minikube节点创建一个污点:
kubectl taint nodes minikube app=banking:NoSchedule

你应该看到以下响应:

node/minikube tainted
  1. 验证节点是否已正确被污点。你可以使用describe命令查看节点上应用了哪些污点:
kubectl describe node minikube

你应该看到以下响应:

图 17.17:检查 minikube 节点上的污点

图 17.17:检查 minikube 节点上的污点

  1. 现在,我们需要根据污点定义创建一个具有容忍度的 Pod。创建一个名为pod-toleration-noschedule.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-node-toleration-noschedule
spec:
  tolerations:
  - key: "app"
    operator: "Equal"
    value: "banking"
    effect: "NoSchedule"
  containers:
    - name: pod-with-node-toleration-noschedule-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep         5 ; done" ]

请注意,容忍度值与步骤 1中定义的污点相同,即app=bankingeffect属性控制容忍度行为的类型。在这里,我们将effect定义为NoSchedule

  1. 让我们根据前面的规范创建 Pod:
kubectl create -f pod-toleration-noschedule.yaml -n schedulerdemo

这应该得到以下响应:

pod/pod-with-node-toleration-noschedule created
  1. 使用以下命令验证 Pod 是否正在运行:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

图 17.18:获取 Pod 列表

图 17.18:获取 Pod 列表

  1. 现在让我们定义一个不匹配节点污点的容忍度的不同 Pod。创建一个名为pod-toleration-noschedule2.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-node-toleration-noschedule2
spec:
  tolerations:
  - key: "app"
    operator: "Equal"
    value: "hr"
    effect: "NoSchedule"
  containers:
    - name: pod-with-node-toleration-noschedule-container2
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep         5 ; done" ]

请注意,这里我们将容忍度设置为app=hr。我们需要一个具有相同污点以匹配此容忍度的 Pod。由于我们已经用app=banking污点了我们的节点,这个 Pod 不应该被调度程序调度。让我们在以下步骤中尝试一下。

  1. 使用上一步的定义创建 Pod:
kubectl create -f pod-toleration-noschedule2.yaml -n schedulerdemo

这应该得到以下响应:

pod/pod-with-node-toleration-noschedule2 created
  1. 使用以下命令检查 Pod 的状态:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

图 17.19:获取 Pod 列表

图 17.19:获取 Pod 列表

您可以看到 Pod 处于Pending状态,而不是Running状态。

  1. 在本练习的剩余部分中,我们将看到NoExecute效果如何指示调度程序甚至在将 Pod 调度到节点后将其删除。在此之前,我们需要进行一些清理。使用以下命令删除两个 Pod:
kubectl delete pod pod-with-node-toleration-noschedule pod-with-node-toleration-noschedule2 -n schedulerdemo

您应该看到以下响应:

pod "pod-with-node-toleration-noschedule" deleted
pod "pod-with-node-toleration-noschedule2" deleted
  1. 使用以下命令从节点中删除污点:
kubectl taint nodes minikube app:NoSchedule-

请注意命令末尾的连字符(-),它告诉 Kubernetes 删除此标签。您应该看到以下响应:

node/minikube untainted

我们的节点处于未定义污点的状态。现在,我们想先以app=banking的容忍度运行一个 Pod 并分配该 Pod。一旦 Pod 处于Running状态,我们将从节点中删除污点并查看 Pod 是否已被删除。

  1. 现在,再次使用NoExecute类型对节点进行污染:
kubectl taint nodes minikube app=banking:NoExecute

您应该看到以下响应:

node/minikube tainted
  1. 现在,我们需要定义一个具有匹配容忍度的 Pod。创建一个名为pod-toleration-noexecute.yaml的文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-node-toleration-noexecute
spec:
  tolerations:
  - key: "app"
    operator: "Equal"
    value: "banking"
    effect: "NoExecute"
  containers:
    - name: pod-with-node-toleration-noexecute-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep         5 ; done" ]

请注意,tolerations部分将标签定义为app=banking,效果定义为NoExecute

  1. 使用以下命令创建我们在上一步中定义的 Pod:
kubectl create -f pod-toleration-noexecute.yaml -n schedulerdemo

您应该看到以下响应:

pod/pod-with-node-toleration-noexecute created
  1. 使用以下命令验证 Pod 是否处于Running状态:
kubectl get pods -n schedulerdemo

您应该看到以下响应:

图 17.20:获取 Pod 列表

图 17.20:获取 Pod 列表

  1. 现在使用以下命令从节点中删除污点:
kubectl taint nodes minikube app:NoExecute-

请注意此命令末尾的连字符(-),它告诉 Kubernetes 删除污点。您将看到以下响应:

node/minikube untainted

如前所述,具有容忍度的 Pod 可以附加到没有污点的节点。删除污点后,Pod 仍将被执行。请注意,我们尚未删除 Pod,它仍在运行。

  1. 现在,如果我们向节点添加一个带有NoExecute的新污点,Pod 应该会从中删除。要查看此操作,请添加一个与 Pod 容忍度不同的新污点:
kubectl taint nodes minikube app=hr:NoExecute

如您所见,我们已将app=hr污点添加到 Pod 中。您应该看到以下响应:

node/minikube tainted
  1. 现在,让我们检查一下 Pod 的状态:
kubectl get pods -n schedulerdemo

您将看到以下响应:

图 17.21:检查我们的 Pod 的状态

图 17.21:检查我们的 Pod 的状态

Pod 将被删除或进入Terminating(标记为删除)状态。几秒钟后,Kubernetes 将删除 Pod。

在这个练习中,您已经看到我们如何在节点上配置污点,以便它们只接受特定的 Pod。您还配置了污点以影响正在运行的 Pod。

使用自定义 Kubernetes 调度程序

构建自己的功能齐全的调度程序超出了本研讨会的范围。但是,重要的是要理解,Kubernetes 平台允许您编写自己的调度程序,如果您的用例需要,尽管不建议使用自定义调度程序,除非您有非常专业的用例。

自定义调度程序作为普通 Pod 运行。您可以在运行应用程序的 Pod 的定义中指定使用自定义调度程序。您可以在 Pod 规范中添加一个schedulerName字段,其中包含自定义调度程序的名称,如此示例定义所示:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-custom-scheduler
spec:
  containers:
    - name: mutating-pod-example-container
      image: k8s.gcr.io/busybox
      command: [ "/bin/sh", "-c", "while :; do echo '.'; sleep 5 ;         done" ]
  schedulerName: "custom-scheduler"

为使此配置工作,假定集群中有一个名为custom-scheduler的自定义调度程序。

活动 17.01:配置 Kubernetes 调度程序以安排 Pod

假设您是 Kubernetes 集群的管理员,并且您面临以下情景:

  1. 有一个 API Pod 提供当前的货币转换率。

  2. 有一个 GUI Pod 在网站上显示转换率。

  3. 有一个 Pod 为股票交易所提供实时货币转换率的服务。

您被要求确保 API 和 GUI Pod 在同一节点上运行。您还被要求在流量激增时给予实时货币转换器 Pod 更高的优先级。在此活动中,您将控制 Kubernetes 调度程序的行为以完成此活动。

此活动中的每个 Pod 应分配 0.1 CPU 和 100 MiB 内存。请注意,我们已经将 Pod 命名为 API、GUI 和实时,以便操作更简单。此活动中的 Pod 预计只会在控制台上打印表达式。您可以为它们全部使用 k8s.gcr.io/busybox 镜像。

注意

在开始此活动之前,请确保节点没有从之前的练习中被污染。要了解如何去除污点,请参阅本章的“练习 17.01”中的“步骤 15”,“在节点亲和性下运行 Pod”。

以下是此活动的一些指南:

  1. 创建一个名为scheduleractivity的命名空间。

  2. 为 API Pod 创建 Pod 优先级。

  3. 部署并确保 API 和 GUI Pod 使用 Pod 亲和性在同一节点上。GUI Pod 应定义与 API Pod 在同一节点上的亲和性。

  4. 将 API 和 GUI Pod 的副本扩展到各自的两个。

  5. 为实时货币转换器 Pod 创建一个 Pod 优先级。确保之前定义的 API Pod 优先级低于实时 Pod,但大于 0。

  6. 部署并运行一个实时货币转换器 Pod,副本数为 1。

  7. 确保所有 Pod 都处于“运行”状态。

  8. 现在,将实时货币转换器 Pod 的副本数量从 1 增加到 10。

  9. 查看实时货币转换器 Pod 是否正在启动,GUI Pod 是否正在被驱逐。如果没有,请继续以 5 的倍数增加实时 Pod。

  10. 根据您的资源和 Pod 的数量,调度程序可能会开始驱逐 API Pod。

  11. 将实时 Pod 的副本数量从 10 减少到 1,并确保 API 和 GUI Pod 被重新调度到集群上。

完成活动后,预计 API 和 GUI Pod 每个将处于“运行”状态,以及一个实时 Pod,如下截图所示:

图 17.22:活动 17.01 的预期输出

图 17.22:活动 17.01 的预期输出

请注意,您的输出将根据系统资源而变化,因此您可能看不到与此截图完全相同的内容。

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

总结

Kubernetes 调度器是一个强大的软件,它抽象了在集群上为 Pod 选择合适节点的工作。调度器会监视未调度的 Pod,并尝试为它们找到合适的节点。一旦找到一个适合的节点,它会通过 API 服务器更新 etcd,表示该 Pod 已绑定到该节点。

随着 Kubernetes 的每一个发布,调度器都得到了成熟。调度器的默认行为对各种工作负载已经足够,尽管您也看到了许多定制调度器与 Pod 关联资源的方式。您已经看到了节点亲和性如何帮助您在所需的节点上调度 Pod。Pod 亲和性可以帮助您相对于另一个 Pod 调度一个 Pod,这对于多个模块被放置在一起的应用程序是一个很好的工具。污点和容忍也可以帮助您将特定的工作负载分配给特定的节点。您还看到了 Pod 优先级如何帮助您根据集群中可用的总资源调度工作负载。

在下一章中,我们将升级一个 Kubernetes 集群,实现零停机。如果您在集群中使用本章展示的任何技术配置了自定义调度,您可能需要相应地计划升级。由于升级将逐个关闭一个工作节点,可能会导致一些 Pod 由于您的配置而变得不可调度,这可能不是一个可接受的解决方案。

第十八章: 在没有停机的情况下升级你的集群

概述

在本章中,我们将讨论如何在没有停机的情况下升级你的集群。我们将首先了解保持你的 Kubernetes 集群最新的需求。然后,我们将了解基本的应用部署策略,可以帮助实现 Kubernetes 集群的零停机升级。然后,我们将通过在没有应用停机的情况下对 Kubernetes 集群进行升级来将这些策略付诸实践。

介绍

我们在第十一章《构建你自己的 HA 集群》中学习了如何在 AWS 上使用 kops 搭建多节点 Kubernetes 平台。在本章中,你将学习如何将 Kubernetes 平台升级到新版本。我们将通过实际示例为你演示升级 Kubernetes 平台所需的步骤。这些练习还将使你具备维护 Kubernetes 集群所需的技能。

不同的组织以不同的方式设置和维护他们的 Kubernetes 集群。在第十二章《你的应用和 HA》中,你看到了设置集群的多种方式。我们将介绍一个简单的技术来升级你的集群,根据你处理的集群的不同,你需要采取的确切技术和步骤可能会有所不同,尽管我们在这里提到的基本原则和预防措施将适用于你升级集群的方式。

升级你的 Kubernetes 集群的需求

建立起你的业务应用并将其推向世界只是游戏的一半。让你的应用能够以安全、可扩展和一致的方式被客户使用是另一半,也是你必须不断努力的一半。为了能够很好地执行这另一半,你需要一个坚固的平台。

在当今竞争激烈的环境中,及时向客户提供最新功能对于让你的业务获得优势至关重要。这个平台不仅必须可靠,还必须提供新的和更新的功能,以满足运行现代应用的需求。Kubernetes 是一个快速发展的平台,非常适合这样一个动态的环境。Kubernetes 的开发和进步速度可以从官方 Kubernetes GitHub 存储库的提交数量中得到证明。让我们来看一下下面的截图:

图 18.1:2019 年 8 月 25 日至 31 日期间对 Kubernetes 项目的每日提交

图 18.1:2019 年 8 月 25 日至 31 日期间对 Kubernetes 项目的每日提交

橙色条形图代表每周的提交次数,您可以看到平均每周超过 100 次。下面的绿线图显示了 8 月 25 日至 8 月 31 日的提交次数。仅在一个星期的星期二就有超过 50 次提交。

到目前为止,很明显 Kubernetes 正在快速发展,但您可能仍然不确定是否需要更新集群上的 Kubernetes 版本。以下是一些重要原因,说明为什么保持平台更新至关重要:

  • 新功能:Kubernetes 社区不断添加新功能,以满足现代应用程序的需求。您的软件团队可能会开发一个依赖于较新 Kubernetes 功能的新软件组件。因此,坚持使用较旧版本的 Kubernetes 将阻碍软件的开发。

  • 安全补丁:Kubernetes 平台中有许多组件在不断变化。不仅需要修补 Kubernetes 二进制文件,还需要修补许多 Linux 功能,如 iptables 和 cgroups。如果 Kubernetes 使用的任何组件存在漏洞,您可能需要修补底层组件,如操作系统本身。以一种一致的方式进行升级对于尽可能保持 Kubernetes 生态系统的安全性非常重要。

例如,在 Kubernetes API 服务器的 1.0–1.12 版本中存在一个漏洞,导致 API 服务器可能因为无效的 YAML 或 JSON 负载而消耗大量资源。您可以在此链接找到有关此漏洞的更多详细信息:cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11253

  • 更好地处理现有功能:Kubernetes 团队不仅添加新功能,还不断改进现有功能以提高稳定性和性能。这些改进可能对您现有的应用程序或自动化脚本有用。因此,从这个角度来看,保持平台更新也是一个好主意。

Kubernetes 组件 – 复习

到目前为止,您已经了解了 Kubernetes 平台的基本组件。作为一个复习,让我们重新审视一下主要组件:

  • API 服务器负责公开 RESTful Kubernetes API,并且是无状态的。您集群上的所有用户、Kubernetes 主控组件、kubectl 客户端、工作节点,甚至可能是您的应用程序都需要与 API 服务器进行交互。

  • 键值存储(etcd 服务器)存储对象并为 API 服务器提供持久后端。

  • 调度程序和控制器管理器用于实现集群的状态和存储在 etcd 中的对象。

  • kubelet 是在每个工作节点上运行的程序,类似于代理,按照 Kubernetes 主控组件的指示执行工作。

当我们更新平台时,正如您将在后面的部分中看到的,我们将利用这些组件并将它们作为单独的模块进行升级。

警告

Kubernetes 版本标记为 A.B.C,遵循语义化版本概念。A 是主要版本,B 是次要版本,C 是补丁发布。根据 Kubernetes 文档," 高可用 (HA) 集群 中,最新和最旧的 kube-apiserver 实例必须在一个次要版本内。'

在规划升级时,以下是最安全的方法:

  • 始终首先升级到当前次要版本的最新修补版本。例如,如果您使用的是 1.14.X,首先升级到 1.14.X 发行系列的最新可用版本。这将确保平台已应用了该集群版本的所有可用修复程序。最新的修补程序可能有 bug 修复,这可能为您提供通往下一个次要版本的更顺畅的路径,在我们的示例中将是 1.15.X

  • 升级到下一个次要版本。尽量避免跨越多个次要版本,即使可能,因为通常 API 兼容性在一个次要发布版本内。在升级过程中,Kubernetes 平台将同时运行两个不同版本的 API,因为我们一次只升级一个节点。例如,最好从 1.14 升级到 1.15,而不是升级到 1.16

另一个重要的事情要考虑的是,看看新版本是否需要来自底层 Linux 操作系统的一些更新的库。尽管一般来说,补丁版本不需要任何底层组件的升级,但保持底层操作系统的最新状态也应该是您的首要任务,以为 Kubernetes 平台提供一个安全和一致的环境。

升级过程

在这一部分,您将看到升级 Kubernetes 平台所需的步骤。请注意,这里不涵盖升级底层操作系统。为了满足零停机升级的要求,您必须拥有一个具有至少三个主节点和 etcd 服务器的 HA Kubernetes 集群,这样可以实现无摩擦的升级。该过程将使三个节点中的一个脱离集群并进行升级。然后升级后的组件将重新加入集群,然后我们将对第二个节点应用升级过程。由于在任何给定时间,至少有两个服务器保持可用,因此在升级过程中集群将保持可用。

kops 的一些考虑因素

我们已经在第十一章中指导您创建了一个 HA Kubernetes 集群。因此,在本章中,我们将指导您升级相同的集群。

如该章节中所述,部署和管理 Kubernetes 集群有各种方式。我们选择了 kops,它具有用于升级 Kubernetes 组件的内置工具。我们将在本章中利用它们。

kops 的版本设置为与其实现的 Kubernetes 的次要版本类似。例如,kops 版本1.14.x实现了 Kubernetes 版本1.14.x。有关更多详细信息,请参阅此链接:kops.sigs.k8s.io/welcome/releases/

注意

在我们在第十一章中创建的 HA 集群中,我们部署了三个主节点,这些节点承载了所有 Kubernetes 主平面组件,包括 etcd。

升级过程概述

整个升级过程可以用图表总结如下:

图 18.2:推荐的升级过程

图 18.2:推荐的升级过程

在我们继续实施之前,让我们快速查看每个步骤:

  1. 阅读发布说明

这些将指示在升级过程中可能需要的任何特殊注意事项。每个版本的发布说明都可以在 GitHub 的此链接上找到:github.com/kubernetes/kubernetes/tree/master/CHANGELOG

  1. 备份 etcd 数据存储

正如您之前学到的那样,etcd 存储了集群的整个状态。etcd 的备份可以让您在需要时恢复数据存储的状态。

  1. 备份节点作为可选的故障保护

如果升级过程不顺利,并且您想要恢复到先前的状态,这可能会派上用场。云供应商(如 AWS、GCP、Azure 等)使您能够对主机进行快照。如果您在私有数据中心运行并为您的机器使用虚拟化技术,您的虚拟化提供商(例如 VMware)可能会提供工具来对节点进行快照。在开始升级 Kubernetes 平台之前,进行快照超出了本书的范围,但尽管如此,这是一个有用的步骤。

  1. 如有必要,升级 etcd

用于部署和管理 Kubernetes 集群的工具的更新版本(例如我们的 kops)通常会自动处理这一点。即便如此,这是一个重要的考虑因素,特别是如果您没有使用 kops 等工具。

检查并验证新版本的 Kubernetes 是否需要不同版本的 etcd 存储。这并不总是必要的,但根据您的版本可能需要。例如,Kubernetes 版本1.13需要 etcd v3,而较早版本可以使用 etcd v2。

通过阅读发布说明(步骤 1)可以确定是否需要升级 etcd。例如,当较早版本的 etcd 在 1.13 版本中被淘汰时,在发布说明中明确提到了这一点:github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.13.md#urgent-upgrade-notes

  1. 升级主要组件

登录到堡垒主机,并根据期望的 Kubernetes 版本升级 kops 的版本。这个兼容矩阵应该是一个有用的指南:kops.sigs.k8s.io/welcome/releases/#compatibility-matrix

在第一个主节点上运行升级,验证其是否正确更新,然后对所有其他主节点重复相同的步骤。

  1. 升级工作节点组

正如您在第十一章构建您自己的 HA 集群中看到的,kops 允许您使用实例组来管理节点,这与 AWS 的自动扩展组相关联。在工作节点的第一个实例组上运行升级。要验证节点是否成功升级,您需要检查节点是否升级到所需版本的 Kubernetes,以及是否在升级后的节点上调度了 pod。对所有其他工作节点的实例组重复相同的步骤。

  1. 验证升级过程是否成功

检查所有节点是否已升级,并且所有应用程序是否按预期运行。

自动化的重要性

从概述中可以看出,升级集群需要几个步骤。考虑到发布和补丁的数量,您可能经常需要这样做。由于该过程有很好的文档记录,强烈建议您考虑使用自动化工具,如 Ansible 或 Puppet,来自动化整个过程。所有前面的步骤都可以完全自动化,您可以重复升级集群的方式。但是,本章不涵盖自动化,因为这超出了本书的范围。

备份 etcd 数据存储

etcd 存储整个集群的状态。因此,对 etcd 进行快照可以让我们将整个集群恢复到快照被拍摄时的状态。如果您想将集群恢复到先前的状态,这可能会很有用。

注意

在开始任何练习之前,请确保按照第十一章构建您自己的 HA 集群中的说明设置并可用集群,并且您可以通过 SSH 从计算机访问节点。还建议您在开始升级过程之前对节点进行快照。这是特别有益的,因为在本章中,您将对集群进行两次升级-一次在练习期间,一次在活动期间。

现在,在我们进行第一个练习之前,我们需要更多地了解 etcd。它的工作方式是在您的集群中作为一个 pod 在kube-system命名空间中运行(正如您在第二章Kubernetes 概述中看到的),并公开一个 API,用于向其写入数据。每当 Kubernetes API 服务器想要将任何数据持久化到 etcd 时,它将使用 etcd 的 API 来访问它。

为了备份 etcd,我们还需要访问其 API 并使用内置函数保存快照。为此,我们将使用一个名为etcdctl的命令行客户端,它已经存在于 etcd pod 中。对于我们的目的,不需要详细介绍此工具和 etcd API,因此我们不在本书中包含它。您可以在此链接了解更多信息:github.com/etcd-io/etcd/tree/master/etcdctl

现在,让我们看看如何在以下练习中使用 etcdctl 来备份 etcd。

练习 18.01:对 etcd 数据存储进行快照

在这个练习中,我们将看到如何对 etcd 存储进行快照。如前一节所述,根据您的升级路径,可能不需要手动升级 etcd,但备份 etcd 是必不可少的。对于此操作和所有后续的练习和活动,请使用相同的机器(您的笔记本电脑或台式机),您用来执行练习 11.01设置我们的 Kubernetes 集群

  1. 我们已经使用 kops 安装了集群。Kops 使用两个不同的 etcd 集群 - 一个用于 Kubernetes 组件生成的事件,另一个用于其他所有内容。您可以通过发出以下命令来查看这些 pods:
kubectl get pods -n kube-system | grep etcd-manager

这应该获取 etcd pods 的详细信息。您应该看到类似以下的输出:

图 18.3:获取 etcd-manager pods 的列表

图 18.3:获取 etcd-manager pods 的列表

  1. 默认情况下,kops 的etcd-manager功能每 15 分钟创建一次备份。备份的位置与 kops 工具使用的 S3 存储相同。在练习 11.01中,您配置了 S3 存储桶以存储 kops 的状态。让我们查询存储桶,看看那里是否有备份可用:
aws s3api list-objects --bucket $BUCKET_NAME | grep backups/etcd/main

您应该看到类似这样的响应:

图 18.4:获取可用备份列表

图 18.4:获取可用备份列表

您可以看到备份每 15 分钟自动进行,并且备份的时间戳已标记。我们将在下一步中使用在上一张截图中突出显示的最新备份的Key

  1. 下一步是从 S3 存储桶获取备份。我们可以使用 AWS CLI 命令来获取我们需要的备份:
aws s3api get-object --bucket $BUCKET_NAME --key "myfirstcluster.k8s.local/backups/etcd/main/2020-06-14T02:06:33Z-000001/etcd.backup.gz'  etcd-backup-$(date +%Y-%m-%d_%H:%M:%S_%Z).db

请注意,此命令包含存储桶的名称,上一步中文件的Key,以及我们在保存文件时要使用的文件名。使用在上一步的输出中获取的Key。您应该看到类似于此的响应:

图 18.5:从我们的 S3 存储桶中保存 etcd 备份

图 18.5:从我们的 S3 存储桶中保存 etcd 备份

请注意,我们使用date命令生成文件名。这是系统管理员常用的技术,用于确保不会覆盖任何文件。

请注意

如果您想使用此备份恢复您的 etcd 实例,您可以在此链接找到恢复说明:kops.sigs.k8s.io/operations/etcd_backup_restore_encryption/

  1. 验证备份文件是否已创建:
ls -lrt 

您应该看到以下响应:

图 18.6:确认保存的 etcd 备份

图 18.6:确认保存的 etcd 备份

您应该能够在响应中看到我们创建的快照。

在这个练习中,您已经学会了如何生成 etcd 数据存储的备份。这个备份是 Kubernetes 的状态,不仅在您的升级遇到任何问题时可能有用,而且在任何其他情况下恢复集群也可能有用,比如灾难恢复DR)场景。

排空节点并使其不可调度

在我们开始升级任何节点(主节点或工作节点)之前,我们需要确保没有任何 pod(包括主要组件的 pod)在此节点上运行。这是准备升级任何节点的重要步骤。此外,该节点需要标记为不可调度。不可调度的节点是调度程序不在此节点调度任何 pod 的标志。

我们可以使用drain命令将节点标记为不可调度,并驱逐所有 pod。drain命令不会删除任何 DaemonSet pod,除非我们告诉标志这样做。这种行为的原因之一是,DaemonSet pod 不能被调度到任何其他节点上。

请注意,drain命令等待优雅终止 pod,并强烈建议在生产环境中等待所有 pod 优雅地终止。让我们在以下练习中看到这一点。

练习 18.02:从节点中排空所有的 Pod

在这个练习中,我们将删除在一个节点上运行的所有 pods。一旦所有的 pods 都被移除,我们将把节点改回可调度状态,以便它可以接受新的工作负载。这是当节点已经升级并准备接受新的 pods 时。

  1. 获取所有节点的列表:
kubectl get nodes

您应该看到类似于这样的响应:

图 18.7:获取节点列表

图 18.7:获取节点列表

在这个例子中,我们有两个 worker 节点和三个 master 节点。

  1. 创建一个名为upgrade-demo的新命名空间:
kubectl create ns upgrade-demo

您应该看到以下响应:

namespace/upgrade-demo created
  1. 运行一堆 pods 来模拟工作负载。创建一个名为multiple-pods.yaml的文件,其中包含以下内容:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
replicas: 4
  selector:
    matchLabels:
      app.kubernetes.io/name: sleep
  template:
    metadata:
      labels:
        app.kubernetes.io/name: sleep
    spec:
      containers:
      - name: sleep
        image: k8s.gcr.io/busybox
        command: [ "/bin/sh', "-c', "while :; do echo 'this is           backend pod'; sleep 5 ; done' ]
        imagePullPolicy: IfNotPresent

部署将创建四个 pods 的副本。

  1. 现在,使用配置来创建部署:
kubectl create -f multiple-pod.yaml -n upgrade-demo

您应该看到这个响应:

deployment.apps/sleep created
  1. 验证它们是否在 worker pods 上运行:
kubectl get pods -n upgrade-demo -o wide

您的输出应该是这样的:

图 18.8:验证 pods 是否在 worker 节点上运行

图 18.8:验证 pods 是否在 worker 节点上运行

请注意,默认调度程序行为会将 pods 分布在两个 worker 节点之间。

  1. 使用drain命令从任何节点中驱逐所有的 pods。这个命令也会将节点标记为不可调度:
kubectl drain kube-group-1-mdlr --ignore-daemonsets

使用您从上一步的输出中获得的节点的名称。注意,我们传递了一个标志来忽略 daemon sets。您应该看到以下响应:

图 18.9:排水节点

图 18.9:排水节点

如果我们不设置--ignore-daemonsets标志,并且节点上有一些 DaemonSet pods,drain将不会在没有这个标志的情况下继续进行。我们建议使用这个标志,因为您的集群可能正在运行一些关键的 pods 作为 DaemonSet - 例如,一个从节点上的所有其他 pods 收集日志并将它们发送到中央日志服务器的 Fluentd pod。您可能希望在最后一刻之前保留这个日志收集 pod 的可用性。

  1. 验证所有的 pods 是否已经从该节点排空。为此,获取一个列表的 pods:
kubectl get pods -n upgrade-demo -o wide

您应该看到以下响应:

图 18.10:检查 pods 是否已经从排空的节点移开

图 18.10:检查 pods 是否已经从排空的节点移开

在前面的截图中,您可以看到所有的 pod 都在另一个节点上运行。我们的集群中只有两个工作节点,所以所有的 pod 都被调度到了唯一可调度的节点上。如果我们有几个可用的工作节点,调度器会将 pod 分布在它们之间。

  1. 让我们描述一下我们的排水节点并做一些重要的观察:
kubectl describe node kube-group-1-mdlr

使用您在步骤 6中排水的节点名称。这将产生一个相当长的输出,但有两个值得观察的部分:

图 18.11:检查我们排水节点的污点和不可调度状态

图 18.11:检查我们排水节点的污点和不可调度状态

前面的截图显示我们的节点被标记为不可调度。接下来,在您的输出中找到以下类似的部分:

图 18.12:检查排水节点上的非终止 pod

图 18.12:检查排水节点上的非终止 pod

这表明我们系统上唯一正在运行的非终止 pod 的名称以kube-proxyweave-net开头。第一个 pod 实现了kube-proxy,它是管理节点上的 pod 和服务网络规则的组件。第二个 pod 是weave-net,它为我们的集群实现了虚拟网络(请注意,您的网络提供程序取决于您选择的网络类型)。由于我们在步骤 6中添加了一个排除 DaemonSets 的标志,这些由 DaemonSet 管理的 pod 仍在运行。

  1. 一旦您在步骤 6中排水了 pod,您就可以升级节点。即使升级不是本练习的一部分,我们只是想让节点再次可调度。为此,请使用以下命令:
kubectl uncordon kube-group-1-mdlr

您应该看到类似于以下内容的响应:

node/kube-group-1-mdlr uncordoned
  1. 验证节点是否再次可调度。检查以下输出中的“污点”部分:
kubectl describe node kube-group-1-mdlr

您应该看到类似于以下内容的响应:

图 18.13:检查我们未封锁节点的污点和不可调度状态

图 18.13:检查我们未封锁节点的污点和不可调度状态

前面的截图显示节点现在是可调度的,并且我们在步骤 8中观察到的污点已经被移除。

在这个练习中,您已经看到如何从节点中删除所有的 pod 并将节点标记为不可调度。这将确保在该节点中不会安排新的 pod,并且我们可以开始升级该节点。我们还学习了如何使节点再次可调度,以便在完成升级后继续使用它。

升级 Kubernetes 主要组件

当您以任何重要程度运行 Kubernetes 对您的组织很重要时,您将以 HA 配置运行平台。为了实现这一点,典型的配置至少是三个主要组件的副本,运行在三个不同的节点上。这允许您逐个将单个节点从一个次要版本升级到下一个次要版本,同时在升级后重新加入集群时仍然保持 API 兼容性,因为 Kubernetes 提供了一次次要版本的兼容性。这意味着在逐个升级每个节点时,主要组件可以处于不同的版本。以下表格提供了版本的逻辑流。假设您正在从版本 1.14 升级到 1.15:

图 18.14:三个主节点的升级计划

图 18.14:三个主节点的升级计划

在接下来的练习中,我们将继续升级 Kubernetes 主要组件。

练习 18.03:升级 Kubernetes 主要组件

在这个练习中,您将升级 Kubernetes 主节点上的所有主要组件。此练习假定您仍然登录到集群的堡垒主机。

在这个练习中,我们演示了一个较少数量的节点的过程,以简化操作,但是升级大量节点的过程是相同的。然而,为了实现无缝升级,三个主节点是最少的,并且您的应用程序应该是 HA,并且至少在两个工作节点上运行:

  1. 运行 kops 验证器来验证现有的集群:
kops validate cluster 

您应该看到类似以下的响应:

图 18.15:验证我们的 kops 集群

图 18.15:验证我们的 kops 集群

这是输出的截断版本。它显示了集群的主要基础设施组件。

  1. 列出集群中的所有节点:
kubectl get nodes

您应该看到类似这样的响应:

图 18.16:获取节点列表

图 18.16:获取节点列表

请注意,我们有三个主节点,它们都在 1.15.7 版本上。

注意

在这个练习中,我们展示了从 Kubernetes 版本 1.15.7 升级到 1.15.10。您可以应用相同的步骤来升级到 kops 在您执行此练习时支持的 Kubernetes 版本。只需记住我们之前的建议,先升级到最新的补丁版本(这就是我们在这里所做的)。

  1. 使用kops upgrade cluster命令查看可用的更新:
kops upgrade cluster ${NAME}

请注意,这个命令不会直接运行更新,但它会给出可能的最新更新版本。NAME环境变量保存了您的集群名称。您应该看到类似以下的输出:

图 18.17:检查可用的集群版本

图 18.17:检查可用的集群版本

您可以从前面的截图中看到,OLD版本是1.15.7,这是我们当前的版本,NEW版本是1.15.10,这是我们的目标版本。

  1. 一旦您验证了步骤 4中的命令的更改,使用--yes标志运行相同的命令。这将在 kops 状态存储中标记集群的期望状态:
kops upgrade cluster --yes

您应该看到类似以下的输出:

图 18.18:升级 kops 集群配置

图 18.18:升级 kops 集群配置

这个输出表明了 Kubernetes 集群的期望版本已记录在更新的 kops 配置中。在下一步中,我们将要求 kops 更新云或集群资源以匹配新的规格-即 Kubernetes 版本1.15.10

  1. 现在,让我们运行以下命令,以便 kops 更新集群以匹配更新的 kops 配置:
kops update cluster ${NAME} --yes

这将产生一个长输出,最终会以类似以下的方式结束:

图 18.19:根据我们集群升级的要求更新我们的集群基础架构我们集群升级的要求

图 18.19:根据我们集群升级的要求更新我们的集群基础架构

这已经更新了集群基础架构,以匹配更新的 kops 配置。接下来,我们需要对运行在这个基础架构上的 Kubernetes 主组件进行升级。

  1. 如果您在不同实例组上运行多个主/工作节点实例,那么您可以控制哪个实例组接收更新。为此,让我们首先获取我们实例组的名称。使用以下命令获取名称:
kops get instancegroups

您应该看到以下响应:

图 18.20:获取实例组列表

图 18.20:获取实例组列表

  1. 在这一步中,kops 将更新 Kubernetes 集群以匹配 kops 规范。让我们使用滚动更新将第一个主节点升级到新版本:
kops rolling-update cluster ${NAME} --instance-group master-australia-southeast1-a --yes

请注意,此命令只会在您指定--yes标志时应用更改。根据您的节点配置,此命令可能需要一些时间。请耐心等待并观察日志,看是否有任何错误。过一段时间后,您应该看到类似以下截图中的成功消息:

图 18.21:对我们的第一个实例组应用滚动更新

图 18.21:对我们的第一个实例组应用滚动更新

  1. 验证节点是否已升级到目标版本,即1.15.10,在我们的情况下:
kubectl get nodes

这应该给出类似以下的响应:

图 18.22:检查节点上的主要组件是否已升级

图 18.22:检查节点上的主要组件是否已升级

您可以看到第一个主节点的版本为1.15.10

  1. 验证新升级的节点上是否正在运行 pod:
kubectl describe node master-australia-southeast1-a-q2pw

使用您在之前步骤中升级的节点的名称。这将给出一个很长的输出。查找Non-terminated Pod部分,如下截图所示:

图 18.23:检查我们升级的节点是否正在运行 pod

图 18.23:检查我们升级的节点是否正在运行 pod

注意

重复步骤 79,对所有额外的主节点进行更新和验证,使用相应实例组的适当名称。

  1. 验证 kops 是否成功更新了主节点:
kops rolling-update cluster ${NAME}

您应该看到以下输出:

图 18.24:检查所有主节点是否已升级

图 18.24:检查所有主节点是否已升级

如前所述,这是一个干跑,输出显示哪些节点需要更新。由于它们都显示STATUSReady,我们知道它们已经更新。相比之下,您可以看到nodes(工作节点)返回NeedsUpdate,因为我们还没有更新它们。

  1. 验证所有主节点是否已升级到所需版本:
kubectl get nodes

您应该看到类似以下的响应:

图 18.25:检查所有主节点上 Kubernetes 的版本

图 18.25:检查所有主节点上 Kubernetes 的版本

如您所见,所有主节点都在运行版本1.15.10,这是期望的版本。

在这个练习中,您已经看到了如何在不影响用户的情况下升级 Kubernetes 集群的主节点。逐个节点更新将确保有足够的主服务器可用(至少需要三个才能正常工作),并且在更新期间不会影响用户和集群。

注意

当您对实例组应用滚动更新时,kops 将通过逐个将节点脱机来滚动更新实例组中的节点。除此之外,在这个练习中,我们一次只对一个实例组应用滚动更新。最终,您应该实现的是集群中只有一个节点被逐个脱机。如果您选择自动化这个过程,请记住这一点。

升级 Kubernetes 工作节点

尽管 Kubernetes 支持主节点(API 服务器)和工作节点(kubelet)在一个次要版本内的兼容性,但强烈建议您一次性升级主节点和工作节点。使用 kops,升级工作节点类似于升级主节点。由于在一个次要版本内的向后兼容性,如果工作节点与主节点的版本不匹配,工作节点可能仍然可以工作,但强烈不建议在工作节点和主节点上运行不同版本的 Kubernetes,因为这可能会为集群创建问题。

然而,如果您希望在升级过程中保持应用程序在线,以下考虑非常重要:

  • 确保您的应用程序配置为高可用。这意味着您应该为每个应用程序至少在不同节点上拥有两个 pod。如果不是这种情况,一旦您从节点中驱逐 pod,您的应用程序可能会出现停机时间。

  • 如果您运行有状态的组件,请确保这些组件的状态已备份,或者您的应用程序设计能够承受有状态组件的部分不可用。

例如,假设您正在运行一个具有单个主节点和多个读取副本的数据库。一旦运行数据库主副本的节点驱逐数据库 pod,如果您的应用程序没有正确配置来处理这种情况,它们将遭受停机时间。这与 Kubernetes 集群的升级无关,但重要的是要了解您的应用程序在升级期间的行为,并确保它们被正确配置为容错。

现在我们已经了解了确保应用程序的正常运行时间的要求,让我们看看如何在以下练习中升级工作节点。

练习 18.04:升级工作节点

在这个练习中,我们将升级 Kubernetes 集群的所有工作节点。工作节点是您的应用程序的主机。

  1. 获取工作节点的实例组列表:
kops get instancegroups

您应该看到类似以下的响应:

图 18.26:获取实例组列表

图 18.26:获取实例组列表

从这个图像中,我们可以看到我们的工作节点实例组的名称是nodes

  1. 验证节点是否准备就绪:
kubectl get nodes

您应该看到类似于这样的响应:

图 18.27:检查节点状态

图 18.27:检查节点状态

如果我们有多个实例组,我们将逐个升级每个实例组。然而,我们的任务很简单,因为我们只有一个 - 那就是nodes

  1. 运行kops rolling update命令,针对nodes实例组使用--yes标志。这将为您提供使用kops rolling-update命令将要更新的摘要:
kops rolling-update cluster ${NAME} --node-interval 3m --instance-group nodes --post-drain-delay 3m --logtostderr --v 9

请注意,我们已经在前面的命令中更改了详细日志的详细程度。

让我们分解这个命令:

  • node-interval标志设置不同节点重新启动之间的最小延迟。

  • instance-group标志指定滚动更新应该应用到哪个实例组。

  • post-drain-delay标志设置在排空节点之后重新启动之前的延迟。请记住,在本章的前面部分,排空操作将等待 pod 的正常终止。这个延迟将在此之后应用。

node-intervalpost-drain-delay标志提供了控制集群变化速率的选项。这些选项的值部分取决于您正在运行的应用程序类型。例如,如果您在节点上运行一个日志代理 DaemonSet,您可能希望给足够的时间让 pod 将内容刷新到中央日志服务器。

注意

在上一个案例中,我们在执行滚动更新时没有使用这些延迟,因为在那种情况下,实例组中每个只有一个节点。在这里,这个实例组中有三个节点。

  • logtosterr标志将所有日志输出到stderr流,以便我们可以在终端输出中看到它们。

  • v标志设置我们将看到的日志的详细程度。

此命令将显示以下输出:

图 18.28:执行滚动更新的干跑

图 18.28:执行滚动更新的干跑

  1. 现在,运行升级。使用与上一步相同的命令,并添加--yes标志。这告诉 kops 执行升级:
kops rolling-update cluster ${NAME} --node-interval 3m --instance-group nodes --post-drain-delay 3m --logtostderr --v 9 --yes

Kops 将排空一个节点,等待排空后的延迟时间,然后升级并重新启动节点。这将逐个节点重复进行。您将在终端中看到一个很长的日志,这个过程可能需要长达半个小时才能完成。在您的终端中,您应该开始看到日志,如下所示:

图 18.29:开始滚动更新过程

图 18.29:开始滚动更新过程

过一会儿,您将看到集群升级已经完成,并显示成功消息,如下所示:

图 18.30:滚动更新完成消息

图 18.30:滚动更新完成消息

细心的读者会注意到,在图 18.29中,作者的日志显示,集群升级在大约 3:05 开始,如图 18.29所示,大约在 3:25 完成。三个节点的总时间约为 20 分钟。我们在停止每个节点后设置了 3 分钟的延迟,以及在排空所有 pod 后设置了 3 分钟的延迟。因此,每个节点的等待时间加起来为 6 分钟。在实例组中有三个节点,总等待时间为 6×3=18 分钟。

  1. 验证工作节点是否已更新到目标版本-即1.15.10
kubectl get nodes 

您应该看到以下响应:

图 18.31:检查工作节点上的 Kubernetes 版本

图 18.31:检查工作节点上 Kubernetes 的版本

  1. 验证 pod 是否处于运行状态:
kubectl get pods -n upgrade-demo

您应该看到所有的 pod 的STATUS都设置为Running,就像这个截图中一样:

图 18.32:检查我们的 pod 的状态

图 18.32:检查我们的 pod 的状态

在这个练习中,您已经看到了通过 kops 升级工作节点是多么容易。但是,我们不建议一次性升级所有生产集群的工作节点,并强烈建议为工作节点创建实例组。以下是一些可用于生产级集群的策略:

  • 不要将所有的工作节点都放在一个实例组中。为不同的工作节点集创建多个实例组。默认情况下,kops 只创建一个实例组,但您可以更改此行为,为工作节点创建多个实例组。我们建议为基础设施组件(如监控和日志记录)、入口、关键应用程序、非关键应用程序和静态应用程序创建不同的工作实例组。这将帮助您首先将升级应用于集群中不太关键的部分。这种策略将有助于限制升级过程中的任何问题,并将受影响的节点与集群的其余部分隔离开来。

  • 如果您在云中运行集群,可以根据需要提供新节点。因此,创建一个姐妹实例组进行升级可能是一个好主意。这个新的实例组应该运行升级后的 Kubernetes 版本。现在,从旧的实例组中关闭和排空所有的 pod。Kubernetes 调度器将看到新节点可用,并自动将所有 pod 移动到新节点。完成后,您只需删除旧的实例组,升级就完成了。

这种策略需要一些规划,特别是如果您在集群上运行有状态的应用程序。这种策略还假定您能够根据需要提供新节点,因为创建一个姐妹实例组可能需要临时的额外硬件,这对于自建数据中心可能是一个挑战。

请注意,这些都是高级策略,超出了本书的范围。但是,您可以在kops.sigs.k8s.io/tutorial/working-with-instancegroups/找到更多信息。

现在您已经看到升级集群所需的所有步骤,您可以在以下活动中将它们整合起来。

活动 18.01:将 Kubernetes 平台从版本 1.15.7 升级到 1.15.10

在这个活动中,您将把 Kubernetes 平台从版本1.15.7升级到版本1.15.10。在这里,我们将整合本章学到的所有内容。以下准则应该帮助您完成这个活动:

注意

在这个活动中,我们展示了从 Kubernetes 版本1.15.7升级到1.15.10的过程。您可以应用相同的步骤来升级到 kops 在您执行此活动时支持的 Kubernetes 版本。

  1. 使用练习 11.01设置我们的 Kubernetes 集群,建立一个运行 Kubernetes 版本1.15.7的新集群。如果您正在使用云来启动机器,您可以在升级之前对机器进行快照(您的云供应商可能会向您收费),以便快速重新运行升级。

  2. 将 kops 升级到您想要在主节点或堡垒节点上升级的版本。对于这个活动,我们需要版本1.15

  3. 将其中一个主节点升级到 Kubernetes 版本1.15.10

  4. 验证主节点是否已恢复服务并处于Ready状态。

  5. 同样,升级所有其他主节点。

  6. 验证所有主节点是否已升级到所需版本,如下截图所示:图 18.33:主节点上的 Kubernetes 升级版本

图 18.33:主节点上的 Kubernetes 升级版本

  1. 现在,升级工作节点。

  2. 验证 Pod 是否成功运行在新升级的节点上。最后,您应该能够验证您的 Pod 正在新节点上运行,如下所示:图 18.34:运行在升级后的工作节点上的 Pod

图 18.34:运行在升级后的工作节点上的 Pod

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

总结

在本章中,您已经了解到,保持 Kubernetes 平台的最新状态对于提供安全可靠的应用程序运行基础非常重要。在这个快速发展的数字世界中,许多企业依赖于关键应用程序,并保持它们可用,即使升级底层平台也很重要。

您已经看到,如果您一开始就以高可用性配置设置了集群,那么平台的无停机升级是可能的。然而,除非您以容错的方式设计和部署应用程序,否则平台不能保证应用程序的可用性。一个因素是确保您的应用程序有多个实例运行,并且该应用程序被设计为优雅地处理这些实例的终止。

考虑到这一点,我们已经看到了升级集群的重要考虑因素,以确保平台本身不会导致应用程序的停机时间。我们分别研究了主节点和工作节点的升级过程。本章的关键要点是在不同情况下强调的原则,您可以将其应用于不同工具管理的不同类型的 Kubernetes 集群。

正如本章开头提到的,保持平台的最新状态对于跟上 DevOps 的最新发展并使您的应用开发团队能够继续向最终客户提供新功能是很重要的。通过本章获得的技能,您应该能够在升级平台时不会对客户造成中断。

在下一章中,我们将讨论如何使用自定义资源扩展您的 Kubernetes 平台。自定义资源允许您为自己的项目提供 Kubernetes 本机 API 体验。

第十九章: Kubernetes 中的自定义资源定义

概述

在本章中,我们将展示如何使用自定义资源定义CRDs)来扩展 Kubernetes 并向您的 Kubernetes 集群添加新功能。您还将学习如何定义、配置和实现完整的 CRD。我们还将描述各种示例场景,其中 CRDs 可以非常有帮助。在本章结束时,您将能够定义和配置 CRD 和自定义资源CR)。您还将学习如何部署一个基本的自定义控制器来实现集群中 CR 所需的功能。

介绍

在之前的章节中,我们学习了不同的 Kubernetes 对象,比如 Pods、Deployments 和 ConfigMaps。这些对象是由 Kubernetes API 定义和管理的(也就是说,对于这些对象,API 服务器管理它们的创建和销毁,以及其他操作)。然而,您可能希望扩展 Kubernetes 提供的功能,以提供一个标准 Kubernetes 中没有的功能,并且不能通过 Kubernetes 提供的内置对象来启用。

为了在 Kubernetes 之上构建这些功能,我们使用自定义资源CRs)。自定义资源定义CRDs)允许我们通过这种方式向 Kubernetes 服务器添加自定义对象,并像任何其他本机 Kubernetes 对象一样使用这些 CRs。CRD 帮助我们将我们的自定义对象引入 Kubernetes 系统。一旦我们的 CRD 被创建,它就可以像 Kubernetes 服务器中的任何其他对象一样使用。不仅如此,我们还可以使用 Kubernetes API、基于角色的访问控制RBAC)策略和其他 Kubernetes 功能来管理我们引入的 CRs。

当您定义一个 CRD 时,它会存储在 Kubernetes 配置数据库(etcd)中。将 CRD 视为自定义对象结构的定义。一旦定义了 CRD,Kubernetes 就会创建符合 CRD 定义的对象。我们称这些对象为 CRs。如果我们将其比作编程语言的类比,CRD 就是类,CR 就是类的实例。简而言之,CRD 定义了自定义对象的模式,CR 定义了您希望实现的对象的期望状态。

CRs 是通过自定义控制器实现的。我们将在本章的第一个主题中更详细地了解自定义控制器。

什么是自定义控制器?

CRD 和 CR 可帮助您定义 CR 的期望状态。需要一个组件来确保 Kubernetes 系统的状态与 CR 定义的期望状态相匹配。正如您在前几章中所看到的,执行此操作的 Kubernetes 组件称为控制器。Kubernetes 提供了许多这些控制器,它们的工作是确保期望状态(例如,在部署中定义的 Pod 副本数)等于部署对象中定义的值。总之,控制器是一个通过 Kubernetes API 服务器监视资源状态并尝试将当前状态与期望状态匹配的组件。

标准 Kubernetes 设置中包含的内置控制器旨在与内置对象(如部署)一起使用。对于我们的 CRD 及其 CR,我们需要编写自己的自定义控制器。

CRD、CR 和控制器之间的关系

CRD 提供了定义 CR 的方法,自定义控制器提供了对 CR 对象进行操作的逻辑。以下图表总结了 CRD、CR 和控制器:

图 19.1:CRD、CR 和控制器如何相互关联

图 19.1:CRD、CR 和控制器如何相互关联

如前图所示,我们有一个 CRD、一个自定义控制器和根据 CRD 定义期望状态的 CR 对象。这里有三件事需要注意:

  • CRD 是定义对象外观的模式。每个资源都有一个定义的模式,告诉 Kubernetes 引擎在定义中期望什么。诸如PodSpec之类的核心对象具有内置到 Kubernetes 项目中的模式。

注意

您可以在此链接找到 PodSpec 的源代码:github.com/kubernetes/kubernetes/blob/master/pkg/apis/core/types.go#L2627

  • 基于模式(CRD)创建的 CR 对象定义了资源的期望状态。

  • 自定义控制器是提供功能的应用程序,将当前状态带到期望的状态。

请记住,CRD 是 Kubernetes 允许我们声明性地定义 CR 的模式或定义的一种方式。一旦我们的 CRD(模式)在 Kubernetes 服务器上注册,CR(对象)将根据我们的 CRD 进行定义。

标准 Kubernetes API 资源

让我们列出 Kubernetes 集群中所有可用的资源和 API。请记住,我们使用的所有内容都被定义为 API 资源,而 API 是我们与 Kubernetes 服务器通信以处理该资源的网关。

使用以下命令获取当前 Kubernetes 资源的列表:

kubectl api-resources

您应该看到以下响应:

图 19.2:标准 Kubernetes API 资源

图 19.2:标准 Kubernetes API 资源

在上面的截图中,您可以看到 Kubernetes 中定义的资源具有APIGroup属性,该属性定义了负责管理此资源的内部 API。Kind列出了资源的名称。正如我们在本主题中之前所看到的,对于标准的 Kubernetes 对象,比如 Pods,Pod 对象的模式或定义内置在 Kubernetes 中。当您定义一个 Pod 规范来运行一个 Pod 时,这可以说类似于 CR。

对于每个资源,都有一些可以针对该资源采取行动的代码。这被定义为一组 API(APIGroup)。请注意,可以存在多个 API 组;例如,一个稳定版本和一个实验版本。发出以下命令以查看您的 Kubernetes 集群中有哪些 API 版本可用:

kubectl api-versions

您应该看到以下响应:

图 19.3:各种 API 组及其版本

图 19.3:各种 API 组及其版本

在上面的截图中,请注意apps API 组有多个可用版本。每个版本可能具有其他组中不可用的不同功能集。

为什么我们需要自定义资源?

如前所述,CR 提供了一种方式,通过这种方式我们可以扩展 Kubernetes 平台,以提供特定于某些用例的功能。以下是一些您将遇到 CR 使用的用例。

示例用例 1

考虑这样一个用例,您希望自动将业务应用程序或数据库自动部署到 Kubernetes 集群上。抽象掉技术细节,比如配置和部署应用程序,允许团队在不需要深入了解 Kubernetes 的情况下管理它们。例如,您可以创建一个 CR 来抽象数据库的创建。因此,用户只需在 CRD 中定义数据库的名称和大小,控制器就会提供其余部分来创建数据库 Pod。

示例用例 2

考虑这样一个情景,您有自助团队。您的 Kubernetes 平台被多个团队使用,您希望团队自行定义工作负载所需的总 CPU 和内存,以及 Pod 的默认限制。您可以创建一个 CRD,团队可以使用命名空间名称和其他参数创建 CR。您的自定义控制器将创建他们需要的资源,并为每个团队关联正确的 RBAC 策略。您还可以添加其他功能,例如限制团队只能使用三个环境。控制器还可以生成审计事件并记录所有活动。

示例用例 3

假设您是开发 Kubernetes 集群的管理员,开发人员会在这里测试他们的应用程序。您面临的问题是开发人员留下了正在运行的 Pod,并已转移到新项目。这可能会对您的集群造成资源问题。

在本章中,我们将围绕这种情景构建一个 CRD 和一个自定义控制器。我们可以实现的解决方案是在创建后的一定时间后删除 Pod。让我们称这个时间为podLiveForThisMinutes。另一个要求是以可配置的方式为每个命名空间定义podLiveForThisMinutes,因为不同的团队可能有不同的优先级和要求。

我们可以为每个命名空间定义一个时间限制,这将为在不同命名空间应用控制提供灵活性。为了实现本示例用例中定义的要求,我们将定义一个 CRD,允许两个字段 - 命名空间名称和允许 Pod 运行的时间量(podLiveForThisMinutes)。在本章的其余部分,我们将构建一个 CRD 和一个控制器,使我们能够实现这里提到的功能。

注意

有其他(更好的)方法来实现前面的场景。在现实世界中,如果 Pod 是使用Deployment资源创建的,Kubernetes 的Deployment对象将重新创建 Pod。我们选择了这个场景,以使示例简单易实现。

我们的自定义资源是如何定义的

为了解决前一节中示例用例 3的问题,我们决定我们的 CRD 将定义两个字段,如前面的示例中所述。为了实现这一点,我们的 CR 对象将如下所示。

apiVersion: "controllers.kube.book.au/v1"
kind: PodLifecycleConfig
metadata:
  name: demo-pod-lifecycle
spec:
  namespaceName: crddemo
  podLiveForThisMinutes: 1

上述规范定义了我们的目标对象。正如你所看到的,它看起来就像普通的 Kubernetes 对象,但规范(spec部分)根据我们的需求进行了定义。让我们深入了解一下细节。

apiVersion

这是 Kubernetes 用来对对象进行分组的字段。请注意,我们将版本(v1)作为组键的一部分。这种分组技术帮助我们保持对象的多个版本。考虑是否要添加新属性而不影响现有用户。你可以只创建一个带有v2的新组,同时存在v1v2两个版本的对象定义。因为它们是分开的,所以不同组的不同版本可以以不同的速度发展。

这种方法还有助于我们测试新功能。假设我们想要向同一对象添加一个新字段。然后,我们可以只更改 API 版本并添加新字段。因此,我们可以将稳定版本与新的实验版本分开。

kind

这个字段提到了由apiVersion定义的组中的特定类型对象。把kind想象成 CR 对象的名称,比如Pod

注意

不要将其与使用此规范创建的对象的名称混淆,该对象在metadata部分中定义。

通过这个,我们可以在一个 API 组下拥有多个对象。想象一下,你要创建一个需要创建多种不同类型对象的功能。你可以在同一个 API 组下使用Kind字段创建多个对象。

spec

这个字段定义了定义对象规范所需的信息。规范包含定义资源期望状态的信息。描述资源特性的所有字段都放在spec部分。对于我们的用例,spec部分包含我们 CR 所需的两个字段——podLiveForThisMinutesnamespaceName

namespaceName 和 podLiveForThisMinutes

这些是我们想要定义的自定义字段。namespaceName将包含目标命名空间的名称,而podLiveForThisMinutes将包含我们希望 Pod 活动的时间(以分钟为单位)。

CRD 的定义

在前面的部分中,我们展示了 CR 的不同组件。然而,在我们定义 CR 之前,我们需要定义一个模式,它规定了 CR 的定义方式。在接下来的练习中,您将为我们的自定义资源是如何定义的部分中提到的资源定义模式或 CRD。

考虑这个示例 CRD,在接下来的练习中我们将使用它。让我们通过观察以下定义来理解 CRD 的重要部分:

pod-normaliser-crd.yaml

1  apiVersion: apiextensions.k8s.io/v1beta1
2  kind: CustomResourceDefinition
3  metadata:
4    name: podlifecycleconfigs.controllers.kube.book.au
5  spec:
6    group: controllers.kube.book.au
7    version: v1
8    scope: Namespaced
9    names:
10     kind: PodLifecycleConfig
11     plural: podlifecycleconfigs
12     singular: podlifecycleconfig
13  #1.15 preserveUnknownFields: false
14   validation:
15     openAPIV3Schema:
16       type: object
17       properties:
18         spec:
19           type: object
20           properties:
21             namespaceName:
22               type: string
23             podLiveForThisMinutes:
24               type: integer

现在,让我们看看这个 CRD 的各个组件:

  • apiVersionkind:这些是 CRD 本身的 API 和资源,由 Kubernetes 提供给 CRD 定义。

  • groupversion:将 API 组想象成一组逻辑相关的对象。这两个字段定义了我们 CR 的 API 组和版本,然后将被翻译成我们在前面部分中定义的 CR 的apiVersion字段。

  • kind:这个字段定义了我们 CR 的kind,在我们的自定义资源是如何定义的部分中已经定义过。

  • metadata/name:名称必须与spec字段匹配,格式是两个字段的组合,即<plural>.<group>

  • scope:这个字段定义了 CR 是命名空间范围还是集群范围。默认情况下,CR 是集群范围的。我们在这里定义它为命名空间范围。

  • plurals:这些是用于 Kubernetes API 服务器 URL 中的复数名称。

  • openAPIV3Schema:这是基于 OpenAPI v3 标准定义的模式。它指的是我们 CR 的实际字段/模式。模式定义了我们 CR 中可用的字段、字段的名称和它们的数据类型。它基本上定义了我们 CR 中spec字段的结构。我们在 CR 中使用了namespaceNamepodLiveForMinutes字段。你可以在以下练习的步骤 2中看到这一点。

有趣的是,API 服务器中服务 CR 的组件被称为apiextensions-apiserver。当 kubectl 请求到达 API 服务器时,它首先检查资源是否是标准的 Kubernetes 资源,比如 Pod 或 Deployment。如果资源不是标准资源,那么就会调用apiextensions-apiserver

练习 19.01:定义 CRD

在这个练习中,我们将定义一个 CRD,在下一个练习中,我们将为定义的 CRD 创建一个 CR。CRD 的定义存储在 Kubernetes etcd 服务器中。请记住,CRD 和 CR 只是定义,直到您部署与您的 CR 相关联的控制器,CRD/CR 才会有功能附加。通过定义 CRD,您正在向 Kubernetes 集群注册一个新类型的对象。在定义 CRD 之后,它将通过正常的 Kubernetes API 可访问,并且您可以通过 Kubectl 访问它:

  1. 创建一个名为crddemo的新命名空间:
kubectl create ns crddemo

这应该得到以下响应:

namespace/crddemo created
  1. 现在,我们需要定义一个 CRD。使用以下内容创建一个名为pod-normaliser-crd.yaml的文件:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: podlifecycleconfigs.controllers.kube.book.au
spec:
  group: controllers.kube.book.au
  version: v1
  scope: Namespaced
  names:
    kind: PodLifecycleConfig
    plural: podlifecycleconfigs
    singular: podlifecycleconfig
  #1.15 preserveUnknownFields: false
  validation:
    openAPIV3Schema:
      type: object
      properties:
        spec:
          type: object
          properties:
            namespaceName:
              type: string
            podLiveForThisMinutes:
              type: integer
  1. 使用上一步的定义,使用以下命令创建 CRD:
kubectl create -f pod-normaliser-crd.yaml -n crddemo

您应该看到以下响应:

图 19.4:创建我们的 CRD

图 19.4:创建我们的 CRD

  1. 使用以下命令验证 CR 是否已在 Kubernetes 中注册:
kubectl api-resources | grep podlifecycleconfig

您应该看到以下资源列表:

图 19.5:验证 CR 是否已在 Kubernetes 中注册

图 19.5:验证 CR 是否已在 Kubernetes 中注册

  1. 使用以下命令验证 Kubernetes API 服务器中是否可用 API:
kubectl api-versions | grep controller

您应该看到以下响应:

controllers.kube.book.au/v1

在这个练习中,我们已经定义了一个 CRD,现在,Kubernetes 将能够知道我们的 CR 应该是什么样子的。

现在,在下一个练习中,让我们根据我们定义的 CRD 创建一个资源对象。这个练习将是上一个练习的延伸。但是,我们将它们分开,因为 CRD 对象可以独立存在;您不必将 CR 与 CRD 配对。可能的情况是,CRD 由某些第三方软件供应商提供,并且您只需要创建 CR。例如,供应商提供的数据库控制器可能已经有了 CRD 和控制器。要使用功能,您只需要定义 CR。

让我们继续在下一个练习中将我们的 CRD 制作成一个 CR。

练习 19.02:使用 CRD 定义 CR

在这个练习中,我们将根据上一个练习中定义的 CRD 创建一个 CR。CR 将作为一个普通的 Kubernetes 对象存储在 etcd 数据存储中,并由 Kubernetes API 服务器提供服务-也就是说,当您尝试通过 Kubectl 访问它时,它将由 Kubernetes API 服务器处理:

注意

只有在成功完成本章的上一个练习后,您才能执行此练习。

  1. 首先,确保podlifecycleconfigs类型没有 CR。使用以下命令进行检查:
kubectl get podlifecycleconfigs -n crddemo

如果没有 CR,您应该会看到以下响应:

No resources found.

如果已定义资源,可以使用以下命令删除它:

kubectl delete podlifecycleconfig <RESOURCE_NAME> -n crddemo
  1. 现在,我们必须创建一个 CR。使用以下内容创建名为pod-normaliser.yaml的文件:
apiVersion: "controllers.kube.book.au/v1"
kind: PodLifecycleConfig
metadata:
  name: demo-pod-lifecycle
  # namespace: "crddemo"
spec:
  namespaceName: crddemo
  podLiveForThisMinutes: 1
  1. 使用以下命令创建来自上一步创建的文件的资源:
kubectl create -f pod-normaliser.yaml -n crddemo

您应该会看到以下响应:

图 19.6:创建我们的 CR

图 19.6:创建我们的 CR

  1. 使用以下命令验证 Kubernetes 是否已注册它:
kubectl get podlifecycleconfigs -n crddemo

您应该会看到以下响应:

NAME                  AGE
demo-pod-lifecycle    48s

请注意,我们现在正在使用普通的 kubectl 命令。这是扩展 Kubernetes 平台的一种非常棒的方式。

我们已经定义了自己的 CRD,并已创建了一个 CR。下一步是为我们的 CR 添加所需的功能。

编写自定义控制器

现在我们在集群中有一个 CR,我们将继续编写一些代码来执行它,以实现我们在为什么需要自定义资源部分中设定的场景的目的。

注意

我们不会教授编写控制器 Go 代码的实际编程,因为这超出了本书的范围。但是,我们会为示例用例 3提供所需的编程逻辑。

假设我们的自定义控制器代码正在作为一个 Pod 运行。为了响应 CR,它需要做些什么?

  1. 首先,控制器必须知道在集群中定义/删除了新的 CR,以获取所需的状态。

  2. 其次,代码需要一种与 Kubernetes API 服务器交互的方式,以请求当前状态,然后请求所需的状态。在我们的情况下,我们的控制器必须知道命名空间中所有 Pod 的时间以及 Pod 的创建时间。然后,代码可以要求 Kubernetes 根据 CRD 删除 Pods,如果它们的允许时间已到。请参考示例用例 3部分,以刷新您对我们的控制器将要执行的操作的记忆。

我们的代码逻辑可以通过以下图表进行可视化:

图 19.7:描述自定义控制器逻辑的流程图

图 19.7:描述自定义控制器逻辑的流程图

如果我们要将逻辑描述为简单的伪代码,那么它将如下所示:

  1. 从 Kubernetes API 服务器获取为我们的自定义 CRD 创建的所有新 CR。

  2. 注册回调以便在添加或删除 CR 时触发。每当在我们的 Kubernetes 集群中添加或删除新的 CR 时,都会触发这些回调。

  3. 如果将 CR 添加到集群中,回调将创建一个子例程,该子例程将持续获取由 CR 定义的命名空间中的 Pod 列表。如果 Pod 已运行时间超过指定时间,它将被终止。否则,它将休眠几秒钟。

  4. 如果删除 CR,回调将停止子例程。

自定义控制器的组件

如前所述,详细解释自定义控制器的构建方式超出了本书的范围,我们已经提供了一个完全可用的自定义控制器,以满足示例用例 3的需求。我们的重点是确保您可以构建和执行控制器以了解其行为,并且您对所有涉及的组件都感到满意。

自定义控制器是针对 CR 提供功能的组件。为了提供这一点,自定义控制器需要了解 CR 的用途及其不同的参数,或者结构模式。为了使我们的控制器了解模式,我们通过代码文件向控制器提供有关我们模式的详细信息。

以下是我们提供的控制器代码的摘录:

types.go

12 type PodLifecycleConfig struct {
13
14     // TypeMeta is the metadata for the resource, like kind and           apiversion
15     meta_v1.TypeMeta `json:",inline"`
16
17     // ObjectMeta contains the metadata for the particular           object like labels
18     meta_v1.ObjectMeta `json:"metadata,omitempty"`
19
20     Spec PodLifecycleConfigSpec `json:"spec"`
21 }
22
23 type PodLifecycleConfigSpec struct{
24     NamespaceName   string `json:"namespaceName"`
25     PodLiveForMinutes int `json:"podLiveForThisMinutes"`
26 }
...
32 type PodLifecycleConfigList struct {
33     meta_v1.TypeMeta `json:",inline"`
34     meta_v1.ListMeta `json:"metadata"`
35
36     Items []PodLifecycleConfig `json:"items"`
37 }

您可以在此链接找到完整的代码:packt.live/3jXky9G

正如您所看到的,我们已经根据“自定义资源的定义方式”部分提供的 CR 示例定义了PodLifecycleConfig结构。这里重复列出以便更容易参考:

apiVersion: "controllers.kube.book.au/v1"
kind: PodLifecycleConfig
metadata:
  name: demo-pod-lifecycle
  # namespace: "crddemo"
spec:
  namespaceName: crddemo
  podLiveForThisMinutes: 1

请注意,在types.go中,我们已经定义了可以保存此示例规范的完整定义的对象。还要注意在types.go中,namespaceName被定义为stringpodLiveForThisMinuets被定义为int。这是因为我们在 CR 中使用字符串和整数来表示这些字段,正如您所看到的。

控制器的下一个重要功能是监听与 CR 相关的来自 Kubernetes 系统的事件。我们使用Kubernetes Go客户端库连接到 Kubernetes API 服务器。该库使连接到 Kubernetes API 服务器(例如,用于身份验证)更容易,并具有预定义的请求和响应类型,以与 Kubernetes API 服务器进行通信。

注意

您可以在此链接找到有关 Kubernetes Go 客户端库的更多详细信息:github.com/kubernetes/client-go

但是,您可以自由地使用任何其他库或任何其他编程语言通过 HTTPS 与 API 服务器通信。

您可以通过查看此链接上的代码来了解我们是如何实现的:packt.live/3ieFtVm。首先,我们需要连接到 Kubernetes 集群。这段代码正在集群中的一个 Pod 中运行,并且需要连接到 Kubernetes API 服务器。我们需要给予我们的 Pod 足够的权限来连接到主服务器,这将在本章后面的活动中介绍。我们将使用 RBAC 策略来实现这一点。请参考第十三章Kubernetes 中的运行时和网络安全,以了解 Kubernetes 如何实现 RBAC 功能的复习。

一旦我们连接上了,我们就使用SharedInformerFactory对象来监听控制器的 Kubernetes 事件。将事件视为 Kubernetes 在创建或删除新 CR 时通知我们的一种方式。SharedInformerFactory是 Kubernetes Go 客户端库提供的一种方式,用于监听 Kubernetes API 服务器生成的事件。对SharedInformerFactory的详细解释超出了本书的范围。

以下代码片段是我们的 Go 代码中创建SharedInformerFactory的摘录:

main.go

40 // create the kubernetes client configuration
41     config, err := clientcmd.BuildConfigFromFlags("", "")
42     if err != nil {
43         log.Fatal(err)
44     }
45
46     // create the kubernetes client
47     podlifecyelconfgiclient, err := clientset.NewForConfig(config)
48
49
50     // create the shared informer factory and use the client           to connect to kubernetes
51     podlifecycleconfigfactory :=          informers.NewSharedInformerFactoryWithOptions            (podlifecyelconfgiclient, Second*30,
52     informers.WithNamespace(os.Getenv(NAMESPACE_TO_WATCH)))

您可以在此链接找到完整的代码:packt.live/3lXe3FM

一旦我们连接到 Kubernetes API 服务器,我们需要注册以便在我们的 CR 被创建或删除时得到通知。以下代码执行了这个动作:

main.go

62 // fetch the informer for the PodLifecycleConfig
63 podlifecycleconfiginformer :=      podlifecycleconfigfactory.Controllers().V1().     PodLifecycleConfigs().Informer()
64
65 // register with the informer for the events
66 podlifecycleconfiginformer.AddEventHandler(
...
69 //define what to do in case if a new custom resource is created
70         AddFunc: func(obj interface{}) {
...
83 // start the subroutine to check and kill the pods for this namespace
84             go checkAndRemovePodsPeriodically(signal, podclientset, x)
85         },
86
87 //define what to do in case if a  custom resource is removed
88         DeleteFunc: func(obj interface{}) {

您可以在此链接找到完整的代码:packt.live/2ZjtQoy

请注意,上述代码是从完整代码中提取的,这里的片段经过了轻微修改,以便在本书中更好地呈现。此代码正在向 Kubernetes 服务器注册回调。请注意,我们已经注册了 AddFuncDeleteFunc。一旦 CR 被创建或删除,这些函数将被调用,我们可以针对此编写自定义逻辑。您可以看到,对于 AddFunc,正在调用 Go 子例程。对于每个新的 CR,我们都有一个单独的子例程来继续监视在命名空间中创建的 Pods。另外,请注意,AddFunc 将在日志中打印出 A Custom Resource has been Added。您可能还注意到,在 DeleteFunc 中,我们已关闭了 signal 通道,这将标记 Go 子例程停止自身。

活动 19.01:CRD 和自定义控制器的实际应用

在这个活动中,我们将构建和部署自定义控制器、CR 和 CRD。请注意,构建自定义控制器所需的编码超出了本书的范围,并且代码库中提供了现成的代码,以便部署工作控制器。

我们将创建一个新的 CRD,可以接受两个字段 - podLiveForThisMinutes 字段,定义了 Pod 在被杀死之前允许运行的时间(以分钟为单位),以及 namespaceName 字段,定义了这些规则将应用于哪个命名空间。

我们将根据 CRD 创建一个新的 CR。此外,我们将创建一个新的 Kubernetes 角色,允许从 Kubernetes API 服务器查询此新的 CRD。然后,我们将向您展示如何将新创建的角色与名为 default 的 ServiceAccount 关联起来,这是在命名空间 default 中运行 Pod 时默认使用的 ServiceAccount。

通常,我们构建一个自定义控制器,提供针对我们创建的 CRD 的逻辑。我们将使用打包为容器的代码,并将其部署为 Pod。控制器将作为普通 Pod 部署。

在活动结束时,为了测试我们的控制器,您将创建一个简单的 Pod,并验证我们的自定义控制器是否能够删除该 Pod。

活动指南:

  1. 删除现有的 crddemo 命名空间,并使用相同的名称创建一个新的命名空间。

  2. 使用以下命令获取用于创建控制器的代码和 Dockerfile

git clone  https://github.com/PacktWorkshops/Kubernetes-Workshop.git 
cd Chapter19/Activity19.01/controller
  1. 创建一个具有以下字段的 CRD。

元数据应包含以下内容:

name: podlifecycleconfigs.controllers.kube.book.au

OpenAPIV3Schema 部分应包含以下 properties 设置:

openAPIV3Schema:
  type: object
  properties:
    spec:
      type: object
      properties:
        namespaceName:
          type: string
        podLiveForThisMinutes:
          type: integer
  1. 创建一个 CR,允许crddemo命名空间中的 Pod 存活 1 分钟。

  2. 创建一个角色,为指定的 API 资源允许以下权限:

rules:
- apiGroups: ["controllers.kube.book.au"]
  resources: ["podlifecycleconfigs"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""] 
  resources: ["pods"]
  verbs: ["get", "watch", "list", "delete"]
  1. 使用 RoleBinding 对象,将此新角色与crddemo命名空间中的default ServiceAccount 关联起来。

  2. 使用步骤 2中提供的Dockerfile构建和部署控制器 Pod。

  3. crddemo命名空间中使用k8s.gcr.io/busybox镜像创建一个长时间运行的 Pod。

观察上一步创建的 Pod,并观察我们的控制器是否正在终止它。预期结果是 Pod 应该被创建,然后大约一分钟后应该自动终止,如下面的屏幕截图所示:

图 19.8:活动 19.01 的预期输出

图 19.8:活动 19.01 的预期输出

注意

此活动的解决方案可以在以下地址找到:packt.live/304PEoD

向我们的自定义资源添加数据

在上一个活动中,您创建了 CRD 和 CR。我们之前提到过,一旦定义了我们的 CR,就可以使用标准 kubectl 命令查询它们。例如,如果您想查看已定义的PodLifecycleConfig类型的 CR 数量,可以使用以下命令:

kubectl get PodLifecycleConfig -n crddemo

您将看到以下响应

NAME                AGE
demo-pod-lifecycle  8h

请注意,它只显示对象的名称和年龄。但是,如果您为本机 Kubernetes 对象发出命令,您将看到更多列。让我们尝试部署:

kubectl get deployment -n crddemo

您应该看到类似于这样的响应:

NAME          READY    UP-TO-DATE   AVAILABLE   AGE
crd-server    1/1      1            1           166m

请注意 Kubernetes 添加的附加列,这些列提供了有关对象的更多信息。

如果我们想要添加更多列,以便前面的命令输出显示我们的 CR 的更多细节怎么办?您很幸运,因为 Kubernetes 提供了一种方法来为 CR 添加附加信息列。这对于显示每种自定义对象的关键值非常有用。这可以通过在 CRD 中定义的附加数据来实现。让我们看看我们如何在以下练习中做到这一点。

练习 19.03:向 CR 列表命令添加自定义信息

在这个练习中,您将学习如何通过kubectl get命令添加自定义信息到 CR 列表中:

注意

只有在成功完成活动 19.01CRD 和自定义控制器实战之后,您才能执行此练习。

  1. 让我们定义另一个带有附加列的 CRD。创建一个名为pod-normaliser-crd-adv.yaml的文件,内容如下:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: podlifecycleconfigsadv.controllers.kube.book.au
spec:
  group: controllers.kube.book.au
  version: v1
  scope: Namespaced
  names:
    kind: PodLifecycleConfigAdv
    plural: podlifecycleconfigsadv
    singular: podlifecycleconfigadv
  #1.15 preserveUnknownFields: false
  validation:
    openAPIV3Schema:
      type: object
      properties:
        spec:
          type: object
          properties:
            namespaceName:
              type: string
            podLiveForThisMinutes:
              type: integer    
  additionalPrinterColumns:
  - name: NamespaceName
    type: string
    description: The name of the namespace this CRD is applied       to.
    JSONPath: .spec.namespaceName
  - name: PodLiveForMinutes
    type: integer
    description: Allowed number of minutes for the Pod to       survive
    JSONPath: .spec.podLiveForThisMinutes
  - name: Age
    type: date
    JSONPath: .metadata.creationTimestamp

请注意我们有一个名为additionalPrinterColumns的新部分。顾名思义,这定义了资源的附加信息。additionalPrinterColumns部分的两个重要字段如下:

  • name:这定义了要打印的列的名称。

  • JSONPath:这定义了字段的位置。通过这个路径,从资源中获取信息,并在相应的列中显示。

  1. 现在,让我们使用以下命令创建这个新的 CRD:
kubectl create -f pod-normaliser-crd-adv.yaml -n crddemo

您将看到以下输出:

图 19.9:创建我们修改后的 CRD

图 19.9:创建我们修改后的 CRD

  1. 创建了 CRD 后,让我们创建 CRD 的对象。创建一个名为pod-normaliser-adv.yaml的文件,内容如下:
apiVersion: "controllers.kube.book.au/v1"
kind: PodLifecycleConfigAdv
metadata:
  name: demo-pod-lifecycle-adv
  # namespace: "crddemo"
spec:
  namespaceName: crddemo
  podLiveForThisMinutes: 20

现在,spec部分中的字段应该在kubectl get命令获取的列表中可见,类似于原生 Kubernetes 对象。

  1. 使用以下命令创建前一步中定义的 CR:
kubectl create -f pod-normaliser-adv.yaml -n crddemo

这应该得到以下响应:

图 19.10:创建我们的 CR

图 19.10:创建我们的 CR

  1. 现在,让我们发出kubectl get命令,看看是否显示了附加字段:
kubectl get PodLifecycleConfigAdv -n crddemo

您应该看到我们的对象显示了以下信息:

NAME                    NAMESPACENAME  PODLIVEFORMINUTES  AGE
demo-pod-lifecycle-adv  crddemo        20                 27m

您可以看到附加字段已显示,现在我们对 CR 有了更多信息。

在这个练习中,您已经看到我们可以在通过 Kubernetes API 服务器查询 CR 时关联附加数据。我们可以定义字段名称和字段数据的路径。当您拥有许多相同类型的资源时,这种资源特定信息变得重要,对于运维团队来说,更好地理解定义的资源也很有用。

摘要

在本章中,您了解了自定义控制器。根据 Kubernetes 词汇表,控制器实现控制循环,通过 API 服务器监视集群的状态,并进行更改,以尝试将当前状态移向期望的状态。

控制器不仅可以监视和管理用户定义的 CR,还可以对部署或服务等资源进行操作,这些资源通常是 Kubernetes 控制器管理器的一部分。控制器提供了一种编写自己的代码以满足业务需求的方式。

CRD 是 Kubernetes 系统中用于扩展其功能的中心机制。CRD 提供了一种原生方式来实现符合您业务需求的 Kubernetes API 服务器的自定义逻辑。

您已经了解了 CRD 和控制器如何帮助为 Kubernetes 平台提供扩展机制。您还看到了如何配置和部署自定义控制器到 Kubernetes 平台的过程。

当我们接近旅程的尽头时,让我们回顾一下我们取得了什么成就。我们从 Kubernetes 的基本概念开始,了解了其架构以及如何与其交互。我们介绍了 Kubectl,这是与 Kubernetes 交互的命令行工具,然后,我们看到了 Kubernetes API 服务器的工作原理以及如何使用curl命令与其通信。

前两章建立了容器化和 Kubernetes 的基础知识。此后,我们学习了 kubectl 的基础知识- Kubernetes 命令中心。在第四章,如何与 Kubernetes(API 服务器)通信中,我们看到了 kubectl 和其他 HTTP 客户端如何与 Kubernetes API 服务器通信。我们通过在章节末创建一个部署来巩固我们的学习。

第五章Pods第十章ConfigMaps 和 Secrets,我们深入探讨了理解平台并开始设计在 Kubernetes 上运行应用程序所必不可少的概念。诸如 Pods、Deployments、Services 和 PersistentVolumes 等概念使我们能够利用该平台编写容错应用程序。

在接下来的一系列章节中,从第十一章构建您自己的 HA 集群第十五章Kubernetes 中的监控和自动缩放,我们了解了如何在云平台上安装和运行 Kubernetes。这涵盖了在高可用性(HA)配置中安装 Kubernetes 平台以及如何在平台中管理网络安全。在本书的这一部分,您还了解了有状态组件以及应用程序如何使用平台的这些特性。最后,本节讨论了监视您的集群并设置自动缩放。

最后,在这最后一部分,从第十六章开始,Kubernetes 准入控制器,我们开始学习一些高级概念,比如如何使用准入控制器应用自定义策略。你也已经了解了 Kubernetes 调度器,这是一个决定你的应用程序将在集群中的哪个位置运行的组件。你学会了如何改变调度器的默认行为。你也看到了 CRD 提供了一种扩展 Kubernetes 的方式,这不仅可以用来构建自定义增强功能,也可以作为第三方提供商为 Kubernetes 添加功能的一种方式。

这本书可以作为一个很好的起点,帮助你开始学习 Kubernetes。现在你已经具备了在 Kubernetes 之上设计和构建系统的能力,可以为你的组织带来云原生的体验。虽然这本书到此结束了,但这只是作为一个 Kubernetes 专业人员的旅程的开始。

posted @ 2024-05-20 12:03  绝不原创的飞龙  阅读(21)  评论(0编辑  收藏  举报