Automatiser la vérification du commit
Automatiser la vérification du commit avec commitlint
Sommaire
J'ai récemment été amené à écrire mon premier contrôleur pour Kubernetes et je dois reconnaitre que mes premiers pas ont été difficiles. Kubernetes était encore relativement nouveau pour moi, et le concept de contrôleur complètement flou. Après avoir pris un peu de recul sur le sujet, j'ai eu envie d'écrire cet article pour tenter de démystifier le concept tel que j'aurais aimé le découvrir quand j'ai commencé à m'y intéresser.
Dans cet article, je me limiterai à l'utilisation du framework controller-runtime que l'on ma recommandé pour faire mes premiers pas. Ce dernier s'appuie sur les projets kubebuilder et operator-sdk et en partage les principaux concepts.
Un contrôleur Kubernetes est un programme qui scrute indéfiniment les ressources d'un cluster Kubernetes. Lorsque ces dernières sont modifiées, il interagit alors avec d'autres ressources afin d'atteindre un état désiré. Cet état est propre à chaque controleur et varie en fonction du besoin.
Kubernetes exécute déjà plusieurs contrôleurs nativement tel que le Replication Controller. Pour reprendre notre définition, lorsqu'on scale un Replica Set à 5 Pods, le Replication Controller listera les pods déjà présents dans ce Replica Set et en crééra le nombre nécessaire pour être à 5. Il atteindra alors son état désiré et fera tout son possible pour le maintenir.
On trouve couramment des contrôleurs qui facilitent la mise en place et l'évolution d'architectures complexes. Ils scrutent alors généralement des CRs (Custom Resources), et adaptent des configurations / des architectures. On retrouve assez souvent ce fonctionnement pour la mise en place de clusters par exemple. La CR contient alors une description abstraite du cluster et l'opérateur s'occupe d'appliquer les changements effectifs sur les Configmaps ; les Ingresses ; etc.
Le concept du contrôleur est très libre et on peut tout à fait s'en servir sans CRs aussi. On peut imaginer plus simplement qu'un contrôleur modifie automatiquement la configuration d'un pod lorsqu'un Ingress est modifié par exemple.
Les contrôleurs reposent fortement sur l'API Kubernetes pour manipuler les différentes ressources (get/update/create/etc) et nous allons réaliser ces appels via un client Kubernetes. J'ai choisi d'utiliser le client Go afin de rester dans l'ecosystème Kubernetes, mais il en existe pour plusieurs langages (voir la liste officielle).
Rentrons directement dans le vif du sujet. Voici un premier exemple d'un controlleur qui scrute les Ingress d'un cluster Kubernetes.
go mod init <votre-projet>
main.go
et mettez-y le contenu suivant :package main import ( networkingv1 "k8s.io/api/networking/v1" "context" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/manager/signals" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) type reconcileIngress struct { client client.Client } func (r *reconcileIngress) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { // votre logique return reconcile.Result{}, nil } func main() { mgr, _ := manager.New(config.GetConfigOrDie(), manager.Options{}) myCustomController, _ := controller.New("my-controller-name", mgr, controller.Options{ Reconciler: &reconcileIngress{ client: mgr.GetClient(), }, }) myCustomController.Watch(&source.Kind{Type: &networkingv1.Ingress{}}, &handler.EnqueueRequestForObject{}) mgr.Start(signals.SetupSignalHandler()) }
go get <nom_de_paquet>
(pour aller plus vite, vous pouvez installer directement le paquet principal sigs.k8s.io/controller-runtime
)Ces paquets contiennent tout le nécessaire pour utiliser un client Kubernetes ; le configurer pour qu'il puisse se connecter à un cluster ; scruter / manipuler des Ingresses
; configurer et démarrer notre controller.
Notre premier controller ne fait (presque) rien pour le moment, mais c'est une base déjà largement suffisante pour introduire quelques notions essentielles à sa bonne compréhension.
Le controller dispose d'une Work Queue pour stocker les différents évènements associés aux ressources qu'il scrute. À chaque fois qu'un évènement est dépilé, le controller déclenche une réconciliation en appelant la méthode Reconcile()
.
La réconciliation consiste à faire correspondre l'état courant (i.e. avant la modification de la ressource) avec l'état attendu. La méthode Reconcile()
contiendra la logique de notre controller, c'est à dire l'algorithme qui décrit comment atteindre cet état.
Il est important de comprendre que la réconciliation est déclenchée à chaque fois qu'un évènement a lieu sur le type de ressource scrutée. Si on scrute les Ingress et que IngressA
et IngressB
sont modifiés, Reconcile()
s'exécutera 2 fois d'affilé et à chaque appel sera un objet reconcile.Request
qui contiendra 1 nom de resource et 1 namespace --- généralement ceux de la ressource concernée par l'évènement --- qui nous permettent d'identifier cette resource.
Pour pouvoir instancier notre controller, il faut au préalable créer les éléments suivants :
manager.Start()
notamment).reconcile.Requests
).controller.Watch()
Le manager peut être configuré via de nombreuses options qui ne sont toutefois pas nécessaires pour ce premier contrôleur. Dans notre exemple, il obtient la configuration du client via la méthode config.GetConfigOrDie()
qui :
$HOME/.kube/config
si le contrôleur s'exécute en dehors d'un cluster kubernetes (on peut préciser un autre path en utilisant l'argument --kubeconfig <path>
)Attention
Vérifiez le contenu de votre kube config avant de tester votre contrôleur en local. Faites très attention au contexte utilisé pour éviter d'éventuelles suprises en production !
Le handler supporte différents modes de fonctionnement qui influent sur les valeurs contenues dans reconcile.Request
. Plus précisemment, ces modes de fonctionnements définissent quels objets vont être réconciliés lorsqu'une ressource est modifiée.
toto
du namespace tata
a été modifié, on retrouvera ces valeurs dans reconcile.Request
.reconcile.Request
.{Name;Namespace}
à réconcilier par évènements.Regardons maintenant de plus près la définition de la méthode Watch()
.
Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error
Elle prend en argument les éléments suivants :
source.Kind
et externe (e.g hook github) en utilisant source.Channel
Delete
si on le souhaite.Pour notre premier contrôleur, nous scruterons une source interne de type Ingress
et notre handler fonctionnera en mode EnqueueRequestForObject
.
Pour manipuler un objet Kubernetes, on doit au préalable importer le paquet correspondant à son API Kubernetes (i.e. networking
pour les Ingresses
; apps
pour les Deployments
; etc.) afin d'accéder aux différentes structures des ressources.
Concentrons nous cette fois-ci sur la méthode Reconcile()
dans laquelle on implémente notre logique. On récupérera une ressource avec client.Get()
qui prend en argument un NamespacedName
(qui représente simplement un couple {Name;Namespace}
).
Note
Rappellez-vous que reconcile.Request
n'est pas la représentation de l'objet en cours de réconciliation. Il ne contient que le nécessaire pour identifier une ressource.
func (r *reconcileIngress) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { // Fetch Ingress ig := &networkingv1.Ingress{} r.client.Get(ctx, request.NamespacedName, ig) // Add Ingress annotation and update object ig.Annotations = map[string]string{ "foo": "bar", } r.client.Update(ctx, ig) return reconcile.Result{}, nil }
Une fois l'objet récupéré on peut le manipuler et changer son statut via les différentes méthodes --- Update()
; Patch()
; Delete()
; etc --- du client et on termine la réconciliation en retournant un couple (reconcile.Result{}, error)
.
Pour finaliser l'explication, notre contrôleur scrute les Ingresses et déclenche des réconciliations à chaque fois qu'il reçoit un évènement dessus. La réconciliation consiste à récupérer l'Ingress qui a été modifié et à s'assurer qu'il porte bien un label foo:bar
, rien de plus.
Note
Lorsque le contrôleur démarre, il déclenche des réconciliations pour toutes les ressources du type scruté qui existent déjà dans le cluster afin de partir d'un état cohérent.
Il ne vous reste plus qu'à vous assurer que le compte utilisé par le controller pour accéder aux ressources dispose de suffisamment de droits pour fonctionner correctement.
Voilà, c'est à peu près tout ce qu'il faut selon moi pour commencer à s'amuser. La suite, c'est à vous de l'écrire. Si je ne devais vous donner qu'un seul conseil, ce serait vraiment d'explorer la documentation des différents paquets du controller-runtime et les paquets des différentes API Kubernetes. Ils contiennent toutes les informations dont vous avez besoin.
Auteur(s)
Dimitri Fert
Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !
Notre équipe d'experts répond à toutes vos questions.
Nous contacterDécouvrez nos autres contenus dans le même thème
Automatiser la vérification du commit avec commitlint
Dans le domaine de la data, la qualité de la donnée est primordiale. Pour s'en assurer, plusieurs moyens existent, et nous allons nous attarder dans cet article sur l'un d'entre eux : tester unitairement avec Pytest.
Le domaine de la data est présent dans le quotidien de chacun : la majorité de nos actions peut être traduite en données. Le volume croissant de ces données exploitables a un nom : "Big Data". Dans cet article, nous verrons comment exploiter ce "Big data" à l'aide du framework Apache Spark.