Пишем оператор Kubernetes на Python без фреймворков и SDK

Kate

Administrator
Команда форума
На сегодня Go – фактический монополист в реализации Kubernetes-операторов. Вот почему так сложилось:

  1. Operator SDK – это мощный фреймворк, который создан специально для реализации операторов на Go.
  2. Docker и Kubernetes реализованы на Go, и это меняет правила игры. Оператор, реализованный на Go, позволяет взаимодействовать со всей экосистемой.
  3. В языке Go есть простой механизм использования параллельности, поэтому приложения получаются высокопроизводительными.
Наверняка вам не захочется учить Go, если вы уже знаете Python. Поэтому мы сделаем надёжный оператор, используя язык программирования Python.

Copyrator – это наш оператор копирования!​

Мы напишем простой оператор, спроектированный, чтобы копировать ConfigMap, когда появляется новое пространство имён или при изменении состояния объектов ConfigMap, Secret. Нашим оператором удобно производить массовые обновления настроек приложения, а также сбрасывать им секреты, например, ключи репозитория образов Docker (когда Secret добавлен в пространство имён).

Так какие функции должен выполнять хороший оператор Kubernetes? Вот они:

  1. Взаимодействие с оператором производится с помощью Custom Resource Definitions (далее CRD).
  2. Оператор поддерживает настройку. Мы можем использовать флаги в командной строке и переменные окружения для настройки.
  3. Образ Docker и чарты Helm создаются с учётом облегчения установки для пользователей (обычно одной командой) в их кластер Kubernetes.

CRD​

Чтобы оператор знал, где и какие ресурсы искать, нужно настроить некоторые правила. Каждое правило будет представлено как особый CRD-объект. Какие поля должен иметь CRD-объект?

  1. Тип ресурсов, которые нам интересны (ConfigMap или Secret).
  2. Список пространств имён, хранящих ресурсы.
  3. Селектор, который помогает нам искать ресурсы в конкретном пространстве.
Давайте определим наш 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

И немедленно добавим простое правило для выбора ConfigMaps с метками, совпадающими с copyrator: "true" в пространстве имён по умолчанию:

apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: default
Отлично! Теперь нам нужно как-то получить информацию о наших правилах. Пришло время признаться: мы не будем делать запросы API нашего кластера вручную. Для этих целей есть Python библиотека – kubernetes-client:

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 способом.

Переменные окружения или флаги? Всё сразу!​

Пришло время приступить к базовой настройке оператора. Есть два главных подхода к настройке приложений:

С помощью параметров командной строки вы сможете извлекать настройки с большей гибкостью и с поддержкой проверки типов данных. Используем модуль argparser из стандартной библиотеки Python. Детали и примеры использования доступны в официальной документации Python.

Вот пример настройки поиска флагов командной строки, адаптированных для нашего случая:

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. Например, вы можете получить информацию о пространстве имён, в котором запущен модуль с помощью следующей структуры:

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', {})
# Искать совпадения с помощью селектора
for key, value in specs['selector'].items():
if labels.get(key) != value:
return
# Получить активные пространства имён
namespaces = map(
lambda x: x.metadata.name,
filter(
lambda x: x.status.phase == 'Active',
v1.list_namespace().items
)
)
for namespace in namespaces:
# Очистить метаданные, задать пространство имён
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 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),
]
}
)
Сейчас наш проект имеет следующую структуру:

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"]
Развернуть 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 }}

Заключение​

В этой статье мы показали, как создать ваш собственный оператор Kubernetes на Python. Конечно, он ещё сыроват: вы можете обогатить его возможностями обработки нескольких правил, самостоятельным мониторингом изменений в своих CRD, извлечь выгоду из возможностей параллелизма.

Весь код лежит в репозитории.


Источник статьи: https://proglib.io/p/kubernetes-operator-python
 
Сверху