🛠 Outils IT
🛠 102 outils 📚 53 docs
🤖 Assistant ☀

Ansible - Automatisation et gestion de configuration

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.

ComposantRôle
Nœud de contrôleLinux/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
InventaireListe des hôtes, groupes et variables (fichier ou plugin dynamique)
ModuleUnité d'action idempotente (ansible.builtin.copy, community.general.ufw...)
PluginExtension côté contrôleur : connexion, inventaire, lookup, filtre, callback, cache
CollectionPaquet de distribution (modules, plugins, rôles) avec FQCN namespace.collection.module
Playbook / play / taskFichier YAML / association hôtes-tâches / appel d'un module

1.1 Déroulement d'un play

  1. Résolution de l'inventaire et des variables.
  2. Collecte des facts (module setup) si gather_facts est actif.
  3. Pour chaque tâche : exécution en parallèle sur forks hôtes (5 par défaut), stratégie linear : tous les hôtes terminent la tâche N avant de passer à N+1.
  4. Les hôtes en échec sont retirés du reste du play.
  5. Les handlers notifiés sont exécutés en fin de play (ou à un meta: flush_handlers).
CritèreAnsibleTerraform / OpenTofu
DomaineConfiguration des systèmes, orchestration d'opérationsCycle de vie de l'infrastructure (créer/détruire)
ÉtatAucun : l'état est constaté sur la cible à chaque exécutionFichier d'état central
SuppressionExplicite (state: absent)Implicite (retirer le code)
OrdreSéquentiel, écrit par l'auteurGraphe 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 collection ansible.builtin uniquement. 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
Version de Python : chaque version d'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.
ansible-core 2.19 : cette version a introduit une refonte du moteur de templating (« data tagging ») plus stricte : une condition 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
Sécurité : Ansible ignore un 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
Astuce : les tags Proxmox (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.ymlvars/main.yml
PrioritéLa plus basse de toutesTrès haute (bat inventaire et play vars)
Destiné àParamètres que l'utilisateur du rôle peut surchargerConstantes internes (noms de paquets par OS, chemins)
Exemplebaseline_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ésolutionAu chargement du playbookÀ l'exécution
Boucles (loop)NonOui
when / tagsCopiés sur chaque tâche importéeS'appliquent à l'include lui-même
--list-tasks, --start-at-taskVoient les tâchesNe les voient pas
Nom de fichier variableNonOui (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 :

#SourceCommentaire
1Valeurs de ligne de commande (-u, etc.)Ce ne sont pas des variables à proprement parler
2Role defaults (defaults/main.yml)Le niveau « surchargeable par tous »
3Variables de groupe du fichier/script d'inventaire
4group_vars/all de l'inventaire
5group_vars/all du playbook
6group_vars/* de l'inventaireGroupe enfant > groupe parent ; à niveau égal, ordre alphabétique (ou ansible_group_priority)
7group_vars/* du playbook
8Variables d'hôte du fichier/script d'inventaire
9host_vars/* de l'inventaire
10host_vars/* du playbook
11Facts de l'hôte / set_fact mis en cache
12vars: du play
13vars_prompt du play
14vars_files du play
15Role vars (vars/main.yml)Bat l'inventaire : piège fréquent
16Variables de blockPortée du block
17Variables de tâchePortée de la tâche
18include_vars
19set_fact / registerPersistent pour l'hôte jusqu'à la fin du run
20Paramètres de rôle (roles: / include_role)
21Paramètres d'include
22Extra vars (-e)Gagnent toujours
Règle pratique : placer chaque variable à un seul endroit. Valeurs par défaut dans 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

VariableContenu
inventory_hostnameNom de l'hôte dans l'inventaire
ansible_hostAdresse 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_hostsHôtes encore actifs dans le play
ansible_facts['distribution_major_version']Facts (préférer ansible_facts[...] aux variables ansible_* injectées)
ansible_check_modeVrai 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
  • listen regroupe plusieurs handlers sous un même événement ; ils s'exécutent dans l'ordre de leur définition dans handlers/, 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: true au niveau du play, ou --force-handlers.
  • - ansible.builtin.meta: flush_handlers force 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églages trim_blocks (actif par défaut dans Ansible) et lstrip_blocks se 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 %s ne 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

  1. Module dédié (apt, user, systemd_service, community.mysql.mysql_user...) : idempotent par construction.
  2. template / copy du fichier complet plutôt que des modifications ligne à ligne.
  3. lineinfile / blockinfile pour un fichier géré par ailleurs (paquet), avec regexp précise.
  4. command/shell en dernier recours, avec creates/removes ou changed_when. Préférer command (pas de shell, pas d'injection) à shell.

7.3 Mode check et diff

ansible-playbook playbooks/web.yml --check --diff --limit web01.example.internal
  • --check simule : chaque module qui supporte le check mode indique ce qu'il changerait.
  • --diff affiche les différences de contenu (templates, lineinfile). Penser à diff: false sur 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_mode ou ignore_errors: "{{ ansible_check_mode }}" sur ces tâches précises.
  • Un --check --diff planifié sur la prod, avec alerte si changed > 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
Performance : les modules de paquets acceptent une liste : passer la liste en un seul appel plutôt que boucler (un appel 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
Piège : pas de {{ }} 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èreWinRMSSH (OpenSSH Windows)
DisponibilitéHistorique, intégré à WindowsComposant OpenSSH Server inclus dans Windows Server 2019+ ; pris en charge en tant que connexion stable dans les versions récentes d'ansible-core
Port5985 (HTTP) / 5986 (HTTPS)22
AuthentificationNTLM, Kerberos, CredSSP, certificatClé SSH ou mot de passe
Double saut (accès réseau depuis la cible)Kerberos avec délégation ou CredSSPAvec mot de passe oui, avec clé non
Dépendance côté contrôleurpywinrm (+ pykerberos/krb5)Aucune
AlternativeConnexion 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
À éviter en production : WinRM en HTTP avec authentification Basic et 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
Ne pas se couper l'accès :
  • Les fichiers de sshd_config.d/ sont lus avant le reste de sshd_config et c'est la première valeur rencontrée qui gagne : un fichier préfixé 10- l'emporte sur un 50-cloud-init.conf qui réactive PasswordAuthentication.
  • Tester d'abord avec --limit sur un hôte, en gardant une session SSH ouverte, et vérifier que le compte utilisé par Ansible (remote_user) fait partie de AllowGroups.
  • Les règles ufw autorisant SSH doivent être posées avant state: enabled. Si le port SSH change, ouvrir le nouveau port avant de modifier Port, et mettre à jour ansible_port.
  • Sur Debian 12+/Ubuntu récents, ssh.socket peut 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
Remarques : le template 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ètreEffet
serial: N / "N%" / listeTaille des lots ; une liste permet un canari puis une montée progressive
max_fail_percentagePourcentage d'échecs toléré par lot ; il faut le dépasser strictement pour arrêter
any_errors_fatal: trueLe 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)
orderOrdre de traitement : inventory, sorted, reverse_sorted, shuffle
Bases de données et clusters : pour un cluster (Galera, Patroni, Proxmox), 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
Limites : un conteneur n'est pas une VM (pas de noyau propre, ufw/iptables et certains services se comportent différemment). Pour un rôle réseau ou noyau, utiliser un driver VM (Vagrant, ou un scénario « delegated » qui clone une VM Proxmox) ou exécuter en complément le rôle sur un environnement de recette. Depuis Molecule 6, 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

LevierGainPrécaution
forks (5 par défaut)Parallélisme entre hôtes ; 20 à 50 courantCPU/RAM du contrôleur, connexions simultanées vers un bastion
pipelining = TrueEnvoie le module par stdin au lieu de copier un fichier : divise fortement le nombre d'opérations SSHIncompatible avec requiretty dans sudoers (absent par défaut sur les distributions récentes)
ControlPersistRéutilise la connexion SSH entre tâchesChemin du socket trop long : ajuster control_path_dir
gather_facts: false / gather_subsetSupprime 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 disponiblesFacts potentiellement obsolètes (fact_caching_timeout)
strategy: freeChaque hôte avance à son rythmeSortie entrelacée ; incompatible avec une logique qui suppose la synchronisation
async + poll: 0Tâches longues en arrière-plan, suivies par async_statusPas avec les modules qui nécessitent une interaction
Listes au lieu de bouclesUn appel de module au lieu de NPossible seulement si le module accepte une liste
Callback profile_tasksIdentifie les tâches lentesDiagnostic seulement
Mitogen : la stratégie 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

OutilPoints fortsPoints d'attention
AWXProjet amont d'AAP : RBAC, inventaires et credentials centralisés, planification, workflows, journal d'audit, APIDé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 UILéger (binaire Go + base de données), installation simple, prend aussi en charge Terraform/OpenTofu et scriptsMoins de fonctionnalités d'entreprise qu'AWX
CI (GitLab, etc.)Aucun composant de plus, exécution versionnée avec le codePas 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ômeCauseCorrection
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 failedHôte recréé (nouvelle clé) ou inconnussh-keygen -R hôte puis ssh-keyscan vérifié ; ne pas désactiver host_key_checking en prod
Missing sudo passwordsudo exige un mot de passe-K, ou ansible_become_password (vault), ou règle NOPASSWD dédiée
Timeout (12s) waiting for privilege escalation promptsudo lent (résolution DNS du nom d'hôte, LDAP), prompt inattenduCorriger /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 userbecome_user non root sans ACL sur la cibleInstaller le paquet acl, ou activer pipelining
/usr/bin/python: not found ou No module named ...Interpréteur absent ou module Python manquant côté cibleansible_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éteurPlusieurs Python sur la cibleinterpreter_python = auto_silent ou valeur explicite par groupe
couldn't resolve module/actionCollection 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.19Condition 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 changedcommand/shell sans changed_when, liste non triée dans un template, horodatage dans un fichierVoir section 7
Handler jamais exécutéNom notifié différent (casse, espaces), play en échec avant la fin, tâche non changedUtiliser listen, force_handlers
WinRM ntlm: HTTP 401 / Kerberos: ... not found in Kerberos databaseTransport non activé, nom court au lieu du FQDN, horloge décaléeVérifier winrm get winrm/config/service, utiliser le FQDN, NTP
WinRM certificate verify failedCertificat d'écoute auto-signé ou AC interne inconnueansible_winrm_ca_trust_path vers l'AC interne

16.3 Checklist de production

Socle

  • ansible-core et collections épinglés (requirements.yml), idéalement dans un execution environment versionné.
  • ansible.cfg de projet versionné, host_key_checking actif, pipelining et ControlPersist configuré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_specs sur les rôles partagés.
  • Second passage à changed=0 (vérifié par Molecule) ; changed_when sur toute commande.
  • validate sur les fichiers critiques (sudoers, sshd, nginx/apache via handler), mode explicite sur chaque fichier.
  • ansible-lint au profil production en CI, --syntax-check, --check --diff planifié 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: true sur toutes les tâches manipulant des secrets ; diff: false sur 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 --limit sur 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é).
Pour aller plus loin : Terraform / OpenTofu pour créer les machines, Linux Server et systemd pour les réglages système que les rôles automatisent, Git pour le flux de revue des playbooks.