Kubernetes Operator op Python zonder frameworks en SDK

Kubernetes Operator op Python zonder frameworks en SDK

Go is currently the dominant language among those chosen for writing operators for Kubernetes. This is due to objective reasons such as:

  1. There is a powerful framework for developing operators in Go — Operator SDK.
  2. Game-changing applications like Docker and Kubernetes are written in Go. Writing your operator in Go means speaking the same language as the ecosystem.
  3. The high performance of applications in Go and simple tools for working with concurrency "out of the box."

NB: By the way, how to write your operator in Go has been previously described in one of our translations from foreign authors.

But what if your lack of time or simply motivation is hindering you from learning Go? The article provides an example of how to write a decent operator using one of the most popular languages known to nearly every DevOps engineer — Python.

Introducing: Copier — the copy operator!

For example, let’s consider the development of a simple operator designed for copying ConfigMap either when a new namespace appears or when one of the two entities — ConfigMap or Secret — changes. From a practical perspective, the operator can be useful for mass updating application configurations (by updating ConfigMap) or for updating secret data — such as keys for working with Docker Registry (when a Secret is added to the namespace).

So, what makes a good operator:

  1. Interaction with the operator is done via Custom Resource Definitions (hereinafter referred to as CRD).
  2. The operator can be configured. To do this, we will use command-line flags and environment variables.
  3. Building the Docker container and Helm chart is done in such a way that users can easily (literally with one command) install the operator in their Kubernetes cluster.

CRD

For the operator to know which resources to look for and where, we need to define a rule for it. Each rule will be represented as a single CRD object. What fields should this CRD have?

  1. Type bron, which we will be looking for (ConfigMap or Secret).
  2. List of namespaces, in which the resources should reside.
  3. Selector, by which we will search for resources in the namespace.

Let's describe the 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

En laten we direct een eenvoudige regel — voor het zoeken in de namespace met de naam default alle ConfigMaps met labels van het type copyrator: "true":

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

Klaar! Nu moeten we op de een of andere manier informatie over onze regel verkrijgen. Ik wil meteen vermelden dat we niet zelf verzoeken aan de API-server van het cluster gaan schrijven. Hiervoor zullen we gebruik maken van een beschikbare Python-bibliotheek 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')}

Het resultaat van deze code is als volgt:

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

Geweldig: we zijn erin geslaagd om de regel voor de operator te krijgen. En het belangrijkste — we hebben dit gedaan, zoals men zegt, op de Kubernetes-manier.

Omgevingsvariabelen of vlaggen? We nemen alles!

Laten we verder gaan met de basisconfiguratie van de operator. Er zijn twee basisbenaderingen voor het configureren van toepassingen:

  1. gebruik maken van opdrachtregelparameters;
  2. gebruik maken van omgevingsvariabelen.

Opdrachtregelparameters maken flexibele configuratie mogelijk, met ondersteuning en validatie van gegevenstypen. In de standaardbibliotheek van Python is er een module argparser, waarmee we dit zullen doen. Details en voorbeelden van de mogelijkheden zijn beschikbaar in de officiële documentatie.

Hier is hoe een voorbeeld van het instellen van opdrachtregelvlaggen eruit zou zien voor onze situatie:

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

Aan de andere kant kunnen we met omgevingsvariabelen in Kubernetes gemakkelijk de service-informatie van de pod naar binnen in de container verplaatsen. Bijvoorbeeld, de informatie over de namespace waarin de pod draait, kunnen we verkrijgen met de volgende constructie:

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

De logica van de operator

Om te begrijpen hoe we de methoden voor het werken met ConfigMap en Secret kunnen scheiden, maken we gebruik van speciale kaarten. Dan kunnen we begrijpen welke methoden we nodig hebben voor het monitoren en creëren van objecten:

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

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

Daarna moeten we gebeurtenissen van de API-server ontvangen. Dit implementeren we als volgt:

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

    # Verkrijg de methode voor het monitoren van objecten
    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)

Na het ontvangen van een gebeurtenis gaan we verder met de belangrijkste logica voor het verwerken ervan:

# Типы событий, на которые будем реагировать
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 belangrijkste logica is klaar! Nu moeten we dit alles in één Python-package verpakken. We maken het bestand aan setup.py, en we schrijven daar de metadata van het project in:

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: De Kubernetes-client voor Python heeft zijn eigen versiebeheer. Meer informatie over de compatibiliteit van clientversies en Kubernetes-versies is te vinden in de compatibiliteitsmatrix.

Momenteel ziet ons project er als volgt uit:

copyrator
├── copyrator
│   ├── cli.py # Logica voor commandoregelwerking
│   ├── constant.py # Constanten die we hierboven hebben genoemd
│   ├── load_crd.py # Logica voor het laden van CRD
│   └── operator.py # De belangrijkste logica van de operator
└── setup.py # Aankleding van het pakket

Docker en Helm

Dockerfile wordt ongelooflijk eenvoudig: we nemen een basisafbeelding van python-alpine en installeren ons pakket. We stellen de optimalisatie uit tot betere tijden:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

De implementatie voor de operator is ook heel eenvoudig:

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

Ten slotte is het nodig om een bijbehorende rol voor de operator te creëren met de vereiste rechten:

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

Conclusie

Zo, zonder angst, verwijten of kennis van Go, hebben we onze eigen operator voor Kubernetes opgezet met Python. Natuurlijk is er nog ruimte voor groei: in de toekomst zal hij in staat zijn om meerdere regels te verwerken, in meerdere threads te werken, zelf veranderingen in zijn CRD te monitoren...

Om de code beter te leren kennen, hebben we deze samengevoegd in een publieke repository. Als je geïnteresseerd bent in voorbeelden van meer geavanceerde operators die met Python zijn geïmplementeerd, kun je je aandacht richten op twee operators voor het uitrollen van mongodb (the first en the second).

P.S. En als je geen zin hebt om je in Kubernetes-events te verdiepen of gewoon liever Bash gebruikt - onze collega's hebben een kant-en-klare oplossing voorbereid in de vorm van shell-operator (we aangekondigd het in april).

P.P.S.

Lees ook op onze blog:

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster