
Go este în prezent singurul limbaj de programare pe care oamenii îl aleg pentru a scrie operatori pentru Kubernetes. Acest lucru se datorează unor motive obiective, cum ar fi:
- Există un framework puternic pentru dezvoltarea operatorilor în Go — .
- Aplicații de tip "game-changer" precum Docker și Kubernetes sunt scrise în Go. A scrie propriul operator în Go înseamnă a comunica cu ecosistemul într-o singură limbă.
- Performanța ridicată a aplicațiilor în Go și instrumentele simple pentru lucru cu concurența "din cutie".
NB: Apropo, cum să scrii propriul operator în Go, am în una dintre traducerile noastre ale autorilor străini.
Dar ce se întâmplă dacă lipsa de timp sau, pur și simplu, lipsa de motivație te împiedică să înveți Go? În articol este prezentat un exemplu de cum poți scrie un operator bine realizat, folosind unul dintre cele mai populare limbaje, pe care aproape fiecare inginer DevOps îl știe — Python.
Salutări: Copierator — operatorul de copiere!
Ca exemplu, să luăm în considerare dezvoltarea unui operator simplu, destinat pentru a copia ConfigMap fie la apariția unui nou namespace, fie la modificarea uneia dintre cele două entități: ConfigMap și Secret. Din punct de vedere practic, operatorul poate fi util pentru actualizarea în masă a configurațiilor aplicației (prin actualizarea ConfigMap) sau pentru actualizarea datelor secrete — de exemplu, cheile pentru lucrul cu Docker Registry (când se adaugă un Secret în namespace).
Deci, ce ar trebui să aibă un bun operator:
- Interacțiunea cu operatorul se realizează prin intermediul (denumite în continuare — CRD).
- Operatorul poate fi configurat. Pentru aceasta, vom folosi steaguri de linie de comandă și variabile de mediu.
- Crearea containerului Docker și a chart-ului Helm sunt gândite astfel încât utilizatorii să poată instala operatorul în clusterul lor Kubernetes cu ușurință (practic cu o singură comandă).
CRD
Pentru ca operatorul să știe ce resurse să caute și unde, trebuie să definim o regulă pentru el. Fiecare regulă va fi reprezentată sub forma unui singur obiect CRD. Ce câmpuri ar trebui să aibă acest CRD?
- Tipul resursei, pe care o vom căuta (ConfigMap sau Secret).
- Lista namespace-urilor, în care trebuie să se afle resursele.
- Selector, după care vom căuta resursele în namespace.
Să descriem 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 imediat vom crea o regulă simplă — pentru căutarea în namespace-ul cu numele default tuturor ConfigMap cu etichetele de forma copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultGata! Acum trebuie să obținem informații despre regula noastră. Voi preciza de la început că nu vom scrie manual cereri către API Server-ul cluster-ului. Vom folosi pentru aceasta o bibliotecă Python deja existentă :
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')}Ca rezultat al acestui cod vom obține următoarele:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Excelent: am reușit să obținem regula pentru operator. Și cel mai important — am făcut-o în stilul Kubernetes.
Variabile de mediu sau două puncte? Luăm totul!
Trecem la configurația principală a operatorului. Există două abordări de bază pentru configurarea aplicațiilor:
- folosind parametrii liniei de comandă;
- folosind variabilele de mediu.
Parametrii liniei de comandă permit citirea setărilor mai flexibil, cu suport pentru validarea tipurilor de date. În biblioteca standard Python există un modul argparser, pe care îl vom folosi. Detalii și exemple ale capacităților sale sunt disponibile în .
Iată cum va arăta, în cazul nostru, un exemplu de configurare pentru citirea parametrilor liniei de comandă:
parser = ArgumentParser(
description='Copyrator - operator de copiere.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Namespace-ul operatorului'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='Numele CRD'
)
args = parser.parse_args()Pe de altă parte, prin intermediul variabilelor de mediu în Kubernetes, putem transfera cu ușurință informațiile de serviciu despre pod într-un container. De exemplu, informațiile despre namespace-ul în care este rulat pod-ul pot fi obținute cu următoarea construcție:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Logica de funcționare a operatorului
Pentru a înțelege cum să separăm metodele pentru lucrul cu ConfigMap și Secret, vom folosi hărți speciale. Astfel, vom putea înțelege ce metode ne sunt necesare pentru monitorizare și crearea obiectului:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Apoi, este necesar să primim evenimente de la serverul API. Vom implementa acest lucru astfel:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Obținem metoda pentru monitorizarea obiectelor
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)După primirea evenimentului, trecem la logica principală a procesării acestuia:
# Типы событий, на которые будем реагировать
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) Logica principală este gata! Acum trebuie să împachetăm totul într-un singur pachet Python. Structurăm fișierul setup.py, scriind acolo metainformațiile despre proiect:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Versiune python %s neacceptată.' % '.'.join(version_info)
)
_NAME = 'copyrator'
setup(
name=_NAME,
version='0.0.1',
packages=find_packages(),
classifiers=[
'Statusul dezvoltării :: 3 - Alpha',
'Limbaj de programare :: Python',
'Limbaj de programare :: Python :: 3',
'Limbaj de programare :: Python :: 3.5',
'Limbaj de programare :: Python :: 3.6',
'Limbaj de programare :: 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: Clientul kubernetes pentru Python are propria versiune. Mai multe informații despre compatibilitatea versiunilor clientului și versiunile de Kubernetes pot fi găsite în .
Acum proiectul nostru arată astfel:
copyrator
├── copyrator
│ ├── cli.py # Logica activității cu linia de comandă
│ ├── constant.py # Constantele pe care le-am menționat mai sus
│ ├── load_crd.py # Logica încărcării CRD
│ └── operator.py # Logica principală de funcționare a operatorului
└── setup.py # Structurarea pachetuluiDocker și Helm
Dockerfile va fi extrem de simplu: vom lua imaginea de bază python-alpine și vom instala pachetul nostru. Optimizarea acestuia o vom amâna pentru vremuri mai bune:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Implementarea pentru operator este de asemenea foarte simplă:
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 }}-accÎn cele din urmă, trebuie să creăm rolul corespunzător pentru operator cu permisiunile necesare:
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 }}Rezultatul
Așa, fără frică, reproșuri și studierea limbii Go, am reușit să construim propriul nostru operator pentru Kubernetes utilizând Python. Desigur, mai este loc de îmbunătățiri: pe viitor, va putea gestiona mai multe reguli, va lucra pe mai multe fire, va monitoriza singur modificările CRD-urilor sale...
Pentru a ne familiariza mai bine cu codul, l-am plasat în . Dacă doriți exemple de operatori mai serioși, implementați cu Python, puteți să vă îndreptați atenția către doi operatori pentru desfășurarea mongodb ( și ).
P.S. Și dacă vă este lene să vă ocupați de evenimentele Kubernetes sau pur și simplu vă este mai familiar să folosiți Bash — colegii noștri au pregătit o soluție gata făcută sub forma unui (noi acesta în aprilie).
P.P.S.
Citiți și în blogul nostru:
- «»;
- «»;
- «»;
- «»;
- «».
Sursa: habr.com
