Mise à jour des paquets, synchro NTP et notifications par e-mail HTML sur Ubuntu 24.04
Gérer manuellement plusieurs serveurs Linux devient vite fastidieux et source d’erreurs : oubli d’une mise à jour, décalage horaire non détecté, configuration NTP hétérogène… Ansible répond à ce problème en permettant d’automatiser l’administration système de façon reproductible, sans agent à installer sur les machines cibles.
Nous allons mettre en place un serveur Ansible sous Ubuntu 24.04 LTS, configurer un inventaire de serveurs, et déployer un playbook complet qui réalise en une seule exécution :
- La configuration du fuseau horaire (Europe/Paris) sur tous les serveurs
- Le déploiement de Chrony et la synchronisation NTP sur un serveur interne
- La mise à jour complète des paquets (
apt upgrade) - L’envoi d’un e-mail récapitulatif HTML avec le statut de chaque serveur
Un point important de cette configuration : le port SSH par défaut (22) a été remplacé par un port personnalisé sur tous les serveurs, ce que nous intégrerons dans la configuration Ansible.
Pour reproduire cette configuration, vous aurez besoin de :
- Un serveur Ansible sous Ubuntu 24.04 LTS (le « contrôleur »)
- Plusieurs serveurs Linux Ubuntu 24.04 à gérer (les « nœuds »)
- Un serveur NTP interne accessible sur le réseau (port UDP 123)
- Un serveur SMTP interne pour l’envoi des e-mails (Postfix ou équivalent)
- Un utilisateur commun avec accès sudo sur tous les serveurs
Authentification SSH par clé
Ansible utilise SSH pour se connecter aux nœuds. Il est impératif de configurer une authentification par clé publique/privée pour éviter de saisir des mots de passe à chaque exécution.
Sur le serveur Ansible, générez une paire de clés SSH si ce n’est pas déjà fait :
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa
Puis déployez la clé publique sur chaque nœud. Si votre port SSH est personnalisé (ici le port 12345 sera utilisé comme exemple dans cet article), spécifiez-le explicitement :
ssh-copy-id -p 12345 -i ~/.ssh/id_rsa.pub utilisateur@192.168.1.10
Vérifiez que la connexion fonctionne sans mot de passe :
ssh -p 12345 utilisateur@192.168.1.10 'echo Connexion OK'
Installation d’Ansible
Sur Ubuntu 24.04, Ansible est disponible dans les dépôts officiels :
sudo apt update
sudo apt install ansible -y
Vérifiez l’installation :
ansible --version
Vous devriez obtenir une sortie similaire à :
ansible [core 2.16.3]
config file = /etc/ansible/ansible.cfg
python version = 3.12.3
jinja version = 3.1.2
Installez également la collection community.general nécessaire pour la gestion du fuseau horaire :
ansible-galaxy collection install community.general
Configuration de l’inventaire
L’inventaire Ansible liste les machines à gérer. Éditez le fichier /etc/ansible/hosts :
sudo nano /etc/ansible/hosts
Voici un exemple d’inventaire avec plusieurs serveurs répartis sur différents sous-réseaux, en utilisant un port SSH personnalisé :
[serveurs]
srv-ansible ansible_host=192.168.1.5 ansible_port=12345 ansible_user=admin ansible_ssh_private_key_file=/home/admin/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
srv-ntp ansible_host=192.168.1.10 ansible_port=12345 ansible_user=admin ansible_ssh_private_key_file=/home/admin/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
srv-web ansible_host=192.168.2.20 ansible_port=12345 ansible_user=admin ansible_ssh_private_key_file=/home/admin/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
srv-mail ansible_host=192.168.2.35 ansible_port=12345 ansible_user=admin ansible_ssh_private_key_file=/home/admin/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
srv-backup ansible_host=192.168.3.50 ansible_port=12345 ansible_user=admin ansible_ssh_private_key_file=/home/admin/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
| Paramètre | Description |
|---|---|
ansible_host |
Adresse IP ou nom DNS du serveur cible |
ansible_port |
Port SSH (ici 12345 au lieu du port 22 par défaut) |
ansible_user |
Utilisateur SSH avec droits sudo |
ansible_ssh_private_key_file |
Chemin vers la clé privée SSH |
ansible_python_interpreter |
Force l’utilisation de Python 3 |
Testez la connectivité vers tous les serveurs :
ansible serveurs -m ping
Une réponse pong pour chaque serveur confirme que tout est en ordre.
Le playbook de maintenance
Créez le fichier du playbook :
sudo nano /etc/ansible/update_servers.yml
Structure générale
Le playbook commence par déclarer ses métadonnées et le groupe de serveurs cibles :
---
- name: Configuration NTP, fuseau horaire et mise à jour des paquets
hosts: serveurs
become: true
become_user: root
tasks:
Le paramètre become: true permet l’escalade de privilèges via sudo, nécessaire pour les opérations d’administration système.
Tâche 1 : Configuration du fuseau horaire
- name: Définir le fuseau horaire sur Europe/Paris
community.general.timezone:
name: Europe/Paris
register: timezone_set
when: inventory_hostname != 'srv-ntp'
Le module community.general.timezone configure le fuseau horaire de façon idempotente : il ne modifiera le système que si nécessaire. Le résultat est stocké dans timezone_set pour être utilisé dans le rapport final.
UPDATE :
Je rajoute le parametre when: inventory_hostname != 'srv-ntp' par ce que mon serveur ntp est lui meme un chrony et qu’il fait partie de la liste des serveur a updater donc si j’ecase la config du serveur par la config des clients ca va pas le faire
Tâche 2 : Installation de Chrony
- name: S'assurer que Chrony est installé
ansible.builtin.apt:
name: chrony
state: present
register: chrony_installed
when: inventory_hostname != 'srv-ntp'
Le paramètre state: present garantit que le paquet est installé sans le réinstaller s’il l’est déjà. C’est le principe d’idempotence d’Ansible.
Tâche 3 : Déploiement de la configuration NTP
C’est une tâche clé : au lieu d’ajouter simplement une ligne dans le fichier de configuration existant (ce qui laisserait les serveurs publics Ubuntu actifs), on remplace intégralement le fichier chrony.conf par une configuration épurée pointant uniquement vers notre serveur NTP interne :
- name: Déployer la configuration chrony.conf
ansible.builtin.copy:
dest: /etc/chrony/chrony.conf
content: |
server 192.168.1.10 iburst
keyfile /etc/chrony/chrony.keys
driftfile /var/lib/chrony/chrony.drift
ntsdumpdir /var/lib/chrony
logdir /var/log/chrony
maxupdateskew 100.0
rtcsync
makestep 1 3
leapsectz right/UTC
backup: yes
register: chrony_config_changed
when: inventory_hostname != 'srv-ntp'
Le paramètre backup: yes crée automatiquement une sauvegarde de l’ancien fichier avant de l’écraser. Le paramètre iburst accélère la synchronisation initiale en envoyant plusieurs requêtes rapprochées au démarrage.
Pourquoi remplacer entièrement le fichier ? Par défaut, Ubuntu 24.04 configure Chrony avec plusieurs pools de serveurs publics. Si on se contente d’ajouter notre serveur interne en fin de fichier, Chrony continuera à utiliser les serveurs publics en priorité. En remplaçant le fichier, on garantit que seul notre NTP interne est utilisé.
Tâche 4 : Redémarrage de Chrony
- name: Redémarrer le service Chrony
ansible.builtin.systemd:
name: chronyd
state: restarted
enabled: yes
register: chrony_service_restarted
when: inventory_hostname != 'srv-ntp'
Le paramètre enabled: yes s’assure également que Chrony démarrera automatiquement au prochain reboot du serveur.
Tâche 5 : Vérification de la synchronisation NTP
- name: Vérifier la synchronisation NTP
ansible.builtin.command:
cmd: chronyc tracking
register: ntp_status
changed_when: false
Le paramètre changed_when: false indique à Ansible que cette tâche ne modifie jamais l’état du système. Sans ce paramètre, Ansible afficherait un CHANGED à chaque exécution, ce qui fausserait les statistiques.
Tâches 6 et 7 : Mise à jour des paquets
- name: Mettre à jour la liste des paquets
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
register: apt_update_result
- name: Effectuer une mise à jour complète des paquets
ansible.builtin.apt:
upgrade: full
autoremove: yes
autoclean: yes
register: apt_upgrade_result
Le paramètre cache_valid_time: 3600 évite de re-télécharger les listes de paquets si elles ont déjà été mises à jour il y a moins d’une heure. Le paramètre upgrade: full est l’équivalent de apt full-upgrade qui gère également les changements de dépendances. Les options autoremove et autoclean nettoient les paquets orphelins et le cache APT.
Tâche 8 : Détection du besoin de redémarrage
- name: Vérifier si un redémarrage est nécessaire
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required
Après une mise à jour noyau ou de bibliothèques critiques, Ubuntu crée le fichier /var/run/reboot-required. Ce module vérifie son existence pour le signaler dans le rapport. Ansible ne redémarre pas le serveur automatiquement — c’est une décision volontaire pour éviter toute interruption de service non planifiée.
Tâche 9 : Construction du rapport HTML
C’est la tâche la plus complexe. Elle utilise le moteur de templates Jinja2 intégré à Ansible pour construire dynamiquement un e-mail HTML avec le statut de chaque serveur :
- name: Construire le message récapitulatif global
ansible.builtin.set_fact:
recap_message: |
<html>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
<div style="max-width: 800px; margin: auto; background: white; border-radius: 8px; padding: 20px;">
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
??? Rapport de mise à jour Ansible
</h2>
<p style="color: #7f8c8d;">Exécuté le {{ ansible_date_time.date }} à {{ ansible_date_time.time }}</p>
{% for host in ansible_play_hosts %}
<div style="margin-top: 20px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;">
<div style="background-color: #3498db; color: white; padding: 10px 15px;">
<h3 style="margin: 0;">{{ host | upper }}</h3>
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background-color: #f9f9f9;">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee; width: 60%;">Fuseau horaire (Europe/Paris)</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if hostvars[host].timezone_set.changed %}
<span style="color: #e67e22;">?? MODIFIÉ</span>
{% else %}
<span style="color: #27ae60;">? OK</span>
{% endif %}
</td>
</tr>
<tr>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Chrony</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if hostvars[host].chrony_installed.changed %}
<span style="color: #e67e22;">?? INSTALLÉ</span>
{% else %}
<span style="color: #27ae60;">? PRÉSENT</span>
{% endif %}
</td>
</tr>
<tr style="background-color: #f9f9f9;">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Configuration NTP</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if hostvars[host].chrony_config_changed.changed %}
<span style="color: #e67e22;">?? MODIFIÉ</span>
{% else %}
<span style="color: #27ae60;">? OK</span>
{% endif %}
</td>
</tr>
<tr>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Mise à jour des paquets</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if hostvars[host].apt_upgrade_result.changed %}
<span style="color: #3498db;">?? EFFECTUÉE</span>
{% else %}
<span style="color: #27ae60;">? À JOUR</span>
{% endif %}
</td>
</tr>
<tr style="background-color: #f9f9f9;">
<td style="padding: 8px 15px;">Redémarrage nécessaire</td>
<td style="padding: 8px 15px;">
{% if hostvars[host].reboot_required.stat.exists %}
<span style="color: #e74c3c;">?? OUI</span>
{% else %}
<span style="color: #27ae60;">? NON</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% endfor %}
</div>
</body>
</html>
run_once: true
Points importants de cette tâche :
run_once: true: la tâche ne s’exécute qu’une seule fois mais accède aux données de tous les serveurs viahostvarsansible_play_hosts: variable magique Ansible contenant la liste de tous les hôtes du play en courshostvars[host]: permet d’accéder aux variables d’un hôte spécifique depuis n’importe quelle tâcheansible_date_time: variable automatique Ansible contenant la date et l’heure d’exécution
Tâche 10 : Envoi de l’e-mail récapitulatif
- name: Envoyer un e-mail récapitulatif unique
ansible.builtin.mail:
host: smtp.mondomaine.local
port: 25
to: "admin@mondomaine.local"
subject: "Rapport de mise à jour Ansible - {{ ansible_date_time.date }}"
body: "{{ recap_message }}"
from: "ansible@srv-ansible.mondomaine.local"
secure: never
subtype: html
run_once: true
delegate_to: localhost
Deux paramètres méritent une explication :
secure: never: désactive le TLS pour l’envoi SMTP. Nécessaire si votre serveur SMTP interne n’a pas de certificat TLS configuré. Ne jamais utiliser cette option vers un serveur SMTP externe.delegate_to: localhost: délègue l’envoi de l’e-mail au serveur Ansible lui-même plutôt qu’à l’un des nœuds gérés. C’est logique puisque c’est le contrôleur qui doit envoyer le rapport.
Planification avec Cron
Pour automatiser l’exécution hebdomadaire, ajoutez une tâche cron sur le serveur Ansible. L’exemple ci-dessous planifie l’exécution chaque lundi à 3h00 :
sudo crontab -e
Ajoutez la ligne :
0 3 * * 1 ansible-playbook /etc/ansible/update_servers.yml >> /var/log/ansible-update.log 2>&1
La redirection >> /var/log/ansible-update.log 2>&1 sauvegarde la sortie complète (stdout et stderr) dans un fichier de log pour pouvoir diagnostiquer d’éventuels problèmes.
Résultat : le rapport par e-mail
À chaque exécution du playbook, vous recevez un e-mail HTML structuré avec un tableau par serveur :
| Indicateur | Signification |
|---|---|
| ? OK / PRÉSENT / À JOUR | La vérification est passée, aucune modification nécessaire |
| ?? MODIFIÉ / INSTALLÉ | Une modification a été apportée lors de cette exécution |
| ?? EFFECTUÉE | Des paquets ont été mis à jour |
| ?? OUI (redémarrage) | Un redémarrage est requis suite aux mises à jour |
Le sujet de l’e-mail inclut automatiquement la date d’exécution, ce qui permet de distinguer facilement les rapports dans votre boîte mail.
Dépannage fréquent
Le NTP ne se synchronise pas
Si chronyc sources affiche ? devant votre serveur NTP interne, vérifiez que le port UDP 123 est accessible :
nc -uz 192.168.1.10 123 && echo 'Port 123 OK' || echo 'Port 123 BLOQUÉ'
Si vous utilisez un serveur Windows comme référence NTP, vérifiez que le service est bien actif :
w32tm /query /status
L’e-mail n’arrive pas
Testez l’envoi SMTP depuis le serveur Ansible :
echo 'Test Ansible' | mail -s 'Test' admin@mondomaine.local
Vérifiez les logs du serveur SMTP et assurez-vous que le port 25 est accessible depuis le serveur Ansible.
Erreur StartTLS
Si vous obtenez l’erreur StartTLS is not offered on server, c’est que votre serveur SMTP interne ne supporte pas TLS. Vérifiez que vous avez bien secure: never dans la tâche d’envoi d’e-mail du playbook.
Voila ! Nous avons mis en place une solution complète d’automatisation avec Ansible qui gère en une seule commande la configuration NTP, le fuseau horaire, les mises à jour de paquets et la notification par e-mail sur l’ensemble du parc de serveurs Linux.
Quelques points clés à retenir :
- L’idempotence : chaque tâche Ansible vérifie l’état actuel avant d’agir. Vous pouvez exécuter le playbook autant de fois que vous voulez sans risque.
- Le remplacement complet de
chrony.confest essentiel pour garantir que seul votre NTP interne est utilisé. - Le rapport HTML avec Jinja2 et
hostvarspermet de centraliser les informations de tous les serveurs dans un seul e-mail lisible. - Le port SSH personnalisé est une mesure de sécurité simple qui réduit le bruit des scans automatiques, et s’intègre facilement dans l’inventaire Ansible.
Enjoy !
Annexe : Playbook complet
---
- name: Configuration NTP, fuseau horaire et mise à jour des paquets
hosts: serveurs
become: true
become_user: root
tasks:
- name: Définir le fuseau horaire sur Europe/Paris
community.general.timezone:
name: Europe/Paris
register: timezone_set
- name: S'assurer que Chrony est installé
ansible.builtin.apt:
name: chrony
state: present
register: chrony_installed
- name: Déployer la configuration chrony.conf
ansible.builtin.copy:
dest: /etc/chrony/chrony.conf
content: |
server 192.168.1.10 iburst
keyfile /etc/chrony/chrony.keys
driftfile /var/lib/chrony/chrony.drift
ntsdumpdir /var/lib/chrony
logdir /var/log/chrony
maxupdateskew 100.0
rtcsync
makestep 1 3
leapsectz right/UTC
backup: yes
register: chrony_config_changed
- name: Redémarrer le service Chrony
ansible.builtin.systemd:
name: chronyd
state: restarted
enabled: yes
register: chrony_service_restarted
- name: Vérifier la synchronisation NTP
ansible.builtin.command:
cmd: chronyc tracking
register: ntp_status
changed_when: false
- name: Mettre à jour la liste des paquets
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
register: apt_update_result
- name: Effectuer une mise à jour complète des paquets
ansible.builtin.apt:
upgrade: full
autoremove: yes
autoclean: yes
register: apt_upgrade_result
- name: Vérifier si un redémarrage est nécessaire
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required
- name: Construire le message récapitulatif global
ansible.builtin.set_fact:
recap_message: |
[... voir section Tâche 9 pour le contenu HTML complet ...]
run_once: true
- name: Envoyer un e-mail récapitulatif unique
ansible.builtin.mail:
host: smtp.mondomaine.local
port: 25
to: "admin@mondomaine.local"
subject: "Rapport Ansible - {{ ansible_date_time.date }}"
body: "{{ recap_message }}"
from: "ansible@srv-ansible.mondomaine.local"
secure: never
subtype: html
run_once: true
delegate_to: localhost