Aller au contenu
Infrastructure as Code medium

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

11 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.

- ansible.builtin.shell: "echo lab > /tmp/marker"

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

- 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 :

- ansible.builtin.shell: "rm -rf /tmp/old-cache"
args:
removes: /tmp/old-cache # ← skip si le fichier n'existe PAS

Anti-pattern 2, command de lecture sans changed_when:

Section intitulée « Anti-pattern 2, command de lecture sans changed_when: »
- ansible.builtin.command: curl --version
register: curl_out

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

- ansible.builtin.command: curl --version
register: curl_out
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 :

- 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.

- 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 :

- 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.

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

🔍 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
stdout_callback = 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.

Le lab troubleshooting/idempotence-perfs (labs/troubleshooting/idempotence-perfs/) couvre les 7 exercices : 3 anti-patterns d'idempotence + baseline de perfs + tuning + test idempotence en CI. Challenge final : refactorer un playbook non-idempotent vers idempotent (3 tâches mesurées).

  • 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.

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