认证 Kubernetes 应用开发者(CKAD)学习指南(三)

发布于 作者: Benjamin Muschko

第三章:与 Kubernetes 交互

作为一名应用开发人员,你需要与 Kubernetes 集群进行交互,以管理运行你应用程序的对象。对集群的每一次调用都会被 API Server 组件接收并处理。调用 API Server 有多种方式,例如使用基于 Web 的仪表盘、像 kubectl 这样的命令行工具,或者直接向 RESTful API 端点发送 HTTPS 请求。

考试并不会考察使用可视化用户界面与 Kubernetes 集群交互的方式。你在解决考试问题时唯一的客户端工具是 kubectl。本章将介绍 Kubernetes API 的原语和对象,以及使用 kubectl 管理对象的不同方式。

API 原语与对象

Kubernetes 原语是 Kubernetes 架构中用于创建和运行应用程序的基本构建块。即使你是 Kubernetes 的初学者,可能也听说过 PodDeploymentService 这些术语,它们都是 Kubernetes 原语。除此之外,还有许多原语在 Kubernetes 架构中承担着特定的职责。

为了类比,可以回想面向对象编程的概念。在面向对象编程语言中, 定义了现实世界功能的蓝图,包括其属性和行为。Kubernetes 原语就相当于一个类。而在面向对象编程中,类的实例是一个对象,它管理自身状态,并能够与系统的其他部分通信。每当你创建一个 Kubernetes 对象时,就会生成这样一个实例。

例如,Kubernetes 中的 Pod 是一个类,它可以有多个实例,并且每个实例都有自己的身份。每个 Kubernetes 对象都有一个由系统生成的唯一标识符(也称为 UID),用于在系统中清晰地区分不同实体。

稍后我们将查看 Kubernetes 对象的属性。图 3-1 展示了 Kubernetes 原语与对象之间的关系。

kcad-3-1

图 3-1:Kubernetes 对象标识

每个 Kubernetes 原语都遵循一种通用结构,你可以通过查看对象的清单文件(manifest)来观察这一点,如 图 3-2 所示。Kubernetes 清单主要使用的标记语言是 YAML

kcad-3-2

图 3-2:Kubernetes 对象结构

让我们来看一下每个部分及其在 Kubernetes 系统中的作用:

API version

Kubernetes API 版本定义了某个原语的结构,并用于验证数据的正确性。API 版本的作用类似于 XML 文档中的 XML Schema,或 JSON 文档中的 JSON Schema。该版本通常会经历一个成熟过程,例如从 alphabeta 再到最终版本。有时你会看到用斜杠分隔的不同前缀(如 apps)。你可以通过运行命令 kubectl api-versions 来列出与你的集群版本兼容的 API 版本。

Kind

kind 定义了原语的类型,例如 Pod 或 Service。它最终回答了这样一个问题:“我们正在处理哪一种资源?”

Metadata

metadata 描述了对象的高层信息,例如对象的名称、所在的命名空间,或者是否定义了标签(labels)和注解(annotations)。这一部分还定义了 UID。

Spec

spec(即 specification)声明了期望状态,例如:对象在创建完成后应该呈现什么样子?容器中应该运行哪个镜像?应该设置哪些环境变量?

Status

status 描述了对象的实际状态。Kubernetes 控制器及其调谐循环会不断尝试将对象从期望状态转换为实际状态。如果 YAML 中的 status 显示为 {},则表示对象尚未被真正实例化。

在理解了这一基本结构之后,让我们看看如何借助 kubectl 来创建 Kubernetes 对象。

使用 kubectl

kubectl 是通过命令行与 Kubernetes 集群交互的主要工具。考试完全围绕 kubectl 的使用展开,因此,深入理解它的使用方式并进行大量练习至关重要。

本节将简要介绍其典型的使用模式。首先来看运行命令的语法。一次 kubectl 的执行由命令、资源类型、资源名称以及可选的命令行标志组成:

$ kubectl [command] [TYPE] [NAME] [flags]

command 指定你要执行的操作,常见的命令是动词形式,例如 creategetdescribedelete。接下来,你需要提供要操作的资源类型,可以使用完整名称,也可以使用其简写形式。例如,可以使用 service,或者其简写形式 svc

资源名称用于标识面向用户的对象标识符,本质上对应于 YAML 表示中的 metadata.name。需要注意的是,对象名称并不等同于 UID。UID 是由 Kubernetes 自动生成的内部对象引用,通常不需要直接与之交互。对象名称在同一命名空间内、同一资源类型下必须是唯一的。

最后,你可以提供零个或多个命令行标志,用于描述额外的配置行为。一个典型的命令行标志示例是 --port,它用于暴露 Pod 中容器的端口。

图 3-3 展示了一个完整的 kubectl 命令示例。

kcad-3-3

图 3-3:kubectl 使用模式

在本书的学习过程中,我们将探索那些能让你在考试中最具效率的 kubectl 命令。当然,还有许多其他命令,但它们通常超出了应用开发人员日常使用的范围。

接下来,我们将深入了解 create 命令,即以 命令式(imperative) 的方式创建 Kubernetes 对象。同时,我们也会将命令式对象创建方式与 声明式(declarative) 方式进行比较。

管理对象

你可以通过两种方式在 Kubernetes 集群中创建对象:命令式声明式。接下来的章节将分别介绍这两种方式,包括它们的优点、缺点以及适用场景。

命令式对象管理

命令式对象管理不需要定义清单(manifest)。你可以使用 kubectl 通过一条命令以及一个或多个命令行选项来驱动对象的创建、修改和删除。关于命令式对象管理的更详细说明,请参考 Kubernetes 官方文档。

创建对象

使用 runcreate 命令可以即时创建一个对象。运行时所需的所有配置都通过命令行选项提供。这种方式的优点是 反馈速度快,无需处理复杂的 YAML 结构。

下面的 run 命令创建了一个名为 frontend 的 Pod,该 Pod 在容器中运行镜像 nginx:1.24.0,并暴露端口 80:

$ kubectl run frontend --image=nginx:1.24.0 --port=80
pod/frontend created

更新对象

已经存在的对象配置仍然可以被修改。kubectl 通过提供 editpatch 命令来支持这一使用场景。

edit 命令会打开一个编辑器,显示该对象的实时原始配置。退出编辑器后,对配置所做的更改将应用到实时对象中。该命令会优先使用由 KUBE_EDITOREDITOR 环境变量定义的编辑器;如果未定义,则在 Linux 上回退到 vi,在 Windows 上回退到 notepad。下面的命令演示了对名为 frontend 的 Pod 实时对象使用 edit 命令:

$ kubectl edit pod frontend

patch 命令允许使用 JSON 合并补丁(JSON merge patch) 在属性级别对实时对象进行精细化修改。下面的示例展示了如何使用 patch 命令更新之前创建的 Pod 中容器的镜像标签。-p 标志定义了用于修改实时对象的 JSON 结构:

$ kubectl patch pod frontend -p '{"spec":{"containers":[{"name":"frontend",\
"image":"nginx:1.25.1"}]}}'
pod/frontend patched

删除对象

你可以在任何时候删除一个 Kubernetes 对象。在考试中,如果你在解决问题时犯了错误,可能需要删除对象并从头开始,以确保一个干净的初始状态。在生产环境中,你也需要删除不再需要的对象。下面的 delete 命令通过对象名称 frontend 删除 Pod 对象:

$ kubectl delete pod frontend
pod "frontend" deleted

执行 delete 命令后,Kubernetes 会尝试以 优雅(graceful) 的方式删除目标对象,以尽量减少对最终用户的影响。如果对象无法在默认的优雅期(30 秒)内被删除,kubelet 将尝试强制终止该对象。

在考试过程中,终端用户的影响并不是关注重点。最重要的目标是在规定时间内完成所有任务。因此,等待对象被优雅删除是一种时间浪费。你可以通过 --now 命令行选项强制立即删除对象。下面的命令使用 SIGKILL 信号立即终止名为 nginx 的 Pod:

$ kubectl delete pod nginx --now

声明式对象管理

声明式对象管理需要一个或多个使用 YAMLJSON 格式编写的清单文件,用来描述对象的期望状态。你可以通过这种方式创建、更新和删除对象。

使用声明式方法的好处是 可重现性强维护性更好,因为这些文件通常会被提交到版本控制系统中。声明式方式是生产环境中创建对象的推荐方法。有关声明式对象管理的更多信息,请参考 Kubernetes 官方文档。

创建对象

声明式方法通过 apply 命令从清单文件(通常是 YAML 文件)创建对象。该命令通过 -f 选项指向单个文件、一个目录、目录树,或者一个通过 HTTP(S) URL 引用的文件。如果一个或多个对象已经存在,该命令会将配置中的更改同步到实时对象中。

为了演示其功能,假设存在如下目录结构和配置文件。下面的示例展示了如何从单个文件、目录中的所有文件,以及递归目录中的所有文件创建对象。稍后章节将解释这里所使用的原语的用途:

.
├── app-stack
│   ├── mysql-pod.yaml
│   ├── mysql-service.yaml
│   ├── web-app-pod.yaml
│   └── web-app-service.yaml
├── nginx-deployment.yaml
└── web-app
    ├── config
    │   ├── db-configmap.yaml
    │   └── db-secret.yaml
    └── web-app-pod.yaml

从单个文件创建对象:

$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx-deployment created

从目录中的多个文件创建对象:

$ kubectl apply -f app-stack/
pod/mysql-db created
service/mysql-service created
pod/web-app created
service/web-app-service created

从包含文件的递归目录树创建对象:

$ kubectl apply -f web-app/ -R
configmap/db-config configured
secret/db-creds created
pod/web-app created

从通过 HTTP(S) URL 引用的文件创建对象:

$ kubectl apply -f https://raw.githubusercontent.com/bmuschko/\
ckad-study-guide/master/ch03/object-management/nginx-deployment.yaml
deployment.apps/nginx-deployment created

apply 命令通过添加或修改键为 kubectl.kubernetes.io/last-applied-configuration 的注解来跟踪更改。下面是 get pod 命令输出中该注解的一个示例:

$ kubectl get pod web-app -o yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{}, \
      "labels":{"app":"web-app"},"name":"web-app","namespace":"default"}, \
      "spec":{"containers":[{"envFrom":[{"configMapRef":{"name":"db-config"}}, \
      {"secretRef":{"name":"db-creds"}}],"image":"bmuschko/web-app:1.0.1", \
      "name":"web-app","ports":[{"containerPort":3000,"protocol":"TCP"}]}], \
      "restartPolicy":"Always"}}
...

更新对象

更新已有对象同样使用 apply 命令。你只需要修改配置文件,然后针对该文件再次运行命令即可。

示例 3-1 修改了文件 nginx-deployment.yaml 中 Deployment 的现有配置。我们新增了一个键为 team 的标签,并将副本数从 3 改为 5。

示例 3-1:Deployment 的修改后配置文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
    team: red
spec:
  replicas: 5
...

下面的命令应用了修改后的配置文件。结果是,由底层 ReplicaSet 控制的 Pod 数量变为 5:

$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx-deployment configured

Deployment 的 kubectl.kubernetes.io/last-applied-configuration 注解反映了最新的配置变更:

$ kubectl get deployment nginx-deployment -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{}, \
      "labels":{"app":"nginx","team":"red"},"name":"nginx-deployment", \
      "namespace":"default"},"spec":{"replicas":5,"selector":{"matchLabels": \
      {"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}}, \
      "spec":{"containers":[{"image":"nginx:1.14.2","name":"nginx", \
      "ports":[{"containerPort":80}]}]}}}}
...

删除对象

虽然你可以通过 apply 命令并提供 --prune -l <labels> 选项来删除对象,但推荐使用 delete 命令并直接指向配置文件来删除对象。下面的命令会删除一个 Deployment 以及它所控制的对象(ReplicaSet 和 Pod):

$ kubectl delete -f nginx-deployment.yaml
deployment.apps "nginx-deployment" deleted

你可以使用 --now 选项来强制删除 Pod,正如在“删除对象”一节中所描述的那样。

混合方式

有时你可能希望采用一种 混合方式。你可以先使用命令式方法生成一个清单文件,而不实际创建对象。为此,可以在执行 runcreate 命令时使用命令行选项 -o yaml--dry-run=client

$ kubectl run frontend --image=nginx:1.25.1 --port=80 \
  -o yaml --dry-run=client > pod.yaml

现在,你可以将生成的 YAML 清单作为起点,在创建对象之前进行进一步修改。只需使用编辑器打开该文件,修改内容,然后执行声明式的 apply 命令:

$ vim pod.yaml
$ kubectl apply -f pod.yaml
pod/frontend created

应该使用哪种方式?

在考试过程中,使用命令式命令是管理对象最快、最有效的方式。并非所有配置选项都能通过命令行标志暴露出来,这可能会迫使你使用声明式方式。在这种情况下,混合方式会非常有帮助。

GitOps 与 Kubernetes

GitOps 是一种实践,它利用提交到 Git 仓库中的源代码来自动化基础设施管理,尤其适用于由 Kubernetes 驱动的云原生环境。诸如 Argo CDFlux 等工具通过声明式方式实现 GitOps 原则,将应用部署到 Kubernetes 中。负责管理真实 Kubernetes 集群及其内部应用的团队,极有可能采用声明式方式。

尽管以命令式方式创建对象可以优化响应时间,但在真实的 Kubernetes 生产环境中,你几乎一定会使用声明式方式。YAML 清单文件代表了 Kubernetes 对象的最终真实来源(source of truth)。纳入版本控制的文件可以被审计和共享,并且在需要回滚到先前版本时,还能保留变更历史。

总结

Kubernetes 通过一组原语来表示其用于部署和运行云原生应用的功能。每个原语都遵循一种通用结构:API 版本KindMetadata 以及资源的期望状态(也称为 spec)。在对象被创建或修改后,Kubernetes 调度器会自动尝试确保对象的实际状态符合所定义的规范。每一个运行中的对象都可以被检查、编辑和删除。

kubectl 作为一个基于 CLI 的客户端,用于与 Kubernetes 集群交互。你可以使用它的命令和标志来管理 Kubernetes 对象。命令式方式 通过单条命令即可管理对象,提供了极快的反馈速度,但前提是你需要记住可用的标志。更复杂的配置则需要使用 YAML 清单 来定义原语,并通过声明式命令从该定义中实例化对象。YAML 清单通常会被提交到版本控制系统中,从而提供一种跟踪配置变更的方式。