Kubernetes Operator in Python zonder frameworks en SDK

Kubernetes Operator in Python zonder frameworks en SDK

Go heeft momenteel het monopolie op de programmeertalen die mensen kiezen om instructies voor Kubernetes te schrijven. Daar zijn objectieve redenen voor, zoals:

  1. Er is een krachtig raamwerk voor het ontwikkelen van operators in Go - Operator-SDK.
  2. Baanbrekende applicaties zoals Docker en Kubernetes zijn geschreven in Go. Het schrijven van uw operator in Go betekent dat u dezelfde taal spreekt met het ecosysteem.
  3. Hoge prestaties van Go-applicaties en eenvoudige tools om out-of-the-box met gelijktijdigheid te werken.

NB: Trouwens, hoe je je eigen verklaring in Go schrijft, wij al beschreven in een van onze vertalingen door buitenlandse auteurs.

Maar wat als u Go niet kunt leren vanwege tijdgebrek of, simpel gezegd, motivatie? Het artikel geeft een voorbeeld van hoe je een goede verklaring kunt schrijven in een van de populairste talen die vrijwel elke DevOps-engineer kent: Python.

Maak kennis met: Kopieermachine - kopieeroperator!

Overweeg bijvoorbeeld om een ​​eenvoudige instructie te ontwikkelen die is ontworpen om een ​​ConfigMap te kopiëren wanneer een nieuwe naamruimte verschijnt of wanneer een van de twee entiteiten verandert: ConfigMap en Secret. Vanuit praktisch oogpunt kan de operator nuttig zijn voor het bulksgewijs bijwerken van applicatieconfiguraties (door de ConfigMap bij te werken) of voor het bijwerken van geheime gegevens - bijvoorbeeld sleutels voor het werken met de Docker Registry (bij het toevoegen van Secret aan de naamruimte).

aldus wat een goede operator moet hebben:

  1. Interactie met de operator vindt plaats met behulp van Aangepaste resourcedefinities (hierna CRD genoemd).
  2. De operator kan worden geconfigureerd. Om dit te doen, zullen we opdrachtregelvlaggen en omgevingsvariabelen gebruiken.
  3. De build van de Docker-container en het Helm-diagram is zo ontworpen dat gebruikers de operator eenvoudig (letterlijk met één opdracht) in hun Kubernetes-cluster kunnen installeren.

CRD

Zodat de operator weet welke middelen en waar hij moet zoeken, moeten we een regel voor hem instellen. Elke regel wordt weergegeven als één enkel CRD-object. Welke velden moet deze CRD hebben?

  1. Brontype, waarnaar we zullen zoeken (ConfigMap of Secret).
  2. Lijst met naamruimten, waarin de bronnen zich moeten bevinden.
  3. Keuzeschakelaar, waarmee we naar bronnen in de naamruimte zullen zoeken.

Laten we de CRD beschrijven:

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 we maken het meteen eenvoudige regel — om in de naamruimte met de naam te zoeken default alle ConfigMap met labels zoals copyrator: "true":

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

Klaar! Nu moeten we op de een of andere manier informatie krijgen over onze regel. Laat ik meteen een voorbehoud maken dat we zelf geen verzoeken naar de cluster API Server zullen schrijven. Om dit te doen, zullen we een kant-en-klare Python-bibliotheek gebruiken 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 resultaat van het uitvoeren van deze code krijgen we het volgende:

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

Geweldig: het is ons gelukt om een ​​regel voor de operator te krijgen. En het allerbelangrijkste: we deden wat de Kubernetes-manier wordt genoemd.

Omgevingsvariabelen of vlaggen? Wij nemen alles!

Laten we verder gaan met de configuratie van de hoofdoperator. Er zijn twee basisbenaderingen voor het configureren van applicaties:

  1. gebruik opdrachtregelopties;
  2. gebruik omgevingsvariabelen.

Met opdrachtregelopties kunt u de instellingen flexibeler lezen, met ondersteuning en validatie van gegevenstypen. De standaardbibliotheek van Python heeft een module argparser, die we zullen gebruiken. Details en voorbeelden van de mogelijkheden zijn beschikbaar in officiële documentatie.

In ons geval zou een voorbeeld van het instellen van leesopdrachtregelvlaggen er als volgt uitzien:

   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 de andere kant kunt u met behulp van omgevingsvariabelen in Kubernetes eenvoudig service-informatie over de pod in de container overbrengen. We kunnen bijvoorbeeld informatie krijgen over de naamruimte waarin de pod wordt uitgevoerd met de volgende constructie:

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

Operatorlogica

Om te begrijpen hoe we de methoden voor het werken met ConfigMap en Secret kunnen scheiden, zullen we speciale kaarten gebruiken. Dan kunnen we begrijpen welke methoden we nodig hebben om het object te volgen en te maken:

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 gebeurtenissen ontvangen van de API-server. Laten we het als volgt implementeren:

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 we de gebeurtenis hebben ontvangen, gaan we verder met de hoofdlogica van de verwerking ervan:

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

De hoofdlogica is klaar! Nu moeten we dit allemaal in één Python-pakket verpakken. Wij bereiden het dossier voor setup.py, schrijf daar meta-informatie over het project:

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 voor Python heeft zijn eigen versiebeheer. Meer informatie over compatibiliteit tussen clientversies en Kubernetes-versies vindt u in compatibiliteitsmatrices.

Ons project ziet er nu als volgt uit:

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

Dokwerker en Helm

Het Dockerbestand zal ongelooflijk eenvoudig zijn: neem de basis-python-alpine-image en installeer ons pakket. Laten we de optimalisatie ervan uitstellen tot betere tijden:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Ook de inzet voor de operator is heel 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 een passende rol voor de operator creëren met de nodige rechten:

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

Totaal

Zo konden we, zonder angst, verwijten of het leren van Go, onze eigen operator voor Kubernetes in Python bouwen. Natuurlijk heeft het nog ruimte om te groeien: in de toekomst zal het in staat zijn om meerdere regels te verwerken, in meerdere threads te werken, onafhankelijk veranderingen in zijn CRD's te monitoren...

Om je de code beter te laten zien, hebben we deze erin gezet openbare opslagplaats. Als je voorbeelden wilt van serieuzere operators die zijn geïmplementeerd met Python, kun je je aandacht richten op twee operators voor het inzetten van mongodb (eerste и tweede).

PS En als je te lui bent om met Kubernetes-evenementen om te gaan of gewoon meer gewend bent aan het gebruik van Bash, hebben onze collega's een kant-en-klare oplossing voorbereid in de vorm shell-operator (Wij aangekondigd het in april).

PPS

Lees ook op onze blog:

Bron: www.habr.com

Voeg een reactie