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

AstuceÀ 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é.

AstuceC’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

...

AstuceNotez 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 natifs apt, dnf et zypper 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 variables chrony_package, chrony_service et chrony_confdir et utilisera le module de gestion de paquets générique package.

ImportantAttention : 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.

 

Catégories : Formation

0 commentaire

Laisser un commentaire

Emplacement de l’avatar

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *