Livres de Jeu Ansible

Si vous poursuivez des objectifs de certification, ce document rencontre les suivants :

  • 1. Comprendre les composants de base d’Ansible
    • Inventaires
    • Modules
    • Variables
    • “Facts”
    • Jeux
    • Playbooks
  • 5. Créez des jeux et des playbooks Ansible
    • Savoir travailler avec les modules Ansible d’usage courant
    • Utiliser des variables pour récupérer les résultats de l’exécution d’une commande
    • Utiliser les conditions (“conditionals”) pour contrôler l’exécution de la lecture
    • Configurer la gestion des erreurs
    • Créer des playbooks pour configurer les systèmes dans un état spécifié
  • 6. Créer et utiliser des modèles pour créer des fichiers de configuration personnalisés
  • 7. Travailler avec des variables et des “Facts” Ansibles

On proposera pour ce document et les suivants sur les livres de jeu différentes activités pratiques :

Tous les exemples opérationnels, c’est-à-dire que l’on peut exécuter tels que présentés, se trouvent dans le dossier demo du tutoriel interactif sur Ansible, 16) Exercices du support.

1. L’analogie d’un livre de jeu Ansible

Les livres de jeu (playbooks) sont écrits selon un langage d’automatisation simple et puissant. Les Playbooks peuvent orchestrer avec précision plusieurs parties d’une topologie d’infrastructure, avec un contrôle très détaillé du nombre de machines à traiter à la fois.

L’approche d’Ansible en matière d’orchestration est une approche simple et précise : le code d’automatisation devrait être pérenne et il devrait y avoir très peu de choses à retenir sur la syntaxe ou des fonctionnalités spéciales.

Les livres de jeu (playbooks) sont écrits en langage YAML, Ain’t Markup Language. YAML expose un minimum de syntaxe et propose un modèle de configuration ou de processus plutôt qu’un langage de script ou de programmation.

Chaque livre de jeu est composé de un ou plusieurs “séances de jeu (plays)” dans une liste.

Un jeu est une analogie sportive qui définit un état ou un modèle et qui se “rejoue” de différentes manières à d’autres moments.

Le but d’un “jeu” est de faire correspondre un “groupe” contenant des “hôtes (hosts)” dans des “roles” bien définis représentés par des objets Ansible appelés des “tâches (tasks)”. Sur le plan fondamental, une “tâche” Ansible n’est rien de plus qu’un appel à un module Ansible accompagné de paramètres. Un module est un script Ansible en vue de réaliser certaines actions sur les “hôtes”.

Un module est généré sur le contrôleur, il est copié, exécuté et effacé sur les noeud (dans le cadre de l’automation des serveurs). Par contre , quand il s’agit de gérer des périphériques réseau, un module est généré et exécuté localement sur le contrôleur.

En composant un “livre de jeu” avec plusieurs “jeux” sous forme de liste, il est possible d’orchestrer un déploiement multi-machines, en exécutant certaines tâches dans un groupe de routeurs, certaines tâches dans un groupe de commutateurs, et encore d’autres commandes dans un autre groupe de périphériques …

2. Aperçu d’un livre de jeu Ansible

En résumé, un “livre de jeu” consiste à :

  • exécuter des “tâches (tasks)” pour un “inventaire (inventory)” d’”hôtes (hosts)” rassemblés en “groupes”,
  • utilisant certains modules,
  • utilisant ou peuplant certaines variables,
  • notamment, dans des modèles (templates) de fichiers.

Un “livre de jeu” organise des tâches. Mais il de bonne pratique de l’organiser en “roles”. Un “role” ajoute un niveau d’abstraction dans l’exécution d’un livre de jeu.

3. Programme ansible-playbook

ansible-playbook est le programme qui permet de lancer des livres de jeu. Avec tous les paramètres par défaut de connexion et d’inventaire définis, il demande juste le ou les livres de jeu à lancer. De manière non-exhaustive, on manquera pas d’évoquer et de tester quelques options utiles et courantes :

OptionsDescription
-iPrécise le fichier ou le script dynamique d’inventaire
-eAttend une variable définie sous la forme clé="valeur"
-tExécute uniquement les tâches en liste marquées d’un “tag”, une balise, une étiquette.
--skip-tagsEvite des “tags”
-lLimite une liste d’hôtes ou de groupes d’un inventaire sur lesquels le livre de jeu est appliqué.
-v, -vvvDéfinit le niveau de verbosité : utile au déboggage.

On trouvera des options informatives, j’entends ici qui n’agissent pas sur les cibles et qui informent du livre de jeu invoqué :

OptionsDescription
--syntax-checkVérifie la syntaxe du livre de jeu sans l’éxécuter.
--list-hostsFournit la liste des hôtes concernés par le livre de jeu sans l’éxécuter.
--list-tagsFournit la liste des balises (“tags”) dans le livre de jeu sans l’éxécuter.
--list-tasksFournit la liste des tâches qui devraient être exécutées par un livre de jeu.

Options d’éxécution/déboggage

OptionsDescription
-C, --checkCette option essaie de prédire certains changement sans les exécuter.
-D, --diffAffiche les changements des (petits) fichiers et modèles. Utile avec l’option --check.
--step“One-step-at-a-time” : chaque tâche est confirmée avant d’être exécutée.

Options de connexion :

OptionsDescription
-f FORKSDéfinit le nombre de processus parallèles.
-k, --ask-passDemande le mot de passe de connexion.
--key-file=PRIVATE_KEY_FILEPrécise la clé privée à utiliser.
-uPrécise le nom d’utilisateur pour la connexion.
-cPrécise le type de connexion à utiliser.

Options d’élévation de privilèges

-b, --become | Indique l’usage de l’élévation de privilège (su, sudo, …). -K, --ask-become-pass | Active la demande mot de passe avec l’élévation de privilège.

Tous les livres de jeu qui suivent dans les exemples sont fonctionnels et peuvent être joués par le programme ansible-playbook.

4. Liste de jeux

Un livre de jeux (playbook) est un fichier YAML constitué d’une liste de jeux. Chaque jeu comporte des sections qui définissent de manière obligatoire sa portée en désignant les hôtes ou les groupes d’un inventaire (hosts:). De manière optionnelle, mais probablement recommandée, on trouvera au niveau du jeu des paramètres de connexion, des paramètres d’élévation de privilèges, des variables définies et, aussi heureusement, différentes actions qui comportent des listes de tâches (tasks:, roles:, handlers:, etc.).

Un premier exemple de livre de jeu demo/empty-playbook.yaml pourrait être celui-ci :

---
#empty-playbook.yaml
- name: "PLAY 1"
  hosts: localhost
  tasks:
- name: "PLAY 2"
  hosts: localhost
  tasks:
- name: "PLAY 3"
  hosts: localhost
  tasks:

Ce livre de jeu porte sur l’hôte localhost soit dans notre cas sur le noeud de contrôle. localhost peut être sollicité par des tâches Ansible même s’il n’est pas précisé dans l’inventaire. Ansible m’en informe avec des messages d’avertissement :

~/workspace/demo $ ansible-playbook empty-playbook.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

5. Structure d’un jeu

En effet, un jeu dispose toujours au minimum de la clé hosts: qui désigne un hôte ou un groupe de l’inventaire, comme par exemple dans le fichier demo/minimal-playbook.yaml :

---
#minimal-playbook.yaml
- hosts: localhost

Il est toujours conseillé voir fortement recommandé de nommer ses objets jeu, tâche, rôle, … avec la directive name:.

Dans l’exemple suivant demo/helloworld-playbook.yaml, on trouve un livre de jeu “Hello World” d’un seul jeu avec une seule tâche qui affiche le message “Hello World!” :

---
#helloworld-playbook.yaml
- name: "helloworld play"
  hosts: localhost
  tasks:
    - name: "Hello World Task"
      debug:
        msg: "Hello World!"

Mais on pourrait aussi trouver d’autres clés, je le rappelle, qu’il conviendrait de définir dans le jeu selon les cas :

  • des paramètres de connexion (connexion:),
  • des variables (voir plus bas), l’activation des facts (gather_facts: False),
  • des listes d’action de type pre_tasks:, post_tasks:, handlers:, roles:,
  • des paramètres d’élévation de privilèges (become: True, become_user:), etc …

Voici une image de synthèse pour ceux qui préfèrent :

Livre de jeu Hello World

6. Valeurs de retour

Ansible génère des valeurs de retour à la suite de l’exécution des tâches et des modules.

Valeurs de retour, Return Values

Valeurs de retourSignication de la valeurs de retour
backup_file (fichier_de_sauvegarde)Pour les modules qui implémentent backup=no
changed (modifié)Un booléen indiquant si la tâche a dû effectuer des modifications.
failed (échoué)Un booléen qui indique si la tâche a échoué ou non.
invocationInformations sur la manière dont le module a été invoqué.
msgUne chaîne avec un message générique relayé à l’utilisateur.
rcCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter des commandes directement (raw, shell, commande, etc), ce champ contient le ‘code de retour’ de ces utilitaires.
resultsSi cette clé existe, elle indique qu’une boucle était présente pour la tâche et qu’elle contient une liste du module normal’résultat’ par élément.
skipped (évité)Un booléen qui indique si la tâche a été ignorée ou non.
stderrCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter des commandes directement (raw, shell, commande, etc), ce champ contient la sortie d’erreur de ces utilitaires.
stderr_linesLorsque stderr est retourné, nous fournissons aussi toujours ce champ qui est une liste de chaînes de caractères, un élément par ligne de l’original.
stdoutCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter directement des commandes (raw, shell, commande, etc). Cette zone contient l’édition normale de ces utilitaires.
stdout_linesLorsque stdout est retourné, Ansible fournit toujours une liste de chaînes de caractères, chacune contenant un élément par ligne de la sortie originale.

7. Variables

Il est de bonne pratique d’organiser ses procédures avec des variables. Les variables Ansible peuvent être déclarées à différents endroits : dans l’inventaire, dans les dossiers group_vars/ ou host_vars/ par exemple.

On peut aussi les définir sur la ligne de commande d’ansible-playbook -e comme argument supplémentaire et prépondérant, mais aussi dans le livre de jeu sous des forme diverses :

  • en valorisation directe dans la tâche ou dans le jeu ou encore dans le rôle (vars:)
  • en référence à un fichier (vars_files:)
  • en les “incluant” (include_vars:)1
  • en appelant des variables d’environnement
  • en important d’autres livres de jeu (import_playbook:)
  • par génération ou récolte dynamique (facter, gather_facts:)
  • définies par des tâches du livre de jeu (set_facts:)
  • définies à partir de la sortie standard d’une tâche (register:)
  • définies à partir d’invites interactives (vars_prompt:)
  • A partir des dossiers default/ ou variables/ d’un rôle

8. Appel aux variables

Les variables sont appelées en format Jinja2 sous la forme : {{ interface }} ou encore {{ ipv4.address }} quand on dispose d’une présentation de données comme par exemple dans le fichier demo/variables.yaml :

---
#variables.yaml
interface: eth0
ipv4:
  address: 192.168.1.1
ipv6:
  address:
    - fe80::1
    - fe80::11
    - fe80::111

Les guillemets sont nécessaires dans l’appel aux variables dans les paramètres car on appelle sa valeur.

Par contre dans les opérations logiques telles que les conditions cette mise entre guillemets n’est pas nécessaire.

Ansible est capable de récupérer des métadonnées sous forme de variables “dynamiques” ansible_*, aussi on peut aller chercher des variables d’autres hôtes de l’inventaire (hostvars, groups, group_names et inventory_hostname)

Enfin les modèles Jinja2 permet des traitement conditionnels, des boucles, des filtres …

Le livre de jeu suivant demo/variables-playbook.yaml démontre l’utilisation de variables dans le livre de jeu lui-même (vars:), venant d’un fichier (vars_files:), d’une interaction utilisateur (vars_prompt:) et de la collecte des “Facts” (gather_facts: True).

---
#variables-playbook.yaml
- name: "PLAY 1: variables playbook"
  hosts: localhost
  gather_facts: True
  vars:
    variable: a new value
  vars_files:
    - variables.yaml
  vars_prompt:
    name: response
    prompt: "What do to want to do?"
    private: no
  tasks:
    - name: "print variables"
      debug:
        msg:
          - "variable value: {{ variable }},"
          - "prompt value: {{ response }},"
          - "interface {{ interface }}: {{ ipv4.address }},"
          - "real hostname: {{ ansible_hostname }}"
          - "inventory hostname: {{ inventory_hostname }}"

On abordera ultérieurement dans le support de formation la manipulation des variables.

Notons aussi que Ansible-Vault permet de protéger ses variables. La protection des données sensibles avec ansible-vault est abordée plus loin dans le support de formation.

9. Appels à des tâches

On trouvera dans le livre de jeu demo/tasks-playbook.yaml suivant des actions pre_tasks:, roles:, tasks:, post_tasks:, handlers:, include_tasks: qui peuvent contenir des listes de tâches.

---
#tasks-playbook.yaml
- name: "PLAY 1: tasks playbook"
  hosts: localhost
  connection: local
  pre_tasks:
  roles:
  tasks:
  post_tasks:
  handlers:
  include_tasks:

En toute logique, ce livre de jeu n’exécute aucune tâche.

10. Structure d’une tâche

Les tâches peuvent être directement appelées de manière séquentielle dans le livre de jeux selon la syntaxe formelle suivante (les modules présentés ici n’existent pas) :

- name: "Task 1 example"
  module_x:
    argument1: foo
    argument2: bar
- name: "Task 2 example"
  module_y:
    argument1: foo
    argument2: bar

Les tâches font appel à une seule action, c’est-à-dire à un module et ses arguments, comme par exemple dans le livre de jeu demo/playbook-with-simple-tasks.yaml :

---
#playbook-with-simple-tasks.yaml
- name: "PLAY 1: play with simple tasks"
  hosts: localhost
  connection: local
  gather_facts: False
  vars:
    test: True
  pre_tasks:
    - name: "PRE-TASK 1: collect facts"
      setup:
  tasks:
    - name: "TASK 1: check connectivity"
      ping:
    - name: "TASK 2: print hostname by var arg"
      debug:
        var: ansible_hostname
    - name: "TASK 3: print hostname by msg arg"
      debug:
        msg: "{{ ansible_hostname }}"
  post_tasks:
    - name: "POST-TASK 1: Succeed"
      command: /bin/true
    - name: "POST-TASK 2: Error"
      command: /bin/false

Dans ce dernier livre de jeu, on retrouve les modules suivants avec l’usage d’arguments :

  • setup : qui collecte les “facts” sur l’hôte.
  • ping : qui fait un test de connectivité.
  • debug : qui affiche des messages sur la sortie standard.
  • command : qui exécute des commandes système.

11. Idempotence des tâches

“Être idempotent permet à une tâche définie d’être exécutée une seule fois ou des centaines de fois sans créer un effet contraire sur le système cible, ne provoquant un changement à une seule reprise. En d’autres mots, si un changement est nécessaire pour obtenir le système dans un état désiré, alors le changement est réalisé ; par contre si le périphérique est déjà dans l’état désiré, aucun changement n’intervient. Ce comportement est différent des pratiques de scripts personnalisés et de copier/coller de lignes de commandes. Quand on exécute les mêmes commandes ou scripts sur un même système de manière répétée, le taux d’erreur est souvent élevé.”

Extrait de: Jason Edelman. « Network Automation with Ansible. », O’Reilly Media, 2016.

Attention, Ansible autorise l’idempotence, mais selon le module utilisé, il faudra le manipuler pour atteindre cette exigence de conception qu’est l’idempotence.

12. Contrôle sur les tâches

Dans le livre de jeu suivant demo/playbook-with-tasks.yaml, on retrouve du contrôle sur les tâches :

---
#playbook-with-tasks.yaml
- name: "PLAY 1: playbook with tasks"
  hosts: localhost
  connection: local
  gather_facts: False
  vars:
    test: True
  pre_tasks:
    - name: "PRE-TASK 1: collect facts"
      setup:
  tasks:
    - name: "TASK 1: check connectivity"
      ping:
      register: output
    - name: "TASK 2: print variable output"
      debug:
        var: output
      when: test == True
    - name: "TASK 3: print hostname"
      debug:
        msg: "{{ ansible_hostname }} is {{ output.ping }}ing"
  post_tasks:
    - name: "POST-TASK 1: Error handeling and idempotency"
      command: /bin/false
      register: output
      ignore_errors: True
      changed_when: output.rc == 0

Une tâche peut être contrôlée de différentes manières, dans cette exemple :

  • La clé register: enregistre la sortie d’une tâche dans une variable.
  • La clé when: place des conditions à l’exécution de la tâche.
  • La clé ignore_errors n’interrompt pas l’exécution du livre de jeu en cas d’erreur.
  • La clé changed_when modifiera la valeur de sortie changed sous condition.

13. Inclusion de tâches

Si toutes les tâches étaient placées dans un fichier tasks.yaml, on pourrait parfaitement l’appeler du livre de jeux comme dans cet exemple demo/playbook-with-tasks-included.yaml :

---
#playbook-with-tasks-included.yaml
- name: "PLAY 1: playbook with tasks included"
  hosts: localhost
  connection: local
  gather_facts: True
  vars:
    test: True
  tasks:
  - name: Include task list in play
    include_tasks: some-tasks.yaml

Ce dernier livre de jeu appelle le fichier de tâches demo/some-tasks.yaml :

---
#some-tasks.yaml
- name: "PRE-TASK 1: check connectivity"
  ping:
  register: output
- name: "TASK 1: print variable output"
  debug:
    var: output
  when: test == True
- name: "TASK 2: print hostname"
  debug:
    msg: "{{ ansible_hostname }} is {{ output.ping }}ing"
  when: test == True
- name: "POST-TASK 1: Error handeling and idempotency"
  command: /bin/false
  register: output
  ignore_errors: True
  changed_when: output.rc == 0

14. Gestionnaire (Handler)

Un “handler” (gestionnaire) est une tâche qui ne se sera réalisée qu’une seule fois à la suite d’un appel notify: associé à une tâche si la tâche rend une valeur de retour ‘changed’. Par exemple avec le livre de jeu demo/playbook-with-handler.yaml

---
- name: "PLAY 1: playbook with handler"
  hosts: localhost
  connection: local
  gather_facts: False
  vars_prompt:
    - name: "response"
      prompt: "Do you want execute the task?\n1- Yes\n2- no\n"
      private: no
  tasks:
    - name: "TASK 1: always true"
      command: "true"
      notify: print state
      when: response == "1"
    - name: "TASK 1: always true"
      debug:
        msg: "Goodbye"
      when: response == "2"
  handlers:
    - name: "print state"
      debug:
        msg: "CHANGED !!!"