Пишем оператор Kubernetes на Python без фреймворков и SDK
Нужен оператор Kubernetes, но нет времени учить Go? В этой статье мы покажем, как создать надёжный оператор, используя Python.
На сегодня Go – фактический монополист в реализации Kubernetes-операторов. Вот почему так сложилось:
- Operator SDK – это мощный фреймворк, который создан специально для реализации операторов на Go.
- Docker и Kubernetes реализованы на Go, и это меняет правила игры. Оператор, реализованный на Go, позволяет взаимодействовать со всей экосистемой.
- В языке Go есть простой механизм использования параллельности, поэтому приложения получаются высокопроизводительными.
Наверняка вам не захочется учить Go, если вы уже знаете Python. Поэтому мы сделаем надёжный оператор, используя язык программирования Python.
Copyrator – это наш оператор копирования!
Мы напишем простой оператор, спроектированный, чтобы копировать ConfigMap, когда появляется новое пространство имён или при изменении состояния объектов ConfigMap, Secret. Нашим оператором удобно производить массовые обновления настроек приложения, а также сбрасывать им секреты, например, ключи репозитория образов Docker (когда Secret добавлен в пространство имён).
Так какие функции должен выполнять хороший оператор Kubernetes? Вот они:
- Взаимодействие с оператором производится с помощью Custom Resource Definitions (далее CRD).
- Оператор поддерживает настройку. Мы можем использовать флаги в командной строке и переменные окружения для настройки.
- Образ Docker и чарты Helm создаются с учётом облегчения установки для пользователей (обычно одной командой) в их кластер Kubernetes.
CRD
Чтобы оператор знал, где и какие ресурсы искать, нужно настроить некоторые правила. Каждое правило будет представлено как особый CRD-объект. Какие поля должен иметь CRD-объект?
- Тип ресурсов, которые нам интересны (ConfigMap или Secret).
- Список пространств имён, хранящих ресурсы.
- Селектор, который помогает нам искать ресурсы в конкретном пространстве.
Давайте определим наш 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
И немедленно добавим простое правило для выбора ConfigMaps с метками, совпадающими с copyrator: "true"
в пространстве имён по умолчанию:
apiVersion: flant.com/v1 kind: CopyratorRule metadata: name: main-rule labels: module: copyrator ruleType: configmap selector: copyrator: "true" namespace: default
Отлично! Теперь нам нужно как-то получить информацию о наших правилах. Пришло время признаться: мы не будем делать запросы API нашего кластера вручную. Для этих целей есть 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 способом.
Переменные окружения или флаги? Всё сразу!
Пришло время приступить к базовой настройке оператора. Есть два главных подхода к настройке приложений:
- с помощью параметров командной строки;
- с помощью переменных окружения.
С помощью параметров командной строки вы сможете извлекать настройки с большей гибкостью и с поддержкой проверки типов данных. Используем модуль argparser
из стандартной библиотеки Python. Детали и примеры использования доступны в официальной документации Python.
Вот пример настройки поиска флагов командной строки, адаптированных для нашего случая:
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. Например, вы можете получить информацию о пространстве имён, в котором запущен модуль с помощью следующей структуры:
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', {}) # Искать совпадения с помощью селектора for key, value in specs['selector'].items(): if labels.get(key) != value: return # Получить активные пространства имён namespaces = map( lambda x: x.metadata.name, filter( lambda x: x.status.phase == 'Active', v1.list_namespace().items ) ) for namespace in namespaces: # Очистить метаданные, задать пространство имён 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 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), ] } )
Сейчас наш проект имеет следующую структуру:
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"]
Развернуть 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 }}
Заключение
В этой статье мы показали, как создать ваш собственный оператор Kubernetes на Python. Конечно, он ещё сыроват: вы можете обогатить его возможностями обработки нескольких правил, самостоятельным мониторингом изменений в своих CRD, извлечь выгоду из возможностей параллелизма.
Весь код лежит в репозитории.