
Chef exécute un cookbook en deux temps : il le compile entièrement, puis il fait converger les ressources. Ignorer cette mécanique conduit à des surprises : du code Ruby qui s'exécute « trop tôt », une ressource qui refait son travail à chaque passage. Ce guide ouvre le capot de la convergence : les deux phases, le piège du Ruby en compilation, les gardes d'idempotence, et le mode unifié des versions récentes. Public visé : lecteur ayant pratiqué ressources, recettes et attributs. Testé avec CINC Client 19.3.14 et kitchen-dokken sur Ubuntu 24.04.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Distinguer la phase de compilation de la phase de convergence.
- Repérer une ressource non idempotente et la corriger.
- Utiliser les gardes
not_ifetonly_if. - Comprendre ce qu'apporte le mode unifié des Chef récents.
Prérequis
Section intitulée « Prérequis »- Les ressources, recettes et attributs pratiqués.
- Un cookbook avec la configuration dokken des guides précédents.
Les deux phases : compilation puis convergence
Section intitulée « Les deux phases : compilation puis convergence »Une exécution de cinc-client se déroule en deux temps :
- La compilation : Chef lit toute la recette de haut en bas, exécute le Ruby rencontré et construit la liste des ressources. Rien n'est encore appliqué à la machine.
- La convergence : Chef parcourt cette liste et applique chaque ressource dans l'ordre, en ne changeant que ce qui doit l'être.
La conséquence est capitale : le Ruby « nu » (hors d'un bloc de ressource) s'exécute pendant la compilation, donc avant que la moindre ressource ait convergé.
Le piège du Ruby exécuté en compilation
Section intitulée « Le piège du Ruby exécuté en compilation »Regardons ce piège en action. Cette recette crée un fichier, puis teste sa présence avec du Ruby :
file "/tmp/marqueur.txt" do content "present\n"end
if ::File.exist?("/tmp/marqueur.txt") log "compilation : le fichier existe deja"else log "compilation : le fichier n'existe pas encore"endÀ la première convergence, le test tombe sur la branche « n'existe pas encore », alors que la ressource file juste au-dessus va bel et bien créer le fichier :
* log[compilation : le fichier n'existe pas encore] action writeRelancez : le message change :
* log[compilation : le fichier existe deja] action writeLe if n'a pas été évalué au moment où on le lit dans le code, mais en compilation, avant la convergence : au premier passage le fichier n'existait pas encore, au second il avait été créé par le passage précédent. Ne fondez jamais une décision sur l'état que vos propres ressources produisent : au moment du test Ruby, elles n'ont pas encore convergé.
Une ressource non idempotente
Section intitulée « Une ressource non idempotente »L'idempotence n'est pas automatique. La ressource execute lance une commande, et sans garde, elle la relance à chaque convergence :
execute "horodatage" do command "date >> /tmp/journal.log"endConvergez deux fois : la commande tourne les deux fois.
# 1er passage* execute[horodatage] action runInfra Phase complete, 1/1 resources updated
# 2e passage* execute[horodatage] action runInfra Phase complete, 1/1 resources updatedChaque exécution ajoute une ligne au journal : ce cookbook n'est pas idempotent. Contrairement à package, file ou service qui savent comparer l'état voulu à l'état courant, execute ne sait pas, par défaut, si son travail est déjà fait.
Rendre une ressource idempotente : les gardes
Section intitulée « Rendre une ressource idempotente : les gardes »On ajoute une garde qui dit à Chef quand l'action est nécessaire. not_if empêche l'exécution si sa condition est vraie ; only_if ne l'autorise que si sa condition est vraie.
execute "horodatage" do command "date >> /tmp/journal.log" not_if { ::File.exist?("/tmp/journal.log") }endCette fois, la commande ne tourne qu'une fois :
# 1er passage : la garde est fausse, la commande tourne* execute[horodatage] action runInfra Phase complete, 1/1 resources updated
# 2e passage : la garde est vraie, la commande est ignorée* execute[horodatage] action run (skipped due to not_if)Infra Phase complete, 0/1 resources updatedskipped due to not_if : la garde a fait son travail. Une garde peut tester un fichier (not_if { ::File.exist?(...) }), une commande shell (not_if "test -f /tmp/journal.log"), ou pour les commandes qui créent un fichier, la propriété creates suffit.
Le mode unifié des Chef récents
Section intitulée « Le mode unifié des Chef récents »Le modèle en deux phases vaut pour les recettes. À l'intérieur d'une ressource personnalisée, les versions récentes de Chef (et de CINC) activent par défaut le mode unifié (unified_mode) : compilation et convergence y sont fusionnées en une seule passe, dans l'ordre d'écriture.
L'intérêt est concret : à l'intérieur d'une ressource personnalisée, le Ruby et les sous-ressources s'exécutent dans l'ordre où vous les lisez, ce qui supprime toute une classe de bugs d'ordre et de notification. Vous profiterez du mode unifié quand vous écrirez vos propres ressources, sujet traité dans un guide dédié. Retenez pour l'instant que les recettes gardent les deux phases, et que le mode unifié concerne les ressources personnalisées.
Vérifier l'idempotence
Section intitulée « Vérifier l'idempotence »La preuve reste la même depuis votre premier cookbook : un second kitchen converge à 0 ressource mise à jour. C'est le test à faire systématiquement avant de considérer un cookbook comme fini.
Pour anticiper sans rien modifier, Chef propose le mode why-run (cinc-client --why-run) : il annonce ce qu'il changerait sans l'appliquer. Utile pour inspecter un cookbook hérité avant de le lancer pour de vrai.
Exercice : rendre un cookbook idempotent
Section intitulée « Exercice : rendre un cookbook idempotent »Un collègue vous laisse cette ressource. Corrigez-la. Cherchez la solution avant d'ouvrir la réponse.
Cette ressource recrée un fichier de configuration à chaque convergence :
execute "generer-config" do command "echo 'genere le' $(date) > /etc/app.conf"endAjoutez une garde pour qu'elle ne s'exécute qu'une fois : convergez, la commande tourne ; reconvergez, elle est ignorée.
Indice : le fichier /etc/app.conf prouve que le travail est fait.
On garde l'exécution sur l'existence du fichier produit :
execute "generer-config" do command "echo 'genere le' $(date) > /etc/app.conf" not_if { ::File.exist?("/etc/app.conf") }endAu second passage : execute[generer-config] action run (skipped due to not_if), soit 0 ressource mise à jour. Pour une commande qui crée un fichier, creates "/etc/app.conf" donne le même résultat de façon plus lisible.
À retenir
Section intitulée « À retenir »- Chef s'exécute en deux phases : compilation (le Ruby s'exécute, la liste des ressources se construit) puis convergence (les ressources s'appliquent).
- Le Ruby nu s'exécute en compilation, avant toute convergence : ne décidez rien sur l'état que vos ressources vont produire.
- Une ressource
executesans garde n'est pas idempotente : elle retourne à chaque passage. - Les gardes
not_ifetonly_if(oucreates) rendent une commande idempotente. - Le mode unifié fusionne les deux phases dans les ressources personnalisées ; les recettes gardent le modèle en deux phases.
- La preuve d'idempotence reste un second passage à 0 ressource mise à jour.