Aller au contenu
Infrastructure as Code medium

Idempotence cassée et tuning performances : creates, changed_when, pipelining, forks

75 min de lecture

Logo Ansible

Un playbook idempotent affiche changed=0 au second passage. Si vous voyez changed=N à chaque run, vos tâches mentent sur leur état, chaque exécution refait tout, casse les caches HTTP/CDN, redémarre des services inutilement, et fait perdre confiance dans le code. Cette page diagnostique les 3 anti-patterns les plus fréquents (shell sans creates:, lineinfile sans regexp:, command sans changed_when:), puis enchaîne sur le tuning performances SSH (pipelining + forks + ControlPersist) qui réduit typiquement le temps de run de 50 % à 60 %.

À la fin, vous saurez transformer un playbook bavard et lent en playbook idempotent et optimisé, et automatiser le test d'idempotence dans votre CI.

  • Identifier les modules non-idempotents par défaut (command, shell, raw, script).
  • Rendre idempotent un shell avec creates: ou removes:.
  • Forcer changed_when: sur un command de lecture.
  • Fixer un lineinfile qui duplique avec regexp:.
  • Mesurer avant/après avec ansible.posix.profile_tasks.
  • Tuner SSH : pipelining + forks + ControlPersist dans ansible.cfg.
  • Automatiser le test d'idempotence en CI.

Quatre modules toujours changed=1 sans configuration explicite :

ModulePourquoi non-idempotentFix
commandExécute toujours la commandechanged_when: ou creates:/removes:
shellIdem (avec shell)changed_when: ou creates:/removes:
rawBypass Python, pas d'étatRéservé au bootstrap, sinon switcher vers un module
scriptExécute un script localcreates:/removes:

À l'inverse, copy, template, lineinfile, file, dnf, systemd sont idempotents par construction : ils comparent l'état désiré à l'état réel.

- name: Poser le marqueur de passage
ansible.builtin.shell: "echo lab > /tmp/marker"

Run 1 : changed=1. Run 2 : changed=1. Run 3 : changed=1. Anti-pattern, refait à l'infini.

- name: Poser le marqueur de passage
ansible.builtin.shell: "echo lab > /tmp/marker"
args:
creates: /tmp/marker # ← skip si le fichier existe

Run 1 : changed=1. Run 2 : ok (skip). Idempotent.

Variante inverse, pour une commande qui supprime :

- name: Purger l'ancien cache
ansible.builtin.command:
cmd: rm -rf /tmp/old-cache
removes: /tmp/old-cache # ← skip si le fichier n'existe PAS

Cet exemple mérite une réserve : il montre comment rattraper l'idempotence d'une commande, mais supprimer un répertoire n'a aucune raison de passer par une commande. Le module dédié le fait déjà, et il est idempotent par construction, sans removes: à gérer.

- name: Purger l'ancien cache (la bonne façon)
ansible.builtin.file:
path: /tmp/old-cache
state: absent

Anti-pattern 2, command de lecture sans changed_when:

Section intitulée « Anti-pattern 2, command de lecture sans changed_when: »
- name: Relever la version de l'application
ansible.builtin.command: /usr/local/bin/myapp --version
register: app_version

Le module command retourne toujours changed=1, alors qu'on n'a fait que lire. Casse l'idempotence du playbook.

- name: Relever la version de l'application
ansible.builtin.command: /usr/local/bin/myapp --version
register: app_version
changed_when: false # ← lecture seule, jamais de changement

🔍 Observation : changed_when: false est le pattern standard pour toute commande de diagnostic / lecture. Préserve l'idempotence.

Cas plus subtil, on veut signaler changed uniquement si la sortie indique un changement :

- name: Appeler l'API
ansible.builtin.uri:
url: "http://localhost:8080/health"
return_content: true
register: health
changed_when: "'OK' not in health.content" # ← changed seulement si KO

🔍 Observation : changed_when: accepte une expression Jinja2/Python. Permet de dériver le verdict du résultat de la tâche.

- name: Ajuster myapp.cfg
ansible.builtin.lineinfile:
path: /etc/myapp.cfg
line: "max_connections = 100"
state: present
create: true

Run 1 : changed=1, fichier contient max_connections = 100.

Plus tard, on update :

- name: Ajuster le fichier de configuration
ansible.builtin.lineinfile:
line: "max_connections = 200"

Run 2 : changed=1, fichier contient maintenant les 2 lignes :

max_connections = 100
max_connections = 200

Duplication. Casse la config.

- name: Ajuster myapp.cfg
ansible.builtin.lineinfile:
path: /etc/myapp.cfg
regexp: '^max_connections\s*=' # ← match les lignes existantes
line: "max_connections = 200"
state: present
create: true
mode: "0644"

🔍 Observation : avec regexp:, lineinfile remplace au lieu d'ajouter. Toujours mettre regexp: sauf au tout premier state: present.

Fenêtre de terminal
# Run 1 (peut avoir des changes)
ansible-playbook lab.yml > /tmp/run1.log
# Run 2 (DOIT avoir changed=0)
ansible-playbook lab.yml | tee /tmp/run2.log
# Échec si le 2e run a des changes
if grep -E 'changed=[1-9]' /tmp/run2.log; then
echo "❌ IDEMPOTENCE KO"
exit 1
fi
echo "✅ IDEMPOTENCE OK"

À mettre dans le pipeline ansible-playbook --check --diff. Bloque les régressions où un dev oublie un changed_when: ou un creates:.

Sans pipelining, chaque tâche fait 3 appels SSH : mkdir tmp/, scp module.py, python module.py. Avec pipelining, 1 seul appel SSH qui pipe tout via stdin.

[ssh_connection]
pipelining = True

Gain typique : -30 % à -40 % sur le temps total.

[defaults]
forks = 20 # default : 5

Exécute 20 hôtes en parallèle au lieu de 5. Gain typique : x2-x4 sur une fleet de 50+ hosts.

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s

OpenSSH réutilise la connexion pendant 60 s pour les tâches suivantes, pas besoin de refaire le handshake TCP+SSH (~1s économisée par tâche).

[defaults]
forks = 20
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-fact-cache
fact_caching_timeout = 7200
callback_result_format = yaml
callbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
[ssh_connection]
pipelining = True
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s

🔍 Observation : avec ces 3 optimisations cumulées, gain typique -50 % à -60 % sur un playbook multi-host. Mesurez avec time ansible-playbook ... avant/après.

Pour une fleet stable, les facts (collectés au début de chaque play) sont identiques entre runs successifs. Activer le cache :

[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-fact-cache
fact_caching_timeout = 7200 # 2 heures

🔍 Observation : gathering: smart ne re-collecte que si le cache est expiré. Sur une fleet de 50 hosts, économise typiquement 5-10 secondes au début de chaque playbook.

Un playbook qui affiche changed=N à chaque passage ne se répare qu'en observant le second run, pas en relisant le YAML. Le lab vous fait reproduire les trois anti-patterns décrits plus haut, puis les corriger un par un : creates: sur un shell, changed_when: sur une commande de lecture, regexp: sur un lineinfile qui duplique. Vous relevez ensuite un baseline avec profile_tasks avant d'activer pipelining, forks et ControlPersist, et le challenge refactore un playbook complet dont les tâches sont mesurées. La vérification compare le second passage au premier : c'est changed=0 qui fait foi.

  • command, shell, raw, script = non-idempotents par défaut.
  • creates: / removes: rendent un shell idempotent par check de fichier.
  • changed_when: false sur les commandes de lecture/diagnostic.
  • lineinfile toujours avec regexp: (sauf premier state: present).
  • 3 leviers de tuning : pipelining, forks, ControlPersist.
  • gathering: smart + cache : économise les facts collectés.
  • Test idempotence en CI : run 2× et grep changed=[1-9] doit retourner vide.
  • Pipeline CI matrice : Le workflow complet qui rejoue les tests à chaque commit, double passage compris.
  • Aide-mémoire RHCE : Une page imprimable qui rassemble les pièges classiques, dont ceux qui cassent l'idempotence.
  • Sécurité Ansible : vue d'ensemble : Les règles de production, où l'abus de shell et de command devient un risque de sécurité et plus seulement un défaut de qualité.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn