Kubernetes Operator σε Python χωρίς πλαίσια και SDK

Kubernetes Operator σε Python χωρίς πλαίσια και SDK

Η Go έχει επί του παρόντος το μονοπώλιο στις γλώσσες προγραμματισμού που επιλέγουν οι άνθρωποι να γράφουν δηλώσεις για το Kubernetes. Υπάρχουν αντικειμενικοί λόγοι για αυτό, όπως:

  1. Υπάρχει ένα ισχυρό πλαίσιο για την ανάπτυξη χειριστών στο Go - SDK χειριστή.
  2. Οι εφαρμογές που αλλάζουν παιχνίδια όπως το Docker και το Kubernetes είναι γραμμένες στο Go. Το να γράψετε τον χειριστή σας στο Go σημαίνει να μιλάτε την ίδια γλώσσα με το οικοσύστημα.
  3. Υψηλή απόδοση των εφαρμογών Go και απλά εργαλεία για εργασία με ταυτόχρονη χρήση.

NB: Παρεμπιπτόντως, πώς να γράψετε τη δική σας δήλωση στο Go, εμείς έχει ήδη περιγραφεί σε μια από τις μεταφράσεις μας από ξένους συγγραφείς.

Τι γίνεται όμως αν σας εμποδίσει να μάθετε Go λόγω έλλειψης χρόνου ή, με απλά λόγια, κινήτρων; Το άρθρο παρέχει ένα παράδειγμα για το πώς μπορείτε να γράψετε μια καλή δήλωση χρησιμοποιώντας μια από τις πιο δημοφιλείς γλώσσες που σχεδόν κάθε μηχανικός DevOps γνωρίζει - Python.

Γνωρίστε: Αντιγραφέας - χειριστής αντιγραφής!

Για παράδειγμα, εξετάστε το ενδεχόμενο να αναπτύξετε μια απλή πρόταση που έχει σχεδιαστεί για την αντιγραφή ενός ConfigMap είτε όταν εμφανίζεται ένας νέος χώρος ονομάτων είτε όταν αλλάζει μία από τις δύο οντότητες: ConfigMap και Secret. Από πρακτική άποψη, ο χειριστής μπορεί να είναι χρήσιμος για μαζική ενημέρωση των διαμορφώσεων εφαρμογών (με ενημέρωση του ConfigMap) ή για ενημέρωση μυστικών δεδομένων - για παράδειγμα, κλειδιά για εργασία με το Μητρώο Docker (όταν προσθέτετε Secret στον χώρο ονομάτων).

Ετσι, τι πρέπει να έχει ένας καλός χειριστής:

  1. Η αλληλεπίδραση με τον χειριστή πραγματοποιείται χρησιμοποιώντας Προσαρμοσμένοι ορισμοί πόρων (εφεξής καλούμενο CRD).
  2. Ο χειριστής μπορεί να διαμορφωθεί. Για να γίνει αυτό, θα χρησιμοποιήσουμε σημαίες γραμμής εντολών και μεταβλητές περιβάλλοντος.
  3. Η κατασκευή του γραφήματος Docker container και 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 του συμπλέγματος. Για να το κάνουμε αυτό, θα χρησιμοποιήσουμε μια έτοιμη βιβλιοθήκη Python kubernetes-πελάτης:

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 μέσα στο κοντέινερ. Για παράδειγμα, μπορούμε να λάβουμε πληροφορίες σχετικά με τον χώρο ονομάτων στον οποίο εκτελείται το 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='[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 # Оформление пакета

Docker and 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 (πρώτα и δεύτερος).

Υ.Γ Και αν είστε πολύ τεμπέλης για να ασχοληθείτε με εκδηλώσεις του Kubernetes ή απλά είστε πιο συνηθισμένοι να χρησιμοποιείτε το Bash, οι συνάδελφοί μας έχουν ετοιμάσει μια έτοιμη λύση στη μορφή κέλυφος-χειριστής (Εμείς ανακοινώθηκε τον Απρίλιο).

ΜΑΔ

Διαβάστε επίσης στο blog μας:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο