📍 Article principal : Ansible 2026 : la stack pratique pour automatiser Linux et Windows
Ce tutoriel suppose que vous avez déjà installé ansible-core et lancé un premier playbook. On passe ici à la structuration : transformer un playbook monolithique en rôles réutilisables.
Pourquoi structurer en rôles dès le deuxième projet
Un premier playbook qui installe Nginx et dépose une page d’accueil tient en cinquante lignes. Un projet qui orchestre Nginx, MariaDB, PHP-FPM, fail2ban, des sauvegardes et un certificat Let’s Encrypt en compte cinq cents si tout reste dans un seul fichier. À ce stade, la duplication apparaît, la lecture devient pénible, et la moindre modification déclenche une revue complète. Les rôles règlent ce problème en imposant une organisation canonique du code Ansible — la même chez vous, chez Red Hat, chez les mainteneurs de Galaxy. Apprendre cette structure, c’est apprendre à lire et à écrire l’Ansible que tout le monde produit.
Un rôle est un répertoire qui contient ce qu’il faut pour rendre un service opérationnel : les tâches d’installation et de configuration, les gabarits, les fichiers à copier, les valeurs par défaut, les handlers à déclencher en cas de changement, et la déclaration des dépendances. Une fois écrit, on l’invoque depuis un playbook en une ligne, on le partage entre projets via un dépôt Git ou Galaxy, on le teste avec Molecule. La logique de l’application reste isolée dans son rôle, le playbook se réduit à de l’orchestration.
Prérequis
- ansible-core 2.20 ou supérieur installé. Vérifier avec
ansible --version. - Un projet existant qui tourne en mode playbook monolithique (ou recréez celui du tutoriel précédent).
- Une cible Linux accessible via SSH avec sudo NOPASSWD.
- Niveau attendu : avoir écrit au moins un playbook qui passe l’idempotence.
- Temps : 60 à 90 minutes en lecture active.
Étape 1 — La structure canonique d’un rôle
Tout commence par ansible-galaxy, l’outil livré avec ansible-core qui sait générer le squelette d’un rôle. On crée un répertoire roles à la racine du projet et on y génère un premier rôle nommé nginx :
mkdir -p ansible-lab/roles
cd ansible-lab
ansible-galaxy init roles/nginx
La commande crée huit sous-répertoires plus un fichier README.md. Chacun a un rôle précis et apprend en cinq minutes ce que beaucoup de tutoriels expliquent en confondant tout :
roles/nginx/
├── defaults/main.yml # variables par défaut, surchargeables n'importe où
├── files/ # fichiers à copier tels quels (pas de templating)
├── handlers/main.yml # actions différées, déclenchées par notify:
├── meta/main.yml # métadonnées (auteur, dépendances, licence)
├── tasks/main.yml # le cœur — la liste des actions du rôle
├── templates/ # gabarits Jinja2
├── tests/ # squelette pour les tests (Molecule complétera)
├── vars/main.yml # variables internes, NON surchargeables facilement
└── README.md
La distinction entre defaults/ et vars/ est l’une des plus importantes à intégrer. Les variables de defaults/ sont à la priorité la plus basse — elles servent de valeurs par défaut, surchargées par presque tout (group_vars, host_vars, paramètres passés au rôle). Les variables de vars/ sont à priorité élevée — utilisées pour des constantes internes au rôle qu’on ne veut pas voir surchargées par accident. La règle pratique : tout ce qu’un utilisateur du rôle pourrait vouloir personnaliser va dans defaults/. Tout ce qu’il ne devrait pas toucher reste dans vars/.
Étape 2 — Remplir defaults/main.yml
On commence toujours par les valeurs par défaut, parce que c’est la liste qui décrit l’API publique du rôle. Les autres fichiers vont consommer ces variables ; mieux vaut les nommer correctement dès le début. On édite roles/nginx/defaults/main.yml :
---
nginx_package_state: present
nginx_service_state: started
nginx_service_enabled: true
nginx_user: www-data
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_default_site:
server_name: _
root: /var/www/html
index: index.html
nginx_extra_packages: []
Quelques principes à comprendre. Tous les noms commencent par nginx_ — c’est la convention de Galaxy pour éviter les collisions entre rôles qui définiraient une variable service_state chacun. Les valeurs sont génériques mais sensées : state: present par défaut signifie « le rôle s’assure que Nginx est installé, sans imposer une version particulière ». Un utilisateur qui veut épingler une version surchargera la variable depuis ses group_vars.
Le dictionnaire nginx_default_site décrit la configuration du vhost par défaut. On reviendra dessus à l’étape 4 — pour l’instant, ce sont des valeurs raisonnables qui permettent au rôle de fonctionner sans aucune surcharge.
Étape 3 — Remplir tasks/main.yml
Le fichier de tâches est le cœur du rôle. On l’écrit dans roles/nginx/tasks/main.yml :
---
- name: Installer le paquet nginx et les paquets supplémentaires
ansible.builtin.apt:
name: "{{ ['nginx'] + nginx_extra_packages }}"
state: "{{ nginx_package_state }}"
update_cache: true
cache_valid_time: 3600
- name: Déposer la configuration nginx principale
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Déposer le vhost par défaut
ansible.builtin.template:
src: default-site.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: '0644'
notify: reload nginx
- name: S'assurer que nginx est activé et démarré
ansible.builtin.systemd_service:
name: nginx
state: "{{ nginx_service_state }}"
enabled: "{{ nginx_service_enabled }}"
Trois éléments méritent l’attention. Le premier est le drapeau validate: 'nginx -t -c %s' sur le module template. Avant de remplacer le fichier de configuration, Ansible exécute la commande de validation sur le fichier temporaire — si la syntaxe Nginx est invalide, le déploiement échoue avant d’avoir cassé la configuration en place. C’est cette ceinture de sécurité qui distingue un rôle robuste d’un rôle dangereux.
Le deuxième élément est notify: reload nginx — il déclenche un handler qu’on va définir à l’étape suivante. Un handler ne s’exécute qu’à la fin du play, et seulement si la tâche qui le notifie a effectivement changé l’état du système. Si vous relancez le rôle sans modification, aucun reload n’est déclenché.
Le troisième élément est l’expression "{{ ['nginx'] + nginx_extra_packages }}" qui construit dynamiquement la liste de paquets à installer. Si nginx_extra_packages est vide (cas par défaut), seul nginx est installé. Si l’utilisateur surcharge la variable avec ['nginx-extras', 'libnginx-mod-http-headers-more-filter'], Ansible installe les trois paquets en une seule transaction APT.
Étape 4 — Les handlers
On édite roles/nginx/handlers/main.yml :
---
- name: reload nginx
ansible.builtin.systemd_service:
name: nginx
state: reloaded
- name: restart nginx
ansible.builtin.systemd_service:
name: nginx
state: restarted
On a déclaré deux handlers. Le premier, reload nginx, recharge la configuration sans interrompre les connexions en cours — c’est ce qu’on veut quand on change un vhost ou un fichier de configuration. Le second, restart nginx, redémarre complètement le service — réservé aux changements profonds (paquet upgradé, modules ajoutés). Les tâches notifient l’un ou l’autre selon la nature du changement.
Les handlers s’exécutent une seule fois à la fin du play, même si trois tâches ont notifié reload nginx. C’est l’intelligence du modèle : on coalesce les rechargements pour ne pas redémarrer cinq fois le service en quinze secondes.
Étape 5 — Les gabarits
On crée roles/nginx/templates/nginx.conf.j2 avec un gabarit minimal qui consomme les variables des defaults :
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
On crée également roles/nginx/templates/default-site.conf.j2 :
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name {{ nginx_default_site.server_name }};
root {{ nginx_default_site.root }};
index {{ nginx_default_site.index }};
location / {
try_files $uri $uri/ =404;
}
}
Aucune valeur en dur dans ces gabarits — tout vient des variables. Cette discipline rend le rôle réutilisable. Si demain vous voulez un rôle qui sert un site avec un server_name: blog.example.com et un root: /var/www/blog, il suffit de surcharger le dictionnaire au moment de l’invocation.
Étape 6 — Le playbook qui invoque le rôle
Le playbook se réduit maintenant à de l’orchestration. On crée playbooks/site.yml :
---
- name: Configurer la ferme web
hosts: web
become: true
roles:
- role: nginx
vars:
nginx_extra_packages:
- libnginx-mod-http-headers-more-filter
nginx_default_site:
server_name: lab.example.com
root: /var/www/lab
index: index.html
Cinq lignes utiles. Le rôle nginx est invoqué, deux variables sont surchargées localement : on ajoute un module Nginx et on personnalise le vhost. Le rôle reste générique, le playbook spécifie le contexte.
On exécute :
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
La sortie liste les tâches préfixées par [nginx :] — c’est Ansible qui indique de quel rôle vient chaque tâche. La première exécution affiche plusieurs changed ; la seconde exécution doit être à changed=0, exactement comme pour un playbook monolithique. La structuration en rôle ne change rien à l’idempotence — elle ne fait que ranger le code.
Étape 7 — Le fichier meta/main.yml et les dépendances
Le fichier roles/nginx/meta/main.yml mérite quelques minutes d’attention. C’est lui qui transforme un répertoire en rôle distribuable, qui déclare les compatibilités OS, l’auteur, la licence, et — surtout — les dépendances vers d’autres rôles :
---
galaxy_info:
author: Votre Équipe
description: Installe et configure Nginx avec un vhost paramétrable
license: MIT
min_ansible_version: '2.16'
platforms:
- name: Debian
versions: ['bookworm']
- name: Ubuntu
versions: ['jammy', 'noble']
galaxy_tags:
- web
- nginx
- http
dependencies: []
Le tableau dependencies est ce qui rend les rôles composables. Si votre rôle app a besoin que Nginx soit installé d’abord, on déclare dependencies: [{ role: nginx }] et Ansible exécute le rôle nginx avant app. Cette mécanique évite les playbooks à liste plate de quinze rôles dans l’ordre exact à respecter.
Étape 8 — Variables par environnement avec group_vars
Une fois le rôle propre, la spécialisation par environnement se fait dans les group_vars, jamais dans le rôle lui-même. On crée group_vars/production.yml :
---
nginx_worker_processes: 4
nginx_worker_connections: 4096
nginx_default_site:
server_name: app.example.com
root: /var/www/app/public
index: index.html
Et group_vars/staging.yml :
---
nginx_worker_processes: 2
nginx_worker_connections: 1024
Avec un inventaire qui place les hôtes de production dans le groupe production et ceux de staging dans staging, le même rôle nginx produit deux configurations différentes selon l’environnement. Vous n’avez écrit le rôle qu’une fois.
Étape 9 — Réutiliser des rôles externes
Galaxy publie des milliers de rôles maintenus par la communauté. Pour des tâches courantes — Docker, PostgreSQL, fail2ban — il est presque toujours préférable d’utiliser un rôle existant que de réinventer le sien. Le mainteneur de référence est Jeff Geerling, dont les rôles geerlingguy.docker, geerlingguy.postgresql, geerlingguy.security sont la base implicite d’une grande partie de l’écosystème.
On déclare ces rôles dans roles/requirements.yml :
---
roles:
- name: geerlingguy.docker
version: 7.4.0
- name: geerlingguy.security
version: 2.4.0
On les installe avec :
ansible-galaxy role install -r roles/requirements.yml -p roles/
Le drapeau -p roles/ place les rôles dans le répertoire local du projet plutôt que dans le cache global, ce qui rend le projet auto-suffisant. Les versions sont épinglées — un upgrade volontaire passe par un bump explicite du fichier requirements.yml et un --force à l’install.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
Variable de defaults ignorée par les group_vars |
La variable a été dupliquée dans vars/main.yml du rôle |
Tout ce qui doit être surchargeable va dans defaults, jamais dans vars |
| Handler ne se déclenche pas | Le nom dans notify: ne correspond pas exactement au name: du handler |
Vérifier la casse et l’orthographe — Ansible ne suggère pas de proche |
| Validation nginx échoue | Variable mal échappée dans le gabarit Jinja2 | Tester le rendu avec ansible -m template en mode ad-hoc avant le play complet |
| Rôle Galaxy introuvable | Pas de requirements.yml ou install effectué dans le mauvais répertoire |
ansible-galaxy role install -r roles/requirements.yml -p roles/ à la racine du projet |
| Tâches du rôle exécutées dans le mauvais ordre | Confusion entre roles: et tasks: — les rôles s’exécutent avant les tâches du play |
Pour intercaler, utiliser pre_tasks et post_tasks |
| Conflit entre rôles qui définissent la même variable | Les noms de variables ne sont pas préfixés | Toujours préfixer par le nom du rôle (nginx_*, postgres_*) |
Tutoriels frères
- Installer Ansible et exécuter son premier playbook
- Terraform et Ansible : provisionner puis configurer
Pour creuser ce sujet
- Documentation officielle des rôles
- Ansible Galaxy — index des rôles communautaires
- Les rôles de Jeff Geerling — référence informelle de l’écosystème
- 🔝 Retour à l’article principal du dossier Ansible
FAQ
Quand créer un rôle plutôt que d’allonger un playbook ?
Dès qu’une logique est susceptible d’être appliquée sur plusieurs groupes d’hôtes ou réutilisée dans un autre projet. Un rôle qui n’est utilisé qu’une fois est rarement justifié — un playbook bien commenté suffit.
Faut-il un rôle par paquet à installer ?
Non. Un rôle traite un service applicatif cohérent : Nginx (paquet + config + vhosts + handlers), PostgreSQL (paquet + cluster + utilisateurs + bases). Découper plus finement crée une jungle de rôles qui se déclenchent en cascade — la complexité que les rôles devaient justement résorber.
Pourquoi defaults/main.yml a la priorité la plus basse ?
Précisément pour permettre la surcharge. Si defaults avait une priorité élevée, un utilisateur du rôle ne pourrait pas paramétrer le service depuis ses group_vars ou host_vars. La hiérarchie de priorité Ansible est documentée dans la page officielle sur la précédence des variables ; lisez-la une fois, accrochez-la au mur.
Comment tester un rôle isolément ?
Avec Molecule. Un rôle bien fait s’accompagne d’une configuration molecule/default/molecule.yml qui provisionne un conteneur, applique le rôle, vérifie l’idempotence, lance des assertions, puis détruit le conteneur. C’est l’objet d’un tutoriel dédié dans la suite.
Que mettre dans files/ qui ne pourrait pas être dans templates/ ?
Tout ce qui est binaire ou volumineux et qui n’a pas besoin de templating : un binaire à déployer, un certificat racine, une image. Le module copy consomme files/, le module template consomme templates/. Ne mélangez pas — un fichier figé qui passe par template ralentit le run et expose à des bugs si le contenu inclut accidentellement la syntaxe Jinja.
Comment lister les rôles installés ?
ansible-galaxy role list affiche les rôles trouvés dans les chemins de recherche, avec leur version. Utile pour vérifier ce qu’un environnement CI a réellement installé après un requirements.yml.