Opérateur Kubernetes en Python sans frameworks ni SDK

Opérateur Kubernetes en Python sans frameworks ni SDK

Go a actuellement le monopole des langages de programmation que les gens choisissent pour écrire des déclarations pour Kubernetes. Il y a des raisons objectives à cela, telles que :

  1. Il existe un cadre puissant pour développer des opérateurs dans Go - SDK opérateur.
  2. Des applications révolutionnaires comme Docker et Kubernetes sont écrites en Go. Écrire son opérateur en Go, c’est parler le même langage avec l’écosystème.
  3. Hautes performances des applications Go et outils simples pour travailler avec la simultanéité prêts à l'emploi.

NB: Au fait, comment rédiger votre propre déclaration dans Go, nous déjà décrit dans l'une de nos traductions d'auteurs étrangers.

Mais que se passe-t-il si vous ne parvenez pas à apprendre le Go par manque de temps ou, tout simplement, de motivation ? L'article fournit un exemple de la façon dont vous pouvez rédiger une bonne déclaration en utilisant l'un des langages les plus populaires que presque tous les ingénieurs DevOps connaissent - Python.

Rencontre : Copieur - opérateur de copie !

À titre d'exemple, envisagez de développer une instruction simple conçue pour copier un ConfigMap soit lorsqu'un nouvel espace de noms apparaît, soit lorsqu'une des deux entités change : ConfigMap et Secret. D'un point de vue pratique, l'opérateur peut être utile pour la mise à jour groupée des configurations d'application (en mettant à jour le ConfigMap) ou pour la mise à jour des données secrètes - par exemple, des clés pour travailler avec le registre Docker (lors de l'ajout de Secret à l'espace de noms).

ainsi, ce qu'un bon opérateur devrait avoir:

  1. L'interaction avec l'opérateur s'effectue à l'aide Définitions de ressources personnalisées (ci-après dénommé CRD).
  2. L'opérateur peut être configuré. Pour ce faire, nous utiliserons des indicateurs de ligne de commande et des variables d'environnement.
  3. La construction du conteneur Docker et du graphique Helm est conçue pour que les utilisateurs puissent facilement (littéralement avec une seule commande) installer l'opérateur dans leur cluster Kubernetes.

CRD

Pour que l'opérateur sache quelles ressources rechercher et où chercher, nous devons lui fixer une règle. Chaque règle sera représentée comme un seul objet CRD. Quels champs ce CRD doit-il contenir ?

  1. Type de ressource, que nous rechercherons (ConfigMap ou Secret).
  2. Liste des espaces de noms, dans lequel les ressources doivent être situées.
  3. Sélecteur, par lequel nous rechercherons des ressources dans l'espace de noms.

Décrivons le 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

Et nous allons le créer tout de suite règle simple — pour rechercher dans l'espace de noms avec le nom default tous les ConfigMap avec des étiquettes comme copyrator: "true":

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

Prêt! Nous devons maintenant obtenir des informations sur notre règle. Permettez-moi de faire une réserve tout de suite : nous n'écrirons pas nous-mêmes de requêtes au serveur API du cluster. Pour ce faire, nous utiliserons une bibliothèque Python prête à l'emploi client kubernetes:

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

En exécutant ce code, nous obtenons ce qui suit :

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

Génial : nous avons réussi à obtenir une règle pour l’opérateur. Et surtout, nous avons fait ce qu’on appelle la méthode Kubernetes.

Variables d'environnement ou indicateurs ? On prend tout !

Passons à la configuration principale de l'opérateur. Il existe deux approches de base pour configurer les applications :

  1. utilisez les options de ligne de commande ;
  2. utiliser des variables d'environnement.

Les options de ligne de commande vous permettent de lire les paramètres de manière plus flexible, avec la prise en charge et la validation des types de données. La bibliothèque standard de Python a un module argparser, que nous utiliserons. Des détails et des exemples de ses capacités sont disponibles dans documents officiels.

Pour notre cas, voici à quoi ressemblerait un exemple de configuration de la lecture des indicateurs de ligne de commande :

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

D'un autre côté, en utilisant les variables d'environnement dans Kubernetes, vous pouvez facilement transférer des informations de service sur le pod à l'intérieur du conteneur. Par exemple, nous pouvons obtenir des informations sur l'espace de noms dans lequel le pod s'exécute avec la construction suivante :

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

Logique de l'opérateur

Pour comprendre comment séparer les méthodes de travail avec ConfigMap et Secret, nous utiliserons des cartes spéciales. Nous pouvons ensuite comprendre de quelles méthodes nous avons besoin pour suivre et créer l'objet :

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

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

Ensuite, vous devez recevoir les événements du serveur API. Implémentons-le comme suit :

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)

Après avoir reçu l'événement, on passe à la logique principale de son traitement :

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

La logique principale est prête ! Nous devons maintenant regrouper tout cela dans un seul package Python. Nous préparons le dossier setup.py, écrivez-y les méta-informations sur le projet :

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: Le client Kubernetes pour Python a son propre versioning. Plus d'informations sur la compatibilité entre les versions client et les versions Kubernetes peuvent être trouvées dans matrices de compatibilité.

Maintenant, notre projet ressemble à ceci :

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

Docker et barre

Le Dockerfile sera incroyablement simple : prenez l'image de base python-alpine et installez notre package. Reportons son optimisation à des temps meilleurs :

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Le déploiement pour l’opérateur est également très simple :

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

Enfin, vous devez créer un rôle approprié pour l'opérateur avec les droits nécessaires :

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

Total

C'est ainsi que, sans crainte, ni reproche, ni apprentissage de Go, nous avons pu construire notre propre opérateur pour Kubernetes en Python. Bien sûr, il a encore de la marge pour se développer : à l'avenir, il sera capable de traiter plusieurs règles, de travailler dans plusieurs threads, de surveiller de manière indépendante les modifications de ses CRD...

Pour vous donner un aperçu plus approfondi du code, nous l'avons mis dans référentiel public. Si vous souhaitez des exemples d'opérateurs plus sérieux implémentés à l'aide de Python, vous pouvez porter votre attention sur deux opérateurs de déploiement de mongodb (premier и deuxième).

PS Et si vous êtes trop paresseux pour gérer les événements Kubernetes ou si vous êtes simplement plus habitué à utiliser Bash, nos confrères vous ont préparé une solution toute faite sous la forme opérateur shell (Nous annoncé en avril).

PPS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire