Ansible - les rôles

Partie 4

Pour cette quatrième partie, nous nous intéressons aux rôles Ansible.

Les rôles Ansible

Les rôles partagent de nombreux points communs avec les playbooks. En effet, ils utilisent les mêmes instructions et le même code déclaratif. La principale différence est qu'il s'agit d'un ensemble structuré de fichiers et de dossiers qui contiendront chacun une liste d'action précise.

Un rôle Ansible est constitué d'une hiérarchie de sous dossiers dans laquelle Ansible sait qu'il va trouver ce qu'il doit faire.

Exemple de structure d'un rôle. (exemple repris de la Documentation officielle)

site.yml
webservers.yml
fooservers.yml
roles/
    common/
        tasks/
        handlers/
        files/
        templates/
        vars/
        defaults/
        meta/
    webservers/
        tasks/
        defaults/
        meta/

Dans cet exemple :

  • site.yml, webservers.yml et fooservers.yml sont des playbooks.
  • roles/common est un rôle nommé "common"
  • roles/webservers est un rôle nommé "webservers"

Comme vous pouvez le constater, le rôle common et le rôle webservers n'ont pas la même liste de dossiers. Pour qu'un rôle fonctionne, il faut à minima un des dossiers qui contient un fichier main.yml. Les dossiers contiennent les éléments suivants :

  • tasks : Les tâches qui seront jouées par le rôle. C'est la même écriture qu'un Playbook à l'exception qu'il n'y a pas besoin de la déclaration hosts.
  • handlers : Les tâches liées aux handlers seront à déclarer dans ce dossier.
  • defaults : Contient les variables par défaut du rôle. Nous verrons un peu plus loin avec les group_vars / hosts_vars.
  • vars : Une autre section de variables qui a un niveau de priorité sur son application différente du defaults. Elles peuvent servir à surcharger le default.
  • files : Contient des fichiers statiques qu'Ansible peut déployer sur les hosts (exemple : un script).
  • templates : Contient des fichiers dynamiques qu'Ansible peut déployer sur les hosts (exemple : la conf Apache contextualisée selon le serveur).
  • meta : Contient des meta data pour le rôle : la license, les dépendances...

L'intérêt principal du rôle est de pouvoir écrire un code spécialisé sur une liste concrète et cohérente d'actions, et pouvant être réutilisé au travers d'appels via les Playbook. Vulgairement, il faut voir cela comme une fonction d'un langage de programmation par exemple.

Comprendre les interactions inventaire / hosts_vars / group_vars / vars / extra-vars

L'ordre de priorité des variables

Dans sa définition, le rôle peut posséder deux fichiers de variables : le default et le vars. Ansible possède un ordre de priorité pour assigner une valeur à une variable. Il s'agit de la liste suivante, triée du moins prioritaire au plus prioritaire :

  1. Les valeurs passées en ligne de commande (exemple : -u user pour l'utilisateur de connexion)
  2. Le defaults d'un rôle
  3. Le fichier d'inventaire ou le group_vars
  4. Le group_vars/all d'un inventaire
  5. Le group_vars/all d'un playbook
  6. N'importe quel autre fichier dans group_vars/* d'un inventaire
  7. N'importe quel autre fichier dans group_vars/* d'un playbook
  8. Fichier d'inventaire ou hosts_vars
  9. Les fichiers hosts_vars/* d'un inventaire
  10. Les fichiers hosts_vars/* d'un playbook
  11. hosts_facts et set_facts mis en cache
  12. Les variables définies dans le playbook
  13. Les variables demandées au prompt par un playbook
  14. Le fichier vars d'un playbook
  15. Le fichier vars/main.yml d'un rôle
  16. Les variables d'un block (type de tâche spécifique)
  17. Les variables d'une tâche (instruction vars: sur une tâche)
  18. Instruction include_vars
  19. Instruction set_facts et register de variables
  20. Les paramètres du rôle
  21. L'inclusion de paramètres
  22. L'argument extra-vars de la ligne de commande (il aura toujours la priorité absolue)

Concrètement, à titre personnel, je n'utilise pas tout cet ordre de priorité. J'applique généralement le suivant :

  1. Valeurs par défaut spécifiées dans defaults
  2. Surchargées par le group_vars
  3. Eventuellement surchargées par un set_facts dans le code si le besoin s'en fait sentir
  4. Argument extra-vars si le besoin s'en fait sentir

La documentation Ansible possède une section sur les recommandations d'usage des variables.

Comment le lien avec les fichiers de variables est fait

C'est grâce à l'inventaire qu'Ansible sait comment charger quel fichier de variable et déduire selon son ordre de priorité. En effet, quand nous avons construit notre fichier hosts dans l'article dédié, nous avions mis des noms de groupe. Par exemple webservers pour les serveurs web, db pour les bases de données, etc.

Les fichiers que vous mettez dans hosts_vars et group_vars peuvent être nommés selon un groupe de l'inventaire. Ainsi, le fichier group_vars/webservers.yml pourra contenir des variables spécifiques pour les membres du groupe webservers. La portée de ces variables ne concernera que les hosts qui en font partie. C'est la même chose pour le hosts_vars.

Initialiser un rôle

Pour créer un rôle, vous pouvez créer un dossier pourtant son nom et créer l'arborescence attendue. Sinon vous pouvez utiliser la commande ansible-galaxy qui va vous macher le taff :)

A minima, vous devez créer un dossier roles dans votre répertoire de sources Ansible et vous placer dedans.

Ensuite, lancez la commande suivante :

$ ansible-galaxy init monrole --offline
- Role monrole was created successfully

Dans l'introduction nous avions vu la commande ansible-galaxy dans la liste des commandes Ansible. Il s'agit de celle qui fait le lien avec le repository public de rôles maintenu par Ansible. L'argument --offline permet d'initialiser le rôle directement sans qu'il ne cherche à se connecter aux dépôts.

Un dossier "monrole" vient d'être créé avec toute la structure de base.

monrole/
├── defaults
   └── main.yml
├── files
├── handlers
   └── main.yml
├── meta
   └── main.yml
├── README.md
├── tasks
   └── main.yml
├── templates
├── tests
   ├── inventory
   └── test.yml
└── vars
    └── main.yml

Un rôle simple

Nous allons créer un rôle simple de déploiement Apache HTTPD avec installation du service avec la dernière version disponible et déploiement de la conf par Ansible. Nous testons au préalable si l'exécution est faite sur un Linux de famille Red Hat car le module yum ne fonctionnera pas sur une Debian par exemple.

On initialise le rôle.

$ ansible-galaxy init webserver --offline
- Role webserver was created successfully

Dans roles/webserver/tasks/main.yml :

---
- name: "Check host compatibility"
  assert:
    that: ansible_facts['os_family']|lower == 'redhat'
    msg: "Unsupported OS, this role works only for Red Hat family"

- name: "Install apache httpd"
  yum:
    name: "{{ apache_rpm_name }}"
    state: latest

- name: "Deploy httpd.conf"
  template:
    src: etc_httpd_conf_httpd.conf.j2
    dest: /etc/httpd/conf/httpd.conf
    owner: root
    group: root
    mode: '0644'

Dans roles/webserver/template/etc_httpd_conf_httpd.conf.j2 nous avons repris le fichier httpd.conf standard d'Apache auquel nous avons variabilisé le port d'écoute.

(...)
# Change this to Listen on specific IP addresses as shown below to 
# prevent Apache from glomming onto all bound IP addresses.
#
#Listen 12.34.56.78:80
Listen {{ apache_listen_port }}

#
# Dynamic Shared Object (DSO) Support
(...)

Dans roles/webserver/defaults/main.yml nous spécifions le nom du package apache et le port par défaut.

---
apache_rpm_name: httpd
apache_listen_port: 80

De ce fait, en exécutant ce rôle sans surcharge, nous installerons le package httpd et écouterons le port 80 avec.

Si on veut surcharger le port pour un inventaire, par exemple la preprod nous pouvons mettre dans le fichier host_vars de son inventaire une surcharge de la variable.

---
apache_listen_port: 81

Ansible mettra lors le port 81 pour ce groupe de hosts.

Exécuter un rôle

Un rôle ne peut s'exécuter en tant que tel, il est nécessaire de l'invoquer via un playbook. Pour continuer dans notre exemple, nous créons un playbook nommé deploy_httpd.yml.

Dedans nous mettrons le code suivant, notez que l'instruction tasks est remplacée par roles :

---
- hosts: webservers
  roles:
    - webserver

On lance ce playbook avec l'inventaire cible, le rôle s'exécutera dessus en prenant comme contexte les éventuelles surcharges de variables dans hosts_vars ou group_vars.

$ ansible-playbook -i inventory/webservers/hosts deploy_httpd.yml

Cette méthode de lancement est la plus historique pour Ansible. Avec les versions 2.3 et 2.4, ils ont introduit la possibilité d'exécuter un rôle au travers d'une liste de tâches. Il s'agit des modules import_role et include_role.

---
- hosts: webservers
  tasks:
    - import_role:
        name: role1
    - include_role:
        name: role2

Vous remarquerez que cette syntaxe se fait au travers de l'instruction tasks.

import_role et include_role ont une légère nuance. En effet, leur façon d'exécuter le rôle sera respectivement statique (import) et dynamique (include). Une inclusion dynamique dans Ansible signifie que le code est lu durant l'exécution, là où Ansible lit la totalité du playbook avant de l'exécuter. Dans ce mode d'inclusion, les options des tâches ne s'appliqueront qu'à celles-ci et ne seront pas transmises à d'éventuelles sous-tâches. Une inclusion statique signifie qu'Ansible va lire tout le code et les tâches parentes transmettront les options et variables qu'elles produisent aux tâches filles. C'est comme l'instruction export dans un script bash pour une variable.

Un rôle ne sera exécuté qu'une seule fois. Si le nom du rôle est répété dans le playbook, les itérations suivantes seront ignorées à moins d'activer le paramètre allow_duplicates: true dans le fichier meta/main.yml. A noter cependant que le même rôle peut être rappelé plusieurs fois si celui-ci a des paramètres différents, avec l'instruction vars notamment.

Les dépendances de rôles

Le but d'un rôle est d'être réutilisable. Pour éviter qu'il soit trop spécialisé, ou bien si on doit installer des objets communs sur les environnements, il est possible d'utiliser les dépendances.

Cela se gère dans le fichier meta/main.yml avec l'instruction dependencies.

Exemple, nous avons un rôle "common" qui créé des utilisateurs système prédéfinis avec un UID spécifique qui doit être commun peu importe le rôle applicatif du serveur. Nous avons un rôle de déploiement Apache HTTPD et un rôle de déploiement Tomcat.

Dans le fichier meta/main.yml de ces deux rôles nous retrouverons alors l'instruction :

---
dependencies:
  - role: common

Leurs playbooks seront :

deploy_apache.yml

---
- hosts: webservers
  roles:
    - webserver

deploy_tomcat.yml

---
- hosts: appservers
  roles:
    - tomcat

A l'exécution, ces deux playbooks lanceront le rôle common car indiqué en dépendance des rôles webserver et tomcat.

Conclusion

Les rôles sont la partie la plus normée et réutilisable d'un code Ansible. Cet article a été un peu dense mais je me suis concentré sur l'essentiel pour vous présenter le fonctionnement d'un rôle et son concept. A partir de là, vous pourrez commencer à écrire vos propres rôles pour vous faire la main.

Le prochain article sur Ansible, et qui concluera cette série, vous proposera quelques liens utiles, et quelques conseils et bonnes pratiques.