前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用shell-operator实现Operator

使用shell-operator实现Operator

作者头像
CNCF
发布2020-09-22 14:51:10
3.5K0
发布2020-09-22 14:51:10
举报
文章被收录于专栏:CNCFCNCF

在本文我们(Flant)将介绍简化 Kubernetes Operator 创建的方法,并展示如何使用 shell-operator 轻松实现自己的 Operator。本文基于我们在 KubeCon Europe 2020上的最新演讲,这是此演讲的完整视频[1]

Kubernetes API 和控制器

我们可以将 Kubernetes API 看成包含每种对象文件夹的文件服务器,这些资源对象通过服务器上的 YAML 文件来表示。APIServer 有一个基本的 HTTP API,使我们可以对这些对象执行三件事。我们可以:

  • 根据资源类型和名称获取资源
  • 更改资源
  • watch 资源

换句话说,我们可以将 Kubernetes 看作基本上是具有三种通用方法的YAML 文件服务器(当然还有其他方法,我们现在可以先忽略它们)。

但是,服务端本身只能存储信息,为了使其正常工作,我们需要一个控制器 - Kubernetes 中第二重要的基础工具。

通常,有两种类型的控制器,第一种类型从 Kubernetes 读取信息,使用某种逻辑对其进行处理,然后将其写回到 Kubernetes。第二种类型也从 Kubernetes 读取数据,但是与第一种类型不同,它改变了某些外部资源的状态。

我们先看看用户创建 Kubernetes Deployment 时会发生什么:

  • Deployment 控制器(kube-controller-manager 的一部分)获取对应的资源信息并创建一个 ReplicaSet。
  • 然后,ReplicaSet 使用对应的信息来创建两个 Pod 副本,但是还没有调度这些 Pod。
  • 然后才是调度程序调度 Pod 并将调度结果的节点信息更新回YAML。
  • 最后 Kubelets watch 到 Pod 数据后去启动对应的容器。

然后以相反的顺序重复所有操作:kubelet 检查容器,计算容器的状态,然后将其发送回去。ReplicaSet 控制器 接收它并更新副本集的状态。Deployment 控制器也发生了同样的事情,用户最终获得了当前状态。

Shell-operator

事实上 Kubernetes 完全就是各种控制器一起运行实现的(Operator 也是控制器)。为了能够轻松创建一个控制器呢,我们引入了一个工具 shell-operator[2],它可以让系统管理员使用他们习惯的方法来创建 Operator。

简单的示例:复制 Secrets

让我们看一个简单的例子,假设我们有一个 Kubernetes 集群。其中有一个默认的名称空间,其中包含一些 Secret(mysecret)资源对象。此外,集群中还有其他名称空间。这些名称空间中有几个具有额外的特定标签。我们的目标是将 Secret 复制到带有此标签的名称空间中。

新的命名空间可以出现在集群中,并且其中一些可能带有此标签,这一事实使任务变得复杂。另一方面,如果标签被删除,则 Secret 也必须被删除。Secret 本身也可以更改,在这种情况下,新的 Secret 必须传播到所有带标签的命名空间中去。如果 Secret 在某个命名空间中被意外删除,则 Operator 必须立即将其还原。

现在我们已经了解了需要实现的需求,接下来我们来使用 shell-oprerator 来真正实现它。

运行原理

与其他 Kubernetes 工作负载类似,shell-operator 部署在 Pod中。在 Pod 中有一个 /hooks 的一个子目录,其中存储了可执行文件,它们可以用 Bash、Python、Ruby等编写的,我们称这些可执行文件为hooks

Shell-opeator 订阅 Kubernetes 事件并执行这些钩子来响应我们感兴趣的事件。

但是,shell-operator 如何知道何时执行钩子呢?事实上每个钩子都有两个阶段。在启动过程中,shell-operator 使用-config参数运行每个钩子。一旦配置阶段结束,钩子将以“正常”方式执行:响应附加给它们的事件。在这种情况下,钩子会获取绑定上下文

使用 Bash 实现

现在,如果我们使用 Bash,我们需要实现两个函数(强烈建议使用shell_lib[3] 库,因为它大大简化了 Bash 中钩子的编写):

  • 第一个用于配置阶段,并且应该输出绑定上下文;
  • 第二个包含钩子的核心逻辑。
#!/bin/bash
source /shell_lib.sh
function __config__() {
  cat << EOF
    configVersion: v1
    # BINDING CONFIGURATION
EOF
}
function __main__() {
  #THE LOGIC
}
hook::run "$@"

下一步是确定我们感兴趣的对象,在我们的示例中,我们需要跟踪:

  • 变更的 Secret 对象;
  • 集群中的所有命名空间,以查看带有标签的命名空间;
  • 目标 Secret,以验证它们是否已和源 Secret 同步了。

订阅源 Secret

绑定配置非常简单,这里我们的mysecretdefault 命名空间中的 Secrets 感兴趣。

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

结果会根据源 Secret(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显示相关的命名空间是否具有mysecret标签,keepFullObjectsInMemory: false选择器表示将删除内存中的完整对象。

追踪目的 Secret

我们订阅所有具有 managed-secret: "yes"注释的 Secrets (这些就是是我们的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参数之外的所有信息。创建此目标 Secret 时,我们将该参数传递给注释。

以这种方式配置的钩子在执行时将获得上述三个绑定上下文,你可以将它们视为集群的某种快照

我们可以使用所有这些信息来设计一种最基本的算法,它遍历所有命名空间,如果当前命名空间 hasLabeltrue,则进行迭代:

  • 比较源和目标 Secret
  • 如果它们相同,则什么都不做
  • 如果它们不同 - 执行 kubectl replace 或者 create 操作。

如果当前命名空间 hasLabelfalse,则:

  • 确保命名空间中没有 Secret
  • 如果目标 Secret 存在 - 执行kubectl delete
  • 如果目标 Secret 不存在,则不执行任何操作。

在我们的示例仓储库中[4],可以找到上述算法的完整 Bash 实现。

35 行 YAML 和相同数量的 Bash 组成了一个简单的 Kubernetes 控制器!Shell-operator 的工作是将它们全部绑定在一起。

显然,使用 Shell-operator 并不是只能复制 Secrets,我们还会用更多示例来了解它的用法。

示例1:更新 ConfigMap

比如现在我们有一个具有三个 Pod 的 Deployment,这些 Pods 使用ConfigMap 来存储一些配置,当这些 Pod 启动时,ConfigMap 处于某种状态(我们将其称为版本1:v.1),我们所有的 Pod 都具有相同的 v.1 版本的 ConfigMap。

现在,假设 ConfigMap 更改为另一个版本 v.2,在这种情况下,我们的Pod 仍将使用 ConfigMap 的早期版本 v.1。

在这种情况下我们通常怎么做呢?是的,我们可以在 Pod 的模板中添加一些内容。因此,让我们将 checksum 注解添加到 Deployment 定义的模板部分:

现在,我们所有的 Pod 都有 checksum,并且与 Deployment 的 checksum 相同。接下来,我们应该更新注释来响应 ConfigMap 的更改。这就是 shell-operator 可能派上用场的时候,我们只需要编写一个钩子即可订阅 ConfigMap 并更新 checksum

当用户修改 ConfigMap 时,shell-operator 会 watch 到变更并更新 checksum。然后,Kubernetes 会杀死 Pod,创建一个新 Pod,等到准备就绪后再进行下一个 Pod。因此,我们的 Deployment 可以完美同步并与更新的 ConfigMap 一起运行。

示例2:使用 CRD

我们知道 Kubernetes 允许我们创建自定义类型的对象。例如,我们可以创建一个名为 MysqlDatabase 的资源,假设这种类型只有两个元数据参数:namenamespace

apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
  name: foo
  namespace: bar

因此,我们可以在 Kubernetes 集群中创建 MySQL 数据库,在这种情况下,可以使用 shell-operator 来 watch MysqlDatabase这类资源,将它们连接到 MySQL 数据库服务器,并同步所需状态和 watch 到的状态。

示例3:监控集群网络

如您所知,ping 是监视网络的最简单方法,当然我们也可以使用 shell-operator 来实现。

首先,我们需要订阅节点,shell-operator 需要每个节点的名称和 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 可以通过将指标写入存储在 $METRICS_PATH 环境变量中指定路径下的文件中来将指标暴露到 Prometheus。

这样我们就使用最少的代码[5]在群集中实现了基本网络监视的方式。

排队机制

如果不讨论 shell-operator 必不可少的排队机制,那么将是不完整的。想象一下,shell-operator 响应集群中的某些事件而执行了一个钩子。

  • 如果集群中发生了另一个事件,将会怎样?
  • shell-operator 会运行该钩子的另一个实例吗?
  • 例如,如果集群中同时发生五个事件,该怎么办?
  • shell-operator 会并行运行它们吗?
  • 消耗的资源(如内存和CPU)又如何呢?

幸运的是,shell-operator 具有内置的排队机制,所有事件都放入队列并顺序处理。

假设我们有两个钩子,第一个事件转到第一个钩子,处理完成后,队列前进。接下来的三个事件是另一个钩子,它们从队列中弹出并作为批处理传递给钩子。因此,该钩子接收事件数组 -更准确地说是绑定上下文数组。

另一种选择是将这些事件合并为一个较大的事件,绑定配置的group参数对此负责。

此外,您可以根据需要获取任意数量的队列或钩子及其组合,例如,您可以在一个队列中使用两个钩子,反之亦然。

您要做的就是将queue字段插入绑定配置中,如果queue省略该名称,则钩子在default队列中运行,这种排队机制可以整体解决所有资源管理问题。

总结

在本文中,我们解释了什么是 shell-operator,展示了如何快速简单地创建它的 Kubernetes Operator,并提供了使用它的一些示例。

有关我们工具的详细信息以及快速入门指南,请参考其 GitHub 存储库。另外也可以看看我们的其他项目,例如,addon-operator[6] ,它可以绑定 Helm Charts,对其进行升级,监视各种 Chart 参数/值(以及控制 Helm Chart 的安装)并根据集群事件进行更新。

原文链接:https://medium.com/flant-com/meet-the-shell-operator-kubecon-36c14ba2f8fe

此外在k8s技术圈后台回复 shell 可以获取 shell-operator 完整的 PDF 文档。

参考资料

[1]

shell-operator 演讲视频: https://www.youtube.com/watch?v=we0s4ETUBLc

[2]

shell-operator: https://github.com/flant/shell-operator

[3]

shell_lib: https://github.com/flant/shell-operator/blob/master/shell_lib.sh

[4]

示例仓储库: https://github.com/flant/examples/tree/master/2020/08-kubecon

[5]

集群网络监控代码: https://github.com/flant/examples/blob/master/2020/08-kubecon/container/ping_exporter.sh

[6]

addon-operator: https://github.com/flant/addon-operator


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Kubernetes API 和控制器
  • Shell-operator
    • 简单的示例:复制 Secrets
      • 运行原理
        • 使用 Bash 实现
          • 订阅源 Secret
            • 处理命名空间
              • 追踪目的 Secret
              • 示例1:更新 ConfigMap
              • 示例2:使用 CRD
              • 示例3:监控集群网络
              • 排队机制
              • 总结
                • 参考资料
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档