Kubernetes Operator på Python uden frameworks og SDK

Kubernetes Operator på Python uden frameworks og SDK

Go er i øjeblikket en monopolist blandt programmeringssprog, som folk vælger til at skrive operatorer til Kubernetes. Der er objektive grunde til dette, såsom:

  1. Der findes et kraftfuldt framework til udvikling af operatorer i Go — Operator SDK.
  2. Docker og Kubernetes er bygget på Go og har revolutioneret spillet. At skrive sin egen operator i Go betyder at tale med økosystemet på samme sprog.
  3. Den høje ydeevne af applikationer skrevet i Go og de enkle værktøjer til håndtering af concurrency 'out of the box'.

NB: For øvrigt, hvordan man skriver sin egen operator i Go, har vi allerede beskrevet i en af vores oversættelser af udenlandske forfattere.

Men hvad nu hvis manglende tid eller bare mangel på motivation forhindrer dig i at lære Go? Artiklen indeholder et eksempel på, hvordan man kan skrive en solid operator ved at bruge et af de mest populære sprog, som næsten hver DevOps-ingeniør kender — Python.

Præsentér: Kopiator — kopieringsoperatoren!

Som eksempel vil vi overveje udviklingen af en simpel operator designet til at kopiere ConfigMap enten ved oprettelsen af et nyt namespace eller ved ændring af en af de to enheder: ConfigMap og Secret. Fra et praktisk synspunkt kan operatoren være nyttig til masseopdateringer af applikationens konfigurationer (ved at opdatere ConfigMap) eller til opdatering af hemmelige data — for eksempel nøgler til brug med Docker Registry (ved tilføjelse af Secret til namespace).

Så, hvad der bør være i en god operator:

  1. Interaktion med operatoren sker gennem Custom Resource Definitions (herefter — CRD).
  2. Operatoren kan konfigureres. Til dette vil vi bruge kommandolinjeflag og miljøvariabler.
  3. Bygningen af Docker-containeren og Helm-diagrammet arbejder på en måde, så brugere nemt kan (bogstaveligt talt med ét kommando) installere operatoren i deres Kubernetes-klynge.

CRD

For at operatoren skal vide, hvilke ressourcer og hvor den skal lede efter dem, skal vi definere en regel for den. Hver regel vil blive repræsenteret som et enkelt CRD-objekt. Hvilke felter skal dette CRD have?

  1. Ressourcetype, som vi vil søge efter (ConfigMap eller Secret).
  2. Listen over namespaces, hvor ressourcerne skal findes.
  3. Selector, som vi vil bruge til at finde ressourcer i namespace.

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 lad os straks oprette en simpel regel — til at søge i namespace med navnet default alle ConfigMap med labels af typen copyrator: "true":

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

Færdig! Nu skal vi på en eller anden måde hente oplysninger om vores regel. Jeg vil straks påpege, at vi ikke vil skrive anmodninger til API-serveren i klyngen selv. I stedet vil vi benytte et færdigt Python-bibliotek 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')}

Som et resultat af denne kode får vi følgende:

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

Fremragende: vi har formået at hente reglen for operatøren. Og det vigtigste — vi gjorde det, som man siger, på Kubernetes måde.

Miljøvariabler eller flag? Vi tager det hele!

Lad os gå videre til den grundlæggende konfiguration af operatøren. Der er to grundlæggende tilgange til konfiguration af applikationer:

  1. bruge kommandolinjeparametre;
  2. bruge miljøvariabler.

Kommandolinjeparametre giver en mere fleksibel måde at indlæse indstillinger på med typevalidering. I Python's standardbibliotek findes der en modul argparser, som vi vil bruge. Detaljer og eksempler på dens muligheder er tilgængelige i den officielle dokumentation.

Her er hvordan en eksempelkonfiguration til indlæsning af kommandolinjeflag vil se ud i vores tilfælde:

   parser = ArgumentParser(
        description='Copyrator - kopieringsoperatør.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Operatørens namespace'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD navn'
    )
    args = parser.parse_args()

På den anden side kan vi ved hjælp af miljøvariabler i Kubernetes nemt overføre metadata om pod'en ind i containeren. For eksempel kan vi hente oplysninger om det namespace, hvori pod'en kører, med følgende konstruktion:

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

Operatorens logik

For at forstå, hvordan man adskiller metoder til at arbejde med ConfigMap og Secret, vil vi bruge særlige kort. Så kan vi forstå, hvilke metoder der er nødvendige for overvågning og oprettelse af 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',
}

Derefter skal vi modtage hændelser fra API-serveren. Vi implementerer dette på følgende måde:

def handle(specs):
    kubernetes.config.load_incluster_config()
    v1 = kubernetes.client.CoreV1Api()

    # Få metoden til at overvåge objekterne
    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 hændelsen går vi videre til den vigtigste logik for dens behandling:

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

Den grundlæggende logik er klar! Nu skal vi pakke alt dette i en Python-pakke. Vi opretter filen setup.py, skriver der metaoplysninger om projektet:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Ustøttet 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='maksim.nabokikh@flant.com',
    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. Yderligere oplysninger om versionkompatibilitet mellem klienten og Kubernetes-versioner kan findes i kompatibilitetsmatrixen.

Lige nu ser vores projekt sådan her ud:

copyrator
├── copyrator
│   ├── cli.py # Logik til arbejde med kommandolinjen
│   ├── constant.py # Konstantværdier, som vi nævnte tidligere
│   ├── load_crd.py # Logik til indlæsning af CRD
│   └── operator.py # Den grundlæggende logik for operatoren
└── setup.py # Oprettelse af pakken

Docker og Helm

Dockerfile vil være latterligt simpel: vi tager den grundlæggende python-alpine-billede og installerer vores pakke. Dens optimering udsættes til senere:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Implementeringen 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 er det nødvendigt at oprette en passende rolle til 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 }}

Resultat

På denne måde, uden frygt, bebrejdelse eller at lære Go, har vi været i stand til at samle vores egen operatør til Kubernetes i Python. Naturligvis er der endnu plads til forbedring: i fremtiden vil den kunne håndtere flere regler, arbejde med flere tråde, selv overvåge ændringer i sine CRD'er...

For at du kan lære koden bedre at kende, har vi samlet den i et offentligt repository. Hvis du er interesseret i eksempler på mere seriøse operatører, der er realiseret med Python, kan du se nærmere på to operatører til implementering af mongodb (den første og den anden).

P.S. Hvis du ikke gider at lære om Kubernetes-hændelser, eller hvis du simpelthen foretrækker at bruge Bash – vores kolleger har forberedt en klar løsning i form af shell-operator (vi annoncerede den i april).

P.P.S.

Læs også på vores blog:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster