
Go is currently the dominant language among those chosen for writing operators for Kubernetes. This is due to objective reasons such as:
- There is a powerful framework for developing operators in Go — .
- Game-changing applications like Docker and Kubernetes are written in Go. Writing your operator in Go means speaking the same language as the ecosystem.
- The high performance of applications in Go and simple tools for working with concurrency "out of the box."
NB: By the way, how to write your operator in Go has been in one of our translations from foreign authors.
But what if your lack of time or simply motivation is hindering you from learning Go? The article provides an example of how to write a decent operator using one of the most popular languages known to nearly every DevOps engineer — Python.
Introducing: Copier — the copy operator!
For example, let’s consider the development of a simple operator designed for copying ConfigMap either when a new namespace appears or when one of the two entities — ConfigMap or Secret — changes. From a practical perspective, the operator can be useful for mass updating application configurations (by updating ConfigMap) or for updating secret data — such as keys for working with Docker Registry (when a Secret is added to the namespace).
So, what makes a good operator:
- Interaction with the operator is done via (hereinafter referred to as CRD).
- The operator can be configured. To do this, we will use command-line flags and environment variables.
- Building the Docker container and Helm chart is done in such a way that users can easily (literally with one command) install the operator in their Kubernetes cluster.
CRD
For the operator to know which resources to look for and where, we need to define a rule for it. Each rule will be represented as a single CRD object. What fields should this CRD have?
- Type bron, which we will be looking for (ConfigMap or Secret).
- List of namespaces, in which the resources should reside.
- Selector, by which we will search for resources in the namespace.
Let's describe the 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 En laten we direct een eenvoudige regel — voor het zoeken in de namespace met de naam default alle ConfigMaps met labels van het type copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultKlaar! Nu moeten we op de een of andere manier informatie over onze regel verkrijgen. Ik wil meteen vermelden dat we niet zelf verzoeken aan de API-server van het cluster gaan schrijven. Hiervoor zullen we gebruik maken van een beschikbare Python-bibliotheek :
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')}Het resultaat van deze code is als volgt:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Geweldig: we zijn erin geslaagd om de regel voor de operator te krijgen. En het belangrijkste — we hebben dit gedaan, zoals men zegt, op de Kubernetes-manier.
Omgevingsvariabelen of vlaggen? We nemen alles!
Laten we verder gaan met de basisconfiguratie van de operator. Er zijn twee basisbenaderingen voor het configureren van toepassingen:
- gebruik maken van opdrachtregelparameters;
- gebruik maken van omgevingsvariabelen.
Opdrachtregelparameters maken flexibele configuratie mogelijk, met ondersteuning en validatie van gegevenstypen. In de standaardbibliotheek van Python is er een module argparser, waarmee we dit zullen doen. Details en voorbeelden van de mogelijkheden zijn beschikbaar in .
Hier is hoe een voorbeeld van het instellen van opdrachtregelvlaggen eruit zou zien voor onze situatie:
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()Aan de andere kant kunnen we met omgevingsvariabelen in Kubernetes gemakkelijk de service-informatie van de pod naar binnen in de container verplaatsen. Bijvoorbeeld, de informatie over de namespace waarin de pod draait, kunnen we verkrijgen met de volgende constructie:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace De logica van de operator
Om te begrijpen hoe we de methoden voor het werken met ConfigMap en Secret kunnen scheiden, maken we gebruik van speciale kaarten. Dan kunnen we begrijpen welke methoden we nodig hebben voor het monitoren en creëren van objecten:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Daarna moeten we gebeurtenissen van de API-server ontvangen. Dit implementeren we als volgt:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Verkrijg de methode voor het monitoren van objecten
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)Na het ontvangen van een gebeurtenis gaan we verder met de belangrijkste logica voor het verwerken ervan:
# Типы событий, на которые будем реагировать
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) De belangrijkste logica is klaar! Nu moeten we dit alles in één Python-package verpakken. We maken het bestand aan setup.py, en we schrijven daar de metadata van het project in:
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: De Kubernetes-client voor Python heeft zijn eigen versiebeheer. Meer informatie over de compatibiliteit van clientversies en Kubernetes-versies is te vinden in .
Momenteel ziet ons project er als volgt uit:
copyrator
├── copyrator
│ ├── cli.py # Logica voor commandoregelwerking
│ ├── constant.py # Constanten die we hierboven hebben genoemd
│ ├── load_crd.py # Logica voor het laden van CRD
│ └── operator.py # De belangrijkste logica van de operator
└── setup.py # Aankleding van het pakketDocker en Helm
Dockerfile wordt ongelooflijk eenvoudig: we nemen een basisafbeelding van python-alpine en installeren ons pakket. We stellen de optimalisatie uit tot betere tijden:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]De implementatie voor de operator is ook heel eenvoudig:
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 }}-accTen slotte is het nodig om een bijbehorende rol voor de operator te creëren met de vereiste rechten:
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 }}Conclusie
Zo, zonder angst, verwijten of kennis van Go, hebben we onze eigen operator voor Kubernetes opgezet met Python. Natuurlijk is er nog ruimte voor groei: in de toekomst zal hij in staat zijn om meerdere regels te verwerken, in meerdere threads te werken, zelf veranderingen in zijn CRD te monitoren...
Om de code beter te leren kennen, hebben we deze samengevoegd in . Als je geïnteresseerd bent in voorbeelden van meer geavanceerde operators die met Python zijn geïmplementeerd, kun je je aandacht richten op twee operators voor het uitrollen van mongodb ( en ).
P.S. En als je geen zin hebt om je in Kubernetes-events te verdiepen of gewoon liever Bash gebruikt - onze collega's hebben een kant-en-klare oplossing voorbereid in de vorm van (we het in april).
P.P.S.
Lees ook op onze blog:
- «»;
- «»;
- «»;
- «»;
- «».
Bron: habr.com
