Heterogeneous Targets¶
Objectif
Écrire des playbooks capables de gérer un parc multi-distributions (Debian, Rocky, SUSE, Ubuntu) en utilisant l'exécution conditionnelle (when) et les variables par famille d'OS, puis factoriser avec include_vars et le module générique package.
Durée
~60 minutes (2 playbooks, dont un long, + validation NTP)
Jusqu'à présent, nous avons appelés les mêmes rôles sur tous les hôtes/groupe d'hôtes.
- Le nom du paquet peut être différent (
apache2vshttpd, cf. Workshop 08). - Le nom du service :
chronysous Debian,chronydsous RedHat/SUSE. - Les chemins de configuration :
/etc/chrony/chrony.confsous Debian,/etc/chrony.confailleurs.
Un playbook qui part du principe qu'on est sur Debian peut casser sur une autre distribution. L'enjeu du workshop 13 est de gérer ce type d'hétérogénéité sans écrire un playbook par distribution.
Pour cela, deux outils complémentaires :
when: conditionne l'exécution d'une tâche à une expression Jinja2 (sur un fact), ce qui permet d'activer/désactiver un bloc de tâches selon le Target Host courant.- Les variables par famille d'OS (chargées via
include_vars) : seules les valeurs (nom du paquet, du service, chemin du fichier de conf…) varient selon le factansible_os_family.
Mise en place du lab¶
cd $HOME/git/gh/formation-ansible/atelier-17
vagrant up
^up^ssh ansible
# sur le control host
cd ansible/projets/ema/playbooks/
when¶
Le mot-clé when s'applique au niveau d'une tâche et accepte une expression Jinja2 évaluée par hôte. Si l'expression est fausse pour un hôte donné, la tâche est marquée skipped pour cet hôte et ignorée ; les autres hôtes continuent normalement.
- name: install chrony (debian based)
ansible.builtin.apt:
name: chrony
state: present
when: ansible_os_family == "Debian"
when ne met pas les variables entre {{ }}
L'expression après le when: est déjà évaluée comme du Jinja2. On écrit donc when: ansible_os_family == "Debian" pas when: "{{ ansible_os_family }} == 'Debian'" (qui fonctionne mais meh).
Le fact ansible_os_family est collecté automatiquement au début de chaque playbook (sauf si gather_facts: false). Il regroupe plusieurs distributions, plus stable que ansible_distribution qui change à chaque version... La liste complète des valeurs possibles est maintenue directement dans le code source d'ansible.
Voir Workshop 12 pour l'exploration des facts (ansible -m setup) et le rappel sur INJECT_FACTS_AS_VARS.
À vous de jouer¶
chrony-01.yml approche naïve (un when par distro)¶
Première version : duplicat des tâches (install, déploiement de conf, démarrage, vérifications) une fois par famille d'OS, when: conservé en "garde-fou".
- Modules spécifiques :
ansible.builtin.aptpour Debian,ansible.builtin.dnfpour RedHat,community.general.zypperpour SUSE. Le dernier n'est pas dansansible.builtin, il faut installer la collectioncommunity.general(ansible-galaxy collection install community.general). - Deux copies de
chrony.confavec unwhenchacune : Debian stocke la conf dans/etc/chrony/chrony.conf, RedHat et SUSE dans/etc/chrony.conf. Le contenu est identique, seul ledest:change. - Deux handlers distincts (
restart chrony (debian based)/restart chrony (rocky/suse)) car le nom du service systemd diffère (chronyvschronyd). meta: flush_handlers: force l'exécution des handlers avant les tâches de vérification (chronyc tracking,chronyc sources). Sinon les handlers tournent après les vérifications, et on inspecterait un daemon qui n'aurait pas encore relu sa conf (cf. Workshop 09 Handlers).pause: 15avant leschronyc: même une fois le service (re)démarré,chronydmet quelques secondes à ouvrir son socket de ce que j'ai vu. Sans pause, la première commandechronycpeut renvoyer506 Cannot talk to daemon.
| chrony-01.yml | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |

À l'exécution les tâches affichent ok / changed / skipped selon l'hôte, les lignes skipped sont la trace visible des when: qui ont filtré les tâches non applicables.
À la fin de l'exécution :
TASK [show chrony sources] ***********************************************************************
ok: [rocky] => {
"chrony_sources.stdout_lines": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample ",
"===============================================================================",
"^- 5.196.76.84 2 6 17 11 -5589us[-5589us] +/- 18ms",
"^* 51.68.44.27 3 6 17 12 +1385us[+6364us] +/- 23ms",
"^? 78.197.169.39 0 7 0 - +0ns[ +0ns] +/- 0ns",
"^- 185.123.84.51 3 6 17 12 +577us[+5555us] +/- 79ms"
]
}
ok: [debian] => {
"chrony_sources.stdout_lines": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample ",
"===============================================================================",
"^- 5.196.76.84 2 6 17 12 +7683us[+7683us] +/- 24ms",
"^- 51.68.44.27 3 6 17 12 +1394us[+1394us] +/- 15ms",
"^? 78.197.169.39 0 7 0 - +0ns[ +0ns] +/- 0ns",
"^* 185.123.84.51 3 6 17 12 +388us[-4584us] +/- 71ms"
]
}
ok: [suse] => {
"chrony_sources.stdout_lines": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample ",
"===============================================================================",
"^- 5.196.76.84 2 6 17 11 +2632us[+2632us] +/- 23ms",
"^* 51.68.44.27 3 6 17 11 -207us[-3923us] +/- 14ms",
"^? 78.197.169.39 0 7 0 - +0ns[ +0ns] +/- 0ns",
"^- 185.123.84.51 3 6 17 11 +3618us[ -98us] +/- 75ms"
]
}
ok: [ubuntu] => {
"chrony_sources.stdout_lines": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample ",
"===============================================================================",
"^? 54.38.114.34 0 6 0 - +0ns[ +0ns] +/- 0ns",
"^- 51.68.44.27 3 6 17 12 +1535us[+1535us] +/- 17ms",
"^? 78.197.169.39 2 7 100 19 +325us[-3932us] +/- 67ms",
"^* 185.123.84.51 3 6 17 12 +283us[ +275us] +/- 70ms"
]
}
Bilan de chrony-01.yml : ça marche, mais le fichier fait plus de 200 lignes pour installer un seul service, et chaque modification (ex. changer un serveur NTP) impose de toucher les deux tâches deploy chrony.conf. Sur un vrai parc avec 5+ services et 3+ familles d'OS, on obtiendrait rapidement des milliers de lignes dupliquées.
chrony-02.yml include_vars et package¶
Isolation des valeurs qui varient dans des fichiers de variables par famille de distribution. La logique devient identique pour toutes les cibles ; seul le jeu de variables change.
include_varsavec un nom de fichier dynamique :vars/chrony02_{{ ansible_os_family | lower }}.yml. Le factansible_os_familypassé en minuscules avec le filtre jinja2| lower, charge alors automatiquement le fichier qui le concerne.package: abstraction qui délègue àapt,dnf,zypper… selon la distribution cible. Combiné à une variablechrony_package, on écritname: "{{ chrony_package }}"une seule fois.
Après réinstallation des VMs, on crée les trois fichiers de variables un par famille de distros. Pour chrony, seuls le nom du service (chrony vs chronyd) et le répertoire de configuration (/etc/chrony vs /etc) varient :
mkdir -p vars
for f in debian redhat suse; do
[ "$f" = "debian" ] && s=chrony c=/etc/chrony || s=chronyd c=/etc
cat > vars/chrony02_${f}.yml <<EOF
---
chrony_package: chrony
chrony_service: ${s}
chrony_confdir: ${c}
EOF
done
Le playbook chrony-02.yml contient qu'une tâche d'installation, une de déploiement de configuration et un handler. Toute la logique spécifique à la distribution a migré vers les fichiers vars/.
| chrony-02.yml | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |

Même résultat fonctionnel que chrony-01.yml. Mais approche à privilégier pour un parc hétérogène. Le Workshop 14 Jinja + Templates va plus loin en déplaçant le contenu du fichier chrony.conf dans un template jinja2 réutilisable.
Je retiens
when: ansible_os_family == "Debian"conditionne l'exécution d'une tâche à la famille de distribution (cf. Workshop 12 pour les facts).- L'approche
chrony-01.yml(un blocwhenpar distro) fonctionne mais duplique beaucoup de code. - L'approche
chrony-02.ymlavecinclude_varset le module génériquepackageest plus maintenable, les différences sont isolées dans des fichiers de variables par famille de distribution. - Les handlers utilisent des variables pour s'adapter au nom du service (
chronyouchronyd), comme vu dans Workshop 09.
Cheatsheet¶
| Symptôme | Cause probable | Correction |
|---|---|---|
include_vars: "vars/chrony02_{{ ansible_os_family \| lower }}.yml" → Could not find or access sur Rocky |
ansible_os_family vaut RedHat → redhat (ok ici) mais sur Ubuntu c'est Debian → attention aux mappings |
Aligner les noms de fichiers sur les valeurs réelles de ansible_os_family (liste) |
community.general.zypper → couldn't resolve module |
La collection community.general n'est pas installée |
ansible-galaxy collection install community.general |
when: ansible_os_family in ["RedHat", "Suse"] ignoré |
ansible_os_family absent (pas de gather_facts) |
S'assurer de gather_facts: true (par défaut) ou inclure setup au début |
Le handler restart chrony ne tourne pas avant les tâches de vérification |
Les handlers s'exécutent en fin de playbook (cf. Workshop 09) | Ajouter ansible.builtin.meta: flush_handlers avant les tâches de vérification |
chronyc tracking → 506 Cannot talk to daemon après le démarrage |
Le service chronyd / chrony vient de démarrer et n'est pas encore prêt à répondre |
Conserver la pause: seconds: 5..15 ou utiliser wait_for sur le port/socket |
Sources (principales)¶
- Docs
ansibleConditionals (when) - Docs
ansibleansible.builtin.include_vars - Docs
ansibleansible.builtin.group_by(alternative pour dynamiser les groupes) - Docs
ansibleansible.builtin.package(module générique multi-distros) - Docs
ansibleansible_os_familyvsansible_distribution - Docs
ansibleblock/rescue/always(gestion d'erreur par bloc) - Docs
ansibleTags (exécution sélective de tâches) - Docs
ansibleCollectioncommunity.general - Docs
ansibleansible-galaxy collection install
Précédent : Facts + Implicit Vars Suite : Jinja + Templates