SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-21

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 options. Le --limit est intentionnel. Le diff d’un hôte est lisible. Le diff de vingt hôtes est quelque chose que vous faites défiler.

Quatre mots de résultat couvrent l’ensemble du rapport.

  • ok: [web1] signifie que le module a vérifié l’état et qu’il correspond déjà à la configuration. Aucun changement ne serait effectué.
  • changed: [web1] signifie que le module aurait écrit quelque chose. Avec --diff, les lignes précédentes indiquent quoi.
  • skipping: [web1] signifie que la tâche n’a pas été évaluée. Soit un when était faux, soit le module ne peut pas s’exécuter en mode check.
  • 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 manipulent des fichiers. Les lignes supprimées portent - et les lignes ajoutées portent +. 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. ansible.builtin.user indique donc les attributs qui seraient modifiés, et non le contenu du fichier.

Activez diff de manière permanente dans ansible.cfg pour ne jamais oublier l’option :

[diff]
always = true
context = 5

Deux vérifications plus rapides s’effectuent avant le mode check. ansible-playbook site.yml --syntax-check analyse le YAML et la structure du play sans contacter un seul hôte. ansible-playbook site.yml --list-tasks affiche les tâches qui seraient exécutées. Cela permet de découvrir qu’un rôle que vous pensiez associé à un tag ne l’est pas. Aucune des deux commandes ne se connecte. Elles s’exécutent donc instantanément.

Le mode check se connecte réellement. Il ouvre une connexion SSH vers chaque hôte du pattern et collecte les facts. Un hôte indisponible fait donc échouer la simulation. Ce signal est utile en lui-même. C’est aussi pourquoi il faut déterminer ce qu’un playbook doit faire avec les hôtes injoignables avant d’intégrer une simulation à la CI.

Pourquoi le mode check échoue sur un serveur vierge

Ce play est correct. Exécutez-le avec --check sur un serveur qui n’a pas encore nginx, et 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: true

La tâche apt indique changed, et c’est normal : 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 ne permet d’interroger ce service. Aucun de ces échecs ne provient d’un bug dans le playbook. L’exécution à blanc ne disposait pas de l’état nécessaire. C’est ce qu’indique la documentation lorsqu’elle précise que le mode check ne peut pas produire une sortie utile pour une tâche dont l’entrée dépend de la modification effectuée par une tâche précédente.

La règle exacte est donc la suivante : le mode check est fiable sur un hôte sur lequel le playbook a déjà convergé, mais il produit beaucoup de messages sur un hôte vierge. Une exécution --check dans laquelle chaque tâche indique ok fournit une information réelle sur un hôte convergé : rien ne serait modifié. Sur un hôte tout neuf, --check indique surtout que l’hôte est nouveau. Lorsque vous rédigez votre premier playbook Ansible pour un VPS, attendez-vous à ce que la première exécution à blanc affiche une longue liste d’erreurs, puis évaluez le playbook à partir de la deuxième.

Pourquoi les tâches de commande et de 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 pas de méthode en lecture seule pour 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 proposée est creates et removes. Donnez à la tâche un chemin creates. Le mode check pourra alors au moins évaluer le test de 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/app

Si /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. Il s’agit également d’une réponse réelle. Sans creates, cette tâche ne produit aucune information pendant votre dry run.

La conséquence est plus grave que cette absence d’information. Une tâche ignorée enregistre tout de même un résultat. Mais il s’agit d’un résultat d’ignorance qui ne possède pas de clé stdout. La condition de la tâche suivante échoue alors lors de son évaluation, avec une erreur proche de 'dict object' has no attribute 'stdout'. Votre playbook fonctionne lors d’une exécution réelle, mais échoue pendant le dry run. C’est le comportement le plus déroutant de cette fonctionnalité.

check_mode : false, et le seul endroit où il doit être utilisé

check_mode: false sur une tâche signifie « l’exécuter réellement, même sous --check ». C’est la correction du problème des commandes ignorées. Cette option est sûre uniquement pour une tâche qui lit.

- name: Read the installed app version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  check_mode: false
  changed_when: false

Cette tâche est fiable 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, et check_mode: false fait exister app_version.stdout pendant une exécution à blanc. Les conditions qui en dépendent peuvent donc toujours être évaluées.

Lisez le mot-clé littéralement avant de le copier ailleurs. Une tâche avec check_mode: false écrit sur vos serveurs pendant ansible-playbook --check. Si vous l’ajoutez à une tâche apt ou à une tâche template pour rendre une exécution à blanc plus propre, cette exécution n’est plus une exécution à blanc. Lorsqu’une tâche d’écriture ne peut pas être rendue sûre, protégez-la plutôt :

- name: Apply the database migration
  ansible.builtin.command: /usr/local/bin/app migrate --apply
  when: not ansible_check_mode

ansible_check_mode est une variable magique qu’Ansible définit sur 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 une sonde de dérive : enregistrez le résultat, et un rapport changed signifie que l’hôte ne correspond plus à ce que demande la tâche.

Pourquoi une tâche signale une modification à chaque exécution

Exécutez le playbook deux fois de suite, sans rien modifier entre les deux. Toutes les tâches doivent signaler ok lors de la deuxième exécution. Toute tâche qui signale encore changed indique l’un des deux problèmes suivants : 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. Il ne faut pas masquer ces changements.

  • command et shell sans creates, removes ni changed_when signalent changed à chaque exécution, car le module ne peut pas déterminer si une modification a eu lieu. Ajoutez creates ou définissez changed_when sur une chaîne présente dans la sortie.
  • ansible.builtin.file avec state: touch signale changed à chaque exécution par conception, car la modification d’un fichier met à jour ses horodatages. Utilisez state: file si votre seul objectif était de définir le propriétaire ou le mode.
  • Un template dont la sortie générée change réécrit le fichier à chaque exécution. Un horodatage provenant de ansible_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.user avec password: "{{ pw | password_hash('sha512') }}" change à chaque exécution, car password_hash choisit un salt aléatoire à chaque appel. Le hash obtenu ne correspond donc jamais à celui déjà présent dans /etc/shadow. Fournissez un salt explicite dérivé d’une valeur stable.
  • state: latest sur un module de gestion de paquets signale changed lorsqu’une mise à niveau est disponible. Ce comportement est attendu. C’est aussi pourquoi state: latest produit un playbook dont le résultat est imprévisible. Utilisez state: present et effectuez les mises à niveau volontairement.
  • ansible.builtin.unarchive pointant vers une URL sans creates télécharge et extrait de nouveau le contenu. Fournissez-lui un chemin creates.

--diff est le moyen le plus rapide de distinguer ces cas. Si une tâche indique changed et que le diff montre des octets différents, votre entrée n’est pas stable. Si elle indique changed et que le diff ne montre aucune différence, le module ne peut pas représenter ce qu’il a modifié. Il s’agit généralement d’une tâche command ou d’une écriture limitée aux métadonnées, comme la mise à jour d’un horodatage.

N’utilisez pas changed_when: false pour faire taire une tâche bruyante. Cette option masque le rapport. Ainsi, notify ne s’exécute jamais et le handler qui redémarre le service ne s’exécute pas non plus. Corrigez plutôt la tâche.

Réduire le rayon d’impact : --limit, --tags et --step

Le mode check indique les changements qui seraient effectués. 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:, donc --limit web1 et --limit 'webservers:!web3' fonctionnent tous les deux. Mettez le motif entre guillemets. Un ! sans guillemets 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 voie.

Vérifiez le motif avant de lui faire confiance. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts affiche les hôtes correspondants, puis quitte 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 quitte avec une erreur précisant que les hôtes et --limit ne correspondent à aucun hôte. Savoir comment le fichier d’inventaire définit ces groupes permet de rendre un motif prévisible dès le départ.

--tags deploy exécute uniquement les tâches marquées, tandis que --skip-tags packages exécute toutes les autres. --list-tags affiche les éléments 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 découper 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-le pour récupérer l’exécution, mais comprenez le 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 votre réponse : yes, no ou continue. Cette option est lente, mais c’est le bon outil lors de la première exécution d’une opération destructive. Vous pouvez ainsi vous arrêter entre deux tâches plutôt qu’après vingt tâches.

Déployer la modification en mode séquentiel

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 tâche défectueuse atteint toute la flotte dans la même seconde. Le temps de lire l’erreur et d’appuyer sur Ctrl-C, la modification est déjà partout.

serial divise 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: webapp

Le premier lot contient un hôte. S’il fonctionne correctement, 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 play dès qu’un hôte d’un lot échoue. Une release défectueuse s’arrête donc sur une seule machine. any_errors_fatal: true est la version plus radicale : il arrête le play pour tout le monde dès qu’un premier hôte échoue.

Commencer par un seul hôte n’est pas une précaution excessive. La raison est précise. Les groupes d’inventaire évoluent. Un serveur ajouté six mois après les autres peut utiliser une autre release de distribution, héberger un service installé manuellement ou avoir une disposition différente de ses disques. Le playbook est correct pour le groupe, mais incorrect pour cet hôte. Aucun dry run effectué sur un hôte conforme ne le révélera. Gérer une flotte de serveurs Linux consiste en grande partie à détecter l’hôte atypique avant que la modification ne l’atteigne.

L’ordre d’exécution

  1. ansible-playbook site.yml --syntax-check détecte les erreurs YAML et les erreurs de structure sans aucun accès réseau.
  2. ansible-playbook site.yml --limit web1 --list-hosts vérifie que votre motif correspond bien à ce que vous pensez.
  3. ansible-playbook site.yml --limit web1 --check --diff effectue une simulation. Lisez le diff.
  4. ansible-playbook site.yml --limit web1 --diff l’applique à cet hôte uniquement.
  5. Exécutez de nouveau l’étape 4. Tout doit retourner ok. Tout ce qui retourne encore changed doit être corrigé avant de l’appliquer au reste du parc.
  6. ansible-playbook site.yml --check --diff sur 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

ansible-playbook --check modifie-t-il quoi que ce soit 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 roles 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 le changement et affiche les lignes modifiées. Utilisez-les ensemble pour obtenir un dry run réellement lisible. Laissez aussi --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 change à chaque exécution. command et shell signalent toujours changed, sauf si vous ajoutez creates ou changed_when. file avec state: touch change par conception. Un template qui génère un timestamp 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. Tout ce qui est encore changed lors du second passage correspond à la tâche à corriger.

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 sur le fichier. Pour une tâche qui se contente de lire l’état, définissez check_mode: false avec changed_when: false. Le résultat enregistré existe ainsi toujours pendant le dry run, et les conditions qui en dépendent continuent 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 attendu. Le mode check détecte les écarts sur des hôtes que le playbook a déjà mis en conformité. Il ne peut pas valider une première exécution. Sur un nouvel hôte, appliquez le playbook à une machine, puis analysez le second passage.

#ansible#check-mode#idempotency#automation#safety