Kubernetes Operator am Python ouni Kaderen an SDK

Kubernetes Operator am Python ouni Kaderen an SDK

Go huet de Moment e Monopol op d'Programméierungssproochen déi d'Leit wielen Aussoe fir Kubernetes ze schreiwen. Et ginn objektiv Grënn dofir, wéi:

  1. Et gëtt e mächtege Kader fir Betreiber am Go z'entwéckelen - Bedreiwer SDK.
  2. Spillverännerend Uwendungen wéi Docker a Kubernetes ginn a Go geschriwwen. Äre Bedreiwer op Go ze schreiwen heescht déi selwecht Sprooch mam Ökosystem ze schwätzen.
  3. Héich Leeschtung vu Go Uwendungen an einfach Tools fir mat der Konkurrenz aus der Këscht ze schaffen.

NB: Iwwregens, wéi Dir Är eege Ausso an Go schreift, mir scho beschriwwen an enger vun eisen Iwwersetzunge vun auslänneschen Auteuren.

Awer wat wann Dir verhënnert sidd Go ze léieren duerch Mangel un Zäit oder, einfach gesot, Motivatioun? Den Artikel liwwert e Beispill wéi Dir eng gutt Ausso schreiwe kënnt mat enger vun de beléifste Sproochen déi bal all DevOps Ingenieur weess - Python.

Trefft: Copier - Kopie Bedreiwer!

Als Beispill, betruecht eng einfach Ausso ze entwéckelen entwéckelt fir e ConfigMap ze kopéieren entweder wann en neien Nummraum erschéngt oder wann eng vun zwou Entitéiten ännert: ConfigMap a Secret. Aus enger praktescher Siicht kann de Bedreiwer nëtzlech sinn fir d'Massaktualiséierung vun Applikatiounskonfiguratiounen (duerch d'Aktualiséierung vun der ConfigMap) oder fir d'Aktualiséierung vun geheimen Donnéeën - zum Beispill Schlësselen fir mat der Docker Registry ze schaffen (wann Dir Secret zum Nummraum bäidréit).

An dofir, wat e gudde Bedreiwer soll hunn:

  1. Interaktioun mam Bedreiwer gëtt duerchgefouert mat Benotzerdefinéiert Ressource Definitiounen (nachfolgend als CRD bezeechent).
  2. De Bedreiwer kann konfiguréiert ginn. Fir dëst ze maachen, benotze mir Kommandozeil Fändelen an Ëmfeldvariablen.
  3. De Bau vum Docker Container an Helm Chart ass entwéckelt sou datt d'Benotzer einfach (wuertwiertlech mat engem Kommando) den Bedreiwer an hire Kubernetes Cluster installéiere kënnen.

CRD

Fir datt de Bedreiwer weess wéi eng Ressourcen sicht a wou sicht, musse mir eng Regel fir hien setzen. All Regel gëtt als eenzegen CRD Objet vertruede ginn. Wéi eng Felder soll dës CRD hunn?

  1. Ressource Typ, déi mir no kucken (ConfigMap oder Secret).
  2. Lëscht vun den Nummraim, an deem d'Ressourcen solle sinn.
  3. Gewielt, duerch déi mir no Ressourcen am Nummraum sichen.

Loosst eis d'CRD beschreiwen:

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

A mir kreéieren et direkt einfach Regel - fir am Nummraum mam Numm ze sichen default all ConfigMap mat Etiketten wéi copyrator: "true":

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

Fäerdeg! Elo musse mir iergendwéi Informatioun iwwer eis Regel kréien. Loosst mech direkt eng Reservatioun maachen, datt mir keng Ufroe fir de Cluster API Server selwer schreiwen. Fir dëst ze maachen, benotze mir eng fäerdeg Python-Bibliothéik 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')}

Als Resultat vun dësem Code lafen mir déi folgend:

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

Super: mir hunn et fäerdeg bruecht eng Regel fir de Bedreiwer ze kréien. A virun allem hu mir dat gemaach wat de Kubernetes Wee genannt gëtt.

Ëmweltverännerlechen oder Fändelen? Mir huelen alles!

Loosst eis op d'Haaptbetreiberkonfiguratioun goen. Et ginn zwou Basis Approche fir Uwendungen ze konfiguréieren:

  1. benotzen Kommando Linn Optiounen;
  2. benotzen Ëmwelt Verännerlechen.

Kommando Linn Optiounen erlaben Iech Astellunge méi flexibel ze liesen, mat Daten Typ Ënnerstëtzung a Validatioun. Python Standardbibliothéik huet e Modul argparser, déi mir wäerte benotzen. Detailer an Beispiller vu senge Fäegkeeten sinn verfügbar an offiziell Dokumentatioun.

Fir eise Fall ass dat wéi e Beispill fir d'Liesen vun Kommandozeil Fändelen opzestellen:

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

Op der anerer Säit, mat Ëmfeldvariablen a Kubernetes, kënnt Dir einfach Serviceinformatioun iwwer de Pod am Container transferéieren. Zum Beispill kënne mir Informatioun iwwer den Nummraum kréien an deem de Pod leeft mat der folgender Konstruktioun:

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

Bedreiwer Logik

Fir ze verstoen wéi Methoden fir mat ConfigMap a Secret ze trennen, benotze mir speziell Kaarten. Da kënne mir verstoen wéi eng Methoden mir brauchen fir den Objet ze verfolgen an ze kreéieren:

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

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

Als nächst musst Dir Eventer vum API Server kréien. Loosst eis et wéi follegt ëmsetzen:

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)

Nodeems mir d'Evenement kritt hunn, gi mir weider op d'Haaptlogik vun der Veraarbechtung:

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

D'Haaptlogik ass prett! Elo musse mir dat alles an ee Python Package packen. Mir preparéieren de Fichier setup.py, schreift do Meta-Informatiounen iwwer de Projet:

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: De kubernetes Client fir Python huet seng eege Versioun. Méi Informatioun iwwer Kompatibilitéit tëscht Client Versiounen a Kubernetes Versioune fannt Dir an Onbedenklechkeet matrices.

Elo gesäit eise Projet esou aus:

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

Docker et Helm

D'Dockerfile wäert onheemlech einfach sinn: huelt de Basis Python-Alpine Bild an installéiert eise Package. Loosst eis seng Optimiséierung bis besser Zäiten ausstelle:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Deployment fir de Bedreiwer ass och ganz einfach:

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

Schlussendlech musst Dir eng entspriechend Roll fir de Bedreiwer mat den néidege Rechter kreéieren:

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

D 'Resultat

Dat ass wéi mir, ouni Angscht, Reproche oder Go léieren, eisen eegene Bedreiwer fir Kubernetes am Python bauen. Natierlech huet et nach ëmmer Plaz fir ze wuessen: an Zukunft wäert et fäeg sinn verschidde Reegelen ze veraarbechten, a multiple thread ze schaffen, onofhängeg Ännerungen a senge CRDs iwwerwaachen ...

Fir Iech de Code méi no ze kucken, hu mir et gesat ëffentleche Repository. Wann Dir Beispiller vu méi eeschte Betreiber wëllt implementéiert mat Python, kënnt Dir Är Opmierksamkeet op zwee Betreiber dréien fir mongodb (первый и déi zweet).

PS A wann Dir ze faul sidd fir mat Kubernetes Eventer ze këmmeren oder Dir sidd einfach méi gewinnt Bash ze benotzen, hunn eis Kollegen eng fäerdeg Léisung an der Form virbereet Shell-Operateur (Mir ugekënnegt Abrëll).

Pps

Liest och op eisem Blog:

Source: will.com

Setzt e Commentaire