Chapitre 825 minIntermédiaire
Rôles Ansible : créer et réutiliser un rôle
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- découper un playbook en rôles réutilisables ;
- placer chaque fichier dans la bonne partie du rôle ;
- distinguer
defaultsetvars; - passer des valeurs différentes au même rôle d'un groupe à l'autre ;
- choisir entre
roles,import_roleetinclude_role.
2 exercices corrigés1 schéma
Un rôle Ansible est un dossier à l'arborescence normalisée qui regroupe tout ce qu'il faut pour une fonction (tâches, handlers, modèles, fichiers, variables par défaut), afin de l'appliquer à n'importe quel groupe d'hôtes et de la réutiliser d'un projet à l'autre. Ce chapitre crée les deux rôles de Sorbier, socle et web, explique la différence entre defaults et vars, comment passer des valeurs à un rôle et les différentes façons de l'appeler.
Pourquoi découper en rôles
Au fil des chapitres, le playbook de Sorbier grossit : paquets, heure, comptes, clés, SSH, pare-feu, puis Apache, son site, ses modules. Un seul fichier de 300 lignes devient difficile à lire, à relire et à réutiliser. Les rôles découpent ce travail par fonction :
- le rôle
soclecontient ce que reçoit tout serveur Debian de Sorbier : paquets de base, fuseau horaire, synchronisation de l'heure, comptes des administrateurs, durcissement SSH, pare-feu ; - le rôle
webcontient ce qui fait un serveur web : Apache, ses modules, l'hôte virtuel, l'ouverture des ports.
Le playbook principal se réduit alors à quelques lignes qui disent quel rôle va sur quel groupe. Un nouveau serveur web reçoit socle puis web ; un futur serveur de base de données recevra socle puis un rôle bdd.
L'arborescence d'un rôle
La commande ansible-galaxy role init crée le squelette :
ansible-galaxy role init roles/socle
roles/socle/
├── README.md
├── defaults/
│ └── main.yml
├── files/
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── tasks/
│ └── main.yml
├── templates/
├── tests/
│ ├── inventory
│ └── test.yml
└── vars/
└── main.yml
| Dossier | Contenu | Utilisé par |
|---|---|---|
tasks/main.yml | Les tâches, point d'entrée du rôle | Toujours exécuté |
handlers/main.yml | Les handlers du rôle | Les notify des tâches |
templates/ | Les modèles Jinja2 (.j2) | template avec un src sans chemin |
files/ | Les fichiers copiés tels quels | copy avec un src sans chemin |
defaults/main.yml | Les valeurs par défaut des variables | Priorité la plus faible |
vars/main.yml | Des variables internes, rarement utiles | Priorité élevée |
meta/main.yml | Métadonnées et dépendances | ansible-galaxy, Ansible |
README.md | La documentation : rôle, variables, exemple | Les collègues |
Seul tasks/main.yml est obligatoire ; supprime les dossiers vides que tu n'utilises pas (tests/, vars/). Le nom d'un rôle suit les mêmes règles qu'une variable : minuscules, chiffres, tirets bas.
Le rôle socle
roles/socle/defaults/main.yml : toutes les valeurs réglables, avec une valeur raisonnable.
---
socle_paquets:
- chrony
- curl
- htop
- sudo
- ufw
- unattended-upgrades
- vim
socle_fuseau: Europe/Paris
# Réseau d'administration : seule source autorisée en SSH
socle_reseau_admin: 192.168.10.208/28
# Ouvertures propres au parc ; complétées hôte par hôte dans host_vars
socle_ufw_regles:
- {port: "10050", src: 192.168.10.180, comment: Agent de supervision}
socle_ufw_regles_hote: []
socle_admins:
- login: amercier
nom: Antoine Mercier
cle: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDm6Ks0yWq3k1P6cX5L2dYv8wXo0fJ9tq3hR4uE7nB2a amercier@pc-admin
- login: clambert
nom: Chloé Lambert
cle: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF3pQ8rT1mZ6sV0yK4nD9cW2xJ7hB5eL8aG1uR3tN6oP clambert@pc-admin
socle_admins_partis: []
# Clés autorisées pour le compte ansible, depuis le bastion seulement
socle_ip_bastion: 192.168.10.210
socle_cles_ansible:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF3pQ8rT1mZ6sV0yK4nD9cW2xJ7hB5eL8aG1uR3tN6oP clambert@bastion
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq7Wm2sY5tH8vB3nR6xL1cF9pD4eJ0aU2iO7gZ5wQ3r mdiallo@bastion
roles/socle/tasks/main.yml : les tâches, dans l'ordre. Le pare-feu est activé après l'ouverture de SSH, sinon Ansible se couperait lui-même la connexion.
---
- name: Installer les paquets du socle
ansible.builtin.apt:
name: "{{ socle_paquets }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Régler le fuseau horaire
community.general.timezone:
name: "{{ socle_fuseau }}"
- name: Synchroniser l'heure sur les contrôleurs de domaine
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: "0644"
notify: Redémarrer chrony
- name: Créer les comptes des administrateurs
ansible.builtin.user:
name: "{{ item.login }}"
comment: "{{ item.nom }}"
groups: sudo
append: true
shell: /bin/bash
loop: "{{ socle_admins }}"
loop_control:
label: "{{ item.login }}"
- name: Déposer la clé publique de chaque administrateur
ansible.posix.authorized_key:
user: "{{ item.login }}"
key: "{{ item.cle }}"
exclusive: true
loop: "{{ socle_admins }}"
loop_control:
label: "{{ item.login }}"
- name: Autoriser les clés des administrateurs sur le compte ansible
ansible.posix.authorized_key:
user: ansible
key: "{{ socle_cles_ansible | join('\n') }}"
key_options: from="{{ socle_ip_bastion }}"
exclusive: true
- name: Supprimer les comptes des administrateurs partis
ansible.builtin.user:
name: "{{ item }}"
state: absent
remove: true
loop: "{{ socle_admins_partis }}"
- name: Durcir le serveur SSH
ansible.builtin.template:
src: sshd-sorbier.conf.j2
dest: /etc/ssh/sshd_config.d/10-sorbier.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Recharger SSH
- name: Autoriser SSH depuis le réseau d'administration
community.general.ufw:
rule: allow
port: "22"
proto: tcp
src: "{{ socle_reseau_admin }}"
comment: SSH depuis ADMIN
- name: Ouvrir les ports des services du serveur
community.general.ufw:
rule: allow
port: "{{ item.port }}"
proto: "{{ item.proto | default('tcp') }}"
src: "{{ item.src | default(omit) }}"
comment: "{{ item.comment }}"
loop: "{{ socle_ufw_regles + socle_ufw_regles_hote }}"
loop_control:
label: "{{ item.comment }}"
- name: Activer le pare-feu, entrant refusé par défaut
community.general.ufw:
state: enabled
default: deny
direction: incoming
Le pare-feu ne laisse entrer que ce qui est listé : SSH depuis le réseau d'administration, l'agent de supervision depuis srv-journaux (TCP 10050, voir le cours Supervision ; le même serveur reçoit les journaux de sécurité analysés par Wazuh), et les ports propres à chaque serveur. Ces derniers vont dans host_vars. Par exemple, host_vars/srv-journaux.yml reçoit :
socle_ufw_regles_hote:
- {port: "6514", comment: Syslog TLS}
- {port: "10051", comment: Serveur de supervision}
- {port: "162", proto: udp, comment: Traps SNMP}
Sans ces lignes, le socle couperait la collecte des journaux de toutes les zones, les données envoyées au serveur de supervision et les alertes des switchs. C'est le piège d'un pare-feu « entrant refusé » appliqué à tout un groupe : avant de lancer le rôle, fais la liste de ce que chaque serveur doit recevoir. Une liste propre à l'hôte, additionnée à la liste commune, évite de recopier les règles communes dans chaque host_vars (Ansible remplace les listes au lieu de les fusionner).
Deux autres détails. exclusive: true sur les clés retire toute clé qui n'est pas dans la liste : une clé ajoutée à la main sur un serveur disparaît au passage suivant, et retirer une personne de socle_cles_ansible lui coupe l'accès partout. Avec plusieurs clés et exclusive, il faut les passer en une seule chaîne, une clé par ligne (d'où le join('\n')), car l'option ne fonctionne pas dans une boucle.
roles/socle/handlers/main.yml :
---
- name: Redémarrer chrony
ansible.builtin.service:
name: chrony
state: restarted
- name: Recharger SSH
ansible.builtin.service:
name: ssh
state: reloaded
Les modèles chrony.conf.j2 et sshd-sorbier.conf.j2 du chapitre 7 vont dans roles/socle/templates/. Le service SSH s'appelle ssh sur Debian (et sshd sur Red Hat) : un rôle qui vise plusieurs familles de systèmes met ce nom dans une variable.
Reste roles/socle/meta/main.yml, qui décrit le rôle : auteur, licence, plateformes visées (Debian trixie), dépendances. Le chapitre 15 le montre, là où tu apprends à lire celui des rôles écrits par d'autres.
Le rôle web
roles/web/defaults/main.yml :
---
web_nom_site: suivi.sorbier.example
web_racine: /var/www/suivi
web_admin_mail: informatique@sorbier.example
web_certificat: /etc/ssl/certs/suivi.sorbier.example.pem
web_cle_privee: /etc/ssl/private/suivi.sorbier.example.key
roles/web/tasks/main.yml :
---
- name: Installer Apache
ansible.builtin.apt:
name: apache2
state: present
update_cache: true
cache_valid_time: 3600
- name: Activer les modules ssl, headers et rewrite
community.general.apache2_module:
name: "{{ item }}"
state: present
loop:
- ssl
- headers
- rewrite
notify: Redémarrer Apache
- name: Créer la racine du site
ansible.builtin.file:
path: "{{ web_racine }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Déployer l'hôte virtuel
ansible.builtin.template:
src: site.conf.j2
dest: /etc/apache2/sites-available/{{ web_nom_site }}.conf
owner: root
group: root
mode: "0644"
notify: Recharger Apache
- name: Activer le site
ansible.builtin.file:
src: /etc/apache2/sites-available/{{ web_nom_site }}.conf
dest: /etc/apache2/sites-enabled/{{ web_nom_site }}.conf
state: link
notify: Recharger Apache
- name: Désactiver le site par défaut
ansible.builtin.file:
path: /etc/apache2/sites-enabled/000-default.conf
state: absent
notify: Recharger Apache
- name: Ouvrir HTTP et HTTPS dans le pare-feu
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
- name: Vérifier que le service tourne et démarre au boot
ansible.builtin.service:
name: apache2
state: started
enabled: true
Activer un site sous Debian, c'est créer un lien de sites-enabled/ vers sites-available/ : c'est ce que fait la commande a2ensite, et ce que fait ici le module file avec state: link, de façon idempotente.
roles/web/handlers/main.yml : avant de recharger Apache, on vérifie sa configuration. Le premier handler écoute (listen) les deux notifications ; comme les handlers s'exécutent dans leur ordre d'écriture, le test passe toujours avant le rechargement, et une configuration invalide arrête tout avant qu'Apache ne la lise.
---
- name: Tester la configuration d'Apache
ansible.builtin.command: apache2ctl configtest
changed_when: false
listen:
- Recharger Apache
- Redémarrer Apache
- name: Recharger Apache
ansible.builtin.service:
name: apache2
state: reloaded
- name: Redémarrer Apache
ansible.builtin.service:
name: apache2
state: restarted
L'activation d'un module (ssl) exige un redémarrage ; une modification de l'hôte virtuel, un simple rechargement. Si les deux sont notifiés au même passage, Apache est rechargé puis redémarré : c'est un peu redondant, mais toujours correct.
Utiliser les rôles dans un playbook
Le playbook principal site.yml de Sorbier :
---
- name: Appliquer le socle à tous les serveurs Linux
hosts: linux
become: true
roles:
- socle
- name: Configurer le serveur web
hosts: serveurs_web
become: true
roles:
- web
ansible-playbook site.yml --list-tasks affiche les tâches préfixées par leur rôle (socle : Installer les paquets du socle). Les rôles d'un play s'exécutent avant ses tasks.
Pour passer des valeurs à un rôle dans le playbook, plutôt que dans l'inventaire :
roles:
- role: web
vars:
web_nom_site: intranet.sorbier.example
web_racine: /var/www/intranet
On peut aussi appeler un rôle comme une tâche, au milieu d'autres tâches :
tasks:
- name: Préparer le dossier des sauvegardes
ansible.builtin.file:
path: /var/backups/suivi
state: directory
mode: "0750"
- name: Appliquer le rôle web
ansible.builtin.import_role:
name: web
- name: Appliquer le rôle de supervision sur les serveurs de production
ansible.builtin.include_role:
name: supervision
when: "'production' in group_names"
roles: | import_role | include_role | |
|---|---|---|---|
| Moment du chargement | Au lancement | Au lancement (statique) | Pendant l'exécution (dynamique) |
| Position | Avant les tasks du play | Où on l'écrit | Où on l'écrit |
when | S'applique à chaque tâche du rôle | S'applique à chaque tâche du rôle | Décide d'inclure ou non le rôle entier |
Boucle (loop) | Non | Non | Oui |
--list-tasks, --tags | Voient les tâches du rôle | Voient les tâches du rôle | Ne voient pas les tâches avant l'exécution |
Retiens : roles: ou import_role dans la grande majorité des cas ; include_role quand le choix du rôle dépend d'une valeur connue seulement à l'exécution, ou pour boucler.
defaults ou vars ?
C'est la question qui revient le plus sur les rôles.
defaults/main.ymlcontient les variables réglables : leur priorité est la plus faible de toutes, donc n'importe quelle valeur de l'inventaire, du playbook ou de-eles remplace. C'est là que va presque tout.vars/main.ymlcontient des variables internes au rôle, qu'on ne veut pas voir remplacées par l'inventaire : leur priorité est au-dessus de l'inventaire et du play. Exemple : le nom du paquet ou du service selon la distribution.
Les dépendances entre rôles
meta/main.yml peut déclarer qu'un rôle a besoin d'un autre :
dependencies:
- role: socle
Avec cette ligne dans le rôle web, appliquer web applique d'abord socle. Un rôle ne s'exécute qu'une fois par play. Mais le site.yml de Sorbier applique déjà socle au groupe linux dans un premier play : la dépendance le ferait rejouer sur srv-web dans le second, sans danger (tout est idempotent), mais pour rien. Sorbier préfère des rôles sans dépendances, assemblés explicitement par le playbook.
Les rôles écrits par d'autres et publiés sur Ansible Galaxy sont une autre sorte de dépendance : celle du dépôt tout entier. On les déclare dans requirements.yml, avec une version figée, comme les collections ; le chapitre 15 explique comment les choisir, les installer et les relire avant de leur confier tes serveurs.
Exercices
Exercice 1 sur 2 Ranger les fichiers Réussi À revoir
Tu transformes un playbook en rôle ntp. Dans quel dossier du rôle va chaque élément ?
- La tâche qui installe chrony.
- Le modèle
chrony.conf.j2. - La variable
ntp_serveurs, que chaque site doit pouvoir changer. - Le handler « Redémarrer chrony ».
- La variable
ntp_service: chrony, que personne ne doit modifier. - Un fichier
leap-seconds.listcopié tel quel.
Voir la réponseMasquer la réponse de l'exercice 1
tasks/main.yml.templates/chrony.conf.j2.defaults/main.yml: priorité minimale, chaque site la remplace dans son inventaire.handlers/main.yml.vars/main.yml: elle ne doit pas être remplacée par l'inventaire.files/leap-seconds.list.
Tu avais trouvé ?
Exercice 2 sur 2 Pourquoi la valeur ne change pas ? Réussi À revoir
Chloé a écrit web_racine: /var/www/suivi dans roles/web/vars/main.yml. Pour un nouveau serveur, elle met web_racine: /srv/intranet dans host_vars/srv-web2.yml, lance le playbook : le site est toujours créé dans /var/www/suivi. Explique et corrige.
Voir la réponseMasquer la réponse de l'exercice 2
Les variables de vars/main.yml d'un rôle ont une priorité supérieure à celles de l'inventaire (host_vars, group_vars) : la valeur du rôle l'emporte. Il faut déplacer web_racine dans roles/web/defaults/main.yml, dont la priorité est la plus faible : la valeur de host_vars/srv-web2.yml s'imposera alors, et les autres serveurs garderont la valeur par défaut. Vérification : ansible-inventory --host srv-web2 montre la variable d'inventaire, mais pas la priorité du rôle ; un ansible.builtin.debug de web_racine dans le playbook montre la valeur réellement utilisée.
Tu avais trouvé ?