Operater Kubernetes v Pythonu brez okvirov in SDK

Operater Kubernetes v Pythonu brez okvirov in SDK

Go ima trenutno monopol nad programskimi jeziki, ki jih ljudje izberejo za pisanje izjav za Kubernetes. Za to obstajajo objektivni razlogi, kot so:

  1. V Go obstaja zmogljivo ogrodje za razvoj operaterjev - SDK operaterja.
  2. Aplikacije, ki spreminjajo igre, kot sta Docker in Kubernetes, so napisane v Go. Pisanje operaterja v Go pomeni govoriti isti jezik z ekosistemom.
  3. Visoka zmogljivost aplikacij Go in enostavna orodja za delo s sočasnostjo.

NB: Mimogrede, kako napisati lastno izjavo v Go, we že opisano v enem od naših prevodov tujih avtorjev.

Kaj pa, če vam pri učenju Go onemogoča pomanjkanje časa ali, preprosto rečeno, motivacije? Članek ponuja primer, kako lahko napišete dobro izjavo z uporabo enega najbolj priljubljenih jezikov, ki jih pozna skoraj vsak inženir DevOps - Python.

Spoznajte: Kopirni stroj - kopirni operater!

Kot primer razmislite o razvoju preprostega stavka, namenjenega kopiranju ConfigMap, ko se pojavi nov imenski prostor ali ko se spremeni ena od dveh entitet: ConfigMap in Secret. S praktičnega vidika je lahko operater uporaben za množično posodabljanje konfiguracij aplikacije (s posodobitvijo ConfigMap) ali za posodabljanje skrivnih podatkov - na primer ključev za delo z registrom Docker (pri dodajanju Secret v imenski prostor).

Torej, kaj mora imeti dober operater:

  1. Interakcija z operaterjem poteka z uporabo Definicije virov po meri (v nadaljevanju CRD).
  2. Operaterja je mogoče konfigurirati. Za to bomo uporabili zastavice ukazne vrstice in spremenljivke okolja.
  3. Zgradba vsebnika Docker in grafikona Helm je zasnovana tako, da lahko uporabniki preprosto (dobesedno z enim ukazom) namestijo operaterja v svojo gručo Kubernetes.

CRD

Da bo operater vedel, katere vire naj išče in kje naj išče, mu moramo postaviti pravilo. Vsako pravilo bo predstavljeno kot en sam objekt CRD. Katera polja mora imeti ta CRD?

  1. Vrsta vira, ki ga bomo iskali (ConfigMap ali Secret).
  2. Seznam imenskih prostorov, v kateri naj bodo viri.
  3. Izbirnik, s katerim bomo iskali vire v imenskem prostoru.

Opišimo 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

In ustvarili ga bomo takoj preprosto pravilo — za iskanje v imenskem prostoru z imenom default vse ConfigMap z oznakami, kot je copyrator: "true":

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

pripravljena! Zdaj moramo nekako pridobiti informacije o našem pravilu. Naj takoj rezerviram, da sami ne bomo pisali zahtev za API strežnik grozda. Za to bomo uporabili že pripravljeno knjižnico Python odjemalec 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')}

Kot rezultat izvajanja te kode dobimo naslednje:

{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}

Super: uspelo nam je pridobiti pravilo za operaterja. In kar je najpomembnejše, naredili smo tako imenovani način Kubernetes.

Okoljske spremenljivke ali zastavice? Vzamemo vse!

Preidimo na glavno konfiguracijo operaterja. Obstajata dva osnovna pristopa za konfiguriranje aplikacij:

  1. uporabite možnosti ukazne vrstice;
  2. uporabite spremenljivke okolja.

Možnosti ukazne vrstice vam omogočajo bolj prilagodljivo branje nastavitev s podporo in preverjanjem vrste podatkov. Pythonova standardna knjižnica ima modul argparser, ki ga bomo uporabili. Podrobnosti in primeri njegovih zmogljivosti so na voljo v uradna dokumentacija.

Za naš primer bi takole izgledal primer nastavitve branja zastavic ukazne vrstice:

   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()

Po drugi strani pa lahko z uporabo spremenljivk okolja v Kubernetesu enostavno prenesete storitvene informacije o podu znotraj vsebnika. Na primer, informacije o imenskem prostoru, v katerem teče pod, lahko pridobimo z naslednjo konstrukcijo:

env:
- name: NAMESPACE
  valueFrom:
     fieldRef:
         fieldPath: metadata.namespace 

Operaterska logika

Da bi razumeli, kako ločiti metode za delo s ConfigMap in Secret, bomo uporabili posebne zemljevide. Nato lahko razumemo, katere metode potrebujemo za sledenje in ustvarjanje predmeta:

LIST_TYPES_MAP = {
    'configmap': 'list_namespaced_config_map',
    'secret': 'list_namespaced_secret',
}

CREATE_TYPES_MAP = {
    'configmap': 'create_namespaced_config_map',
    'secret': 'create_namespaced_secret',
}

Nato morate prejeti dogodke s strežnika API. Izvedimo ga na naslednji način:

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)

Po prejemu dogodka preidemo na glavno logiko njegove obdelave:

# Типы событий, на которые будем реагировать
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)

Glavna logika je pripravljena! Zdaj moramo vse to zapakirati v en paket Python. Pripravimo datoteko setup.py, tam napišite meta informacije o projektu:

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: Odjemalec kubernetes za Python ima lastno različico. Več informacij o združljivosti med različicami odjemalcev in različicami Kubernetes lahko najdete v združljivostne matrike.

Zdaj je naš projekt videti takole:

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

Docker in Helm

Dockerfile bo neverjetno preprost: vzemite osnovno sliko python-alpine in namestite naš paket. Odložimo njegovo optimizacijo na boljše čase:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Tudi uvedba za operaterja je zelo preprosta:

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

Končno morate ustvariti ustrezno vlogo za operaterja s potrebnimi pravicami:

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 }}

Skupaj

Tako smo lahko brez strahu, očitkov ali učenja Go zgradili lasten operater za Kubernetes v Pythonu. Seveda ima še prostor za rast: v prihodnosti bo lahko obdeloval več pravil, deloval v več nitih, neodvisno spremljal spremembe v svojih CRD-jih ...

Da bi si podrobneje ogledali kodo, smo jo vstavili javni repozitorij. Če želite primere resnejših operaterjev, implementiranih s Pythonom, lahko svojo pozornost usmerite na dva operaterja za uvajanje mongodb (первый и 2.).

PS In če ste preleni, da bi se ukvarjali z dogodki Kubernetes ali ste preprosto bolj navajeni uporabljati Bash, so naši sodelavci pripravili že pripravljeno rešitev v obliki lupina-operater (Mi napovedal aprila).

PPS

Preberite tudi na našem blogu:

Vir: www.habr.com

Dodaj komentar