مشغل Kubernetes في Python بدون أطر عمل و SDK

مشغل Kubernetes في Python بدون أطر عمل و 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. تم تصميم بنية حاوية 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

وسوف نقوم بإنشائه على الفور قاعدة بسيطة — للبحث في مساحة الاسم بالاسم 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، يمكنك بسهولة نقل معلومات الخدمة حول الكبسولة داخل الحاوية. على سبيل المثال، يمكننا الحصول على معلومات حول مساحة الاسم التي تعمل فيها الكبسولة بالبنية التالية:

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='[email protected]',
    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 # Оформление пакета

عامل الميناء وهيلم

سيكون ملف 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. بالطبع، لا يزال لديه مجال للنمو: في المستقبل سيكون قادرًا على معالجة قواعد متعددة، والعمل في سلاسل رسائل متعددة، ومراقبة التغييرات في CRDs الخاصة به بشكل مستقل...

لإلقاء نظرة فاحصة على الكود، قمنا بوضعه المستودع العام. إذا كنت تريد أمثلة على عوامل أكثر جدية تم تنفيذها باستخدام بايثون، فيمكنك تحويل انتباهك إلى عاملين لنشر mongodb (الأول и ثان).

ملاحظة: وإذا كنت كسولًا جدًا في التعامل مع أحداث Kubernetes أو كنت ببساطة معتادًا على استخدام Bash، فقد قام زملاؤنا بإعداد حل جاهز في النموذج مشغل القشرة (نحن أعلن ذلك في أبريل).

ذكر المكتب الصحفى

اقرأ أيضًا على مدونتنا:

المصدر: www.habr.com

إضافة تعليق