
🌐 Aussi en: English · Deutsch · Español
Chaque lundi, un petit script m’envoie un rapport de versions : quelles versions épinglées sont dépassées, quelles images ont été reconstruites sous le même tag, quels paquets attendent sur les hôtes ? Cette fois, la liste était longue – dont le passage de Nextcloud 34 à 35 sur les deux instances. En fin d’après-midi, tout était à jour, aucun service cassé, et la vérification de dérive affichait changed=0.
Cet article reprend les étapes dans l’ordre. Chacune apporte sa petite leçon – et l’une d’elles était un bug dans mon propre outil.
📬 Le rapport informe, il n’agit pas
Le script tourne sur un serveur de mon homelab et ne fait que lire. Il compare les versions épinglées dans versions.yml avec Docker Hub et GitHub, vérifie si une image a été reconstruite sous le même tag, compare la version de PHP dans les pods avec php.net et compte les paquets en attente par hôte. Il ne modifie rien : j’applique les mises à jour moi-même, volontairement, un hôte après l’autre.
Cette fois, le rapport indiquait : Nextcloud 35.0.2 disponible, Trivy 0.76.1 disponible, l’image WordPress « reconstruite » et six paquets du noyau sur les trois hôtes.
Comment fonctionne le rapport : le script est un programme Python qui tourne sur HP3, un serveur de mon homelab qui récupère aussi chaque nuit les sauvegardes des trois serveurs – pas sur les serveurs eux-mêmes. Une entrée cron le lance chaque lundi à 07:17, et il envoie l’e-mail par son propre compte de messagerie. En cinq étapes :
- Il lit
versions.ymldans la copie locale du dépôt – chaque version épinglée s’y trouve à un seul endroit. - Il interroge Docker Hub pour chaque image et GitHub pour la dernière version de chaque binaire, puis compare avec la version épinglée.
- Il se connecte en SSH à chaque serveur, liste avec
crictl imagesles images réellement présentes et les compare avec la version actuelle publiée sous le même tag. - Il lit la version de PHP dans les pods en cours d’exécution et la compare avec php.net.
- Il compte les paquets en attente par hôte avec
dnf check-update.
Tous les constats arrivent dans un seul e-mail – avec les commandes des mises à jour dans le bon ordre. Les exécuter reste mon affaire.
Voici à quoi ressemblait le rapport de ce jour de mise à jour (seuls les noms des deux serveurs Nextcloud et les noms d’hôtes des commandes sont modifiés) :
== Container images (pin vs Docker Hub) ==
nextcloud_image_fpm 34.0.5-fpm (newest overall 35.0.2-fpm)
nextcloud_image_nginx 1.30.6-alpine (newest overall 1.31.8-alpine)
nextcloud_image_collabora 26.04.5.2.1 ok
nextcloud_image_fpm_exporter 2.2.0 ok
blog_image_wordpress 7.1.4-php8.5-fpm ok
blog_image_nginx 1.30.6-alpine (newest overall 1.31.8-alpine)
blog_image_mariadb 10.11.19 (newest overall 13.0.2)
blog_image_redis 7.4.12-alpine (newest overall 8.10.3-alpine)
blog_image_mysqld_exporter v0.21.0 ok
blog_image_fpm_exporter 2.2.0 ok
== Binaries / charts (pin vs GitHub latest release) ==
k3s_version v1.37.1+k3s1 ok
helm_version v4.3.1 ok
helm_diff_version 3.15.15 ok
node_exporter_version 1.12.1 ok
prometheus_version 3.15.1 ok
mysqld_exporter_version 0.21.0 ok
loki_version 3.7.9 ok
alloy_version 1.21.3 ok
trivy_version 0.75.0 -> v0.76.1
blog_wp_cli_version 2.12.0 ok
cert_manager_chart_version 1.21.3 ok
== Same tag rebuilt upstream (image refresh needed) ==
blog wordpress:7.1.4-php8.5-fpm REBUILT upstream
== PHP in the pods ==
blog PHP 8.5.12
cloud1 PHP 8.5.12
cloud2 PHP 8.5.12
php.net 8.4: 8.4.27
php.net 8.5: 8.5.12
== Host packages (dnf check-update) ==
blog 6 pending incl. kernel, kernel-core, kernel-modules, kernel-tools ...
cloud1 6 pending incl. kernel, kernel-core, kernel-modules, kernel-tools ...
cloud2 6 pending incl. kernel, kernel-core, kernel-modules, kernel-tools ...
Update runs (one host at a time, blog first):
ansible-playbook blog-update.yml -l <blog host> --vault-password-file .vault_pass
ansible-playbook nextcloud-update.yml -l <nextcloud host> --vault-password-file .vault_pass (one at a time)
then per host: --tags common_rkhunter,common_aide_refresh
Pins (versions.yml) are changed by hand or via the Renovate PRs - this report only informs.Pourquoi pas la version la plus récente partout ? Pour certains composants, le rapport indique une version majeure plus récente. Ce n’est pas un oubli, mais un choix :
| Composant | Branche | Source | Pourquoi |
|---|---|---|---|
| MariaDB | 10.11 | Docker Hub / dépôt MariaDB | LTS, prise en charge jusqu’en février 2028 |
| Redis | 7.4 et 7.2 | Docker Hub / AlmaLinux | dernières branches de Redis 7, prises en charge jusqu’à fin 2029 |
| nginx | 1.30 | Docker Hub | branche stable ; le contrôleur d’ingress apporte sa propre version |
Pour les paquets d’une distribution, le numéro de version dit d’ailleurs peu de chose : il est souvent en retard sur celui de l’éditeur, parce que la distribution intègre les correctifs à sa manière. Ce n’est donc pas la comparaison avec l’éditeur qui montre si un paquet est exposé, mais le bulletin de sécurité de la distribution.
À retenir : « à jour » ne veut pas dire « dernière version majeure », mais : le dernier état de la branche choisie volontairement et encore prise en charge.
📦 Pour le reproduire : weekly-update-check.zip (script, wrapper cron et petit README, 4 Ko). Les hôtes sont des valeurs d’exemple en haut de check.py ; les versions épinglées viennent de versions.yml dans mon dépôt public. Licence MIT, sans garantie – à vérifier soi-même avant toute utilisation.
À retenir : un rapport qui ne fait qu’informer peut se permettre d’être exhaustif. La décision de ce qui est appliqué, et quand, reste humaine.
☁️ Nextcloud 34 → 35 : vérifier d’abord, sauter ensuite
Avec Nextcloud, une mise à jour majeure n’est jamais automatique. On ne peut pas sauter de version, et les applications du magasin ont besoin d’une version compatible avec la nouvelle version majeure. Avant le saut, j’ai donc vérifié :
- L’image existe-t-elle ?
nextcloud:35.0.2-fpmétait disponible sur Docker Hub. - Les applications suivent-elles ? L’API du magasin liste les applications disponibles par version de la plateforme. Les neuf applications du magasin de chaque instance – dont Collabora, Deck, notify_push et Groupfolders – avaient une version pour la 35.
- Les prérequis sont-ils remplis ? Nextcloud 35 exige au moins MariaDB 10.11 et abandonne PHP 8.2. La base tournait déjà en 10.11, et PHP est fourni par l’image du conteneur.
- Y a-t-il un retour possible ? En plus de la sauvegarde nocturne, j’ai fait un dump frais de la base juste avant la mise à jour.
J’ai d’abord fixé la nouvelle version pour une seule instance, dans ses variables d’hôte. La seconde instance et la valeur globale sont restées en 34.0.5 jusqu’à ce que la première tourne proprement. Ce n’est qu’ensuite que 35.0.2 est passée dans versions.yml, et les deux épinglages par hôte ont été retirés.
À retenir : une mise à jour majeure ne commence pas par la mise à jour, mais par trois questions : tout est-il disponible, tout est-il compatible, puis-je revenir en arrière ?
⚙️ Ce qui se passe vraiment pendant la mise à jour
Ce n’est pas Ansible qui lance la mise à jour, mais l’image Nextcloud elle-même. Au démarrage, son script d’entrée compare la version installée avec celle de l’image. Si l’image est plus récente, il copie le nouveau code et lance occ upgrade, qui migre la base de données. Pendant ce temps, Nextcloud est en mode maintenance.
Le playbook garantit deux choses :
- Recreate plutôt qu’un rolling update : avec une nouvelle image, l’ancien pod s’arrête complètement avant que le nouveau démarre. Sinon, l’ancien et le nouveau code travailleraient en même temps dans le même répertoire.
- Une sonde de démarrage patiente : PHP-FPM ne démarre qu’après la mise à jour. La startupProbe accorde jusqu’à dix minutes, pour que Kubernetes ne prenne pas une longue migration de base pour un plantage.
Cela a pris 40 et 47 secondes. Le journal contenait pourtant des lignes inquiétantes au premier abord : « Disabled incompatible app: richdocuments » – et ainsi de suite pour chaque application du magasin. C’est normal : lors d’une mise à jour majeure, Nextcloud désactive toute application dont la version installée ne correspond pas à la nouvelle version majeure. L’exécution suivante de nextcloud-update.yml récupère les versions adaptées, les réactive et redémarre le pod, pour qu’OPcache n’exécute plus d’ancien bytecode.
Ensuite, toutes les applications étaient revenues, la plupart dans une nouvelle version (Collabora 11 → 12, Groupfolders 22 → 23). Nouvelle venue : l’application intégrée sharing, pour le partage unifié de Nextcloud 35.
À retenir : une mise à jour majeure comporte deux étapes indissociables – le cœur, puis les applications. S’arrêter après la première, c’est avoir un Nextcloud sans Office.
🔍 Ce que « terminé » veut dire chez moi
« La page s’affiche » ne me suffit pas comme recette. Après chaque instance, j’ai vérifié :
- le journal de Nextcloud : aucune erreur depuis la mise à jour, seulement deux ou trois avertissements connus concernant des applications désactivées volontairement
- les journaux du serveur web : aucune réponse 5xx
- l’autotest de notify_push
- sur l’hôte : aucun refus SELinux, MariaDB et Redis dans leurs propres domaines, aucune unité en échec, toutes les cibles Prometheus joignables
- et pour finir la vérification de dérive : une exécution du playbook en mode check qui ne doit plus rien changer
Les deux instances ont affiché changed=0. C’est seulement là qu’une mise à jour est terminée pour moi : le serveur est exactement ce que décrit le code.
À retenir : « ça marche » est une impression, changed=0 est un constat.
🧪 La vérification de dérive en détail
La vérification de dérive est un second script, tout à fait différent : scripts/check-drift.sh dans le dépôt public. Il ne tourne pas selon un planning, mais à la main, sur la machine depuis laquelle je lance Ansible – toujours juste après une vraie exécution de playbook et après chaque mise à jour.
L’idée : Ansible décrit l’état voulu d’un serveur. Si l’on relance le même playbook en mode check, il ne doit plus rien rester à faire. Le script fait exactement cela :
- Il appelle
ansible-playbook … --check --diffpour un hôte. En mode check, Ansible ne modifie rien ; il signale seulement ce qu’il ferait. - Il récupère la sortie en JSON et en extrait les tâches qui signaleraient « changed » – avec leur diff. Pour les objets Kubernetes, il masque les champs de gestion du serveur, pour ne garder que les vraies différences.
- Le code de sortie donne le résultat :
0= pas de dérive,1= dérive,2= l’exécution de contrôle elle-même a échoué.
Voici ce que cela donnait aujourd’hui après la mise à jour :
$ scripts/check-drift.sh nextcloud-k3s.yml <host>
<host>: changed=0 failed=0
NO DRIFTEt voici à quoi ressemble une détection – un cas plus ancien, où une tâche signalait « changed » à chaque exécution alors que rien n’avait changé :
### would change: rkhunter | Build / refresh file-properties database
<host>: changed=1 failed=0
DRIFT: 1 task(s) would changeUne détection juste après une vraie exécution signifie toujours l’une de deux choses : quelqu’un a modifié le serveur à la main, ou une tâche du code n’est pas idempotente. Dans les deux cas, on veut le savoir avant que la prochaine exécution ne le « corrige » en silence.
À retenir : la vérification de dérive est la réception finale par rapport au plan. Elle ne prouve pas que tout fonctionne – mais que le serveur correspond exactement au code.
⏳ Deux semaines d’attente pour Trivy
Trivy, mon scanner d’images nocturne, avait une nouvelle version. Je ne l’ai pas installée. En mars 2026, la chaîne de publication de Trivy a été compromise et a distribué une version manipulée. Depuis, j’applique une règle : Trivy uniquement dans des versions publiées depuis au moins deux semaines, toujours avec une somme SHA-256 fixe tirée du fichier de sommes de contrôle de la version.
La version 0.76.1 est sortie le 21 octobre : elle n’est donc autorisée qu’à partir du 4 novembre. J’ai vérifié la somme à l’avance – l’archive téléchargée correspond au fichier officiel. Le commit attend un jour de plus.
À retenir : pour les outils de sécurité, « récent » ne veut pas automatiquement dire « plus sûr ». Un délai d’attente coûte peu, et il aurait servi en mars.
🧩 La fausse alerte : nouvel index, même image
Le rapport signalait wordpress:7.1.4-php8.5-fpm comme « reconstruite en amont ». Le playbook de mise à jour l’a récupérée – et n’a redémarré aucun pod. Le journal de containerd expliquait pourquoi : environ 21 Ko lus, et le même identifiant d’image.
L’explication : mon script comparait le digest de l’index. Un tag sur Docker Hub pointe vers un index qui liste les images pour toutes les plateformes. Si l’image d’une autre architecture change, ou seulement une attestation, l’index reçoit un nouveau digest – mais l’image amd64 que j’exécute reste identique, octet pour octet.
La correction était simple : le script résout désormais l’index, prend l’entrée linux/amd64 et compare son digest de configuration avec l’identifiant de l’image sur l’hôte. C’est exactement ce qu’exécute le conteneur. Depuis : « none – every pinned tag is current ».
À retenir : un nouveau digest ne signifie pas une nouvelle image. Il faut comparer ce qui tourne réellement.
🐧 Pourquoi Ansible n’installe jamais mes noyaux
Restaient les paquets du noyau. Mon playbook de paquets installe toutes les mises à jour – sauf le noyau, volontairement. Le service SSH est durci avec systemd, notamment avec ProtectKernelModules=yes. Chaque session SSH, y compris celle d’Ansible, tourne donc dans son propre espace de noms de montage, où /usr/lib/modules apparaît vide et en lecture seule. Une installation de noyau via SSH y échoue à coup sûr.
Les noyaux passent donc par un autre chemin :
- dnf-automatic installe la nuit les seules mises à jour de sécurité, en tant que service systemd – hors de toute session SSH. Le nouveau noyau était une telle mise à jour de sécurité.
- À 03:30, l’hôte redémarre si
needs-restarting -rle demande. - À 04:00, AIDE met à jour sa base de référence, pour que le nouveau noyau ne déclenche pas d’alerte le lendemain.
Cette fois, je ne voulais pas attendre la nuit. Au lieu d’installer le paquet via SSH, j’ai lancé à la main dnf-automatic-install.service sur chaque hôte – le même chemin que la nuit, simplement plus tôt. Puis un redémarrage, un hôte après l’autre. Chacun est revenu au bout de 40 à 50 secondes, avec le nouveau noyau, sans refus SELinux et avec tous ses pods en fonctionnement.
Ce moment avancé avait une conséquence : le contrôle quotidien d’AIDE tourne à minuit, avant la mise à jour de 04:00. Sans rien faire, il aurait signalé le nouveau noyau comme une modification. Chaque hôte a donc besoin ensuite d’un rafraîchissement d’AIDE et de rkhunter.
À retenir : le durcissement a des effets sur sa propre automatisation. Plutôt que de l’affaiblir, on donne à la mise à jour du noyau un chemin adapté.
🎯 Les leçons
- Les rapports informent, les humains décident – vérifier automatiquement, appliquer volontairement.
- Trois questions avant une mise à jour majeure – tout est-il disponible, compatible, et puis-je revenir en arrière ?
- Le cœur et les applications forment une seule mise à jour – Nextcloud n’est terminé que quand les applications tournent à nouveau.
- Comparer ce qui tourne réellement – pas l’index, mais l’image de sa propre plateforme.
- Terminé veut dire
changed=0– la vérification de dérive clôt chaque jour de mise à jour.





