Хабрахабр

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

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

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

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

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

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

Для примера рассмотрим разработку простого оператора, предназначенного для копирования 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

В результате работы этого кода получим следующее:

{'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 (первый и второй). Чтобы можно было поближе познакомиться с кодом, мы сложили его в публичный репозиторий.

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

P.P.S.

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

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть