Go на данный момент является монополистом среди языков программирования, которые люди выбирают для написания операторов для Kubernetes. Тому есть такие объективные причины, как:
- Существует мощнейший фреймворк для разработки операторов на Go — Operator SDK.
- На Go написаны такие «перевернувшие игру» приложения, как Docker и Kubernetes. Писать свой оператор на Go — говорить с экосистемой на одном языке.
- Высокая производительность приложений на Go и простые инструменты для работы с concurrency «из коробки».
NB: Кстати, как написать свой оператор на Go, мы уже описывали в одном из наших переводов зарубежных авторов.
Но что, если изучать Go вам мешает отсутствие времени или, банально, мотивации? В статье приведен пример того, как можно написать добротный оператор, используя один из самых популярных языков, который знает практически каждый DevOps-инженер, — Python.
Встречайте: Копиратор — копировальный оператор!
Для примера рассмотрим разработку простого оператора, предназначенного для копирования ConfigMap либо при появлении нового namespace, либо при изменении одной из двух сущностей: ConfigMap и Secret. С точки зрения практического применения оператор может быть полезен для массового обновления конфигураций приложения (путем обновления ConfigMap) или же для обновления секретных данных — например, ключей для работы с Docker Registry (при добавлении Secret’а в namespace).
Итак, что должно быть у хорошего оператора:
- Взаимодействие с оператором осуществляется при помощи Custom Resource Definitions (далее — CRD).
- Оператор может настраиваться. Для этого будем использовать флаги командной строки и переменные окружения.
- Сборка Docker-контейнера и Helm-чарта прорабатываются так, чтобы пользователи могли легко (буквально одной командой) установить оператор в свой Kubernetes-кластер.
CRD
Чтобы оператор знал, какие ресурсы и где ему искать, нам нужно задать для него правило. Каждое правило будет представлено в виде одного объекта CRD. Какие поля должны быть у этого CRD?
- Тип ресурса, который мы будем искать (ConfigMap или Secret).
- Список namespace’ов, в которых должны находиться ресурсы.
- 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.
Переменные окружения или флаги? Берем всё!
Переходим к основной конфигурации оператора. Есть два базовых подхода к конфигурированию приложений:
- использовать параметры командной строки;
- использовать переменные окружения.
Параметры командной строки позволяют считывать настройки более гибко, с поддержкой и валидацией типов данных. В стандартной библиотеке 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/
Добавить комментарий