Ansible - Automatisation et gestion de configuration
- 1. Architecture et modèle d'exécution
- 2. Installation et collections
- 3. Inventaires statiques et dynamiques
- 4. Playbooks, rôles et structure
- 5. Variables et précédence
- 6. Handlers et templates Jinja2
- 7. Idempotence, check et diff
- 8. Boucles, conditions, blocks
- 9. ansible-vault et gestionnaires de secrets
- 10. Windows
- 11. Exemple : durcissement Debian
- 12. Exemple : déploiement LAMP / PHP
- 13. Mises à jour en rolling
- 14. Tests : Molecule et ansible-lint
- 15. Performances et exécution à l'échelle
- 16. Dépannage et checklist
1. Architecture et modèle d'exécution
Ansible est agentless et push : un nœud de contrôle (poste d'admin, runner CI, AWX) se connecte aux cibles par SSH (Linux) ou WinRM/SSH (Windows), y copie un module (code Python ou PowerShell autonome), l'exécute, récupère un résultat JSON, puis supprime le fichier temporaire. Aucun démon ni port dédié sur les cibles.
| Composant | Rôle |
|---|---|
| Nœud de contrôle | Linux/macOS (ou WSL) avec Python ; Windows n'est pas supporté comme contrôleur natif |
| Cibles (managed nodes) | SSH + Python 3 pour Linux ; PowerShell 5.1+ et .NET pour Windows |
| Inventaire | Liste des hôtes, groupes et variables (fichier ou plugin dynamique) |
| Module | Unité d'action idempotente (ansible.builtin.copy, community.general.ufw...) |
| Plugin | Extension côté contrôleur : connexion, inventaire, lookup, filtre, callback, cache |
| Collection | Paquet de distribution (modules, plugins, rôles) avec FQCN namespace.collection.module |
| Playbook / play / task | Fichier YAML / association hôtes-tâches / appel d'un module |
1.1 Déroulement d'un play
- Résolution de l'inventaire et des variables.
- Collecte des facts (module
setup) sigather_factsest actif. - Pour chaque tâche : exécution en parallèle sur
forkshôtes (5 par défaut), stratégielinear: tous les hôtes terminent la tâche N avant de passer à N+1. - Les hôtes en échec sont retirés du reste du play.
- Les handlers notifiés sont exécutés en fin de play (ou à un
meta: flush_handlers).
| Critère | Ansible | Terraform / OpenTofu |
|---|---|---|
| Domaine | Configuration des systèmes, orchestration d'opérations | Cycle de vie de l'infrastructure (créer/détruire) |
| État | Aucun : l'état est constaté sur la cible à chaque exécution | Fichier d'état central |
| Suppression | Explicite (state: absent) | Implicite (retirer le code) |
| Ordre | Séquentiel, écrit par l'auteur | Graphe de dépendances calculé |
Les deux se complètent : Terraform / OpenTofu crée les VM, Ansible les configure.
2. Installation et collections
2.1 ansible-core ou ansible
ansible-core: moteur, CLI et collectionansible.builtinuniquement. Versions 2.18, 2.19, 2.20... sur un cycle d'environ six mois.ansible(paquet « communautaire ») :ansible-core+ une sélection de collections (community.general, ansible.posix, ansible.windows, azure.azcollection...). Numérotation différente (10, 11, 12...).
Pour la production, installer ansible-core et déclarer explicitement les collections dans un requirements.yml versionné : on sait exactement ce qui tourne.
# Installation isolée par utilisateur
sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible-core
pipx inject ansible-core ansible-lint # outils dans le même environnement
pipx inject ansible-core pywinrm # si cibles Windows en WinRM
ansible --version # vérifier versions core, Python, config utilisée
# Alternative reproductible pour la CI : venv + requirements figés
python3 -m venv .venv && . .venv/bin/activate
pip install "ansible-core>=2.19,<2.20" ansible-lint
ansible-core impose une plage de Python côté contrôleur (3.11+ pour les 2.18/2.19, plus récent ensuite) et côté cibles (les vieux Python des distributions anciennes sont abandonnés progressivement). Vérifier la matrice de support de la version cible avant une montée : une CentOS 7 ou une Debian 10 peut ne plus être pilotable par une version récente.
when doit s'évaluer en booléen (une chaîne non vide n'est plus acceptée implicitement), certains usages de templates imbriqués et de {{ }} dans when lèvent désormais des erreurs. Tester les playbooks existants en préproduction avant de migrer depuis 2.18.
2.2 Collections
# requirements.yml
collections:
- name: ansible.posix
version: ">=1.6.0,<3.0.0"
- name: community.general
version: ">=10.0.0,<12.0.0"
- name: community.proxmox # contenu Proxmox extrait de community.general
- name: azure.azcollection
- name: ansible.windows
- name: community.windows
- name: community.hashi_vault
roles:
- name: geerlingguy.mysql
version: "X.Y.Z"
ansible-galaxy collection install -r requirements.yml -p ./collections
ansible-galaxy role install -r requirements.yml -p ./roles_ext
ansible-galaxy collection list
Installer les collections dans le projet (collections_path dans ansible.cfg) plutôt que dans ~/.ansible rend l'exécution reproductible entre postes et CI. Certaines collections ont leurs propres dépendances Python (azure.azcollection en a beaucoup) : les installer dans le même environnement que ansible-core.
2.3 ansible.cfg de projet
# ansible.cfg (à la racine du dépôt)
[defaults]
inventory = inventories/prod
roles_path = roles:roles_ext
collections_path = ./collections
remote_user = deploy
host_key_checking = True
interpreter_python = auto_silent
forks = 20
stdout_callback = default
callback_result_format = yaml
callbacks_enabled = ansible.posix.profile_tasks
fact_caching = jsonfile
fact_caching_connection = .cache/facts
fact_caching_timeout = 7200
retry_files_enabled = False
[privilege_escalation]
become = False
become_method = sudo
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
ansible.cfg situé dans un répertoire accessible en écriture à tous (world-writable), pour éviter qu'un tiers injecte une configuration. Sur un montage partagé ou un dossier Windows monté dans WSL, définir ANSIBLE_CONFIG explicitement. ansible-config dump --only-changed montre la configuration effective.
2.4 Execution environments et ansible-navigator
Un execution environment (EE) est une image de conteneur contenant ansible-core, les collections et leurs dépendances Python/système. C'est le format utilisé par AWX / Ansible Automation Platform, et la meilleure réponse au « ça marche sur mon poste ».
# execution-environment.yml (ansible-builder, schéma version 3)
version: 3
images:
base_image:
name: quay.io/fedora/fedora:latest # ou une image de base Python de votre choix
dependencies:
ansible_core:
package_pip: ansible-core>=2.19,<2.20
ansible_runner:
package_pip: ansible-runner
galaxy: requirements.yml
python:
- pywinrm
- hvac
system:
- openssh-clients
- sshpass
pipx install ansible-builder ansible-navigator
ansible-builder build -t registry.example.com/ops/ee-infra:1.0 -f execution-environment.yml
ansible-navigator run site.yml -i inventories/prod \
--eei registry.example.com/ops/ee-infra:1.0 --mode stdout
3. Inventaires statiques et dynamiques
3.1 INI et YAML
# inventories/prod/hosts.ini
[web]
web01.example.internal
web02.example.internal ansible_host=192.168.10.22
[db]
db01.example.internal
[debian:children]
web
db
# inventories/prod/hosts.yml (équivalent YAML, plus lisible pour les structures)
all:
children:
debian:
children:
web:
hosts:
web01.example.internal:
web02.example.internal:
ansible_host: 192.168.10.22
db:
hosts:
db01.example.internal:
windows:
hosts:
srv-app01.example.internal:
3.2 group_vars et host_vars
inventories/
├── prod/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all/
│ │ │ ├── main.yml # variables communes
│ │ │ └── vault.yml # secrets chiffrés (vault_*)
│ │ ├── web.yml
│ │ └── windows.yml
│ └── host_vars/
│ └── db01.example.internal.yml
└── staging/
└── ...
Un inventaire par environnement, chacun avec ses group_vars : l'environnement se choisit avec -i inventories/staging, sans conditionnelles when: env == 'prod' dans les rôles.
ansible-inventory -i inventories/prod --graph # arbre des groupes
ansible-inventory -i inventories/prod --host web01.example.internal # variables résolues
ansible -i inventories/prod web -m ansible.builtin.ping
3.3 Inventaire dynamique Proxmox
Le plugin interroge l'API Proxmox ; le nom du fichier doit se terminer par proxmox.yml (ou .yaml). Depuis 2025, le contenu Proxmox a été déplacé de community.general vers community.proxmox : utiliser le nom de plugin correspondant à la collection installée.
# inventories/lab/inventory.proxmox.yml
plugin: community.proxmox.proxmox # anciennement community.general.proxmox
url: https://pve01.example.internal:8006
user: ansible@pve
token_id: inventory
# token_secret : via la variable d'environnement PROXMOX_TOKEN_SECRET
validate_certs: true
want_facts: true
want_proxmox_nodes_ansible_host: false
compose:
ansible_host: proxmox_agent_interfaces[1]["ip-addresses"][0].split("/")[0]
groups:
running: proxmox_status == "running"
keyed_groups:
- key: proxmox_tags_parsed
prefix: tag
separator: "_"
filters:
- proxmox_template != 1
web, prod...) deviennent des groupes Ansible (tag_web) grâce à keyed_groups. L'adresse IP vient de l'agent invité : sans qemu-guest-agent, ansible_host reste vide. Inspecter les variables disponibles avec ansible-inventory -i inventory.proxmox.yml --list pour ajuster l'expression compose.
3.4 Inventaire dynamique Azure
# inventories/azure/inventory.azure_rm.yml
plugin: azure.azcollection.azure_rm
auth_source: auto # CLI az, identité managée ou variables AZURE_*
include_vm_resource_groups:
- rg-app-prod
plain_host_names: true
conditional_groups:
linux: "'linux' in os_profile.system"
keyed_groups:
- prefix: tag_role
key: tags.role | default('none')
- prefix: loc
key: location
hostvar_expressions:
ansible_host: private_ipv4_addresses | first
Voir Azure pour l'identité à utiliser (rôle Reader suffit pour l'inventaire).
4. Playbooks, rôles et structure
4.1 Arborescence de dépôt
infra-ansible/
├── ansible.cfg
├── requirements.yml
├── inventories/{prod,staging,lab}/
├── playbooks/
│ ├── site.yml # import_playbook de tous les autres
│ ├── baseline.yml
│ ├── web.yml
│ └── rolling-update.yml
├── roles/
│ └── baseline/
│ ├── defaults/main.yml # valeurs par défaut (faible priorité)
│ ├── vars/main.yml # constantes internes (forte priorité)
│ ├── tasks/main.yml
│ ├── handlers/main.yml
│ ├── templates/
│ ├── files/
│ ├── meta/main.yml # dépendances, plateformes
│ ├── meta/argument_specs.yml
│ └── molecule/default/
├── .ansible-lint
└── .gitlab-ci.yml
4.2 defaults ou vars
defaults/main.yml | vars/main.yml | |
|---|---|---|
| Priorité | La plus basse de toutes | Très haute (bat inventaire et play vars) |
| Destiné à | Paramètres que l'utilisateur du rôle peut surcharger | Constantes internes (noms de paquets par OS, chemins) |
| Exemple | baseline_ssh_port: 22 | _baseline_ssh_service: ssh |
Préfixer toutes les variables d'un rôle par son nom (baseline_*) : l'espace de noms des variables est global, une collision entre rôles est silencieuse.
4.3 Validation des paramètres d'un rôle
# roles/baseline/meta/argument_specs.yml
argument_specs:
main:
short_description: Socle de durcissement Debian
options:
baseline_admin_users:
type: list
elements: dict
required: true
description: Comptes administrateurs et leurs clés
baseline_ssh_port:
type: int
default: 22
baseline_allowed_cidrs:
type: list
elements: str
default: []
Le rôle échoue dès son entrée si un paramètre manque ou a le mauvais type, plutôt qu'au milieu de l'exécution.
4.4 import ou include
import_* (statique) | include_* (dynamique) | |
|---|---|---|
| Résolution | Au chargement du playbook | À l'exécution |
Boucles (loop) | Non | Oui |
when / tags | Copiés sur chaque tâche importée | S'appliquent à l'include lui-même |
--list-tasks, --start-at-task | Voient les tâches | Ne les voient pas |
| Nom de fichier variable | Non | Oui (include_tasks: "{{ ansible_os_family }}.yml") |
5. Variables et précédence
Ansible fusionne les variables de 22 sources. Une valeur définie à un niveau supérieur remplace entièrement celle d'un niveau inférieur (pas de fusion de dictionnaires par défaut). Du plus faible au plus fort :
| # | Source | Commentaire |
|---|---|---|
| 1 | Valeurs de ligne de commande (-u, etc.) | Ce ne sont pas des variables à proprement parler |
| 2 | Role defaults (defaults/main.yml) | Le niveau « surchargeable par tous » |
| 3 | Variables de groupe du fichier/script d'inventaire | |
| 4 | group_vars/all de l'inventaire | |
| 5 | group_vars/all du playbook | |
| 6 | group_vars/* de l'inventaire | Groupe enfant > groupe parent ; à niveau égal, ordre alphabétique (ou ansible_group_priority) |
| 7 | group_vars/* du playbook | |
| 8 | Variables d'hôte du fichier/script d'inventaire | |
| 9 | host_vars/* de l'inventaire | |
| 10 | host_vars/* du playbook | |
| 11 | Facts de l'hôte / set_fact mis en cache | |
| 12 | vars: du play | |
| 13 | vars_prompt du play | |
| 14 | vars_files du play | |
| 15 | Role vars (vars/main.yml) | Bat l'inventaire : piège fréquent |
| 16 | Variables de block | Portée du block |
| 17 | Variables de tâche | Portée de la tâche |
| 18 | include_vars | |
| 19 | set_fact / register | Persistent pour l'hôte jusqu'à la fin du run |
| 20 | Paramètres de rôle (roles: / include_role) | |
| 21 | Paramètres d'include | |
| 22 | Extra vars (-e) | Gagnent toujours |
defaults, spécificités d'environnement dans group_vars de l'inventaire, exceptions dans host_vars. -e réservé aux paramètres d'exécution (-e target_version=1.4.2), jamais à la configuration permanente. Pour savoir d'où vient une valeur : ansible-inventory --host et ansible.builtin.debug: var=ma_variable.
5.1 Variables magiques et facts utiles
| Variable | Contenu |
|---|---|
inventory_hostname | Nom de l'hôte dans l'inventaire |
ansible_host | Adresse de connexion réelle |
hostvars['db01'] | Variables d'un autre hôte (ses facts seulement s'ils ont été collectés ou mis en cache) |
groups['web'] | Liste des hôtes du groupe |
ansible_play_hosts | Hôtes encore actifs dans le play |
ansible_facts['distribution_major_version'] | Facts (préférer ansible_facts[...] aux variables ansible_* injectées) |
ansible_check_mode | Vrai en --check |
6. Handlers et templates Jinja2
6.1 Handlers
Un handler ne s'exécute que si une tâche qui le notifie renvoie changed, une seule fois par play même s'il est notifié plusieurs fois, et à la fin du play par défaut.
# tasks
- name: Déployer la configuration nginx
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-available/app.conf
mode: "0644"
notify: Recharger nginx
# handlers/main.yml
- name: Vérifier la configuration nginx
ansible.builtin.command: nginx -t
changed_when: false
listen: Recharger nginx
- name: Recharger nginx service
ansible.builtin.systemd_service:
name: nginx
state: reloaded
listen: Recharger nginx
listenregroupe plusieurs handlers sous un même événement ; ils s'exécutent dans l'ordre de leur définition danshandlers/, pas dans l'ordre de notification.- Si le play échoue après une notification, le handler n'est pas exécuté : la configuration est changée mais le service pas rechargé. Utiliser
force_handlers: trueau niveau du play, ou--force-handlers. - ansible.builtin.meta: flush_handlersforce l'exécution immédiate (utile avant un test de santé).
6.2 Templates Jinja2
{# templates/site.conf.j2 #}
# {{ ansible_managed }}
upstream app_backend {
{% for host in groups['app'] | sort %}
server {{ hostvars[host]['ansible_host'] | default(host) }}:{{ app_port }} max_fails=3;
{% endfor %}
}
server {
listen 443 ssl;
server_name {{ app_server_name }};
{% if app_allowed_cidrs | length > 0 %}
{% for cidr in app_allowed_cidrs %}
allow {{ cidr }};
{% endfor %}
deny all;
{% endif %}
client_max_body_size {{ app_upload_max_mb | default(20) }}m;
}
{{ ansible_managed }}en tête signale que le fichier sera écrasé.- Contrôle des espaces :
{%- ... -%}retire les blancs autour du bloc ; les réglagestrim_blocks(actif par défaut dans Ansible) etlstrip_blocksse définissent par template. - Toujours trier les listes issues de groupes ou de dictionnaires (
| sort,| dictsort) : sinon l'ordre peut varier et rendre le fichier « changé » à chaque run. - Valider le fichier avant de le mettre en place : paramètre
validate(validate: nginx -t -c %sne marche que pour un fichier de configuration complet ; pour un fragment, tester après coup via handler comme ci-dessus).
6.3 Filtres courants
"{{ users | selectattr('state', 'equalto', 'present') | map(attribute='name') | list }}"
"{{ config | combine(overrides, recursive=true) }}"
"{{ ip_cidr | ansible.utils.ipaddr('address') }}" # collection ansible.utils + netaddr
"{{ secret_value | password_hash('sha512', 'selfixe') }}" # sel fixe = idempotent
"{{ lookup('ansible.builtin.file', 'files/motd.txt') }}"
"{{ query('ansible.builtin.fileglob', 'files/keys/*.pub') }}"
"{{ some_var | mandatory('some_var doit être définie') }}"
"{{ (ansible_facts['memtotal_mb'] * 0.25) | int }}"
7. Idempotence, check et diff
Un playbook idempotent rejoué sur un système déjà conforme renvoie changed=0. C'est le critère de qualité n°1 : sans lui, impossible de distinguer une dérive réelle d'un bruit permanent, et les handlers redémarrent des services pour rien.
7.1 Rendre les commandes idempotentes
- name: Initialiser le cluster (une seule fois)
ansible.builtin.command:
cmd: /usr/local/bin/app-init --data /srv/app
creates: /srv/app/.initialized # ne s'exécute pas si le fichier existe
- name: Lire la version installée
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
changed_when: false # lecture seule
check_mode: false # s'exécute même en --check
- name: Appliquer une migration
ansible.builtin.command: /usr/local/bin/app migrate
register: migrate
changed_when: "'No migrations to apply' not in migrate.stdout"
failed_when: migrate.rc not in [0]
7.2 Hiérarchie des approches
- Module dédié (
apt,user,systemd_service,community.mysql.mysql_user...) : idempotent par construction. template/copydu fichier complet plutôt que des modifications ligne à ligne.lineinfile/blockinfilepour un fichier géré par ailleurs (paquet), avecregexpprécise.command/shellen dernier recours, aveccreates/removesouchanged_when. Préférercommand(pas de shell, pas d'injection) àshell.
7.3 Mode check et diff
ansible-playbook playbooks/web.yml --check --diff --limit web01.example.internal
--checksimule : chaque module qui supporte le check mode indique ce qu'il changerait.--diffaffiche les différences de contenu (templates, lineinfile). Penser àdiff: falsesur les tâches qui manipulent des secrets.- Limite : en check, une tâche qui dépend du résultat d'une précédente (paquet pas encore installé, donc service inexistant) peut échouer. Prévoir
when: not ansible_check_modeouignore_errors: "{{ ansible_check_mode }}"sur ces tâches précises. - Un
--check --diffplanifié sur la prod, avec alerte sichanged > 0, constitue une détection de dérive de configuration simple et efficace.
8. Boucles, conditions, blocks
8.1 Boucles
- name: Créer les comptes
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups | default([]) }}"
shell: /bin/bash
state: "{{ item.state | default('present') }}"
loop: "{{ baseline_admin_users }}"
loop_control:
label: "{{ item.name }}" # n'affiche que le nom (pas les clés) dans la sortie
- name: Clés autorisées (produit cartésien utilisateur x clé)
ansible.posix.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
loop: "{{ baseline_admin_users | subelements('ssh_keys') }}"
loop_control:
label: "{{ item.0.name }}"
- name: Installer les paquets (un seul appel, pas de boucle)
ansible.builtin.apt:
name: "{{ baseline_packages }}"
state: present
apt au lieu de 30).
8.2 Conditions
- name: Paquets spécifiques Debian 12 et plus
ansible.builtin.apt:
name: systemd-resolved
when:
- ansible_facts['os_family'] == "Debian"
- ansible_facts['distribution_major_version'] | int >= 12
- name: Variable booléenne venant de l'inventaire (chaîne possible)
ansible.builtin.debug:
msg: activé
when: feature_x_enabled | bool
{{ }} dans when (c'est déjà une expression Jinja). Une valeur "false" passée par -e est une chaîne non vide : toujours la filtrer par | bool. Depuis ansible-core 2.19, une condition qui ne produit pas un booléen lève une erreur au lieu d'être interprétée silencieusement.
8.3 block / rescue / always
- name: Mise à jour applicative avec retour arrière
block:
- name: Sauvegarder la version courante
ansible.builtin.command: cp -a /srv/app/current /srv/app/rollback
changed_when: true
- name: Déployer la nouvelle version
ansible.builtin.unarchive:
src: "https://artifacts.example.com/app/app-{{ app_version }}.tar.gz"
dest: /srv/app/current
remote_src: true
- name: Test de santé
ansible.builtin.uri:
url: "http://127.0.0.1:8080/health"
status_code: 200
register: health
retries: 10
delay: 3
until: health.status == 200
rescue:
- name: Restaurer la version précédente
ansible.builtin.command: rsync -a --delete /srv/app/rollback/ /srv/app/current/
changed_when: true
- name: Signaler l'échec (le play reste en échec)
ansible.builtin.fail:
msg: "Déploiement {{ app_version }} annulé sur {{ inventory_hostname }}"
always:
- name: Nettoyer les fichiers temporaires
ansible.builtin.file:
path: /tmp/app-deploy
state: absent
Sans le fail final, un rescue réussi « absorbe » l'erreur et l'hôte est considéré comme OK : bien choisir le comportement voulu.
8.4 Délégation et exécution unique
- name: Retirer l'hôte du répartiteur
ansible.builtin.uri:
url: "https://lb.example.internal/api/backends/{{ inventory_hostname }}/disable"
method: POST
delegate_to: localhost
- name: Migration de schéma (une seule fois pour tout le groupe)
ansible.builtin.command: /srv/app/current/bin/migrate
run_once: true
changed_when: true
9. ansible-vault et gestionnaires de secrets
9.1 ansible-vault
Chiffrement AES-256 de fichiers ou de valeurs individuelles, déchiffrés en mémoire à l'exécution.
# Fichier complet
ansible-vault create inventories/prod/group_vars/all/vault.yml
ansible-vault edit inventories/prod/group_vars/all/vault.yml
ansible-vault rekey inventories/prod/group_vars/all/vault.yml
# Valeur unique dans un fichier en clair
ansible-vault encrypt_string --vault-id prod@prompt --stdin-name db_password
# Plusieurs identités de vault (une clé par environnement)
ansible-playbook site.yml -i inventories/prod \
--vault-id prod@/usr/local/bin/vault-pass-client.sh
Convention lisible : le fichier chiffré ne contient que des variables préfixées vault_, référencées depuis un fichier en clair. Un grep retrouve ainsi où une variable est définie, sans déchiffrer.
# group_vars/all/main.yml (en clair)
db_password: "{{ vault_db_password }}"
# group_vars/all/vault.yml (chiffré)
vault_db_password: "..."
Le mot de passe du vault ne doit jamais être dans le dépôt. Un script client (--vault-id prod@script) peut le récupérer dans un gestionnaire de secrets ou le trousseau du système ; en CI, il provient d'une variable masquée et protégée.
9.2 Lire les secrets à la source
Pour les secrets qui tournent (mots de passe de base, jetons), mieux vaut les lire au moment de l'exécution dans le gestionnaire central que de les recopier dans un vault :
# HashiCorp Vault / OpenBao (collection community.hashi_vault, dépendance Python hvac)
- name: Lire le mot de passe de la base
ansible.builtin.set_fact:
db_password: "{{ lookup('community.hashi_vault.vault_kv2_get', 'prod/db',
engine_mount_point='kv',
url='https://vault.example.internal:8200',
auth_method='approle',
role_id=lookup('ansible.builtin.env', 'VAULT_ROLE_ID'),
secret_id=lookup('ansible.builtin.env', 'VAULT_SECRET_ID')).secret.password }}"
no_log: true
# Azure Key Vault (collection azure.azcollection)
- name: Lire un secret Key Vault
ansible.builtin.set_fact:
api_token: "{{ lookup('azure.azcollection.azure_keyvault_secret', 'api-token',
vault_url='https://kv-example.vault.azure.net') }}"
no_log: true
no_log: true est indispensable sur toute tâche qui manipule un secret : sans lui, la valeur apparaît en -v, dans les callbacks (AWX, journaux) et dans les messages d'erreur. Corollaire : une tâche en no_log qui échoue n'affiche plus rien ; pour déboguer, la désactiver temporairement sur un environnement de test uniquement (no_log: "{{ not debug_secrets | default(false) | bool }}").
10. Windows
| Critère | WinRM | SSH (OpenSSH Windows) |
|---|---|---|
| Disponibilité | Historique, intégré à Windows | Composant OpenSSH Server inclus dans Windows Server 2019+ ; pris en charge en tant que connexion stable dans les versions récentes d'ansible-core |
| Port | 5985 (HTTP) / 5986 (HTTPS) | 22 |
| Authentification | NTLM, Kerberos, CredSSP, certificat | Clé SSH ou mot de passe |
| Double saut (accès réseau depuis la cible) | Kerberos avec délégation ou CredSSP | Avec mot de passe oui, avec clé non |
| Dépendance côté contrôleur | pywinrm (+ pykerberos/krb5) | Aucune |
| Alternative | Connexion psrp (PowerShell Remoting, bibliothèque pypsrp), souvent plus rapide |
10.1 Variables de connexion WinRM (HTTPS + Kerberos)
# inventories/prod/group_vars/windows.yml
ansible_connection: winrm
ansible_port: 5986
ansible_winrm_scheme: https
ansible_winrm_transport: kerberos
ansible_winrm_server_cert_validation: validate
ansible_winrm_ca_trust_path: /etc/pki/ca-internal.pem
ansible_user: svc-ansible@EXAMPLE.INTERNAL
ansible_password: "{{ vault_svc_ansible_password }}"
# Variante SSH
ansible_connection: ssh
ansible_shell_type: powershell
ansible_user: EXAMPLE\svc-ansible
AllowUnencrypted, et ansible_winrm_server_cert_validation: ignore. Utiliser HTTPS avec un certificat de l'AC interne (voir PKI et certificats) et Kerberos, ou SSH par clé. Kerberos impose que le contrôleur résolve le FQDN de la cible et soit à l'heure (écart < 5 minutes).
10.2 Modules Windows
- name: Configuration de base Windows
hosts: windows
tasks:
- name: Rôles IIS
ansible.windows.win_feature:
name: [Web-Server, Web-Asp-Net45]
include_management_tools: true
register: iis
- name: Redémarrer si nécessaire
ansible.windows.win_reboot:
when: iis.reboot_required
- name: Mises à jour de sécurité
ansible.windows.win_updates:
category_names: [SecurityUpdates, CriticalUpdates]
reboot: true
reboot_timeout: 1800
- name: Clé de registre
ansible.windows.win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server
name: Enabled
data: 0
type: dword
- name: Script PowerShell idempotent (DSC si possible)
ansible.windows.win_powershell:
script: |
$svc = Get-Service -Name 'Spooler'
if ($svc.StartType -ne 'Disabled') {
Set-Service -Name 'Spooler' -StartupType Disabled
Stop-Service -Name 'Spooler' -Force
$Ansible.Changed = $true
} else {
$Ansible.Changed = $false
}
Collections utiles : ansible.windows (socle), community.windows (compléments), microsoft.ad (Active Directory). Voir aussi Windows Server.
11. Exemple : durcissement Debian
Rôle baseline : comptes administrateurs par clé, sudo, durcissement SSH par fichier déposé dans sshd_config.d, pare-feu ufw, mises à jour automatiques. Contexte : SSH, Pare-feu, Droits Linux.
# roles/baseline/defaults/main.yml
baseline_ssh_port: 22
baseline_admin_group: sshadmins
baseline_admin_users: []
# - name: alice
# ssh_keys: ["ssh-ed25519 AAAA... alice@poste"]
# - name: bob
# state: absent
baseline_allowed_ssh_cidrs:
- 192.168.10.0/24
baseline_open_ports: [] # ex. [{port: 443, proto: tcp}]
baseline_packages:
- unattended-upgrades
- apt-listchanges
- fail2ban
- ufw
- sudo
# roles/baseline/tasks/main.yml
- name: Paquets de base
ansible.builtin.apt:
name: "{{ baseline_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Groupe autorisé en SSH
ansible.builtin.group:
name: "{{ baseline_admin_group }}"
state: present
- name: Comptes administrateurs
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ [baseline_admin_group, 'sudo'] if item.state | default('present') == 'present' else omit }}"
append: true
shell: /bin/bash
password: "!" # pas de mot de passe : connexion par clé uniquement
update_password: on_create
state: "{{ item.state | default('present') }}"
remove: "{{ item.state | default('present') == 'absent' }}"
loop: "{{ baseline_admin_users }}"
loop_control:
label: "{{ item.name }}"
- name: Clés SSH (exclusives : retire toute clé non déclarée)
ansible.posix.authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_keys | join('\n') }}"
exclusive: true
loop: "{{ baseline_admin_users | selectattr('ssh_keys', 'defined') | list }}"
loop_control:
label: "{{ item.name }}"
- name: sudo sans mot de passe pour le groupe admin
ansible.builtin.copy:
dest: /etc/sudoers.d/90-sshadmins
content: "%{{ baseline_admin_group }} ALL=(ALL:ALL) NOPASSWD: ALL\n"
owner: root
group: root
mode: "0440"
validate: /usr/sbin/visudo -cf %s
- name: Durcissement SSH
ansible.builtin.template:
src: 10-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/10-hardening.conf
owner: root
group: root
mode: "0644"
notify: Redémarrer SSH
- name: Politique ufw par défaut
community.general.ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- name: SSH depuis les réseaux d'administration uniquement
community.general.ufw:
rule: allow
port: "{{ baseline_ssh_port | string }}"
proto: tcp
from_ip: "{{ item }}"
comment: ansible-ssh-admin
loop: "{{ baseline_allowed_ssh_cidrs }}"
- name: Ports applicatifs
community.general.ufw:
rule: allow
port: "{{ item.port | string }}"
proto: "{{ item.proto | default('tcp') }}"
loop: "{{ baseline_open_ports }}"
- name: Activer ufw (après les règles SSH !)
community.general.ufw:
state: enabled
- name: Mises à jour de sécurité automatiques
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
mode: "0644"
{# roles/baseline/templates/10-hardening.conf.j2 #}
# {{ ansible_managed }}
Port {{ baseline_ssh_port }}
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowGroups {{ baseline_admin_group }}
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
# roles/baseline/handlers/main.yml
- name: Valider la configuration SSH
ansible.builtin.command: /usr/sbin/sshd -t
changed_when: false
listen: Redémarrer SSH
- name: Redémarrer le service SSH
ansible.builtin.systemd_service:
name: ssh # "ssh" sur Debian/Ubuntu, "sshd" sur RHEL
state: restarted
listen: Redémarrer SSH
# playbooks/baseline.yml
- name: Socle Debian
hosts: debian
become: true
roles:
- role: baseline
- Les fichiers de
sshd_config.d/sont lus avant le reste desshd_configet c'est la première valeur rencontrée qui gagne : un fichier préfixé10-l'emporte sur un50-cloud-init.confqui réactivePasswordAuthentication. - Tester d'abord avec
--limitsur un hôte, en gardant une session SSH ouverte, et vérifier que le compte utilisé par Ansible (remote_user) fait partie deAllowGroups. - Les règles ufw autorisant SSH doivent être posées avant
state: enabled. Si le port SSH change, ouvrir le nouveau port avant de modifierPort, et mettre à jouransible_port. - Sur Debian 12+/Ubuntu récents,
ssh.socketpeut piloter l'écoute : un changement de port demande alors de recharger systemd et le socket.
12. Exemple : déploiement LAMP / PHP
Apache + PHP-FPM sur le groupe web, MariaDB sur db, base et compte dédiés par application. Voir Apache pour la configuration détaillée du serveur web.
# inventories/prod/group_vars/all/main.yml
app_name: shop
app_domain: shop.example.com
app_db_name: shop
app_db_user: shop_app
app_db_password: "{{ vault_app_db_password }}"
app_db_host: db01.example.internal
php_version: "8.2" # version fournie par la distribution (Debian 12)
# playbooks/lamp.yml
- name: Base de données
hosts: db
become: true
tasks:
- name: MariaDB et bibliothèque Python pour les modules
ansible.builtin.apt:
name: [mariadb-server, python3-pymysql]
state: present
- name: Écouter sur l'interface privée
ansible.builtin.copy:
dest: /etc/mysql/mariadb.conf.d/60-bind.cnf
content: |
[mysqld]
bind-address = {{ ansible_host }}
mode: "0644"
notify: Redémarrer MariaDB
- name: Base applicative
community.mysql.mysql_db:
name: "{{ app_db_name }}"
encoding: utf8mb4
collation: utf8mb4_unicode_ci
login_unix_socket: /run/mysqld/mysqld.sock
- name: Compte applicatif limité aux serveurs web
community.mysql.mysql_user:
name: "{{ app_db_user }}"
host: "{{ hostvars[item]['ansible_host'] | default(item) }}"
password: "{{ app_db_password }}"
priv: "{{ app_db_name }}.*:SELECT,INSERT,UPDATE,DELETE,CREATE,ALTER,INDEX,DROP"
login_unix_socket: /run/mysqld/mysqld.sock
loop: "{{ groups['web'] }}"
no_log: true
handlers:
- name: Redémarrer MariaDB
ansible.builtin.systemd_service:
name: mariadb
state: restarted
- name: Serveurs web
hosts: web
become: true
tasks:
- name: Apache et PHP-FPM
ansible.builtin.apt:
name:
- apache2
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-intl"
- "php{{ php_version }}-mbstring"
- "php{{ php_version }}-xml"
- "php{{ php_version }}-opcache"
state: present
- name: Modules Apache
community.general.apache2_module:
name: "{{ item }}"
state: present
loop: [proxy_fcgi, setenvif, rewrite, headers, ssl, http2]
notify: Recharger Apache
- name: Configuration PHP-FPM d'Apache
ansible.builtin.command: "a2enconf php{{ php_version }}-fpm"
args:
creates: "/etc/apache2/conf-enabled/php{{ php_version }}-fpm.conf"
notify: Recharger Apache
- name: Réglages PHP de production
ansible.builtin.copy:
dest: "/etc/php/{{ php_version }}/fpm/conf.d/90-prod.ini"
content: |
expose_php = Off
display_errors = Off
log_errors = On
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
opcache.validate_timestamps = 0
mode: "0644"
notify: Redémarrer PHP-FPM
- name: Arborescence applicative
ansible.builtin.file:
path: "/srv/{{ app_name }}/{{ item }}"
state: directory
owner: root
group: www-data
mode: "0750"
loop: [releases, shared]
- name: Fichier de configuration de l'application (hors racine web)
ansible.builtin.template:
src: app-config.php.j2
dest: "/srv/{{ app_name }}/shared/config.php"
owner: root
group: www-data
mode: "0640"
no_log: true
- name: Virtual host
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/apache2/sites-available/{{ app_name }}.conf"
mode: "0644"
notify: Recharger Apache
- name: Activer le site
ansible.builtin.command: "a2ensite {{ app_name }}"
args:
creates: "/etc/apache2/sites-enabled/{{ app_name }}.conf"
notify: Recharger Apache
handlers:
- name: Valider Apache
ansible.builtin.command: apache2ctl configtest
changed_when: false
listen: Recharger Apache
- name: Recharger le service Apache
ansible.builtin.systemd_service:
name: apache2
state: reloaded
listen: Recharger Apache
- name: Redémarrer PHP-FPM
ansible.builtin.systemd_service:
name: "php{{ php_version }}-fpm"
state: restarted
app-config.php.j2 produit un fichier PHP ; c'est un fichier Jinja ordinaire côté Ansible, il suffit d'échapper correctement les valeurs insérées (filtre to_json pour obtenir une chaîne littérale sûre). Le déploiement du code se fait en releases horodatées + lien symbolique current, ce qui rend le retour arrière instantané (section 8.3). Avec opcache.validate_timestamps = 0, un rechargement de PHP-FPM est nécessaire après chaque déploiement de code.
13. Mises à jour en rolling
Objectif : patcher un groupe de serveurs derrière un répartiteur sans interruption de service, par lots, en s'arrêtant dès qu'un lot échoue.
# playbooks/rolling-update.yml
- name: Mise à jour en rolling des serveurs web
hosts: web
become: true
serial:
- 1 # premier lot : un canari
- "25%" # puis par quarts
max_fail_percentage: 0 # un seul échec dans un lot arrête tout
order: sorted
pre_tasks:
- name: Vérifier qu'il reste assez de nœuds actifs
ansible.builtin.assert:
that: (groups['web'] | length) >= 3
fail_msg: "Pas assez de serveurs pour un rolling sans coupure"
run_once: true
- name: Retirer du répartiteur (HAProxy via socket admin)
community.general.haproxy:
state: disabled
host: "{{ inventory_hostname_short }}"
backend: be_web
socket: /run/haproxy/admin.sock
wait: true
drain: true
delegate_to: "{{ item }}"
loop: "{{ groups['lb'] }}"
tasks:
- name: Mise à jour des paquets
ansible.builtin.apt:
upgrade: dist
update_cache: true
autoremove: true
register: upgrade
- name: Redémarrage requis ?
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_flag
- name: Redémarrer si nécessaire
ansible.builtin.reboot:
reboot_timeout: 600
post_reboot_delay: 10
when: reboot_flag.stat.exists
- name: Attendre l'application
ansible.builtin.uri:
url: "http://{{ ansible_host }}/health"
status_code: 200
register: health
retries: 20
delay: 5
until: health.status == 200
post_tasks:
- name: Remettre dans le répartiteur
community.general.haproxy:
state: enabled
host: "{{ inventory_hostname_short }}"
backend: be_web
socket: /run/haproxy/admin.sock
wait: true
delegate_to: "{{ item }}"
loop: "{{ groups['lb'] }}"
| Paramètre | Effet |
|---|---|
serial: N / "N%" / liste | Taille des lots ; une liste permet un canari puis une montée progressive |
max_fail_percentage | Pourcentage d'échecs toléré par lot ; il faut le dépasser strictement pour arrêter |
any_errors_fatal: true | Le moindre échec arrête immédiatement tout le play pour tous les hôtes |
throttle: N (tâche) | Limite le parallélisme d'une seule tâche (appel à une API fragile) |
order | Ordre de traitement : inventory, sorted, reverse_sorted, shuffle |
serial: 1 et une vérification d'état du cluster (quorum, réplication à jour) en pre_tasks et post_tasks, avec until. Ne jamais redémarrer le nœud suivant tant que le précédent n'a pas réintégré le cluster. Voir PostgreSQL pour l'ordre primaire / réplicas.
14. Tests : Molecule et ansible-lint
14.1 ansible-lint
# .ansible-lint
profile: production # min, basic, moderate, safety, shared, production
exclude_paths:
- collections/
- roles_ext/
- .cache/
warn_list:
- experimental
skip_list: []
enable_list:
- no-same-owner
ansible-lint # tout le dépôt
ansible-lint playbooks/baseline.yml
ansible-lint --fix # corrige automatiquement FQCN, YAML, etc.
Règles qui évitent de vrais incidents : fqcn (nom complet des modules), no-changed-when (commande sans changed_when), risky-file-permissions (fichier créé sans mode), command-instead-of-module, no-free-form, var-naming. Une exception se justifie en ligne : # noqa: no-changed-when.
14.2 Molecule
Molecule crée des instances jetables (conteneurs ou VM), applique le rôle, vérifie l'idempotence (second passage avec changed=0 exigé) puis exécute des vérifications.
pipx inject ansible-core molecule "molecule-plugins[docker]"
cd roles/baseline
molecule init scenario default --driver-name docker
# roles/baseline/molecule/default/molecule.yml
driver:
name: docker
platforms:
- name: debian12
image: geerlingguy/docker-debian12-ansible:latest # image avec systemd
command: ""
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
pre_build_image: true
provisioner:
name: ansible
inventory:
host_vars:
debian12:
baseline_admin_users:
- name: testadmin
ssh_keys: ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExempleDeCleFactice test"]
verifier:
name: ansible
# roles/baseline/molecule/default/verify.yml
- name: Vérifications
hosts: all
become: true
tasks:
- name: Lire la configuration SSH effective
ansible.builtin.command: sshd -T
register: sshd_t
changed_when: false
- name: Root et mots de passe interdits
ansible.builtin.assert:
that:
- "'permitrootlogin no' in sshd_t.stdout"
- "'passwordauthentication no' in sshd_t.stdout"
- name: Le compte de test existe
ansible.builtin.getent:
database: passwd
key: testadmin
molecule test # create, converge, idempotence, verify, destroy
molecule converge # itération rapide pendant le développement
molecule login # shell dans l'instance pour déboguer
molecule init role n'existe plus : créer le rôle avec ansible-galaxy role init puis ajouter le scénario.
14.3 Intégration continue
# .gitlab-ci.yml (extrait)
lint:
image: ${EE_IMAGE} # même execution environment qu'en production
script:
- ansible-lint
- ansible-playbook playbooks/site.yml --syntax-check -i inventories/staging
check-prod:
image: ${EE_IMAGE}
script:
- ansible-playbook playbooks/site.yml -i inventories/prod --check --diff
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" # détection de dérive nocturne
Voir CI/CD pour la gestion des runners et des variables protégées.
15. Performances et exécution à l'échelle
| Levier | Gain | Précaution |
|---|---|---|
forks (5 par défaut) | Parallélisme entre hôtes ; 20 à 50 courant | CPU/RAM du contrôleur, connexions simultanées vers un bastion |
pipelining = True | Envoie le module par stdin au lieu de copier un fichier : divise fortement le nombre d'opérations SSH | Incompatible avec requiretty dans sudoers (absent par défaut sur les distributions récentes) |
ControlPersist | Réutilise la connexion SSH entre tâches | Chemin du socket trop long : ajuster control_path_dir |
gather_facts: false / gather_subset | Supprime ou réduit la collecte (plusieurs secondes par hôte) | Vérifier que les tâches n'utilisent pas les facts |
Cache de facts (jsonfile, redis) | Réutilise les facts entre runs, rend hostvars d'autres hôtes disponibles | Facts potentiellement obsolètes (fact_caching_timeout) |
strategy: free | Chaque hôte avance à son rythme | Sortie entrelacée ; incompatible avec une logique qui suppose la synchronisation |
async + poll: 0 | Tâches longues en arrière-plan, suivies par async_status | Pas avec les modules qui nécessitent une interaction |
| Listes au lieu de boucles | Un appel de module au lieu de N | Possible seulement si le module accepte une liste |
Callback profile_tasks | Identifie les tâches lentes | Diagnostic seulement |
mitogen_linear a longtemps été vantée pour ses gains de vitesse, mais elle accuse régulièrement du retard sur les nouvelles versions d'ansible-core, qu'elle modifie en profondeur. À éviter en production ; si vous l'utilisez, vérifier la compatibilité exacte avec votre version d'ansible-core à chaque montée, et pouvoir revenir à linear sans changer les playbooks. Le couple pipelining + ControlPersist + forks couvre la plupart des besoins.
15.1 AWX, Ansible Automation Platform, Semaphore
| Outil | Points forts | Points d'attention |
|---|---|---|
| AWX | Projet amont d'AAP : RBAC, inventaires et credentials centralisés, planification, workflows, journal d'audit, API | Déploiement via opérateur Kubernetes ; projet en profonde refonte depuis 2024, rythme de publication irrégulier : évaluer la maintenance avant d'en dépendre |
| Ansible Automation Platform (Red Hat) | Version supportée d'AWX + hub de contenus certifiés + EDA (automatisation événementielle) | Souscription |
| Semaphore UI | Léger (binaire Go + base de données), installation simple, prend aussi en charge Terraform/OpenTofu et scripts | Moins de fonctionnalités d'entreprise qu'AWX |
| CI (GitLab, etc.) | Aucun composant de plus, exécution versionnée avec le code | Pas de RBAC fin par inventaire, journal d'audit limité aux pipelines |
Dans tous les cas : exécuter dans un execution environment versionné, stocker les identifiants dans l'outil (ou un gestionnaire de secrets), jamais dans le dépôt, et limiter qui peut lancer un job sur l'inventaire de production.
16. Dépannage et checklist
16.1 Outils de diagnostic
ansible-playbook site.yml -vvvv # commandes SSH exactes, chemins, become
ansible-playbook site.yml --syntax-check
ansible-playbook site.yml --list-hosts --list-tasks --list-tags
ansible-playbook site.yml --step # confirmation tâche par tâche
ansible-playbook site.yml --start-at-task "Durcissement SSH" --limit web01.example.internal
ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook ... # garde le module sur la cible pour l'exécuter à la main
ansible all -m ansible.builtin.setup -a "filter=ansible_distribution*" --limit web01.example.internal
ansible-config dump --only-changed
ansible-doc community.general.ufw # documentation hors ligne d'un module
Débogueur interactif : debugger: on_failed sur un play ou une tâche (ou ANSIBLE_ENABLE_TASK_DEBUGGER=True) ouvre une console qui permet d'inspecter task.args et les variables, de corriger, puis de rejouer la tâche (redo).
16.2 Erreurs courantes
| Symptôme | Cause | Correction |
|---|---|---|
| UNREACHABLE ... Permission denied (publickey) | Mauvais remote_user, clé absente de l'agent, ou AllowGroups trop restrictif | -vvvv pour voir la commande SSH ; tester la même commande à la main |
| Host key verification failed | Hôte recréé (nouvelle clé) ou inconnu | ssh-keygen -R hôte puis ssh-keyscan vérifié ; ne pas désactiver host_key_checking en prod |
| Missing sudo password | sudo exige un mot de passe | -K, ou ansible_become_password (vault), ou règle NOPASSWD dédiée |
| Timeout (12s) waiting for privilege escalation prompt | sudo lent (résolution DNS du nom d'hôte, LDAP), prompt inattendu | Corriger /etc/hosts / NSS ; augmenter timeout ; vérifier become_method |
| Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user | become_user non root sans ACL sur la cible | Installer le paquet acl, ou activer pipelining |
| /usr/bin/python: not found ou No module named ... | Interpréteur absent ou module Python manquant côté cible | ansible_python_interpreter=/usr/bin/python3 ; installer la dépendance (python3-pymysql...) ; bootstrap avec ansible.builtin.raw: apt-get install -y python3 |
| Avertissement de découverte d'interpréteur | Plusieurs Python sur la cible | interpreter_python = auto_silent ou valeur explicite par groupe |
| couldn't resolve module/action | Collection absente ou non trouvée (mauvais collections_path) | ansible-galaxy collection list ; installer depuis requirements.yml |
| The conditional check ... failed / erreurs de templating après montée en 2.19 | Condition non booléenne, {{ }} dans when, template imbriqué | Ajouter | bool, retirer les accolades, simplifier l'expression |
| 'dict object' has no attribute ... | Variable non définie pour cet hôte ou fact non collecté | | default(...), vérifier la précédence (section 5) et gather_facts |
| Une tâche est toujours changed | command/shell sans changed_when, liste non triée dans un template, horodatage dans un fichier | Voir section 7 |
| Handler jamais exécuté | Nom notifié différent (casse, espaces), play en échec avant la fin, tâche non changed | Utiliser listen, force_handlers |
| WinRM ntlm: HTTP 401 / Kerberos: ... not found in Kerberos database | Transport non activé, nom court au lieu du FQDN, horloge décalée | Vérifier winrm get winrm/config/service, utiliser le FQDN, NTP |
| WinRM certificate verify failed | Certificat d'écoute auto-signé ou AC interne inconnue | ansible_winrm_ca_trust_path vers l'AC interne |
16.3 Checklist de production
Socle
ansible-coreet collections épinglés (requirements.yml), idéalement dans un execution environment versionné.ansible.cfgde projet versionné,host_key_checkingactif,pipeliningetControlPersistconfigurés.- Un inventaire par environnement ; aucune logique
when: env == ...dans les rôles. - Compte de service dédié sur les cibles, clé SSH dédiée, sudo limité et journalisé.
Code
- FQCN partout, variables de rôle préfixées,
argument_specssur les rôles partagés. - Second passage à
changed=0(vérifié par Molecule) ;changed_whensur toute commande. validatesur les fichiers critiques (sudoers, sshd, nginx/apache via handler),modeexplicite sur chaque fichier.ansible-lintau profilproductionen CI,--syntax-check,--check --diffplanifié pour la dérive.
Sécurité
- Aucun secret en clair dans Git ; vault par environnement ou lecture dans le gestionnaire de secrets ; mot de passe du vault hors dépôt.
no_log: truesur toutes les tâches manipulant des secrets ;diff: falsesur les templates qui en contiennent.- WinRM en HTTPS + Kerberos (ou SSH), certificats validés.
- Exécutions de production traçables (AWX/Semaphore/CI), droits de lancement restreints.
Exploitation
- Changements risqués testés d'abord avec
--limitsur un hôte, en--check --diff. - Rolling avec
serial, canari,max_fail_percentage, retrait du répartiteur et test de santé. - Procédure de retour arrière documentée (block/rescue, releases horodatées).
- Montées de version d'ansible-core testées en recette, notes de version lues (changements de templating, Python supporté).