今年,主要的欧洲 Kubernetes 会议 - KubeCon + CloudNativeCon Europe 2020 - 是虚拟的。 不过,这样的格式变化并没有妨碍我们交付蓄谋已久的报告《走? 猛击! 认识一下 Shell 操作员”致力于我们的开源项目
本文受到演讲的启发,提出了一种简化为 Kubernetes 创建运算符的过程的方法,并展示了如何使用 shell 运算符以最少的努力创建自己的运算符。
介绍
在 Flant,我们不断优化和自动化一切。 今天我们将讨论另一个令人兴奋的概念。 见面: 云原生 shell 脚本!
然而,让我们从这一切发生的背景开始:Kubernetes。
Kubernetes API 和控制器
Kubernetes 中的 API 可以表示为一种文件服务器,其中包含每种类型对象的目录。 该服务器上的对象(资源)由 YAML 文件表示。 此外,服务器还有一个基本的 API,允许您执行三件事:
- 收到 资源的种类和名称;
- 改变 资源(在这种情况下,服务器仅存储“正确”的对象 - 所有格式不正确或用于其他目录的对象都将被丢弃);
- 遵循 对于资源(在这种情况下,用户立即收到其当前/更新版本)。
因此,Kubernetes 充当一种文件服务器(用于 YAML 清单),具有三种基本方法(是的,实际上还有其他方法,但我们现在将忽略它们)。
问题是服务器只能存储信息。 为了让它发挥作用,你需要 调节器 - Kubernetes 世界中第二重要和基本的概念。
有两种主要类型的控制器。 第一个从 Kubernetes 获取信息,根据嵌套逻辑对其进行处理,然后将其返回给 K8s。 第二种从 Kubernetes 获取信息,但与第一种不同的是,它会更改某些外部资源的状态。
让我们仔细看看 Kubernetes 中创建 Deployment 的过程:
- 部署控制器(包含在
kube-controller-manager
)接收有关 Deployment 的信息并创建 ReplicaSet。 - ReplicaSet 根据此信息创建两个副本(两个 Pod),但这些 Pod 尚未调度。
- 调度程序调度 Pod 并将节点信息添加到其 YAML 中。
- Kubelet 对外部资源(例如 Docker)进行更改。
然后以相反的顺序重复整个序列:kubelet 检查容器,计算 pod 的状态并将其发回。 ReplicaSet 控制器接收状态并更新副本集的状态。 部署控制器也会发生同样的事情,用户最终获得更新的(当前)状态。
Shell 操作符
事实证明,Kubernetes 是基于各种控制器的联合工作(Kubernetes 操作员也是控制器)。 问题来了,如何以最小的努力创建自己的操作符? 我们开发的这款产品可以拯救您
简单的例子:复制秘密
让我们看一个简单的例子。
假设我们有一个 Kubernetes 集群。 它有一个命名空间 default
带着一些秘密 mysecret
。 此外,集群中还有其他命名空间。 其中一些贴有特定的标签。 我们的目标是将 Secret 复制到带有标签的命名空间中。
由于集群中可能出现新的命名空间,并且其中一些命名空间可能具有此标签,因此任务变得复杂。 另一方面,当标签被删除时,Secret也应该被删除。 除此之外,Secret 本身也可以更改:在这种情况下,必须将新的 Secret 复制到所有带有标签的命名空间。 如果任何命名空间中的 Secret 被意外删除,我们的操作员应该立即恢复它。
现在任务已经制定完毕,是时候开始使用 shell 操作符来实现它了。 但首先值得谈谈 shell 操作符本身。
shell 操作符如何工作
与 Kubernetes 中的其他工作负载一样,shell-operator 在自己的 pod 中运行。 在这个pod目录中 /hooks
存储可执行文件。 这些可以是 Bash、Python、Ruby 等中的脚本。 我们称这样的可执行文件为钩子(挂钩).
Shell-operator 订阅 Kubernetes 事件并运行这些钩子来响应我们需要的事件。
shell 操作员如何知道运行哪个钩子以及何时运行? 要点是每个钩子都有两个阶段。 在启动期间,shell 操作符运行带有参数的所有钩子 --config
这是配置阶段。 之后,挂钩以正常方式启动 - 响应它们所附加的事件。 在后一种情况下,钩子接收绑定上下文(绑定上下文) - JSON 格式的数据,我们将在下面更详细地讨论。
在 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
表示不需要在内存中保留完整的对象。
追踪目标秘密
我们订阅所有指定了注释的 Secret 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
过滤掉除命名空间和参数之外的所有信息 resourceVersion
。 最后一个参数在创建机密时传递给注释:它允许您比较机密的版本并使其保持最新。
以这种方式配置的钩子在执行时将接收上述三个绑定上下文。 它们可以被认为是一种快照(快照) 簇。
基于所有这些信息,可以开发基本算法。 它迭代所有名称空间并且:
- 如果
hasLabel
事项true
对于当前命名空间:- 将全局秘密与本地秘密进行比较:
- 如果它们相同,则不执行任何操作;
- 如果它们不同 - 执行
kubectl replace
илиcreate
;
- 将全局秘密与本地秘密进行比较:
- 如果
hasLabel
事项false
对于当前命名空间:- 确保 Secret 不在给定的命名空间中:
- 如果本地 Secret 存在,请使用删除它
kubectl delete
; - 如果未检测到本地 Secret,则不执行任何操作。
- 如果本地 Secret 存在,请使用删除它
- 确保 Secret 不在给定的命名空间中:
这就是我们如何使用 35 行 YAML 配置和大约相同数量的 Bash 代码创建一个简单的 Kubernetes 控制器! shell 操作符的工作是将它们连接在一起。
然而,复制机密并不是该实用程序的唯一应用领域。 这里还有几个例子来展示他的能力。
示例 1:对 ConfigMap 进行更改
让我们看一下由三个 Pod 组成的 Deployment。 Pod 使用 ConfigMap 来存储一些配置。 当 Pod 启动时,ConfigMap 处于某种状态(我们称之为 v.1)。 因此,所有 Pod 都使用此特定版本的 ConfigMap。
现在我们假设 ConfigMap 已更改 (v.2)。 但是,Pod 将使用以前版本的 ConfigMap (v.1):
我怎样才能让他们切换到新的 ConfigMap (v.2)? 答案很简单:使用模板。 让我们在该部分添加一个校验和注释 template
部署配置:
结果,这个校验和将被注册到所有的 pod 中,并且它将与 Deployment 的校验和相同。 现在您只需要在 ConfigMap 更改时更新注释即可。 在这种情况下,shell 运算符就派上用场了。 您所需要做的就是编程 一个将订阅 ConfigMap 并更新校验和的钩子.
如果用户对 ConfigMap 进行更改,shell 操作员将注意到它们并重新计算校验和。 之后 Kubernetes 的魔力将发挥作用:编排器将杀死 pod,创建一个新的 pod,等待它成为 Ready
,然后继续下一个。 这样一来,Deployment就会同步并切换到新版本的ConfigMap。
示例 2:使用自定义资源定义
如您所知,Kubernetes 允许您创建自定义类型的对象。 例如,您可以创建种类 MysqlDatabase
。 假设该类型有两个元数据参数: name
и namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
我们有一个具有不同命名空间的 Kubernetes 集群,可以在其中创建 MySQL 数据库。 在这种情况下,可以使用 shell-operator 来跟踪资源 MysqlDatabase
,将它们连接到 MySQL 服务器并同步集群的所需状态和观察到的状态。
示例3:集群网络监控
如您所知,使用 ping 是监控网络的最简单方法。 在这个例子中,我们将展示如何使用 shell-operator 来实现这样的监控。
首先,您需要订阅节点。 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-operator 会运行钩子的另一个实例吗?
- 比如说,如果集群中同时发生五个事件怎么办?
- shell 操作符会并行处理它们吗?
- 消耗的资源(例如内存和CPU)怎么样?
幸运的是,shell-operator 有一个内置的排队机制。 所有事件都按顺序排队和处理。
让我们用例子来说明这一点。 假设我们有两个钩子。 第一个事件进入第一个钩子。 一旦处理完成,队列就会向前移动。 接下来的三个事件被重定向到第二个钩子 - 它们被从队列中删除并以“捆绑”的形式进入队列。 那是 钩子接收事件数组 ——或者更准确地说,是一组绑定上下文。
还有这些 事件可以合并为一个大事件。 该参数负责此操作 group
在绑定配置中。
您可以创建任意数量的队列/挂钩及其各种组合。 例如,一个队列可以使用两个钩子,反之亦然。
您需要做的就是相应地配置该字段 queue
在绑定配置中。 如果未指定队列名称,则挂钩在默认队列(default
)。 这种排队机制可以让你彻底解决使用钩子时的所有资源管理问题。
结论
我们解释了什么是 shell-operator,展示了如何使用它来快速、轻松地创建 Kubernetes 运算符,并给出了几个使用示例。
有关 shell 操作符的详细信息以及如何使用它的快速教程可在相应的
如果您喜欢它,我们总是很高兴在 GitHub 上看到新问题/PR/stars,顺便说一句,您可以在其中找到其他内容
视频和幻灯片
表演视频(约 23 分钟):
报告介绍:
PS
另请阅读我们的博客:
- «
使用 shell-operator 轻松创建 Kubernetes Operator:一年来项目进展 “; - «
引入 shell-operator:为 Kubernetes 创建操作符变得更加容易 “; - «
准备一个Kubernetes集群是不是简单方便? 宣布插件运营商 “; - «
扩展和补充 Kubernetes”(评论和视频报告) .
来源: habr.com