## Introduction

Les webhooks d'admission Kubernetes sont des points d'extension puissants qui permettent de valider ou de muter les ressources avant leur persistance. Cependant, les tester directement sur un cluster partagé est risqué : un webhook mal configuré peut bloquer les déploiements, provoquer des défaillances en cascade, voire rendre le cluster partiellement inopérant. Un environnement de laboratoire local offre un bac à sable sûr et reproductible pour expérimenter la logique des webhooks, comprendre les modes de défaillance et gagner en confiance avant une mise en production.

Dans ce guide, nous allons configurer un laboratoire local à l'aide d'outils légers comme kind ou minikube, créer un webhook d'admission de validation et un webhook de mutation avec des exemples pratiques, vérifier leur comportement et couvrir les étapes de récupération. En suivant ce guide, vous disposerez d'un environnement pleinement fonctionnel pour développer et tester des webhooks d'admission sans risquer l'infrastructure partagée.

Une configuration claire réduit les retouches en séparant les étapes : préparation de l'environnement, gestion des certificats, déploiement du webhook et tests. Nous commencerons par un exemple restreint et mesurable pour que chaque étape soit observable.

## Inventaire des versions et de l'environnement

Avant de commencer, confirmez les versions et les outils que vous utiliserez. Pour ce guide, nous supposons ce qui suit :

| Composant | Version | Rôle |
|-----------|---------|------|
| Kubernetes | v1.27+ | Serveur d'API du cluster local |
| kind | v0.20.0+ | Cluster local léger |
| ou minikube | v1.31.0+ | Cluster local alternatif |
| OpenSSL | 3.0+ | Génération de certificats TLS |
| Go | 1.20+ | Écriture du serveur webhook |
| curl | toute version récente | Tester les points de terminaison du webhook |

Prérequis :

- Une machine avec au moins 2 CPU et 4 Go de RAM disponibles.
- Docker ou un conteneur runtime pris en charge par kind/minikube.
- Une familiarité de base avec les manifestes Kubernetes et les outils en ligne de commande.

Topologie : Nous allons créer un cluster à nœud unique où le serveur d'API s'exécute dans un conteneur (kind) ou une machine virtuelle (minikube). Le serveur webhook sera déployé sous forme de pod dans le même cluster et exposé via un service. Le serveur d'API appellera le webhook en HTTPS à l'aide d'un certificat auto-signé.

Assurez-vous que votre cluster local est opérationnel :

```bash
kind create cluster --name webhook-lab
```

Sortie attendue :

```
Creating cluster "webhook-lab" ...
 ✓ Ensuring node image (kindest/node: v1.27.3) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-webhook-lab"
```

Si vous utilisez minikube :

```bash
minikube start --driver=docker --kubernetes-version=v1.27.3
```

Vérifiez l'accès au cluster :

```bash
kubectl cluster-info
```

## Chemin de configuration sécurisé

Nous allons maintenant implémenter deux webhooks : un webhook de mutation qui ajoute une étiquette à chaque pod, et un webhook de validation qui rejette les pods avec une valeur d'étiquette spécifique. Cette approche ciblée nous permet de vérifier chaque élément indépendamment.

### Étape 1 : Générer les certificats TLS

Le serveur d'API doit faire confiance au serveur webhook. Comme nous sommes dans un laboratoire, nous pouvons utiliser une autorité de certification auto-signée et émettre un certificat de serveur. La configuration du webhook spécifiera le bundle de l'autorité de certification. Exécutez ces commandes pour générer les certificats (en utilisant un répertoire temporaire) :

```bash
mkdir -p /tmp/webhook-certs && cd /tmp/webhook-certs

# Générer la clé et le certificat de l'autorité de certification
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=webhook-ca" -days 365 -out ca.crt

# Générer la clé du serveur et la CSR (demande de signature de certificat)
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=webhook-server.default.svc" -out server.csr

# Signer le certificat du serveur avec notre autorité de certification
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
```

Le CN du certificat doit correspondre au nom DNS du service : `webhook-server.default.svc`.

Créez un secret Kubernetes avec le certificat et la clé du serveur :

```bash
kubectl create secret tls webhook-server-cert --cert=server.crt --key=server.key
```

Vous aurez également besoin du certificat de l'autorité de certification encodé en base64 pour la configuration du webhook. Nous l'utiliserons lors de la création de la configuration du webhook.

### Étape 2 : Déployer le serveur webhook

Créez un serveur HTTP simple qui répond aux demandes d'examen d'admission. Voici un exemple en Go. Enregistrez-le sous le nom `main.go` :

```go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "crypto/tls"

    admissionv1 "k8s.io/api/admission/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func mutatePods(w http.ResponseWriter, r *http.Request) {
    var admissionReview admissionv1.AdmissionReview
    body, _ := ioutil.ReadAll(r.Body)
    if err := json.Unmarshal(body, &admissionReview); err != nil {
        http.Error(w, "could not parse admission review", http.StatusBadRequest)
        return
    }

    // Construire le patch pour ajouter une étiquette
    patchType := admissionv1.PatchTypeJSONPatch
    pod := corev1.Pod{}
    if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
        http.Error(w, "could not unmarshal pod", http.StatusBadRequest)
        return
    }

    // Si les étiquettes sont nil, créer une carte vide
    if pod.Labels == nil {
        pod.Labels = map[string]string{}
    }
    pod.Labels["webhook-lab"] = "mutated"

    // Créer le patch
    patch := []map[string]interface{}{
        {"op": "add", "path": "/metadata/labels/webhook-lab", "value": "mutated"},
    }
    patchBytes, _ := json.Marshal(patch)

    admissionResponse := &admissionv1.AdmissionResponse{
        UID:       admissionReview.Request.UID,
        Allowed:   true,
        PatchType: &patchType,
        Patch:     patchBytes,
    }

    response := admissionv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            APIVersion: "admission.k8s.io/v1",
            Kind:       "AdmissionReview",
        },
        Response: admissionResponse,
    }
    respBytes, _ := json.Marshal(response)
    w.Header().Set("Content-Type", "application/json")
    w.Write(respBytes)
}

func main() {
    http.HandleFunc("/mutate", mutatePods)
    server := &http.Server{
        Addr: ":8443",
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{loadX509KeyPair("/etc/webhook/certs/tls.crt", "/etc/webhook/certs/tls.key")},
        },
    }
    fmt.Println("Starting webhook server on :8443")
    server.ListenAndServeTLS("", "")
}

func loadX509KeyPair(certFile, keyFile string) tls.Certificate {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        panic(err)
    }
    return cert
}
```

Construisez l'image Docker et chargez-la dans kind :

```bash
docker build -t webhook-server:latest .
kind load docker-image webhook-server:latest --name webhook-lab
```

Créez un déploiement et un service :

```yaml
# webhook-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-server
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webhook-server
  template:
    metadata:
      labels:
        app: webhook-server
    spec:
      containers:
      - name: server
        image: webhook-server:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: certs
          mountPath: /etc/webhook/certs
          readOnly: true
      volumes:
      - name: certs
        secret:
          secretName: webhook-server-cert
```

```yaml
# webhook-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: webhook-server
  namespace: default
spec:
  selector:
    app: webhook-server
  ports:
  - port: 443
    targetPort: 8443
```

Appliquez :

```bash
kubectl apply -f webhook-deployment.yaml
kubectl apply -f webhook-service.yaml
```

Vérifiez que le pod est en cours d'exécution :

```bash
kubectl get pods -l app=webhook-server
```

Sortie attendue :

```
NAME                            READY   STATUS    RESTARTS   AGE
webhook-server-79c4f4c9b7-9x2kq 1/1     Running   0          5s
```

### Étape 3 : Créer la MutatingWebhookConfiguration

Créez une configuration qui indique au serveur d'API d'appeler notre webhook lors de la création de pods. Remplacez `CA_BUNDLE` par le base64 de `ca.crt` :

```bash
CA_BUNDLE=$(cat /tmp/webhook-certs/ca.crt | base64 | tr -d '\n')
```

```yaml
# mutating-webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-mutator
webhooks:
- name: pod-mutator.default.svc
  clientConfig:
    service:
      name: webhook-server
      namespace: default
      path: "/mutate"
      port: 443
    caBundle: ${CA_BUNDLE}
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
```

Appliquez :

```bash
envsubst < mutating-webhook.yaml | kubectl apply -f -
```

### Étape 4 : Tester le webhook de mutation

Créez un pod simple :

```bash
kubectl run test-pod --image=nginx
```

Vérifiez les étiquettes :

```bash
kubectl get pod test-pod --show-labels
```

La sortie attendue inclut `webhook-lab=mutated`.

### Étape 5 : Ajouter un webhook de validation

Ajoutez maintenant un webhook de validation qui rejette les pods si l'étiquette `webhook-lab` est définie sur `mutated` (pour démontrer le refus). Vous pouvez étendre le serveur ou en créer un séparé. Par souci de concision, supposez un serveur similaire avec un point de terminaison `/validate`. La configuration :

```yaml
# validating-webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-validator
webhooks:
- name: pod-validator.default.svc
  clientConfig:
    service:
      name: webhook-server
      namespace: default
      path: "/validate"
      port: 443
    caBundle: ${CA_BUNDLE}
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
```

Appliquez et testez : la création du pod doit être refusée avec un message.

## Vérification et diagnostics

Pour vérifier que les webhooks fonctionnent correctement, effectuez ces contrôles :

- Vérifiez que les configurations de webhook existent :

```bash
kubectl get mutatingwebhookconfigurations, validatingwebhookconfigurations
```

- Inspectez les journaux du webhook :

```bash
kubectl logs deployment/webhook-server
```

Vous devriez voir les demandes d'examen d'admission entrantes.

- Utilisez `kubectl apply --dry-run=server` pour voir si le webhook est appelé sans persister l'objet.

- Testez avec un dry-run :

```bash
kubectl run test-pod --image=nginx --dry-run=server
```

- Utilisez `kubectl describe` sur la configuration du webhook pour voir le bundle CA et les règles.

Si le webhook n'est pas appelé, vérifiez les journaux du serveur d'API (pour kind, voir les journaux du conteneur) pour des messages d'erreur comme `x509: certificate signed by unknown authority` ou `connection refused`.

## Modes de défaillance et récupération

Les webhooks d'admission peuvent causer des problèmes. Modes de défaillance courants :

- **Serveur webhook indisponible :** Si `failurePolicy` est `Fail`, toutes les requêtes correspondantes seront rejetées. Pour récupérer, vous pouvez soit supprimer la configuration du webhook, soit mettre à l'échelle le déploiement pour restaurer la disponibilité. Si le serveur d'API ne peut pas être joint pour supprimer le webhook (peu probable mais possible), vous devrez peut-être modifier le manifeste du serveur d'API pour désactiver les plugins d'admission.
- **Bundle CA incorrect :** Le serveur d'API rejettera l'appel du webhook, et avec `failurePolicy: Fail`, la création de ressources échouera. Corrigez le bundle CA dans la configuration du webhook.
- **Délai d'attente :** Si le serveur webhook ne répond pas dans le délai imparti (10 secondes par défaut), le serveur d'API appliquera la politique d'échec.

Étapes de récupération :

```bash
# Désactiver temporairement le webhook en supprimant sa configuration
kubectl delete mutatingwebhookconfiguration pod-mutator
kubectl delete validatingwebhookconfiguration pod-validator
```

Ensuite, corrigez le problème sous-jacent et réappliquez.

Exemple de retour en arrière : si un nouveau webhook cause des problèmes, revenez à une version précédente en réappliquant l'ancienne configuration ou en utilisant `kubectl apply` avec le manifeste connu comme bon.

## Pièges courants et comment les éviter

Au-delà des modes de défaillance déjà couverts, plusieurs autres pièges peuvent vous faire trébucher lorsque vous travaillez avec des webhooks d'admission. Être conscient de ceux-ci vous fera gagner du temps de débogage.

- **Versions `admissionReviewVersions` incompatibles :** Le serveur d'API envoie les demandes d'admission dans les versions listées dans la configuration du webhook. Si votre serveur webhook ne comprend que v1beta1 mais que la configuration liste v1, la demande échouera. Déclarez toujours au moins v1 et assurez-vous que votre serveur la gère. Testez avec un point de terminaison d'écho simple pour voir quelle version est envoyée.

- **Oublier `sideEffects: None` :** Si votre webhook n'a pas d'effets secondaires (il ne modifie pas l'état externe), définissez `sideEffects: None`. Cela permet au serveur d'API de l'appeler pendant les demandes de dry-run et l'empêche d'être ignoré lorsque la fonctionnalité dry-run est utilisée. Si votre webhook a des effets secondaires, documentez-les et envisagez `sideEffects: NoneOnDryRun`.

- **Règles trop larges :** Faire correspondre toutes les ressources avec `*/*` peut amener le webhook à interférer avec les composants système. Limitez vos règles au minimum nécessaire, comme des ressources, des groupes et des opérations spécifiques. Par exemple, au lieu de faire correspondre tous les pods, faites correspondre uniquement les pods avec une étiquette spécifique ou dans un espace de noms particulier.

- **Espace de noms manquant dans clientConfig :** Si vous omettez `namespace` dans `clientConfig.service`, le serveur d'API suppose le même espace de noms que la configuration du webhook, ce qui n'est souvent pas là où vit votre service. Spécifiez toujours l'espace de noms explicitement.

- **Renouvellement du certificat :** Les certificats auto-signés ont une date d'expiration. Si le certificat expire, le webhook échouera. En production, utilisez une solution de gestion des certificats comme cert-manager avec renouvellement automatique. Dans le laboratoire, notez la date d'expiration et régénérez si nécessaire.

- **Ignorer les délais d'attente des webhooks :** Le délai par défaut est de 10 secondes, mais certaines opérations peuvent prendre plus de temps. Définissez un `timeoutSeconds` approprié dans la configuration du webhook et assurez-vous que votre webhook répond dans ce délai. Si votre webhook a besoin de plus de temps, ajustez le délai, mais soyez prudent quant au ralentissement de toutes les requêtes correspondantes.

## Liste de contrôle des opérations

Utilisez cette liste de contrôle pour votre laboratoire local et votre future utilisation en production. Chaque tâche a un seul responsable redevable et une fréquence de révision. Dans un cadre de production, attribuez un responsable tel que le responsable de l'ingénierie de plateforme.

| Tâche | Commande / Action | Résultat attendu | Responsable | Fréquence de révision |
|------|------------------|-----------------|-------|------------------|
| Vérifier que les pods du webhook s'exécutent | `kubectl get pods -l app=webhook-server` | 1/1 En cours d'exécution | Ingénieur DevOps de garde | À chaque déploiement, ou hebdomadaire |
| Vérifier les configurations de webhook | `kubectl get mutatingwebhookconfigurations, validatingwebhookconfigurations` | La liste montre les configurations | Responsable de plateforme | Mensuel |
| Tester le webhook de mutation | Créer un pod et inspecter les étiquettes | Étiquette ajoutée | Développeur | À chaque modification |
| Tester le webhook de validation | Créer un pod avec une étiquette interdite | Demande rejetée | Développeur | À chaque modification |
| Surveiller les journaux du webhook | `kubectl logs deployment/webhook-server` | Demandes d'admission journalisées | SRE | Quotidien pendant un incident, hebdomadaire sinon |
| Vérifier les journaux du serveur d'API pour les erreurs | Variable selon le cluster | Aucune erreur TLS ou de connexion | Administrateur de cluster | Hebdomadaire |
| Tester la politique d'échec | Mettre à l'échelle le déploiement du webhook à 0, créer un pod | La création du pod échoue avec une erreur | Ingénieur QA | Avant chaque version |
| Restaurer le webhook | Remettre à l'échelle à 1 | La création du pod réussit | Ingénieur QA | Après le test d'échec |

## Conclusion

Vous disposez maintenant d'un laboratoire local fonctionnel pour les webhooks d'admission Kubernetes. Vous avez appris à générer des certificats, déployer un serveur webhook, créer des configurations de webhooks de mutation et de validation, vérifier le comportement et récupérer après des défaillances. Utilisez cet environnement pour développer et tester en toute sécurité une logique d'admission plus complexe avant de la déployer en production. Commencez par de petits changements mesurables, et testez toujours les scénarios de défaillance pour comprendre l'impact. Les compétences que vous avez pratiquées ici—configuration TLS, enregistrement de webhook et récupération après défaillance—sont directement transférables aux environnements de production, où des tests minutieux et un plan de retour en arrière solide sont essentiels.