Fullmoon System

Playbooks Ansible en pratique : inventaire, idempotence, Vault et déploiements progressifs

EdwardMoon

Les playbooks Ansible gèrent déclarativement paquets, fichiers de configuration et états des services sur les serveurs Linux, réduisant oublis et écarts entre environnements. Mais lancer toutes les cibles après un simple ping réussi, abuser de command/shell ou placer les mots de passe en YAML peut accélérer la propagation des pannes et l'exposition des secrets.

Ce guide ne dépend ni d'une distribution unique ni d'une version Ansible obsolète. Il combine environnements Python isolés, inventaires YAML, FQCN ansible.builtin, idempotence, handlers, modes check/diff, Vault, serial et block/rescue dans une procédure vérifiable. Consultez la documentation des versions d'ansible-core et des collections installées avant application.

Automatisation Ansible : inventaire, contrôleur, SSH, groupes de serveurs, validation et récupération
Valider inventaire et tâches déclaratives sur le contrôleur, les appliquer progressivement par SSH selon l'environnement, puis vérifier les résultats

Composants essentiels d'un playbook

Composant Rôle Pratique d'exploitation
Inventory Hôtes cibles, groupes et variables de connexion Séparer dev, stage et prod et limiter les variables dupliquées
Play Groupe cible et politique d'exécution Préciser hosts, become, serial et la politique d'échec
Task Un appel de module Utiliser un nom, un FQCN et un état souhaité explicite
Module Opérations sur les paquets, fichiers, services, etc. Privilégier les modules dédiés à command ou shell
Handler Action complémentaire déclenchée par une modification Redémarrer les services après validation de la configuration
Role Tâches, handlers, modèles et valeurs par défaut réutilisables Limiter les responsabilités et documenter les variables d'interface
Collection Unité de distribution des modules, plugins et rôles Fixer les versions testées et examiner les changements
Dans les résultats Ansible, ok indique que l'état souhaité était déjà atteint, changed qu'une modification a eu lieu et failed un échec. Pour vérifier l'idempotence, contrôlez que les exécutions suivantes produisent changed=0 et déclenchent uniquement les handlers attendus ; la seule réussite ne suffit pas.

Préparer l'environnement d'exécution

Créez un venv de projet plutôt que d'ajouter des paquets au Python système du contrôleur. Consignez les versions d'ansible-core et Python, les fichiers de configuration et les chemins des collections pour reproduire l'automatisation.

Installer Ansible dans un venv Python

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core ansible-lint

ansible --version
ansible-playbook --version
ansible-lint --version
python -m pip freeze > requirements-lock.txt

En production, fixez les versions validées d'ansible-core et des bibliothèques via un fichier de verrouillage ou un dépôt approuvé. Installer systématiquement la dernière version compromet la reproductibilité si la compatibilité des collections ou les prérequis Python changent.

Organisation des répertoires

install -d inventories/dev/group_vars/all inventories/prod/group_vars/all
install -d roles/web/{tasks,handlers,templates,defaults}
install -d playbooks/templates

find . -maxdepth 3 -type d | sort

Valeurs sûres dans ansible.cfg

[defaults]
inventory = inventories/dev/hosts.yml
roles_path = roles
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent
forks = 10
timeout = 15

[privilege_escalation]
become = False
become_ask_pass = True

Désactiver host_key_checking empêche de détecter une interception. Vérifiez les empreintes des clés hôtes SSH par un canal fiable avant de les enregistrer dans known_hosts. Activez become uniquement pour les plays ou tâches nécessaires et excluez les mots de passe de la configuration.

Créer un inventaire YAML

Les noms de groupes doivent refléter les rôles et domaines de panne ; ansible_host contient l'adresse de connexion réelle. Séparer les sources d'inventaire de production et de développement réduit les erreurs de ciblage par rapport à de simples tags dans un fichier commun.

inventories/dev/hosts.yml

all:
  children:
    web:
      hosts:
        dev-web-01:
          ansible_host: 192.0.2.11
        dev-web-02:
          ansible_host: 192.0.2.12
    database:
      hosts:
        dev-db-01:
          ansible_host: 192.0.2.21
  vars:
    ansible_user: automation
    ansible_become: true

Valider la syntaxe de l'inventaire et les cibles

ansible-inventory -i inventories/dev/hosts.yml --graph
ansible-inventory -i inventories/dev/hosts.yml --list | jq .

ansible web   -i inventories/dev/hosts.yml   -m ansible.builtin.ping   --limit dev-web-01
ansible.builtin.ping vérifie l'exécution distante de Python et la connexion Ansible ; ce n'est pas un ping ICMP. Sa réussite confirme le fonctionnement de SSH, de Python et des autorisations, mais pas la disponibilité des dépôts, de l'espace disque ou des dépendances du service.

Écrire un premier playbook avec handlers

Le play suivant déclare les paquets, modèles de configuration et états des services. Chaque tâche possède un nom lisible et un FQCN. Le modèle ne notifie le handler que si la configuration produite change, évitant les redémarrages inutiles.

playbooks/web.yml

---
- name: Configure web servers
  hosts: web
  become: true
  gather_facts: true

  tasks:
    - name: Ensure Nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Render Nginx virtual host
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
        validate: '/usr/sbin/nginx -t -c %s'
      notify: Restart Nginx

    - name: Ensure Nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        enabled: true
        state: started

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Cet exemple gère tout /etc/nginx/nginx.conf sur un nouvel hôte de laboratoire. Son modèle inclut events et http pour que nginx -t -c %s valide le fichier temporaire comme configuration principale autonome. Ne l'appliquez pas tel quel en production : remplacer le fichier principal peut supprimer d'autres hôtes virtuels. Un rôle déployant uniquement un fragment vhost nécessite une validation distincte assemblant la configuration complète.

Exemple de modèle Jinja

Enregistrez ce contenu sous playbooks/templates/nginx.conf.j2. Placez les variables dans inventories/dev/group_vars/all/app.yml comme indiqué et faites pointer app_port vers une application réellement active.

app_server_name: app.example.internal
app_port: 8080
events {
    worker_connections 1024;
}

http {
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    server {
        listen 80;
        server_name {{ app_server_name }};

        location /healthz {
            access_log off;
            return 200 "ok\n";
        }
        location / {
            proxy_pass http://127.0.0.1:{{ app_port }};
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Validation préalable : syntaxe, check et diff

Après validation syntaxique, exécutez check sur un seul hôte de développement et examinez le diff. Ce mode simule les changements, mais tous les modules ne le prennent pas en charge. Les plays dont les conditions dépendent de valeurs créées par des tâches précédentes peuvent différer d'une exécution réelle.

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --syntax-check

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --check --diff   --limit dev-web-01

Le diff peut révéler des secrets en montrant la configuration avant et après. Envisagez diff: false et no_log: true pour les modèles sensibles et limitez l'accès et la rétention des journaux CI. Après validation, conservez --limit pour l'exécution réelle, puis élargissez un groupe à la fois.

ansible-lint playbooks/web.yml

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --limit dev-web-01

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --check --limit dev-web-01

Rendre les playbooks idempotents

Approche fragile Approche recommandée Motif
shell: echo >> file lineinfile·blockinfile·template Évite les doublons lors des exécutions suivantes
shell: yum install package·dnf Lit l'état actuel et applique uniquement les changements nécessaires
Initialiser avec command à chaque exécution creates/removes ou module dédié Explicite la condition d'achèvement
Redémarrer les services systématiquement Handler notify Redémarre uniquement si la configuration change
Ignorer toutes les erreurs failed_when·block/rescue Distingue les erreurs attendues des véritables échecs

Définir une condition d'achèvement pour command

- name: Initialize application database once
  ansible.builtin.command:
    argv:
      - /usr/local/bin/myapp-init
      - --database
      - /var/lib/myapp/app.db
    creates: /var/lib/myapp/.initialized
  register: init_result
  # Laisser command signaler skipped/changed selon creates.

creates ignore la commande si le chemin existe. Vérifiez que le programme d'initialisation crée effectivement ce marqueur après réussite. Définir inconditionnellement changed_when à false masque les changements réels et notifications de handlers ; fondez-le sur le sens du code de retour et de la sortie.

Protéger les secrets avec Vault

Ansible Vault chiffre variables et fichiers pour réduire les secrets en clair dans les dépôts. Comme le précise sa documentation, il protège uniquement les données au repos. Les valeurs déchiffrées à l'exécution peuvent apparaître dans les arguments des modules, diffs, sorties debug et erreurs : combinez Vault avec no_log, restrictions de diff et contrôles d'accès aux journaux.

Chiffrer une chaîne saisie au terminal

read -rsp 'Secret value: ' SECRET_VALUE
echo
printf '%s' "$SECRET_VALUE"   | ansible-vault encrypt_string       --vault-id prod@prompt       --stdin-name 'db_password'   > inventories/prod/group_vars/all/vault.yml
unset SECRET_VALUE

chmod 0600 inventories/prod/group_vars/all/vault.yml

Exécuter avec un identifiant Vault

ansible-playbook   -i inventories/prod/hosts.yml   playbooks/web.yml   --vault-id prod@prompt   --check --limit prod-web-01
# Les variables YAML !vault sont déchiffrées lors de l'exécution du playbook avec --vault-id.

Masquer la sortie des tâches sensibles

- name: Render application secret configuration
  ansible.builtin.template:
    src: app-secret.conf.j2
    dest: /etc/myapp/secret.conf
    owner: root
    group: root
    mode: '0600'
  no_log: true
  diff: false
no_log masque la sortie d'une tâche, mais ne protège pas contre du code malveillant, des tâches debug distinctes ou tous les journaux de la cible. Privilégiez si possible un gestionnaire de secrets externe et des identifiants de courte durée, et excluez les fichiers de mots de passe Vault de Git.

Déploiements progressifs

Après déploiement, exécutez les redémarrages notifiés avec meta: flush_handlers avant les contrôles de santé. Pour mettre à niveau un paquet installé, précisez une version approuvée dans son nom ou définissez explicitement myapp_package_state à latest pendant une maintenance. present ne met pas automatiquement les paquets à niveau.

Utilisez serial pour limiter la taille des lots plutôt que de modifier toute la production simultanément. max_fail_percentage arrête l'exécution si le taux d'échec du lot dépasse le seuil. Pour arrêter après un échec dans un lot de deux hôtes, utilisez par exemple 49 et non 50, car la comparaison est strictement supérieure.

---
- name: Roll out application safely
  hosts: web
  become: true
  serial: 2
  max_fail_percentage: 49

  pre_tasks:
    - name: Confirm target batch
      ansible.builtin.debug:
        msg: "Deploying to {{ ansible_play_batch }}"

  tasks:
    - name: Deploy application package
      ansible.builtin.package:
        name: myapp
        state: "{{ myapp_package_state | default('present') }}"
      notify: Restart MyApp

    - name: Restart changed services before checking health
      ansible.builtin.meta: flush_handlers

    - name: Verify local health endpoint
      ansible.builtin.uri:
        url: http://127.0.0.1:8080/healthz
        status_code: 200
        return_content: false
      register: health
      retries: 10
      delay: 3
      until: health.status == 200

  handlers:
    - name: Restart MyApp
      ansible.builtin.service:
        name: myapp
        state: restarted

Cet exemple illustre la structure minimale. Avec un véritable répartiteur, retirez les hôtes du lot du trafic, attendez la fin des connexions, déployez, vérifiez leur santé et réinscrivez-les. Avec delegate_to ou des modules API, vérifiez aussi certificats, traitement des erreurs et sûreté des nouvelles tentatives.

Gestion des erreurs et récupération

Un block applique des directives communes comme become et when à des tâches liées et exprime le traitement des erreurs avec rescue et always. Si rescue réussit, l'échec initial peut être considéré comme récupéré et le play continuer. Les erreurs syntaxiques et hôtes injoignables ne déclenchent pas rescue : prévoyez séparément politique de connexion et supervision.

- name: Back up current configuration
  ansible.builtin.copy:
    src: /etc/myapp/myapp.conf
    dest: /var/backups/myapp.conf.pre-ansible
    remote_src: true
    owner: root
    group: root
    mode: '0600'

- name: Update service configuration with recovery
  block:
    - name: Render candidate configuration
      ansible.builtin.template:
        src: myapp.conf.j2
        dest: /etc/myapp/myapp.conf
        owner: root
        group: root
        mode: '0640'
        validate: '/usr/local/bin/myapp --check-config %s'
      notify: Restart MyApp

  rescue:
    - name: Restore previous configuration
      ansible.builtin.copy:
        src: /var/backups/myapp.conf.pre-ansible
        dest: /etc/myapp/myapp.conf
        remote_src: true
        owner: root
        group: root
        mode: '0640'

    - name: Stop this host after recovery
      ansible.builtin.fail:
        msg: Configuration deployment failed and was restored

  always:
    - name: Record completion state
      ansible.builtin.debug:
        msg: "Configuration block finished for {{ inventory_hostname }}"

Restaurer un fichier de configuration ne signifie pas revenir aussi à l'ancienne application ou à son schéma de base. Conservez une procédure distincte couvrant contrôles avant/après, retour arrière des paquets, changements de données et déclenchement des handlers.

Contrôles de qualité et ordre d'exécution

  1. Consigner ansible-core, Python, collections et commit Git à déployer.
  2. Vérifier cibles, variables et groupes avec inventory --graph et --list.
  3. Valider ansible-lint et --syntax-check.
  4. Exécuter --check --diff sur un hôte de développement.
  5. Examiner les diffs sensibles et le périmètre de no_log.
  6. Après exécution réelle sur un hôte, vérifier la santé et changed=0 lors d'une répétition.
  7. Tester les arrêts sur erreur et la récupération en préproduction avec de petits lots serial.
  8. Déployer pendant une maintenance approuvée en conservant le récapitulatif, les métriques et journaux.
ansible-config dump --only-changed
ansible-inventory -i inventories/prod/hosts.yml --graph
ansible-lint playbooks roles
ansible-playbook -i inventories/prod/hosts.yml playbooks/web.yml --syntax-check

ansible-playbook   -i inventories/prod/hosts.yml   playbooks/web.yml   --check --diff   --limit prod-web-01   --vault-id prod@prompt

Points à vérifier en exploitation

  • Séparer les sources d'inventaire et droits d'écriture de dev, stage et prod.
  • Conserver la vérification des clés SSH et minimiser les privilèges du compte d'automatisation et de sudo.
  • Nommer chaque tâche, utiliser un FQCN et définir les conditions d'achèvement/changement pour command et shell.
  • Valider la configuration avec validate et les handlers, puis confirmer changed=0 aux répétitions.
  • Combiner Vault, no_log, restrictions de diff et contrôles d'accès aux journaux.
  • Élargir la validation de la syntaxe et du lint à check, diff, un hôte, puis de petits lots serial.
  • Préparer les cas d'hôtes injoignables, d'erreurs hors block/rescue et de retour arrière des données.
  • Consigner commit, inventaire, limit, opérateur, récapitulatif et contrôles finaux dans l'historique.

Documentation officielle et articles associés

Conclusion

Une automatisation Ansible sûre exprime en code l'état souhaité, les conditions de changement, les frontières d'échec et l'ordre de validation. Séparez les inventaires par environnement, construisez l'idempotence avec des modules FQCN dédiés et des handlers, et comprenez les limites de Vault. N'étendez à la production qu'après réussite des validations syntaxe, lint, check, diff, hôte unique, lots serial et récupération.