Table des matières

2022/11/11 (atelier) : Ansible

Présences :

Météo

Moment informel durant lequel on exprime en peu de mots comment on se sent et si on a une attente forte pour la réunion. Ce n'est pas un moment de discussion mais d'expression individuelle et ce n'est pas obligatoire 🙂

Attente(s) forte(s)

Si l'une ou l'autre personne exprime une attente forte, merci de vous en occuper en priorité ou de la noter dans le hub ou dans un point approprié.

Création d'un rôle

Qu'est-ce qu'un rôle ?

Un rôle, dans Ansible, c'est en quelque sorte un paquet d'un gestionnaire de paquets : cela fait tout ce qu'il faut pour que ça soit installé. Que ce soit les fichiers de config, les variables par défaut, la liste des tâches ansible, les handlers propre au déploiement de l'application, etc.

Le playbook va appeler un ou plusieurs rôles. L'avantage c'est qu'on peut utiliser un rôle pour plusieurs choses. Par exemple, on a un rôle qui ne fait qu'installer nginx et qu'on mobilisera partout. Qui ne gérera que l'installation, et non la configuration du vhost qui sera propre à chaque cas, mais qui pourra être utilisé pour beaucoup d'applications différentes.

La configuration du vhost sera par exemple faite dans le rôle qui déploie l'application en elle-même (exemple: un rôle pour Mobilizon)

Un autre avantage, c'est de cloisonner. Si tout est dans le même playbook, cela ferait beaucoup de variable et sera difficilement lisible.

Quand on fait un rôle, on va toujours préfixer les variables du nom du rôle.

Par exemple, pour mobilizon :

mobilizon_domain au lieu de domain

Cela évite qu'un autre rôle utilise la même variable. C'est aussi plus lisible : on sait tout de suite de quelle variable il s'agit.

Différences entre module et rôle

La différence entre module (apt par exemple) et rôle est qu'un module est écrit en python, il est fourni par Ansible. Même si on peut écrire des modules, on le fait rarement car la plupart du temps ils existent déjà et il faut ensuite maintenir le code en python.

Par contre, pour coder correctement avec Ansible, on doit écrire des rôles, pour toutes les raisons qu'on a expliqué plus haut.

Structure d'un rôle

Dans le cours https://supports.uptime-formation.fr/06-ansible/cours3/, il y a une image qui montre la structure d'un rôle.

Quand on lance un playbook, Ansible va d'abord regarder les hosts puis va ensuite regarder les variables. Ensuite, il va regarder les rôles.

Le rôle common va s'appliquer à un groupe de serveurs (sur le schéma, il s'applique à tous). Il contient des tasks, des handlers, des templates, des files. Le rôle common, ça va être les configs de bases : ssh, etc.

Ensuite on a les rôles plus spécialisés, dans l'ordre de leur spécialisation. Par exemple :

Tous les rôles doivent être dans le dossier roles. Chaque rôle est un sous dossier :

roles/
    common/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            foo.sh        #  <-- script files for use with the script resource
        vars/             #
            main.yml      #  <-- variables associated with this role
        defaults/         #
            main.yml      #  <-- default lower priority variables for this role
        meta/             #
            main.yml      #  <-- role dependencies
        library/          # roles can also include custom modules
        module_utils/     # roles can also include custom module_utils
        lookup_plugins/

Pour chaque dossier, Ansible va regarder dans le fichier main.yml.

Pour les variables, on a default et vars. Vars sont les variables qu'on écrit, default les variables par défaut. Les variables par défaut sont modifiées dans de très rares cas (par exemple si on a besoin d'une version de php par défaut). Mais chez Neutrinet, dans ce cas, on préfère souvent hardcoder plutôt qu'utiliser les variables par défaut.

Le reste est moins important : meta permet de mettre des propriétés aux rôles, licence… C'est important si on met à disposition son rôle de tout le monde. Chez Neutrinet, on n'utilise Ansible qu'en interne donc on n'a pas besoin de s'en occuper.

Ansible Galaxy et rôles externes

https://galaxy.ansible.com/

Ansible Galaxy est un annuaire de rôles et de modules. C'est une mine d'or pour trouver des rôles sans trop se casser la tête.

Par contre, la plupart sont fait par des gens, et donc ne sont pas toujours maintenus. Il y a des étoiles et des nombres de téléchargement, donc on peut avoir une indication.

En général, on va regarder ce qui existe déjà comme rôle avant de créer un nouveau, ceci pour ne pas réinventer la roue. Mais la plupart du temps, les rôles proposés sont trop génériques ou trop spécifiques et impose une certaine structure pour le déploiement de l'application.

On les réécrit donc généralement pour les mettre à notre sauce.

Conversion du playbook précédent vers un rôle

On avait ceci comme structure la dernière fois:

├── ansible.cfg
├── inventories
│   ├── group_vars
│   │   └── all.yml
│   ├── hosts.ini
│   └── host_vars
│       ├── debian1.yml
│       └── debian2.yml
├── playbooks
│   ├── files
│   ├── playbook1.yml
│   └── templates
│       └── nginx.conf.j2
└── Vagrantfile

On va rajouter notre rôle:

├── ansible.cfg
├── inventories
│   ├── group_vars
│   │   └── all.yml
│   ├── hosts.ini
│   └── host_vars
│       ├── debian1.yml
│       └── debian2.yml
├── playbooks
│   ├── files
│   ├── playbook1.yml
│   └── templates
│       └── nginx.conf.j2
├── roles
│   └── nginx
│       ├── defaults
│       │   └── main.yml
│       ├── handlers
│       │   └── main.yml
│       ├── tasks
│       │   └── main.yml
│       └── templates
│           └── nginx.conf.j2
└── Vagrantfile

On ne met que la liste des tâches dans roles/nginx/tasks/main.yml, la liste des handlers dans roles/nginx/handlers/main.yml, et le dictionnaire contenant les variables (ici juste la variable domains) dans roles/nginx/defaults/main.yml.

On enlève l'en-tête task ou handler, et on enlève l'indentation.

On copie le template de la config nginx dans roles/nginx/templates/nginx.conf.j2.

On renomme la variable domains en nginx_domains pour respecter les bonnes pratiques. On le fait dans les variables par défaut (defaults) et dans la tâche qui configure nginx.

Pour l'instant on a notre rôle, mais on ne peut pas l'exécuter. Pour ce faire, il nous faut l'inclure dans un playbook. On va reprendre notre playbook et le simplifier.

On enlève la partie tasks, vars, et handlers, et on ajoute (attention à l'indentation!):

snippet.yaml
  roles:
    - nginx

Pourquoi c'est si simple ? Parce que Ansible sait où aller chercher les rôles (on peut le définir dans ansible.cfg, mais par défaut c'est le dossier roles au même niveau que là où on exécute la commande ansible-playbook). Donc il va chercher dans le dossier roles/nginx et puis le fichier de tasks tasks/main.yml de ce rôle-là.

Dans le cas de nginx, cela a peu d'intérêt, mais on peut séparer la partie installation et la partie configuration dans deux fichiers séparés : install.yml et config.yml.

Ensuite, dans main.yml, on utilise le module import_tasks avec le lien vers le fichier.

Dans roles/nginx/tasks/main.yml, on a donc:

snippet.yaml
- name: Installation de nginx
  import_tasks: install.yml
  
- name: Configuration de nginx
  import_tasks: config.yml

Dans roles/nginx/tasks/install.yml:

snippet.yaml
- name: Installation de nginx
  package:
    name: nginx
    state: present
    update_cache: true

Et dans roles/nginx/tasks/config.yml:

snippet.yaml
- name: Configuration de nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/{{ domain }}
    owner: root
    group: root
    mode: "0644" # équivalent à "u=rw,g=r,o=r"
    #        validate: nginx -T -c %s
  notify: Restart nginx
  loop: "{{ domains }}"
  loop_control:
    loop_var: domain

À présent, on va lancer le playbook… mais visiblement il ne trouve pas le rôle 😕

Dans le fichier ansible.cfg, dans la section defaults, on rajoute ceci:

snippet.ini
roles_path=./roles

Lors de l'exécution du playbook, Ansible préfixe les tâches avec nginx : pour indiquer qu'il travaille dans le rôle nginx.

Différence entre import et include

L'import est statique. En informatique, cela veut dire que c'est fait avant l'exécution du playbook et plus jamais après - c'est plus optimisé.

Les include sont dynamiques.

Dans la tâche config, on fait une boucle.

On pourrait aussi la faire au niveau de main.yml, dans la partie import des tâches de la configuration de nginx. On importerait alors la config une fois par domaine. Cela permet de simplifier la config dans config.yml. Cela permet aussi d'avoir un ensemble de tâche qui seront appliquées à chaque domaine.

Au final, on déplace ce bout de code qui était dans le fichier config.yml vers main.yml en dessous de l'import de config.yml:

snippet.yaml
  loop: "{{ nginx_domains }}"
  loop_control:
    loop_var: domain

On voit qu'on a une erreur, car les import_tasks sont statiques, or on ne peut utiliser une boucle qui est par définition dynamique ici. Il faut utiliser le include_tasks.

En gros, il faut retenir que si on n'est pas dans une boucle on va faire un import, et sinon ce sera un include.

Molecule

Pour l'explication de ce qu'est Molecule, voir le Neutriton du 12/06/2021

Installation de molecule

On va installer molecule:

snippet.bash
pip install molecule

On installe un plugin pour que molecule puisse gérer des VMs vagrant (c'est un provider):

snippet.bash
pip install molecule-vagrant

Création d'une VM via molecule

Molecule se configure toujours au niveau d'un rôle. Donc on va reprendre le dossiers roles qu'on avait créé, et on va rajouter des bouts de config pour molecule.

Pour faire les opérations molécules, on se place dans le dossier du rôle qu'on est en train d'écrire. Donc chez nous dans le dossier nginx.

On doit créer un dossier molecule/default. Ici default représente le scénario par défaut dans Molécule. On peut avoir plusieurs scénario, typiquement le scénario par défaut va créer une seule VM et appliquer le rôle ansible dessus. Mais on pourrait avoir un scénario cluster qui va générer trois VMs pour tester si tout fonctionne bien en mode cluster. On peut avoir un scénario debian et un autre archlinux qui vont tester le rôle sur les distributions.

Une fonction, molecule init, permet de faire la conf à notre place en proposant de choisir le nom du scénario, le type de driver (on utilisera vagrant plutôt que delegated).

> molecule init scenario -r nginx -d vagrant
INFO     Initializing new scenario default...
INFO     Initialized scenario in ./molecule/default successfully.

On voit que molecule crée différents fichiers:

├── molecule
│   └── default
│       ├── converge.yml
│       ├── INSTALL.rst
│       ├── molecule.yml
│       └── verify.yml

On va un peu modifier la configuration de molecule.yml :

snippet.yaml
dependency:
  name: galaxy
driver:
  name: vagrant
  provider:
    name: libvirt
platforms:
  - name: bullseye-nginx-molecule
    box: debian/bullseye64
    cpu: 2
    memory: 1024
provisioner:
  name: ansible
  config_options:
    defaults:
      interpreter_python: /usr/bin/python3
    ssh_connection:
      pipelining: true
verifier:
  name: ansible

Le driver permet d'indiquer si on utilise vagrant ou docker par exemple. Le provider indique qu'est-ce que vagrant va utiliser (libvirt, virtualbox, etc)

La partie platforms indique le nombre de VMs et quels types de VM. La box, c'est comme dans vagrant, cela indique quelle image utiliser.

La partie provisionner permet de configurer ansible (utile pour forcer la version de python)

La partie verifier c'est pour effectuer les tests, mais on ne l'utilise pas.

A partir de ces infos, molecule va générer un vagrantfile.

Pour créer la VM, c'est très compliqué:

snippet.bash
molecule create

Note: Il râle sur l'option pipelining: true donc on va commenter ça comme ce n'est pas nécessaire.

On voit bien que Molécule crée la VM default_bullseye-nginx-molecule 🙂

Molécule génère tout un tas de fichier dans le dossier suivant:

snippet.bash
ls -a ~/.cache/molecule/nginx/default

On voit par exemple qu'il a généré un fichier Vagrantfile.

On voit aussi qu'il y a un dossier .vagrant et si on creuse dedans, on retrouve la private key associée à la VM ! Ce sera utile si jamais on doit y accéder en ssh directement. Par exemple si on veut faire du port forwarding, cela peut être pratique.

snippet.bash
ls ~/.cache/molecule/nginx/default/.vagrant/machines/bullseye-nginx-molecule/libvirt    

Comment se connecter à la VM ?

À partir du dossier du rôle nginx, on lance:

snippet.bash
molecule login

Si on a plusieurs VMs, on devra utiliser:

snippet.bash
molecule login -h <nom de la machine>

Mais de toute façon, molecule l'explique quand c'est nécessaire.

C'est tout pareil qu'avec vagrant \o/

Destruction de la VM

C'est encore plus pareil qu'avec vagrant:

snippet.bash
molecule destroy

En cas de bug, on peut aussi effacer les fichiers du cache avec la commande reset. Mais normalement, destroy le fait aussi.

Comment exécuter son rôle avec molecule ?

Dans le dossier molécule, on a un fichier converge.yml. Il est correct tel quel mais on va le modifier pour que ce soit mieux :

snippet.yaml
- name: Converge
  hosts: all
  become: true
  
  roles:
    - nginx

On va lancer un molecule converge, ce qui va exécuter le rôle sur la VM. Si elle n'est pas créée, il va la créer pour nous 🙂

molecule converge fonctionne comme ansible-playbook, on peut rajouter des options, par exemple pour débugguer.

Par exemple:

snippet.bash
molecule converge -- -vvv

Mais en vrai toutes les options de ansible-playbook sont disponibles 🙂

Installer des dépendences avant d'exécuter le rôle

On va faire un nouveau rôle qui va simplement cloner un repo git, celui-ci: https://github.com/e-lie/flask_hello_ansible.git

Cependant, on part du principe que git est déjà installé sur le host ! On va voir comment faire pour que ça marche dans molecule.

On reprend la structure de notre précédent rôle, et on va modifier le fichier tasks/main.yml dans notre nouveau rôle.

On utilise le module git: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/git_module.html#

On ne garde que les dossiers molecule et tasks.

snippet.yaml
- name: Clonage du repo git
  ansible.builtin.git:
    repo: 'https://github.com/e-lie/flask_hello_ansible.git'
    dest: /opt/flask
    version: master

Dans molecule/default/converge.yml, on modifie le rôle par git (le nom du nouveau rôle).

Dans molecule/default/molecule.yml, on change le nom de la machine.

Ansible râle parce qu'il ne trouve pas le binaire git (surprenant!)

Pour cela, molécule à tout prévu… On va créer un nouveau fichier prepare.yml, dans lequel on va copier converge.yml et le modifier. Cela devient un autre playbook qui s'exécutera juste après la création de la machine, mais avant converge.yml 🙂

Surtout, il ne s'exécute qu'une seule fois (après la création de la VM), et plus jamais après. Ce qui permet d'alléger le converge.

C'est l'intérêt de mettre des informations comme l'installation des dépendance dans ce second playbook.

snippet.yaml
- name: Prepare
  hosts: all
  become: true
  
  tasks:
    - name: Installation de git
      package:
        name: git
        update_cache: true

On lance la commande molecule prepare, mais il nous dit que tout est déjà fait…

En effet, on a déjà créé les machines. Même si cela a échoué, ce n'est pas la première fois qu'on lance molecule et molecule le sait. Soit on fait un destroy, soit on force.

Et là on force:

snippet.bash
molecule prepare -f

Maintenant que tout est prêt, on peut faire le converge:

snippet.bash
molecule converge

Remarque pour le repo de Neutrinet

Là on a tout ce qu'il faut pour utiliser molecule.

Pour l'utiliser avec l'infra de Neutrinet, il faudra installer préalablement les requierments pour avoir la même version, dans un environnement virtuel évidemment 😛

snippet.bash
pip install -r requirements.txt

Secrets

Commande ansible-doc pour avoir la doc. → équivalent au man page

Keycloak

Prochaine réunion

Prochain atelier Ansible : À définir

Lieu : Chez Célo (Crainhem)

Météo de fin

Moment informel durant lequel on exprime en peu de mots comment, à titre personnel, la réunion a été vécue que ce soit positif ou négatif. Si une ou plusieurs tension est née durant la réunion, il est peut-être nécessaire d'envisager l'une ou l'autre réunion pour y remédier.