Chapitre 1523 minIntermédiaire
Versionner l'infrastructure : Git, Galaxy, ansible-lint et CI
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- organiser un dépôt d'infrastructure, son
.gitignoreet ses rôles Galaxy aux versions figées ; - faire passer chaque modification par une branche et une revue ;
- contrôler les playbooks avec
ansible-lint; - écrire un pipeline qui vérifie les playbooks à chaque push ;
- documenter l'automatisation pour le dossier de l'E6.
4 exercices corrigés
Versionner l'infrastructure, c'est ranger tout le projet Ansible (inventaire, playbooks, rôles, fichiers chiffrés) dans un dépôt Git, faire passer chaque modification par une branche, des contrôles automatiques et une relecture, puis l'appliquer depuis la branche principale. Ce chapitre organise le dépôt de Sorbier et ses dépendances venues d'Ansible Galaxy, contrôle les playbooks avec ansible-lint, écrit un pipeline GitHub Actions qui les vérifie à chaque modification, présente les façons de tester avant la production et dit comment documenter l'automatisation pour l'E6.
Le dépôt et son .gitignore
Le dépôt sorbier-infra contient tout ce qui décrit l'infrastructure, et rien de ce qui peut se reconstruire ou doit rester secret :
sorbier-infra/
├── .ansible-lint configuration d'ansible-lint
├── .github/workflows/
│ └── verifier.yml le pipeline de contrôle
├── .gitignore
├── README.md comment utiliser le dépôt
├── ansible.cfg
├── requirements.yml collections et rôles externes, versions figées
├── inventaire/
│ ├── hosts.yml
│ ├── group_vars/
│ └── host_vars/
├── roles/
│ ├── socle/
│ └── web/
├── files/
├── vars/
│ └── arrivees.yml
├── site.yml le playbook principal
├── web.yml
├── fichiers.yml
├── comptes_ad.yml
├── vm_recette.yml
└── terraform/
└── main.tf
Le fichier .gitignore :
# Reconstruits à partir de requirements.yml
collections/
roles_galaxy/
# Secrets et fichiers produits par les playbooks
*.vault-pass
coffre*.txt
identifiants/
sauvegardes/
# Terraform et OpenTofu : fournisseurs téléchargés et état
terraform/.terraform/
*.tfstate
*.tfstate.*
# Divers
*.retry
__pycache__/
Les rôles téléchargés depuis Galaxy vont dans roles_galaxy/ (avec roles_path = roles:roles_galaxy dans ansible.cfg), pour ne pas se mélanger aux rôles de Sorbier, qui, eux, sont versionnés. Les fichiers vault.yml chiffrés sont versionnés ; le mot de passe du coffre, jamais (chapitre 9). Les règles générales du .gitignore et le traitement d'un secret commité par erreur sont dans le cours Git et GitHub.
Ansible Galaxy et requirements.yml
Ansible Galaxy (galaxy.ansible.com) est le dépôt public des rôles et des collections de la communauté. On y trouve des collections officielles ou certifiées (ansible.windows, cisco.ios), des collections communautaires (community.general, community.zabbix, voir le cours Supervision) et des milliers de rôles d'auteurs individuels.
Le dépôt ne contient pas ce code écrit par d'autres : il déclare ce qu'il utilise dans requirements.yml, rôles et collections ensemble :
---
roles:
- name: geerlingguy.docker
version: 8.0.0
collections:
- name: community.general
version: ">=14.0.0"
- name: ansible.windows
version: ">=3.9.0"
# Les rôles (dans roles_galaxy/) et les collections (dans collections/)
ansible-galaxy role install -r requirements.yml -p roles_galaxy
ansible-galaxy collection install -r requirements.yml -p collections
Un rôle de Galaxy s'utilise comme les tiens (- geerlingguy.docker, pour Docker) ; un rôle fourni par une collection s'appelle par son nom complet (- community.zabbix.zabbix_agent).
Pour savoir si un rôle a été écrit pour ton parc, commence par son meta/main.yml : il donne l'auteur, la licence, la version minimale d'Ansible et les systèmes testés. Celui du rôle socle de Sorbier :
---
galaxy_info:
author: Service informatique de Sorbier Logistique
description: Socle commun des serveurs Debian de Sorbier
license: MIT
min_ansible_version: "2.18"
platforms:
- name: Debian
versions:
- trixie
dependencies: []
Un rôle dont les plateformes ne citent ni Debian 13 (trixie) ni ta distribution n'a sans doute jamais été essayé dessus : à toi de le tester en recette avant toute chose.
| Rôle | Collection | |
|---|---|---|
| Contenu | Des tâches, handlers, modèles, variables pour une fonction | Des modules, plugins (connexion, inventaire, filtres) et des rôles |
| Nom | auteur.role ou simplement role en local | espace.collection |
| Installation | ansible-galaxy role install | ansible-galaxy collection install |
| Exemple | geerlingguy.docker | community.general, microsoft.ad |
Une modification, du poste à la production
Chez Sorbier, personne ne modifie un serveur à la main, et personne ne pousse directement sur la branche main. Une modification suit ce chemin :
- Une branche :
git switch -c ajout-module-headerssur le bastion (branches Git). - La modification, testée sur le serveur de recette :
ansible-playbook site.yml --limit srv-recette, deux fois, pour vérifier l'idempotence. - Des commits au message clair :
feat(web): active le module headers et l'en-tête HSTS. - Une demande de fusion (pull request) : le pipeline lance les contrôles, un collègue relit le code et le résultat d'un
--check --diffsur la production joint à la demande (pull request et revue de code). - La fusion dans
main, puis l'application depuismain:git pullsur le bastion, puisansible-playbook site.yml.
Ce circuit donne à chaque changement un auteur, une date, une raison, une relecture, et un retour arrière simple : git revert du commit fautif, puis on rejoue le playbook.
Contrôler les playbooks avec ansible-lint
ansible-lint analyse les playbooks et les rôles sans rien exécuter : syntaxe YAML, noms complets des modules, tâches sans nom, droits de fichiers absents, commandes qui devraient être des modules, idempotence douteuse. Version 26.10 en novembre 2026 :
pipx install ansible-lint
ansible-lint
Sur un playbook écrit trop vite :
---
- hosts: linux
become: yes
tasks:
- name: installer apache
apt:
name: apache2
state: latest
- shell: systemctl restart apache2
- name: Déposer la page
ansible.builtin.copy:
src: files/index.html
dest: /var/www/html/index.html
ansible-lint répond (extrait) :
name[play]: All plays should be named.
mauvais.yml:2:3
yaml[truthy]: Truthy value should be one of [false, true]
mauvais.yml:3
name[casing]: All names should start with an uppercase letter.
mauvais.yml:5:13 Task/Handler: installer apache
package-latest: Package installs should not use latest.
mauvais.yml:5 Task/Handler: installer apache
fqcn[action-core]: Use FQCN for builtin module actions (apt).
mauvais.yml:6:7 Use `ansible.builtin.apt` or `ansible.legacy.apt` instead.
command-instead-of-module: systemctl used in place of systemd module
mauvais.yml:10 Task/Handler: shell systemctl restart apache2
name[missing]: All tasks should be named.
mauvais.yml:10 Task/Handler: shell systemctl restart apache2
no-changed-when: Commands should not change things if nothing needs doing.
mauvais.yml:10 Task/Handler: shell systemctl restart apache2
risky-file-permissions: File permissions unset or incorrect.
mauvais.yml:12 Task/Handler: Déposer la page
Failed: 11 failure(s), 0 warning(s) in 1 files processed of 1 encountered. Profile 'production' was required, but 'min' profile passed.
Chaque ligne donne la règle (entre crochets, sa variante), le fichier et la ligne. Les règles à connaître :
| Règle | Ce qu'elle signale | Correction |
|---|---|---|
name[play], name[missing], name[casing] | Un play ou une tâche sans nom, un nom sans majuscule | Nommer chaque play et chaque tâche |
yaml[truthy] | yes, no, on, off | true, false |
fqcn | Un module sans son nom complet | ansible.builtin.apt |
package-latest | state: latest : la version change au gré des dépôts | state: present, et les mises à jour dans un playbook dédié |
command-instead-of-module, command-instead-of-shell | Une commande là où un module existe ; shell sans besoin du shell | Le module dédié ; command |
no-changed-when | Une commande toujours changed | changed_when, creates ou un module |
risky-file-permissions | Un fichier déposé sans mode | mode: "0644" |
no-handler | Une tâche qui teste changed d'une autre pour redémarrer un service | Un handler |
var-naming[no-role-prefix] | Une variable de rôle sans le préfixe du rôle | web_racine dans le rôle web |
ansible-lint classe ses règles en profils de plus en plus exigeants : min, basic, moderate, safety, shared, production. Le fichier .ansible-lint à la racine du dépôt fixe le profil et les dossiers à ignorer :
---
profile: production
exclude_paths:
- collections/
- roles_galaxy/
Tous les playbooks et les rôles de ce cours passent le profil production. Pour une exception justifiée, un commentaire # noqa: nom-de-la-regle en fin de ligne désactive la règle sur cette seule ligne ; écris toujours pourquoi.
Un pipeline qui vérifie chaque modification
Le pipeline d'intégration continue lance les contrôles à chaque push et à chaque demande de fusion, sur une machine propre : un playbook cassé ne peut pas arriver dans main sans que tout le monde le voie. Le fichier .github/workflows/verifier.yml pour GitHub Actions :
name: Vérifier les playbooks
on:
push:
branches: [main]
pull_request:
jobs:
verifier:
runs-on: ubuntu-24.04
steps:
- name: Récupérer le dépôt
uses: actions/checkout@v7
- name: Installer Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Installer ansible-core et ansible-lint
run: pip install "ansible-core==2.22.0" "ansible-lint==26.10.1"
- name: Installer les collections du projet
run: ansible-galaxy collection install -r requirements.yml -p collections
- name: Vérifier la syntaxe des playbooks
run: ansible-playbook site.yml --syntax-check
- name: Lancer ansible-lint
run: ansible-lint
- Les versions sont figées : le pipeline teste avec exactement les outils du bastion, et une nouvelle version d'
ansible-lintqui ajoute une règle ne casse pas le pipeline par surprise. - Le pipeline n'a pas le mot de passe du coffre : la vérification de syntaxe et
ansible-lintn'en ont pas besoin (ils ne déchiffrent pas les fichiersvault.yml,ansible-lintse contente d'un avertissement). - Il ne se connecte à aucun serveur : un exécuteur hébergé par GitHub ne peut pas joindre le réseau interne de Sorbier, et c'est voulu.
La même logique s'écrit pour GitLab CI dans un fichier .gitlab-ci.yml. La structure d'un pipeline, les déclencheurs, les secrets et les exécuteurs auto-hébergés sont détaillés dans le cours Intégration continue ; l'analyse automatique des secrets commités par erreur, dans son chapitre DevSecOps.
Tester avant la production
Les contrôles statiques ne disent pas si le playbook fonctionne. Trois niveaux de test, du plus simple au plus complet :
- Une recette : un inventaire
inventaire/recette/avec des VM qui ressemblent à la production (commesrv-recette), où l'on applique d'abord chaque modification. Deux exécutions de suite : la première doit réussir, la seconde ne rien changer. --check --diffsur la production, joint à la demande de fusion : la liste exacte de ce qui va changer. Il faut pour cela un exécuteur qui joint le réseau, par exemple un exécuteur auto-hébergé sur le bastion, avec le mot de passe du coffre dans les secrets du dépôt.- Molecule : l'outil de test des rôles Ansible. Il crée une machine jetable (souvent un conteneur), y applique le rôle, vérifie l'idempotence par une deuxième exécution, lance des vérifications, puis détruit la machine. C'est la méthode des rôles publiés sur Galaxy.
Séparer recette et production dans deux dossiers d'inventaire, avec chacun ses group_vars, permet d'utiliser les mêmes playbooks avec des valeurs différentes :
ansible-playbook -i inventaire/recette site.yml
ansible-playbook -i inventaire/production site.yml --check --diff
Planifier et centraliser l'exécution
Lancé à la main depuis le bastion, Ansible ne corrige la dérive que quand quelqu'un y pense. Pour aller plus loin :
| Solution | Principe |
|---|---|
| Tâche planifiée (cron, timer systemd) sur le bastion | ansible-playbook site.yml --check chaque nuit, avec un rapport ; ou l'application réelle sur les rôles sûrs |
ansible-pull | Chaque serveur clone le dépôt et s'applique lui-même le playbook, en mode tiré, comme Puppet |
| Semaphore UI, AWX, Red Hat Ansible Automation Platform | Une interface web : projets liés au dépôt Git, inventaires, secrets rangés côté serveur, planification, droits par équipe, historique de chaque exécution |
Une interface centralisée a un avantage de sécurité : les administrateurs n'ont plus besoin du mot de passe du coffre ni des clés sur leur poste ; ils lancent des modèles d'exécution autorisés, et tout est journalisé.
Documenter pour le dossier de l'E6
Une réalisation d'automatisation se juge sur ce que la commission peut voir et comprendre. Dans ton dossier et ta documentation :
- le besoin : ce qui était fait à la main, combien de temps, quels problèmes (oublis, dérive) ;
- un schéma du laboratoire et des flux d'Ansible, comme celui du chapitre 2 ;
- le dépôt : son arborescence, le rôle de chaque playbook et de chaque rôle, la liste des variables réglables (
defaults/) ; - les choix : pourquoi Ansible, pourquoi WinRM avec Kerberos ou SSH, pourquoi un compte dédié, comment les secrets sont protégés ;
- les preuves : la sortie de deux exécutions successives (la seconde avec
changed=0), le résultat d'ansible-lint, une capture du pipeline vert, un--check --diffqui détecte une modification faite à la main ; - les limites et les améliorations possibles : Molecule, interface centralisée, gestionnaire de secrets.
Le jour de l'épreuve, pendant l'entretien de la phase 2, la preuve la plus parlante tient en deux minutes : modifier une variable, relancer le playbook, montrer le changed sur le seul élément concerné, relancer, montrer changed=0. Le cours sur l'épreuve E6 SISR détaille le déroulement des deux phases et la préparation du dossier.
Exercices
Exercice 1 sur 4 Ce qui va dans le dépôt Réussi À revoir
Pour chaque fichier, dis s'il doit être commité dans sorbier-infra, et pourquoi.
roles/socle/tasks/main.ymlinventaire/group_vars/windows/vault.yml, chiffré~/.config/ansible/coffre-sorbier.txtcollections/ansible_collections/community/general/…identifiants/eberard.txtterraform/terraform.tfstaterequirements.yml
Voir la réponseMasquer la réponse de l'exercice 1
- Oui : c'est le code du rôle.
- Oui : chiffré, il est illisible sans le mot de passe, et il doit suivre l'évolution du projet.
- Non : c'est le mot de passe du coffre ; il est d'ailleurs hors du dossier du projet.
- Non : les collections se reconstruisent avec
ansible-galaxy collection install -r requirements.yml. - Non : un mot de passe initial en clair, à remettre puis à supprimer.
- Non : l'état de Terraform contient des secrets et se range dans un backend distant verrouillé.
- Oui : il fige les dépendances du projet.
Tu avais trouvé ?
Exercice 2 sur 4 Corriger d'après ansible-lint Réussi À revoir
Corrige le playbook mauvais.yml de ce chapitre pour qu'il passe le profil production. Le service doit être redémarré seulement si la page change.
Voir la réponseMasquer la réponse de l'exercice 2
---
- name: Installer Apache et sa page d'accueil
hosts: linux
become: true
tasks:
- name: Installer Apache
ansible.builtin.apt:
name: apache2
state: present
- name: Déposer la page
ansible.builtin.copy:
src: files/index.html
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: "0644"
notify: Redémarrer Apache
handlers:
- name: Redémarrer Apache
ansible.builtin.service:
name: apache2
state: restarted
Le play est nommé, become: true remplace yes, les modules ont leur FQCN, la tâche est nommée avec une majuscule, present remplace latest, le fichier a des droits explicites, et le redémarrage passe par un handler au lieu d'une commande shell lancée à chaque fois. Pour une simple page HTML, Apache n'a d'ailleurs pas besoin d'être redémarré : le handler sert ici à l'exercice ; pour une page, on le supprimerait.
Tu avais trouvé ?
Exercice 3 sur 4 Un pipeline pour GitLab Réussi À revoir
Sorbier migre son dépôt sur un GitLab interne. Écris le fichier .gitlab-ci.yml qui fait les mêmes contrôles que le workflow GitHub Actions de ce chapitre, dans une image python:3.13.
Voir la réponseMasquer la réponse de l'exercice 3
verifier:
image: python:3.13
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- pip install "ansible-core==2.22.0" "ansible-lint==26.10.1"
- ansible-galaxy collection install -r requirements.yml -p collections
- ansible-playbook site.yml --syntax-check
- ansible-lint
Les rules lancent le travail sur les demandes de fusion et sur la branche par défaut, comme pull_request et push: branches: [main] côté GitHub. Le travail n'a besoin d'aucun secret ni d'aucun accès au réseau interne. Les détails de la syntaxe de GitLab CI sont dans le cours Intégration continue.
Tu avais trouvé ?
Exercice 4 sur 4 Adopter un rôle de Galaxy Réussi À revoir
Moussa a trouvé sur Galaxy un rôle qui installe et configure l'agent de supervision. Il propose d'ajouter - name: auteur.agent_supervision sans version à requirements.yml et de l'appliquer à tout le parc dès ce soir. Quelles vérifications et quelles modifications proposes-tu, dans l'ordre ?
Voir la réponseMasquer la réponse de l'exercice 4
- Vérifier le rôle : auteur identifiable, dernière version récente, compatible avec Debian 13 et ansible-core 2.22 (
meta/main.yml), tickets ouverts, licence. - Lire ses tâches : ce qu'il installe, d'où il télécharge (dépôt officiel de l'éditeur ou source inconnue), ce qu'il ouvre dans le pare-feu, s'il désactive des protections.
- Figer la version dans
requirements.yml(version: 3.2.0par exemple) pour que rien ne change sans relecture. - Tester en recette sur un seul serveur (
--limit), avec--check --diffd'abord, puis deux exécutions pour vérifier l'idempotence. - Passer par une branche et une revue dans Git avant de fusionner, comme toute modification du dépôt, puis déployer progressivement (
serial).
Si le rôle est trop générique ou mal maintenu, un rôle maison court, appuyé sur la collection officielle de l'éditeur quand elle existe, reste souvent le meilleur choix.
Tu avais trouvé ?