Ansible : que valent vraiment --check et --diff ?
Découvrez ce que prouvent réellement --check et --diff dans Ansible, et pourquoi certains modules peuvent rendre la simulation trompeuse avant l’exécution réelle.
Ce que fait le check mode d’Ansible
Le check mode d’Ansible est une exécution à blanc : ansible-playbook --check se connecte à chaque hôte du play, demande à chaque module si l’état actuel correspond déjà à l’état demandé, puis indique ce qui changerait sans rien écrire. Ajoutez --diff pour afficher également le contenu avant et après des fichiers qui seraient modifiés. Ensemble, ces options répondent à la question à poser avant toute exécution réelle : qu’est-ce qui va changer sur ces serveurs ?
Le check mode ne simule pas votre playbook. Il n’existe aucun modèle du serveur. Chaque module est simplement invité à lire au lieu d’écrire. Un module capable de répondre en lecture seule indique changed, puis continue. Un module qui ne le permet pas ne fait rien et n’indique rien. La documentation Ansible le résume en une phrase : « Les modules qui ne prennent pas en charge le check mode n’indiquent rien et ne font rien. » C’est cette limite qui peut rendre une exécution à blanc trompeuse ; l’essentiel de ce guide porte donc sur cette limite.
Exécuter la simulation : --check et --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C et -D sont les formes courtes des deux flags. Le --limit est volontaire. Le diff d’un seul hôte est lisible. Le diff de vingt hôtes est quelque chose que l’on fait défiler.
Quatre mots de résultat suffisent pour comprendre tout le rapport.
ok: [web1]signifie que la tâche s’est exécutée sans signaler de changement. Pour un module qui gère un état, commeaptoutemplate, cela signifie qu’il a vérifié l’état et que celui-ci correspond déjà à l’état attendu. Pourdebugouset_fact, cela signifie seulement que la tâche s’est exécutée.changed: [web1]signifie que le module prévoit d’avoir écrit quelque chose. Avec--diff, les lignes précédentes indiquent quoi. La plupart des modules prédisent correctement le résultat, mais certains modules dont la prise en charge du check mode est partielle le signalent dans leur documentation :get_urlenvoie uniquement une requête HEAD en check mode et précise qu’il indiquera un statut changed incorrect.skipping: [web1]signifie que la tâche ne s’est pas exécutée. Soit sonwhena été évalué et a renvoyé false, soit le module ne peut pas s’exécuter en check mode.fatal: [web1]signifie que la tâche a échoué pendant la vérification. Lisez le message avant de conclure que le playbook est défectueux.
--diff affiche un diff unifié pour les modules qui gèrent des fichiers. Les lignes supprimées sont marquées par - et les lignes ajoutées par +. Elles apparaissent sous un en-tête dont les lignes commencent par --- before et +++ after et indiquent le chemin de destination. Les modules qui n’écrivent pas de fichiers affichent leur propre état avant et après. Ainsi, ansible.builtin.user affiche les attributs qui seraient modifiés plutôt que le contenu du fichier.
Activez diff de façon permanente dans ansible.cfg pour ne jamais oublier le flag :
[diff]
always = true
context = 5Deux vérifications moins coûteuses doivent précéder le check mode. ansible-playbook site.yml --syntax-check analyse le YAML et la structure du play sans contacter le moindre hôte. ansible-playbook site.yml --list-tasks affiche les tâches qui seraient exécutées. C’est ainsi que vous découvrez qu’un rôle que vous pensiez associé à un tag ne l’est pas. Aucune de ces commandes n’établit de connexion : leur exécution est donc instantanée.
Le check mode lui-même établit des connexions. Il ouvre une connexion SSH vers chaque hôte du pattern et collecte les facts. Un hôte hors ligne fait donc échouer la simulation. C’est déjà un signal utile. C’est aussi pourquoi déterminer ce qu’un playbook doit faire des hôtes injoignables est important avant d’intégrer une simulation à la CI.
Pourquoi le mode check échoue sur un serveur neuf
Ce playbook est correct. Exécutez-le avec --check sur un serveur qui n’a pas encore nginx : la plupart des tâches échouent.
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueLa tâche apt indique changed, et c’est exact : le paquet est absent, donc une exécution réelle l’installerait. Le mode check ne l’a pas installé. La tâche template échoue ensuite, car /etc/nginx/conf.d/ n’existe pas sur cet hôte et rien ne l’a créé. La tâche service échoue également, car aucune unité nginx n’est disponible pour la requête. Aucun de ces échecs n’est un bug du playbook. Le dry run ne disposait pas de l’état requis. C’est ce que signifie l’avertissement de la documentation : le mode check ne peut pas produire de résultat utile pour une tâche dont l’entrée dépend d’une modification effectuée par une tâche précédente.
La règle complète est donc la suivante : le mode check est plus fiable sur un hôte que le playbook a déjà amené à l’état attendu, et il produit davantage de bruit sur un hôte neuf. Une exécution --check dans laquelle chaque tâche indique ok est une information réelle sur un hôte arrivé à l’état attendu : rien ne serait modifié. Deux réserves s’appliquent toutefois à tous les hôtes : une tâche command est une zone vide, pas une réponse, et un module dont la prise en charge du mode check est partielle, comme get_url, peut encore signaler incorrectement son état changed. Sur un hôte entièrement neuf, --check indique surtout que l’hôte est nouveau. Lorsque vous écrivez votre premier playbook Ansible pour un VPS, attendez-vous à ce que le premier dry run soit entièrement rouge, puis évaluez le playbook à partir du deuxième.
Pourquoi les tâches command et shell sont ignorées en mode check
ansible.builtin.command et ansible.builtin.shell ne savent pas ce que fait votre commande. Il n’existe aucun moyen en lecture seule d’exécuter un binaire arbitraire. En mode check, le module refuse donc de l’exécuter. Le résultat de la tâche contient skipped: true et le message Command would have run if not in check mode. La sortie affiche skipping: [web1].
La documentation du module qualifie la prise en charge du mode check de « partielle ». La solution indiquée est creates et removes. Donnez à la tâche un chemin creates. Le mode check pourra alors au moins évaluer le test sur le fichier :
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/appSi /opt/app/bin/app existe déjà, le mode check indique Would not run command since '/opt/app/bin/app' exists. Il s’agit d’une réponse réelle. Si le chemin est absent, vous obtenez Command would have run if not in check mode, ce qui constitue également une réponse réelle. Sans creates, cette tâche reste vide lors de votre dry run.
Les conséquences vont au-delà de cette absence d’exécution. Une tâche command ignorée enregistre tout de même un résultat. Ce résultat contient bien stdout, mais sa valeur est une chaîne vide, car rien n’a été exécuté. La condition de la tâche suivante est évaluée avec cette chaîne vide et prend silencieusement la mauvaise branche : un contrôle de version ne trouve aucune version et un test tel que 'active' in svc.stdout renvoie false. Votre playbook fournit une réponse lors de l’exécution réelle et une autre lors du dry run, sans aucune erreur permettant d’identifier le problème. C’est l’échec le plus déroutant de toute cette fonctionnalité. L’erreur 'dict object' has no attribute 'stdout' correspond à un autre type d’ignorance : une tâche ignorée parce que son when était false enregistre un résultat qui ne contient aucune clé stdout.
check_mode: false, et l’unique emplacement où l’utiliser
check_mode: false sur une tâche signifie « exécuter réellement cette tâche, même sous --check ». C’est la solution au problème des commandes ignorées. Elle est sûre uniquement pour une tâche qui effectue une lecture.
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: falseCette tâche reste correcte dans les deux modes. Elle lit une version et n’écrit jamais. changed_when: false l’empêche de signaler une modification qu’elle n’a pas effectuée. check_mode: false fait conserver à app_version.stdout la version réelle pendant une simulation, plutôt qu’une chaîne vide. Les conditions qui en dépendent continuent donc à être évaluées correctement.
Lisez littéralement ce mot-clé avant de le copier ailleurs. Une tâche avec check_mode: false écrit sur vos serveurs pendant ansible-playbook --check. L’ajouter à une tâche apt ou à une tâche template rend la simulation plus propre en apparence, mais cette simulation n’en est alors plus une. Lorsqu’une tâche d’écriture ne peut pas être rendue sûre, protégez-la avec une condition :
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode est une variable spéciale qu’Ansible définit à true pendant une exécution en mode check. Le mot-clé inverse existe également. check_mode: true force toujours une tâche à s’exécuter en mode check, même pendant une exécution réelle. Elle devient ainsi un outil de détection de dérive : enregistrez le résultat ; un rapport changed signifie que l’hôte ne correspond plus à ce que la tâche demande.
Pourquoi une tâche signale une modification à chaque exécution
Exécutez le playbook deux fois de suite, sans rien faire entre les deux. Chaque tâche doit signaler ok lors de la deuxième exécution. Toute tâche qui signale encore changed indique l’un de deux problèmes : le module ne peut pas voir l’état qu’il gère, ou l’entrée que vous lui fournissez n’est pas stable. Les deux problèmes se corrigent. Ce ne sont pas des messages à masquer.
commandetshellsanscreates,removesouchanged_whensignalentchangedà chaque exécution, car le module ne peut pas savoir si quelque chose s’est produit. Ajoutezcreates, ou définissezchanged_whenavec une chaîne présente dans la sortie.ansible.builtin.fileavecstate: touchsignalechangedà chaque exécution, par conception, car la modification d’un fichier met à jour ses horodatages. Utilisezstate: filesi vous vouliez seulement définir le propriétaire ou le mode.- Un
templatedont le rendu change réécrit le fichier à chaque exécution. Un horodatage provenant deansible_date_time, un appel ànow()ou un mot de passe généré à chaque fois produisent des octets différents. Le module signale donc correctement une modification. Retirez la valeur variable du template. ansible.builtin.useravecpassword: "{{ pw | password_hash('sha512') }}"change à chaque exécution, carpassword_hashchoisit un salt aléatoire à chaque appel. Le hash obtenu ne correspond donc jamais à celui qui se trouve déjà dans/etc/shadow. Fournissez un salt explicite dérivé d’une valeur stable.state: latestsur un module de gestion de paquets signalechangedlorsqu’une mise à niveau est disponible. Ce comportement est normal. C’est aussi pourquoistate: latestproduit un playbook dont le résultat est imprévisible. Utilisezstate: presentet effectuez la mise à niveau volontairement.ansible.builtin.unarchivequi pointe vers une URL sanscreatestélécharge de nouveau l’archive à chaque exécution. Pour les formats d’archive, il ne peut pas non plus comparer la destination, qu’il extrait de nouveau. Fournissez-lui un chemincreates.
--diff est le moyen le plus rapide de distinguer ces cas. Si une tâche indique changed et que le diff affiche des octets différents, votre entrée n’est pas stable. Si elle indique changed et que le diff n’affiche rien, le module ne peut pas représenter la modification effectuée. Il s’agit généralement d’une tâche command ou d’une écriture limitée aux métadonnées, comme un horodatage.
N’utilisez pas changed_when: false pour faire taire une tâche bruyante. Cette option masque le résultat. notify ne s’exécute alors jamais et le handler qui redémarre le service n’est jamais exécuté. Corrigez plutôt la tâche.
Réduire le rayon d’impact : --limit, --tags et --step
Le mode de vérification indique ce qui changerait. Ces options déterminent le nombre de machines concernées simultanément.
--limit limite le play à un sous-ensemble de l’inventaire. Il accepte les mêmes motifs que hosts:. Vous pouvez donc utiliser --limit web1 et --limit 'webservers:!web3'. Mettez le motif entre guillemets. Un ! non protégé dans une session bash interactive déclenche l’expansion de l’historique sur le point d’exclamation, et votre shell réécrit la commande avant même qu’Ansible ne la reçoive.
Vérifiez le motif avant de lui faire confiance. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts affiche les hôtes correspondants, puis se termine sans se connecter à aucun d’eux. Un motif qui ne correspond à aucun hôte est sans danger, car Ansible ne revient pas à l’ensemble de l’inventaire. Il affiche un avertissement indiquant qu’il n’a pas pu faire correspondre le motif d’hôtes, puis se termine avec une erreur précisant que l’inventaire, le motif d’hôtes et --limit ne laissent aucun hôte à cibler. Savoir comment le fichier d’inventaire définit ces groupes rend un motif prévisible dès le départ.
--tags deploy exécute uniquement les tâches associées aux tags, tandis que --skip-tags packages exécute toutes les autres tâches. --list-tags affiche les tags disponibles. Les tags deviennent utiles lorsqu’un play dépasse le stade où vous acceptez de l’exécuter entièrement. C’est également l’une des raisons de diviser un long playbook en rôles.
--start-at-task "Write the site config" reprend une exécution en échec à partir d’une tâche nommée. Utilisez cette option pour récupérer l’exécution, mais tenez compte de son coût : tout ce qui précède cette tâche est ignoré, y compris les tâches qui définissent des facts ou enregistrent les variables lues par les tâches suivantes.
--step demande une confirmation avant chaque tâche et attend une réponse yes, no ou continue. Cette option est lente. Elle convient lorsque vous exécutez pour la première fois une opération destructive, car vous pouvez vous arrêter entre deux tâches plutôt qu’après vingt tâches.
Déployer la modification en série
Par défaut, Ansible exécute une tâche sur chaque hôte du play avant de passer à la tâche suivante. C’est rapide, mais une mauvaise tâche atteint toute la flotte dans la même seconde. Lorsque vous avez lu l’erreur et appuyé sur Ctrl-C, la modification est déjà partout.
serial découpe le play en lots. Le play complet s’exécute sur le premier lot, puis sur le suivant.
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webappLe premier lot contient un hôte. S’il passe sans problème, le deuxième lot en contient cinq, puis chaque lot suivant représente 30 pour cent des hôtes du play. max_fail_percentage: 0 arrête le reste du play dès qu’un hôte d’un lot échoue, car le seuil doit être dépassé et tout échec dépasse zéro. La garantie d’un seul hôte vient du premier lot, pas de ce paramètre : une release qui casse partout échoue sur l’hôte unique du premier lot et s’arrête là. Un échec qui apparaît uniquement dans le lot de cinq hôtes a déjà atteint les autres hôtes de ce lot, car Ansible exécute chaque tâche sur l’ensemble du lot avant de passer à la suivante. any_errors_fatal: true est la version plus brutale : elle arrête le play pour tout le monde dès l’échec du premier hôte.
Commencer par un seul hôte n’est pas de la paranoïa, et la raison est précise. Les groupes d’inventaire évoluent. Un serveur ajouté six mois après les autres peut exécuter une autre release de distribution, avoir un service installé manuellement par quelqu’un ou utiliser une organisation différente de ses disques. Le playbook est correct pour le groupe, mais incorrect pour cet hôte, et aucun dry run sur un hôte conforme ne le montrera. Gérer une flotte de serveurs Linux consiste en grande partie à repérer l’hôte atypique avant que la modification ne l’atteigne.
L’ordre d’exécution
ansible-playbook site.yml --syntax-checkdétecte les erreurs YAML et les erreurs de structure sans aucun accès réseau.ansible-playbook site.yml --limit web1 --list-hostsvérifie que votre motif correspond bien à ce que vous pensez.ansible-playbook site.yml --limit web1 --check --diffeffectue une simulation. Lisez le diff.ansible-playbook site.yml --limit web1 --diffl’applique à cet hôte uniquement.- Exécutez de nouveau l’étape 4. Tout doit retourner
ok. Tout ce qui retourne encorechangeddoit être corrigé avant de l’appliquer au reste du parc. ansible-playbook site.yml --check --diffsur l’inventaire complet renvoie maintenant un résultat exploitable, car les hôtes convergés restent silencieux et ce qui reste correspond au véritable écart.
Un avertissement concernant l’étape 3. --diff affiche le contenu des fichiers dans votre terminal et dans le journal de votre tâche CI. Ainsi, si un template génère le mot de passe d’une base de données, ce mot de passe est écrit dans le journal. Définissez diff: false sur cette tâche pour supprimer sa sortie, ou no_log: true pour masquer le résultat complet. Conservez également la valeur elle-même dans un fichier Ansible Vault chiffré plutôt que dans le dépôt.
FAQ
Est-ce que ansible-playbook --check modifie quelque chose sur le serveur ?
Non, à une exception près que vous contrôlez. En mode check, chaque module doit signaler les changements au lieu de les appliquer. Les modules qui ne savent pas fonctionner ainsi ne signalent rien et n’effectuent aucune action. L’exception est le mot-clé de tâche check_mode: false, qui force l’exécution réelle de cette tâche, même pendant une exécution --check. Recherchez check_mode: false dans vos playbooks et vos rôles avant de faire confiance à un dry run. Vérifiez que chaque occurrence correspond à une tâche qui se contente de lire l’état.
Quelle est la différence entre --check et --diff ?
--check détermine si des changements sont réellement appliqués. --diff détermine le niveau de détail affiché. --check seul indique qu’un fichier serait modifié. --diff seul applique la modification et affiche les lignes modifiées. Utilisez-les ensemble pour obtenir un dry run lisible. Laissez également --diff activé lors des exécutions réelles en définissant always = true sous [diff] dans ansible.cfg.
Pourquoi ma tâche Ansible signale-t-elle un changement à chaque exécution ?
Parce que le module ne peut pas voir l’état qu’il gère, ou parce que la valeur fournie est différente à chaque fois. command et shell signalent toujours changed, sauf si vous ajoutez creates ou changed_when. L’utilisation de file avec state: touch entraîne volontairement des changements. Un template qui génère un horodatage ou un mot de passe nouvellement généré produit des octets différents à chaque exécution. Le fichier est donc réellement réécrit. Exécutez le playbook deux fois de suite. Toute tâche qui signale encore changed lors de la deuxième exécution doit être corrigée.
Pourquoi mes tâches command et shell sont-elles ignorées pendant un dry run ?
Parce qu’il n’existe pas de méthode en lecture seule pour exécuter une commande arbitraire. En mode check, le module command définit skipped: true avec le message Command would have run if not in check mode. Ajoutez creates ou removes afin que le mode check puisse évaluer le test de fichier à la place. Pour une tâche qui se contente de lire l’état, définissez check_mode: false avec changed_when: false. Le résultat enregistré contiendra ainsi la sortie réelle pendant le dry run, et les conditions qui en dépendent continueront de fonctionner.
Pourquoi le mode check échoue-t-il sur un nouveau serveur, mais fonctionne-t-il sur un serveur existant ?
Parce que le mode check ne crée pas l’état dont dépendent les tâches suivantes. Lors d’un dry run sur un hôte sans nginx, l’installation est signalée comme changed, puis la tâche qui écrit dans /etc/nginx/conf.d/ échoue, car ce répertoire n’a jamais été créé. Ce comportement est normal. Le mode check détecte les écarts sur les hôtes que le playbook a déjà configurés. Il ne peut pas valider une première exécution. Sur un nouvel hôte, appliquez le playbook à une machine, puis examinez la deuxième exécution.