564 lines
18 KiB
Markdown
564 lines
18 KiB
Markdown
\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](https://puppet.com/), [Chef](https://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](https://www.yaml.org/spec/1.2/spec.html), 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](https://www.ansible.com/products/tower),
|
||
[AWX](https://github.com/ansible/awx) ou [Semaphore](https://github.com/ansible-semaphore/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](https://docs.ansible.com/ansible/latest/intro_installation.html).
|
||
|
||
|
||
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](https://gohugo.io/), sur la [machine exécutant
|
||
Ansible](https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html#delegating-tasks)
|
||
(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`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/command_module.html#ansible-collections-ansible-builtin-command-module).
|
||
|
||
:::::
|
||
|
||
|
||
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 :
|
||
|
||
<div lang="en-US">
|
||
```yaml
|
||
all:
|
||
hosts:
|
||
192.168.0.106
|
||
```
|
||
</div>
|
||
|
||
::::: {.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.](https://docs.ansible.com/ansible/latest/inventory_guide/index.html)
|
||
|
||
### `ping`
|
||
|
||
Lançons maintenant la commande suivante :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
42sh$ ansible --inventory-file hosts all --user root --module-name ping
|
||
192.168.0.106 | SUCCESS => {
|
||
"changed": false,
|
||
"ping": "pong"
|
||
}
|
||
```
|
||
</div>
|
||
|
||
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 :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
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"
|
||
}
|
||
```
|
||
</div>
|
||
|
||
|
||
### 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`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html) :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
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
|
||
}
|
||
```
|
||
</div>
|
||
|
||
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 :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
42sh$ ansible --inventory-file hosts all --user root --module-name user --args "name=login_x uid=1042"
|
||
192.168.0.106 | SUCCESS => {
|
||
[...]
|
||
```
|
||
</div>
|
||
|
||
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`](https://docs.ansible.com/ansible/latest/collections/ansible/posix/authorized_key_module.html) :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
ansible --inventory-file hosts all --module-name authorized_key --args "user=login_x key='ssh-ed25519 AAAAC3NzaC1lZD...FD'"
|
||
```
|
||
</div>
|
||
|
||
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 :
|
||
|
||
- [`apt`](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/apt_module.html)
|
||
- [`user`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html)
|
||
- [`lineinfile`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/lineinfile_module.html)
|
||
|
||
:::::
|
||
|
||
|
||
## 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) :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
ansible --inventory-file hosts all -m ping -u login_x --become --ask-become-pass
|
||
```
|
||
</div>
|
||
|
||
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 :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
ansible -i hosts all -m setup
|
||
```
|
||
</div>
|
||
|
||
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 :
|
||
|
||
<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 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
|
||
```
|
||
</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
|
||
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
|
||
```
|
||
</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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html)
|
||
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 :
|
||
|
||
- <https://linuxhandbook.com/ssh-hardening-tips/> ;
|
||
- <https://www.ssi.gouv.fr/guide/recommandations-pour-un-usage-securise-dopenssh/> ;
|
||
- <https://stribika.github.io/2015/01/04/secure-secure-shell.html>.
|
||
|
||
Pour modifier un fichier de configuration, on utilise généralement le module [`ansible.builtin.lineinfile`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/lineinfile_module.html).
|
||
|
||
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
|
||
|
||
<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](https://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 :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
My first nameserver is: {{ ansible_dns.nameservers[0] }}.
|
||
```
|
||
</div>
|
||
|
||
ou d'un dictionnaire lorsque la clef doit être récupérée d'une variable :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
My disks are: {% for disk in ansible_device_links.uuids %}
|
||
- {{ disk }}: {{ ansible_device_links.uuids[disk] }}, {{ ansible_device_links.ids[disk]|join(', ') }}
|
||
{% endfor %}
|
||
```
|
||
</div>
|
||
|
||
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](https://docs.ansible.com/ansible/latest/playbooks_conditionals.html).
|