Kubernetes Operator pÄ Python utan ramverk och SDK

Kubernetes Operator pÄ Python utan ramverk och SDK

Go Àr för nÀrvarande en monopollanguage bland programmeringssprÄk som mÀnniskor vÀljer för att skriva operatorer för Kubernetes. Det finns objektiva skÀl till detta, som:

  1. Det finns ett kraftfullt ramverk för att utveckla operatorer i Go — Operator SDK.
  2. Applikationer som har "förÀndrat spelreglerna" som Docker och Kubernetes Àr skrivna i Go. Att skriva sin egen operator i Go innebÀr att prata med ekosystemet pÄ ett gemensamt sprÄk.
  3. Hög prestanda för applikationer i Go och enkla verktyg för att hantera concurrency "direkt ur lÄdan".

NB: Förresten har vi redan beskrivit hur man skriver sin egen operator i Go i en av vĂ„ra översĂ€ttningar av utlĂ€ndska författare. Men vad gör man om brist pĂ„ tid eller helt enkelt brist pĂ„ motivation hindrar dig frĂ„n att studera Go? Artikeln ger ett exempel pĂ„ hur man kan skriva en solid operator med ett av de mest populĂ€ra sprĂ„ken som nĂ€stan varje DevOps-ingenjör kĂ€nner till —

Möt: Kopiatorn — kopieringsoperatorn! Python.

För vĂ„rt exempel kommer vi att titta pĂ„ utvecklingen av en enkel operator som Ă€r avsedd för att kopiera ConfigMap antingen vid skapandet av ett nytt namespace eller vid Ă€ndringar i en av tvĂ„ entiteter: ConfigMap och Secret. Ur ett praktiskt perspektiv kan operatorn vara anvĂ€ndbar för massiv uppdatering av applikationskonfigurationer (genom att uppdatera ConfigMap) eller för att uppdatera kĂ€nsliga data — till exempel nycklar för att arbeta med Docker Registry (vid tillĂ€gg av ett Secret i namespace).

vad som bör finnas hos en bra operator

SĂ„, Interaktion med operatorn sker genom:

  1. (hĂ€danefter — CRD). ) för sjĂ€lva Operatorn kan konfigureras. För detta kommer vi att anvĂ€nda kommandoradsflaggor och miljövariabler.
  2. Byggandet av Docker-container och Helm-diagram Àr utformat sÄ att anvÀndarna enkelt kan (bokstavligen med ett kommando) installera operatorn i sitt Kubernetes-kluster.
  3. CRD

För att operatorn ska veta vilka resurser och var den ska söka, behöver vi definiera en regel för den. Varje regel kommer att representeras som ett enda CRD-objekt. Vilka fÀlt bör detta CRD ha?

, som vi kommer att söka efter (ConfigMap eller Secret).

  1. ResurstyperListan över namespace
  2. , dÀr resurserna ska finnas.VÀljare
  3. , som vi kommer att söka efter resurser i namespace.Vi beskriver CRD:

ĐžĐżĐžŃˆĐ”ĐŒ 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 ska genast skapa en enkel regel — för att söka i namnomrĂ„det med namnet default alla ConfigMap med etiketter av typen copyrator: "true":

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

Klart! Nu mÄste vi pÄ nÄgot sÀtt fÄ information om vÄr regel. Jag vill genast pÄpeka att vi inte kommer att skriva förfrÄgningar till API-servern sjÀlva. Vi kommer att anvÀnda ett 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 ett resultat av denna kod fÄr vi följande:

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

Perfekt: vi lyckades fĂ„ regeln för operatören. Och det viktigaste — vi gjorde detta, sĂ„ att sĂ€ga, pĂ„ Kubernetes sĂ€tt.

Miljövariabler eller flaggor? Vi tar allt!

Vi gÄr vidare till huvudkonfigurationen för operatören. Det finns tvÄ grundlÀggande tillvÀgagÄngssÀtt för att konfigurera applikationer:

  1. anvÀnda kommandoradsparametrar;
  2. anvÀnda miljövariabler.

Kommandoradsparametrar gör det möjligt att lÀsa instÀllningar mer flexibelt, med stöd för och validering av datatyper. I Python-biblioteket finns en modul argparser, som vi kommer att anvÀnda. Detaljer och exempel pÄ dess möjligheter finns tillgÀngliga i den officiella dokumentationen.

SÄ hÀr ser ett exempel pÄ instÀllning av lÀsning av kommandoradsflaggor ut för vÄrt fall:

   parser = ArgumentParser(
        description='Copyrator - kopiera operatör.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Operatörs NamnomrÄde'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD Namn'
    )
    args = parser.parse_args()

Å andra sidan kan vi enkelt föra in tjĂ€nsteinformation om podden i containern med hjĂ€lp av miljövariabler i Kubernetes. Till exempel kan vi fĂ„ information om det namespace dĂ€r podden körs med följande konstruktion:

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

Operatorns arbetslogik

För att förstÄ hur vi delar upp metoderna för att arbeta med ConfigMap och Secret, anvÀnder vi speciella kartor. DÄ kan vi förstÄ vilka metoder vi behöver för att övervaka och skapa objekt:

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 vi fÄ hÀndelser frÄn API-servern. Vi implementerar detta pÄ följande sÀtt:

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

    # HÀmta metoden för övervakning av objekt
    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 mottagit hÀndelsen gÄr vi vidare till den huvudsakliga logiken 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)

Den huvudsakliga logiken Àr klar! Nu behöver vi packa allt detta i ett enda Python-paket. Vi upprÀttar filen setup.py, skriver in metadata om projektet:

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='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: Klienten kubernetes för Python har sin egen versionering. Mer om kompatibiliteten mellan klientversioner och Kubernetes-versioner kan du lÀsa i kompatibilitetsmatrisen.

Just nu ser vÄrt projekt ut sÄ hÀr:

copyrator
├── copyrator
│   ├── cli.py # Logik för arbete med kommandoraden
│   ├── constant.py # Konstanter som vi nĂ€mnde tidigare
│   ├── load_crd.py # Logik för inlĂ€sning av CRD
│   └── operator.py # Huvudlogik för operatorn
└── setup.py # Utformning av paketet

Docker och Helm

Dockerfilen kommer att bli extremt enkel: vi tar basbilden python-alpine och installerar vÄrt paket. Dess optimering fÄr vÀnta tills senare:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Distribution 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 vi skapa en motsvarande 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 }}

Resultat

PÄ sÄ sÀtt, utan rÀdsla eller fördömelse och utan att studera Go, har vi lyckats skapa vÄr egen operatör för Kubernetes i Python. SjÀlvklart finns det mycket mer att utveckla: i framtiden kan den hantera flera regler, arbeta i flera trÄdar, och sjÀlvstÀndigt övervaka Àndringar i sina CRD...

För att du ska kunna lÀra kÀnna koden bÀttre har vi lagt den i ett offentligt arkiv. Om du Àr intresserad av exempel pÄ mer seriösa operatörer som har implementerats med Python kan du titta pÄ tvÄ operatörer för distribution av mongodb (första och andra).

P.S. Och om du inte orkar förstĂ„ dig pĂ„ Kubernetes-hĂ€ndelser eller föredrar att anvĂ€nda Bash – vĂ„ra kollegor har förberett en fĂ€rdig lösning i form av shell-operator (vi tillkĂ€nnagav gjorde det i april).

P.P.S.

LÀs Àven i vÄr blogg:

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster