08 — Ansible : approfondissement#

Le livre Linux — Administration système couvre les bases d’Ansible : playbooks, inventaire statique, modules essentiels (apt, copy, template, service…). Ce chapitre part de ces acquis pour explorer les fonctionnalités avancées : rôles structurés, collections Galaxy, Ansible Vault, inventaire dynamique, stratégies d’exécution, tests avec Molecule et intégration CI/CD.

Rappel des bases#

Un playbook Ansible est un fichier YAML décrivant un ensemble de plays : chaque play associe un groupe d’hôtes à une liste de tasks exécutées séquentiellement par défaut.

# playbook.yml — structure minimale
---
- name: Configurer les serveurs web
  hosts: web_servers
  become: true
  vars:
    nginx_port: 80

  tasks:
    - name: Installer nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Démarrer et activer nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

L’inventaire statique liste les hôtes par groupe ; les variables peuvent être définies inline ou dans host_vars/ et group_vars/.

Rôles avancés#

Un rôle est une unité de réutilisation structurée. La convention de répertoires est stricte et chaque sous-dossier a un rôle précis.

roles/
└── nginx/
    ├── defaults/
    │   └── main.yml      # variables par défaut (priorité la plus faible)
    ├── vars/
    │   └── main.yml      # variables internes (priorité haute, non surchargeables par l'utilisateur)
    ├── tasks/
    │   ├── main.yml      # point d'entrée des tâches
    │   ├── install.yml
    │   └── configure.yml
    ├── handlers/
    │   └── main.yml      # handlers déclenchés par notify
    ├── templates/
    │   └── nginx.conf.j2 # templates Jinja2
    ├── files/
    │   └── index.html    # fichiers statiques
    ├── meta/
    │   └── main.yml      # métadonnées : dépendances entre rôles
    ├── molecule/
    │   └── default/      # scénarios de test Molecule
    └── README.md

defaults/main.yml vs vars/main.yml#

# defaults/main.yml — valeurs par défaut surchargeables
nginx_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_log_format: combined
# vars/main.yml — constantes internes (non surchargeables par l'inventaire)
nginx_service_name: nginx
nginx_config_dir: /etc/nginx
nginx_sites_dir: "{{ nginx_config_dir }}/sites-enabled"

Handlers#

# handlers/main.yml
---
- name: reload nginx
  ansible.builtin.service:
    name: "{{ nginx_service_name }}"
    state: reloaded

- name: restart nginx
  ansible.builtin.service:
    name: "{{ nginx_service_name }}"
    state: restarted
# tasks/configure.yml — déclenchement du handler
- name: Déployer la configuration nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: "{{ nginx_config_dir }}/nginx.conf"
    owner: root
    group: root
    mode: "0644"
    validate: nginx -t -c %s
  notify: reload nginx

meta/main.yml — dépendances entre rôles#

# meta/main.yml
---
galaxy_info:
  role_name: nginx
  author: monorg
  description: Installation et configuration d'Nginx
  min_ansible_version: "2.14"
  platforms:
    - name: Ubuntu
      versions: ["22.04", "24.04"]

dependencies:
  - role: common          # installé avant nginx
  - role: firewall
    vars:
      firewall_allowed_ports: [80, 443]

Collections Ansible Galaxy#

Depuis Ansible 2.9, le code est distribué via des collections : un ensemble cohérent de modules, plugins, rôles et playbooks sous un namespace.

# requirements.yml
---
collections:
  - name: community.general
    version: ">=8.0"
  - name: amazon.aws
    version: "~=7.0"
  - name: kubernetes.core
    version: ">=3.0"
  - name: community.postgresql
    version: ">=3.2"

roles:
  - name: geerlingguy.docker
    version: "7.0.0"
  - src: https://github.com/monorg/ansible-role-app.git
    scm: git
    version: v2.1.0
    name: monorg.app
# Installation depuis requirements.yml
ansible-galaxy collection install -r requirements.yml
ansible-galaxy role install -r requirements.yml

# Lister les collections installées
ansible-galaxy collection list

# Créer un squelette de collection
ansible-galaxy collection init monorg.infrastructure

Utilisation d’un module depuis une collection :

- name: Créer un bucket S3
  amazon.aws.s3_bucket:
    name: "mon-bucket-{{ env }}"
    region: eu-west-1
    versioning: true
    tags:
      Environment: "{{ env }}"
      ManagedBy: ansible

Ansible Vault#

Vault permet de chiffrer des variables sensibles (mots de passe, clés API, certificats) directement dans les fichiers YAML du dépôt git.

# Chiffrer un fichier entier
ansible-vault encrypt group_vars/prod/secrets.yml

# Editer un fichier chiffré
ansible-vault edit group_vars/prod/secrets.yml

# Déchiffrer (pour inspection)
ansible-vault decrypt group_vars/prod/secrets.yml

# Chiffrer une valeur inline (vault-encrypted string)
ansible-vault encrypt_string 'MonMotDePasse123!' --name 'db_password'

# Exécuter un playbook avec le mot de passe Vault
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Fichier de secrets chiffré :

# group_vars/prod/secrets.yml (chiffré avec ansible-vault encrypt)
$ANSIBLE_VAULT;1.1;AES256
66386439343533346637623039...

Intégration CI — variable d’environnement :

# .github/workflows/deploy.yml
- name: Déployer avec Ansible
  env:
    ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
  run: |
    echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass
    ansible-playbook -i inventories/prod site.yml \
      --vault-password-file /tmp/.vault_pass
    rm /tmp/.vault_pass

Rotation des clés Vault

Utilisez ansible-vault rekey pour changer le mot de passe Vault sans déchiffrer/rechiffrer manuellement. En production, préférez un backend de secrets externe (HashiCorp Vault, AWS Secrets Manager) avec le plugin lookup hashi_vault ou aws_ssm.

Inventaire dynamique#

L’inventaire dynamique interroge une source externe (AWS, GCP, Azure, Kubernetes, VMware…) pour construire l’inventaire à la volée.

# inventories/aws/aws_ec2.yml — plugin AWS EC2
plugin: amazon.aws.aws_ec2
regions:
  - eu-west-1
  - eu-west-3

filters:
  tag:Environment: prod
  instance-state-name: running

keyed_groups:
  - key: tags.Role
    prefix: role
  - key: placement.availability_zone
    prefix: az

compose:
  ansible_host: public_ip_address
  ansible_user: "'ubuntu'"

hostnames:
  - tag:Name
  - private-ip-address
# Lister l'inventaire dynamique (sans exécuter de playbook)
ansible-inventory -i inventories/aws/ --list
ansible-inventory -i inventories/aws/ --graph

# Tester la connectivité
ansible -i inventories/aws/ role_web -m ping

Plugin GCP :

# inventories/gcp/gcp_compute.yml
plugin: google.cloud.gcp_compute
projects:
  - mon-projet-gcp
filters:
  - status = RUNNING
  - labels.environment = prod
keyed_groups:
  - key: labels.role
    prefix: role

Stratégies d’exécution et optimisation#

Stratégies#

# playbook.yml
- name: Déploiement avec stratégie free
  hosts: app_servers
  strategy: free         # chaque hôte avance à son propre rythme (pas d'attente)
  # strategy: linear     # défaut : tous les hôtes terminent chaque task avant de passer à la suivante
  # strategy: host_pinned # comme free, mais toutes les tasks d'un hôte s'exécutent en séquence

Optimisation#

# ansible.cfg
[defaults]
forks              = 20          # parallélisme (défaut : 5)
gathering          = smart       # ne collecte les facts qu'une fois par hôte
fact_caching       = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

[ssh_connection]
pipelining         = true        # réduit le nombre de connexions SSH (~30% plus rapide)
ssh_args           = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
control_path       = /tmp/ansible-ssh-%%h-%%p-%%r
# Délégation et parallélisme avancé
- name: Mettre à jour les serveurs par lots de 20%
  hosts: web_servers
  serial: "20%"          # rolling update : 20% des hôtes à la fois
  max_fail_percentage: 10

  tasks:
    - name: Désactiver dans le load balancer
      delegate_to: localhost
      uri:
        url: "http://lb.internal/drain/{{ inventory_hostname }}"
        method: POST

    - name: Mettre à jour l'application
      ansible.builtin.apt:
        name: myapp
        state: latest
      notify: restart app

Tests de rôles avec Molecule#

Molecule est le framework de test officiel pour les rôles Ansible. Il crée des instances éphémères (Docker, Vagrant, cloud), applique le rôle, puis vérifie le résultat.

# Initialiser un rôle avec Molecule
molecule init role monorg.nginx --driver-name docker

# Cycle de test complet
molecule test

# Étapes individuelles
molecule create    # créer les instances de test
molecule converge  # appliquer le rôle
molecule verify    # exécuter les tests
molecule destroy   # supprimer les instances
molecule login     # se connecter à une instance pour debug
# molecule/default/molecule.yml
---
dependency:
  name: galaxy
  options:
    requirements-file: requirements.yml

driver:
  name: docker

platforms:
  - name: ubuntu-22
    image: "geerlingguy/docker-ubuntu2204-ansible:latest"
    pre_build_image: true
    privileged: true
  - name: debian-12
    image: "geerlingguy/docker-debian12-ansible:latest"
    pre_build_image: true

provisioner:
  name: ansible
  playbooks:
    converge: converge.yml
  inventory:
    host_vars:
      ubuntu-22:
        nginx_port: 8080

verifier:
  name: ansible
# molecule/default/verify.yml
---
- name: Vérifier la configuration nginx
  hosts: all
  gather_facts: false

  tasks:
    - name: nginx doit être démarré
      ansible.builtin.service_facts:

    - name: Vérifier que le service est actif
      ansible.builtin.assert:
        that: ansible_facts.services['nginx.service'].state == 'running'
        fail_msg: "nginx n'est pas en cours d'exécution"

    - name: Vérifier la réponse HTTP
      ansible.builtin.uri:
        url: "http://localhost:{{ nginx_port | default(80) }}"
        status_code: 200

Intégration dans un pipeline CI/CD#

GitHub Actions#

# .github/workflows/ansible.yml
---
name: CI Ansible

on:
  push:
    paths: ["roles/**", "playbooks/**", "inventories/**"]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Installer ansible-lint
        run: pip install ansible-lint
      - name: Linter les rôles et playbooks
        run: ansible-lint

  molecule:
    runs-on: ubuntu-latest
    needs: lint
    strategy:
      matrix:
        role: [nginx, postgresql, app]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Installer les dépendances
        run: pip install molecule molecule-plugins[docker] ansible-core
      - name: Tester le rôle ${{ matrix.role }}
        run: molecule test
        working-directory: roles/${{ matrix.role }}

  deploy:
    runs-on: ubuntu-latest
    needs: molecule
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Configurer la clé SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Déployer en production
        env:
          ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
        run: |
          echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass
          ansible-playbook -i inventories/prod site.yml \
            --vault-password-file /tmp/.vault_pass \
            --diff

GitLab CI#

# .gitlab-ci.yml (extrait)
variables:
  ANSIBLE_FORCE_COLOR: "true"
  PY_COLORS: "1"

.ansible-base:
  image: python:3.12-slim
  before_script:
    - pip install ansible-core molecule molecule-plugins[docker]
    - ansible-galaxy install -r requirements.yml

lint:
  extends: .ansible-base
  script:
    - pip install ansible-lint
    - ansible-lint

molecule-test:
  extends: .ansible-base
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2376
  script:
    - molecule test
  parallel:
    matrix:
      - ROLE: [nginx, postgresql, app]
  before_script:
    - cd roles/$ROLE

Ansible vs Terraform — complémentarité#

Ces deux outils répondent à des besoins différents et se complètent naturellement dans un pipeline IaC.

Dimension

Terraform

Ansible

Paradigme

Déclaratif (état désiré)

Procédural (étapes séquentielles)

Modèle de ressources

Immuable (recréer > modifier)

Muable (modifier en place)

Domaine principal

Provisioning (VM, réseau, DB…)

Configuration (packages, fichiers, services)

State

Fichier tfstate explicite

Sans state (idempotence par design)

Connexion

APIs cloud (HTTP)

SSH / WinRM

Idéal pour

Créer et détruire l’infrastructure

Configurer et maintenir les serveurs

Pattern d’usage combiné :

  1. Terraform provisionne le VPC, l’EKS, la RDS, le DNS, et écrit les IPs/endpoints dans SSM.

  2. Ansible récupère ces valeurs depuis SSM, configure les serveurs (packages, monitoring, app), et maintient la configuration dans le temps.

Ansible AWX, Ansible Tower et AAP#

AWX est la version open-source upstream d’Ansible Tower/AAP. Il fournit une interface web, une API REST, un scheduler, et la gestion RBAC pour exécuter des playbooks en équipe.

Ansible Automation Platform (AAP) est la version Red Hat commerciale, avec support, conteneurisation (Execution Environments), et intégration Event-Driven Ansible.

Points clés :

  • Les Execution Environments (EE) sont des images OCI contenant Ansible Core, les collections et leurs dépendances Python — reproductibles entre dev et prod.

  • L”inventaire dynamique est une source de première classe dans AWX/Tower.

  • Les workflows permettent d’enchaîner plusieurs job templates avec des conditions de succès/échec.

  • L’API REST d’AWX permet de déclencher des playbooks depuis n’importe quel système CI.


Visualisations#

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import numpy as np
import networkx as nx
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Visualisation 1 : Heatmap de durée de run Ansible (tasks × hosts)

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

np.random.seed(42)

tasks = [
    "Gather facts",
    "apt update",
    "Install packages",
    "Deploy config",
    "Start services",
    "Run smoke tests",
    "Notify handlers",
]

hosts = [f"web-{i:02d}" for i in range(1, 9)]

# Durées simulées en secondes
base = np.array([
    [3, 3, 3, 3, 3, 3, 3, 3],     # gather facts — stable
    [8, 12, 7, 15, 9, 11, 8, 10],  # apt update — variable réseau
    [22, 25, 20, 30, 19, 28, 21, 24],  # install packages
    [2,  2,  2,  2,  2,  2,  2,  2],   # deploy config — rapide
    [3,  4,  3,  3,  4,  3,  3,  4],   # start services
    [5,  8,  5, 12,  6,  7,  5,  9],   # smoke tests — variable
    [1,  1,  1,  1,  1,  1,  1,  1],   # handlers
])

fig, ax = plt.subplots(figsize=(12, 6))
im = ax.imshow(base, cmap="YlOrRd", aspect="auto", vmin=0, vmax=35)

ax.set_xticks(range(len(hosts)))
ax.set_yticks(range(len(tasks)))
ax.set_xticklabels(hosts, fontsize=9.5)
ax.set_yticklabels(tasks, fontsize=9.5)
ax.set_title("Durée d'exécution Ansible par task et par hôte (secondes simulées)", fontsize=13, fontweight="bold", pad=15)

for i in range(len(tasks)):
    for j in range(len(hosts)):
        val = base[i, j]
        color = "white" if val > 20 else "#333"
        ax.text(j, i, f"{val}s", ha="center", va="center", fontsize=8.5, fontweight="bold", color=color)

plt.colorbar(im, ax=ax, label="Durée (secondes)", shrink=0.85)

plt.savefig("ansible_heatmap_duration.png", dpi=120, bbox_inches="tight")
plt.show()
_images/2a32e1d3724a25c7489bab22ac52cbbb004f2a0100a38b61458f69a414725bbb.png
# Visualisation 2 : Matrice 2×2 — positionnement des outils IaC/configuration

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(10, 9))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)

# Quadrants
ax.axvline(x=5, color="#ced4da", linewidth=2, linestyle="--")
ax.axhline(y=5, color="#ced4da", linewidth=2, linestyle="--")

# Labels des axes et quadrants
ax.set_xlabel("Provisioning  ←                                    →  Configuration", fontsize=11, labelpad=12)
ax.set_ylabel("Muable (in-place)  ←                          →  Immuable (recreate)", fontsize=11, labelpad=12)
ax.set_xticks([])
ax.set_yticks([])

quadrants = [
    (1.5, 8.5, "Provisioning\n+ Immuable", "#e8f4fd"),
    (7.5, 8.5, "Configuration\n+ Immuable", "#e8f7ee"),
    (1.5, 1.5, "Provisioning\n+ Muable",   "#fff3cd"),
    (7.5, 1.5, "Configuration\n+ Muable",  "#fde8e8"),
]
for x, y, label, color in quadrants:
    rect = FancyBboxPatch((x - 1.4, y - 1.0), 2.8, 2.0,
                          boxstyle="round,pad=0.1",
                          facecolor=color, edgecolor="#ced4da", linewidth=1.2, zorder=1, alpha=0.6)
    ax.add_patch(rect)
    ax.text(x, y, label, ha="center", va="center", fontsize=8.5, color="#555", style="italic")

# Outils positionnés
outils = [
    ("Terraform /\nOpenTofu",  2.2, 7.8, "#ff922b", 350),
    ("Pulumi",                 3.2, 7.0, "#ffd43b", 280),
    ("AWS CDK",                2.5, 6.2, "#e9ecef", 260),
    ("Ansible",                7.2, 3.5, "#51cf66", 350),
    ("Chef",                   6.5, 2.8, "#74c0fc", 280),
    ("Puppet",                 7.8, 2.5, "#4dabf7", 280),
    ("Salt",                   6.8, 4.2, "#a9e34b", 250),
    ("Packer",                 3.8, 6.8, "#f783ac", 230),
]

for label, x, y, color, size in outils:
    ax.scatter(x, y, s=size, color=color, alpha=0.88, edgecolors="white", linewidths=2, zorder=4)
    ax.annotate(label, (x, y), textcoords="offset points", xytext=(10, 6),
                fontsize=9.5, fontweight="bold", color="#333", zorder=5)

ax.set_title("Positionnement des outils IaC / gestion de configuration\nAxes : provisioning↔configuration et muable↔immuable",
             fontsize=13, fontweight="bold", pad=15)

plt.savefig("iac_tools_matrix.png", dpi=120, bbox_inches="tight")
plt.show()
_images/9ee07e607fd63a364fd72c99fe1d263a73a0c7d710a7183d2a1fa329bc1965b7.png
# Visualisation 3 : Graphe de dépendances entre rôles Ansible

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

G = nx.DiGraph()

roles = {
    "common":       {"color": "#4dabf7", "layer": 0},
    "firewall":     {"color": "#4dabf7", "layer": 0},
    "users":        {"color": "#4dabf7", "layer": 0},
    "python":       {"color": "#74c0fc", "layer": 1},
    "docker":       {"color": "#74c0fc", "layer": 1},
    "postgresql":   {"color": "#51cf66", "layer": 2},
    "redis":        {"color": "#51cf66", "layer": 2},
    "nginx":        {"color": "#51cf66", "layer": 2},
    "app":          {"color": "#ffd43b", "layer": 3},
    "monitoring":   {"color": "#ff922b", "layer": 4},
}

for role, attrs in roles.items():
    G.add_node(role, **attrs)

# Dépendances : A → B signifie que le rôle A dépend de B (B est installé en premier)
deps = [
    ("firewall",   "common"),
    ("users",      "common"),
    ("python",     "common"),
    ("docker",     "python"),
    ("docker",     "firewall"),
    ("postgresql", "python"),
    ("postgresql", "firewall"),
    ("redis",      "firewall"),
    ("nginx",      "firewall"),
    ("app",        "docker"),
    ("app",        "postgresql"),
    ("app",        "redis"),
    ("app",        "nginx"),
    ("monitoring", "app"),
    ("monitoring", "common"),
]
G.add_edges_from(deps)

pos = {
    "common":     (4.0, 4.0),
    "firewall":   (2.0, 4.0),
    "users":      (6.0, 4.0),
    "python":     (2.0, 3.0),
    "docker":     (2.0, 2.0),
    "postgresql": (4.0, 2.0),
    "redis":      (6.0, 2.5),
    "nginx":      (7.5, 3.0),
    "app":        (4.5, 1.0),
    "monitoring": (4.5, 0.0),
}

fig, ax = plt.subplots(figsize=(12, 8))
ax.set_title("Graphe de dépendances entre rôles Ansible\n(A → B : le rôle A dépend du rôle B)", fontsize=13, fontweight="bold", pad=15)

node_colors = [roles[n]["color"] for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=2000, alpha=0.9, ax=ax)
nx.draw_networkx_labels(G, pos, font_size=8.5, font_weight="bold", ax=ax)
nx.draw_networkx_edges(G, pos, arrows=True, arrowsize=20, width=2.0,
                       edge_color="#868e96", alpha=0.75,
                       connectionstyle="arc3,rad=0.05", ax=ax)

layer_labels = {0: "Couche de base", 1: "Runtime", 2: "Services", 3: "Application", 4: "Observabilité"}
colors_legend = ["#4dabf7", "#74c0fc", "#51cf66", "#ffd43b", "#ff922b"]
patches = [mpatches.Patch(color=c, label=l) for c, l in zip(colors_legend, layer_labels.values())]
ax.legend(handles=patches, loc="lower right", fontsize=9)
ax.axis("off")

plt.savefig("ansible_roles_dag.png", dpi=120, bbox_inches="tight")
plt.show()
_images/5d05011141d698e27d605118e097b69090cfc181313f53a2bc5163eba6d355d5.png

Résumé#

  1. Les rôles Ansible structurent le code de configuration en couches claires (defaults, vars, tasks, handlers, meta) et constituent l’unité de réutilisation et de test élémentaire.

  2. La distinction entre defaults/ (surchargeables par l’inventaire) et vars/ (constantes internes) est fondamentale pour concevoir des rôles flexibles sans exposer leurs détails d’implémentation.

  3. Les collections Ansible Galaxy permettent de distribuer et versionner des ensembles cohérents de modules, rôles et plugins — le fichier requirements.yml rend les dépendances explicites et reproductibles.

  4. Ansible Vault intégré dans le dépôt git permet de chiffrer les secrets au plus proche du code ; en production, le combiner avec un backend de secrets externe (HashiCorp Vault, AWS Secrets Manager) via les plugins lookup.

  5. L”inventaire dynamique transforme Ansible en outil scalable : les plugins AWS EC2, GCP Compute et Azure RM interrogent directement les APIs cloud pour construire l’inventaire sans maintenance manuelle.

  6. La stratégie free avec pipelining = true et un nombre élevé de forks peut réduire les temps d’exécution de 30 à 50% sur des parcs de serveurs importants.

  7. Molecule est indispensable pour tester les rôles dans des conteneurs éphémères ; l’intégration dans la CI garantit que chaque modification de rôle est validée avant merge.

  8. L’intégration Ansible dans GitHub Actions / GitLab CI suit le même pattern que le code applicatif : lint, test (Molecule), puis déploiement conditionné à la branche principale.

  9. Ansible et Terraform sont complémentaires : Terraform provisionne l’infrastructure immuable, Ansible configure et maintient les systèmes muables — les deux se parlent via SSM, outputs, ou inventaires dynamiques.

  10. AWX / Ansible Tower / AAP apportent une interface web, un contrôle d’accès fin et des Execution Environments reproductibles pour opérer Ansible à l’échelle d’une organisation.