Kubernetes-operaattori Pythonissa ilman kehyksiä ja SDK:ta

Kubernetes-operaattori Pythonissa ilman kehyksiä ja SDK:ta

Golla on tällä hetkellä monopoli ohjelmointikielissä, joita ihmiset valitsevat kirjoittaa lausuntoja Kubernetesille. Tähän on objektiivisia syitä, kuten:

  1. Gossa on tehokas kehys operaattoreiden kehittämiseen - Operaattorin SDK.
  2. Peliä muuttavat sovellukset, kuten Docker ja Kubernetes, on kirjoitettu Golla. Operaattorin kirjoittaminen Go-kieleen tarkoittaa, että puhut samaa kieltä ekosysteemin kanssa.
  3. Go-sovellusten korkea suorituskyky ja yksinkertaiset työkalut samanaikaiseen työskentelyyn heti valmiina.

NB: Muuten, kuinka kirjoittaa oma lausunto Go, we -kirjassa jo kuvattu yhdessä ulkomaisten kirjailijoiden käännöksistämme.

Mutta entä jos ajan tai yksinkertaisesti sanottuna motivaation puute estää sinua oppimasta Go? Artikkelissa on esimerkki siitä, kuinka voit kirjoittaa hyvän lausunnon yhdellä suosituimmista kielistä, jonka melkein jokainen DevOps-insinööri osaa - Python.

Tapaa: Kopiokone - kopiooperaattori!

Harkitse esimerkiksi yksinkertaisen käskyn kehittämistä, joka on suunniteltu kopioimaan ConfigMap joko uuden nimitilan ilmestyessä tai kun toinen kahdesta entiteetistä muuttuu: ConfigMap ja Secret. Käytännön näkökulmasta operaattori voi olla hyödyllinen sovelluskokoonpanojen joukkopäivityksessä (päivittämällä ConfigMap) tai päivittäessä salaisia ​​tietoja - esimerkiksi avaimia Docker-rekisterin kanssa työskentelyä varten (kun lisätään Secret nimiavaruuteen).

Niin, mitä hyvällä operaattorilla pitäisi olla:

  1. Vuorovaikutus operaattorin kanssa tapahtuu käyttämällä Mukautetut resurssien määritelmät (jäljempänä CRD).
  2. Operaattori voidaan konfiguroida. Tätä varten käytämme komentorivin lippuja ja ympäristömuuttujia.
  3. Docker-säiliön ja Helm-kaavion rakenne on suunniteltu siten, että käyttäjät voivat helposti (kirjaimellisesti yhdellä komennolla) asentaa operaattorin Kubernetes-klusteriinsa.

CRD

Jotta operaattori tietää, mitä resursseja etsiä ja mistä etsiä, meidän on asetettava hänelle sääntö. Jokainen sääntö esitetään yhtenä CRD-objektina. Mitä kenttiä tässä CRD:ssä pitäisi olla?

  1. Resurssin tyyppi, jota etsimme (ConfigMap tai Secret).
  2. Luettelo nimiavaruuksista, johon resurssit tulisi sijoittaa.
  3. Valitsin, jolla etsimme resursseja nimiavaruudesta.

Kuvataanpa CRD:tä:

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

Ja luomme sen heti yksinkertainen sääntö — Voit etsiä nimiavaruudesta nimellä default kaikki ConfigMap-tunnisteet kuten copyrator: "true":

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

Valmis! Nyt meidän täytyy jotenkin saada tietoa säännöstämme. Sallikaa minun tehdä varaus heti, ettemme kirjoita pyyntöjä klusterin API-palvelimelle itse. Tätä varten käytämme valmista Python-kirjastoa kubernetes-asiakas:

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

Tämän koodin suorittamisen seurauksena saamme seuraavan:

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

Hienoa: onnistuimme saamaan säännön operaattorille. Ja mikä tärkeintä, teimme niin sanotun Kubernetes-tavan.

Ympäristömuuttujat vai liput? Otamme kaiken!

Siirrytään pääkäyttäjän kokoonpanoon. Sovellusten määrittämiseen on kaksi perustapaa:

  1. käytä komentorivin valintoja;
  2. käyttää ympäristömuuttujia.

Komentorivivalintojen avulla voit lukea asetuksia joustavammin tietotyyppituen ja validoinnin avulla. Pythonin vakiokirjastossa on moduuli argparser, jota käytämme. Yksityiskohdat ja esimerkit sen ominaisuuksista ovat saatavilla osoitteessa virallinen dokumentaatio.

Meidän tapauksessamme esimerkki komentorivin lukemisen määrittämisestä näyttäisi tältä:

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

Toisaalta Kubernetesin ympäristömuuttujien avulla voit helposti siirtää palvelutietoja podista kontin sisällä. Voimme esimerkiksi saada tietoa nimiavaruudesta, jossa pod toimii seuraavalla rakenteella:

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

Operaattorin logiikka

Ymmärtääksemme kuinka erottaa menetelmät ConfigMapin ja Secretin kanssa työskentelyssä, käytämme erityisiä karttoja. Sitten voimme ymmärtää, mitä menetelmiä tarvitsemme objektin seurantaan ja luomiseen:

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

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

Seuraavaksi sinun on vastaanotettava tapahtumat API-palvelimelta. Toteutetaan se seuraavasti:

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)

Tapahtuman saatuaan siirrymme sen käsittelyn päälogiikkaan:

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

Päälogiikka on valmis! Nyt meidän on pakattava kaikki tämä yhteen Python-pakettiin. Valmistelemme tiedoston setup.py, kirjoita sinne metatiedot projektista:

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: Pythonin kubernetes-asiakkaalla on oma versiointinsa. Lisätietoja asiakasversioiden ja Kubernetes-versioiden yhteensopivuudesta löytyy osoitteesta yhteensopivuusmatriisit.

Nyt projektimme näyttää tältä:

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

Docker ja Helm

Dockerfile on uskomattoman yksinkertainen: ota python-alpine-peruskuva ja asenna pakettimme. Siirretään sen optimointia parempiin aikoihin:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Käyttöönotto operaattorille on myös hyvin yksinkertaista:

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

Lopuksi sinun on luotava operaattorille sopiva rooli, jolla on tarvittavat oikeudet:

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

Koko

Näin ilman pelkoa, moitteita tai Go:n oppimista pystyimme rakentamaan oman operaattorimme Kubernetesille Pythonissa. Tietysti sillä on vielä tilaa kasvaa: tulevaisuudessa se pystyy käsittelemään useita sääntöjä, työskentelemään useissa säikeissä, seuraamaan itsenäisesti muutoksia CRD-dokumenteissaan...

Jotta voisit tarkastella koodia tarkemmin, olemme lisänneet sen julkinen arkisto. Jos haluat esimerkkejä vakavammista Pythonilla toteutetuista operaattoreista, voit kääntää huomiosi kahteen operaattoriin mongodb (первый и toinen).

PS Ja jos olet liian laiska käsittelemään Kubernetes-tapahtumia tai olet yksinkertaisesti tottunut käyttämään Bashia, kollegamme ovat laatineet valmiin ratkaisun muodossa kuorioperaattori (Me ilmoitti se huhtikuussa).

PPS

Lue myös blogistamme:

Lähde: will.com

Lisää kommentti