Ansible – construire des rôles simples et réutilisables

Ansible – construire des rôles simples et réutilisables

Quand on travaille avec un outil de gestion de configuration comme Ansible, notre premier réflexe est de construire des rôles complexes qui font tout. C’est beau, ça remet à plat tout une infrastructure avec une seule ligne de commande, ça défile avec du vert et du orange…

Mais c’est très compliqué à maintenir, à faire évoluer, c’est lent et lourd… Il est aussi difficile de lancer une action ciblée sauf en copiant des morceaux de rôles dans un nouveau.

L’objectif de cet article est de vous montrer comment construire des rôles simples et efficaces, que l’on peut combiner et faire évoluer facilement. C’est en combinant plusieurs rôles par inclusion que l’on arrivera à réaliser des tâches complexes.

Pour les débutants, attachez vos ceintures.

Des rôles simples

KISS, DRY, TIMTOWTDI, YAGNI… Tous ces gros mots mettent en exergue une même volonté de simplicité et de clarté en ingénierie logicielle. En somme, d’aller à l’essentiel mais de le faire bien sans fioriture ni extravagance.

Appliqué à notre contexte, ça signifie créer des rôles d’une simplicité extrême qui se limitent à quelques tâches, et qui ont un objectif unique et précis.

Si votre rôle est une succession de when: ou de with_items: , ou qu’il contient 42 tâches, il est assurément trop complexe.

Un exemple parfait de rôle simple est la création d’un compte utilisateur. Présentes dans la plupart des rôles et playbooks un peu touffus, ces quelques tâches peuvent parfaitement être rationalisées dans un rôle à part entière qui peut être appelé au besoin comme une fonction.

--- 
# tasks file for roles/x_user_add

##
# création du groupe principal si aucun spécifié
- name: x - Creation du groupe principal pour "{{ x_user.uname }}"
  group: 
    name="{{ x_user.uname }}"
    gid={{ x_user.uid | default(omit) }}
  when: x_user.gid is undefined
  become: yes

##
# création de l'utilisateur
- name: x - Création de l'utilisateur "{{ x_user.uname }}"
  user: 
    name="{{ x_user.uname }}" 
    uid="{{ x_user.uid | default(omit) }}" 
    password="{{ x_user.hash | default("*") }}"
    createhome=yes
    state=present
    shell="{{ x_user.shell | default('/bin/bash') }}"
    group="{{ x_user.gid | default(x_user.uname) }}"
    groups="{{ x_user.groups | default(omit) }}"
  become: yes

Ce rôle très simple permet de créer un compte utilisateur avec des paramètres obligatoires et d’autres optionnels, ainsi qu’un groupe principal du même nom que l’utilisateur si aucun groupe n’est spécifié (pour reproduire le comportement de Debian).

Note:
« {{ var | default(omit) }} »  permet de se passer de condition et de simplifier énormément les rôles. C’est particulièrement pratique.

Vous pouvez constater que j’utilise une variable x_user qui n’est ici pas définie, celle-ci le sera une fois le rôle inclus avec un autre.

Des rôles combinables et imbricables

A l’image des légos, c’est en combinant et imbriquant des rôles que nous parviendrons à réaliser des ensembles complexes d’actions.

Si nous prenons l’image suivante comme exemple, nous aurons autant de rôles que de pastilles numérotées. Soit 7 rôles et/playbooks au total. Je ne couvrirai cependant que les points 1 à 4 dans cet article, le 4 étant juste au dessus.

Capture du 2016-04-28 23:37:25

Les rôles inférieurs sont appelés par ceux du niveau d’au dessus et ainsi de suite jusqu’à la racine. Cette notion d’imbrication et de combinaison est possible grâce à la fonction « include » d’Ansible.

Dans notre premier exemple, j’utilise une variable x_user qui n’est pas définie. C’est au moment de l’inclusion dans le rôle supérieur qu’elle est créée :

---
# tasks file for roles/x_web_add_site

[...]
## 
# appel x_user_add et création de l'utilisateur
- include: "{{ roles_path }}x_user_add/tasks/main.yml"
  vars:
    x_user: "{{ x_web }}"
[...]

vars: permet de transmettre des données au fichier inclus. En l’occurence, on lui donne une variable… qui elle même est définie au niveau au dessus …

Des rôles qui prennent des arguments

Pour entrer un peu plus dans le détail, prenons ce playbook :

---
- name: Playbook pour créer un hébergement
  hosts: web
  
  roles:
    - { role: x_web_add_site, selected_web: "ancel1" }

Le fichier de tâche du rôle x_web_add_site , équivalent au rôle n°3 « hébergement » de mon schéma plus haut, qui permet de créer et activer un hébergement :

---
# tasks file for roles/x_web_add_site

##
# set_fact pour propager la variable aux fichiers inclus
- set_fact:
    cur_web: "{{ selected_web }}"

##
# inclusion du fichier de variables, pour définir x_web en fonction de cur_web
- include_vars: "{{ roles_path }}x_web_add_site/vars/main.yml"

## 
# appel x_user_add et création de l'utilisateur
- include: "{{ roles_path }}x_user_add/main.yml"
  vars:
    x_user: "{{ x_web }}"

##
# création de la structure du site
- name: x_web - ajout des fichiers de base pour "{{ x_web.uname }}"
  template:
    src="{{ item }}"
    dest="/home/{{ x_web.uname }}/www/"
    owner="{{ x_web.uname }}"
    group="{{ x_web.gid }}"
    force=no
  with_fileglob: 
    - "{{ roles_path }}/x_web_add_site/templates/bundle/*"

##
# création du vhost dans /etc/apache2/site-available
- name: x_web - création du vhost "{{ x_web.uname }}"
  template:
    dest="/etc/apache2/sites-available/{{ x_web.uname }}"
    src=vhost.conf
    force=no
  become: yes

##
# Activation du vhost
- name: x_web - activation du vhost "{{ x_web.uname }}"
  command: a2ensite {{ x_web.uname }}
  args:
    creates: /etc/apache2/sites-enabled/{{ x_web.uname }}
  become: yes
  notify:
    - restart apache

Mon fichier de variable :

---
# vars file for roles/x_web_add_site

x_web: "{{ web_enabled[cur_web] }}"

Je ne peux pas vous donner l’inventaire complet puisqu’il est dynamique (voir mon article précédent sur les inventaires dynamiques avec PHP et MySQL), mais l’équivalent JSON serait du genre :

"web_enabled":{
	"ancel1":{
		"uid":"xxx",
		"gid":"yyy",
		"shell":"/bin/false",
		"uname":"ancel1",
		"ServerName":"ancel1.fr",
		"ServerAliases":"",
		"tld":"fr",
		"cms":"Wordpress",
		"php":"54",
		"vhost":"1",
		"hash":"zzz"
	},
	"exemple2":{
		"uid":"xxx",
		"gid":"yyy",
		"shell":"/bin/false",
		"uname":"exemple2",
		"ServerName":"exemple.fr",
		"ServerAliases":"*.exemple.fr exemple.com",
		"tld":"fr",
		"cms":"Wordpress",
		"php":"54",
		"vhost":"1",
		"hash":"zzz"
	}
...
}

Pour expliquer un peu le principe, j’appelle mon rôle x_web_add_site depuis mon playbook avec une variable selected_web= »ancel1″  . set_fact: me permet de définir la variable cur_web  à la même valeur que selected_web  (ici « ancel1 »).

Mon fichier de variables définit une variable x_web  qui prend comme valeur une portion de mon tableau web_enabled  (qui lui contient tous mes vhosts) avec uniquement les données qui correspondent à selected_web .

x_user  prendra la valeur de x_web  au moment ou le rôle x_user_add  sera inclus, propageant ainsi les données nécessaires pour créer l’utilisateur.

Note : Les variables ont un scope limité au rôle et ne sont par conséquent pas transmises au niveau d’en dessous. Il est nécessaire d’utiliser vars: lors de l’include pour pouvoir les propager.

Je peux donc choisir quel vhost déployer avec selected_web  défini dans le playbook. Mais je peux aussi surcharger selected_web  avec les extra_vars ! : ansible-playbook -i inventory.php playbooks/hebergement.yml –check –diff -e selected_web= »exemple2″

Je n’ai pas besoin d’éditer un quelconque fichier pour déployer un hébergement précis ! Cette technique ouvre plein d’autres possibilités… Mais c’est un autre sujet…

Appeler mon rôle de cette façon limite le nombre d’hébergements créés à.. : un. Il faudrait donc créer autant de lignes dans mon playbook qu’il y a de vhosts à créer. Où alors inclure mon rôle dans un autre rôle avec une boucle.

Des rôles « inception »

Mon rôle x_web_add_site  est un rôle unitaire et particulièrement simple. Il correspond aux critères de mon premier chapitre.

Mais quand le serveur est tout neuf, il faut pouvoir tous les créer d’un seul coup. Nous devrons créer un nouveau rôle x_web_provision , équivalent du rôle n°2 dans mon schéma, qui boucle l’include du rôle x_web_add_site  :

# tasks file for roles/x_web_provision

- include: "{{ roles_path }}x_web_add_site/tasks/main.yml"
  vars:
    selected_web: "{{ item }}"
  with_items: "{{ web_enabled }}"
[...]

Reste plus qu’à modifier notre playbook pour appeler le bon rôle :

---
- name: Playbook pour créer TOUS les hébergements
  hosts: web
  
  roles:
    - { role: x_web_provision }

C’est là que ça devient trèeeees compliqué par rapport au reste ;).

Note : Dans la continuité de ma précédente note par rapport à la propagation des variables, un bug déjà connu des développeurs ne permet pas de transmettre la variable item d’un niveau Y à un niveau Z, si Y est lui même dans une boucle dans X. La variable item de X prend le dessus sur celle de Y.

Pour avoir notre playbook n°1 de mon schéma de départ, nous pouvons y ajouter un premier rôle pour l’installation des logiciels.

Conclusion

Cette méthode demande un peu de réflexion et sans doute plus de travail, mais à mon sens, avec mon maigre recul et ma petite infrastructure, elle offre beaucoup plus de souplesse et de possibilités ! J’imagine très bien pouvoir coupler l’interface web de mon inventaire dynamique à un message broker, ce qui me permettrait de lancer automatiquement Ansible pour créer ou modifier UN vhost et un seul.

Je serais curieux de savoir comment vous utilisez Ansible et créez vos rôles. N’hésitez pas à laisser un commentaire.

4 réflexions sur « Ansible – construire des rôles simples et réutilisables »

  1. Bonjour, et merci pour le lien Cascador.

    J’ai déjà consulté quelques uns de ces rôles, et m’en inspire parfois.
    Pour ma part je préfère savoir faire avant d’utiliser ce que les autres font. D’autant plus que j’ai parfois des besoins bien spécifiques.

  2. Merci pour l’article !

    Je te conseille cependant d’utiliser un fichier default/main.yml dans ton rôle pour lister toutes tes variables d’un coup et éviter de faire des defaults() dans tes tasks qui salissent un peu le code. J’aime bien voir d’un seul coup d’œil le fichier defaults/main.yml dans les rôles ça me sert de doc rapide sur ce que je peux variabiliser hors du rôle.

  3. Merci pour ton commentaire.
    Je vais regarder ça, mais je suis pas sûr de pouvoir le mettre en place.

    J’utilise default essentiellement pour deux cas : omit et valeur pré-définie.
    A ma connaissance, la seule autre façon d’obtenir le omit, c’est avec des conditions. Ce qui serait encore plus sale.
    Pour les valeurs pré-définies, je regarderai s’il est possible de fusionner proprement des lists et dicts.

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.