Kubenetes-扩展教程-全-
Kubenetes 扩展教程(全)
一、简介
生活就像洋葱;你一层一层地剥开它,有时你会流泪。
卡尔沙堡
美国诗人、传记作家、记者和编辑,三次获得普利策奖
Kubernetes 就像洋葱。你一次剥开一层,有时你会流泪,检查你的 YAML 文件,并阅读更多的文档。
Kubernetes 是一个复杂的系统。第一章将从 Kubernetes 的简史以及它如何发展成为一个复杂的系统开始。虽然它已经有许多层,但它还可以用附加层来扩展。本章还将讨论如何配置 Kubernetes 系统、它的扩展模式和要点。在本章的最后,你将理解 Kubernetes 的复杂性和它的能力。
让我们先简要回顾一下 Kubernetes 的历史及其特点。
忽必烈再世
Kubernetes 是一个管理容器化应用的开源系统。名字源于希腊语,意为舵手。所以,说 Kubernetes 是帮助你在容器和微服务的风暴海洋中找到莫比迪克的工具并没有错。
谷歌在 2014 年开源了 Kubernetes,它是几十年来在容器中运行生产工作负载的经验积累。2015 年,谷歌宣布将 Kubernetes 项目移交给云计算原生计算基金会(CNCF)。 1 CNCF 拥有超过 500 名成员, 2 包括全球最庞大的公有云和企业软件公司以及超过 100 家创新创业公司。该基金会是许多发展最快的项目的供应商中立之家,包括 Kubernetes 、普罗米修斯和特使。
Kubernetes 是目前最受欢迎的开源项目之一,有近 3000 名贡献者,超过 1000 个拉请求和 2000 个开放问题。这个库(图 1-1 )可以在 GitHub 上以名称kubernetes/kubernetes
获得。 3
图 1-1
库存储库
有大量的开放问题需要解决,如果你想深入开源世界做出贡献,这个社区也是最受欢迎的。现在让我们再深入一个层次,看看我们所说的 Kubernetes 系统是什么意思。
Kubernetes 被设计为在集群上运行。Kubernetes 集群由节点组成,容器化的应用在这些节点上运行。
我们可以从逻辑上将 Kubernetes 系统分成两部分:控制平面和工作节点。控制平面管理工作节点和群集的工作负载,而工作节点运行工作负载。在图 1-2 中,您可以看到 Kubernetes 集群的组件是如何联系在一起的。
图 1-2
库比特组件
控制平面组件
控制平面是 Kubernetes 集群的大脑,负责做出决策、检测事件,并在需要时做出响应。例如,期望控制平面将 pod 的调度决策给予工作节点,识别故障节点,并重新调度新的 pod 以确保可扩展性。
控制平面组件可以在 Kubernetes 集群中的任何节点上运行;但是,为控制平面组件保存一些节点是一种典型的方法。这种方法将工作负载从集群中的控制平面组件中分离出来,使操作节点进行扩展和缩减或维护变得更加容易。
现在,让我们回顾一下每个控制平面组件及其对群集的重要性。
多维数据集 apiserver
Kubernetes API 是控制平面的前端,kube-apiserver
公开了它。kube-apiserver
可以通过运行多个实例进行水平扩展,从而创建一个高度可用的 Kubernetes API。
和 cd
etcd
是一个开源的分布式键值存储,Kubernetes 将其所有数据存储在其中。集群的状态和变化仅通过kube-apiserver
保存在etcd
中,并且可以水平扩展etcd
。
多维数据集调度程序
Kubernetes 是一个容器编排系统,它需要将容器化的应用分配给节点。kube-scheduler
负责通过考虑资源需求、可用资源、硬件和策略约束、相似性规则和数据局部性来做出调度决策。
kube-控制器-管理器
Kubernetes 的关键设计概念之一是控制器。Kubernetes 中的控制器是控制循环,用于监视集群的状态,并在需要时做出更改。每个控制器都与 Kubernetes API 交互,并试图将当前的集群状态转移到所需的状态。在本书中,您不仅会熟悉原生的 Kubernetes 控制器,还会学习如何创建新的控制器来实现新的功能。kube-controller-manager
是 Kubernetes 集群的一组核心控制器。
云控制器管理器
Kubernetes 被设计成一个独立于平台的可移植系统。因此,it 需要与云提供商进行交互,以创建和管理节点、路由或负载平衡器等基础设施。cloud-controller-manager
是运行特定于云提供商的控制器的组件。
节点组件
节点组件安装在 Kubernetes 集群中的每个工作节点上。工作节点负责运行容器化的应用。在 Kubernetes 中,容器被分组到一个名为 pod 的资源中。kube-scheduler
将 pod 分配给节点,节点组件确保它们启动并运行。
忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈
kubelet
是运行在各个节点上的代理。它获取分配给该节点的 pod 的规范。然后,它与容器运行时交互,在 pod 中创建、删除和观察容器的状态。
多维数据集代理
容器化的应用在 Kubernetes 集群中运行时,就像在单个网络中运行一样。作为网络代理在每个节点上运行,并连接应用。它还维护群集内外网络通信的网络规则。
在生产环境中,控制平面组件在多个节点上运行,以提供容错和高可用性。类似地,工作节点的数量随着工作负载和资源需求而扩展。另一方面,可以创建更多可移植的 Kubernetes 系统,在 Docker 容器或虚拟机内的单个节点上运行,用于开发和测试环境。在本书中,我们将创建生产就绪型和单节点 Kubernetes 集群,并观察它们的运行情况。在接下来的小节中,我们将重点介绍如何配置 Kubernetes 系统,以了解它的功能。
配置 Kubernetes 集群
您可以通过两种广泛的方法来约束或释放 Kubernetes 集群:配置和扩展。在配置方法中,您可以更改标志、配置文件或 API 资源。这一节将着重于配置 Kubernetes,然后我们将在本书的其余部分把重点转移到扩展上。
参考文档中定义了控制平面和节点组件的标志和配置文件。 4 还有,也有可能弄脏自己的手,用 Docker 图像检查一下。让我们从kube-apiserver
开始,检查它的标志,如清单 1-1 所示。
$ docker run -it --rm k8s.gcr.io/kube-apiserver:v1.19.0 kube-apiserver --help
The Kubernetes API server validates and configures data for the api objects which include pods, services, replicationcontrollers, and others. The API Server services REST operations and provides the frontend to the cluster's shared state through which all other components interact.
Usage:
kube-apiserver [flags]
Generic flags:
--advertise-address ip
The IP address on which to advertise the apiserver to members of the cluster. ...
...
--cors-allowed-origins strings
List of allowed origins for CORS, comma separated.
...
Listing 1-1kube-apiserver flags
命令行输出是巨大的,kube-apiserver
二进制文件有将近 150 个标志。但是,每个集群管理员都需要知道一个标志:--feature-gates
。特性门是一组键和值对,用于启用 alpha 或实验性 Kubernetes 特性。它在每个 Kubernetes 组件中都可用,并且可以通过它的帮助来访问。这次让我们检查一下kube-scheduler
,如清单 1-2 所示。
$ docker run -it --rm k8s.gcr.io/kube-scheduler:v1.19.0 kube-scheduler --help 2>&1 |grep -A 250 feature-gates
--feature-gates mapStringBool
A set of key=value pairs that describe feature gates for alpha/experimental features. Options are:
APIListChunking=true|false (BETA - default=true) APIPriorityAndFairness=true|false (ALPHA - default=false) APIResponseCompression=true|false (BETA - default=true) AllAlpha=true|false (ALPHA - default=false)
AllBeta=true|false (BETA - default=false)
...
Listing 1-2kube-scheduler flags
特定版本的kube-scheduler
有 85 个特征门选项,因此输出也很长。Kubernetes 中的实验特性需要在毕业或贬值前进行 alpha 和 beta 测试。您可以在官方参考文档 5 中跟踪特性的状态及其默认值、阶段、开始和结束版本,如图 1-3 所示。
图 1-3
特征门
在受管理的 Kubernetes 系统中,如亚马逊弹性 Kubernetes 服务(EKS)或谷歌 Kubernetes 引擎(GKE) ,无法编辑控制平面组件的标志。但是,在 Google Kubernetes 引擎 6 中有选项启用所有 alpha 功能,带有类似于--feature-gates=AllAlpha=true
的--enable-kubernetes-alpha
标志。使用 alpha 集群对新特性进行早期测试和验证是很有价值的。
Kubernetes 的配置支持设计定制的集群。因此,掌握控制平面和节点组件的配置参数至关重要。然而,配置参数只允许您调整 Kubernetes 中已经存在的内容。在下一节中,我们将通过扩展来扩展 Kubernetes 的边界。
Kubernetes 扩展模式
Kubernetes 设计以 Kubernetes API 为核心。所有的 Kubernetes 组件如kube-scheduler
和客户端如kubectl
都与 Kubernetes API 交互操作。同样,扩展模式被设计成与 API 交互。然而,与客户机或 Kubernetes 组件不同,扩展模式丰富了 Kubernetes 的功能。有三种广为接受的设计模式来扩展 Kubernetes。
控制器
控制器是用于管理至少一种 Kubernetes 资源类型的循环。
他们检查资源的spec
和status
字段,并在需要时采取行动。在spec
字段中,定义了期望状态,而status
字段代表实际状态。我们可以用图 1-4 来说明控制器的流程。
图 1-4
Kubernetes 中的控制器模式
让我们从 Kubernetes 拿一个真正的控制器,试着理解它们是如何操作的。CronJob
是一个 Kubernetes 资源,支持按照重复的时间表运行Jobs
。Job
是另一个 Kubernetes 资源,它运行一个或多个 pod 并确保它们成功终止。CronJob
在 Go 包k8s.io/kubernetes/pkg/controller/cronjob
中定义了一个控制器。您可以像下面这样创建一个example CronJob
资源。
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: example
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes CronJob
restartPolicy: OnFailure
Listing 1-3Example CronJob resource
期望的状态在spec
字段中,有两个重要的部分:schedule
和jobTemplate
。schedule
定义间隔,example CronJob
以分钟为单位。jobTemplate
字段具有要执行的容器的Job
定义。
我们可以期望CronJob
控制器监视CronJob
资源,并在它们的调度发生时创建Jobs
。源代码比较长,但是我们可以突出一些重点。cronjob_controller.go
中的syncOne
函数负责创建Jobs
并更新单个CronJob
实例的状态。
jobReq, err := getJobFromTemplate(cj, scheduledTime)
...
jobResp, err := jc.CreateJob(cj.Namespace, jobReq)
...
klog.V(4).Infof("Created Job %s for %s", jobResp.Name, nameForLog)
recorder.Eventf(cj, v1.EventTypeNormal, "SuccessfulCreate", "Created job %v", jobResp.Name)
...
// Add the just-started job to the status list.
ref, err := getRef(jobResp)
if err != nil {
klog.V(2).Infof("Unable to make object reference for job for %s", nameForLog)
} else {
cj.Status.Active = append(cj.Status.Active, *ref)
}
cj.Status.LastScheduleTime = &metav1.Time{Time: scheduledTime}
if _, err := cjc.UpdateStatus(cj); err != nil {
klog.Infof("Unable to update status for %s (rv = %s): %v", nameForLog, cj.ResourceVersion, err)
}
...
Listing 1-4CronJob controller
当您部署示例CronJob
资源时,您可以在集群中看到更新的状态和创建的作业资源,如清单 1-5 所示。
$ kubectl apply -f cronjob_example.yaml
cronjob.batch/example created
$ kubectl get cronjob example -o yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
...
name: example
namespace: default
...
spec:
concurrencyPolicy: Allow
failedJobsHistoryLimit: 1
jobTemplate:
...
schedule: '*/1 * * * *'
successfulJobsHistoryLimit: 3
suspend: false
status:
active:
- apiVersion: batch/v1
kind: Job
name: example-1598968200
namespace: default
resourceVersion: "588"
uid: e4603eb1-e2b3-419f-9d35-eeea9021fc34
lastScheduleTime: "2020-09-01T13:50:00Z"
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
example-1598968200 1/1 4s 119s
example-1598968260 1/1 4s 59s
example-1598968320 1/1 3s 8s
Listing 1-5CronJob in action
Note
CronJob 控制器的源代码可以在 GitHub 上找到: https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/cronjob
。
借助 Kubernetes 中的定制资源,控制器提供了一个健壮的扩展模式。可以通过定义定制资源来扩展 Kubernetes API,并通过控制器来管理它们。在第四章中,我们将扩展 Kubernetes API 并编写定制控制器来实现这种设计模式。
web 手册
Webhook 是一个 HTTP 回调函数,用于发送事件通知并获取结果。在 Kubernetes API 中,可以通过外部 webhooks 来验证一些事件,如授权、验证或资源突变。Kubernetes 查询外部 REST 服务来处理此类事件。我们可以用图 1-5 来说明请求的流程。
图 1-5
Kubernetes 中的请求流
当一个新用户想要连接到 Kubernetes API 时,请求被打包并发送到定义的 webhook 地址,并检查响应。如果用户被授权,webhook 服务器可以发送如下数据,如清单 1-6 所示。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": true
}
}
Listing 1-6Authorization webhook response
类似地,如果用户想要更改 Kubernetes API 中的资源,可以查询 webhook 服务器来验证更改。当 webhook 后端通过发送类似于清单 1-7 的以下数据接受更改时,Kubernetes API 将应用这些更改。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": true
}
}
Listing 1-7Change webhook response
Webhook 后端易于遵循设计模式来扩展软件应用。然而,webhooks 给系统增加了一个故障点,在开发和运行过程中需要非常注意。
二进制插件
在二进制插件模式中,Kubernetes 组件执行第三方二进制文件。像kubelet
这样的节点组件或者像kubectl
这样的客户端程序利用这种模式,因为它需要主机系统上额外的二进制文件,例如,kubectl
用清单 1-8 中的函数执行第三方二进制文件。
// Execute implements PluginHandler
func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
// Windows does not support exec syscall.
if runtime.GOOS == "windows" {
cmd := exec.Command(executablePath, cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = environment
err := cmd.Run()
if err == nil {
os.Exit(0)
}
return err
}
// invoke cmd binary relaying the environment and args given
..
return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment)
}
Listing 1-8kubectl binary plugin handling
Go 函数Execute
调用外部二进制文件,并将其输入和输出捕获到命令行。在接下来的章节中,您将创建类似的插件,并看到二进制插件模式的运行。
Note
kubectl
的源代码可以在 GitHub: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cmd.go
获得。
作为软件工程设计模式,扩展模式在 Kubernetes 中被接受,并且是常见问题的可重复解决方案。如果您有类似的障碍,这些模式有助于实现解决方案。但是,应该记住,设计模式和扩展模式都不是灵丹妙药。它们应该被视为扩展 Kubernetes 系统的方法。有了多个组件和 API 端点,Kubernetes 对扩展有了广泛的开放点。在下一节中,我们将对 Kubernetes 中的这些扩展点进行更技术性的概述。
立方扩展点
Kubernetes 是一个开放的系统,但它并不像 Kubernetes 的每个组件都是一个乐高积木,可以插入新的东西。有一些特殊的扩展点,您可以扩展 Kubernetes 系统的技能。有五组主要的扩展点及其实现模式和工作区域:
kubectl
插件 :kubectl
是用户与 Kubernetes API 交互不可或缺的工具。可以通过在 CLI 中添加新命令来扩展kubectl
。kubectl
插件实现了二进制插件扩展模式,用户需要将它们安装在本地工作区。API 流扩展:对 Kubernetes API 的每个请求都要经过几个步骤:认证、授权和准入控制。Kubernetes 用 webhooks 为每个步骤提供了一个扩展点。
Kubernetes API 扩展 : Kubernetes API 有各种原生资源,比如 pods 或 nodes。您可以将自定义资源添加到 API 中,并对其进行扩展以适用于您的新资源。此外,Kubernetes 为其本地资源提供了控制器,您可以为您的定制资源编写和运行控制器。
调度器扩展 : Kubernetes 有一个控制平面组件,即
kube-scheduler
,用于在集群节点上分配工作负载。此外,还可以开发定制的调度程序,并在kube-scheduler
旁边运行。大多数调度程序遵循控制器扩展模式来监视资源并采取行动。基础设施扩展:节点组件与基础设施交互,以创建集群网络或将卷挂载到容器。Kubernetes 通过指定的容器网络接口(CNI)和容器存储接口(CSI)拥有网络和存储的扩展点。基础设施中的扩展点遵循二进制插件扩展模式,并且需要在节点上安装可执行文件。
我们已经根据功能和实现的扩展模式对扩展点进行了分组。在本书接下来的章节中,我们将深入讨论每一组。您不仅将学习扩展点及其技术背景,还将创建它们并在集群中运行。
关键要点
Kubernetes 是一个复杂的系统。
您可以从逻辑上将 Kubernetes 集群分成两部分:控制平面和节点组件。
Kubernetes 组件有丰富的配置选项。
扩展 Kubernetes 有三种扩展模式:控制器、webhook 和二进制插件。
Kubernetes 组件及其设计允许许多开放点进行扩展:
kubectl
、API 流、Kubernetes API、调度器和基础设施。
在下一章中,我们将从第一个扩展点开始:kubectl
插件。我们将为kubectl
创建新的插件,并使用自定义命令来丰富它的功能。
二、kubectl
插件
我们塑造工具,然后工具塑造我们。
—马歇尔·麦克卢汉
媒体学者和评论家
命令行工具是开发人员的瑞士军刀。您可以连接到后端系统,运行复杂的命令,并使用它们自动化您的日常任务。Kubernetes 的官方命令行工具是kubectl
。作为神话中的盖茨镇之神,kubectl
是进入星团的入口之神。它允许您通过与 Kubernetes API 通信来创建工作负载、管理资源和检查状态。在这一章中,我们将通过编写插件来扩展kubectl
。在本章的最后,你将开发并安装新的插件到kubectl
中,并运行自定义命令。
让我们从将 Kubernetes API 网关安装到您的本地工作站开始。
kubectl 安装和使用
kubectl
是与 Kubernetes API 通信的客户端工具。因此,最好有一个与 Kubernetes API 版本完全相同或接近的版本。否则,可能会有不兼容的 API 请求和失败的操作。kubectl
的源代码是官方 Kubernetes 库的一部分,它的发布版本与 Kubernetes 发布版本共同管理。但是需要查看图 2-1 中的kubernetes/kubectl
1 库,了解kubectl
的相关问题。
图 2-1
忽必烈〔??〕资料档案库
安装kubectl
相当简单,因为它是一个单二进制应用。您需要首先从您的操作系统的发布库下载二进制文件,如清单 2-1 所示。
# Linux
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.19.0/bin/linux/amd64/kubectl
# macOS
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.19.0/bin/darwin/amd64/kubectl
Listing 2-1Downloading kubectl binary
然后您需要使二进制文件可执行。
chmod +x ./kubectl
Listing 2-2Executable kubectl binary
最后,您需要将二进制文件移动到您的PATH
中。
sudo mv ./kubectl /usr/local/bin/kubectl
Listing 2-3Moving kubectl binary
您可以使用清单 2-4 中的以下命令来测试kubectl
。
kubectl version --client
Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.0", GitCommit:"e19964183377d0ec2052d1f1fa930c4d7575bd50", GitTreeState:"clean", BuildDate:"2020-08-26T14:30:33Z", GoVersion:"go1.15", Compiler:"gc", Platform:"darwin/amd64"}
Listing 2-4kubectl version check
该命令打印客户端版本的kubectl
,即v1.19.0
。在下面的练习中,您将创建一个本地 Kubernetes 集群,并继续使用更复杂的kubectl
命令与集群进行交互。
EXERCISE: STARTING A LOCAL KUBERNETES CLUSTER
虽然 Kubernetes 是一个用于大型云的容器管理系统,但是也可以在本地创建单实例 Kubernetes 集群。minikube
是推荐的、官方支持的创建单节点集群的方式。它主要用于开发和测试目的。
在本练习中,您将安装minikube
并启动一个新的 Kubernetes 集群。
根据您的操作系统下载 minikube 的二进制文件:
# Linux curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 # macOS curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64
将二进制文件安装到路径:
# Linux sudo install minikube-linux-amd64 /usr/local/bin/minikube # macOS sudo install minikube-darwin-amd64 /usr/local/bin/minikube
Start a local cluster with minikube:
Kubernetes 操作的简单性被打包成一个简单的命令minikube
start。它下载映像,启动控制面板组件,启用插件,并验证集群组件。在最后一步中,它配置kubectl
连接到由minikube
创建的集群。
您有一个 Kubernetes 集群和一个客户端工具。现在,是时候通过部署应用、扩展它们并检查它们的状态来享受 Kubernetes 的乐趣了。
kubectl
的用法基于清单 2-5 中的以下语法。
kubectl [command] [TYPE] [NAME] [flags]
Listing 2-5kubectl syntax
command
指定要对 Kubernetes API 执行的操作,比如创建、获取、描述或删除。您可以通过运行kubectl --help
列出所有命令。它列出了按功能和细节分组的所有命令,如清单 2-6 所示。
$ kubectl --help
kubectl controls the Kubernetes cluster manager.
Find more information at: https://kubernetes.io/docs/reference/kubectl/overview/
Basic Commands (Beginner):
create Create a resource from a file or from stdin.
expose Take a replication controller, service,
deployment or pod and expose it as a new Kubernetes Service
run Run a particular image on the cluster
set Set specific features on objects
Basic Commands (Intermediate):
explain Documentation of resources
get Display one or many resources
edit Edit a resource on the server
delete Delete resources by filenames, stdin,
resources and names, or by resources and label
selector
...
Listing 2-6kubectl help output
TYPE
指定 Kubernetes API 资源的类型,如 pod、部署或节点。在清单 2-7 中使用以下命令可以列出 Kubernetes API 上支持的 API 资源。
$ kubectl api-resources --output=name
bindings
componentstatuses
configmaps
endpoints
events
limitranges
namespaces
nodes
persistentvolumeclaims
persistentvolumes
pods
podtemplates
replicationcontrollers
Listing 2-7kubectl API resources
这是一个很长的列表,目前在 Kubernetes API 中支持 50 多种资源。
NAME
指定对其执行命令的资源的名称,操作。如果您没有指定一个NAME
,那么将对该类型中的所有资源执行命令。
flags
是命令的可选变量,如--namespace
或--kubeconfig
。你可以用kubectl
options
列出可以传递给任何命令的选项,如清单 2-8 所示。
$ kubectl options
The following options can be passed to any command:
...
--cluster='': The name of the kubeconfig cluster to use
--context='': The name of the kubeconfig context to use
...
--kubeconfig='': Path to the kubeconfig file to use for CLI requests.
-n, --namespace='': If present, the namespace scope for this CLI request
...
--token='': Bearer token for authentication to the API server
...
-v, --v=0: number for the log level verbosity
Listing 2-8kubectl options output
您可以运行kubectl <command> --help
来获得更多的信息,比如关于给定命令的选项、用法和示例。考虑到大量的资源和命令,kubectl
是一个打包了大量动作的工具。建议通过尝试不同的命令来使用kubectl
。kubectl
几乎是集群部署、状态跟踪和故障排除的唯一入口。在下面的练习中,在开发扩展之前,您将使用最常见的kubectl
命令来习惯它。
EXERCISE: GETTING STARTED WITH kubectl
在本练习中,您将使用kubectl
与 Kubernetes 集群进行交互。
Start with checking the version of your client tool and the API server:
$ kubectl version Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.0", GitCommit:"e19964183377d0ec2052d1f1fa930c4d7575bd50", GitTreeState:"clean", BuildDate:"2020-08-26T14:30:33Z", GoVersion:"go1.15", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.0", GitCommit:"e19964183377d0ec2052d1f1fa930c4d7575bd50", GitTreeState:"clean", BuildDate:"2020-08-26T14:23:04Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"}
它显示客户端和服务器的版本都是 1.19.0。
Check the nodes available in the cluster:
$ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready master 88s v1.19.0
节点也是 Kubernetes 中的一种资源类型,命令是从 Kubernetes API 中检索它们。您将有一个节点,因为您正在运行一个
minikube
集群。Create a deployment with the following command:
$ kubectl create deployment my-first-deployment --image=nginx deployment.apps/my-first-deployment created
该命令使用映像
nginx
创建名为my-first-deployment
的部署资源类型。Check the status of the deployment created in Step 3:
$ kubectl get deployment my-first-deployment NAME READY UP-TO-DATE AVAILABLE AGE my-first-deployment 1/1 1 1 16s
此命令检索资源及其名称。该部署有一个可用的就绪实例。
Scale the deployment to five instances:
$ kubectl scale deployment/my-first-deployment --replicas=5 deployment.apps/my-first-deployment scaled
这是一个特殊的命令,用于扩展所提供资源的实例数量。
--replicas
标志指定了请求的复制计数。Check the pods after scale-up:
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-first-deployment-...-26xpn 1/1 Running 0 13s my-first-deployment-...-87fcw 1/1 Running 0 13s my-first-deployment-...-b7nzv 1/1 Running 0 2m45s my-first-deployment-...-kxg2w 1/1 Running 0 13s my-first-deployment-...-wmg92 1/1 Running 0 13s
正如所料,现在有五个 pod,后四个是在第一个之后创建的。
使用以下命令清理部署:
$ kubectl delete deployment my-first-deployment deployment.apps "my-first-deployment" deleted
您的 CLI 环境有了一个新成员,您已经开始发现它的功能。现在,是时候更进一步,扩展它的技能了。在接下来的部分,我们将继续插件设计,为kubectl
添加自定义命令。
立方插件设计
核心命令对于与 Kubernetes API 的交互来说是必不可少的。插件用新的子命令扩展了kubectl
,用于新的定制特性。kubectl
扩展实现了二进制插件方法。与二进制插件模式一样,kubectl
将第三方应用作为扩展来执行。插件二进制文件有三个主要规则:
-可执行
-用户
PATH
的任何地方-从
kubectl-
开始
这三条规则基于kubectl
如何发现插件。我们来看看kubectl
中插件处理的源代码。
// Lookup implements PluginHandler
func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) {
for _, prefix := range h.ValidPrefixes {
path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename))
if err != nil || len(path) == 0 {
continue
}
return path, true
}
return "", false
}
Listing 2-9Plugin handler in kubectl
注意默认插件句柄的源代码可以在 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cmd.go
获得。
DefaultPluginHandler
检查从ValidPrefix
、kubectl
开始的路径中的可执行文件。因此,PATH
环境变量中任何名为kubectl-my-first-plugin
或kubectl-whoami
的二进制文件都是合适的kubectl
插件。插件名被解释为子命令,比如名为kubectl-whoami
的二进制文件代表kubectl whoami
命令。因此,kubectl
会检查本机实现中是否有命令,然后检查插件,如图 2-2 所示。
图 2-2
kubectl
命令处理
我们来看看kubectl
中的插件是如何执行的。
// Execute implements PluginHandler
func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
// Windows does not support exec syscall.
if runtime.GOOS == "windows" {
cmd := exec.Command(executablePath, cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = environment
err := cmd.Run()
if err == nil {
os.Exit(0)
}
return err
}
// invoke cmd binary relaying the environment and args given
..
return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment)
}
Listing 2-10kubectl binary plugin handling
注意默认插件句柄的源代码可以在 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cmd.go
获得。
DefaultPluginHandler
有一个Execute
函数,输入可执行路径、参数和环境变量。该函数将这些变量传递给第三方二进制文件,也就是插件。在 Windows 中,它将标准输入和输出连接到命令,然后执行它。在 Linux 和 macOS 中,该函数在操作系统级别使用syscall
,带有参数和环境变量。
现在,是时候通过创建插件向kubectl
添加一个新的定制命令了。
创建您的第一个 kubectl 插件
您可以使用kubectl
plugin 命令在本地列出可用的插件。
$ kubectl plugin list
error: unable to find any kubectl plugins in your PATH
Listing 2-11Installed
plugins
kubectl
没有在本地找到插件。现在,用以下内容创建一个名为kubectl-whoami
的文件。
#!/bin/bash
kubectl config view --template='{{ range .contexts }}{{ if eq .name "'$(kubectl config current-context)'" }}User: {{ printf "%s\n" .context.user }}{{ end }}{{ end }}'
Listing 2-12Plugin code
将文件移动到 PATH 环境变量中的一个文件夹,并使其可执行。
sudo chmod +x ./kubectl-whoami
sudo mv ./kubectl-whoami /usr/local/bin
Listing 2-13Plugin installation
现在,重新运行kubectl
插件列表命令。
$ kubectl plugin list
The following compatible plugins are available:
/usr/local/bin/kubectl-whoami
Listing 2-14Installed plugins
说明kubectl
可以发现插件。让我们测试一下,看看它是如何工作的。
$ kubectl whoami
User: minikube
Listing 2-15kubectl whoami plugin
运行最后一个命令有两个关键点。第一点是kubectl whoami
是一个扩展命令,在本地实现中不可用。但是,通过扩展功能,您可以运行自定义子命令。第二点是现在有可能检索信息,干扰kubectl
的操作。
在下面的练习中,您将创建一个kubectl
提示符命令,在 bash 提示符下显示当前的 Kubernetes 集群和用户名。
EXERCISE: KUBERNETES BASH PROMPT
处理一个 Kubernetes 集群很容易,但是当它变成日常事务中的几十个集群时就变得很麻烦了。在终端 bash 提示符中了解当前集群和用户有助于避免犯严重错误。我们将在每个带有(user @ cluster)
信息的命令前显示一个字符串。
Create a file with the name
kubectl-prompt
with the following content:#!/bin/bash currentContext=$(kubectl config current-context) prompt="(%s @ %s) > " template="{{ range .contexts }}{{ if eq .name \"$currentContext\" }}{{ printf \"$prompt\" .context.user .context.cluster}}{{ end }}{{ end }}" kubectl config view --template="$template"
该脚本检查
kubeconfig
中的所有上下文,并检索集群和用户名字段。将文件移动到
PATH
环境变量中的文件夹,并使其可执行:sudo chmod +x ./kubectl-prompt sudo mv ./kubectl-prompt /usr/local/bin
使用以下命令测试插件:
$ kubectl prompt (minikube @ minikube) >
设置提示环境变量:
$ export PS1=$(kubectl prompt) (minikube @ minikube) >
从现在开始,每个终端命令都将提供提示符。哪个集群和用户处于控制中,这将始终在您的视线之内。
插件扩展了kubectl
并帮助您在与 Kubernetes 集群交互时实现更多。预计在操作集群时会有类似的困难,这导致开发类似的插件。在下一节中,重点将放在kubectl
的插件库以及如何使用它。
插件库:krew
Kubernetes 社区有一个名为krew
的kubectl
插件管理器。插件管理器帮助发现、安装和更新开源和社区维护的插件。目前,krew
上分布着 100 多个插件。因此,在创建一个新的插件库之前,检查一下插件库是值得的。Kubernetes 社区中可能已经有人开发了相同的功能并发布了它。
让我们开始安装krew
,一个kubectl
插件本身,并发现一些存储库插件。在终端运行该命令下载krew
。
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz"
tar zxf krew.tar.gz
Listing 2-16Downloading krew
现在,根据操作系统安装二进制文件。
# Linux
./krew-linux_amd64 install krew
# macOS
./krew-darwin_amd64 install krew
Adding "default" plugin index from https://github.com/kubernetes-sigs/krew-index.git.
Updated the local copy of plugin index.
Installing plugin: krew
Installed plugin: krew
\
| Use this plugin:
| kubectl krew
| Documentation:
| https://krew.sigs.k8s.io/
| Caveats:
| \
| | krew is now installed! To start using kubectl plugins, you need to add
| | krew's installation directory to your PATH:
| |
| | * macOS/Linux:
| | - Add the following to your ~/.bashrc or ~/.zshrc:
| | export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
| | - Restart your shell.
| |
| | * Windows: Add %USERPROFILE%\.krew\bin to your PATH environment variable
| |
| | To list krew commands and to get help, run:
| | $ kubectl krew
| | For a full list of available plugins, run:
| | $ kubectl krew search
| |
| | You can find documentation at
| | https://krew.sigs.k8s.io/docs/user-guide/quickstart/.
| /
/
Listing 2-17Downloading krew
最后,将 krew 安装目录添加到路径中。
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
Listing 2-18Path expansion
现在,我们可以通过调用一个kubectl
插件来测试它。
$ kubectl krew
krew is the kubectl plugin manager.
You can invoke krew through kubectl: "kubectl krew [command]..."
Usage:
kubectl krew [command]
Available Commands:
help Help about any command
index Manage custom plugin indexes
info Show information about an available plugin
install Install kubectl plugins
list List installed kubectl plugins
search Discover kubectl plugins
uninstall Uninstall plugins
update Update the local copy of the plugin index
upgrade Upgrade installed plugins to newer versions
version Show krew version and diagnostics
Flags:
-h, --help help for krew
-v, --v Level number for the log level verbosity
Use "kubectl krew [command] --help" for more information about a command.
Listing 2-19kubectl krew output
现在可以搜索、安装和升级由krew
管理的插件。在krew
网站上可以获得最新的插件列表,包括名称、描述和 GitHub 流行度,如图 2-3 所示。
图 2-3
krew 插件列表
让我们假设您在 Kubernetes 中运行一个 web 应用,它在实例前面有一个服务。要访问和测试应用,您需要到达服务端点。幸运的是,Kubernetes 社区有一个插件可以完成这项任务。open-svc
是通过本地代理服务器在浏览器中打开指定服务 URL 的kubectl
插件。你可以通过krew
安装。
$ kubectl krew install open-svc
Updated the local copy of plugin index.
Installing plugin: open-svc
Installed plugin: open-svc
\
| Use this plugin:
| kubectl open-svc
| Documentation:
| https://github.com/superbrothers/kubectl-open-svc-plugin
/
WARNING: You installed plugin "open-svc" from the krew-index plugin repository.
These plugins are not audited for security by the Krew maintainers.
Run them at your own risk.
Listing 2-20Installing open-svc plugin
注意如果您还没有为您的集群启用 Kubernetes 仪表板,您可以运行minikube dashboard
来安装它。
现在,让我们使用kubectl open-svc
插件打开 Kubernetes 仪表板。
$ kubectl open-svc kubernetes-dashboard -n kubernetes-dashboard
Starting to serve on 127.0.0.1:8001
Opening service/kubernetes-dashboard in the default browser...
Listing 2-21open-svc plugin in action
该命令应在浏览器中打开仪表板,如图 2-4 所示。
图 2-4
忽必烈的控制板
只需要几个命令就可以从存储库中安装新插件并开始使用它们。因此,在从头开始创建社区之前,检查社区已经开发了什么是有用的。
关键要点
kubectl
是与 Kubernetes API 交互的官方客户端。本机命令对于操作 Kubernetes 集群是必不可少的。
通过创建插件,可以用新命令扩展
kubectl
。kubectl
插件是第三方二进制,由kubectl
执行。有一个由社区维护的插件库,名为
krew
。
在下一章中,我们将继续 API 流扩展,并学习如何使用认证、授权和准入控制来扩展流。
三、API 流扩展
把鸡蛋放在一个篮子里没关系,只要你能控制那个篮子会发生什么。
—埃隆·马斯克
商业巨头、工业设计师、工程师
Kubernetes 是安全、可靠、可扩展的云原生应用之家。Kubernetes API 流使得认证请求、决定授权和通过许可步骤成为可能。这个流程使 Kubernetes 成为一个受保护的环境,同时让您定义什么是允许的,什么是不允许的。在这一章中,我们将着重于扩展 Kubernetes API 流,并干预我们的自定义决策。在本章结束时,你将对 API 流有一个自信的看法,并对扩展 webhooks 有实际操作经验。
我们先来总结一下 Kubernetes API 流程及其扩展点。
忽必烈 API Flow
您可以使用kubectl
、客户端库或者直接发送 REST 请求来连接和使用 Kubernetes API。对 API 的每个请求都要经过认证、授权和几个准入控制阶段。这三个阶段都通过 webhooks 提供了扩展点,将在本章中讨论。流程和扩展点可在图 3-1 中说明。
图 3-1
忽必烈 API flow
证明
身份验证是验证传入 API 请求身份的第一步。Kubernetes 使用客户端证书、不记名令牌、基本身份验证和身份验证插件来审查请求。此外,它能够同时运行多个授权码。普遍的 Kubernetes 装置预计具有以下特点:
服务帐户用户的服务帐户令牌
至少一种其他方法,如用于用户身份验证的客户端证书、承载令牌或基本 auth
您可以通过添加 webhook 身份验证器来验证不记名令牌,从而扩展身份验证机制。Kubernetes 将向远程服务发送一个 JSON 请求,远程服务充当您的 webhook 服务。在远程服务中,您将验证令牌并决定是否允许请求。
批准
授权是确定用户是否可以读取、写入或更新 API 资源的第二个阶段。Kubernetes 中的授权模块检查请求的用户、组、HTTP 动词、资源和名称空间属性来验证它们。像身份验证一样,可以使用多个授权模块,如基于属性的访问控制(ABAC)、基于角色的访问控制(RBAC)和 webhooks。您可以通过添加新的 webhook 模块来扩展授权。您的定制 webhook 服务接收带有访问检查数据的 HTTP POST 请求。评估完成后,webhook 服务会根据允许与否发回响应。
准入控制
传入请求的第三和最后阶段是准入控制模块。准入控制器是一个代码,用于在身份验证和授权之后、持久化到存储之前拦截到达 Kubernetes API 服务器的请求。与前面的阶段类似,可以依次运行多个准入控制器。然而,早期阶段之间有两个主要区别。第一个问题是,接纳控制器不是简单地应用于读取对象的 GET 请求。第二个区别是这些模块可以修改请求和相关的实体。因此,他们可以验证规范值(如容器图像)或设置默认值(如 CPU 请求)。
各种准入控制器已经打包到kube-apiserver
中,并根据 Kubernetes 版本启用或禁用。让我们直接从kube-apiserver
二进制文件中查看控制器列表:
$ docker run -it --rm k8s.gcr.io/kube-apiserver:v1.19.0 kube-apiserver --help | grep enable-admission-plugins
...
--enable-admission-plugins strings admission plugins that should be enabled in addition to default enabled ones (NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, ...). Comma-delimited list of admission plugins: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, CertificateApproval, CertificateSigning, CertificateSubjectRestriction, DefaultIngressClass, DefaultStorageClass, ...
Listing 3-1kube-apiserver plugin listing
Note
输出被连接起来,因为它长得令人难以置信,我们不会一一介绍每个准入插件。如果您需要关于插件的更多信息,您可以查看参考文档。
除了kube-apiserver
中的默认准入控制器之外,还可以在运行时添加新的控制器作为 webhooks。与身份验证和授权插件不同,当集群运行时,可以添加或删除准入控制器;因此,他们被称为动态接纳控制器。
让我们通过开发 webhooks 和配置 Kubernetes 集群来扩展认证流程。
身份验证网页挂钩
身份验证 webhooks 通过外部安全逻辑扩展了 Kubernetes API 流。Kubernetes 中的 Webhooks 是对外部系统的 HTTP 回调。当集群中发生特定事件时,Kubernetes API 服务器通过 HTTP POST 向外部服务发送结构化请求。webhook 服务器应该返回一个结构化的响应,以便 Kubernetes API 服务器继续运行。运行认证 webhooks 需要配置两个基本部分: Kubernetes API 服务器和 webhook 服务器。让我们从 Kubernetes API 服务器开始,让它知道作为 webhook 连接到哪里。
服务器 API 配置库
Kubernetes API 服务器运行在控制平面中,需要知道作为 webhook 连接到哪里。通过kube-apiserver
二进制的标志和配置文件设置配置。因此,集群管理员(很可能是您)应该处理设置。认证 webhook 配置的kube-apiserver
有两个基本标志:
--authentication-token-webhook-config-file
:描述如何访问远程 webhook 服务的配置文件--authentication-token-webhook-cache-ttl
:多长时间缓存认证决策,默认为两分钟
Note
还有一个名为--authentication-token-webhook-version
的版本标志。它决定是否使用authentication.k8s.io/v1beta1
或authentication.k8s.io/v1 TokenReview
对象来发送/接收来自 webhook 的信息。默认为v1beta1
,在本章中使用。
authentication-token-webhook-config-file
标志没有默认值,需要一个类似于kubeconfig
的配置文件。
apiVersion: v1
kind: Config
clusters:
- name: remote-auth-service
cluster:
certificate-authority: /path/to/ca.pem
server: https://extend.k8s.io/authenticate
users:
- name: remote-auth-service-user
user:
client-certificate: /path/to/cert.pem
client-key: /path/to/key.pem
current-context: webhook
contexts:
- context:
cluster: remote-auth-service
user: remote-auth-service-user
name: webhook
Listing 3-2Authentication token webhook config example
当承载令牌认证激活时,Kubernetes API 服务器连接到集群中定义的服务器,并在必要时使用certificate-authority
。此外,API 服务器利用client-certificate
和client-key
与 webhook 服务器进行安全通信。现在,让我们继续 webhook 服务器和 Kubernetes API 之间的通信。
web 手册服务器
当 API 服务器配置了 webhook 令牌认证时,它将发送一个带有TokenReview
对象的 JSON 请求。示例TokenReview
对象可以按如下方式构造。
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"spec": {
"token": "0x123...",
}
}
Listing 3-3TokenReview object
webhook 服务器验证传入的令牌并收集用户信息。远程服务器必须填写 TokenReview 对象的状态字段并发回数据。不记名令牌的成功验证将返回以下令牌 Review 作为示例。
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": true,
"user":{
"username": "user@k8s.io",
"uid": "21",
"groups":[ "system", "qa" ]
}
}
}
Listing 3-4TokenReview with a successful status
用户名、UID 和组是经过验证的用户的标识符。该信息对于授权阶段决定谁有权访问哪个组是至关重要的。当令牌验证失败时,webhook 服务器应该返回一个类似如下的请求。
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": false,
"error": "Credentials are not validated"
}
}
Listing 3-5TokenReview with a failed status
当 webhook 服务器拒绝用户时,kube-apiserver
使用TokenReview
状态的错误信息。在图 3-2 中可以总结出 webhook 和 Kubernetes API 服务器之间的 TokenReview 消息流。
图 3-2
认证消息流
webhook 服务器可以在 Kubernetes 集群的内部或外部。换句话说,您可以在 Kubernetes 集群内部运行一个服务器,并将其用作 webhook 服务器。Kubernetes API 服务器使用 HTTPS 连接到 webhook 服务器;因此,您还需要设置 TLS 证书。或者,您可以将 webhook 服务器设置在集群外部,并使其对外部世界可用。这两个选项的关键点是启动并运行 webhook 服务器,因为身份验证流程依赖于它。在下面的练习中,您将把一个无服务器的 webhook 部署到 Google Cloud,并配置一个本地 minikube 集群,将其用作身份验证端点。
EXERCISE: SERVERLESS AUTHENTICATION WEBHOOK
本练习中的 webhook 服务器将运行在 Google Cloud 上,作为其无服务器平台的一部分。您将开始创建一个云功能,并将其部署到 Google Cloud。然后,您将配置本地 minikube 集群,以使用无服务器功能的地址作为身份验证 webhook。
图 3-4
创建功能
图 3-3
GCP 云函数
在如图 3-4 所示的“创建函数”视图中,填写 name 字段,选择“允许未授权调用,然后点击下一步。
打开谷歌云控制台,前往计算 ➤ 云功能。在功能列表视图中点击创建功能,如图 3-3 所示。
记下触发器 URL,因为您将在步骤 5 中将其用作 SERVERLESS_ENDPOINT 环境变量。
图 3-5
功能的部署
In the “Code” view, select Go as runtime and fill “Entry Point” field with Authenticate. Authenticate is the name of the function that Google Cloud will call when the serverless endpoint is reached. Change the contents of the
function.go
with the following content:package authenticate import ( "encoding/json" "errors" "log" "net/http" "strings" authentication "k8s.io/api/authentication/v1beta1" ) func Authenticate(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var tr authentication.TokenReview err := decoder.Decode(&tr) if err != nil { handleError(w, err) return } user, err := logon(tr.Spec.Token) if err != nil { handleError(w, err) return } log.Printf("[Success] login as %s", user.username) w.WriteHeader(http.StatusOK) trs := authentication.TokenReviewStatus{ Authenticated: true, User: authentication.UserInfo{ Username: user.username, Groups: []string{user.group}, }, } tr.Status = trs json.NewEncoder(w).Encode(tr) } func handleError(w http.ResponseWriter, err error) { log.Println("[Error]", err.Error()) tr := new(authentication.TokenReview) trs := authentication.TokenReviewStatus{ Authenticated: false, Error: err.Error(), } tr.Status = trs w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(tr) } func logon(token string) (*User, error) { data := strings.Split(token, ";") if len(data) < 3 { return nil, errors.New("no token data") } for _, u := range allowed { if u.group == data[0] && u.username == data[1] && u.password == data[2] { return &u, nil } } return nil, errors.New("no user found") } type User struct { username string password string group string } var allowed = []User{ { username: "minikube-user", group: "system:masters", password: "mysecret", }, }
这个文件有一个
Authenticate
HTTP 端点来解析TokenReview
数据,登录用户,并将其发送回来。它使用logon
助手函数来搜索允许的用户。被允许的用户只有一个:minikube-user
,其有效令牌为system:masters;minikube-user;mysecret
。Change the contents of
go.mod
as follows:module extend.k8s.io/authenticate go 1.14 require k8s.io/api v0.19.0
在
function.go
中,我们使用的是 Kubernetes Go 客户端库;因此,我们将其列为k8s.io/api
,版本v0.19.0
的需求。在图 3-5 中点击页面底部的部署。
Note:
为了构建和部署该功能,如果您之前没有这样做,您需要在云控制台 API 库视图中启用云构建 API。
图 3-7
功能日志
图 3-6
成功部署
Create a local webhook config file
serverless-authn.yaml
with the following content:apiVersion: v1 kind: Config clusters: - name: serverless-authn cluster: server: SERVERLESS_ENDPOINT users: - name: authn-user current-context: webhook contexts: - context: cluster: serverless-authn user: authn-user name: webhook
不要忘记用步骤 2 中的 URL 更改
SERVERLESS_ENDPOINT
。将文件移动到 minikube 文件:
mkdir -p $HOME/.minikube/files/var/lib/minikube/certs mv serverless-authn.yaml $HOME/.minikube/files/var/lib/minikube/certs/serverless-authn.yaml
使用额外的标志启动 minikube 集群:
创建一个新的空用户并在当前上下文中使用:
$ kubectl config set-credentials auth-test User "auth-test" set. $ kubectl config set-context --current --user=auth-test Context "minikube" modified.
Run
kubectl
with the valid token and check the result:$ kubectl get nodes --token="system:masters;minikube-user;mysecret" NAME STATUS ROLES AGE VERSION minikube Ready master 116s v1.19.2
正如预期的那样,Kubernetes API 发送列出节点的输出。
使用随机令牌运行
kubectl
并检查结果:$ kubectl get nodes --token="xyz" error: You must be logged in to the server (Unauthorized)
检查 Google Cloud 中的无服务器功能日志,并查看 webhook 的运行情况,如图 3-7 所示。
在如图 3-6 所示的功能列表视图中等待,直到旁边出现绿色复选标记。
日志显示成功的(第一次)和失败的(第二次)登录活动。
在本练习中,您已经公开了一个公共函数,并在 Kubernetes 集群中使用了它。在您的生产设置中,建议使用受保护的功能。
在下一节中,我们将使用定制的授权模块来扩展 Kubernetes API 流。我们将学习 webhook 和 Kubernetes API 服务器需求,然后实现自定义决策逻辑来决定谁可以访问或修改集群中的资源。
授权网页挂钩
授权 webhooks 扩展了 Kubernetes API 的访问控制,以实现定制策略。当请求通过身份验证阶段时,授权模块按顺序评估属性。如果任何授权模块批准或拒绝请求,结果将立即返回。如果请求被批准,API 请求继续流程,并进入下一个阶段。与身份验证阶段一样,运行授权 webhooks 有两种基本配置:Kubernetes API 服务器和 webhook 服务器。
服务器 API 配置库
kube-apiserver
具有定义授权模式和 webhook 配置的标志。授权模式通过--authorization-mode
标志设置,默认值为AlwaysAllow
。换句话说,默认情况下,Kubernetes API 允许所有经过身份验证的请求。但是,在典型的 Kubernetes 设置中,启用了以下授权模式:RBAC 和节点。因此,要添加 webhook 授权,您需要通过添加 Webhook 来更新标志值。配置授权 webhook 操作有三个基本标志:
--authorization-webhook-config-file
:描述如何访问和查询远程服务的配置文件。该标志类似于认证中的标志,并且需要与kubeconfig
相同的配置。确保授权 webhook 服务器地址和证书数据(如有必要)正确无误。--authorization-webhook-cache-authorized-ttl
:缓存已验证请求的持续时间;默认值为五分钟。--authorization-webhook-cache-unauthorized-ttl
:缓存无效请求的持续时间;默认值为 30 秒。
Note
还有一个名为--authorization-webhook-version
的版本标志。它设置了authorization.k8s.io SubjectAccessReview
的应用编程接口版本,以发送到网络钩子并从其获得期望。缺省值为v1beta1
并在本章中使用。
web 手册服务器
Kubernetes API 服务器通过发送一个SubjectAccessReview
对象来描述要检查的动作,从而调用 webhook 服务器。发送的 JSON 对象包含关于资源、用户和请求属性的信息。用户ece
获取名称空间default
中的 pod 的示例SubjectAccessReview
具有以下结构。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"spec": {
"resourceAttributes": {
"namespace": "default",
"verb": "get",
"group": "",
"resource": "pods"
},
"user": "ece"
}
}
Listing 3-6SubjectAccessReview for pod listing
当 Kubernetes API 中的非资源路径被调用时,比如/version
或/metrics
,nonResourceAttributes
字段被发送到 webhook 服务器。例如,当用户nursin
调用version
端点时,Kubernetes API 服务器将发布下面的SubjectAccessReview
。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"spec": {
"nonResourceAttributes": {
"path": "/version",
"verb": "get"
},
"user": "nursin"
}
}
Listing 3-7SubjectAccessReview for version information
webhook 服务器通过填充status
字段来响应SubjectAccessReview
对象。如果 webhook 服务器接受请求,它可以很容易地将以下数据发送回 Kubernetes API 服务器。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": true
}
}
Listing 3-8Accepted response
另一方面,在 webhook 服务器中有两种方法可以拒绝请求。第一种方法只表示请求不是如下的allowed
。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"reason": "user has no access"
}
}
Listing 3-9Rejected response
当只有allowed
字段被设置为假时,也检查其他授权模块是否允许它。如果没有一个授权模块允许该请求,则 API 服务器会拒绝该请求。第二种方法是立即拒绝任何请求,绕过剩余的授权模块。响应数据与前一个相似,只是增加了一个简单的内容。
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"denied": true,
"reason": "user has no access"
}
}
Listing 3-10Rejected and denied response
Kubernetes API 和 authorization webhook 服务器之间的消息流可以总结在图 3-8 中。
图 3-8
授权消息流
尽管消息流看起来很简单,但是您将在 webhook 服务器中实现的逻辑是没有限制的。您可以设计一个授权系统来限制特定组的用户执行特定的操作。让我们假设你有两个团队,开发和生产,以及一个用于发布的持续部署(CD) 系统。可以创建一个授权 webhook,让开发团队只访问阅读框。类似地,您可以限制生产团队更新部署,并且只允许技术用户从 CD 到创建新的部署。考虑到团队成员是在任何其他外部系统(如 LDAP 或 GitHub)中定义的,webhook 服务器将拥有相关的逻辑并扩展 Kubernetes 授权。
在下面的练习中,您将创建一个无服务器授权 webhook,使 Kubernetes 中的名称空间成为只读的。用户只能读取、列出或查看资源,但不能在受保护的命名空间中更新、创建或删除资源。
EXERCISE: AUTHORIZATION WEBHOOK FOR READ-ONLY NAMESPACE
在本练习中,您将在 Google Cloud Functions 中开发一个无服务器 webhook。webhook 将做出授权决定,将名称空间protected
设为只读。然后,您将启动一个本地 minikube 集群,并将其配置为使用无服务器端点作为授权 webhook。
图 3-10
创建功能
图 3-9
GCP 云函数
在图 3-10 的“创建函数”视图中,填写名称字段并选择“允许未认证调用”,然后点击下一步。
打开谷歌云控制台,在主菜单中点击计算 ➤ 云功能。在图 3-9 的函数列表视图中点击“创建函数”。
在“代码”视图中,选择 Go as runtime,并用Authorize
填充“入口点”字段。当到达无服务器端点时,它是我们的部署中要调用的函数。用以下内容更改function.go
的内容:
package authorize
import (
"encoding/json"
"fmt"
"log"
"net/http"
authorization "k8s.io/api/authorization/v1beta1"
)
const NAMESPACE = "protected"
func Authorize(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var sar authorization.SubjectAccessReview
err := decoder.Decode(&sar)
if err != nil {
log.Println("[Error]", err.Error())
sar := new(authorization.SubjectAccessReview)
status := authorization.SubjectAccessReviewStatus{
Allowed: false,
Reason: err.Error(),
}
sar.Status = status
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(sar)
return
}
if sar.Spec.ResourceAttributes != nil {
v := sar.Spec.ResourceAttributes.Verb
n := sar.Spec.ResourceAttributes.Namespace
if n == NAMESPACE && (v == "create" || v == "delete" || v == "update") {
log.Printf("[Not Allowed] %s in namespace %s", sar.Spec.ResourceAttributes.Verb, NAMESPACE)
response := new(authorization.SubjectAccessReview)
status := authorization.SubjectAccessReviewStatus{
Allowed: false,
Denied: true,
Reason: fmt.Sprintf("%s is not allowed in the namespace: %s", sar.Spec.ResourceAttributes.Verb, NAMESPACE),
}
response.Status = status
json.NewEncoder(w).Encode(response)
return
}
}
response := new(authorization.SubjectAccessReview)
status := authorization.SubjectAccessReviewStatus{
Allowed: true,
}
response.Status = status
json.NewEncoder(w).Encode(response)
}
在这个文件中,只有一个名为Authorize
的函数。它是一个 HTTP 处理程序,用于解析传入的SubjectAccessReview
数据。如果传入的数据有ResourceAttributes
,它将检查名称空间是否为protected
,动词是否为create
、delete
或update
。当发现这样的请求时,它通过发送Allowed: false
和Denied: true
来拒绝。对于所有其他请求,它允许请求并让其他授权模块决定。
将go.mod
的内容更改如下:
module extend.k8s.io/authorize
go 1.13
require k8s.io/api v0.19.0
在function.go
中,我们使用的是 Kubernetes Go 客户端库;因此,我们将其列为对k8s.io/api
,版本v0.19.0
的依赖。
在图 3-11 中点击页面底部的部署。
图 3-11
功能的部署
图 3-12
成功部署
- 在图 3-12 中的功能列表视图中等待,直到旁边出现绿色复选标记。
用以下内容创建一个本地 webhook 配置文件serverless-authz.yaml
:
apiVersion: v1
kind: Config
clusters:
- name: serverless-authz
cluster:
server: SERVERLESS_ENDPOINT
users:
- name: authz-user
current-context: webhook
contexts:
- context:
cluster: serverless-authz
user: authz-user
name: webhook
不要忘记用步骤 2 中的 URL 更改SERVERLESS_ENDPOINT
。
图 3-13
功能日志
将文件移动到 minikube 文件:
$ mkdir -p $HOME/.minikube/files/var/lib/minikube/certs $ mv serverless-authz.yaml $HOME/.minikube/files/var/lib/minikube/certs/serverless-authz.yaml
使用额外的标志启动 minikube 集群:
该命令使用两个额外的配置参数启动本地 minikube 集群。第一个配置将 webhook 添加到授权模式中,第二个配置指出了第 3 步中配置文件的位置。
Check what the user is allowed to do with the following commands:
$ kubectl auth can-i create deployments --as developer yes
It shows that it is possible to create deployments in the default namespace.
$ kubectl auth can-i create deployments --as developer --namespace protected no - create is not allowed the namespace protected $ kubectl auth can-i delete secrets --as developer --namespace protected no - delete is not allowed in the namespace protected
However, it is not allowed to create deployments or delete secrets in the
protected
namespace. It ensures that the resources in the namespace stay as it is in a read-only mode.$ kubectl auth can-i list pods --as developer --namespace protected yes
另一方面,可以在
protected
名称空间中列出 pod,这是我们在只读模式下想要的。检查 Google Cloud 中的无服务器功能日志,并查看 webhook 的运行情况,如图 3-13 所示。
日志表明授权 webhook 不允许创建和删除请求。
在下一节中,我们将扩展 Kubernetes API 流的最后一个阶段:准入控制器。准入控制器是在将请求保存到etcd
存储器之前检查或改变请求的最后步骤。我们将学习准入 webhook 的设置以及如何动态定义来扩展和实现定制需求。
动态准入控制器
准入控制器是 Kubernetes API 流中对象持久化之前的最后一个阶段。这些控制器拦截验证或改变资源的请求。已经有各种准入控制器打包成kube-apiserver
二进制,增加了两个扩展点:MutatingAdmissionWebhook
和ValidatingAdmissionWebhook
。这些扩展点执行变异和验证准入控制 webhooks,这是在 Kubernetes API 中动态定义的。与身份验证和授权 webhooks 不同,您可以在集群启动和运行时创建、更新或删除准入控制器。因此,它们主要包含在“动态接纳控制器”一节中
准入网络挂钩对于 Kubernetes 控制平面及其操作至关重要。变异准入 webhooks 允许设置复杂的默认值或注入字段,而验证 webhooks 对于控制部署到集群中的内容至关重要。首先,变异的 web 钩子被串行调用,因为每个钩子都可以修改资源对象。然后,并行调用所有验证 webhooks 如果它们中的任何一个拒绝了请求,那么它就会被 API 服务器拒绝。
有两个方面需要配置和设置来扩展接纳控制机制:webhook 配置资源和 webhook 服务器。让我们首先关注 webhook 配置资源,让 Kubernetes API 知道在哪里以及何时调用 webhooks。
Webhook 配置资源
准入控制器的动态配置由ValidatingWebhookConfiguration
和MutatingWebhookConfiguration
API 资源处理。可以为 pod 创建定义一个验证 webhook 的示例,如下所示。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: "validation.extend-k8s.io"
webhooks:
- name: "validation.extend-k8s.io"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
scope: "Namespaced"
clientConfig:
url: "https://extend-k8s.io/validate"
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
Listing 3-11Webhook configuration example
API 资源有两个关键部分:rules
和clientConfig
。当 Kubernetes API 服务器收到一个符合规则的请求时,就会向在clientConfig
中定义的 webhook 发出一个 HTTP 请求。例如,使用清单 3-11 中的定义,当创建一个新的 pod 时,API 服务器将调用 https://extend-k8s.io/validate
。
变异 webhook 配置是用具有类似结构的MutatingWebhookConfiguration
资源完成的。创建机密时调用的示例 webhook 可以定义如下。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: "mutation.extend-k8s.io"
webhooks:
- name: "mutation.extend-k8s.io"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["secrets"]
scope: "Namespaced"
clientConfig:
service:
namespace: "extension"
name: "mutation-service"
caBundle: "Ci0tLS0tQk...tLS0K"
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
Listing 3-12Webhook configuration example
当接收到一个秘密创建请求时,Kubernetes API 服务器将到达在extension
名称空间中运行的mutation-service
的 443 端口。它将使用caBundle
来验证 webhook 服务器的 TLS 证书。在下一节中,我们将介绍准入控制器 webhooks 之间的消息流。
web 手册服务器
Kubernetes API 服务器发送一个带有AdmissionReview
对象的 POST 请求来定义请求及其属性。例如,当创建一个新的 pod 时,webhook 服务器将收到一个类似如下的项目。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "4b8bd269-bfc7-4dd5-8022-7ca57a334fa3",
"name": "example-app",
"namespace": "default",
"operation": "CREATE",
"kind": {"group":"","version":"v1","kind":"Pod"},
"requestKind": {"group":"","version":"v1","kind":"Pod"},
"resource": {"group":"","version":"v1","resource":"pods"},
"requestResource": {"group":"","version":"v1","resource":"pods"},
"object": {"apiVersion":"v1","kind":"Pod",...},
"userInfo": {
"username": "minikube",
"groups": ["system:authenticated"]
},
"options": {"apiVersion":"meta.k8s.io/v1","kind":"CreateOptions",...},
"dryRun": false
}
}
Listing 3-13AdmissionReview example
准入审查对象被合理地打包,因为它传输与请求和相关项目相关的所有信息。例如,在清单 3-13 中的request.object
字段中有一个完整的 pod 定义。Webhook 服务器需要再次发送一个加载了响应字段的AdmissionReview
对象。最小接受响应可以按如下方式构建。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": true
}
}
Listing 3-14Accepted admission review response
类似地,可以用以下数据发送一个简单的拒绝。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": false
}
}
Listing 3-15Rejected admission review response
变异的 webhooks 也可以修改请求中的对象。因此,webhook 服务器应该在AdmissionReview
响应中发送更改。Kubernetes 支持JSONPatch
种改变资源领域的操作。例如,将一个部署的副本更改为 5 的JSONPatch
可以如下构建:[{"op": "replace", "path": "/spec/replicas", "value": 5}]
。当补丁在AdmissionReview
内部传输时,将使用base64
进行如下编码。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": true,
"patchType": "JSONPatch",
"patch": "W3sib3AiOiAicmVwbGFjZSIsICJwYXRoIjogIi9zcGVjL3JlcGxpY2FzIiwgInZhbHVlIjogNX1d"
}
}
Listing 3-16Accepted admission review response with patch
当 Kubernetes API 获得带有补丁的响应时,它将在资源上应用更改,并继续处理下一个准入控制器。webhook 服务器和 Kubernetes API 之间的消息摘要可以总结在图 3-14 中。
图 3-14
准入 webhook 消息流
变异和验证 webhooks 都是 API 流中的关键组件,因为它们可以通过编程方式改变资源,接受或拒绝请求。当疏忽或设计不当使用时,它会很快造成混乱。对于可靠的准入控制设置,有三个最佳实践可以遵循:
幂等:变异的 webhooks 应该是幂等的;换句话说,可以多次调用 admission webhook,而不会改变第一次运行后的结果。
可用性:准入 webhooks 作为 Kubernetes API 操作的一部分被调用。因此,它们应该像所有其他 webhook 服务器一样,尽快评估并返回响应,以最小化总延迟。
死锁:如果 webhook 端点在集群内部运行,它们会干扰集群的资源,比如 pod、secrets 或 volumes。因此,建议不要在 webhook 的名称空间上运行准入控制器。
在以下练习中,您将首先创建一个动态验证准入 webhook 来检查和验证容器图像。在第二个练习中,您将把环境变量注入到在特定名称空间中运行的带有可变许可 webhooks 的 pod 中。
EXERCISE: VALIDATING WEBHOOK FOR CONTAINER IMAGE CHECK
在本练习中,您将在 Google Cloud Functions 中开发一个无服务器 webhook。webhook 将通过评估容器图像来验证 pod 创建请求。它将只允许包含 nginx 的容器图像,而拒绝所有其他容器。然后,您将启动一个 GKE 集群,并将集群的名称空间配置为使用验证 webhook。
图 3-15
GCP 云壳
创建一个名称空间,并将其标记为:
$ kubectl create namespace nginx-only namespace/nginx-only created $ kubectl label namespace nginx-only nginx=true namespace/nginx-only labeled
Create a file with the name
validating-webhook.yaml
with the following content:apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: nginx.validate.extend.k8s webhooks: - name: nginx.validate.extend.k8s namespaceSelector: matchLabels: nginx: "true" rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] scope: "Namespaced" clientConfig: url: https://us-central1-extend-k8s.cloudfunctions.net/validate admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None timeoutSeconds: 10
该文件将创建一个验证 webhook,当在标有
nginx=true
的名称空间中创建新的 pod 时,将调用该 web hook。不要忘记将url
更改为步骤 3 中复制的那个。Deploy the validating webhook conifguration with the following code:
$ kubectl apply -f validating-webhook.yaml validatingwebhookconfiguration.admissionregistration.k8s.io/nginx.validate.extend.k8s created
Create pod with
nginx
image in thenginx-only
namespace:$ kubectl run --generator=run-pod/v1 nginx --image=nginx --namespace=nginx-only pod/nginx created
由于许可 webhook 只允许运行
nginx
图像,因此创建了 pod。Create a pod with
busybox
image in thenginx-only
namespace:$ kubectl run --generator=run-pod/v1 busybox --image=busybox --namespace=nginx-only Error from server: admission webhook "nginx.validate.extend.k8s" denied the request without explanation
许可 webhook 拒绝了指定名称空间中的图像名称
busybox
。它显示了 webhook 服务器和 Kubernetes API 服务器都被正确地配置为扩展验证准入控制器。删除云功能和 Kubernetes 集群,避免额外的云费用:
$ gcloud container clusters delete test-validation --region=us-central1 The following clusters will be deleted. - [test-validation] in [us-central1] Do you want to continue (Y/n)? Y Deleting cluster test-validation...done. $ gcloud functions delete validate Resource [projects/extend-k8s/locations/us-central1/functions/validate] will be deleted. Do you want to continue (Y/n)? Y Waiting for operation to finish...done. Deleted
Create a folder named
validation
and change the directory into it:$ mkdir validation $ cd validation
Create a file named
function.go
in the terminal or open the editor inside Google Cloud Console. The file should have the following content:package validate import ( "encoding/json" "log" "net/http" "regexp" admission "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Validation(w http.ResponseWriter, r *http.Request) { ar := new(admission.AdmissionReview) err := json.NewDecoder(r.Body).Decode(ar) if err != nil { handleError(w, nil, err) return } response := &admission.AdmissionResponse{ UID: ar.Request.UID, Allowed: true, } pod := &corev1.Pod{} if err := json.Unmarshal(ar.Request.Object.Raw, pod); err != nil { handleError(w, ar, err) return } re := regexp.MustCompile(`(?m)(nginx|nginx:\S+)`) for _, c := range pod.Spec.Containers { if !re.MatchString(c.Image) { response.Allowed = false break } } responseAR := &admission.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: response, } json.NewEncoder(w).Encode(responseAR) } func handleError(w http.ResponseWriter, ar *admission.AdmissionReview, err error) { if err != nil { log.Println("[Error]", err.Error()) } response := &admission.AdmissionResponse{ Allowed: false, } if ar != nil { response.UID = ar.Request.UID } ar.Response = response json.NewEncoder(w).Encode(ar) }
该文件有一个名为
Validation
的 HTTP 处理程序来解析传入的AdmissionReview
对象并检查所有容器的图像。当它发现一个容器图像不符合nginx
时,它将直接拒绝审查并发送响应。否则,它将通过发送Allowed: true
来接受。Create another file named
go.mod
with the following content:module extend.k8s.io/validate go 1.13 require ( k8s.io/api v0.19.0 k8s.io/apimachinery v0.19.0 )
在
function.go
中,我们使用的是 Kubernetes Go 客户端库;因此,我们将其列为与k8s.io/api
和k8s.io/apimachinery
,版本v0.19.0
的依赖关系。Deploy the function with the following command:
$ gcloud functions deploy validate --allow-unauthenticated --entry-point=Validation --trigger-http --runtime=go113 .. entryPoint: Validation httpsTrigger: url: https://us-central1-extend-k8s.cloudfunctions.net/validate ... runtime: go113 ... status: ACTIVE timeout: 60s .. versionId: '1'
复制在以下步骤中使用的
httpsTrigger
URL。Create a Kubernetes cluster with the following command:
$ gcloud container clusters create test-validation --num-nodes=1 --region=us-central1 Creating cluster test-validation in us-central1... Cluster is being health-checked (master is healthy)... done. kubeconfig entry generated for test-validation. NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS test-validation us-central1 1.16.15-gke.4300 34.69.30.171 n1-standard-1 1.16.15-gke.4300 3 RUNNING
注意为了创建一个 Kubernetes 集群,您需要在云控制台 API 库视图中启用 Kubernetes 引擎 API,如果您之前没有这样做的话。
打开谷歌云控制台,点击导航栏中的激活云壳。它应该在你的浏览器中加载一个终端来运行如图 3-15 所示的命令。
EXERCISE: MUTATING WEBHOOK FOR ENVIRONMENT VARIABLE INJECTION
在本练习中,您将在 Google Cloud Functions 中开发一个无服务器 webhook。在创建新的 pod 时,webhook 将改变传入的请求。它将为在名为debug=true
的名称空间中创建的 pod 注入一个值为true
的环境变量DEBUG
。此外,您将启动并配置一个 GKE 集群,以查看 webhook 的运行情况。
图 3-16
GCP 云壳
Create a folder named
mutation
and change the directory into it:$ mkdir mutation $ cd mutation
Create a file named
function.go
in the terminal with the following content:package mutator import ( "encoding/json" "log" "net/http" admission "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Mutation(w http.ResponseWriter, r *http.Request) { ar := new(admission.AdmissionReview) err := json.NewDecoder(r.Body).Decode(ar) if err != nil { handleError(w, nil, err) return } pod := &corev1.Pod{} if err := json.Unmarshal(ar.Request.Object.Raw, pod); err != nil { handleError(w, ar, err) return } for i := 0; i < len(pod.Spec.Containers); i++ { pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, corev1.EnvVar{ Name: "DEBUG", Value: "true", }) } containersBytes, err := json.Marshal(&pod.Spec.Containers) if err != nil { handleError(w, ar, err) return } patch := []JSONPatchEntry{ { OP: "replace", Path: "/spec/containers", Value: containersBytes, }, } patchBytes, err := json.Marshal(&patch) if err != nil { handleError(w, ar, err) return } patchType := admission.PatchTypeJSONPatch response := &admission.AdmissionResponse{ UID: ar.Request.UID, Allowed: true, Patch: patchBytes, PatchType: &patchType, } responseAR := &admission.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: response, } json.NewEncoder(w).Encode(responseAR) } type JSONPatchEntry struct { OP string `json:"op"` Path string `json:"path"` Value json.RawMessage `json:"value,omitempty"` } func handleError(w http.ResponseWriter, ar *admission.AdmissionReview, err error) { if err != nil { log.Println("[Error]", err.Error()) } response := &admission.AdmissionResponse{ Allowed: false, } if ar != nil { response.UID = ar.Request.UID } ar.Response = response json.NewEncoder(w).Encode(ar) }
该函数有一个名为
Mutation
的 HTTP 处理程序来解析AdmissionReview
并准备响应。它首先向 pod 中的所有容器添加环境变量,然后创建一个JSONPatch
。最后,它发送一个带有补丁数据的批准的AdmissionReview
响应。Create another file named
go.mod
with the following content:module extend.k8s.io/mutate go 1.13 require ( k8s.io/api v0.19.0 k8s.io/apimachinery v0.19.0 )
在
function.go
中,我们使用的是 Kubernetes Go 客户端库;因此,我们将其列为与k8s.io/api
和k8s.io/apimachinery
,版本v0.19.0
的依赖关系。Deploy the function with the following command:
$ gcloud functions deploy mutate --allow-unauthenticated --entry-point=Mutation --trigger-http --runtime=go113 .. entryPoint: Mutation httpsTrigger: url: https://us-central1-extend-k8s.cloudfunctions.net/mutate ... runtime: go113 ... status: ACTIVE timeout: 60s .. versionId: '1'
复制步骤 6 中使用的
httpsTrigger
URL。使用以下命令创建一个 Kubernetes 集群:
$ gcloud container clusters create test-mutation --num-nodes=1 --region=us-central1 Creating cluster test-mutation in us-central1... Cluster is being health-checked (master is healthy)... done. kubeconfig entry generated for test-mutation. NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS test-mutation us-central1 1.16.15-gke.4300 34.122.242.6 n1-standard-1 1.16.15-gke.4300 3 RUNNING
创建一个名称空间,并将其标记为:
$ kubectl create namespace testing namespace/testing created $ kubectl label namespace testing debug=true namespace/testing labeled
Create a file with the name
mutating-webhook.yaml
with the following content. Do not forget to change thewith the URL from Step 3: apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: debug.mutate.extend.k8s webhooks: - name: debug.mutate.extend.k8s namespaceSelector: matchLabels: debug: "true" rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] scope: "Namespaced" clientConfig: url: <httpsTrigger> admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None timeoutSeconds: 10
当在标有
debug=true
的名称空间中创建新的 pod 时,该文件将创建一个可变的 webhook 来调用。Deploy the mutating webhook configuration with the following code:
$ kubectl apply -f mutating-webhook.yaml mutatingwebhookconfiguration.admissionregistration.k8s.io/debug.mutate.extend.k8s created
Create pod in
testing
namespace:$ kubectl run --generator=run-pod/v1 nginx --image=nginx --namespace testing pod/nginx created
Check for the environment variables in the
nginx
pod:$ kubectl --namespace testing exec nginx -- env | grep DEBUG DEBUG=true
pod 有
DEBUG=true
环境变量,它是由变异的 webhook 注入的。它显示了 webhook 服务器和 Kubernetes API 服务器都被正确地配置为扩展可变准入控制器。Create pod in
default
namespace:$ kubectl run --generator=run-pod/v1 nginx --image=nginx pod/nginx created
检查 nginx 窗格中的环境变量:
打开谷歌云控制台,点击导航栏中的激活云壳。它应该在你的浏览器中加载一个终端来运行如图 3-16 所示的命令。
$ kubectl exec nginx -- env | grep DEBUG
正如所料,在位于default
名称空间中的 pod 中没有发现环境变量。
删除云功能和 Kubernetes 集群,避免额外的云费用:
$ gcloud container clusters delete test-mutation --region=us-central1 The following clusters will be deleted. - [test-mutation] in [us-central1] Do you want to continue (Y/n)? Y Deleting cluster test-mutation...done. $ gcloud functions delete mutate Resource [projects/extend-k8s/locations/us-central1/functions/mutate] will be deleted. Do you want to continue (Y/n)? Y Waiting for operation to finish...done. Deleted
关键要点
对 Kubernetes API 的每个请求都要经过 Kubernetes API 流中的认证、授权和准入控制阶段。
Webhooks 可以扩展 Kubernetes API 流中的每个阶段。
身份验证 webhooks 支持使用自定义逻辑和外部系统验证无记名令牌。
Authorization webhooks 支持验证用户并控制谁可以访问集群中的哪些资源。
动态准入控制器可以修改相关资源并验证传入的 API 请求。
在下一章中,我们将使用自定义资源和自定义资源的自动化(即操作符)来扩展 Kubernetes API。
四、扩展 Kubernetes API
当我看着人类的大脑时,我仍然对它充满敬畏。
—本杰明“本”所罗门卡森
神经外科医生,美国政治家,作家
Kubernetes API 是云原生容器管理系统的大脑;它让你同时感到钦佩、尊重和惊讶。这是一个复杂的 API,有多层、各种资源,幸运的是,还有两个扩展点。本章将着重于通过创建定制资源和 API 聚合来扩展 Kubernetes API。在本章的最后,你将能够创建定制的资源和控制器;也就是说,您将实现一个 Kubernetes 操作符。此外,您将创建和部署扩展 API 服务器,并实际使用聚合 API。
让我们从 Kubernetes API 及其扩展点的概述开始。
kuble API 概述
Kubernetes API 是系统的核心基础。对集群的所有内部和外部操作都是对 API 服务器的请求。因此,Kubernetes 中的所有东西都是一个具有相应动作的 API 对象。官方的版本化参考文档包含了所有的 API 对象以及大量的信息和例子,比如 v1。19 参考。
API 是一个基于资源的接口,用于读取、创建、更新或删除资源。kube-apiserver
组件通过它的 HTTP REST 端点为 API 提供服务。因此,控制平面、节点或终端用户的每个动作都是对kube-apiserver
的一种 HTTP 调用。让我们假设您想要创建一个新的 pod。kubectl create
命令向 Kubernetes API 服务器发送一个带有 pod 定义有效负载的请求。该请求是对图 4-1 的引用中提到的/api/v1/namespaces/{namespace}/pods
端点的 HTTP POST。
图 4-1
Pod 创建参考
然后,kube-scheduler
将 pod 调度到一个节点。正如所料,调度不是命令性的命令,而是声明性的 Kubernetes 资源:Binding
。您可以创建一个带有节点和 pod 的Binding
请求,如下所示。
apiVersion: v1
kind: Binding
metadata:
name: pod-to-be-assigned
namespace: default
target:
apiVersion: v1
kind: Node
name: available-node
Listing 4-1Example Binding
kube-scheduler
通过 HTTP POST 请求向/api/v1/namespaces/{namespace}/bindings
端点发送绑定资源。然后kubelet
在节点上施展魔法,创建容器、附加卷并等待就绪。在此期间,kubelet
更新 pod 的状态,它是 Kubernetes 中的一个子资源。状态端点是/api/v1/namespaces/{namespace}/pods/{name}/status
,更新通过补丁请求发送。最后,使用kubectl get pods
命令列出本地工作站中的 pod。让我们用一些日志来调试这个命令,以检查它的 HTTP 请求。
$ kubectl get pods -v 9
...
* Starting client certificate rotation controller
* curl -k -v -XGET -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.19.0 (darwin/amd64) kubernetes/e199641" 'https://127.0.0.1:55000/api/v1/namespaces/default/pods?limit=500'
* GET https://127.0.0.1:55000/api/v1/namespaces/default/pods?limit=500 200 OK in 19 milliseconds
* Response Headers:
Cache-Control: no-cache, private
Content-Type: application/json
...
Listing 4-2Getting pods with additional logs
正如所料,这是一个指向/api/v1/namespaces/default/pods
地址的 GET 命令,用于列出默认名称空间中的 pod。如您所知,端点由两个主要部分构成:API 版本和组。
API 版本控制
Kubernetes 中有三个级别的 API 版本,具有以下特征:
稳定:稳定版本的名字是
vX
,其中X
是一个整数,比如v1
。正如所料,稳定的 API 端点提供了完善的特性,这些特性将存在于 Kubernetes 的后续版本中。Beta : Beta API 版本有一个包含
beta
的名字,比如v1beta1
。默认情况下,这些特性和资源都经过了很好的测试和启用。但是,对这些 API 的支持在即将发布的版本中可能会过时。因此,您应该在生产中非常小心地使用 beta APIs。Alpha : Alpha API 版本有一个包含
alpha
的名字,比如v1alpha1
。Alpha 特性是新的,可能包含一些错误。更重要的是,Kubernetes 可能会在不考虑向后兼容性的情况下放弃支持或更改 API。因此,您应该只在测试中使用 alpha APIs,而不是在生产中使用。
API 组
API 组打破了 API 服务器的整体结构,可以单独启用或禁用这些组。在 Kubernetes 中,有几个 API 组有两种命名约定:
遗留核心组有
apiVersion: v1
,由于历史原因位于/api/v1
。所有其他组以
apiVersion: $GROUP_NAME/$VERSION
命名,位于/apis/$GROUP_NAME/$VERSION
。例如,使用apps/v1
的apiVersion
如下构造部署对象。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 5
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14
Listing 4-3Example deployment
部署的 API 端点是/apis/apps/v1/namespaces/$NAMESPACE/deployments
,包括组名和版本。
对 Kubernetes API 的扩展主要集中在两个部分:添加新的端点和为位于端点的资源添加自定义实现逻辑。现在,让我们继续 Kubernetes API 的两个扩展点。
库 API 中的扩展点
Kubernetes API 是一个面向资源的 API,通过创建自定义资源可以进行扩展。当群集启动并运行时,可以动态地添加或删除自定义资源。启用自定义资源时,它具有与本地资源(如 pod)类似的功能。Kubernetes 提供了两种添加定制资源的方法。
自定义资源定义
CustomResourceDefinition
(CRD)是一个本地的 Kubernetes API 资源,用于定义自定义资源。在 CRD 中,您用名称、组、版本和模式来表示新的自定义资源。Kubernetes API 服务器为您的定制资源创建一个 REST 端点,并处理 API 操作,如创建、读取、更新和删除。像所有其他 Kubernetes 资源一样,自定义资源实例存储在etcd
中。
API 服务器聚合
Kubernetes 中的每个资源都有一个 REST 端点来处理 CRUD 操作。APIService
是一个本地 Kubernetes 资源,用于向组、版本和后端端点注册定制资源。您可以声明一个 URL 路径,比如/apis/k8s-extend.io/v1
,并让kube-apiserver
将请求委派到您的定制后端。
这两种方法的主要区别在于,CRD 通过在 Kubernetes API 中添加新的资源来扩展 Kubernetes API。另一方面,服务器聚合创建了由外部服务器处理的新资源。这两种方法如图 4-2 所示。
图 4-2
库比特 API 扩展
带有 CRDs 的定制资源是存储在 Kubernetes API 中的结构化数据。然而,它们的动力来自于定制控制器。控制器作用于定制资源的状态,并采取诸如创建、删除或更新之类的动作。带有控制器的自定义资源也被称为 CoreOs 在 2016 年首次定义的操作符模式。您可以创建自定义控制器来实现基于存储在自定义资源中的状态的业务逻辑。定制控制器和聚合服务器都需要与 Kubernetes API 服务器通信。因此,您需要开发符合 Kubernetes REST API 的应用。幸运的是,您不需要从头开始实现每个资源和请求,因为客户端库是可用的。
Kubernetes 客户库
Kubernetes 客户端库通过请求和响应来实现本地资源。此外,它们还处理日常任务,如身份验证、凭证发现和kubeconfig
读取。Go、Python、Java、Dotnet、JavaScript 和 Haskell 都有官方支持的客户端库。此外,还有许多由社区维护的客户端库,它们覆盖了不同的本地资源和关注领域。
Kubernetes 在官方文档中维护着客户端库的列表;但是,建议使用 Go 或 Python,因为它们拥有最活跃的社区。另外,Kubernetes 及其生态系统是在 Go 语言上开发的;因此,Go 客户端库是无可争议的赢家。在下面的练习中,您将使用 Go 客户端库,即client-go
,连接到一个 Kubernetes 集群。
EXERCISE: KUBERNETES GO CLIENT IN ACTION
在本练习中,您将使用client-go
为秘密创建一个自定义观察器。我们将从创建依赖文件和源代码开始。然后,我们将借助 Go 中的跨平台选项来构建二进制文件。最后,您将运行自定义观察器并查看它的运行情况。
注意为了继续这个练习,您需要一个正在运行的 Kubernetes 集群和一个kubeconfig
来访问。由minikube
创建的本地集群足以执行这些步骤。
Create a dependency file
go.mod
with the following content:module secret-watcher go 1.14 require ( k8s.io/apimachinery v0.19.0 k8s.io/client-go v0.19.0 )
文件由我们应用的需求组成。第一个是
apimachinery
,是提供资源定义的库。第二个是client-go
库,包括认证、实用程序和客户端命令。Create a file
secret_watcher.go
with the following content:package main import ( "context" "flag" "fmt" "path/filepath" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" _ "k8s.io/client-go/plugin/pkg/client/auth" ) func main() { // kubeconfig flag var kubeconfig *string if home := homedir.HomeDir(); home != "" { kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) path to the kubeconfig file") } else { kubeconfig = flag.String("kubeconfig", "", "path to the kubeconfig file") } flag.Parse() // create config config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) if err != nil { panic(err.Error()) } // create client set clientset, err := kubernetes.NewForConfig(config) if err != nil { panic(err.Error()) } // watch for secrets for { secrets, err := clientset.CoreV1().Secrets("").List(context.TODO(), metav1.ListOptions{}) if err != nil { panic(err.Error()) } fmt.Printf("There are %d secrets in the cluster\n", len(secrets.Items)) time.Sleep(10 * time.Second) } }
这是我们将构建并运行以与集群通信的主文件。如果没有使用默认目录,该函数开始解析
kubeconfig
标志。然后它使用client-go
库读取kubeconfig
。随后,创建一个由本地资源客户端组成的clientset
。最后,所有的秘密都被列出,并在一个无限循环中打印出计数。Start a Go build environment in Docker with the following command:
$ docker run -v "$(pwd)":/go/src/secret-watcher -it onuryilmaz/multi-platform-go-build:1.14-buster bash root@e45653990bb6:/go#
该命令将挂载当前的工作目录,并在容器内部启动一个交互式 bash。
Run the following command to build the binary:
$ cd src/secret-watcher/ $ export GOOS=darwin # for MacOS. Set to linux or windows based on your local operating system $ go build -v go: downloading k8s.io/apimachinery v0.19.0 go: downloading k8s.io/client-go v0.19.0 go: downloading github.com/google/gofuzz v1.1.0 go: downloading gopkg.in/inf.v0 v0.9.1 ... k8s.io/client-go/kubernetes/typed/storage/v1alpha1 k8s.io/client-go/kubernetes/typed/storage/v1 k8s.io/client-go/kubernetes/typed/storage/v1beta1 k8s.io/client-go/kubernetes secret-watcher
输出列表检索所有的依赖项,并最终构建二进制文件。使用
exit
命令从容器退出到本地工作站。运行
secret-watcher
二进制文件,设置kubeconfig
标志或者留空以使用默认位置:./secret-watcher There are 37 secrets in the cluster There are 37 secrets in the cluster There are 37 secrets in the cluster ...
secret-watcher
应用在一个无限循环中列出集群中的所有秘密,如输出所示。二进制文件的成功运行表明我们可以使用client-go
库创建一个定制的 Go 应用。此外,它还与集群进行通信,这表明集群配置、请求和响应工作正常。
在下一节中,我们将使用定制资源和控制器来扩展 Kubernetes API。我们将学习操作符模式的基础知识,然后创建自定义资源来扩展 Kubernetes API。然后我们将理解控制器的概念,并让 Kubernetes 为我们的定制资源和业务逻辑工作。
自定义资源定义和控制器
CustomResourceDefinition
(CRD)是在 Kubernetes API 中创建定制资源的简单方法。有了新的资源,Kubernetes API 被扩展来处理 REST 操作和etcd
中的存储。这意味着您可以创建、读取、更新或删除定制资源,最重要的是,您可以在它们之上创建自动化。因此,的想法是为 vanilla Kubernetes 中没有实现的业务需求创建定制资源。让我们假设您想要在 Kubernetes 上安装一个集群化和托管的数据库。您将部署机密、卷、配置、状态集和更多 Kubernetes 资源。此外,您希望运行一些业务逻辑,如数据库初始化、迁移或数据库升级。定制资源和控制器是以 Kubernetes 本地方式管理此类应用所遵循的设计模式。让我们从创建一些 CRD 来定义定制资源开始。
CRDs 类似于任何其他 Kubernetes 资源;它们是期望状态的声明性定义。在这种情况下,所需的状态是具有组名、版本、范围、模式和名称的新自定义资源。用于TimeseriesDB
资源的示例 CRD 可以被构造如下。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: timeseriesdbs.extend-k8s.io
spec:
group: extend-k8s.io
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
dbType:
type: string
replicas:
type: integer
status:
type: object
properties:
stage:
type: string
message:
type: string
scope: Namespaced
names:
plural: timeseriesdbs
singular: timeseriesdb
kind: TimeseriesDB
shortNames:
- tsdb
Listing 4-4TimeseriesDB CRD
在 CRD 的规格中,有四个块:
group
:多个定制资源可以被分组到一个 Kubernetes API 组中。该字段表示 API 组的名称。在 Kubernetes 中,资源的版本随着模式的变化而变化。在 CRD,提供了受支持的版本及其
schema
。scope
:定制资源实例可以位于Namespace
中,也可以位于Cluster
中。names
:复数、单数和种类字段是资源的名称,用于 REST 端点、资源定义文件和kubectl
命令。
CRD 资源的名称在metadata.name
中定义,其格式为<plural>.<group>
。此外,Kubernetes 将结构化数据存储在带有自定义字段的自定义资源中。字段的结构在模式字段中指定,并且是以 OpenAPI 规范 v3 的形式。0 。现在,将 CRD 保存在一个文件中,并将其部署到集群。
$ kubectl apply -f tsdb-crd.yaml
customresourcedefinition.apiextensions.k8s.io/timeseriesdbs.extend-k8s.io created
Listing 4-5Deployment of CRD
现在,您可以看到timeseriesdbs
被添加到集群中的 API 资源。
$ kubectl api-resources --output=name | grep timeseriesdbs
timeseriesdbs.extend-k8s.io
Listing 4-6API resources listing
此外,您可以运行kubectl
命令并与 Kubernetes API 交互TimeseriesDB
资源。让我们在启用一些日志记录的情况下尝试一下。
$ kubectl get timeseriesdb -v=6 | grep extend-k8s
Config loaded from file: ...
Starting client certificate rotation controller
GET https://127.0.0.1:55000/api?timeout=32s 200 OK in 18 milliseconds
GET https://127.0.0.1:55000/apis?timeout=32s 200 OK in 6 milliseconds
GET https://127.0.0.1:55000/apis/extend-k8s.io/v1?timeout=32s 200 OK in 11 milliseconds
GET https://127.0.0.1:55000/apis/autoscaling/v1?timeout=32s 200 OK in 10 milliseconds
...
GET https://127.0.0.1:55000/apis/storage.k8s.io/v1beta1?timeout=32s 200 OK in 20 milliseconds
GET https://127.0.0.1:55000/apis/extend-k8s.io/v1/namespaces/default/timeseriesdbs?limit=500 200 OK in 4 milliseconds
No resources found in default namespace.
Listing 4-7kubectl custom resource listing
Note
如果您没有看到 API 检索日志,这是因为kubectl
缓存了它们。您可以清除位于$HOME/的缓存目录。kube/cache 并重新运行该命令。
在日志中,kubectl
首先连接到api
和apis
端点,以发现可用的 API 组和版本。API 发现结果被本地缓存,这样在第二次运行时,kubectl
将直接调用/apis/extend-k8s.io/v1/namespaces/default/timeseriesdbs
。
随着 CRD 的创建,Kubernetes API 得到了扩展;因此,API 服务器和客户端工具都可以使用新的资源了。不出所料,TimeseriesDB
没有资源。现在,让我们继续在集群中创建定制资源。
或者您将创建的任何自定义资源与本地 Kubernetes 资源(如 pods 或 secrets)并无不同。对于timeseriesdbs.extend-k8s.io
CRD 和v1
模式,您可以创建以下资源。
apiVersion: extend-k8s.io/v1
kind: TimeseriesDB
metadata:
name: example-tsdb
spec:
dbType: InfluxDB
replicas: 4
status:
stage: Created
message: New TimeseriesDB
Listing 4-8TimeseriesDB example
example-tsdb
是创建一个有四个副本的 InfluxDB 数据库的定义。status
域解释了资源的当前情况。现在,让我们在图 4-3 中直观地匹配 CRD 和示例资源字段。
图 4-3
CRD 和定制资源
您可以使用以下命令将资源部署到集群。
$ kubectl apply -f example-tsdb.yaml
timeseriesdb.extend-k8s.io/example-tsdb created
Listing 4-9Custom resource deployment
您还可以使用 CRD 中定义的shortNames
来访问资源。
$ kubectl get tsdb NAME AGE
example-tsdb 1m
Listing 4-10Custom resource listing
现在,我们有一个定制的资源来管理我们的时间序列数据库。现在的关键问题是,谁将创建和管理四个实例 InfluxDB 到我们的集群中。同样,新版本发布时,谁来升级数据库?换句话说,需要有一个操作员来创建、更新、删除和管理应用。
Kubernetes 中的运算符模式
Kubernetes API 存储并提供定制资源。另一方面,操作员是创建和管理自定义资源中定义的应用的软件扩展。软件操作员背后的动机是取代人类操作员的知识和经验。在传统方法中,运营团队知道如何部署和管理应用。他们观察特定的指标或仪表板,以跟踪整个系统的状态,并在必要时采取行动。在云原生世界中,您被期望使用自动化来处理这样的操作。
运算符是将人类知识和任务实现到代码中的模式。该模式很好地集成到了 Kubernetes 中,因为它遵循了 Kubernetes 中流行的控制器扩展模式。运营商管理生产就绪的云原生应用有四个主要级别:
安装:自动安装应用,并在定制资源中定义所需的状态。
升级:应用的自动化和用户触发升级,用户交互最少。
生命周期管理:决策规则和自动化的应用的初始化、备份和故障恢复。
监控和可伸缩性:监控和分析应用的指标和警报。必要时,采取自动化措施进行扩展、计划和重新平衡。
操作员被部署到带有CustomResourceDefinition
和相关控制器的集群中。控制器在 Kubernetes 中作为容器化的应用运行,最常见的是作为部署运行。操作员应用与 Kubernetes API 交互;所以建议使用可以充当 Kubernetes 客户端的编程语言。开源和社区维护的运营商在 OperatorHub 共享,它有 175 个运营商可供使用。如果您计划将一个流行的数据库部署到 Kubernetes,比如 etcd、MongoDB、PostgreSQL 或 CockroachDB,那么您应该检查 OperatorHub 中的操作符。使用有社区支持的现成操作员可以帮助您节省时间和金钱;因此,它是有价值的。
如果您想开发自己的操作符,有两个基本工具需要考虑:
Operator SDK: 它是 Operator 框架的一部分,以有效的自动化方式创建 Kubernetes-native 应用。SDK 提供了构建、测试、打包和部署操作者到集群的工具。可以在 Operator SDK 中开发 Go、Ansible 或 Helm 图表。
kubebuilder: 这是一个使用 CRDs 构建 Kubernetes APIs 的框架。它关注于创建和部署 Kubernetes API 扩展的速度和降低的复杂性。该工具为 Go 中的自定义资源生成客户端、接口和 webhooks。它还生成将操作员部署到集群所需的资源。接下来,我们将关注 kubebuilder 框架,因为它有两个基本特性:临近 Kubernetes 社区和增强的开发人员体验。
kubebuilder 框架
kubebuilder
是一个框架,用于初始化、生成和部署 Kubernetes-native API 扩展代码到集群。框架是包含的电池,这样创建的项目就有了测试环境、部署文件和容器规范。本节将使用框架一步一步地为TimeseriesDB
定制资源生成一个项目,并查看它的运行情况。
Note
本节的其余部分将让您接触以下先决条件:Go 版本 1.14+,Docker 版本 17.03+,访问新的 Kubernetes 集群,以及kubectl
和 kustomize 。
让我们从将kubebuilder
二进制文件安装到本地工作站开始。
$ export os=$(go env GOOS)
$ export arch=$(go env GOARCH)
$ curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/
$ sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder
$export PATH=$PATH:/usr/local/kubebuilder/bin
Listing 4-11kubebuilder installation
这些命令为您的操作系统下载二进制文件,并将其安装到您的PATH
环境变量中。接下来,是时候创建一个新的 Go 项目了。
$ mkdir -p $GOPATH/src/extend-k8s.io/timeseries-operator
$ cd $GOPATH/src/extend-k8s.io/timeseries-operator
$ kubebuilder init --domain extend-k8s.io
Writing scaffold for you to edit...
Get controller runtime:
go get sigs.k8s.io/controller-runtime@v0.5.0
go: downloading sigs.k8s.io/controller-runtime v0.5.0
go: downloading k8s.io/apimachinery v0.17.2
...
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Next: define a resource with:
kubebuilder create api
Listing 4-12Initializing a project
这些命令在GOPATH
中创建一个文件夹,然后kubebuilder
通过创建一个 scaffold 项目进行初始化。使用以下命令检查文件夹的内容。
$ tree -a
.
├── .gitignore
├── Dockerfile
├── Makefile
├── PROJECT
├── bin
│ └── manager
├── config
│ ├── certmanager
│ │ ├── certificate.yaml
│ │ ├── kustomization.yaml
│ │ └── kustomizeconfig.yaml
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ ├── manager_webhook_patch.yaml
│ │ └── webhookcainjection_patch.yaml
│ ├── manager
│ │ ├── kustomization.yaml
│ │ └── manager.yaml
│ ├── prometheus
│ │ ├── kustomization.yaml
│ │ └── monitor.yaml
│ ├── rbac
│ │ ├── auth_proxy_client_clusterrole.yaml
│ │ ├── auth_proxy_role.yaml
│ │ ├── auth_proxy_role_binding.yaml
│ │ ├── auth_proxy_service.yaml
│ │ ├── kustomization.yaml
│ │ ├── leader_election_role.yaml
│ │ ├── leader_election_role_binding.yaml
│ │ └── role_binding.yaml
│ └── webhook
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── service.yaml
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
└── main.go
9 directories, 31 files
Listing 4-13Project structure
用最少的资源初始化项目来构建和运行一个操作符。大部分文件在config
文件夹中,格式为kustomize
,集成在kubectl
中的无模板定制方式。
通过创建资源和控制器将TimeseriesDB
API 添加到项目中。
$ kubebuilder create api --group operator --version v1 --kind TimeseriesDB
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/timeseriesdb_types.go
controllers/timeseriesdb_controller.go
Running make:
make
...bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Listing 4-14Adding new API
样板资源和控制器被添加到项目中。让我们首先检查位于/ api/v1/timeseriesdb_types.go
的资源定义。
type TimeseriesDBSpec struct {
...
Foo string `json:"foo,omitempty"`
}
type TimeseriesDBStatus struct {
...
}
// +kubebuilder:object:root=true
// TimeseriesDB is the Schema for the timeseriesdbs API
type TimeseriesDB struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TimeseriesDBSpec `json:"spec,omitempty"`
Status TimeseriesDBStatus `json:"status,omitempty"`
}
Listing 4-15Boilerplate TimeseriesDB resource
用下面的 Go 结构更新TimeseriesDBSpec
和TimeseriesDBStatus
,删除示例字段并存储实际数据。
// TimeseriesDBSpec defines the desired state of TimeseriesDB
type TimeseriesDBSpec struct {
DBType string `json:"dbType,omitempty"`
Replicas int `json:"replicas,omitempty"`
}
Listing 4-16TimeseriesDBSpec with actual fields
// TimeseriesDBStatus defines the observed state of TimeseriesDB
type TimeseriesDBStatus struct {
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
}
Listing 4-17TimeseriesDBStatus with actual fields
此外,更改kubebuilder
标志,在TimeseriesDB
定义之前将status
设置为subresource
。
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
Listing 4-18Status subresource flag
检查位于/controllers/timeseriesdb_controller.go
的控制器代码的Reconcile
方法。
func (r *TimeseriesDBReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
_ = context.Background()
_ = r.Log.WithValues("timeseriesdb", req.NamespacedName)
// your logic here
return ctrl.Result{}, nil
}
Listing 4-19Boilerplate TimeseriesDB controller
Kubernetes API 中的TimeseriesDB
实例的每个事务都将调用控制器的Reconcile
方法。用以下内容更新函数。
func (r *TimeseriesDBReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("timeseriesdb", req.NamespacedName)
timeseriesdb := new(operatorv1.TimeseriesDB)
if err := r.Client.Get(ctx, req.NamespacedName, timeseriesdb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
log = log.WithValues("dbType", timeseriesdb.Spec.DBType, "replicas", timeseriesdb.Spec.Replicas)
if timeseriesdb.Status.Status == "" || timeseriesdb.Status.Message == "" {
timeseriesdb.Status = operatorv1.TimeseriesDBStatus{Status: "Initialized", Message: "Database creation is in progress"}
err := r.Status().Update(ctx, timeseriesdb)
if err != nil {
log.Error(err, "status update failed")
return ctrl.Result{}, err
}
log.Info("status updated")
}
return ctrl.Result{}, nil
}
Listing 4-20Updated controller
更新后的协调器方法展示了如何使用由kubebuilder
生成的客户端检索TimeseriesDB
实例。检索到的对象的字段被打印到输出中。此外,如果状态部分为空,则填充该部分,并在集群中更新资源。
运行make
命令,用更新后的TimeseriesDB
资源生成客户机和样本文件。
$ make
.../controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Listing 4-21Code generation after resource update
kubebuilder
平台使用controller-gen
工具生成 YAML 格式的实用程序代码和 Kubernetes 资源文件。
将 CRD 安装到集群,并在本地运行控制器。
$ make install run
.../controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
...
kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/timeseriesdbs.operator.extend-k8s.io created
...
.../controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
...
INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"}
INFO setup starting manager
INFO controller-runtime.manager starting metrics server {"path": "/metrics"}
INFO controller-runtime.controller Starting EventSource {"controller": "timeseriesdb", "source": "kind source: /, Kind="}
INFO controller-runtime.controller Starting Controller {"controller": "timeseriesdb"}
INFO controller-runtime.controller Starting workers {"controller": "timeseriesdb", "worker count": 1}
Listing 4-22Running the controller locally
在日志中,安装了 CRD,然后控制器代码开始使用 Go 命令。Metrics server、events source、controllers 和 workers 按列出的顺序开始,我们的控制器现在已经准备好,正在等待资源更改。
用以下内容更新位于/config/sample/operator_v1_timeseriesdb.yaml
的示例TimeseriesDB
实例。
apiVersion: operator.extend-k8s.io/v1
kind: TimeseriesDB
metadata:
name: timeseriesdb-sample
spec:
dbType: Prometheus
replicas: 5
Listing 4-23Example TimeseriesDB instance
在另一个终端中,将示例自定义资源部署到集群。
$ kubectl apply -f config/samples/
timeseriesdb.operator.extend-k8s.io/timeseriesdb-sample created
Listing 4-24Deploying example TimeseriesDB
instance
在运行控制器的终端中,您现在应该看到下面两行。
INFO controllers.TimeseriesDB status updated {"timeseriesdb": "default/timeseriesdb-sample", "dbType": "Prometheus", "replicas": 5}
DEBUG controller-runtime.controller Successfully Reconciled {"controller": "timeseriesdb", "request": "default/timeseriesdb-sample"}
Listing 4-25Controller logs after custom resource creation
它显示了创建资源时调用了控制器的Reconcile
方法。让我们检查状态更新是否正确。
$ kubectl describe timeseriesdb timeseriesdb-sample
Name: timeseriesdb-sample
Namespace: default
...
Spec:
Db Type: Prometheus
Replicas: 5
Status:
Message: Database creation is in progress
Status: Initialized
Events: <none>
Listing 4-26Custom resource status
控制器更新状态字段,并显示控制器不处于只读模式,它可以对资源进行更改。
在使用kubebuilder
框架从头开始创建一个操作符之后,您对如何使用定制资源和控制器来扩展 Kubernetes API 有了一个印象。值得一提的是,在开发运营商的同时,有三个要点需要考虑:
声明式 : Kubernetes 有一个声明式 API,它的资源应该是一样的。您的定制资源和它们周围的控制器应该只读取
spec
和更新status
字段。如果您发现自己改变了控制器中的spec
,您需要修改您的定制资源和控制器逻辑。幂等:控制器做的改变应该是幂等的,原子的。当操作员窗格在协调过程中重新启动时,它可以防止您从头开始创建完整的数据库。
抵抗错误:控制器在集群内部或外部创建资源,因此,它容易出现错误、超时或取消。您需要考虑控制器的每个动作及其潜在的故障。Kubernetes-native 方法是使用回退策略重试,更新资源的状态,并在必要时发布事件。
接下来,我们将继续 Kubernetes API 中的第二个扩展点,以创建由外部服务器处理的新资源。
聚合的 API 和扩展服务器
聚合层用额外的 API 扩展了 Kubernetes,以提供超出 Kubernetes API 服务器所提供的内容。CRD 和 operator 模式的主要区别在于新资源不存储在 Kubernetes API 中。资源的请求被定向到外部服务器,并收集响应。虽然这种方法增加了灵活性,但也增加了操作的复杂性。通过聚合扩展 Kubernetes API 有三个基本要素:
聚合层:该层在
kube-apiserver
内部运行,代理新 API 类型的请求。APIService
资源:新的 API 类型由APIService
资源动态注册。扩展 API 服务器:扩展 API 服务器响应聚合层上代理的请求。
我们可以在图 4-4 中说明一个请求的流程,该流程从 Kubernetes API 开始,到扩展 API 服务器结束。传入请求的旅程从用户的身份验证和授权开始。然后,聚合层将请求定向到扩展 API 服务器。在扩展服务器中,针对 API 服务器对传入的请求进行身份验证。换句话说,扩展 API 服务器检查请求是否来自 Kubernetes API 服务器。然后,扩展服务器向原始用户验证请求的授权。最后,如果请求通过了所有阶段,那么它将被执行并存储在扩展服务器中。请求流显示,用扩展服务器扩展 Kubernetes API 服务器遵循 webhook 设计模式。
图 4-4
聚合 API 请求流
扩展服务器和新资源的动态配置由APIService
资源控制。APIService
资源由 API 组、版本和扩展服务器的端点组成。为backup.extend-k8s.io
组和v1
版本扩展 Kubernetes API 的示例APIService
资源可以如下构建。
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1.backup.extend-k8s.io
spec:
version: v1
group: backup.extend-k8s.io
groupPriorityMinimum: 2000
service:
name: extension-server
namespace: kube-extensions
versionPriority: 10
caBundle: "LS0tL...LS0K"
Listing 4-27Example APIService resource
该定义没有指定自定义资源的实际名称。相反,聚合层重定向所有到达/apis/backup.extend-k8s.io/v1/
端点的请求。扩展服务器管理 API 组中的所有定制资源。扩展服务器被指定为 Kubernetes 服务及其名称和命名空间。默认情况下,使用服务的 HTTPS 端口,通过 TLS 处理通信。扩展服务器需要使用由caBundle
中指定的 CA 证书签名的证书运行。
一个扩展 API 服务器的开发几乎和开发一个 Kubernetes API 服务器一样复杂,也就是kube-apiserver
。对于完整获取的具体例子,参考实现,您可以查看 Kubernetes 的 sample-apiserver 存储库。使用sample-apiserver
的推荐方式是派生存储库,修改 API 类型,并频繁地改变基础以跟踪改进和错误修复。
在本节中,我们将使用 apiserver-builder 从头开始生成和部署一个扩展 API 服务器。它是一个用于开发 API 服务器、客户端库和安装资源的完整框架。我们将使用该工具来初始化一个项目,添加定制的资源组和版本、代码生成以及集群部署。
Note
除了正在运行的 Kubernetes 集群之外,本节中的步骤还要求安装以下组件:Go v1.14+、Docker 版本 17.03+和 OpenSSL 1.1.1g+。
首先,让我们安装最新版本的apiserver-boot
工具。
$ export os=$(go env GOOS)
$ mkdir -p /tmp/apiserver
$ cd /tmp/apiserver
$ curl --output /tmp/apiserver/apiserver-builder-alpha.tar.gz -L https://github.com/kubernetes-sigs/apiserver-builder-alpha/releases/download/v2.0.0-alpha.0/apiserver-builder-alpha-v2.0.0-alpha.0-${os}-amd64.tar.gz
$ tar -xf /tmp/apiserver/ apiserver-builder-alpha.tar.gz
$ chmod +x /tmp/apiserver/bin/apiserver-boot
$ mkdir -p /usr/local/apiserver-builder/bin
$ mv /tmp/apiserver/bin/apiserver-boot /usr/local/apiserver-builder/bin/apiserver-boot
$ export PATH=$PATH:/usr/local/apiserver-builder/bin
Listing 4-28apiserver-boot installation
您可以通过运行以下命令来验证安装。
$ apiserver-boot version
Version: version.Version{ApiserverBuilderVersion:"8f12f3e43", KubernetesVendor:"kubernetes-1.19.2", GitCommit:"8f12f3e43cb0a75c82e8a6b316772a230f5fd471", BuildDate:"2020-11-04-20:35:32", GoOs:"darwin", GoArch:"amd64"}
Listing 4-29apiserver-boot version check
让我们从在GOPATH
中创建一个文件夹开始。
$ mkdir -p $GOPATH/src/extend-k8s.io/timeseries-apiserver
$ cd $GOPATH/src/extend-k8s.io/timeseries-apiserver
Listing 4-30Go project initialization
使用apiserver-boot
创建一个 scaffold API 服务器。
$ apiserver-boot init repo --domain extend-k8s.io
Writing scaffold for you to edit...
Listing 4-31API server initialization
现在,用下面的命令检查生成的文件。
$ tree -a
.
├── .gitignore
├── BUILD.bazel
├── Dockerfile
├── Makefile
├── PROJECT
├── WORKSPACE
├── bin
├── cmd
│ ├── apiserver
│ │ └── main.go
│ └── manager
│ └── main.go -> ../../main.go
├── go.mod
├── hack
│ └── boilerplate.go.txt
├── main.go
└── pkg
└── apis
└── doc.go
7 directories, 12 files
Listing 4-32Folder structure
生成的代码是一个基本的 API 服务器,带有工具,如Makefile
和bazel
文件。
使用以下命令向扩展服务器添加自定义资源和控制器。
$ apiserver-boot create group version resource --group backup --version v1 --kind TimeseriesDBBackup
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
controllers/backup/timeseriesdbbackup_controller.go
Listing 4-33Adding custom resource
运行代码生成工具,确保新资源和控制器按预期工作。
$ make generate
...controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Listing 4-34Code generation for new resource and controller
首先,导出$REPOSITORY 环境变量作为 Docker 存储库,然后为扩展服务器构建容器映像。
$ apiserver-boot build container --image $REPOSITORY/timeseries-apiserver:v1
Will build docker Image from directory /var/folders/nn/.../T/apiserver-boot-build-container656172154
Writing the Dockerfile.
Building binaries for Linux amd64.
CGO_ENABLED=0
GOOS=linux
GOARCH=amd64
go build -o /var/folders/nn/.../T/apiserver-boot-build-container656172154/apiserver cmd/apiserver/main.go
go build -o /var/folders/nn/.../T/apiserver-boot-build-container656172154/controller-manager cmd/manager/main.go
Building the docker Image using /var/folders/nn/.../T/apiserver-boot-build-container656172154/Dockerfile.
docker build -t $REPOSITORY /timeseries-apiserver:v1 /var/folders/nn/../T/apiserver-boot-build-container656172154
Sending build context to Docker daemon 102.2MB
Step 1/5 : FROM ubuntu:14.04
---> df043b4f0cf1
Step 2/5 : RUN apt-get update
---> Using cache
---> 60dfe53c07c6
Step 3/5 : RUN apt-get install -y ca-certificates
---> Using cache
---> ff5f3be9ac8d
Step 4/5 : ADD apiserver .
---> 3778467102f7
Step 5/5 : ADD controller-manager .
---> 32672f63fd9b
Successfully built 32672f63fd9b
Successfully tagged $REPOSITORY/timeseries-apiserver:v1
Listing 4-35Container build
将 Docker 容器推到注册表中,以便在 Kubernetes 集群中使用。
$ docker push $REPOSITORY/timeseries-apiserver:v1
Listing 4-36Container push
使用以下命令部署扩展 API 服务器。
$ apiserver-boot run in-cluster --name timeseriesdb-api --namespace default --image $REPOSITORY/timeseries-apiserver:v1 --build-image=false
openssl req -x509 -newkey rsa:2048 -addext basicConstraints=critical,CA:TRUE,pathlen:1 -keyout config/certificates/apiserver_ca.key -out config/certificates/apiserver_ca.crt -days 365 -nodes -subj /C=un/ST=st/L=l/O=o/OU=ou/CN=timeseriesdb-api-certificate-authority
Generating a RSA private key
.+++++
.................................+++++
writing new private key to 'config/certificates/apiserver_ca.key'
...
Adding APIs:
backup.v1
...
kubectl apply -f config
deployment.apps/timeseriesdb-api-apiserver created
secret/timeseriesdb-api created
service/timeseriesdb-api created
apiservice.apiregistration.k8s.io/v1.backup.extend-k8s.io created
deployment.apps/timeseriesdb-api-controller created
statefulset.apps/etcd created
service/etcd-svc created
clusterrole.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-delegator created
clusterrole.rbac.authorization.k8s.io/timeseriesdb-api-controller created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-controller created
Listing 4-37Deployment of extension server
该命令处理一系列自动化操作,以创建 TLS 证书、添加 API 和部署一长串 Kubernetes YAML 文件。现在,是时候检查定制资源 API 是否已启用并按预期运行了。
让我们从描述新的APIService
资源开始。
$ kubectl describe apiservice v1.backup.extend-k8s.io
Name: v1.backup.extend-k8s.io
Namespace:
...
Status:
Conditions:
...
Message: all checks passed
Reason: Passed
Status: True
Type: Available
Events: <none>
Listing 4-38APIService status
Message
字段表示所有检查通过,同时APIService
列为Available
。在example-tsdb-backup.yaml
中创建一个包含以下内容的示例TimeseriesDBBackup
资源。
apiVersion: backup.extend-k8s.io/v1
kind: TimeseriesDBBackup
metadata:
name: example-tsdb-backup
Listing 4-39Example TimeseriesDBBackup
将示例资源部署到集群并检索回来。
$ kubectl apply -f example-tsdb-backup.yaml
timeseriesdbbackup.backup.extend-k8s.io/example-tsdb-backup created
$ kubectl get TimeseriesDBBackups
NAME CREATED AT
example-tsdb-backup 2021-07-28T09:05:00Z
Listing 4-40Create and read of the custom resource
输出显示,我们可以与 Kubernetes API 进行交互,以获得由 aggregated API 扩展的定制资源。
最后一步是更进一步检查etcd
中扩展资源的物理数据。Kubernetes API 服务器使用etcd
作为它的数据库。类似地,扩展 API 服务器与其数据库交互来存储定制资源。etcd
部署在扩展 API 服务器旁边,它应该有一个 pod 正在运行并且可以访问。
$ kubectl exec -it etcd-0 -- sh
/ # ETCDCTL_API=3 etcdctl get --prefix /registry
/registry/sample-apiserver/backup.extend-k8s.io/timeseriesdbbackups/example-tsdb-backup
{"kind":"TimeseriesDBBackup","apiVersion":"backup.extend-k8s.io/v1","metadata":{"name":"example-tsdb-backup","uid":"...","creationTimestamp":"...","annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"backup.extend-k8s.io/v1\",\"kind\":\"TimeseriesDBBackup\",\"metadata\":{\"annotations\":{},\"name\":\"example-tsdb-backup\"}}\n"}},"spec":{},"status":{}}
Listing 4-41Access to etcd
来自etcd
的数据显示扩展 API 服务器存储了TimeseriesDBBackup
个实例。此外,使用kubectl
表明 Kubernetes API 是用 aggregated API 方法扩展的。
与使用 CRDs 相比,创建和部署独立的服务器来处理 API 请求并不简单。然而,用聚合服务器扩展 Kubernetes API 比 CRDs 有以下两个好处:更灵活的资源验证检查和对客户机的协议缓冲支持。另一方面,在部署扩展服务器时,有三个要点需要考虑:
新的故障点:聚合 API 服务器根据业务逻辑和需求自行运行。它应该设计得很好,操作时要非常小心,不要在集群中出现单点故障。
存储:聚合的 API 服务器可以选择如何存储数据。应该考虑初始化、备份、恢复和存储容量。
安全和审计:聚合服务器的认证、授权和审计设置应该与主 API 服务器保持一致。
关键要点
Kubernetes API 是系统的核心结构,集群上的所有操作都作为对 API 服务器的请求来处理。
CustomResourceDefinition
(CRD)是向 Kubernetes API 添加新的定制资源的简单方法。它处理由 CRDs 创建的资源的 REST 操作和存储。当自定义资源与自定义控制器相结合时,就有可能将人类的知识和任务实现为代码,即操作符模式。
聚合层用额外的 API 扩展了 Kubernetes,以提供超出 Kubernetes API 服务器所提供的内容。Kubernetes API 代理聚合 API 资源的请求。
在下一章中,我们将通过运行多个调度器和开发定制的调度器来扩展 Kubernetes 调度。
五、调度扩展
行动表达轻重缓急。
—圣雄甘地
印度律师、政治家、社会活动家、作家
调度器是 Kubernetes 的核心部分,用于将工作负载分配给集群中的节点。分配操作基于集群管理员和操作员设置的优先级和规则。本章将着重于通过创建定制调度程序和开发扩展来扩展 Kubernetes 调度程序。在本章的最后,您将在集群中同时运行多个调度程序。此外,您将通过创建调度程序扩展程序来干预调度决策。
让我们从 Kubernetes 调度器及其扩展点的概述开始。
库调度程序概述
Kubernetes 调度程序在控制平面中运行,并将 pod 分配给节点。默认行为是平衡节点的资源利用率,同时应用集群中资源的规则和优先级。调度器的原理遵循 Kubernetes 的控制器设计模式。它会观察新创建的 pod,并在集群中找到最佳节点。
让我们通过从头创建一个多节点集群和一个 pod 来看看 Kubernetes 调度程序的运行情况。
$ minikube start --nodes 5
Listing 5-1Starting a multi-node local cluster
该命令将创建一个包含五个节点的本地集群,您可以使用以下命令列出这些节点。
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready master 7m17s v1.19.2
minikube-m02 Ready <none> 5m43s v1.19.2
minikube-m03 Ready <none> 4m10s v1.19.2
minikube-m04 Ready <none> 2m22s v1.19.2
minikube-m05 Ready <none> 34s v1.19.2
Listing 5-2Node listing
现在,让我们创建一个 pod 并等待它运行。
$ kubectl run nginx-1 --image=nginx
pod/nginx-1 created
$ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
nginx-1 0/1 Pending 0 0s
nginx-1 0/1 Pending 0 0s
nginx-1 0/1 ContainerCreating 0 0s
nginx-1 1/1 Running 0 16s
Listing 5-3Pod creation
你将在几秒钟内看到Pending
、ContainerCreating
和Running
阶段。调度的关键步骤是Pending
。它表明 Kubernetes API 接受了 pod,但是它还没有被安排到集群节点。让我们检查事件,以找到任何与调度相关的信息。
$ kubectl get events
...
2m54s Normal Scheduled pod/nginx-1 Successfully assigned default/nginx-1 to minikube-m05
...
Listing 5-4Event listing
您将看到kube-scheduler
已经选择了minikube-m05
节点。让我们深入了解一下kube-scheduler
的内部结构,了解更多关于决策是如何做出的。
调度框架
调度框架是 Kubernetes 调度器的架构。它是一个可插入的框架,插件在其中实现调度特性。框架中的顺序工作流有多个步骤,如图 5-1 所示。工作流程主要分为调度和绑定两种。调度的重点是寻找最佳节点,而绑定处理 Kubernetes API 操作来完成调度。
图 5-1
调度框架
每个步骤都有不言自明的名称,但是有一些要点需要考虑:
QueueSort
:在kube-scheduler
的等待队列中对待调度的 pod 进行排序。PreFilter
:检查与调度周期相关的 pod 的条件和信息。Filter
:过滤节点,通过使用插件和调用外部调度程序扩展程序,为 pod 找到合适的节点列表。PostFilter
:如果没有可行节点,则运行可选步骤。在一个典型的场景中,PostFilter
将导致其他 pod 的抢占,从而为调度打开一些空间。PreScore
:为评分插件创建一个可共享状态。Score/Prioritize
:通过调用各个评分插件和调度器扩展程序,对过滤后的节点进行排名。综合多个来源的得分,计算出最终排名。具有最高加权分数的节点将赢得 pod。
Reserve/Unreserve
:通知插件关于所选节点的信息是一个可选步骤。Permit
:批准、拒绝或暂停(超时)调度决策。PreBind
:在将 pod 绑定到节点之前,执行所需的任何工作,例如提供网络卷并安装它。Bind
:该步骤只由一个插件处理,因为它需要将决策发送给 Kubernetes API。PostBind
:通知装订循环结果的可选信息步骤。
一个插件可以在多个工作流点注册并执行调度子任务。虽然框架和插件创建了一个开放的架构,但是所有的插件都被编译成了kube-scheduler
二进制文件。您可以从参考文档中查看可用插件的列表。如果你想改变控制平面中运行的kube-scheduler
的配置,这是终极知识。
我们已经看到了调度程序的运行,并对其架构有所了解。现在我们将继续定义扩展点以及如何使用它们。
扩展点
您可以用四种主要方法定制或扩展 Kubernetes 调度程序。
第一种方式是克隆修改上游kube-scheduler
代码。然后,您需要编译、封装和运行,而不是在控制平面中部署kube-scheduler
。然而,这并不那么简单,而且需要付出巨大的努力,在下一个版本中调整上游代码的变化。
第二种方式是为kube-scheduler
内部的调度框架开发插件。这不是第一种方法 hacky ,但是同样,它需要和第一种方法一样多的努力,因为您需要更新、编译和维护上游kube-scheduler
存储库的变更。
第三种方法是在集群中运行一个单独的调度程序和默认的调度程序。在PodSpec
中有一个特定的字段来定义调度器:schedulerName
。如果该字段为空,则将其设置为default-scheduler
,并由kube-scheduler
处理。因此,可以运行第二个调度程序,并在schedulerName
字段中指定它。然后,自定义调度程序会将 pod 分配给节点。这种方法实现了控制器 Kubernetes 设计模式。它将监视具有特定schedulerName
的 pod,并为它们分配一个节点。
第四种也是最后一种方法是开发和运行调度程序扩展程序。调度器扩展器是外部服务器,Kubernetes 调度器在调度框架的特定步骤调用它们。这种方法类似于调度框架插件,但是扩展程序是带有 HTTP 端点的外部服务。因此,扩展程序实现了 webhook Kubernetes 设计模式。
前两个扩展方法不是真正的扩展点,因为它们修改了 vanilla Kubernetes 组件。因此,在这一章中,我们将关注最后两种方式:多调度器和调度器扩展程序。我们可以在图 5-2 中说明这两种方法与 Kubernetes 调度程序的交互。
图 5-2
立方调度程序扩展点
三个阶段与调度程序扩展程序交互:Filter
、Prioritize
和Bind
。因此,使用扩展器在kube-scheduler
的规则内操作是有益的。如果您正在寻求更大的灵活性,选择运行自定义调度程序是明智的。自定义调度程序是外部应用,因此它们不限于调度框架的流程和请求。
在接下来的小节中,您将了解这两种方法的细节,并看到它们的实际应用。
配置和管理多个调度程序
Kubernetes scheduler 以其精细的架构和丰富的配置功能将 pod 分配给节点。但是,如果默认计划程序不符合您的要求,可以创建一个新的计划程序并同时运行它们。多调度器的基本思想是基于 pod 规范中的一个字段:schedulerName
。如果指定了字段,则 pod 由相应的调度程序调度。另一方面,如果没有设置,默认调度程序将调度 pod。
让我们从运行minikube start --nodes 5
创建一个多节点集群开始,如果您还没有启动和运行集群的话。然后,您可以创建一个 pod 并检查它是否为schedulerName
。
$ kubectl run nginx-by-default-scheduler --image=nginx
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-by-default-scheduler 1/1 Running 0 99s
$ kubectl get pods nginx-by-default-scheduler -o jsonpath="{.spec.schedulerName}"
default-scheduler
Listing 5-5Pod with the default scheduler
当您在没有指定schedulerName
字段的情况下创建时,它由缺省值填充,然后由缺省调度程序分配。现在,让我们创建另一个由定制调度程序处理的 pod。
$ kubectl run nginx-by-custom-scheduler --image=nginx --overrides='{"spec":{"schedulerName":"custom-scheduler"}}'
pod/nginx-by-custom-scheduler created
$ kubectl get pods nginx-by-custom-scheduler
NAME READY STATUS RESTARTS AGE
nginx-by-custom-scheduler 0/1 Pending 0 16s
Listing 5-6Pod with a custom scheduler
pod 处于Pending
状态,因为没有调度程序来处理它。现在是时候为集群部署第二个调度程序来处理schedulerName
字段等于custom-scheduler
的 pod 了。
在custom-scheduler
中,我们将禁用上游调度程序中启用的所有 beta 功能。调度程序将在kube-scheduler
旁边的kube-system
名称空间中运行。创建一个名为kube-scheduler-custom.yaml
的文件,内容如下。
apiVersion: v1
kind: Pod
metadata:
name: kube-scheduler-custom
namespace: kube-system
spec:
containers:
- name: kube-scheduler-custom
image: k8s.gcr.io/kube-scheduler:v1.19.0
command:
- kube-scheduler
- --kubeconfig=/etc/kubernetes/scheduler.conf
- --leader-elect=false
- --scheduler-name=custom-scheduler
- --feature-gates=AllBeta=false
volumeMounts:
- mountPath: /etc/kubernetes/scheduler.conf
name: kubeconfig
readOnly: true
nodeName: minikube
restartPolicy: Always
volumes:
- hostPath:
path: /etc/kubernetes/scheduler.conf
type: FileOrCreate
name: kubeconfig
Listing 5-7Custom scheduler pod definition
pod 是运行k8s.gcr.io/kube-scheduler:v1.19.0
映像并将kubeconfig
附加为只读卷的坦率定义。以下三个标志定义了自定义计划程序的功能:
leader-elect=false
在运行调度程序之前禁用领导者选举阶段,因为只有一个自定义调度程序实例会运行。scheduler-name=custom-scheduler
定义调度程序的名称。feature-gates=AllBeta=false
禁用所有测试版功能。
使用kubectl apply -f kube-scheduler-custom.yaml
文件创建部署并检查 pod 状态。
$ kubectl -n kube-system get pods kube-scheduler-custom
NAME READY STATUS RESTARTS AGE
kube-scheduler-custom 1/1 Running 0 24s
Listing 5-8Custom scheduler pod in the cluster
现在,检查我们的 pod 的状态,它卡在 Pending 中。
$ kubectl get pods nginx-by-custom-scheduler
NAME READY STATUS RESTARTS AGE
nginx-by-custom-scheduler 1/1 Running 0 42s
Listing 5-9Pod assignment
pod 处于Running
阶段,这意味着定制调度程序可以完美地工作。创建定制调度器可能不是每个云工程师日常工作的一部分,因为默认的 Kubernetes 调度器在大多数情况下都工作得很好。然而,当您需要实现更复杂的需求时,您将创建您的定制调度程序并将其部署到集群中。让我们假设您想要创建一个调度程序来最小化成本。在您的自定义调度程序中,您可能需要首先将 pod 分配给最便宜的节点。相反,您可以创建一个自定义调度程序,在选择节点时考虑监视指标。在这种情况下,您可能需要将 pod 分发到节点,以最小化系统中的总延迟。然而,最小化成本或优化延迟取决于外部系统,而不在默认的 Kubernetes 调度程序的范围内。
运行多个调度器并用它们想要的调度器标记 pod 是一种简单的 Kubernetes-native 方法。最重要的部分是开发一个防弹调度程序。创建和操作自定义调度程序时,有三个关键点需要考虑:
Kubernetes API 兼容性 : Scheduler 与 Kubernetes API 交互,以观察 pod、检索节点列表并创建绑定。因此,您需要开发与 Kubernetes API 版本兼容的定制调度程序。如果您使用的是官方客户端库,幸运的是,您只需要使用正确的版本。
高可用性:如果您的调度程序停止运行或出现故障,将导致 pod 处于挂起状态。因此,您的应用将不会在集群中运行。因此,您需要将应用设计为高可用性运行。
配合默认调度器:如果集群中有不止一个决策者,您需要小心冲突的决策。例如,默认调度程序控制资源请求和限制。如果您的自定义计划程序在不考虑群集资源的情况下填充节点,默认计划程序的窗格可能会移动到其他节点。因此,您的定制调度程序应该与默认调度程序配合良好,并避免决策冲突。
在下面的练习中,您将使用kubebuilder
从头开始创建一个定制调度程序。此外,您将在集群中运行它,并将一些 pod 分配给节点。
EXERCISE: DEVELOPING A CUSTOM SCHEDULER WITH KUBEBUILDER
在本练习中,您将使用kubebuilder
创建一个定制的混沌调度器。本质上,调度程序是监视集群中的 pod 的控制器。因此,您将创建一个控制器并实现协调方法。最后,您将运行控制器并看到它的运行。
注意剩下的练习是基于kubebuilder
的,它需要以下先决条件:kubebuilder
v2.3.1,Go 版本 v1.14+,访问一个 Kubernetes 集群,以及kubectl
。
检索节点列表。
随机选择一个节点。
创建一个包含节点和 pod 的绑定资源。
将绑定资源发送给 Kubernetes API。
Initialize the project structure with the following commands:
$ mkdir -p $GOPATH/src/extend-k8s.io/chaos-scheduler $ cd $GOPATH/src/extend-k8s.io/chaos-scheduler $ kubebuilder init Writing scaffold for you to edit... Get controller runtime: $ go get sigs.k8s.io/controller-runtime@v0.5.0 Update go.mod: $ go mod tidy Running make: $ make .../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." go fmt ./... go vet ./... go build -o bin/manager main.go Next: define a resource with: $ kubebuilder create api
这些命令创建一个文件夹,并用样板代码引导项目。
Create controller for watching the pods with the following command:
$ kubebuilder create api --kind Pod --group core --version v1 Create Resource [y/n] n Create Controller [y/n] y Writing scaffold for you to edit... controllers/pod_controller.go Running make: $ make .../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." go fmt ./... go vet ./... go build -o bin/manager main.go
由于 pods 已经是 Kubernetes 资源,选择
no
跳过Create Resource
提示。但是,接受第二个提示,为 pod 资源生成一个控制器。打开位于
controllers
文件夹的pod_controlller.go
。你会看到两个功能SetupWithManager
和Reconcile
。SetupWithManager
是控制器启动时调用的函数。Reconcile
是由集群中每个观察到的变化调用的函数。Change the
SetupWithManager
function with the following content:func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { filter := predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { pod, ok := e.Object.(*corev1.Pod) if ok { if pod.Spec.SchedulerName == "chaos-scheduler" && pod.Spec.NodeName == "" { return true } return false } return false }, UpdateFunc: func(e event.UpdateEvent) bool { return false }, DeleteFunc: func(e event.DeleteEvent) bool { return false }, } return ctrl.NewControllerManagedBy(mgr). For(&corev1.Pod{}). WithEventFilter(filter). Complete(r) }
它添加了一个过滤器来观察带有
schedulerName chaos-scheduler
和空nodeName
的 pod 的创建事件。Change the
Reconcile
function with the following content:func (r *PodReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("pod", req.NamespacedName) nodes := new(corev1.NodeList) err := r.Client.List(ctx, nodes) if err != nil { return ctrl.Result{Requeue: true}, err } node := nodes.Items[rand.Intn(len(nodes.Items))].Name log.Info("scheduling", "node", node) binding := new(corev1.Binding) binding.Name = req.Name binding.Namespace = req.Namespace binding.Target = corev1.ObjectReference{ Kind: "Node", APIVersion: "v1", Name: node, } err = r.Client.Create(ctx, binding) if err != nil { return ctrl.Result{Requeue: true}, err } return ctrl.Result{}, nil }
更新后的
Reconcile
功能执行以下操作:
选择一个随机节点是调度器制造混乱的基本部分。它将测试 Kubernetes 在动荡和意外条件下的能力和恢复力。
注混沌工程是一种在不断变化的情况下(即混沌)对系统进行实验的常用方法。该方法对大规模和分布式应用进行试验,以建立对弹性和复原力的信心。
将以下库添加到pod_controlller.go
的导入列表中:
Start the controller with the following command:
$ make run ../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." go fmt ./... go vet ./... ../bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases go run ./main.go INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"} INFO setup starting manager INFO controller-runtime.manager starting metrics server {"path": "/metrics"} INFO controller-runtime.controller Starting EventSource {"controller": "pod", "source": "kind source: /, Kind="} INFO controller-runtime.controller Starting Controller {"controller": "pod"} INFO controller-runtime.controller Starting workers {"controller": "pod", "worker count": 1}
如日志所示,控制器启动并等待集群中 pod 的事件。
在另一个终端,创建一个由
chaos-scheduler
:$ kubectl run nginx-by-chaos-scheduler --image=nginx --overrides='{"spec":{"schedulerName":"chaos-scheduler"}}' pod/nginx-by-chaos-scheduler created
安排的 pod
Check the logs of controller started in Step 4:
... INFO controllers.Pod scheduling {"pod": "default/nginx-by-chaos-scheduler", "node": "minikube-m02"} DEBUG controller-runtime.controller Successfully Reconciled {"controller": "pod", "request": "default/nginx-by-chaos-scheduler"}
额外的日志行表示自定义计划程序分配了 pod。
检查在步骤 5 中启动的 pod 的状态:
$ kubectl get pods nginx-by-chaos-scheduler NAME READY STATUS RESTARTS AGE nginx-by-chaos-scheduler 1/1 Running 0 34s
"math/rand"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
混沌调度程序将新的 pod 分配给一个节点,它正在运行。它显示了使用kubebuilder
从头开始开发的定制调度程序可以完美地工作。
在下一节中,我们将使用第二个扩展点来扩展 Kubernetes 调度程序:调度程序扩展程序。scheduler extender 方法将作为 webhooks 工作,并干扰调度框架阶段。
调度程序扩展器
调度器扩展器是外部的 webhooks,用于在调度框架的不同阶段调整调度决策。框架有多个阶段寻找合适的节点,每一步都调用编译成kube-scheduler
的插件。在四个特定的阶段,它还调用调度器扩展程序:过滤器、评分/优先化、抢占,以及绑定。来自 webhooks 的响应与调度器插件的结果相结合。因此,调度程序扩展程序方便了对kube-scheduler
的扩展,而无需深入其源代码。
在本节中,将介绍配置细节和扩展器 API。最后,您将开发一个 scheduler extender webhook 服务器,并在 Kubernetes 集群中运行它。
配置详细信息
Kubernetes 调度程序连接到外部进程,因此它应该知道在哪里连接和评估响应。配置通过一个模式为KubeSchedulerConfiguration
的文件传递。最低配置如下所示。
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
Listing 5-10Minimal KubeSchedulerConfiguration
Note
KubeSchedulerConfiguration
的全面细节可在参考文件中获得。
您也可以按如下方式向KubeSchedulerConfiguration
添加扩展器。
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
extenders:
- urlPrefix: http://localhost:8888/
filterVerb: filter
ignorable: true
weight: 1
- urlPrefix: http://localhost:8890/
filterVerb: filter
prioritizeVerb: prioritize
bindVerb: bind
ignorable: false
weight: 1
Listing 5-11Extenders in KubeSchedulerConfiguration
在前面的例子中,两个扩展器在localhost:8888
和localhost:8890
上运行。第一个仅用于过滤节点,当它失败时,它不会阻止调度。然而,第二个是在框架的过滤、评分和绑定阶段调用的。此外,它是不可忽略的,所以如果 webhook 不可达或失败,pod 的调度将被卡在Pending
。
Note
您可以在源代码中检查扩展器配置的字段,因为它不是 Kubernetes API 文档的一部分。
在用扩展器信息配置了kube-scheduler
之后,现在让我们深入研究它们之间的交互。
调度程序扩展器 API
Kubernetes scheduler 使用与其阶段相关的数据对扩展程序进行 HTTP 调用,并期望得到结构化的响应。在 scheduler extender 中,您需要用 JSON 请求和响应来实现这些调用。最重要的优点是,您可以用独立于 Kubernetes 二进制文件的任何语言来开发扩展器。
过滤器
Filter webhooks 接收以下数据作为参数。
type ExtenderArgs struct {
// Pod being scheduled
Pod *v1.Pod
// List of candidate nodes where the pod can be
// scheduled; to be populated only if
// Extender.NodeCacheCapable == false
Nodes *v1.NodeList
// List of candidate node names where the pod
// can be scheduled; to be populated only if
// Extender.NodeCacheCapable == true
NodeNames *[]string
}
Listing 5-12ExtenderArgs data structure
它只是由一个 pod 和一个基于扩展器中缓存状态的节点或节点名称列表组成。作为响应,下面的数据结构被发送回来。
type ExtenderFilterResult struct {
// Filtered set of nodes where the pod can be scheduled
// only if Extender.NodeCacheCapable == false
Nodes *v1.NodeList
// Filtered set of nodes where the pod can be scheduled
// only if Extender.NodeCacheCapable == true
NodeNames *[]string
// Filtered out nodes where the pod can't be scheduled
// and the failure messages
FailedNodes FailedNodesMap
// Error message indicating failure
Error string
}
type FailedNodesMap map[string]string
Listing 5-13ExtenderFilterResult data structure
响应由 pod 的过滤节点组成。此外,不可调度的节点作为FailedNodes
与它们的消息一起被发回。最后,如果过滤由于任何原因失败,还有一个Error
字段。
优先考虑
优先化 webhooks 接收相同的数据结构ExtenderArgs
,就像过滤器 webhooks 一样。webhook 应该为节点创建分数,以分配 pod 并发回以下数据结构。
type HostPriorityList []HostPriority
type HostPriority struct {
// Name of the host
Host string
// Score associated with the host
Score int64
}
Listing 5-14HostPriorityList data structure
来自 webhook 的分数被添加到由其他扩展器和 Kubernetes scheduler 插件计算的分数中。调度框架为 pod 分配选择具有最高分数的节点。
先取
当 Kubernetes 将 pod 调度到节点时,在集群中找到合适的节点并不总是可能的。在这种情况下,抢占逻辑被触发以从节点中驱逐一些 pod。如果抢占成功,pod 将被调度到节点,被驱逐的将找到新家。在抢占过程中,调度器还使用以下数据结构调用启用的 webhooks。
type ExtenderPreemptionArgs struct {
//pod being scheduled
Pod *v1.Pod
// Victims map generated by scheduler preemption phase
// Only set NodeNameToMetaVictims if
// Extender.NodeCacheCapable == true.
// Otherwise, only set NodeNameToVictims.
NodeNameToVictims map[string]*Victims
NodeNameToMetaVictims map[string]*MetaVictims
}
type Victims struct {
// a group of pods expected to be preempted.
Pods []*v1.Pod
// the count of violations of PodDisruptionBudget
NumPDBViolations int64
}
type MetaVictims struct {
// a group of pods expected to be preempted.
Pods []*v1.Pod
// the count of violations of PodDisruptionBudget
NumPDBViolations int64
}
Listing 5-15ExtenderPreemptionArgs data structure
数据由一个 pod 和一个潜在节点图组成,这些节点上有Victims
。作为响应,webhook 发送以下数据。
type ExtenderPreemptionResult struct {
NodeNameToMetaVictims map[string]*MetaVictims
}
Listing 5-16ExtenderPreemptionResult data structure
webhook 评估抢占的节点和单元,并发送回潜在的受害者。
约束
绑定调用用于委托节点和 pod 分配。当它被实现时,与 Kubernetes API 交互以进行绑定就成了扩展器的责任。Webhooks 接收以下数据作为参数。
type ExtenderBindingArgs struct {
// PodName is the name of the pod being bound
PodName string
// PodNamespace is the namespace of the pod being bound
PodNamespace string
// PodUID is the UID of the pod being bound
PodUID types.UID
// Node selected by the scheduler
Node string
}
Listing 5-17ExtenderBindingArgs data structure
作为响应,如果在绑定期间发生错误,它将返回。
type ExtenderBindingResult struct {
// Error message indicating failure
Error string
}
Listing 5-18ExtenderBindingResult data structure
在下面的练习中,您将从头开始创建一个 scheduler extender,并实际使用它。扩展器将干扰调度框架决策和 pod 到节点的分配。
EXERCISE: DEVELOPING AND RUNNING A SCHEDULER EXTENDER
在本练习中,您将创建一个定制的 chaos scheduler 扩展器,并在 Kubernetes 集群中运行它。您将在 Go 中开发一个 HTTP web 服务器,因为调度程序扩展程序原则上是 webhook 服务器。此外,您将在 minikube 中配置kube-scheduler
以连接到您的调度程序扩展器。
注意剩下的练习是基于在 Go 中编写一个 web 服务器,它需要以下先决条件:Docker、minikube 和kubectl
。
使用以下命令在
minikube
中启动多节点集群:Create a pod definition for scheduler extender with the name
kube-scheduler-extender.yaml
undermanifests
folder with the following content:apiVersion: v1 kind: Pod metadata: labels: component: kube-scheduler-extender tier: control-plane name: kube-scheduler-extender namespace: kube-system spec: containers: - image: DOCKER_REPOSITORY/k8s-scheduler-extender:v1 name: kube-scheduler-extender hostNetwork: true
注意不要忘记将
DOCKER_REPOSITORY
更改为步骤 8 中设置的环境变量。使用以下命令将当前工作目录挂载到
minikube
节点:In another terminal, SSH into the
minikube
node and copy the manifests with the following commands, and restart thekubelet
:$ minikube ssh docker@minikube:~$ sudo su root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler-extender.yaml /etc/kubernetes/manifests/kube-scheduler-extender.yaml root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler-config.yaml /etc/kubernetes/kube-scheduler-config.yaml root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml root@minikube:/home/docker# systemctl restart kubelet
在复制步骤中,您已经将清单和配置文件添加到了
kubelet
查找的位置。在最后一步中,您已经重启了kubelet
来加载新文件并使用它们。您可以退出 minikube 节点并在本地工作站上继续。创建一个有 25 个副本的部署,并观察集群中的事件:
$ kubectl create deployment nginx --image=nginx --replicas=25 deployment.apps/nginx created $ kubectl get events --field-selector reason=FailedScheduling LAST SEEN TYPE REASON OBJECT MESSAGE ... 3m23s Warning FailedScheduling pod/nginx-6799fc88d8-qnn9p 0/5 nodes are available: 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m02: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m03: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m04: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m05: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube: coin is tails. ...
如果您运气好,所有五个节点都有 tails,那么 pod 也会有类似的事件。如果您没有幸运地看到该事件,您还可以检查调度程序扩展器的日志:
$ kubectl -n kube-system logs -f kube-scheduler-extender-minikube ... time=".." level=info msg="Flipped the coin and it is heads" time=".." level=info msg="Flipped the coin and it is heads" time=".." level=info msg="Flipped the coin and it is tails" time=".." level=info msg="Flipped the coin and it is heads" time=".." level=info msg="Flipped the coin and it is tails" time=".." level=info msg="Rolled the dice and it is 1" time=".." level=info msg="Rolled the dice and it is 10" time=".." level=info msg="Rolled the dice and it is 1" ...
命令标志
config
名为
kube-scheduler-config
的卷卷
kube-scheduler-config
的卷安装
Create the following folder structure in your Go environment:
$ mkdir -p cd $GOPATH/src/extend-k8s.io/k8s-scheduler-extender $ cd $GOPATH/src/extend-k8s.io/k8s-scheduler-extender $ mkdir -p cmd manifests pkg/filter pkg/prioritize $ tree -a . ├── cmd ├── manifests └── pkg ├── filter └── prioritize 5 directories, 0 files
文件夹结构是创建 Go 应用的主流方式。在下面的步骤中,您将在每个目录中创建文件。
Create a file
flip.go
inpkg/filter
folder with the following content:package filter import ( "math/rand" "time" "github.com/sirupsen/logrus" ) const ( HEADS = "heads" TAILS = "tails" ) var coin []string func init() { rand.Seed(time.Now().UnixNano()) coin = []string{HEADS, TAILS} } func Flip() string { side := coin[rand.Intn(len(coin))] logrus.Info("Flipped the coin and it is ", side) return side }
函数
Flip
返回正面或反面来随机过滤节点。Create a file
filter.go
inpkg/filter
folder with the following content:package filter import ( "fmt" corev1 "k8s.io/api/core/v1" extenderv1 "k8s.io/kube-scheduler/extender/v1" ) func Filter(args extenderv1.ExtenderArgs) extenderv1.ExtenderFilterResult { filtered := make([]corev1.Node, 0) failed := make(extenderv1.FailedNodesMap) pod := args.Pod for _, node := range args.Nodes.Items { side := Flip() if side == HEADS { filtered = append(filtered, node) } else { failed[node.Name] = fmt.Sprintf("%s cannot be scheduled to %s: coin is %s", pod.Name, node.Name, side) } } return extenderv1.ExtenderFilterResult{ Nodes: &corev1.NodeList{ Items: filtered, }, FailedNodes: failed, } }
Filter
函数通过接收ExtenderArgs
作为参数和ExtenderFilterResult
作为响应来实现调度程序扩展器调用的逻辑。Create a file
roll.go
inpkg/prioritize
folder with the following content:package prioritize import ( "math/rand" "time" "github.com/sirupsen/logrus" extenderv1 "k8s.io/kube-scheduler/extender/v1" ) func init() { rand.Seed(time.Now().UnixNano()) } func Roll() int64 { number := rand.Int63n(extenderv1.MaxExtenderPriority + 1) logrus.Info("Rolled the dice and it is ", number) return number }
Roll
function imitates rolling dice to find a score for the nodes. Create a fileprioritize.go
inpkg/prioritize
folder with the following content:package prioritize import ( extenderv1 "k8s.io/kube-scheduler/extender/v1" ) func Prioritize(args extenderv1.ExtenderArgs) extenderv1.HostPriorityList { hostPriority := make(extenderv1.HostPriorityList, 0) for _, node := range args.Nodes.Items { hostPriority = append(hostPriority, extenderv1.HostPriority{ Host: node.Name, Score: Roll(), }) } return hostPriority }
Prioritize
函数实现调度器扩展器调用来接收ExtenderArgs
并将HostPriorityList
发送回kube-scheduler
。Create a
main.go
file undercmd
folder with the following content:package main import ( "encoding/json" "log" "net/http" "github.com/gorilla/mux" "github.com/extend-k8s.io/k8s-scheduler-extender/pkg/filter" "github.com/extend-k8s.io/k8s-scheduler-extender/pkg/prioritize" "github.com/sirupsen/logrus" extenderv1 "k8s.io/kube-scheduler/extender/v1" ) func main() { r := mux.NewRouter() r.HandleFunc("/", homeHandler) r.HandleFunc("/filter", filterHandler) r.HandleFunc("/prioritize", prioritizeHandler) log.Fatal(http.ListenAndServe(":8888", r)) } func filterHandler(w http.ResponseWriter, r *http.Request) { args := extenderv1.ExtenderArgs{} response := extenderv1.ExtenderFilterResult{} if err := json.NewDecoder(r.Body).Decode(&args); err != nil { response.Error = err.Error() } else { response = filter.Filter(args) } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { logrus.Error(err) return } } func prioritizeHandler(w http.ResponseWriter, r *http.Request) { args := extenderv1.ExtenderArgs{} response := make(extenderv1.HostPriorityList, 0) if err := json.NewDecoder(r.Body).Decode(&args); err == nil { response = prioritize.Prioritize(args) } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { logrus.Error(err) return } } func homeHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("scheduler extender is running!")) }
它是带有 HTTP 处理程序的 webhook 服务器的入口点,用于过滤和区分调用的优先级。默认情况下,服务器将在 8888 端口上运行。
在根文件夹中创建一个
go.mod
文件来设置依赖版本:module github.com/extend-k8s.io/k8s-scheduler-extender go 1.14 require ( github.com/gorilla/mux v1.8.0 github.com/sirupsen/logrus v1.6.0 k8s.io/api v0.19.0 k8s.io/kube-scheduler v0.19.0 )
在根文件夹中创建一个
Dockerfile
来构建容器镜像,步骤如下:FROM golang:1.14-alpine as builder ADD . /go/src/github.com/extend-k8s.io/k8s-scheduler-extender WORKDIR /go/src/github.com/extend-k8s.io/k8s-scheduler-extender/cmd RUN go build -v FROM alpine:latest COPY --from=builder /go/src/github.com/extend-k8s.io/k8s-scheduler-extender/cmd/cmd /usr/local/bin/k8s-scheduler-extender CMD ["k8s-scheduler-extender"]
现在,您可以使用以下命令构建并推送调度程序扩展器的 Docker 映像:
Note Set
DOCKER_REPOSITORY
environment variable according to your Docker repository.$ docker build -t $DOCKER_REPOSITORY/k8s-scheduler-extender:v1 . Step 1/7 : FROM golang:1.14-alpine as builder ... Step 7/7 : CMD ["k8s-scheduler-extender"] ---> Running in 6655404206c1 Removing intermediate container 6655404206c1 ---> 0ce4bb201541 Successfully built 0ce4bb201541 Successfully tagged $DOCKER_REPOSITORY/k8s-scheduler-extender:v1 $ docker push $DOCKER_REPOSITORY/k8s-scheduler-extender:v1 The push refers to repository [docker.io/$DOCKER_REPOSITORY/k8s-scheduler-extender] ... v1: digest: sha256:0e62a24a4b9e9e0215f5f02e37b5f86d9235ee950e740069f80951e370ae5b34 size: 739
Create a Kubernetes scheduler configuration file with the name
kube-scheduler-config.yaml
undermanifests
folder:apiVersion: kubescheduler.config.k8s.io/v1beta1 kind: KubeSchedulerConfiguration clientConnection: kubeconfig: /etc/kubernetes/scheduler.conf extenders: - urlPrefix: http://localhost:8888/ filterVerb: filter prioritizeVerb: prioritize weight: 1
这是一个将被传递给
kube-scheduler
的简单配置,它用端点定义了您的扩展器的位置。Create a Kubernetes scheduler pod file to replace the default pod definition of
kube-scheduler
. Set the filenamekube-scheduler.yaml
undermanifests
folder with the following content:apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: component: kube-scheduler tier: control-plane name: kube-scheduler namespace: kube-system spec: containers: - command: - kube-scheduler - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf - --bind-address=127.0.0.1 - --kubeconfig=/etc/kubernetes/scheduler.conf - --leader-elect=false - --port=0 - --config=/etc/kubernetes/kube-scheduler-config.yaml image: k8s.gcr.io/kube-scheduler:v1.19.0 imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 8 httpGet: host: 127.0.0.1 path: /healthz port: 10259 scheme: HTTPS initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 15 name: kube-scheduler resources: requests: cpu: 100m startupProbe: failureThreshold: 24 httpGet: host: 127.0.0.1 path: /healthz port: 10259 scheme: HTTPS initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 15 volumeMounts: - mountPath: /etc/kubernetes/scheduler.conf name: kubeconfig readOnly: true - mountPath: /etc/kubernetes/kube-scheduler-config.yaml name: kube-scheduler-config readOnly: true hostNetwork: true priorityClassName: system-node-critical volumes: - hostPath: path: /etc/kubernetes/scheduler.conf type: FileOrCreate name: kubeconfig - hostPath: path: /etc/kubernetes/kube-scheduler-config.yaml type: FileOrCreate name: kube-scheduler-config status: {}
它添加了三个部分来使用步骤 9 中的
kube-scheduler-config.yaml
。
$ minikube start --kubernetes-version v1.19.0 --nodes 5
这表明kube-scheduler
配置正确,并且连接到调度程序扩展器 webhook。webhook 通过投掷硬币来随机过滤节点。此外,它通过掷骰子给节点打分。换句话说,scheduler extender 在调度过程中产生了一些随机性和混乱。
开发和运行调度程序扩展程序非常简单,因为您可以扩展现有的默认调度程序的功能,而无需重新编译二进制文件。此外,您可以用任何想要的编程语言创建扩展程序。但是,最好在以下问题上保持谨慎,因为您会生成 Kubernetes 控制平面组件的接触点:
配置:使用静态文件为 Kubernetes 调度程序定义扩展程序。因此,请确保文件位置及其内容是正确的。此外,确保该文件不会因集群升级而被覆盖或删除。
性能:像所有的 webhooks 一样,扩展器作为外部进程运行。连接到另一个服务器并检索响应在时间上是很昂贵的。确保 webhook 尽可能快地提供响应,并且控制平面组件可以访问它。
缓存不一致:可以在扩展器中为节点信息启用缓存。如果您的节点不经常改变,或者调度决策不那么重要,那么将节点信息缓存在扩展器中是有益的。另一方面,如果您总是需要关于节点的最新信息,可以禁用缓存,使用 Kubernetes 调度程序发送给您的数据。
关键要点
Kubernetes 调度器是在集群上分配工作负载的控制平面组件。
Kubernetes 调度程序在优先级和规则集中选择最佳节点。
通过在集群中运行多个调度器,可以扩展调度决策。
调度框架是 Kubernetes 调度器的可插拔架构,并且可以通过 webhooks 进行扩展。
在下一章中,我们将通过开发和运行存储、网络和设备插件来扩展 Kubernetes 与基础设施的交互。
六、基础设施扩展
凭借稳定的基础设施快速移动。
—马克·扎克伯格
美国企业家、脸书创始人兼首席执行官
Kubernetes 是事实上的容器编排系统,它提供什么,也不提供什么。Kubernetes 提供了可伸缩的、可靠的容器运行时管理,但是它将与基础设施相关的决策留给了最终用户。它允许用户在任何基础设施上创建他们的集群,只要它符合 API。本章将重点关注通过改变底层的云原生基础设施来扩展 Kubernetes。本章结束时,您将配置和运行存储、网络和设备插件,以实现定制和灵活的要求。
让我们从云原生基础设施以及 Kubernetes 如何集成它的概述开始。
云原生基础架构
Kubernetes 不是一个"编写一次,在任何地方运行"类型的系统,但它也不限制任何云提供商或本地系统。它允许用户从生态系统中广泛的开放式基础架构选项中进行选择。您可以在几乎所有公共云提供商、数据中心甚至笔记本电脑上创建 Kubernetes 集群。然而,制作云原生应用需要底层的云原生基础设施。基础设施的设计应充分利用虚拟化和分布式微服务架构。
Kubernetes 是容器编排,所以它的重点是创建、运行和操作容器。然而,容器不是运行在裸机节点上的简单应用。相反,它们是需要复杂存储、网络和设备操作的虚拟化系统。图 6-1 显示了 Kubernetes 的堆叠情况。底层有与物理基础设施交互的存储、网络和设备插件。当它实现连接性和容量时,Kubernetes 可以创建和运行您的容器作为 pod 的构建块。Kubernetes 的上层使得用更复杂的 Kubernetes 资源开发可伸缩的、可靠的、云原生的应用成为可能。
图 6-1
Kubernetes 和基础设施
基础设施和 Kubernetes 之间的插件是将您的基础设施与定制存储或网络解决方案连接起来的工具。由于 Kubernetes 并不强制执行“一刀切”的方法,因此它可以与提供的每一个基础设施一起工作,只要它们符合公开可用的插件 API。在接下来的章节中,我们将介绍这三个插件及其 API 和示例。
存储插件
存储因其耐用性和刚性特征而成为云原生架构的挑战之一。容器是临时的,Kubernetes 可以重新启动或重新安排它们。如果有卷连接到容器,它们也可能会丢失。但是,您需要将数据库、ERP 系统和以数据为中心的应用部署到 Kubernetes,因此并不是集群中运行的所有应用都是短暂的。因此,基础设施应该将容器的应用数据作为持久存储的一部分。
让我们看看持久存储在 Kubernetes 中是如何工作的。用以下数据创建一个PersistentVolumeClaim
(PVC)。
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-test
spec:
accessModes:
- ReadWriteOnce
requests:
storage: 1Gi
Listing 6-1Example PVC
现在,检查集群中的PersistentVolumeClaim
和PersistentVolume
资源。
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-test Bound pvc-467fa613-0396-481a-aa73-d4b6c5fbcc4b 1Gi RWO standard 10s
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-467fa613-0396-481a-aa73-d4b6c5fbcc4b 1Gi RWO Delete Bound default/pvc-test standard 13s
Listing 6-2Volume listing
在 PVC 中,您请求了一个带有1Gi
的卷,集群相应地创建了一个PersistentVolume
。现在,您可以在 pod、statefulsets 甚至函数中使用它来存储您的应用数据。Kubernetes 中的 PVC 和 PV 看起来很简单,因为它从基础设施层创建了一个强大的抽象。作为最终用户,我们不需要知道基础架构中存储的生命周期:
卷是如何创建的?
分配了多少容量?
卷附在哪里?
如何提供对存储的访问?
如何管理卷的备份和恢复?
存储供应商和 Kubernetes 之间的通信被标准化为一个开源 API。因此,实施细节和问题的答案留给存储提供商。Kubernetes 和存储基础设施之间的抽象在容器存储接口(CSI) 中定义,接下来您将学习它的基础知识。
容器存储接口
容器存储接口(CSI)是一个开源 API,使容器编排器能够与存储系统一起工作。在 Kubernetes 的早期阶段,卷是由编译成 Kubernetes 二进制文件的插件管理的。随着供应商数量的增加,管理树内插件变得更加复杂。此外,它创建了一个封闭的环境,因为添加新的存储供应商需要成为 Kubernetes 源代码的一部分。另一方面,CSI 插件是具有开放标准 API 的外部应用。
从厂商的角度来看,主要优势是只开发插件,遵循 CSI 的要求。与每一个容器 orchestrator 合作就足够了,比如 Kubernetes、Apache Mesos,以及未来的许多合作伙伴。CSI 插件提供以下功能:
卷的动态供应和停用
卷的连接/安装和分离/卸载
完整的规范可从 GitHub 获得,包括所有的功能、请求和响应。CSI 插件和容器编排器之间的通信由 gRPC(远程过程调用 ) 处理,这是一个开放的高性能通信框架。
CSI 插件分为两种,即节点插件和控制器插件:
节点插件是一个 gRPC 服务器,运行在存储提供商卷被配置的节点上。
控制器插件是一个 gRPC 服务器,可以在集群中的任何地方运行。
这两个插件还实现了一个 Identity gRPC 服务来提供关于它们功能的信息。因此,您可以将节点和控制器插件部署为两个二进制文件,也可以组合成一个二进制文件。
下图图 6-2 总结了 gRPC 调用卷的生命周期。
图 6-2
动态卷的生命周期
流程开始于对控制器插件的CreateVolume
调用,以提供新的卷。然后,进行ControllerPublishVolume
调用以指示容器编排器想要使用卷上的节点。在这一步中,插件执行使卷在给定节点上可用的必要工作。随后,NodePublishVolume
调用被发送到在特定节点上运行的节点插件,以发布工作负载已被调度并且它想要使用该卷。类似地,NodeUnpublishVolume
、ControllerUnpublishVolume
和DeleteVolume
调用是在删除存储提供商中的卷时进行的。
CSI 界面使用起来很简单,但是将卷集成到一个容器编排系统(如 Kubernetes)中就不那么简单了。在下一节中,您将了解如何将 CSI 插件集成到 Kubernetes 中。
kubernetes 中的 CSI 插件
CSI 是存储系统与 Kubernetes 等容器编排系统一起工作的标准。主要想法是让供应商开发插件,并将其安装到容器编排器中。在本节中,您将了解 Kubernetes 如何将 CSI 插件与本地 Kubernetes 资源集成在一起。
Kubernetes 用以下两条规则定义了kubelet
和 CSI 插件之间的通信:
kubelet
运行在节点上,直接调用 CSI 函数。因此,CSI 插件应该在节点上运行,其 Unix 套接字对kubelet
可用。kubelet
发现带有插件注册机制的 CSI 插件。因此,CSI 插件应该向运行在节点上的kubelet
注册自己。
此外,Kubernetes 存储社区提供了 sidecar 容器和资源,以最大限度地减少部署工作和样板代码。Sidecar 容器具有观察 Kubernetes API 和触发针对 CSI 插件的动作的通用逻辑。想法是将 sidecar 容器与 CSI 插件捆绑在一起,并将它们作为 pods 部署到集群中。使用边车容器不是强制性的;然而,我们强烈建议这样做,因为它们在 Kubernetes 和 CSI 之间创建了一个强大的抽象。目前,维护下列边车:
外部供应器:它在 Kubernetes API 中监视
PersistentVolumeClaim
对象,并针对 CSI 插件调用CreateVolume
。当供应新卷时,外部供应器在 Kubernetes API 中创建一个PersistentVolume
对象。外部附加器:通过调用 CSI 驱动程序的
ControllerPublish
函数,将卷附加到节点上。外部快照:它在 Kubernetes API 中监听
VolumeSnapshotContent
资源,对 CSI 驱动采取CreateSnapshot
、DeleteSnapshot
、ListSnapshots
动作。external-resizer :它监视
PersistentVolumeClaim
对象的变化,以捕捉是否有更多的存储请求。在这种情况下,它调用 CSI 插件的ControllerExpandVolume
函数。节点-驱动-注册器:从插件端点获取 CSI 驱动信息,并注册到
kubelet
。livenessprobe :它监控 CSI 插件端点的健康状况,并在必要时帮助 Kubernetes 重启 pod。
如图 6-3 所示,CSI 插件通常作为两个组件部署到 Kubernetes,由边车容器打包。
图 6-3
CSI 在 Kubernetes 的部署
控制器插件由实现控制器服务的 CSI 插件和以下 sidecars 组成:外部供应器、外部附加器、外部快照器和外部调整大小器。它可以作为 deployment 或 statefulset 进行部署,因为它可以在集群中的任何节点上运行。
节点插件由一个 CSI 插件组成,用 node-driver-registrar sidecar 实现节点服务。它应该由 DaemonSet 部署在集群中的每个节点上。
sidecars 使得开发一个定制的存储插件,并将其集成到 Kubernetes 是很简单的。您只需要按照标准实现 CSI 服务。所有云提供商(如 Google Cloud、Azure、AWS 或 AliCloud)、基础设施提供商(如 IBM、Dell、VMware 或 Hewlett Packard)和存储技术(如 OpenEBS、GlusterFS 或 Vault)都已经准备好了他们的 CSI 驱动程序,并且公开可用。下一节将配置 CSI 驱动程序并将其部署到 Kubernetes 集群中,并查看它的运行情况。
CSI 主机路径驱动程序正在运行
CSI 主机路径驱动程序是使用本地目录创建卷的 CSI 实现。因此,它是非生产驱动程序,在单个节点上运行。在本节中,我们将把它部署到集群中,并查看它的运行情况。
让我们通过运行minikube start --kubernetes-version v1.19.0
来创建一个minikube
集群。
当您的集群启动并运行时,启用volumesnapshots
和csi-hostpath-driver
插件。
清单 6-3。 迷你裙
第一个命令启用volumesnapshots
并部署卷快照控制器和卷快照 CRDs。由于minikube
是一个单节点集群,默认情况下,没有可用的 CSI 实现。第二个命令部署 CSI Hostpath 驱动,它将为你提供存储。
下一步是检查集群中安装了哪些CSIDrivers
。
$ kubectl get CSIDrivers
NAME ATTACHREQUIRED PODINFOONMOUNT MODES AGE
hostpath.csi.k8s.io true true Persistent,Ephemeral 9m17s
Listing 6-4CSIDrivers in the cluster
CSI 驱动程序的名称遵循域名符号hostpath.csi.k8s.io
。此外,hostpath 驱动程序管理的卷应该有StorageClass
。
$ kubectl get StorageClass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
csi-hostpath-sc hostpath.csi.k8s.io Delete Immediate false 12m
standard (default) k8s.io/minikube-hostpath Delete Immediate false 18m
Listing 6-5Storage classes in the cluster
除了 minikube 的default
存储集群,您还会看到一个名为csi-hostpath-sc
的 CSI 管理的存储类。
现在,让我们深入了解插件和 sidecar 容器是如何部署的。
$ kubectl -n kube-system get statefulset
NAME READY AGE
csi-hostpath-attacher 1/1 20m
csi-hostpath-provisioner 1/1 20m
csi-hostpath-resizer 1/1 20m
csi-hostpath-snapshotter 1/1 20m
csi-hostpathplugin 1/1 20m
volume-snapshot-controller 1/1 20m
Listing 6-6Statefulsets in the kube-system namespace
你可以一个接一个地检查每个状态集合,检查容器或者运行下面的jq
魔法。
$ kubectl -n kube-system get statefulsets -o json | jq '.items[] | "\(.metadata.name): \(.spec.template.spec.containers[].name)"'
"csi-hostpath-attacher: csi-attacher"
"csi-hostpath-provisioner: csi-provisioner"
"csi-hostpath-resizer: csi-resizer"
"csi-hostpath-snapshotter: csi-snapshotter"
"csi-hostpathplugin: node-driver-registrar"
"csi-hostpathplugin: hostpath"
"csi-hostpathplugin: liveness-probe"
"volume-snapshot-controller: volume-snapshot-controller"
Listing 6-7Statefulsets and containers in the kube-system namespace
如您所见,控制器边盘独立运行,而节点边盘打包在csi-hostpathplugin
statefulset 中。sidecars 和驱动程序之间的连接是通过共享 Unix 套接字作为卷来处理的。
$ kubectl -n kube-system describe statefulsets csi-hostpath-provisioner
Name: csi-hostpath-provisioner
Namespace: kube-system
...
Volumes:
socket-dir:
Type: HostPath (bare host directory volume)
Path: /var/lib/kubelet/plugins/csi-hostpath
HostPathType: Director
Listing 6-8Description of csi-hostpath-provisioner
从前面的命令输出可以看出,csi-provisioner
sidecar 容器使用卷socket-dir
中定义的套接字连接到 CSI 服务。
用以下内容创建一个名为example-pvc.yaml
的文件。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: csi-hostpath-sc
Listing 6-9Example PVC
部署到群集并检查卷。
$ kubectl apply -f example-pvc.yaml
persistentvolumeclaim/example-pvc created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-dd4570bc-58cc-4074-a284-b13651970d17 1Gi RWO Delete Bound default/example-pvc csi-hostpath-sc 13s
Listing 6-10Volume creation
使用正确的存储类为PersistentVolumeClaim
创建了一个卷,并对其进行了绑定。让我们检查 CSI 服务的日志并检查CreateVolume
呼叫。
$ kubectl -n kube-system logs csi-hostpathplugin-0 hostpath | grep -A 3 CreateVolume
* GRPC call: /csi.v1.Controller/CreateVolume
* GRPC request: {"accessibility_requirements":{"preferred":[{"segments":{"topology.hostpath.csi/node":"minikube"}}],"requisite":[{"segments":{"topology.hostpath.csi/node":"minikube"}}]},"capacity_range":{"required_bytes":1073741824},"name":"pvc-dd4570bc-58cc-4074-a284-b13651970d17","volume_capabilities":[{"AccessType":{"Mount":{}},"access_mode":{"mode":1}}]}
created volume af5c51ee-7aa2-11eb-a960-0242ac110004 at path /csi-data-dir/af5c51ee-7aa2-11eb-a960-0242ac110004
* GRPC response: {"volume":{"accessible_topology":[{"segments":{"topology.hostpath.csi/node":"minikube"}}],"capacity_bytes":1073741824,"volume_id":"af5c51ee-7aa2-11eb-a960-0242ac110004"}}
Listing 6-11CSI logs
gRPC 调用和响应表明 hostpath 服务按照PersistentVolumeClaim
中的请求创建了卷。边车容器处理从PersistentVolumeClaim
到有效 CSI 呼叫的转换。这种抽象使得 CSI 插件不关心容器编排器的实现细节。
通过遵循本节中的步骤,您已经将 CSI 插件部署到 Kubernetes 集群中,并检查了它是如何集成到 Kubernetes 生态系统中的。此外,您还看到了在 Kubernetes 中,it 是如何根据请求调配存储的。在 Kubernetes 中创建和操作 CSI 插件时有三个要点:
CSI 规范和功能:确保您已经实施了与您的基础设施相关的所有必需功能。
等幂和容错驱动 : CSI 驱动在容器编排器和基础设施之间建立了一座桥梁。因此,请求应该是幂等的,并且能够从失败中恢复。
Kubernetes 边车和资源 : Kubernetes 存储社区提供边车容器和定制资源,以便将 CSI 服务轻松集成到集群中。在即将到来的 CSI 和 Kubernetes 版本中使用它们。
容器存储接口(CSI)是扩展 Kubernetes 存储操作的开源 API。它支持定制存储要求和基础架构特征。您可以通过实现所需的服务来创建 CSI 插件,并使用本地 Kubernetes 资源进行部署。然后,您可以在 Kubernetes 中动态创建卷,CSI 插件将在基础设施层提供存储。关注点的抽象和分离使得为 Kubernetes 轻松开发、测试和部署存储扩展成为可能。
下一节将介绍 Kubernetes 和网络基础设施与 Kubernetes 网络模型和容器网络接口(CNI)插件之间的交互。
网络插件
Kubernetes 是一个可伸缩的容器编排工具,可以在集群中的多个节点之间分配工作负载。因此,分布在数据中心的容器和节点之间的通信带来了基础设施挑战。由于其开放的架构,Kubernetes 没有规定任何网络设置,而只是定义了需求。Kubernetes 和基础设施的分离使供应商能够开发他们的插件并将其集成到 Kubernetes 中。
在这一节中,您将了解到更多关于 Kubernetes 网络模型和网络集成规范的知识,最后,您将看到正在运行的插件。
库柏网络模型
Kubernetes 的构造块是 pod,每个 pod 都有一个唯一的 IP 地址。这种方法有两个显著的优点:首先,您不需要开发 pod 之间的复杂链接,包括容器端口到主机端口的匹配。其次,在命名、服务发现、负载平衡和应用配置方面,您可以像对待虚拟机一样对待 pod。
Kubernetes 在网络实施中需要以下基本规则:
节点上的 Pod 可以与群集中的所有 pod 通信,而无需 NAT。
系统守护程序或
kubelet
等节点代理可以与同一节点上的所有 pod 通信。
用这两个规则定义的简化模型源于分配了 IP 的虚拟机及其与其他虚拟机的通信。Kubernetes 中的模型被命名为“ IP-per-pod ”,这表明 IP 地址存在于 pod 范围内。虽然实现细节留给了网络插件,但是可以讨论和说明三个主要的通信挑战:
同一容器的容器
同一节点上的单元
不同节点上的 pod
Kubernetes 中的一个 pod 由一个或多个用户定义的容器和一个附加的pause
容器组成。您可以通过连接到 Kubernetes 节点来检查pause
容器。
$ docker ps
CONTAINER ID IMAGE COMMAND ...
...
tm574dmcbc gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
zsxqd5lcvx gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
Listing 6-12Containers on a Kubernetes node
容器的基本任务是在其他容器崩溃并重新连接时创建并保持网络名称空间。它确保所有 pod 的容器共享一个网络名称空间,并通过localhost
连接。如图 6-4 所示,容器 A 可以通过localhost:8080
连接到容器 B 的8080
口。
图 6-4
容器对容器通信
每个 pod 都有其网络名称空间和唯一的 IP 地址。此外,每个 pod 都有一个虚拟以太网设备。虚拟以太网设备在节点和 pod 网络之间创建一个隧道。虚拟隧道的命名在 pod 端为eth0
,在节点端为veth0
、veth1
和vethN
。因此,请求从 pod 的eth0
接口开始,到达vethN
接口。在节点上,有一个名为cbr0
的网桥连接多个网络。节点上的每个 pod 都是cbr0
桥的一部分,请求通过它找到自己的路,如图 6-5 所示。
图 6-5
同一节点上的点对点通信
节点上的网桥知道 pod IPs 及其虚拟以太网设备。当一个 pod 试图连接到网桥中没有列出的 IP 时,路由就变得有点复杂。尽管实现可能因网络插件和基础设施而异,但我们可以讨论如图 6-6 所示的请求的典型流程。当网桥没有关于 IP 的信息时,它会在集群级别请求一个默认网关。在群集级别,为节点及其 IP 保存一个 IP 表。在典型的设置中,pod IPs 在节点之间分配,例如,节点 1 具有100.11.1.0/24
范围,节点 2 具有100.11.2.0/24
范围,等等。当找到正确的节点时,请求会发送到网桥、虚拟以太网,最后是 pod。
图 6-6
不同节点上的点对点通信
Kubernetes 网络实现处理节点和集群级别之间的通信。这些实现遵循容器网络接口(CNI),一种开源 API 规范,并且它们作为插件安装到集群中。在接下来的部分中,您将了解更多关于 CNI 的信息,并看到它的实际应用。
容器网络接口(CNI)
容器网络接口(CNI)是网络插件和容器运行时之间的标准接口定义。它被所有重要的容器编排者采用,比如 Kubernetes、Mesos 和 Cloud Foundry。此外,各种云提供商,如 Amazon ECS、OpenShift、Cisco 和 VMware,也将 CNI 插件作为其容器平台的一部分。因此,开发一个遵循 CNI 标准的新网络插件将使你与大多数现代云生态系统兼容。
CNI 由基于 JSON 的二进制插件规范、内置插件和开发定制第三方插件的库组成。CNI 团队在三组下提供并维护以下内置插件:
创建界面的主要插件:
创建一个桥,并将主机和容器添加到桥中。
ipvlan
在容器中添加一个 ipvlan 接口。loopback
将环回接口的状态设置为 up。macvlan
创建新的 MAC 地址,并将所有流量转发到容器。创建一个 veth 对。
vlan
分配一个 vlan 设备。host-device
将现有设备移入容器。win-bridge
创建一个桥,并在特定于 Windows 的环境中向其中添加主机和容器。在特定于 Windows 的环境中创建容器的覆盖接口。
IPAM 插件 进行 IP 地址分配:
dhcp
在主机上运行一个守护程序,代表容器发出 DHCP 请求。host-local
维护已分配 IP 的本地数据库。static
给容器分配静态 IP 地址。
元和其他插件:
flannel
生成一个法兰绒配置文件对应的接口。tuning
调整现有接口的 sysctl 参数。portmap
维护基于 iptables 的端口映射,并将端口从主机映射到容器。bandwidth
允许带宽限制。sbr
配置基于源的路由。firewall
使用 iptables 或 firewalld 添加规则,以允许进出容器的流量。
CNI 用面向插件的方法关注容器的网络连接。列出的插件涵盖了基本的网络操作,如创建网桥、IP 分配或主机和容器之间的连接。除了内置插件之外,还维护了一个带有样板代码的示例插件,以便轻松开发定制的 CNI 插件。
CNI 插件的集成从提供给容器运行时的 JSON 网络配置开始,如图 6-7 所示。然后,运行时使用基于容器生命周期的命令调用 CNI 可执行文件。该插件在网络基础设施上运行,并连接容器以创建容器编排的结构。
图 6-7
CNI 和容器运行时集成
根据规范,您应该实现以下命令:
ADD
向网络添加容器的命令DEL
从网络中移除容器的命令CHECK
验证连通性的命令VERSION
返回支持的 CNI 版本
由于 CNI 的命令和期望很简单,你可以在BASH
中创建你的插件,并将其部署为一个定制的第三方插件。在下一节中,您将了解更多关于 Kubernetes 中的 CNI 插件及其集成。
CNI 插件在 kubernetes 中的应用
Kubernetes 是 CNI 的维护者、最积极的贡献者和热心用户。Kubernetes 要求容器到达位于相同或远程节点上的其他容器。当你在诸如 GCP、AWS 或 Azure 这样的云平台上创建一个 Kubernetes 集群时,这个网络就起作用了。云提供商安装并配置了他们的 CNI 插件,以最佳方式与他们的基础设施协同工作。然而,当您创建本地 Kubernetes 集群时,您可以自由选择 CNI 插件或从头开始开发。
containernetwork ing/ cni 官方资源库中列出了近 30 个第三方 CNI 插件。每个插件都有其优点和缺点;因此,比较是相当棘手的。但是,我们可以讨论特色插件及其基本特性:
法兰绒是最古老、最成熟的插件之一,它提供了一种简单易行的方式来配置第三层网络结构。它负责提供和管理集群节点之间的 IP 网络。很容易将它部署到 Kubernetes 集群,因为它在每个节点上作为 DaemonSet 运行。
编织网是一个简单易用的 CNI 插件,不需要复杂的配置或额外的代码。此外,Weave Net 还提供了额外的功能,如 DNS、IPAM 和防火墙。您可以使用命令行工具 weave 来配置和启动网络结构。
Multus 是 Kubernetes 的一个插件,可以将多个网络接口连接到 pods。默认情况下,Kubernetes 中的每个 pod 只有一个接口;然而,multus 作为一个元插件来调用其他各种 CNI 插件。它是通过 CRDs 配置的,所以在设计上是 Kubernetes-native。
Calico 是一个针对容器、虚拟机和节点服务的网络和安全解决方案。它支持各种数据平面,如纯 Linux eBPF、标准 Linux 网络或 Windows HNS 数据平面。它提供了完整的网络堆栈;但是,通常将其与其他云提供商 CNIs 结合使用,以提供网络策略功能。
在下一节中,您将看到 Calico 在 Kubernetes 集群中运行。
卡利科 CNI 插件在行动
默认情况下,minikube
提供一个具有基本网络设置的单节点 Kubernetes 集群。您可以使用以下命令启用 CNI 并安装 Calico。
***清单 6-13。*用印花布做的
几秒钟后,安装将完成,您可以使用以下命令检查 Calico pods。
$ kubectl -n kube-system get pods -l k8s-app=calico-node
NAME READY STATUS RESTARTS AGE
calico-node-p9bjs 1/1 Running 0 4m46s
Listing 6-14Calico pods
现在,向minikube
添加第二个节点,并检查它是否已经连接到集群。
清单 6-15。 向集群添加节点
清单显示了两个节点,这表明节点之间的网络连接已经成功建立。现在,让我们深入了解一下一直在运行什么,以及它是如何配置的。
在集群的每个节点上,都应该有一个 Calico 应用在运行。
$ kubectl -n kube-system get daemonsets
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
calico-node 2 2 2 2 2 kubernetes.io/os=linux 18m
kube-proxy 2 2 2 2 2 kubernetes.io/os=linux 18m
Listing 6-16DaemonSets in the cluster
Calico 应以 JSON 格式提供网络配置。您可以在节点中使用以下命令进行检查。
$ minikube ssh
docker@minikube:~$ cat /etc/cni/net.d/10-calico.conflist
{
"name": "k8s-pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"log_level": "info",
"datastore_type": "kubernetes",
"nodename": "minikube",
"mtu": 1440,
"ipam": {
"type": "calico-ipam"
},
"policy": {
"type": "k8s"
},
"kubernetes": {
"kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
}
},
{
"type": "portmap",
"snat": true,
"capabilities": {"portMappings": true}
},
{
"type": "bandwidth",
"capabilities": {"bandwidth": true}
}
]
}
Listing 6-17Network configuration in the cluster
最后,当 CNI 插件遵循二进制扩展模式时,预计会在主机系统中看到 CNI 二进制文件。让我们检查节点中的二进制文件夹。
docker@minikube:~$ ls /opt/cni/bin/
bandwidth calico dhcp flannel host-local loopback portmap sbr tuning vrf
bridge calico-ipam firewall host-device ipvlan macvlan ptp static vlan
Listing 6-18CNI binaries
随着 Calico 的安装,CNI 插件和配置被部署到 Kubernetes 节点。如果您需要通过配置、安装或开发 CNI 插件来扩展 Kubernetes 的网络功能,您需要遵循相同的模式。如果您打算构建自己的 CNI 插件,以下三点值得一提:
可扩展性:就可扩展性而言,网络既是瓶颈也是机遇。在扩展节点时,您会发现自己受到网络拓扑的复杂性和开销的限制。因此,您需要根据集群的预期节点数量来设计您的 CNI 插件。
基础设施限制:大多数 CNI 插件都是通过在上层创建解决方案来克服基础设施限制的。了解您的计算和网络基础设施中的界限和障碍,并相应地设计您的 CNI 插件。
复杂度 vs .功能集:当你看着可用的第三方 CNI 插件,你会被提供的功能淹没。建议从小处着手,先给出必备功能。例如,如果你不希望使用网络策略,你可以在设计你的 CNI 插件时去掉这个特性。
在下一节中,设备插件将作为 Kubernetes 的第三个也是最后一个基础设施扩展点来讨论。
设备插件
Kubernetes 工作负载在节点上运行并消耗资源。节点上的资源可以是 CPU、内存、存储或供应商提供的任何自定义设备,如 GPU、高性能网卡或 FPGAs。由于在源代码中把所有的厂商和设备都作为一个资源来覆盖是不可行的,Kubernetes 提供了一个设备插件框架。框架是通过kubelet
通告和分配系统硬件资源的扩展点。供应商不定制代码,而是实现他们的插件并部署到集群中,以扩展 Kubernetes 的资源分配机制。
在本节中,您将了解设备插件 API 和生命周期。此外,您将从头开始开发一个设备插件,并将其部署到集群中以查看其运行情况。
设备插件 API
设备插件是在节点上运行并与kubelet
通信的应用。因此,它们遵循二进制插件扩展模式。入口点是使用提供的 gRPC 服务注册到kubelet
。
service Registration {
rpc Register(RegisterRequest) returns (Empty) {}
}
Listing 6-19kubelet registration service
在注册中,插件向位于/var/lib/kubelet/device-plugins/kubelet.sock
的kubelet
Unix 套接字发送以下信息:
设备插件插座的名称
设备插件 API 版本
扩展资源命名方案中的资源名称,如
vendor-domain/resource-type
注册后,设备插件负责使用位于/var/lib/kubelet/device-plugins/
的 Unix 套接字提供 gRPC 服务。该服务实现以下接口。
GetDevicePluginOptions
是提供关于插件信息的元功能。ListAndWatch
是持续运行的功能,用于传输更新的设备列表。Allocate
是在容器创建过程中调用的基本函数。设备插件处理与基础设施相关的准备操作。然后,它返回参数以使设备对容器可用。GetPreferredAllocation
和PreStartContainer
功能是可选功能。
service DevicePlugin {
rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}
rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}
rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}
}
Listing 6-20Device plugin interface
pods 请求定制设备插件资源作为其容器规范的一部分。例如,如果设备插件广告说在节点上有十个extend-k8s.io/custom-resource
实例可用,那么 Kubernetes API 就使用这个信息来决定节点状态和调度。以下 pod 定义需要三个extend-k8s.io/custom-resource
设备实例。Kubernetes 只会将 pod 调度到一个节点,如果有足够的资源来满足需求的话。
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
containers:
- name: example-container
image: k8s.gcr.io/pause:2.0
resources:
limits:
extend-k8s.io/custom-resource: 3
Listing 6-21Pod with custom device resource
设备插件应该在 Kubernetes 节点上可用,更具体地说是在/var/lib/kubelet/device-plugins
文件夹中。因此,您需要手动安装它们,或者将它们作为 DaemonSet 部署在集群中。DaemonSets 有额外的优势,如自动升级和在kubelet
失败后重启插件。
在云原生生态系统中已经有一些设备插件实现。这些是由供应商创建和维护的开源插件:
面向 GPU、FPGA、QAT、VPU、SGX 和 DSA 设备的英特尔设备插件
KubeVirt 设备插件用于硬件辅助虚拟化
在下一节中,您将创建一个示例设备插件,并将其部署到集群中。
设备插件的开发和部署
让我们从使用下面的命令创建一个集群开始:minikube start --kubernetes-version v1.19.0
。
然后,在您的 Go 环境中创建以下文件夹结构。
$ mkdir -p $GOPATH/src/extend-k8s.io/k8s-device-plugin-example
$ cd $GOPATH/src/extend-k8s.io/k8s-device-plugin-example
$ mkdir -p cmd pkg
Listing 6-22Go project initialization
在cmd
文件夹中创建一个main.go
文件,内容如下。
package main
import (
"flag"
"github.com/kubevirt/device-plugin-manager/pkg/dpm"
"github.com/extend-k8s.io/k8s-device-plugin-example/pkg"
)
func main() {
flag.Parse()
manager := dpm.NewManager(pkg.Lister{})
manager.Run()
}
Listing 6-23Main file for device plugin
使用Lister
创建新经理是一个非常简单的main
函数。现在,让我们在the
pkg 文件夹中创建一个lister.go
文件,内容如下。
package pkg
import (
"github.com/kubevirt/device-plugin-manager/pkg/dpm"
)
type Lister struct{}
func (Lister) GetResourceNamespace() string {
return "extend-k8s.io"
}
func (Lister) Discover(pluginListCh chan dpm.PluginNameList) {
pluginListCh <- dpm.PluginNameList{"example"}
}
func (Lister) NewPlugin(deviceID string) dpm.PluginInterface {
return &ExamplePlugin{}
}
Listing 6-24Lister implementation
它用于名称为extend-k8s.io/example
的设备插件的注册和发现。此外,它返回一个您接下来将实现的PluginInterface
。
在pkg
文件夹中创建一个plugin.go
文件,内容如下。
package pkg
import (
"context"
"math/rand"
"time"
"github.com/thanhpk/randstr"
. "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
)
type ExamplePlugin struct{}
func (dp *ExamplePlugin) ListAndWatch(e *Empty, s DevicePlugin_ListAndWatchServer) error {
s.Send(&ListAndWatchResponse{Devices: randomDevices()})
for {
time.Sleep(5 * time.Second)
s.Send(&ListAndWatchResponse{Devices: randomDevices()})
}
}
func (dp *ExamplePlugin) Allocate(c context.Context, r *AllocateRequest) (*AllocateResponse, error) {
envs := map[string]string{"K8S_DEVICE_PLUGIN_EXAMPLE": randstr.Hex(16)}
responses := []*ContainerAllocateResponse{{Envs: envs}}
return &AllocateResponse{ContainerResponses: responses}, nil
}
func (ExamplePlugin) GetDevicePluginOptions(context.Context, *Empty) (*DevicePluginOptions, error) {
return nil, nil
}
func (ExamplePlugin) PreStartContainer(context.Context, *PreStartContainerRequest) (*PreStartContainerResponse, error) {
return nil, nil
}
func (dp *ExamplePlugin) GetPreferredAllocation(context.Context, *PreferredAllocationRequest) (*PreferredAllocationResponse, error) {
return nil, nil
}
func randomDevices() []*Device {
devices := make([]*Device, 0)
for i := 0; i < rand.Intn(5)+1; i++ {
devices = append(devices, &Device{
ID: randstr.Hex(16),
Health: Healthy,
})
}
return devices
}
Listing 6-25Plugin implementation
该文件有两个要点值得一提:
ListAndWatch
功能从注册一组随机设备开始。然后,它每 5 秒钟用一组新的随机设备更新设备。Allocate
函数返回要传递给容器的环境变量K8S_DEVICE_PLUGIN_EXAMPLE
。这种方法有助于在应用中使用自定义设备。
在项目的根文件夹中创建一个go.mod
文件,内容如下。
module github.com/extend-k8s.io/k8s-device-plugin-example
go 1.14
require (
github.com/kubevirt/device-plugin-manager v1.18.8
github.com/thanhpk/randstr v1.0.4
k8s.io/kubelet v0.19.0
)
Listing 6-26Dependency file
最后,用下面的两层方法创建一个Dockerfile
。
FROM golang:1.14-alpine as builder
ADD . /go/src/github.com/extend-k8s.io/k8s-device-plugin-example
WORKDIR /go/src/github.com/extend-k8s.io/k8s-device-plugin-example/cmd
RUN go build -v
FROM alpine:latest
COPY --from=builder /go/src/github.com/extend-k8s.io/k8s-device-plugin-example/cmd/cmd /usr/local/bin/k8s-device-plugin-example
CMD ["k8s-device-plugin-example"]
Listing 6-27Dockerfile for device plugin
现在,您可以使用以下命令构建和部署设备插件的 Docker 映像。
Note
根据您的 Docker 存储库设置DOCKER_REPOSITORY
环境变量。
$ docker build -t $DOCKER_REPOSITORY/k8s-device-plugin-example:v1 .
=> [internal] load build definition from Dockerfile
...
naming to docker.io/$DOCKER_REPOSITORY/k8s-device-plugin-example:v1
$ docker push $DOCKER_REPOSITORY/k8s-device-plugin-example:v1
The push refers to repository [docker.io/$DOCKER_REPOSITORY/k8s-device-plugin-example]
...
v1: digest: sha256:91e41..bffc size: 739
Listing 6-28Container build
使用以下 DaemonSet 定义将插件部署到集群。
Note
不要忘记将DOCKER_REPOSITORY
更改为环境变量。
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
name: device-plugin-example
name: device-plugin-example
namespace: kube-system
spec:
selector:
matchLabels:
name: device-plugin-example
template:
metadata:
labels:
name: device-plugin-example
spec:
containers:
- name: device-plugin-example
image: $DOCKER_REPOSITORY/k8s-device-plugin-example:v1
securityContext:
privileged: true
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
Listing 6-29DaemonSet for device plugin
检查并确保设备插件窗格正在运行。
$ kubectl -n kube-system get pods -l name=device-plugin-example
NAME READY STATUS RESTARTS AGE
device-plugin-example-bgktv 1/1 Running 0 2m6s
Listing 6-30Pod listing
您可以从节点状态数据中检查自定义设备的状态。
$ kubectl get node minikube -w -o json | jq '.status.allocatable."extend-k8s.io/example"'
"3"
"2"
"1"
"2"
Listing 6-31Device information in the node status
该命令会监视节点,并仅打印自定义设备信息。由于设备插件用随机数量的设备更新,您应该会看到与前面类似的变化。它显示设备插件配置正确,并与kubelet
交互以设置节点状态。您可以通过CTRL+C
停止手表命令并返回终端。
现在,让我们创建一个 pod 来使用包含以下内容的定制设备。
apiVersion: v1
kind: Pod
metadata:
name: device-plugin-consumer
spec:
containers:
- name: pause
image: busybox
command: ["/bin/sleep", "1000"]
resources:
limits:
extend-k8s.io/example: 1
Listing 6-32Pod with custom device
当 pod 运行时,执行以下命令来检查环境变量。
$ kubectl exec device-plugin-consumer -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=device-plugin-consumer
K8S_DEVICE_PLUGIN_EXAMPLE=5a1b85e33a06f47501504a9c570e4e32
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
HOME=/root
Listing 6-33Container environment variables
环境变量列表显示K8S_DEVICE_PLUGIN_EXAMPLE
从设备插件传递到kubelet
并注入容器。最后一步补充了从 gRPC 服务器到容器的设备插件方法流程。
与存储和网络插件相比,创建和运行设备插件更简单。设备插件 API 相对较新,还没有类似于 CSI 和 CNI 的成熟标准。因此,在设备插件开发过程中,检查 API 变化和 Kubernetes 版本兼容性是必不可少的。
关键要点
Kubernetes 是一个开放的生态系统,不限制任何云提供商或本地系统。
Kubernetes 与基础设施层的交互可通过存储、网络和设备插件进行扩展。
存储插件扩展了 Kubernetes 中的卷供应,同时实现了 CSI 标准。
网络插件在实现 CNI 标准的同时扩展了集群中的容器网络。
设备插件扩展了供应商对定制设备的资源分配和使用。
在下一章中,我们将讨论 Kubernetes 世界中即将到来的趋势、平台和库。
七、即将到来的扩展点
眼睛盯着星星,脚踏实地。
—西奥多·罗斯福
美国第 26 任总统
Kubernetes 凭借其在云原生世界中的强大基础获得了成功。它提供了一组丰富的特性来管理容器化的应用,并提供了各种扩展点来添加新的功能。尽管如此,Kubernetes 并不是一个完成的项目;它可能是软件开发历史上最活跃的开源项目。因此,这最后一章将关注 Kubernetes 的未来趋势、扩展点和库。在这一章的最后,你将了解到更多关于 Kubernetes 世界的最新发展和未来趋势。Kubernetes 的光明未来会增加你的兴奋感,你会很高兴成为这个星空之旅的一部分。
在这最后一章中,将介绍两个即将到来的仍在开发中的扩展点。让我们从服务目录扩展 API 开始,使运行在 Kubernetes 中的应用能够使用外部托管服务。
服务目录
服务目录是 Kubernetes 中向其他托管服务敞开大门的扩展点。它支持在集群中运行的应用使用由云提供商运营的外部软件。假设您有一个 Kubernetes 集群和一个需要消息队列的应用。您基本上有两种选择:将消息队列部署到您的集群中,或者使用您的云提供商提供的消息队列服务。第一种选择非常灵活,但它带来了操作负担。对于后一种选择,您需要在 Kubernetes 集群和云提供商的消息队列服务之间找到一个桥。网桥是服务目录,具有图 7-1 中的以下架构。
图 7-1
服务目录概述
服务目录通过列出托管服务、提供实例和绑定到集群内运行的应用来扩展 Kubernetes API。外部服务使用由开放服务代理 API 定义的服务代理端点连接到集群。在 Kubernetes API 端,服务目录安装了一个扩展 API 服务器和一个控制器来管理servicecatalog.k8s.io
组下的以下 API 资源:
ClusterServiceBroker
:这是服务代理及其连接细节的定义。集群管理员使用ClusterServiceBroker
资源安装新的代理。ClusterServiceClass
:由ClusterServiceBroker
提供的托管服务。当一个新的ClusterServiceBroker
添加到集群中时,服务目录连接到代理并检索托管服务的列表以创建相应的ClusterServiceClass
资源。ClusterServicePlan
:描述ClusterServiceClass
的具体提供,如免费层、付费层或特定版本。ClusterServicePlan
资源是在新的代理安装后由服务目录创建的。ServiceInstance
:是ClusterServiceClass
的预配实例。当您需要托管服务的新实例时,您需要创建一个ServiceInstance
。然后,服务目录控制器连接到服务代理并提供服务实例。ServiceBinding
:在集群中的应用中使用ServiceInstance
的访问凭证。当您创建一个新的ServiceBinding
时,服务目录会创建一个带有到ServiceInstance
的连接细节的秘密。您可以将这个秘密装载到您的应用中,并连接到外部托管服务。
API 资源和服务目录控制器确保外部托管服务和计划在集群中可用。此外,它还支持创建新的服务实例,并绑定到集群中运行的应用。在下面的练习中,您将看到服务目录的运行,并使用托管应用扩展 Kubernetes 集群。
EXERCISE 1: SERVICE CATALOG IN ACTION
在本练习中,您将浏览所有服务目录功能,并了解它如何扩展 Kubernetes。首先将服务目录部署到集群,安装 service broker,最后创建一些托管服务。
注意本练习的其余部分是基于向集群部署资源,它需要以下先决条件:minikube
、kubectl
和helm
。
使用 minikube 和以下命令创建一个 Kubernetes 集群:
minikube start --kubernetes-version v1.19.0
。服务目录有一个要安装的舵图。因此,您需要首先添加它的图表存储库,然后使用以下命令安装它:
$ helm repo add svc-cat https://kubernetes-sigs.github.io/service-catalog "svc-cat" has been added to your repositories $ kubectl create namespace catalog namespace/catalog created $ helm install catalog svc-cat/catalog --namespace catalog .. NAME: catalog LAST DEPLOYED: ... NAMESPACE: catalog STATUS: deployed REVISION: 1 TEST SUITE: None
命令安装服务目录 API 资源和控制器。您可以使用以下命令查看定制资源列表:
$ kubectl get crd | grep servicecatalog.k8s.io clusterservicebrokers.servicecatalog.k8s.io clusterserviceclasses.servicecatalog.k8s.io clusterserviceplans.servicecatalog.k8s.io servicebindings.servicecatalog.k8s.io servicebrokers.servicecatalog.k8s.io serviceclasses.servicecatalog.k8s.io serviceinstances.servicecatalog.k8s.io serviceplans.servicecatalog.k8s.io
您需要安装 service broker 来管理第三方应用,并且它必须具有开放的 Service Broker API 来与服务目录进行交互。在
minikube
中已经有一个名为小代理的服务代理在工作,您可以使用以下命令部署它:$ helm repo add minibroker https://minibroker.blob.core.windows.net/charts "minibroker" has been added to your repositories $ kubectl create namespace minibroker namespace/minibroker created $ helm install minibroker minibroker/minibroker --namespace minibroker NAME: minibroker LAST DEPLOYED: Fri Mar 26 12:21:27 2021 NAMESPACE: minibroker STATUS: deployed REVISION: 1 TEST SUITE: None
Service Catalog controller creates
ClusterServiceClass
for each service that the broker provides. You can list the provided services with the following command:$ kubectl get clusterserviceclasses NAME EXTERNAL-NAME BROKER AGE mariadb mariadb minibroker 3m mongodb mongodb minibroker 3m mysql mysql minibroker 3m postgresql postgresql minibroker 3m rabbitmq rabbitmq minibroker 3m redis redis minibroker 3m
The broker service deployed in Step 3 provides the preceding listed services. In addition, these services should have plans as follows:
$ kubectl get clusterserviceplans NAME EXTERNAL-NAME BROKER CLASS AGE mariadb-10-1-26 10-1-26 minibroker mariadb 9m ... mongodb-3-4-10 3-4-10 minibroker mongodb 8m ... mysql-5-7-30 5-7-30 minibroker mysql 8m ... postgresql-9-6-2 9-6-2 minibroker postgresql 8m ... rabbitmq-3-6-10 3-6-10 minibroker rabbitmq 8m ... redis-5-0-7 5-0-7 minibroker redis 8m
这个长列表由代理中可用的每个预先配置的服务计划组成。
现在,是时候创建一些托管数据库实例了。在名为 db-instance.yaml 的文件中创建一个包含以下内容的
ServiceInstance
资源,并将其部署到集群:apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: name: db-instance namespace: test-db spec: clusterServiceClassExternalName: mysql clusterServicePlanExternalName: 5-7-30 $ kubectl create namespace test-db namespace/test-db created $ kubectl apply -f db-instance.yaml serviceinstance.servicecatalog.k8s.io/db-instance created
等待几秒钟,您的数据库实例就准备好了:
$ kubectl get serviceinstances -n test-db NAME CLASS PLAN STATUS AGE db-instance ClusterServiceClass/mysql 5-7-30 Ready 2m53s
创建一个
ServiceBinding
资源,以便在应用中使用托管数据库。当ServiceBinding
资源被创建时,服务目录控制器将连接到代理并检索连接细节和凭证。使用以下内容创建一个ServiceBinding
,并将其放入名为 db-binding.yaml 的文件中:apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: name: db-binding namespace: test-db spec: instanceRef: name: db-instance $ kubectl apply -f db-binding.yaml servicebinding.servicecatalog.k8s.io/db-binding created
几秒钟后,连接信息和凭证将被收集到一个秘密资源中,如下所示:
$ kubectl -n test-db describe secret db-binding Name: db-binding Namespace: test-db Labels: <none> Annotations: <none> Type: Opaque Data ==== database: 0 bytes mysql-password: 10 bytes password: 10 bytes username: 4 bytes uri: 81 bytes host: 52 bytes mysql-root-password: 10 bytes port: 4 bytes protocol: 5 bytes
创建一个 pod,用于连接到数据库并使用密码中的凭证。使用以下内容并保存到名为 my-app.yaml 的文件中:
apiVersion: v1 kind: Pod metadata: name: my-app namespace: test-db spec: containers: - name: app image: mysql command: ["bash"] args: ["-c", "sleep infinity"] env: - name: MYSQL_HOST valueFrom: secretKeyRef: name: db-binding key: host - name: MYSQL_TCP_PORT valueFrom: secretKeyRef: name: db-binding key: port - name: MYSQL_USER valueFrom: secretKeyRef: name: db-binding key: username - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: db-binding key: password restartPolicy: Never $ kubectl apply -f my-app.yaml pod/my-app created
Wait until the test-db pod is in Running state and then connect to the pod and execute MySQL commands to validate managed database application:
$ kubectl exec -n test-db my-app -it -- bash root@my-app:/# mysql -u$MYSQL_USER -p$MYSQL_PASSWORD -e "select version()" mysql: [Warning] Using a password on the command line interface can be insecure. +-----------+ | version() | +-----------+ | 5.7.30 | +-----------+
它显示了在 Kubernetes 集群中运行的应用可以连接到数据库并运行查询。连接信息和凭证由服务目录创建的密码提供。
您可以使用以下命令清理资源和安装:
$ kubectl delete namespace test-db $ kubectl delete servicebinding db-binding $ kubectl delete serviceinstances test-db $ kubectl delete clusterservicebrokers minibroker $ helm delete --purge minibroker $ kubectl delete namespace minibroker $ helm delete --purge catalog $ kubectl delete namespace catalog
在上一个练习中,涵盖了服务目录扩展的所有方面。您已经开始安装服务目录 API 资源和控制器。然后,按照 Open service broker API 安装 Service Broker。接下来,创建一个托管数据库实例,并从 Kubernetes 集群中运行的应用进行连接。服务目录扩展了 Kubernetes,用于管理外部应用并在集群内部使用它们。它在集群内外运行的应用之间创建了一个强大的连接。
为了用服务目录扩展 Kubernetes,您需要为您的第三方外部应用开发和运行服务代理。服务目录 API 和控制器将使它们集成到集群中,以便集群中的应用可以连接和使用。
接下来,将介绍如何扩展 Kubernetes API 来提供、升级和操作多个 Kubernetes 集群。
集群 API
集群 API 是 Kubernetes 的一种本地方式,它提供了一种声明式 API 来提供、升级和操作 Kubernetes 集群。换句话说,它是 Kubernetes 管理 Kubernetes 集群的扩展点。Kubernetes 集群和基础设施,如虚拟机、网络、存储或负载平衡器,可以定义为任何其他 Kubernetes 本地资源,如部署或 pods。在集群中运行的平台操作者自动执行集群和底层基础设施的生命周期。这种方法的主要优势是跨不同云或基础架构提供商的可重复且一致的集群管理。
运行 Kubernetes 集群并不简单,因为它需要正确配置多个组件。对于 Kubernetes 生态系统中的新手来说,这是一个很高的准入门槛。此外,还有超过 60 个 Kubernetes 认证的发行版和安装程序以及它们的配置和风格。kubeadm
是 Kubernetes 社区对遵循最佳实践的引导集群的回应。虽然kubeadm
解决了集群安装的复杂性,但是它没有解决集群生命周期管理的问题。集群 API 是填补日常集群管理操作相关空白的解决方案,例如配置新虚拟机、负载平衡器和自动化。Cluster API 在 Kubernetes API 中创建一个扩展点,以声明的方式定义和管理 Kubernetes 集群。您可以连接到任何基础设施或云系统来提供资源,或者连接到 bootstrap provider 来安装 Kubernetes 控制平面。
集群 API 中有两种类型的集群:
工作负载集群是通过集群 API 管理的 Kubernetes 集群。
管理集群通过已安装的提供者管理工作负载集群的生命周期。管理集群拥有集群 API CRDs,并遵循控制器模式来管理工作负载集群的生命周期。
两种类型的提供者管理工作负载集群的生命周期:
基础设施提供商负责创建计算、存储和网络等基础设施资源。
引导提供者负责安装 Kubernetes 控制平面,并将工作节点加入集群。
供应商和集群之间的关系可以总结如下,如图 7-2 所示。
图 7-2
集群 API 概述
您可以使用带有两个扩展点的集群 API 来扩展 Kubernetes 集群管理:添加新的基础设施提供者和添加新的引导提供者。您可以添加新的基础架构提供商,以根据您的定制需求创建裸机节点、虚拟机或任何其他虚拟化实例。类似地,您可以开发和部署新的引导提供程序来实现 Kubernetes 内部操作的定制需求。然而,随着集群 API 资源和控制器的不断更新,您应该查看最新的参考文档和集群 API 手册。
关键要点
Kubernetes 不是成品;它正处于积极发展阶段。
为了进一步扩展 Kubernetes,还有一些扩展点正在开发中。
服务目录是将外部服务纳入集群的扩展点。
集群 API 提供了一个声明性的 API 来管理其他 Kubernetes 集群的生命周期。
结论
Kubernetes 是一个带有扩展点的复杂容器编排器,本书通过对功能和底层扩展模式进行分组,涵盖了所有可用的扩展点。在每一章中,你都从技术的角度学习了扩展点,并且亲自动手。根据您在整本书中获得的信息和经验,我希望您对 Kubernetes 的未来以及您将如何扩展 Kubernetes 以丰富其生态系统更有雄心。