Voici le quinzième article de la formation Ansible. Dans mon précédent article, nous avons vu en détail les facts et les variables implicites (magic vars) et le rôle que ces éléments peuvent jouer dans un playbook. Aujourd’hui nous allons nous intéresser à un cas de figure où nous avons affaire à des Target Hosts hétérogènes. Nous allons voir une série d’approches pour gérer les différences entre les différents systèmes.
Atelier pratique
Placez-vous dans le répertoire du dix-septième atelier pratique :
$ cd ~/formation-ansible/atelier-17
Voici les cinq machines virtuelles de cet atelier :
Machine virtuelle | Adresse IP |
---|---|
ansible |
10.23.45.10 |
rocky |
10.23.45.20 |
debian |
10.23.45.30 |
suse |
10.23.45.40 |
ubuntu |
10.23.45.50 |
Démarrez les VM :
$ vagrant up
Connectez-vous au Control Host :
$ vagrant ssh ansible
L’environnement de cet atelier est préconfiguré et prêt à l’emploi :
- Ansible est installé sur le Control Host.
- Le fichier
/etc/hosts
du Control Host est correctement renseigné. - L’authentification par clé SSH est établie sur les trois Target Hosts.
- Le répertoire du projet existe et contient une configuration de base et un inventaire.
- Direnv est installé et activé pour le projet.
- Le validateur de syntaxe
yamllint
est également disponible.
Rendez-vous dans le répertoire des playbooks :
$ cd ansible/projets/ema/playbooks/ direnv: loading ~/ansible/projets/ema/.envrc direnv: export +ANSIBLE_CONFIG
Exécution conditionnelle d’une tâche
Les tâches (tasks) d’un playbook peuvent être liées à une condition, c’est-à-dire qu’elles s’exécutent uniquement lorsqu’un ou plusieurs prérequis sont remplis. Pour ce faire, on utilise le paramètre when
en combinaison avec des facts ou d’autres variables.
Un exemple pratique vous permettra tout de suite de mieux comprendre. Voici un playbook qui affiche uniquement les hôtes qui tournent sur un système basé sur Debian :
--- # hello-debian.yml - hosts: all tasks: - name: Check if target host is a Debian-based system debug: msg: "This is a Debian-based system." when: ansible_os_family == "Debian" ...
Voilà le résultat :
TASK [Check if target host is a Debian-based system] *************************** skipping: [rocky] ok: [debian] => { "msg": "This is a Debian-based system." } skipping: [suse] ok: [ubuntu] => { "msg": "This is a Debian-based system." }
Dans l’exemple ci-dessous, on utilise l’opérateur not
pour inverser la condition :
--- # hello-non-debian.yml - hosts: all tasks: - name: Check if target host is not a Debian-based system debug: msg: "This is not a Debian-based system." when: ansible_os_family != "Debian" ...
L’opérateur in
permet de vérifier l’occurrence dans une liste :
--- # hello-others.yml - hosts: all tasks: - name: Check if target host is running Rocky or SUSE debug: msg: "This is either Rocky or SUSE." when: ansible_distribution in ["Rocky", "openSUSE Leap"] ...
Il est parfois nécessaire de vérifier la version d’un système ou d’un composant logiciel. Ansible offre un test version
pour ce cas de figure. Dans l’exemple ci-dessous, on utilise également l’opérateur and
pour combiner deux conditions :
--- # system-check.yml - hosts: all tasks: - name: Check if we're running Ubuntu 20.04 or higher debug: msg: We're running Ubuntu 20.04 or higher when: ansible_distribution == "Ubuntu" and ansible_distribution_version is version('20.04', '>=') ...
Gérer les cibles hétérogènes
Maintenant que nous savons utiliser les conditions, nous avons tous les outils nécessaires pour gérer une installation sur des cibles qui n’utilisent pas la même distribution. Concrètement, nous allons reprendre notre installation Apache et l’étendre sur l’ensemble de nos Target Hosts. Le premier problème à résoudre, c’est que le paquet Apache n’a pas le même nom selon la distribution que vous utilisez.
Les gros sabots
Voici une première idée pas très subtile pour différencier vos Target Hosts :
--- # apache-01.yml
- hosts: all
tasks:
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache on Debian/Ubuntu
apt:
name: apache2
when: ansible_os_family == "Debian"
- name: Install Apache on Rocky Linux
dnf:
name: httpd
when: ansible_distribution == "Rocky"
- name: Install Apache on SUSE Linux
zypper:
name: apache2
when: ansible_distribution == "openSUSE Leap"
...
Cette méthode fonctionne dans la mesure où une poule est capable de voler et un cheval de nager. Exécutez le playbook, et vous verrez que les paquets s’installent certes comme prévu. Il n’empêche qu’on a sorti les gros sabots et que l’approche est peu élégante, puisqu’il faudrait opérer la distinction pour chacune des tâches :
- Le nom du paquet Apache est différent.
- Le service correspondant n’a pas le même nom.
- La page web par défaut n’a pas le même emplacement.
- Le fichier de configuration n’est pas rangé au même endroit non plus.
Conclusion : on laisse tomber et on cherche une autre solution.
Un peu plus subtil
Voici un playbook revu et amélioré avec une approche un peu plus subtile :
--- # apache-02.yml
- hosts: all
tasks:
- name: Parameters for Debian/Ubuntu
set_fact:
apache_package_name: apache2
when: ansible_os_family == "Debian"
- name: Parameters for Rocky Linux
set_fact:
apache_package_name: httpd
when: ansible_distribution == "Rocky"
- name: Parameters for SUSE Linux
set_fact:
apache_package_name: apache2
when: ansible_distribution == "openSUSE Leap"
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache
package:
name: "{{apache_package_name}}"
...
Dans un premier temps, on définit systématiquement une variable qui contient le nom du paquet Apache. Ensuite, il ne reste plus qu’à installer le paquet grâce au module générique package
.
À première vue, on se complique un peu la tâche ici. Mais une fois que vous compris que les trois premières tâches nous permettent de définir toutes les propriétés spécifiques des cibles, vous commencez à comprendre qu’on est peut-être sur une bonne piste.
On va donc poursuivre cette idée et l’étendre sur les deux premières tâches à exécuter :
--- # apache-03.yml
- hosts: all
tasks:
- name: Parameters for Debian/Ubuntu
set_fact:
apache_package_name: apache2
apache_service_name: apache2
when: ansible_os_family == "Debian"
- name: Parameters for Rocky Linux
set_fact:
apache_package_name: httpd
apache_service_name: httpd
when: ansible_distribution == "Rocky"
- name: Parameters for SUSE Linux
set_fact:
apache_package_name: apache2
apache_service_name: apache2
when: ansible_distribution == "openSUSE Leap"
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache
package:
name: "{{apache_package_name}}"
- name: Start Apache & enable it on boot
service:
name: "{{apache_service_name}}"
state: started
enabled: true
...
Beaucoup plus subtil
Selon un adage un peu moins connu, différents chemins mènent à Saint-Bauzille-de-Putois. Voici une approche encore plus subtile pour installer et lancer Apache sur des cibles hétérogènes :
--- # apache-04.yml
- hosts: all
vars:
apache:
Debian:
package_name: apache2
service_name: apache2
Ubuntu:
package_name: apache2
service_name: apache2
Rocky:
package_name: httpd
service_name: httpd
openSUSE Leap:
package_name: apache2
service_name: apache2
tasks:
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache
package:
name: "{{apache[ansible_distribution].package_name}}"
- name: Start Apache & enable it on boot
service:
name: "{{apache[ansible_distribution].service_name}}"
state: started
enabled: true
...
Ici on met en place une structure de données dans laquelle les valeurs sont disponibles en utilisant le nom de la distribution comme clé.
C’est déjà plus élégant comme approche, et vous noterez au passage qu’elle permet de se passer des distinctions de cas de figure avec when
(sauf pour le rafraîchissement des paquets sous Debian/Ubuntu que le module générique package
ne gère pas).
Carrément plus subtil
Enfin, une dernière possibilité consiste en une approche modulaire qui se distingue par une certaine élégance, même si on se complique la vie un petit peu :
--- # apache-05.yml
- hosts: all
tasks:
- include_vars: >
apache_{{ansible_distribution|lower|replace(" ", "-")}}.yml
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache
package:
name: "{{apache_package_name}}"
- name: Start Apache & enable it on boot
service:
name: "{{apache_service_name}}"
state: started
enabled: true
...
Ici, les paramètres spécifiques à la distribution sont enregistrés dans un fichier externe apache_<DISTRIBUTION>.yml
. On se retrouve donc avec quatre fichiers supplémentaires :
apache_debian.yml
apache_rocky.yml
apache_opensuse-leap.yml
apache_ubuntu.yml
La directive include_vars
va rechercher ces fichiers dans le répertoire du playbook ou dans un sous-répertoire vars/
à côté du playbook. J’ai préféré cette deuxième variante pour plus de lisibilité.
Debian:
--- # vars/apache_debian.yml
apache_package_name: apache2
apache_service_name: apache2
...
OpenSUSE Leap:
--- # vars/apache_opensuse-leap.yml
apache_package_name: apache2
apache_service_name: apache2
...
Rocky:
--- # vars/apache_rocky.yml
apache_package_name: httpd
apache_service_name: httpd
...
Ubuntu:
--- # vars/apache_ubuntu.yml
apache_package_name: apache2
apache_service_name: apache2
...
Notez en passant qu’on aurait pu remplacer le fichier apache_ubuntu.yml
par un simple lien symbolique vers apache_debian.yml
, étant donné que sous le capot, Ubuntu est essentiellement un système Debian.
Le playbook complet
Nos compétences nouvellement acquises nous permettent à présent d’effectuer une installation complète d’Apache sur toutes nos cibles :
--- # apache-06.yml
- hosts: all
tasks:
- name: Load distribution-specific parameters
include_vars: >
apache_{{ansible_distribution|lower|replace(" ", "-") }}.yml
- name: Update package information on Debian/Ubuntu
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Apache
package:
name: "{{apache_package_name}}"
- name: Start Apache & enable it on boot
service:
name: "{{apache_service_name}}"
state: started
enabled: true
- name: Install custom web page
copy:
dest: "{{apache_document_root}}/index.html"
mode: 0644
content: |
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Test</title>
</head>
<body>
<h1>My Ansible-managed website</h1>
</body>
</html>
- name: Configure redirect
copy:
dest: "{{apache_config_directory}}/redirect.conf"
content: |
Redirect /start http://www.startpage.com
notify: Reload Apache
- name: Activate redirect on Debian/Ubuntu
command:
cmd: a2enconf redirect
creates: /etc/apache2/conf-enabled/redirect.conf
when: ansible_os_family == "Debian"
handlers:
- name: Reload Apache
service:
name: "{{apache_service_name}}"
state: reloaded
...
Et voici la liste complète des fichiers de variables externes.
Debian :
--- # vars/apache_debian.yml apache_package_name: apache2 apache_service_name: apache2 apache_document_root: /var/www/html apache_config_directory: /etc/apache2/conf-available ...
OpenSUSE Leap:
--- # vars/apache_opensuse-leap.yml apache_package_name: apache2 apache_service_name: apache2 apache_document_root: /srv/www/htdocs apache_config_directory: /etc/apache2/conf.d ...
Rocky:
--- # vars/apache_rocky.yml apache_package_name: httpd apache_service_name: httpd apache_document_root: /var/www/html apache_config_directory: /etc/httpd/conf.d ...
Ubuntu:
--- # vars/apache_ubuntu.yml apache_package_name: apache2 apache_service_name: apache2 apache_document_root: /var/www/html apache_config_directory: /etc/apache2/conf-available ...
Exercice
Écrivez successivement deux playbooks pour mettre en place la synchronisation NTP avec Chrony sur vos quatre Target Hosts sous Debian, Rocky Linux, SUSE Linux et Ubuntu. Il vous faudra identifier le nom du paquet, le service correspondant et le chemin vers le fichier de configuration par défaut pour chacune des distributions.
Voici la configuration qu’il faudra installer sur chacune des quatre cibles :
# chrony.conf
server 0.fr.pool.ntp.org iburst
server 1.fr.pool.ntp.org iburst
server 2.fr.pool.ntp.org iburst
server 3.fr.pool.ntp.org iburst
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony
- Le premier playbook
chrony-01.yml
utilisera les modules de gestion de paquets natifsapt
,dnf
etzypper
et s’inspirera de la méthode « gros sabots » utilisée plus haut dans cet article. - Le deuxième playbook
chrony-02.yml
définira trois variableschrony_package
,chrony_service
etchrony_confdir
et utilisera le module de gestion de paquets génériquepackage
.
Attention : vous mettez en place un service et une configuration personnalisée. Vous aurez donc forcément un handler
avec un notify
correspondant quelque part dans vos deux playbooks.
Lire la suite : Jinja & Templates
La rédaction de cette documentation demande du temps et des quantités significatives de café espresso. Vous appréciez ce blog ? Offrez un café au rédacteur en cliquant sur la tasse.
0 commentaire