Kubernetes Operator in Python sonder raamwerke en SDK's

Kubernetes Operator in Python sonder raamwerke en SDK's

Go het tans 'n monopolie op die programmeertale wat mense kies om stellings vir Kubernetes te skryf. Daar is objektiewe redes hiervoor, soos:

  1. Daar is 'n kragtige raamwerk vir die ontwikkeling van operateurs in Go - Operator SDK.
  2. Spelveranderende toepassings soos Docker en Kubernetes word in Go geskryf. Om jou operateur in Go te skryf, beteken om dieselfde taal met die ekosisteem te praat.
  3. Hoë werkverrigting van Go-toepassings en eenvoudige gereedskap om met gelyktydigheid uit die boks te werk.

NB: Terloops, hoe om jou eie stelling te skryf in Go, we reeds beskryf in een van ons vertalings deur buitelandse skrywers.

Maar wat as jy verhinder word om Go te leer deur gebrek aan tyd of, eenvoudig gestel, motivering? Die artikel verskaf 'n voorbeeld van hoe jy 'n goeie stelling kan skryf deur een van die gewildste tale te gebruik wat byna elke DevOps-ingenieur ken - Python.

Ontmoet: Kopieerder - kopieeroperateur!

As 'n voorbeeld, oorweeg dit om 'n eenvoudige stelling te ontwikkel wat ontwerp is om 'n ConfigMap te kopieer óf wanneer 'n nuwe naamruimte verskyn óf wanneer een van twee entiteite verander: ConfigMap en Secret. Uit 'n praktiese oogpunt kan die operateur nuttig wees vir grootmaat-opdatering van toepassingkonfigurasies (deur die ConfigMap op te dateer) of vir die opdatering van geheime data - byvoorbeeld sleutels om met die Docker Registry te werk (wanneer Geheim by die naamruimte gevoeg word).

So, wat 'n goeie operateur moet hê:

  1. Interaksie met die operateur word uitgevoer met behulp van Pasgemaakte hulpbrondefinisies (hierna verwys as CRD).
  2. Die operateur kan gekonfigureer word. Om dit te doen, sal ons opdragreëlvlae en omgewingsveranderlikes gebruik.
  3. Die bou van die Docker-houer en Helm-kaart is so ontwerp dat gebruikers die operateur maklik (letterlik met een opdrag) in hul Kubernetes-groep kan installeer.

CRD

Ten einde die operateur te weet watter hulpbronne om te soek en waar om te soek, moet ons 'n reël vir hom stel. Elke reël sal as 'n enkele CRD-voorwerp voorgestel word. Watter velde moet hierdie CRD hê?

  1. Hulpbron tipe, waarna ons sal soek (ConfigMap of Secret).
  2. Lys van naamruimtes, waarin die hulpbronne geleë moet wees.
  3. selector, waardeur ons na hulpbronne in die naamruimte sal soek.

Kom ons beskryf die 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

En ons sal dit dadelik skep eenvoudige reël — om in die naamruimte met die naam te soek default alles ConfigMap met etikette soos copyrator: "true":

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

Klaar! Nou moet ons op een of ander manier inligting oor ons reël kry. Laat ek dadelik 'n bespreking maak dat ons nie self versoeke na die cluster API Server sal skryf nie. Om dit te doen, sal ons 'n klaargemaakte Python-biblioteek gebruik kubernetes-kliënt:

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

As gevolg van die uitvoering van hierdie kode, kry ons die volgende:

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

Groot: ons het daarin geslaag om 'n reël vir die operateur te kry. En die belangrikste, ons het gedoen wat die Kubernetes-manier genoem word.

Omgewingsveranderlikes of vlae? Ons vat alles!

Kom ons gaan aan na die hoofoperateurkonfigurasie. Daar is twee basiese benaderings om toepassings op te stel:

  1. gebruik opdragreëlopsies;
  2. omgewingsveranderlikes te gebruik.

Opdragreëlopsies laat jou toe om instellings meer buigsaam te lees, met datatipe ondersteuning en validering. Python se standaard biblioteek het 'n module argparser, wat ons sal gebruik. Besonderhede en voorbeelde van sy vermoëns is beskikbaar in amptelike dokumentasie.

Vir ons geval, dit is hoe 'n voorbeeld van die opstel van leesopdragreëlvlae sou lyk:

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

Aan die ander kant, met behulp van omgewingsveranderlikes in Kubernetes, kan jy maklik diensinligting oor die peul in die houer oordra. Ons kan byvoorbeeld inligting kry oor die naamruimte waarin die peul loop met die volgende konstruksie:

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

Operator logika

Om te verstaan ​​hoe om metodes te skei om met ConfigMap en Secret te werk, sal ons spesiale kaarte gebruik. Dan kan ons verstaan ​​watter metodes ons nodig het om die voorwerp op te spoor en te skep:

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

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

Vervolgens moet u gebeure vanaf die API-bediener ontvang. Kom ons implementeer dit soos volg:

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)

Nadat ons die gebeurtenis ontvang het, gaan ons voort na die hooflogika om dit te verwerk:

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

Die hooflogika is gereed! Nou moet ons dit alles in een Python-pakket verpak. Ons berei die lêer voor setup.py, skryf meta-inligting oor die projek daar:

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: Die kubernetes-kliënt vir Python het sy eie weergawe. Meer inligting oor versoenbaarheid tussen kliëntweergawes en Kubernetes-weergawes kan gevind word in verenigbaarheidsmatrikse.

Nou lyk ons ​​projek so:

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

Docker en Helm

Die Dockerfile sal ongelooflik eenvoudig wees: neem die basis python-alpine-beeld en installeer ons pakket. Kom ons stel die optimalisering daarvan uit tot beter tye:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Ontplooiing vir die operateur is ook baie eenvoudig:

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

Ten slotte moet u 'n toepaslike rol vir die operateur skep met die nodige regte:

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

Totale

Dit is hoe ons, sonder vrees, verwyt of Go leer, ons eie operateur vir Kubernetes in Python kon bou. Natuurlik het dit nog ruimte om te groei: in die toekoms sal dit veelvuldige reëls kan verwerk, in verskeie drade kan werk, veranderinge in sy CRD's onafhanklik kan monitor ...

Om jou die kode van nader te bekyk, het ons dit ingesit openbare bewaarplek. As jy voorbeelde wil hê van meer ernstige operateurs wat met Python geïmplementeer word, kan jy jou aandag vestig op twee operateurs vir die implementering van mongodb (eerste и 2).

NS En as jy te lui is om Kubernetes-gebeure te hanteer of jy is bloot meer gewoond daaraan om Bash te gebruik, het ons kollegas 'n klaargemaakte oplossing in die vorm voorberei dop-operateur (Ons aangekondig dit in April).

PPS

Lees ook op ons blog:

Bron: will.com

Voeg 'n opmerking