Kubernetes Operator на Python без рамки и SDK.

Kubernetes Operator на Python без рамки и SDK.

Go в момента е монополист сред езиците за програмиране, които хората избират за написване на оператори за Kubernetes. Има обективни причини за това, като:

  1. Съществува мощен фреймворк за разработка на оператори на Go — Operator SDK.
  2. На Go са написани приложения, които промениха играта, като Docker и Kubernetes. Да напишеш свой оператор на Go значи да разговаряш с екосистемата на един и същ език.
  3. Високата производителност на приложенията на Go и простите инструменти за работа с concurrency "извън кутията".

NB: Между другото, как да напишеш свой оператор на Go, ние вече описахме в една от нашите преводи на чуждестранни автори.

Но какво, ако ти пречи да учиш Go поради липса на време или, банално, мотивация? В статията е приведен пример за това как може да се напише качествен оператор, използвайки един от най-популярните езици, който практически всеки DevOps инженер знае — Python.

Представяме: Копиратор — копировъчен оператор!

За примера ще разгледаме разработването на прост оператор, предназначен за копирането на ConfigMap или при появата на нов namespace, или при изменение на едно от двете същества: ConfigMap и Secret. По отношение на практическото приложение, операторът може да бъде полезен за масово обновяване на конфигурации на приложението (чрез обновяване на ConfigMap) или за обновяване на секретни данни — например, ключове за работа с Docker Registry (при добавяне на Secret в namespace).

И така, какво трябва да има един добър оператор:

  1. Взаимодействието с оператора се извършва чрез Custom Resource Definitions (нататък — CRD).
  2. Операторът може да се конфигурира. За това ще използваме флагове на командния ред и променливи на околната среда.
  3. Сглобяването на Docker контейнер и Helm chart е проектирано така, че потребителите да могат лесно (буквално с една команда) да инсталират оператора в своя Kubernetes кластер.

CRD

За да знае операторът какви ресурси и къде да търси, трябва да му зададем правило. Всяко правило ще бъде представено под формата на един обект CRD. Какви полета трябва да има този CRD?

  1. Тип ресурс, който ще търсим (ConfigMap или Secret).
  2. Списък на namespace-ите, в които трябва да се намират ресурсите.
  3. 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 библиотека 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.

Променливи на средата или флагове? Взимаме всичко!

Преминаваме към основната конфигурация на оператора. Има два основни подхода за конфигуриране на приложения:

  1. да се използват параметри на командния ред;
  2. да се използват променливи на средата.

Параметрите на командния ред позволяват да прочитаме настройките по-гъвкаво с поддръжка и валидация на типове данни. В стандартната библиотека на 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 — нашите колеги приготвиха готово решение под формата на shell-operator (ние анонсираха го през април).

P.P.S.

Прочетете също в нашия блог:

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster