Intermédiaire
⭐ Article vedette

Ansible : Maîtriser l'Automatisation Infrastructure as Code

Guide complet pour automatiser vos infrastructures avec Ansible : playbooks, rôles, inventaires dynamiques, et bonnes pratiques pour la production.

Publié le
16 décembre 2024
Lecture
19 min
Vues
0
Auteur
Florian Courouge
Ansible
Automation
IaC
DevOps
Configuration Management
Orchestration

Table des matières

📋 Vue d'ensemble rapide des sujets traités dans cet article

Cliquez sur les sections ci-dessous pour naviguer rapidement

Ansible : Maîtriser l'Automatisation Infrastructure as Code

Ansible révolutionne la gestion d'infrastructure en permettant l'automatisation sans agent. Ce guide vous accompagnera de l'installation aux techniques avancées pour une utilisation en production.

💡Fondamentaux et Architecture

Installation et Configuration

#!/bin/bash
# ansible-setup.sh

# Installation sur Ubuntu/Debian
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

# Installation via pip (recommandé pour la dernière version)
pip3 install ansible ansible-core

# Vérification de l'installation
ansible --version
ansible-config dump --only-changed

# Configuration SSH pour Ansible
ssh-keygen -t rsa -b 4096 -C "ansible@$(hostname)"

# Distribution des clés SSH
for host in server1 server2 server3; do
    ssh-copy-id -i ~/.ssh/id_rsa.pub user@$host
done

Structure de Projet Ansible

# Structure recommandée pour un projet Ansible
mkdir -p ansible-infrastructure/{
    inventories/{production,staging,development},
    group_vars,
    host_vars,
    roles,
    playbooks,
    files,
    templates,
    vault,
    collections,
    plugins/{modules,filters,lookup}
}

# Fichier de configuration ansible.cfg
cat > ansible-infrastructure/ansible.cfg << 'EOF'
[defaults]
inventory = inventories/production/hosts.yml
remote_user = ansible
private_key_file = ~/.ssh/ansible_rsa
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
bin_ansible_callbacks = True
callback_whitelist = timer, profile_tasks, profile_roles

# Performance
forks = 20
poll_interval = 2
timeout = 30
gather_timeout = 30

# Logging
log_path = /var/log/ansible.log

[inventory]
enable_plugins = host_list, script, auto, yaml, ini, toml

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
EOF

💡Inventaires et Variables

Inventaire Dynamique

# inventories/production/hosts.yml
all:
  children:
    webservers:
      hosts:
        web01:
          ansible_host: 10.0.1.10
          ansible_user: ubuntu
          server_role: frontend
        web02:
          ansible_host: 10.0.1.11
          ansible_user: ubuntu
          server_role: frontend
      vars:
        http_port: 80
        https_port: 443
        
    databases:
      hosts:
        db01:
          ansible_host: 10.0.2.10
          ansible_user: ubuntu
          server_role: primary
        db02:
          ansible_host: 10.0.2.11
          ansible_user: ubuntu
          server_role: replica
      vars:
        mysql_port: 3306
        
    loadbalancers:
      hosts:
        lb01:
          ansible_host: 10.0.0.10
          ansible_user: ubuntu
          vip: 10.0.0.100
          
    monitoring:
      hosts:
        monitor01:
          ansible_host: 10.0.3.10
          ansible_user: ubuntu
          
  vars:
    environment: production
    datacenter: us-east-1
    backup_enabled: true

Script d'Inventaire Dynamique

#!/usr/bin/env python3
# dynamic_inventory.py

import json
import boto3
import argparse
from collections import defaultdict

class EC2Inventory:
    def __init__(self):
        self.inventory = defaultdict(dict)
        self.read_cli_args()
        
        if self.args.list:
            self.inventory = self.get_inventory()
        elif self.args.host:
            self.inventory = self.get_host_info(self.args.host)
            
        print(json.dumps(self.inventory, indent=2))
    
    def read_cli_args(self):
        parser = argparse.ArgumentParser()
        parser.add_argument('--list', action='store_true')
        parser.add_argument('--host', action='store')
        self.args = parser.parse_args()
    
    def get_inventory(self):
        """Récupère l'inventaire depuis AWS EC2"""
        ec2 = boto3.client('ec2')
        inventory = {
            '_meta': {
                'hostvars': {}
            }
        }
        
        # Récupérer toutes les instances
        response = ec2.describe_instances(
            Filters=[
                {'Name': 'instance-state-name', 'Values': ['running']}
            ]
        )
        
        for reservation in response['Reservations']:
            for instance in reservation['Instances']:
                # Extraire les informations de l'instance
                instance_id = instance['InstanceId']
                private_ip = instance.get('PrivateIpAddress', '')
                public_ip = instance.get('PublicIpAddress', '')
                
                # Récupérer les tags
                tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
                name = tags.get('Name', instance_id)
                environment = tags.get('Environment', 'unknown')
                role = tags.get('Role', 'unknown')
                
                # Ajouter aux groupes
                if environment not in inventory:
                    inventory[environment] = {'hosts': []}
                inventory[environment]['hosts'].append(name)
                
                if role not in inventory:
                    inventory[role] = {'hosts': []}
                inventory[role]['hosts'].append(name)
                
                # Variables d'hôte
                inventory['_meta']['hostvars'][name] = {
                    'ansible_host': public_ip or private_ip,
                    'ansible_user': 'ubuntu',
                    'instance_id': instance_id,
                    'instance_type': instance['InstanceType'],
                    'private_ip': private_ip,
                    'public_ip': public_ip,
                    'environment': environment,
                    'role': role,
                    'tags': tags
                }
        
        return inventory
    
    def get_host_info(self, hostname):
        """Récupère les informations d'un hôte spécifique"""
        inventory = self.get_inventory()
        return inventory['_meta']['hostvars'].get(hostname, {})

if __name__ == '__main__':
    EC2Inventory()

Gestion des Variables

# group_vars/all.yml
---
# Variables globales
timezone: "Europe/Paris"
ntp_servers:
  - "0.pool.ntp.org"
  - "1.pool.ntp.org"

# Utilisateurs système
system_users:
  - name: deploy
    groups: ["sudo", "docker"]
    shell: /bin/bash
    ssh_keys:
      - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ..."

# Packages de base
base_packages:
  - curl
  - wget
  - git
  - htop
  - vim
  - unzip

# Configuration de sécurité
security:
  ssh_port: 22
  fail2ban_enabled: true
  ufw_enabled: true
  automatic_updates: true

# group_vars/webservers.yml
---
nginx_version: "1.20"
php_version: "8.1"
ssl_enabled: true
ssl_cert_path: "/etc/ssl/certs"

web_applications:
  - name: "myapp"
    domain: "example.com"
    document_root: "/var/www/myapp"
    php_enabled: true

# group_vars/databases.yml
---
mysql_version: "8.0"
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_databases:
  - name: "myapp_prod"
    encoding: "utf8mb4"
    collation: "utf8mb4_unicode_ci"

mysql_users:
  - name: "myapp_user"
    password: "{{ vault_mysql_user_password }}"
    priv: "myapp_prod.*:ALL"
    host: "10.0.1.%"

💡Playbooks et Tâches

Playbook Principal

# playbooks/site.yml
---
- name: Configure all servers
  hosts: all
  become: yes
  gather_facts: yes
  
  pre_tasks:
    - name: Update package cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"
    
    - name: Ensure system is up to date
      apt:
        upgrade: safe
      when: ansible_os_family == "Debian"
  
  roles:
    - common
    - security
    - monitoring

- name: Configure web servers
  hosts: webservers
  become: yes
  
  roles:
    - nginx
    - php
    - ssl-certificates
    - application-deployment
  
  post_tasks:
    - name: Verify web service is running
      uri:
        url: "http://{{ ansible_default_ipv4.address }}"
        method: GET
        status_code: 200
      delegate_to: localhost

- name: Configure database servers
  hosts: databases
  become: yes
  
  roles:
    - mysql
    - database-backup
  
  post_tasks:
    - name: Verify MySQL is running
      service:
        name: mysql
        state: started
        enabled: yes

- name: Configure load balancers
  hosts: loadbalancers
  become: yes
  
  roles:
    - haproxy
    - keepalived

Playbook de Déploiement

# playbooks/deploy.yml
---
- name: Deploy application
  hosts: webservers
  become: yes
  serial: "50%"  # Déploiement par batch
  
  vars:
    app_name: "myapp"
    app_version: "{{ version | default('latest') }}"
    app_path: "/var/www/{{ app_name }}"
    backup_path: "/var/backups/{{ app_name }}"
    
  pre_tasks:
    - name: Create backup directory
      file:
        path: "{{ backup_path }}"
        state: directory
        mode: '0755'
    
    - name: Backup current application
      archive:
        path: "{{ app_path }}"
        dest: "{{ backup_path }}/{{ app_name }}-{{ ansible_date_time.epoch }}.tar.gz"
      when: app_path is directory
      
  tasks:
    - name: Stop application services
      service:
        name: "{{ item }}"
        state: stopped
      loop:
        - nginx
        - php8.1-fpm
      
    - name: Download application archive
      get_url:
        url: "https://releases.example.com/{{ app_name }}/{{ app_version }}.tar.gz"
        dest: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
        mode: '0644'
      
    - name: Extract application
      unarchive:
        src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
        dest: "{{ app_path }}"
        remote_src: yes
        owner: www-data
        group: www-data
        mode: '0755'
      
    - name: Install dependencies
      composer:
        command: install
        working_dir: "{{ app_path }}"
        no_dev: yes
        optimize_autoloader: yes
      become_user: www-data
      
    - name: Run database migrations
      command: php artisan migrate --force
      args:
        chdir: "{{ app_path }}"
      become_user: www-data
      run_once: true
      delegate_to: "{{ groups['webservers'][0] }}"
      
    - name: Clear application cache
      command: "{{ item }}"
      args:
        chdir: "{{ app_path }}"
      become_user: www-data
      loop:
        - php artisan config:cache
        - php artisan route:cache
        - php artisan view:cache
      
    - name: Start application services
      service:
        name: "{{ item }}"
        state: started
        enabled: yes
      loop:
        - php8.1-fpm
        - nginx
  
  post_tasks:
    - name: Wait for application to be ready
      uri:
        url: "http://{{ ansible_default_ipv4.address }}/health"
        method: GET
        status_code: 200
      retries: 5
      delay: 10
      
    - name: Clean up old backups
      find:
        paths: "{{ backup_path }}"
        age: "7d"
        patterns: "*.tar.gz"
      register: old_backups
      
    - name: Remove old backups
      file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"

💡Rôles Ansible

Structure d'un Rôle

# Créer la structure d'un rôle
ansible-galaxy init roles/nginx

# Structure générée
roles/nginx/
├── defaults/main.yml      # Variables par défaut
├── files/                 # Fichiers statiques
├── handlers/main.yml      # Handlers (actions déclenchées)
├── meta/main.yml         # Métadonnées du rôle
├── tasks/main.yml        # Tâches principales
├── templates/            # Templates Jinja2
├── tests/               # Tests du rôle
└── vars/main.yml        # Variables du rôle

Rôle Nginx Complet

# roles/nginx/defaults/main.yml
---
nginx_user: www-data
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 64m

nginx_remove_default_vhost: true
nginx_vhosts: []

nginx_upstreams: []

nginx_extra_conf_options: ""

# SSL Configuration
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
nginx_ssl_prefer_server_ciphers: "off"

# roles/nginx/tasks/main.yml
---
- name: Install nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  notify: restart nginx

- name: Create nginx directories
  file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: root
    mode: '0755'
  loop:
    - /etc/nginx/sites-available
    - /etc/nginx/sites-enabled
    - /etc/nginx/conf.d
    - /var/log/nginx

- name: Generate nginx main configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
  notify: restart nginx

- name: Remove default nginx vhost
  file:
    path: "{{ item }}"
    state: absent
  loop:
    - /etc/nginx/sites-enabled/default
    - /etc/nginx/sites-available/default
  when: nginx_remove_default_vhost
  notify: restart nginx

- name: Generate nginx vhost configurations
  template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ item.server_name }}"
    owner: root
    group: root
    mode: '0644'
  loop: "{{ nginx_vhosts }}"
  notify: restart nginx

- name: Enable nginx vhosts
  file:
    src: "/etc/nginx/sites-available/{{ item.server_name }}"
    dest: "/etc/nginx/sites-enabled/{{ item.server_name }}"
    state: link
  loop: "{{ nginx_vhosts }}"
  notify: restart nginx

- name: Generate nginx upstream configurations
  template:
    src: upstream.conf.j2
    dest: "/etc/nginx/conf.d/{{ item.name }}-upstream.conf"
    owner: root
    group: root
    mode: '0644'
  loop: "{{ nginx_upstreams }}"
  notify: restart nginx

- name: Ensure nginx is started and enabled
  service:
    name: nginx
    state: started
    enabled: yes

- name: Validate nginx configuration
  command: nginx -t
  changed_when: false

Templates Nginx

{# roles/nginx/templates/nginx.conf.j2 #}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections }};
    use epoll;
    multi_accept on;
}

http {
    # Basic Settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout {{ nginx_keepalive_timeout }};
    types_hash_max_size 2048;
    client_max_body_size {{ nginx_client_max_body_size }};
    
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # SSL Settings
    ssl_protocols {{ nginx_ssl_protocols }};
    ssl_ciphers {{ nginx_ssl_ciphers }};
    ssl_prefer_server_ciphers {{ nginx_ssl_prefer_server_ciphers }};
    
    # Logging Settings
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log;
    
    # Gzip Settings
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # Rate Limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
    
    {{ nginx_extra_conf_options }}
    
    # Virtual Host Configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

{# roles/nginx/templates/vhost.conf.j2 #}
{% if item.upstream is defined %}
upstream {{ item.upstream.name }} {
    {% for server in item.upstream.servers %}
    server {{ server }};
    {% endfor %}
}
{% endif %}

server {
    listen {{ item.listen | default('80') }};
    {% if item.ssl is defined and item.ssl.enabled %}
    listen {{ item.ssl.listen | default('443') }} ssl http2;
    {% endif %}
    
    server_name {{ item.server_name }};
    
    {% if item.ssl is defined and item.ssl.enabled %}
    ssl_certificate {{ item.ssl.certificate }};
    ssl_certificate_key {{ item.ssl.certificate_key }};
    {% endif %}
    
    root {{ item.root | default('/var/www/html') }};
    index {{ item.index | default('index.html index.htm index.php') }};
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    
    {% if item.locations is defined %}
    {% for location in item.locations %}
    location {{ location.path }} {
        {% if location.proxy_pass is defined %}
        proxy_pass {{ location.proxy_pass }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        {% endif %}
        
        {% if location.try_files is defined %}
        try_files {{ location.try_files }};
        {% endif %}
        
        {% if location.fastcgi_pass is defined %}
        fastcgi_pass {{ location.fastcgi_pass }};
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        {% endif %}
        
        {{ location.extra_config | default('') }}
    }
    {% endfor %}
    {% endif %}
    
    # Default location
    location / {
        try_files $uri $uri/ =404;
    }
    
    # PHP handling
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # Static files caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }
}

Handlers

# roles/nginx/handlers/main.yml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted
  listen: restart nginx

- name: reload nginx
  service:
    name: nginx
    state: reloaded
  listen: reload nginx

- name: validate nginx config
  command: nginx -t
  listen: validate nginx config

💡Techniques Avancées

Ansible Vault

# Créer un fichier vault
ansible-vault create vault/secrets.yml

# Éditer un fichier vault
ansible-vault edit vault/secrets.yml

# Chiffrer un fichier existant
ansible-vault encrypt group_vars/production/vault.yml

# Déchiffrer un fichier
ansible-vault decrypt group_vars/production/vault.yml

# Utiliser un fichier de mot de passe
echo "my_vault_password" > .vault_pass
chmod 600 .vault_pass

# Configuration dans ansible.cfg
vault_password_file = .vault_pass
# vault/secrets.yml (chiffré)
---
vault_mysql_root_password: "super_secret_password"
vault_mysql_user_password: "another_secret_password"
vault_api_keys:
  stripe: "sk_live_..."
  sendgrid: "SG...."
vault_ssl_private_keys:
  example_com: |
    -----BEGIN PRIVATE KEY-----
    MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
    -----END PRIVATE KEY-----

Modules Personnalisés

#!/usr/bin/python
# library/custom_service_check.py

from ansible.module_utils.basic import AnsibleModule
import requests
import time

def check_service_health(url, timeout=30, retries=3):
    """Vérifie la santé d'un service web"""
    for attempt in range(retries):
        try:
            response = requests.get(url, timeout=timeout)
            if response.status_code == 200:
                return True, f"Service is healthy (HTTP {response.status_code})"
        except requests.RequestException as e:
            if attempt == retries - 1:
                return False, f"Service check failed: {str(e)}"
            time.sleep(5)
    
    return False, "Service check failed after all retries"

def main():
    module = AnsibleModule(
        argument_spec=dict(
            url=dict(type='str', required=True),
            timeout=dict(type='int', default=30),
            retries=dict(type='int', default=3),
            expected_status=dict(type='int', default=200)
        ),
        supports_check_mode=True
    )
    
    url = module.params['url']
    timeout = module.params['timeout']
    retries = module.params['retries']
    
    if module.check_mode:
        module.exit_json(changed=False, msg="Check mode - would check service health")
    
    success, message = check_service_health(url, timeout, retries)
    
    if success:
        module.exit_json(changed=False, msg=message, status="healthy")
    else:
        module.fail_json(msg=message, status="unhealthy")

if __name__ == '__main__':
    main()

Filtres Personnalisés

# filter_plugins/custom_filters.py

def generate_password(length=12, include_symbols=True):
    """Génère un mot de passe aléatoire"""
    import random
    import string
    
    chars = string.ascii_letters + string.digits
    if include_symbols:
        chars += "!@#$%^&*"
    
    return ''.join(random.choice(chars) for _ in range(length))

def format_bytes(bytes_value):
    """Formate les bytes en unités lisibles"""
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if bytes_value < 1024.0:
            return f"{bytes_value:.1f} {unit}"
        bytes_value /= 1024.0
    return f"{bytes_value:.1f} PB"

class FilterModule(object):
    def filters(self):
        return {
            'generate_password': generate_password,
            'format_bytes': format_bytes
        }

Stratégies de Déploiement

# playbooks/rolling-deployment.yml
---
- name: Rolling deployment with health checks
  hosts: webservers
  become: yes
  serial: 1  # Un serveur à la fois
  max_fail_percentage: 0  # Arrêter si un serveur échoue
  
  pre_tasks:
    - name: Remove server from load balancer
      uri:
        url: "http://{{ load_balancer_ip }}/api/servers/{{ inventory_hostname }}/disable"
        method: POST
        headers:
          Authorization: "Bearer {{ lb_api_token }}"
      delegate_to: localhost
      
    - name: Wait for connections to drain
      wait_for:
        timeout: 30
      
  tasks:
    - name: Deploy application
      include_role:
        name: application-deployment
        
  post_tasks:
    - name: Health check
      uri:
        url: "http://{{ ansible_default_ipv4.address }}/health"
        method: GET
        status_code: 200
      retries: 5
      delay: 10
      
    - name: Add server back to load balancer
      uri:
        url: "http://{{ load_balancer_ip }}/api/servers/{{ inventory_hostname }}/enable"
        method: POST
        headers:
          Authorization: "Bearer {{ lb_api_token }}"
      delegate_to: localhost

# playbooks/blue-green-deployment.yml
---
- name: Blue-Green deployment
  hosts: webservers
  become: yes
  
  vars:
    current_color: "{{ 'blue' if deployment_slot == 'green' else 'green' }}"
    target_color: "{{ deployment_slot }}"
    
  tasks:
    - name: Deploy to {{ target_color }} environment
      include_role:
        name: application-deployment
      vars:
        app_path: "/var/www/{{ app_name }}-{{ target_color }}"
        
    - name: Health check {{ target_color }} environment
      uri:
        url: "http://{{ ansible_default_ipv4.address }}:{{ target_color == 'blue' and '8080' or '8081' }}/health"
        method: GET
        status_code: 200
      retries: 10
      delay: 5
      
    - name: Switch load balancer to {{ target_color }}
      template:
        src: nginx-upstream.conf.j2
        dest: /etc/nginx/conf.d/app-upstream.conf
      vars:
        active_color: "{{ target_color }}"
      notify: reload nginx
      run_once: true
      delegate_to: "{{ groups['loadbalancers'][0] }}"

💡Monitoring et Debugging

Callbacks Personnalisés

# callback_plugins/custom_logger.py

from ansible.plugins.callback import CallbackBase
import json
import time
from datetime import datetime

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'custom_logger'
    
    def __init__(self):
        super(CallbackModule, self).__init__()
        self.start_time = time.time()
        self.results = []
    
    def v2_playbook_on_start(self, playbook):
        self.playbook_name = playbook._file_name
        print(f"Starting playbook: {self.playbook_name}")
    
    def v2_runner_on_ok(self, result):
        self.results.append({
            'host': result._host.get_name(),
            'task': result._task.get_name(),
            'status': 'ok',
            'changed': result._result.get('changed', False),
            'timestamp': datetime.now().isoformat()
        })
    
    def v2_runner_on_failed(self, result, ignore_errors=False):
        self.results.append({
            'host': result._host.get_name(),
            'task': result._task.get_name(),
            'status': 'failed',
            'error': result._result.get('msg', 'Unknown error'),
            'timestamp': datetime.now().isoformat()
        })
    
    def v2_playbook_on_stats(self, stats):
        duration = time.time() - self.start_time
        
        summary = {
            'playbook': self.playbook_name,
            'duration': f"{duration:.2f}s",
            'summary': {
                'ok': sum(1 for r in self.results if r['status'] == 'ok'),
                'failed': sum(1 for r in self.results if r['status'] == 'failed'),
                'changed': sum(1 for r in self.results if r.get('changed', False))
            },
            'results': self.results
        }
        
        # Sauvegarder dans un fichier JSON
        with open(f'/var/log/ansible-{int(time.time())}.json', 'w') as f:
            json.dump(summary, f, indent=2)
        
        print(f"Playbook completed in {duration:.2f}s")

Tests et Validation

# playbooks/test-infrastructure.yml
---
- name: Test infrastructure
  hosts: all
  gather_facts: yes
  
  tasks:
    - name: Check system uptime
      command: uptime
      register: uptime_result
      changed_when: false
      
    - name: Verify disk space
      assert:
        that:
          - ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first > 1000000000
        fail_msg: "Root filesystem has less than 1GB available"
        success_msg: "Root filesystem has sufficient space"
    
    - name: Check memory usage
      assert:
        that:
          - (ansible_memfree_mb / ansible_memtotal_mb * 100) > 10
        fail_msg: "Less than 10% memory available"
        success_msg: "Memory usage is acceptable"
    
    - name: Verify services are running
      service_facts:
      
    - name: Check critical services
      assert:
        that:
          - ansible_facts.services[item + '.service'].state == 'running'
        fail_msg: "Service {{ item }} is not running"
      loop:
        - nginx
        - mysql
        - ssh
      when: inventory_hostname in groups[item + '_servers'] | default([])

# molecule/default/molecule.yml (pour les tests de rôles)
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: ubuntu:20.04
    pre_build_image: true
provisioner:
  name: ansible
  inventory:
    host_vars:
      instance:
        ansible_python_interpreter: /usr/bin/python3
verifier:
  name: ansible

💡Optimisation et Bonnes Pratiques

Performance et Parallélisation

# ansible.cfg optimisé pour la performance
[defaults]
forks = 50
poll_interval = 1
timeout = 10
gather_timeout = 10
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400
gathering = smart

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=300s
pipelining = True
control_path_dir = /tmp/.ansible-cp

Idempotence et Tests

# Exemple de tâche idempotente
- name: Ensure application configuration
  template:
    src: app.conf.j2
    dest: /etc/myapp/app.conf
    owner: myapp
    group: myapp
    mode: '0644'
    backup: yes
  notify: restart myapp
  register: config_result

- name: Validate configuration syntax
  command: myapp --check-config /etc/myapp/app.conf
  changed_when: false
  when: config_result is changed

- name: Test configuration before restart
  command: myapp --test-config /etc/myapp/app.conf
  changed_when: false
  failed_when: false
  register: config_test
  when: config_result is changed

- name: Fail if configuration is invalid
  fail:
    msg: "Configuration validation failed"
  when: 
    - config_result is changed
    - config_test.rc != 0

💡Conclusion

Ansible transforme la gestion d'infrastructure en permettant :

Automatisation Complète

Simplicité d'Usage

Flexibilité

Fiabilité

Les techniques présentées dans cet article vous permettront de maîtriser Ansible pour automatiser efficacement vos infrastructures. L'investissement dans l'apprentissage d'Ansible se traduit rapidement par des gains de productivité et de fiabilité significatifs.

Pour un accompagnement dans la mise en place de vos automatisations Ansible, contactez-moi pour une consultation personnalisée.

À propos de l'auteur

Florian Courouge - Expert DevOps et Apache Kafka avec plus de 5 ans d'expérience dans l'architecture de systèmes distribués et l'automatisation d'infrastructures.

Cet article vous a été utile ?

Découvrez mes autres articles techniques ou contactez-moi pour discuter de vos projets DevOps et Kafka.