This repository has been archived on 2024-03-03. You can view files and clone it, but cannot push or open issues or pull requests.
adlin/tutorial/ansible/ansible.md

18 KiB

\newpage

Automatiser la configuration de son SI

Comme tout bon paresseux sys. admin qui se respecte, sans plus attendre, nous allons vouloir automatiser toutes les actions rébarbatives que nous avons faites jusque là. Comme de très nombreuses personnes sont passées par là avant nous, il existe un grand nombre de solutions pour gérer les configurations d'un parc de machines. Parmi les plus connues, citons : Puppet, Chef, SaltStack ou encore Ansible.

Introduction à Ansible

Ansible est une solution de gestion de configuration. Basé sur Python et YAML, sa principale particularité est de ne pas nécessiter de daemon sur les machines qu'il va gérer : tout se fait exclusivement via SSH, à partir de la machine d'un administrateur, possédant Ansible (ou bien d'un système de gestion de configuration tel qu'Ansible Tower, AWX ou Semaphore).

Son installation est très simple, car les dépendances sont minimes et l'outil n'a pas besoin de base de données pour fonctionner : tout va se faire à partir d'une arborescence de fichiers, qui sera gérée par Git.

Commençons par installer Ansible, sur une machine distincte de la machine virtuelle démarrée : la machine hôte sera parfaitement adaptée à cette tâche. Mais vous pouvez aussi choisir de l'installer dans une seconde machine virtuelle.

Consultez la procédure d'installation pour votre distribution ici.

Résultat attendu

À la fin de ce TP, vous devez être en mesure de déployer une nouvelle machine, identique à celle que vous avez configurée, à partir d'une ISO et d'un nouveau disque.

Vous devrez pour cela réaliser des playbooks :

  • basis.yml, accompagné de toutes ses dépendances : celui-ci doit faire les configurations minimales du système et des utilisateurs (le seul playbook qui se connecte à l'utilisateur root directement, retirez le maximum de chose de ce playbook qui pourraient aller dans le suivant).

  • securing.yml, accompagné de toutes ses dépendances : ce playbook doit s'occuper de toute l'installation de la machine pour qu'elle soit aussi sécurisée que possible.

  • vitrine.yml et ses dépendances : celui-ci doit permettre de déployer une page vitrine typique d'une entreprise. Cette page doit être accessible depuis votre domaine https://login-x.adlin2024.example.tld/.

  • name-server.yml et ses dépendances : celui-ci doit mettre en place le serveur DNS faisant autorité sur le domaine qui vous est concédé.

::::: {.warning}

Il est attendu que la vitrine soit générée avec Hugo, sur la machine exécutant Ansible (et non pas sur la machine où elle sera hébergée).

Pour réaliser cela, vous êtes autorisé à faire usage du module ansible.builtin.command.

:::::

Ma première commande

Inventaire

Comme il sera bientôt question de maintenir plusieurs machines, la première chose à faire consiste à les inventorier.

Commençons par créer un répertoire, qui contiendra l'ensemble de nos configurations à destination d'Ansible. Dans ce répertoire, créons un fichier d'inventaires hosts, contenant l'IP de notre machine :

```yaml all: hosts: 192.168.0.106 ```

::::: {.more}

Vous pouvez lancer une autre machine virtuelle à ce stade et ajouter son IP sur une nouvelle ligne du fichier hosts.

:::::

Plus tard, c'est dans ce fichier que vous pourrez créer des groupes de machines (pour, par exemple, regrouper les serveurs web, les bases de données, etc.) ou donner des caractéristiques spécifiques à vos machines.

Plus d'infos sur le fichier d'inventaire par ici.

ping

Lançons maintenant la commande suivante :

``` 42sh$ ansible --inventory-file hosts all --user root --module-name ping 192.168.0.106 | SUCCESS => { "changed": false, "ping": "pong" } ```

Nous demandons avec cette commande, qu'ansible utilise le fichier d'inventaire hosts ; all est un filtre qui nous permet d'indiquer les machines auxquelles on souhaite se restreindre (all, on va lancer le module sur toutes les machines de l'inventaire). L'option --user précise le nom de l'utilisateur que l'on veut utiliser pour la connexion SSH. Enfin nous avons les options du modules, et cela commence par le nom du module lui-même.

Vous devriez avoir un retour similaire à celui-ci, indiquant simplement que la connexion a bien été effectuée et que le nécessaire pour utiliser Ansible est bien installé sur la machine distance.\

::::: {.question}

Si votre clef SSH n'a pas été récupérée depuis le CRI, vous pouvez rajouter l'option --ask-pass, afin de pouvoir indiquer le mot de passe de connexion au compte, sinon vous obtiendrez une erreur plutôt incompréhensible.

:::::

En ajoutant une seconde machine dans notre inventaire, nous aurions quelque chose comme cela :

``` 42sh$ ansible --inventory-file hosts all --user root --module-name ping 192.168.0.106 | SUCCESS => { "changed": false, "ping": "pong" } 192.168.0.142 | SUCCESS => { "changed": false, "ping": "pong" } ```

Confort

Une des premières choses que nous devrions faire, est de nous créer un utilisateur puis d'installer notre clef SSH sur les machines, pour éviter d'avoir à taper le mot de passe à chaque test.

Pour créer notre utilisateur, nous pouvons utiliser le module user :

``` 42sh$ ansible --inventory-file hosts all --user root --module-name user --args "name=login_x uid=1042" 192.168.0.106 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "append": false, "changed": true, "comment": "", "group": 1001, "home": "/home/login_x", "move_home": false, "name": "login_x", "shell": "/bin/sh", "state": "present", "uid": 1042 } ```

On voit que le retour de notre commande est bien différent (ne serait-ce que la couleur du texte !). En particulier, on peut constater sur la première ligne de sortie standard CHANGED au lieu de SUCCESS précédemment.

En fait notre utilisateur vient d'être créé, Ansible rapporte qu'il a apporté une modification au système cible. Si l'on renvoie exactement la même commande :

``` 42sh$ ansible --inventory-file hosts all --user root --module-name user --args "name=login_x uid=1042" 192.168.0.106 | SUCCESS => { [...] ```

Aucun changement, l'utilisateur est déjà créé et possède ces attributs, Ansible rapporte qu'il n'y a pas eu d'altération.\

Maintenant que notre utilisateur est créé, nous pouvons lui ajouter notre clef SSH. Pour cela, on utilisera le module authorized_key :

``` ansible --inventory-file hosts all --module-name authorized_key --args "user=login_x key='ssh-ed25519 AAAAC3NzaC1lZD...FD'" ```

Notez que j'ai maintenant arrêté d'utilisé l'option --user root, puisque je peux me connecter avec l'utilisateur que j'ai créé. Sans préciser, le nom d'utilisateur de votre compte est utilisé pour établir les connexions.

::::: {.exercice}

Pour continuer, il va nous falloir installer sudo (en root), ajouter notre utilisateur au groupe sudo et éventuellement configurer le fichier sudoers pour accepter que l'on puisse lancer des commandes sans utiliser de mot de passe. Vous pourrez faire tout cela avec les modules :

:::::

Utiliser sudo

Après avoir installé et configuré sudo, voyons comment l'utiliser avec Ansible.

Ansible peut utiliser différents programme pour élever ses privilèges (pas seulement sudo) ; il va détecter de lui-même quel programme utiliser.

Il faudra cependant lui indiquer que l'on souhaite qu'il élève ses privilège juste après la connexion en tant que simple utilisateur. On ajoute pour cela les options --become et --ask-become-pass (utilisez --sudo et --ask-sudo-pass pour les vieilles versions) :

```bash ansible --inventory-file hosts all -m ping -u login_x --become --ask-become-pass ```

L'option --ask-become-pass vous demandera systématiquement le mot de passe sudoer, avant d'établir la connexion.

Maintenant que l'on sait se connecter proprement, essayons d'en apprendre un peu plus sur les modules.

Les modules

Les modules Ansible ont généralement deux missions distinctes :

  • récupérer des informations en allant les extraire sur chaque machine ;
  • pousser des nouvelles informations pour atteindre un état précis.

Lorsque les informations récupérées montrent que l'état est déjà atteint, aucune modification n'est faite sur la machine, on parle d'idempotence.

À noter cependant que lorsque l'on retire un état de notre recette, il est conservé tel quel sur la machine. Par exemple, si un utilisateur toto est créé suite à l'application d'une recette décrivant l'utilisateur toto ; si l'on supprime la description, l'utilisateur ne sera pas supprimé des machines sur lequel la recette aura été appliquée. Pour qu'il soit supprimé, il faut modifier la description pour signaler que cet utilisateur ne doit pas exister. À l'application de cette nouvelle recette, si un tel utilisateur est trouvé, il sera donc supprimé.

Collecte d'informations

Parmi les modules de base, le module setup permet de récupérer un grand nombre de caractéristiques de la machine distance, voyez plutôt :

```bash ansible -i hosts all -m setup ```

Les informations récupérées (quel que soit le module), sont ensuite accessibles dans des variables afin de permettre de compléter des modèles ou pour faire de l'exécution conditionnelle d'état (par exemple on pourra utiliser la variable ansible_distribution pour distinguer les gestionnaires de paquets à utiliser selon la distribution de la machine).

Ma première recette

Un livre de recettes (playbook dans le vocabulaire d'Ansible), regroupe les descriptions des états que l'on souhaite obtenir sur un groupe de machines données.

Par exemple, voici à quoi pourrait ressembler un tel recueil :

```yaml --- - hosts: webservers remote_user: root

tasks:

  • name: ensure nginx is at the latest version apt: name=nginx state=latest

  • name: write the apache config file template: src=/srv/httpd.j2 dest=/etc/httpd.conf

  • hosts: databases remote_user: root

    tasks:

    • name: ensure postgresql is at the latest version yum: name=postgresql state=latest
    • name: ensure that mysql is started service: name=mysqld state=started
</div>

On filtre d'abord sur les hôtes concernés par la configuration, on donne
ensuite des paramètres qui seront utilisées pour chaque tâche et enfin on
décrit les tâches.

Le guide de référence pour connaître toutes les syntaxes possibles est
disponible dans [la documentation
d'Ansible](https://docs.ansible.com/ansible/latest/playbooks.html).


### Exécution d'un *Playbook*

Placer le contenu dans un fichier YAML, par exemple `playbook.yml`, non loin du
fichier `hosts` créé à la section précédente, puis lancez :

<div lang="en-US">
```bash
ansible-playbook -i hosts playbook.yml

Coup de pouce

Voici à quoi ressemblerait votre premier playbook créant l'utilisateur adeline :

```yaml --- - hosts: all remote_user: root

tasks:

  • name: ensure adeline as an account ansible.builtin.user: name: adeline state: present shell: /bin/fish groups: sudo append: yes
</div>

::::: {.exercice}

À vous maintenant, à l'aide [des collections de modules à votre
disposition](https://docs.ansible.com/ansible/latest/collections/index.html)
de copier vos fichiers de configuration à leur place et avec les bons droits
(`authorized_keys`, `.bashrc`, ...). Installez également les paquets que vous
aviez installés à la main (client DHCP, `htop`, ...).

:::::


Les notifications
-----------------

Au sein d'un *playbook*, il est possible de définir des tâches particulières
(nommées *handlers* dans le vocabulaire Ansible), qui ne seront exécutées que
si un changement a eu lieu. Par exemple, nous pourrions déclarer une tâche de
redémarrage du serveur web, seulement lorsque sa configuration est mise à
jour.

Pour se faire, il faut ajouter un élément `notify` à sa tâche :

<div lang="en-US">
```yaml
- name: template configuration file
  template: src=template.j2 dest=/etc/httpd.conf
  notify:
    - restart cherokee

Puis, au même niveau que les tasks, on déclare nos handlers :

```yaml handlers: - name: restart cherokee service: name=cherokee state=restarted ```

Il est attendu que le nom de la notification corresponde à l'attribut name d'un handler.

Voir la documentation pour davantage de détails et d'exemples.

::::: {.exercice}

La configuration de votre serveur SSH laisse à désirer. Corriger les problèmes énoncés par ces guides et articles :

Pour modifier un fichier de configuration, on utilise généralement le module ansible.builtin.lineinfile.

Puis, mettez en place un handler pour relancer votre serveur SSH en cas de modification de sa configuration.

:::::

Les variables

Les variables peuvent provenir de très nombreux emplacements : définies spécifiquement pour l'hôte, chargées depuis des fichiers lié au groupe auquel appartient la machine, liées à la recette ou à la tâche en cours d'exécution, récupérée par un module (comme le module setup).

Définition

Dans l'inventaire

```yaml all: hosts: 192.168.0.106 vars: ntp_server: ntp.epitech.eu ```

Cet inventaire définit une variable ntp_server qui pourra ensuite être utilisée par une recette installant un serveur NTP. Le cadre d'utilisation de ce genre de variable est adapté lorsqu'un hôte ou un groupe d'hôte seulement partagent des caractéristiques, comme par exemple leur localisation ou datacenter de rattachement, etc.

Dans un playbook

De la même manière que dans l'inventaire, il est possible de définir des variables dans une recette :

```yaml - hosts: webservers vars: http_port: 80 ```

Ces variables peuvent être placées dans un fichier, pour plus de lisibilité :

```yaml - hosts: webservers vars_files: - vars/webservers_common.yml ```

Le format correspond au sous arbre que l'on pourrait trouver sous vars :

```yaml http_port: 80 https_port: 443 ```

Utilisation

Modèles

Ces variables peuvent être utilisées pour remplir un emplacement de texte dans un modèle. Ansible utilise Jinja2 comme moteur de template, un format très courant dans le milieu du développement Python.

``` I'm running {{ ansible_system }} {{ ansible_os_family }} on {{ ansible_system_vendor }}, with {{ ansible_memory_mb.swap.free }} MB Swap available. ```

Ce format est relativement générique. Ainsi, pour accéder aux éléments d'une table de hash/dictionnaire, on utilise le caractère . ; les crochets [x] sont utilisés principalement pour accéder aux éléments des tableaux :

``` My first nameserver is: {{ ansible_dns.nameservers[0] }}. ```

ou d'un dictionnaire lorsque la clef doit être récupérée d'une variable :

``` My disks are: {% for disk in ansible_device_links.uuids %} - {{ disk }}: {{ ansible_device_links.uuids[disk] }}, {{ ansible_device_links.ids[disk]|join(', ') }} {% endfor %} ```

Notez dans ce dernier exemple l'utilisation d'un filtre join permettant d'aplatir la liste retournée.

Dans les recettes

Au sein même de votre description YAML, vous pouvez utiliser la puissance de Jinja2 :

```yaml - hosts: webservers vars_files: - vars/webservers_common.yml - vars/webservers_{{ ansible_os_family }}.yml ```

Dans cet exemple, les variables des fichiers /vars/webservers_common.yml, puis celles de /vars/webservers_Debian.yml seront chargées dans le contexte.

Conditions

Une autre utilisation possible des variables est d'appliquer ou de passer des tâches. Par exemple :

```yaml tasks: - name: "pkg setup (Debian)" apt: name=chrony state=present when: ansible_os_family == "Debian" - name: "pkg setup (CentOS)" yum: name=chrony state=present when: ansible_os_family == "CentOS" ```

Vous trouverez bien d'autres cas d'utilisation dans la documentation.