Ansible 
Ansible est un outil permettant l'automatisation de tâches se concentrant principalement sur des tâches à effectuer sur des parcs de machines distantes. Son utilisation a peu de pré-requis pour fonctionner :
- Un accès ssh aux machines distantes
- Python d'installé sur ces mêmes machines
Cependant, Ansible permet beaucoup de choses, entre autre:
- Installer et configurer des logiciels
- Faire de la maintenance système (mises à jour, ajout d'utilisateurs, création de cron jobs, etc.)
- Manipuler et interagir avec des conteneurs
Pour cela, Ansible utilise des playbooks (l'équivalent de scripts en bash par exemple), des modules (l'équivalent de commandes en bash), et un inventaire. Ce dernier est propre à Ansible et permet de facilement définir des groupes de machines ainsi que les variables associées.
Installation
Les versions des dépôts des distributions linux courantes (Ubuntu, centos) ne sont souvent pas à jour, aussi il est conseillé de suivre la documentation officielle Ansible pour installer la version la plus récente sur votre système.
Vérification
Pour vérifier que l'installation s'est bien passée, exécuter la commande suivante :
ansible localhost -m ping
Si l'installation s'est bien passée, le résultat devrait être :
[WARNING]: No inventory was parsed, only implicit localhost is available localhost | SUCCESS => { "changed": false, "ping": "pong" }
Inventaire
L'inventaire Ansible permet de définir deux choses :
- Des groupes de machines distantes
- Des variables
Groupes
Les groupes de machines sont définies via un fichier au format TOML où chaque groupe est défini par son nom, et où chaque machine est définie soit par son adresse IP, soit par un nom de domaine. Par exemple :
[groupe_1] 10.0.2.2 10.0.2.3 localhost [groupe_2] 188.23.2.7 serveur1.bobbyblues.com
Ce fichier d'inventaire décrit deux groupes (groupe_1 et groupe_2), qui contiennent pour le premier quatre machines, et pour le second, deux machines.
Variables
Les tâches Ansible étant paramètrables, il est nécessaire de pouvoir définir la valeurs de variables pour chacune des machines distantes. Ces variables peuvent-être définies à plusieurs endroits de l'inventaire.
Fichier inventaire
On peut déclarer des variables directement dans le fichier d'inventaire, sur la même ligne que chaque machine, au format variable=valeur, chaque variable étant séparée d'une espace.
Par exemple:
[groupe_1] 10.0.2.2 variable1=valeur1 10.0.2.3 variable1=valeur2 localhost variable1=valeur3 [groupe_2] 188.23.2.7 variable2=valeur4 serveur1.bobbyblues.com variable2=valeur5 variable3=valeur6
Variables de groupe
Des variables peuvent être déclarées pour tout un groupe en les définissant dans un fichier yaml dont le chemin relatif par rapport au fichier d'inventaire est : group_vars/group_name.yml.
Par exemple, pour définir pour la variable variable2 une valeur commune à 188.23.2.7 et serveur1.bobbyblues.com, le fichier group_vars/group_2.yml serait :
# group_vars/group_2.yml variable2: valeur_commune
Variables spécifiques à chaque machine
Enfin, pour éviter de surcharger le fichier d'inventaire, on peut aussi définir des variables pour chaque machine distante dont le chemin relatif par rapport au fichier d'inventaire est : host_vars/host_name.yml.
Par exemple, pour définir la variables variable1 pour la machine 10.0.2.2, le fichier host_vars/10.0.2.2.yml serait :
# host_vars/10.0.2.2.yml variable1: valeur
Priorité des variables
Une même variable peut être définie à plusieurs endroits. Ansible va chercher la variable dans l'ordre suivant, n'allant à l'étape suivante que si la variable n'est pas définie dans l'étape courante :
- fichier machine dans
host_vars - fichier inventaire
- fichier groupe dans
group_vars
Utilisateur SSH
Certaines des variables servent directement à Ansible, notamment pour définir quel utilisateur Ansible doit utiliser pour se connecter aux machines distantes.
Par défaut, l'utilisateur utilisé est celui lançant le playbook, mais il est possible de redéfinir l'utilisateur via la variable ansible_user.
De la même manière, on peut changer le port utilisé pour la connection ssh via la variable ansible_port, et on peut se connecter en utilisant un mot de passe plutôt qu'une clef ssh via la variable ansible_ssh_pass.
Ansible Vault
Très vite, il sera nécessaire d'avoir des variables contenant des données sensibles (mots de passe, clefs de chiffrement, etc.).
Ansible propose une solution appelée Ansible Vault pour chiffrer ces variables tout en gardant des fichiers group_vars et host_vars lisible.
Pour chiffrer une chaîne de charactères sensible, par exemple le mot de passe mon_super_mot_de_passe, lancez la commande :
ansible-vault encrypt-string "my_super_password"
Ansible vault va vous demander le mot de passe vault (ce mot de passe doit être le même pour toutes les variables d'un même inventaire), puis vas afficher une clef chiffrée :
!vault | $ANSIBLE_VAULT;1.1;AES256 30363739386532643532653038646439376463303633323061336234643662313666396139343233 6238346532323535323462643330666262383934633730390a333031313561663535653262303434 32663632383039653333663838306638313833383164323733353536643133326231356431633363 3539343034653836320a323363306136303131643838623233623635633562646235663763363365 38376164383930346230353837373031333835366534323230336235623065616639ç
Cette clef peut alors être utilisée telle quelle au sein d'un des fichiers de l'inventaire :
... mot_de_passe: !vault | $ANSIBLE_VAULT;1.1;AES256 30363739386532643532653038646439376463303633323061336234643662313666396139343233 6238346532323535323462643330666262383934633730390a333031313561663535653262303434 32663632383039653333663838306638313833383164323733353536643133326231356431633363 3539343034653836320a323363306136303131643838623233623635633562646235663763363365 38376164383930346230353837373031333835366534323230336235623065616639 ...
Lors de l'execution du playbook, Ansible demandera le mot de passe vault, et déchiffrera les données sensibles à la volée.
Playbook
Une fois un inventaire défini, on peut écrire un playbook. Celui-ci est un fichier au format yaml, composé d'une liste de plusieurs éléments. Chaque élément possède (au minimum) les trois clefs suivantes :
name: une chaîne de charactère décrivant le but général du playbook. Bien que cette variable ne soit pas strictement nécessaire, elle est conseillée.hosts: le nom d'un groupe de machines dans l'inventaire, c'est sur ces machines que sera joué cet élément du playbook.tasks: une liste de tâches ansible.
Les tâches sont expliquées à la sous-section suivante, cependant voici un exemple de playbook simple copiant un fichier depuis la machine executant le playbook vers la ou les machines distantes d'un group lab:
- name: "Exemple de copie de fichier" hosts: lab tasks: - name: "Copie le fichier" copy: src: "/tmp/config.json" dest: "/mon-service/config.json"
Tâches
La plupart des tâches sont constituées d'une description qui, comme pour les playbooks, est définie via le mot-clef name, et d'un module Ansible.
La plupart des modules vont aussi demander la définition de certains paramètres.
Si l'on prend l'exemple du module copy, il faut définir au moins le paramètre dest qui est le chemin vers le fichier destination sur les machines distantes.
On peut choisir de lui fournir un fichier source local via le paramètre src.
Ainsi, une tâche dont le but serait de téléverser un fichier local /tmp/config.json vers un un dossier /mon-service/ sur mes machines distante pourrait être définie ainsi:
name: "Envoie du fichier de configuration" copy: src: "/tmp/config.json" dest: "/mon-service/config.json"
Ici, le mot-clef copy fait directement réference au module copy d'Ansible, d'autres modules utilisant d'autres mots-clefs.
La liste des modules Ansible est disponible sur la documentation officielle.
Notons qu'il est possible de définir (ou redéfinir) localement des variables via le mot-clef vars:
name: "Envoie du fichier de configuration" copy: src: "/tmp/config.json" dest: "/mon-service/config.json" vars: variable1: valeur1 variable2: valeur2
Cette définition est prioritaire par rapport à celles faites dans les différents fichiers d'inventaire. Il est de plus possible d'utiliser les variables au sein même des tâches en utilisant le format de "templates" de Jinja 2, c'est-à-dire en mettant un nom de variable entre doubles accolades. Par exemple, la tâche suivante est équivalente à celle définie plus haut :
name: "Envoie du fichier de configuration" copy: src: "{{ dossier }}/{{ fichier }}" dest: "/mon-service/config.json" vars: fichier: config.json dossier: /tmp
Quelques modules utiles Ansible utiles:
- file : Permet de manipuler fichiers et dossiers sur les machines distantes.
- copy : Permet la copie de fichiers de la machine locale vers les machines distantes
- template : Un des modules les plus puissants d'Ansible, il permet de remplacer les variables (toujours au format Jinja 2) au sein même d'un fichier par leurs valeurs avant d'envoyer ce fichier vers les machines distantes
- shell : Permet d'executer une commande ou un script shell
- docker container : Permet la manipulation de conteneurs dockers, le "docker compose" d'Ansible
Roles
Pour mutualiser des tâches qui seraient dupliquées ou similaires entre plusieurs playbooks, Ansible propose la création de rôle. Ceux-ci sont simplement des listes de tâches qui peuvent être paramétrées, et appelées autant de fois que nécessaire.
Appeler un rôle
Pour appeler un rôle depuis un playbook (ou depuis un autre rôle), on utilise le module include_role.
Celui-ci ne prend pour paramètre que le nom du rôle à appeler.
Par exemple pour appeler un rôle nommé role_test, je peux utiliser la tâche suivante dans mon playbook pour l'appeler :
name: "Appel de mon role de test" include_role: name: role_test
Comme pour n'importe quelle tâche, je peux fournir des variables à mon rôle grâce au mot-clef vars.
Structure d'un rôle
Chaque rôle doit être stocké dans un dossier portant le nom du rôle, ce dernier se trouvant dans un dossier roles à la racine du projet Ansible.
Par exemple, un rôle nommé role_test sera défini dans le dossier roles/role_test.
Le minimum à avoir dans ce dossier est un sous-dossier appelé tasks qui contient lui-même un fichier main.yml.
Ce fichier est le point d'entrée quand on appelle le rôle depuis l'extérieur, et contient simplement une liste de tâche.
Par exemple, si le but de role_test est de créer deux fichiers sur les machines distantes, le contenu de main.yml pourrait être :
- name: "Creation du premier fichier" file: dest: "/tmp/fichier1" - name: "Creation du second fichier" file: dest: "/tmp/fichier2"
Pour les rôles les plus complexes, il est possible de définir autant de fichiers yaml que désiré dans le dossier tasks, puis d'appeler n'importe lequel de ces fichiers (depuis main.yml ou ailleurs) via le mot-clef import_tasks.
Par exemple, si role_test se décompose en deux sections, une partie installation puis une partie installation, le contenu de main.yml pourrait devenir :
- name: "Installation" import_tasks: installation.yml - name: "Configuration" import_tasks: configuration.yml
Auquel cas le dossier roles/role_test/tasks devrait contenir au moins les fichiers suivants :
main.ymlinstallation.ymlconfiguration.yml
Les fichiers installation.yml et configuration.yml pourraient eux-aussi appeler d'autres fichiers, auquel cas ils devraient être définis eux aussi dans le dossier tasks.
Pour plus d'information sur les rôles et les possibilités offertes, se réferer à la documentation officielle.
Fichier requirements.yml
Le but des rôles étant d'être réutilisables, il est probable qu'ils se retrouvent à être partagés entre plusieurs projets Ansible.
De plus, les rôles sont souvent ammenés à évoluer et s'étoffer ou se débuguer au fil du temps.
Il est donc conseillé de les stocker sous forme de dépots git.
Une bonne pratique est de créer un dépôt par rôle, et d'avoir les différents dossiers qui composent le rôle (au minimum le dossier tasks directement à la racine du dépôt).
De plus il est conseillé d'utiliser les tags git pour gérer les différentes versions des rôles.
En suivant ces recommandations, il est alors possible de joindre un fichier requirements.yml aux différents projets ansible dont le but est de décrire quel rôle, et quelle version, sont nécessaire à un ensemble de playbooks.
Supposons le cas suivant:
- Un rôle
role_1existe sous forme de dépot à l'adresse suivantegit.domain.tld/role_1.git - Un rôle
role_2existe sous forme de dépot à l'adresse suivantegit.domain.tld/role_2.git - Un projet Ansible nécessite
role_1en version0.1.0etrole_2en version2.4.0
Le fichier requirements.yml peut être ajouté au projet Ansible, et contiendra :
- name: role_1 src: git.domain.tld/role_1.git version: 0.1.0 scm: git - name: role_2 src: git.domain.tld/role_2.git version: 2.4.0 scm: git
Il est ensuite facile d'installer les rôles à leur bonne versions en executant à la racine du projet Ansible:
ansible-galaxy install -r requirements.yml
Ansible va alors automatiquement récupérer le commit du dépôt de chaque rôle correspondant au tag donné via le mot-clef version.
Condition
Le but des rôles étant d'être modulaire, il est possible de conditionner l'execution de certaines tâches.
Cela se fait avec le mot-clef when.
L'utilisation la plus courante est avec des booléens :
Imaginons le role suivant:
# main.yml - name: "Creation du dossier de configuration" file: dest: /config state: directory when: install | default('false') | bool - name: "Suppression du dossier de configuration" file: dest: /config state: absent when: uninstall | default('false') | bool
Ici, chaque tâche ne sera appelée que si une variable existe (soit install soit uninstall).
Suivant les bonnes pratiques d'Ansible, il est précisé que ces deux variables ont pour valeur par défaut false, et sont des booléens.
Un playbook d'installation pourra ensuite appeler ce rôle ainsi :
# installation.playbook.yml - name: "Installation de mon logiciel" hosts: lab tasks: - name: "Role d'installation" include_role: name: mon_logiciel vars: install: yes
Un playbook pour désinstaller ce même logiciel sera :
# desinstallation.playbook.yml - name: "Desinstallation de mon logiciel" hosts: lab tasks: - name: "Role d'installation" include_role: name: mon_logiciel vars: uninstall: yes
Il est de plus possible de combiner des variables avec les opérateurs or et and ainsi que d'utiliser des parenthèses.
Par exemple :
.... when: var1 | default('false') | bool or (var2 | default('false') | bool and var3| default('true') | bool)
Pour plus d'information sur les conditions Ansible, voir la documentation officielle.
Boucles
Il peut arriver de vouloir éxecuter une même tâche ansible plusieurs fois de suite avec des paramètres différents.
Par exemple la création de multiple dossiers.
Cela est faisable au sein d'une tâche avec le mot-clef loop :
- name: "Creation de dossiers" file: dest: {{ item }} state: directory loop: - "/chemin/vers/dossier1" - "/chemin/vers/dossier2" - "/chemin/vers/dossier3"
Ici, la liste d'élements déclarée dans loop sera automatiquement affectée à la variable item, et le module file sera appelé trois fois.
Notons qu'il est possible de définier des dictionaires pour chacun des éléments de la liste, on utilisera alors la syntaxe item.clef pour accéder à la valeur du dictionaire associée au mot-clef clef.
Par exemple:
- name: "Creation de dossiers et de fichiers file: dest: {{ item.chemin }} state: {{ item.status }} loop: - { chemin: "/chemin/vers/dossier1", status: directory } - { chemin: "/chemin/vers/dossier2", status: directory } - { chemin: "/chemin/vers/fichier1", status: present }
Ici, le module file est appelé parfois pour créer des dossiers, parfois pour créer des fichiers.
Beaucoup de boucles sont réalisables via Ansible, pour plus d'information, se référer à la documentation officielle.