Loading search data...

Ansible - Ecrire des rôles pour factoriser votre code

Publié le : 17 septembre 2022 | Mis à jour le : 27 juin 2023

logo ansible

Pour éviter de réécrire toujours le même code dans vos playbooks Ansible, je vous conseille d’écrire des rôles. Les rôles Ansible permettent de créer l’équivalent des librairies que l’on rencontre sur les langages de programmation.

Introduction

Nous allons voir ici comment écrire des rôles ainsi que quelques recommandations pour faciliter leur utilisation. Pourquoi les écrire alors qu’il en existe des tonnes sur ansible-galaxy ? Tout simplement parce que je n’aime utiliser des programmes sans en maitriser le fonctionnement. Et parfois, certains ne respectent pas les bonnes pratiques et peuvent mettre en péril vos infrastructures, surtout si vous l’appliquez sur vos environnements de production. Mais aussi comment voulez-vous maitriser le fonctionnement d’une librairie alors que vous ne savez pas en écrire !

La structure des répertoires de rôles Ansible

Plutôt que de créer la structure d’un role Ansible manuellement, je vous conseille d’utiliser molecule. Molecule est un framework de développement de rôles Ansible.

Il permet surtout de faire du TDD (Test Driven Development): Le TDD est une méthode de développement qui consiste à construire par étape le code de votre application. En premier, on écrit un test qui doit échouer tant que notre tâche Ansible n’est pas exécuté. Ensuite, on écrit la tâche permettant de résoudre cet échec. Cela permet d’écrire tous les tests sans en oublier un seul.

Ensuite molecule permet de provisionner automatiquement des environnements de tests avec l’outil de virtualisation de votre choix. Pour son installation référer au lien ci-dessus.

molecule init role --driver-name docker --verifier-name ansible steph.monrole
cd monrole
tree .
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── converge.yml
│       ├── molecule.yml
│       └── verify.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

Les différents dossiers :

  • defaults : les variables par défaut du rôle
  • files : Les fichiers sources qui seront copiés tels quel sur vos serveurs cibles.
  • handlers : Déclaration des handlers de votre role
  • meta : La description du rôle, indispensable si vous souhaitez soumettre votre role à Ansible Galaxy.
  • molecule : Le framework de tests
  • tasks : Les taches de votre rôles
  • templates: Les templates Jinja
  • tests :
  • vars : les variables du role

Dans certains de ses dossiers contient au moins un fichier main.yml et chacun possède sa structure yaml propre.

Vous avez remarqué il y a deux répertoires pour les variables : vars et defaults. Alors se pose la question que mettre dedans ? :

  • Celles qui sont déclarées dans defaults ont la priorité la plus faible et contiennent généralement des valeurs par défaut définies par les auteurs du rôle. Ce sont celles qui pourront être surchargées lors de l’utilisation de votre rôle dans vos playbooks. Exemple la version du package de cotre serveur http.
  • Celles qui sont dans le répertoire vars sont donc les autres. Celles qui ne peuvent pas être modifié par l’utilisateur de vos rôles.

Le fichier README.md est la documentation du rôle. Il doit indiquer comment installer et utiliser celui-ci. Il doit également décrire toutes les variables permettant de le configurer. Afin de vous simplifier cette tâche, j’ai écrit un outil répondant au nom de ansible-gendoc.

Passons à la pratique

Écrivons un role chargé de créer un ou plusieurs user(s) sur nos serveurs !

molecule init role --driver-name docker --verifier-name ansible steph.users

Notre rôle créé des users, donc users est une variable et comme il doit pouvoir être modifié lors de l’appel de votre role dans un de vos playbooks, nous le plaçons dans defaults/main.yml. Ce qui donne :

---
# defaults file for monrole
users: []

Vous avez remarqué, j’utilise une liste, car oui notre rôle doit pouvoir créer plusieurs utilisateurs. Par défaut cette liste est vide.

Que dois-je faire maintenant ? Écrire un test, vérifié qu’il échoue et ensuite écrire le code permettant de résoudre cet échec. Notre premier test est l’utilisateur admuser existe-t-il sur notre serveur.

Éditons le fichier molecule/default/verify.yml dont voici son contenu par défaut :

- name: Verify
  hosts: all
  gather_facts: false
  tasks:
  - name: Example assertion
    ansible.builtin.assert:
      that: true

Mettez en commentaire la première tache et ajoutez-en une permettant de vérifier que l’utilisateur toto existe ?

Mais comment écrire ce test avec Ansible ? Eh bien, comme on le ferait normalement sauf que l’on va indiquer que la tache échoue également si son status est changed en plus de celui de failed.

---
# This is an example playbook to execute Ansible tests.

- name: Verify
  hosts: all
  gather_facts: false
  tasks:
  # - name: Example assertion
  #   ansible.builtin.assert:
  #     that: true
  - name: User exist
    ansible.builtin.user:
      name: "{{ item }}"
      state: present
    check_mode: true
    register: user
    failed_when: user is changed or user is failed
    with_items: "{{ users }}"

Pour éviter que le test ne change quoi que ce soit nous utilisons le mode dry_run avec check_mode à true.

Mais on doit indiquer notre liste d’utilisateurs quelque part non ? Oui dans le fichier molecule/default/converge.yml car c’est le playbook qui est exécuté et qui appel votre rôle par molecule. Vous voyez l’intérêt de molecule ! On teste son role directement sans instancier de serveur physique. Tout se passe dans une instance docker (ici). On lance le test !

molecule converge # on créé l'instance docker
...

molecule verify # on lance les tests

CRITICAL Ansible return code was 2

Notre test en bien sorti en échec. Maintenant, écrivons le code qui permet de résoudre cet échec.

On copie le code du test pour le mettre dans les taches du rôle. Sauf qu’on retire la ligne failed_when et register. Ca se passe dans le fichier tasks/main.yml :

---
# tasks file for monrole
- name: Create user
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
  with_items: "{{ users }}"

On relance :

molecule converge # on créé l'instance docker
...

molecule verify # on lance les tests

INFO     Verifier completed successfully.

Vous avez compris le principe. Allez à présent, essayons de tester que l’utilisateur a comme shell /bin/zsh. Il faut changer la structure de la variable users dans le fichier molecule/default/verify.yml :

---
# This is an example playbook to execute Ansible tests.
- name: Verify
  hosts: all
  gather_facts: false
  vars:
    users:
      - name: toto
        shell: /bin/zsh

Il faut donc changer notre précédent test comme ceci :

  tasks:
  # - name: Example assertion
  #   ansible.builtin.assert:
  #     that: true
  - name: User exist
    ansible.builtin.user:
      name: "{{ item.name }}"
      state: present
    check_mode: true
    register: user
    failed_when: user is changed or user is failed
    with_items: "{{ users }}"

Faites de même dans le fichier molecule/default/converge.yml !

Relancez converge et verify avant d’écrire le test suivant. Ca passe, on écrit le test suivant. Comment contrôler avec ansible qu’user est configuré avec le shell zsh ? Tout simplement dans le fichier /etc/passwd :

toto:x:1000:1000::/home/toto:/bin/zsh

Nous allons donc utiliser le module lineinfile :

  - name: Check shell
    ansible.builtin.lineinfile:
      path: "/etc/passwd"
      regexp: '^{{ item.name }}(.*){{ item.shell }}$'
      line: '{{ item.name }}\1{{ item.shell }}'
      state: present
    check_mode: true
    register: shell
    failed_when: (shell is changed) or (shell is failed)
    with_items: "{{ users }}"

On relance le test, il échoue bien. Maintenant corrigeons notre task user dans le fichier tasks/main.yml:

---
# tasks file for monrole
- name: Create user
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    shell: "{{ item.shell }}"
  with_items: "{{ users }}"

On relance converge et verify et ça passe.

Je ne vais écrire tous les tests possibles, mais sachez que les modules les plus utiles sont services_facts pour charger l’état des services, le module assert et les facts bien sûr (module setup).

Par contre, bonne nouvelle depuis la version 2.11 d’Ansible nous ne sommes plus obligés de faire des tests sur les variables d’entrée. En effet, il existe un mécanisme automatique. Voyons cela maintenant.

Spécification d’arguments pour les rôles Ansible.

Pour activer ce mécanisme, il faut créer un fichier meta/argument_specs.yml Dans ce fichier, nous allons indiquer pour chaque variable d’entrée de notre role ce qui est attendu. C’est-à-dire le type, la valeur par défaut, sa description, la liste des valeurs possibles…

Reprenons notre exemple ci-dessus. Notre variable d’entrée est une liste de dictionnaires requis :

---
argument_specs:
  main:
    short_description: "Create Users on the servers."
    description:
      - Create an configure a list of users on the servers.
    author: Stephane ROBERT

    options:
      users:
        type: list
        required: true
        description:
          - List of users
        elements: dict
        options:
          name:
            description:
              - The username
            type: str
            required: true
          shell:
            description:
              - The usr shell
            type: str
            default: /bin/bash
            choices:
              - /bin/bash
              - /bin/zsh

Ici, nous décrivons les variables en entrée de la tache main.yml

Si nous lançons molecule converge nous allons voir une tache supplémentaire apparaître :

TASK [steph.monrole : Validating arguments against arg spec 'main' - Create Users on the servers.] ***
ok: [instance]

Cette tâche vérifie que les données en entrées sont celles attendues. Essayez de modifier les choix possibles de shell en retirant zsh et relancez :

fatal: [instance]: FAILED! => {"argument_errors": ["value of shell must be one of: /bin/bash, got: /bin/zsh found in users"], "argument_spec_data": {"users": {"description": "List of users.", "elements": "dict", "options": {"name": {"description": "The username.", "required": true, "type": "str"}, "shell": {"choices": ["/bin/bash"], "default": "/bin/bash", "description": "The username.", "type": "str"}}, "required": true, "type": "list"}}, "changed": false, "msg": "Validation of arguments failed:\nvalue of shell must be one of: /bin/bash, got: /bin/zsh found in users", "validate_args_context": {"argument_spec_name": "main", "name": "steph.monrole", "path": "/tmp/monrole", "type": "role"}}

Trop fort. N’est-ce pas ? Vous imaginez le nombre d’assert à taper pour valider cette structure, alors avec la structure du role nginx_config je ne vous raconte pas. Elle fait un millier de lignes.

Autre chose, grâce à ce système la documentation des rôles peut être obtenu avec la commande ansible-doc.

ansible-doc --type role --list

monrole                                     main         Create Users on the servers.
sensu.sensu_go.agent                        configure    Configure Sensu Go agent
sensu.sensu_go.agent                        start        Start Sensu Go agent
sensu.sensu_go.agent                        main         Install, configure, and start Sensu Go agent
sensu.sensu_go.backend                      configure    Configure Sensu Go backend
sensu.sensu_go.backend                      start        Start Sensu Go backend
sensu.sensu_go.backend                      main         Install, configure, and start Sensu Go backend
sensu.sensu_go.install                      repositories Enable Sensu Go repos
sensu.sensu_go.install                      packages     Install selected Sensu Go packages
sensu.sensu_go.install                      main         Enable Sensu Go repos and install selected packages

Si nous voulons afficher la documentation du fichier main de notre role :

ansible-doc --type role --entry-point main monrole
> MONROLE    (/home/vagrant/roles/monrole)

ENTRY POINT: main - Create Users on the servers.

        Create an configure a list of users on the servers.

OPTIONS (= is mandatory):

= users
        List of users.

        elements: dict
        type: list

        OPTIONS:

        = name
            The username.

            type: str

        - shell
            The username.
            (Choices: /bin/bash, /bin/zsh)[Default: /bin/bash]
            type: str


AUTHOR: Stephane ROBERT

Ce n’est pas parfait, mais c’est un bon début non ? D’ailleurs ce mécanisme, je vais l’inclure dans mon outil de génération de documentation de rôles. Cela va permettre d’ajouter la description des variables, leur type, valeur par défaut…

Je ne vais pas documenter plus cette partie, car l’exemple que je vous ai écrit permet de construire des spécifications complexes de listes de dictionnaires. Tout est documenté ici. Un exemple de spécifications.

Publier vos rôles sur Ansible Galaxy

Ansible Galaxy est une registry où les développeurs de rôles, collections, plugins, modules peuvent partager leurs productions. C’est dans cette base que la commande ansible-galaxy va puiser lors de l’installation des rôles et collections.

Avant de publier un rôle sur Ansible Galaxy, il est important de compléter les informations du fichier meta/main.yml. C’est en sorte sa carte d’identité.

Le fichier meta/main.yml.

Voici le contenu minimal à fournir :

galaxy_info:
  author: Stephane ROBERT
  namespace: steph
  description: Mon premier role ansible
  company: Claranet
  license: MLP2
  min_ansible_version: 2.11
  platforms:
    - name: Debian
      versions:
        - 11
    - name: Ubuntu
      versions:
        - 22.04
  galaxy_tags:
    - user
    - system
dependencies: []

Certains champs sont explicites, donc je vais me limiter à l’explication de :

  • dependencies: Ce sont des dépendances d’autres rôles
  • platforms: la liste des plateformes prises en charge par votre role. Elles doivent correspondre à celles que vous avez déclarées dans vos tests molecule.
  • galaxy_tags: la liste des mots clés qui permettront de retrouver votre rôle sur le site Ansible-Galaxy.

La publication

Il est indispensable que le code source de votre rôle soit stocké dans un repository github. Une fois déposé, il suffit de se rendre sur le site Ansible-Galaxy, de vous connecter avec votre compte Github et d’autoriser ansible-galaxy à accéder à votre compte en lecture seul. Ensuite aller dans la section [My content] et cliquer sur [Add Content]. Puis d’importer votre role avec le bouton [import Role from Github].

Utilisation de vos rôles dans vos playbooks

Il est possible d’intégrer vos rôles de 3 façons différentes :

  • Le classique roles qui est prioritaire sur les tasks du playbook :
---
- hosts: webservers
  roles:
    - common
    - webservers
  tasks:

Pour ordonner l’exécution des rôles vous pouvez utiliser :

  • Un appel dynamique de role : include_role.
---
- hosts: webservers
  tasks:
    - name: Print a message
      ansible.builtin.debug:
        msg: "this task runs before the example role"

    - name: Include the example role
      include_role:
        name: example
      vars:
        dir: '/opt/a'
        app_port: 5000
  • Un appel statique de role : import_role.
---
- hosts: webservers
  tasks:
    - name: Print a message
      ansible.builtin.debug:
        msg: "this task runs before the example role"
    - name: Import the example role
      import_role:
        name: example

La différence entre les deux est que pour import_role le(s) role(s) sont parsés au démarrage du playbook alors que pour include_role cela est fait durant son exécution.

Quelques recommandations sur l’écriture des rôles

Pour rappel, seuls les playbooks peuvent être exécuté avec la commande ansible-playbook!

  • Un rôle doit être limité à une tâche précise. Par exemple l’installation de votre serveur http.
  • Un rôle doit pouvoir être exécuté de manière autonome.
  • Même si on peut mettre des rôles en dépendances d’un autre rôle, éviter de faire des rôles, de rôles, de rôles. À déboguer c’est l’horreur.
  • Tous les résultats de vos tâches de vos rôles doivent être testé.
  • Pensez à décrire les variables dans le fichier meta/argument_specs.yml. Gain ansible se charge de lancer les asserts correspondants.
  • Tous les packages et librairies que vous installez ou que vous utilisez dans votre role doivent être versionné. Utilisez pour cela des variables. Exemple : nginx_version: 1.18.0
  • Éviter de créer un role base ou common dans lequel on retrouve un gros mélange de taches qui devraient plutôt découper en plusieurs rôles.

Pour vous aider vous pouvez vous inspirer des rôles de gueerlinguy (la référence dans ce domaine, d’ailleurs je vous conseille la lecture de son livre ansible for devops.)

Plus loin

Voilà pour le moment. Je complèterai par la suite ce billet. Faites-moi vos remarques. Pour ceux qui veulent plus d’infos sur Ansible, mon blog regorge d’informations sur cet outil. Il suffit de se rendre sur ce billet

Mots clés :

devops ansible tutorials infra as code formation ansible

Si vous avez apprécié cet article de blog, vous pouvez m'encourager à produire plus de contenu en m'offrant un café sur  Ko-Fi. Vous pouvez aussi passer votre prochaine commande sur amazon, sans que cela ne vous coûte plus cher, via  ce lien . Vous pouvez aussi partager le lien sur twitter ou Linkedin via les boutons ci-dessous. Je vous remercie pour votre soutien.

Autres Articles


Commentaires: