Kubernetes Operator i Python uden rammer og SDK

Kubernetes Operator i Python uden rammer og SDK

Go har i øjeblikket monopol på de programmeringssprog, folk vælger at skrive udsagn til Kubernetes. Det er der objektive grunde til, såsom:

  1. Der er en kraftfuld ramme for udvikling af operatører i Go - Operatør SDK.
  2. Spilskiftende applikationer som Docker og Kubernetes er skrevet i Go. At skrive din operatør i Go betyder, at du taler det samme sprog med økosystemet.
  3. Høj ydeevne af Go-applikationer og enkle værktøjer til at arbejde med samtidighed ud af boksen.

NB: Forresten, hvordan du skriver din egen erklæring i Go, vi allerede beskrevet i en af ​​vores oversættelser af udenlandske forfattere.

Men hvad nu, hvis du bliver forhindret i at lære Go af mangel på tid eller, kort sagt, motivation? Artiklen giver et eksempel på, hvordan du kan skrive en god erklæring ved hjælp af et af de mest populære sprog, som næsten alle DevOps-ingeniører kender - Python.

Mød: Kopimaskine - kopioperatør!

Som et eksempel kan du overveje at udvikle en simpel sætning designet til at kopiere et ConfigMap, enten når et nyt navneområde vises, eller når en af ​​to entiteter ændres: ConfigMap og Secret. Fra et praktisk synspunkt kan operatøren være nyttig til masseopdatering af applikationskonfigurationer (ved at opdatere ConfigMap) eller til opdatering af hemmelige data - for eksempel nøgler til at arbejde med Docker Registry (når du tilføjer Secret til navneområdet).

således hvad en god operatør skal have:

  1. Interaktion med operatøren udføres vha Brugerdefinerede ressourcedefinitioner (herefter benævnt CRD).
  2. Operatøren kan konfigureres. For at gøre dette bruger vi kommandolinjeflag og miljøvariabler.
  3. Opbygningen af ​​Docker-containeren og Helm-diagrammet er designet, så brugerne nemt (bogstaveligt talt med én kommando) kan installere operatøren i deres Kubernetes-klynge.

CRD

For at operatøren skal vide, hvilke ressourcer han skal kigge efter, og hvor han skal lede, er vi nødt til at sætte en regel for ham. Hver regel vil blive repræsenteret som et enkelt CRD-objekt. Hvilke felter skal denne CRD have?

  1. Ressourcetype, som vi vil lede efter (ConfigMap eller Secret).
  2. Liste over navnerum, hvori ressourcerne skal placeres.
  3. Selector, hvorved vi vil søge efter ressourcer i navnerummet.

Lad os beskrive 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

Og vi skaber det med det samme simpel regel — for at søge i navneområdet med navnet default alt 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

Parat! Nu skal vi på en eller anden måde få information om vores regel. Lad mig tage forbehold med det samme, at vi ikke selv vil skrive anmodninger til cluster API Server. For at gøre dette vil vi bruge et færdiglavet 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 et resultat af at køre denne kode får vi følgende:

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

Fantastisk: det lykkedes os at få en regel for operatøren. Og vigtigst af alt, vi gjorde det, der kaldes Kubernetes-måden.

Miljøvariabler eller flag? Vi tager alt!

Lad os gå videre til hovedoperatørkonfigurationen. Der er to grundlæggende tilgange til konfiguration af applikationer:

  1. brug kommandolinjeindstillinger;
  2. bruge miljøvariabler.

Kommandolinjeindstillinger giver dig mulighed for at læse indstillinger mere fleksibelt med datatypeunderstøttelse og validering. Pythons standardbibliotek har et modul argparser, som vi vil bruge. Detaljer og eksempler på dets muligheder er tilgængelige i officiel dokumentation.

I vores tilfælde er dette, hvordan et eksempel på opsætning af læsekommandolinjeflag ville se ud:

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

På den anden side kan du ved hjælp af miljøvariabler i Kubernetes nemt overføre serviceoplysninger om poden inde i containeren. For eksempel kan vi få information om det navneområde, som poden kører i, med følgende konstruktion:

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

Operatørlogik

For at forstå, hvordan man adskiller metoder til at arbejde med ConfigMap og Secret, vil vi bruge specielle kort. Så kan vi forstå, hvilke metoder vi har brug for til at spore og skabe 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',
}

Dernæst skal du modtage hændelser fra API-serveren. Lad os implementere det som følger:

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 at have modtaget begivenheden, går vi videre til hovedlogikken i at behandle 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)

Hovedlogikken er klar! Nu skal vi pakke alt dette ind i én Python-pakke. Vi forbereder filen setup.py, skriv metainformation om projektet der:

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 til Python har sin egen versionering. Flere oplysninger om kompatibilitet mellem klientversioner og Kubernetes-versioner kan findes i kompatibilitetsmatricer.

Nu ser vores projekt således ud:

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

Docker og Helm

Dockerfilen vil være utrolig enkel: Tag det grundlæggende python-alpine billede og installer vores pakke. Lad os udsætte dens optimering til bedre tider:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Udrulningen for operatøren er også meget 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

Endelig skal du oprette en passende rolle for operatøren med de nødvendige rettigheder:

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

Total

Det var sådan, vi uden frygt, bebrejdelser eller lære Go, var i stand til at bygge vores egen operatør til Kubernetes i Python. Selvfølgelig har det stadig plads til at vokse: i fremtiden vil det være i stand til at behandle flere regler, arbejde i flere tråde, uafhængigt overvåge ændringer i dets CRD'er...

For at give dig et nærmere kig på koden, har vi lagt den ind offentligt depot. Hvis du vil have eksempler på mere seriøse operatører implementeret ved hjælp af Python, kan du rette opmærksomheden mod to operatører til implementering af mongodb (первый и sekund).

PS Og hvis du er for doven til at beskæftige dig med Kubernetes-begivenheder, eller du simpelthen er mere vant til at bruge Bash, har vores kollegaer udarbejdet en færdig løsning i formularen shell-operatør (Vi annonceret det i april).

PPS

Læs også på vores blog:

Kilde: www.habr.com

Tilføj en kommentar