Aller au contenu
Open Source9 min de lecturePar Sebastian Gieseler

Des releases quotidiennes sans tâches perdues : le mode maintenance dans la CI

Livrer presque chaque jour, c'est déployer presque chaque jour au milieu de tâches de fond en cours. Comment nous avons fait de la fenêtre de déploiement une étape prévisible de la pipeline grâce au mode maintenance de Croniq, à une gate de configuration et à run_on_register.

Une release par semaine peut encore être déployée à trois heures du matin, quand presque aucune tâche ne tourne. Une release par jour, non. La livraison a lieu en journée et rencontre une application occupée à synchroniser, rapprocher et calculer. Chaque déploiement redémarre des services alors que des tâches de fond tournent ou arrivent à échéance.

Nous exploitons une application .NET avec de nombreuses tâches récurrentes, livrée presque chaque jour. Les tâches tournent sur Croniq, notre ordonnanceur open source écrit en Rust. Cet article décrit comment la fenêtre de déploiement y est devenue une étape ordinaire et prévisible de la pipeline.

La fenêtre de déploiement est un état d'exploitation à part entière

Pendant un déploiement, trois choses se produisent qu'un ordonnanceur doit distinguer :

  1. Les exécutions en cours sont touchées par le redémarrage. Si un runner est arrêté en plein travail, il reste une exécution à moitié terminée, et une relance ultérieure repart de zéro.
  2. Les exécutions arrivées à échéance sont confiées à une stack encore en train de démarrer. Au mieux elles échouent et passent en relance, au pire elles tombent sur un mélange d'ancienne et de nouvelle version.
  3. Les tâches modifiées ne correspondent plus à l'état. Si une release change ce qu'une tâche de rapprochement doit faire, l'ancien comportement reste en place jusqu'à sa prochaine exécution planifiée.

Un ordonnanceur qui ignore simplement le déploiement traite tout cela comme des erreurs aléatoires. Mieux vaut lui dire explicitement : nous livrons maintenant.

Mode maintenance : geler la distribution, sans arrêter l'ordonnanceur

Croniq dispose pour cela d'un mode maintenance global (PUT /v1/maintenance, réservé aux administrateurs). Tant qu'il est actif, l'ordonnanceur ne crée aucun nouveau travail et les runners ne reçoivent rien lorsqu'ils interrogent le serveur. Le serveur lui-même continue de tourner, accepte les déclencheurs et répond aux requêtes.

Dans la pipeline, cela devient une séquence fixe en quatre étapes :

  1. Activer. Avant le déploiement, la CI active la maintenance avec une note qui contient la version livrée et le lien vers l'exécution de la pipeline. Quiconque consulte ensuite le tableau de bord voit immédiatement pourquoi rien ne tourne.
  2. Attendre. La CI interroge GET /health jusqu'à ce qu'aucune exécution ne tourne plus (running vaut zéro), avec une durée d'attente maximale fixe. Les tâches en cours peuvent terminer proprement leur travail.
  3. Livrer. Ce n'est qu'à ce moment que les conteneurs sont redémarrés, et la pipeline attend le health check de l'application.
  4. Désactiver et vérifier. Une fois tout en bonne santé, la CI désactive la maintenance puis relit l'état. Le déploiement n'est considéré comme terminé que lorsque Croniq confirme que la maintenance est inactive.

L'essentiel est ce qui arrive au travail pendant la maintenance. Croniq fait volontairement la distinction :

  • Les exécutions en cours se terminent normalement. Une relance qu'elles programment est mise en file et retenue.
  • Le travail déjà en file, ainsi que les déclencheurs manuels et événementiels, restent dans la file et sont distribués dès la fin de la maintenance.
  • Les échéances planifiées sont sautées. Le planning avance, rien n'est rattrapé.

Le saut est voulu. Une fenêtre de maintenance de plusieurs heures ne doit pas se terminer par une avalanche de toutes les exécutions manquées. Le revers : une tâche quotidienne à 02:45 ne tourne pas ce jour-là si la maintenance reste active toute la nuit. C'est pourquoi Croniq journalise chaque échéance sautée et la compte dans une métrique Prometheus par tâche. Après la fenêtre, on sait clairement quelles tâches relancer à la main si besoin. Pour un déploiement de quelques minutes, cela ne concerne en pratique que des tâches à haute fréquence dont la prochaine exécution arrive de toute façon bientôt.

Quand le déploiement échoue

Si un déploiement échoue, la maintenance reste volontairement active. C'est du fail-closed : tant que personne ne sait dans quel état se trouve la stack, aucune tâche ne doit tourner contre elle. Pour que cela ne devienne pas un état permanent silencieux, il existe trois signaux :

  • GET /health signale la maintenance sans authentification, à côté du statut. Le statut lui-même reste ok, afin qu'aucun health check ne redémarre le serveur simplement parce qu'il fait ce qu'on lui a demandé.
  • Une règle d'alerte maintenance_active se déclenche lorsque la maintenance est active depuis plus longtemps qu'une durée définie. Chez nous, c'est 30 minutes, signalées par les mêmes canaux que les tâches en échec.
  • Le chemin d'échec de la pipeline vérifie lui-même à la fin si la maintenance est encore active et l'indique de façon bien visible dans le résumé de l'exécution.

Le prochain déploiement réussi met fin à la maintenance, y compris un retour à la version précédente. Qui ne veut pas attendre la désactive dans le tableau de bord.

Un détail facile à manquer : pendant la maintenance, Croniq suspend les alertes de vivacité, c'est-à-dire les règles pour les exécutions manquées, en retard et bloquées. Pendant un déploiement, les échéances en retard sont le but recherché et non une erreur de l'ordonnanceur. Sans cette règle, chaque déploiement un peu plus long déclencherait une fausse alerte pour chaque tâche active.

Des erreurs avant le serveur : la gate de configuration

Toutes les tâches, tous les plannings et toutes les règles d'alerte se trouvent dans le Croniqfile, un langage de configuration dédié. Croniq traite les directives inconnues comme des erreurs et non comme des avertissements. C'est juste, mais cela a une conséquence : si le Croniqfile utilise une directive que le serveur livré ne connaît pas encore, celui-ci rejette tout le fichier et ne démarre pas. Si l'application dépend de l'ordonnanceur via un health check, elle ne démarre pas non plus.

C'est pourquoi une gate dédiée tourne dans la CI. Elle démarre exactement l'image Croniq qui sera livrée et y exécute croniq validate sur le Croniqfile. Elle vérifie aussi que tous les endroits où la version de Croniq est fixée pointent vers le même tag. Une erreur apparaît ainsi dans la pull request et non lors du déploiement sur le serveur.

Le trou après le déploiement : run_on_register

Reste le troisième point du début : les tâches dont la définition change avec la release. Une tâche de rapprochement qui tourne chaque jour à 01:30 ne prend en compte une logique modifiée que la nuit suivante.

La directive run_on_register règle ce cas. Une tâche ainsi marquée s'exécute exactement une fois lorsque Croniq adopte sa définition : la première fois, puis chaque fois que la définition compilée de la tâche change. Un simple redémarrage ne la déclenche pas. Si l'adoption tombe pendant la maintenance, l'exécution est retenue et démarre dès la fin de la maintenance, c'est-à-dire précisément quand la nouvelle stack est en bonne santé.

IaC first, mais avec une issue de secours

Derrière tout cela se trouve un principe : l'état cible vit dans le Croniqfile, versionné dans Git et relu comme n'importe quel autre code. Dans le tableau de bord, les tâches issues du Croniqfile sont marquées et ne peuvent pas être modifiées directement, car une modification y serait silencieusement écrasée au prochain rechargement.

En exploitation, il faut pourtant des moyens d'intervenir qui ne peuvent pas attendre le prochain déploiement. Croniq les sépare clairement :

  • Les actions opérationnelles ne changent pas la définition mais le comportement du moment : lancer ou annuler une tâche manuellement, rejouer une dead letter, activer ou désactiver la maintenance.
  • Les dérogations aux alertes mettent une règle en sourdine pour une durée donnée, la désactivent ou modifient sa fenêtre de limitation. Chacune exige une justification consignée dans le journal d'audit, et les dérogations limitées dans le temps expirent d'elles-mêmes.
  • L'adoption est la sortie volontaire du Croniqfile pour une tâche donnée. Elle doit être autorisée au niveau du serveur, rend la tâche modifiable via l'API et le tableau de bord, et peut être annulée. Ensuite, la définition du Croniqfile s'applique à nouveau.

Le Croniqfile reste ainsi la source fiable, sans que l'administrateur ait à attendre une pull request en cas d'urgence.

Ce qu'il faut retenir

Les releases quotidiennes ne sont pas un problème d'ordonnanceur, tant que l'ordonnanceur sait qu'une livraison est en cours. Le mode maintenance fait de la fenêtre de déploiement un état explicite avec des règles claires, la gate déplace les erreurs de configuration dans la pull request, et run_on_register comble le trou qui suit. Ces trois éléments sont nés de l'exploitation en production et sont disponibles pour tous dans Croniq.

Appeler