Déployer Hugo via Gitea et Drone-CI

  1. Pourquoi faire simple quand on peut faire compliqué ?
  2. Composant logiciels
    1. Gitea
    2. Hugo
    3. Drone-CI
    4. Runners
  3. Performances
  4. Conclusion

Pourquoi faire simple quand on peut faire compliqué ?

De prime-abord, on pourrait se dire : "si je veux un blog, je lance un WordPress comme la moitié du web et je commence à publier en deux minutes". Mais quand on est un vieux routard du web, on a des aspirations et des principes un peu plus élitistes :

  • WordPress ne génère pas de sites statiques (nativement)
  • Le fait qu'il soit utilisé par la moitié d'Internet me révèle que c'est une cible facile pour des personnes ou des robots mal-intentionnés
  • PHP est indispensable pour faire fonctionner WordPress, même si WordPress est configuré pour générer des pages pseudo-statiques
  • WordPress est une usine à gaz, qui sort du HTML, du CSS et du Javascript pas optimisés
  • Le code de WordPress est abominablement dégueulasse, impossible de faire quoique ce soit de propre, ni même de léger
  • WordPress est impersonnel, ce que je trouve embêtant pour un blog (et encore plus pour un site corporate d'ailleurs) : il doit être le reflet de son auteur ; s'il ressemble à tous les autres, quel intérêt ? (et je vous le dis : ce n'est pas simplement parce que vous customisez un thème qu'on ne voit pas que vous utilisez WordPress...)
  • WordPress a besoin d'une base de données
  • L'écrasante majorité des extensions tierces un tant soit peu intéressantes sont payantes ou sur l'immonde modèle du premium

Un générateur de sites statiques comme Hugo me permet de :

  • Séparer l'apparence, le contenu, et l'outil qui construit le site final (je peux utiliser autre chose qu'Hugo si j'en ai envie, moyennant quelques ajustement mineurs). Impossible de faire ça avec WordPress
  • Publier un site 100% sécurisé : sans PHP, sans formulaires, sans Javascript, le maillon faible de la sécurité de mon site n'est pas mon site
  • Avoir un suivi de mes modifications (grâce à Git)
  • Simplifier les processus de sauvegarde et de restauration (en gros, avoir un plan de relance en béton armé en cas de défaillance technique - je peux même faire héberger temporairement mon site ailleurs sans avoir besoin d'aucune autre dépendance qu'un serveur web, impossible de faire ça avec WordPress en un minimum de temps)
  • Être réellement libre du point de vue logiciel

L'utilisation de git dans ce processus est totalement facultative : il est tout à fait possible de construire son site avec Hugo sans tout ce que je vais présenter dans cet article. Néanmoins, cette procédure qui semble fastidieuse de prime-abord est en réalité très puissante et très confortable au quotidien :

  • Je créé ou modifie mes articles sous la forme de fichiers Markdown - pratiquement du texte brut
  • Je les publie sur ma forge Gitea
  • Drone-CI fait le reste : construire le site en lançant l'exécutable Hugo et envoyer les fichiers HTML et CSS au serveur de production

Pour peu qu'on choisisse un thème existant plutôt qu'en créer un, on peut réellement ne se préoccuper que du contenu de son site.

Composant logiciels

Tout passe par des containers (docker, évidemment, mais l'utilisation de podman est possible). Je présume donc que docker est installé et fonctionnel.

Gitea

Gitea est une forge logicielle qui s'appuie sur le système de gestion de code Git. J'ai choisi Gitea pour sa sobriété, sa légèreté et sa simplicité, mais il existe d'autres forges logicielles : GitHub qui appartient à Microsoft et qu'il n'est pas possible d'auto-héberger, ou Gitlab pour ne citer que ces deux-là.

docker-compose.yml :

version: "3"
services:
    gitea:
        image: gitea/gitea:latest
        restart: always
        ports:
            - "${HTTP_PORT}:3000"
            - "${SSH_PORT}:22"
        volumes:
            - ${DATA_DIR}:/data
            - /etc/timezone:/etc/timezone:ro
            - /etc/localtime:/etc/localtime:ro

Remplacez les variables en fonction de vos besoins :

  • HTTP_PORT : le port sur lequel les requêtes HTTP arrivent sur votre serveur (pas le port HTTP du container)
  • SSH_PORT : le port du serveur SSH du serveur (pas le port SSH du container)
  • DATA_DIR : le chemin complet du dossier dans lequel stocker les données de Gitea, ou le volume si vous préférez utiliser un volume

Hugo

Hugo est un générateur de sites statiques. C'est ce que j'utilise pour créer et maintenir mon blog. Il en existe d'autres, tels que Jekyll. J'ai choisi Hugo pour sa légèreté.

L'essentiel du travail avec Hugo se fait sur votre machine locale. Il n'y a rien à installer côté serveur. La construction du site se fera via Drone-CI, dans lequel on va configurer un pipeline pour compiler les ressources CSS (et éventuellement Javascript), et créer l'ensemble du site statique dans un container docker. Tous ces fichiers seront ensuite envoyé au serveur web via rsync, scp, ou ce que vous voulez.

Pour faire tout cela, il "suffit" d'ajouter un fichier .drone.yml à la racine de votre projet Hugo avec le contenu suivant :

kind: pipeline
type: docker
name: default

steps:

  - name: submodules
    image: alpine/git
    commands:
    - git submodule update --init --recursive

  - name: build
    image: klakegg/hugo:ext-ci
    commands:
      - cd themes/blog_theme && npm i && cd -
      - hugo --gc --minify --environment production

  - name: deploy
    image: appleboy/drone-scp
    settings:
      host: "10.0.2.1"
      target: /mnt/volume1/shares/www/richard-dern.fr/www/
      source: public/*
      username:
        from_secret: ssh_user
      password:
        from_secret: ssh_password

trigger:
  branch:
    - master

L'étape submodules (entre les lignes 7 et 10) n'est nécessaire que si le thème que vous utilisez est dans un submodule git dans le répertoire theme de votre projet Hugo. Si le thème est directement dans l'arborescence de votre projet, vous pouvez supprimer les lignes concernées.

Le thème que j'ai créé utilise le framework Tailwind CSS, j'ai donc besoin de compiler les ressources via npm, ce que je fais à la ligne 15, juste avant de construire toutes les pages du site. Jusqu'à présent, c'est plutôt simple, non ?

Ensuite, l'étape la plus compliquée (deploy). Il faut créer des secrets dans Drone-CI (ici, ssh_user et ssh_password), mais nous verrons cela un peu plus bas. Pensez simplement à modifier les valeurs de host et target : l'adresse IP du serveur qui va héberge Hugo et le chemin où les fichiers générés par Hugo seront envoyés.

Ceci dit, ici j'utilise scp, mais d'autres services sont disponibles. Consultez la liste des plugins Drone-CI pour utiliser celui qui convient à votre hébergement.

Enfin, on a ajouté la directive trigger pour que tout ce processus de publication ne soit déclenché que lorsque la branche git master est modifiée, l'idée étant de protéger la branche master en écriture, modifier le contenu dans une nouvelle branche, fusionner avec master une fois terminé, mais là on part dans des considérations philosophiques devops :smile:

Drone-CI

Drone-CI est une application d'intégration continue : c'est le genre d'applications qui s'intercalent entre la forge logicielle et les serveurs de mise en production, une position privilégiée qui confère à ces outils beaucoup de puissance et un intérêt considérable. Tests unitaires, construction de ressources, publication automatique, les cas d'usage sont nombreux.

La documentation de Drone-CI préconise de l'installer sur une machine physique différente du serveur Gitea afin d'éviter les problèmes et de simplifier la configuration. C'est l'option que j'ai choisi, mais en pratique, si vous savez ce que vous faites, vous ne devriez pas avoir de soucis.

Il faut toutefois prendre en considération les ressources du (des) serveurs. Si vous utilisez des Raspberry Pi par exemple, il vaut peut-être mieux lancer Drone-CI sur un Pi dédié à cet usage.

Pour que Drone-CI puisse communiquer avec Gitea, il faut aller dans Gitea, votre profil, puis dans l'onglet Applications. Là, créez une application OAuth. Mettez ce que vous voulez comme nom ("drone", par exemple), et l'URL de redirection, qui correspond à l'URL complète à laquelle vous accèderez à Drone-CI, suffixée par /login. Dans mon cas, l'URL de redirection est :

https://ci.athaliasoft.com/login

Validez, et Gitea vous communiquera un certain nombre d'informations à renseigner dans le fichier docker-compose.yml pour Drone-CI, notamment un Client ID et un Client Secret, à remplacer ci-dessous.

docker-compose.yml :

version: "3"
services:
    drone:
        image: drone/drone
        restart: always
        ports:
            - "${PORT}:80"
        environment:
            - DRONE_GITEA_SERVER=${DRONE_GITEA_SERVER}
            - DRONE_GITEA_CLIENT_ID=${DRONE_GITEA_CLIENT_ID}
            - DRONE_GITEA_CLIENT_SECRET=${DRONE_GITEA_CLIENT_SECRET}
            - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
            - DRONE_SERVER_HOST=${DRONE_SERVER_HOST}
            - DRONE_SERVER_PROTO=https
        volumes:
            - ${VOLUME_ROOT}:/data
  • DRONE_GITEA_SERVER correspond à l'URL de votre serveur Gitea
  • DRONE_RPC_SECRET est une clé à générer via la commande openssl rand -hex 16 (par exemple)
  • DRONE_SERVER_HOST est le nom d'hôte correspondant à l'URL de redirection (donc, dans mon cas spécifique, drone.git.dern.ovh)
  • VOLUME_ROOT, un volume docker ou un répertoire dans lequel seront stockées les données de Drone-CI

Runners

En plus de Drone-CI, il faut configurer des runners, c'est-à-dire un ou plusieurs services qui vont exécuter les pipelines configurés. Comme je dispose de plusieurs serveurs, j'ai installé un runner par serveur, ce qui permet d'améliorer les performances générales de Drone-CI, mais il est tout à fait possible (et viable) d'avoir un seul runner installé sur la même machine que Drone-CI.

docker-compose.yml :

version: "3"
services:
    runner:
        image: drone/drone-runner-docker
        restart: always
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        ports:
            - "3000:3000"
        environment:
            - DRONE_RPC_PROTO=http
            - DRONE_RPC_HOST=drone
            - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
            - DRONE_RUNNER_CAPACITY=${DRONE_RUNNER_CAPACITY}
            - DRONE_RUNNER_NAME=${DRONE_RUNNER_NAME}

Pensez à modifier les variables suivantes pour convenir à votre environnement :

  • DRONE_RPC_SECRET, la même clé que celle définie plus haut pour Drone-CI
  • DRONE_RUNNER_CAPACITY, le nombre de pipelines que le runner peut exécuter en même temps (1 est suffisant pour des environnements modestes, 2 ou plus pour des environnements plus musclés)
  • DRONE_RUNNER_NAME, un nom à attribuer au runner pour savoir quel runner a exécuté quoi et quand

Performances

En voyant tout ça, on pourrait se dire que c'est consommateur de ressources. En réalité, tout cela est très, très léger, et très performant. Il faut moins de 30 secondes pour déployer en production un nouvel article ; une durée qui serait beaucoup - beaucoup - plus courte s'il n'y avait pas l'étape de compilation CSS avec npm.

La possibilité de tirer partie d'un nombre indéterminé de runners est un gage de scalabilité, même si les performances sont déjà élevées avec un seul serveur, grâce à la sobriété des applications mises en oeuvre.

J'aime ce qui est rapide et performant, même si cela nécessite un peu de temps pour être mis en oeuvre. Le processus expliqué ici respecte mon cahier des charges.

Conclusion

J'ai présenté ici le déploiement automatisé d'un site statique créé avec Hugo, puisant dans un serveur Gitea, en construisant le site via Drone-CI avant de copier les fichiers sur le serveur de production. C'est un cas d'usage parmi d'autres : je me sers également de cette configuration pour créer automatiquement des releases pour certaines de mes librairies, par exemple.

Le simple fait de parcourir la liste des plugins de Drone-CI donne un aperçu de ce qu'il est possible de faire out-of-the-box, mais en réalité, il n'y a pas vraiment de limite dans la mesure où on peut facilement créer ses propres plugins, puisque ce ne sont que des containers docker !