Shell-operator:用于简化Kubernetes operator的创建


我们很高兴在此介绍我们新的开源方案,它能将Kubernetes中Operator的开发提升到一个全新的更简单的水平。它可以让你在15分钟内将你的小脚本变成完全成熟的Operator而吸引你。欢迎shell-operator



目标

shell-operator的思路很简单,它订阅来自Kubernetes对象的事件,并在事件发生后执行外部程序,为其提供有关事件的信息:



在我们运行Kubernetes集群期间,很多小任务开始显现。在Flant,我们迫切想要以正确的方式来自动化它们,因此我们觉得需要更智能的解决方案。通常你可以使用基本的bash脚本来解决所有这些任务,但是如你所知,更推荐的方式是使用Golang来编写Operators。很显然,为每个小任务分别开发成熟的Operator将会很低效。

15分钟内创建一个Operator

我们将举一个在Kubernetes集群中可以被自动化的例子,以及shell-operator可以如何帮助我们。我们将尝试复制用于访问Docker仓库的凭证。

使用私有仓库镜像的Pod应在其清单中包含指定的用于访问仓库的secret。这个secret必须在创建Pod之前先创建在每个命名空间中。你可以手动执行此操作,但是,如果我们将配置动态的多个环境,我们将为单个应用程序创建许多命名空间。在多个应用程序(甚至两个或三个)的情况下,secret的数量会变得巨大。关于secret还有一个需求:我们希望能够偶尔更改(注册表)仓库的访问密钥。因此,手动解决方案变得非常低效,你必须自动创建和更新secret。

简单的自动化

我们来写一个脚本,每N秒运行一次,并检查命名空间中secret是否存在。如果secret不存在,那么它将会被创建。这个解决方案的优势是它看起来就像是cron中的一个shell脚本,一种经典且易于理解的方法。缺点是在此脚本的两次启动之间的间隔期间可能会出现一些新的命名空间,因此在一段时间内它将不会持有这个secret。这种情况会导致启动Pod的过程中出错。

使用shell-operator进行自动化

为了使我们的脚本准确运行,经典的cron执行应该被当有新增命名空间事件发生时的执行所取代。在这种情况下,你可以在使用之前创建一个secret。让我们看看如何使用shell-operator来实现这个功能。

首先,我们先分析一下脚本,就shell-operator而言,脚本都被称之为“钩子“。每个钩子在使用--config标志执行时都会通知shell-operator将其绑定(即需要执行哪些事件)。在我们的例子中,我们将使用onKubernetesEvent
#!/bin/bash
if [[ $1 == "--config" ]] ; then
cat <<EOF
{
"onKubernetesEvent": [
{
  "kind": "namespace",
  "event": [ "add" ]
}
]
}
EOF
fi

在这里,我们定义我们关注的namespace类型的添加(add)对象事件。

现在我们需要添加当事件发生时需要执行的代码:
#!/bin/bash
if [[ $1 == "--config" ]] ; then
# configuration
cat <<EOF
{
"onKubernetesEvent": [
{
  "kind": "namespace",
  "event": [ "add" ]
}
]
}
EOF
else
# response:
# find out what namespace has emerged
createdNamespace=$(jq -r '.[0].resourceName' $BINDING_CONTEXT_PATH)
# create the appropriate secret in it
kubectl create -n ${createdNamespace} -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
...
data:
...
EOF
fi

真棒!我们现在已有一个简洁且漂亮的脚本,想让它能真正发挥作用,我们需要准备一个镜像并将其跑在集群中。

使用钩子制作我们的镜像

你可以很轻易观察到我们在脚本里面使用了kubectljq命令。这意味着镜像中需要包含钩子,shell-operator二进制文件(它将监视事件并执行这个钩子),以及钩子需要用到的命令(kubectljq)。hub.docker.com上已提供了包含shell-operator,kubectl和jq的即用型镜像。现在是时候使用Dockerfile来添加一个钩子:
$ cat Dockerfile
FROM flant/shell-operator:v1.0.0-beta.1-alpine3.9
ADD namespace-hook.sh /hooks
$ docker build -t registry.example.com/my-operator:v1 .
$ docker push registry.example.com/my-operator:v1

在集群中运行

我们再来看看这个钩子,这次我们将关注具体的操作以及它在集群中执行的对象:
  1. 它订阅了namespace的创建事件;
  2. 它在不与它所运行的命名空间相同的空间创建一个secret。


这里我们会发现运行这个镜像的Pod需要有执行这些操作的权限。你可以授权给一个ServiceAccount。由于我们是关注整个集群中的对象,那么权限需要使用ClusterRoleClusterRoleBinding形式来配置。

YAML最终配置描述如下:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: monitor-namespaces-acc
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: monitor-namespaces
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: monitor-namespaces
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: monitor-namespaces
subjects:
- kind: ServiceAccount
name: monitor-namespaces-acc
namespace: example-monitor-namespaces

你可以将创建的镜像部署为一个简单的Deployment:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: my-operator
spec:
template:
spec:
  containers:
  - name: my-operator
    image: registry.example.com/my-operator:v1
  serviceAccountName: monitor-namespaces-acc

为方便起见,我们将创建一个单独的命名空间,用于运行shell-operator并应用创建的部署清单:
$ kubectl create ns example-monitor-namespaces
$ kubectl -n example-monitor-namespaces apply -f rbac.yaml
$ kubectl -n example-monitor-namespaces apply -f deployment.yaml

好了,shell-operator启动,它将订阅命名空间创建事件并在需要时执行钩子。



这样一个简单的shell脚本就变成了Kubernetes中一个真正的Operator,并成为集群的一部分。这样做的好处是我们避免了使用Golang来开发Operator的复杂过程:


过滤

关于对象的观察很棒,但我们通常需要响应对象中某些属性的更改,例如,增加/减少部署中的副本数量或对象对标签中的任何更新。

当一个事件发生时,shell-operator接收该对象的JSON清单。在此JSON中,你可以选择要监视的属性,并仅在更改时启动钩子。jqFilter字段可以帮助你完成这点:你应该输入将应用于JSON清单的jq表达式。

举个例子,要响应Deployment对象标签中的修改,你必须从metadata字段中提取labels字段。这个例子中你将需要如下的配置:
cat <<EOF
{
"onKubernetesEvent": [
{
  "kind": "deployment",
  "event":["update"],
  "jqFilter": ".metadata.labels"
}
]
}
EOF

jqFilter表达式将Deployment的长长的JSON清单转换成带有标签的简短的JSON:


shell-operator将只会在这个简短的JSON发生变化时执行钩子。其它属性的变更将会被忽略。

钩子的执行上下文

钩子的配置允许你指定几种事件。例如你可以定义两个Kubernetes事件和两个计划调度:
{
"onKubernetesEvent": [
{
  "name": "OnCreatePod",
  "kind": "pod",
  "event": [
    "add"
  ]
},
{
  "name": "OnModifiedNamespace",
  "kind": "namespace",
  "event": [
    "update"
  ],
  "jqFilter": ".metadata.labels"
}
],
"schedule": [
{
  "name": "every 10 min",
  "crontab": "0 */10 * * * *"
},
{
  "name": "on Mondays at 12:10",
  "crontab": "0 10 12 * * 1"
}
]


注意:shell-operator支持以crontab样式运行脚本!你可以在文档中找到额外的信息。

为了区分钩子执行的原因,shell-operator会创建一个临时文件并将其路径保存到BINDING_CONTEXT_TYPE变量中。此文件包含了执行钩子的原因的JSON描述。例如,每隔10分钟将会使用以下内容启动钩子:
[{ "binding": "every 10 min" }]

在周一的话它将以以下内容启动:
[{ "binding": "every 10 min" }, { "binding": "on Mondays at 12:10" }]

同时将有onKubernetesEvent调用的更详细的JSON,因为它包含了对象的描述:
[
{
"binding": "onCreatePod",
"resourceEvent": "add",
"resourceKind": "pod",
"resourceName": "foo",
"resourceNamespace": "bar"
}


你能通过名称来全面了解字段的内容(更多详细信息可在文档中找到)。使用jq从resourceName获取资源名称的示例已经在复制secret的钩子中展示:
jq -r '.[0].resourceName' $BINDING_CONTEXT_PATH

你可以通过类似的方式去获取到其它字段。

下一步是什么呢?

在该项目仓库中的/examples directory目录里包含了一些可以直接在集群中使用的示例。你可以将它们用作你开发自己的钩子的基础。

Shell-operator同样支持使用Prometheus来收集指标。METRICS章节已描述了这些可用的指标。

你能轻易想到,shell-operator是使用Go编写的,并根据开源许可证(Apache 2.0)的条款进行分发。我们非常感谢任何关于开发在Github上的这个项目的帮助。你可以通过给我们点Star,反馈问题或者是PR来支持我们!

原文链接:Announcing shell-operator to simplify creating of Kubernetes operators(翻译:冯旭松)

0 个评论

要回复文章请先登录注册