Je fais beaucoup de revues de code sur Ansible et j'écris beaucoup moi-même. Au cours de l'analyse des erreurs (celles des autres comme les miennes), ainsi que lors de plusieurs entretiens, j'ai compris la principale erreur commise par les utilisateurs d'Ansible : ils s'attaquent à des choses complexes sans avoir maîtrisé les bases.
Pour remédier à cette injustice universelle, j'ai décidé d'écrire une introduction à Ansible pour ceux qui le connaissent déjà. Je préviens, ce n'est pas un résumé de manuels, c'est un long article avec beaucoup de mots et pas d'images.
Le niveau attendu du lecteur est celui qui a déjà écrit plusieurs milliers de lignes de YAML, qui a déjà quelque chose en production, mais qui trouve que "tout est un peu tordu".
Noms
La principale erreur d'un utilisateur d'Ansible est de ne pas savoir comment s'appelle chaque chose. Si vous ne connaissez pas les noms, vous ne pouvez pas comprendre ce qui est écrit dans la documentation. Un exemple vivant : lors de l'entretien, une personne, qui prétendait apparemment avoir beaucoup écrit sur Ansible, n'a pas pu répondre à la question "de quels éléments se compose un playbook ?". Et quand j'ai suggéré que "la réponse attendue était que le playbook se compose de plays", le commentaire dévastateur a suivi : "nous ne l'utilisons pas". Les gens écrivent sur Ansible pour de l'argent et ne utilisent pas de plays. En réalité, ils les utilisent, mais ne savent pas ce que c'est.
Commençons donc par le simple : comment chaque chose s'appelle. Peut-être que vous le savez, peut-être que non, parce que vous n'avez pas fait attention en lisant la documentation.
ansible-playbook exécute le playbook. Un playbook est un fichier avec l'extension .yml/.yaml, à l'intérieur duquel il y a quelque chose comme :
---
- hosts: group1
roles:
- role1
- hosts: group2,group3
tasks:
- debug:Nous avons déjà compris que tout ce fichier est un playbook. Nous pouvons montrer où se trouvent les rôles (roles), où se trouvent les tâches (tasks). Mais où est le play ici ? Et quelle est la différence entre un play, un role ou un playbook ?
Tout cela se trouve dans la documentation. Et beaucoup de gens passent à côté. Les débutants parce qu'il y a trop d'informations et qu'on ne peut pas tout retenir d'un coup. Les plus expérimentés parce que ce sont des "choses triviales". Si vous êtes expérimenté, relisez ces pages au moins une fois tous les six mois, et votre code s'améliorera considérablement.
Alors, retenez bien : un playbook est une liste composée de plays et import_playbook.
Voici un play :
- hosts: group1
roles:
- role1Et voici un autre play :
- hosts: group2,group3
tasks:
- debug:Qu'est-ce qu'un play ? À quoi sert-il ?
Un play est un élément clé du playbook, car le play et uniquement le play lie la liste des rôles et/ou des tâches à la liste des hôtes sur lesquels ils doivent être exécutés. Dans les profondeurs de la documentation, on peut trouver une mention de delegate_to, les plugins de lookup locaux, les paramètres spécifiques au network-cli, les jump hosts, etc. Ils permettent de modifier légèrement l'emplacement d'exécution des tâches. Mais, oubliez cela. Chacune de ces options complexes a des applications très spécifiques, et elles ne sont certainement pas universelles. Ici, nous parlons des bases que tout le monde devrait connaître et utiliser.
Si vous voulez exécuter "quelque chose" "quelque part" — vous écrivez un play. Pas un rôle. Pas un rôle avec des modules et des délégués. Vous prenez et vous écrivez un play. Dans lequel, dans le champ hosts, vous listez où exécuter, et dans roles/tasks — ce qui doit être exécuté.
C'est simple, non ? Et comment pourrait-il en être autrement ?
Un des moments caractéristiques où les gens veulent le faire autrement qu'avec un play, c'est le "rôle qui configure tout". On veut avoir un rôle qui configure et de serveurs un type de premier niveau, et des serveurs de deuxième niveau.
Un exemple archétypal est la surveillance. On veut avoir un rôle monitoring qui configure la surveillance. Le rôle monitoring est attribué aux hôtes de surveillance (dans le play correspondant). Mais, il s'avère que pour surveiller, nous devons installer des paquets sur les hôtes que nous surveillons. Pourquoi ne pas utiliser un delegate ? Et puis il faut configurer iptables. delegate ? Et puis il faut écrire/corriger la configuration pour la base de données, pour que la surveillance fonctionne. delegate ! Et si la créativité arrive, on peut faire une délégation include_role dans une boucle imbriquée avec un filtre astucieux sur la liste des groupes, et à l'intérieur include_role on peut aussi faire delegate_to encore une fois. Et ça continue…
Un souhait louable — avoir un seul rôle monitoring qui "fait tout" — nous entraîne dans un véritable enfer dont la sortie est souvent de tout réécrire depuis le début.
Où cela a-t-il mal tourné ? Au moment où vous avez découvert que pour exécuter la tâche "x" sur l'hôte X, vous deviez aller sur l'hôte Y et faire "y" là-bas, vous auriez dû réaliser un simple exercice : aller et écrire un play qui effectue y sur l'hôte Y. Ne pas ajouter quelque chose dans "x", mais écrire depuis zéro. Même avec des variables en dur.
En fait, dans les paragraphes ci-dessus, tout est dit correctement. Mais ce n'est pas votre cas ! Parce que vous voulez écrire un code réutilisable, qui est DRY et ressemble à une bibliothèque, et vous devez trouver comment le faire.
Voici une autre grave erreur cachée ici. Une erreur qui a transformé de nombreux projets, initialement admissibles (cela pourrait être mieux, mais tout fonctionne et peut être facilement complété), en un véritable désastre, où même l'auteur ne peut s'y retrouver. Cela fonctionne, mais Dieu nous préserve de vouloir changer quoi que ce soit.
Cette erreur se résume ainsi : un rôle est une fonction de bibliothèque. Cette analogie a tué tant de bonnes initiatives qu’il est triste de regarder. Un rôle n'est pas une fonction de bibliothèque. Il ne peut pas effectuer de calculs et ne peut pas prendre de décisions au niveau de play. Rappelez-moi quelles décisions prend play ?
Merci, vous avez raison. Play prend une décision (plus précisément, contient l'information) concernant les tâches et les rôles à exécuter sur quels hôtes.
Si vous déléguez cette décision à un rôle, en plus des calculs, vous condamnez votre existence (et celle de la personne qui tentera de comprendre votre code) à être misérable. Un rôle ne décide pas où il s'exécute. Cette décision est prise par play. Un rôle fait ce qu'on lui dit, là où on lui dit.
Pourquoi programmer avec Ansible est dangereux, et pourquoi COBOL est meilleur qu'Ansible, nous en parlerons dans le chapitre sur les variables et jinja. Pour l'instant, disons simplement une chose : chaque calcul que vous effectuez laisse une empreinte indélébile de modifications globales de variables, et vous ne pouvez rien y faire. Dès que deux "empreintes" se croisent, tout est perdu.
Remarque pour les curieux : un rôle peut en effet influencer le contrôle de flux. Il existe delegate_to et il a des applications raisonnables. Il y a meta : end host/play. Mais ! Rappelez-vous, nous apprenons les bases ? Avez-vous oublié delegate_to. Nous parlons du code le plus simple et le plus beau sur Ansible. Qui est facile à lire, facile à écrire, facile à déboguer, facile à tester et facile à compléter. Donc, une fois de plus :
play et uniquement play décide sur quels hôtes quoi exécuter.
Dans cette section, nous avons examiné l'opposition entre play et rôle. Parlerons maintenant des relations tasks vs rôle.
Tâches et Rôles
Considérons play :
- hosts: somegroup
pre_tasks:
- some_tasks1:
roles:
- role1
- role2
post_tasks:
- some_task2:
- some_task3:Supposons que vous deviez faire foo. Et cela ressemble à foo: name=foobar state=present. Où cela doit-il être écrit ? dans pre ? post ? Faut-il créer un rôle ?
… Et où sont passées les tasks ?
Nous repartons des bases — la structure de play. Si vous nagez dans cette question, vous ne pouvez pas utiliser play comme fondement de tout le reste, et votre résultat devient "précaire".
L'appareil play : directive hosts, paramètres de play lui-même et sections pre_tasks, tasks, roles, post_tasks. Les autres paramètres pour play ne nous importent pas pour le moment.
L'ordre de leurs sections avec les tâches et les rôles : pre_tasks, roles, tasks, post_tasks. Étant donné que l'ordre d'exécution sémantiquement entre tasks et roles n'est pas clair, les meilleures pratiques indiquent que nous ajoutons la section tasks, seulement s'il n'y a pas de roles. S'il y a roles, alors toutes les tâches associées sont placées dans les sections pre_tasks/post_tasks.
Il ne reste que ce qui est sémantiquement clair : d'abord pre_tasks, puis roles, puis post_tasks.
Mais nous n'avons toujours pas répondu à la question : où écrire l'appel du module foo ? Faut-il écrire un rôle entier pour chaque module ? Ou mieux vaut-il avoir un rôle global ? Et si ce n'est pas un rôle, où écrire - dans pre ou dans post ?
S'il n'y a pas de réponse argumentée à ces questions, c'est un signe d'absence d'intuition, c'est-à-dire les fameuses "bases instables". Voyons cela. D'abord, une question de contrôle : si play a pre_tasks et post_tasks (et pas de tasks ni de roles), quelque chose peut-il se casser si je déplace la première tâche de post_tasks à la fin pre_tasks?
Bien sûr, la formulation de la question sous-entend que cela va casser. Mais quoi précisément ?
... Les handlers. L'étude des bases révèle un fait important : tous les handlers se flushent automatiquement après chaque section. C'est-à-dire que toutes les tâches de pre_tasks, puis tous les handlers qui ont été notifiés sont exécutés. Ensuite, toutes les rôles et tous les handlers qui ont été notifiés dans les rôles sont exécutés. Puis post_tasks et leurs handlers.
Ainsi, si vous déplacez une tâche de post_tasks dans pre_tasks, alors, potentiellement, vous l'exécuterez avant d'exécuter le handler. Par exemple, si dans pre_tasks , quelque chose est installé et configuré serveur web, et dans post_tasks vers lequel quelque chose est envoyé, alors déplacer cette tâche dans la section pre_tasks fera que, au moment de "l'envoi", le serveur ne sera pas encore lancé et tout cassera.
Et maintenant, réfléchissons encore, pourquoi avons-nous besoin de pre_tasks et post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks nous permettra de travailler avec les résultats d'exécution des rôles (y compris les handlers).
Un expert pointilleux d'Ansible nous dira qu'il y a meta: flush_handlers, mais pourquoi aurions-nous besoin de flush_handlers, si nous pouvons compter sur l'ordre d'exécution des sections dans play ? De plus, l'utilisation de meta: flush_handlers peut nous causer des surprises avec des handlers répétés, générer des avertissements étranges en cas d'utilisation de when au block , etc. Plus vous connaissez Ansible, plus vous pourrez nommer de nuances pour une solution "rusée". Et la solution simple - l'utilisation d'une séparation naturelle entre pre/roles/post - ne provoque pas de nuances.
Et revenons à notre 'foo'. Où le placer ? Dans pre, post ou dans roles ? Évidemment, cela dépend de si nous avons besoin des résultats du gestionnaire pour foo. S'il n'y en a pas, alors foo ne doit pas être placé ni dans pre, ni dans post — ces sections ont un sens particulier — exécuter des tâches avant et après le tableau principal de code.
Maintenant, la réponse à la question "rôle ou tâche" dépend de ce qui existe déjà dans le play — s'il y a des tasks, il faut les ajouter dans tasks. S'il y a des roles, il faut faire un rôle (même s'il ne se compose que d'une seule task). Je rappelle que tasks et roles ne sont pas utilisés en même temps.
Comprendre les bases d'Ansible donne des réponses justifiées à ce qui semble être des questions de goût.
Tâches et rôles (partie deux)
Discutons maintenant de la situation où vous commencez à écrire un playbook. Vous devez faire foo, bar et baz. S'agit-il de trois tâches, d'un rôle ou de trois rôles ? En résumé : à quel moment doit-on commencer à écrire des rôles ? Quel est l'intérêt d'écrire des rôles quand on peut écrire des tâches ?… Qu'est-ce qu'un rôle ?
Une des erreurs les plus graves (j'en ai déjà parlé) est de considérer qu'un rôle est comme une fonction dans la bibliothèque d'un programme. À quoi ressemble une description généralisée d'une fonction ? Elle prend des arguments en entrée, interagit avec des effets secondaires, produit des effets collatéraux et retourne une valeur.
Maintenant, attention. Qu'est-ce qui peut être fait dans un rôle ? Appeler des effets secondaires — toujours, cela fait partie de la définition même d'Ansible — créer des effets secondaires. Avoir des causes secondaires ? Évidemment. Mais concernant "transmettre une valeur et la retourner" — c'est ici que ça ne marche pas. Premièrement, vous ne pouvez pas transmettre une valeur à un rôle. Vous pouvez définir une variable globale avec une durée de vie correspondant au play dans la section vars pour le rôle. Vous pouvez également définir une variable globale avec une durée de vie au play à l'intérieur du rôle. Ou même avec une durée de vie du playbook (set_fact/register). Mais vous ne pouvez pas avoir de "variables locales". Vous ne pouvez pas "recevoir une valeur" et "la retourner".
Ce qui en découle est essentiel : il est impossible d'écrire quelque chose sur Ansible sans provoquer d'effets secondaires. La modification de variables globales est toujours un effet secondaire pour une fonction. Dans Rust, par exemple, la modification d'une variable globale — c'est unsafe. Et dans Ansible — c'est le seul moyen d'influencer les valeurs pour un rôle. Faites attention aux mots utilisés : pas "transmettre une valeur à un rôle", mais "modifier les valeurs que le rôle utilise". Il n'y a pas d'isolation entre les rôles. Il n'y a pas d'isolation entre les tâches et les rôles.
Total : un rôle n'est pas une fonction.
Quels sont les avantages des rôles ? Tout d'abord, les rôles ont des valeurs par défaut (/default/main.yaml), deuxièmement, les rôles ont des répertoires supplémentaires pour stocker des fichiers.
Pourquoi les valeurs par défaut sont-elles bonnes ? Parce que dans la pyramide de Maslow, qui présente une hiérarchie assez complexe des variables dans Ansible, les valeurs par défaut des rôles sont les moins prioritaires (à l'exception des paramètres de ligne de commande d'Ansible). Cela signifie que si vous devez fournir des valeurs par défaut sans vous inquiéter qu'elles ne remplacent des valeurs provenant d'inventaires ou de variables de groupe, alors les valeurs par défaut des rôles sont le seul endroit approprié pour vous. (Je mens un peu - il y a aussi |d(your_default_here), mais si nous parlons d'emplacements fixes, alors ce ne sont que les valeurs par défaut des rôles).
Qu'est-ce qui est encore bon dans les rôles ? Ils ont leurs propres répertoires. Ce sont des répertoires pour les variables, tant permanentes (c’est-à-dire calculées pour le rôle) que dynamiques (il existe un motif ou un anti-motif appelé include_vars avec {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Ce sont des répertoires pour files/, templates/. De plus, cela permet d'avoir des modules et des plugins propres aux rôles (library/). Cependant, comparé aux tâches dans des playbooks (qui peuvent également avoir tout cela), l'avantage ici est uniquement que les fichiers ne sont pas mélangés en un seul grand tas, mais en plusieurs petits tas séparés.
Un autre détail : il est possible d'essayer de créer des rôles qui seront disponibles pour la réutilisation (via galaxy). Après l'apparition des collections, la distribution des rôles peut être considérée comme presque oubliée.
Ainsi, les rôles possèdent deux caractéristiques importantes : ils ont des valeurs par défaut (une caractéristique unique) et ils permettent de structurer le code.
Pour revenir à la question initiale : quand faire des tâches et quand faire des rôles ? Les tâches dans un playbook sont le plus souvent utilisées soit comme "colle" avant/après les rôles, soit comme élément de construction autonome (dans ce cas, il ne devrait pas y avoir de rôles dans le code). Un mélange de tâches normales et de rôles est clairement désordonné. Il faut s'en tenir à un style spécifique - soit des tâches, soit des rôles. Les rôles offrent une séparation des entités et des valeurs par défaut, tandis que les tâches permettent une lecture plus rapide du code. En général, des morceaux de code plus "fixes" (importants et complexes) sont placés dans les rôles, tandis que des scripts auxiliaires sont écrits sous forme de tâches.
Il est possible de faire un import_role comme tâche, mais si vous écrivez cela, préparez-vous à expliquer pourquoi vous voulez le faire pour votre propre sens esthétique.
Un lecteur pointilleux peut dire que les rôles peuvent importer des rôles, que les rôles peuvent avoir des dépendances via galaxy.yml, et il y a aussi le terrible et horrible. include_role — je rappelle que nous améliorons nos compétences en Ansible de base, et non en gymnastique rythmique.
Gestionnaires et tâches
Discutons d'une autre chose évidente : les gestionnaires. Savoir les utiliser correctement est presque un art. Quelle est la différence entre un gestionnaire et une tâche ?
Puisque nous nous rappelons les bases, voici un exemple :
- hosts: group1
tasks:
- foo:
notify: handler1
handlers:
- name: handler1
bar:Dans un rôle, les gestionnaires se trouvent dans rolename/handlers/main.yaml. Les gestionnaires sont partagés entre tous les participants au jeu : pre/post_tasks peuvent appeler les gestionnaires du rôle, et le rôle peut appeler les gestionnaires du play. Cependant, les appels de gestionnaires "inter-rôles" entraînent beaucoup plus de WTF que la répétition d'un gestionnaire trivial. (Un autre élément des meilleures pratiques est d'éviter de répéter les noms des gestionnaires).
La principale différence est que la tâche s'exécute (idempotemment) toujours (plus ou moins les balises et when), tandis que le gestionnaire s'exécute en fonction du changement d'état (notify ne se déclenche que s'il y a eu un changement). Qu'est-ce que cela implique ? Par exemple, lors de l'exécution répétée, si aucune modification n'a eu lieu, il n'y aura pas de gestionnaire. Et pourquoi pourrait-il être nécessaire d'exécuter un gestionnaire alors qu'il n'y a pas eu de changement dans la tâche génératrice ? Par exemple, parce que quelque chose s'est cassé et qu'il y a eu un changement, mais l'exécution n'est pas parvenue jusqu'au gestionnaire. Par exemple, parce que le réseau était temporairement hors service. La configuration a changé, le service n'a pas été redémarré. Lors de la prochaine exécution, la configuration ne change plus, et le service reste avec l'ancienne version de la configuration.
La situation avec la configuration n'est pas résoluble (en fait, on pourrait inventer un protocole spécial de redémarrage avec des drapeaux de fichiers, etc., mais ce ne serait plus du 'basic ansible' sous aucune forme). En revanche, il existe une autre histoire fréquente : nous avons installé une application, enregistré son .service- fichier, et maintenant nous voulons faire son daemon_reload et state=started. Et un endroit naturel pour cela semble être un gestionnaire. Mais si nous en faisons une tâche au bas de la liste des tâches ou un rôle, elle sera exécutée de manière idempotente à chaque fois. Même si le playbook échoue en cours de route. Cela ne résout absolument pas le problème du redémarrage (il n'est pas possible de faire une tâche avec l'attribut restarted, car cela perd l'idempotence), mais il est certainement utile de faire state=started, la stabilité générale des playbooks augmente car le nombre de dépendances et d'états dynamiques diminue.
Une autre propriété positive d'un gestionnaire est qu'il n'encombre pas la sortie. Pas de changements — pas de 'skipped' ou 'ok' superflus dans la sortie — plus facile à lire. C'est aussi un inconvénient — si vous trouvez une faute de frappe dans une tâche exécutée linéairement à la première exécution, les gestionnaires ne seront exécutés que si un changement a eu lieu, c'est-à-dire dans certaines conditions — très rarement. Par exemple, la première fois de votre vie après cinq ans. Et, bien sûr, il y aura une faute de frappe dans le nom et tout se cassera. Et la deuxième fois, on ne pourra pas les exécuter — car il n'y a pas eu de changement.
Il faut parler séparément de l'accessibilité des variables. Par exemple, si vous notifiez pour une tâche avec une boucle, que se passera-t-il dans les variables ? On peut le deviner par voie analytique, mais ce n'est pas toujours trivial, surtout si les variables proviennent de différents endroits.
Ainsi, les gestionnaires sont beaucoup moins utiles et beaucoup plus problématiques qu'il n'y paraît. Si vous pouvez écrire quelque chose de joli (sans complications) sans gestionnaires, il est préférable de le faire. Si cela ne peut pas être fait joliment — mieux vaut alors utiliser des gestionnaires.
Un lecteur perspicace remarque à juste titre que nous n'avons pas discuté écouter, que le gestionnaire peut appeler notify pour un autre gestionnaire, que le gestionnaire peut inclure import_tasks (qui peut faire include_role avec with_items), que le système de gestionnaires dans Ansible est Turing-complet, que les gestionnaires d'un include_role se croisent d'une manière très intéressante avec les gestionnaires du play, etc. — tout cela n'est clairement pas "fondamentaux").
Bien qu'il y ait un certain WTF qui est en réalité une fonctionnalité, et dont il faut se souvenir. Si vous avez une tâche qui s'exécute avec delegate_to et qu'elle a un notify, alors le gestionnaire correspondant s'exécute sans delegate_to, c'est-à-dire sur l'hôte auquel le play est assigné. (Bien que le gestionnaire puisse, bien sûr, avoir delegate_to aussi).
Je voudrais également dire quelques mots sur les rôles réutilisables. Avant l'apparition des collections, l'idée était de créer des rôles universels qui peuvent être ansible-galaxy install et je suis parti. Cela fonctionne sur tous les systèmes d'exploitation dans toutes les variantes et toutes les situations. Mon avis est le suivant : cela ne fonctionne pas. Tout rôle très général avec un support pour 100500 cas est condamné aux abîmes des bugs spécifiques. On peut les juguler par des tests massifs, mais comme pour tout test, soit vous avez le produit cartésien des valeurs d'entrée et une fonction totale, soit vous avez "des scénarios couverts". À mon avis, il est bien mieux d'avoir un rôle linéaire (complexité cyclomatique 1). include_vars, moins vous avez d'if (explicites ou déclaratifs — sous la forme
ou sous la forme when sur un ensemble de variables), mieux c'est pour le rôle. Il arrive parfois qu'il faille faire des embranchements, mais, je le répète, moins il y en a, mieux c'est. Donc, il semble qu'un bon rôle avec galaxy (ça fonctionne, après tout !) avec une tonne include_vars puisse être moins préféré qu'un rôle "propre" de cinq tâches. Le moment où un rôle avec galaxy est meilleur — c'est quand vous commencez à écrire quelque chose. Le moment où cela devient pire — c'est quand quelque chose se casse, et vous soupçonnez que cela vient du "rôle avec galaxy". Vous l'ouvrez, et là, vous avez cinq inclusions, huit listes de tâches et une pile when ‘ov… Et il faut s'y retrouver. Au lieu de cinq tâches dans une liste linéaire où il n'y a même pas de quoi se casser. whenDans les prochaines parties
Un peu sur l'inventaire, les variables de groupe, le plugin host_group_vars, les hostvars. Comment attacher une épine de Gordius à partir d'une spaghetti. Portée et priorité des variables, modèle de mémoire d'Ansible. "Alors où stocker le nom d'utilisateur pour la base de données ?".
- jinja: {{ jinja }}
— nosql notype nosense plastique malléable. Il est partout, même là où vous ne l'attendez pas. Un peu sur!!unsafeet un yaml savoureux.Je fais beaucoup de revues de code pour d'autres sur Ansible et j'écris beaucoup moi-même.
Source : habr.com
