Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 defaults et vars ;
  • passer des valeurs différentes au même rôle d'un groupe à l'autre ;
  • choisir entre roles, import_role et include_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 socle contient 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 web contient 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 :

bash
ansible-galaxy role init roles/socle
text
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
roles/socle/tasks/main.ymlles tâches, point d'entrée du rôle, exécutées en premierhandlers/main.ymlles handlers : Redémarrer chrony, Recharger SSHtemplates/les modèles Jinja2 : chrony.conf.j2, sshd-sorbier.conf.j2files/les fichiers copiés tels quels par copydefaults/main.ymlvariables réglables : priorité la plus faiblevars/main.ymlvariables internes : priorité élevéemeta/main.ymlauteur, licence, plateformes, dépendancestemplates/, files/ et handlers/ sont trouvés par les tâches du rôle sans qu'on donne de chemin.
Chaque dossier d'un rôle a un rôle précis ; Ansible les trouve sans qu'on donne de chemin
DossierContenuUtilisé par
tasks/main.ymlLes tâches, point d'entrée du rôleToujours exécuté
handlers/main.ymlLes handlers du rôleLes notify des tâches
templates/Les modèles Jinja2 (.j2)template avec un src sans chemin
files/Les fichiers copiés tels quelscopy avec un src sans chemin
defaults/main.ymlLes valeurs par défaut des variablesPriorité la plus faible
vars/main.ymlDes variables internes, rarement utilesPriorité élevée
meta/main.ymlMétadonnées et dépendancesansible-galaxy, Ansible
README.mdLa documentation : rôle, variables, exempleLes 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.

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

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

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

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

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

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

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

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

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

yaml
  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_roleinclude_role
Moment du chargementAu lancementAu lancement (statique)Pendant l'exécution (dynamique)
PositionAvant les tasks du playOù on l'écritOù on l'écrit
whenS'applique à chaque tâche du rôleS'applique à chaque tâche du rôleDécide d'inclure ou non le rôle entier
Boucle (loop)NonNonOui
--list-tasks, --tagsVoient les tâches du rôleVoient les tâches du rôleNe 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.yml contient les variables réglables : leur priorité est la plus faible de toutes, donc n'importe quelle valeur de l'inventaire, du playbook ou de -e les remplace. C'est là que va presque tout.
  • vars/main.yml contient 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 :

yaml
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 ?

  1. La tâche qui installe chrony.
  2. Le modèle chrony.conf.j2.
  3. La variable ntp_serveurs, que chaque site doit pouvoir changer.
  4. Le handler « Redémarrer chrony ».
  5. La variable ntp_service: chrony, que personne ne doit modifier.
  6. Un fichier leap-seconds.list copié tel quel.
Voir la réponseMasquer la réponse de l'exercice 1
  1. tasks/main.yml.
  2. templates/chrony.conf.j2.
  3. defaults/main.yml : priorité minimale, chaque site la remplace dans son inventaire.
  4. handlers/main.yml.
  5. vars/main.yml : elle ne doit pas être remplacée par l'inventaire.
  6. 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é ?

Chapitre terminé !

Fais les 2 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Ansible Vault : chiffrer les secretsRevoir le sommaire du cours