Kubernetes Operator i Python uten rammer og SDK

Kubernetes Operator i Python uten rammer og SDK

Go har for tiden monopol på programmeringsspråkene folk velger å skrive uttalelser for Kubernetes. Det er objektive grunner til dette, som:

  1. Det er et kraftig rammeverk for å utvikle operatører i Go - Operatør SDK.
  2. Spillendrende applikasjoner som Docker og Kubernetes er skrevet i Go. Å skrive operatøren din i Go betyr å snakke samme språk med økosystemet.
  3. Høy ytelse av Go-applikasjoner og enkle verktøy for å jobbe med samtidighet rett ut av esken.

NB: Forresten, hvordan skrive din egen uttalelse i Go, vi allerede beskrevet i en av våre oversettelser av utenlandske forfattere.

Men hva om du blir forhindret i å lære Go på grunn av mangel på tid eller, rett og slett, motivasjon? Artikkelen gir et eksempel på hvordan du kan skrive en god uttalelse ved å bruke et av de mest populære språkene som nesten alle DevOps-ingeniører kan - Python.

Møt: Kopimaskin - kopioperatør!

Som et eksempel kan du vurdere å utvikle en enkel setning designet for å kopiere et ConfigMap enten når et nytt navneområde vises eller når en av to enheter endres: ConfigMap og Secret. Fra et praktisk synspunkt kan operatøren være nyttig for masseoppdatering av applikasjonskonfigurasjoner (ved å oppdatere ConfigMap) eller for å oppdatere hemmelige data - for eksempel nøkler for å jobbe med Docker Registry (når du legger til Secret til navneområdet).

således hva en god operatør bør ha:

  1. Samhandling med operatør utføres ved hjelp av Egendefinerte ressursdefinisjoner (heretter kalt CRD).
  2. Operatøren kan konfigureres. For å gjøre dette bruker vi kommandolinjeflagg og miljøvariabler.
  3. Byggingen av Docker-beholderen og rorkartet er utformet slik at brukere enkelt (bokstavelig talt med én kommando) kan installere operatøren i Kubernetes-klyngen.

CRD

For at operatøren skal vite hvilke ressurser han skal se etter og hvor han skal lete, må vi sette en regel for ham. Hver regel vil bli representert som et enkelt CRD-objekt. Hvilke felt bør denne CRD ha?

  1. Ressurstype, som vi skal se etter (ConfigMap eller Secret).
  2. Liste over navneområder, der ressursene skal ligge.
  3. Selector, der vi vil søke etter ressurser i navneområdet.

La oss 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 lager det med en gang enkel regel – for å søke 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

Klar! Nå må vi på en eller annen måte få informasjon om regelen vår. La meg ta en reservasjon med en gang om at vi ikke vil skrive forespørsler til cluster API Server selv. For å gjøre dette bruker vi et ferdig 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 av å kjøre denne koden får vi følgende:

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

Flott: vi klarte å få en regel for operatøren. Og viktigst av alt, vi gjorde det som kalles Kubernetes-måten.

Miljøvariabler eller flagg? Vi tar alt!

La oss gå videre til hovedoperatørkonfigurasjonen. Det er to grunnleggende tilnærminger for å konfigurere applikasjoner:

  1. bruk kommandolinjealternativer;
  2. bruke miljøvariabler.

Kommandolinjealternativer lar deg lese innstillinger mer fleksibelt, med datatypestøtte og validering. Pythons standardbibliotek har en modul argparser, som vi skal bruke. Detaljer og eksempler på funksjonene er tilgjengelige i offisiell dokumentasjon.

For vårt tilfelle er dette hvordan et eksempel på å sette opp lesekommandolinjeflagg vil 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()

På den annen side, ved å bruke miljøvariabler i Kubernetes, kan du enkelt overføre tjenesteinformasjon om poden inne i beholderen. For eksempel kan vi få informasjon om navneområdet som poden kjører i med følgende konstruksjon:

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

Operatørlogikk

For å forstå hvordan man skiller metoder for å jobbe med ConfigMap og Secret, vil vi bruke spesielle kart. Da kan vi forstå hvilke metoder vi trenger for å spore og lage 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',
}

Deretter må du motta hendelser fra API-serveren. La oss 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)

Etter å ha mottatt hendelsen, går vi videre til hovedlogikken for å 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! Nå må vi pakke alt dette inn i én Python-pakke. Vi forbereder filen setup.py, skriv metainformasjon om prosjektet 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 for Python har sin egen versjonering. Mer informasjon om kompatibilitet mellom klientversjoner og Kubernetes-versjoner finner du i kompatibilitetsmatriser.

Nå ser prosjektet vårt slik ut:

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

Docker og Helm

Dockerfilen vil være utrolig enkel: ta det grunnleggende python-alpine-bildet og installer pakken vår. La oss utsette optimaliseringen til bedre tider:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Utplasseringen for operatøren er også veldig 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

Til slutt må du opprette en passende rolle for operatøren med de nødvendige rettighetene:

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 slik vi, uten frykt, bebreidelser eller lære Go, kunne bygge vår egen operatør for Kubernetes i Python. Selvfølgelig har den fortsatt plass til å vokse: i fremtiden vil den kunne behandle flere regler, jobbe i flere tråder, uavhengig overvåke endringer i CRD-ene ...

For å gi deg en nærmere titt på koden, har vi lagt den inn offentlig depot. Hvis du vil ha eksempler på mer seriøse operatører implementert med Python, kan du rette oppmerksomheten mot to operatører for å distribuere mongodb (første и andre).

PS Og hvis du er for lat til å håndtere Kubernetes-arrangementer, eller du rett og slett er mer vant til å bruke Bash, har våre kolleger utarbeidet en ferdig løsning i skjemaet shell-operatør (Vi kunngjort det i april).

PPS

Les også på bloggen vår:

Kilde: www.habr.com

Legg til en kommentar