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 tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn