Operator Kubernetes dengan Python tanpa kerangka kerja dan SDK

Operator Kubernetes dengan Python tanpa kerangka kerja dan SDK

Go saat ini memonopoli bahasa pemrograman yang dipilih orang untuk menulis pernyataan untuk Kubernetes. Ada alasan obyektif untuk ini, seperti:

  1. Ada kerangka kerja yang kuat untuk mengembangkan operator di Go - SDK Operator.
  2. Aplikasi pengubah permainan seperti Docker dan Kubernetes ditulis dalam Go. Menulis operator Anda di Go berarti berbicara dalam bahasa yang sama dengan ekosistem.
  3. Aplikasi Go berkinerja tinggi dan alat sederhana untuk bekerja dengan konkurensi di luar kotak.

NB: Omong-omong, bagaimana cara menulis pernyataan Anda sendiri di Go, kami sudah dijelaskan dalam salah satu terjemahan kami oleh penulis asing.

Namun bagaimana jika Anda terhambat untuk belajar Go karena kurangnya waktu atau sederhananya, motivasi? Artikel ini memberikan contoh bagaimana Anda dapat menulis pernyataan yang baik menggunakan salah satu bahasa paling populer yang diketahui hampir setiap insinyur DevOps - Ular sanca.

Temui: Mesin Fotokopi - Operator Fotokopi!

Sebagai contoh, pertimbangkan untuk mengembangkan pernyataan sederhana yang dirancang untuk menyalin ConfigMap ketika namespace baru muncul atau ketika salah satu dari dua entitas berubah: ConfigMap dan Secret. Dari sudut pandang praktis, operator dapat berguna untuk memperbarui konfigurasi aplikasi secara massal (dengan memperbarui ConfigMap) atau untuk memperbarui data rahasia - misalnya, kunci untuk bekerja dengan Docker Registry (saat menambahkan Rahasia ke namespace).

Dengan demikian, apa yang seharusnya dimiliki oleh operator yang baik:

  1. Interaksi dengan operator dilakukan dengan menggunakan Definisi Sumber Daya Kustom (selanjutnya disebut CRD).
  2. Operator dapat dikonfigurasi. Untuk melakukan ini, kita akan menggunakan flag baris perintah dan variabel lingkungan.
  3. Pembuatan container Docker dan diagram Helm dirancang agar pengguna dapat dengan mudah (secara harfiah hanya dengan satu perintah) menginstal operator ke dalam cluster Kubernetes mereka.

CRD

Agar operator mengetahui sumber daya apa yang harus dicari dan di mana mencarinya, kita perlu menetapkan aturan untuknya. Setiap aturan akan direpresentasikan sebagai satu objek CRD. Bidang apa yang harus dimiliki CRD ini?

  1. Jenis sumber daya, yang akan kita cari (ConfigMap atau Secret).
  2. Daftar namespace, di mana sumber daya harus ditempatkan.
  3. Pemilih, yang dengannya kita akan mencari sumber daya di namespace.

Mari kita jelaskan CRDnya:

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

Dan kami akan segera membuatnya aturan sederhana β€” untuk mencari di namespace dengan nama default semua ConfigMap dengan label seperti copyrator: "true":

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

Siap! Sekarang kita perlu mendapatkan informasi tentang aturan kita. Izinkan saya segera membuat reservasi bahwa kami sendiri tidak akan menulis permintaan ke Server API cluster. Untuk melakukan ini, kita akan menggunakan perpustakaan Python yang sudah jadi kubernetes-klien:

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

Sebagai hasil dari menjalankan kode ini, kami mendapatkan yang berikut:

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

Hebat: kami berhasil mendapatkan aturan untuk operator. Dan yang paling penting, kami melakukan apa yang disebut dengan cara Kubernetes.

Variabel atau tanda lingkungan? Kami mengambil semuanya!

Mari beralih ke konfigurasi operator utama. Ada dua pendekatan dasar untuk mengonfigurasi aplikasi:

  1. gunakan opsi baris perintah;
  2. menggunakan variabel lingkungan.

Opsi baris perintah memungkinkan Anda membaca pengaturan dengan lebih fleksibel, dengan dukungan dan validasi tipe data. Pustaka standar Python memiliki modul argparser, yang akan kita gunakan. Detail dan contoh kemampuannya tersedia di dokumentasi resmi.

Untuk kasus kami, seperti inilah contoh pengaturan tanda baris perintah pembacaan:

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

Di sisi lain, dengan menggunakan variabel lingkungan di Kubernetes, Anda dapat dengan mudah mentransfer informasi layanan tentang pod di dalam container. Misalnya, kita bisa mendapatkan informasi tentang namespace tempat pod berjalan dengan konstruksi berikut:

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

Logika operator

Untuk memahami cara memisahkan metode bekerja dengan ConfigMap dan Secret, kami akan menggunakan peta khusus. Kemudian kita dapat memahami metode apa yang kita perlukan untuk melacak dan membuat objek:

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

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

Selanjutnya, Anda perlu menerima acara dari server API. Mari kita terapkan sebagai berikut:

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)

Setelah menerima acara tersebut, kita beralih ke logika utama pemrosesannya:

# Π’ΠΈΠΏΡ‹ событий, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π΅Π°Π³ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ
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)

Logika utamanya sudah siap! Sekarang kita perlu mengemas semua ini ke dalam satu paket Python. Kami menyiapkan filenya setup.py, tulis informasi meta tentang proyek di sana:

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: Klien kubernetes untuk Python memiliki versinya sendiri. Informasi lebih lanjut tentang kompatibilitas antara versi klien dan versi Kubernetes dapat ditemukan di matriks kompatibilitas.

Sekarang proyek kami terlihat seperti ini:

copyrator
β”œβ”€β”€ copyrator
β”‚   β”œβ”€β”€ cli.py # Π›ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строкой
β”‚   β”œβ”€β”€ constant.py # ΠšΠΎΠ½ΡΡ‚Π°Π½Ρ‚Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅
β”‚   β”œβ”€β”€ load_crd.py # Π›ΠΎΠ³ΠΈΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ CRD
β”‚   └── operator.py # Основная Π»ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Π°
└── setup.py # ΠžΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚Π°

buruh pelabuhan dan Helm

Dockerfile akan sangat sederhana: ambil gambar dasar python-alpine dan instal paket kami. Mari kita tunda pengoptimalannya hingga waktu yang lebih baik:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Penerapan untuk operator juga sangat sederhana:

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

Terakhir, Anda perlu membuat peran yang sesuai untuk operator dengan hak yang diperlukan:

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

Begitulah, tanpa rasa takut, celaan, atau mempelajari Go, kami dapat membangun operator kami sendiri untuk Kubernetes dengan Python. Tentu saja, ia masih memiliki ruang untuk berkembang: di masa depan ia akan mampu memproses banyak aturan, bekerja di banyak thread, memantau perubahan dalam CRD-nya secara mandiri...

Untuk memberi Anda gambaran lebih dekat tentang kodenya, kami telah memasukkannya repositori publik. Jika Anda ingin contoh operator yang lebih serius diimplementasikan menggunakan Python, Anda dapat mengalihkan perhatian Anda ke dua operator untuk menerapkan mongodb (ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ ΠΈ kedua).

PS Dan jika Anda terlalu malas untuk menangani event Kubernetes atau Anda lebih terbiasa menggunakan Bash, rekan kami telah menyiapkan solusi siap pakai dalam bentuk operator shell (Kami diumumkan itu pada bulan April).

PPS

Baca juga di blog kami:

Sumber: www.habr.com

Tambah komentar