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.


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

  • «Готовить Kubernetes-кластер просто и удобно? Анонсируем addon-operator»;
  • «Представляем shell-operator: создавать операторы для Kubernetes стало ещё проще»;
  • «Расширяем и дополняем Kubernetes (обзор и видео доклада)»;
  • «Пишем оператора для Kubernetes на Golang»;
  • «Операторы для Kubernetes: как запускать stateful-приложения».
Источник: https://habr.com/ru/company/flant/blog/459320/


Интересные статьи

Интересные статьи

Внедряем оплату BTC куда угодно (Python)- генерация кошелька на основе seed фразы- проверка баланса и транзакций- отправка BTC на другие кошельки- создаем телеграм бота д...
TL;DR, уважаемые хабровчане. Наступила осень, в который раз перевернулся лист календаря и третье сентября наконец-то снова минуло. А значит пора возвращаться к работе — и не только к ...
Прим. перев.: авторы этой статьи — инженеры из небольшой чешской компании pipetail. Им удалось собрать замечательный список из [местами банальных, но всё ещё] столь актуальных проблем...
Новая подборка советов про Python и программирование из моего авторского канала @pythonetc. ← Предыдущие публикации Порядок блоков except имеет значение: если исключение может быть по...
Эксперимент по подготовке докладов на Moscow Python Conf ++ с нуля на финишной прямой. Слайды готовы, прогоны провели, осталось только дождаться премьеры — уже в эту пятницу 5 апреля. В расписани...