Aller au contenu
English
English
Infrastructure as Code medium

Style guide Ansible : conventions de nommage, FQCN, ansible-lint

40 min de lecture

Logo Ansible

Un style guide Ansible n'est pas du purisme : c'est ce qui rend un playbook lisible six mois après, et relisable par un collègue qui ne l'a pas écrit. Cette page donne les conventions attendues sur du code Ansible en 2026, avec la règle ansible-lint qui contrôle chacune : FQCN obligatoire, tâches toujours nommées, modules command et shell réservés au dernier recours, tags orthogonaux, variables préfixées.

Ces conventions ne sont pas une affaire de goût, et c'est ce qui les rend utiles : chacune correspond à une règle outillée, donc vérifiable en intégration continue plutôt que discutée en revue de code. Le référentiel couvre aussi le périmètre de la RHCE EX294, sans s'y limiter.

  • Les conventions de nommage des tâches, plays, variables, tags ;
  • L'usage du FQCN (Fully Qualified Collection Name), et pourquoi il ne se discute plus ;
  • Les modules à éviter (command, shell) et quand ils restent légitimes ;
  • Les commentaires utiles, par opposition à ceux qui répètent le code ;
  • L'usage d'ansible-lint en intégration continue, et ce que chacun de ses six profils exige.

Chaque play a un name: qui décrit l'objectif métier, pas la liste des modules :

# ❌ Trop technique, paraphrase les modules
- name: Run dnf install nginx and systemd start
hosts: webservers
# ✅ Décrit l'objectif métier
- name: Déployer nginx sur les serveurs web
hosts: webservers

Ce nom s'affiche en tête d'exécution, sous la forme PLAY [Déployer nginx sur les serveurs web] ****. C'est le premier repère que lit celui qui diagnostique une sortie de plusieurs centaines de lignes, souvent des semaines après avoir écrit le playbook. Ne pas le confondre avec le PLAY RECAP de fin d'exécution, qui ne contient aucun nom : il ne liste que les hôtes et leurs compteurs ok, changed, failed.

Chaque tâche porte un name:, sans exception. La règle name[missing] d'ansible-lint est active dès le profil basic, c'est-à-dire pratiquement partout :

# ❌ Pas de name → ansible-lint refuse
- ansible.builtin.dnf:
name: nginx
state: present
# ✅ Name explicite
- name: Installer nginx
ansible.builtin.dnf:
name: nginx
state: present

Attention au piège de lecture : le name: de la tâche, celui qui manque dans le premier bloc, n'a rien à voir avec le name: du module dnf, qui désigne le paquet à installer. Deux clés homonymes à deux niveaux d'indentation différents, et c'est la plus fréquente confusion des débutants sur ce module.

Verbe à l'infinitif, capitale en début, pas de point final. Le nom choisi s'affiche dans la bannière de tâche : TASK [Installer nginx] *******.

Les variables sont préfixées par le rôle ou le composant, ce qui évite les collisions quand plusieurs rôles cohabitent dans le même play :

# ❌ Variables génériques, conflits garantis entre rôles
port: 80
user: ansible
version: "1.26"
# ✅ Préfixées par le rôle/composant
nginx_port: 80
nginx_user: ansible
nginx_version: "1.26"
pg_port: 5432
pg_user: postgres
app_version: "3.4.1"

Le préfixe n'est pas forcément le nom d'un service : app_version préfixe par le composant déployé, ce qui suffit dès lors que deux rôles du même play ne se disputent plus la variable version. La règle var-naming d'ansible-lint, active dès le profil basic, contrôle la forme du nom (minuscules et underscores) mais pas ce choix de préfixe, qui reste le vôtre.

Snake_case pour les variables (jamais kebab-case ni camelCase, c'est la convention Ansible).

Tags orthogonaux et descriptifs. Le rôle d'un tag est de pouvoir cibler un sous-ensemble de tâches via --tags ou les ignorer via --skip-tags :

# ❌ Tags non descriptifs, redondants avec le name
tags:
- tag1
- step2
# ✅ Tags descriptifs et orthogonaux
tags:
- nginx # service concerné
- firewall # type d'opération
- configuration # phase

Convention : nom de service, type d'opération, phase. Pas de pluriel (nginx pas nginxs).

Le FQCN (Fully Qualified Collection Name) prend la forme <namespace>.<collection>.<module> :

Forme courteFQCN
dnfansible.builtin.dnf
systemdansible.builtin.systemd_service
firewalldansible.posix.firewalld
selinuxansible.posix.selinux
timezonecommunity.general.timezone

La deuxième ligne mérite un arrêt, parce qu'elle piège même les praticiens confirmés : le nom canonique du module est désormais ansible.builtin.systemd_service, systemd n'étant conservé que comme alias historique. Les deux fonctionnent aujourd'hui, mais un seul est le nom réel, et c'est celui qu'affiche la documentation.

Le FQCN est obligatoire :

  • Pour ansible-lint à partir du profil production, qui active la règle fqcn.
  • Pour éviter les collisions : deux collections peuvent fournir un module portant le même nom court.
  • Pour la RHCE EX294, dont les objectifs publiés demandent d'installer des Content Collections et de s'en servir dans un playbook. Red Hat ne publie aucun barème, donc personne ne peut affirmer combien coûte une forme courte ; ce qui est certain, c'est que le FQCN désigne sans ambiguïté le module voulu, y compris sur une machine d'examen dont vous ne connaissez pas la configuration.
# ❌ Forme courte : le module retenu dépend des collections chargées
- name: Ouvrir SSH dans firewalld
firewalld: # ← peut désigner un autre module que celui attendu
service: ssh
state: enabled
# ✅ FQCN explicite
- name: Ouvrir SSH dans firewalld
ansible.posix.firewalld:
service: ssh
state: enabled

Les modules command et shell sont non-idempotents par défaut et constituent un anti-pattern dès qu'un module dédié existe. Avant d'écrire un shell:, posez-vous trois questions :

  1. Existe-t-il un module dédié ? dnf, service, user, file, copy, lineinfile, template, git, unarchive. Privilégiez-les systématiquement.
  2. Si vous insistez sur shell : avez-vous posé une garde, creates:, removes: ou changed_when:, pour qu'il cesse de se déclarer changed à chaque passage ?
  3. Avez-vous justifié dans un commentaire pourquoi shell est nécessaire ?
# ❌ shell sans garde, non-idempotent
- name: Initialiser la base
ansible.builtin.shell: pg_ctl initdb -D /var/lib/pgsql/data
# ✅ shell avec creates: (idempotent)
- name: Initialiser la base si pas déjà fait
ansible.builtin.shell: pg_ctl initdb -D /var/lib/pgsql/data
args:
creates: /var/lib/pgsql/data/PG_VERSION

Deux règles d'ansible-lint travaillent ici, et elles ne regardent pas la même chose. no-changed-when est celle qui traque l'absence de garde : elle se déclenche sur command, shell et raw dès qu'aucun de changed_when, creates ou removes n'est présent, et elle n'est active qu'à partir du profil shared. command-instead-of-shell, active dès le profil basic, pose une autre question : elle signale un shell: dont la commande ne contient aucun caractère réclamant un shell, comme un tube ou une redirection, et qui aurait donc dû être un command:.

Les commentaires Ansible (lignes commençant par #) servent à expliquer le pourquoi d'une décision non-évidente, pas à paraphraser le code :

# ❌ Commentaire qui paraphrase le code
- name: Installer nginx
ansible.builtin.dnf:
name: nginx # On installe nginx
state: present # avec l'état présent
# ✅ Commentaire qui explique le pourquoi
- name: Installer nginx (version figée car la 1.27 casse notre configuration TLS)
ansible.builtin.dnf:
name: "nginx-1.26.*"
state: present

Un playbook gagne en revanche à porter un commentaire d'en-tête, qui dit à quoi il sert et sur quoi il s'applique. C'est le seul endroit où un commentaire apporte ce que le code ne dit pas : la liste des tâches se lit toute seule, l'intention générale ne se lit nulle part. Les fichiers de configuration que vous déposez avec template: suivent la même logique, avec une ligne supplémentaire qui prévient qu'ils sont générés et qu'une édition à la main sera écrasée.

Ces quatre points ne changent rien au comportement d'un playbook, et c'est précisément pourquoi ils se négligent. Leur intérêt est ailleurs : un formatage homogène rend les diffs lisibles, et un diff lisible est ce qui permet à une revue de porter sur la logique plutôt que sur des déplacements de lignes. La règle yaml d'ansible-lint, active dès le profil basic, les contrôle pour vous.

  • 2 espaces d'indentation (jamais de tabulation), convention Ansible.
  • --- au début de chaque fichier YAML.
  • Une ligne vide entre les tâches d'un même play pour aérer.
  • Quotes systématiques sur les valeurs ambiguës (cf. YAML pour Ansible).

ansible-lint est l'outil de référence pour faire respecter le style guide automatiquement. Configuration via .ansible-lint à la racine du projet :

---
profile: production # min < basic < moderate < safety < shared < production
exclude_paths:
- .ansible/
- tests/
skip_list:
- meta-runtime # règles que vous décidez d'ignorer
warn_list:
- experimental # règles en avertissement plutôt qu'en erreur

Les six profils forment une chaîne, et c'est ce qui rend le tableau lisible : chacun reprend tout le précédent et y ajoute ses propres règles. Monter d'un cran n'échange donc jamais une exigence contre une autre, il en empile une de plus. Le tableau les donne dans cet ordre, du plus permissif au plus strict.

ProfilCe qu'il ajouteQuand l'utiliser
minLes erreurs qui empêchent d'analyser le fichierDémarrage rapide, projets hérités
basicLe gros du style : name, var-naming, yaml, command-instead-of-shellProjets en cours de reprise
moderateLa forme des noms : name[casing], name[imperative]Projets matures
safetyCe qui expose : risky-file-permissions, risky-octal, latestAudit sécurité avant production
sharedCe qu'exige un code publié : galaxy, no-changed-when, meta-*Galaxy, Automation Hub
productionLe dernier cran : fqcn, meta-no-dependencies, use-loopProduction attendue à terme

L'outil s'exécute sur un fichier ou sur un répertoire entier, et l'option --profile surcharge ce que déclare le .ansible-lint. C'est le moyen de mesurer la dette avant de s'engager : lancez le profil production sur un projet configuré en basic pour voir ce qu'un passage au cran supérieur coûterait, sans rien changer au dépôt.

Fenêtre de terminal
ansible-lint playbooks/site.yml
ansible-lint --profile production playbooks/

Sortie typique :

WARNING Listing 3 violation(s) that are fatal
fqcn[action-core]: Use FQCN for builtin module actions (dnf).
playbooks/site.yml:8 Task/Handler: Installer nginx
name[missing]: All tasks should be named.
playbooks/site.yml:15 Task/Handler: ansible.builtin.systemd_service
no-changed-when: Commands should not change things if nothing needs doing.
playbooks/site.yml:22 Task/Handler: Initialiser la base

Chaque violation porte la règle qui l'a levée et la ligne fautive, ce qui permet de traiter la dette par règle plutôt que fichier par fichier : on corrige tous les fqcn[action-core] d'un coup, puis on passe à la suivante.

Le workflow ci-dessous applique trois précautions qui n'ont rien d'optionnel sur une chaîne d'intégration publique. Les actions sont épinglées au SHA du commit, la version en commentaire, parce qu'un tag se redéplace ; les permissions du jeton sont vidées puis rouvertes au strict nécessaire ; et persist-credentials: false empêche le jeton de rester écrit dans le dépôt cloné, où n'importe quelle étape suivante pourrait s'en servir.

.github/workflows/ansible-lint.yml
on: [push, pull_request]
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- run: pip install ansible-lint==26.9.0
- run: ansible-lint --profile production playbooks/

La version d'ansible-lint est figée elle aussi, et ce n'est pas un détail de confort : l'outil ajoute des règles à chaque version, si bien qu'une chaîne non épinglée peut échouer du jour au lendemain sur du code que personne n'a touché. Vous relevez cette version quand vous décidez de traiter les nouvelles règles, pas quand l'amont publie.

Ce tableau récapitule les huit défauts qui reviennent le plus souvent dans du code Ansible repris, et il se lit dans le sens de la correction : la dernière colonne donne le geste, pas un principe. Les quatre premières lignes sont couvertes par les règles vues plus haut ; les quatre dernières relèvent de la relecture humaine, aucun outil ne décidant à votre place si un ignore_errors: est justifié.

Anti-patternPourquoi c'est fauxCorrection
Tâche sans name:Sortie illisible, la bannière affiche TASK [ansible.builtin.dnf]Ajouter un name: qui décrit l'objectif
Forme courte de module (dnf:)Ambigu dès que deux collections se ressemblentUtiliser le FQCN (ansible.builtin.dnf)
shell: sans gardeNon idempotent, changed à chaque exécutionUtiliser un module dédié, ou ajouter une garde
Variables génériques (port, user)Conflits entre rôlesPréfixer (nginx_port, pg_user)
Secrets en clair dans host_vars/Visibles dans git logansible-vault ou gestionnaire de secrets externe
Tags step1, step2Non descriptifTags orthogonaux : service + opération + phase
command: qui appelle un gestionnaire de paquets (pip install)Contourne le module idempotentUtiliser le module dédié (ansible.builtin.pip)
ignore_errors: true partoutMasque les vraies erreursJustifier par un commentaire ; préférer failed_when:

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • name: obligatoire sur chaque tâche, décrit l'objectif métier, pas le module utilisé.
  • FQCN systématique : exigé par le profil production d'ansible-lint, et seule forme non ambiguë sur une machine que vous ne configurez pas.
  • Variables préfixées par le rôle/composant pour éviter les collisions.
  • Modules command/shell en dernier recours, toujours préférer un module dédié.
  • Commentaires expliquent le pourquoi, pas le quoi (le code est déjà documentation).
  • ansible-lint en intégration continue : profil production, version figée, blocage à chaque demande de fusion.
  • Modules built-in : où chercher le module dédié qui remplace un command: ou un shell: mal placé.
  • Plays et tasks : les mots-clés d'un play, pour appliquer ces conventions au bon niveau.
  • Tags : les tags orthogonaux évoqués ici, avec leur syntaxe, leur héritage et --list-tags.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. 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