Mon infra se met à jour toute seule !
Dans le précédent billet, je vous ai expliqué le fonctionnement de mon infra perso, avec une structure Ansible me permettant de déployer chaque service indépendemment des autres. Cette manière de gérer mon infra m’a permis de gagner énormément de temps, mais il me restait encore une chose, longue, chiante et rébarbative à faire : Mettre à jour mon infra.
Si vous avez déjà hébergé par vous-même des services sur un temps assez long, vous savez que les mises à jour sont le parent pauvre des
infrastructures (et même en entreprise, ne me dites pas que votre parc est toujours à jour). Si les solutions de conteneurisation et d’orchestration
modernes (Podman, Docker, Kubernetes Swarm, Rancher, […]) ont résolu de nombreux problèmes en découplant proprement l’OS et les services, il
n’en reste
pas moins que les mises à jour sont souvent vécues comme une souffrance : c’est du temps de pur run, perdu “à ne rien faire”. Et en général, les
mises à jour, ça se passe comme ça :
- On déploie nos nouveaux services tout beaux, tout neufs, sur notre orchestrateur favori ;
- On oublie ce service pendant 8 mois ;
- Le SOC de votre boîte (ou vous même avec votre veille) trouve une vulnérabilité ;
- Vous devez mettre à jour en urgence le service en question, sauf qu’il y a 5 versions d’écart ;
- Et vous passez 2 jours à migrer alors que vous auriez mis 20x moins de temps en le faisant au fil de l’eau.
A la maison, c’est beaucoup plus simple, on y va en fire & forget, le service déployé est désormais une production que plus personne ne touchera. 😎
Hors, à la maison, j’ai aussi de la prod. C’est peut-être juste ma prod à moi, mais il n’empêche qu’elle est importante pour moi (et c’est pour ça qu’elle est backupée d’ailleurs, cf ce post) : mes mots de passe, mes fichiers, mon agenda, mes mails, mon code… Bref, que des choses que je n’ai pas envie de voir exposées à cause d’une faille de sécurité potentielle. Sauf que, assez logiquement, mon infra à la maison, ce n’est pas mon boulot à plein temps. C’est un side-project (voire même un side-side-project), sur lequel je consacre peu de temps. Il me fallait donc trouver une solution pour automatiser un peu tout ça, et retrouver une véritable hygiène numérique sur ce pan là.
Et ça tombe bien, car il y à quelques mois, Olivier est arrivé, étoiles dans les yeux au bureau, et nous a présenté n8n (prononcez-le nodemation). n8n, c’est un moteur de workflow, comme IFTTT ou Zappier, mais en beaucoup plus généraliste (et open-source + self-hostable) ! Il s’en sert pour automatiser sa veille techno, et je ne peux que vous encourager à aller lire son excellent billet de blog à ce sujet.
D’ailleurs, sachez que j’ai totalement copié son idée à la maison, et que c’est vraiment trop pratique : je reçois sur mon serveur Discord les news locales, IT, et les conférences à venir (et les alertes quand la T2C coupe le tramway 🤬).
Et, partant de ce constat, je me suis posé une question : Et si n8n pouvait détecter des mises à jour sur mon infra, me les proposer, et les déployer ? Et bien devinez quoi : c’est possible, et je l’ai fait ! Alors, après cette introduction BEAUCOUP trop longue, c’est parti pour automatiser nos mises à jour. 😎
Infra de base
Pour les besoins de cet article, nous allons partir sur les prédicats suivants :
- Vous avez une infra qui publie ses services sur un Docker Swarm ;
- Note : La seule chose importante que Swarm me donne, c’est le rollback automatique des services. Ça fonctionnera à merveille sur un Kubernetes, Minikube, Rancher, ou n’importe quel orchestrateur de containers digne de ce nom. D’ailleurs, mon swarm est en mono-node, donc vraiment, tant que vous avez le rollback auto, le reste, on s’en tape.
- Vous déployez votre infra avec Ansible ;
- Note : La seule chose importante que Ansible me donne, c’est la possibilité de lancer un déploiement en automatique depuis une CI. Puppet, Salt, Chef, ArgoCD… Tant que vous avez un truc qui permet de faire ça, pareil, ça fonctionnera. Même du bash, c’est ok (tant que ça peut lire des variables d’environnement).
- Vous avez un serveur GitLab avec un runner à disposition (et ce runner peut déployer sur votre serveur).
- Note : Vous avez compris, ici GitLab sert juste à lancer des pipelines en automatique. Jenkins, Gitea, BitBucket, GitHub Actions… N’importe quoi fera l’affaire (il faut juste pouvoir ajouter des variables d’environnement à l’exécution).
- Il vous faut également un compte et un serveur Discord (c’est gratuit et ça prend 5 minutes à faire).
- Vous pouvez utiliser n’importe quel autre système de messagerie compatible avec n8n (Telegram, mail, Matrix, etc…)
Vous l’aurez compris, cette infra n’est absolument pas obligatoire : adaptez à votre environnement, vous verrez, ce sera pas très long.
Installer n8n
Première chose à faire : Installer n8n. Pour cela, rien de plus simple, nous allons démarrer un nouveau service Swarm. Et comme on est pas des sauvages, on va le faire avec Ansible :
---
- name: "deploy n8n"
hosts: app_servers
vars:
data_location: "/srv/n8n" #Change with your value
tasks:
- name: "create dirs"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0777'
loop:
- "{{ data_location }}/data"
- "{{ data_location }}/files"
- name: "Create n8n service"
run_once: true
community.general.docker_swarm_service:
name: "n8n"
image: "docker.n8n.io/n8nio/n8n:1.105.3"
mode: "replicated"
replicas: 1
mounts:
- source: "{{ data_location }}/data"
target: "/home/node/.n8n"
type: "bind"
- source: "{{ data_location }}/files"
target: "/files"
type: "bind"
publish:
- mode: "ingress"
protocol: "tcp"
published_port: 5678
target_port: 5678
rollback_config:
failure_action: "pause"
order: "stop-first"
update_config:
failure_action: "rollback"
order: "stop-first"
monitor: "1m"
restart_config:
condition: any
delay: 5s
max_attempts: 3
window: 120s
Votre serveur n8n devrait être accessible sur le port 5678 de votre serveur. Connectez vous sur l’interface. Maintenant, prenons un peu de temps pour parler du fonctionnement de n8n à proprement parler.
n8n, sa vie, son oeuvre
n8n est un moteur de workflow. Un workflow consiste en un ensemble d’actions qui vont être exécutées par n8n de manière séquentielles. Ces actions peuvent être chaînées. Ainsi, l’action 2 peut se baser sur le résultat de l’action 1, et ainsi de suite.
Et les workflows peuvent être déclenchées de plusieurs manières : automatiquement, manuellement, en se basant sur un événement extérieur, etc. D’ailleurs, c’est la grande force de n8n : il existe des connecteurs (actions et triggers) pour à peu près tout et n’importe quoi, allant de Discord jusqu’à Palo Alto en passant par Gemini et Dropbox.
Commençons par un exemple tout simple : récupérer un flux RSS, et envoyer les nouveaux éléments sur un canal Discord. Créez un nouveau workflow dans votre n8n, et vous allez arriver sur cette interface :
Cliquons sur “Add first step”. Cette première étape doit obligatoirement être un trigger, c’est à dire une étape qui permet de démarrer notre workflow. Vous pouvez voir que de nombreuses options sont à disposition : Depuis un webhook, à la demande, selon un cron, etc. Recherchez “RSS” dans la barre de recherche, et sélectionnez “RSS Feed Trigger”.
Vous voici dans la configuration d’un noeud. Pour chaque étape, vous allez pouvoir donner des informations à n8n pour lui indiquer comment le noeud en
question doit se comporter. Ici, le trigger est assez simple : vous pouvez définir la fréquence à laquelle le workflow va s’exécuter, et une adresse
RSS. Vous pouvez utiliser celle du blog pour vos tests (https://forestier.re/index.xml
). Cliquez sur “Fetch Test Event”, et vous devriez voir la
partie de droite afficher du contenu :
Hourra, nous avons récupéré du contenu ! Nous pouvons maintenant nous en servir. Cliquez sur “Back to canvas”, en haut à gauche, puis sur le petit “+”
situé à droite de votre premier noeud. Pour l’instant, votre workflow va s’exécuter à chaque fois, même si je n’ai rien publié de nouveau. C’est
sympa, mais vous risquez d’être rapidement spammé. Nous allons donc ajouter un node qui va supprimer les éléments qu’on a déjà vu par le passé. Dans
la barre de recherche, tapez “Remove Duplicates”. et sélectionnez le noeud portant le même nom, puis “Remove items processed in previous executions”.
La fenêtre de configuration s’ouvre alors. Ici, le champ qui nous intéresse, c’est le “Value to dedupe on”. Ce champ permet de définir quel élément de
notre flux RSS va être utilisé pour savoir si le contenu a déjà été vu par n8n ou non. Passez le champ en mode “Expression” (voir screen juste en
dessous), et tapez {{ $json.guid }}
(note : vous pouvez aussi glisser-déposer depuis l’élément JSON à gauche de la configuration).
Grâce à ce noeud, les noeuds suivants ne recevront que les éléments qui n’ont jamais été traités par n8n. Les autres seront supprimés de la liste (ce qui est très pratique pour ne recevoir que les nouveaux articles publiés). Et ainsi de suite, vous allez pouvoir ajouter d’autres noeuds, effectuer des transformations, etc.
Sachez que n8n propose de nombreuses autres fonctionnalités, notamment les sub-workflows, qui permettent de mutualiser des morceaux de workflows complets. On le verra par la suite, car nous allons nous en servir pour notre système de mise à jour automatique.
Comme vous pouvez le voir, les workflows peuvent vite grossir…
Maintenant que nous avons vu les bases de n8n, passons au vif du sujet : mettre à jour nos containers automatiquement.
Mettre à jour mes services automatiquement - plan d’attaque
Pour rappel, notre service à mettre à jour est déjà :
- Déployé en service Swarm ;
- Avec un Ansible ;
- Le tout démarré en CI/CD depuis GitLab + un runner.
Pour l’exemple, nous allons reprendre le service n8n déjà déployé (oui, n8n va se mettre à jour lui-même, et oui, ça va fonctionner). Le système est exactement le même pour tout vos autres services, donc n’hésitez pas à dupliquer l’exemple.
1. Modifier mon Ansible
Pour arriver à notre besoin, nous allons jouer avec le système de tags de GitLab, et les extra-vars d’Ansible. L’idée est simple : Lorsque n8n détecte une nouvelle version, il va créer un tag dans GitLab. Cela va alors déclencher une CI automatiquement, et communiquer le nom du tag créé à Ansible, qui va s’en servir pour déployer la nouvelle version du service en utilisant le nom du tag en version d’image Docker.
Reprenons notre Ansible, et variabilisons simplement la version du service :
```yaml
---
- name: "deploy n8n"
hosts: app_servers
vars:
data_location: "/srv/n8n" #Change with your value
tasks:
- name: "create dirs"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0777'
loop:
- "{{ data_location }}/data"
- "{{ data_location }}/files"
- name: "Create n8n service"
run_once: true
community.general.docker_swarm_service:
name: "n8n"
image: "docker.n8n.io/n8nio/n8n:{{ version }}" # <-- ICI !
mode: "replicated"
replicas: 1
mounts:
- source: "{{ data_location }}/data"
target: "/home/node/.n8n"
type: "bind"
- source: "{{ data_location }}/files"
target: "/files"
type: "bind"
publish:
- mode: "ingress"
protocol: "tcp"
published_port: 5678
target_port: 5678
rollback_config:
failure_action: "pause"
order: "stop-first"
update_config:
failure_action: "rollback"
order: "stop-first"
monitor: "1m"
restart_config:
condition: any
delay: 5s
max_attempts: 3
window: 120s
Maintenant, modifiez votre fichier .gitlab-ci.yml
pour apporter les modifications suivantes :
- Ajouter
--extra-vars "version=$CI_COMMIT_TAG"
dans la commande d’exécution du playbook Ansible ; - Le démarrer automatiquement lorsqu’un tag est créé.
Un exemple de fichier fonctionnel serait :
stages:
- deploy
deploy:
image: ansible:latest
stage: deploy
script:
- ansible-playbook --key-file /id_rsa --extra-vars "version=$CI_COMMIT_TAG" -i inventories/servers playbook.yml
when: always
only:
- tags
Notre Ansible (et GitLab) est désormais prêt pour la suite des opérations.
2. Créer mon bot Discord
Maintenant, nous allons avoir besoin d’un bot Discord. Allez sur https://discord.com/developers/applications et créez une nouvelle application. Donnez-lui le petit nom que vous souhaitez.
Dans l’onglet “OAuth2”, récupérez le Client ID de votre bot. Puis, allez sur la page suivante :
https://discord.com/oauth2/authorize?client_id=VOTRE_CLIENT_ID&scope=bot&permissions=377957247040
. Ce lien vous permet d’ajouter le bot avec l'
autorisation d’envoyer et de gérer les messages (ce qui nous sera très utile) à votre serveur.
3. Créer les credentials
Maintenant que nous avons tout ce qu’il nous faut, il est temps d’indiquer à n8n comment s’authentifier à notre GitLab, et à notre Bot Discord. Au lieu de créer un Workflow, créez un “Credential” (la petite flêche à droite de “Create Workflow” vous permet d’en créer un). Cherchez “Discord” dans la barre de recherche, et créez un Discord Bot Account. Indiquez le bot token (vous le trouverez dans la console développeurs de Discord, onglet " Bot", puis “Reset Token”).
Faites de même avec GitLab : Créez un Credentials de type “GitLab API”. Votre access_token
a simplement besoin du scope api
.
C’est bon, n8n a toute la plomberie requise. Maintenant, il est temps de créer notre workflow !
4. Récupérer les informations
Commençons par créer un nouveau workflow. Comme j’aime bien que mon workflow se lance en mode Cron, je pars sur un “Schedule Trigger”, qui se lance
toutes les nuits à 2h du matin (je vais pas vous montrer cette étape, on l’a déjà fait plus haut 😁). Puis, je vais récupérer les releases publiée sur
le dépôt GitHub de n8n. L’URL est : https://github.com/n8n-io/n8n/releases.atom
. Pour cela, utilisez simplement un noeud “RSS Feed”. Puis, je
supprime les duplicas avec un node “Remove Duplicates - Remove Items Processed In Previous Executions” (qu’on a déjà vu, souvenez vous), sur la clé
{{ $json.title }}
.
5. Parser les informations
Et là, c’est la tuile : n8n peut potentiellement renvoyer plusieurs version d’un coup ! Par exemple, un fix sur la v1.106.x qui est aussi backporté sur la v1.105.x. J’utilise donc un bloc de type “code”, qui me permet de vérifier le semantic-versioning pour ne garder que le plus récent. Le code est le suivant :
let newest = "0.0.0"
for (const item of $input.all()) {
let testClean = item.json.title.replaceAll("n8n@", "")
let cleanSplitted = testClean.split(".")
let newestSplitted = newest.split(".")
if (cleanSplitted[2].includes("-rc")) {
continue
}
if (parseInt(cleanSplitted[0]) > parseInt(newestSplitted[0])) {
newest = testClean
} else if (parseInt(cleanSplitted[0]) === parseInt(newestSplitted[0])) {
if (parseInt(cleanSplitted[1]) > parseInt(newestSplitted[1])) {
newest = testClean
} else if (parseInt(cleanSplitted[1]) === parseInt(newestSplitted[1])) {
if (parseInt(cleanSplitted[2]) > parseInt(newestSplitted[2])) {
newest = testClean
}
}
}
}
if (newest != "0.0.0") {
return {version: newest};
}
return []
Nous avons maintenant la version la plus récente à notre disposition. Il est temps de demander à l’administrateur (vous!) si vous souhaitez installer ou non cette version. Oui, on est en automatique, mais on est pas complètement fou non plus.
Juste avant, nous allons ajouter une étape “Edit Fields”, qui va nous permettre de ne conserver que quelques informations qui nous intéressent (et accessoirement, ça vous simplifiera la tâche si un jour vous passez à un système de sub-workflow).
Les champs que votre Edit Fields doit indiquer sont :
- Champs techniques :
version
: La valeur est un String valant{{ $json.version }}
de type Expressiongitlab_project_id
: La valeur est un number valant l’id de votre projet contenant l’Ansible sur Gitlab, de type Fixed- Note : Pour trouver votre ID de projet, allez sur votre projet GitLab, Settings -> General. Le projectID sera indiqué à côté du nom.
- Champs pour améliorer l’affichage dans Discord :
name
: La valeur est un string valantn8n
de type Fixedrepo_name
: La valeur est un string valantdocker.n8n.io/n8nio/n8n
de type Fixedchangelog_link
: La valeur est un string valanthttps://github.com/n8n-io/n8n/releases/tag/n8n%40{{ $json.version }}
de type * Expression*
Voici pour l’instant où en est votre workflow :
6. Demander son avis à l’admin
L’idée ici, c’est d’informer l’administrateur (= vous) qu’une mise à jour est disponible, et de vous laisser le choix de l’installer ou non. Ce système vous permet ainsi de garder le contrôle sur ce qui se passe sur votre infra. Cela évite, à tout hasard, de lancer une mise à jour breaking-change sans avoir fait de backup, ou de tomber un service pile au moment où vous en avez besoin…
Pour cela, nous allons créer un nouveau node “Discord” “Send message and wait for response”. Ce node va envoyer un message Discord qui va contenir deux boutons : un pour accepter, l’autre pour refuser. Selon l’action effectuée, le workflow continuera ou non. Configurez le noeud pour envoyer le message dans le chan désiré.
Vous pouvez utiliser le message suivant (en mode Expression) :
**{{ $json.name }} v{{ $json.version }} est disponible.**
🚀 [Notes de version]({{ $json.changelog_link }})
🐳 [{{ $json.repo_name}}:{{ $json.version }}](https://hub.docker.com/r/{{ $json.repo_name }}/tags)
Mettre à jour le serveur automatiquement ?
*(RequestID: {{ $execution.id }})*
Définissez le “Response Type” sur “Approval”, avec “Approve or Disapprove” en “Type of Approval”. Vous pouvez lancer l’exécution pour voir si le message est reçu ou pas.
Puis, créez un node de type “If”. La condition est la suivante :
{{ $json.data.approved }}
is true
(Boolean -> is true)
Vous pouvez ensuite envoyer un message d’annulation sur la branche “False”… Et sur la branche True, il est temps de connecter notre GitLab ! Actuellement, notre workflow a cette tête :
7. Pousser dans GitLab
Maintenant, il ne reste plus qu’à créer notre tag dans GitLab, et regarder le résultat de la pipeline. Je vais être un peu plus synthétique dans mes explications car normalement, vous commencez à comprendre la logique !
Sur la continuation de la branche “True” de notre condition, créons une “HTTP Request”. Voici le détail de la configuration :
- Method :
POST
- URL :
https://<YOUR_GITLAB_URL>/api/v4/projects/{{ $('Edit Fields').item.json.gitlab_project_id }}/repository/tags
- Authentication : “Predefined Credential Type”
- Credential Type : “Gitlab API”
- Gitlab API : “GitLab account” (ou le nom que vous avez donné à votre credential GitLab)
- Send query parameters : Oui
- Specify Query Parameters : Using Fields Below
- Query Parameters :
- Name :
tag_name
- Value :
{{ $('Edit Fields').item.json.version }}
- Value :
- Name:
ref
- Value : Votre branche principale sur le projet (sûrement
main
oumaster
)
- Value : Votre branche principale sur le projet (sûrement
- Name :
Ce noeud dispose de deux sorties : Success ou Error. En cas d’erreur, envoyez simplement un message Discord.
En cas de réussite, nous alons nous mettre à vérifier nos pipelines. Voici la démarche :
- On attend 2 minutes (noeud de type Wait)
- On vérifie le statut de la pipeline avec une requête HTTP (noeud de type HTTP Request)
- Method :
GET
- URL :
https://<YOUR_GITLAB_URL>/api/v4/projects/{{ $('Edit Fields').item.json.gitlab_project_id }}/pipelines
- Authentication : “Predefined Credential Type”
- Credential Type : “Gitlab API”
- Gitlab API : “GitLab account” (ou le nom que vous avez donné à votre credential GitLab)
- Method :
- On garde uniquement la première pipeline reçue (noeud de type Limit)
- On vérifie si le statut de cette pipeline est terminé ou non (noeud de type If). Les conditions sont les suivantes :
{{ $json.status }}
is equal to
success
OR{{ $json.status }}
is equal to
failed
OR{{ $json.status }}
is equal to
canceled
- Si pas terminé, on retourne au bloc “Wait” ; sinon on vérifie l’état (réussite ou non, noeud de type If) :
{{ $('Check pipeline status').item.json.status }}
is equal to
success
- Si oui, on envoie un message de réussite sur Discord, sinon un message d’échec.
Et voici la tête finale de notre pipeline (le JSON est donné sous la conclusion) :
Il ne vous reste plus qu’à activer votre Workflow (le petit “Inactive” en haut à droite à côté des boutons Share/Save), et vous avez désormais un n8n qui s’auto-update ! 🎉 Il ne vous reste plus qu’à recommencer avec vos autres services (vous verrez, ça va vachement plus vite).
Accessoirement, utilisez un Sub-Workflow ! Tout ce qui est après votre “Edit Fields” est en réalité générique à tout vos projets et services. Copiez l’ensemble de ces noeuds, et déplacez les dans un workflow à part, et utilisez un noeud “Execute Sub-workflow”. Ainsi, vous mutualisez une énorme partie de votre workflow, et vous vous simplifiez la maintenance.
Conclusion
Maintenant, vous savez utiliser n8n pour mettre à jour automatiquement votre infra. Avec les quelques conseils donnés ci-dessus, vous serez bientôt capable de mettre à jour l’ensemble de vos services conteneurisés de manière automatique. Bien entendu, il faut rester prudent en utilisant ce genre d’outils : Méfiez-vous des breaking-changes, vérifiez bien que l’image Docker existe, et de votre politique d’update et de rollback de votre orchestrateur. Bref, la confiance dans l’outil n’exclut pas le contrôle !
En améliorations, il est possible d’envisager plusieurs pistes :
- Utiliser une IA pour détecter les breaking changes dans le changelog et informer l’administrateur avant le deploy ;
- Ajouter des noeuds pour lancer une backups ;
- Vérifier le statut du service directement sur l’orchestrateur (là, on utilise juste le statut de la pipeline, ce qui est potentiellement léger comme check…) ;
- Et bien d’autres choses.
A la maison, cet outil me sert désormais quotidiennement pour mettre à jour n8n, mon serveur mail, mon VaultWarden, mon Nextcloud, GitLab, Grafana, et bien d’autres services. C’est un énorme gain de temps et d’énergie car vous n’avez plus qu’un clic à effectuer dans Discord. Ainsi, même en vacances, les fix de sécurité urgents n’ont plus à attendre !
L’idée de ce post était également de votre présenter l’énorme avantage et l’intérêt d’un outil comme n8n. Veille technologique, mise à jour d’infras en temps réel, synthétisations de documents à la demande… Les possibilités sont quasiment infinies, alors lancez-vous !
Le dump JSON du workflow complet est disponible ici.