Pourquoi ne pas écrire un opérateur Kubernetes en Python pour la gestion de votre SI ?

Introduction
Kubernetes a révolutionné la façon dont nous déployons et gérons les applications dans l’ère cloud-native. Il fournit une plateforme puissante et flexible pour orchestrer efficacement les charges de travail conteneurisées. Cependant, pour étendre les capacités de Kubernetes au-delà de ses fonctionnalités natives, les Custom Resource Definitions (CRDs) et les Opérateurs entrent en jeu. Dans cet article, nous explorerons ce que sont les CRDs et les Opérateurs, comment ils fonctionnent ensemble, comment construire un opérateur et comment ils apportent une valeur immense au déploiement et à la gestion de l’infrastructure et des applications dans l’écosystème Kubernetes.

Custom Resource Definitions (CRDs)
À la base, Kubernetes s’appuie sur un ensemble de ressources intégrées comme les Pods, Services et Deployments pour gérer les conteneurs. Cependant, Kubernetes vous permet de définir vos ressources personnalisées via les CRDs, qui agissent essentiellement comme un mécanisme d’extension. Une CRD vous permet de définir un nouveau type de ressource avec ses propres propriétés et comportements.
Pensez aux CRDs comme des plans qui définissent à quoi ressembleront et comment se comporteront vos ressources personnalisées dans le cluster Kubernetes. Une fois que vous définissez une CRD, Kubernetes peut gérer le cycle de vie de ces ressources personnalisées, tout comme ses ressources natives.
Opérateurs
Les opérateurs sont un concept puissant introduit par CoreOS (maintenant Red Hat) qui exploitent les CRDs pour automatiser la gestion d’applications complexes dans Kubernetes. Un Opérateur est essentiellement un contrôleur Kubernetes spécifiquement conçu pour fonctionner avec les CRDs. Il utilise des contrôleurs personnalisés pour surveiller et réagir aux changements dans les ressources personnalisées, automatisant les opérations nécessaires pour maintenir l’état désiré des ressources.
Comment fonctionnent les Opérateurs
Les opérateurs sont adaptés pour gérer les subtilités d’une application particulière ou d’un composant d’infrastructure. Ils comprennent les rouages internes de l’application et agissent en conséquence pour maintenir sa santé et sa disponibilité.
Lorsque vous créez une ressource personnalisée, l’Opérateur surveille les changements de cette ressource. Lors de la détection d’un changement, l’Opérateur prend les mesures appropriées pour réconcilier l’état réel de la ressource avec son état désiré, tel que défini dans la CRD. Cela pourrait impliquer la mise à l’échelle de l’application, l’exécution de sauvegardes, la mise à jour des configurations, ou toute autre tâche spécifique à l’application.
Les opérateurs sont conçus pour être intelligents et autonomes, libérant les développeurs et les opérateurs des tâches manuelles fastidieuses et réduisant la probabilité d’erreurs humaines.

Comment écrire un opérateur
Nous allons maintenant prendre quelques minutes pour écrire un opérateur qui génère un UUID comme exemple KISS. Nous utiliserons Kopf, un framework Python, pour créer un opérateur simple. Bien que la plupart des opérateurs sur le marché soient écrits en Golang, Python est également populaire dans le monde Ops. Le choix dépend des connaissances de votre équipe.
D’abord, nous créerons une ressource de type (kind) Uuid. La CRD est créée avec le YAML ci-dessous.
# Une CRD simple pour générer un UUID
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: uuid.demo.operator
spec:
scope: Namespaced
group: demo.operator
names:
kind: Uuid
plural: uuid
singular: uuid
shortNames:
- id
- uuid
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- name: UUID
type: string
description: L'UUID généré
jsonPath: .status.uuid
- name: Status
type: string
description: Indique si le composant a été créé avec succès ou non.
jsonPath: .status.state
schema:
openAPIV3Schema:
type: object
properties:
status:
properties:
state:
type: string
uuid:
type: string
message:
type: string
type: object
Pour plus d’informations (et il y en a beaucoup) sur les CRDs, consultez la documentation.
Notez que nous avons spécifié notre propre API et version dans la CRD (demo.operator/v1) et que nous avons défini le kind à Uuid. Dans les additionalPrinterColumns, nous avons défini certaines propriétés qui peuvent être définies dans le spec qui seront également affichées à l’écran.
Créer la CRD et les ressources personnalisées ne suffit pas. Pour générer réellement un uuid lorsque la ressource personnalisée est créée, nous devons écrire et exécuter l’opérateur.
Installez d’abord Kopf et le package Kubernetes :
kopf==1.36.0
kubernetes==18.20.0
Mon code pour cet exemple :
import kopf
import logging
import kubernetes
import uuid
@kopf.on.login()
def login_fn(**kwargs):
return kopf.login_via_client(**kwargs)
@kopf.on.startup()
def configure(settings: kopf.OperatorSettings, **_):
settings.posting.level = logging.WARNING
@kopf.on.create('uuid') #code logique quand vous créez un objet uuid sur le cluster
def create_fn(spec, name, namespace, **kwargs):
myuuid = uuid.uuid4()
try:
logging.info(f"Demande de création pour la ressource [{name}] CRÉATION dans le namespace [{namespace}]")
logging.info(f"UUID généré pour la ressource [{name}] UUID [{myuuid}]")
State = "Ok"
Message = "Création de la ressource réussie"
except Exception as e:
logging.error(f"Exception générée pour uuid [{name}] CRÉATION dans le namespace [{namespace}]: {str(e)}")
Message = (str(e))
State = "Erreur"
myuuid = "NA"
updateObjectStatus(namespace, name, Message, State, myuuid)
@kopf.on.update('uuid') #code logique quand vous mettez à jour un objet uuid sur le cluster
def update_fn(spec, name, namespace, **kwargs):
pass
@kopf.on.delete('uuid') #code logique quand vous supprimez un objet uuid sur le cluster
def delete_fn(spec, name, namespace, **kwargs):
pass
def updateObjectStatus(custom_object_namespace, custom_object_name, Message, State, myuuid):
custom_object_status = buildObjectStatus(Message, State, myuuid)
sendObjectStatusToKubernetes(custom_object_namespace, custom_object_name, custom_object_status)
def buildObjectStatus(Message, State, myuuid):
return {
'status': {
'state': State,
'message': Message,
'uuid': str(myuuid)
}
}
def sendObjectStatusToKubernetes(custom_object_namespace, custom_object_name, custom_object_status):
kubernetes.client.CustomObjectsApi().patch_namespaced_custom_object(
namespace=custom_object_namespace,
name=custom_object_name,
body=custom_object_status,
group="demo.operator",
version="v1",
plural="uuid",
)
Exécution de l’opérateur localement, mais la deuxième étape sera de créer une image docker et d’exécuter cet opérateur dans le cluster.
kopf run --standalone --liveness=http://0.0.0.0:8080/healthz app.py
Maintenant vous avez une CRD uuid déployée sur le cluster et l’opérateur. Vous pouvez créer l’objet uuid sur le cluster.
apiVersion: demo.operator/v1
kind: Uuid
metadata:
name: my-first-ressource
Exemple en direct.

Conclusion
Les CRDs et les Opérateurs sont des outils puissants qui étendent les capacités de Kubernetes, en faisant plus qu’une simple plateforme d’orchestration de conteneurs. Avec les CRDs, vous pouvez définir des ressources personnalisées adaptées à vos besoins spécifiques, favorisant la cohérence et la réutilisabilité. Les opérateurs, à leur tour, exploitent ces ressources personnalisées pour automatiser des tâches complexes, consommer des produits et des APIs, s’assurant que vos applications et votre infrastructure sont toujours dans l’état désiré.
En incorporant les CRDs et les Opérateurs dans votre écosystème Kubernetes, vous pouvez simplifier la gestion de l’infrastructure, étendre la façon de consommer les informations de votre système, et rationaliser le déploiement d’applications, résultant en un environnement cloud-native plus efficace et robuste.
Traduction de mon article Medium