Operator Kubernetes w Pythonie bez frameworków i SDK
Go ma obecnie monopol na języki programowania, w których ludzie piszą oświadczenia dla Kubernetes. Istnieją ku temu obiektywne przyczyny, takie jak:
Istnieje potężna platforma do rozwijania operatorów w Go - Pakiet SDK operatora.
Aplikacje zmieniające zasady gry, takie jak Docker i Kubernetes, są pisane w Go. Napisanie operatora w Go oznacza mówienie tym samym językiem z ekosystemem.
Wysoka wydajność aplikacji Go i proste narzędzia do pracy ze współbieżnością od razu po wyjęciu z pudełka.
NB: Swoją drogą, jak napisać własną wypowiedź w Go, my już opisane w jednym z naszych tłumaczeń autorstwa zagranicznych autorów.
Co jednak, jeśli naukę Go uniemożliwia Ci brak czasu lub, mówiąc prościej, motywacji? W artykule podany jest przykład jak napisać dobre oświadczenie korzystając z jednego z najpopularniejszych języków, które zna niemal każdy inżynier DevOps - Python.
Poznaj: Kopiarka - operator kopiarki!
Jako przykład rozważ opracowanie prostej instrukcji zaprojektowanej do kopiowania ConfigMap, gdy pojawi się nowa przestrzeń nazw lub gdy zmieni się jedna z dwóch jednostek: ConfigMap i Secret. Z praktycznego punktu widzenia operator może przydać się do zbiorczej aktualizacji konfiguracji aplikacji (poprzez aktualizację ConfigMap) lub do aktualizacji tajnych danych - na przykład kluczy do pracy z Docker Registry (przy dodawaniu Secret do przestrzeni nazw).
Możliwość konfiguracji operatora. W tym celu użyjemy flag wiersza poleceń i zmiennych środowiskowych.
Kompilacja kontenera Docker i wykresu Helm została zaprojektowana tak, aby użytkownicy mogli łatwo (dosłownie jednym poleceniem) zainstalować operatora w swoim klastrze Kubernetes.
CRD
Aby operator wiedział jakich zasobów szukać i gdzie szukać musimy ustawić dla niego regułę. Każda reguła będzie reprezentowana jako pojedynczy obiekt CRD. Jakie pola powinien zawierać ten CRD?
Typ zasobu, którego będziemy szukać (ConfigMap lub Secret).
Lista przestrzeni nazw, w którym powinny być zlokalizowane zasoby.
Selektor, według którego będziemy szukać zasobów w przestrzeni nazw.
I od razu go stworzymy prosta zasada — aby przeszukać przestrzeń nazw według nazwy default wszystkie ConfigMap z etykietami takimi jak copyrator: "true":
Gotowy! Teraz musimy w jakiś sposób zdobyć informacje o naszej regule. Od razu zastrzegam, że sami nie będziemy pisać żądań do serwera API klastra. W tym celu skorzystamy z gotowej biblioteki Pythona klient kubernetes:
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')}
W wyniku uruchomienia tego kodu otrzymamy następujący komunikat:
Świetnie: udało nam się uzyskać regułę dla operatora. A co najważniejsze, zrobiliśmy tak zwaną drogę Kubernetesa.
Zmienne środowiskowe lub flagi? Bierzemy wszystko!
Przejdźmy do konfiguracji głównego operatora. Istnieją dwa podstawowe podejścia do konfigurowania aplikacji:
użyj opcji wiersza poleceń;
użyj zmiennych środowiskowych.
Opcje wiersza poleceń umożliwiają bardziej elastyczne odczytywanie ustawień, z obsługą typów danych i sprawdzaniem poprawności. Standardowa biblioteka Pythona zawiera moduł argparser, z którego skorzystamy. Szczegóły i przykłady jego możliwości dostępne są w oficjalna dokumentacja.
W naszym przypadku tak mógłby wyglądać przykład ustawienia flag wiersza poleceń do odczytu:
Z drugiej strony, używając zmiennych środowiskowych w Kubernetesie, możesz łatwo przenieść informacje o usłudze o podie do wnętrza kontenera. Przykładowo informację o przestrzeni nazw, w której działa pod, możemy uzyskać następującą konstrukcją:
Aby zrozumieć, jak oddzielić metody pracy z ConfigMap i Secret, użyjemy specjalnych map. Wtedy możemy zrozumieć, jakich metod potrzebujemy do śledzenia i tworzenia obiektu:
Następnie musisz odbierać zdarzenia z serwera API. Zaimplementujmy to w następujący sposób:
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)
Po otrzymaniu zdarzenia przechodzimy do głównej logiki jego przetwarzania:
# Типы событий, на которые будем реагировать
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)
Główna logika jest gotowa! Teraz musimy spakować to wszystko w jeden pakiet Pythona. Przygotowujemy plik setup.py, wpisz tam metainformacje o projekcie:
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: Klient kubernetes dla języka Python ma własną wersję. Więcej informacji na temat kompatybilności pomiędzy wersjami klienckimi i wersjami Kubernetesa znajdziesz w macierze zgodności.
Teraz nasz projekt wygląda tak:
copyrator
├── copyrator
│ ├── cli.py # Логика работы с командной строкой
│ ├── constant.py # Константы, которые мы приводили выше
│ ├── load_crd.py # Логика загрузки CRD
│ └── operator.py # Основная логика работы оператора
└── setup.py # Оформление пакета
Docker i Helm
Plik Dockerfile będzie niezwykle prosty: weź podstawowy obraz Pythona-alpine i zainstaluj nasz pakiet. Odłóżmy jego optymalizację na lepsze czasy:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]
Wdrożenie dla operatora jest również bardzo proste:
W ten sposób bez strachu, wyrzutów i nauki Go udało nam się zbudować własnego operatora dla Kubernetesa w Pythonie. Oczywiście ma jeszcze przestrzeń do rozwoju: w przyszłości będzie mógł przetwarzać wiele reguł, pracować w wielu wątkach, samodzielnie monitorować zmiany w swoich CRD...
Aby dać Ci bliższe spojrzenie na kod, umieściliśmy go repozytorium publiczne. Jeśli chcesz przykładów poważniejszych operatorów zaimplementowanych przy użyciu Pythona, możesz zwrócić uwagę na dwóch operatorów do wdrożenia mongodb (первый и drugi).
PS A jeśli jesteś zbyt leniwy, aby zajmować się zdarzeniami Kubernetesa lub po prostu jesteś bardziej przyzwyczajony do korzystania z Basha, nasi koledzy przygotowali gotowe rozwiązanie w postaci operator powłoki (My ogłosił to w kwietniu).