Ce blog a été reconfiguré sous Hugo (https://gohugo.io/), un générateur statique de pages HTML à partir de pages markdown, extrêmement souple et rapide (quelques millisecondes pour générer un grand nombre de pages).

Voici la présentation d’une solution possible (parmi plusieurs autres alternatives) pour obtenir une chaîne de publication complète (depuis les sources jusqu’à la publication “en prod”), aussi automatisée que possible et nécessitant aussi peu d’interventions manuelles que possible (quasiment aucune, au final) pour toute mise à jour.

Introduction

Hugo est donc utilisé ici pour transformer les pages .md (markdown) en pages .html et ressources associées.

Plusieur solutions sont envisageables pour publier le résultat (liste non-exhaustive) :

  1. générer les pages .html en local (avec le binaire hugo) et publier le répertoire final en ligne au sein d’un serveur web sur le serveur de production
  2. idem mais en hébergeant les pages générées sous GitHub
  3. stocker le répertoire source du projet sous GiT, le push-er sur un repository hébergé sur le serveur de production et automatiser le déclenchement de la génération des pages .html sur ce dernier, pages qui seront ensuite publiées via un serveur web
  4. idem mais en utilisant le binaire hugo en mode “server” directement sur le serveur de production (Hugo réalisera à la fois la génération des pages finales et leur publication)
  5. utiliser le serveur web Caddy (https://caddyserver.com/) en activant le plugin GiT : c’est alors Caddy qui va checker les modifications sur le GiT origin et déclencher le lancement du binaire hugo

Ce billet va présenter la solution 3., solution qui offre plusieurs avantages :

Chaîne de publication automatisée d'un blog sous Hugo /media/images/hugo-publication-chain.png
  • une seule opération à exécuter : pousser le répertoire source (qui peut très bien être cloné sur un laptop afin d’être mis à jour, etc.) vers le serveur de production - sur ce dernier, le stockage se fera bien sûr au sein d’un repository GiT distant et dédié ;
  • les mises à jour sont répercutées particulièrement rapidement ;
  • l’ensemble est relativement facile à mettre en place (au prérequis près qu’il faut un daemon docker) ;
  • la chaîne est fiable : chaque opération ne réalise qu’une seule opération unitaire, sans frioritures - ainsi, en cas d’erreur lors du commit, ou lors de la génération des .html depuis les .md, le site publié n’est jamais arrêté (et reste donc en ligne sans interruptions) ;
  • à l’identique, la chaîne peut être mise à jour en temps masqué, ici aussi afin de minimiser les perturbations sur le site publié.

Etapes

Sur le serveur distant, créer un nouveau repository GIT, dans un répertoire dont le nom se termine, par convention, par .git.

mkdir -p /datas/git/blog.git/
cd /datas/git/blog.git/
git init --bare

Sur le client, dans le projet git courant, ajouter le repository distant qui vient d’être créé :

git remote add live root@tensin.org:/datas/git/blog.git

Ensuite, toujours sur le client, il suffira de pusher les mises à jour soit sur le repository habituel (origin master), soit sur le repository de publication live :

git push -u live

Pour mettre à jour en automatique les pages HTML, il va nous falloir ensuite trois choses :

  • un mécanisme qui va faire, lors du push de nouvelles données, un checkout du repository GIT distant dans un répertoire temporaire : ce sera un webhook git (étape 3 du schéma) ;
  • un mécanisme qui va créer les pages HTML à partir des sources .md du projet : ce sera un premier container docker, constamment démarré, avec le binaire hugo en mode watch (détection des modifications et republish des .html correspondants) (étape 4 du schéma) ;
  • un mécanisme qui va publier les pages HTML sur une URL : ce sera un deuxième container docker embarquant un serveur web (apache, nginx, etc. - dans mon cas, j’utilise caddy) (étape 5 du schéma)

Ajout du webhook, sur le serveur distant, qui sera déclenché après chaque commit, en créant un fichier sous hooks/post-receive au sein du dépôt GIT (dans mon cas, dans /datas/git/blog.git/).

Ce fichier sera exécuté à chaque push réalisé avec succès : il va se contenter de faire un checkout / mise à jour des données à publier, depuis le repository git du serveur, vers un espace temporaire (toujours sur le serveur).

#!/bin/bash
cd /datas/blog/ && env -i git pull

Le container docker embarquant hugo en mode watch sera chargé de monitorer ce répertoire et, à la moindre modification, de régénérer les pages .html correspondantes (dans un répertoire de publication).

# Clonage initial
mkdir /datas/blog/
git clone /datas/git/blog.git/ /datas/blog/

# Démarrage du container (une fois créé)
docker run -d -v /datas/blog/:/data/input/ -v /datas/www/:/data/output/ hugo-build

Dockerfile correspondant :

FROM    alpine

ENTRYPOINT ["hugo"]
CMD     ["--watch", "-s", "/data/input/", "-d", "/data/output/", "--cleanDestinationDir"]
VOLUME  ["/data/input", "/data/output/"]
WORKDIR /data/

ENV     PATH $PATH:/opt/hugo/
ENV     HUGO_PACKAGE_URL        Linux-64bit
ENV     HUGO_PACKAGE_NAME       linux_amd64
ENV     HUGO_VERSION            0.18.1

RUN     apk --update add git curl && \
        mkdir -p /data/ /opt/ && \
        curl -sSL https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_${HUGO_PACKAGE_URL}.tar.gz | tar xzf - -C /opt/ && \
        mv /opt/hugo_${HUGO_VERSION}_${HUGO_PACKAGE_NAME} /opt/hugo && \
        mv /opt/hugo/hugo_${HUGO_VERSION}_${HUGO_PACKAGE_NAME} /opt/hugo/hugo && \
        rm -rf /opt/hugo_*gz

Enfin, un container sera chargé de publier les pages HTML, en se mappant simplement sur le répertoire de publication alimenté par le container précédent. Par exemple :

FROM    alpine

VOLUME  ["/data"]
WORKDIR /data
EXPOSE  80 443 2015

CMD     ["/opt/caddy/caddy", "--conf", "/etc/caddy.conf"]

ADD     caddy.conf /etc/caddy.conf

RUN     apk add --update curl git tar && \
        mkdir -p /opt/caddy/ && \
        curl --silent --show-error --fail --location \
        --header "Accept: application/tar+gzip, application/x-gzip, application/octet-stream" -o - \
        "https://caddyserver.com/download/build?os=linux&arch=amd64&features=filemanager%2Cminify" \
        | tar --no-same-owner -C /opt/caddy/ -xz caddy \
        && chmod a+x,a+r /opt/caddy/caddy \
        && /opt/caddy/caddy -version

Qui sera quant à lui démarré via :

docker run -d -v /datas/blog/:/data/input/ -v /datas/www/:/data/output/ --name hugo-publish hugo-publish

Avec une configuration par exemple aussi simple que :

:2015
root /data/
gzip
log /var/log/caddy/access.log

Ces deux containers n’ont jamais besoin d’être arrêtés ni redémarrés ! (en usage courant, i.e. sauf montée de version du binaire hugo)

Les répertoires utilisés sur le serveur de production sont donc pour rappel :

Répertoire                    Usage
/datas/git/blog.git Le repository GiT distant vers lequel on poussera les mises-à-jour via git push
/datas/blog/ La zone temporaire dans laquelle sera extraite le repository GiT distant via le hoot sur les commits git, zone qui sera monitorée par le premier container docker (chargé de transformer les .md en .html)
/datas/www/ La zone de publication des pages générées par hugo (depuis le premier container), zone qui sera montée au sein du serveur web pour publication en ligne des .html

La publication / mise à jour est extrêmement rapide. Ci-dessous un push + déclenchemnt du hook (ce sont les lignes commençant par remote:). La durée du push comprendra donc le temps de push, le temps du checkout “local”, et le temps de transformations des pages markdown via hugo.

0:03 root@jupiter /home/downloads/downloaded/workspaces/blog# git push live
Décompte des objets: 5, fait.
Delta compression using up to 4 threads.
Compression des objets: 100% (5/5), fait.
Écriture des objets: 100% (5/5), 418 bytes | 0 bytes/s, fait.
Total 5 (delta 4), reused 0 (delta 0)
remote: From /datas/git/blog
remote:    6775c32..97d2eed  master     -> origin/master
remote: Updating 6775c32..97d2eed
remote: Fast-forward
remote:  content/drafts/hugo.md | 2 +-
remote:  1 file changed, 1 insertion(+), 1 deletion(-)
To tensin.org:/datas/git/blog.git
   6775c32..97d2eed  master -> master

Références et liens complémentaires