Opérateur Kubernetes en Python sans frameworks et SDK



Go est actuellement un monopole parmi les langages de programmation que les gens choisissent d'écrire des instructions pour Kubernetes. Il existe des raisons objectives telles que:

  1. Il existe un cadre puissant pour développer des opérateurs sur Go - SDK opérateur .
  2. Go a écrit des applications à l'envers comme Docker et Kubernetes. Pour écrire votre propre opérateur dans Go - parlez la même langue avec l'écosystème.
  3. Des applications hautes performances sur Go et des outils simples pour travailler avec la concurrence dès la sortie de la boîte.

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

Mais que se passe-t-il si l'apprentissage du Go est empêché par le manque de temps ou, trivialement, la motivation? L'article fournit un exemple de la façon dont vous pouvez écrire un opérateur solide en utilisant l'un des langages les plus populaires que presque tous les ingénieurs DevOps connaissent - Python .

Rencontre: Copywriter - copieur!


Par exemple, envisagez le développement d'un opérateur simple conçu pour copier ConfigMap soit lorsqu'un nouvel espace de noms apparaît, soit lorsque l'une des deux entités change: ConfigMap et Secret. Du point de vue de l'application pratique, l'opérateur peut être utile pour la mise à jour en masse des configurations d'application (en mettant à jour ConfigMap) ou pour mettre à jour les données secrètes - par exemple, les clés pour travailler avec le Docker Registry (lors de l'ajout de Secret à l'espace de noms).

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

  1. L'interaction avec l'opérateur est effectuée à l'aide de définitions de ressources personnalisées (ci-après - CRD).
  2. L'opérateur peut être personnalisé. Pour ce faire, nous utiliserons des indicateurs de ligne de commande et des variables d'environnement.
  3. L'assemblage du conteneur Docker et du graphique Helm est en cours d'élaboration afin 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 et où le chercher, nous devons lui fixer une règle. Chaque règle sera représentée comme un seul objet CRD. Quels champs doit avoir ce CRD?

  1. Le type de ressource que nous recherchons (ConfigMap ou Secret).
  2. Une liste d'espace de noms où les ressources doivent être situées.
  3. Sélecteur , par lequel nous rechercherons des ressources dans l'espace de noms.

Nous 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éez immédiatement une 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 

C'est fait! Maintenant, vous devez en quelque sorte obtenir des informations sur notre règle. Je dois faire une réservation immédiatement pour que nous n'écrivions pas de demandes à l'API du serveur de cluster. Pour ce faire, nous utiliserons la 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')} 

Ă€ la suite de ce code, nous obtenons ce qui suit:

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

Excellent: nous avons réussi à obtenir une règle pour l'opérateur. Et surtout, nous avons fait ce qu'on appelle la voie Kubernetes.

Variables d'environnement ou indicateurs? Nous prenons tout!


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

  1. Utiliser 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 prise en charge et validation des types de données. La bibliothèque standard Python possède un module argparser , que nous utiliserons. Des détails et des exemples de ses capacités sont disponibles dans la documentation officielle .

Voici à quoi ressemblera l'exemple de définition des indicateurs de ligne de commande dans notre cas:

  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'autre part, en utilisant des variables d'environnement dans Kubernetes, vous pouvez facilement transférer des informations de service sur le pod vers le 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. Ensuite, nous pouvons 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 des événements du serveur API. Nous l'implémentons 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 réception de l'événement, nous procédons à 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 de base est prête! Vous devez maintenant regrouper tout cela dans un package Python. Nous setup.py , y écrivons des 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='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 son propre versioning. Vous pouvez en savoir plus sur la compatibilité entre les versions client et les versions de Kubernetes à partir de la matrice de compatibilité .

Maintenant, notre projet ressemble Ă  ceci:

 copyrator ├── copyrator │ ├── cli.py #      │ ├── constant.py # ,     │ ├── load_crd.py #   CRD │ └── operator.py #     └── setup.py #   

Docker et Helm


Le Dockerfile sera incroyablement simple: prenez l'image de base python-alpine et installez notre package. Nous reporterons 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 le 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 }}-acc 

Résumé


Ainsi, sans crainte, sans reproche et sans apprendre Go, nous avons pu monter notre propre opérateur pour Kubernetes en Python. Bien sûr, il a encore de la place pour grandir: à l'avenir, il pourra traiter plusieurs règles, travailler sur plusieurs threads, suivre indépendamment les évolutions de son CRD ...

Pour regarder de plus près le code, nous l'avons placé dans un référentiel public . Si vous voulez des exemples d'opérateurs plus sérieux implémentés en utilisant Python, vous pouvez tourner votre attention vers deux opérateurs pour déployer mongodb (le premier et le second ).

PS Et si vous êtes trop paresseux pour faire face aux événements Kubernetes ou si vous êtes simplement plus à l'aise avec Bash, nos collègues ont préparé une solution toute faite sous la forme d'un opérateur shell (nous l'avons annoncé en avril).

PPS


Lisez aussi dans notre blog:

Source: https://habr.com/ru/post/fr459320/


All Articles