اپراتور Kubernetes در پایتون بدون چارچوب و SDK

اپراتور Kubernetes در پایتون بدون چارچوب و SDK

Go در حال حاضر دارای انحصار زبان های برنامه نویسی است که افراد برای نوشتن بیانیه برای Kubernetes انتخاب می کنند. دلایل عینی برای این امر وجود دارد، مانند:

  1. یک چارچوب قدرتمند برای توسعه اپراتورها در Go وجود دارد - SDK اپراتور.
  2. برنامه های تغییر بازی مانند Docker و Kubernetes در Go نوشته شده اند. نوشتن اپراتور خود در Go به معنای صحبت کردن به همان زبان با اکوسیستم است.
  3. عملکرد بالا برنامه های Go و ابزارهای ساده برای کار با همزمانی خارج از جعبه.

NB: به هر حال، چگونه بیانیه خود را در Go بنویسیم، ما قبلا شرح داده شده است در یکی از ترجمه های ما توسط نویسندگان خارجی.

اما اگر به دلیل کمبود وقت یا به عبارت ساده، انگیزه از یادگیری Go جلوگیری شود، چه؟ این مقاله مثالی ارائه می دهد که چگونه می توانید یک بیانیه خوب با استفاده از یکی از محبوب ترین زبان هایی که تقریباً هر مهندس DevOps می داند بنویسید - پــایتــون.

ملاقات: دستگاه کپی - اپراتور کپی!

به عنوان مثال، ایجاد یک عبارت ساده را در نظر بگیرید که برای کپی کردن یک ConfigMap یا زمانی که یک فضای نام جدید ظاهر می شود یا زمانی که یکی از دو موجودیت تغییر می کند: ConfigMap و Secret طراحی شده است. از نقطه نظر عملی، اپراتور می تواند برای به روز رسانی انبوه تنظیمات برنامه (با به روز رسانی ConfigMap) یا برای به روز رسانی داده های مخفی مفید باشد - به عنوان مثال، کلیدهای کار با Docker Registry (هنگام افزودن Secret به فضای نام).

بنابراین، یک اپراتور خوب چه چیزی باید داشته باشد:

  1. تعامل با اپراتور با استفاده از تعاریف منابع سفارشی (از این پس CRD نامیده می شود).
  2. اپراتور را می توان پیکربندی کرد. برای این کار از پرچم های خط فرمان و متغیرهای محیطی استفاده می کنیم.
  3. ساختار کانتینر داکر و نمودار 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

و بلافاصله آن را ایجاد خواهیم کرد قانون ساده - برای جستجو در فضای نام با نام default همه ConfigMap با برچسب هایی مانند copyrator: "true":

apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
  name: main-rule
  labels:
    module: copyrator
ruleType: configmap
selector:
  copyrator: "true"
namespace: default

آماده! اکنون باید به نحوی در مورد قانون خود اطلاعاتی به دست آوریم. اجازه دهید فوراً رزرو کنم که خودمان درخواستی برای سرور API کلاستر ننویسیم. برای این کار از یک کتابخانه آماده پایتون استفاده می کنیم 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. از متغیرهای محیطی استفاده کنید

گزینه های خط فرمان به شما این امکان را می دهد که تنظیمات را با پشتیبانی از نوع داده و اعتبارسنجی انعطاف پذیرتر بخوانید. کتابخانه استاندارد پایتون دارای یک ماژول است 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 را در داخل کانتینر انتقال دهید. به عنوان مثال، ما می توانیم اطلاعاتی در مورد فضای نامی که 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)

منطق اصلی آماده است! اکنون باید همه اینها را در یک بسته پایتون بسته بندی کنیم. فایل را آماده می کنیم 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='[email protected]',
    include_package_data=True,
    install_requires=[
        'kubernetes==9.0.0',
    ],
    entry_points={
        'console_scripts': [
            '{0} = {0}.cli:main'.format(_NAME),
        ]
    }
)

NB: کلاینت kubernetes برای پایتون نسخه سازی خاص خود را دارد. اطلاعات بیشتر در مورد سازگاری بین نسخه های مشتری و نسخه های Kubernetes را می توانید در اینجا بیابید ماتریس های سازگاری.

اکنون پروژه ما به شکل زیر است:

copyrator
├── copyrator
│   ├── cli.py # Логика работы с командной строкой
│   ├── constant.py # Константы, которые мы приводили выше
│   ├── load_crd.py # Логика загрузки CRD
│   └── operator.py # Основная логика работы оператора
└── setup.py # Оформление пакета

داکر و هلم

Dockerfile بسیار ساده خواهد بود: تصویر پایه پایتون-آلپاین را بگیرید و بسته ما را نصب کنید. بیایید بهینه سازی آن را به زمان های بهتر موکول کنیم:

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 در پایتون بسازیم. البته هنوز جای رشد دارد: در آینده قادر خواهد بود چندین قانون را پردازش کند، در چندین رشته کار کند، به طور مستقل تغییرات در CRD خود را نظارت کند...

برای اینکه نگاه دقیق تری به کد داشته باشید، آن را در آن قرار داده ایم مخزن عمومی. اگر می‌خواهید نمونه‌هایی از عملگرهای جدی‌تر پیاده‌سازی شده با استفاده از پایتون را داشته باشید، می‌توانید توجه خود را به دو عملگر برای استقرار mongodb معطوف کنید (первый и دوم).

PS و اگر برای مقابله با رویدادهای Kubernetes خیلی تنبل هستید یا به سادگی به استفاده از Bash عادت دارید، همکاران ما راه حل آماده ای را در قالب آماده کرده اند. اپراتور پوسته (ما اعلام کرد در آوریل).

PPS

در وبلاگ ما نیز بخوانید:

منبع: www.habr.com

اضافه کردن نظر