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 :

  1. On déploie nos nouveaux services tout beaux, tout neufs, sur notre orchestrateur favori ;
  2. On oublie ce service pendant 8 mois ;
  3. Le SOC de votre boîte (ou vous même avec votre veille) trouve une vulnérabilité ;
  4. Vous devez mettre à jour en urgence le service en question, sauf qu’il y a 5 versions d’écart ;
  5. 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 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à :

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 :

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 :

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 :

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 :

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 :

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 :

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.