Aller au contenu
Infrastructure as Code medium

Modules raw, command, shell, script Ansible : exécution directe en dernier recours

20 min de lecture

Logo Ansible

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.

  • Choisir entre raw, command, shell, script selon 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.
  • Connaître les bases d'un playbook (tasks:, register:, when:).
  • Avoir déjà rencontré l'un de ces modules dans du code legacy.

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.

ModuleQuand l'utiliserIdempotenceShell
rawCible sans Python (routeur, embedded, bootstrap)NonNon
commandCommande simple, pas de pipe ni redirectioncreates: / removes:Non
shellPipes |, redirections >, opérateurs &&creates: / removes:Oui
scriptLancer un script local sur la cible distantecreates: / removes:Oui

Règle absolue : essayer dans l'ordre package → module dédié → commandshell. raw en dernier.

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

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

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

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

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ée

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

Diffé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).

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

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

failed_when: false = ne pas faire échouer le play même si rc != 0. À combiner avec un test sur result.rc plus loin si besoin.

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ômeCauseFix
changed à chaque run sur une commande de lecturePas de changed_when:changed_when: false
ansible.builtin.command rejette cmd | grepPas de shell, pas de pipeUtiliser shell: ou repenser la commande
Injection via variable Jinjashell: + variable non validéePréférer command: avec une liste, ou échapper avec le filtre quote
script: ne trouve pas le fichierChemin relatif non résoluPréfixer par files/ (résolu relativement au playbook/rôle)
Idempotence cassée à chaque runPas de creates: ou removes:Identifier un artefact persistant et le déclarer
  • Essayer un module dédié avant chaque command: ou shell:.
  • command: > shell: quand pas de pipe, moins de surface d'attaque.
  • creates: / removes: = restauration d'idempotence à coût zéro.
  • changed_when: false sur toute commande de lecture pure.
  • raw: = bootstrap uniquement, jamais en mode normal.
  • script: = copy + command en une tâche, idéal pour les scripts ad-hoc.
  • Structure standard : le répertoire files/ d'un rôle, qui est justement le chemin résolu par script:.
  • Tests avec testinfra : prouver l'effet réel d'une commande brute, puisque son code de retour ne dit presque rien.

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