Un peu de YAML et 300 lignes de Python pour mieux comprendre et enrichir les Controllers de Kubernetes !
Sous prétexte d’améliorer la robustesse des Services rendus par un cluster Kubernetes, nous allons montrer, étape par étape, comment créer un Custom Controller qui va gérer un nouveau type de ressources, qu’on nommera les CriticalServices.
Tout d’abord, il s’agira d’écrire une application capable d’invoquer l’API server de Kubernetes. Cette application sera ensuite embarquée dans un POD configurable via une ConfigMap.
Pour finir, nous montrerons comment configurer dynamiquement ce Custom Controller en créant nos propres Custom Resources ! Vous verrez que l’objectif est moins complexe qu’il n’y paraît !
Le code Python présenté dans cet article a été testé avec Python 3.7 et 3.8. Les sources sont disponibles sur GitHub [1].
1. Kubernetes, POD et Services
1.1 Petit rappel
Parmi tous les types d’objets gérés par Kubernetes, intéressons-nous aux POD et aux Services : si un POD permet d’encapsuler un ou plusieurs Containers, le Service définit l’abstraction (constituée au minimum d’une adresse IP virtuelle) qui permet à des clients d’accéder au service réseau rendu par un POD/Container.
Comme il est recommandé de créer le Service avant le POD [2], allons-y gaiement et analysons le comportement obtenu.
Les deux fichiers YAML suivants décrivent notre Service et notre POD. Rien de compliqué, il s’agit de lancer un serveur NGinx en le rendant accessible depuis l’extérieur du cluster (d’où le choix du type NodePort dans la définition du Service) :
Le fichier de définition du Service se nomme service.yaml et contient les directives suivantes :
Appliquons ce fichier et vérifions que le Service ne possède aucun Endpoint :
La définition de notre POD nginx est contenue dans le fichier nginx-pod.yaml suivant :
La valeur des labels correspond bien au champ selector du Service ! Appliquons ce fichier et vérifions que tout est opérationnel :
Le Service possède bien un Endpoint. Si on est connecté sur un nœud du cluster, une requête sur l’adresse IP du Service (ici : 10.109.48.92) permet de vérifier qu’on peut atteindre le serveur NGinx.
Si on est sur une machine qui est « à l’extérieur » du cluster, on peut joindre NGinx sur le port 32732 (dans notre exemple), mais en utilisant l’adresse IP du nœud (ici : 3.249.248.137) :
Parfait !
1.2 Le problème...
Si vous avez été attentif (ou si vous le savez déjà !), vous avez remarqué que nous avons créé un Service qui, dans un premier temps, n’a pas trouvé de POD correspondant à la valeur de son selector, ainsi le champ Endpoints avait pour valeur <none>. Puis quand nous avons démarré un POD correctement labélisé, le Service a rajouté l’adresse IP de ce POD à sa liste d’Endpoints.
C’est comme cela que fonctionnent les Services : ils peuvent au cours de leur vie avoir une liste d’Endpoints vide. Est-ce normal pour un Service d’être dans cette situation ? Parfois oui, lors de sa création par exemple, mais en général ce n’est pas bon signe : un paquet qui va parvenir sur l’adresse IP du Service ne sera pas transmis à un POD et le client va expérimenter des timeouts. Essayons cela en supprimant le POD :
Essayez ensuite de vous connecter sur l’adresse IP de Service :
Pas terrible... Je n’ai pas eu la patience d’attendre la fin du timeout ! Maintenant, imaginez que suite à une erreur, un Service « perde » sa relation avec ses POD. Dans le meilleur des cas, votre génial outil de supervision va vous alerter au bout de plusieurs minutes. Pendant ce temps, les clients vont expérimenter des erreurs ou des timeouts. Ne pourrait-on pas améliorer la réactivité de nos outils et limiter les dégâts pour les clients ?
1.3 Une solution
Nous proposons dans cet article de montrer comment surveiller les Services du cluster pour y connecter automatiquement un « POD de secours », lorsqu’un Service ne possédera plus d’Endpoints. Il nous faudra pour cela être capables d’invoquer l’API fournie par Kubernetes via son processus kube-apiserver. Il nous faudra aussi définir notre propre type de ressources, c’est-à-dire une Custom Resource, afin de rendre notre application configurable et correctement intégrée avec les outils comme kubectl.
Par la suite, nous appellerons un Service sans Endpoints un « Service bancal ».
2. Création de notre application locale
2.1 Comment effectuer un appel à l’API server (en Python)
L’API de Kubernetes est bien documentée [3] et peut s’utiliser en envoyant les requêtes avec un client HTTP. Par exemple à l’aide du module requests de Python, nous pouvons écrire le code suivant pour obtenir la liste des POD correspondant à une certaine valeur de label et pour un espace de nommage (namespace) particulier :
Maintenant que nous avons compris comment appeler l’API à partir d’une requête HTTP, nous allons plutôt utiliser le module officiel pour Python [4] qui se nomme kubernetes (à installer avec pip). Le code précédent s’écrit maintenant :
Les deux exemples de code précédents occultent juste une chose : comment fournit-on nos credentials pour être autorisé à interroger l’API server ?
Avec le module kubernetes pour Python, nous allons utiliser le code suivant qui charge par défaut la configuration issue de notre fichier ~/.kube/config, mais qui nous permet aussi de spécifier notre propre fichier :
Si le Client affiche une erreur relative à l’invalidité du certificat fourni par l’API server, vous pouvez ajouter l’option insecure-skip-tls-verify: true dans le fichier de configuration. Si vous obtenez un warning de la part la librairie urllib3 indiquant que « vérifier un certificat est toujours conseillé », et que vous souhaitez supprimer ce warning, ajoutez les lignes suivantes dans votre code :
2.2 Surveiller les objets du cluster
N’oublions pas notre objectif : détecter le plus rapidement possible la présence de Services bancals. Nous ne souhaitons pas effectuer un polling régulier, ce qui est une mauvaise pratique en général. Nous allons plutôt profiter d’une fonctionnalité de l’API server qui nous permet de surveiller (watch) les modifications apportées à un ou plusieurs objets : ici, ce seront des Services.
Le code ci-dessous est une boucle infinie qui obtient tout d’abord la liste de tous les Services, tous namespaces confondus, puis qui reçoit les modifications apportées aux Services. Chaque événement (event) rapporté est un document JSON qui indique par son champ type si le Service a été ajouté (ADDED), détruit (DELETED) ou modifié (MODIFIED).
Rien de plus simple alors que de vérifier si un Service possède des Endpoints ou pas. Le fichier watch_service.py est un exemple de code qui effectue un « watch » :
Quand on exécute ce script, on obtient la liste de tous les Services actuellement définis et un message doit indiquer que le Service frontend ne possède pas de POD (vous avez détruit son POD en section 1.2) : il est bancal !
Que se passe-t-il si nous créons le POD à partir d’un autre terminal ? Eh bien, rien : si vous appliquez le fichier nginx-pod.yaml pour créer le POD comme précédemment, le Service sera fonctionnel, pourtant vous n’aurez reçu aucun nouvel événement susceptible de vous avertir que le Service possède maintenant un Endpoint !
2.3 Un peu de parallélisme
Nous pouvons donc surveiller les modifications apportées directement aux Services, c’est à dire, savoir si un Service est créé, détruit ou bien si sa spécification est modifiée. Mais pour savoir si un nouveau POD est supprimé ou ajouté aux Endpoints d’un Service, nous devons aussi surveiller les POD !
Aïe, pour faire deux « watches », il nous faut deux boucles ? Ça sent le parallélisme… Quelles sont nos possibilités en Python : l’idéal serait d’utiliser les co-routines et d’employer les mots-clés async et await. Mais il semble que le client kubernetes pour Python ne soit pas compatible avec ce modèle de programmation. On trouve bien des projets qui proposent une réécriture du module standard pour le rendre compatible avec ce modèle, mais je préfère la stabilité et donc, je préfère utiliser le module officiel. On trouve aussi des exemples qui ne « marchent » pas en réalité – je ne dénoncerai aucun blog ! Dès lors, entre les modes multithread et multiprocess, je choisis... le multiprocess ! L’utilisation du module multiprocess de la librairie standard permet de réaliser cela simplement. Le fichier watch_service_pods_v1.py est notre première version de notre implémentation multiprocess :
C’est globalement le même code que précédemment : nous avons ajouté une fonction watch_pods() qui surveille les modifications apportées aux POD, mais sans prendre de décisions particulières. Les fonctions watch_services() et watch_pods() sont exécutées dans des processus différents. On peut vérifier que le code fonctionne et affiche les événements correctement :
Si dans un autre terminal, vous créez le POD NGinx manquant :
Vous verrez apparaître les lignes suivantes :
Mais rien ne nous prévient que le Service frontend possède maintenant un Endpoint !
Il faut compléter l’algorithme de détection des Services bancals en considérant que si un POD est détruit ou modifié, un Service peut devenir bancal alors que si un POD est créé, un Service bancal peut maintenant posséder un Endpoint !
Comme nous sommes dans un environnement multiprocessus, on décide que les fonctions watch_services() et watch_pods() vont publier leurs événements dans une Queue (objet partagé) et qu’un troisième processus aura pour mission de vider cette Queue et de faire les tests de cohérence entre Services et POD.
Pour pouvoir afficher un message indiquant qu’un Service possède dorénavant un Endpoint, nous mémoriserons dans la liste lame_svc les Services bancals. Le fichier watch_service_pods_v2.py définit donc 3 fonctions lancées chacune par un processus :
Le code de la fonction handle_events() [4] a pour vocation de déterminer si l’événement reçu rend un Service bancal ou bien le rend opérationnel.
2.4 Un peu de souplesse et de paramètres
Pour finir, on aimerait ajouter un peu de paramétrisation à notre script Python. En particulier, on introduit la prise en compte de 2 variables d’environnement nommées LOG_LEVEL et NAMESPACES qui permettent de choisir le niveau de verbosité et de sélectionner les namespaces concernés par l’analyse des Services bancals. La valeur de NAMESPACES est une liste de nom de namespaces séparés par des virgules.
La nouvelle version de notre script s’appelle watch_services_pods_v3.py [4] et s’utilise ainsi :
3. Intégration de l’application dans un POD
Notre petite application fonctionne localement, mais pour des raisons de cohérence (et pour prévoir la suite !), nous devons la faire exécuter dans un POD qui pourra être contrôlé par un Deployment assurant ainsi une meilleure robustesse.
3.1 Création de l’image pour le Container
Pour créer un POD, il faut une image de Container. La création de l’image est réalisée via le fichier Dockerfile-local suivant :
Construisons et testons notre image localement :
Remarquez que nous avons passé le fichier de configuration ./ma_conf, qui définit l’accès au cluster Kubernetes comme paramètre de l’entrypoint, après avoir réalisé un montage de ce fichier dans le Container.
Notre image est fonctionnelle, passons à la création du POD...
3.2 Création d’un POD et des ressources connexes
Avant d’écrire le fichier YAML qui va décrire le Deployment du POD utilisant cette image, nous allons anticiper deux problèmes : comment accéder à l’API server depuis un POD et comment régler les autorisations d’accès?
3.2.1 Accès à l’API server depuis un POD
Comment notre script Python va-t-il pouvoir se connecter à l’API server du cluster ? Si la solution du fichier de configuration peut encore être utilisée, la documentation officielle expose les différentes solutions [5]. Une solution utilisant un Container « sidecar » qui est utilisé comme Proxy entre l’application et l’API server est décrite dans cet article [6]. Mais la documentation indique aussi une solution pour les utilisateurs de la librairie cliente Python : il suffit d’appeler la bonne fonction de configuration !
Nous créons une nouvelle version watch_service_pods_v4.py. La différence avec la version v3 est affichée ci-dessous :
En cas d’échec du chargement du fichier de configuration par défaut, on appelle la fonction load_incluster_config() !
Il suffit de modifier le Dockerfile-local précédent en remplaçant v3 par v4 (on appellera le nouveau fichier Dockerfile-k8s). Puis on recrée l’image. On pensera bien ensuite à « pousser » notre image sur une Registry accessible par notre cluster. On suppose que cette nouvelle image s’appelle service-watcher-controller:v1 :
3.2.2 Réglages des autorisations
Notre deuxième souci concerne les autorisations : pour pouvoir consulter les descriptions des Services et des POD, notre application doit posséder les bons privilèges. Ce besoin passe par la création d’un ServiceAccount, d’un ClusterRole et d’un ClusterRoleBinding. Comme nous commençons à créer des objets dans Kubernetes, nous en profitons pour créer aussi un nouveau namespace qui s’appelle linux-mag ! Le namespace est décrit dans le fichier linux-mag-ns.yaml suivant :
Les autres objets sont décrits dans le fichier authorization.yaml :
Appliquons ces définitions :
Nous pouvons maintenant définir le fichier qui va déployer notre POD (fichier controller-depl.yaml) :
Créons ce Deployment et testons :
Les logs obtenus montrent bien que notre application détecte les Services bancals !
3.3 Paramétrisation via une ConfigMap
Rappelons que notre script Python peut être configuré via des variables d’environnement. Nous pourrions définir ces variables directement, avec leurs valeurs, dans le fichier controller-depl.yaml, mais nous préférons découpler les valeurs en les passant au POD via une ConfigMap. Le fichier configmap.yaml est un exemple d’une telle ConfigMap :
Pour utiliser cette ConfigMap, il faut modifier le fichier de déploiement qui devient le fichier controller-depl-cm.yaml : il suffit de rajouter les 3 lignes suivantes après la ligne « image » à la fin du fichier controller-depl.yaml :
On crée la ConfigMap, puis on détruit le Deployment courant pour le remplacer par le nouveau :
On peut maintenant reconfigurer le POD en modifiant les valeurs de la ConfigMap. Seul hic : quand une ConfigMap est modifiée, les POD qui l’utilisent doivent être détruits et relancés pour prendre en compte la modification. C’est d’ailleurs cette contrainte qui est donnée en exemple dans cet article [6] et qui a inspiré notre propre Custom Controller. Justement, venons-en au Custom Controller… !
4. Création d’une « Custom Resource »
Dorénavant, nous pouvons surveiller les Services bancals ! Mais parmi tous les Services définis dans notre cluster, nous considérons que seuls quelques-uns sont véritablement « critiques » et doivent toujours être associés à au moins un POD.
Notre Controller devra alors déterminer si un Service bancal est un Service critique et dans ce cas, il créera un POD supplétif qui pourra être sélectionné par ce Service bancal, le rendant ainsi disponible ! Si le Service bancal (qui ne l’est donc plus) retrouve à nouveau des POD applicatifs « normaux », alors notre Controller n’oubliera pas de supprimer le POD supplétif qu’il a lancé !
4.1 Définition d’un Service critique
Avant de définir formellement un nouveau type de ressources que nous appellerons CriticalService, donnons un exemple d’un tel service critique. Si nous voulons nommer ce CriticalService frontend et l’associer aux Services définis dans le namespace default qui sont labélisés avec la paire clé/valeur role=frontend, on devra écrire le fichier suivant (fichier critical-service.yaml) :
La valeur de la clé apiVersion est arbitraire, mais sera expliquée dans le paragraphe suivant.
Nous ne pouvons pas appliquer cette définition, car l’API server ne connaît pas encore ce nouveau type de ressource, comme le montre la tentative suivante :
4.2 Définition du nouveau type de ressource CriticalService
La documentation de référence pour la création d’une ressource personnalisée [7] nous explique comment structurer notre définition (fichier critical-service-crd.yaml) :
C’est très verbeux et encore, nous n’avons pas défini les champs optionnels, comme les champs description. Voici quelques explications sur le contenu de ce fichier :
Nom du champ |
Description |
metadata.name |
La valeur de metadata.name est de la forme <nom>.<spec.group>. |
spec.group |
Doit ressembler à un nom de domaine (ici : mycrd.com). On retrouve cette valeur comme valeur de apiVersion dans la définition d’un CriticalService. |
spec.versions[] |
Il est possible de servir plusieurs versions. Il faut que le champ served soit true pour que la version puisse être utilisée. Le champ storage indique quelle version est utilisée pour stocker la ressource dans le key-store (etcd), ce qui permettra de réaliser des conversions en cas de futurs changements de version [9]. La valeur de la version apparaît aussi dans la valeur de apiVersion dans la définition d’un CriticalService. |
spec.versions[].schema |
Permet de définir un schéma pour valider les futures définitions des objets de type CriticalService (la syntaxe de référence est disponible sur [8]). Dans notre cas, nous indiquons qu’un CriticalService pourra définir un champ namespace de type string et un champ matchLabels, obligatoire (required), qui sera une liste de paires clés/valeurs. Notez que si un champ est de type object, alors le champ properties est obligatoire. Si un champ est de type array, alors le champ items est obligatoire. |
spec.scope |
Indique si la ressource est globale (Cluster) ou non (Namespaced). |
spec.names |
Donne le nom de la ressource, au singulier, au pluriel, pour la valeur du champ kind et ses éventuelles abréviations. |
Nous pouvons maintenant définir notre Custom Resource et définir un CriticalService :
5. Gestion applicative des CriticalServices
Il nous reste la partie purement applicative qui consiste à modifier notre script watch_service_pods_v4.py (et notre image) pour appliquer les algorithmes suivants :
- il faut s’abonner aux modifications apportées aux objets de type CriticalService ;
- si un Service est détecté comme étant bancal et que c’est un CriticalService, il faut démarrer un nouveau POD à partir d’un modèle de POD (un PODTemplate) déjà prévu, puis il faut faire en sorte que ce POD soit sélectionné par le Service ;
- si un Service qui est un CriticalService reçoit un nouvel Endpoint et que le seul Endpoint qu’il possédait était notre POD, alors il faut détruire ce POD.
5.1 Modification des autorisations
Pour implémenter ces modifications, notre nouveau script watch_service_pods_v5.py doit posséder de nouveaux droits. En particulier, il doit pouvoir lire les ressources CriticalService, il doit pouvoir créer et détruire des POD et lire les PODTemplates. Le ClusterRole service-watcher-runner doit être enrichi avec les autorisations suivantes (fichier authorization-v5.yaml) :
Ensuite :
5.2 Création du modèle de POD
Pour apporter de la souplesse à notre Custom Controller, nous allons utiliser un modèle de POD (un PODTemplate) pour créer les POD dynamiquement. Par défaut, le nom du modèle sera critical-service-pod-template. Mais il serait facile de rajouter une clé dans notre ConfigMap pour paramétrer ce nom !
Nous proposons d’utiliser une image de base d’un serveur NGinx, mais il serait plus judicieux (et plus facilement paramétrable, peut-être) d’utiliser notre propre image. Quoi qu’il en soit, notre PODTemplate ressemble à ceci (fichier critical-service-pod-template.yaml) :
Appliquons ce fichier pour définir notre modèle :
Nous supposons que le POD qui sera lancé à partir de ce PODTemplate écoutera bien sur le port attendu par le Service.
5.3 Modification de notre script
Nous vous rappelons que le code intégral du code est disponible sur le dépôt GitHub [4]. Nous nous attachons ici à démontrer comment appeler l’API server via la librairie kubernetes pour Python.
5.3.1 Prise en compte des CriticalServices
Avec l’API Python, les objets qui correspondent aux Custom Resources sont gérés différemment. En particulier, il n’est pas possible d’utiliser la notation pointée pour accéder aux clés du dictionnaire event[‘object’]. Nous devons écrire event[‘object’][‘metadata’][name’] au lieu de event[‘object’].metadata.name. Dommage...
Pour gérer les CriticalServices, nous définissons un nouveau processus qui va appeler la fonction watch_critical_services() :
La fonction handle_events() doit être modifiée : elle va gérer un dictionnaire contenant les noms des CriticalServices et elle doit traiter les événements relatifs à ce type d’objets :
5.3.2 Création d’un POD par défaut et association au CriticalService
La fonction _create_default_pod() a pour rôle la création d’un nouveau POD basé sur le PODTemplate dont le nom peut être passé via une variable d’environnement.
Le POD qui sera créé doit appartenir au même namespace que celui du Service bancal et doit aussi posséder les labels qui feront qu’il sera automatiquement sélectionné par le Service. Le code en est le suivant :
Nous avons ajouté une Annotation à notre POD ce qui nous permettra de repérer nos POD plus facilement. La valeur par défaut de la variable pod_name_prefix est service-watcher, ce préfixe sert à définir le nom du POD créé.
5.3.3 Destruction du POD par défaut
Détruire est toujours plus facile que créer ! Le code de la fonction _delete_default_pod() détruit le POD en utilisant le couple namespace/nom_du_POD (on aurait pu aussi utiliser l’Annotation placée précédemment) :
5.3.4 Intégralité du code et nouvelle image
Nous vous renvoyons au code du fichier watch_service_pods_v5.py (environ 300 lignes de Python) disponible sur le dépôt [4]. Les fonctions de gestion des événements ont aussi été modifiées pour gérer correctement les cas de création ou de destruction des POD par défaut.
Pour utiliser la nouvelle version de notre « Service Watcher Controller », il faut reconstruire son image en suivant les étapes suivantes :
- modifier le fichier Dockerfile-k8s pour y indiquer la v5 ;
- reconstruire l’image ainsi :
- modifier le fichier controller-depl-cm.yaml pour qu’il fasse référence à notre v5, puis appliquer ce fichier avec la commande kubectl apply.
5.4 Tests
Il est temps de réaliser quelques tests ! Nettoyons les Services et les POD du namespace default. Nous supposons que nous n’avons pas défini de CriticalService :
Nous définissons alors le Service suivant (fichier service.yaml) :
Nous pouvons créer le Service et vérifier qu’il ne possède pas d’Endpoints :
Vérifions que le PODTemplate de la section 5.2 est toujours présent :
Vérifions aussi que notre Custom Controller est démarré grâce au fichier controller-depl-cm-v5.yaml :
Ajoutons maintenant la définition du CriticalService défini en section 4.1 :
Normalement, un POD avec le nom service-watcher-frontend a été créé pour devenir un Endpoint du Service frontend :
À l’aide du fichier nginx-pod.yaml suivant, ajoutons maintenant un POD qui possède les labels attendus par le Service :
Normalement, le POD nommé service-watcher-frontend créé par notre Custom Controller est détruit et c’est le nouveau POD qui apparaît dans la valeur des Endpoints !
5.5 Diagramme récapitulatif
Au terme de cet article, nous avons créé une dizaine d’objets de types différents (nous n’avons pas mentionné le ReplicaSet qui s’intercale entre le Deployment et le POD). Le diagramme de la figure 1 résume les interactions entre ces objets : oui, nous avons créé toute cette architecture !
Conclusion
Kubernetes est considéré comme un orchestrateur riche et puissant, mais plutôt complexe. Cependant, nous espérons qu’au terme de cet article, nous avons réussi à démystifier cette relative complexité en montrant comment intégrer, plutôt facilement, notre propre code à cet environnement. Bien sûr, si cela fut possible, c’est que l’architecture de Kubernetes est bien pensée – mais qui en aurait douté ?
Nous avons donc appris qu’écrire un code qui communique avec l’API server de Kubernetes est une chose aisée, ce qui permet de contrôler les ressources du cluster, pour peu qu’on en ait les droits !
Pour finir, je vous propose des pistes d’amélioration pour notre Custom Controller :
- S’il est possible d’intégrer du code écrit en Python, Go reste le langage privilégié dans l’écosystème de Kubernetes. Si nous l’avions utilisé, nous n’aurions pas eu à écrire un script multiprocess, les Go-Routines auraient été idéales.
- Dans notre exemple, le POD créé à partir du PODTemplate n’est pas paramétrable : comment lui passer le contenu de la page par défaut à afficher (une page du style « Site en maintenance », par exemple) ?
- Nous avons supposé que les Services relayaient les paquets sur le port 80 des POD listés dans leurs Endpoints : comment adapter notre Custom Controller (et le PODTemplate) pour prendre en compte la véritable valeur de ce TargetPort ?
À vos claviers...
Références
[1] Dépôt GitHub du projet : https://github.com/majeinfo/critsvc-k8s-controller
[2] Les « bonnes pratiques » pour Kubernetes : https://kubernetes.io/docs/concepts/configuration/overview/
[3] Documentation de l’API de Kubernetes : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
[4] Les sources et les exemples de l’API officielle pour Python : https://github.com/kubernetes-client/python/
[5] Les différentes méthodes pour accéder à l’API server: https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-api/
[6] Comment accéder à l’API server depuis un POD à l’aide d’un Container « sidecar » qui fait office de proxy : https://www.magalix.com/blog/extending-the-kubernetes-controller
[7] Comment créer une Custom Resource : https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#customresourcedefinition-v1-apiextensions-k8s-io
[8] Spécification des schémas de validation pour JSON : http://json-schema.org/
[9] Comment gérer les changements de version des Custom Resources : https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/