Ansible : la structure de rôles qui rend tout reproductible
Un playbook par groupe, des rôles découpés par service, un pattern mode pour paramétrer leur comportement — voici la structure Ansible qui rend le home lab reproductible, lisible et extensible.
Dans l'article précédent, je décrivais l'approche IaC en deux couches et le rôle d'Ansible pour configurer les services. Entrons dans le détail de la structure qui rend cette configuration lisible, maintenable et extensible.
Un repo, une convention
Tout le code Ansible suit la même organisation. Un playbook par groupe de machines, des rôles découpés par service, et une convention unique pour les appeler. Une fois la structure comprise sur un service, elle est identique pour tous les autres.
ansible/
├── infra-core.yml
├── home-automation.yml
├── media-server.yml
├── web-server.yml
├── group_vars/
│ ├── all.yml
│ ├── infra-core.yml
│ └── ...
└── roles/
├── common/
│ ├── directory/
│ ├── docker/
│ ├── tools/
│ └── driver/
├── infra-core/
│ ├── iam/
│ ├── dns/
│ ├── proxy/
│ └── ...
└── home-automation/
├── domotic/
└── wap/
Deux niveaux de rôles
Les rôles sont organisés en deux catégories.
Les rôles communs (common/) sont partagés par tous les groupes de machines. Ils s'occupent des prérequis systématiques : créer l'arborescence de répertoires, installer et configurer Docker, créer les réseaux Docker, installer les outils de base, gérer les drivers (GPU notamment). Chaque nouveau conteneur LXC passe par ces rôles avant même que le premier service soit configuré.
Les rôles métier sont organisés par groupe (infra-core/, home-automation/, media-server/, web-server/), puis par service au sein de chaque groupe. Le rôle infra-core/iam gère Authentik, infra-core/dns gère AdGuard Home, home-automation/domotic gère Home Assistant et Frigate, etc.
Le pattern mode : un rôle, plusieurs comportements
C'est la convention centrale de toute l'architecture Ansible. Chaque tasks/main.yml contient une seule ligne :
- include_tasks: "{{ mode }}.yml"
Le comportement du rôle est entièrement déterminé par la variable mode passée à l'appel — et la convention va plus loin : le nom du fichier de tâches correspond exactement à la valeur de mode. Pas de table de correspondance à maintenir ailleurs, juste un nom de fichier à retrouver :
roles/infra-core/iam/tasks/
├── main.yml # - include_tasks: "{{ mode }}.yml"
├── base-directory.yml # appelé avec mode: base-directory
├── authentik-db.yml # appelé avec mode: authentik-db
├── authentik-server-worker.yml # appelé avec mode: authentik-server-worker
└── backup.yml # appelé avec mode: backup
Et le contenu d'un de ces fichiers reste de l'Ansible tout ce qu'il y a de plus classique :
# tasks/base-directory.yml
- name: IAM => Create iam directory
become: true
file:
path: "{{ directory_path }}/iam"
state: directory
owner: "{{ directory_owner }}"
group: "{{ directory_group }}"
mode: '0750'
Dans le playbook, ça donne une lecture très claire de ce qui s'exécute et dans quel ordre :
- { role: infra-core/iam, mode: base-directory, tags: iam }
- { role: infra-core/iam, mode: authentik-db, tags: iam }
- { role: infra-core/iam, mode: authentik-server-worker, tags: iam }
- { role: infra-core/iam, mode: backup, tags: [iam, backup] }
Les tags pour cibler sans tout rejouer
Chaque appel de rôle porte un ou plusieurs tags. Cela permet de ne rejouer qu'une partie du playbook sans toucher aux autres services. Redéployer uniquement Caddy sur infra-core, recréer uniquement la base de données d'Authentik, redéployer uniquement les scripts de sauvegardes, tout cela se fait sans modifier le playbook, juste en passant le bon tag à la commande :
ansible-playbook -i ansible/hosts ansible/infra-core.yml \
--vault-password-file ~/.vault --tags iam
Les variables : du global au spécifique
Les variables suivent une hiérarchie simple. group_vars/all.yml porte les valeurs partagées par toutes les machines :
# group_vars/all.yml
root_path: "/opt"
nas_ip: 192.168.1.100
lxc:
media_server_tvheadend_uid: 1001
media_server_conf_path: "/etc/pve/lxc/300.conf"
home_automation_conf_path: "/etc/pve/lxc/400.conf"
Chaque group_vars/<groupe>.yml surcharge ou complète ces valeurs pour son groupe. Et les vars/main.yml au sein de chaque rôle portent les variables propres au service, y compris, pour les services conteneurisés, leur adressage sur le réseau Docker dédié :
# roles/infra-core/iam/vars/main.yml
directory_path: "{{ root_path }}/{{ directory_name }}"
container_authentik_db:
name: authentik_db
image: postgres:14.19-alpine
default_network:
name: bridge_infra-core
ipv4: "192.168.255.10"
Les secrets, mots de passe, tokens, clés API, vivent dans ces mêmes fichiers, chiffrés avec ansible-vault :
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
34383163613663326639386661663738663833...
Ce que cette structure apporte au quotidien
Ajouter un service, c'est créer un répertoire de rôle avec ses fichiers de tâches, l'ajouter au playbook avec les bons modes et les bons tags, et c'est tout. La convention fait le reste. Pas de surprise, pas d'exception, pas de fichier fourre-tout qui grossit indéfiniment.
C'est aussi ce qui rend le repo lisible pour quelqu'un qui ne l'a pas écrit : la structure parle d'elle-même.