i

Ansible

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: Google Trends

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.

Ansible diagram

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 depuis lequel tout est exécuté via des connexions, essentiellement en SSH, aux nodes de l’inventaire. à sa connexion SSH.
  • 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
  1. docker : détermine la portée dans le fichier d’inventaire ( ici le groupe docker), on peut aussi utiliser all
  2. -i hosts : spécifie le path du fichier d’inventaire ( ici dans le répertoire courant )
  3. -u <ssh_username> : spécifie l’utilisateur autorisé à se connecter en ssh avec une clé publique (root par défaut)
  4. -m shell: indique l’utilisation du module shell
  5. -a: indique les arguments à passer au module
  6. cat /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:

  1. installer les pré-requis
  2. ajouter le dépot Docker et la clé GPG
  3. installer docker-ce
  4. s’assurer que le service docker est activé et démarré
  5. installer docker-compose et ctop
  6. s’assurer que le groupe docker existe
  7. créer un utilisateur formation, l’affecter aux groupes sudo et docker et définir un password
  8. 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 et ansible_architecture pour construire l’URL. Elles sont valuées par ansible lors de la connexion à chaque machine ( mécanisme de Facts)

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