
Go je v současnosti monopolním jazykem, který lidé volí pro psaní operatorů pro Kubernetes. Existují k tomu objektivní důvody, jako:
- Existuje mocný framework pro vývoj operatorů v Go — .
- Na Go byly napsány aplikace, které «změnily hru», jako Docker a Kubernetes. Psát svého operátora v Go znamená hovořit s ekosystémem jedním jazykem.
- Vysoký výkon aplikací v Go a jednoduché nástroje pro práci s concurrency „přímo z krabice“.
NB: Mimochodem, jak napsat svého operátora v Go, jsme v jednom z našich překladů zahraničních autorů.
Ale co když vám ve studiu Go brání nedostatek času nebo, banálně, motivace? V článku je uveden příklad, jak napsat solidního operátora s použitím jednoho z nejpopulárnějších jazyků, který zná prakticky každý DevOps inženýr — CouchDB.
Představujeme: Kopirátor — kopírovací operátor!
Pro příklad se zaměříme na vývoj jednoduchého operatora určeného k kopírování ConfigMap buď při vzniku nového namespace, nebo při změně jedné ze dvou entit: ConfigMap a Secret. Z praktického hlediska může být operator užitečný pro masivní aktualizaci konfigurací aplikace (aktualizací ConfigMap) nebo pro aktualizaci tajných údajů — například klíčů pro práci s Docker Registry (při přidání Secret do namespace).
Takže, co by měl mít dobrý operator:
- Interakce s operátorem probíhá pomocí (dále — CRD).
- Operator může být konfigurován. K tomu použijeme příznaky příkazového řádku a proměnné prostředí.
- Sestavení Docker kontejneru a Helm chartu je navrženo tak, aby uživatelé mohli snadno (doslova jedním příkazem) nainstalovat operátora do svého Kubernetes clusteru.
CRD
Aby operator věděl, jaké zdroje a kde hledat, musíme mu stanovit pravidlo. Každé pravidlo bude reprezentováno jako jeden objekt CRD. Jaká pole by toto CRD měla mít?
- Typ zdroje, které budeme hledat (ConfigMap nebo Secret).
- Seznam namespace'ů, ve kterých by měly být zdroje.
- Selector, podle kterého budeme hledat zdroje v namespace.
Popišme 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 A hned vytvoříme jednoduché pravidlo — na vyhledávání v namespace s názvem default všech ConfigMap s labely ve formátu copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultHotovo! Teď potřebujeme nějak získat informace o našem pravidle. Hned na začátek podotýkám, že nebudeme psát dotazy na API server clusteru sami. K tomu využijeme hotovou Python knihovnu :
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')}V důsledku běhu tohoto kódu získáme následující:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Skvěle: podařilo se nám získat pravidlo pro operátora. A co je nejdůležitější — udělali jsme to, jak se říká, způsobem Kubernetes.
Přenvěné proměnné nebo příznaky? Bereme vše!
Přejdeme k hlavní konfiguraci operátora. Existují dva základní přístupy k konfiguraci aplikací:
- použít parametry příkazového řádku;
- použít proměnné prostředí.
Parametry příkazového řádku umožňují číst nastavení flexibilněji, s podporou a validací datových typů. Ve standardní knihovně Python existuje modul argparser, který využijeme. Podrobnosti a příklady jeho funkcí jsou k dispozici v .
Takto vypadá příklad pro naši situaci, jak nastavit čtení příznaků příkazového řádku:
parser = ArgumentParser(
description='Copyrator - copy operator.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Operátor Namespace'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='CRD Název'
)
args = parser.parse_args()Na druhé straně lze pomocí proměnných prostředí v Kubernetes snadno přenést provozní informace o podu dovnitř kontejneru. Například informace o jmenném prostoru, ve kterém je pod spuštěn, můžeme získat následující konstrukcí:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Logika fungování operátora
Abychom porozuměli, jak oddělit metody pro práci s ConfigMap a Secret, využijeme speciální mapy. Tím získáme přehled o metodách potřebných k sledování a vytváření objektu:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Dále je třeba přijímat události od API serveru. Realizujeme to následujícím způsobem:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Získáváme metodu pro sledování objektů
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 obdržení události přecházíme k hlavní logice jejího zpracování:
# Типы событий, на которые будем реагировать
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) Hlavní logika je hotova! Nyní je třeba zabalit to všechno do jednoho Python balíčku. Upravujeme soubor setup.py, píšeme tam meta informace o projektu:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Nepodporovaná verze pythonu %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),
]
}
)NB: Klient kubernetes pro Python má své verzionování. Podrobnosti o kompatibilitě verzí klienta a verzí Kubernetes lze nalézt v .
Nyní náš projekt vypadá takto:
copyrator
├── copyrator
│ ├── cli.py # Logika práce s příkazovým řádkem
│ ├── constant.py # Konstanty, které jsme uváděli výše
│ ├── load_crd.py # Logika načítání CRD
│ └── operator.py # Hlavní logika fungování operátora
└── setup.py # Úprava balíčkuDocker a Helm
Dockerfile bude neuvěřitelně jednoduchý: vezmeme základní obraz python-alpine a nainstalujeme náš balíček. Jeho optimalizaci odložíme na lepší časy:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Nasazení pro operátora je také velmi jednoduché:
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 }}-accNakonec je třeba vytvořit odpovídající roli pro operátora s potřebnými právy:
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 }}Výsledek
Takto, bez strachu, výčitek a studia Go, jsme dokázali sestavit vlastního operátora pro Kubernetes v Pythonu. Samozřejmě, že má ještě co zdokonalovat: v budoucnu bude moci zpracovávat více pravidel, pracovat v několika vláknech, autonomně sledovat změny svých CRD...
Abychom se s kódem mohli lépe seznámit, shromáždili jsme ho v . Pokud hledáte příklady vážnějších operátorů vytvořených pomocí Pythonu, můžete se zaměřit na dva operátory pro nasazení mongodb ( a ).
P.S. A pokud se vám nechce rozumět událostem Kubernetes nebo vám jednoduše vyhovuje používat Bash — naši kolegové připravili hotové řešení v podobě (my to udělali v dubnu).
P.P.S.
Přečtěte si také náš blog:
- «»;
- «»;
- «»;
- «»;
- «».
Zdroj: habr.com
