Py学习  »  Python

无需框架和SDK!使用Python来写一个Kubernetes Operator

Docker • 4 年前 • 488 次点击  

目前,Go在人们创建Kubernetes Operator时选用的编程语言中成为了事实上的垄断者。他们的偏好源于以下客观原因:

  1. Operator SDK[1]这个强大的框架可用于使用Go来开发Operator

  2. 许多基于Go的应用程序,例如Docker和Kubernetes,已成为改变游戏规则的角色。使用Go来编写Operator允许你使用同种语言与这些生态对话。

  3. 基于Go的应用程序的高性能以及开箱即用的简单机制。


但是如果你缺少时间或仅是积极性阻碍了你学习Go呢?在此文中,我们将向你展示如何使用几乎所有DevOps工程师熟悉的、最流行的编程语言之一即Python来创建一个可靠的Operator。

欢迎Copyrator!


为了简单实用,我们将创建一个简单的Operator,用于当新的命名空间出现或当ConfigMap或Secret两者之一更改其状态时复制ConfigMap。从实用角度来看,我们新的Operator可用于批量更新应用程序配置(通过更新ConfigMap)或者重设secrets。例如用于Docker Registry的密钥(当Secret添加到命名空间时)。

那么一个优秀的Kubernetes Operator需具备什么功能呢?让我们罗列一下:

  1. 与Operator的交互是通过Custom Resource Definitions[2](以下简称CRD)

  2. 该Operator是可配置的,我们能使用命令行参数或者是环境变量来配置它。

  3. Docker镜像和Helm图表在创建时考虑了易用性,所以用户可以毫不费力地安装它(基本上只需一个命令)到他们的Kubernetes集群。


CRD


为了让Operator知道哪些资源以及从哪里查找,我们需要配置一些规则。每个规则将被表示为指定的CRD对象。那这个CRD对象中需要有哪些字段呢?

  1. 我们所感兴趣的资源的类型(ConfigMap或者是Secret)

  2. 存储资源的命名空间列表

  3. Selector用于帮助我们在特定的命名空间中查找资源。


让我们来定义我们的CRD:

  1. apiVersion: apiextensions.k8s.io/v1beta1

  2. kind: CustomResourceDefinition

  3. metadata:

  4. name: copyrator.flant.com

  5. spec:

  6. group: flant.com

  7. versions:

  8. - name: v1

  9. served: true

  10. storage: true

  11. scope: Namespaced

  12. names:

  13. plural: copyrators

  14. singular: copyrator

  15. kind: CopyratorRule

  16. shortNames:

  17. - copyr

  18. validation:

  19. openAPIV3Schema :

  20. type: object

  21. properties:

  22. ruleType:

  23. type: string

  24. namespaces:

  25. type: array

  26. items:

  27. type: string

  28. selector:

  29. type: string


并立即添加一个简单的规则来选择匹配在default命名空间中带有 copyrator:"true"标签的ConfigMap。

  1. apiVersion: flant.com/v1

  2. kind: CopyratorRule

  3. metadata:

  4. name: main-rule

  5. labels:

  6. module: copyrator

  7. ruleType: configmap

  8. selector:

  9. copyrator: "true"

  10. namespace: default


现在我们必须以某种方式获取有关我们规则的信息。我们将不使用手动方式制作集群API请求。所以我们将使用名为kubernetes-client的Python库:

  1. import kubernetes

  2. from contextlib import suppress



  3. CRD_GROUP = 'flant.com'

  4. CRD_VERSION = 'v1'

  5. CRD_PLURAL = 'copyrators'



  6. def load_crd(namespace, name):

  7. client = kubernetes.client.ApiClient ()

  8. custom_api = kubernetes.client.CustomObjectsApi(client)


  9. with suppress(kubernetes.client.api_client.ApiException):

  10. crd = custom_api.get_namespaced_custom_object(

  11. CRD_GROUP,

  12. CRD_VERSION,

  13. namespace,

  14. CRD_PLURAL,

  15. name,

  16. )

  17. return {x: crd[x] for x in ('ruleType', 'selector', 'namespace')}


执行以上代码之后,我们将能看到以下结果:

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


非常好!现在我们已经有一个针对Operator的规则。更重要的是,我们已经可以使用所谓的Kubernetes的方式来做到这一点。

环境变量还是标志呢?我全都要!


现在是时候进行基本的Operator设置了。配置应用程序有两种主要的方法:

  • 通过命令行参数

  • 通过环境变量


你可以通过具备更多灵活性以及支持数据类型验证的命令行参数检索配置。我们将使用 *argparser*标准Python库中的模块。Python文档中[3]提供了其使用的详细信息和示例。

以下是适配我们需求的用于配置命令行标志检索的示例:

  1. parser = ArgumentParser(

  2. description='Copyrator - copy operator.',

  3. prog='copyrator'

  4. )

  5. parser.add_argument(

  6. '--namespace' ,

  7. type=str,

  8. default=getenv('NAMESPACE', 'default'),

  9. help='Operator Namespace'

  10. )

  11. parser.add_argument(

  12. '--rule-name',

  13. type=str,

  14. default=getenv('RULE_NAME', 'main-rule'),

  15. help='CRD Name'

  16. )

  17. args = parser.parse_args()


另一方面,你可以通过Kubernetes中的环境变量轻松地将有关Pod的服务信息传递到容器中。例如,你可以通过以下结构获取有关运行Pod的命名空间的信息:

  1. env:

  2. - name: NAMESPACE

  3. valueFrom:

  4. fieldRef:

  5. fieldPath: metadata.namespace


Operator的操作逻辑


让我们使用指定的字典来划分使用ConfigMap和Secret的方法。它们将使我们能够找出跟踪和创建对象所需的方法:

  1. LIST_TYPES_MAP = {

  2. 'configmap': 'list_namespaced_config_map',

  3. 'secret': 'list_namespaced_secret',

  4. }


  5. CREATE_TYPES_MAP = {

  6. 'configmap': 'create_namespaced_config_map',

  7. 'secret': 'create_namespaced_secret',

  8. }


然后你需要从APIserver获取事件。我们将以下面的方式来实现该功能:

  1. def handle(specs):

  2. kubernetes.config.load_incluster_config()

  3. v1 = kubernetes.client.CoreV1Api()

  4. # Get the method for tracking objects

  5. method = getattr(v1, LIST_TYPES_MAP[specs['ruleType']])

  6. func = partial(method, specs[ 'namespace'])


  7. w = kubernetes.watch.Watch()

  8. for event in w.stream(func, _request_timeout=60):

  9. handle_event(v1, specs, event)


收到事件后,我们继续处理它的基本逻辑:

  1. # Types of events to which we will respond

  2. ALLOWED_EVENT_TYPES = {'ADDED', 'UPDATED' }

  3. def handle_event(v1, specs, event):

  4. if event['type'] not in ALLOWED_EVENT_TYPES:

  5. return


  6. object_ = event['object']

  7. labels = object_['metadata'].get('labels', {})

  8. # Look for the matches using selector

  9. for key, value in specs['selector'].items():

  10. if labels.get(key) != value:

  11. return

  12. # Get active namespaces

  13. namespaces = map(

  14. lambda x: x.metadata.name,

  15. filter(

  16. lambda x: x.status.phase == 'Active',

  17. v1.list_namespace().items

  18. )

  19. )

  20. for namespace in namespaces:

  21. # Clear the metadata, set the namespace

  22. object_['metadata'] = {

  23. 'labels': object_['metadata']['labels'],

  24. 'namespace': namespace,

  25. 'name': object_['metadata']['name'],

  26. }

  27. # Call the method for creating/updating an object

  28. methodcaller(

  29. CREATE_TYPES_MAP[specs['ruleType']],

  30. namespace,

  31. object_

  32. )(v1)


完成基本逻辑之后,现在我们需要将它打包到单个Python包中。我们将创建 setup.py并添加有关项目的元数据:

  1. from sys import version_info

  2. from sys import version_info


  3. from setuptools import find_packages, setup


  4. if version_info[:2] < (3, 5):

  5. raise RuntimeError(

  6. 'Unsupported python version %s.' % '.'.join(version_info)

  7. )



  8. _NAME = 'copyrator'

  9. setup(

  10. name=_NAME,

  11. version='0.0.1',

  12. packages=find_packages(),

  13. classifiers=[

  14. 'Development Status :: 3 - Alpha',

  15. 'Programming Language :: Python',

  16. 'Programming Language :: Python :: 3',

  17. 'Programming Language :: Python :: 3.5',

  18. 'Programming Language :: Python :: 3.6',

  19. 'Programming Language :: Python :: 3.7',

  20. ],

  21. author='Flant',

  22. author_email='maksim.nabokikh@flant.com',

  23. include_package_data=True,

  24. install_requires=[

  25. 'kubernetes==9.0.0',

  26. ],

  27. entry_points={

  28. 'console_scripts': [

  29. '{0} = {0}.cli:main'.format(_NAME),

  30. ]

  31. }

  32. )


注意:Kubernetes的Python客户端库有自己的版本控制系统。此矩阵[4]中概述了客户端和Kubernetes版本的兼容性。

目前,我们的项目具备以下结构:

  1. copyrator

  2. ├── copyrator

  3. ├── cli.py # 命令行操作逻辑

  4. ├── constant.py # 上面定义的常量

  5. ├── load_crd.py # CRD加载逻辑

  6. └── operator.py # operator的集成逻辑

  7. └── setup.py # 包描述


Docker和Helm


生成的Dockerfile非常简单:我们将采用基本的python-alpine基础镜像并安装我们的软件包(我们先忽略掉优化相关部分):

  1. FROM python:3.7.3-alpine3.9

  2. ADD . / app

  3. RUN pip3 install /app

  4. ENTRYPOINT ["copyrator"]


Copyrator的部署也非常简单。

  1. apiVersion: apps/v1

  2. kind: Deployment

  3. metadata:

  4. name: {{ .Chart.Name }}

  5. spec:

  6. selector:

  7. matchLabels:

  8. name: {{ .Chart.Name }}

  9. template:

  10. metadata:

  11. labels:

  12. name: {{ .Chart.Name }}

  13. spec:

  14. containers:

  15. - name: {{ .Chart.Name }}

  16. image: privaterepo.yourcompany.com/copyrator:latest

  17. imagePullPolicy: Always

  18. args: ["--rule-type", "main-rule"]

  19. env:

  20. - name: NAMESPACE

  21. valueFrom:

  22. fieldRef:

  23. fieldPath: metadata.namespace

  24. serviceAccountName: {{ .Chart.Name }}-acc


最后,我们必须为Operator创建一个具有必要权限的相关角色:

  1. apiVersion: v1

  2. kind: ServiceAccount

  3. metadata:

  4. name: {{ .Chart.Name }}-acc


  5. ---

  6. apiVersion: rbac.authorization.k8s.io/v1beta1

  7. kind: ClusterRole

  8. metadata:

  9. name: {{ .Chart.Name }}

  10. rules:

  11. - apiGroups: [""]

  12. resources: ["namespaces"]

  13. verbs : ["get", "watch", "list"]

  14. - apiGroups: [""]

  15. resources: ["secrets", "configmaps"]

  16. verbs: ["*"]

  17. ---

  18. apiVersion: rbac.authorization.k8s.io/v1beta1

  19. kind: ClusterRoleBinding

  20. metadata:

  21. name: {{ .Chart.Name }}

  22. roleRef:

  23. apiGroup: rbac.authorization.k8s.io

  24. kind: ClusterRole

  25. name: {{ .Chart.Name }}

  26. subjects:

  27. - kind: ServiceAccount

  28. name: {{ .Chart.Name }}


结论


在本文中,我们展示了如何为Kubernetes创建自己的基于Python的Operator。当然它还有增长的空间,例如你可以通过处理多个规则的能力来丰富它,通过自身来监控CRD的变化,从并发能力中受益等等。

所有代码都可以在我们的公共存储库[5]中找到,以便你了解它。如果你对基于Python的Operator的其他示例感兴趣,我们建议你关注两个用来部署MongoDB的Operator, ([6]和[7])。

PS. 如果你不想处理Kubernetes事件,或者你更喜欢使用Bash,那么你可能也会喜欢我们易于使用的称为shell-operator的解决方案(我们已在4月份宣布)。

再次PS,有一种使用Python编写Kubernetes的替代方案——通过称为kopf[8](Kubernetes Operator Pythonic Framework)的特定框架。如果你想最小化你的Python代码,它会很有用。点击这里查看kopf文档[9]。

相关链接:

  1. https://github.com/operator-framework/operator-sdk

  2. https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/

  3. https://docs.python.org/3/library/argparse.html

  4. https://github.com/kubernetes-client/python#compatibility-matrix

  5. https://github.com/flant/examples/tree/master/2019/08-k8s-python-operator

  6. https://github.com/Ultimaker/k8s-mongo-operator

  7. https://github.com/kbst/mongodb

  8. https://github.com/zalando-incubator/kopf/

  9. https://kopf.readthedocs.io/


原文链接:https://medium.com/flant-com/kubernetes-operator-in-python-451f2d2e33f3


基于Kubernetes的DevOps实战培训


基于Kubernetes的DevOps战培训将于2019年10月11日在上海开课,3天时间带你系统掌握Kubernetes,学习效果不好可以继续学习。本次培训包括:容器特性、镜像、网络;Kubernetes架构、核心组件、基本功能;Kubernetes设计理念、架构设计、基本功能、常用对象、设计原则;Kubernetes的数据库、运行时、网络、插件已经落地经验;微服务架构、组件、监控方案等,点击下方图片或者阅读原文链接查看详情。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/44931
 
488 次点击