Kubernetes Operator на Python без фрэймворкаў і SDK
Go на дадзены момант з'яўляецца манапалістам сярод моў праграмавання, якія людзі выбіраюць для напісання аператараў для Kubernetes. Таму ёсць такія аб'ектыўныя прычыны, як:
Існуе наймагутны фрэймворк для распрацоўкі аператараў на Go Operator SDK.
На Go напісаны такія «якія перавярнулі гульню» прыкладанні, як Docker і Kubernetes. Пісаць свой аператар на Go – гаварыць з экасістэмай на адной мове.
Высокая прадукцыйнасць прыкладанняў на Go і простыя інструменты для працы з concurrency «са скрынкі».
NB: Дарэчы, як напісаць свой аператар на Go, мы ужо апісвалі у адным з нашых пераводаў замежных аўтараў.
Але што, калі вывучаць Go вам мяшае адсутнасць часу ці, банальна, матывацыі? У артыкуле прыведзены прыклад таго, як можна напісаць дыхтоўны аператар, выкарыстоўваючы адну з самых папулярных моў, якую ведае практычна кожны DevOps-інжынер. Пітон.
Сустракайце: Капіратар - капіявальны аператар!
Для прыкладу разгледзім распрацоўку простага аператара, прызначанага для капіявання 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 (першы и 2.).
PS А калі вам лянота разбірацца з падзеямі Kubernetes ці ж вам папросту звыкла выкарыстоўваць Bash — нашы калегі прыгатавалі гатовае рашэнне ў выглядзе shell-operator (мы анансавалі яго ў красавіку).