Python 中的 Kubernetes Operator,无需框架和 SDK

Python 中的 Kubernetes Operator,无需框架和 SDK

目前,Go 垄断了人们选择为 Kubernetes 编写语句的编程语言。 造成这种情况是有客观原因的,比如:

  1. Go 有一个强大的开发算子的框架 - 运营商SDK.
  2. Docker 和 Kubernetes 等改变游戏规则的应用程序是用 Go 编写的。 用 Go 编写运算符意味着与生态系统使用相同的语言。
  3. 高性能的 Go 应用程序和开箱即用的并发处理简单工具。

NB:顺便说一下,如何在Go中编写自己的语句,我们 已经描述过 在我们的一本由外国作者翻译的作品中。

但是,如果你因为缺乏时间或者简单地说缺乏动力而无法学习 Go,该怎么办? 本文提供了一个示例,说明如何使用几乎每个 DevOps 工程师都知道的最流行的语言之一编写良好的语句 - 蟒蛇.

认识:复印机 - 复印操作员!

例如,考虑开发一个简单的语句,旨在在出现新命名空间或两个实体之一发生更改时复制 ConfigMap:ConfigMap 和 Secret。 从实际角度来看,该操作符对于批量更新应用程序配置(通过更新 ConfigMap)或更新秘密数据非常有用 - 例如,用于使用 Docker 注册表的密钥(将 Secret 添加到命名空间时)。

因此, 一个好的经营者应该具备什么:

  1. 与操作员的交互是使用 自定义资源定义 (以下简称CRD)。
  2. 可以配置操作员。 为此,我们将使用命令行标志和环境变量。
  3. Docker 容器和 Helm 图表的构建旨在使用户可以轻松(实际上只需一个命令)将操作员安装到其 Kubernetes 集群中。

CRD

为了让操作员知道要寻找什么资源、到哪里去寻找,我们需要为他设定一个规则。 每个规则将表示为单个 CRD 对象。 这个 CRD 应该有哪些字段?

  1. 资源类型,我们将寻找(ConfigMap 或 Secret)。
  2. 命名空间列表,资源应位于其中。
  3. 选择,我们将通过它在命名空间中搜索资源。

我们来描述一下 CRD:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: copyrator.flant.com
spec:
  group: flant.com
  versions:
  - name: v1
    served: true
    storage: true
  scope: Namespaced
  names:
    plural: copyrators
    singular: copyrator
    kind: CopyratorRule
    shortNames:
    - copyr
  validation:
    openAPIV3Schema:
      type: object
      properties:
        ruleType:
          type: string
        namespaces:
          type: array
          items:
            type: string
        selector:
          type: string

我们将立即创建它 简单规则 — 在名称空间中搜索名称 default 所有带有标签的 ConfigMap copyrator: "true":

apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
  name: main-rule
  labels:
    module: copyrator
ruleType: configmap
selector:
  copyrator: "true"
namespace: default

准备好! 现在我们需要以某种方式获取有关我们规则的信息。 让我立即预约,我们不会自己向集群 API Server 写入请求。 为此,我们将使用现成的 Python 库 kubernetes 客户端:

import kubernetes
from contextlib import suppress


CRD_GROUP = 'flant.com'
CRD_VERSION = 'v1'
CRD_PLURAL = 'copyrators'


def load_crd(namespace, name):
    client = kubernetes.client.ApiClient()
    custom_api = kubernetes.client.CustomObjectsApi(client)

    with suppress(kubernetes.client.api_client.ApiException):
        crd = custom_api.get_namespaced_custom_object(
            CRD_GROUP,
            CRD_VERSION,
            namespace,
            CRD_PLURAL,
            name,
        )
    return {x: crd[x] for x in ('ruleType', 'selector', 'namespace')}

运行此代码的结果是,我们得到以下结果:

{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}

太棒了:我们设法为操作员制定了一条规则。 最重要的是,我们采用了所谓的 Kubernetes 方式。

环境变量或标志? 我们带走一切!

让我们继续讨论主要的操作员配置。 配置应用程序有两种基本方法:

  1. 使用命令行选项;
  2. 使用环境变量。

命令行选项允许您更灵活地读取设置,并提供数据类型支持和验证。 Python的标准库有一个模块 argparser,我们将使用它。 其功能的详细信息和示例可在 官方文档.

对于我们的例子,设置读取命令行标志的示例如下所示:

   parser = ArgumentParser(
        description='Copyrator - copy operator.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Operator Namespace'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD Name'
    )
    args = parser.parse_args()

另一方面,使用 Kubernetes 中的环境变量,您可以轻松地传输容器内 pod 的服务信息。 例如,我们可以通过以下构造获取有关 pod 运行所在的命名空间的信息:

env:
- name: NAMESPACE
  valueFrom:
     fieldRef:
         fieldPath: metadata.namespace 

算子逻辑

为了了解如何分离使用 ConfigMap 和 Secret 的方法,我们将使用特殊的映射。 然后我们就可以明白我们需要什么方法来跟踪和创建对象了:

LIST_TYPES_MAP = {
    'configmap': 'list_namespaced_config_map',
    'secret': 'list_namespaced_secret',
}

CREATE_TYPES_MAP = {
    'configmap': 'create_namespaced_config_map',
    'secret': 'create_namespaced_secret',
}

接下来,您需要从 API 服务器接收事件。 让我们按如下方式实现它:

def handle(specs):
    kubernetes.config.load_incluster_config()
    v1 = kubernetes.client.CoreV1Api()

    # Получаем метод для слежения за объектами
    method = getattr(v1, LIST_TYPES_MAP[specs['ruleType']])
    func = partial(method, specs['namespace'])

    w = kubernetes.watch.Watch()
    for event in w.stream(func, _request_timeout=60):
        handle_event(v1, specs, event)

接收到事件后,我们进入处理它的主要逻辑:

# Типы событий, на которые будем реагировать
ALLOWED_EVENT_TYPES = {'ADDED', 'UPDATED'}


def handle_event(v1, specs, event):
    if event['type'] not in ALLOWED_EVENT_TYPES:
        return

    object_ = event['object']
    labels = object_['metadata'].get('labels', {})

    # Ищем совпадения по selector'у
    for key, value in specs['selector'].items():
        if labels.get(key) != value:
            return
    # Получаем активные namespace'ы
    namespaces = map(
        lambda x: x.metadata.name,
        filter(
            lambda x: x.status.phase == 'Active',
            v1.list_namespace().items
        )
    )
    for namespace in namespaces:
        # Очищаем метаданные, устанавливаем namespace
        object_['metadata'] = {
            'labels': object_['metadata']['labels'],
            'namespace': namespace,
            'name': object_['metadata']['name'],
        }
        # Вызываем метод создания/обновления объекта
        methodcaller(
            CREATE_TYPES_MAP[specs['ruleType']],
            namespace,
            object_
        )(v1)

主要逻辑已经准备好了! 现在我们需要将所有这些打包到一个 Python 包中。 我们准备文件 setup.py,在那里写入有关该项目的元信息:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Unsupported python version %s.' % '.'.join(version_info)
    )


_NAME = 'copyrator'
setup(
    name=_NAME,
    version='0.0.1',
    packages=find_packages(),
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
    ],
    author='Flant',
    author_email='[email protected]',
    include_package_data=True,
    install_requires=[
        'kubernetes==9.0.0',
    ],
    entry_points={
        'console_scripts': [
            '{0} = {0}.cli:main'.format(_NAME),
        ]
    }
)

NB:Python 的 kubernetes 客户端有自己的版本控制。 有关客户端版本和 Kubernetes 版本之间兼容性的更多信息,请参见 兼容性矩阵.

现在我们的项目看起来像这样:

copyrator
├── copyrator
│   ├── cli.py # Логика работы с командной строкой
│   ├── constant.py # Константы, которые мы приводили выше
│   ├── load_crd.py # Логика загрузки CRD
│   └── operator.py # Основная логика работы оператора
└── setup.py # Оформление пакета

Docker 和 Helm

Dockerfile 将非常简单:获取基础 python-alpine 映像并安装我们的包。 让我们推迟它的优化,直到更好的时机:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

对于运营商来说部署也非常简单:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
spec:
  selector:
    matchLabels:
      name: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        name: {{ .Chart.Name }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: privaterepo.yourcompany.com/copyrator:latest
        imagePullPolicy: Always
        args: ["--rule-type", "main-rule"]
        env:
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
      serviceAccountName: {{ .Chart.Name }}-acc

最后,您需要为操作员创建具有必要权限的适当角色:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Chart.Name }}-acc

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: {{ .Chart.Name }}
rules:
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get", "watch", "list"]
  - apiGroups: [""]
    resources: ["secrets", "configmaps"]
    verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: {{ .Chart.Name }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: {{ .Chart.Name }}
subjects:
- kind: ServiceAccount
  name: {{ .Chart.Name }}

就这样,我们能够在没有恐惧、责备或学习 Go 的情况下,用 Python 为 Kubernetes 构建我们自己的操作符。 当然,它仍然有成长的空间:将来它将能够处理多个规则,在多个线程中工作,独立监视 CRD 的变化......

为了让您更仔细地查看代码,我们将其放入 公共存储库。 如果您想要使用 Python 实现的更严肃的运算符的示例,您可以将注意力转向用于部署 mongodb 的两个运算符(第一 и 第二).

PS 如果您懒得处理 Kubernetes 事件,或者您只是更习惯使用 Bash,我们的同事已经准备了以下形式的现成解决方案 外壳操作符 (我们 宣布 四月份的事)。

聚苯硫醚

另请阅读我们的博客:

来源: habr.com

添加评论