
Vous avez un script Bash qui marche au premier run, mais qui casse ou « dérive » dès que vous le relancez. C’est le problème classique de l’impératif : votre script raconte quelles actions enchaîner, sans se soucier de l’état actuel du système. Ansible prend l’approche déclarative opposée — vous décrivez l’état désiré, Ansible converge le système vers cet état, et n’agit que sur ce qui doit changer. Ce guide vous fait le déclic mental sur un cas concret : déployer nginx avec une page d’accueil personnalisée, en Bash puis en Ansible, et observer ce qui se passe en relançant.
Analogie : un script Bash, c’est une recette de cuisine lue mot à mot du début à la fin — relancez-la sur le même plat fini et vous le détruisez. Un playbook Ansible, c’est un inventaire de courses — relancez-le, votre placard reste celui que vous vouliez. Cette différence de philosophie change tout.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Voir concrètement la dérive d’un script Bash relancé plusieurs fois.
- Comprendre la convergence d’un playbook Ansible et la propriété d’idempotence.
- Identifier les modules Ansible idempotents par conception (
dnf,lineinfile,firewalld,systemd). - Repérer les pièges qui rendent un playbook non idempotent (modules
command/shellsans garde-fou).
La même tâche, deux philosophies
Section intitulée « La même tâche, deux philosophies »Notre objectif est simple : sur le serveur web1.lab, installer nginx, ouvrir le port 80 dans firewalld, démarrer le service, et personnaliser la page d’accueil avec une ligne « Servi par web1 ». Quatre actions banales, que vous feriez à la main en deux minutes.
L’enjeu n’est pas la première exécution — les deux approches y arrivent. L’enjeu, c’est ce qui se passe au deuxième run, au dixième, au centième. Un sysadmin ne lance jamais un script « une seule fois » : il l’intègre dans un cron, dans un pipeline CI/CD, dans une procédure de récupération après incident. Le script sera relancé. La question est : est-ce qu’il fait alors la bonne chose, ou est-ce qu’il dérive ?
L’approche impérative : un script Bash naïf
Section intitulée « L’approche impérative : un script Bash naïf »Voici un script Bash qui enchaîne les quatre actions. Il est volontairement simpliste — c’est exactement ce que beaucoup de débutants écriraient en première intention. Lisez-le ligne par ligne avant la suite, et repérez la commande qui pose problème au second run :
#!/bin/bashset -euo pipefail
echo "[1/4] Installation du paquet nginx..."sudo dnf install -y nginx
echo "[2/4] Ouverture du port 80 dans firewalld..."sudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --reload
echo "[3/4] Démarrage et activation du service nginx..."sudo systemctl enable --now nginx
echo "[4/4] Personnalisation de la page d'accueil (PIÈGE non idempotent)..."echo "<p>Servi par $(hostname)</p>" | sudo tee -a /usr/share/nginx/html/index.html >/dev/null
echo "OK : nginx déployé. Occurrences de 'Servi par' dans la page :"curl -s http://localhost/ | grep -c "Servi par" || trueLa commande à problème, c’est l’étape 4 : echo … | tee -a. Le -a signifie append — ajouter à la fin. À chaque run, le script ajoute une nouvelle ligne « Servi par … » à index.html, sans regarder si elle est déjà là. Les trois premières étapes sont par chance quasi idempotentes (dnf install ne réinstalle pas un paquet présent, systemctl enable --now est sûr), mais l’étape 4 trahit la nature impérative du Bash.
Trois runs successifs sur web1
Section intitulée « Trois runs successifs sur web1 »Relançons le script trois fois de suite et regardons le compteur d’occurrences. Sortie réelle capturée sur le lab :
=== 1er run ===[1/4] Installation du paquet nginx...[2/4] Ouverture du port 80 dans firewalld...[3/4] Démarrage et activation du service nginx...[4/4] Personnalisation de la page d'accueil (PIÈGE non idempotent)...OK : Occurrences de 'Servi par' dans la page : 1
=== 2e run ===[2/4] Ouverture du port 80 dans firewalld...Warning: ALREADY_ENABLED: http[4/4] Personnalisation de la page d'accueil (PIÈGE non idempotent)...OK : Occurrences de 'Servi par' dans la page : 2
=== 3e run ===[4/4] Personnalisation de la page d'accueil (PIÈGE non idempotent)...OK : Occurrences de 'Servi par' dans la page : 3Lecture de la sortie : au premier run, la page contient 1 ligne « Servi par ». Au deuxième, 2 lignes. Au troisième, 3 lignes. Le script dérive linéairement. Si vous l’intégrez dans un cron horaire, votre page d’accueil contiendra 24 lignes demain matin et 8760 lignes dans un an. C’est l’inverse de ce qu’on veut.
L’approche déclarative : un playbook Ansible
Section intitulée « L’approche déclarative : un playbook Ansible »Voici le playbook équivalent. Lisez-le entièrement avant la suite, et notez que la structure est différente : on décrit l’état, on n’enchaîne pas des actions.
---- name: Déployer nginx (équivalent déclaratif du script Bash) hosts: web1.lab become: true
tasks: - name: Installer le paquet nginx ansible.builtin.dnf: name: nginx state: present
- name: Ouvrir le service http dans firewalld de manière permanente ansible.posix.firewalld: service: http permanent: true immediate: true state: enabled
- name: Activer et démarrer nginx ansible.builtin.systemd: name: nginx enabled: true state: started
- name: Garantir une UNIQUE ligne « Servi par … » dans index.html (idempotent) ansible.builtin.lineinfile: path: /usr/share/nginx/html/index.html regexp: "^<p>Servi par " line: "<p>Servi par {{ inventory_hostname_short }}</p>" state: present create: falseCe playbook contient quatre tâches qui correspondent une à une aux quatre étapes du script Bash, mais formulées en termes d’état. La tâche 1 ne dit pas « installe nginx », elle dit « state: present » — peu importe comment on y arrive. La tâche 4 ne dit pas « append cette ligne », elle dit « garantis qu’une ligne matchant ce regex existe » avec ce contenu — la lecture sémantique est : je veux exactement une ligne, voici son motif et sa valeur.
C’est le module lineinfile qui porte cette logique. Il lit le fichier, cherche une ligne qui matche le regexp, et fait l’une de ces trois choses :
- Si aucune ligne ne matche : il ajoute la ligne fournie.
- Si une seule ligne matche et a déjà la valeur attendue : il ne fait rien (
changed=0). - Si une ligne matche avec une valeur différente : il la remplace par la nouvelle valeur.
Cette logique « match-or-add » est l’essence de l’idempotence déclarative. Vous la retrouverez dans dnf (« state: present » = installé, peu importe la version actuelle), firewalld (« state: enabled » = règle active, peu importe l’historique), systemd (« state: started » = service en marche), et la quasi-totalité des modules Ansible bien écrits.
Trois runs successifs : la convergence
Section intitulée « Trois runs successifs : la convergence »Relançons le playbook trois fois de suite et regardons le compteur changed. Sortie réelle capturée sur le lab :
=== 1er run (état initial : nginx absent) ===PLAY RECAP *********************************************************************web1.lab : ok=4 changed=4 unreachable=0 failed=0
=== 2e run (idempotence) ===PLAY RECAP *********************************************************************web1.lab : ok=4 changed=0 unreachable=0 failed=0
=== 3e run (idempotence) ===PLAY RECAP *********************************************************************web1.lab : ok=4 changed=0 unreachable=0 failed=0
=== Vérification finale ===$ curl -s http://10.10.20.21/ | grep -c "Servi par"1Trois lectures à faire ici. La première : au 1er run, changed=4 — Ansible a appliqué les 4 changements (paquet, firewall, service, ligne de page). La deuxième : au 2e et au 3e run, changed=0 — Ansible a vérifié l’état, constaté qu’il était déjà conforme à ce qu’on demandait, et n’a rien touché. La troisième : la page contient toujours exactement une ligne « Servi par », même après plusieurs runs. C’est la convergence.
Ce qui rend un module idempotent
Section intitulée « Ce qui rend un module idempotent »Tous les modules Ansible ne sont pas idempotents par défaut. Voici la règle pour s’y retrouver, et les pièges les plus fréquents.
Modules idempotents par conception
Section intitulée « Modules idempotents par conception »La grande majorité des modules « système » sont idempotents parce qu’ils raisonnent en état désiré : dnf, apt, service, systemd, user, group, file, copy, template, lineinfile, blockinfile, firewalld, selinux, mount, cron. Quand vous les utilisez avec state: present ou équivalent, ils vérifient l’état actuel avant d’agir. C’est ce qu’on appelle un module structuré.
Modules non idempotents par défaut
Section intitulée « Modules non idempotents par défaut »Trois modules cassent l’idempotence parce qu’ils exécutent une commande shell brute sur la cible : ansible.builtin.command, ansible.builtin.shell, ansible.builtin.raw. Ils n’ont aucun moyen de savoir si la commande a déjà été lancée, et la rejouent systématiquement. Le compteur changed augmente à chaque run.
Pour les rendre idempotents, deux garde-fous existent. Le premier est le couple creates: / removes: : « ne lance la commande que si ce fichier n’existe pas / existe déjà ». Le second est changed_when: false : déclarer explicitement que la commande ne change rien (utile pour des commandes de lecture comme id ou getent).
- name: Compiler le code uniquement si le binaire n'existe pas ansible.builtin.command: make build args: chdir: /opt/myapp creates: /opt/myapp/bin/myapp # garde-fou : pas de re-exec si présent
- name: Vérifier que l'utilisateur 'ansible' existe (lecture seule) ansible.builtin.command: id ansible changed_when: false # déclare : « jamais changed »Règle pratique : préférez toujours un module structuré quand il existe. Si vous tapez command: dnf install nginx, c’est un mauvais signe — utilisez ansible.builtin.dnf à la place. Si aucun module ne fait ce que vous voulez, ajoutez creates: ou changed_when: false pour rester idempotent.
Tableau récapitulatif
Section intitulée « Tableau récapitulatif »Pour fixer le déclic mental, voici la comparaison côte à côte des deux approches. Lisez les colonnes : à gauche, ce qu’on perd avec un Bash impératif ; à droite, ce qu’on gagne avec Ansible déclaratif.
| Aspect | Script Bash impératif | Playbook Ansible déclaratif |
|---|---|---|
| Ce qu’on écrit | Une séquence d’actions | Un état désiré |
| 1er run | Fonctionne | Fonctionne |
| 2e run | Dérive (ligne dupliquée) | changed=0 (rien à faire) |
| Modèle mental | « Comment » | « Quoi » |
| Connaissance de l’état actuel | Aucune | Vérifié avant chaque action |
Modules type lineinfile | Inexistant | Standard |
Annulation propre (state: absent) | À coder à la main | Built-in |
| Adapté au cron / CI | Non sans précaution | Oui |
| Lisibilité long terme | Diminue avec la complexité | Reste constante |
La conclusion concrète : tant que votre script est petit et lancé une fois, le Bash fait l’affaire. Dès qu’il est rejoué, intégré dans un workflow ou partagé en équipe, l’idempotence devient indispensable. C’est précisément la valeur d’Ansible. Vous gagnez en fiabilité ce que vous perdez en familiarité — et la familiarité s’acquiert vite.
Pièges fréquents à éviter
Section intitulée « Pièges fréquents à éviter »À retenir
Section intitulée « À retenir »- Un script impératif dit comment enchaîner des actions. Un playbook déclaratif dit quel état on veut.
- L’idempotence signifie qu’un second run ne change rien (
changed=0) — c’est le critère qualité d’un playbook. - Les modules structurés (
dnf,systemd,lineinfile,firewalld) sont idempotents par conception. Les modulescommand/shell/rawne le sont pas — ajoutezcreates:ouchanged_when: false. - Sur l’examen RHCE comme en code review, on attend
changed=0au second passage. Un playbook qui dérive est non conforme. - Le lab
decouvrir/declaratif-vs-imperatif/du dépôtlab-ansiblereproduit cette démonstration en quelques minutes.
Pratiquer dans le lab
Section intitulée « Pratiquer dans le lab »Cette page a un lab d’accompagnement : labs/decouvrir/declaratif-vs-imperatif/ dans
stephrobert/ansible-training. Il contient
un README.md guidé, un Makefile (make verify lance les tests), et un
challenge final auto-évalué : comparer un script Bash impératif (qui dérive) à un playbook Ansible déclaratif (qui converge).
Une fois le lab provisionné :
cd ~/Projets/ansible-training/labs/decouvrir/declaratif-vs-imperatif/
cat README.md # tuto pas à pascat challenge/README.md # consigne du challenge finalpytest -v challenge/tests/ # lancer les tests testinfraSi les tests passent, vous maîtrisez les concepts couverts dans ce guide. En
cas de blocage, docs/troubleshooting.md
à la racine du repo couvre les pièges fréquents (rate-limit SSH, clé absente,
collection manquante).