Observabilité de la boucle : metrics, events, conditions & Déboguer un reconciler
Instrumenter et observer la boucle de réconciliation grâce aux métriques Prometheus, aux Events Kubernetes et aux Status Conditions, puis diagnostiquer méthodiquement les pannes d'un reconciler.
Observabilité de la boucle : metrics, events, conditions
L’idée en une phrase
L’observabilité d’un réconciliateur repose sur trois instruments complémentaires — les métriques Prometheus pour quantifier le comportement de la boucle, les Events pour consigner les faits ponctuels, et les Status Conditions pour décrire l’état structuré de chaque ressource — permettant de mesurer en permanence l’écart entre état désiré et état réel et la capacité du contrôleur à le combler.
Analogie : Considérons le tableau de bord d’un avion de ligne. Les cadrans — altitude, vitesse, cap — sont les métriques : des mesures continues et chiffrées que l’équipage surveille en permanence. Les alertes sonores (« terrain ahead », « engine fire ») sont les Events : des signaux ponctuels qui attirent l’attention sur un fait précis. Les checklists annotées par le pilote (« moteur 1 : nominal, moteur 2 : dégradé, hydraulique : OK ») sont les Conditions : un état structuré, mis à jour au fil du vol, qui résume la situation de chaque sous-système.
Points clés
- Les contrôleurs exposent par convention un endpoint
/metricsau format Prometheus. Les métriques de la work queue (workqueue_depth,workqueue_adds_total,workqueue_retries_total,workqueue_queue_duration_seconds) quantifient la pression sur la boucle. Les métriques de réconciliation (controller_runtime_reconcile_total,controller_runtime_reconcile_time_seconds,controller_runtime_reconcile_errors_total) mesurent le débit, la latence et le taux d’erreur du reconciler. - Un Event Kubernetes est un objet API (
v1/Event) qui consigne un fait ponctuel lié à une ressource : création réussie, échec de connexion, mise à jour appliquée. Les Events sont stockés dans etcd avec une durée de rétention limitée (1 heure par défaut) et peuvent être évincés sous pression mémoire. Ils sont visibles viakubectl describesur la ressource concernée. - Une Status Condition est un élément structuré du champ
.status.conditionsd’une ressource. Chaque condition porte untype(par exempleReady,Progressing,Degraded), unstatus(True,FalseouUnknown), unreasonmachine-readable, unmessagelisible et unlastTransitionTime. Les conditions forment un instantané persistant de l’état de la ressource, à la différence des Events qui sont éphémères. - La convention de nommage recommandée suit le pattern positif pour les états normaux (
Ready,Available) et négatif pour les anomalies (Degraded,MemoryPressure). Le contrôleur met à jour les conditions à chaque réconciliation — elles reflètent donc le résultat du dernier tick de la boucle. - Ces trois instruments se complètent : les métriques répondent à « combien et à quelle vitesse ? », les Events à « que s’est-il passé ? », les conditions à « dans quel état est cet objet maintenant ? ».
Exemple concret
Un Operator BackupOperator gère 200 CR Backup. Son endpoint /metrics montre un controller_runtime_reconcile_time_seconds au p99 de 1,2 seconde et un controller_runtime_reconcile_errors_total de 14 sur les 10 dernières minutes. L’administrateur inspecte les Events du namespace backup-system et trouve 14 Events de type Warning avec la reason S3ConnectionFailed sur 3 CR distinctes. En examinant la condition Ready de la CR backup-prod-db, il constate status: False, reason: StorageUnreachable, message: "connexion au bucket S3 refusée depuis 08:42 UTC". Le diagnostic est immédiat : un problème réseau vers S3 affecte 3 sauvegardes. Les métriques ont signalé l’anomalie quantitative, les Events ont identifié la cause, la condition a résumé l’état de chaque ressource affectée.
Trois piliers de l’observabilité d’un reconciler
| Instrument | Nature | Persistance | Répond à | Consultation |
|---|---|---|---|---|
| Métriques Prometheus | Séries temporelles chiffrées | Durée de rétention de Prometheus | « Combien ? À quelle vitesse ? Quel taux d’erreur ? » | Grafana, PromQL, curl /metrics |
| Events | Objets API éphémères | 1 heure par défaut dans etcd | « Que s’est-il passé ? Quand ? Sur quel objet ? » | kubectl describe, kubectl get events |
| Status Conditions | Champ structuré dans .status | Persiste avec la ressource | « Dans quel état est cet objet maintenant ? » | kubectl get -o jsonpath, kubectl describe |
Code YAML — une CR avec status conditions
# État d'une CR Backup après réconciliation réussie.
# Les conditions résument le résultat du dernier tick de la boucle.
apiVersion: backup.example.com/v1
kind: Backup
metadata:
name: backup-prod-db
namespace: backup-system
spec:
schedule: "0 2 * * *" # état désiré : sauvegarde quotidienne à 02h00
target: prod-db
destination: s3://backups/prod-db
status:
lastBackupTime: "2026-07-09T02:00:42Z"
conditions:
- type: Ready # la ressource est-elle opérationnelle ?
status: "True"
reason: BackupSucceeded
message: "Dernière sauvegarde complétée en 34s"
lastTransitionTime: "2026-07-09T02:01:16Z"
- type: StorageReachable # le stockage de destination est-il accessible ?
status: "True"
reason: S3Connected
message: "Connexion au bucket S3 établie"
lastTransitionTime: "2026-07-08T02:00:05Z"
Code kubectl — inspecter métriques, Events et conditions
# Métriques de réconciliation — interroger l'endpoint Prometheus du contrôleur
kubectl port-forward -n backup-system deploy/backup-operator 8080:8080 &
curl -s localhost:8080/metrics | grep controller_runtime_reconcile
# controller_runtime_reconcile_total{controller="backup",result="success"} 1847
# controller_runtime_reconcile_total{controller="backup",result="error"} 14
# controller_runtime_reconcile_time_seconds_bucket{controller="backup",le="1"} 1820
# Events — filtrer par type Warning pour repérer les anomalies
kubectl get events -n backup-system --field-selector type=Warning --sort-by='.lastTimestamp'
# LAST SEEN TYPE REASON OBJECT MESSAGE
# 3m Warning S3ConnectionFailed backup/backup-prod-db connexion au bucket S3 refusée
# Conditions — extraire l'état structuré d'une CR
kubectl get backup backup-prod-db -n backup-system \
-o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\n"}{end}'
# Ready True BackupSucceeded
# StorageReachable True S3Connected
Code C# — émettre des Events et mettre à jour les conditions en KubeOps
// Reconciler instrumenté : émet des Events et met à jour les conditions
// à chaque passage de la boucle observe → diff → agit.
public class BackupController : IEntityController<V1Backup>
{
private readonly IKubernetesClient _client;
private readonly IEventRecorder _eventRecorder;
public BackupController(IKubernetesClient client, IEventRecorder eventRecorder)
{
_client = client;
_eventRecorder = eventRecorder;
}
public async Task ReconcileAsync(V1Backup entity, CancellationToken token)
{
try
{
// 1. Observer : vérifier la connectivité vers le stockage S3
await CheckStorageConnectivity(entity, token);
// 2. Diff + Agir : exécuter la sauvegarde si nécessaire
var result = await RunBackupIfDue(entity, token);
// Mettre à jour la condition Ready — reflet du dernier tick
SetCondition(entity, "Ready", "True", "BackupSucceeded",
$"Sauvegarde complétée en {result.Duration.TotalSeconds:F0}s");
// Émettre un Event de succès — fait ponctuel, horodaté
_eventRecorder.Publish(entity, "Normal", "BackupCompleted",
$"Sauvegarde {entity.Metadata.Name} terminée avec succès");
}
catch (StorageException ex)
{
// Condition : état persistant de la ressource
SetCondition(entity, "Ready", "False", "StorageUnreachable",
$"Connexion au bucket S3 refusée : {ex.Message}");
// Event : occurrence ponctuelle pour l'historique
_eventRecorder.Publish(entity, "Warning", "S3ConnectionFailed",
$"Impossible de joindre le stockage : {ex.Message}");
throw new RequeueException(TimeSpan.FromSeconds(30));
}
}
private static void SetCondition(V1Backup entity, string type,
string status, string reason, string message)
{
entity.Status.Conditions ??= new List<V1Condition>();
var condition = entity.Status.Conditions
.FirstOrDefault(c => c.Type == type);
if (condition is null)
{
condition = new V1Condition { Type = type };
entity.Status.Conditions.Add(condition);
}
// Ne mettre à jour lastTransitionTime que si le status change —
// préserve l'information « depuis combien de temps cet état dure ».
if (condition.Status != status)
condition.LastTransitionTime = DateTime.UtcNow;
condition.Status = status;
condition.Reason = reason;
condition.Message = message;
}
}
Piège courant : « Les Events suffisent pour surveiller le reconciler » est inexact. Les Events sont éphémères : leur durée de rétention par défaut est de 1 heure, et ils peuvent être évincés plus tôt si etcd subit une pression mémoire. Un Event émis à 03h00 peut avoir disparu à 05h00 lorsque l’administrateur enquête. Les métriques Prometheus (stockées dans un TSDB dédié, avec des semaines de rétention) et les Status Conditions (persistantes tant que la ressource existe) comblent cette lacune.
Déboguer un reconciler
L’idée en une phrase
Déboguer un réconciliateur revient à identifier où la boucle observe → diff → agit se rompt — l’observation est-elle incomplète, le diff calcule-t-il un écart erroné, ou l’action échoue-t-elle silencieusement — en exploitant les métriques, les Events, les conditions et les logs structurés pour retracer le chemin de chaque réconciliation.
Analogie : Considérons un médecin face à un patient dont la fièvre ne baisse pas malgré le traitement prescrit. Le diagnostic suit un protocole systématique : vérifier que le thermomètre fonctionne (l’observation est-elle fiable ?), s’assurer que le bon antibiotique a été prescrit (le diff est-il correct ?), confirmer que le patient prend effectivement ses comprimés (l’action s’exécute-t-elle ?). Chaque étape élimine une cause possible. Déboguer un reconciler suit la même logique : remonter la boucle étape par étape jusqu’à identifier la rupture.
Points clés
- La première question à poser est : la réconciliation se déclenche-t-elle ? La métrique
controller_runtime_reconcile_totalindique combien de fois le reconciler a été invoqué. Si le compteur ne progresse pas, le problème est en amont : l’informer ne détecte pas le changement, le watch est cassé, ou le leader election empêche la réplique d’agir (comme vu au chapitre day 29-30). - Si la réconciliation se déclenche mais que l’état réel ne converge pas, le problème est dans la logique du reconciler : soit l’observation de l’état réel est incomplète (le contrôleur ne lit pas toutes les ressources nécessaires), soit le calcul du diff est erroné (l’écart n’est pas détecté), soit l’action échoue silencieusement (le contrôleur avale les erreurs sans requeue ni Event).
- Les logs structurés sont l’outil principal du débogage en production. Chaque réconciliation doit journaliser la clé de l’objet traité, l’écart détecté, l’action entreprise et le résultat. Le format structuré (JSON) permet de filtrer et corréler les entrées.
- La métrique
workqueue_retries_total(vue au chapitre day 29-30) révèle les objets en boucle d’erreur : un nombre élevé de retries sur un même objet signale un échec récurrent que le backoff exponentiel ne suffit pas à résoudre. - Un reconciler qui termine sans erreur mais ne modifie rien est le cas le plus insidieux : il observe, calcule un diff nul (à tort) et ne fait rien. L’absence d’Event et de changement de condition est elle-même un symptôme — si l’état réel ne correspond pas à l’état désiré mais que le reconciler est « au vert », la logique de diff est en cause.
Exemple concret
Une CR WebApp déclare replicas: 5 dans sa spec, mais seuls 3 pods tournent. L’administrateur vérifie les métriques : controller_runtime_reconcile_total progresse normalement (le reconciler tourne). Les Events de la CR ne montrent aucun Warning. La condition Ready affiche status: True, reason: Reconciled. Le reconciler croit avoir convergé. L’administrateur active les logs au niveau debug et relance une réconciliation manuelle en annotant la CR. Les logs révèlent : le contrôleur lit le Deployment enfant et compare deployment.Spec.Replicas (5) avec entity.Spec.Replicas (5) — il conclut « pas d’écart ». Mais le Deployment a effectivement 5 replicas demandés et seulement 3 disponibles (un noeud manque de ressources). L’erreur est dans le diff : le contrôleur compare la spec du Deployment (5 = 5) au lieu de comparer l’état réel observé (3 disponibles) avec l’état désiré (5). La correction consiste à comparer deployment.Status.AvailableReplicas avec entity.Spec.Replicas.
Symptômes et diagnostic d’un reconciler défaillant
| Symptôme | Cause probable | Commande de diagnostic |
|---|---|---|
Le compteur reconcile_total ne progresse pas | Watch cassé, informer en erreur, leader election bloquée | kubectl get lease -n <ns> et logs du contrôleur |
retries_total explose sur un objet | Erreur récurrente non résoluble par la boucle | kubectl get events --field-selector involvedObject.name=<name> |
Condition Ready=True mais état réel divergent | Le diff compare les mauvais champs | Activer les logs debug, annoter la CR pour forcer un tick |
| Aucun Event émis sur une CR | Le reconciler avale les erreurs sans les signaler | Vérifier les blocs catch dans le code du reconciler |
reconcile_time_seconds en hausse continue | Fuite de ressource ou appels externes ralentis | Profiler le reconciler, vérifier les timeouts HTTP |
Code kubectl — session de débogage pas à pas
# 1. La réconciliation se déclenche-t-elle ? Vérifier le compteur.
kubectl port-forward -n webapp-system deploy/webapp-operator 8080:8080 &
curl -s localhost:8080/metrics | grep reconcile_total
# controller_runtime_reconcile_total{controller="webapp",result="success"} 342
# 2. Inspecter les Events récents de la CR problématique
kubectl describe webapp my-app -n production
# Events:
# (aucun Event récent — le reconciler ne signale rien)
# 3. Vérifier la condition — elle prétend que tout va bien
kubectl get webapp my-app -n production \
-o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
# {"type":"Ready","status":"True","reason":"Reconciled","message":"Reconciled"}
# 4. Forcer une réconciliation en annotant la CR (déclenche un événement watch)
kubectl annotate webapp my-app -n production debug-trigger=$(date +%s) --overwrite
# webapp.webapp.example.com/my-app annotated
# 5. Suivre les logs du contrôleur en temps réel pendant le tick
kubectl logs -n webapp-system deploy/webapp-operator -f --since=10s
# {"level":"info","controller":"webapp","key":"production/my-app",
# "msg":"reconcile start","specReplicas":5}
# {"level":"debug","controller":"webapp","key":"production/my-app",
# "msg":"deployment spec matches","deploymentReplicas":5,"desiredReplicas":5}
# → Le log révèle le bug : le diff porte sur spec et non sur status
Code C# — logs structurés pour tracer la boucle
// Un reconciler correctement instrumenté : chaque branche de la boucle
// est journalisée, facilitant le diagnostic en production.
public class WebAppController : IEntityController<V1WebApp>
{
private readonly IKubernetesClient _client;
private readonly ILogger<WebAppController> _logger;
public WebAppController(IKubernetesClient client,
ILogger<WebAppController> logger)
{
_client = client;
_logger = logger;
}
public async Task ReconcileAsync(V1WebApp entity, CancellationToken token)
{
var key = $"{entity.Metadata.NamespaceProperty}/{entity.Metadata.Name}";
_logger.LogInformation("Réconciliation démarrée pour {Key}", key);
// 1. Observer — lire l'état réel du Deployment enfant
var deployment = await _client.Get<V1Deployment>(
entity.Metadata.Name, entity.Metadata.NamespaceProperty);
if (deployment is null)
{
_logger.LogInformation(
"Deployment absent pour {Key}, création en cours", key);
await CreateDeployment(entity, token);
return;
}
// 2. Diff — comparer l'état RÉEL (available) avec l'état désiré (spec)
var desired = entity.Spec.Replicas;
var available = deployment.Status?.AvailableReplicas ?? 0;
_logger.LogDebug(
"Diff pour {Key}: désiré={Desired}, disponible={Available}",
key, desired, available);
if (available < desired)
{
// 3. Agir — l'écart existe, intervenir
_logger.LogInformation(
"Écart détecté pour {Key}: {Available}/{Desired}, correction",
key, available, desired);
deployment.Spec.Replicas = desired;
await _client.Update(deployment);
SetCondition(entity, "Ready", "False", "ScalingUp",
$"Mise à l'échelle en cours : {available}/{desired}");
}
else
{
_logger.LogDebug("Pas d'écart pour {Key}, état convergé", key);
SetCondition(entity, "Ready", "True", "Reconciled",
$"Tous les {desired} replicas disponibles");
}
}
}
Piège courant : « Le reconciler a terminé sans erreur, donc il a convergé » est un raisonnement trompeur. Un reconciler qui compare
spec.replicasdu Deployment enfant (la valeur qu’il a lui-même écrite) au lieu destatus.availableReplicas(l’état réel observé par le ReplicaSet controller) ne détecte jamais l’écart entre pods désirés et pods effectivement disponibles. L’absence d’erreur n’est pas synonyme de convergence — seule la comparaison entre l’état réel observé et l’état désiré déclaré constitue un diff valide.