フレームワークと SDK を使用しない Python の Kubernetes Operator

フレームワークと SDK を使用しない Python の Kubernetes Operator

Go は現在、人々が Kubernetes 用のステートメントを作成するために選択するプログラミング言語を独占しています。 これには次のような客観的な理由があります。

  1. Go にはオペレーターを開発するための強力なフレームワークがあります。 オペレーターSDK.
  2. Docker や Kubernetes などの革新的なアプリケーションは Go で書かれています。 Go でオペレーターを記述するということは、エコシステムと同じ言語で話すことを意味します。
  3. Go アプリケーションの高いパフォーマンスと、すぐに同時実行を行うためのシンプルなツール。

NB: ところで、Go で独自のステートメントを記述する方法は、 すでに説明した 外国人著者による私たちの翻訳のXNUMXつで。

しかし、時間の不足、または簡単に言うとモチベーションの不足によって Go を学ぶことができない場合はどうすればよいでしょうか? この記事では、ほぼすべての DevOps エンジニアが知っている最も人気のある言語の XNUMX つを使用して、優れたステートメントを作成する方法の例を示しています。 Python .

紹介:コピー機 - コピーオペレーター!

例として、新しい名前空間が出現したとき、または XNUMX つのエンティティ (ConfigMap と Secret) のいずれかが変更されたときに ConfigMap をコピーするように設計された単純なステートメントを開発することを考えてみましょう。 実用的な観点から見ると、このオペレーターは、アプリケーション構成の一括更新 (ConfigMap の更新による) や、シークレット データの更新 (たとえば、Docker レジストリーを操作するためのキー (名前空間にシークレットを追加する場合) など) に役立ちます。

このように、 優れたオペレーターが持つべきもの:

  1. オペレーターとの対話は次を使用して実行されます。 カスタムリソース定義 (以下、CRDといいます。)
  2. 演算子を設定できます。 これを行うには、コマンド ライン フラグと環境変数を使用します。
  3. Docker コンテナーと Helm チャートのビルドは、ユーザーが (文字通り XNUMX つのコマンドで) オペレーターを Kubernetes クラスターに簡単にインストールできるように設計されています。

CRD

オペレーターがどのリソースをどこで探すべきかを知るためには、オペレーター用のルールを設定する必要があります。 各ルールは単一の CRD オブジェクトとして表されます。 この CRD にはどのようなフィールドが必要ですか?

  1. リソースの種類、これを探します (ConfigMap または Secret)。
  2. 名前空間のリスト、リソースはそこに配置されます。
  3. Selector、これにより、名前空間内のリソースを検索します。

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

そして、すぐに作成します 単純なルール — 名前空間内で名前を検索します。 default 次のようなラベルが付いたすべての ConfigMap copyrator: "true":

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

準備ができて! 次に、何らかの方法でルールに関する情報を取得する必要があります。 すぐに予約させていただきますが、クラスター API サーバーに自分でリクエストを書き込むことはありません。 これを行うには、既製の Python ライブラリを使用します。 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')}

このコードを実行した結果、次の結果が得られます。

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

素晴らしいです。オペレーターのルールを取得することができました。 そして最も重要なのは、私たちがいわゆる Kubernetes 方式を採用したことです。

環境変数またはフラグ? 私たちはすべてを受け止めます!

メインのオペレーター構成に移りましょう。 アプリケーションを構成するには、次の XNUMX つの基本的なアプローチがあります。

  1. コマンドラインオプションを使用します。
  2. 環境変数を使用します。

コマンド ライン オプションを使用すると、データ型のサポートと検証により、設定をより柔軟に読み取ることができます。 Pythonの標準ライブラリにはモジュールがあります argparserを使用します。 その機能の詳細と例は、次の場所で入手できます。 公式ドキュメント.

この例では、読み取りコマンド ライン フラグの設定例は次のようになります。

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

一方、Kubernetes で環境変数を使用すると、コンテナ内のポッドに関するサービス情報を簡単に転送できます。 たとえば、次の構造を使用して、ポッドが実行されている名前空間に関する情報を取得できます。

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

演算子のロジック

ConfigMap と Secret を操作するためのメソッドを分離する方法を理解するために、特別なマップを使用します。 そうすれば、どのメソッドを追跡してオブジェクトを作成する必要があるかを理解できます。

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

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

次に、API サーバーからイベントを受信する必要があります。 次のように実装してみましょう。

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)

イベントを受信したら、それを処理するメイン ロジックに進みます。

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

メインロジックの準備が完了しました。 次に、これらすべてを XNUMX つの Python パッケージにパッケージ化する必要があります。 ファイルを用意します setup.py、そこにプロジェクトに関するメタ情報を書き込みます。

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: Python 用の kubernetes クライアントには独自のバージョン管理があります。 クライアントのバージョンと Kubernetes のバージョン間の互換性の詳細については、次のリンクを参照してください。 互換性マトリックス.

これで、プロジェクトは次のようになります。

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

ドッカーとヘルム

Dockerfile は非常にシンプルです。ベースの python-alpine イメージを取得し、パッケージをインストールします。 最適化を良い時期まで延期しましょう。

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

オペレーターの導入も非常に簡単です。

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

最後に、必要な権限を持つオペレーターに適切なロールを作成する必要があります。

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

合計

このようにして、恐れたり、非難したり、Go を学習したりすることなく、Python で Kubernetes 用の独自のオペレーターを構築することができました。 もちろん、まだ成長の余地はあります。将来的には、複数のルールを処理したり、複数のスレッドで動作したり、CRD の変更を個別に監視したりできるようになるでしょう...

コードを詳しく見るために、次のコードを配置しました。 パブリックリポジトリ。 Python を使用して実装されたより本格的な演算子の例が必要な場合は、mongodb をデプロイするための XNUMX つの演算子に注目してください (最初の и 2番目の).

PS そして、あなたが Kubernetes イベントに対処するのが面倒すぎる、または単に Bash の使用に慣れている場合は、私たちの同僚が次の形式の既製のソリューションを用意しています。 シェルオペレーター (私たちは 発表した XNUMX月にあります)。

PPS

私たちのブログもお読みください:

出所: habr.com

コメントを追加します