Aller au contenu
Infrastructure as Code medium

CI/CD GitHub Actions pour rôle Ansible : workflow complet 2026

75 min de lecture

Logo Ansible

Un workflow GitHub Actions complet pour un rôle Ansible 2026 : lint en fail-fast, puis matrice Molecule (Rocky Linux 9, AlmaLinux 10, Ubuntu 24.04 croisées avec ansible-core 2.17 et 2.18). Actions épinglées par SHA, permissions minimales, persist-credentials: false.

  • Structure d'un workflow .github/workflows/test.yml.
  • Configurer un job lint en fail-fast (avant Molecule).
  • Définir une matrice multi-dimensions : distros × versions Ansible.
  • Sécuriser le workflow (SHA pinning, permissions minimales).
  • Déclencheurs : push, pull_request, schedule (re-test périodique).

Voici le fichier complet, à poser tel quel à la racine du dépôt de votre rôle. Lisez-le en repérant d'abord ses deux jobs : lint s'exécute seul et rapidement, molecule ne démarre qu'ensuite et se démultiplie en six exécutions parallèles. Tout le reste, les permissions vides, les SHA à la place des tags, persist-credentials: false, relève du durcissement et se justifie section par section plus bas. Les commandes pip install figent volontairement des versions : un rôle Ansible testé sur une version d'ansible-core qui bouge sans prévenir produit des échecs impossibles à rattacher à un changement de code.

---
name: Test webserver role
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # lundi 6h, re-test contre les dernières versions
permissions: {} # ← deny-by-default au top
jobs:
lint:
name: Lint (ansible-lint + yamllint)
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install lint tools
run: pip install ansible-lint==25.* yamllint
- name: Run yamllint
run: yamllint roles/
- name: Run ansible-lint (profile production)
run: ansible-lint --profile=production roles/
molecule:
name: Molecule (${{ matrix.distro }} / ansible-core ${{ matrix.ansible }})
runs-on: ubuntu-24.04
needs: lint # ← lint doit passer en premier
permissions:
contents: read
strategy:
fail-fast: false
matrix:
distro:
- rockylinux9
- alma10
- ubuntu2404
ansible:
- 2.17
- 2.18
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install Molecule + plugins
run: |
pip install \
"ansible-core==${{ matrix.ansible }}.*" \
"molecule>=26.0" \
"molecule-plugins[podman]>=25.0" \
"ansible-lint==25.*"
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Run molecule test
run: molecule test
env:
MOLECULE_DISTRO: ${{ matrix.distro }}
PY_COLORS: 1
ANSIBLE_FORCE_COLOR: 1

Trois déclencheurs valent mieux qu'un, parce qu'ils attrapent des régressions différentes. Le couple push et pull_request couvre vos propres modifications, avant et après fusion. Le schedule répond à un cas que les deux autres ne voient jamais : votre rôle n'a pas changé, mais son environnement, si. Une nouvelle version d'ansible-core, une image de base mise à jour, une collection Galaxy remontée, et le rôle casse sans le moindre commit de votre part.

on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # lundi 6h
  • push sur main/develop : re-test après merge.
  • pull_request sur main : test avant merge.
  • schedule hebdomadaire : détecte les régressions liées aux mises à jour upstream (nouvelles versions Ansible, nouvelles images Docker base).

Sans cette ligne, le GITHUB_TOKEN fourni au workflow hérite des permissions par défaut du dépôt, qui restent en écriture sur beaucoup de dépôts créés avant 2023. Un attrset vide révoque tout, et chaque job redéclare ensuite le strict nécessaire. Un job de test n'a besoin que de lire le code, d'où contents: read et rien d'autre.

permissions: {}

Deny-by-default au top du workflow. Chaque job redéclare ses permissions explicitement (contents: read pour les jobs qui n'écrivent rien).

Pourquoi : si une dépendance malveillante s'exécute pendant le job, elle ne peut pas pousser sur le dépôt, créer une release ni poser un tag. C'est la recommandation explicite de la documentation GitHub sur le durcissement des workflows.

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Pas @v4 ni @main. Le SHA de 40 caractères du commit garantit que vous exécutez exactement ce code, pas une version remontée silencieusement.

Pourquoi : actions/checkout@v4 est un tag mobile, quiconque a les droits sur le dépôt de l'action peut le repointer vers un autre commit. Un SHA de commit, lui, ne se réécrit pas.

- uses: actions/checkout@<SHA>
with:
persist-credentials: false

Par défaut, actions/checkout écrit le jeton GitHub dans le .git/config du runner, pour que les opérations Git suivantes s'authentifient sans configuration. Toute étape exécutée ensuite peut lire ce fichier et réutiliser le jeton pour pousser sur le dépôt.

persist-credentials: false supprime cette persistance. À poser systématiquement dès que le job installe des dépendances tierces, ce qui est le cas ici avec pip et ansible-galaxy.

strategy:
fail-fast: false
matrix:
distro: [rockylinux9, alma10, ubuntu2404]
ansible: [2.17, 2.18]

3 distros × 2 versions = 6 jobs en parallèle. Si un job échoue, les 5 autres continuent (fail-fast: false), vous voyez toutes les régressions, pas juste la première.

molecule:
needs: lint

Le job molecule attend que lint passe. Sans cette dépendance, les six jobs de matrice démarrent en parallèle du lint et consomment plusieurs dizaines de minutes de runner avant de révéler une simple erreur d'indentation. La règle vaut aussi pour votre confort de relecture : un rapport de CI où le lint a échoué en 40 secondes se lit plus vite que six échecs Molecule à interpréter.

Le workflow suppose une arborescence précise, et deux chemins y sont vérifiés à l'exécution. Le job lint passe yamllint et ansible-lint sur roles/, ce qui impose que vos rôles soient bien sous ce répertoire et non à la racine. Le job molecule appelle ansible-galaxy collection install -r requirements.yml, qui échoue si le fichier manque, même quand le rôle n'utilise aucune collection externe. Si votre dépôt ne contient qu'un seul rôle et le publie à sa racine, adaptez les chemins du job lint en conséquence.

repo-rôle/
├── .github/
│ └── workflows/
│ └── test.yml ← ce fichier
├── roles/
│ └── webserver/
│ ├── tasks/
│ ├── defaults/
│ └── meta/
├── molecule/
│ └── default/
├── requirements.yml ← collections requises
└── README.md

Relire un workflow durci et l'écrire soi-même sont deux exercices très différents. Ce lab vous remet un .github/workflows/test.yml amputé de ses valeurs sensibles, à vous de rétablir le job lint en amont, la matrice distros × ansible-core, l'épinglage par SHA et les permissions minimales par job. La correction contrôle la structure obtenue puis passe actionlint sur le fichier, exactement comme le ferait une revue de code sérieuse.

Ces cinq situations reviennent systématiquement lors de la première mise en place. Deux d'entre elles se ressemblent et n'ont rien à voir : Unable to resolve action signale un SHA introuvable dans le dépôt de l'action, alors que Permission denied sur l'API GitHub vient du permissions: du job. Le réflexe utile est de lire la colonne Cause avant de toucher au fichier, car la correction naturelle du second symptôme, réélargir les permissions, est aussi le meilleur moyen d'annuler le durcissement mis en place ici.

SymptômeCauseFix
Error: Unable to resolve actionSHA invalide ou repo renomméMettre à jour le SHA après avoir vérifié le repo officiel
Tests passent localement, ratent en CIVersion de Python ou d'Ansible différenteAligner basepython de tox avec python-version du workflow
molecule test lent (>20 min)Pas de cache Docker layersUtiliser actions/cache sur ~/.local
secret manquantPas de secrets: configuré pour la publicationAller dans repo Settings → Secrets and variables
Permission denied sur l'API GitHubpermissions: {} trop restrictifAjouter contents: read ou pull-requests: write selon besoin
  • permissions: {} au top + redéclaration par job = least-privilege.
  • Actions pinnées par SHA + persist-credentials: false = sécurité supply chain.
  • needs: lint avant Molecule évite de consommer des minutes de runner sur une erreur de lint.
  • Matrice 2D distros × versions = couverture complète automatique.
  • schedule: hebdomadaire = détecte régressions upstream.
  • Installer rôles Galaxy : le requirements.yml épinglé sans lequel la matrice teste un contenu qui bouge.
  • Versionner et publier : transformer un tag Git en release publiée automatiquement, la suite naturelle de ce pipeline.
  • HashiCorp Vault / OpenBao : récupérer les jetons du pipeline depuis un coffre, plutôt que de multiplier les secrets de dépôt.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn