Admission webhooks : valider l'état désiré & KubeOps en profondeur : webhooks et conversion
Intercepter et valider l'état désiré avant sa persistance grâce aux admission webhooks, puis implémenter validation, mutation et conversion de CRDs en C# avec KubeOps.
Admission webhooks : valider l’état désiré
L’idée en une phrase
Les admission webhooks interceptent chaque requête de modification avant que l’état désiré ne soit persisté dans etcd — ils constituent un point de contrôle synchrone dans la chaîne d’admission, garantissant que seul un état désiré valide entre dans la boucle de réconciliation.
Analogie : Considérons un bureau d’enregistrement foncier. Avant d’inscrire un nouveau titre de propriété dans le registre officiel, un notaire vérifie la conformité du dossier : identité des parties, absence de servitudes incompatibles, respect des règles d’urbanisme. Si le dossier est invalide, il est rejeté avant toute inscription — le registre ne contient jamais de données incohérentes. Les admission webhooks jouent ce rôle de notaire : ils valident — ou corrigent — chaque demande de modification avant qu’elle n’atteigne etcd, le registre de l’état désiré.
Points clés
- La chaîne d’admission traite chaque requête API dans un ordre strict : authentification → autorisation (RBAC) → webhooks de mutation → validation du schéma OpenAPI → webhooks de validation → persistance dans etcd. Les webhooks de mutation interviennent en premier et peuvent modifier l’objet ; les webhooks de validation interviennent ensuite et ne peuvent qu’accepter ou rejeter.
- Un MutatingAdmissionWebhook reçoit l’objet soumis et retourne un patch JSON. Cas d’usage typiques : injecter un sidecar, ajouter des labels par défaut, définir des valeurs manquantes dans la spec. L’objet modifié est ensuite soumis aux webhooks de validation.
- Un ValidatingAdmissionWebhook reçoit l’objet (éventuellement déjà muté) et retourne un verdict
allowed: trueouallowed: falseavec un message d’erreur. Il ne peut pas modifier l’objet. - Le champ failurePolicy de la configuration détermine le comportement en cas d’indisponibilité du webhook :
Fail(par défaut) rejette la requête,Ignorela laisse passer. Un webhook de validation critique doit utiliserFailpour éviter de persister un état désiré invalide lorsque le webhook ne peut pas le vérifier. - Depuis Kubernetes 1.26, les ValidatingAdmissionPolicy permettent d’exprimer des règles de validation en CEL (Common Expression Language) directement dans un manifeste YAML, sans déployer un serveur HTTPS externe — une forme déclarative de validation de l’état désiré.
Exemple concret
Un cluster héberge un Operator qui gère des CR Database. La spec contient un champ replicas (nombre de répliques) et un champ engine (moteur : postgres, mysql). Un ValidatingAdmissionWebhook vérifie deux invariants avant persistance : replicas doit être compris entre 1 et 7, et engine doit figurer dans la liste des moteurs supportés. À t₀, un utilisateur soumet une CR Database avec replicas: 10 et engine: "postgres". Le webhook rejette la requête avec le message « replicas doit être compris entre 1 et 7 ». La CR n’est jamais inscrite dans etcd, le reconciler ne voit aucune CR invalide et la boucle de réconciliation n’est pas déclenchée. L’utilisateur corrige et soumet replicas: 3. Le webhook accepte, l’objet entre dans etcd, le reconciler observe la nouvelle CR et agit.
Chaîne d’admission — traitement d’une requête API
| Étape | Composant | Rôle | Peut modifier l’objet |
|---|---|---|---|
| 1 | Authentification | Identifier l’appelant (certificat, token) | Non |
| 2 | Autorisation (RBAC) | Vérifier les permissions | Non |
| 3 | Mutating webhooks | Injecter des valeurs par défaut, ajouter des sidecars | Oui |
| 4 | Validation du schéma | Vérifier la conformité au schéma OpenAPI de la CRD | Non |
| 5 | Validating webhooks | Accepter ou rejeter selon des règles métier | Non |
| 6 | Persistance dans etcd | Écrire l’état désiré validé | — |
Code YAML — configuration d’un ValidatingAdmissionWebhook
# validating-webhook.yaml — interception des CR Database avant persistance
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: database-validation
webhooks:
- name: validate.database.example.com
admissionReviewVersions: ["v1"]
sideEffects: None # le webhook ne modifie rien en dehors de la réponse
failurePolicy: Fail # rejeter si le webhook est indisponible
clientConfig:
service:
name: database-operator # le Service qui expose le webhook
namespace: operator-system
path: /validate/database # endpoint HTTPS du webhook
caBundle: LS0tLS1CRUdJTi... # certificat CA encodé en base64
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["db.example.com"]
apiVersions: ["v1"]
resources: ["databases"]
Code bash — tester le rejet par un webhook de validation
# Soumettre une CR invalide (replicas > 7)
cat <<EOF | kubectl apply -f -
apiVersion: db.example.com/v1
kind: Database
metadata:
name: my-db
namespace: default
spec:
engine: postgres
replicas: 10
EOF
# Error from server: admission webhook "validate.database.example.com"
# denied the request: replicas doit être compris entre 1 et 7
# Soumettre une CR valide — le webhook accepte, etcd persiste
cat <<EOF | kubectl apply -f -
apiVersion: db.example.com/v1
kind: Database
metadata:
name: my-db
namespace: default
spec:
engine: postgres
replicas: 3
EOF
# database.db.example.com/my-db created
Piège courant : « Un ValidatingAdmissionWebhook garantit à lui seul l’intégrité de l’état désiré » est une simplification. Le webhook ne s’exécute qu’au moment de la requête API — il ne revalide pas les objets déjà persistés. Si une règle de validation change (par exemple,
replicasmaximum passe de 7 à 5), les CR existantes avecreplicas: 7restent dans etcd sans être rejetées rétroactivement. Un mécanisme complémentaire — un reconciler d’audit ou un CronJob de conformité — est nécessaire pour détecter et corriger les objets devenus non conformes après coup.
KubeOps en profondeur : webhooks et conversion
L’idée en une phrase
KubeOps expose des interfaces C# typées — IValidationWebhook<T>, IMutationWebhook<T> et les conversion webhooks — qui transforment l’implémentation de webhooks Kubernetes en logique métier pure, sans gérer manuellement les certificats TLS, le routage HTTP ni le format AdmissionReview : le framework réconcilie l’infrastructure technique pour que le développeur se concentre sur la validation de l’état désiré.
Analogie : Considérons un cabinet d’avocats qui emploie un secrétariat juridique. Les avocats rédigent les conclusions (la logique de validation et de mutation) sans se soucier du format des actes, de la mise en page officielle ni de la distribution au tribunal. Le secrétariat reçoit le dossier brut, le met en forme selon les règles de procédure, le signe électroniquement et le transmet au greffe. L’avocat se concentre sur le fond ; le secrétariat gère la forme. KubeOps joue le rôle du secrétariat : le développeur implémente une interface C#, KubeOps génère la configuration du webhook, gère les certificats et traduit les requêtes HTTP en appels typés.
Points clés
IValidationWebhook<T>implémente des méthodesCreate,UpdateetDeletequi reçoivent l’entité et retournent unValidationResult— succès ou échec avec message. KubeOps génère automatiquement laValidatingWebhookConfiguration, le routage HTTPS et la gestion des certificats au démarrage de l’Operator.IMutationWebhook<T>implémente une méthodeCreate(et optionnellementUpdate) qui reçoit l’entité et retourne unMutationResult. KubeOps calcule le patch JSON différentiel entre l’objet original et l’objet muté, et retourne ce patch à l’API server dans la réponseAdmissionReview.- Les conversion webhooks gèrent le passage entre versions d’une CRD (
v1alpha1→v1beta1→v1). Lorsqu’un client demande une version différente de celle stockée, l’API server appelle le webhook de conversion. KubeOps utilise le pattern hub-and-spoke : une version « hub » (la version de stockage) sert de référence ; chaque version « spoke » implémente la conversion vers et depuis le hub, évitant de coder N×(N-1) convertisseurs pour N versions. - Le déploiement des webhooks en cluster nécessite un certificat TLS valide. KubeOps intègre un mécanisme de certificate provisioning qui crée un Secret contenant le certificat et l’injecte dans le
caBundlede la configuration du webhook au démarrage. En production, on remplace souvent ce mécanisme par cert-manager pour obtenir des certificats signés par une autorité reconnue. - L’ordre d’exécution dans KubeOps suit la chaîne d’admission Kubernetes : mutation d’abord (les valeurs par défaut sont injectées), puis validation (les invariants métier sont vérifiés). Un webhook de validation peut donc s’appuyer sur les valeurs déjà injectées par le webhook de mutation — ce couplage séquentiel est un choix architectural à documenter.
Exemple concret
On développe un Operator de gestion de bases de données avec KubeOps. La CR Database possède un champ optionnel backupSchedule (expression cron). Le webhook de mutation injecte une valeur par défaut "0 2 * * *" (sauvegarde quotidienne à 2h) si le champ est absent. Le webhook de validation vérifie ensuite que l’expression cron est syntaxiquement correcte et que replicas est compris entre 1 et 7. À t₀, un utilisateur crée une CR sans backupSchedule et avec replicas: 3. Le MutatingWebhook ajoute backupSchedule: "0 2 * * *". Le ValidatingWebhook vérifie les invariants — tout est conforme. La CR est persistée dans etcd avec la valeur par défaut. Six mois plus tard, la CRD évolue vers v1beta1 avec un champ storageClass. Le conversion webhook convertit à la volée les CR v1alpha1 existantes vers v1beta1 en ajoutant storageClass: "standard" comme valeur par défaut.
Webhooks KubeOps — interfaces et responsabilités
| Interface | Méthode principale | Retour | Modifie l’objet | Cas d’usage |
|---|---|---|---|---|
IValidationWebhook<T> | Create, Update | ValidationResult | Non | Vérifier des invariants métier |
IMutationWebhook<T> | Create, Update | MutationResult | Oui (via patch JSON) | Injecter des valeurs par défaut |
| Conversion webhook | Convert | Objet dans la version cible | Oui (changement de version) | Migrer entre versions de CRD |
Code C# — webhook de validation avec KubeOps
// Webhook de validation : vérifier les invariants métier AVANT persistance dans etcd.
// KubeOps génère la ValidatingWebhookConfiguration automatiquement.
[ValidationWebhook(typeof(V1Database))]
public class DatabaseValidator : IValidationWebhook<V1Database>
{
// Opérations interceptées par ce webhook
public AdmissionOperations Operations
=> AdmissionOperations.Create | AdmissionOperations.Update;
public ValidationResult Create(V1Database entity, bool dryRun)
{
// Valider le nombre de replicas — invariant métier
if (entity.Spec.Replicas is < 1 or > 7)
return ValidationResult.Fail(
$"replicas doit être compris entre 1 et 7, reçu : {entity.Spec.Replicas}");
// Valider le moteur de base de données
var supportedEngines = new[] { "postgres", "mysql", "mariadb" };
if (!supportedEngines.Contains(entity.Spec.Engine))
return ValidationResult.Fail(
$"engine '{entity.Spec.Engine}' non supporté. "
+ $"Moteurs valides : {string.Join(", ", supportedEngines)}");
// L'état désiré est valide — autoriser la persistance
return ValidationResult.Success();
}
public ValidationResult Update(
V1Database oldEntity, V1Database newEntity, bool dryRun)
{
// Interdire le changement de moteur après création
if (oldEntity.Spec.Engine != newEntity.Spec.Engine)
return ValidationResult.Fail(
"Le changement de moteur n'est pas autorisé après création.");
// Appliquer les mêmes règles que la création
return Create(newEntity, dryRun);
}
}
Code C# — webhook de mutation avec KubeOps
// Webhook de mutation : injecter des valeurs par défaut avant validation.
// S'exécute AVANT le ValidatingWebhook dans la chaîne d'admission.
[MutationWebhook(typeof(V1Database))]
public class DatabaseMutator : IMutationWebhook<V1Database>
{
public AdmissionOperations Operations => AdmissionOperations.Create;
public MutationResult<V1Database> Create(V1Database entity, bool dryRun)
{
var modified = false;
// Injecter le planning de sauvegarde par défaut si absent
if (string.IsNullOrEmpty(entity.Spec.BackupSchedule))
{
entity.Spec.BackupSchedule = "0 2 * * *";
modified = true;
}
// Ajouter le label standard "managed-by" si absent
entity.Metadata.Labels ??= new Dictionary<string, string>();
if (!entity.Metadata.Labels.ContainsKey("app.kubernetes.io/managed-by"))
{
entity.Metadata.Labels["app.kubernetes.io/managed-by"] = "database-operator";
modified = true;
}
// KubeOps calcule le patch JSON entre l'original et l'entité modifiée
return modified
? MutationResult.Modified(entity)
: MutationResult.NoChanges();
}
}
Code YAML — CR soumise et résultat après mutation
# database.yaml — la CR telle que soumise par l'utilisateur (sans backupSchedule)
apiVersion: db.example.com/v1
kind: Database
metadata:
name: my-db
namespace: default
spec:
engine: postgres
replicas: 3
---
# Après passage dans le MutatingWebhook, l'objet persisté dans etcd :
# apiVersion: db.example.com/v1
# kind: Database
# metadata:
# name: my-db
# namespace: default
# labels:
# app.kubernetes.io/managed-by: database-operator # ajouté par le webhook
# spec:
# engine: postgres
# replicas: 3
# backupSchedule: "0 2 * * *" # injecté par le webhook
Code bash — observer l’effet du webhook de mutation
# Appliquer la CR sans backupSchedule
kubectl apply -f database.yaml
# database.db.example.com/my-db created
# Vérifier que le webhook de mutation a injecté la valeur par défaut
kubectl get database my-db -o jsonpath='{.spec.backupSchedule}'
# 0 2 * * *
# Vérifier le label injecté
kubectl get database my-db \
-o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}'
# database-operator
Piège courant : « Un webhook de mutation peut remplacer la validation — il suffit de corriger silencieusement les valeurs invalides » est une approche dangereuse. Un MutatingWebhook qui remplace
replicas: 100parreplicas: 7sans le signaler masque une intention erronée. L’utilisateur croit avoir demandé 100 répliques et ne comprend pas pourquoi la base n’en a que 7. La mutation sert à injecter des valeurs par défaut pour des champs optionnels ; la validation sert à rejeter explicitement les valeurs incorrectes avec un message d’erreur clair.