Kubernetes Operator sa Python na walang mga framework at SDK

Kubernetes Operator sa Python na walang mga framework at SDK

Kasalukuyang may monopolyo si Go sa mga programming language na pinipili ng mga tao na magsulat ng mga pahayag para sa Kubernetes. May mga layuning dahilan para dito, tulad ng:

  1. Mayroong malakas na balangkas para sa pagbuo ng mga operator sa Go - Operator SDK.
  2. Ang mga application na nagbabago ng laro tulad ng Docker at Kubernetes ay nakasulat sa Go. Ang pagsusulat ng iyong operator sa Go ay nangangahulugan ng pagsasalita ng parehong wika sa ecosystem.
  3. Mataas na pagganap ng mga Go application at simpleng tool para sa pagtatrabaho nang may concurrency out of the box.

NB: By the way, paano magsulat ng sarili mong statement sa Go, kami inilarawan na sa isa sa aming mga pagsasalin ng mga dayuhang may-akda.

Ngunit paano kung mapipigilan kang matuto ng Go dahil sa kakulangan ng oras o, sa madaling salita, motibasyon? Nagbibigay ang artikulo ng isang halimbawa kung paano ka makakasulat ng isang mahusay na pahayag gamit ang isa sa mga pinakasikat na wika na alam ng halos bawat engineer ng DevOps - Sawa.

Kilalanin: Copier - copy operator!

Bilang isang halimbawa, isaalang-alang ang pagbuo ng isang simpleng pahayag na idinisenyo upang kopyahin ang isang ConfigMap kapag lumitaw ang isang bagong namespace o kapag nagbago ang isa sa dalawang entity: ConfigMap at Secret. Mula sa praktikal na pananaw, maaaring maging kapaki-pakinabang ang operator para sa maramihang pag-update ng mga configuration ng application (sa pamamagitan ng pag-update ng ConfigMap) o para sa pag-update ng lihim na data - halimbawa, mga susi para sa pagtatrabaho sa Docker Registry (kapag nagdaragdag ng Secret sa namespace).

Kaya, kung ano ang dapat magkaroon ng isang mahusay na operator:

  1. Ang pakikipag-ugnayan sa operator ay isinasagawa gamit ang Mga Kahulugan ng Custom na Resource (mula rito ay tinutukoy bilang CRD).
  2. Maaaring i-configure ang operator. Para magawa ito, gagamitin namin ang mga flag ng command line at mga variable ng kapaligiran.
  3. Ang build ng Docker container at Helm chart ay idinisenyo para madaling ma-install ng mga user (literal na may isang command) ang operator sa kanilang Kubernetes cluster.

CRD

Upang malaman ng operator kung anong mga mapagkukunan ang hahanapin at kung saan hahanapin, kailangan nating magtakda ng panuntunan para sa kanya. Ang bawat panuntunan ay kakatawanin bilang isang CRD object. Anong mga field ang dapat magkaroon ng CRD na ito?

  1. Uri ng mapagkukunan, na hahanapin natin (ConfigMap o Secret).
  2. Listahan ng mga namespace, kung saan dapat matatagpuan ang mga mapagkukunan.
  3. Ang pumipili, kung saan maghahanap kami ng mga mapagkukunan sa namespace.

Ilarawan natin ang 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

At gagawin namin ito kaagad simpleng panuntunan β€” upang maghanap sa namespace na may pangalan default lahat ng ConfigMap na may mga label tulad ng copyrator: "true":

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

handa na! Ngayon kailangan naming makakuha ng impormasyon tungkol sa aming panuntunan. Hayaan akong magpareserba kaagad na hindi kami mismo magsusulat ng mga kahilingan sa cluster API Server. Upang gawin ito, gagamit kami ng isang handa na library ng Python kubernetes-kliyente:

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

Bilang resulta ng pagpapatakbo ng code na ito, nakukuha namin ang sumusunod:

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

Mahusay: nakakuha kami ng panuntunan para sa operator. At higit sa lahat, ginawa namin ang tinatawag na Kubernetes na paraan.

Mga variable ng kapaligiran o mga flag? Kinukuha namin ang lahat!

Lumipat tayo sa pangunahing pagsasaayos ng operator. Mayroong dalawang pangunahing diskarte sa pag-configure ng mga application:

  1. gumamit ng mga pagpipilian sa command line;
  2. gumamit ng mga variable ng kapaligiran.

Nagbibigay-daan sa iyo ang mga opsyon sa command line na magbasa ng mga setting nang mas flexible, na may suporta sa uri ng data at pagpapatunay. Ang karaniwang library ng Python ay may isang module argparser, na aming gagamitin. Ang mga detalye at mga halimbawa ng mga kakayahan nito ay makukuha sa opisyal na dokumentasyon.

Para sa aming kaso, ito ang magiging hitsura ng isang halimbawa ng pag-set up ng pagbabasa ng mga flag ng command line:

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

Sa kabilang banda, gamit ang mga variable ng kapaligiran sa Kubernetes, madali mong mailipat ang impormasyon ng serbisyo tungkol sa pod sa loob ng container. Halimbawa, makakakuha tayo ng impormasyon tungkol sa namespace kung saan tumatakbo ang pod na may sumusunod na construction:

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

Logic ng operator

Upang maunawaan kung paano paghiwalayin ang mga pamamaraan para sa pagtatrabaho sa ConfigMap at Secret, gagamit kami ng mga espesyal na mapa. Pagkatapos ay mauunawaan natin kung anong mga pamamaraan ang kailangan natin upang subaybayan at likhain ang bagay:

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

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

Susunod, kailangan mong makatanggap ng mga kaganapan mula sa API server. Ipatupad natin ito tulad ng sumusunod:

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)

Pagkatapos matanggap ang kaganapan, lumipat kami sa pangunahing lohika ng pagproseso nito:

# Π’ΠΈΠΏΡ‹ событий, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π΅Π°Π³ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ
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)

Ang pangunahing lohika ay handa na! Ngayon kailangan nating i-package ang lahat ng ito sa isang pakete ng Python. Inihahanda namin ang file setup.py, isulat ang meta impormasyon tungkol sa proyekto doon:

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: Ang kubernetes client para sa Python ay may sariling bersyon. Higit pang impormasyon tungkol sa pagiging tugma sa pagitan ng mga bersyon ng kliyente at mga bersyon ng Kubernetes ay matatagpuan sa compatibility matrices.

Ngayon ang aming proyekto ay ganito ang hitsura:

copyrator
β”œβ”€β”€ copyrator
β”‚   β”œβ”€β”€ cli.py # Π›ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строкой
β”‚   β”œβ”€β”€ constant.py # ΠšΠΎΠ½ΡΡ‚Π°Π½Ρ‚Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅
β”‚   β”œβ”€β”€ load_crd.py # Π›ΠΎΠ³ΠΈΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ CRD
β”‚   └── operator.py # Основная Π»ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Π°
└── setup.py # ΠžΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚Π°

Docker at Helm

Ang Dockerfile ay magiging napakasimple: kunin ang batayang python-alpine na imahe at i-install ang aming package. Ipagpaliban natin ang pag-optimize nito hanggang sa mas magandang panahon:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Ang pag-deploy para sa operator ay napaka-simple din:

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

Sa wakas, kailangan mong lumikha ng angkop na tungkulin para sa operator na may mga kinakailangang karapatan:

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

Kabuuan

Ganyan, nang walang takot, paninisi, o pag-aaral ng Go, nakagawa kami ng sarili naming operator para sa Kubernetes sa Python. Siyempre, mayroon pa itong puwang para lumago: sa hinaharap ay makakapagproseso ito ng maraming panuntunan, makakapagtrabaho sa maraming thread, malayang subaybayan ang mga pagbabago sa mga CRD nito...

Upang mabigyan ka ng mas malapitang pagtingin sa code, inilagay namin ito pampublikong imbakan. Kung gusto mo ng mga halimbawa ng mas seryosong mga operator na ipinatupad gamit ang Python, maaari mong ibaling ang iyong pansin sa dalawang operator para sa pag-deploy ng mongodb (ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ ΠΈ pangalawa).

PS At kung tinatamad kang harapin ang mga kaganapan sa Kubernetes o mas sanay kang gumamit ng Bash, ang aming mga kasamahan ay naghanda ng isang handa na solusyon sa form shell-operator (Kami inihayag noong Abril).

Pps

Basahin din sa aming blog:

Pinagmulan: www.habr.com

Magdagdag ng komento