\newpage

Automatiser la configuration de son SI
=======================================

Aujourd'hui, vous n'avez qu'une seule machine à administrer, et déjà, j'ai pu
entendre des soupirs lorsque vous avez dû configurer ENCORE CE RÉSEAU DE
*$&*%#. Imaginez maintenant que vous ayez à en administrer 10, 100, 1000 ou
encore davantage !

Comme tout bon ~~paresseux~~ sys. admin qui se respecte, sans plus attendre,
vous allez vouloir automatiser toutes ces actions rébarbatives. Comme de très
nombreuses personnes sont passées par là avant vous, il existe un grand nombre
de solutions pour gérer les configurations d'un parc de machines. Parmi les
plus connues, citons : [Puppet](https://puppet.com/),
[Chef](http://www.chef.io/), [SaltStack](https://saltstack.com/) ou encore
[Ansible](https://www.ansible.com/).


Introduction à Ansible
----------------------

Ansible est une solution de gestion de configuration. Basé sur
[Python](https://www.python.org/) et
[YAML](http://www.yaml.org/spec/1.2/spec.html), sa principale particularité est
de ne pas nécessité 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.

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.

Commencez 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,
d'autant plus que l'installation de la plate-forme est propre et légère : vous
ne risquez pas de vous retrouver avec une usine à gaz impossible à retirer.

[Consultez la procédure d'installation pour votre distribution ici](http://docs.ansible.com/ansible/latest/intro_installation.html).


Résultat attendu
----------------

À la fin de cette partie, vous devez être en mesure de pouvoir déployer une
nouvelle machine, identique à celle que vous venez de configurer, à partir
d'une ISO et d'un nouveau disque.

Maintenant que vous savez vous connecter au réseau et formater un disque, vous
pouvez ajouter les options `adlin.format=/dev/sda` et `adlin.net=easy` à la
ligne de commande du noyau afin de, respectivement, formater le disque
`/dev/sda` si la partition racine n'est pas trouvée, et obtenir une adresse IP
par DHCP. Vous pourrez ainsi très facilement tester vos recettes.


Mon 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 :

<div lang="en-US">
```yaml
all:
  hosts:
      192.168.0.106
```
</div>

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
(par exemple pour regrouper les serveurs web, les bases de données, etc.) ou
donner des caractéristiques spécifiques à vos machines.


### `ping`

Lancez ensuite la commande suivante :

<div lang="en-US">
```
    42sh$ ansible --inventory-file hosts all --module-name ping --user root --ask-pass
	192.168.0.106 | SUCCESS => {
	    "changed": false,
		"ping": "pong"
	}
```
</div>

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


### Confort

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

Si vous avez bien suivi jusqu'ici, vous savez qu'il ne faut pas utiliser le
compte `root` directement ! Pas d'inquiétude, tout est prévu dans Ansible :
retirer l'option `--user root` si votre nom d'utilisateur local est identique
que celui dans la machine virtuelle, ou adaptez l'option en conséquence.

Et ajoutez les options `--become` et `--ask-become-pass` (utilisez `--sudo` et
`--ask-sudo-pass` pour les vieilles versions) :

<div lang="en-US">
```
    ansible --inventory-file hosts all -m ping -u bruce --become --ask-become-pass
```
</div>


### 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 du mercredi

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 :

<div lang="en-US">
```
    ansible -i hosts all -m setup
```
</div>

Les informations récupérées (quelque soit le module), sont ensuite accessible
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_facts.ansible_distribution` pour distinguer les gestionnaires de
paquets).


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 :

<div lang="en-US">
```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 postgresql is started
    service: name=postgresql 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](http://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édentes, puis lancez :

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


### Coup de pouce

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

<div lang="en-US">
```yaml
---
- hosts: all
  remote_user: root

  tasks:
  - name: ensure adeline as an account
    user:
	  name: adeline
	  state: present
	  shell: /bin/fish
	  groups: sudo
	  append: yes
```
</div>

La création de l'utilisateur se fait à l'aide du module
[`user`](http://docs.ansible.com/ansible/latest/user_module.html).

À vous maintenant, à l'aide [des modules à votre
disposition](http://docs.ansible.com/ansible/latest/modules_by_category.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é à 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 à 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
```
</div>

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

<div lang="en-US">
```yaml
    handlers:
      - name: restart cherokee
        service: name=cherokee state=restarted
```
</div>

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

Voir [la
documentation](http://docs.ansible.com/ansible/latest/playbooks_intro.html#handlers-running-operations-on-change)
pour davantage de détails et d'exemples.

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

- <https://ringyt.wordpress.com/2017/05/23/default-hardened-ssh-server-config/> ;
- <https://stribika.github.io/2015/01/04/secure-secure-shell.html>.

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

<div lang="en-US">
```yaml
    all:
      hosts:
        192.168.0.106
      vars:
        ntp_server: ntp.epitech.eu
```
</div>

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 :

<div lang="en-US">
```yaml
    - hosts: webservers
      vars:
        http_port: 80
```
</div>

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

<div lang="en-US">
```yaml
    - hosts: webservers
      vars_files:
        - /vars/webservers_common.yml
```
</div>

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

<div lang="en-US">
```yaml
    http_port: 80
    https_port: 443
```
</div>


### Utilisation

#### Modèles

Ces variables peuvent être utilisées pour remplir un emplacement de texte dans
un modèle. Ansible utilise [Jinja2](http://jinja.pocoo.org/) comme moteur de
template, un format très courant dans le milieu du développement Python.

<div lang="en-US">
```
    I'm running {{ ansible_system }} {{ ansible_os_family }} on {{ ansible_system_vendor }},
      with {{ ansible_memory_mb.swap.free }} MB Swap available.
```
</div>

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 :

<div lang="en-US">
```yaml
    - hosts: webservers
      vars_files:
        - /vars/webservers_common.yml
        - /vars/webservers_{{ ansible_os_family }}.yml
```
</div>

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 :

<div lang="en-US">
```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"
```
</div>

Vous trouverez bien d'autres cas d'utilisation dans [la
documentation](http://docs.ansible.com/ansible/latest/playbooks_conditionals.html).
