Kubernetes Operator на Python без фреймворков и SDK

от автора

Go на данный момент является монополистом среди языков программирования, которые люди выбирают для написания операторов для Kubernetes. Тому есть такие объективные причины, как:

  1. Существует мощнейший фреймворк для разработки операторов на Go — Operator SDK.
  2. На Go написаны такие «перевернувшие игру» приложения, как Docker и Kubernetes. Писать свой оператор на Go — говорить с экосистемой на одном языке.
  3. Высокая производительность приложений на Go и простые инструменты для работы с concurrency «из коробки».

NB: Кстати, как написать свой оператор на Go, мы уже описывали в одном из наших переводов зарубежных авторов.

Но что, если изучать Go вам мешает отсутствие времени или, банально, мотивации? В статье приведен пример того, как можно написать добротный оператор, используя один из самых популярных языков, который знает практически каждый DevOps-инженер, — Python.

Встречайте: Копиратор — копировальный оператор!

Для примера рассмотрим разработку простого оператора, предназначенного для копирования ConfigMap либо при появлении нового namespace, либо при изменении одной из двух сущностей: ConfigMap и Secret. С точки зрения практического применения оператор может быть полезен для массового обновления конфигураций приложения (путем обновления ConfigMap) или же для обновления секретных данных — например, ключей для работы с Docker Registry (при добавлении Secret’а в namespace).

Итак, что должно быть у хорошего оператора:

  1. Взаимодействие с оператором осуществляется при помощи Custom Resource Definitions (далее — CRD).
  2. Оператор может настраиваться. Для этого будем использовать флаги командной строки и переменные окружения.
  3. Сборка Docker-контейнера и Helm-чарта прорабатываются так, чтобы пользователи могли легко (буквально одной командой) установить оператор в свой Kubernetes-кластер.

CRD

Чтобы оператор знал, какие ресурсы и где ему искать, нам нужно задать для него правило. Каждое правило будет представлено в виде одного объекта CRD. Какие поля должны быть у этого CRD?

  1. Тип ресурса, который мы будем искать (ConfigMap или Secret).
  2. Список namespace’ов, в которых должны находиться ресурсы.
  3. Selector, по которому мы будем искать ресурсы в namespace’е.

Опишем 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

И сразу же создадим простое правило — на поиск в namespace’е с именем default всех ConfigMap c label’ами вида 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-client:

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 way.

Переменные окружения или флаги? Берем всё!

Переходим к основной конфигурации оператора. Есть два базовых подхода к конфигурированию приложений:

  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’е внутрь контейнера. Например, информацию о namespace, в котором запущен 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 server. Реализуем это следующим образом:

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 package. Оформляем файл 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='maksim.nabokikh@flant.com',     include_package_data=True,     install_requires=[         'kubernetes==9.0.0',     ],     entry_points={         'console_scripts': [             '{0} = {0}.cli:main'.format(_NAME),         ]     } )

NB: Клиент kubernetes для Python имеет своё версионирование. Подробнее о совместимости версий клиента и версий 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"]

Deployment для оператора тоже очень прост:

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, мы смогли собрать своего собственного оператора для Kubernetes на Python. Конечно, ему ещё есть куда расти: в будущем он сможет обрабатывать несколько правил, работать в несколько потоков, самостоятельно мониторить изменения своих CRD…

Чтобы можно было поближе познакомиться с кодом, мы сложили его в публичный репозиторий. Если хочется примеров более серьезных операторов, реализованных при помощи Python, можете обратить своё внимание на два оператора для развёртывания mongodb (первый и второй).

P.S. А если вам лень разбираться с событиями Kubernetes или же вам попросту привычнее использовать Bash — наши коллеги приготовили готовое решение в виде shell-operator (мы анонсировали его в апреле).

P.P.S.

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/ru/company/flant/blog/459320/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *