走?重击!与壳牌运营商会面(KubeCon EU'2020报告的回顾和视频)

今年,主要的欧洲Kubernetes会议-KubeCon + CloudNativeCon Europe 2020-是虚拟的。但是,这种格式上的改变并没有阻止我们进行长期计划的演讲“ Go?重击!认识专门针对我们的开源Shell-operer项目的Shell-operator”



受这次演讲的启发,本文介绍了一种简化为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创建机密时,最后一个参数传递给了注释:它允许您比较机密的版本并保持最新。



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







基于所有这些信息,可以开发一种基本算法。遍历所有名称空间,并:



  • 如果hasLabeltrue当前名称空间相关
    • 将全局机密与本地机密进行比较:
      • 如果它们相同,则不执行任何操作;
      • 如果它们不同,则执行kubectl replacecreate;
  • 如果hasLabelfalse当前名称空间相关

    • 确保Secret不在给定的名称空间中:
      • 如果存在本地机密,请使用删除kubectl delete
      • 如果未找到本地机密,则不执行任何操作。






您可以通过示例在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假设此类型具有两个元数据参数:namenamespace.



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分钟):





报告介绍:







聚苯乙烯



另请参阅我们的博客:






All Articles