
Go jest obecnie monopolistą wśród języków programowania, które ludzie wybierają do pisania operatorów dla Kubernetes. Istnieją obiektywne powody, by tak myśleć, jak:
- Istnieje potężny framework do tworzenia operatorów w Go — .
- Aplikacje, które zmieniły zasady gry, takie jak Docker i Kubernetes, są napisane w Go. Pisanie własnego operatora w Go oznacza rozmowę z ekosystemem w tym samym języku.
- Wysoka wydajność aplikacji w Go i proste narzędzia do pracy z concurrency "z pudełka".
NB: Przy okazji, jak napisać własnego operatora w Go, w jednym z naszych tłumaczeń zagranicznych autorów.
Ale co jeśli przeszkadza Ci w nauce Go brak czasu lub, po prostu, motywacji? W artykule przedstawiony jest przykład, jak można napisać dobrze zbudowanego operatora, używając jednego z najpopularniejszych języków, który zna praktycznie każdy inżynier DevOps — Python.
Przedstawiamy: Koper – operator kopiujący!
Na przykład rozważmy rozwój prostego operatora, który ma na celu kopiowanie ConfigMap, zarówno przy pojawieniu się nowego namespace, jak i przy zmianie jednej z dwóch encji: ConfigMap i Secret. Z punktu widzenia praktycznego zastosowania, operator może być przydatny do masowej aktualizacji konfiguracji aplikacji (poprzez aktualizację ConfigMap) lub do aktualizacji danych poufnych — na przykład kluczy do pracy z Docker Registry (przy dodaniu Secret'a do namespace).
Otóż co powinno mieć dobry operator:
- Interakcja z operatorem odbywa się za pomocą (dalej — CRD).
- Operator może być konfigurowany. W tym celu użyjemy flag linii poleceń i zmiennych środowiskowych.
- Kompilacja kontenera Docker i wykresu Helm jest tak zaplanowana, aby użytkownicy mogli łatwo (dosłownie jednym poleceniem) zainstalować operatora w swoim klastrze Kubernetes.
CRD
Aby operator wiedział, jakie zasoby i gdzie ma szukać, musimy nadać mu zasady. Każda zasada będzie reprezentowana jako jeden obiekt CRD. Jakie pola powinny mieć te CRD?
- Typ zasobu, którego będziemy szukać (ConfigMap lub Secret).
- Lista namespace'ów, w których powinny znajdować się zasoby.
- Selector, według którego będziemy szukać zasobów w namespace.
Opiszmy 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 I od razu stworzymy proste reguły — do wyszukiwania w przestrzeni nazw o nazwie default wszystkich ConfigMap z etykietami w formacie copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultGotowe! Teraz musimy jakoś uzyskać informacje o naszej regule. Od razu zaznaczam, że nie będziemy sami pisać zapytań do API Server klastra. Zamiast tego skorzystamy z gotowej biblioteki Python :
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 działania tego kodu uzyskamy następujące:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Świetnie: udało nam się uzyskać regułę dla operatora. A co najważniejsze — zrobiliśmy to, co nazywamy 'Kubernetes way'.
Zmienne środowiskowe czy flagi? Bierzemy wszystko!
Przechodzimy do głównej konfiguracji operatora. Istnieją dwa podstawowe podejścia do konfigurowania aplikacji:
- używać parametrów linii poleceń;
- używać zmiennych środowiskowych.
Parametry linii poleceń pozwalają na bardziej elastyczne odczytywanie ustawień, z obsługą i walidacją typów danych. W standardowej bibliotece Pythona znajduje się moduł argparser, z którego skorzystamy. Szczegóły i przykłady jego możliwości są dostępne w .
Oto jak będzie wyglądał przykład konfiguracji odczytu flag linii poleceń dla naszego przypadku:
parser = ArgumentParser(
description='Copyrator - operator kopiujący.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Przestrzeń nazw operatora'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='Nazwa CRD'
)
args = parser.parse_args()Z drugiej strony, korzystając z zmiennych środowiskowych w Kubernetes, można łatwo przenieść informacje o pod’zie do wnętrza kontenera. Na przykład, informacje o namespace, w którym uruchomiono pod, możemy uzyskać następującą konstrukcją:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Logika działania operatora
Aby zrozumieć, jak rozdzielić metody pracy z ConfigMap i Secret, posłużymy się specjalnymi mapami. Dzięki temu zrozumiemy, jakie metody są potrzebne do śledzenia i tworzenia obiektu:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Następnie musimy odbierać zdarzenia z serwera API. Realizujemy to w następujący sposób:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Odbieramy metodę do śledzenia obiektów
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 odebraniu 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. Tworzymy plik setup.py, wpisujemy tam metainformacje o projekcie:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Nieobsługiwana wersja Pythona %s.' % '.'.join(version_info)
)
_NAME = 'copyrator'
setup(
name=_NAME,
version='0.0.1',
packages=find_packages(),
classifiers=[
'Status rozwoju :: 3 - Alpha',
'Język programowania :: Python',
'Język programowania :: Python :: 3',
'Język programowania :: Python :: 3.5',
'Język programowania :: Python :: 3.6',
'Język programowania :: 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: Klient kubernetes dla Pythona ma swoje własne wersjonowanie. Szczegóły dotyczące kompatybilności wersji klienta i wersji Kubernetes można znaleźć w .
Teraz nasz projekt wygląda tak:
copyrator
├── copyrator
│ ├── cli.py # Logika obsługi wiersza poleceń
│ ├── constant.py # Stałe, które przytoczyliśmy wcześniej
│ ├── load_crd.py # Logika ładowania CRD
│ └── operator.py # Główna logika działania operatora
└── setup.py # Przygotowanie pakietuDocker i Helm
Dockerfile będzie niezwykle prosty: weźmiemy podstawowy obraz python-alpine i zainstalujemy nasz pakiet. Jego optymalizację odłożymy na lepsze czasy:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Wdrażanie dla operatora jest również bardzo proste:
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 }}-accNa koniec należy utworzyć odpowiednią rolę dla operatora z wymaganymi uprawnieniami:
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 }}Podsumowanie
Tak, bez strachu, pretensji i nauki Go, stworzyliśmy własnego operatora dla Kubernetes w Pythonie. Oczywiście, ma jeszcze wiele do zrobienia: w przyszłości będzie mógł obsługiwać wiele reguł, pracować w kilku wątkach, samodzielnie monitorować zmiany swoich CRD...
Aby można było lepiej zapoznać się z kodem, zgromadziliśmy go w . Jeśli szukasz bardziej zaawansowanych przykładów operatorów zrealizowanych w Pythonie, możesz zwrócić uwagę na dwa operatory do wdrażania mongodb ( i ).
P.S. A jeśli nie chce Ci się zgłębiać wydarzeń Kubernetes lub po prostu wolisz używać Bash — nasi koledzy przygotowali gotowe rozwiązanie w postaci (zrobiliśmy to w kwietniu). Prezentujemy shell-operator: tworzenie operatorów dla Kubernetes stało się jeszcze prostsze
P.P.S.
Przeczytaj także na naszym blogu:
- «»;
- «»;
- «»;
- «»;
- «».
Źródło: habr.com
