Toán tử Kubernetes trong Python không có framework và SDK

Toán tử Kubernetes trong Python không có framework và SDK

Go hiện đang độc quyền về ngôn ngữ lập trình mà mọi người chọn để viết câu lệnh cho Kubernetes. Có những lý do khách quan cho việc này, chẳng hạn như:

  1. Có một khuôn khổ mạnh mẽ để phát triển các toán tử trong Go - SDK nhà điều hành.
  2. Các ứng dụng thay đổi trò chơi như Docker và Kubernetes được viết bằng Go. Viết toán tử của bạn bằng Go có nghĩa là nói cùng một ngôn ngữ với hệ sinh thái.
  3. Hiệu suất cao của các ứng dụng Go và các công cụ đơn giản để làm việc đồng thời ngay lập tức.

NB: Nhân tiện, cách viết tuyên bố của riêng bạn trong Go, chúng tôi đã được mô tả trong một trong những bản dịch của chúng tôi bởi các tác giả nước ngoài.

Nhưng điều gì sẽ xảy ra nếu bạn không thể học cờ vây vì thiếu thời gian hay nói một cách đơn giản là vì động lực? Bài viết cung cấp một ví dụ về cách bạn có thể viết một tuyên bố hay bằng cách sử dụng một trong những ngôn ngữ phổ biến nhất mà hầu hết mọi kỹ sư DevOps đều biết - Python.

Gặp gỡ: Máy photocopy - nhà điều hành sao chép!

Ví dụ: hãy xem xét việc phát triển một câu lệnh đơn giản được thiết kế để sao chép Bản đồ cấu hình khi một không gian tên mới xuất hiện hoặc khi một trong hai thực thể thay đổi: Bản đồ cấu hình và Bí mật. Từ quan điểm thực tế, toán tử có thể hữu ích trong việc cập nhật hàng loạt cấu hình ứng dụng (bằng cách cập nhật ConfigMap) hoặc cập nhật dữ liệu bí mật - ví dụ: các khóa để làm việc với Docker Register (khi thêm Bí mật vào không gian tên).

Vì vậy, một nhà điều hành giỏi nên có những gì:

  1. Tương tác với người vận hành được thực hiện bằng cách sử dụng Định nghĩa tài nguyên tùy chỉnh (sau đây gọi tắt là CRD).
  2. Người vận hành có thể được cấu hình. Để làm điều này, chúng tôi sẽ sử dụng cờ dòng lệnh và biến môi trường.
  3. Việc xây dựng bộ chứa Docker và biểu đồ Helm được thiết kế để người dùng có thể dễ dàng (theo nghĩa đen chỉ bằng một lệnh) cài đặt toán tử vào cụm Kubernetes của họ.

CRD

Để người điều hành biết cần tìm tài nguyên nào và tìm ở đâu, chúng ta cần đặt ra quy tắc cho anh ta. Mỗi quy tắc sẽ được biểu diễn dưới dạng một đối tượng CRD duy nhất. CRD này nên có những lĩnh vực nào?

  1. Loại tài nguyên, mà chúng tôi sẽ tìm kiếm (ConfigMap hoặc Secret).
  2. Danh sách các không gian tên, trong đó các tài nguyên nên được đặt.
  3. Chọn, qua đó chúng ta sẽ tìm kiếm tài nguyên trong không gian tên.

Hãy mô tả 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

Và chúng ta sẽ tạo nó ngay luật đơn giản — để tìm kiếm trong không gian tên với tên default tất cả ConfigMap có nhãn như copyrator: "true":

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

Sẵn sàng! Bây giờ chúng ta cần bằng cách nào đó có được thông tin về quy tắc của mình. Hãy để tôi đặt chỗ ngay rằng chúng tôi sẽ không tự viết yêu cầu đến Máy chủ API cụm. Để làm điều này, chúng tôi sẽ sử dụng thư viện Python được tạo sẵn 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')}

Kết quả của việc chạy mã này, chúng tôi nhận được như sau:

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

Tuyệt vời: chúng tôi đã có được quy tắc cho nhà điều hành. Và quan trọng nhất, chúng tôi đã làm theo cách gọi là Kubernetes.

Biến môi trường hoặc cờ? Chúng tôi lấy mọi thứ!

Hãy chuyển sang cấu hình toán tử chính. Có hai cách tiếp cận cơ bản để định cấu hình ứng dụng:

  1. sử dụng các tùy chọn dòng lệnh;
  2. sử dụng các biến môi trường.

Các tùy chọn dòng lệnh cho phép bạn đọc cài đặt linh hoạt hơn với sự hỗ trợ và xác thực kiểu dữ liệu. Thư viện chuẩn của Python có một mô-đun argparser, mà chúng tôi sẽ sử dụng. Thông tin chi tiết và ví dụ về khả năng của nó có sẵn trong tài liệu chính thức.

Đối với trường hợp của chúng tôi, đây là ví dụ về việc thiết lập cờ dòng lệnh đọc sẽ như thế nào:

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

Mặt khác, bằng cách sử dụng các biến môi trường trong Kubernetes, bạn có thể dễ dàng chuyển thông tin dịch vụ về nhóm bên trong vùng chứa. Ví dụ: chúng ta có thể lấy thông tin về không gian tên mà pod đang chạy với cấu trúc sau:

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

logic toán tử

Để hiểu cách tách các phương pháp làm việc với ConfigMap và Secret, chúng tôi sẽ sử dụng các bản đồ đặc biệt. Sau đó, chúng ta có thể hiểu những phương pháp nào chúng ta cần theo dõi và tạo đối tượng:

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

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

Tiếp theo, bạn cần nhận các sự kiện từ máy chủ API. Hãy thực hiện nó như sau:

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)

Sau khi nhận được sự kiện, chúng ta chuyển sang logic chính để xử lý nó:

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

Logic chính đã sẵn sàng! Bây giờ chúng ta cần gói tất cả những thứ này vào một gói Python. Chúng tôi chuẩn bị hồ sơ setup.py, viết thông tin meta về dự án ở đó:

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: Ứng dụng khách kubernetes cho Python có phiên bản riêng. Bạn có thể tìm thêm thông tin về khả năng tương thích giữa phiên bản máy khách và phiên bản Kubernetes trong ma trận tương thích.

Bây giờ dự án của chúng tôi trông như thế này:

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

Docker và Helm

Dockerfile sẽ cực kỳ đơn giản: lấy hình ảnh python-alpine cơ bản và cài đặt gói của chúng tôi. Hãy hoãn việc tối ưu hóa nó cho đến thời điểm tốt hơn:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Việc triển khai cho người vận hành cũng rất đơn giản:

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

Cuối cùng, bạn cần tạo một vai trò phù hợp cho người vận hành với các quyền cần thiết:

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

Tổng

Đó là cách mà không hề sợ hãi, trách móc hay học cờ vây, chúng tôi có thể xây dựng toán tử của riêng mình cho Kubernetes bằng Python. Tất nhiên, nó vẫn còn chỗ để phát triển: trong tương lai nó sẽ có thể xử lý nhiều quy tắc, hoạt động trong nhiều luồng, giám sát độc lập các thay đổi trong CRD của nó...

Để giúp bạn xem mã kỹ hơn, chúng tôi đã đưa nó vào kho lưu trữ công cộng. Nếu bạn muốn có ví dụ về các toán tử nghiêm túc hơn được triển khai bằng Python, bạn có thể chuyển sự chú ý sang hai toán tử để triển khai mongodb (đầu tiên и 2).

Tái bút Và nếu bạn quá lười xử lý các sự kiện Kubernetes hoặc đơn giản là bạn đã quen với việc sử dụng Bash hơn, các đồng nghiệp của chúng tôi đã chuẩn bị một giải pháp làm sẵn trong biểu mẫu người điều hành shell (Chúng tôi công bố nó vào tháng Tư).

PPS

Đọc thêm trên blog của chúng tôi:

Nguồn: www.habr.com

Thêm một lời nhận xét