Kubernetes Operator en Python sans frameworks ni SDK

Kubernetes Operator en Python sans frameworks ni SDK

Go est actuellement le langage de programmation dominant pour écrire des opérateurs pour Kubernetes. Cela s'explique par des raisons objectives telles que :

  1. Il existe un puissant cadre pour le développement d'opérateurs en Go — Operator SDK.
  2. Des applications « révolutionnaires » comme Docker et Kubernetes sont écrites en Go. Écrire votre propre opérateur en Go signifie parler le même langage que l'écosystème.
  3. La haute performance des applications en Go et les outils simples pour travailler avec la concurrence « par défaut ».

NB: D'ailleurs, comment écrire votre propre opérateur en Go, nous l'avons déjà décrit dans l'une de nos traductions d'auteurs étrangers.

Mais que faire si le manque de temps ou, tout simplement, le manque de motivation vous empêche d'apprendre Go ? Cet article propose un exemple de comment écrire un bon opérateur en utilisant l'un des langages les plus populaires, que presque tous les ingénieurs DevOps connaissent — Python.

Voici : Copirator — l'opérateur de copie !

Prenons l'exemple du développement d'un simple opérateur destiné à copier un ConfigMap soit lors de l'apparition d'un nouveau namespace, soit lors de la modification de l'une des deux entités : ConfigMap et Secret. D'un point de vue pratique, l'opérateur peut être utile pour mettre à jour massivement les configurations d'application (en mettant à jour le ConfigMap) ou pour mettre à jour des données secrètes — par exemple, des clés pour travailler avec Docker Registry (lorsque l'on ajoute un Secret dans un namespace).

Alors, quelles sont les caractéristiques d'un bon opérateur:

  1. L'interaction avec l'opérateur se fait par le biais de Custom Resource Definitions (ci-après — CRD).
  2. L'opérateur peut être configuré. Pour cela, nous utiliserons des drapeaux de ligne de commande et des variables d'environnement.
  3. La construction du conteneur Docker et du diagramme Helm sera conçue de manière à ce 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 quels ressources et où les chercher, nous devons lui définir une règle. Chaque règle sera représentée sous forme d'un objet CRD. Quels champs ce CRD doit-il avoir ?

  1. Le type de ressource, que nous allons chercher (ConfigMap ou Secret).
  2. La liste des namespaces, dans lesquels les ressources doivent se trouver.
  3. Sélecteur, par lequel nous allons rechercher des ressources dans le namespace.

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 créons immédiatement une règle simple — pour rechercher dans le namespace nommé default tous les ConfigMap avec des labels de type copyrator: "true":

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

C'est fait ! Maintenant, nous devons trouver un moyen d'obtenir des informations sur notre règle. Je précise tout de suite que nous n'allons pas écrire manuellement des requêtes à l'API Server du cluster. Pour cela, nous allons utiliser une bibliothèque Python prête à l'emploi. 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')}

En résultat de l'exécution de ce code, nous obtiendrons ce qui suit :

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

Super : nous avons réussi à obtenir la règle pour l'opérateur. Et surtout, nous l'avons fait, comme on dit, à la manière de Kubernetes.

Variables d'environnement ou drapeaux ? Nous prenons tout !

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

  1. utiliser des paramètres de ligne de commande ;
  2. utiliser des variables d'environnement.

Les paramètres de ligne de commande permettent de lire les réglages de manière plus flexible, avec support et validation des types de données. La bibliothèque standard de Python comprend un module argparser, que nous allons utiliser. Les détails et des exemples de ses capacités sont disponibles dans la documentation officielle.

Voici à quoi ressemblerait notre exemple de réglage de lecture des drapeaux de ligne de commande :

   parser = ArgumentParser(
        description='Copyrator - opérateur de copie.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Namespace de l'opérateur'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='Nom du CRD'
    )
    args = parser.parse_args()

D'un autre côté, grâce aux variables d'environnement dans Kubernetes, nous pouvons facilement transférer des informations de service sur un pod à l'intérieur du conteneur. Par exemple, nous pouvons obtenir des informations sur l'espace de noms dans lequel le pod est exécuté avec la syntaxe suivante :

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

La logique de fonctionnement de l'opérateur

Pour comprendre comment séparer les méthodes de travail avec ConfigMap et Secret, utilisons des cartes spéciales. Nous pourrons ainsi déterminer quelles méthodes sont nécessaires pour suivre et créer des objets :

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, il est nécessaire de recevoir des événements du serveur API. Nous allons le réaliser comme suit :

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

    # Obtenir la méthode de suivi des objets
    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 un événement, nous passons à 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 ! Maintenant, nous devons encapsuler tout cela dans un package Python. Nous préparons le fichier setup.py, y rédigeons les métadonnées du projet :

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Version de python non prise en charge %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: Le client kubernetes pour Python a sa propre versioning. Plus d'informations sur la compatibilité des versions du client et des versions de Kubernetes peuvent être trouvées dans la matrice de compatibilités.

Actuellement, notre projet se présente comme suit :

copyrator
├── copyrator
│   ├── cli.py # Logique de travail avec la ligne de commande
│   ├── constant.py # Constantes que nous avons présentées précédemment
│   ├── load_crd.py # Logique de chargement du CRD
│   └── operator.py # Logique principale de l'opérateur
└── setup.py # Préparation du package

Docker et Helm

Le Dockerfile sera incroyablement simple : prenons l'image de base python-alpine et installons notre package. Nous reporterons son optimisation à plus tard :

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, il est nécessaire de créer le rôle correspondant 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 }}

Conclusion

Ainsi, sans peur ni reproche et sans avoir besoin d'apprendre Go, nous avons pu créer notre propre opérateur pour Kubernetes en Python. Bien sûr, il a encore de la marge de progression : à l'avenir, il pourra gérer plusieurs règles, fonctionner en plusieurs threads, et surveiller automatiquement les changements de ses CRD…

Pour vous permettre de mieux découvrir le code, nous l'avons mis dans un dépôt public. Si vous recherchez des exemples d'opérateurs plus avancés réalisés avec Python, vous pouvez jeter un œil à deux opérateurs pour le déploiement de mongodb (le premier et le second).

P.S. Si vous n'avez pas envie de vous occuper des événements Kubernetes ou si vous préférez simplement utiliser Bash, nos collègues ont préparé une solution prête à l'emploi sous la forme de shell-operator (nous avons annoncé cela en avril).

P.P.S.

Lisez aussi dans notre blog :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster