Kubernetes Operator yn Python sûnder kaders en SDK

Kubernetes Operator yn Python sûnder kaders en SDK

Go hat op it stuit in monopoalje op 'e programmeartalen dy't minsken kieze om ferklearrings foar Kubernetes te skriuwen. Dêr binne objektive redenen foar, lykas:

  1. D'r is in krêftich ramt foar it ûntwikkeljen fan operators yn Go - Operator SDK.
  2. Game-feroarjende applikaasjes lykas Docker en Kubernetes binne skreaun yn Go. It skriuwen fan jo operator yn Go betsjut deselde taal prate mei it ekosysteem.
  3. Hege prestaasjes fan Go-applikaasjes en ienfâldige ark foar it wurkjen mei tagelyk út 'e doaze.

NB: Trouwens, hoe skriuwe jo eigen ferklearring yn Go, wy al beskreaun yn ien fan ús oersettingen fan bûtenlânske skriuwers.

Mar wat as jo foarkomme om Go te learen troch gebrek oan tiid of, gewoanwei, motivaasje? It artikel jout in foarbyld fan hoe't jo in goede ferklearring kinne skriuwe mei ien fan 'e populêrste talen dy't hast elke DevOps-yngenieur wit - Python.

Meet: Copier - kopyoperator!

As foarbyld, beskôgje it ûntwikkeljen fan in ienfâldige ferklearring ûntworpen om in ConfigMap te kopiearjen as in nije nammeromte ferskynt of as ien fan twa entiteiten feroaret: ConfigMap en Secret. Ut in praktysk eachpunt kin de operator nuttich wêze foar bulk bywurkjen fan applikaasjekonfiguraasjes (troch it bywurkjen fan de ConfigMap) of foar it bywurkjen fan geheime gegevens - bygelyks kaaien foar wurkjen mei de Docker Registry (by it tafoegjen fan Geheim oan 'e nammeromte).

En sa, wat in goede operator moat hawwe:

  1. Ynteraksje mei de operator wurdt útfierd mei help fan Oanpaste Resource Definitions (hjirnei oantsjutten as CRD).
  2. De operator kin ynsteld wurde. Om dit te dwaan, sille wy kommandorigelflaggen en omjouwingsfariabelen brûke.
  3. De bou fan 'e Docker-kontener en Helm-kaart is ûntwurpen sadat brûkers de operator maklik (letterlik mei ien kommando) kinne ynstallearje yn har Kubernetes-kluster.

CRD

Om de operator te witten hokker middels om te sykjen en wêr te sykjen, moatte wy in regel foar him ynstelle. Elke regel sil wurde fertsjintwurdige as ien CRD-objekt. Hokker fjilden moat dizze CRD hawwe?

  1. Soart boarne, dêr't wy nei sille sykje (ConfigMap of Secret).
  2. List fan nammeromten, dêr't de middels yn sitte moatte.
  3. Selektor, wêrmei wy sille sykje nei boarnen yn de nammeromte.

Litte wy de CRD beskriuwe:

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

En wy meitsje it direkt ienfâldige regel - om te sykjen yn de nammeromte mei de namme default allegear ConfigMap mei labels lykas copyrator: "true":

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

Klear! No moatte wy op ien of oare manier ynformaasje krije oer ús regel. Lit my direkt in reservearje meitsje dat wy sels gjin fersiken skriuwe nei de kluster API-tsjinner. Om dit te dwaan, sille wy in klearmakke Python-bibleteek brûke 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')}

As gefolch fan it útfieren fan dizze koade krije wy it folgjende:

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

Geweldich: it is ús slagge om in regel te krijen foar de operator. En it wichtichste, wy diene wat de Kubernetes-manier hjit.

Miljeu fariabelen of flaggen? Wy nimme alles!

Litte wy trochgean nei de konfiguraasje fan 'e haadoperator. D'r binne twa basisbenaderingen foar it konfigurearjen fan applikaasjes:

  1. gebrûk kommandorigelopsjes;
  2. brûke omjouwingsfariabelen.

Opsjes foar kommandorigel kinne jo ynstellings fleksibeler lêze, mei stipe en falidaasje fan gegevenstype. De standertbibleteek fan Python hat in module argparser, dy't wy sille brûke. Details en foarbylden fan syn mooglikheden binne beskikber yn offisjele dokumintaasje.

Foar ús gefal, dit is wat in foarbyld fan it ynstellen fan it lêzen fan kommandorigelflaggen der útsjen soe:

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

Oan 'e oare kant, mei help fan omjouwingsfariabelen yn Kubernetes, kinne jo maklik tsjinstynformaasje oerbringe oer de pod yn' e kontener. Wy kinne bygelyks ynformaasje krije oer de nammeromte dêr't de pod yn rint mei de folgjende konstruksje:

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

Operator logika

Om te begripen hoe't jo metoaden skiede kinne foar wurkjen mei ConfigMap en Secret, sille wy spesjale kaarten brûke. Dan kinne wy ​​begripe hokker metoaden wy moatte folgje en meitsje it 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êrnei moatte jo eveneminten ûntfange fan 'e API-tsjinner. Litte wy it as folgjend útfiere:

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)

Nei ûntfangst fan it evenemint geane wy ​​troch nei de haadlogika fan it ferwurkjen:

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

De wichtichste logika is klear! No moatte wy dit alles yn ien Python-pakket ferpakke. Wy meitsje it bestân klear setup.py, skriuw dêr meta-ynformaasje oer it projekt:

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: De kubernetes-kliïnt foar Python hat in eigen ferzje. Mear ynformaasje oer kompatibiliteit tusken clientferzjes en Kubernetes-ferzjes is te finen yn komptabiliteit matriks.

No sjocht ús projekt der sa út:

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

Docker en Helm

De Dockerfile sil ongelooflijk ienfâldich wêze: nim de basis python-alpine-ôfbylding en ynstallearje ús pakket. Litte wy syn optimalisaasje útstelle oant bettere tiden:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Ynset foar de operator is ek hiel ienfâldich:

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

Uteinlik moatte jo in passende rol meitsje foar de operator mei de nedige rjochten:

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

It resultaat

Dat is hoe't wy, sûnder eangst, smaad of Go learen, ús eigen operator foar Kubernetes yn Python bouwe kinnen. Fansels hat it noch romte om te groeien: yn 'e takomst sil it meardere regels kinne ferwurkje, wurkje yn meardere diskusjes, ûnôfhinklik kontrolearje fan feroaringen yn syn CRD's ...

Om jo de koade fan tichterby te sjen, hawwe wy it ynset iepenbiere repository. As jo ​​​​foarbylden wolle fan mear serieuze operators ymplementearre mei Python, kinne jo jo oandacht rjochtsje op twa operators foar it ynsetten fan mongodb (первый и de twadde).

PS En as jo te lui binne om te gean mei Kubernetes-eveneminten of as jo gewoan mear wend binne om Bash te brûken, hawwe ús kollega's in klearmakke oplossing taret yn 'e foarm shell-operator (Wy oankundige it yn april).

PPS

Lês ek op ús blog:

Boarne: www.habr.com

Add a comment