Pour ce lab, nous n’allons pas avoir besoin d’autre chose qu’une machine de test, avec Ansible installé, un inventory configuré, et c’est tout. Pour ceux qui débarquent sur le sujet, je vous conseille le très concis Guide Ansible pour Administrateurs Systèmes de RedHat.
Sur quoi sont basées les conditions
Allons droit au but. Les conditions se basent sur dans la plupart des cas 2 éléments:
- Les facts, données fournies par les libraires Ansible (pour remonter par exemple le nom ou la configuration d’un hôte)
- Les variables, données arbitraires en votre contrôle
Faisons le tour du propriétaire. Pour avoir accès aux facts de notre machine de test, lançons la commande suivante :
# Afficher les facts d'un hôte ou groupe
ansible -l <host> -m setup
Pour avoir accès aux variables, la commande ansible-inventory fera très bien l’affaire
# Afficher les variables assignées à une machine de l'inventory
ansible-inventory --vars --host <host>
# Afficher toutes les variables selon les hôtes de l'inventory
ansible-inventory --vars --list
Très bien, on voit nos data réellement utilisables au sein de nos playbooks.
Conditions sur des facts
C’est la chose qui est le mieux documenté, donc passons vite dessus. Dans le cas où ma machine est une distribution CentOS, je vais avoir ces facts de définis:
os_family: RedHat
distribution: CentOS
Libre à moi d’utiliser le fact que je préfère sur ces deux-là. Je peux donc définir un comportement différent selon mes différentes distributions en charge. Dans tous les cas, j’utilise le mot when
qui va servir durant tout ce billet de blog, me permettant de définir une condition à l’exécution d’une tâche purement et simplement. À noter que ce when
peut également servir à encadrer l’exécution d’un playbook ou d’un bloc de tâches (block
).
tasks:
- name: Installer l'ancêtre Apache sur RedHat/CentOS
when: ansible_facts['os_family'] == "RedHat"
package:
name: httpd
state: present
- name: Installer l'ancêtre Apache sur Debian
when: ansible_facts['distribution'] == "CentOS"
package:
name: apache2
state: present
Il s’agit d’un exemple pas vraiment optimisé. Dans l’idéal, le mieux pour ce cas précis serait de définir le nom du paquet dans des variables de groupes, si mon inventaire me permet de gérer des groupes sur les distributions.
# dans group_vars/debian.yaml
apache_package: apache2
# dans group_vars/redhat.yaml
apache_package: httpd
Conditions sur les variables
Les variables étant définies par vos soins, vous pouvez approcher le comportement que vous voulez exactement grâce à un jeu de variables bien construites. L’héritage de variables d’Ansible permet de les définir niveau hôtes, groupes, groupes de groupes, à différents endroits (par défaut, override), les possibilités sont très nombreuses et expliquées dans la documentation.
Partons sur cet exemple de variables définies par exemple dans un groupe d’hôtes via group_vars/kube.yaml
.
install_docker: true
docker_version: 20.10.1
On peut très bien imaginer le bloc de tâches suivant:
tasks:
- name: Install Docker and configure it
when: install_docker
block:
- name: Package installation
apt:
name: docker.io={{ docker_version }}
Les conditions dans les templates Jinja
Une des grosses forces de Ansible tout aussi bien que Salt-Stack est d’emporter le moteur de templating Jinja. Étant un langage de template entier, il a une syntaxe différente et demande un temps d’adaptation et de chute dans certains pièges évidents. Voyons comment ça s’utilise en reprenant l’exemple ci-dessus:
# Permet de vérifier la présence d'une variable et son contenu si elle est booléenne
{% if install_docker is defined and install_docker %}
Selon l'exemple ci-dessus avec la variable install_docker à {{ install_docker }},
la version de Docker étant installée sera la version {{ docker_version | default('18.04') }}.
Un filtre vient d'être utilisé en suffixe de l'appel à une variable pour définir une valeur par défaut au cas où docker_version n'est pas renseigné.
{% endif %}
Spécifier des filtres “default” évite certaines mauvaises surprises mais peut rendre du code complexe compliqué à debugger, à vous de voir comment vous voulez gérer vos cas de variables vides. Vous retrouverez la liste complète des filtres jinja sur sa doc officielle.
Conditions sur des variables temporaires (registers)
Il est possible de renseigner des variables temporaires selon l’exécution d’une commande, par exemple pour exécuter un test dans la pure tradition de la commande du même nom, ou alors pour interroger le résultat d’une commande et utiliser son résultat dans une variable dépendante. Le mot clé ici est register
, déclarant une variable sur les informations de la tâche exécutée.
- name: undeploy service
shell:
cmd: "test -f /etc/systemd/system/{{ my_custom_service }}.service"
register: found_service
failed_when: false
changed_when: false
- name: undeploy service action block
when: found_service is succeeded
block:
- name: stop and disable service
service:
name: "{{ my_custom_service }}"
state: stopped
enabled: no
- name: rm service file
file:
path: "/etc/systemd/system/{{ my_custom_service }}.service"
state: absent
Ici des commandes ont été executées dans le cas où une commande a renvoyé un code d’erreur 0 (succeeded), ce qui revient au même de faire une condition sur found_service.rc == 0
, rc
signifiant bien sûr return code. Grâce à ce concept, on garantit l’idempotence et on évite d’utiliser des variables long-terme venant alourdir inutilement l’inventory.
EDIT 2022-02-04 : Je recommande l’utilisation de failed_when
et changed_when
dans le cas où une tâche permet de déclencher l’exécution ou non de tâches sous-jacentes. Au lieu d’utiliser ignore_errors
qui est la pratique la plus répandue mais aussi la plus moche, disons-le, avec failed_when
on peut spécifier assez finement ce qui est répertorié comme un échec. Ici, il n’y a pas d’échec devant être reporter à l’administrateur, seulement un test permettant de vérifier qu’un service est déployé (ou non).
Il existe d’autres données liées à un register, notamment stdout
, results
et d’autres dont vous trouverez la liste exhaustive ici.
Voici un exemple qui me sert en production, vérifier que iptables-legacy est utilisé sur mes systèmes Debian plutôt que iptables-nft. Ces deux implémentations d’iptables sont des alernatives dans le jargon Debian, mais nft n’est pas compatible avec la brique Kube-proxy de clusters Kubernetes.
- name: iptables get binary
command: /usr/bin/update-alternatives --query iptables
register: iptables_alternatives
- name: iptables set alternative
command: update-alternatives --set iptables /usr/sbin/iptables-legacy
when: "not iptables_alternatives.stdout | regex_search('Value: /usr/sbin/iptables-legacy')"
Comme vous l’avez compris, je regarde la sortie d’une commande, puis y applique un filtre (cette fois-ci pas jinja mais Ansible) faisant une recherche regex. Je prends l’inverse de la condition avec le mot clé not
et le tour est joué !
En espérant vous avoir aidé à y voir un peu plus clair sur les possibilités d’automatisation Ansible.