Kubernetes Operator на Python без фреймворків та SDK
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).
Оператор може налаштовуватись. Для цього будемо використовувати прапори командного рядка та змінні оточення.
Складання Docker-контейнера та Helm-чарта проробляються так, щоб користувачі могли легко (буквально однією командою) встановити оператор у свій Kubernetes-кластер.
CRD
Щоб оператор знав, які ресурси та де йому шукати, нам потрібно задати для нього правило. Кожне правило буде подано у вигляді одного об'єкта CRD. Які поля мають бути у цього CRD?
Тип ресурсу, який ми шукатимемо (ConfigMap або Secret).
Список namespace'ів, у яких мають бути ресурси.
Селектор, по якому ми шукатимемо ресурси в namespace'і.
Готово! Тепер потрібно якось отримати інформацію про наше правило. Відразу зазначу, що самостійно писати запити до 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')}
Добре: нам вдалося отримати правило для оператора. І найголовніше — ми це зробили, як то кажуть, Kubernetes way.
Змінні оточення чи прапори? Беремо все!
Переходимо до основної конфігурації оператора. Є два базові підходи до конфігурування додатків:
використовувати параметри командного рядка;
використовувати змінні оточення.
Параметри командного рядка дозволяють зчитувати налаштування гнучкіше, з підтримкою та валідацією типів даних. У стандартній бібліотеці Python'а є модуль argparser, Яким ми й скористаємося. Подробиці та приклади його можливостей доступні в офіційної документації.
Ось як для нашого випадку виглядатиме приклад налаштування зчитування прапорів командного рядка:
З іншого боку, за допомогою змінних оточення в Kubernetes можна легко перенести службову інформацію про pod'є всередину контейнера. Наприклад, інформацію про namespace, в якому запущено pod, ми можемо отримати таку конструкцію:
Щоб розуміти, як розділити методи роботи з ConfigMap і 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='[email protected]',
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"]
Ось так, без страху, докору та вивчення Go ми змогли зібрати свого власного оператора для Kubernetes на Python. Звичайно, йому ще є куди зростати: у майбутньому він зможе обробляти кілька правил, працювати в кілька потоків, самостійно моніторити зміни своїх CRD.
Щоб можна було ближче познайомитися з кодом, ми склали його в публічний репозиторій. Якщо хочеться прикладів серйозніших операторів, реалізованих за допомогою Python, можете звернути свою увагу на два оператори для розгортання mongodb (перший и друга).
PS А якщо вам ліньки розбиратися з подіями Kubernetes або вам просто звичніше використовувати Bash — наші колеги приготували готове рішення у вигляді shell-operator (ми анонсували його у квітні).