Les bases d'Ansible, sans lesquelles vos playbooks ne seraient qu'un morceau de pâte collante

Je fais beaucoup de critiques sur le code Ansible d'autres personnes et j'écris beaucoup moi-même. Au cours de l'analyse des erreurs (celles des autres et des miennes), ainsi que d'un certain nombre d'entretiens, j'ai réalisé la principale erreur commise par les utilisateurs d'Ansible : ils se lancent dans des choses complexes sans maîtriser les bases.

Pour corriger cette injustice universelle, j'ai décidé d'écrire une introduction à Ansible pour ceux qui le connaissent déjà. Je vous préviens, ce n'est pas un récit de Mans, c'est un long livre avec beaucoup de lettres et pas d'images.

Le niveau attendu du lecteur est que plusieurs milliers de lignes de yamla ont déjà été écrites, quelque chose est déjà en production, mais « d'une manière ou d'une autre, tout est tordu ».

Les titres

La principale erreur commise par un utilisateur d'Ansible est de ne pas savoir comment s'appelle quelque chose. Si vous ne connaissez pas les noms, vous ne pouvez pas comprendre ce que dit la documentation. Un exemple vivant : lors d'une interview, une personne qui semblait dire qu'elle écrivait beaucoup en Ansible n'a pas pu répondre à la question « de quels éléments se compose un playbook ? Et lorsque j’ai suggéré que « la réponse était attendue que le playbook consiste en du jeu », le commentaire accablant « nous n’utilisons pas cela » a suivi. Les gens écrivent Ansible pour de l'argent et n'utilisent pas le jeu. Ils l'utilisent réellement, mais ne savent pas ce que c'est.

Alors commençons par quelque chose de simple : comment ça s’appelle ? Peut-être que vous le savez, ou peut-être pas, parce que vous n’y avez pas prêté attention lorsque vous avez lu la documentation.

ansible-playbook exécute le playbook. Un playbook est un fichier avec l'extension yml/yaml, à l'intérieur duquel se trouve quelque chose comme ceci :

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Nous avons déjà réalisé que l’ensemble de ce fichier est un playbook. Nous pouvons montrer où se trouvent les rôles et où se trouvent les tâches. Mais où est le jeu ? Et quelle est la différence entre un jeu et un rôle ou un livre de jeu ?

Tout est dans la documentation. Et ça leur manque. Débutants - parce qu'il y en a trop et que vous ne vous souviendrez pas de tout d'un coup. Expérimenté - parce que « des choses triviales ». Si vous êtes expérimenté, relisez ces pages au moins une fois tous les six mois et votre code deviendra le meilleur de sa catégorie.

Alors n'oubliez pas : Playbook est une liste composée de jeux et import_playbook.
C'est une pièce de théâtre :

- hosts: group1
  roles:
    - role1

et ceci est aussi une autre pièce :

- hosts: group2,group3
  tasks:
    - debug:

Qu'est-ce que le jeu ? Pourquoi l'est-elle ?

Play est un élément clé d'un playbook, car play et only play associent une liste de rôles et/ou de tâches à une liste d'hôtes sur lesquels ils doivent être exécutés. Dans les profondeurs de la documentation, vous pouvez trouver mention de delegate_to, plugins de recherche locale, paramètres spécifiques au réseau, hôtes de saut, etc. Ils permettent de modifier légèrement le lieu où les tâches sont effectuées. Mais oubliez ça. Chacune de ces options astucieuses a des utilisations très spécifiques et elles ne sont certainement pas universelles. Et nous parlons de choses de base que tout le monde devrait connaître et utiliser.

Si vous voulez jouer « quelque chose » « quelque part », vous écrivez une pièce de théâtre. Pas un rôle. Pas un rôle avec des modules et des délégués. Vous le prenez et écrivez une pièce de théâtre. Dans lequel, dans le champ des hôtes, vous indiquez où exécuter et dans les rôles/tâches - quoi exécuter.

Simple, non ? Comment pourrait-il en être autrement?

L’un des moments caractéristiques où les gens ont le désir de faire cela sans jouer est le « rôle qui met tout en place ». J'aimerais avoir un rôle qui configure à la fois les serveurs du premier type et les serveurs du deuxième type.

Un exemple archétypal est la surveillance. J'aimerais avoir un rôle de surveillance qui configurera la surveillance. Le rôle de surveillance est attribué aux hôtes de surveillance (selon le jeu). Mais il s'avère que pour la surveillance, nous devons livrer des packages aux hôtes que nous surveillons. Pourquoi ne pas utiliser un délégué ? Vous devez également configurer iptables. déléguer? Vous devez également écrire/corriger une configuration pour le SGBD afin d'activer la surveillance. déléguer! Et si la créativité fait défaut, alors vous pouvez faire une délégation include_role dans une boucle imbriquée en utilisant un filtre délicat sur une liste de groupes, et à l'intérieur include_role tu peux faire plus delegate_to encore. Et c'est parti...

Un vœu positif - celui d'avoir un seul rôle de surveillance, qui "fait tout" - nous conduit dans un enfer complet dont le plus souvent il n'y a qu'une seule issue : tout réécrire à partir de zéro.

Où l'erreur s'est-elle produite ici ? Au moment où vous avez découvert que pour effectuer la tâche "x" sur l'hôte X, vous deviez vous rendre sur l'hôte Y et y faire "y", vous deviez faire un exercice simple : aller écrire une pièce de théâtre, qui sur l'hôte Y fait y. N'ajoutez rien à "x", mais écrivez-le à partir de zéro. Même avec des variables codées en dur.

Il semble que tout soit dit correctement dans les paragraphes ci-dessus. Mais ce n'est pas votre cas ! Parce que vous souhaitez écrire du code réutilisable, DRY et semblable à une bibliothèque, et vous devez rechercher une méthode pour le faire.

C’est là que se cache une autre grave erreur. Une erreur qui a transformé de nombreux projets assez bien écrits (cela pourrait être mieux, mais tout fonctionne et est facile à terminer) en une horreur complète que même l'auteur ne peut pas comprendre. Cela fonctionne, mais Dieu vous interdit de changer quoi que ce soit.

L'erreur est la suivante : le rôle est une fonction de bibliothèque. Cette analogie a ruiné tant de bons débuts qu’elle est tout simplement triste à voir. Le rôle n'est pas une fonction de bibliothèque. Elle ne peut pas faire de calculs et elle ne peut pas prendre de décisions au niveau du jeu. Rappelez-moi quelles décisions le jeu prend ?

Merci, vous avez raison. Play prend une décision (plus précisément, il contient des informations) sur les tâches et les rôles à effectuer sur quels hôtes.

Si vous déléguez cette décision à un rôle, et même avec des calculs, vous vous condamnez (et celui qui tentera d'analyser votre code) à une existence misérable. Le rôle ne décide pas où il est joué. Cette décision se prend par le jeu. Le rôle fait ce qu'on lui dit, là où on le lui dit.

Pourquoi est-il dangereux de programmer dans Ansible et pourquoi COBOL est meilleur qu'Ansible, nous en parlerons dans le chapitre sur les variables et jinja. Pour l'instant, disons une chose : chacun de vos calculs laisse derrière lui une trace indélébile des changements dans les variables globales, et vous ne pouvez rien y faire. Dès que les deux « traces » se sont croisées, tout a disparu.

Remarque pour les délicats : le rôle peut certainement influencer le flux de contrôle. Manger delegate_to et il a des utilisations raisonnables. Manger meta: end host/play. Mais! Vous vous souvenez que nous enseignons les bases ? Oublié à propos delegate_to. Nous parlons du code Ansible le plus simple et le plus beau. Ce qui est facile à lire, facile à écrire, facile à déboguer, facile à tester et facile à compléter. Alors encore une fois :

play et seul le jeu décide qui héberge ce qui est exécuté.

Dans cette section, nous avons traité de l’opposition entre jeu et rôle. Parlons maintenant de la relation tâches/rôles.

Tâches et rôles

Pensez à jouer :

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

Disons que vous devez faire foo. Et on dirait foo: name=foobar state=present. Où dois-je écrire ça ? en pré ? poste? Créer un rôle ?

...Et où sont passées les tâches ?

Nous recommençons par les bases : le périphérique de jeu. Si vous flottez sur cette question, vous ne pouvez pas utiliser le jeu comme base pour tout le reste, et votre résultat sera « fragile ».

Appareil de lecture : directive hosts, paramètres pour le jeu lui-même et les pre_tasks, tâches, rôles, sections post_tasks. Les autres paramètres de jeu ne sont plus importants pour nous pour le moment.

L'ordre de leurs sections avec tâches et rôles : pre_tasks, roles, tasks, post_tasks. Puisque sémantiquement l’ordre d’exécution se situe entre tasks и roles n'est pas clair, alors les meilleures pratiques disent que nous ajoutons une section tasks, seulement si ce n'est pas le cas roles. S'il y a roles, alors toutes les tâches attachées sont placées dans des sections pre_tasks/post_tasks.

Reste que tout est sémantiquement clair : d’abord pre_tasksalors rolesalors post_tasks.

Mais nous n’avons toujours pas répondu à la question : où est l’appel du module ? foo écrire? Devons-nous écrire un rôle complet pour chaque module ? Ou vaut-il mieux avoir un rôle épais pour tout ? Et si ce n'est pas un rôle, alors où dois-je écrire - en pré ou en post ?

S’il n’y a pas de réponse raisonnée à ces questions, c’est le signe d’un manque d’intuition, c’est-à-dire de ces mêmes « fondations fragiles ». Voyons cela. Tout d'abord, une question de sécurité : si le jeu a pre_tasks и post_tasks (et il n'y a pas de tâches ou de rôles), quelque chose peut-il se briser si j'effectue la première tâche de post_tasks je vais le déplacer jusqu'à la fin pre_tasks?

Bien entendu, la formulation de la question laisse entendre qu’elle va se briser. Mais quoi exactement ?

... Gestionnaires. La lecture des bases révèle un fait important : tous les gestionnaires sont automatiquement vidés après chaque section. Ceux. toutes les tâches de pre_tasks, puis tous les gestionnaires qui ont été notifiés. Ensuite, tous les rôles et tous les gestionnaires notifiés dans les rôles sont exécutés. Après post_tasks et leurs maîtres.

Ainsi, si vous faites glisser une tâche depuis post_tasks в pre_tasks, alors potentiellement vous l'exécuterez avant que le gestionnaire ne soit exécuté. par exemple, si dans pre_tasks le serveur Web est installé et configuré, et post_tasks quelque chose lui est envoyé, puis transférez cette tâche dans la section pre_tasks cela conduira au fait qu'au moment de «l'envoi», le serveur ne fonctionnera pas encore et tout tombera en panne.

Maintenant réfléchissons à nouveau, pourquoi avons-nous besoin pre_tasks и post_tasks? Par exemple, afin de compléter tout le nécessaire (y compris les gestionnaires) avant de remplir le rôle. UN post_tasks nous permettra de travailler avec les résultats de l'exécution des rôles (y compris les gestionnaires).

Un expert Ansible astucieux nous dira de quoi il s’agit. meta: flush_handlers, mais pourquoi avons-nous besoin de flush_handlers si nous pouvons nous fier à l'ordre d'exécution des sections en jeu ? De plus, l'utilisation de meta:flush_handlers peut nous donner des choses inattendues avec des gestionnaires en double, nous donnant d'étranges avertissements lorsqu'ils sont utilisés. when у block etc. Mieux vous connaissez l’ansible, plus vous pouvez nommer de nuances pour une solution « délicate ». Et une solution simple - utilisant une division naturelle entre pré/rôles/post - n'apporte pas de nuances.

Et revenons à notre « foo ». Où dois-je le mettre ? En pré, post ou rôles ? Évidemment, cela dépend si nous avons besoin des résultats du gestionnaire pour foo. S'ils ne sont pas là, alors foo n'a pas besoin d'être placé ni avant ni après - ces sections ont une signification particulière - exécutant des tâches avant et après le corps principal du code.

Maintenant, la réponse à la question « rôle ou tâche » se résume à ce qui est déjà en jeu - s'il y a des tâches, vous devez les ajouter aux tâches. S'il y a des rôles, vous devez créer un rôle (même à partir d'une seule tâche). Je vous rappelle que les tâches et les rôles ne sont pas utilisés en même temps.

Comprendre les bases d'Ansible fournit des réponses raisonnables à des questions apparemment de goût.

Tâches et rôles (deuxième partie)

Discutons maintenant de la situation dans laquelle vous commencez tout juste à écrire un playbook. Vous devez faire du foo, du bar et du baz. S'agit-il de trois tâches, d'un rôle ou de trois rôles ? Pour résumer la question : à quel moment faut-il commencer à écrire des rôles ? A quoi ça sert d'écrire des rôles quand on peut écrire des tâches ?... Qu'est-ce qu'un rôle ?

L’une des plus grosses erreurs (j’en ai déjà parlé) est de penser qu’un rôle est comme une fonction dans la bibliothèque d’un programme. À quoi ressemble une description de fonction générique ? Il accepte les arguments d'entrée, interagit avec les causes secondaires, génère des effets secondaires et renvoie une valeur.

Maintenant, attention. Que peut-on faire à partir de cela dans le rôle ? Vous êtes toujours les bienvenus pour appeler des effets secondaires, c'est l'essence même de tout Ansible : créer des effets secondaires. Vous avez des causes secondaires ? Élémentaire. Mais avec "passer une valeur et la renvoyer" - c'est là 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 taille de jeu à vie dans la section vars pour le rôle. Vous pouvez définir une variable globale avec une durée de vie en jeu à l'intérieur du rôle. Ou même avec la durée de vie des playbooks (set_fact/register). Mais vous ne pouvez pas avoir de "variables locales". Vous ne pouvez pas « prendre une valeur » et « la renvoyer ».

L'essentiel en découle : on ne peut pas écrire quelque chose dans Ansible sans provoquer d'effets secondaires. Changer les variables globales est toujours un effet secondaire pour une fonction. Dans Rust, par exemple, modifier une variable globale est unsafe. Et dans Ansible, c'est la seule méthode pour influencer les valeurs d'un rôle. Notez les mots utilisés : non pas « transmettre une valeur au rôle », mais « modifier les valeurs que le rôle utilise ». Il n’y a pas d’isolement 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.

Qu'est-ce qui est bien dans le rôle ? Premièrement, le rôle a des valeurs par défaut (/default/main.yaml), deuxièmement, le rôle dispose de répertoires supplémentaires pour stocker les fichiers.

Quels sont les avantages des valeurs par défaut ? Parce que dans la pyramide de Maslow, le tableau plutôt déformé des priorités des variables d'Ansible, les valeurs par défaut des rôles sont les moins prioritaires (moins les paramètres de ligne de commande Ansible). Cela signifie que si vous devez fournir des valeurs par défaut et ne pas vous soucier qu'elles remplacent les valeurs des variables d'inventaire ou de groupe, alors les valeurs par défaut des rôles sont le seul bon endroit pour vous. (je mens un peu - il y en a d'autres |d(your_default_here), mais si nous parlons de lieux stationnaires, alors seul le rôle est par défaut).

Qu'y a-t-il d'autre de génial dans les rôles ? Parce qu'ils ont leurs propres catalogues. Ce sont des répertoires de variables, à la fois constantes (c'est-à-dire calculées pour le rôle) et dynamiques (il existe soit un modèle, soit un anti-modèle - include_vars avec {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Ce sont les répertoires pour files/, templates/. De plus, il vous permet d'avoir vos propres modules et plugins (library/). Mais, par rapport aux tâches d'un playbook (qui peut aussi avoir tout cela), le seul avantage ici est que les fichiers ne sont pas stockés dans une seule pile, mais dans plusieurs piles distinctes.

Encore un détail : vous pouvez essayer de créer des rôles qui seront disponibles pour être réutilisés (via galaxy). Avec l’avènement des collections, la répartition des rôles peut être considérée comme presque oubliée.

Ainsi, les rôles ont deux fonctionnalités importantes : ils ont des valeurs par défaut (une fonctionnalité unique) et ils vous permettent de structurer votre code.

Revenons à la question initiale : quand effectuer des tâches et quand effectuer des rôles ? Les tâches d'un playbook sont le plus souvent utilisées soit comme « colle » avant/après les rôles, soit comme élément de construction indépendant (il ne devrait alors y avoir aucun rôle dans le code). Un tas de tâches normales mélangées à des rôles est une négligence sans ambiguïté. Vous devez adhérer à un style spécifique – soit une tâche, soit un rôle. Les rôles assurent la séparation des entités et des valeurs par défaut, les tâches vous permettent de lire le code plus rapidement. Habituellement, du code plus « stationnaire » (important et complexe) est placé dans des rôles et les scripts auxiliaires sont écrits dans le style des tâches.

Il est possible de faire import_role en tant que tâche, mais si vous écrivez ceci, soyez prêt à expliquer à votre propre sens de la beauté pourquoi vous voulez faire cela.

Un lecteur avisé peut dire que les rôles peuvent importer des rôles, que les rôles peuvent avoir des dépendances via galaxy.yml, et qu'il existe également un terrible et terrible include_role — Je vous rappelle que nous améliorons les compétences en Ansible de base, et non en gymnastique artistique.

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 un drag ?

Puisque nous rappelons les bases, voici un exemple :

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

Les gestionnaires du rôle se trouvent dans rolename/handlers/main.yaml. Les gestionnaires fouillent entre tous les participants à la pièce : les pre/post_tasks peuvent extraire les gestionnaires de rôle, et un rôle peut extraire les gestionnaires de la pièce. Cependant, les appels « à rôles croisés » aux gestionnaires provoquent beaucoup plus de wtf que la répétition d'un gestionnaire trivial. (Un autre élément des meilleures pratiques consiste à essayer de ne pas répéter les noms des gestionnaires).

La principale différence est que la tâche est toujours exécutée (de manière idempotente) (balises plus/moins et when), et le gestionnaire - par changement d'état (notifier les incendies uniquement s'il a été modifié). Qu'est-ce que cela signifie? Par exemple, le fait que lorsque vous redémarrez, s'il n'y a pas de changement, alors il n'y aura pas de gestionnaire. Pourquoi pourrions-nous devoir exécuter le gestionnaire alors qu'il n'y a eu aucun changement dans la tâche de génération ? Par exemple, parce que quelque chose s'est cassé et a changé, mais que l'exécution n'a pas atteint le gestionnaire. Par exemple, parce que le réseau était temporairement en panne. La config a changé, le service n'a pas été redémarré. Au prochain démarrage, la configuration ne change plus et le service reste avec l'ancienne version de la configuration.

La situation avec la configuration ne peut pas être résolue (plus précisément, vous pouvez inventer vous-même un protocole de redémarrage spécial avec des indicateurs de fichier, etc., mais ce n'est plus un « ansible de base » sous quelque forme que ce soit). Mais il y a une autre histoire commune : nous avons installé l'application, l'avons enregistrée .service-file, et maintenant nous le voulons daemon_reload и state=started. Et le lieu naturel pour cela semble être le gestionnaire. Mais si vous en faites non pas un gestionnaire mais une tâche à la fin d'une liste de tâches ou d'un rôle, alors il sera exécuté de manière idempotente à chaque fois. Même si le playbook s’est cassé au milieu. Cela ne résout pas du tout le problème du redémarrage (vous ne pouvez pas effectuer une tâche avec l'attribut restarted, car l'idempotence est perdue), mais cela vaut vraiment la peine de faire state=started, la stabilité globale des playbooks augmente, car le nombre de connexions et l'état dynamique diminuent.

Une autre propriété positive du gestionnaire est qu’il n’obstrue pas la sortie. Il n'y a eu aucun changement - aucun extra sauté ou ok dans la sortie - plus facile à lire. C'est également une propriété négative - si vous trouvez une faute de frappe dans une tâche exécutée linéairement dès la première exécution, alors les gestionnaires ne seront exécutés que lorsqu'ils seront modifiés, c'est-à-dire dans certaines conditions - très rarement. Par exemple, pour la première fois de ma vie, cinq ans plus tard. Et bien sûr, il y aura une faute de frappe dans le nom et tout se cassera. Et si vous ne les exécutez pas une deuxième fois, il n’y a aucun changement.

Séparément, nous devons parler de la disponibilité des variables. Par exemple, si vous notifiez une tâche avec une boucle, que contiendront les variables ? Vous pouvez deviner de manière analytique, mais ce n’est pas toujours trivial, surtout si les variables proviennent d’endroits différents.

... Les gestionnaires sont donc beaucoup moins utiles et bien plus problématiques qu'il n'y paraît. Si vous pouvez écrire quelque chose de magnifiquement (sans fioritures) sans gestionnaires, il est préférable de le faire sans eux. Si ça ne marche pas à merveille, c’est mieux avec eux.

Le lecteur corrosif souligne à juste titre que nous n’avons pas discuté listenqu'un gestionnaire peut appeler notify pour un autre gestionnaire, qu'un 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 de include_role se croisent d'une manière curieuse avec les gestionnaires de play, etc..d. - tout cela n'est clairement pas la « base »).

Bien qu’il existe un WTF spécifique, c’est en fait une fonctionnalité que vous devez garder à l’esprit. Si votre tâche est exécutée avec delegate_to et il a notifié, alors le gestionnaire correspondant est exécuté sans delegate_to, c'est à dire. sur l'hôte où la lecture est attribuée. (Bien que le gestionnaire, bien sûr, puisse avoir delegate_to aussi).

Séparément, je voudrais dire quelques mots sur les rôles réutilisables. Avant l'apparition des collections, il y avait l'idée que l'on pouvait créer des rôles universels qui pourraient être ansible-galaxy install et est allé. Fonctionne sur tous les systèmes d'exploitation de toutes les variantes dans toutes les situations. Donc mon avis : ça ne marche pas. Tout rôle avec masse include_vars, prenant en charge 100500 1 cas, est voué à l'abîme des bogues de coin. Ils peuvent être couverts par des tests massifs, mais comme pour tout test, soit vous disposez d'un produit cartésien de valeurs d'entrée et d'une fonction totale, soit vous avez des « scénarios individuels couverts ». Mon avis est que c'est bien mieux si le rôle est linéaire (complexité cyclomatique XNUMX).

Moins il y a de si (explicites ou déclaratifs - sous la forme when ou formulaire include_vars par ensemble de variables), meilleur est le rôle. Parfois, il faut faire des branches, mais, je le répète, moins il y en a, mieux c'est. Cela semble donc être un bon rôle avec Galaxy (ça marche !) avec un tas de when peut être moins préférable que « son propre » rôle parmi cinq tâches. Le moment où le rôle avec Galaxy est meilleur, c'est quand vous commencez à écrire quelque chose. Le moment où la situation empire est lorsque quelque chose se brise et que vous soupçonnez que c'est à cause du « rôle avec la galaxie ». Vous l'ouvrez et il y a cinq inclusions, huit feuilles de tâches et une pile when'ov... Et nous devons comprendre ça. Au lieu de 5 tâches, une liste linéaire dans laquelle il n'y a rien à casser.

Dans les parties suivantes

  • Un peu sur l'inventaire, les variables de groupe, le plugin host_group_vars, les hostvars. Comment faire un nœud gordien avec des spaghettis. Variables de portée et de priorité, modèle de mémoire Ansible. « Alors, où stockons-nous le nom d'utilisateur de la base de données ? »
  • jinja: {{ jinja }} — pâte à modeler souple nosql notype nosense. Il est partout, même là où on ne l'attend pas. Un peu sur !!unsafe et un délicieux yaml.

Source: habr.com

Ajouter un commentaire