08 — Ansible : approfondissement#
Le livre Linux — Administration système couvre les bases d’Ansible : playbooks, inventaire statique, modules essentiels (apt, copy, template, service…). Ce chapitre part de ces acquis pour explorer les fonctionnalités avancées : rôles structurés, collections Galaxy, Ansible Vault, inventaire dynamique, stratégies d’exécution, tests avec Molecule et intégration CI/CD.
Rappel des bases#
Un playbook Ansible est un fichier YAML décrivant un ensemble de plays : chaque play associe un groupe d’hôtes à une liste de tasks exécutées séquentiellement par défaut.
# playbook.yml — structure minimale
---
- name: Configurer les serveurs web
hosts: web_servers
become: true
vars:
nginx_port: 80
tasks:
- name: Installer nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Démarrer et activer nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
L’inventaire statique liste les hôtes par groupe ; les variables peuvent être définies inline ou dans host_vars/ et group_vars/.
Rôles avancés#
Un rôle est une unité de réutilisation structurée. La convention de répertoires est stricte et chaque sous-dossier a un rôle précis.
roles/
└── nginx/
├── defaults/
│ └── main.yml # variables par défaut (priorité la plus faible)
├── vars/
│ └── main.yml # variables internes (priorité haute, non surchargeables par l'utilisateur)
├── tasks/
│ ├── main.yml # point d'entrée des tâches
│ ├── install.yml
│ └── configure.yml
├── handlers/
│ └── main.yml # handlers déclenchés par notify
├── templates/
│ └── nginx.conf.j2 # templates Jinja2
├── files/
│ └── index.html # fichiers statiques
├── meta/
│ └── main.yml # métadonnées : dépendances entre rôles
├── molecule/
│ └── default/ # scénarios de test Molecule
└── README.md
defaults/main.yml vs vars/main.yml#
# defaults/main.yml — valeurs par défaut surchargeables
nginx_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_log_format: combined
# vars/main.yml — constantes internes (non surchargeables par l'inventaire)
nginx_service_name: nginx
nginx_config_dir: /etc/nginx
nginx_sites_dir: "{{ nginx_config_dir }}/sites-enabled"
Handlers#
# handlers/main.yml
---
- name: reload nginx
ansible.builtin.service:
name: "{{ nginx_service_name }}"
state: reloaded
- name: restart nginx
ansible.builtin.service:
name: "{{ nginx_service_name }}"
state: restarted
# tasks/configure.yml — déclenchement du handler
- name: Déployer la configuration nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ nginx_config_dir }}/nginx.conf"
owner: root
group: root
mode: "0644"
validate: nginx -t -c %s
notify: reload nginx
meta/main.yml — dépendances entre rôles#
# meta/main.yml
---
galaxy_info:
role_name: nginx
author: monorg
description: Installation et configuration d'Nginx
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions: ["22.04", "24.04"]
dependencies:
- role: common # installé avant nginx
- role: firewall
vars:
firewall_allowed_ports: [80, 443]
Collections Ansible Galaxy#
Depuis Ansible 2.9, le code est distribué via des collections : un ensemble cohérent de modules, plugins, rôles et playbooks sous un namespace.
# requirements.yml
---
collections:
- name: community.general
version: ">=8.0"
- name: amazon.aws
version: "~=7.0"
- name: kubernetes.core
version: ">=3.0"
- name: community.postgresql
version: ">=3.2"
roles:
- name: geerlingguy.docker
version: "7.0.0"
- src: https://github.com/monorg/ansible-role-app.git
scm: git
version: v2.1.0
name: monorg.app
# Installation depuis requirements.yml
ansible-galaxy collection install -r requirements.yml
ansible-galaxy role install -r requirements.yml
# Lister les collections installées
ansible-galaxy collection list
# Créer un squelette de collection
ansible-galaxy collection init monorg.infrastructure
Utilisation d’un module depuis une collection :
- name: Créer un bucket S3
amazon.aws.s3_bucket:
name: "mon-bucket-{{ env }}"
region: eu-west-1
versioning: true
tags:
Environment: "{{ env }}"
ManagedBy: ansible
Ansible Vault#
Vault permet de chiffrer des variables sensibles (mots de passe, clés API, certificats) directement dans les fichiers YAML du dépôt git.
# Chiffrer un fichier entier
ansible-vault encrypt group_vars/prod/secrets.yml
# Editer un fichier chiffré
ansible-vault edit group_vars/prod/secrets.yml
# Déchiffrer (pour inspection)
ansible-vault decrypt group_vars/prod/secrets.yml
# Chiffrer une valeur inline (vault-encrypted string)
ansible-vault encrypt_string 'MonMotDePasse123!' --name 'db_password'
# Exécuter un playbook avec le mot de passe Vault
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Fichier de secrets chiffré :
# group_vars/prod/secrets.yml (chiffré avec ansible-vault encrypt)
$ANSIBLE_VAULT;1.1;AES256
66386439343533346637623039...
Intégration CI — variable d’environnement :
# .github/workflows/deploy.yml
- name: Déployer avec Ansible
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass
ansible-playbook -i inventories/prod site.yml \
--vault-password-file /tmp/.vault_pass
rm /tmp/.vault_pass
Rotation des clés Vault
Utilisez ansible-vault rekey pour changer le mot de passe Vault sans déchiffrer/rechiffrer manuellement. En production, préférez un backend de secrets externe (HashiCorp Vault, AWS Secrets Manager) avec le plugin lookup hashi_vault ou aws_ssm.
Inventaire dynamique#
L’inventaire dynamique interroge une source externe (AWS, GCP, Azure, Kubernetes, VMware…) pour construire l’inventaire à la volée.
# inventories/aws/aws_ec2.yml — plugin AWS EC2
plugin: amazon.aws.aws_ec2
regions:
- eu-west-1
- eu-west-3
filters:
tag:Environment: prod
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
compose:
ansible_host: public_ip_address
ansible_user: "'ubuntu'"
hostnames:
- tag:Name
- private-ip-address
# Lister l'inventaire dynamique (sans exécuter de playbook)
ansible-inventory -i inventories/aws/ --list
ansible-inventory -i inventories/aws/ --graph
# Tester la connectivité
ansible -i inventories/aws/ role_web -m ping
Plugin GCP :
# inventories/gcp/gcp_compute.yml
plugin: google.cloud.gcp_compute
projects:
- mon-projet-gcp
filters:
- status = RUNNING
- labels.environment = prod
keyed_groups:
- key: labels.role
prefix: role
Stratégies d’exécution et optimisation#
Stratégies#
# playbook.yml
- name: Déploiement avec stratégie free
hosts: app_servers
strategy: free # chaque hôte avance à son propre rythme (pas d'attente)
# strategy: linear # défaut : tous les hôtes terminent chaque task avant de passer à la suivante
# strategy: host_pinned # comme free, mais toutes les tasks d'un hôte s'exécutent en séquence
Optimisation#
# ansible.cfg
[defaults]
forks = 20 # parallélisme (défaut : 5)
gathering = smart # ne collecte les facts qu'une fois par hôte
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[ssh_connection]
pipelining = true # réduit le nombre de connexions SSH (~30% plus rapide)
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
# Délégation et parallélisme avancé
- name: Mettre à jour les serveurs par lots de 20%
hosts: web_servers
serial: "20%" # rolling update : 20% des hôtes à la fois
max_fail_percentage: 10
tasks:
- name: Désactiver dans le load balancer
delegate_to: localhost
uri:
url: "http://lb.internal/drain/{{ inventory_hostname }}"
method: POST
- name: Mettre à jour l'application
ansible.builtin.apt:
name: myapp
state: latest
notify: restart app
Tests de rôles avec Molecule#
Molecule est le framework de test officiel pour les rôles Ansible. Il crée des instances éphémères (Docker, Vagrant, cloud), applique le rôle, puis vérifie le résultat.
# Initialiser un rôle avec Molecule
molecule init role monorg.nginx --driver-name docker
# Cycle de test complet
molecule test
# Étapes individuelles
molecule create # créer les instances de test
molecule converge # appliquer le rôle
molecule verify # exécuter les tests
molecule destroy # supprimer les instances
molecule login # se connecter à une instance pour debug
# molecule/default/molecule.yml
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: ubuntu-22
image: "geerlingguy/docker-ubuntu2204-ansible:latest"
pre_build_image: true
privileged: true
- name: debian-12
image: "geerlingguy/docker-debian12-ansible:latest"
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
inventory:
host_vars:
ubuntu-22:
nginx_port: 8080
verifier:
name: ansible
# molecule/default/verify.yml
---
- name: Vérifier la configuration nginx
hosts: all
gather_facts: false
tasks:
- name: nginx doit être démarré
ansible.builtin.service_facts:
- name: Vérifier que le service est actif
ansible.builtin.assert:
that: ansible_facts.services['nginx.service'].state == 'running'
fail_msg: "nginx n'est pas en cours d'exécution"
- name: Vérifier la réponse HTTP
ansible.builtin.uri:
url: "http://localhost:{{ nginx_port | default(80) }}"
status_code: 200
Intégration dans un pipeline CI/CD#
GitHub Actions#
# .github/workflows/ansible.yml
---
name: CI Ansible
on:
push:
paths: ["roles/**", "playbooks/**", "inventories/**"]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Installer ansible-lint
run: pip install ansible-lint
- name: Linter les rôles et playbooks
run: ansible-lint
molecule:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
role: [nginx, postgresql, app]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Installer les dépendances
run: pip install molecule molecule-plugins[docker] ansible-core
- name: Tester le rôle ${{ matrix.role }}
run: molecule test
working-directory: roles/${{ matrix.role }}
deploy:
runs-on: ubuntu-latest
needs: molecule
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Configurer la clé SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Déployer en production
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass
ansible-playbook -i inventories/prod site.yml \
--vault-password-file /tmp/.vault_pass \
--diff
GitLab CI#
# .gitlab-ci.yml (extrait)
variables:
ANSIBLE_FORCE_COLOR: "true"
PY_COLORS: "1"
.ansible-base:
image: python:3.12-slim
before_script:
- pip install ansible-core molecule molecule-plugins[docker]
- ansible-galaxy install -r requirements.yml
lint:
extends: .ansible-base
script:
- pip install ansible-lint
- ansible-lint
molecule-test:
extends: .ansible-base
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2376
script:
- molecule test
parallel:
matrix:
- ROLE: [nginx, postgresql, app]
before_script:
- cd roles/$ROLE
Ansible vs Terraform — complémentarité#
Ces deux outils répondent à des besoins différents et se complètent naturellement dans un pipeline IaC.
Dimension |
Terraform |
Ansible |
|---|---|---|
Paradigme |
Déclaratif (état désiré) |
Procédural (étapes séquentielles) |
Modèle de ressources |
Immuable (recréer > modifier) |
Muable (modifier en place) |
Domaine principal |
Provisioning (VM, réseau, DB…) |
Configuration (packages, fichiers, services) |
State |
Fichier tfstate explicite |
Sans state (idempotence par design) |
Connexion |
APIs cloud (HTTP) |
SSH / WinRM |
Idéal pour |
Créer et détruire l’infrastructure |
Configurer et maintenir les serveurs |
Pattern d’usage combiné :
Terraform provisionne le VPC, l’EKS, la RDS, le DNS, et écrit les IPs/endpoints dans SSM.
Ansible récupère ces valeurs depuis SSM, configure les serveurs (packages, monitoring, app), et maintient la configuration dans le temps.
Ansible AWX, Ansible Tower et AAP#
AWX est la version open-source upstream d’Ansible Tower/AAP. Il fournit une interface web, une API REST, un scheduler, et la gestion RBAC pour exécuter des playbooks en équipe.
Ansible Automation Platform (AAP) est la version Red Hat commerciale, avec support, conteneurisation (Execution Environments), et intégration Event-Driven Ansible.
Points clés :
Les Execution Environments (EE) sont des images OCI contenant Ansible Core, les collections et leurs dépendances Python — reproductibles entre dev et prod.
L”inventaire dynamique est une source de première classe dans AWX/Tower.
Les workflows permettent d’enchaîner plusieurs job templates avec des conditions de succès/échec.
L’API REST d’AWX permet de déclencher des playbooks depuis n’importe quel système CI.
Visualisations#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import numpy as np
import networkx as nx
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Visualisation 1 : Heatmap de durée de run Ansible (tasks × hosts)
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
tasks = [
"Gather facts",
"apt update",
"Install packages",
"Deploy config",
"Start services",
"Run smoke tests",
"Notify handlers",
]
hosts = [f"web-{i:02d}" for i in range(1, 9)]
# Durées simulées en secondes
base = np.array([
[3, 3, 3, 3, 3, 3, 3, 3], # gather facts — stable
[8, 12, 7, 15, 9, 11, 8, 10], # apt update — variable réseau
[22, 25, 20, 30, 19, 28, 21, 24], # install packages
[2, 2, 2, 2, 2, 2, 2, 2], # deploy config — rapide
[3, 4, 3, 3, 4, 3, 3, 4], # start services
[5, 8, 5, 12, 6, 7, 5, 9], # smoke tests — variable
[1, 1, 1, 1, 1, 1, 1, 1], # handlers
])
fig, ax = plt.subplots(figsize=(12, 6))
im = ax.imshow(base, cmap="YlOrRd", aspect="auto", vmin=0, vmax=35)
ax.set_xticks(range(len(hosts)))
ax.set_yticks(range(len(tasks)))
ax.set_xticklabels(hosts, fontsize=9.5)
ax.set_yticklabels(tasks, fontsize=9.5)
ax.set_title("Durée d'exécution Ansible par task et par hôte (secondes simulées)", fontsize=13, fontweight="bold", pad=15)
for i in range(len(tasks)):
for j in range(len(hosts)):
val = base[i, j]
color = "white" if val > 20 else "#333"
ax.text(j, i, f"{val}s", ha="center", va="center", fontsize=8.5, fontweight="bold", color=color)
plt.colorbar(im, ax=ax, label="Durée (secondes)", shrink=0.85)
plt.savefig("ansible_heatmap_duration.png", dpi=120, bbox_inches="tight")
plt.show()
# Visualisation 2 : Matrice 2×2 — positionnement des outils IaC/configuration
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(10, 9))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
# Quadrants
ax.axvline(x=5, color="#ced4da", linewidth=2, linestyle="--")
ax.axhline(y=5, color="#ced4da", linewidth=2, linestyle="--")
# Labels des axes et quadrants
ax.set_xlabel("Provisioning ← → Configuration", fontsize=11, labelpad=12)
ax.set_ylabel("Muable (in-place) ← → Immuable (recreate)", fontsize=11, labelpad=12)
ax.set_xticks([])
ax.set_yticks([])
quadrants = [
(1.5, 8.5, "Provisioning\n+ Immuable", "#e8f4fd"),
(7.5, 8.5, "Configuration\n+ Immuable", "#e8f7ee"),
(1.5, 1.5, "Provisioning\n+ Muable", "#fff3cd"),
(7.5, 1.5, "Configuration\n+ Muable", "#fde8e8"),
]
for x, y, label, color in quadrants:
rect = FancyBboxPatch((x - 1.4, y - 1.0), 2.8, 2.0,
boxstyle="round,pad=0.1",
facecolor=color, edgecolor="#ced4da", linewidth=1.2, zorder=1, alpha=0.6)
ax.add_patch(rect)
ax.text(x, y, label, ha="center", va="center", fontsize=8.5, color="#555", style="italic")
# Outils positionnés
outils = [
("Terraform /\nOpenTofu", 2.2, 7.8, "#ff922b", 350),
("Pulumi", 3.2, 7.0, "#ffd43b", 280),
("AWS CDK", 2.5, 6.2, "#e9ecef", 260),
("Ansible", 7.2, 3.5, "#51cf66", 350),
("Chef", 6.5, 2.8, "#74c0fc", 280),
("Puppet", 7.8, 2.5, "#4dabf7", 280),
("Salt", 6.8, 4.2, "#a9e34b", 250),
("Packer", 3.8, 6.8, "#f783ac", 230),
]
for label, x, y, color, size in outils:
ax.scatter(x, y, s=size, color=color, alpha=0.88, edgecolors="white", linewidths=2, zorder=4)
ax.annotate(label, (x, y), textcoords="offset points", xytext=(10, 6),
fontsize=9.5, fontweight="bold", color="#333", zorder=5)
ax.set_title("Positionnement des outils IaC / gestion de configuration\nAxes : provisioning↔configuration et muable↔immuable",
fontsize=13, fontweight="bold", pad=15)
plt.savefig("iac_tools_matrix.png", dpi=120, bbox_inches="tight")
plt.show()
# Visualisation 3 : Graphe de dépendances entre rôles Ansible
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
G = nx.DiGraph()
roles = {
"common": {"color": "#4dabf7", "layer": 0},
"firewall": {"color": "#4dabf7", "layer": 0},
"users": {"color": "#4dabf7", "layer": 0},
"python": {"color": "#74c0fc", "layer": 1},
"docker": {"color": "#74c0fc", "layer": 1},
"postgresql": {"color": "#51cf66", "layer": 2},
"redis": {"color": "#51cf66", "layer": 2},
"nginx": {"color": "#51cf66", "layer": 2},
"app": {"color": "#ffd43b", "layer": 3},
"monitoring": {"color": "#ff922b", "layer": 4},
}
for role, attrs in roles.items():
G.add_node(role, **attrs)
# Dépendances : A → B signifie que le rôle A dépend de B (B est installé en premier)
deps = [
("firewall", "common"),
("users", "common"),
("python", "common"),
("docker", "python"),
("docker", "firewall"),
("postgresql", "python"),
("postgresql", "firewall"),
("redis", "firewall"),
("nginx", "firewall"),
("app", "docker"),
("app", "postgresql"),
("app", "redis"),
("app", "nginx"),
("monitoring", "app"),
("monitoring", "common"),
]
G.add_edges_from(deps)
pos = {
"common": (4.0, 4.0),
"firewall": (2.0, 4.0),
"users": (6.0, 4.0),
"python": (2.0, 3.0),
"docker": (2.0, 2.0),
"postgresql": (4.0, 2.0),
"redis": (6.0, 2.5),
"nginx": (7.5, 3.0),
"app": (4.5, 1.0),
"monitoring": (4.5, 0.0),
}
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_title("Graphe de dépendances entre rôles Ansible\n(A → B : le rôle A dépend du rôle B)", fontsize=13, fontweight="bold", pad=15)
node_colors = [roles[n]["color"] for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=2000, alpha=0.9, ax=ax)
nx.draw_networkx_labels(G, pos, font_size=8.5, font_weight="bold", ax=ax)
nx.draw_networkx_edges(G, pos, arrows=True, arrowsize=20, width=2.0,
edge_color="#868e96", alpha=0.75,
connectionstyle="arc3,rad=0.05", ax=ax)
layer_labels = {0: "Couche de base", 1: "Runtime", 2: "Services", 3: "Application", 4: "Observabilité"}
colors_legend = ["#4dabf7", "#74c0fc", "#51cf66", "#ffd43b", "#ff922b"]
patches = [mpatches.Patch(color=c, label=l) for c, l in zip(colors_legend, layer_labels.values())]
ax.legend(handles=patches, loc="lower right", fontsize=9)
ax.axis("off")
plt.savefig("ansible_roles_dag.png", dpi=120, bbox_inches="tight")
plt.show()
Résumé#
Les rôles Ansible structurent le code de configuration en couches claires (
defaults,vars,tasks,handlers,meta) et constituent l’unité de réutilisation et de test élémentaire.La distinction entre
defaults/(surchargeables par l’inventaire) etvars/(constantes internes) est fondamentale pour concevoir des rôles flexibles sans exposer leurs détails d’implémentation.Les collections Ansible Galaxy permettent de distribuer et versionner des ensembles cohérents de modules, rôles et plugins — le fichier
requirements.ymlrend les dépendances explicites et reproductibles.Ansible Vault intégré dans le dépôt git permet de chiffrer les secrets au plus proche du code ; en production, le combiner avec un backend de secrets externe (HashiCorp Vault, AWS Secrets Manager) via les plugins lookup.
L”inventaire dynamique transforme Ansible en outil scalable : les plugins AWS EC2, GCP Compute et Azure RM interrogent directement les APIs cloud pour construire l’inventaire sans maintenance manuelle.
La stratégie
freeavecpipelining = trueet un nombre élevé deforkspeut réduire les temps d’exécution de 30 à 50% sur des parcs de serveurs importants.Molecule est indispensable pour tester les rôles dans des conteneurs éphémères ; l’intégration dans la CI garantit que chaque modification de rôle est validée avant merge.
L’intégration Ansible dans GitHub Actions / GitLab CI suit le même pattern que le code applicatif : lint, test (Molecule), puis déploiement conditionné à la branche principale.
Ansible et Terraform sont complémentaires : Terraform provisionne l’infrastructure immuable, Ansible configure et maintient les systèmes muables — les deux se parlent via SSM, outputs, ou inventaires dynamiques.
AWX / Ansible Tower / AAP apportent une interface web, un contrôle d’accès fin et des Execution Environments reproductibles pour opérer Ansible à l’échelle d’une organisation.