Kubernetes Operator i Python utan ramverk och SDK

Kubernetes Operator i Python utan ramverk och SDK

Go har för närvarande monopol på de programmeringsspråk som folk väljer att skriva uttalanden för Kubernetes. Det finns objektiva skäl till detta, såsom:

  1. Det finns ett kraftfullt ramverk för att utveckla operatörer i Go - Operatörs-SDK.
  2. Spelförändrande applikationer som Docker och Kubernetes är skrivna i Go. Att skriva din operatör i Go innebär att du talar samma språk som ekosystemet.
  3. Hög prestanda för Go-applikationer och enkla verktyg för att arbeta med samtidighet direkt.

NB: Förresten, hur man skriver ett eget uttalande i Go, vi redan beskrivits i en av våra översättningar av utländska författare.

Men vad händer om du hindras från att lära dig Go på grund av tidsbrist eller, enkelt uttryckt, motivation? Artikeln ger ett exempel på hur du kan skriva ett bra uttalande med ett av de mest populära språken som nästan alla DevOps-ingenjörer kan - Python.

Möt: Kopiator - kopieringsoperatör!

Som ett exempel, överväg att utveckla en enkel sats som är utformad för att kopiera en ConfigMap antingen när ett nytt namnområde dyker upp eller när en av två entiteter ändras: ConfigMap och Secret. Ur praktisk synvinkel kan operatören vara användbar för massuppdatering av applikationskonfigurationer (genom att uppdatera ConfigMap) eller för att uppdatera hemlig data - till exempel nycklar för att arbeta med Docker Registry (när du lägger till Secret till namnområdet).

Så, vad en bra operatör ska ha:

  1. Interaktion med operatören utförs med hjälp av Anpassade resursdefinitioner (nedan kallat CRD).
  2. Operatören kan konfigureras. För att göra detta kommer vi att använda kommandoradsflaggor och miljövariabler.
  3. Bygget av Docker-behållaren och rordiagrammet är designat så att användare enkelt (bokstavligen med ett kommando) kan installera operatören i sitt Kubernetes-kluster.

CRD

För att operatören ska veta vilka resurser han ska leta efter och var han ska leta måste vi sätta en regel för honom. Varje regel kommer att representeras som ett enda CRD-objekt. Vilka fält ska denna CRD ha?

  1. Resurstyp, som vi kommer att leta efter (ConfigMap eller Secret).
  2. Lista över namnutrymmen, där resurserna bör finnas.
  3. Selector, med vilken vi kommer att söka efter resurser i namnområdet.

Låt oss beskriva 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

Och vi skapar det direkt enkel regel — för att söka i namnområdet med namnet default alla ConfigMap med etiketter som copyrator: "true":

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

Redo! Nu måste vi på något sätt få information om vår regel. Låt mig göra en reservation omedelbart att vi inte kommer att skriva förfrågningar till kluster API Server själva. För att göra detta kommer vi att använda ett färdigt Python-bibliotek kubernetes-klient:

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

Som ett resultat av att köra den här koden får vi följande:

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

Bra: vi lyckades få en regel för operatören. Och viktigast av allt, vi gjorde det som kallas Kubernetes-sättet.

Miljövariabler eller flaggor? Vi tar allt!

Låt oss gå vidare till huvudoperatörskonfigurationen. Det finns två grundläggande metoder för att konfigurera applikationer:

  1. använd kommandoradsalternativ;
  2. använda miljövariabler.

Kommandoradsalternativ låter dig läsa inställningar mer flexibelt, med datatypsstöd och validering. Pythons standardbibliotek har en modul argparser, som vi kommer att använda. Detaljer och exempel på dess kapacitet finns i officiell dokumentation.

För vårt fall är det så här ett exempel på att ställa in läskommandoradsflaggor skulle se ut:

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

Å andra sidan, med hjälp av miljövariabler i Kubernetes, kan du enkelt överföra tjänstinformation om podden inuti behållaren. Till exempel kan vi få information om namnutrymmet där podden körs med följande konstruktion:

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

Operatörslogik

För att förstå hur man separerar metoder för att arbeta med ConfigMap och Secret kommer vi att använda speciella kartor. Då kan vi förstå vilka metoder vi behöver för att spåra och skapa objektet:

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

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

Därefter måste du ta emot händelser från API-servern. Låt oss implementera det enligt följande:

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)

Efter att ha tagit emot händelsen går vi vidare till huvudlogiken för att bearbeta den:

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

Huvudlogiken är klar! Nu måste vi paketera allt detta i ett Python-paket. Vi förbereder filen setup.py, skriv metainformation om projektet där:

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-klienten för Python har sin egen versionshantering. Mer information om kompatibilitet mellan klientversioner och Kubernetes-versioner finns i kompatibilitetsmatriser.

Nu ser vårt projekt ut så här:

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

Docker och Helm

Dockerfilen kommer att vara otroligt enkel: ta basen python-alpine-bilden och installera vårt paket. Låt oss skjuta upp optimeringen till bättre tider:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Installationen för operatören är också mycket enkel:

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

Slutligen måste du skapa en lämplig roll för operatören med nödvändiga rättigheter:

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

Totalt

Det var så vi, utan rädsla, förebråelser eller lära oss Go, kunde bygga vår egen operatör för Kubernetes i Python. Naturligtvis har det fortfarande utrymme att växa: i framtiden kommer det att kunna bearbeta flera regler, arbeta i flera trådar, självständigt övervaka ändringar i sina CRD:er...

För att ge dig en närmare titt på koden har vi lagt in den offentligt förvar. Om du vill ha exempel på mer seriösa operatörer implementerade med Python, kan du rikta uppmärksamheten mot två operatörer för att distribuera mongodb (первый и 2.).

PS Och om du är för lat för att ta itu med Kubernetes-evenemang eller om du helt enkelt är mer van vid att använda Bash, har våra kollegor förberett en färdig lösning i formen skal-operatör (Vi meddelat det i april).

PPS

Läs även på vår blogg:

Källa: will.com

Lägg en kommentar