受这次演讲的启发,本文介绍了一种简化为Kubernetes创建运算符的过程的方法,并展示了如何使用Shell运算符以最小的努力来制作自己的运算符。
我们以报告的形式呈现视频(约23分钟,英语,比文章多得多),并以文本形式摘录主要内容。走!
在Flant,我们不断优化和自动化一切。今天,我们将讨论另一个令人兴奋的概念。认识云原生的shell脚本!
但是,让我们从发生所有这些情况的上下文开始-Kubernetes。
Kubernetes API和控制器
Kubernetes中的API可以表示为一种文件服务器,其中包含每种对象类型的目录。此服务器上的对象(资源)由YAML文件表示。此外,服务器还有一个基本的API可以完成三件事:
- 通过种类和名称获得资源;
- 更改资源(在这种情况下,服务器仅存储“正确的”对象-丢弃所有格式错误或用于其他目录的对象);
- ( / ).
因此,Kubernetes充当一种文件服务器(用于YAML清单),它具有三种基本方法(是的,实际上还有其他方法,但我们现在将省略它们)。
问题在于服务器只能存储信息。为了使其工作,您需要一个控制器-Kubernetes世界中第二重要的基础概念。
控制器有两种主要类型。第一个从Kubernetes获取信息,根据嵌套逻辑对其进行处理,然后将其返回给K8s。第二种从Kubernetes获取信息,但是与第一种不同,它改变了某些外部资源的状态。
让我们仔细看看在Kubernetes中创建Deployment的过程:
- Deployment Controller(包含在中
kube-controller-manager
)接收有关Deployment的信息并创建一个ReplicaSet。 - ReplicaSet基于此信息创建两个副本(两个Pod),但是尚未计划这些Pod。
- 调度程序调度pod并将节点信息添加到其YAML。
- Kubelets对外部资源(例如Docker)进行更改。
然后以相反的顺序重复整个过程:kubelet检查容器,计算容器的状态,然后将其发送回去。ReplicaSet控制器获取状态并更新副本集的状态。部署控制器也会发生同样的事情,并且用户最终会获得更新(当前)状态。
壳牌运营商
事实证明,Kubernetes基于各种控制器的协作(Kubernetes运营商也是控制器)。问题出现了,如何以最小的努力创建自己的运营商?在这里,由我们开发的shell操作员可以提供帮助。它允许系统管理员使用熟悉的方法创建自己的语句。
简单示例:复制机密
让我们看一个简单的例子。
假设我们有一个Kubernetes集群。它有一个
default
带有一些Secret的名称空间mysecret
。此外,集群中还有其他名称空间。其中一些带有特定标签。我们的目标是将Secret复制到带有标签的名称空间中。
新的名称空间可能会出现在群集中,并且其中一些可能带有此标签,这一事实使任务变得复杂。另一方面,删除标签时,还必须删除秘密。除所有内容外,Secret本身也可以更改:在这种情况下,必须将新的Secret复制到带有标签的所有命名空间中。如果在任何命名空间中意外删除了Secret,我们的操作员应立即将其还原。
现在已经制定了任务,是时候开始使用Shell-operator实施它了。但首先,值得一提的是关于Shell操作程序本身的一些话。
Shell操作员如何工作
像Kubernetes中的其他工作负载一样,shell运算符在其pod中运行。此Pod在目录中
/hooks
包含可执行文件。这些可以是Bash,Python,Ruby等脚本。我们称这些可执行文件为hooks。
Shell运算符订阅Kubernetes事件并触发这些钩子以响应我们需要的任何事件。
Shell操作员如何知道运行哪个钩子以及何时运行?关键是每个挂钩都有两个阶段。在启动时,shell运算符使用参数运行所有钩子
--config
-这是配置阶段。然后,以正常方式启动钩子-响应钩子所附着的事件。在后一种情况下,钩子接收绑定上下文)-JSON格式的数据,我们将在下面对此进行详细讨论。
在Bash中创建运算符
我们现在准备实施。为此,我们需要编写两个函数(顺便说一句,我们建议您使用shell_lib库,它大大简化了Bash中的编写钩子):
- 在配置阶段需要第一个-它显示绑定上下文;
- 第二个包含钩子的主要逻辑。
#!/bin/bash
source /shell_lib.sh
function __config__() {
cat << EOF
configVersion: v1
# BINDING CONFIGURATION
EOF
}
function __main__() {
# THE LOGIC
}
hook::run "$@"
下一步是确定我们需要哪些对象。就我们而言,我们需要跟踪:
- 变更的秘密来源;
- 集群中的所有名称空间,这样您就可以知道标签所附加的名称空间;
- 目标机密,以确保它们都与源机密同步。
订阅秘密来源
绑定配置对他来说非常简单。我们表明我们对Secret感兴趣,其名称
mysecret
空间中的名称为default
:
function __config__() {
cat << EOF
configVersion: v1
kubernetes:
- name: src_secret
apiVersion: v1
kind: Secret
nameSelector:
matchNames:
- mysecret
namespace:
nameSelector:
matchNames: ["default"]
group: main
EOF
结果,该挂钩将在源密钥(
src_secret
)更改并接收以下绑定上下文时运行:
如您所见,挂钩包含名称和整个对象。
跟踪名称空间
现在,您需要订阅名称空间。为此,我们将指定以下绑定配置:
- name: namespaces
group: main
apiVersion: v1
kind: Namespace
jqFilter: |
{
namespace: .metadata.name,
hasLabel: (
.metadata.labels // {} |
contains({"secret": "yes"})
)
}
group: main
keepFullObjectsInMemory: false
如您所见,配置中出现了一个名为jqFilter的新字段。顾名思义,它会
jqFilter
过滤掉所有不必要的信息,并使用我们感兴趣的字段创建一个新的JSON对象。具有此配置的钩子将收到以下绑定上下文:
它包含
filterResults
集群中每个名称空间的数组。hasLabel
指示标签是否附加到给定名称空间的布尔变量。选择器keepFullObjectsInMemory: false
说,不需要将完整的对象保留在内存中。
跟踪秘密目标
我们订阅所有带有注解的Secrets
managed-secret: "yes"
(这些是我们的目标dst_secrets
):
- name: dst_secrets
apiVersion: v1
kind: Secret
labelSelector:
matchLabels:
managed-secret: "yes"
jqFilter: |
{
"namespace":
.metadata.namespace,
"resourceVersion":
.metadata.annotations.resourceVersion
}
group: main
keepFullObjectsInMemory: false
在这种情况下,
jqFilter
过滤掉除namespace和parameter之外的所有信息resourceVersion
。创建机密时,最后一个参数传递给了注释:它允许您比较机密的版本并保持最新。
以这种方式配置的钩子在执行时将接收上述三个绑定上下文。可以将它们视为集群的快照。
基于所有这些信息,可以开发一种基本算法。遍历所有名称空间,并:
- 如果
hasLabel
与true
当前名称空间相关:- 将全局机密与本地机密进行比较:
- 如果它们相同,则不执行任何操作;
- 如果它们不同,则执行
kubectl replace
或create
;
- 将全局机密与本地机密进行比较:
- 如果
hasLabel
与false
当前名称空间相关:
- 确保Secret不在给定的名称空间中:
- 如果存在本地机密,请使用删除
kubectl delete
; - 如果未找到本地机密,则不执行任何操作。
- 如果存在本地机密,请使用删除
- 确保Secret不在给定的名称空间中:
您可以通过示例在Bash的资源库中下载算法实现。
这就是我们能够使用35行YAML配置和大约相同数量的Bash代码创建简单的Kubernetes控制器的方式!shell运算符的工作是将它们串在一起。
但是,复制机密不是该实用程序唯一的应用领域。这里还有一些例子来展示他的能力。
示例1:对ConfigMap进行更改
让我们看一下三脚架部署。 Pod使用ConfigMap来存储一些配置。启动Pod时,ConfigMap处于某种状态(我们称之为v.1)。因此,所有吊舱均使用此特定版本的ConfigMap。
现在,假设ConfigMap已更改(第2版)。但是,Pod将使用ConfigMap(v.1)的旧版本:
如何使它们迁移到新的ConfigMap(v.2)?答案很简单:使用模板。让我们在“
template
部署配置”部分添加一个校验和注释:
结果,此校验和将被注册到所有Pod中,并且与Deployment中的相同。现在,您只需要在ConfigMap更改时更新注释。在这种情况下,shell操作符会派上用场。您需要做的只是编写一个钩子,该钩子将订阅ConfigMap并更新校验和。
如果用户对ConfigMap进行了更改,则shell操作员将注意到它们并重新计算校验和。然后,Kubernetes的魔力开始发挥作用:协调器将杀死该吊舱,创建一个新的吊舱,等待其变为
Ready
,然后继续前进。结果,部署将同步并迁移到新版本的ConfigMap。
示例2:使用自定义资源定义
如您所知,Kubernetes允许您创建对象的自定义类型(种类)。例如,您可以创建kind
MysqlDatabase
。假设此类型具有两个元数据参数:name
和namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
我们有一个带有不同名称空间的Kubernetes集群,可以在其中创建MySQL数据库。在这种情况下,可以使用shell-operator跟踪资源
MysqlDatabase
,将资源连接到MySQL服务器,并同步所需的集群状态和观察到的集群状态。
示例3:监视群集网络
如您所知,使用ping是监视网络的最简单方法。在此示例中,我们将展示如何使用shell运算符实现这种监视。
首先,您需要订阅节点。Shell运算符需要每个节点的名称和IP地址。在他们的帮助下,它将对这些节点执行ping操作。
configVersion: v1
kubernetes:
- name: nodes
apiVersion: v1
kind: Node
jqFilter: |
{
name: .metadata.name,
ip: (
.status.addresses[] |
select(.type == "InternalIP") |
.address
)
}
group: main
keepFullObjectsInMemory: false
executeHookOnEvent: []
schedule:
- name: every_minute
group: main
crontab: "* * * * *"
该参数
executeHookOnEvent: []
可防止响应于任何事件(即,响应于节点的更改,添加,删除)而启动挂钩。但是,它将按时间表(每分钟)运行(并更新主机列表),具体取决于该字段schedule
。
现在出现了问题,我们如何确切地知道诸如丢包之类的问题?让我们看一下代码:
function __main__() {
for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
packets_lost=0
if ! ping -c 1 "$node_ip" -t 1 ; then
packets_lost=1
fi
cat >> "$METRICS_PATH" <<END
{
"name": "node_packets_lost",
"add": $packets_lost,
"labels": {
"node": "$node_name"
}
}
END
done
}
我们遍历节点列表,获取它们的名称和IP地址,执行ping操作并将结果发送给Prometheus。Shell-operator可以将指标导出到Prometheus,并将其保存到根据环境变量中指定的路径定位的文件中
$METRICS_PATH
。
这样,您便可以为集群中的简单网络监控操作员。
队列机制
如果不描述shell运算符中内置的另一种重要机制,本文将是不完整的。想象一下,它响应集群中的事件执行了一个钩子。
- 如果群集中同时发生另一个事件,会发生什么?
- shell运算符会启动该钩子的另一个实例吗?
- 但是,如果说集群中立即发生五个事件该怎么办?
- shell运算符会并行处理它们吗?
- 内存和CPU等消耗的资源呢?
幸运的是,shell运算符具有内置的排队机制。所有事件都按顺序排队和处理。
让我们用示例说明这一点。假设我们有两个钩子。第一个事件转到第一个挂钩。处理完成后,队列前进。接下来的三个事件将重定向到第二个挂钩-将它们从队列中删除并以“批处理”的形式馈入队列。也就是说,该钩子接收事件数组,或更准确地说,接收绑定上下文数组。
而且,这些事件可以合并为一个大事件。
group
绑定配置中的参数对此负责。
您可以创建任意数量的队列/钩子及其各种组合。例如,一个队列可以使用两个钩子,反之亦然。
您需要做的就是
queue
在绑定配置中相应地调整字段。如果未指定队列名称,则挂钩在默认队列(default
)上运行。使用排队机制,您可以在使用挂钩时完全解决所有资源管理问题。
结论
我们讨论了什么是shell运算符,展示了如何使用它来快速轻松地创建Kubernetes运算符,并给出了几个使用它的示例。
有关shell操作器的详细信息以及使用它的快速指南,可在GitHub上的相应存储库中找到。如有疑问,请随时与我们联系:您可以在特殊的Telegram组(俄语)或在此论坛(英语)中讨论它们。
并且,如果您喜欢它,我们总是很高兴在GitHub上发布新期刊/ PR / star,顺便说一下,您可以在那里找到其他有趣的项目。其中,值得强调的是addon-operator,它是shell运算符的哥哥... 该实用程序使用Helm图表来安装加载项,能够提供更新并监视各种图表参数/值,控制图表的安装过程,还可以根据集群中的事件对其进行修改。
影片和幻灯片
表演视频(〜23分钟):
报告介绍:
聚苯乙烯
另请参阅我们的博客: