
Quand aucun module dédié n'existe, vous tombez sur les quatre modules d'exécution directe : raw, command, shell, script. Ils ne sont pas idempotents par nature et ouvrent la porte à des pièges sécurité (injection, redirections, état imprévu).
Cette page donne le mode d'emploi pragmatique : quand utiliser lequel, comment retrouver l'idempotence avec creates: / removes: / changed_when:, et pourquoi un module dédié reste toujours préférable.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Choisir entre
raw,command,shell,scriptselon le contexte. - Restaurer l'idempotence avec
creates:,removes:,changed_when:. - Éviter les injections de commandes via shell.
- Capturer les sorties (
register:) pour conditionner les tâches suivantes.
Prérequis
Section intitulée « Prérequis »- Connaître les bases d'un playbook (
tasks:,register:,when:). - Avoir déjà rencontré l'un de ces modules dans du code legacy.
Tableau de décision
Section intitulée « Tableau de décision »La colonne décisive est la dernière : Shell. Elle dit si votre commande sera
interprétée par un interpréteur, donc si une variable Jinja mal maîtrisée peut
devenir une injection de commande. Les trois modules du bas savent restaurer
l'idempotence avec creates: ou removes: ; raw en est incapable, c'est
pourquoi il ne sert qu'à amorcer une machine.
| Module | Quand l'utiliser | Idempotence | Shell |
|---|---|---|---|
raw | Cible sans Python (routeur, embedded, bootstrap) | Non | Non |
command | Commande simple, pas de pipe ni redirection | creates: / removes: | Non |
shell | Pipes |, redirections >, opérateurs && | creates: / removes: | Oui |
script | Lancer un script local sur la cible distante | creates: / removes: | Oui |
Règle absolue : essayer dans l'ordre package → module dédié → command → shell. raw en dernier.
Module raw, bootstrap minimal
Section intitulée « Module raw, bootstrap minimal »raw n'utilise aucun module Python sur la cible : Ansible ouvre une session
SSH et envoie la commande telle quelle. C'est la seule façon d'agir sur une
machine où l'interpréteur Python n'est pas encore présent, et cela explique
toutes ses limites. Notez le changed_when: true du snippet : raw ne sait
pas dire s'il a modifié quelque chose, l'auteur du playbook doit donc trancher
lui-même ce que la tâche rapporte.
- name: Installer Python sur une cible bare-metal ansible.builtin.raw: dnf -y install python3 changed_when: trueCas d'usage unique : la machine cible n'a pas Python, donc les autres modules ne fonctionnent pas. Typique du bootstrap d'une fresh install ou d'un équipement réseau (Cisco IOS, switches).
Limites : pas de structuration de la sortie, pas de creates:, pas d'idempotence. À utiliser sur 1 ou 2 tâches max pour amorcer Python, puis basculer sur les modules normaux.
Module command, la sécurité d'abord
Section intitulée « Module command, la sécurité d'abord »command exécute le binaire directement, sans passer par un interpréteur.
La conséquence pratique surprend souvent : $HOME, *, | et > ne sont pas
interprétés, ils arrivent au programme sous forme de caractères littéraux. Une
commande qui « ne marche pas » avec command alors qu'elle fonctionne dans un
terminal signale presque toujours qu'elle dépendait du shell.
- name: Générer un certificat (sans pipe ni redirection) ansible.builtin.command: cmd: openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/key.pem -out /etc/ssl/cert.pem -days 365 -nodes -subj "/CN=db1.lab" creates: /etc/ssl/cert.pemPas de shell = pas d'interprétation des $VAR, des |, des >. Plus sûr car pas d'injection possible via une variable Jinja mal échappée. Préféré à shell: chaque fois que c'est possible.
creates: /etc/ssl/cert.pem = la commande ne s'exécute que si le fichier n'existe pas. Restaure l'idempotence sans changer le module.
Module shell, quand vous avez besoin de pipes
Section intitulée « Module shell, quand vous avez besoin de pipes »shell passe la commande à un interpréteur sur la cible, ce qui rend
disponibles les tubes, les redirections et l'expansion de variables. Le prix à
payer est double : la sortie n'est plus structurée, et toute donnée extérieure
insérée dans la chaîne devient exécutable. L'exemple ci-dessous montre la forme
durcie à copier, avec les trois garde-fous qui manquent presque toujours dans
le code existant.
- name: Compter les lignes d'erreur dans le log ansible.builtin.shell: | set -o pipefail grep ERROR /var/log/app.log | wc -l args: executable: /bin/bash register: error_count changed_when: false failed_when: error_count.rc not in [0, 1] # 1 = aucune ligne trouvéechanged_when: false = la tâche est marquée ok, jamais changed. Indispensable pour les commandes de lecture pure, sinon Ansible affiche changed à chaque run et pourrit votre rapport.
set -o pipefail est indispensable dès qu'il y a un tube. Sans lui, le shell ne retient que le code de sortie de la dernière commande du pipeline : si grep échoue parce que le fichier n'existe pas, wc -l renvoie tranquillement 0 et la tâche passe au vert alors que rien n'a été lu. L'option exige bash, d'où le executable: /bin/bash, car le /bin/sh de certaines distributions ne la connaît pas.
Ce durcissement a une conséquence à connaître, mesurée sur bash : grep renvoie 1 quand il ne trouve aucune correspondance, ce qui n'est pas une erreur mais un résultat. Avec pipefail, ce 1 remonte et fait échouer la tâche précisément dans le cas favorable, celui où le journal ne contient aucune erreur. D'où le failed_when qui accepte 0 et 1, et refuse le reste.
Risque sécurité : si une variable Jinja arrive dans la commande (grep "{{ user_input }}" file), un ; ou un && dans la valeur peut exécuter du code arbitraire. Toujours valider l'input ou utiliser command: avec une liste d'arguments.
Module script, lancer un script local sur la cible
Section intitulée « Module script, lancer un script local sur la cible »script répond au cas où la logique tient dans un fichier plutôt que dans une
ligne : plusieurs étapes, des tests, une boucle. Le fichier reste sur le
control node et n'a pas besoin d'exister sur la cible au préalable, ce qui
évite de gérer sa distribution et son nettoyage. En contrepartie, Ansible ne
peut rien dire de ce que le script a fait : c'est à vous de fournir le repère
d'idempotence.
- name: Initialiser la base si elle n'existe pas ansible.builtin.script: files/init_db.sh --env prod args: creates: /var/lib/app/db.sqliteDifférence avec command: : script: transfère le fichier depuis le control node vers la cible avant de l'exécuter, puis le supprime automatiquement. Pas besoin de copy: + command: en deux étapes.
Paramètres utiles :
creates:/removes:: idempotence basée sur l'existence d'un fichier.chdir:: changer de répertoire avant exécution.executable:: forcer un interpréteur (python3,bash).
Restaurer l'idempotence
Section intitulée « Restaurer l'idempotence »Une commande shell ne sait pas dire si elle a changé quelque chose : Ansible
signale donc changed à chaque exécution, ce qui rend le rapport final
inutilisable et déclenche à tort les handlers associés. Les trois mécanismes
ci-dessous rendent la tâche lisible sans changer de module. Le point commun des
deux premiers est qu'ils s'appuient sur un artefact persistant : un fichier
dont la présence prouve que le travail a déjà été fait. Si aucun artefact de ce
genre n'existe, c'est le signe qu'un module dédié serait plus adapté.
- name: Compiler l'application (idempotent via creates) ansible.builtin.command: cmd: /usr/local/bin/build.sh --release creates: /opt/myapp/bin/myapp
- name: Supprimer un fichier temporaire (idempotent via removes) ansible.builtin.command: cmd: rm /tmp/tempfile removes: /tmp/tempfile
- name: Lecture seule, ne jamais marquer changed ansible.builtin.command: cat /etc/hostname register: hostname_out changed_when: falseGérer failed_when: proprement
Section intitulée « Gérer failed_when: proprement »Par défaut, un code de retour non nul fait échouer la tâche et arrête le play.
Or beaucoup de commandes utilisent ce code pour transmettre un résultat,
pas pour signaler une panne : grep renvoie 1 quand il ne trouve rien, test
renvoie 1 quand la condition est fausse. failed_when: sert à redéfinir ce qui
constitue un échec pour votre contexte, et non à masquer les erreurs.
- name: Lire un log qui peut être absent ansible.builtin.command: cat /var/log/maybe.log register: log_content failed_when: false changed_when: falsefailed_when: false = ne pas faire échouer le play même si rc != 0. À combiner avec un test sur result.rc plus loin si besoin.
Pièges courants
Section intitulée « Pièges courants »Ces cinq symptômes couvrent la quasi-totalité des tickets liés à ces modules.
Deux d'entre eux ne provoquent aucune erreur visible : le changed permanent et
l'idempotence cassée passent les tests, polluent les rapports et finissent par
masquer les vrais changements. Traitez-les en priorité, ce sont eux qui érodent
la confiance dans les exécutions Ansible.
| Symptôme | Cause | Fix |
|---|---|---|
changed à chaque run sur une commande de lecture | Pas de changed_when: | changed_when: false |
ansible.builtin.command rejette cmd | grep | Pas de shell, pas de pipe | Utiliser shell: ou repenser la commande |
| Injection via variable Jinja | shell: + variable non validée | Préférer command: avec une liste, ou échapper avec le filtre quote |
script: ne trouve pas le fichier | Chemin relatif non résolu | Préfixer par files/ (résolu relativement au playbook/rôle) |
| Idempotence cassée à chaque run | Pas de creates: ou removes: | Identifier un artefact persistant et le déclarer |
À retenir
Section intitulée « À retenir »- Essayer un module dédié avant chaque
command:oushell:. command:>shell:quand pas de pipe, moins de surface d'attaque.creates:/removes:= restauration d'idempotence à coût zéro.changed_when: falsesur toute commande de lecture pure.raw:= bootstrap uniquement, jamais en mode normal.script:=copy + commanden une tâche, idéal pour les scripts ad-hoc.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Structure standard : le répertoire
files/d'un rôle, qui est justement le chemin résolu parscript:. - Tests avec testinfra : prouver l'effet réel d'une commande brute, puisque son code de retour ne dit presque rien.