Présentation de ma méthode de publication.

Pour mon blog j’ai choisi le générateur de site statique Hugo1. Contrairement au site dynamique comme Wordpress les générateurs de site statique sont très simples d’utilisation: il n’y a pas d’interface d’administration, pas de base de données à gérer et pas besoin d’un éditeur spécialisé car je rédige mes articles en Markdown.

Le principe est le suivant, je travaille en local sur mon ordinateur et je diffuse les articles sur mon serveur auto-hebergé. Pour suivre les évolutions de mon blog, j’utilise donc le gestionnaire de version Git et pour conserver une copie à distance j’utilise le service Gitlab sur framagit.org.

Après la rédaction de mon article, comment mon blog est mis à jour automatiquement sur mon serveur auto-hebergé ?

Ma première solution fût d’utiliser Ansible pour le déploiement des articles sur mon blog. Les fichiers de configuration me permettaient de réaliser un bon fonctionnent mais cela ne me satisfait pas pleinement. En effet pour faire les mises à jour du blog il faut obligatoirement une installation d’Ansible, ce qui n’est pas toujours possible notamment sur mon smartphone.

Ma solution actuelle repose sur le service CI de Gitlab : on crée un fichier appelé gitlab-ci.yml, situé à la racine du référentiel Git, qui définit un ensemble d’actions (ou pipeline de déploiement) à réaliser dans un ordre chronologique.

Le pipeline de déploiement gitlab-ci.yml :

stages:
  - build
  - deploy
build:
  stage: build
  image: alpine:latest
  tags:
    - private
  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/
  script:
  - apk update && apk add hugo git
  - git submodule update --init --recursive
  - hugo -d public
  artifacts:
    paths:
    - public
    expire_in: 10 mins
deploy:
  stage: deploy
  image: alpine:latest
  tags:
    - private
  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/
  before_script:
    - apk update && apk add openssh-client bash rsync
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - rsync -rz --delete public/ $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH

Le pipeline de déploiement
Il est constitué de deux étapes :

  1. build : permet de construire le dossier public qui contient la structure et les fichiers du blog traduits du Markdown en html/css. Le dossier public est ensuite mis en cache pour être utilisé par l’étape suivante.
  2. deploy : permet de copier avec Rsync ce dossier public sur mon serveur auto-hébergé.

Ce pipeline est conteneurisé dans Gitlab.

Le choix de l’image docker
Si possible je choisis de préférence l’image Alpine, elle est légère ce qui me permet de réduire le temps d’exécution du pipeline. J’ai comparé avec une image Debian, je suis passé de 5min34s à 2min11s avec l’image Alpine.

  image: alpine:latest

Les conditions à respecter
Le pipeline est exécuté si et seulement si la branche est master et le message du commit contient le mot clé deploy

  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/

La gestion du thème du blog
Le theme que j’utilise pour le blog s’appelle Nederburg2, le code source est disponible sur Gitub.
Avec le générateur de site statique Hugo il y a un répertoire nommé theme. C’est dans ce répertoire que j’importe le thème Nederburg avec la commande git submodule :

git submodule add https://github.com/appernetic/hugo-nederburg-theme.git

Le theme est un submodule, c’est juste l’adresse URL qui est définie. C’est pourquoi j’utilise la commande suivante dans le script gitlab-ci.yml pour copier le code du thème.

  script:
    git submodule update --init --recursive

La génération des clés SSH
Afin que le service Gitlab CI déploie en toute sécurité le dossier public sur mon serveur auto-hébergé, la création d’une paire de clés SSH est nécessaire.
Je crée une paire de clés SSH ed25519 qui est une implémentation de la Courbe d’Edwards tordue. Elle propose le même niveau de sécurité que RSA tout en consommant moins de ressources CPU.
Les clés sont générées sans utiliser de passphrase.

ssh-keygen -f ~/.ssh/gitlabci -t ed25519  
cat ~/.ssh/gitlabci.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Par sécurité, la clé privée n’est pas écrite dans le fichier de déploiement gitlab-ci.yml. J’utilise une variable d’environnement, ce qui signifie que la clé privée est écrite dynamiquement, lorsque le pipeline de déploiement est en cours d’exécution.

Je définis mes variables d’environnement dans les options de mon compte Framagit.

Voir l’image ci-dessous :
ssh-variables

  • SSH_KNOWN_HOSTS : La clé d’hôte. Permet de vérifier que le client est connecté au bon hôte. Je l’obtiens avec la commande suivante : ssh-keyscan mydomainname.
  • SSH_PRIVATE_KEY: La clé privée sans passphrase.

Mise en place d’une instance GitLab Runner

  tags:
    - private

Framagit met à disposition un GitLab Runnner pour exécuter un pipeline de déploiement. Pour des raisons de sécurité, j’exécute GitLab Runner depuis mon serveur auto-hebergé.

La commande ci-dessous génère le fichier de configuration : /srv/gitlab-runner/config/config.toml avec l’adresse URL du serveur framagit, le token d’authenfication et d’autres options.

docker run --rm -v /home/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
  --non-interactive \
  --executor "docker" \
  --docker-image alpine:latest \
  --url "https://*******" \
  --registration-token "******" \
  --description "private-runner" \
  --tag-list "private" \
  --run-untagged="false" \
  --locked="true" \
  --access-level="ref_protected"

Ensuite je démare l’instance GitLab Runner :

docker run -d --name gitlab-runner --restart always -v /home/gitlab-runner/config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock  gitlab/gitlab-runner:latest


private-gitlab-runner

Mise en place du serveur web
Pour servir les pages statiques du blog j’utilise le serveur web Nginx. Le service écoute sur le port 80, mais j’ai plusieurs autres services web avec le port 80 ouvert, c’est pourquoi j’utilise le reverse-proxy Traefix. De plus Traefix génère automatiquement les certificats TLS.
Voici ci-dessous le fichier docker-compose.yml

---
version: '3.7'
services:
  traefik:
    container_name: traefik
    restart: always
    image: traefik:v1.7.16
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /home/traefik/traefik.toml:/traefik.toml
      - /home/traefik/acme.json:/acme.json
      - /home/traefik/log:/var/log/traefik
    labels:
      - "traefik.docker.network=traefiknet"
      - "traefik.enable=true"
    networks:
      - traefiknet

  blog_web:
    container_name: blog_web
    image: nginx:stable-alpine
    restart: always
    volumes:
      - /home/blog/output:/usr/share/nginx/html:ro
    labels:
      - "traefik.docker.network=traefiknet"
      - "traefik.frontend.rule=Host:blog.la-forge.ml"
      - "traefik.enable=true"
      - "traefik.port=80"
    networks:
      - internal
      - traefiknet
      
networks:
  traefiknet:
    external: true
  internal:
    external: false

Cas pratique de publication
La procédure est simple (c’est en effet l’objectif !).

  1. Je rédige l’article en Markdown.
  2. La rédaction de l’article terminée, je fais un git commit et j’ajoute dans le message de commit le mot clé deploy
  3. Je fais un git push. Le pipeline de publication du blog est automatiquement déclenché.
  4. J’attends deux minutes, le temps que le pipeline se termine

Et voilà, le nouvel article est visible sur le blog

pipeline-de-deploiement