## Introduction

Les définitions de ressources personnalisées (Custom Resource Definitions, CRD) de Kubernetes étendent l'API Kubernetes, permettant aux équipes plateforme et aux développeurs de modéliser des ressources spécifiques à un domaine. Cependant, la gestion des CRD dans les pipelines CI/CD présente des défis uniques : évolution des schémas, dépendance à l'ordre des contrôleurs, conversion des versions d'API et sécurité des retours en arrière. Cet article fournit des conseils opérationnels pratiques pour automatiser la gestion du cycle de vie des CRD, de la validation initiale au déploiement progressif et à la récupération.

Nous couvrirons l'inventaire des versions et de l'environnement, les modèles de configuration sûrs, la vérification et les diagnostics, les modes de défaillance avec des procédures de récupération, et une liste de contrôle opérationnelle pour la mise en production. Chaque section comprend des commandes kubectl concrètes, des exemples de manifestes, les sorties attendues et les points de décision. Les pratiques décrites ici s'adressent aux développeurs, consultants DevOps et équipes techniques de startups qui doivent livrer des modifications de CRD en toute confiance.

## Inventaire des versions et de l'environnement

Avant d'automatiser toute modification de CRD, établissez un état des lieux de la configuration actuelle de votre cluster. Cet inventaire évite les suppositions et permet de prendre des décisions de retour en arrière.

### Identifier la version du cluster et la compatibilité de l'API CRD

Commencez par enregistrer la version du serveur Kubernetes et celle des contrôleurs ou opérateurs pertinents.

kubectl version --short
# Exemple de sortie :
# Client Version: v1.28.2
# Server Version: v1.27.5 
 Vérifiez la version de l'API apiextensions.k8s.io prise en charge pour les CRD. Dans Kubernetes 1.16+, apiextensions.k8s.io/v1 est stable ; les versions plus anciennes peuvent utiliser v1beta1 . Utilisez kubectl api-versions pour lister les groupes d'API disponibles.

kubectl api-versions | grep apiextensions
# apiextensions.k8s.io/v1 

### Inventorier les CRD existantes et leurs versions de stockage

 Listez toutes les CRD et leur version de stockage actuelle. La version de stockage détermine comment les objets sont persistés.

kubectl get crd -o custom-columns=NAME:.metadata.name,GROUP:.spec.group,STORAGE:.spec.versions[*].storage
# Exemple de sortie :
# NAME GROUP STORAGE
# appconfigs.example.com example.com true,false
# backups.example.com example.com true 
 Pour une CRD spécifique, inspectez les détails de ses versions et préservez les champs inconnus.

kubectl get crd appconfigs.example.com -o yaml | grep -A5 versions:
# versions:
# - name: v1alpha1
# served: true
# storage: false
# - name: v1beta1
# served: true
# storage: true 

### Collecter les ressources prérequises et les RBAC

 Les modifications de CRD nécessitent souvent des règles RBAC mises à jour pour les contrôleurs. Capturez les rôles et rôles de cluster actuels qui interagissent avec le groupe d'API de la CRD.

kubectl get clusterrole,role -l app=crd-controller -o yaml
# ou par groupe d'API :
kubectl get clusterrole -o yaml | grep -B2 'example.com' 
 Vérifiez si des webhooks d'admission sont configurés pour la CRD, car ils peuvent bloquer les mises à jour.

kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations -o yaml | grep -B3 'example.com' 

### Documenter l'état actuel avec horodatage

 Créez un répertoire d'instantané avec horodatage.

TIMESTAMP=$(date +%Y%m%d%H%M%S)
mkdir -p inventory-$TIMESTAMP
kubectl get crd -o yaml > inventory-$TIMESTAMP/crds.yaml
kubectl get deploy,sts,ds -n crd-controller-system -o yaml > inventory-$TIMESTAMP/controllers.yaml
kubectl get cm,secret -n crd-controller-system -o yaml > inventory-$TIMESTAMP/configs.yaml 
 Cet instantané est votre référence pour le retour en arrière. Stockez-le dans un système de contrôle de version ou un stockage d'objets.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Question rapide">
Question rapide 1 sur 2

Que recommande l'article comme première étape avant d'automatiser tout changement de CRD ?

- Créer une sauvegarde du cluster
- Établir une base de référence de l'état actuel du cluster
- Installer un contrôleur CRD
- Définir un webhook de conversion

<button type="button" class="eno-quiz-submit" disabled>Valider</button>
<div class="eno-quiz-explanation">L&#39;article indique : « Avant d&#39;automatiser tout changement de CRD, établissez une base de référence de l&#39;état actuel de votre cluster. Cet inventaire évite les suppositions et permet de décider d&#39;une restauration. »</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Chemin de configuration sûr

Un chemin de configuration sûr garantit que chaque modification est cadrée, révisable et réversible. Pour les CRD, cela implique le versionnage, la validation du schéma et un déploiement progressif.

### Utiliser des manifestes de CRD versionnés avec des schémas structurels

Spécifiez toujours un schéma structurel dans votre CRD. Cela permet la validation et la conversion future. Exemple de CRD minimale :

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
 name: appconfigs.example.com
spec:
 group: example.com
 names:
 kind: AppConfig
 listKind: AppConfigList
 plural: appconfigs
 singular: appconfig
 scope: Namespaced
 versions:
 - name: v1alpha1
 served: true
 storage: true
 schema:
 openAPIV3Schema:
 type: object
 properties:
 spec:
 type: object
 properties:
 replicas:
 type: integer
 minimum: 1
 image:
 type: string
 required: ["replicas", "image"] 
 Utilisez kubectl apply --dry-run=server pour valider sans persister.

kubectl apply -f crd.yaml --dry-run=server
# Attendu : customresourcedefinition.apiextensions.k8s.io/appconfigs.example.com created (server dry run) 

### Maintenir plusieurs versions d'API avec stratégie de conversion

 Si vous devez prendre en charge des clients sur différentes versions, définissez plusieurs versions et la conversion. Pour de simples changements de champs, la conversion None peut suffire.

spec:
 versions:
 - name: v1alpha1
 served: true
 storage: false
 schema: ...
 - name: v1beta1
 served: true
 storage: true
 schema: ...
 conversion:
 strategy: None 
 Pour des changements complexes, utilisez un webhook de conversion et testez-le rigoureusement.

### Appliquer le principe du moindre privilège RBAC

Créez des rôles dédiés pour les pipelines CI/CD qui n'autorisent que des opérations spécifiques sur la CRD et ses ressources.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 namespace: default
 name: crd-deployer
rules:
- apiGroups: ["apiextensions.k8s.io"]
 resources: ["customresourcedefinitions"]
 verbs: ["get", "list", "watch", "create", "patch"]
- apiGroups: ["example.com"]
 resources: ["appconfigs", "appconfigs/status"]
 verbs: ["get", "list", "watch", "create", "update", "patch"] 
 Évitez cluster-admin pour les comptes de service du pipeline.

### Déploiements progressifs avec canary et livraison progressive

Appliquez les modifications de CRD à un espace de noms ou un cluster de staging d'abord. Utilisez kubectl apply --server-side pour des mises à jour atomiques si nécessaire.

kubectl apply -f crd.yaml --server-side --field-manager=ci-pipeline 
 Surveillez la santé du contrôleur avant de promouvoir.

kubectl rollout status deployment/crd-controller -n crd-controller-system --timeout=60s
# Attendu : deployment "crd-controller" successfully rolled out 

## Vérification et diagnostics

 La vérification garantit que votre modification de CRD se comporte comme prévu. Utilisez une combinaison de vérifications déclaratives et de création réelle de ressources.

### Valider l'existence de la CRD et son schéma

Après l'application, confirmez que la CRD est établie et acceptée.

kubectl get crd appconfigs.example.com -o jsonpath='{.status.conditions[?(@.type=="Established")].status}'
# Attendu : True

kubectl get crd appconfigs.example.com -o jsonpath='{.status.conditions[?(@.type=="NamesAccepted")].status}'
# Attendu : True 
 Vérifiez les versions stockées.

kubectl get crd appconfigs.example.com -o jsonpath='{.status.storedVersions}'
# Attendu : ["v1beta1"] si v1beta1 est la version de stockage 

### Créer une ressource personnalisée de test

 Créez une ressource d'exemple pour vérifier la validation et le comportement du contrôleur.

apiVersion: example.com/v1beta1
kind: AppConfig
metadata:
 name: test-appconfig
 namespace: default
spec:
 replicas: 2
 image: nginx:1.21 
 kubectl apply -f test-cr.yaml
kubectl get appconfig test-appconfig -o yaml 
 Si le contrôleur de CRD définit un statut, vérifiez-le.

kubectl get appconfig test-appconfig -o jsonpath='{.status.conditions}' 

### Tester les échecs de validation

 Créez intentionnellement une ressource invalide pour vous assurer que le schéma est appliqué.

apiVersion: example.com/v1beta1
kind: AppConfig
metadata:
 name: bad-appconfig
spec:
 replicas: 0 # viole le minimum de 1 
 kubectl apply -f bad-cr.yaml
# Erreur attendue :
# The AppConfig "bad-appconfig" is invalid: spec.replicas: Invalid value: 0: spec.replicas in body should be greater than or equal to 1 

### Diagnostiquer les problèmes du contrôleur

 Si les ressources ne sont pas réconciliées, inspectez les journaux du contrôleur.

kubectl logs -n crd-controller-system deploy/crd-controller --tail=50
# Recherchez des erreurs comme "failed to list appconfigs" ou des erreurs de conversion. 
 Vérifiez les événements dans l'espace de noms.

kubectl get events -n default --sort-by=.lastTimestamp | grep appconfig 

 Question rapide 2 sur 2

Selon l'article, quel est le but de l'utilisation de `kubectl apply --dry-run=server` lors de l'application d'une CRD ?

- Appliquer la CRD sans la persister
- Appliquer la CRD et la supprimer immédiatement
- Appliquer la CRD en mode simulation pour tester
- Appliquer la CRD et ignorer les erreurs

<button type="button" class="eno-quiz-submit" disabled>Valider</button>
<div class="eno-quiz-explanation">Le texte indique : « Utilisez kubectl apply --dry-run=server pour valider sans persister. »</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

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

Planifiez les défaillances avant qu'elles ne surviennent. Voici les modes de défaillance courants des CRD en CI/CD et les étapes de récupération.

### Défaillance : Mise à jour de la CRD rejetée en raison d'un schéma invalide

Symptôme : kubectl apply renvoie une erreur 422 ou 400 avec des détails.

Récupération :

- Validez le manifeste avec kubectl apply --dry-run=server .

- Corrigez le schéma en fonction des messages d'erreur.

- Réappliquez.

Exemple :

The CustomResourceDefinition "appconfigs.example.com" is invalid: spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.replicas.type: Required value: must not be empty for specified object fields 

### Défaillance : Le contrôleur ne peut pas réconcilier la nouvelle version de stockage

 Symptôme : La CRD s'applique, mais status.conditions montre NonStructuralSchema ou les journaux du contrôleur montrent des erreurs de conversion.

Récupération :

- Vérifiez kubectl get crd appconfigs.example.com -o yaml pour les conditions de statut.

- Si NonStructuralSchema est vrai, inspectez le schéma pour des champs non pris en charge (par exemple, nullable: true à certains endroits) et corrigez.

- En cas d'erreurs de conversion, assurez-vous que le webhook de conversion est joignable et renvoie des réponses appropriées.

- Si nécessaire, revenez au manifeste de CRD précédent : kubectl apply -f backup/crds.yaml .

### Défaillance : Un changement radical supprime une version servie encore utilisée

Symptôme : Les clients utilisant l'ancienne version d'API obtiennent une erreur 404 ou no kind is registered .

Récupération :

- Identifiez les clients via les journaux d'audit ou les requêtes d'admission.

- Rétablissez temporairement la version servie : définissez served: true pour l'ancienne version et réappliquez.

- Coordonnez la migration des clients, puis dépréciez à nouveau dans une version future.

### Procédure de retour en arrière pour les CRD

Comme les CRD sont à portée de cluster, le retour en arrière doit être prudent.

- Restaurez le manifeste de CRD de sauvegarde.

kubectl apply -f inventory-$TIMESTAMP/crds.yaml 

- Assurez-vous que la version de stockage est définie sur la précédente.

- Attendez que la condition Established soit vraie.

- Vérifiez que les contrôleurs sont sains.

- Supprimez toutes les ressources personnalisées créées avec une version désormais non servie (si nécessaire, après sauvegarde).

### Défaillance : Le compte de service du pipeline manque de permissions

 Symptôme : Le job CI échoue avec forbidden: User "system:serviceaccount:ci:deployer" cannot create resource "customresourcedefinitions" .

Récupération :

- Examinez le Role/RoleBinding utilisé par le pipeline.

- Ajoutez les groupes d'API et verbes requis.

- Appliquez les modifications RBAC et relancez le pipeline.

## Liste de contrôle opérationnelle

Utilisez cette liste avant et après les modifications d'automatisation des CRD pour garantir la sécurité opérationnelle.

### Liste de contrôle pré-déploiement

- [ ] Version du cluster et compatibilité API documentées.

- [ ] CRD existantes et versions de stockage inventoriées.

- [ ] Sauvegarde des YAML de CRD actuels stockée.

- [ ] Nouveau manifeste de CRD utilisant apiextensions.k8s.io/v1 et schéma structurel.

- [ ] Modifications de schéma validées avec kubectl apply --dry-run=server .

- [ ] RBAC mis à jour pour le contrôleur et le pipeline.

- [ ] Stratégie de conversion définie si plusieurs versions.

- [ ] Plan de retour en arrière documenté avec commandes exactes.

### Exécution du déploiement

# 1. Appliquer la CRD
kubectl apply -f crd.yaml --server-side

# 2. Attendre que la CRD soit établie
kubectl wait --for=condition=Established --timeout=60s crd/appconfigs.example.com

# 3. Déployer/mettre à jour le contrôleur si nécessaire
kubectl apply -f controller-deployment.yaml
kubectl rollout status deploy/crd-controller -n crd-controller-system --timeout=120s

# 4. Créer une CR de test de fumée
kubectl apply -f test-cr.yaml
kubectl wait --for=condition=Ready --timeout=30s appconfig/test-appconfig 

### Vérification post-déploiement

- [ ] Condition Established de la CRD est vraie.

- [ ] CR de test créée et réconciliée.

- [ ] Ressource invalide correctement rejetée.

- [ ] Journaux du contrôleur sans nouvelles erreurs.

- [ ] Ressources personnalisées existantes toujours accessibles et saines.

- [ ] Les métriques/alertes ne montrent aucun pic d'erreurs API.

### Surveillance continue

 Configurez des alertes pour :

- Erreurs de l'API apiextensions.k8s.io/v1 ( apiserver_request_total avec group=example.com et code >= 400).

- Échecs de réconciliation du contrôleur.

- Latence des webhooks si des webhooks de conversion sont utilisés.

Exemple de règle d'alerte Prometheus :

- alert: CRDControllerDown
 expr: up{job="crd-controller"} == 0
 for: 5m
 labels:
 severity: critical
 annotations:
 summary: "CRD controller is down" 

## Conclusion

 L'automatisation du CI/CD des CRD Kubernetes exige de la discipline en matière de versionnage, de validation, de déploiement et de récupération. En inventoriant votre état actuel, en suivant un chemin de configuration sûr, en vérifiant avec des signaux concrets et en préparant des procédures pour les défaillances courantes, vous réduisez le risque d'incohérence de l'API et de perturbation du contrôleur.

Commencez petit : choisissez une amélioration de CRD à faible risque, appliquez la liste de contrôle et mesurez le résultat. À mesure que votre confiance grandit, intégrez ces pratiques dans vos pipelines GitOps avec des tests automatisés et une livraison progressive. L'objectif n'est pas seulement de déployer des CRD, mais de les exploiter de manière sûre et réversible.