Ansible
Dans le cadre de la mise en place d’une formation Docker, j’ai récemment dû installer une dizaine de serveurs identiques. C’est l’occasion idéale pour rédiger un petit article sur Ansible…
Photo by Clément Hélardot on Unsplash
Ansible fait parti de la famille des outils de gestion de configuration à l’instar de Puppet, SaltStach et Chef. Il s’inscrit dans la mouvance IaC (Infrastructure As Code) qui permet de gérer son infrastructure à partir de fichiers descripteurs facilement versionnables dans un Git.
Par rapport à ces concurrents, Ansible se distingue par:
- un fonctionnement “agentless” qui repose sur SSH
- une faible courbe d’apprentissage
- un fonctionnement en mode push
- stateless (ansible ne stocke pas l’état des serveurs)
- indempotence (résultat identique à chaque exécution)
Un petit tour du coté de Google Trends permet de mesurer l’évolution de l’intérêt pour les différents challengers depuis 2015:
Le fonctionnement
Ansible utilise un simple fichier d’inventaire au format INI
pour se connecter en SSH sur les machines à configurer . La liste des tâches à réaliser est décrite dans un fichier YAML appelé playbook
. Ansible utilise des petits programmes appelés modules
ou plugins
pour exécuter les tâches sur les machines.
Avec Ansible, vous disposez de plus de 4000 modules qui permettent de pratiquement tout gérer.
Pour cette entrée en matière, nous allons nous concentrer sur ces composants de bases :
- Inventaire: fichier au format INI qui contient la liste des machines à gerer (l’utilisation d’une database est possible)
- Node Mannager ( ou control node): la machine qui exécute les playbooks
- Playbook: un fichier au format YAML qui contient une liste de tâches ou de rôles à exécuter sur les machines de l’inventaire
- Rôle: un ensemble de tâches factorisables
- Module (ou plugins): bout de code généralement en python qui peut être utilisé directement en CLI ou dans une tâche de playbook
Le contexte
Pour cette mise en oeuvre, j’ai déployé 10 VMs sous Debian 10 avec un utilisateur ansible
membre du groupe sudo
configuré en mode nopasswd
( %sudo ALL=(ALL:ALL) NOPASSWD:ALL
). Cet utilisateur me permet de me connecter aux serveurs avec ma clé publique SSH ( en clair ma clé publique SSH est présente dans le fichier /home/ansible/.ssh/authorized_keys
des serveurs ).
L’installation
Ansible est un outil écrit en python et la façon la plus de l’installer est d’utiliser PIP. Vous pouvez également utiliser votre gestionnaire de paquets habituel (voir la documentation en fonction de votre OS).
Ansible s’installe uniquement sur le node manager
qui peut être un serveur dédié ou simplement votre laptop.
Une fois installé, je vérifie que tout fonctionne correctement:
1$ ansible --version
2ansible 2.10.8
3
4$ ansible all -i "localhost," -c local -m ping
5localhost | SUCCESS => {
6 "ansible_facts": {
7 "discovered_interpreter_python": "/usr/bin/python"
8 },
9 "changed": false,
10 "ping": "pong"
11}
La 2nd commande permet d’utiliser Ansible en mode ac-hoc
avec le module ping
en local ( -c local ).
Creation de l’arborescence
Pour ce projet, je vais créer une arborescence standard avec un répertoire Ansible
, des sous-dossiers host_vars
et group_vars
ainsi qu’un fichier d’inventaire hosts
. Pour plus de détails sur la structure des dossiers et les différentes alternatives, vous pouvez consulter cette documentation.
1$ mkdir Ansible && cd $_
2$ mkdir host_vars group_vars roles
3$ touch hosts
L’inventaire
L’inventaire contient la liste des machines gérées par Ansible. Pour notre projet, j’utilise un simple fichier statique mais il est possible d’interroger une base de données. Pour plus de détails sur les différentes options d’utilisations de l’inventaire , vous pouvez consulter la documentation.
Voici un exemple avec une classification par type mais on peut aussi envisager un découpage géographique, par système d’exploitation…
1[webservers]
2foo.example.tld
3bar.example.tld
4
5[dbservers]
6one.example.tld
7two.example.tld
8three.example.tld
Pour notre cas, je vais renseigner le fichier hosts
avec une section docker
qui correspond à un groupe de serveurs ( docker-00 à docker-09 ):
1$ cat hosts
2[docker]
3docker-[00:09].exemple.tld
Ansible en mode ad-hoc
Pour tester notre inventaire, j’utilise Ansible en mode interactif (ad-hoc):
1$ ansible docker -i hosts -u <ssh_username> -m shell -a 'cat /etc/issue'
2 \____/ \______/ \_______________/ \______/ \_/ \______________/
3 1 2 3 4 5 6
docker
: détermine la portée dans le fichier d’inventaire ( ici le groupedocker
), on peut aussi utiliserall
-i hosts
: spécifie le path du fichier d’inventaire ( ici dans le répertoire courant )-u <ssh_username>
: spécifie l’utilisateur autorisé à se connecter en ssh avec une clé publique (root par défaut)-m shell
: indique l’utilisation du moduleshell
-a
: indique les arguments à passer au modulecat /etc/issue
: spécifie la commande à exécuter sur la machine distante
1$ ansible docker -i hosts -u <ssh_username> -m shell -a 'cat /etc/issue'
2
3docker-04.exemple.tld | CHANGED | rc=0 >>
4Debian GNU/Linux 10 \n \l
5
6docker-03.exemple.tld | CHANGED | rc=0 >>
7Debian GNU/Linux 10 \n \l
8
9docker-00.exemple.tld | CHANGED | rc=0 >>
10Debian GNU/Linux 10 \n \l
11
12docker-02.exemple.tld | CHANGED | rc=0 >>
13Debian GNU/Linux 10 \n \l
14
15docker-01.exemple.tld | CHANGED | rc=0 >>
16Debian GNU/Linux 10 \n \l
17
18docker-05.exemple.tld | CHANGED | rc=0 >>
19Debian GNU/Linux 10 \n \l
20
21docker-09.exemple.tld | CHANGED | rc=0 >>
22Debian GNU/Linux 10 \n \l
23
24docker-08.exemple.tld | CHANGED | rc=0 >>
25Debian GNU/Linux 10 \n \l
26
27docker-06.exemple.tld | CHANGED | rc=0 >>
28Debian GNU/Linux 10 \n \l
29
30docker-07.exemple.tld | CHANGED | rc=0 >>
31Debian GNU/Linux 10 \n \l
On constate qu’Ansible exécute la commande cat /etc/issue
sur les serveurs contenus dans la section docker
en parallèle (5 par défauts mais configurable avec l’option -f 10
pour 10 threads parallèles )
On peut lancer des commandes nécessitant une élévation de privilèges via sudo avec l’option -b
( –become ). Dans cet exemple volontairement tronqué, j’utilise le module package
pour installer l’outil htop
:
1$ ansible docker -i hosts -u <ssh-username> -m package -a 'name=htop state=latest' -b
2docker-04.exemple.tld | CHANGED => {
3 "ansible_facts": {
4 "discovered_interpreter_python": "/usr/bin/python"
5 },
6 "cache_update_time": 1617896165,
7 "cache_updated": false,
8 "changed": true,
9 "stderr": "",
10 "stderr_lines": [],
11 "stdout": "Reading package lists...\nBuilding dependency tree...
12 "stdout_lines": [
13 "Reading package lists...",
14 "Building dependency tree...",
15 "Reading state information...",
16 "Suggested packages:",
17 " strace",
18 "The following NEW packages will be installed:",
19 " htop",
20 "0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.",
21 "Need to get 92.8 kB of archives.",
22 "After this operation, 230 kB of additional disk space will be used.",
23 "Get:1 http://deb.debian.org/debian buster/main amd64 htop amd64 2.2.0-1+b1 [92.8 kB]",
24 "Fetched 92.8 kB in 0s (1950 kB/s)",
25 "Selecting previously unselected package htop.",
26 "(Reading database ... ",
27 "(Reading database ... 5%",
28 "(Reading database ... 50%",
29 "(Reading database ... 100%",
30 "(Reading database ... 37073 files and directories currently installed.)",
31 "Preparing to unpack .../htop_2.2.0-1+b1_amd64.deb ...",
32 "Unpacking htop (2.2.0-1+b1) ...",
33 "Setting up htop (2.2.0-1+b1) ...",
34 "Processing triggers for mime-support (3.62) ...",
35 "Processing triggers for man-db (2.8.5-2) ..."
36 ]
37}
Si comme moi vous réutilisez souvent le même pool d’adresse IP pour vos labs, vous pouvez désactiver temporairement la vérification des clés SSH avec la commande
export ANSIBLE_HOST_KEY_CHECKING=False
Les Facts
Ansible utilise les “Facts” pour collecter des informations sur les serveurs ( managed nodes
). Ce mecanisme permet de peupler des variables ansible_xx
utilisables dans les playbooks et des templates JinJa2.
Pour consulter les listes des variables disponibles, vous pouvez utiliser le module setup
:
1$ ansible docker-01.exemple.tld -i hosts -u <ssh_username> -m setup -a "filter=ansible_distribution*"
2docker-01.exemple.tld | SUCCESS => {
3 "ansible_facts": {
4 "ansible_distribution": "Debian",
5 "ansible_distribution_file_parsed": true,
6 "ansible_distribution_file_path": "/etc/os-release",
7 "ansible_distribution_file_variety": "Debian",
8 "ansible_distribution_major_version": "10",
9 "ansible_distribution_release": "buster",
10 "ansible_distribution_version": "10",
11 "discovered_interpreter_python": "/usr/bin/python"
12 },
13 "changed": false
14}
ici je limite le scope au serveur docker-01
et je filtre la sortie du module setup
pour conserver uniquement les infos concernants la distribution. Vous pouvez essayer sans le filtre pour voir l’étendue des informations collectées.
Les rôles
Les rôles permettent de créer un ensemble de tâches facilement réutilisables. Ils correspondent à un ensemble de dossiers et fichiers dont la structure est normalisée. Vous pouvez directement importer un rôle mis à disposition par la communauté sur Ansible Galaxy avec la commande:
1$ ansible-galaxy install --roles-path <install_path> role_name
Structure d’un rôle:
1├── README.md # description du rôle
2├── defaults # variables par défaut du rôle
3│ └── main.yml
4├── files # contient des fichiers à déployer
5├── handlers # actions déclenchées par une notification
6│ └── main.yml
7├── meta # metadonnées et notamment les dépendances
8│ └── main.yml
9├── tasks # contient la liste des tâches à exécuter
10│ └── main.yml
11├── templates # contient des templates au format Jinja2
12| └── template.j2
13├── tests
14│ ├── inventory
15│ └── test.yml
16└── vars # autres variables pour le rôle
17 └── main.yml
Pour que le rôle soit facilement factorisable, il est important de bien réfléchir au découpage des tâches. Par exemple, il ne serait pas pertinent d’avoir un rôle unique pour la création de comptes locaux, l’installation d’un serveur apache et d’une base de données mariadb. Il faut mieux prévoir un rôle dédié pour chaque opération pour pouvoir l’utiliser individuellement.
Pour ce besoin, je vais créer mon propre rôle avec la commande ansible-galaxy
qui va générer la structure des répertoires.
1$ ansible-galaxy role init --init-path ./roles deb10_docker
2- Role deb10_docker was created successfully
3
4$ tree ./roles/
5./roles/
6└── deb10-docker
7 ├── README.md
8 ├── defaults
9 │ └── main.yml
10 ├── files
11 │ └── docker-cleanup
12 ├── handlers
13 │ └── main.yml
14 ├── meta
15 │ └── main.yml
16 ├── tasks
17 │ └── main.yml
18 ├── templates
19 ├── tests
20 │ ├── inventory
21 │ └── test.yml
22 └── vars
23 └── main.yml
Toujours dans le répertoire Ansible, la commande va générer la structure du rôle deb10_docker
dans le dossier roles
On va maintenant pouvoir entrer dans le vif du sujet en listant les tâches à automatiser:
- installer les pré-requis
- ajouter le dépot Docker et la clé GPG
- installer docker-ce
- s’assurer que le service docker est activé et démarré
- installer docker-compose et ctop
- s’assurer que le groupe docker existe
- créer un utilisateur
formation
, l’affecter aux groupessudo
etdocker
et définir un password - ajouter un petit script pour purger les conteneurs, images et volumes Docker ( pratique en formation !)
L’écriture des tâches peut paraître complexe au premier abord mais fondamentalement cela se résume à:
- donner une description de la tâche (affichée pendant l’exécution, ça facilite le debug)
- identifier le module appropié pour réaliser la tâche (vous pouvez utiliser Ansible Galaxy pour trouver des exemples)
- utiliser la documentation du module qui propose souvent de nombreux exemples
1- name : description de la tâche
2 module_name: # nom du module à utiliser
3 paramètre_1: valeur
4 paramètre_2: valeur
Pour cela, je vais éditer le fichier ./roles/deb10_docker/tasks/main.yml
qui doit contenir la liste des tâches à accomplir.
1---
2# tasks file for deb10_docker
3
4# Etape 1
5- name: install requirements
6 ansible.builtin.package:
7 name: ['apt-transport-https', 'ca-certificates', 'curl', 'gnupg', 'lsb-release']
8 update_cache: yes
9
10# Etape 2
11- name: add Docker GPG key
12 ansible.builtin.apt_key: url=https://download.docker.com/linux/debian/gpg
13
14- name: Add Docker Repository
15 ansible.builtin.apt_repository:
16 repo: deb [arch=amd64] https://download.docker.com/linux/debian buster stable
17 state: present
18 filename: 'docker'
19
20# Etape 3
21- name: install Docker CE
22 ansible.builtin.package:
23 name: ['docker-ce', 'docker-ce-cli', 'containerd.io']
24 update_cache: yes
25
26# Etape 4
27- name: start and enable Docker service
28 ansible.builtin.service:
29 name: docker
30 state: started
31 enabled: yes
32
33# Etape 5
34- name: install Docker-compose
35 ansible.builtin.get_url:
36 url: "https://github.com/docker/compose/releases/download/1.29.0/docker-compose-{{ansible_system}}-{{ansible_architecture}}"
37 dest: /usr/local/bin/docker-compose
38 mode: +x
39
40- name: install ctop docker monitoring tool
41 ansible.builtin.get_url:
42 url: "https://github.com/bcicen/ctop/releases/download/v0.7.5/ctop-0.7.5-linux-amd64"
43 dest: /usr/local/bin/ctop
44 mode: +x
45
46# Etape 6
47- name: ensure group docker exists
48 ansible.builtin.group:
49 name: docker
50 state: present
51
52# Etape 7
53- name: create user "formation" and add it to docker and sudo groups
54 ansible.builtin.user:
55 name: formation
56 comment: compte formation
57 groups: docker,sudo
58 password: $6$.qVo0HYyg$08EJX88IrAl.2uXs4pzP6GGR068qqfNhMI8Tg1TpDwKNyOmkyvfZiOSc12wk9YToEs3byaS2rP9D5fUWun8Rc0
59 update_password: always
60# password hash generated by: mkpasswd --method=sha-512
61
62# Etape 8
63- name: add docker-cleanup script
64 ansible.builtin.copy:
65 src: docker-cleanup.sh
66 dest: /usr/local/bin
67 owner: root
68 group: root
69 mode: +x
Je vous renvoie à la documentation de chaque module pour bien comprendre son fonctionnement.
Quelques précisions néanmoins:
-
Etape 1: le module
package
permet d’invoquer le gestionnaire de paquet de la machine cible ( apt, yum, dnf…) -
Etape 5: j’utilise les variables
ansible_system
etansible_architecture
pour construire l’URL. Elles sont valuées par ansible lors de la connexion à chaque machine ( mécanisme deFacts
) -
Etape 8: par défaut Ansible va chercher le fichier à copier dans le répertoire
files
du rôle
Les Playbooks
Un playbooks est un fichier au format YAML
qui peut contenir un ou plusieurs play
. Chaque play permet de définir sur quelle portion de l’inventaire il s’exécute et la liste des tâches ou des rôles à accomplir.
Exemple:
1---
2- hosts: webservers # 1er play sur le groupe "webservers" de l'inventaire
3 roles: # utilisation du role "common"
4 - common
5 tasks: # ajout d'une tâche
6 - service: # nom du module
7 name: apache2 # options du module
8 state: started
9
10- hosts: dbservers # 2nd play sur le groupe "dbservers" de l'inventaire
11 roles: # utilisation des rôles "mariadb et "common"
12 - mariadb
13 - common
Dans notre cas, il se résume à :
- décrire l’usage
- la portée sur l’inventaire (les serveurs du groupe
docker
) - indiquer l’utilisation de sudo (
become: yes
) pour l’installation des paquets - l’utilisation du rôle
deb10-docker
1cat docker-training.yml
2
3---
4- name: configure docker training platform
5 hosts: docker
6 become: yes
7 roles:
8 - deb10-docker
Reste à lancer notre playbook pour construire la plate-forme avec la commande ansible-playbook
1$ ansible-playbook -i hosts docker-training.yml -u <ssh_username>
2 \_______/ \_________________/ \_______________/
3 | | |
4path de l'inventaire __| | |
5nom du playbook _________________________| |
6nom de l'utilisateur ssh _____________________________________|
Résultat (ici à la 2nd exécution et avec une sortie tronquée à 3 serveurs pour faciliter la lisibilité )
On peut voir:
- la collecte des infos sur les machines (Gathering Facts)
- l’exécution de chacune des tâches:
TASK [nom_du_rôle : description_de_la_tâche]
- le récapitulatif final
1PLAY [configure docker training platform] **************************************************************************************************
2
3TASK [Gathering Facts] *********************************************************************************************************************
4ok: [docker-04.exemple.tld]
5ok: [docker-02.exemple.tld]
6ok: [docker-03.exemple.tld]
7
8TASK [deb10-docker : install requirements] *************************************************************************************************
9ok: [docker-00.exemple.tld]
10ok: [docker-02.exemple.tld]
11ok: [docker-03.exemple.tld]
12
13TASK [deb10-docker : add Docker GPG key] ***************************************************************************************************
14ok: [docker-04.exemple.tld]
15ok: [docker-02.exemple.tld]
16ok: [docker-00.exemple.tld]
17
18TASK [deb10-docker : add Docker Repository] ************************************************************************************************
19ok: [docker-01.exemple.tld]
20ok: [docker-04.exemple.tld]
21ok: [docker-03.exemple.tld]
22
23TASK [deb10-docker : install Docker CE] ****************************************************************************************************
24ok: [docker-04.exemple.tld]
25ok: [docker-00.exemple.tld]
26ok: [docker-01.exemple.tld]
27
28
29TASK [deb10-docker : start and enable Docker] **********************************************************************************************
30ok: [docker-04.exemple.tld]
31ok: [docker-01.exemple.tld]
32ok: [docker-03.exemple.tld]
33
34TASK [deb10-docker : install Docker-compose] ***********************************************************************************************
35ok: [docker-03.exemple.tld]
36ok: [docker-00.exemple.tld]
37ok: [docker-04.exemple.tld]
38
39TASK [deb10-docker : install ctop docker monitoring tool] **********************************************************************************
40ok: [docker-04.exemple.tld]
41ok: [docker-02.exemple.tld]
42ok: [docker-00.exemple.tld]
43
44TASK [deb10-docker : ensure group docker exists] *******************************************************************************************
45ok: [docker-04.exemple.tld]
46ok: [docker-02.exemple.tld]
47ok: [docker-01.exemple.tld]
48
49TASK [deb10-docker : add docker-cleanup script] ********************************************************************************************
50ok: [docker-04.exemple.tld]
51ok: [docker-03.exemple.tld]
52ok: [docker-01.exemple.tld]
53
54TASK [deb10-docker : create user "formation" and put it in docker and sudo groups] *********************************************************
55ok: [docker-04.exemple.tld]
56ok: [docker-02.exemple.tld]
57ok: [docker-00.exemple.tld]
58
59PLAY RECAP *********************************************************************************************************************************
60docker-00.exemple.tld : ok=11 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
61docker-01.exemple.tld : ok=11 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
62docker-02.exemple.tld : ok=11 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Conclusion
On mesure facilement l’intérêt d’un tel outil pour la gestion d’un parc de serveur. Il limite grandement les tâches répétitives et garantie l’homogénéité des configurations. Cette article ne couvre que la partie émergée de l’iceberg Ansible, n’hésistez pas à vous plonger dans la doc plutôt bien faite. Pour les allergiques de la ligne de commande, sachez qu’il existe un front-end Web: AWX.