
Go è attualmente un monopolista tra i linguaggi di programmazione che le persone scelgono per scrivere operatori per Kubernetes. Ci sono motivi oggettivi come:
- Esiste un potente framework per lo sviluppo di operatori in Go — .
- In Go sono scritte applicazioni che hanno "cambiato il gioco" come Docker e Kubernetes. Scrivere il proprio operatore in Go significa comunicare con l'ecosistema nella stessa lingua.
- L'alta performance delle applicazioni in Go e gli strumenti semplici per la gestione della concorrenza "out of the box".
NB: A proposito, come scrivere il proprio operatore in Go, lo in una delle nostre traduzioni di autori stranieri.
Ma cosa succede se la mancanza di tempo o, banalmente, la mancanza di motivazione ti ostacolano nello studio di Go? L'articolo offre un esempio di come scrivere un operatore di qualità, utilizzando uno dei linguaggi più popolari, conosciuto praticamente da ogni ingegnere DevOps, — Python.
Presentiamo: Copiatore — un operatore di copia!
Prendiamo ad esempio lo sviluppo di un semplice operatore destinato a copiare un ConfigMap o all'insorgere di un nuovo namespace, oppure quando una delle due entità cambia: ConfigMap e Secret. Dal punto di vista pratico, l'operatore può essere utile per l'aggiornamento massivo delle configurazioni delle applicazioni (aggiornando il ConfigMap) o per l'aggiornamento dei dati segreti — ad esempio, delle chiavi per lavorare con il Docker Registry (con l'aggiunta di un Secret nel namespace).
Quindi, cosa deve avere un buon operatore:
- L'interazione con l'operatore avviene attraverso (di seguito — CRD).
- L'operatore può essere configurabile. Utilizzeremo per questo i flag delle righe di comando e le variabili d'ambiente.
- La costruzione del contenitore Docker e del chart Helm è progettata in modo che gli utenti possano facilmente (letteralmente con un solo comando) installare l'operatore nel proprio cluster Kubernetes.
CRD
Affinché l'operatore sappia quali risorse cercare e dove trovarle, dobbiamo definire una regola per esso. Ogni regola sarà rappresentata come un singolo oggetto CRD. Quali campi deve avere questo CRD?
- Tipo di risorsa, che stiamo cercando (ConfigMap o Secret).
- Elenco dei namespace, in cui devono trovarsi le risorse.
- Selettore, con cui cercheremo le risorse nel namespace.
Descriviamo il 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 E subito creiamo una regola semplice — per cercare nel namespace con nome default tutti i ConfigMap con label del tipo copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultFatto! Ora dobbiamo in qualche modo ottenere informazioni sulla nostra regola. Preciso subito che non scriveremo richieste al API Server del cluster da soli. Useremo una libreria Python pronta :
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')}Il risultato dell'esecuzione di questo codice sarà il seguente:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Ottimo: siamo riusciti a ottenere la regola per l'operatore. E la cosa più importante è che l'abbiamo fatto, come si suol dire, nel modo Kubernetes.
Variabili d'ambiente o flag? Prendiamo tutto!
Passiamo alla configurazione principale dell'operatore. Ci sono due approcci fondamentali per configurare le applicazioni:
- utilizzare parametri della riga di comando;
- utilizzare variabili d'ambiente.
I parametri della riga di comando consentono di leggere le impostazioni in modo più flessibile, supportando la convalida dei tipi di dati. Nella libreria standard di Python c'è un modulo argparser, di cui faremo uso. I dettagli e alcuni esempi delle sue funzionalità sono disponibili in .
Ecco come apparirà un esempio di configurazione della lettura dei flag della riga di comando nel nostro caso:
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()D'altra parte, tramite le variabili d'ambiente in Kubernetes è possibile trasferire facilmente le informazioni di servizio del pod all'interno del container. Ad esempio, possiamo ottenere le informazioni sul namespace in cui è stato avviato il pod con la seguente struttura:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace La logica di funzionamento dell'operatore
Per capire come separare i metodi per lavorare con ConfigMap e Secret, utilizzeremo delle mappe speciali. In questo modo potremo comprendere quali metodi sono necessari per il monitoraggio e la creazione dell'oggetto:
LIST_TYPES_MAP = {
'configmap': 'list_namespaced_config_map',
'secret': 'list_namespaced_secret',
}
CREATE_TYPES_MAP = {
'configmap': 'create_namespaced_config_map',
'secret': 'create_namespaced_secret',
}Successivamente, è necessario ricevere eventi dal server API. Realizziamo ciò nel seguente modo:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Otteniamo il metodo per monitorare gli oggetti
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)Dopo aver ricevuto l'evento, passiamo alla logica principale del suo trattamento:
# Типы событий, на которые будем реагировать
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) La logica principale è pronta! Ora dobbiamo impacchettare tutto questo in un unico pacchetto Python. Componiamo il file setup.py, scriviamo lì le meta-informazioni del progetto:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Versione python non supportata %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: Il client kubernetes per Python ha la sua versioning. Maggiori dettagli sulla compatibilità tra le versioni del client e le versioni di Kubernetes possono essere trovati nella .
Adesso il nostro progetto appare così:
copyrator
├── copyrator
│ ├── cli.py # Logica per il funzionamento della riga di comando
│ ├── constant.py # Costanti che abbiamo fornito sopra
│ ├── load_crd.py # Logica di caricamento CRD
│ └── operator.py # Logica principale del funzionamento dell'operatore
└── setup.py # Compilazione del pacchettoDocker e Helm
Il Dockerfile sarà indebitamente semplice: prenderemo un'immagine base python-alpine e installeremo il nostro pacchetto. L'ottimizzazione verrà rimandata a tempi migliori:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Il deployment per l'operatore è altrettanto semplice:
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 }}-accInfine, è necessario creare il ruolo corrispondente per l'operatore con i diritti necessari:
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 }}Risultato
Così, senza paura, rimprovero e senza dover studiare Go, siamo riusciti a costruire il nostro operatore per Kubernetes in Python. Certo, c'è ancora molto da fare: in futuro sarà in grado di gestire più regole, lavorare in parallelo e monitorare autonomamente le modifiche ai suoi CRD...
Per poter conoscere meglio il codice, lo abbiamo raccolto in . Se desideri esempi di operatori più avanzati realizzati con Python, puoi dare un'occhiata a due operatori per il deployment di mongodb ( e ).
P.S. Se ti sembra faticoso districarti tra gli eventi di Kubernetes o ti è più comodo usare Bash, i nostri colleghi hanno preparato una soluzione pronta sotto forma di (noi l'abbiamo presentata ad aprile).
P.P.S.
Leggi anche nel nostro blog:
- «»;
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
