
Go trenutno djeluje kao monopolist među programskim jezicima koje ljudi biraju za pisanje operatora za Kubernetes. Postoje objektivni razlozi za to, kao što su:
- Postoji moćan okvir za razvoj operatora na Go — .
- Na Go su napisane aplikacije koje su 'izmijenile igru', kao što su Docker i Kubernetes. Pisanje vlastitog operatora na Go znači razgovarati s ekosustavom na istom jeziku.
- Visoka izvedba aplikacija na Go i jednostavni alati za rad s konkurentnošću "iz kutije".
NB: Usput, kako napisati vlastiti operator na Go, mi u jednom od naših prijevoda stranih autora.
Ali šta ako vam nedostatak vremena ili, jednostavno, motivacije ometa učenje Go? U članku je prikazan primjer kako se može napisati dobar operator koristeći jedan od najpopularnijih jezika, koji gotovo svaki DevOps inženjer poznaje — Python.
Upoznajte: Kopirator — kopirni operator!
Kao primjer, razmotrit ćemo razvoj jednostavnog operatora koji je namijenjen kopiranju ConfigMap-a ili pri pojavljivanju novog namespace-a, ili pri promjeni jedne od dviju entiteta: ConfigMap i Secret. Iz praktične primjene, operator može biti koristan za masovno ažuriranje konfiguracija aplikacije (putem ažuriranja ConfigMap-a) ili za ažuriranje tajnih podataka — na primjer, ključeva za rad s Docker Registry (prilikom dodavanja tajne u namespace).
Dakle, što bi trebalo biti kod dobrog operatora:
- Interakcija s operatorom odvija se putem (dalje — CRD).
- Operator se može konfigurirati. Za to ćemo koristiti opcije naredbenog retka i varijable okruženja.
- Izrada Docker kontejnera i Helm charta osmišljena je tako da korisnici mogu lako (doslovno jednim naredbom) instalirati operator u svoj Kubernetes klaster.
CRD
Kako bi operator znao koje resurse i gdje tražiti, trebamo mu postaviti pravilo. Svako pravilo bit će predstavljeno kao jedan objekt CRD. Koja polja trebaju biti u ovom CRD-u?
- Tip resursa, koji ćemo tražiti (ConfigMap ili Secret).
- Lista namespace-ova, u kojima bi resursi trebali biti.
- Selector, prema kojem ćemo tražiti resurse u namespace-u.
Opisat ćemo 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 odmah ćemo stvoriti jednostavno pravilo — za pretraživanje u namespace-u imenom default svi ConfigMap s oznakama oblika copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultGotovo! Sada moramo na neki način dobiti informacije o našem pravilu. Odmah da kažem, da sami nećemo pisati upite ka API Serveru klastera. Za to ćemo iskoristiti gotovu Python biblioteku :
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')}Kao rezultat rada ovog koda dobit ćemo sljedeće:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Odlično: uspjeli smo dobiti pravilo za operatora. I što je najvažnije — učinili smo to na Kubernetes način.
Okolišne varijable ili zastavice? Uzimamo sve!
Prelazimo na glavnu konfiguraciju operatora. Postoje dva osnovna pristupa u konfiguriranju aplikacija:
- koristiti opcije komandne linije;
- koristiti okolišne varijable.
Opcije komandne linije omogućuju fleksibilnije čitanje postavki, uz podršku i validaciju tipova podataka. U standardnoj Python biblioteci postoji modul argparser, koji ćemo koristiti. Detalji i primjeri njegovih mogućnosti dostupni su u .
Evo kako će za naš slučaj izgledati primjer konfiguracije za čitanje zastavica komandne linije:
parser = ArgumentParser(
description='Copyrator - copy operator.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Operator Namespace'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='CRD Name'
)
args = parser.parse_args()S druge strane, uz pomoć varijabli okruženja u Kubernetesu, lako možemo prenijeti informacije o pod-u unutar kontejnera. Na primjer, informacije o imenskom prostranstvu u kojem je pod pokrenut, možemo dobiti sljedećom konstrukcijom:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Logika rada operatora
Da bismo razumjeli kako podijeliti metode za rad s ConfigMap i Secret, iskoristit ćemo posebne mape. Tako ćemo moći shvatiti koje su nam metode potrebne za praćenje i stvaranje objekta:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Nakon toga potrebno je primati događaje s API servera. Realiziramo to na sljedeći način:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Dobivamo metodu za praćenje objekata
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)Nakon primitka događaja pređemo na glavnu logiku njegove obrade:
# Типы событий, на которые будем реагировать
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) Glavna logika je spremna! Sada trebamo sve to upakirati u jedan Python paket. Pripremamo datoteku setup.py, pišemo tamo meta-informacije o projektu:
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='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: Kubernetes klijent za Python ima vlastito verzioniranje. Više o kompatibilnosti verzija klijenta i verzija Kubernetes možete saznati iz .
Trenutno naš projekt izgleda ovako:
copyrator
├── copyrator
│ ├── cli.py # Logika rada s komandnom linijom
│ ├── constant.py # Konstantne vrijednosti koje smo naveli ranije
│ ├── load_crd.py # Logika učitavanja CRD
│ └── operator.py # Glavna logika rada operatora
└── setup.py # Priprema paketaDocker i Helm
Dockerfile će biti neizmjerno jednostavan: uzet ćemo osnovnu sliku python-alpine i instalirati naš paket. Njegovu optimizaciju odgađamo za bolja vremena:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Razvoj za operatera je također vrlo jednostavan:
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 kraju, potrebno je kreirati odgovarajuću ulogu za operatera s potrebnim pravima:
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 }}Zaključak
Tako, bez straha, prijekora i proučavanja Go, uspjeli smo sastaviti vlastitog operatera za Kubernetes na Pythonu. Naravno, ima još prostora za rast: u budućnosti će moći obrađivati više pravila, raditi u više tokova, samostalno pratiti promjene svojih CRD…
Kako bismo se bolje upoznali s kodom, skupili smo ga u . Ako tražite primjere ozbiljnijih operatera razvijenih uz pomoć Pythona, možete obratiti pažnju na dva operatera za implementaciju mongodb ( i ).
P.S. Ako vam se ne da istraživati događaje Kubernetes ili vam je jednostavnije koristiti Bash — naši kolege su pripremili gotovo rješenje u obliku (mi ga u travnju).
P.S.
Pročitajte i na našem blogu:
- «»;
- «»;
- «»;
- «»;
- «».
Izvor: habr.com
