Ecriture, Exécution et Debug de playbook ansible
Publié le : 3 octobre 2019 | Mis à jour le : 22 janvier 2023Dans le précédent article vous aviez découvert l’installation et l’exécution de modules Ansible. Aujourd’hui attardons sur l’écriture de playbook Ansible en utilisant toutes les fonctions mises à notre disposition.
Si vous utilisez un environnement virtuel python, avant tout exécution de vos playbooks Ansible ou installation de librairies n’oublier pas d’activer l’environnement virtuel python avec la commande suivante :
. venv/bin/activate
Construction d’un playbook Ansible
Nous avons vu qu’un playbook ansible est une séquence de tâches ou de rôles décrits dans un fichier. Pour le moment nous n’utiliserons que des tâches (tasks). Un playbook ansible est un fichier au format yaml. YAML est un langage de description comme l’est le HTML. En YAML Le plus important est de respecter l’indentation sans quoi la structure n’est plus compréhensible. Le débogage est de ce fait complexe quand le fichier devient volumineux, mais des EDI ont de très nombreux plugins permettant de détecter les erreurs.
Exemple de playbook ansible permettant d’ajouter le repo EPEL sur Centos :
---
- hosts: localhost
tasks:
- name: Install EPEL repo.
ansible.builtin.package:
name: epel-release
state: present
- name: Install htop (available in epel repo)
yum:
name: htop
state: present
Ce playbook fait appel au module package que j’ai documenté dans ce billet.
Pour lancer un playbook ansible on utilise la commande ansible-playbook. Pour lancer un playbook sur un inventaire on ajoute comme pour la commande ansible l’argument -i avec le chemin de celui-ci. On indique ensuite la localisation du ou des playbook ansible. Il est possible d’enchaîner plusieurs groupes d’exécution sur différentes cibles. Pour cela il suffit de rajouter des blocs Ansible -hosts. Ce qui donne :
---
- hosts: target1
tasks:
- name: Install EPEL repo.
ansible.builtin.package:
name: epel-release
state: present
- hosts: target2
- name: Install htop
ansible.builtin.package:
name: htop
state: present
Pour vérifier la syntaxe d’un ansible playbook on peut ajouter –syntax-check et le lancer en mode dry-run avec –check
ansible-playbook -i inventories/production myplaybook.yml --check
# ou
ansible-playbook -i inventories/production myplaybook.yml --dry-run
Si vous voulez écrire du code de qualité je vous recommande vivement d’installer ansible-lint.
Les variables Ansible
Les facts d’Ansible
Les « gather facts », sont des variables qu’Ansible recueille sur les machines
cibles : le système d’exploitation, les adresses IP, la mémoire, le disque, etc.
Ensuite, il stocke ces informations dans des variables qu’on nomme facts. Le
moyen le plus simple est de lancer le module setup sur localhost : ansible -m setup localhost
Ces variables prédéfinies peuvent ensuite être utilisées dans les playbook ansible. Pour cela il suffit de les mettre entre doubles accolades :
- name: Show ansible_selinux var
ansible.builtin.debug:
msg: "{{ ansible_selinux }}"
Ce qui donne :
ok: [localhost] => {
"msg": {
"config_mode": "enforcing",
"mode": "enforcing",
"policyvers": 28,
"status": "enabled",
"type": "targeted"
}
}
Dans le cas où vous n’utilisez pas ces facts
vous pouvez désactiver sa collecte
en ajoutant dans l’entête de votre playbook Ansible [gather_facts: no]. Cela va accélérer
l’exécution de votre playbook ansible.
Vos variables
Le plus simple pour ajouter une variable à votre playbook ansible est de définir une
section vars. Ensuite, vous pouvez les récupérer comme pour les facts
avec des
doubles accolades.
---
- hosts: all
vars:
nginx_ssl: /etc/nginx/ssl
nginx_conf_file: /etc/nginx/nginx.conf
Il est possible de définir des variables Ansible dans un fichier séparé en ajoutant la section vars_files.
---
hosts: all
vars_files:
- myvarsfile.yml
Définir des variables Ansible par group et host
Il est possible de créer des variables dans des fichiers séparés qui seront
automatiquement chargés par Ansible. Il faut qu’il se nomme host_vars
et
group_vars
et doivent se trouver là où se trouve l’inventaire.
Pour définir une variable à un host
ou un groupe spécifique il suffit de nommer
le fichier par le nom du host
ou du groupe.
Ensuite leur syntaxe est fonction de l’extension du fichier.
---
test2: test2
Sauvegarde du résultat d’une tache Ansible dans une variable
Pour enregistrer une variable il suffit d’ajouter à votre tache le mot-clé register. Dans le cas de dictionnaire, il suffit d’ajouter un . entre les champs (test.ping)
---
- hosts: localhost
tasks:
- name: Ping localhost
ansible.builtin.ping:
register: test
- name: display var
ansible.builtin.debug:
msg: "{{ test.ping }}"
Les boucles Ansible
Dans certains cas, nous avons besoin de réaliser une tâche sur plusieurs cibles, par exemple installer plusieurs paquets et tout cela en une seule opération. Ansible intègre les boucles de différentes manières :
Les boucles standards : with_items ou loop
Le premier type de boucle permet de répéter une action sur une liste de valeurs, par exemple une liste de packages :
...
- name: add several package
ansible.builtin.package:
name={{ item }}
state=present
with_items:
- nginx
- python
Les boucles imbriquées : with_nested
Cet exemple permettra de comprendre le fonctionnement de ce type de boucle :
vars:
keys:
- key1
- key2
- key3
tasks:
- name: Distribute SSH keys among multiple users
ansible.builtin.lineinfile:
dest: /home/{{ item[0] }}/.ssh/authorized_keys
line: {{ item[1] }}
state: present
with_nested:
- [ ’calvin’, ’josh’, ’alice’ ]
- ’{{ keys }}’
Ici Ansible va boucler sur chaque utilisateur et remplira leur fichier authorized_keys avec les 3 clés définies dans la liste.
Les boucles sur dictionnaire: with_dict
Les boucles sur dictionnaires : En terminologie Python, un dictionnaire est un ensemble défini de variables possédant plusieurs valeurs :
vars:
users:
alice:
name: Alice
telephone: 123-456-7890
bob:
name: Bob
telephone: 987-654-3210
tasks:
- name: Print phone records
ansible.builtin.debug:
msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
with_dict: "{{users}}"
Do until
Do until peut être utilisé pour attendre qu’une condition soit vraie où qu’elle ait atteint le nombre maxi d’itération pour sortir de la boucle.
- ansible.builtin.shell: /usr/bin/foo
register: result
until: result.stdout.find("OK")
retries: 5
delay: 10
Le playbook exemple ci-dessus exécute le module shell de manière récursif jusqu’à ce que le résultat du module retourne “OK” dans sa sortie standard ou que la tâche ait été itérée 5 fois avec un délai de 10 secondes. La variable result aura également une nouvelle clé «attemps» qui aura le nombre des tentatives effectuées par la boucle.
Et bien d’autres
Il existe toute une série de type de boucle répondant à des besoins spécifiques. Si vous avez des choses complexes à construire n’hésitez pas à jeter un œil à cette section de la documentation Ansible.
Les conditions d’Ansible: when
Parfois, vous voudrez qu’une tache particulière ne s’exécute ou pas dans certaines conditions. Par exemple ne pas installer un certain paquet si le système d’exploitation correspond à une version particulière, ou encore de procéder à certaines étapes de nettoyage si un système de fichiers est saturé.
tasks:
- name: "shut down all Debian"
ansible.builtin.command: /sbin/shutdown -t now
when: ansible_facts[’os_family’] == "Debian"
La syntaxe des conditions (and or, in, not in, is … ) reprend celle de Jinja2 qui nous servira à créer par la suite des modèles.
Il est possible de mettre des conditions en fonction de l’exécution ou de l’échec ou du bypass d’une tache précédente. Pour ignorer une erreur d’une tache, il suffit d’ajouter la clé ignore_errors à true (Ne pas utiliser dans tous les cas).
tasks:
- ansible.builtin.command: /bin/false
register: result
ignore_errors: True
- ansible.builtin.command: /bin/something
when: result is failed
- ansible.builtin.command: /bin/something_else
when: result is succeeded
- command: /bin/still/something_else
when: result is skipped
On peut regrouper des actions utilisant les mêmes conditions en recourant à des blocks Ansible.
Jinja
Jinja est à la base un module python écrit pour Django permettant de produire rapidement du texte dynamique. L’idée principale est de fournir à un module, un modèle et une liste de valeurs pour qu’il construise dynamiquement un résultat. Jinja fournit des possibilités plus avancées, comme appliquer des filtres sur une variable, appliquer des boucles sur des listes et bien d’autre encore. Et cela va nous servir dans les playbook Ansible.
Les filtres Ansible
Les filtres dans Ansible sont utilisés pour transformer des données. Sa syntaxe est : {{ var | filter }}
---
tasks:
- ansible.builtin.shell: cat /some/path/to/file.json
register: result
- ansible.builtin.set_fact:
myvar: "{{ result.stdout | from_json }}"
D’autres sont là pour contraindre leur existence, pour assigner une valeur par défaut ou encore leur omission :
---
- name: touch files with an optional mode
ansible.builtin.file:
dest: "{{ item.path | mandatory }}"
state: touch
mode: "{{ item.mode | default(omit) }}"
loop:
- path: /tmp/foo
- path: /tmp/bar
- path: /tmp/baz
mode: "0444"
Il existe toute une série de filtres, des filtres mathématiques, pour des opérations sur des listes, les dictionnaires, les adresses mac ou ip, des expressions régulières, … Leur documentation se trouve dans ses trois billets : 1, 2 et 3
Si vous ne trouvez pas votre bonheur vous pouvez développer vos propres filtres.
Les tests Ansible
Les tests en Jinja permettent d’évaluer les expressions de modèle et de renvoyer True ou true. La principale différence entre les tests et les filtres réside dans le fait que les tests Jinja sont utilisés à des fins de comparaison, alors que les filtres sont utilisés pour la manipulation de données.
Ils sont tous écrits de la forme valeur is type_de_test (paramètres)
Tests sur les strings
Ansible propose trois types de recherche sur les strings : is match
, is search
et is regex
match retourne vrai s’il trouve le motif au début de la chaîne, contrairement à
search qui lui réussit s’il trouve le motif n’importe où dans la chaîne. Ces
tests acceptent des arguments ignorecase
et multiline
.
vars:
url: "http://example.com/users/foo/resources/bar"
tasks:
- debug:
msg: "matched pattern 1"
when: url is match("http://example.com/users/.*/resources/.*")
- debug:
msg: "matched pattern 2"
when: url is search("/users/.*/resources/.*")
- debug:
msg: "matched pattern 3"
when: url is search("/users/")
Les tests sur les booléens
Les tests truthy
et falsy
acceptent un paramètre facultatif appelé
convert_bool
qui tentera de convertir les indicateurs en booléens.
- debug:
msg: "Truthy"
when: value is truthy
vars:
value: "some string"
- debug:
msg: "Falsy"
when: value is falsy(convert_bool=True)
vars:
value: "off"
Comparer des versions
Pour comparer un numéro de version, il existe un test version
, On peut ainsi
vérifier que la version trouvée ansible_facts[‘distribution_version’] version est
supérieure ou égale à celle attendue.
tasks:
- debug:
msg: "Attention votre distribution est trop vieille"
when: ansible_facts['distribution_version'] is version('20.04', '>')
Ce test accepte trois paramètres : operator
, version_type
et strict
.
- operator l’opérateur de comparaison <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne
- version_type défini le type de versioning et accepte les valeurs suivantes:
loose
,strict
,semver
,semantic
- strict ne peut être utilisé avec version_type
tasks:
- debug:
msg: "Attention votre distribution est trop vieille"
when: sample_version_var is version('1.0', operator='lt', strict=True) }}
Tester si une liste contient une valeur
contains
permet de vérifier l’existence d’une valeur dans une liste. Il
accepte les filtres select
, reject
, selectattr
, et rejectattr
vars:
lacp_groups:
- master: lacp0
network: 10.65.100.0/24
gateway: 10.65.100.1
dns4:
- 10.65.100.10
- 10.65.100.11
interfaces:
- em1
- em2
- master: lacp1
network: 10.65.120.0/24
gateway: 10.65.120.1
dns4:
- 10.65.100.10
- 10.65.100.11
interfaces:
- em3
- em4
tasks:
- debug:
msg: "{{ (lacp_groups|selectattr('interfaces', 'contains', 'em1')|first).master }}"
Tests sur les ensembles
Pour voir si une liste inclut ou est incluse dans une autre liste, vous pouvez
utiliser subset
et superset
:
vars:
a: [1,2,3,4,5]
b: [2,3]
tasks:
- debug:
msg: "A includes B"
when: a is superset(b)
- debug:
msg: "B is included in A"
when: b is subset(a)
Tests sur les fichiers
Ces tests permettent de tester le type de fichiers ou son état :
- debug:
msg: "path is a directory"
when: mypath is directory
- debug:
msg: "path is a file"
when: mypath is file
- debug:
msg: "path is a symlink"
when: mypath is link
- debug:
msg: "path already exists"
when: mypath is exists
- debug:
msg: "path is {{ (mypath is abs)|ternary(’absolute’,’relative’)}}"
- debug:
msg: "path is the same file as path2"
when: mypath is same_file(path2)
- debug:
msg: "path is a mount"
when: mypath is mount
Tests sur les résultats de taches
On peut controler le statut d’une tache parmi : failed
, changed
, succeeded
et skipped
tasks:
- shell: /usr/bin/foo
register: result
ignore_errors: True
- debug:
msg: "it failed"
when: result is failed
Les handlers Ansible
Les handlers d'Ansible permettent de déclencher des événements après l’activation d’un module ou la modification d’un fichier de config (les handlers ne sont lancés que si le fichier de conf change réellement : état changed). En outre, bien que plusieurs tâches puissent nécessiter une même action, l’action en question ne sera lancée qu’après l’exécution de tous les blocs tâches.
Pour cela il suffit d’ajouter un notify à votre tache Ansible avec le nom du handler et de définir le bloc handler avec le même nom à la fin du fichier.
- name: enable vhost
command: a2ensite test_site
notify:
- restart apache
handlers:
- name: restart apache
service:
name: apache2
state: restarted
Si vous souhaitez forcer le déclenchement d’un handler après une tache ajouter la ligne suivante :
- name: enable vhost
command: a2ensite test_site
notify:
- restart apache
# Force restart handlers
- meta: flush_handlers
Escalation de privilèges
Avant tout, il faut bien se rappeler qu’Ansible utilise principalement le
protocole ssh
pour se connecter à vos machines hôtes. La bonne pratique est de
créer sur les machines hôtes un user ansible ne possédant pas les droits root
,
mais lui donner les droits sudo
.
Dans ce cas, il faudra peut-être demander à Ansible de faire à ce qu’uon appelle de l’escalade de privilèges.
Pour cela il est possible d’utiliser les paramètres Ansible.
become
: mis à true
pour activer l’escalade de privilèges
become_user
: défini l’utilisateur souhaité par défaut, il s’agit de root
become_method
: la méthode d’escalade de privilège. Par défaut sudo
, mais on
peut aussi utiliser su
ou doas
. On peut aussi développer ses propres plugins
de connexion.
Exemples
Démarrer le service apache avec sudo
- name: Ensure the httpd service is running
ansible.builtin.service:
name: httpd
state: started
become: true
Utiliser le compte apache
- name: Run a command as the apache user
ansible.builtin.command: somecommand
become: true
become_user: apache
Il est possible de définir ces paramètres directement dans les variables de groupes. Vous pouvez aussi les définir au niveau le plus haut de votre playbook ou dans la commande de lancement de votre playbook (j’aime moins) :
---
- hosts: all
become: true
become_method: enable
tasks:
- name: Install EPEL repo.
ansible.builtin.package:
name: epel-release
state: present
ou dans la ligne de commande :
ansible-playbook -i staging --become --become-method=su --ask-become-pass
Plus d’infos sur become
Debug des playbooks Ansible
Vous avez plusieurs outils à votre disposition pour débuger vos playbooks.
Utilisons ce playbook comme exemple qui ne fonctionne pas bien sur :
---
- hosts: all
gather_facts: true
become: true
vars:
pkg_name: not_exist
tasks:
- name: Install a package
ansible.builtin.package:
name: "{{ pkg_name }}"
Le mode verbose
Un premier moyen de voir ce qui se passe lorsqu’on lance un playbook ansible, avec d’activer le mode verbose -v. Chaque “v” supplémentaire permet à d’augmenter la verbosité de la sortie.
ansible-playbook -vvv -i inventory --limit host1 test.yml
ansible-playbook 2.9.15
<host1> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/home/vagrant/.ansible/cp/5401241998 host1 '/bin/sh -c '"'"'rm -f -r /home/vagrant/.ansible/tmp/ansible-tmp-1614782435.453363-165214-175234282313188/ > /dev/null 2>&1 && sleep 0'"'"''
<host1> (0, b'', b'')
fatal: [host1]: FAILED! => {
"changed": false,
"failures": [
"No package not_exist available."
],
"invocation": {
"module_args": {
"allow_downgrade": false,
"autoremove": false,
"bugfix": false,
"conf_file": null,
"disable_excludes": null,
"disable_gpg_check": false,
"disable_plugin": [],
"disablerepo": [],
"download_dir": null,
"download_only": false,
"enable_plugin": [],
"enablerepo": [],
"exclude": [],
"install_repoquery": true,
"install_weak_deps": true,
"installroot": "/",
"list": null,
"lock_timeout": 30,
"name": [
"not_exist"
],
"releasever": null,
"security": false,
"skip_broken": false,
"state": null,
"update_cache": false,
"update_only": false,
"validate_certs": true
}
},
"msg": "Failed to install some of the specified packages",
"rc": 1,
"results": []
}
PLAY RECAP *************************************************************************************
host1 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 resc
Avec ce niveau, vous obtenez déja bcp d’informations.
Check_mode
Le second moyen est de lancer votre playbook ansible en mode check
. En
fait, dans ce mode :
- Les modules qui effectuent une action de modification, d’ajout ou de suppression vont uniquement vérifier que la source et la destination sont présentes, l’action ne sera pas effectuée.
- Les modules qui effectuent peut-être des actions de modifications, mais sans possibilité de contrôle, comme modules command et shell, seront ignorés.
- Ceux qui ne font que rendre une information comme les modules find et stat seront tout de même exécutés.
ansible-playbook main.yml --check
Pour contrôler vous pouvez activer le mode diff et vous ne verrez aucune modification (sauf pour les tasks utilisant shell ou command):
ansible-playbook main.yml --check --diff
Si vous voulez désactiver le mode check sur une tache il suffit de rajouter ceci à votre tache :
check_mode: no
Si certaines de vos taches retourne des erreurs vous pouvez ajouter ceci à ces taches pour poursuivre le playbook:
ignore_errors: '{{ ansible_check_mode }}'
Le mode debugger
Vous pouvez activer le mode débogage pour chaque exécution de vos playbooks en
mettant la variable enable_task_debugger
à True dans le fichier ansible.cfg:
[defaults]
enable_task_debugger = True
Un autre moyen est de le rajouter dans votre playbook :
---
- hosts: all
gather_facts: true
become: true
debugger: on_failed
vars:
pkg_name: not_exist
tasks:
- name: Install a package
ansible.builtin.package:
name: "{{ pkg_name }}"
Lorsqu’on lance ce playbook ansible, qui plante, car le package n’existe pas, on voit apparaître cette ligne :
TASK [Install a package] ***********************************************************************
fatal: [host2]: FAILED! => {"changed": false, "failures": ["No package not_exist available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
[host2] TASK: Install a package (debug)>
A partir de là nous pouvons utiliser les commandes suivantes pour debugger :
p : affiche des informations utilisées pendant l’exécution par votre tâche module task.args[key] = value: met à jour l’argument du module. task_vars[key] = value: met à jour les variables de votre playbook. update_task: si vous modifiez les task_vars alors utilisez cette commande pour recréer la tâche à partir de votre nouvelle structure de données. redo: exécutez à nouveau la tâche. continue: passe à la tâche suivante. quit: quitte le débogueur. L’exécution du playbook est abandonnée.
Ici modifions le user le nom du package avant de relancer :
TASK [Install a package] ***********************************************************************
fatal: [host2]: FAILED! => {"changed": false, "failures": ["No package not_exist available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
[host2] TASK: Install a package (debug)> p task.args
{'name': 'not_exist'}
[host2] TASK: Install a package (debug)> task.args['name'] = 'bash'
[host2] TASK: Install a package (debug)> redo
ok: [host2]
Si vous avez plusieurs hosts il faudra le répéter autant de fois.
Relancer depuis une étape particulière
Parfois si vous voulez relancer un playbook à partir d’une étape particulière et
non depuis le début, il suffit d’utiliser l’option --start-at-task
ansible-playbook playbook.yml --start-at-task="install packages"
Jouer un playbook de manière interactive
Avec cette option, Ansible s’arrête à chaque tâche et demande s’il doit l’exécuter ou pas. Par exemple, si vous avez une tâche appelée “configure ssh”, l’exécution du playbook s’arrêtera et demandera :
ansible-playbook playbook.yml --step
...
Perform task: configure ssh (y/n/c):
c
permet de quitter le mode interactif !
L’utilitaire ansible-console
Je l’ai documenté dans ce billet. Cet outil permet de lancer les modules ansible de manière interactifs.
Changer la sortie d’Ansible
Si comme moi vous trouvez que la sortie standard de la commande ansible-playbook vous pouvez en changer.
On va prendre cet exemple pour montrer les différences.
- name: Show ansible_selinux var
ansible.builtin.debug:
msg: "{{ ansible_selinux }}"
donne :
PLAY [Test stdout] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Show ansible_selinux var] ************************************************
ok: [localhost] => {
"msg": {
"status": "Missing selinux Python library"
}
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Sortie au format YAML
Il suffit dans le fichier ansible.cfg
[defaults]
stdout_callback = yaml
PLAY [Test stdout] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Show ansible_selinux var] ************************************************
ok: [localhost] =>
msg:
status: Missing selinux Python library
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Sortie au format JSON
[defaults]
stdout_callback = json
{
"custom_stats": {},
"global_custom_stats": {},
"plays": [
{
"play": {
"duration": {
"end": "2021-04-10T12:36:31.761991Z",
"start": "2021-04-10T12:36:30.529488Z"
},
"id": "7d9a4545-499f-5cc6-45f2-000000000005",
"name": "Test stdout"
},
...
D’autres formats de sortie
La liste complète de la communauté des stdout_callback Ansible
D’autres qu’il faut placer dans le répertoire plugins/callback :
Utilisation de vagrant pour provisionner des machines Ansible de test
J’utilise beaucoup vagrant pour mettre au point mes playbooks Ansible. Même si le nombre de serveurs est restreint par les ressources de votre machine hôte, c’est un excellent moyen pour finaliser vos playbooks avant de les lancer sur des machines réelles. je vois propose de lire mon billet sur vagrant
Si vous voulez d’autres infos sur Ansible, à la fin de mon billet d'introduction à Ansible j’ai regroupé tous les articles parlant d’Ansible.