
Go в момента е монополист сред езиците за програмиране, които хората избират за написване на оператори за Kubernetes. Има обективни причини за това, като:
- Съществува мощен фреймворк за разработка на оператори на Go — .
- На Go са написани приложения, които промениха играта, като Docker и Kubernetes. Да напишеш свой оператор на Go значи да разговаряш с екосистемата на един и същ език.
- Високата производителност на приложенията на Go и простите инструменти за работа с concurrency "извън кутията".
NB: Между другото, как да напишеш свой оператор на Go, ние в една от нашите преводи на чуждестранни автори.
Но какво, ако ти пречи да учиш Go поради липса на време или, банално, мотивация? В статията е приведен пример за това как може да се напише качествен оператор, използвайки един от най-популярните езици, който практически всеки DevOps инженер знае — Python.
Представяме: Копиратор — копировъчен оператор!
За примера ще разгледаме разработването на прост оператор, предназначен за копирането на ConfigMap или при появата на нов namespace, или при изменение на едно от двете същества: ConfigMap и Secret. По отношение на практическото приложение, операторът може да бъде полезен за масово обновяване на конфигурации на приложението (чрез обновяване на ConfigMap) или за обновяване на секретни данни — например, ключове за работа с Docker Registry (при добавяне на Secret в namespace).
И така, какво трябва да има един добър оператор:
- Взаимодействието с оператора се извършва чрез (нататък — CRD).
- Операторът може да се конфигурира. За това ще използваме флагове на командния ред и променливи на околната среда.
- Сглобяването на Docker контейнер и Helm chart е проектирано така, че потребителите да могат лесно (буквално с една команда) да инсталират оператора в своя Kubernetes кластер.
CRD
За да знае операторът какви ресурси и къде да търси, трябва да му зададем правило. Всяко правило ще бъде представено под формата на един обект CRD. Какви полета трябва да има този CRD?
- Тип ресурс, който ще търсим (ConfigMap или Secret).
- Списък на namespace-ите, в които трябва да се намират ресурсите.
- Selector, по който ще търсим ресурсите в namespace.
Нека опишем 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 И сега ще създадем опростено правило — за търсене в namespace с име по подразбиране всички ConfigMap с етикети от вида copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultГотово! Сега трябва да получим информация за правилото ни. С радост уточнявам, че няма да пишем заявки към API сървър на клъстера. Вместо това ще използваме готова 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')}В резултат на изпълнението на този код ще получим следното:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Отлично: успяхме да получим правилото за оператора. И най-важното — направихме го в стил Kubernetes.
Променливи на средата или флагове? Взимаме всичко!
Преминаваме към основната конфигурация на оператора. Има два основни подхода за конфигуриране на приложения:
- да се използват параметри на командния ред;
- да се използват променливи на средата.
Параметрите на командния ред позволяват да прочитаме настройките по-гъвкаво с поддръжка и валидация на типове данни. В стандартната библиотека на Python има модул argparser, който ще използваме. Подробности и примери за неговите възможности са налични в .
Ето как ще изглежда пример за настройка на четенето на параметри от командния ред в нашия случай:
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()От друга страна, с помощта на променливи на средата в Kubernetes можем лесно да прехвърлим служебната информация за pod'а вътре в контейнера. Например, информацията за namespace, в който е стартиран pod, можем да получим със следния код:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Логиката на работа на оператора
За да разберем как да разделим методите за работа с ConfigMap и Secret, ще използваме специални карти. Тогава можем да разберем какви методи ни трябват за наблюдение и създаване на обекти:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}След това е необходимо да получаваме събития от API сървъра. Реализираме това по следния начин:
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)След получаването на събитие преминаваме към основната логика на обработката му:
# Типы событий, на которые будем реагировать
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) Основната логика е готова! Сега трябва да опаковаме всичко това в един Python пакет. Оформяме файла setup.py, пишем в него метаинформация за проекта:
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 за Python има собствено версиониране. За повече информация относно съвместимостта между версиите на клиента и версиите на Kubernetes можете да се запознаете с .
Сега нашият проект изглежда така:
copyrator
├── copyrator
│ ├── cli.py # Логиката за работа с командния ред
│ ├── constant.py # Константи, които изброихме по-горе
│ ├── load_crd.py # Логиката за зареждане на CRD
│ └── operator.py # Основната логика на работа на оператора
└── setup.py # Оформление на пакетаDocker и Helm
Dockerfile ще бъде примитивно прост: ще вземем базовия образ python-alpine и ще инсталираме нашия пакет. Оптимизацията ще отложим за по-добри времена:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Развитието за оператора също е много просто:
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Накрая, необходимо е да се създаде съответната роля за оператора с необходимите права:
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 }}Резюме
Ето така, без страх, упрек и изучаване на Go, успяхме да съберем собствения си оператор за Kubernetes на Python. Разбира се, той все още има много какво да развива: в бъдеще ще може да обработва множество правила, да работи в множество потоци, самостоятелно да следи промените на своите CRD...
За да може по-близо да се запознаете с кода, го събрахме в . Ако търсите примери на по-сериозни оператори, реализирани с Python, можете да се запознаете с двата оператора за разгръщане на mongodb ( и ).
P.S. А ако сте мързеливи да разбирате събитията на Kubernetes или просто ви е по-удобно да използвате Bash — нашите колеги приготвиха готово решение под формата на (ние го през април).
P.P.S.
Прочетете също в нашия блог:
- «»;
- «»;
- «»;
- «»;
- «».
Източник: habr.com
